Я один из разработчиков операционной системы Embox, и в этой статье я расскажу про то, как у меня получилось запустить OpenCV на плате STM32746G. Если вбить в поисковик что-то вроде "OpenCV on STM32 board", можно найти довольно много тех, кто интересуется использованием этой библиотеки на платах STM32 или других микроконтроллерах.
Есть несколько видео, которые, судя по названию, должны демонстрировать то, что нужно, но обычно (во всех видео, которые я видел) на плате STM32 производилось только получение картинки с камеры и вывод результата на экран, а сама обработка изображения делалась либо на обычном компьютере, либо на платах помощнее (например, Raspberry Pi). Популярность поисковых запросов объясняется тем, что OpenCV — самая популярная библиотека компьютерного зрения, а значит, с ней знакомо больше разработчиков, да и возможность запускать готовый для десктопа код на микроконтроллере значительно упрощает процесс разработки. Но почему до сих пор нет каких-то популярных готовых рецептов решения этой проблемы? Проблема использования OpenCV на небольших платках связана с двумя особенностиями: Как обычно, перед портированием каких-либо программ в операционную систему неплохо попробовать собрать её в том виде, в котором это задумывали разработчики. В нашем случае проблем с этим не возникает — исходники можно найти на гитхабе, библиотека собирается под GNU/Linux обычным cmake-ом. Из хороших новостей — OpenCV из коробки можно собирать в виде статической библиотеки, что делает портирование проще. Собираем библиотеку со стандартным конфигом и смотрим, сколько места они занимает. Каждый модуль собирается в отдельную библиотеку. Как видно из последней строки, .bss и .data занимают не так много места, зато кода больше 70 МиБ. Понятно, что если это слинковать статически с конкретным приложением, кода станет меньше. Попробуем выкинуть как можно больше модулей, чтобы собрался минимальный пример (который, например, просто выведет версию OpenCV), так что смотрим cmake .. -LA и отключаем в опциях всё, что отключается. С одной стороны, это только один модуль библиотеки, с другой стороны, это без оптимизации компилятором по размеру кода (-Os). ~3 МиБ кода — это всё ещё достаточно много, но уже даёт надежду на успех. На эмуляторе отлаживаться гораздо проще, поэтому сначала убедимся, что библиотека работает на qemu. В качестве эмулируемой платформы я выбрал Integrator/CP, т.к. во-первых, это тоже ARM, а во-вторых, Embox поддерживает вывод графики для этой платформы. В Embox есть механизм для сборки внешних библиотек, с его помощью добавляем OpenCV как модуль (передав все те же опции для "минимальной" сборки в виде статических библиотек), после этого добавляю простейшее приложение, которое выглядит так: Собираем систему, запускаем — получаем ожидаемый вывод. Следующий шаг — запустить какой-нибудь пример, лучше всего какой-нибудь стандартный из тех, что предлагают сами разработчики у себя на сайте. Я выбрал детектор границ Кэнни. Пример пришлось немного переписать, чтобы отображать картинку с результатом напрямую во фрэйм-буффер. Сделать это пришлось, т.к. функция imshow() умеет отрисовывать изображения через интерфейсы QT, GTK и Windows, которых, само собой, в конфиге для STM32 точно не будет. На самом деле, QT тоже можно запустить на STM32F7Discovery, но об этом будет рассказано уже в другой статье :) После недолгого выяснения, в каком именно формате хранится результат работы детектора границ, получаем изображение. Оригинальная картинка Результат На 32F746GDISCOVERY есть несколько аппаратных разделов памяти, которые мы можем так или иначе использовать SD-карту можно использовать для хранения изображений, но в контексте запуска минимального примера это не очень полезно.
Дисплей имеет разрешение 480x272, а значит, память под фреймбуффер составит 522 240 байт при глубине 32 бита, т.е. это больше, чем размер оперативной памяти, так что фреймбуффер и кучу (которая потребуется в том числе для OpenCV, чтобы хранить данные для изображений и вспомогательных структур) будем располагать в SDRAM, всё остальное (память под стэки и прочие системные нужды) отправится в RAM. Если взять минимальный конфиг для STM32F7Discovery (выкинуть всю сеть, все команды, сделать стэки как можно меньше и т.д.) и добавить туда OpenCV с примерами, с требуемой памятью будет следующее: Для тех, кто не очень знаком с тем, какие секции куда складывается, поясню: в .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 длительных секунд занимает у платы поиск границ фильтром Кэнни. Это, конечно, слишком долго (есть соображения, как это дело оптимизировать, об этом можно будет написать отдельную статью в случае успеха).   0: Качаем исходники Embox, например так: 1: Начнём со сборки загрузчика, который "прошьёт" QSPI-флэшку. Теперь нужно настроить сеть, т.к. загружать образ будем по TFTP. Для того, чтобы задать IP-адреса платы и хоста, нужно изменить файл conf/rootfs/network. Пример конфигурации: gateway — адрес хоста, откуда будет загружаться образ, address — адрес платы. После этого собираем загрузчик: 2: Обычная загрузка загрузчика (простите за каламбур) на плату — здесь ничего специфичного, нужно это сделать как для любого другого приложения для STM32F7Discovery. Если вы не знаете, как это делается, можно почитать об этом тут.
3: Компиляция образа с конфигом для OpenCV. 4: Извлечение из ELF секций, которые нужно записать в QSPI, в qspi.bin В директории conf лежит скрипт, который это делает, так что можно запустить его 5: С помощью tftp загружаем qspi.bin.bin на QSPI-флэшку. На хосте для этого нужно скопировать qspi.bin в корневую папку tftp-сервера (обычно это /srv/tftp/ или /var/lib/tftpboot/; пакеты для соответствующего сервера есть в большинстве популярных дистрибутивов, обычно называется tftpd или tftp-hpa, иногда нужно сделать systemctl start tftpd.service для старта). На Embox-е (т.е. в загрузчике) нужно выполнить такую команду (предполагаем, что у сервера адрес 192.168.2.1): 6: С помощью команды goto нужно "прыгнуть" в QSPI-память. Конкретная локация будет варьироваться в зависимости от того, как образ слинкуется, посмотреть этот адрес можно командой mem 0x90000000 (адрес старта укладывается во второе 32-битное слово образа); также потребуется выставить стэк флагом -s, адрес стэка лежит по адресу 0x90000000, пример: 7: Запускаем и наслаждаемся 40-секундным поиском границ :) Если что-то пойдёт не так — пишите issue в нашем репозитории, или в рассылку embox-devel@googlegroups.com, или в комментарии здесь.