If you’ve ever worked with medical IoT or fluid dynamics, you know that raw sensor data is rarely useful out of the box. A pressure sensor gives us voltage, but for a spirometer, we need liters.
I built a digital spirometer using an Arduino Nano Every and an MPX2010 pressure sensor. The analog frontend (op-amps and filters) is fun, but the real magic happens in the firmware. We have to turn a differential pressure reading into a volumetric flow calculation using numerical integration.
Here is a breakdown of the calibration algorithm I wrote to derive the proportionality constant () that bridges the gap between hardware voltage and physical volume.
The Engineering Challenge
The MPX2010 outputs a voltage proportional to air flow rate (). To get volume (), we have to integrate that flow over time:
The catch? We don’t know the exact relationship between the sensor’s voltage () and the flow rate yet. We need a “Calibration Factor” ().
By pushing a known volume (a standard 3-liter medical syringe) through the device, we can solve for :
Prerequisites & Tooling
- MCU: Arduino Nano Every (chosen for the 5V logic levels).
- Sensor: MPX2010DP (Differential Pressure).
- The “Standard”: A 3L Calibration Syringe.
- IDE: Standard Arduino IDE.
1. Handling the DC Offset (Zeroing)
First things first: analog sensors drift. Even with a good op-amp circuit, the MPX2010 will output a non-zero voltage when the air is perfectly still. If we feed this “ghost voltage” into an integrator, our calculated volume will drift to infinity within seconds.
We need to establish a “Zero” baseline immediately upon boot.
float V_amb; // Ambient "Zero-point" voltage
void setup() {
pinMode(A0, INPUT);
Serial.begin(9600);
// Take a snapshot of the sensor at rest
// This is our new "0" for the session
V_amb = bit_to_V(analogRead(A0));
delay(2000);
Serial.println("Pump 3L when prompted");
}
Why this matters: This V_amb value is subtracted from every subsequent reading. It dynamically calibrates the device against ambient pressure and circuit noise every time you turn it on.
2. The Sampling Loop & Hysteresis
One of the trickier parts of this build was detecting when the user actually starts pushing the syringe. The sensor has a noise floor of about ±0.03V. If we start integrating as soon as the voltage changes by 0.01V, we’ll just be integrating noise.
I implemented a hysteresis threshold of 0.05V. The code effectively ignores the sensor until it detects a deliberate “push.”
// Wait for voltage to cross the threshold (V_amb + 0.05V)
while(V_val >= V_amb + 0.05 && i < 500) {
// 1. Read the ADC
V_val = bit_to_V(analogRead(A0));
// 2. Normalize the data (Remove the offset)
voltages[i] = V_val - V_amb;
// 3. Locking the Sampling Rate (100Hz)
// DELTA_T is defined as 0.01 seconds
delay(DELTA_T * 1000);
i++;
}
Implementation Detail:
Notice the delay(DELTA_T * 1000). In a production RTOS environment, I’d probably use a hardware timer interrupt to trigger the ADC. However, for a calibration script, a blocking delay is sufficient to roughly lock our sampling rate to 100Hz. This consistency is critical because dt (time) is a constant in our integration math.
3. The Math: Trapezoidal Integration
Once we have filled our buffer (voltages[]) with the waveform of a 3-liter push, we need to calculate the “Area Under the Curve.”
I opted for the Trapezoidal Rule rather than a simple Riemann sum (Rectangular rule). It provides slightly better accuracy for continuous analog signals by averaging the current sample with the next one.
float integrate_vol(float vals[], float dt) {
int i = 0;
float sum = 0;
// Iterate through the buffer
while(vals[i] > 0 && i < 500) {
// Area = (Height_A + Height_B) / 2 * Width
sum += (vals[i] + vals[i+1]) * 0.5 * dt;
i++;
}
return sum;
}
4. Deriving the Factor
The script runs this process 5 times to average out any human inconsistency in pushing the syringe. Finally, it spits out the magic number:
// avg is the average integrated voltage sum from 5 trials
Serial.println("Calibration factor: " + String(3.0 / avg));
The Result:
This outputs a value (e.g., 2.94). This tells us that for every “Volt-Second” of integrated pressure, we have moved 2.94 Liters of air. We can now hardcode this constant into our final firmware to get real-time volume measurements.