Теперь пора переносить проект под управление ОС 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 picocom

  • bootgen, он входит в 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-контексте

pc

Адрес следующей инструкции - куда “прыгнули” при handoff

sp

Указатель стека - без валидного стека C-код U-Boot не заработает

r0-r3

По соглашению 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. Иначе тогда:

  1. Запустите Vitis Unified IDE (vitis &);

  2. FileSwitch Workspace… (или Open Workspace на стартовой странице);

  3. Укажите новую директорию, например: <repo>/vitis/workspace_linux (имя может быть любым; главное - не полагаться на чужой собранный workspace без вашего XSA).

  4. Open.

Создаем Platform Component из XSA:

  1. FileNew Component Platform.

  2. Component name: zynq_mini_oled_platform.

  3. Next.

  4. Create platform from hardware specification (XSA).

  5. Browse → выберите файл, созданный в предыдущей статье: <repo>/vivado/zynq_mini_oled.xsa

  6. Не используйте XSA из другого проекта или чужой машины.

  7. Next:

    1. OS: standalone

    2. CPU: ps7_cortexa9_0

    3. Domain: standalone_ps7_cortexa9_0

  8. 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 (ошибки тоже не печатаются через fsbl_printf)

Уровень 1

FSBL_DEBUG

Баннер, режим загрузки, коды ошибок (DDR_INIT_FAIL, SD_INIT_FAIL, …), handoff

Уровень 2

FSBL_DEBUG_INFO

Всё из уровня 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 вручную:

  1. В Explorer / Components откройте: <workspace>/zynq_mini_oled_platform/Sources/zynq_fsbl/UserConfig.cmake

  2. Включаем режим редактирования Source через кнопку в заголовке;

  3. Найдите блок USER_COMPILE_DEFINITIONS (в начале файла, секция USER SETTINGS).

  4. Замените пустую строку на один макрос (для полного лога - 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.

  5. Сохраните файл (Ctrl+S). Откройте UserConfig.cmake снова и убедитесь, что строка записана на диск.

  6. Clean + Build (важно именно пересобрать FSBL, не полагаться на «ничего не изменилось»):

    • правый клик zynq_fsblClean (если есть);

    • затем правый клик zynq_mini_oled_platformBuild  (или 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" увидит, что узел должен быть включен.

Краткое резюме по основным конструкциям:

Конструкция

Пример

Смысл

Узел

uart1: serial@e0001000 { ... };

Устройство, шина, контроллер или логический блок

Имя узла

serial@e0001000

Человекочитаемое имя и адрес после @

Метка

uart1:

Имя для ссылок на узел через &uart1

Свойство

status = "okay";

Пара ключ-значение

Строка

"okay"

Строковое значение в кавычках

Число

<115200>

Одно 32-битное значение

Массив чисел

<0x43c00000 0x1000>

Несколько 32-битных ячеек подряд

Пустое свойство

broken-cd;

Булевый флаг без значения

Phandle-ссылка

serial0 = &uart1;

Ссылка на другой узел

Include

#include "xilinx/zynq-7000.dtsi"

Подключение внешнего .dtsi файла

Доработка узла

&uart1 { status = "okay"; };

Изменение или дополнение уже описанного узла

Отдельно важно понять свойства #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

0x43C00000

Vivado Address Editor. DTS сообщает ядру, что по этому адресу находится AXI I2C-контроллер

Размер окна reg

0x1000

Vivado Address Editor, поле Range. Для простого IP-блока хватает окна 4 KiB

FCLK0

50 MHz

PS7 configuration в Vivado. Частота тактирования логики PL

clocks = <&clkc 15>

15

Binding для Zynq clock controller. Индекс 15 соответствует FCLK0

input-clock-frequency

50000000

Частота входного тактирования IP-блока в герцах

clock-frequency для I2C

100000

Желаемая частота SCL. 100000 соответствует standard mode 100 kHz

IRQ в DTS

<0 29 4>

Формат ARM GIC: 0 - SPI, 29 - номер interrupt в DT, 4 - level high

DDR size

0x20000000

512 MiB. Размер памяти платы

UART

&uart1

Включенный UART PS7, выведенный через MIO 48/49

SD

&sdhci0

SDIO-контроллер PS7, обычно MIO 40..45

Ethernet PHY

reg = <1>

MDIO-адрес PHY, например RTL8211E на адресе 1

OLED I2C address

0x3c

Даташит или схема модуля OLED. Для SSD1306/SSD1307 часто 0x3c, иногда 0x3d

OLED geometry

128x64

Параметры OLED-модуля из даташита или описания платы

root=/dev/mmcblk0p2

второй раздел SD

Разметка образа. Например p1 - FAT boot, p2 - ext4 rootfs

Пины OLED

T20/P20

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 = <&ethernet_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:

Токен

Смысл

Источник

console=ttyPS0,115200

Консоль ядра через UART PS

UART1, скорость 115200

earlycon

Ранний вывод до полной инициализации UART

Удобно для отладки загрузки

fbcon=font:MINI4x6

Мелкий шрифт framebuffer-консоли

Требует поддержку CONFIG_FONT_MINI_4x6

root=/dev/mmcblk0p2

Корневая файловая система на втором разделе SD

Разметка genimage.cfg

rootwait

Ждать появления root-устройства

SD-карта может появиться не сразу

rw

Монтировать 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;
};

Смысл свойств:

Свойство

Смысл

status = "okay"

Включить SDIO0

bus-width = <4>

Использовать 4-битный режим SD

broken-cd

Нет рабочей линии Card Detect

disable-wp

Нет линии Write Protect

Без broken-cd драйвер может считать, что карта отсутствует, если на плате не разведена линия обнаружения карты.

Ethernet описывается через связку MAC и PHY:

&gem0 {
	status = "okay";
	phy-mode = "rgmii-id";
	phy-handle = <&ethernet_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 = <&ethernet_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>;
	};
};

