☰
AD9361官方例程详解:从HDL到BPSK调制与设备树迁移
2026/10/5 17:35:01 网站建设 项目流程

年初我接手一个无线基带原型项目,第一次把AD9361评估板从“上电闪灯”状态跑到“能从MATLAB抓到星座图”,整整折腾了一个多星期。那会儿最深的感受是:AD9361的官方例程不是不能跑,而是很少有人告诉你该按什么顺序去啃它。官方仓库里堆着一大摞HDL、Linux内核补丁、设备树、libiio工具,单是README里能点出去的链接就有十几条,新人很容易迷失在目录树里。

这篇是“AD9361官方例程详解”系列的第一篇,我先把官方例程这套体系拆开讲清楚,然后围绕两个很多人私信问过的问题逐步展开:一是怎么在官方例程里实现BPSK调制解调并真正把数据取出来,二是怎么把AD9361原有的设备树搬到新建的PetaLinux工程里。整体内容偏“动手之前先看懂”,等基础链路通了,后续再聊寄存器级的优化和射频性能调校。

1. 官方例程到底包含什么:一张链路图看清AD9361的软硬件分工

1.1 “官方例程”不是一份代码,而是一整套参考系统

如果你刚接触AD9361,第一反应可能是去官网找“例程下载”。实际上下载下来之后会发现,ADI官方给出的是整套参考设计,涵盖三个层次:HDL层、Linux内核层、用户空间层。

层次主要仓库/组件作用
HDL参考设计analogdevicesinc/hdl生成Vivado工程和FPGA比特流,处理FPGA与AD9361的接口、时钟、DMA
Linux内核与设备树analogdevicesinc/linux、meta-adi提供ad9361驱动、设备树描述硬件连接关系,完成寄存器初始化和数据收发
用户空间工具libiio、iio-oscilloscope提供API和图形界面,用于配置频率、增益、采样率,采集I/Q数据

这三层缺一不可。我见过很多卡住的案例:只盯着SPI去配AD9361寄存器,忽略了FPGA端HDL和Linux驱动之间的匹配关系,结果射频看起来通了,数据却全是乱的。

第一层是HDL仓库,里面按板卡名称分目录,比如fmcomms2、adrv9361z7035等。每个目录都是一个完整的Vivado工程,解决的是“FPGA怎么和AD9361对接”的问题,包括时钟管理、接口时序、数据位宽转换、AXI DMA通路。

第二层是Linux内核驱动和设备树。ADI维护了一套带ad9361驱动和libiio框架的Linux内核分支。设备树负责告诉内核“AD9361挂在哪条SPI上、中断接到哪个脚、DMA通道叫什么名字”,驱动则负责把芯片配置成你要的频率和带宽。

第三层是我们日常用得最多的libiio库和iio-oscilloscope图形工具。通过它们,你可以在上位机直接读写寄存器、配置参数、抓取I/Q样本。

1.2 数据链路全局视角:从RF口到DMA内存的完整路径

我画过很多次数据链路图,最后印在脑子里的版本是这样的:

发射路径:PS端(ARM Linux)或者PL端自定义IP,把要发送的I/Q基带数据写入AXI DMA;HDL参考设计中的数据打包模块,把AXI总线位宽的数据转换成AD9361数字接口所需的CMOS或LVDS格式;AD9361芯片内部完成插值滤波、DAC、上混频,最终从RF端口输出。

接收路径:RF信号进入AD9361,完成下混频、ADC、抽取滤波,把I/Q数据送到数字接口;HDL里的接口逻辑把LVDS上的串行数据拼回AXI总线位宽;AXI DMA再把数据搬到PS内存。之后你就能用libiio的接口把数据拉出来,做FFT、画星座图。

为什么理解这条链路很重要?因为你后面会接触到的所有上层应用——BPSK调制解调、频谱感知、OFDM同步——本质上都是在“基带I/Q数据”上做处理。AD9361只负责把射频信号变成数字I/Q流,它不关心你的调制方式是BPSK还是QPSK,更不负责帧同步和位同步。官方例程的价值,恰好是帮你把数据通路的“最后一公里”打通,省去从零写RTL和各种接口逻辑的痛苦,剩下的信号处理算法才是你自己的发挥空间。

1.3 给初学者推荐的学习顺序

结合我踩过的坑,官方例程的建议学习顺序是:

  1. 先跑通Linux镜像,确认AD9361能被驱动正确识别;
  2. 用iio-oscilloscope做一次完整的收发测试;
  3. 再用libiio的C或Python API写一个最简单的收发程序;
  4. 最后才去碰HDL参考设计,对着代码分析数据通路;
  5. 等需要定制功能时,再改HDL和设备树。

