19  Lab: Sensor Calibration

Hands-On Wokwi Workshop

sensing
lab
calibration
wokwi
Author

IoT Textbook

Published

July 22, 2026

Keywords

sensor calibration, Wokwi, ESP32, two-point calibration, signal conditioning, hands-on lab

19.1 Start With the Measurement Story

A calibration lab should feel like detective work: compare the raw reading to a known condition, identify the error, apply a correction, then test a new point to prove the sensor has become more trustworthy.

19.2 Learning Objectives

By completing this lab, you will be able to:

  1. Explain calibration fundamentals: Describe why sensors need calibration and how raw readings differ from true values
  2. Implement two-point calibration: Calculate offset and gain correction using low and high reference points
  3. Apply signal conditioning: Configure moving average filtering to reduce noise in sensor readings
  4. Evaluate raw vs calibrated data: Measure the impact of calibration on measurement accuracy
  5. Integrate calibration persistence: Deploy EEPROM-based coefficient storage for production systems
In 60 Seconds

This hands-on Wokwi lab walks through two-point calibration on an ESP32 — calculating gain and offset from two known reference points using calibrated = raw * gain + offset. You configure a moving average filter, persist calibration coefficients in EEPROM, and verify that calibrated readings track true values across the sensor’s full range. No physical hardware needed; everything runs in your browser.

Phoebe the physics guide

Phoebe’s Why

A quick honesty check first: this lab’s panel-scope brief mentions antenna gain and radiation, but nothing in this ESP32 calibration lab is an RF antenna – its “gain” is the calibration slope \(m\) in calibrated = raw * gain + offset, a different physical quantity that happens to share the word. The physics that genuinely lives in this lab is quantization. A dithering ADC count behaves like independent random noise from sample to sample, so it averages down with \(1/\sqrt{N}\) exactly the way any other noise does. That means the moving-average filter this lab already runs for “noise reduction” is quietly buying back some of the resolution the chapter’s own beehive worked example says the 12-bit ADC is wasting – for free, without touching a single component. It could never rescue an aliased signal, but a beehive’s weight does not change fast enough for that to be the risk here.

The Derivation

Quantization step and its RMS noise-floor equivalent:

\[q = \frac{\text{full scale}}{2^{bits}}, \qquad e_{rms}(1) = \frac{q}{\sqrt{12}}\]

Averaging \(N\) independent samples – this lab’s own moving-average filter – shrinks that RMS error by the square root of the window size:

\[e_{rms}(N) = \frac{e_{rms}(1)}{\sqrt{N}}\]

Worked Numbers: This Lab’s Own Beehive Load Cell

  • Theoretical LSB (this chapter’s own figure): \(50\,000\text{ g}/4096 = 12.2\) g/count; RMS quantization alone: \(12.2/\sqrt{12}=3.52\) g
  • Actual LSB (this chapter’s own figure, using the 3275-count span the load cell actually spans): \(15.27\) g/count; RMS quantization alone: \(15.27/\sqrt{12}=4.41\) g
  • After this lab’s own \(N=10\) moving-average window: theoretical case \(3.52/\sqrt{10}=1.11\) g; actual case \(4.41/\sqrt{10}=1.39\) g – roughly a \(3.16\times\) tightening (\(\sqrt{10}\)) in both cases, turning a 15 g raw LSB into an effective resolution near 1.4 g without any hardware change
  • The limit: this only works because a beehive’s mass changes on the order of hours, not milliseconds; a fast-changing signal would need an anti-alias filter before the ADC, which no amount of post-conversion averaging can substitute for
