Любой программист, решивший заняться разработкой под Embedded Linux, придя будь то из высокоуровневых языков программирования, либо из программирования микроконтроллеров на С/С++, неизбежно оказывается удивлен крайней недружелюбностью embedded linux. Текстовый блокнот и консольные утилиты вместо столь привычных IDE, и отладка по логам вместо отладки программатором сильно замедляют процесс разработки. В статье описывается, как мне удалось снизить время доставки изменений до целевого железа при кросс-компиляции в 10 раз. Под кросс-компиляцией подразумевается процесс, когда сборка прошивки целевой железки (target device) под управлением Embedded Linux производится на удаленной машине (host machine), которой, как правило, является обычный ПК. С этим приходится сталкиваться при разработке кода под относительно слабые микропроцессоры с ограниченными объемами доступной оперативной и постоянной памяти (128 Мб и 32 Мб соответственно в нашем случае). В этом случае, типовой процесс обновления программного кода на целевой платформе состоит из трех этапов: Доставка исходников программного кода до хоста (проводится лишь один раз при первой сборке прошивки). git clone в нашем случае.  Сборка всей прошивки целевой железки либо отдельного исполняемого файла на хосте. Утилитой make в нашем случае. Доставка свеже-собранной прошивки/исполняемого файла до целевой платформы. Утилиты scp и sysupgrade в нашем случае. Больше всего времени, естественно, уходит на второй этап. Особенно сильно он замедляется в случае работы с тяжеловесными библиотеками, тянущими за собой кучу зависимостей. В нашем случае это библиотеки tensorflow-lite (для нейронок) и alexa voice sdk (голосовой ассистент). На все три этапа (полная пересборка прошивки и перепрошивка железки) у нас уходило 150 минут. Наиболее редко применяемый сценарий. Второй этап в случае повторной пересборки прошивки занимал 30 минут. В случае пересборки тяжеловесного приложения, применящего нейронки, - 13 минут. Третий этап сам по себе (при обновлении прошивки) занимал минут 5: вначале прошивка копировалась с хоста на целевое железо утилитой scp. После чего производилось обновление прошивки утилитой sysupgrade. В какой-то момент времени такое положение дел стало вымораживать, поскольку разработка выливалась в многочасовые потери времени на компиляцию. В итоге, нам удалось получить: 16 минут вместо 150 минут для всех трех этапов вместе при первой сборке прошивки; 3 минуты вместо 30 минут для второго этапа в случае пересборки всей прошивки и 1,5 минуты вместо 13 минут при перекомпиляции отдельного приложения. 10 секунд вместо 5 минут для третьего этапа самого по себе. Наиболее просто удалось максимально ускорить, естественно, первый этап. Для этого оказалось достаточно доплатить пару сотен рублей, чтобы переключиться на самый быстрый интернет тариф нашего провайдера. Долго сомневались, будет ли смысл, но все же пошли ва-банк, начитавшись статей о вдохновляющих бенчмарках, и раскошелились на  рабочую станцию на базе процессора AMD Threadripper 3970X (64 ядра): попутно приобретя видео-карту NVIDIA GeForce 3080TI для работы с нейронками, 128 Гб оперативной памяти и 1 Тб ssd диск. Закончил сборку системника уже глубокой ночи, установил Ubuntu и запустил сборку прошивки, естественно, не забыв про make -j64... Разочарованию не было предела, когда получил 30 минут, вместо обещанных бенчмарками 2 минут. Плюнул на все и лег спать. С утра принялся выжимать все соки из дорогостоящей покупки. Первым делом запустил htop параллельно с процессом компиляции. Лишь изредка наблюдал подобную картину: иногда такую: но, чаще всего, такую: Это говорило о том, что задействуются не все ядра. Ответ в интернете нашел быстро: Enable parallel builds by default?. Недолго думая закоммитил соответствующие изменения к себе в репу и получил заветные 3 минуты на пересборку прошивки и 1,5 минуты на пересборку приложения. htop показывал полную загрузку. Радости не было предела! Деньги не ушли на ветер! Вдобавок к этому попробовал поиграться работой из RAMDISK: но особого ускорения не заметил. Единственный омрачавший радость нюанс, - сборка иногда, непредсказуемым образом падала на самым разных пакетах. Погуглив, выяснилось, что такое случается, - нестабильность при многоядерной работе.  Но этот кейс довольно легко закрыл костылем. В сборочном скрипте заменил: на: т.е., повторный запуск сборки при падении. Замедление в таких случаях было незначительным и даже при падениях удавалось собрать образ за 3-4 минуты. Погуляв на просторах интернета, узнал, что вместо копирования прошивки утилитой scp и ее обновления утилитой sysupgrade можно грузить и ядро, и файловую систему по сети, с помощью tftp и nfs. Сразу же возникло желание включить и tftp-сервер и nfs-сервер в состав репозитория, чтобы отладив все один раз в будущем не тратить на это время при переезде на другой хост. И в этом помогли великие и могучие docker и docker-compose. Если вкратце, - то это волшебная пилюля, которая позволяет разработчику запускать свой софт на любой другой машине одним нажатием кнопки ($ docker-compose up -d) не тратя время на бесконечные ошибки, неизбежно возникающие при установке и запуске каких-либо программ в первый раз или на другом ПК. Итак, Его Dockerfile: docker-compose.yml файл: Запуск на хосте командой: После этого остается положить в каталог volume образ ядра (uImage). В моем случае это делается командой: Исходники можно найти тут. Все готово для загрузки ядра загрузчиком (u-boot, в нашем случае). Но перед тем как переключиться в консоль целевой железки проверьте состояние фаерволла: и если он включен, то временно отключите его: Ядро грузит загрузчик. В случае, если у вас это u-boot, то он, скорее всего умеет это делать из коробки. Для этого, в консоли вашей целевой железки введите reboot. Дождитесь перезагрузки и, увидев обратный отсчет таймера,  нажмите Enter. Если увидите значок стрелочки: =>, то вы в консоли U-boot. Итак, первым делом сохраняем текущие настройки загрузки: к которым вы в дальнейшем сможете вернуться в любой момент: Теперь задаем сетевые настройки, чтобы u-boot знал, куда стучаться за образом ядра: где 192.168.31.99 - это IP-адрес, присваиваемый железке, а 192.168.31.37 - это IP-адрес хоста, на котором вы ранее запустили tftp-сервер. openwrt-imx6ull-cortexa7-tensorflow_wifi_dev-uImage - название файла (образ ядра, который вы ранее положили в каталог volume на хосте), который нужно скачать с tftp-сервера. loadaddr - адрес, по которому нужно его расположить для дальнейшей загрузки (у вас он, скорее всего уже задан). Попробуйте пингануть сервер: Теперь попробуйте скачать образ с сервера: Отлично. Значит пока нигде не напортачили и можно сохранять настройки и запускать ядро: Теперь снова возвращайтесь в консоль U-boot и настройте автоматическую загрузку ядра с TFTP-сервера с последующей загрузкой: Теперь u-boot должен будет автоматически грузить образ с сервера и стартовать с него. Файловая система, при этом, пока еще, будет использоваться дефолтная. Та, что у вашей железки во флеш-памяти/microSD-карте. Следующий шаг - научиться монтировать сетевую файловую систему.  Для начала, аналогично tftp-серверу соберем и запустим в контейнере NFS-сервер.  Dockerfile NFS-сервера: entrypoint.sh, используемые в Dockerfile: и docker-compose.yml: запускаем точно также как и TFTP-сервер: Программы разные (tftp-hpa и nfs-kernel-server), а запускаются одной и той же командой). Осталось распаковать в каталог volume архив с файловой системой, который у вас должен был быть скомпилировать вместе с образом ядра. В моем случае: Протестировать сервер можно с другой машины в той же сети командой: где 192.168.31.37 - IP-адрес машины, на которой вы запустили сервер, а /home/al/nfs - каталог, к которому вы примонтируете удаленную файловую систему.  Теперь пришло время загрузиться с NFS-сервера. Первым делом необходимо выяснить, включена ли в вашем дистрибутиве поддержка NFS. Для этого, на хосте, в корне репозитория с вашими исходниками Linux нужно ввести: В открывшемся окне нужно найти опцию Compile the kernel with rootfs on NFS. В моем случае она расположена по пути: Global build settings-->Kernel build options-->Compile the kernel with rootfs on NFS: Если видите значок *, значит вы можете загрузиться с NFS задав лишь дополнительные настройки в u-boot. Если нет (как было в моем случае), то вам потребуется: включить данную опцию, нажав кнопку Y выйдите из конфигуратора, сохранив изменения. пересоберите образ ядра по новой и сохраните результирующий uImage файл в каталоге volume вашего TFTP-сервера. перезагрузите u-boot на всякий случай, если вдруг вы уже успели загрузить образ ядра командой tftp Итак, единственная настройка в u-boot, необходима для того, чтобы примонтировать сетевую файловую систему вместо файловой системы, расположенной на флеш-накопителе: Обратите внимание на вывод первой команды и запомните его. После этого введите: U-boot перезагрузится, передаст управление ядру. После этого введите: Отлично! Сетевая файловая система примонтирована. Теперь любое изменение на хосте в каталоге volume TFTP-сервера будет моментально отображаться в файловой системе целевой железки. Для проверки можете в каталоге volume NFS-сервера (на хосте) создать какой-нибудь файл: Он должен сразу же отобразиться в файловой системе железки: В U-boot вы теперь можете любой момент включать отладочный (сетевой) режим загрузки: таки и "продакшн" режим загрузки: или: если ранее => printenv bootargs вам ничего не выдало. Даже прошив флешку утилитой sysupgrade, но с флагом -c вы сохраните переменные загрузчика U-boot и сохраните возможность в любой момент переключиться в отладочных режим. Можно пойти еще дальше и разработать отладочную и продуктовую конфигурации прошивки. Коммиты, реализующие данный функционал. В чем же его преимущества? В том, что любое изменение на хосте в каталоге volume TFTP-сервера будет моментально отображаться в файловой системе целевой железки. Дело осталось за малым написать скрипт, который будет копировать свеже-собранный образ ядра и распаковывать свеже-собранный архив файловой системы в соответствующие каталоги volume TFTP- и NFS-серверов. Таким образом, длительность третьего этапа (доставки изменений в коде) до целевой железки сокращается до 10 секунд в худшем случае (время перезагрузки ядра) при внесении изменений в ядро Линукс и до 0 секунд при внесении изменений в приложения пользовательского пространства! В общем, длительность наиболее типового сценария разработки кода (второй + третий этап): пересборки ядра либо пользовательского приложения с последующей доставкой на целевое железо удалось сократить с 30+ минут до 2-3 минут! Это дало возможность тестировать порядка 20 изменений в коде на целевой платформе, вместо 1-2 изменений ранее. Скорость разработки возросла на порядок! Даже не верится, что раньше я мирился с такими потерями времени!   P.s. Фото видео с полей разработки embedded linux - в телеграм-канале.