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

121а. OpenMV: камера с машинным зрением на STM32H7 — DCMI, MicroPython и поиск объектов

Разделы 116–121 работали с числами от датчиков: вибрация, ток, напряжение. Камера даёт другой объём: один кадр 320×240 в цвете — это 153 600 байт, больше, чем вся встроенная SRAM у многих STM32. Раздел 22б показал, как DCMI и DMA кладут кадр в память. Здесь — готовая открытая система, которая идёт дальше: принимает кадр, ищет на нём цветные пятна, метки AprilTag, QR-коды и запускает нейросеть, а управляется сценарием на MicroPython. Это проект OpenMV.

Раздел разбирает его исходники: какие платы и камни, как кадр проходит от датчика до буфера, сколько кадров помещается в память, как цвет переводится в LAB и как find_blobs находит пятна. Функция поиска пятен собрана на ПК из исходников проекта и прогнана на синтетическом кадре с известными ответами.

Опоры: исходники OpenMV (github.com/openmv/openmv, коммит 62369938a94b). Лицензия: MIT — строка SPDX-License-Identifier: MIT в заголовках imlib (blob.c, framebuffer.c; таблицу lab_tab.c создаёт MIT-скрипт tools/gen_rgb2lab.py), stm_dma.c и py_image.c. Но не везде. Слой камеры (common/omv_csi.c, ports/stm32/omv_csi.c, modules/py_csi_ng.c), модуль нейросетей modules/py_ml.c и загрузчик boot/ идут под другой лицензией: копировать можно, но «solely for personal benefit and not for any commercial purpose», для коммерции — отдельный договор с OpenMV. Декодер штрихкодов lib/imlib/zbar.c — LGPL 2.1, библиотека ST в cubeai/AI — лицензия ST SLA0044. Регистры DCMI и деление кадра на части для DMA — в разделе 22б, кэш и домены памяти H7 — в разделе 11б, квантование и TensorFlow Lite Micro — в разделе 119.

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

Оригинал Перевод
machine vision машинное зрение: поиск объектов на кадре программой
frame buffer буфер кадра
line buffer строчный буфер: одна-две строки кадра
MDMA (master DMA) DMA домена D1 у H7: копирует блоки между памятями
UMA pool пул памяти OpenMV, из которого берутся кадры и модели
blob пятно: связная область точек, прошедших порог
threshold порог: шесть границ L, A, B
CIELAB (L*a*b*) цветовое пространство: яркость L и две цветовые оси A и B
flood fill заливка: обход связной области
roundness, elongation округлость, вытянутость пятна
AprilTag квадратная метка с кодом для поиска положения камеры

121а.1 Платы и камни

Каждая плата в OpenMV — каталог boards/<имя> с двумя файлами: board_config.mk (камень, частота, какие модули собирать) и board_config.h (выводы, память, датчики). Из них:

Каталог Камень Частота Пул UMA Где пул
OPENMV2 STM32F427 192 МГц 160 КБ SRAM
OPENMV3 STM32F765 216 МГц 383 КБ SRAM
OPENMV4 (H7) STM32H743 480 МГц 480 КБ SRAM с 0x2400 0000
OPENMV4P (H7 Plus) STM32H743 480 МГц 27 МБ SDRAM 32 МБ с 0xC000 0000
ARDUINO_NICLA_VISION STM32H747 400 МГц 476 КБ SRAM
ARDUINO_PORTENTA_H7 STM32H747 400 МГц 4 МБ SDRAM
OPENMV_RT1060 MIMXRT1062 600 МГц 23 МБ SDRAM
OPENMV_N6 STM32N657 800 МГц 31 МБ внешняя память

Дальше речь об OPENMV4 — STM32H743. Флеш у него 2 МБ: 128 КБ занимает загрузчик, 128 КБ — файловая система, прошивке остаётся 1664 КБ, последние 128 КБ — образ ROMFS. Стек — 64 КБ в ITCM. Кадры, модели нейросетей и временные массивы живут в пуле UMA: 480 КБ из 512 КБ SRAM по 0x2400 0000.

Датчиков в drivers/sensors — 20 файлов драйверов; OPENMV4 включает десять: OV2640, OV5640, OV7725, MT9V0xx, OV9650, MT9M114, тепловизор Lepton, PAG7920, PAJ6100 и FrogEye2020. Такт XCLK для датчика даёт таймер TIM1 на выводе PA8: 12 МГц у H7, 6 МГц у OPENMV2, 9 МГц у OPENMV3 (OMV_CSI_CLK_FREQUENCY). Аппаратный кодер JPEG есть в сборках H7 и N6 (OMV_JPEG_CODEC_ENABLE (1)), у F4 и F7 — нет.

