Embedded C track

Lesson 19 · Part 3: STM32 (Keil MDK-ARM / STM32F103C8, direct register control)

USART Serial Communication

BRR and TXE/RXNE

Part 3: STM32 (Keil MDK-ARM / STM32F103C8, direct register control)6 min readFree lesson

Until now, results were only observed via LEDs and pins, but now the chip communicates with a person or PC in text. UART (Universal Asynchronous Receiver/Transmitter) is the oldest and most widely used communication method, used everywhere from debug message output, sensor module communication, to bootloaders. STM32's USART includes UART functionality and performs two-way communication with only two wires (TX/RX).

In this chapter, we set USART1 to 9600bps to send a greeting string and implement an echo, returning any received characters as they are. You can directly check the transmitted and received text in the µVision simulator’s Serial window, making UART the easiest peripheral to verify with the simulator.

Learning Objectives

  • Understand the principles of UART asynchronous communication and the frame (8N1).
  • Set the pins (PA9/PA10) and clock for USART1.
  • Calculate and set the baud rate with BRR.
  • Implement polling-based transmission and reception with the TXE/RXNE flags.
  • Verify transmission and reception in the µVision Serial window.

Target Environment

  • Compiler: Keil MDK-ARM (Arm Compiler 6).
  • Device: STM32F103C8 (μVision simulator).
  • Verification: µVision simulator (View → Serial Windows → UART #1). BRR calculation on PC gcc(pc_test.c).

Key Concepts

1) UART Asynchronous Communication

UART transmits and receives data at a pre-agreed speed (baud rate) without a clock line. The transmitter TX connects to the receiver RX (crossed), and they share GND. If the speeds don’t match, characters get corrupted.

2) Frame — 8N1

The most common setting is 8N1: 8 data bits, no parity (None), 1 stop bit. One character is transmitted as start bit (1) + data (8) + stop bit (1) = 10 bits.

3) USART1 Pins and Clock

Signal Pin Mode
TX PA9 AF Push-Pull (0xB)
RX PA10 Floating Input (0x4)

USART1 is on the APB2 bus, so turn it on via RCC_APB2ENR_USART1EN. Also, enable the AFIO clock for pin remapping.

4) Baud Rate Setting — BRR

BRR = fCK / baud

The fCK of USART1 is PCLK2 (=72MHz). For 9600bps, write 72000000 / 9600 = 7500 to BRR.

5) Transmission and Reception — Polling

  • Transmit: Wait for SR TXE (transmit buffer empty) and then write to DR.
  • Receive: Check SR RXNE (receive buffer full) and read from DR (reading automatically clears RXNE).
while (!(USART1->SR & USART_SR_TXE)) { }   /*Standby to send*/
USART1->DR = c;                            /*Send one character*/

Creating a μVision Project

  1. Project → New µVision Project… → name it stm32_usart.
  2. Device: STM32F103C8.
  3. Manage Run-Time Environment → CMSIS:CORE, Device:Startup.
  4. After adding main.c, copy and paste.
  5. Apply STM32F10X_MD to C/C++ → Define.
  6. Debug → Use Simulator, -pSTM32F103C8.
  7. Rebuild(F7) → Debug(Ctrl+F5). Open View → Serial Windows → UART #1.

Examples

Example 1 — main.c: Greeting + Echo

#include "stm32f10x.h"

/*
 *Lesson 19 Example — USART1 Transmission + Echo.
 *Target: STM32F103C8, Compiler: Keil MDK-ARM.
 *
 *USART1: TX=PA9, RX=PA10 (APB2 Bus). 9600 8N1.
 *Send a greeting string at startup, then echo back received characters.
 *Baud rate divider BRR = fCK / baud, fCK of USART1 is PCLK2(=72MHz).
 *   BRR = 72000000 / 9600 = 7500
 */

#define BAUD_BRR   7500u   /* 72MHz / 9600 */

