Embedded HandbookПриложения
Все разделы › XIV. Машинное обучение на микроконтроллере
STM32F407 · уровень средний · 29 мин чтения
Открыть в интерактивном справочнике

119б. Модель на плате: TensorFlow Lite Micro изнутри — MicroInterpreter, резолвер операторов, тензорная арена, входной буфер и поток отсчётов

В разделе 118а сеть научилась узнавать пять жестов по окну трёхосевого акселерометра, в разделе 119 — как модель переводят в int8 и сверяют с эталоном бит в бит. Здесь последний шаг: файл .tflite уезжает в прошивку и работает под TensorFlow Lite Micro (TFLM). Раздел разбирает, из чего состоит это развёртывание: модель массивом во флеш-памяти, набор ядер операторов, интерпретатор и его рабочая память — тензорная арена, вход и выход в int8, поток отсчётов от DMA до окна, время по операторам, ошибки и сборка библиотеки без make.

Модель — сеть C раздела 118а (1317 параметров, 23 408 умножений с накоплением на окно), тем же зерном и на тех же данных, в int8 — 9424 байта; в int8 она верна в 475 из 480 отложенных окон, как и во float. Библиотека — пак Keil tensorflow-lite-micro 1.23.2 вместе с паками flatbuffers 2.0.6, gemmlowp, ruy и CMSIS-NN 4.1.0. Прошивка для Cortex-M4F собрана xPack arm-none-eabi-gcc 14.2.1 и выполнена в QEMU netduinoplus2 (STM32F405 — то же ядро, что у F407); тот же код собран g++ 12 на компьютере и сверен с эталонным интерпретатором TensorFlow Lite из Python. Цитаты из нынешнего TFLM — по клону репозитория (коммит 85c6999 от 3 ноября 2026 года), пути — от tensorflow/lite/micro/.

Опоры: тензорная арена и место TinyML — раздел 85; модель и её операторы — 118а; квантование, масштаб и нулевая точка — 119; DMA в кольцевом режиме и прерывания половины буфера — раздел 9; поток со скользящим окном для звука — 121б.6.

Как это называется в документации

Оригинал Перевод
MicroInterpreter интерпретатор: обходит граф и вызывает ядра
op resolver, MicroMutableOpResolver набор ядер операторов, который собирает программист
AllOpsResolver набор из всех ядер библиотеки
tensor arena тензорная арена: один массив под всю рабочую память модели
persistent / non-persistent (tail / head) постоянная часть арены и часть, которую планировщик делит между тензорами
AllocateTensors разметка арены: кто, где и когда живёт
schema version версия схемы файла .tflite
flatbuffer формат файла модели: читается на месте, без разбора
Invoke один прогон модели
MicroProfiler журнал времени по операторам
DebugLog, MicroPrintf вывод сообщений библиотеки — его даёт платформа
half transfer / transfer complete (HT/TC) прерывания DMA: заполнена половина / весь буфер
ring buffer кольцевой буфер

119б.1 Путь отсчёта и карта памяти

Путь отсчёта: акселерометр, DMA в кольцевом режиме с двумя половинами по 32 отсчёта, прерывание половины только перекладывает данные в кольцевой буфер на 128 отсчётов, главный цикл берёт окно 64 со сдвигом 32, квантует вход масштабом и нулевой точкой из тензора и вызывает Invoke; во флеш-памяти модель 9424 байта и код библиотеки, в SRAM тензорная арена и буферы потока

У развёртывания две половины. Неизменная живёт во флеш-памяти: модель массивом байтов и код библиотеки — интерпретатор и ядра. Изменяемая живёт в ОЗУ: тензорная арена, где лежат все промежуточные результаты, и буферы потока. Интерпретатор ничего не выделяет сам — нет ни malloc, ни new в куче: всё, что ему нужно, он берёт из арены, а арену даёт программист.

Модель в прошивку кладут массивом. Так делает xxd -i model.tflite; здесь массив пишет сам train119c.py в том же виде и добавляет две вещи:

alignas(16) static uint8_t tensor_arena[ARENA_SIZE] __attribute__((section(ARENA_SECTION)));

это для арены, а для модели в model119c_data.cc стоит alignas(16) __attribute__((section(".rodata.model"))) и const. const — чтобы массив остался во флеш-памяти, а не копировался в ОЗУ при старте. Своя секция — чтобы скрипт компоновки мог положить модель отдельно, например в конец флеш-памяти, и менять её без кода. Выравнивание 16 нужно потому, что flatbuffer читается на месте: числа внутри файла лежат выровненными относительно его начала, и ядра читают веса как слова. В прошивке модель встала по адресу 0x0800d1d0 — кратно 16.

Читать модель не надо — GetModel лишь приводит указатель к типу. Проверить её надо самому:

