The firmware is the embedded program that runs on the microcontroller: the code that, once uploaded, is the behaviour of the system. This page is not about a language or about one particular board, but about how to structure that code so it stays readable, reliable and open to change, from the simple loop of a first build up to the architecture of a real-time system. The concrete techniques (non-blocking loop, state machines, RTOS) each have their own page. Here we situate the whole and choose the architecture that fits the need.

What is it for?

Firmware is not software in the sense a PC has software. It runs with no operating system most of the time, on counted resources (memory, processing power), in an endless loop, and it has to be robust: a lock-up out in the field, with no keyboard and no screen to step in, is a dead system.

The challenge is not to “write code” (that is the language), but to organise it. A first sketch fits in a few lines inside loop(). A real system piles up dozens of functions, several activities carried on at once, error handling. With no structure, the code soon turns into a “plate of spaghetti” nobody dares touch. Structuring firmware well means choosing, according to how complex the system is, the architecture that will keep it manageable.

How to structure firmware

There is a progression of architectures, from the simplest to the most demanding. You adopt the level that is just necessary: you only move up a step when the need forces it.

Staircase of the five firmware architectures, ranked by increasing complexity: super-loop, non-blocking cooperative loop, state machines, breaking into modules, real-time RTOS.

  1. The super-loop. setup() sets things up, loop() repeats: the whole program inside the loop. Enough for a build that does one thing. It is the starting point (the C++ language).
  2. The non-blocking cooperative loop. As soon as the system does more than one thing, each activity becomes a brief task called on every pass, never blocking. It is the default architecture of nearly every real firmware (non-blocking programming).
  3. State machines. For behaviour with modes (waiting → running → done), you model each task as a state machine: it reads well, and it spares you cascades of flags.
  4. Breaking into modules. You separate the code by responsibility — sensor acquisition, control, communication — into named functions and files. Each module can be tested and read on its own.
  5. The real-time system (RTOS). When some tasks have strict deadlines to meet whatever happens, an RTOS such as FreeRTOS preempts to guarantee priorities. Powerful but heavy: keep it for the cases that demand it, especially on the ESP32 side.

This ladder is the heart of the craft: most school projects live very well at levels 2 to 4, with the RTOS remaining a horizon.

Firmware across the families

The way of structuring described above is the same everywhere. What changes from one microcontroller family to another is the tooling and the API:

  • Arduino (AVR / ARM) — the Wiring API, a super-loop, the Arduino IDE: the gentlest way in (Arduino hub).
  • ESP32 — either the Arduino core (same API as the Arduino, so migration is immediate), or ESP-IDF, Espressif’s native framework built on FreeRTOS (ESP32 hub).
  • STM32 — the HAL library generated by CubeMX for fine control, or the Arduino core (STM32duino) to stay on familiar ground (STM32 hub).
  • MicroPython — an interpreted language and a REPL: the tooling changes completely, but the architectures are the same, while True standing in for the super-loop, non-blocking code and state machines having their twins (MicroPython hub).

The cross-cutting lesson: the architecture concepts do not depend on the board. Only the development environment and the names of the functions change. Learning to structure firmware once serves you on every family. The tooling specifics are covered in each family’s hub.

Pitfalls

Piling everything into loop(). The beginner’s reflex: adding line after line inside the loop until it is a plate of spaghetti. Break it into functions and tasks as soon as it goes past one activity.

delay() everywhere. Every delay() freezes the whole system. Incompatible with firmware that does several things. Move to non-blocking code.

No handling of the unexpected. No default value, no watchdog: at the first anomaly out in the field, the system locks up and never restarts. Robustness is designed in, it is not added afterwards.

Over-architecting too early. Setting up an RTOS to blink two LEDs adds complexity for no benefit. The architecture follows the need, it does not come before it.

Not putting the firmware under version control. The code changes at every attempt. Without Git, you lose track of the version that worked. Version control is part of firmware discipline, not a comfort.

See also

  • The C++ language — writing the code, where firmware is about organising it
  • Non-blocking programming — the cooperative loop, the default architecture
  • State machine — structuring behaviour that has modes
  • Function — the unit you break modules into
  • ESP32 — Arduino core against ESP-IDF, and the FreeRTOS horizon
  • Arduino — the super-loop and the Wiring API
  • STM32 — HAL generated by CubeMX, or the Arduino core
  • MicroPython — the same architectures, interpreted tooling
  • Watchdog — robustness in the face of lock-ups
  • Debugging an embedded system — tracking a bug down in the firmware (messages or hardware debugging)