Introduction

In DMA First Steps, I learned the basics of DMA using it to copy data from one location in memory to another.

In this post, I used DMA to copy data between a peripheral and memory. I chose UART as an example and limited the scope to transmitting data via UART for simplicity.

First Attempt

First, I wanted to print Hello, world! to a serial console.

 char tx_buffer[] = "Hello, world!\r\n";

Next, I configured the DMA in memory-to-peripheral mode with the following configuration. I chose DMA2_Stream3 because it was the only stream available for USART3_TX.

// Init DMA clock
__DMA1_CLK_ENABLE();

// Source for DMA
char tx_data[] = "Hello, world!\r\n";

// 3. Memory address
DMA1_Stream3->M0AR = tx_data;

// 2. Peripheral address
DMA1_Stream3->PAR = &USART3->DR;

// 4. Number of data items
DMA1_Stream3->NDTR = sizeof(tx_data);

// 5. Select channel 4, it connects USART3 to DMA1_Stream3, Table 22, rm0033.
SET_BIT(DMA1_Stream3->CR, 0b100 << DMA_SxCR_CHSEL_Pos);

// 6. Keep DMA(Default) as flow controller.
// 7. Keep default priority
// 8. Keep default FIFO settings
// 9. Set direction as mem to periph, no address increment (default), single transanction (default), data width as 8 bit (default).
SET_BIT(DMA1_Stream3->CR, 0b01 << DMA_SxCR_DIR_Pos);      // Direction: memory to peripheral

// 10. Configure UART to use DMA and enable the stream
SET_BIT(USART3->CR3, USART_CR3_DMAT); // assume that USART3 is already initialized (not shown)
SET_BIT(DMA2_Stream0->CR, DMA_SxCR_EN);

Output:

Welcome to minicom 2.9

OPTIONS: I18n
Port /dev/ttyUSB0, 10:26:44

Press CTRL-A Z for help on special keys

HHHHHHHHHHHHHHHH

It looked like the DMA copied the first character H sixteen times and then stopped. Looking at the code again, this made sense because I was not incrementing the source address. Also, after the DMA transfer completed (NDTR = 0 and EN = 0), it was not re-enabled.

Second Attempt

I realized that a circular mode was available that did exactly what I needed. When NDTR reached 0, it auto-reloaded to the value with which the DMA stream had been initialized.

After enabling circular mode and enabling memory-address increment after each transfer, I saw this:

Code:

SET_BIT(DMA1_Stream3->CR, 0b01 << DMA_SxCR_DIR_Pos);      // Direction: memory to peripheral
SET_BIT(DMA1_Stream3->CR, DMA_SxCR_MINC);                 // Increment memory pointer
SET_BIT(DMA1_Stream3->CR, DMA_SxCR_CIRC);                 // Use circular mode to re-arm the DMA to transfer the data repeatedly.

Output:

Hello, world!
Hello, world!
Hello, world!
Hello, world!
Hello, world!
Hello, world!
Hello, world!

I paused execution with the debugger and was surprised to see the string still being printed. What could explain that?

My guess was that the debugger only halted the CPU. The DMA could still transfer data from SRAM to the UART, and the UART could still transmit the data without the CPU.

Summary

This was relatively straightforward. I learned how to connect a peripheral to DMA and saw a use case for circular mode.