29  Calibration Lab (Wokwi)

In 60 Seconds

Every sensor needs calibration because manufacturing variations cause individual sensors to read slightly differently. Two-point calibration (using a known low and high reference value) corrects both offset and gain errors with a simple linear equation: \(y = m \cdot x + b\). Precision (repeatability) cannot be improved by calibration – only accuracy (closeness to true value) can.

Key Concepts
  • Wokwi Simulator: Browser-based electronics simulator supporting ESP32, Arduino, and Raspberry Pi Pico with real-time serial monitor and component libraries — no hardware installation required
  • Potentiometer as Sensor Substitute: In Wokwi calibration labs, a potentiometer simulates a sensor with deliberate offset and gain errors; turning the knob changes ADC input voltage, mimicking real sensor output variation
  • Serial Plotter: Tool that graphs serial-printed values in real time; essential for visualizing the difference between raw and calibrated sensor readings during calibration exercises
  • Calibration Workflow: Capture reading at low reference, capture reading at high reference, calculate gain = (high_ref - low_ref) / (high_raw - low_raw), calculate offset = low_ref - gain x low_raw, apply to future readings
  • EEPROM.put / EEPROM.get: Arduino EEPROM library functions for writing and reading any data type at a given memory address; used to persist calibration coefficients across power cycles
  • diagram.json: The Wokwi circuit definition file specifying components and wire connections; edit this file to modify the simulated circuit
  • Two-Point vs. Multi-Point Calibration: Two-point calibration corrects offset and linear gain errors and suffices for most IoT sensors. Multi-point polynomial calibration is needed when the sensor has significant nonlinearity
  • Calibration Validation: After applying coefficients, test at intermediate values to detect nonlinearity errors that two-point calibration cannot correct

29.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
  • Apply temperature compensation to reduce drift-related measurement errors
  • 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

This lab teaches you how to make sensor readings more accurate by comparing them against known reference values – like adjusting a kitchen scale by first weighing something you know is exactly one kilogram. You will use an online simulator to practice the technique safely, learning to correct both constant offsets (always reading too high) and scaling errors (reading more wrong as values increase).

29.2 Why Calibration Matters

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

Diagram illustrating why sensor calibration matters showing uncalibrated versus calibrated readings
Figure 29.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-8 C tracking $500k product saved
Industrial process Quality defects In-spec production $50k/month savings
Try It: Offset and Gain Error Explorer

Adjust the offset and gain sliders to see how each type of error distorts sensor readings. The ideal line (green) shows perfect 1:1 mapping, while the error line (orange) shows how your sensor actually reads. Notice how offset shifts the entire line up or down, while gain tilts it.

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

29.4 Wokwi Simulator

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

Simulator 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

29.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.).

Calibration lab circuit diagram showing potentiometer connected to ESP32 GPIO 34 ADC input with LED status indicator on GPIO 2 and power connections
Figure 29.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).

29.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" ] ]
  ]
}

29.6 Complete Calibration Code

This code implements two-point calibration with an interactive Serial menu and an accuracy-vs-precision demonstration. Copy it into the Wokwi editor:

/*
 * SENSOR CALIBRATION LAB - ESP32 Wokwi Simulation
 *
 * This lab demonstrates sensor calibration techniques:
 *   1. Two-Point Calibration (offset + gain correction)
 *   2. Accuracy vs Precision Demonstration
 *   3. Interactive calibration wizard via Serial menu
 *
 * Hardware Setup (in Wokwi):
 *   - Potentiometer connected to GPIO 34 (simulates sensor)
 *   - Built-in LED on GPIO 2 (status indicator)
 *   - Serial input for menu commands
 */

#include <Arduino.h>

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

// Calibration Configuration
const int SAMPLES_PER_READING = 10;

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

TwoPointCalibration twoPointCal = {0.0, 1.0, false};

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

29.7 Lab Exercises

Complete the following exercises to master sensor calibration:

Exercise 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 position (turn fully counterclockwise), 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?
Exercise 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?
Try It: Accuracy vs Precision Target Visualizer

Explore the difference between accuracy and precision using a classic target analogy. Accuracy is how close the average is to the bullseye (true value), while precision is how tightly clustered the measurements are. Adjust the sliders and click “Generate Shots” to see new random measurements.

Challenge: 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 represents the sensor’s temperature sensitivity (for example, -0.01 units per degree C above the reference temperature).

