简介:AD9914与HMC835分别是高性能DDS和锁相环芯片,广泛用于射频信号源、频率合成与通信测试设备。该压缩包提供基于FPGA的Verilog驱动调试程序,面向FPGA开发者和射频调试工程师,完整覆盖AD9914的扫频、定频、调频等模式,并包含HMC835的SPI配置逻辑,寄存器值按注释修改即可适配不同输出频率;全部源码开放,代码风格简明清晰,适合直接学习与二次开发。资源共77个文件,压缩包仅1.31MB,其中.v源代码实现SPI时序与模式控制,.bsf/.bdf提供图形化模块,.qpf/.qsf搭建Quartus工程框架,.jic/.sof用于下载配置,.rpt/.summary为编译报告,README说明操作流程,另有时钟分频、LVDS接口等辅助模块,目录结构便于在FPGA环境中快速恢复工程并上板验证。内容预览中可见定频与扫频对应的配置模块,方便对照两种工作模式。目前已有898人学习,对于正在做DDS/PLL控制、SPI通信或射频信号源项目的读者,能有效缩短驱动编写与调试周期。
1. 这个资源包是干什么的:一块FPGA同时驱DDS和PLL的完整工程
在实验室调一套宽带接收机前端时,手头最缺的就是一台能快速扫频的信号源。通用仪器太贵,于是某开发者把这块基于FPGA控制AD9914与HMC835的工程打包成了资源:一块FPGA同时通过SPI驱动DDS和PLL,既能做1.4 GHz以内的直接扫频,也能把HMC835当作可编程本振,把扫频范围往C波段推。做射频测试工装、雷达信号模拟、频谱监测前端的工程师拿到这份资源,最直接的收益是SPI时序已经调通、寄存器初始化顺序已经排好,不用再从数据手册的字缝里抠配置序列。资源里围绕AD9914和HMC835展开的FPGA工程、寄存器配置与扫频逻辑,正好补上“从点频输出到快速扫频”这一整条链路。
2. 架构设计:AD9914 + HMC835 + FPGA的板级与总线分工
2.1 系统级联与信号流向
这块板子的核心关系可以简化为一条主线:FPGA作为SPI主机,通过两个片选分别控制AD9914和HMC835。AD9914是一颗3.5 GSPS的DDS,内部自带12位DAC,输出范围从直流到1.4 GHz左右;HMC835则是一颗集成VCO的小数N分频锁相环,RF输出覆盖几十MHz到6.8 GHz量级。两者分工明确:需要快速扫频时用AD9914,需要稳定点频本振时用HMC835。
两条信号链可以独立工作,也可以级联。独立模式下,AD9914直接输出扫频中频,HMC835输出本振,两者在外部混频器里合路,得到变频后的宽带信号。级联模式下,HMC835锁定后的输出可以作为AD9914的高质量系统时钟,把DDS的参考从普通晶振提升到GHz级,这样AD9914既能输出更高频率,又能改善近载波相噪。工程里两种接法都预留了配置入口,改动的地方主要在时钟树,不在SPI层。
2.2 频率规划与指标边界
用这套方案之前,先要把两颗芯片的频率边界划清楚,否则后面改参数容易迷茫。
| 参数 | AD9914 | HMC835 |
|---|---|---|
| 输出频率 | 最高约1.4 GHz(3.5 GSPS) | 约25 MHz–6.8 GHz |
| 频率控制方式 | 32位频率调谐字(FTW) | 小数分频比,寄存器配置 |
| 扫频方式 | 数字斜坡 / RAM / profile切换 | 逐点改写分频比,重锁定 |
| 接口 | SPI,三线或四线 | SPI |
| 典型用途 | 快速扫频源、调制源 | 宽带本振、点频源 |
AD9914最值得利用的特性是内部数字斜坡调制器,它能在不占用FPGA资源的情况下自动完成线性扫频。HMC835则适合做“慢速扫频”或“步进点频”:每次写入新的分频比后,片内VCO要重新校准和锁定,所以切换速度远不如DDS。常见做法是让AD9914负责几十MHz到1 GHz的快速扫描,HMC835负责把本振固定在某个频点做上变频,两头各干各的。
频率字计算也很直接:AD9914的频率调谐字为round(f_out / f_sysclk × 2^32),例如3.5 GSPS系统时钟输出700 MHz,FTW约等于0x33333333;HMC835则是根据鉴相频率和VCO输出频率反推分频比,整数部分和小数部分分别写入对应寄存器。
2.3 SPI总线拓扑与片选策略
两颗芯片挂在同一条SPI总线上,FPGA做主,片选分开。连接方式如下:
FPGA(MOSI/MISO/SCLK) ──> AD9914(SDI/SDO/SCLK, CS0) └──> HMC835(SDI/SDO/SCLK, CS1)AD9914和HMC835都支持标准SPI,但细节上有差异。AD9914的串行端口兼容三线制,可以把SDO省掉;HMC835的寄存器帧格式与AD9914不同,地址和数据位宽都不一样。为了保证复用性,工程里建议统一走四线SPI,MOSI和MISO分开,SCLK空闲低电平,数据在上升沿采样,也就是SPI模式0。SCLK频率控制在1 MHz左右比较稳,AD9914虽然手册上标称可以更快,但实际布线稍长就容易翻车,1 MHz对两芯片都安全。
上电顺序也是一个容易忽视的点。常见做法是先配置HMC835,让它在后台完成VCO校准,再去初始化AD9914。如果反过来,AD9914先输出,而此时HMC835还没锁定,混频链路上会出现一段无本振的乱信号,容易误导调试判断。
3. FPGA控制逻辑拆解:SPI主机的状态机与寄存器写入
3.1 SPI主控制器:从移位寄存器到状态机
AD9914和HMC835虽然寄存器格式不同,但底层都只需要一个能完成单字节传输的SPI主机。这里给一个可复用的Verilog实现,模式0,单字节传输,带done脉冲,方便上层模块按字节串起多字节配置序列。
// spi_master_byte.v : 单字节SPI主机,模式0(CPOL=0, CPHA=0) // SCLK = 系统时钟 / (2 * CLK_DIV) module spi_master_byte #( parameter integer CLK_DIV = 50 // 100MHz系统时钟 -> 1MHz SCLK )( input wire clk, input wire rst_n, input wire start, // 单周期高脉冲,启动一次传输 input wire [7:0] tx_data, // 要发送的字节 output reg [7:0] rx_data, // 从机返回的字节 output wire cs_n, // 片选,低有效 output reg sclk, // 串行时钟,空闲为低 output reg mosi, // 主机输出 input wire miso, // 主机输入 output reg done // 传输完成,单周期高脉冲 ); localparam IDLE = 2'd0, HALF_LOW = 2'd1, HALF_HIGH = 2'd2; reg [1:0] state; reg [7:0] tx_shf, rx_shf; reg [15:0] cnt; reg [2:0] bit_cnt; assign cs_n = (state == IDLE); always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= IDLE; sclk <= 1'b0; mosi <= 1'b0; done <= 1'b0; rx_data <= 8'h00; end else begin case (state) IDLE: begin sclk <= 1'b0; done <= 1'b0; if (start) begin tx_shf <= tx_data; rx_shf <= 8'h00; cnt <= 16'd0; bit_cnt <= 3'd0; state <= HALF_LOW; end end HALF_LOW: begin sclk <= 1'b0; mosi <= tx_shf[7]; // 先在低电平期准备好数据 if (cnt == CLK_DIV - 1) begin cnt <= 16'd0; sclk <= 1'b1; // 上升沿,从机采样 rx_shf <= {rx_shf[6:0], miso}; state <= HALF_HIGH; end else begin cnt <= cnt + 16'd1; end end HALF_HIGH: begin if (cnt == CLK_DIV - 1) begin cnt <= 16'd0; sclk <= 1'b0; tx_shf <= {tx_shf[6:0], 1'b0}; if (bit_cnt == 3'd7) begin state <= IDLE; done <= 1'b1; rx_data <= rx_shf; end else begin bit_cnt <= bit_cnt + 3'd1; state <= HALF_LOW; end end else begin cnt <= cnt + 16'd1; end end endcase end end endmodule这个状态机把一次传输拆成“低半周期准备数据、高半周期让从机采样”的节奏。CLK_DIV参数算的是SCLK频率:系统时钟100 MHz、CLK_DIV=50时,SCLK正好是1 MHz。cs_n在IDLE状态为高,一旦进入HALF_LOW就自动拉低,传输结束回到IDLE再释放。上层模块只需要在start上打一个脉冲,等待done脉冲后再发起下一字节。
实际工程里我会再包一层“多字节写”模块,专门处理AD9914那种4字节的FTW写入。每发完一个字节,done信号触发计数器加一,按顺序把下一个字节送到tx_data,最后一字节发完后整体结束。这样做的好处是上层只需要关心“从哪个地址开始、数据是什么”,不用管SCLK的每个边沿细节。
3.2 AD9914配置:单频输出的关键寄存器
AD9914的寄存器默认写入后不会立刻生效,数据先落在缓冲寄存器里,必须等IO_UPDATE引脚来一个脉冲才会整体进入活动寄存器。这个机制的好处是多字节配置可以“先写完、一次生效”,坏处是如果中间少写一个寄存器,IO_UPDATE一打,输出就会按一个半成品配置工作,调试时经常因为这个出现“看起来寄存器没错、输出却不对”的情况。
单频输出配置流程通常是这样的,以3.5 GSPS系统时钟输出固定频率为例:
// AD9914寄存器地址按手册映射填写,工程头文件里有同名宏 #define AD9914_CFR1 0x00 #define AD9914_CFR2 0x01 #define AD9914_FTW0 0x04 void ad9914_set_frequency(uint32_t ftw) { // CFR1: 关闭RAM和数字斜坡,使用FTW0作为频率字 spi_write_byte(AD9914_CFR1, 0x00); // CFR2: 关闭自动IO_UPDATE同步,SYSCLK直接由外部提供 spi_write_byte(AD9914_CFR2, 0x00); // FTW0是4字节寄存器,高位在前 spi_write_byte(AD9914_FTW0 + 0, (ftw >> 24) & 0xFF); spi_write_byte(AD9914_FTW0 + 1, (ftw >> 16) & 0xFF); spi_write_byte(AD9914_FTW0 + 2, (ftw >> 8) & 0xFF); spi_write_byte(AD9914_FTW0 + 3, ftw & 0xFF); io_update_pulse(); // 拉高IO_UPDATE至少一个周期 }这里spi_write_byte就是前面SPI主机模块的C模型封装。FTW0地址偏移加0到3,字节序是高位在前,也就是先发MSB。io_update_pulse在FPGA里实现为一个宽约一个系统时钟周期的脉冲,接到AD9914的IO_UPDATE引脚。
配置完后如果输出频率不对,先回读FTW寄存器,确认写入值没有被截断。AD9914的FTW是32位,SPI写4字节时任何一字节丢失或错位,输出频率都会差得离谱。回读的方法不能省,这是排查这类问题最快的路径。
3.3 HMC835配置:锁定环路与VCO校准
HMC835的寄存器帧格式与AD9914不同,调用层通常封装成“写一个地址、跟两字节数据”的函数。它的配置顺序比AD9914更敏感,参考分频、鉴相频率、电荷泵电流、VCO校准这些环节互相耦合,跳着写很容易出现“寄存器看着对,频率完全不对”的现象。
// HMC835寄存器地址以手册为准,工程里用宏隔离具体数值 void hmc835_write(uint8_t addr, uint16_t data); void hmc835_init(void) { // 1. 参考分频:把外部10MHz参考分到目标鉴相频率 hmc835_write(HMC835_REG_REF_DIV, 10); // 2. 环路控制:小数模式、电荷泵电流档位 hmc835_write(HMC835_REG_PLL_CTRL, pll_cfg_value); // 3. 频率字:按目标频率和鉴相频率计算分频比 hmc835_write(HMC835_REG_FREQ_WORD, freq_frac_value); // 4. 触发VCO自动校准 hmc835_write(HMC835_REG_VCO_CAL, cal_trigger_value); delay_ms(10); // 5. 等待锁定,而不是盲目等待 while (!hmc835_lock_detect()) { delay_ms(1); } }常见做法是把HMC835的初始化拆成“配置寄存器”和“触发校准”两步。第一次配置完成后,片内VCO需要根据目标频率做一次自动校准,校准期间RF输出频率是不准的,只有校准结束且环路锁定后,输出才落到目标频点。如果跳过校准直接查频率,会看到输出停留在上一次或中间状态,这也是很多人觉得PLL是“黑匣子”的原因。
HMC835还应该在正式使用前做一次寄存器回读,特别是VCO校准状态位和锁定状态位。工程里可以加一个调试接口,用SPI读回这两个标志位,烧到上位机或LED上,省得每次都要拿频谱仪确认。
4. 扫频模式实现:数字斜坡、RAM与profile切换三选一
4.1 三种扫频方案对比
AD9914做扫频有三条路:数字斜坡调制、RAM模式、profile切换。三种方案各有适用场景,选错了会要么速度上不去,要么FPGA被拖得很累。
| 方案 | 扫频速度 | 频率精度 | FPGA负载 | 适用场景 |
|---|---|---|---|---|
| 数字斜坡DRM | 最快,内部自动步进 | 32位FTW | 极低 | 线性扫频、锯齿波 |
| RAM模式 | 受RAM深度限制 | 32位FTW | 低 | 任意频率曲线 |
| profile切换 | 取决于切换时序 | 32位FTW | 中 | 离散频点跳变 |
数字斜坡适合绝大多数“起点到终点线性扫”的场景,一次配置后FPGA基本不用管,扫频引擎在芯片内部自己走。RAM模式适合非线性曲线,比如对数扫频、带预畸变的频率曲线,把这些FTW提前算好写进RAM,然后按地址循环播放。profile切换适合跳频,比如雷达的频点序列,每个profile装一个固定频率,用外部引脚或SPI快速选择。
4.2 数字斜坡模式配置流程
数字斜坡的关键参数有四个:上限FTW、下限FTW、步进FTW、步进速率。上限和下限决定扫频边界,步进决定每次跳多少,速率决定多久跳一次。配置完这些后再把CFR1里的数字斜坡使能位打开,打一个IO_UPDATE脉冲,扫频就自动开始了。
// 从f_start扫到f_stop,N步完成 uint32_t ftw_start = freq_to_ftw(f_start); uint32_t ftw_stop = freq_to_ftw(f_stop); uint32_t step_ftw = (ftw_stop - ftw_start) / N; // 按FTW算步长 // 使能数字斜坡调制,方向由边界自动决定 spi_write_byte(AD9914_CFR1, DRM_EN); // 写入扫频边界 spi_write_byte(AD9914_RAMP_LIMIT_HIGH + 0, (ftw_stop >> 24) & 0xFF); spi_write_byte(AD9914_RAMP_LIMIT_HIGH + 1, (ftw_stop >> 16) & 0xFF); spi_write_byte(AD9914_RAMP_LIMIT_HIGH + 2, (ftw_stop >> 8) & 0xFF); spi_write_byte(AD9914_RAMP_LIMIT_HIGH + 3, ftw_stop & 0xFF); // 下限类似,不再重复 spi_write_byte(AD9914_RAMP_LIMIT_LOW + 0, (ftw_start >> 24) & 0xFF); // ... spi_write_byte(AD9914_RAMP_STEP + 0, (step_ftw >> 24) & 0xFF); // ... spi_write_byte(AD9914_RAMP_RATE, rate_value); // 每rate个SYSCLK步进一次 io_update_pulse(); // 一次生效,扫频启动这里的freq_to_ftw就是前面的公式,先算32位FTW,再按字节拆开写入。step_ftw要注意取整问题,如果(ftw_stop - ftw_start)不能被N整除,最后一步会落在边界之外,实际扫频终点和设定值会差一两个步长。常见做法是让N取2的幂,或者最后一步直接把终点写成ftw_stop。
RAMP_RATE的单位是系统时钟周期,rate越大扫频越慢。这个值不能太小,AD9914内部对步进间隔有时序要求,太小会导致斜坡异常或步进丢失。具体最小值以数据手册为准,我的经验是rate至少取8以上,留出余量。
4.3 扫频启动与结束判断
数字斜坡启动后,FPGA不需要逐点参与,但要有一个机制判断“这次扫频是否结束”,以及“要不要循环扫”。AD9914的斜坡在到达边界后默认停在终点,也可以配置成方向翻转,变成三角波扫频。判结束时可以读芯片内部的状态寄存器,但SPI读取会占用总线时间;另一个更简单的做法是在FPGA里自己计时,斜坡总时长等于步进数乘以rate再除以系统时钟,时间一到就认为扫频结束。
// 每10ms触发一次正向扫频,用IO_UPDATE启动 reg [31:0] timer; reg io_update_pulse; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin timer <= 32'd0; io_update_pulse <= 1'b0; end else if (timer == TIMER_10MS) begin timer <= 32'd0; io_update_pulse <= 1'b1; // 启动一次斜坡 end else begin io_update_pulse <= 1'b0; timer <= timer + 32'd1; end end这个代码实现的是周期性的扫频触发。注意io_update_pulse只在timer到期的那个周期拉高,其它时间保持低电平,避免反复触发斜坡配置复位。如果要做往返扫频,需要读回“到达边界”的标志再反向设置方向,或者直接配置CFR1里的自动翻转位,让芯片自己往返。
5. 避坑手记:SPI时序、锁定失败与扫频非线性问题排查
5.1 HMC835写完频率字后输出还是上一次的频率
现象:写入HMC835频率寄存器时没有任何报错,SPI波形也对,但频谱仪上输出频率纹丝不动。
原因:HMC835写完频率字后必须触发片内VCO自动校准,校准不启动,新频率字就不会参与环路计算;另一个常见原因是写入顺序错乱,频率字被后续的寄存器配置覆盖掉。
解决:把初始化固定为“参考分频 → 环路控制 → 频率字 → 校准触发”的顺序,校准触发放最后一步。校准完成后轮询锁定状态寄存器,锁定后再做下一步操作。从那以后我每次改完HMC835配置都强制走一遍回读,确认写入值和读回值一致,避免“写了个寂寞”。
5.2 AD9914单频正常,扫频时边界出现毛刺
现象:单频输出正常,数字斜坡扫频时中间平顺,但在起点和终点有突跳或短促的尖峰。
原因:斜坡边界FTW和初始FTW没有对齐,扫频到边界后内部频率字瞬间跳变;还有可能是IO_UPDATE与AD9914的SYNC_CLK没有同步,斜坡参数部分生效、部分还在缓冲寄存器里,造成一瞬间的错乱状态。
解决:配置斜坡参数时确保所有相关寄存器在一次IO_UPDATE窗口内写完,不要写一半打脉冲。同步问题上,打开CFR2里的IO_UPDATE同步位,让IO_UPDATE的采样沿和SYNC_CLK对齐,毛刺会明显改善。
5.3 同一套配置,换一块板子就锁定失败
现象:原先调好的工程,重新打样后同样代码写进去,HMC835要么锁不住,要么锁定指示灯闪个不停。
原因:HMC835的环路滤波器性能与外部元件、板级寄生电容相关。新板子的地平面、走线分布电容变了,等效环路带宽跟着变,原来的电荷泵电流档位就不合适了。
解决:先降低鉴相频率试试,把锁定难度降下来;再调整电荷泵电流档位,找到一个能让环路稳定锁定的组合。用示波器看环路滤波器控制电压,锁定后这个电压应该稳定不动,如果看到缓慢漂移,说明环路带宽过低或电荷泵电流不对。
5.4 扫频步进频率与设定不符
现象:设置100个步进从100 MHz扫到600 MHz,实际数出来只有90多步,或者每步间隔不均匀。
原因:FTW是按2^32比例量化的,实际频率步进等于FTW步进乘以系统时钟再除以2^32。计算时如果把FTW步进取整,每个步进的实际频率间隔和理想值会出现偏差,多步累积后终点偏离目标。
解决:不要直接按“步数”配步进,先按频率间隔算FTW间隔,再让步数等于边界差除以FTW差。要求精确扫频点或者均匀步进时,改用RAM模式,把每个频点的FTW提前算好存进去,完全绕开斜坡步进的量化问题。
提示:AD9914的FTW计算、HMC835的寄存器帧格式,不同批次数据手册表述有差异,工程里所有寄存器地址和bit位都建议用宏封装,方便拿到手册后一次替换到位。
6. 二次开发:把固定扫频源改造成可程控信号源
6.1 增加幅度控制
AD9914除了频率字,还有幅度字。幅度控制可以解决“扫频时需要同步调整输出功率”的问题,比如做接收机增益平坦度测试时,希望每个频点输出功率一致。把幅度字和频率字一起写在同一个IO_UPDATE窗口里,更新频率的同时更新幅度,实现“频率-幅度”联动。HMC835的输出功率调整能力有限,常见做法是外接数控衰减器,用FPGA的另一组SPI或GPIO控制。
6.2 设计串口控制协议
改成可程控信号源最直接的方式是加一个UART命令接口。用“帧头 + 命令字 + 参数 + 校验”的结构,解析层用状态机实现,避免阻塞式的字符串解析。命令字至少覆盖两类:设置中心频率、设置扫频范围。帧结构建议做成定长,方便FPGA里做状态机,也方便上位机组帧。
| 字节偏移 | 内容 | 说明 |
|---|---|---|
| 0 | 0xAA | 帧头 |
| 1 | 命令字 | 0x01设频点,0x02设扫频 |
| 2–5 | 参数 | 频率值或扫频上下限 |
| 6 | 校验和 | 前6字节异或 |
上层收到完整一帧后,校验通过才执行配置。我之前在被串口调试助手反复打断的时候踩过坑,数据处理到一半被新的字节覆盖,后来强制规定“帧不完整不执行”,这个习惯帮我稳定了后续所有调试流程。
6.3 验证方法
改动后先在频谱仪上用Max Hold看扫频包络,确认边界没有尖峰;再配合检波器和ADC回采,检查扫频曲线线性度;最后把所有配置过的寄存器回读一遍,确认写入值与回读值一致。这套流程排掉了大半“写入但不生效”的暗亏,从那以后我每次改完配置都强制走一遍回读比对,再继续往下调。希望帮到你。
本文还有配套的精品资源,点击获取