Key Concepts
  • Two-Point Calibration: A procedure using two known reference values to calculate gain and offset correction coefficients, correcting sensitivity errors and zero-point errors simultaneously
  • Gain Coefficient: The slope m in calibrated = raw * gain + offset; corrects proportional errors where the sensor reads too high or too low by a percentage of the measured value
  • Offset Coefficient: The intercept b in the calibration equation; corrects constant errors where the sensor always reads a fixed amount above or below the true value
  • Moving Average Filter: A digital filter replacing each reading with the average of the last N samples, reducing noise at the cost of slower response to rapid changes
  • EEPROM Persistence: Storing calibration coefficients in non-volatile memory so they survive power cycles — essential for production IoT deployments
  • Reference Standards: Known, accurate values used as the basis for calibration; their accuracy sets the ceiling for calibrated sensor accuracy
  • Calibration Drift: Gradual change in sensor response over time due to aging, contamination, or thermal stress, requiring periodic recalibration
  • Wokwi Simulator: Browser-based ESP32 simulation environment allowing firmware development and testing without physical hardware

19.3 Most Valuable Understanding (MVU)

Two-point calibration corrects sensor errors by calculating a simple linear equation: calibrated = raw * gain + offset, where gain fixes sensitivity errors and offset fixes zero-point errors.

This is the single most important concept in this lab. Every real sensor has manufacturing variations that cause its readings to deviate from the true value. Two-point calibration uses two known reference points to calculate correction coefficients that map inaccurate raw readings to accurate calibrated values. The formula y = mx + b (from basic algebra) is the foundation - gain is the slope (m) and offset is the y-intercept (b).

Remember: Calibration accuracy depends entirely on your reference standards. Use the most accurate references you can obtain, and bracket your expected measurement range (calibrate at 10% and 90%, not both at 50%).

Related Chapters

This is part of a series on Sensor Interfacing:

  1. Sensor Data Processing - Theory behind filtering and calibration
  2. Sensor Calibration Lab (this chapter) - Hands-on calibration workshop
  3. Sensor Communication Protocols - I2C, SPI interfaces
  4. Sensor Applications - Real-world implementation examples

Related Topics:

19.4 Introduction

In this hands-on lab, you will build a complete sensor calibration system using an ESP32 microcontroller in the Wokwi browser-based simulator. You will wire a potentiometer to simulate a sensor with offset and gain errors, implement two-point calibration to correct those errors, and apply a moving average filter for noise reduction. The lab takes approximately 45-60 minutes to complete and requires no physical hardware – everything runs in your browser.

By the end of this lab, you will have working firmware that interactively captures reference points, calculates calibration coefficients, and applies real-time correction to sensor readings.

Chapter Roadmap

This lab has several moving parts, so read it as one calibration workflow:

  1. First you frame the problem: raw sensors have offset, gain, noise, drift, and non-linearity.
  2. Then you build the ESP32 simulator circuit and run the low/high reference capture.
  3. Next you inspect the firmware state machine that turns serial commands into gain and offset coefficients.
  4. After that you work through the two-point math, ADC resolution, filtering, and validation questions.
  5. Finally you extend the lab toward multi-point calibration, EEPROM storage, drift compensation, and production evidence.

Checkpoints recap what you have proved so far; “Part” headings are required lab steps, while optional challenges can wait until the core simulator works.

19.5 Prerequisites

  • Basic understanding of Arduino/C++ programming
  • Familiarity with analog inputs and ADC concepts
  • Completion of the Sensor Data Processing chapter (recommended)

19.5.1 Learning Path

1. Before This Lab Review ADC readings, Arduino/ESP32 serial output, and basic sensor data processing.

2. In This Lab Build the circuit, capture low/high references, calculate gain and offset, then smooth noisy readings.

3. After This Lab Apply multi-point calibration, sensor fusion, EEPROM persistence, and production validation.

Interactive Browser-Based Lab

This lab uses Wokwi, a free online electronics simulator. No physical hardware required! You can experiment with sensor calibration techniques directly in your browser.

Calibration Matters

Think of calibration like adjusting a musical instrument. Even a brand-new guitar needs to be tuned before it plays the right notes. Sensors are similar - they need to be “tuned” to give accurate readings.

Why do sensors need calibration?

  1. Manufacturing variations: No two sensors are exactly identical, just like no two guitars are perfectly tuned from the factory
  2. Environmental factors: Temperature, humidity, and age can cause sensors to drift over time
  3. Component tolerances: The electronic parts inside sensors have slight variations

