Всем привет! Сегодня я хочу рассмотреть довольно интересную возможность на Zynq SoC, связанную с отладкой с помощью ILA (Integrated Logic Analyzer) без физического подключения через JTAG.
В этой статье мы попробуем разобраться, как использовать Xilinx Virtual Cable, соберем драйвера, посмотрим как подключаться из Hardware Manager Vivado.

Для удобства заранее представлю содержимое статьи:

  1. Что такое XVC?

  2. Готовим XVC для Zynq SoC в Block Design

  3. Добавляем XVC как пакеты (us + lkm) в наш buildroot

  4. Формируем devicetree ovelay для нашего xvc (mmap через uio, ioctl)

  5. Разбираемся, что такое uio, и как его использовать

  6. Разбираемся, как работать с XVC через ioctl lkm

  7. Готовим bitstream с ILA для демонстрации

  8. Разбираемся куда надо жмать в Vivado, чтобы все заработало

  9. Проверяем работу

P. S. Я пока что не разработчик RTL, поэтому если будут ошибки — очень буду рад конструктивной критике!

К использованию данного инструмента меня подтолкнула ситуация, когда у меня была на руках прошивка fpga (bitstream), которая успешно загружалась на плату, плата подавала признаки жизни, но периодически происходила черная магия.

И как назло распаянной FTDI микросхемы на плате не было, а JTAG-разъем был расположен в довольно неудобном месте.

Путем недолгих поисков на просторах интернета было найдено следующее:

https://github.com/Xilinx/XilinxVirtualCable
https://docs.amd.com/r/en-US/ug908-vivado-programming-debugging/Xilinx-Virtual-Cable-XVC
https://xilinx-wiki.atlassian.net/wiki/spaces/A/pages/644579329/Xilinx+Virtual+Cable

И судя по описанию, все было бы довольно хорошо, если бы не одно маленькое но в виде petalinux.
В целом ничего страшного, добавим лапками в наш buildroot все необходимое.

В качестве основной платы у меня будет использоваться Zynq RK-7020F Rev 1.1, для которой мы готовили образ в одной из прошлых статей.
Посмотреть подробнее, достать пакеты, а также готовый образ, sdk можно тут:
https://github.com/FernandesKA/buildroot_custom

Куда добавляем ILA, как это сделать?

Разбираться с ILA я буду на основе своего PET проекта, который представляет собой генератор сигналов на основе AD9361 + Zynq7020.

Добавлять ILA я планирую именно в этот проект, но т.к. в настоящее время он представляет из себя нечто незаконченное, то пока что я не буду приводить ссылок на него.

Первое, что приходит в голову — привычный способ:

SYNTHESIS -> Open Synthesized Design -> Schematic

Пометить нужные для отладки шины через Mark Debug, далее во вкладке
SYNTHESIS -> Set Up Debug

Так я сначала и сделал, и это была ошибка.

Set Up Debug вставляет ILA прямо в нетлист и сам создает для него dbg_hub, который садится на BSCANE2 кристалла, то есть на физический JTAG.
А нам нужно, чтобы хабом был Debug Bridge, до которого мы достучимся по AXI (об этом ниже). И как только в дизайне появляется Debug Bridge в режиме From AXI to BSCAN или From BSCAN to DebugHub, Vivado такую вставку просто отказывается делать:

CRITICAL WARNING: [Vivado 12-727] Error: This design contains one or more debug bridge instances with one or more master interfaces enabled and/or configured in any of the following modes.
 1.AXI to BSCAN 2. PCIE to BSCAN 3. BSCAN Primitive 4. From BSCAN to DebugHub for which insertion is not currently supported.
Use the debug instantiation flow.

Причем это всего лишь critical warning, bitstream спокойно собирается, только ILA в нем нет. Так что внимательно читаем лог.

Поэтому ILA нужно добавлять как обычное IP (тот самый debug instantiation flow): либо экземпляром в RTL, либо в Block Design.

Я выбрал RTL — так видны внутренние сигналы модуля, и не нужно ради отладки выводить их портами наружу.

Создаем IP (через IP Catalog или tcl):

create_ip -name ila -vendor xilinx.com -library ip -module_name ila_dds
set_property -dict [list \
    CONFIG.C_NUM_OF_PROBES {3} \
    CONFIG.C_PROBE0_WIDTH {12} \
    CONFIG.C_PROBE1_WIDTH {12} \
    CONFIG.C_PROBE2_WIDTH {24} \
    CONFIG.C_DATA_DEPTH {2048} \
] [get_ips ila_dds]
generate_target all [get_ips ila_dds]

И вставляем его экземпляр туда, где лежат нужные сигналы:

