In Part 1, we did the heavy lifting of calibrating our analog front-end — calculating the “magic number” (our calibration factor ) that converts voltage-seconds into liters.
Now we need to wrap that math in a usable device. We’re not logging data to a serial monitor anymore; we’re building a standalone medical instrument with a UI, distinct operation modes, and a specific algorithm to calculate Forced Vital Capacity (FVC). Here’s how I structured the firmware for the Arduino Nano Every to handle the logic flow and the physics of bidirectional airflow.
What You Need
- Libraries:
Adafruit_SSD1306andAdafruit_GFX(standard drivers for the OLED) - Hardware: 128x64 I2C OLED, 3 push buttons (Up, Down, Confirm)
Keeping the Device Deterministic: A Simple State Machine
A medical device needs to be predictable. You don’t want the firmware integrating background noise while the user is navigating a menu.
I structured loop() as a basic state machine. The system blocks in a menu function until the user selects a mode, then routes control to the appropriate measurement handler:
void loop() {
// 1. Blocking call: Wait here until user selects a mode
mode = main_menu();
// 2. Route to the correct logic handler
if (mode == 0 || mode == 1) {
display_mode(mode);
measure(mode);
}
else if (mode == 2) {
measure_FVC();
}
delay(2000); // Debounce/Reset delay
}
I used blocking code here rather than an interrupt-based scheduler. For a single-purpose device like this, it dramatically simplifies the logic without hurting the user experience.
Handling Directional Airflow
The MPX2010 is a differential pressure sensor. Exhaling creates positive pressure (voltage above ambient); inhaling creates negative pressure (voltage below ambient). The measure() function needs to handle both directions:
// Inside measure(int num_mode)...
// CASE 0: EXHALATION
while(V_val >= V_amb + 0.05 && i < 300 && num_mode == 0) {
voltages[i] = V_val - V_amb; // Result is positive
// ... sampling delay ...
}
// CASE 1: INHALATION
while(V_val <= V_amb - 0.05 && i < 300 && num_mode == 1) {
voltages[i] = V_amb - V_val; // Result is positive
// ... sampling delay ...
}
Notice V_amb - V_val in the inhalation block. Since our integration function expects positive area values to calculate volume, we have to invert the negative signal in software. Skip this step and the integrator subtracts volume instead of adding it — you get negative liters.
Separate Calibration Constants for Each Direction
In testing, I found that the aerodynamics of pushing air into the sensor versus pulling it out produce slightly different flow characteristics. To account for this, I defined two calibration constants derived from the Part 1 calibration run:
#define EX_VOLT_TO_L_RATIO 2.94 // Exhale factor
#define IN_VOLT_TO_L_RATIO 2.57 // Inhale factor
float integrate_vol(float vals[], float dt, int factor) {
float cal_fac;
if(factor == 0) cal_fac = EX_VOLT_TO_L_RATIO;
if(factor == 1) cal_fac = IN_VOLT_TO_L_RATIO;
// ... standard trapezoidal integration loop ...
return sum * cal_fac; // Returns liters
}
The Clinical Algorithm: Scripting the FVC Maneuver
Forced Vital Capacity isn’t a single breath measurement. It requires a specific sequence of maneuvers — a deep inhale, normal tidal breathing, then a maximal forced exhale. By abstracting the raw sensing logic into measure(), the FVC code reads almost like a clinical script:
void measure_FVC () {
// Step 1: Deep inhale (Inspiratory Capacity)
write_oled("Take a deep breath...");
FVC_vals[0] = measure(1);
// Steps 2 & 3: Normal breathing to find Tidal Volume
write_oled("Breathe normally...");
FVC_vals[1] = measure(1); // TV inhale
FVC_vals[2] = measure(0); // TV exhale
// Step 4: The forceful exhale
write_oled("Exhale as hard as possible...");
FVC_vals[3] = measure(0);
// The math
float TV = (FVC_vals[1] + FVC_vals[2]) / 2.0;
FVC = FVC_vals[0] + FVC_vals[3] - TV;
write_oled("FVC: " + String(FVC) + "L");
}
Because measure() handles hysteresis (waiting for the user to start breathing) and ADC sampling, measure_FVC doesn’t need to know anything about voltages or timing. It asks for a volume, waits for the user to comply, and gets a float back. Clean separation of concerns.
What this project makes obvious is how much software compensation is required for analog hardware. We had to calibrate out the DC offset at boot (V_amb), implement hysteresis to ignore noise, invert negative signals for inhalation, and apply different scaling factors based on airflow direction. Once those low-level hurdles are cleared, building the high-level medical logic becomes a straightforward exercise in state management.