简介:AXI_AD9361 是一套基于 Verilog 实现的硬件驱动工程,面向从事软件定义无线电、无线通信及嵌入式系统开发的工程师与学习者,用于解决 FPGA 与 AD9361 射频收发器之间的高速数据交互问题。工程通过 AXI 总线完成对 AD9361 的初始化、工作模式配置以及收发通道的数据读写管理,涵盖接收通道、发射通道、TDD 控制、LVDS 接口与 PN 监控等模块,并配有约束文件与仿真脚本,便于在目标平台上编译验证。压缩包共 21 个文件,约 40KB,以 12 个 Verilog 源文件为核心,辅以 tcl 脚本、xdc 约束、xpr 工程文件、Makefile 及 xml 组件描述,结构紧凑、模块划分清晰。目前已有 1268 人学习下载,适合希望深入理解 AXI 协议时序与 AD9361 硬件接口、快速搭建收发链路验证环境的开发者参考借鉴。
1. 从 axi_ad9361 这个压缩包说起:它到底解决了什么问题
如果你手头有一块 AD9361 射频板,FPGA 侧跑着 Xilinx 的 AXI 总线,而你需要让 Linux 系统通过 AXI 接口去配置 AD9361 的寄存器、读写收发数据,那么 axi_ad9361 这个驱动方案就是你绕不开的东西。它本质上是一个运行在 Linux 内核空间的 AXI 外设驱动,把 AD9361 的 SPI 配置通道和收发数据通道统一映射到 AXI 地址空间,让用户空间可以通过标准字符设备或 IIO 子系统来操作这颗射频芯片。很多人第一次拿到这个压缩包时,看到里面混杂着 AXI 接口定义、9361 初始化代码和驱动框架,会有点懵——到底从哪开始看?哪些是必须改的?哪些是平台相关的?我当初也是这么过来的,翻车了好几次才理清楚里面的层次关系。这篇文章就是要把 axi_ad9361 驱动从编译、加载、初始化到调试的完整路径讲清楚,适合正在做 AD9361 基带开发、需要自己写或改 AXI 驱动的工程师,也适合刚接触 Linux 驱动开发但手上有 AD9361 硬件的人。
2. axi_ad9361 驱动的架构拆解与编译环境搭建
2.1 AXI 接口层与 AD9361 控制层的分工
axi_ad9361 这个驱动不是一坨代码,它内部有清晰的分层。最底下是 AXI 总线接口层,负责和 FPGA 里的 AXI 从设备通信,通常包括 AXI-Lite 用于寄存器配置、AXI-Stream 用于收发数据。中间是 AD9361 控制层,封装了 AD9361 的 SPI 寄存器读写、初始化序列、滤波器配置、增益控制等逻辑。最上面是 Linux 设备驱动层,注册字符设备或 IIO 设备,暴露给用户空间。
理解这个分层很重要,因为调试的时候你要知道问题出在哪一层。比如 AD9361 初始化时 CP OVRG HIGH 被置为同时 RX PLL 没有锁定,这通常是 SPI 配置通道的问题,属于控制层;而 AXI-Stream 数据不通,那就要查 AXI 接口层的 DMA 和时钟。常见做法是把 AXI-Lite 的基地址和 AXI-Stream 的 DMA 通道分开处理,不要混在一个驱动模块里。
我一般会先确认 FPGA 侧 AXI 地址映射,再对应到驱动里的基地址定义。如果你用的是 Xilinx Zynq 或 MicroBlaze 平台,设备树里会有对应的 reg 属性,驱动通过 platform_get_resource 拿到物理地址再 ioremap。这一步如果地址对不上,后面所有寄存器读写都是白费。
2.2 交叉编译环境与内核头文件准备
编译 axi_ad9361 驱动之前,你需要一套和目标板内核版本匹配的交叉编译工具链和内核头文件。这不是随便找个 arm-linux-gnueabi-gcc 就能糊弄过去的,内核模块的 vermagic 必须和运行内核完全一致,否则 insmod 会直接报 version magic 错误。
# 设置交叉编译环境变量,以 Zynq 为例 export ARCH=arm export CROSS_COMPILE=arm-linux-gnueabihf- export KERNEL_DIR=/path/to/linux-xlnx # 确认内核版本和配置 cd $KERNEL_DIR make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- zynq_defconfig make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules_prepare # 编译驱动模块 cd /path/to/axi_ad9361_driver make -C $KERNEL_DIR M=$(pwd) modules这里的关键参数是 KERNEL_DIR 必须指向你目标板上实际运行的内核源码目录,不是随便一个版本。modules_prepare 会生成编译模块所需的头文件和符号信息。如果你拿到的内核是厂商定制的,最好从板子里把 /proc/config.gz 导出来对比配置。编译完成后用 modinfo 检查生成的 .ko 文件,确认 vermagic 和内核版本字符串一致。
注意:很多新手会忽略内核配置里的 CONFIG_MODVERSIONS 选项,如果目标内核开启了符号版本校验,而你的驱动编译时没有对应的 Module.symvers,加载时会报 disagrees about version of symbol 错误。解决办法是从目标内核源码目录拷贝 Module.symvers 到驱动目录再编译。
2.3 设备树节点的编写与 AXI 地址映射
设备树是 axi_ad9361 驱动能否正确 probe 的关键。你需要在设备树里描述 AXI 外设的基地址、中断号、时钟、以及 AD9361 相关的 GPIO 控制线(比如 RESETB、ENABLE、TXNRX)。
axi_ad9361: axi_ad9361@43c00000 { compatible = "ad9361,axi-ad9361"; reg = <0x43c00000 0x10000>; interrupts = <0 29 4>; interrupt-parent = <&intc>; clocks = <&clk_0>; clock-names = "s_axi_aclk"; adi,reset-gpio = <&gpio0 55 0>; adi,enable-gpio = <&gpio0 56 0>; adi,txnrx-gpio = <&gpio0 57 0>; adi,spi-base = <0x43c10000>; };reg 属性里的 0x43c00000 是 AXI-Lite 寄存器空间的基地址,长度 0x10000 覆盖了 AD9361 的控制寄存器区域。interrupts 里的 29 是中断号,具体数值要看你的 FPGA 设计里 AXI 中断控制器的连接。adi,spi-base 是 AD9361 SPI 配置通道的独立地址,有些设计会把 SPI 和 AXI 数据通道分开编址。
编写设备树时最容易踩的坑是地址和 FPGA 综合出来的地址不一致。我一般会先在 Vivado 的 Address Editor 里确认每个 AXI 从设备的 offset,再写到设备树里。另外 GPIO 的极性也要注意,RESETB 是低有效,设备树里如果写成高有效,AD9361 永远起不来。
3. AD9361 初始化流程与寄存器配置实战
3.1 上电时序与 SPI 寄存器写入顺序
AD9361 的上电初始化不是简单写几个寄存器就完事,它有一套严格的时序要求。芯片手册里明确规定了 RESETB 拉高之后需要等待至少 1ms 才能开始 SPI 通信,ENABLE 信号要在 RESETB 之后至少 20us 拉高。这些时间参数如果不对,芯片内部状态机会卡住,表现就是后续所有寄存器读出来都是 0x80 或者 0xFF。
// AD9361 上电时序控制示例 static int ad9361_power_up(struct ad9361_dev *dev) { gpio_set_value(dev->reset_gpio, 0); mdelay(1); gpio_set_value(dev->reset_gpio, 1); udelay(100); // 等待 RESETB 稳定 gpio_set_value(dev->enable_gpio, 1); mdelay(20); // ENABLE 拉高后等待 // 开始 SPI 通信,先读芯片 ID 验证 ret = ad9361_spi_read(dev, 0x037); if (ret != 0x0A) { dev_err(dev->dev, "AD9361 chip ID mismatch: 0x%02X\n", ret); return -ENODEV; } return 0; }这段代码里 mdelay 和 udelay 的数值不是随便写的,1ms 和 20us 来自 AD9361 数据手册的 Power-Up Sequence 章节。读 0x037 寄存器是验证 SPI 通道是否正常工作的第一步,正常应该返回 0x0A。如果你读出来是 0x80 或者 0x00,说明 SPI 时序或者片选有问题。
常见做法是在 SPI 读写函数里加超时重试,因为 AD9361 在初始化阶段偶尔会出现 SPI 无响应的情况。我一般会重试 3 次,每次间隔 1ms,如果还是失败就报错退出。
3.2 初始化脚本的加载与滤波器配置
AD9361 的初始化寄存器序列非常长,通常有几百个寄存器需要配置。ADI 官方提供的初始化脚本是一个文本文件,里面按地址和值的格式列出了所有需要写入的寄存器。axi_ad9361 驱动需要解析这个脚本并逐条写入。
// 解析初始化脚本并写入寄存器 static int ad9361_load_init_script(struct ad9361_dev *dev, const char *script) { char line[64]; u32 addr, val; int ret; while (sscanf(line, "%x %x", &addr, &val) == 2) { ret = ad9361_spi_write(dev, addr, val); if (ret < 0) { dev_err(dev->dev, "init script write failed at 0x%03X\n", addr); return ret; } // 某些寄存器写入后需要延时 if (addr == 0x03A || addr == 0x03B) mdelay(1); } return 0; }初始化脚本里有两个地方需要特别注意:0x03A 和 0x03B 是 RX 和 TX 的 PLL 控制寄存器,写入后需要等待 PLL 锁定,通常延时 1ms 再继续。如果你发现 RX PLL 没有锁定,先检查这两个寄存器写入后有没有加延时,再检查参考时钟频率是否和脚本里配置的一致。
滤波器配置是另一个容易翻车的地方。AD9361 内部有 RX FIR、TX FIR、RX HB1/HB2、TX HB1/HB2 等多级滤波器,每一级的系数都需要通过 SPI 写入。ADI 提供了一个滤波器设计工具,可以生成系数文件。我一般会先用工具生成系数,再手动转换成驱动能识别的格式。滤波器系数写错不会导致芯片报错,但频谱会完全不对,这种问题最难查。
3.3 收发通道使能与数据通路验证
初始化完成后,需要使能 RX 和 TX 通道,然后验证 AXI-Stream 数据通路是否正常。AD9361 的 RX 通道使能是通过 0x003 寄存器的 bit0 控制,TX 通道是 bit1。
// 使能 RX 和 TX 通道 static int ad9361_enable_channels(struct ad9361_dev *dev) { u8 val; // 使能 RX 通道 ad9361_spi_read(dev, 0x003, &val); val |= 0x01; ad9361_spi_write(dev, 0x003, val); // 使能 TX 通道 ad9361_spi_read(dev, 0x003, &val); val |= 0x02; ad9361_spi_write(dev, 0x003, val); // 等待通道稳定 mdelay(10); // 检查 RX PLL 锁定状态 ad9361_spi_read(dev, 0x247, &val); if (!(val & 0x02)) { dev_err(dev->dev, "RX PLL not locked, 0x247=0x%02X\n", val); return -EIO; } return 0; }0x247 寄存器是 PLL 锁定状态指示,bit1 对应 RX PLL,bit0 对应 TX PLL。如果你读出来一直是 0x80 或者 RX PLL 位不置位,先检查参考时钟有没有正常输入,再检查 0x03A 和 0x03B 的配置值。我遇到过因为参考时钟频率设错导致 PLL 死活锁不定的情况,查了两天才发现是晶振频率和脚本里写的不一致。
数据通路验证可以用 AXI-Stream 的 DMA 回环测试,发一组已知数据从 TX 出去,再从 RX 收回来对比。如果数据不对,先查 AXI-Stream 的时钟域和位宽配置,再查 DMA 的缓冲区地址对齐。
4. axi_ad9361 驱动调试中常见的翻车现场
4.1 寄存器读出固定值 0x80 或 0xFF
现象:AD9361 初始化时,读任何寄存器都返回 0x80 或 0xFF,芯片 ID 也读不对。
原因:SPI 通信根本没有建立起来。可能是 SPI 片选信号没有正确拉低,或者 SPI 时钟极性/相位配置错误,也可能是 AXI 地址映射不对,读写操作打到了错误的地址空间。
解决:先用示波器或逻辑分析仪抓 SPI 的 CS、CLK、MOSI 三根线,确认片选有拉低、时钟有波形、数据有变化。如果波形正常但读出来还是固定值,检查设备树里的 reg 地址和 FPGA 综合出来的地址是否一致。我遇到过 AXI 地址偏移写错 0x1000 的情况,读写全打到保留区域,读出来全是 0xFF。
4.2 RX PLL 无法锁定
现象:初始化脚本执行完毕,0x247 寄存器的 RX PLL 锁定位始终为 0,同时 CP OVRG HIGH 被置位。
原因:参考时钟频率和初始化脚本里配置的不匹配,或者 PLL 环路滤波器参数不对。CP OVRG HIGH 表示电荷泵过压,通常是 VCO 频率范围设置错误。
解决:先确认参考时钟的实际频率,用示波器量晶振或者时钟芯片的输出。然后检查初始化脚本里 0x03A 和 0x03B 的写入值,这两个寄存器决定了 PLL 的反馈分频比。如果参考时钟是 40MHz 而脚本是按 10MHz 写的,PLL 肯定锁不住。另外检查 0x23B 寄存器的 VCO 范围设置,AD9361 的 VCO 覆盖 6GHz 到 12GHz,范围设错也会导致 CP OVRG。
4.3 驱动加载后 probe 失败但没有任何报错
现象:insmod 驱动后,dmesg 里看不到 probe 成功的日志,也没有明显的错误信息,/dev 下没有生成设备节点。
原因:设备树里的 compatible 字符串和驱动里的 of_match_table 不匹配,或者设备树节点没有被内核解析到。
解决:先检查 /proc/device-tree 下有没有对应的节点,如果没有,说明设备树没有正确加载。如果有节点但驱动没 probe,用 cat /sys/firmware/devicetree/base/axi_ad9361/compatible 确认 compatible 字符串,再和驱动代码里的 of_device_id 结构体对比。我一般会在驱动 probe 函数入口加一句 printk,确认到底有没有进 probe。
4.4 AXI-Stream 数据丢包或错位
现象:DMA 传输的数据偶尔丢包,或者 I/Q 数据错位,星座图完全乱掉。
原因:AXI-Stream 的时钟域和 AD9361 的采样时钟不同步,或者 DMA 缓冲区没有按 AXI-Stream 位宽对齐。
解决:检查 AXI-Stream 的 tvalid/tready 握手信号,用 ILA 抓一下波形,看有没有 tvalid 拉高但 tready 一直低的情况。DMA 缓冲区地址要按 64 字节对齐,这是 AXI-Stream DMA 的常见要求。另外检查 AD9361 的采样时钟和 AXI-Stream 的 aclk 是不是同源,如果不是同源需要加异步 FIFO。
4.5 滤波器系数写入后频谱异常
现象:初始化完成,PLL 锁定,数据也能收发,但频谱仪上看输出信号带外抑制很差,或者通带内有纹波。
原因:滤波器系数写入顺序错误,或者系数值被截断。AD9361 的 FIR 滤波器系数是 16 位有符号数,如果驱动里用 u16 存储会丢失符号位。
解决:确认滤波器系数的数据类型是 s16 而不是 u16。检查写入顺序,RX FIR 和 TX FIR 的系数寄存器地址不同,不要搞混。我一般会先把系数读回来对比,确认写入的值和原始文件一致。如果读回来不一致,检查 SPI 写入函数有没有对数据做字节序转换。
5. 用 IIO 子系统暴露 AD9361 控制接口的进阶做法
5.1 从字符设备迁移到 IIO 的理由
早期 axi_ad9361 驱动多用字符设备暴露接口,用户空间通过 ioctl 来配置频率、增益、滤波器。这种方式能用,但不够灵活,每加一个功能就要加一个 ioctl 命令,而且和 Linux 标准的射频设备接口不兼容。IIO 子系统是 Linux 内核专门为 ADC/DAC 类设备设计的框架,AD9361 作为收发器天然适合用 IIO 来管理。
迁移到 IIO 之后,你可以用标准的 sysfs 接口来读写属性,比如 /sys/bus/iio/devices/iio:device0/out_altvoltage0_RX_LO_frequency 来设置 RX 本振频率。用户空间工具 iio_attr 和 libiio 可以直接操作,不用自己写 ioctl 解析。
// IIO 通道定义示例 static const struct iio_chan_spec ad9361_channels[] = { { .type = IIO_ALTVOLTAGE, .indexed = 1, .output = 1, .channel = 0, .extend_name = "RX_LO", .info_mask_separate = BIT(IIO_CHAN_INFO_FREQUENCY), }, { .type = IIO_VOLTAGE, .indexed = 1, .channel = 0, .extend_name = "RX", .info_mask_separate = BIT(IIO_CHAN_INFO_HARDWAREGAIN) | BIT(IIO_CHAN_INFO_SAMP_FREQ), }, };这段代码定义了两个 IIO 通道:一个是 RX 本振频率,一个是 RX 增益和采样率。info_mask_separate 决定了哪些属性可以通过 sysfs 读写。注册 IIO 设备后,内核会自动在 /sys/bus/iio/devices/ 下生成对应的目录和属性文件。
5.2 用 libiio 在用户空间读写 AD9361 参数
libiio 是 ADI 提供的用户空间库,封装了 IIO 设备的操作。用它来读写 AD9361 参数比直接操作 sysfs 方便得多,而且支持网络后端,可以在 PC 上远程控制板子上的 AD9361。
#include <iio.h> struct iio_context *ctx; struct iio_device *phy; struct iio_channel *rx_lo; ctx = iio_create_local_context(); phy = iio_context_find_device(ctx, "ad9361-phy"); rx_lo = iio_device_find_channel(phy, "altvoltage0", true); // 设置 RX 本振频率为 2.4GHz iio_channel_attr_write_longlong(rx_lo, "frequency", 2400000000LL); // 读取当前频率 long long freq; iio_channel_attr_read_longlong(rx_lo, "frequency", &freq); printf("RX LO frequency: %lld Hz\n", freq); iio_context_destroy(ctx);这段代码展示了用 libiio 设置和读取 RX 本振频率的完整流程。iio_create_local_context 创建本地上下文,iio_context_find_device 找到 ad9361-phy 设备,iio_device_find_channel 找到 altvoltage0 通道。频率单位是 Hz,用 longlong 类型避免 32 位溢出。
我一般会在板子上跑一个 iiod 服务,然后在 PC 上用 iio_info 和 iio_attr 工具远程调试。这样不用每次改参数都重新编译驱动,效率高很多。iio_attr 的命令行用法很简单,比如 iio_attr -d ad9361-phy altvoltage0 frequency 2400000000 就能设置频率。
5.3 用 IIO 触发缓冲实现高速数据采集
AD9361 的收发数据速率很高,用 sysfs 逐点读根本不现实。IIO 提供了缓冲和触发机制,可以把 ADC 数据批量读到用户空间。你需要配置一个触发源(比如 hrtimer 或者外部中断),然后使能 IIO 缓冲,用 read 系统调用从 /dev/iio:device0 读取数据。
// 配置 IIO 缓冲 echo 1 > /sys/bus/iio/devices/iio:device0/scan_elements/in_voltage0_en echo 1 > /sys/bus/iio/devices/iio:device0/scan_elements/in_voltage1_en echo 1000 > /sys/bus/iio/devices/iio:device0/buffer/length echo 1 > /sys/bus/iio/devices/iio:device0/buffer/enable // 用户空间读取数据 int fd = open("/dev/iio:device0", O_RDONLY); char buf[4096]; int ret = read(fd, buf, sizeof(buf));scan_elements 目录下控制哪些通道参与缓冲采集,buffer/length 设置缓冲区大小,buffer/enable 启动采集。用户空间用 read 从设备节点读数据,每次读到的是一批采样点。数据格式是 I/Q 交替的 16 位有符号数,解析的时候注意字节序。
注意:IIO 缓冲的采样率由触发频率决定,如果触发太快而用户空间读取太慢,缓冲区会溢出,数据会丢。我一般会把 buffer/length 设大一点,比如 4096 或 8192,给用户空间留足够的处理时间。
5.4 调试 IIO 接口时的验证方法
IIO 接口配好之后,怎么确认它真的在工作?我一般分三步验证。第一步用 iio_info 确认设备树和驱动都正常加载,能看到 ad9361-phy 设备和所有通道。第二步用 iio_attr 读写几个关键参数,比如本振频率、采样率、增益,确认读写都生效。第三步用 iio_readdev 或者自己写的小程序从缓冲区读数据,用 Python 画个频谱图,看信号是不是正常的。
如果 iio_info 看不到设备,先查 dmesg 里 IIO 相关的报错。如果参数读写不生效,检查通道的 info_mask 有没有正确设置。如果缓冲区读不到数据,检查触发源有没有使能,scan_elements 有没有打开。
我习惯在驱动里加一些 debugfs 节点,把 AD9361 的关键寄存器状态暴露出来,调试的时候直接 cat 一下就能看到 PLL 锁定状态、校准完成标志、温度值这些信息。这比每次用 SPI 读寄存器方便得多,也不用停掉正常的数据流。
希望帮到你。
本文还有配套的精品资源,点击获取