Check your understanding of the calibration math before continuing:

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

29.8.1 Try It: Two-Point Calibration Calculator

Use this interactive calculator to experiment with two-point calibration. Enter your raw ADC readings at two known reference points and see the calibration equation computed instantly.

29.9 Knowledge Check

Sammy the Sensor thought he was measuring temperature perfectly, but when Max the Microcontroller compared Sammy’s reading to an ice bath (which should be exactly 0 degrees), Sammy said “2 degrees!” That is called an offset – Sammy was always reading a little too high.

“No worries,” said Lila the LED. “We just need to calibrate you!” They dunked Sammy in ice water and wrote down what he said (2 degrees instead of 0). Then they dunked him in boiling water and wrote that down too (98.5 degrees instead of 100).

Now Max could do some math: every time Sammy says a number, Max adjusts it using those two reference points, like drawing a straight line between them. “I am calibrated!” Sammy cheered. “Now my readings match reality!”

Bella the Battery reminded everyone: “But calibration only fixes if Sammy is consistently wrong. If he is randomly jumpy (saying different numbers each time), that is a precision problem – we need a filter for that, not calibration!”

Scenario: You’re building a smart beehive scale to track honey production. The system uses a 50kg load cell with HX711 amplifier. Raw ADC readings don’t directly correspond to weight — calibration is required to convert raw counts to kilograms.

Hardware Setup:

  • Load cell: 50kg capacity, 2 mV/V sensitivity
  • HX711: 24-bit ADC, 128× gain
  • ESP32: Reading HX711 via bit-banging protocol
  • Calibration weights: 0kg (empty), 5kg, 10kg, 20kg known masses (covering the expected 0-20 kg measurement range for typical hive weight changes)

Step 1: Collect Raw Readings

#include "HX711.h"

HX711 scale;

void setup() {
    Serial.begin(115200);
    scale.begin(LOADCELL_DOUT_PIN, LOADCELL_SCK_PIN);
    scale.set_scale(); // No scale factor yet
    scale.tare();      // Reset to zero
}

void collectCalibrationData() {
    Serial.println("=== CALIBRATION DATA COLLECTION ===");

    Serial.println("Remove all weight. Press any key...");
    waitForKeypress();
    long raw_0kg = scale.get_units(10); // Average 10 readings
    Serial.print("0 kg → Raw: "); Serial.println(raw_0kg);

    Serial.println("Place 5 kg weight. Press any key...");
    waitForKeypress();
    long raw_5kg = scale.get_units(10);
    Serial.print("5 kg → Raw: "); Serial.println(raw_5kg);

    Serial.println("Place 10 kg weight. Press any key...");
    waitForKeypress();
    long raw_10kg = scale.get_units(10);
    Serial.print("10 kg → Raw: "); Serial.println(raw_10kg);

    Serial.println("Place 20 kg weight. Press any key...");
    waitForKeypress();
    long raw_20kg = scale.get_units(10);
    Serial.print("20 kg → Raw: "); Serial.println(raw_20kg);
}

Example Output:

=== CALIBRATION DATA COLLECTION ===
0 kg → Raw: 150
5 kg → Raw: 42650
10 kg → Raw: 85150
20 kg → Raw: 170150

Step 2: Two-Point Calibration (Simple Method)

Using 0kg and 20kg points:

// Calibration data
const long RAW_AT_0KG = 150;
const long RAW_AT_20KG = 170150;

// Calculate calibration factor
float calculateCalibrationFactor() {
    float raw_span = RAW_AT_20KG - RAW_AT_0KG;
    float kg_span = 20.0 - 0.0;
    float calibration_factor = raw_span / kg_span;

    Serial.print("Calibration factor: ");
    Serial.print(calibration_factor);
    Serial.println(" raw units per kg");

    return calibration_factor;
}

void setup() {
    // ... initialization code ...

    float cal_factor = calculateCalibrationFactor();
    scale.set_scale(cal_factor);
    scale.tare(); // Zero the scale
}

void loop() {
    float weight_kg = scale.get_units(5); // Average 5 readings
    Serial.print("Weight: ");
    Serial.print(weight_kg, 2);
    Serial.println(" kg");
    delay(1000);
}

Putting Numbers to It:

The two-point calibration formula converts raw ADC counts to physical weight using a linear equation.