Смысл свойств:

Свойство

Значение

Смысл

status = "okay"

включено

Активировать QSPI-контроллер

is-dual = <0>

0

Один flash-чип, не dual-stack

num-cs = <1>

1

Один chip select

flash@0

CS0

Flash подключена к первому chip select

compatible

winbond,w25q128, jedec,spi-nor

Драйвер spi-nor распознает flash по JEDEC ID

reg = <0>

0

Номер chip select

spi-max-frequency = <100000000>

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 - это пассивный контейнер. У него нет собственного сложного драйвера. Ядро просто проходит по его дочерним узлам и создает устройства для них.

Свойства:

Элемент

Смысл

compatible = "simple-bus"

Контейнер для MMIO-устройств

#address-cells = <1>

Адрес дочернего устройства занимает одну 32-битную ячейку

#size-cells = <1>

Размер окна занимает одну 32-битную ячейку

ranges;

Адреса дочерних устройств напрямую совпадают с адресным пространством 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

0

Тип прерывания: SPI

1

29

Номер SPI в формате Device Tree

2

IRQ_TYPE_LEVEL_HIGH

Активный высокий уровень

Расчет для 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>;

Их смысл:

Свойство

Значение

Кто использует

input-clock-frequency

50000000

Драйвер IP-блока, входная частота AXI/FCLK0

clock-frequency

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>;
};

Смысл свойств:

Свойство

Смысл

compatible = "solomon,ssd1306fb-i2c"

Привязка к драйверу ssd1307fb

reg = <0x3c>

7-битный I2C-адрес OLED

solomon,height = <64>

Высота панели

solomon,width = <128>

Ширина панели

solomon,page-offset = <0>

Смещение страниц памяти дисплея

solomon,com-invdir

Направление COM scan

solomon,prechargep1 / prechargep2

Параметры 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 выполнит следующий порядок действий:

  1. Распаковка исходников Linux в $BR_OUT/build/linux-*

  2. Копирование arch/arm/configs/multi_v7_defconfig в качестве стартового .config.

  3. Слияние фрагментов из BR2_LINUX_KERNEL_CONFIG_FRAGMENT_FILES (наш  board/zynq_mini_revb/linux.fragment) поверх базы производит утилита scripts/kconfig/merge_config.sh из дерева ядра.

  4. Сборка всех артефатов: uImage, модули, DTB (DTS задаётся отдельно в BR2_LINUX_KERNEL_CUSTOM_DTS_PATH).

Важно разделять две вещи:

  1. linux.fragment - влияет на то, какие драйверы и возможности будут собраны в ядро;

  2. 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

CONFIG_I2C, CONFIG_I2C_CHARDEV

Базовая I2C-подсистема и доступ к шинам через /dev/i2c-*

OLED framebuffer

CONFIG_FB, CONFIG_FB_SSD1307

Драйвер для SSD1306/SSD1307 OLED через framebuffer

Консоль на экране

CONFIG_FRAMEBUFFER_CONSOLE, CONFIG_FONT_MINI_4x6

Возможность вывести Linux console на framebuffer

QSPI

CONFIG_SPI_ZYNQ_QSPI

Поддержка QSPI-контроллера Zynq и SPI NOR flash

USB

CONFIG_USB_*, CONFIG_NOP_USB_XCEIV

USB host и простой PHY-драйвер

Ethernet

CONFIG_MACB, CONFIG_PHYLIB, CONFIG_REALTEK_PHY

GEM/MACB-драйвер Zynq и Realtek PHY

devtmpfs

CONFIG_DEVTMPFS_MOUNT

Автоматическое появление устройств в /dev

Отладка

CONFIG_PRINTK_TIME, CONFIG_DYNAMIC_DEBUG, CONFIG_DEBUG_FS

Удобство диагностики при первом запуске

После сборки нужно проверить не только наличие строк в итоговом .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:

  1. DTB для U-Boot нужен самому U-Boot, чтобы он корректно стартовал и описал свою минимальную платформу;

  2. 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:

Адрес

Что загружается

Назначение

0x100000

uEnv.txt

Временная область для переменных окружения

0x2A00000

zynq-mini-revb.dtb

Device Tree для Linux

0x3000000

uImage

Образ ядра Linux

Ключевые опции uboot.fragment:

Опция

Зачем нужна

CONFIG_TOOLS_LIBCRYPTO=y

Нужна части инструментов U-Boot при сборке образов и проверке форматов

CONFIG_OF_EMBED=y

Встроить DTB для самого U-Boot в его образ

CONFIG_USE_BOOTCOMMAND=y

Разрешить использование заданной команды автозагрузки

CONFIG_BOOTDELAY=1

Дать 1 секунду на остановку автозагрузки

CONFIG_BOOTCOMMAND=...

Описать полную последовательность загрузки с SD

CONFIG_FS_FAT=y

Поддержка FAT-файловой системы

CONFIG_CMD_FAT=y

Команды работы с FAT, включая fatload

CONFIG_FS_EXT4=y

Поддержка ext4

CONFIG_CMD_EXT4=y

Команды работы с ext4

CONFIG_LEGACY_IMAGE_FORMAT=y

Поддержка старого формата uImage

CONFIG_CMD_BOOTM=y

Команда bootm для запуска uImage

CONFIG_CMD_MMC=y

Команды работы с MMC/SD

CONFIG_SYS_PROMPT="zynq-mini> "

Приглашение командной строки 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.

В нем задаются основные переменные загрузки:

Переменная

