Bare-Metal STM32 Firmware
A button press raises a hardware interrupt and the LED blinks five times, with no HAL, no CMSIS and no standard library.
VIEW CODE ↗2026
Overview
Bare-metal C firmware for the STM32F401RE (Nucleo board, ARM Cortex-M4). Everything between power-on and main() is written by hand: the startup code, the interrupt vector table and the linker script. The point of the project is to understand what the vendor libraries normally hide.
At a glance
- Board
- Nucleo STM32F401RE (Cortex-M4)
- Language
- C, GNU linker script
- Button
- B1 on PC13, input, rising-edge EXTI
- LED
- LD2 on PA5, output
- Toolchain
- arm-none-eabi-gcc, OpenOCD, ST-Link
The interrupt path
- Button on PC13
- EXTI line 13
- NVIC IRQ 40
- EXTI15_10_IRQHandler
- event_count += 5
- Main loop blinks LED
What it shows
- Startup and vector table. startup.c has a hand-written interrupt vector table and Reset_Handler. It copies .data from flash to SRAM and zeroes .bss before calling main().
- Custom linker script. linker.ld defines the FLASH (512K) and SRAM (96K) regions and places .text, .rodata, .data and .bss.
- Direct register access. RCC, GPIO, SYSCFG, EXTI and NVIC are configured through their registers directly, using the datasheet and reference manual.
- Safe sharing with the interrupt. The handler and the main loop share a volatile counter. The main loop reads and clears it inside a short interrupt-disabled section (cpsid i / cpsie i).
- Atomic LED writes. The LED is set and cleared through the GPIO BSRR register. It is a single write, so there is no read-modify-write that an interrupt could split.
How it works
- main() enables the clocks for SYSCFG, GPIOA and GPIOC, then sets PA5 as output and PC13 as input.
- SYSCFG routes PC13 to EXTI line 13. EXTI is set to rising edge and the line is unmasked. NVIC IRQ 40 is enabled.
- On a button event, EXTI15_10_IRQHandler clears the pending bit and adds 5 to event_count.
- The main loop copies and clears event_count with interrupts disabled, then blinks the LED that many times.
Lesson learned
EXTI line 13 did not fire until the SYSCFG clock was enabled (RCC_APB2ENR bit 14). Without that clock, SYSCFG cannot route PC13 to the EXTI line.
Build and flash
Build
arm-none-eabi-gcc \
-mcpu=cortex-m4 \
-mthumb \
-nostdlib \
-T linker.ld \
startup.c main.c \
-o blink.elfFlash
openocd -f interface/stlink.cfg -f target/stm32f4x.cfg \
-c "program blink.elf verify reset exit"