01
Simple Blink
Toggle both onboard LEDs (PG13 & PG14) using raw GPIO register writes and a software busy-wait delay. The most fundamental bare-metal exercise — prove the clock, pin, and output path all work before adding any complexity.
GPIORCC AHB1GPIOG ODRSoftware delay
Pins
PG13 · PG14
Peripheral
GPIOG
Clock bus
AHB1 bit 6
Toggle
ODR ^= mask
simple blink/main.c
C
Key lines
#include "stm32f4xx.h"

RCC->AHB1ENR |= (1 << 6);
GPIOG->MODER &= ~((3<<26)|(3<<28));
GPIOG->MODER |=  ((1<<26)|(1<<28));

while (1) {
    GPIOG->ODR ^= (1<<13) | (1<<14);
    delay(500000);
}
#include <stdint.h>
#define STM32F429xx
#include "stm32f4xx.h"

void delay(volatile uint32_t count) { while (count--); }

int main(void) {
    RCC->AHB1ENR |= (1 << 6);
    GPIOG->MODER &= ~((3<<26)|(3<<28));
    GPIOG->MODER |=  ((1<<26)|(1<<28));
    GPIOG->OTYPER  &= ~((1<<13)|(1<<14));
    GPIOG->OSPEEDR &= ~((3<<26)|(3<<28));
    GPIOG->PUPDR   &= ~((3<<26)|(3<<28));
    GPIOG->ODR     &= ~((1<<13)|(1<<14));
    while (1) {
        GPIOG->ODR ^= (1<<13) | (1<<14);
        delay(500000);
    }
  }
RegisterOperationEffect
RCC→AHB1ENR|= (1<<6)Enable GPIOG clock
GPIOG→MODERbits [29:26] = 0101PG13/PG14 → output mode
GPIOG→ODR^= (1<<13)|(1<<14)Toggle both LEDs

02
External Interrupt
Button B1 on PA0 fires an EXTI0 interrupt on the rising edge. The ISR toggles a flag; the main loop reads it to enable or freeze LED blinking. Demonstrates the full interrupt pipeline: SYSCFG mux → EXTI config → NVIC enable → ISR clear.
EXTINVICSYSCFGISRPA0 · PG13 · PG14
Interrupt line
EXTI0
Trigger
Rising edge
Source pin
PA0 (B1)
ISR clears
PR |= 1 (w1c)
Why write 1 to EXTI→PR to clear it? The pending register is write-1-to-clear by hardware design — avoids race conditions where a second edge fires between the read and clear of a normal read-modify-write.
external interrupt/main.c
C
Key lines
volatile uint8_t on_off = 0;

RCC->APB2ENR |= (1<<14);
SYSCFG->EXTICR[0] &= ~(0xF<<0);
EXTI->IMR |= (1<<0); EXTI->RTSR |= (1<<0);
NVIC_EnableIRQ(EXTI0_IRQn);

if (EXTI->PR & (1<<0)) { on_off ^= 1; EXTI->PR |= (1<<0); }

if (on_off) { GPIOG->ODR ^= (1<<13) | (1<<14); delay(500000); }
#include <stdint.h>
#define STM32F429xx
#include "stm32f4xx.h"

volatile uint8_t on_off = 0;
void delay(volatile uint32_t count) { while (count--); }

void GPIO_Init(void) {
    RCC->AHB1ENR |= (1<<0) | (1<<6);
    GPIOA->MODER &= ~(3<<0); GPIOA->PUPDR &= ~(3<<0);
    GPIOG->MODER &= ~((3<<26)|(3<<28));
    GPIOG->MODER |=  ((1<<26)|(1<<28));
}

void EXTI_Init(void) {
    RCC->APB2ENR |= (1<<14);
    SYSCFG->EXTICR[0] &= ~(0xF<<0);
    EXTI->IMR |= (1<<0); EXTI->RTSR |= (1<<0); EXTI->FTSR &= ~(1<<0);
    NVIC_SetPriority(EXTI0_IRQn, 1); NVIC_EnableIRQ(EXTI0_IRQn);
}

void EXTI0_IRQHandler(void) {
    if (EXTI->PR & (1<<0)) { on_off ^= 1; EXTI->PR |= (1<<0); }
}

int main(void) {
    GPIO_Init(); EXTI_Init();
    while (1) {
        if (on_off) { GPIOG->ODR ^= (1<<13) | (1<<14); delay(500000); }
        else { GPIOG->ODR &= ~((1<<13)|(1<<14)); }
    }
  }