ila_dds ila_dds_inst (
    .clk(i_clk),
    .probe0(dbg_i),
    .probe1(dbg_q),
    .probe2(dbg_phase)
);

К хабу ILA руками подключать не нужно — Vivado сделает это сам при имплементации. Рядом с bitstream у нас должен появиться файл с расширением .ltx, в котором описаны выведенные сигналы на ILA.

Добавляем DebugBridge на AXI шину

Для того чтобы общаться с ILA через ethernet, нужно будет добавить IP core Debug Bridge.
Добавим на AXI шину DebugBridge, через который будем общаться с ILA.

Сделать это можно добавив IP core DebugBridge.

После чего в окне Re-customize IP в Debug Bridge Options следует выбрать From AXI to BSCAN, и подключить S_AXI, aclk, aresetn.

Но одного такого моста мало. From AXI to BSCAN только превращает обращения по AXI в JTAG и выдает их на BSCAN-интерфейс m0_bscan, а сам хаб, к которому подключаются ILA, живет в другом режиме.
Причем по умолчанию количество BSCAN master-интерфейсов (C_NUM_BS_MASTER) равно нулю, то есть m0_bscan вообще нет. Если так и оставить, то DRC при генерации bitstream ругнется на неподключенный TDO внутри моста:

[DRC NDRV-1] Driverless Nets: Undriven nets found. ... Net bsip_tdo_0 are undriven.

Поэтому:

  1. В первом мосте (debug_bridge_0, From AXI to BSCAN) выставляем количество BSCAN master-интерфейсов в 1 — появится m0_bscan.

  2. Добавляем второй Debug Bridge (debug_bridge_1) в режиме From BSCAN to DebugHub. Это и будет наш debug hub.

  3. Соединяем debug_bridge_0/m0_bscan -> debug_bridge_1/S_BSCAN, а debug_bridge_1/clk заводим на тот же клок, что и AXI (в моем случае FCLK_CLK0 с PS).

В tcl это выглядит так:

set debug_bridge_0 [ create_bd_cell -type ip -vlnv xilinx.com:ip:debug_bridge:3.0 debug_bridge_0 ]
set_property -dict [list \
  CONFIG.C_DEBUG_MODE {2} \
  CONFIG.C_NUM_BS_MASTER {1} \
] $debug_bridge_0

set debug_bridge_1 [ create_bd_cell -type ip -vlnv xilinx.com:ip:debug_bridge:3.0 debug_bridge_1 ]
set_property CONFIG.C_DEBUG_MODE {1} $debug_bridge_1

connect_bd_intf_net [get_bd_intf_pins debug_bridge_0/m0_bscan] [get_bd_intf_pins debug_bridge_1/S_BSCAN]

Хаб в итоге получается один — внутри debug_bridge_1, и все ILA в дизайне Vivado подключит к нему.

Почему не всегда стоит добавлять DebugBridge в корневой dts, и что с этим делать

Сразу оговорюсь, что добавлять DebugBridge в корневой можно и нужно при условии, что bitstream будет загружен до запуска linux, и в процессе с клоком ничего не будет происходить.
Но если ILA будет висеть на клоке, который появится только после загрузки bitstream, или каких-то определенных манипуляций из linux, то скорее всего ничего работать не будет.
Как пример — если сигнал для тактирования идет с какого-нибудь генератора клока типа LMK, HMC и т.д.
Мы же будем рассматривать самый простой случай — клок для ILA забираем с PS.

Процесс прошивки fpga bitstream был описан в прошлой статье, найти его можно по этой ссылке ниже.
https://habr.com/ru/articles/1052912/

Формируем DeviceTree

В зависимости от того, каким образом мы будем общаться из userspace с нашим драйвером, будем по разному формировать devicetree

uio_xvc {
	status = "disabled";
	compatible = "generic-uio";
	reg = <0x43C00000 0x10000>;
    ...
};

xvc: debug-bridge@43C00000 {
	compatible = "xlnx,xvc";
	reg = <0x43C00000 0x10000>;
	...
};

Обратите внимание, что не стоит держать в одном dts файл обе ноды с status = "okay", это возможно и будет работать, но содержит потенциальные проблемы, особенно если обращаться в одну память из двух мест одновременно.

Руками прописывать эти ноды не стоит — достаточно экспортировать xsa файл, и из него сгенерировать dts. И уже на основе полученных dts файлов начинать формировать devicetree overlay.

Как сформировать devicetree на основе экспортированного xsa