Значение

Смысл

bootargs

строка параметров ядра

Передается Linux при старте

ethaddr

00:0a:35:01:02:03

MAC-адрес GEM0

load_dtb_addr

0x2A00000

Адрес загрузки DTB

load_kernel_addr

0x3000000

Адрес загрузки ядра

uenvcmd

команда загрузки

Основной сценарий запуска 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 должен назначаться из управляемого диапазона и не должен дублироваться между платами.

Итоговая логика загрузки получается такой:

  1. FSBL инициализирует PS7, DDR и передает управление U-Boot.

  2. U-Boot стартует со своим встроенным Device Tree.

  3. U-Boot сканирует SD-карту.

  4. U-Boot пытается загрузить uEnv.txt с FAT-раздела.

  5. Если uEnv.txt найден, U-Boot импортирует переменные и выполняет uenvcmd.

  6. uenvcmd загружает uImage и zynq-mini-revb.dtb в DDR.

  7. Команда bootm запускает Linux и передает ему DTB.

  8. 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 не реализует доступ к железу. Он только сообщает ядру:

  1. По адресу 0x43c00000 есть устройство;

  2. Устройство совместимо с “user,i2c-master-axi-1.0”;

  3. У него есть регистровое окно 0x1000 байт;

  4. Оно может использовать прерывание;

  5. На его 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

compatible, reg, interrupts, clocks, clock-frequency

Описание устройства

Драйвер i2c-master-axi.c

probe, ioremap, i2c_adapter, master_xfer

Программная логика работы с IP

RTL IP-блок в PL

регистры CTRL, STATUS, CMD, TX_DATA, RX_DATA, PRESCALE, ISR

Реальный контроллер I2C на AXI

После загрузки этого модуля цепочка должна стать рабочей:

  1. DTB содержит узел i2c@43c00000

  2. ядро находит compatible = “user,i2c-master-axi-1.0”

  3. platform_driver вызывает probe()

  4. драйвер отображает MMIO-регистры через ioremap

  5. драйвер считывает частоты из Device Tree

  6. драйвер рассчитывает PRESCALE для SCL

  7. драйвер включает IP-блок

  8. драйвер регистрирует struct i2c_adapter

  9. в Linux появляется I2C-шина

  10. ядро видит дочерний oled@3c

  11. ssd1307fb привязывается к OLED

  12. появляется 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(), он сообщает ядру:

  1. Появился новый I2C master;

  2. Через него можно отправлять struct i2c_msg;

  3. Для передачи сообщений вызывайте мой master_xfer callback.

После этого userspace увидит новую шину, например:

/dev/i2c-1
/sys/bus/i2c/devices/i2c-1

Номер может быть другим, потому что зависит от порядка регистрации I2C-адаптеров.

Пятая функция - реализовать передачу I2C-сообщений.

Linux I2C core работает не с регистрами CMD, STATUS, TX_DATA. Он передает драйверу массив структур:

struct i2c_msg

Каждое сообщение содержит:

  1. адрес slave-устройства;

  2. флаг чтения или записи;

  3. буфер;

  4. длину буфера.

Драйвер должен перевести это в последовательность операций на шине:

  1. START адрес + bit R/W

  2. данные

  3. ACK/NACK

  4. STOP

Для этого в драйвере есть связка функций:

axi_master_xfer()
    -> axi_xfer_one()
        -> axi_send_cmd()
            -> axi_wait_tip()

Шестая функция - корректно обрабатывать ошибки.

На I2C типовые ошибки такие:

  1. устройство не ответило на адрес;

  2. устройство ответило на адрес, но не приняло байт данных;

  3. шина занята;

  4. контроллер не завершил транзакцию;

  5. потеря арбитража;

  6. неверные частоты или невозможный PRESCALE.

Драйвер должен возвращать стандартные коды ошибок Linux:

  1. -ENXIO - нет устройства на адресе;

  2. -EIO - ошибка обмена данными;

  3. -ETIMEDOUT - контроллер не завершил операцию;

  4. -EAGAIN - шина занята или потеря арбитража;

  5. -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-блока

Каждый слой решает свою задачу.

Слой

Что содержит

За что отвечает

Регистры

I2C_REG_CTRL, I2C_REG_STATUS, I2C_REG_CMD, биты CMD_*, STATUS_*

Соответствие C-кода карте регистров RTL

MMIO-доступ

axi_read, axi_write

Чтение и запись 32-битных регистров

Ожидание IP

axi_wait_tip

Ждать завершения микрокоманды по TIP или IRQ

Команда IP

axi_send_cmd

Записать CMD и дождаться завершения

Одно I2C-сообщение

axi_xfer_one

Преобразовать один i2c_msg в START, адрес, data, STOP

I2C core adapter

axi_master_xfer, axi_algo, axi_functionality

Реализовать интерфейс Linux I2C subsystem

Инициализация железа

i2c_master_axi_hw_init

Рассчитать PRESCALE, включить CTRL.EN, IRQ

Platform layer

probe, remove, of_match_table

Связать Device Tree, модуль и устройство

IRQ layer

i2c_master_axi_isr, completion

Будить поток после завершения команды

Ниже разберем эти слои последовательно.

Карта регистров и константы

В начале драйвера обычно находятся определения регистров и битовых масок. Они должны соответствовать 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

Роль регистров:

Регистр

Назначение

CTRL

Включение IP-блока и разрешение IRQ

STATUS

Состояние контроллера: TIP, RXACK, BUSY, AL

CMD

Запуск фазы I2C: START, STOP, READ, WRITE, NACK

TX_DATA

Байт, который нужно отправить на I2C

RX_DATA

Байт, который был принят с I2C

PRESCALE