RegisterOperationEffect
RCC→APB2ENR|= (1<<14)Enable SYSCFG clock
SYSCFG→EXTICR[0]&= ~0xFRoute EXTI0 to PA0
EXTI→IMR / RTSR|= (1<<0)Unmask + rising edge trigger
EXTI→PR|= (1<<0)Clear pending flag (write-1-to-clear)

03
Timer 1s Blink
TIM2 is configured to fire an update interrupt exactly every 1 second. The ISR clears the flag and toggles PG13. The main loop stays empty — timing is fully handled in hardware, no busy-wait.
TIM2Update IRQPSC · ARRNVICHardware timing
Clock
16 MHz HSI
Prescaler
PSC = 15999
Auto-reload
ARR = 999
Period
1 second
Timer period math — f_tick = 16 MHz / (PSC+1) = 1 kHz. Period = (ARR+1) / f_tick = 1 s.
timer 1s blink/main.c
C
Key lines
RCC->APB1ENR |= (1<<0);
TIM2->PSC = 15999; TIM2->ARR = 999;
TIM2->DIER |= TIM_DIER_UIE;
NVIC_EnableIRQ(TIM2_IRQn);
TIM2->CR1 |= TIM_CR1_CEN;

if (TIM2->SR & TIM_SR_UIF) {
    TIM2->SR &= ~TIM_SR_UIF;
    GPIOG->ODR ^= (1<<13);
}
#include <stdint.h>
#define STM32F429xx
#include "stm32f4xx.h"

void GPIO_Init(void) {
    RCC->AHB1ENR |= (1<<6);
    GPIOG->MODER &= ~(3<<26); GPIOG->MODER |= (1<<26);
}

void TIM2_Init(void) {
    RCC->APB1ENR |= (1<<0);
    TIM2->PSC = 15999; TIM2->ARR = 999; TIM2->CNT = 0;
    TIM2->DIER |= TIM_DIER_UIE;
    NVIC_SetPriority(TIM2_IRQn, 1); NVIC_EnableIRQ(TIM2_IRQn);
    TIM2->CR1 |= TIM_CR1_CEN;
}

void TIM2_IRQHandler(void) {
    if (TIM2->SR & TIM_SR_UIF) {
        TIM2->SR &= ~TIM_SR_UIF;
        GPIOG->ODR ^= (1<<13);
    }
}

int main(void) {
    GPIO_Init(); TIM2_Init();
    while (1) { /* timer runs autonomously */ }
}
RegisterValueEffect
TIM2→PSC1599916 MHz → 1 kHz tick
TIM2→ARR9991000 ticks → 1 s period
TIM2→DIER UIEsetFire IRQ on overflow
TIM2→SR UIFcleared in ISRMust clear or IRQ re-fires immediately

04
PWM — 4 Channels
TIM4 drives four hardware PWM outputs on PD12–PD15 at 10%, 20%, 30%, 40% duty cycles. Each pin configured in AF2, mapped to a TIM4 capture-compare channel. Runs fully in hardware with zero CPU load.
TIM4 PWMAF2CCR · CCMR · CCERPD12–PD15
PWM freq
~1 kHz
PSC / ARR
15 / 999
PWM mode
Mode 1 (110)
AF
AF2 = TIM4
PWM Mode 1 — output HIGH while counter < CCR, LOW otherwise. Duty = CCR / (ARR+1). CCR=100 → 10%.
PWM 4 channel/main.c
C
Key lines
RCC->AHB1ENR |= (1<<3);
GPIOD->MODER |=  ((2<<24)|(2<<26)|(2<<28)|(2<<30));
GPIOD->AFR[1] |=  (0x22220000);

RCC->APB1ENR |= (1<<2);
TIM4->PSC = 15; TIM4->ARR = 999;
TIM4->CCMR1 |= (6<<4)|TIM_CCMR1_OC1PE; TIM4->CCER |= TIM_CCER_CC1E; TIM4->CCR1 = 100;
TIM4->CCMR1 |= (6<<12)|TIM_CCMR1_OC2PE; TIM4->CCER |= TIM_CCER_CC2E; TIM4->CCR2 = 200;
TIM4->CCMR2 |= (6<<4)|TIM_CCMR2_OC3PE; TIM4->CCER |= TIM_CCER_CC3E; TIM4->CCR3 = 700;
TIM4->CCMR2 |= (6<<12)|TIM_CCMR2_OC4PE; TIM4->CCER |= TIM_CCER_CC4E; TIM4->CCR4 = 900;
TIM4->CR1 |= TIM_CR1_ARPE; TIM4->EGR |= TIM_EGR_UG; TIM4->CR1 |= TIM_CR1_CEN;
#include <stdint.h>
#define STM32F429xx
#include "stm32f4xx.h"

