Bare-metal UART Driver for STM32F411CEx

Kunal Salvi
5 min readSep 1, 2021

Now print on the serial monitor from your microcontroller like a boss (or at least not like an amateur)

UART/USART is one of the most used communication protocols along with I2C and SPI. UART stands for Universal Asynchronous Receiver/Transmitter. S in USART stands for Synchronous.

I will not go into how the protocol works so feel free to go through this article to get a clear idea about what you’re dealing with. If you know already, let's start. STM32F411CEU6 is the board we’re using but this applies to all the STM32Fx devices. USART block in STM32Fx series packs a lot of similar protocols like LIN (local interconnection network), Smartcard Protocol, and IrDA (infrared data association). I’ll be tackling UART in this post, but be tuned for the rest.

UART frame consists of a start bit, data bits (8 or 9), and stop bits (0.5, 1, 1.5, 2). Data bits can be configured to be either 8 or 9 and stop bits to be 0.5, 1, 1.5, 2. The baud rate (actually the data rate) can be configured and dedicated registers are provided for the same. Hardware control can be enabled.

Fig 1: Word Length, RM0383 Page no: 509

Let’s configure the UART block first. The procedure to do so is listed down below.

  • Enable the USART by writing the UE bit in the USART_CR1 register to
  • Program the M bit in USART_CR1 to define the word length.
  • Program the number of stop bits in USART_CR2.
  • Select the desired baud rate using the USART_BRR register.
  • Set the TE bit in USART_CR1 to send an idle frame as the first transmission.
  • Write the data to send in the USART_DR register (this clears the TXE bit). Repeat this for each data to be transmitted in case of a single buffer.
  • After writing the last data into the USART_DR register, wait until TC=1. This indicates that the transmission of the last frame is complete. This is required for instance when the USART is disabled or enters the Halt mode to avoid corrupting the last transmission.

We’ll configure USART1 of STM32F411CEU6. Pins A9 are used as TX and A10 as RX. These pins need to be configured as alternate functional push-pull pins as shown below:

RCC -> AHB1ENR |= RCC_AHB1ENR_GPIOAEN;  
RCC -> APB2ENR |= RCC_APB2ENR_USART1EN;
GPIOA -> MODER |= (2 << 18) | (2 << 20); //9 -> tx 10 -> rx
GPIOA -> OTYPER |= (0 << 9) | (0 << 10);
GPIOA -> OSPEEDR |= (3 << 9) | (3 << 10);
GPIOA -> PUPDR |= (0 << 18) | (0 << 20);
GPIOA -> AFR[1] |= (7 << 4) | (5 << 8);

If hardware flow control is needed,

GPIOA -> MODER   |= (2 << 22)  | (2 << 24); //11 -> CTS 12->RTS   GPIOA -> OTYPER  |= (0 << 12)  | (0 << 11);   
GPIOA -> OSPEEDR |= (3 << 12) | (3 << 11);
GPIOA -> PUPDR |= (0 << 22) | (0 << 24);
GPIOA -> AFR[1] |= (7 << 12) | (5 << 16);

Now to configure the hardware block, make sure what variables you want to configure such as baud rate, data length, and stop bits. Most common settings are baud rate = 9600, data length = 8 and stop bits = 1. We’ll use the same to configure our block.

USART1 ->CR1 |= USART_CR1_UE; 
USART1 ->BRR = (int)(SystemCoreClock / (16*UART.baudrate)) << 4; USART1 -> CR2 &= ~USART_CR2_STOP;
USART1 -> CR3 &= ~USART_CR3_HDSEL;
USART1 ->CR1 |= USART_CR1_TE | USART_CR1_RE ;

Enable the USART1 block by setting the USART_CR1_UE bit in the USART1_CR1 register. To set the baud rate, use the formula below:

x = PClk / (16 * desired_baud_rate)
Pclk = peripheral clock fed to the hardware block, if USART1 is used it'll be 96 MHz.
Baud_rate = can be any of the acceptable values like 9600, 115200, etc

Shift the value you get by using the above formula and shift left 4 digits as the lower 4 places of the register USART_BRR is dedicated to the fraction. Put 0 in 12th place of the register USART_CR2 to configure the stop bits. Reset the HDSEL bit to configure the USART in full-duplex mode. Set the TE and RE bits to enable both transmission and reception of data. That’s all for the preliminary UART setup.

To transmit data on the TX line, write the USART_DR register with the data, set the USART_CR1_SBK bit in the USART_CR1 register, and wait till the USART_SR_TC is set.

USART1 ->DR = data; 
USART1 -> CR1 |= USART_CR1_SBK;
while((USART1->SR & USART_SR_TC) == 0);

To receive data on the RX line, wait till the USART_SR_RXNE bit is set. Once it is set, the data is available in the USART_DR register to be read.

while((USART1 ->SR & USART_SR_RXNE) == 0); 
c = USART1-> DR;

If you wish to enable the clock and turn the UART into USART, just set the USART_CR2_CLKEN bit in the USART_CR2 register.

USART1 -> CR2 |= USART_CR2_CLKEN;

Similarly, if you wish to use hardware flow control, you need to set the USART_CR3_CTSE and USART_CR3_RTSE bits in the USART_CR3 register.

USART1 -> CR3 |= USART_CR3_CTSE | USART_CR3_RTSE;

After you are done writing the code for configuration, you can print on your serial monitor as such:

char *d = "Hello World! My name is Kuna Salvi and welocome to my medium post \n\r";
int x = strlen(d);
for(int i = 0; i < x; i++)
{
USART1 ->DR = d[i];
USART1 -> CR1 |= USART_CR1_SBK;
while((USART1->SR & USART_SR_TC) == 0);
}
Fig 2: Snip of Putty.

I have uploaded an easy and ready-to-use driver on my Github. Make sure you fork it and follow.

Follow me on Medium at Kunal Salvi.

Instagram at Ziran_Daruwala and Blackshield Engineering.

For complete code and the latest version, check out my Github Page.

--

--

Kunal Salvi

Embedded Systems Engineer | Roboticist | Researcher | Innovator