☰
Zynq+AD9361基于PetaLinux的IIO驱动调试与初始化故障排查
2026/9/28 14:26:17 网站建设 项目流程

做软件无线电的朋友,十有八九绕不开AD9361这颗芯片。70MHz到6GHz的覆盖范围、最大56MHz的接收带宽,混频、滤波、增益控制、双通道ADC/DAC统统集成在一颗芯片里,在Zynq平台上通过PetaLinux构建Linux环境、用IIO驱动框架把射频链路跑通,已经是目前SDR原型验证最主流的一条技术路线。但这套流程文档虽然多,真上手之后会撞上各种刁钻问题:驱动静默加载失败、SPI读回全F、初始化时0x247寄存器固定读出0x80、CP_OVRG被置位导致RX PLL不锁定,任何一个都能卡住你好几天。这篇文章把我从创建PetaLinux工程、定制AD9361设备树到完整跑通IIO驱动的过程拆开讲,重点分析那些卡过我脖子的初始化异常,适合准备拿Zynq+AD9361做原型验证,或者板子已经在手边但驱动一直调不通的朋友参考。

1. 项目整体设计思路与方案选型

1.1 为什么是Zynq + PetaLinux + AD9361这个组合

我第一次接触AD9361的时候也纠结过:明明STM32也能通过SPI配置芯片寄存器,为什么业界几乎清一色用Zynq?原因有三层。

第一,AD9361是一个宽带零中频收发器,它需要高带宽、低延迟的数据通路。芯片输出的双通道12位ADC数据,速率为40MSPS时就接近百兆字节每秒,普通MCU根本搬不动。Zynq的PL部分可以高速接管这个数据流,做DDC、DUC、自定义调制解调,然后再把处理结果交给PS端的Linux做上层协议。

第二,Zynq的PS端是一颗双核ARM Cortex-A9,可以完整跑Linux。射频前端的控制和管理在Linux里非常方便,网络接口、文件系统、调试工具链全是现成的。

第三,PetaLinux是Xilinx官方的嵌入式Linux构建工具。Zynq的启动过程很繁琐——FSBL、U-Boot、内核、设备树、根文件系统,每个环节都有大量配置。手动交叉编译内核和U-Boot不是不行,但每次改一个设备树都要重新走一遍流程,效率很低。PetaLinux把这些步骤封装成了工程化管理方式,硬件描述文件导进来,整体构建,镜像打包一条龙,对搞射频的人来说确实省心。

1.2 为什么选用IIO子系统而不是自写字符设备驱动

Linux内核里给设备驱动分了门别类,网络设备走netdev,输入设备走input,而AD9361这种数据采集+数据转换器件,最合适的就是IIO(Industrial I/O)子系统。

IIO的设计初衷就是为传感器、ADC、DAC这类设备提供统一抽象。它把设备的能力拆成“通道”和“属性”,比如AD9361的两个接收通道、两个发射通道,各自有频率、采样率、增益、带宽这些属性,通过sysfs接口就能读写。用户层还有libiio库,封装了更友好的API,并且支持本地调用和网络调用两种方式。这意味着你可以在宿主机上通过网口直接操作目标板上的AD9361,这对调试射频链路来说非常方便。

如果自己写一个裸的字符设备驱动行不行?当然行,但你需要自己处理设备节点分配、IOCTL接口、并发访问、中断处理,而且用户层还得专门写配套工具。IIO把这些基础设施都做好了,ADI官方还提供了libiio、iio-oscilloscope这些现成工具,没必要重复造轮子。

1.3 AD9361 Linux驱动在软件栈中的位置

AD9361的驱动代码并没有合入Linux内核主线,而是由ADI维护在自己的内核分支中,代码路径在drivers/iio/adc/ad9361.c。这颗驱动挂在SPI总线上,通过SPI接口配置芯片寄存器的同时,注册成IIO设备。它跟普通的SPI设备驱动还有个区别:AD9361驱动内部还要管理两个PLL链路(RX和TX)、VCO校准、滤波器配置、增益表等,初始化逻辑非常长,而且对时序敏感。