Программа пользователя — сценарий на MicroPython. Кадр — одна строка, поиск пятен — ещё одна (пример из scripts/examples/02-Image-Processing/02-Color-Tracking/single_color_rgb565_blob_tracking.py, MIT):

thresholds = [
    (30, 100, 15, 127, 15, 127),  # generic_red_thresholds
    (30, 100, -64, -8, -32, 32),  # generic_green_thresholds
    (0, 30, 0, 64, -128, 0),  # generic_blue_thresholds
]

Порог — шесть чисел: границы L, A и B. Под Python работает библиотека imlib на C: сценарий вызывает img.find_blobs(...), а считает функция imlib_find_blobs из lib/imlib/blob.c.

Путь кадра в OpenMV Cam H7: датчик OV7725 или OV5640 получает такт XCLK 12 МГц от TIM1 и настраивается по I²C; данные D0–D7 и PIXCLK идут в DCMI, который обрезает кадр по вертикали; поток 1 DMA2 по кругу пишет одну строку в строчный буфер на 3 КБ; каналы 0 и 1 MDMA копируют строку без лишних точек в буфер кадра в пуле UMA на 480 КБ, куда помещаются до трёх кадров QVGA RGB565 по 153 600 байт; сценарий MicroPython получает кадр вызовом snapshot и передаёт его imlib; для окна IDE кадр сжимается в JPEG с качеством от 50 до 90


121а.2 Путь кадра: DCMI, строка и MDMA

Раздел 22б вёл DMA по всему кадру, частями до 65 535 слов. OpenMV на H7 делает иначе: DMA принимает одну строку по кругу, а раскладывает строки по кадру второй контроллер — MDMA. Драйвер камеры для STM32 (ports/stm32/omv_csi.c) включает у потока DMA круговой режим, запускает DCMI в непрерывном захвате и даёт DMA длину ровно в одну строку, в словах, с адресом строчного буфера. DMA пишет строку, доходит до конца буфера и начинает сначала — за это время MDMA должен забрать готовую строку.

Поток DMA — DMA2_Stream1, запрос DCMI. Строчный буфер задан в boards/OPENMV4/board_config.h:

#define OMV_LINE_BUF_SIZE                     (3 * 1024) // Image line buffer round(640 * 2BPP * 2 buffers).

Проверка перед запуском требует, чтобы строка была не длиннее половины буфера: 1536 байт, то есть 768 точек RGB565. Строка VGA — 1280 байт, проходит. По прерыванию конца передачи обработчик запускает MDMA: для MDMA выделены каналы 0 и 1 (OMV_MDMA_CHANNEL_DCMI_0, _1), а у AXI MDMA стоит наивысший приоритет 15 на чтение и запись.

Что это даёт:

Поток по шине DCMI считается как в разделе 22б: 8 бит за такт PIXCLK, 2 байта на точку. Частоту PIXCLK задаёт настройка датчика, у платы известен только XCLK 12 МГц; вот что даёт пример на 24 и 48 МГц:

PIXCLK QVGA RGB565, данные кадра VGA RGB565, данные кадра
24 МГц 6,4 мс — не больше 156 кадров/с 25,6 мс — не больше 39 кадров/с
48 МГц 3,2 мс — не больше 312 кадров/с 12,8 мс — не больше 78 кадров/с

Это верхняя граница без гашения строк и кадров и без обработки: реальная частота кадров ниже.


121а.3 Буферы кадра и JPEG для IDE

Размер кадра:

Кадр RGB565 Серый Буфер в пуле
QQVGA 160×120 38 400 байт 19 200 байт 38 432 байт
QVGA 320×240 153 600 байт 76 800 байт 153 632 байт
VGA 640×480 614 400 байт 307 200 байт 614 432 байт

Буфер в пуле больше кадра на заголовок vbuffer_t — смещение и флаги, а данные с границы строки кэша: 32 байта, выравнивание тоже 32 (lib/imlib/framebuffer.h). Буферы ходят по двум очередям: свободные и заполненные. Число буферов подбирает функция omv_csi_set_framebuffers в common/omv_csi.c: если число не задано сценарием, она просит у framebuffer_resize три буфера, при неудаче два, потом один; не вышло и с одним — ошибка.

Размер кадра она считает как ширина окна × высота × 2 — по два байта на точку при любом формате. framebuffer_resize добавляет две очереди по 28 байт на три буфера (24 — на два, 20 — на один). Сколько буферов помещается в пул, если он пуст:

Плата Пул Буферов QVGA Буферов VGA
OPENMV2 163 840 байт 1 0
OPENMV3 392 192 байт 2 0
OPENMV4 (H7) 491 520 байт 3 0
ARDUINO_NICLA_VISION 487 424 байт 3 0
OPENMV4P (H7 Plus) 28 311 552 байт 3 3

Три буфера QVGA — 460 952 байт из 491 520 у H7. VGA в 16 битах требует 614 472 байт на один буфер — у H7 это невозможно, у H7 Plus в SDRAM (раздел 41б) помещаются три.

Три буфера — это тройная буферизация: DMA пишет в один, программа читает другой, третий держит последний готовый кадр. Когда свободный буфер остался последним, framebuffer_release при трёх буферах меняет старый готовый кадр на новый, а при двух новый кадр не отдаёт: следующий пишется в тот же буфер.

Предпросмотр в IDE. Кадр уходит в OpenMV IDE по USB сжатым в JPEG (lib/imlib/framebuffer.c). Качество подстраивается: начальное — середина между 50 и 90, то есть 70. Если сжатый кадр не влез в буфер передачи, качество делится пополам, кадр пропускается, и 60 следующих кадров качество не растёт: 70 → 35 → 17 → 8 → 4 при переполнениях подряд. Без переполнений оно растёт на 1 за кадр до потолка: 90 для кадров до 153 600 байт включительно (QVGA RGB565 — ровно столько) и 50 для больших. Потолок только останавливает рост: у VGA RGB565 качество выше 50 не поднимется, но стартовые 70 само не снизит — только переполнение.


121а.4 Цвет: RGB565 в LAB таблицей

Порог по цвету в RGB плохо переносит свет: тень меняет все три канала. В LAB яркость — отдельная ось L (0…100), а цвет — оси A (зелёный — красный) и B (синий — жёлтый). Поэтому пороги в OpenMV — в LAB.

Считать LAB на каждой точке дорого: степень 2,4 и кубический корень. imlib берёт готовое из таблицы lab_table в lib/imlib/lab_tab.c, по три байта L, A, B на вход. Входов не 65 536, а 32 768 — индекс pixel >> 1, младший бит синего отброшен:

#define COLOR_RGB565_TO_L(pixel)                lab_table[((pixel >> 1) * 3) + 0]

Таблица занимает 98 304 байт вместо 196 608. Генератор лежит в проекте — tools/gen_rgb2lab.py: переводит каждую из 65 536 точек по формулам sRGB и CIELAB (белая точка D65), округляет и усредняет соседние пары. Прогон генератора на ПК:

Цвета тестового кадра по этой таблице:

Цвет, RGB L A B
красный 200, 30, 30 44 65 51
зелёный 30, 170, 40 61 −60 54
синий 20, 40, 170 25 42 −69
серый 128, 128, 128 55 2 −3

Красный проходит «общий красный» порог примера (L 30…100, A и B от 15), синий — «общий синий» (L до 30, A 0…64, B до 0). А зелёный — нет: у него B = 54, а порог «общего зелёного» ограничивает B числом 32. Этот зелёный с жёлтым оттенком — ниже в прогоне его не найдёт.


121а.5 find_blobs: заливка строками

imlib_find_blobs проходит кадр строками с шагом x_stride по X (по умолчанию 2) и y_stride по Y (1). Точку, прошедшую порог и ещё не занятую, берёт за начало пятна и заливает его построчно: расширяет отрезок влево и вправо, пока точки проходят порог, отмечает их в битовой карте размером с кадр и ищет продолжение в строке выше и ниже. Вместо рекурсии — стек LIFO из записей xylr_t (шесть чисел по 16 бит, 12 байт); под стек lifo_alloc_all забирает всё, что свободно в пуле UMA.

По ходу заливки каждый отрезок добавляет в пятно число точек и суммы X, Y, X², X·Y и Y² — из этих моментов получаются центр, угол поворота и округлость без второго прохода. Потом пятна меньше pixels_threshold точек или area_threshold площади рамки отбрасываются (по умолчанию оба — 10), а при merge=True пересекающиеся рамки сливаются; margin расширяет рамку перед проверкой.

Прогон. Файлы blob.c, collections.c, fmath.c, lab_tab.c, sincos_tab.c и три функции из imlib.c собраны gcc на ПК с заглушками MicroPython и пула UMA. Кадр QVGA RGB565: серый фон с шумом ±12 по каждому каналу, прямоугольники 60×40 (красный), 50×80 (зелёный), 70×30 (синий), красная полоса 60×10 на 4 строки ниже красного прямоугольника, красный круг радиусом 20 из 1257 точек и 40 одиночных красных точек.

