☰
基于AD9914与HMC835的宽带扫频源FPGA设计解析
2026/10/21 3:52:38 网站建设 项目流程

简介:面向FPGA射频控制开发的完整驱动调试资源,聚焦AD9914与HMC835两款高速信号链芯片的协同控制,解决扫频、定频、调频等模式切换与SPI配置问题。源码采用Verilog编写,结构清晰,适合有一定数字逻辑基础、正在开展DDS或锁相环相关项目的工程师快速移植验证。压缩包共77个文件,包含核心Verilog源码文件(.v)、Quartus工程配置与编译报告(.qpf、.qsf、.rpt)、仿真与约束文件(.stp、.pin)及配置文件,烧录文件(.jic、.sof)可直接用于板级测试;资源整体仅1.31MB,轻量易获取。已有898人学习浏览,实用性获同行认可。通过精简的模块划分与寄存器参数说明,读者可轻松调整频率控制字,快速验证AD9914的多种工作模式,并参考HMC835的SPI时序实现多芯片联动,适用于实验室验证、课程设计或工程预研阶段,是一份兼具参考价值与上手效率的开源设计资料。

1. 宽带扫频源不好做:AD9914 和 HMC835 为什么要成对出现

做射频测试的人迟早会遇到这个需求:要一台能从 2 GHz 扫到 10 GHz、步进小到 kHz 级、每个频点驻留时间还能自己定的信号源。买现成仪器当然行,但成本压不下来;自己用单片 DDS 做,输出频率上到 1.4 GHz 基本到头;用单片 PLL 做,步进和跳频速度又不够看。我拿到这份 AD9914 与 HMC835 的工程包时眼前一亮——FPGA 用 SPI 同时管住两片芯片:HMC835 负责高频本振,AD9914 负责中频精细扫描,再用混频级把两路信号合起来。这个 rar 里既有 Verilog 的 SPI 驱动、两片芯片的寄存器配置表,也有扫频协同状态机和配套文档。对做雷达目标模拟、宽带接收机自检、EMC 预测试这类需要“宽频段 + 细步进”的开发者来说,是一份可以直接落地的工程参考。

2. 解压工程之后:文件结构、芯片分工与顶层调度

2.1 两级频率合成架构:本振与细扫各干各的

这套方案的思路是典型的 DDS + PLL 混频级联。HMC835 是一款集成 VCO 的小数分频 PLL,输出范围能覆盖 6.8~13.4 GHz,用它来做粗调本振;AD9914 是 3.5 GSPS 的 DDS,DAC 输出可以直接出到 1.4 GHz,用它来做细调中频。两者各进混频器的一边,取上边带 LO+IF,再经带通滤波器选频,整机输出就是“本振频率 + 中频频率”的合成结果。

信号链路大致是:板载 100 MHz TCXO 进 AD9914 的 REFCLK 引脚,由 AD9914 内部 PLL 倍频到 2.4 GHz 作为系统时钟,DDS 在这个系统时钟下合成 200~400 MHz 的中频扫频信号;另一路 HMC835 输出 6.8~13.4 GHz 的本振;两路进混频器,上边带经滤波器之后就是最终射频输出。这样整机覆盖范围从 7 GHz 起步,每段扫宽由 AD9914 决定,整机频段由 HMC835 一个台阶一个台阶往上抬。

为什么不在 AD9914 上直接做斜坡扫频覆盖全频段?首先是频率上限不够,1.4 GHz 的 DAC 输出离 10 GHz 差得远;其次是测试仪器通常希望每个频点能驻留一段时间,让被测设备把功率、频率响应稳定下来,离散步进比连续斜坡更好控制驻留时间。工程包里的扫频状态机正是按“HMC835 粗跳一次、AD9914 细扫一段、再跳下一次”的循环设计的,每段边界做拼接,最终拼出整段大跨度扫频。

2.2 解压后的文件清单

我拿到的这份 rar 解压之后,典型目录结构如下,具体文件名可能因版本略有出入,但模块划分基本就这几块:

