☰
基于FPGA的DDS信号发生器设计与Verilog实现全流程
2026/10/20 22:57:37 网站建设 项目流程

简介:基于FPGA的DDS信号发生器设计资源,面向FPGA/Verilog学习者与数字系统课程设计、电子竞赛备赛人群,提供频率、幅度和波形均可调的完整工程方案。DDS(直接数字频率合成)利用相位累加与查表方式重建波形,资源内含可运行工程与配套说明,能帮助读者将原理落实到实际设计。资源包共254个文件,压缩后约22MB,其中23个Verilog源文件构成核心逻辑,10个MIF文件提供波形样本,6个MATLAB脚本用于波形生成与验证,另有Quartus工程配置、仿真记录及备份文档,目录结构清晰,便于按模块定位学习。已有10018人学习该资源,适合对照工程代码理解DDS原理、调整频率、幅度和波形参数,并迁移到自己的实验与项目中。

1. 需求与方案定位:为什么从 FPGA 入手做 DDS

做课程设计时,A 同学拿到的是「基于 FPGA 的 DDS 信号发生器(频率、幅度、波形可调)」这个题目,一开始他想的是用单片机加定时器翻转 IO 口输出方波,频率调到几百千赫兹以上波形就明显变形了,更别提输出正弦波和三角波。问题出在「定时器翻转」本质上是用 CPU 中断去拼波形,频率越高 CPU 越忙,波形精度越差。后来他把方案换成 FPGA 加 DDS 架构,用 Verilog 实现相位累加器加波形查找表,才真正把「频率、幅度、波形都可调」这几个要求同时落地。这篇方案面向正在做 FPGA 课设、毕设或者想快速搭建一个可演示信号源的人,从总体架构、Verilog 实现、外围控制到最后的示波器验收,按一套完整可复现的流程走下来。

2. 总体架构与关键器件选型:存储资源与 DAC 如何配合

2.1 DDS 核心原理:相位累加器如何决定输出频率

DDS(直接数字频率合成)的核心思想并不复杂:把一个周期的波形数据预先存到 ROM 里,然后用一个相位累加器去循环寻址。累加器每溢出一次,就相当于走完一个完整的波形周期。输出频率由这个公式决定:

f_out = f_clk × Fcw / 2^N

其中 f_clk 是系统时钟频率,Fcw 是频率控制字(Frequency Control Word),N 是累加器位宽。比如系统时钟 50 MHz,累加器位宽 32 位,想要输出 1 kHz 的正弦波,Fcw ≈ 1e3 × 2^32 / 50e6 ≈ 85899。这个值算出来之后直接写到频率控制字寄存器里,输出频率就定了。频率可调的本质也就是实时改写 Fcw。

Verilog 里相位累加器通常写成这样:

// 相位累加器:每个时钟周期累加一次频率控制字 reg [31:0] phase_acc; always @(posedge clk or negedge rst_n) begin if (!rst_n) phase_acc <= 32'd0; else phase_acc <= phase_acc + freq_word; // freq_word 为外部写入的频率控制字 end

累加器的高位作为 ROM 地址去查波形表。这里有一个很常见的取舍:32 位累加器完全展开需要 4G 深度,显然不现实,所以一般只取高 12 位或高 10 位作为查找表地址,低位截断。相位截断会引入很小的杂散,但对课程设计级别的信号发生器来说完全够用,后面避坑章再细说。

2.2 存储资源规划:波形 ROM 的表项选择

波形查找表是 DDS 的核心存储资源。以 12 位地址、单周期 4096 个采样点为例,每个点用 8 位幅值存储,一个波形表占 4096 × 8 bit = 4 KB。如果同时存正弦波、三角波、方波三种波形,总占用大约 12 KB 块内存,对于主流 FPGA 芯片来说非常宽裕。实际项目中通常用片内 ROM 或者分布式 RAM 实现,因为查表操作需要每个时钟周期都能读出一个数据,片外存储的访问延迟和总线开销会让时序变得不可控。

