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

85. TinyML: machine learning on a microcontroller

TinyML is machine learning right where the data is born: on a microcontroller next to the sensor, drawing on the order of milliwatts. The device does not send the raw signal to the cloud; it decides by itself: was the keyword spoken, which gesture was made, is the bearing wearing out. This is faster, needs no network and keeps the data on the device.

The main thing to understand: on a microcontroller the model does not learn, it only runs. You train it on a computer, then shrink it and put it into Flash as an ordinary array of numbers. This section walks the whole path on a small example — from the data to integer inference in C — and every number is verified by a build and by calculation.

Terms used in the documentation

Term Meaning
machine learning, model learning from data; the trained result
feature, feature vector a number the model decides by; a set of them
training / inference fitting the model / running it on new data
supervised / unsupervised learning with known answers / without answers
classification, anomaly detection choosing a class; finding the unusual
train / test split data for training and data held back for testing
quantization, scale, zero point storing reals as integers; step; integer for zero
multiply–accumulate (MAC) multiply and add to a running sum
keyword spotting, wake word recognizing a spoken keyword
spectrogram, MFCC spectra over time; mel-frequency cepstral coefficients
autoencoder, k-means a network that rebuilds its input; clustering around centers

85.1 How it works

Step Where What you get
data collection device, recording thousands of labeled examples
features computer and device — identically the numbers the model decides by
training computer a float model
quantization computer an int8 model, four times smaller
transfer computer → firmware an array of weights in Flash
inference microcontroller a decision in milliseconds

Six steps from recording data to inference on the microcontroller, and where each one runs

Supervised learning is when the correct answer is known for each example: "this is the word 'yes'", "this is wear". Unsupervised is when there are no answers and the model looks for patterns by itself; this is how anomalies are found.

Features can be binary (yes/no), categorical (color, type), ordinal (a rating on a scale) and quantitative (a measured value). A quantitative feature does not have to be unbounded — a temperature within its allowed limits is quantitative too; but a rating "bad — good — excellent" is written as numbers, yet adding them makes no sense: it is an order.

Four typical TinyML tasks and their data:

Task Sensor How much data
keyword microphone 16000 samples per second
gesture accelerometer hundreds of samples per second
wear, anomaly vibration sensor thousands of samples per second
object in the frame camera tens of kilobytes per frame

85.2 Features: not the raw signal but its essence

Audio for speech recognition is sampled at 16 kHz — enough for a bandwidth of up to 8 kHz. One second of such audio in int16 is 32000 bytes, more than the whole RAM of an F103C8 (20 KB, section 80.1). So the signal is not stored whole; it is cut into windows of 30 ms with a step of 20 ms, and each window is immediately turned into features:

One second of audio is 49 windows of 13 coefficients, 637 features instead of 16000 samples. Whether the word is said loudly or quietly, the features look alike — that is why they are computed.

The rule that projects break on: features on the device must be computed exactly as during training — the same windows, the same constants, the same scale. A model trained on one kind of features that receives another answers with garbage, and no accuracy on the computer will show it. Simple features — mean, moving average, peak-to-peak range — are computed by the filters from section 81.


85.3 The model in memory: why int8

A small network for keywords: 637 features → 32 neurons → 3 answers. It has 20515 parameters:

Storage Size F103C8 (Flash 64 KB)
float, 4 bytes 82060 bytes does not fit
int8, int32 biases 20620 bytes fits

A network of 20515 parameters: in float it does not fit into 64 KB of Flash, in int8 it takes a third

"Fits" here refers to the weights. The firmware needs two more things. Code that runs the model: your own loop, as in 85.5, is hundreds of bytes, while TensorFlow Lite for Microcontrollers, even with a single FullyConnected operation, takes about 21 KB of Flash (armclang -Os, Cortex-M3). Together with the weights that is 41 KB out of 64 — about a third is left for the rest of the firmware. And RAM for intermediate results: for this network that is the 637-byte input, 32 bytes of the first layer and 3 answers, plus the audio window buffers. Libraries call this area the working memory (tensor arena) and require you to allocate it in advance as one array; how much a particular model needs is estimated by STM32Cube.AI (85.8).

Quantization replaces each real number with an integer from −128 to 127: x ≈ scale · (q − zero point).

/* ── квантование: x ≈ scale · (q − zero) ── */
int8_t quantize(float x, float scale, int zero)
{
    long q = lroundf(x / scale) + zero;
    if (q > 127)
        q = 127;
    if (q < -128)
        q = -128;
    return (int8_t)q;
}

The error is no more than half a step: in the check with a step of 1/127 on the interval from −1 to 1 it came to 0.003937, exactly 1/254. This usually hardly hurts the model — below you can see by how much.


85.4 The cost of inference and what Cortex-M computes it with

One inference of this network is 20480 multiply–accumulates, one for each weight. How the core executes them is visible in the machine code:

Core Instructions per multiply–accumulate
Cortex-M0 two: muls and adds
Cortex-M3 one: mla
Cortex-M4, compiler code one: smlabb (a DSP instruction, but one multiply at a time)
Cortex-M4, CMSIS-NN library smlad — two multiplies per instruction, sxtb16 unpacks the bytes

The compiler did not insert the packed instruction by itself: a plain loop on the M4 is smlabb, one multiply per element. The CMSIS-NN kernel for the M4 has 42 smlad instructions and as many sxtb16, while the same kernel for the M3 builds with mla only. That is why on an F407 the neural network is computed with the library, not with your own loop.

