300x250 AD TOP

Search This Blog

Pages

Paling Dilihat

Powered by Blogger.

Monday, November 15, 2021

USART with DMA on STM32

 I've been working with many projects that use the USART and not one was like the other alghough hardware resources were pretty similar. 

So I've sat down and decided to make a boilerplate for USART with DMA implementation that uses binary semaphores to notify when data arrives and buffers the output to create as little delay as possible as well as leave as much CPU as possible for the rest of the system.

For this demo I'll be using the STM32F446 Nucleo-64.



By default, it has the USART2 pins connected to the on board ST-Link so its possible to just open a terminal, watch logs and send commands to the MCU with as little effort as possible.


Once we have the basics setup in the IDE and the USART2 Enabled as Asynchronous, We'll go ahead and add DMA Channels:



One for read, one for write and set them both to Normal mode.

Enable global interrupts:



We then go ahead and add FreeRTOS, so we can demo a general application:


And go ahead and USE_NEWLIB_REENTRANT so we can use printf:


And lastly we'll go to project manager and mark the Generate peripheral initialization as pair of '.c/.h' files per peripheral for just to keep our application a bit cleaner:


A known bug (1,2,3,4) in HAL generated projects is that the DMA is not initialized in order, a simple solution will be to duplicate the DMA initialization call to the 'USER CODE BEGIN SysInit' section in main.c so whenever the project is regenerated, the change won't get lost.

1
2
3
/* USER CODE BEGIN SysInit */
  MX_DMA_Init();
/* USER CODE END SysInit */


Once our project is generated, we'll add a circular buffer of choice, in this case I've chosen to use Tilen Majerle's lwrb - Lightweight ring buffer manager.

Next in our usart.c, we'll add 2 semaphores for the tx and rx buffers, 2 aligned buffers for the DMA and 2 buffers for rx and tx, we'll use our "USER  CODE BEGIN 0" for that so we'll keep them when the project is regenerated through STM32CubeMX/IDE. 

Feel free to change the buffer sizes, though for my needs I didn't see a reason to go higher.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/* USER CODE BEGIN 0 */

#include <cmsis_os.h>
#include "lwrb/lwrb.h"

static SemaphoreHandle_t readSemaphore;
static osSemaphoreId writeSemaphore;

#define TX_DMA_BUFFER_SIZE 16
__aligned(32) uint8_t TX_DMA_buffer[TX_DMA_BUFFER_SIZE];

#define RX_DMA_BUFFER_SIZE 16
__aligned(32) uint8_t RX_DMA_buffer[RX_DMA_BUFFER_SIZE];

lwrb_t rx_buffer;
uint8_t rx_buffer_container[255];

lwrb_t tx_buffer;
uint8_t tx_buffer_container[255];

void initialize_buffers(void) {
	osSemaphoreDef(WRITESEM);
	writeSemaphore = osSemaphoreCreate(osSemaphore(WRITESEM), 1);

	vSemaphoreCreateBinary(readSemaphore);
	if (readSemaphore == NULL) {
		Error_Handler();
	}

	if (lwrb_init(&rx_buffer, rx_buffer_container, sizeof(rx_buffer_container)) != 1){
		Error_Handler();
	}
	if (lwrb_init(&tx_buffer, tx_buffer_container, sizeof(tx_buffer_container)) != 1){
		Error_Handler();
	}
}

/* USER CODE END 0 */

Note we included also our buffer initialization routine in the header.

Next we'll add the DMA start in our MX_USART2_UART_Init function in usart.c:

1
2
3
4
  /* USER CODE BEGIN USART2_Init 2 */
  HAL_UARTEx_ReceiveToIdle_DMA(&huart2, RX_DMA_buffer, RX_DMA_BUFFER_SIZE);
  __HAL_DMA_DISABLE_IT(&hdma_usart2_rx, DMA_IT_HT);
  /* USER CODE END USART2_Init 2 */

