DMA
module reference#include <dma.h>Makefile :
dma
is already included by default
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 :
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()
.
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.
RELOAD_EMPTY
, TRANSFER_FINISHED
or TRANSFER_ERROR
.
startChannel()
function below.
setupChannel()
must have been called beforehand to configure the channel.
setupChannel()
and startChannel()
functions above.
startChannel
with a buffer AreloadChannel
reloadChannel
with the buffer A when it is done processing ittrue
if the current transfer is finished. This is equivalent to checking if the counter has reach 0.
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()
.
The DMA controller is pretty straightforward and doesn't allow much customization.
#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); int setupChannel(int channel, 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
#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; _nChannels++; // Setup the chosen channel setupChannel(n, device, size, address, length, ring); return n; } // Create a channel or reuse an existing one int setupChannel(int channel, Device device, Size size, uint32_t address, uint16_t length, bool ring) { // If the given channel is negative, create a new one if (channel < 0) { return newChannel(device, size, address, length, ring); } const uint32_t REG_BASE = BASE + channel * 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[channel].started = false; _channels[channel].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; return channel; } 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) } } } }