Хабрахабр

OpenCV на STM32F7-Discovery

Я один из разработчиков операционной системы Embox, и в этой статье я расскажу про то, как у меня получилось запустить OpenCV на плате STM32746G.

Если вбить в поисковик что-то вроде "OpenCV on STM32 board", можно найти довольно много тех, кто интересуется использованием этой библиотеки на платах STM32 или других микроконтроллерах.
Есть несколько видео, которые, судя по названию, должны демонстрировать то, что нужно, но обычно (во всех видео, которые я видел) на плате STM32 производилось только получение картинки с камеры и вывод результата на экран, а сама обработка изображения делалась либо на обычном компьютере, либо на платах помощнее (например, Raspberry Pi).

Но почему до сих пор нет каких-то популярных готовых рецептов решения этой проблемы? Популярность поисковых запросов объясняется тем, что OpenCV — самая популярная библиотека компьютерного зрения, а значит, с ней знакомо больше разработчиков, да и возможность запускать готовый для десктопа код на микроконтроллере значительно упрощает процесс разработки.

Проблема использования OpenCV на небольших платках связана с двумя особенностиями:

  • Если скомпилировать библиотеку даже с минимальным набором модулей, во флэш-память той же STM32F7Discovery она просто не влезет (даже без учёта ОС) из-за очень большого кода (несколько мегабайт инструкций)
  • Сама библиотека написана на C++, а значит
    • Нужна поддержка плюсового рантайма (исключения и т.п.)
    • Мало поддержки LibC/Posix, которые обычно есть в ОС для встроенных систем — нужна стандартная библиотека плюсов и стандартная библиотека шаблонов STL (vector и т.д.)

В нашем случае проблем с этим не возникает — исходники можно найти на гитхабе, библиотека собирается под GNU/Linux обычным cmake-ом. Как обычно, перед портированием каких-либо программ в операционную систему неплохо попробовать собрать её в том виде, в котором это задумывали разработчики.

Собираем библиотеку со стандартным конфигом и смотрим, сколько места они занимает. Из хороших новостей — 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 МиБ.

-LA и отключаем в опциях всё, что отключается. Попробуем выкинуть как можно больше модулей, чтобы собрался минимальный пример (который, например, просто выведет версию OpenCV), так что смотрим cmake ..

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

~3 МиБ кода — это всё ещё достаточно много, но уже даёт надежду на успех. С одной стороны, это только один модуль библиотеки, с другой стороны, это без оптимизации компилятором по размеру кода (-Os).

Запуск в эмуляторе

В качестве эмулируемой платформы я выбрал Integrator/CP, т.к. На эмуляторе отлаживаться гораздо проще, поэтому сначала убедимся, что библиотека работает на qemu. во-первых, это тоже 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 включены в сборку и т.п.>

Я выбрал детектор границ Кэнни. Следующий шаг — запустить какой-нибудь пример, лучше всего какой-нибудь стандартный из тех, что предлагают сами разработчики у себя на сайте.

Сделать это пришлось, т.к. Пример пришлось немного переписать, чтобы отображать картинку с результатом напрямую во фрэйм-буффер. На самом деле, QT тоже можно запустить на STM32F7Discovery, но об этом будет рассказано уже в другой статье 🙂 функция imshow() умеет отрисовывать изображения через интерфейсы QT, GTK и Windows, которых, само собой, в конфиге для STM32 точно не будет.

После недолгого выяснения, в каком именно формате хранится результат работы детектора границ, получаем изображение.

Оригинальная картинка

Результат

Запуск на STM32F7Discovery

На 32F746GDISCOVERY есть несколько аппаратных разделов памяти, которые мы можем так или иначе использовать

  1. 320KiB оперативной памяти
  2. 1MiB флэш-памяти для образа
  3. 8MiB SDRAM
  4. 16MiB QSPI NAND-флэшка
  5. Разъём для microSD-карточки

это больше, чем размер оперативной памяти, так что фреймбуффер и кучу (которая потребуется в том числе для OpenCV, чтобы хранить данные для изображений и вспомогательных структур) будем располагать в SDRAM, всё остальное (память под стэки и прочие системные нужды) отправится в RAM. SD-карту можно использовать для хранения изображений, но в контексте запуска минимального примера это не очень полезно.
Дисплей имеет разрешение 480x272, а значит, память под фреймбуффер составит 522 240 байт при глубине 32 бита, т.е.

