Skip to content

Start Here22 min read

What Firmware Is (and Is Not)

Firmware is the code that runs closest to the hardware: the program inside your washing machine, the motor controller in a drone, the code in a USB keyboard that turns key presses into packets. This lesson defines firmware precisely, places it in the stack from transistors to applications, shows you the constraints that make firmware engineering its own discipline, and explains how this course is organised so you can study it effectively.

Firmware is software that is written for one specific piece of hardware, ships inside that hardware, and is responsible for making that hardware do its job. It usually runs without a general-purpose operating system, it usually lives in on-chip flash memory, and it usually cannot be replaced by the end user without special tools.

That definition is fuzzy at the edges on purpose. The word dates from the 1960s, when it described microcode that sat “between” hardware and software: firmer than software, softer than hardware. Today the useful distinction is about how tightly the code is bound to the hardware:

LayerWhat it isWho changes itExample
HardwareTransistors, wires, silicon, PCBChip designers, electrical engineersThe STM32F407 chip, the LED on pin PD12
FirmwareCode that knows the exact hardware: register addresses, pin numbers, clock speeds, timingFirmware engineers, shipped with the productThe code that sets bit 12 of GPIOD to turn the LED on
SoftwareCode that runs on top of an OS that hides the hardwareApplication developers, updated freelyA phone app, a web server

The boundary moves. A Linux kernel driver is “firmware-like” code living inside “software”. The BIOS or UEFI in a PC is called firmware even though it is millions of lines long. A smart thermostat may run firmware on one chip and Linux on another. What stays constant is the skill set: if your code writes to hardware registers, must meet timing deadlines, and has to fit in kilobytes, you are doing firmware.

  • It is not “just C”. C is the dominant language, but the hard part is the hardware model, the timing, and the debugging.
  • It is not software with smaller numbers. There is often no operating system to catch your mistakes. A wild pointer does not produce “Segmentation fault”: it silently overwrites a variable, or a peripheral register, and the device misbehaves an hour later.
  • It is not write-once. Modern devices receive firmware updates over the air for a decade. Updatability, security, and field diagnostics are part of the job.

Four families of chips run code close to hardware. Knowing the differences tells you which kind of firmware you will be writing.

ChipWhat is insideMemoryTypical codeExample
MCU (microcontroller)CPU core, flash, SRAM, peripherals on one dieKB to a few MB of flash, KB of RAM, all on chipBare metal C or an RTOSSTM32F407, nRF52840, RP2040
MPU (microprocessor)Faster CPU core(s) with an MMU, no or little on-chip memoryExternal DRAM (hundreds of MB), external flash or eMMCEmbedded LinuxNXP i.MX 8, TI AM335x
SoC (system on chip)Usually an MPU plus GPU, radios, DSPs, sometimes MCU cores, on one dieExternal DRAMLinux or Android, plus firmware on the helper coresPhone application processors, Raspberry Pi’s Broadcom chip
FPGAReconfigurable logic fabric, no fixed CPU (unless you add one)Configuration memory plus block RAMA hardware description (Verilog, VHDL), not a programXilinx/AMD Artix, Lattice iCE40

The term “SoC” is used loosely: vendors call many MCUs “SoCs” too, especially wireless ones such as the nRF52 or ESP32. The practical question is whether the chip has an MMU and runs Linux, or runs your code directly from on-chip flash. This course is about the second case.

Once you know the chip, you pick how much system software sits under your application.

Bare metal means your code is the only code on the chip. After reset, your startup code sets up memory and calls main, and main runs forever. You talk to peripherals by writing their registers directly. There is no scheduler, no file system, no memory protection unless you set it up. Bare metal is right for small, single-purpose devices: a battery charger, a sensor, a motor controller, a bootloader. It gives you complete control over timing and the smallest footprint.

RTOS (real-time operating system) means a small kernel, often 5 to 20 KB of flash, that gives you tasks (threads), a priority-based scheduler, queues, mutexes, and timers. FreeRTOS and Zephyr are the two you will meet most. Your code is still compiled into one image with the kernel, and there is still no MMU. An RTOS is right when the device does several independent things with different deadlines: read a sensor every 10 ms, maintain a Bluetooth connection, update a display, log to flash.

Embedded Linux means a full Linux kernel on an MPU with an MMU and external RAM. You get processes, a file system, networking stacks, and every Linux library. You give up boot time (seconds instead of milliseconds), determinism (latency is harder to bound), power (hundreds of mW instead of microwatts in sleep), and cost. It is right for devices with displays, cameras, complex networking, or large software stacks: routers, smart displays, industrial gateways.

QuestionBare metalRTOSEmbedded Linux
Typical flash/RAM4 KB to 1 MB / 1 KB to 256 KB32 KB to 2 MB / 8 KB to 1 MB64 MB+ / 64 MB+
Boot timeMicroseconds to millisecondsMillisecondsSeconds
Worst-case interrupt latencyTens of cycles, fully predictableTens of cycles plus kernel critical sectionsMicroseconds to milliseconds, needs care
Sleep currentMicroamps or lessMicroampsMilliamps
Concurrency modelSuperloop plus interruptsTasks plus interruptsProcesses, threads

Many real products combine all three: a Linux MPU for the user interface, an MCU running an RTOS for the radio, and a tiny bare-metal MCU managing the battery.

Job descriptions say “develop embedded software”. The day-to-day work looks like this:

  • Board bring-up. A new prototype PCB arrives from the factory. You are the first person to run code on it. You check that the debug probe connects, the clock starts, the UART prints, then work through every peripheral. When something does not work you have to decide whether it is your code, the chip, or the board.
  • Writing drivers. You read the chip’s reference manual (often 1,500+ pages) and a sensor’s datasheet, then write the code that configures the peripheral, moves data, and handles errors.
  • Debugging with instruments. Your tools include a debugger connected over SWD, but also an oscilloscope to see whether a signal actually toggles, a logic analyzer to decode SPI or I2C traffic, and a multimeter or power profiler to see current draw. “The code says it sent the byte” and “the byte appeared on the wire” are different claims.
  • Reading datasheets and errata. Chips have bugs. Vendors publish errata sheets listing them. Experienced engineers read the errata before trusting a peripheral.
  • Working with electrical engineers. You review schematics (“this pin cannot do I2C”), agree on pin assignments, and explain firmware’s needs (a test point here, a pull-up there). When a board misbehaves you debug it together.
  • Making it robust. Watchdogs, fault handlers, safe firmware updates, power-fail-safe storage. A crashed phone app restarts; a crashed pacemaker or car brake controller is a disaster.
  • Fitting it. Squeezing a feature into the last 3 KB of flash, or cutting average current from 80 to 40 microamps so the battery lasts two years instead of one.

Most of this course teaches the knowledge behind those tasks. The later parts teach the professional practices (testing, CI, security, release management) that separate hobby projects from products.

Every layer in a microcontroller builds on the one below it. You will work mainly in the top four, but bugs come from all of them.

+-----------------------------------------------+
| Application: "sample sensor, send over BLE" | your logic
+-----------------------------------------------+
| Services: scheduler, logging, storage, FSMs | your code or an RTOS
+-----------------------------------------------+
| Drivers: uart_write(), spi_transfer() | your code
+-----------------------------------------------+
| C code: *(volatile uint32_t *)0x40020C14 = x | the language
+-----------------------------------------------+
| Memory map: flash, SRAM, peripheral regs | one address space
+-----------------------------------------------+
| Bus: AHB / APB matrix connecting everything | silicon
+-----------------------------------------------+
| Peripherals: GPIO, UART, timers, ADC, DMA | silicon
+-----------------------------------------------+
| CPU core: Cortex-M4 (registers, ALU, NVIC) | silicon (Arm design)
+-----------------------------------------------+
| Digital logic: flip-flops, gates, clocks | silicon
+-----------------------------------------------+
| Transistors, voltage levels, the PCB | physics
+-----------------------------------------------+

Here is one concrete trip down the stack. Your application decides to turn on the green LED. Your GPIO driver computes that this means “set pin 12 of port D”. In C that becomes a store of the value 1u << 12 to address 0x40020C18 (the BSRR register of GPIOD). The compiler turns that into a str instruction. The Cortex-M4 core executes it and puts a write transaction on its bus. The bus matrix routes the address to the AHB1 bus, where GPIOD lives. The GPIO peripheral’s logic latches the bit into its output register. That register drives a transistor pair connected to the physical pin, which pulls the pin to 3.3 V, and current flows through the LED.

If any layer is wrong, the LED stays dark. If GPIOD’s clock is not enabled, the peripheral ignores the write. If the pin is not configured as an output, the output register changes but the pin does not. If the LED is soldered backwards, all the firmware is correct and nothing happens. Firmware engineering is the skill of reasoning across these layers.

