DMA module reference

Header :
#include <dma.h>
Makefile : dma is already included by default

Description

This module is meant to be used internally by the library's modules, not by the main program. However, it's still interesting to know what a DMA controller is, and how it works.

The DMA (Direct Memory Access) controller is a peripheral dedicated to moving data between RAM and peripherals. This is useful to automatically send or receive a buffer of data to or from a peripheral, without CPU intervention, kind of like a background task.

Indeed, most communication peripherals (such as USART, I2C or SPI) work one byte at a time :

  • when sending data, you have to copy a byte from RAM, wait for it to be sent, then copy the next byte, and so on
  • when receiving data, you have to set up an interrupt triggered when a byte is received, copy it to a buffer in RAM, then continue normal operation while waiting for the next byte
This is neither reliable nor efficient. When sending, if you want to send a large amount of data on a slow bus, the CPU will waste a lot of time waiting for each byte to be sent. When receiving, an interrupt will be triggered for each byte (triggering an interrupt involves saving and restoring the CPU context, which is a relatively complex and slow process), considerably reducing the amount of CPU time spent on your main process. Moreover, if you fail to handle the interrupt quickly enough, you have the risk of losing data.

This is where the DMA comes in handy. It is internally connected to the receiving/sending registers and the "data received" and "data sent" signals in every peripheral, and is smart enough to copy a new byte every time the signal rises. All you need to do is set up a new DMA channel with a peripheral name, a transfer direction (RAM -> peripheral or peripheral -> RAM), and a buffer address and size allocated in RAM. Once started, it will wait and copy data every time the peripheral is ready, until the buffer is empty (when sending) or full (when receiving). You can check at any time if the transfer is finished using isFinished(), or set up an interrupt using enableInterrupt().

Make sure not to activate a sleep mode higher than SLEEP0 when a DMA channel is active, otherwise the RAM clock and Peripheral clocks will be disabled and the DMA will freeze in an undefined state. By default, Core::sleep() uses the SLEEP0 mode which is fine.

API

int newChannel(Device device, Size size, uint32_t address=0x00000000, uint16_t length=0, bool ring=false)
Create a new DMA channel. The peripheral and the transfer direction are deduced from device, see the Device enum in the header below for the list of available devices. size refer to the amount of bytes to be transfered at a time (it depends on the size of the register in the peripheral), and can be BYTE, HALFWORD (2 bytes) or WORD (4 bytes). address and length refer to the buffer in memory where data is copied to/from, which are null by default and usually specified in startChannel() or setupChannel(). If ring is enabled, the address will be reset as soon as the transfer is complete and another transfer will start with the same buffer.
void enableInterrupt(int channel, void (*handler)(), Interrupt interrupt=Interrupt::TRANSFER_FINISHED)
Register a handler to call when a given event occurs : RELOAD_EMPTY, TRANSFER_FINISHED or TRANSFER_ERROR.
void disableInterrupt(int channel)
Disable the interrupt.
void setupChannel(int channel, uint32_t address, uint16_t length)
Configure the buffer and transfer length for this channel. The transfer will be started by calling the startChannel() function below.
void startChannel(int channel)
Start the transfer on the specified channel. setupChannel() must have been called beforehand to configure the channel.
void startChannel(int channel, uint32_t address, uint16_t length)
Configure and immediately start a transfer on this channel. This function is only a helper which calls the setupChannel() and startChannel() functions above.
void reloadChannel(int channel, uint32_t address, uint16_t length)
Set the reload buffer of the specified channel. This new buffer will be used as soon as the current transfer is complete. This allows an efficient double-buffering process, for example for a peripheral -> RAM transfer :
  1. the transfer is started with startChannel with a buffer A
  2. a reload buffer B is set with reloadChannel
  3. when the transfer completes and buffer A is full, the DMA automatically switches to buffer B and calls the interrupt handler to process the data in buffer A
  4. even if more data is received when the handler is being executed, it will be stored in buffer B, without interfering with the processing of buffer A
  5. the handler only needs to use reloadChannel with the buffer A when it is done processing it