我一般会在设计里把三种波形分别存在三个独立的 ROM 实例中,而不是塞进一个大 ROM 用地址偏移区分。这样做的原因是波形切换逻辑更简单:三个 ROM 并行读同一份相位地址,输出端用多路选择器选一路,切换瞬间不会出现地址跳变导致的数据毛刺。ROM 初始化用$readmemh加载十六进制系数文件,文件里每行一个采样点的幅值。生成这些系数可以直接用 MATLAB 或 Python 脚本算好,量化成 8 位无符号数后写入 .hex 文件。

// 波形查找表:读取相位地址对应波形幅值 wire [7:0] sine_out, tri_out, square_out; sine_rom u_sine( .clk(clk), .addr(phase_addr), // 12 位相位地址 .data(sine_out) ); tri_rom u_tri( .clk(clk), .addr(phase_addr), .data(tri_out) ); square_rom u_square( .clk(clk), .addr(phase_addr), .data(square_out) );

注意 ROM 读数据有一个时钟周期的延迟,相位地址在上升沿锁存后,输出端得到的是上一个地址对应的数据。这个延迟在 DDS 链路里通常不需要额外补偿,因为后面接的乘法器和寄存器本身也是同步流水线,整体相位延迟是恒定的,不会影响波形形状。

2.3 DAC 与模拟前端:决定输出质量的关键

数字域能算出完美的波形,但最终看到的是 DAC 输出的模拟信号。很多第一次做 DDS 的同学把精力全放在 Verilog 代码上,结果示波器一看输出波形全是台阶状,误以为是设计有问题。实际上这是 DAC 的零阶保持特性造成的固有现象,数字频率越高、每个采样周期内模拟输出保持不变,波形就越接近阶梯波。

DAC 选型上,8 位并行 DAC 是课设最常见的方案,接口简单,直接接 FPGA 的 8 个 IO 引脚即可。但如果追求更好的波形质量,可以考虑 10 位或 12 位并行 DAC。位宽每增加一位,理论信噪比提升约 6 dB,但输出数据位宽也要相应调整。幅度可调功能通常在数字域实现:用乘法器把波形采样值乘以幅度控制字,再送到 DAC。乘法器输出位宽要适当扩展,比如 8 位波形乘 8 位幅度,结果位宽 16 位,取高 8 位或者高 10 位作为 DAC 输入,否则幅度调低时信号细节会被截断丢失。

DAC 后面还需要一级低通滤波器。因为 DDS 输出的阶梯波在频域上包含丰富的镜像频率分量,如果不加滤波,频谱上会看到很多以采样时钟为中心的高频杂散。工程上常用简单的 RC 低通或运放构成的二阶有源低通,截止频率根据最高输出频率来定。如果最高输出频率是 1 MHz,低通截止频率设在 3~5 MHz 左右比较合适,太高了滤不掉镜像,太低了会衰减有用信号。这一级虽然不写 Verilog,但它直接决定了示波器上看到的波形是光滑正弦还是带刺的阶梯波,验收时属于扣分高频区。

3. 核心模块的 Verilog 实现:从相位累加到波形合成

3.1 频率控制字模块:实时更新与预分频处理

频率控制字的更新是「频率可调」的直接落点。系统启动时默认输出 1 kHz 正弦波,此时 freq_word 是一个固定初值。用户按下按键调整频率时,不能直接修改正在累加的 freq_word,否则可能出现相位跳变——这次累加用的是新频率字,下一次又用旧频率字,波形会突然扭曲一下。通常的做法是在按键消抖确认后,先把新频率字写入一个缓存寄存器,再在相位累加器溢出的瞬间加载进去,保证频率切换发生在波形周期的整数倍位置。

