02 UART
Last modified: Jul 07, 2026
After getting blinky working, I wanted to make the board do some basic communication. The obvious choice was a UART driver.
For this part, I used UART4 on the STM32F407 Discovery board (I do not remember why I chose UART4, but that is what it is):
- TX: PA0, alternate function AF8
- RX: PA1, alternate function AF8
- Baud rate: 115200
For testing, I connected TX and RX together as a loopback. If the byte I send comes back correctly, the green LED toggles. If something does not match, the red LED toggles.
Starting with One Byte
The first goal was just to send and receive one byte.
UART_transmitByte(UART4, 'A');
uint8_t received = UART_readByte(UART4);
if (received == 'A') {
LED_green_bool = !LED_green_bool;
} else {
LED_red_bool = !LED_red_bool;
}
At the register level, this mostly means:
- Enable the GPIO clock.
- Set PA0 and PA1 to alternate function mode.
- Select AF8 for PA0 and PA1.
- Enable the UART4 peripheral clock from RCC.
- Set the baud rate register.
- Enable the transmitter, receiver, and UART itself.
For UART4, the important registers are:
RCC->APB1ENR: enables the UART4 peripheral clock.USARTx->BRR: sets the baud rate.USARTx->CR1: enables TX, RX, UART, and optionally the RX interrupt.USARTx->SR: contains status flags likeRXNEandTXE.USARTx->DR: the data register used for both transmit and receive.
My first blocking functions looked like this:
void UART_transmitByte(USART_t *uart, uint8_t data) {
while ((uart->SR & BIT(7)) == 0) {
;
}
uart->DR = data;
}
uint8_t UART_readByte(USART_t *uart) {
while ((uart->SR & BIT(5)) == 0) {
;
}
return (uint8_t)(uart->DR & 0xFF);
}
BIT(7) is TXE, which tells me the transmit data register is empty. BIT(5) is RXNE, which tells me received data is ready to read.
This worked for sending one byte, and it was a good first step to check that the GPIO alternate function setup, UART clock, baud rate, and loopback wiring were all correct.
Multiple-Byte
One byte worked, so the next step was sending and receiving multiple bytes.
This caused some problems because the UART data register is not a string buffer. I cannot write several bytes and expect the hardware to remember all of them for me.
To solve this, I started using interrupt-based UART communication. The blocking transmit side is still simple because I can wait for TXE before writing each byte:
void UART_transmitBytes(USART_t *uart, uint8_t *data, uint32_t size) {
for (uint32_t i = 0; i < size; i++) {
while ((uart->SR & BIT(7)) == 0) {
;
}
uart->DR = data[i];
}
}
Receiving is different. If bytes arrive and I do not read them quickly enough, I can lose data. The microcontroller needs to immediately check when the data comes in. This is where interrupts make more sense.
RXNE Interrupt
The useful receive interrupt is RXNE: receive data register not empty.
The idea:
- A byte arrives.
- The UART sets
RXNE. - The interrupt handler runs.
- The handler reads
DR. - The byte gets stored in a software buffer.
- The main loop can read from that buffer later.
For UART4, the handler looks like this (for others it basically looks the same):
void UART4_IRQHandler(void) {
if (UART4->SR & BIT(5)) {
uint8_t byte = (uint8_t)(UART4->DR);
RING_push(&UART4_ringBuffer, byte);
}
}
Ring Buffer
For the software buffer, I used a ring buffer.
A ring buffer has:
data: fixed-size byte arrayhead: where data is popped fromtail: where new data is pushed
When head == tail, the buffer is empty. When moving tail forward would make it equal to head, the buffer is full.
typedef struct {
uint8_t data[RINGBUFFER_SIZE];
uint32_t head;
uint32_t tail;
} RINGBUFFER_t;
This is my implementation:
bool RING_push(RINGBUFFER_t *rb, uint8_t byte) {
uint32_t nextTail = (rb->tail + 1) % RINGBUFFER_SIZE;
if (nextTail == rb->head) {
return false;
}
rb->data[rb->tail] = byte;
rb->tail = nextTail;
return true;
}
Popping does the same thing from the head side:
bool RING_pop(RINGBUFFER_t *rb, uint8_t *byte) {
if (rb->head == rb->tail) {
return false;
}
*byte = rb->data[rb->head];
rb->head = (rb->head + 1) % RINGBUFFER_SIZE;
return true;
}
NVIC Setup
Adding the UART interrupt also meant I needed to deal with NVIC (nested vector interrupt controller).
For UART4, the IRQ number is 52:
#define IRQn_UART4 52
The driver enables the interrupt through helper functions:
NVIC_setPriority(irqn, priority);
NVIC_enableIRQ(irqn);
Testing with OpenOCD and GDB
I tested this with OpenOCD and GDB. Since the OpenOCD command is easy to forget, I usually make an alias:
alias openocd-stm32f407d='openocd -f interface/stlink.cfg -f target/stm32f4x.cfg'
Then I use another terminal for GDB:
arm-none-eabi-gdb firmware.elf
target remote :3333
monitor reset halt
load
break main
continue
For checking the ring buffer, GDB commands were really useful:
(gdb) define rb
>p UART4_ringBuffer.head
>p UART4_ringBuffer.tail
>x/16cb UART4_ringBuffer.data
>end
Example output:
$11 = 36
$12 = 40
0x20000004 <UART4_ringBuffer>: 111 'o' 104 'h' 101 'e' 108 'l'
This was a nice way to see that bytes were actually being received by the interrupt handler and stored in RAM.
Generalizing the Driver
After UART4 worked, I started making the driver usable for other UART/USART peripherals too.
The current initialization flow is:
UART_gpioInit(UART4, PIN('A', 0), PIN('A', 1));
UART_init(UART4, 115200, true, 5);
UART_gpioInit does the following:
- Checks if the requested TX/RX pins are valid for the selected UART.
- Enables the GPIO ports.
- Sets the pins to alternate function mode.
- Sets the correct AF number.
UART_init does the following:
- Enables the UART/USART peripheral clock.
- Clears
CR1,CR2, andCR3. - Sets
BRRfrom the baud rate. - Enables TX, RX, and UART.
- If requested, enables the RXNE interrupt and NVIC.
Right now, the baud rate calculation assumes a 16 MHz peripheral clock:
uint32_t pclk = 16000000;
uart->BRR = (pclk + (baudRate / 2U)) / baudRate;
This is good enough for the current setup, but if I change the clock tree later, I will need to update this so pclk comes from the actual APB bus clock.
UART Pin Table
I do not want to keep opening the datasheet every time I choose UART pins, so I made this table from the STM32F407 alternate function mapping.
| Peripheral | TX Pins | RX Pins | AF Number | APB Bus | RCC Enable Bit |
|---|---|---|---|---|---|
| USART1 | PA9, PB6 | PA10, PB7 | AF7 | APB2 | APB2ENR bit 4 |
| USART2 | PA2, PD5 | PA3, PD6 | AF7 | APB1 | APB1ENR bit 17 |
| USART3 | PB10, PC10, PD8 | PB11, PC11, PD9 | AF7 | APB1 | APB1ENR bit 18 |
| UART4 | PA0, PC10 | PA1, PC11 | AF8 | APB1 | APB1ENR bit 19 |
| UART5 | PC12 | PD2 | AF8 | APB1 | APB1ENR bit 20 |
| USART6 | PC6, PG14 | PC7, PG9 | AF8 | APB2 | APB2ENR bit 5 |
Current Status
The current loopback test sends "hello" through UART4 and waits for the same bytes to appear in the UART4 ring buffer:
uint8_t bytes[] = "hello";
uint32_t size = sizeof(bytes) - 1;
uint8_t buf[sizeof(bytes) - 1];
UART_transmitBytes(UART4, bytes, size);
for (uint32_t i = 0; i < size;) {
if (RING_pop(&UART4_ringBuffer, &buf[i])) {
i++;
}
}
If the received buffer matches the transmitted bytes, the green LED toggles. Otherwise, the red LED toggles.
That is enough for now: UART4 loopback works, interrupts are being used for RX, and the driver is in better shape for reuse later.
Things to Improve Later
- Use the real APB clock instead of assuming 16 MHz.
- Add timeout versions of blocking reads.
- Decide how to handle ring buffer overflow.
- Maybe add interrupt-driven TX too.
- Test the generalized driver on USART1, USART2, USART3, UART5, and USART6.
- Clean up the difference between
UART_transmitBufandUART_transmitBytes.