How to Run a TinyML Model on an STM32 Microcontroller

Most TinyML tutorials assume you’re working with an ESP32 or a Raspberry Pi. If you’re an STM32 developer, you’ve probably pieced together fragments from ST’s 200-page user manual, a couple of outdated YouTube walkthroughs, and forum posts that reference X-CUBE-AI version 5. Meanwhile, the toolchain has moved to v9.x, ST has shipped new Cortex-M33 parts with serious ML headroom, and the actual deployment workflow is now surprisingly straightforward once someone shows you the path end to end.
That’s what this article does. You’ll take a pre-trained TensorFlow/Keras model, convert it with STM32Cube.AI, and run inference on an STM32U5 Nucleo board. The whole process takes an afternoon. No ML expertise required, just your normal STM32 tooling and a willingness to flash something unusual into that microcontroller.
We’re building the “hello world” of TinyML: a sine-wave regression model. It’s small enough to focus entirely on the deployment mechanics without getting tangled in data pipelines or model architecture debates. Once this works, you can swap in keyword spotting, anomaly detection, or any model that fits your Flash and SRAM budget.
┌──────────┐ ┌───────────┐ ┌─────────────┐ ┌─────────────┐ ┌──────────┐
│ Train │───▶│ Quantize │───▶│ STM32Cube.AI│───▶│ Integrate │───▶│ Flash │
│ (Keras) │ │ (TFLite) │ │ (Analyze & │ │ (C code in │ │ & Run │
│ │ │ │ │ Convert) │ │ CubeIDE) │ │ │
└──────────┘ └───────────┘ └─────────────┘ └─────────────┘ └──────────┘
Python Python CubeIDE GUI C / HAL ST-LinkWhat You Need Before You Start
Hardware: This tutorial targets the NUCLEO-U575ZI-Q. It’s an STM32U5 Cortex-M33 running at 160 MHz with 2 MB Flash and 786 KB SRAM, plenty of room for real models, not just toys. It costs roughly $25 and is widely stocked. If you have a NUCLEO-H743ZI2 (Cortex-M7) or even an older STM32F4 Discovery board, the workflow is identical. You’ll just have different memory constraints.
Software: Install these before proceeding.
+---------------------------+----------------------------------+
| Component | Version / Part |
+---------------------------+----------------------------------+
| Dev Board | NUCLEO-U575ZI-Q |
| MCU Core | Cortex-M33 @ 160 MHz |
| Flash / SRAM | 2 MB / 786 KB |
| IDE | STM32CubeIDE 1.15+ |
| AI Expansion | X-CUBE-AI 9.x |
| Model Framework | TensorFlow/Keras → TFLite int8 |
+---------------------------+----------------------------------+You’ll also need Python 3.10+ with TensorFlow 2.x installed (pip install tensorflow) and STM32CubeProgrammer. A companion GitHub repository (linked at the end) contains the pre-trained .tflite model and the finished project if you want to skip ahead.
Step 1 — Train and Export a TinyML Model
You don’t need to be an ML engineer for this part. Here’s the entire training script:
import numpy as np
import tensorflow as tf
# Generate training data: sin(x) for x in [0, 2π]
x_train = np.random.uniform(0, 2 * np.pi, 1000).astype(np.float32)
y_train = np.sin(x_train).astype(np.float32)
# Build a tiny dense network
model = tf.keras.Sequential([
tf.keras.layers.Dense(16, activation='relu', input_shape=(1,)),
tf.keras.layers.Dense(16, activation='relu'),
tf.keras.layers.Dense(1)
])
model.compile(optimizer='adam', loss='mse')
model.fit(x_train, y_train, epochs=500, batch_size=64, verbose=0)
# Quantize to int8 TFLite
converter = tf.lite.TFLiteConverter.from_keras_model(model)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
converter.representative_dataset = lambda: (
[np.array([[x]], dtype=np.float32)] for x in x_train[:100]
)
converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
converter.inference_input_type = tf.int8
converter.inference_output_type = tf.int8
tflite_model = converter.convert()
with open('sine_model.tflite', 'wb') as f:
f.write(tflite_model)
print(f"Model size: {len(tflite_model)} bytes")This produces sine_model.tflite at roughly 2 KB. The int8 quantization is critical: STM32Cube.AI’s Cortex-M33 kernels are heavily optimized for 8-bit operations and will outperform float32 inference by a wide margin.
Important: STM32Cube.AI accepts .tflite, .onnx, and Keras .h5 files. If you already have a model in one of those formats, skip this step entirely and bring your own.
Step 2 — Set Up STM32CubeIDE with X-CUBE-AI
Create a new STM32 project. Open STM32CubeIDE → File → New → STM32 Project. In the board selector, search for
NUCLEO-U575ZI-Qand select it. Accept the default peripheral initialization when prompted.Install the X-CUBE-AI expansion pack. In the CubeMX perspective (the
.ioceditor), go to Software Packs → Manage Software Packs. Under STMicroelectronics, find X-CUBE-AI and install version 9.x. This downloads ST’s optimized inference runtime and the code generator.Enable X-CUBE-AI in your project. Go to Software Packs → Select Components. Check the X-CUBE-AI box and select the Application → Validation template. This generates a built-in test harness you’ll use later to verify correctness. You can switch to a “bare” ApplicationTemplate once you’re confident.
Import your model. In the CubeMX config under the X-CUBE-AI middleware section, click Add Network. Load
sine_model.tflite, name the networksine, and click Analyze.
The analyzer runs in seconds and shows exactly what you need to know:
Analyze Report (example)
─────────────────────────────────
Model: sine_model.tflite
Type: TFLite (int8 quantized)
Flash usage: 2.14 KB / 2,048 KB ✔
RAM usage: 0.58 KB / 786 KB ✔
MACs: 832
Complexity: < 0.01 ms @ 160 MHz (est.)
─────────────────────────────────This is where STM32Cube.AI earns its keep over a generic TensorFlow Lite Micro port. The analyzer tells you, before you compile a single line of C, whether your model fits. If it doesn’t, you know immediately to shrink the model or choose a larger MCU. No guessing, no linker errors halfway through integration.
- Generate code. Click Generate Code (the gear icon). CubeIDE creates the optimized C implementation of your network.
Step 3 — Integrate Inference into Your Firmware
After code generation, your project tree looks like this:
Project/
├── Core/
│ ├── Inc/
│ └── Src/
│ └── main.c ← your inference call goes here
├── X-CUBE-AI/
│ ├── App/
│ │ ├── sine.c ← network implementation
│ │ ├── sine.h
│ │ ├── sine_data.c ← weights & biases
│ │ └── sine_data.h
│ └── Lib/
│ └── libai_runtime.a ← optimized inference engine
└── sine_model.tflite ← original model (reference)sine.c and sine_data.c are generated; don’t edit them. The libai_runtime.a library contains ST’s hand-optimized Cortex-M33 kernels with DSP intrinsics. This is why STM32Cube.AI outperforms a vanilla TFLite Micro build: ST compiles operator kernels that exploit the specific instruction set of your target core.
Open main.c and add the following inference code. Drop it into the /* USER CODE BEGIN 2 */ section (after peripheral init, before the main loop):
/* Includes */
#include "sine.h"
#include "sine_data.h"
#include <stdio.h>
#include <math.h>
/* In main(), after MX_X_CUBE_AI_Init(): */
/* Allocate input/output buffers */
AI_ALIGNED(4) ai_i8 in_data[AI_SINE_IN_1_SIZE];
AI_ALIGNED(4) ai_i8 out_data[AI_SINE_OUT_1_SIZE];
ai_buffer *ai_input = ai_sine_inputs_get(NULL, NULL);
ai_buffer *ai_output = ai_sine_outputs_get(NULL, NULL);
ai_input[0].data = AI_HANDLE_PTR(in_data);
ai_output[0].data = AI_HANDLE_PTR(out_data);
/* Run inference for x = 0.0 to 6.28 in 0.1 steps */
for (float x = 0.0f; x < 6.28f; x += 0.1f)
{
/* Quantize float input to int8 */
ai_i8 x_quantized = (ai_i8)((x / ai_sine_inputs_get_scale(0))
+ ai_sine_inputs_get_zeropoint(0));
in_data[0] = x_quantized;
/* Run the network */
ai_sine_run(ai_input, ai_output);
/* Dequantize int8 output to float */
float y_pred = (out_data[0] - ai_sine_outputs_get_zeropoint(0))
* ai_sine_outputs_get_scale(0);
printf("x=%.2f predicted=%.4f actual=%.4f\r\n", x, y_pred, sinf(x));
}A few things to note:
- Quantization/dequantization. Since the model uses int8, you must scale your float inputs into the quantized range and scale the outputs back. The
_get_scale()and_get_zeropoint()helpers are generated for you. ai_bufferabstraction. This is how STM32Cube.AI maps data into and out of the network. For sensor data (accelerometer, microphone ADC values), you’d fillin_datafrom your DMA buffer or HAL read instead of a loop variable.- UART output. The
printfabove assumes you’ve retargetedprintfto UART2 (the default ST-Link VCP on Nucleo boards). If you need a refresher on this, the STM32 UART communication guide covers it.
Step 4 — Build, Flash, and See Results
Build. Hit Ctrl+B (or Project → Build All). You should see zero errors. If you get linker errors about missing symbols, confirm X-CUBE-AI was enabled in CubeMX and code was regenerated.
Flash. Connect the Nucleo board via USB. Run → Debug As → STM32 C/C++ Application. The integrated ST-Link flashes and halts at
main(). Hit Resume (F8).Observe. Open a serial terminal (PuTTY, minicom, or the STM32CubeIDE built-in terminal) at 115200 baud. You’ll see output like:
x=0.00 predicted=0.0012 actual=0.0000
x=0.10 predicted=0.0998 actual=0.0998
x=0.50 predicted=0.4791 actual=0.4794
x=1.57 predicted=0.9987 actual=1.0000
x=3.14 predicted=0.0021 actual=0.0016
x=6.28 predicted=-0.0009 actual=-0.0031The predictions won’t be perfect (it’s a tiny two-layer network) but they’ll track the sine wave closely.
- Use the built-in Validation mode. Because you selected the Validation application template earlier, STM32Cube.AI can run a more rigorous check. In CubeIDE, go to the X-CUBE-AI settings and click Validate on target. The tool sends hundreds of test vectors from your PC over UART, runs each through both the desktop TFLite interpreter and the MCU, and reports the maximum absolute error. This is extremely useful when you move to larger, real-world models where eyeballing output isn’t practical.
Scaling Up to Real-World Models
The sine model proves the pipeline works. Here’s what changes when you deploy something meaningful.
Model size budgeting. A practical rule of thumb: keep model weights under 50% of Flash and activation buffers under 70% of SRAM. That leaves headroom for your actual firmware, peripheral drivers, and RTOS if you use one. On the NUCLEO-U575ZI-Q, that means roughly 1 MB for model weights and 550 KB for activations, enough for a serious keyword-spotting or vibration-anomaly model.
Quantization is non-negotiable. Always deploy int8 quantized models on Cortex-M. STM32Cube.AI’s runtime is optimized for 8-bit multiply-accumulate operations. A float32 model that barely fits will also run several times slower.
Proven use cases on STM32: keyword spotting from a MEMS microphone, predictive maintenance from accelerometer data, gesture recognition from an IMU, simple image classification from a low-res camera module. ST publishes pre-optimized reference models for all of these in the STM32 Model Zoo on GitHub. Download, analyze, and deploy using the exact same steps above.
Benchmark without hardware. ST offers the STEdgeAI Developer Cloud, a free online tool where you upload a model and get Flash/RAM/latency estimates for dozens of STM32 targets. Useful when you’re choosing which MCU to design into a product.
From Tutorial to Product
You now have a working pipeline from Keras to STM32 inference in C. The sine model is trivial by design; the workflow is identical whether the model is 2 KB or 500 KB. Swap sine_model.tflite for a keyword-spotting model from ST’s Model Zoo, re-run Analyze, check that it fits, generate code, and flash. Same steps. Same tools.
The fact that STM32Cube.AI generates optimized C targeting your specific Cortex-M core, rather than interpreting a generic flatbuffer at runtime like TFLite Micro, means you’re getting performance you can’t match with a framework port. For production deployments, that difference translates directly into a smaller MCU (lower BOM cost) or a more capable model (better accuracy) within the same silicon budget.
Clone the companion repository, run the steps on your own board, and break something. Then try a model that actually matters to your product.
Hubble Network connects your edge devices to the cloud via Bluetooth — from space — so the TinyML models you deploy can report back without terrestrial infrastructure. Learn more →