И снова здравствуй, Хабр! Вместе со мной, ведущим инженером-программистом в дивизионе искусственного интеллекта YADRO, вы добрались до второй части повествования о параллельном запуске двух ОС на FPGA с процессорной подсистемой.
В этой статье мы сначала определим минимально необходимые компоненты для запуска Embedded Linux. Затем осуществим сборку под ARM стандартными инструментами производителя и под Soft-CPU «вручную». И наконец, подготовим загрузочный носитель, чтобы подойти во всеоружии к запуску и верификации проекта, которые ожидают нас в заключительной части цикла.
Кратко напомню, что было в предыдущей части. Мы синтезировали проект программируемой логики и получили файл описания аппаратного обеспечения. Последний содержит необходимую информацию о компонентах Hard- и Soft-подсистем и их конфигурации.
Этот файл послужит базой для сборки ОС, но перед этим давайте посмотрим на необходимые составляющие загрузочного образа Embedded Linux.
Необходимые компоненты Embedded Linux
Для успешного запуска Embedded Linux нам понадобится всего три компонента, а именно:
Kernel — ядро операционной системы.
Initramfs — базовый образ файловой системы, монтируемый ядром для организации загрузки. Как правило, ядро ОС ищет в составе этого образа исполняемый файл с именем init, который впоследствии становится процессом с PID=1.
DTB — Device Tree Blob, бинарное представление текстового файла devicetree, описывающего аппаратные компоненты системы и их параметры.
Не стоит путать последний с файлом описания аппаратного обеспечения, полученным после синтеза проекта программируемой логики. Сравним:
DTB используется ядром ОС при загрузке с целью понять, какие компоненты содержит система, какие драйверы и с какими настройками для корректной работы компонентов необходимо загружать.
Файл, полученный после синтеза проекта программируемой логики, содержит метаинформацию о самом проекте. В том числе данные, на основании которых будут сгенерированы DTB для Hard- и Soft-подсистем.
Все три компонента независимы, их можно передать загрузчику в виде трех отдельных файлов для размещения в ОЗУ и организации запуска ОС. Но существует возможность и упаковать (слинковать) эти компоненты в единый образ. Эта опция нам на руку, так как мы планируем запустить две ОС параллельно, а значит, у нас уже не три отдельных файла, а шесть. Упаковка снизит их число до двух.
Кроме того, этот подход упростит нам и сам процесс загрузки, но об этом позже — в финальной части цикла. А пока посмотрим, что же нам необходимо собрать и какие инструменты для этого выбрать.
Подходы к сборке двух ОС
В проекте мы используем два CPU с разными архитектурами — arm64 и microblaze-el.
Различные архитектуры подразумевают два набора инструкций, которые процессор может выполнять, а следовательно — два различных набора компиляторов для сборки ОС.
В первой части я уже рассмотрел потенциальный набор инструментов для сборки Embedded Linux. Более того — дал небольшую наводку на подходы, которые использовал в ходе проекта. Сейчас постараюсь объяснить, почему выбрал конкретную связку инструментов.
Я привык разделять подходы к сборке Embedded Linux на две основные группы: фреймворки и тулчейны.
Фреймворки, как правило, позволяют сконфигурировать единый комплексный проект. Результат его сборки — все требуемые компоненты для запуска того или иного программного обеспечения, в нашем случае — операционной системы. Описанные выше три компонента, включаемые в состав образа Embedded Linux, успешно собираются при помощи фреймворков.
Однако эти компоненты — лишь состав образа ОС. Боюсь, этого недостаточно для загрузки самой аппаратной платформы. Для полноценного запуска всего программно-аппаратного комплекса потребуется еще целый ряд компонентов, таких как первичный и вторичный загрузчики, ПО вспомогательных подсистем, доверенное ПО, исполняемое ядром ARM, и другое. Все эти компоненты так же успешно собираются в составе комплексного проекта, генерируемого фреймворком.
Тулчейны же представляют собой наборы связанных инструментов. Ими можно выполнять точечную и деликатную работу по сборке отдельных компонентов «вручную». На первый взгляд, это излишнее усложнение процесса, но не спешите с выводами:
Тулчейны — это своего рода «бэкэнд» для фреймворков. Они служат тем самым набором инструментов, на базе которых фреймворки автоматизируют процесс сборки.
Не все задачи успешно решаются фреймворками. И тут гибкость работы с тулчейнами порой оказывается чуть ли не единственным вариантом для достижения цели. За доказательствами далеко ходить не придется. Выбор набора инструментов для реализации нашего проекта, в числе прочего, был обусловлен этой проблемой.
Одни из самых популярных фреймворков для сборки Embedded Linux — это Buildroot и Yocto Project. Компания AMD (Xilinx) также предлагает такой фреймворк, как Petalinux, это надстройка над Yocto Project. Petalinux упрощает работу с родительским фреймворком — особенно в задачах разработки гетерогенных проектов на базе систем на кристалле. Он позволяет напрямую конфигурировать компоненты, основываясь на файле описания аппаратного обеспечения, полученном после синтеза проекта программируемой логики.
Мой выбор пал именно на Petalinux не только потому, что это основной инструмент, рекомендованный производителем. Другая причина — в том, что в сопроводительном ПО платы разработчика был конфигурационный файл, используемый Petalinux для создания проекта. Применение этого файла гарантировало сборку ОС и всех прочих компонентов для успешного запуска на конечном устройстве.
Поясню подробнее. Фреймворк генерирует проект с набором всех необходимых компонентов. У каждого из них есть собственные настройки и настройки, отвечающие за взаимодействие с «соседями». Помимо этого, на плате для разработчика есть периферия, организующая связь с внешним миром. Она также требует внесения изменений в devicetree для корректной работы.
Если собирать проект без конфигурационного файла, придется искать детальную информацию о всех подключениях и конфигурировать все самому. В рекомендованном же сценарии базовые настройки выполняются автоматически.
Остановившись на Petalinux, я планировал использовать его и для сборки ОС под Soft-CPU, но здесь открылся неприятный нюанс. Оказалось, у данного фреймворка (по крайней мере на момент написания статьи) есть одно существенное ограничение. Конфигурация проекта Soft-CPU с использованием файла описания аппаратного обеспечения, содержащего в себе две архитектуры (arm64 и microblaze-el), не дает желаемого результата. Точнее просто не выполняется. Я приведу конкретный пример, когда буду рассказывать о порядке сборки ОС этим фреймворком.
На данном же этапе мне нужно было выбрать альтернативный подход для сборки ОС под процессор, размещенный в программируемой логике. Еще работая над проектом, я подразумевал, что он станет основой для цикла статей. И чтобы получить возможность рассказать о диаметрально ином способе, решил остановиться на сборке ОС для Soft-CPU вручную, то есть с использованием совместимого кросс-компилятора.
Дисклеймер: во избежание холиваров хочу отметить, что я не даю оценочные суждения тому или иному подходу к сборке или набору инструментов. Тем более, не склоняю вас к выбору чего-то конкретного, заявляя, что это — лучший вариант. Полагаю, что у всякого инструмента есть свои достоинства и недостатки, и каждый может быть применен наиболее эффективно для решения конкретных задач.
Определившись с подходами к сборке, приступим к самому процессу. Начнем мы с ОС для Hard-CPU.
Сборка ОС для Hard-CPU средствами Petalinux
Итак, мы определили, что ОС для Hard-CPU, а также весь набор компонентов, необходимых для загрузки аппаратной платформы, мы будем собирать с помощью фреймворка Petalinux.
Я не буду останавливаться на тонкостях скачивания и установки, а сразу перейду к деталям сборки. Но один момент, который может кого-то ввести в заблуждение, отмечу.
Синтаксис некоторых команд в свежей версии Petalinux отличается от старых версий. Наиболее заметное отличие связано с основным аргументом (подкомандой). Например, ранее команда создания проекта выглядела следующим образом:
petalinux-create -t project …
. Теперь же это:petalinux-create project …
. Я не сильно углублялся, почему это произошло, но посчитал необходимым об этом предупредить. Также отмечу, что буду приводить аргументы относительно разрабатываемой мною системы, опуская прочие опции.
Создание проекта
Очевидно, что для работы с проектом первым делом нужно его создать. Тут есть два варианта.
Первый: создание проекта при помощи так называемого конфигурационного файла (bsp).
Команда в этом случае выглядит следующим образом:
petalinux-create project -n <PROJECT> -s <PATH_TO_PETALINUX_PROJECT_BSP>
Аргумент -n
задает имя проекта, а -s
указывает путь к конфигурационному файлу.
Второй: создание проекта из шаблона.
Команда для этого метода:
petalinux-create project -n <PROJECT> --template zynqMP
Аргумент -n
все так же имя проекта, а --template
указывает название шаблона, который будет взят за основу проекта. Изначально Petalinux содержит шаблоны для всех основных платформ производителя.
В нашем случае поставщик платы для разработчика FZ5BOX (MYIR Tech) любезно предоставил конфигурационный файл для Petalinux в сопроводительном ПО. Но этот файл сильно зависит от версии фреймворка. Мы буквально ограничены в выборе до версии, при помощи которой файл был получен.
По своей сути файл — не что иное, как проект, созданный из шаблона, модифицированный, и сохраненный в виде пресета. Поэтому и наш проект, создаваемый на базе данного пресета, должен разрабатываться в аналогичной среде. Для производителя платы FZ5BOX это версия фреймворка 2020.1.
На первый взгляд, здесь нет особой проблемы. Но, если посмотреть детальнее, более старые версии фреймворка содержат и более старые версии инструментов и компонентов, доступных для выбора при настройке. Поставщик фреймворка предлагает руководство по миграции проекта и обновлению его на более свежую версию, но, как оказалось, это работает только в рамках одного мажорного релиза. То есть мы можем обновить версию 2020.1 на 2020.2, но не можем обновить, скажем, на 2024.1.
Вероятно, есть и альтернативные методы — например, при конфигурации проекта указывать прямые ссылки на исходный код компонентов с более свежими ревизиями. Однако гарантий сборки такого проекта никто, естественно, вам не даст. Таким образом, использование конфигурационного файла — неплохое упрощение, особенно если вы только начинаете свой путь в разработке. Но, безусловно, было бы лучше, если поставщик платы актуализировал этот файл относительно фреймворка хотя бы раз в несколько мажорных релизов.
Первичная конфигурация
Вне зависимости от способа создания проекта, вторым шагом будет его первичная конфигурация. Тут нам как раз понадобится файл описания аппаратного обеспечения, полученный в результате сборки проекта программируемой логики.
Выполняется конфигурация следующим образом (важно: все последующие команды запускаем из корня проекта):
petalinux-config --get-hw-description <path_to .xsa>
Команда вызовет конфигурационное меню. Для целей проекта ничего менять не нужно, просто сохраняем и выходим.
Первичная сборка
После первичной конфигурации я бы предложил выполнить первичную сборку с помощью команды:
petalinux-build
С помощью нее будут скачаны исходные коды всех компонентов — они соберутся и скомпонуются. Также будут созданы необходимые файлы devicetree. Сделать первичную сборку на данном этапе необходимо, чтобы все встало на свои места. Только после этого рекомендую приступать к модификации отдельных частей.
Сборка проекта, созданного при помощи конфигурационного файла (bsp), может быть неуспешной. Дело в том, что компоненты PL-части вашего проекта могут отличаться от компонентов PL-части проекта, который лежал в основе, при создании файла конфигурации. Как правило, в таком случае сборка завершается на этапе компиляции devicetree, так как там как раз проявляются отличия.
Модификация devicetree-файла
После успешной (или безуспешной) первичной сборки нам так или иначе придется модифицировать devicetree файл. Давайте детальнее посмотрим на изменения, которые нам нужно внести.
Devicetree-файл, в который пользователь может внести изменения, находится по пути:
…/project-spec/meta-user/recipes-user/recipes-bsp/device-tree/files/system-user.dtsi
Этот файл — включаемый (include) в основной файл, который располагается по пути:
…/components/plnx_workspace/device-tree/device-tree/system-top.dts
Также в одной директории с последним находится еще целый ряд интересных включаемых файлов. Например, pl.dtsi — файл с описанием компонентов, расположенных в PL-части. Эти файлы автоматически модифицируются как результат первичной конфигурации и построения.
Изменение devicetree может отличаться для подходов создания проекта при помощи конфигурационного файла для Petalinux и без него:
при создании из шаблона данный файл будет пустым: все изменения мы будем вносить с чистого листа.
при создании из пресета в файле уже могут быть некоторые сконфигурированные компоненты, которые способны конфликтовать с теми, что мы реализовали или не реализовали в проекте программируемой логики.
Для проекта я выбрал создание с помощью конфигурационного файла, но здесь хотел бы рассказать подробнее о случае сборки из шаблона. Потому что далеко не все производители плат для разработчика предоставляют конфигурационные файлы. А еще — интересно собрать так называемую “latest and greatest” версию всех компонентов.
Создаем devicetree-файл
Приступаем к модификации (созданию) файла system-user.dtsi. Первым делом вспомним, что оба процессора обращаются к единой ОЗУ. Для исключения одновременного доступа зарезервируем регион памяти в терминах ОС Hard-CPU для исключительного использования Soft-CPU. Делается это стандартным компонентом reserved-memory:
reserved-memory {
#address-cells = <2>;
#size-cells = <2>;
ranges;
reserved: microblaze@0x40000000 {
no-map;
reg = <0x0 0x40000000 0x0 0x20000000>;
};
};
Здесь мы видим знакомый нам из первой части регион памяти, начинающийся с адреса 0x40000000, размером в 512 МБ.
Далее — еще одна особенность более свежих версий Petalinux. Пересобирая проект в версии фреймворка 2024.1, я заметил, что PL-часть перестала отвечать на запросы. Некоторое время спустя я установил причину. Корнем проблемы оказалось отсутствие объявления в базовых фалах devicetree нужного источника тактового сигнала для PL-части.
Пришлось объявить источник самостоятельно, а также добавить опцию clk_ignore_unused
в аргументы, передаваемые ядру при загрузке. Последняя позволяет не отключать источники тактового сигнала, включенные в процессе загрузки, даже если ни один драйвер не заявил об их использовании. Интересно, что в ранних версиях Petalinux все это было объявлено по умолчанию.
chosen {
bootargs = “earlycon console=ttyPS0, 115200, root=/dev/mmcblk1p2 rw rootwait clk_ignore_unused uio_pdrv_genirq.of_id=mb-uio”;
stdout-path = “serial0:115200n8”;
};
fclk0: fclk0 {
status = “okay”;
compatible = “xlnx,fclk”;
clocks = <&zynqmp_clk 71>;
};
Внимательный читатель заметит еще один интересный аргумент в составе bootargs — uio_pdrv_genirq.of_id=mb_uio
. Это не что иное, как Linux Kernel's Userspace I/O system. В двух словах: это фреймворк, позволяющий не писать драйверы для простых memory-mapped устройств.
В проекте я использовал его для доступа к портам AXI-GPIO. Конечно, для них есть свой поставляемый вендором драйвер. Он позволяет обращаться к портам стандартными методами — через /sys/class/gpio
, но работа с Userspace I/O мне показалась более простой, требующей меньше команд. В составе образа ОС от AMD (Xilinx) есть замечательная утилита devmem, позволяющая писать напрямую по адресам физической памяти. Таким образом, выставление логической единицы на требуемом порту сводится к одной команде записи.
Чтобы подключить данный драйвер к нашему AXI-GPIO, потребуется модифицировать уже существующий компонент в devicetree.
&axi_gpio_0 {
compatible = “mb-uio”;
};
На этом мы сконфигурировали все необходимые компоненты devicetree.
Прикладываю полный файл system-user.dtsi:
/include/ "system-conf.dtsi"
/ {
chosen {
bootargs = "earlycon console=ttyPS0,115200 root=/dev/mmcblk1p2 rw rootwait clk_ignore_unused uio_pdrv_genirq.of_id=mb-uio";
stdout-path = "serial0:115200n8";
};
fclk0: fclk0 {
status = "okay";
compatible = "xlnx,fclk";
clocks = <&zynqmp_clk 71>;
};
reserved-memory {
#address-cells = <2>;
#size-cells = <2>;
ranges;
reserved: microblaze@0x40000000 {
no-map;
reg = <0x0 0x40000000 0x0 0x20000000>;
};
};
leds {
compatible = "gpio-leds";
led1 {
label = "rs485_de";
gpios = <&gpio 12 0>;
linux,default-trigger = "gpio";
};
led2 {
label = "wdt_en";
gpios = <&gpio 33 0>;
linux,default-trigger = "gpio";
};
led3 {
label = "led_sys";
gpios = <&gpio 43 0>;
linux,default-trigger = "gpio";
};
};
watchdog {
compatible = "gpio-watchdog";
};
};
&axi_gpio_0 {
compatible = "mb-uio";
};
&gem3 {
phy-handle = <&phy0>;
phy-mode = "rgmii-id";
phy0: phy@21 {
reg = <4>;
};
};
&i2c0 {
adv7619@4c {
compatible = "hdmi,adv7619";
reg = <0x4c>;
};
};
&sdhci0 {
no-1-8-v;
};
&sdhci1 {
disable-wp;
no-1-8-v;
};
&qspi {
flash@0 {
#address-cells = <1>;
#size-cells = <1>;
compatible = "m25p80";
reg = <0x0>;
spi-tx-bus-width = <1>;
spi-rx-bus-width = <4>;
spi-max-frequency = <54000000>;
};
};
Помимо вышеупомянутых изменений, там есть и другие конфигурации. Они специфичны для конкретной платы FZ5BOX. В них нет никакой магии, я просто скопировал все из проекта, созданного на базе конфигурационного файла от поставщика. Детально описывать это не буду.
Конфигурация необходимых компонентов в составе проекта
После работы с devicetree-файлом нам осталось лишь сконфигурировать необходимые компоненты, входящие в состав проекта:
petalinux-config -c <component_name>
Команда нам уже знакома, сменился только аргумент -c
, задающий имя компонента для конфигурации. Нам интересны компоненты kernel и rootfs.
В конфигурации ядра -c
kernel нам нужно перейти в раздел Device Drivers → Userspace I/O drivers и включить в состав ядра компоненты Userspace I/O platform driver with generiq IRQ handling и Userspace platform driver with generic irq and dynamic memory. Это обязательное условие для использования подсистемы Userspace I/O.
В конфигурации файловой системы -c rootfs
нам нужно перейти в раздел Filesystem Packages → console → utils → screen и включить утилиту в состав образа ОС. Она понадобится нам для доступа к Soft-CPU.
Выполнив все шаги, смело можем выполнять финальную сборку проекта. Делается это командой petalinux-build
.
Последний шаг
После сборки проекта нам остается сделать еще один шаг, чтобы начать формировать набор загрузочных образов, а именно — упаковать набор загрузчиков, вспомогательного ПО и файла прошивки программируемой логики в единый бинарный файл.
Для этого выполним команду:
petalinux-package boot --fsbl images/linux/zynqmp_fsbl.elf --u-boot images/linux/u-boot.elf --pmufw --atf --fpga --force
Аргументы данной команды, за исключением первого и последнего, — это, по сути, сами компоненты, которые мы включаем в бинарный образ.
Первый аргумент
boot
указывает на то, что мы собираем загрузочный образ. Вместо него мы можем передать, например,bsp
и собрать тот самый (точнее аналогичный) конфигурационный файл для Petalinux, который нам предоставлял поставщик платы.Последний аргумент
--force
говорит о том, что если файл ранее был создан, то необходимо его перезаписать.
Про компоненты в составе образа мы поговорим детальнее в третьей части цикла.
На этом мы завершили сборку ОС для Hard-CPU. Давайте двигаться дальше — поговорим о сборке Embedded Linux для Soft-CPU.
Сборка ОС для Soft-CPU совместимым тулчейном
Прежде чем засучить рукава и вовсю «размахивать» тем самым тулчейном, давайте разберемся, что мы вообще будем собирать.
На предыдущем этапе фреймворк нам помог в сборке необходимой цепочки компонентов для загрузки аппаратной платформы и запуска на ней ОС Hard-CPU. Отсюда следует разумный вывод: раз платформа уже может быть загружена, нам не нужно инициализировать ее второй раз. Смело исключаем все эти компоненты из списка сборки. Остается, пожалуй, только сам образ Embedded Linux, который, как мы выяснили в начале статьи, состоит из kernel, initramfs и dtb.
Про исключение компонентов
Про исключение всех компонентов я немного слукавил. Точнее спекулятивно заявил, не прояснив процесс загрузки образа ОС для Soft-CPU в оперативную память. За это отвечает вторичный загрузчик (U-Boot в нашем случае).
Есть ли необходимость собирать второй экземпляр вторичного загрузчика (простите за тавтологию)? Отвечу, не затягивая: для наших целей — нет. Потенциальный риск одновременного доступа двух CPU к единому разделу памяти, который мы решали средствами devicetree, дает приятный бонус. Раз этот доступ возможен для двух ОС, то он возможен и для загрузчика. Следовательно, мы можем положиться на вторичный загрузчик, исполняемый Hard-CPU, чтобы он разместил для нас в ОЗУ все необходимые образы для обоих процессоров. Ну разве это не классно?!
Initramfs
С initramfs у нас меньше всего работы — для чего он нужен, я писал ранее. Сам термин состоит из трех частей: init — инициализация, ram — оперативная память, fs — файловая система. Так что вполне очевидно, что initramfs — это образ файловой системы, подгружаемый в оперативную память для последующей инициализации ОС. В нем нет ничего уникального: лишь дерево каталогов и несколько конфигурационных и исполняемых файлов, главный из которых — init. Именно его ОС ищет в первую очередь после монтирования файловой системы.
Сам исполняемый файл init обычно представляет собой символическую ссылку на инициализирующую программу. В последнее время для этого в основном используется systemd, но это необязательно. В качестве init могут быть использованы busybox или написанная пользователем программа. Таким образом, минимально необходимая initramfs может быть создана буквально вручную — такими командами, как mkdir
, ln
и компиляцией простейшего “helloworld”.
Также мы можем поручить сборку initramfs фреймворку, такому как Petalinux, Yocto-project или Buildroot. Должным образом сконфигурировав компоненты, которые мы хотим включить в состав файловой системы. Однако в этом проекте я поступил еще проще. Производитель системы на кристалле предоставляет заранее собранные архивы, которые можно просто скачать, — microblaze-le_minimal.cpio.gz. Также советую обратить внимание на wiki-страницу от производителя, посвященную процессу сборки rootfs, — Build and Modify a Rootfs.
Скачав архив с минимальным initramfs, перейдем к подготовке devicetree.
Devicetree
Процесс подготовки devicetree-файла для Soft-CPU будет совершенно не таким, как для Hard-CPU. Для последнего подготовкой занимался Petalinux, а мы лишь добавляли необходимые настройки в подключаемый файл system-user.dtsi. Я обещал привести пример, почему подход с Petalinux не работает для мультиархитектурного решения. Исполняю обещание.
Попробуем создать проект Petalinux для MicroBlaze:
petalinux-create project -n <PROJECT> --template microblaze
Далее выполняем первичную конфигурацию:
petalinux-config --get-hw-description <path_to .xsa>
При конфигурации видим уже знакомое окно параметров.
Перейдем в раздел Subsystem Hardware Settings → System Processor.
Тут мы можем увидеть, что единственно доступный процессор из списка — наш Hard-CPU (psu_cortexa53_0). Для успешной конфигурации проекта тут должен быть microblaze_0. Попытка закрыть окно в таком виде, увы, завершится ошибкой.
Нам нужно найти альтернативный способ получить devicetree для MicroBlaze. Опытный читатель скажет: «Давайте скачаем шаблон с wiki-страницы, подобно архиву initramfs. И просто внесем в него изменения относительно компонентов, размещенных в PL-части нашего проекта». И будет прав, это вполне рабочий вариант. Но я хочу показать более простой способ.
Я уже писал, что по своей сути фреймворки — это обертка над инструментами командной строки, тулчейнами. А раз так, мы можем воспроизвести последовательность команд, которыми фреймворк манипулирует devicetree, но с требуемыми параметрами.
В состав стандартной поставки Vivado/Vitis, используемой для разработки проекта программируемой логики, (как и в состав Petalinux) входит очень полезная утилита — xsct (Xilinx Software Command-line Tool). Для Vivado/Vitis она располагается по пути: …/Vitis/2024.1/bin/xsct
. Путь может варьироваться от версии к версии, но найти файл не составит труда по команде find. В составе xsct есть компонент hsi, который поможет нам в решении поставленной задачи. Благодаря ему мы буквально вручную поработаем с файлом описания аппаратного обеспечения. Приступим.
Для работы с файлом описания аппаратного обеспечения нам нужно его открыть. Запускаем утилиту xsct и в ее консоли выполняем следующее:
%xsct hsi open_hw_design <path_to .xsa>
Так как мы планируем работать с devicetree, нам нужно указать путь к репозиторию с шаблонами компонентов последнего. Сам файл описания аппаратного обеспечения содержит лишь имена компонентов, а шаблоны берутся из внешнего репозитория — system-device-tree-xlnx. Клонируем этот репозиторий к себе на файловую систему и выполняем команду:
%xsct hsi set_repo_path <path_to_cloned_repo>
Следующая команда создаст проект devicetree:
%xsct hsi create_sw_design device-tree -os device_tree -proc microblaze_0
Здесь мы как раз явно указали требуемый нам процессор (microblaze_0), для которого создаем проект.
Все, что нам остается, — сгенерировать целевые файлы. Выполняем следующую команду:
%xsct hsi generate_target -dir <path_to_work_dir>
В аргументе -dir мы передаем путь, по которому хотим сохранить полученные файлы.
Завершаем работу с проектом двумя командами:
%xsct hsi close_hw_design [hsi current_hw_design]
%xsct exit
В результате мы получим набор файлов. Нас интересует три: pcw.dtsi, pl.dtsi, system-top.dts. Последний — основной, а первые два — включаемые. Файл pcw.dtsi пустой, его мы можем отбросить в сторону. Файлы pl.dtsi и system-top.dts я предлагаю объединить в один, чтобы упростить работу и не хранить два файла.
Итоговый результат
/dts-v1/;
/ {
#address-cells = <1>;
#size-cells = <1>;
compatible = "xlnx,microblaze";
model = "Xilinx MicroBlaze";
chosen {
bootargs = "earlycon";
stdout-path = "serial0:115200n8";
};
aliases {
serial0 = &axi_uartlite_0;
};
memory@0 {
device_type = "memory";
reg = <0x40000000 0x20000000>;
};
cpus {
#address-cells = <1>;
#cpus = <1>;
#size-cells = <0>;
microblaze_0: cpu@0 {
bus-handle = <&amba_pl>;
clock-frequency = <99999001>;
clocks = <&clk_cpu>;
compatible = "xlnx,microblaze-11.0";
d-cache-baseaddr = <0x40000000>;
d-cache-highaddr = <0x7fffffff>;
d-cache-line-size = <0x10>;
d-cache-size = <0x4000>;
device_type = "cpu";
i-cache-baseaddr = <0x40000000>;
i-cache-highaddr = <0x7fffffff>;
i-cache-line-size = <0x20>;
i-cache-size = <0x4000>;
interrupt-handle = <µblaze_0_axi_intc>;
model = "microblaze,11.0";
reg = <0>;
timebase-frequency = <99999001>;
xlnx,addr-size = <0x20>;
xlnx,addr-tag-bits = <0x10>;
xlnx,allow-dcache-wr = <0x1>;
xlnx,allow-icache-wr = <0x1>;
xlnx,area-optimized = <0x0>;
xlnx,async-interrupt = <0x1>;
xlnx,async-wakeup = <0x3>;
xlnx,avoid-primitives = <0x0>;
xlnx,base-vectors = <0x00000000>;
xlnx,branch-target-cache-size = <0x0>;
xlnx,cache-byte-size = <0x4000>;
xlnx,d-axi = <0x1>;
xlnx,d-lmb = <0x1>;
xlnx,d-lmb-mon = <0x0>;
xlnx,d-lmb-protocol = <0x0>;
xlnx,daddr-size = <0x20>;
xlnx,data-size = <0x20>;
xlnx,dc-axi-mon = <0x0>;
xlnx,dcache-addr-tag = <0x10>;
xlnx,dcache-always-used = <0x1>;
xlnx,dcache-byte-size = <0x4000>;
xlnx,dcache-data-width = <0x0>;
xlnx,dcache-force-tag-lutram = <0x0>;
xlnx,dcache-line-len = <0x4>;
xlnx,dcache-use-writeback = <0x0>;
xlnx,dcache-victims = <0x8>;
xlnx,debug-counter-width = <0x20>;
xlnx,debug-enabled = <0x0>;
xlnx,debug-event-counters = <0x5>;
xlnx,debug-external-trace = <0x0>;
xlnx,debug-interface = <0x0>;
xlnx,debug-latency-counters = <0x1>;
xlnx,debug-profile-size = <0x0>;
xlnx,debug-trace-async-reset = <0x0>;
xlnx,debug-trace-size = <0x2000>;
xlnx,div-zero-exception = <0x1>;
xlnx,dp-axi-mon = <0x0>;
xlnx,dynamic-bus-sizing = <0x0>;
xlnx,ecc-use-ce-exception = <0x0>;
xlnx,edge-is-positive = <0x1>;
xlnx,enable-conversion = <0x0>;
xlnx,enable-discrete-ports = <0x1>;
xlnx,endianness = <0x1>;
xlnx,fault-tolerant = <0x0>;
xlnx,fpu-exception = <0x0>;
xlnx,freq = <0x5f5dd19>;
xlnx,fsl-exception = <0x0>;
xlnx,fsl-links = <0x0>;
xlnx,i-axi = <0x0>;
xlnx,i-lmb = <0x1>;
xlnx,i-lmb-mon = <0x0>;
xlnx,i-lmb-protocol = <0x0>;
xlnx,iaddr-size = <0x20>;
xlnx,ic-axi-mon = <0x0>;
xlnx,icache-always-used = <0x1>;
xlnx,icache-data-width = <0x0>;
xlnx,icache-force-tag-lutram = <0x0>;
xlnx,icache-line-len = <0x8>;
xlnx,icache-streams = <0x1>;
xlnx,icache-victims = <0x8>;
xlnx,ill-opcode-exception = <0x1>;
xlnx,imprecise-exceptions = <0x0>;
xlnx,instr-size = <0x20>;
xlnx,interconnect = <0x2>;
xlnx,interrupt-is-edge = <0x0>;
xlnx,interrupt-mon = <0x0>;
xlnx,ip-axi-mon = <0x0>;
xlnx,lmb-data-size = <0x20>;
xlnx,lockstep-master = <0x0>;
xlnx,lockstep-select = <0x0>;
xlnx,lockstep-slave = <0x0>;
xlnx,mmu-dtlb-size = <0x4>;
xlnx,mmu-itlb-size = <0x2>;
xlnx,mmu-privileged-instr = <0x0>;
xlnx,mmu-tlb-access = <0x3>;
xlnx,mmu-zones = <0x2>;
xlnx,num-sync-ff-clk = <0x2>;
xlnx,num-sync-ff-clk-debug = <0x2>;
xlnx,num-sync-ff-clk-irq = <0x1>;
xlnx,num-sync-ff-dbg-clk = <0x1>;
xlnx,num-sync-ff-dbg-trace-clk = <0x2>;
xlnx,number-of-pc-brk = <0x1>;
xlnx,number-of-rd-addr-brk = <0x0>;
xlnx,number-of-wr-addr-brk = <0x0>;
xlnx,opcode-0x0-illegal = <0x1>;
xlnx,optimization = <0x0>;
xlnx,part = "xczu5ev-sfvc784-1-i";
xlnx,pc-width = <0x20>;
xlnx,piaddr-size = <0x20>;
xlnx,pvr = <0x2>;
xlnx,pvr-user1 = <0x00>;
xlnx,pvr-user2 = <0x00000000>;
xlnx,reset-msr = <0x00000000>;
xlnx,reset-msr-bip = <0x0>;
xlnx,reset-msr-dce = <0x0>;
xlnx,reset-msr-ee = <0x0>;
xlnx,reset-msr-eip = <0x0>;
xlnx,reset-msr-ice = <0x0>;
xlnx,reset-msr-ie = <0x0>;
xlnx,sco = <0x0>;
xlnx,temporal-depth = <0x0>;
xlnx,trace = <0x0>;
xlnx,unaligned-exceptions = <0x1>;
xlnx,use-barrel = <0x1>;
xlnx,use-branch-target-cache = <0x0>;
xlnx,use-config-reset = <0x0>;
xlnx,use-dcache = <0x1>;
xlnx,use-div = <0x1>;
xlnx,use-ext-brk = <0x0>;
xlnx,use-ext-nm-brk = <0x0>;
xlnx,use-extended-fsl-instr = <0x0>;
xlnx,use-fpu = <0x0>;
xlnx,use-hw-mul = <0x2>;
xlnx,use-icache = <0x1>;
xlnx,use-interrupt = <0x2>;
xlnx,use-mmu = <0x3>;
xlnx,use-msr-instr = <0x1>;
xlnx,use-non-secure = <0x0>;
xlnx,use-pcmp-instr = <0x1>;
xlnx,use-reorder-instr = <0x1>;
xlnx,use-stack-protection = <0x0>;
};
};
clocks {
#address-cells = <1>;
#size-cells = <0>;
clk_cpu: clk_cpu@0 {
#clock-cells = <0>;
clock-frequency = <99999001>;
clock-output-names = "clk_cpu";
compatible = "fixed-clock";
reg = <0>;
};
clk_bus_0: clk_bus_0@1 {
#clock-cells = <0>;
clock-frequency = <99999001>;
clock-output-names = "clk_bus_0";
compatible = "fixed-clock";
reg = <1>;
};
};
amba_pl: pl-bus {
#address-cells = <1>;
#size-cells = <1>;
compatible = "simple-bus";
ranges;
microblaze_0_axi_intc: interrupt-controller@80020000 {
#interrupt-cells = <2>;
compatible = "xlnx,axi-intc-4.1", "xlnx,xps-intc-1.00.a";
interrupt-controller ;
reg = <0x80020000 0x10000>;
xlnx,kind-of-intr = <0x2>;
xlnx,num-intr-inputs = <0x2>;
};
axi_timer_0: timer@80000000 {
clock-frequency = <99999001>;
clocks = <&clk_bus_0>;
compatible = "xlnx,axi-timer-2.0", "xlnx,xps-timer-1.00.a";
interrupt-names = "interrupt";
interrupt-parent = <µblaze_0_axi_intc>;
interrupts = <0 2>;
reg = <0x80000000 0x10000>;
xlnx,count-width = <0x20>;
xlnx,gen0-assert = <0x1>;
xlnx,gen1-assert = <0x1>;
xlnx,one-timer-only = <0x0>;
xlnx,trig0-assert = <0x1>;
xlnx,trig1-assert = <0x1>;
};
axi_uartlite_0: serial@80010000 {
clock-frequency = <99999001>;
clocks = <&clk_bus_0>;
compatible = "xlnx,axi-uartlite-2.0", "xlnx,xps-uartlite-1.00.a";
current-speed = <115200>;
device_type = "serial";
interrupt-names = "interrupt";
interrupt-parent = <µblaze_0_axi_intc>;
interrupts = <1 0>;
port-number = <0>;
reg = <0x80010000 0x10000>;
xlnx,baudrate = <0x1c200>;
xlnx,data-bits = <0x8>;
xlnx,odd-parity = <0x0>;
xlnx,s-axi-aclk-freq-hz-d = "99.999001";
xlnx,use-parity = <0x0>;
};
};
};
Этот файл содержит все необходимое из нашего проекта программируемой логики и может быть использован для последующей сборки Embedded Linux.
Подготовив два из трех компонентов образа ОС для Soft-CPU, переходим к заключительному этапу.
Сборка ОС Soft-CPU
Для сборки ОС нам потребуется совместимый тулчейн. Его, как и утилиту xsct, можно найти в стандартной поставке Vivado/Vitis или Petalinux. Для Vivado/Vitis он располагается по пути
…/Vitis/2024.1/gnu/microblaze/linux_toolchain/lin64_le/bin/microblazeel-xilinx-linux-gnu-.
Первым этапом клонируем репозиторий с исходным кодом ОС от вендора — linux-xlnx.
Далее выполняем базовую конфигурацию следующей командой:
make ARCH=microblaze CROSS_COMPLIE=microblazeel-xilinx-linux-gnu- O=<path_to_workdir> defconfig
Аргумент
ARCH
задает целевую архитектуру.CROSS_COMPILE
должен указывать на совместимый тулчейн. Я добавил его в переменнуюPATH
, поэтому указываю без абсолютного пути. Также отмечу, что здесь указывается только префикс тулчейна, оканчивающийся на символ “-“. Требуемые названия утилит, таких какas
,gcc
,ld
и прочих, будут автоматически добавлены в конец в процессе сборки.Опция
O
необязательна — она устанавливает рабочую директорию для сборки. Можно собирать и в основной директории исходного кода, но я предпочитаю разделять.defconfig
говорит о том, что мы хотим подгрузить конфигурацию по умолчанию для заданной архитектуры. В случае с MicroBlaze этоmmu_defconfig
.
Далее нам нужно сделать ряд дополнительных шагов, прежде чем мы перейдем к конфигурации ядра.
Для начала скопируем архив initramfs в корневой каталог исходного кода ОС. Это опциональный шаг. Он позволит в процессе конфигурации ядра указать только имя этого архива для линковки в единый образ, без абсолютного пути к нему. Вопрос не принципиальный, но печатать лишнее часто лень.
Затем копируем полученный devicetree-файл в папку с требуемой архитектурой, а именно — в linux-xlnx/arch/microblaze/boot/dts. Этот шаг обязателен, так как devicetree будет скомпилирован и слинкован с образом ОС в процессе сборки. Важно запомнить имя скопированного dts-файла. В нашем случае это system-top.dts. Имя файла будет частью цели для сборки.
Теперь можем приступить к детальной конфигурации ядра. Выполняем следующее:
make ARCH=microblaze CROSS_COMPLIE=microblazeel-xilinx-linux-gnu- O=<path_to_workdir> menuconfig
Все аргументы, кроме последнего, совпадают с первой командой. menuconfig
вызовет переход в окно конфигурации.
Здесь нас интересуют два раздела.
В разделе General setup включаем опцию Initial RAM filesystem and RAM disk (initramfs/initrd) support. Ниже, в появившейся строке Initramfs source file(s), указываем имя архива, если скопировали его в корневой каталог, или абсолютный путь к архиву, если он за пределами дерева исходного кода.
В разделе Platform options устанавливаем поле Physical address where Linux Kernel is в значение 0x40000000. Это уже знакомый нам адрес региона, выделенного для Soft-CPU. Также необходимо убедиться, что все аппаратные функции процессора совпадают с заданными при конфигурации IP-блока MicroBlaze. За деталями обращайтесь к первой части. Если допустить тут ошибку, исполняемый файл ОС может содержать инструкции, не поддерживаемые IP-блоком Soft-CPU.
Сохраняем полученную конфигурацию и приступаем к сборке.
make ARCH=microblaze CROSS_COMPLIE=microblazeel-xilinx-linux-gnu- O=<path_to_workdir> simpleImage.system-top
Уже знакомая нам команда. Но теперь в качестве последнего аргумента указываем цель для сборки. simpleImage
говорит о том, что мы хотим собрать единый образ ОС, к которому будет прилинкован devicetree. Initramfs мы прилинковали путем соответствующей конфигурации ядра. Далее идет разделительный символ .
, а после — имя файла devicetree, который будет скомпилирован и слинкован, без расширения .dts.
Спустя некоторое время проект успешно собирается — мы получаем исполняемый файл с именем, аналогичным цели сборки, то есть simpleImage.system-top. Находится он по пути: workdir/arch/microblaze/boot
.
На этом процесс сборки ОС для Hard- и Soft-CPU можем считать завершенным. Мы получили все необходимые образы. Теперь подготовим загрузочный носитель.
Советую обратить внимание на полезное видео на канале The Linux Foundation от Rob Landley — Building the Simplest Possible Linux System. Оно повествует о многих концептах, которые я применил в ходе работы над проектом.
Подготовка загрузочного носителя
В качестве загрузочного носителя для платы разработчика, я использую SD-карту. На ней нам нужно создать два раздела. В готовом проекте занятое место на каждом разделе — BOOT 57 МБ, ROOT 127 МБ.
Так, для нашего проекта достаточно SD-карты на 256 МБ. Но под рукой у меня была только карта на 32 ГБ, поэтому опишу процесс создания носителя для этого случая. Принципиальной разницы в подходе для карт другого объема нет — отличие лишь в размере разделов.
Первым делом будет неплохо затереть карту, включая таблицу разделов. Сделать это можно следующей командой:
dd if=/dev/zero of=/dev/sdX bs=1G count=32
Здecь X
в названии устройства карты (/dev/sdX
) нужно заменить на букву, соответствующую требуемому носителю. Будьте аккуратны при выборе устройства — можно случайно удалить что-то нужное. В зависимости от объема носителя и скорости вашего кард-ридера процесс может занять весьма длительное время. Очищать все содержимое необязательно — достаточно таблицы разделов в начале диска.
После очистки нужно создать два раздела. Один — загрузочный с файловой системой FAT16 или FAT32 — для размещения там образов ОС и вспомогательных компонентов. Другой — с файловой системой ext4 — для хранения основной rootfs ОС Hard-CPU.
По идее, второй раздел опциональный. ОС Hard-CPU может загрузиться исключительно с помощью initramfs. Правда, в таком случае, после перезагрузки вы обнаружите, что все созданное или сохраненное в файловой системе пропало. Не стоит забывать, что initramfs монтируется в оперативную память, которая очищается при потере питания или ребуте.
Для создания таблицы разделов и самих разделов я использовал утилиту cfdisk. Выполняем от имени суперпользователя:
sudo cfdisk /dev/sdX
Интерфейс программы весьма прост и понятен, ознакомиться с функционалом можно на man-страничке. Я создал два раздела: загрузочный размером 1 ГБ и раздел для rootfs на 4 ГБ. Результат на рисунке ниже.
После создания разделов можно размещать в них требуемые файлы, полученные в процессе сборки двух ОС.
Состав и назначение файлов, буквально следующее:
BOOT.BIN — файл, содержащий загрузчики и вспомогательное ПО, а также битстрим для программируемой логики. В его составе:
zynqmp_fsbl.elf — первичный загрузчик,
u-boot.elf — вторичный загрузчик,
bl31.elf — доверенное ПО ARM Trusted Firmware (ATF),
pmufw.elf — ПО менеджера платформы (platform management unit – pmu),
system.bit — битстрим для PL-части
Image.ub — единый образ ОС для Hard-CPU. В его составе:
Linux kernel — ядро ОС,
Initramfs — первичный образ файловой системы,
DTB — бинарное представление devicetree.
boot.scr — pагрузочный скрипт U-Boot.
rootfs.tar.gz — файловая система Hard-CPU, монтируемая вместо initramfs.
На рисунке показан единый файл, но в действительности необходимо распаковать этот архив в корень Partition 1. Сделать это можно командой:
tar -xzf rootfs.tar.gz
simpleImage.system-top — единый образ ОС для Soft-CPU. В его составе:
Linux kernel — ядро ОС,
Initramfs — первичный образ файловой системы,
DTB — бинарное представление devicetree.
Получив подготовленный загрузочный носитель, можем смело переходить к запуску и верификации проекта.
Поверьте, там еще есть о чем рассказать, но об этом уже в следующей, финальной, части. Подписывайтесь, чтобы не пропустить. А если не терпится узнать, чем все закончится, подключайтесь к митапу сообщества FPGA Systems уже в эту субботу, 26 октября. Там я расскажу про этот проект в более коротком формате.