Делитель для формирования SCL

ISR

Флаги прерываний, обычно DONE и AL

Ключевые биты STATUS:

Бит

Смысл

STATUS_TIP

Transfer in progress. IP сейчас выполняет команду

STATUS_RXACK

Ответ slave. Важно: 1 означает NACK

STATUS_BUSY

Шина занята

STATUS_AL

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);

Это не просто косметика. Такие функции фиксируют модель доступа к железу:

  1. драйвер обращается только к MMIO-регистрам;

  2. каждый регистр имеет 32-битный доступ;

  3. смещения задаются через I2C_REG_*;

  4. обычные указатели 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

может означать:

  1. сформировать START;

  2. выдать байт из TX_DATA;

  3. дождаться ACK/NACK;

  4. обновить STATUS.

Пока команда выполняется, в STATUS установлен бит TIP: TIP = transfer in progress

Следующую команду нельзя подавать, пока TIP = 1. Поэтому почти каждая операция с I2C строится по схеме:

  1. записать CMD;

  2. ждать TIP = 0;

  3. прочитать STATUS;

  4. проверить ошибки.

Этим занимается функция 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-блок не завершил команду. Возможные причины:

  1. bitstream не загружен;

  2. адрес reg в DTS неверный;

  3. clock на IP не подан;

  4. IP завис;

  5. шина SDA/SCL физически удерживается;

  6. reset IP не снят.

Режим IRQ

Если IRQ подключен, драйвер может не опрашивать STATUS в цикле. Последовательность такая:

  1. драйвер записал CMD;

  2. поток уснул на completion;

  3. IP завершил команду;

  4. IP поднял irq_o;

  5. GIC вызвал IRQ handler;

  6. IRQ handler сбросил ISR;

  7. IRQ handler вызвал complete();

  8. поток проснулся;

  9. драйвер прочитал STATUS.

IRQ-режим экономит CPU, но для первого запуска polling часто проще. Он требует меньше условий: можно поднять I2C даже до полной проверки interrupt routing. В обоих режимах после завершения команды драйвер проверяет STATUS_AL.AL означает arbitration lost. Для платы с одним I2C-master это редкая ситуация, но она может появиться при некорректном состоянии шины или сбое транзакции. В этом случае возвращается-EAGAIN

Смысл: операцию теоретически можно повторить.

axi_send_cmd: одна атомарная фаза I2C

Функция axi_send_cmd - это минимальный строительный блок всех I2C-транзакций в драйвере.

Ее задача:

  1. подготовить ожидание;

  2. сбросить старые IRQ-флаги;

  3. записать команду в CMD;

  4. дождаться завершения через axi_wait_tip;

  5. вернуть статус или ошибку.

Упрощенный вид:

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;
};

Где:

  1. addr - 7-битный адрес slave;

  2. flags - направление и дополнительные признаки;

  3. len - длина буфера;

  4. buf - данные для записи или место для чтения.

Функция axi_xfer_one берет одно такое сообщение и превращает его в последовательность команд для AXI IP. У нее есть дополнительные флаги:

  1. first - это первое сообщение в группе;

  2. last - это последнее сообщение в группе.

Они нужны, чтобы корректно расставить START и STOP. I2C допускает combined transactions. Например, многие устройства читаются так:

  1. START

  2. slave address + write register address

  3. REPEATED START

  4. slave address + read data

  5. 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

Типовые причины:

  1. OLED не запитан;

  2. адрес не 0x3c, а 0x3d;

  3. нет pull-up на SDA/SCL;

  4. ошибка в XDC;

  5. bitstream не тот;

  6. IP-блок не подключен к нужным пинам;

  7. на плате обрыв или неверная распиновка.

Запись данных

Если сообщение не содержит I2C_M_RD, это write. Для каждого байта:

  1. положить байт в TX_DATA;

  2. сформировать CMD_WR;

  3. если это последний байт последнего сообщения, добавить CMD_STO;

  4. отправить команду;

  5. дождаться TIP = 0;

  6. проверить 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. Для каждого байта:

  1. сформировать CMD_RD;

  2. если это последний байт, добавить CMD_NACK;

  3. если это последний байт последнего сообщения, добавить CMD_STO;

  4. отправить команду;

  5. дождаться TIP = 0;

  6. прочитать 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-адреса и не передает данные. Его задача намного проще:

  1. понять, что прерывание действительно от нашего IP;

  2. сбросить флаги ISR;

  3. разбудить поток, который ждет завершения команды.

Если ISR = 0, обработчик возвращает:IRQ_NONE Это означает, что прерывание не принадлежит этому устройству или флагов нет. Если флаги есть, они сбрасываются записью тех же битов в ISR. Это W1C-логика: write 1 to clear

После этого вызывается:

complete(&i->cmd_done);

Поток, который спит в axi_wait_tip, просыпается и читает STATUS. Важно: IRQ handler не заменяет проверку STATUS. Он только сообщает, что событие произошло. После пробуждения драйвер все равно должен прочитать STATUS и проверить:

  1. TIP;

  2. RXACK;

  3. AL;

  4. 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, нужно проверять:

  1. input-clock-frequency в DTS;

  2. clock-frequency в DTS;

  3. FCLK0 в Vivado;

  4. получение clock через clock framework;

  5. порядок применения DTB на SD-карте.

probe: главный вход драйвера

probe() - центральная функция platform-драйвера. Она вызывается, когда ядро нашло устройство из Device Tree, совместимое с этим драйвером. Условие вызова:

  1. в DTB есть узел compatible = “user,i2c-master-axi-1.0”;

  2. модуль i2c-master-axi загружен;

  3. of_match_table драйвера содержит ту же строку.

