Skip to main content

48 docs tagged with "embedded"

View all tags

A GPIO Driver from Scratch

The blink program drove one pin with two #defines and it was the right amount of code for one pin. The second pin costs another two, the first alternate-function pin costs four, and by the tenth you have shift arithmetic scattered across the codebase with the pin number written out by hand in each place. The fix is not a HAL. It is about eighty lines that name what the hardware already does.

Analog Basics: ADC and DAC

An analog-to-digital converter looks, from firmware, like a register you read. That framing hides the two things that actually determine whether the number is right. First, the conversion is a comparison against a reference, so the answer is a ratio, not a voltage — and a wrong or noisy reference is invisible in the result. Second, before any comparing happens the converter must charge a small capacitor through your circuit, and if you did not give it long enough, it will confidently report the voltage it managed to reach rather than the voltage that was there.

Bare-Metal, RTOS, or Linux

An engineer coming from application software tends to reach for the environment closest to what they already know — an RTOS, or better yet Linux, because it has threads and a filesystem and feels familiar. That instinct is worth resisting. Every layer of software you add between your code and the hardware costs something real: flash and RAM you don't get back, boot time, a scheduler whose behavior you now have to understand rather than one you wrote yourself, and — for Linux specifically — an MMU-capable microprocessor in the bill of materials at all. The right choice is the cheapest one that actually meets the product's requirements, and picking a heavier environment "to be safe" is itself a common and expensive mistake. This page exists to make that trade-off concrete instead of a matter of taste.

Build Systems and Vendor Tooling

Choosing a build system for firmware feels like a taste question and is not. The real question underneath it is who owns the generated code, and it has consequences that outlive the project: whether a colleague can build your firmware without installing an IDE, whether CI can build it at all, and whether the day you need to change a pin assignment costs ten minutes or a merge conflict across forty files you did not write.

C Libraries: newlib, newlib-nano, picolibc

The C standard library is written as though a process exists. printf writes to a file descriptor. malloc asks the kernel for more address space. exit tells a parent that a child finished. fopen needs a filesystem, time needs a clock somebody set, errno needs somewhere thread-local to live. On a Cortex-M4 with no OS, not one of those things is true — and yet #include compiles fine and printf links, because the library was built with the bottom of every one of those paths left as a hole for you to fill.

Choosing a Toolchain

For a hobby project the toolchain question is nearly free: install Arm GNU, move on. For a product it is one of the longest-lived decisions in the codebase. A toolchain choice outlives the engineers who made it, because the compiler is baked into every build artefact you have ever released and into every certification argument you have ever made. Changing it later is not a flag change; it is a re-qualification.

Clocks and Oscillators

On a desktop machine the clock is somebody else's problem — it was configured by firmware you never see, and by the time your program runs it is a constant. On a microcontroller you are that firmware. The chip comes out of reset running on a cheap internal RC oscillator at a fraction of its rated speed, with almost every peripheral's clock switched off, and the first job your code has is to build the clock tree the rest of the system will run on. Nothing you write behaves as intended until that is done.

CMake for Embedded

CMake's defaults encode one assumption so deeply that it is easy to miss: the machine running the build can also run what the build produces. That is what lets CMake test a compiler by compiling and linking a tiny program, what lets findpackage look in /usr/lib, and what lets checkcsourceruns exist at all. Every one of those assumptions is false for a Cortex-M4 with 128 KB of RAM and no operating system.

CMSIS and Vendor HALs

"Bare metal" does not have to mean "type every address yourself". Between raw pointer casts and a full vendor framework there are three or four distinct layers, each with a different bargain, and the useful skill is knowing which one you are standing on and why — not picking a side.

Configuring the Clock Tree

Out of reset the STM32F411RE runs at 16 MHz on an internal RC oscillator, which is a deliberately conservative choice: it works with no crystal, no configuration, and no risk. It is also one sixth of what the part can do, it is accurate to about ±1 % over temperature rather than the ±20 ppm a crystal gives, and it cannot produce the 48 MHz that USB requires. Somewhere in the first week of a real project you will need to change it.

Cross-Compilation

The compiler on your laptop is not a general-purpose translator that happens to be pointed at x86. It is a program that was built to emit x86-64 instructions, linked against a C library that was built assuming a Linux kernel is underneath it, and wired to a startup object that assumes something already created a process, set up a stack, and handed it argc and argv. Every one of those assumptions is false on a microcontroller. That is the whole reason a separate toolchain exists — not because the MCU is "different hardware", but because three independent layers of the build all encode a machine and an environment, and all three have to change together.

Cross-Compilation

Cross-compilation builds executables for a different platform (target) than the one running the compiler (host). Essential for embedded systems, mobile development, and deploying to different architectures.

Embedded Systems

Embedded software runs on hardware that was never meant to run much of anything: a few hundred

