news 2026/10/5 6:10:05

基于FPGA的DDS信号发生器设计与实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于FPGA的DDS信号发生器设计与实现

1. DDS的工作原理:相位累积不是"走表查值"这么简单

DDS(Direct Digital Synthesis,直接数字频率合成)这名字听起来很学术,但拆开看就一句话:用数字方式控制一个读表的速度,从而产生不同频率的信号。很多人理解DDS就是"查表输出正弦波",这个说法没错,但如果只停在查表层面,你根本解释不了"为什么频率能精确到小数点后几位"或者"为什么输出会有一个个杂散尖峰"。

我把DDS的核心工作流程拆成三步来理解:

  • 相位累加器:每个时钟周期,把一个频率控制字(Frequency Control Word,FCW)累加到相位寄存器上,寄存器的输出就是当前相位。
  • 相位到幅度的映射:用这个相位去查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 <= 32'd0; else phase_acc <= phase_acc + fcw; end

3.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是仿真和上板都能用的,别用initial+case的大段赋值写法,那会让综合工具把资源撑爆。

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 == 20'd999_999) begin key_reg <= key_in; end else begin debounce_cnt <= debounce_cnt + 1; end end

在100MHz时钟下,1千万个时钟周期大约是10ms,正好掩盖机械按键的抖动窗口。消抖后的按键边沿用于修改频率控制字寄存器。

频率控制字的计算是:FCW = 目标频率 × 2^32 / 系统时钟频率。这里有一个非常容易踩的坑——频率控制字必须是整数,而算出来的往往是小数。处理方式一般有两种:四舍五入取整,或者用累加余数误差。四舍五入方法在频率较低时误差显得很大。比如50MHz时钟、目标1Hz,FCW约为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倍。比如最高输出5MHz,LPF截止设在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点)
  • DAC:AD9708,12位,更新率12.5MHz
  • 输出滤波器:二阶LPF,截止10MHz

8.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内核本身就是一个稳定的基础模块。如果让我给新手一个建议,那就是先把单通道正弦波做出来,确认能连续调频、波形干净,再往上面堆功能。先把地基打牢,后面一切扩展都是水到渠成的事。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/5 6:09:23

室内植物养护实验:数据驱动 vs 直觉,如何用传感器量化种菜

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/5 6:09:23

分位数回归原理与Stata实操:从均值视角到全貌视角的异质性分析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/5 6:08:41

瑞芯微RV1126B实战:AI-ISP实现0.01Lux低光彩色成像

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/5 6:08:21

Excel模板自动计算AQI:线性插值与首要污染物识别全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/5 6:07:55

车牌字符识别实战:VOC数据集转YOLO格式与YOLOv8训练全流程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华