
AD9361这颗射频收发芯片在软件无线电圈子里有多常见不用我多说。70 MHz到6 GHz的频段覆盖收发同时工作200 kHz到56 MHz的可配置通道带宽配合FPGA或者Linux主机做IQ数据流处理它几乎是SDR硬件平台里的万金油。但很多朋友第一次拿到这颗芯片照着数据手册去初始化寄存器往往一个礼拜都点不亮。原因很简单芯片内部的收发链路、频率规划、滤波器配置、校准流程相互耦合几百个寄存器互相影响靠手册一页页啃效率实在太低。ADI官方提供的例程包括HDL参考设计、Linux内核驱动IIO框架和No-OS裸机代码就是用来打破这个僵局的。整套东西把芯片的初始化、校准、数据通路都跑通了一遍你要做的只是把它移植到自己的板子或者工程里。这篇文章是系列第一篇重点讲清楚官方例程的整体结构、如何在例程基础上实现BPSK调制解调、以及怎么把AD9361的设备树完整迁到新建的Petalinux工程里。适合正在做FPGALinux无线收发项目、或者准备把AD9361集成到自研板卡上的工程师和学生参考。1. 官方例程到底帮你解决了什么问题1.1 AD9361难上手难在哪儿AD9361不是一颗普通的窄带收发芯片它是一个完整的宽带收发前端内部从LNA、混频器、可配置滤波器到12bit ADC/DAC、抽取和内插滤波器、数字校正逻辑全部集成在一起。好处是系统BOM变得特别简单坏处是你要面对一张几百页的寄存器手册以及一个极其复杂的初始化序列。手动写初始化代码最大的坑在于顺序芯片必须先让内部电源、基准时钟都稳定再依次完成RX/TX本振校准、基带滤波校准、发射正交校准、接收正交校准。中间任何一步顺序错了或者等待时间不够后续校准结果就会异常表现出来的现象往往是电平不对、EVM奇差、或者干脆没有输出数据。官方例程的核心价值就是把这套已知可用的初始化、校准、数据通路配置流程固化成了代码。你不用再去猜哪个寄存器该先写、哪个该后写也不用自己推导滤波器抽取内插系数怎么配直接用官方这套链路把板子点亮然后再去改自己的应用逻辑。对刚接手AD9361项目的工程师来说这等于跳过了最痛苦的一到两周调试期。1.2 官方例程的三块拼图ADI的官方例程不是一个仓库而是针对不同使用场景拆成了三套东西组成部分存放位置适用场景核心作用HDL参考设计GitHub上的hdl仓库FPGA设计提供AXI接口的AD9361收发数据通路、DMA、SPI控制器Linux内核驱动内核staging/iio/adc部分版本路径有调整Linux系统通过IIO框架管理芯片初始化、校准、增益、频率等No-OS裸机例程GitHub上的no-OS仓库无操作系统的MCU/FPGA纯C实现寄存器读写和API封装之所以拆成三套是因为AD9361的使用者差异太大。有人用Zynq跑Linux有人用纯FPGA还有人用单片机配合AD9361做窄带无线节点。三套代码共用同一个寄存器配置计算核心只是平台抽象层不同。理解了这个结构你再去查资料就知道该往哪个仓库里翻而不是在论坛里大海捞针。1.3 这套例程适合谁、覆盖到什么程度先说结论官方例程最擅长的是把板子跑起来它不负责解决你的协议和应用层问题。覆盖范围是AD9361的RF前端配置、数据接口、校准、以及基本的收发数据流不覆盖调制方式、编解码、组包、抗干扰算法这些上层应用。BPSK怎么做是在例程之上扩展的应用层工作但前提是例程已经把IQ数据通路打通。这也是为什么这篇文章把BPSK和设备树移植放在一起讲——前者代表例程之上怎么扩展应用后者代表例程怎么适配到自己的工程环境。适合人群很明确正在调FMCOMMS2/3/4这类ADI评估板的工程师、准备把AD9361放在自研板卡上做SDR的软硬件工程师、以及需要搭无线通信平台的研究生和高校实验室。即使你是纯做算法的人搞清楚这块芯片底层的工作方式对理解真实射频链路里的种种非理想因素也很有帮助。2. 官方例程的工程结构逐层拆解2.1 HDL参考设计先把数据通路搭起来如果你从GitHub上拉下hdl仓库会看到library和projects两个目录。library下面是通用IPprojects/fmcomms2等目录下是针对不同开发板的参考设计。以Zynq平台的fmcomms2为例整个工程可以通过Makefile在Vivado里一键生成。生成的块设计里最有价值的几个IP是axi_ad9361负责把AD9361的LVDS或CMOS数据接口翻译成AXI Stream内部包含接收端的解包逻辑和发射端的组包逻辑。axi_ad9361_dds一个基于DDS的测试信号源可以直接向发射链路灌正弦波、扫频信号。新板子第一次上电验证发射通路全靠它。axi_dmac负责DMA搬数把PL侧的AXI Stream数据搬到DDR或者从DDR搬到PL。接收和发射各挂一个。up_clock_mon等监控模块用来检查时钟状态调试的时候非常管用。看懂HDL这层你就明白了AD9361芯片和软件之间那一层翻译是怎么工作的。AD9361的采样时钟和数据接口时序是固定的软件不可能直接操作那些LVDS信号必须由FPGA把这些物理信号变成便于软件处理的数据包AXI DMA再把数据包搬进DDR。官方例程把这套链路归纳成了标准模板只要你的自研板管脚定义和时钟关系与模板一致大部分情况下直接套用就行这也是为什么很多人做AD9361硬件时都尽量保留和评估板相同的接口设计。2.2 内核驱动与IIO框架芯片变成Linux设备Linux侧的重点是IIOIndustrial I/O框架。AD9361的驱动在内核里一般位于staging/iio/adc目录新版内核的路径可能略有变化但机制是一样的。IIO框架把AD9361抽象成两类设备一类是收发物理层设备通常叫ad9361-phy负责频率、增益、采样率、滤波器这些RF属性的配置另一类是数据接口设备通常叫cf-ad9361-lpc或者类似名字负责把IQ数据通过buffer字符设备暴露给应用层。比如调发射频率你不需要去算寄存器直接写sysfs节点echo 2450000000 /sys/bus/iio/devices/iio:device0/out_altvoltage0_frequency这句在ad9361-phy设备里设置发射本振频率为2.45GHz驱动会自己完成频率规划、VCO校准和后续的寄存器写入。具体属性名在不同的内核版本里可能略有差异用iio_attr这类工具先列一遍最保险。接收数据则在数据接口设备上通过IIO buffer读取。整个设计把芯片控制和数据传输分离符合软件无线电把硬件抽象成通用设备的思路。内核源码的tools/iio目录下还有iio_generic_buffer.c这样的官方示例程序它会自己解析设备属性、申请buffer并持续读数据是研究IIO buffer机制的绝佳入门材料。ADI维护的例程里也常见一个ad9361-iiostream.c专门演示AD9361的流式收发建议找出来逐行读一遍。2.3 No-OS裸机例程不要Linux也能跑如果你的项目里没有Linux官方还提供了no-OS裸机代码。它做的事情和Linux驱动基本对等包括ad9361.c里的底层寄存器读写、ad9361_api.c里的高层面板配置、以及针对Xilinx等平台的操作系统抽象层。这套代码特别适合用来反查配置流程。我在调试时会拿它和Linux驱动做交叉验证同一个参数两边计算出来应该一致如果不一致说明某个宏或者初始化顺序有差异。用裸机代码还能在MicroBlaze这样的软核上直接跑把FPGA当成一个小型SDR处理器。很多没有MMU的低成本平台最终都走的是这条路线。2.4 一条完整的上手路线把三块拼图串起来一个典型的上手路径是这样先按hdl仓库的文档用Vivado生成bitstream并烧到板子里然后启动一个带AD9361驱动的Linux环境可以用ADI的Kuiper Linux也可以自己编译Petalinux接着加载设备树让内核识别出ad9361-phy和cf-ad9361这类IIO设备最后用libiio工具配置频率、读回IQ数据。这一步走通之后无论做BPSK还是更复杂的OFDM都是在同一条数据通路上的应用扩展。接下来就按这条路径展开先讲BPSK怎么在例程基础上落地再讲设备树怎么迁到新建的Petalinux工程。3. 在例程基础上实现BPSK调制解调3.1 BPSK的基带符号映射BPSK是最简单的数字调制方式它用载波的两种相位0度和180度表示二进制的0和1。在基带IQ坐标里看就是星座图上I轴上的两个点比特1映射为1比特0映射为-1Q路恒为0。写成公式就是s(t) A·d(t)·cos(2πfct)d(t)取1或-1。为什么说BPSK适合在AD9361例程上做第一个实验因为它只涉及I路实现逻辑最简单调试时星座图上一眼就能看出好坏。这里有一个重要的工程细节实际系统里接收端如果载波恢复存在相位模糊0度和180度可能颠倒解出来的比特会全部取反。所以真正的通信系统一般用差分BPSKDBPSK用相邻符号之间的相位变化来编码这样即使整体相位反了相邻符号的相对关系仍然不变。无论用哪种方式基带采样点的生成逻辑都一样DBPSK只是在映射前对比特做一次差分编码。第一次在板子上验证时我建议直接用已知的伪随机序列别用真随机数这样接收端对起来方便。3.2 发射侧把比特流变成IQ数据推给AD9361发射侧要做的事是把要发的比特流映射成IQ采样值通过IIO buffer接口写进AD9361的TX数据通路同时把发射频率、发射增益、采样率这些参数配置好。先用官方例程验证发射通路最省事的办法是用HDL里内置的DDS发单音如果频谱仪上能看到预期频率的谱线说明RF链路和数据接口没问题然后再换成软件注入的调制数据。用libiio配置设备逻辑大概是下面这个样子import iio import numpy as np ctx iio.Context(ip:192.168.1.10) phy ctx.find_device(ad9361-phy) # 配置发射频率和采样率具体属性名以驱动为准 iio.channel_attr_write(phy.find_channel(altvoltage0), frequency, 2450000000) iio.channel_attr_write(phy.find_channel(voltage0, True), sampling_frequency, 1000000) dev ctx.find_device(cf-ad9361-lpc) tx dev.find_channel(voltage0, True) tx.enabled True buf iio.Buffer(dev, 1024, False) # 生成BPSK基带样本 bits np.random.randint(0, 2, 512) symbols 2 * bits - 1 sps 8 # 每个符号8个采样点 iq np.repeat(symbols, sps).astype(np.float32) samples np.zeros(len(iq) * 2, dtypenp.float32) samples[0::2] iq # I路 samples[1::2] 0 # Q路为0 samples samples * 16384 # 留出幅度余量 buf.write(samples.astype(np.int16).tobytes()) buf.push()这里有几个必须注意的点。第一AD9361的数据接口常用16位I、16位Q交错排布写buffer时I和Q必须交错顺序错了星座图是乱的。第二幅度一定要留余量直接把样本填到满幅经过发射链路的数字滤波和模拟增益很容易削顶波形失真后EVM会非常难看一般先按半幅到四分之三幅试。第三采样率要结合AD9361的配置来理解IIO接口上的采样率是IQ复采样率配置为1MHz时芯片内部会自动把抽取和内插链路调整到对应的射频采样率。另外发射通道的使能属性和增益属性也要检查很多板卡默认增益在衰减状态不调一下输出会低到仪器根本测不出来。3.3 接收侧从IQ流里把比特判决出来接收侧反过来把AD9361 RX端口采样到的IQ数据从IIO buffer读出来做判决。最简单的情况是发射和接收用同一个时钟源或者用线缆直连不存在大的频偏和相偏这时只需要做符号定时同步。实操中先用官方工具抓一段数据iio_readdev -u ip:192.168.1.10 cf-ad9361-lpc -s 65536 rx.bin然后拿Python离线分析。假设buffer里是按I、Q交错存储的int16那么把I路取出来按过采样倍数sps分成一组一组每组做个平均相当于一个简单的匹配滤波加降采样。判决规则就是看符号为正还是为负。判出来之后和发射端保存的比特序列比对统计误码率。这个过程可以完全离线反复做不占用板子资源。这里要特别注意一个坑如果发射和接收是两个独立板卡、两个独立晶振哪怕晶振标称频率一样实际也会有几Hz甚至几十Hz的频偏。BPSK解调遇到频偏时星座图上的点会绕着原点缓慢旋转I路符号变得不稳定之前那句直接判正负就行不通了。碰到这种情况一种做法是发射端发一串已知的训练序列接收端用训练序列估计出残余频偏再对数据做频偏补偿另一种更省事的做法是直接用DBPSK配合一阶差分相位旋转的影响会小很多。从工程角度讲第一次做板级验证时把发射和接收的参考时钟接在同一个信号源上能少走很多弯路。3.4 用官方IIO工具快速闭环验证如果你只是想验证整个收发通路能不能工作不一定要立刻写完整的调制解调程序。官方libiio里带了iio_writedev和iio_readdev这样的命令行工具可以配合HDL里的DDS做闭环测试。发射端用FPGA内部的DDS发一个单音接收端用iio_readdev抓数据在Python里做FFT看到对应频率的峰值就说明RF收发和IQ通路都正常。然后再把DDS换成自产的BPSK数据同样的流程抓下来看星座图。整个调试流程的逻辑是先分割验证再整体闭环先确认发射链路单独工作再确认接收链路单独工作最后才把两边合起来看调制质量。不要一上来就做完整收发实验出了问题你根本不知道是发射的问题、接收的问题还是算法的问题排查起来非常痛苦。官方例程的价值就在于把底层链路先替你验证好你只需要在高层做增量验证。4. 把AD9361设备树搬到新建Petalinux工程4.1 设备树节点到底在描述什么很多朋友在Petalinux里折腾AD9361卡住的第一件事是设备树看不懂。换个角度理解就会清楚很多设备树就是告诉内核你的板子上有哪些外设、它们接在哪个总线上、寄存器地址是什么、时钟是哪个、中断是哪个。对AD9361来说一个完整描述至少要包含两类节点。一类是物理层节点挂在SPI总线上compatible为adi,ad9361描述芯片和ARM之间的SPI连接、参考时钟、收发端口选择。另一类是数据接口节点挂在PL的AXI总线上compatible为adi,axi-ad9361描述FPGA内部那个负责IQ数据组包的IP以及它和DMA之间的连接。典型的结构长这样spi0 { status okay; ad9361: ad93610 { compatible adi,ad9361; reg 0; spi-max-frequency 20000000; clocks ad9361_clkin; clock-names ad9361_clkin; adi,rx-rf-port-input-select 0; adi,tx-rf-port-input-select 0; }; }; fpga_axi { rx_dmac: dma7c400000 { compatible adi,axi-dmac; reg 0x7c400000 0x10000; #dma-cells 1; }; adc_ad9361: cf-ad9361-lpc79020000 { compatible adi,axi-ad9361; reg 0x79020000 0x4000; dmas rx_dmac 0; dma-names rx; }; };SPI那部分的RF参数频率、增益、滤波器系数为什么不写在设备树里因为它们是运行时配置由驱动通过IIO sysfs节点暴露后应用层再去设置。设备树只负责描述硬件是怎么接的不负责描述软件要跑成什么样。这个分工搞清楚了你移植设备树时就不会纠结该在dts里塞多少寄存器配置。还有一些可选的绑定属性比如数字接口模式LVDS还是CMOS、外部时钟分频关系这些要参考内核自带的那份ad9361设备树绑定文档不同版本的驱动支持程度不太一样。4.2 参考设备树从哪儿拿移植的第一步是找一个靠谱的参考设备树。最直接的来源是ADI评估板对应的内核dts比如Zynq平台的参考设计里内核源码arch/arm/boot/dts目录下会有类似zynq-zed-adv7511-ad9361-fmcomms2-3.dts这样的文件里面包含了AD9361 PHY节点、axi-ad9361节点、DMA节点以及相关的时钟和SPI配置。还有一个来源是ADI的Kuiper Linux BSP它里面已经编译好的dtb可以用dtc命令反汇编成dts来参考节点内容和属性写法。我的建议是优先用和你的板子最接近的评估板dts做底子。比如用FMCOMMS2/3/4评估板因为这类板子的PL地址分配、SPI ID、时钟结构都有官方验证过的一致性比从零写dts靠谱得多。拿到参考dts之后别急着大改先原样放进工程里跑一次确认环境和工具链没问题再逐步调整成你板子实际的硬件连接。直接上来就改地址、改SPI号、改时钟出错时根本分不清是dts问题还是别的配置问题。4.3 Petalinux移植的完整操作在新建Petalinux工程里把AD9361设备树搬过去我的实际操作步骤是这样。第一步创建工程并导入硬件描述文件。用Vivado导出xsa之后执行petalinux-create -t project --template zynq -n my_sdr cd my_sdr petalinux-config --get-hw-description/path/to/xsa第二步找到设备树编辑的入口。Petalinux的meta-user层里有一个system-user.dtsi这是给板级定制用的路径在project-spec/meta-user/recipes-bsp/device-tree/files/下面。我自己习惯把从参考dts整理出来的AD9361相关内容放到单独的dtsi文件里然后在system-user.dtsi里用/include/引进来。这样以后换板子只需要替换这个dtsi文件不用来回改Petalinux自动生成的pl.dtsi。要注意pl.dtsi是Petalinux根据xsa自动生成的直接改它的话每次重新导入硬件描述就会被覆盖所以自定义节点尽量放system-user.dtsi或者独立dtsi里。第三步对照你的硬件调整节点。重点检查三样东西SPI总线号是否和PS端的连接一致、axi-ad9361和DMA的寄存器地址是否和Vivado块设计里分配的地址一致、参考时钟频率是否和板上晶振一致。地址不对是最容易忽略的问题Vivado给IP分配的基地址是多少dts里的reg就要写多少多一位少一位都可能导致驱动probe不出来甚至产生总线错误。第四步配置内核把AD9361驱动编进去。执行petalinux-config -c kernel在Device Drivers→Staging drivers→Industrial I/O相关的层级里找到AD9361的驱动并使能同时确认IIO buffer、DMA相关的内核支持也打开了。如果驱动没有编进去设备树写得再对也没有用因为内核根本没有对应的驱动去解析这个节点。第五步编译和打包。先单独构建设备树验证语法执行petalinux-build -c device-tree然后再整体petalinux-build。最后用petalinux-package --boot生成BOOT.BIN烧到SD卡启动。整个过程里最容易出的是设备树语法错误和节点地址冲突编译时会报出来报错也不用慌读一下错误信息基本都能定位到是哪个节点重复定义或者少了哪个属性。4.4 移植完成后的验证清单设备树移植完不是能启动就算成功我习惯按下面这个清单过一遍启动日志里有没有AD9361相关的打印尤其是probe成功的提示和校准完成的信息/sys/bus/iio/devices/下有没有出现对应的IIO设备一个名字带ad9361-phy一个名字带cf-ad9361用iio_info或iio_attr读取物理层设备属性确认频率、采样率、增益属性存在且可写读一次接收buffer确认能出数据而不是卡死在DMA等待上用频谱仪或者另一台接收设备验证发射是否真的有信号输出。如果前面几步都过了但最后一步没信号那多半不是设备树问题而是RF配置问题回到第3节的方法去逐段排查。这里我特别提醒一点设备树移植成功只代表内核认识了这个硬件不代表RF链路就是好的。很多人在dts上花了大量时间最后发现是发射增益没开、端口选择错了这类问题跟设备树一点关系都没有。5. 实战中踩过的坑和调试心得5.1 典型问题速查表把我在AD9361上遇到的问题整理成一张表基本能覆盖八成的新手卡点现象可能原因排查方向内核日志里没有ad9361 probe信息设备树节点没匹配上或驱动未编译检查compatible字符串、SPI地址、内核配置probe卡在校准阶段超时参考时钟频率不对、SPI速率过低、电源不稳核对clkin频率、spi-max-frequency、供电纹波sysfs下找不到数据接口设备axi-dma或axi-ad9361节点地址错误和Vivado地址比对确认dmas和dma-names发射无输出TX使能没开、增益配置为0、端口选择不对检查使能属性和增益属性、频谱仪实测接收buffer读超时DMA没工作、中断配置缺失检查dma节点、中断号、内核dma驱动BPSK星座图旋转收发频偏未补偿训练序列估频偏或改用DBPSKPetalinux编译dts报重复节点在system-user.dtsi里重复定义了已有节点用标签引用合并不在同一地址重复定义这张表不是让你遇到问题才翻而是在动手之前就看一遍。我在实际项目里发现很多问题在设计阶段就可以避免比如地址冲突在做Vivado块设计时就把地址规划得和参考设计一致后面根本不会出差错。5.2 调试工具怎么组合用调试AD9361链路时我会同时备几类工具。第一类是libiio自带的命令行工具iio_attr用来读写属性、iio_reg可以直接读写芯片寄存器、iio_readdev和iio_writedev用来搬数据。这套工具在完全没有图形界面的嵌入式环境里最实用SSH上去就能操作。第二类是Python加pylibiio适合做星座图、频谱分析、误码率统计。我会先用iio_readdev抓一坨raw数据存成文件再用numpy离线分析因为离线分析可以反复折腾数据而不占用板子资源改判决算法、换滤波方式都很方便。第三类是硬件工具频谱仪看发射频谱和杂散示波器看AD9361的DATA_CLK和FB_CLK有没有正常的采样时钟。很多时候软件查了半天没问题最后发现是物理层一个时钟没起来这时候示波器一测就知道了。组合使用这三类工具时我的原则是从物理层往应用层查。先用示波器确认时钟和接口信号再用频谱仪确认射频信号最后才在软件里查数据流和算法。反过来查的话一旦数据不对你会同时怀疑硬件、驱动、设备树和算法排查空间太大。5.3 几条值得记住的经验最后分享几条我自己的经验。第一第一次上电不要急着调调制先把官方例程原封不动跑通用内置DDS发个单音看频谱确认硬件链路正常之后再动软件上的调制解调。第二改设备树每次只改一处启动后确认没问题再改下一处不要一次性把SPI、DMA、时钟全改了出了问题你根本不知道是谁引起的。第三AD9361的校准对电源质量非常敏感特别是DVDD和AVDD的纹波如果校准结果随机性很大先怀疑供电而不是怀疑代码。第四自研板卡尽量保留和评估板一致的参考时钟频率和接口模式CMOS还是LVDS这个选择会直接决定HDL参考设计能不能套用改起来牵扯很广。第五Petalinux的版本和内核版本要一起考虑老版本BSP里设备树的组织方式和现在不太一样多查一下和你Petalinux版本匹配的官方文档比在论坛瞎摸索快很多。我个人在实际操作中还有一个习惯每次调试前先把当前的设备树、内核配置、bitstream三者的版本号记录下来。AD9361这套东西更新很勤驱动、HDL、设备树三者版本不匹配的情况时有发生写下版本号能在出问题时帮你快速缩小范围。官方例程本身不是终点它是你验证硬件、理解数据通路的最短路径。把这条路走通之后不管后面是做BPSK、QPSK还是更复杂的系统起码硬件底子已经牢靠了。