// 频率控制字缓存与加载 reg [31:0] freq_word, freq_word_buf; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin freq_word <= 32'd85899; // 默认 1kHz@50MHz, 32bit 累加器 freq_word_buf <= 32'd85899; end else if (freq_update_en) begin freq_word_buf <= new_freq_word; // 按键修改时先写缓存 end else if (phase_acc_overflow) begin freq_word <= freq_word_buf; // 相位溢出时再装载 end end

频率档位设计上,我建议把调整步进跟当前频率联动:低频段每次按一下加 10 Hz,中频段加 100 Hz,高频段加 1 kHz。如果全程固定一个步进,要么低频调不准,要么高频调起来慢得让人失去耐心。这个逻辑用一段简单的组合判断即可实现,根据当前 freq_word 所在区间选择步进值,再送给按键处理模块。

3.2 波形选择与 ROM 地址映射:三种波形共用同一相位地址

波形可调的核心是让三种波形同步读取同一份相位地址,然后由选择信号决定输出哪一路。前面提到的三个独立 ROM 实例,每个 ROM 内部存储的都是 4096 个点。方波和三角波的系数文件不需要真的逐点手写,用脚本生成即可:方波前 2048 个点为高电平、后 2048 个点为低电平;三角波从 0 线性上升到满幅,再对称下降。这里需要注意方波不做任何滤波的话,输出沿会比较陡,在示波器上可能看到轻微的过冲,这属于正常现象,如果做过冲抑制需要额外加输出限幅电路,课设阶段一般不用处理。

波形选择信号通常来自按键或拨码开关,选通逻辑用 case 语句实现:

// 波形选择:根据模式字选通对应波形数据 reg [1:0] wave_sel; // 00:正弦 01:三角 10:方波 reg [7:0] wave_data; always @(*) begin case (wave_sel) 2'd0: wave_data = sine_out; 2'd1: wave_data = tri_out; 2'd2: wave_data = square_out; default: wave_data = sine_out; endcase end

这里的wave_data是组合逻辑输出,后续要接幅度控制模块。组合逻辑路径会引入一定的延迟,如果时序收敛困难,可以在选通后再加一级寄存器打拍,把组合逻辑变成流水线结构。FPGA 开发软件在综合时通常会自动优化这类多路选择器,但手动加一级寄存器能显著降低布线压力,属于花小钱买后期省心的操作。

3.3 幅度控制模块:乘法器实现与精度处理

幅度可调在数字域实现的核心是一个乘法器。波形采样值是 8 位无符号数(0~255),幅度控制字也设计为 8 位(0~255),两者相乘得到 16 位结果。但直接把 16 位结果送到 DAC 是不行的,因为 DAC 输入位宽通常只有 8 位或 10 位。取高 8 位是最简单的做法,但会带来一个问题:当幅度控制字较小时,信号的有效位数被压缩。比如幅度字设为 16(满幅的约 6%),8 位波形值乘以 16 后最大才 4080,取高 8 位等于右移 8 位,很多低幅值数据被截成 0,波形会明显失真。

更合理的做法是动态调整截位策略。幅度控制字小的时候,乘法结果右移更少的位数;幅度控制字大时右移更多位数。但这个逻辑在硬件里做动态移位电路比较繁琐,课设级别的方案我建议直接用 16 位结果的高 10 位送 DAC,并且 DAC 选 10 位输入型号。这样即使幅度字较小,波形细节也保留得更多,整体线性度会好很多。如果条件限制只能用 8 位 DAC,那就把幅度控制字的最小值限制在 16 以上,避免输出波形的量化台阶过于明显。

// 幅度控制:波形数据乘以幅度字,取高 10 位输出到 DAC wire [15:0] mult_result; assign mult_result = wave_data * amp_word; // amp_word 为 8 位幅度控制字 assign dac_data = mult_result[15:6]; // 取高 10 位,输出到 DAC 数据总线

幅度调整的按键逻辑和频率调整类似:短按步进调整,长按连续变化。这里有一个容易忽略的点:幅度控制字为 0 时,DAC 输出固定为 0,但模拟前端如果还有直流偏置电路,输出端可能不是 0 V 而是某个直流电平。所以如果需要「静音」功能,必须同时把 DAC 输出强制拉零,并在模拟前端加隔直电容,否则开关机瞬间会出现幅度跳变,听起来就是「噗」的一声。