1. Выбор инструмента. В моей системе стоит Vitis 2025.1.
Я использовал классический DTG-flow: xsct и HSI API вместе с шаблонами из device-tree-xlnx.
Этот репозиторий уже был у меня в ~/dev_linux/device-tree-xlnx на ветке xlnx_rel_v2025.1, то есть той же версии, что и Vitis.
Это важно, потому что при несовпадении версий шаблоны могут не подойти к HSI.

2. TCL-скрипт для xsct:

set xsa   [lindex $argv 0]
set repo  [lindex $argv 1]
set out   [lindex $argv 2]
hsi::open_hw_design $xsa
hsi::set_repo_path $repo
hsi::create_sw_design device-tree -os device_tree -proc ps7_cortexa9_0
hsi::set_property CONFIG.dt_overlay true [hsi::get_os]
hsi::generate_target -dir $out
hsi::close_hw_design [hsi::current_hw_design]

По шагам:

  • открывается XSA;

  • подключается репозиторий device-tree-xlnx;

  • создаётся SW-дизайн с ОС device_tree для ядра ps7_cortexa9_0;

  • включается overlay (CONFIG.dt_overlay true), чтобы PL-узлы сразу оформились в виде /plugin/-фрагмента (pl.dtsi), готового к компиляции в .dtbo, а не влились в основное дерево;

  • генерируются файлы.

3. Запуск:

source ~/tools/Xilinx/2025.1/Vitis/settings64.sh
xsct gen_dt_overlay.tcl $PWD/system_wrapper_ila_db.xsa ~/dev_linux/device-tree-xlnx $PWD/dts

4. Проверка сборки:

Базовое дерево собирается так же, как раньше, но с -@:

gcc -E -nostdinc -undef -D__DTS__ -x assembler-with-cpp -I dts -I dts/include dts/system-top.dts -o pp.dts
dtc -@ -I dts -O dtb -o devicetree.dtb pp.dts

А сам оверлей — pl.dtsi — ничего не подтягивает через include, поэтому препроцессор не нужен, он собирается напрямую:

dtc -@ -O dtb -o pl.dtbo -I dts dts/pl.dtsi

Что получилось

  • system-top.dts — в overlay-режиме почти пустой, только точечные изменения (chosen, mac-адрес gem0).

  • pl.dtsi — собственно PL-часть. HSI оборачивает все PL-периферийные узлы не в существующую PS-шину amba, а в новую ноду amba_pl: pl-bus (compatible = "simple-bus") — это отдельный simple-bus, а не фрагмент существующей amba: axi { ... } из zynq-7000.dtsi:

    • &fpga_full { firmware-name = "system_wrapper_ila_db.bit.bin"; ... };

    • amba_pl { ... } — узел со всеми PL-периферийными узлами: пять AXI GPIO (dds_ctrl, dds_ftw, lfm_start, lfm_stop, lfm_incr, адреса 0x4121_0000–0x4125_0000) и debug_bridge_0 по адресу 0x43c00000 — уже в готовом виде.

  • zynq-7000.dtsi, pcw.dtsi, skeleton.dtsi, include/ — общие файлы SoC из репозитория, нужны только базовому дереву.

  • system.dts, device-tree.mss — служебные файлы HSI.

На что стоит обратить внимание

  • Xilinx помечает DTG как устаревший. Новый путь — sdtgen + lopper (System Device Tree), оба есть в той же папке Vitis/bin. Для Zynq-7000 и классической сборки ядра DTG пока проще.

Из всего этого набора нам нужна нода debug_bridge_0 — и в overlay-режиме она уже лежит готовой внутри pl.dtsi, в новой ноде amba_pl (см. выше), руками её вытаскивать из полного дерева и переоформлять не требуется.

Сформированная нода выглядит примерно так:

debug_bridge_0: debug_bridge@43c00000 {
	clock-names = "s_axi_aclk";
	clocks = <&clkc 15>;
	compatible = "xlnx,debug-bridge-3.0", "generic-uio";
	reg = <0x43c00000 0x10000>;
	xlnx,bscan-mux = <0x1>;
	xlnx,build-revision = <0x0>;
	xlnx,chip-id = <0x0>;
	xlnx,clk-input-freq-hz = <0x11e1a300>;
	xlnx,core-major-ver = <0x1>;
	xlnx,core-minor-alpha-ver = <0x61>;
	xlnx,core-minor-ver = <0x0>;
	xlnx,core-type = <0x1>;
	xlnx,dclk-has-reset = <0x0>;
	xlnx,debug-mode = <0x2>;
	xlnx,design-type = <0x0>;
	xlnx,device-family = <0x0>;
	xlnx,en-bscanid-vec = "false";
	xlnx,en-int-sim = <0x1>;
	xlnx,en-passthrough = <0x0>;
	xlnx,enable-clk-divider = "false";
	xlnx,fifo-style = "SUBCORE";
	xlnx,ir-id-instr = <0x0>;
	xlnx,ir-user1-instr = <0x0>;
	xlnx,ir-width = <0x0>;
	xlnx,major-version = <0xe>;
	xlnx,master-intf-type = <0x1>;
	xlnx,minor-version = <0x1>;
	xlnx,num-bs-master = <0x1>;
	xlnx,pcie-ext-cfg-base-addr = <0x400>;
	xlnx,pcie-ext-cfg-next-ptr = <0x000>;
	xlnx,pcie-ext-cfg-vsec-id = <0x0008>;
	xlnx,pcie-ext-cfg-vsec-length = <0x020>;
	xlnx,pcie-ext-cfg-vsec-rev-id = <0x0>;
	xlnx,tck-clock-ratio = <0x8>;
	xlnx,two-prim-mode = "false";
	xlnx,use-bufr = <0x0>;
	xlnx,use-ext-bscan = "true";
	xlnx,use-softbscan = <0x1>;
	xlnx,use-startup-clk = "false";
	xlnx,user-scan-chain = <0x1>;
	xlnx,xsdb-num-slaves = <0x0>;
	xlnx,xvc-hw-id = <0x0001>;
	xlnx,xvc-sw-id = <0x0001>;
};

После формирования мой dts overlay выглядел следующим образом (в моем присутствовал не только debug bridge). Для Вашего конкретного случая нужно будет оставить только необходимые элементы.

Обратите внимание: debug_bridge_0 в сгенерированном pl.dtsi лежит внутри новой ноды amba_pl: pl-bus, а не внутри существующей PS-шины amba. Если в вашем базовом devicetree нет заранее заведённой amba_pl (а в моём случае её не было), ссылка &amba_pl в оверлее не разрешится. Поэтому target фрагмента я указал корень дерева &{/} — это надёжно работает независимо от того, объявлена ли у вас amba_pl в базовом дереве:

/dts-v1/;
/plugin/;

&spi0 {
	status = "okay";
	#address-cells = <1>;
	#size-cells = <0>;

	ad9361: ad9361@0 {
		compatible = "rohm,dh2228fv";
		status = "okay";
		reg = <0>;
		spi-cpha;
		spi-max-frequency = <10000000>;
	};
};

&{/} {
	debug_bridge_0: debug_bridge@43c00000 {
		clock-names = "s_axi_aclk";
		clocks = <&clkc 15>;
		compatible = "xlnx,debug-bridge-3.0", "generic-uio";
		reg = <0x43c00000 0x10000>;
		xlnx,bscan-mux = <0x1>;
		xlnx,build-revision = <0x0>;
		xlnx,chip-id = <0x0>;
		xlnx,clk-input-freq-hz = <0x11e1a300>;
		xlnx,core-major-ver = <0x1>;
		xlnx,core-minor-alpha-ver = <0x61>;
		xlnx,core-minor-ver = <0x0>;
		xlnx,core-type = <0x1>;
		xlnx,dclk-has-reset = <0x0>;
		xlnx,debug-mode = <0x2>;
		xlnx,design-type = <0x0>;
		xlnx,device-family = <0x0>;
		xlnx,en-bscanid-vec = "false";
		xlnx,en-int-sim = <0x1>;
		xlnx,en-passthrough = <0x0>;
		xlnx,enable-clk-divider = "false";
		xlnx,fifo-style = "SUBCORE";
		xlnx,ir-id-instr = <0x0>;
		xlnx,ir-user1-instr = <0x0>;
		xlnx,ir-width = <0x0>;
		xlnx,major-version = <0xe>;
		xlnx,master-intf-type = <0x1>;
		xlnx,minor-version = <0x1>;
		xlnx,num-bs-master = <0x1>;
		xlnx,pcie-ext-cfg-base-addr = <0x400>;
		xlnx,pcie-ext-cfg-next-ptr = <0x000>;
		xlnx,pcie-ext-cfg-vsec-id = <0x0008>;
		xlnx,pcie-ext-cfg-vsec-length = <0x020>;
		xlnx,pcie-ext-cfg-vsec-rev-id = <0x0>;
		xlnx,tck-clock-ratio = <0x8>;
		xlnx,two-prim-mode = "false";
		xlnx,use-bufr = <0x0>;
		xlnx,use-ext-bscan = "true";
		xlnx,use-softbscan = <0x1>;
		xlnx,use-startup-clk = "false";
		xlnx,user-scan-chain = <0x1>;
		xlnx,xsdb-num-slaves = <0x0>;
		xlnx,xvc-hw-id = <0x0001>;
		xlnx,xvc-sw-id = <0x0001>;
	};
};

