UART with STM32 LL Send (DMA)

Hello everyone.

In this article, we will try to send LL UART communication using DMA.

What is LL? If you are wondering what LL is, please refer to the article HAL and LL.

The following is the development environment at the time of posting.

PC: Windows 10 OS
IDE: STM32CubeIDE Version1.5.0
Configurator: STM32CubeMX Version6.1.0
Board: STM32Nucleo-F401RE

What is DMA?

DMA stands for Direct Memory Access.
Normally, data is read from the peripheral (peripheral I/O) into memory via the CPU, and data is written from memory to the peripheral via the CPU.
With DMA, you can literally access memory directly, so there is no need to go through the CPU, and that time can be used for other processing.

In the last two articles, I wrote about interrupts and polling transmission.

DMA > interrupt > polling, in that order, is probably the most efficient use of the CPU.

Create a project

Start the IDE, select File- New – STM32 Project, when the Target Selection window comes up, select the Board Selector tab, select NUCLEO-F401RE from the Boards List and press Next. Click the Next button.

Enter F401UartLLTxDMA as the Project Name and press the Finish button.
When it asks “Initialize all peripherals with their default Mode ? Press Yes.

Preparing to use the UART with LL

Select the Project Manager tab and change the USART in Advanced Settings from HAL (default) to LL. (Use the combo box to select)

Click on the HAL section to display a list of combo boxes, and select LL.

Select Project – Build All.

It will ask “Do you want Generate Code ? Check the Remember my decision checkbox and press OK.

This action can be associated with C/C++ perspective.

This action can be associated with C/C++ perspective.

Modify UART parameters

Double-click F401UartLL.ioc from Project Explorer to open the GUI window.

Select Pinout & Configuration – Connectivity – USART2.

For Mode, Asynchronus is selected and ready to use. (Disable if you don’t want to use it.)

We will use parity again.
In the Configuration – Parameter Settings section, make the following settings.
(The only change from the default is to change Parity from None to Even)

Baud Rate : 115200
Word Length : 8bits
Parity : Even
Stop Bits : 1

Configure interrupt settings

Check the Enabled checkbox for USART2 global interrupt in Configuration – NVIC Settings.

Configure DMA settings

In Configuration – DMA Settings, press the Add button and select USART2_TX from the Select combo box.

Now let’s build it once to check the code.
Select Project – Build All.

STM32 DMA

In order to use LL and DMA in the future, we need to understand the registers of the peripherals.

To do so, please download the reference manual RM0368 for the microcontroller used in this board from here. We will use DMA to run the UART.

This time, we will use DMA to run UART, so please read the DMA section in section 9 and the USART section in section 19 carefully.

We have already explained about UART in detail in the last two articles, so we will explain about DMA here as much as we can.

The block diagram of DMA is shown below.
There are two DMA peripherals, 1 and 2, and several channels and streams.

Which one should I choose to use? I was wondering…

This figure below will help you solve the problem.
(In the reference manual, there is another unused DMA2 diagram below this one, so make sure you read through that one as well.)

It seems that each peripheral has its own combination of streams and channels that can be used.
In the case of USART2, the table is as follows.
Since this is a transmission, we will use 1 for the DMA peripheral, 6 for the stream, and 4 for the channel.

DMA peripheral stream channel

USART2 TX DMA1 6 4
USART2 RX DMA1 5 4

Stream means “flow,” and you can think of it as a communication path.

DMA registers explained

Setting registers DMA_SxCR (x=0-7)

First, let’s look at the configuration registers.

The Sx part is the stream number. As explained earlier, we will be setting it to S6.

I will explain this based on what I read in the reference manual.

Bits 27, 26, 25 : CHSEL Channel selection

As explained above, 4

Bit 24,23 : MBURST Memory Burst Transfer Setting

0 for single transfer

Bit 22,21 : PBURST Peripheral Burst Transfer

0 for single transfer

Bit 19 : CT Current target

0 since single buffer is used
Current target memory is memory 0 (addressed by DMA_SxM0AR pointer)

Bit 18 : DBM Double buffer mode

0 since a single buffer is used.

Bit 17, 16 : PL Priority level

Low 0

Bit 15 : PINCOS Peripheral Increment Offset Size

0 because it is not used.
(When PINC = 0, this bit has no meaning.)

Bit 14,13 : MSIZE Memory data size

Byte 0

Bit 12,11 : PSIZE Peripheral data size

Byte 0