Exceptions and the Vector Table

On most processors, getting from "an interrupt line went high" to "my C function is running" involves software: a dispatcher reads a status register, works out which source fired, and calls the right handler. Cortex-M does none of that. The hardware reads a table of function pointers at a known address, indexes it by exception number, and branches — having already pushed the registers a C function is allowed to clobber. Your handler is an ordinary function with an ordinary prologue, and it starts running a fixed and small number of cycles after the event.

Flashing and Programming

On a hosted system, "running the program" means handing a file to a loader. Here it means writing your image into non-volatile memory inside the chip and then resetting it, using a second piece of hardware that talks to the silicon over a two-wire debug port. That second piece of hardware is doing considerably more than copying bytes: it halts the core, drives the flash controller through a sequence the reference manual specifies, verifies, and releases reset.

Floating Point and DSP Extensions

Writing float in C on a microcontroller does not tell you what the hardware will do. The same line of source can compile to one instruction, to a forty-cycle library call, or to a two-hundred-cycle double-precision emulation — and the compiler chooses silently, based on flags you may not have set deliberately. Nothing in the source distinguishes the three cases, which is why "why is my control loop suddenly missing deadlines" is so often a floating-point question.

Glossary

Embedded engineering has its own vocabulary, and a lot of it is acronyms that mean something quite specific in this field even when the letters look familiar from elsewhere. The problem isn't that the terms are hard — it's that skimming past one you half-recognize (assuming "MPU" means the same thing every time, or that "RTOS" is just "a small OS") is exactly how a plausible-but-wrong mental model gets built, and those are expensive to unlearn once you've written code around them. This page defines the terms every later folder in this section assumes you already know, once, in one place, so you can look one up instead of re-deriving it from context. Where a term's proper home is a folder that doesn't exist in this build yet, it's still defined here — it just isn't linked anywhere yet.

How a GPIO Pin Really Behaves

GPIOA->ODR |= (1 << 5); looks exactly like every other memory write you have ever done, and that resemblance is the problem. On the far side of that register is not a bit of storage but a pair of transistors, wired to a physical pin, with a maximum current, a maximum switching rate, and a real-world net on the other end that may already be being driven by something else. The register model hides all of it, right up until the moment it matters.

How This Section Is Organised

Sixteen folders is a lot to land on with no map. The organising idea behind this section is that embedded engineering isn't one linear skill you learn top to bottom — it's several tracks (hardware, the toolchain, bare-metal fundamentals, concurrency, connectivity, safety, and so on) that a real project touches in a different order depending on what you're actually trying to do. A folder map tells you where a topic lives; a learning path tells you a sane order to read folders in for a specific goal. This page gives you both, plus the one policy that explains why some pages here link out to computer-science/ instead of repeating material you might expect to find locally.

Lab Equipment and What It Answers

The debugger on your Nucleo can single-step your code, read every register, and show you the contents of memory — and it is blind to everything that happens outside the package. It will tell you, truthfully, that you wrote 0xA5 to the SPI data register. It cannot tell you whether 0xA5 left the pin, whether the clock that carried it was clean, whether the device on the other end was even powered.

Memory Sections and VMA vs LMA

Every variable and every function in your firmware ends up in one of about six buckets, and which bucket it lands in is decided by two things: whether it is code or data, and whether its initial value is zero. That is nearly the whole rule. int counter; goes in .bss because its initial value is zero. int counter = 5; goes in .data because it is not. const int limit = 5; goes in .rodata because it never changes and can therefore stay in flash. Nobody chose those placements for your variable; the compiler applied that rule and emitted a section name.

Microcontroller, Microprocessor, SoC

"It's an ARM chip" tells you almost nothing useful. The question that actually matters — can this thing run Linux, does it need a bootloader partition scheme, will your firmware fit without an external memory chip, is there hardware memory protection between tasks — all comes down to one boundary: where the code and data live relative to the CPU core, and whether there's a hardware unit that translates and protects memory addresses on the way there. That boundary is what separates a microcontroller from a microprocessor, and it's a hardware property you can check on a datasheet, not a marketing category.

Optimization for Size and Speed

Raising the optimization level is the one build change that routinely alters what a firmware does. Not what it does more quickly — what it does. A delay loop disappears. A register write that was there at -O0 is gone at -O2. Code that worked for two years starts failing, and nothing in the source changed.

Power Supplies and Regulators

Firmware is written as though the supply rail were a constant — a number in the datasheet, 3.3 V, always there. The rail is not a constant. It is the output of a control loop with finite bandwidth, fed through traces with real resistance and inductance, feeding a load whose current draw your own code is modulating thousands of times a second. Every time the CPU switches from an idle loop to a burst of floating-point work, every time a GPIO drives an LED, every time the chip wakes from Stop mode, the load steps and the rail moves.

