Day 01 Foundations

Bare-Metal C Basics

Firmware runs on metal — no OS, no malloc, no printf. Write C that directly controls hardware registers with nothing in between: startup sequences, memory maps, and atomic GPIO.

~1 hour Intermediate Hands-on Precision AI Academy

Today's Objective

Firmware runs on metal — no OS, no malloc, no printf. Write C that directly controls hardware registers with nothing in between: startup sequences, memory maps, and atomic GPIO.

01

What Bare Metal Means

Bare-metal firmware runs directly on the CPU with no operating system. You control the startup sequence, memory layout, interrupt vectors, and every peripheral register. Advantages: deterministic timing, minimal overhead, smallest footprint. The startup sequence: CPU resets → loads PC from reset vector → runs startup code → initializes .data section from flash, zeros .bss section → calls main(). If main() returns, the CPU executes whatever is next in memory — always add an infinite loop.

02

The Memory Map

A microcontroller's address space is divided into regions defined by the manufacturer. Typical ARM Cortex-M: 0x00000000–0x1FFFFFFF = flash (code), 0x20000000–0x3FFFFFFF = SRAM (data/stack/heap), 0x40000000–0x5FFFFFFF = peripheral registers, 0xE0000000+ = core peripherals (NVIC, SysTick, etc.). The linker script (.ld file) maps your code sections (.text, .data, .bss) to these physical addresses.

03

CMSIS and Peripheral Headers

CMSIS (Cortex Microcontroller Software Interface Standard) provides a common C header for ARM Cortex-M peripherals. MCU vendors provide device-specific headers that define all peripheral register addresses as C structures: GPIOA->ODR = 0x01 sets PA0 high directly via memory-mapped I/O. No HAL needed — write to the register address and the hardware responds immediately.

c
// Bare-metal GPIO blink on STM32F1 (no HAL)
// Directly access peripheral registers via CMSIS headers

#include "stm32f1xx.h"

// The startup file provides this symbol
extern uint32_t SystemCoreClock;

void delay_ms(uint32_t ms) {
    // Simple busy-wait using SysTick
    uint32_t ticks = (SystemCoreClock / 1000) * ms;
    volatile uint32_t i;
    for (i = 0; i < ticks; i++) __NOP();
}

int main(void) {
    // 1. Enable clock to GPIOC (bit 4 of APB2ENR)
    RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;

    // 2. Configure PC13 as general-purpose push-pull output, 2MHz
    // CRH controls pins 8-15; PC13 is bits 23:20
    GPIOC->CRH &= ~(0xF << 20);   // clear bits
    GPIOC->CRH |=  (0x2 << 20);   // output, 2MHz (CNF=00, MODE=10)

    while (1) {
        // Set PC13 high (LED OFF on most Blue Pill boards — inverted)
        GPIOC->BSRR = (1 << 13);     // Bit Set Register
        delay_ms(500);

        // Set PC13 low (LED ON)
        GPIOC->BRR  = (1 << 13);     // Bit Reset Register
        delay_ms(500);
    }
    // Never return — would be undefined behavior
}
💡
Never use = 0b00000010 to set a register field — you'll accidentally clear other bits. Always use read-modify-write: REG &= ~MASK; REG |= VALUE; or the atomic BSRR/BRR registers for GPIO. Clearing other bits in a control register can disable unrelated peripherals.
📝 Day 1 Exercise
Register-Level GPIO Control
  1. Set up a development environment: install arm-none-eabi-gcc, OpenOCD, and VS Code with Cortex-Debug extension.
  2. Clone or create a bare-metal project for STM32F103 (Blue Pill, ~$2). Compile the blink sketch with: arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb -o blink.elf blink.c startup.c -T stm32f1.ld
  3. Flash with OpenOCD: openocd -f interface/stlink.cfg -f target/stm32f1x.cfg -c 'program blink.elf verify reset exit'
  4. Read the STM32F1 reference manual section on GPIO. Find the register that reads the input state of a pin. Implement a software button read without any library.
  5. Compare register-level code size (arm-none-eabi-size blink.elf) to the same sketch using Arduino HAL. What's the size difference?

Day 1 Summary

Challenge

Implement SysTick-based millisecond timer without any library. Configure SysTick to interrupt every 1ms (reload value = SystemCoreClock/1000 - 1). In the ISR, increment a volatile uint32_t counter. Implement uint32_t millis() that returns the counter. Implement void delay_ms(uint32_t) using it. Verify accuracy with a scope or LED toggle timing.

What's Next

The foundations from today carry directly into Day 2. In the next session the focus shifts to Registers and Peripherals — building directly on everything covered here.

Day 1 Checkpoint

Before moving on, verify you can answer these without looking:

  • What is the core concept introduced in this lesson, and why does it matter?
  • What are the two or three most common mistakes practitioners make with this topic?
  • Can you explain the key code pattern from this lesson to a colleague in plain language?
  • What would break first if you skipped the safeguards or best practices described here?
  • How does today's topic connect to what comes in Day 2?

Live Bootcamp

Learn this in person — 2 days, 5 cities

Thu–Fri sessions in Denver, Los Angeles, New York, Chicago, and Dallas. $1,490 per seat. June–October 2026.

Reserve Your Seat →
Continue To Day 2
Day 2: Registers and Peripherals