void stopChannel(int channel)
Stop the transfer on the specified channel.
int getCounter(int channel)
Get the current counter of the specified channel. This counter decrements from the buffer size to 0.
bool isFinished(int channel)
Return true if the current transfer is finished. This is equivalent to checking if the counter has reach 0.
bool isReloadEmpty(int channel)
Return true if the reload buffer is empty. If one was set with reloadChannel(), this means that the transfer has finished and the reload buffer is now in use. The channel can be reloaded again with reloadChannel().
void enableRing(int channel)
Enable the ring mode on the given channel. In ring mode, the reload buffer address is not cleared after being transfered to the active buffer address. This means the DMA will always be reloaded with the same buffer when it finishes a transfer.
void disableRing(int channel)
Disable the ring mode on the given channel.

Hacking

The DMA controller is pretty straightforward and doesn't allow much customization.

Code

Header

#ifndef _DMA_H_
#define _DMA_H_

#include <stdint.h>
#include "error.h"

// Direct Memory Access
// This module is able to automatically copy data between RAM and peripherals,
// without CPU intervention
namespace DMA {

    // Peripheral memory space base address
    const uint32_t BASE = 0x400A2000;
    const uint32_t CHANNEL_REG_SIZE = 0x40;

    // Registers addresses
    const uint32_t OFFSET_MAR =      0x000; // Memory Address Register
    const uint32_t OFFSET_PSR =      0x004; // Peripheral Select Register
    const uint32_t OFFSET_TCR =      0x008; // Transfer Counter Register
    const uint32_t OFFSET_MARR =     0x00C; // Memory Address Reload Register
    const uint32_t OFFSET_TCRR =     0x010; // Transfer Counter Reload Register
    const uint32_t OFFSET_CR =       0x014; // Control Register
    const uint32_t OFFSET_MR =       0x018; // Mode Register
    const uint32_t OFFSET_SR =       0x01C; // Status Register
    const uint32_t OFFSET_IER =      0x020; // Interrupt Enable Register
    const uint32_t OFFSET_IDR =      0x024; // Interrupt Disable Register
    const uint32_t OFFSET_IMR =      0x028; // Interrupt Mask Register
    const uint32_t OFFSET_ISR =      0x02C; // Interrupt Status Register

    // Subregisters
    const uint8_t MR_SIZE = 0;
    const uint8_t MR_ETRIG = 2;
    const uint8_t MR_RING = 3;
    const uint8_t CR_TEN = 0;
    const uint8_t CR_TDIS = 1;
    const uint8_t ISR_RCZ = 0;
    const uint8_t ISR_TRC = 1;
    const uint8_t ISR_TERR = 2;
    const uint8_t SR_TEN = 0;

    // Error codes
    const Error::Code ERR_NO_CHANNEL_AVAILABLE = 0x0001;
    const Error::Code ERR_CHANNEL_NOT_INITIALIZED = 0x0002;

    // Size constants
    enum class Size {
        BYTE,
        HALFWORD,
        WORD
    };

    // Interrupts
    const int N_INTERRUPTS = 3;
    enum class Interrupt {
        RELOAD_EMPTY,
        TRANSFER_FINISHED,
        TRANSFER_ERROR
    };

    // Device constants
    enum class Device {
        USART0_RX = 0,
        USART1_RX = 1,
        USART2_RX = 2,
        USART3_RX = 3,
        SPI_RX = 4,
        I2C0_M_RX = 5,
        I2C1_M_RX = 6,
        I2C2_M_RX = 7,
        I2C3_M_RX = 8,
        I2C0_S_RX = 9,
        I2C1_S_RX = 10,
        USART0_TX = 18,
        USART1_TX = 19,
        USART2_TX = 20,
        USART3_TX = 21,
        SPI_TX = 22,
        I2C0_M_TX = 23,
        I2C1_M_TX = 24,
        I2C2_M_TX = 25,
        I2C3_M_TX = 26,
        I2C0_S_TX = 27,
        I2C1_S_TX = 28,
        DAC = 35
    };