These are the facts of life that make firmware different from application programming:

  • Memory measured in kilobytes. The STM32F407 is a generous MCU with 1 MB of flash and 192 KB of RAM. Many products use chips with 32 KB of flash and 4 KB of RAM. You will count bytes, and you will care whether a table lives in flash or RAM.
  • No operating system underneath. Nobody loads your program, cleans up after it, or catches its crashes. Your code sets up the stack, initialises memory, and configures the clocks.
  • No MMU. Every pointer can reach every byte of the address space, including peripheral registers. Writing through a bad pointer can reconfigure hardware.
  • Real-time deadlines. A motor control loop that runs 50 microseconds late can destroy the motor. Correctness includes timing: a right answer delivered late is wrong.
  • Power budgets. A coin cell holds about 200 mAh. Running at 30 mA, it lasts under 7 hours. Sleeping at 5 microamps and waking briefly, it lasts years. Firmware decides which.
  • It cannot crash. Or more precisely, when it crashes it must recover by itself, safely, without a human. Devices sit in walls, fields, and bodies for years.
  • Hardware is shared with you. Peripherals run in parallel with your code. Interrupts arrive between any two instructions. Concurrency bugs exist even in a program with no threads.

You will encounter many MCU families. This course uses one, but the concepts transfer to all of them.

FamilyVendorCoreKnown for
STM32STMicroelectronicsArm Cortex-M0 to M7, M33Huge range, cheap boards, the industry default
nRF52 / nRF53 / nRF54Nordic SemiconductorCortex-M4F, M33Bluetooth Low Energy, excellent low power
ESP32EspressifXtensa LX6/LX7, RISC-V on newer partsWi-Fi and Bluetooth at very low cost
RP2040 / RP2350Raspberry PiDual Cortex-M0+ / dual M33 or RISC-VProgrammable I/O blocks, cheap, well documented
AVRMicrochip (originally Atmel)8-bit AVRClassic Arduino boards, very simple
PICMicrochip8, 16, 32-bit PIC coresLong industrial history, tiny parts
MSP430Texas Instruments16-bit MSP430Ultra-low power, metering
RISC-V MCUsMany (Espressif, WCH, GigaDevice, SiFive)Open RISC-V ISARoyalty-free ISA, growing fast

Arm does not make chips. It designs CPU cores and licenses them to chip vendors, who add memory and peripherals around the core and sell the result. Cortex-M is Arm’s family of cores for microcontrollers. They share a programmer’s model: the same Thumb instruction set, the same interrupt controller (the NVIC), the same system timer (SysTick), the same memory map layout, and the same debug interface.

That is why learning one Cortex-M chip teaches you most of the others. An STM32, an nRF52, and an NXP LPC all handle interrupts the same way because Arm designed that part. What differs between vendors is the peripherals: ST’s UART registers look nothing like Nordic’s.

The common members are the M0 and M0+ (smallest, cheapest), M3 (the original mainstream core), M4 (M3 plus DSP instructions and an optional single-precision FPU), M7 (fast, with caches), and the ARMv8-M cores M23 and M33 (which add TrustZone security). The STM32F407 uses an M4 with the FPU, written Cortex-M4F.

Every worked example in this course targets the STM32F407VG, the chip on ST’s STM32F4 Discovery board. It has a Cortex-M4F core that runs up to 168 MHz, 1 MB of flash, 128 KB of SRAM plus 64 KB of core-coupled memory, and a built-in ST-Link debug probe, so a single USB cable powers, programs, and debugs it. The board has four user LEDs on PD12 (green), PD13 (orange), PD14 (red), and PD15 (blue), a user button on PA0, an accelerometer on SPI, and an audio DAC on I2C. You will use all of them.

No board? You can still do almost everything:

  • Renode is an open-source simulator from Antmicro that models whole boards, including the STM32F4 Discovery. It runs your unmodified ELF file, simulates GPIO, UART, timers, and more, and supports scripted tests. It is the recommended option.
  • QEMU can emulate an STM32F405-based board with qemu-system-arm -M netduinoplus2. Its peripheral model is limited, but it is good for UART-only examples.

A simulator is never a perfect substitute: it will not show you a noisy signal, a marginal clock, or a solder bridge. But it runs the exact same machine code, so everything about C, memory, startup, and interrupts carries over.