Real-world example: Imagine you buy a cheap thermometer that always reads 2 degrees too hot. You could either: - Buy an expensive, perfectly calibrated thermometer ($$$) - Or calibrate your cheap thermometer by noting “always subtract 2 degrees” (FREE!)

The mathematical approach: Instead of just “subtract 2”, calibration gives us a formula: corrected = raw × gain + offset

  • Offset fixes constant errors (like always being 2 degrees off)
  • Gain fixes scaling errors (like reading 50% when it should be 55%)

This lab teaches you how to find those correction values using two known reference points - just like tuning a guitar by comparing it to a tuner at two different notes!


Teaching Sensors Truth

Hey there, young scientist! Let’s learn about sensor calibration with the Sensor Squad!

Sammy the Sensor has a problem - when the room is actually warm (like 25 degrees), Sammy says “It’s 28 degrees!” And when it’s cold (like 10 degrees), Sammy says “It’s 13 degrees!” Sammy is not lying - Sammy was just built a little differently than other sensors! Sammy always reads too high - that is an offset error (always adding a constant). But notice something: at 10 degrees Sammy is off by 3, and at 25 degrees Sammy is off by 3 too. That constant +3 shift is a pure offset error.

Think of it like a bathroom scale: Imagine your bathroom scale always shows 3 pounds more than your real weight. That is an offset error - it is always off by the same amount! You could fix it by subtracting 3 from every reading.

But what if the scale also stretches the numbers - showing 11 pounds when you are really 10? That is a gain error - the scale reads proportionally too high!

Sammy’s Calibration Adventure:

  1. Step 1 - Find a “known cold” reference: Sammy measures ice water (which we KNOW is 0 degrees). Sammy says “It’s 3 degrees!” Oops - that is Sammy’s offset error! (0 + 3 = 3)

  2. Step 2 - Find a “known hot” reference: Sammy measures boiling water (which we KNOW is 100 degrees). Sammy says “It’s 103 degrees!” Still 3 degrees too high - the offset is consistent! (100 + 3 = 103)

  3. Step 3 - Do the math magic: Using both reference points, we calculate the correction formula: subtract 3 from every reading. Now whenever Sammy gives a reading, we fix it automatically!

Lila the Light explains: “It’s like being a translator! Sammy speaks ‘Sammy language’ and we translate it to ‘real temperature language’ using our special formula!”

Max the Motor adds: “I need calibration too! When someone tells me ‘go 50% speed’, I might actually go 55% without calibration. That could make robots bump into walls!”

Fun Experiment: Ask a grown-up if you can calibrate a kitchen thermometer! Put it in ice water (should read 0C or 32F) and see if it’s accurate. Many thermometers are off by a few degrees!

Remember: Calibration is like teaching your sensor to tell the truth by giving it a “cheat sheet” of corrections!


19.6 Calibration Matters

Flowchart showing calibration process: raw sensor readings with errors on left, calibration process using reference points in middle, accurate calibrated readings on right, illustrating how calibration transforms inaccurate data into precise measurements.
Figure 19.1: Calibration transforms inaccurate raw sensor readings into precise, reliable measurements

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.

19.7 Folded Error And Quality Metrics Notes

Offset and gain errors behave differently in the calibration record. Offset shifts the whole response by a constant amount, so a zero reference can be wrong even when the slope is usable. Gain changes the slope, so error grows as the true value moves away from the reference point. A two-point calibration should therefore record both reference points, the calculated slope and offset, and at least one verification point between or beyond the references.

After calibration, do not report only “it works.” Compare measured values with actual reference values and record metrics that match the claim:

  • Maximum absolute error shows the worst observed miss in the checked range.
  • RMSE summarizes typical error across several verification points.
  • R-squared helps detect whether the calibrated response is close to linear, but it does not replace endpoint error checks.

These metrics are evidence for the tested range only. If the reference points are too close together, small reference mistakes can amplify at the extremes, so span and verification evidence matter as much as the equation.

Two-Point Calibration Limits

Two-point calibration fits a straight line through two reference points. This corrects linear errors (offset and gain) but cannot correct non-linearity – cases where the sensor’s response curve bends or deviates from a straight line.