    struct ChannelConfig {
        bool started;
        bool interruptsEnabled;
    };

    const int N_CHANNELS_MAX = 16;

    // Module API
    int newChannel(Device device, Size size, uint32_t address=0x00000000, uint16_t length=0, bool ring=false);
    void enableInterrupt(int channel, void (*handler)(), Interrupt interrupt=Interrupt::TRANSFER_FINISHED);
    void disableInterrupt(int channel, Interrupt interrupt=Interrupt::TRANSFER_FINISHED);
    void setupChannel(int channel, uint32_t address, uint16_t length);
    void startChannel(int channel);
    void startChannel(int channel, uint32_t address, uint16_t length);
    void reloadChannel(int channel, uint32_t address, uint16_t length);
    void stopChannel(int channel);
    int getCounter(int channel);
    bool isEnabled(int channel);
    bool isFinished(int channel);
    bool isReloadEmpty(int channel);
    void enableRing(int channel);
    void disableRing(int channel);

}

#endif

Module

#include "dma.h"
#include "core.h"
#include "error.h"
#include "pm.h"

namespace DMA {

    // Current number of channels
    int _nChannels = 0;

    ChannelConfig _channels[N_CHANNELS_MAX];

    // Interrupt handlers
    extern uint8_t INTERRUPT_PRIORITY;
    uint32_t _interruptHandlers[N_CHANNELS_MAX][N_INTERRUPTS];
    const int _interruptBits[N_INTERRUPTS] = {ISR_RCZ, ISR_TRC, ISR_TERR};
    void interruptHandlerWrapper();

    int newChannel(Device device, Size size, uint32_t address, uint16_t length, bool ring) {
        // Check that there is an available channel
        if (_nChannels == N_CHANNELS_MAX) {
            Error::happened(Error::Module::DMA, ERR_NO_CHANNEL_AVAILABLE, Error::Severity::CRITICAL);
            return -1;
        }

        // Channel number : take the last channel
        int n = _nChannels;
        const uint32_t REG_BASE = BASE + n * CHANNEL_REG_SIZE;

        // Make sure the clock for the PDCA (Peripheral DMA Controller) is enabled
        PM::enablePeripheralClock(PM::CLK_DMA);

        // Set up the channel
        (*(volatile uint32_t*)(REG_BASE + OFFSET_PSR)) = static_cast<int>(device);                  // Peripheral select
        (*(volatile uint32_t*)(REG_BASE + OFFSET_MAR)) = address;                                   // Buffer memory address
        (*(volatile uint32_t*)(REG_BASE + OFFSET_TCR)) = length;                                    // Buffer length
        (*(volatile uint32_t*)(REG_BASE + OFFSET_MARR)) = 0;                                        // Buffer memory address (reload value)
        (*(volatile uint32_t*)(REG_BASE + OFFSET_TCRR)) = 0;                                        // Buffer length (reload value)
        (*(volatile uint32_t*)(REG_BASE + OFFSET_MR)) = (static_cast<int>(size) & 0b11) << MR_SIZE; // Buffer unit size (byte, half-word or word)
        _channels[n].started = false;
        _channels[n].interruptsEnabled = false;

        // Enable the ring buffer
        if (ring) {
            (*(volatile uint32_t*)(REG_BASE + OFFSET_MARR)) = address;
            (*(volatile uint32_t*)(REG_BASE + OFFSET_TCRR)) = length;
            (*(volatile uint32_t*)(REG_BASE + OFFSET_MR)) |= 1 << MR_RING;
        }

        // Enable transfer
        (*(volatile uint32_t*)(REG_BASE + OFFSET_CR)) = 1;

        _nChannels++;

        return n;
    }

