Hello! Welcome to the first lesson of our fourth module, where we'll explore the essential world of serial communication.
In our last module, you successfully built a complete sense-process-actuate system, where an ultrasonic sensor's distance reading controlled an LED's brightness. While observing the LED gives you a high-level sense of what the system is doing, it's not very useful for detailed debugging or data logging. To become a professional firmware engineer, you need a way to see exactly what's happening inside the microcontroller.
This is where the Universal Asynchronous Receiver-Transmitter (UART) comes in. It's one of the most fundamental and widely used peripherals for communication, allowing your MCU to "talk" to a computer, another MCU, or various modules. Today, our learning outcome is to configure the UART peripheral and its corresponding GPIOs for serial communication. We will lay the foundation that the rest of this module will build upon.
1. What is UART?
UART stands for Universal Asynchronous Receiver-Transmitter. Let's break that down:
- Universal: It's a widely adopted standard.
- Asynchronous: Unlike protocols like SPI or I2C (which we'll cover later), there is no shared clock signal between the sender and receiver. They only need to agree on the communication speed (the baud rate) beforehand.
- Receiver-Transmitter: It can both send (transmit) and receive data, often simultaneously (which is called full-duplex communication).
Communication happens over just two wires:
- TX (Transmit): The line used for sending data.
- RX (Receive): The line used for receiving data.
A "frame" of data sent over UART typically consists of a start bit, 8 data bits, and a stop bit. This allows the receiver to synchronize with the incoming data without a clock signal.
For a formal definition and overview, you can start with this ST wiki page.
Getting started with UART - stm32mcu
This official STMicroelectronics article, 'Getting started with UART', provides a concise introduction to the UART peripheral's capabilities.
Read just the first section, '1. What is a Universal Asynchronous Receiver Transmitter (UART)?', to get a high-level overview of its features.
2. Hardware Connection: MCU to PC
To see the data from your STM32, you'll need a USB-to-UART (or USB-to-Serial) adapter, which acts as a bridge to your computer. Common adapters use chips like FT232, CP2102, or CH340.
Wiring:
A crucial point to remember is that the transmit pin of one device must connect to the receive pin of the other.
- Connect the adapter's
GNDto one of theGNDpins on your Blue Pill. - Connect the adapter's
RXD(Receive) pin to the STM32'sTXpin. - Connect the adapter's
TXD(Transmit) pin to the STM32'sRXpin.
For our STM32F103C8T6, the default pins for the USART1 peripheral are:
PA9=USART1_TXPA10=USART1_RX

Once wired, you can use a serial terminal program on your PC (like PuTTY, Tera Term, or the built-in serial monitor in VS Code) to view the data.
3. Bare-Metal Register Configuration
To truly understand how a peripheral works, we must configure it directly using its registers. This gives you complete control and a deep understanding that is invaluable, even when using higher-level tools.
The process involves two main steps: configuring the GPIO pins and then configuring the UART peripheral itself.
3.1. GPIO and Peripheral Clock & Pin Configuration
Before we can use USART1 or its GPIO pins (PA9, PA10), we must enable their clocks. Both GPIO Port A and USART1 are on the APB2 bus.
- Enable Clocks: We need to set the
IOPAEN(bit 2) andUSART1EN(bit 14) bits in theRCC->APB2ENRregister. - Configure GPIOs: The STM32F103 series uses the
CRL(pins 0-7) andCRH(pins 8-15) registers for GPIO configuration. Since we're usingPA9andPA10, we'll modifyGPIOA->CRH.- PA9 (TX): Must be configured as an Alternate Function output.
- PA10 (RX): Must be configured as an Input. The recommended mode is floating input.
The following article provides an excellent, detailed guide on how to perform this configuration, with a specific section for the F103 series.
How to Setup UART using Registers
The article 'How to Setup UART using Registers' from ControllersTech is a practical guide to bare-metal UART configuration. It clearly explains the registers involved.
Read the sections 'PIN Configuration' (paying close attention to the sub-section 'Pin Config in F103', as it applies directly to your board) and 'UART Configuration'. This will show you exactly which bits to set in the CRH and CR1 registers.
3.2. UART Parameter Configuration
With the pins ready, we now configure the UART peripheral's behavior. This involves setting the baud rate and other communication parameters in the UART's control registers.
To understand the purpose of each configuration bit (e.g., oversampling, word length, stop bits), the following video provides a very thorough explanation.
STM32 UART: Register Configuration, Transmission and Reception
This video, 'STM32 UART: Register Configuration, Transmission and Reception', explains the meaning behind the various control bits in great detail. It uses a different STM32 series, so the register names might differ slightly, but the concepts are universal.
Watch the video from 03:16 to 15:59. Focus on understanding the purpose of the settings in Control Register 1 (word length, oversampling), Control Register 2 (stop bits), and the Baud Rate Register. Note: Use the code from the 'ControllersTech' article for your implementation, as this video's code is for a different MCU family. The value here is in the conceptual explanation.
A key step is calculating the value for the Baud Rate Register (USART_BRR). The formula is:
where is the peripheral clock frequency (for USART1, this is PCLK2, which is 72 MHz in our setup). USARTDIV is the number we need to find and load into the BRR register.
Rearranging for USARTDIV:
For a standard baud rate of 115200:
The BRR register stores this value as a fixed-point number: a 12-bit mantissa and a 4-bit fraction.
- Mantissa:
39(decimal) =0x27 - Fraction:
0.0625 * 16 = 1(decimal) =0x1
The final BRR value is (Mantissa << 4) | Fraction, so (0x27 << 4) | 0x1 = 0x271.
4. Hands-On: Writing the UART Init Function
Now, let's apply this knowledge. Create a new project in STM32CubeIDE and write a function UART1_Init() that performs all the register-level configurations we've discussed. Place the call to this function in main() before the while(1) loop.
Here are the key register manipulations to guide you. Try to write the full function yourself based on the resources.
void UART1_Init(void) {
// 1. Enable clocks for GPIOA and USART1
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_USART1EN;
// 2. Configure PA9 (TX) as Alternate Function Push-Pull
// CNF9[1:0] = 10 (AF Push-pull), MODE9[1:0] = 11 (50MHz output)
GPIOA->CRH &= ~(GPIO_CRH_CNF9 | GPIO_CRH_MODE9); // Clear current settings
GPIOA->CRH |= GPIO_CRH_CNF9_1 | GPIO_CRH_MODE9_0 | GPIO_CRH_MODE9_1;
// 3. Configure PA10 (RX) as Floating Input
// CNF10[1:0] = 01 (Floating input), MODE10[1:0] = 00 (Input)
GPIOA->CRH &= ~(GPIO_CRH_CNF10 | GPIO_CRH_MODE10); // Clear current settings
GPIOA->CRH |= GPIO_CRH_CNF10_0;
// 4. Configure UART parameters
// Set baud rate to 115200
USART1->BRR = 0x271; // 39.0625 -> Mantissa=39, Fraction=1 -> 0x271 for 72MHz
// 5. Enable UART (UE), Transmitter (TE), and Receiver (RE)
USART1->CR1 |= USART_CR1_UE | USART_CR1_TE | USART_CR1_RE;
}
5. The Modern Workflow: STM32CubeIDE
While bare-metal configuration provides a robust foundation, in many professional settings, you'll use tools like STM32CubeIDE to accelerate development. The graphical configurator handles all the register setup for you.
Let's see how to achieve the same result in just a few clicks.
22. STM32CubeIDE UART between STM32F103C8T6
This short video demonstrates how quickly you can configure UART using the STM32CubeIDE graphical interface.
Watch from 02:37 to 03:37. This shows the exact steps: enabling the system debug interface, selecting USART1 in Asynchronous mode, and setting the baud rate. Note that it sets the baud rate to 9600; for our purposes, you should set it to 115200.
The process is:
- In the
.iocfile, go toConnectivityand selectUSART1. - Set the
ModetoAsynchronous. - In the
Parameter Settingstab, set theBaud Rateto115200. Leave the other settings at their defaults (8 data bits, no parity, 1 stop bit). - CubeIDE will automatically highlight
PA9andPA10and configure them correctly.

After you save the .ioc file, CubeIDE generates the MX_USART1_UART_Init() function, which uses the HAL (Hardware Abstraction Layer) to perform the same low-level configuration. It's a great exercise to look through this generated function and see how it corresponds to the bare-metal code you wrote.
Conclusion
In this lesson, you've learned the essential first step of using a vital communication peripheral. You now understand what UART is, how to connect it, and how to configure it at both the fundamental register level and with modern code-generation tools.
Key Takeaways:
- UART is an asynchronous serial communication protocol that is indispensable for debugging and interfacing with other devices.
- Configuring UART involves enabling clocks for the peripheral and its GPIOs, setting the GPIO pins to their alternate function modes, and setting communication parameters like the baud rate and word length in the UART's registers.
- Bare-metal register access provides deep understanding, while STM32CubeIDE provides speed and convenience. A professional engineer knows how and when to use both.
Preview of the Next Lesson:
With the UART peripheral now fully configured and ready, our next step is to put it to work. In the next lesson, you will learn how to implement blocking (polling-based) functions to transmit and receive data over UART, allowing your MCU to finally send its first "Hello, World!" to your computer.