简介:这是一份基于FPGA的DDS信号发生器工程资源包,面向FPGA初学者、电子竞赛选手及数字信号处理学习者,可用来学习直接数字合成技术并生成方波、正弦波、三角波、锯齿波四种基本波形,适合课程设计、毕业设计与竞赛备赛。资源共449个文件,zip压缩包约15.57MB,以Quartus工程数据库文件(hdb/cdb/rdb等)为主,还包含Verilog源码、配置下载文件(sof/pof)、波形存储文件(mif)以及说明文档,便于直接打开工程开展仿真与上板验证。目前已有498人学习。工程内含方波、锯齿波、三角波等独立子模块,通过修改频率控制字即可改变输出频率,帮助理解相位累加器、相位到幅度转换等DDS核心原理,掌握FPGA并行计算在实时波形生成中的实现技巧,同时可通过调整参数快速切换波形,为通信测试、音频处理等应用开发打基础。
1. 从相位累加器到方波:DDS在FPGA上的轻量输出路径
多数讲DDS的文章都围绕正弦查表展开,方波只是顺手提一句“加个比较器”。实际做信号发生器时,方波输出可以完全不碰ROM:直接取相位累加器的最高位,一个N位累加器加一个输出寄存器,就是一路频率分辨率高达0.02Hz量级的方波输出。这个方案在FPGA上只用几十个LUT,不需要DDS IP核,也不占Block RAM,适合做多通道时间基准、脉冲群触发源和信号发生器入门工程。下面从累加器原理讲起,给出完整Verilog代码、FCW计算方法、时序约束和示波器验证步骤。会用Vivado、能看懂基础Verilog的工程师可以直接跳到第3章抄代码。
2. DDS方波生成原理与频率误差:先搞清累加器再写代码
2.1 相位累加器的真实分辨率:32位累加器能把频率细分到什么程度
DDS(直接数字频率合成)的核心是N位相位累加器,不是存波形的ROM。每个系统时钟上升沿,累加器把频率控制字FCW加到当前相位值上,自然溢出回绕,相位值从0扫到2^N-1再折回0,周而复始。输出频率由 fout = (FCW * fclk) / 2^N 决定,频率步进即分辨率等于 fclk / 2^N。用100MHz时钟配32位累加器,分辨率约0.023Hz,可以在数字域精确区分1.000000MHz和1.000000023MHz。速算分辨率的脚本写法如下:
fclk = 100e6 N = 32 print(f"频率分辨率: {fclk / 2**N:.4f} Hz")这套机制意味着方波频率不是靠PLL分频得到的整数倍关系,而是通过累加器在每个周期积累一个小增量实现的。FCW越大,相位增长越快,溢出越频繁,输出频率越高。误差只来自FCW的整数取整,取整误差不会累积,因为溢出回绕之后相位关系的统计均值不受取整影响。这一点与普通计数器分频相比,优劣势正好调过来:计数器分频的每个输出周期都是参考时钟的整数倍,周期稳定,但只能输出fclk/n的离散频率;DDS能输出任意精度的平均频率,代价是相邻边沿间隔存在±1个时钟周期的量化抖动。
2.2 取最高位为什么就是50%占空比方波
累加器数值从0递增到2^N-1,最高位MSB在前半个相位区间稳定为0,后半个相位区间稳定为1,输出正好是50%占空比、频率等于fout的方波。一个N位累加器的最高位翻转频率只有累加器更新频率的一半,所以方波输出频率上限是fclk/2。想输出比这更高的频率,要么提高系统时钟,要么换更高速的FPGA器件。
这种生成方式绕开了查表法固有的相位截断杂散问题。查表实现正弦波时,为了省ROM通常只取相位高位做地址,产生周期性相位截断误差,造成频谱杂散;而MSB输出完全不做相位映射,所有N位相位都参与翻转判断,不存在截断路径。代价是输出边沿完全依赖系统时钟,时钟抖动直接传导到方波上,对时钟源的要求比查表法更苛刻。
有一点容易疏忽:运行时不要随便复位累加器。任何一次复位都会把相位清零,造成输出方波出现相位跳变。如果你的系统需要从某个外部触发开始输出方波,正确做法是保持累加器自由运行,用输出使能en信号把sq_out钳位到0,等累加器相位走到期望位置再释放en。
2.3 方波转正弦波的滤波边界:为什么只滤三次谐波不够
标题里只有方波,但不少信号发生器场景最终要的是干净的近似正弦。用DDS方波做源再外接低通滤波器是常见做法,这也是FPGA信号发生器项目中最容易被低估的一步。方波频谱的能量集中在奇次谐波:基波、三次、五次、七次,幅度分别按1、1/3、1/5、1/7递减。三次谐波能量最大,比二次谐波强得多,所以滤波器不能只用简单RC,至少需要两级以上椭圆或切比雪夫低通,通带截止频率取在1.3~1.5倍基频,阻带在三次谐波处至少压掉40dB才有可用波形。
如果你用外部比较器法做无感方波驱动,就不需要这种滤波,因为驱动对象只关心边沿位置。这个区别反映了同一块DDS电路的两种使用形态:输出用来驱动功率级的,波形越“方”越好;输出用来当模拟信号源的,反而要把方波“圆”化。对老工程师来说,这里有一个工程判断:立项时先确认下游到底是边沿敏感还是幅度敏感,这决定了要不要铺滤波网络、要不要预留二阶有源滤波器位置。
3. Verilog实现DDS方波发生器:频率控制字计算与两种输出模式
3.1 顶层模块:累加器和输出寄存器分开写
下面这段Verilog可以直接在Vivado或Quartus里综合。模块默认参数N等于32,既保留频率分辨率又不至于让每个通道的加法器链路过长。累加器是一个独立always块,输出寄存器是另一个always块,两个块之间只有一条phase_acc线,后续加多通道或插入流水线都不需要改核心逻辑。
module dds_square_wave #( parameter N = 32 )( input wire clk, // 系统时钟,建议50~200MHz input wire rst_n, // 异步复位,低有效 input wire en, // 输出使能,0则sq_out强制为0 input wire [N-1:0] fcw, // 频率控制字,由软件或寄存器配置 input wire [N-1:0] duty, // 占空比阈值,PWM模式用 input wire pwm_mode, // 1:比较模式;0:MSB直接输出 output reg sq_out // 方波输出,同步于clk ); reg [N-1:0] phase_acc = {N{1'b0}}; always @(posedge clk or negedge rst_n) begin if (!rst_n) phase_acc <= {N{1'b0}}; else phase_acc <= phase_acc + fcw; end always @(posedge clk or negedge rst_n) begin if (!rst_n) sq_out <= 1'b0; else if (!en) sq_out <= 1'b0; // 使能关闭时不产生边沿 else if (pwm_mode) sq_out <= (phase_acc < duty) ? 1'b1 : 1'b0; else sq_out <= phase_acc[N-1]; // 直接取最高位,50%占空比 end endmodule逻辑说明:phase_acc每个时钟沿无条件累加,任何时刻都不会被清零或预置,因此相位的连续性在DDS中是天然成立的。sq_out是寄存器输出,不是组合逻辑直连,综合后边沿位置由时钟精确决定。pwm_mode比较的是完整相位值phase_acc而不是MSB,所以duty可以表达小于一个完整周期的任意相位宽度,占空比调节粒度远高于普通PWM计数器。
参数说明:N取值越大频率分辨率越高,代价是加法器的进位链变长。在Artix-7这类中端器件上,N=32时的累加器Fmax通常能跑到300MHz以上,100MHz系统时钟下没有压力。fcw的上限按实际需求算,不要超过2^(N-1),否则输出频率高于fclk/2,会退化成欠采样波形。
3.2 频率控制字计算:用Python算FCW并核对误差
FCW的计算公式为 FCW = round(fout / fclk * 2^N)。手工算一遍容易出错,尤其32位大数的十进制和十六进制转换。常见做法是用Python脚本批量生成,配置寄存器时直接烧十六进制值。
def calc_fcw(fout_hz, fclk_hz, n_bits=32): return int(round(fout_hz / fclk_hz * (2 ** n_bits))) def actual_freq(fcw, fclk_hz, n_bits=32): return fcw * fclk_hz / (2 ** n_bits) fclk = 100e6 for target in [1e3, 1e6, 10e6]: fcw = calc_fcw(target, fclk) f_act = actual_freq(fcw, fclk) print(f"目标{target:>8.0f}Hz, FCW=0x{fcw:08X}, " f"实际{f_act:>10.3f}Hz, 误差{f_act-target:+.3f}Hz")上面脚本的运行结果可以整理成一张常用频率配置表,方便做寄存器映射时直接查。以100MHz系统时钟、32位累加器为例:
| 目标频率 | FCW(十六进制) | 实际频率 | 误差 |
|---|---|---|---|
| 1 kHz | 0x0000A7C6 | 999.992 Hz | -0.008 Hz |
| 1 MHz | 0x028F5C29 | 999999.98 Hz | -0.02 Hz |
| 10 MHz | 0x1999999A | 10000000.005 Hz | +0.005 Hz |
注意这里的误差是理论误差,实际频率还有晶振本身的ppm误差,两者是乘法关系。系统时钟源用50ppm晶振时,1MHz输出会有±0.5Hz的绝对漂移,这比FCW取整误差大一个数量级,所以不需要为了追求FCW精确而牺牲N位宽。
3.3 占空比调节与半桥正负方波输出电路的衔接
如果要的不是50%占空比,用pwm_mode和duty即可。duty的物理含义是“高电平相位区间长度”,占空比百分比 = duty / 2^N。想输出25%占空比,duty设为2^N的四分之一即可。注意duty的调节与fout无关,不会因为改变占空比而改变输出频率,这个特性在各种驱动场景里很实用。比较操作phase_acc < duty会引入一个额外的比较器,逻辑量级和加法器相当,对时序影响不大。
实际电路设计里,方波从FPGA引脚出来后如果要驱动半桥正负方波输出电路,需要考虑两件事:一是FPGA引脚驱动能力有限,需要加推挽驱动器;二是半桥上下管切换时如果存在直通窗口,会有大电流尖峰。DDS方波的边沿由时钟同步,边沿较陡,反而比软件翻转IO的方式更容易触发直通,驱动电路里要留死区时间或使用带死区控制的栅极驱动器。有些项目直接把FPGA的输出经过一个施密特整形门再进半桥,利用整形门的回差电压消除边沿抖动,效果比单纯加大驱动电流更好。
4. 时序约束与外部整形:从FPGA引脚到可用的正负方波
4.1 方波场景的set_output_delay约束怎么写
很多FPGA信号发生器工程的约束文件只写时钟,不写输出延迟,导致外部模拟电路的建立保持时间没有边界,板子调试时相位关系全靠运气。方波输出虽然是单bit信号,但它送给外部电路时同样有时序要求。以100MHz时钟为例,XDC约束写成下面这样:
create_clock -period 10.000 -name sys_clk [get_ports clk] # sq_out 送往外部比较器或电平转换电路 set_output_delay -clock sys_clk -max 6.000 [get_ports sq_out] set_output_delay -clock sys_clk -min 2.000 [get_ports sq_out]约束里的max和min对应外部器件数据要求相对于时钟沿的距离。如果外部是74LVC1G14施密特触发器,传播延迟约3~6ns,再留PCB走线延迟0.5ns,max取6ns、min取2ns是比较常见的起点。这个约束会让Vivado布局布线时把sq_out的输出寄存器尽量靠近IOB,缩短输出路径。如果看到时序报告中这条路径slack为负,优先检查是不是min约束给得太紧。
set_input_delay在信号发生器里用得不多,除非FPGA需要读取外部旋钮或编码器输入。那种场景下要对输入信号先做同步打拍,再谈约束,否则Vivado报的时序分析没有意义。方波信号发生器的核心时序路径就是累加器到输出寄存器再到IOB,约束的重点始终在时钟和输出延迟上。
4.2 推挽、开漏上拉与施密特整形:方波功率输出的选型判断
FPGA引脚输出电流一般只有几毫安到十几毫安,直接驱动继电器或功率MOS管不够。常见做法是FPGA引脚先接推挽缓冲器,再驱动后级。推挽结构的典型器件有74LVC1G34单路缓冲或74LVC245八路缓冲,输出摆率可以到1ns以内,适合100MHz量级的方波。注意推挽输出如果后级是长线,要在源端串一个33欧姆电阻吸收反射,FPGA的IOB内部也有可编程压摆率设置,Vivado中可以通过SLEW属性调成FAST。
开漏上拉在这里的适用场景是电平域转换:FPGA输出3.3V,外部电路需要5V或1.8V逻辑电平,可以把引脚配置成开漏,外面按目标电平接上拉电阻。开漏本身不能主动输出高电平,上升沿由上拉电阻充电决定,所以只适合低速方波,超过1MHz的方波开漏输出会明显变形,建议换成电平转换芯片如TXB0104。判断依据可以看下面这个选型表:
| 外部电路 | 推荐器件类型 | 注意事项 |
|---|---|---|
| 推挽缓冲 | 74LVC1G34 / 74LVC245 | 源端串33Ω电阻抑制反射 |
| 开漏上拉 | FPGA内部OD + 外部上拉 | 只适合1MHz以下低速方波 |
| 差分输出 | LVDS引脚 | 接收端100Ω终端匹配 |
脉冲输出还有一条路是差分输出。FPGA的LVDS引脚输出差分方波,抗干扰能力强,边沿陡,适用于长距离传输或干扰环境。代价是接收端必须接100欧姆差分电阻,普通探头不能直接测,需要差分探头。
4.3 用示波器测方波:带宽不够时看到的不是真实波形
方波测量最容易被误导。一个边沿1ns的方波,要测量它至少要500MHz带宽示波器,200MHz带宽测出来的上升沿会被展宽到约1.8ns,这会让工程师误判驱动能力不足。更隐蔽的是探头地线问题:标配的长地线夹等效电感和探头电容谐振,会在几十MHz到一两百MHz引起振铃,看起来像过冲,实际是测量伪迹。测方波时把探头弹簧地直接拧在测试点附近,或者用差分探头,才是可信读数。
示波器上还有两个参数要看:一是频率计读数,GPIB和大多数示波器频率计用长时计数,会平滑掉DDS的周期抖动,显示值非常稳定;二是打开余晖模式看边沿累积轨迹,能看到一个时钟周期的抖动带。若抖动带的宽度明显大于一个系统时钟周期,就要怀疑复位释放时序或电源纹波,而不是DDS结构本身。
5. ILA验证方波边沿与ODDR输出对齐:最后一步的实测细节
5.1 抓方波信号:直接抓MSB会看到假毛刺
用Vivado的ILA核调试时,别直接抓phase_acc[N-1]或者它产生的组合信号。ILA的采样时钟如果和系统时钟是同一个,触发位置在时钟上升沿,组合逻辑毛刺可能落在采样窗口里,显示成很窄的毛刺或不确定电平。正确做法是抓sq_out这个寄存器输出,或者再单独打一拍生成sq_out_del。ILA会额外消耗片上存储资源,抓取深度设置成1024就够观察频率关系,设太大反而影响布局。
调试顺序建议是:先只抓phase_acc最高位和sq_out,确认信号翻转周期符合FCW设计;再抓使能信号en释放的瞬间,确认没有半个脉冲;最后把ILA从综合网表里移除,用ODDR方案做正式输出。
5.2 常见的三个坑:取整误差、过奈奎斯特、复位释放
FCW取整导致的频率误差在大FCW时比例很小,但如果想产生1Hz这种极低频率,FCW只有0.000几,取整后要么输出0Hz要么输出约23mHz的跳变频率,此时应该提高N位宽到40位,或者改用更低系统时钟。另一个容易出错的地方是fout到fclk/2附近时,MSB输出波形占空比会随机化,表现为频率计读数乱跳,这是欠采样现象,不是代码错误,要用户把目标频率限制在fclk/2以下留足裕量。
复位释放也值得单独检查:FPGA配置完成后rst_n释放、fcw生效、en拉高这三个事件如果不按顺序来,输出第一拍可能不完整。常见做法是软件或状态机先配置fcw和duty,延时几十个时钟周期后再拉高en,把输出使能和参数更新解耦。
5.3 ODDR原语把方波直接送到IOB:抖动最少的一条路径
普通寄存器输出到引脚要经过IOB内部的输入输出级,信号路径较长。Xilinx FPGA上可以用ODDR原语把sq_out直接放到IOB的输出寄存器里,让方波边沿和系统时钟的相位关系更稳定,这是信号发生器设计的常见优化手段。
ODDR #( .DDR_CLK_EDGE("SAME_EDGE"), .INIT(1'b0), .SRTYPE("SYNC") ) u_oddr ( .Q(sq_out_pad), // 直接连到顶层输出端口 .C(clk), .CE(1'b1), .D1(sq_out), .D2(sq_out), .R(1'b0), .S(1'b0) );逻辑说明:D1和D2输入相同,ODDR在时钟上升沿把sq_out送到Q,Q直接面对封装引脚,路径比普通寄存器输出短得多。SAME_EDGE模式保证D1和D2在同一个时钟周期被采样,使两个输入可以保持一致。加了ODDR之后,sq_out_pad的延迟主要由IOB和引脚寄生决定,抖动明显小于逻辑阵列内部布线后的输出,实测差异在几百皮秒到1纳秒量级。
ODDR调通后,如果还想再压一点边沿时间,可以在XDC里给输出引脚加一条SLEW属性,把默认的SLOW改成FAST,配合源端串阻把过冲控制在10%以内。多通道复制dds_square_wave模块时,各模块相位独立自由运行,需要多通道对齐就靠同一时刻拉高en实现,各通道的相位关系由拉高瞬间的phase_acc初值决定,所以上电复位后先让累加器跑一段稳定再拉en,能保证多通道起始位置一致。
本文还有配套的精品资源,点击获取