const tflite::Model *model = tflite::GetModel(g_model);
if (model->version() != TFLITE_SCHEMA_VERSION) {

Константа равна 3 и в паке 1.23.2, и в нынешнем TFLM (micro_interpreter.h:40: #define TFLITE_SCHEMA_VERSION (3)). Опыт: поле версии в копии модели переписано на 4 — и AllocateTensors всё равно вернул успех. Библиотека версию не смотрит, проверка в прошивке — единственная.


119б.2 Набор ядер: только нужные операторы

Интерпретатор знает граф, но не знает, как считать: ядра ему дают набором. MicroMutableOpResolver<N> — набор на N операторов, куда кладут ровно те, что есть в модели. В модели восемь узлов и пять разных операторов — их список печатает конвертер (раздел 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());

плюс AddAveragePool2D (почему не AddMean — в 119б.4). Набор из всех ядер — AllOpsResolver — удобен для первой пробы, но компоновщик уже не может выбросить ни одного ядра. Сборка -Os для Cortex-M4F: библиотека с пятью ядрами и CMSIS-NN заняла 56 791 байт флеш-памяти, с AllOpsResolver — 242 903 байта, вчетверо больше. В нынешнем TFLM AllOpsResolver уже нет — только набор, собранный руками.

Если ядра не хватает, AllocateTensors откажет и скажет, какого:

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» здесь — версия оператора, а не схемы: конвертер ставит её по тому, какие возможности ядра нужны модели, и старая библиотека может не знать новую версию того же оператора.


119б.3 Тензорная арена: сколько и где

Арена — один массив. AllocateTensors раскладывает в нём с одного конца постоянные данные (описания тензоров, параметры ядер, буферы CMSIS-NN), с другого — тензоры, которые живут не весь прогон: планировщик кладёт в одно место те, что не нужны одновременно. Сколько занято, говорит arena_used_bytes() (micro_interpreter.h:148: size_t arena_used_bytes() const { return allocator_.used_bytes(); }).

На Cortex-M4F с ядрами CMSIS-NN занято 3140 байт: 1056 — тензоры (head), 2180 — постоянная часть (tail), так раскладывает RecordingMicroAllocator. Но самая маленькая арена, при которой AllocateTensors проходит, — 3312 байт: при разметке нужно временное место, которое потом отдаётся. С эталонными ядрами — 2932 байта и 2960 байт: CMSIS-NN просит рабочие буферы под свёртки. На компьютере та же модель занимает 3744 байта — указатели там по 8 байт, поэтому арену подбирают на той архитектуре, где она будет работать.

Как подобрать размер: дать с запасом (здесь 16 КБ), запустить на плате, напечатать arena_used_bytes(), затем уменьшать, пока AllocateTensors проходит, и оставить запас на версию библиотеки и модели. Арену на 16 меньше нужной TFLM отверг с сообщением Too many buffers (max is 12) и кодом ошибки. Хуже с ареной вдвое меньше: в паке 1.23.2 разметка дошла до временного тензора, напечатала Failed to allocate temp memory. Requested: 64, available 16, missing: 48 и упала в abort() — программа на компьютере завершилась с кодом 134. Поэтому размер ищут шагами вверх, а не делением пополам.

Начало арены выравнивается на 16 (micro_arena_constants.h:25: constexpr int MicroArenaBufferAlignment() { return 16; }); у массива без alignas(16) до 15 байт пропадут. Место — обычная SRAM. CCM на F407 быстрая и недоступна DMA (раздел 2), а арену DMA не трогает, так что на плате её можно перенести туда и освободить SRAM под буферы обмена. В QEMU 7.2 у netduinoplus2 памяти CCM нет: прошивка с ареной по адресу 0x10000000 упала на первом обращении — на плате это не проверено. Внешняя SDRAM годится только для больших моделей: она медленнее, а арена трогается на каждом операторе.


119б.4 Вход, выход и сверка с Python

Масштаб и нулевую точку входа берут из тензора, а не переписывают руками:

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);

Вход — тензор int8 1 × 64 × 3, 192 байта, нулевая точка −42; выход — 1 × 5, нулевая точка −6. Классу нужен только номер наибольшего выхода, переводить его обратно в вещественное число незачем.

Сверка — все 480 отложенных окон, выходы int8 сравниваются с эталонным интерпретатором TensorFlow Lite 2.21 (не XNNPACK) число в число:

Модель и сборка Окон бит в бит Наибольшая разница Класс совпал
AVERAGE_POOL_2D, TFLM по умолчанию 478 из 480 1 480
то же, ключ TFLITE_SINGLE_ROUNDING 231 из 480 3 480
MEAN (как в разделе 118а), по умолчанию 355 из 480 1 480
Dense с масштабом на каждый выход 0 из 480 13 479

Отсюда три вывода. Первый: эталонные ядра и CMSIS-NN дали одни и те же выходы, а прошивка в QEMU — те же, что программа на компьютере: хэш выходов на всех 95 окнах потока совпал. Значит, эталон для прибора — тот же TFLM на компьютере, и сверять с ним можно бит в бит.

Второй: настольный TensorFlow и TFLM 1.23.2 — разные реализации. Глобальное среднее Keras превращается в оператор MEAN, и его ядра у них считают по-разному: 355 окон из 480. Та же сеть, где среднее взято слоем AveragePooling2D на всё окно, во float не отличается (разница меньше 2·10⁻⁶), а оператор AVERAGE_POOL_2D совпадает в 478 окнах. Поэтому развёрнута именно она. Ключ TFLITE_SINGLE_ROUNDING здесь только мешает — какое округление совпадёт с эталоном, решают ядра, и узнают это сравнением.

Третий — самый опасный. TensorFlow 2.21 по умолчанию даёт слою Dense свой масштаб на каждый выход, а TFLM 1.23.2 такого FULLY_CONNECTED не знает и берёт один масштаб на всех — без единого предупреждения. Выходы расходятся до 13 уровней, а модель всё ещё «почти работает»: верно 474 вместо 475. В нынешнем TFLM это ядро масштаб на выход понимает (kernels/fully_connected_common.cc:75: is_per_channel = affine_quantization->scale->size > 1;); для старой библиотеки конвертеру ставят _experimental_disable_per_channel_quantization_for_dense_layers = True.


119б.5 Поток: DMA, кольцо и окно с перекрытием

Окно — 64 отсчёта (1,28 с при 50 Гц), сдвиг — 32: каждое окно наполовину перекрывает предыдущее, и жест, попавший на стык, целиком виден в следующем окне. DMA в кольцевом режиме пишет отсчёты в буфер из двух половин по 32 отсчёта: пока процессор берёт одну половину, DMA пишет другую — это и есть двойная буферизация. Прерывание половины (HT) или конца (TC) только перекладывает готовую половину в кольцевой буфер на 128 отсчётов:

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;

Модель в прерывании не считают: Invoke длится сотни тысяч команд (119б.6), и всё это время стояли бы прерывания того же и более низкого приоритета — UART, таймеры, следующий же блок DMA. Главный цикл (или задача RTOS) смотрит на счётчик head и, если готово новое окно, собирает его из кольца, квантует и вызывает Invoke. Счётчик пишет только прерывание, читает только главный цикл, поэтому хватает одного volatile 32-битного слова без запрета прерываний.

Если главный цикл отстал так, что прерывание уже затирает начало окна, окно пропускается целиком:

if (h - next_end > RING - WIN - HOP) {
uint32_t lag = (h - next_end) / HOP;
st.dropped += (int)lag;

Лучше пропуск, чем окно из кусков двух разных моментов. В опыте поток из 48 отложенных окон подряд — 3072 отсчёта, 95 окон: при опросе после каждого блока посчитаны все 95, без пропусков, и выходы 93 из 95 бит в бит совпали с Python. Когда главный цикл заглядывал только после каждого третьего блока, успел 32 окна из 95, а 63 пропустил — и честно это посчитал. В QEMU «прерывание DMA» — настоящее: обработчик DMA2_Stream0 вызывается через NVIC, программа ставит его в ожидание после каждого блока.


119б.6 Время по операторам и сборка

Время одного Invoke и каждого оператора даёт MicroProfiler: интерпретатор отмечает начало и конец каждого узла, часы — функция платформы GetCurrentTimeTicks(). На кристалле это счётчик тактов DWT->CYCCNT; в QEMU с -icount время идёт по числу команд, и SysTick считает их же (калибровка: 2 000 000 команд — 336 002 отсчёта SysTick). Команд на один Invoke:

Сборка Команд CONV_2D DEPTHWISE_CONV_2D
CMSIS-NN, -O2 191 142 54 % 43 %
CMSIS-NN, -Os 228 647 55 % 42 %
эталонные ядра, -O2 2 047 791 78 % 21 %

Итого: с ядрами CMSIS-NN 191 142 команды (с -Os — 228 647 команд) — около 8 на умножение с накоплением; эталонные ядра — 2 047 791 команда, в 10,7 раза больше. По операторам (CMSIS-NN, -O2): CONV_2D — 54 %, DEPTHWISE_CONV_2D — 43 %, остальное — среднее, Reshape и Dense. Это команды, не такты: на плате тактов больше. Но сдвиг окна — 0,64 с, это 107 миллионов тактов на 168 МГц, так что запас на порядки.

Журнал профилировщика — массив в самом объекте: в паке 1.23.2 он на 1024 события, sizeof(MicroProfiler) на M4 — 20 488 байт, больше арены. В нынешнем TFLM событий 4096 (micro_profiler.h:73: static constexpr int kMaxEvents = 4096;), и при переполнении он не начинает сначала, как 1.23.2, а останавливает программу. Поэтому ClearEvents() — перед каждым Invoke, а в выпускной прошивке профилировщик не подключают. Сам MicroInterpreter — 176 байт.

Сообщения библиотеки идут через MicroPrintf в функцию DebugLog, которую даёт платформа: здесь на M4 это semihosting, на плате — UART или SWO (раздел 21). Ключ TF_LITE_STRIP_ERROR_STRINGS выкидывает тексты ошибок: библиотека -Os уменьшилась до 37 870 байт, но AllocateTensors будет отказывать молча.

Сборка без make. Makefile нынешнего TFLM скачивает зависимости сам: flatbuffers v25.9.23, kissfft, printf, для Cortex-M — CMSIS_6 и CMSIS-NN с GitHub, и без сети не собирается. Пак Keil 1.23.2 содержит всё нужное на диске, а файлов немного: 137 файлов .cpp TFLM (ядра, интерпретатор, планировщик, разбор flatbuffer, kernels/internal) и 72 файла .c CMSIS-NN. Для варианта с CMSIS-NN восемь ядер — add, conv, depthwise_conv, fully_connected, mul, pooling, softmax, svdf — берутся из kernels/cmsis_nn/ вместо одноимённых эталонных:

sed -i "s#^tensorflow/lite/micro/kernels/$k.cpp\$#tensorflow/lite/micro/kernels/cmsis_nn/$k.cpp#" "$OUT/srcs.txt"

Ключи: -DTF_LITE_STATIC_MEMORY (без кучи), -DCMSIS_NN, -fno-rtti -fno-exceptions -fno-threadsafe-statics, -ffunction-sections -fdata-sections и --gc-sections при компоновке — без последних пары компоновщик не выбросит неиспользуемые ядра. Тестовые файлы, Ethos-U и cortex_m_generic не нужны: DebugLog и часы своя платформа даёт сама (port_m4_119c.cc ниже). Нужна и стандартная библиотека C++ для arm-none-eabi: у gcc из образа stm-arm:1 нет даже <new>, поэтому сборка шла xPack GCC 14.2.1.


119б.7 Грабли

Модель «почти работает», но выходы на плате не те, что в Python. Dense с масштабом на каждый выход, а TFLM старый (здесь 1.23.2): ядро берёт один масштаб, не говоря ни слова. Конвертеру — _experimental_disable_per_channel_quantization_for_dense_layers = True, или новая библиотека. Сравнивать выходы, а не долю верных: 474 против 475 такую ошибку не выдаёт.

Выходы расходятся на единицу в сотнях окон. Ядро оператора в TFLM и в настольном TensorFlow считает иначе — здесь MEAN, 355 из 480. Эталон для прибора — тот же TFLM, собранный на компьютере; заменить оператор (AveragePooling2D вместо глобального среднего) — 478 из 480.

Плата падает в AllocateTensors. Арена сильно меньше нужной — в TFLM 1.23.2 это abort(), а не ошибка. Размер подбирать на целевой архитектуре по arena_used_bytes() и пробам вверх.

Арена посчитана на компьютере, а на плате не хватает или наоборот. Указатели 8 байт против 4: 3744 байта на x86-64 против 3140 на M4.

Через несколько минут работы программа останавливается. Подключён MicroProfiler, а ClearEvents() не вызывается: в нынешнем TFLM после 4096 событий это abort, в 1.23.2 журнал молча идёт по кругу.

Чужая модель прошита, всё «работает». TFLM версию схемы не проверяет — опыт с версией 4 прошёл AllocateTensors. Проверять model->version() самому, и операторы набора — тоже.

Окно из двух кусков. Главный цикл отстал, прерывание затёрло начало окна, а прошивка всё равно его посчитала. Держать счётчик отсчётов и пропускать такое окно целиком.

Буфер DMA в CCM. DMA до CCM не достаёт (раздел 2). В CCM можно арену, но не dma_buf.


119б.8 Итоги

Полные исходники

app119c.cc — приложение: модель, набор ядер, арена, поток через кольцо DMA, сверка и опыты с ошибками.

// 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 — платформа Cortex-M4F: DebugLog через semihosting, часы на SysTick, прерывание DMA, запуск.

// 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 — сборка библиотеки TFLM из пака без make: список файлов, замена ядер на CMSIS-NN, архив.

# 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)"

Дальше: раздел 120 «Обнаружение аномалий» — когда дефектных записей нет вовсе и модель учится только на норме.

Открыть в интерактивном справочнике