1. 项目缘起与整体设计思路
AD9361 这颗射频收发芯片,搞过软件无线电或者通信基带的朋友应该都不陌生。它覆盖 70 MHz 到 6 GHz 的频段,通道带宽可调,支持 TDD 和 FDD 两种双工模式,在小型基站、手持电台、频谱监测、教学实验平台里出镜率极高。而 ZYNQ 系列 SoC 把 ARM 处理器和 FPGA 逻辑集成在一颗芯片上,天然适合做 AD9361 的控制器——PL 端负责高速数据接口和时序逻辑,PS 端跑驱动和上层协议栈。这套组合我前后搭过不下五次,每次都有新坑,所以干脆把完整流程梳理一遍。
这篇内容面向的是已经具备基本 Vivado 操作能力、了解 ZYNQ 架构、但还没完整走过 AD9361 工程搭建流程的开发者。我会从工程创建讲起,一直讲到导出硬件描述文件、导入 SDK 建立裸机工程,把中间每个关键决策点的理由讲清楚。整套流程走完,你手里应该有一个能编译通过、能在板子上跑起来的基础工程框架,后续加自己的逻辑或者跑 Linux 都有个靠谱的起点。
先说一下整体思路。AD9361 和 ZYNQ 的连接方式通常有两种:一种是 PS 端通过 SPI 配置 AD9361 的寄存器,PL 端通过 LVDS 或者 CMOS 接口收发数据;另一种是全部用 PL 端的逻辑做配置和数据通路。第一种方式更常见,因为 SPI 配置时序用软件控制更灵活,调试也方便。我这次采用的方案是 PS 端 SPI 配置加 PL 端 LVDS 数据接口,这也是官方参考设计和大多数开源项目采用的架构。
为什么选 LVDS 而不是 CMOS?LVDS 差分信号抗干扰能力强,速率可以拉得更高,AD9361 在双通道全速工作时数据率不低,CMOS 接口在高速下时序裕量很紧张。当然如果你的采样率不高、通道数少,CMOS 也能用,接线还简单些。这个取舍后面会细说。
整个工程的核心模块包括:ZYNQ Processing System 配置、SPI 接口逻辑、LVDS 接收和发送通路、时钟管理模块、以及必要的复位和同步逻辑。SDK 部分则包括 SPI 驱动、AD9361 初始化序列、以及一个简单的主循环来验证配置是否成功。下面按步骤展开。
2. Vivado 工程创建与 ZYNQ PS 端配置
2.1 新建工程与器件选型
打开 Vivado,新建工程,器件选型这一步很关键。你得根据自己实际用的板子来选 ZYNQ 型号。常见的比如 XC7Z020、XC7Z035、XC7Z045 这些,封装和速度等级也要对上。选错了后面管脚约束全得重来。我一般习惯在工程名里带上芯片型号和日期,比如ad9361_zynq7020_202406,方便后续版本管理。
工程类型选 RTL Project,不勾选“Do not specify sources at this time”,因为后面要手动添加 IP 和约束文件。目标语言选 Verilog 还是 VHDL 看个人习惯,我这边 Verilog 用得多,后续示例也以 Verilog 为主。
2.2 ZYNQ Processing System IP 配置
在 Block Design 里添加 ZYNQ7 Processing System IP。双击进去配置,几个关键页面不能漏:
PS-PL Configuration 页面:使能 M_AXI_GP0 接口,这是 PS 端访问 PL 端寄存器的通道,SPI 配置逻辑需要挂在上面。如果你打算用 PS 端的 SPI 控制器直接连 AD9361,那还要使能 SPI0 或 SPI1,但那样就得把 AD9361 的 SPI 管脚接到 MIO 上,灵活性差一些。我倾向于用 PL 端逻辑实现 SPI,这样管脚分配自由,时序也好调。
Peripheral I/O Pins 页面:使能需要的 MIO 外设,比如 UART1 用于打印调试信息,SD0 用于后续跑 Linux 时挂文件系统,GPIO 用于控制 AD9361 的复位和使能引脚。UART 是必须的,不然你连配置成功没有都不知道。
Clock Configuration 页面:输入时钟频率要填对,一般是 33.333 MHz 或者 50 MHz,取决于板子上的晶振。PS 端会从这个输入时钟产生各个外设的时钟,PL 端也会用到 FCLK_CLK0,这个时钟后面要供给 SPI 逻辑和 LVDS 接口逻辑使用。我一般把 FCLK_CLK0 设成 100 MHz,够用且时序好收敛。
DDR Configuration 页面:根据板子上的 DDR 型号配置,这个如果选错,SDK 里的程序跑不起来,而且报错信息往往很隐晦。建议直接找板子厂商提供的配置文件导入,别自己手动填。
配置完成后,ZYNQ IP 会引出 M_AXI_GP0 接口和 FCLK_CLK0 等信号。接下来要把这些信号连到我们自己的逻辑模块上。
2.3 自定义 SPI 主控模块
AD9361 的 SPI 接口支持 4 线模式,时序上要注意几个点:SPI 时钟空闲时保持高电平或者低电平取决于 CPOL 设置,数据在时钟的哪个边沿采样取决于 CPHA。AD9361 的 SPI 模式通常是 CPOL=0、CPHA=0,也就是时钟空闲低电平,数据在上升沿采样。但具体还要看你的逻辑设计,我见过有人用 CPOL=1 也能跑通,关键是和 AD9361 数据手册里的时序图对上。
我写的 SPI 主控模块包含以下端口:时钟、复位、开始传输信号、片选、以及一个简单的寄存器地址和数据接口。传输时先发 16 位命令字,其中 bit15 是读写标志,bit14 到 bit12 是字节数编码,bit11 到 bit0 是起始地址。然后根据读写方向发送或接收数据。这个协议在 AD9361 的数据手册里有详细说明,实现的时候建议用状态机来写,状态包括 IDLE、SEND_CMD、SEND_DATA、RECV_DATA、DONE 这几个。
一个容易忽略的点是 SPI 时钟的分频系数。AD9361 的 SPI 时钟最高支持 50 MHz,但实际用的时候建议先降到 10 MHz 以下调试,等逻辑稳定了再往上提。我一般初始设成 FCLK_CLK0 的 8 分频,也就是 12.5 MHz,这个速率下时序裕量充足,示波器或者逻辑分析仪抓波形也方便。
2.4 LVDS 接口逻辑设计
LVDS 接口这部分是整个工程里最复杂的。AD9361 的 LVDS 模式有几种不同的数据格式,包括单通道全双工、双通道全双工、以及不同的数据位宽模式。我这次用的是双通道全双工、DDR 模式,数据在时钟的上升沿和下降沿都传输。
接收通路方面,AD9361 会输出一个数据时钟(RX_CLK)和一个帧同步信号(RX_FRAME),数据线是 RX_D[11:0] 或者更少位宽取决于配置。你需要用 IDDR 原语或者 ISERDES 原语来把 DDR 数据转成 SDR 数据,然后用 RX_FRAME 来对齐通道和 I/Q 数据。发送通路类似,用 ODDR 或者 OSERDES 把并行数据转成 DDR 输出。
这里有个大坑:IDDR 和 ODDR 原语的使用需要配合正确的时序约束,否则综合出来的结果和仿真对不上。我建议在 XDC 约束文件里对 LVDS 管脚加上set_input_delay和set_output_delay约束,具体数值参考 AD9361 的数据手册和你的 PCB 走线长度。如果板子是自己画的,走线等长控制得好,约束可以宽松些;如果是买的开发板,最好找厂商要参考约束。
时钟管理方面,AD9361 的 DATA_CLK 需要接到 PL 端的全局时钟引脚上,然后用 BUFG 或者 MMCM 来驱动后续逻辑。如果 DATA_CLK 频率较高,可能还需要做时钟域转换。我一般会在接收通路里加一个异步 FIFO,把 DATA_CLK 域的数据转到 FCLK_CLK0 域,方便 PS 端读取。
2.5 管脚约束与综合实现
管脚约束文件里要包含所有用到的管脚:SPI 的 SCLK、MOSI、MISO、CS_N,LVDS 的 DATA_CLK、RX_FRAME、RX_D[11:0]、TX_FRAME、TX_D[11:0],以及 AD9361 的复位和使能控制线。每个管脚都要指定电平标准,LVDS 管脚要指定为 LVDS 或者 LVDS_25,具体看你的 Bank 电压。
综合和实现过程中要关注时序报告。如果出现建立时间或者保持时间违例,先检查时钟约束是否正确,再看逻辑级数是否过多。LVDS 接口的逻辑通常需要流水线化处理,不然在高速下很难收敛。我一般会在关键路径上插入一级寄存器,虽然增加了一个时钟周期的延迟,但时序裕量会好很多。
生成比特流之后,导出硬件描述文件。在 Vivado 里选择 File -> Export -> Export Hardware,勾选 Include bitstream,导出格式选 XSA。这个文件就是后续导入 SDK 的基础。
3. SDK 工程建立与 AD9361 初始化
3.1 新建 SDK 工程与硬件平台导入
打开 SDK,选择 File -> New -> Application Project。在硬件平台页面,点击 New 创建新的硬件平台,浏览到刚才导出的 XSA 文件。SDK 会自动解析出 PS 端的配置信息,包括 UART、DDR、时钟等。给硬件平台起个名字,比如ad9361_hw_platform,然后选择工程模板。我一般先用 Hello World 模板验证硬件平台是否正常,确认 UART 能打印出信息之后,再换成 Empty Application 写自己的代码。
这里有个细节:如果你的 XSA 文件里包含了比特流,SDK 在下载程序时会自动把比特流也烧进去。但有时候你只想更新软件不想重新烧比特流,可以在 Run Configuration 里取消勾选“Program FPGA”。不过第一次跑的时候还是建议勾上,确保 PL 端逻辑已经加载。
3.2 SPI 驱动编写与寄存器配置
AD9361 的初始化本质上就是通过 SPI 往一系列寄存器里写值。这些值决定了芯片的采样率、带宽、增益、本振频率等参数。官方提供了一个初始化脚本,但那个脚本是针对特定配置生成的,换一个采样率或者频段就得重新生成。我建议先理解几个关键寄存器的含义,这样调试的时候心里有数。
比如寄存器 0x002 是控制收发通道使能的,0x003 是控制数据接口模式的,0x010 是控制时钟分频的。这些寄存器的具体位定义在数据手册里都有,但手册很长,我一般会把常用的几个整理成一张表放在手边。
写 SPI 驱动的时候,要注意 AD9361 的 SPI 传输是 MSB 优先的,而且片选信号在传输期间要保持低电平。我见过有人片选信号在字节之间拉高了,结果 AD9361 把每个字节当成独立命令处理,配置全乱套。另外,两次 SPI 传输之间要留足够的间隔时间,AD9361 内部处理需要时间,太快了会丢配置。我一般会在每次写操作后加一个微秒级的延时,具体值参考数据手册的时序要求。
3.3 初始化序列与验证方法
初始化序列一般包括以下几个步骤:复位 AD9361、配置时钟和 PLL、配置收发通道、配置数据接口、使能收发通道。每一步都要读回寄存器确认写入成功。我习惯在每写一个关键寄存器后读回校验,如果读回的值和写入的不一致,就打印错误信息并停止初始化。这样能快速定位问题出在哪一步。
验证初始化是否成功,最直接的方法是看 AD9361 的 DATA_CLK 有没有输出。如果时钟正常输出,说明芯片已经进入工作状态。然后可以用一个简单的回环测试:发送端发一个单音信号,接收端看能不能收到。如果收不到,先检查 LVDS 接口的帧同步信号是否对齐,再检查收发通道的使能位是否写对了。
还有一个常用的验证手段是读 AD9361 的产品 ID 寄存器,地址是 0x037,正常应该读到 0x0A 或者 0x0B 取决于芯片版本。如果读不到,说明 SPI 通信本身就有问题,先查硬件连接和 SPI 时序。
4. 常见问题排查与实操避坑指南
4.1 SPI 通信失败排查
SPI 通信失败是最常见的问题,表现是读产品 ID 返回全 0 或者全 F。排查步骤我一般这样走:先用逻辑分析仪或者示波器抓 SCLK、MOSI、CS_N 三根线的波形,确认时钟频率是否在 AD9361 允许范围内,片选信号是否在传输期间保持低电平,MOSI 上的数据是否符合 MSB 优先的格式。如果波形看起来没问题,再检查 MISO 线是否有数据返回。如果 MISO 一直没反应,可能是 AD9361 没有正常上电或者复位引脚时序不对。
复位引脚的时序要求是:上电后保持低电平至少 1 毫秒,然后拉高,再等待至少 1 毫秒才能开始 SPI 通信。我见过有人复位后立刻发 SPI 命令,结果 AD9361 还没完成内部初始化,自然不响应。这个延时宁可长一点,别卡着最小值。
4.2 LVDS 数据对齐问题
LVDS 数据对齐是另一个高频问题。表现是接收到的数据看起来有规律但就是不对,或者 I/Q 通道交换了。这通常是帧同步信号和数据的相位关系没调对。AD9361 的 RX_FRAME 信号指示了帧的起始位置,你需要用这个信号来复位接收状态机或者对齐通道。如果相位差了一个时钟周期,数据就会整体偏移。
解决方法是在接收逻辑里加一个可调的延时链,通过寄存器控制延时级数,然后扫描不同的延时值,看哪个值下接收到的数据是正确的。这个调试过程比较耗时,但一旦调好,后续就很稳定。我一般会在逻辑里预留 8 到 16 级的延时调节范围,每级大约 100 到 200 皮秒,具体取决于你的器件速度等级。
4.3 时序违例与时钟域问题
时序违例在高速 LVDS 接口里很常见。如果综合报告里出现建立时间违例,先看违例路径是不是在 LVDS 接收逻辑里。如果是,尝试在路径中间插入寄存器,把长组合逻辑打断。另外检查时钟约束是否覆盖了所有时钟域,特别是 DATA_CLK 和 FCLK_CLK0 之间的跨时钟域路径,一定要加set_false_path或者set_clock_groups约束,否则工具会按同步时钟去分析,报一堆假违例。
跨时钟域数据传输一定要用异步 FIFO 或者双口 RAM,不能用简单的寄存器打拍。我见过有人用两级寄存器同步多比特数据,结果偶尔出现数据错位,查了很久才发现是跨时钟域问题。多比特信号跨时钟域必须用握手协议或者 FIFO,这是铁律。
4.4 SDK 调试技巧
SDK 里调试的时候,我习惯把 UART 打印级别分成几档:错误信息、警告信息、调试信息。初始化阶段打开调试信息,能看到每一步的执行情况。等系统稳定了再关掉调试信息,减少打印开销。另外,SDK 的调试器可以实时查看变量和寄存器,但要注意 PL 端的寄存器读取有延迟,连续读同一个寄存器可能读到旧值。如果需要频繁读取,建议在 PL 端加一个寄存器缓存,PS 端读缓存而不是直接读 PL。
还有一个实用技巧:在 SDK 里写一个简单的命令行接口,通过 UART 接收命令来读写 AD9361 寄存器。这样调试的时候不用反复编译下载,直接敲命令就能改配置,效率高很多。这个接口实现起来不复杂,一个字符接收中断加一个简单的解析函数就够了。
5. 工程扩展与后续优化方向
基础工程跑通之后,后续可以往几个方向扩展。一个是加上 DMA 传输,把接收到的数据直接搬到 DDR 里,PS 端做后续处理。AXI DMA IP 用起来不难,但要注意数据位宽和突发长度的配置,配错了会导致传输效率很低甚至传输出错。另一个方向是跑 Linux 系统,在 Linux 下用 IIO 子系统来管理 AD9361,这样上层应用开发会方便很多,但驱动移植和设备树配置又是另一套东西了。
如果要做实时信号处理,可以考虑把部分算法放到 PL 端实现,比如数字下变频、抽取滤波、FFT 等。ZYNQ 的 PL 端资源有限,做复杂算法之前先评估资源占用,别到时候布局布线过不去。我一般会先用 Vivado 的 IP 核评估一下资源,比如 FIR Compiler 和 FFT IP 核,看看逻辑资源和 DSP 切片够不够用。
功耗方面也值得关注。AD9361 在双通道全速工作时功耗不低,加上 ZYNQ 本身的功耗,如果做手持设备,散热和电池续航都是问题。可以在不需要的时候关掉一个通道,或者降低采样率来省电。AD9361 支持多种省电模式,具体配置方法在数据手册里有说明。
最后说一个我踩过的坑:AD9361 的配置寄存器里有一些是只写的,读回来不是写入的值。如果你在初始化序列里对这类寄存器做了读回校验,会误判为配置失败。解决办法是查数据手册确认哪些寄存器是可读可写的,只对可读的寄存器做校验。这个细节很容易忽略,但一旦碰上,能让你查半天。