528  Sensor Calibration Lab (Wokwi Simulation)

528.1 Learning Objectives

Time: ~45 min | Level: Advanced | Code: P06.C10.LAB01

By the end of this lab, you will be able to:

  • Implement two-point calibration to correct offset and gain errors in analog sensors
  • Build multi-point calibration tables with linear interpolation for non-linear sensors
  • Detect and compensate for sensor drift over time using running baselines
  • Distinguish accuracy from precision through practical measurement exercises
  • Verify calibration quality using statistical metrics (RMSE, R-squared)
  • Store calibration coefficients in non-volatile memory for persistence across power cycles

528.2 Why Calibration Matters

Every sensor comes from manufacturing with slight variations. A temperature sensor rated at +/-0.5C accuracy might actually read 2C too high consistently. Without calibration, your IoT system makes decisions based on incorrect data.

%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#2C3E50', 'primaryTextColor': '#fff', 'primaryBorderColor': '#16A085', 'lineColor': '#16A085', 'secondaryColor': '#E67E22', 'tertiaryColor': '#fff'}}}%%
flowchart LR
    subgraph Uncalibrated["Without Calibration"]
        A1[Sensor reads 25C] --> B1[Actual temp: 23C]
        B1 --> C1[Error: +2C]
        C1 --> D1[Wrong decisions]
    end

    subgraph Calibrated["With Calibration"]
        A2[Sensor reads 25C] --> B2[Apply correction]
        B2 --> C2[Corrected: 23C]
        C2 --> D2[Accurate data]
    end

    style Uncalibrated fill:#FFEBEE,stroke:#E74C3C
    style Calibrated fill:#E8F5E9,stroke:#27AE60

Figure 528.1: Calibration transforms raw sensor readings into accurate measurements by applying mathematical corrections based on known reference values.

Real-World Impact Example:

Scenario Uncalibrated Result Calibrated Result Cost Impact
Smart thermostat AC runs 20% more Optimal comfort $180/year savings
Cold chain monitoring False vaccine spoilage alarm Accurate 2-8C tracking $500k product saved
Industrial process Quality defects In-spec production $50k/month savings

528.3 Prerequisites

Before starting this lab, ensure you understand:

  • Basic Arduino/ESP32 programming (setup, loop, Serial.print)
  • Analog-to-digital conversion concepts (ADC resolution, voltage dividers)
  • Linear equations (y = mx + b) for calibration math

528.4 Wokwi Simulator

NoteAbout Wokwi

Wokwi is a free online simulator for Arduino, ESP32, and other microcontrollers. This lab uses simulated sensors to demonstrate calibration concepts. The techniques you learn apply directly to real hardware with physical sensors.

Launch the simulator below and copy the calibration code to explore sensor calibration interactively.

TipSimulator Tips
  • Click the + button to add components (search for “Potentiometer” to simulate a sensor)
  • Connect the potentiometer middle pin to GPIO 34 (ADC input)
  • Use the Serial Monitor to view calibration output
  • Click “Start Simulation” (green play button) to run the code
  • Adjust the potentiometer during simulation to test calibration

528.5 Circuit Setup

For this lab, we simulate a sensor using a potentiometer. In real applications, replace this with your actual sensor (thermistor, pressure sensor, etc.).

%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#2C3E50', 'primaryTextColor': '#fff', 'primaryBorderColor': '#16A085', 'lineColor': '#16A085', 'secondaryColor': '#E67E22', 'tertiaryColor': '#fff'}}}%%
flowchart TB
    subgraph ESP32["ESP32 DevKit"]
        ADC["GPIO 34 (ADC1_CH6)"]
        V3["3.3V"]
        GND["GND"]
        LED1["GPIO 2 (Built-in LED)"]
        BTN["GPIO 0 (Boot Button)"]
    end

    subgraph POT["Potentiometer (10kOhm)"]
        P1["Pin 1 (VCC)"]
        P2["Pin 2 (Wiper/Signal)"]
        P3["Pin 3 (GND)"]
    end

    V3 --> P1
    P2 --> ADC
    P3 --> GND

    style ESP32 fill:#2C3E50,color:#fff
    style POT fill:#16A085,color:#fff