很多新手一上来就跳到HDL编译,四五个小时等一个工程综合实现跑完,结果板卡上电连设备都没挂上,非常浪费时间。按这个顺序走,至少你能先确信硬件链路是好的,后面出了问题才知道往哪一层去定位。

2. 把官方例程跑通:环境准备和首次点亮AD9361

2.1 硬件平台和连接线:FMC不是插上去就能用的

官方例程支持的板卡很多,我手头常用的两块:一块是ZedBoard配AD-FMCOMMS2-EBZ,适合入门学习;另一块是ADI的ADRV9361-Z7035整板,集成度高,适合做原型验证。无论用哪种,有几个硬件细节必须提前确认:

  • FMC电源轨:FMCOMMS2板上有VADJ、3.3V等供电要求,必须和主板的FMC接口定义匹配;
  • 数字接口模式:官方HDL默认常用LVDS模式,CMOS模式占用引脚更多、速率受限;
  • 时钟源:默认用板载参考时钟即可,但调试时容易在时钟分频设置上出问题;
  • 串口连接:Zynq平台通常需要一根UART串口线连到PS端调试串口,Linux启动日志和命令行都在这里。

我见过有人把FMCOMMS2插在某块国产Zynq开发板上,上电后系统日志里根本找不到ad9361-phy设备。后来查下来,是主板FMC的VADJ电压不对,导致AD9361的数字IO电平异常。所以硬件选型真不能拍脑袋,先用官方支持的开发板把路跑通,再迁移到自定义硬件上。

2.2 Vivado工程编译:用官方hdl仓库一次生成比特流

编译HDL参考设计,直接跟着hdl仓库根目录的README操作。以fmcomms2的ZedBoard工程为例:

git clone https://github.com/analogdevicesinc/hdl.git cd hdl git checkout <对应版本分支> # 务必和要使用的Linux发布版本匹配 cd projects/fmcomms2/zed make

几个值得记下来的踩坑点:

  • 工具版本必须匹配。不同HDL分支依赖的Vivado版本差别很大,老分支在新Vivado上往往无法直接综合。建议先看仓库里的CI配置文件,确认官方用的工具链版本,再安装对应的Vivado。
  • 编译内存要够。AD9361参考设计综合实现时,峰值内存可能超过20GB,虚拟机如果只分配了16GB以内内存,很容易卡死在中途。
  • 导出硬件时要留意手工指定的比特流。后面用PetaLinux或SDK导入硬件工程时,要确保能找到对应的.xsa或.hdf文件。

编译成功的标志是生成BOOT.BIN或配套的比特流文件,然后烧入SD卡或QSPI Flash。这块操作细节比较多,我后面会单独写一篇展开。

2.3 运行ADI官方Linux镜像并验证设备

最快的方式是使用ADI发布的预编译Linux镜像,把镜像写进SD卡,板卡上电后通过串口登录。登录后先做三件事:

iio_info | grep ad9361 dmesg | grep ad9361 iio_info -s

正常情况下,你会看到:

  • 系统里存在ad9361-phy设备;
  • dmesg里出现“AD9361 Rev.X initialized”之类的成功信息;
  • iio_info -s能列出FMCOMMS2对应的设备项,类似cf-ad9361-dds-core-lpc、cf-ad9361-lpc。

如果这里就没通过,先别急着做BPSK或设备树迁移,回到硬件连接和镜像版本上排查。这个阶段的问题,九成出在供电、时钟、镜像版本这三件事上。

3. 在官方例程里玩转I/Q数据:配置方式与最小收发验证

3.1 三种配置方式:图形界面、API、设备树

官方例程的价值之一,是同时给了你不同层次的配置手段。我实际使用中大概会用三种方式:

配置方式适用场景特点
iio-oscilloscope图形界面快速验证、看时域/频域波形直观,所见即所得
libiio API(Python/C)自动化测试和数据处理灵活,方便脚本化
设备树/驱动默认参数产品固件化上电即用,无需人工干预

这三种方式的底层都走同一套抽象机制:libiio的attribute机制。AD9361的每个频率、增益、采样率参数,在Linux里都对应一个attribute,读写attribute本质上就是在读写寄存器。理解这一点之后,你会发现很多看似复杂的工具操作,背后都是同一套“读属性、写属性、读缓冲、写缓冲”。

3.2 用Python通过libiio完成一次收发

下面这段是我在官方例程镜像上跑通的最小收发示例,使用Python的pylibiio绑定:

import iio ctx = iio.Context() # 扫描本地IIO设备 # 找到发射、接收、PHY控制设备 tx = ctx.find_device('cf-ad9361-dds-core-lpc') rx = ctx.find_device('cf-ad9361-lpc') phy = ctx.find_device('ad9361-phy') # 配置射频参数 phy.attrs['frequency'].value = str(2400000000) # 2.4GHz phy.attrs['sampling_frequency'].value = str(40000000) # 40MSPS # 打开对应通道 tx_chn = tx.find_channel('altvoltage0', False) # DDS频率控制通道 rx_chn = rx.find_channel('voltage0', True) # 接收数据通道 # 构造一段正弦波样本发送 import numpy as np N = 4096 t = np.arange(N) samples = (32767 * np.sin(2 * np.pi * 0.01 * t)).astype(np.int16) tx_buf = iio.Buffer(tx, N, cyclic=True) tx_buf.write(samples.tobytes()) tx_buf.push() # 接收 rx_buf = iio.Buffer(rx, N) rx_buf.refill() data = rx_buf.read()

这里有几个容易错的地方,逐个说一下:

  • find_channel的类型参数。voltage0代表数据通道,altvoltage0代表DDS的频率控制通道,搞反了会报通道找不到。
  • 发射缓冲的cyclic=True可以避免反复重新push数据,调试时很方便,但正式跑连续流时记得关掉。
  • 接收缓冲的refill是阻塞的,实际工程里最好放到独立线程,否则UI线程会被卡死。

3.3 如何判断数据链路是否正常

拿到接收数据后,判断链路是否正常的标准不是“波形看起来像正弦波”,而是几个关键指标:

  • 信号功率:对I/Q样本求均方根,看看数值是否在合理范围,避免饱和或过小;
  • 频谱纯净度:对样本做FFT,看峰值是否落在设定的中心频率上;
  • IQ幅度差:看I和Q两路的幅度是否接近,差距过大通常意味着接口时序或增益不平衡有问题。

这些用iio-oscilloscope的FFT窗口来看最直观。其实官方例程里带着的iio-oscilloscope本身就是检验链路的好工具,很多人只把它当成“示波器”用,忽略了它在调试数据通路时的重要价值。

4. 在AD9361官方例程上实现BPSK调制解调并取出数据

4.1 BPSK在AD9361链路里的正确打开方式

BPSK是最简单的数字调制方式,把信息比特映射到0和π两个相位上。在AD9361平台上做BPSK,很多人第一反应是“直接把串行比特流喂给AD9361”——这是典型的误区。AD9361的基带接口只认识I/Q样本,不会帮你做符号映射。你需要自己先把比特变成I/Q符号流,再从发射通道发出去。

以1个样本代表1个BPSK符号为例,映射规则就是:

  • 数据位1映射为 I = +A,Q = 0
  • 数据位0映射为 I = -A,Q = 0

如果发送端和接收端之间有频率偏差,解调端还需要在软件或FPGA里做频偏估计和载波恢复,否则星座图会整体旋转。官方例程自带的DDS模式可以生成纯载波,适合先做频率校准,再用自定义数据流测试BPSK。

4.2 软件侧的最小BPSK发射机与接收机实现

如果暂时不具备FPGA改造条件,直接用libiio的发射缓冲推样本是最快的做法。下面是一段在官方镜像上跑过的发射逻辑:

import iio import numpy as np FREQ = 2400000000 # 2.4GHz FS = 40000000 # 采样率 SYMBOL_RATE = 1000000 # 符号率 SPB = FS // SYMBOL_RATE # 每个符号对应的样本数 def bits_to_bpsk_interleaved(bits, amplitude=30000): """把比特序列映射成I/Q交织的int16数据""" symbols = [] for bit in bits: phase = 1.0 if bit == 1 else -1.0 symbols.extend([phase] * SPB) sig = np.array(symbols, dtype=np.float32) i_data = (sig * amplitude).astype(np.int16) q_data = np.zeros_like(i_data) interleaved = np.empty(len(i_data) * 2, dtype=np.int16) interleaved[0::2] = i_data interleaved[1::2] = q_data return interleaved.tobytes() # 发送已知PN序列,便于接收端同步 pn = [1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1] raw_bytes = bits_to_bpsk_interleaved(pn) # 构造发射设备并循环发送 ctx = iio.Context() tx = ctx.find_device('cf-ad9361-dds-core-lpc') tx_buf = iio.Buffer(tx, len(raw_bytes) // 4, cyclic=True) tx_buf.write(raw_bytes) tx_buf.push()

接收解调部分,因为有频偏和噪声,纯软件解调的步骤通常是:

  1. 从接收缓冲取I/Q样本;
  2. 做幅度归一化,等效自动增益控制;
  3. 用短训练序列估计频偏,做混频校正;
  4. 匹配滤波或相关解调,提取符号;
  5. 帧同步,用PN序列做滑动相关找到帧头,再做硬判决。

我见过不少人在“解调不出数据”这一步卡住,多半不是调制问题,而是漏了频偏补偿。实测中,即使频率偏差只有几十kHz,BPSK星座图也会持续旋转,如果不做估计和补偿,解调器输出的误码率会非常高。经验做法是:先发一段已知的短训练序列,接收端用自相关法算出频偏,再去做后续解调。

4.3 星座图与误码率验证:量化确认链路质量

当BPSK收发跑通后,下一步是定量验证。在官方例程里,我习惯在发送端构造已知伪随机序列,接收端解调后统计误码率。用iio-oscilloscope的星座图窗口能直接看到三类问题:

  • 星座点聚类不明显:信号功率太低,或者频偏太大;
  • 星座点旋转:载波频偏未纠正;
  • 上下幅度不对称:I/Q增益不平衡,需要调整AD9361的发射或接收增益校准参数。

实际测试时,我还喜欢把发射功率设为约-10dBm,接收端开启AGC,然后以发射功率和接收增益为变量,记录“功率-误码率”表格。这也是官方例程最实用的一步:它能帮你建立对整个链路的量化认知。

发射功率(dBm)RX增益(dB)接收功率(dBm)实测误码率
-1010-300
-3030-300
-5040-40约1e-4
-6040-50无法同步

5. 把AD9361原有设备树迁移到新PetaLinux工程的完整流程

5.1 为什么需要自定义PetaLinux工程,以及设备树迁移踩坑的根因

很多项目到了产品化阶段,都要为AD9361建立定制化PetaLinux工程。比如你要集成WiFi模组、自定义FPGA IP、裁剪内核,这时候直接跟着ADI的Yocto分支走并不合适,因为它面向的是通用开发板,冗余东西太多。

设备树迁移之所以容易踩坑,根因在于AD9361驱动要正常运行,依赖一大堆节点和属性:时钟节点、GPIO节点、SPI节点、DMA通道节点,以及FPGA侧的AXI IP节点。这些节点之间存在依赖关系,只复制一个ad9361节点的片段,往往缺胳膊少腿,驱动就算被加载,也会在probe阶段失败。

另一个常见误区是:把ADI内核源码里整套设备树文件不加修改扔进PetaLinux就编译。这种做法大概率会在编译阶段爆出大量错误,因为PetaLinux工程的内核版本、设备树include头文件路径,可能和ADI分支不一致。

5.2 迁移具体步骤:从ADI源码中提取关键文件

下面这套步骤是我在实际迁移中使用过的流程,尽量做到与内核版本弱相关:

  1. 在ADI Linux内核源码里找到目标板卡对应的设备树文件。例程里通常是类似zynq-zed-...-ad9361.dts,或者ADI整板上对应的.dts文件;
  2. 提取核心片段:
    • AD9361自身节点,通常在ad9361.dtsi或adi-ad9361.dtsi里;
    • FPGA相关节点:fpga-axi@0下面的axi-ad9361、cf-ad9361-dds-core等子节点;
    • 时钟节点和clocks属性;
    • SPI、GPIO、中断等连接信息;
  3. 在PetaLinux工程里,把设备树源文件放到meta-user/recipes-kernel/linux/linux-xlnx目录下,或者放到自定义layer中;
  4. 在petalinux-config的Subsystem AUTO Hardware Settings里,将设备树生成方式改为“从用户提供的dts/dtsi编译”;
  5. 修改主dts文件的include关系,确保编译顺序正确;
  6. 重新执行petalinux-build -c kernel,出现错误时优先检查include头文件路径。

5.3 必须手动检查的依赖项

设备树迁移完成后,不要急着打包boot镜像,先手动检查几个关键依赖:

  • clocks:AD9361驱动需要外部参考时钟和内部时钟相关属性,确认属性名和频率符合驱动要求;
  • interrupts:中断号要匹配FPGA的中断控制器编号;
  • spi:AD9361的SPI控制接口通常挂在PS端SPI控制器下,片选号要正确;
  • dmas:接收和发射DMA通道对应的dma-names必须分别是rx和tx。