ESP32 ADC non-linearity: The ESP32’s built-in ADC has well-documented non-linearity, with errors of up to 6% at the extremes of its input range (below ~100 mV and above ~3.1 V). Even after a perfect two-point calibration, readings in these regions will still be inaccurate.

Solutions for non-linear sensors:

  • Use Espressif’s esp_adc_cal characterization library, which applies factory-measured correction curves
  • Perform multi-point calibration with 3 or more reference points and piecewise linear interpolation (see Challenge 1 in Part 5 below)
  • For highly non-linear sensors (thermistors, pH probes), use lookup tables or polynomial fits
Physics PhoebeCheckpoint: Why Calibration Exists

You now know:

  • Offset error is a constant shift, gain error changes the slope, and non-linearity bends the response away from a straight line.
  • Two-point calibration uses low and high reference points to correct linear offset and gain errors together.
  • The ESP32 ADC warning matters because errors can reach 6% near the range extremes, so validation points are evidence, not decoration.

19.7.1 Calibration Fundamentals

19.8 Part 1: Circuit Setup

19.8.1 Wokwi Simulator

Use Wokwi when you are ready to test the circuit. If the embedded panel stays on “Loading,” open the workspace in a new tab and keep this page beside it.

Launch the Simulator

Open a new ESP32 Wokwi workspace and use the wiring diagram and optional sketch below.

Optional Embedded Wokwi Panel

Simulator Tips
  • 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)

19.8.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

19.8.3 Wiring Diagram

Circuit schematic showing ESP32 DevKit connections: GPIO 34 connected to potentiometer wiper for analog input, GPIO 2 connected to LED anode through 220 ohm resistor for status indication, with 3.3V and GND connections to potentiometer ends.
Figure 19.2: Circuit schematic showing ESP32 connections to potentiometer (sensor) and calibration status LED

The circuit is deliberately small: one analog input, one status output, and a potentiometer you can move to create repeatable low, middle, and high readings.

19.9 Part 2: Calibration Simulator Activity

The purpose of this lab is not to memorize a long sketch. Your goal is to see how two known reference points turn an unreliable raw reading into a corrected measurement.

Run it: Watch two reference points become a correction in the calibration-process animation below before you drive the simulator by hand. Pick the Two-point method on a scenario such as Greenhouse or Cold chain, then Play and Step through capturing the low and high references, and open the Fit and Residuals views to see the slope and offset the two points produce and how well mid-range readings are corrected. This mirrors the low-capture, high-capture, and verify steps in the What to Try First list.

What to Try First
  1. Run the simulator and open the Serial Monitor.
  2. Move the potentiometer to a low reference position and capture that point.
  3. Move it to a high reference position and capture that point.
  4. Watch the reported gain and offset.
  5. Move the potentiometer to a middle value and compare raw, filtered, and calibrated readings.

If the calibrated value does not make sense, repeat the capture and check whether the two reference points were far enough apart.

Firmware Behavior in Plain Language

The sketch behaves like a small calibration wizard:

Stage Student Action What the Firmware Learns
Normal reading Turn the potentiometer Shows raw, filtered, and calibrated values
Start calibration Send c Switches into reference-capture mode
Low reference Set a known low value and send l Stores the raw reading for the low point
High reference Set a known high value and send h Stores the raw reading for the high point
Calculation No action Computes slope and offset
Verification Test middle values Confirms that correction works away from the endpoints

The essential algorithm is short:

Calibration Flow
  1. Capture raw_low at a known low reference value.
  2. Capture raw_high at a known high reference value.
  3. Calculate the slope from the two reference points.
  4. Calculate the offset needed to align the low point.
  5. Apply the correction: calibrated value = slope x raw value + offset.

Physics PhoebeCheckpoint: Running the Lab

You now know:

  • GPIO 34 carries the simulated sensor reading and GPIO 2 reports calibration status with an LED.
  • The lab uses a low reference near 10% and a high reference near 90%, then checks middle values instead of trusting the endpoints alone.
  • The firmware state machine moves from normal reading to low capture, high capture, coefficient calculation, and verification.