Figure 528.2: Wiring diagram: Connect potentiometer to ESP32 ADC pin 34 for sensor simulation. The potentiometer simulates varying sensor output from 0V (0% rotation) to 3.3V (100% rotation).

528.5.1 Wokwi diagram.json

Copy this JSON into the diagram.json tab in Wokwi:

{
  "version": 1,
  "author": "IoT Class",
  "editor": "wokwi",
  "parts": [
    { "type": "wokwi-esp32-devkit-v1", "id": "esp", "top": 0, "left": 0, "attrs": {} },
    {
      "type": "wokwi-potentiometer",
      "id": "pot1",
      "top": 150,
      "left": 100,
      "attrs": { "value": "50" }
    }
  ],
  "connections": [
    [ "esp:3V3", "pot1:VCC", "red", [ "v:30", "h:50" ] ],
    [ "esp:GND.1", "pot1:GND", "black", [ "v:40", "h:80" ] ],
    [ "pot1:SIG", "esp:34", "green", [ "h:0" ] ]
  ]
}

528.6 Complete Calibration Code

This comprehensive code demonstrates all calibration techniques. Copy it into the Wokwi editor:

/*
 * SENSOR CALIBRATION LAB - ESP32 Wokwi Simulation
 *
 * This lab demonstrates professional sensor calibration techniques:
 *   1. Two-Point Calibration (offset + gain correction)
 *   2. Multi-Point Calibration with Linear Interpolation
 *   3. Drift Detection and Compensation
 *   4. Accuracy vs Precision Demonstration
 *   5. Calibration Verification and Validation
 *
 * Hardware Setup (in Wokwi):
 *   - Potentiometer connected to GPIO 34 (simulates sensor)
 *   - Built-in LED on GPIO 2 (status indicator)
 *   - Boot button on GPIO 0 (calibration trigger)
 */

#include <Arduino.h>
#include <Preferences.h>  // For storing calibration in NVS

// Pin Definitions
const int SENSOR_PIN = 34;
const int LED_PIN = 2;
const int ADC_MAX_VALUE = 4095;
const float ADC_MAX_VOLTAGE = 3.3;

// Calibration Configuration
const int NUM_CALIBRATION_POINTS = 5;
const int SAMPLES_PER_READING = 10;
const float REFERENCE_VALUES[] = {0.0, 25.0, 50.0, 75.0, 100.0};

// Two-Point Calibration Structure
struct TwoPointCalibration {
    float offset;
    float gain;
    bool isValid;
};

TwoPointCalibration twoPointCal = {0.0, 1.0, false};
Preferences preferences;

// Read raw ADC with averaging
int readRawSensor(int pin, int samples) {
    long sum = 0;
    for (int i = 0; i < samples; i++) {
        sum += analogRead(pin);
        delay(10);
    }
    return sum / samples;
}

// Apply two-point calibration
float applyTwoPointCalibration(int rawValue) {
    if (!twoPointCal.isValid) {
        return (rawValue / (float)ADC_MAX_VALUE) * 100.0;
    }
    return (rawValue * twoPointCal.gain) + twoPointCal.offset;
}

// Perform two-point calibration
void performTwoPointCalibration(int rawLow, float trueLow, int rawHigh, float trueHigh) {
    Serial.println("\n=== TWO-POINT CALIBRATION ===");

    if (rawHigh == rawLow) {
        Serial.println("ERROR: Low and high values cannot be equal!");
        return;
    }

    twoPointCal.gain = (trueHigh - trueLow) / (float)(rawHigh - rawLow);
    twoPointCal.offset = trueLow - (twoPointCal.gain * rawLow);
    twoPointCal.isValid = true;

    Serial.println("\nCalibration Equation:");
    Serial.printf("  y = %.6f * x + %.4f\n", twoPointCal.gain, twoPointCal.offset);

    // Verify
    float verifyLow = applyTwoPointCalibration(rawLow);
    float verifyHigh = applyTwoPointCalibration(rawHigh);
    Serial.printf("\nVerification: Low=%.2f (expected %.2f), High=%.2f (expected %.2f)\n",
                  verifyLow, trueLow, verifyHigh, trueHigh);
}

