Close

Firmware Architecture: Taming Complexity with a State Machine

A project log for R909-Tester: A Modular RP2040 zero Instrument

R909-Tester is a low-cost workshop instrument platform based on the Raspberry Pi RP2040 zero

nobchanobcha 07/11/2026 at 08:190 Comments

This is a project log for R909-Tester: A Modular RP2040 zero Instrument

In the previous log, I walked through the hardware design. Now that the prototype is taking shape on a breadboard, it's time to dive into the firmware—specifically, how I'm managing the growing complexity of this multi-function instrument.

The Problem with Simple Sketches
When this project started as just a continuity tester, the firmware was trivial. A single loop() function ran the test and that was that. But as I added a capacitance meter, a frequency counter, and a diode identifier, things got messy fast.

The naive approach would be something like this:

cpp
void loop() {
  if(mode == 0) continuityTest();
  else if(mode == 1) capacitorMeter();
  else if(mode == 2) frequencyCounter();
  // ... and so on

This works for a handful of functions, but it quickly becomes a maintenance nightmare. What happens when you want to add long-press vs. short-press button handling? What about sub-states within a measurement mode, like charging, waiting, and calculating for the capacitance test? 

The answer, as many experienced embedded developers will tell you, is a state machine. I adopted this approach for the R909-Tester, and it's made all the difference.

Two-Level State Management
Level 1: Application Modes (Top-Level States)

First, I defined the main operating modes of the instrument:

cpp
enum Mode {
  CONTINUITY,
  CAPACITOR,
  COUNTER,
  DIODE
};

Switching between these modes is triggered by the rotary encoder. The logic is clean and contained:

cpp
switch(currentMode) {
  case CONTINUITY: continuityTest(); break;
  case CAPACITOR:  capacitorMeter();  break;
  case COUNTER:    frequencyCounter(); break;
  case DIODE:      diodeChecker();    break;
}


Adding a new function is as simple as adding a new enum value and a case statement. This modularity is a huge win for code organization and maintainability. 

Level 2: Internal States (Sub-States)
But the story doesn't end there. Some measurement modes have their own internal sequences. The capacitance meter, for example, needs to:

Discharge the capacitor

Wait for the discharge to complete

Start charging through a known resistor

Wait for the voltage to reach 63.2% of VCC

Calculate capacitance from the time constant (τ = RC)

Display the result

Using delay() to manage this sequence would block the entire processor, making the rotary encoder unresponsive and the user interface feel sluggish. That's a non-starter.

Instead, each mode manages its own internal state. Here's the state enum for the capacitance meter:

cpp
enum CapState {
  CAP_IDLE,
  CAP_DISCHARGE,
  CAP_DISCHARGE_WAIT,
  CAP_CHARGE,
  CAP_CHARGE_WAIT,
  CAP_CALC,
  CAP_RESULT
};


The main loop doesn't block. It runs continuously, checking the current sub-state, performing a small step of the measurement, and then updating the state as needed. This keeps the user interface responsive at all times. https://nobcha23.hatenablog.com/entry/2026/06/25/160209

Why This Approach Works
Responsiveness: By avoiding blocking delay() calls, the rotary encoder remains responsive during measurements.

Modularity: Adding a new mode or changing the behavior of an existing one is localized. I don't have to rewrite the entire firmware.

Future-Proofing: This architecture provides a solid foundation for future features like a settings menu, EEPROM storage for calibration data, or even a simple data logging capability. 

Easier Debugging: With well-defined states, tracing the flow of the program is much simpler. You can log state transitions and instantly see where things are going wrong.

https://chitose6thplant.fc2.page/r909-tester/

The RP2040 Advantage: PIO State Machines
It's worth noting that the RP2040's PIO (Programmable I/O) blocks are also state machines, but at the hardware level. While the software state machine I've described here manages the overall application flow, the PIO state machines run independently on the RP2040 to handle real-time I/O tasks, like the high-speed pulse counting for the frequency counter. This is a powerful combination: software state machines for application logic, and hardware PIO state machines for performance-critical tasks. 

In the next log, I'll show how this architecture made it almost trivial to integrate the frequency counter and add a diode identifier.

Discussions