void GPIO_Config(void) {
    RCC->AHB1ENR |= (1<<3);
    GPIOD->MODER &= ~((3<<24)|(3<<26)|(3<<28)|(3<<30));
    GPIOD->MODER |=  ((2<<24)|(2<<26)|(2<<28)|(2<<30));
    GPIOD->AFR[1] &= ~(0xFFFF0000);
    GPIOD->AFR[1] |=  (0x22220000);
}

void TIM_Config(void) {
    RCC->APB1ENR |= (1<<2);
    TIM4->PSC = 15; TIM4->ARR = 999;
    TIM4->CCMR1 |= (6<<4)|TIM_CCMR1_OC1PE; TIM4->CCER |= TIM_CCER_CC1E; TIM4->CCR1 = 100;
    TIM4->CCMR1 |= (6<<12)|TIM_CCMR1_OC2PE; TIM4->CCER |= TIM_CCER_CC2E; TIM4->CCR2 = 200;
    TIM4->CCMR2 |= (6<<4)|TIM_CCMR2_OC3PE; TIM4->CCER |= TIM_CCER_CC3E; TIM4->CCR3 = 700;
    TIM4->CCMR2 |= (6<<12)|TIM_CCMR2_OC4PE; TIM4->CCER |= TIM_CCER_CC4E; TIM4->CCR4 = 900;
    TIM4->CR1 |= TIM_CR1_ARPE; TIM4->EGR |= TIM_EGR_UG; TIM4->CR1 |= TIM_CR1_CEN;
}

int main(void) {
    GPIO_Config(); TIM_Config();
    while (1) { /* PWM runs in hardware */ }
}

05
Manual PWM — Button Controls Brightness
PG13/PG14 are not wired to any timer CCx output, so PWM is implemented in software via TIM2 ISR. Button B1 raises duty by 10% per press via EXTI. CPU sleeps with WFI between interrupts.
Software PWMTIM2 ISREXTI0WFI sleepPG13 · PG14 · PA0
ISR rate
10 kHz
PWM freq
100 Hz
Steps
100 (0–100%)
Step / press
+10%
Why software PWM? PG13/PG14 are not physically connected to any timer output channel on STM32F429I-DISC1. Hardware PWM requires that physical link — so ISR-based software PWM is the only option.
manual PWM/main.c
C
#define PWM_STEPS  100
  #define STEP_SIZE   10

  volatile uint8_t duty = 0, pwm_counter = 0;

  void TIM2_IRQHandler(void) {
    if (TIM2->SR & TIM_SR_UIF) {
      TIM2->SR &= ~TIM_SR_UIF;
      pwm_counter = (pwm_counter + 1) % PWM_STEPS;
      if (pwm_counter < duty)
        GPIOG->ODR |=  ((1<<13)|(1<<14));
      else
        GPIOG->ODR &= ~((1<<13)|(1<<14));
    }
  }

  void EXTI0_IRQHandler(void) {
    if (EXTI->PR & (1<<0)) {
      duty = (duty + STEP_SIZE) % (PWM_STEPS + STEP_SIZE);
      EXTI->PR |= (1<<0);
    }
  }

  int main(void) {
    GPIO_Init(); EXTI_Init(); TIM2_Init();
    while (1) { __WFI(); }
  }
ConceptImplementationNote
PWM period100 TIM2 overflowspwm_counter cycles 0→99
Duty cyclecounter < dutyON for first N ticks of 100
Brightness stepduty += 1010 steps 0%→100%
CPU idle__WFI()Sleep until next interrupt

Introduction

This report documents the bare metal implementation of UART communication on the STM32F429I-DISC1, covering character transmission and reception using USART1 with direct register programming — no HAL, no abstraction layer.

Instead of a dedicated USB-to-TTL adapter, an Arduino Uno was used as a serial bridge between the STM32 and the PC, using its SoftwareSerial library to avoid conflicts on its hardware UART pins D0/D1.

MCU
STM32F429ZIT6
Clock
16 MHz HSI
UART
USART1 / 9600
Bridge
Arduino Uno
TX Pin
PA9 — AF7
RX Pin
PA10 — AF7

Setup & Wiring