Floating-point arithmetic is the same story: the square root in the anomaly detector from 85.6 on an M4 with an FPU is a single vsqrt.f32 instruction; on an M3 it is a call to a library function (section 69).


85.5 The whole path on a small example

The task: tell a healthy bearing from a worn one by vibration. Two features per window: the RMS value of the vibration and the share of energy in the upper frequency band. On the computer there are 2000 examples: 1500 for training, 500 for testing, which the model has never seen.

Training. Logistic regression: a weighted sum of the features plus a bias; the decision is made by the sign. Gradient descent in Python gave weights of 2.392 and 4.215 and a bias of 0.781.

Quantization. Features and weights go to int8, each with its own scale; the bias goes to int32 in units of "feature scale × weight scale". The quantization here is symmetric: the zero point of the features and weights is zero, so dot_s8 simply multiplies the codes. With a non-zero zero point, you first subtract it from each code (or fold a correction into the bias in advance), otherwise the sum drifts. The weights became the integers 72 and 127, the bias 1015. The script writes them into a header — this is how the model gets into the firmware, as an array in Flash:

#define ML_N 2
static const int8_t ML_W[ML_N] = {72, 127};
#define ML_B (1015)

Inference on the microcontroller — entirely in integers:

/* ── скалярное произведение int8 с накоплением в int32 ── */
int32_t dot_s8(const int8_t *a, const int8_t *b, int n)
{
    int32_t acc = 0;
    for (int i = 0; i < n; i++)
        acc += (int32_t)a[i] * b[i];
    return acc;
}

/* ── классификатор: знак w·x + b целиком в целых числах ── */
int ml_classify_q(const int8_t *xq)
{
    return dot_s8(ML_W, xq, ML_N) + ML_B > 0;
}

Result on 500 test examples:

Model Correct
float on the computer 494 of 500
int8 in C 494 of 500
float and int8 answers matching 500 of 500

Quantization did not cost a single correct answer. The int8 inference in C gave exactly the same result as the integer calculation in Python — this is the proof that the model was transferred without errors. The code is built with armclang for Cortex-M0, M3 and M4 without warnings.


85.6 Anomalies: learn only what is normal

Wear, leaks and breakdowns share a difficulty: there are few examples of faults, and thousands of ways to break. You can collect data on "how a machine breaks" only by breaking machines. The way out is to teach the model only what is normal and treat everything that does not look like it as an anomaly.

The simplest option needs no neural network: compute the mean and spread of the signal on the fly and raise an alarm when a sample moves away from the mean by more than a set number of sigmas. Welford's algorithm does this in a single pass, without storing the samples:

typedef struct {
    uint32_t n;
    float mean;
    float m2;       /* сумма квадратов отклонений от среднего */
} welford_t;

void welford_add(welford_t *w, float x)
{
    w->n++;
    float d = x - w->mean;
    w->mean += d / (float)w->n;
    w->m2 += d * (x - w->mean);
}

/* На сколько сигм x отстоит от накопленной нормы */
float welford_z(const welford_t *w, float x)
{
    if (w->n < 2)
        return 0.0f;
    float var = w->m2 / (float)(w->n - 1);
    return var > 0.0f ? (x - w->mean) / sqrtf(var) : 0.0f;
}

In the check, the detector learned on 1000 samples of normal data with a 4σ threshold: false alarms — zero, and a 6σ shift of the mean was caught on the very first sample. The state is 12 bytes.

For multidimensional data the same principle is taken further: k-means remembers the centers of groups of normal examples and measures the distance to the nearest one; an autoencoder — a neural network trained to rebuild the normal signal through a narrow bottleneck — rebuilds anomalies poorly, and a large reconstruction error raises the flag. The key point: you stop training the detector as soon as it has started working, otherwise it gradually accepts the anomaly as normal.


85.7 Images: why not a sliding window

The most direct way to find an object in a frame is to slide a window over it and ask the model about each position. A 48×48 window with a step of 16 over a 160×120 frame is 40 positions; at 100 ms per inference that is 4 seconds per frame. So on a microcontroller you use a low resolution and networks that in a single pass give both the class and the location of the object, or first pick out "interesting" regions with a simple algorithm and classify only those.


85.8 Tools


85.9 Responsibility

A model always makes mistakes — the question is how often and what it costs. A false alarm about an engine failure means stopping the line; a missed one means an accident. The threshold is chosen by the cost of an error, and accuracy is measured on data from real conditions: with noise, echo, other voices. A sensor that is always listening or watching collects data about people: protect it like any other data (section 37.4), or better not store it at all — the decision is made on the spot and only the result leaves the device.


85.10 Pitfalls

In the lab the model recognizes the word, outdoors it does not. The data was collected in silence; you need noise, echo, different voices and distances.

Accuracy on the computer is 99%, on the device it is random. The features on the device are computed differently than during training.

The test showed 100%. It was tested on the same data the model was trained on.

The model does not fit into Flash. The weights are in float; after quantization to int8 they are four times smaller.

After quantization the answers "drifted". The scale was not chosen by the range of the data, and large values hit ±127.

Inference takes seconds. Your own loop instead of CMSIS-NN on the M4, a network too big for the chip, a sliding window over the frame.

Floating-point calculations are slow. The M0 and M3 have no FPU — everything goes through the library.

The anomaly detector stopped firing. It kept learning and accepted the fault as normal.


85.11 Summary

Next: section 86 "Network types and core technologies".

Open in the interactive handbook