119б. The model on the board: TensorFlow Lite Micro from the inside — MicroInterpreter, op resolver, tensor arena, input buffer and the sample stream
In section 118а a network learned to recognise five gestures from a window of
a three-axis accelerometer; section 119 showed how a model is converted to int8
and checked against a reference bit for bit. This is the last step: the .tflite
file goes into the firmware and runs under TensorFlow Lite Micro (TFLM). The
section takes the deployment apart: the model as an array in flash, the set of
operator kernels, the interpreter and its working memory — the tensor arena —
int8 input and output, the sample stream from DMA to the window, time per
operator, errors, and building the library without make.
The model is network C from section 118а (1317 parameters, 23 408
multiply-accumulates per window), trained with the same seed on the same data;
in int8 it is 9424 bytes, and in int8 it is right on 475 of 480 held-out windows, as in
float. The library is the Keil pack tensorflow-lite-micro 1.23.2 with the
flatbuffers 2.0.6, gemmlowp, ruy and CMSIS-NN 4.1.0 packs. The Cortex-M4F
firmware was built with xPack arm-none-eabi-gcc 14.2.1 and run in QEMU
netduinoplus2 (STM32F405 — the same core as the F407); the same code was built
with g++ 12 on the computer and compared with the reference TensorFlow Lite
interpreter from Python. Quotes from today's TFLM come from a clone of the
repository (commit 85c6999 of 3 November 2026); paths are relative to
tensorflow/lite/micro/.
Background: the tensor arena and where TinyML fits — section 85; the model and its operators — 118а; quantization, scale and zero point — 119; DMA in circular mode and half-buffer interrupts — section 9; a sliding-window stream for audio — 121б.6.
Terms used in the documentation
Term Meaning MicroInterpreter the interpreter: walks the graph and calls the kernels op resolver, MicroMutableOpResolver the set of operator kernels the programmer assembles AllOpsResolver a set with every kernel of the library tensor arena one array for all of the model's working memory persistent / non-persistent (tail / head) the permanent part of the arena and the part the planner shares between tensors AllocateTensors laying out the arena: who lives where and when schema version the version of the .tflitefile schemaflatbuffer the model file format: read in place, without parsing Invoke one run of the model MicroProfiler a log of time per operator DebugLog, MicroPrintf library message output — supplied by the platform half transfer / transfer complete (HT/TC) DMA interrupts: half / all of the buffer is filled ring buffer a circular buffer
119б.1 The path of a sample and the memory map
The deployment has two halves. The fixed half lives in flash: the model as a
byte array and the library code — interpreter and kernels. The changing half
lives in RAM: the tensor arena with every intermediate result, and the stream
buffers. The interpreter allocates nothing by itself — no malloc, no heap
new: everything it needs comes from the arena, and the programmer provides
the arena.
The model goes into the firmware as an array. xxd -i model.tflite does
that; here train119c.py writes the array in the same form and adds two things:
alignas(16) static uint8_t tensor_arena[ARENA_SIZE] __attribute__((section(ARENA_SECTION)));
that is for the arena; the model in model119c_data.cc gets
alignas(16) __attribute__((section(".rodata.model"))) and const. const
keeps the array in flash instead of copying it to RAM at start-up. Its own
section lets the linker script place the model separately, for example at the
end of flash, and replace it without touching the code. The 16-byte alignment
is needed because a flatbuffer is read in place: numbers inside the file are
aligned relative to its start, and the kernels read weights as words. In the
firmware the model landed at 0x0800d1d0 — a multiple of 16.
The model does not need parsing — GetModel only casts the pointer. Checking
it is your job:
const tflite::Model *model = tflite::GetModel(g_model);
if (model->version() != TFLITE_SCHEMA_VERSION) {
The constant is 3 both in pack 1.23.2 and in today's TFLM
(micro_interpreter.h:40: #define TFLITE_SCHEMA_VERSION (3)). Experiment:
the version field in a copy of the model was rewritten to 4 — and
AllocateTensors still returned success. The library does not look at the
version; the check in the firmware is the only one.
119б.2 The kernel set: only the operators you need
The interpreter knows the graph but not how to compute it: kernels are handed
to it as a set. MicroMutableOpResolver<N> is a set for N operators that holds
exactly those found in the model. The model has eight nodes and five distinct
operators — the converter prints the list (section 118а.6):
using Resolver = tflite::MicroMutableOpResolver<5>;
TF_LITE_ENSURE_STATUS(r.AddReshape());
TF_LITE_ENSURE_STATUS(r.AddConv2D());
TF_LITE_ENSURE_STATUS(r.AddDepthwiseConv2D());
TF_LITE_ENSURE_STATUS(r.AddFullyConnected());
plus AddAveragePool2D (why not AddMean — see 119б.4). The set with every
kernel, AllOpsResolver, is handy for a first try, but then the linker cannot
drop a single kernel. With -Os for Cortex-M4F the library with five kernels
and CMSIS-NN took 56 791 bytes of flash; with AllOpsResolver, 242 903 bytes —
four times more. Today's TFLM no longer has AllOpsResolver at all — only a
hand-assembled set.
If a kernel is missing, AllocateTensors refuses and says which one:
Didn't find op for builtin opcode 'AVERAGE_POOL_2D' version '2'. An older version of this builtin might be supported. Are you using an old TFLite binary with a newer model?
Failed to get registration from op code AVERAGE_POOL_2D
"Version" here is the operator version, not the schema: the converter sets it by which kernel features the model needs, and an old library may not know a newer version of the same operator.
119б.3 The tensor arena: how much and where
The arena is one array. AllocateTensors lays out permanent data at one end
(tensor descriptors, kernel parameters, CMSIS-NN buffers) and, at the other end,
tensors that do not live for the whole run: the planner puts tensors that are
never needed at the same time into the same place. arena_used_bytes() tells
how much is used (micro_interpreter.h:148:
size_t arena_used_bytes() const { return allocator_.used_bytes(); }).
On Cortex-M4F with CMSIS-NN kernels 3140 bytes are used: 1056 for tensors
(head) and 2180 for the permanent part (tail), as RecordingMicroAllocator
breaks it down. But the smallest arena for which AllocateTensors succeeds is
3312 bytes: the layout needs temporary room that is given back afterwards. With
reference kernels it is 2932 bytes and 2960 bytes: CMSIS-NN asks for scratch
buffers for the convolutions. On the computer the same model takes 3744 bytes —
pointers are 8 bytes there — so the arena is sized on the architecture where it
will run.
How to size it: start generous (16 KB here), run on the board, print
arena_used_bytes(), then shrink while AllocateTensors still succeeds, and
keep a margin for new versions of the library and the model. An arena 16 bytes
short of the minimum was rejected with Too many buffers (max is 12) and an
error code. An arena half the size is worse: in pack 1.23.2 the layout got as
far as a temporary tensor, printed
Failed to allocate temp memory. Requested: 64, available 16, missing: 48
and fell into abort() — the program on the computer ended with code 134. So
the size is searched upwards in steps, not by bisection.
The start of the arena is aligned to 16 (micro_arena_constants.h:25:
constexpr int MicroArenaBufferAlignment() { return 16; }); an array without
alignas(16) loses up to 15 bytes. Placement: ordinary SRAM. The F407's CCM is
fast and not reachable by DMA (section 2), and DMA never touches the arena, so
on the board it can move there and free SRAM for the transfer buffers. QEMU 7.2
has no CCM for netduinoplus2: firmware with the arena at 0x10000000 faulted on
the first access — on a real board this was not checked. External SDRAM is only
for large models: it is slower, and the arena is touched by every operator.
119б.4 Input, output and the comparison with Python
Take the input scale and zero point from the tensor; do not copy them by hand:
const float s = in->params.scale;
const int zp = in->params.zero_point;
float g = (float)x[a] / 1000.0f; // mg → g, как при обучении
int v = (int)floorf(g / s + 0.5f) + zp;
*q++ = (int8_t)(v < -128 ? -128 : v > 127 ? 127 : v);
The input is an int8 tensor 1 × 64 × 3, 192 bytes, zero point −42; the output is 1 × 5, zero point −6. The class only needs the index of the largest output; there is no need to convert it back to a real number.
The comparison covers all 480 held-out windows; int8 outputs are compared with the reference TensorFlow Lite 2.21 interpreter (not XNNPACK) number by number:
| Model and build | Windows bit for bit | Largest difference | Class agrees |
|---|---|---|---|
| AVERAGE_POOL_2D, TFLM default | 478 of 480 | 1 | 480 |
| same, TFLITE_SINGLE_ROUNDING | 231 of 480 | 3 | 480 |
| MEAN (as in 118а), default | 355 of 480 | 1 | 480 |
| Dense with a scale per output | 0 of 480 | 13 | 479 |
Three conclusions. First: reference kernels and CMSIS-NN produced the same outputs, and the firmware in QEMU produced the same outputs as the program on the computer — the hash of the outputs on all 95 stream windows matched. So the reference for the device is the same TFLM built on the computer, and the comparison with it can be bit for bit.
Second: desktop TensorFlow and TFLM 1.23.2 are different implementations.
Keras global average pooling becomes the MEAN operator, and their kernels
compute it differently: 355 windows of 480. The same network with the average
taken by an AveragePooling2D layer over the whole window is no different in
float (less than 2·10⁻⁶ apart), while the AVERAGE_POOL_2D operator agrees in
478 windows. That is the network deployed here. The TFLITE_SINGLE_ROUNDING
switch only hurts here — which rounding matches the reference is decided by the
kernels and found out by comparing.
The third is the most dangerous. By default TensorFlow 2.21 gives a Dense layer
a scale for every output, and TFLM 1.23.2 does not know such a FULLY_CONNECTED
and uses one scale for all — without a single warning. Outputs differ by up to
13 levels while the model still "almost works": 474 right instead of 475.
Today's TFLM handles a scale per output in this kernel
(kernels/fully_connected_common.cc:75:
is_per_channel = affine_quantization->scale->size > 1;); for the old library
set _experimental_disable_per_channel_quantization_for_dense_layers = True in
the converter.
119б.5 The stream: DMA, ring and overlapping windows
The window is 64 samples (1.28 s at 50 Hz) and the hop is 32: each window half overlaps the previous one, and a gesture that falls on the boundary is seen whole in the next window. DMA in circular mode writes samples into a buffer of two halves of 32 samples: while the CPU takes one half, DMA writes the other — that is double buffering. The half-transfer (HT) or transfer-complete (TC) interrupt only moves the finished half into a 128-sample ring buffer:
extern "C" void dma_half_done(int half)
memcpy(&ring[((h + i) & (RING - 1)) * AXES], &src[i * AXES], AXES * sizeof(int16_t));
head = h + HOP;
The model is not run in the interrupt: Invoke takes hundreds of thousands of
instructions (119б.6), and all that time interrupts of the same and lower
priority would wait — UART, timers, the next DMA block itself. The main loop
(or an RTOS task) watches the head counter and, when a new window is ready,
assembles it from the ring, quantizes it and calls Invoke. Only the interrupt
writes the counter and only the main loop reads it, so one volatile 32-bit
word is enough, without disabling interrupts.
If the main loop falls so far behind that the interrupt is already overwriting the start of the window, the window is skipped whole:
if (h - next_end > RING - WIN - HOP) {
uint32_t lag = (h - next_end) / HOP;
st.dropped += (int)lag;
A gap is better than a window made of two different moments. In the experiment the stream is 48 held-out windows in a row — 3072 samples, 95 windows: polling after every block, all 95 were computed with no drops, and the outputs of 93 of 95 matched Python bit for bit. When the main loop looked only after every third block, it managed 32 windows of 95 and skipped 63 — and counted them honestly. In QEMU the "DMA interrupt" is real: the DMA2_Stream0 handler is called through the NVIC, the program sets it pending after every block.
119б.6 Time per operator and the build
MicroProfiler gives the time of one Invoke and of each operator: the
interpreter marks the start and end of every node, and the clock is the platform
function GetCurrentTimeTicks(). On silicon that is the DWT->CYCCNT cycle
counter; in QEMU with -icount time follows the instruction count and SysTick
counts the same (calibration: 2 000 000 instructions — 336 002 SysTick counts).
Instructions per Invoke:
| Build | Instructions | CONV_2D | DEPTHWISE_CONV_2D |
|---|---|---|---|
| CMSIS-NN, -O2 | 191 142 | 54 % | 43 % |
| CMSIS-NN, -Os | 228 647 | 55 % | 42 % |
| reference kernels, -O2 | 2 047 791 | 78 % | 21 % |
So with CMSIS-NN kernels it is 191 142 instructions (228 647 with -Os) — about 8 per multiply-accumulate; reference kernels need 2 047 791 instructions, 10.7 times more. By operator (CMSIS-NN, -O2): CONV_2D — 54 %, DEPTHWISE_CONV_2D — 43 %, the rest is the average, Reshape and Dense. These are instructions, not cycles: the board needs more cycles. But the window hop is 0.64 s, which is 107 million cycles at 168 MHz, so the margin is orders of magnitude.
The profiler log is an array inside the object: in pack 1.23.2 it holds 1024
events, and sizeof(MicroProfiler) on M4 is 20 488 bytes — more than the
arena. Today's TFLM has 4096 events (micro_profiler.h:73:
static constexpr int kMaxEvents = 4096;), and when it overflows it does not
start over as 1.23.2 does but stops the program. So call ClearEvents() before
every Invoke, and leave the profiler out of release firmware. MicroInterpreter
itself is 176 bytes.
Library messages go through MicroPrintf to the DebugLog function supplied by
the platform: semihosting here on M4, UART or SWO on a board (section 21). The
TF_LITE_STRIP_ERROR_STRINGS switch drops error texts: the -Os library shrank
to 37 870 bytes, but AllocateTensors will then fail silently.
Building without make. The Makefile of today's TFLM downloads its dependencies
itself — flatbuffers v25.9.23, kissfft, printf, and for Cortex-M CMSIS_6 and
CMSIS-NN from GitHub — and does not build offline. Keil pack 1.23.2 has
everything on disk, and there are not many files: 137 TFLM .cpp files
(kernels, interpreter, planner, flatbuffer parsing, kernels/internal) and 72
CMSIS-NN .c files. For the CMSIS-NN variant eight kernels — add, conv,
depthwise_conv, fully_connected, mul, pooling, softmax, svdf — are
taken from kernels/cmsis_nn/ instead of the reference ones of the same name:
sed -i "s#^tensorflow/lite/micro/kernels/$k.cpp\$#tensorflow/lite/micro/kernels/cmsis_nn/$k.cpp#" "$OUT/srcs.txt"
Flags: -DTF_LITE_STATIC_MEMORY (no heap), -DCMSIS_NN,
-fno-rtti -fno-exceptions -fno-threadsafe-statics,
-ffunction-sections -fdata-sections and --gc-sections at link time — without
the last pair the linker will not drop unused kernels. Test files, Ethos-U and
cortex_m_generic are not needed: the platform supplies DebugLog and the
clock itself (port_m4_119c.cc below). A C++ standard library for
arm-none-eabi is needed too: the gcc in the stm-arm:1 image lacks even <new>,
so the build used xPack GCC 14.2.1.
119б.7 Pitfalls
The model "almost works", but board outputs differ from Python. Dense has a
scale per output and TFLM is old (1.23.2 here): the kernel uses one scale
without a word. Set _experimental_disable_per_channel_quantization_for_dense_layers = True
in the converter, or use a newer library. Compare outputs, not accuracy:
474 against 475 does not reveal this error.
Outputs differ by one in hundreds of windows. An operator kernel in TFLM and in desktop TensorFlow computes differently — MEAN here, 355 of 480. The reference for the device is the same TFLM built on the computer; replacing the operator (AveragePooling2D instead of global average) gives 478 of 480.
The board dies in AllocateTensors. The arena is far too small — in TFLM
1.23.2 that is abort(), not an error. Size it on the target architecture with
arena_used_bytes() and upward probes.
The arena was sized on the computer and does not fit the board, or the other way round. Pointers are 8 bytes against 4: 3744 bytes on x86-64 against 3140 on M4.
After a few minutes the program stops. MicroProfiler is attached and
ClearEvents() is never called: in today's TFLM that is abort after 4096
events; in 1.23.2 the log silently wraps.
Someone else's model is flashed and everything "works". TFLM does not check
the schema version — the experiment with version 4 passed AllocateTensors.
Check model->version() yourself, and the operator set too.
A window made of two pieces. The main loop fell behind, the interrupt overwrote the start of the window, and the firmware computed it anyway. Keep a sample counter and skip such a window whole.
The DMA buffer in CCM. DMA cannot reach CCM (section 2). The arena may go
there, dma_buf may not.
119б.8 Summary
- The model is a
constarray in flash,alignas(16), its own section: 9424 bytes;GetModelparses nothing, the firmware checks the schema version. - Only the kernels you need:
-Os56 791 bytes against 242 903 bytes withAllOpsResolver; without error strings, 37 870 bytes. - Arena on Cortex-M4F: 3140 bytes used (1056 tensors, 2180 permanent), 3312 bytes needed; 3744 on the computer — size it on the target.
- TFLM outputs against Python: 478 of 480 bit for bit; with MEAN, 355; with a per-channel Dense on the old TFLM, 0 and up to 13 apart. The firmware in QEMU and the program on the computer matched bit for bit.
- Stream: DMA with two halves, the interrupt only copies, the 64-sample window with a 32-sample hop is computed in the main loop; falling behind means skipping whole windows, 32 windows of 95 when polling every third block.
- Invoke: 191 142 instructions with CMSIS-NN, 2 047 791 with reference kernels; the convolutions take 97 % of the time.
Full sources
app119c.cc — the application: model, op set, arena, stream through the DMA ring, comparison and the error experiments.
// app119c.cc — раздел 119б: модель жестов (раздел 118а, int8) на TensorFlow Lite Micro.
// Один исходник для компьютера (-DML_HOST, файл test119c.bin в argv[1]) и для Cortex-M4
// (QEMU netduinoplus2). Поток акселерометра идёт через кольцо DMA из двух половин,
// прерывание только перекладывает половину в кольцевой буфер, окно 64 отсчёта со сдвигом 32
// собирается и считается в главном цикле. Выходы int8 сверяются с эталонным интерпретатором
// TensorFlow Lite из Python (stream119c.h, test119c.bin).
#include <math.h>
#include <new>
#include <stdint.h>
#include <string.h>
#include "tensorflow/lite/micro/micro_interpreter.h"
#include "tensorflow/lite/micro/micro_log.h"
#include "tensorflow/lite/micro/micro_mutable_op_resolver.h"
#include "tensorflow/lite/micro/micro_profiler.h"
#include "tensorflow/lite/micro/micro_time.h"
#include "tensorflow/lite/micro/recording_micro_interpreter.h"
#include "tensorflow/lite/schema/schema_generated.h"
#ifdef ALL_OPS
#include "tensorflow/lite/micro/all_ops_resolver.h"
#endif
#if defined(MODEL_MEAN) // та же сеть с оператором MEAN — опыт раздела 119б.7
#include "model119c_mean_data.h"
#define POOL_OP AddMean
#define POOL_NAME "MEAN"
#elif defined(MODEL_PC) // Dense с масштабом на каждый выход — опыт 119б.7
#include "model119c_pc_data.h"
#define POOL_OP AddAveragePool2D
#define POOL_NAME "AVERAGE_POOL_2D"
#else
#include "model119c_data.h"
#define POOL_OP AddAveragePool2D
#define POOL_NAME "AVERAGE_POOL_2D"
#endif
#include "stream119c.h"
#if defined(MODEL_MEAN)
#define S_REF S_OUT_MEAN
#elif defined(MODEL_PC)
#define S_REF S_OUT_PC
#else
#define S_REF S_OUT
#endif
#ifndef ARENA_SIZE
#define ARENA_SIZE (16 * 1024)
#endif
#ifndef ARENA_SECTION
#define ARENA_SECTION ".bss.arena"
#endif
#define RING 128 // кольцевой буфер отсчётов: 4 сдвига, степень двойки
alignas(16) static uint8_t tensor_arena[ARENA_SIZE] __attribute__((section(ARENA_SECTION)));
int g_quiet; // 1 — DebugLog молчит (подбор арены)
static tflite::MicroProfiler profiler; // события по операторам; чистить перед каждым Invoke
extern "C" void dma_half_done(int half);
void port_init(void);
void port_kick_dma(int half); // «DMA заполнил половину»: вызвать прерывание
void port_exit(int code);
// ── операторы модели: ровно те, что в model119c.tflite ──
using Resolver = tflite::MicroMutableOpResolver<5>;
static TfLiteStatus register_ops(Resolver &r, bool with_pool)
{
TF_LITE_ENSURE_STATUS(r.AddReshape());
TF_LITE_ENSURE_STATUS(r.AddConv2D());
TF_LITE_ENSURE_STATUS(r.AddDepthwiseConv2D());
if (with_pool)
TF_LITE_ENSURE_STATUS(r.POOL_OP());
TF_LITE_ENSURE_STATUS(r.AddFullyConnected());
return kTfLiteOk;
}
// ── поток: кольцо DMA из двух половин и кольцевой буфер окон ──
static int16_t dma_buf[2 * HOP * AXES]; // DMA в кольцевом режиме пишет сюда АЦП/датчик
static int16_t ring[RING * AXES];
static volatile uint32_t head; // отсчётов в кольце всего; пишет только прерывание
/* Прерывание DMA (половина HT или конец TC): переложить половину в кольцо — и всё. */
extern "C" void dma_half_done(int half)
{
const int16_t *src = &dma_buf[half * HOP * AXES];
uint32_t h = head;
for (int i = 0; i < HOP; i++)
memcpy(&ring[((h + i) & (RING - 1)) * AXES], &src[i * AXES], AXES * sizeof(int16_t));
head = h + HOP;
}
/* Окно [end − WIN, end) из кольца → входной тензор int8: масштаб и нулевая точка — из тензора. */
static void window_to_input(uint32_t end, const TfLiteTensor *in)
{
const float s = in->params.scale;
const int zp = in->params.zero_point;
int8_t *q = in->data.int8;
for (int i = 0; i < WIN; i++) {
const int16_t *x = &ring[((end - WIN + i) & (RING - 1)) * AXES];
for (int a = 0; a < AXES; a++) {
float g = (float)x[a] / 1000.0f; // mg → g, как при обучении
int v = (int)floorf(g / s + 0.5f) + zp;
*q++ = (int8_t)(v < -128 ? -128 : v > 127 ? 127 : v);
}
}
}
static int argmax(const int8_t *o)
{
int b = 0;
for (int j = 1; j < NCLS; j++)
if (o[j] > o[b])
b = j;
return b;
}
struct StreamStat {
int done, same, dropped, cls_same;
uint32_t ticks_max, hash;
};
/* Главный цикл: если в кольце есть новое окно — посчитать его. Окно, которое прерывание
* успело затереть, пропускается целиком: лучше пропуск, чем окно из двух разных кусков. */
static uint32_t next_end;
static int poll_window(tflite::MicroInterpreter &ip, StreamStat &st)
{
uint32_t h = head;
if (h < next_end)
return 0;
if (h - next_end > RING - WIN - HOP) {
uint32_t lag = (h - next_end) / HOP;
st.dropped += (int)lag;
next_end += lag * HOP;
}
window_to_input(next_end, ip.input(0));
profiler.ClearEvents(); // иначе журнал переполнится: в TFLM 1.23.2 — по кругу, в новой — abort
uint32_t t0 = tflite::GetCurrentTimeTicks();
if (ip.Invoke() != kTfLiteOk)
return -1;
uint32_t dt = tflite::GetCurrentTimeTicks() - t0;
if (dt > st.ticks_max)
st.ticks_max = dt;
const int8_t *o = ip.output(0)->data.int8;
int k = (int)((next_end - WIN) / HOP); // номер окна потока
st.same += memcmp(o, &S_REF[k * NCLS], NCLS) == 0;
st.cls_same += argmax(o) == argmax(&S_REF[k * NCLS]);
for (int j = 0; j < NCLS; j++) // FNV-1a по выходам: сверка прибора с компьютером
st.hash = (st.hash ^ (uint8_t)o[j]) * 16777619u;
st.done++;
next_end += HOP;
return 1;
}
/* Поток из stream119c.h блоками по HOP отсчётов; главный цикл заглядывает в кольцо
* после каждого блока (every = 1) или реже, будто занят чем-то ещё (every > 1). */
static StreamStat run_stream(tflite::MicroInterpreter &ip, int every)
{
StreamStat st = {0, 0, 0, 0, 0, 2166136261u};
head = 0;
next_end = WIN;
for (int b = 0; b < N_STREAM / HOP; b++) {
int half = b & 1;
memcpy(&dma_buf[half * HOP * AXES], &S_X[b * HOP * AXES], sizeof(int16_t) * HOP * AXES);
port_kick_dma(half);
if ((b + 1) % every == 0 || b == N_STREAM / HOP - 1)
if (poll_window(ip, st) < 0)
break;
}
while (poll_window(ip, st) > 0) {
}
return st;
}
/* Пробный интерпретатор для опытов: создаётся в своём буфере и не разрушается —
* деструктор MicroInterpreter после неудачного AllocateTensors в TFLM 1.23.2 падает. */
alignas(16) static uint8_t probe_buf[sizeof(tflite::MicroInterpreter)];
static TfLiteStatus try_alloc(const tflite::Model *m, const tflite::MicroOpResolver &r, size_t n)
{
tflite::MicroInterpreter *ip = new (probe_buf) tflite::MicroInterpreter(m, r, tensor_arena, n);
return ip->AllocateTensors();
}
/* Наименьшая арена, при которой AllocateTensors проходит: вверх шагами по 16 от arena_used_bytes().
* Не двоичный поиск: арена вдвое меньше нужной роняет TFLM 1.23.2 в abort(), а не в ошибку. */
static size_t min_arena(const tflite::Model *m, const tflite::MicroOpResolver &r, size_t used)
{
size_t n = used / 16 * 16;
g_quiet = 1;
while (n < ARENA_SIZE && try_alloc(m, r, n) != kTfLiteOk)
n += 16;
g_quiet = 0;
return n;
}
alignas(16) static uint8_t model_copy[MODEL_LEN]; // опыт «неверная версия схемы»
#ifdef ML_HOST
#include <stdio.h>
static int16_t T_X[N_TEST * WIN * AXES];
static uint8_t T_Y[N_TEST];
static int8_t T_OUT[N_TEST * NCLS];
#endif
int main(int argc, char **argv)
{
(void)argc;
(void)argv;
port_init();
// 1. Модель: указатель на массив во флеше, разбор не нужен — только проверка версии
const tflite::Model *model = tflite::GetModel(g_model);
if (model->version() != TFLITE_SCHEMA_VERSION) {
MicroPrintf("model schema %d, library %d", model->version(), TFLITE_SCHEMA_VERSION);
port_exit(1);
}
MicroPrintf("model_len=%d align16=%d schema=%d", (int)g_model_len, (int)((uintptr_t)g_model % 16 == 0),
(int)model->version());
MicroPrintf("model_addr=%x arena_addr=%x", (unsigned)(uintptr_t)g_model, (unsigned)(uintptr_t)tensor_arena);
// 2. Набор ядер, интерпретатор, арена
#ifdef ALL_OPS
static tflite::AllOpsResolver resolver;
#else
static Resolver resolver;
if (register_ops(resolver, true) != kTfLiteOk)
port_exit(1);
#endif
static tflite::MicroInterpreter interpreter(model, resolver, tensor_arena, ARENA_SIZE, nullptr, &profiler);
if (interpreter.AllocateTensors() != kTfLiteOk) {
MicroPrintf("AllocateTensors failed");
port_exit(1);
}
TfLiteTensor *in = interpreter.input(0), *out = interpreter.output(0);
MicroPrintf("arena_size=%d arena_used=%d", ARENA_SIZE, (int)interpreter.arena_used_bytes());
MicroPrintf("in=%s,%d,%d,%d,%d bytes=%d zp=%d", TfLiteTypeGetName(in->type), in->dims->size, in->dims->data[0],
in->dims->data[1], in->dims->data[2], (int)in->bytes, (int)in->params.zero_point);
MicroPrintf("out=%s,%d,%d,%d bytes=%d zp=%d", TfLiteTypeGetName(out->type), out->dims->size, out->dims->data[0],
out->dims->data[1], (int)out->bytes, (int)out->params.zero_point);
MicroPrintf("scale_ok=%d,%d", (int)(in->params.scale == IN_SCALE), (int)(out->params.scale == OUT_SCALE));
const tflite::SubGraph *sg = model->subgraphs()->Get(0);
MicroPrintf("ops=%d tensors=%d profiler_bytes=%d interpreter_bytes=%d", (int)sg->operators()->size(),
(int)sg->tensors()->size(), (int)sizeof(profiler), (int)sizeof(interpreter));
#ifdef ML_HOST
if (argc > 2) { // опыт: арена вдвое меньше нужной
size_t half = interpreter.arena_used_bytes() / 2 / 16 * 16;
MicroPrintf("--- arena %d", (int)half);
MicroPrintf("status=%d", (int)try_alloc(model, resolver, half));
port_exit(0);
}
#endif
// 3. Поток: 95 окон через кольцо DMA, сверка с Python
StreamStat st = run_stream(interpreter, 1);
MicroPrintf("stream windows=%d same=%d class_same=%d dropped=%d ticks_max=%u hash=%x", st.done, st.same,
st.cls_same, st.dropped, (unsigned)st.ticks_max, (unsigned)st.hash);
StreamStat sl = run_stream(interpreter, 3);
MicroPrintf("slow windows=%d same=%d dropped=%d", sl.done, sl.same, sl.dropped);
// 4. Время по операторам: одно окно, MicroProfiler
profiler.ClearEvents();
window_to_input(WIN, in);
interpreter.Invoke();
MicroPrintf("profile_total=%u", (unsigned)profiler.GetTotalTicks());
profiler.LogTicksPerTagCsv();
#ifdef ML_HOST
// 5. Все 480 отложенных окон: выходы int8 против интерпретатора TFLite
FILE *f = fopen(argv[1], "rb");
if (!f || fread(T_X, sizeof T_X, 1, f) != 1 || fread(T_Y, sizeof T_Y, 1, f) != 1 ||
fread(T_OUT, sizeof T_OUT, 1, f) != 1)
port_exit(1);
#if defined(MODEL_MEAN) || defined(MODEL_PC)
if (fread(T_OUT, sizeof T_OUT, 1, f) != 1) // вторые в файле — выходы модели с MEAN
port_exit(1);
#endif
#ifdef MODEL_PC
if (fread(T_OUT, sizeof T_OUT, 1, f) != 1) // третьи — Dense с масштабом на выход
port_exit(1);
#endif
fclose(f);
int same = 0, right = 0, maxd = 0, cls = 0;
for (int n = 0; n < N_TEST; n++) {
memcpy(ring, &T_X[n * WIN * AXES], sizeof(int16_t) * WIN * AXES);
window_to_input(WIN, in);
interpreter.Invoke();
const int8_t *o = out->data.int8;
int eq = 1;
for (int j = 0; j < NCLS; j++) {
int d = o[j] - T_OUT[n * NCLS + j];
d = d < 0 ? -d : d;
maxd = d > maxd ? d : maxd;
eq &= d == 0;
}
same += eq;
right += argmax(o) == T_Y[n];
cls += argmax(o) == argmax(&T_OUT[n * NCLS]);
}
MicroPrintf("test windows=%d same=%d class_same=%d right=%d maxdiff=%d", N_TEST, same, cls, right, maxd);
#endif
// 6. Сколько арены нужно на самом деле и куда она уходит
size_t need = min_arena(model, resolver, interpreter.arena_used_bytes());
MicroPrintf("arena_min=%d", (int)need);
{
static tflite::RecordingMicroAllocator *ra = tflite::RecordingMicroAllocator::Create(tensor_arena, ARENA_SIZE);
tflite::RecordingMicroInterpreter rip(model, resolver, ra);
if (rip.AllocateTensors() == kTfLiteOk)
rip.GetMicroAllocator().PrintAllocations();
}
// 7. Ошибки: мало арены, нет оператора, чужая версия схемы
MicroPrintf("--- arena %d", (int)(need - 16));
MicroPrintf("status=%d", (int)try_alloc(model, resolver, need - 16));
MicroPrintf("--- no " POOL_NAME);
{
static Resolver r4;
register_ops(r4, false);
MicroPrintf("status=%d", (int)try_alloc(model, r4, ARENA_SIZE));
}
MicroPrintf("--- schema 4");
{
memcpy(model_copy, g_model, MODEL_LEN);
uint32_t v4 = 4;
memcpy(&model_copy[VERSION_OFFSET], &v4, 4);
const tflite::Model *m4 = tflite::GetModel(model_copy);
MicroPrintf("version=%d status=%d", (int)m4->version(), (int)try_alloc(m4, resolver, ARENA_SIZE));
}
MicroPrintf("done");
port_exit(0);
return 0;
}
port_m4_119c.cc — the Cortex-M4F platform: DebugLog over semihosting, SysTick clock, DMA interrupt, start-up.
// port_m4_119c.cc — раздел 119б: платформа Cortex-M4F (QEMU netduinoplus2, STM32F405).
// DebugLog — semihosting, время — SysTick (в QEMU с -icount он идёт по числу команд;
// на кристалле вместо него берут DWT->CYCCNT), «DMA» — настоящее прерывание
// DMA2_Stream0 (IRQ 56), которое поднимается программно через NVIC.
#include <stdint.h>
#include <string.h>
#include "tensorflow/lite/micro/debug_log.h"
#include "tensorflow/lite/micro/micro_log.h"
#include "tensorflow/lite/micro/micro_time.h"
extern int g_quiet;
extern "C" void dma_half_done(int half);
int main(int argc, char **argv);
#define SYST_CSR (*(volatile uint32_t *)0xE000E010u)
#define SYST_RVR (*(volatile uint32_t *)0xE000E014u)
#define SYST_CVR (*(volatile uint32_t *)0xE000E018u)
#define NVIC_ISER1 (*(volatile uint32_t *)0xE000E104u)
#define NVIC_ISPR1 (*(volatile uint32_t *)0xE000E204u)
#define DMA2_S0_BIT (1u << (56 - 32))
static volatile uint32_t wraps;
static volatile int dma_half;
extern "C" void SysTick_Handler(void) { wraps++; }
extern "C" void DMA2_Stream0_IRQHandler(void) { dma_half_done(dma_half); }
static void sh_call(uint32_t op, const void *arg)
{
register uint32_t r0 __asm("r0") = op;
register const void *r1 __asm("r1") = arg;
__asm volatile("bkpt 0xAB" : "+r"(r0) : "r"(r1) : "memory");
}
extern "C" void DebugLog(const char *s)
{
if (!g_quiet)
sh_call(0x04u, s); // SYS_WRITE0
}
namespace tflite {
uint32_t ticks_per_second() { return 0; }
uint32_t GetCurrentTimeTicks()
{
uint32_t w, v;
do {
w = wraps;
v = SYST_CVR;
} while (w != wraps);
return w * 0x1000000u + (0xFFFFFFu - v);
}
} // namespace tflite
void port_init(void)
{
uint32_t n = 1000000u;
SYST_RVR = 0xFFFFFFu;
SYST_CVR = 0u;
SYST_CSR = 7u;
while (SYST_CVR == 0u) {
}
NVIC_ISER1 = DMA2_S0_BIT;
uint32_t t0 = tflite::GetCurrentTimeTicks();
__asm volatile("1: subs %0, %0, #1\n"
" bne 1b\n"
: "+r"(n));
uint32_t t1 = tflite::GetCurrentTimeTicks();
MicroPrintf("cal insn=2000000 ticks=%u", (unsigned)(t1 - t0));
}
void port_kick_dma(int half)
{
dma_half = half;
NVIC_ISPR1 = DMA2_S0_BIT; // прерывание DMA «пришло»
__asm volatile("dsb\n isb" ::: "memory");
}
void port_exit(int code)
{
static const uint32_t blk[2] = {0x20026u, 0u}; // ADP_Stopped_ApplicationExit
(void)code;
sh_call(0x18u, blk);
for (;;) {
}
}
// C++ без кучи: delete не нужен, но виртуальные деструкторы на него ссылаются
void operator delete(void *) noexcept {}
void operator delete(void *, unsigned int) noexcept {}
extern "C" void __cxa_pure_virtual(void) { for (;;) {} }
// статические объекты живут до выключения: регистрировать их деструкторы незачем
extern "C" int __cxa_atexit(void (*)(void *), void *, void *) { return 0; }
extern "C" {
void *__dso_handle = nullptr;
}
extern "C" void _init(void) {}
extern uint32_t _estack, _sidata, _sdata, _edata, _sbss, _ebss;
extern "C" void __libc_init_array(void);
extern "C" void Reset_Handler(void)
{
uint32_t *s = &_sidata, *d = &_sdata;
(*(volatile uint32_t *)0xE000ED88u) |= 0xFu << 20; // CPACR: FPU
__asm volatile("dsb\n isb");
while (d < &_edata)
*d++ = *s++;
for (d = &_sbss; d < &_ebss;)
*d++ = 0u;
__libc_init_array();
main(0, 0);
port_exit(0);
}
extern "C" void Default_Handler(void)
{
MicroPrintf("fault");
port_exit(1);
}
lib119c.sh — building the TFLM library from the pack without make: file list, CMSIS-NN kernel swap, archive.
# lib119c.sh — раздел 119б: библиотека TensorFlow Lite Micro из пака Keil 1.23.2 без make и без загрузок.
# sh lib119c.sh <каталог> <ref|nn> <c++> <cc> <флаги…>
# Пак смонтирован в /tf, flatbuffers 2.0.6 — /fb, gemmlowp — /gl, ruy — /ruy, CMSIS-NN 4.1.0 — /nn.
# ref — эталонные ядра; nn — восемь ядер заменены файлами kernels/cmsis_nn/ (как OPTIMIZED_KERNEL_DIR=cmsis_nn
# в make) и добавлена CMSIS-NN. Тесты, Ethos-U и cortex_m_generic не берутся: DebugLog и часы — свои.
set -eu
OUT=$1; KERN=$2; CXX=$3; CC=$4; shift 4
rm -rf "$OUT/o"
mkdir -p "$OUT/o"
: > "$OUT/warn.txt"
INC="-I/tf -I/fb -I/gl -I/ruy -I/nn/Include"
NNK="add conv depthwise_conv fully_connected mul pooling softmax svdf"
cd /tf
find tensorflow -name '*.cpp' | grep -v -e '/ethos_u/' -e '/cmsis_nn/' -e '/cortex_m_generic/' \
-e 'test_helper' -e 'fake_micro_context' -e 'mock_micro_graph' -e 'kernel_runner' | sort > "$OUT/srcs.txt"
if [ "$KERN" = nn ]; then
for k in $NNK; do
sed -i "s#^tensorflow/lite/micro/kernels/$k.cpp\$#tensorflow/lite/micro/kernels/cmsis_nn/$k.cpp#" "$OUT/srcs.txt"
done
fi
n=0
while read -r f; do
n=$((n + 1))
echo "$CXX -std=c++17 -fno-rtti -fno-exceptions -fno-threadsafe-statics $INC $* -c $f -o $OUT/o/$(echo "$f" | tr / _).o"
done < "$OUT/srcs.txt" > "$OUT/cmds.txt"
if [ "$KERN" = nn ]; then
find /nn/Source -name '*.c' | sort | while read -r f; do
echo "$CC -std=c11 -include stdint.h $INC $* -c $f -o $OUT/o/nn_$(basename "$f" .c).o"
done >> "$OUT/cmds.txt"
fi
xargs -P "$(nproc)" -I{} sh -c '{} 2>>'"$OUT"'/warn.txt' < "$OUT/cmds.txt"
rm -f "$OUT/libtflm.a"
${AR:-ar} rcs "$OUT/libtflm.a" "$OUT"/o/*.o
echo "lib_$KERN tflm_files=$n objects=$(ls "$OUT"/o | wc -l)"
Next: section 120 "Anomaly detection" — when there are no defect recordings at all and the model learns only from normal operation.