    void enableInterrupt(int channel, void (*handler)(), Interrupt interrupt) {
        // Save the user handler
        _interruptHandlers[channel][static_cast<int>(interrupt)] = (uint32_t)handler;

        // IER (Interrupt Enable Register) : enable the requested interrupt
        (*(volatile uint32_t*)(BASE + channel * CHANNEL_REG_SIZE + OFFSET_IER))
                = 1 << _interruptBits[static_cast<int>(interrupt)];

        // Enable the interrupt in the NVIC
        Core::Interrupt interruptChannel = static_cast<Core::Interrupt>(static_cast<int>(Core::Interrupt::DMA0) + channel);
        Core::setInterruptHandler(interruptChannel, interruptHandlerWrapper);
        Core::enableInterrupt(interruptChannel, INTERRUPT_PRIORITY);
        _channels[channel].interruptsEnabled = true;
    }

    void disableInterrupt(int channel, Interrupt interrupt) {
        // IDR (Interrupt Disable Register) : disable the requested interrupt
        (*(volatile uint32_t*)(BASE + channel * CHANNEL_REG_SIZE + OFFSET_IDR))
                = 1 << _interruptBits[static_cast<int>(interrupt)];

        // If no interrupt is enabled anymore, disable the channel interrupt at the Core level
        if ((*(volatile uint32_t*)(BASE + channel * CHANNEL_REG_SIZE + OFFSET_IMR)) == 0) {
            Core::disableInterrupt(static_cast<Core::Interrupt>(static_cast<int>(Core::Interrupt::DMA0) + channel));
            _channels[channel].interruptsEnabled = false;
        }
    }

    void setupChannel(int channel, uint32_t address, uint16_t length) {
        // Check that this channel exists
        if (channel >= _nChannels) {
            Error::happened(Error::Module::DMA, ERR_CHANNEL_NOT_INITIALIZED, Error::Severity::CRITICAL);
            return;
        }
        
        const uint32_t REG_BASE = BASE + channel * CHANNEL_REG_SIZE;

        // Empty TCR and disable the transfer
        (*(volatile uint32_t*)(REG_BASE + OFFSET_TCR)) = 0;
        (*(volatile uint32_t*)(REG_BASE + OFFSET_CR)) = 1 << CR_TDIS; // Disable transfer

        // Configure this channel
        (*(volatile uint32_t*)(REG_BASE + OFFSET_MAR)) = address;   // Buffer memory address
        (*(volatile uint32_t*)(REG_BASE + OFFSET_TCR)) = length;    // Buffer length
    }

    void startChannel(int channel) {
        // Check that this channel exists
        if (channel >= _nChannels) {
            Error::happened(Error::Module::DMA, ERR_CHANNEL_NOT_INITIALIZED, Error::Severity::CRITICAL);
            return;
        }

        // Enable this channel
        _channels[channel].started = true;
        const uint32_t REG_BASE = BASE + channel * CHANNEL_REG_SIZE;
        (*(volatile uint32_t*)(REG_BASE + OFFSET_CR)) = 1 << CR_TEN;    // Enable transfer

        // Reenable interrupts if necessary
        if (_channels[channel].interruptsEnabled) {
            Core::Interrupt interruptChannel = static_cast<Core::Interrupt>(static_cast<int>(Core::Interrupt::DMA0) + channel);
            Core::enableInterrupt(interruptChannel, INTERRUPT_PRIORITY);
        }
    }

    void startChannel(int channel, uint32_t address, uint16_t length) {
        setupChannel(channel, address, length);
        startChannel(channel);
    }

    void reloadChannel(int channel, uint32_t address, uint16_t length) {
        // Check that this channel exists
        if (channel >= _nChannels) {
            Error::happened(Error::Module::DMA, ERR_CHANNEL_NOT_INITIALIZED, Error::Severity::CRITICAL);
            return;
        }

        // Reload this channel
        const uint32_t REG_BASE = BASE + channel * CHANNEL_REG_SIZE;
        (*(volatile uint32_t*)(REG_BASE + OFFSET_MARR)) = address;  // Buffer memory address
        (*(volatile uint32_t*)(REG_BASE + OFFSET_TCRR)) = length;   // Buffer length

        // Reenable interrupts if necessary
        if (_channels[channel].interruptsEnabled) {
            Core::Interrupt interruptChannel = static_cast<Core::Interrupt>(static_cast<int>(Core::Interrupt::DMA0) + channel);
            Core::enableInterrupt(interruptChannel, INTERRUPT_PRIORITY);
        }
    }

    void stopChannel(int channel) {
        // Check that this channel exists
        if (channel >= _nChannels) {
            Error::happened(Error::Module::DMA, ERR_CHANNEL_NOT_INITIALIZED, Error::Severity::CRITICAL);
            return;
        }
        
        const uint32_t REG_BASE = BASE + channel * CHANNEL_REG_SIZE;

        // Disable the channel interrupt line, it will be reenabled when the channel is started again
        if (_channels[channel].interruptsEnabled) {
            Core::Interrupt interruptChannel = static_cast<Core::Interrupt>(static_cast<int>(Core::Interrupt::DMA0) + channel);
            Core::disableInterrupt(interruptChannel);
        }

        // Disable transfer and empty TCR
        _channels[channel].started = false;
        (*(volatile uint32_t*)(REG_BASE + OFFSET_CR)) = 1 << CR_TDIS;
        (*(volatile uint32_t*)(REG_BASE + OFFSET_TCR)) = 0;
    }

    int getCounter(int channel) {
        // TCR : Transfer Counter Register
        return (*(volatile uint32_t*)(BASE + channel * CHANNEL_REG_SIZE + OFFSET_TCR));
    }

    bool isEnabled(int channel) {
        // SR : Status Register
        return (*(volatile uint32_t*)(BASE + channel * CHANNEL_REG_SIZE + OFFSET_SR)) & (1 << SR_TEN);
    }

    bool isFinished(int channel) {
        // TCR : Transfer Counter Register
        return (*(volatile uint32_t*)(BASE + channel * CHANNEL_REG_SIZE + OFFSET_TCR)) == 0;
    }

    bool isReloadEmpty(int channel) {
        // TCR : Transfer Counter Reload Register
        return (*(volatile uint32_t*)(BASE + channel * CHANNEL_REG_SIZE + OFFSET_TCRR)) == 0;
    }

    void enableRing(int channel) {
        // MR : set the RING bit to keep reloading the channel with the same buffer
        (*(volatile uint32_t*)(BASE + channel * CHANNEL_REG_SIZE + OFFSET_MR)) |= 1 << MR_RING;
    }

    void disableRing(int channel) {
        // MR : reset the RING bit
        (*(volatile uint32_t*)(BASE + channel * CHANNEL_REG_SIZE + OFFSET_MR)) &= ~(uint32_t)(1 << MR_RING);
    }


    void interruptHandlerWrapper() {
        // Get the channel number through the current interrupt number
        int channel = static_cast<int>(Core::currentInterrupt()) - static_cast<int>(Core::Interrupt::DMA0);
        const uint32_t REG_BASE = BASE + channel * CHANNEL_REG_SIZE;

        // Call the user handler of every interrupt that is enabled and pending
        for (int i = 0; i < N_INTERRUPTS; i++) {
            if ((*(volatile uint32_t*)(REG_BASE + OFFSET_IMR)) & (1 << _interruptBits[i]) // Interrupt is enabled
                    && (*(volatile uint32_t*)(REG_BASE + OFFSET_ISR)) & (1 << _interruptBits[i])) { // Interrupt is pending
                void (*handler)() = (void (*)())_interruptHandlers[channel][i];
                if (handler != nullptr) {
                    handler();
                }

                // These interrupts are level-sensitive, they are cleared when their sources are resolved (e.g. 
                // the registers are written with non-zero values)
            }
        }
    }
}