ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

AD9914 DDS配置全解析:从MCU到FPGA的移植实践

AD9914 DDS配置全解析:从MCU到FPGA的移植实践 简介基于亚德诺半导体 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.hADuC7026 外设地址和寄存器定义查 GPIO、SPI、电源管理位ADuC702x.sARM 启动代码初始化堆栈和向量表一般不修改掉电复位出问题再查ad9914test.c主程序AD9914 初始化与写寄存器函数核心阅读文件先看main()irq_arm.c中断入口处理定时器/UART 等工程有中断时用AD9914Test.uvprojKeil MDK 工程文件双击打开需要对应 MDK 版本AD9914Test.hex编译后的烧写文件用 Keil ULINK 或 J-Link 烧入 ADuC7026AD9914Test.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 GHzcalc_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 到 FPGAVerilog 驱动 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 2d0; localparam LOAD 2d1; localparam SHIFT 2d2; localparam DONE 2d3; 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 40d0; bit_cnt 6d0; clk_cnt 8d0; sclk 1b1; sdo 1b0; cs_n 1b1; io_update 1b0; busy 1b0; end else begin case (state) IDLE: begin if (start) begin busy 1b1; // {写位[0], 地址[6:0], 32位数据} shift_reg {1b0, reg_addr, reg_data}; bit_cnt 6d40; clk_cnt 8d0; cs_n 1b1; state LOAD; end end LOAD: begin cs_n 1b0; sclk 1b0; sdo shift_reg[39]; clk_cnt 8d0; state SHIFT; end SHIFT: begin if (clk_cnt (CLK_DIV / 2 - 1)) begin sclk 1b1; end else if (clk_cnt (CLK_DIV - 1)) begin sclk 1b0; sdo shift_reg[38]; shift_reg {shift_reg[38:0], 1b0}; bit_cnt bit_cnt - 1b1; clk_cnt 8d0; end else begin clk_cnt clk_cnt 1b1; end if (bit_cnt 6d0) begin sclk 1b1; cs_n 1b1; state DONE; end end DONE: begin io_update 1b1; // 保持一个时钟周期足够触发内部锁存 if (clk_cnt 8d3) begin io_update 1b0; busy 1b0; state IDLE; end else begin clk_cnt clk_cnt 1b1; 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 工程里常见做法是每次都发 FTWFPGA 里更高效的做法是先把 4 组频率预写到 profile 寄存器然后通过 GPIO 翻转PS引脚实现快速跳频。SPI 和并行端口的选择要结合目标场景接口信号数单次写延迟适合场景串行 SPI4 根40 个 SCLK约几 µs初始化、慢速配置、线缆连接16 位并行20 根以上2 个写周期Profile 预装载、突发写入并行 PS 切换并行配置 3 根 PSProfile 切换 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做频率计数确认倍频器稳定这时候频谱仪上的输出才会真正变成干净的载波。本文还有配套的精品资源点击获取
返回列表