
1. DDS的工作原理相位累积不是走表查值这么简单DDSDirect Digital Synthesis直接数字频率合成这名字听起来很学术但拆开看就一句话用数字方式控制一个读表的速度从而产生不同频率的信号。很多人理解DDS就是查表输出正弦波这个说法没错但如果只停在查表层面你根本解释不了为什么频率能精确到小数点后几位或者为什么输出会有一个个杂散尖峰。我把DDS的核心工作流程拆成三步来理解相位累加器每个时钟周期把一个频率控制字Frequency Control WordFCW累加到相位寄存器上寄存器的输出就是当前相位。相位到幅度的映射用这个相位去查ROM查找表或者内部用CORDIC算法实时算拿到对应的正弦幅值。数模转换把数字幅值送给DAC变成模拟电压经过滤波之后输出平滑波形。1.1 奈奎斯特的边界DDS的输出频率受奈奎斯特采样定理限制理论上最大输出频率不能超过时钟频率的一半。实际工程里能做到0.4倍f_clk就已经很吃力了。我们市面上见到的那些几百MHz输出的信号发生器内部时钟往往是几GHz量级的靠的是高速DAC配合数字插值这是后话。设计DDS时第一件事就是把应用场景想清楚输出频率范围多少、频率分辨率多少。频率分辨率就是相位累加器位宽的功劳[ \Delta f \frac{f_{clk}}{2^N} ]N是相位累加器位宽。如果你用的时钟是100MHz累加器位宽32位那么频率分辨率就是100MHz / 2^32 ≈ 0.023Hz。这意味着你理论上可以输出0.001Hz这种超低频率还不抖动。这就是DDS对比锁相环方案的一个核心优势——跳频速度快、分辨力变态高、相位连续。1.2 相位截断与杂散直接查表的话查找表深度不能无限大成本摆在那里。一般查找表存2^10或2^12个点就够用了。高位宽的相位累加器输出不能全部作为查表地址只取高10位或高12位低位的相位信息被丢掉了这叫做相位截断。相位截断直接后果就是输出频谱上会出现杂散分量。你说这是坏事吧也不全是。合理设计截断位宽能把杂散压低到DAC本身量化噪声之下那它就不是主要矛盾了。我一般取相位累加器32位、查表地址12位这样截断造成的无杂散动态范围SFDR大约在-72dBc左右配合12位DAC能压到量化噪声水平附近。提示DDS性能瓶颈往往不在算法本身而在DAC的位数和时钟质量。这是很多新手调了半天、换了各种查找表都没改善的关键原因。2. 芯片选型的真实考量为什么我放弃单片机转向FPGA做信号发生器最简单方案是单片机加DAC芯片很多课程设计、毕业设计就这么干。可一旦把指标提上去比如要求两路相位可调的正弦波、调频响应在微秒量级、输出波形不依赖CPU中断处理单片机就吃不消了。核心原因是单片机是顺序执行的而DDS天生适合并行流水线处理。FPGA在这个场景里的不可替代性体现在三个地方2.1 相位连续切换的优势单片机改输出频率要么改定时器分频要么改写DDS相位累加器的值。前者在频率切换瞬间波形相位不连续输出会有毛刺后者虽然可以做到连续但DAC需要CPU不断填数据频率一旦高了CPU就卡死在中断里根本没空干别的。FPGA里频率控制字只是一个寄存器值你把它改了下一个时钟周期的相位累加器就基于新值继续累加波形完全连续没有毛刺。这在扫频、跳频、Chirp信号等应用中非常关键。2.2 多通道与IQ信号需要双通道输出时FPGA只需要例化两个DDS模块共享同一个系统时钟。相位差是固定的初始相位偏移值永远不用软件同步天然就是个相位相干源。我做过一个需要IQ正交信号的项目要求两路正弦波严格相差90度。用MCU实现要命了得精确控制两条DAC输出时序。FPGA里就简单了两个DDS模块一个初相设为0另一个设为0.25×2^N相位差天然就是90度不随频率变化。这一点在模块解调、矢量信号合成上非常有用。2.3 逻辑资源开销一个最基本的DDS模块32位累加器12位查找表寄存器输出在FPGA上只占几十个逻辑单元加一个乘法器一百多号逻辑单元就搞定了一路信号。Spartan-6或者Cyclone IV这种老片子跑几百路DDS一点压力没有这是单片机完全没法比的。选型层面的结论是做桌面仪器级别的信号发生器FPGA是最优解没有之一。当然代价就是开发周期长、门槛高这就是另一回事了。3. 系统架构设计从相位累加器到输出端口下面直接进入正题我以Xilinx Spartan-6 12位DAC比如AD9708为例讲一个完整的可复现设计。这套架构换到Altera或者国产FPGA上思路完全一致。3.1 顶层模块划分整个系统分成四块DDS核、按键与编码器接口、DAC接口、以及一个可选的串口配置模块。DDS核模块的输入输出module dds_core #( parameter PHASE_WIDTH 32, parameter ADDR_WIDTH 12, parameter DATA_WIDTH 12 )( input wire clk, // 系统时钟 input wire rst_n, // 异步复位低有效 input wire [PHASE_WIDTH-1:0] fcw, // 频率控制字 input wire [PHASE_WIDTH-1:0] phase_offset, // 相位偏移 output reg [DATA_WIDTH-1:0] data_out // 幅度输出 );内部的相位累加器实现三行代码的事reg [PHASE_WIDTH-1:0] phase_acc; always (posedge clk or negedge rst_n) begin if (!rst_n) phase_acc 32d0; else phase_acc phase_acc fcw; end3.2 相位累加器复位处理的一个细节锁相环锁定后第一个时钟周期相位累加器应该清零保证波形的起始相位是确定的这个做法特别关键。我在实际项目里加了一个sync_rst信号在PLL锁定后的第一个时钟沿把累加器清零。如果没有这一步每次上电后输出波形相位是随机的后面接示波器触发或者相敏检测就直接出问题。// 相位累加后的输出地址 wire [ADDR_WIDTH-1:0] rom_addr; assign rom_addr phase_acc[PHASE_WIDTH-1:PHASE_WIDTH-ADDR_WIDTH];这里注意我取的是累加器最高12位作为查表地址。累加器溢出正好对应完成一个完整周期。这个位宽截取方式就是DDS的灵魂所在——相位和频率完全解耦改频率就是改步长波形长相与频率无关。3.3 ROM查找表实现ROM用FPGA内部的块RAM来实现初始化文件用.coe或.mif。我习惯用12位地址深度也就是存储4096个点。(* rom_style block *) reg [DATA_WIDTH-1:0] sine_rom [0:4095]; initial begin $readmemh(sine_rom.hex, sine_rom); end always (posedge clk) begin data_out sine_rom[rom_addr]; end这里输出加了寄存器是时序清爽的关键。组合逻辑直接出去接DAC的话时序收敛难而且会有毛刺。片内ROM的读延迟是一个时钟周期输出寄存器再打一拍相位滞后两拍但同一个系统里所有通道都一样不影响一致性。注意ROM初始化文件一定得是十六进制文本格式。用$readmemh是仿真和上板都能用的别用initialcase的大段赋值写法那会让综合工具把资源撑爆。4. 波形ROM的构建细节数据来源比想象中讲究这个环节看着简单其实翻车率高。最容易犯的错拿C语言生成一个正弦表直接取整就写到ROM里了然后发现输出的正弦波底部有台阶或者正负半周不对称。4.1 MATLAB生成12位正弦表的标准姿势12位DAC输出范围是0到4095但实际上DAC不是以0为中心输出的。AD9708是二进制补码输入满量程输出0到20mA电流。所以ROM里的数据实际上应该覆盖整个12位范围再通过后级的运放转成双极性电压。我用MATLAB生成数据N 4096; % 采样点数 bits 12; % DAC位数 depth 12; % ROM地址位宽 % 完整周期正弦波归一化后映射到0~4095 t (0:N-1) / N; sine_wave round((sin(2*pi*t) 1) * (2^bits - 1) / 2); % 写成十六进制文本 fid fopen(sine_rom.hex, w); for i 1:N fprintf(fid, %03X\n, sine_wave(i)); end fclose(fid);这里有个关键点数据峰值的计算要用2^bits - 1而不是2^bits。否则波形最高点会溢出一位DAC输出满量程时出现削波而且波形正半周顶部会被削平。4.2 查找表点数对输出质量的影响查找表点数越多相位截断误差越小但ROM资源开销线性增长。4096点的ROM在一个块RAM里稳稳放下1024点只需要四分之一块。从1024提高到4096输出波形THD能改善大概10dB性价比很高。再往上走收益就不明显了因为主要矛盾已经转移到DAC的量化噪声和时钟抖动。4.3 生成非正弦波形的扩展这个架构最迷人的地方就在这要变成其他波形只要换ROM内容就行硬件一个字不用改。我做三角波就是同一套ROM地址把数据文件替换成三角波数据。方波更简单直接看ROM地址最高位输出全0或全1。我还把RAM和ROM做了个双模式设计正常模式下从ROM读波形数据用户可以通过串口把自定义波形数据写到RAM里然后切换到RAM模式。这样一个设备既能输出标准波形也能变成任意波形发生器。RAM的成本和ROM完全一样在FPGA里都是块RAM切换到任意波形模式后用户下载波形数据然后DDS从RAM读取并循环输出。4.4 带宽与分辨率之间的权衡查找表的位宽也不是越大越好。我试过用14位DAC配上14位深度的查找表结果发现12位数据其实就足够了因为DAC的有效位数ENOB在12位左右位数再高也除了徒增资源开销波形质量看不出什么改善。真正对波形质量影响大的是DAC的建立时间、时钟抖动、电源纹波和输出滤波器的截止特性。5. 频率控制与交互按键调频的实用方案信号发生器必须能调频率这是最基本的交互。我做的控制面板有一路增量编码器加四个按键还有一个1602液晶显示当前频率。5.1 键盘消抖与频率步进的设置逻辑按键消抖用了一个简单的延时扫描法不占用额外定时器资源reg [19:0] debounce_cnt; reg [3:0] key_reg; always (posedge clk) begin if (debounce_cnt 20d999_999) begin key_reg key_in; end else begin debounce_cnt debounce_cnt 1; end end在100MHz时钟下1千万个时钟周期大约是10ms正好掩盖机械按键的抖动窗口。消抖后的按键边沿用于修改频率控制字寄存器。频率控制字的计算是FCW 目标频率 × 2^32 / 系统时钟频率。这里有一个非常容易踩的坑——频率控制字必须是整数而算出来的往往是小数。处理方式一般有两种四舍五入取整或者用累加余数误差。四舍五入方法在频率较低时误差显得很大。比如50MHz时钟、目标1HzFCW约为85.9取整到86实际输出1.001Hz误差看起来不起眼。但目标0.1Hz时FCW为8.59取整到9误差直接到5%。解决方法是控制界面按整数步进设计且步进值设定为使FCW接近整数的值。或者更优雅的方案把控制字拆成整数部分和小数部分用Dithering的方式周期性地对整数部分做扰动实现平均意义上的精确频率。实际工程中只要目标分辨率在0.1Hz以上四舍五入的误差都不至于对应用造成实质影响。5.2 频率切换的实时性DDS有个容易被忽略的优势是频率切换可以在一个时钟周期内完成。但注意实际系统中频率控制字更新到DAC输出会延迟固定周期。FPGA内部没有这个问题因为所有寄存器都是并行更新的。真正影响实时性的是控制界面的处理方式——按键边沿到来时直接修改FCW寄存器硬件DDS立即响应不用经过CPU级的中断调度。这意味着FPGA做扫频信号时扫频步进和驻留时间全程由硬件状态机控制扫描速度可以远远超出人机交互的范畴达到微秒量级。比如用FPGA做FDA雷达信号仿真线性调频信号从10MHz扫到20MHz只需要一个累加器加一个加法器就能实现驻留时间短到几个时钟周期都行。5.3 基于曲线拟合的频率预校正方法液晶显示上看到的频率和实际输出频率之间总是有偏差主要来源是晶振的误差和DAC的建立时间。高精度应用下我在控制界面里加了一个一阶频率校正实际频率 显示频率 × (1 error_ppm / 1e6)error_ppm是实测得到的频率误差用高精度频率计或者示波器测过之后填入。这样校正之后普通有源晶振也能做到优于10ppm的频率精度再往上就要用温补晶振TCXO或者恒温晶振OCXO了信号源整机档次立刻不同。这也是为什么很多商用信号发生器贵在晶振上而不是FPGA上。6. 实测波形与杂散分析那些仿真里看不到的问题仿真跑通了不代表上板能出好波形。我在这套系统上踩过的坑值得单独拿出来讲很多是教科书里永远不会写的。6.1 我碰到的第一个问题是波形底部有毛刺上电一测正弦波干净得很就是每个周期的负半周过零点会蹦出一个窄毛刺。查了半天发现是DAC数据总线和FPGA引脚之间没有做等长约束。数据位之间到达DAC的时间差异导致瞬间出现错误的中间码反映在模拟输出上就是毛刺。解决办法在UCF/XDC文件里给数据总线加OFFSET OUT约束匹配DAC的建立时间和保持时间窗口。同时物理布局上让FPGA的DAC数据引脚尽量走同一层、等长度。这个经验在PCB设计阶段就要考虑到了调试阶段只能改约束效果有限。6.2 相位截断杂散的实测表现在频谱仪上能看到频谱基线抬高、出现成对的杂散峰离主峰越远杂散越大。这就是前面提到的相位截断杂散。我实际测试了一个32位累加器、12位查表地址的系统无杂散动态范围约-70dBc。考虑12位DAC的理论量化噪声是-74dBc这个结果已经接近极限。如果想在同一个FPGA里把SFDR再做高一些可以试试泰勒级数补偿法把相位截断掉的低20位作为泰勒展开的输入对查表结果做一阶误差补偿。我试过一阶补偿SFDR大概能提升8到10dB代价是多一个乘法器加加法器。对资源不敏感的应用这个收益很实惠。6.3 DAC建立时间与更新率不匹配DAC建立时间决定了它能跑多快的更新率。AD9708的建立时间是35ns意味着最高更新率得在20MHz以下才安全。我当时图省事直接用100MHz时钟跑DDS核DAC接口没有做降速处理结果输出波形严重失真像是正弦波上盖了一层随机数。正确做法是在DAC接口模块里做一个分频使能信号控制数据更新率reg [2:0] clk_div; reg dac_clk_en; always (posedge clk) begin clk_div clk_div 1; dac_clk_en (clk_div 0); // 每8个时钟周期更新一次DAC end always (posedge clk) begin if (dac_clk_en) dac_data data_out; end这样DAC实际更新率变成了12.5MHz实测波形好了很多。这个细节很简单但很多第一次做DDS的新手都会在中招之后才回头看DAC数据手册。6.4 输出滤波与阻抗匹配DAC输出是电流型必须接IV转换电路变成电压。这部分属于模拟电路范畴但直接决定信号质量。我用的电路是运放加RC低通滤波截止频率取最高输出频率的1.5倍到2倍。比如最高输出5MHzLPF截止设在10MHz能压掉部分镜像频率对带内信号影响又不大。镜像频率是DDS输出的天然产物很多新手第一次在频谱仪上看到输出频率之外还有一大坨信号会吓一跳其实那是采样时钟的镜像不是故障。处理办法就是用低通滤波器把它压掉压多少由滤波器阶数决定。7. 从单通道正弦波到多波形IQ输出DDS平台的扩展思路做完基本功能之后我开始在这个平台上扩展玩法。DDS架构加FPGA的灵活性决定了这平台的成长空间远超一台普通信号发生器。7.1 双通道相位差可调输出前面说了IQ输出对相位一致性要求极高。我在DDS核的端口上加了一个相位偏移输入通过寄存器配置初相。设置两个DDS核的初相分别为0和0度偏移量就可得到任意相位差的两路输出。实测下来两路之间的相位误差在0.5度以内这个指标用模拟移相电路很难做到。实际用的时候相位偏移寄存器是32位的最小相位分辨率是360度 / 2^32约等于0.000000084度简直夸张。这个精度级别在相控阵、多通道激励场景中非常有用。7.2 调频与线性调频Chirp信号我在DDS核外面包了一个频率扫描状态机让FCW按时间线性递增就能产生线性调频信号。扫描范围、步进、驻留时间都可以配置。FPGA处理这种任务简直游刃有余因为FCW的累加完全是硬件行为不像软件控制那样受中断抖动影响。我还试过把两个FCW分别设置成正弦扫频和负弦扫频让两个DDS模块输出一上一下的对称Chirp这在某些雷达回波模拟场景中很好用。7.3 数字调制ASK、FSK、PSK在DDS核前端加一个调制状态机切换FCW或者切换初相就能实现最基本的数字调制ASK在DDS输出路径上加一个使能门控控制输出幅值的通断FSK两个FCW寄存器用基带数据选择用哪个PSK两个初相寄存器用数据切换初相值这些调制方式本质上是DDS内核的寄存器值切换。重要特点是切换严格同步于一个全局位同步时钟符号切换时刻是确定的不会出现码间串扰。7.4 和PC联调的上位机控制后期我加了串口模块通过UART接收PC下发的配置指令以ASCII格式解析频率、波形、相位、幅度参数。协议自定义一个很轻量的版本FREQ 1.25MHz WAVE SIN PHAS 90 AMPL 0.8V解析之后直接写入寄存器即可。这样就能在PC上用Python脚本批量扫频、自动生成测试激励做自动化测试事半功倍。上位机控制这部分对初学者来说可以晚点再搞先用按键把基本功能跑通再考虑加串口。8. 我的一次实测记录为了写这篇文章我把这套系统重新搭了一遍记录了完整测试过程。硬件是Spartan-6开发板加AD9708子卡测试仪器是100MHz示波器和一台入门级频谱仪。8.1 测试参数系统时钟50MHz有源晶振经FPGA的PLL倍频到100MHz相位累加器32位ROM地址12位4096点DACAD970812位更新率12.5MHz输出滤波器二阶LPF截止10MHz8.2 测试结果目标频率实测频率频率误差实测幅度备注1kHz1.0001kHz0.01%2.50Vpp非常稳100kHz99.98kHz0.02%2.49Vpp正常1MHz0.9995MHz0.05%2.48Vpp开始有跌落5MHz4.997MHz0.06%2.35Vpp带内衰减明显幅度跌落是低通滤波器的滚降特性导致的。如果要求幅度平坦输出就得在输出端加AGC自动增益控制电路或者用数字方式在DDS内核里对输出做幅度预失真补偿。数字补偿做起来方便查表修正每个频率点上的输出增益就行。8.3 频谱纯度在1MHz输出时频谱仪上看二次谐波约-40dBc三次谐波约-46dBc最靠近主峰的相位截断杂散约-68dBc。作为参考普通单片机方案能做到-30dBc的谐波指标已经不错了。FPGA方案能达到这个水平主要得益于相位累加器的长字宽和稳定的时钟。如果想要更好的谐波指标需要从DAC后面的模拟电路入手加有源滤波器或者改用差分输出DAC加变压器合成。这些都是硬件层面的改动DDS逻辑本身已经没有瓶颈了。8.4 一个意外的发现调试时发现当输出频率刚好是采样率的约数时杂散会明显变小。比如更新率12.5MHz时输出500kHz正好是12.5MHz的1/25频谱干净很多。这是因为采样点刚好均匀分布在完整的周期上相位截断误差变成周期性的杂散集中到固定频点上而不是铺散开来。这个现象在做频率规划的时候可以利用一下。另一个体会是晶振质量对输出信号相噪的影响远大于FPGA内部的逻辑设计。我一开始用的普通有源晶振相噪差强人意后来换了一块低抖动晶振频谱仪上主峰明显变尖锐旁瓣能量降低了不少。做高指标DDS先把时钟源做干净这句话值得说三遍。这套DDS系统前前后后改了好几版从最早的纯按键控制到现在的串口控制加任意波形下载逻辑部分始终没伤筋动骨因为DDS内核本身就是一个稳定的基础模块。如果让我给新手一个建议那就是先把单通道正弦波做出来确认能连续调频、波形干净再往上面堆功能。先把地基打牢后面一切扩展都是水到渠成的事。