Once that flow is clear, the full sketch below becomes a reference implementation rather than a wall of code to memorize.

19.10 Optional: Full Wokwi Sketch

Use this only when you want to implement the simulator after you understand the calibration behavior.

/*
 * 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 (0-4095)
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,
    WAIT_HIGH_POINT
};

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)
    analogSetAttenuation(ADC_11db);  // Set 11dB attenuation for full 0-3.3V input range

    // 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("%  |  CORRECTION: ");
            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);
    }
}

19.11 Calibration Procedure

The calibration code implements a state machine that guides you through the process:

Calibration State Machine Workflow

The firmware stays in normal reading mode until the learner sends serial commands to capture low and high reference points.

1. Normal Mode Read the sensor, smooth the signal, and print raw, filtered, and calibrated values.
2. Start Calibration User sends c; the firmware explains the low/high reference capture sequence.
3. Capture Low Point User sets the simulated sensor near 10% and sends l.
4. Capture High Point User sets the simulated sensor near 90% and sends h.
5. Calculate Coefficients The code computes gain and offset from the two reference measurements.
6. Verify and Store Check known values, then save coefficients so calibration survives reset.

Recovery path: send r to reset calibration, or repeat the low/high capture if a reference point was wrong.

Calibration state machine workflow for the Wokwi lab

Follow these steps to perform two-point calibration:

Understanding the Simulation

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.

19.11.1 Step 1: Run the Simulation

  1. Click Start in Wokwi to run the simulation
  2. Open the Serial Monitor (bottom panel)
  3. Observe the output showing RAW, FILTERED, and CALIBRATED values

19.11.2 Step 2: Observe Raw Readings

  1. Turn the potentiometer to different positions
  2. Notice the RAW values in the Serial Monitor
  3. Before calibration, RAW and CALIBRATED values are identical

19.11.3 Step 3: Start Calibration

  1. Type c in the Serial Monitor and press Enter
  2. You will see instructions for the calibration procedure

19.11.4 Step 4: Capture Low Reference Point

  1. Turn the potentiometer to approximately 10% position
  2. Let the reading stabilize for 2-3 seconds
  3. Type l (lowercase L) and press Enter
  4. The system captures this as the “low reference” point

19.11.5 Step 5: Capture High Reference Point

  1. Turn the potentiometer to approximately 90% position
  2. Let the reading stabilize for 2-3 seconds
  3. Type h and press Enter
  4. The system captures this as the “high reference” point

19.11.6 Step 6: Verify Calibration

  1. The system calculates and displays calibration coefficients
  2. Move the potentiometer through its range
  3. Observe how CALIBRATED values now differ from RAW values
  4. The CORRECTION column shows how much calibration adjusts each reading

19.12 Part 4: Understanding the Math

Flowchart showing two-point calibration mathematics: input reference points at top, gain and offset calculation formulas in middle, final calibration equation output, and verification showing corrected values matching expected actual values.
Figure 19.3: Two-point calibration calculates gain (slope) and offset (intercept) from two known reference points

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:

  1. Gain (slope)
    gain = (actual_high - actual_low) / (raw_high - raw_low)
  2. Offset (intercept)
    offset = actual_low - (raw_low * gain)

Apply calibration:

calibrated_value = raw_value * gain + offset

19.12.1 Two-Point Calibration Calculator





Putting Numbers to It

Two-Point Calibration Calculation: A load cell for beehive monitoring reads 410 raw ADC counts at 0 kg and 3685 counts at 50 kg (using a 12-bit ADC with 0-4095 range). Calculate gain and offset to convert raw ADC to kilograms.

Given reference points:

  • Low: \((410\text{ counts}, 0\text{ kg})\)
  • High: \((3685\text{ counts}, 50\text{ kg})\)

Step 1: Calculate gain (slope of the line): \[ \text{Gain} = \frac{y_2 - y_1}{x_2 - x_1} = \frac{50\text{ kg} - 0\text{ kg}}{3685 - 410} = \frac{50}{3275} = 0.01527\text{ kg/count} \]

Step 2: Calculate offset (y-intercept): \[ \text{Offset} = y_1 - (x_1 \times \text{Gain}) = 0 - (410 \times 0.01527) = -6.26\text{ kg} \]

Step 3: Calibration equation: \[ \text{Weight (kg)} = (\text{Raw ADC counts} \times 0.01527) - 6.26 \]

Verification:

  • At 410 counts: \((410 \times 0.01527) - 6.26 = 6.26 - 6.26 = 0\text{ kg}\)
  • At 3685 counts: \((3685 \times 0.01527) - 6.26 = 56.27 - 6.26 = 50.01\text{ kg} \approx 50\text{ kg}\)

Resolution calculation: \[ \text{Resolution} = \text{Gain} = 0.01527\text{ kg/count} \approx 15\text{ grams/count} \]

With a 12-bit ADC (4096 levels) spanning the full 50 kg range, theoretical resolution would be 50/4096 = 12.2 g. Our actual resolution of 15 g is slightly worse because the load cell does not use the full ADC range (3275 out of 4096 counts, or about 80%). Adjusting amplification to use more of the ADC range would improve resolution.

Physics PhoebeCheckpoint: Calibration Math

You now know:

  • Two reference points define the line: gain equals (actual_high - actual_low) / (raw_high - raw_low).
  • The load-cell example maps 410 counts to 0 kg and 3685 counts to 50 kg, giving a gain of 0.01527 kg/count and an offset of -6.26 kg.
  • A 12-bit ADC has 4096 levels, but usable resolution depends on how much of that range the sensor actually spans.

19.12.2 Interactive ADC Resolution Calculator

Explore how ADC bit depth and measurement range affect the smallest detectable change (resolution per count).



19.12.3 Knowledge Check: Calibration Math

19.12.4 Moving Average Filter Explorer

A moving average filter smooths noisy data by averaging a window of recent samples. The window size (N) controls the tradeoff between smoothing and responsiveness. Larger windows produce smoother output but react more slowly to real changes.



The math and filter sections answer “what coefficients should I calculate?” The challenges below ask the production question: how do you keep those coefficients trustworthy after the first successful run?

19.13 Part 5: Challenge Exercises

19.14 Challenge 1: Three-Point Calibration

Run it: Before you code the three-point version, compare the methods in the reference animation below. Select 2-point and then Multi-point on the same sensor type and watch how a straight two-point line leaves error in a bending response while multi-point interpolation follows the curve. Use it to predict how much accuracy the added midpoint should buy you before you measure it in the simulator.

Goal: Extend the calibration to use three reference points for improved accuracy across the range.

Tasks:

  1. Add a third reference point at 50% (midpoint)
  2. Capture three points: low (10%), mid (50%), high (90%)
  3. Use piecewise linear interpolation:
    • For values < 50%: use low-to-mid segment
    • For values >= 50%: use mid-to-high segment
  4. Compare accuracy against two-point calibration

Hint: Store two sets of gain/offset coefficients and select based on input value.

19.15 Challenge 2: EEPROM Calibration Storage

Goal: Persist calibration coefficients so they survive power cycles.

Tasks:

  1. Add EEPROM library and save calibration after calculation
  2. Load calibration automatically at startup
  3. Add validity check (magic number) to detect uncalibrated state
  4. 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;
    }
}

19.16 Automatic Drift Compensation

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:

  1. Track the minimum reading over a 24-hour window
  2. Assume this minimum represents the “baseline” reference value
  3. Automatically adjust offset to correct drift
  4. Add drift alarm if correction exceeds threshold

19.17 Key Calibration Concepts Summary

Mind map showing sensor calibration concepts organized into four branches: error types including offset, gain, and non-linearity; calibration methods including two-point, multi-point, and single-point; signal conditioning including moving average, low-pass, and median filters; and production considerations including EEPROM storage, drift compensation, and recalibration schedules.
Figure 19.4: Mind map of key sensor calibration concepts covered in this lab
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

Best Practices for Sensor Calibration
  1. Use reference standards that bracket your expected measurement range
  2. Allow sensor warm-up time before calibration (typically 5-30 minutes)
  3. Document environmental conditions during calibration (temperature, humidity)
  4. Recalibrate periodically based on manufacturer recommendations
  5. Store calibration metadata including date, conditions, and number of points
  6. Validate calibration by checking known reference values after applying coefficients
Physics PhoebeCheckpoint: Production Calibration

You now know:

  • Multi-point calibration adds 3 or more reference points when a sensor curve is not linear.
  • EEPROM storage needs a validity marker such as 0xCAFE so startup code can reject uninitialized memory.
  • Automatic Baseline Correction tracks a 24-hour minimum only when the sensor is expected to see a known baseline condition.
Calibration Formulas

Two-Point Calibration Formula:

Step Formula Description
1 gain = (actual_high - actual_low) / (raw_high - raw_low) Calculate slope
2 offset = actual_low - (raw_low × gain) Calculate y-intercept
3 calibrated = raw × gain + offset Apply correction

See the Interactive Two-Point Calibration Calculator above for a hands-on tool. The worked load-cell example in Part 4 shows the same calculation with real numbers.

19.17.1 Production Considerations

19.18 Span Validation

The core lab above shows how to build and store a two-point calibration. The companion page focuses on the failure modes that determine whether those coefficients are trustworthy in production.

Next Calibration Practice

Continue with Calibration Span Error and Validation to test reference-point spacing, residual checks, range guards, and release evidence for calibrated sensors.

19.19 Summary

This lab provided hands-on experience with essential sensor calibration techniques used in production IoT systems.

19.19.1 Key Takeaways

Concept What You Learned When to Apply
Two-Point Calibration Calculate gain and offset from two known reference points All linear sensors (temperature, pressure, light)
Moving Average Filter Smooth noisy readings by averaging N recent samples Noisy environments, before capturing calibration references
State Machine Design Guide users through multi-step processes Any interactive calibration or configuration procedure
Calibration Formula calibrated = raw * gain + offset Apply correction to every raw sensor reading
EEPROM Persistence Store calibration across power cycles with magic number validation Production deployments, field-calibrated devices
Automatic Drift Compensation Track minimum over time window to correct sensor drift Long-term deployments, sensors prone to aging

19.19.2 Skills You Practiced

  1. Circuit building: Connecting potentiometer and LED to ESP32
  2. Serial communication: Interactive command interface for calibration
  3. Mathematical modeling: Applying linear algebra to sensor correction
  4. Firmware architecture: State machine design for multi-step workflows
  5. Data persistence: Using EEPROM for non-volatile storage

19.19.3 Common Pitfalls to Avoid

Diagram showing four common calibration pitfalls: reference points too close together, no sensor warm-up time, not filtering before capture, and assuming calibration lasts forever, with fixes for each
Figure 19.5: Common calibration pitfalls to avoid
Pitfall Why It’s Bad Solution
Reference points too close together Small errors in reference measurement cause large errors in calculated gain Use 10% and 90%, not 45% and 55%
Forgetting sensor warm-up time Sensors drift significantly in first few minutes after power-on Allow 5-30 minutes before calibration
Not filtering before capture Single noisy sample can corrupt entire calibration Apply moving average filter before capturing reference
Assuming calibration lasts forever Sensors drift over time due to aging and environment Recalibrate periodically based on manufacturer guidance

19.20 Knowledge Check

19.20.1 Quiz: Sensor Calibration

Match each calibration concept with its correct definition:

Arrange the two-point calibration procedure steps in the correct order:

19.21 See Also

For the full list of related chapters, see the “Related Chapters” section at the top of this page. Additional resources:

19.22 What’s Next

If you want to… Read this
Understand the theory behind filtering and calibration Sensor Data Processing
Learn I2C and SPI sensor communication protocols Sensor Communication Protocols
Apply calibration to specific sensor types in depth Sensor Types: Calibration
Practice more sensor labs on ESP32 with Wokwi Sensor Labs: Implementation and Review