static void usart1_init(void)
{
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_AFIOEN | RCC_APB2ENR_USART1EN;

    /*PA9 = TX: AF push-pull (0xB), PA10 = RX: floating input (0x4). Both CRH.*/
    GPIOA->CRH &= ~((0xFu << ((9u - 8u) * 4u)) | (0xFu << ((10u - 8u) * 4u)));
    GPIOA->CRH |=  (0xBu << ((9u - 8u) * 4u));    /*PA9 AF push-pull*/
    GPIOA->CRH |=  (0x4u << ((10u - 8u) * 4u));   /*PA10 floating input*/

    USART1->BRR = BAUD_BRR;                        /* 9600 baud */
    USART1->CR1 = USART_CR1_TE | USART_CR1_RE | USART_CR1_UE;  /*Transmit/Receive + Enable*/
}

static void usart1_send_char(char c)
{
    while (!(USART1->SR & USART_SR_TXE)) { }      /*Wait until the transmit buffer is empty*/
    USART1->DR = (uint16_t)c;
}

static void usart1_send_str(const char *s)
{
    while (*s) {
        usart1_send_char(*s++);
    }
}

int main(void)
{
    usart1_init();
    usart1_send_str("Hello, STM32 UART!\r\n");

    while (1) {
        if (USART1->SR & USART_SR_RXNE) {          /*Is there received data?*/
            char c = (char)USART1->DR;             /*Reading automatically clears RXNE*/
            usart1_send_char(c);                   /*Echo as is*/
        }
    }
}

Example 2 — pc_test.c: BRR Calculation

#include <stdio.h>
#include <stdint.h>

/*
 *Lecture 19 PC Check — USART BRR (Baud Rate Register) Calculation.
 *BRR = fCK / baud (for USART1, PCLK2 = 72MHz).
 *If you truncate to an integer, the actual baud rate becomes fCK/BRR, so a slight error can occur.
 *(72MHz divides well into standard baud rates, so the error is 0.)
 */

static void report(uint32_t fck, uint32_t baud)
{
    uint32_t brr    = fck / baud;
    uint32_t actual = fck / brr;
    printf("baud %lu -> BRR=%lu (actual %lu)\n",
           (unsigned long)baud, (unsigned long)brr, (unsigned long)actual);
}

int main(void)
{
    uint32_t fck = 72000000u;
    report(fck, 9600u);
    report(fck, 19200u);
    report(fck, 38400u);
    report(fck, 57600u);
    report(fck, 115200u);
    return 0;
}

Checking with Simulator

  • Open View → Serial Windows → UART #1 after starting debug.
  • When you run (F5), Hello, STM32 UART! will appear in the UART #1 window.
  • If you type characters in the UART #1 window (delivered to RX), the same characters echo back.
  • Check via register whether USART1->BRR is 7500 (0x1D4C) or whether CR1's UE/TE/RE-is is 1.
  • Expected behavior: After printing the greeting string, the input character is returned as it was.

Verification on a PC

gcc pc_test.c -o pc_test && ./pc_test

Expected output (stdout):

baud 9600 -> BRR=7500 (actual 9600)
baud 19200 -> BRR=3750 (actual 19200)
baud 38400 -> BRR=1875 (actual 38400)
baud 57600 -> BRR=1250 (actual 57600)
baud 115200 -> BRR=625 (actual 115200)

Common Mistakes

Q. The characters are garbled (□□□).

A. Baud rate mismatch is the most common. Check if the fCK used in BRR calculation matches the actual clock (USART1 has PCLK2=72MHz) and if the terminal baud rate is the same.

Q. Only the first character appears and then it stops.

A. If you write continuously to DR without waiting for TXE, it overwrites the previous character. Wait with while(!(SR & TXE)) before each character.

Q. Echo is not working.

A. Check whether RE (Receiver Enable) is turned on in CR1, whether RXNE is checked, and whether DR is read. RXNE is cleared and the next reception is possible only after reading DR.

Q. I set PA9/PA10 to general input/output.

A. TX (PA9) must be AF push-pull (0xB), RX (PA10) must be input for USART signals to be connected to the pins. If set as general output, USART cannot use the pins.

Summary

  • UART communicates asynchronously with two lines (TX/RX) that match the baud rate (8N1 is common).
  • USART1 is PA9 (TX)/PA10 (RX), APB2 bus, BRR=fCK/baud.
  • Transmission: wait for TXE then write DR; Reception: check RXNE then read DR.
  • Check transmitted and received text directly using the simulator UART #1 window.
  • TX must be set to AF push-pull.

Try it yourself

Solve the assignment below. The reference solution is shown below.

