Embedded HandbookApps
All sections › X. Design, networks and IoT
STM32F103, STM32F407 · level basic · 9 min read
Open in the interactive handbook

81. Sensors on a prototype and fighting noise

A sensor turns a physical quantity into an electrical signal, and a microcontroller turns the signal into a number. Between them there is always noise: the readings "jitter" even when the potentiometer knob is not moving. This section covers the kinds of sensors, how to convert an ADC code into the units you need correctly, and which filter to choose to remove the noise without losing speed.

All filters in this section were built with armclang for Cortex-M3 and M0 and checked by a test on a computer: with a step, with noise and with a single outlier. Every number in the test was recalculated in Python with filters written anew.

Terms used in the documentation

Term Meaning
sensor, transducer a device that converts a physical quantity into a signal
analog / digital output a voltage output / a number sent over an interface
resolution, LSB the number of bits, the size of one quantisation step
full scale the whole input range of the converter
ratiometric measurement a measurement relative to the same reference that powers the sensor
noise, outlier (spike) random jitter, a single wild sample
moving average the mean of the last N samples
exponential moving average (EMA) smoothing where old samples fade out gradually
median filter a filter that takes the middle value of several samples
step response how the output follows a sudden step at the input
fixed-point an integer that stores a fraction with an implied scale

81.1 Kinds of sensors

By output — two large classes:

By operating principle — resistive (the resistance depends on the quantity), capacitive (touch buttons, humidity — section 45), inductive (proximity of metal), piezoelectric (impact, vibration), photoelectric (light).

For a prototype the easiest place to start is a potentiometer: it is a voltage divider (section 50.2), and its reading is easy to check by eye.


81.2 From ADC code to the units you need

The Uno has a 10-bit ADC — codes 0…1023. The F103 and F407 have a 12-bit one, 0…4095, and the voltage is calculated as code × Vref / 4095 (section 14). In integer arithmetic, with rounding and without losing range:

#define VREF_MV 3300
#define ADC_MAX 4095

uint16_t adc_to_mv(uint16_t code)
{
    return (uint16_t)(((uint32_t)code * VREF_MV + ADC_MAX / 2) / ADC_MAX);
}

Code 2048 gives 1650 mV, 4095 gives exactly 3300. The quantisation step is 0.806 mV. The cast to uint32_t is mandatory: 4095 * 3300 does not fit into 16 bits, and on AVR int is exactly that wide (section 80.1).

⚠️ map() divides the scale unevenly. In Arduino you scale with the function map(x, 0, 1023, 0, 10). It computes in integers with truncation, and the value 10 comes from exactly one code out of 1024 — from 1023. The other ten values get 102–103 codes each. The potentiometer knob will almost never give "10". To split the scale into 11 equal steps, compute x * 11 / 1024 — 93–94 codes per step. A continuous quantity needs conversion into units, like adc_to_mv above, not steps.

map(x, 0, 1023, 0, 10): the value 10 gets one code out of 1024 — the knob almost never produces it

Ratiometric measurement. A potentiometer powered from the same reference as the ADC gives a code that does not depend on the supply voltage: at a quarter of its travel it is 1024 at 3.0 V, at 3.3 and at 3.6 V alike. The supply cancels out. On the F103 in LQFP packages with up to 64 pins the ADC reference is the analog supply VDDA, so it is enough to power the potentiometer from it. In packages with 100 pins or more the reference is applied to a separate VREF+ pin. But if the sensor is powered from a separate 3.3 V source while the reference has sagged to 3.0 V, the code becomes 1126 — the change in supply goes straight into the reading.


81.3 Where noise comes from

Noise is reduced in hardware first: a capacitor at the ADC input (section 50.6), an RC filter (section 62.7), a separate ground, powering the sensor from the reference. A software filter is the last stage, not a replacement for the first ones.


81.4 Three filters and one wrong one

Moving average — the mean of the last N samples. The sum is not recalculated from scratch: the new sample is added to it and the one leaving the window is subtracted.

#define MA_N 16

typedef struct {
    uint16_t buf[MA_N];
    uint32_t sum;
    uint8_t pos;
    uint8_t filled;
} ma_t;

uint16_t ma_update(ma_t *f, uint16_t x)
{
    f->sum += x;
    f->sum -= f->buf[f->pos];   /* уходит самый старый — сумма не пересчитывается */
    f->buf[f->pos] = x;
    f->pos = (uint8_t)((f->pos + 1) % MA_N);
    if (f->filled < MA_N)
        f->filled++;
    return (uint16_t)(f->sum / f->filled);
}