Упрощенный порядок probe():

  1. Выделить struct i2c_master_axi.

  2. Сохранить указатель на struct device.

  3. Инициализировать completion.

  4. Получить MMIO-регистры через ioremap.

  5. Получить clock.

  6. Прочитать input-clock-frequency и clock-frequency.

  7. Попробовать получить IRQ.

  8. Если IRQ есть, зарегистрировать обработчик.

  9. Выполнить hw_init.

  10. Заполнить struct i2c_adapter.

  11. Привязать adapter data.

  12. Зарегистрировать адаптер через i2c_add_adapter.

  13. Вернуть 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>;

Если здесь ошибка, драйвер дальше не стартует. Типовые причины:

  1. в DTB нет нужного узла;

  2. адрес в DTS неверный;

  3. размер окна некорректный;

  4. ресурс конфликтует; загружен не тот 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. Обычно задаются:

  1. owner;

  2. algo;

  3. quirks;

  4. dev.parent;

  5. dev.of_node;

  6. 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 происходит следующее:

  1. модуль загружается;

  2. platform_driver регистрируется в ядре;

  3. ядро сравнивает DT-узлы с of_match_table;

  4. для i2c@43c00000 вызывается probe();

  5. probe регистрирует i2c_adapter;

  6. дочерний oled@3c становится доступен I2C core.

Если modprobe прошел успешно, это еще не значит, что probe() был вызван. modprobe только загружает модуль. Для вызова probe() нужно совпадение с Device Tree.

Поэтому при отладке нужно различать:

  1. модуль загружен;

  2. драйвер зарегистрирован;

  3. probe вызван;

  4. MMIO успешно отображен;

  5. адаптер I2C зарегистрирован;

  6. 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

Если строки нет, нужно проверять:

  1. compatible в DTS;

  2. of_match_table в драйвере;

  3. загружен ли нужный DTB;

  4. есть ли узел i2c@43c00000 в /proc/device-tree;

  5. загружен ли модуль.

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 не появился, нужно проверять:

  1. CONFIG_FB_SSD1307;

  2. compatible OLED в DTS;

  3. параметры solomon,width/height;

  4. логи ssd1307fb;

  5. наличие дочернего узла oled@3c в загруженном DTB.

Типовые ошибки и их смысл

Симптом

Вероятная причина

Где искать

modprobe успешен, но dmesg молчит

Не совпал compatible, не тот DTB или нет DT-узла

DTS, DTB на FAT, of_match_table

probe есть, но I2C-шины нет

Ошибка до i2c_add_adapter()

MMIO, clock, hw_init, PRESCALE

prescale не 124

Неверная входная частота или SCL

input-clock-frequency, clock-frequency, Vivado FCLK0

irq=polled

IRQ не подключен или не зарегистрирован

DTS interrupts, Vivado IRQ_F2P, GIC

-ETIMEDOUT

TIP не сбросился

bitstream, clock, reset, MMIO address

-ENXIO

NACK на адресном байте

нет устройства на 0x3c, питание OLED, пины

-EIO

NACK на байте данных

slave ответил на адрес, но отверг данные

-EAGAIN

BUSY или AL

зависшая шина, arbitration lost

i2cdetect показывает -- на 0x3c

OLED не отвечает

питание, SA0, XDC, bitstream, pull-up

i2cdetect показывает UU

Адрес занят драйвером

Обычно нормально, если поднялся ssd1307fb

Итоговая логика драйвера

Если свернуть весь модуль до основной идеи, получается такая последовательность.

При загрузке:

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"

Ожидаемо это файл примерно на несколько сотен строк. Важно проверить несколько вещей:

  1. Вверху файла есть SPDX-License-Identifier.

  2. Строка compatible совпадает с DTS: user,i2c-master-axi-1.0

  3. Карта регистров соответствует RTL IP-блока.

  4. Формула PRESCALE соответствует реализации IP.

  5. Драйвер поддерживает режим polling, если IRQ пока не подключен.

  6. В 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 сделает следующее:

  1. Соберет Linux kernel.

  2. Возьмет каталог package/i2c-master-axi.

  3. Запустит сборку модуля через дерево ядра.

  4. Получит i2c-master-axi.ko.

  5. Установит модуль в target/lib/modules//.

  6. Упакует его в 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 ожидается одно из двух:

  1. 3c - устройство отвечает и свободно;

  2. UU - адрес уже занят kernel driver, например ssd1307fb.

Если драйвер i2c-master-axi успешно загрузился, но 0x3c пустой, проблема чаще находится ниже уровня Linux-драйвера: bitstream, XDC, пины, питание OLED, pull-up на I2C, адрес SA0 или физическое подключение дисплея. В исходном фрагменте это также выделено как типовая граница диагностики: при живом probe и пустом 3c следует проверять PL, проводку и bitstream, а не C-код драйвера.

Типовые ошибки на этом шаге

Симптом

Вероятная причина

Что проверить

modprobe проходит, но шины нет

Не совпал compatible

DTS и of_match_table

В dmesg нет строки i2c-master-axi

Модуль не загружен или нет DT-узла

lsmod, /proc/device-tree, имя DTB на FAT

prescale не 124

Неверная входная частота или clock-frequency

DTS, Vivado FCLK0

invalid module format

Модуль собран для другого ядра

uname -r, modinfo vermagic

i2cdetect не видит 0x3c

OLED не отвечает

питание, пины, XDC, bitstream, адрес SA0

UU вместо 3c

Адрес занят драйвером

Это нормально, если привязался ssd1307fb

Таймаут TIP

IP не отвечает или завис

reg, bitstream, clock, reset IP

-ENXIO

NACK на адресном байте

нет устройства на I2C-адресе

-EIO

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

