STM32 - Using DMA for Serial Receive

From Embedded Workshop
Jump to navigation Jump to search

USART - Using DMA for Serial Receive

With high speed serial data transfer, especially reception, it is critical to get the data
out of the receive shift register before another byte is received, else the earlier data
will be lost.
Early serial modules included FIFOs in the design, allowing data to flow from receive shift
register into a FIFO for storage.
These STM32 parts we are using DO NOT have FIFOs for data storage.  Instead, they rely on
DMA to move data out of the receive shift register directly into the client's receive buffer.

This document was originally created for the NUCLEO-F103RB, which connects USART2 to the on-board
USB-Serial JTAG part.

Configure your project to use DMA with USART2:

1) Open up "Pinout & Configuration" tab (accessible by opening your project's .ioc file)
2) Open up Connectivity -> USART2

3) Select the "DMA Settings" tab, and click "Add" at the bottom of the dialog.
4) Select "USART2_RX" from the pull-down menu
5) Select "Circular" for Mode, Byte Data Width, and "Memory" for Increment Address

Add the following code to main.c, inside USER CODE BEGIN Includes:

/* USER CODE BEGIN Includes */
#include <stdio.h>
/* USER CODE END Includes */

Add the following code to main.c, inside USER CODE BEGIN 0

// DMA Buffer for usart2 RX
#define USART2_RX_DMA_BUFFER_SIZE 128
uint8_t usart2_rx_dma_buffer[USART2_RX_DMA_BUFFER_SIZE];

#define HAL_SMALL_DELAY  40
// Define serial input and output functions using UART2
int __io_putchar(int ch)
{
   HAL_UART_Transmit(&huart2, (uint8_t *)&ch, 1, HAL_SMALL_DELAY); // infinite delay value
   return 1;
}

// Implement a get_byte(void) function that utilizes a circular DMA RX buffer that:
// 1) Tests if data is available in the DMA RX buffer
// 2) Pulls and returns the data
// 3) Manages an index_out value for next removal of data
// 4) As a non-blocking function, return EOF when no bytes are available, else returns data byte
int __io_getchar(void)
{
  static uint16_t index_out = 0;
  uint16_t index_in = USART2_RX_DMA_BUFFER_SIZE - huart2.hdmarx->Instance->CNDTR;
  if(index_in == index_out) return EOF;
  // Else, we have a byte in buffer to return
  uint8_t data = usart2_rx_dma_buffer[index_out];
  // advance index
  index_out++;
  // wrap index when necessary
  if(index_out >= USART2_RX_DMA_BUFFER_SIZE) index_out = 0;
  return data;
}

Add the following code to main.c, following the MX_USART2_UART_Init() call, inside USER CODE BEGIN 2

/* USER CODE BEGIN 2 */
/* Put UART peripheral in DMA reception mode */
if (HAL_UART_Receive_DMA(&huart2, usart2_rx_dma_buffer, USART2_RX_DMA_BUFFER_SIZE) != HAL_OK)
{
  /* Transfer error in reception process */
  printf("HAL_UART_Receive_DMA() error!\n");
}

You can now test your serial receive with the following code in main's while-loop:

/* USER CODE BEGIN WHILE */
while (1)
{
  // Using TeraTerm, paste large text blocks into the terminal.
  // The DMA assisted receive function won't miss a single character,
  //   and will display all text received.
  int mybyte = __io_getchar();
  if(mybyte < 0) continue;
  __io_putchar(mybyte);
  /* USER CODE END WHILE */

  /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

Command Line - X-Modem

Once DMA receive was implemented, I returned to a previous project, X-Modem.
Now, X-Modem Just Worked!
I started with the X-Modem package, xmodem-1k-master.zip, from here:
https://github.com/kelvinlawson/xmodem-1k/blob/master/xmodem.c
I added code to use STM32 serial interfaces and LittleFS:
xmodem.c
crc16.c
crc16.h