🛠 Bare-metal sur le STM32F401 : clignoter LD2 en écrivant les registres GPIO
Goals
Strip away HAL, CMSIS device headers and the startup file, and blink LD2 by writing GPIO registers directly. This is the fastest way to understand what every abstraction above is doing — and it shows what the Cortex-M4F at 84 MHz gives you for the price of a Nucleo-64 board.
Hardware
- 1× Nucleo-F401RE + micro-USB cable (the on-board ST-LINK does the flashing)
arm-none-eabi-gcctoolchain
The memory map (RM0368)
- GPIOA base 0x4002_0000:
MODERat offset 0x00,ODRat offset 0x140. RCC_AHB1ENRat 0x4002_3830, bit 0 enables the GPIOA clock — every GPIO bug you will ever have starts here.- LD2 is PA5 (variant header): bits 11:10 of MODER select the pin mode, bit 5 of ODR drives it.
- With no clock setup the core runs off the default HSI 16 MHz — the blink is slow but honest.
Code
/* blink_bare.c — arm-none-eabi-gcc -mcpu=cortex-m4 -mthumb -O2 -nostdlib */
#define GPIOA_MODER (*(volatile unsigned int *)0x40020000u)
#define GPIOA_ODR (*(volatile unsigned int *)0x40020140u)
#define RCC_AHB1ENR (*(volatile unsigned int *)0x40023830u)
static void busy_wait(void) { for (volatile int i = 0; i < 2000000; ++i) {} }
void Reset_Handler(void)
{
RCC_AHB1ENR |= (1u << 0); /* enable GPIOA clock (default HSI 16 MHz) */
GPIOA_MODER &= ~(3u << 10);
GPIOA_MODER |= (1u << 10); /* PA5 -> general-purpose output */
while (1) {
GPIOA_ODR ^= (1u << 5); /* toggle LD2 */
busy_wait;
}
}
Steps
- Write a minimal linker script placing the vector table at 0x0800_0000: initial stack pointer, then the address of
Reset_Handler. - Compile:
arm-none-eabi-gcc -mcpu=cortex-m4 -mthumb -O2 -nostdlib -nostartfiles -T flash.ld blink_bare.c -o blink_bare.elf - Flash over the on-board ST-LINK with STM32CubeProgrammer (or drop the binary on the ST-LINK mass-storage function).
- Press RESET: LD2 toggles.
Expected result
A steady heartbeat on PA5 with a binary of a few dozen bytes. Every HAL_GPIO_WritePin you ever wrote is this, plus clock trees, plus the vector table, plus a startup file — the HAL call hides bookkeeping, not magic.
Common mistakes
- Forgetting the GPIOA clock enable: MODER writes silently do nothing.
- Writing MODER with
=instead of read-modify-write: you clobber the other PA pins. - No vector table or wrong linker script: the ROM bootloader or garbage runs instead of your code. >
- Leaving the FPU idle: add
-mfpu=fpv4-sp-d16 -mfloat-abi=hardwhen you want the single-precision FPU and DSP instructions the Cortex-M4F offers.
Other tutorials for this board
- Prise en main de la Nucleo-F401RE : ST-LINK, LED LD2 et alimentation
- Premier Blink Arduino sur la Nucleo-F401RE avec le core STM32duino
- Console série sur le ST-LINK VCP : printf, ADC 12 bits et piège du USB
- STM32CubeF4 et HAL : blink + UART printf générés pour la Nucleo-F401RE
- ADC 12 bits et PWM sur la Nucleo-F401RE : oversampling, millivolts et LED en fondu
- Zephyr RTOS sur la Nucleo-F401RE : west build, devicetree et west flash