ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Zynq平台上AD9361驱动调试实战:从设备树到IIO链路完全指南

Zynq平台上AD9361驱动调试实战:从设备树到IIO链路完全指南 手里这块板子Zynq-7020加AD9361参考设计跑起来一切正常换成自己PCB以后问题全来了串口停在U-Boot之后没反应内核起来后dmesg里只有一行不到十个字的“AD9361 probe failed”iio_info下面空空荡荡频率写不进去PLL死活不锁。这篇文章就围绕一个主题怎么在PetaLinux工程里把AD9361的IIO驱动真正跑通设备树怎么改、初始化卡住时怎么定位。适合正在用Zynq/ZynqMP这类平台做SDR项目的工程师参考就算你用的不是PetaLinux而是Yocto或者Buildroot内核配置和设备树的思路也一样能用。1. 先搞清楚AD9361在Linux里的驱动架构1.1 IIO子系统和AD9361驱动的位置很多人一上来就照着教程改设备树改完probe还是失败就开始怀疑是不是语法错误其实很多问题出在没搞明白AD9361驱动在内核里的工作方式。这颗芯片在Linux下的驱动是ADI维护的标准驱动归属于IIO子系统源码位置在内核树里的drivers/iio/adc目录下。IIO平时大家接触最多的是传感器比如加速度计、温度计但它的抽象方式很适合AD9361这类射频收发设备一个设备下面挂多个channel每个channel带一组属性用户空间通过统一接口读写比直接写一个字符设备驱动要规范得多。AD9361驱动注册出来的IIO设备名通常是ad9361-phy下面挂的channel包括RX/TX两路的I/Q、本振、增益、带宽、采样率等。用户空间想改中心频率不是直接写SPI寄存器而是通过libiio去写本振channel对应的frequency属性驱动内部完成从频率到寄存器值的换算。这个抽象很关键它把芯片那么多页寄存器藏在了标准接口后面但也意味着设备树里某个属性配错最终表现可能是在用户空间写一个普通的频率数值底层寄存器就被配置成一个不可能的值然后PLL失锁。1.2 从裸机no-OS到Linux设备的对应关系ADI官方提供两套软件参考no-OS跑在裸机上另一套就是Linux IIO驱动。两边代码完全不同但概念是对应的。no-OS里用户在ad9361_init_param这个结构体里填的RX频率、TX频率、带宽、增益模式、滤波器配置在Linux设备树里就是AD9361节点下一长串以adi,开头的属性。很多从裸机直接跳过来的工程师第一次看设备树会懵属性名又长又多但静下心对照芯片手册会发现它们都能一一对应到初始化参数或寄存器配置。我调试这种板卡的习惯是拿到新硬件先用no-OS例程在SDK里把AD9361初始化跑通读版本号、看PLL是否锁定。这一步过了再进Linux能省掉大量排查时间。因为Linux下probe失败时你很难判断到底是驱动配置问题还是硬件本身没准备好。1.3 一整套收发链路包含四个部分一个完整的AD9361收发链路在Linux里至少有四部分协作内核里的IIO驱动负责通过SPI配置芯片FPGA里的AXI_AD9361 IP负责和芯片的数字接口对接AXI DMAC负责把IQ数据从FPGA搬到DDR用户空间的libiio库和工具负责把数据送进应用层。这四层只要有一层断了应用层都拿不到数据。所以设备树里不止有SPI总线下的ad9361-phy节点还要有AXI总线上挂的rx/tx节点和DMA节点。很多人只盯SPI那部分ad9361-phy倒是probe成功了频率也写得进去但iio_readdev抓不到数据最后折腾半天发现是DMA设备树节点没写。这个顺序我后面会展开讲。2. PetaLinux工程组装从XSA到可启动镜像2.1 版本链是第一个坑在PetaLinux里做AD9361驱动第一件事不是写设备树而是确认版本链。Vivado、PetaLinux、内核版本必须配套。Xilinx的PetaLinux是跟着Vivado版本走的2019.1就是2019.12020.2就是2020.2混搭很容易出现内核启动阶段设备树解析报错这类诡异问题。从2020.1开始硬件描述文件从传统的HDF换成了XSApetalinux-config --get-hw-description后面可以直接指xsa文件。另外还要考虑ADI的HDL参考设计和Vivado版本的匹配。ADI官方HDL仓库对每个版本有明确说明比如某个版本要求Vivado 2020.2你就不要拿2019.1去编否则AXI_AD9361的IP版本对不上综合出来可能都是问题。我踩过一回Vivado 2020.2建的工程用PetaLinux 2021.1去编译内核起来后设备树解析一堆报错后来退回配套版本才稳定。2.2 创建工程并接入硬件描述文件先创建PetaLinux工程以ZynqMP为例petalinux-create -t project --name ad9361_sdr --template zynqMP cd ad9361_sdr petalinux-config --get-hw-description../hardware/adi_sdr.xsa执行之后PetaLinux会从XSA中提取硬件平台信息自动生成一部分设备树。这一步跑完不要急着build先进配置菜单看一下petalinux-config -c kernel在Device Drivers - Industrial I/O support下确认AD9361驱动相关配置。新版内核里的选项路径可能不太一样直接在配置界面按/搜索AD9361把它设为y而不是m。编成模块就得额外处理模块加载顺序和rootfs打包多一层麻烦。另外确保IIO核心、DMA相关的驱动也都打开比如AD9361的AXI数据通路依赖的AXI DMAC驱动通常在FPGA桥和DMA控制器相关配置里。2.3 rootfs里补上libiio和调试工具内核光有驱动还不够用户空间要能用工具操作设备。在PetaLinux里通过下面命令配置rootfspetalinux-config -c rootfs在user packages或者libs里勾选libiio、iiod、iio-tools。iiod是后台服务启动后可以让你在PC上通过libiio的网络后端远程访问目标板上的设备对开发调试特别方便。iio-tools则提供iio_info、iio_readdev、iio_writedev、iio_attr、iio_reg这些命令行工具基本就是调试IIO设备的主力。另外要想在设备树里正确解析GPIO控制比如复位引脚记得确认内核里GPIO控制器驱动已经编进去。Zynq系列的GPIO驱动一般是默认打开的但有些人裁剪内核时会把gpio-zynq相关的配置关掉结果设备树里reset-gpios属性看起来写了实际驱动拿不到引脚初始化就乱套。2.4 外部内核补丁和设备树补丁怎么塞进工程PetaLinux工程里改文件不是直接改内核源码目录而是通过bbappend文件来引入补丁。比如要打一个内核patch在project-spec/meta-user下面建recipes-kernel/linux目录放一个linux-xlnx_%.bbappend文件FILESEXTRAPATHS_prepend : ${THISDIR}/files: SRC_URI file://ad9361-fix.patch然后往同目录下的files子目录丢进ad9361-fix.patch重新petalinux-build补丁就会在编译内核时自动应用。设备树补丁的机制类似位置在recipes-bsp/device-tree/files下面不过设备树一般用不着打补丁直接改system-user.dtsi就够了。这一点正好回答很多新手的问题怎么通过PetaLinux编译外部的设备树和内核。核心思路就是通过meta-user层把外部源文件和补丁引入构建系统别去动生成目录下面的文件因为导入新的XSA时会被覆盖。3. 设备树定制每个节点都在描述硬件事实3.1 顶层SPI节点和复位信号AD9361挂在某个SPI控制器下面Zynq上有SPI0/SPI1。设备树里首先要保证对应的SPI控制器status是okay然后在它下面挂ad9361-phy子节点reg指定片选编号spi0 { status okay; ad9361-phy0 { compatible adi,ad9361; reg 0; spi-max-frequency 10000000; reset-gpios gpio0 47 0; enable-gpios gpio0 48 0; }; };这里最容易被忽略的是reset-gpios。驱动初始化时会把复位引脚拉低再拉高给芯片一个干净的复位。如果这个引脚在设备树里没定义芯片可能处于上电后的默认状态但各种数字接口的电平状态不可控初始化时序很容易错。enable-gpios对应ENSM相关的使能控制如果硬件设计里ENSM引脚接的是GPIO而不是用SPI寄存器控制这行一定要写对否则芯片的状态机切不到收发模式。3.2 AD9361节点的厂商属性AD9361节点的重点在厂商属性也就是那些adi,开头的项。这些属性在驱动初始化时会被解析映射到芯片寄存器。给一个常见配置片段ad9361-phy0 { compatible adi,ad9361; reg 0; spi-max-frequency 10000000; clocks ad9528 1; clock-names ad9361_ext_refclk; reset-gpios gpio0 47 0; enable-gpios gpio0 48 0; adi,spi-3wire-enabled; adi,rx-synthesizer-frequency-hz /bits/ 64 2400000000; adi,tx-synthesizer-frequency-hz /bits/ 64 2450000000; adi,rf-rx-bandwidth-hz /bits/ 64 18000000; adi,rf-tx-bandwidth-hz /bits/ 64 18000000; adi,rx-rf-port-input-select 0; adi,tx-rf-port-input-select 0; adi,filter-fir-enable; adi,rx-fir-decimation 2; adi,tx-fir-interpolation 2; adi,bb-dc-offset-tracking-enable; adi,quadrature-tracking-enable; };解释几个关键属性。adi,rx-synthesizer-frequency-hz和adi,tx-synthesizer-frequency-hz是上电后的初始本振频率类型是64位整数所以前面必须写/bits/ 64这是很多人容易写错的地方。adi,rf-rx-bandwidth-hz和adi,rf-tx-bandwidth-hz是模拟链路上的射频带宽这个值要和实际应用匹配带宽设太窄信号会被滤掉设太宽又可能带进更多噪声。adi,spi-3wire-enabled表示使用三线SPI即MISO和MOSI共用一根线。如果硬件设计不是三线SPI这一行千万不要写否则驱动会用错IO方向配置导致读回来的寄存器值全是乱的。还有一个属性值得单独提醒adi,rx-fir-decimation和adi,tx-fir-interpolation。这两个值决定了数字FIR滤波器的抽取和插值倍数会影响最终数据接口的有效采样率。设备树里写2还是4一定要和FPGA那一侧AXI_AD9361 IP的期望采样率匹配起来两边对不上后面抓数据就是乱码。3.3 AXI数据通路与DMA节点AD9361的模拟射频信号经过内部ADC/DAC之后数字IQ数据通过LVDS或CMOS接口进入FPGA接着由ADI的AXI_AD9361 IP转换为AXI-Stream协议再经AXI DMAC搬到DDR内存。Linux驱动这边看到的节点一般长这样axi-ad9361-rx79020000 { compatible adi,axi-ad9361-rx-1.0; reg 0x79020000 0x1000; dmas axi_dmac_rx 0; dma-names rx; clocks ad9528 11; clock-names sampl_clk; }; axi_dmac_rx: dma7c420000 { compatible adi,axi-dmac-1.00.a; reg 0x7c420000 0x1000; interrupt-parent intc; interrupts 0 57 4; };dmas属性指向DMA节点这样IIO驱动在使能buffer时才知道把数据往哪个DMA通道送。如果只改了SPI下的ad9361-phy节点而AXI节点缺失那么设备树里就少了一半内容应用层能配置频率但拿不到流数据。检查这类问题时可以先看/sys/bus/platform/devices下面有没有对应的axi-ad9361-rx/tx节点名再比对Vivado工程里的地址映射确认reg值写对了没有。3.4 时钟树参考时钟和AD9528AD9361正常工作需要一个外部参考时钟范围通常在10MHz到80MHz之间最常见的配置是40MHz。这个时钟可以来自板载晶振也可以来自AD9528这类时钟分发芯片。设备树里通过clocks和clock-names属性指定clocks ad9528 1; clock-names ad9361_ext_refclk;如果板子上用的是AD9528那AD9528自己也要有设备树节点并在里面配置输入时钟频率和每一路输出的分频关系。很多初始化问题追根溯源都出在这里参考时钟频率和设备树里写的对不上或者AD9528输出没有稳定导致AD9361内部的BBPLL先失锁后续所有PLL都跟着起不来。参考时钟的波形质量也需要留意。用示波器量一下参考时钟引脚的上升沿是不是干净如果毛刺很多会直接影响鉴相器的稳定性表现为PLL偶尔锁定偶尔失锁。这种情况靠改设备树解决不了得回到硬件电路上去查时钟源的滤波和端接。3.5 system-user.dtsi的正确打开方式PetaLinux从硬件描述文件生成设备树后用户不要直接去改生成目录下的dts文件因为重新导入XSA会覆盖。官方指定的用户修改入口是system-user.dtsi位于project-spec/meta-user/recipes-bsp/device-tree/files/下面。在里面直接覆盖节点/include/ system-conf.dtsi / { }; spi0 { status okay; }; ad9361_phy { adi,rx-synthesizer-frequency-hz /bits/ 64 2400000000; };语法是引用设备树里已有的节点展开后就是给这个节点添加或覆盖属性。修改完之后重新petalinux-buildPetaLinux会把system-user.dtsi和生成的dts合并编译成最终dtb。验证改动是否生效可以在目标板上执行dtc -I fsdt -O dts /sys/firmware/devicetree/base | grep ad9361如果没装dtc直接把/sys/firmware/devicetree/base里的节点名列出来也能看个大概。4. 初始化失败实战排查0x247、CP OVRG与RX PLL失锁4.1 起点SPI读回固定值0x80意味着什么初始化失败最常遇到的一个现象是指定读某个SPI寄存器比如热搜索里提到的0x247不管怎么读返回都是0x80。这种固定值通常意味着SPI链路本身就有问题而不是0x247这个寄存器里真的正好存了0x80。先解释一下0x80这个特殊值它是bit7为1、其余位全0。如果SPI读时序稍微错位读回的第一个字节可能是命令字节的一部分或者芯片在MISO上输出的采样点不对就容易得到这种有规律但不正确的值。排查顺序是这样的。第一步确认读写命令的格式。AD9361的SPI命令分命令头和地址标准寻址和数据长度都是8位但芯片也支持更长的寻址模式。如果驱动或测试脚本里地址长度配置不对读出来的数据就会整体错位。拿示波器抓CSB、SCLK、MOSI、MISO四根线对照芯片手册的命令时序走一遍基本能看出问题。第二步确认地址是对的。有些寄存器属于不同的分页表读之前需要切表如果你拿着表1的地址去表0里读返回的自然不是期望值。这是我见过很多新手犯的错误他们对照手册找到寄存器地址却没注意芯片手册里同一地址在不同表中的含义完全不同。第三步也是最容易被忽略的检查硬件连线。AD9361的MISO如果上拉或下拉电阻不对或者与FPGA之间的电平不匹配主机读到的数据就可能被某根线钳位。0x80正好是bit7为1其他位为0如果MISO线上有个弱上拉而芯片输出三态时被读到就会出现这种像是什么都没读到、实际又有个规律值的现象。所以看到固定0x80时第一反应应该是怀疑SPI链路而不是急着查0x247寄存器本身的功能定义。4.2 CP OVRG标志电荷泵过压的排查链路另一个典型故障是初始化过程中状态寄存器里的CP OVRG标志被置位同时RX PLL没有锁定。CP OVRG是charge pump overrange的缩写意思是电荷泵输出电压已经超出了正常可调范围。PLL的基本原理是鉴相器比较参考时钟和反馈时钟的相位差然后控制电荷泵往环路滤波器里充放电产生一个控制电压去调VCO。如果电荷泵输出电压超出VCO调谐电压的正常范围VCO就永远调不到目标频率。CP OVRG标志的作用就是告诉你这个电压已经过头了。排查这个问题的链路是这样的。先用示波器量电荷泵输出引脚也就是环路滤波器电容两端看电压值是不是接近电源轨。正常工作时这个电压应该在VCO调谐电压范围的中间区域附近如果接近3.3V或更高基本可以断定电荷泵输出过压。然后回头检查环路滤波器的参数是不是和驱动里配置的PLL环路带宽匹配。AD9361的PLL环路参数主要由寄存器配置决定设备树里一般不会直接放一套电容电感值而是通过驱动默认参数或板级适配代码来匹配。如果硬件参考设计用的环路滤波器元件和驱动默认参数差太多就会出现CP OVRG。这种问题最好的参照物就是ADI官方评估板原理图看看自己的环路滤波器和评估板差异在哪。还有一个常见来源是PLL相关电源的问题。电荷泵供电如果没接对或者供电纹波太大电荷泵在输出端的电压检测就会异常也会导致CP OVRG被错误置位。排查时可以顺手量一下AD9361的各个模拟电源引脚电压别一上来就怀疑寄存器。4.3 RX PLL不锁定参考时钟和ENSM状态机RX PLL不锁定是初始化失败里最折磨人的一类因为报错信息常常只有一行timeout根本不告诉你为什么。AD9361内部有BBPLL、RX PLL、TX PLL三套锁相环先分清楚是哪一个没锁。BBPLL不锁驱动连内部寄存器配置都做不下去RX PLL不锁射频本振起不来接收通路自然不能用。第一种要查的是参考时钟这是PLL锁定最基础的条件。用示波器直接量EXT_REF_CLK引脚确认频率是不是设备树里写的值幅度是不是在芯片要求的范围内。我遇到过一块板子40MHz晶振焊接虚了示波器看起来有个脉冲但频率不对驱动初始化就卡在PLL等待超时上。这种硬件问题查设备树再久都查不出来。第二种要查的是频率配置是否合理。AD9361覆盖70MHz到6GHz但不同频段对应的VCO调谐范围和电感选型不同。如果目标频率设得离硬件设计支持范围太远VCO可能调不上去。往极端好处想驱动本身有频率范围检查一般不会让你写一个完全离谱的值但设在某个频段边界附近时PLL锁定慢或者失锁是完全可能的。第三种要查的是ENSM状态机。这一点真的值得单独拿出来说。AD9361的ENSM有几种工作模式FDD模式、TDD模式、以及依赖外部引脚控制还是SPI控制的说法。PLL并不是上电就锁而是要等ENSM进入对应收发状态之后才启动锁定流程。如果驱动配置成了TDD模式或者ENSM控制引脚接法不对芯片可能一直停留在ALERT状态你读PLL锁定状态位永远都是0。这就能解释为什么频率看起来写对了、参考时钟也对但PLL就是锁不住。要看当前ENSM状态可以通过寄存器读出来对照手册确认状态机的当前状态。4.4 把调试信息用到极致排查AD9361初始化问题时最常用的命令组合是这几条dmesg | grep -i ad9361 iio_info | grep ad9361 iio_reg ad9361-phy 0x247dmesg里驱动会打印初始化过程的错误信息包括具体是哪个校准步骤超时。比如LO calibration失败、gain table加载失败日志里的关键字能直接定位到哪一步。如果ad9361-phy根本没有出现在iio_info里先去看设备树SPI节点有没有被正确识别方法是在/sys/bus/spi/devices下面看有没有对应的设备目录。iio_reg是libiio提供的一个直接读写寄存器的工具它可以绕过用户空间属性接口直接操作SPI寄存器适合在驱动probe失败时手动探测芯片状态。比如手动读产品ID寄存器看能不能读回一个合理的版本号。如果连产品ID都读不对说明SPI通信都还没通后面的寄存器状态都不用看了。还有一种情况是驱动已经在初始化了但卡在中间的某个校准步骤。这时可以借助内核日志级别比如在启动参数里加initcall_debug或loglevel8把内核启动过程打得更详细能看到驱动probe函数执行到什么位置。这些日志虽然多但关键的那几行足够帮你少走半天弯路。5. 验证数据通路与用户空间调试频率、增益、流数据5.1 iio_info、iio_attr、iio_reg三板斧驱动起来之后先用iio_info确认设备列表iio_info -s如果能列出ad9361-phy并且下面有voltage0到voltage3这些channel说明SPI配置阶段已经过了。接下来配置频率iio_attr -d ad9361-phy -c altvoltage0 frequency 2400000000 iio_attr -d ad9361-phy -c altvoltage1 frequency 2450000000altvoltage0是RX本振altvoltage1是TX本振。执行完以后用iio_attr再读一次确认驱动确实把频率写进去了。如果写失败回到第4章排查。增益的动态调整也走类似的属性接口RX增益可以设置为手动模式再写具体增益值也可以切回自动增益控制。采样率的配置比较特殊它和数字接口速率、FIR抽取倍数都有关系。改完采样率最好重新读一下当前生效的值确保驱动内部换算没有出问题。设备树里的adi,rx-fir-decimation和实际采样率是联动的改一个就得检查另一个。5.2 验证DMA链路从设备文件到在线数据配置好频率和采样率之后用iio_readdev抓一段数据验证DMA链路iio_readdev -s 128 ad9361-phy voltage0 rx_iq.bin抓下来的bin文件是IQ交错数据可以拿Python读出来画个图看看底噪。如果频谱上什么都没有先检查发射端是不是真的在发信号或者用一根线把TX和RX环回。如果读出来的数据是一堆固定的0x00或0xFF问题大概率在数字接口时序去设备树里调adi,rx-data-delay和adi,tx-data-delay这两个参数专门用于调整FPGA和AD9361之间的数据采样相位尤其是在LVDS模式下走线长度稍有变化就需要微调。我见过有人在LVDS模式下抓数据始终不对最后把adi,rx-data-delay从0加到3、4才恢复正常。这个值加一点会影响一整条数据通路的建立保持时间但具体加多少没有公式可算只能逐个试。好在每次改完重新加载设备树驱动就行不需要重新编译整个内核。5.3 对接GNU Radio和Python脚本数据通路通了之后真正好用的玩法是让GNU Radio直接对接。gr-iio是GNU Radio的一个外部模块里面的AD9361源和宿可以直接通过libiio访问ad9361-phy在PC上就能看到实时频谱。目标板上不需要跑GNU Radio只要iiod服务在监听PC上用libiio的网络后端连接目标板IP数据就能通过网络流式传过来。Python脚本的对接方式也类似libiio提供了Python绑定写个简单脚本就能控制频率并接收一段IQ数据import iio ctx iio.Context(ip:192.168.1.10) dev ctx.find_device(ad9361-phy) rx_lo dev.find_channel(altvoltage0) rx_lo.attr.write(frequency, 2400000000)这个脚本虽然短但跑通它意味着从芯片寄存器到网络传输的整条链路都没有问题。到这一步AD9361的驱动工程算是真正立起来了后面再怎么调都只是优化参数的问题不是找bug的问题。我在实际调试中还有一个体会设备树里的属性不是写出来就没完的每加一个属性都应该用iio_attr、iio_reg去验证它确实影响到了芯片寄存器的实际值。用这样的方式逐步搭建比一股脑照抄参考设备树再慢慢猜原因要高效得多。只要能保持这个节奏AD9361看起来复杂的初始化过程其实也就是SPI通信、PLL锁定、数据通路这三关一关一关过而已。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进