The STM32F429I-DISC1 onboard LCD uses many GPIO pins including PA2/PA3 (which would normally carry USART2). After verifying pin availability, USART1 on PA9/PA10 was selected as the only UART with both TX and RX freely accessible.

STM32F429I-DISC1 connected to Arduino Uno
fig.1 — STM32F429I-DISC1 (top) wired to Arduino Uno (bottom). Yellow = PA9 TX → D2, Blue = PA10 RX ← D3, Black = GND.

Wiring Table

STM32 PinDirectionArduino PinPurpose
PA9D2 (SoftwareSerial RX)USART1 TX
PA10D3 (SoftwareSerial TX)USART1 RX
GNDGNDCommon ground
Why not D0/D1? On Arduino Uno, D0/D1 are shared between ATmega328P hardware UART and the ATmega16U2 USB bridge chip. Connecting an external TX there creates a bus conflict. SoftwareSerial on D2/D3 eliminates this entirely.

Sending a Character

Configure USART1 and transmit A, B, C followed by CR/LF periodically. Three register groups must be configured: RCC clocks, GPIO alternate function, and USART baud rate + enable.

STM32 Code — main.c

Src/main.c
C · bare metal
Key lines
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
GPIOA->MODER  |= (2U << (9*2));
GPIOA->AFR[1] |= (7U << ((9-8)*4));
USART1->BRR = 0x0683;
USART1->CR1 = USART_CR1_UE | USART_CR1_TE;

while(!(USART1->SR & USART_SR_TXE));
USART1->DR = (ch & 0xFF);
while(!(USART1->SR & USART_SR_TC));

UART1_SendChar('A'); UART1_SendChar('B'); UART1_SendChar('C');
#define STM32F429xx
#include "stm32f4xx.h"

void delay(volatile uint32_t count) { while(count--); }

void USART1_Init(void) {
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
    RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
    GPIOA->MODER  &= ~(3U << (9*2)); GPIOA->MODER  |= (2U << (9*2));
    GPIOA->AFR[1] &= ~(0xF << ((9-8)*4)); GPIOA->AFR[1] |= (7U << ((9-8)*4));
    USART1->BRR = 0x0683;
    USART1->CR1 = USART_CR1_UE | USART_CR1_TE;
}

void UART1_SendChar(int ch) {
    while(!(USART1->SR & USART_SR_TXE));
    USART1->DR = (ch & 0xFF);
    while(!(USART1->SR & USART_SR_TC));
}