The course has five parts plus a capstone:

  1. Start Here (this part): what firmware is, setting up tools, the mental model of a running program.
  2. The C Language: all of C, taught with the machine in mind. This is the heart of the course and the longest part. You will compile and run most of it on your computer before touching hardware.
  3. C Meets the Metal: reading assembly, registers, startup code, linker scripts, the Cortex-M architecture, clocks, and every common peripheral, all written from scratch with no vendor library.
  4. Firmware Systems: driver design, state machines, RTOS internals, concurrency, bootloaders, security, debugging, power, and build systems.
  5. Professional Firmware: testing, communication stacks, storage, design patterns, architecture, safety standards, embedded Linux, and production.
  6. Capstone: a complete battery-powered sensor node with a bootloader and firmware updates.
  • Type everything. Do not copy and paste code. Typing forces you to read each line, and the typos you make teach you what compiler errors look like.
  • Break things on purpose. When a lesson says “without volatile this loop is optimised away”, remove volatile and look at the disassembly. When it says “forgetting the clock enable does nothing”, forget it and watch nothing happen. You will meet these bugs in real work; meet them first where you know the cause.
  • Read what the tools tell you. Compiler warnings, map files, disassembly, and register dumps are how professionals see inside the machine. Every lesson asks you to read at least one of them.
  • Do the exercises. The quiz checks understanding; the exercises build skill. Skipping them is the most common way to finish a course and still be unable to do the job.
  • Go in order the first time. Later lessons assume earlier ones. After that, use the reference pages and jump around.
  • Treating firmware as “small software” and expecting an OS to catch errors: assume every bad pointer silently corrupts something, and design defensively.
  • Starting with a vendor HAL and code generator before understanding registers: learn the register level first so you can debug the HAL when it fails.
  • Trusting the code instead of the hardware: when a signal “should” be there, measure it with a scope or logic analyzer.
  • Ignoring the errata sheet: read it before blaming your own driver for an unexplained peripheral bug.
  • Choosing embedded Linux for a job an MCU can do: check boot time, power, and cost requirements first.
  • Learning on a board with no debugger: pick hardware with an on-board probe (like the Discovery’s ST-Link) so you can single-step from day one.
  1. Pick three devices in your home (for example a microwave, a TV remote, a router). For each, decide whether it most likely contains an MCU running bare metal, an MCU running an RTOS, or an MPU running Linux. Write one sentence of reasoning for each based on the constraints in this lesson (power source, user interface, network features).
  2. Find the label on any electronic board you own, or look up the main chip of a product you use. Identify the vendor and, if it is a microcontroller, which CPU core it uses. A correct answer names the core family (for example “Cortex-M0+” or “Xtensa LX7”).
  3. Using the numbers in this lesson, compute how long a 200 mAh coin cell lasts at a constant 30 mA, and at an average of 10 microamps. You should get about 6.7 hours and about 2.3 years.
  4. Redraw the transistor-to-application stack diagram from memory, then annotate where each of these bugs lives: the LED is soldered backwards; the GPIO clock is not enabled; the compiler removed a write because a pointer was not volatile; the application logic never asks for the LED to turn on. Each bug belongs to a different layer.
  5. Write a one-page “study contract” for yourself: how many hours per week, which hardware option (Discovery board or Renode), and where you will keep your exercise code (a Git repository is the right answer). Keep it; you will revisit it at the end of the course.

Question 1:A product must sleep for months on a coin cell, wake every second to read a sensor, and send a radio packet. Which software base fits best?

Question 2:Your code writes the correct value to a GPIO output register, but the pin does not change. Which explanation is impossible on a microcontroller with no MMU?

Question 3:Why does learning interrupts on an STM32F407 transfer well to an nRF52840?

Question 4:A pointer in your bare-metal program is uninitialised and your code writes through it. What is the most likely immediate result?

Question 5:What distinguishes an MCU from an MPU most directly?

  • Firmware is code bound to specific hardware: it knows register addresses, pins, and timing, and it ships inside the product.
  • MCUs run firmware directly from on-chip flash; MPUs with MMUs and external RAM run embedded Linux. Pick by power, boot time, determinism, and cost.
  • Bare metal, RTOS, and embedded Linux are points on a spectrum of how much system software sits under your code.
  • With no OS and no MMU, mistakes do not crash loudly. They corrupt memory or hardware state silently, so firmware must be defensive.
  • Arm designs Cortex-M cores; vendors add peripherals. Core features (NVIC, SysTick, exceptions) transfer across vendors, peripherals do not.
  • This course targets the STM32F407 on the STM32F4 Discovery, with Renode as the simulator when you have no board.

Next: In Setting Up Your Toolchain you will install the compilers, debugger, and simulators used for the rest of the course and verify each one works.