Privilege Modes and the Two Stacks

A Cortex-M has two independent switches that most bare-metal firmware never touches, and that an RTOS depends on completely. One decides which stack the processor is using; the other decides whether the code running is allowed to change anything important. They are separate — you can be unprivileged on the main stack or privileged on the process stack — and confusing them is the source of a lot of half-right explanations.

Reading a Datasheet

Coming from software, the instinct when you meet a new chip is to look for "the docs" — one document, searchable, that tells you everything. That document does not exist, and looking for it is the reason people bounce off hardware. Silicon vendors ship a set of documents, deliberately separated, because they answer questions that different people ask at different times: the person choosing a part, the person laying out the board, the person writing the firmware, and the person whose product works on the bench but fails one unit in fifty. Each document is written for one of those people and is close to useless for the others.

Reading a Schematic

A schematic is not a picture of a board. It is a graph: components are nodes, and the wires between them — nets — are edges. Two points drawn at opposite corners of the page with the same net label are the same electrical point, as surely as two references to the same object in memory. Once you read it as a graph rather than as a drawing, the intimidating density stops mattering, because you are never reading the whole thing. You are tracing one path.

Reading the Map File

Every firmware project reaches the same afternoon. The build that fitted last week does not fit this week, or it fits but the RAM figure has doubled, and nobody changed anything that should have cost 12 KB. The instinct is to start deleting features. The correct move is to ask the toolchain, which has known the answer the whole time and wrote it down.

Register-Level Programming

A peripheral is a piece of digital logic sitting on the same bus as your RAM. It has no API, no calling convention and no way to be invoked. The only interface it exposes is a small block of addresses: write a word to one of them and some flip-flops change state; read from another and you get the current state of some wires. That is the whole model.

Reset and Boot Configuration

There is a gap between the moment power reaches the chip and the moment your first instruction executes, and firmware engineers habitually treat it as empty. It is not. In that gap the supply supervisor decides whether the rail is trustworthy, a pulse generator stretches whatever event caused the reset into a signal long enough for every block on the die to see it, an option-byte loader runs, boot-mode pins are sampled and latched, an address decoder is reconfigured so that a completely different memory appears at address zero, and only then does the CPU fetch two words and start running.

RISC-V for Arm Developers

Everything in this folder so far has described one architecture. The reason that is a reasonable way to spend eleven pages is that Cortex-M is what the overwhelming majority of microcontroller work is written against. The reason it is not the only thing worth knowing is that RISC-V parts are now genuinely shipping in volume — the ESP32-C3 in a hobbyist's hands, the CH32V003 at ten cents, the RISC-V management cores inside SoCs whose application processors are Arm — and the transition is much easier than a new instruction set sounds, provided you know which of your Cortex-M assumptions are architectural and which are Arm's.

Signal Integrity and Noise

A schematic draws a wire as a line with no properties. That abstraction holds beautifully for DC and falls apart on the edges — the few nanoseconds after a driver switches, when the wire is not a connection but a component, with inductance, capacitance, a characteristic impedance and a finite speed. For most of the time your signal is idle and the abstraction is fine. For the small fraction of time when it is changing, the wire is the circuit.

Startup Code: Reset to main

C has a set of guarantees that programmers stop noticing after the first month. Initialised globals hold their initialisers. Uninitialised globals are zero. The stack works. printf has somewhere to write. Static C++ objects have had their constructors run before main starts. On a hosted system a program loader and a crt0 object you have never opened deliver all of that before your first line executes.

SysTick and the Core Peripherals

Almost everything on a microcontroller is the vendor's. The timers, the UARTs, the clock tree, the GPIO blocks — all of it is ST's design, at ST's addresses, described in ST's reference manual, and none of it transfers to a part from a different vendor. A handful of peripherals are not like that. They are Arm's, they are built into the processor itself, they sit at the same addresses on every Cortex-M ever made, and code that uses them ports between vendors without changes.

The Cortex-M Family

Arm does not sell chips. It sells processor designs, and a silicon vendor — ST, NXP, Nordic, Raspberry Pi — licenses one, wraps it in memory and peripherals, and sells you the result. That arrangement is why "it's an Arm chip" tells you almost nothing on its own, and why the useful question is always which Arm core, in which configuration, from which vendor.

The Cortex-M Memory Map

A microcontroller has one 4 GB address space and everything lives in it: flash, RAM, every peripheral register, the interrupt controller, the debug hardware. There is no MMU, so what you write in a pointer is the physical address the bus sees. That is the simplification that makes bare-metal firmware tractable — and it means the layout of that space is not a vendor's private business but part of the architecture you program against.

The Embedded Landscape

