%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#2C3E50', 'primaryTextColor': '#fff', 'primaryBorderColor': '#16A085', 'lineColor': '#16A085', 'secondaryColor': '#E67E22', 'tertiaryColor': '#fff'}}}%%
flowchart LR
subgraph PROBLEM["Without Calibration"]
R1["Raw: 512"] --> E1["Shows: 50.0%"]
R2["Actual: 45%"] --> X1["Error: 5%"]
end
subgraph SOLUTION["With Calibration"]
R3["Raw: 512"] --> C1["Apply Offset<br/>and Gain"]
C1 --> E2["Shows: 45.2%"]
R4["Actual: 45%"] --> X2["Error: 0.2%"]
end
PROBLEM --> |"Two-Point<br/>Calibration"| SOLUTION
style PROBLEM fill:#E74C3C,stroke:#2C3E50,color:#fff
style SOLUTION fill:#16A085,stroke:#2C3E50,color:#fff
style C1 fill:#E67E22,stroke:#2C3E50,color:#fff
554 Sensor Calibration Lab
Hands-On Wokwi Workshop
sensor calibration, Wokwi, ESP32, two-point calibration, signal conditioning, hands-on lab
554.1 Learning Objectives
By completing this lab, you will be able to:
- Understand calibration fundamentals: Learn why sensors need calibration and how raw readings differ from true values
- Perform two-point calibration: Implement offset and gain correction using low and high reference points
- Apply signal conditioning: Use moving average filtering to reduce noise in sensor readings
- Compare raw vs calibrated data: Visualize the impact of calibration on measurement accuracy
- Store and retrieve calibration coefficients: Persist calibration data for production deployments
554.2 Prerequisites
- Basic understanding of Arduino/C++ programming
- Familiarity with analog inputs and ADC concepts
- Completion of the Sensor Data Processing chapter (recommended)
This lab uses Wokwi, a free online electronics simulator. No physical hardware required! You can experiment with sensor calibration techniques directly in your browser.
554.3 Why Calibration Matters
Real sensors have manufacturing variations that cause:
- Offset errors: Sensor reads non-zero when it should read zero
- Gain errors: Sensor’s sensitivity differs from the ideal specification
- Non-linearity: Response curve deviates from expected linear relationship
Two-point calibration corrects both offset and gain errors by measuring at two known reference points.
554.4 Part 1: Circuit Setup
554.4.1 Wokwi Simulator
Use the embedded simulator below to build and test your sensor calibration circuit. Click “Start Simulation” after entering your code.
- The potentiometer simulates an “uncalibrated” sensor with offset and gain errors
- Adjust the potentiometer during simulation to test different readings
- Watch the Serial Monitor to compare raw vs calibrated values
- LED brightness indicates calibration mode (blinking) vs normal mode (steady)
554.4.2 Component Connections
| ESP32 Pin | Component | Connection | Purpose |
|---|---|---|---|
| GPIO 34 | Potentiometer | Wiper (middle) | Simulated sensor input |
| 3.3V | Potentiometer | One outer pin | Reference voltage |
| GND | Potentiometer | Other outer pin | Ground reference |
| GPIO 2 | LED | Anode (long leg) | Calibration status indicator |
| GND | LED | Cathode (via 220 ohm resistor) | Complete LED circuit |
554.4.3 Wiring Diagram
%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#2C3E50', 'primaryTextColor': '#fff', 'primaryBorderColor': '#16A085', 'lineColor': '#16A085', 'secondaryColor': '#E67E22', 'tertiaryColor': '#fff'}}}%%
flowchart TB
subgraph ESP32["ESP32 DevKit"]
P34["GPIO 34<br/>(ADC Input)"]
P2["GPIO 2<br/>(LED)"]
V33["3.3V"]
GND1["GND"]
end
subgraph POT["Potentiometer<br/>(Simulated Sensor)"]
PT["Top Pin"]
PW["Wiper<br/>(Output)"]
PB["Bottom Pin"]
end
subgraph LED_CKT["Status LED"]
LED["LED"]
RES["220 ohm"]
end
V33 --> PT
PW --> P34
PB --> GND1
P2 --> LED
LED --> RES
RES --> GND1
style ESP32 fill:#2C3E50,stroke:#16A085,color:#fff
style POT fill:#E67E22,stroke:#2C3E50,color:#fff
style LED_CKT fill:#16A085,stroke:#2C3E50,color:#fff
554.5 Part 2: Complete Calibration Code
Copy this code into the Wokwi editor. It demonstrates comprehensive sensor calibration with filtering.
/*
* Sensor Calibration Workshop
* Interactive Lab: Two-Point Calibration with Signal Conditioning
*
* This code demonstrates:
* - Raw ADC reading and conversion
* - Two-point calibration (offset + gain correction)
* - Simple moving average filter for noise reduction
* - Interactive calibration procedure
*
* Components:
* - ESP32 DevKit
* - Potentiometer on GPIO 34 (simulates uncalibrated sensor)
* - LED on GPIO 2 (calibration status indicator)
*/
// ============ PIN DEFINITIONS ============
const int SENSOR_PIN = 34; // Analog input (ADC1_CH6)
const int LED_PIN = 2; // Built-in LED for status
// ============ ADC CONFIGURATION ============
const float ADC_MAX = 4095.0; // 12-bit ADC resolution
const float VREF = 3.3; // Reference voltage
const int FILTER_SIZE = 10; // Moving average window size
// ============ CALIBRATION VARIABLES ============
float calOffset = 0.0; // Offset correction (additive)
float calGain = 1.0; // Gain correction (multiplicative)
// Reference values for two-point calibration
float lowRefActual = 10.0; // Known low reference (e.g., 10%)
float highRefActual = 90.0; // Known high reference (e.g., 90%)
float lowRefRaw = 0.0; // Raw reading at low reference
float highRefRaw = 0.0; // Raw reading at high reference
// ============ FILTER VARIABLES ============
float filterBuffer[FILTER_SIZE];
int filterIndex = 0;
bool filterFilled = false;
// ============ STATE MACHINE ============
enum CalibrationState {
NORMAL_MODE,
WAIT_LOW_POINT,
CAPTURE_LOW_POINT,
WAIT_HIGH_POINT,
CAPTURE_HIGH_POINT,
CALCULATE_COEFFICIENTS
};
CalibrationState currentState = NORMAL_MODE;
unsigned long lastPrintTime = 0;
const unsigned long PRINT_INTERVAL = 500;
// ============ FUNCTION DECLARATIONS ============
float readRawSensor();
float applyFilter(float newValue);
float applyCalibration(float rawValue);
void calculateCalibrationCoefficients();
void printCalibrationStatus();
void handleSerialCommands();
void blinkLED(int times, int delayMs);
void setup() {
Serial.begin(115200);
delay(1000);
// Configure pins
pinMode(LED_PIN, OUTPUT);
analogReadResolution(12); // 12-bit ADC (0-4095)
// Initialize filter buffer
for (int i = 0; i < FILTER_SIZE; i++) {
filterBuffer[i] = 0.0;
}
// Welcome message
Serial.println("\n========================================");
Serial.println(" SENSOR CALIBRATION WORKSHOP");
Serial.println(" Interactive Two-Point Calibration Lab");
Serial.println("========================================\n");
Serial.println("COMMANDS:");
Serial.println(" 'c' - Start calibration procedure");
Serial.println(" 'l' - Capture LOW reference point");
Serial.println(" 'h' - Capture HIGH reference point");
Serial.println(" 'r' - Reset calibration to defaults");
Serial.println(" 's' - Show current calibration status");
Serial.println("\nTurn the potentiometer to simulate sensor readings.\n");
// Initial LED flash to indicate startup
blinkLED(3, 200);
digitalWrite(LED_PIN, HIGH); // LED on = normal mode
}
void loop() {
// Handle serial commands for calibration
handleSerialCommands();
// Read and process sensor
float rawPercent = readRawSensor();
float filteredRaw = applyFilter(rawPercent);
float calibratedValue = applyCalibration(filteredRaw);
// Print values periodically
if (millis() - lastPrintTime >= PRINT_INTERVAL) {
lastPrintTime = millis();
if (currentState == NORMAL_MODE) {
Serial.print("RAW: ");
Serial.print(rawPercent, 1);
Serial.print("% | FILTERED: ");
Serial.print(filteredRaw, 1);
Serial.print("% | CALIBRATED: ");
Serial.print(calibratedValue, 1);
Serial.print("% | ERROR: ");
Serial.print(abs(filteredRaw - calibratedValue), 2);
Serial.println("%");
}
}
// Update LED based on mode
if (currentState != NORMAL_MODE) {
// Blink LED during calibration
digitalWrite(LED_PIN, (millis() / 250) % 2);
} else {
digitalWrite(LED_PIN, HIGH); // Steady on in normal mode
}
delay(50);
}
// ============ SENSOR READING ============
float readRawSensor() {
int rawADC = analogRead(SENSOR_PIN);
float percent = (rawADC / ADC_MAX) * 100.0;
return percent;
}
// ============ MOVING AVERAGE FILTER ============
float applyFilter(float newValue) {
filterBuffer[filterIndex] = newValue;
filterIndex = (filterIndex + 1) % FILTER_SIZE;
if (filterIndex == 0) {
filterFilled = true;
}
int count = filterFilled ? FILTER_SIZE : filterIndex;
if (count == 0) return newValue;
float sum = 0.0;
for (int i = 0; i < count; i++) {
sum += filterBuffer[i];
}
return sum / count;
}
// ============ CALIBRATION APPLICATION ============
float applyCalibration(float rawValue) {
return (rawValue * calGain) + calOffset;
}
// ============ CALIBRATION PROCEDURE ============
void calculateCalibrationCoefficients() {
if (abs(highRefRaw - lowRefRaw) < 0.001) {
Serial.println("ERROR: Reference points too close together!");
return;
}
calGain = (highRefActual - lowRefActual) / (highRefRaw - lowRefRaw);
calOffset = lowRefActual - (lowRefRaw * calGain);
Serial.println("\n========================================");
Serial.println(" CALIBRATION COMPLETE!");
Serial.println("========================================");
Serial.print(" Gain (slope): ");
Serial.println(calGain, 4);
Serial.print(" Offset (intercept): ");
Serial.println(calOffset, 4);
Serial.println("----------------------------------------");
Serial.println(" Calibration Equation:");
Serial.print(" Calibrated = Raw * ");
Serial.print(calGain, 4);
Serial.print(" + ");
Serial.println(calOffset, 4);
Serial.println("========================================\n");
// Verification
float verifyLow = applyCalibration(lowRefRaw);
float verifyHigh = applyCalibration(highRefRaw);
Serial.println("VERIFICATION:");
Serial.print(" Low point: Raw=");
Serial.print(lowRefRaw, 1);
Serial.print("% -> Calibrated=");
Serial.print(verifyLow, 1);
Serial.print("% (Expected: ");
Serial.print(lowRefActual, 1);
Serial.println("%)");
Serial.print(" High point: Raw=");
Serial.print(highRefRaw, 1);
Serial.print("% -> Calibrated=");
Serial.print(verifyHigh, 1);
Serial.print("% (Expected: ");
Serial.print(highRefActual, 1);
Serial.println("%)\n");
currentState = NORMAL_MODE;
blinkLED(5, 100); // Success indication
}
// ============ SERIAL COMMAND HANDLER ============
void handleSerialCommands() {
if (Serial.available() > 0) {
char cmd = Serial.read();
switch (cmd) {
case 'c':
case 'C':
Serial.println("\n========================================");
Serial.println(" CALIBRATION PROCEDURE STARTED");
Serial.println("========================================");
Serial.println("\nStep 1: Set the potentiometer to the LOW reference point");
Serial.print(" (This represents ");
Serial.print(lowRefActual, 0);
Serial.println("% actual value)");
Serial.println(" Press 'l' when ready to capture.\n");
currentState = WAIT_LOW_POINT;
break;
case 'l':
case 'L':
if (currentState == WAIT_LOW_POINT) {
lowRefRaw = applyFilter(readRawSensor());
Serial.print("\nLOW POINT CAPTURED: Raw = ");
Serial.print(lowRefRaw, 1);
Serial.print("% (Actual = ");
Serial.print(lowRefActual, 0);
Serial.println("%)\n");
Serial.println("Step 2: Set the potentiometer to the HIGH reference point");
Serial.print(" (This represents ");
Serial.print(highRefActual, 0);
Serial.println("% actual value)");
Serial.println(" Press 'h' when ready to capture.\n");
currentState = WAIT_HIGH_POINT;
} else {
Serial.println("Press 'c' first to start calibration!");
}
break;
case 'h':
case 'H':
if (currentState == WAIT_HIGH_POINT) {
highRefRaw = applyFilter(readRawSensor());
Serial.print("\nHIGH POINT CAPTURED: Raw = ");
Serial.print(highRefRaw, 1);
Serial.print("% (Actual = ");
Serial.print(highRefActual, 0);
Serial.println("%)\n");
Serial.println("Calculating calibration coefficients...\n");
calculateCalibrationCoefficients();
} else {
Serial.println("Capture low point first with 'l'!");
}
break;
case 'r':
case 'R':
calOffset = 0.0;
calGain = 1.0;
lowRefRaw = 0.0;
highRefRaw = 0.0;
currentState = NORMAL_MODE;
Serial.println("\nCalibration RESET to defaults (gain=1.0, offset=0.0)\n");
break;
case 's':
case 'S':
printCalibrationStatus();
break;
}
}
}
// ============ STATUS DISPLAY ============
void printCalibrationStatus() {
Serial.println("\n========================================");
Serial.println(" CURRENT CALIBRATION STATUS");
Serial.println("========================================");
Serial.print(" Gain: ");
Serial.println(calGain, 4);
Serial.print(" Offset: ");
Serial.println(calOffset, 4);
Serial.println("----------------------------------------");
Serial.print(" Low Ref: Raw=");
Serial.print(lowRefRaw, 1);
Serial.print("% -> Actual=");
Serial.print(lowRefActual, 0);
Serial.println("%");
Serial.print(" High Ref: Raw=");
Serial.print(highRefRaw, 1);
Serial.print("% -> Actual=");
Serial.print(highRefActual, 0);
Serial.println("%");
Serial.println("========================================\n");
}
// ============ LED INDICATOR ============
void blinkLED(int times, int delayMs) {
for (int i = 0; i < times; i++) {
digitalWrite(LED_PIN, HIGH);
delay(delayMs);
digitalWrite(LED_PIN, LOW);
delay(delayMs);
}
}554.6 Part 3: Step-by-Step Calibration Procedure
Follow these steps to perform two-point calibration:
In this lab, the potentiometer position represents the sensor reading. We are pretending that when the potentiometer is at ~10% position, the “true” value is 10%, and at ~90% position, the “true” value is 90%. The calibration corrects any discrepancies.
554.6.1 Step 1: Run the Simulation
- Click Start in Wokwi to run the simulation
- Open the Serial Monitor (bottom panel)
- Observe the output showing RAW, FILTERED, and CALIBRATED values
554.6.2 Step 2: Observe Raw Readings
- Turn the potentiometer to different positions
- Notice the RAW values in the Serial Monitor
- Before calibration, RAW and CALIBRATED values are identical
554.6.3 Step 3: Start Calibration
- Type
cin the Serial Monitor and press Enter - You will see instructions for the calibration procedure
554.6.4 Step 4: Capture Low Reference Point
- Turn the potentiometer to approximately 10% position
- Let the reading stabilize for 2-3 seconds
- Type
l(lowercase L) and press Enter - The system captures this as the “low reference” point
554.6.5 Step 5: Capture High Reference Point
- Turn the potentiometer to approximately 90% position
- Let the reading stabilize for 2-3 seconds
- Type
hand press Enter - The system captures this as the “high reference” point
554.6.6 Step 6: Verify Calibration
- The system calculates and displays calibration coefficients
- Move the potentiometer through its range
- Observe how CALIBRATED values now differ from RAW values
- The ERROR column shows the correction being applied
554.7 Part 4: Understanding the Math
%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#2C3E50', 'primaryTextColor': '#fff', 'primaryBorderColor': '#16A085', 'lineColor': '#16A085', 'secondaryColor': '#E67E22', 'tertiaryColor': '#fff'}}}%%
flowchart TB
subgraph INPUT["Reference Points"]
LP["Low Point<br/>(rawLow, actualLow)<br/>e.g., (15%, 10%)"]
HP["High Point<br/>(rawHigh, actualHigh)<br/>e.g., (85%, 90%)"]
end
subgraph CALC["Calculate Coefficients"]
G["Gain = (actualHigh - actualLow) / (rawHigh - rawLow)<br/>= (90 - 10) / (85 - 15) = 1.143"]
O["Offset = actualLow - (rawLow * gain)<br/>= 10 - (15 * 1.143) = -7.14"]
end
subgraph APPLY["Apply Calibration"]
F["Calibrated = Raw * Gain + Offset<br/>e.g., 50% raw -> 50 * 1.143 + (-7.14) = 50%"]
end
LP --> G
HP --> G
G --> O
O --> F
style INPUT fill:#2C3E50,stroke:#16A085,color:#fff
style CALC fill:#E67E22,stroke:#2C3E50,color:#fff
style APPLY fill:#16A085,stroke:#2C3E50,color:#fff
The Two-Point Calibration Formula:
Given two reference points:
- Low point: (raw_low, actual_low) - e.g., sensor reads 15% when true value is 10%
- High point: (raw_high, actual_high) - e.g., sensor reads 85% when true value is 90%
Calculate:
- Gain (slope):
gain = (actual_high - actual_low) / (raw_high - raw_low) - Offset (intercept):
offset = actual_low - (raw_low * gain)
Apply calibration:
calibrated_value = raw_value * gain + offset
554.8 Part 5: Challenge Exercises
Goal: Extend the calibration to use three reference points for improved accuracy across the range.
Tasks:
- Add a third reference point at 50% (midpoint)
- Capture three points: low (10%), mid (50%), high (90%)
- Use piecewise linear interpolation:
- For values < 50%: use low-to-mid segment
- For values >= 50%: use mid-to-high segment
- Compare accuracy against two-point calibration
Hint: Store two sets of gain/offset coefficients and select based on input value.
Goal: Persist calibration coefficients so they survive power cycles.
Tasks:
- Add EEPROM library and save calibration after calculation
- Load calibration automatically at startup
- Add validity check (magic number) to detect uncalibrated state
- Add a ‘w’ command to write calibration and ‘e’ command to erase
#include <EEPROM.h>
// EEPROM addresses
const int EEPROM_MAGIC_ADDR = 0;
const int EEPROM_GAIN_ADDR = 4;
const int EEPROM_OFFSET_ADDR = 8;
const int EEPROM_MAGIC_VALUE = 0xCAFE;
void saveCalibrationToEEPROM() {
EEPROM.begin(64);
EEPROM.put(EEPROM_MAGIC_ADDR, EEPROM_MAGIC_VALUE);
EEPROM.put(EEPROM_GAIN_ADDR, calGain);
EEPROM.put(EEPROM_OFFSET_ADDR, calOffset);
EEPROM.commit();
Serial.println("Calibration saved to EEPROM!");
}
void loadCalibrationFromEEPROM() {
EEPROM.begin(64);
int magic;
EEPROM.get(EEPROM_MAGIC_ADDR, magic);
if (magic == EEPROM_MAGIC_VALUE) {
EEPROM.get(EEPROM_GAIN_ADDR, calGain);
EEPROM.get(EEPROM_OFFSET_ADDR, calOffset);
Serial.println("Calibration loaded from EEPROM");
} else {
Serial.println("No valid calibration found, using defaults");
calGain = 1.0;
calOffset = 0.0;
}
}Goal: Implement automatic baseline drift correction for long-term deployments.
Background: Sensors drift over time due to aging, temperature changes, and contamination. Automatic Baseline Correction (ABC) can compensate by assuming the sensor occasionally sees a known reference (e.g., CO2 sensors assume 400ppm outdoor air).
Tasks:
- Track the minimum reading over a 24-hour window
- Assume this minimum represents the “baseline” reference value
- Automatically adjust offset to correct drift
- Add drift alarm if correction exceeds threshold
554.9 Key Calibration Concepts Summary
%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#2C3E50', 'primaryTextColor': '#fff', 'primaryBorderColor': '#16A085', 'lineColor': '#16A085', 'secondaryColor': '#E67E22', 'tertiaryColor': '#fff'}}}%%
mindmap
root((Sensor<br/>Calibration))
Error Types
Offset Error
Zero point shift
Additive correction
Gain Error
Sensitivity change
Multiplicative correction
Non-linearity
Polynomial fit
Lookup tables
Calibration Methods
Single-Point
Offset only
Quick but limited
Two-Point
Offset + Gain
Most common
Multi-Point
Non-linear sensors
Piecewise linear
Signal Conditioning
Filtering
Moving average
Median filter
Noise Reduction
Oversampling
RC low-pass
Production Considerations
EEPROM Storage
Field Recalibration
Drift Monitoring
| Concept | Description | When to Use |
|---|---|---|
| Offset Error | Sensor reads non-zero when true value is zero | Always needs correction |
| Gain Error | Sensor’s sensitivity differs from specification | When readings scale incorrectly |
| Two-Point Calibration | Uses two reference points to calculate offset and gain | Linear sensors (most common) |
| Multi-Point Calibration | Uses 3+ reference points with interpolation | Non-linear sensors (thermistors, pH) |
| Moving Average Filter | Averages N recent readings to reduce noise | Noisy environments, slow-changing signals |
| EEPROM Storage | Persists calibration across power cycles | Production deployments |
- Use reference standards that bracket your expected measurement range
- Allow sensor warm-up time before calibration (typically 5-30 minutes)
- Document environmental conditions during calibration (temperature, humidity)
- Recalibrate periodically based on manufacturer recommendations
- Store calibration metadata including date, conditions, and number of points
- Validate calibration by checking known reference values after applying coefficients
554.10 Summary
This lab covered hands-on sensor calibration techniques:
- Two-Point Calibration: Offset and gain correction using reference points
- Moving Average Filter: Noise reduction through sample averaging
- Interactive Procedure: Command-driven calibration workflow
- Calibration Math: Gain and offset calculation from reference measurements
- Production Considerations: EEPROM storage, drift compensation
554.11 What’s Next
Return to the Sensor Interfacing Overview for a summary of all sensor interfacing topics, or explore Multi-Sensor Fusion for advanced sensor combination techniques.
- Sensor Data Processing - Theory behind filtering and calibration
- Sensor Communication Protocols - I2C, SPI interfaces
- Sensor Applications - Real-world implementation examples