M0AGX / LB9MG

Amateur radio and embedded systems

STM32L4 UART driver for FreeRTOS without HAL

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 public domain.