去年做一个小型无人机的数据链项目,甲方现场只提供了一台Windows工控机,而手上这块AD9361评估板平时都是在Linux主机上编译、调试的。被逼无奈之下,我花了两天时间把ADI的no-os master工程完整搬到了Windows环境下,从交叉编译工具链到JTAG烧录,把整条路走通了一遍。这篇文章就是这次迁移的完整记录,包括工具选型、工程结构拆解、编译过程和排坑经验,给同样只能在Windows下开发AD9361的朋友一份能直接照着做的参考。
如果你是刚拿到FMCOMMS3-EBZ这类评估板,想快速把AD9361跑起来做数据收发实验,又不打算为了编译一个工程专门装一台Linux机器,那这篇文章应该能帮你省下大量试错时间。
1. AD9361 no-os master工程到底是个什么工程
1.1 AD9361芯片与no-os软件的搭配逻辑
AD9361是ADI推出的一款高性能射频收发器,覆盖70MHz到6GHz频段,支持200kHz到56MHz的信号带宽,内部集成了完整的零中频收发链路。这颗芯片在软件无线电、点对点通信、无人机数据链、雷达模拟器这些领域用得非常多。芯片本身的模拟性能只是一半,另一半在于它内部的数字接口和配置机制,这些都要靠软件来驱动。
ADI针对AD9361提供了几条软件路线:一是Linux内核驱动程序,适合跑嵌入式Linux的系统;二是本文要讲的no-os裸机驱动;三是libiio用户空间库,配合Linux或跨平台使用。no-os这个名字就是字面意思,不依赖操作系统跑,直接在硬件上操作外设,通过SPI接口配置AD9361的寄存器,通过并口或LVDS接口传输采样数据。它的优势在于启动快、没有操作系统开销,适合对实时性有要求、又想完全控制硬件行为的场景。
1.2 master工程的版本包袱
这里要先说明一个容易让人困惑的点:ADI的no-OS仓库其实迭代过好几轮目录结构。早些年,仓库根目录下会有一个projects/ad9361这样的工程目录,再配合drivers/文件夹下的驱动源码,整体被称为master工程。后来ADI做了工程重构,目录改成projects/ad9361,并引入了更灵活的构建变量,但很多老工程师和教程里仍然习惯叫它master工程。
所以你现在去GitHub上拉代码,看到的仓库结构和老教程可能不一致。这种版本差异是新手第一个坑:拿一份旧教程去套新代码,或者拿新脚本去跑老版本,都会遇到"文件找不到""Makefile规则不存在"这类问题。我在后面的章节里会具体讲如何选择版本、如何对应教程。
1.3 no-os工程不是只靠软件就能跑的
需要特别强调一点:AD9361的no-os工程运行的前提,是FPGA里已经烧录了配套的HDL比特流。AD9361和处理器之间的数据接口(通常是LVDS或CMOS并行数据线)需要FPGA做桥接,处理器内部的SPI控制器也往往是通过FPGA逻辑映射到AD9361的SPI引脚。ADI提供了完整的HDL参考设计,对应不同的开发板,比如ZC702、ZC706配合FMCOMMS2/3/4/5评估板都有现成的比特流。
软件工程师容易忽视的方向,就是"我的程序没跑起来,首先应该怀疑FPGA里的逻辑对不对"。这一点在Windows下开发特别明显,因为Vivado和SDK经常是分开安装的,有人甚至只装了SDK没装Vivado,然后发现没法生成比特流。记住这条依赖链:
FPGA比特流 + no-os裸机代码 + 正确的硬件连接 = 能跑起来的AD9361系统。
这三者缺一不可,任何一环不对都会导致程序"看起来编译通过、跑起来没反应"。
2. Windows下环境准备:工具链和终端的正确打开方式
2.1 方案选型:MSYS2、WSL和虚拟机该怎么选
开始之前先解决一个方向性问题:在Windows下构建no-os工程,到底用什么运行环境?我试过几种方案,各有各的坑。
| 方案 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|
| MSYS2/AUR原生终端 | 轻量、和Windows文件系统交互方便、包管理简单 | 有些路径转换要手动处理 | 日常编译no-os、修改驱动代码 |
| WSL(Windows Subsystem for Linux) | 接近真实Linux环境、交叉编译工具链好装 | 访问Windows文件系统路径麻烦、USB/JTAG透传需要额外配置 | 需要经常执行Linux专用脚本的场景 |
| 虚拟机跑Ubuntu | 最接近官方开发环境 | 资源开销大、启动慢、共享文件夹偶发兼容问题 | 需要完整Linux开发环境且不介意重启切换 |
我最终选择的是MSYS2。原因很直接:no-os工程本质上就是一套Makefile加C代码,只需要一个符合POSIX风格的环境来执行make脚本。MSYS2提供了bash、make、git、perl这些工具,而且和Windows文件系统交互非常自然,编译生成的elf文件可以直接被Windows下的XSCT工具读取烧录,不需要做虚拟机网络映射。如果你的开发流程还涉及到Linux内核驱动移植、或者经常要跑Python脚本,那WSL可能更合适。但对于纯no-os裸机工程,MSYS2的轻量优势非常明显。
2.2 ARM交叉编译工具链安装与验证
no-os工程的运行目标通常是Zynq-7000系列SoC或者ADI自家的微控制器平台,我这次用的是Zynq平台,CPU是ARM Cortex-A9架构,所以要用ARM的裸机GCC工具链。
建议去Arm官方站点下载gcc-arm-none-eabi-10.3-2021.10-win32.exe这个安装包。安装的时候有个细节要注意:默认路径是C:\Program Files (x86)\Arm GNU Toolchain arm-none-eabi\10.3 2021.10,路径里带着空格和括号,这会给后续Makefile调用编译器带来麻烦。我的做法是改安装路径为C:\arm-gcc,直接把版本优先级降低,图个清静。
装完以后需要把编译器bin目录加入系统PATH环境变量。验证方式:
arm-none-eabi-gcc --version如果能看到类似arm-none-eabi-gcc (GNU Arm Embedded Toolchain 10.3-2021.10)的输出,说明编译器已经可用。
注意:这里装的是ARM裸机GCC,和Windows本地GCC完全不是一回事。不要试图用你装了Python或者搞Web开发时装的MinGW GCC来编译ARM代码,它们是给不同目标平台用的。
2.3 获取no-OS源码与版本选择
ADI的no-OS源码托管在GitHub上,仓库名就是analogdevicesinc/no-OS。直接用git拉代码:
git clone https://github.com/analogdevicesinc/no-OS.git cd no-OS git tag -l拉下来以后建议先看一下有哪些版本标签。我的经验是不要直接拿最新main分支,因为有些新提交还没来得及补充完整文档,可能存在已知问题。建议选择稳定的发布版本,比如2021_R2或者2022_R2,这两个版本对AD9361的支持都很完整,结构也相对清晰。
这里有一个关键对应关系:no-OS的版本需要跟HDL的版本尽量对齐。ADI的release note里会标注某个no-OS版本配套哪个HDL版本,如果软件和硬件版本差距太大,有可能出现寄存器配置不一致、数据接口时序对不上的问题。我这次用的组合是no-OS 2021_R2搭配HDL 2021_r2。
2.4 辅助工具的安装:make、perl和sh
在MSYS2环境下安装辅助工具非常方便,打开MSYS2终端执行:
pacman -Syu pacman -S make git perl diffutils findutils这里有个容易踩的坑:MSYS2默认会安装一个make,但它的实际命令可能是make而不是mingw32-make。某些环境下如果同时安装了MinGW的make,会出现版本冲突。我的建议是只用MSYS2自带的make,不要额外安装或改变PATH顺序,否则后面执行make时会报一些莫名其妙的老毛病。
验证make是否正常:
make --version如果看到GNU Make 4.3这类版本号,说明环境基本可用。
另外,MSYS2终端和Windows命令提示符的路径表示不一样。MSYS2里C:\my_project写作/c/my_project,这个细节虽然小,但是在导入工程路径时需要特别注意。你可以在MSYS2终端里直接cd到Windows路径对应的目录,也可以把源码直接放在C:\msys64\home\你的用户名\下面,这样访问起来最顺。
3. no-os master工程结构拆解与关键文件修改
3.1 目录结构总览
以2021_R2版本的no-OS仓库为例,拉下来以后主要的目录结构如下:
no-OS/ ├── drivers/ │ ├── ad9361/ │ │ ├── ad9361.c │ │ ├── ad9361_api.c │ │ ├── ad9361_api.h │ │ ├── ad9361.h │ │ └── ad9361_conv.c │ ├── axi_ad9361/ │ └── ... ├── platforms/ │ ├── xilinx/ │ │ ├── xilinx_spi.c │ │ ├── xilinx_gpio.c │ │ ├── xilinx_timer.c │ │ ├── xilinx_platform.c │ │ └── ... ├── projects/ │ └── ad9361/ │ ├── common/ │ ├── zc706_fmcomms3/ │ │ ├── Makefile │ │ ├── main.c │ │ └── ... ├── include/ └── Makefiledrivers/ad9361下是芯片驱动本体,platforms/xilinx是平台适配层,projects/ad9361下面按不同开发板细分。以ZC706+FMCOMMS3为例,工程目录是projects/ad9361/zc706_fmcomms3。
3.2 平台适配层的核心价值
很多初学者打开工程后一头扎进ad9361.c,看AVT配置和收发寄存器,但其实真正决定工程能否运行的是platforms/目录下的那几组函数。no-os对平台的抽象非常简洁,platform.h里定义了SPI读写、GPIO控制、定时器这些基础接口。Xilinx平台的实现就在xilinx_spi.c、xilinx_gpio.c这些文件里面。
理解平台层的关键在于:AD9361的配置是通过SPI接口发送寄存器读写命令来实现的,而SPI的底层实现完全由平台决定。在Zynq平台,SPI控制器可能直接映射到ARM的SPI外设,也可能通过AXI接口连接FPGA内部的SPI IP核。不同板子对应不同的寄存器地址,这些地址在平台初始化代码里写死。如果你换了开发板或者在自定义板卡上跑,最需要改的就是这一层。
调试过程中如果发现写寄存器没反应、回读全是0xFF,八成是平台层的SPI基地址配置不对,而不一定是芯片的问题。
3.3 驱动初始化流程和关键结构体
main.c里做的事情通常很简单:初始化平台、初始化DDR、调用ad9361_init。
ad9361_init是整颗芯片的配置入口,它接收一个ad9361_init_param结构体,里面包含了参考时钟频率、RF端口配置、采样率、带宽、滤波器配置等一大堆参数。部分关键参数:
| 参数 | 含义 | 典型值 |
|---|---|---|
| reference_clk_rate | 参考时钟频率 | 40MHz(取决于板载晶振) |
| rx_gain | 接收增益模式 | 0=快速攻击AGC,1=手动 |
| tx_attenuation_md | 发射衰减控制模式 | 0=手动,1=自动 |
| freq | 射频中心频率 | 2400000000(2.4GHz) |
| rx_bandwidth / tx_bandwidth | 基带带宽 | 20000000(20MHz) |
| tx_fir_enable | TX FIR滤波器使能 | 1 |
这些参数定义在ad9361.h的ad9361_init_param结构体里。初次上手时,建议先照着数据手册和参考设计把参考时钟和频率带宽设对,其他高级参数先维持默认,跑通了再逐步优化。
3.4 针对你自己的板卡需要改什么
如果你用的不是ADI官方评估板,而是自己画的板子,那主要改动集中在三个地方:
第一,参考时钟频率。AD9361需要一个外部参考时钟(通常是40MHz或38.4MHz晶振),这个值必须和电路板实际使用的晶振一致,否则PLL锁不住,频率配置全部会失败。
第二,SPI读写时序参数。在芯片手册的"SPI Interface"章节,会明确支持CPOL/CPHA的两种组合模式。no-os的驱动里默认配置可能只要一种,如果你的硬件设计导致SPI模式和默认不一致,需要在平台层调整。
第三,GPIO映射。AD9361的使能引脚、复位引脚、TX/RX切换相关的GPIO编号在平台代码里定义,不同板卡的连接可能完全不同。最典型的就是复位引脚的GPIO号,一些评估板上连到了FPGA的某个用户GPIO,有的板子连到了ARM处理器的MIO,改错了会导致芯片一直处于复位状态,代码跑半天毫无反应。
4. Windows下编译的完整过程与实测排坑记录
4.1 编译命令详解
在MSYS2终端里进入工程目录,执行:
make PLATFORM=zync CROSS_COMPILE=arm-none-eabi- all注意这里的PLATFORM=zync是我这台机器的实际值,具体值要到projects/ad9361/zc706_fmcomms3/Makefile里去确认。不同的硬件平台对应不同的PLATFORM取值,常见的有xilinx、altera、versal等。老版本统一的PLATFORM是xilinx,新版可能细化到了zync或versal。如果发现PLATFORM名称不对,编译会在平台文件搜索阶段就报错。
编译过程会先编译drivers和platforms下的各个模块,最后链接出ELF文件。第一次编译时间可能比较长,之后增量编译就快很多。
4.2 编译报错对照表:我最常遇到的几个问题
这里把我在Windows环境下实际踩过、且具有代表性的编译错误整理成表:
| 错误现象 | 根本原因 | 解决办法 |
|---|---|---|
arm-none-eabi-gcc: command not found | 编译器路径没在PATH中或安装路径含空格 | 把C:\arm-gcc\bin加入PATH,重新打开MSYS2终端 |
make: command not found | MSYS2里make未安装 | pacman -S make |
fatal error: stdint.h: No such file or directory | GCC的sysroot路径被环境变量干扰 | 检查是否设置了错误的CPATH或C_INCLUDE_PATH,清掉后重试 |
unknown type name 'uint32_t' | 头文件搜索路径缺include目录 | 检查Makefile的CFLAGS里是否有-Iinclude或者对应心跳的-I路径 |
/c/Program Files (x86)/...: No such file or directory | 路径里有空格导致Makefile解析异常 | 将工具链安装在无空格路径,如C:\arm-gcc |
line endings are not consistent或编译器诡异报错 | Windows换行符CRLF和Linux换行符LF混用 | 执行git config core.autocrlf false重新拉取代码,或用dos2unix批量转换 |
undefined reference to 'main' | 链接时找不到入口函数 | 检查是否把main.c排除在编译列表外,或链接脚本入口设置错误 |
其中CRLF的问题是Windows下最具迷惑性的坑。no-OS源码仓库里文件默认是LF换行,但如果你用某些Windows文本编辑器动过源文件,它可能自动转成CRLF,后续git再拉代码时就会混行。GCC对LF换行的要求很严格,一旦源码里混了CRLF,编译器报错位置往往和实际错误风马牛不相及。我最严重的一次是报错指向ad9361.c第300行,实际问题是另一个文件被改了换行符。排查方法很简单:
file drivers/ad9361/ad9361.c如果输出里带with CRLF line terminators,就说明需要转回LF:
sed -i 's/\r$//' drivers/ad9361/ad9361.c4.3 生成产物和确认编译成功的标准
编译成功后,会在工程目录下生成build子目录,里面是各个模块的目标文件,以及最终的ELF文件。以zc706_fmcomms3为例,生成的可执行文件通常叫ad9361.elf。
确认编译成功的标准不只是"没有error",还要确认生成了完整ELF:
ls -l build/ad9361.elf用编译器自带的工具查看ELF的CPU架构和入口信息:
arm-none-eabi-readelf -h build/ad9361.elf在ELF头信息里,Machine字段应当显示ARM,Entry point address应当是链接脚本指定的入口地址。如果入口地址是0x00000000而链接脚本里定义的DDR地址不在那里,说明链接脚本加载位置可能不对,启动阶段会有问题。
5. 下载、运行与第一轮验证
5.1 烧录路径:JTAG直连最稳妥
Zynq平台的启动方式很灵活,可以从SD卡启动、从QSPI Flash启动,也可以通过JTAG把程序直接加载到DDR里运行。对于调试阶段,我强烈建议用JTAG直连,省去格式化SD卡、做BOOT.BIN的流程,改代码重编译直接加载,调试周期短。
Windows环境下常用的两个工具是Xilinx Vitis/SDK自带的XSCT命令行工具,和OpenOCD。我这边用的XSCT,因为它对Zynq的原生支持最好,不需要额外写板级配置。
XSCT工具在Vitis安装目录下,比如C:\Xilinx\Vitis\2021.2\bin\xsct.bat。启动之前需要确保USB-JTAG驱动已经安装好,通常Vitis安装的时候会提示安装Cable Drivers。
在XSCT终端里,连接目标板并加载程序:
connect targets -set -filter {name =~ "*A9*"} rst -system after 500 dow build/ad9361.elf condow是下载到DDR的命令,con是继续运行。如果一切正常,程序会从ad9361的初始化函数开始执行。
5.2 上电后第一件事:观察串口和电平状态
程序跑起来以后,首先要确认的是CPU是否在正确执行。最简单的方式是串口打印,main.c里初始化完之后一般都有printf输出,或者至少会配置一个UART打印初始化信息。如果串口终端上什么都看不到,先别急着调AD9361,要确认是不是程序根本没跑到main。
确认方式:在main函数开头加一个GPIO翻转或者点亮LED的操作。我就遇到过编译下载都正常,但程序根本没执行,原因是DDR没有完成初始化,CPU一跳到DDR地址就死了。Zynq平台跑外部程序之前,必须要先运行FSBL或者用XSCT的dow命令下载并运行FSBL来初始化DDR。直接下载裸机ELF到DDR地址,会因为没有初始化DDR控制器而崩溃。正确顺序是:
connect targets -set -filter {name =~ "*A9*"} rst -system dow -data ../fsbl.elf con # 等FSBL跑完,初始化DDR之后 rst -processor dow build/ad9361.elf con或者更简单的方法:在XSCT里用dow -range 0x00100000这种命令把程序放到OCM(片上内存)里跑,OCM不需要DDR初始化,但空间有限。调试阶段的替代方式就是编译时把链接脚本的加载地址改到OCM里。
5.3 寄存器回读验证:比频谱仪更快的确认方式
如果程序已经跑过ad9361_init,此时通过调试器或者再编写一段回读代码,读取AD9361的寄存器,是验证芯片与CPU通信是否正常的最快路径。AD9361有一个SPI配置寄存器,地址是0x00,芯片的版本信息可以从这个寄存器读出来。以AD9361为例,0x00寄存器中的低6位是REVISION ID,常见的版本包括0x82表示Rev2等。
如果你用的是no-os提供的API,直接在main.c里加一段:
uint8_t reg_val; ad9361_spi_read(&ad9361_phy, 0x00, ®_val); printf("AD9361 REG0x00 = 0x%02X\r\n", reg_val);能够读到非0xFF的值,基本可以断定SPI链路、芯片上电和复位状态都是正常的。这一步比拿频谱仪看信号快得多,建议每次调整硬件连接后都先做寄存器回读。
继续往后验证,可以调用API配置一个小信号输出,然后用频谱仪观察。典型配置是设单音:
ad9361_set_tx_lo_freq(&ad9361_phy, 2400000000); ad9361_set_tx_attenuation(&ad9361_phy, 10000);实际测试中,设完频率后频谱仪中心频率调到2.4GHz,应该能看到一个明显的单音信号。把这个简单流程跑通,说明从SPI配置到射频链路都已经可以工作,后面再做调制解调实验就有了基础。
6. 从"能编译"到"能开发":几个过来人的忠告
6.1 版本对齐:HDL、no-OS和Vitis的配套关系
开发AD9361最忌讳的就是"版本随便搭"。ADI每一代release都做了严格的配套验证,比如no-OS2021_R2对应HDL2021_r2,Vitis版本建议用2021.2。这个配套关系在ADI官方Wiki的Release Notes页面写得很清楚,开始项目前务必花十分钟对一遍。
我见过一个案例,有人拿着no-OS最新代码配合Vivado 2019.2生成的比特流,结果HDL里的AXI寄存器映射变了,no-OS程序读写的SPI地址对不上,AD9361根本配置不进去,排查了好几天。根源就是软件和硬件设计版本不匹配。Windows开发环境下这个问题更容易发生,因为各种版本的工具链装得多,同一个工程被拿到不同版本的Vitis里编译,链接脚本可能都会变。
6.2 调试工具的补充建议
纯Windows环境下调试裸机程序,在线的调试器终端是主要手段,但我推荐再配合两个工具:
一是AD936x Filter Design软件,ADI官方推出的滤波器设计工具。no-os驱动内部有FIR滤波器配置,你可以用这个软件根据你的采样率和带宽要求生成滤波器系数,然后填入驱动代码里。这个工具是GUI界面,Windows下直接跑,极大降低了配置RF前端的门槛。
二是逻辑分析仪或者示波器,调试SPI时序时看波形。Windows下配合国产的逻辑分析仪很便宜,几百块就能买到,看到SPI片选、时钟和数据线的实际波形,比起靠猜要高效得多。
6.3 关于Windows下开发的心态和习惯
最后聊点实际的。很多嵌入式工程师天然抗拒Windows下开发,觉得"正规做法"就该用Linux。但这两年我越来越觉得,工具只是工具,能解决问题的就是好工具。在Windows下用MSYS2做编译,用XSCT做下载调试,用ADI的Windows工具做滤波器设计,反而在某些场景下比Linux更顺。关键是养成几个习惯:
- 所有工程文件放在一个不包含空格和中文的短路径下,比如
C:\work\no-OS - 源码统一用LF换行,用git管理时把
core.autocrlf设为false - 每次换电脑或者换网络环境,先花十分钟检查PATH顺序和工具链版本
- 编译报错先看是不是环境问题(工具链路径、换行符、路径含空格),再怀疑代码
我当时踩完这些坑以后,把整套流程写成了内部文档,后来团队里其他同事在Windows上也都能半小时内把AD9361跑起来了。这套流程如果你照着走一遍,第一次可能还是要花点时间,但走通之后,Windows环境下开发射频前端这件事,真的没有想象中那么难。现在想起来,当初被迫在Windows下搭这套工程,反倒逼着我养成了检查版本、记录日志的习惯,后面在做其他平台移植时省了不少力气。