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.
nobcha
Discussions
Become a Hackaday.io Member
Create an account to leave a comment. Already have an account? Log In.