STM32 Bare-Metal Programming
July 10, 2026 · by Aayush Rai
There is a point in embedded systems where blinking an LED using digitalWrite()
is not enough anymore. It works, yes. But the same questions keep coming back. What is actually happening
inside the microcontroller? Why do we need to enable clocks? Why does a GPIO pin need mode configuration?
What is RCC? What are these long register names?
This blog is about that starting point — not expert-level bare-metal work, not writing an RTOS from scratch, not building a bootloader on day one. Just getting started with STM32 bare-metal programming: why it matters, how to pick the right board, how to set up the tools, and how the STM32 ecosystem works from writing code to actually running it on hardware.
What is Bare-Metal Programming?
Bare-metal programming means programming the microcontroller directly, very close to the hardware. With
Arduino or a high-level framework, a line like digitalWrite(13, HIGH); is easy
and beautiful for beginners — but internally it's selecting the correct GPIO port, configuring
registers, setting or clearing bits, and finally changing the pin's output state, all hidden from view.
In bare-metal programming, none of that is hidden. We work with the registers directly. Turning on an LED on an STM32 GPIO pin actually looks like this:
// Enable clock for GPIOD RCC->AHB1ENR |= (1U << 3); // Clear mode bits for pin 12 GPIOD->MODER &= ~(3U << 24); // Set pin 12 as output GPIOD->MODER |= (1U << 24); // Set pin 12 HIGH GPIOD->ODR |= (1U << 12);
It looks scary at first, but it slowly makes sense. RCC is the clock control
block, GPIOD is the GPIO port, MODER selects the
pin mode, and ODR is the output data register — the bit shifts just pick
the exact pin inside each register. We're not asking the board to do something; we're telling the
microcontroller exactly what to configure and where to write.
Why Learn Bare-Metal?
The biggest reason is understanding. HAL, Arduino, and other abstraction layers make things easier, but they also cost us visibility. Bare-metal programming teaches:
- How microcontroller registers and GPIO pins actually work
- Why clocks are required before using any peripheral
- How timers generate delays and PWM
- How UART, SPI, I2C, ADC, and interrupts really operate
- How memory-mapped peripherals are accessed
- How the processor starts executing code after reset
None of this means HAL or CubeMX are bad — they're genuinely useful tools. But when something breaks, bare-metal knowledge helps pinpoint where the actual problem is.
When the LED won't blink: it could be an unenabled GPIO clock, the wrong port, the wrong pin number, a pin mode that isn't set to output, a broken delay loop, a board LED wired to a different pin, or the program getting stuck before it even reaches the GPIO code. With only high-level code, debugging this is guesswork. With register-level understanding, you can actually inspect what's happening.
Choosing the Right Board
STM32 has a huge family — F0, F1, F3, F4, G0, G4, H7, L series, and more — which can be confusing for a beginner. The goal isn't the most powerful board; it's the board that helps you learn properly. A good STM32 learning board should have:
- Good documentation and strong community support
- A built-in debugger/programmer
- Enough GPIO pins, onboard LEDs, and buttons
- Enough peripherals to learn UART, SPI, I2C, ADC, timers, PWM, DMA, and interrupts
- Easy availability and affordable pricing
- A learning curve that's neither too basic nor too overwhelming
For this reason, we chose the STM32F407G-DISC1 Discovery board — built around the STM32F407VGT6, a 32-bit ARM Cortex-M4 with FPU support, running up to 168 MHz with 1 MB Flash and 192 KB RAM. For a learning board, that's more than enough headroom.
But the real reason it's useful isn't just performance — it's what's already on the board:
- Onboard ST-LINK debugger/programmer, so no separate programmer is needed at the start
- User LEDs, a push button, and USB support
- Audio-related hardware and a MEMS sensor
- Exposed pins with strong STM32CubeIDE support
- Solid documentation and examples
The onboard debugger matters a lot in bare-metal learning: connect the board, write code, flash it, debug it, and inspect registers, all from the same setup. Because things won't always work on the first try — sometimes the LED won't blink, a register value won't change, or the code compiles but nothing visible happens. That's when the debugger becomes your best friend. The board is capable enough to grow into GPIO, RCC, SysTick, timers, PWM, UART, SPI, I2C, ADC, DMA, external interrupts, startup files, linker scripts, and CMSIS register access — not just LED blinking, but a genuine long-term learning board.
STM32 vs Arduino vs Pi vs ESP32
Arduino, Raspberry Pi, ESP32, and STM32 aren't direct replacements for each other — each is built for different work.
| Parameter | STM32F407G-DISC1 | Arduino Uno | Raspberry Pi | ESP32 |
|---|---|---|---|---|
| Device type | Microcontroller dev board | Microcontroller board | Single-board computer | Wireless microcontroller |
| Processor | ARM Cortex-M4 | ATmega328P AVR | ARM application processor | Xtensa / RISC-V, by variant |
| Clock speed | Up to 168 MHz | 16 MHz | GHz-level processor | Up to 240 MHz (common variants) |
| Cores | Single-core | Single-core | Multi-core | Usually dual-core |
| OS | Bare metal / RTOS | Bare metal / Arduino framework | Linux | Bare metal / FreeRTOS |
| Real-time control | Very good | Good for simple tasks | Not ideal for strict real-time | Good |
| Power usage | Efficient | Low | Higher | Moderate, more with Wi-Fi/BT |
| Best use case | Professional firmware, control systems, robotics | Beginner electronics, simple projects | Linux apps, servers, AI, multimedia | IoT and wireless embedded systems |
| Low-level learning value | Very high | Medium | Low for MCU-level learning | Medium to high |
Arduino is great for starting out in electronics. ESP32 shines when you need Wi-Fi, Bluetooth, and IoT. Raspberry Pi is the right call for Linux, Python, computer vision, or web servers. STM32 is different: it's where professional microcontroller-level firmware development begins, giving direct control over hardware and teaching concepts used in real embedded products.
CubeIDE and CubeMX
STM32CubeIDE is the main tool for writing, building, flashing, and debugging STM32 programs. It bundles a code editor, project manager, compiler and build system, flashing and debugging support, a register and memory viewer, watch windows, breakpoints, and peripheral inspection — a complete environment even if you never touch HAL-generated code.
STM32CubeMX is a graphical configuration tool. It lets you select the microcontroller or board, configure pins and clocks, set up peripherals, check alternate functions, and generate initialization code. For UART, it shows which pins can serve as TX and RX; for the system clock, it gives a clock-tree view of how the external crystal, PLL, and bus clocks connect. You don't have to depend on it blindly for bare-metal learning — use it to understand available pins, alternate functions, and the clock tree, then write the register-level code yourself. That turns CubeMX into a learning tool instead of just a code generator.
The simple split: use CubeIDE for coding and debugging, CubeMX for configuration and code generation, and bare-metal programming to understand what the generated code is actually doing. ST has separated CubeMX from CubeIDE in newer releases — useful in professional projects, since it lets you update the IDE without changing the CubeMX version that shapes your generated code.
The STM32 Debugger
Many beginner platforms debug with serial prints like printf("Program reached
here\n"); — useful, but limited. With STM32 and the ST-LINK debugger, you can pause the
program, step through it line by line, set breakpoints, watch variable values, inspect registers, check
memory, and see exactly where execution is stuck.
Say you're configuring GPIOD pin 12 as an output. You can pause the program and directly inspect
RCC->AHB1ENR, GPIOD->MODER, and
GPIOD->ODR — telling you whether the clock is enabled, the pin mode is
correct, and the output register is actually changing. Without a debugger, you're mostly guessing; with
one, you're actually seeing what's happening inside the microcontroller. That's why the onboard ST-LINK on
the STM32F407G-DISC1 is such an advantage — it makes the whole learning process easier.
From Code to Running Firmware
The full STM32 development flow starts with selecting the board or microcontroller — in our case, the STM32F407G-DISC1 — then creating a project in STM32CubeIDE. That project holds a startup file, a linker script, system initialization files, a main source file, and CMSIS device headers.
The startup file handles the early boot process: on reset, the microcontroller doesn't jump
straight to main(). It first runs startup code, sets up the stack, initializes
memory sections, and only then reaches main(). The linker script
tells the compiler and linker where Flash and RAM live in memory. CMSIS headers give
readable register names like RCC->AHB1ENR instead of raw memory addresses.
Building the project turns the C code into machine code, producing an .elf file
for debugging, a .bin raw binary, and a .hex file
for flashing and distribution. Then the code is flashed to the board via ST-LINK, the microcontroller runs
it from internal Flash, and debugging begins — breakpoints, register inspection, variable checks, and
slowly understanding how the hardware behaves.
Where We Are Starting From
The starting point is simple — not a dive straight into advanced peripherals. The first goal is to understand:
- What registers are, and how RCC and GPIO actually work
- How to configure a pin as output and blink an LED
- How to use the debugger and read the reference manual
- How to connect code with real hardware behavior
The first LED blink in bare metal isn't just an LED blink — it's the first time you manually enable a peripheral clock, configure a register, and control a hardware pin directly. It's slower than an Arduino blink program at first, but it builds real understanding.
Getting started with STM32 bare-metal programming can feel difficult at first. The datasheet looks big, the reference manual looks even bigger, the register names feel strange, and even blinking an LED takes multiple steps. That's exactly what makes it valuable — bare-metal programming teaches how embedded systems actually work. A microcontroller isn't magic; it's a collection of registers, buses, clocks, peripherals, memory, and logic. Once that clicks, higher-level tools like HAL, CubeMX, and RTOS start making a lot more sense.
The STM32F407G-DISC1 is a strong board for this journey — enough power, enough peripherals, and an onboard debugger to learn properly. Arduino helps you start. ESP32 helps you build connected devices. Raspberry Pi helps you work with Linux and high-level applications. But STM32 bare-metal programming is what helps you understand the core of embedded firmware — and that's the foundation this journey is built on.
Next up: enabling RCC and configuring your first GPIO register
Written with coffee and love — by Aayush Rai