Теперь пора переносить проект под управление ОС Linux. В этой статье мы подробно вопросы разберем как пройти весь путь от FSBL до вывода системной консоли на OLED SSD1306. Для этого нам потребуется собрать все необходимые загрузочные артефакты: FSBL c отладкой для информативной загрузки, DTS, out-of-tree драйвер для I²C Master Controller, ядро и rootfs, соберем uImage, потом слепим BOOT.BIN и загрузим его на SD-карту. И я планирую сопроводить это все глубокой детализацией о каждом проделываемом действии.
В общем, всем интересующимся добро пожаловать под кат!

Важно! Перед началом повествования, хотелось бы заранее оговориться, что основная цель, которую я преследую при написании этой статьи - рассказать о своем опыте, с чего можно начать, при изучении отладочных плат на базе Zynq. Я не являюсь профессиональным разработчиком под ПЛИС и SoC Zynq, не являюсь системным программистом под Linux и могу допускать какие-либо ошибки в использовании терминологии, использовать не самые оптимальные пути решения задач, etc. Но отмечу, что любая конструктивная и аргументированная критика только приветствуется.
Необходимое для начала
Перед началом убедитесь, что у вас есть все необходимое для работы, то есть то над чем мы трудились в прошлой статье:
Проект
vivado/proj/zynq_mini_oled.xpr, в котором все настроено для работы PS7 с DDR, UART1, SD0, правильным тактированием FCLK, и AXI GP0 подключенным к нашему AXI SmartConnect;i2c_master_axiдобавлен в Block Design по адресу0x43C00000, прописаны все XDC, произведен синтез, implementation, сгенерирован bitstream;Сгенерирован XSA-файл
<repo>/vivado/zynq_mini_oled.xsaс галочкой Include bitstream;
По железу требования получаются все те же, на всякий случай:
Плата ZYNQ MINI Rev B, питание Type-C;
MicroSD ≥ 2 ГБ;
OLED SSD1306 128×64, I²C, адрес
0x3C(хотя иногда это0x3D);OLED подключен на гребенку контактов CAM1:
SDA к T20,SCL к P20, питание 3.3 V, GND;USB-клавиатура через type-C/type-A переходник в разъем USB host платы (необходимо будет для ввода в консоли);
BOOT-перемычки в положении "загрузка с SD";
По программному обеспечению требования такие:
Vitis 2025.2;
Клон репозитория I2C_Master_Controller, может пригодиться для поиска ошибок в артефактах;
Примерно ~30 ГБ под сборку Buildroot;
Пакеты на хосте. Все необходимое установить можно выполнив команду:
sudo apt install -y build-essential bc bison flex libssl-dev
libgnutls28-dev libncurses-dev pkg-config python3 rsync wget
cpio unzip device-tree-compiler gawk u-boot-tools git
mtools dosfstools picocombootgen, он входит в Vitis, но понадобится запускsource .../Vitis/settings64.shперед вызовом.
Цели и задачи
Основная цель - собрать работоспособную версию Linux с драйвером который обеспечивает корректную работу OLED SSD1306 и позволяет с помощью userspace-приложения выводить произвольную информацию на него.
В ходе выполнения работы должна получиться вот такая структура сущностей:

Подключаться к плате будем все так же через UART. Для вывода изображения на OLED и ввода информации будем использовать /dev/fb0 + fbcon + tty1 + USB-клавиатуру. Что ж, перейдем к сборке каждого пункта по шагам.
Загрузка от BootROM до U-Boot
Но для начала разберем, чот нам нужно для загрузки платы и как проиходит процесс загрузки. На Zynq-7000 загрузка - цепочка программ в PS, а не один монолитный “биос”. Первые две ступени загрузки жёстко заданы архитектурой Xilinx. Первая это BootROM - “нулевой загрузчик” и он знает, откуда взять следующий кусок кода (SD, QSPI, NAND, JTAG), но не знает ничего о конфигурации платы (какая DDR, какие MIO и т.п.). Второй это FSBL - настраиваемый код который получает на вход таблицу ps7_init и настраивает именно ту платформу которая в ней предоставлена.
Разберемся с каждым по очереди. Первый BootROM (Boot Read-Only Memory) - постоянная программа Xilinx внутри кристалла PS7 на Zynq-7000. После power-on или системного reset происходит сброс PS и PL который, к слову, остается в сброшенном состоянии пока не будет загружен bitstream. В этот момент ядро CPU0 работает одно, а второе ядро CPU1 ждет.
После включается ремап адресов, то есть аппаратная “подмена” цели для адресов начала диапазона сразу после reset. ARM по правилам после reset начинает с PC=0x0000_0000 (вектор сброса). на Zynq-7000 BootROM размещён в верхней области адресного пространства (в TRM UG585 - около 0xFFFC_0000), но при старте подставляется под адрес 0x0.
Поскольку процессор ARM не подключен напрямую к чипам DDR3, SPI flash или SD-карте то он видит только 32-битные адреса на внутренней шине PS. Контроллер памяти / маршрутизатор (в составе PS) решает: запрос к адресу 0x0000_0100 пойдёт в BootROM, в OCM (внутренняя SRAM), в DDR-контроллер или в регистры периферии.
OCM (внутренняя SRAM 256 KiB) физически существует в адресном пространстве с самого начала загрузки, но сразу после reset на 0x0 стоит не “ваш код в OCM”, а подмена на ROM. BootROM позже сам скопирует FSBL в OCM и переключит режим так, что CPU пойдёт уже выполнять код из SRAM, а не из ROM.

После BootROM считывает с SD образ FSBL и кладёт байты в “нижнюю” OCM (регион 0x0000_0000, 192 KiB). BootROM снимает или перенастраивает ремап (запись в регистры SLCR - System Level Control Registers), чтобы 0x0 указывал на OCM, а не на ROM. И происходит прыжок на entry point FSBL - дальше PC бегает по SRAM, в которой лежит fsbl.elf.
Пока FSBL не вызовет ps7_init(), DDR остаётся недоступной как основная большая RAM; FSBL сам живёт в OCM. Только после инициализации DDR FSBL сможет копировать туда U-Boot (потому что целиком он в OCM не поместится).
Потом идет определение Boot Mode. Определение режима загрузки - аппаратное, считывается один раз при старте из выводов MODE[2:0] (имена на схеме: BOOT_MODE, перемычки, резисторы подтяжки). ПО на ПК этот выбор не меняет и не может поменять. Типичные значения для Zynq-7000 (см. UG585, таблица Boot Mode):
MODE[2:0] (пример) |
Режим |
Где BootROM ищет FSBL |
001 |
SD / eMMC |
Файл BOOT.BIN на FAT (обычно 1-й раздел SD) |
010 |
QSPI |
Образ в SPI flash (смещение по дизайну платы) |
110 |
NAND |
Образ в NAND |
111 |
JTAG |
Образ подгружается отладчиком; BootROM минимален |
На нашей же плате перемычки загрузки установлены в режим загрузки с TF/SD (см. схему платы) и BootROM инициализирует SDIO0 (MIO 40...45 те же, что потом в DTS будет объявлено как &sdhci0) и обращается к карте как к блочному устройству. Он ищет на разделе с FAT12/FAT16/FAT32 файл с именем BOOT.BIN (регистр может иметь значение - используйте именно это имя). Но при этом BootROM не монтирует ext4, не открывает uImage, *.dtb, uEnv.txt и не запускает U-Boot или Linux - в BOOT.BIN для BootROM важна только партиция с атрибутом bootloader (= FSBL).
Обобщая все сводится к процессу изображенному на диаграмме:

Рассмотрим формат BOOT.BIN. Это не сырой fsbl.elf и bootgen упаковывает ELF все необходимые в Boot Image с таблицами (UG12800):

Partition с bitstream и U-Boot в файле BOOT.BIN так же находятся на SD, но BootROM их не обрабатывает - после старта FSBL читает тот же файл дальше (FSBL знает полный формат образа).
После происходит передача управления в FSBL который находится в OCM. В этом смысле FSBL - это обычная программа для процессора ARM и собирается из шаблона Xilinx (zynq_fsbl в embeddedsw) в совокупности с набором драйверов для инициализации периферии (SD/QSPI/RAM/PCAP/etc.). При этом FSBL при экспорте XSA генерирует функцию ps7_init() ровно под настройки PS7 Block Design которые мы производили в Vivado.
FSBL по шагам
Опишу, в образовательных целях и для полноты понимания процесса загрузки, как работает FSBL по шагам.
Шаг 1. Точка входа после BootROM. CPU выполняет код FSBL в OCM, происходит инициализация стека, базовые настройки из ps7_init или вызов полного ps7_init() (в зависимости от версии/хуков). Производится печать баннера в UART (если включен DEBUG): Xilinx First Stage Boot Loader, версия.
Шаг 2. Определение boot mode. FSBL сверяет, как BootROM стартовал систему. Для SD: Boot mode is SD. Если видите ILLEGAL_BOOT_MODE - то перемычки BOOT не те.
Шаг 3. Инициализация носителя. Загружается драйвер SDIO (PS7 sdhci0, MIO 40...45 в XSA) и выводится SD Init Done - карта отвечает, можно читать сектора FAT или сырой образ. Если происходит сбой → выводится SD_INIT_FAIL (проверяйте контакты, питание, корректность выбранных выводов MIO в XSA, или проверяйте не битая ли карта microSD).
Шаг 4. Инициализация PS - ps7_init(). Ключевой момент. Vivado при экспорте XSA сгенерировал ps7_init.c: таблицы записей в регистры SLCR, DDR controller, PLL, MIO. FSBL вызывает ps7_init(): настраивает PLL и тактирование, обучает DDR3, включает периферию, нужную для дальнейшей загрузки. Если происходит ошибка → PS7_INIT_FAIL / DDR_INIT_FAIL - Linux запущен не будет и UART может молчать без DEBUG. Если все прошло успешно - то по адресам DDR (с 0x00000000 или как в вашем проекте) она будет доступна для записи U-Boot.
Шаг 5. DevCfg / PCAP - загрузка bitstream в PL. FSBL открывает DevCfg и через PCAP (Processor Configuration Access Port) передаёт bitstream из partition BOOT.BIN в конфигурационную логику PL. В PL появляется наш i2c_master_axi по адресу 0x43C00000, становится активен FCLK0 (50 MHz), подключаются линии прерыванийIRQ_F2P[0] и т.д. - всё то, что было в synthesized design. Типичные сообщения в DEBUG-режиме FSBL: Devcfg driver initialized, включение level shifters PS↔PL, AXI Interface enabled и прочие. Если происходит сбой → PCAP_INIT_FAIL - то наверняка неверный/битый .bit, несовпадение с версией чипа (z010 vs z020).
Шаг 6. Разбор partition и загрузка U-Boot. Далее FSBL обходит partition headers в BOOT.BIN и находит образ U-Boot (ELF), копирует его в DDR по load address из заголовка (для Zynq обычно “высокая” область DDR, задаётся при линковке U-Boot). При необходимости выполняет аутентификацию (если secure boot включен). Вывод UART в режиме DEBUG: Handoff Address: 0x....
Шаг 7. Handoff - передача управления U-Boot. Handoff - это осознанный переход CPU на entry point следующей стадии. Упрощённо FSBL завершает свои драйверы (SD, PCAP) или оставляет то, что ожидает U-Boot, приводит кэши/MMU в состояние, совместимое с U-Boot (зависит от версии FSBL) и прыгает на адрес входа U-Boot (из partition header) - часто это секция _start ELF в DDR. В UART выводится в режиме DEBUG: SUCCESSFUL HANDOFF. После прыжка код FSBL больше не выполняется. Память OCM может быть перезаписана потому что FSBL не остается “висеть в фоне”.

FSBL делает прыжок на фиксированный адрес в DDR, где лежит начало кода U-Boot (_start из u-boot.elf). Это обычный переход CPU: меняется PC (Program Counter), стек и регистры могут быть в произвольном состоянии, если FSBL явно их не подготовил. При этом нет единого стандарта, что именно должно лежать в регистрах процессора при переходе FSBL → U-Boot на Zynq. Xilinx FSBL для U-Boot в типичной SD-схеме передаёт минимум - по сути “запусти код по этому адресу”.
Проблема со стартом U-Boot из FSBL
Отдельно расскажу про косяк, из-за которого у меня не стартовал U-Boot. Возможно кому-то это сэкономит время.
Итак. На Cortex-A9 (ядро в PS7) есть регистры общего назначения r0-r12, плюс sp, lr, pc.
Регистр |
Роль в boot-контексте |
|
Адрес следующей инструкции - куда “прыгнули” при handoff |
|
Указатель стека - без валидного стека C-код U-Boot не заработает |
|
По соглашению ARM могут передавать аргументы при вызове функции / передаче управления |
И оказалось что в экосистеме Linux для старых схем загрузки zImage иногда договаривались: r2 = указатель на Device Tree (DTB) в памяти, чтобы ядро Linux и U-Boot сразу знали где взять файл описания железа при загрузке. Отсюда есть стереотипная привычка “DTB в r2”.
На Zynq-7000 при цепочке BootROM → FSBL → U-Boot (SD):
FSBL не обязан класть в
r2адрес DTB;на практике после handoff
r2часто мусор (0, случайное значение);U-Boot, если ожидает DTB именно в
r2, попытается разобрать память по неверному адресу и зависнет ещё до строки U-Boot на UART.
И оказалось так, что при сборке U-Boot у меня был указан конфиг CONFIG_OF_BOARD=y , который говорит U-Boot, что его control DTB должен быть предоставлен board-specific кодом во время выполнения, например через board_fdt_blob_setup(). На некоторых ARM-платформах этот board-код может использовать сохраненный входной r2 как адрес DTB, если предыдущий загрузчик действительно передал DTB по ARM boot ABI.
То есть данная опция у меня должна была быть отключена, потому что FSBL ничего не кладет и оказывалось что U-Boot не получал указатель и вис. Ну, пришлось покопаться и посмотреть в каком участке кода происходит переход в бесконечный цикл и почему и потом понять что не так. Но это отдельная история с дебагом и прочими отладочными делами. Мораль - не ставьте в конфиге U-Boot опцию CONFIG_OF_BOARD=y, если при переходе не кладете в r2 адрес DTB.
Теперь перейдем понимая все это к сборке проекта FSBL.
Собираем FSBL
Проверяем что нужные файлы есть и запускаем Vitis:
# source /opt/xilinx/2025.2/Vitis/settings64.sh # which vitis bootgen /opt/xilinx/2025.2/Vitis/bin/vitis /opt/xilinx/2025.2/Vitis/bin/bootgen
Если вы уже выполняли действия из прошлой статьи то открываем директорию workspace. Иначе тогда:
Запустите Vitis Unified IDE (
vitis &);File → Switch Workspace… (или Open Workspace на стартовой странице);
Укажите новую директорию, например:
<repo>/vitis/workspace_linux(имя может быть любым; главное - не полагаться на чужой собранный workspace без вашего XSA).Open.
Создаем Platform Component из XSA:
File → New Component → Platform.
Component name:
zynq_mini_oled_platform.Next.
Create platform from hardware specification (XSA).
Browse → выберите файл, созданный в предыдущей статье:
<repo>/vivado/zynq_mini_oled.xsaНе используйте XSA из другого проекта или чужой машины.
-
Next:
OS: standalone
CPU:
ps7_cortexa9_0Domain:
standalone_ps7_cortexa9_0
Finish.
Vitis создаст platform и вложенный компонент zynq_fsbl (шаблон Xilinx) и теперь его необходимо собрать:

Дожидаемся BUILD SUCCESS (обычно 30-90 с) и запишем путь к FSBL (подставьте свой workspace) - он понадобится ниже для переменной FSBL_ELF для сборки:
# find vitis/ -name fsbl.elf 2>/dev/null vitis/zynq_mini_oled_platform/zynq_fsbl/build/fsbl.elf vitis/zynq_mini_oled_platform/export/zynq_mini_oled_platform/sw/boot/fsbl.elf
По умолчанию FSBL из Vitis собирается без отладочных макросов: на UART ничего не печатается, пока не стартует U-Boot. Если плата “молчит” между включением питания и строкой U-Boot, невозможно понять на каком этапе произошла проблема в загрузке. Для вывода таких сообщений рекомендую включить DEBUG_FSBL флаг при сборке - это самый быстрый способ понять, на каком этапе загрузка остановилась.
FSBL Xilinx для Zynq-7000 выводит диагностику через макрос fsbl_printf() (файл fsbl_debug.h в исходниках FSBL). Внутри, при включенном уровне, вызывается xil_printf() который выводит данные через драйвер UART PS. Можете напихать своего отладочного вывода, если потребуется :)
Есть два уровня отладочного вывода:
Режим |
Макрос при сборке |
Что появляется на UART |
По умолчанию |
(ничего) |
Полная тишина от FSBL (ошибки тоже не печатаются через |
Уровень 1 |
|
Баннер, режим загрузки, коды ошибок (DDR_INIT_FAIL, SD_INIT_FAIL, …), handoff |
Уровень 2 |
|
Всё из уровня 1 + детали: инициализация SD/DevCfg, адрес handoff, level shifters PL↔PS |
По можно увидеть вывод по всем стадиям:
видно, дошёл ли FSBL до инициализации DDR (ps7_init из вашего XSA);
какой boot mode выбрал BootROM (SD, JTAG, QSPI, …);
успешна ли инициализация SD и чтение разделов с карты;
загружается ли bitstream в PL (через DevCfg/PCAP);
какой адрес handoff передаётся следующей стадии (U-Boot в BOOT.BIN).
Итак. Разумеется мы включим его потому что проект наверняка может потребовать отладку. Для этого найдем проект в Vitis. FSBL живёт внутри platform-компонента (zynq_fsbl), а не в отдельном Application.
Включаем опцию через правку UserConfig.cmake вручную:
В Explorer / Components откройте: <workspace>/zynq_mini_oled_platform/Sources/zynq_fsbl/UserConfig.cmake
Включаем режим редактирования
Sourceчерез кнопку в заголовке;Найдите блок
USER_COMPILE_DEFINITIONS(в начале файла, секцияUSER SETTINGS).Замените пустую строку на один макрос (для полного лога - FSBL_DEBUG_INFO, не FSBL_DEBUG):
set(USER_COMPILE_DEFINITIONS "FSBL_DEBUG_INFO"). Если нужен только уровень 1 (баннер + ошибки, без SD Init Done), можно "FSBL_DEBUG". Не задавайте оба сразу - приFSBL_DEBUG+FSBL_DEBUG_INFOв коде Xilinx остаётся только уровень 1.Сохраните файл (Ctrl+S). Откройте
UserConfig.cmakeснова и убедитесь, что строка записана на диск.-
Clean + Build (важно именно пересобрать FSBL, не полагаться на «ничего не изменилось»):
правый клик
zynq_fsbl→ Clean (если есть);затем правый клик
zynq_mini_oled_platform→ Build (или Build на zynq_fsbl, если IDE даёт отдельную цель).
Другие способы у меня почему-то не взелетели, в т.ч. через установку констант при сборке. Проверьте сами =)
Проверить был ли эффект от изменений и собрался ли FSBL с нужными флагами просто:
# WORK=vitis/ # grep -h FSBL_DEBUG "${WORK}/zynq_mini_oled_platform/zynq_fsbl/build/compile_commands.json" | head -3 "command": "/opt/xilinx/2025.2/Vitis/gnu/aarch32/lin/gcc-arm-none-eabi/bin/arm-none-eabi-gcc -DFSBL_DEBUG_INFO -I/home/megalloid/devel/xilinx/projects/zynq_mini_oled/vitis/zynq_mini_oled_platform/zynq_fsbl/build/include -DSDT -mcpu=cortex-a9 -mfpu=vfpv3 -mfloat-abi=hard -MMD -MP -specs=/home/megalloid/devel/xilinx/projects/zynq_mini_oled/vitis/zynq_mini_oled_platform/zynq_fsbl/zynq_fsbl_bsp/Xilinx.spec -I/home/megalloid/devel/xilinx/projects/zynq_mini_oled/vitis/zynq_mini_oled_platform/zynq_fsbl/zynq_fsbl_bsp/include -o CMakeFiles/fsbl.elf.dir/fsbl_handoff.S.obj -c /home/megalloid/devel/xilinx/projects/zynq_mini_oled/vitis/zynq_mini_oled_platform/zynq_fsbl/fsbl_handoff.S", "command": "/opt/xilinx/2025.2/Vitis/gnu/aarch32/lin/gcc-arm-none-eabi/bin/arm-none-eabi-gcc -DFSBL_DEBUG_INFO -I/home/megalloid/devel/xilinx/projects/zynq_mini_oled/vitis/zynq_mini_oled_platform/zynq_fsbl/build/include -isystem /home/megalloid/devel/xilinx/projects/zynq_mini_oled/vitis/zynq_mini_oled_platform/zynq_fsbl/zynq_fsbl_bsp/include -isystem /opt/xilinx/2025.2/gnu/aarch32/lin/gcc-arm-none-eabi/x86_64-oesdk-linux/usr/lib/arm-xilinx-eabi/gcc/arm-xilinx-eabi/13.3.0/include -isystem /opt/xilinx/2025.2/gnu/aarch32/lin/gcc-arm-none-eabi/x86_64-oesdk-linux/usr/lib/arm-xilinx-eabi/gcc/arm-xilinx-eabi/13.3.0/include-fixed -isystem /opt/xilinx/2025.2/Vitis/gnu/aarch32/lin/gcc-arm-none-eabi/aarch32-xilinx-eabi/usr/include -DSDT -mcpu=cortex-a9 -mfpu=vfpv3 -mfloat-abi=hard -MMD -MP -specs=/home/megalloid/devel/xilinx/projects/zynq_mini_oled/vitis/zynq_mini_oled_platform/zynq_fsbl/zynq_fsbl_bsp/Xilinx.spec -I/home/megalloid/devel/xilinx/projects/zynq_mini_oled/vitis/zynq_mini_oled_platform/zynq_fsbl/zynq_fsbl_bsp/include -Wall -Wextra -O0 -g3 -U__clang__ -o CMakeFiles/fsbl.elf.dir/fsbl_hooks.c.obj -c /home/megalloid/devel/xilinx/projects/zynq_mini_oled/vitis/zynq_mini_oled_platform/zynq_fsbl/fsbl_hooks.c", "command": "/opt/xilinx/2025.2/Vitis/gnu/aarch32/lin/gcc-arm-none-eabi/bin/arm-none-eabi-gcc -DFSBL_DEBUG_INFO -I/home/megalloid/devel/xilinx/projects/zynq_mini_oled/vitis/zynq_mini_oled_platform/zynq_fsbl/build/include -isystem /home/megalloid/devel/xilinx/projects/zynq_mini_oled/vitis/zynq_mini_oled_platform/zynq_fsbl/zynq_fsbl_bsp/include -isystem /opt/xilinx/2025.2/gnu/aarch32/lin/gcc-arm-none-eabi/x86_64-oesdk-linux/usr/lib/arm-xilinx-eabi/gcc/arm-xilinx-eabi/13.3.0/include -isystem /opt/xilinx/2025.2/gnu/aarch32/lin/gcc-arm-none-eabi/x86_64-oesdk-linux/usr/lib/arm-xilinx-eabi/gcc/arm-xilinx-eabi/13.3.0/include-fixed -isystem /opt/xilinx/2025.2/Vitis/gnu/aarch32/lin/gcc-arm-none-eabi/aarch32-xilinx-eabi/usr/include -DSDT -mcpu=cortex-a9 -mfpu=vfpv3 -mfloat-abi=hard -MMD -MP -specs=/home/megalloid/devel/xilinx/projects/zynq_mini_oled/vitis/zynq_mini_oled_platform/zynq_fsbl/zynq_fsbl_bsp/Xilinx.spec -I/home/megalloid/devel/xilinx/projects/zynq_mini_oled/vitis/zynq_mini_oled_platform/zynq_fsbl/zynq_fsbl_bsp/include -Wall -Wextra -O0 -g3 -U__clang__ -o CMakeFiles/fsbl.elf.dir/image_mover.c.obj -c /home/megalloid/devel/xilinx/projects/zynq_mini_oled/vitis/zynq_mini_oled_platform/zynq_fsbl/image_mover.c",
В флагах сборки должен быть FSBL_DEBUG.
Внутри fsbl.elf должны быть отладочные сообщения:
# ELF="${WORK}/zynq_mini_oled_platform/zynq_fsbl/build/fsbl.elf" # ls -l "$ELF" # strings "$ELF" | grep -E 'Boot mode|First Stage Boot Loader|SD Init' -rwxrwxr-x 1 megalloid megalloid 340636 Jun 3 03:10 vitis//zynq_mini_oled_platform/zynq_fsbl/build/fsbl.elf Xilinx First Stage Boot Loader Boot mode is QSPI Boot mode is NOR Boot mode is SD SD Init Done Boot mode is JTAG
Порядок строк при выводе отладочной информации может слегка отличаться, но логика одна:
Xilinx First Stage Boot Loader Release 2025.2 May 17 2026-23:35:47 Devcfg driver initialized Silicon Version 3.1 Boot mode is JTAG === ZYNQ MINI OLED demo (PS+PL build) === I2C base = 0x43C00000, PRESCALE = 30 OLED init OK
Так. Сборка FSBL как компонента платформы да с дебаг выводом - на этом закончена. Можно переходить к следующему этапу и соберем в buildroot основные компоненты: U-Boot, DTS/DTB, RootFS, Linux Kernel. А для этого всего надо организовать иерархию директорий, нашпиговать это все необходимыми файлами и конфигами. Приступим.
Сборка Linux с нуля через Buildroot
Цель данной большой главы - объяснить как все настроить и как все работает, и после - получить на выходе uImage, zynq-mini-revb.dtb, u-boot и заготовку sdcard.img.

Создаем директорию в которой будет находиться все необходимое:
export WORK=/home/megalloid/devel/xilinx/projects/zynq_mini_oled/linux/ mkdir -p "$WORK" cd "$WORK"
Клонируем LTS-ветку Buildroot с предсказуемыми версиями Linux 6.6.x и U-Boot 2024.01, проверенными временем на Zynq-7000:
git clone --branch 2024.02.7 --depth 1 \ https://gitlab.com/buildroot.org/buildroot.git buildroot-2024.02.7
Задаем переменные для указания места хранения buildroot-репозитория, с местом хранения результатов сборки:
export BR_SRC="$WORK/buildroot-2024.02.7" export BR_OUT="$WORK/br-output" mkdir -p "$BR_OUT"
Buildroot позволяет вынести данные отдельного проекта в отдельное дерево BR2_EXTERNAL. Но перед созданием файлов имеет смысл зафиксировать назначение каталогов. Вынесенное дерево BR2_EXTERNAL используется для хранения всего, что относится не к самому Buildroot, а к конкретному проекту: описания платы, defconfig, overlay rootfs, пользовательских пакетов, патчей и вспомогательных скриптов. В результате структура проекта будет выглядеть следующим образом:
zynq-mini-i2c/ ├── buildroot-2024.02.7/ # Клонированный репозиторий Buildroot ├── br-output/ # Каталог результатов сборки └── board-support/ # Внешнее дерево BR2_EXTERNAL ├── external.desc # Описание внешнего дерева Buildroot ├── external.mk # Подключение make-рецептов из package/ ├── Config.in # Подключение Kconfig-описаний для menuconfig ├── configs/ # defconfig-файлы целевых плат │ └── zynq_mini_i2c_defconfig ├── board/ # Файлы, специфичные для платы │ └── zynq-mini-i2c/ │ ├── post-build.sh # Действия после сборки rootfs │ ├── post-image.sh # Действия после формирования образов │ ├── genimage.cfg # Описание структуры итогового носителя │ └── rootfs_overlay/ # Файлы, добавляемые в rootfs поверх сборки └── package/ # Пользовательские пакеты проекта └── i2c-master-axi/ ├── Config.in # Kconfig-описание пакета ├── i2c-master-axi.mk └── src/ # Исходный код пользовательского приложения
Смысл такого разделения в том, чтобы не изменять исходное дерево Buildroot. Сам Buildroot остается внешней базовой системой сборки, а все проектные изменения хранятся отдельно в board-support/. Это упрощает обновление Buildroot, перенос проекта на другую рабочую машину и воспроизводимость сборки.
Каталог configs/ содержит готовые конфигурации плат. Именно отсюда Buildroot берет zynq_mini_i2c_defconfig при вызове команды вида:
make BR2_EXTERNAL=$BR2_EXTERNAL_ZYNQ_MINI_I2C_PATH zynq_mini_i2c_defconfig
Каталог board/ содержит файлы, связанные с конкретной аппаратной платформой. Здесь размещаются скрипты постобработки, overlay для корневой файловой системы, конфигурация разметки SD-карты или eMMC, а также дополнительные файлы, которые нужны именно этой плате.
Каталог package/ используется для добавления собственных пакетов в Buildroot. В данном случае там будет размещен пакет i2c-master-axi, который собирает пользовательское приложение для работы с AXI I2C master. После подключения через external.mk этот пакет становится доступен в menuconfig и может быть включен в итоговый rootfs через defconfig.
Три файла верхнего уровня в board-support/ нужны Buildroot для регистрации и подключения внешнего дерева:
external.desc. Используется при старте Buildroot, до сборки. Задает имя дерева → переменная
BR2_EXTERNAL_ZYNQ_MINI_I2C_PATH;external.mk. Используется на фазе
make(подключение рецептов). Основная роль отыгрывается при сборке: подключает всеpackage/*/*.mk;Config.in. Задает содержание для menuconfig/defconfig.
Без этих файлов Buildroot увидит только основное дерево сборки и не сможет найти проектные defconfig, board-файлы и пользовательский пакет i2c-master-axi.
Далее создаем минимальную структуру внешнего дерева:
export BR_EXT="$WORK/board-support" mkdir -p "$BR_EXT"/{configs,board/zynq_mini_revb,dts,package/i2c-master-axi}
После этого можно переходить к созданию служебных файлов external.desc, external.mk и Config.in. Производим их наполнение.
Первый файл external.desc - для регистрации дерева:
cat > "$BR_EXT/external.desc" << 'EOF' name: ZYNQ_MINI_I2C desc: ZYNQ MINI Rev B + custom AXI I2C (manual BR2_EXTERNAL) EOF
В файл заносятся две строки в формате Buildroot. Поле name: критично т.к. из него Buildroot делает имя переменной BR2_EXTERNAL_<NAME>_PATH → у нас BR2_EXTERNAL_ZYNQ_MINI_I2C_PATH (верхний регистр, подчёркивания). Эту переменную дальше используют defconfig, i2c-master-axi.mk и пути к board/, dts/.
Второй файл external.mk для подключение makefile-рецептов:
cat > "$BR_EXT/external.mk" << 'EOF' include $(sort $(wildcard $(BR2_EXTERNAL_ZYNQ_MINI_I2C_PATH)/package/*/*.mk)) EOF
Одна строка сообщает "возьми все *.mk из подкаталогов package/и подключи их к общей системе сборки". Без неё файл package/i2c-master-axi/i2c-master-axi.mk не участвует в сборке, даже если лежит на диске.
Третий файл Config.in - задаем пункты в меню конфигурации:
cat > "$BR_EXT/Config.in" << 'EOF' menu "ZYNQ MINI Rev B (manual)" source "$BR2_EXTERNAL_ZYNQ_MINI_I2C_PATH/package/i2c-master-axi/Config.in" endmenu EOF
Создает корневое меню Kconfig для пакетов. Строка source .../i2c-master-axi/Config.in добавляет в menuconfig опцию BR2_PACKAGE_I2C_MASTER_AXI (включить модуль i2c-master-axi в rootfs). В zynq_mini_revb_defconfig позже ставят BR2_PACKAGE_I2C_MASTER_AXI=y.
Итак. Структуру каталогов мы создали. Теперь перейдем к их наполнению и начнем с более детального разбора и подготовки Device Tree.
Device Tree - ключ к железу платы
Device Tree здесь выступает не просто как еще один конфигурационный файл в проекте. Это описание того, какое железо реально существует на плате, по каким адресам оно доступно, какие драйверы должны быть к нему привязаны и какие параметры нужны ядру Linux для корректной инициализации.
На классическом ПК значительную часть этой работы берет на себя BIOS/UEFI: он обнаруживает устройства, готовит таблицы ACPI, описывает память, шины, контроллеры и передает операционной системе уже достаточно полную картину платформы. В мире embedded ARM, особенно на таких SoC как Zynq-7000, такой универсальной автоконфигурации обычно нет. Ядру Linux нужно заранее дать карту аппаратной платформы. Эту роль и выполняет Device Tree.
В нашем случае эта карта должна описать как минимум несколько уровней железа:
SoC Zynq-7000 ├── процессорная система PS7 │ ├── DDR memory │ ├── UART │ ├── SDIO │ ├── USB │ └── Ethernet ├── AXI-шина между PS и PL │ └── i2c-master-axi @ 0x43C00000 └── внешние устройства на I2C └── OLED SSD1307FB @ 0x3c
Здесь важно разделить две разные сущности.
Первая сущность - сам контроллер i2c-master-axi. Это IP-блок в программируемой логике PL, подключенный к процессорной системе через AXI. Для Linux он выглядит как memory-mapped устройство: у него есть базовый адрес, диапазон регистров, совместимая строка compatible и, при необходимости, interrupt, clock и дополнительные параметры.
Вторая сущность - OLED-дисплей ssd1307fb, который висит уже не на AXI, а на I2C-шине, созданной этим контроллером. У дисплея нет собственного адреса в памяти процессора. Его адрес - это I2C slave address 0x3c. Поэтому он должен быть дочерним узлом внутри узла I2C-контроллера.
Логика получается следующая:
Linux видит узел i2c-master-axi в Device Tree ↓ по compatible подбирает драйвер I2C-контроллера ↓ драйвер регистрирует новую I2C-шину ↓ ядро видит дочерний узел oled@3c ↓ по compatible подбирает драйвер ssd1307fb ↓ драйвер OLED начинает работать через созданную I2C-шину
Именно поэтому в Device Tree нельзя просто где-то указать "у меня есть OLED". Нужно правильно описать путь доступа к нему. Если OLED подключен к I2C-контроллеру, то в дереве он должен находиться внутри узла этого контроллера. Если контроллер находится в PL и подключен через AXI, то он должен быть описан как устройство на AXI/MMIO с корректным адресом из Address Editor в Vivado.
Общий флоу работы с Device Tree выглядит так:

Файл .dts - это исходник, который мы редактируем вручную. В нем задаются узлы, свойства, адреса, compatible-строки и связи между устройствами.
Перед компиляцией файл проходит через cpp. Это нужно для обработки #include, макросов и заголовков dt-bindings. Поэтому в .dts можно подключать общие описания SoC, например zynq-7000.dtsi, а также использовать символьные константы вместо части числовых значений.
После препроцессинга файл попадает в dtc - Device Tree Compiler. Он превращает текстовое описание .dts в бинарный blob .dtb.
Именно .dtb, а не .dts, попадает на SD-карту или внутрь boot-образа. Дальше U-Boot загружает ядро Linux и передает ему указатель на этот бинарный Device Tree. Ядро при старте обходит дерево, создает platform devices, инициализирует шины и запускает процедуру probe для подходящих драйверов.
Если описание корректное, драйвер i2c-master-axi получит свой MMIO-регион по адресу 0x43C00000, создаст I2C-шину, а драйвер ssd1307fb найдет на ней устройство с адресом 0x3c.
Если описание ошибочное, возможны типовые симптомы:
ошибка в compatible -> драйвер не привяжется к устройству;
ошибка в reg -> драйвер получит неверный адрес регистров;
ошибка в #address-cells -> дочерние устройства будут разобраны некорректно;
ошибка в I2C-адресе -> OLED не ответит на probe;
ошибка в иерархии узлов -> устройство окажется не на той шине;
отсутствует status = "okay" -> узел может остаться отключенным.
Поэтому Device Tree лучше воспринимать не как набор случайных параметров, а как формальную схему связности платы. Каждое значение в нем должно иметь источник: схема платы, настройки PS7 в Vivado, Address Editor, документация на IP-блок, datasheet внешнего устройства или binding-документация Linux.
Для нашего проекта источники значений такие:
zynq-7000.dtsi - базовое описание SoC Zynq-7000: CPU, interrupt controller, clocks, UART, SDIO, USB, Ethernet и другие стандартные узлы PS;
Vivado PS7 configuration - включенные интерфейсы PS, настройки DDR, UART, SDIO, USB, Ethernet;
Vivado Address Editor - адресное пространство AXI-устройств в PL, в частности базовый адрес i2c-master-axi 0x43C00000;
схема платы - какие интерфейсы реально выведены, куда подключен OLED, какие линии используются;
datasheet OLED SSD1306 - I2C-адрес, тип контроллера, параметры дисплея;
Linux bindings - какие свойства ожидают драйверы, какие compatible-строки допустимы, как должны называться узлы и дочерние устройства.
После этого можно переходить к синтаксису самого .dts: узлам, свойствам, меткам, адресам и ссылкам между частями дерева. Итак. Составим файл zynq-mini-revb.dts с нуля.
Синтаксис Device Tree
Для старта разберем базовый синтаксис Device Tree: узлы, свойства, метки, ссылки и правила записи адресов.
Device Tree - это не C и не JSON. Это древовидное описание аппаратной платформы. В нем есть узлы, которые обычно соответствуют устройствам, контроллерам, шинам или логическим блокам SoC, и свойства этих узлов в формате key = value.
Минимальный пример выглядит так:
uart1: serial@e0001000 { compatible = "xlnx,xuartps"; reg = <0xe0001000 0x1000>; status = "okay"; };
В этом примере описан UART-контроллер по адресу 0xe0001000. Ядро Linux по строке compatible подберет драйвер, по reg поймет, где находится окно регистров устройства, а по status = "okay" увидит, что узел должен быть включен.
Краткое резюме по основным конструкциям:
Конструкция |
Пример |
Смысл |
|---|---|---|
Узел |
|
Устройство, шина, контроллер или логический блок |
Имя узла |
|
Человекочитаемое имя и адрес после |
Метка |
|
Имя для ссылок на узел через |
Свойство |
|
Пара ключ-значение |
Строка |
|
Строковое значение в кавычках |
Число |
|
Одно 32-битное значение |
Массив чисел |
|
Несколько 32-битных ячеек подряд |
Пустое свойство |
|
Булевый флаг без значения |
Phandle-ссылка |
|
Ссылка на другой узел |
Include |
|
Подключение внешнего |
Доработка узла |
|
Изменение или дополнение уже описанного узла |
Отдельно важно понять свойства #address-cells и #size-cells. Они определяют, как читать свойство reg у дочерних узлов. Свойство reg обычно описывает адрес устройства и размер его адресного окна. Но количество 32-битных ячеек для адреса и размера зависит от родительской шины.
Для корня дерева или AXI/MMIO-устройств Zynq часто используется такая схема:
#address-cells = <1>; #size-cells = <1>;
Это означает:
reg = <адрес размер>;
Например:
reg = <0x43c00000 0x1000>;
Здесь 0x43c00000 - базовый адрес IP-блока в адресном пространстве процессора, а 0x1000 - размер окна регистров.
Для I2C-шины логика другая. У подчиненного I2C-устройства нет MMIO-окна в памяти процессора. У него есть только адрес на I2C-шине. Поэтому для дочерних устройств I2C обычно задают:
#address-cells = <1>; #size-cells = <0>;
Тогда OLED-дисплей с адресом 0x3c описывается так:
oled@3c { compatible = "solomon,ssd1307fb"; reg = <0x3c>; };
Здесь reg = <0x3c> - это не адрес в памяти ARM-процессора, а 7-битный адрес устройства на I2C-шине.
Самое важное свойство для привязки драйвера - compatible.
compatible = "user,i2c-master-axi-1.0";
По этому полю ядро Linux ищет подходящий драйвер. В драйвере должна быть таблица совместимости, например:
static const struct of_device_id i2c_master_axi_of_match[] = { { .compatible = "user,i2c-master-axi-1.0" }, { } };
Если строка в Device Tree не совпадает со строкой в of_match_table[], функция probe() не будет вызвана. При этом остальные параметры могут быть правильными: адрес, размер окна, interrupt, clocks. Но без совпадения compatible ядро не свяжет узел с драйвером.
Строки compatible обычно указывают от более специфичной к более общей:
compatible = "vendor,device-rev1", "vendor,device";
Сначала ядро пытается найти наиболее точное совпадение, затем более общее.
Теперь сведем основные значения для нашего .dts в одну таблицу.
Параметр в DTS |
Значение |
Источник и смысл |
Базовый адрес IP |
|
Vivado Address Editor. DTS сообщает ядру, что по этому адресу находится AXI I2C-контроллер |
Размер окна |
|
Vivado Address Editor, поле Range. Для простого IP-блока хватает окна 4 KiB |
FCLK0 |
|
PS7 configuration в Vivado. Частота тактирования логики PL |
|
|
Binding для Zynq clock controller. Индекс |
|
|
Частота входного тактирования IP-блока в герцах |
|
|
Желаемая частота SCL. |
IRQ в DTS |
|
Формат ARM GIC: |
DDR size |
|
512 MiB. Размер памяти платы |
UART |
|
Включенный UART PS7, выведенный через MIO 48/49 |
SD |
|
SDIO-контроллер PS7, обычно MIO 40..45 |
Ethernet PHY |
|
MDIO-адрес PHY, например RTL8211E на адресе 1 |
OLED I2C address |
|
Даташит или схема модуля OLED. Для SSD1306/SSD1307 часто |
OLED geometry |
|
Параметры OLED-модуля из даташита или описания платы |
|
второй раздел SD |
Разметка образа. Например |
Пины OLED |
|
XDC-файл, схема платы, Vivado constraints. В DTS обычно не указываются |
Для interrupt полезно отдельно расшифровать запись:
interrupts = <0 29 4>;
Здесь первая ячейка 0 означает тип линии interrupt. Для ARM GIC значение 0 соответствует SPI, то есть Shared Peripheral Interrupt.
Вторая ячейка 29 - номер interrupt в формате Device Tree. Если в Vivado fabric interrupt подключен как IRQ_F2P[0], а в GIC он соответствует SPI 61, то в Device Tree пишется 61 - 32 = 29. Первые 32 номера зарезервированы под SGI и PPI внутри CPU.
Третья ячейка 4 - тип срабатывания. Если подключить заголовок:
#include <dt-bindings/interrupt-controller/irq.h>
то вместо числа можно использовать более читаемую форму:
interrupts = <0 29 IRQ_TYPE_LEVEL_HIGH>;
Это означает прерывание по уровню, активный высокий уровень. Такой вариант соответствует типичному выходу irq_o у простого AXI IP-блока.
Итоговое правило простое: в Device Tree не должно быть "магических" чисел без источника. Адреса берутся из Vivado Address Editor, clocks - из PS7 configuration и clock bindings, interrupt - из схемы подключения PL к GIC, I2C-адреса - из даташита устройства, а параметры boot-разделов - из конфигурации образа genimage.
Создадим файл zynq-mini-revb.dts и сразу заложим в него полное описание платы: базовый SoC Zynq-7000, включенную PS-периферию, память DDR, QSPI flash, USB, Ethernet, а также наш пользовательский I2C-контроллер в PL и OLED-дисплей на его I2C-шине.
mkdir -p "$BR_EXT/dts" cat > "$BR_EXT/dts/zynq-mini-revb.dts" << 'EOF' /dts-v1/; #include "xilinx/zynq-7000.dtsi" #include <dt-bindings/gpio/gpio.h> #include <dt-bindings/interrupt-controller/irq.h> / { model = "ZYNQ MINI Rev B (manual Linux port)"; compatible = "user,zynq-mini-revb", "xlnx,zynq-7000"; aliases { ethernet0 = &gem0; i2c0 = &i2c_pl; serial0 = &uart1; spi0 = &qspi; }; chosen { bootargs = "console=ttyPS0,115200 earlycon fbcon=font:MINI4x6 root=/dev/mmcblk0p2 rootwait rw"; stdout-path = "serial0:115200n8"; }; memory@0 { device_type = "memory"; reg = <0x0 0x20000000>; }; }; &uart1 { status = "okay"; }; &sdhci0 { status = "okay"; bus-width = <4>; broken-cd; disable-wp; }; &gem0 { status = "okay"; phy-mode = "rgmii-id"; phy-handle = <ðernet_phy>; ethernet_phy: ethernet-phy@1 { reg = <1>; device_type = "ethernet-phy"; }; }; &usb0 { status = "okay"; dr_mode = "host"; usb-phy = <&usb_phy0>; }; &qspi { status = "okay"; is-dual = <0>; num-cs = <1>; flash@0 { compatible = "winbond,w25q128", "jedec,spi-nor"; reg = <0>; spi-max-frequency = <100000000>; }; }; &clkc { ps-clk-frequency = <33333333>; fclk-enable = <0xf>; }; / { usb_phy0: usb-phy { compatible = "usb-nop-xceiv"; #phy-cells = <0>; }; amba_pl: amba_pl@0 { compatible = "simple-bus"; #address-cells = <1>; #size-cells = <1>; ranges; i2c_pl: i2c@43c00000 { compatible = "user,i2c-master-axi-1.0"; reg = <0x43c00000 0x1000>; interrupt-parent = <&intc>; interrupts = <0 29 IRQ_TYPE_LEVEL_HIGH>; clocks = <&clkc 15>; clock-names = "axi"; clock-frequency = <100000>; input-clock-frequency = <50000000>; #address-cells = <1>; #size-cells = <0>; ssd1306: oled@3c { compatible = "solomon,ssd1306fb-i2c"; reg = <0x3c>; solomon,height = <64>; solomon,width = <128>; solomon,page-offset = <0>; solomon,com-invdir; solomon,prechargep1 = <2>; solomon,prechargep2 = <2>; }; }; }; }; EOF
Теперь разберем этот файл по частям.
Первая строка обязательна для исходного файла Device Tree:
/dts-v1/;
Она сообщает компилятору dtc, что файл написан в формате Device Tree Source версии 1.
Далее подключается базовое описание SoC:
#include "xilinx/zynq-7000.dtsi"
Файл zynq-7000.dtsi содержит общее описание процессорной системы Zynq-7000: CPU, interrupt controller, clock controller, UART, SDIO, USB, Ethernet, QSPI и другую стандартную периферию PS7. Большая часть этих узлов в базовом .dtsi обычно находится в состоянии status = "disabled". В нашем .dts мы не создаем их заново, а включаем и дополняем только те блоки, которые реально используются на плате.
В Linux 6.x этот файл находится в дереве ядра по пути:
arch/arm/boot/dts/xilinx/zynq-7000.dtsi
Так как Buildroot копирует наш .dts в общий каталог Device Tree, путь указывается как:
#include "xilinx/zynq-7000.dtsi"
Следующие include-файлы подключают символьные константы:
#include <dt-bindings/gpio/gpio.h> #include <dt-bindings/interrupt-controller/irq.h>
Первый нужен для GPIO-флагов, второй - для читаемых имен типов прерываний. Например, вместо числа 4 можно писать:
IRQ_TYPE_LEVEL_HIGH
Это делает DTS более понятным и снижает риск ошибки при чтении.
Далее объявляется корневой узел платы:
/ { model = "ZYNQ MINI Rev B (manual Linux port)"; compatible = "user,zynq-mini-revb", "xlnx,zynq-7000"; ... };
Свойство model - человекочитаемое имя платформы. После загрузки его можно посмотреть в Linux:
cat /proc/device-tree/model
Свойство compatible задает список совместимости платформы. Первая строка описывает конкретную плату:
"user,zynq-mini-revb"
Вторая строка указывает семейство SoC:
"xlnx,zynq-7000"
Это дает ядру и платформенным драйверам возможность сопоставлять устройство как с конкретной платой, так и с общим классом Zynq-7000.
Блок aliases задает короткие имена для важных устройств:
aliases { ethernet0 = &gem0; i2c0 = &i2c_pl; serial0 = &uart1; spi0 = &qspi; };
Alias не создает новое устройство. Он только дает устойчивое имя для уже существующего узла. Например:
serial0 = &uart1;
означает, что serial0 ссылается на UART1. Далее это имя используется в stdout-path:
stdout-path = "serial0:115200n8";
Блок chosen задает параметры, которые передаются ядру Linux при старте:
chosen { bootargs = "console=ttyPS0,115200 earlycon fbcon=font:MINI4x6 root=/dev/mmcblk0p2 rootwait rw"; stdout-path = "serial0:115200n8"; };
Основные параметры bootargs:
Токен |
Смысл |
Источник |
|---|---|---|
|
Консоль ядра через UART PS |
UART1, скорость 115200 |
|
Ранний вывод до полной инициализации UART |
Удобно для отладки загрузки |
|
Мелкий шрифт framebuffer-консоли |
Требует поддержку |
|
Корневая файловая система на втором разделе SD |
Разметка |
|
Ждать появления root-устройства |
SD-карта может появиться не сразу |
|
Монтировать rootfs в режиме read-write |
Удобно на этапе разработки |
Секция memory@0 описывает DDR:
memory@0 { device_type = "memory"; reg = <0x0 0x20000000>; };
Свойство reg задает начало и размер памяти:
reg = <0x0 0x20000000>;
Здесь:
0x0 - начальный адрес DDR; 0x20000000 - размер памяти, то есть 512 MiB.
Для памяти 512 MiB значение считается так:
0x20000000 = 536870912 байт = 512 MiB
Свойство device_type = "memory" считается устаревшим, но часто сохраняется ради совместимости с загрузчиками и парсерами.
Далее идут патчи к узлам, которые уже описаны в zynq-7000.dtsi. Синтаксис вида:
&uart1 { status = "okay"; };
не создает новый узел. Он находит существующий узел с меткой uart1 и добавляет или изменяет его свойства.
UART1 включается одной строкой:
&uart1 { status = "okay"; };
После этого драйвер UART активируется, и в Linux появляется консольное устройство, обычно /dev/ttyPS0.
SD-контроллер включается так:
&sdhci0 { status = "okay"; bus-width = <4>; broken-cd; disable-wp; };
Смысл свойств:
Свойство |
Смысл |
|
Включить SDIO0 |
|
Использовать 4-битный режим SD |
|
Нет рабочей линии Card Detect |
|
Нет линии Write Protect |
Без broken-cd драйвер может считать, что карта отсутствует, если на плате не разведена линия обнаружения карты.
Ethernet описывается через связку MAC и PHY:
&gem0 { status = "okay"; phy-mode = "rgmii-id"; phy-handle = <ðernet_phy>; ethernet_phy: ethernet-phy@1 { reg = <1>; device_type = "ethernet-phy"; }; };
На плате Ethernet состоит из двух частей:
GEM0 в PS7 - MAC-контроллер внутри Zynq; RTL8211E - внешний PHY рядом с RJ45; MDIO/MDC - управляющая шина между MAC и PHY; RGMII - шина данных между MAC и PHY.
Свойство phy-handle связывает MAC с конкретным PHY-узлом:
phy-handle = <ðernet_phy>;
А дочерний узел PHY задает адрес микросхемы на MDIO-шине:
ethernet_phy: ethernet-phy@1 { reg = <1>; };
Значение reg = <1> - это не произвольный номер. Это MDIO-адрес PHY, заданный аппаратно, например через strapping-пины PHY_ADx на схеме платы.
Свойство:
phy-mode = "rgmii-id";
указывает режим интерфейса RGMII с внутренними задержками. Это должно соответствовать схеме платы и настройкам PHY.
USB включается следующим блоком:
&usb0 { status = "okay"; dr_mode = "host"; usb-phy = <&usb_phy0>; };
Здесь dr_mode = "host" означает, что USB работает как host-порт для внешних устройств: клавиатуры, флешки, USB-UART и т.д.
Ниже создается простой PHY-узел:
usb_phy0: usb-phy { compatible = "usb-nop-xceiv"; #phy-cells = <0>; };
usb-nop-xceiv - это "пустой" PHY-драйвер. Для Zynq такой подход часто используется, когда внешний ULPI/USB PHY не требует отдельной сложной настройки из Device Tree.
QSPI flash включается так:
&qspi { status = "okay"; is-dual = <0>; num-cs = <1>; flash@0 { compatible = "winbond,w25q128", "jedec,spi-nor"; reg = <0>; spi-max-frequency = <100000000>; }; };
Смысл свойств:
Свойство |
Значение |
Смысл |
|
включено |
Активировать QSPI-контроллер |
|
0 |
Один flash-чип, не dual-stack |
|
1 |
Один chip select |
|
CS0 |
Flash подключена к первому chip select |
|
|
Драйвер spi-nor распознает flash по JEDEC ID |
|
0 |
Номер chip select |
|
100 MHz |
Верхняя частота SPI-шины |
При необходимости этот блок можно расширить: добавить spi-rx-bus-width, spi-tx-bus-width и разметку flash-разделов. В минимальном варианте достаточно описать сам чип.
Далее задается clock controller PS7:
&clkc { ps-clk-frequency = <33333333>; fclk-enable = <0xf>; };
ps-clk-frequency - частота входного тактового сигнала PS. В данном случае это 33.333 MHz.
fclk-enable - битовая маска включенных fabric clocks:
бит 0 - FCLK0 бит 1 - FCLK1 бит 2 - FCLK2 бит 3 - FCLK3
Значение:
fclk-enable = <0xf>;
означает, что объявлены включенными FCLK0...FCLK3. Если реально используется только FCLK0, технически достаточно:
fclk-enable = <0x1>;
Ниже добавляется контейнер для устройств в программируемой логике PL:
amba_pl: amba_pl@0 { compatible = "simple-bus"; #address-cells = <1>; #size-cells = <1>; ranges; ... };
simple-bus - это пассивный контейнер. У него нет собственного сложного драйвера. Ядро просто проходит по его дочерним узлам и создает устройства для них.
Свойства:
Элемент |
Смысл |
|
Контейнер для MMIO-устройств |
|
Адрес дочернего устройства занимает одну 32-битную ячейку |
|
Размер окна занимает одну 32-битную ячейку |
|
Адреса дочерних устройств напрямую совпадают с адресным пространством CPU |
На Zynq PL-устройства, подключенные через AXI GP, часто описывают именно внутри такого AMBA/simple-bus контейнера. Формально AXI I2C-контроллер можно было бы поместить сразу под корень /, но отдельный amba_pl лучше отражает аппаратную структуру: это блок в PL, подключенный к PS через AXI.
Главный пользовательский узел в этом файле - наш AXI I2C-контроллер:
i2c_pl: i2c@43c00000 { compatible = "user,i2c-master-axi-1.0"; reg = <0x43c00000 0x1000>; interrupt-parent = <&intc>; interrupts = <0 29 IRQ_TYPE_LEVEL_HIGH>; clocks = <&clkc 15>; clock-names = "axi"; clock-frequency = <100000>; input-clock-frequency = <50000000>; #address-cells = <1>; #size-cells = <0>; ... };
Имя узла:
i2c@43c00000
указывает тип устройства и его базовый адрес. Адрес после @ должен совпадать с первым значением в reg:
reg = <0x43c00000 0x1000>;
Значение 0x43c00000 берется из Vivado Address Editor. Это базовый адрес IP-блока в адресном пространстве ARM-процессора.
Значение 0x1000 - размер окна регистров. В драйвере это окно будет отображено в виртуальную память ядра через вызов вроде:
devm_platform_ioremap_resource()
После этого драйвер сможет обращаться к регистрам IP-блока по смещениям от базового адреса: CTRL, STATUS, TX, RX, PRESCALE и т.д.
Строка:
compatible = "user,i2c-master-axi-1.0";
должна буквально совпадать со строкой в of_match_table драйвера i2c-master-axi.c.
Например:
static const struct of_device_id i2c_master_axi_of_match[] = { { .compatible = "user,i2c-master-axi-1.0" }, { } };
Если в compatible будет опечатка, модуль ядра может загрузиться, но probe() не вызовется, I2C-адаптер не зарегистрируется, и устройства на этой шине не появятся.
Прерывание задается строками:
interrupt-parent = <&intc>; interrupts = <0 29 IRQ_TYPE_LEVEL_HIGH>;
interrupt-parent = <&intc> говорит, что прерывание подключено к interrupt controller Zynq, то есть к GIC.
Формат interrupts для ARM GIC:
Ячейка |
Значение |
Смысл |
0 |
|
Тип прерывания: SPI |
1 |
|
Номер SPI в формате Device Tree |
2 |
|
Активный высокий уровень |
Расчет для IRQ_F2P[0]:
Vivado: irq_o -> PS7 IRQ_F2P[0] UG585: первый fabric IRQ = GIC interrupt ID 61 Device Tree SPI number = 61 - 32 = 29
Если использовать IRQ_F2P[1], то номер был бы 30. Если прерывание в hardware design не подключено, свойство interrupts можно убрать, при условии что драйвер поддерживает polling mode.
Clock-связь с PS7 задается так:
clocks = <&clkc 15>; clock-names = "axi";
&clkc - это clock controller из zynq-7000.dtsi.
Индекс 15 соответствует FCLK_CLK0 в clock binding для Zynq. Этот clock подается из PS в PL и тактирует AXI-логику.
clock-names = "axi" задает имя clock-ресурса. Драйвер может получить его так:
devm_clk_get(dev, "axi")
или использовать упрощенную схему, если частота передается отдельным числом.
Частоты для I2C-контроллера заданы двумя свойствами:
clock-frequency = <100000>; input-clock-frequency = <50000000>;
Их смысл:
Свойство |
Значение |
Кто использует |
|
50000000 |
Драйвер IP-блока, входная частота AXI/FCLK0 |
|
100000 |
Драйвер IP-блока, целевая частота SCL |
На основе этих значений драйвер рассчитывает делитель PRESCALE для получения I2C SCL около 100 kHz.
Внутри i2c_pl задан формат адресации дочерних устройств:
#address-cells = <1>; #size-cells = <0>;
Это означает, что дочерние устройства на этой шине имеют только адрес, без размера окна памяти.
Для I2C это корректно: у OLED-дисплея нет адресного диапазона в памяти CPU. У него есть только 7-битный адрес на I2C-шине.
Поэтому дочерний OLED описан так:
ssd1306: oled@3c { compatible = "solomon,ssd1306fb-i2c"; reg = <0x3c>; solomon,height = <64>; solomon,width = <128>; solomon,page-offset = <0>; solomon,com-invdir; solomon,prechargep1 = <2>; solomon,prechargep2 = <2>; };
Смысл свойств:
Свойство |
Смысл |
|
Привязка к драйверу |
|
7-битный I2C-адрес OLED |
|
Высота панели |
|
Ширина панели |
|
Смещение страниц памяти дисплея |
|
Направление COM scan |
|
Параметры pre-charge |
Адрес 0x3c берется из схемы модуля или даташита. Для SSD1306/SSD1307 типичны два варианта:
SA0 = 0 -> 0x3c SA0 = 1 -> 0x3d
После загрузки порядок инициализации должен быть таким:
Ядро читает Device Tree. 2. Находит узел i2c@43c00000. 3. По compatible вызывает probe() драйвера i2c-master-axi. 4. Драйвер отображает MMIO-регистры и регистрирует I2C adapter. 5. Ядро обходит дочерние устройства этой I2C-шины. 6. Находит oled@3c. 7. По compatible запускает драйвер ssd1307fb. 8. В системе появляется framebuffer, например /dev/fb0.
Если все описано корректно, в Linux можно ожидать примерно такую картину:
ls /sys/bus/i2c/devices/ ls /dev/fb* dmesg | grep -iE "i2c|ssd|fb"
Отдельно важно, что пины OLED, например T20/P20, в этом DTS не описываются. Для PL-логики они задаются в Vivado constraints, то есть в XDC-файле и bitstream. Device Tree описывает уже готовый результат с точки зрения Linux: по адресу 0x43c00000 есть I2C-контроллер, а на его шине есть OLED с адресом 0x3c.
На этом описание платы для первого запуска можно оставить в таком виде. Валидность DTS проверим после сборки ядра и загрузки системы: сначала через dtc, затем через dmesg, /proc/device-tree, /sys/bus/platform/devices, /sys/bus/i2c/devices и наличие framebuffer-устройства.
Фрагменты конфигурации для ядра: linux.fragment
Теперь перейдем к настройкам ядра Linux, которые нужны для нашей платы и периферии.
В Buildroot ядро собирается не из одного небольшого конфигурационного файла. Внутри Linux используется итоговый .config, где перечислены десятки тысяч параметров вида:
CONFIG_...=y CONFIG_...=m # CONFIG_... is not set
Эти параметры определяют, что будет встроено прямо в vmlinux, что будет собрано отдельным модулем, а что будет отключено.
Полный .config для Zynq-7000 вручную обычно не пишут. Вместо этого берут готовую базовую конфигурацию ядра и накладывают поверх нее небольшой фрагмент с проектными изменениями.
В нашем случае схема такая:

multi_v7_defconfig - это базовая конфигурация ядра для ARM multiplatform. Она уже есть в исходниках Linux и подходит в том числе для Zynq-7000.
linux.fragment - наш небольшой файл с дополнительными опциями для конкретной платы. В нем включаются только те параметры, которых не хватает в базовой конфигурации: поддержка OLED framebuffer, нужные I2C-опции, шрифты консоли, отдельные драйверы и диагностические возможности.
zynq_mini_revb_defconfig - это уже не конфиг ядра, а конфиг Buildroot. В нем мы укажем, какую базовую конфигурацию ядра брать и какой фрагмент надо применить поверх нее.
Связка в Buildroot будет выглядеть примерно так:
BR2_LINUX_KERNEL_DEFCONFIG="multi_v7" BR2_LINUX_KERNEL_CONFIG_FRAGMENT_FILES="$(BR2_EXTERNAL_...)/board/zynq_mini_revb/linux.fragment"
При сборке Buildroot выполнит следующий порядок действий:
Распаковка исходников Linux в
$BR_OUT/build/linux-*Копирование
arch/arm/configs/multi_v7_defconfigв качестве стартового.config.Слияние фрагментов из
BR2_LINUX_KERNEL_CONFIG_FRAGMENT_FILES(нашboard/zynq_mini_revb/linux.fragment) поверх базы производит утилитаscripts/kconfig/merge_config.shиз дерева ядра.Сборка всех артефатов: uImage, модули, DTB (DTS задаётся отдельно в
BR2_LINUX_KERNEL_CUSTOM_DTS_PATH).
Важно разделять две вещи:
linux.fragment - влияет на то, какие драйверы и возможности будут собраны в ядро;
zynq-mini-revb.dts - описывает, какие устройства реально есть на плате и как они подключены.
Например, если в DTS есть узел:
ssd1306: oled@3c { compatible = "solomon,ssd1306fb-i2c"; reg = <0x3c>; };
но в конфигурации ядра не включен драйвер ssd1307fb, то устройство будет описано в Device Tree, но драйвер не сможет к нему привязаться.
И наоборот: если драйвер включен в ядро, но в DTS нет узла oled@3c, то драйверу просто не к чему будет привязываться.
Поэтому для рабочей системы нужны обе части:
DTS - что есть на плате; linux.fragment - какие драйверы и подсистемы включить в ядре.
Пока основной zynq_mini_revb_defconfig еще не создан, файл linux.fragment можно положить на диск заранее. Buildroot начнет применять его после первого вызова:
make BR2_EXTERNAL="$BR_EXT" zynq_mini_revb_defconfig
После изменения linux.fragment не обязательно пересобирать весь Buildroot с нуля. Обычно достаточно пересобрать только ядро:
make linux-reconfigure make linux-rebuild make
Если нужно полностью удалить старую сборку ядра и пересобрать его заново:
make linux-dirclean make
Отдельный практический момент: merge_config.sh не всегда жестко останавливает сборку при ошибке в имени опции. Если в linux.fragment допустить опечатку в имени CONFIG_*, такая строка может быть проигнорирована. Поэтому после сборки полезно проверять итоговый .config:
grep CONFIG_FB_SSD1307 "$BR_OUT/build/linux-"*/.config grep CONFIG_FONT_MINI_4x6 "$BR_OUT/build/linux-"*/.config grep CONFIG_I2C "$BR_OUT/build/linux-"*/.config
Теперь создадим сам фрагмент конфигурации ядра для нашей платы:
cat > "$BR_EXT/board/zynq_mini_revb/linux.fragment" << 'EOF' # Kernel config fragment merged on top of multi_v7_defconfig. # (Buildroot pulls this via BR2_LINUX_KERNEL_CONFIG_FRAGMENT_FILES.) # Core Zynq + standard helpers CONFIG_ARCH_ZYNQ=y CONFIG_SOC_ZYNQ7000=y CONFIG_DEVTMPFS=y CONFIG_DEVTMPFS_MOUNT=y # I2C subsystem CONFIG_I2C=y CONFIG_I2C_CHARDEV=y CONFIG_I2C_HELPER_AUTO=y # SSD1306 framebuffer (in-tree, in drivers/video/fbdev/ssd1307fb.c) CONFIG_FB=y CONFIG_FB_SSD1307=y CONFIG_FRAMEBUFFER_CONSOLE=y # Дополнительные мелкие шрифты для fbcon на 128x64 OLED. # CONFIG_FONTS=y превращает FONT_* в явный выбор. Сохраняем 8x8/8x16 из # дефолта multi_v7 и добавляем компактные MINI4x6 (32x10 на 128x64) и # 6x10 (21x6). Имя для bootargs fbcon=font: ниже в .name каждого шрифта. CONFIG_FONTS=y CONFIG_FONT_8x8=y CONFIG_FONT_8x16=y CONFIG_FONT_MINI_4x6=y CONFIG_FONT_6x10=y # AT24 EEPROM (the on-board AT24C02 sits at FPGA_I2C_SCL2/SDA2 — optional) CONFIG_EEPROM_AT24=y # USB host (USB3320C ULPI PHY) CONFIG_USB=y CONFIG_USB_SUPPORT=y CONFIG_USB_EHCI_HCD=y CONFIG_USB_EHCI_HCD_PLATFORM=y CONFIG_USB_ULPI=y CONFIG_NOP_USB_XCEIV=y # Ethernet PHY (RTL8211E uses the realtek driver) CONFIG_REALTEK_PHY=y CONFIG_NETDEVICES=y CONFIG_ETHERNET=y CONFIG_NET_VENDOR_XILINX=y CONFIG_XILINX_EMACLITE=y CONFIG_MACB=y CONFIG_PHYLIB=y CONFIG_REALTEK_PHY=y # QSPI flash CONFIG_MTD=y CONFIG_MTD_SPI_NOR=y CONFIG_SPI=y CONFIG_SPI_ZYNQ_QSPI=y # Useful userspace CONFIG_OVERLAY_FS=y CONFIG_TMPFS=y CONFIG_TMPFS_POSIX_ACL=y CONFIG_PRINTK_TIME=y CONFIG_DYNAMIC_DEBUG=y CONFIG_DEBUG_FS=y EOF
Назначение основных групп опций:
Группа |
Опции |
Зачем нужны |
|---|---|---|
I2C |
|
Базовая I2C-подсистема и доступ к шинам через |
OLED framebuffer |
|
Драйвер для SSD1306/SSD1307 OLED через framebuffer |
Консоль на экране |
|
Возможность вывести Linux console на framebuffer |
QSPI |
|
Поддержка QSPI-контроллера Zynq и SPI NOR flash |
USB |
|
USB host и простой PHY-драйвер |
Ethernet |
|
GEM/MACB-драйвер Zynq и Realtek PHY |
devtmpfs |
|
Автоматическое появление устройств в |
Отладка |
|
Удобство диагностики при первом запуске |
После сборки нужно проверить не только наличие строк в итоговом .config, но и фактическое поведение системы:
dmesg | grep -iE "i2c|ssd|fb|macb|phy|qspi|usb" ls /dev/i2c-*ls /dev/fb* ls /sys/bus/i2c/devices/
Если нужная опция есть в linux.fragment, но отсутствует в итоговом .config, значит она была переопределена зависимостями Kconfig или указана с ошибкой. В таком случае нужно открыть конфигурацию ядра через Buildroot:
make linux-menuconfig
и проверить, какие зависимости требуются для нужного драйвера.
Фрагмент конфигурации U-Boot: uboot.fragment
После настройки ядра нужно задать параметры U-Boot. В нашей схеме U-Boot должен стартовать с SD-карты и самостоятельно загрузить два файла с FAT-раздела:
uImage zynq-mini-revb.dtb
То есть загрузка должна идти без JTAG, без ручных команд в консоли и без ожидания, что FSBL передаст U-Boot указатель на Device Tree через регистр r2.
Здесь важно не смешивать два разных Device Tree:
DTB для U-Boot нужен самому U-Boot, чтобы он корректно стартовал и описал свою минимальную платформу;
DTB для Linux загружается U-Boot с FAT-раздела и передается ядру Linux через bootm.
В нашем случае DTB для U-Boot будет встроен внутрь самого U-Boot, а DTB для Linux будет лежать отдельным файлом на FAT-разделе SD-карты.
Создадим фрагмент конфигурации U-Boot:
cat > "$BR_EXT/board/zynq_mini_revb/uboot.fragment" << 'EOF' CONFIG_TOOLS_LIBCRYPTO=y CONFIG_OF_EMBED=y CONFIG_USE_BOOTCOMMAND=y CONFIG_BOOTDELAY=1 CONFIG_BOOTCOMMAND="mmc rescan; mmc dev 0; if fatload mmc 0 0x100000 uEnv.txt 10000; then env import -t 0x100000 10000; if test -n \"$uenvcmd\"; then run uenvcmd; fi; fi; fatload mmc 0 0x3000000 uImage; fatload mmc 0 0x2A00000 zynq-mini-revb.dtb; bootm 0x3000000 - 0x2A00000" CONFIG_FS_FAT=y CONFIG_CMD_FAT=y CONFIG_FS_EXT4=y CONFIG_CMD_EXT4=y CONFIG_LEGACY_IMAGE_FORMAT=y CONFIG_CMD_BOOTM=y CONFIG_CMD_MMC=y CONFIG_SYS_PROMPT="zynq-mini> " EOF
Этот фрагмент делает несколько вещей.
Во-первых, включает embedded Device Tree для самого U-Boot:
CONFIG_OF_EMBED=y
Это означает, что U-Boot не будет ждать внешний DTB от FSBL. Нужное описание платформы будет встроено в образ U-Boot. Для Zynq это удобно на этапе ручного Linux port, потому что FSBL часто только инициализирует PS7 и DDR, а дальше передает управление U-Boot без отдельного Device Tree.
Во-вторых, задается фиксированная команда загрузки:
CONFIG_USE_BOOTCOMMAND=y CONFIG_BOOTCOMMAND="..."
Именно CONFIG_BOOTCOMMAND определяет, что U-Boot сделает после старта, если пользователь не остановит автозагрузку.
Разберем команду по шагам:
mmc rescan
U-Boot повторно сканирует MMC/SD-устройства.
mmc dev 0
Выбирает первое MMC-устройство. Обычно это SD-карта.
if fatload mmc 0 0x100000 uEnv.txt 10000; then ...
Пытается загрузить файл uEnv.txt с FAT-раздела в память по адресу 0x100000. Если файл найден, U-Boot импортирует из него переменные окружения.
env import -t 0x100000 10000
Импортирует текстовые переменные из загруженного uEnv.txt.
if test -n "$uenvcmd"; then run uenvcmd; fi
Если в uEnv.txt задана переменная uenvcmd, U-Boot выполнит ее. Это дает удобный механизм изменения загрузочной команды без пересборки U-Boot.
Если uEnv.txt отсутствует или в нем нет uenvcmd, сработает fallback-команда:
fatload mmc 0 0x3000000 uImage fatload mmc 0 0x2A00000 zynq-mini-revb.dtb bootm 0x3000000 - 0x2A00000
Здесь U-Boot загружает ядро uImage и Linux Device Tree zynq-mini-revb.dtb с FAT-раздела SD-карты, затем запускает ядро командой bootm.
Адреса выбраны так, чтобы образы не перекрывали друг друга в DDR:
Адрес |
Что загружается |
Назначение |
|---|---|---|
|
|
Временная область для переменных окружения |
|
|
Device Tree для Linux |
|
|
Образ ядра Linux |
Ключевые опции uboot.fragment:
Опция |
Зачем нужна |
|
Нужна части инструментов U-Boot при сборке образов и проверке форматов |
|
Встроить DTB для самого U-Boot в его образ |
|
Разрешить использование заданной команды автозагрузки |
|
Дать 1 секунду на остановку автозагрузки |
|
Описать полную последовательность загрузки с SD |
|
Поддержка FAT-файловой системы |
|
Команды работы с FAT, включая |
|
Поддержка ext4 |
|
Команды работы с ext4 |
|
Поддержка старого формата |
|
Команда |
|
Команды работы с MMC/SD |
|
Приглашение командной строки U-Boot |
Отдельно стоит отметить пару CONFIG_OF_EMBED и CONFIG_OF_BOARD.
Для нашего варианта нужно, чтобы U-Boot использовал встроенный Device Tree:
CONFIG_OF_EMBED=y
И не ждал, что board-код или предыдущий загрузчик передаст ему внешний DTB:
# CONFIG_OF_BOARD is not set
Если U-Boot будет ожидать Device Tree в r2, а FSBL его не передаст, возможны зависания или неочевидные ошибки еще до появления нормального баннера U-Boot.
Файл uEnv.txt на FAT-разделе
Чтобы не пересобирать U-Boot при каждом изменении bootargs или адресов загрузки, вынесем переменные в отдельный файл uEnv.txt.
Создадим его в каталоге board-файлов:
cat > "$BR_EXT/board/zynq_mini_revb/uEnv.txt" << 'EOF' bootargs=console=ttyPS0,115200 earlycon fbcon=font:MINI4x6 root=/dev/mmcblk0p2 rootwait rw ethaddr=00:0a:35:01:02:03 load_dtb_addr=0x2A00000 load_kernel_addr=0x3000000 uenvcmd=mmc rescan; fatload mmc 0 ${load_kernel_addr} uImage; fatload mmc 0 ${load_dtb_addr} zynq-mini-revb.dtb; bootm ${load_kernel_addr} - ${load_dtb_addr} EOF
Этот файл должен попасть на FAT-раздел SD-карты рядом с uImage и zynq-mini-revb.dtb.
В нем задаются основные переменные загрузки:
Переменная |
Значение |
Смысл |
|
строка параметров ядра |
Передается Linux при старте |
|
|
MAC-адрес GEM0 |
|
|
Адрес загрузки DTB |
|
|
Адрес загрузки ядра |
|
команда загрузки |
Основной сценарий запуска Linux |
Строка bootargs здесь повторяет параметры, которые мы ранее указывали в секции chosen внутри DTS:
console=ttyPS0,115200 earlycon fbcon=font:MINI4x6 root=/dev/mmcblk0p2 rootwait rw
Практически важно понимать приоритет: если U-Boot задает переменную bootargs, то при запуске Linux именно она обычно становится фактической командной строкой ядра. Поэтому параметры в uEnv.txt должны быть согласованы с chosen/bootargs в DTS.
Например, параметр:
fbcon=font:MINI4x6
имеет смысл только если в конфигурации ядра включена поддержка соответствующего шрифта:
CONFIG_FONT_MINI_4x6=y
Параметр:
root=/dev/mmcblk0p2
должен соответствовать реальной разметке SD-карты. В нашем варианте предполагается схема:
mmcblk0p1 - FAT-раздел с boot-файлами; mmcblk0p2 - ext4-раздел с rootfs.
Переменная ethaddr задает MAC-адрес Ethernet-интерфейса. Его нужно держать согласованным с описанием Ethernet в Device Tree, если там дополнительно используется local-mac-address или аналогичное свойство. Для прототипа можно использовать временный локальный адрес, но для серийного устройства MAC должен назначаться из управляемого диапазона и не должен дублироваться между платами.
Итоговая логика загрузки получается такой:
FSBL инициализирует PS7, DDR и передает управление U-Boot.
U-Boot стартует со своим встроенным Device Tree.
U-Boot сканирует SD-карту.
U-Boot пытается загрузить uEnv.txt с FAT-раздела.
Если uEnv.txt найден, U-Boot импортирует переменные и выполняет uenvcmd.
uenvcmd загружает uImage и zynq-mini-revb.dtb в DDR.
Команда bootm запускает Linux и передает ему DTB.
Linux использует переданный DTB как карту железа платы.
Таким образом, uboot.fragment описывает поведение загрузчика, а uEnv.txt дает удобную точку настройки без пересборки. Для разработки это заметно удобнее: можно менять bootargs, адреса загрузки, имя DTB или режим rootfs простым редактированием файла на FAT-разделе SD-карты.
Out-of-tree модуль i2c-master-axi
До этого момента мы подготовили почти всю инфраструктуру загрузки:
Buildroot -> Linux kernel config -> Device Tree -> U-Boot -> uEnv.txt -> SD boot flow
Но отвлечемся немного в сторону и разберем основной момент этой статьи - а именно модуль ядра, который обеспечит интерфейс для отправки данных на отображение из userspace. Разберем все максимально подробно.
Итак. После подготовки Device Tree возникает следующий вопрос: что именно Linux должен делать с узлом i2c@43c00000?
В DTS мы уже описали, что в программируемой логике PL есть AXI4-Lite IP-блок:
i2c_pl: i2c@43c00000 { compatible = "user,i2c-master-axi-1.0"; reg = <0x43c00000 0x1000>; interrupt-parent = <&intc>; interrupts = <0 29 IRQ_TYPE_LEVEL_HIGH>; clocks = <&clkc 15>; clock-names = "axi"; clock-frequency = <100000>; input-clock-frequency = <50000000>; #address-cells = <1>; #size-cells = <0>; ssd1306: oled@3c { compatible = "solomon,ssd1306fb-i2c"; reg = <0x3c>; ... }; };
Но сам по себе Device Tree не реализует доступ к железу. Он только сообщает ядру:
По адресу 0x43c00000 есть устройство;
Устройство совместимо с “user,i2c-master-axi-1.0”;
У него есть регистровое окно 0x1000 байт;
Оно может использовать прерывание;
На его I2C-шине есть OLED с адресом 0x3c.
Чтобы это описание превратилось в рабочее устройство, нужен драйвер. Драйвер должен объяснить ядру, как именно работать с регистрами этого IP-блока, как рассчитать скорость I2C, как выполнить START, WRITE, READ, STOP, как обработать NACK и как зарегистрировать новую I2C-шину в Linux.
Именно эту роль выполняет модуль ядра i2c-master-axi. Он называется out-of-tree, потому что его исходный код не лежит внутри основного дерева Linux в drivers/i2c/. Мы держим его отдельно от ядра, но собираем как обычный kernel module той же версии Linux, которая будет загружена на плате.
То есть модуль является мостом между тремя уровнями:
Уровень |
Что содержит |
Роль |
|---|---|---|
Device Tree |
|
Описание устройства |
Драйвер |
|
Программная логика работы с IP |
RTL IP-блок в PL |
регистры |
Реальный контроллер I2C на AXI |
После загрузки этого модуля цепочка должна стать рабочей:
DTB содержит узел i2c@43c00000
ядро находит compatible = “user,i2c-master-axi-1.0”
platform_driver вызывает probe()
драйвер отображает MMIO-регистры через ioremap
драйвер считывает частоты из Device Tree
драйвер рассчитывает PRESCALE для SCL
драйвер включает IP-блок
драйвер регистрирует struct i2c_adapter
в Linux появляется I2C-шина
ядро видит дочерний oled@3c
ssd1307fb привязывается к OLED
появляется framebuffer, а-ля /dev/fb0
По задумке i2c-master-axi.c видно - это не просто "драйвер OLED". Сам драйвер ничего не рисует на экране и не знает про графику. Его задача ниже уровнем: создать I2C master adapter, через который уже сможет работать встроенный драйвер OLED ssd1307fb.
Код драйвера
Приведу полный код драйвера и ниже мы его разберем по частям:
// SPDX-License-Identifier: GPL-2.0+ /* * i2c-master-axi.c — Linux I2C bus driver for the custom i2c_master_axi IP * (AXI4-Lite slave) shipped in this repository. * * Hardware register map (32-bit data, byte-address step = 4): * 0x00 CTRL R/W [1:0] = {IEN, EN} * 0x04 STATUS R [3:0] = {AL, BUSY, RXACK, TIP} * 0x08 CMD W [4:0] = {NACK, WR, RD, STO, STA} * 0x0C TX_DATA R/W [7:0] * 0x10 RX_DATA R [7:0] * 0x14 PRESCALE R/W [15:0] SCL = clk / (4*(PRESCALE+1)) * 0x18 ISR R/W1C [1:0] = {AL_IRQ, DONE_IRQ} * * SCL frequency: f_SCL = f_clk / (4 * (PRESCALE + 1)) * * Device-tree binding (compatible = "user,i2c-master-axi-1.0"): * * i2c0: i2c@43c00000 { * compatible = "user,i2c-master-axi-1.0"; * reg = <0x43c00000 0x1000>; * interrupts = <0 29 4>; * interrupt-parent = <&intc>; * clocks = <&clkc 15>; // FCLK0 (50 MHz on this design) * clock-frequency = <100000>; // I2C bus speed * #address-cells = <1>; * #size-cells = <0>; * * ssd1306@3c { * compatible = "solomon,ssd1306fb-i2c"; * reg = <0x3c>; * solomon,height = <64>; * solomon,width = <128>; * solomon,page-offset = <0>; * }; * }; * * Polled mode is the default: the IRQ wiring on Zynq sometimes lags behind * device-tree changes, but the controller is fast enough for SSD1306 traffic * even without interrupts. Pass `interrupts = <...>` and the driver will * automatically use IRQ-driven completion. */ #include <linux/clk.h> #include <linux/completion.h> #include <linux/delay.h> #include <linux/i2c.h> #include <linux/interrupt.h> #include <linux/io.h> #include <linux/iopoll.h> #include <linux/kernel.h> #include <linux/module.h> #include <linux/of.h> #include <linux/platform_device.h> #include <linux/slab.h> #define DRV_NAME "i2c-master-axi" #define I2C_REG_CTRL 0x00 #define I2C_REG_STATUS 0x04 #define I2C_REG_CMD 0x08 #define I2C_REG_TX_DATA 0x0C #define I2C_REG_RX_DATA 0x10 #define I2C_REG_PRESCALE 0x14 #define I2C_REG_ISR 0x18 #define CTRL_EN BIT(0) #define CTRL_IEN BIT(1) #define STATUS_TIP BIT(0) #define STATUS_RXACK BIT(1) #define STATUS_BUSY BIT(2) #define STATUS_AL BIT(3) #define CMD_STA BIT(0) #define CMD_STO BIT(1) #define CMD_RD BIT(2) #define CMD_WR BIT(3) #define CMD_NACK BIT(4) #define ISR_DONE BIT(0) #define ISR_AL BIT(1) #define I2C_DEFAULT_BUS_HZ 100000U #define I2C_DEFAULT_INPUT_HZ 50000000U /* TIP polling timeout — generous, covers full byte at 50 kHz */ #define I2C_TIP_TIMEOUT_US 20000U struct i2c_master_axi { void __iomem *regs; struct device *dev; struct clk *clk; struct i2c_adapter adap; struct completion cmd_done; int irq; bool use_irq; u32 input_hz; u32 bus_hz; }; static inline u32 axi_read(struct i2c_master_axi *i, u32 off) { return ioread32(i->regs + off); } static inline void axi_write(struct i2c_master_axi *i, u32 off, u32 val) { iowrite32(val, i->regs + off); } static int axi_wait_tip(struct i2c_master_axi *i, u32 *status) { u32 st; int ret; if (i->use_irq) { unsigned long t = wait_for_completion_timeout(&i->cmd_done, usecs_to_jiffies(I2C_TIP_TIMEOUT_US) + 1); st = axi_read(i, I2C_REG_STATUS); if (!t && (st & STATUS_TIP)) { dev_err(i->dev, "TIP IRQ timeout (status=0x%02x)\n", st); return -ETIMEDOUT; } ret = 0; } else { ret = readl_poll_timeout(i->regs + I2C_REG_STATUS, st, !(st & STATUS_TIP), 1, I2C_TIP_TIMEOUT_US); if (ret) { dev_err(i->dev, "TIP poll timeout (status=0x%02x)\n", st); return -ETIMEDOUT; } } if (st & STATUS_AL) { dev_dbg(i->dev, "arbitration lost\n"); return -EAGAIN; } if (status) *status = st; return 0; } static int axi_send_cmd(struct i2c_master_axi *i, u32 cmd, u32 *status) { if (i->use_irq) { reinit_completion(&i->cmd_done); axi_write(i, I2C_REG_ISR, ISR_DONE | ISR_AL); } axi_write(i, I2C_REG_CMD, cmd); return axi_wait_tip(i, status); } static int axi_xfer_one(struct i2c_master_axi *i, struct i2c_msg *m, bool first, bool last) { u32 cmd, status; u8 addr_byte; int j, ret; addr_byte = (m->addr << 1) | ((m->flags & I2C_M_RD) ? 1 : 0); axi_write(i, I2C_REG_TX_DATA, addr_byte); cmd = CMD_WR | (first ? CMD_STA : 0); ret = axi_send_cmd(i, cmd, &status); if (ret) return ret; if (status & STATUS_RXACK) { dev_dbg(i->dev, "no ACK on address 0x%02x\n", m->addr); axi_write(i, I2C_REG_CMD, CMD_STO); axi_wait_tip(i, NULL); return -ENXIO; } if (m->flags & I2C_M_RD) { for (j = 0; j < m->len; j++) { cmd = CMD_RD; if (j == m->len - 1) { cmd |= CMD_NACK; if (last) cmd |= CMD_STO; } ret = axi_send_cmd(i, cmd, &status); if (ret) return ret; m->buf[j] = axi_read(i, I2C_REG_RX_DATA) & 0xff; } } else { for (j = 0; j < m->len; j++) { axi_write(i, I2C_REG_TX_DATA, m->buf[j]); cmd = CMD_WR; if (j == m->len - 1 && last) cmd |= CMD_STO; ret = axi_send_cmd(i, cmd, &status); if (ret) return ret; if (status & STATUS_RXACK) { dev_dbg(i->dev, "no ACK on data byte %d (0x%02x)\n", j, m->buf[j]); if (!last) { axi_write(i, I2C_REG_CMD, CMD_STO); axi_wait_tip(i, NULL); } return -EIO; } } } return 0; } static int axi_master_xfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num) { struct i2c_master_axi *i = i2c_get_adapdata(adap); int k, ret; u32 status; status = axi_read(i, I2C_REG_STATUS); if (status & STATUS_BUSY) { dev_dbg(i->dev, "bus busy at xfer start (status=0x%02x)\n", status); return -EAGAIN; } for (k = 0; k < num; k++) { ret = axi_xfer_one(i, &msgs[k], (k == 0), (k == num - 1)); if (ret < 0) return ret; } return num; } static u32 axi_functionality(struct i2c_adapter *adap) { return I2C_FUNC_I2C | I2C_FUNC_SMBUS_EMUL; } static const struct i2c_algorithm axi_algo = { .master_xfer = axi_master_xfer, .functionality = axi_functionality, }; static const struct i2c_adapter_quirks axi_quirks = { .flags = I2C_AQ_NO_ZERO_LEN, }; static irqreturn_t i2c_master_axi_isr(int irq, void *dev_id) { struct i2c_master_axi *i = dev_id; u32 isr = axi_read(i, I2C_REG_ISR); if (!isr) return IRQ_NONE; axi_write(i, I2C_REG_ISR, isr); complete(&i->cmd_done); return IRQ_HANDLED; } static int i2c_master_axi_hw_init(struct i2c_master_axi *i) { u32 prescale; if (i->bus_hz == 0 || i->input_hz == 0) return -EINVAL; if (i->bus_hz * 4 > i->input_hz) { dev_err(i->dev, "requested bus %u Hz exceeds input/4 = %u Hz\n", i->bus_hz, i->input_hz / 4); return -EINVAL; } prescale = (i->input_hz / (4U * i->bus_hz)); if (prescale == 0) { dev_err(i->dev, "computed prescale=0, refusing\n"); return -EINVAL; } prescale -= 1; if (prescale > 0xFFFFU) { dev_err(i->dev, "prescale %u out of 16-bit range\n", prescale); return -EINVAL; } axi_write(i, I2C_REG_CTRL, 0); axi_write(i, I2C_REG_PRESCALE, prescale); axi_write(i, I2C_REG_ISR, ISR_DONE | ISR_AL); axi_write(i, I2C_REG_CTRL, CTRL_EN | (i->use_irq ? CTRL_IEN : 0)); dev_info(i->dev, "input=%u Hz, bus=%u Hz, prescale=%u, irq=%s\n", i->input_hz, i->bus_hz, prescale, i->use_irq ? "yes" : "polled"); return 0; } static int i2c_master_axi_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; struct i2c_master_axi *i; int ret; i = devm_kzalloc(dev, sizeof(*i), GFP_KERNEL); if (!i) return -ENOMEM; i->dev = dev; init_completion(&i->cmd_done); platform_set_drvdata(pdev, i); i->regs = devm_platform_ioremap_resource(pdev, 0); if (IS_ERR(i->regs)) return PTR_ERR(i->regs); i->clk = devm_clk_get_optional(dev, NULL); if (IS_ERR(i->clk)) return PTR_ERR(i->clk); if (i->clk) { ret = clk_prepare_enable(i->clk); if (ret) return ret; i->input_hz = clk_get_rate(i->clk); } if (!i->input_hz) i->input_hz = I2C_DEFAULT_INPUT_HZ; of_property_read_u32(dev->of_node, "input-clock-frequency", &i->input_hz); i->bus_hz = I2C_DEFAULT_BUS_HZ; of_property_read_u32(dev->of_node, "clock-frequency", &i->bus_hz); i->irq = platform_get_irq_optional(pdev, 0); if (i->irq > 0) { ret = devm_request_irq(dev, i->irq, i2c_master_axi_isr, 0, dev_name(dev), i); if (ret) { dev_warn(dev, "request_irq(%d) failed (%d), falling back to polling\n", i->irq, ret); i->use_irq = false; } else { i->use_irq = true; } } ret = i2c_master_axi_hw_init(i); if (ret) goto err_clk; i->adap.owner = THIS_MODULE; i->adap.class = I2C_CLASS_DEPRECATED; i->adap.algo = &axi_algo; i->adap.quirks = &axi_quirks; i->adap.dev.parent = dev; i->adap.dev.of_node = of_node_get(dev->of_node); strscpy(i->adap.name, dev_name(dev), sizeof(i->adap.name)); i2c_set_adapdata(&i->adap, i); ret = i2c_add_adapter(&i->adap); if (ret) goto err_disable; return 0; err_disable: axi_write(i, I2C_REG_CTRL, 0); err_clk: if (i->clk) clk_disable_unprepare(i->clk); return ret; } /* * .remove signature changed across kernel versions: * < 6.11 : int (*remove)(struct platform_device *) * >= 6.11 : void (*remove)(struct platform_device *) * 6.6+ : void (*remove_new)(struct platform_device *) (transitional) * * The "int + return 0" form below works on every kernel from 4.x to 6.10 * inclusive, and Linux 6.11+ accepts an int-returning callback as long as * we keep using `.remove` (it just ignores the return value). For the * Buildroot 2024.02 default (Linux 6.6) this is the correct shape. */ static int i2c_master_axi_remove(struct platform_device *pdev) { struct i2c_master_axi *i = platform_get_drvdata(pdev); i2c_del_adapter(&i->adap); axi_write(i, I2C_REG_CTRL, 0); if (i->clk) clk_disable_unprepare(i->clk); return 0; } static const struct of_device_id i2c_master_axi_of_match[] = { { .compatible = "user,i2c-master-axi-1.0" }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, i2c_master_axi_of_match); static struct platform_driver i2c_master_axi_driver = { .driver = { .name = DRV_NAME, .of_match_table = i2c_master_axi_of_match, }, .probe = i2c_master_axi_probe, .remove = i2c_master_axi_remove, }; module_platform_driver(i2c_master_axi_driver); MODULE_AUTHOR("I2C Master Controller project"); MODULE_DESCRIPTION("AXI4-Lite I2C master driver (i2c_master_axi IP)"); MODULE_LICENSE("GPL v2");
Что должен делать драйвер
Минимально рабочий драйвер для нашего AXI I2C IP должен выполнить несколько функций. Первая функция - привязаться к узлу из Device Tree. Для этого в коде драйвера должна быть таблица совместимости:
static const struct of_device_id i2c_master_axi_of_match[] = { { .compatible = "user,i2c-master-axi-1.0" }, { } };
Строка compatible в DTS и строка в of_match_table должны совпадать буквально. Если в одном месте будет user,i2c-master-axi-1.0, а в другом, например, user,i2c-master-axi, то модуль может загрузиться, но probe() не будет вызван.
Вторая функция - получить доступ к регистрам IP-блока. В DTS указан физический адрес:
reg = <0x43c00000 0x1000>;
Для CPU это адресное окно AXI4-Lite устройства. Но код ядра не должен напрямую работать с физическим адресом как с обычным указателем. В probe() драйвер вызывает механизм devm_platform_ioremap_resource(). Он берет диапазон из reg, проверяет ресурс и отображает его в виртуальное адресное пространство ядра.
После этого драйвер получает указатель вида:
void __iomem *regs;
И все обращения к IP-блоку идут через ioread32() и iowrite32(). Третья функция - прочитать параметры тактирования. В DTS заданы две частоты:
input-clock-frequency = <50000000>; clock-frequency = <100000>;
input-clock-frequency- входная частота IP-блока, то есть частота AXI/FCLK0. В нашем примере это 50 MHz.clock-frequency- требуемая частота I2C SCL. В нашем примере это 100 kHz.
По этим значениям драйвер рассчитывает делитель PRESCALE. Формула:
f_SCL = f_input / (4 * (PRESCALE + 1))
Отсюда:
PRESCALE = f_input / (4 * f_SCL) - 1
Для 50 MHz и 100 kHz:
PRESCALE = 50 000 000 / (4 * 100 000) - 1 PRESCALE = 125 - 1 PRESCALE = 124
Это значение записывается в регистр PRESCALE IP-блока.
Четвертая функция - зарегистрировать I2C-шину.
Linux не должен знать, что конкретно за AXI-регистры используются внутри. Для остального ядра важен стандартный объект:
struct i2c_adapter
Когда драйвер вызывает i2c_add_adapter(), он сообщает ядру:
Появился новый I2C master;
Через него можно отправлять
struct i2c_msg;Для передачи сообщений вызывайте мой
master_xfercallback.
После этого userspace увидит новую шину, например:
/dev/i2c-1 /sys/bus/i2c/devices/i2c-1
Номер может быть другим, потому что зависит от порядка регистрации I2C-адаптеров.
Пятая функция - реализовать передачу I2C-сообщений.
Linux I2C core работает не с регистрами CMD, STATUS, TX_DATA. Он передает драйверу массив структур:
struct i2c_msg
Каждое сообщение содержит:
адрес slave-устройства;
флаг чтения или записи;
буфер;
длину буфера.
Драйвер должен перевести это в последовательность операций на шине:
START адрес + bit R/W
данные
ACK/NACK
STOP
Для этого в драйвере есть связка функций:
axi_master_xfer() -> axi_xfer_one() -> axi_send_cmd() -> axi_wait_tip()
Шестая функция - корректно обрабатывать ошибки.
На I2C типовые ошибки такие:
устройство не ответило на адрес;
устройство ответило на адрес, но не приняло байт данных;
шина занята;
контроллер не завершил транзакцию;
потеря арбитража;
неверные частоты или невозможный PRESCALE.
Драйвер должен возвращать стандартные коды ошибок Linux:
-ENXIO - нет устройства на адресе;
-EIO - ошибка обмена данными;
-ETIMEDOUT - контроллер не завершил операцию;
-EAGAIN - шина занята или потеря арбитража;
-EINVAL - некорректная конфигурация.
И седьмая функция - корректно выключить устройство при выгрузке. При rmmod, отвязке устройства или остановке драйвера нужно удалить I2C-адаптер, выключить IP-блок и освободить clock. Это делает remove().
Слои драйвера
Код i2c-master-axi.c удобнее читать не как один длинный файл, а как несколько уровней абстракции.
platform_driver / of_match_table ↓ probe / remove ↓ hw_init ↓ i2c_adapter / i2c_algorithm ↓ axi_master_xfer ↓ axi_xfer_one ↓ axi_send_cmd ↓ axi_wait_tip ↓ axi_read / axi_write ↓ MMIO-регистры IP-блока
Каждый слой решает свою задачу.
Слой |
Что содержит |
За что отвечает |
|---|---|---|
Регистры |
|
Соответствие C-кода карте регистров RTL |
MMIO-доступ |
|
Чтение и запись 32-битных регистров |
Ожидание IP |
|
Ждать завершения микрокоманды по |
Команда IP |
|
Записать |
Одно I2C-сообщение |
|
Преобразовать один |
I2C core adapter |
|
Реализовать интерфейс Linux I2C subsystem |
Инициализация железа |
|
Рассчитать |
Platform layer |
|
Связать Device Tree, модуль и устройство |
IRQ layer |
|
Будить поток после завершения команды |
Ниже разберем эти слои последовательно.
Карта регистров и константы
В начале драйвера обычно находятся определения регистров и битовых масок. Они должны соответствовать RTL-описанию IP-блока. Упрощенно карта выглядит так:
#define I2C_REG_CTRL 0x00 #define I2C_REG_STATUS 0x04 #define I2C_REG_CMD 0x08 #define I2C_REG_TX_DATA 0x0c #define I2C_REG_RX_DATA 0x10 #define I2C_REG_PRESCALE 0x14 #define I2C_REG_ISR 0x18
Каждое значение - это смещение от базового адреса 0x43c00000.
То есть:
0x43c00000 + 0x00 -> CTRL 0x43c00000 + 0x04 -> STATUS 0x43c00000 + 0x08 -> CMD 0x43c00000 + 0x0c -> TX_DATA 0x43c00000 + 0x10 -> RX_DATA 0x43c00000 + 0x14 -> PRESCALE 0x43c00000 + 0x18 -> ISR
Роль регистров:
Регистр |
Назначение |
|
Включение IP-блока и разрешение IRQ |
|
Состояние контроллера: |
|
Запуск фазы I2C: START, STOP, READ, WRITE, NACK |
|
Байт, который нужно отправить на I2C |
|
Байт, который был принят с I2C |
|
Делитель для формирования SCL |
|
Флаги прерываний, обычно DONE и AL |
Ключевые биты STATUS:
Бит |
Смысл |
|
Transfer in progress. IP сейчас выполняет команду |
|
Ответ slave. Важно: |
|
Шина занята |
|
Arbitration lost |
Особенно важно не перепутать RXACK.
В обычной терминологии ACK воспринимается как положительное подтверждение. Но в регистре многих I2C-контроллеров бит называется RXACK и равен 1, когда подтверждения не было. То есть:
STATUS_RXACK = 0 -> slave ответил ACK; STATUS_RXACK = 1 -> slave ответил NACK или не ответил.
Поэтому проверка в коде выглядит не как "если ACK", а как "если RXACK установлен, это ошибка":
if (status & STATUS_RXACK) return -ENXIO;
Для адресного байта обычно возвращается -ENXIO, потому что устройство не найдено на шине. Для байта данных может возвращаться -EIO, потому что устройство ответило на адрес, но не приняло конкретный байт.
MMIO-доступ: axi_read и axi_write
После ioremap драйвер получает базовый указатель на регистровое окно:
void __iomem *regs;
Дальше удобно сделать две короткие функции:
static inline u32 axi_read(struct i2c_master_axi *i, u32 reg) { return ioread32(i->regs + reg); } static inline void axi_write(struct i2c_master_axi *i, u32 reg, u32 val) { iowrite32(val, i->regs + reg); }
Их смысл простой: не писать каждый раз ioread32(i->regs + I2C_REG_STATUS), а работать через понятные обертки:
status = axi_read(i, I2C_REG_STATUS); axi_write(i, I2C_REG_CMD, cmd);
Это не просто косметика. Такие функции фиксируют модель доступа к железу:
драйвер обращается только к MMIO-регистрам;
каждый регистр имеет 32-битный доступ;
смещения задаются через I2C_REG_*;
обычные указатели C не используются.
Для Linux kernel это важно, потому что MMIO-память не равна обычной RAM. Доступ должен идти через специальные primitives ioread32 и iowrite32, чтобы архитектура корректно выполнила ordering и доступ к device memory.
struct i2c_master_axi: состояние одного экземпляра IP
Обычно драйвер заводит одну структуру состояния на один экземпляр устройства. Упрощенно:
struct i2c_master_axi { void __iomem *regs; struct device *dev; struct clk *clk; struct i2c_adapter adap; struct completion cmd_done; int irq; bool use_irq; u32 input_hz; u32 bus_hz; };
Это центральная структура драйвера.
Поле regs - отображенное MMIO-окно. Через него выполняются все чтения и записи регистров IP.
Поле dev - указатель на Linux device. Он нужен для логов:
dev_info(i->dev, ...); dev_err(i->dev, ...); dev_warn(i->dev, ...);
Также через dev работают devm_* функции, которые автоматически освобождают ресурсы при отвязке устройства.
Поле clk - ссылка на clock, если драйвер получил его через clock framework. В DTS это соответствует строке:
clocks = <&clkc 15>; clock-names = "axi";
Если clock framework доступен, драйвер может получить реальную частоту через clk_get_rate(). Если нет, он использует input-clock-frequency из Device Tree или дефолтное значение.
Поле adap - объект struct i2c_adapter. Это самый важный объект для внешнего мира. После регистрации адаптера Linux I2C core узнает, что появилась новая I2C-шина.
Поле cmd_done - completion для IRQ-режима. Поток, который отправил команду в IP, может уснуть на wait_for_completion_timeout(), а IRQ-обработчик разбудит его через complete().
Поле irq - номер Linux IRQ, полученный из Device Tree.
Поле use_irq - режим работы ожидания. Если true, драйвер ждет завершения команд через IRQ. Если false, драйвер опрашивает STATUS.TIP.
Поле input_hz - входная частота IP-блока.
Поле bus_hz - целевая частота I2C-шины.
Эта структура не описывает железо как таковое. Железо описано в DTS и RTL. Структура хранит runtime-состояние драйвера: какие ресурсы выделены, где регистры, какие частоты выбраны, какой режим ожидания используется и какой I2C-адаптер зарегистрирован.
axi_wait_tip: ожидание завершения микрокоманды
I2C IP работает как небольшой секвенсер. CPU не дергает SDA/SCL напрямую. CPU пишет команду в регистр CMD, а железо само выполняет нужную фазу на шине.
Например:
CMD_WR | CMD_STA
может означать:
сформировать START;
выдать байт из TX_DATA;
дождаться ACK/NACK;
обновить STATUS.
Пока команда выполняется, в STATUS установлен бит TIP: TIP = transfer in progress
Следующую команду нельзя подавать, пока TIP = 1. Поэтому почти каждая операция с I2C строится по схеме:
записать CMD;
ждать TIP = 0;
прочитать STATUS;
проверить ошибки.
Этим занимается функция axi_wait_tip. Логика в общем виде:
static int axi_wait_tip(struct i2c_master_axi *i, u32 *status) { u32 st; if (i->use_irq) { wait_for_completion_timeout(&i->cmd_done, ...); st = axi_read(i, I2C_REG_STATUS); } else { readl_poll_timeout(i->regs + I2C_REG_STATUS, st, !(st & STATUS_TIP), 1, I2C_TIP_TIMEOUT_US); } if (st & STATUS_AL) return -EAGAIN; *status = st; return 0; }
Есть два режима.
Режим polling
Если IRQ не подключен или драйвер не смог его зарегистрировать, используется опрос.
Драйвер циклически читает STATUS и проверяет: TIP стал 0?
Если да, команда завершена.
Если нет, драйвер продолжает ждать, но не бесконечно. Используется timeout, например 20 ms.
Если timeout истек, возвращается:-ETIMEDOUT
Это означает, что IP-блок не завершил команду. Возможные причины:
bitstream не загружен;
адрес reg в DTS неверный;
clock на IP не подан;
IP завис;
шина SDA/SCL физически удерживается;
reset IP не снят.
Режим IRQ
Если IRQ подключен, драйвер может не опрашивать STATUS в цикле. Последовательность такая:
драйвер записал CMD;
поток уснул на completion;
IP завершил команду;
IP поднял irq_o;
GIC вызвал IRQ handler;
IRQ handler сбросил ISR;
IRQ handler вызвал complete();
поток проснулся;
драйвер прочитал STATUS.
IRQ-режим экономит CPU, но для первого запуска polling часто проще. Он требует меньше условий: можно поднять I2C даже до полной проверки interrupt routing. В обоих режимах после завершения команды драйвер проверяет STATUS_AL.AL означает arbitration lost. Для платы с одним I2C-master это редкая ситуация, но она может появиться при некорректном состоянии шины или сбое транзакции. В этом случае возвращается-EAGAIN
Смысл: операцию теоретически можно повторить.
axi_send_cmd: одна атомарная фаза I2C
Функция axi_send_cmd - это минимальный строительный блок всех I2C-транзакций в драйвере.
Ее задача:
подготовить ожидание;
сбросить старые IRQ-флаги;
записать команду в CMD;
дождаться завершения через axi_wait_tip;
вернуть статус или ошибку.
Упрощенный вид:
static int axi_send_cmd(struct i2c_master_axi *i, u32 cmd, u32 *status) { if (i->use_irq) { reinit_completion(&i->cmd_done); axi_write(i, I2C_REG_ISR, ISR_DONE | ISR_AL); } axi_write(i, I2C_REG_CMD, cmd); return axi_wait_tip(i, status); }
Если используется IRQ, перед каждой новой командой нужно сбросить completion:
reinit_completion(&i->cmd_done);
Иначе поток может проснуться по старому событию. Также перед новой командой сбрасываются флаги ISR_DONE и ISR_AL. Обычно такие ISR-регистры работают по правилу W1C: write 1 to clearТо есть для сброса флага нужно записать в него 1.
После этого команда уходит в CMD. Примеры команд:
CMD_WR | CMD_STA отправить байт с START; CMD_WR отправить байт без START/STOP; CMD_WR | CMD_STO отправить байт и завершить STOP; CMD_RD принять байт; CMD_RD | CMD_NACK | CMD_STO принять последний байт, ответить NACK и завершить STOP.
С точки зрения CPU это одна запись в CMD. С точки зрения I2C-шины это фаза обмена на SDA/SCL. Важный момент: axi_send_cmd не знает, что именно передается - адрес OLED, команда дисплею или байт данных. Она только запускает фазу, которую заранее подготовил вышестоящий код.
axi_xfer_one: преобразование одного i2c_msg
Linux I2C core передает драйверу сообщения в формате struct i2c_msg. Упрощенно:
struct i2c_msg { __u16 addr; __u16 flags; __u16 len; __u8 *buf; };
Где:
addr - 7-битный адрес slave;
flags - направление и дополнительные признаки;
len - длина буфера;
buf - данные для записи или место для чтения.
Функция axi_xfer_one берет одно такое сообщение и превращает его в последовательность команд для AXI IP. У нее есть дополнительные флаги:
first - это первое сообщение в группе;
last - это последнее сообщение в группе.
Они нужны, чтобы корректно расставить START и STOP. I2C допускает combined transactions. Например, многие устройства читаются так:
START
slave address + write register address
REPEATED START
slave address + read data
STOP
Между write и read не должно быть STOP. Поэтому драйвер не может ставить STOP после каждого i2c_msg. Он должен знать, последнее ли это сообщение во всей группе.
Адресный байт
Сначала формируется адресный байт. I2C использует 7-битный адрес, но на проводе передается 8 бит:
бит 7..1 - адрес; бит 0 - направление: 0 write, 1 read.
Код концептуально такой:
addr_byte = (m->addr << 1) | ((m->flags & I2C_M_RD) ? 1 : 0);
Для OLED с адресом 0x3c:
write: 0x3c << 1 | 0 = 0x78 read: 0x3c << 1 | 1 = 0x79
Дальше адресный байт кладется в TX_DATA:
axi_write(i, I2C_REG_TX_DATA, addr_byte);
И отправляется командой:
cmd = CMD_WR | (first ? CMD_STA : 0); ret = axi_send_cmd(i, cmd, &status);
Если это первое сообщение в группе, добавляется CMD_STA, то есть START. После завершения адресной фазы проверяется RXACK. Если RXACK = 1, значит slave не ответил ACK на адрес. Тогда драйвер должен освободить шину и вернуть ошибку: -ENXIO
Типовые причины:
OLED не запитан;
адрес не 0x3c, а 0x3d;
нет pull-up на SDA/SCL;
ошибка в XDC;
bitstream не тот;
IP-блок не подключен к нужным пинам;
на плате обрыв или неверная распиновка.
Запись данных
Если сообщение не содержит I2C_M_RD, это write. Для каждого байта:
положить байт в TX_DATA;
сформировать CMD_WR;
если это последний байт последнего сообщения, добавить CMD_STO;
отправить команду;
дождаться TIP = 0;
проверить RXACK.
Концептуально:
for (j = 0; j < m->len; j++) { axi_write(i, I2C_REG_TX_DATA, m->buf[j]); cmd = CMD_WR; if (j == m->len - 1 && last) cmd |= CMD_STO; ret = axi_send_cmd(i, cmd, &status); if (ret) return ret; if (status & STATUS_RXACK) return -EIO; }
Если NACK пришел на адресе, это -ENXIO. Если NACK пришел на байте данных, это уже -EIO: устройство было найдено, но конкретный байт не приняло. Для OLED это может быть, например, некорректная команда, проблемы питания или сбой на шине.
Чтение данных
Если установлен I2C_M_RD, это read. Для каждого байта:
сформировать CMD_RD;
если это последний байт, добавить CMD_NACK;
если это последний байт последнего сообщения, добавить CMD_STO;
отправить команду;
дождаться TIP = 0;
прочитать RX_DATA.
Почему на последнем байте нужен NACK? В I2C при чтении slave может отдавать данные до тех пор, пока master подтверждает прием ACK. Когда master больше не хочет читать, он отвечает NACK на последний байт и затем формирует STOP. Это нормальное завершение чтения.
Концептуально:
for (j = 0; j < m->len; j++) { cmd = CMD_RD; if (j == m->len - 1) { cmd |= CMD_NACK; if (last) cmd |= CMD_STO; } ret = axi_send_cmd(i, cmd, &status); if (ret) return ret; m->buf[j] = axi_read(i, I2C_REG_RX_DATA) & 0xff; }
Для OLED основной поток обычно write, потому что framebuffer-драйвер отправляет команды и данные на дисплей. Но поддержка read полезна для универсальности адаптера и для других I2C-устройств.
axi_master_xfer: вход из Linux I2C core
axi_xfer_one работает с одним сообщением. Но Linux I2C core вызывает не ее напрямую, а функцию master_xfer из структуры i2c_algorithm. Драйвер объявляет алгоритм:
static const struct i2c_algorithm axi_algo = { .master_xfer = axi_master_xfer, .functionality = axi_functionality, };
axi_master_xfer - это точка входа из подсистемы I2C. Пользовательская программа или другой драйвер делает, например:
i2c_transfer(adapter, msgs, num)
Linux находит нужный i2c_adapter, берет его algo->master_xfer и вызывает axi_master_xfer. Упрощенная логика:
static int axi_master_xfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num) { struct i2c_master_axi *i = i2c_get_adapdata(adap); int k; int ret; u32 status; status = axi_read(i, I2C_REG_STATUS); if (status & STATUS_BUSY) return -EAGAIN; for (k = 0; k < num; k++) { ret = axi_xfer_one(i, &msgs[k], k == 0, k == num - 1); if (ret < 0) return ret; } return num; }
Сначала драйвер получает свой контекст:
struct i2c_master_axi *i = i2c_get_adapdata(adap);
Этот указатель был сохранен ранее в probe() через:
i2c_set_adapdata(&i->adap, i);
Дальше проверяется STATUS_BUSY. Если шина уже занята, драйвер возвращает: -EAGAIN
Затем идет цикл по всем сообщениям. Для каждого сообщения вызывается axi_xfer_one.
Флаги first и last вычисляются так:
first = (k == 0) last = (k == num - 1)
Именно здесь несколько i2c_msg объединяются в одну I2C-транзакцию. Если все сообщения успешно переданы, master_xfer возвращает не 0, а num. Это контракт Linux I2C subsystem: успешный возврат означает количество обработанных сообщений. Если было передано 2 сообщения, функция должна вернуть 2. Если возникла ошибка, возвращается отрицательный код ошибки.
axi_functionality и ограничения адаптера
Кроме master_xfer, драйвер сообщает ядру, какие возможности поддерживает адаптер. Например:
static u32 axi_functionality(struct i2c_adapter *adap) { return I2C_FUNC_I2C | I2C_FUNC_SMBUS_EMUL; }
I2C_FUNC_I2C означает, что адаптер поддерживает обычные I2C-транзакции.
I2C_FUNC_SMBUS_EMUL означает, что часть SMBus-операций может быть эмулирована через обычные I2C-сообщения.
Также могут задаваться ограничения:
static const struct i2c_adapter_quirks axi_quirks = { .flags = I2C_AQ_NO_ZERO_LEN, };
I2C_AQ_NO_ZERO_LEN означает, что адаптер не поддерживает сообщения нулевой длины. Для нашего IP это логично: каждая фаза передачи связана либо с байтом в TX_DATA, либо с байтом в RX_DATA.
Такие ограничения важны, чтобы верхний уровень ядра не пытался отправлять транзакции, которые физически не реализованы этим контроллером.
IRQ-обработчик
Драйвер может работать в двух режимах:
polling - ждать TIP=0 опросом STATUS; IRQ - ждать completion, который завершит обработчик прерывания.
IRQ не обязателен для первой версии, но полезен для более корректной интеграции.
В DTS мы описали:
interrupt-parent = <&intc>; interrupts = <0 29 IRQ_TYPE_LEVEL_HIGH>;
Это означает, что IP-блок подключен к GIC через fabric interrupt. Если линия в Vivado реально подключена к IRQ_F2P[0], то первый fabric IRQ соответствует GIC interrupt ID 61, а в Device Tree это записывается как SPI number 61 - 32 = 29.
IRQ handler выглядит концептуально так:
static irqreturn_t i2c_master_axi_isr(int irq, void *dev_id) { struct i2c_master_axi *i = dev_id; u32 isr = axi_read(i, I2C_REG_ISR); if (!isr) return IRQ_NONE; axi_write(i, I2C_REG_ISR, isr); complete(&i->cmd_done); return IRQ_HANDLED; }
Обработчик читает ISR IP-блока. Он не анализирует I2C-адреса и не передает данные. Его задача намного проще:
понять, что прерывание действительно от нашего IP;
сбросить флаги ISR;
разбудить поток, который ждет завершения команды.
Если ISR = 0, обработчик возвращает:IRQ_NONE Это означает, что прерывание не принадлежит этому устройству или флагов нет. Если флаги есть, они сбрасываются записью тех же битов в ISR. Это W1C-логика: write 1 to clear
После этого вызывается:
complete(&i->cmd_done);
Поток, который спит в axi_wait_tip, просыпается и читает STATUS. Важно: IRQ handler не заменяет проверку STATUS. Он только сообщает, что событие произошло. После пробуждения драйвер все равно должен прочитать STATUS и проверить:
TIP;
RXACK;
AL;
BUSY.
Если IRQ не описан в DTS, не подключен в Vivado или request_irq завершился ошибкой, драйвер может продолжить работу в polling-режиме. Это нормальный сценарий для отладки.
i2c_master_axi_hw_init: настройка IP-блока
После того как probe() получил MMIO, clock и параметры из Device Tree, нужно настроить сам IP. Этим занимается i2c_master_axi_hw_init. Упрощенная логика:
static int i2c_master_axi_hw_init(struct i2c_master_axi *i) { u32 prescale; if (!i->input_hz || !i->bus_hz) return -EINVAL; if (i->bus_hz * 4 > i->input_hz) return -EINVAL; prescale = i->input_hz / (4 * i->bus_hz) - 1; if (prescale > 0xffff) return -EINVAL; axi_write(i, I2C_REG_CTRL, 0); axi_write(i, I2C_REG_PRESCALE, prescale); axi_write(i, I2C_REG_ISR, ISR_DONE | ISR_AL); axi_write(i, I2C_REG_CTRL, CTRL_EN | (i->use_irq ? CTRL_IEN : 0)); dev_info(i->dev, "input=%u Hz, bus=%u Hz, prescale=%u, irq=%s\n", i->input_hz, i->bus_hz, prescale, i->use_irq ? "yes" : "polled"); return 0; }
Порядок записи важен. Сначала контроллер выключается:
axi_write(i, I2C_REG_CTRL, 0);
Так безопаснее менять PRESCALE. Если менять делитель во время активной транзакции, можно получить некорректную форму SCL или подвешенное состояние шины.
Затем записывается PRESCALE:
axi_write(i, I2C_REG_PRESCALE, prescale);
Затем очищаются старые флаги прерываний:
axi_write(i, I2C_REG_ISR, ISR_DONE | ISR_AL);
И только потом контроллер включается:
axi_write(i, I2C_REG_CTRL, CTRL_EN | ...);
Если используется IRQ, дополнительно включается CTRL_IEN. После этого IP готов принимать команды через регистр CMD. Строка dev_info очень полезна при первом запуске. В dmesg ожидаем увидеть примерно:
i2c-master-axi 43c00000.i2c: input=50000000 Hz, bus=100000 Hz, prescale=124, irq=polled
или:
i2c-master-axi 43c00000.i2c: input=50000000 Hz, bus=100000 Hz, prescale=124, irq=yes
Если prescale не равен 124 при 50 MHz и 100 kHz, нужно проверять:
input-clock-frequency в DTS;
clock-frequency в DTS;
FCLK0 в Vivado;
получение clock через clock framework;
порядок применения DTB на SD-карте.
probe: главный вход драйвера
probe() - центральная функция platform-драйвера. Она вызывается, когда ядро нашло устройство из Device Tree, совместимое с этим драйвером. Условие вызова:
в DTB есть узел compatible = “user,i2c-master-axi-1.0”;
модуль i2c-master-axi загружен;
of_match_table драйвера содержит ту же строку.
Упрощенный порядок probe():
Выделить struct i2c_master_axi.
Сохранить указатель на struct device.
Инициализировать completion.
Получить MMIO-регистры через ioremap.
Получить clock.
Прочитать input-clock-frequency и clock-frequency.
Попробовать получить IRQ.
Если IRQ есть, зарегистрировать обработчик.
Выполнить hw_init.
Заполнить struct i2c_adapter.
Привязать adapter data.
Зарегистрировать адаптер через i2c_add_adapter.
Вернуть 0.
Разберем по смыслу.
Выделение состояния
В начале создается структура:
i = devm_kzalloc(&pdev->dev, sizeof(*i), GFP_KERNEL);
devm_kzalloc удобен тем, что память автоматически освободится при отвязке устройства. Это снижает риск утечек в error paths. Дальше:
i->dev = &pdev->dev; platform_set_drvdata(pdev, i); init_completion(&i->cmd_done);
platform_set_drvdata нужен, чтобы позже remove() и другие части драйвера могли получить тот же указатель.
MMIO
Ключевой шаг:
i->regs = devm_platform_ioremap_resource(pdev, 0);
Он берет первый ресурс из reg:
reg = <0x43c00000 0x1000>;
Если здесь ошибка, драйвер дальше не стартует. Типовые причины:
в DTB нет нужного узла;
адрес в DTS неверный;
размер окна некорректный;
ресурс конфликтует; загружен не тот DTB.
Clock и частоты
Драйвер пытается получить clock:
i->clk = devm_clk_get_optional(...);
Если clock есть, он включается:
clk_prepare_enable(i->clk);
И частота читается:
i->input_hz = clk_get_rate(i->clk);
Если clock framework не дал частоту, драйвер использует fallback, например 50 MHz, или читает свойство:
input-clock-frequency = <50000000>;
Целевая частота I2C читается из:
clock-frequency = <100000>;
Если свойства нет, обычно используется 100 kHz по умолчанию.
IRQ
Драйвер пробует получить IRQ:
irq = platform_get_irq_optional(pdev, 0);
Если IRQ есть, регистрирует обработчик:
devm_request_irq(..., i2c_master_axi_isr, ...);
Если получилось:
use_irq = true
Если нет:
use_irq = false
Ошибка IRQ не обязательно должна быть фатальной, если драйвер поддерживает polling. Для bring-up это практично: можно сначала поднять шину без прерываний, а потом включить IRQ.
Инициализация IP
После получения ресурсов вызывается:
i2c_master_axi_hw_init(i);
Если здесь ошибка, адаптер не регистрируется. Например, если input_hz = 0, bus_hz = 0 или PRESCALE не помещается в регистр, probe() должен завершиться ошибкой.
Регистрация I2C-адаптера
После успешной настройки железа заполняется struct i2c_adapter. Обычно задаются:
owner;
algo;
quirks;
dev.parent;
dev.of_node;
name.
Особенно важно:
i->adap.algo = &axi_algo; i->adap.dev.parent = &pdev->dev; i->adap.dev.of_node = pdev->dev.of_node; i2c_set_adapdata(&i->adap, i);
dev.of_node нужен, чтобы I2C core смог увидеть дочерние устройства из Device Tree, расположенные внутри i2c@43c00000, например oled@3c. Затем:
ret = i2c_add_adapter(&i->adap);
После этого в Linux появляется I2C-шина. Если i2c_add_adapter() прошел успешно, ядро может начать создавать дочерние I2C-устройства из Device Tree. Для нашего OLED это означает попытку привязать узел oled@3c к драйверу ssd1307fb.
remove: обратная сторона probe
Если модуль выгружается или устройство отвязывается, вызывается remove(). Упрощенно:
static int i2c_master_axi_remove(struct platform_device *pdev) { struct i2c_master_axi *i = platform_get_drvdata(pdev); i2c_del_adapter(&i->adap); axi_write(i, I2C_REG_CTRL, 0); if (i->clk) clk_disable_unprepare(i->clk); return 0; }
Порядок важен. Сначала удаляется I2C-адаптер:
i2c_del_adapter(&i->adap);
После этого ядро больше не будет отправлять новые I2C-транзакции через этот контроллер, а клиенты шины будут отвязаны. Затем выключается IP-блок:
axi_write(i, I2C_REG_CTRL, 0);
Это сбрасывает CTRL.EN и запрещает новые операции секвенсера. Затем отключается clock, если он был включен драйвером. Ресурсы, выделенные через devm_*, освобождаются автоматически.
of_match_table, platform_driver и загрузка модуля
В конце файла находятся элементы, которые связывают модуль с Device Tree. Таблица совместимости:
static const struct of_device_id i2c_master_axi_of_match[] = { { .compatible = "user,i2c-master-axi-1.0" }, { } }; MODULE_DEVICE_TABLE(of, i2c_master_axi_of_match);
Пустая запись { } в конце обязательна. Это sentinel, по которому ядро понимает, что таблица закончилась. MODULE_DEVICE_TABLE экспортирует таблицу совместимости в метаданные модуля. Это полезно для автоматической загрузки и диагностики через modinfo.
Дальше объявляется platform driver:
static struct platform_driver i2c_master_axi_driver = { .driver = { .name = "i2c-master-axi", .of_match_table = i2c_master_axi_of_match, }, .probe = i2c_master_axi_probe, .remove = i2c_master_axi_remove, };
И регистрируется модуль:
module_platform_driver(i2c_master_axi_driver);
Этот макрос разворачивается в стандартные module_init и module_exit. При modprobe i2c-master-axi происходит следующее:
модуль загружается;
platform_driver регистрируется в ядре;
ядро сравнивает DT-узлы с of_match_table;
для i2c@43c00000 вызывается probe();
probe регистрирует i2c_adapter;
дочерний oled@3c становится доступен I2C core.
Если modprobe прошел успешно, это еще не значит, что probe() был вызван. modprobe только загружает модуль. Для вызова probe() нужно совпадение с Device Tree.
Поэтому при отладке нужно различать:
модуль загружен;
драйвер зарегистрирован;
probe вызван;
MMIO успешно отображен;
адаптер I2C зарегистрирован;
OLED найден на шине.
Это разные уровни.
Как ssd1307fb добирается до OLED
Важный момент: драйвер i2c-master-axi не вызывает ssd1307fb напрямую. Связь идет через стандартную модель Linux. В Device Tree OLED является дочерним узлом внутри I2C-контроллера:
i2c_pl: i2c@43c00000 { ... ssd1306: oled@3c { compatible = "solomon,ssd1306fb-i2c"; reg = <0x3c>; ... }; };
Когда i2c-master-axi регистрирует i2c_adapter, I2C core смотрит дочерние узлы этого adapter node. Он видит oled@3c, создает I2C client с адресом 0x3c, затем ищет драйвер, у которого есть compatible:
solomon,ssd1306fb-i2c
Если в ядре включен CONFIG_FB_SSD1307, то находится встроенный драйвер ssd1307fb. Дальше этот драйвер уже использует обычный I2C API:
i2c_master_send(); i2c_transfer();
А Linux I2C core вызывает:
axi_master_xfer()
То есть цепочка такая:
ssd1307fb -> i2c_transfer() -> axi_master_xfer() -> axi_xfer_one() -> axi_send_cmd() -> axi_wait_tip() -> MMIO регистры IP -> SDA/SCL -> OLED 0x3c
Это хороший признак правильной архитектуры: драйвер контроллера не знает про OLED, а драйвер OLED не знает про AXI-регистры. Их связывает стандартная подсистема I2C.
Что проверять при отладке
После загрузки системы диагностику лучше вести слоями.
1. Загружен ли модуль
lsmod | grep i2c modinfo i2c-master-axi
Если модуля нет, probe() не мог быть вызван.
2. Вызвался ли probe
dmesg | grep -i i2c-master
Ожидаем строку вида:
i2c-master-axi 43c00000.i2c: input=50000000 Hz, bus=100000 Hz, prescale=124, irq=polled
или:
i2c-master-axi 43c00000.i2c: input=50000000 Hz, bus=100000 Hz, prescale=124, irq=yes
Если строки нет, нужно проверять:
compatible в DTS;
of_match_table в драйвере;
загружен ли нужный DTB;
есть ли узел i2c@43c00000 в /proc/device-tree;
загружен ли модуль.
3. Зарегистрировалась ли I2C-шина
ls /sys/bus/i2c/devices/
Ожидаем новый адаптер:
i2c-0 i2c-1 ...
Номер не фиксирован.
4. Отвечает ли OLED
i2cdetect -y 1
Номер 1 нужно заменить на фактический номер шины. На адресе 0x3c возможны варианты:
3c - устройство отвечает и свободно; UU - устройство уже занято kernel driver, например ssd1307fb; -- - ответа нет.
UU в данном случае может быть хорошим признаком: это значит, что адрес уже привязан к драйверу.
5. Появился ли framebuffer
ls /dev/fb* dmesg | grep -iE "ssd|fb|framebuffer"
Если I2C работает, но framebuffer не появился, нужно проверять:
CONFIG_FB_SSD1307;
compatible OLED в DTS;
параметры solomon,width/height;
логи ssd1307fb;
наличие дочернего узла oled@3c в загруженном DTB.
Типовые ошибки и их смысл
Симптом |
Вероятная причина |
Где искать |
|
Не совпал |
DTS, DTB на FAT, |
|
Ошибка до |
MMIO, clock, |
|
Неверная входная частота или SCL |
|
|
IRQ не подключен или не зарегистрирован |
DTS |
|
|
bitstream, clock, reset, MMIO address |
|
NACK на адресном байте |
нет устройства на 0x3c, питание OLED, пины |
|
NACK на байте данных |
slave ответил на адрес, но отверг данные |
|
|
зависшая шина, arbitration lost |
|
OLED не отвечает |
питание, SA0, XDC, bitstream, pull-up |
|
Адрес занят драйвером |
Обычно нормально, если поднялся |
Итоговая логика драйвера
Если свернуть весь модуль до основной идеи, получается такая последовательность.
При загрузке:
module_platform_driver регистрирует platform_driver ↓ ядро сопоставляет compatible из DT с of_match_table ↓ вызывается probe ↓ probe получает MMIO, clock, IRQ и параметры частот ↓ hw_init рассчитывает PRESCALE и включает IP ↓ probe регистрирует i2c_adapter ↓ I2C core создает клиентов из дочерних DT-узлов ↓ ssd1307fb привязывается к oled@3c
При обмене:
ssd1307fb вызывает i2c_transfer ↓ I2C core вызывает axi_master_xfer ↓ axi_master_xfer перебирает struct i2c_msg ↓ axi_xfer_one формирует адрес, START, READ/WRITE, STOP ↓ axi_send_cmd пишет CMD ↓ axi_wait_tip ждет завершения ↓ драйвер проверяет RXACK, AL, timeout ↓ данные уходят через SDA/SCL на OLED
При выгрузке:
remove удаляет i2c_adapter ↓ клиенты I2C отвязываются ↓ CTRL.EN сбрасывается ↓ clock отключается ↓ devm-ресурсы освобождаются автоматически
Поэтому i2c-master-axi.c можно рассматривать как тонкий, но принципиально важный слой адаптации. Он не реализует графику и не зависит от Buildroot. Его задача - превратить AXI4-Lite IP-блок в стандартный Linux I2C adapter. После этого все остальное начинает работать через штатные механизмы ядра: Device Tree, I2C core, client drivers и framebuffer.
Кладем исходник драйвера в BR2_EXTERNAL
Теперь вернемся к формированию итогового набора артефактов и создадим каталог пакета и положим туда исходный файл драйвера.
mkdir -p "$BR_EXT/package/i2c-master-axi"
Если исходник уже есть в репозитории проекта, проще скопировать его:
export I2C_REPO="${I2C_REPO:-$HOME/sources/I2C_Master_Controller}" cp "$I2C_REPO/linux/drivers/i2c-master-axi/i2c-master-axi.c" \ "$BR_EXT/package/i2c-master-axi/i2c-master-axi.c" wc -l "$BR_EXT/package/i2c-master-axi/i2c-master-axi.c"
Ожидаемо это файл примерно на несколько сотен строк. Важно проверить несколько вещей:
Вверху файла есть SPDX-License-Identifier.
Строка compatible совпадает с DTS:
user,i2c-master-axi-1.0Карта регистров соответствует RTL IP-блока.
Формула PRESCALE соответствует реализации IP.
Драйвер поддерживает режим polling, если IRQ пока не подключен.
В probe есть регистрация
i2c_adapter.
Минимальная контрольная точка по compatible:
grep -n "user,i2c-master-axi-1.0" \ "$BR_EXT/package/i2c-master-axi/i2c-master-axi.c" grep -n "user,i2c-master-axi-1.0" \ "$BR_EXT/dts/zynq-mini-revb.dts"
Обе строки должны совпадать буквально.
Makefile для out-of-tree модуля
Рядом с i2c-master-axi.c нужен короткий Makefile в формате kbuild. Это не обычный Makefile для ручного вызова gcc. Модуль должен собираться системой сборки ядра, с тем же кросс-компилятором, теми же заголовками и той же версией ядра, что и основной uImage.
Создадим файл:
cat > "$BR_EXT/package/i2c-master-axi/Makefile" << 'EOF' # SPDX-License-Identifier: GPL-2.0+ # Out-of-tree kernel module for AXI I2C master obj-m += i2c-master-axi.o EOF
Главная строка здесь:
obj-m += i2c-master-axi.o
Она говорит kbuild: собрать объект i2c-master-axi.o как внешний модуль. На выходе получится i2c-master-axi.ko
Имя в obj-m должно совпадать с именем C-файла без расширения .c. Если исходник называется i2c-master-axi.c, то в Makefile должно быть именно: obj-m += i2c-master-axi.o
Если имя не совпадает, сборка модуля завершится ошибкой kbuild.
Сам по себе этот Makefile еще ничего не запускает. Buildroot подхватит его позже через рецепт i2c-master-axi.mk, где будет использована инфраструктура kernel-module. В исходном фрагменте это описано как связка $(eval $(kernel-module)) и $(eval $(generic-package)): Buildroot сначала собирает ядро, затем вызывает kbuild для каталога внешнего модуля и устанавливает готовый .ko в rootfs.
Упрощенно Buildroot сделает следующее:
Соберет Linux kernel.
Возьмет каталог package/i2c-master-axi.
Запустит сборку модуля через дерево ядра.
Получит i2c-master-axi.ko.
Установит модуль в target/lib/modules//.
Упакует его в rootfs.
Это принципиально лучше, чем собирать .ko отдельно руками. Если собрать модуль для другой версии ядра, на плате можно получить:
insmod: invalid module format
Причина обычно в несовпадении vermagic.
Проверка структуры пакета
На этом этапе в каталоге пакета должно быть минимум два файла:
$BR_EXT/package/i2c-master-axi/ ├── i2c-master-axi.c └── Makefile
Быстрая проверка:
find "$BR_EXT/package/i2c-master-axi" -maxdepth 1 -type f -print
Ожидаемый вывод:
.../package/i2c-master-axi/i2c-master-axi.c .../package/i2c-master-axi/Makefile
Также стоит сразу проверить строки, от которых зависит привязка к DTS:
grep -n "MODULE_DEVICE_TABLE" "$BR_EXT/package/i2c-master-axi/i2c-master-axi.c" grep -n "of_match" "$BR_EXT/package/i2c-master-axi/i2c-master-axi.c" grep -n "i2c_add_adapter" "$BR_EXT/package/i2c-master-axi/i2c-master-axi.c" grep -n "PRESCALE" "$BR_EXT/package/i2c-master-axi/i2c-master-axi.c"
Если в исходнике нет of_match_table, то драйвер не сможет автоматически привязаться к узлу из Device Tree. Если нет i2c_add_adapter, то драйвер может работать с регистрами, но не создаст нормальную I2C-шину для Linux.
Как модуль должен выглядеть после сборки
После полной сборки Buildroot модуль должен появиться в целевой файловой системе:
find "$BR_OUT/target" -name 'i2c-master-axi.ko'
Обычно путь будет похож на:
$BR_OUT/target/lib/modules/<kernel-release>/extra/i2c-master-axi.ko
На загруженной плате проверка начинается с модуля:
modinfo i2c-master-axi
Затем проверяем, загружен ли он:
lsmod | grep i2c
Если автозагрузки еще нет, можно попробовать вручную:
modprobe i2c-master-axi
После этого основной диагностический источник - dmesg:
dmesg | grep -i i2c-master
Ожидаемая строка примерно такая:
i2c-master-axi 43c00000.i2c: input=50000000 Hz, bus=100000 Hz, prescale=124, irq=yes
или:
i2c-master-axi 43c00000.i2c: input=50000000 Hz, bus=100000 Hz, prescale=124, irq=polled
irq=polled не является ошибкой, если прерывание в PL пока не подключено или драйвер не смог зарегистрировать IRQ. Для первого запуска polling-режим даже удобнее: меньше зависимостей от корректности interrupt routing.
Дальше проверяем I2C-шину:
ls /sys/bus/i2c/devices/
Номер шины может отличаться, поэтому сначала смотрим список, а затем подставляем найденный номер в i2cdetect:
i2cdetect -y 1
На адресе 0x3c ожидается одно из двух:
3c - устройство отвечает и свободно;
UU - адрес уже занят kernel driver, например ssd1307fb.
Если драйвер i2c-master-axi успешно загрузился, но 0x3c пустой, проблема чаще находится ниже уровня Linux-драйвера: bitstream, XDC, пины, питание OLED, pull-up на I2C, адрес SA0 или физическое подключение дисплея. В исходном фрагменте это также выделено как типовая граница диагностики: при живом probe и пустом 3c следует проверять PL, проводку и bitstream, а не C-код драйвера.
Типовые ошибки на этом шаге
Симптом |
Вероятная причина |
Что проверить |
|---|---|---|
|
Не совпал |
DTS и |
В |
Модуль не загружен или нет DT-узла |
|
|
Неверная входная частота или |
DTS, Vivado FCLK0 |
|
Модуль собран для другого ядра |
|
|
OLED не отвечает |
питание, пины, XDC, bitstream, адрес SA0 |
|
Адрес занят драйвером |
Это нормально, если привязался |
Таймаут |
IP не отвечает или завис |
|
|
NACK на адресном байте |
нет устройства на I2C-адресе |
|
NACK на байте данных |
устройство ответило на адрес, но отвергло данные |
Интеграция модуля в Buildroot: Config.in и i2c-master-axi.mk
На предыдущем шаге мы положили в каталог пакета два файла:
package/i2c-master-axi/ ├── i2c-master-axi.c └── Makefile
Этого достаточно для kbuild, но еще недостаточно для Buildroot. Система сборки должна узнать, что такой пакет вообще существует, как его включать через конфигурацию и каким способом его собирать.
Для этого нужны еще два файла:
package/i2c-master-axi/ ├── Config.in ├── i2c-master-axi.mk ├── i2c-master-axi.c └── Makefile
Config.in добавляет пакет в систему конфигурации Buildroot, а i2c-master-axi.mk описывает рецепт сборки. Создадим Config.in:
cat > "$BR_EXT/package/i2c-master-axi/Config.in" << 'EOF' config BR2_PACKAGE_I2C_MASTER_AXI bool "i2c-master-axi (custom AXI I2C)" depends on BR2_LINUX_KERNEL help Out-of-tree kernel module for the i2c_master_axi PL IP (AXI4-Lite). Registers an i2c_adapter so the in-tree ssd1307fb driver can talk to the OLED on ZYNQ MINI Rev B. EOF
Этот файл описывает пользовательскую опцию Buildroot:
BR2_PACKAGE_I2C_MASTER_AXI
После подключения через верхнеуровневый Config.in внешнего дерева эта опция появится в menuconfig. Ее можно будет включить вручную или зафиксировать в defconfig:
BR2_PACKAGE_I2C_MASTER_AXI=y
Строка:
bool "i2c-master-axi (custom AXI I2C)"
задает название пункта в меню Buildroot.
Строка:
depends on BR2_LINUX_KERNEL
фиксирует зависимость от сборки Linux kernel. Это важно, потому что i2c-master-axi собирается не как обычная userspace-программа, а как kernel module. Для его сборки Buildroot должен иметь собранное или подготовленное дерево ядра.
Блок help нужен не для сборки, а для читаемости конфигурации. Он поясняет назначение пакета: это внешний модуль ядра для PL IP-блока i2c_master_axi, который регистрирует Linux i2c_adapter, чтобы встроенный драйвер ssd1307fb мог общаться с OLED-дисплеем на плате ZYNQ MINI Rev B.
Теперь создадим make-рецепт пакета:
cat > "$BR_EXT/package/i2c-master-axi/i2c-master-axi.mk" << 'EOF' ################################################################################ # i2c-master-axi - out-of-tree kernel module (i2c_master_axi PL IP) ################################################################################ I2C_MASTER_AXI_VERSION = 1.0 I2C_MASTER_AXI_SITE = $(BR2_EXTERNAL_ZYNQ_MINI_I2C_PATH)/package/i2c-master-axi I2C_MASTER_AXI_SITE_METHOD = local I2C_MASTER_AXI_LICENSE = GPL-2.0+ I2C_MASTER_AXI_LICENSE_FILES = i2c-master-axi.c $(eval $(kernel-module)) $(eval $(generic-package)) EOF
Разберем его по строкам.
I2C_MASTER_AXI_VERSION = 1.0
Это версия пакета внутри Buildroot. Для локального проектного модуля это может быть простая ручная версия. Она не обязана совпадать с версией ядра или версией bitstream, но в реальном проекте лучше синхронизировать ее с версией IP-блока или релизом BSP.
I2C_MASTER_AXI_SITE = $(BR2_EXTERNAL_ZYNQ_MINI_I2C_PATH)/package/i2c-master-axi
Эта строка говорит Buildroot, где лежат исходники пакета. Мы используем не внешний git-репозиторий и не tarball, а локальный каталог внутри BR2_EXTERNAL.
Переменная:
$(BR2_EXTERNAL_ZYNQ_MINI_I2C_PATH)
появилась из external.desc. Поэтому имя внешнего дерева в external.desc критично: если там задано name: ZYNQ_MINI_I2C, Buildroot создает переменную пути именно с таким именем:
BR2_EXTERNAL_ZYNQ_MINI_I2C_PATH
I2C_MASTER_AXI_SITE_METHOD = local
Эта строка указывает, что пакет нужно брать из локальной директории. Buildroot не будет ничего скачивать из сети. Он скопирует содержимое каталога package/i2c-master-axi во временную директорию сборки и запустит сборку оттуда.
I2C_MASTER_AXI_LICENSE = GPL-2.0+ I2C_MASTER_AXI_LICENSE_FILES = i2c-master-axi.c
Эти строки описывают лицензию пакета. Для kernel module это особенно важно: модуль использует API ядра Linux, поэтому лицензия должна быть совместима с GPL. Файл i2c-master-axi.c должен содержать соответствующий SPDX-заголовок, например:
// SPDX-License-Identifier: GPL-2.0+
Главная часть рецепта находится в конце:
$(eval $(kernel-module)) $(eval $(generic-package))
$(kernel-module) подключает инфраструктуру Buildroot для сборки внешних модулей ядра. Это означает, что Buildroot будет собирать пакет примерно по такой схеме:
make -C <linux-build-dir> M=<package-build-dir> modules
Где:
<linux-build-dir> - каталог собранного ядра Linux; <package-build-dir> - временная копия package/i2c-master-axi; M=... - указание kbuild собрать внешний модуль.
Дальше kbuild находит Makefile пакета:
obj-m += i2c-master-axi.o
и собирает модульi2c-master-axi.ko
$(generic-package) подключает стандартную инфраструктуру Buildroot-пакета: подготовку, копирование локальных исходников, staging/build/install steps и включение пакета в общий граф сборки.
В результате Buildroot понимает полный маршрут:
BR2_PACKAGE_I2C_MASTER_AXI=y ↓ подключить package/i2c-master-axi/i2c-master-axi.mk ↓ взять локальные исходники из BR2_EXTERNAL ↓ собрать модуль через kbuild ↓ установить i2c-master-axi.ko в rootfs
После создания этих двух файлов структура пакета должна выглядеть так:
package/i2c-master-axi/ ├── Config.in ├── i2c-master-axi.mk ├── i2c-master-axi.c └── Makefile
Быстрая проверка:
find "$BR_EXT/package/i2c-master-axi" -maxdepth 1 -type f -print
Ожидаемый результат:
.../package/i2c-master-axi/Config.in .../package/i2c-master-axi/i2c-master-axi.mk .../package/i2c-master-axi/i2c-master-axi.c .../package/i2c-master-axi/Makefile
Теперь нужно убедиться, что верхнеуровневый Config.in внешнего дерева действительно подключает конфигурацию пакета:
menu "Zynq mini I2C packages" source "$BR2_EXTERNAL_ZYNQ_MINI_I2C_PATH/package/i2c-master-axi/Config.in" endmenu
А external.mk подключает make-рецепты:
include $(sort $(wildcard $(BR2_EXTERNAL_ZYNQ_MINI_I2C_PATH)/package/*/*.mk))
Если Config.in не подключен, опция BR2_PACKAGE_I2C_MASTER_AXI не появится в menuconfig.
Если external.mk не подключает *.mk, Buildroot увидит опцию пакета, но не будет знать, как его собирать.
После этого пакет можно добавить в defconfig платы:
BR2_PACKAGE_I2C_MASTER_AXI=y
И при следующей сборке Buildroot должен собрать модуль вместе с остальной системой.
Проверка после сборки на стороне host:
find "$BR_OUT" -name 'i2c-master-axi.ko' find "$BR_OUT/target" -name 'i2c-master-axi.ko'
Обычно модуль должен оказаться в каталоге вида:
$BR_OUT/target/lib/modules/<kernel-release>/extra/i2c-master-axi.ko
Если модуль не появился, стоит проверить четыре точки:
В defconfig есть BR2_PACKAGE_I2C_MASTER_AXI=y.
package/i2c-master-axi/Config.in подключен из верхнеуровневого Config.in.
package/i2c-master-axi/i2c-master-axi.mk подключается через external.mk.
В каталоге пакета есть Makefile с obj-m += i2c-master-axi.o.
Типовые ошибки на этом шаге:
Симптом |
Вероятная причина |
|---|---|
Опции нет в |
Не подключен |
Опция есть, но пакет не собирается |
Не подключен |
Ошибка kbuild |
Имя в |
|
Пакет не включен через |
|
Модуль собран не для той версии ядра, которое загружено |
На этом пакет i2c-master-axi полностью описан для Buildroot. Следующий шаг - включить его в zynq_mini_revb_defconfig, добавить post-build действия для автозагрузки модуля и убедиться, что uImage, zynq-mini-revb.dtb, uEnv.txt и rootfs попадают в итоговый образ SD-карты.
Defconfig - сводная конфигурация Buildroot
Итак. К этому моменту мы уже подготовили отдельные элементы проекта в дереве BR2_EXTERNAL:
board-support/ ├── external.desc ├── external.mk ├── Config.in ├── configs/ ├── dts/ │ └── zynq-mini-revb.dts ├── board/ │ └── zynq_mini_revb/ │ ├── linux.fragment │ ├── uboot.fragment │ └── uEnv.txt └── package/ └── i2c-master-axi/ ├── Config.in ├── i2c-master-axi.mk ├── i2c-master-axi.c └── Makefile
Пока это набор связанных, но отдельных файлов. Buildroot должен получить одну итоговую конфигурацию, которая скажет:
какую архитектуру собирать;
какой toolchain использовать;
какое ядро Linux взять;
какой defconfig ядра применить;
какой linux.fragment наложить поверх;
какой DTS собрать;
какой U-Boot собрать;
какой uboot.fragment применить;
какие пакеты включить в rootfs;
какой out-of-tree модуль собрать;
какие post-build и post-image скрипты вызвать;
какие образы rootfs и SD-карты сформировать.
Эту роль выполняет defconfig Buildroot. defconfig - это текстовый снимок настроек Buildroot в формате:
BR2_...=y BR2_...=n BR2_...="строка"
Технически это сокращенная версия большого .config, который обычно получается после make menuconfig. Полный .config может содержать тысячи строк, а defconfig фиксирует только значимые отличия от значений по умолчанию.
Главное преимущество defconfig - воспроизводимость. Его можно положить в git, передать на другую машину, применить к чистому Buildroot и получить такую же конфигурацию без ручного прохода по меню.
Для нашей платы файл будет называться:
$BR_EXT/configs/zynq_mini_revb_defconfig
Имя zynq_mini_revb здесь является именем платы внутри внешнего дерева Buildroot. Из него автоматически получается make-цель:
make BR2_EXTERNAL="$BR_EXT" O="$BR_OUT" zynq_mini_revb_defconfig
То есть файл:
configs/zynq_mini_revb_defconfig
превращается в цель:
zynq_mini_revb_defconfig
Создание файла zynq_mini_revb_defconfig
Создадим каталог configs и сам файл defconfig:
mkdir -p "$BR_EXT/configs" cat > "$BR_EXT/configs/zynq_mini_revb_defconfig" << 'EOF' # ZYNQ MINI Rev B # # Apply with: # cd <buildroot-source> # make BR2_EXTERNAL=<repo>/board-support O=<output-dir> zynq_mini_revb_defconfig # --- Architecture ---------------------------------------------------- BR2_arm=y BR2_cortex_a9=y BR2_ARM_FPU_NEON=y BR2_ARM_INSTRUCTIONS_THUMB2=y # --- Toolchain ------------------------------------------------------- BR2_TOOLCHAIN_BUILDROOT_GLIBC=y BR2_TOOLCHAIN_BUILDROOT_CXX=y BR2_TOOLCHAIN_BUILDROOT_WCHAR=y # --- Linux kernel ---------------------------------------------------- BR2_LINUX_KERNEL=y BR2_LINUX_KERNEL_LATEST_VERSION=y BR2_LINUX_KERNEL_USE_ARCH_DEFAULT_CONFIG=n BR2_LINUX_KERNEL_USE_DEFCONFIG=y BR2_LINUX_KERNEL_DEFCONFIG="multi_v7" BR2_LINUX_KERNEL_CONFIG_FRAGMENT_FILES="$(BR2_EXTERNAL_ZYNQ_MINI_I2C_PATH)/board/zynq_mini_revb/linux.fragment" BR2_LINUX_KERNEL_DTS_SUPPORT=y BR2_LINUX_KERNEL_CUSTOM_DTS_PATH="$(BR2_EXTERNAL_ZYNQ_MINI_I2C_PATH)/dts/zynq-mini-revb.dts" BR2_LINUX_KERNEL_UIMAGE=y BR2_LINUX_KERNEL_UIMAGE_LOADADDR="0x8000" # --- U-Boot ---------------------------------------------------------- BR2_TARGET_UBOOT=y BR2_TARGET_UBOOT_LATEST_VERSION=y BR2_TARGET_UBOOT_BOARD_DEFCONFIG="xilinx_zynq_virt" BR2_TARGET_UBOOT_CONFIG_FRAGMENT_FILES="$(BR2_EXTERNAL_ZYNQ_MINI_I2C_PATH)/board/zynq_mini_revb/uboot.fragment" BR2_TARGET_UBOOT_FORMAT_BIN=y BR2_TARGET_UBOOT_FORMAT_IMG=y BR2_TARGET_UBOOT_FORMAT_ELF=y BR2_TARGET_UBOOT_NEEDS_DTC=y BR2_TARGET_UBOOT_NEEDS_PYLIBFDT=y BR2_TARGET_UBOOT_NEEDS_OPENSSL=y BR2_TARGET_UBOOT_NEEDS_GNUTLS=y # --- Init / system --------------------------------------------------- BR2_INIT_BUSYBOX=y BR2_TARGET_GENERIC_HOSTNAME="zynq-mini" BR2_TARGET_GENERIC_ISSUE="ZYNQ MINI Rev B - I2C_Master_Controller" BR2_TARGET_GENERIC_GETTY_PORT="ttyPS0" BR2_TARGET_GENERIC_GETTY_BAUDRATE_115200=y BR2_SYSTEM_DHCP="eth0" BR2_PACKAGE_BUSYBOX_SHOW_OTHERS=y # --- Userspace utilities -------------------------------------------- BR2_PACKAGE_I2C_TOOLS=y BR2_PACKAGE_FBSET=y BR2_PACKAGE_FBV=y BR2_PACKAGE_DROPBEAR=y BR2_PACKAGE_HTOP=y BR2_PACKAGE_FILE=y BR2_PACKAGE_NANO=y # --- Out-of-tree i2c-master-axi module ------------------------------ BR2_PACKAGE_I2C_MASTER_AXI=y # --- Root filesystem ------------------------------------------------- BR2_TARGET_ROOTFS_EXT2=y BR2_TARGET_ROOTFS_EXT2_4=y BR2_TARGET_ROOTFS_EXT2_SIZE="256M" BR2_TARGET_ROOTFS_TAR=y # --- Host tools for SD image ---------------------------------------- BR2_PACKAGE_HOST_GENIMAGE=y BR2_PACKAGE_HOST_DOSFSTOOLS=y BR2_PACKAGE_HOST_MTOOLS=y BR2_PACKAGE_HOST_GENEXT2FS=y # --- Post processing ------------------------------------------------- BR2_ROOTFS_POST_BUILD_SCRIPT="$(BR2_EXTERNAL_ZYNQ_MINI_I2C_PATH)/board/zynq_mini_revb/post-build.sh" BR2_ROOTFS_POST_IMAGE_SCRIPT="$(BR2_EXTERNAL_ZYNQ_MINI_I2C_PATH)/board/zynq_mini_revb/post-image.sh" EOF
В этом heredoc используется закрывающий маркер в кавычках:
<< 'EOF'
Это важно. Shell не должен раскрывать выражения вида $(BR2_EXTERNAL_ZYNQ_MINI_I2C_PATH) во время создания файла.
Эти строки должны попасть в defconfig именно в буквальном виде. Потом их будет интерпретировать Buildroot, а не текущий shell.
Проверим, что переменные не раскрылись преждевременно:
grep 'BR2_EXTERNAL_ZYNQ_MINI_I2C_PATH' \ "$BR_EXT/configs/zynq_mini_revb_defconfig"
В выводе должны остаться строки с буквальным текстом:
$(BR2_EXTERNAL_ZYNQ_MINI_I2C_PATH)
Если вместо этого получились пустые пути вроде:
/board/zynq_mini_revb/linux.fragment
значит файл был создан неправильно. Нужно пересоздать его через << 'EOF'.
Архитектура
Первый блок задает целевую архитектуру:
BR2_arm=y BR2_cortex_a9=y BR2_ARM_FPU_NEON=y BR2_ARM_INSTRUCTIONS_THUMB2=y
Zynq-7000 содержит процессорную систему на ARM Cortex-A9. Это 32-битная ARM-платформа, не AArch64 и не MicroBlaze.
Значение:
BR2_arm=y
говорит Buildroot, что целевая архитектура - ARM.
Значение:
BR2_cortex_a9=y
уточняет профиль CPU. Это влияет на параметры компилятора и оптимизации.
Опция:
BR2_ARM_FPU_NEON=y
включает использование NEON/FPU, доступных в Cortex-A9.
Опция:
BR2_ARM_INSTRUCTIONS_THUMB2=y
разрешает Thumb-2. Это может уменьшать размер кода и соответствует типичным настройкам для ARMv7.
Итог: Buildroot будет собирать 32-битную ARM Linux-систему под Cortex-A9.
Toolchain
Блок toolchain:
BR2_TOOLCHAIN_BUILDROOT_GLIBC=y BR2_TOOLCHAIN_BUILDROOT_CXX=y BR2_TOOLCHAIN_BUILDROOT_WCHAR=y
говорит Buildroot собрать собственный кросс-компилятор и стандартную библиотеку.
BR2_TOOLCHAIN_BUILDROOT_GLIBC=y выбирает glibc. Это более тяжелый вариант по сравнению с musl или uClibc, но он удобен для отладки, совместимости и привычного Linux userspace.
BR2_TOOLCHAIN_BUILDROOT_CXX=y включает поддержку C++. Для текущего I2C-драйвера она не нужна, потому что модуль написан на C. Но C++ может понадобиться для userspace-утилит, диагностических программ или будущих тестовых приложений.
BR2_TOOLCHAIN_BUILDROOT_WCHAR=y включает wide char. Некоторые userspace-пакеты требуют поддержку wchar на уровне toolchain.
Можно использовать внешний toolchain, например из Vitis или Linaro, но для учебного проекта собственный toolchain Buildroot проще с точки зрения воспроизводимости: сборка меньше зависит от того, что установлено на конкретной рабочей машине.
Linux kernel
Блок ядра:
BR2_LINUX_KERNEL=y BR2_LINUX_KERNEL_LATEST_VERSION=y BR2_LINUX_KERNEL_USE_ARCH_DEFAULT_CONFIG=n BR2_LINUX_KERNEL_USE_DEFCONFIG=y BR2_LINUX_KERNEL_DEFCONFIG="multi_v7" BR2_LINUX_KERNEL_CONFIG_FRAGMENT_FILES="$(BR2_EXTERNAL_ZYNQ_MINI_I2C_PATH)/board/zynq_mini_revb/linux.fragment"
включает сборку Linux kernel и задает принцип формирования его .config.
Опция:
BR2_LINUX_KERNEL=y
включает сборку ядра.
Опция:
BR2_LINUX_KERNEL_LATEST_VERSION=y
говорит Buildroot использовать актуальную версию ядра, заданную в выбранной версии Buildroot. Для жестко воспроизводимого проекта лучше позже заменить это на конкретную версию Linux. На этапе bring-up вариант LATEST_VERSION удобен, но он зависит от версии Buildroot.
Опции:
BR2_LINUX_KERNEL_USE_DEFCONFIG=y BR2_LINUX_KERNEL_DEFCONFIG="multi_v7"
говорят взять базовую конфигурацию ядра:
arch/arm/configs/multi_v7_defconfig
multi_v7_defconfig - это типовой ARM multiplatform defconfig. Он подходит для многих ARMv7-платформ, включая Zynq-7000.
Но одного multi_v7_defconfig недостаточно. Нам нужны дополнительные опции для OLED, framebuffer, I2C, QSPI, USB, отладки и т.д. Поэтому поверх базовой конфигурации накладывается наш фрагмент:
BR2_LINUX_KERNEL_CONFIG_FRAGMENT_FILES="$(BR2_EXTERNAL_ZYNQ_MINI_I2C_PATH)/board/zynq_mini_revb/linux.fragment"
Итоговая логика:
multi_v7_defconfig + linux.fragment ↓ итоговый .config ядра
Если в linux.fragment включен CONFIG_FB_SSD1307=y, то встроенный драйвер OLED попадет в ядро. Если в linux.fragment включен CONFIG_I2C_CHARDEV=y, то появятся userspace-интерфейсы /dev/i2c-*.
Важно: linux.fragment не заменяет Device Tree. Он только говорит, какие драйверы и подсистемы собрать. DTS отдельно описывает, какие устройства есть на плате.
Device Tree и формат ядра
Следующий блок отвечает за сборку нашего DTS:
BR2_LINUX_KERNEL_DTS_SUPPORT=y BR2_LINUX_KERNEL_CUSTOM_DTS_PATH="$(BR2_EXTERNAL_ZYNQ_MINI_I2C_PATH)/dts/zynq-mini-revb.dts"
BR2_LINUX_KERNEL_DTS_SUPPORT=y включает сборку Device Tree.
BR2_LINUX_KERNEL_CUSTOM_DTS_PATH указывает путь к нашему файлу:
$BR_EXT/dts/zynq-mini-revb.dts
Buildroot скопирует этот DTS в дерево сборки ядра и соберет из него бинарный файл:
zynq-mini-revb.dtb
Он должен появиться в каталоге:
$BR_OUT/images/
Далее задается формат ядра:
BR2_LINUX_KERNEL_UIMAGE=y BR2_LINUX_KERNEL_UIMAGE_LOADADDR="0x8000"
Для U-Boot на Zynq удобно использовать uImage, то есть legacy U-Boot image с заголовком. Этот формат запускается командой:
bootm
Адрес загрузки:
0x8000
является типичным адресом для ARM Linux на платформах, где DDR начинается с 0x0. Это start_of_DDR + 32 KiB.
Кавычки вокруг 0x8000 обязательны, потому что в Buildroot эта опция имеет строковый тип.
Итог этого блока:
Buildroot соберет uImage;
Buildroot соберет zynq-mini-revb.dtb;
оба файла попадут в $BR_OUT/images;
post-image позже положит их на FAT-раздел SD-карты.
U-Boot
Блок U-Boot:
BR2_TARGET_UBOOT=y BR2_TARGET_UBOOT_LATEST_VERSION=y BR2_TARGET_UBOOT_BOARD_DEFCONFIG="xilinx_zynq_virt" BR2_TARGET_UBOOT_CONFIG_FRAGMENT_FILES="$(BR2_EXTERNAL_ZYNQ_MINI_I2C_PATH)/board/zynq_mini_revb/uboot.fragment" BR2_TARGET_UBOOT_FORMAT_BIN=y BR2_TARGET_UBOOT_FORMAT_IMG=y BR2_TARGET_UBOOT_FORMAT_ELF=y BR2_TARGET_UBOOT_NEEDS_DTC=y BR2_TARGET_UBOOT_NEEDS_PYLIBFDT=y BR2_TARGET_UBOOT_NEEDS_OPENSSL=y BR2_TARGET_UBOOT_NEEDS_GNUTLS=y
BR2_TARGET_UBOOT=y включает сборку U-Boot.
BR2_TARGET_UBOOT_BOARD_DEFCONFIG="xilinx_zynq_virt" задает базовый defconfig U-Boot для Zynq.
Поверх него накладывается наш фрагмент:
BR2_TARGET_UBOOT_CONFIG_FRAGMENT_FILES="$(BR2_EXTERNAL_ZYNQ_MINI_I2C_PATH)/board/zynq_mini_revb/uboot.fragment"
В uboot.fragment мы задавали:
CONFIG_OF_EMBED=y;
CONFIG_BOOTCOMMAND;
поддержку FAT;
поддержку ext4;
поддержку bootm;
поддержку MMC;
приглашение "zynq-mini>".
То есть базовый U-Boot берется из upstream, а проектное поведение загрузки добавляется точечно через фрагмент.
Форматы:
BR2_TARGET_UBOOT_FORMAT_BIN=y BR2_TARGET_UBOOT_FORMAT_IMG=y BR2_TARGET_UBOOT_FORMAT_ELF=y
нужны для разных сценариев.
u-boot.bin и u-boot.img полезны как обычные артефакты U-Boot.
u-boot.elf нужен для Zynq boot flow, потому что итоговый BOOT.BIN собирается через bootgen из нескольких компонентов:
FSBL.elf bitstream.bit u-boot.elf
Buildroot сам не генерирует полноценный BOOT.BIN, потому что FSBL и bitstream приходят из Vivado/Vitis. Но Buildroot должен дать корректный u-boot.elf, который потом будет включен в BOOT.BIN.
Опции:
BR2_TARGET_UBOOT_NEEDS_DTC=y BR2_TARGET_UBOOT_NEEDS_PYLIBFDT=y BR2_TARGET_UBOOT_NEEDS_OPENSSL=y BR2_TARGET_UBOOT_NEEDS_GNUTLS=y
подтягивают host-зависимости для сборки U-Boot и его инструментов. На практике это уменьшает риск ошибок сборки host-tools U-Boot, связанных с dtc, libfdt, crypto и capsule/image tooling.
Init, консоль и базовая система
Блок:
BR2_INIT_BUSYBOX=y BR2_TARGET_GENERIC_HOSTNAME="zynq-mini" BR2_TARGET_GENERIC_ISSUE="ZYNQ MINI Rev B - I2C_Master_Controller" BR2_TARGET_GENERIC_GETTY_PORT="ttyPS0" BR2_TARGET_GENERIC_GETTY_BAUDRATE_115200=y BR2_SYSTEM_DHCP="eth0" BR2_PACKAGE_BUSYBOX_SHOW_OTHERS=y
задает базовое поведение rootfs.
BR2_INIT_BUSYBOX=y выбирает BusyBox init. Это простой init, который хорошо подходит для компактного embedded-образа.
BR2_TARGET_GENERIC_HOSTNAME="zynq-mini" задает hostname.
BR2_TARGET_GENERIC_ISSUE задает текст, который будет показан перед login prompt.
BR2_TARGET_GENERIC_GETTY_PORT="ttyPS0" и BR2_TARGET_GENERIC_GETTY_BAUDRATE_115200=y включают login-консоль на UART:
/dev/ttyPS0 115200 baud
Это согласуется с bootargs: onsole=ttyPS0,115200 и с stdout-path в DTS.
BR2_SYSTEM_DHCP="eth0" включает DHCP для интерфейса eth0. Если Ethernet MAC и PHY описаны корректно, система сможет получить адрес по DHCP.
BR2_PACKAGE_BUSYBOX_SHOW_OTHERS=y позволяет видеть и выбирать некоторые пакеты, которые функционально пересекаются с BusyBox-апплетами. Это удобно при добавлении отдельных userspace-утилит.
Userspace-утилиты
Блок userspace:
BR2_PACKAGE_I2C_TOOLS=y BR2_PACKAGE_FBSET=y BR2_PACKAGE_FBV=y BR2_PACKAGE_DROPBEAR=y BR2_PACKAGE_HTOP=y BR2_PACKAGE_FILE=y BR2_PACKAGE_NANO=y
добавляет в rootfs утилиты, полезные для отладки.
i2c-tools дает команды:
i2cdetect i2cget i2cset i2cdump
Для нашей платы это основной userspace-инструмент проверки I2C-шины. После загрузки драйвера i2c-master-axi можно выполнить:
i2cdetect -y 1
и проверить, отвечает ли OLED на адресе 0x3c.
fbset показывает параметры framebuffer. Это полезно после привязки ssd1307fb.
fbv позволяет выводить изображения на framebuffer. Его можно использовать для быстрой проверки /dev/fb0.
dropbear добавляет SSH-сервер. Это удобно после поднятия Ethernet.
htop, file, nano не обязательны для финального образа, но полезны при разработке. В production-образе их можно убрать.
Out-of-tree модуль i2c-master-axi
Ключевая строка для нашего драйвера:
BR2_PACKAGE_I2C_MASTER_AXI=y
Она включает пакет, который мы описали ранее в:
package/i2c-master-axi/Config.in package/i2c-master-axi/i2c-master-axi.mk
Если этой строки нет, то:
исходник i2c-master-axi.c может лежать на диске; Makefile может быть корректным; i2c-master-axi.mk может быть написан правильно; но Buildroot не будет собирать пакет; i2c-master-axi.ko не попадет в rootfs; probe драйвера на плате не вызовется.
Поэтому наличие BR2_PACKAGE_I2C_MASTER_AXI=y - обязательная контрольная точка.
После успешной сборки модуль должен появиться в target rootfs:
find "$BR_OUT/target" -name 'i2c-master-axi.ko'
Обычно путь будет примерно таким:
$BR_OUT/target/lib/modules/<kernel-release>/extra/i2c-master-axi.ko
Но одного наличия .ko недостаточно. Позже post-build.sh добавит автозагрузку модуля через modules-load.d и init-скрипт для BusyBox init.
Rootfs и host-утилиты для SD-образа
Блок rootfs:
BR2_TARGET_ROOTFS_EXT2=y BR2_TARGET_ROOTFS_EXT2_4=y BR2_TARGET_ROOTFS_EXT2_SIZE="256M" BR2_TARGET_ROOTFS_TAR=y
создает root filesystem.
Несмотря на имя EXT2, опция:
BR2_TARGET_ROOTFS_EXT2_4=y
указывает формировать ext4-образ.
Размер:256M выбран с запасом для rootfs, модулей, отладочных утилит и логов. Для минимального production-образа размер можно уменьшить, но для bring-up 256 MiB удобнее.
BR2_TARGET_ROOTFS_TAR=y дополнительно создает tar-архив rootfs. Он полезен для диагностики, ручной распаковки или альтернативных сценариев подготовки носителя.
Далее включаются host-утилиты:
BR2_PACKAGE_HOST_GENIMAGE=y BR2_PACKAGE_HOST_DOSFSTOOLS=y BR2_PACKAGE_HOST_MTOOLS=y BR2_PACKAGE_HOST_GENEXT2FS=y
Они нужны не на целевой плате, а на машине сборки. genimage будет собирать итоговый sdcard.img. dosfstools и mtools нужны для создания и наполнения FAT-раздела. genext2fs нужен для генерации ext-раздела. На практике его отсутствие часто приводит к ошибке на этапе genimage, даже если на host-системе установлены обычные mkfs.ext4 утилиты.
Итоговая SD-карта будет состоять из двух разделов:
Раздел 1: FAT32 BOOT.BIN uImage zynq-mini-revb.dtb uEnv.txt Раздел 2: ext4 rootfs /lib/modules /etc /sbin /usr ...
bootargs указывают: root=/dev/mmcblk0p2 то есть Linux будет монтировать второй раздел SD-карты как rootfs.
Post-build и post-image скрипты
После настройки defconfig Buildroot уже знает, какие компоненты нужно собрать: toolchain, Linux kernel, DTB, U-Boot, rootfs, userspace-пакеты и out-of-tree модуль i2c-master-axi.
Но на выходе базовой сборки Buildroot мы получаем не полностью готовую SD-карту, а набор промежуточных артефактов:
$BR_OUT/target/ будущая корневая файловая система $BR_OUT/images/ uImage zynq-mini-revb.dtb u-boot u-boot.elf rootfs.ext2 / rootfs.ext4 другие файлы сборки
Этого уже достаточно для анализа, но еще недостаточно для удобного запуска платы.
Нам нужно дополнительно решить несколько задач:
Добавить автозагрузку модуля i2c-master-axi.ko в rootfs.
Настроить BusyBox init так, чтобы он реально загружал модули из /etc/modules-load.d.
Добавить login-консоль на tty1 для framebuffer/OLED-сценария.
При необходимости зафиксировать MAC-адрес eth0.
Подготовить uEnv.txt для U-Boot.
Описать разметку SD-карты через genimage.
Собрать итоговый sdcard.img с FAT-разделом и rootfs-разделом.
Учесть, что настоящий BOOT.BIN Buildroot сам не создает.
Для таких действий в Buildroot есть два механизма:
post-build script - вызывается после подготовки target rootfs, но до упаковки rootfs-образа;
post-image script вызывается после формирования бинарных артефактов в images/.
В нашем defconfig эти скрипты подключаются строками:
BR2_ROOTFS_POST_BUILD_SCRIPT="$(BR2_EXTERNAL_ZYNQ_MINI_I2C_PATH)/board/zynq_mini_revb/post-build.sh" BR2_ROOTFS_POST_IMAGE_SCRIPT="$(BR2_EXTERNAL_ZYNQ_MINI_I2C_PATH)/board/zynq_mini_revb/post-image.sh"
То есть Buildroot сам вызовет их в нужный момент сборки. Логика этапов получается такой:
Buildroot make -> собрать rootfs в $BR_OUT/target -> вызвать post-build.sh -> упаковать rootfs -> собрать uImage, DTB, U-Boot, rootfs image -> вызвать post-image.sh -> собрать sdcard.img через genimage
С точки зрения файлов:
post-build.sh работает с $TARGET_DIR, то есть с будущим содержимым ext4-раздела;
post-image.sh работает с $BINARIES_DIR, то есть с $BR_OUT/images; собирает итоговый sdcard.img.
Каталог board-файлов
Все board-specific скрипты положим в каталог:
$BR_EXT/board/zynq_mini_revb/
Создадим его, если он еще не создан:
mkdir -p "$BR_EXT/board/zynq_mini_revb"
К концу главы в нем должны появиться файлы:
$BR_EXT/board/zynq_mini_revb/ ├── linux.fragment ├── uboot.fragment ├── post-build.sh ├── post-image.sh ├── genimage.cfg └── uEnv.txt
linux.fragment и uboot.fragment уже были разобраны ранее. Здесь добавим четыре файла:
post-build.sh post-image.sh genimage.cfg uEnv.txt
Что делает post-build.sh
post-build.sh вызывается после того, как Buildroot подготовил каталог целевой rootfs:
$BR_OUT/target
Buildroot передает путь к этому каталогу первым аргументом скрипта:
post-build.sh <target-dir>
Поэтому в скрипте обычно первая строка логики такая:
TARGET="${1:?usage: post-build.sh <target-dir>}"
Все изменения внутри TARGET потом попадут в rootfs-раздел SD-карты. Для нашей платы post-build.sh будет делать следующее:
Создавать /etc/modules-load.d/i2c-master-axi.conf.
Создавать init-скрипт /etc/init.d/S03modules.
Добавлять getty на tty1.
Создавать /etc/eth0-mac.
Создавать init-скрипт /etc/init.d/S39set-eth0-mac.
Добавлять helper-скрипт /usr/local/bin/oled-console.
Создавать init-скрипт /etc/init.d/S45oled-console.
Добавлять удобный /etc/profile.d/zynq-mini.sh.
При необходимости обновлять /etc/issue.
Первые три пункта являются минимально необходимыми для автоматической загрузки модуля и framebuffer-консоли. Остальные пункты удобны для стабильной сетевой работы и работы OLED-консоли.
post-build.sh: полный файл
Создадим post-build.sh:
cat > "$BR_EXT/board/zynq_mini_revb/post-build.sh" << 'EOF' #!/usr/bin/env bash set -euo pipefail TARGET="${1:?usage: post-build.sh <target-dir>}" MAC_ADDR="${ZYNQ_MINI_MAC_ADDR:-00:0a:35:01:02:03}" echo "post-build: target=${TARGET}" # -------------------------------------------------------------------- # 1. Autoload i2c-master-axi module # -------------------------------------------------------------------- install -d "${TARGET}/etc/modules-load.d" cat > "${TARGET}/etc/modules-load.d/i2c-master-axi.conf" <<'MOD' i2c-master-axi MOD # BusyBox init does not process /etc/modules-load.d by itself. # Add a small init script that loads modules listed in *.conf files. install -d "${TARGET}/etc/init.d" cat > "${TARGET}/etc/init.d/S03modules" <<'INIT' #!/bin/sh case "$1" in start|"") [ -d /etc/modules-load.d ] || exit 0 for f in /etc/modules-load.d/*.conf; do [ -e "$f" ] || continue while IFS= read -r mod; do case "$mod" in ""|#*) continue ;; esac echo "Loading module: $mod" modprobe "$mod" 2>/dev/null || true done < "$f" done ;; stop|restart|reload) : ;; *) echo "usage: $0 {start|stop|restart|reload}" exit 1 ;; esac exit 0 INIT chmod 0755 "${TARGET}/etc/init.d/S03modules" # -------------------------------------------------------------------- # 2. Add getty on tty1 for framebuffer / OLED console # -------------------------------------------------------------------- if [ -f "${TARGET}/etc/inittab" ]; then if ! grep -q '^tty1::' "${TARGET}/etc/inittab"; then sed -i '/^ttyPS0::/a tty1::respawn:/sbin/getty -L tty1 0 linux' \ "${TARGET}/etc/inittab" || true fi fi # -------------------------------------------------------------------- # 3. Fixed MAC address for eth0 # -------------------------------------------------------------------- install -d "${TARGET}/etc" cat > "${TARGET}/etc/eth0-mac" <<MAC ${MAC_ADDR} MAC cat > "${TARGET}/etc/init.d/S39set-eth0-mac" <<'INIT' #!/bin/sh MAC_FILE="/etc/eth0-mac" IFACE="eth0" case "$1" in start|"") [ -r "$MAC_FILE" ] || exit 0 MAC="$(cat "$MAC_FILE" | tr -d '[:space:]')" case "$MAC" in ""|*[!0-9a-fA-F:]*) echo "Invalid MAC in $MAC_FILE: $MAC" exit 0 ;; esac if ip link show "$IFACE" >/dev/null 2>&1; then echo "Setting $IFACE MAC address to $MAC" ip link set dev "$IFACE" down 2>/dev/null || true ip link set dev "$IFACE" address "$MAC" 2>/dev/null || true ip link set dev "$IFACE" up 2>/dev/null || true fi ;; stop|restart|reload) : ;; *) echo "usage: $0 {start|stop|restart|reload}" exit 1 ;; esac exit 0 INIT chmod 0755 "${TARGET}/etc/init.d/S39set-eth0-mac" # -------------------------------------------------------------------- # 4. OLED console helper # -------------------------------------------------------------------- install -d "${TARGET}/usr/local/bin" cat > "${TARGET}/usr/local/bin/oled-console" <<'SCRIPT' #!/bin/sh FB="${1:-/dev/fb0}" TTY="${2:-/dev/tty1}" echo "oled-console: framebuffer=${FB}, tty=${TTY}" if [ ! -e "$FB" ]; then echo "oled-console: $FB not found" exit 1 fi # Bind framebuffer console if vtconsole binding exists. for bind in /sys/class/vtconsole/vtcon*/bind; do [ -e "$bind" ] || continue echo 1 > "$bind" 2>/dev/null || true done # Make tty1 active when chvt is available. if command -v chvt >/dev/null 2>&1; then chvt 1 2>/dev/null || true fi # Clear screen on tty1 if possible. if [ -e "$TTY" ]; then printf '\033c' > "$TTY" 2>/dev/null || true fi exit 0 SCRIPT chmod 0755 "${TARGET}/usr/local/bin/oled-console" cat > "${TARGET}/etc/init.d/S45oled-console" <<'INIT' #!/bin/sh case "$1" in start|"") echo "Starting OLED console" i=0 while [ ! -e /dev/fb0 ] && [ "$i" -lt 10 ]; do sleep 1 i=$((i + 1)) done if [ -e /dev/fb0 ]; then /usr/local/bin/oled-console /dev/fb0 /dev/tty1 || true else echo "OLED console: /dev/fb0 not found" fi ;; stop) echo "Stopping OLED console" killall oled-clock 2>/dev/null || true for bind in /sys/class/vtconsole/vtcon*/bind; do [ -e "$bind" ] || continue echo 0 > "$bind" 2>/dev/null || true done ;; restart|reload) "$0" stop "$0" start ;; *) echo "usage: $0 {start|stop|restart|reload}" exit 1 ;; esac exit 0 INIT chmod 0755 "${TARGET}/etc/init.d/S45oled-console" # -------------------------------------------------------------------- # 5. Network defaults # -------------------------------------------------------------------- install -d "${TARGET}/etc/network" if [ ! -f "${TARGET}/etc/network/interfaces" ]; then cat > "${TARGET}/etc/network/interfaces" <<'NET' auto lo iface lo inet loopback auto eth0 iface eth0 inet dhcp NET fi # -------------------------------------------------------------------- # 6. Login banner and shell helpers # -------------------------------------------------------------------- cat > "${TARGET}/etc/issue" <<'ISSUE' ZYNQ MINI Rev B - Buildroot Linux UART console: ttyPS0 115200 Framebuffer console: tty1 if /dev/fb0 is available ISSUE install -d "${TARGET}/etc/profile.d" cat > "${TARGET}/etc/profile.d/zynq-mini.sh" <<'PROFILE' export PS1='zynq-mini:\w# ' alias ll='ls -la' alias dmesg-i2c='dmesg | grep -iE "i2c|ssd|fb|macb|phy"' alias show-fb='ls -l /dev/fb* 2>/dev/null || true' alias show-i2c='ls -l /dev/i2c-* 2>/dev/null || true' PROFILE chmod 0644 "${TARGET}/etc/profile.d/zynq-mini.sh" echo "post-build: done" EOF chmod +x "$BR_EXT/board/zynq_mini_revb/post-build.sh"
Разбор post-build.sh
Автозагрузка i2c-master-axi
Пакет Buildroot собирает модуль: i2c-master-axi.ko и кладет его в rootfs примерно сюда:
/lib/modules/<kernel-release>/extra/i2c-master-axi.ko
Но наличие .ko в rootfs не означает, что модуль автоматически загрузится. Для автозагрузки создается файл:
/etc/modules-load.d/i2c-master-axi.conf
с содержимым:i2c-master-axi
На системах с systemd такие файлы обычно обрабатываются автоматически. Но у нас выбран BusyBox init:
BR2_INIT_BUSYBOX=y
BusyBox init сам по себе не читает /etc/modules-load.d. Поэтому мы добавляем отдельный init-скрипт: /etc/init.d/S03modules
Он проходит по всем файлам:
/etc/modules-load.d/*.conf
и вызывает: modprobe <module-name>
Порядок S03 выбран намеренно. Модуль нужно загрузить рано, до пользовательского login и до запуска OLED-консоли. После загрузки i2c-master-axi должен выполниться probe() драйвера, зарегистрироваться I2C-шина, затем сможет привязаться ssd1307fb, после чего появится /dev/fb0.
Цепочка:
S03modules -> modprobe i2c-master-axi -> probe i2c@43c00000 -> i2c_add_adapter() -> oled@3c -> ssd1307fb -> /dev/fb0
Если S03modules не добавить, модуль можно будет загрузить вручную через UART:
modprobe i2c-master-axi
Но автоматического bring-up OLED после boot не будет.
Getty на tty1
UART-консоль у нас уже настроена через Buildroot:
BR2_TARGET_GENERIC_GETTY_PORT="ttyPS0" BR2_TARGET_GENERIC_GETTY_BAUDRATE_115200=y
Это дает login на последовательном порту. Но если OLED работает как framebuffer, полезно иметь отдельную виртуальную консоль:
tty1
Поэтому post-build.sh добавляет в /etc/inittab строку:
tty1::respawn:/sbin/getty -L tty1 0 linux
Это позволяет получить login на framebuffer-консоли. Практически это имеет смысл, если:
USB host работает.
Подключена USB-клавиатура.
/dev/fb0 создан драйвером ssd1307fb.
fbcon привязан к framebuffer.
UART ttyPS0 остается основной отладочной консолью. tty1 нужен для локального сценария "экран + клавиатура".
Фиксированный MAC-адрес
Для Ethernet желательно иметь стабильный MAC. Иначе DHCP lease может меняться между загрузками, а SSH-подключение будет неудобным.
В этой главе мы задаем MAC через файл:/etc/eth0-mac и init-скрипт:
/etc/init.d/S39set-eth0-mac
Он выполняется до типового сетевого старта S40network и делает:
ip link set dev eth0 down ip link set dev eth0 address "$MAC" ip link set dev eth0 up
Важно держать один и тот же MAC в трех местах:
Device Tree: local-mac-address = [00 0a 35 01 02 03];
U-Boot uEnv.txt: ethaddr=00:0a:35:01:02:03
Rootfs: /etc/eth0-mac
Для прототипа можно использовать пример:00:0a:35:01:02:03
Но для нескольких плат последние октеты должны отличаться. Нельзя включать в одну сеть несколько устройств с одинаковым MAC.
OLED console helper
Скрипт: /usr/local/bin/oled-console делает минимальную привязку framebuffer-консоли и переключение на tty1, если это возможно.
Init-скрипт:/etc/init.d/S45oled-console ждет появления: /dev/fb0до 10 секунд. Это нужно, потому что /dev/fb0 появляется не мгновенно. Сначала должен загрузиться модуль i2c-master-axi, затем должен отработать probe, затем I2C core должен создать OLED client, затем ssd1307fb должен создать framebuffer.
Порядок init-скриптов получается такой:
S03modules загружает i2c-master-axi
S39set-eth0-mac задает MAC до DHCP
S40network поднимает сеть
S45oled-console ждет /dev/fb0 и настраивает tty1
Если позже появится отдельный демон, который рисует на OLED, например oled-clock, нужно учитывать конфликт: S45oled-console и демон одновременно используют /dev/fb0. В таком случае перед тестом демона можно остановить консоль:
/etc/init.d/S45oled-console stop
genimage.cfg: разметка SD-карты
Теперь нужно описать, как из файлов Buildroot получить SD-образ. Для этого используется genimage. В defconfig должны быть включены host-пакеты:
BR2_PACKAGE_HOST_GENIMAGE=y BR2_PACKAGE_HOST_DOSFSTOOLS=y BR2_PACKAGE_HOST_MTOOLS=y BR2_PACKAGE_HOST_GENEXT2FS=y
Создадим genimage.cfg:
cat > "$BR_EXT/board/zynq_mini_revb/genimage.cfg" << 'EOF' image boot.vfat { vfat { files = { "BOOT.BIN", "uImage", "zynq-mini-revb.dtb", "uEnv.txt", } extraargs = "-F 32" label = "BOOT" } size = 64M } image rootfs.ext4 { ext4 { } mountpoint = "/" size = 256M } image sdcard.img { hdimage { } partition boot { partition-type = 0xC bootable = "true" image = "boot.vfat" } partition rootfs { partition-type = 0x83 image = "rootfs.ext4" } } EOF
Этот файл описывает три образа:
boot.vfat FAT32-раздел с загрузочными файлами;
rootfs.ext4 ext4-раздел с корневой файловой системой;
sdcard.img итоговый образ SD-карты с MBR и двумя разделами.
Раздел boot.vfat
Секция:image boot.vfatсоздает FAT-раздел.В него попадут только файлы, указанные в files:BOOT.BIN uImage zynq-mini-revb.dtb uEnv.txt
Это важный момент. Если не ограничить список файлов, можно получить попытку скопировать лишние артефакты в маленький FAT-раздел.
Строка:extraargs = "-F 32"заставляет mkfs.fat создать FAT32. Для Zynq это важно, потому что BootROM ожидает корректный загрузочный FAT-раздел. Если на маленьком разделе будет создан FAT16, плата может не найти BOOT.BIN.
Размер:size = 64Mвыбран с запасом. Это не только место под файлы, но и практичный способ обеспечить нормальное создание FAT32.
Раздел rootfs.ext4
Секция:image rootfs.ext4создает ext4-раздел с содержимым rootfs.
Содержимое берется из TARGET_DIR, который передается в genimage через --rootpath. Это уже rootfs после post-build.sh, то есть с добавленными:
/etc/modules-load.d/i2c-master-axi.conf /etc/init.d/S03modules /etc/init.d/S39set-eth0-mac /etc/init.d/S45oled-console /usr/local/bin/oled-console /etc/eth0-mac
Размер:size = 256Mдолжен быть согласован с rootfs-настройками в defconfig:
BR2_TARGET_ROOTFS_EXT2_SIZE="256M"
Командная строка ядра указывает:root=/dev/mmcblk0p2Это означает, что rootfs должен находиться на втором разделе SD-карты.
Итоговый sdcard.img
Секция:image sdcard.imgсоздает образ всей SD-карты.
В нем два раздела:
partition boot тип 0x0C, FAT32 LBA, bootable;
partition rootfs тип 0x83, Linux filesystem.
Итоговая структура SD:
sdcard.img ├── partition 1: FAT32, label BOOT │ ├── BOOT.BIN │ ├── uImage │ ├── zynq-mini-revb.dtb │ └── uEnv.txt └── partition 2: ext4 └── rootfs ├── /bin ├── /sbin ├── /etc ├── /lib/modules ├── /usr └── ...
uEnv.txt: переменные U-Boot на FAT
В uboot.fragment мы уже задали CONFIG_BOOTCOMMAND, который пытается загрузить uEnv.txt с FAT-раздела. Это позволяет менять bootargs и адреса загрузки без пересборки U-Boot.
Создадим файл:
cat > "$BR_EXT/board/zynq_mini_revb/uEnv.txt" << 'EOF' bootargs=console=ttyPS0,115200 earlycon fbcon=font:MINI4x6 root=/dev/mmcblk0p2 rootwait rw ethaddr=00:0a:35:01:02:03 load_dtb_addr=0x2A00000 load_kernel_addr=0x3000000 uenvcmd=mmc rescan; fatload mmc 0 ${load_kernel_addr} uImage; fatload mmc 0 ${load_dtb_addr} zynq-mini-revb.dtb; bootm ${load_kernel_addr} - ${load_dtb_addr} EOF
Разберем строки.
bootargs=console=ttyPS0,115200 earlycon fbcon=font:MINI4x6 root=/dev/mmcblk0p2 rootwait rw
Это командная строка ядра Linux. Параметры:
Параметр |
Смысл |
|
Основная консоль ядра через UART |
|
Ранний вывод до полной инициализации UART |
|
Малый шрифт framebuffer-консоли |
|
rootfs на втором разделе SD |
|
Ждать появления SD-устройства |
|
Монтировать rootfs read-write |
bootargs в uEnv.txt должны быть согласованы с chosen/bootargs в DTS. Если U-Boot задает bootargs, именно они обычно и становятся фактической командной строкой ядра. Поэтому при расхождении между DTS и uEnv.txt нужно смотреть, что реально пришло в Linux:
cat /proc/cmdline
Строка:ethaddr=00:0a:35:01:02:03задает MAC для U-Boot. Его нужно согласовать с /etc/eth0-mac и с Device Tree, если там задан local-mac-address.
Адреса:
load_dtb_addr=0x2A00000 load_kernel_addr=0x3000000
задают области DDR, куда U-Boot загрузит DTB и ядро. Они должны не пересекаться между собой и не конфликтовать с областью U-Boot.
Команда:
uenvcmd=mmc rescan; fatload mmc 0 ${load_kernel_addr} uImage; fatload mmc 0 ${load_dtb_addr} zynq-mini-revb.dtb; bootm ${load_kernel_addr} - ${load_dtb_addr}
делает полный запуск Linux:
Пересканировать SD.
Загрузить uImage с FAT в DDR.
Загрузить zynq-mini-revb.dtb с FAT в DDR.
Запустить bootm с ядром и DTB.
Средний аргумент - в bootm означает, что initrd не используется:
bootm <kernel> - <dtb>
Важно не путать два разных DTB:
DTB для U-Boot встроен в U-Boot через CONFIG_OF_EMBED;
DTB для Linux лежит на FAT как zynq-mini-revb.dtb и передается в bootm.
post-image.sh: сборка sdcard.img
post-image.sh вызывается после того, как Buildroot собрал основные бинарные файлы в BINARIES_DIR, обычно это:$BR_OUT/images
Скрипт должен:
Скопировать uEnv.txt в BINARIES_DIR.
Проверить наличие BOOT.BIN.
Если BOOT.BIN отсутствует, временно создать placeholder.
Запустить genimage.
Получить sdcard.img.
Создадим файл:
cat > "$BR_EXT/board/zynq_mini_revb/post-image.sh" << 'EOF' #!/usr/bin/env bash set -euo pipefail BOARD_DIR="$(dirname "$0")" GENIMAGE_CFG="${BOARD_DIR}/genimage.cfg" GENIMAGE_TMP="${BUILD_DIR}/genimage.tmp" echo "post-image: board_dir=${BOARD_DIR}" echo "post-image: binaries_dir=${BINARIES_DIR}" echo "post-image: target_dir=${TARGET_DIR}" cp -v "${BOARD_DIR}/uEnv.txt" "${BINARIES_DIR}/uEnv.txt" if [[ ! -f "${BINARIES_DIR}/BOOT.BIN" ]]; then echo "NOTE: BOOT.BIN missing." echo "NOTE: Buildroot does not generate real Zynq BOOT.BIN." echo "NOTE: Replace ${BINARIES_DIR}/BOOT.BIN with bootgen output before booting the board." echo "PLACEHOLDER - replace with real bootgen output" > "${BINARIES_DIR}/BOOT.BIN" fi rm -rf "${GENIMAGE_TMP}" mkdir -p "${GENIMAGE_TMP}" genimage \ --rootpath "${TARGET_DIR}" \ --tmppath "${GENIMAGE_TMP}" \ --inputpath "${BINARIES_DIR}" \ --outputpath "${BINARIES_DIR}" \ --config "${GENIMAGE_CFG}" echo "post-image: SD image created: ${BINARIES_DIR}/sdcard.img" EOF chmod +x "$BR_EXT/board/zynq_mini_revb/post-image.sh"
Разбор post-image.sh
Переменные окружения Buildroot
Buildroot вызывает post-image.sh уже с подготовленным окружением. Важные переменные:
Переменная |
Смысл |
|
Каталог с готовыми бинарными артефактами, обычно |
|
Каталог target rootfs, обычно |
|
Каталог временных сборок, обычно |
Скрипт вычисляет каталог платы:
BOARD_DIR="$(dirname "$0")"
и путь к конфигу genimage:
GENIMAGE_CFG="${BOARD_DIR}/genimage.cfg"
Копирование uEnv.txt
Строка:
cp -v "${BOARD_DIR}/uEnv.txt" "${BINARIES_DIR}/uEnv.txt"
переносит uEnv.txt в $BR_OUT/images. Именно оттуда genimage возьмет его и положит в корень FAT-раздела. Если забыть этот шаг, U-Boot не найдет uEnv.txt и будет использовать fallback-команду из CONFIG_BOOTCOMMAND.
BOOT.BIN и placeholder
Для Zynq первый файл, который ищет BootROM на SD, это:BOOT.BIN Он должен быть собран отдельно через bootgen из: FSBL.elf bitstream.bit u-boot.elf
Buildroot собирает u-boot.elf, но не генерирует FSBL и bitstream. Поэтому полноценный BOOT.BIN находится за пределами чистой Buildroot-сборки. В учебном сценарии удобно временно создать placeholder, чтобы genimage не падал из-за отсутствующего файла:
echo "PLACEHOLDER - replace with real bootgen output" > "${BINARIES_DIR}/BOOT.BIN"
Это позволяет собрать sdcard.img и проверить весь Linux/rootfs pipeline. Но важно: SD-карта с таким BOOT.BIN не загрузится на плате. Перед реальным запуском нужно заменить файл:
$BR_OUT/images/BOOT.BIN
на настоящий результат bootgen, затем заново вызвать post-image.sh или пересобрать образ.
Минимальная проверка:
head -c 64 "$BR_OUT/images/BOOT.BIN" | strings
Если видно слово PLACEHOLDER, это не загрузочный файл.
Запуск genimage
Команда:
genimage \ --rootpath "${TARGET_DIR}" \ --tmppath "${GENIMAGE_TMP}" \ --inputpath "${BINARIES_DIR}" \ --outputpath "${BINARIES_DIR}" \ --config "${GENIMAGE_CFG}"
использует четыре пути.
--rootpath - откуда брать содержимое rootfs-раздела. Это TARGET_DIR, уже измененный post-build.sh.
--inputpath - откуда брать файлы для FAT-раздела: BOOT.BIN, uImage, zynq-mini-revb.dtb, uEnv.txt.
--outputpath - куда положить итоговые файлы, включая sdcard.img.
--tmppath - временный каталог genimage. Перед запуском он очищается:
rm -rf "${GENIMAGE_TMP}" mkdir -p "${GENIMAGE_TMP}"
Это снижает риск, что старые временные файлы повлияют на новую сборку.
Связь post-build, post-image и defconfig
Чтобы эти скрипты реально выполнялись, в zynq_mini_revb_defconfig должны быть строки:
BR2_ROOTFS_POST_BUILD_SCRIPT="$(BR2_EXTERNAL_ZYNQ_MINI_I2C_PATH)/board/zynq_mini_revb/post-build.sh" BR2_ROOTFS_POST_IMAGE_SCRIPT="$(BR2_EXTERNAL_ZYNQ_MINI_I2C_PATH)/board/zynq_mini_revb/post-image.sh"
Если post-build.sh не подключен, то:
i2c-master-axi.ko может попасть в rootfs;
но не будет /etc/modules-load.d/i2c-master-axi.conf;
не будет S03modules;
модуль не загрузится автоматически;
probe i2c-master-axi не вызовется при boot.
Если post-image.sh не подключен, то:
Buildroot соберет uImage, DTB, U-Boot и rootfs;
но не будет итогового sdcard.img;
uEnv.txt не попадет на FAT автоматически;
genimage не будет вызван.
Если genimage.cfg отсутствует или содержит ошибку, сборка упадет на этапе post-image.
Проверка после сборки
После полной сборки:
cd "$BR_SRC" make BR2_EXTERNAL="$BR_EXT" O="$BR_OUT" -j"$(nproc)"
нужно проверить несколько уровней.
Проверка rootfs в target
grep -r i2c-master "$BR_OUT/target/etc/modules-load.d" || true test -x "$BR_OUT/target/etc/init.d/S03modules" && echo "S03modules OK" test -x "$BR_OUT/target/etc/init.d/S39set-eth0-mac" && echo "S39set-eth0-mac OK" test -x "$BR_OUT/target/etc/init.d/S45oled-console" && echo "S45oled-console OK" test -x "$BR_OUT/target/usr/local/bin/oled-console" && echo "oled-console OK" cat "$BR_OUT/target/etc/eth0-mac"
Ожидаемо:
/etc/modules-load.d/i2c-master-axi.conf
содержит i2c-master-axi;
S03modules исполняемый;
S39set-eth0-mac исполняемый;
S45oled-console исполняемый;
oled-console исполняемый;
eth0-mac содержит выбранный MAC.
Проверка модуля
find "$BR_OUT/target" -name 'i2c-master-axi.ko'
Если модуль не найден, проблема не в post-build.sh. Нужно возвращаться к пакету i2c-master-axi и BR2_PACKAGE_I2C_MASTER_AXI=y.
Проверка images
ls -lh "$BR_OUT/images/uImage" ls -lh "$BR_OUT/images/zynq-mini-revb.dtb" ls -lh "$BR_OUT/images/uEnv.txt" ls -lh "$BR_OUT/images/BOOT.BIN" ls -lh "$BR_OUT/images/sdcard.img"
Проверка placeholder BOOT.BIN
head -c 128 "$BR_OUT/images/BOOT.BIN" | strings
Если вывод содержит:PLACEHOLDERзначит образ SD собран только для проверки структуры. Для реального запуска нужно заменить BOOT.BIN.
Проверка структуры SD-образа
Можно посмотреть разметку:
fdisk -l "$BR_OUT/images/sdcard.img"
Ожидаемо два раздела:
FAT32, тип 0x0C, bootable
Linux, тип 0x83
Если нужно посмотреть содержимое FAT без записи на SD, можно использовать loop-mount:
mkdir -p /tmp/zynq-mini-boot sudo losetup -Pf --show "$BR_OUT/images/sdcard.img"
Команда losetup выведет имя loop-устройства, например: /dev/loop0
Тогда:
sudo mount /dev/loop0p1 /tmp/zynq-mini-boot ls -la /tmp/zynq-mini-boot sudo umount /tmp/zynq-mini-boot sudo losetup -d /dev/loop0
На FAT должны быть:
BOOT.BIN
uImage
zynq-mini-revb.dtb
uEnv.txt
Запись SD-карты
После замены BOOT.BIN на настоящий файл можно записывать SD-карту.
Сначала определить устройство:
lsblk
Затем записать образ:
sudo dd if="$BR_OUT/images/sdcard.img" of=/dev/sdX bs=4M conv=fsync status=progress sync
Где /dev/sdX - устройство SD-карты, не раздел. Например:
/dev/sdb
а не:
/dev/sdb1
Ошибка выбора устройства уничтожит данные на выбранном диске. Перед dd нужно проверять lsblk. Если после сборки был заменен только BOOT.BIN, можно не пересобирать весь Buildroot. Достаточно:
Смонтировать первый FAT-раздел SD-карты.
Скопировать туда новый BOOT.BIN.
Выполнить sync.
Но для воспроизводимости лучше регенерировать sdcard.img через post-image.sh.
Проверка на плате после boot
После загрузки платы через UART проверяем rootfs-часть.
Проверить командную строку ядра
cat /proc/cmdline
Ожидаемые фрагменты:
console=ttyPS0,115200 fbcon=font:MINI4x6 root=/dev/mmcblk0p2 rootwait rw
Если fbcon=font:MINI4x6 отсутствует, значит фактические bootargs отличаются от ожидаемых. Нужно проверить uEnv.txt на FAT и команду U-Boot.
Проверить автозагрузку модуля
lsmod | grep i2c dmesg | grep -i i2c-master
Ожидаем строку вида:
i2c-master-axi 43c00000.i2c: input=50000000 Hz, bus=100000 Hz, prescale=124, irq=polled
или:
i2c-master-axi 43c00000.i2c: input=50000000 Hz, bus=100000 Hz, prescale=124, irq=yes
Если строки нет, проверить вручную:
modprobe i2c-master-axi dmesg | tail -50
Если вручную работает, но при boot не работает, проблема в S03modules или /etc/modules-load.d.
Проверить I2C
ls /dev/i2c-* 2>/dev/null ls /sys/bus/i2c/devices/
Затем:
i2cdetect -y 1
Номер шины нужно подставить фактический.
На адресе 0x3c ожидается3cилиUU. UU означает, что адрес уже занят kernel driver, например ssd1307fb.
Проверить framebuffer
ls -l /dev/fb* dmesg | grep -iE "ssd|fb|framebuffer" fbset -fb /dev/fb0
Если /dev/fb0 нет, но I2C-адрес 0x3c виден, нужно проверять CONFIG_FB_SSD1307, compatible OLED в DTS и логи ssd1307fb.
Проверить Ethernet MAC
cat /sys/class/net/eth0/address cat /etc/eth0-mac
Значения должны совпадать.
Проверка после reboot:
reboot cat /sys/class/net/eth0/address
MAC должен остаться тем же.
Типичные ошибки
Симптом |
Вероятная причина |
Где проверять |
|
Не подключен |
|
|
|
|
|
Нет placeholder и нет настоящего BOOT.BIN |
|
Плата молчит до UART |
На FAT placeholder BOOT.BIN или неверный FAT |
настоящий |
Модуль есть, но не загружается автоматически |
BusyBox init не читает modules-load.d без скрипта |
|
|
Модуль не попал в rootfs |
|
|
Не совпал compatible или не тот DTB |
DTS, DTB на FAT, |
|
OLED driver не привязался |
|
|
Нет строки inittab или fbcon не активен |
|
MAC меняется после reboot |
Не сработал |
порядок init, |
|
Неверная разметка SD |
|
U-Boot игнорирует |
Файл не на FAT или bootcmd не импортирует env |
FAT contents, |
Что получилось на этом шаге
После этой главы у нас появляется слой автоматизации поверх обычной сборки Buildroot.
post-build.sh отвечает за содержимое rootfs:
/etc/modules-load.d/i2c-master-axi.conf /etc/init.d/S03modules /etc/init.d/S39set-eth0-mac /etc/init.d/S45oled-console /usr/local/bin/oled-console /etc/eth0-mac /etc/issue /etc/profile.d/zynq-mini.sh
genimage.cfg описывает структуру SD-карты:
partition 1: FAT32 BOOT.BIN uImage zynq-mini-revb.dtb uEnv.txt partition 2: ext4 rootfs
uEnv.txt задает переменные U-Boot:
bootargs ethaddr load_dtb_addr load_kernel_addr uenvcmd
post-image.sh собирает итоговый образ:
$BR_OUT/images/sdcard.img
Теперь Buildroot-сборка становится завершенной с точки зрения Linux-части проекта:
make BR2_EXTERNAL="$BR_EXT" O="$BR_OUT" -j"$(nproc)" -> rootfs с автозагрузкой i2c-master-axi -> uImage -> zynq-mini-revb.dtb -> u-boot.elf -> sdcard.img
Остается внешний к Buildroot шаг: собрать настоящий BOOT.BIN через bootgen из FSBL, bitstream и u-boot.elf, заменить placeholder и записать SD-карту.
Применим defconfig
Сборку нужно запускать из исходников upstream Buildroot:
cd "$BR_SRC"
При этом важно всегда указывать два параметра:
BR2_EXTERNAL="$BR_EXT" O="$BR_OUT"
BR2_EXTERNAL говорит Buildroot, где лежит наше внешнее дерево.
O задает отдельный каталог вывода. Это не позволяет засорять исходное дерево Buildroot результатами сборки и позволяет держать несколько независимых сборок.
Применяем defconfig:
make BR2_EXTERNAL="$BR_EXT" O="$BR_OUT" zynq_mini_revb_defconfig
После этой команды должен появиться рабочий конфиг:
$BR_OUT/.config
Проверим ключевые строки:
grep -E 'I2C_MASTER_AXI|LINUX_KERNEL_CUSTOM_DTS|POST_BUILD_SCRIPT|POST_IMAGE' \ "$BR_OUT/.config"
Ожидаемые признаки:
BR2_PACKAGE_I2C_MASTER_AXI=y BR2_LINUX_KERNEL_CUSTOM_DTS_PATH=... BR2_ROOTFS_POST_BUILD_SCRIPT=... BR2_ROOTFS_POST_IMAGE_SCRIPT=...
Если BR2_PACKAGE_I2C_MASTER_AXI отсутствует, пакет не будет собран.
Если путь к DTS пустой или начинается с /dts/..., значит при создании defconfig shell раньше времени раскрыл $(BR2_EXTERNAL_ZYNQ_MINI_I2C_PATH).
Если post-build.sh или post-image.sh не прописались, rootfs и SD-образ будут неполными.
Проверяем через menuconfig
После применения defconfig можно открыть меню Buildroot:
make BR2_EXTERNAL="$BR_EXT" O="$BR_OUT" menuconfig
Это не обязательный шаг, но он полезен для контроля. Нужно проверить, что пакет i2c-master-axi виден в меню внешнего дерева и включен. Если верхнеуровневый Config.in
внешнего дерева подключен правильно, в menuconfig должен появиться пункт:
i2c-master-axi (custom AXI I2C)
Если пункта нет, проблема обычно в одном из мест:
не подключен package/i2c-master-axi/Config.in;
ошибка в верхнеуровневом Config.in;
не передан BR2_EXTERNAL;
имя переменной BR2_EXTERNAL_* не совпадает с external.desc.
Если опция есть, но пакет не собирается, нужно проверять external.mk и наличие i2c-master-axi.mk.
После ручных изменений в menuconfig можно сохранить рабочую конфигурацию. Но для статьи и воспроизводимости лучше не полагаться на ручные клики: все нужные строки должны быть явно внесены в zynq_mini_revb_defconfig.
Запуск полной сборки
Общая структура взаимосвязей будет выглядеть следующим образом:

И теперь после применения defconfig запускаем сборку:
make BR2_EXTERNAL="$BR_EXT" O="$BR_OUT" -j"$(nproc)"
Первый запуск может занять продолжительное время, потому что Buildroot будет:
скачивать исходники;
собирать toolchain;
собирать Linux kernel;
собирать U-Boot;
собирать host-утилиты;
собирать target-пакеты;
собирать out-of-tree модуль;
формировать rootfs;
выполнять post-build;
выполнять post-image;
собирать sdcard.img.
После успешной сборки основные артефакты будут находиться в:
# ll $BR_OUT/images/ total 187M drwxr-xr-x 2 megalloid megalloid 4.0K Jun 4 12:07 boot -rw-r--r-- 1 megalloid megalloid 215 Jun 4 12:07 BOOT.BIN -rw-r--r-- 1 megalloid megalloid 0 Jun 4 12:07 BOOT.BIN.MISSING -rw-r--r-- 1 megalloid megalloid 64M Jun 4 12:07 boot.vfat -rw-r--r-- 1 megalloid megalloid 256M Jun 4 12:07 rootfs.ext2 lrwxrwxrwx 1 megalloid megalloid 11 Jun 4 12:07 rootfs.ext4 -> rootfs.ext2 -rw-r--r-- 1 megalloid megalloid 44M Jun 4 12:07 rootfs.tar -rw-r--r-- 1 megalloid megalloid 321M Jun 4 12:07 sdcard.img -rwxr-xr-x 1 megalloid megalloid 6.9M Jun 4 04:40 u-boot -rwxr-xr-x 1 megalloid megalloid 904K Jun 4 04:40 u-boot.bin -rw-r--r-- 1 megalloid megalloid 1.2M Jun 4 04:40 u-boot.img -rw-r--r-- 1 megalloid megalloid 957 Jun 4 12:07 uEnv.txt -rw-r--r-- 1 megalloid megalloid 11M Jun 4 12:07 uImage -rwxr-xr-x 1 megalloid megalloid 12K Jun 4 12:07 zynq-mini-revb.dtb
Проверим:
ls -l "$BR_OUT/images/uImage" ls -l "$BR_OUT/images/zynq-mini-revb.dtb" ls -l "$BR_OUT/images/u-boot"* ls -l "$BR_OUT/images/sdcard.img" find "$BR_OUT/target" -name 'i2c-master-axi.ko'
Если sdcard.img появился, это означает, что Buildroot дошел до post-image.sh. Но это еще не гарантирует, что карта загрузится на плате.
Для реального boot нужен настоящий BOOT.BIN.
Проверить, что BOOT.BIN не является заглушкой:
file "$BR_OUT/images/BOOT.BIN" head -c 64 "$BR_OUT/images/BOOT.BIN" | strings
Если в BOOT.BIN виден текст вроде PLACEHOLDER, это не загрузочный файл. Его нужно заменить результатом bootgen. Это мы и сделаем в следующей главе.
Сборка BOOT.BIN через bootgen
После предыдущей главы у нас уже есть Linux-часть проекта:
$BR_OUT/images/uImage $BR_OUT/images/zynq-mini-revb.dtb $BR_OUT/images/u-boot.elf $BR_OUT/images/rootfs.ext4 $BR_OUT/images/sdcard.img
Но для Zynq-7000 этого еще недостаточно. При включении питания BootROM не ищет uImage, не читает uEnv.txt и не запускает Linux напрямую. Первым файлом, который BootROM ищет на загрузочном FAT-разделе SD-карты, являетсяBOOT.BIN
Именно BOOT.BIN запускает раннюю цепочку загрузки Zynq. Внутри BOOT.BIN обычно находятся три компонента:
FSBL
bitstream PL
U-Boot
Их объединяет утилита bootgen из состава Xilinx Vitis/Vivado. Важно разделить содержимое BOOT.BIN и файлы, которые лежат рядом на FAT-разделе.
В нашем сценарии в BOOT.BIN входят:fsbl.elf zynq_mini_oled_top.bit u-boot.elfА рядом на FAT лежат: uImage zynq-mini-revb.dtb uEnv.txt
То есть uImage и DTB в BOOT.BIN не упаковываются. Это сделано намеренно: так проще итеративно менять Linux, DTS, rootfs или kernel module без пересборки раннего загрузочного образа. Если меняется только uImage, DTB, .ko или rootfs, новый BOOT.BIN не нужен. Если меняется FSBL, bitstream или U-Boot, BOOT.BIN нужно собрать заново.
Что должно быть готово до сборки BOOT.BIN
Перед запуском bootgen должны существовать три входных файла.
Компонент |
Пример файла |
Откуда берется |
Зачем нужен |
|---|---|---|---|
FSBL |
|
Vitis, проект FSBL из XSA |
Первый загрузчик, который запускает BootROM |
Bitstream |
|
Vivado, результат Generate Bitstream |
Конфигурация PL, включая AXI I2C IP |
U-Boot ELF |
|
Buildroot |
Второй загрузчик, который стартует Linux |
Все три файла должны соответствовать одной и той же аппаратной конфигурации.
Это критично. Возможная ошибка - собрать FSBL из старого XSA, взять bitstream из другого проекта, а U-Boot из текущей Buildroot-сборки. В таком случае загрузка может остановиться еще до Linux, и отладка будет выглядеть неочевидно.
Для fsbl.elf и u-boot.elf ожидается ELF-файл. Для .bit - бинарный bitstream Vivado.
Если в $BR_OUT/images/BOOT.BIN сейчас лежит файл-заглушка из post-image.sh, он не является загрузочным. Он был нужен только для того, чтобы genimage смог собрать структуру SD-образа. Плата с таким BOOT.BIN не загрузится.
Подготовка окружения Vitis
bootgen входит в состав Xilinx Vitis или Vivado. Перед использованием нужно подключить окружение Xilinx.
Пример:
source /opt/xilinx/2025.2/Vitis/settings64.sh
Путь зависит от установленной версии.
Проверим, что bootgen доступен:
which bootgen bootgen -version
Если команда не найдена:
bootgen: command not found
значит не выполнен settings64.sh или установлен неполный набор Xilinx tools.
Зададим рабочие переменные. Пути ниже нужно адаптировать под свой проект:
export WORK=~/devel/xilinx/projects/zynq_mini_oled export BR_OUT="$WORK/linux/br-output" export BR_EXT="$WORK/linux/board-support" export VIVADO_PROJ=/home/megalloid/devel/xilinx/projects/zynq_mini_oled/ export VITIS_WS=/home/megalloid/devel/xilinx/projects/zynq_mini_oled/vitis/
Теперь зададим пути к трем входным артефактам:
export FSBL_ELF="$VITIS_WS/zynq_mini_oled_platform/zynq_fsbl/build/fsbl.elf" export BIT="$VIVADO_PROJ/zynq_mini_oled.runs/impl_1/zynq_mini_oled_top.bit" export UBOOT="$BR_OUT/images/u-boot"
В некоторых сборках Buildroot ELF-файл U-Boot может называться u-boot без расширения .elf. Это нормально, если команда file показывает ELF:
file "$UBOOT"
Ожидаемо что-то вида:
u-boot: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, with debug_info, not stripped
Если $BR_OUT/images/u-boot отсутствует, проверьте, что в defconfig включено:
BR2_TARGET_UBOOT_FORMAT_ELF=y
и пересоберите U-Boot.
Файл boot.bif
bootgen не принимает просто список файлов в командной строке. Ему нужен манифест в формате BIF. BIF, Boot Image Format, описывает, какие partition нужно положить в BOOT.BIN и в каком порядке.
Для Zynq-7000 с SD-загрузкой типичный BIF выглядит так:
the_ROM_image: { [bootloader] fsbl.elf design.bit u-boot.elf }
Смысл строк:
[bootloader] fsbl.elf - первая partition, которую BootROM считает загрузчиком и копирует в OCM;
design.bit - bitstream, который FSBL загрузит в PL через PCAP;
u-boot.elf - приложение, которому FSBL передаст управление после инициализации.
Атрибут [bootloader] должен быть указан ровно для FSBL. Это специальная partition, с которой начинается выполнение после BootROM.
Создадим каталог для загрузочных файлов:
mkdir -p "$WORK/boot"
Проверим, что все входные файлы существуют:
ls -lh "$FSBL_ELF" "$BIT" "$UBOOT"
Теперь создадим BIF:
cat > "$WORK/boot/boot_linux.bif" << EOF the_ROM_image: { [bootloader] ${FSBL_ELF} ${BIT} ${UBOOT} } EOF
Посмотрим результат:
cat "$WORK/boot/boot_linux.bif"
Ожидаемо:
the_ROM_image: { [bootloader] /path/to/fsbl.elf /path/to/zynq_mini_oled_top.bit /path/to/u-boot }
Если в путях есть пробелы, лучше избегать таких каталогов для проекта. Если это невозможно, пути в BIF нужно оформлять аккуратно с учетом синтаксиса bootgen.
Почему порядок в BIF важен
Порядок partition в boot.bif не является косметикой.
Для Zynq цепочка выглядит так: BootROM -> читает заголовки BOOT.BIN -> находит partition с [bootloader] -> копирует FSBL в OCM -> передает управление FSBL
Дальше уже FSBL обрабатывает следующие partition. Сначала FSBL выполняет инициализацию PS а Затем FSBL загружает bitstream в PL. Только после загрузки bitstream в PL физически появляется наш AXI IP-блок. До этого запись Linux-драйвера по адресу 0x43c00000 не имеет смысла, потому что соответствующей логики в PL еще нет.
После этого FSBL загружает U-Boot в DDR и передает ему управление.
Если bitstream не включить в BOOT.BIN, Linux может загрузиться, но драйвер i2c-master-axi не сможет работать с PL IP. Типичный симптом - probe есть, но операции по MMIO зависают или TIP не сбрасывается.
Если взять bitstream от старого Vivado design, где IP находится по другому адресу, будет расхождение между:Vivado Address Editor DTS reg = <0x43c00000 0x1000> реальной PL-конфигурацией В результате драйвер будет обращаться не туда.
C.5. Запуск bootgen
Команда сборки:
bootgen -arch zynq \ -image "$WORK/boot/boot_linux.bif" \ -o "$WORK/boot/BOOT.BIN" \ -w on
Разбор параметров:
Параметр |
Смысл |
|
Целевая архитектура Zynq-7000 |
|
Входной BIF-манифест |
|
Выходной загрузочный образ |
|
Перезаписать выходной файл без запроса |
Проверим результат:
ls -lh "$WORK/boot/BOOT.BIN" file "$WORK/boot/BOOT.BIN"
Ожидаемый результат:BOOT.BIN: data
Команда file чаще всего покажет просто:
data
Это нормально.
Если file показывает: ASCII textили внутри виден текстPLACEHOLDER значит вы смотрите не на результат bootgen, а на заглушку из post-image.sh.
Быстрая проверка:
head -c 128 "$WORK/boot/BOOT.BIN" | strings
У настоящего BOOT.BIN не должно быть строки PLACEHOLDER.
Когда нужно пересобирать BOOT.BIN
BOOT.BIN нужно пересобирать не при каждом изменении проекта. Нужно понимать границу ответственности.
Что изменилось |
Нужно пересобирать |
Почему |
FSBL |
Да |
FSBL входит в |
XSA / PS7 init / DDR settings |
Да |
FSBL должен соответствовать новой аппаратной платформе |
Bitstream |
Да |
Bitstream входит в |
Адрес AXI IP в Vivado |
Да |
Меняется bitstream и, возможно, DTS |
Пины OLED в XDC |
Да |
Меняется bitstream |
U-Boot |
Да |
U-Boot входит в |
|
Да |
После пересборки U-Boot нужен новый |
|
Нет |
U-Boot загружает его отдельно с FAT |
|
Нет |
U-Boot загружает DTB отдельно с FAT |
|
Нет, если не менялся U-Boot |
Влияет на ядро, не на |
|
Нет |
Модуль лежит в rootfs |
rootfs |
Нет |
rootfs лежит на ext4-разделе |
|
Нет |
Лежит отдельно на FAT |
Копирование BOOT.BIN в Buildroot images
post-image.sh ожидает, что файл BOOT.BIN лежит в:
$BR_OUT/images/BOOT.BIN
Скопируем туда результат bootgen:
cp -v "$WORK/boot/BOOT.BIN" "$BR_OUT/images/BOOT.BIN"
Проверим, что рядом есть остальные файлы FAT-раздела:
ls -lh \ "$BR_OUT/images/BOOT.BIN" \ "$BR_OUT/images/uImage" \ "$BR_OUT/images/zynq-mini-revb.dtb" \ "$BR_OUT/images/uEnv.txt"
Теперь нужно пересобрать sdcard.img, чтобы новый BOOT.BIN попал в FAT-раздел образа.
Пересборка sdcard.img после замены BOOT.BIN
Полную сборку Buildroot запускать не обязательно. Если менялся только BOOT.BIN, достаточно снова вызвать genimage. Подготовим переменные:
export BOARD="$BR_EXT/board/zynq_mini_revb" export PATH="$BR_OUT/host/bin:$BR_OUT/host/sbin:$PATH" export GENIMAGE_TMP="$BR_OUT/build/genimage.tmp"
Скопируем актуальный uEnv.txt:
cp -v "$BOARD/uEnv.txt" "$BR_OUT/images/uEnv.txt"
Очистим временный каталог genimage:
rm -rf "$GENIMAGE_TMP" mkdir -p "$GENIMAGE_TMP"
Запустим genimage:
genimage \ --rootpath "$BR_OUT/target" \ --tmppath "$GENIMAGE_TMP" \ --inputpath "$BR_OUT/images" \ --outputpath "$BR_OUT/images" \ --config "$BOARD/genimage.cfg"
Проверим результат:
ls -lh "$BR_OUT/images/sdcard.img"
Теперь sdcard.img содержит настоящий BOOT.BIN. Альтернатива для быстрой отладки: не пересобирать весь sdcard.img, а смонтировать первый FAT-раздел уже записанной SD-карты и заменить только BOOT.BIN.
Например:
sudo mount /dev/sdX1 /mnt sudo cp -v "$WORK/boot/BOOT.BIN" /mnt/BOOT.BIN sync sudo umount /mnt
Здесь /dev/sdX1 - первый раздел SD-карты. Его нужно определить через lsblk.
Проверка SD-образа перед записью
Проверим, что образ существует:
ls -lh "$BR_OUT/images/sdcard.img"
Проверим разметку:
fdisk -l "$BR_OUT/images/sdcard.img"
Ожидаем два раздела:
Device Boot Start End Sectors Size Id Type sdcard.img1 * ... ... ... 64M c W95 FAT32 (LBA) sdcard.img2 ... ... ... ... 83 Linux
Проверим содержимое FAT-раздела через loop mount. Создадим точку монтирования:
mkdir -p /tmp/zynq-mini-boot
Подключим образ:
LOOP="$(sudo losetup -Pf --show "$BR_OUT/images/sdcard.img")" echo "$LOOP"
Смонтируем первый раздел:
sudo mount "${LOOP}p1" /tmp/zynq-mini-boot ls -lh /tmp/zynq-mini-boot
Ожидаемые файлы:
BOOT.BIN uImage zynq-mini-revb.dtb uEnv.txt
Размонтируем:
sudo umount /tmp/zynq-mini-boot sudo losetup -d "$LOOP"
Запись SD-карты
Определяем устройство SD-карты:
lsblk
Нужно выбрать устройство целиком, например:
/dev/sdb
а не раздел:
/dev/sdb1
Запись:
sudo dd if="$BR_OUT/images/sdcard.img" of=/dev/sdX bs=4M conv=fsync status=progress
Где /dev/sdX нужно заменить на реальное устройство SD-карты.
Внимание: ошибка в выборе /dev/sdX приведет к перезаписи другого диска. Перед dd нужно еще раз сверить lsblk.
После записи можно сделать команду синхронизации и извлечь карту:
sync
и вставить ее в плату.
Проверка перед включением платы
Перед первым запуском стоит пройти короткий чек-лист.
Проверка |
Что должно быть |
Boot mode |
Плата выставлена в режим загрузки с SD |
FAT-раздел |
Первый раздел FAT32, содержит |
|
Настоящий bootgen output, не placeholder |
|
Лежит на FAT |
|
Лежит на FAT |
|
Лежит на FAT |
UART |
Подключен, 115200 8N1 |
Питание |
Плата получает стабильное питание |
Bitstream |
Соответствует текущему Vivado design |
На UART ожидаем последовательность:
FSBL messages U-Boot banner U-Boot countdown fatload uImage fatload zynq-mini-revb.dtb Starting kernel ... Linux boot log login prompt
Если на UART полная тишина, это почти всегда проблема до Linux:
неверный BOOT.BIN; placeholder BOOT.BIN; не тот boot mode; FAT не FAT32; карта не читается BootROM; FSBL не стартует.
В такой ситуации не нужно начинать отладку с i2c-master-axi, DTS или rootfs. До них выполнение еще не дошло.
Типичные ошибки bootgen и BOOT.BIN
Симптом |
Вероятная причина |
Что делать |
|
Не подключено окружение Vitis |
Выполнить |
|
Неверный путь в |
Проверить |
|
Используется placeholder |
Пересобрать через |
Плата молчит на UART |
BootROM не нашел или не запустил |
Проверить FAT32, boot mode, настоящий |
FSBL стартует, но дальше зависание |
Неверный bitstream или FSBL не от этого XSA |
Пересобрать FSBL и bitstream из одной аппаратной платформы |
U-Boot стартует, но Linux нет |
Нет |
Проверить FAT-раздел и U-Boot env |
Linux стартует, но I2C IP не работает |
Bitstream не содержит нужный IP или адрес не совпадает |
Проверить Vivado Address Editor, DTS |
После изменения DTS ничего не поменялось |
На FAT старый DTB |
Пересобрать Linux/DTB и заменить |
После изменения U-Boot ничего не поменялось |
В |
Пересобрать U-Boot и заново выполнить |
После изменения bitstream PL поведение старое |
В |
Проверить BIF и заново выполнить |
Итоговая цепочка загрузки
После успешной сборки BOOT.BIN и записи SD-карты цепочка проекта становится полной.
При включении платы:

На этом этапе Buildroot-часть, bootgen-часть и SD-образ соединены в единую загрузочную систему. Дальше отладку можно вести уже по слоям: сначала FSBL/U-Boot, затем загрузка Linux, затем rootfs, затем модуль i2c-master-axi, затем I2C-шина и OLED.
Подготовка платы к первому запуску и проверка
Перед включением платы проверьте:
Проверка |
Что должно быть |
|---|---|
microSD |
Вставлена в слот платы |
Boot mode |
Перемычки или DIP-переключатели выставлены в режим SD boot |
Питание |
Плата получает стабильное питание |
UART |
Подключен к host-машине |
UART параметры |
115200 8N1, без flow control |
OLED |
Подключен к правильному разъему или CAM1, если используется внешний модуль |
Bitstream |
В |
Для UART можно использовать picocom, minicom или screen. Пример через picocom:
sudo picocom -b 115200 /dev/ttyUSB0
Пример через screen:
sudo screen /dev/ttyUSB0 115200
Если неизвестно, какой порт появился после подключения платы:
dmesg | tail -50 ls /dev/ttyUSB* /dev/ttyACM* 2>/dev/null
Для первого запуска UART обязателен. Без UART вы не увидите, на каком этапе остановилась загрузка.
Ожидаемая картина в UART
После подачи питания на плату в UART должны появиться сообщения ранней загрузки и загрузка должна пройти полностью до появления приглашения на ввод на дисплее.
Сообщения от FSBL:
Xilinx First Stage Boot Loader Release 2025.2 Jun 3 2026-00:10:01 Devcfg driver initialized Silicon Version 3.1 Boot mode is SD SD: rc= 0 SD Init Done Flash Base Address: 0xE0100000 Reboot status register: 0x60400000 Multiboot Register: 0x0000C000 Image Start Address: 0x00000000 Partition Header Offset:0x00000C80 Partition Count: 12884901891 Partition Number: 12884901889 Header Dump Image Word Len: 0x000F6EC0 Data Word Len: 0x000F6EC0 Partition Word Len:0x000F6EC0 Load Addr: 0x00000000 Exec Addr: 0x00000000 Partition Start: 0x0000B3A0 Partition Attr: 0x00000020 Partition Checksum Offset: 0x00000000 Section Count: 0x00000001 Checksum: 0xFFD0FDAE Bitstream In FsblHookBeforeBitstreamDload function PCAP:StatusReg = 0x40000A3000000000 PCAP:device ready PCAP:Clear done Level Shifter Value = 0xA0000000A Devcfg Status register = 0x40000A3000000000 PCAP:Fabric is Initialized done PCAP register dump: PCAP CTRL 0xF8007000: 0x4C00E07F PCAP LOCK 0xF8007004: 0x0000001A PCAP CONFIG 0xF8007008: 0x00000508 PCAP ISR 0xF800700C: 0x0802000B PCAP IMR 0xF8007010: 0xFFFFFFFF PCAP STATUS 0xF8007014: 0x00001A30 PCAP DMA SRC ADDR 0xF8007018: 0x00100001 PCAP DMA DEST ADDR 0xF800701C: 0xFFFFFFFF PCAP DMA SRC LEN 0xF8007020: 0x000F6EC0 PCAP DMA DEST LEN 0xF8007024: 0x000F6EC0 PCAP ROM SHADOW CTRL 0xF8007028: 0xFFFFFFFF PCAP MBOOT 0xF800702C: 0x0000C000 PCAP SW ID 0xF8007030: 0x00000000 PCAP UNLOCK 0xF8007034: 0x757BDF0D PCAP MCTRL 0xF8007080: 0x30800100 DMA Done ! FPGA Done ! In FsblHookAfterBitstreamDload function Partition Number: 12884901890 Header Dump Image Word Len: 0x00038722 Data Word Len: 0x00038722 Partition Word Len:0x00038722 Load Addr: 0x04000000 Exec Addr: 0x04000000 Partition Start: 0x00102260 Partition Attr: 0x00000010 Partition Checksum Offset: 0x00000000 Section Count: 0x00000001 Checksum: 0xF7E545C8 Application Handoff Address: 0x04000000 In FsblHookBeforeHandoff function SUCCESSFUL_HANDOFF FSBL Status = 0xA
Сообщения от U-Boot:
U-Boot 2024.01 (Jun 04 2026 - 04:40:03 +0300) CPU: Zynq 7z020 Silicon: v3.1 Model: Xilinx ZC706 board DRAM: ECC disabled 1 GiB Core: 29 devices, 19 uclasses, devicetree: embed Flash: 0 Bytes NAND: 0 MiB MMC: mmc@e0100000: 0 Loading Environment from FAT... *** Error - No Valid Environment Area found *** Warning - bad env area, using default environment In: serial@e0001000 Out: serial@e0001000 Err: serial@e0001000 Net: Could not get PHY for eth0: addr 7 No ethernet found. Hit any key to stop autoboot: 0 switch to partitions #0, OK mmc0 is current device 957 bytes read in 12 ms (77.1 KiB/s) Device: mmc@e0100000 Manufacturer ID: 27 OEM: 5048 Name: SD16G Bus Speed: 50000000 Mode: SD High Speed (50MHz) Rd Block Len: 512 SD version 3.0 High Capacity: Yes Capacity: 14.5 GiB Bus Width: 4-bit Erase Group Size: 512 Bytes 10990144 bytes read in 676 ms (15.5 MiB/s) 11534 bytes read in 14 ms (803.7 KiB/s) ## Booting kernel from Legacy Image at 03000000 ... Image Name: Linux-6.6.51 Image Type: ARM Linux Kernel Image (uncompressed) Data Size: 10990080 Bytes = 10.5 MiB Load Address: 00008000 Entry Point: 00008000 Verifying Checksum ... OK ## Flattened Device Tree blob at 02a00000 Booting using the fdt blob at 0x2a00000 Working FDT set to 2a00000 Could not get PHY for eth0: addr 7 Could not get PHY for eth0: addr 7 Loading Kernel Image Loading Device Tree to 2fffa000, end 2ffffd0d ... OK Working FDT set to 2fffa000 Starting kernel ...
Сообщения от Linux kernel и приглашение на ввод:
[ 0.000000] Booting Linux on physical CPU 0x0 [ 0.000000] Linux version 6.6.51 (megalloid@NB-10371) (arm-buildroot-linux-gnueabi-gcc.br_real (Buildroot 2024.02.7) 12.4.0, GNU ld (GNU Binutils) 2.40) #1 SMP Thu Jun 4 12:06:37 MSK 2026 [ 0.000000] CPU: ARMv7 Processor [413fc090] revision 0 (ARMv7), cr=18c5387d [ 0.000000] CPU: PIPT / VIPT nonaliasing data cache, VIPT aliasing instruction cache [ 0.000000] OF: fdt: Machine model: ZYNQ MINI Rev B (manual Linux port) [ 0.000000] earlycon: cdns0 at MMIO 0xe0001000 (options '115200n8') [ 0.000000] printk: bootconsole [cdns0] enabled [ 0.000000] Memory policy: Data cache writealloc [ 0.000000] efi: UEFI not found. [ 0.000000] cma: Reserved 64 MiB at 0x1c000000 on node -1 [ 0.000000] Zone ranges: [ 0.000000] DMA [mem 0x0000000000000000-0x000000001fffffff] [ 0.000000] Normal empty [ 0.000000] HighMem empty [ 0.000000] Movable zone start for each node [ 0.000000] Early memory node ranges [ 0.000000] node 0: [mem 0x0000000000000000-0x000000001fffffff] [ 0.000000] Initmem setup node 0 [mem 0x0000000000000000-0x000000001fffffff] [ 0.000000] percpu: Embedded 16 pages/cpu s36180 r8192 d21164 u65536 [ 0.000000] Kernel command line: console=ttyPS0,115200 earlycon fbcon=font:MINI4x6 root=/dev/mmcblk0p2 rootwait rw [ 0.000000] Dentry cache hash table entries: 65536 (order: 6, 262144 bytes, linear) [ 0.000000] Inode-cache hash table entries: 32768 (order: 5, 131072 bytes, linear) [ 0.000000] Built 1 zonelists, mobility grouping on. Total pages: 130048 [ 0.000000] mem auto-init: stack:all(zero), heap alloc:off, heap free:off [ 0.000000] Memory: 425940K/524288K available (15360K kernel code, 2484K rwdata, 6480K rodata, 2048K init, 418K bss, 32812K reserved, 65536K cma-reserved, 0K highmem) [ 0.000000] SLUB: HWalign=64, Order=0-3, MinObjects=0, CPUs=2, Nodes=1 [ 0.000000] trace event string verifier disabled [ 0.000000] rcu: Hierarchical RCU implementation. [ 0.000000] rcu: RCU event tracing is enabled. [ 0.000000] rcu: RCU restricting CPUs from NR_CPUS=16 to nr_cpu_ids=2. [ 0.000000] rcu: RCU calculated value of scheduler-enlistment delay is 10 jiffies. [ 0.000000] rcu: Adjusting geometry for rcu_fanout_leaf=16, nr_cpu_ids=2 [ 0.000000] NR_IRQS: 16, nr_irqs: 16, preallocated irqs: 16 [ 0.000000] slcr mapped to (ptrval) [ 0.000000] L2C: platform modifies aux control register: 0x72360000 -> 0x72760000 [ 0.000000] L2C: DT/platform modifies aux control register: 0x72360000 -> 0x72760000 [ 0.000000] L2C-310 erratum 769419 enabled [ 0.000000] L2C-310 enabling early BRESP for Cortex-A9 [ 0.000000] L2C-310 full line of zeros enabled for Cortex-A9 [ 0.000000] L2C-310 ID prefetch enabled, offset 1 lines [ 0.000000] L2C-310 dynamic clock gating enabled, standby mode enabled [ 0.000000] L2C-310 cache controller enabled, 8 ways, 512 kB [ 0.000000] L2C-310: CACHE_ID 0x410000c8, AUX_CTRL 0x76760001 [ 0.000000] rcu: srcu_init: Setting srcu_struct sizes based on contention. [ 0.000000] zynq_clock_init: clkc starts at (ptrval) [ 0.000000] Zynq clock init [ 0.000002] sched_clock: 64 bits at 167MHz, resolution 6ns, wraps every 4398046511103ns [ 0.002384] clocksource: arm_global_timer: mask: 0xffffffffffffffff max_cycles: 0x26703d7dd8, max_idle_ns: 440795208065 ns [ 0.013447] Switching to timer-based delay loop, resolution 6ns [ 0.020441] Console: colour dummy device 80x30 [ 0.023821] Calibrating delay loop (skipped), value calculated using timer frequency.. 333.33 BogoMIPS (lpj=1666666) [ 0.034341] CPU: Testing write buffer coherency: ok [ 0.039180] CPU0: Spectre v2: using BPIALL workaround [ 0.044238] pid_max: default: 32768 minimum: 301 [ 0.048958] Mount-cache hash table entries: 1024 (order: 0, 4096 bytes, linear) [ 0.056161] Mountpoint-cache hash table entries: 1024 (order: 0, 4096 bytes, linear) [ 0.064720] CPU0: thread -1, cpu 0, socket 0, mpidr 80000000 [ 0.070622] Setting up static identity map for 0x300000 - 0x3000ac [ 0.076426] rcu: Hierarchical SRCU implementation. [ 0.080536] rcu: Max phase no-delay instances is 1000. [ 0.087202] EFI services will not be available. [ 0.090517] smp: Bringing up secondary CPUs ... [ 0.095642] CPU1: thread -1, cpu 1, socket 0, mpidr 80000001 [ 0.095663] CPU1: Spectre v2: using BPIALL workaround [ 0.105620] smp: Brought up 1 node, 2 CPUs [ 0.109581] SMP: Total of 2 processors activated (666.66 BogoMIPS). [ 0.115827] CPU: All CPU(s) started in SVC mode. [ 0.121326] devtmpfs: initialized [ 0.126945] VFP support v0.3: implementor 41 architecture 3 part 30 variant 9 rev 4 [ 0.131663] clocksource: jiffies: mask: 0xffffffff max_cycles: 0xffffffff, max_idle_ns: 19112604462750000 ns [ 0.141259] futex hash table entries: 512 (order: 3, 32768 bytes, linear) [ 0.151478] pinctrl core: initialized pinctrl subsystem [ 0.155226] DMI not present or invalid. [ 0.157880] NET: Registered PF_NETLINK/PF_ROUTE protocol family [ 0.166273] DMA: preallocated 256 KiB pool for atomic coherent allocations [ 0.172119] thermal_sys: Registered thermal governor 'step_wise' [ 0.172202] cpuidle: using governor menu [ 0.183952] platform axi: Fixed dependency cycle(s) with /axi/interrupt-controller@f8f01000 [ 0.194056] platform replicator: Fixed dependency cycle(s) with /axi/etb@f8801000 [ 0.196015] amba f8801000.etb: Fixed dependency cycle(s) with /replicator [ 0.202968] platform replicator: Fixed dependency cycle(s) with /axi/tpiu@f8803000 [ 0.210389] amba f8803000.tpiu: Fixed dependency cycle(s) with /replicator [ 0.217404] platform replicator: Fixed dependency cycle(s) with /axi/funnel@f8804000 [ 0.224982] amba f8804000.funnel: Fixed dependency cycle(s) with /axi/ptm@f889d000 [ 0.232480] amba f8804000.funnel: Fixed dependency cycle(s) with /axi/ptm@f889c000 [ 0.240045] amba f8804000.funnel: Fixed dependency cycle(s) with /replicator [ 0.247336] amba f8804000.funnel: Fixed dependency cycle(s) with /axi/ptm@f889c000 [ 0.254737] amba f889c000.ptm: Fixed dependency cycle(s) with /axi/funnel@f8804000 [ 0.262464] amba f8804000.funnel: Fixed dependency cycle(s) with /axi/ptm@f889d000 [ 0.269879] amba f889d000.ptm: Fixed dependency cycle(s) with /axi/funnel@f8804000 [ 0.278556] No ATAGs? [ 0.279720] hw-breakpoint: found 5 (+1 reserved) breakpoint and 1 watchpoint registers. [ 0.287617] hw-breakpoint: maximum watchpoint size is 4 bytes. [ 0.297946] Serial: AMBA PL011 UART driver [ 0.298739] e0001000.serial: ttyPS0 at MMIO 0xe0001000 (irq = 26, base_baud = 6249999) is a xuartps [ 0.312292] printk: console [ttyPS0] enabled [ 0.312292] printk: console [ttyPS0] enabled [ 0.316585] printk: bootconsole [cdns0] disabled [ 0.316585] printk: bootconsole [cdns0] disabled [ 0.337423] iommu: Default domain type: Translated [ 0.342217] iommu: DMA domain TLB invalidation policy: strict mode [ 0.349287] SCSI subsystem initialized [ 0.353676] usbcore: registered new interface driver usbfs [ 0.359247] usbcore: registered new interface driver hub [ 0.364615] usbcore: registered new device driver usb [ 0.369950] usb_phy_generic usb-phy: dummy supplies not allowed for exclusive requests [ 0.379009] pps_core: LinuxPPS API ver. 1 registered [ 0.383976] pps_core: Software ver. 5.3.6 - Copyright 2005-2007 Rodolfo Giometti <giometti@linux.it> [ 0.393157] PTP clock support registered [ 0.398237] EDAC MC: Ver: 3.0.0 [ 0.402084] scmi_core: SCMI protocol bus registered [ 0.409463] vgaarb: loaded [ 0.412796] clocksource: Switched to clocksource arm_global_timer [ 0.436854] NET: Registered PF_INET protocol family [ 0.441956] IP idents hash table entries: 8192 (order: 4, 65536 bytes, linear) [ 0.450473] tcp_listen_portaddr_hash hash table entries: 512 (order: 0, 4096 bytes, linear) [ 0.458889] Table-perturb hash table entries: 65536 (order: 6, 262144 bytes, linear) [ 0.466651] TCP established hash table entries: 4096 (order: 2, 16384 bytes, linear) [ 0.474437] TCP bind hash table entries: 4096 (order: 4, 65536 bytes, linear) [ 0.481709] TCP: Hash tables configured (established 4096 bind 4096) [ 0.488157] UDP hash table entries: 256 (order: 1, 8192 bytes, linear) [ 0.494713] UDP-Lite hash table entries: 256 (order: 1, 8192 bytes, linear) [ 0.501868] NET: Registered PF_UNIX/PF_LOCAL protocol family [ 0.508286] RPC: Registered named UNIX socket transport module. [ 0.514212] RPC: Registered udp transport module. [ 0.518929] RPC: Registered tcp transport module. [ 0.523628] RPC: Registered tcp-with-tls transport module. [ 0.529114] RPC: Registered tcp NFSv4.1 backchannel transport module. [ 0.535556] PCI: CLS 0 bytes, default 64 [ 0.540091] armv7-pmu f8891000.pmu: hw perfevents: no interrupt-affinity property, guessing. [ 0.552670] hw perfevents: enabled with armv7_cortex_a9 PMU driver, 7 counters available [ 0.562148] Initialise system trusted keyrings [ 0.566862] workingset: timestamp_bits=30 max_order=17 bucket_order=0 [ 0.573699] squashfs: version 4.0 (2009/01/31) Phillip Lougher [ 0.579923] NFS: Registering the id_resolver key type [ 0.585055] Key type id_resolver registered [ 0.589252] Key type id_legacy registered [ 0.593287] nfs4filelayout_init: NFSv4 File Layout Driver Registering... [ 0.600025] nfs4flexfilelayout_init: NFSv4 Flexfile Layout Driver Registering... [ 0.607457] ntfs: driver 2.1.32 [Flags: R/O]. [ 0.612251] Key type asymmetric registered [ 0.616368] Asymmetric key parser 'x509' registered [ 0.621331] Block layer SCSI generic (bsg) driver version 0.4 loaded (major 245) [ 0.628754] io scheduler mq-deadline registered [ 0.633285] io scheduler kyber registered [ 0.637343] io scheduler bfq registered [ 0.684230] zynq-pinctrl 700.pinctrl: zynq pinctrl initialized [ 0.741495] dma-pl330 f8003000.dma-controller: Loaded driver for PL330 DMAC-241330 [ 0.749116] dma-pl330 f8003000.dma-controller: DBUFF-128x8bytes Num_Chans-8 Num_Peri-4 Num_Events-16 [ 0.836017] Serial: 8250/16550 driver, 5 ports, IRQ sharing enabled [ 0.847143] SuperH (H)SCI(F) driver initialized [ 0.852460] msm_serial: driver initialized [ 0.856586] STMicroelectronics ASC driver initialized [ 0.862508] STM32 USART driver initialized [ 0.897356] brd: module loaded [ 0.907123] loop: module loaded [ 0.919260] spi-nor spi0.0: w25q128 (16384 Kbytes) [ 0.930653] CAN device driver interface [ 2.156573] macb e000b000.ethernet eth0: Cadence GEM rev 0x00020118 at 0xe000b000 irq 40 (00:0a:35:01:02:03) [ 2.166994] bgmac_bcma: Broadcom 47xx GBit MAC driver loaded [ 2.173711] e1000e: Intel(R) PRO/1000 Network Driver [ 2.178722] e1000e: Copyright(c) 1999 - 2015 Intel Corporation. [ 2.184680] igb: Intel(R) Gigabit Ethernet Network Driver [ 2.190091] igb: Copyright (c) 2007-2014 Intel Corporation. [ 2.199364] pegasus: Pegasus/Pegasus II USB Ethernet driver [ 2.204976] usbcore: registered new interface driver pegasus [ 2.210689] usbcore: registered new interface driver asix [ 2.216128] usbcore: registered new interface driver ax88179_178a [ 2.222259] usbcore: registered new interface driver cdc_ether [ 2.228133] usbcore: registered new interface driver smsc75xx [ 2.233907] usbcore: registered new interface driver smsc95xx [ 2.239703] usbcore: registered new interface driver net1080 [ 2.245391] usbcore: registered new interface driver cdc_subset [ 2.251351] usbcore: registered new interface driver zaurus [ 2.256966] usbcore: registered new interface driver cdc_ncm [ 2.266480] usbcore: registered new interface driver usb-storage [ 2.274443] ci_hdrc ci_hdrc.0: EHCI Host Controller [ 2.279419] ci_hdrc ci_hdrc.0: new USB bus registered, assigned bus number 1 [ 2.315657] ci_hdrc ci_hdrc.0: USB 2.0 started, EHCI 1.00 [ 2.322032] hub 1-0:1.0: USB hub found [ 2.325863] hub 1-0:1.0: 1 port detected [ 2.335341] i2c_dev: i2c /dev entries driver [ 2.352775] cpufreq: cpufreq_online: CPU0: Running at unlisted initial frequency: 666666 KHz, changing to: 666667 KHz [ 2.364805] Xilinx Zynq CpuIdle Driver started [ 2.371324] sdhci: Secure Digital Host Controller Interface driver [ 2.377543] sdhci: Copyright(c) Pierre Ossman [ 2.383637] Synopsys Designware Multimedia Card Interface Driver [ 2.391277] sdhci-pltfm: SDHCI platform and OF driver helper [ 2.400814] ledtrig-cpu: registered to indicate activity on CPUs [ 2.409099] clocksource: ttc_clocksource: mask: 0xffff max_cycles: 0xffff, max_idle_ns: 537538477 ns [ 2.418342] timer #0 at (ptrval), irq=43 [ 2.422895] usbcore: registered new interface driver usbhid [ 2.428467] usbhid: USB HID core driver [ 2.438275] mmc0: SDHCI controller on e0100000.mmc [e0100000.mmc] using ADMA [ 2.439453] NET: Registered PF_INET6 protocol family [ 2.451844] Segment Routing with IPv6 [ 2.455646] In-situ OAM (IOAM) with IPv6 [ 2.459668] sit: IPv6, IPv4 and MPLS over IPv4 tunneling driver [ 2.466381] NET: Registered PF_PACKET protocol family [ 2.471431] can: controller area network core [ 2.475856] NET: Registered PF_CAN protocol family [ 2.480650] can: raw protocol [ 2.483632] can: broadcast manager protocol [ 2.487817] can: netlink gateway - max_hops=1 [ 2.492591] Key type dns_resolver registered [ 2.497178] ThumbEE CPU extension supported. [ 2.501455] Registering SWP/SWPB emulation handler [ 2.518152] Loading compiled-in X.509 certificates [ 2.540644] clk: Disabling unused clocks [ 2.545162] Waiting for root device /dev/mmcblk0p2... [ 2.559342] mmc0: new high speed SDHC card at address 5048 [ 2.566035] mmcblk0: mmc0:5048 SD16G 14.5 GiB [ 2.573896] mmcblk0: p1 p2 [ 2.912734] EXT4-fs (mmcblk0p2): mounted filesystem a8d72ef4-a949-4b83-b66b-246c019ace49 r/w with ordered data mode. Quota mode: disabled. [ 4.173876] VFS: Mounted root (ext4 filesystem) on device 179:2. [ 4.182218] devtmpfs: mounted [ 4.190936] Freeing unused kernel image (initmem) memory: 2048K [ 4.198372] Run /sbin/init as init process [ 4.412900] EXT4-fs (mmcblk0p2): re-mounted a8d72ef4-a949-4b83-b66b-246c019ace49 r/w. Quota mode: disabled. Saving 256 bits of non-creditable seed for next boot Starting syslogd: OK Starting klogd: OK Running sysctl: OK [ 4.646335] i2c_master_axi: loading out-of-tree module taints kernel. [ 4.653903] i2c-master-axi 43c00000.i2c: input=50000000 Hz, bus=100000 Hz, prescale=124 [ 5.163755] Console: switching to mono frame buffer device 32x10 [ 5.493987] ssd1307fb 0-003c: fb0: Solomon SSD1307 framebuffer device registered, using 1024 bytes of video memory [ 5.568915] macb e000b000.ethernet eth0: validation of with support 00,00000000,00000000,00006000 and advertisement 00,00000000,00000000,00000000 failed: -EINVAL [ 5.583586] macb e000b000.ethernet eth0: Could not attach PHY (-22) Starting network: [ 5.672046] macb e000b000.ethernet eth0: validation of with support 00,00000000,00000000,00006000 and advertisement 00,00000000,00000000,00000000 failed: -EINVAL [ 5.686791] macb e000b000.ethernet eth0: Could not attach PHY (-22) ip: SIOCSIFFLAGS: Invalid argument FAIL OLED console ready — login on tty1 (USB keyboard) Starting dropbear sshd: OK ZYNQ MINI Rev B - I2C_Master_Controller demo ---------------------------------------------- login: root (no password) i2cdetect -y 1 # SSD1306 lives at 0x3C on i2c-1 zynq-mini login: [ 7.632872] random: crng init done root #
Ключевые маркеры успешного прохождения этапов:
Маркер в UART |
Что означает |
Появился текст FSBL |
BootROM нашел и запустил |
Появился U-Boot banner |
FSBL успешно передал управление U-Boot |
Видно |
U-Boot читает FAT-раздел |
Видно |
U-Boot передал управление Linux |
Видны строки Linux kernel |
Ядро стартовало |
Появился login prompt |
rootfs смонтирован, init работает |
Если на UART полная тишина, это почти всегда проблема до Linux:
Неверный boot mode;
плата не питается;
UART подключен не к тому порту;
неправильная скорость UART;
FAT-раздел не читается BootROM;
BOOT.BIN отсутствует;
BOOT.BIN является placeholder;
BOOT.BIN собран не для этой платы;
FSBL не стартует.
В этом случае проверка DTS, rootfs, i2c-master-axi.ko и OLED пока не имеет смысла.
Проверка U-Boot
Если U-Boot появился, остановите автозагрузку нажатием любой клавиши во время countdown. Ожидаемый prompt:
zynq-mini>
Если prompt другой, это не обязательно ошибка, но значит, что CONFIG_SYS_PROMPT из uboot.fragment мог не примениться или загружен не тот U-Boot.
Проверим MMC:
mmc list mmc dev 0 mmc rescan
Проверим файлы на FAT:
fatls mmc 0:1
Ожидаемо:
BOOT.BIN uImage zynq-mini-revb.dtb uEnv.txt
Проверим загрузку uEnv.txt вручную:
fatload mmc 0:1 0x100000 uEnv.txt env import -t 0x100000 ${filesize} printenv bootargs printenv uenvcmd
Ожидаемый bootargs:
console=ttyPS0,115200 earlycon fbcon=font:MINI4x6 root=/dev/mmcblk0p2 rootwait rw
Проверим ручную загрузку ядра и DTB:
fatload mmc 0:1 0x3000000 uImage fatload mmc 0:1 0x2A00000 zynq-mini-revb.dtb bootm 0x3000000 - 0x2A00000
Если ручная загрузка работает, а автоматическая нет, проблема в CONFIG_BOOTCOMMAND или uEnv.txt. Если fatls не видит файлы, проблема в FAT-разделе, genimage.cfg или записи SD. Если bootm падает на DTB, проверьте, что на FAT лежит актуальный zynq-mini-revb.dtb, а не старый или поврежденный файл.
Проверка факта загрузки Linux
После строки:
Starting kernel ...
должен начаться вывод Linux.
После появления login prompt войдите под пользователем root. В типовой Buildroot-конфигурации пароль может отсутствовать, если это не было изменено отдельно.
Сразу проверим командную строку ядра:
cat /proc/cmdline
Ожидаемые фрагменты:
console=ttyPS0,115200 earlycon fbcon=font:MINI4x6 root=/dev/mmcblk0p2 rootwait rw
Если root=/dev/mmcblk0p2 отличается, Linux может пытаться монтировать не тот раздел.
Если отсутствует fbcon=font:MINI4x6, значит фактические bootargs отличаются от ожидаемых. Нужно проверить uEnv.txt, printenv bootargs в U-Boot и секцию chosen в DTS.
Проверим версию ядра:
# uname -a Linux zynq-mini 6.6.51 #1 SMP Thu Jun 4 12:06:37 MSK 2026 armv7l GNU/Linux # uname -r 6.6.51
Проверим, что rootfs смонтирован со второго раздела microSD:
# mount /dev/root on / type ext4 (rw,relatime,errors=remount-ro) devtmpfs on /dev type devtmpfs (rw,relatime,size=212968k,nr_inodes=53242,mode=755) proc on /proc type proc (rw,relatime) devpts on /dev/pts type devpts (rw,relatime,gid=5,mode=620,ptmxmode=666) tmpfs on /dev/shm type tmpfs (rw,relatime) tmpfs on /tmp type tmpfs (rw,relatime) tmpfs on /run type tmpfs (rw,nosuid,nodev,relatime,mode=755) sysfs on /sys type sysfs (rw,relatime) # df -h Filesystem Size Used Available Use% Mounted on /dev/root 245.9M 41.9M 187.2M 18% / devtmpfs 208.0M 0 208.0M 0% /dev tmpfs 241.0M 0 241.0M 0% /dev/shm tmpfs 241.0M 28.0K 240.9M 0% /tmp tmpfs 241.0M 20.0K 241.0M 0% /run # cat /proc/partitions major minor #blocks name 1 0 65536 ram0 1 1 65536 ram1 1 2 65536 ram2 1 3 65536 ram3 1 4 65536 ram4 1 5 65536 ram5 1 6 65536 ram6 1 7 65536 ram7 1 8 65536 ram8 1 9 65536 ram9 1 10 65536 ram10 1 11 65536 ram11 1 12 65536 ram12 1 13 65536 ram13 1 14 65536 ram14 1 15 65536 ram15 31 0 16384 mtdblock0 179 0 15224832 mmcblk0 179 1 65536 mmcblk0p1 179 2 262144 mmcblk0p2 #
Ожидаемо должен быть раздел вида:
/dev/mmcblk0p2 on / type ext4
Если rootfs не монтируется, типичные причины:
в genimage второй раздел не ext4;
root=/dev/mmcblk0p2 не соответствует реальной разметке;
SD-карта не успевает появиться и нет rootwait;
rootfs поврежден;
ядро не содержит нужную поддержку MMC/ext4.
Проверка загруженного Device Tree
Нужно убедиться, что Linux получил именно наш DTB.
Проверим модель платы:
# cat /proc/device-tree/model ZYNQ MINI Rev B (manual Linux port)
Проверим compatible корня:
№tr '\0' '\n' < /proc/device-tree/compatible user,zynq-mini-revb xlnx,zynq-7000
Проверим наличие нашего PL-узла:
# ls /proc/device-tree/amba_pl@0/ #address-cells compatible name #size-cells i2c@43c00000 ranges # ls /proc/device-tree/amba_pl@0/i2c@43c00000 #address-cells clocks interrupts #size-cells compatible name clock-frequency input-clock-frequency oled@3c clock-names interrupt-parent reg
Проверим compatible I2C-контроллера:
# tr '\0' '\n' < /proc/device-tree/amba_pl@0/i2c@43c00000/compatible user,i2c-master-axi-1.0
Проверим reg в DTB:
# hexdump -Cv /proc/device-tree/amba_pl@0/i2c@43c00000/reg 00000000 43 c0 00 00 00 00 10 00 |C.......| 00000008
Там должны быть закодированы значения:
0x43c00000 0x00001000
Проверим дочерний OLED:
# ls /proc/device-tree/amba_pl@0/i2c@43c00000/oled@3c compatible solomon,com-invdir solomon,prechargep1 name solomon,height solomon,prechargep2 reg solomon,page-offset solomon,width # tr '\0' '\n' < /proc/device-tree/amba_pl@0/i2c@43c00000/oled@3c/compatible solomon,ssd1306fb-i2c # hexdump -Cv /proc/device-tree/amba_pl@0/i2c@43c00000/oled@3c/reg 00000000 00 00 00 3c |...<| 00000004
Если этих узлов нет, значит Linux загрузился не с тем DTB. Нужно заменить zynq-mini-revb.dtb на FAT-разделе и перезапустить плату.
Проверка модулей и автозагрузки i2c-master-axi
Проверим, есть ли модуль в rootfs:
# find /lib/modules/$(uname -r) -name 'i2c-master-axi.ko' /lib/modules/6.6.51/updates/i2c-master-axi.ko
Проверим конфиг автозагрузки:
# cat /etc/modules-load.d/i2c-master-axi.conf i2c-master-axi
Проверим init-скрипт:
# ls -l /etc/init.d/S03modules -rwxr-xr-x 1 1000 1000 570 Jun 4 2026 /etc/init.d/S03modules
Проверим, загрузился ли модуль:
# lsmod | grep i2c i2c_master_axi 12288 0
Проверим логи драйвера:
# dmesg | grep -i i2c-master [ 4.653903] i2c-master-axi 43c00000.i2c: input=50000000 Hz, bus=100000 Hz, prescale=124
Значение prescale=124 ожидаемо для:
input-clock-frequency = 50000000 clock-frequency = 100000
Если модуль не загружен автоматически, попробуйте вручную:
modprobe i2c-master-axi dmesg | tail -80
Если вручную загрузился, проблема в S03modules или /etc/modules-load.d.
Если modprobe пишет invalid module format, модуль собран для того ядра, которое реально загружено. Проверьте:
uname -r modinfo i2c-master-axi | grep vermagic
Если modprobe проходит, но в dmesg нет probe, проверяйте:
compatible в загруженном DTB;
of_match_table в драйвере;
имя модуля;
наличие узла i2c@43c00000.
Проверка I2C-шины
После успешного probe драйвер должен зарегистрировать I2C-адаптер. Проверим устройства I2C:
# ls -l /dev/i2c-* 2>/dev/null crw------- 1 root root 89, 0 Jan 1 00:00 /dev/i2c-0 # ls /sys/bus/i2c/devices/ 0-003c i2c-0
Если /dev/i2c-* отсутствует, проверьте, включен ли CONFIG_I2C_CHARDEV в linux.fragment.
Чтобы понять номер шины, можно посмотреть все адаптеры:
# for d in /sys/class/i2c-adapter/i2c-*; do > echo "$d" > cat "$d/name" 2>/dev/null || true > done /sys/class/i2c-adapter/i2c-0 43c00000.i2c
Ищем адаптер, имя которого связано с i2c-master-axi или platform-устройством 43c00000.i2c.
Далее сканируем шину. Если адаптер оказался i2c-1:
# i2cdetect -y 0 0 1 2 3 4 5 6 7 8 9 a b c d e f 00: -- -- -- -- -- -- -- -- 10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 30: -- -- -- -- -- -- -- -- -- -- -- -- UU -- -- -- 40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 70: -- -- -- -- -- -- -- --
Ожидаемый результат на адресе 0x3c:3cилиUU
Расшифровка:
Значение |
Смысл |
|
Устройство отвечает и не занято kernel-драйвером |
|
Устройство отвечает, но адрес уже занят драйвером, например |
|
На адресе ответа нет |
Если i2cdetect показывает -- на 0x3c, но i2c-master-axi успешно загружен, проблема, скорее всего, ниже уровня Linux:
OLED не запитан;
адрес OLED 0x3d, а не 0x3c;
неверно подключены SDA/SCL;
нет pull-up;
не тот bitstream;
пины в XDC не соответствуют подключению;
IP не подключен к нужным выводам PL;
OLED находится в SPI-режиме, а не I2C.
Для внешнего модуля через CAM1 нужно отдельно сверить, что в XDC используются правильные пины. Для варианта CAM1 из Vivado-гайда указывались SDA на T20 и SCL на P20. Для встроенного разъема J4 использовались другие линии, например E18 и E19. Если bitstream собран под один вариант подключения, а физически OLED подключен к другому, Linux-драйвер будет работать, но на реальных линиях OLED ответа не будет.
Проверка framebuffer и OLED
Если I2C-дисплей описан в DTS и драйвер ssd1307fb включен в ядро, после регистрации I2C-адаптера должен появиться framebuffer.
Проверка:
# ls -l /dev/fb* crw------- 1 root root 29, 0 Jan 1 00:00 /dev/fb0 # dmesg | grep -iE "ssd|fb|framebuffer" [ 0.000000] Kernel command line: console=ttyPS0,115200 earlycon fbcon=font:MINI4x6 root=/dev/mmcblk0p2 rootwait rw [ 5.493987] ssd1307fb 0-003c: fb0: Solomon SSD1307 framebuffer device registered, using 1024 bytes of video memory
Если есть /dev/fb0, проверим параметры:
# fbset -fb /dev/fb0 mode "128x64" geometry 128 64 128 64 1 timings 0 0 0 0 0 0 0 rgba 1/0,1/0,1/0,0/0 endmode
Проверим framebuffer простым выводом:
dd if=/dev/zero of=/dev/fb0 bs=1024 count=1
Если установлен fbv, можно попробовать вывести изображение. Но для монохромного OLED 128x64 нужно использовать подходящий формат или отдельную тестовую утилиту. Для первичной проверки достаточно появления /dev/fb0, сообщений ssd1307fb в dmesg и признаков активности на OLED.
Если /dev/fb0 нет, но i2cdetect видит 3c, вероятные причины:
не включен CONFIG_FB_SSD1307;
неверный compatible OLED в DTS;
дочерний узел oled@3c не подхвачен I2C core;
драйвер OLED собран модулем, но не загружен;
ошибка параметров solomon,width/height;
ошибка в последовательности probe ssd1307fb.
Проверить наличие драйвера в конфиге ядра:
zcat /proc/config.gz | grep CONFIG_FB_SSD1307
Если /proc/config.gz недоступен, проверять нужно на host в итоговом .config ядра:
grep CONFIG_FB_SSD1307 "$BR_OUT/build/linux-"*/.config
Проверка Ethernet и DHCP
Если Ethernet нужен для дальнейшей отладки, проверим интерфейс:
# ip link 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 2: eth0: <BROADCAST,MULTICAST> mtu 1500 qdisc noop qlen 1000 link/ether 00:0a:35:01:02:03 brd ff:ff:ff:ff:ff:ff 3: sit0@NONE: <NOARP> mtu 1480 qdisc noop qlen 1000 link/sit 0.0.0.0 brd 0.0.0.0 # ip addr show eth0 2: eth0: <BROADCAST,MULTICAST> mtu 1500 qdisc noop qlen 1000 link/ether 00:0a:35:01:02:03 brd ff:ff:ff:ff:ff:ff # dmesg | grep -iE "macb|gem|phy|eth" [ 0.000000] Booting Linux on physical CPU 0x0 [ 0.369950] usb_phy_generic usb-phy: dummy supplies not allowed for exclusive requests [ 2.156573] macb e000b000.ethernet eth0: Cadence GEM rev 0x00020118 at 0xe000b000 irq 40 (00:0a:35:01:02:03) [ 2.184680] igb: Intel(R) Gigabit Ethernet Network Driver [ 2.199364] pegasus: Pegasus/Pegasus II USB Ethernet driver [ 2.222259] usbcore: registered new interface driver cdc_ether [ 5.568915] macb e000b000.ethernet eth0: validation of with support 00,00000000,00000000,00006000 and advertisement 00,00000000,00000000,00000000 failed: -EINVAL [ 5.583586] macb e000b000.ethernet eth0: Could not attach PHY (-22) [ 5.672046] macb e000b000.ethernet eth0: validation of with support 00,00000000,00000000,00006000 and advertisement 00,00000000,00000000,00000000 failed: -EINVAL [ 5.686791] macb e000b000.ethernet eth0: Could not attach PHY (-22) #
Что-то не взлетело. Кажись ошиблись где-то номером.
Проверим MAC:
# cat /sys/class/net/eth0/address 00:0a:35:01:02:03 # # cat /etc/eth0-mac 00:0a:35:01:02:03
Значения должны совпадать.
Если DHCP включен через Buildroot:
BR2_SYSTEM_DHCP="eth0"
то после загрузки интерфейс должен получить IP-адрес.
Проверка:
ip addr show eth0
Если адрес не получен:
udhcpc -i eth0
Проверить связь:
ping -c 3 192.168.1.1
или до известного узла в вашей сети.
Если Ethernet не работает, проверять нужно слоями:
узел &gem0 в DTS;
phy-mode = “rgmii-id”;
MDIO-адрес PHY;
питание PHY;
MAC-адрес;
линк на RJ45;
DHCP;
сетевой кабель.
Что считается успешным результатом главы
После этой проверки у нас должно быть подтверждено:
microSD корректно записана.
BootROM запускает настоящий BOOT.BIN.
FSBL инициализирует PS и загружает bitstream в PL.
U-Boot стартует и видит FAT-раздел.
U-Boot загружает uImage и DTB.
Linux стартует с правильными bootargs.
Rootfs монтируется с /dev/mmcblk0p2.
Загружен правильный DTB с узлом i2c@43c00000.
Модуль i2c-master-axi загружается автоматически.
Драйвер регистрирует I2C-адаптер.
OLED виден на I2C-адресе 0x3c или занят драйвером как UU.
Создан framebuffer /dev/fb0.
При необходимости поднят Ethernet и SSH.
Если этот набор выполнен, можно считать, что весь путь от Vivado bitstream до Linux-драйвера и OLED прошел успешно.
Ну и если у вас получилось увидеть долгожданный мерцающий курсор что и у меня - значит все точно получилось :)

Правда не вышло подключить клавиатуру т.к. нет напряжения питания на VBUS. Вероятно есть косяк в DTS с питанием. Но думаю не будем на этом этапе раздувать статью с дебагом и оставим это для следующей статьи.
Вместо заключения
В этой статье был разобран полный маршрут подготовки Linux-образа для платы на Zynq-7000: от структуры BR2_EXTERNAL и описания платы в Device Tree до сборки rootfs, U-Boot, out-of-tree модуля ядра и итогового загрузочного образа для microSD.
Главный результат - не просто рабочий набор команд, а воспроизводимая структура проекта. Все изменения, относящиеся к конкретной плате, вынесены из дерева Buildroot в отдельное внешнее дерево: DTS, фрагменты конфигурации, пользовательский пакет, скрипты постобработки и defconfig. Это делает сборку переносимой, пригодной для версионирования и понятной при повторном bring-up.
Отдельно показан принцип интеграции пользовательского IP-блока из PL в Linux. Device Tree описывает наличие AXI-устройства, драйвер i2c-master-axi превращает его в стандартный Linux I2C adapter, а дальнейшая работа с OLED выполняется уже через штатные механизмы ядра. Такой подход хорошо масштабируется: вместо ручных обходных решений появляется нормальная связка "железо - Device Tree - драйвер - подсистема ядра - userspace".
Практически важный вывод состоит в разделении зон ответственности. Buildroot собирает Linux-часть системы: ядро, DTB, rootfs, модули и пользовательские утилиты. bootgen собирает ранний загрузочный BOOT.BIN из FSBL, bitstream и U-Boot. Благодаря этому можно отдельно обновлять Linux, DTS или rootfs, не пересобирая ранний загрузчик, если не менялись FSBL, bitstream или U-Boot. Получившийся проект можно использовать как базу для дальнейшей инженерной доводки: фиксации версий инструментов, расширения диагностики, автоматизации сборки BOOT.BIN, добавления новых PL IP-блоков и оформления полноценного checklist для проверки платы после каждой сборки.
В следующей статье мы сделаем работу над ошибками и несовершенствами этого материала:
исправим проблему с Ethernet и USB;
разберем код скриптов для автостарта;
сделаем Userspace-утилиту для вывода нужной нам информации из системы взамен системной консоли;
добавим нужные утилиты и пересоберем образ;
До встречи в следующей статье! Всем спасибо! :)
Присоединяйтесь к моему паблику https://t.me/zynq7000
Я туда выкладываю всякое-интересное по теме в т.ч. ссылки на примеры, BSP и ссылки на Aliexpress :)
Размещайте облачную инфраструктуру и масштабируйте сервисы с надежным облачным провайдером Beget.
Эксклюзивно для читателей Хабра мы даем бонус 10% при первом пополнении.