// Accuracy vs Precision demonstration
void demonstrateAccuracyVsPrecision() {
    Serial.println("\n=== ACCURACY vs PRECISION ===");

    const int NUM_SAMPLES = 20;
    float readings[NUM_SAMPLES];
    float sum = 0.0;

    Serial.println("Collecting 20 samples...");
    for (int i = 0; i < NUM_SAMPLES; i++) {
        int raw = readRawSensor(SENSOR_PIN, SAMPLES_PER_READING);
        readings[i] = applyTwoPointCalibration(raw);
        sum += readings[i];
        Serial.printf("  Sample %2d: %.2f\n", i+1, readings[i]);
        delay(100);
    }

    // Calculate statistics
    float mean = sum / NUM_SAMPLES;
    float variance = 0;
    for (int i = 0; i < NUM_SAMPLES; i++) {
        variance += (readings[i] - mean) * (readings[i] - mean);
    }
    float stdDev = sqrt(variance / NUM_SAMPLES);

    Serial.println("\n--- PRECISION (repeatability) ---");
    Serial.printf("  Mean: %.4f\n", mean);
    Serial.printf("  Std Dev: %.4f (lower = more precise)\n", stdDev);

    if (stdDev < 0.5) {
        Serial.println("  PRECISION: EXCELLENT");
    } else if (stdDev < 1.0) {
        Serial.println("  PRECISION: GOOD");
    } else {
        Serial.println("  PRECISION: FAIR - consider filtering");
    }
}

// Interactive menu
void displayMenu() {
    Serial.println("\n=== CALIBRATION LAB MENU ===");
    Serial.println("  r - Read current sensor value");
    Serial.println("  2 - Two-point calibration wizard");
    Serial.println("  a - Accuracy vs Precision demo");
    Serial.println("  c - Clear calibration");
    Serial.println("  h - Help (this menu)");
}

void processCommand() {
    if (Serial.available() > 0) {
        char cmd = Serial.read();
        while (Serial.available()) Serial.read();  // Clear buffer

        switch (cmd) {
            case 'r':
            case 'R': {
                int raw = readRawSensor(SENSOR_PIN, SAMPLES_PER_READING);
                float calibrated = applyTwoPointCalibration(raw);
                Serial.printf("\nRaw: %d, Calibrated: %.2f\n", raw, calibrated);
                break;
            }
            case '2': {
                Serial.println("\n=== TWO-POINT CALIBRATION WIZARD ===");
                Serial.println("Set potentiometer to LOW position, press any key...");
                while (!Serial.available()) delay(100);
                while (Serial.available()) Serial.read();
                int rawLow = readRawSensor(SENSOR_PIN, SAMPLES_PER_READING);
                Serial.printf("Low raw: %d\n", rawLow);

                Serial.println("Set potentiometer to HIGH position, press any key...");
                while (!Serial.available()) delay(100);
                while (Serial.available()) Serial.read();
                int rawHigh = readRawSensor(SENSOR_PIN, SAMPLES_PER_READING);
                Serial.printf("High raw: %d\n", rawHigh);

                performTwoPointCalibration(rawLow, 0.0, rawHigh, 100.0);
                break;
            }
            case 'a':
            case 'A':
                demonstrateAccuracyVsPrecision();
                break;
            case 'c':
            case 'C':
                twoPointCal.isValid = false;
                twoPointCal.gain = 1.0;
                twoPointCal.offset = 0.0;
                Serial.println("\nCalibration cleared.");
                break;
            case 'h':
            case 'H':
                displayMenu();
                break;
            default:
                if (cmd != '\n' && cmd != '\r') {
                    Serial.printf("Unknown command: '%c'\n", cmd);
                }
        }
    }
}

void setup() {
    Serial.begin(115200);
    delay(1000);

    Serial.println("\n========================================");
    Serial.println("   SENSOR CALIBRATION LAB");
    Serial.println("   ESP32 Wokwi Simulation");
    Serial.println("========================================");

    analogReadResolution(12);
    pinMode(LED_PIN, OUTPUT);

    displayMenu();
}

