Bare Metal STM32 · Part 1 of 7

STM32 HAL vs Bare Metal: Which Should You Use?

STM32 HAL Beginner Source Code
Bare Metal STM32 Part 1
Bare Metal STM32 — Part 1
STM32 HAL vs Bare Metal: Which Should You Use?
38:22
Part 1 / 7
38:22
Beginner
STM32F4 · CubeIDE
01

Overview

Every STM32 developer eventually faces the same question: HAL or bare metal? HAL hides register complexity behind clean function calls — great for prototyping. Bare-metal register access gives you total control, deterministic timing, and smaller binaries — essential for production. This part teaches you both approaches side-by-side so you can make an informed choice for every project.

  • Understand the three layers: HAL, Low-Level (LL) drivers, and direct register access
  • Toggle a GPIO using all three approaches and compare the generated assembly
  • Measure the overhead difference between HAL and register writes with a logic analyser
  • Know when to use each layer — and why mixing them in the same project is fine
02

The Three Layers

🏗️
HAL
High-level, portable API. Handles clock enables, state machines, and callbacks for you. Largest code size, non-deterministic timing. Best for getting started and complex peripherals like USB.
HAL_GPIO_WritePin()
LL Drivers
Thin inline wrappers around registers. Portable like HAL but with near-zero overhead. Generated by CubeMX. Best of both worlds for most peripheral access.
LL_GPIO_SetOutputPin()
🔩
Registers
Direct read/write to memory-mapped peripheral registers. Single instruction, zero overhead, fully deterministic. Requires reading the reference manual but produces the tightest code.
GPIOD->BSRR = (1<<12)
03

Code

01
Three ways to toggle a GPIO — HAL, LL, and register
gpio_comparison.c
C
/* ── Method 1: HAL ─────────────────────────────────────── */
// ~20 instructions — checks state, handles locks, portable
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_12, GPIO_PIN_SET);
HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_12);

/* ── Method 2: LL Driver ───────────────────────────────── */
// ~2 instructions — inline, no overhead, still portable
LL_GPIO_SetOutputPin(GPIOD, LL_GPIO_PIN_12);
LL_GPIO_TogglePin(GPIOD, LL_GPIO_PIN_12);

/* ── Method 3: Direct Register ─────────────────────────── */
// 1 instruction — BSRR is a write-only atomic set/reset reg
GPIOD->BSRR = (1 << 12);            // Set   PD12
GPIOD->BSRR = (1 << (12 + 16));     // Reset PD12
GPIOD->ODR ^= (1 << 12);            // Toggle (not atomic!)

/* ── Clock Enable — always required before any GPIO use ── */
// HAL way:
__HAL_RCC_GPIOD_CLK_ENABLE();
// Register way:
RCC->AHB1ENR |= RCC_AHB1ENR_GPIODEN;
02
Bare-metal GPIO init from scratch
gpio_bare.c
C
void GPIO_Init_PD12_Output(void) {
    // 1. Enable GPIOD clock on AHB1 bus
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIODEN;

    // 2. Set MODER[25:24] = 01 → General purpose output
    GPIOD->MODER &= ~(0x3 << (12 * 2));
    GPIOD->MODER |=  (0x1 << (12 * 2));

    // 3. Set OTYPER bit 12 = 0 → Push-pull (default)
    GPIOD->OTYPER &= ~(1 << 12);

    // 4. Set OSPEEDR[25:24] = 10 → High speed
    GPIOD->OSPEEDR |= (0x2 << (12 * 2));

    // 5. Set PUPDR[25:24] = 00 → No pull (output drives the line)
    GPIOD->PUPDR &= ~(0x3 << (12 * 2));
}
💡
BSRR is your best friend for atomic GPIO
The Bit Set/Reset Register (BSRR) lets you set or clear any output pin in a single atomic write — no read-modify-write needed. Bits [15:0] set pins; bits [31:16] clear pins. This is safe in both task and ISR context without disabling interrupts.

Continue the Series

Work through all 7 parts of Bare Metal STM32 to master low-level embedded development from the ground up.