This is a driver for STM32L432 LPUART. It should also work with the “full” UART. The LPUART is a simple peripheral (compared to the clock tree or ADC). In this case it is easier to master the usage of a couple of registers, than use full-size HAL drivers, as they are very generic to cover every possible flavor of a peripheral across the whole STM32 line, which in turn makes them big in terms of code size and actually harder to follow than the register layout.
The driver can be safely used within FreeRTOS, It can even be used by multiple tasks, but it probably would make little sense anyway, unless there can be different devices connected at runtime to the same UART or the application has separate operating modes implemented in different tasks.
For simplicity everything is interrupt driven, rather than DMA driven. In my particular case there is not much traffic, so setting up the DMA every time for just a couple of bytes could be less efficient than interrupts. Data is passed between tasks and interrupts using two FreeRTOS queues. One for the received bytes, another for the bytes to be transmitted.
Interface
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#pragma once #include <stdbool.h> #include <stdint.h> void uart_init(uint32_t baud); char uart_getc(void); char uart_try_getc(bool *status); void uart_putc(char c); void uart_write(const uint8_t *s, uint32_t length); void uart_puts(char *s); uint32_t uart_read_line(char *target, uint32_t max_length, uint32_t timeout_ms); void uart_unblock_read_line(void); #define UART_CLOCK 10000000u //PCLK1 - adjust for you bus frequency |
The interface has an init function that should be called only once. It initializes the UART for a particular speed and the data queues.
There is a group of functions for sending data – uart_putc, uart_puts, uart_write (the last one for raw binary). These functions block if the TX queue is full.
Data can be retrieved using the uart_getc (blocks until there is a byte available) and uart_try_getc (does not block).
uart_read_line is a helper function for implementing ASCII protocols that send data line by line. This function keeps loading data to the provided buffer until a newline character is encountered. It can be useful for receiving AT command responses from a modem. uart_unblock_read_line can be used (from another task) if it is necessary to unblock the read line function early.
Implementation
Thanks to FreeRTOS queues the implementation is very simple. All transmit functions load data to the transmit queue and set the UART to generate transmit buffer empty interrupts. All receive functions try to fetch data from the receive queue. When an interrupt fires it check if the UART has just received or sent a byte and acts accordingly by placing the received byte to the RX queue or by transmitting a byte from the TX queue.
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 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 |
#include "uart.h" #include <FreeRTOS.h> #include <queue.h> #include <task.h> #include <stm32l432xx.h> #define UART LPUART1 #define UART_IRQn LPUART1_IRQn #define UART_RX_BUFFER_SIZE 1024 #define UART_TX_BUFFER_SIZE 1024 static QueueHandle_t _rx_queue_handle; static QueueHandle_t _tx_queue_handle; void uart_init(uint32_t baud){ static StaticQueue_t rx_queue; static uint8_t rx_queue_storage_buffer[UART_RX_BUFFER_SIZE]; _rx_queue_handle = xQueueCreateStatic( UART_RX_BUFFER_SIZE, sizeof(char), rx_queue_storage_buffer, &rx_queue); static StaticQueue_t tx_queue; static uint8_t tx_queue_storage_buffer[UART_TX_BUFFER_SIZE]; _tx_queue_handle = xQueueCreateStatic( UART_TX_BUFFER_SIZE, sizeof(char), tx_queue_storage_buffer, &tx_queue); //initialize the hardware taskENTER_CRITICAL(); RCC->APB1ENR2 |= RCC_APB1ENR2_LPUART1EN_Msk; //enable clock to the UART taskEXIT_CRITICAL(); UART->CR1 = 0; //reset everything UART->BRR = 256u * UART_CLOCK / baud; //reference manual 39.4.4 UART->CR1 = USART_CR1_RXNEIE_Msk | USART_CR1_RE_Msk | USART_CR1_UE_Msk; taskENTER_CRITICAL(); NVIC_SetPriority(UART_IRQn, 6); //very low priority NVIC_EnableIRQ(UART_IRQn); taskEXIT_CRITICAL(); } char uart_getc(void){ char c; xQueueReceive(_rx_queue_handle, &c, portMAX_DELAY); return c; } char uart_try_getc(bool *status){ char c; *status = xQueueReceive(_rx_queue_handle, &c, 0/*don't wait*/); return c; } void uart_putc(char c){ xQueueSend(_tx_queue_handle, &c, portMAX_DELAY); //Configure the UART for transmission and reception, TX empty interrupt should fire now. //If the UART is already transmitting this write does not change anything. UART->CR1 = USART_CR1_RXNEIE_Msk | USART_CR1_TE_Msk | USART_CR1_TXEIE_Msk | USART_CR1_RE_Msk | USART_CR1_UE_Msk; } void uart_write(const uint8_t *s, uint32_t length){ for (uint32_t i = 0; i < length; i++){ uart_putc(*s); s++; } } void uart_puts(char *s){ while (*s){ uart_putc(*s); s++; } } //line ends by \r\n or \n, \r is always skipped uint32_t uart_read_line(char *target, uint32_t max_length, uint32_t timeout_ms){ uint32_t bytes_read = 0; *target = '\0'; while (max_length){ char c; bool status = xQueueReceive(_rx_queue_handle, &c, pdMS_TO_TICKS(timeout_ms)); if (status){ if (c == '\r'){ //skip \r continue; } bytes_read++; max_length--; if (c == '\n'){ *target = '\0'; return bytes_read; } *target = c; target++; } else { //no more characters in queue return bytes_read; } } //received line is too long *(target - 1) = '\0'; return bytes_read; } void uart_unblock_read_line(void){ char c = '\n'; xQueueSend(_rx_queue_handle, &c, portMAX_DELAY); } void LPUART1_IRQHandler(void){ BaseType_t wakeup_rx = pdFALSE; BaseType_t wakeup_tx = pdFALSE; if (UART->ISR & USART_ISR_RXNE_Msk){ //something has been received char c = UART->RDR; bool status = xQueueSendFromISR(_rx_queue_handle, &c, &wakeup_rx); if (status == false){ //input buffer overflow! __BKPT(1); //must never happen, the RX task must empty the incoming buffer fast enough } } if (UART->ISR & USART_ISR_TXE_Msk){ char c; if (xQueueReceiveFromISR(_tx_queue_handle, &c, &wakeup_tx)){ UART->TDR = c; } else { //no more data to transmit - let the transmitter output the last byte UART->CR1 = USART_CR1_RXNEIE_Msk | USART_CR1_TE_Msk | USART_CR1_RE_Msk | USART_CR1_UE_Msk; } } portYIELD_FROM_ISR(wakeup_rx || wakeup_tx); } |
I release this code into the public domain.