void loop() {
    processCommand();

    // Blink LED to show system running
    static unsigned long lastBlink = 0;
    if (millis() - lastBlink > 1000) {
        digitalWrite(LED_PIN, !digitalRead(LED_PIN));
        lastBlink = millis();
    }

    delay(10);
}

528.7 Lab Exercises

Complete the following exercises to master sensor calibration:

NoteExercise 1: Two-Point Calibration (15 min)

Objective: Perform basic two-point calibration and verify results.

Steps:

  1. Run the code in Wokwi simulator
  2. Type r to read the raw sensor value
  3. Set potentiometer to minimum (~10%), note the raw value
  4. Type 2 to start two-point calibration wizard
  5. Follow prompts to collect low and high calibration points
  6. Type r again to verify calibrated output

Success Criteria:

  • Calibration equation displayed correctly
  • Low position reads ~0, high position reads ~100

Questions to Answer:

  1. What calibration equation did you obtain?
  2. How much did accuracy improve after calibration?
NoteExercise 2: Accuracy vs Precision Analysis (10 min)

Objective: Understand the critical difference between accuracy and precision.

Steps:

  1. Type a to run the accuracy vs precision demonstration
  2. Observe the statistical metrics (mean, std dev)
  3. Note the difference between precision (std dev) and accuracy (error from true)

Reflection Questions:

  1. Can a sensor be precise but not accurate? Give an example.
  2. Can calibration fix precision problems? Why or why not?
  3. Which is more important for trend detection: accuracy or precision?
WarningChallenge: Temperature Coefficient Compensation

Advanced: Modify the code to include temperature compensation.

Real sensors drift with ambient temperature. Add a second “temperature sensor” (another potentiometer) and implement:

float compensatedReading = calibratedValue + (ambientTemp - 25.0) * TEMP_COEFFICIENT;

Where TEMP_COEFFICIENT might be -0.01 (1% per degree C).

528.8 Expected Outcomes

After completing this lab, you should observe:

Metric Before Calibration After Two-Point
Accuracy Error 2-5% <0.5%
Calibration Equation N/A y = gain*x + offset
Verification N/A Both points match expected
TipKey Takeaways
  1. Two-point calibration fixes offset and gain errors - sufficient for linear sensors
  2. Multi-point calibration handles non-linearity using interpolation tables
  3. Drift detection monitors sensor health over time
  4. Precision (repeatability) cannot be improved by calibration - only accuracy can
  5. Store calibration in NVS to persist across power cycles
  6. Verify calibration using statistical metrics (RMSE, R-squared)

528.9 Knowledge Check

Question 1: You calibrate a temperature sensor using ice bath (0C reads as 1.5C) and boiling water (100C reads as 98.5C). What is the calibration slope?

Explanation: Using two-point calibration formula: Slope = (True_High - True_Low) / (Raw_High - Raw_Low) = (100 - 0) / (98.5 - 1.5) = 100 / 97 = 1.031

Question 2: What is sensor hysteresis?

Explanation: Hysteresis means the sensor gives different outputs for the same input depending on whether the input is increasing or decreasing. Example: A pressure sensor reads 100 kPa when rising but 99.5 kPa when falling from above.

Question 3: Which is an advantage of digital sensors (I2C, SPI) over analog sensors?

Explanation: Digital sensors are immune to electrical noise because they send discrete digital signals (0s and 1s) rather than analog voltages. A 10m I2C cable works fine, while an analog sensor would have significant voltage drop and noise pickup.

528.10 Summary

This calibration lab covered professional sensor calibration techniques:

  • Two-point calibration corrects both offset (y-intercept) and gain (slope) errors using two known reference values
  • Calibration equation y = gain * raw + offset transforms raw ADC readings to calibrated units
  • Accuracy vs precision distinction: precision is repeatability (improved by filtering), accuracy is correctness (improved by calibration)
  • Wokwi simulation provides hands-on practice without physical hardware
  • NVS storage preserves calibration coefficients across power cycles

528.11 What’s Next?

Return to the sensor labs overview to review all topics, or continue to actuators to learn about output devices.

Return to Sensor Labs Overview →

Continue to Actuators →

528.12 See Also