Если взять минимальный конфиг для STM32F7Discovery (выкинуть всю сеть, все команды, сделать стэки как можно меньше и т.д.) и добавить туда OpenCV с примерами, с требуемой памятью будет следующее:

text data bss dec hex filename
2876890 459208 312736 3648834 37ad42 build/base/bin/embox

Для тех, кто не очень знаком с тем, какие секции куда складывается, поясню: в .text и .rodata лежат интструкции и константы (грубо говоря, readonly-данные), в .data лежат данные изменяемые, в .bss лежит "занулённые" переменные, которым, тем не менее, нужно место (эта секция "отправится" в RAM).

Можно выкинуть из .text картинку из примера и читать её, например, с SD-карты в память при запуске, но fruits.png весит примерно 330KiB, так что проблему это не решит: большая часть .text состоит именно из кода OpenCV. Хорошая новость в том, что .data/.bss должны помещаться, а вот с .text беда — под образ есть только 1MiB памяти.

режим работы для мэпирования памяти на системную шину, так что процессор сможет обращаться к этим данным напрямую). По большому счёту, остаётся только одно — загрузка части кода на QSPI-флэшку (у неё есть спец. При этом возникает проблема: во-первых, память QSPI-флэшки недоступна сразу после перезагрузки устройства (нужно отдельно инициализировать memory-mapped-режим), во-вторых, нельзя "прошить" эту память привычным загрузчиком.

В итоге было решено слинковать весь код в QSPI, а прошивать его самописным загрузчиком, который будет получать нужный бинарник по TFTP.

Одна из них — поддержка libstdc++ и standart template library. Идея портировать эту библиотеку на Embox появилось ещё примерно год назад, но раз за разом это откладывалось из-за разных причин. Проблема поддержки C++ в Embox выходит за рамки этой статьи, поэтому здесь только скажу, что нам удалось добиться этой поддержки в нужном объёме для работы этой библиотеки 🙂

40 длительных секунд занимает у платы поиск границ фильтром Кэнни. В итоге эти проблемы были преодолены (по крайней мере, в достаточной степени для работы примера OpenCV), и пример запустился. Это, конечно, слишком долго (есть соображения, как это дело оптимизировать, об этом можно будет написать отдельную статью в случае успеха).


Тем не менее, промежуточной целью было создание прототипа, который покажет принципиальную возможность запуска 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

Если вы не знаете, как это делается, можно почитать об этом тут.
3: Компиляция образа с конфигом для OpenCV. 2: Обычная загрузка загрузчика (простите за каламбур) на плату — здесь ничего специфичного, нужно это сделать как для любого другого приложения для STM32F7Discovery.

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

На хосте для этого нужно скопировать qspi.bin в корневую папку tftp-сервера (обычно это /srv/tftp/ или /var/lib/tftpboot/; пакеты для соответствующего сервера есть в большинстве популярных дистрибутивов, обычно называется tftpd или tftp-hpa, иногда нужно сделать systemctl start tftpd.service для старта). 5: С помощью tftp загружаем qspi.bin.bin на QSPI-флэшку.

# вариант для tftpd sudo cp build/base/bin/qspi.bin /srv/tftp # вариант для tftp-hpa sudo cp build/base/bin/qspi.bin /var/lib/tftpboot

в загрузчике) нужно выполнить такую команду (предполагаем, что у сервера адрес 192. На Embox-е (т.е. 2. 168. 1):

embox> qspi_loader qspi.bin 192.168.2.1

Конкретная локация будет варьироваться в зависимости от того, как образ слинкуется, посмотреть этот адрес можно командой mem 0x90000000 (адрес старта укладывается во второе 32-битное слово образа); также потребуется выставить стэк флагом -s, адрес стэка лежит по адресу 0x90000000, пример: 6: С помощью команды goto нужно "прыгнуть" в QSPI-память.

embox>mem 0x90000000 0x90000000: 0x20023200 0x9000c27f 0x9000c275 0x9000c275 ↑ ↑ это адрес это адрес стэка первой инструкции embox>goto -i 0x9000c27f -s 0x20023200 # Флаг -i нужен чтобы запретить прерывания во время инициализации системы < Начиная отсюда будет вывод не загрузчика, а образа с OpenCV >

7: Запускаем

embox> edges 20

и наслаждаемся 40-секундным поиском границ 🙂

Если что-то пойдёт не так — пишите issue в нашем репозитории, или в рассылку embox-devel@googlegroups.com, или в комментарии здесь.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть