Declarative Devices
The adidt.devices package is the single source of truth for how ADI
hardware maps to device-tree nodes. Every modeled device is a small
pydantic class whose fields are the DT properties it emits — no Jinja2
templates, no intermediate context dicts. A device’s render_dt
method returns the DTS text directly.
Design in three pieces
1. Typed fields with DT-property aliases. Each field on a
Device subclass carries the exact DT
property name via Field(alias="adi,..."). The Python attribute is
the human-facing handle; the alias is the on-disk name.
class HMC7044(ClockDevice):
compatible: ClassVar[str] = "adi,hmc7044"
dt_header: ClassVar[dict] = {"#clock-cells": 1, "#jesd204-cells": 2}
dt_flags: ClassVar[tuple] = ("jesd204-device",)
vcxo_hz: int = Field(..., alias="adi,vcxo-frequency")
pll2_output_hz: int = Field(..., alias="adi,pll2-output-frequency")
pll1_ref_autorevert: bool = Field(False, alias="adi,pll1-ref-autorevert-enable")
The adidt.devices._dt_render renderer walks these fields,
formats each value by its Python type, and emits:
adi,vcxo-frequency = <122880000>;
adi,pll2-output-frequency = <2949120000>;
adi,pll1-ref-autorevert-enable;
2. Field markers for non-scalar cases. Fields that don’t map 1:1
to a DT value use adidt.devices._fields markers, applied via
typing.Annotated:
DtSubnodes— adict[key, child]renders as child DT nodes (e.g. HMC7044 channels).DtSkip— excludes a field from rendering (Python-only state, ports, etc.).DtBits64— emit/bits/ 64 <N>for sampling/converter frequencies that don’t fit in a 32-bit cell.
3. An ``extra_dt_lines`` hook for coupled properties. Properties
that need System-supplied context (phandles, gpio_label) or that
render as tied pairs (clocks + clock-names) are emitted by
overriding extra_dt_lines(context).
def extra_dt_lines(self, context: dict | None = None) -> list[str]:
ctx = context or {}
if self.clkin0_ref is not None:
return [
f"clocks = <&{self.clkin0_ref}>;",
'clock-names = "clkin0";',
]
return []
Component device models
Clock distributors and PLLs:
adidt.devices.clocks.HMC7044adidt.devices.clocks.AD9523_1adidt.devices.clocks.AD9528adidt.devices.clocks.AD9528_1adidt.devices.clocks.ADF4382
Converters / MxFE transceivers:
adidt.devices.converters.AD9081(withAD9081Adc/AD9081Dac)adidt.devices.converters.AD9084(withAD9084Adc/AD9084Dac)adidt.devices.converters.AD9172adidt.devices.converters.AD9680adidt.devices.converters.AD9144adidt.devices.converters.AD9152
RF transceivers:
adidt.devices.transceivers.ADRV9009— reused for ADRV9025/9026/9029 (Talise silicon) and for AD9371/ADRV9371 (Mykonos silicon). The kernel binding differs per chip: setcompatible_strings=["adi,ad9371"]plusnode_name_base="ad9371-phy"for AD9371, otherwise the ADRV9009 default applies.
FPGA-side JESD204 IP overlays:
adidt.devices.fpga_ip.Adxcvr— AXI ADXCVR overlayadidt.devices.fpga_ip.Jesd204Overlay— AXI JESD204 RX/TX overlayadidt.devices.fpga_ip.TplCore— AXI TPL core overlay
Composition layer
The composition API lives in adidt.eval, adidt.fpga, and
adidt.system:
import adidt
fmc = adidt.eval.ad9081_fmc()
fmc.converter.set_jesd204_mode(18, "jesd204c")
fmc.converter.adc.sample_rate = int(250e6)
fmc.converter.adc.cddc_decimation = 4
fmc.converter.adc.fddc_decimation = 4
fpga = adidt.fpga.zcu102()
system = adidt.System(name="ad9081_zcu102", components=[fmc, fpga])
system.connect_spi(bus_index=0, primary=fpga.spi[0],
secondary=fmc.clock.spi, cs=0)
system.connect_spi(bus_index=1, primary=fpga.spi[1],
secondary=fmc.converter.spi, cs=0)
system.add_link(source=fmc.converter.adc, sink=fpga.gt[0],
sink_reference_clock=fmc.dev_refclk,
sink_core_clock=fmc.core_clk_rx,
sink_sysref=fmc.dev_sysref)
print(system.generate_dts())
adidt.system.System— collects devices + connection records, produces aBoardModel, delegates toBoardModelRendererfor DTS emission.adidt.eval.EvalBoardsubclasses (ad9081_fmc,ad9084_fmc,adrv937x_fmc) pre-wire a clock chip and a converter with the schematic-level channel assignments a specific FMC expects, and expose named clock-output aliases (fmc.dev_refclk,fmc.fpga_sysref, …).adidt.fpga.FpgaBoardsubclasses (zcu102,vpk180,zc706) hold platform constants: address-cells, PS clock label/index, GPIO controller, SPI masters, GT lane count, default QPLL selection.
Writing a new device
See Authoring a new device class for the full walkthrough —
class hierarchy, end-to-end call flow from
adidt.system.System.generate_dts() through
adidt.devices._dt_render.render_node(), and cookbook recipes for
adding a new clock, converter / transceiver, or eval-board / FPGA-board
class. adidt.devices.clocks.hmc7044 is the canonical full
reference implementation.
End-to-end hardware verification
Hardware tests live in test/hw/. Each one exercises every stage —
XSA parsing → sdtgen → DTS generation → DTB compile → labgrid boot →
IIO + JESD204 link verification on real hardware — and covers all three
supported boot strategies (SD-card, TFTP, and MicroBlaze/JTAG):
Test module |
Board + carrier |
Boot strategy |
Path |
|---|---|---|---|
|
AD9081 + ZCU102 |
|
Declarative |
|
AD9081 + ZCU102 |
|
|
|
ADRV9009 + ZCU102 |
|
|
|
ADRV9371 + ZC706 |
|
|
|
FMCDAQ3 + VCU118 |
|
Prebuilt simpleImage (smoke test) |
Tests support two connection modes, selected by .env at the project
root (loaded via pytest-dotenv):
Coordinator mode — set
LG_COORDINATOR=<host>:<port>plusLG_ENV=<test/hw/env/*.yaml>. The env YAML binds aRemotePlaceto the coordinator-published resources.Direct mode — set
LG_ENV=<local_yaml>only.
Each board family has a dedicated single-target env file under
test/hw/env/: mini2.yaml (ZCU102 + AD9081),
bq.yaml (ZC706 + ADRV9371), and nuc.yaml (VCU118 + FMCDAQ3).
The combined test/hw/env/all.yaml exposes all three as named
targets.
Example — run the ADRV9371+ZC706 test via a coordinator:
LG_COORDINATOR=10.0.0.41:20408 \
LG_ENV=test/hw/env/bq.yaml \
pytest test/hw/test_adrv9371_zc706_hw.py
Copy .env.example to .env for the supported variables. The skip
guard in each hw test module requires one of LG_COORDINATOR or
LG_ENV to be set.
Kernel image caching
test/hw/conftest.py provides session-scoped built_kernel_image_*
fixtures backed by a file-based cache keyed on the sha256 of the
pyadi-build YAML config. First run per config builds through
pyadi-build and copies the produced kernel image into
~/.cache/adidt/kernel/<platform>/<hash>/<image>; subsequent runs
skip prepare_source + build entirely. Zynq-7000 zImage is
wrapped as a uImage via mkimage so BootFPGASoCTFTP’s
tftpboot uImage can find the file.
Control via env vars:
ADIDT_KERNEL_CACHE=0— force a rebuild.ADIDT_KERNEL_CACHE_DIR=<path>— relocate the cache (default~/.cache/adidt/kernel).
Measured: first ZC706 run ≈ 600 s; cached re-run ≈ 70 s.