Module 0 — Before You Begin: The Essential Primer
Ready for the full course?
9 modules · register-level exercises & scenario quizzes.
Get full course

Before You Begin: The Essential Primer

ARM/STM32 Embedded Engineering Course

MODULE 0
Module 0 — The Prerequisite

You don't need to understand your MCU.
Until suddenly you do.

HAL functions can hide hardware for a while. But the first time an interrupt fires unexpectedly, a peripheral stops responding for no apparent reason, or a delay takes ten times longer than expected — you need the foundation. This module builds it before you need it.

What this module is
1

A vocabulary reference

Every technical term used across Modules 1–9, defined precisely with context. Stop Googling mid-lesson.

2

A concept ladder

The ten ideas that everything else builds on — in the correct order, with no assumed knowledge.

3

A practical primer

Hex numbers, binary, bit manipulation, and register patterns — the four mechanical skills every embedded engineer uses daily.

4

A guided debugging case

A real UART failure traced from the wire waveform to the RCC clock tree, register value, root cause, correction, and measured result.

5

A course map

Where each module fits, what it teaches, and which terms from this glossary it will use.

Who this is for

This course assumes you can write C code — loops, functions, pointers, structs. It does not assume you know anything about hardware. If you've ever wondered:

— "Why do I write GPIOA->ODR |= (1 << 5) instead of just setting a variable?"

— "What is a clock and why does everything depend on it?"

— "Why does volatile exist and when do I need it?"

— this module answers those questions first, so the rest of the course makes immediate sense.

The single most important mental model

"An MCU is a CPU, some memory, and a set of peripherals — all connected by a single shared address bus. Every component you interact with — RAM, Flash, GPIO, timers, UART — is just a range of addresses on that bus."

This is the model. Everything else is detail.

Once you hold this model, register writes make sense. Peripheral clock enables make sense. Memory-mapped I/O makes sense. Linker scripts make sense. DMA makes sense. The whole course is elaborating on this one idea from different angles — closer and closer to the metal.

01

The Big Picture

What an MCU is, what it contains, and how software talks to hardware

What an MCU actually is

An MCU (Microcontroller Unit) is a complete computer on a single chip. It contains:

CPU Core

The arithmetic brain. Fetches instructions from memory, executes them, reads/writes data. On our boards: ARM Cortex-M4 — a 32-bit processor running up to 168 MHz.

Memory

Two types. Flash (non-volatile, stores your firmware — survives power loss). SRAM (volatile, stores running data — cleared on power loss). Both live on the same address bus.

Peripherals

Hardware blocks that do specific jobs: GPIO (pins), UART (serial comms), timers, ADC, SPI, I2C, DMA. Each one is a cluster of registers at a fixed address.

The key insight Everything — Flash, SRAM, GPIO, timers, the interrupt controller — is a range of addresses on one 32-bit bus. Writing to address 0x40020014 doesn't write to memory. It changes the voltage on a GPIO pin. That's memory-mapped I/O.
How software talks to hardware

There is no system call, no driver API, no OS in between. Your C code writes directly to registers in hardware. The path is:

C variable assignment → compiled to STR instruction
STR instruction → puts value on address bus
Address bus → peripheral reads it, hardware responds
Physical effect: pin goes high, timer starts, data is sent
/* Every line below is a direct hardware write */ /* Enable GPIOA clock — peripheral bus write */ RCC->AHB1ENR |= (1 << 0); /* Set PA5 as output — register field write */ GPIOA->MODER |= (1 << 10); /* Drive PA5 high — voltage on a pin */ GPIOA->ODR |= (1 << 5); /* There is no OS, no driver, no magic. */ /* Just addresses and their hardware effects. */
Why you need to know this When a peripheral doesn't respond, 90% of the time the answer is: (1) clock not enabled, (2) wrong register field, or (3) missing volatile. Knowing the direct path from C to hardware tells you exactly where to look.
Bare metal vs HAL vs RTOS — three layers
LayerWhat it isWho uses itWhat you sacrifice
Bare metalDirect register writes. No abstractions. Your code IS the driver.This course, low-level drivers, safety-critical codePortability, development speed
HAL / LLST's Hardware Abstraction Layer. C functions that wrap register writes.Most production STM32 codeSome overhead, less control
RTOSOperating system layer — tasks, semaphores, queues. FreeRTOS is the most common.Complex multi-task systemsComplexity, timing overhead

This course teaches at the bare-metal level, using HAL only where it helps clarity. The goal is that you understand what HAL does — so you can debug it when it fails.

02

The Concept Ladder

Ten ideas in the right order — each one makes the next possible