Данный dts overlay уже можно компилировать, и прошивать на плату bitstream с наложением оверлея через FpgaManager.

Общаемся через mmap uio

В данном случае нам нужно создать uio ноду, в которой мы укажем наш адрес. После чего будет произведено отражение физической памяти на виртуальную, и по полученному указателю мы сможем писать в указанную область памяти.

Ноду для этого мы уже получили автоматически — debug_bridge_0 из pl.dtsi с compatible = "xlnx,debug-bridge-3.0", "generic-uio";


Вторая строка в compatible — это как раз то, по чему ядро может забиндить её к обычному UIO-драйверу, никакого написания своей платформенной привязки не требуется. Но само по себе это не произойдет, и дальше нужно добавить ещё несколько вещей, чтобы всё это заработало.

1. Конфиг ядра. Нужны CONFIG_UIO=y и CONFIG_UIO_PDRV_GENIRQ=y — это тот самый uio_pdrv_genirq, через который мы и будем биндить ноду с compatible = "generic-uio" (но одного конфига для этого мало, см. нюанс ниже).
У меня в buildroot это отдельный пакет xvc-uio, который не собирает никакого кода, а только доправляет конфиг ядра:

define XVC_UIO_LINUX_CONFIG_FIXUPS
	$(call KCONFIG_ENABLE_OPT,CONFIG_UIO)
	$(call KCONFIG_ENABLE_OPT,CONFIG_UIO_PDRV_GENIRQ)
endef

Обратите внимание — в devicetree-ноде нет interrupts, и это не недосмотр: uio_pdrv_genirq спокойно пробирается и без IRQ-ресурса, а сам протокол XVC — целиком поллинговый.

Важный нюанс:

одного compatible = "generic-uio" недостаточно .

У uio_pdrv_genirq нет строки generic-uio в собственной таблице of_match — строка, по которой он матчится, задается параметром модуля of_id.
Если его не передать, драйвер к ноде не привяжется:

нода debug_bridge@43c00000 в дереве есть, платформенное устройство 43c00000.debug_bridge создано, а /dev/uio* нет, и в dmesg по этому поводу тишина.
Ровно на это я и наткнулся при проверке на железе.

Так как у меня uio_pdrv_genirq встроен в ядро (=y), а не собран модулем, параметр передается через командную строку ядра (bootargs):

uio_pdrv_genirq.of_id=generic-uio

Проверить, что получилось, можно так:

cat /proc/cmdline
ls -l /sys/bus/platform/devices/43c00000.debug_bridge/driver
ls /sys/class/uio/

Если пересобирать образ или менять bootargs прямо сейчас не хочется, драйвер можно привязать руками, без перезагрузки — через driver_override:

echo uio_pdrv_genirq > /sys/bus/platform/devices/43c00000.debug_bridge/driver_override
echo 43c00000.debug_bridge > /sys/bus/platform/drivers_probe

После этого появляется /dev/uio0, и в /sys/class/uio/uio0/name будет debug_bridge@43c00000, а в maps/map0/addr — 0x43c00000.
Но это действует только до перезагрузки (и до повторного наложения оверлея), так что для постоянного решения все-таки стоит прописать of_id в bootargs.

2. Userspace-сервер. Собирается из официального Xilinx/XilinxVirtualCable (пакет xvc-server), но есть нюанс: upstream xvcServer.c безусловно объявляет

#define USE_IOCTL

#ifndef USE_IOCTL

до проверки #ifndef, из-за чего mmap-таргет Makefile (который должен собираться без -DUSE_IOCTL) на деле молча собирал тот же ioctl-бинарник — макрос был захардкожен прямо в исходнике, а не только в сборочном флаге. Пришлось патчить, убрав безусловный #define:

-#define USE_IOCTL
-
 #ifndef USE_IOCTL

(полный патч у меня лежит как 0001-user-fix-USE_IOCTL-hardcoded-so-mmap-build-works.patch в buildroot_external/package/xvc-server).
Без него xvcServer_mmap собирался бы, но по факту пытался открыть /dev/xilinx_xvc_driver вместо /dev/uioN.

3. Что происходит под капотом. xvcServer_mmap открывает UIO-устройство и мапит ровно те же 0x10000 байт, что указаны в reg нашей ноды:

fd_uio = open(UIO_PATH, O_RDWR);
ptr = (volatile jtag_t*) mmap(NULL, MAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd_uio, 0);

и дальше работает напрямую с пятью регистрами Debug Bridge (From AXI to BSCAN) по фиксированным смещениям:

typedef struct {
    uint32_t length_offset; // 0x00
    uint32_t tms_offset;    // 0x04
    uint32_t tdi_offset;    // 0x08
    uint32_t tdo_offset;    // 0x0C
    uint32_t ctrl_offset;   // 0x10
} jtag_t;

Запись ctrl_offset = 0x01 запускает сдвиг JTAG-цепочки, а завершение — это busy-poll того же регистра:

while (ptr->ctrl_offset) {}

Именно поэтому прерывание не нужно: обмен полностью синхронный со стороны userspace, uio_pdrv_genirq тут используется просто как способ получить mmap-доступ к региону, а не как источник событий.

4. Сборка и запуск. В defconfig платы включены оба пакета:

BR2_PACKAGE_XVC_UIO=y
BR2_PACKAGE_XVC_SERVER=y

это даёт бинарник /usr/bin/xvcServer_mmap.

Перед первым запуском стоит проверить, какой /dev/uioN достался именно debug_bridge — порядок появления устройств не гарантирован, особенно если в системе есть другие UIO-ноды:

for f in /sys/class/uio/uio*/name; do echo "$f: $(cat "$f")"; done

и передать нужное устройство явно (в коде по умолчанию зашит /dev/uio0, но это переопределяется флагом -d):

xvcServer_mmap -d /dev/uio0 -p 2542 -v

Порт по умолчанию — 2542, тот самый, который слушает Vivado Hardware Manager при подключении по XVC.
/dev/uioN по умолчанию доступен только root — если сервис запускается не под root, понадобится udev-правило на смену владельца/прав.

Общаемся через ioctl

Здесь же нам нужно будет общаться с драйвером посредством ioctl, что также поддержано у Xilinx.

В отличии от uio, тут просто взять готовую ноду не получится — придётся кое-что добавить руками, и в devicetree, и в сборку.

1. Devicetree.

Драйвер xilinx_xvc_driver биндится не по generic-uio, а по строго заданной строке compatible = "xlnx,xvc" (это захардкожено в xvc_driver.h как DEBUG_BRIDGE_COMPAT_STRING).
Наша сгенерированная нода этого не содержит, поэтому её нужно переопределить ещё одним фрагментом поверх pl.dtsi — ровно так, как это и предлагает сам Xilinx в xvc_fragment.dts:

&debug_bridge_0 {
	compatible = "xlnx,xvc";
};

Важный нюанс merge devicetree: свойство compatible при наложении фрагмента не дополняется, а полностью заменяется. То есть после появления этого фрагмента у debug_bridge_0 останется только "xlnx,xvc" — "xlnx,debug-bridge-3.0" и "generic-uio" из него пропадут.

Адреса в этом фрагменте нет, и это не ошибка. Фрагмент не создает новую ноду, а только переписывает свойство compatible у уже существующей debug_bridge_0 — она к этому моменту уже лежит в дереве (из основного оверлея), и reg = <0x43c00000 0x10000> у нее остается прежним. Поэтому такой фрагмент имеет смысл только поверх основного оверлея, и тот должен быть собран с -@, чтобы меткаdebug_bridge_0 была видна.

Если же мы сразу собираемся работать через ioctl, то никакой фрагмент не нужен — достаточно в основном оверлее сразу указать compatible = "xlnx,xvc".

Полный оверлей для моей платы под ioctl-вариант выглядит так (SPI-часть для AD9361 оставил, как и в UIO-варианте, для вашего случая лишнее можно убрать):

/dts-v1/;
/plugin/;

&spi0 {
	status = "okay";
	#address-cells = <1>;
	#size-cells = <0>;

	ad9361: ad9361@0 {
		compatible = "rohm,dh2228fv";
		status = "okay";
		reg = <0>;
		spi-cpha;
		spi-max-frequency = <10000000>;
	};
};


