简介:一套基于现场可编程门阵列(FPGA)的信号发生器电路设计工程,面向电子工程师、FPGA开发者及通信测试人员,可用于科研实验、教学实践或二次开发。工程采用硬件描述语言编写核心逻辑,在数字域实现直接数字频率合成(DDS)生成正弦波,并实现频移键控(FSK)调制等功能,配合数模转换输出模拟波形,涵盖查找表波形生成、锁相环频率合成、AM/FM/PM调制、时钟管理与噪声抑制等关键技术,有助于理解现场可编程门阵列并行处理在信号处理中的实际应用。资源共501个文件,压缩包约5.02兆字节,主要包含源程序、仿真数据、工程配置、综合布线报告、IP核配置以及功能模块定义与测试平台文件,目录结构较为完整。目前已有894人学习,适合作为现场可编程门阵列数字系统设计的进阶实战参考,可跟随工程从代码编写、仿真验证到综合实现进行全流程学习。
1. 基于FPGA的信号发生器,难的不是波形而是输出链路
把FPGA和信号发生器放在一起,第一反应是用DDS技术在内部生成正弦波、方波、三角波。但真做过一个能接示波器、能送进后级电路的信号发生器之后会发现,相位累加器反而只是热身,最花时间的是DAC接口时序、参考电压噪声和末级滤波电路。FPGA的优势在于频率字可以做到32位甚至更宽,扫频、调频、多波形切换都在数字域完成,灵活性远超DDS专用芯片,代价是外部模拟链路的每一处失误都会被频谱仪放大。
这篇内容面向两类人:一类是从单片机或DDS芯片方案转向FPGA的嵌入式工程师,另一类是已经会写Verilog但想把输出做到仪表级别的学生或开发者。下文按“DDS数字域实现 — DAC与模拟电路 — 多波形与调制 — 验证与调试”的顺序展开,所有代码和参数都按可复现的标准写。
2. DDS相位累加器的原理与FPGA实现细节
2.1 相位累加器为什么不用浮点
DDS(直接数字频率合成)的根本操作只有一个:每个时钟周期把一个频率控制字加到相位累加器上,累加器溢出即完成一个完整波形周期。频率输出公式是:
f_out = freq_word * Fclk / 2^N
其中N是相位累加器位宽,freq_word是频率控制字。这个计算在FPGA里不需要浮点,因为累加器本身就是定点数,溢出是硬件行为。浮点在这里既浪费DSP资源,也无法达到时钟级确定性。
关键参数是频率分辨率。Fclk为100MHz、N为32时,分辨率约为0.0233Hz。也就是说,最低能做到0.0233Hz的步进,这对大多数测量和激励场景都足够。N取24以下时,低频段步进会变得粗糙,做音频或生物电信号激励就很勉强。实际工程里我一般固定N=32,不轻易缩减。
| 系统时钟 (MHz) | 累加器位宽 N | 相位累加器溢出频率 (Hz) | 最小频率步进 (Hz) |
|---|---|---|---|
| 100 | 24 | 5.96 | 5.96 |
| 100 | 28 | 0.37 | 0.37 |
| 100 | 32 | 0.0233 | 0.0233 |
频率控制字与输出频率这对参数,调试时最容易被忽略的是符号扩展。freq_word使用无符号数时输出频率范围是0到Fclk/2;想产生负频率(即反方向扫频的相位递增逻辑),才需要把freq_word声明为有符号数。一般信号发生器只需要正向输出,用无符号即可。
2.2 波形ROM如何生成更省资源
正弦波查询表可以直接存一个完整周期的采样点,也可以用1/4周期镜像展开,后者能省3/4的BRAM。但如果目标是“先跑通再优化”,直接存整周期更直观。ROM表地址位宽根据相位累加器高位截取,例如累加器高10位作为ROM地址,则一个周期内有1024个采样点。
用Python生成正弦波COE文件是常见做法。下面的脚本生成1024点、12位幅度、无符号偏移的正弦表:
import math depth = 1024 width = 12 offset = (1 << (width - 1)) - 1 with open("sine_1024x12.coe", "w") as f: f.write("; 1024-point 12-bit sine lookup table\n") f.write("memory_initialization_radix=16;\n") f.write("memory_initialization_vector=\n") for i in range(depth): val = int((math.sin(2 * math.pi * i / depth) * offset) + offset) if val >= (1 << width): val = (1 << width) - 1 f.write(f"{val:03X}") if i != depth - 1: f.write(",\n") else: f.write(";\n")参数说明:depth对应ROM深度,width对应DAC位数,这里按12位输出设计;offset做电平偏移,保证DAC单极性输出时正弦波不出现负值。若使用差分DAC或双极性输出电路,则不需要加这个偏移,后面模拟电路会做电平搬移。
相位截断的杂散问题在这里就暴露了。高10位做地址意味着累加器低22位被丢弃,理想情况下会产生相位截断杂散。对大多数应用,10位地址配合.12位DAC,杂散在可接受范围。若要求更高的无杂散动态范围,可以把地址宽度提高到12或14位,代价是BRAM增加。
2.3 最小DDS模块与迭代频率更新
下面给出一个可直接综合的DDS模块,包含相位累加器和ROM例化:
module dds_signal #( parameter N = 32, parameter M = 10 )( input wire clk, input wire rst_n, input wire [N-1:0] freq_word, output reg [11:0] wave_out ); reg [N-1:0] phase_acc; always @(posedge clk or negedge rst_n) begin if (!rst_n) phase_acc <= {N{1'b0}}; else phase_acc <= phase_acc + freq_word; end wire [M-1:0] rom_addr = phase_acc[N-1:N-M]; rom_sine u_rom ( .clk(clk), .addr(rom_addr), .data(wave_out) ); endmodule逻辑说明:相位累加器在每个时钟上升沿累加freq_word;rom_addr取累加器最高M位,作为正弦ROM的地址。wave_out在ROM读出的同时刻更新,由于ROM读操作有一个时钟周期延迟,整个链路的输出延迟是固定的,对后端DAC没有影响。
频率控制字的更新要注意同步。如果freq_word由外部串口或按键逻辑实时改变,不能让它异步进入累加器。做法是在DDS模块内部用两级寄存器打拍,保证同一个时钟域内频率字稳定变化。否则会出现瞬时频率抖动,在输出波形上表现为一个窄毛刺。
2.4 时钟与复位对信号质量的影响
FPGA内部资源再干净,外部时钟抖动也会直接映射到输出相位噪声上。50MHz或100MHz有源晶振即可满足入门级信号发生器需求,但要避免用逻辑门做分频后当全局时钟使用。正确做法是保留原始时钟,用时钟使能信号控制模块运行,例如每两个周期产生一个使能脉冲,等效于二分频,但不产生新的时钟域。
复位也建议采用异步复位、同步释放的方式,避免复位释放瞬间的亚稳态在累加器里留下不确定值。一个EGO1这类入门板卡上的信号发生器项目,时钟和复位处理好之后,输出频谱的底噪会比默认模板明显干净。这个环节不需要额外电路,但值得在新设计里优先落实。
3. DAC接口、参考电压与低通滤波器设计
3.1 并行DAC、PWM还是R-2R
FPGA输出模拟信号有几种路径:PWM用积分电路平滑,适合低频和低速控制;R-2R梯形电阻网络速度快但精度受电阻温度系数限制;并行接口DAC是信号发生器的常规选择。PWM在1kHz以下做直流或慢变信号够用,但输出2MHz正弦波时,PWM频率需要远高于2MHz,穷举占空比也需要极大计数位宽,不现实。
并行DAC重点看三个参数:分辨率、更新速率、输出形式。12位、125MSPS级别的并行DAC足够支撑10MHz以内的信号发生器。接口上,FPGA只需把wave_out寄存后送到DAC数据引脚,同时给出DAC时钟。需要注意DAC的数据建立时间和保持时间,FPGA侧可以加一级输出寄存器,让数据沿与时钟沿严格对齐。
R-2R网络在低成本自制项目中常见,但实际做下来温度漂移明显。除非只做演示,否则不建议用在需要读数重复的电路里。
3.2 差分转单端与输出驱动电路
大多数并行DAC输出是互补电流形式,例如IOUT和IOUTB,后端需要用运放搭差分转单端电路。这里自然落到差分放大电路的设计上。经典结构是把DAC两个电流输出分别接到运放的反相和同相输入端,用跨阻放大把电流转成电压,再用减法器合成单端信号。
以12位DAC、满量程电流20mA为例,若希望满幅输出2V,反馈电阻取100Ω。运放的增益带宽积至少要为最高输出频率的10倍以上,输出10MHz正弦波时,GBW不能低于100MHz。实际项目里我倾向选择电流反馈型运放,压摆率更高,但要注意反馈电阻不能随意减小,否则会振荡。
输出级后面通常还要加一级缓冲,用来提高带载能力。这个时候需要考虑是单端输出还是差分输出。驱动后级ADC或示波器时,单端50Ω匹配最简单;驱动差分输入ADC时,可以用运放输出级做单端转差分。不要指望DAC直接驱动电缆,源端阻抗匹配和缓冲级是必须的。
3.3 二阶低通滤波器截止频率与参数计算
DAC输出含有采样时钟相关的镜像频率,低通滤波器是信号发生器电路不可缺少的环节。常见做法是两级Sallen-Key低通,巴特沃斯响应。设计指标是截止频率略高于最高输出频率,例如最大输出10MHz,则截至频率取12MHz左右,既保留基波又抑制高频镜像。
用Python快速计算二阶巴特沃斯滤波器的RC参数:
import math fc = 12e6 # 截止频率 12MHz C1 = 22e-12 # 电容选 22pF C2 = 2 * C1 # 巴特沃斯二阶级联的典型比例 R = 1 / (2 * math.pi * fc * math.sqrt(C1 * C2)) print(f"R = {R:.0f} ohm")参数说明:C2取C1的2倍是巴特沃斯二阶低通的常用配比;R约为540Ω时,Sallen-Key拓扑在12MHz附近提供-40dB/十倍频程衰减。实际焊接时用标称值510Ω并联或串联逼近即可。LC低通也是一个替代方向,但电感值在10MHz附近通常只有几百纳亨,寄生参数影响大,不如RC结构容易控制。若需要陷波特定频点,LC并联谐振或串联谐振可以作为一个局部修正,但它不是主滤波器的最优选型。
3.4 电源去耦与参考电压的噪声控制
DAC输出的信噪比上限由参考电压噪声决定,这一条经常被忽略。使用Buck电路作为数字电源供电没问题,但DAC模拟电源和参考电压必须再经过一级低噪声LDO。开关电源的纹波直接串到参考电压上时,输出波形会有固定的开关频率纹波,在频谱上表现为尖峰。
PCB布局上,模拟地和数字地单点连接,DAC下方不要铺数字地铜皮。参考电压引脚旁边放10μF钽电容和0.1μF陶瓷电容并联,去耦回路尽可能短。若板子上还有FPGA的IO翻转噪声,可以在DAC电源输入端串一个磁珠,进一步隔离数字噪声。做到这里,频谱底噪才会接近DAC器件本身的水平。
4. 多波形与调频调幅的工程实现
4.1 波形切换:一个ROM输出三种波形
正弦波靠ROM查表,方波和三角波其实不需要额外存储空间。方波直接取相位累加器的最高位,三角波对相位字做绝对值变换即可。三条路径用一个多路选择器选出最终波形,切换只在数据通路末端进行,不会打断相位累加过程。
always @(*) begin case (wave_sel) 2'b00: wave_pre = rom_data; // 正弦 2'b01: wave_pre = phase_acc[N-1] ? 12'hFFF : 12'h000; // 方波 2'b10: wave_pre = phase_acc[N-1:M]; // 三角波 default: wave_pre = rom_data; endcase end逻辑说明:方波仅用相位最高位,频率依然是freq_word控制;三角波用相位高位,最高位为0时输出递增段,为1时输出递减段,实际波形是折叠后的三角。wave_sel作为寄存器同步切换,避免在组合逻辑上产生毛刺。
4.2 调频扫描的频率字斜坡更新
扫频信号发生器的核心是让频率控制字按时间线性变化。通常先设定起始频率字、终止频率字和步进时钟,每个步进脉冲把当前频率字加一个增量。常用于测量滤波器幅频特性或驱动压电换能器扫描。
Verilog里的典型结构是:
always @(posedge clk or negedge rst_n) begin if (!rst_n) freq_word_target <= start_word; else if (sweep_en && freq_word_target < end_word) freq_word_target <= freq_word_target + sweep_step; end参数说明:sweep_step决定扫频速度,步进时钟频率乘以步进值就是频率变化率。扫频时要注意频率字更新不能太快,否则输出频率跳变太陡,模拟滤波器会振铃。对大多数低频测试场景,每步停留100个时钟周期是比较保守的选择。
4.3 调幅与输出电平控制
调幅在数字域只需要在DDS输出后乘一个幅度字。12位波形数据乘以12位幅度字得到24位结果,截取高12位后送DAC。这里要注意截断方式,直接截断会产生直流偏移,建议使用舍入而不是截位。幅度字平滑变化时,输出就能得到AM调幅波。
幅度控制的另一种实现是数控脉宽脉冲信号发生器模式。方波输出场景下,用户往往需要调节脉冲宽度。这时可用一个比较器把相位累加器值与脉宽字比较,输出高电平的持续时间即为脉宽。这个方案比独立PWM模块更自然,因为它和信号发生器共享同一套频率控制逻辑。电路层面若需要更高精度的输出保护,可以在输出端加限幅电路,防止后级被测设备被过压损坏。
4.4 输出链路的应用案例:扫频阻抗测量
信号发生器最有价值的应用之一是与ADC配合做扫频阻抗测量。FPGA产生扫频正弦波,经过被测器件后由同步ADC采集,FPGA内做相关解调,算出幅值和相位。这种方式避免使用网络分析仪,在低频段可以得到约1%的测量精度。
设计要点是保证DAC和ADC的时钟同源,否则相关解调算法会引入频率偏差。FPGA内部把DAC时钟分频给ADC接口,并把采样信号与DDS相位累加器做同步,这样每个采样点都能对应到精确的相位位置。该结构在电化学阻抗谱、超声换能器测试中直接可用。
5. 验证与调参技巧:testbench、频谱观察和频率字更新
5.1 先跑信号发生器仿真,再上板
上板前至少要做一次仿真,用testbench检查输出频率是否与计算一致。仿真中定义一个期望频率,采集一段wave_out,用阈值检测统计周期数:
module tb_dds; reg clk = 0; reg rst_n = 0; reg [31:0] freq_word = 32'd4294967; // 100MHz时钟下输出约1MHz wire [11:0] wave; dds_signal u_dut (.*); always #5 clk = ~clk; // 100MHz initial begin #20 rst_n = 1; #100000 $finish; end endmodule逻辑说明:freq_word取4294967,按照f_out公式计算得到约1.000MHz。仿真时间跑满100μs,用波形窗口数出100个完整周期,测量总时间应接近100μs。若偏差超过0.1%,优先检查freq_word赋值是否有进制错误。
5.2 实测时优先看频谱而不是只看波形
示波器看时域波形只能确认形状,频谱质量必须靠FFT功能或频谱仪判断。实测时先输出1MHz满幅正弦波,观察二次谐波与基波的差值。这个差值一般在40dB以上合格,低于30dB时检查滤波电路是否焊错或运放供电是否不足。
频谱底的开关纹波尖峰则指向电源去耦不足,重点排查DAC模拟电源与参考电压之间的LDO和磁珠。输出端接50Ω负载时也要注意幅度会减半,拿到读数先确认是空载还是带载状态。
5.3 用串口实时改频率字:按键防抖与UART接收
调试阶段的实用技巧是让频率字可以被PC端修改。FPGA中例化一个最简单的UART接收模块,每次收到3个字节就更新freq_word。上位机用串口助手发十六进制数,就能在不解线不重编译的情况下快速扫频。
板载按键调整频率时,机械抖动会导致频率字多跳几次。简单做法是用10ms计数器做按键防抖电路,按键电平连续稳定10ms后才视为有效,然后产生一个单周期脉冲去锁存频率字。防抖逻辑放在FPGA内部,不增加外部元件,且效果比RC硬件防抖更可控。调好这一步,整个信号发生器的操作体验才算完整。
本文还有配套的精品资源,点击获取