int main(void) {
    USART1_Init();
    while (1) {
        UART1_SendChar('A'); UART1_SendChar('B'); UART1_SendChar('C');
        UART1_SendChar('
'); UART1_SendChar('
');
        delay(4000000);
    }
  }

Arduino Bridge Sketch

bridge.ino
Arduino
Key lines
#include <SoftwareSerial.h>
SoftwareSerial stm32Serial(2, 3);
void setup() { Serial.begin(9600); stm32Serial.begin(9600); }

if (stm32Serial.available()) Serial.write(stm32Serial.read());
if (Serial.available())       stm32Serial.write(Serial.read());
#include <SoftwareSerial.h>
SoftwareSerial stm32Serial(2, 3);  // RX=D2, TX=D3
void setup() { Serial.begin(9600); stm32Serial.begin(9600); }
void loop()  {
    if (stm32Serial.available()) Serial.write(stm32Serial.read());
    if (Serial.available())       stm32Serial.write(Serial.read());
}
Observed: "ABC" printed every ~1 second in Serial Monitor

Receiving a Character

Extend the driver to receive a character and react to it. Typing 'A' in Serial Monitor toggles the green LED on PG13. Requires enabling PA10 as AF7 (USART1_RX), setting RE in CR1, and polling RXNE.

STM32 Code — main.c

Src/main.c
C · bare metal
Key lines
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN | RCC_AHB1ENR_GPIOGEN;
RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
GPIOA->MODER  |= (2U<<(9*2));  GPIOA->AFR[1] |= (7U<<((9-8)*4));
GPIOA->MODER  |= (2U<<(10*2)); GPIOA->AFR[1] |= (7U<<((10-8)*4));
USART1->BRR = 0x0683;
USART1->CR1 = USART_CR1_UE | USART_CR1_TE | USART_CR1_RE;

while(!(USART1->SR & USART_SR_RXNE));
return (unsigned char)(USART1->DR);

if (c == 'A') GPIOG->ODR ^= (1U<<13);
#define STM32F429xx
#include "stm32f4xx.h"

void USART1_Init(void) {
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN | RCC_AHB1ENR_GPIOGEN;
    RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
    // PA9 TX
    GPIOA->MODER  &= ~(3U<<(9*2));  GPIOA->MODER  |= (2U<<(9*2));
    GPIOA->AFR[1] &= ~(0xF<<((9-8)*4)); GPIOA->AFR[1] |= (7U<<((9-8)*4));
    // PA10 RX
    GPIOA->MODER  &= ~(3U<<(10*2)); GPIOA->MODER  |= (2U<<(10*2));
    GPIOA->AFR[1] &= ~(0xF<<((10-8)*4)); GPIOA->AFR[1] |= (7U<<((10-8)*4));
    // PG13 LED output
    GPIOG->MODER &= ~(3U<<(13*2)); GPIOG->MODER |= (1U<<(13*2));
    USART1->BRR = 0x0683;
    USART1->CR1 = USART_CR1_UE | USART_CR1_TE | USART_CR1_RE;
}

unsigned char UART1_GetChar(void) {
    while(!(USART1->SR & USART_SR_RXNE));
    return (unsigned char)(USART1->DR);
}

int main(void) {
    USART1_Init();
    while (1) {
        unsigned char c = UART1_GetChar();
        if (c == 'A') GPIOG->ODR ^= (1U<<13);
    }
  }
RegisterBit / FieldRole
USART1→CR1RE (bit 2)Enable the receiver
USART1→SRRXNE (bit 5)Set when a byte is ready
USART1→DR[7:0]Received byte — reading clears RXNE
GPIOG→ODRbit 13Green LED PG13 toggle
Observed: typing 'A' in Serial Monitor toggles the green LED on PG13

Challenges & Key Findings

Pin conflict — USART2 blocked by LCD

The TD references PA2/PA3 (USART2), valid on STM32F407. On STM32F429I-DISC1 those pins are routed to the onboard SDRAM and LCD controller. USART1 on PA9/PA10 was selected instead.

Arduino Uno as USB-TTL bridge

Using hardware UART pins D0/D1 caused bus contention with the ATmega16U2 USB chip. The solution was SoftwareSerial on D2/D3 — dedicated pins with no shared silicon.

Device header mismatch

The project required an explicit #define STM32F429xx before including stm32f4xx.h, because compiler flags provided -DSTM32F429ZITx rather than the family-level define the header checks for.

Exercises 3–8: Same Core, Higher Abstraction

Every remaining exercise is built on the same primitive — writing a byte into USART→DR and waiting for a flag in USART→SR. The complexity shifts from the driver layer to the application layer.

ExerciseObjectiveUART role
3 — String TX/RXSend and receive full strings, echo loopLoop over SendChar(), buffer chars until CR/LF
4 — SIM808 InitSend AT commands to configure GSM moduleSendString("AT+CMGF=1 ") on a second UART
5 — Send SMSButton press triggers an SMSAT+CMGS, wait > prompt via GetChar(), send Ctrl+Z
6 — Voice CallButton press triggers a phone callSendString("ATD+num; ") then "ATH "
7 — GSM Frame RXDetect +CLIP: caller ID in streamRead chars into buffer, strstr() for keyword
8 — GPS ParsingRead +CGPSINF: frame, extract lat/lonSend AT+CGPSINF=0, read line, tokenize with strtok()
Key insight — Once SendChar() and GetChar() are solid, every higher-level protocol is just what string you send and what string you wait to receive back. The register-level driver never changes.
abstraction layers
concept
Key lines
// Layer 3 — Application (ex 4–8)
configSIM808()  GSM_SendSms()  GSM_MakeCall()  GPS_readInfo()

// Layer 2 — String (ex 3)
SendString(char *str)   ReceiveString(char *buf)

// Layer 1 — Character (ex 1 & 2)  ← built in this report
SendChar(char ch)        GetChar()

// Layer 0 — Registers (always the same)
USART->DR   USART->SR&TXE   USART->SR&RXNE   USART->SR&TC
// Layer 3 — Application (ex 4–8)
configSIM808()  GSM_SendSms()  GSM_MakeCall()  GPS_readInfo()

// Layer 2 — String (ex 3)
SendString(char *str)   ReceiveString(char *buf)

// Layer 1 — Character (ex 1 & 2)  ← built in this report
SendChar(char ch)        GetChar()

// Layer 0 — Registers (always the same)
USART->DR   USART->SR&TXE   USART->SR&RXNE   USART->SR&TC