&{/} {
	debug_bridge_0: debug_bridge@43c00000 {
		clock-names = "s_axi_aclk";
		clocks = <&clkc 15>;
		compatible = "xlnx,xvc";
		reg = <0x43c00000 0x10000>;
		xlnx,bscan-mux = <0x1>;
		xlnx,build-revision = <0x0>;
		xlnx,chip-id = <0x0>;
		xlnx,clk-input-freq-hz = <0x11e1a300>;
		xlnx,core-major-ver = <0x1>;
		xlnx,core-minor-alpha-ver = <0x61>;
		xlnx,core-minor-ver = <0x0>;
		xlnx,core-type = <0x1>;
		xlnx,dclk-has-reset = <0x0>;
		xlnx,debug-mode = <0x2>;
		xlnx,design-type = <0x0>;
		xlnx,device-family = <0x0>;
		xlnx,en-bscanid-vec = "false";
		xlnx,en-int-sim = <0x1>;
		xlnx,en-passthrough = <0x0>;
		xlnx,enable-clk-divider = "false";
		xlnx,fifo-style = "SUBCORE";
		xlnx,ir-id-instr = <0x0>;
		xlnx,ir-user1-instr = <0x0>;
		xlnx,ir-width = <0x0>;
		xlnx,major-version = <0xe>;
		xlnx,master-intf-type = <0x1>;
		xlnx,minor-version = <0x1>;
		xlnx,num-bs-master = <0x1>;
		xlnx,pcie-ext-cfg-base-addr = <0x400>;
		xlnx,pcie-ext-cfg-next-ptr = <0x000>;
		xlnx,pcie-ext-cfg-vsec-id = <0x0008>;
		xlnx,pcie-ext-cfg-vsec-length = <0x020>;
		xlnx,pcie-ext-cfg-vsec-rev-id = <0x0>;
		xlnx,tck-clock-ratio = <0x8>;
		xlnx,two-prim-mode = "false";
		xlnx,use-bufr = <0x0>;
		xlnx,use-ext-bscan = "true";
		xlnx,use-softbscan = <0x1>;
		xlnx,use-startup-clk = "false";
		xlnx,user-scan-chain = <0x1>;
		xlnx,xsdb-num-slaves = <0x0>;
		xlnx,xvc-hw-id = <0x0001>;
		xlnx,xvc-sw-id = <0x0001>;
	};
};

От UIO-варианта он отличается только строкой compatible. Адрес и размер окна драйвер берет из reg, остальные свойства xlnx,* он не читает — они остались от сгенерированного HSI узла, и их можно не трогать.

2. Сборка модуля. В отличие от xvc-uio, который просто донастраивал Kconfig ядра, xvc-driver — это отдельный загружаемый модуль ядра (xilinx_xvc_driver.ko), собирается как обычный kernel-module пакет buildroot:

XVC_DRIVER_MODULE_SUBDIRS = jtag/zynqMP/src/driver

$(eval $(kernel-module))
$(eval $(generic-package))

Никаких CONFIG_UIO* опций ему не нужно — это самостоятельный platform_driver с собственным of_match_table:

static const struct of_device_id xvc_of_ids[] = {
	{ .compatible = DEBUG_BRIDGE_COMPAT_STRING, }, // "xlnx,xvc"
	{}
};

3. Важный нюанс с автозагрузкой. В исходнике модуля нет MODULE_DEVICE_TABLE(of, xvc_of_ids).

Без этого макроса depmod не генерирует alias в modules.alias, а значит udev/mdev не сможет автоматически подгрузить модуль по событию от появившейся в devicetree ноды — модуль нужно будет загружать вручную (modprobe xilinx_xvc_driver).

4. Что происходит под капотом. По умолчанию (xvc_user_config.h пустой) драйвер обслуживает только первую подходящую ноду и создаёт один символьный файл /dev/xilinx_xvc_driver, беря базовый адрес и размер прямо из reg через стандартный Linux resource API:

db_res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
db_ptrs[i] = devm_ioremap_resource(&pdev->dev, db_res);

то есть, в отличие от mmap-пути, тут вообще не нужно ничего мапить руками из userspace — адрес и размер драйвер узнаёт сам из devicetree. Если в проекте несколько debug_bridge, их можно перечислить в xvc_user_config.h (по образцу xvc_user_config_example.h) — тогда появятся /dev/xilinx_xvc_driver_0, /dev/xilinx_xvc_driver_1 и так далее, каждый под свою ноду. Для нашего случая с одним debug_bridge_0 это не требуется.

Регистровая карта та же самая, что и в mmap-варианте (LENGTH/TMS/TDI/TDO/CONTROL по смещениям 0x00/0x04/0x08/0x0C/0x10), но обработка идёт уже в ядре, через ioctl(XDMA_IOCXVC, ...):

#define XDMA_IOCXVC      _IOWR(XIL_XVC_MAGIC, 1, struct xil_xvc_ioc)
#define XDMA_RDXVC_PROPS _IOR(XIL_XVC_MAGIC, 2, struct xil_xvc_properties)

Второй ioctl, XDMA_RDXVC_PROPS, отдаёт userspace-у адрес, размер и compatible-строку моста — это как раз то, что xvcServer_ioctl печатает при старте через display_driver_properties() (кусок кода мы уже видели в разделе про mmap).

5. Сборка и запуск. В defconfig платы уже включён нужный пакет:

BR2_PACKAGE_XVC_DRIVER=y