\[ \text{weight}_{\text{kg}} = \frac{\text{raw} - \text{raw}_{0\text{kg}}}{\text{raw}_{20\text{kg}} - \text{raw}_{0\text{kg}}} \times 20 \]

Using our beehive load cell with raw readings of 150 at 0 kg and 170,150 at 20 kg:

\[ \text{weight}_{\text{kg}} = \frac{\text{raw} - 150}{170150 - 150} \times 20 = \frac{\text{raw} - 150}{170000} \times 20 \]

For a measured raw value of 85,150:

\[ \text{weight}_{\text{kg}} = \frac{85150 - 150}{170000} \times 20 = \frac{85000}{170000} \times 20 = 0.5 \times 20 = 10.00 \text{ kg} \]

This perfectly matches the 10 kg calibration weight, verifying our calibration is accurate.

Calculation:

Calibration factor = (170150 - 150) / (20 - 0)
                   = 170000 / 20
                   = 8500 raw units per kg

Step 3: Verify Two-Point Calibration

Place known weights and compare measured vs actual:

Actual Weight Raw Reading Calculated Weight Error
0 kg (cal point) 150 0.00 kg 0.00 kg
5 kg 42,680 5.04 kg +0.04 kg
10 kg 85,100 9.99 kg -0.01 kg
20 kg (cal point) 170,150 20.00 kg 0.00 kg

The calibration points (0 kg and 20 kg) always show zero error because they were used to compute the equation. Intermediate points may show small residual errors due to minor sensor non-linearity.

Actual Weight Raw Reading Calculated Weight Error
7.5 kg 63,920 7.50 kg +0.00 kg
15 kg 127,700 15.01 kg +0.01 kg

Excellent – the load cell is highly linear, with errors well below 0.1%. Two-point calibration is sufficient.

Step 4: Multi-Point Calibration (Advanced)

For sensors with non-linearity, use interpolation between multiple points:

// Multi-point calibration table
struct CalPoint {
    long raw;
    float kg;
};

const CalPoint CAL_TABLE[] = {
    {150, 0.0},
    {42650, 5.0},
    {85150, 10.0},
    {127650, 15.0},
    {170150, 20.0}
};
const int CAL_POINTS = 5;

float interpolateWeight(long raw_reading) {
    // Find bracketing points
    for (int i = 0; i < CAL_POINTS - 1; i++) {
        if (raw_reading >= CAL_TABLE[i].raw &&
            raw_reading <= CAL_TABLE[i+1].raw) {

            // Linear interpolation
            long raw_low = CAL_TABLE[i].raw;
            long raw_high = CAL_TABLE[i+1].raw;
            float kg_low = CAL_TABLE[i].kg;
            float kg_high = CAL_TABLE[i+1].kg;

            float fraction = (float)(raw_reading - raw_low) /
                           (float)(raw_high - raw_low);

            float weight = kg_low + fraction * (kg_high - kg_low);
            return weight;
        }
    }

    // Outside calibration range - use linear extrapolation
    if (raw_reading < CAL_TABLE[0].raw) {
        return CAL_TABLE[0].kg; // Below minimum
    } else {
        return CAL_TABLE[CAL_POINTS-1].kg; // Above maximum
    }
}

Step 5: Temperature Compensation (Optional)

Load cells drift with temperature. If your beehive experiences 0-40°C swings:

#include <OneWire.h>
#include <DallasTemperature.h>

// Temperature compensation coefficient (from load cell datasheet)
const float TEMP_COEFFICIENT = 0.0002; // 0.02% per °C
const float REFERENCE_TEMP = 25.0; // Calibration temperature

float applyTemperatureCompensation(float raw_weight, float current_temp) {
    float temp_diff = current_temp - REFERENCE_TEMP;
    float compensation = 1.0 + (TEMP_COEFFICIENT * temp_diff);
    float compensated_weight = raw_weight / compensation;

    return compensated_weight;
}

void loop() {
    sensors.requestTemperatures();
    float temp_c = sensors.getTempCByIndex(0);

    float raw_weight = scale.get_units(5);
    float compensated_weight = applyTemperatureCompensation(raw_weight, temp_c);

    Serial.print("Raw: "); Serial.print(raw_weight, 2);
    Serial.print(" kg, Temp: "); Serial.print(temp_c, 1);
    Serial.print(" °C, Compensated: "); Serial.print(compensated_weight, 2);
    Serial.println(" kg");
}