Example files

The runnable files used in this lesson. Copy them into your own project and experiment.

src/stm32_usart/main.cC
#include "stm32f10x.h"

/*
 *Lesson 19 Example — USART1 Transmission + Echo.
 *Target: STM32F103C8, Compiler: Keil MDK-ARM.
 *
 *USART1: TX=PA9, RX=PA10 (APB2 Bus). 9600 8N1.
 *Send a greeting string at startup, then echo back received characters.
 *Baud rate divider BRR = fCK / baud, fCK of USART1 is PCLK2(=72MHz).
 *   BRR = 72000000 / 9600 = 7500
 */

#define BAUD_BRR   7500u   /* 72MHz / 9600 */

static void usart1_init(void)
{
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_AFIOEN | RCC_APB2ENR_USART1EN;

    /*PA9 = TX: AF push-pull (0xB), PA10 = RX: floating input (0x4). Both CRH.*/
    GPIOA->CRH &= ~((0xFu << ((9u - 8u) * 4u)) | (0xFu << ((10u - 8u) * 4u)));
    GPIOA->CRH |=  (0xBu << ((9u - 8u) * 4u));    /*PA9 AF push-pull*/
    GPIOA->CRH |=  (0x4u << ((10u - 8u) * 4u));   /*PA10 floating input*/

    USART1->BRR = BAUD_BRR;                        /* 9600 baud */
    USART1->CR1 = USART_CR1_TE | USART_CR1_RE | USART_CR1_UE;  /*Transmit/Receive + Enable*/
}

static void usart1_send_char(char c)
{
    while (!(USART1->SR & USART_SR_TXE)) { }      /*Wait until the transmit buffer is empty*/
    USART1->DR = (uint16_t)c;
}

static void usart1_send_str(const char *s)
{
    while (*s) {
        usart1_send_char(*s++);
    }
}

int main(void)
{
    usart1_init();
    usart1_send_str("Hello, STM32 UART!\r\n");

    while (1) {
        if (USART1->SR & USART_SR_RXNE) {          /*Is there received data?*/
            char c = (char)USART1->DR;             /*Reading automatically clears RXNE*/
            usart1_send_char(c);                   /*Echo as is*/
        }
    }
}
src/stm32_usart/pc_test.cC
#include <stdio.h>
#include <stdint.h>

/*
 *Lecture 19 PC Check — USART BRR (Baud Rate Register) Calculation.
 *BRR = fCK / baud (for USART1, PCLK2 = 72MHz).
 *If you truncate to an integer, the actual baud rate becomes fCK/BRR, so a slight error can occur.
 *(72MHz divides well into standard baud rates, so the error is 0.)
 */

static void report(uint32_t fck, uint32_t baud)
{
    uint32_t brr    = fck / baud;
    uint32_t actual = fck / brr;
    printf("baud %lu -> BRR=%lu (actual %lu)\n",
           (unsigned long)baud, (unsigned long)brr, (unsigned long)actual);
}

int main(void)
{
    uint32_t fck = 72000000u;
    report(fck, 9600u);
    report(fck, 19200u);
    report(fck, 38400u);
    report(fck, 57600u);
    report(fck, 115200u);
    return 0;
}

Assignment

Problem 1 is pure logic (PC gcc), Problem 2 is MDK (STM32F103C8, μVision simulator).


Problem 1 — Convert a Byte to Two Hexadecimal ASCII Characters

  • File name: homework_01/main.c
  • Key Concept: Nibble → Hex Character, UART Debug Output Helper
  • Platform: Common (PC gcc)

Requirements

  • Convert a byte value to a two-character uppercase hexadecimal (e.g., 0x2A → "2A") and output it.
  • Input byte is {0x00, 0x2A, 0xFF, 0x7E, 0xA5}.
  • Format 0xNN -> "XX" should match the expected output character by character (both two uppercase characters).

Expected output

0x00 -> "00"
0x2A -> "2A"
0xFF -> "FF"
0x7E -> "7E"
0xA5 -> "A5"

Hint

  • High nibble (b >> 4) & 0xF, Low nibble b & 0xF.
  • Nibble → Character: n < 10 ? '0'+n : 'A'+(n-10).
  • It's convenient to match both hex digits in the output format to %02X.

