基于DDS原理的正弦波信号发生器设计与实现

📅 2026/7/31 5:27:58
基于DDS原理的正弦波信号发生器设计与实现
1. 项目缘起为什么从DDS开始做信号源做硬件开发或者电子设计的朋友手里总得有个信号源。无论是调试模拟电路、测试ADC性能还是验证通信算法一个稳定、参数可调的正弦波信号都是刚需。市面上的成品信号发生器功能强大但价格不菲对于学生、爱好者或者初创团队来说往往是一笔不小的开销。更重要的是成品仪器是一个“黑盒”你只知道它能输出什么却很难深入理解它“为什么”能这样输出以及其性能的边界在哪里。于是自己动手做一个信号发生器的想法就冒出来了。在众多技术方案中直接数字频率合成DDS以其高频率分辨率、快速频率切换和相位连续可调等优点成为了数字信号源设计的首选核心技术。你可能在各类电赛题目、FPGA学习项目或者开源硬件社区里频繁看到它的身影。DDS听起来高大上但其核心思想却非常直观它就像一个在数字域预先存储好波形数据的“播放器”通过控制“播放速度”和“起始位置”来改变输出波形的频率和相位。这个系列文章我就打算从头开始手把手实现一个基于DDS原理的正弦波形发生器并且要实现幅值、频率、相位三个关键参数的可调。这是第一部分我们将聚焦于理解DDS的核心原理并搭建一个最基础的、固定参数的DDS信号发生器模型。把地基打牢了后续的调参、优化、性能提升才能顺理成章。2. DDS核心原理拆解数字域的“波形播放器”要动手做先得弄明白DDS到底是怎么工作的。我们可以用一个非常生活化的类比来理解想象你要播放一段音乐。波形存储器ROM这相当于你的音乐文件比如一首歌的WAV文件。对于DDS正弦波发生器这个“文件”里存储的是一个完整正弦周期0到2π的幅度采样值。我们提前以很高的精度比如用8位、10位或12位的数字量把这个正弦波“描”出来存进一块只读存储器里。这块ROM就是我们的“正弦波表”。相位累加器这是DDS的“心脏”决定了播放速度。它是一个位宽很宽比如32位的加法器。每个时钟周期它都会累加一个固定的值这个值叫做频率控制字Frequency Tuning Word, FTW。你可以把相位累加器想象成音乐播放器的“播放指针”而FTW就是指针每次向前移动的“步长”。步长越大指针跑得越快遍历完整个波形表对应相位从0到2π所需的时间就越短输出的波形频率自然就越高。相位-幅度转换相位累加器的输出是一个不断增长的相位值比如一个32位的数。但这个值不能直接用来输出波形我们需要把它转换成ROM的地址。通常我们会取相位累加器输出值的高若干位比如高10位作为ROM的查找地址。因为ROM里存储的是整个周期的波形相位值的高位就对应着当前相位在波形周期中的位置。数模转换器DACROM根据地址输出对应的幅度数字量比如一个8位的数。这个数字量需要经过DAC转换成模拟电压才能得到我们最终需要的模拟正弦波。DAC的性能分辨率、建立时间、线性度直接决定了输出波形的质量。这个过程可以用一个简单的公式来概括输出频率F_out与系统时钟频率F_clk、相位累加器位宽N以及频率控制字FTW之间的关系F_out (FTW * F_clk) / (2^N)从这个公式可以看出频率分辨率当FTW1时得到最小输出频率步进即F_clk / (2^N)。N越大分辨率越高。一个32位的相位累加器在100MHz时钟下分辨率可以达到惊人的0.023 Hz这是传统模拟振荡器难以企及的。频率控制只需改变FTW的数值就能立即、连续地改变输出频率且切换相位是连续的没有传统PLL频率合成可能出现的相位突变或失锁问题。相位控制如果在送入ROM查表前给相位累加器的输出加上一个固定的偏移量相位控制字 Phase Offset Word, POW就能直接控制输出波形的初始相位。这是DDS另一个非常强大的特性。注意这里存在一个常见的理解误区。有人会觉得DDS的输出频率可以无限高。实际上根据奈奎斯特采样定理DDS能无失真产生的最高理论频率是F_clk / 2。在实际工程中由于DAC和滤波器性能的限制通常建议输出频率不超过F_clk / 4或F_clk / 3以保证输出波形有较好的纯度。3. 基础架构设计与关键模块实现理解了原理我们就可以开始设计一个最基础的DDS系统了。为了便于理解和仿真我们首先在算法层面例如使用Python或MATLAB进行建模然后再考虑硬件描述语言如Verilog/VHDL的实现。3.1 系统参数定义首先我们需要确定几个核心的系统参数这些参数将直接影响最终的性能和资源消耗。系统时钟频率F_clk假设我们使用一个100MHz的时钟源。这个频率越高理论上能输出的信号频率上限也越高。相位累加器位宽N设为32位。这是一个非常常见的选择能在高时钟频率下提供极高的频率分辨率100MHz / 2^32 ≈ 0.0233 Hz。波形查找表ROM地址位宽A设为10位。这意味着我们的正弦波表有 2^10 1024 个采样点。地址位宽决定了波形的相位精度。10位地址对应360°/1024 ≈ 0.35°的相位分辨率对于大多数应用已经足够。波形幅度数据位宽D设为8位。即ROM输出的幅度值是0到255无符号或-128到127有符号。这决定了输出幅度的量化精度也直接影响后续DAC的选择。频率控制字FTW这是我们的主要调节参数。根据目标输出频率F_out计算FTW round(F_out * 2^N / F_clk)。例如要输出1MHz的正弦波FTW round(1e6 * 2^32 / 100e6) 42949673。3.2 正弦波表ROM的生成ROM的内容是项目的基石。生成波表时有几个细节需要注意量化方式对于8位DAC如果它接受的是无符号数据0-255对应0-Vref电压那么我们的波表数据应该映射到[0, 255]区间。公式为data round(127.5 * sin(phase) 127.5)。如果DAC接受有符号补码数据则应映射到[-128, 127]。存储格式在硬件描述语言中我们通常用一个数组或case语句来初始化ROM。在Python建模时我们可以直接用列表或数组存储。优化技巧得益于正弦波的对称性我们实际上只需要存储1/4周期0到π/2的数据然后通过地址映射来还原整个周期这样可以节省大量的存储资源。这对于FPGA等资源受限的平台尤其重要。不过在第一版中为了逻辑清晰我们可以先实现全周期存储。下面是一个用Python生成全周期正弦波表的示例代码它清晰地展示了从相位到幅度的映射过程import numpy as np # 系统参数 ROM_ADDR_WIDTH 10 # 地址位宽 ROM_DATA_WIDTH 8 # 数据位宽 ROM_DEPTH 2**ROM_ADDR_WIDTH # 1024 # 生成相位序列从0到2π不包含2π共ROM_DEPTH个点 phases np.linspace(0, 2*np.pi, ROM_DEPTH, endpointFalse) # 生成正弦波幅度范围[-1, 1] sine_wave np.sin(phases) # 量化到无符号8位整数范围 [0, 255] # 先归一化到[0, 1]: (sine_wave 1) / 2 # 再乘以255并取整 sine_wave_quantized np.round((sine_wave 1) / 2 * (2**ROM_DATA_WIDTH - 1)).astype(np.uint8) # 查看前10个数据点 print(相位弧度:, phases[:5]) print(正弦值:, sine_wave[:5]) print(量化值0-255:, sine_wave_quantized[:5]) # 可以将其保存为文件供后续Verilog或C代码初始化使用 # np.savetxt(sine_rom_1024x8.txt, sine_wave_quantized, fmt%d)3.3 相位累加器与查表逻辑这是DDS的数字核心部分。在每个时钟上升沿相位累加器执行一次累加操作。然后取累加结果的高位作为ROM的读取地址。这里有一个关键点相位累加器是N位如32位的但ROM地址是A位如10位。我们通常取相位累加器输出的最高A位phase_acc[N-1:N-A]作为ROM地址。这是因为最高位代表了相位周期中最大跨度的部分用它们做地址相当于把整个32位的相位空间均匀地映射到了1024个点的波表上。这种截断操作会引入微小的相位截断误差这是DDS输出频谱中产生杂散信号的主要来源之一在基础版本中我们可以先接受它。在Python中我们可以这样模拟一个持续运行的DDS核心class SimpleDDS: def __init__(self, f_clk100e6, n_bits32, rom_addr_bits10, rom_data): self.f_clk f_clk self.n_bits n_bits self.rom_addr_bits rom_addr_bits self.rom rom_data # 之前生成的正弦波表 self.phase_acc 0 self.ftw 0 # 频率控制字 def set_frequency(self, f_out): 设置输出频率计算并更新FTW self.ftw int(round(f_out * (2**self.n_bits) / self.f_clk)) print(f目标频率 {f_out/1e6:.3f} MHz, 设置 FTW {self.ftw}) def step(self): 执行一个时钟周期的操作累加相位查表输出 # 相位累加模拟32位无符号溢出 self.phase_acc (self.phase_acc self.ftw) ((1 self.n_bits) - 1) # 取高ROM_ADDR_BITS位作为地址 rom_addr (self.phase_acc (self.n_bits - self.rom_addr_bits)) ((1 self.rom_addr_bits) - 1) # 查表输出幅度 amplitude self.rom[rom_addr] return amplitude # 使用示例 dds SimpleDDS(rom_datasine_wave_quantized) dds.set_frequency(1e6) # 设置输出1MHz # 模拟运行10个时钟周期 output_samples [] for i in range(10): output_samples.append(dds.step()) print(前10个输出幅度值:, output_samples)3.4 从数字到模拟DAC的选择与考量DDS的数字部分输出的是离散的数字序列必须经过DAC才能变为连续的模拟信号。DAC的选择至关重要它直接决定了输出信号的动态范围、信噪比SNR和无杂散动态范围SFDR。分辨率与我们波表的数据位宽匹配这里至少需要8位DAC。分辨率越高输出波形的量化噪声越低波形越光滑。建立时间DAC从收到数字输入到输出稳定模拟电压所需的时间。这个时间必须远小于系统时钟周期否则会严重限制DDS的最高输出频率。例如100MHz时钟对应10ns周期DAC的建立时间最好在1-2ns以内。接口类型并行接口速度最快但占用引脚多串行接口如SPI节省引脚但更新速率可能受限于串行时钟。对于100MHz时钟的8位DDS并行接口是更稳妥的选择。输出类型电流输出型DAC通常需要外接运放构成I-V转换电路电压输出型DAC则更简单。根据后续电路需求选择。实操心得在项目初期可以选用常见的、资料丰富的DAC芯片如ADI的AD97088位并行、TI的DAC082S0858位SPI。对于高速应用要特别关注DAC的数据手册中关于“更新速率”和“建立时间”的参数。一个常见的坑是只看了DAC的分辨率却忽略了其建立时间无法跟上系统时钟导致波形严重失真。4. 系统仿真与性能分析在真正烧录到FPGA或制作PCB之前进行充分的仿真是必不可少的。仿真能帮助我们验证逻辑正确性并初步评估系统性能。4.1 功能仿真验证波形正确性我们可以用Python或MATLAB模拟运行足够多的时钟周期将输出的数字序列绘制成波形图并做FFT分析。import matplotlib.pyplot as plt from scipy.fft import fft, fftfreq # 模拟参数 f_clk 100e6 sim_cycles 5000 # 仿真5000个时钟周期 dds.set_frequency(1e6) # 设置1MHz输出 # 运行仿真 samples [] for _ in range(sim_cycles): samples.append(dds.step()) # 将数字量转换为模拟电压值假设DAC参考电压Vref3.3V vref 3.3 voltage [sample / 255.0 * vref for sample in samples] # 绘制时域波形前200个点 plt.figure(figsize(12, 4)) plt.subplot(1, 2, 1) plt.plot(voltage[:200]) plt.title(DDS Output Waveform (First 200 samples)) plt.xlabel(Clock Cycles) plt.ylabel(Voltage (V)) plt.grid(True) # 绘制频域频谱 plt.subplot(1, 2, 2) N len(voltage) T 1.0 / f_clk # 采样间隔 yf fft(voltage) xf fftfreq(N, T)[:N//2] # 取单边频谱 plt.plot(xf[:N//100], 2.0/N * np.abs(yf[0:N//100])) # 只看低频部分 plt.title(Single-Sided Amplitude Spectrum) plt.xlabel(Frequency (Hz)) plt.ylabel(Amplitude) plt.grid(True) plt.tight_layout() plt.show()通过时域图我们可以看到生成的正弦波形是否连续、光滑。通过频谱图我们可以清晰地看到在1MHz处有一个主峰这就是我们想要的信号。同时我们也能看到频谱底部的噪声基底以及其他频率的杂散信号。这些杂散主要来源于相位截断误差因为我们只用了相位累加器的高10位去寻址丢弃了低22位这引入了周期性误差。幅度量化误差波表数据只有8位精度对正弦波幅度进行了量化。DAC非线性误差这是实际DAC器件固有的缺陷。4.2 关键指标初探SFDR与频率调谐在仿真中我们可以初步估算系统的无杂散动态范围SFDR即主信号幅度与最大杂散信号幅度的比值用dB表示。SFDR是衡量信号发生器纯度的一个核心指标。# 简单的SFDR估算找到主峰后寻找次高峰 spectrum 2.0/N * np.abs(yf[0:N//2]) main_freq_idx np.argmax(spectrum) # 主信号索引 main_amp spectrum[main_freq_idx] # 将主信号附近的区域置零避免主信号影响寻找杂散 mask_width 10 # 屏蔽主频左右10个频点 spectrum[main_freq_idx-mask_width:main_freq_idxmask_width] 0 spur_amp np.max(spectrum) # 最大杂散幅度 sfdr_db 20 * np.log10(main_amp / spur_amp) print(f主信号幅度: {main_amp:.4f} V) print(f最大杂散幅度: {spur_amp:.4f} V) print(f估算SFDR: {sfdr_db:.2f} dB)对于一个8位DDS其理论SFDR主要由量化噪声决定大约在6.02 * N 1.76 ≈ 50 dB附近N8。我们的仿真结果应该与此量级相符。如果远低于此值可能需要检查波表生成或仿真代码是否有误。此外我们可以通过改变FTW来验证频率调谐功能。分别设置FTW对应100kHz、1MHz、10MHz观察输出波形的周期变化以及频谱主峰的移动确认频率控制是否准确、连续。5. 从模型到硬件FPGA实现要点软件模型验证无误后就可以着手用硬件描述语言以Verilog为例进行实现了。这是将想法落地的关键一步。5.1 Verilog模块划分一个典型的DDS顶层模块可能包含以下子模块phase_accumulator.v相位累加器模块。sine_rom.v存储正弦波表的ROM模块。dds_top.v顶层模块例化并连接上述模块可能还包含控制接口如AXI-Lite用于配置FTW。相位累加器模块示例module phase_accumulator #( parameter PHASE_WIDTH 32 )( input wire clk, input wire rst_n, input wire [PHASE_WIDTH-1:0] ftw_i, // 频率控制字输入 output reg [PHASE_WIDTH-1:0] phase_acc_o // 当前相位输出 ); always (posedge clk or negedge rst_n) begin if (!rst_n) begin phase_acc_o {PHASE_WIDTH{1b0}}; end else begin phase_acc_o phase_acc_o ftw_i; // 自动溢出实现周期累加 end end endmodule正弦ROM模块示例使用$readmemh初始化module sine_rom #( parameter ADDR_WIDTH 10, parameter DATA_WIDTH 8, parameter ROM_FILE sine_rom_1024x8.hex // 存储波表数据的文件 )( input wire [ADDR_WIDTH-1:0] addr_i, output reg [DATA_WIDTH-1:0] data_o ); reg [DATA_WIDTH-1:0] rom [0:(1ADDR_WIDTH)-1]; initial begin $readmemh(ROM_FILE, rom); // 从文件加载波表数据 end always (*) begin data_o rom[addr_i]; end endmodule顶层连接module dds_top #( parameter PHASE_WIDTH 32, parameter ROM_ADDR_WIDTH 10, parameter ROM_DATA_WIDTH 8 )( input wire sys_clk, input wire sys_rst_n, input wire [PHASE_WIDTH-1:0] ftw, output wire [ROM_DATA_WIDTH-1:0] dac_data ); wire [PHASE_WIDTH-1:0] phase_acc; wire [ROM_ADDR_WIDTH-1:0] rom_addr; // 实例化相位累加器 phase_accumulator #( .PHASE_WIDTH(PHASE_WIDTH) ) u_phase_accumulator ( .clk(sys_clk), .rst_n(sys_rst_n), .ftw_i(ftw), .phase_acc_o(phase_acc) ); // 取相位累加器的高位作为ROM地址 assign rom_addr phase_acc[PHASE_WIDTH-1:PHASE_WIDTH-ROM_ADDR_WIDTH]; // 实例化正弦ROM sine_rom #( .ADDR_WIDTH(ROM_ADDR_WIDTH), .DATA_WIDTH(ROM_DATA_WIDTH) ) u_sine_rom ( .addr_i(rom_addr), .data_o(dac_data) ); endmodule5.2 综合与实现中的注意事项时序约束必须为系统时钟sys_clk添加正确的时序约束确保工具能优化关键路径从相位累加器到ROM输出以满足100MHz的时钟要求。ROM的实现方式在FPGA中ROM通常会用块存储器Block RAM, BRAM来实现。需要确认综合工具是否成功将你的ROM推断为BRAM因为BRAM的性能和资源利用率远优于用逻辑单元LUT搭建的分布式RAM。资源评估一个32位加法器和一个1024x8的BRAM在主流FPGA上只占用极少资源。这为后续添加幅值控制、相位控制、甚至多通道DDS留下了充足空间。IO规划dac_data输出端口需要分配到FPGA的引脚上并连接到外部DAC芯片的数据线。需要根据DAC的数据手册确认接口电平通常是LVCMOS和时序要求建立/保持时间并在约束文件中设置正确的set_output_delay。5.3 板级调试第一步观察DAC输出将比特流文件下载到FPGA开发板后第一步不是直接测波形而是先用逻辑分析仪或者FPGA的在线逻辑分析仪工具如Xilinx的ILA抓取dac_data总线上的数字信号。你应该能看到一个不断变化的8位数据流。将其以模拟波形的方式显示应该能看到一个初步的正弦波阶梯图。这一步验证了数字逻辑部分工作正常。接下来连接示波器到DAC的模拟输出端。你可能会看到一个阶梯状的正弦波这是正常的因为8位DAC只有256个离散电平。高频毛刺在DAC数字码值跳变时产生这是由于各数据位开关时间不完全同步导致的“闪码”现象。可以在DAC输出后添加一个低通滤波器也称为重构滤波器或抗镜像滤波器来平滑波形滤除高频分量主要是采样时钟及其谐波附近的分量。一个简单的一阶或二阶RC低通滤波器其截止频率设置为略高于你需要的最高输出频率例如10MHz就能显著改善波形质量。这是DDS系统不可或缺的一部分。至此一个最基础的、频率可调的DDS正弦波发生器就已经搭建完成了。我们实现了从原理理解、软件建模、算法仿真到硬件FPGA实现和初步调试的完整流程。输出频率可以通过改变输入给FPGA的ftw寄存器值来精确控制。在下一部分我们将在这个稳定的基础上增加幅值控制和相位控制功能并深入探讨如何优化输出频谱纯度让这个自制的信号源真正变得实用、好用。