В прошлой статье мы собрали готовый образ Linux и запустили I2C Master Controller который живет в ПЛИС и управляется драйвером предоставляющим в ОС доступ к нему как типовому I2C-контроллеру. В этой, заключительной части из общей серии статей, я хотел бы филигранно отшлифовать все мелкие недочеты и привести примеры утилит, которые могли бы отправлять нужные нам данные на I2C OLED-дисплей SSD1306, например температуру кристалла, время или что-нибудь еще.
Всем кому интересна тема - го под кат! :)

Важно! Перед началом повествования, хотелось бы заранее оговориться, что основная цель, которую я преследую при написании этой статьи - рассказать о своем опыте, с чего можно начать, при изучении отладочных плат на базе Zynq. Я не являюсь профессиональным разработчиком под ПЛИС и SoC Zynq, не являюсь системным программистом под Linux и могу допускать какие-либо ошибки в использовании терминологии, использовать не самые оптимальные пути решения задач, etc. Но отмечу, что любая конструктивная и аргументированная критика только приветствуется.
Вступление
В прошлых статьях мы собрали первый работающий вариант проекта: подготовили аппаратную часть в Vivado, добавили в PL собственный I2C Master Controller, подключили его к процессорной системе Zynq через AXI и запустили Linux, в котором этот IP-блок стал виден как обычный I2C-контроллер.
Мне не хотелось писать отдельный узкоспециализированный драйвер только под OLED-дисплей, а пришла идея сделать кажущуюся более правильной интеграцию. IP-ядро в ПЛИС должно выглядеть для Linux как стандартный I2C master adapter. После этого все, что находится на этой I2C-шине, может обслуживаться уже обычными механизмами ядра: через Device Tree, I2C subsystem и существующие драйверы устройств.
В нашем случае на эту шину подключен OLED-дисплей SSD1306 с адресом 0x3C. Подробное отражение архитектуры проекта я составил в облюбованном мной Mermaid следующим образом:

И в финальной точке я хотел получить не просто факт обмена байтами по I2C, а нормальное Linux-устройство /dev/fb0, на которое можно выводить текст, часы, диагностические паттерны, температуру кристалла, uptime или любую другую небольшую статусную информацию.
На первый взгляд после прошлой статьи оставалось сделать совсем немного: написать маленькую userspace-утилиту, открыть framebuffer, нарисовать пару строк текста и вывести их на OLED. Но при переходе от "первого запуска" к более аккуратному и воспроизводимому состоянию обнаружилось несколько мелких, но важных недочетов.
Загрузчик стартовал, UART-консоль работала, SD-карта определялась, но в логе были строки которые надо было шлифануть:
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.
Во-первых. Физически используется не ZC706, а ZYNQ MINI Rev B. Это означает, что U-Boot был собран или сконфигурирован с чужим описанием платы. Потом ошибка Ethernet PHY:
Could not get PHY for eth0: addr 7 No ethernet found.
Сама по себе эта ошибка не мешает вывести картинку на OLED. Но она показывает более важную проблему: загрузчик и, возможно, дальнейшая Linux-конфигурация не полностью соответствуют реальному железу. Если оставить это как есть, часть периферии может работать случайно, часть не будет работать вообще, а часть будет выглядеть рабочей до первого более сложного сценария.
Потом не заработала USB-клавиатура, вероятно из-за того, что чего-то не хватило в описании DTS.
Поэтому в этой заключительной части я хочу не просто "дорисовать часы на OLED из userspace", а привести проект в более аккуратное состояние:
причесать DTS под мою ZYNQ MINI Rev B, чтобы работал Ethernet, USB;
убрать все ошибки при инициализации железа на этапе загрузки U-Boot;
разобрать init-скрипты которые выполняют всю работу чтобы плата стартовала в состоянии которое нам необходимо;
понять, в какой момент появляются модули, I2C-шина и /dev/fb0;
добавить userspace-утилиты для вывода кастомных данных на SSD1306;
сделать переключение между режимом Linux console и режимом небольшого статусного дисплея.
Поэтому статья начнется не с OLED и Linux, а начать придется с исправления косяков. Начнем с того места, где проблема стала видна первой: с лога U-Boot и неверного определения платы.
Исправляем ошибки в U-Boot
U-Boot показал, что проект формально загружается, но описание платы не соответствует реальному железу. На первый взгляд приведенный выше лог не выглядит полностью аварийным. Загрузчик стартовал, процессор определился, DDR инициализирована, UART работает, SD-карта видна. Но в нем есть несколько строк, которые нельзя игнорировать.
Первая строка, которая меня сразу смутила: Model: Xilinx ZC706 board и сразу стало понятно, что U-Boot использует описание другой платы.
В строке Core: 29 devices, 19 uclasses, devicetree: embed важна последняя часть devicetree: embed.
Это означает, что U-Boot использует control Device Tree, встроенный непосредственно в образ U-Boot. То есть описание платы не загружается отдельным файлом с SD-карты и не передается загрузчиком предыдущей стадии. Оно уже находится внутри самого U-Boot и собирается вместе с ним.
И исходя из этого отсюда вытекают все остальные ошибки связанные с некорректной периферией. Что ж, сделаем свой кастомный DTS для U-Boot и исправим все.
Готовим каталог для U-Boot DTS:
export WORK=/home/megalloid/devel/xilinx/projects/zynq_mini_oled/linux export BR_SRC="$WORK/buildroot-2024.02.7" export BR_OUT="$WORK/br-output" export BR_EXT="$WORK/board-support" mkdir -p "$BR_EXT/uboot-dts"
Файл назовем так:
$BR_EXT/uboot-dts/zynq-mini-revb-u-boot.dts
Суффикс u-boot здесь нужен только для ясности. Он показывает, что этот DTS предназначен для control DTB самого U-Boot, а не для Linux kernel.
На первом шаге не нужно описывать все железо платы. Для исправления загрузчика достаточно описать то, что нужно самому U-Boot:
модель платы;
memory node;
UART1 для консоли;
SDIO0 для загрузки с microSD;
QSPI flash W25Q128;
GEM0 и Ethernet PHY;
aliases;
stdout-path.
Создадим файл:
cat > "$BR_EXT/uboot-dts/zynq-mini-revb-u-boot.dts" << 'EOF' /dts-v1/; #include "zynq-7000.dtsi" / { model = "ZYNQ MINI Rev B"; compatible = "custom,zynq-mini-revb", "xlnx,zynq-7000"; aliases { serial0 = &uart1; mmc0 = &sdhci0; ethernet0 = &gem0; spi0 = &qspi; }; chosen { stdout-path = "serial0:115200n8"; }; memory@0 { device_type = "memory"; reg = <0x0 0x20000000>; }; }; &uart1 { status = "okay"; bootph-all; }; &sdhci0 { status = "okay"; bus-width = <4>; broken-cd; disable-wp; bootph-all; }; &qspi { status = "okay"; is-dual = <0>; num-cs = <1>; bootph-all; flash@0 { compatible = "winbond,w25q128", "jedec,spi-nor"; reg = <0>; spi-max-frequency = <50000000>; }; }; &gem0 { status = "okay"; phy-mode = "rgmii-id"; phy-handle = <ðernet_phy>; ethernet_phy: ethernet-phy@0 { reg = <1>; device_type = "ethernet-phy"; }; }; EOF
Во-первых я изменил MDIO-адрес PHY на реальный с 0 на 1:
ethernet_phy: ethernet-phy@0 { reg = <1>; };
После сборки U-Boot не увидел PHY и этот адрес нужно заменить просмотрев как подключены strap-пины RTL8211E.
Далее надо поправить memory node.
memory@0 { device_type = "memory"; reg = <0x0 0x20000000>; };
Значение 0x20000000 соответствует 512 MiB: 0x20000000 = 536870912 bytes = 512 MiB
Это важная правка, потому что в текущем логе U-Boot было:
DRAM: ECC disabled 1 GiB
Если фактическая память платы около 512 MiB, а U-Boot считает, что доступен 1 GiB, система может работать нестабильно при обращении выше физически установленного объема памяти. Поэтому на первом исправленном варианте безопаснее явно задать 512 MiB.
Теперь нужно сказать U-Boot, что его default device tree - это не ZC706, а наш файл.
В Buildroot для этого удобно использовать два механизма:
BR2_TARGET_UBOOT_CUSTOM_DTS_PATH- передать U-Boot внешний DTS.uboot.fragment- зафиксировать Kconfig-опции U-Boot.
Сначала добавим путь к custom DTS в Buildroot defconfig.
Откроем файл:
vim $BR_EXT/configs/zynq_mini_revb_defconfig
В секцию U-Boot добавим строку:
BR2_TARGET_UBOOT_CUSTOM_DTS_PATH="$(BR2_EXTERNAL_ZYNQ_MINI_I2C_PATH)/uboot-dts/zynq-mini-revb-u-boot.dts"
Итоговый блок 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_CUSTOM_DTS_PATH="$(BR2_EXTERNAL_ZYNQ_MINI_I2C_PATH)/uboot-dts/zynq-mini-revb-u-boot.dts" 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
Если в используемой версии Buildroot опции BR2_TARGET_UBOOT_CUSTOM_DTS_PATH нет, есть запасной вариант: положить DTS в дерево сборки U-Boot через patch или post-patch hook. Но для Buildroot 2024.02 нормальный путь - использовать custom DTS path.
Теперь поправим файл дописав в конце следующие инструкции, включив необходимые диагностические утилиты и драйверы для периферии:
# --- Device Tree control ------------------------------------------------ # При SD-boot legacy-FSBL прыгает в U-Boot, но НЕ устанавливает r2 на DTB. # По умолчанию U-Boot может быть собран с CONFIG_OF_BOARD=y и ждать DTB # от board-specific кода или от предыдущего загрузчика. В нашем SD-сценарии # FSBL не передает такой DTB, поэтому U-Boot должен использовать DTB, # встроенный прямо в u-boot.elf. CONFIG_OF_CONTROL=y # CONFIG_OF_SEPARATE is not set # CONFIG_OF_BOARD is not set CONFIG_DEFAULT_DEVICE_TREE="zynq-mini-revb-u-boot" # Файловые системы и команды для SD-загрузки. CONFIG_FS_FAT=y CONFIG_CMD_FAT=y CONFIG_FAT_WRITE=y CONFIG_FS_EXT4=y CONFIG_CMD_EXT2=y CONFIG_CMD_EXT4=y CONFIG_LEGACY_IMAGE_FORMAT=y CONFIG_CMD_BOOTM=y CONFIG_CMD_MMC=y CONFIG_SYS_PROMPT="zynq-mini> " # --- Boot image and filesystems ----------------------------------------- # Файловые системы и команды для SD-загрузки. CONFIG_LEGACY_IMAGE_FORMAT=y CONFIG_CMD_BOOTM=y CONFIG_FS_FAT=y CONFIG_CMD_FAT=y CONFIG_FAT_WRITE=y CONFIG_FS_EXT4=y CONFIG_CMD_EXT2=y CONFIG_CMD_EXT4=y # --- MMC / SD0 boot ----------------------------------------------------- # SD0 на Zynq PS используется как загрузочный носитель. CONFIG_CMD_MMC=y CONFIG_MMC=y CONFIG_MMC_SDHCI=y CONFIG_MMC_SDHCI_ZYNQ=y # --- SPI / QSPI / SPI NOR flash ----------------------------------------- # На плате установлена Winbond W25Q128, 128 Mbit = 16 MiB. # Проверяется командой sf probe. CONFIG_SPI=y CONFIG_DM_SPI=y CONFIG_ZYNQ_QSPI=y CONFIG_SPI_FLASH=y CONFIG_SPI_FLASH_WINBOND=y CONFIG_CMD_SPI=y CONFIG_CMD_SF=y # --- MTD diagnostics for SPI NOR ---------------------------------------- # Нужны для дополнительной диагностики SPI NOR через mtd list. CONFIG_MTD=y CONFIG_CMD_MTD=y # --- Ethernet core ------------------------------------------------------ # Базовая сеть U-Boot и Driver Model Ethernet. CONFIG_NET=y CONFIG_CMD_NET=y CONFIG_NETDEVICES=y CONFIG_DM_ETH=y # --- Zynq GEM Ethernet and PHY ------------------------------------------ # GEM0 в PS7 + внешний Realtek RTL8211E PHY. CONFIG_ZYNQ_GEM=y CONFIG_PHYLIB=y CONFIG_PHY_REALTEK=y CONFIG_MII=y # --- Ethernet diagnostic commands --------------------------------------- # Нужны для диагностики MDIO/MII: # mdio list # mii device # mii info CONFIG_CMD_MII=y CONFIG_CMD_MDIO=y CONFIG_CMD_PING=y CONFIG_CMD_DHCP=y # --- Driver Model diagnostics ------------------------------------------- # Нужен для: # dm tree # dm uclass eth # dm uclass mdio # dm uclass spi # dm uclass mtd CONFIG_CMD_DM=y
Ключевые строки здесь:
CONFIG_OF_CONTROL=y CONFIG_OF_EMBED=y # CONFIG_OF_BOARD is not set CONFIG_DEFAULT_DEVICE_TREE="zynq-mini-revb-u-boot"
Их смысл:
CONFIG_OF_CONTROL=y- U-Boot использует Device Tree для описания своих устройствCONFIG_OF_EMBED=y- control DTB встраивается внутрь U-Boot# CONFIG_OF_BOARD is not set- U-Boot не ждет DTB от board code или предыдущего загрузчикаCONFIG_DEFAULT_DEVICE_TREE=...- выбирает наш DTS как основной
Строка # CONFIG_OF_BOARD is not setособенно важна для этого проекта. FSBL на Zynq в нашей схеме не передает U-Boot указатель на Device Tree через регистр CPU. Если оставить режим, при котором U-Boot ожидает внешний DTB от предыдущей стадии, можно снова получить зависание или странное поведение до нормального вывода баннера U-Boot.
Перед полной сборкой нужно применить defconfig и проверить, что Buildroot действительно увидел наши настройки.
cd "$BR_SRC" make BR2_EXTERNAL="$BR_EXT" O="$BR_OUT" zynq_mini_revb_defconfig
Проверяем строки в Buildroot .config:
grep -E 'UBOOT|CUSTOM_DTS|CONFIG_FRAGMENT' "$BR_OUT/.config"
Ожидаем увидеть примерно:
BR2_TARGET_UBOOT=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_CUSTOM_DTS_PATH="$(BR2_EXTERNAL_ZYNQ_MINI_I2C_PATH)/uboot-dts/zynq-mini-revb-u-boot.dts"
Далее можно открыть меню U-Boot:
make BR2_EXTERNAL="$BR_EXT" O="$BR_OUT" uboot-menuconfig
В меню нужно проверить:
Device Tree Control [*] Run-time configuration via Device Tree [*] Provider of DTB for DT control: Embedded DTB Default Device Tree for DT control: zynq-mini-revb-u-boot
Если каких-то опций из uboot.fragment нет или они называются иначе, это нормально для U-Boot: имена Kconfig-опций иногда меняются между версиями. В таком случае нужно найти актуальное имя через поиск в uboot-menuconfig.
Если U-Boot уже был собран раньше, лучше очистить именно его сборочный каталог:
make BR2_EXTERNAL="$BR_EXT" O="$BR_OUT" uboot-dirclean
Затем собрать U-Boot заново:
make BR2_EXTERNAL="$BR_EXT" O="$BR_OUT" uboot-rebuild
Или выполнить общую сборку:
make BR2_EXTERNAL="$BR_EXT" O="$BR_OUT" -j"$(nproc)"
После сборки должны появиться артефакты:
ls -l "$BR_OUT/images"/u-boot*
Ожидаемые файлы: u-boot.bin, u-boot.img, u-boot
Для Zynq boot flow нам в первую очередь нужен:
$BR_OUT/images/u-boot
Именно u-boot.elf будет включен в итоговый BOOT.BIN. Проверяем, что U-Boot собрался с нашим DTB.
grep -E 'CONFIG_OF_CONTROL|CONFIG_OF_EMBED|CONFIG_OF_BOARD|CONFIG_DEFAULT_DEVICE_TREE' "$BR_OUT/build/uboot-"*/.config
Ожидаемый результат:
CONFIG_OF_CONTROL=y CONFIG_OF_EMBED=y # CONFIG_OF_BOARD is not set CONFIG_DEFAULT_DEVICE_TREE="zynq-mini-revb-u-boot"
Потом проверим строки внутри u-boot.elf:
strings "$BR_OUT/images/u-boot" | grep -E 'ZYNQ MINI|ZC706|zynq-mini'
Хороший признак: ZYNQ MINI Rev B, а плохой признак: Xilinx ZC706 board
Если strings все еще показывает Xilinx ZC706 board, значит U-Boot по-прежнему собран с чужим DTS. В таком случае нужно проверить:
grep BR2_TARGET_UBOOT_CUSTOM_DTS_PATH "$BR_OUT/.config" grep CONFIG_DEFAULT_DEVICE_TREE "$BR_OUT/build/uboot-"*/.config find "$BR_OUT/build/uboot-"* -name '*zynq-mini*' -o -name '*zc706*'
И последним шагом пересоберем BOOT.BIN через bootgen. Buildroot собирает u-boot.elf, но полноценный BOOT.BIN для Zynq обычно собирается отдельно, потому что в него также входят FSBL и bitstream из Vivado/Vitis.
У нас должны быть три входных файла:
fsbl.elf- Vitis, собранный из XSAsystem.bitили другой.bit- Vivado implementationu-boot- Buildroot output
Пример переменных:
export VIVADO_PROJ=/home/megalloid/devel/xilinx/projects/zynq_mini_oled/ export BITSTREAM="$VIVADO_PROJ/zynq_mini_oled.runs/impl_1/zynq_mini_oled_top.bit" export FSBL_ELF="$WORK/../vitis/zynq_mini_oled_platform/zynq_fsbl/build/fsbl.elf" export UBOOT_ELF="$BR_OUT/images/u-boot" export BOOT_OUT="$BR_OUT/images/BOOT.BIN"
Пути нужно заменить на фактические для своего проекта. Создаем boot.bif:
cat > "$BR_OUT/images/boot.bif" << EOF the_ROM_image: { [bootloader] $FSBL_ELF $BITSTREAM $UBOOT_ELF } EOF
Подключаем окружение Vitis, чтобы появился bootgen:
source /opt/xilinx/2025.2/Vitis/settings64.sh which bootgen
Собираем BOOT.BIN:
bootgen -arch zynq -image "$BR_OUT/images/boot.bif" -o "$BOOT_OUT" -w
Проверяем файл:
ls -lh "$BOOT_OUT"
Залил на BOOT-раздел SD-карты новый BOOT.BIN и запускаю. Вижу это:
U-Boot 2024.01 (Jun 07 2026 - 01:47:00 +0300) CPU: Zynq 7z020 Silicon: v3.1 Model: ZYNQ MINI Rev B DRAM: ECC disabled 512 MiB Core: 20 devices, 16 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 1 No ethernet found. Hit any key to stop autoboot: 0
Так, плата теперь пишется правильная, объем RAM - тоже. Посмотрим почему Flash 0 Bytes:
zynq-mini> sf probe SF: Detected w25q128 with page size 256 Bytes, erase size 64 KiB, total 16 MiB
Так, QSPI Flash обнаруживается, видимо не надо трактовать эту надпись как неисправность. Для этой сборки U-Boot это, вероятнее всего, вывод legacy flash layer, а не результат sf probe для SPI NOR.
zynq-mini> mtd list List of MTD devices: * nor0 - device: flash@0 - parent: spi@e000d000 - driver: jedec_spi_nor - path: /axi/spi@e000d000/flash@0 - type: NOR flash - block size: 0x10000 bytes - min I/O: 0x1 bytes - 0x000000000000-0x000001000000 : "nor0"
Попробуем для полной уверенности прочитать что-нибудь ?
zynq-mini> mtd read nor0 0x100000 0x0 0x100 Reading 256 byte(s) at offset 0x00000000 zynq-mini> md.b 0x100000 0x100 00100000: fe ff ff ea fe ff ff ea fe ff ff ea fe ff ff ea ................ 00100010: fe ff ff ea fe ff ff ea fe ff ff ea fe ff ff ea ................ 00100020: 66 55 99 aa 58 4e 4c 58 00 00 00 00 00 00 01 01 fU..XNLX........ 00100030: 00 17 00 00 08 00 01 00 00 00 00 00 00 00 00 00 ................ 00100040: 08 00 01 00 01 00 00 00 30 45 17 fc 00 00 00 00 ........0E...... 00100050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00100060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00100070: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00100080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00100090: 00 00 00 00 00 00 00 00 c0 08 00 00 80 0c 00 00 ................ 001000a0: ff ff ff ff 00 00 00 00 ff ff ff ff 00 00 00 00 ................ 001000b0: ff ff ff ff 00 00 00 00 ff ff ff ff 00 00 00 00 ................ 001000c0: ff ff ff ff 00 00 00 00 ff ff ff ff 00 00 00 00 ................ 001000d0: ff ff ff ff 00 00 00 00 ff ff ff ff 00 00 00 00 ................ 001000e0: ff ff ff ff 00 00 00 00 ff ff ff ff 00 00 00 00 ................ 001000f0: ff ff ff ff 00 00 00 00 ff ff ff ff 00 00 00 00 ................
Ну, что-то есть. Если бы нам потребовалась сейчас загрузка с QSPI Flash - нужно было бы знать точные смещения и можно было бы образы скормить как raw:
mtd list mtd read kernel 0x3000000 mtd read dtb 0x2A00000 bootm 0x3000000 - 0x2A00000
Но это так, отступление. Надо разобраться с Ethernet. С ним пришлось повеселиться, пока в ходе дебага я не обнаружил, что тупо не включил шину MDIO в Vivado. После этого пришлось полностью пересобирать весь дизайн от генерации block design, bitstream, генерации платформы в Vitis и пересборки FSBL, который инициализирует PS-ную часть.