Problem 2 — Convert Lowercase to Uppercase and Echo

  • File name: homework_02/main.c
  • Key Concept: USART Receive/Send, Character Conversion
  • Platform: MDK (STM32F103C8)

Requirements

  • Initialize USART1 to 9600 8N1, and send READY\r\n at the start.
  • If the received character is a lowercase letter (a~z), convert it to uppercase and echo it; otherwise, echo it as-is.

Expected Operation (Simulator)

  • READY is first output in UART #1 window.
  • Input abc123 will echo ABC123 (only lowercase letters are converted to uppercase).

Hint

  • Lowercase check: if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';.
  • Use the example usart1_init / usart1_send_char as-is.

Check your answers

After solving it yourself, compare it with the reference solution below. The solution files include comments on key points and common mistakes.

Show the reference solution

Try the assignment yourself first — comparing your attempt with a working solution is where most of the learning happens.

homework_01/main.cC
#include <stdio.h>
#include <stdint.h>

/*
 *Answer 1 — Convert a byte into two hexadecimal ASCII characters
 *
 *Key Points:
 *- Divide one byte into high and low nibbles (4 bits each) and convert each to a hexadecimal character.
 *- For nibbles 0~9 use '0'+n, for 10~15 use 'A'+(n-10).
 *- A pattern often used when sending numbers over UART for human reading.
 *
 *Common mistake:
 *- If '0'+n is used directly instead of 'A' for 10~15, garbled characters can appear.
 */

static char nibble_to_hex(unsigned int n)
{
    return (n < 10u) ? (char)('0' + n) : (char)('A' + (n - 10u));
}

int main(void)
{
    unsigned char bytes[5] = { 0x00u, 0x2Au, 0xFFu, 0x7Eu, 0xA5u };
    int i;

    for (i = 0; i < 5; i++) {
        unsigned char b = bytes[i];
        char hi = nibble_to_hex((b >> 4) & 0xFu);
        char lo = nibble_to_hex(b & 0xFu);
        printf("0x%02X -> \"%c%c\"\n", b, hi, lo);
    }
    return 0;
}
homework_02/main.cC
#include "stm32f10x.h"

/*
 *Correct answer 2 (MDK, STM32F103C8) — Convert lowercase letters to uppercase and echo
 *
 *Key Points:
 *- If the received character is 'a'~'z', convert to uppercase and then echo; otherwise, leave as is.
 *- Conversion formula: c - 'a' + 'A' (using the fact that the ASCII gap between lowercase/uppercase is constant).
 *
 *Common mistake:
 *- Applying -32 to all characters without range check, breaking numbers/symbols as well.
 *
 *Expected behavior (simulator): Output READY on UART #1, then echo input lowercase letters as uppercase.
 */

#define BAUD_BRR   7500u   /* 72MHz / 9600 */

static void usart1_init(void)
{
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_AFIOEN | RCC_APB2ENR_USART1EN;

    GPIOA->CRH &= ~((0xFu << ((9u - 8u) * 4u)) | (0xFu << ((10u - 8u) * 4u)));
    GPIOA->CRH |=  (0xBu << ((9u - 8u) * 4u));    /*PA9 TX AF Push-Pull*/
    GPIOA->CRH |=  (0x4u << ((10u - 8u) * 4u));   /*PA10 RX Floating input*/

    USART1->BRR = BAUD_BRR;
    USART1->CR1 = USART_CR1_TE | USART_CR1_RE | USART_CR1_UE;
}

static void usart1_send_char(char c)
{
    while (!(USART1->SR & USART_SR_TXE)) { }
    USART1->DR = (uint16_t)c;
}

static void usart1_send_str(const char *s)
{
    while (*s) {
        usart1_send_char(*s++);
    }
}

int main(void)
{
    usart1_init();
    usart1_send_str("READY\r\n");

    while (1) {
        if (USART1->SR & USART_SR_RXNE) {
            char c = (char)USART1->DR;
            if (c >= 'a' && c <= 'z') {
                c = (char)(c - 'a' + 'A');   /*Lowercase → Uppercase*/
            }
            usart1_send_char(c);
        }
    }
}

Learn this lesson with a mentor

Work through the Embedded C track on your own, or join a guided cohort at our Pitampura campus and online.

Call NowEnroll Now