目录 / 文件内容怎么用
fpga/src/spi_master.v参数化 SPI 主控,支持位宽和模式配置所有寄存器读写入口,AD9914 和 HMC835 共用
fpga/src/ad9914_driver.vAD9914 寄存器封装,含 I/O UPDATE 同步逻辑扫频时逐点写 FTW0,触发频率更新
fpga/src/hmc835_driver.vHMC835 寄存器表 + 锁定检测上电初始化和每个扫频段切换时配置本振
fpga/src/sweep_control.v扫频状态机粗跳本振与细扫 DDS 的时序协调
fpga/constraints/pin.xdc引脚约束,定义 SPI 引脚、复位、锁定输入换板卡必须对照原理图改
docs/寄存器配置指南.txt两片芯片寄存器地址速查写驱动时对照,省得翻几百页 datasheet
python/reg_gen.py按起止频率自动生成 FTW 和寄存器序列上位机把扫频参数转成 FPGA 能用的寄存器值
README.md编译与烧录步骤先读它,再动工程

fpga/constraints 和 python 这两个目录往往被忽略,其实反而是二次开发最快的入口。引脚约束决定了你换板卡时哪里要动;reg_gen.py 决定了你在上位机改一个频率参数时不用回头改 Verilog。我一般拿到工程第一件事就是把 README 和寄存器配置指南打印出来,把引脚对照表过一遍,再开仿真。

2.3 顶层调度关系:谁在指挥谁

顶层 top.v 里挂四个主要模块:spi_master 被两个芯片驱动模块共享,sweep_control 是总指挥。sweep_control 给 hmc835_driver 发“跳到某个本振频点”的命令,HMC835 锁定了之后,sweep_control 再给 ad9914_driver 发“当前这个本振下,从哪个中频频率开始扫”的指令。AD9914 的每个频率点更新,都由 ad9914_driver 内部的 I/O UPDATE 脉冲来触发,这个脉冲不是随便拉的,必须和 AD9914 的 SYNC_CLK 对齐,后面避坑章会专门讲。

上电初始化顺序也很有讲究:先配置 HMC835,因为 AD9914 的系统时钟虽然不直接来自 HMC835,但整机的频率参考关系、混频器的工作点都依赖本振先稳定;HMC835 锁定并读完锁定状态之后,再初始化 AD9914 的 CFR、FR1、FR2,把 DDS 的倍频、输出使能、扫频方向设好,最后才进入扫频循环。这个顺序在顶层里是通过一个 init_done 信号串起来的,仿真时能看到复位释放后大概几百微秒内,两个芯片的 CS 依次被拉低、寄存器按序写入,整个过程有条不紊。

3. 驱动层拆解:一个 SPI 主控怎么同时伺候两片芯片

3.1 spi_master:参数化位宽与 SPI 模式

两片芯片的 SPI 协议不完全一样。AD9914 的写操作是“8 位指令字节 + 数据字节”,指令字节里包含读写位、传输模式位和寄存器地址;HMC835 则是固定 32 位包,地址和数据都打包在一条传输里。为了让驱动层不重复造轮子,spi_master 把位宽做成了参数,一次传输发多少位由调用方决定。传输数据统一按 MSB 先行,这样无论哪片芯片都不用额外倒序。

module spi_master #( parameter CLK_DIV = 8 // 主时钟分频系数,决定 SCLK 频率 )( input wire clk, input wire rst_n, input wire start, // 启动一次传输,单脉冲 input wire [31:0] tx_data, // 要发送的数据,MSB 先发 input wire [5:0] bit_len, // 本次传输位数,1~32 input wire cpol, // SCLK 空闲电平 input wire cpha, // 采样沿选择:0=首沿,1=次沿 input wire sdo, // MISO,读寄存器时使用 output reg sclk, output reg sdi, // MOSI output reg cs_n, output reg busy ); reg [5:0] bit_cnt; reg [31:0] shift_reg; reg [2:0] clk_cnt; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin sclk <= cpol; // 空闲电平 cs_n <= 1'b1; busy <= 1'b0; sdi <= 1'b0; bit_cnt <= 6'd0; end else if (start && !busy) begin busy <= 1'b1; cs_n <= 1'b0; shift_reg <= tx_data; bit_cnt <= bit_len; clk_cnt <= 3'd0; end else if (busy) begin // 分频产生 SCLK,每 CLK_DIV 个主时钟翻转一次 if (clk_cnt == CLK_DIV[2:0] - 1) begin clk_cnt <= 3'd0; sclk <= ~sclk; // 数据在 SCLK 边沿附近更新 if (sclk == cpol) begin sdi <= shift_reg[31]; shift_reg <= {shift_reg[30:0], 1'b0}; if (bit_cnt == 6'd1) begin busy <= 1'b0; cs_n <= 1'b1; end else begin bit_cnt <= bit_cnt - 6'd1; end end end else begin clk_cnt <= clk_cnt + 3'd1; end end end endmodule