Impact of Temperature Compensation:

Temperature Actual Weight Measured (No Compensation) Measured (With Compensation) Error Reduction
0 C 10.00 kg 9.95 kg 10.00 kg 0.05 kg fixed
25 C (ref) 10.00 kg 10.00 kg 10.00 kg 0.00 kg
40 C 10.00 kg 10.03 kg 10.00 kg 0.03 kg fixed
Try It: Temperature Compensation Simulator

See how ambient temperature affects sensor readings and how compensation corrects the error. Adjust the actual weight and ambient temperature to observe drift, then toggle compensation on or off to see the correction in action.

Step 6: Store Calibration in NVS (Non-Volatile Storage)

Preserve calibration across power cycles using the ESP32’s NVS partition (accessed through the Preferences library):

#include <Preferences.h>

Preferences preferences;

void saveCalibration(float cal_factor, long tare_offset) {
    preferences.begin("scale", false);
    preferences.putFloat("cal_factor", cal_factor);
    preferences.putLong("tare_offset", tare_offset);
    preferences.end();
    Serial.println("Calibration saved to NVS");
}

bool loadCalibration(float &cal_factor, long &tare_offset) {
    preferences.begin("scale", true); // Read-only

    if (!preferences.isKey("cal_factor")) {
        preferences.end();
        Serial.println("No calibration found in NVS");
        return false;
    }

    cal_factor = preferences.getFloat("cal_factor");
    tare_offset = preferences.getLong("tare_offset");
    preferences.end();

    Serial.print("Loaded calibration: factor=");
    Serial.print(cal_factor);
    Serial.print(", offset=");
    Serial.println(tare_offset);

    return true;
}

void setup() {
    // ... other setup ...

    float cal_factor;
    long tare_offset;

    if (loadCalibration(cal_factor, tare_offset)) {
        // Use stored calibration
        scale.set_scale(cal_factor);
        scale.set_offset(tare_offset);
    } else {
        // Perform new calibration
        performCalibration();
        saveCalibration(scale.get_scale(), scale.get_offset());
    }
}

Step 7: Calibration Quality Metrics

Verify your calibration is good:

void verifyCalibration() {
    const int TEST_POINTS = 4;
    float actual_weights[] = {5.0, 10.0, 15.0, 20.0};
    float measured_weights[TEST_POINTS];

    Serial.println("\n=== CALIBRATION VERIFICATION ===");

    for (int i = 0; i < TEST_POINTS; i++) {
        Serial.print("Place ");
        Serial.print(actual_weights[i]);
        Serial.println(" kg. Press key...");
        waitForKeypress();

        measured_weights[i] = scale.get_units(10);
        Serial.print("Measured: ");
        Serial.print(measured_weights[i], 3);
        Serial.println(" kg");
    }

    // Calculate RMSE (Root Mean Square Error)
    float sum_sq_error = 0;
    for (int i = 0; i < TEST_POINTS; i++) {
        float error = measured_weights[i] - actual_weights[i];
        sum_sq_error += error * error;
    }
    float rmse = sqrt(sum_sq_error / TEST_POINTS);

    Serial.print("\nRMSE: ");
    Serial.print(rmse, 4);
    Serial.println(" kg");

    if (rmse < 0.05) {
        Serial.println("✓ EXCELLENT calibration (< 50g error)");
    } else if (rmse < 0.1) {
        Serial.println("✓ GOOD calibration (< 100g error)");
    } else {
        Serial.println("✗ POOR calibration - recalibrate!");
    }

    // Calculate R² (coefficient of determination)
    float mean_actual = 0;
    for (int i = 0; i < TEST_POINTS; i++) {
        mean_actual += actual_weights[i];
    }
    mean_actual /= TEST_POINTS;

    float ss_total = 0, ss_residual = 0;
    for (int i = 0; i < TEST_POINTS; i++) {
        ss_total += pow(actual_weights[i] - mean_actual, 2);
        ss_residual += pow(actual_weights[i] - measured_weights[i], 2);
    }

    float r_squared = 1 - (ss_residual / ss_total);
    Serial.print("R² (linearity): ");
    Serial.println(r_squared, 4);

    if (r_squared > 0.999) {
        Serial.println("✓ EXCELLENT linearity");
    } else if (r_squared > 0.99) {
        Serial.println("✓ GOOD linearity");
    } else {
        Serial.println("✗ POOR linearity - check sensor");
    }
}