Расскажу об этом. Вдруг кому-то пригодится в будущем в реальном дебаге неисправностей.
Изначально в старом логе U-Boot была такая ошибка:
Net: Could not get PHY for eth0: addr 7 No ethernet found.
Адрес 7 был первым первым признаком того, что U-Boot использует чужой DTS тот что от ZC706. После перехода на собственный DTS для ZYNQ MINI Rev B ситуация изменилась:
Net: Could not get PHY for eth0: addr 1 No ethernet found.
Это уже был прогресс. U-Boot перестал искать PHY по адресу 7 и начал использовать адрес 1, который был задан в нашем DTS:
&gem0 { status = "okay"; local-mac-address = [00 0a 35 01 02 03]; phy-mode = "rgmii-id"; phy-handle = <ðernet_phy>; ethernet_phy: ethernet-phy@1 { reg = <1>; device_type = "ethernet-phy"; }; };
Но Ethernet по-прежнему не поднимался. Первым шагом - я решил убедиться, что U-Boot загружен с правильным embedded Device Tree.
Для этого в консоли U-Boot были выполнены команды:
fdt addr $fdtcontroladdr fdt print / model fdt print /aliases
Результат:
Working FDT set to 40bfd10 model = "ZYNQ MINI Rev B" aliases { serial0 = "/axi/serial@e0001000"; mmc0 = "/axi/mmc@e0100000"; ethernet0 = "/axi/ethernet@e000b000"; spi0 = "/axi/spi@e000d000"; };
Это подтвердило, что U-Boot использует уже не ZC706, а наш DTS. Одновременно стало понятно, что Ethernet-узел находится не в корне дерева и не в /amba, а по пути /axi/ethernet@e000b000
Поэтому старые диагностические команды вида fdt print /amba/ethernet@e000b000 или fdt print /ethernet@e000b000 возвращали ошибку libfdt fdt_path_offset() returned FDT_ERR_NOTFOUND
Правильная команда для нашего дерева fdt print /axi/ethernet@e000b000
Вывод fdt print /axi/ethernet@e000b000 показал, что узел существует и включен:
ethernet@e000b000 { compatible = "xlnx,zynq-gem", "cdns,gem"; reg = <0xe000b000 0x00001000>; status = "okay"; interrupts = <0x00000000 0x00000016 0x00000004>; clocks = <0x00000001 0x0000001e 0x00000001 0x0000001e 0x00000001 0x0000000d>; clock-names = "pclk", "hclk", "tx_clk"; #address-cells = <0x00000001>; #size-cells = <0x00000000>; phy-mode = "rgmii-id"; phy-handle = <0x00000008>; ethernet-phy@1 { reg = <0x00000001>; device_type = "ethernet-phy"; phandle = <0x00000008>; }; };
Здесь были важны следующие признаки:
status = "okay" phy-mode = "rgmii-id" phy-handle = <...> ethernet-phy@1 reg = <1>
На уровне DTS все выглядело правдоподобно. Но команды MDIO тогда еще не показывали рабочую шину:
mdio list No MDIO bus found
А команда mii device возвращала пустой список:
MII devices:
Это означало, что U-Boot либо не создал Ethernet-устройство, либо драйвер GEM не дошел до регистрации MDIO bus. На этом месте до меня еще не дошло что шина MDIO не включена, я продолжил копать глубже :D
Следующий шаг который я сделал - я решил проверить итоговый .config U-Boot, вдруг в нем что-то не так. В Buildroot фрагмент конфигурации был дополнен Ethernet, PHY и диагностическими командами:
CONFIG_NET=y CONFIG_CMD_NET=y CONFIG_NETDEVICES=y CONFIG_DM_ETH=y CONFIG_ZYNQ_GEM=y CONFIG_PHYLIB=y CONFIG_PHY_REALTEK=y CONFIG_MII=y CONFIG_CMD_MII=y CONFIG_CMD_MDIO=y CONFIG_CMD_PING=y CONFIG_CMD_DHCP=y CONFIG_CMD_DM=y
Проверка:
grep -E 'CONFIG_NET=|CONFIG_DM_ETH=|CONFIG_NETDEVICES=|CONFIG_ZYNQ_GEM=|CONFIG_PHYLIB=|CONFIG_PHY_REALTEK=|CONFIG_MII=|CONFIG_CMD_MII=|CONFIG_CMD_MDIO=|CONFIG_CMD_DM=' "$BR_OUT/build/uboot-"*/.config CONFIG_CMD_DM=y CONFIG_CMD_MII=y CONFIG_CMD_MDIO=y CONFIG_NET=y CONFIG_PHYLIB=y CONFIG_PHY_REALTEK=y CONFIG_DM_ETH=y CONFIG_NETDEVICES=y CONFIG_MII=y CONFIG_ZYNQ_GEM=y
Вывод подтвердил, что все нужные опции попали в сборку. Значит, проблема была уже не в том, что U-Boot собран без Ethernet-драйвера.
Ну тогда я решил посмотреть, правильный ли драйвер привязан к узлу устройства. Для проверки привязки драйверов использовалась команда dm tree
На одном из этапов отладки вывод узла ethernet выглядел так:
ethernet 0 [ ] zynq_gem | |-- ethernet@e000b000
Это важная строка, которая означала что драйвер zynq_gem в U-Boot найден, устройство ethernet@e000b000 создано, но устройство еще не прошло probe, потому что в колонке Probed стояло [ ].
Я попробовал закинуть конкретную привязку к драйверу:
ethernet_phy: ethernet-phy@1 { compatible = "realtek,rtl8211e", "ethernet-phy-id001c.c915", "ethernet-phy-ieee802.3-c22"; reg = <1>; device_type = "ethernet-phy"; };
И на удивление после дополнительной правки DTS Ethernet-устройство таки появилось в uclass:
dm uclass ethernet uclass 40: ethernet 0 ethernet@e000b000 @ 1eaf03d0, seq 0
Но mdio list все еще показывал:
No MDIO bus found
На этом этапе стало понятно, что U-Boot видит MAC, но не может корректно дойти до PHY. Тут я пустился в проверку схемы платы ZYNQ MINI Rev B. На странице 7 схемы есть два PHY RTL8211E:
U18 - RTL8211E-VLPS Gigabit Ethernet
U17 - RTL8211E-VBPL Gigabit Ethernet
Для U-Boot gem0 относится именно к PS Ethernet, то есть к U18. На странице 4 схемы видно, что PS Ethernet-сигналы заведены на MIO банка 501, включая PHY_MDIO и PHY_MDC. На странице 7 видно, что PS Ethernet использует RTL8211E-VL, линии PHY_MDIO, PHY_MDC, PHY_nRST, RGMII-сигналы и отдельный 25 MHz кварц X5.
Особенно важными оказались strap-пины адреса PHY:
pin 34 - PHY_AD0/PHY_LED0 - подтяжка к VCC_1V8
pin 35 - PHY_AD1/PHY_LED1 - подтяжка к GND
pin 13 - PHY_AD2/PHY_RXCTL - подтяжка к GND
То есть адрес PHY_AD2 PHY_AD1 PHY_AD0 = 0 0 1 = 1
Схема подтвердила, что reg = <1> выбран правильно. Менять адрес на иной точно не требовалось.
Тут я решил подсмотреть, какие параметры Ethernet в DTS использованы в рабочем экземпляре одного из образов Linux на этой же плате. Его решено было использовать как источник проверенных параметров.
Сначала смотрим интерфейсы:
ip link | grep eth0 -A1 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 link/ether c6:92:6f:9f:5d:df
Дальше смотрим PHY device:
# readlink -f /sys/class/net/eth0/phydev /sys/devices/soc0/axi/e000b000.ethernet/mdio_bus/e000b000.ethernet-ffffffff/e000b000.ethernet-ffffffff:01
Последняя часть :01 означает MDIO-адрес PHY. То есть Linux подтвердил , что PHY address = 1
Проверяем MDIO bus:
ls -l /sys/bus/mdio_bus/devices/ e000b000.ethernet-ffffffff:01 -> ../../../devices/soc0/axi/e000b000.ethernet/mdio_bus/e000b000.ethernet-ffffffff/e000b000.ethernet-ffffffff:01
Потом смотрим PHY ID и драйвер:
for d in /sys/bus/mdio_bus/devices/*; do echo "$d" [ -f "$d/phy_id" ] && cat "$d/phy_id" [ -f "$d/uevent" ] && cat "$d/uevent" echo done
Результат:
/sys/bus/mdio_bus/devices/e000b000.ethernet-ffffffff:01 0x001cc915 DEVTYPE=PHY DRIVER=RTL8211E Gigabit Ethernet OF_NAME=ethernet-phy OF_FULLNAME=/axi/ethernet@e000b000/ethernet-phy@1 OF_TYPE=ethernet-phy
Это дало сразу несколько точных значений:
MAC -
e000b000.ethernetPHY address -
1PHY ID -
0x001cc915PHY driver -
RTL8211E Gigabit EthernetPHY path -
/axi/ethernet@e000b000/ethernet-phy@1
Также Linux показал режим интерфейса:
# cat /proc/device-tree/axi/ethernet@e000b000/phy-mode rgmii-id
А reg PHY-узла был подтвержден через hexdump:
hexdump -Cv /proc/device-tree/axi/ethernet@e000b000/ethernet-phy@1/reg 00000000 00 00 00 01 |....|
Значит, адрес и phy-mode были закрыты как подтвержденные.
В U-Boot DTS был добавлен явный compatible для PHY по ID, который показал Linux:
ethernet_phy: ethernet-phy@1 { compatible = "realtek,rtl8211e", "ethernet-phy-id001c.c915", "ethernet-phy-ieee802.3-c22"; reg = <1>; device_type = "ethernet-phy"; };
Формат строки ethernet-phy-id001c.c915 получен из PHY ID 0x001cc915
То есть 001c -> старшие 16 бит, c915 -> младшие 16 бит
После этого U-Boot начал создавать Ethernet-устройство:
Net: ZYNQ GEM: e000b000, mdio bus e000b000, phyaddr 1, interface rgmii-id eth0: ethernet@e000b000
А mdio list начал показывать PHY:
eth0: 1 - RealTek RTL8211E <--> ethernet@e000b000
На этом этапе казалось, что Ethernet уже исправлен. Но следующая проверка показала, что MDIO-обмен еще не работает физически. Я попробовал прочитать MDIO-регистры:
mii device eth0 mii read 1 2 mii read 1 3 mii read 1 0 mii read 1 1
Также через mdio:
mdio read eth0 1 2 mdio read eth0 1 3 mdio read eth0 1 0 mdio read eth0 1 1
Результат был не тем, что ожидался, все по нулям:
mii read 1 2 -> 0000 mii read 1 3 -> 0000 mdio read eth0 1 2 -> 0x0 mdio read eth0 1 3 -> 0x0
Для RTL8211E ожидалось:
reg 2 = 0x001c reg 3 = 0xc915
То есть U-Boot уже создал объект PHY на основе DTS, но реальные чтения MDIO-регистров возвращали нули. Это объясняло и ошибку auto-negotiation:
Waiting for PHY auto negotiation to complete......... TIMEOUT !
Если регистр 1, Basic Status Register, читается как 0x0000, U-Boot не увидит ни link up, ни autoneg complete. На этом этапе стало понятно, что проблема не в PHY address, не в compatible и не в phy-mode. Проблема находилась ниже: в аппаратной инициализации PS Ethernet/MDIO.
После проверки проекта Vivado выяснилось, что в PS7 configuration не была включена MDIO-шина для Ethernet. Device Tree сообщает U-Boot, что есть GEM0 и PHY, но MIO-мультиплексор, тактирование и часть PS-периферии настраиваются не DTS, а кодом ps7_init(), который попадает в FSBL из XSA.
Поэтому просто изменить DTS или пересобрать U-Boot было недостаточно. Нужно было:
включить MDIO в Vivado PS7 configuration
перегенерировать bitstream
экспортировать новый XSA с bitstream
пересобрать platform в Vitis
пересобрать FSBL из нового XSA
собрать новый BOOT.BIN из нового FSBL, нового bitstream и u-boot.elf
заменить BOOT.BIN на SD-карте
После полной пересборки U-Boot начал читать PHY по MDIO.
Новый лог U-Boot выглядел уже как нужно:
Net: ZYNQ GEM: e000b000, mdio bus e000b000, phyaddr 1, interface rgmii-id eth0: ethernet@e000b000
Но надо было проверить, читаем MDIO:
mdio list eth0: 1 - RealTek RTL8211E <--> ethernet@e000b000
Проверяем PHY ID:
mii read 1 2 001C
То есть U-Boot наконец начал физически читать PHY ID-регистр. Ну и после этого сеть заработала, по DHCP получили адрес:
setenv serverip 192.168.2.1 dhcp BOOTP broadcast 1 BOOTP broadcast 2 DHCP client bound to address 192.168.2.142 (545 ms)
Ну и ping стал успешным:
ping 192.168.2.1 Using ethernet@e000b000 device host 192.168.2.1 is alive
Очень удобными при дебаге, кстати, оказались команды fdt:
zynq-mini> fdt addr $fdtcontroladdr Working FDT set to 40bfd10 zynq-mini> fdt print / model model = "ZYNQ MINI Rev B" zynq-mini> fdt print /aliases aliases { serial0 = "/axi/serial@e0001000"; mmc0 = "/axi/mmc@e0100000"; ethernet0 = "/axi/ethernet@e000b000"; spi0 = "/axi/spi@e000d000"; }; zynq-mini> fdt print /axi/ethernet@e000b000 ethernet@e000b000 { compatible = "xlnx,zynq-gem", "cdns,gem"; reg = <0xe000b000 0x00001000>; status = "okay"; interrupts = <0x00000000 0x00000016 0x00000004>; clocks = <0x00000001 0x0000001e 0x00000001 0x0000001e 0x00000001 0x0000000d>; clock-names = "pclk", "hclk", "tx_clk"; #address-cells = <0x00000001>; #size-cells = <0x00000000>; local-mac-address = [00 0a 35 01 02 03]; phy-mode = "rgmii-id"; phy-handle = <0x00000008>; phandle = <0x00000023>; ethernet-phy@1 { compatible = "realtek,rtl8211e", "ethernet-phy-id001c.c915", "ethernet-phy-ieee802.3-c22"; reg = <0x00000001>; device_type = "ethernet-phy"; phandle = <0x00000008>; }; }; zynq-mini> fdt print /axi/ethernet@e000b000 status status = "okay" zynq-mini> fdt print /axi/ethernet@e000b000 phy-mode phy-mode = "rgmii-id" zynq-mini> fdt print /axi/ethernet@e000b000 phy-handle phy-handle = <0x00000008>
Эти команды помогли понять:
какой DTS реально использует U-Boot;
где находится Ethernet-узел;
включен ли он;
какой phy-mode задан;
какой PHY node связан через phy-handle.
Еще не менее полезными в дебаге оказались команды из Driver Model:
zynq-mini> dm tree Class Index Probed Driver Name ----------------------------------------------------------- root 0 [ + ] root_driver root_driver rsa_mod_ex 0 [ ] mod_exp_sw |-- mod_exp_sw simple_bus 0 [ + ] simple_bus |-- axi gpio 0 [ ] gpio_zynq | |-- gpio@e000a000 serial 0 [ + ] serial_zynq | |-- serial@e0001000 spi 0 [ ] zynq_qspi | |-- spi@e000d000 spi_flash 0 [ ] jedec_spi_nor | | `-- flash@0 ethernet 0 [ + ] zynq_gem | |-- ethernet@e000b000 bootdev 0 [ ] eth_bootdev | | |-- ethernet@e000b000.bootdev eth_phy_ge 0 [ + ] eth_phy_generic_drv | | `-- ethernet-phy@1 mmc 0 [ + ] arasan_sdhci | |-- mmc@e0100000 blk 0 [ + ] mmc_blk | | |-- mmc@e0100000.blk partition 0 [ + ] blk_partition | | | |-- mmc@e0100000.blk:1 partition 1 [ + ] blk_partition | | | `-- mmc@e0100000.blk:2 bootdev 1 [ ] mmc_bootdev | | `-- mmc@e0100000.bootdev simple_bus 1 [ + ] simple_bus | |-- slcr@f8000000 clk 0 [ + ] zynq_clk | | `-- clkc@100 timer 0 [ + ] arm_twd_timer | `-- timer@f8f00600 bootstd 0 [ ] bootstd_drv `-- bootstd bootmeth 0 [ ] bootmeth_extlinux |-- extlinux bootmeth 1 [ ] bootmeth_pxe |-- pxe bootmeth 2 [ ] vbe_simple `-- vbe_simple zynq-mini> dm uclass ethernet uclass 40: ethernet 0 * ethernet@e000b000 @ 1eaf03d0, seq 0 zynq-mini> dm uclass mdio
Эти команды помогли разделить проблемы:
нет ethernet@e000b000драйвер или DTS не сработали
есть zynq_gem, но [ ]устройство создано, но probe не завершился
есть zynq_gem и [ + ]MAC-драйвер поднялся
есть eth_phy_generic_drvPHY-узел разобран
нет mdio uclassэто не всегда блокер, важнее фактические mii read
Ну и держите на вооружении команды MDIO/MII:
zynq-mini> mdio list eth0: 1 - RealTek RTL8211E <--> ethernet@e000b000 zynq-mini> mii device MII devices: 'eth0' Current device: 'eth0' zynq-mini> mii info PHY 0x00: OUI = 0x0732, Model = 0x11, Rev = 0x05, 10baseT, HDX PHY 0x01: OUI = 0x0732, Model = 0x11, Rev = 0x05, 10baseT, HDX zynq-mini> mii read 1 2 001C zynq-mini> mii read 1 3 C915 zynq-mini> mii read 1 0 1140 zynq-mini> mii read 1 1 7969 zynq-mini> mdio read eth0 1 2 Reading from bus eth0 PHY at address 1: 2 - 0x1c zynq-mini> mdio read eth0 1 3 Reading from bus eth0 PHY at address 1: 3 - 0xc915
Эти команды были ключевыми. Именно mii read 1 2 и mii read 1 3 показали, что U-Boot не читает PHY-регистры, когда MDIO не был включен в Vivado.
Основной урок который я для себя извлек: если речь идет о PS-периферии и MIO, нужно проверять всю цепочку:
Vivado PS7 configuration -> XSA -> FSBL / ps7_init -> U-Boot DTS -> U-Boot driver -> MDIO/MII diagnostics -> DHCP / ping
Именно разделение этой цепочки на уровни позволило не гадать, а последовательно найти ошибку.
Ну и последнее, что оставалось поправить в этом безобразии - ошибка про отсутствие валидного uboot.env и нерабочий environment с переменными и прочим. Чтобы saveenv работал и U-Boot не ругался, нужно создать именно бинарный environment-файл, а не uEnv.txt. Будем поправлять, благо это не сложно.
Проверяем, как настроен U-Boot:
grep -E 'CONFIG_ENV_IS_IN_FAT|CONFIG_ENV_FAT_INTERFACE|CONFIG_ENV_FAT_DEVICE_AND_PART|CONFIG_ENV_FAT_FILE|CONFIG_ENV_SIZE' \ "$BR_OUT/build/uboot-"*/.config
Типичный результат может быть таким:
CONFIG_ENV_SIZE=0x20000 CONFIG_ENV_IS_IN_FAT=y CONFIG_ENV_FAT_INTERFACE="mmc" CONFIG_ENV_FAT_DEVICE_AND_PART="0:auto" CONFIG_ENV_FAT_FILE="uboot.env" CONFIG_ENV_FAT_FILE_REDUND="uboot-redund.env"
Тогда U-Boot ищет файл:
mmc 0:auto /uboot.env
Что ж, теперь необходимо его создать. Достаточно добавить только то, что вы реально хотите переопределять:
cat > /tmp/uboot-env.txt << 'EOF' bootdelay=1 ethaddr=00:0a:35:01:02:03 ipaddr=192.168.2.145 netmask=255.255.255.0 gatewayip=192.168.2.1 serverip=192.168.2.1 bootcmd=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 EOF
Создать бинарный uboot.env:
"$BR_OUT/host/bin/mkenvimage" -s 0x20000 -o /tmp/uboot.env /tmp/uboot-env.txt
Если mkenvimage не найден:
find "$BR_OUT" -name mkenvimage -type f
Скопировать на FAT-раздел SD:
sudo mount /dev/sdX1 /mnt sudo cp /tmp/uboot.env /mnt/uboot.env sync sudo umount /mnt
После этого при старте видим:
U-Boot 2024.01 (Jun 07 2026 - 02:34:34 +0300) CPU: Zynq 7z020 Silicon: v3.1 Model: ZYNQ MINI Rev B DRAM: ECC disabled 512 MiB Core: 19 devices, 15 uclasses, devicetree: embed Flash: 0 Bytes NAND: 0 MiB MMC: mmc@e0100000: 0 Loading Environment from FAT... OK In: serial@e0001000 Out: serial@e0001000 Err: serial@e0001000 Net: ZYNQ GEM: e000b000, mdio bus e000b000, phyaddr 1, interface rgmii-id eth0: ethernet@e000b000 Hit any key to stop autoboot: 0 zynq-mini>
Всё! Красота! Можно идти дальше в дебаг проблем с USB, но это уже связано с Linux.
Фиксим проблемы в Linux DTS
После старта Linux Kernel сразу стало видно, что Ethernet завёлся. Класс. Одной проблемо меньше.
[ 1.086279] macb e000b000.ethernet eth0: Cadence GEM rev 0x00020118 at 0xe000b000 irq 40 (00:0a:35:01:02:03) [ 3.333741] macb e000b000.ethernet eth0: PHY [e000b000.ethernet-ffffffff:01] driver [RTL8211E Gigabit Ethernet] (irq=POLL) [ 3.344859] macb e000b000.ethernet eth0: configuring for phy/rgmii-id link mode [ 7.537190] macb e000b000.ethernet eth0: Link is Up - 1Gbps/Full - flow control tx
Теперь перейдем к проблеме с USB. При подключении клавиатуры я ожидал, что у меня по умолчанию все взлетит и клавиатура будет работать для ввода сообщений в консоли на OLED-дисплее:
[ 132.412403] usb 1-1: new low-speed USB device number 2 using ci_hdrc [ 132.659561] input: USB Keyboard as /devices/soc0/axi/e0002000.usb/ci_hdrc.0/usb1/1-1/1-1:1.0/0003:04D9:1702.0001/input/input0 [ 132.862800] hid-generic 0003:04D9:1702.0001: input: USB HID v1.10 Keyboard [ USB Keyboard] on usb-ci_hdrc.0-1/input0 [ 132.890783] input: USB Keyboard System Control as /devices/soc0/axi/e0002000.usb/ci_hdrc.0/usb1/1-1/1-1:1.1/0003:04D9:1702.0002/input/input1 [ 132.973063] input: USB Keyboard Consumer Control as /devices/soc0/axi/e0002000.usb/ci_hdrc.0/usb1/1-1/1-1:1.1/0003:04D9:1702.0002/input/input2 [ 132.986432] hid-generic 0003:04D9:1702.0002: input: USB HID v1.10 Device [ USB Keyboard] on usb-ci_hdrc.0-1/input1
Но ввод не работал, Num Lock не включался. Питания не было. Как оказалось в ходе копаний - USB host на этой плате требует не только включенного &usb0, но и двух управляющих GPIO-линий:
USBPHY_nRSET- сброс ULPI PHY USB3320C.USBCP_EN- включение питания VCC_USB_5.0 на USB host VBUS.
В исходном DTS у меня было есть только:
&usb0 { status = "okay"; dr_mode = "host"; usb-phy = <&usb_phy0>; };
Этого недостаточно, если питание VBUS управляется отдельным GPIO.
На странице 10 схемы стоит USB PHY U19 USB3320C-EZK. У него есть линия RESETB, заведенная в сеть USBPHY_nRSET, а ULPI-сигналы называются USBPHY_DATA0..7, USBPHY_CLKOUT, USBPHY_STP, USBPHY_NXT, USBPHY_DIR.
Там же видно управление питанием USB host: сеть USBCP_EN управляет ключами Q3 AO3407 и Q4 AO3400, которые формируют VCC_USB_5.0 для разъема USB. То есть без активного USBCP_EN на VBUS физически не будет 5 В.

На странице 4 схемы USB-сигналы заведены в PS MIO bank 501. Это значит, что в Vivado PS7 configuration должен быть включен USB0 через MIO, а не через EMIO.

Проверяем:

Вроде все верно.

И тут. Ключевой момент такой же, как с MDIO: если в Vivado PS7 не включен нужный MIO или GPIO, то Linux DTS сам по себе не сможет физически поднять линию.
Теперь необходимо внести правки в DTS, который загружается в память при старте Linux. Слава богу тут ничего не надо будет перетыкать взад-вперед и нужно будет только собрать blob-файл и перекинуть его по сети на плату.
Открываем файл dts:
vim $WORK/board-support/dts/zynq-mini-revb.dts
И после блока &usb0 вставим следующее:
&gpio0 { status = "okay"; usb0_phy_reset { gpio-hog; gpios = <7 GPIO_ACTIVE_HIGH>; output-high; line-name = "usb0-phy-reset"; }; usb0_vbus_en { gpio-hog; gpios = <9 GPIO_ACTIVE_HIGH>; output-high; line-name = "usb-vbus-en"; }; };
MIO7 - USBPHY_nRSET - держит USB3320C вне reset
MIO9 - USBCP_EN - включает ключ питания VBUS
output-high - высокий уровень- reset отпущен, VBUS включен
Теперь необходимо пересобрать бинарный DTS и перезалить его на SD-карту.
export WORK=/home/megalloid/devel/xilinx/projects/zynq_mini_oled/linux export BR_SRC="$WORK/buildroot-2024.02.7" export BR_OUT="$WORK/br-output" export BR_EXT="$WORK/board-support" cd "$BR_SRC"
Если DTS лежит во внешнем дереве, например: $BR_EXT/dts/zynq-mini-revb.dts и подключен через BR2_LINUX_KERNEL_CUSTOM_DTS_PATH="$(BR2_EXTERNAL_ZYNQ_MINI_I2C_PATH)/dts/zynq-mini-revb.dts"
то достаточно выполнить команду:
make BR2_EXTERNAL="$BR_EXT" O="$BR_OUT" linux-rebuild
После сборки проверить DTB:
ls -lh "$BR_OUT/images/"*.dtb
Должен появиться файл: $BR_OUT/images/zynq-mini-revb.dtb
Если файл не обновился по timestamp, можно сделать жестче:
make BR2_EXTERNAL="$BR_EXT" O="$BR_OUT" linux-dirclean make BR2_EXTERNAL="$BR_EXT" O="$BR_OUT" linux-rebuild
Но обычно для DTS достаточно linux-rebuild.
Теперь заливаем на плату файл подсмотрев ее IP:
# ifconfig | grep inet inet addr:192.168.2.142 Bcast:192.168.2.255 Mask:255.255.255.0 inet6 addr: fe80::20a:35ff:fe01:203/64 Scope:Link inet addr:127.0.0.1 Mask:255.0.0.0 inet6 addr: ::1/128 Scope:Host
На ПК делаем следующее:
export TARGET=root@192.168.2.145 scp -O "$BR_OUT/images/zynq-mini-revb.dtb" "$TARGET:/tmp/zynq-mini-revb.dtb"
Прям через SSH выполним все необходимо на плате Zynq:
ssh "$TARGET" 'mkdir -p /mnt/boot && mount /dev/mmcblk0p1 /mnt/boot' ssh "$TARGET" 'ls -la /mnt/boot' root@192.168.2.142's password: total 15985 drwx------ 2 root root 512 Jan 1 00:00 . drwxr-xr-x 3 1000 1000 1024 Jan 1 00:28 .. -rwx------ 1 root root 5101256 Jun 7 2026 BOOT.BIN -rwx------ 1 root root 957 Jun 4 2026 uEnv.txt -rwx------ 1 root root 10990144 Jun 4 2026 uImage -rwx------ 1 root root 131072 Jan 1 1980 uboot-redund.env -rwx------ 1 root root 131072 Jun 7 2026 uboot.env -rwx------ 1 root root 11534 Jun 4 2026 zynq-mini-revb.dtb
Заменяем сохраняя backup рабочей версии:
ssh "$TARGET" ' set -e cp /mnt/boot/zynq-mini-revb.dtb /mnt/boot/zynq-mini-revb.dtb.bak cp /tmp/zynq-mini-revb.dtb /mnt/boot/zynq-mini-revb.dtb sync ls -lh /mnt/boot/zynq-mini-revb.dtb /mnt/boot/zynq-mini-revb.dtb.bak '
Размонтируем и перезагружаем:
ssh "$TARGET" 'umount /mnt/boot' ssh "$TARGET" 'reboot'
После перезагрузки проверяем на устройстве:
# cat /proc/device-tree/model ZYNQ MINI Rev B (manual Linux port) # mount -t debugfs none /sys/kernel/debug 2>/dev/null || true # cat /sys/kernel/debug/gpio | grep -iE 'usb|vbus|reset' gpio-519 ( |usb0-phy-reset ) out lo gpio-521 ( |usb-vbus-en ) out hi
Проверяем USB:
# dmesg | grep -iE 'usb|ulpi|ci_hdrc|ehci|phy|vbus' [ 0.000000] Booting Linux on physical CPU 0x0 [ 0.353653] usbcore: registered new interface driver usbfs [ 0.359228] usbcore: registered new interface driver hub [ 0.364595] usbcore: registered new device driver usb [ 0.369930] usb_phy_generic usb-phy: dummy supplies not allowed for exclusive requests [ 1.128957] pegasus: Pegasus/Pegasus II USB Ethernet driver [ 1.134565] usbcore: registered new interface driver pegasus [ 1.140295] usbcore: registered new interface driver asix [ 1.145736] usbcore: registered new interface driver ax88179_178a [ 1.151875] usbcore: registered new interface driver cdc_ether [ 1.157755] usbcore: registered new interface driver smsc75xx [ 1.163533] usbcore: registered new interface driver smsc95xx [ 1.169320] usbcore: registered new interface driver net1080 [ 1.175007] usbcore: registered new interface driver cdc_subset [ 1.180966] usbcore: registered new interface driver zaurus [ 1.186582] usbcore: registered new interface driver cdc_ncm [ 1.196093] usbcore: registered new interface driver usb-storage [ 1.204001] ci_hdrc ci_hdrc.0: EHCI Host Controller [ 1.209000] ci_hdrc ci_hdrc.0: new USB bus registered, assigned bus number 1 [ 1.285657] ci_hdrc ci_hdrc.0: USB 2.0 started, EHCI 1.00 [ 1.292029] hub 1-0:1.0: USB hub found [ 1.419767] usbcore: registered new interface driver usbhid [ 1.425373] usbhid: USB HID core driver [ 1.601955] usb 1-1: new low-speed USB device number 2 using ci_hdrc [ 1.859948] input: USB Keyboard as /devices/soc0/axi/e0002000.usb/ci_hdrc.0/usb1/1-1/1-1:1.0/0003:04D9:1702.0001/input/input0 [ 2.082699] hid-generic 0003:04D9:1702.0001: input: USB HID v1.10 Keyboard [ USB Keyboard] on usb-ci_hdrc.0-1/input0 [ 2.116164] input: USB Keyboard System Control as /devices/soc0/axi/e0002000.usb/ci_hdrc.0/usb1/1-1/1-1:1.1/0003:04D9:1702.0002/input/input1 [ 2.202358] input: USB Keyboard Consumer Control as /devices/soc0/axi/e0002000.usb/ci_hdrc.0/usb1/1-1/1-1:1.1/0003:04D9:1702.0002/input/input2 [ 2.209758] hid-generic 0003:04D9:1702.0002: input: USB HID v1.10 Device [ USB Keyboard] on usb-ci_hdrc.0-1/input1 [ 3.313396] macb e000b000.ethernet eth0: PHY [e000b000.ethernet-ffffffff:01] driver [RTL8211E Gigabit Ethernet] (irq=POLL) [ 3.324553] macb e000b000.ethernet eth0: configuring for phy/rgmii-id link mode
И можно увидеть, что клавиатура определилась. Попробовать ввести что-то и увидеть, как происходит ввод информации, и можно спокойно выполнять любые команды после авторизации:

Разбираемся с init-скриптами: сеть, модули, OLED, USB и SSH
После того как U-Boot загрузил uImage и передал ядру Linux правильный zynq-mini-revb.dtb, начинается следующая часть загрузки: старт userspace. На этом этапе ядро уже инициализировало базовое железо, смонтировало rootfs и запустило первый процесс /sbin/init.
В нашем Buildroot-образе используется классическая BusyBox init-схема. Поэтому последовательность запуска определяется простыми shell-скриптами в /etc/init.d.
Главный стартовый скрипт - rcS. Он проходит по всем файлам вида /etc/init.d/S??* и запускает их в числовом порядке.
Упрощенно логика такая:
for i in /etc/init.d/S??* ; do [ ! -f "$i" ] && continue $i start done
То есть порядок запуска определяется именем файла:
S01... S02... S03... S39... S40... S45... S50...
Для остановки системы используется обратная логика. Скрипт rcK проходит по тем же S??*, но в обратном порядке и вызывает stop.
rcS: запуск сервисов по возрастанию номеровrcK: остановка сервисов по убыванию номеров
Это важно, потому что сеть, дисплей и SSH зависят друг от друга не формально через dependency graph, как в systemd, а только через выбранные номера скриптов.
В текущем образе важные init-скрипты идут так:
S01seedrng S01syslogd S02klogd S02sysctl S03modules S39set-eth0-mac S40network S45oled-console S50dropbear
На практике ключевая логика такая:
модули должны загрузиться до OLED-консоли
MAC должен быть задан до ifup
сеть должна подняться до SSH
/dev/fb0 должен появиться до запуска oled-console
S01seedrng: состояние генератора случайных чисел
Первым запускается S01seedrng. Он сохраняет и восстанавливает seed для генератора случайных чисел между перезагрузками.
Для нашего OLED-проекта этот скрипт напрямую не нужен, но он важен для сетевых сервисов и SSH. Dropbear при первом старте генерирует host keys, и ему нужен нормальный источник случайных чисел.
Скрипт читает дополнительные параметры из /etc/default/seedrng и затем вызывает seedrng $SEEDRNG_ARGS || true
Принципиально важно, что скрипт не должен ломать загрузку. Поэтому ошибка seedrng игнорируется через || true.
S01syslogd и S02klogd: логи userspace и ядра
Дальше стартуют два логирующих демона: S01syslogd и S02klogd. syslogd принимает сообщения userspace, а klogd забирает сообщения ядра.
Для отладки и bring-up это весьма полезно. Если модуль i2c-master-axi не загрузился, если ssd1307fb не создал /dev/fb0, если network startup завершился ошибкой, это должно быть видно в логах.
Проверка на плате:
ps | grep -E 'syslogd|klogd' logread dmesg
Если в системе есть logread, то через него удобно смотреть накопленные сообщения BusyBox syslog.
S02sysctl: применение параметров ядра
S02sysctl применяет параметры из:
/etc/sysctl.d/*.conf /usr/local/lib/sysctl.d/*.conf /usr/lib/sysctl.d/*.conf /lib/sysctl.d/*.conf /etc/sysctl.conf
Для текущего проекта этот скрипт не является главным, но он может понадобиться для сетевых параметров. Например, через sysctl можно включать или менять:
net.ipv4.ip_forward kernel.printk vm.overcommit_memory
Если таких файлов нет, скрипт просто проходит без существенного эффекта.
S03modules: загрузка модулей ядра
S03modules - один из ключевых скриптов для этого проекта. Он загружает модули из файлов /etc/modules-load.d/*.conf
Логика простая:
for f in /etc/modules-load.d/*.conf; do while read mod; do modprobe "$mod" 2>/dev/null || true done < "$f" done
Пустые строки и комментарии игнорируются. Именно здесь удобно автоматически грузить out-of-tree драйвер i2c-master-axi.Например, можно создать файл:
cat > /etc/modules-load.d/i2c-master-axi.conf << 'EOF' i2c-master-axi EOF
После старта S03modules должен произойти такой путь:
Загружается модуль
i2c-master-axiРегистрируется платформенный драйвер
Драйвер сопоставляется с
compatible = “user,i2c-master-axi-1.0”Функция
probeотображает в память регистровую область по адресу0x43c00000Драйвер регистрирует
i2c_adapterLinux видит дочерний узел
oled@3cДрайвер
ssd1307fbвыполняетprobeдля OLED-дисплеяПоявляется устройство
/dev/fb0
Проверки:
lsmod dmesg | grep -iE 'i2c|axi|ssd|fb' ls /dev/i2c-* ls /dev/fb* ls /sys/bus/i2c/devices/
Если i2c-master-axi собран не модулем, а встроен в ядро, S03modules для него не нужен. Тогда драйвер должен привязаться еще на этапе инициализации ядра. Но для out-of-tree разработки модульный вариант удобнее: можно менять драйвер, копировать .ko на плату и перезагружать только модуль.
S39set-eth0-mac: MAC-адрес до поднятия сети
S39set-eth0-mac запускается перед S40network. Это правильный порядок. Скрипт делает следующее:
[ -d /sys/class/net/eth0 ] || exit 0 MAC=$(grep ... /etc/eth0-mac | head -1) [ -n "$MAC" ] || MAC="00:0a:35:01:02:03" ip link set dev eth0 down ip link set dev eth0 address "$MAC" ip link set dev eth0 up
То есть если файл /etc/eth0-mac существует и содержит валидный MAC-адрес, он будет применен. Если файла нет, будет использован fallback 00:0a:35:01:02:03
Это удобно для прототипа, но для реального устройства такой MAC нельзя оставлять одинаковым на всех платах. Для статьи лучше прямо указать, что это временный адрес для bring-up.
Файл /etc/eth0-mac должен содержать одну строку 02:00:00:00:00:01
Для локально назначенного адреса лучше использовать MAC с установленным local bit, например диапазон 02:....
Проверка:
cat /etc/eth0-mac ip link show eth0 cat /sys/class/net/eth0/address
Если eth0 на этом этапе не существует, скрипт просто завершится. Это значит, что Ethernet-драйвер или PHY еще не поднялись.
S40network: ifup -a
S40network поднимает сеть через ifupdown: /sbin/ifup -a. Перед этим создается каталог: mkdir -p /run/network. Это нужно для lock-файлов ifupdown.
Фактическая настройка сети лежит не в S40network, а в /etc/network/interfaces.
Пример DHCP-конфигурации:
auto lo iface lo inet loopback auto eth0 iface eth0 inet dhcp
Пример статической конфигурации:
auto lo iface lo inet loopback auto eth0 iface eth0 inet static address 192.168.2.145 netmask 255.255.255.0 gateway 192.168.2.1
Проверки:
ip link show eth0 ip addr show eth0 ip route cat /etc/network/interfaces dmesg | grep -iE 'macb|gem|phy|rtl|link'
Если S40network пишет FAIL, нужно отдельно смотреть:
ifup -v eth0 udhcpc -i eth0 -f -q
S45oled-console: включение OLED-консоли
S45oled-console - второй ключевой проектный скрипт после S03modules.
Он делает две вещи:
Ждет появления
/dev/fb0.Запускает
/usr/local/bin/oled-console.
Фрагмент логики:
i=0 while [ ! -c /dev/fb0 ] && [ "$i" -lt 40 ]; do sleep 0.25 i=$((i + 1)) done [ -x /usr/local/bin/oled-console ] && /usr/local/bin/oled-console
Итого ожидание длится до 10 секунд: 40 * 0.25 s = 10 s
Это правильно, потому что /dev/fb0 появляется не сразу после старта userspace. Он появляется только после того, как:
i2c-master-axiзарегистрировал I2C-шинуssd1307fb привязался к oled@3c
framebuffer subsystem создал fb0
devtmpfsсоздал/dev/fb0
Если /dev/fb0 появился, вызывается oled-console. Этот скрипт:
останавливает
oled-clockпривязывает
fbconобратно к/dev/fb0запускает
gettyнаtty1
То есть OLED начинает работать как маленькая Linux-консоль. Проверки:
ls -l /dev/fb0 cat /sys/class/graphics/fb0/name 2>/dev/null dmesg | grep -iE 'ssd|fb|framebuffer|console' cat /sys/class/vtconsole/vtcon1/bind ps | grep '[g]etty.*tty1'
Если нужно переключиться обратно в режим часов (создадим позже), используется другой userspace-скрипт:
/usr/local/bin/oled-clock-start
Он останавливает getty на tty1, отвязывает fbcon от framebuffer и запускает демон oled-clock.
S50dropbear: SSH после сети
S50dropbear запускает SSH-сервер Dropbear. Он стоит после S40network, что логично: SSH имеет смысл запускать после настройки интерфейса eth0.
Скрипт читает опции из /etc/default/dropbear и запускает /usr/sbin/dropbear
Ключевая строка: DROPBEAR_ARGS="$DROPBEAR_ARGS -R"
Опция -R означает, что Dropbear может сгенерировать host keys при старте, если они отсутствуют. Поэтому здесь снова важен ранний S01seedrng.
Проверки:
ps | grep dropbear netstat -ltnp 2>/dev/null | grep ':22' ip addr show eth0
Что стоит улучшить
Текущая схема уже рабочая, но для более чистого проекта я бы сделал несколько улучшений.
Добавить логирование в S03modules. Сейчас ошибки modprobe подавляются modprobe
"$mod" 2>/dev/null trueи для bring-up лучше хотя бы временно писать результат:echo “Loading module: $mod; echo "Failed to load module: $mod"
Иначе можно не заметить, чтоi2c-master-axiвообще не загрузился.В S45oled-console проверять результат ожидания /dev/fb0. Сейчас после ожидания скрипт все равно пытается вызвать oled-console, если файл исполняемый. Лучше явно диагностировать timeout:
if [ ! -c /dev/fb0 ];
then
echo "S45oled-console: /dev/fb0 not found"
exit 0
fiРазвести режимы OLED console и OLED clock. Ниже я покажу второй режим. Первый console mode:
fbcon + getty on tty1, а второй status display mode:oled-clock writes directly to /dev/fb0. Они конфликтуют, потому что оба пишут в один framebuffer. Поэтому перед запуском oled-clock нужно отвязать fbcon, а перед возвратом к консоли - остановить oled-clock и снова привязать fbcon.Не полагаться на фиксированный eth0, если интерфейсов станет больше. Для текущей платы eth0 допустим. Но если появится USB Ethernet или второй MAC, лучше будет привязываться к MACB/GEM интерфейсу более явно.
Без подготовки этих скриптов даже запускающаяся плата не дала бы нам сразу результат. Поэтому автоматизация сразу готовит все в рабочее состояние. С этим разобрались. Идем дальше.
Разбираемся с userspace-утилитами
После того как Linux загрузился, модуль i2c-master-axi зарегистрировал I2C-адаптер, а драйвер ssd1307fb создал /dev/fb0, можно переходить к последнему уровню проекта - userspace-приложению которое я упоминал выше - приложение для вывода нужной нам информации на экран. Назвал я его oled_clock.
На этом уровне уже не нужно вручную дергать регистры AXI IP, отправлять I2C-команды или знать внутреннюю последовательность инициализации SSD1306.
Приложение будет работать с обычным framebuffer-устройством:

Это и есть смысл всей предыдущей работы: кастомный контроллер в ПЛИС становится частью стандартной Linux-модели. Пользовательское приложение пишет картинку в /dev/fb0, а ядро само превращает обновление framebuffer в I2C-транзакции к адресу 0x3c.
Для проверки и эксплуатации OLED в проекте удобно иметь несколько утилит:
oled-clock- Основная C-утилита: тестовые паттерны, информация о framebuffer, часы, uptime, температура, напряженияfb_test.sh- Простой shell/python тест, который пишет сырые 1024 байта в /dev/fb0oled-clock-start- Переключение OLED в режим кастомной картинки или часовoled-console- Возврат OLED в режим Linux console через fbcon и gettydeploy.sh- Удобная обертка на хосте: скомпилить утилиту и залить бинарник по SSH, запустить тест, переключить режим.
Общая архитектура userspace-вывода
OLED SSD1306 128x64 в режиме monochrome framebuffer дает небольшой буфер: 128 * 64 / 8 = 1024 bytes. То есть один полный кадр занимает всего 1024 байта. Это удобно для отладки: можно генерировать весь кадр в userspace и целиком отправлять его в /dev/fb0.
Общая схема работы oled-clock такая:

Ключевой момент: oled-clock не вызывает i2c_transfer() напрямую. Он работает с /dev/fb0. Это упрощает userspace-код и сохраняет стандартную модель Linux: графика отдельно, I2C-контроллер отдельно, OLED-драйвер отдельно.
Предварительные проверки перед запуском userspace
Перед тем как запускать oled-clock, нужно убедиться, что низшие уровни уже работают.
Проверяем, что загружен модуль:
lsmod | grep i2c dmesg | grep -i i2c-master
Ожидаемая строка:
i2c-master-axi 43c00000.i2c: input=50000000 Hz, bus=100000 Hz, prescale=124
Проверяем I2C-адаптер:
ls /sys/class/i2c-adapter/ for d in /sys/class/i2c-adapter/i2c-*; do echo "$d" cat "$d/name" 2>/dev/null || true done
Ожидаем адаптер с именем, связанным с нашим platform-устройством 43c00000.i2c
Проверяем OLED-клиент:
ls /sys/bus/i2c/devices/
Типичный вариант:
0-003c i2c-0
Номер шины может отличаться. Важно не число 0 или 1, а наличие клиента 003c.
Проверяем framebuffer:
ls -l /dev/fb0 cat /sys/class/graphics/fb0/name 2>/dev/null || true
Если /dev/fb0 отсутствует, userspace-утилиты для framebuffer запускать рано. Сначала нужно смотреть DTS, ssd1307fb, i2c-master-axi, питание OLED и XDC.
Минимальный сырой тест через /dev/fb0
Самый простой тест - записать в framebuffer ровно 1024 байта. Например, погасить экран (если на плату затащили python):
python3 -c "import sys; sys.stdout.buffer.write(b'\x00' * 1024)" > /dev/fb0
Или залить экран:
python3 -c "import sys; sys.stdout.buffer.write(b'\xff' * 1024)" > /dev/fb0
Но такой тест не всегда надежен. Драйвер ssd1307fb может использовать deferred I/O, то есть не каждый одиночный короткий write() немедленно уходит на I2C. Поэтому для диагностики лучше использовать отдельную утилиту, которая:
читает параметры
framebufferчерезioctl;учитывает
line_length;формирует полный кадр;
пишет весь кадр целиком;
при необходимости использует
mmap;может дублировать
write()послеmmap, чтобы гарантировать обновление.
Для быстрых ручных тестов можно использовать fb_test.sh.
fb_test.sh: быстрые паттерны без C-кода
fb_test.sh - это небольшой shell-скрипт, который через Python генерирует 1024 байта и отправляет их прямо в /dev/fb0.
Код скрипта:
#!/bin/sh # Test framebuffer bit-order/orientation on SSD1306 via /dev/fb0. # Usage: fb_test.sh A|B|C|D set -eu killall -q oled-clock 2>/dev/null || true sleep 0.3 case "${1:-A}" in A) # only bit 0 of byte 0 (LSB-first => pixel (0,0); MSB-first => pixel (7,0)) python3 -c "import sys; b=bytearray(1024); b[0]=0x01; sys.stdout.buffer.write(bytes(b))" > /dev/fb0 echo "A: byte[0]=0x01" ;; B) # only bit 7 of byte 0 (MSB) python3 -c "import sys; b=bytearray(1024); b[0]=0x80; sys.stdout.buffer.write(bytes(b))" > /dev/fb0 echo "B: byte[0]=0x80" ;; C) # whole first row lit (row-major) => thin horizontal line on top (y=0) python3 -c "import sys; b=bytearray(1024); [setattr(b, '__setitem__', None) for _ in []] for i in range(16): b[i]=0xFF sys.stdout.buffer.write(bytes(b))" > /dev/fb0 echo "C: row 0 all white" ;; D) # whole first column lit (column 0 => bit 0 of every row's byte 0) python3 -c "import sys; b=bytearray(1024) for y in range(64): b[y*16]=0x01 sys.stdout.buffer.write(bytes(b))" > /dev/fb0 echo "D: col 0 LSB on each row" ;; E) # whole first column lit assuming MSB-first => bit7 of every row's byte 0 python3 -c "import sys; b=bytearray(1024) for y in range(64): b[y*16]=0x80 sys.stdout.buffer.write(bytes(b))" > /dev/fb0 echo "E: col 0 MSB on each row" ;; F) # fill: every other row white => stripes python3 -c "import sys; b=bytearray(1024) for y in range(64): v = 0xFF if (y%2)==0 else 0x00 for x in range(16): b[y*16+x]=v sys.stdout.buffer.write(bytes(b))" > /dev/fb0 echo "F: horizontal stripes 1px" ;; G) # solid white python3 -c "import sys; sys.stdout.buffer.write(b'\\xFF'*1024)" > /dev/fb0 echo "G: all white" ;; Z) python3 -c "import sys; sys.stdout.buffer.write(b'\\x00'*1024)" > /dev/fb0 echo "Z: all black" ;; *) echo "unknown pattern $1"; exit 1 ;; esac
Типовые паттерны:
Паттерн |
Что делает |
Что проверяет |
Z |
Все байты 0x00 |
Экран гаснет |
G |
Все байты 0xff |
Экран заливается |
A |
byte[0] = 0x01 |
LSB-first, точка в начале буфера |
B |
byte[0] = 0x80 |
MSB-first, другой бит первого байта |
C |
Первые 16 байт 0xff |
Первая строка или первая область кадра |
D |
Первый бит в каждой строке |
Вертикальная линия |
E |
Старший бит в каждой строке |
Проверка порядка битов |
F |
Полосы через строку |
Проверка ориентации и stride |
Пример запуска:
killall -q oled-clock 2>/dev/null || true echo 0 > /sys/class/vtconsole/vtcon1/bind 2>/dev/null || true sh /usr/local/bin/fb_test.sh Z sleep 1 sh /usr/local/bin/fb_test.sh G sleep 1 sh /usr/local/bin/fb_test.sh A sleep 1 sh /usr/local/bin/fb_test.sh F
Если A и B показывают точку не там, где ожидается, проблема может быть в порядке битов при упаковке кадра или в параметрах solomon,* в Device Tree.
Для Buildroot-образа нужно учитывать: fb_test.sh требует python3 на плате. Если Python в rootfs не включен, этот скрипт не подойдет. Тогда лучше использовать C-утилиту oled-clock.
oled-clock: основная userspace-утилита
oled-clock - это C-программа, которая умеет работать в нескольких режимах. Код программы следующий:
/* * oled-clock - live OLED status display for Zynq Mini Rev B * * Каждую секунду: * 1) читает PS XADC температуру (/sys/bus/iio/devices/iio:device0/in_temp0_*) * 2) читает время/дату (localtime_r) * 3) читает uptime + voltages * 4) растрит 128x64 1bpp кадр (шрифты 6x8 и 8x16 в формате ssd1306xled — * «page-mode», т.е. байт описывает 8 вертикальных пикселей одной колонки) * 5) пишет 1024 байта в /dev/fb0 (ssd1307fb ждёт row-major LSB-first MONO10, * driver сам преобразует в SSD1306 page format) * * Layout жёстко выровнен по 8-пиксельным страницам SSD1306, чтобы избежать * визуальных «провалов» на горизонтальных page-boundary: * page 0 (y=0..7) : header 6x8 "zynq mini rev. b" * page 1-2(y=8..23) : big time 8x16 "HH:MM:SS" * page 3 (y=24..31) : date 6x8 "YYYY-MM-DD" * page 4-5(y=32..47) : big T 8x16 "T=XX.X C" * page 6 (y=48..55) : voltages 6x8 "Vi=X.XXV Va=X.XXV" * page 7 (y=56..63) : uptime 6x8 "up HhMMmSSs" */ #define _GNU_SOURCE #include <errno.h> #include <fcntl.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <time.h> #include <unistd.h> #include <sys/ioctl.h> #include <sys/mman.h> #include <sys/stat.h> #include <sys/sysinfo.h> #include <linux/fb.h> #define W 128 #define H 64 #define FB_BYTES (W * H / 8) /* = 1024 */ #define XADC_TEMP_RAW "/sys/bus/iio/devices/iio:device0/in_temp0_raw" #define XADC_TEMP_OFF "/sys/bus/iio/devices/iio:device0/in_temp0_offset" #define XADC_TEMP_SCALE "/sys/bus/iio/devices/iio:device0/in_temp0_scale" #define XADC_VINT_RAW "/sys/bus/iio/devices/iio:device0/in_voltage3_vccpint_raw" #define XADC_VINT_SCALE "/sys/bus/iio/devices/iio:device0/in_voltage3_vccpint_scale" #define XADC_VAUX_RAW "/sys/bus/iio/devices/iio:device0/in_voltage4_vccpaux_raw" #define XADC_VAUX_SCALE "/sys/bus/iio/devices/iio:device0/in_voltage4_vccpaux_scale" /* ------------------------------------------------------------------------- * Стандартный 6x8 ssd1306xled-шрифт. * Формат: 6 байт на символ; каждый байт описывает одну колонку (8 пикселей * по вертикали), bit 0 = верхний пиксель, bit 7 = нижний. * ------------------------------------------------------------------------- */ static const uint8_t FONT6x8[][6] = { {0x00,0x00,0x00,0x00,0x00,0x00}, /* sp */ {0x00,0x00,0x00,0x2F,0x00,0x00}, /* ! */ {0x00,0x00,0x07,0x00,0x07,0x00}, /* " */ {0x00,0x14,0x7F,0x14,0x7F,0x14}, /* # */ {0x00,0x24,0x2A,0x7F,0x2A,0x12}, /* $ */ {0x00,0x23,0x13,0x08,0x64,0x62}, /* % */ {0x00,0x36,0x49,0x55,0x22,0x50}, /* & */ {0x00,0x00,0x05,0x03,0x00,0x00}, /* ' */ {0x00,0x00,0x1C,0x22,0x41,0x00}, /* ( */ {0x00,0x00,0x41,0x22,0x1C,0x00}, /* ) */ {0x00,0x14,0x08,0x3E,0x08,0x14}, /* * */ {0x00,0x08,0x08,0x3E,0x08,0x08}, /* + */ {0x00,0x00,0x00,0xA0,0x60,0x00}, /* , */ {0x00,0x08,0x08,0x08,0x08,0x08}, /* - */ {0x00,0x00,0x60,0x60,0x00,0x00}, /* . */ {0x00,0x20,0x10,0x08,0x04,0x02}, /* / */ {0x00,0x3E,0x51,0x49,0x45,0x3E}, /* 0 */ {0x00,0x00,0x42,0x7F,0x40,0x00}, /* 1 */ {0x00,0x42,0x61,0x51,0x49,0x46}, /* 2 */ {0x00,0x21,0x41,0x45,0x4B,0x31}, /* 3 */ {0x00,0x18,0x14,0x12,0x7F,0x10}, /* 4 */ {0x00,0x27,0x45,0x45,0x45,0x39}, /* 5 */ {0x00,0x3C,0x4A,0x49,0x49,0x30}, /* 6 */ {0x00,0x01,0x71,0x09,0x05,0x03}, /* 7 */ {0x00,0x36,0x49,0x49,0x49,0x36}, /* 8 */ {0x00,0x06,0x49,0x49,0x29,0x1E}, /* 9 */ {0x00,0x00,0x36,0x36,0x00,0x00}, /* : */ {0x00,0x00,0x56,0x36,0x00,0x00}, /* ; */ {0x00,0x08,0x14,0x22,0x41,0x00}, /* < */ {0x00,0x14,0x14,0x14,0x14,0x14}, /* = */ {0x00,0x00,0x41,0x22,0x14,0x08}, /* > */ {0x00,0x02,0x01,0x51,0x09,0x06}, /* ? */ {0x00,0x32,0x49,0x59,0x51,0x3E}, /* @ */ {0x00,0x7C,0x12,0x11,0x12,0x7C}, /* A */ {0x00,0x7F,0x49,0x49,0x49,0x36}, /* B */ {0x00,0x3E,0x41,0x41,0x41,0x22}, /* C */ {0x00,0x7F,0x41,0x41,0x22,0x1C}, /* D */ {0x00,0x7F,0x49,0x49,0x49,0x41}, /* E */ {0x00,0x7F,0x09,0x09,0x09,0x01}, /* F */ {0x00,0x3E,0x41,0x49,0x49,0x7A}, /* G */ {0x00,0x7F,0x08,0x08,0x08,0x7F}, /* H */ {0x00,0x00,0x41,0x7F,0x41,0x00}, /* I */ {0x00,0x20,0x40,0x41,0x3F,0x01}, /* J */ {0x00,0x7F,0x08,0x14,0x22,0x41}, /* K */ {0x00,0x7F,0x40,0x40,0x40,0x40}, /* L */ {0x00,0x7F,0x02,0x0C,0x02,0x7F}, /* M */ {0x00,0x7F,0x04,0x08,0x10,0x7F}, /* N */ {0x00,0x3E,0x41,0x41,0x41,0x3E}, /* O */ {0x00,0x7F,0x09,0x09,0x09,0x06}, /* P */ {0x00,0x3E,0x41,0x51,0x21,0x5E}, /* Q */ {0x00,0x7F,0x09,0x19,0x29,0x46}, /* R */ {0x00,0x46,0x49,0x49,0x49,0x31}, /* S */ {0x00,0x01,0x01,0x7F,0x01,0x01}, /* T */ {0x00,0x3F,0x40,0x40,0x40,0x3F}, /* U */ {0x00,0x1F,0x20,0x40,0x20,0x1F}, /* V */ {0x00,0x3F,0x40,0x38,0x40,0x3F}, /* W */ {0x00,0x63,0x14,0x08,0x14,0x63}, /* X */ {0x00,0x07,0x08,0x70,0x08,0x07}, /* Y */ {0x00,0x61,0x51,0x49,0x45,0x43}, /* Z */ {0x00,0x00,0x7F,0x41,0x41,0x00}, /* [ */ {0x00,0x55,0x2A,0x55,0x2A,0x55}, /* \ */ {0x00,0x00,0x41,0x41,0x7F,0x00}, /* ] */ {0x00,0x04,0x02,0x01,0x02,0x04}, /* ^ */ {0x00,0x40,0x40,0x40,0x40,0x40}, /* _ */ {0x00,0x00,0x01,0x02,0x04,0x00}, /* ` */ {0x00,0x20,0x54,0x54,0x54,0x78}, /* a */ {0x00,0x7F,0x48,0x44,0x44,0x38}, /* b */ {0x00,0x38,0x44,0x44,0x44,0x20}, /* c */ {0x00,0x38,0x44,0x44,0x48,0x7F}, /* d */ {0x00,0x38,0x54,0x54,0x54,0x18}, /* e */ {0x00,0x08,0x7E,0x09,0x01,0x02}, /* f */ {0x00,0x18,0xA4,0xA4,0xA4,0x7C}, /* g */ {0x00,0x7F,0x08,0x04,0x04,0x78}, /* h */ {0x00,0x00,0x44,0x7D,0x40,0x00}, /* i */ {0x00,0x40,0x80,0x84,0x7D,0x00}, /* j */ {0x00,0x7F,0x10,0x28,0x44,0x00}, /* k */ {0x00,0x00,0x41,0x7F,0x40,0x00}, /* l */ {0x00,0x7C,0x04,0x18,0x04,0x78}, /* m */ {0x00,0x7C,0x08,0x04,0x04,0x78}, /* n */ {0x00,0x38,0x44,0x44,0x44,0x38}, /* o */ {0x00,0xFC,0x24,0x24,0x24,0x18}, /* p */ {0x00,0x18,0x24,0x24,0x18,0xFC}, /* q */ {0x00,0x7C,0x08,0x04,0x04,0x08}, /* r */ {0x00,0x48,0x54,0x54,0x54,0x20}, /* s */ {0x00,0x04,0x3F,0x44,0x40,0x20}, /* t */ {0x00,0x3C,0x40,0x40,0x20,0x7C}, /* u */ {0x00,0x1C,0x20,0x40,0x20,0x1C}, /* v */ {0x00,0x3C,0x40,0x30,0x40,0x3C}, /* w */ {0x00,0x44,0x28,0x10,0x28,0x44}, /* x */ {0x00,0x1C,0xA0,0xA0,0xA0,0x7C}, /* y */ {0x00,0x44,0x64,0x54,0x4C,0x44}, /* z */ }; #define FONT6x8_FIRST 0x20 /* ' ' */ #define FONT6x8_LAST 0x7A /* 'z' */ /* ------------------------------------------------------------------------- * Стандартный 8x16 ssd1306xled-шрифт (старый/правый half stacked): * bytes 0..7 — top page (y = 0..7) для каждой из 8 колонок * bytes 8..15 — bot page (y = 8..15) для каждой из 8 колонок * Каждый байт — 8 вертикальных пикселей; bit 0 = top. * Здесь храним только символы '0'..'9', ':', '.' и '-' — достаточно для * времени и температурной строки (включая дробную часть и отрицательные * значения). * ------------------------------------------------------------------------- */ static const uint8_t FONT8x16[][16] = { /* '0' */ {0x00,0xE0,0x10,0x08,0x08,0x10,0xE0,0x00, 0x00,0x0F,0x10,0x20,0x20,0x10,0x0F,0x00}, /* '1' */ {0x00,0x10,0x10,0xF8,0x00,0x00,0x00,0x00, 0x00,0x20,0x20,0x3F,0x20,0x20,0x00,0x00}, /* '2' */ {0x00,0x70,0x08,0x08,0x08,0x88,0x70,0x00, 0x00,0x30,0x28,0x24,0x22,0x21,0x30,0x00}, /* '3' */ {0x00,0x30,0x08,0x88,0x88,0x48,0x30,0x00, 0x00,0x18,0x20,0x20,0x20,0x11,0x0E,0x00}, /* '4' */ {0x00,0x00,0xC0,0x20,0x10,0xF8,0x00,0x00, 0x00,0x07,0x04,0x24,0x24,0x3F,0x24,0x00}, /* '5' */ {0x00,0xF8,0x08,0x88,0x88,0x08,0x08,0x00, 0x00,0x19,0x21,0x20,0x20,0x11,0x0E,0x00}, /* '6' */ {0x00,0xE0,0x10,0x88,0x88,0x18,0x00,0x00, 0x00,0x0F,0x11,0x20,0x20,0x11,0x0E,0x00}, /* '7' */ {0x00,0x38,0x08,0x08,0xC8,0x38,0x08,0x00, 0x00,0x00,0x00,0x3F,0x00,0x00,0x00,0x00}, /* '8' */ {0x00,0x70,0x88,0x08,0x08,0x88,0x70,0x00, 0x00,0x1C,0x22,0x21,0x21,0x22,0x1C,0x00}, /* '9' */ {0x00,0xE0,0x10,0x08,0x08,0x10,0xE0,0x00, 0x00,0x00,0x31,0x22,0x22,0x11,0x0F,0x00}, /* ':' */ {0x00,0x00,0x00,0xC0,0xC0,0x00,0x00,0x00, 0x00,0x00,0x00,0x30,0x30,0x00,0x00,0x00}, /* '.' */ {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x30,0x30,0x00,0x00,0x00}, /* '-' */ {0x00,0x00,0x80,0x80,0x80,0x80,0x80,0x00, 0x00,0x00,0x01,0x01,0x01,0x01,0x01,0x00}, }; /* отображение '0'..'9'->[0..9], ':'->10, '.'->11, '-'->12. * Возвращает индекс или -1 если символ не поддержан. */ static int idx_8x16(char c) { if (c >= '0' && c <= '9') return c - '0'; if (c == ':') return 10; if (c == '.') return 11; if (c == '-') return 12; return -1; } /* ------------------------------------------------------------------------- * Framebuffer: row-major bool grid 64x128, пакуется в 1024 байта LSB-first. * ------------------------------------------------------------------------- */ static uint8_t pix[H][W]; static void clear_fb(void) { memset(pix, 0, sizeof pix); } static inline void set_px(int x, int y) { if ((unsigned)x < W && (unsigned)y < H) pix[y][x] = 1; } static void hline(int y, int x0, int x1) __attribute__((unused)); static void hline(int y, int x0, int x1) { for (int x = x0; x <= x1; x++) set_px(x, y); } /* Рисуем 6x8 символ в (x, y). y ДОЛЖЕН быть кратен 8, иначе появится * page-split (буква пересечёт границу страниц SSD1306). */ static void draw_6x8(char c, int x, int y) { unsigned uc = (unsigned char)c; if (uc < FONT6x8_FIRST || uc > FONT6x8_LAST) uc = '?'; const uint8_t *gl = FONT6x8[uc - FONT6x8_FIRST]; for (int col = 0; col < 6; col++) { uint8_t byte = gl[col]; for (int row = 0; row < 8; row++) if (byte & (1u << row)) set_px(x + col, y + row); } } static void text_6x8(const char *s, int x, int y) { for (; *s; s++) { draw_6x8(*s, x, y); x += 6; } } static int width_6x8(const char *s) { return (int)strlen(s) * 6; } static void center_6x8(const char *s, int y) { text_6x8(s, (W - width_6x8(s)) / 2, y); } /* Рисуем 8x16 символ в (x, y). y ДОЛЖЕН быть кратен 8 (идеально кратен 16), * иначе появится разрыв между верхней и нижней половиной глифа. */ static void draw_8x16(char c, int x, int y) { int i = idx_8x16(c); if (i < 0) { /* для пробела/неизвестных просто оставим пустое место */ return; } const uint8_t *gl = FONT8x16[i]; /* top page */ for (int col = 0; col < 8; col++) { uint8_t byte = gl[col]; for (int row = 0; row < 8; row++) if (byte & (1u << row)) set_px(x + col, y + row); } /* bottom page */ for (int col = 0; col < 8; col++) { uint8_t byte = gl[8 + col]; for (int row = 0; row < 8; row++) if (byte & (1u << row)) set_px(x + col, y + 8 + row); } } static void text_8x16(const char *s, int x, int y) { for (; *s; s++) { draw_8x16(*s, x, y); x += 8; } } static int width_8x16(const char *s) { return (int)strlen(s) * 8; } static void center_8x16(const char *s, int y) { text_8x16(s, (W - width_8x16(s)) / 2, y); } /* ------------------------------------------------------------------------- * Сборка кадра /dev/fb0. * * ssd1307fb (mainline) хранит screen_buffer как row-major mono с LSB-first * упаковкой битов внутри байта: * pixel(x, y) = (vmem[y * line_length + x/8] >> (x % 8)) & 1 * * Драйвер использует fb_deferred_io, поэтому write()-only через char-устройство * НЕ всегда триггерит обновление контроллера: правильный способ — mmap() * страничного буфера + msync(MS_INVALIDATE), чтобы кадр поехал по I2C. * ------------------------------------------------------------------------- */ static uint8_t *g_fbmem = NULL; /* указатель на mmap-region */ static size_t g_fbsize = 0; /* реальный line_length * yres */ static int g_line_len = W / 8; /* line_length из ядра */ static int fb_setup(int fd) { struct fb_var_screeninfo v; struct fb_fix_screeninfo f; if (ioctl(fd, FBIOGET_VSCREENINFO, &v) == 0 && ioctl(fd, FBIOGET_FSCREENINFO, &f) == 0) { g_line_len = (int)f.line_length; g_fbsize = (size_t)f.line_length * v.yres_virtual; if (g_fbsize < (size_t)FB_BYTES) g_fbsize = FB_BYTES; } else { g_line_len = W / 8; g_fbsize = FB_BYTES; } g_fbmem = mmap(NULL, g_fbsize, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (g_fbmem == MAP_FAILED) { g_fbmem = NULL; return -1; } return 0; } /* Параметр: 0 = LSB-first within byte (стандарт ssd1307fb mainline), * 1 = MSB-first within byte (на случай других ядер/прошивок). */ static int g_bit_order_msb = 0; /* Параметр: путь записи кадра. ssd1307fb через defio регистрирует * fb_sys_write_with_damage — это самый надёжный способ прокинуть кадр в * контроллер. mmap+msync формально тоже работает, но требует чтобы ядро * выловило write-fault — что не всегда устойчиво на 6.6.51 sysmem mappings. */ static void pack_buf(uint8_t *buf, int stride) { memset(buf, 0, stride * H); for (int y = 0; y < H; y++) { uint8_t *row = buf + y * stride; for (int X = 0; X < W / 8; X++) { uint8_t byte = 0; if (g_bit_order_msb) { for (int b = 0; b < 8; b++) if (pix[y][X * 8 + b]) byte |= (uint8_t)(0x80u >> b); } else { for (int b = 0; b < 8; b++) if (pix[y][X * 8 + b]) byte |= (uint8_t)(1u << b); } row[X] = byte; } } } static void flush_to_fb(int fd) { uint8_t buf[FB_BYTES]; int stride = (g_line_len > 0) ? g_line_len : (W / 8); pack_buf(buf, stride); /* mmap путь: пишем в страничный буфер и просим ядро инвалидировать. */ if (g_fbmem) { memset(g_fbmem, 0, g_fbsize); memcpy(g_fbmem, buf, (size_t)stride * H); msync(g_fbmem, g_fbsize, MS_SYNC | MS_INVALIDATE); } /* Дублируем write(): defio_write гарантированно вызовет damage_range. */ if (lseek(fd, 0, SEEK_SET) == (off_t)-1) return; ssize_t left = (ssize_t)stride * H, off = 0; while (left > 0) { ssize_t n = write(fd, buf + off, left); if (n <= 0) { if (errno == EINTR) continue; break; } left -= n; off += n; } } static void fill_test_pattern(const char *name) { clear_fb(); if (!strcmp(name, "Z")) { /* all black */ } else if (!strcmp(name, "G")) { for (int y = 0; y < H; y++) for (int x = 0; x < W; x++) set_px(x, y); } else if (!strcmp(name, "A")) { set_px(0, 0); /* единичный pixel */ } else if (!strcmp(name, "D")) { for (int y = 0; y < H; y++) set_px(0, y); /* левая колонка */ } else if (!strcmp(name, "F")) { for (int y = 0; y < H; y += 2) for (int x = 0; x < W; x++) set_px(x, y); } else if (!strcmp(name, "T")) { /* угловые маркеры + диагональ */ for (int i = 0; i < 8; i++) { set_px(i, 0); set_px(0, i); } for (int i = 0; i < 8; i++) { set_px(W - 1 - i, 0); set_px(W - 1, i); } for (int i = 0; i < 8; i++) { set_px(i, H - 1); set_px(0, H - 1 - i); } for (int i = 0; i < 8; i++) { set_px(W - 1 - i, H - 1); set_px(W - 1, H - 1 - i); } for (int i = 0; i < 64; i++) set_px(i, i); } else if (!strcmp(name, "R")) { /* "ABC" слева сверху простым 6x8 */ text_6x8("ABC", 0, 0); } else if (!strcmp(name, "H")) { /* "HELLO" по центру первой страницы */ center_6x8("HELLO", 0); } else if (!strcmp(name, "L")) { /* L-маркер: гориз (0..7, 0) + верт (0, 0..7) + одиночка (16, 4) */ for (int i = 0; i < 8; i++) set_px(i, 0); for (int i = 0; i < 8; i++) set_px(0, i); set_px(16, 4); } else if (!strcmp(name, "B")) { /* Рамка по всему экрану — 4 линии шириной 1px */ for (int x = 0; x < W; x++) { set_px(x, 0); set_px(x, H - 1); } for (int y = 0; y < H; y++) { set_px(0, y); set_px(W - 1, y); } } else if (!strcmp(name, "8")) { /* Одна цифра '0' шрифтом 8x16 в углу (0,0) */ text_8x16("0", 0, 0); } else if (!strcmp(name, "9")) { /* "00:00" 8x16 в углу (0,0) */ text_8x16("00:00", 0, 0); } else if (!strcmp(name, "P")) { /* Диагональ из 8 одиночных точек: (0,0),(8,8),(16,16),...,(56,56) */ for (int i = 0; i < 8; i++) set_px(i * 8, i * 8); } else if (!strcmp(name, "Q")) { /* Точки на всех пересечениях page-boundary y=0,8,16,...,56 */ for (int y = 0; y < H; y += 8) set_px(0, y); } else { text_6x8("?pattern", 0, 0); } } /* ------------------------------------------------------------------------- * sysfs helpers * ------------------------------------------------------------------------- */ static int read_int_file(const char *path) { int fd = open(path, O_RDONLY); if (fd < 0) return 0; char b[32]; ssize_t n = read(fd, b, sizeof b - 1); close(fd); if (n <= 0) return 0; b[n] = 0; return atoi(b); } static double read_double_file(const char *path) { int fd = open(path, O_RDONLY); if (fd < 0) return 0.0; char b[64]; ssize_t n = read(fd, b, sizeof b - 1); close(fd); if (n <= 0) return 0.0; b[n] = 0; return atof(b); } static int g_temp_offset = 0; static double g_temp_scale = 0.0; static double g_vint_scale = 0.0; static double g_vaux_scale = 0.0; static void sensors_init(void) { g_temp_offset = read_int_file(XADC_TEMP_OFF); g_temp_scale = read_double_file(XADC_TEMP_SCALE); g_vint_scale = read_double_file(XADC_VINT_SCALE); g_vaux_scale = read_double_file(XADC_VAUX_SCALE); } static double temp_celsius(void) { int raw = read_int_file(XADC_TEMP_RAW); return (raw + g_temp_offset) * g_temp_scale / 1000.0; } static double vint_volts(void) { return read_int_file(XADC_VINT_RAW) * g_vint_scale / 1000.0; } static double vaux_volts(void) { return read_int_file(XADC_VAUX_RAW) * g_vaux_scale / 1000.0; } /* ------------------------------------------------------------------------- * Отвязываем fbcon от /dev/fb0, чтобы kernel console не накладывался поверх. * ------------------------------------------------------------------------- */ static void detach_fbcon(void) { for (int i = 0; i < 8; i++) { char path[64]; snprintf(path, sizeof path, "/sys/class/vtconsole/vtcon%d/bind", i); int fd = open(path, O_WRONLY); if (fd < 0) continue; (void)write(fd, "0", 1); close(fd); } } static void print_fb_info(int fd) { struct fb_var_screeninfo v; struct fb_fix_screeninfo f; if (ioctl(fd, FBIOGET_VSCREENINFO, &v) == 0) { fprintf(stderr, "var: xres=%u yres=%u xres_virtual=%u yres_virtual=%u bpp=%u " "red.length=%u grayscale=%u rotate=%u\n", v.xres, v.yres, v.xres_virtual, v.yres_virtual, v.bits_per_pixel, v.red.length, v.grayscale, v.rotate); } if (ioctl(fd, FBIOGET_FSCREENINFO, &f) == 0) { fprintf(stderr, "fix: id=%.16s type=%u visual=%u line_length=%u smem_len=%u\n", f.id, f.type, f.visual, f.line_length, f.smem_len); } } static void usage(const char *p) { fprintf(stderr, "usage: %s [options]\n" " --fb PATH framebuffer device (default /dev/fb0)\n" " --info print FBIOGET_VSCREENINFO/FSCREENINFO and exit\n" " --msb pack pixels MSB-first within byte\n" " --lsb pack pixels LSB-first within byte (default)\n" " --no-mmap do not mmap, write() only\n" " --no-detach do not detach fbcon\n" " --test NAME draw single pattern and exit\n" " Z=black G=white A=pixel(0,0) D=left-col F=stripes\n" " T=corners+diag R=\"ABC\"6x8 H=HELLO6x8 L=L-corner\n" " B=border 8=\"0\"8x16 9=\"00:00\"8x16\n" " P=8 points on (i*8,i*8) Q=points at every y=0,8,...,56\n", p); } int main(int argc, char **argv) { const char *fb_path = "/dev/fb0"; const char *test = NULL; int do_info = 0, use_mmap = 1, detach = 1; for (int i = 1; i < argc; i++) { if (!strcmp(argv[i], "--fb") && i + 1 < argc) fb_path = argv[++i]; else if (!strcmp(argv[i], "--info")) do_info = 1; else if (!strcmp(argv[i], "--msb")) g_bit_order_msb = 1; else if (!strcmp(argv[i], "--lsb")) g_bit_order_msb = 0; else if (!strcmp(argv[i], "--no-mmap")) use_mmap = 0; else if (!strcmp(argv[i], "--no-detach")) detach = 0; else if (!strcmp(argv[i], "--test") && i + 1 < argc) test = argv[++i]; else if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) { usage(argv[0]); return 0; } else if (argv[i][0] != '-') fb_path = argv[i]; else { usage(argv[0]); return 2; } } int fb_fd = open(fb_path, O_RDWR); if (fb_fd < 0) { fprintf(stderr, "open(%s): %s\n", fb_path, strerror(errno)); return 1; } if (do_info) { print_fb_info(fb_fd); close(fb_fd); return 0; } sensors_init(); if (detach) detach_fbcon(); if (use_mmap && fb_setup(fb_fd) != 0) fprintf(stderr, "mmap fallback: используем только write()\n"); if (test) { print_fb_info(fb_fd); fill_test_pattern(test); flush_to_fb(fb_fd); fprintf(stderr, "pattern '%s' drawn (mmap=%d msb=%d)\n", test, g_fbmem ? 1 : 0, g_bit_order_msb); close(fb_fd); return 0; } while (1) { time_t now = time(NULL); struct tm tm; localtime_r(&now, &tm); struct sysinfo si; sysinfo(&si); clear_fb(); char line[64]; /* Page 0 (y=0..7): header — 6x8 */ center_6x8("zynq mini rev. b", 0); /* Pages 1-2 (y=8..23): HH:MM:SS — 8x16, ровно две выровненных страницы */ snprintf(line, sizeof line, "%02d:%02d:%02d", tm.tm_hour, tm.tm_min, tm.tm_sec); center_8x16(line, 8); /* Page 3 (y=24..31): YYYY-MM-DD — 6x8 */ snprintf(line, sizeof line, "%04d-%02d-%02d", tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday); center_6x8(line, 24); /* Pages 4-5 (y=32..47): T=XX.X C — 8x16. В 8x16 поддержаны цифры, * ':', '.' и '-', поэтому "%4.1f" (вкл. отрицательные значения и * дробную часть) рисуется целиком; подпись "T=" и "C" — 6x8. */ if (g_temp_scale > 0.0) { double tC = temp_celsius(); snprintf(line, sizeof line, "%4.1f", tC); /* Подпись слева 6x8, цифры по центру справа 8x16. * "%4.1f" даёт 4 символа для значений 0..99.9 ("XX.X" или " X.X") * и 5 символов для |T|>=100 — 5*8 = 40 px по ширине. */ text_6x8("T=", 8, 36); text_8x16(line, 28, 32); text_6x8("C", 28 + 40 + 4, 36); } else { center_6x8("T = N/A", 36); } /* Page 6 (y=48..55): voltages — 6x8. * Шрифт 6x8 даёт 128/6 = 21 символ на строку, поэтому используем * укороченные подписи "Vi=" / "Va=", чтобы строка гарантированно * влезала и не уезжала за край при центрировании. */ if (g_vint_scale > 0.0 && g_vaux_scale > 0.0) { snprintf(line, sizeof line, "Vi=%4.2fV Va=%4.2fV", vint_volts(), vaux_volts()); center_6x8(line, 48); } else { center_6x8("PS XADC", 48); } /* Page 7 (y=56..63): uptime — 6x8 */ long s = (long)si.uptime; long h = s / 3600, m = (s % 3600) / 60, sec = s % 60; snprintf(line, sizeof line, "up %ldh%02ldm%02lds", h, m, sec); center_6x8(line, 56); flush_to_fb(fb_fd); /* Просыпаемся на границе следующей секунды — без дрейфа. */ struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); struct timespec req = { .tv_sec = 0, .tv_nsec = 1000000000L - ts.tv_nsec, }; nanosleep(&req, NULL); } close(fb_fd); return 0; }
Примерный набор режимов:
oled-clock --info oled-clock --test Z oled-clock --test A oled-clock --test F oled-clock --test T oled-clock --test A --msb oled-clock --test A --no-mmap oled-clock
Режимы:
Режим |
Назначение |
--info |
Показать параметры framebuffer через FBIOGET_VSCREENINFO и FBIOGET_FSCREENINFO |
--test <NAME> |
Нарисовать один тестовый паттерн |
--msb |
Упаковывать биты в байте в обратном порядке |
--no-mmap |
Не использовать mmap, писать через write() |
без аргументов |
Запустить демон часов и системной информации |
Режим --info: проверяем framebuffer
Первый запуск лучше делать с --info: /usr/local/bin/oled-clock --info
Ожидаемый вывод концептуально такой:
var: xres=128 yres=64 ... bpp=1 ... grayscale=1 fix: id=SSD1307 ... line_length=16 smem_len=1024
Поля, которые важно проверить:
Поле |
Ожидание |
Что означает отклонение |
|
128 |
Неверная геометрия в DTS или не тот framebuffer |
|
64 |
Неверная геометрия в DTS |
|
1 |
Ожидается monochrome framebuffer |
|
16 |
128 пикселей / 8 = 16 байт на строку |
|
1024 |
128 * 64 / 8 |
|
SSD1307 или похожее |
Привязался драйвер ssd1307fb |
Если --info не может открыть /dev/fb0, надо вернуться к проверкам ядра. Если /dev/fb0 есть, но геометрия не 128x64, значит проблема не в userspace, а в DTS или в выбранном framebuffer.
Тестовые паттерны oled-clock
После --info проверяем картинку:
/usr/local/bin/oled-clock --test Z sleep 1 /usr/local/bin/oled-clock --test G sleep 1 /usr/local/bin/oled-clock --test A sleep 1 /usr/local/bin/oled-clock --test F sleep 1 /usr/local/bin/oled-clock --test T
Рекомендуемый набор паттернов:
Паттерн |
Смысл |
Z |
Очистить экран |
G |
Полная заливка |
A |
Один пиксель или один бит в начале буфера |
D |
Вертикальная линия |
F |
Горизонтальные полосы |
T |
Углы и диагональ |
R |
Короткий текст |
H |
Центрированная строка |
B |
Рамка по периметру |
8 |
Крупная цифра шрифтом 8x16 |
9 |
Строка времени |
P |
Диагональные точки |
Q |
Точки по page-boundary SSD1306 |
Если паттерн есть в памяти, но экран не обновляется, нужно проверить конфликт с fbcon:
cat /sys/class/vtconsole/vtcon1/bind 2>/dev/null
Если там 1, консоль ядра может перерисовывать экран поверх пользовательского кадра. Для режима тестов лучше отвязать fbcon:
echo 0 > /sys/class/vtconsole/vtcon1/bind
mmap или write
Для /dev/fb0 есть два практических способа записи:
Способ |
Плюсы |
Минусы |
mmap |
Быстрый доступ к framebuffer-памяти |
Нужно правильно учитывать line_length и размер |
write |
Простой и предсказуемый путь |
Может быть медленнее |
mmap + write fallback |
Наиболее надежно для отладки |
Дублирует работу |
Для маленького SSD1306 разница по CPU обычно не критична, потому что узким местом становится I2C, а не копирование 1024 байт в userspace. Но mmap удобен для демона, который обновляет экран раз в секунду. При проблемах с обновлением можно запустить тест без mmap:
/usr/local/bin/oled-clock --test A --no-mmap
Если через --no-mmap картинка появляется стабильнее, значит проблема может быть в deferred I/O, msync, размере mmap-области или конфликте с fbcon.
Демон часов и системной информации
Без --test программа работает как демон: раз в секунду собирает данные, рисует кадр и отправляет его в /dev/fb0.
Пример запуска:
killall -q oled-clock 2>/dev/null echo 0 > /sys/class/vtconsole/vtcon1/bind 2>/dev/null || true nohup /usr/local/bin/oled-clock >/tmp/oled.log 2>&1 & sleep 1 pgrep -a oled-clock tail -20 /tmp/oled.log
Типовой набор данных для вывода:
Строка на OLED |
Источник в Linux |
Время |
localtime_r() |
Дата |
localtime_r() |
Uptime |
sysinfo() |
Температура кристалла |
XADC через /sys/bus/iio/devices/iio:device*/in_temp0_* |
Напряжение VCCPINT |
in_voltage*_vccpint_* |
Напряжение VCCPAUX |
in_voltage*_vccpaux_* |
Hostname |
/proc/sys/kernel/hostname |
IP-адрес |
ioctl(SIOCGIFADDR) или парсинг /proc/net/fib_trie |
Load average |
/proc/loadavg |
Частота обновления |
внутренний таймер программы |
Минимальный макет экрана:

Для 128x64 лучше не пытаться выводить слишком много. На таком дисплее полезнее 4-6 коротких строк, чем полная системная сводка мелким шрифтом.
Температура кристалла через XADC
На Zynq температуру PS/кристалла обычно можно получить через IIO/XADC. На разных ядрах и DTS путь может отличаться, поэтому утилита должна не жестко открывать один файл, а искать подходящее устройство.
Типовая проверка на плате find /sys/bus/iio/devices -type f | grep -E 'temp|voltage|scale|offset'
Возможные файлы:
/sys/bus/iio/devices/iio:device0/in_temp0_raw /sys/bus/iio/devices/iio:device0/in_temp0_scale /sys/bus/iio/devices/iio:device0/in_temp0_offset
Расчет обычно имеет вид temperature = (raw + offset) * scale / 1000
Но конкретные единицы лучше проверить по sysfs-атрибутам и драйверу XADC. Для статьи достаточно заложить безопасную логику: если XADC найден - вывести температуру, иначе вывести T=N/A
Отсутствие температуры на OLED означает, что нужен XADC в DTS и соответствующий драйвер в ядре.
Cross-компиляция oled-clock
Userspace-утилиту нужно собирать тем же toolchain, которым Buildroot собрал rootfs. Это снижает риск несовместимости libc и ABI.
Задаем пути:
export WORK=~/devel/xilinx/projects/zynq_mini_oled/linux/ export BR_OUT="$WORK/br-output" export PATH="$BR_OUT/host/bin:$PATH"
Находим toolchain:
ls "$BR_OUT/host/bin" | grep -E 'arm.*gcc$'
Для Buildroot glibc ARM hard-float имя обычно похоже на arm-buildroot-linux-gnueabi-gcc
Создадим директорию и туда положим файл oled-clock.c:
mkdir -p /home/megalloid/devel/xilinx/projects/zynq_mini_oled/linux/userspace/oled-clock/
Сборка вручную:
arm-buildroot-linux-gnueabi-gcc \ -O2 -Wall -Wextra \ -o oled-clock oled-clock.c
Проверка бинарника на хосте:
# file oled-clock ELF 32-bit LSB executable, ARM, EABI5, dynamically linked
Если rootfs собран на glibc, динамически связанный бинарник подходит. Если rootfs собран на musl или uClibc, нужно использовать именно toolchain этого Buildroot output.
Вариант через Makefile:
CC ?= gcc CFLAGS ?= -O2 -Wall -Wextra LDFLAGS ?= all: oled-clock oled-clock: oled-clock.c $(CC) $(CFLAGS) -o $@ $< $(LDFLAGS) clean: rm -f oled-clock
Сборка:
make clean make CC="$BR_OUT/host/bin/arm-buildroot-linux-gnueabi-gcc"
Или через префикс если Makefile поддерживает CROSS_COMPILE:
make CROSS_COMPILE="$BR_OUT/host/bin/arm-buildroot-linux-gnueabi-"
Доставка бинарника на плату
Если сеть и SSH уже работают:
scp oled-clock root@192.168.2.142:/usr/local/bin/oled-clock ssh root@192.168.2.142 'chmod +x /usr/local/bin/oled-clock'
Если scp не работает из-за отсутствия sftp-server, можно использовать поток через ssh:
cat oled-clock | ssh root@192.168.2.145 \ 'cat > /usr/local/bin/oled-clock && chmod +x /usr/local/bin/oled-clock'
Если сети нет, варианты:
Положить бинарник на FAT-раздел SD.
Добавить его в
rootfs_overlay.Добавить userspace-пакет в Buildroot.
Временно скопировать через UART, если есть подходящие инструменты, но это неудобно.
Для быстрой отладки удобно использовать deploy.sh.
deploy.sh: быстрый цикл “собрал - залил - проверил”
Скрипт выглядит следующим образом:
#!/bin/bash # deploy.sh — заливает свежий oled-clock на плату и помогает гонять диагностики. # # Запускать с твоего хоста (там, где есть sshpass, ip связь с платой): # bash linux/userspace/oled-clock/deploy.sh # залить и запустить (LSB+mmap) # bash linux/userspace/oled-clock/deploy.sh stop # остановить демон # bash linux/userspace/oled-clock/deploy.sh info # FBIOGET_*SCREENINFO # bash linux/userspace/oled-clock/deploy.sh test A # один паттерн (Z,G,A,D,F,T,R,H,L,B,8,9,P,Q) # bash linux/userspace/oled-clock/deploy.sh test A --msb # тот же паттерн, MSB-first # bash linux/userspace/oled-clock/deploy.sh test A --no-mmap # тот же, только write() # bash linux/userspace/oled-clock/deploy.sh run --msb # запустить часы с MSB-форматом # bash linux/userspace/oled-clock/deploy.sh run --no-mmap # запустить только через write() # bash linux/userspace/oled-clock/deploy.sh console # OLED в режим Linux console + getty (USB-клавиатура) # bash linux/userspace/oled-clock/deploy.sh clock # OLED в режим часов (по умолчанию) # bash linux/userspace/oled-clock/deploy.sh shell '<cmd>' # выполнить <cmd> на плате set -eu HOST=${OLED_HOST:-root@192.168.2.142} PASS=${OLED_PASS:-root} SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) BIN="$SCRIPT_DIR/oled-clock" SWITCH_CLK="$SCRIPT_DIR/oled-clock-start.sh" SWITCH_CON="$SCRIPT_DIR/oled-console.sh" ssh_cmd() { sshpass -p "$PASS" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ -o LogLevel=ERROR "$HOST" "$@" } copy_to_remote() { local src="$1" dst="$2" sshpass -p "$PASS" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ -o LogLevel=ERROR "$HOST" "cat > '$dst' && chmod +x '$dst'" < "$src" } ensure_module_and_bin() { ssh_cmd 'mkdir -p /usr/local/bin && modprobe i2c-master-axi 2>/dev/null; \ ls /dev/fb0 /dev/i2c-0 2>&1 | head' if [ "${SKIP_UPLOAD:-0}" != 1 ] && [ -x "$BIN" ]; then copy_to_remote "$BIN" /usr/local/bin/oled-clock fi # Скрипты-переключатели режимов (clock ↔ Linux-console на /dev/fb0). [ -r "$SWITCH_CLK" ] && copy_to_remote "$SWITCH_CLK" /usr/local/bin/oled-clock-start [ -r "$SWITCH_CON" ] && copy_to_remote "$SWITCH_CON" /usr/local/bin/oled-console } case "${1-deploy}" in stop) ssh_cmd 'killall -q oled-clock 2>/dev/null; sleep 0.3; pgrep -laf oled-clock || echo stopped' ;; info) ensure_module_and_bin ssh_cmd 'killall -q oled-clock 2>/dev/null; sleep 0.2; /usr/local/bin/oled-clock --info' ;; test) PATTERN=${2:-A} shift 2 2>/dev/null || shift 1 ensure_module_and_bin ssh_cmd "killall -q oled-clock 2>/dev/null; sleep 0.2; /usr/local/bin/oled-clock --test $PATTERN $*" ;; run) shift ensure_module_and_bin # передаём дополнительные флаги в демона ssh_cmd "killall -q oled-clock 2>/dev/null; sleep 0.2; \ nohup /usr/local/bin/oled-clock $* >/tmp/oled.log 2>&1 & \ sleep 0.6; pgrep -laf oled-clock; cat /tmp/oled.log" ;; shell) shift ssh_cmd "$*" ;; console) ensure_module_and_bin ssh_cmd '/usr/local/bin/oled-console' ;; clock) ensure_module_and_bin ssh_cmd '/usr/local/bin/oled-clock-start' ;; deploy|"") ensure_module_and_bin ssh_cmd 'killall -q oled-clock 2>/dev/null; sleep 0.2; \ nohup /usr/local/bin/oled-clock >/tmp/oled.log 2>&1 & \ sleep 0.6; pgrep -laf oled-clock; cat /tmp/oled.log' ;; *) echo "unknown subcommand: $1" exit 1 ;; esac
deploy.sh работает на хосте и выполняет типовые действия по SSH:
export OLED_HOST=root@192.168.2.145 export OLED_PASS=root bash deploy.sh info bash deploy.sh test A bash deploy.sh test F --msb bash deploy.sh run bash deploy.sh stop bash deploy.sh console bash deploy.sh clock bash deploy.sh shell 'dmesg | grep -i i2c'
Назначение подкоманд:
Подкоманда |
Что делает |
|
Загружает бинарник и запускает oled-clock --info |
|
Запускает один тестовый паттерн |
|
Запускает демон oled-clock |
|
Останавливает демон |
|
Переключает OLED в режим Linux console |
|
Переключает OLED в режим часов |
|
Выполняет произвольную команду на плате |
deploy.sh полезен именно после того, как плата уже доступна по сети. На этапе первичного bring-up он не заменяет UART.
Переключение режимов: Linux console и кастомная картинка
OLED может использоваться в двух взаимоисключающих режимах.
Режим |
Кто пишет в /dev/fb0 |
Что видно |
Linux console |
|
login prompt, shell, kernel messages |
Custom display |
|
часы, температура, паттерны |
Оба режима используют один и тот же framebuffer. Если одновременно оставить fbcon и запустить oled-clock, они будут перерисовывать экран друг поверх друга.
Переключение в режим кастомной картинки
Для передачи OLED под управление oled-clock нужно:
Остановить getty на tty1, если он мешает.
Отвязать fbcon от framebuffer.
Запустить oled-clock.
Скрипт oled-clock-start:
cat > /usr/local/bin/oled-clock-start << 'EOF' #!/bin/sh VTCON=/sys/class/vtconsole/vtcon1/bind LOG=/tmp/oled.log ps | grep -v grep | grep -E '[g]etty[^/]*tty1' | awk '{print $1}' | xargs -r kill 2>/dev/null sleep 0.2 if [ -w "$VTCON" ]; then echo 0 > "$VTCON" fi if ! pidof oled-clock >/dev/null 2>&1; then nohup /usr/local/bin/oled-clock >"$LOG" 2>&1 & sleep 0.5 fi echo "OLED clock running (log: $LOG)" pidof oled-clock | xargs -r echo "pid:" EOF
Делаем файл исполняемым:
chmod +x /usr/local/bin/oled-clock-start
Запуск:
/usr/local/bin/oled-clock-start
Проверка:
cat /sys/class/vtconsole/vtcon1/bind pgrep -a oled-clock tail -20 /tmp/oled.log
Ожидаем:
# /usr/local/bin/oled-clock-start OLED clock running (log: /tmp/oled.log) pid: 329
Переключение в режим Linux console на OLED
Для возврата к консоли нужно:
Остановить oled-clock.
Привязать fbcon обратно.
Запустить getty на tty1.
Скрипт oled-console:
cat > /usr/local/bin/oled-console << 'EOF' #!/bin/sh VTCON=/sys/class/vtconsole/vtcon1/bind killall -q oled-clock 2>/dev/null sleep 0.2 if [ -w "$VTCON" ]; then echo 1 > "$VTCON" else echo "oled-console: cannot write $VTCON" >&2 exit 1 fi if ! ps | grep -v grep | grep -E '[g]etty[^/]*tty1' >/dev/null; then setsid /sbin/getty -L tty1 0 linux </dev/null >/dev/null 2>&1 & fi echo "OLED console ready: login on tty1, input from USB keyboard" EOF
Делаем файл исполняемым:
chmod +x /usr/local/bin/oled-console
Запуск:
# /usr/local/bin/oled-console [28149.446330] Console: switching to mono frame buffer device 32x10 OLED console ready: login on tty1, input from USB keyboard
Проверка:
# cat /sys/class/vtconsole/vtcon1/bind 1 # ps | grep '[g]etty.*tty1' 333 root /sbin/getty -L tty1 0 linux
Ввод в этой консоли идет с USB-клавиатуры, а не через UART.
Добавление userspace-утилит в Buildroot rootfs
Копировать бинарник вручную удобно для отладки, но для воспроизводимого образа лучше добавить его в rootfs.
Простой способ - через rootfs_overlay:
mkdir -p "$BR_EXT/board/zynq_mini_revb/rootfs_overlay/usr/local/bin" cp oled-clock "$BR_EXT/board/zynq_mini_revb/rootfs_overlay/usr/local/bin/oled-clock" cp oled-clock-start.sh "$BR_EXT/board/zynq_mini_revb/rootfs_overlay/usr/local/bin/oled-clock-start" cp oled-console.sh "$BR_EXT/board/zynq_mini_revb/rootfs_overlay/usr/local/bin/oled-console" chmod +x "$BR_EXT/board/zynq_mini_revb/rootfs_overlay/usr/local/bin/"*
В defconfig должен быть указан overlay: BR2_ROOTFS_OVERLAY="$(BR2_EXTERNAL_ZYNQ_MINI_I2C_PATH)/board/zynq_mini_revb/rootfs_overlay"
Более правильный способ - сделать отдельный Buildroot-пакет oled-clock. Тогда Buildroot будет сам cross-компилировать утилиту и класть ее в /usr/local/bin или /usr/bin.
Рабочее дерево проекта находится в /home/megalloid/devel/xilinx/projects/zynq_mini_oled/linux
Внешнее дерево Buildroot находится в /home/megalloid/devel/xilinx/projects/zynq_mini_oled/linux/board-support
Создаем в директории board-support/package/ каталог для пакета:
export WORK=/home/megalloid/devel/xilinx/projects/zynq_mini_oled/linux export BR_EXT="$WORK/board-support" mkdir -p "$BR_EXT/package/oled-clock/src"
В результате должна получиться такая структура:
board-support/ └── package/ └── oled-clock/ ├── Config.in ├── oled-clock.mk └── src/ ├── oled-clock.c ├── oled-clock-start.sh └── oled-console.sh
Здесь:
Файл |
Назначение |
Config.in |
Добавляет пункт oled-clock в меню Buildroot |
oled-clock.mk |
Описывает правила сборки и установки пакета |
src/oled-clock.c |
Исходный код userspace-утилиты |
src/oled-clock-start.sh |
Переключение OLED в режим часов или статусного экрана |
src/oled-console.sh |
Переключение OLED обратно в режим Linux console |
Копируем исходники и скрипты в каталог пакета:
cp /home/megalloid/devel/xilinx/projects/zynq_mini_oled/linux/userspace/oled-clock/oled-clock.c "$BR_EXT/package/oled-clock/src/oled-clock.c" cp /home/megalloid/devel/xilinx/projects/zynq_mini_oled/linux/userspace/oled-clock/oled-clock-start.sh "$BR_EXT/package/oled-clock/src/oled-clock-start.sh" cp /home/megalloid/devel/xilinx/projects/zynq_mini_oled/linux/userspace/oled-clock/oled-console.sh "$BR_EXT/package/oled-clock/src/oled-console.sh" chmod +x "$BR_EXT/package/oled-clock/src/oled-clock-start.sh" chmod +x "$BR_EXT/package/oled-clock/src/oled-console.sh"
Если файлы уже лежат в проекте, можно использовать конкретные пути репозитория. Например:
cp "$WORK/userspace/oled-clock/oled-clock.c" \ "$BR_EXT/package/oled-clock/src/oled-clock.c" cp "$WORK/userspace/oled-clock/oled-clock-start.sh" \ "$BR_EXT/package/oled-clock/src/oled-clock-start.sh" cp "$WORK/userspace/oled-clock/oled-console.sh" \ "$BR_EXT/package/oled-clock/src/oled-console.sh"
После копирования проверяем
find "$BR_EXT/package/oled-clock" -maxdepth 3 -type f -print
Ожидаемый вывод:
.../package/oled-clock/src/oled-clock.c .../package/oled-clock/src/oled-clock-start.sh .../package/oled-clock/src/oled-console.sh
Теперь создаем структуру пакета. Файл Config.in описывает опцию Buildroot, которая включает или отключает пакет.
Создаем файл:
cat > "$BR_EXT/package/oled-clock/Config.in" << 'EOF' config BR2_PACKAGE_OLED_CLOCK bool "oled-clock" depends on BR2_LINUX_KERNEL help Userspace utility for the SSD1306 framebuffer on ZYNQ MINI Rev B. The utility can print framebuffer information, draw test patterns, show clock/status information, and work together with helper scripts that switch the OLED between fbcon console mode and custom display mode. EOF
Смысл основных строк:
Строка |
Смысл |
|
Имя опции Buildroot |
|
В menuconfig появится галочка oled-clock |
|
Пакет имеет смысл только в Linux-системе |
|
Справочный текст для menuconfig |
Имя BR2_PACKAGE_OLED_CLOCK потом будет использоваться в defconfig: BR2_PACKAGE_OLED_CLOCK=y
И подключаем пакет в корневой Config.in BR2_EXTERNAL. Внешнее дерево board-support уже имеет корневой файл $BR_EXT/Config.in
В нем нужно подключить новый пакет. Если файл уже содержит меню вида:
menu "ZYNQ MINI Rev B (manual)" source "$BR2_EXTERNAL_ZYNQ_MINI_I2C_PATH/package/i2c-master-axi/Config.in" endmenu
добавляем еще одну строку source:
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" source "$BR2_EXTERNAL_ZYNQ_MINI_I2C_PATH/package/oled-clock/Config.in" endmenu EOF
Теперь в menuconfig рядом с i2c-master-axi появится пакет oled-clock.
Проверяем файл $BR_EXT/external.mk, он должен подключать все .mk из подкаталогов package/.
Ожидаемое содержимое:
include $(sort $(wildcard $(BR2_EXTERNAL_ZYNQ_MINI_I2C_PATH)/package/*/*.mk))
Если такая строка уже есть, менять ничего не нужно. Благодаря маске package/*/*.mk новый файл package/oled-clock/oled-clock.mk будет подключен автоматически.
И создаем oled-clock.mk. Файл oled-clock.mk описывает, как именно собрать и установить утилиту.
Создаем рецепт:
cat > "$BR_EXT/package/oled-clock/oled-clock.mk" << 'EOF' ################################################################################ # oled-clock - userspace SSD1306 framebuffer utility ################################################################################ OLED_CLOCK_VERSION = 1.0 OLED_CLOCK_SITE = $(BR2_EXTERNAL_ZYNQ_MINI_I2C_PATH)/package/oled-clock/src OLED_CLOCK_SITE_METHOD = local OLED_CLOCK_LICENSE = MIT define OLED_CLOCK_BUILD_CMDS $(TARGET_CC) $(TARGET_CFLAGS) $(TARGET_LDFLAGS) \ -O2 -Wall -Wextra \ -o $(@D)/oled-clock $(@D)/oled-clock.c endef define OLED_CLOCK_INSTALL_TARGET_CMDS $(INSTALL) -D -m 0755 $(@D)/oled-clock \ $(TARGET_DIR)/usr/local/bin/oled-clock $(INSTALL) -D -m 0755 $(@D)/oled-clock-start.sh \ $(TARGET_DIR)/usr/local/bin/oled-clock-start $(INSTALL) -D -m 0755 $(@D)/oled-console.sh \ $(TARGET_DIR)/usr/local/bin/oled-console endef $(eval $(generic-package)) EOF
Разбор ключевых строк:
Строка |
Смысл |
|
Локальная версия пакета |
|
Откуда Buildroot берет исходники |
|
Источник не скачивается, а берется с диска |
|
Целевой ARM-компилятор Buildroot |
|
Флаги целевой сборки |
|
Корень будущей rootfs |
|
Установка файла с созданием каталогов и правами executable |
|
Подключение стандартной инфраструктуры Buildroot для userspace-пакета |
В отличие от i2c-master-axi, здесь не используется $(kernel-module), потому что oled-clock - обычная userspace-программа, а не модуль ядра.
Включаем пакет в defconfig. Открываем defconfig платы:
vim $BR_EXT/configs/zynq_mini_revb_defconfig
Добавляем строку
BR2_PACKAGE_OLED_CLOCK=y
После этого Buildroot будет автоматически собирать oled-clock при полной сборке образа.
Применяем defconfig. Переходим в дерево Buildroot:
export WORK=/home/megalloid/devel/xilinx/projects/zynq_mini_oled/linux export BR_SRC="$WORK/buildroot-2024.02.7" export BR_OUT="$WORK/br-output" export BR_EXT="$WORK/board-support" cd "$BR_SRC"
Применяем defconfig:
make BR2_EXTERNAL="$BR_EXT" O="$BR_OUT" zynq_mini_revb_defconfig
Проверяем, что пакет включился:
grep BR2_PACKAGE_OLED_CLOCK "$BR_OUT/.config"
Ожидаемый результат:
BR2_PACKAGE_OLED_CLOCK=y
Если строки нет, значит пакет не добавлен в defconfig или Config.in не подключен в корневом board-support/Config.in.
Проверяем пакет в menuconfig. Можно дополнительно открыть menuconfig:
make BR2_EXTERNAL="$BR_EXT" O="$BR_OUT" menuconfig
Пакет должен быть виден в меню.
Если пакет не виден, нужно проверить три файла:
cat "$BR_EXT/Config.in" cat "$BR_EXT/external.mk" cat "$BR_EXT/package/oled-clock/Config.in"
Типовые ошибки:
Симптом |
Причина |
Пакет не виден в menuconfig |
Не добавлен source .../oled-clock/Config.in |
Пакет виден, но не собирается |
Нет BR2_PACKAGE_OLED_CLOCK=y |
Buildroot не видит .mk |
Неверный external.mk |
Сборка падает на oled-clock.c |
Нет файла в src/ или ошибка в C-коде |
Бинарник не попал в rootfs |
Ошибка в OLED_CLOCK_INSTALL_TARGET_CMDS |
Собираем пакет. Для первой сборки можно запустить общую сборку:
make BR2_EXTERNAL="$BR_EXT" O="$BR_OUT" -j"$(nproc)"
Если ядро и rootfs уже собраны, а нужно пересобрать только oled-clock, используем:
make BR2_EXTERNAL="$BR_EXT" O="$BR_OUT" oled-clock-rebuild make BR2_EXTERNAL="$BR_EXT" O="$BR_OUT"
Первая команда пересобирает пакет. Вторая команда обновляет rootfs и итоговые образы. Проверяем, что бинарник появился в target rootfs:
ls -l "$BR_OUT/target/usr/local/bin/oled-clock" ls -l "$BR_OUT/target/usr/local/bin/oled-clock-start" ls -l "$BR_OUT/target/usr/local/bin/oled-console"
Ожидаемый результат:
-rwxr-xr-x ... oled-clock -rwxr-xr-x ... oled-clock-start -rwxr-xr-x ... oled-console
Проверяем тип бинарника:
file "$BR_OUT/target/usr/local/bin/oled-clock" ELF 32-bit LSB executable, ARM, EABI5
Если file показывает x86-64, значит утилита была собрана хостовым gcc, а не $(TARGET_CC). Нужно проверить OLED_CLOCK_BUILD_CMDS.
Проверяем установку в итоговом rootfs. После сборки образа можно проверить содержимое rootfs:
find "$BR_OUT/target/usr/local/bin" -maxdepth 1 -type f -name 'oled*' -print
Ожидаемо:
/usr/local/bin/oled-clock /usr/local/bin/oled-clock-start /usr/local/bin/oled-console
Если используется rootfs.ext2, можно проверить уже готовый образ через loop mount:
mkdir -p /tmp/zynq-rootfs sudo mount -o loop "$BR_OUT/images/rootfs.ext2" /tmp/zynq-rootfs ls -l /tmp/zynq-rootfs/usr/local/bin/oled* sudo umount /tmp/zynq-rootfs
Если используется sdcard.img, rootfs будет на втором разделе:
LOOP=$(sudo losetup --find --show --partscan "$BR_OUT/images/sdcard.img") sudo mount "${LOOP}p2" /tmp/zynq-rootfs ls -l /tmp/zynq-rootfs/usr/local/bin/oled* sudo umount /tmp/zynq-rootfs sudo losetup -d "$LOOP"
Проверяем на плате. После записи нового образа на SD и загрузки платы:
ls -l /usr/local/bin/oled-clock ls -l /usr/local/bin/oled-clock-start ls -l /usr/local/bin/oled-console
Проверяем framebuffer:
ls -l /dev/fb0 /usr/local/bin/oled-clock --info
Проверяем тестовые паттерны:
/usr/local/bin/oled-clock --test Z sleep 1 /usr/local/bin/oled-clock --test G sleep 1 /usr/local/bin/oled-clock --test F sleep 1 /usr/local/bin/oled-clock --test T
Запускаем режим часов или статусного экрана:
/usr/local/bin/oled-clock-start
Возвращаем режим консоли:
/usr/local/bin/oled-console
Автозапуск oled-clock или console mode
Сам пакет только устанавливает бинарники. Автоматически запускать oled-clock при старте можно отдельным init-скриптом, например S45oled-console или S46oled-clock.
Если по умолчанию нужен режим консоли на OLED, оставляем init-скрипт /etc/init.d/S45oled-console
Если по умолчанию нужен кастомный статусный экран, можно сделать отдельный скрипт:
cat > "$BR_EXT/board/zynq_mini_revb/rootfs_overlay/etc/init.d/S46oled-clock" << 'EOF' #!/bin/sh case "$1" in start) echo "Starting oled-clock..." [ -x /usr/local/bin/oled-clock-start ] && /usr/local/bin/oled-clock-start ;; stop) echo "Stopping oled-clock..." killall -q oled-clock 2>/dev/null || true ;; restart) "$0" stop sleep 0.2 "$0" start ;; *) echo "Usage: $0 {start|stop|restart}" exit 1 ;; esac exit 0 EOF
Делаем его исполняемым:
chmod +x "$BR_EXT/board/zynq_mini_revb/rootfs_overlay/etc/init.d/S46oled-clock"
Но одновременно держать S45oled-console и S46oled-clock не стоит, если оба пишут в /dev/fb0.
И после оформления oled-clock как Buildroot-пакета userspace-часть проекта становится воспроизводимой. Теперь для получения готовой SD-карты не нужно отдельно собирать утилиту и копировать ее на плату. Достаточно выполнить обычную сборку Buildroot, и в rootfs автоматически попадут все необходимые файлы.
Такой подход удобнее и полезнее для дальнейшего развития проекта: драйвер i2c-master-axi собирается как kernel module, а oled-clock собирается как обычный userspace-пакет Buildroot.
Ускорение вывода картинки через PRESCALE
Стандартная частота I2C в проекте - 100 kHz. Для SSD1306 это работает, но полный кадр по I2C обновляется не мгновенно. Каждый кадр 1024 байта превращается в I2C-трафик с адресом, управляющими байтами, командами и задержками. Поэтому повышение частоты SCL может заметно ускорить обновление экрана.
В нашем IP частота SCL задается регистром PRESCALE: SCL = input_clock / (4 * (PRESCALE + 1))
При input_clock = 50 MHz:
PRESCALE |
Примерная SCL |
124 |
100 kHz |
61 |
около 201.6 kHz |
31 |
около 390.6 kHz |
30 |
около 403.2 kHz |
24 |
500 kHz |
12 |
около 961.5 kHz |
Для SSD1306 разумный первый шаг - около 400 kHz. Это соответствует fast mode I2C и обычно подходит для коротких проводов на плате или макетном подключении. Но гарантировать это нельзя: нужно смотреть качество фронтов, pull-up, длину проводов, питание и конкретный OLED-модуль.
Можно попытаться вручную изменить регистр PRESCALE:
BASE=0x43c00000 devmem $((BASE + 0x00)) 32 0 devmem $((BASE + 0x14)) 32 31 devmem $((BASE + 0x00)) 32 1
Здесь:
BASE + 0x00 - CTRL
BASE + 0x14 - PRESCALE
Но делать это при загруженном i2c-master-axi и привязанном ssd1307fb небезопасно:
Ядро может в этот момент выполнять I2C-транзакцию.
ssd1307fbможет отправлять deferred update.Ручная запись в
CTRL=0выключает контроллер за спиной драйвера.Ручная запись в
PRESCALEне меняет состояние драйвера и его представление о bus frequency.Следующий
modprobe i2c-master-axiснова выставитPRESCALEпо DTS и перезапишет ручное значение.
Симптом из такой ситуации: ssd1307fb 0-003c: Couldn't send I2C command.
И это будет ожидаемо. OLED-драйвер попытался отправить команду, а I2C-контроллер в этот момент был выключен, перенастраивался или оказался в несогласованном состоянии.
Корректный способ - поменять желаемую частоту в Device Tree:
i2c_pl: i2c@43c00000 { compatible = "user,i2c-master-axi-1.0"; reg = <0x43c00000 0x1000>; input-clock-frequency = <50000000>; clock-frequency = <400000>; ... };
После этого пересобрать DTB, положить его на FAT-раздел SD и перезагрузить плату:
make BR2_EXTERNAL="$BR_EXT" O="$BR_OUT" linux-rebuild make BR2_EXTERNAL="$BR_EXT" O="$BR_OUT"
и затем заменить zynq-mini-revb.dtb на FAT-разделе SD.
После загрузки проверяем:dmesg | grep -i i2c-master
Ожидаем что-то вроде:
i2c-master-axi 43c00000.i2c: input=50000000 Hz, bus=400000 Hz, prescale=30
Из-за целочисленного деления точный prescale может отличаться от расчетного "идеального" значения. Важно не само число, а фактическая устойчивая работа OLED.
Для более точного таргета на PRESCALE = 31 можно задать частоту около 390625:
clock-frequency = <390625>;
Тогда расчет даст:
50000000 / (4 * 390625) - 1 = 31
Можно экспериментально попробовать PRESCALE без пересборки DTB, делать это лучше при выгруженном модуле и отвязанном framebuffer.
Последовательность:
killall -q oled-clock 2>/dev/null echo 0 > /sys/class/vtconsole/vtcon1/bind 2>/dev/null || true rmmod i2c-master-axi 2>/dev/null || true BASE=0x43c00000 devmem $((BASE + 0x00)) 32 0 devmem $((BASE + 0x14)) 32 31 devmem $((BASE + 0x18)) 32 3 devmem $((BASE + 0x00)) 32 1
Но важное ограничение: после modprobe i2c-master-axi драйвер снова выполнит hw_init() и запишет свой PRESCALE из DTS. Поэтому такой devmem-эксперимент имеет ограниченную пользу. Он может показать, что железо в принципе принимает новое значение, но не является устойчивой настройкой системы.
Если ssd1307fb встроен в ядро, выгрузить его командой rmmod ssd1307fb не получится:
rmmod: can't unload module 'ssd1307fb': No such file or directory
Это значит не то, что драйвера нет, а то, что он встроен в ядро, а не загружен как модуль. При удалении i2c-master-axi исчезает I2C-адаптер, и framebuffer-консоль может переключиться на dummy device:
Console: switching to colour dummy device 80x30
При повторном modprobe i2c-master-axi адаптер появится снова, ssd1307fb заново привяжется к OLED, и /dev/fb0 зарегистрируется повторно.
Ускоряем вывод на OLED: runtime-настройка частоты I2C в драйвере i2c-master-axi
Теперь перейдем на следующий шаг. На текущий момент чтобы попробовать 200 kHz или 400 kHz, приходится:
править DTS;
пересобирать DTB;
заменять DTB на FAT-разделе SD;
перезагружать плату.
То есть нам нужен не обходной доступ к MMIO, а нормальный runtime-интерфейс самого драйвера.
Сейчас же хочется дать пользователю возможность менять частоту I2C без пересборки DTB, без перезагрузки платы и без прямой записи в MMIO через devmem.
Для этого в драйвер добавим sysfs-атрибут bus_hz. Он находится на platform device, соответствующем узлу i2c@43c00000.
Пример использования:
BUS=$(dirname $(readlink -f /sys/bus/i2c/devices/i2c-1/device)) cat "$BUS/bus_hz" echo 400000 > "$BUS/bus_hz" dmesg | tail -3
После записи нового значения драйвер сам:
разбирает число из userspace;
берет mutex;
обновляет поле i->bus_hz;
вызывает уже существующую i2c_master_axi_hw_init();
пересчитывает PRESCALE;
кратковременно выключает контроллер;
пишет новый PRESCALE;
снова включает контроллер;
отпускает mutex.
Такой путь остается согласованным с состоянием драйвера и не требует ручной записи в регистры. Логично было бы спросить, мол, а почему sysfs, а не module parameter. Можно было бы добавить параметр модуля:
modprobe i2c-master-axi bus_hz=400000
Это тоже рабочий вариант, но он требует выгрузки и повторной загрузки модуля при каждой смене частоты. И sysfs удобнее:
echo 400000 > "$BUS/bus_hz"
Поэтому для экспериментов с OLED лучше подходит sysfs. Итоговый алгоритм доработки:

Внесем изменения по шагам, наглядно демонстрируя как дорабатывается драйвер ?
Изменение 1: добавляем документацию в комментарий драйвера
В шапке файла можно добавить короткое описание новой возможности:
/* * Runtime bus speed (without DT rebuild): sysfs on the platform device * .../bus_hz (read/write, Hz) * Example: echo 400000 > .../bus_hz (Fast-mode I2C, if wiring/OLED allow) */
Это полезно, потому что пользовательский интерфейс теперь не ограничивается Device Tree. Начальное значение по-прежнему берется из clock-frequency, но после загрузки модуля его можно переопределить через sysfs.
Изменение 2: добавляем include-файлы
Для mutex и sysfs нужны дополнительные заголовки:
#include <linux/mutex.h> #include <linux/sysfs.h>
mutex.h нужен для:
struct mutex mutex_init() mutex_lock() mutex_unlock()
sysfs.h нужен для:
sysfs_emit() DEVICE_ATTR_RW() struct attribute_group devm_device_add_group()
Изменение 3: добавляем mutex в структуру драйвера
В структуру экземпляра контроллера добавляется поле:
struct i2c_master_axi { void __iomem *regs; struct device *dev; struct clk *clk; struct i2c_adapter adap; struct completion cmd_done; struct mutex lock; int irq; bool use_irq; u32 input_hz; u32 bus_hz; };
Назначение lock - защитить регистры IP-блока от одновременного доступа из двух путей:
обычная I2C-транзакция через
axi_master_xfer()смена частоты через запись в
sysfsbus_hz
Без mutex возможна ситуация:
ssd1307fbотправляет байт на OLEDв этот момент пользователь пишет
echo 400000 > bus_hzдрайвер выключает
CTRL.ENи меняетPRESCALEтекущая I2C-транзакция ломается
ssd1307fbполучает ошибку отправки команды
С mutex такая гонка блокируется: либо сначала заканчивается текущий master_xfer, либо сначала выполняется перенастройка частоты. Важно добавить инициализацию mutex в probe():
init_completion(&i->cmd_done); mutex_init(&i->lock);
Без mutex_init() поведение mutex не определено.
Изменение 4: защищаем axi_master_xfer
Раньше axi_master_xfer() выполнял передачу без внешней блокировки:
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;
После добавления runtime-смены частоты нужно защитить весь 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, ret; u32 status; mutex_lock(&i->lock); 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); ret = -EAGAIN; goto out_unlock; } for (k = 0; k < num; k++) { ret = axi_xfer_one(i, &msgs[k], (k == 0), (k == num - 1)); if (ret < 0) goto out_unlock; } ret = num; out_unlock: mutex_unlock(&i->lock); return ret; }
Здесь важно не только наличие mutex_lock(), но и единая точка выхода через out_unlock.
Если сделать прямой return -EAGAIN внутри критической секции, mutex останется захваченным, и все следующие I2C-транзакции зависнут.
Изменение 5: добавляем чтение bus_hz через sysfs
Функция чтения атрибута:
static ssize_t bus_hz_show(struct device *dev, struct device_attribute *attr, char *buf) { struct i2c_master_axi *i = dev_get_drvdata(dev); return sysfs_emit(buf, "%u\n", i->bus_hz); }
Она возвращает текущее значение i->bus_hz. Важно: это не измеренная осциллографом частота SCL, а программное целевое значение, которое драйвер использовал для расчета PRESCALE
Изменение 6: добавляем запись bus_hz через sysfs
Функция записи:
static ssize_t bus_hz_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { struct i2c_master_axi *i = dev_get_drvdata(dev); unsigned int hz; int ret; if (kstrtouint(buf, 0, &hz) || hz == 0) return -EINVAL; mutex_lock(&i->lock); i->bus_hz = hz; ret = i2c_master_axi_hw_init(i); mutex_unlock(&i->lock); return ret ? ret : count; }
Разбор по шагам:
Шаг |
Код |
Смысл |
1 |
|
Разобрать строку из userspace |
2 |
|
Нулевая частота запрещена |
3 |
|
Дождаться конца текущей I2C-транзакции |
4 |
|
Обновить программное целевое значение |
5 |
|
Пересчитать PRESCALE и перенастроить IP |
6 |
|
Отпустить шину |
7 |
|
Вернуть ошибку или количество принятых байт |
Функция kstrtouint() с основанием 0 позволяет писать значения в разных форматах:
echo 400000 > "$BUS/bus_hz"
echo 0x61a80 > "$BUS/bus_hz"
При успехе в dmesg снова появится строка из i2c_master_axi_hw_init():
i2c-master-axi 43c00000.i2c: input=50000000 Hz, bus=400000 Hz, prescale=30
Важное замечание про ошибку восстановления bus_hz. Делаем rollback
В простой реализации bus_hz_store() есть тонкий дефект: поле i->bus_hz обновляется до вызова i2c_master_axi_hw_init().
Если пользователь запишет слишком большую частоту, hw_init() вернет ошибку, но программное состояние драйвера уже будет содержать нерабочее значение. Например, при input_hz = 50000000 запись заведомо невозможной частоты приведет к отказу в hw_init(), но cat bus_hz после этого может показать уже новое, не примененное значение.
Поэтому в обработчик записи нужно добавить rollback: сохранить старое значение, попробовать применить новое, а при ошибке вернуть старое значение и повторно вызвать hw_init(), чтобы восстановить не только поле i->bus_hz, но и состояние регистров контроллера.
Итоговая реализация bus_hz_store():
static ssize_t bus_hz_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { struct i2c_master_axi *i = dev_get_drvdata(dev); unsigned int hz; u32 old_hz; int ret, rollback; if (kstrtouint(buf, 0, &hz) || hz == 0) return -EINVAL; mutex_lock(&i->lock); old_hz = i->bus_hz; i->bus_hz = hz; ret = i2c_master_axi_hw_init(i); if (ret) { i->bus_hz = old_hz; rollback = i2c_master_axi_hw_init(i); if (rollback) dev_err(i->dev, "bus_hz rollback to %u Hz failed (%d)\n", old_hz, rollback); } mutex_unlock(&i->lock); return ret ? ret : count; }
Логика получается такой:
Шаг |
Действие |
Назначение |
1 |
|
Разобрать значение из sysfs |
2 |
|
Запретить нулевую частоту |
3 |
|
Исключить параллельную I2C-транзакцию |
4 |
|
Сохранить предыдущее рабочее значение |
5 |
|
Подготовить новое целевое значение |
6 |
|
Пересчитать PRESCALE и применить настройки |
7 |
|
Если новая частота не принята |
8 |
|
Вернуть старое значение в состояние драйвера |
9 |
|
Восстановить старый PRESCALE в железе |
10 |
|
Зафиксировать, если восстановление не удалось |
11 |
|
Освободить контроллер |
12 |
|
Вернуть ошибку или успешную запись |
Важный момент: простого возврата i->bus_hz = old_hz недостаточно. Если i2c_master_axi_hw_init() успела выключить контроллер, очистить ISR или частично записать регистры, программное поле и железо могут оказаться в разных состояниях. Поэтому при ошибке выполняется повторный hw_init() уже со старой частотой.
Также в probe() обязательно нужно инициализировать mutex:
i->dev = dev; init_completion(&i->cmd_done); mutex_init(&i->lock); platform_set_drvdata(pdev, i);
Без mutex_init(&i->lock) использование mutex_lock() и mutex_unlock() некорректно.
После этой доработки запись в bus_hz ведет себя предсказуемо:
cat "$BUS/bus_hz" # 100000 echo 400000 > "$BUS/bus_hz" dmesg | tail -2 # i2c-master-axi ... input=50000000 Hz, bus=400000 Hz, prescale=30 cat "$BUS/bus_hz" # 400000
Если записать невозможное значение, драйвер вернет ошибку, а bus_hz останется на предыдущей рабочей частоте:
echo 50000000 > "$BUS/bus_hz" # write error: Invalid argument cat "$BUS/bus_hz" # 400000
Если при этом восстановление старого PRESCALE в железе не удалось, в dmesg появится диагностическое сообщение: bus_hz rollback to 400000 Hz failed (-...)
Такой rollback делает sysfs-интерфейс безопаснее: userspace может экспериментировать с частотой шины, но не оставляет драйвер в заведомо неконсистентном состоянии после неудачной записи.
Изменение 7: регистрируем sysfs-атрибут
Создаем атрибут и группу:
static DEVICE_ATTR_RW(bus_hz); static struct attribute *i2c_master_axi_dev_attrs[] = { &dev_attr_bus_hz.attr, NULL, }; static const struct attribute_group i2c_master_axi_dev_group = { .attrs = i2c_master_axi_dev_attrs, };
Макрос DEVICE_ATTR_RW(bus_hz) создает атрибут с правами чтения и записи и связывает его с функциями bus_hz_show, bus_hz_store. В результате в sysfs появляется файл .../bus_hz
Он находится у platform device, то есть у устройства 43c00000.i2c, а не напрямую в каталоге /sys/bus/i2c/devices/i2c-1/.
Изменение 8: добавляем sysfs-группу в probe
После успешного i2c_add_adapter() регистрируем группу:
ret = i2c_add_adapter(&i->adap); if (ret) goto err_disable; ret = devm_device_add_group(dev, &i2c_master_axi_dev_group); if (ret) goto err_del_adap; return 0; err_del_adap: i2c_del_adapter(&i->adap); err_disable: axi_write(i, I2C_REG_CTRL, 0); err_clk: if (i->clk) clk_disable_unprepare(i->clk); return ret;
Порядок здесь такой:
сначала регистрируем
i2c_adapter;затем создаем sysfs-атрибут;
если sysfs-группа не создалась, снимаем уже зарегистрированный адаптер через
i2c_del_adapter().
Так мы не оставляем в системе "висячую" I2C-шину без управляющего sysfs-интерфейса.
devm_device_add_group() автоматически удалит группу при удалении устройства, поэтому в remove() отдельный sysfs_remove_group() не нужен.
Как найти путь к bus_hz на плате
После загрузки модуля и появления I2C-шины:
ls /sys/bus/i2c/devices/
Допустим, адаптер называется i2c-1. Тогда путь к platform device можно получить так:
BUS=$(dirname $(readlink -f /sys/bus/i2c/devices/i2c-1/device)) echo "$BUS"
Типичный результат:
/sys/devices/soc0/amba_pl@0/43c00000.i2c
Проверяем атрибут:
cat "$BUS/bus_hz"
Ожидаемо после загрузки 100000
Меняем частоту:
echo 400000 > "$BUS/bus_hz" dmesg | tail -5
Ожидаем строку i2c-master-axi 43c00000.i2c: input=50000000 Hz, bus=400000 Hz, prescale=30
Проверяем, что OLED все еще отвечает:
i2cdetect -y 1
Если адрес 0x3c занят драйвером, в таблице может быть UU, а не 3c. Это нормально.
Типовые значения bus_hz и PRESCALE
При input-clock-frequency = 50000000 расчет такой: PRESCALE = input_hz / (4 * bus_hz) - 1
bus_hz |
PRESCALE |
Примерная частота SCL |
Комментарий |
100000 |
124 |
100000 Hz |
Standard mode, значение по умолчанию |
200000 |
61 |
около 201613 Hz |
Промежуточный режим |
390625 |
31 |
390625 Hz |
Точное значение для prescale 31 |
400000 |
30 |
около 403226 Hz |
Fast mode около 400 kHz |
500000 |
24 |
500000 Hz |
Может работать не на каждом модуле |
1000000 |
11 или 12 |
около 1 MHz |
Риск NACK, зависит от платы |
Формально многие OLED-модули SSD1306 работают на fast-mode I2C 400 kHz. Но реальная надежность зависит не только от микросхемы.
Фактор |
Влияние |
Длина проводов до OLED |
Чем длиннее, тем хуже фронты SDA/SCL |
Номинал pull-up |
Слишком слабые подтяжки замедляют фронты |
Емкость шины |
Большая емкость ухудшает форму сигнала |
Уровень питания |
3.3 V обычно нормально, но зависит от модуля |
Качество XDC и разводки PL-пинов |
Ошибка пинов даст NACK на любой скорости |
Реализация RTL IP |
Тайминги должны держать выбранную SCL |
Поведение ssd1307fb |
Использует framebuffer/deferred I/O, не только I2C |
Поэтому не будем утверждать "400 kHz всегда работает", а формулируем так: 400 kHz - разумный первый fast-mode режим для проверки, если проводка и OLED-модуль его выдерживают.
Тесты после изменения частоты
После смены bus_hz нужно проверить не только dmesg, но и фактическую работу framebuffer. Последовательность:
dmesg | grep -i i2c-master /usr/local/bin/oled-clock --info /usr/local/bin/oled-clock --test Z /usr/local/bin/oled-clock --test G /usr/local/bin/oled-clock --test F /usr/local/bin/oled-clock --test T /usr/local/bin/oled-clock
Пересборка драйвера
Если менялся только файл linux/drivers/i2c-master-axi/i2c-master-axi.c
ядро и DTB пересобирать не нужно. Достаточно пересобрать Buildroot-пакет с модулем:
export WORK=/home/megalloid/devel/xilinx/projects/zynq_mini_oled/linux export BR_SRC="$WORK/buildroot-2024.02.7" export BR_OUT="$WORK/br-output" export BR_EXT="$WORK/board-support" cd "$BR_SRC" make BR2_EXTERNAL="$BR_EXT" O="$BR_OUT" i2c-master-axi-rebuild make BR2_EXTERNAL="$BR_EXT" O="$BR_OUT"
Первый вызов пересобирает модуль. Второй обновляет rootfs и итоговые образы. Проверяем, что модуль появился в target rootfs:
find "$BR_OUT/target/lib/modules" -name 'i2c-master-axi.ko' -print
Если нужно быстро заменить модуль на уже загруженной плате, можно скопировать .ko по сети, затем выполнить:
killall -q oled-clock 2>/dev/null echo 0 > /sys/class/vtconsole/vtcon1/bind 2>/dev/null || true rmmod i2c-master-axi modprobe i2c-master-axi
Но стоит сразу оговориться, что Runtime-настройка через bus_hz не сохраняется между перезагрузками и перезагрузкой модуля.
Если после тестов 400 kHz работает стабильно, можно автоматически выставлять частоту при загрузке.
Пример init-скрипта:
cat > /etc/init.d/S44i2c-bus-speed << 'EOF' #!/bin/sh case "$1" in start|"") for i2cdev in /sys/bus/i2c/devices/i2c-*; do [ -e "$i2cdev/device" ] || continue busdir=$(readlink -f "$i2cdev/device") [ -n "$busdir" ] || continue if [ -w "$busdir/bus_hz" ]; then if echo 400000 > "$busdir/bus_hz"; then echo "Set I2C bus speed to 400000 Hz on $busdir" else echo "Failed to write bus_hz on $busdir" >&2 exit 1 fi exit 0 fi done echo "i2c-master-axi bus_hz not found" >&2 ;; stop|restart|reload) : ;; *) echo "usage: $0 {start|stop|restart|reload}"; exit 1 ;; esac exit 0 EOF chmod 0755 /etc/init.d/S44i2c-bus-speed
Скрипт лучше запускать после загрузки модуля i2c-master-axi, но до запуска oled-clock. Если используется текущая последовательность S03modules и S45oled-console то логичное место для установки скорости S44i2c-bus-speed.
Порядок будет таким:
S03modules -> modprobe i2c-master-axi -> появляется bus_hz S44i2c-bus-speed -> echo 400000 > bus_hz S45oled-console или S46oled-clock -> начинается активный вывод на экран
В результате можно быстро сравнивать 100 kHz, 200 kHz и 400 kHz без пересборки DTB и без перезагрузки платы. Для статьи это хороший финальный штрих: проект не только запускает OLED через кастомный I2C-контроллер в PL, но и получает управляемый способ подбирать скорость шины под конкретную плату, проводку и OLED-модуль.
Заключение
Что ж. На этом цикл статей про I2C Master Controller под ПЛИС можно считать завершенным.
Он начался с идеи собственного I2C Master Controller, мне пришлось заново сформулировать требования и перепроектировать ядро, которое поддерживает START, STOP, READ, WRITE, RESTART, ACK/NACK, clock stretching, потерю арбитража, настраиваемый prescaler, IRQ-события и пакетную передачу. В первой части цикла эта задача была поставлена как переход от старой версии контроллера к более полноценной реализации, которую сначала нужно проверить в симуляции, затем на реальном железе, а позже интегрировать в Zynq через AXI и Linux-драйвер.
Дальше мы прошли через тестирование низкоуровневого ядра. Для i2c_master_core были разобраны тестбенчи, модели slave-устройств, сценарии single read/write, repeated START, NACK, clock stretching, arbitration lost, reset в середине передачи и последовательное чтение нескольких байт. Это был важный этап: пока модуль изолирован, ошибки в протоколе, FSM и обработке пограничных состояний искать проще, чем в составе большой системы.
После этого появился следующий уровень абстракции: управляющий контроллер вокруг ядра I2C. Он нужен был для того, чтобы перейти от отдельных низкоуровневых команд к более удобной логике работы с реальными устройствами, в том числе EEPROM и последующей интеграцией в Zynq/Linux. Затем цикл был расширен до burst-транзакций и OLED SSD1306: была разобрана организация видеопамяти, режимы адресации дисплея, пакетная передача по I2C, framebuffer в BRAM, генерация изображения и пример с выводом 3D-куба и текста целиком в аппаратной логике, без процессора и микрокода.
В этой заключительной части мы перенесли тот же смысловой контур уже в Linux-мир на Zynq. Кастомный IP-блок в PL был подключен через AXI4-Lite, описан в Device Tree, поднят out-of-tree драйвером как стандартный i2c_adapter, после чего штатный ssd1307fb создал /dev/fb0 для OLED SSD1306. В финале появились userspace-утилиты, Buildroot-пакеты, init-скрипты, переключение между framebuffer-консолью и кастомным статусным экраном, а также runtime-настройка скорости I2C через bus_hz без пересборки DTB.
Итоговая цепочка получилась очень внушительной, кажется тянет на целую книгу.
Главный результат здесь не только в том, что на OLED удалось вывести часы, тестовые паттерны или системную информацию. Более важен сам путь: низкоуровневая цифровая логика, написанная на Verilog, была постепенно доведена до состояния нормального устройства в Linux. Один и тот же I2C-контроллер прошел через симуляцию, проверку на FPGA, расширение до burst-передач, работу с реальными I2C-устройствами, интеграцию в Zynq и включение в стандартную драйверную модель ядра.
Проект остается учебным и экспериментальным. В нем еще можно развивать RTL, улучшать тайминги, добавлять DMA, формализовать devicetree binding, сделать поддержку нескольких экземпляров контроллера, расширить userspace-утилиты и аккуратнее оформить upstream-like интеграцию. Но базовая задача цикла выполнена: от самодельного I2C Master Controller на Verilog мы дошли до полноценной цепочки "PL -> Linux kernel -> userspace", где аппаратная логика, драйвер и пользовательское приложение работают как единая система.
Большое вам спасибо что хотя бы доскороллили до этого момента. В следующих материалах будем курить OLED по SPI, тот что установлен на самой плате ?.
До встречи в других материалах!
Размещайте облачную инфраструктуру и масштабируйте сервисы с надежным облачным провайдером Beget.
Эксклюзивно для читателей Хабра мы даем бонус 10% при первом пополнении.