这段代码把 SCLK 频率降成主时钟的 1/8,对 AD9914 和 HMC835 都足够安全,它们的 SCLK 上限都在几十 MHz 量级。cpol 和 cpha 从顶层开关接入,上电后由初始化代码根据芯片手册配好。读操作在这个简化版本里没有展开写,但工程里的完整版本会在 bit_cnt 结束前把 sdo 采样进一根移位寄存器,逻辑上就是“最后一位发完的同时采回来”,HMC835 的锁定检测就靠这个回读通道。

3.2 AD9914 寄存器写入:FTW0 的四字节顺序与 I/O UPDATE

AD9914 写寄存器的指令格式是:1 位读写位、2 位传输模式、3 位寄存器地址、2 位保留位。单 bit 串行模式下,指令字节就是 {1'b0, 2'b00, reg_addr, 2'b00},数据字节跟在后面逐个发出。工程里把写寄存器封装成了一个任务,这样扫频状态机里每次更新频率只需要一行调用。

localparam AD9914_CFR = 3'd0; // 控制功能寄存器 localparam AD9914_FR1 = 3'd1; // 功能寄存器 1 localparam AD9914_FR2 = 3'd2; // 功能寄存器 2 localparam AD9914_FTW0 = 3'd4; // 频率调谐字 0,地址以 datasheet 为准 task spi_write_reg_9914( input [2:0] reg_addr, input [31:0] data ); reg [7:0] inst; begin // 指令字节:写、单bit模式、寄存器地址 inst = {1'b0, 2'b00, reg_addr, 2'b00}; spi_start(inst, 8); // 发指令字节 spi_start(data, 32); // 发 4 字节数据,MSB 先出 end endtask task write_ftw0(input [31:0] ftw); begin spi_write_reg_9914(AD9914_FTW0, ftw); pulse_io_update(); // 频率调谐字写入后必须触发更新 end endtask

FTW 的计算式是 FTW = f_out × 2^32 / f_sysclk。比如 f_sysclk = 2.4 GHz,想输出 300 MHz,FTW 就是 300e6 × 2^32 / 2.4e9 ≈ 0x19999999。这里有个默认前提是 AD9914 的 CFR 里已经配好了 2.4 GHz 的倍频系数,这个值由初始化序列在扫频开始前写好。pulse_io_update 不能直接拿主时钟去拉,正确做法是等 SYNC_CLK 上升沿到来后再打一拍,保证寄存器数据在同步时钟域里安全落地。这段逻辑在 ad9914_driver.v 里单独成模块,后面避坑章会提到它为什么值得单独写。

3.3 HMC835:寄存器表驱动与锁定检测

HMC835 的 SPI 写操作是一整条 32 位包:高位是固定的 header,中间是寄存器地址,低位是数据,后面补零。因为数据位只有 6 位,所以大部分配置都要拆成多个寄存器来写。工程里的 hmc835_driver 不直接暴露 SPI 时序,而是提供一个按地址写数据的任务,配置序列在初始化时一次性灌进去。

task hmc835_write( input [5:0] addr, input [5:0] data ); reg [31:0] pkt; begin pkt = {4'h0, addr, data, 16'h0}; spi_start(pkt, 32); // 整包发送,MSB 先行 end endtask task hmc835_init(input [5:0] ref_div, input [15:0] n_int, input [13:0] n_frac); begin hmc835_write(6'd0, ref_div); // 参考分频 hmc835_write(6'd2, n_int); // N 整数分频 hmc835_write(6'd3, n_frac); // N 小数分频 hmc835_write(6'd5, 6'b100001); // VCO 频段与输出配置 hmc835_write(6'd6, 6'b000001); // 触发 VCO 校准 end endtask

HMC835 的锁定检测走回读通道:读状态寄存器某个位,为高表示 VCO 已经锁到目标频率。工程里在 WAIT_LOCK 状态里加了一个超时计数器,防止芯片因为配置错误永远锁不上时程序卡死。锁定位地址和状态寄存器的 bit 位置在 docs/寄存器配置指南.txt 里有标注,不同版本的 HMC835 回读格式略有差异,以手册为准。这里要注意的是 HMC835 的校准是异步的,写完校准使能寄存器之后不能立刻认为频率已经就绪,必须等锁定标志,这是扫频拼接能不能成功的第一道关口。

4. 扫频协同状态机:粗跳本振、细扫 DDS、拼接宽带

4.1 扫频参数与拼接原则

整套扫频的核心参数就五个:起始频率 F_start、终止频率 F_stop、步进 F_step、每点驻留时间 T_dwell、本振跳步宽度 LO_step。LO_step 必须小于等于 AD9914 在这个本振点上的中频扫宽,否则两段之间会有盲区。工程里给的典型配置是 LO_step = 200 MHz,AD9914 的中频扫宽也是 200 MHz,两边刚好接上,边界处靠滤波器过渡。

参数含义典型值
F_start扫频起始射频频率7 GHz
F_stop扫频终止射频频率13 GHz
F_step每个频点的步进1 MHz
T_dwell每个频点驻留时间10 ms
LO_step本振每次跳变的带宽200 MHz
IF_spanAD9914 中频扫宽200 MHz

拼接原则一句话:同一个本振频率下,AD9914 从中频起始扫到中频结束,每个频点的射频输出等于本振加中频;这段扫完后,HMC835 跳高一个 LO_step,AD9914 回到中频起点,继续下一段。这么做的理由很务实:HMC835 虽然能覆盖很宽的频段,但每次跳频都要等 VCO 校准和锁定,耗时是毫秒级;AD9914 是 DDS,频率切换只要微秒级,步进可以做到很小。让快的芯片干细活、慢的芯片干粗活,整体扫频效率最高。

4.2 状态机实现:从 IDLE 到 DONE 的完整迁移

sweep_control.v 的状态机是整个工程的调度核心。它不管 SPI 底层的字节时序,只关心“本振锁没锁”“当前频率字是多少”“驻留时间到没到”这三件事。

localparam S_IDLE = 3'd0; localparam S_SET_LO = 3'd1; localparam S_WAIT_LOCK = 3'd2; localparam S_SET_FTW = 3'd3; localparam S_DWELL = 3'd4; localparam S_NEXT_STEP = 3'd5; localparam S_DONE = 3'd6; reg [2:0] state; reg [31:0] cur_ftw; reg [19:0] dwell_cnt; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= S_IDLE; end else begin case (state) S_IDLE: begin if (start_sweep) state <= S_SET_LO; end S_SET_LO: begin // 给 HMC835 写本振频率,然后进入锁等待 hmc835_set_freq(lo_freq); state <= S_WAIT_LOCK; end S_WAIT_LOCK: begin if (hmc835_locked) // 锁定标志有效 state <= S_SET_FTW; else if (lock_timeout) // 超时保护 state <= S_DONE; end S_SET_FTW: begin // 用绝对频率计算当前点的 FTW cur_ftw <= calc_ftw(f_rf_current); ad9914_write_ftw0(cur_ftw); dwell_cnt <= T_DWELL; state <= S_DWELL; end S_DWELL: begin if (dwell_cnt == 0) begin if (f_rf_current >= f_stop) state <= S_DONE; else begin f_rf_current <= f_rf_current + F_STEP; state <= S_SET_FTW; // 继续下一个频点 end end else begin dwell_cnt <= dwell_cnt - 1'b1; end end S_DONE: begin sweep_done <= 1'b1; state <= S_IDLE; end endcase end end

状态机里最关键的一点是:S_SET_FTW 里用的是绝对频率重新计算 FTW,而不是在上一拍的 FTW 上加一个增量。增量累加看着省事,但 32 位 FTW 在 2.4 GHz 系统时钟下,每个步进的舍入误差会随着扫频点数累积,扫到几百个点之后拼接处就会出现频率偏错。绝对计算每次都用当前射频频率减去本振频率得到中频频率,再换 FTW,误差不会累积。

4.3 FTW 计算与 reg_gen.py 的配合

FTW 的计算在 FPGA 里做会消耗不少乘法器资源,工程里的做法是:FPGA 只负责按状态机跑,FTW 表由上位机 Python 脚本提前算好,通过 UART 或 JTAG 灌进 RAM 再逐点取。reg_gen.py 干的就是这个活:

def calc_ftw(f_out_mhz, f_sysclk_mhz): return int(round(f_out_mhz * 2**32 / f_sysclk_mhz)) def gen_sweep_table(f_lo_mhz, if_start_mhz, if_stop_mhz, step_mhz, f_sysclk_mhz=2400): ftw_list = [] f_if = if_start_mhz while f_if <= if_stop_mhz: ftw_list.append(calc_ftw(f_if, f_sysclk_mhz)) f_if += step_mhz return ftw_list

calc_ftw 的输入频率和系统时钟都用 MHz 为单位,避免大数溢出。round 在这里很重要,直接截断会让输出频率系统性偏低。改成绝对计算之后,表里每个元素只和当前中频频率有关,和上一个点无关,出错范围最多一个 LSB,不会滚雪球。

工程里的 Python 脚本还支持把整张表按 32 位十六进制输出成 Verilog 的初始化文件,直接 $readmemh 进 RAM。这样换扫频参数时不用重新综合 FPGA,最多重新生成一张表,再烧一次存储。对于要反复调扫频范围的项目,这套流程比打开 Vivado 改参数重新跑一遍实现要快一个数量级。

5. 避坑手记:SPI 相位、I/O UPDATE、VCO 校准这四道坎

5.1 SPI 回读全 0xFF 或写进去不生效

现象:往 HMC835 写配置寄存器后,回读状态寄存器永远返回 0xFF,或者 AD9914 的输出频率完全不变化。用逻辑分析仪抓 SCLK、SDI、CS,看起来波形都在动,但芯片就是不认。

原因:SPI 模式不匹配。AD9914 和 HMC835 虽然都支持标准 SPI,但一个可能工作在 CPOL=0/CPHA=0,另一个要求 CPOL=0/CPHA=1。如果你的 spi_master 把 cpol/cpha 写死了,就会遇到“这片芯片好好的,那片芯片永远写不进”的诡异情况。更隐蔽的是,有些板卡上 SCLK 走线太长,上升沿变缓,本来 mode 0 能跑的结果采样点落在了边沿上。

解决:把 cpol 和 cpha 做成寄存器位,上电初始化时按芯片分别配置。我习惯先把 spi_master 的发送频率降到 5 MHz 以下排除信号完整性问题,再用逻辑分析仪确认 SDO 上的回读数据和预期一致。这个坑是第一优先级,因为后面所有问题都建立在 SPI 通路正确的前提上。

5.2 AD9914 写了 FTW 但输出频率纹丝不动

现象:往 FTW0 写入新频率字,I/O UPDATE 也拉了,示波器看 AD9914 输出频率和写之前一模一样,好像寄存器没更新。

原因:I/O UPDATE 没有和 SYNC_CLK 对齐。AD9914 内部寄存器分好几组,频率调谐字属于需要 I/O UPDATE 才能生效的寄存器。但这个 UPDATE 信号不能在任意时刻拉高,它必须相对 SYNC_CLK 满足建立时间。如果直接用系统主时钟去拉 I/O UPDATE,十个周期里可能有一两次落在错误的位置,寄存器就没更新成功,而且不是每次必现,排查起来很玄学。

解决:在 ad9914_driver 里单独做一个 io_update_pulse 模块,等 SYNC_CLK 上升沿到来后再输出一个周期宽的高脉冲。这在工程源码里有现成实现,移植到自己项目时别图省事把它删掉。另外注意 AD9914 的 CFR 里有一个位控制 SYNC_CLK 输出是否使能,如果这个位没打开,SYNC_CLK 引脚永远为低,I/O UPDATE 同步逻辑自然失效。

5.3 HMC835 输出频点不对:VCO 校准没完成就接着扫

现象:扫频结果在每一段的起始位置频率偏差几十 MHz,越往后越乱,但单独看 HMC835 锁定后输出又是准的。

原因:HMC835 写完频率字和校准使能之后,内部 VCO 校准需要毫秒级时间。扫频状态机如果没有等锁定标志,只延时几百微秒就切到 AD9914 开始扫,VCO 可能还在往目标频率收敛,输出的本振频率自然是错的。

解决:WAIT_LOCK 状态不要用固定延时,必须回读 HMC835 的锁定状态位。同时加超时保护,正常 5 ms 内能锁住,设置 20 ms 超时后强制报错,避免配置错误时状态机死等。这里我踩过一次:当时图省事直接用固定延时 2 ms,结果低本振段能锁住、高本振段锁不住,表现为“低频段正常、高频段全偏”,排查了很久才意识到是校准时间不够。

5.4 扫频拼接点在边界处跳变不平滑

现象:整段扫频在每 200 MHz 的分段边界上,频率不是连续过渡的,而是明显有一个台阶或瞬断。

原因:FTW 计算方式不对,或者说扫频表的生成方式不对。如果 FPGA 里用“当前 FTW + 步进增量”的方式累加,每步的舍入误差会积累,跑到本振段末尾时中频频率已经比理论值偏了几个 kHz,和下一段本振切换后重算的起点就对不上。

解决:回到 4.2 讲的绝对计算——每个点都用当前射频频率减本振频率,单独算 FTW。用 reg_gen.py 生成整表的话,保证表里的每个元素都是独立 round 出来的,不要用上一个点加步进来生成。边界是否平滑,拿频谱仪用 Max Hold 扫一遍整段就能看出来,拼接点如果有断裂,再检查是不是误差累积。

5.5 本振相位噪声比手册差 20 dB

现象:频谱仪测 HMC835 输出,相位噪声曲线比数据手册典型值差接近 20 dB @ 10 kHz offset,而且杂散明显增多。

原因:供电问题。HMC835 的 VCO 供电对电源噪声非常敏感,如果模拟供电和数字供电混用,或者 VCO 供电来自开关电源而没有经过低噪声 LDO,相位噪声会被电源上的纹波调制。另一个常见因素是 PCB 布局上 SPI 信号线没有包地,串扰进了 VCO 的控制端。

解决:VCO 供电单独用一颗低噪声 LDO,模拟地和数字地在 PCB 上单点连接。AD9914 的 AVDD 和 DVDD 也要分开走线,至少用磁珠隔开。这个坑不会让系统不工作,但会让指标很难看,属于“功能对了、性能翻车”的典型情况。如果时间紧改不了板,至少可以把 SPI 时钟频率降下来,减少数字噪声耦合。

6. 拿来改:换本振、加调制、接上位机命令

6.1 换成其它频段的 PLL,驱动不用重写

HMC835 覆盖 6.8~13.4 GHz,如果你要扫更低频段,可以换成同类 PLL 芯片。spi_master 的位宽参数已经支持 32 位包,换芯片通常只需要改 hmc835_driver 里的寄存器表和地址映射,SPI 传输层一行都不用动。我在模拟项目X里就把它换过一片 2~4 GHz 的 PLL,同样的小数分频结构,改完寄存器表就能跑。

6.2 给 AD9914 加 BPSK 调制

AD9914 除了 FTW 之外还有相位偏移字 POW。要加 BPSK 调制,只需要在 ad9914_driver 里加一个 write_pow0 入口,调制时每个符号周期交替写 0 和 0x800(12 位相位字的一半),同时触发 I/O UPDATE。这个改动很小,但能直接把扫频源变成一个带调制的信号源,做雷达回波模拟时非常有用。

6.3 扫频参数改成上位机可配

现在改扫频参数要重新生成表、重烧 FPGA,不方便。我一般会在顶层加一个 UART 解析模块,帧格式定成:帧头 0xAA 0xA5,后面依次是 F_start、F_stop、F_step、T_dwell 各 4 字节,最后两个字节 CRC。FPGA 收到完整帧后直接把参数寄存器更新掉,扫频状态机在下一轮循环读取新参数,不用重新综合。从那以后我每次换新板卡,都先写一个最小回读测试把 SPI 通路验一遍,再让扫频状态机上电跑,这个习惯救过我至少两次,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询