Read these once before starting Module 1

These ten ideas are the prerequisite for everything else. They appear in order — each concept uses the previous one. Read them slowly. If one doesn't click, everything after it will feel fuzzy.

03

Course Map

What each module covers and how they connect — click any module to expand

Nine modules — one complete mental model

The modules form a deliberate sequence: hardware foundation → toolchain → CPU → memory → peripherals → timing → power → design thinking. Each module builds on all previous ones. The glossary in this module covers every technical term you'll encounter.

04

Keyword Glossary

Every technical term used across Modules 1–9 — defined before you need them

Search and filter — 60+ terms across 5 categories
05

Number Systems

Decimal, binary, hexadecimal — and why embedded engineers live in hex

Why three number systems?
Decimal (base-10)

What humans use. 0–9. Useless for hardware because it doesn't map to bits. You'll never write a register address in decimal.

Binary (base-2)

What hardware actually uses. 0 and 1. Essential for understanding bit fields in registers. Every GPIO pin, every interrupt flag — one bit.

Hexadecimal (base-16)

What embedded engineers use. 0–F. One hex digit = exactly 4 bits. 8-digit hex number = 32-bit address. All register addresses are in hex.

The fundamental reason for hex 0xFF = 0b11111111 = 255. One hex digit maps to exactly 4 binary bits. So 0x4002001C maps perfectly to 32 binary bits — you can read each nibble and know the exact bit pattern. Decimal 1073807388 tells you nothing about the hardware structure.
Conversion quick reference
Hex → Decimal: multiply each digit by 16^position
0xAB
= 10×16¹ + 11×16⁰
= 160 + 11 = 171
Nibble table (memorize this)
0x0=0000
0x4=0100
0x8=1000
0xC=1100
0x1=0001
0x5=0101
0x9=1001
0xD=1101
0x2=0010
0x6=0110
0xA=1010
0xE=1110
0x3=0011
0x7=0111
0xB=1011
0xF=1111
/* Important values you'll see constantly: */ 0xFF /* 255 = 0b11111111 (8-bit mask) */ 0xFFFF /* 65535 = all 16 bits set */ 0xFFFFFFFF /* max uint32 = -1 in signed */ 0xDEADBEEF /* debug canary value */ 0xC5ACCE55 /* DWT unlock magic number */ /* In C, hex literals: */ uint32_t mask = 0x0F; /* = 0b00001111 */ uint32_t addr = 0x40020014; /* GPIOA_ODR */
The 32-bit address space, mapped

Every one of those addresses lives somewhere on a single 4GB address line. This is schematic, not to scale — Peripherals and reserved space are actually far larger than Flash or SRAM — but the order and boundaries are real.

Reserved Flash SRAM Peripherals Cortex-M System 0x00000000 0x08000000 0x20000000 0x40000000 0xE0000000 0xFFFFFFFF 0x40020014 → GPIOA_ODR
Why this matters The CPU doesn't know or care what's at an address — Flash, SRAM, or a GPIO register look identical to it. Only the address range tells you (and the compiler) what you're actually touching.
Reading a 32-bit hex address like an engineer — try it

Type any 8-digit hex address. Watch which region it falls in and what that byte pattern tells you.

0x 40 02 00 14
06

Bit Manipulation

The four operations that appear in every embedded program — and why

Why bits matter in embedded

Hardware registers pack many settings into a single 32-bit word. The MODER register for GPIOA holds configuration for 16 pins — 2 bits per pin. You must be able to change one pin's bits without touching the other 15 pins. That requires precise bit manipulation.

Set a bit — use OR (|=)

REG |= (1 << n) — sets bit n to 1. Never clears any other bit. "Open the gate at position n."

REG = 0b11001010; REG |= (1 << 2); /* set bit 2 */ /* REG = 0b11001110 — only bit 2 changed */
Clear a bit — use AND NOT (&= ~)

REG &= ~(1 << n) — clears bit n to 0. Never sets any other bit. "Close the gate at position n."

REG = 0b11001110; REG &= ~(1 << 3); /* clear bit 3 */ /* REG = 0b11000110 — only bit 3 changed */
Toggle a bit — use XOR (^=)

REG ^= (1 << n) — flips bit n. Useful for LED blink: toggles the current state without reading it first.

GPIOA->ODR ^= (1 << 5); /* toggle PA5 */ /* high→low OR low→high each call */
Read a bit — use AND (&)

(REG >> n) & 1 — extracts bit n. Result is 0 or 1. Use to check if a flag is set.

