简介这是一份基于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{1b0}}; always (posedge clk or negedge rst_n) begin if (!rst_n) phase_acc {N{1b0}}; else phase_acc phase_acc fcw; end always (posedge clk or negedge rst_n) begin if (!rst_n) sq_out 1b0; else if (!en) sq_out 1b0; // 使能关闭时不产生边沿 else if (pwm_mode) sq_out (phase_acc duty) ? 1b1 : 1b0; 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这类中端器件上N32时的累加器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_bits32): return int(round(fout_hz / fclk_hz * (2 ** n_bits))) def actual_freq(fcw, fclk_hz, n_bits32): 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, FCW0x{fcw:08X}, f实际{f_act:10.3f}Hz, 误差{f_act-target:.3f}Hz)上面脚本的运行结果可以整理成一张常用频率配置表方便做寄存器映射时直接查。以100MHz系统时钟、32位累加器为例目标频率FCW十六进制实际频率误差1 kHz0x0000A7C6999.992 Hz-0.008 Hz1 MHz0x028F5C29999999.98 Hz-0.02 Hz10 MHz0x1999999A10000000.005 Hz0.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.5nsmax取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(1b0), .SRTYPE(SYNC) ) u_oddr ( .Q(sq_out_pad), // 直接连到顶层输出端口 .C(clk), .CE(1b1), .D1(sq_out), .D2(sq_out), .R(1b0), .S(1b0) );逻辑说明D1和D2输入相同ODDR在时钟上升沿把sq_out送到QQ直接面对封装引脚路径比普通寄存器输出短得多。SAME_EDGE模式保证D1和D2在同一个时钟周期被采样使两个输入可以保持一致。加了ODDR之后sq_out_pad的延迟主要由IOB和引脚寄生决定抖动明显小于逻辑阵列内部布线后的输出实测差异在几百皮秒到1纳秒量级。ODDR调通后如果还想再压一点边沿时间可以在XDC里给输出引脚加一条SLEW属性把默认的SLOW改成FAST配合源端串阻把过冲控制在10%以内。多通道复制dds_square_wave模块时各模块相位独立自由运行需要多通道对齐就靠同一时刻拉高en实现各通道的相位关系由拉高瞬间的phase_acc初值决定所以上电复位后先让累加器跑一段稳定再拉en能保证多通道起始位置一致。本文还有配套的精品资源点击获取