Thanks for the tip about DMA_IT_HT from ControllersTech.

Next we'll add our USART tx/rx functions in usart.c. If you're wondering about the xSemaphoreGiveFromISR at line 23, its used to notify the waiting thread about new data rather than continuous polling that will either waste CPU time or cause a delay between received bytes until the thread realizes it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/* USER CODE BEGIN 1 */

static int tx_next_chunk(void) {
	int number_of_items_in_tx_buffer = lwrb_read(&tx_buffer, TX_DMA_buffer, TX_DMA_BUFFER_SIZE);
	if (number_of_items_in_tx_buffer > 0) {
		if (HAL_UART_Transmit_DMA(&huart2, TX_DMA_buffer,
				number_of_items_in_tx_buffer) != HAL_OK) {
			assert(0);
		}
		__HAL_DMA_DISABLE_IT(&hdma_usart2_rx, DMA_IT_HT);
	}
	return number_of_items_in_tx_buffer;
}

void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) {
	if (huart->Instance == USART2) {
		if (lwrb_write(&rx_buffer,  RX_DMA_buffer, Size) != Size ){
			//buffer overrun
		}

		HAL_UARTEx_ReceiveToIdle_DMA(huart, RX_DMA_buffer, RX_DMA_BUFFER_SIZE);
		BaseType_t xHigherPriorityTaskWoken;
		xSemaphoreGiveFromISR(readSemaphore,&xHigherPriorityTaskWoken);
	}
}

int get_rx_data(uint8_t *buffer, size_t buffer_length, uint32_t timeout) {
	xSemaphoreTake(readSemaphore,pdMS_TO_TICKS(timeout ));
	return lwrb_read(&rx_buffer, buffer, buffer_length);
}

void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) {
	if (huart->Instance == USART2) {
		tx_next_chunk();
	}
}

void put_tx_data_with_wait(uint8_t *buffer, size_t buffer_length) {
	int retries = 1000;
	while (retries > 0) {
		int pushed_bytes = put_tx_data(buffer, buffer_length);
		buffer_length -= pushed_bytes;
		buffer += pushed_bytes;
		if (buffer_length <= 0) {
			break;
		}
		osDelay(1);
		retries--;
	}
}

int put_tx_data(uint8_t *buffer, size_t buffer_length) {
	int ret = 0;
	if (osSemaphoreWait(writeSemaphore, osWaitForever) == osOK) {
		ret = lwrb_write(&tx_buffer, buffer, buffer_length);
		osSemaphoreRelease(writeSemaphore);
	}

	if (huart2.gState == HAL_UART_STATE_READY) {
		tx_next_chunk();
	}
	return ret;
}

/* USER CODE END 1 */

And our function prototypes in usart.h:

1
2
3
4
5
/* USER CODE BEGIN Prototypes */
void put_tx_data_with_wait(uint8_t *buffer, size_t buffer_length);
int put_tx_data(uint8_t *buffer, size_t buffer_length);
int get_rx_data(uint8_t *buffer, size_t buffer_length, uint32_t timeout);
/* USER CODE END Prototypes */

And lastly we'll create our echo demo in StartDefaultTask in our freertos.c:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void StartDefaultTask(void const * argument)
{
  /* USER CODE BEGIN StartDefaultTask */
	while (1){
		uint8_t temp_buffer[64];
		size_t read_bytes;
		read_bytes =get_rx_data(temp_buffer, sizeof(temp_buffer), 100);
		put_tx_data_with_wait(temp_buffer,read_bytes);
	}
  /* USER CODE END StartDefaultTask */
}

What the demo does is essentially waiting for up to 64 bytes or 100ms and transmitting back what it got. so this thread is waiting most of the time, the DMA does most of the work and the ring buffer is just there to make sure everything plays together nicely.

The demo project can be found here:

https://github.com/drorgl/usart-boilerplate




Tags: , , ,