用户空间工具链则是libiio及其配套程序。当你运行iio_info时,它会通过sysfs或网络访问IIO设备,列出所有通道和属性。这套架构的好处是隔离性很好,驱动层只负责“把硬件配置正确”,用户层负责“怎么用硬件”。

需要注意的是,设备树下发的属性必须和驱动期望的属性严格匹配,比如compatible = "adi,ad9361",这句字符串就是驱动和设备树之间的约定。一旦写错或漏掉,内核就找不到匹配的驱动,probe函数根本不会执行。我见过很多“驱动没反应”的问题,最后查下来都是设备树节点格式有问题,而不是驱动代码真的挂了。

2. PetaLinux工程配置与内核定制

2.1 安装PetaLinux前的环境准备

PetaLinux对宿主机环境要求挺严格,版本之间差异也大。我用的是PetaLinux 2022.2,搭配Ubuntu 20.04。如果你用的是Ubuntu 22.04或更新的系统,建议优先选PetaLinux 2023.2以后的版本,避免遇到shell兼容性问题。

安装步骤本身不复杂,但有几个细节必须提前处理:

  1. 安装编译依赖。PetaLinux安装器只是解压工具链和配置脚本,真正编译内核时需要gcc、make、libncurses5-dev、libssl-dev、flex、bison等一堆工具。建议先把所有基础编译工具装上:
sudo apt install gcc make build-essential libncurses5-dev libssl-dev \ flex bison u-boot-tools dosfstools mtools python3 python3-pip
  1. 下载安装器并执行:
chmod +x petalinux-v2022.2-10141622-installer.run ./petalinux-v2022.2-10141622-installer.run /opt/pkg/petalinux

安装路径强烈建议放在根目录下的非中文、无空格路径。我曾经把工具链装在一个带空格的目录里,后面编译时一堆shell脚本报路径错误,排查了很久才醒悟。

  1. 每次开新终端使用前,都要先source环境变量:
source /opt/pkg/petalinux/settings.sh

2.2 创建工程并导入Vivado硬件描述

PetaLinux的工程创建、配置、编译都在命令行下完成。先说创建工程:

petalinux-create --type project --template zynq --name ad9361_sdr cd ad9361_sdr

模板参数要注意,如果你的平台是Zynq UltraScale+ MPSoC,模板就换zynqMP;如果只是Zynq-7000,用zynq。

硬件信息来自Vivado工程导出的XSA文件。在Vivado里完成Block Design搭建后,File -> Export Hardware,把XSA文件导出,然后在PetaLinux里导入:

petalinux-config --get-hw-description=./hw/

执行后会自动解析XSA并打开配置界面。这里我建议把根文件系统暂时选为initramfs,方便初期快速启动验证;等驱动跑通后再切到SD卡方式。配置界面里Subsystem AUTO Hardware Settings -> Flash settings是启动方式等参数,也可以先不管。

2.3 内核菜单里使能AD9361驱动

这一步是重点。在PetaLinux工程目录下执行:

petalinux-config -c kernel

进入内核配置菜单后,按/搜索AD9361,我用的PetaLinux版本里,它位于:

Device Drivers -> Industrial I/O support -> Analog to digital converters -> Analog Devices AD9361

这里有个容易踩坑的地方:如果内核源码不是从ADI仓库同步过来的,菜单里可能根本找不到AD9361。PetaLinux自带的内核仓库一般会包含Xilinx平台补丁,但AD9361驱动有时被打包为模块或者隐藏在别的菜单层级。为了稳妥,我建议在配置界面搜索关键字,确认CONFIG_AD9361是否变成了y或m。如果找不到,说明这个内核源版本不包含ADI驱动,后续需要手动将ADI内核分支的相关补丁合入,这个后面可以单独写一篇,这里先按下不表。

同一时间,也要检查SPI和IIO相关依赖是否开启。AD9361驱动依赖CONFIG_SPI和CONFIG_IIO,这两个通常在Zynq平台默认开启,但值得确认一下。

配置完成后保存退出,回到工程目录。

2.4 用户空间libiio工具的集成方式

AD9361只是驱动层,调试时我们还需要libiio工具链。PetaLinux的rootfs配方里可以集成这些工具,配置路径是:

petalinux-config

在菜单中找到Image Packaging Configuration -> Root filesystem type,如果希望直接可用,可以把libiio、iio-oscilloscope加到rootfs中。不过要注意,这些包在PetaLinux的默认包源里不一定都有。我个人的做法是:初期先保持最小rootfs,启动后用交叉编译的方式把libiio和常用工具编译好,直接拷到板子的/tmp目录。这样不影响rootfs镜像,迭代起来更快。

如果实在想碰运气在菜单里找libiio选项,可以用/搜索libiio,看到有相关条目就直接勾上,能少编译一次就少一次。

3. AD9361设备树定制详解

3.1 设备树在Zynq平台的工作方式

设备树用文本文件描述硬件拓扑,告诉内核“这块板子上有什么设备、挂在哪个总线、需要哪些参数”。ARM平台不像x86那样可以通过PCIe或ACPI枚举设备,所以几乎所有外设都必须先在设备树里声明,驱动才能找到它。

在PetaLinux工程中,用户自定义的设备树内容通常写在:

project-spec/meta-user/recipes-bsp/device-tree/files/system-user.dtsi

这个文件会被最终的设备树编译流程包含进去。有一点要注意:不要在system-user.dtsi里重新定义整个SPI控制器节点,而应该通过&spi0的方式引用并扩展它,这样不会覆盖原有配置。

3.2 AD9361节点完整配置逐项说明

下面是我项目里一份经过验证可用的AD9361设备树节点,放在system-user.dtsi中:

/include/ "system-conf.dtsi" &spi0 { status = "okay"; num-cs = <1>; is-decoded-cs = <0>; ad9361@0 { compatible = "adi,ad9361"; reg = <0>; spi-max-frequency = <10000000>; clocks = <&ad9361_clk>; clock-names = "ad9361_ext_refclk"; clock-frequency = /bits/ 64 <40000000>; adi,rx-synthesizer-frequency-hz = /bits/ 64 <2400000000>; adi,tx-synthesizer-frequency-hz = /bits/ 64 <2400000000>; adi,rx-channel-bandwidth = /bits/ 64 <18432000>; adi,tx-channel-bandwidth = /bits/ 64 <18432000>; adi,rx-rf-port-input-select = <0>; /* RX1A_N */ adi,tx-rf-port-input-select = <0>; /* TX1A_N */ adi,rx-gain-type = <1>; /* slow attack AGC */ adi,tx-gain-type = <0>; /* manual gain */ adi,tdd-use-tx-rx-swap-enable; adi,tdd-use-rx-tx-swap-enable; }; }; &clkc { fclk-enable = <0xf>; ps-clk-frequency = <33333333>; };

这里逐条解释每个属性的含义和坑点。

compatible = "adi,ad9361"是驱动匹配的关键,必须和ad9361.c中of_device_id表的字符串完全一致。如果这个字符串写错,后面的一切都不会发生。reg = <0>表示它挂在SPI控制器的片选0上,对应硬件连接。spi-max-frequency我设为10MHz,AD9361的SPI接口支持更高频率,但布线差的时候提高频率会导致时序不稳定,建议前期先保守一点。

clocks和clock-names指引用的参考时钟。AD9361需要外部提供一个参考时钟,设备树里必须声明这个时钟源。clock-frequency = /bits/ 64 <40000000>是参考时钟频率,我用的是40MHz有源晶振。

在设备树里,AD9361很多频率属性是64位整数,所以必须写上/bits/ 64前缀。这个语法非常容易踩坑,漏掉后内核会按32位解析,数值不对,驱动初始化就可能计算出错误的本振频率。

adi,rx-synthesizer-frequency-hz和adi,tx-synthesizer-frequency-hz是上电时默认的接收、发射本振频率。2.4GHz是我这个工程的目标频点。注意这只是“初始频率”,运行后完全可以通过libiio动态改。adi,rx-channel-bandwidth和adi,tx-channel-bandwidth是模拟滤波器的带宽设置,18.432MHz是我根据CDMA2000信号带宽选的,一般要小于ADC采样率的一半。这里如果设置得太高,带外信号会混叠进来;设置得太低,有用信号会被切掉。

adi,rx-rf-port-input-select和adi,tx-rf-port-input-select选择RF端口。AD9361有RX1A/1B/2A/2B、TX1A/1B/2A/2B多组端口,硬件上接的是哪一组,这里就必须选哪一组。<0>一般对应A端口,具体要查驱动源码里的宏定义。

adi,rx-gain-type和adi,tx-gain-type是增益控制模式。接收端我选了slow attack AGC,发射端用手动增益。如果接收端选manual但又是大动态信号,很容易削波;反过来如果选慢速AGC,在快速跳频场景下增益调整又跟不上。这里没有绝对的对错,看场景。

3.3 参考时钟、BBPLL与LO频率的约束关系

AD9361内部有三个重要的PLL:BBPLL、RX PLL、TX PLL。BBPLL的输入就是外部参考时钟,经过内部倍频后给数字电路用。驱动初始化时会检查BBPLL锁定状态,如果参考时钟异常,整个初始化流程就卡住了。

设备树里clock-frequency必须和实际硬件参考时钟完全一致。比如晶振是40MHz,设备树里写了40M,没问题;但如果晶振实际是38.4MHz,设备树里写成40M,驱动会按错误的值去计算倍频系数,BBPLL最终的锁定频率就会偏,最后表现就是你读0x247寄存器得到的值不正常。

RX/TX PLL的关系更直接:目标LO频率越低,VCO分频比越大;目标LO频率越高,VCO分频比越小。AD9361的VCO工作范围大致在6GHz到8GHz之间(具体看版本),所以LO频率是1.5GHz时,VCO频率是6GHz,分频系数是4;LO频率是2.4GHz时,VCO可能是7.2GHz,分频系数是3。如果在设备树里配置的LO频率超出了VCO范围,PLL肯定锁不住。

改LO频率的正确方式是在libiio里设置,比如:

iio_attr -c ad9361-phy RX_LO 2000000000

改完驱动会自动重新做VCO校准。这个过程一般在几十毫秒到几百毫秒之间,如果靠下设备树再重启来换频率,效率太低。

3.4 多芯片同步场景下的设备树扩展

如果你做相控阵或多通道系统,需要多片AD9361同步工作。我最初也想用两片AD9361做分集接收,但同步问题比想象中麻烦。设备树里需要额外配置adi,multichip-sync-en、同步引脚等属性,同时硬件上要把SYNC、SYSREF这些信号连好。

从实践经验看,多片同步的难点不在设备树,而在时钟源和延迟校准。设备树能做的只是把使能开关打开,真正的同步算法要结合FPGA逻辑来实现。如果你现在只需要单片跑通,建议先不碰这些属性,减少变量。

3.5 设备树编译与快速验证

在PetaLinux里修改完system-user.dtsi之后,不需要全量编译,可以增量执行:

petalinux-build -c device-tree petalinux-build -c kernel

编译完成后生成新的image.ub。把根文件系统做成initramfs的话,整个运行环境都在镜像里,烧写SD卡或通过tftp加载都很方便。

有条件的建议把设备树编译成dtbo,在U-Boot阶段用overlay方式动态加载。这样可以在不重启内核的情况下反复试验不同设备树参数。具体做法是在U-Boot环境变量里增加:

setenv fdtoverlays /boot/ad9361-overlay.dtbo saveenv

然后手动编译overlay:

dtc -@ -I dts -O dtb -o ad9361-overlay.dtbo ad9361-overlay.dts

我习惯先用dtc在宿主机上验证语法,确认没有语法错误再放到板子上,可以省掉很多半路重启的麻烦。

4. 驱动运行验证与经典初始化故障排查

4.1 正常工作时应该看到什么现象

驱动成功probe之后,启动日志里会看到类似这样的信息:

ad9361 spi0.0: ad9361_probe: AD9361 Rev. 3 successfully initialized

可以用dmesg | grep ad9361确认。然后检查IIO设备节点:

ls /dev/iio:device* iio_info

iio_info会列出所有IIO设备和通道。AD9361 phy设备上能看到out_altvoltage0_RX_LO_frequency、out_altvoltage1_TX_LO_frequency、in_voltage0_raw等属性。

用libiio自带的小工具直接读寄存器:

iio_reg ad9361-phy 0x002

AD9361的device ID寄存器地址0x002,正常返回0x03。这个检查非常重要,它从侧面验证了SPI总线是通的。

如果上述现象都正常,再用iio_oscilloscope去看IQ波形,就说明驱动链路完全通了。

4.2 0x247寄存器一直读0x80的深层原因

这个问题在热搜词里反复出现,我调第一块板子时也撞上了。现象是驱动初始化卡在某个等待PLL锁定的循环里,手动去读0x247寄存器,无论怎么读都是0x80。

0x247是AD9361内部时钟/PLL状态相关的寄存器,0x80这个值对应的状态位表示PLL锁定检测异常,可以理解为芯片认为自己的参考时钟链路没有达到正常锁定条件。

它的直接原因通常是BBPLL参考时钟没起振或频率不对,但背后可能有这几个具体场景:

第一,外部参考时钟根本没有送进来。有时候硬件上参考时钟接到了FPGA的某个bank,FPGA bitstream没加载,时钟自然过不来。检查方法是用示波器直接量AD9361的EXT_REF_CLK引脚,确认有稳定的时钟信号。

第二,参考时钟幅度不够。AD9361对参考时钟的输入幅度有要求,一般来说需要保证一定的电平摆幅(常见范围是0.8Vpp到1.6Vpp之间),最大不能超过2Vpp,最小不能低于0.4Vpp左右,具体以手册为准。若时钟源驱动能力不足,或串了过大电阻衰减,芯片内部检测不到有效沿,BBPLL就锁不定。我遇到的一个案例是时钟源输出直接接了一个1k电阻做阻抗匹配,结果压降太大,换成了50欧姆匹配后问题消失。

第三,SPI时序本身不对,导致读到的0x80其实是总线上的毛刺。如果SPI时钟极性、相位配错,寄存器读回的数据就是乱的。设备树里的spi-max-frequency如果设得太高,加上飞线太长,回读数据也容易出错。排查方式就是用逻辑分析仪抓SPI波形,确认读写时序和寄存器地址是不是自己想要的。

第四,寄存器0x247前面某一步配置出错导致PLL进入了错误状态。AD9361的配置有严格的顺序依赖,驱动已经处理了大部分,但如果你用iio_reg手动改过某些寄存器,可能会破坏状态机,重启设备是最快的恢复方式。

我的排查顺序建议是:电源供电→参考时钟有无→参考时钟频率→SPI时序→寄存器值回读。这五步走完,大部分0x247卡住的问题都能定位到。

4.3 CP_OVRG被置位且RX PLL不锁定的组合故障

CP_OVRG是电荷泵过压标志。如果这个位被置位,说明电荷泵在VCO校准时找不到合适的调谐电压,电压顶到了上限或下限,PLL自然锁不住。这通常意味着VCO校准失败。

常见原因之一是LO频率配置和VCO实际工作范围不匹配。AD9361的VCO覆盖范围会有多档切换,如果设备树里初始频率设得极低(比如几十MHz),VCO会尝试用高频范围内分频来实现,校准会在边界值附近反复试探,最终触发CP_OVRG。试着把LO频率调到一个更常规的区间,比如1GHz以上,看问题是否消失。

另一个常见原因是供电电压偏低。AD9361的数字和RF部分分别供电,VCO部分对电压非常敏感。我之前做低功耗优化时,把VCO供电从标准的1.3V调到了1.25V,结果CP_OVRG频繁出现。把电压调回规格要求的1.3V后,这个问题就消失了。如果你用的DC-DC电源纹波偏大,在VCO供电引脚附近加一颗100nF和10uF去耦电容,可以有效改善。

还有一个容易被忽视的点:接收通道的RF端口输入信号幅度过强或直流偏置异常,会在校准时干扰VCO环路。排查时把RF前端断开,只保留本振自检,看是否仍然报CP_OVRG。这样可以区分问题在前端还是在芯片本身。

处理这类问题的实用方法是逐步降速排查:先把RX/TX LO频率都设在2.4GHz,参考时钟40MHz,带宽18MHz,这个配置是社区里验证过最通用的组合。如果它无法工作,优先怀疑硬件;如果它能工作,再逐步改成你的目标参数,二分定位问题。

4.4 常见初始化失败速查表

现象可能原因推荐排查方式
dmesg没有ad9361相关信息设备树compatible错误、SPI节点status禁用、驱动未编译进内核检查设备树节点、搜索CONFIG_AD9361
SPI回读0x002返回全FSPI时序、片选、供电问题用逻辑分析仪抓SPI信号,确认CS、CLK、MOSI电平
0x247一直0x80参考时钟未起振或幅度不够、BBPLL锁定失败量EXT_REF_CLK信号、确认clock-frequency属性
CP_OVRG被置位,RX PLL未锁定VCO校准失败、供电偏低、LO频率超范围调整供电电压、改动LO频率、串电容隔离RF前端
驱动probe成功但RX数据全零AGC配置错误、RF前端增益过低、数据接口未配置用iio_attr查看RX增益、检查FPGA数据通路
iio_info找不到ad9361设备驱动模块未加载、设备树节点未生效检查dmesg、确认CONFIG_AD9361是y

这张表我建议截图收藏,每次初始化失败先对照排查一轮,比自己瞎翻寄存器快得多。

5. 实操经验与调试技巧汇总

5.1 调试AD9361的必备工具组合

工欲善其事,必先利其器。我调试AD9361时最常用的工具是这几个:

  • iio_reg:用它读写AD9361原始寄存器,快速验证SPI通路。
  • iio_attr:读写IIO设备属性,比如查看当前RX LO频率、增益。
  • iio_oscilloscope:图形化界面查看IQ波形、FFT频谱,验证射频信号是否真正收到。
  • dmesg:内核日志是排查probe失败的第一现场。
  • 逻辑分析仪:抓SPI时序,检查寄存器读写是否正确。尤其在怀疑SPI速率或者设备树配置问题时,一抓一个准。

如果板子支持网络,优先用libiio的network backend。先在板子上运行iiod,然后在电脑上执行:

iio_info -u ip:192.168.1.100

这样就能在电脑上直接操作板子上的AD9361,不用每次都在串口终端敲命令,调试体验会好很多。

5.2 PetaLinux构建速度优化的两个习惯

PetaLinux全量编译很慢,动辄十几分钟。我培养了两个习惯来提速。一是尽量只做增量构建,改设备树就只编译device-tree和kernel,不要没事执行全量petalinux-build。二是在开发阶段把rootfs做成initramfs,这样每次修改后只需要打包一个image.ub,通过tftp启动,不用反复插拔SD卡。虽然initramfs在内存中运行有大小限制,但调试阶段完全够用。

5.3 几个容易被忽略的坑

工程路径不能有空格。PetaLinux的构建脚本里有大量路径拼接,空格会导致各种诡异错误。

设备树里64位数字必须用/bits/ 64。忘了写这个前缀,值会被截断,频率直接不对。

SPI速率不要太激进。板子布线不好时,10MHz可能都会出错,8MHz更稳。驱动加载成功后可以再慢慢提高。

改完设备树以后,记住先查启动日志。Linux对设备树不匹配的报错往往隐藏在dmesg中,比如OF: fsl,spi ...或者failed to get ad9361_clk这类。看到类似信息,说明设备树节点没被正确解析,需要回过去检查文件内容。

最后说一点个人感受。AD9361这套系统调试起来确实痛苦,寄存器数量庞大,状态机复杂,芯片手册上千页。但一旦你把PetaLinux工程、设备树、IIO驱动这套框架跑通了后面做频率规划、带宽调整、增益策略这些功能就都变成“改参数”的问题,不会再到处碰壁。我自己也是从0x247读到0x80的懵圈状态一步步走过来的,后来总结出一套排查顺序,效率高了很多。如果你的板子也卡在类似的初始化状态,参考上面第4章的步骤逐项排除,大概率能定位到真因。AD9361本身很成熟,代码没问题的时候,绝大多数故障都出在电源、时钟、SPI这三个基本盘上。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询