4. 外围控制与交互实现:按键、数码管与参数显示

4.1 按键消抖与状态机设计:短按与长按的区分

按键处理看起来简单,却是整套系统里最容易翻车的地方。物理按键在按下和释放的瞬间会产生约 10~20 ms 的机械抖动,如果不做消抖,一次按下可能被识别成多次触发,频率会「跳」好几个档位。常用的消抖方法有两种:一种是延时采样,检测到电平变化后等待 20 ms 再读一次,确认状态稳定;另一种是用计数器持续采样,连续 N 次读到相同电平才认为按键状态有效。第二种更符合 FPGA 的并行思维,代码也不复杂。

// 按键消抖:连续 20ms 采样稳定电平 reg [19:0] cnt_key; reg key_stable; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin cnt_key <= 20'd0; key_stable <= 1'b1; // 默认高电平(上拉) end else if (key_in != key_stable) begin if (cnt_key == 20'd999999) begin // 20ms @50MHz cnt_key <= 20'd0; key_stable <= key_in; end else cnt_key <= cnt_key + 1'b1; end else cnt_key <= 20'd0; end

短按和长按的区分用另一个计数器实现:消抖后的按键变为有效电平后开始计时,超过 1 秒认为是长按,触发连续步进;小于 1 秒释放则触发一次步进。状态机分为空闲、短按确认、长按触发、释放等待四个状态。这里需要特别注意释放检测,如果释放时不消抖,长按结束瞬间可能误触发一次短按,导致参数多跳一档。这个 bug 很难用逻辑分析仪抓,因为现象是偶发的,但用户在演示时多按一次就会被导师看到。

4.2 参数显示模块:频率值实时换算与数码管驱动

显示部分要解决两个问题:一是把 32 位频率控制字换算成人眼可读的频率值,二是驱动数码管稳定显示。频率值的换算在 FPGA 里直接用除法器实现比较浪费资源,常用的做法是查表法:预先把每个档位的频率字对应表项存到 ROM 里,按键每调一档就查一次表,得到以 Hz 为单位的数值,再转成 BCD 码送显示。这个方案有个好处:频率字和显示值天然同步,不会出现显示值与实际输出不符的情况。

数码管驱动采用动态扫描方式,4 位数码管每 4 ms 刷新一轮,每位点亮约 1 ms。扫描频率不能太低,否则人眼能看出闪烁;也不能太高,否则位选信号切换时的拖影会变明显。实际调的时候,我习惯把扫描时钟做成 1 kHz,每位占空比 25%,显示效果最稳定。段选数据用 case 语句按 BCD 码映射成共阴极七段编码,这是纯组合逻辑,注意在 scan 切换时先关段选再切位选,否则会出现「拖影重影」现象。

振幅显示同理,8 位幅度控制字直接映射成 0~255 的数值,或者按百分比显示更直观。按百分比显示需要在数字域做一次乘除法,比较浪费逻辑资源。折中做法是幅度字按每档 5% 步进设计,档位序号直接乘以 5 就是百分比,硬件上只需一次移位加一次加法,连乘法器都省了。

4.3 参数存储与默认档位:掉电不丢配置

掉电保存参数不是课设的硬性要求,但如果加上这个功能,答辩时是一个很加分的点。实现方案是在 FPGA 内部调用一个 SPI 接口的小容量 EEPROM,上电时读出最后一次保存的频率和幅度配置,写入对应的控制字寄存器。需要注意 EEPROM 写入寿命有限,不能每按一次按键就写一次,应该在参数停止变化 2 秒后再写,或者设计一个「保存」按键,由用户主动触发写入。这个逻辑在 Verilog 里用一个计时器实现:检测到参数寄存器变化后清零计时器,计时到 2 秒且参数没有再变化,则拉高写使能信号。

