简介:基于亚德诺半导体 AD9914 直接数字频率合成器,这份 FPGA 驱动工程示例面向通信、测试测量与雷达系统开发人员,适合需要快速实现高精度频率和相位控制的场景。该芯片支持 3.5 GSPS 采样率与 12 位 DAC,驱动实现需兼顾高速接口与精确寄存器配置。压缩包共 24 个文件,包含 C 驱动源码、Verilog 接口模块、Keil 工程配置文件以及编译器生成的 o、axf、hex、map 等结果文件,另有 lst、crf、dep、bak 等辅助文件,整体仅 59KB,便于定位所需模块。已有 445 人学习。通过其中的 AD9914Test 工程,可直观了解器件初始化、频率字写入、相位累加控制及 SPI/并行接口时序配合方法;工程同时保留了链接脚本与启动汇编文件,方便理解芯片上电配置流程。对于需要修改频率步进、输出波形或集成到复杂信号链的项目,这些基础代码能显著缩短开发周期,为自主移植到不同 FPGA 平台、调试高速 DDS 系统提供完整蓝本。
1. 文档里没有 Verilog,但 AD9914 的真正价值是明白 DDS 配置链路
打开 ExampleCode_DDS_AD9914.rar,第一眼看过去不是 FPGA 工程,而是 ADuC7026 的 Keil 工程文件:ad9914test.c、ADuC702x.s、AD9914Test.uvproj、.hex。这个现象在 ADI 的老一批 DDS 评估套件里很常见,参考代码用 ARM7 精密模拟微控制器写,驱动逻辑全部集中在 C 语言里,读起来反而比 Verilog 更直接。它解决的是 AD9914 这套 3.5 GSPS DDS 从初始化、频率调谐字写入到 Profile 切换的完整流程,适合做射频信号源、跳频通信、仪器仪表的人快速起步。真正要往 FPGA 上移植时,思路很清楚:把 C 里的 SPI 时序翻译成状态机,把寄存器地址和参数表原样搬过去。
2. 拆掉工程外壳:AD9914 的相位累加器与 ADuC7026 文件结构
AD9914 的名字容易让人误以为只要写几个寄存器就能出信号,实际上它的核心是一个 32 位相位累加器,配合 12 位、3.5 GSPS 的电流型 DAC,输出频率由频率调谐字(FTW)决定。输出频率和系统时钟的关系是:
Fout = FTW × Fsysclk / 2^32所以 3.5 GHz 系统时钟下,频率分辨率大约是 0.815 Hz。这个精度来自相位累加器的位数,不是外部 PLL 能直接做到的。工程里看到的 ADuC7026 是 ARM7TDMI 内核,自带 SPI、UART、12 位 ADC,用来给 AD9914 做配置通路刚好合适。
这个 rar 包里的文件不是无意义的编译产物。拿到手后先按表格分类,能省掉大量排查时间:
| 文件名 | 作用 | 调试时怎么用 |
|---|---|---|
ADuC7026.h | ADuC7026 外设地址和寄存器定义 | 查 GPIO、SPI、电源管理位 |
ADuC702x.s | ARM 启动代码,初始化堆栈和向量表 | 一般不修改,掉电复位出问题再查 |
ad9914test.c | 主程序,AD9914 初始化与写寄存器函数 | 核心阅读文件,先看main() |
irq_arm.c | 中断入口,处理定时器/UART 等 | 工程有中断时用 |
AD9914Test.uvproj | Keil MDK 工程文件 | 双击打开,需要对应 MDK 版本 |
AD9914Test.hex | 编译后的烧写文件 | 用 Keil ULINK 或 J-Link 烧入 ADuC7026 |
AD9914Test.axf | 带调试信息的镜像 | 硬仿真时加载,配合.map看符号 |
AD9914Test.map | 内存分配映射 | 如果程序跑飞,先看栈和堆是否溢出 |
AD9914Test.lnp | 链接器输入文件 | 构建脚本用,手工移植不用管 |
这些文件里真正能迁移到其他平台的是ad9914test.c和irq_arm.c。ADuC702x.s属于 ARM 平台启动代码,FPGA 方向用不上;.uvopt、.plg、.dep都是 Keil 的本地状态文件,不同电脑打开工程后会自动生成,不要提交到版本管理。
频率字计算那段代码,不同工程师写法差异很大。我一般会把浮点计算和寄存器写操作分开,便于在 FPGA 里用定点逻辑重新实现:
uint32_t calc_ftw(double fout, double fsys) { uint64_t ftw = (uint64_t)(fout / fsys * 4294967296.0); return (uint32_t)(ftw & 0xFFFFFFFFULL); }上面这段把目标频率和系统时钟转换成整数频率字,2^32就是相位累加器满寄存器对应的相位步进。注意fsys一定要用 AD9914 内部倍频器实际输出的 SYSCLK,而不是外部参考晶振频率。倍频配置在 CFR3 里,比如外部参考 100 MHz,内部倍频到 3.5 GHz,calc_ftw()的第二个参数必须传 3.5e9,不能传 100e6。否则算出来的 FTW 偏 35 倍,频谱仪上看到的频率和预期完全对不上。
还有一个容易踩的点:fout超过fsys / 2以后,奈奎斯特区间会出现镜像频率,工程里如果直接输出 3.2 GHz,表面上看 FTW 值还算得出来,实际模拟输出落在带外镜像区。做窄带系统时要把输出频率限制在 30% 到 40% 的 SYSCLK 范围内。AD9914 支持单边带混频输出,但那是专用模式,需要单独配置 CFR1 的混频器使能位,不能靠单纯改 FTW 实现。
3. 用 ADuC7026 驱动 AD9914:寄存器配置与串行写时序
前面讲了文件结构和 FTW 计算,这一章落到真正能跑的代码上。AD9914 支持并行和串行两种配置方式,评估板代码大多用串行 SPI,因为 ADuC7026 的 GPIO 引脚数量有限,而且串行配置频率不敏感,几百 kHz 到几 MHz 都能稳定工作。初始化顺序比代码本身更重要,顺序错了,后续写寄存器经常出现“读回来是全 0xFF”的现象。
我推荐的初始化顺序是:先确认 ADuC7026 的 PLL/时钟已经跑起来,然后拉低 AD9914 的RESET引脚至少 20 ns,释放后延时 1 ms;接着写 CFR1 开启串行编程模式,写 CFR2 配置参考时钟倍频器,写 CFR3 设置REFCLK分频和电荷泵电流,最后写 FTW 和 Profile 相关寄存器。每次写完寄存器后,必须给IO_UPDATE一个上升沿,数据才会从 SFR 锁存到内部工作寄存器。
寄存器地址可以做成宏定义,方便和 Verilog 里的参数表对应:
#define AD9914_REG_CFR1 0x00 #define AD9914_REG_CFR2 0x01 #define AD9914_REG_CFR3 0x02 #define AD9914_REG_CFR4 0x03 #define AD9914_REG_FTW0 0x04SPI 写函数的要点是:AD9914 的指令字是 8 位,最高位表示读写(0 写、1 读),后 7 位是寄存器地址,数据必须是 32 位大端模式。很多从 STM32 平台转过来的工程师会习惯先发地址、再发数据,这个顺序没问题,但数据字节序如果沿用 STM32 固件库的小端写法,寄存器值写入后行为会很怪。下面的 bit-bang 实现可以直接跑在 ADuC7026 的 GPIO 上:
void AD9914_SPI_Write(uint8_t regAddr, uint32_t val) { uint8_t i; CS_LOW(); // 指令字: 7 位地址左移 1 位,最低位置 0 表示写 for (i = 0; i < 8; i++) { SCK_LOW(); SDO(regAddr & 0x80); regAddr <<= 1; SCK_HIGH(); } // 数据长度 32 位,MSB 先行 for (i = 0; i < 32; i++) { SCK_LOW(); SDO((val & 0x80000000) != 0); val <<= 1; SCK_HIGH(); } CS_HIGH(); IO_UPDATE_PULSE(); }这段代码的CS_LOW()到CS_HIGH()就是一次完整的 SPI 事务。SCK拉低后改变SDO,再拉高SCK让 AD9914 采样,这种写法对时序要求不严格,方便单步调试;缺点是 CPU 全程占住。实际项目中可以把SCK改成硬件 SPI 时钟,但要注意 AD9914 默认接受 MSB 优先,硬件 SPI 如果配置成 LSB 优先,所有字节都会反序。
IO_UPDATE_PULSE()要真正做到窄脉冲,用GPIO_Clr和GPIO_Set两条语句连续执行,中间不要做判断或函数调用。有些 ADuC7026 代码在IO_UPDATE后接了一个 delay,结果每写一个寄存器就堵很久。实际上IO_UPDATE只需要一个 SYNC_CLK 周期宽度的脉冲,延时太长不会提升可靠性,反而影响跳频速度。
AD9914 的串行写入还有一个隐藏要求:指令字和数据字之间,SCK 必须连续,中间不能插入其他 GPIO 操作。如果在第一次循环结束和第二次循环开始之间调用delay_us(),有些样片会丢第一个数据位。丢弃现象不明显,因为往往是高位,频率字低一点,频谱仪上看只差几 Hz,但在需要精确锁相的场合会变成系统性误差。
写 CFR1 时最需要注意位 4 和位 5。AD9914 的并行数据端口和串行端口共用一部分引脚,CFR1 里如果没把控制方式切到串行,外部引脚电平会把寄存器内容覆盖成随机值。建议在初始化函数最前面先写一次 CFR1,等 5 ms 后再写其他寄存器。这个延迟不是给 AD9914 的,是给参考时钟倍频器稳定用的。有些 ADuC7026 上电后没等 REFCLK 稳定就写倍频比,导致输出频率漂移几百 kHz,且无法通过重写 FTW 修正,只能重新复位。
4. 从 MCU 到 FPGA:Verilog 驱动 AD9914 的状态机写法与 Profile 细节
ADuC7026 工程的 C 代码可以在 Keil 里运行,但很多人拿到这套代码是为了在 FPGA 里复刻。MCU 写寄存器是顺序执行,FPGA 里必须用状态机,否则你没办法在一条比特流里做到“发完 40 位后立刻拉 IO_UPDATE”。常见做法是把串行写模块拆成四级状态:等待触发、装载指令和数据、按位输出、结束更新。
先看接口定义。FPGA 作为 SPI Master 连接 AD9914 时,最少需要SCLK、SDIO、CSB、IO_UPDATE四根线,SYNC_CLK可以不接,但建议引回来做相位参考。下面是可综合的 Verilog 骨架,只写寄存器,不处理读回:
module ad9914_spi_wr #( parameter CLK_DIV = 8 ) ( input wire clk, input wire rst_n, input wire start, input wire [6:0] reg_addr, input wire [31:0] reg_data, output reg sclk, output reg sdo, output reg cs_n, output reg io_update, output reg busy ); localparam IDLE = 2'd0; localparam LOAD = 2'd1; localparam SHIFT = 2'd2; localparam DONE = 2'd3; reg [1:0] state; reg [39:0] shift_reg; reg [5:0] bit_cnt; reg [7:0] clk_cnt; reg sclk_level; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= IDLE; shift_reg <= 40'd0; bit_cnt <= 6'd0; clk_cnt <= 8'd0; sclk <= 1'b1; sdo <= 1'b0; cs_n <= 1'b1; io_update <= 1'b0; busy <= 1'b0; end else begin case (state) IDLE: begin if (start) begin busy <= 1'b1; // {写位[0], 地址[6:0], 32位数据} shift_reg <= {1'b0, reg_addr, reg_data}; bit_cnt <= 6'd40; clk_cnt <= 8'd0; cs_n <= 1'b1; state <= LOAD; end end LOAD: begin cs_n <= 1'b0; sclk <= 1'b0; sdo <= shift_reg[39]; clk_cnt <= 8'd0; state <= SHIFT; end SHIFT: begin if (clk_cnt == (CLK_DIV / 2 - 1)) begin sclk <= 1'b1; end else if (clk_cnt == (CLK_DIV - 1)) begin sclk <= 1'b0; sdo <= shift_reg[38]; shift_reg <= {shift_reg[38:0], 1'b0}; bit_cnt <= bit_cnt - 1'b1; clk_cnt <= 8'd0; end else begin clk_cnt <= clk_cnt + 1'b1; end if (bit_cnt == 6'd0) begin sclk <= 1'b1; cs_n <= 1'b1; state <= DONE; end end DONE: begin io_update <= 1'b1; // 保持一个时钟周期足够触发内部锁存 if (clk_cnt == 8'd3) begin io_update <= 1'b0; busy <= 1'b0; state <= IDLE; end else begin clk_cnt <= clk_cnt + 1'b1; end end endcase end end endmodule这段代码里CLK_DIV决定SCLK周期,默认 8 表示系统时钟 8 分频,写 40 位需要 320 个系统时钟周期。bit_cnt从 40 递减到 0,每减一次就移出一位。shift_reg的装载顺序也做了处理:最低位在sdo上先给reg_addr[0],然后数据从reg_data[31]往下走。模块不处理连续写多个寄存器的排队,顶层如果需要写 CFR1、CFR2、FTW 三个寄存器,就在每次busy拉低后重新拉高start。
FPGA 方案里,很多人忽略 Profile 引脚的用法,导致浪费了 AD9914 最重要的跳频能力。AD9914 有 4 个 profile,每个 profile 都能独立保存一组频率字和相位信息,外部PS0、PS1、PS2引脚的电平组合决定当前输出用哪个 profile。MCU 工程里常见做法是每次都发 FTW,FPGA 里更高效的做法是先把 4 组频率预写到 profile 寄存器,然后通过 GPIO 翻转PS引脚实现快速跳频。
SPI 和并行端口的选择要结合目标场景:
| 接口 | 信号数 | 单次写延迟 | 适合场景 |
|---|---|---|---|
| 串行 SPI | 4 根 | 40 个 SCLK,约几 µs | 初始化、慢速配置、线缆连接 |
| 16 位并行 | 20 根以上 | 2 个写周期 | Profile 预装载、突发写入 |
| 并行 + PS 切换 | 并行配置 + 3 根 PS | Profile 切换 < 10 ns | 跳频、抗干扰通信 |
如果只需要初始化时配置,SPI 完全够用;如果要做跳频图案,必须用并行端口预装载 profile,再在时序紧凑时去切PS。这里建议把PS0到PS2接 FPGA 的 3 个独立寄存器位,不要用组合逻辑直接接到地址总线,否则多路信号同时变化时会毛刺触发错误的 profile。
同步是另一个在 FPGA 和 AD9914 之间绕不开的问题。AD9914 的SYNC_CLK输出是它内部 DATACLK 的参考,FPGA 侧读回数据时要用这个时钟采样。很多工程师图省事用系统时钟直接采DR数据线,结果数据在速率边缘时跳动。正确的做法是把SYNC_CLK引入 FPGA,经过 BUFG 后作为数据采样的使能时钟,同时用代码约束保证SYNC_CLK到DR的延迟一致。这个在纯 MCU 环境不敏感,因为 MCU 顺序执行天然慢了,但 FPGA 工作在 GSPS 数据率时,CMOS I/O 口的建立时间余量很小。
工程里如果保留.uvproj和.dep,可以清楚看到 ADI 当初用的外设初始化顺序。FPGA 移植时不要直接抄irq_arm.c的中断转发逻辑,那部分和 ARM 异常向量强相关。只需要把ad9914test.c里向寄存器写入的常量值整理成参数列表,例如0x00000001是开串行接口,0x00004000是倍频器设置,这些常量才是真正跨平台可复用的资产。
5. 上电验证与跳频波形的一个实用技巧
硬件焊好后,先别急着看频谱,用示波器测 ADuC7026 GPIO 输出的SCLK和SDIO,确认写时序的脉冲数。连续抓 5 次写操作,如果每次CS低电平区间都是固定长度,说明状态机没有丢位。接着把IO_UPDATE接示波器另一通道,观察它和CS上升沿之间的时间差,不能出现CS刚拉高、IO_UPDATE立刻拉高的情况,因为 AD9914 需要至少一个SYNC_CLK周期来完成内部数据传输。
验证频率一般用 FFT 的方式:设置FTW = 0x20000000,对应Fsysclk / 8,如果参考时钟精确等于 3.5 GHz,输出应该在 437.5 MHz。此时看主频旁边是否有一个约 815 Hz 的凸起,那是 FTW 量化间隔,不是杂散,所有 DDS 都会有。真正要注意的是 2 倍频处的寄生,那是 DAC 非线性产生的,和寄存器配置无关。
最后一个技巧,做跳频时不要反复重写 FTW。假设要输出 100 MHz 和 200 MHz 交替,初始化阶段分别把两个频率字写入 Profile 0 和 Profile 1,然后只切换PS0。切换完保持 20 ns,再做一次IO_UPDATE,跳频建立时间可以压到几十纳秒。如果一开始就把所有频率字写进同一个 FTW 寄存器,每次跳频需要 40 位的 SPI 传输,时间全部耗在移位时钟上。
调试时建议先在 ADuC7026 工程里把ad9914test.c的寄存器初始化函数改成循环打印返回状态,确认 AD9914 能从串行口读回寄存器。如果回读的值全是0xFF,大概率是SDIO方向配置问题,或者CS极性设反;如果回读值全是0x00,检查RESET引脚是否被 ADuC7026 的某个 GPIO 一直拉低。把这两个方向性问题排除后,再对SYSCLK做频率计数,确认倍频器稳定,这时候频谱仪上的输出才会真正变成干净的载波。
本文还有配套的精品资源,点击获取