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:
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:
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.
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:
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.
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
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)
- 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
- 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)”
- 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
- 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