STM32 Bare-Metal Programming

July 10, 2026 · by Aayush Rai

Share
STM32 & Embedded Systems Series Part 4 of 6

Estimated reading time: 8–9 minutes

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.

Registers & RCC GPIO configuration STM32F407G-DISC1 STM32CubeIDE / CubeMX ST-LINK debugging

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.

Write code Build Compile Link Flash Run Debug Improve

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.


Conclusion

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