7  Sensor Calibration Lab

Hands-On Wokwi Workshop

sensing
lab
calibration
wokwi
Author

IoT Textbook

Published

March 22, 2026

Keywords

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

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

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

7.2 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%).

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:

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

7.4 Prerequisites

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

7.4.1 Learning Path

Learning path flowchart showing progression from prerequisites (ADC Fundamentals, Arduino Basics, Sensor Data Processing) through this lab's content (Circuit Setup, Two-Point Calibration, Signal Filtering, EEPROM Storage) to next steps (Multi-Point Calibration, Sensor Fusion, Production Deployment).
Figure 7.1: Suggested learning progression for sensor calibration topics
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.

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!


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!


7.5 Why 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 7.2: 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.

Two-Point Calibration Cannot Correct Non-Linearity

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

7.5.1 Knowledge Check: Calibration Fundamentals

Question: A temperature sensor consistently reads 3 degrees higher than the actual temperature across its entire range. What type of error is this?

    1. Gain error
    1. Offset error
    1. Non-linearity error
    1. Random noise

B) Offset error

An offset error is a constant deviation from the true value. If the sensor always reads 3 degrees high (at 0, 25, 50, or 100 degrees), the error is constant - this is offset error. Gain error would cause the deviation to grow proportionally with the reading (e.g., 1 degree error at 10 degrees but 5 degrees error at 50 degrees).

Question: Why does two-point calibration require measuring at TWO known reference points instead of just one?

    1. One point is always inaccurate
    1. Two points are needed to calculate both gain AND offset
    1. It takes longer with one point
    1. Single-point calibration is illegal

B) Two points are needed to calculate both gain AND offset

A single point can only correct offset error (by subtracting a constant). But to also correct gain error (sensitivity), you need two points to define a line: y = mx + b. The two points let you calculate both the slope (gain/m) and intercept (offset/b) of the correction equation.

7.6 Part 1: Circuit Setup

7.6.1 Wokwi Simulator

Use the embedded simulator below to build and test your sensor calibration circuit. Click “Start Simulation” after entering your code.

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)

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

7.6.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 7.3: Circuit schematic showing ESP32 connections to potentiometer (sensor) and calibration status LED

7.7 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 (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);
    }
}

7.8 Part 3: Step-by-Step Calibration Procedure

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

Flowchart showing calibration state machine with numbered steps: 1) Normal Mode, 2) Wait for Low Point, 3) Capture Low Point, 4) Wait for High Point, 5) Capture High Point, leading back to Normal Mode with calculated coefficients
Figure 7.4: Calibration state machine workflow

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.

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

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

7.8.3 Step 3: Start Calibration

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

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

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

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

7.9 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 7.5: 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

7.9.1 Interactive Two-Point Calibration Calculator

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.

7.9.2 Interactive ADC Resolution Calculator

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

7.9.3 Knowledge Check: Calibration Math

Question: A sensor reads 20% when the true value is 10%, and reads 80% when the true value is 90%. What is the gain (slope) coefficient?

    1. 0.75
    1. 1.0
    1. 1.33
    1. 4.0

C) 1.33

Using the formula: gain = (actual_high - actual_low) / (raw_high - raw_low)

gain = (90 - 10) / (80 - 20) = 80 / 60 = 1.333...

The sensor’s sensitivity is lower than expected (it only spans 60% when it should span 80%), so we multiply readings by 1.33 to “stretch” them to the correct range.

Question: In the lab code, why is the moving average filter applied BEFORE calibration, not after?

    1. It’s faster to filter first
    1. Filtering removes noise that would cause incorrect calibration captures
    1. Calibration doesn’t work on filtered data
    1. It doesn’t matter - the order is arbitrary

B) Filtering removes noise that would cause incorrect calibration captures

When capturing reference points (pressing ‘l’ or ‘h’), you want a stable, accurate reading - not a noisy instantaneous sample. The moving average filter smooths out noise, so the captured reference values are more reliable. Calibrating on noisy data would result in incorrect coefficients.

Additionally, during normal operation, filtering before calibration prevents noise from being amplified by the gain coefficient. When the calibration gain is greater than 1 (stretching the reading range), any noise in the raw signal is also multiplied by that gain factor. Filtering first reduces noise before it gets amplified.

7.9.4 Interactive 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.

7.10 Part 5: Challenge Exercises

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.

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;
    }
}

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

7.11 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 7.6: 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
Quick Reference Card: 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. For a step-by-step calculation with real numbers, expand the “Putting Numbers to It” callout in Part 4 above.

7.11.1 Knowledge Check: Production Considerations

Question: In the EEPROM storage challenge, why use a “magic number” (0xCAFE) before the calibration data?

    1. It’s a required EEPROM format
    1. It detects if valid calibration data exists vs uninitialized memory
    1. It encrypts the calibration data
    1. It compresses the data to save space

B) It detects if valid calibration data exists vs uninitialized memory

When an ESP32 powers up for the first time, EEPROM may contain random garbage data. Without a magic number, the system might try to use garbage as calibration coefficients, causing incorrect readings. By storing a known value (0xCAFE) alongside valid calibration, you can check if calibration was ever saved. If the magic number isn’t present, you know to use default values instead.

