STM32F103C6T6A Bare Metal Programming – UART

In my previous post, Mastering UART: A Comprehensive Overview, I delved into the protocol-level intricacies of UART communication. If you haven’t had a chance to read it, I recommend doing so to get a solid foundation on how UART works. In this blog post, I’ll be diving straight into hands-on programming with the STM32F103C6T6A microcontroller. Without further ado, let’s get started with the practical aspects of UART in bare metal programming.

Exercise

Write a driver for the STM32F103C6T6 to receive data from another UART device (such as a PC or another microcontroller). When the STM32 receives a carriage return (\r), it should send back the data received up to that point.

Solution

Pin Selection

STM32F103C6T6 microcontroller has two USART module. USART1 and USART2. In this post we are dealing with the asynchronous communication. So now onwards, we will use UART instead of USART. We can take any UART module, so I am taking UART1. If you check the table 5 in the datasheet, you can see that the PA9 is the UART1 TX pin and PA10 is the UART1 RX pin. When we are communicating the STM32 device via UART, one thing we need to ensure is that, we need to share the common ground across both the devices.

Registers Used
  • APB2 peripheral clock enable register (RCC_APB2ENR) – Enable clock for UART and GPIOA
  • Baud rate register (USART_BRR) – Configure the baud rate of UART communication
  • PA Control Register High (GPIOA_CRH) – Configure transmitter and receive pins
  • Control register 1 (USART_CR1) – Enable UART module and transmit and receive modes
  • Status register (USART_SR) – UART status register
  • Data register (USART_DR) – UART data register
Code Implementation

As usual, initially we are creating the UART setup function. The core part of the UART setup is decide and configure the required baud rate. In this example I am creating the UART transceiver which use 2400 bps baud rate. I am using the internal RC oscillator which is accurate enough to run the UART with this baud rate.

For calculating the USART_BRR value, first we need to find the USARTDIV value. This value can be calculated using the following equation

USARTDIV = Fclk / (16 * Baud Rate) = 8000000/(16 * 2400) = 208.33333… ~= 208.33

Now we have to convert this value to USART_BRR register value. For that we will take the hex value of the mantissa 208 = 0xD0.

Next step multiply the fraction value with 16 and approximate to nearest integer.

0.33 * 16 = 5.08 ~= 5

So the USART_BRR value will be 0xD05.

Next, I am using a variable to set what are the modes we need to enable. Based on the argument passed to the setup function, the following bits will be set in the variable.

  • USART_CR1_UE – To enable the USART module
  • USART_CR1_TE – To enable transmit mode
  • USART_CR1_RE – To enable reception mode

Finally set this value to USART1_CR1 register.

// Enable clock to USART1, Alternate function IO and GPIOA
RCC->APB2ENR |= (RCC_APB2ENR_USART1EN | RCC_APB2ENR_IOPAEN);

// Baud rate 2400 @ 8MHz clock frequency
USART1->BRR = 0xD05;
if(0 != uart_mode) {
    // Global UART enable if either Tx or Rx enabled.
    uart1_cr1_flags |= USART_CR1_UE;

    // Enable Transmit mode if required
    if(uart_mode & UART_TX_ENABLE) {
        uart1_cr1_flags |= USART_CR1_TE;

        // Configure PA9 as output; 10MHz max; push pull
        GPIOA->CRH &= ~(GPIO_CRH_CNF9 | GPIO_CRH_MODE9);
        GPIOA->CRH |= (GPIO_CRH_MODE9_0 | GPIO_CRH_CNF9_1);
    }

    // Enable Receive mode if required
    if(uart_mode & UART_RX_ENABLE) {
        uart1_cr1_flags |= USART_CR1_RE;

        // Configure PA10 input pull-up/pull-down
        GPIOA->CRH &= ~(GPIO_CRH_CNF10 | GPIO_CRH_MODE10);
        GPIOA->CRH |= GPIO_CRH_CNF10_1;
    }
}

// Enable UART and required mode
USART1->CR1 |= uart1_cr1_flags;

Now the UART setup has been completed. Next we have to implement a function to send a char to remote. In transmit mode the data written into the USART1_DR, will be transferred into a shift register and then it will sent out serially. When the data written into the USART1_DR the TXE bit in the USART1_SR will be set and when the transferred to the shift register, the TXE bit will be cleared. So the procedure to receive a byte is, wait until the TXE bit become cleared and then set the byte in the USART1_DR.

while((USART1->SR & USART_SR_TXE) == 0) {}
USART1->DR = ch;

Similarly for reception, when the shift register data has been transferred to USART1_DR, RXNE bit will set. The application should clear this flag once the data taken from the USART1_DR register

// Wait until new data arrive
while((USART1->SR & USART_SR_RXNE) == 0) {}

// Take the data;
data = USART1->DR;

// CLear the received data flag
USART1->SR &= ~USART_SR_RXNE;

Now we have to send a string or receive a string via UART, but it’s simply C programming logic and nothing related to the microcontroller, so I am not delving to that area. I have implemented the function to send a string through UART using variable length argument and another function to receive until the \r char. You can go and check my github repository for complete source code.

Leave a comment