1. 从零开始理解DDS:为什么说相位累加器是核心?
如果你玩过FPGA,或者接触过数字信号生成,大概率听说过DDS这个词。全称是直接数字频率合成,听起来挺高大上,但其实它的核心思想非常巧妙,而且硬件实现起来并不复杂,关键就在于一个叫做“相位累加器”的模块。我刚开始接触的时候也觉得有点抽象,但后来在几个实际项目里用上之后,发现它真是生成时钟、产生波形的一把好手,尤其是在需要高精度、快速切换频率的场合,比如通信系统的本振、雷达的信号源,用FPGA来实现DDS比用专门的芯片有时更灵活。
那么,相位累加器到底是个啥?你可以把它想象成一个在数字世界里“匀速跑圈”的运动员。这个“跑道”的长度是固定的,比如我们用32位二进制数来表示,那这个跑道就是从0到(2的32次方减1)这么一个巨大的数字范围。运动员每走一步的步长,就是我们设定的“频率控制字”。他沿着跑道不停地走,每一步都加上这个步长。当他走完一整圈,也就是累加的值超过跑道最大值(我们称之为“溢出”)时,他就回到了起点,然后开始下一圈。这个“跑完一圈所用的时间”,恰恰就对应了我们想要生成的输出信号的周期。你看,一个简单的累加动作,就和频率的生成联系起来了。这就是DDS最精髓的部分:通过控制累加的步长,来精确控制“跑一圈”的速度,从而得到我们想要的任意频率。
为什么非得用FPGA来做这个事呢?因为相位累加的这个过程,本质上就是每个时钟周期做一次加法运算,这简直是FPGA的拿手好戏。FPGA内部的寄存器、加法器资源可以非常高效地完成这个任务,而且整个逻辑是同步的,稳定性和确定性极高。相比之下,如果用软件在CPU上实现,实时性和精度都会大打折扣。所以,虽然DDS有专用的集成电路,但在很多需要高度定制化、或者需要与其他数字逻辑紧密配合的系统里,用FPGA来实现相位累加器为核心的DDS,往往是更优的选择。接下来,我们就掰开揉碎,看看这个“跑圈模型”具体是怎么用代码实现的,以及里面有哪些关键的细节和容易踩的坑。
2. 核心原理拆解:频率控制字与相位累加
理解了“跑圈”的比喻,我们现在来把数学模型和硬件描述语言对应上。这一部分是整个设计的基石,我会结合公式和具体的Verilog代码片段,让你彻底明白每个参数是怎么来的,以及它们如何影响最终输出的信号。
2.1 频率控制字的计算:连接理想与现实的桥梁
首先,我们要明确几个关键参数。假设你的FPGA系统主时钟(也就是相位累加器的参考时钟)频率是 f_clk,单位是赫兹。你想要生成一个频率为 f_out 的输出信号。你用的相位累加器位宽是 N 位(常用32位,我们后面就以32位为例)。那么,那个至关重要的“步长”——频率控制字 F_word,就可以通过下面这个公式计算出来:
F_word = (f_out * 2^N) / f_clk
这个公式是怎么来的呢?我们倒着推一下。相位累加器从0开始,每次加 F_word,加到最大值(2^N - 1)后溢出归零(或归到某个值),这整个过程就是一个周期 T_out(T_out = 1 / f_out)。在这个过程中,累加器一共需要加多少次呢?理想情况下,需要加 (2^N / F_word) 次。而每次加法消耗的时间是一个系统时钟周期 T_clk(T_clk = 1 / f_clk)。所以总时间就是:
T_out = (2^N / F_word) * T_clk
把这个公式变换一下,就得到了上面的 F_word 计算公式。这里有个非常重要的点:计算出来的 F_word 很可能不是整数。比如 f_clk 是100MHz(1e8),f_out 是1MHz(1e6),N=32,那么 F_word = (1e6 * 2^32) / 1e8 ≈ 42949672.96。但我们在FPGA里只能用整数进行累加,所以必须对 F_word 取整,通常是直接取整数部分(截断)或者四舍五入。这个取整操作,就直接决定了你生成频率的实际精度。
频率分辨率是DDS的一个重要指标,它表示输出频率的最小改变量。根据公式,当 F_word 改变1时,输出频率的变化量是 Δf = f_clk / 2^N。对于100MHz时钟和32位累加器,分辨率高达约0.023赫兹!这意味着你可以极其精细地调整输出频率。但是,实际能达到的精度还受限于 F_word 的取整误差。所以,在系统设计时,你需要根据需求权衡累加器位宽 N(位宽越大,分辨率越高,但消耗的逻辑资源也越多)和系统时钟 f_clk。
2.2 相位累加与溢出:方波信号的诞生
有了频率控制字 F_word,接下来就是实现累加过程。在Verilog中,这通常是一个简单的时序逻辑:
reg [31:0] phase_acc; // 32位相位累加器寄存器
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
phase_acc <= 32'd0;
end else begin
phase_acc <= phase_acc + F_word; // 每个时钟周期累加一次
end
end
注意,这里的加法是忽略溢出的,也就是说,当 phase_acc + F_word 的结果超过32位最大值时,高位自动丢弃,只保留低32位。这个“丢弃高位”的过程,就是相位溢出,它标志着一个周期的结束和下一个周期的开始。那么,我们怎么从这个不断累加的 phase_acc 值里,得到一个方波时钟信号呢?
最直观的方法就是利用最高位(第31位,假设位宽32,索引从0开始)。让我们回到“跑圈”模型。把整个累加范围(0 到 2^32-1)想象成一个圆。F_word 决定了我们每次在这个圆上前进的角度。当累加值处于圆的“上半区”(0 到 2^31-1)时,最高位是0;当累加值进入“下半区”(2^31 到 2^32-1)时,最高位是1。由于累加是匀速的,所以在“上半区”和“下半区”停留的时间是相等的,各占半个周期。因此,相位累加器的最高位 phase_acc[31] 本身就是一个占空比为50%的方波信号,其频率正是我们想要的 f_out。
assign clk_out = phase_acc[31]; // 直接输出最高位作为方波
这种方法简单直接,不需要任何比较逻辑,只需一个位选择。但它的输出是寄存器直接驱动的,可能会有毛刺吗?实际上,phase_acc[31] 的变化是与系统时钟 clk 同步的,因为 phase_acc 是在 clk 的上升沿更新的。所以 clk_out 的变化也会对齐到 clk 的边沿,理论上不会产生毛刺。不过,它的占空比严格依赖于累加器是否从0开始,以及 F_word 是否恰好能在整数个时钟周期后让最高位翻转。在某些特定的 F_word 下,可能会有极小的偏差,但对于大多数应用,这已经足够精确。
如果你想更明确地控制,或者需要非50%占空比的信号,也可以用比较器的方式:
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
clk_out_reg <= 1'b0;
end else begin
if (phase_acc < 32'h8000_0000) // 与半周期值比较
clk_out_reg <= 1'b0;
else
clk_out_reg <= 1'b1;
end
end
这种方式逻辑更清晰,但会多用一些比较器资源。在实际工程中,我通常首选直接取最高位的方法,因为它最节省资源,时序也最好。
3. 超越方波:生成正弦波与其他复杂波形
前面我们利用相位累加器的最高位,轻松生成了方波。但DDS的能力远不止于此。它的真正威力在于能够生成任意波形的周期信号,尤其是正弦波,这在通信、音频、测量等领域至关重要。这就引出了DDS的另一个核心部件:波形查找表。
3.1 相位到幅度的转换:查找表的作用
相位累加器的输出 phase_acc,本质上代表的是当前信号周期的“相位”信息,范围从0到(2^N - 1),对应着0度到360度(严格说是接近360度)。要得到正弦波,我们需要将相位信息转换为该相位对应的正弦幅度值。这个转换关系由正弦函数 sin(2π * phase / 2^N) 决定。在FPGA里实时计算正弦函数是不现实的,太消耗资源且速度慢。因此,通用的做法是“预计算+查表”。
我们预先计算好一个正弦波周期内,等间隔相位点所对应的幅度值,并将这些值存储在一个ROM(只读存储器)或用FPGA的Block RAM资源实现的查找表中。这个表就是波形查找表。那么,用 phase_acc 的多少位来作为查找表的地址呢?这里就涉及到一个关键的权衡:查找表深度与幅度精度。
假设我们的相位累加器是32位(N=32)。如果我们用全部32位作为地址,那查找表需要有2^32个条目,这是天文数字,任何FPGA都存不下。实际上,我们并不需要那么高的相位分辨率。通常,我们只取 phase_acc 的高 M 位(例如高12位或高14位)作为查找表地址。这相当于对相位进行了“截断”或“舍入”。取高M位,意味着我们将一个周期的相位圆分成了2^M份。只要M足够大(比如10以上),由相位截断引入的误差就非常小,其表现是在生成的信号中引入非常低水平的相位噪声,而不是频率误差。
查找表存储的幅度值也有位宽,比如8位、10位或12位,这决定了输出波形的幅度分辨率。位宽越宽,波形量化噪声越低,信噪比越好。一个典型的配置可能是:32位相位累加器,取高12位作为地址,查找表深度为4096,存储12位有符号幅度值。这样,我们就能在每个系统时钟周期,根据当前相位,输出一个对应的正弦波幅度样本。
3.2 硬件实现与优化技巧
在Verilog中实现带查找表的DDS,代码结构也很清晰。下面是一个简化的示例框架:
module dds_sine #(
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] F_word, // 频率控制字
output reg [DATA_WIDTH-1:0] sine_out // 正弦波输出
);
// 相位累加器
reg [PHASE_WIDTH-1:0] phase_acc;
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
phase_acc <= 0;
else
phase_acc <= phase_acc + F_word;
end
// 取相位累加器的高位作为查找表地址
wire [ADDR_WIDTH-1:0] lut_addr = phase_acc[PHASE_WIDTH-1:PHASE_WIDTH-ADDR_WIDTH];
// 正弦波查找表ROM
// 这里需要预先初始化一个深度为 2^ADDR_WIDTH,宽度为 DATA_WIDTH 的ROM
// 内容为 sin(2π * i / 2^ADDR_WIDTH) 的量化值
sine_lut_rom your_rom_instance (
.clk(clk),
.addr(lut_addr),
.data(sine_out)
);
endmodule
这里有几个实践中的优化点。第一,关于查找表的内容生成。你可以在Verilog代码里用循环初始化一个数组,但更常见的做法是使用FPGA开发工具(如Vivado、Quartus)的IP核生成器来创建ROM,并提供一个COE或MIF文件,里面存放着预先用MATLAB或Python计算好的正弦波数据。这样更规范,也利于管理。
第二,对称性存储。正弦波在0-π和π-2π区间是反对称的。我们可以只存储0-π(或0-π/2)的波形数据,然后通过地址映射和输出取反来得到完整的周期。这可以将查找表的大小减少到原来的1/2甚至1/4,极大地节省宝贵的Block RAM资源。我在一个资源紧张的项目里就用了四分之一波形存储,通过判断地址高位来对数据进行翻转或取反,效果很好。
第三,如果希望输出的是模拟正弦波,还需要在 sine_out 后面连接一个数模转换器。这时要注意DAC的输入位数和我们的 DATA_WIDTH 匹配,同时要考虑对输出数据进行抖动处理,以改善由于幅度量化带来的谐波失真。
4. 关键参数分析与性能考量
实现了一个基本的DDS之后,我们还需要深入理解几个关键参数如何影响系统性能,这样才能在实际项目中做出合适的设计选择。这些参数之间往往是相互制约的,需要根据具体应用场景来权衡。
4.1 无杂散动态范围与相位截断噪声
DDS输出信号的质量,一个核心的衡量指标是无杂散动态范围。它指的是输出信号主频率的功率与最大杂散频率(非谐波)功率的比值。影响SFDR的主要因素有两个:幅度量化误差和相位截断误差。
幅度量化误差就是我们之前提到的,用有限位宽(如12位)的数字量来表示一个模拟幅度值所带来的误差。这会在输出频谱中引入噪声和谐波。提高幅度位宽可以改善这一点,但会增加查找表大小和后续DAC的复杂度。
相位截断误差则是因为我们只用了相位累加器的高M位去寻址查找表,而丢弃了低(N-M)位。这些被丢弃的低位代表了相位信息的小数部分,它们的周期性变化会引入一种特殊的噪声,称为相位截断噪声。这种噪声在频谱上通常表现为均匀分布的底噪,而不是离散的杂散。增加寻址位宽M可以显著降低相位截断噪声,但同样会增加查找表的大小。一个经验法则是,每增加1位寻址位宽,SFDR大约能改善6dB。
为了更直观地对比不同参数选择的影响,可以参考下表:
| 参数 | 提高该参数的影响 | 带来的代价 |
|---|---|---|
| 相位累加器位宽 (N) | 频率分辨率提高 (Δf = f_clk/2^N),频率控制更精细。 | 累加器逻辑资源消耗略有增加,但对最终输出频谱质量无直接影响。 |
| 查找表寻址位宽 (M) | 相位截断噪声降低,SFDR提高,输出信号频谱更纯净。 | 查找表大小指数增长 (2^M),消耗更多Block RAM或逻辑资源。 |
| 幅度输出位宽 (D) | 幅度量化噪声降低,波形更平滑,SFDR提高。 | 查找表存储量线性增加,后续DAC要求更高,数据吞吐量增大。 |
| 系统时钟频率 (f_clk) | 输出频率范围上限提高(理论最高f_clk/2),频率分辨率绝对值不变但相对更优。 | 对FPGA时序要求变高,功耗增加,可能限制累加器最高工作速度。 |
在实际项目中,我通常会先用MATLAB或Python搭建一个行为级模型,模拟不同N、M、D下的输出频谱,估算SFDR,再结合FPGA的资源报告来决定最终方案。比如,对于一个要求SFDR > 80dB的中频信号生成应用,我可能会选择N=32,M=14,D=14,这样查找表深度是16384,用一块36Kb的Block RAM刚好能存下。
4.2 频率切换速度与频谱纯度
DDS另一个巨大优势是频率切换速度极快。因为频率的改变只需要更新频率控制字 F_word 的寄存器值,在下一个时钟周期,相位累加器就会按照新的步长累加,从而实现近乎瞬时的频率切换。这个特性在跳频通信、频率调制等场景中非常有用。
但是,快速切换也带来了频谱纯度的问题。当 F_word 突然改变时,相位本身可能是不连续的。比如旧频率下相位累加器停在某个值,新频率控制字加载后,从这个相位值开始继续累加,可能会导致输出波形出现一个相位跳变。这个跳变在频域上会带来额外的杂散。为了解决这个问题,有时会引入“相位同步”或“相位连续切换”的机制。一种简单的方法是在改变频率控制字的同时,将相位累加器复位到一个固定值(如0)。但这仍然会引入一个已知的相位跳变。更复杂的方法需要计算新旧频率下的相位差并进行补偿,这需要额外的逻辑。
此外,系统时钟 f_clk 的抖动会直接转移到输出信号上,影响其近端相位噪声。因此,为DDS模块提供一个高质量、低抖动的时钟源至关重要。在PCB设计时,时钟走线需要特别处理,做好隔离和匹配。
5. 实战进阶:从仿真到上板调试
理论分析得再透彻,最终还是要落到实际的代码和电路上。这一部分,我想分享一些从编写RTL代码到最终硬件调试的实战经验,包括一些常见的坑和解决技巧。
5.1 仿真验证:Modelsim中的频谱观察
在写好了Verilog代码之后,第一步一定是进行充分的仿真。除了常规的功能仿真(看波形是否正确翻转),我强烈建议做一次简单的频谱分析仿真,这能提前发现很多设计缺陷。虽然Modelsim不是专业的频谱分析工具,但我们也可以借助它做一些基础检查。
我的方法是:在测试平台中,将DDS输出的数字波形(比如12位有符号数)写入一个文本文件,仿真的时间要足够长,至少采集输出信号的数百个周期。然后,用MATLAB读取这个文本文件,做FFT变换并画频谱图。在MATLAB脚本里,你需要明确几个参数:仿真时采用的系统时钟周期(对应采样间隔 Ts),读取的数据点数,以及DDS设定的输出频率。
通过观察频谱图,你可以清晰地看到:
- 主频位置是否正确:频谱峰值是否出现在你预期的频率点上?
- 谐波分量:由于数字波形的非理想性(特别是方波输出),是否在2倍、3倍频等处有较大的谐波?
- 噪声基底:观察整个频带的噪声水平,这能直观反映相位截断和量化噪声的影响。
- 杂散信号:是否有明显的、非谐波的尖峰?这可能是由相位截断的周期性、或者代码中的某些非理想因素(如数据溢出处理不当)引起的。
我遇到过一种情况,仿真发现频谱在某个非谐波频率上有很大的杂散,排查后发现是在将相位累加器高位赋给输出时,不小心引入了组合逻辑的毛刺,导致在Modelsim的波形上看不到的细微定时问题,在频谱上暴露无遗。后来改成用寄存器打一拍输出,问题就解决了。
5.2 资源利用与时序收敛
把代码综合进FPGA时,要特别关注资源利用率和时序报告。相位累加器是一个宽位加法器(如32位加法),虽然现代FPGA的ALU处理这个很轻松,但如果你在高速时钟下(如300MHz以上)使用,它可能会成为关键路径。如果时序报告显示建立时间违例,通常的优化方法包括:
- 流水线化:将32位加法器拆分成多级流水线。例如,第一级加低16位,第二级加高16位并处理来自低位的进位。这能显著提高最高工作频率。
- 使用FPGA的专用进位链:确保综合器能识别出这是一个累加操作,并映射到FPGA底层高效的进位逻辑结构上。
- 降低工作时钟频率:如果性能允许,这永远是最简单有效的办法。
对于查找表,确保它被正确推断为Block RAM而不是分布式RAM(LUTRAM)。分布式RAM会消耗大量逻辑资源,且容量有限。在代码中,通常通过使用 (* ram_style = "block" *) 这样的综合属性来引导工具。在Vivado中,综合后的报告里可以查看ROM资源是用了Block RAM还是LUT,一定要确认。
5.3 硬件调试与测量陷阱
代码下载到板卡后,真正的挑战才开始。如果你用示波器观察生成的模拟正弦波,可能会发现波形有毛刺、失真或者频率不对。这里有几个排查方向:
首先,检查时钟。用示波器测量一下供给FPGA的晶振时钟是否干净,频率是否准确。如果FPGA内部用了PLL倍频,也要测量PLL输出的时钟质量。
其次,关注DAC环节。如果你的设计包含DAC芯片,那么问题很可能出在这里。检查DAC的参考电压是否稳定,电源是否干净。数字信号输出到DAC的走线是否过長,有没有做好阻抗控制?可以在DAC的模拟输出端加一个简单的RC低通滤波器(称为重构滤波器),滤除由于采样产生的高频镜像噪声。这个滤波器的截止频率需要仔细计算,要高于你需要的最大信号频率,但低于系统时钟频率的一半(奈奎斯特频率)。
有一次我调试时发现正弦波上有规律的台阶状失真,查了很久才发现是DAC的电源纹波太大。后来在DAC的电源引脚增加了去耦电容,并使用了线性稳压电源单独供电,波形立刻变得光滑了。
最后,如果输出的是数字方波信号直接用于驱动其他芯片,要注意电平标准和驱动能力。确保FPGA的IO bank电压配置正确,必要时可以增加外部驱动芯片。同时,也要注意信号完整性,避免长距离传输产生振铃和过冲。
调试是一个需要耐心和系统性的过程,从仿真到上板,每一步的验证都不可或缺。当你最终在频谱仪上看到一个干净、频率准确的正弦波信号时,那种成就感就是对之前所有工作的最好回报。FPGA实现DDS的灵活性就在于,你可以根据遇到的具体问题,随时调整相位累加器的位宽、查找表的内容甚至整个架构,这是使用固定功能芯片所无法比拟的优势。
2万+



被折叠的 条评论
为什么被折叠?