это даёт /lib/modules/.../extra/xilinx_xvc_driver.ko и бинарник /usr/bin/xvcServer_ioctl. Порядок действий на плате:

# после наката PL-оверлея с фрагментом compatible = "xlnx,xvc"
modprobe xilinx_xvc_driver
ls /dev/xilinx_xvc_driver
xvcServer_ioctl -p 2542 -v

Путь к устройству, как и в mmap-варианте, можно переопределить флагом -d, если используется multi-bridge конфигурация с суффиксами 0, 1 и т.д.

6. Проверка на железе: переключаемся с UIO на ioctl без перезагрузки.

У меня на плате к этому моменту уже был наложен основной оверлей с generic-uio и работал xvcServer_mmap, поэтому ioctl-вариант я проверял прямо поверх него, без пересборки образа и перезагрузки.

Сам фрагмент — отдельный маленький оверлей, который ссылается на лейбл debug_bridge_0 из основного (поэтому основной обязательно должен быть собран с -@, чтобы в нем был symbols):

/dts-v1/;
/plugin/;

&debug_bridge_0 {
	compatible = "xlnx,xvc";
};
dtc -@ -I dts -O dtb -o xvc-ioctl.dtbo xvc-ioctl.dts

Модуль собирается против того же дерева ядра, что и образ (у меня это output/build/linux-custom из buildroot), и тем же тулчейном:

make -C <linux-build-dir> M=$PWD ARCH=arm CROSS_COMPILE=<toolchain>/arm-linux- modules

Дальше на плате по шагам. Сначала освобождаем мост от UIO: убираем xvcServer_mmap и отвязываем устройство от uio_pdrv_genirq (если привязывали через driver_override, его тоже нужно очистить, иначе устройство так и останется закреплено за UIO-драйвером):

echo 43c00000.debug_bridge > /sys/bus/platform/drivers/uio_pdrv_genirq/unbind
echo > /sys/bus/platform/devices/43c00000.debug_bridge/driver_override

После этого /dev/uio0 пропадает. Накладываем фрагмент вторым оверлеем через configfs:

mkdir /sys/kernel/config/device-tree/overlays/xvc
cat xvc-ioctl.dtbo > /sys/kernel/config/device-tree/overlays/xvc/dtbo
cat /sys/kernel/config/device-tree/overlays/xvc/status   # applied
tr '\0' ' ' < /sys/firmware/devicetree/base/debug_bridge@43c00000/compatible

Последняя команда выводит только xlnx,xvc — заменяя compatible целиком.

И загружаем модуль руками (автозагрузки, как и ожидалось, нет):

insmod xilinx_xvc_driver.ko
ls -l /sys/bus/platform/devices/43c00000.debug_bridge/driver
ls -l /dev/xilinx_xvc_driver
xvcServer_ioctl -p 2542

В отличие от UIO, тут никаких параметров ядра и driver_override не понадобилось: драйвер сам привязался к ноде по compatible = "xlnx,xvc", в dmesg появилось xilinx_xvc_driver: Created device xilinx_xvc_driver. Vivado после этого точно так же увидел debug_bridge_0, ILA и все пробы.

Смотрим ILA в Hardware Manager

Открываем Hardware Manager (на скриншоте уже виден ранее добавленный таргет xilinx_tcf/Xilinx/192.168.0.7:2542 в статусе Closed — сам кабель добавляется следующим шагом, просто Vivado запоминает его между запусками):

Add Virtual Cable
Далее необходимо ввести ip адрес нашей платы.

После чего появится debug_bridge_0:


Далее нужно добавить .ltx файл, который был сгенерирован при сборке битстрима:

debug_bridge_0 в дереве устройств

После чего появится окно waveform, где будут находиться уже добавленные нами сигналы:

Ну и заодно можно посмотреть, что творится на выходе AD9361 — в качестве приёмника тут выступает отдельный PlutoSDR, который принимает сигнал, излучаемый нашей платой (это не прямой дамп с самой RK-7020F, а эфирная проверка со стороны). В принципе наблюдаем похожую картинку:

P. S. Если кому будет полезно - был написан небольшой gui для работы с libiio/hack rf, скрин с которого представлен выше.

Основной задачей является возможность быстро формировать и излучать типовые сигналы, а также смотреть принятый сигнал на ПЧ во временной области на узкой полосе в realtime.
Достать deb или собрать приложение можно здесь:
https://github.com/FernandesKA/iq_forge_gui

Итого

В итоге получилось посмотреть ILA на плате без физического JTAG-программатора, подняв JTAG-over-Ethernet через Debug Bridge и XVC.

Благодарю всех за внимание, буду рад любой обратной связи!