Если модуль не появился, стоит проверить четыре точки:

  1. В defconfig есть BR2_PACKAGE_I2C_MASTER_AXI=y.

  2. package/i2c-master-axi/Config.in подключен из верхнеуровневого Config.in.

  3. package/i2c-master-axi/i2c-master-axi.mk подключается через external.mk.

  4. В каталоге пакета есть Makefile с obj-m += i2c-master-axi.o.

Типовые ошибки на этом шаге:

Симптом

Вероятная причина

Опции нет в menuconfig

Не подключен package/i2c-master-axi/Config.in

Опция есть, но пакет не собирается

Не подключен i2c-master-axi.mk через external.mk

Ошибка kbuild No rule to make target

Имя в obj-m не совпадает с именем .c файла

.ko не попал в rootfs

Пакет не включен через BR2_PACKAGE_I2C_MASTER_AXI=y

invalid module format на плате

Модуль собран не для той версии ядра, которое загружено

На этом пакет 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 должен получить одну итоговую конфигурацию, которая скажет:

  1. какую архитектуру собирать;

  2. какой toolchain использовать;

  3. какое ядро Linux взять;

  4. какой defconfig ядра применить;

  5. какой linux.fragment наложить поверх;

  6. какой DTS собрать;

  7. какой U-Boot собрать;

  8. какой uboot.fragment применить;

  9. какие пакеты включить в rootfs;

  10. какой out-of-tree модуль собрать;

  11. какие post-build и post-image скрипты вызвать;

  12. какие образы 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 эта опция имеет строковый тип.

Итог этого блока:

  1. Buildroot соберет uImage;

  2. Buildroot соберет zynq-mini-revb.dtb;

  3. оба файла попадут в $BR_OUT/images;

  4. 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 мы задавали:

  1. CONFIG_OF_EMBED=y;

  2. CONFIG_BOOTCOMMAND;

  3. поддержку FAT;

  4. поддержку ext4;

  5. поддержку bootm;

  6. поддержку MMC;

  7. приглашение "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
    другие файлы сборки

Этого уже достаточно для анализа, но еще недостаточно для удобного запуска платы.

Нам нужно дополнительно решить несколько задач:

  1. Добавить автозагрузку модуля i2c-master-axi.ko в rootfs.

  2. Настроить BusyBox init так, чтобы он реально загружал модули из /etc/modules-load.d.

  3. Добавить login-консоль на tty1 для framebuffer/OLED-сценария.

  4. При необходимости зафиксировать MAC-адрес eth0.

  5. Подготовить uEnv.txt для U-Boot.

  6. Описать разметку SD-карты через genimage.

  7. Собрать итоговый sdcard.img с FAT-разделом и rootfs-разделом.

  8. Учесть, что настоящий BOOT.BIN Buildroot сам не создает.

Для таких действий в Buildroot есть два механизма:

  1. post-build script - вызывается после подготовки target rootfs, но до упаковки rootfs-образа;

  2. 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 будет делать следующее:

  1. Создавать /etc/modules-load.d/i2c-master-axi.conf.

  2. Создавать init-скрипт /etc/init.d/S03modules.

  3. Добавлять getty на tty1.

  4. Создавать /etc/eth0-mac.

  5. Создавать init-скрипт /etc/init.d/S39set-eth0-mac.

  6. Добавлять helper-скрипт /usr/local/bin/oled-console.

  7. Создавать init-скрипт /etc/init.d/S45oled-console.

  8. Добавлять удобный /etc/profile.d/zynq-mini.sh.

  9. При необходимости обновлять /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-консоли. Практически это имеет смысл, если:

  1. USB host работает.

  2. Подключена USB-клавиатура.

  3. /dev/fb0 создан драйвером ssd1307fb.

  4. 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 в трех местах:

  1. Device Tree: local-mac-address = [00 0a 35 01 02 03];

  2. U-Boot uEnv.txt: ethaddr=00:0a:35:01:02:03

  3. 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-скриптов получается такой:

  1. S03modules загружает i2c-master-axi

  2. S39set-eth0-mac задает MAC до DHCP

  3. S40network поднимает сеть

  4. 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

Этот файл описывает три образа:

  1. boot.vfat FAT32-раздел с загрузочными файлами;

  2. rootfs.ext4 ext4-раздел с корневой файловой системой;

  3. 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. Параметры:

Параметр

Смысл

console=ttyPS0,115200

Основная консоль ядра через UART

earlycon

Ранний вывод до полной инициализации UART

fbcon=font:MINI4x6

Малый шрифт framebuffer-консоли

root=/dev/mmcblk0p2

rootfs на втором разделе SD

rootwait

Ждать появления SD-устройства

rw

Монтировать 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:

  1. Пересканировать SD.

  2. Загрузить uImage с FAT в DDR.

  3. Загрузить zynq-mini-revb.dtb с FAT в DDR.

  4. Запустить bootm с ядром и DTB.

Средний аргумент - в bootm означает, что initrd не используется:

bootm <kernel> - <dtb>

Важно не путать два разных DTB:

  1. DTB для U-Boot встроен в U-Boot через CONFIG_OF_EMBED;

  2. DTB для Linux лежит на FAT как zynq-mini-revb.dtb и передается в bootm.

post-image.sh: сборка sdcard.img

post-image.sh вызывается после того, как Buildroot собрал основные бинарные файлы в BINARIES_DIR, обычно это:$BR_OUT/images

Скрипт должен:

  1. Скопировать uEnv.txt в BINARIES_DIR.

  2. Проверить наличие BOOT.BIN.

  3. Если BOOT.BIN отсутствует, временно создать placeholder.

  4. Запустить genimage.

  5. Получить 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 уже с подготовленным окружением. Важные переменные:

Переменная

Смысл

BINARIES_DIR

Каталог с готовыми бинарными артефактами, обычно $BR_OUT/images

TARGET_DIR

Каталог target rootfs, обычно $BR_OUT/target

BUILD_DIR

Каталог временных сборок, обычно $BR_OUT/build

Скрипт вычисляет каталог платы:

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 не подключен, то:

  1. i2c-master-axi.ko может попасть в rootfs;

  2. но не будет /etc/modules-load.d/i2c-master-axi.conf;

  3. не будет S03modules;

  4. модуль не загрузится автоматически;

  5. probe i2c-master-axi не вызовется при boot.

Если post-image.sh не подключен, то:

  1. Buildroot соберет uImage, DTB, U-Boot и rootfs;

  2. но не будет итогового sdcard.img;

  3. uEnv.txt не попадет на FAT автоматически;

  4. 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"

Ожидаемо:

  1. /etc/modules-load.d/i2c-master-axi.conf

  2. содержит i2c-master-axi;

  3. S03modules исполняемый;

  4. S39set-eth0-mac исполняемый;

  5. S45oled-console исполняемый;

  6. oled-console исполняемый;

  7. 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"

Ожидаемо два раздела:

  1. FAT32, тип 0x0C, bootable

  2. 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 должны быть:

  1. BOOT.BIN

  2. uImage

  3. zynq-mini-revb.dtb

  4. 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. Достаточно:

  1. Смонтировать первый FAT-раздел SD-карты.

  2. Скопировать туда новый BOOT.BIN.

  3. Выполнить 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 должен остаться тем же.

Типичные ошибки

Симптом

Вероятная причина

Где проверять

sdcard.img не появился

Не подключен post-image.sh или ошибка genimage

BR2_ROOTFS_POST_IMAGE_SCRIPT, лог сборки

genimage пишет, что нет uEnv.txt

post-image.sh не копирует файл

$BR_EXT/board/zynq_mini_revb/uEnv.txt, BINARIES_DIR

genimage пишет, что нет BOOT.BIN

Нет placeholder и нет настоящего BOOT.BIN

post-image.sh, $BR_OUT/images/BOOT.BIN

Плата молчит до UART

На FAT placeholder BOOT.BIN или неверный FAT

настоящий BOOT.BIN, FAT32, MBR

Модуль есть, но не загружается автоматически

BusyBox init не читает modules-load.d без скрипта

S03modules, права chmod +x

modprobe i2c-master-axi не найден

Модуль не попал в rootfs

BR2_PACKAGE_I2C_MASTER_AXI=y, пакет Buildroot

probe не вызывается

Не совпал compatible или не тот DTB

DTS, DTB на FAT, of_match_table

/dev/fb0 не появляется

OLED driver не привязался

CONFIG_FB_SSD1307, DTS oled@3c, I2C

tty1 не дает login

Нет строки inittab или fbcon не активен

/etc/inittab, /dev/fb0, S45oled-console

MAC меняется после reboot

Не сработал S39set-eth0-mac или DHCP стартует раньше

порядок init, /etc/eth0-mac

root=/dev/mmcblk0p2 не монтируется

Неверная разметка SD

genimage.cfg, fdisk -l sdcard.img

U-Boot игнорирует uEnv.txt

Файл не на FAT или bootcmd не импортирует env

FAT contents, CONFIG_BOOTCOMMAND

Что получилось на этом шаге

После этой главы у нас появляется слой автоматизации поверх обычной сборки 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)

Если пункта нет, проблема обычно в одном из мест:

  1. не подключен package/i2c-master-axi/Config.in;

  2. ошибка в верхнеуровневом Config.in;

  3. не передан BR2_EXTERNAL;

  4. имя переменной 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 будет:

  1. скачивать исходники;

  2. собирать toolchain;

  3. собирать Linux kernel;

  4. собирать U-Boot;

  5. собирать host-утилиты;

  6. собирать target-пакеты;

  7. собирать out-of-tree модуль;

  8. формировать rootfs;

  9. выполнять post-build;

  10. выполнять post-image;

  11. собирать 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 обычно находятся три компонента:

  1. FSBL

  2. bitstream PL

  3. 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

fsbl.elf

Vitis, проект FSBL из XSA

Первый загрузчик, который запускает BootROM

Bitstream

zynq_mini_oled_top.bit

Vivado, результат Generate Bitstream

Конфигурация PL, включая AXI I2C IP

U-Boot ELF

$BR_OUT/images/u-boot.elf или $BR_OUT/images/u-boot

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
}

Смысл строк:

  1. [bootloader] fsbl.elf - первая partition, которую BootROM считает загрузчиком и копирует в OCM;

  2. design.bit - bitstream, который FSBL загрузит в PL через PCAP;

  3. 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

Разбор параметров:

Параметр

Смысл

-arch zynq

Целевая архитектура Zynq-7000

-image "$WORK/boot/boot_linux.bif"

Входной BIF-манифест

-o "$WORK/boot/BOOT.BIN"

Выходной загрузочный образ

-w on

Перезаписать выходной файл без запроса

Проверим результат:

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 нужно пересобирать не при каждом изменении проекта. Нужно понимать границу ответственности.

Что изменилось

Нужно пересобирать BOOT.BIN?

Почему

FSBL

Да

FSBL входит в BOOT.BIN

XSA / PS7 init / DDR settings

Да

FSBL должен соответствовать новой аппаратной платформе

Bitstream .bit

Да

Bitstream входит в BOOT.BIN

Адрес AXI IP в Vivado

Да

Меняется bitstream и, возможно, DTS

Пины OLED в XDC

Да

Меняется bitstream

U-Boot

