UART Bare-Metal Driver Phase 1: Clock Init and GPIO Configuration
1. Setting up Driver is setting up registers
90% of building an UART driver task is setting up registers.
- RCC (Reset and Clock Control) — specifically the APB1/APB2 peripheral clock enable registers
- GPIO — the MODER, AFRL/AFRH registers
- USART — the CR1, CR2, BRR registers
| ![[diagram-registers.jpeg | 500]] |
Code Template
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void uart_init(void)
{
// Step 1: Enable clocks
RCC->AHB1ENR ...
RCC->APB2ENR ...
// Step 2: Configure PA9, PA10 as Alternate Function mode
// PA9 → bits 19:18 in MODER
GPIOA->MODER ...
GPIOA->MODER ...
// PA10 → bits 21:20 in MODER
GPIOA->MODER ...
GPIOA->MODER ...
// Step 3: Assign AF7 (USART1) to PA9 and PA10 via AFRH
// PA9 → bits 7:4 in AFRH
GPIOA->AFR[1] ...
GPIOA->AFR[1] ...
// PA10 → bits 11:8 in AFRH
GPIOA->AFR[1] ...
GPIOA->AFR[1] ...
}
Now looking back on the project, initializing UART task can be simplified into setting up each proper register values. Now the question is: where should I find the answer from? Every answer is in a datasheet, and that’s why bare-metal project requires reading datasheet skills.
Side Note: Register Bit Manipulation Pattern
Never use = to write a peripheral register. It clears all other bits. Always use the clear-then-set pattern:
1
2
3
4
5
// 1. Clear only the target bits
REGISTER &= ~(MASK << POSITION);
// 2. Set target bits to desired value
REGISTER |= (VALUE << POSITION);
2. The Clock
For the UART driver, the very first thing to enable is the clock signal to the peripheral. Every peripheral is off by default. Without a clock signal, the device is completely inert, so writing registers does nothing.
On STM32, this is controlled by the RCC (Reset and Clock Control) registers. Specifically,
- USART1 lives on the APB2 bus → you enable it via
RCC->APB2ENR - PA9/PA10 are on GPIO Port A → you enable it via
RCC->AHB1ENR
In p.190 of Datasheet UM0090, RCC APB2 peripheral clock enable register (RCC_APB2ENR):
1
2
3
4
Bit 4 USART1EN: USART1 clock enable
This bit is set and cleared by software.
0: USART1 clock disabled
1: USART1 clock enabled
Therefore, if it is translated into code:
1
2
3
4
5
void uart_init(void)
{
// Step 1: Enable clocks
RCC->AHB1ENR |= (1 << 0); // Enable GPIOA clock
RCC->APB2ENR |= (1 << 4); // Enable USART1 clock
Or using CMSIS definitions RCC_AHB1ENR_GPIOAEN or RCC_APB2ENR_USART1EN, which are provided by stm32f429xx.h:
1
2
3
4
5
void uart_init(void)
{
// Step 1: Enable clocks
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN; // Enable GPIOA clock
RCC->APB2ENR |= RCC_APB2ENR_USART1EN; // Enable USART1 clock
Some side notes:
- AHB stands for Advanced High Performance Bus, a high-speed bus designed for high-bandwidth components (CPU, DMA, Memory).
- APB stands for Advanced Peripheral Bus. AHB is , a lower-speed, low-power bus designed for peripherals that don’t need high data rates (UART, Timers, I2C).
- GPIOs sit on AHB1 because they need fast response times. USART1 doesn’t need that speed so it lives on APB2.
- This clock register initialization must be done BEFORE touching any peripheral register — GPIO or USART register.
3. GPIO Mode Configuration
Next step is to set GPIO MODER registers. Before jumping into GPIO register configuration, we need to know “GPIO mode”.
GPIO pins on STM32 have four possible modes:
- Input
- Output
- Alternate Function
- Analog
Let’s look at p.270 of Datasheet RM0090:
| ![[GPIO-functional-description.png | 750]] |
3.1. What Is Alternate Function?
It is like telling a pin (say PA9) to redirect: “You belong to USART1 now, not GPIO.” In other words, it configures General-Purpose Input/Output (GPIO) pins to be controlled by internal peripherals rather than as general-purpose I/O.
1
2
3
4
5
┌─── TIM1_CH2
├─── I2C3_SMBA
Physical Pin ─┤─── USART1_TX ← you select this
├─── DCMI_D0
└─── EVENTOUT
A pin on a microcontroller is a physical wire coming out of the chip. But inside the chip, multiple peripherals might want to use that same wire. The alternate function system is essentially a multiplexer — a hardware switch that connects the physical pin to one internal peripheral at a time. So if I select AF7 (which is for USART1), it will be like closing the hardware switch that connects the pin to USART1’s TX line.
3.2. Finding the Right Pins and AF Number
Since an user can select which one via the AF number (AF0–AF15) in the AFRH/AFRL registers, we need to know which AF number to select for USART1. Let’s go back to the datasheets RM0090 and UM1670 carefully, and answer the following questions:
- What is the pin for USART1 Transmit (USART1_TX) in STM32F429?
- What is the pin for USART1 Receive (USART1_RX) in STM32F429?
-> The answer is in UM1670 Datasheet, p.22:
| ![[USART-pins.jpg | 500]] |
So we found out that the pins for USART1 is PA9 and PA10:
- PA9 → USART1_TX
- PA10 → USART1_RX
Then the next question is: What is the alternate function pin for USART1? Again, the answer is Datasheet RM0090: For USART1, the alternate function pin is AF7.
![[Alternate-Function-STM32F42xxx.jpg]]
3.3. How to Implement in Code?
In p.284 of Datasheet RM0090:
| ![[GPIO_MODER-bits.png | 750]] |
It says:
1
2
3
4
5
6
7
Bits 2y:2y+1 MODERy[1:0]: Port x configuration bits (y = 0..15)
These bits are written by software to configure the I/O direction mode.
00: Input (reset state)
01: General purpose output mode
10: Alternate function mode ← we will select this for UART
11: Analog mode
The GPIO mode can be configured in GPIO port mode register GPIOx_MODER, 2 bits per pin. There are 16 bit fields named MODER0, MODER1… MODER15, each bit field having 2 bits (total 32 bits = one register).
To summarize, each pin gets 2 bits in the MODER register. For PA9 and PA10,
- PA9 is controlled by bits 19:18 (2×9+1 : 2×9)
- PA10 is controlled by bits 21:20 (2×10+1 : 2×10)
Translated into code:
1
2
3
4
5
6
7
8
9
10
void uart_init(void)
{
...
// Step 2: Configure PA9, PA10 as Alternate Function mode
// PA9 → bits 19:18 in MODER
GPIOA->MODER &= ~(0x3 << 18); // clear
GPIOA->MODER |= (0x2 << 18); // set AF mode (10)
// PA10 → bits 21:20 in MODER
GPIOA->MODER &= ~(0x3 << 20); // clear
GPIOA->MODER |= (0x2 << 20); // set AF mode (10)
Breaking that down:
-
0x3is11in binary — a 2-bit mask. -
<< 18shifts it to the PA9 position (bits 19:18) -
~is bitwise NOT operator, which inverts all bits (0 to 1, 1 to 0). So you get all 1s except bits 19:18 -
&=clears only those two bits, leaving everything else untouched
3.4. Side Note: GPIO Port vs Pin (x vs y)
- x = Port (e.g., A, B, C…) is a separate hardware block managing 16 pins.
-
GPIOA,GPIOBare at different memory addresses
-
- y = Pin number within that port (0–15)
→ PA9 = Port A, Pin 9
4. AF Number Assignment
You still need to tell the hardware which alternate function PA9 and PA10 are mapped to. Setting MODER to 10 only says “this pin belongs to a peripheral”. It doesn’t yet say which peripheral to select. And that is done through a separate register called AFRL or AFRH (Alternate Function Low/High registers).
Then what’s the difference between AFRH and AFRL? The split is simple:
- AFRL handles PA0–PA7
- AFRH handles PA8–PA15 GPIOx_AFRL (Low) typically configures pins 0–7, while GPIOx_AFRH (High) configures pins 8–15. Each pin uses 4 bits, allowing 16 different potential functions
Each pin takes 4 bits in AFRH. The bit positions are:
- PA9 → bits 7:4 in AFRH (pin 9 − 8 = position 1, × 4 = bit 4)
- PA10 → bits 11:8 in AFRH (pin 10 − 8 = position 2, × 4 = bit 8)
You want to write 0x7 (which is 0111 in binary = AF7) into each position.
Using the same clear-then-set pattern you already know, try writing both lines for PA9 and PA10 in AFRH.
Hint: The register is GPIOA->AFR[1]
So both PA9 and PA10 use GPIOA->AFR[1] (which is AFRH). Each pin gets 4 bits in the AFR register, allowing values AF0–AF15.
4.1. AFRH Register (GPIOx_AFRH)
- Controls pins 8–15 of a port (AFRL controls pins 0–7)
- Each pin occupies 4 bits → allows AF0–AF15
- Register:
GPIOA->AFR[1](index 1 = AFRH) - Bit position formula: (pin − 8) × 4
- PA9 → (9−8) × 4 = bit 4
- PA10 → (10−8) × 4 = bit 8
| ![[GPIOx_AFRH.png | 750]] |
4.2. AFRL Register (GPIOx_AFRL)
| ![[GPIOx_AFRL.png | 750]] |
1
2
3
4
5
6
7
8
9
10
void uart_init(void)
{
...
// Step 3: Assign AF7 (USART1) to PA9 and PA10 via AFRH
// PA9 → bits 7:4 in AFRH
GPIOA->AFR[1] &= ~(0xF << 4); // clear
GPIOA->AFR[1] |= (0x7 << 4); // AF7
// PA10 → bits 11:8 in AFRH
GPIOA->AFR[1] &= ~(0xF << 8); // clear
GPIOA->AFR[1] |= (0x7 << 8); // AF7
x.8 The NVIC (Nested Vector Interrupt Controller)
- Hardware block that manages which interrupts are enabled and at what priority
- Acts as a gatekeeper — even if USART1 fires an interrupt, NVIC can block it
- You must call
NVIC_EnableIRQ(USART1_IRQn)to allow it through - Your ISR must be named exactly
USART1_IRQHandler()— must match the vector table entry in startup file
Code Written So Far
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void uart_init(void)
{
// Step 1: Enable clocks
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN; // Enable GPIOA clock
RCC->APB2ENR |= RCC_APB2ENR_USART1EN; // Enable USART1 clock
// Step 2: Configure PA9, PA10 as Alternate Function mode
// PA9 → bits 19:18 in MODER
GPIOA->MODER &= ~(0x3 << 18); // clear
GPIOA->MODER |= (0x2 << 18); // set AF mode (10)
// PA10 → bits 21:20 in MODER
GPIOA->MODER &= ~(0x3 << 20); // clear
GPIOA->MODER |= (0x2 << 20); // set AF mode (10)
// Step 3: Assign AF7 (USART1) to PA9 and PA10 via AFRH
// PA9 → bits 7:4 in AFRH
GPIOA->AFR[1] &= ~(0xF << 4); // clear
GPIOA->AFR[1] |= (0x7 << 4); // AF7
// PA10 → bits 11:8 in AFRH
GPIOA->AFR[1] &= ~(0xF << 8); // clear
GPIOA->AFR[1] |= (0x7 << 8); // AF7
}
xxxx. Full Initialization Sequence (Planned)
1
2
3
4
5
6
7
Step 1: Enable clocks → RCC->AHB1ENR (GPIOA), RCC->APB2ENR (USART1)
Step 2: Configure pins → PA9, PA10 to Alternate Function mode (MODER = 10)
Step 3: Assign AF number → AF7 for both pins (AFRH) ← DONE UP TO HERE
Step 4: Configure USART1 → BRR (baud rate), word length, stop bits, parity,
enable TX/RX, enable RXNE interrupt, enable UE bit
Step 5: Configure NVIC → NVIC_EnableIRQ(USART1_IRQn), set priority
Step 6: Write ISR → USART1_IRQHandler()
V. Questions Raised & Resolved
| Question | Resolution |
|---|---|
| Do I need a USB-to-TTL adapter? | No — ST-Link VCP via SB11/SB15 handles it |
| Does UART project help with SPI/I2C later? | Yes — interrupt patterns, register workflow, and init sequence all transfer. Code does not reuse directly |
| What does the startup file do? | Prepares C runtime environment before main() — NOT peripheral init |
| Why are GPIOA and USART1 on different buses? | Different speed requirements — AHB1 is faster, APB2 is slower |
| Does clock enable order matter? | No between buses, but clocks must be enabled BEFORE peripheral register access |
| What is Alternate Function mode? | A hardware multiplexer connecting a pin to a specific internal peripheral |
| What is x vs y in GPIOx_AFRHy? | x = port (A/B/C…), y = pin number within that port (0–15) |
| Where to find bit positions in AFRH? | RM0090 Section 8.4.10 — explicitly listed in register description table |
Vx. Next Session — Where to Resume
Immediate next step: USART1 peripheral configuration (Step 4)
Open RM0090 Section 30.6.3 — Fractional baud rate generation and determine:
- What clock frequency does APB2 run at by default after reset on STM32F429?
- How to calculate the BRR value from that frequency
- Which bits in CR1 to set for: word length, TX enable, RX enable, RXNE interrupt, UE (USART enable)
Key reference documents:
- RM0090 — STM32F429 Reference Manual (main register reference)
- UM1670 — STM32F429I-DISC1 User Manual (board-level details)
- STM32F429 Datasheet — Table 11 (alternate function mapping), Table 12 (pin definitions)