Picking a chip for a project isn't like picking a library — you can't easily swap it out six months in. The instruction set, the vendor's toolchain, the peripheral register layout, and the ecosystem of drivers and examples around a part are all things you commit to for the life of the product, and products in this field often live for years. So "which family of hardware" is really a question about which toolchain, which debugging workflow, and which vendor's support model you're signing up for — the raw specs are almost a secondary concern. This page maps the major families so that when a later folder says "on Cortex-M" or "targeting RISC-V," you have a sense of where that sits in the wider landscape and what it commits you to.

The Linker Script

On a hosted system nobody writes a linker script, because the answer to "where does this code go" is "wherever the loader decides", and the loader is part of the OS. On a microcontroller there is no loader. The addresses in the binary are the addresses the CPU will use, forever, and something has to choose them. That something is a text file, usually about a hundred lines long, that most projects copy once and never read.

The Memory Protection Unit

A null-pointer write on a Cortex-M does not crash. Address 0x00000000 is real memory — it is the start of flash, or the boot alias — so *(uint32t )0 = 42 on a fresh chip silently does nothing at all, and the program carries on. A stack that overflows its intended region does not crash either; it grows down into .bss and corrupts variables that belong to something else, and the failure surfaces minutes later in code that is entirely innocent. Both are the same problem: on a bare Cortex-M, memory has no permissions, so a wrong access is indistinguishable from a right one.*

The NVIC

The vector table answers where. The Nested Vectored Interrupt Controller answers whether, when, and in what order — and it is the only part of the exception model you actively program. Every interrupt line on the chip arrives at the NVIC; the NVIC decides whether that line is enabled, records it as pending if it is not yet serviceable, compares its priority against whatever the processor is currently doing, and hands the winner to the core.

The Register Model

A Cortex-M shows you sixteen 32-bit registers at any moment, and thirteen of them are genuinely interchangeable scratch space. The other three are the processor's control surface — a stack pointer that is secretly one of two, a link register that sometimes holds a return address and sometimes holds a magic number, and a program counter that is one bit weirder than it looks. Alongside those sit half a dozen special registers that are not in the main file at all: you cannot load or store them, and the only way to touch them is a pair of dedicated instructions.

Thumb-2 and Code Density

On a desktop CPU, instruction encoding is an implementation detail you can go a whole career without thinking about. On a microcontroller it is a budget. The STM32F411RE has 512 KB of flash and 128 KB of RAM, and that flash figure is one of the two or three numbers that set the price of the chip. Every byte an instruction occupies is a byte of product cost, multiplied by the production run.

Voltage Levels and Logic

There is no 1 on a wire. There is a voltage, and there is a receiver that has decided in advance which range of voltages it will call one and which it will call zero. Digital logic is an agreement layered on top of an analogue quantity, and the reason firmware normally gets to ignore that is that the agreement usually holds — the hardware on both ends was designed to the same convention, so the bits you read are the bits that were sent.

What "Embedded" Actually Means

Ask most software engineers what "embedded" means and they'll say something about small chips, or soldering, or blinking an LED. That's the wrong mental model, and it's why so many engineers who are perfectly competent on servers and desktops write their first firmware the way they'd write a desktop app — and then spend a week debugging failures that a desktop never produces. Embedded isn't defined by the chip. A phone's application processor and a pacemaker's microcontroller are both "chips," and the software practices around them could not be more different. What actually defines the field is a fixed set of constraints that never fully goes away, no matter how big or small the target is. Understand the constraints, and the rest of this section — why bare-metal code looks the way it does, why an RTOS exists, why "just add more RAM" isn't always an option — falls out as a consequence rather than a pile of arbitrary rules to memorize.

What Hardware to Buy

Firmware is the one branch of software engineering where you genuinely cannot do the work on the machine you write the code on. A simulator will run your main(), but it will not show you that the sensor holds the clock line low for 40 microseconds longer than the datasheet suggests, that your board browns out when the motor starts, or that the pin you thought was an output has been floating since reset. Every important lesson in this section arrives through a physical board, and the reason newcomers stall here is not the money — the whole kit costs less than a mid-range monitor — but the catalogue. There are hundreds of development boards, every tutorial assumes a different one, and nothing on the vendor's site tells you which one the thing you are reading was written against.

What volatile Does and Does Not Do

volatile has a reputation for being either a magic word that makes hardware access work or a deprecated relic that nobody should use. Both readings come from the same place: people learn what it does by observing that adding it fixed a bug, and never learn the boundary of the guarantee. The boundary is narrow, it is written down precisely in the C standard, and knowing exactly where it stops is what separates code that works from code that works on your desk.

Your First Bare-Metal Blink

Blinking an LED from an Arduino sketch takes two lines and teaches nothing about the machine. Blinking one with no HAL, no IDE and no library takes about ninety lines spread over five files, and by the end you know where every byte of the image came from, what the processor did before your first instruction, and which two writes in the whole program actually made the light change.