DMA first steps
Introduction
When writing software, there are always design decisions to make. I had worked on projects that used DMA but I never had to set it up from scratch. Because of that, I had never had to think carefully about the available options or the tradeoffs involved.
My current understanding is that it copies data from peripherals/memory to memory and vice versa. It can be primed with some parameters and once it is done, it lets the CPU know that data has been copied.
This project is my attempt to understand DMA more deeply.
Scope
I started with the simplest case: copying data from one location in memory to another. I chose SysTick->VAL as the first
variable and created a destination variable. The DMA will copy the data from one to another.
I used a Nucleo dev board with an STM32F207 MCU.
Research
I began by looking at the DMA chapter in the STM32F207xx reference manual. From the DMA main features
section, I learned that this MCU has two DMA controllers, each with 8 streams, and each of those streams has
8 channels. I was not completely sure what ‘streams’ and ‘channels’ mean.
Reading further in the same section, I saw this:
• Each stream can be configured by hardware to be:
– a regular channel that supports peripheral-to-memory, memory-to-peripheral and
memory-to-memory transfers
– a double buffer channel that also supports double buffering on the memory side
• Each of the 8 streams are connected to dedicated hardware DMA channels (requests)
From the first bullet point, it seems that a stream is what I need to configure to copy data from memory-to-memory.
The second bullet says that each stream has channels connected for ‘requests’. My guess is that each stream
can receive requests to transfer data using these channels.
It may seem that I am guessing a lot but it is okay. They are educated guesses to build a mental model of how I think this peripheral works. If I am wrong, I will find that out soon when this experiment fails to produce the results I want. Then I can dig deeper and update my understanding.
Looking at the 9.3 DMA functional description, it actually aligns with my understanding so far. A stream is just a conduit of data between two locations.
Figure 22 shows even more detail of how DMA controllers are connected. The key takeaway is the footnote. Only DMA2 can perform memory-to-memory transfers, which is what I want. This means that I can only use DMA2 but still not sure which stream. Can I just pick any or are they fixed? I don’t know yet.
Looking at 9.3.2, I get some more detail. To summarize, each DMA transfer has three operations: Load data from periph/mem, store data to periph/mem, and finally decrement a register containing pending transactions.
9.3.3 talks about how to select a channel. I learned that channels are how requests are sent to a DMA controller from the peripheral. Since I am only interested in mem-to-mem transfer, they are not relevant. But then how do I send a mem-to-mem transfer request?
9.3.6 talks about how to configure source/dest and transfer mode. For our case, DIR[1:0] is 10, source address in
DMA_SxPAR and dest address in DMA_SxM0AR, data width in PSIZE or MSIZE in DMA_SxCR.
Finally, the Memory-to-Memory mode subsection says that I need to set EN in DMA_SxCR. As soon as it
is enabled, the stream will fill up the FIFO and when a configuredthreshold is reached, the data will be written to dest.
This answered my question about how the DMA request is triggered.
9.3.7 explains how to increment source/dest pointers automatically but I do not need this since I am copying a single data item.
9.3.10 Number of data items to be transferred is set in DMA_SxNDTR (= 1, in our case). Size of the data items
in PSIZE and MSIZE in DMA_SxCR (both 32 bits in my case).
9.3.11 Single and burst transfers I will stick to single transfer since I only copy one word.
9.3.12 explains the internal FIFO, I did not think this was needed for this experiment.
9.3.13 states that TCIFx will be set in DMA_LISR/HISR when the transfer is complete. I can read this to confirm that the
transfer is complete.
9.3.14 talks about how to suspend a DMA transfer, I don’t need this.
9.3.15 explains flow control options meaning who decides when and how many data items to transfer. This is
made easier by When configured in memory-to-memory mode, the DMA is always the flow controller and
the PFCTRL bit is forced to 0 by hardware. So setting DMA in this mode will take care of everything for me.
9.3.17 summarizes what I need to do to set up the DMA. Since I have gone through most of the configuration options, I know enough to decide which options I need.
But wait a moment, I still don’t know which stream to use? Table 23 shows stream mappings for DMA2. It appears that the streams are hardwired to specific peripherals via channels. Since I am only interested in memory to memory transfer, DMA2 has access to the memory through all the streams. Therefore, it should not matter which stream I use. This is confirmed by figure 22 which shows that both ports of DMA2 are connected to the Bus Matrix. The blue and black lines show that both ports can access the SRAM.
I will proceed with Stream0.
Implementation
Translating the steps in 9.3.17 into C code, this is what it looks like:
__DMA2_CLK_ENABLE();
// Destination variable
uint32_t systick_copy = 0;
// 1. Skip this step as DMA is not enabled yet.
// 2. Set source addr
DMA2_Stream0->PAR = &(SysTick->VAL);
// 3. Set dest addr
DMA2_Stream0->M0AR = &systick_copy;
// 4. Configure number of data items
DMA2_Stream0->NDTR = 1;
// 5. Skip channel selection for mem-to-mem transfers
// 6. Skip flow controller because this will be set by data transfer direction configuration.
// 7. Keep default priority
// 8. Keep default FIFO settings
// 9. Set direction as mem to mem, no address increment, single transaction, data width as 32bit, enable interrupt after full transfer.
DMA2_Stream0->CR |= (0b10 << DMA_SxCR_DIR_Pos) | (0b10 << DMA_SxCR_MSIZE_Pos) | (0b10 << DMA_SxCR_PSIZE_Pos) | (1 << DMA_SxCR_TCIE_Pos);
// 10. Enable the stream
HAL_NVIC_EnableIRQ(DMA2_Stream0_IRQn);
SET_BIT(DMA2_Stream0->CR, DMA_SxCR_EN);
In the interrupt handler, I clear the Half Transfer and Transfer Complete flags by writing to these registers or otherwise the interrupt kept firing over and over.
void DMA2_Stream0_IRQHandler()
{
DMA2->LIFCR |= (DMA_LIFCR_CHTIF0 | DMA_LIFCR_CTCIF0);
}
Debugging
I ran this using the debugger but systick_copy was still 0. Hmmm, I also noticed that EN is still 0 in CR. Further, Transfer error flag TEIF0 is set in LISR.
Section 9.3.18 says that Transfer error flag might be set due to a bus fault. This gave me a pause. The source address (SysTick->VALUE) is not in SRAM but actually in the peripheral address space. Also, SysTick is not a peripheral in the same sense a UART is. It lives inside the CPU core and it cannot be read by other masters on the Bus Matrix.
To get back on track, I added another local variable as source. Also, I decided to increment the source_var in the while loop. If the DMA is working correctly, after a transfer is complete, dest_var == source_var.
// Destination variable
static uint32_t source_var = 0; //
static uint32_t dest_var = 0;
<other code>
while (1)
{
source_var++;
}
Then, I observed that interrupt would fire once and stop. Some thinking and another look at the reference manual made me realize EN flag is cleared when a DMA transfer is complete and I am not setting it again anywhere. My next step was to enable the DMA stream again in the interrupt handler.
void DMA2_Stream0_IRQHandler()
{
DMA2->LIFCR |= (DMA_LIFCR_CHTIF0 | DMA_LIFCR_CTCIF0);
SET_BIT(DMA2_Stream0->CR, DMA_SxCR_EN);
}
Unfortunately this caused the interrupt to fire right after EN bit set and the control would never return to the while loop.
In retrospect, this could have been easily avoided with a little thought about the sequence of operations.
- I increment the
source_varin the while loop (This happens already) - Then I want this data to be copied over to
dest_varby DMA. Currently, this is done once before the source_var is incremented and never again. - Once this data is copied, I don’t want DMA to do anything. (This happens already by clearing the flags)
I moved the EN bit enabling to after incrementing the source_var inside the while loop.
// 10. Enable the stream
HAL_NVIC_EnableIRQ(DMA2_Stream0_IRQn);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
source_var++;
SET_BIT(DMA2_Stream0->CR, DMA_SxCR_EN);
}
Final result
This works as expected. I set a breakpoint at the start of the while loop and when it is hit, source_var is equal to the dest_var.
This was not as complicated as I had anticipated. Writing my thought process while I worked through it made it easier.
Summary
What I have learned:
- DMA allows us to copy data from peripheral/memory to memory and vice versa.
- Each DMA controller has streams which are conduits between peripheral/memory <—–> memory.
- Each stream has channels which are used to send DMA transfers requests by peripherals.
- A DMA transfer is started by configuring a stream with certain parameters and enabling it.
- When the transfer is complete a flag is set and an interrupt is generated if configured.