Exponential smoothing — each new sample moves the result a fraction of the way towards itself: y ← y + (x − y) / 8. Memory — a single number.

#define EMA_K 3                 /* коэффициент 1/8 */

/* Неверно: результат хранится без дробной части, деление отбрасывает
 * остаток — показание застревает, не дойдя до входа. */
uint16_t ema_naive(uint16_t *y, uint16_t x)
{
    *y = (uint16_t)(*y + ((int32_t)x - *y) / (1 << EMA_K));
    return *y;
}

/* Верно: состояние хранится умноженным на 2^K — дробная часть не теряется. */
uint16_t ema_update(uint32_t *acc, uint16_t x)
{
    *acc += x - (*acc >> EMA_K);
    return (uint16_t)(*acc >> EMA_K);
}

It is this filter that examples often call a "running average" — but it is not a window average, it is smoothing: it has no buffer and no window, and the contribution of old samples fades out gradually.

Median of three — the middle value of the last three samples.

uint16_t med3(uint16_t a, uint16_t b, uint16_t c)
{
    uint16_t t;
    if (a > b) { t = a; a = b; b = t; }
    if (b > c) { t = b; b = c; c = t; }
    return a > b ? a : b;
}

What the test showed

Step — the input jumps from 0 to 1000:

Filter To 95 % Final value
average of 16 16 samples (and straight to 100 %) 1000
smoothing 1/8 23 samples 1000
smoothing 1/8, naive 23 samples 993 — and not a step further

A step from 0 to 1000 through three filters: the average gets there in 16 samples, smoothing in 23, the naive version gets stuck at 993

The naive variant looks the same until you look at the last digits: as soon as less than eight codes remain to the target, (x − y) / 8 is zero, and the reading gets stuck. A shortfall of 7 codes is 5.6 mV, an error that never goes away. The figure 23 agrees with the formula: (7/8)^k ≤ 0.05 at k = 23.

Noise — uniform ±50 codes around 2000, five thousand samples:

Standard deviation Improvement
no filter 29.18 —
average of 16 7.50 3.9 times (theory √16 = 4)
smoothing 1/8 7.65 3.8 times (theory √15 ≈ 3.87)

Single outlier — one sample of 4095 among values of 2000:

Filter Largest deviation
average of 16 130 — and it stays for 16 samples
smoothing 1/8 261 — and it decays
median of three 0

How to choose

Average of 16 Smoothing 1/8 Median of three
memory 40 bytes 4 bytes two previous samples
against noise good good weak
against outliers smears them smears them removes them
delay exactly N samples smooth, 95 % in 23 one sample
division yes: on M3 — udiv, on M0 — a function call no, only shifts no

A common combination is a median of three followed by smoothing: the median removes single outliers, the smoothing removes noise. The smoothing coefficient is taken as a power of two: then division turns into a shift, and on Cortex-M0, which has no divide instruction (section 75.4), the filter stays cheap.

A filter always means delay. Smoothing 1/8 with sampling every 10 ms reaches 95 % in 230 ms — unnoticeable for a thermometer, but for a controller it may turn out to be too slow (section 47).


81.5 Reading a sensor without stopping the program

The example "add up 30 samples in a loop and divide" works, but stops the program for the whole duration of the measurement. The filters above are built differently: each call is one sample, and the result is always ready. The samples come from the ADC interrupt or via DMA (sections 9 and 14), and the program takes the current filtered value when it needs it.

One more consequence of the filter receiving a sample rather than reading the ADC itself: it can be checked on a computer, just as the functions of this section were checked (section 79.5).


81.6 Pitfalls

The readings jitter in the last digit. That is normal: the step of a 12-bit ADC is 0.8 mV. Hardware first — capacitor, ground, reference — then the filter.

The potentiometer knob does not reach the maximum. map() gives the extreme value one code out of 1024.

The smoothed value falls short of the input by a few units. The exponential smoothing stores the result without a fractional part.

After the filter the spikes are still there, only wider. The average and the smoothing smear an outlier; you need a median.

The controller started oscillating after a filter was added. The filter added delay.

The sensor reading "drifts" with the supply voltage. The sensor is not powered from the ADC reference.

The program freezes for the duration of the measurement. Averaging in a loop instead of a filter that works one sample at a time.

Scaling gives garbage on AVR and is correct on STM32. The intermediate product did not fit into a 16-bit int.


81.7 Summary

Next: section 81а "Digital filters: FIR, IIR and CMSIS-DSP" — filters with a specified passband and ready-made library functions.

Open in the interactive handbook