В этой статье мы автоматизируем процесс сборки и установки Linux-системы на микроконтроллер STM32MP157-DK2. ОС будет обладать минимальной функциональностью, но зато мы соберём из исходников собственную систему. А поможет нам в этом Buildroot — система сборки Linux-дистрибутивов.
Что такое Buildroot?
Сначала вспомним, что Linux-система состоит из достаточно большого количества разных компонентов. Так как мы здесь говорим про embedded-платформы, выделим следующие компоненты:
- Загрузчик (обычно для архитектуры ARM это U-Boot): выполняет инициализацию HW, загружает ядро Linux и стартует его.
- Собственно, само ядро, управляющее процессами и памятью, содержащее планировщик, файловые системы, сетевой стек и, конечно, все необходимые драйвера для вашей аппаратной платформы.
- Пользовательские библиотеки и приложения из open source экосистемы: командные оболочки, библиотеки для работы с графикой, сетью, шифрованием и так далее.
- Внутренние пользовательские библиотеки и приложения, реализующие какую-то свою «бизнес-логику».
Собрать все эти компоненты воедино мы можем двумя способами:
- Использовать готовый бинарный дистрибутив, например от Debian, Ubuntu или Fedora.
Некоторые из этих дистрибутивов поддерживают архитектуру ARMv7. Основное преимущество этого решения в том, что оно простое: эти бинарные дистрибутивы знакомы большинству пользователей Linux-систем, они имеют красивую и простую в использовании систему управления пакетами, все пакеты предварительно скомпилированы, поэтому нашей цели можно достичь очень быстро. Однако собранные таким образом системы обычно сложно кастомизировать (компоненты уже созданы, поэтому вы не можете легко изменить их конфигурацию в соответствии с вашими потребностями) и сложно оптимизировать (с точки зрения объёма занимаемой памяти или времени загрузки).
- Использовать систему сборки, например,Buildroot илиYocto/OpenEmbedded.
Они позволяют собрать Linux-систему непосредственно из исходного кода. А это означает, что систему будет легко оптимизировать и кастомизировать под свои нужды. Конечно же, этот способ намного сложнее, а на компиляцию кода придётся потратить немало процессорного времени.
Мы выбираем второй способ. В этой статье используем Buildroot, потому что с этой системой сборки достаточно просто разобраться и она подходит для embedded-платформ.
Buildroot — это набор make-файлов и скриптов, которые автоматизируют загрузку исходного кода различных компонентов, их извлечение, настройку, сборку и установку. В конечном итоге он генерирует образ системы, готовый к прошивке и обычно содержащий загрузчик, файл-образ ядра Linux и корневую файловую систему.
Важно отметить, что Buildroot не поставляется с исходными кодами Linux, с U-Boot или с другими компонентами. Он всего лишь содержит набор скриптов и инструкций, описывающих, какой исходный код загружать и какие настройки использовать при сборке.
Как собрать Linux-систему с Buildroot?
Начнём с установки самой системы Buildroot, а потом перейдём к её настройке:
git clone git://git.buildroot.net/buildroot
cd buildroot
Обычно настройка Buildroot делается с помощью команды make menuconfig, которая позволяет указать необходимые опции для вашей системы. Но мы вместо этого используем свою конфигурацию, которую мы создали заранее — специально для STM32MP157-DK2. Она находится в моём репозитории.
git remote add tpetazzoni https://github.com/tpetazzoni/buildroot.git
git fetch tpetazzoni
git checkout -b stm32mp157-dk2 tpetazzoni/2019.02/stm32mp157-dk
А теперь дадим Buildroot’у команду использовать нашу конфигурацию.
make stm32mp157_dk_defconfig
Мы могли бы сразу приступить к сборке, так как эта конфигурация работает нормально. Но здесь я хочу показать, как можно изменить конфигурацию и ускорить сборку. Мы изменим всего один параметр. Для этого запустим утилиту menuconfig (она встроена в Buildroot). Если кто-то из вас уже настраивал ядро Linux, этот инструмент должен быть вам интуитивно понятен, поскольку это просто утилита для настройки.
make menuconfig
Если команда не сможет работать из-за отсутствия библиотеки ncurses, установите пакет libncurses-dev или ncurses-devel (точное название пакета будет зависеть от версии Linux ОС, на которой вы запускаете Buildroot). Библиотека ncurses предназначена для управления вводом-выводом на терминал.
Успешно выполнив menuconfig, перейдём в подменю Toolchain. По умолчанию в Toolchain Type выбрана опция . Нужно изменить её на , нажав ENTER.
Дело в том, что по умолчанию Buildroot использует собственный кросс-компилятор. Выбрав опцию , мы можем использовать более шустрый кросс-компилятор, специально заточенный под архитектуру ARMv7.
Выходим из menuconfig и сохраняем изменения. Теперь пришло время поработать с командой make. Я люблю всё логировать, поэтому она будет выглядеть вот так:
make 2>&1 | tee build.log
На этом этапе система сборки Buildroot проверит наличие всех необходимых пакетов. Если чего-то не обнаружит, то выполнение команды прервётся. Если это произойдёт, на сайте Buildroot посмотрите раздел System requirements > Mandatory packages и установите все необходимые зависимости. После этого можно запускать команду заново.
На моей машине команда make работала 10 минут. После сборки появится набор каталогов и файлов (самое интересное лежит в output/images):
- output/images/zImage: здесь лежит ядро Linux;
- output/images/stm32mp157c-dk2.dtb: блоб-файл дерева устройств (Device Tree Blob);
- output/images/rootfs.{ext4,ext2}: файл-образ корневой файловой системы ext4, которая на сегодняшний день является самой популярной;
- output/images/u-boot-spl.stm32: загрузчик первой стадии;
- output/images/u-boot.img: загрузчик второй стадии;
- output/images/sdcard.img: финальный образ для SD-карты, сгенерированный на основе предыдущих образов.
Прошивка и тестирование системы
Запишем sdcard.img на карту microSD:
sudo dd if=output/images/sdcard.img of=/dev/mmcblk0 bs=1M conv=fdatasync status=progress
Не забудьте проверить, что в вашей системе карта microSD определяется как /dev/mmcblk0 (и на всякий случай предупреждаю: после того, вы запишете туда образ, вся информация на этой карточке будет затёрта)!
Подключите карту к микроконтроллеру.
Соедините USB-кабелем ваш компьютер и micro-USB разъём с надписью ST-LINK CN11 на плате. Ваша машина должна распознать устройство с именем /dev/ttyACM0, через которое вы сможете получить доступ к последовательному порту платы. Установите на свой компьютер и запустите программу для общения с последовательным портом. Лично мне очень нравится picocom:
picocom -b 115200 /dev/ttyACM0
Он подходит для embedded-систем, так как занимает минимальный объём памяти (менее 20 КБ) и имеет подробную документацию.
Наконец, включите плату, воткнув кабель USB-C в разъём PWR_IN CN6. Затем на последовательный порт начнут приходить сообщения. Нам важно, что в конце появится приглашение залогиниться в системе Buildroot. Можно войти в систему с пользователем root, пароль вводить не нужно.
Этапы загрузки системы и вход
Рассмотрим основные этапы процесса загрузки, изучая сообщения, которые приходят на последовательный порт:
U-Boot SPL 2018.11-stm32mp-r2.1 (Apr 24 2019 — 10:37:17 +0200)
Это сообщение от загрузчика первой стадии: код, содержащимся в файле u-boot-spl.stm32 скомпилирован как часть загрузчика U-Boot. Его непосредственно загружает STM32MP157. Загрузчик первой стадии должен быть достаточно маленьким, чтобы поместиться во внутреннюю память STM32MP157.
U-Boot 2018.11-stm32mp-r2.1 (Apr 24 2019 — 10:37:17 +0200)
Это сообщение от загрузчика второй стадии, который был выгружен из внутренней памяти устройства во внешнюю память загрузчиком первой стадии. Загрузчик второй стадии — это файл u-boot.img, который также является частью загрузчика U-Boot.
Retrieving file: /boot/zImage
Retrieving file: /boot/stm32mp157c-dk2.dtb
Эти сообщения печатает загрузчик второй стадии: мы видим, что он загрузил образ ядра Linux (файл zImage) и блоб дерева устройств (файл stm32mp157c-dk2.dtb), описывающий нашу аппаратную платформу. Хорошо, U-Boot загрузил оба файла в память: теперь он готов к запуску ядра Linux.
Starting kernel ...
Это последнее сообщение U-Boot, после этого управление передаётся ядру.
[ 0.000000] Booting Linux on physical CPU 0x0
[ 0.000000] Linux version 4.19.26 (thomas@windsurf) (gcc version 8.2.1 20180802 (GNU Toolchain for the A-profile Architecture 8.2-2018.11 (arm-rel-8.26))) #1 SMP PREEMPT Wed Apr 24 10:38:00 CEST 2019
И сразу появляются первые сообщения ядра Linux, показывающие версию Linux и дату/время сборки. Далее идут другие, не слишком интересные сообщения… Нам нужно дождаться вот этого:
[ 3.248315] VFS: Mounted root (ext4 filesystem) readonly on device 179:4.
Это сообщение указывает на то, что ядро смонтировало корневую файловую систему. После этого ядро запустит первый пользовательский процесс. Поэтому следующие сообщения будут связаны с инициализацией пользовательских служб:
Starting syslogd: OK
[...]
Welcome to Buildroot
buildroot login:
И вот, наконец, появляется то самое сообщение от Buildroot с просьбой залогиниться.
После входа в систему вы получите доступ к командной оболочке Linux. Введя команду ps, можно посмотреть список процессов, команда ls/ покажет содержимое корневой файловой системы и так далее.
Также можно немного поиграться с платой — например, включить и выключить один из светодиодов:
echo 255 > /sys/class/leds/heartbeat/brightness
echo 0 > /sys/class/leds/heartbeat/brightness
Как сделать это «с нуля»?
Углубляемся в основы конфигурирования Buildroot
В начале статьи мы говорили про настройку и оптимизацию конфигурации Buildroot. По идее, для этого нужно сначала изучить основы конфигурирования в этой системе. Поэтому вернёмся в прошлое к команде make menuconfig.
В меню Target options выбрана архитектура ARM Little Endian, а в Target Architecture Variant указан Cortex-A7. На этом процессоре как раз построен наш микроконтроллер.
В меню Build options используем все значения по умолчанию.
Как я писал выше, в меню Toolchain вместо кросс-компилятора по умолчанию был выбран пункт <External toolchain>.
В меню System configuration мы произвели следующие изменения:
- Оверлей-каталоги корневой файловой системы определены как board/stmicroelectronics/stm32mp157-dk/overlay/. Эта опция сообщает Buildroot, что содержимое этого каталога должно быть скопировано в корневую файловую систему в конце сборки. Такой подход позволяет добавлять собственные файлы в корневую файловую систему.
- Для пользовательских скриптов, запускаемых после создания образов файловой системы, задано значение support/scripts/genimage.sh, а для параметра Extra arguments, передаваемого в пользовательские скрипты, задано значение -c board/stmicroelectronics/stm32mp157-dk/genimage.cfg. Это означает, что Buildroot должен вызвать скрипт genimage.sh в самом конце сборки: его цель — сгенерировать финальный образ для SD-карты, который мы уже использовали.
В меню Kernel мы выбрали версию Linux и путь для конфигурации ядра:
- Мы загрузили исходники ядра Linux с Github с помощью макроса Buildroot под названием github. В соответствии с выбранной версией (v4.19-stm32mp-r1.2), Buildroot пошёл в репозиторий (https://github.com/STMicroelectronics/linux/) и загрузил оттуда ядро.
- Мы указали путь для конфигурации ядра: board/stmicroelectronics/stm32mp157-dk/linux.config. Конфигурацию ядра также можно кастомизировать для ваших нужд (эта тема выходит за рамки данной статьи).
- Мы включили опцию <Build a Device Tree Blob (DTB)> и записали в In-tree Device Tree Source file names имя нашего файла stm32mp157c-dk2. И тогда Buildroot сможет сформировать и использовать дерево устройств именно для нашей платформы.
- Мы установили для Install kernel image значение </boot in target>, поэтому образ ядра и дерево устройств будут находится в каталоге/bootкорневой файловой системы. U-Boot будет загружать их как раз оттуда.
В меню Target packages оставили значения по умолчанию: активен только пакет BusyBox. BusyBox — очень популярный инструмент для embedded-платформ: это легковесная альтернатива Linux shell и другим инструментам командной строки (cp, mv, ls, vi, wget, tar). Для нашей системы нам хватит одного BusyBox!
В меню Filesystem images активировали ext2/3/4root filesystem и выбрали ext4. Эта файловая система отлично подходит для SD-карт.
Теперь в меню Bootloaders активируем U-Boot, для которого выполняем следующий набор действий:
- Загружаем U-Boot из репозитория https://github.com/STMicroelectronics/u-boot.git с Git-тегом v2018.11-stm32mp-r2.1
- U-Boot используем с предустановленной конфигурацией stm32mp15_basic, которую мы указываем для нашей платы с помощью defconfig.
- Вообще, эта конфигурация предполагает использование сторожевого таймера STM32. Но в нашей минималистичной версии Linux его нет. Поэтому мы пойдём в файл board/stmicroelectronics/stm32mp157-dk/uboot-fragment.config и отключим сторожевой таймер для текущей конфигурации. Если мы захотим расширить возможности нашей Linux-системы и добавить его, то использование таймер нужно вновь разрешить.
- Подменю U-Boot binary format: тут нужно предупредить Buildroot, что для загрузчика второй стадии должен быть создан образ u-boot.img, и именно его нужно будет поместить в output/images.
- Мы также сообщаем Buildroot, что на базе нашей конфигурации для U-Boot будет собран загрузчик первой стадии (файл spl/u-boot-spl.stm32). Его тоже нужно будет разместить в output/images.
- В окружение U-Boot мы добавляем опцию DEVICE_TREE=stm32mp157c-dk2. Она понадобится в процессе сборки U-Boot, чтобы использовать дерево устройств именно для нашей платформы.
- В меню Host utilities мы подключаем пакет genimage.
Вся эта конфигурация сохраняется в простом текстовом файле configs/stm32mp157_dk_defconfig, который мы загружали изначально при запуске make stm32mp157_dk_defconfig.
Углубляемся в процесс сборки Buildroot
Надеюсь, после изучения основ конфигурирования стало понятнее, как происходит сборка (для простоты я опустил несколько промежуточных шагов):
- Загрузка и установка компилятора с веб-сайта ARM и установка библиотек C и C ++ на нашу корневую файловую систему.
- Загрузка исходного кода ядра Linux из репозитория STMicroelectronics, сборка ядра в соответствии с нашей конфигурацией, размещение zImage и stm32mp157c-dk2.dtb в каталоге output/images, размещение корневой файловой системы в каталоге /boot. Кроме того, на этом этапе происходит установка модулей ядра в корневую файловую систему.
- Загрузка исходного кода U-Boot из репозитория STMicroelectronics, его сборка в соответствии с нашей конфигурацией, размещение u-boot-spl.stm32 u-boot.img в каталоге output/images.
- Загрузка исходного кода Busybox с официального сайта, его сборка в соответствии с нашей конфигурацией и установка в корневую файловую систему.
- Копирование содержимого оверлей-каталогов в корневую файловую систему.
- Создание ext4-образа корневой файловой системы и его установка в output/images/rootfs.ext4
- Вызов скрипта genimage.sh, который сгенерирует образ для SD-карты, output/images/sdcard.img
Теперь давайте посмотрим на файл board/stmicroelectronics/stm32mp157-dk/genimage.cfg, который советует утилите genimage, как нужно правильно генерировать образ для SD-карты:
image sdcard.img {
hdimage {
gpt = «true»
}
partition fsbl1 {
image = «u-boot-spl.stm32»
}
partition fsbl2 {
image = «u-boot-spl.stm32»
}
partition uboot {
image = «u-boot.img»
}
partition rootfs {
image = «rootfs.ext4»
partition-type = 0x83
bootable = «yes»
size = 256M
}
}
Какие конкретно указания даны в этом скрипте:
- Создать файл sdcard.img
- Этот файл должен содержать несколько разделов в соответствии с таблицей GPT partition table. Это нужно, чтобы встроенный ROM микроконтроллера STM32MP157 смог найти загрузчика первой стадии.
- Два первых раздела должны называться fsbl1 и fsbl2. Там должен храниться «сырой» бинарный код загрузчика первой стадии (отмечу, что в разделах не установлено никакой файловой системы). В коде ROM, встроенном в STM32MP157, жёстко прописано: нужно искать загрузчик первой стадии в первых двух разделах с именами, начинающимися с fsbl.
- Третий раздел (тоже без файловой системы) с именем uboot, по аналогии с предыдущим пунктом, хранит сырой бинарный файл загрузчика второй стадии. Действительно, загрузчик первой стадии должен найти загрузчика второй стадии в третьем разделе SD-карты (это определено в конфигурации U-Boot и может быть при необходимости изменено ).
- Четвёртый раздел содержит образ файловой системы ext4, созданный Buildroot. Этот образ фактически является нашей корневой файловой системой, вместе с BusyBox, стандартными библиотеками C/C ++, а также файлом-образом ядра Linux и блоб-файлом дерева устройств.
Последний раздел помечен как загрузочный (bootable). Это важно, потому что конфигурация U-Boot для аппаратной платформы STM32MP157 по умолчанию следует концепции U-Boot Generic Distro Concept. При загрузке U-Boot будет искать раздел, помеченный как загрузочный, а затем внутри файловой системы, содержащейся в этом разделе, искать файл /boot/extlinux/extlinux.conf, чтобы узнать, как загрузить систему.
Файл extlinux.conf находится внутри оверлей-каталога нашей файловой системы (board/stmicroelectronics/stm32mp157-dk/overlay/boot/extlinux/extlinux.conf), в корневой файловой системе он будет определяться как /boot/extlinux/extlinux.conf и U-Boot легко найдёт его.
Вот что внутри этого файла:
label stm32mp15-buildroot
kernel /boot/zImage
devicetree /boot/stm32mp157c-dk2.dtb
append root=/dev/mmcblk0p4 rootwait
Таким образом мы говорим U-Boot, чтобы он загружал образ ядра из /boot/zImage, дерево устройств — из /boot/stm32mp157c-dk2.dtb. А строка root=/dev/mmcblk0p4 rootwait должна быть передана ядру Linux во время загрузки. Именно в этом выражении (root=/dev/mmcblk0p4) хранится информация о том, где находится корневая файловая система.
Итак, сформулируем этапы загрузки собранной Linux-системы на нашей аппаратной платформе — с учётом новых подробностей:
- Встроенный в STM32MP157 ROM ищет разделы GPT, чьи имена начинаются с fsbl. Если успешно, то загружает их содержимое во внутреннюю память STM32 и запускает загрузчик первой стадии.
- В соответствии с хардкодом, он обязан загрузить из третьего раздела SD-карты загрузчика второй стадии. Так, загрузчик первой стадии инициализирует внешнюю RAM, грузит в неё загрузчика второй стадии и стартует его.
- Загрузчик второй стадии выполняет ещё одну инициализацию, а затем ищет раздел, помеченный как загрузочный (bootable). Он обнаруживает, что таковым является четвёртый раздел. Он загружает файл /boot/extlinux/extlinux.conf, благодаря которому узнаёт, где расположены ядро и дерево устройств. Он загружает их и запускает ядро с аргументами, указанными всё в том же файле extlinux.conf.
- Ядро Linux работает до момента монтирования корневой файловой системы, расположение которой указано в параметре root=/dev/mmcblk0p4. После монтирования корневой файловой системы ядро запускает первый пользовательский процесс.
- В данном случае первый пользовательский процесс — это /sbin/init (спасибо, BusyBox!). Он стартует несколько служб, а потом приглашает пользователя войти (ввести логин).
P.S. Вы можете найти исходный код для Buildroot, использованный в этой статье, вот здесь.
Облачные серверы от Маклауд быстрые и безопасные.
Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!