if (GPIOA->IDR & (1 << 0)) { /* PA0 is HIGH */ } uint8_t bit5 = (REG >> 5) & 1;
Try it — pick the operation

Starting register value: 0b10100101 (0xA5). Pick an operation on bit 3 and watch exactly which bit moves.

Multi-bit fields — the complete read-modify-write pattern

When a register field is wider than 1 bit (e.g., 2-bit pin mode), you must clear the entire field first, then set the new value. The sequence is always: clear → set.

/* Set PA5 to output mode (MODER bits [11:10]) */ /* Step 1: clear bits 11:10 */ GPIOA->MODER &= ~(0x3 << (5 * 2)); /* 0x3 = 0b11 — clears both bits */ /* Step 2: set to output (01) */ GPIOA->MODER |= (0x1 << (5 * 2)); /* 0x1 = 0b01 — output mode */ /* Combined in one operation: */ GPIOA->MODER = (GPIOA->MODER & ~(0x3<<10)) | (0x1<<10);
NEVER use simple assignment on control registers GPIOA->MODER = 0x400; resets ALL 16 pins to input mode and sets PA5 to output. Every other pin loses its configuration. Always use read-modify-write.
Exception: atomic set/clear registers

Some registers like BSRR (GPIO bit set/reset) are designed for direct assignment — writing the upper 16 bits clears pins, lower 16 bits sets them. One write, no race condition. These are the exception; always check the datasheet.

Shift operators — your most-used embedded tool

(1 << n) creates a bitmask with only bit n set. This is the single most common embedded C idiom. Memorize it.

/* (1 << n) examples */ (1 << 0) = 0x00000001 /* bit 0 */ (1 << 5) = 0x00000020 /* bit 5 */ (1 << 16) = 0x00010000 /* bit 16 */ (1 << 31) = 0x80000000 /* bit 31 (MSB) */ /* Multi-bit mask */ (0x3 << 4) = 0b00110000 /* bits 5:4 */ (0xF << 8) = 0x00000F00 /* bits 11:8 */
Use UL suffix for 32-bit safety

In C, 1 << 31 may be undefined behavior (shifting into sign bit of int). Use 1UL << 31 or UINT32_C(1) << 31 to ensure unsigned 32-bit arithmetic. STM32 headers already do this — RCC_AHB1ENR_GPIOAEN expands to a safe value.

07

Worked Debugging Case

The firmware runs and the UART transmits — but the terminal shows garbage

Case file — UART output is unreadable although the code executes correctly
Initial symptom An STM32F4 application configures USART2 for 115200 baud, 8 data bits, no parity and one stop bit. The transmit function executes, the TX pin toggles, and no fault is reported — but the serial terminal displays random characters.
Possible causes

Terminal baud or frame format mismatch; incorrect GPIO alternate-function setup; wrong voltage level or poor wiring; incorrect USART oversampling; wrong BRR divider; or an incorrect assumption about the clock feeding USART2.

Debugging rule

Do not begin by rewriting the UART driver. First classify the failure using evidence from the wire, the peripheral registers and the RCC clock tree.

Evidence gathered — symptom to root cause
ObservationEvidenceWhat it tells us
TX pin is active A logic analyser shows clean 0–3.3 V transitions with recognisable start and stop bits. The GPIO pin, alternate-function path and basic electrical connection are probably working.
Bit time is wrong Measured bit period is approximately 17.36 µs. 1 / 17.36 µs ≈ 57,600 baud — exactly half the intended 115,200 baud. This strongly indicates a clock or divider error.
BRR is internally consistent USART2->BRR reads 0x02D9. 0x02D9 is the divider expected when the code assumes an 84 MHz USART clock.
The actual peripheral clock is lower RCC configuration shows SYSCLK = 168 MHz and APB1 prescaler = 4, so PCLK1 = 42 MHz. USART2 is clocked from PCLK1. The APB timer x2 rule does not double the USART clock.
Actual root cause The BRR value was calculated using 84 MHz because the engineer applied the APB timer-clock doubling rule to USART2. USART2 actually receives 42 MHz from PCLK1, so the programmed divider made the real baud rate approximately 57,600 instead of 115,200.
The incorrect assumption and the correction
Before — wrong clock assumption
/* USART2 is on APB1. This value is wrong. */ #define BAUD_RATE 115200UL #define USART2_CLK_HZ 84000000UL /* For STM32F4, oversampling by 16: */ USART2->BRR = (USART2_CLK_HZ + (BAUD_RATE / 2UL)) / BAUD_RATE; /* Result: 729 decimal = 0x02D9 */ /* Actual baud with 42 MHz PCLK1: 42,000,000 / 729 ≈ 57,613 baud */
After — clock derived from RCC
#define BAUD_RATE 115200UL /* SYSCLK 168 MHz, APB1 prescaler /4 */ const uint32_t pclk1_hz = 42000000UL; USART2->BRR = (pclk1_hz + (BAUD_RATE / 2UL)) / BAUD_RATE; /* Result: 365 decimal = 0x016D */ /* Actual baud ≈ 115,068 baud */ /* In a reusable driver, derive PCLK1 from RCC configuration rather than hard-coding it. */
Result after correction USART2->BRR reads 0x016D. The logic analyser measures a bit period of approximately 8.68 µs, the terminal decodes every byte correctly, and repeated resets produce the same stable result.
The reusable debugging sequence
1