Bit 10 : MINC Memory Increment Mode

1 as it increments

Bit 9 : PINC Peripheral Increment Mode

0 because it does not increment

Bit 8 : CIRC Circular mode

Invalid 0

Bit 7, 6 : DIR Data transfer direction

Memory to peripheral 1

Bit 5 : PFCTRL Peripheral flow controller

DMA is flow controller 0

Bit 4 : TCIE Transmit Complete Interrupt Enable

Enable 1

Bit 3 : HTIE Enable 1/2 transfer interrupt

Not used, so 0

Bit 2 : TEIE Enable transfer error interrupt

0 since it is not used.

Bit 1 : DMEIE Enable direct mode error interrupt

Not used, 0

Bit 0 : Enable stream

0 when setting, 1 when sending

The value of DMA1_S6CR should be 0x08000450 when the initial settings are completed.

Data Register DMA_SxNDTR (x=0-7)

Here we write the size of the array buffer, sizeof(buffer), which is the number of bytes to send.
The value written here will be decremented each time it is sent, so it must be written each time it is sent.

Peripheral address register

Specify the DR for USART2 here.

Memory 0 Address Register

This is where the memory address of the DMA transfer source is specified.

The memory 1 address register after this is omitted since it is not used in single buffer mode.
The FiFo control register is also omitted since it is not used.

Coding

main.c

The initialization code related to DMA is also generated within MX_USART2_UART_Init(), but the last two lines need to be added.

It will be easier to understand if you step through and check what each function of initialization is doing.

You can check the value of each register from Window – Show View – SFRs – DMA1.

static void MX_USART2_UART_Init(void)
{

  /* USER CODE BEGIN USART2_Init 0 */ /* USER CODE END USART2_Init 0

  /* USER CODE END USART2_Init 0 */

  LL_USART_InitTypeDef USART_InitStruct = {0}

  LL_GPIO_InitTypeDef GPIO_InitStruct = {0};

  /* Peripheral clock enable */ LL_APB1_GRP
  LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_USART2);

  LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_GPIOA);
  /**USART2 GPIO Configuration
  PA2 ------> USART2_TX
  PA3 ------> USART2_RX
  */ GPIO_InitStruct.
  GPIO_InitStruct.Pin = LL_GPIO_PIN_2|LL_GPIO_PIN_3;
  GPIO_InitStruct.Mode = LL_GPIO_MODE_ALTERNATE;
  GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_LOW;
  GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
  GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
  GPIO_InitStruct.Alternate = LL_GPIO_AF_7;
  LL_GPIO_Init(GPIOA, &GPIO_InitStruct);

  /* USART2 DMA Init */ /* USART2_TX Init

  /* USART2_TX Init */
  LL_DMA_SetChannelSelection(DMA1, LL_DMA_STREAM_6, LL_DMA_CHANNEL_4);

  LL_DMA_SetDataTransferDirection(DMA1, LL_DMA_STREAM_6, LL_DMA_DIRECTION_MEMORY_TO_PERIPH);

  LL_DMA_SetStreamPriorityLevel(DMA1, LL_DMA_STREAM_6, LL_DMA_PRIORITY_LOW);

  LL_DMA_SetMode(DMA1, LL_DMA_STREAM_6, LL_DMA_MODE_NORMAL);

  LL_DMA_SetPeriphIncMode(DMA1, LL_DMA_STREAM_6, LL_DMA_PERIPH_NOINCREMENT);

  LL_DMA_SetMemoryIncMode(DMA1, LL_DMA_STREAM_6, LL_DMA_MEMORY_INCREMENT);

  LL_DMA_SetPeriphSize(DMA1, LL_DMA_STREAM_6, LL_DMA_PDATAALIGN_BYTE);

  LL_DMA_SetMemorySize(DMA1, LL_DMA_STREAM_6, LL_DMA_MDATAALIGN_BYTE);

  LL_DMA_DisableFifoMode(DMA1, LL_DMA_STREAM_6);

  /* USART2 interrupt Init */
  NVIC_SetPriority(USART2_IRQn, NVIC_EncodePriority(NVIC_GetPriorityGrouping(),0, 0));
  NVIC_EnableIRQ(USART2_IRQn);

  /* USER CODE BEGIN USART2_Init 1 */

  /* USER CODE END USART2_Init 1 */
  USART_InitStruct.BaudRate = 115200;
  USART_InitStruct.DataWidth = LL_USART_DATAWIDTH_8B;
  USART_InitStruct.StopBits = LL_USART_STOPBITS_1;
  USART_InitStruct.Parity = LL_USART_PARITY_EVEN;
  USART_InitStruct.TransferDirection = LL_USART_DIRECTION_TX_RX;
  USART_InitStruct.HardwareFlowControl = LL_USART_HWCONTROL_NONE;
  USART_InitStruct.OverSampling = LL_USART_OVERSAMPLING_16;
  LL_USART_Init(USART2, &USART_InitStruct);
  LL_USART_ConfigAsyncMode(USART2);
  LL_USART_Enable(USART2);
  /* USER CODE BEGIN USART2_Init 2 */

  // Here's the code I added myself.
  // No need to do it every time, so I wrote it here.

  // Transfer source, destination, direction
  LL_DMA_ConfigAddresses(DMA1, LL_DMA_STREAM_6, (uint32_t)&buffer, LL_USART_DMA_GetRegAddr(USART2), LL_DMA_DIRECTION_MEMORY_TO_PERIPH);

  // Enable transmit completion interrupt.
  LL_DMA_EnableIT_TC(DMA1, LL_DMA_STREAM_6);

  /* USER CODE END USART2_Init 2 */ (DMA1, LL_DMA_STREAM_6)

}

