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:

1

![[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:

  1. What is the pin for USART1 Transmit (USART1_TX) in STM32F429?
  2. What is the pin for USART1 Receive (USART1_RX) in STM32F429?

-> The answer is in UM1670 Datasheet, p.22:

2

![[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.

3

![[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:

  • 0x3 is 11 in binary — a 2-bit mask.
  • << 18 shifts 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, GPIOB are 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)
  1. p.270, 8.3. GPIO functional description, RM0090 Reference Manual 

  2. p.22, Table 7. STM32 pin description versus board functions, UM1670 

  3. p.276, Figure 27. Selecting an alternate function on STM32F42xxx and STM32F43xxx, RM0090 Reference Manual