我把可能遇到的问题整理成了表格:

现象主要可能原因
dmesg里ad9361驱动probe失败SPI节点或时钟节点缺失
能识别ad9361-phy但DMA数据不通dmas节点与FPGA AXI地址不匹配
iio_info能看到设备但采样无数据时钟相位问题或FPGA比特流与设备树不一致
编译报找不到ad9361节点定义include头文件路径不对

5.4 关于迁移方式的一点个人建议

实际项目里,我最终采用的方案是把ADI发布的PetaLinux参考模板当作蓝本,在它基础上增删功能,而不是完全从零创建工程。这样可以最大程度保留原有设备树结构,把风险降下来。另外,能跑的DTS/DTSI文件一定要打tag保存,因为内核升级后这些文件很容易因API变化而编译失败,遇到版本升级先比对diff,再决定是手工适配还是继续用旧版本。

6. 官方例程调试中的五个常见故障信号

6.1 故障定位思路和现象对照

这套流程走下来,我总结了五个最常遇到的故障信号,方便你快速对照:

序号现象排查方向
1dmesg无ad9361硬件连接、供电、镜像版本
2ad9361-phy存在但TX无输出LVDS/CMOS设置、TX缓冲是否为空、DAC使能
3RX有数据但全是噪声频率设置不一致、两端未共地、时钟偏差过大
4星座图旋转载波频偏,需要频偏估计与补偿
5DMA缓冲区溢出采样率过高、CPU频率或内存带宽不足

6.2 最值得花时间掌握的调试手段

调试AD9361官方例程,我最推荐“三层联合观察”:

第一层看驱动日志,确认寄存器初始化过程没有异常;第二层用iio-oscilloscope看时域和频谱,判断是模拟链路问题还是数字链路问题;第三层用libiio API抓取原始样本做FFT和星座图,定位是算法问题还是数据通路问题。

这套方法的核心是“分层定位”。一次TX无输出,涉及的环节至少有五六个:Linux驱动有没有配置好、DMA有没有搬运数据、HDL有没有正确打包、AD9361有没有使能发射、天线端有没有接对。如果一股脑从头查,效率极低。先看iio-oscilloscope里有没有样本、样本长什么样,就能把问题快速排除掉一大半。

6.3 一套可以复用的DMA通路问题排查顺序

如果你怀疑数据通路DMA有问题,可以按下面这个顺序排查:

  1. 用iio_info确认DMA相关设备是否存在;
  2. 用官方iio-oscilloscope抓一段数据,看是否有非零样本;
  3. 如果样本全零,检查发射端有没有持续push数据,接收端有没有调用refill;
  4. 如果样本有值但时域波形异常,检查HDL工程中DMA地址和Linux内核解析的设备树地址是否一致;
  5. 最后再用逻辑分析仪或ILA观察FPGA和AD9361数字接口时序,确认LVDS/CMOS的采样沿是否正确。

这套顺序看起来简单,但能避免很多无头苍蝇式的折腾。尤其是第4步,设备树里描述的AXI地址和HDL里实际分配的地址如果不一致,数据流会表现为“设备正常但数据全是错位或全零”,很难直接看出来。

7. 写在最后:我建议你从这套例程里学到的三件事

折腾AD9361这一两年,我最大的体会是:做无线通信算法时的思维习惯,到了真实硬件平台上往往会把事情想简单。调制解调跑不通,原因常常不是算法本身,而是射频参数配置、数字接口时序、数据通路这些“底层细节”没对上。

所以最后送大家三条个人经验:

  1. 先跑通,再优化。先把官方例程原样跑通,不要一开始就想改这改那,尤其是HDL和驱动这种牵一发动全身的部分;
  2. 数据通路永远优先。ADC端拿不到有效样本,后面一切算法都等于纸上谈兵,先把“能不能取到干净数据”这件事搞定;
  3. 给能跑的版本打tag。无论是HDL代码、设备树还是Linux镜像,能跑的版本一定值得备份,开发越深入,回退到基线版本的价值就越大。

下一篇我会写官方例程里FPGA侧HDL的详细解读,包括数据打包模块、AXI DMA的配置流程,以及如何在此基础上增加自定义调制解调IP。如果你在跑官方例程或做设备树迁移时遇到怪问题,欢迎带上日志和现场现象来一起讨论。

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

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

立即咨询