Да

U-Boot входит в BOOT.BIN

uboot.fragment

Да

После пересборки U-Boot нужен новый BOOT.BIN

uImage

Нет

U-Boot загружает его отдельно с FAT

zynq-mini-revb.dtb

Нет

U-Boot загружает DTB отдельно с FAT

linux.fragment

Нет, если не менялся U-Boot

Влияет на ядро, не на BOOT.BIN

i2c-master-axi.ko

Нет

Модуль лежит в rootfs

rootfs

Нет

rootfs лежит на ext4-разделе

uEnv.txt

Нет

Лежит отдельно на 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, содержит BOOT.BIN

BOOT.BIN

Настоящий bootgen output, не placeholder

uImage

Лежит на FAT

zynq-mini-revb.dtb

Лежит на FAT

uEnv.txt

Лежит на 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

Симптом

Вероятная причина

Что делать

bootgen: command not found

Не подключено окружение Vitis

Выполнить source settings64.sh

bootgen пишет, что файл не найден

Неверный путь в .bif

Проверить ls -lh всех входов

BOOT.BIN получился текстовым

Используется placeholder

Пересобрать через bootgen

Плата молчит на UART

BootROM не нашел или не запустил BOOT.BIN

Проверить FAT32, boot mode, настоящий BOOT.BIN

FSBL стартует, но дальше зависание

Неверный bitstream или FSBL не от этого XSA

Пересобрать FSBL и bitstream из одной аппаратной платформы

U-Boot стартует, но Linux нет

Нет uImage, DTB или ошибка uEnv.txt

Проверить FAT-раздел и U-Boot env

Linux стартует, но I2C IP не работает

Bitstream не содержит нужный IP или адрес не совпадает

Проверить Vivado Address Editor, DTS reg, .bit в BIF

После изменения DTS ничего не поменялось

На FAT старый DTB

Пересобрать Linux/DTB и заменить zynq-mini-revb.dtb

После изменения U-Boot ничего не поменялось

В BOOT.BIN старый U-Boot

Пересобрать U-Boot и заново выполнить bootgen

После изменения bitstream PL поведение старое

В BOOT.BIN старый .bit

Проверить BIF и заново выполнить bootgen

Итоговая цепочка загрузки

После успешной сборки 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

В BOOT.BIN включен актуальный .bit из нужного Vivado-проекта

Для 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 нашел и запустил BOOT.BIN

Появился U-Boot banner

FSBL успешно передал управление U-Boot

Видно fatload

U-Boot читает FAT-раздел

Видно Starting kernel ...

U-Boot передал управление Linux

Видны строки Linux kernel

Ядро стартовало

Появился login prompt

rootfs смонтирован, init работает

Если на UART полная тишина, это почти всегда проблема до Linux:

  1. Неверный boot mode;

  2. плата не питается;

  3. UART подключен не к тому порту;

  4. неправильная скорость UART;

  5. FAT-раздел не читается BootROM;

  6. BOOT.BIN отсутствует;

  7. BOOT.BIN является placeholder;

  8. BOOT.BIN собран не для этой платы;

  9. 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 не монтируется, типичные причины:

  1. в genimage второй раздел не ext4;

  2. root=/dev/mmcblk0p2 не соответствует реальной разметке;

  3. SD-карта не успевает появиться и нет rootwait;

  4. rootfs поврежден;

  5. ядро не содержит нужную поддержку 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, проверяйте:

  1. compatible в загруженном DTB;

  2. of_match_table в драйвере;

  3. имя модуля;

  4. наличие узла 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

Расшифровка:

Значение

Смысл

3c

Устройство отвечает и не занято kernel-драйвером

UU

Устройство отвечает, но адрес уже занят драйвером, например ssd1307fb

--

На адресе ответа нет

Если i2cdetect показывает -- на 0x3c, но i2c-master-axi успешно загружен, проблема, скорее всего, ниже уровня Linux:

  1. OLED не запитан;

  2. адрес OLED 0x3d, а не 0x3c;

  3. неверно подключены SDA/SCL;

  4. нет pull-up;

  5. не тот bitstream;

  6. пины в XDC не соответствуют подключению;

  7. IP не подключен к нужным выводам PL;

  8. 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, вероятные причины:

  1. не включен CONFIG_FB_SSD1307;

  2. неверный compatible OLED в DTS;

  3. дочерний узел oled@3c не подхвачен I2C core;

  4. драйвер OLED собран модулем, но не загружен;

  5. ошибка параметров solomon,width/height;

  6. ошибка в последовательности 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 не работает, проверять нужно слоями:

  1. узел &gem0 в DTS;

  2. phy-mode = “rgmii-id”;

  3. MDIO-адрес PHY;

  4. питание PHY;

  5. MAC-адрес;

  6. линк на RJ45;

  7. DHCP;

  8. сетевой кабель.

Что считается успешным результатом главы

После этой проверки у нас должно быть подтверждено:

  1. microSD корректно записана.

  2. BootROM запускает настоящий BOOT.BIN.

  3. FSBL инициализирует PS и загружает bitstream в PL.

  4. U-Boot стартует и видит FAT-раздел.

  5. U-Boot загружает uImage и DTB.

  6. Linux стартует с правильными bootargs.

  7. Rootfs монтируется с /dev/mmcblk0p2.

  8. Загружен правильный DTB с узлом i2c@43c00000.

  9. Модуль i2c-master-axi загружается автоматически.

  10. Драйвер регистрирует I2C-адаптер.

  11. OLED виден на I2C-адресе 0x3c или занят драйвером как UU.

  12. Создан framebuffer /dev/fb0.

  13. При необходимости поднят 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% при первом пополнении.

Комментарии (0)