Запуск Пятен Что найдено
красный, по умолчанию 3 прямоугольник 2400 точек, центр 49,50; 49,50; полоса 600, центр 49,50; 78,50; круг 1257, центр 250,00; 180,00
красный, merge=True, margin=4 3 то же: зазор 4 строки не закрыт
красный, merge=True, margin=5 2 прямоугольник и полоса слиты: рамка 60×54, 3000 точек, центр Y 55,30
красный, шаг 2, порог 1 точка 22 три пятна и 19 одиночных точек из 40
красный, шаг 1, порог 1 точка 43 три пятна и все 40 одиночных точек
зелёный, порог примера 0 —
зелёный, B до 64 1 4000 точек, центр 164,50; 139,50, угол 90,0°
синий, порог примера 1 2100 точек, центр 264,50; 34,50

Площади и центры совпали с построенными точно: 60 × 40 = 2400, 70 × 30 = 2100, центр прямоугольника 60×40 с угла 20, 30 — 49,5; 49,5. Слитое пятно — взвешенный центр: (2400 · 49,5 + 600 · 78,5) / 3000 = 55,3. Округлость круга — 1,000, прямоугольника 60×40 — 0,445, полосы 60×10 — 0,028. Утечек памяти нет: все блоки пула освобождены.

Одиночные точки при пороге по умолчанию (10 точек) отброшены все — так фильтруется шум. При шаге 2 функция вообще не видит 21 точку из 40: начало пятна ищется только на каждой второй точке строки.

Другие поиски в imlib. find_apriltags — обёртка над библиотекой AprilTag 3.4.5 (подмодуль lib/apriltag), на H7 включено семейство TAG36H11. find_qrcodes — декодер quirc (MIT), встроенный в lib/imlib/qrcode.c. Каскады Хаара (lib/imlib/haar.c, каскады лиц и глаз в lib/haar) считают интегральное изображение не по всему кадру, а в скользящем окне строк (imlib_integral_mw_*) — памяти нужно на окно, а не на кадр.


121а.6 Нейросети: TensorFlow Lite Micro и ST Edge AI

Модуль ml (modules/py_ml.c) загружает модель .tflite в пул UMA — в быструю память и насовсем, до выгрузки модели, — и запускает её через движок сборки: у OPENMV4, OPENMV4P, Nicla Vision и Portenta — TensorFlow Lite Micro (MICROPY_PY_ML_TFLM = 1), у N6 — ST Edge AI для нейроускорителя (MICROPY_PY_ML_STAI = 1). Масштаб и нулевая точка входа берутся из самой модели, и вход квантуется по формуле раздела 119: значение делится на масштаб (в коде — умножается на заранее посчитанную обратную величину), к нему прибавляется нулевая точка, результат записывается как int8. Выход переводится назад: (q − zero_point) · scale.

Готовые модели в lib/models и их размеры:

Модель Байт Что делает
fomo_face_detection.tflite 57 040 лица: метки background, face
person_detect.tflite 300 568 есть человек или нет: no_person, person
yolov8n_192.tflite 3 228 400 YOLOv8n, метка person

person_detect — 300 568 байт; вместе с тремя буферами QVGA (460 952) это больше пула H7 в 491 520 байт, так что модели и кадрам приходится делить память: меньше буферов, меньше кадр. YOLOv8n на 3,2 МБ в пул H7 не помещается вовсе — это задача для H7 Plus с SDRAM или N6.

Каталог cubeai/ — вариант сборки с моделями STM32Cube.AI для H7 и H7 Plus: обёртка nn_st.c, py_st_nn.c под MIT, а библиотека в cubeai/AI — под лицензией ST SLA0044.

Сборка прошивки. Для этого раздела подмодули не выкачивались: lib/micropython, lib/tflm/libtflm, lib/apriltag, lib/tinyusb, modules/ulab пусты (git submodule status показывает их с «−»). Без MicroPython прошивка не собирается — поэтому размер прошивки здесь не приведён. На ARM собран только проверочный файл структур буфера кадра: sizeof(vbuffer_t) = 32 и размеры очередей 28/24/20 байт подтверждены _Static_assert в arm-none-eabi-gcc для Cortex-M7.


121а.7 Грабли


121а.8 Итоги

Дальше: раздел 121б «Звук на микроконтроллере: от PDM-микрофона к MFCC и распознаванию команды» — тракт от бита PDM до команды: CIC и КИХ, MFCC в целых числах, сеть int8 и решение по потоку.

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