UNIT 11 · LESSON 5 OF 6

Digital Filtering and Sensor Calibration

How do you tell them apart, and fix each?

INTERACTIVEAveraging away noise costs response time
A noisy step and the output of a smoothing filternoise after the step: 0.31 → 0.12 (×2.6 smaller)reaches 90 % of the step 7 samples after it; the delay is (N − 1)/2 = 3.5 samples onaverage
A noisy step and the output of a smoothing filternoise after the step: 0.31 → 0.12 (×2.6 smaller)reaches 90 % of the step 7 samples after it; thedelay is (N − 1)/2 = 3.5 samples on average

Try this

Filter
Moving-average length N
Exponential α
Choose the exponential filter to change its smoothing factor.
Noise ×2.6 smaller; 90 % after 7 samples.

A moving average of N samples reduces random noise by about √N but delays a step by (N − 1)/2 samples and takes N samples to follow it completely. An exponential filter, y ← y + α(x − y), needs only one stored value; its smoothing and its lag both grow as α gets smaller. Neither removes steady interference such as mains hum, unless the average spans whole periods of it.

What you will be able to do
  • Implement a moving-average and an exponential (IIR) filter and predict their noise reduction and lag.
  • Explain what averaging can and cannot remove.
  • Implement an exponential filter in integer arithmetic without losing resolution.
  • Derive gain and offset from a two-point calibration and apply them.
  • Identify the dominant error in a sensor conversion formula and choose a calibration.
Before you start
  • Resolution, accuracy and ENOB (lesson 2); references (lesson 3).
  • Integer types, shifts and overflow (unit 2, lessons 1 and 2).
Steps in this lesson
  1. Averaging: the moving average
  2. The exponential filter
  3. What averaging cannot do
  4. Calibration
  5. Worked example: the 21 °C room
  6. Common misconceptions

The puzzle

The RP2040’s internal temperature sensor reports 21 °C in a 27 °C room, and the reading jumps by a degree or more between samples. The formula comes straight from the SDK. One problem is noise; the other is not. How do you tell them apart, and fix each?

STEP 1

Averaging: the moving average

A moving average of the last N samples is the simplest low-pass filter:

y[n]=1N∑k=0N−1x[n−k]y[n] = \frac{1}{N} \sum_{k=0}^{N-1} x[n-k]

Random, uncorrelated noise shrinks by about √N: averaging 16 samples gives 4× less noise, 2 bits more effective resolution. The cost is lag: a step appears (N − 1)/2 samples late on average and takes N samples to complete. With a running sum (add the new sample, subtract the one leaving) it costs two additions per sample, plus a buffer of N samples.

STEP 2

The exponential filter

An exponential (first-order IIR) filter stores only its last output:

y[n]=y[n−1]+α (x[n]−y[n−1])y[n] = y[n-1] + \alpha\,\bigl(x[n] - y[n-1]\bigr)

Its time constant is about 1/α samples. With α = 1/2^k the multiplication becomes a shift, but in integers y += (x - y) >> k throws away the low bits of every update, so when rising y stops up to 2^k − 1 codes short of x (falling, an arithmetic shift does reach x; right-shifting negative values is implementation-defined in C before C23). Keeping y scaled up by 2^k (store y × 2^k, add x − (y >> k)) preserves the fraction.

↑ This step uses the figure at the top of the page.

STEP 3

What averaging cannot do

A moving average is the FIR filter whose N coefficients all equal 1/N. Averaging reduces noise that is random and uncorrelated from sample to sample. It does not touch offset, gain error, nonlinearity or a wrong reference, and it only partly reduces interference at a fixed frequency (averaging over exactly one mains period cancels mains hum; averaging over other lengths leaves some). Hardware oversampling, offered by some ADCs (Zephyr exposes it as an oversampling setting), is the same averaging done in the converter.

STEP 4

Calibration

Steady errors are measured once and removed. One-point calibration measures a known input and subtracts the offset. Two-point calibration measures two known inputs, near the ends of the range you use, and fits a line:

gain=v2−v1r2−r1,\text{gain} = \frac{v_2 - v_1}{r_2 - r_1}, offset=v1−gain×r1,\text{offset} = v_1 - \text{gain} \times r_1, v=gain×r+offsetv = \text{gain} \times r + \text{offset}

where r are raw readings and v the true values. The constants go in non-volatile memory (unit 14) and are applied to every reading.

