资讯动态

FPGA正弦波信号发生器Verilog代码:DDS架构与DAC接口实战

发布时间:2026/10/15 16:19:47 来源:尧图企业网站定制
简介这份资源提供基于Verilog的正弦波信号发生器完整工程代码面向数字电路设计初学者与FPGA开发者帮助理解查表法生成波形的实现思路。工程围绕查找表存储正弦采样值配合计数器控制角度递增在时钟驱动下输出连续周期波形涉及模块定义、时序控制、定点量化与仿真验证等关键环节可用于通信、测试测量等场景的波形源设计练习。压缩包共689个文件约4.04MB以dat数据文件、vhd与v源码、cdb与hdb编译数据库、rpt报告及mif存储器初始化文件为主另含qsf约束、sof烧录文件与readme说明完整保留了综合、仿真与实现流程的中间产物。目前已有3242人学习下载。读者可据此掌握查表法建模、角度到二进制转换、量化误差分析及ModelSim仿真验证方法并借助工程文件复现综合与下载流程适合作为Verilog波形生成类设计的实践参考。1. 正弦波信号发生器 Verilog 代码从查找表到 DAC 的完整落地路径很多人第一次在 FPGA 上做正弦波信号发生器都会卡在同一个地方仿真波形漂亮得像教科书一下板子用示波器一看波形要么是阶梯状的毛刺要么频率对不上要么幅度小得可怜。问题往往不在 Verilog 代码本身而在「相位累加器位宽怎么定」「查找表深度取多少」「DAC 接口时序怎么对齐」这几个工程细节上。正弦波信号发生器 Verilog 代码的核心其实就是一个相位累加器加一张正弦查找表再配一个 DAC 输出通道。它适合做音频信号源、锁相环参考、传感器激励、通信基带测试这些场景。下面我按自己实际做过的路径把参数怎么算、代码怎么写、坑在哪一条条讲清楚。2. 相位累加器与频率控制字DDS 架构的参数推导2.1 为什么用 DDS 而不是直接算 sin在 FPGA 里直接调用$sin()或者 CORDIC 迭代当然也能出正弦波但前者综合不了后者资源消耗大、时序收敛麻烦。DDS直接数字频率合成的思路是把「算正弦」变成「查正弦」用一个相位累加器不断累加频率控制字累加结果的高位作为地址去查一张预先存好的正弦表表里存的是量化后的幅度值。这样每个时钟周期只做一次加法和一次查表时序极其干净资源也就是一块 Block RAM 的事。DDS 的输出频率公式是[ f_{out} \frac{f_{clk} \times FCW}{2^N} ]其中 (f_{clk}) 是系统时钟(N) 是相位累加器位宽(FCW) 是频率控制字。这个公式决定了你想要的频率能不能精确得到。比如系统时钟 50 MHz相位累加器 32 位想要 1 kHz 输出那么 (FCW 1000 \times 2^{32} / 50000000 \approx 85899)。实际写代码时这个值一般做成参数或者通过总线配置而不是写死。频率分辨率是 (f_{clk} / 2^N)。32 位累加器在 50 MHz 下分辨率约 0.0116 Hz做音频和一般测试绰绰有余。但位宽不是越大越好累加器每增加一位后面查表地址截取和资源都会跟着变。我一般建议音频段用 24 到 28 位射频段用 32 位再高就要考虑杂散和功耗了。2.2 查找表深度与地址截取的取舍查找表深度直接决定输出波形的谐波失真。假设表深 (2^M)地址取相位累加器的高 (M) 位那么输出波形的一个周期被量化成 (2^M) 个点。表越深阶梯越小THD 越低但消耗的 Block RAM 也越多。常见做法是表深 1024 或 4096。1024 点表在 12 位幅度量化下THD 大约在 -60 dB 量级做一般信号源够用4096 点表能压到 -75 dB 以下适合对频谱纯度有要求的场合。如果资源紧张还可以利用正弦波的对称性只存 1/4 周期的表用地址高位做象限判断和符号翻转这样 1024 点表实际只占 256 个存储单元。地址截取时有一个容易翻车的点不要直接把累加器低位丢掉就完事那样会引入相位截断杂散。如果对杂散敏感可以在截断前加一级相位抖动dither把周期性截断误差打散成噪声。这个技巧在高端信号源里很常见代码里加一个 LFSR 就能实现。2.3 一个可综合的 DDS 核心代码下面这段代码是我常用的 DDS 核心参数化设计相位累加器位宽和查找表地址位宽分开控制方便你按需裁剪。// DDS 正弦波发生器核心 // PHASE_WIDTH: 相位累加器位宽 // ADDR_WIDTH : 查找表地址位宽取相位累加器的高位 // DATA_WIDTH : 输出幅度位宽 module dds_core #( parameter PHASE_WIDTH 32, parameter ADDR_WIDTH 10, parameter DATA_WIDTH 12 )( input wire clk, input wire rst_n, input wire en, input wire [PHASE_WIDTH-1:0] fcw, // 频率控制字 output reg [DATA_WIDTH-1:0] wave_out ); // 相位累加器 reg [PHASE_WIDTH-1:0] phase_acc; always (posedge clk or negedge rst_n) begin if (!rst_n) phase_acc {PHASE_WIDTH{1b0}}; else if (en) phase_acc phase_acc fcw; end // 取高位作为查找表地址 wire [ADDR_WIDTH-1:0] rom_addr phase_acc[PHASE_WIDTH-1 -: ADDR_WIDTH]; // 正弦查找表实际项目中由 ROM IP 或初始化文件提供 reg [DATA_WIDTH-1:0] sin_rom [0:(1ADDR_WIDTH)-1]; initial begin // 这里仅示意实际用 $readmemh 加载 .mem 文件 // sin_rom[0] 12d2048; ... end always (posedge clk or negedge rst_n) begin if (!rst_n) wave_out {DATA_WIDTH{1b0}}; else if (en) wave_out sin_rom[rom_addr]; end endmodule这段代码里phase_acc每个时钟加一次fcw溢出自动回绕这就是 DDS 的自然特性。rom_addr取的是相位累加器的高ADDR_WIDTH位低位被截断。wave_out是同步输出保证时序干净。实际综合时sin_rom会被推断成 Block RAM如果表深小于 64 也可能被推断成分布式 RAM这个由综合工具决定。参数说明PHASE_WIDTH决定频率分辨率ADDR_WIDTH决定波形量化精度DATA_WIDTH要和你的 DAC 位宽匹配。如果 DAC 是 12 位DATA_WIDTH就取 12如果 DAC 是 8 位取 12 反而浪费取 8 或 10 都行。fcw的计算用上面那个公式注意做整数截断时误差不要超过半个 LSB。3. 查找表生成与 DAC 接口从 .mem 文件到板级波形3.1 用 Python 生成正弦表 .mem 文件查找表的内容一般用脚本生成不手写。下面这段 Python 生成一个 1024 点、12 位量化的正弦表输出成 Verilog$readmemh能读的十六进制格式。import math ADDR_WIDTH 10 DATA_WIDTH 12 DEPTH 1 ADDR_WIDTH MAX_VAL (1 DATA_WIDTH) - 1 MID MAX_VAL // 2 with open(sin_rom.mem, w) as f: for i in range(DEPTH): # 正弦值映射到 0 ~ MAX_VAL中心点 MID val MID int((MID - 1) * math.sin(2 * math.pi * i / DEPTH)) # 防止边界溢出 val max(0, min(MAX_VAL, val)) f.write(f{val:03x}\n)这段脚本的关键在MID int((MID - 1) * sin(...))。为什么要减 1因为正弦峰值是 1如果直接乘MID正峰值会到2*MID超出MAX_VAL。减 1 之后输出范围是[1, MAX_VAL-1]留出余量。生成的.mem文件每行一个十六进制数位宽不足三位前面补零$readmemh能正确解析。在 Verilog 里加载这个文件initial begin $readmemh(sin_rom.mem, sin_rom); end注意路径问题。仿真时相对路径是相对于仿真工作目录综合时是相对于工程目录上板时如果用的是 IP 核初始化要在 IP 配置里指定文件。这个路径不一致是新手最常见的翻车点之一仿真过了综合报错找不到文件多半就是路径没对上。3.2 DAC 接口时序并行、SPI 还是 LVDSDAC 接口选型取决于你用的芯片和采样率要求。低速场合几 MHz 以下用并行接口最省事FPGA 直接输出数据加一个时钟沿DAC 在时钟沿采样。中速场合几十 MHz常用 SPI 或者高速串行节省引脚。高速场合上百 MHz就要上 LVDS 或者 JESD204B 这类高速接口了。以常见的并行 DAC 为例接口时序一般是FPGA 在时钟上升沿输出数据DAC 在同一个上升沿或者下一个上升沿采样。这里有一个建立保持时间的坑。如果你的 FPGA 输出时钟和 DAC 采样时钟是同源的但走线延迟不一样数据可能在采样沿附近跳变导致采样错误。解决办法是在 FPGA 输出侧用ODDR原语把时钟和数据对齐输出或者把 DAC 时钟反相 180 度让采样沿落在数据稳定区中间。下面是一个简单的并行 DAC 输出寄存器代码// 并行 DAC 输出接口时钟反相输出以对齐数据 module dac_interface #( parameter DATA_WIDTH 12 )( input wire clk, input wire rst_n, input wire [DATA_WIDTH-1:0] wave_in, output reg [DATA_WIDTH-1:0] dac_data, output wire dac_clk ); always (posedge clk or negedge rst_n) begin if (!rst_n) dac_data {DATA_WIDTH{1b0}}; else dac_data wave_in; end // 时钟反相输出采样沿对准数据稳定区 assign dac_clk ~clk; endmoduledac_data在clk上升沿更新dac_clk反相后下降沿对应clk上升沿DAC 在dac_clk上升沿采样时数据已经稳定了半个周期。这个技巧在低速并行 DAC 上非常实用能省掉一堆时序约束的麻烦。但如果时钟频率很高反相输出的抖动会变大这时候就要老老实实做时序分析和约束。3.3 幅度与偏置的板级调整DAC 输出的是数字量实际波形幅度和偏置由 DAC 的参考电压和外围电路决定。常见做法是 DAC 输出电流型信号经过运放转成电压再加一个偏置电路把双极性波形抬到单极性 ADC 输入范围。如果你发现波形下半部分被削平了多半是偏置没调对或者 DAC 输出范围设置错了。在 Verilog 侧幅度调整可以通过给查找表输出乘一个系数实现但乘法器会消耗 DSP 资源。更省资源的做法是生成多张不同幅度的表用选择器切换。偏置调整则是在输出上加一个常数注意加完之后要饱和处理防止溢出回绕。饱和逻辑很简单// 带饱和的偏置加法 wire [DATA_WIDTH:0] sum {1b0, wave_in} {1b0, offset}; assign wave_out (sum[DATA_WIDTH]) ? {DATA_WIDTH{1b1}} : sum[DATA_WIDTH-1:0];这里用多一位来检测进位进位了就饱和到最大值。这个细节不做波形会在峰值处出现反相尖峰示波器上一眼就能看出来。4. 避坑与排查仿真过、上板翻车的五个血泪经验4.1 波形频率是设定值的两倍或一半现象仿真里频率对上板用示波器测出来正好是设定值的两倍或者一半。原因通常是时钟频率搞错了。比如你按 50 MHz 算的 FCW实际板子上的晶振是 25 MHz或者 PLL 配置成了倍频但代码里没改。还有一种情况是 DAC 采样率和 DDS 时钟不是同一个DDS 输出更新率是 DAC 采样率的一半等效输出频率就变了。解决先用示波器测一下 FPGA 输出时钟的实际频率再反推 FCW。PLL 锁定信号也要确认没锁定时输出频率是不对的。4.2 波形顶部和底部有平顶现象正弦波峰值附近被削平像方波。原因有两个一是查找表量化时幅度范围没留余量正峰值溢出回绕二是 DAC 输出范围设置小于实际数据范围高位被截断。解决检查 Python 生成表时的MID - 1有没有写检查 DAC 配置寄存器里的输出范围是不是满量程。如果是偏置电路的问题用万用表测一下运放输出静态点应该在电源电压的一半附近。4.3 输出有周期性毛刺现象波形上叠加了规律性的尖刺频率和输出频率成整数倍关系。原因多半是相位截断杂散。相位累加器位宽大于查找表地址位宽时低位被直接丢弃截断误差是周期性的反映到频谱上就是杂散。解决加相位抖动或者增大查找表深度让截断误差变小。如果毛刺频率等于系统时钟的分数倍还要检查电源去耦FPGA 开关噪声耦合到 DAC 模拟输出也会出这种问题。4.4 综合报错说查找表推断成锁存器现象综合工具警告sin_rom被推断成 latch 而不是 RAM。原因通常是initial块里只初始化了部分地址或者always块里读地址没有覆盖所有分支。解决确保$readmemh加载完整表或者用 ROM IP 核替代手写数组。手写数组在有些综合工具里行为不一致用厂商提供的 ROM IP 最稳妥初始化文件在 IP 配置界面指定路径问题也一并解决了。4.5 上板后 DAC 无输出现象仿真波形正常上板 DAC 一点信号都没有。原因可能是引脚约束没写对DAC 数据引脚分配到了错误的 Bank或者输出使能信号没拉高。还有一种情况是 DAC 芯片需要配置寄存器才能工作上电后没有初始化。解决先用示波器测 DAC 时钟引脚有没有波形有波形说明 FPGA 侧在工作再测数据引脚有没有跳变没有跳变就查约束文件和引脚分配都有但 DAC 没输出就查 DAC 配置时序用逻辑分析仪抓一下配置接口。5. 进阶技巧用对称性省一半 ROM用抖动换频谱纯度查找表占用的 Block RAM 在低成本 FPGA 上是稀缺资源。一个 1024 点 12 位的表占 12 Kbit差不多是一块 18Kb Block RAM 的 2/3。如果你要做多通道信号源或者表深要加到 4096ROM 就不够用了。这时候可以用正弦波的对称性正弦波在 [0, π/2] 区间内单调递增后面三个象限的值都可以由第一象限推导出来。具体做法是只存 1/4 周期的表地址取相位累加器的高两位做象限判断低位置作为查表地址查出来的值根据象限做符号翻转或者镜像。// 1/4 周期查表 象限还原 wire [1:0] quadrant phase_acc[PHASE_WIDTH-1 -: 2]; wire [ADDR_WIDTH-3:0] sub_addr phase_acc[PHASE_WIDTH-3 -: ADDR_WIDTH-2]; wire [DATA_WIDTH-1:0] quarter_val sin_rom_quarter[sub_addr]; reg [DATA_WIDTH-1:0] full_val; always (*) begin case (quadrant) 2b00: full_val MID quarter_val; // 第一象限 2b01: full_val MID quarter_val; // 第二象限镜像 2b10: full_val MID - quarter_val; // 第三象限 2b11: full_val MID - quarter_val; // 第四象限 default: full_val MID; endcase end这段代码里sin_rom_quarter只存第一象限的递增段深度是原表的 1/4。象限判断用最高两位sub_addr用剩下的高位。注意第二象限和第一象限的查表值是一样的因为正弦在 [0, π] 上关于 π/2 对称第三、四象限取负。这样 ROM 占用直接降到 1/4代价是多了一点组合逻辑在时序上完全不是问题。另一个进阶技巧是相位抖动。相位截断杂散的本质是截断误差的周期性如果你在截断前加一个伪随机数把误差打散成白噪声杂散就会变成底噪虽然总噪声功率没变但频谱上看起来干净很多。实现方法是在相位累加器低位加一个 LFSR 产生的随机数再截断。LFSR 位宽取截断位宽即可多项式选最大长度序列周期足够长就不会引入新的音调。// 简单的 LFSR 相位抖动 reg [7:0] lfsr; always (posedge clk or negedge rst_n) begin if (!rst_n) lfsr 8h01; else lfsr {lfsr[6:0], lfsr[7] ^ lfsr[5] ^ lfsr[4] ^ lfsr[3]}; end wire [PHASE_WIDTH-1:0] phase_dithered phase_acc {24b0, lfsr}; wire [ADDR_WIDTH-1:0] rom_addr phase_dithered[PHASE_WIDTH-1 -: ADDR_WIDTH];这里 LFSR 只加了低 8 位抖动幅度对应截断掉的低位。加完之后再取高位查表截断误差就被随机化了。这个技巧在音频信号源里效果很明显原本能听到的杂散音调会变成轻微的沙沙底噪听感上干净很多。我自己做这类信号源时习惯先把 FCW 和表深在 Excel 里算一遍确认频率分辨率和 THD 满足要求再动手写代码。上板前一定先用仿真跑一遍频谱分析用 FFT 看杂散别等焊好板子才发现指标不够。DAC 接口的时序约束宁严勿松建立保持时间留够余量后面调试能省很多事。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