Raw sensor data is almost never what you actually need. The MPX2010 pressure sensor I used for my spirometer gives you voltage. But what a spirometer measures is liters — specifically, the volume of air exhaled. That gap between “voltage the sensor produces” and “number the clinician reads” is where calibration lives.
This is the algorithm I wrote to derive the proportionality constant that bridges hardware voltage and physical volume on an Arduino Nano Every.
The Engineering Challenge
The MPX2010 outputs a voltage proportional to air flow rate (Q). To get volume (V), you integrate flow over time:
The catch: we don’t know the exact relationship between sensor voltage (v) and flow rate yet. We need a calibration factor (k) that maps the two.
By pushing a known volume through the device — a standard 3-liter medical syringe — we can solve for k:
Hardware required: Arduino Nano Every (5V logic), MPX2010DP (differential pressure), a 3L calibration syringe, standard Arduino IDE.
Step 1: Handling the DC Offset
Even with a good op-amp circuit, the MPX2010 outputs a non-zero voltage when the air is completely still. If you start integrating this “ghost voltage,” your calculated volume drifts toward infinity within seconds. Establishing a zero baseline at boot is non-negotiable:
float V_amb; // Ambient zero-point voltage
void setup() {
pinMode(A0, INPUT);
Serial.begin(9600);
// Snapshot the sensor at rest — this becomes our session zero
V_amb = bit_to_V(analogRead(A0));
delay(2000);
Serial.println("Pump 3L when prompted");
}
V_amb gets subtracted from every subsequent reading. It dynamically calibrates against ambient pressure and circuit noise every time you power the device on — no manual adjustment needed between sessions.
Step 2: The Sampling Loop and Hysteresis Threshold
Detecting when the user actually starts pushing the syringe is trickier than it sounds. The sensor has a noise floor of about ±0.03V. If you start integrating the moment voltage moves by 0.01V, you’re integrating noise, not airflow.
I implemented a hysteresis threshold of 0.05V. The code 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) {
V_val = bit_to_V(analogRead(A0));
// Normalize: remove the ambient offset
voltages[i] = V_val - V_amb;
// Lock sampling rate to 100Hz
// DELTA_T is defined as 0.01 seconds
delay(DELTA_T * 1000);
i++;
}
The delay(DELTA_T * 1000) locks our sampling rate to 100Hz. In a production RTOS environment I’d use a hardware timer interrupt for this — a blocking delay isn’t ideal. But for a calibration script that runs once under controlled conditions, it’s sufficient. The important thing is that dt stays constant, because time is a fixed variable in the integration math.
Step 3: Trapezoidal Integration
Once the buffer has the waveform of a 3-liter push, we calculate the area under the curve. I chose the trapezoidal rule over a simple Riemann sum because it averages adjacent samples rather than using just the left or right endpoint. For a continuous analog signal this gives better accuracy with no added complexity:
float integrate_vol(float vals[], float dt) {
int i = 0;
float sum = 0;
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;
}
Step 4: Deriving the Factor
The script runs this process five times to average out inconsistency in how hard you push the syringe, then outputs the calibration factor:
// avg is the average integrated voltage sum from 5 trials
Serial.println("Calibration factor: " + String(3.0 / avg));
The result — something like 2.94 — means that for every volt-second of integrated pressure, the device moved 2.94 liters of air. This constant gets hardcoded into the production firmware to convert real-time voltage readings into live volume measurements.
That’s the whole calibration pipeline: zero the ambient noise, detect deliberate actuation, integrate the signal numerically, and anchor the result to a known physical volume. The math is straightforward, but getting the noise handling and hysteresis threshold right is what makes the difference between a device that drifts and one that measures accurately.