Observe the physical symptom

Confirm whether the pin is inactive, electrically noisy, or active with the wrong timing.

2

Measure before changing code

Use a scope or logic analyser to measure voltage levels, bit time and frame structure.

3

Read the hardware state

Inspect GPIO alternate-function registers, USART CR1/CR2/BRR and RCC prescalers in the debugger.

4

Trace the clock from source to peripheral

Do not assume every peripheral receives SYSCLK, or that timer-specific clock rules apply to UART, SPI or I2C.

5

Correct one cause and re-measure

A successful fix must be visible both in the register state and on the physical waveform.

What this case teaches Embedded debugging is not staring at C code until an error becomes obvious. It is a closed loop: symptom → physical measurement → register evidence → clock and architecture reasoning → correction → measured verification.
08

Tools and Setup

What you'll need and what each tool does in the embedded workflow

The embedded toolchain — five tools, one workflow
ToolWhat it doesWhen you use it
arm-none-eabi-gccThe C compiler for ARM. Translates your .c files into ARM machine code. The "none-eabi" means it targets bare metal (no OS, embedded ABI).Every build. Called by your Makefile or IDE automatically.
arm-none-eabi-ldThe linker. Combines compiled object files, assigns memory addresses, produces the final .elf using your linker script.Every build. Run automatically after compilation.
arm-none-eabi-objcopyConverts .elf (debug-rich) to .hex or .bin (programmer-friendly). The flash programmer needs .hex; keep .elf for debugging.Every build. Produces the file you flash to the chip.
OpenOCD / ST-LinkOn-chip debugger. Connects to the chip's JTAG/SWD debug port. Flash programming, breakpoints, register inspection — all through this.Flashing firmware and debugging via GDB.
GDB / STM32CubeIDEDebugger interface. Step through code, inspect registers, read memory live. STM32CubeIDE wraps GDB with a GUI.Debugging — and reading peripheral registers live.
Key files you will encounter
.c / .h files

Your source code. Headers define types and function prototypes. C files contain implementations. The compiler processes one .c file at a time.

.ld (linker script)

Tells the linker where to place code and data in memory. Defines the Flash start address, SRAM boundaries, section layout. Module 2 covers this in depth.

.s (startup assembly)

Runs before main(). Sets up the stack, copies initialized data from Flash to SRAM, zeroes uninitialized data, then calls main(). Every STM32 project has one.

.elf (executable)

The output of the linker. Contains the machine code plus debug symbols. Keep this — it's what GDB needs to show you meaningful backtraces and variable names.

.map file

Generated by the linker. Shows the exact Flash and SRAM address of every function and variable. The .map file tells you exactly why your binary is the size it is.

stm32f4xx.h / stm32f4xx_hal.h

ST's device header files. Define the peripheral register structures, bit field constants, and base addresses. These translate 0x40020014 into GPIOA→ODR for you.

Reading a peripheral register in the debugger

The single most powerful debugging skill: stopping execution and reading a peripheral's actual register values. This tells you the hardware state directly — not what your code intended, what actually happened.

/* In GDB: read GPIOA MODER register */ (gdb) x/1xw 0x40020000 0x40020000: 0x0c000000 /* Decode: 0x0C = 0b00001100 */ /* bits [7:6] = 11 → PA3 = analog mode (unexpected?) */ /* All other bits = 0 → all other pins are input mode */ /* Or using peripheral view in STM32CubeIDE: */ /* Window → Show View → Registers → expand GPIOA */ /* See every field decoded and labeled, live */ /* Check if GPIOA clock is enabled: */ (gdb) x/1xw 0x40023830 /* bit 0 = GPIOAEN — if 0, GPIOA registers ignore all writes */
Quick check — did it actually land?

Six real symptoms. Pick the root cause for each — this is exactly the debugging instinct Modules 1–9 will build on.

Score: 0 / 0 answered (6 total)