Я один із розробників операційної системи
Якщо вбити в пошукову систему щось на кшталт OpenCV on STM32 board, можна знайти чимало тих, хто цікавиться використанням цієї бібліотеки на платах STM32 або інших мікроконтролерах.
Є кілька відео, які, судячи з назви, повинні демонструвати те, що потрібно, але зазвичай (у всіх відео, які я бачив) на платі STM32 вироблялося лише отримання картинки з камери та виведення результату на екран, а сама обробка зображення робилася або на звичайному комп'ютері, або на платах потужніших (наприклад, Raspberry Pi).
Чому це складно?
Популярність пошукових запитів пояснюється тим, що OpenCV - найпопулярніша бібліотека комп'ютерного зору, а значить, з нею знайомо більше розробників, та й можливість запускати готовий для робочого столу код на мікроконтролері значно спрощує процес розробки. Але чому й досі немає якихось популярних готових рецептів вирішення цієї проблеми?
Проблема використання OpenCV на невеликих хустках пов'язана із двома особливостями:
- Якщо скомпілювати бібліотеку навіть із мінімальним набором модулів, у флеш-пам'ять тієї ж STM32F7Discovery вона просто не влізе (навіть без урахування ОС) через дуже великий код (кілька мегабайт інструкцій)
- Сама бібліотека написана на C++, отже
- Потрібна підтримка плюсового рантайму (виключення тощо)
- Мало підтримки LibC/Posix, які зазвичай є в ОС для вбудованих систем - потрібна стандартна бібліотека плюсів та стандартна бібліотека шаблонів STL (vector і т.д.)
Портування на Embox
Як завжди, перед портуванням будь-яких програм в операційну систему непогано спробувати зібрати її у вигляді, у якому це замислювали розробники. У нашому випадку проблем із цим не виникає — вихідники можна знайти на
З хороших новин — OpenCV із коробки можна збирати у вигляді статичної бібліотеки, що робить простіше портування. Збираємо бібліотеку зі стандартним конфігом і дивимося, скільки місця вони займають. Кожен модуль збирається до окремої бібліотеки.
> size lib/*so --totals
text data bss dec hex filename
1945822 15431 960 1962213 1df0e5 lib/libopencv_calib3d.so
17081885 170312 25640 17277837 107a38d lib/libopencv_core.so
10928229 137640 20192 11086061 a928ed lib/libopencv_dnn.so
842311 25680 1968 869959 d4647 lib/libopencv_features2d.so
423660 8552 184 432396 6990c lib/libopencv_flann.so
8034733 54872 1416 8091021 7b758d lib/libopencv_gapi.so
90741 3452 304 94497 17121 lib/libopencv_highgui.so
6338414 53152 968 6392534 618ad6 lib/libopencv_imgcodecs.so
21323564 155912 652056 22131532 151b34c lib/libopencv_imgproc.so
724323 12176 376 736875 b3e6b lib/libopencv_ml.so
429036 6864 464 436364 6a88c lib/libopencv_objdetect.so
6866973 50176 1064 6918213 699045 lib/libopencv_photo.so
698531 13640 160 712331 ade8b lib/libopencv_stitching.so
466295 6688 168 473151 7383f lib/libopencv_video.so
315858 6972 11576 334406 51a46 lib/libopencv_videoio.so
76510375 721519 717496 77949390 4a569ce (TOTALS)
Як видно з останнього рядка, .bss і .data займають не так багато місця, проте коду більше 70 МіБ. Зрозуміло, що якщо це злінкувати статично з конкретним додатком, коду стане менше.
Спробуємо викинути якнайбільше модулів, щоб зібрався мінімальний приклад (який, наприклад, просто виведе версію OpenCV), тож дивимося cmake .. -LA
та відключаємо в опціях все, що відключається.
-DBUILD_opencv_java_bindings_generator=OFF
-DBUILD_opencv_stitching=OFF
-DWITH_PROTOBUF=OFF
-DWITH_PTHREADS_PF=OFF
-DWITH_QUIRC=OFF
-DWITH_TIFF=OFF
-DWITH_V4L=OFF
-DWITH_VTK=OFF
-DWITH_WEBP=OFF
<...>
> size lib/libopencv_core.a --totals
text data bss dec hex filename
3317069 36425 17987 3371481 3371d9 (TOTALS)
З одного боку, це лише один модуль бібліотеки, з іншого боку, без оптимізації компілятором за розміром коду (-Os
). ~3 МіБ коду - це все ще досить багато, але вже дає надію на успіх.
Запуск в емуляторі
На емуляторі налагоджуватись набагато простіше, тому спочатку переконаємося, що бібліотека працює на qemu. Як емульована платформа я вибрав Integrator/CP, т.к. по-перше, це теж ARM, а по-друге, Embox підтримує виведення графіки для цієї платформи.
У Embox є механізм для збирання зовнішніх бібліотек, з його допомогою додаємо OpenCV як модуль (передавши ті самі опції для «мінімального» збирання у вигляді статичних бібліотек), після цього додаю найпростіший додаток, який виглядає так:
version.cpp:
#include <stdio.h>
#include <opencv2/core/utility.hpp>
int main() {
printf("OpenCV: %s", cv::getBuildInformation().c_str());
return 0;
}
Збираємо систему, запускаємо - отримуємо очікуваний висновок.
root@embox:/#opencv_version
OpenCV:
General configuration for OpenCV 4.0.1 =====================================
Version control: bd6927bdf-dirty
Platform:
Timestamp: 2019-06-21T10:02:18Z
Host: Linux 5.1.7-arch1-1-ARCH x86_64
Target: Generic arm-unknown-none
CMake: 3.14.5
CMake generator: Unix Makefiles
CMake build tool: /usr/bin/make
Configuration: Debug
CPU/HW features:
Baseline:
requested: DETECT
disabled: VFPV3 NEON
C/C++:
Built as dynamic libs?: NO
< Дальше идут прочие параметры сборки -- с какими флагами компилировалось,
какие модули OpenCV включены в сборку и т.п.>
Наступний крок — запустити якийсь приклад, найкраще якийсь стандартний із тих, що пропонують самі розробники
Приклад довелося трохи переписати, щоб відображати картинку з результатом безпосередньо у фрейм-буфер. Зробити це довелося, т.к. функція imshow()
вміє малювати зображення через інтерфейси QT, GTK і Windows, яких, зрозуміло, в конфізі для STM32 точно не буде. Насправді QT теж можна запустити на STM32F7Discovery, але про це буде розказано вже в іншій статті 🙂
Після недовгого з'ясування, в якому форматі зберігається результат роботи детектора кордонів, отримуємо зображення.
Оригінальна картинка
Результат
Запуск STM32F7Discovery
На 32F746GDISCOVERY є кілька апаратних розділів пам'яті, які ми можемо так чи інакше використовувати
- 320KiB оперативної пам'яті
- 1MiB флеш-пам'яті для образу
- 8MiB SDRAM
- 16MiB QSPI NAND-флешка
- Роз'єм для microSD-картки
SD-карту можна використовувати для зберігання зображень, але в контексті запуску мінімального прикладу це не дуже корисно.
Дисплей має роздільну здатність 480×272, отже, пам'ять під фреймбуффер складе 522 240 байт при глибині 32 біта, тобто. це більше, ніж розмір оперативної пам'яті, так що фреймбуффер і купу (яка буде потрібна в тому числі для OpenCV, щоб зберігати дані для зображень і допоміжних структур) будемо розташовувати в SDRAM, все інше (пам'ять під стеки та інші системні потреби) відправиться в RAM .
Якщо взяти мінімальний конфіг для STM32F7Discovery (викинути всю мережу, всі команди, зробити стеки якнайменше і т.д.) і додати туди OpenCV з прикладами, з пам'яттю буде наступне:
text data bss dec hex filename
2876890 459208 312736 3648834 37ad42 build/base/bin/embox
Для тих, хто не дуже знайомий з тим, які секції куди складається, поясню: .text
и .rodata
лежать інструкції та константи (грубо кажучи, readonly-дані), в .data
лежать дані, що змінюються, в .bss
лежить «занулені» змінні, яким, тим не менш, потрібне місце (ця секція «відправиться» до RAM).
Хороша новина в тому, що .data
/.bss
повинні поміщатися, а от з .text
біда - під образ є лише 1MiB пам'яті. Можна викинути з .text
картинку з прикладу і читати її, наприклад, з SD-карти на згадку при запуску, але fruits.png важить приблизно 330KiB, так що проблему це не вирішить: більша частина .text
складається саме із коду OpenCV.
За великим рахунком, залишається лише одне - завантаження частини коду на QSPI-флешку (у неї є спец. режим роботи для мепування пам'яті на системну шину, так що процесор зможе звертатися до цих даних безпосередньо). При цьому виникає проблема: по-перше, пам'ять QSPI-флешки недоступна відразу після перезавантаження пристрою (потрібно окремо ініціалізувати memory-mapped-режим), по-друге, не можна "прошити" цю пам'ять звичним завантажувачем.
У результаті було вирішено з'єднати весь код в QSPI, а прошивати його самописним завантажувачем, який отримуватиме потрібний бінарник по TFTP.
Результат
Ідея портувати цю бібліотеку на Embox з'явилася ще приблизно рік тому, але щоразу це відкладалося з різних причин. Одна з них – підтримка libstdc++ та standart template library. Проблема підтримки C++ в Embox виходить за межі цієї статті, тому тут тільки скажу, що нам вдалося досягти цієї підтримки в потрібному обсязі для роботи цієї бібліотеки 🙂
У результаті ці проблеми були подолані (принаймні, достатньо для роботи прикладу OpenCV), і приклад запустився. 40 тривалих секунд займає у плати пошук кордонів фільтром Кенні. Це, звичайно, занадто довго (є міркування, як цю справу оптимізувати, про це можна буде написати окрему статтю у разі успіху).
Тим не менш, проміжною метою було створення прототипу, який покаже принципову можливість запуску OpenCV на STM32, відповідно, цієї мети було досягнуто, ура!
tl;dr: покрокова інструкція
0: Качаємо вихідні коди Embox, наприклад так:
git clone https://github.com/embox/embox && cd ./embox
1: Почнемо зі складання завантажувача, який «прошиє» QSPI-флешку.
make confload-arm/stm32f7cube
Тепер необхідно налаштувати мережу, т.к. завантажувати образ будемо по TFTP. Для того, щоб задати IP-адресу плати та хоста, потрібно змінити файл conf/rootfs/network.
Приклад конфігурації:
iface eth0 inet static
address 192.168.2.2
netmask 255.255.255.0
gateway 192.168.2.1
hwaddress aa:bb:cc:dd:ee:02
gateway
- адреса хоста, звідки завантажуватиметься образ, address
- адреса плати.
Після цього збираємо завантажувач:
make
2: Звичайне завантаження завантажувача (вибачте за каламбур) на плату - тут нічого специфічного, потрібно це зробити як для будь-якої іншої програми для STM32F7Discovery. Якщо ви не знаєте, як це робиться, можна почитати про це
3: Компіляція образу з конфігом для OpenCV.
make confload-platform/opencv/stm32f7discovery
make
4: Вилучення з секцій ELF, які потрібно записати в QSPI, в qspi.bin
arm-none-eabi-objcopy -O binary build/base/bin/embox build/base/bin/qspi.bin
--only-section=.text --only-section=.rodata
--only-section='.ARM.ex*'
--only-section=.data
У директорії conf лежить скрипт, який це робить, тож можна запустити його
./conf/qspi_objcopy.sh # Нужный бинарник -- build/base/bin/qspi.bin
5: За допомогою tftp завантажуємо qspi.bin.bin на QSPI-флешку. На хості для цього потрібно скопіювати qspi.bin у кореневу папку tftp-сервера (зазвичай це /srv/tftp/ або /var/lib/tftpboot/; пакети для відповідного сервера є у більшості популярних дистрибутивів, зазвичай називається tftpd або tftp-hpa, іноді потрібно зробити systemctl start tftpd.service
для старту).
# вариант для tftpd
sudo cp build/base/bin/qspi.bin /srv/tftp
# вариант для tftp-hpa
sudo cp build/base/bin/qspi.bin /var/lib/tftpboot
На Embox-і (тобто в завантажувачі) потрібно виконати таку команду (припускаємо, що сервер адресу 192.168.2.1):
embox> qspi_loader qspi.bin 192.168.2.1
6: За допомогою команди goto
потрібно «стрибнути» у QSPI-пам'ять. Конкретна локація варіюватиметься залежно від того, як образ з'єднується, подивитися цю адресу можна командою mem 0x90000000
(Адреса старту вкладається в друге 32-бітне слово образу); також потрібно виставити стек прапором -s
, адреса стека лежить за адресою 0x90000000, приклад:
embox>mem 0x90000000
0x90000000: 0x20023200 0x9000c27f 0x9000c275 0x9000c275
↑ ↑
это адрес это адрес
стэка первой
инструкции
embox>goto -i 0x9000c27f -s 0x20023200 # Флаг -i нужен чтобы запретить прерывания во время инициализации системы
< Начиная отсюда будет вывод не загрузчика, а образа с OpenCV >
7: Запускаємо
embox> edges 20
та насолоджуємось 40-секундним пошуком кордонів 🙂
Якщо щось піде не так - пишіть issue
Джерело: habr.com