保存和恢复过程中还有一个容易踩坑的地方:EEPROM 读出的数据是原始二进制值,直接赋给频率控制字没问题,但赋给波形选择信号时需要做合法性检查。如果 EEPROM 里的值因为写入中断变成了非法状态(比如 2'b11),波形选择 case 语句应该能兜底到默认正弦波,而不是产生一个未定义的输出。养成给所有从外部存储恢复的参数加合法性校验的习惯,能避免很多演示现场翻车的情况。

5. 常见问题排查与避坑:从综合报错到输出波形失真

5.1 输出波形全是毛刺或杂散:检查时钟与滤波器

现象:示波器看到的正弦波边缘有密密麻麻的尖峰,或者频谱仪上看到很多与主频率不相关的谱线。原因一般是两个:一是 DAC 输出端没有加低通滤波器,阶梯波的高次镜像分量直接辐射出来;二是系统时钟抖动过大,导致每个采样点的间隔不均匀,波形在时间轴上被「扭曲」。解决:先确认低通滤波器是否接好,截止频率和阻抗是否匹配;再用示波器测一下 FPGA 输出时钟的抖动,如果抖动明显,检查时钟引脚是否用了普通 IO 而非专用时钟引脚,以及时钟源是否足够稳定。

我自己遇到过一种隐蔽情况:DAC 的数据总线在 PCB 上没有做等长处理,8 根数据线长度差了几厘米,导致高 8 位和低 2 位到达 DAC 的时间相差几个纳秒,输出波形出现周期性毛刺。排查方法是用示波器对比各路数据的建立保持时间,或者干脆把 DAC 输入寄存器放到 FPGA 内部,让数据在 FPGA 内打一拍再输出,这样总线的时序关系就统一了。

5.2 ROM 初始化导致波形输出恒为 0:$readmemh 路径问题

现象:仿真时波形正常,上板后输出恒为 0 V,或者输出波形明显不对称。原因:$readmemh加载的 .hex 文件路径在综合时写的是相对路径,综合工具找不到文件就默认初始化全 0,导致波形表全是零。这个坑极其常见,因为仿真器和综合器对相对路径的解析规则不一样。解决:把 .hex 文件放到工程根目录下的固定文件夹,代码里写绝对路径;或者直接在综合工具里把该文件加入工程资源列表,让工具自动拷贝到综合目录。另外,检查 .hex 文件最后一行是否有多余的空行或非法字符,有些文本编辑器保存时会追加 BOM 头,也会导致加载失败。

更稳妥的做法是在 Verilog 里用initial块配合for循环生成三角波和方波的初始化数据,正弦波因为需要三角函数值所以必须加载外部文件。三角波和方波完全可以通过算法在仿真中直接初始化 ROM,不依赖外部文件路径,这样即使 .hex 加载失败,这两种波形也仍然能工作,故障排查时能缩小问题范围。

5.3 频率显示值与实际输出不符:检查时钟频率和累加器位宽

现象:设置显示 1 kHz,用示波器数周期却发现实际是 998 Hz 或 1003 Hz。原因:公式里用的系统时钟频率和 FPGA 实际工作时钟频率不一致。开发板上的晶振标称 50 MHz,实际可能会有 ±50 ppm 的误差,也就是 25 kHz 的偏差,累加到输出频率上就会产生明显误差。此外,如果用的是 PLL 倍频出来的时钟,PLL 输出频率可能存在几十分之一的舍入误差,这也会直接影响输出频率精度。解决:用示波器或者频率计实测板载时钟的实际频率,以实测值为准重新计算频率控制字;或者在软件上支持频率校准功能,通过测量标准频率调整一个校准因子,写入非易失存储。

5.4 按键切换频率时波形瞬间失真:相位不连续问题

现象:按键调整频率的瞬间,示波器上波形出现一个明显变形的毛刺,随后恢复正常。原因:频率控制字在任意时刻更新,相位累加器没有从标准的相位增量位置继续累加,导致波形相位发生跳变。解决:采用前面提到的缓存寄存器加溢出加载机制,确保频率切换发生在相位累加器自然溢出的时刻。如果切换频率时还需要保持波形相位连续,更严格的做法是改变累加器的步进值的同时,保持相位累加器的当前值不变,这样波形从当前相位点开始以新频率继续走,不会产生跳变。这个方法实现起来只比缓存机制多两行代码,但对正弦波和三角波这类周期连续波形特别有效。

5.5 长按按键频率连续跳变但释放后又多跳一次:释放抖动未处理

现象:长按调频时一切正常,松手后频率又多跳了一档或两档,像是按键「不听使唤」。原因:释放瞬间的机械抖动被当成了新的短按触发。解决:在状态机的释放等待状态中,同样需要做消抖处理,确认释放信号稳定后才能回到空闲状态。另外检查按键按下和释放的判定阈值是否一致,如果不一致会出现迟滞区间,导致短按判定被拉长。这个现象的排查思路和硬件上的施密特触发器类似,逻辑上保持触发和释放的阈值对称即可。

6. 验收方法与答辩技巧:从示波器测量到功能演示

6.1 功能验收清单:逐项核对题目要求

验收环节建议按题目要求拆成一张清单,每项都留出可量化的数据记录。频率可调验证:设定几个标准频率点,用频率计测量实际输出频率,记录误差值,并确认低频段、中频段、高频段都能正常调整;幅度可调验证:设置不同幅度档位,用示波器读出峰峰值,验证线性度;波形可调验证:分别切换正弦波、三角波、方波,观察波形形状是否存在明显失真。每一项的数据记录都要保留在答辩演示文档里,导师通常对「具体数字」比对「演示效果」更敏感。

6.2 答辩演示技巧:先讲设计约束,再展示结果

答辩演示的顺序建议从「为什么选 FPGA」开始,而不是直接展示波形。先说明单片机方案的瓶颈、FPGA 并行架构的优势,再展示 DDS 的相位累加器原理,让导师理解设计思路。演示时先输出默认 1 kHz 正弦波并测量频率,然后按键升频到 10 kHz、100 kHz,让导师观察频率计数值的变化和波形形状;再切换到三角波和方波,最后调幅度,从满幅降到 50% 再降到 10%,展示幅度变化的线性度。全程保持操作节奏稳定,每个调整动作之后停顿两三秒,让导师看清结果。

答辩常见问题之一是关于相位截断误差,提前准备好一个简单的回答思路:截断误差等效于在理想相位上叠加了一个小的扰动,理论上会在输出频谱引入杂散分量,但本设计选用的 12 位地址已经把相位分辨率做到 360/4096 ≈ 0.088 度,杂散幅度远低于 DAC 量化噪声,工程上可以接受。这类问题的应对核心是把设计中的每一个「取舍」都变成「有依据的选择」,导师就会认可这是一份完整的设计而不是拼凑的代码。

6.3 最终检查:从代码规范到上电稳定性

交付前的最后检查我会固定走一遍流程:打开综合报告确认没有未连接的管脚和警告级别的时序违规;用示波器观察输出波形在连续调频 20 次过程中是否有一次失真;长按按键连续调整 30 秒确认显示值和实际输出保持同步;最后断电重启一次,确认系统能恢复到默认状态并且功能正常。这套流程走完,才算一份可以提交的方案。

从那以后我每次交付 FPGA 相关的项目,无论时间多紧都强制自己走一遍「先量化验收指标,再演示功能,最后回归代码规范」的流程。尤其是被问「频率精度是多少」「幅度线性度怎么样」这类问题的时候,手里有实测数据比什么都管用。希望这份从需求到验收的完整方案能帮到你,不管是交课设还是做毕设,按这个链路走下来,你手里的工程就不再是一个只能看现象的演示,而是一份讲得清依据、经得起追问的设计。

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

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

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

立即咨询