In Part 1, we did the heavy lifting of calibrating our analog front-end. We calculated the “magic number” (our calibration factor ) that converts voltage-seconds into liters.
Now, we need to wrap that math in a usable application. We aren’t just logging data to a serial monitor anymore; we are building a standalone medical device with a User Interface (UI), distinct operation modes, and a specific algorithm to calculate Forced Vital Capacity (FVC).
Here is how I structured the firmware for the Arduino Nano Every to handle the logic flow and the physics of bidirectional airflow.
Prerequisites
- Libraries:
Adafruit_SSD1306andAdafruit_GFX(standard drivers for the OLED). - Hardware: 128x64 I2C OLED, 3 Push buttons (Up, Down, Confirm).
1. The Architecture: A Simple State Machine
A medical device needs to be deterministic. You don’t want the device integrating background noise while the user is trying to navigate a menu.
I structured the loop() as a basic state machine. The system sits in a blocking menu function until a mode is selected, then hands off control to the specific measurement logic.
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) {
// Single Measurement Modes (Exhale or Inhale)
display_mode(mode);
measure(mode);
}
else if (mode == 2) {
// Complex Sequence Mode
measure_FVC();
}
delay(2000); // Debounce/Reset delay
}
Design Choice: I used blocking code here rather than an interrupt-based scheduler. For a single-purpose device like this, it simplifies the logic significantly without sacrificing UX.
2. Handling Directional Airflow (The Physics Problem)
Here is where things get interesting. The MPX2010 is a differential pressure sensor.
- Exhaling creates positive pressure (Voltage > Ambient).
- Inhaling creates negative pressure (Voltage < Ambient).
Our measure() function needs to handle both.
// Inside measure(int num_mode)...
// CASE 0: EXHALATION
// We wait for voltage to rise ABOVE the noise floor (+0.05V)
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
// We wait for voltage to drop BELOW the noise floor (-0.05V)
while(V_val <= V_amb - 0.05 && i < 300 && num_mode == 1) {
voltages[i] = V_amb - V_val; // Result is Positive
// ... sampling delay ...
}
The “Gotcha”:
Notice the math in the Inhalation block: V_amb - V_val.
Since our integration function expects positive area values to calculate volume, we have to invert the negative signal in software. If we didn’t, the integrator would subtract volume, giving us negative liters!
3. Applying the Calibration Factors
In testing, we found that the aerodynamics of pushing air into the sensor versus pulling it out resulted in slightly different flow characteristics. To account for this, I defined two separate calibration constants derived from Part 1.
#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;
// Select the correct physics constant based on mode
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
}
4. The Clinical Algorithm: FVC
Forced Vital Capacity (FVC) is a key metric for lung health, but you can’t measure it with a single breath. It requires a sequence of maneuvers.
By abstracting the raw sensing logic into the measure() function, the FVC code reads almost like a script:
- Inspiratory Capacity (IC): Deep breath in.
- Tidal Volume (TV): Normal breathing (In/Out).
- The Blast: Maximal exhalation.
void measure_FVC () {
// Step 1: Deep Inhale (IC)
write_oled("Take a deep breath...");
FVC_vals[0] = measure(1); // Mode 1 is Inhale
// Step 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); // Mode 0 is Exhale
// The Math: Calculating the derived metric
float TV = (FVC_vals[1] + FVC_vals[2]) / 2.0; // Average the Tidal Volumes
FVC = FVC_vals[0] + FVC_vals[3] - TV; // The FVC Formula
// Display Results
write_oled("FVC: " + String(FVC) + "L");
}
Why this works:
Because measure() handles the hysteresis (waiting for the user to start breathing) and the ADC sampling, the measure_FVC function doesn’t need to know anything about voltages or timing. It just asks for a volume, waits for the user to comply, and gets a float back. This creates a very clean separation of concerns.
5. Final Thoughts
This project highlights 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.
- Apply different scaling factors based on airflow direction.
Once those low-level hurdles were cleared, building the high-level medical logic became a straightforward exercise in state management.