INTERACTIVETwo-point calibration
Measurement error across the range before and after a two-point calibration+0.1 V−0.1 Verror: raw (orange), calibrated (teal)gain 7.8989e-4 V/code, offset -19.7 mV1.8 V reads 1.8 V: error -0.632 mV (calibrated: what is left is quantisation)
Measurement error across the range before and after a two-point calibration+0.1 V−0.1 Verror: raw (orange), calibrated (teal)gain 7.8989e-4 V/code, offset -19.7 mV1.8 V reads 1.8 V: error -0.632 mV (calibrated: whatis left is quantisation)
Error -0.632 mV at 1.8 V (calibrated).

Offset and gain errors are steady, so they can be measured once and removed: read the raw value at two known inputs, fit a straight line through the two points, and apply it to every later reading. The points should span the range you care about. Here a sensor channel has an illustrative offset and gain error; the calibration uses readings at 0.5 V and 3.0 V.

STEP 5

Worked example: the 21 °C room

The SDK’s formula assumes V = code × 3.3 / 4096. Suppose the board’s actual reference is 3.25 V and the sensor is exactly at its nominal 0.706 V (27 °C). The ADC reads 0.706 / 3.25 × 4096 = 889.8, so code 889, and the firmware computes

V′=889×3.34096≈0.7162 V,V' = \frac{889 \times 3.3}{4096} \approx 0.7162\ \text{V}, T=27−0.7162−0.7060.001721≈21.1 ∘CT = 27 - \frac{0.7162 - 0.706}{0.001721} \approx 21.1\ ^{\circ}\text{C}

A 1.5 % reference error becomes a 6 °C error, because the sensor’s slope of 1.721 mV/°C is tiny compared with its 0.706 V offset. At 0.806 mV per code, one LSB is also about 0.47 °C, so noise of a few LSB makes single readings jump by degrees. Fix the steady error with a one-point calibration at a known temperature (or by measuring the reference), and the jumps by averaging, say, 64 readings (8× less noise).

MYTHS AND FACTS

Common misconceptions

Averaging makes the reading accurate

It makes it steady; a steady wrong value is still wrong.

More filtering is always better

Every filter trades noise for lag; a control loop may not tolerate the delay.

The SDK’s formula is calibrated

It is a nominal approximation; each device and board differs.

Integer filters are exact

Shifting away low bits every update loses resolution unless the state keeps extra fraction bits.

Check yourself

Answer in your head, then open the card.

How many samples must be averaged to reduce random noise by a factor of 8, and what lag does that add to a step?

64 samples (√64 = 8); the step appears about 31.5 samples late and completes after 64.

A channel reads 410 at 0.500 V and 3690 at 3.000 V. What voltage does a reading of 2000 represent?

gain = 2.5 / 3280 ≈ 7.62 × 10⁻⁴ V per code; offset = 0.5 − 410 × gain ≈ 0.1875 V; v ≈ 2000 × 7.62 × 10⁻⁴ + 0.1875 ≈ 1.712 V.

Why does y += (x - y) >> 4 never settle exactly on a constant input of y + 10?

(10) >> 4 = 0: the update is zero while the difference is below 16, so y stays up to 15 codes away. Keep y scaled by 16 to keep the fraction.

The temperature reading is steady but 3 °C too high at every temperature you test. Which calibration fixes it?

A one-point (offset) calibration: measure at one known temperature and subtract the difference.

Sources (4)
  1. Arm, CMSIS-DSP, Source/FilteringFunctions/arm_fir_f32.c — FIR filter “y[n] = b[0] * x[n] + b[1] * x[n-1] + b[2] * x[n-2] + ...+ b[numTaps-1] * x[n-numTaps+1]”, with a state buffer of previous samples
  2. Raspberry Pi Ltd, pico-sdk 1.5.1, hardware_adc/adc.h — “Temperature sensor values can be approximated in centigrade as: T = 27 - (ADC_Voltage - 0.706)/0.001721”; “12 bit (8.7 ENOB)”
  3. Raspberry Pi Ltd, pico-examples, adc/onboard_temperature/onboard_temperature.c — reads input 4 after adc_set_temp_sensor_enabled(true), converts with 3.3f / (1 << 12) (“assume max value == ADC_VREF == 3.3 V”) and applies the formula above
  4. Zephyr Project, include/zephyr/drivers/adc.h — struct adc_sequence has an oversampling field (zephyr,oversampling): hardware averaging of 2^n samples per result, offered by some ADCs