Question: Why does automatic baseline correction (ABC) track the MINIMUM reading over 24 hours, rather than the average or maximum?

    1. Minimum values are more accurate
    1. The sensor assumes it occasionally sees the baseline/reference condition
    1. Maximum values consume too much memory
    1. Averages can’t be stored in EEPROM

B) The sensor assumes it occasionally sees the baseline/reference condition

For sensors like CO2 monitors, ABC assumes the sensor is exposed to “fresh air” (baseline ~400ppm) at least once during the 24-hour period - typically overnight when the room is empty. The minimum reading approximates this baseline. If the sensor has drifted (reading 420ppm for fresh air instead of 400ppm), ABC adjusts the offset to correct for this drift. This technique only works when the assumption of periodic baseline exposure is valid.

Question: You need to calibrate a soil moisture sensor that will measure between 20% and 80% moisture in a greenhouse. Which reference points should you use?

    1. 0% and 100% (full range)
    1. 50% and 60% (middle of expected range)
    1. 15% and 85% (bracketing the expected range)
    1. 20% and 80% (exact boundaries of expected range)

C) 15% and 85% (bracketing the expected range)

You want reference points that bracket (surround) your expected measurement range, but with some margin. Using 15% and 85% provides calibration slightly outside your 20-80% operating range, ensuring good accuracy throughout. Using exact boundaries (20%, 80%) could cause edge-case errors. Using the middle of the range (50%, 60%) provides poor calibration at the extremes. Using 0% and 100% might be impractical to achieve with soil moisture.

Common Mistake: Reference Points Too Close Together

The Mistake: An engineer performs two-point calibration on a soil moisture sensor using reference points at 45% and 55% moisture (only 10% apart). After deployment, the sensor shows large errors at 10% and 90% moisture because small inaccuracies in the closely-spaced reference measurements were amplified across the full range.

Why It Happens: Two-point calibration creates a linear equation (y = mx + b) using two measurements. When those measurements are close together, any small error in the reference measurement or sensor reading gets amplified when extrapolating to values far from the calibration range.

The Math:

Calibration points: 45% and 55%
Assume sensor reads: 450 and 550 (raw ADC values, 0-1000 scale)

Correct calibration:
  Gain   = (55 - 45) / (550 - 450) = 10 / 100 = 0.1
  Offset = 45 - (450 × 0.1) = 45 - 45 = 0

Now imagine a 10 ADC count error at the high point (550 → 560):
  Gain   = (55 - 45) / (560 - 450) = 10 / 110 = 0.0909
  Offset = 45 - (450 × 0.0909) = 45 - 40.91 = 4.09

At 0% moisture (raw ADC = 0):
- Correct calibration: 0 × 0.1 + 0 = 0% ✓
- Wrong calibration:   0 × 0.0909 + 4.09 = 4.1% (error = +4.1%)

At 100% moisture (raw ADC = 1000):
- Correct calibration: 1000 × 0.1 + 0 = 100% ✓
- Wrong calibration:   1000 × 0.0909 + 4.09 = 95.0% (error = -5.0%)

Real Numbers: A 10 ADC count error (1% of full scale) in your reference point, when calibration range is only 100 counts, produces up to 5% error at the measurement extremes. The narrow calibration range acts as a lever arm amplifying tiny reference errors into significant measurement errors.

The Fix: Use calibration points that bracket your expected measurement range with margin:

BAD Practice:
- Greenhouse needs: 20-80% moisture range
- Calibration points: 45% and 55% ❌
- Span: 10% (only 17% of operating range)

GOOD Practice:
- Greenhouse needs: 20-80% moisture range
- Calibration points: 10% and 90% ✓
- Span: 80% (covers 133% of operating range)

Industry Rule of Thumb: Calibration span should be at least 1.2× your measurement range, ideally centered on your operating range. For a 20-80% application, calibrate at 10% and 90%.

Why Wide Span Matters:

  1. Error division: 10 ADC count error ÷ 800 ADC span = 1.25% relative error in gain (vs 10% relative error with a 100-count span)
  2. Interpolation not extrapolation: Measuring within calibration range (interpolation) is accurate; measuring outside (extrapolation) magnifies errors
  3. Sensor non-linearity: Most sensors are most linear in their mid-range; calibrating at extremes captures any non-linearity

Verification Test: After calibration, check several points across the full range: - 0% moisture: Read dry sensor in air - 25% moisture: Use calibrated reference soil - 50% moisture: Half-saturated reference - 75% moisture: Nearly saturated reference - 100% moisture: Fully submerged in water

If errors at 0% and 100% exceed ±2%, your calibration points were too close together.

7.11.2 Interactive Calibration Span Error Calculator

Explore how the width of your calibration span affects error amplification. Adjust the calibration points and reference error to see the impact on measurement accuracy at the extremes.

7.11.3 Knowledge Check: Calibration Span

7.12 Concept Relationships

This Concept Relates To Relationship Type
Two-Point Calibration Linear Algebra Uses y=mx+b equation for correction
Gain Correction Sensitivity Error Multiplier that fixes slope/sensitivity issues
Offset Correction Zero-Point Error Additive constant that fixes bias
EEPROM Storage Persistence Non-volatile memory preserves calibration across power cycles
Moving Average Filter Noise Reduction Pre-filters data before calibration capture

7.13 Summary

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

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

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

7.13.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 7.7: 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

7.14 Knowledge Check

Match each calibration concept with its correct definition:

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

7.15 See Also

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

7.16 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