Skip to main content

Debugging a HardFault on a Cortex-M: from a mystery to a line of code

· 12 min read

Every embedded developer knows the moment: the firmware has been running, and then it does not. The debugger shows that the program sits in an infinite loop with a name like HardFault_Handler or Default_Handler, and the call stack is a few meaningless frames. That loop is the default handler of the startup code, and it says exactly nothing about what happened. The fault has a cause, and the processor has already written it down: in eight registers on the stack and in four status registers. It is only necessary to read them.

This article shows how to turn the loop into a line of source code, in two real examples that I ran in QEMU (a model of an STM32F4 board) and examined with GDB: a call of a NULL function pointer and a write to an address where nothing is. All outputs are real.

Running the real firmware without the hardware: a simulator in the CI

· 10 min read

In the article about unit testing with Unity and CMock I said that the tests on the PC do not find "the differences between the PC and the MCU". This article is about those differences, and about the tool that finds them: a simulator that runs the real binary. Not the logic compiled for the PC, but the very .elf that the ARM compiler made, with the real startup code and the real linker script, on a model of a processor and its peripherals.

I will show two experiments that I ran: the same test source that passes on the PC and fails on the Cortex-M4, and a firmware that writes to the registers of a USART at the real addresses of an STM32F4 and whose text appears in the terminal. At the end there is an honest list of what a simulator will not tell you.

Reproducible builds: pin the tools, pin the sources, remove the clock

· 11 min read

A customer calls about a firmware that you delivered fourteen months ago. You check out the tag, build it, flash it and the bug is not there. Is the bug gone, or is it a different firmware? If your build is not reproducible, you cannot tell. Nobody can tell, because the binary that you have just built differs from the delivered one in ways that you can neither see nor explain: another version of the compiler, a library that was updated on the PC, the path of the folder, the time of the day.

A reproducible build has a simple definition: the same inputs give the same bytes. This article shows what the inputs of a firmware build are, how the Embedbits platform pins them (the Artifacts Handler), and an experiment that I did: two builds of the same source, in two folders at two different times, give two different binaries. Then three changes later they give the same one, to the last bit.

Designing a peripheral API: eight decisions behind the Gpio module

· 11 min read

The GPIO is the simplest peripheral of a microcontroller: a pin is high or low. That is exactly why it is a good subject for an article about the design of an interface. There is no hardware complexity to hide behind, and every decision is a choice of the designer: how a pin is named, what a function returns, where the polarity of an LED lives. The MCAL module Gpio of the Embedbits BSP is a real example with real answers, and I will go through them one by one, with the alternatives and the price.

The code from the module is quoted from the STM32H5 branch of Bsp-Mcal-Gpio. The examples that use it were compiled and run against the real Gpio_Port.h and Gpio_Types.h, with a small fake of the implementation, so that the API could be tried on a PC.

Doxygen for embedded C: documentation that cannot be forgotten

· 9 min read

Every project has documentation, and every project has documentation that lies. A Word file with the description of the interface was correct in the week when it was written. A comment above the function is more honest, because it is a few lines from the code that it describes, but it is also written by the same people who forget. The way out is not more discipline, it is a tool that reads the comments, builds the documentation from them and fails the build when something is missing. That tool is Doxygen.

This article shows what a documented module looks like in my projects, how it is generated, and how to make the documentation a part of the CI that cannot be skipped. The examples were built with Doxygen 1.9.8 and Graphviz, and the outputs are real.

CMake for embedded firmware: a build that you can read

· 11 min read

An IDE project is a file that nobody reads: a few thousand lines of XML that the IDE writes and the IDE reads, and that nobody can review in a pull request. The build then exists only on the computer where somebody clicked it together. CMake solves it with a different philosophy: the build is a text that you read, review, version and run in the CI exactly the same way as on your desk.

The build system of the Embedbits platform (EmBi_Platform) is made in CMake, and this article explains the pieces that every embedded CMake build needs, on a small project that I built and ran: a toolchain file, flags with names, build types, the modules as libraries, the linker script, the files after the link and a test that runs the firmware in a simulator. All numbers and outputs are real.

Interrupts and main(): how to share data safely

· 11 min read

An interrupt handler and the main loop are two programs that run in the same memory and do not know about each other. The C compiler does not know that the interrupt exists, and the CPU does not know that two variables belong together. The programmer is the only one who knows, and the bugs that follow are the worst kind: they appear once in a thousand runs, they disappear when you attach the debugger and they never appear in the code review, because the code looks right.

This article goes through the three problems of the shared data, one by one, with the code that fails, and then shows the patterns that work. The experiments run on a Cortex-M4 (in QEMU, on a model of an STM32F4 board) or on a PC, and the outputs are real.

Finite state machines in practice: a button with debounce and long press

· 11 min read

In the first article about finite state machines I gave you the template and ended with "to be continued". This is the continuation, and the best way to continue a theory is a problem. I chose the one that every embedded project has and that nobody gets right on the first try: a push button.

A button looks trivial: a pin, high or low. But a mechanical contact bounces, so the pin does 1 0 1 1 0 1 before it settles, and the product usually wants two different things from the same button: a short press and a long press. Written with flags and counters in the main loop, it ends as a few ifs that depend on each other in a way that nobody can explain after a month. A state machine solves it in a way that you can explain with a table.

One BSP, many STM32 families: why Git branches, and what they cost

· 11 min read

STM32 is not one microcontroller, it is a dozen families: the G4, the H5, the U5, the F4 and so on. They have the same Cortex-M core, but different peripherals, different register names and a different vendor driver package. If you want one firmware architecture to run on all of them, you have to decide where the differences live. There is no free option, and this article describes the one that I chose for the Embedbits BSP, what the numbers from the real repositories say about it, and what it costs.

What happens before main(): reset, startup code and linker script

· 12 min read

Every C tutorial starts with int main(void). Nobody explains who calls it. And yet, when you write uint32_t counter = 5; as a global variable and the first line of main() reads 5, a lot of work already happened. When the work is not done, the symptom is a variable with a random value that "worked yesterday".

In this article we follow the microcontroller from the reset to the first line of main(): what the hardware does by itself, what the linker script says and what the startup code has to do. It is the content of two modules of the Embedbits BSP (Linker and Startup), but the principle is the same on every Cortex-M. All code in the article was built with arm-none-eabi-gcc 13.2.1 and run in QEMU, on a model of an STM32F405 board, so the addresses and the outputs are real.