Expected Output:

=== CALIBRATION VERIFICATION ===
Place 5.0 kg. Press key...
Measured: 5.002 kg

Place 10.0 kg. Press key...
Measured: 9.998 kg

Place 15.0 kg. Press key...
Measured: 15.003 kg

Place 20.0 kg. Press key...
Measured: 19.996 kg

RMSE: 0.0032 kg
✓ EXCELLENT calibration (< 50g error)
R² (linearity): 0.9999
✓ EXCELLENT linearity
Try It: Calibration Quality Metrics Calculator

Enter your measured vs actual values at several test points to compute RMSE and R-squared. These metrics tell you how well your calibration is performing. Add up to 6 test points; unused points (actual = 0, measured = 0) are excluded.

Real-World Application Results:

  • Before calibration: Raw counts meaningless
  • After two-point calibration: ±50g accuracy (0.1% error)
  • With temperature compensation: ±25g accuracy (0.05% error)
  • Beehive monitoring: Tracks daily honey production to ±10g resolution

Key Lessons:

  1. Always use known reference weights - bathroom scale not accurate enough
  2. Bracket your measurement range - calibrate at 0 and 20kg for 0-20kg application
  3. Verify with independent points - test at 5, 10, 15kg to check linearity
  4. Store calibration persistently - don’t force users to recalibrate after power cycle
  5. Temperature matters - ±15°C swing causes 0.3% error without compensation
  6. Multi-point helps non-linear sensors - but load cells are usually very linear

29.10 Summary

This calibration lab covered professional sensor calibration techniques through hands-on Wokwi simulation:

  • Two-point calibration corrects both offset (y-intercept) and gain (slope) errors using two known reference values – sufficient for linear sensors like load cells
  • Multi-point calibration with linear interpolation handles non-linear sensors by dividing the range into segments
  • Calibration equation \(y = \text{gain} \times x + \text{offset}\) transforms raw ADC readings to calibrated physical units
  • Accuracy vs precision are distinct: precision is repeatability (improved by filtering), accuracy is closeness to truth (improved by calibration)
  • Calibration verification using RMSE and R-squared confirms that corrections are working correctly
  • NVS storage (ESP32 Preferences library) preserves calibration coefficients across power cycles, avoiding repeated manual recalibration
  • Temperature compensation can further reduce drift-related errors in field-deployed sensors

Common Pitfalls

Capturing a calibration reference point immediately after changing the stimulus gives an unstable reading. Wait for the serial monitor to show stable, consistent values (2-5 seconds) before pressing the calibration capture button.

Using EEPROM with incorrect parameters or writing at the wrong address can corrupt stored coefficients. Always print retrieved coefficients on startup and validate they are within the expected physical range before using them.

The Wokwi ADC is ideal with no noise or nonlinearity. Real ESP32 ADCs have known nonlinearity near supply rails. Calibration from Wokwi simulation will not transfer directly to physical hardware without recalibration.

A window of 10-20 samples works well for a slow temperature sensor. For a fast vibration sensor, a large window destroys the signal. Match the moving average window size to the sensor’s actual time constant.

29.11 What’s Next?

Now that you can calibrate sensors for accuracy, explore related topics to build production-quality IoT measurement systems.

Topic Description
Sensor Labs Overview Review all sensor lab topics and choose your next hands-on exercise
Sensor Best Practices Production-quality implementation guidelines for reliable sensor deployments
Temperature Sensor Labs Apply calibration techniques to temperature sensing with thermistors and digital sensors
Sensor Fundamentals Deepen your understanding of sensor characteristics, specifications, and selection criteria
Data Filtering & Noise Reduction Complement calibration (accuracy) with filtering techniques to improve precision

29.12 Concept Relationships

Core Concept Related Concepts Why It Matters
Two-Point Calibration Offset Error, Gain Error, Linear Equation Fixes both constant and proportional errors simultaneously
Accuracy vs Precision Repeatability, Calibration Limitations Calibration improves accuracy but cannot fix poor precision
Temperature Compensation Drift Correction, Reference Temperature Reduces measurement errors caused by ambient temperature changes
NVS Storage Persistence, Power Cycles Preserves calibration across device resets