Write the function DMA_TransmitData() to send data around the front of the main() function.

/* USER CODE BEGIN 0 */
#define _CR 0x0d
#define _LF 0x0a

uint8_t buffer[] =
{
	'#', 'a', 'b', 'c', 'd', 'e', _CR, _LF
};

void DMA_TransmitData();

void DMA_TransmitData()
{

  LL_DMA_DisableStream(DMA1, LL_DMA_STREAM_6);

  LL_DMA_SetDataLength(DMA1, LL_DMA_STREAM_6, sizeof(buffer));

  LL_DMA_EnableStream(DMA1, LL_DMA_STREAM_6);

  LL_USART_EnableDMAReq_TX(USART2);
}

/* USER CODE END 0 */

/**
  * @brief The application entry point.
  * @retval int
  */ int main(void)
int main(void)

Inside the while() loop

while (1)
{
  DMA_TransmitData();
  HAL_Delay(1000);
  /* USER CODE END WHILE */
  /* USER CODE BEGIN 3 */ /* USER CODE END WHILE
}

stm32f4xx_it.c

DMA1_Stream6_IRQHandler() is an interrupt handler for DMA1 stream 6.
It adds a process to clear the transmit completion flag if it is present.
If the flag is not cleared, the next transmission will not be performed.

void DMA1_Stream6_IRQHandler(void)
{
  /* USER CODE BEGIN DMA1_Stream6_IRQn 0 */
  if( LL_DMA_IsActiveFlag_TC6(DMA1) == 1){
    LL_DMA_ClearFlag_TC6(DMA1);
  }
  /* USER CODE END DMA1_Stream6_IRQn 0 */

  /* USER CODE BEGIN DMA1_Stream6_IRQn 1 */ /* USER CODE

  /* USER CODE END DMA1_Stream6_IRQn 1 */ /* USER CODE
}

Tera Term Setup

The image is omitted because it has been shown many times.
Set up in the following way.

COM port : Select Serial in New Connection and select the STLink Virtual COM port.

From Settings – Serial Port

Speed : 115200
Data : 7bit
Parity : even
Stop bit : 1bit
Flow control : none

Check the Local Echo checkbox in the terminal settings.

Program overview

The configurator did most of the initial configuration for DMA, which was helpful.
About the two lines I added

First

LL_DMA_ConfigAddresses(DMA1, LL_DMA_STREAM_6, (uint32_t)&buffer, LL_USART_DMA_GetRegAddr(USART2), LL_DMA_DIRECTION_MEMORY_TO_PERIPH);

to set the respective addresses and directions of the memory and peripherals.

The interrupt settings needed to be done separately, so

LL_DMA_EnableIT_TC(DMA1, LL_DMA_STREAM_6);

to enable the transmission completion interrupt.

In DMA_TransmitData()

The number of bytes to be transmitted is set each time.
To set it, the stream must be disabled.

After that, transmission starts by requesting it with LL_USART_EnableDMAReq_TX().

Build and run

Build and make sure there are no errors.

Setup Tera Term on the computer side and run the program.

If #abcde is displayed every 1 second, the program is successful.

How did you like it?
Did the UART transmission using DMA work well?

Thank you for your time.

Leave a Reply