基于System Generator的简易DDS设计与实现:图形化FPGA数字信号生成
1. 项目概述:用System Generator实现一个简易DDS
在数字信号处理、通信系统或者测试测量领域,我们经常需要一个能产生特定频率和波形的信号源。过去,这通常意味着要搬出一台笨重的函数发生器。但现在,随着FPGA技术的发展,我们完全可以把一个高度灵活、可编程的信号发生器“塞”进芯片里。DDS,也就是直接数字频率合成技术,就是实现这个想法的核心。它不像传统的模拟振荡器那样依赖电容电感,而是通过数字计算和查表的方式,“合成”出我们想要的波形,精度高、切换快,相位还能连续可调。
这次,我们不写一行传统的HDL代码,而是用Xilinx System Generator这个图形化设计工具,来快速搭建一个简易的DDS。对于算法工程师、系统架构师,或者那些更习惯在Simulink/Matlab环境下思考的硬件开发者来说,System Generator就像一座桥梁,让你能用拖拽模块、连线的方式,直观地构建出最终能在FPGA上运行的硬件系统。我们这次的目标很明确:从原理出发,设计一个能产生正弦波的DDS,然后在System Generator里把它实现出来,最后看看它实际跑起来的效果。你会发现,用这种方式入门FPGA上的DSP设计,门槛低得多,也直观得多。
2. DDS核心原理与System Generator设计思路
2.1 DDS是如何“合成”频率的?
要理解如何在System Generator里搭建DDS,首先得吃透它的工作原理。你可以把DDS想象成一个在圆形跑道上匀速跑步的人,这个跑道就是正弦波的一个完整周期(360度或2π弧度)。这个人手里拿着一张巨大的表格(正弦函数表),表格上按顺序写满了跑道每个位置对应的高度值(正弦波的幅度)。
这个人的跑步速度由两个因素决定:他的步长(频率控制字)和时钟频率。假设时钟每“滴答”一下,他就向前走一步。如果步长是1,那么他需要跑完整整一圈(比如跑完表格的1024个点)才能输出一个完整的正弦波,这时输出频率最低。如果他把步长加大到10,那么时钟每响一下,他就向前蹦10个点,只用原来1/10的时间就跑完了一圈,输出的正弦波频率也就变成了原来的10倍。这个步长,就是DDS的核心参数——频率控制字(Frequency Tuning Word, FTW)。输出频率的计算公式非常直接:F_out = (FTW * F_clk) / 2^N,其中N是相位累加器的位宽。
相位累加器就是这个“跑步的人”,它在一个固定的时钟节拍下,不断地把FTW加到自己的当前位置上。累加的结果(相位值)作为地址,去查询那张巨大的波形表格(查找表,LUT),表格里存储的对应幅值被读取出来,经过数模转换器(DAC)就变成了我们想要的模拟正弦波。在System Generator的纯数字仿真环境里,我们暂时不涉及DAC,关注的是数字幅值序列的正确性。
2.2 为什么选择System Generator来实现?
你可能会问,用VHDL或Verilog写一个DDS也不难,为什么还要用System Generator?这主要取决于你的背景和项目阶段。
对于算法验证和系统级建模来说,System Generator的优势非常明显。首先,直观性。整个数据流、相位累加、查表过程,都以图形化模块和连线的形式呈现,一目了然。你不需要先去理解“进程”、“always块”这些HDL概念,可以直接关注算法本身。其次,与Matlab/Simulink的无缝集成。你可以在同一个环境中,用Simulink丰富的数学和信号处理模块来生成激励、分析输出频谱、计算信噪比,实现算法仿真和硬件实现的闭环验证,效率极高。最后,快速迭代。调整一个参数,比如FTW或者LUT深度,只需要双击模块修改属性,然后重新生成,远比修改和调试HDL代码来得快。
当然,它也有其适用边界。对于追求极致资源利用率、需要复杂控制逻辑、或者要进行芯片级综合优化的最终产品,手写HDL代码可能仍是更优选择。但对于原型验证、教学演示、以及快速构建一个可工作的DDS核心来说,System Generator是一个非常高效的起点。我们这个“简易DDS”项目,正是要利用这些优势,快速走通从原理到实现的完整流程。
2.3 简易DDS的系统架构规划
基于上述原理,我们规划在System Generator中构建如下核心模块链:
- 相位累加器:一个在时钟驱动下不断累加FTW的寄存器。这是DDS的“发动机”。
- 相位截断与查找表地址:相位累加器的输出位宽通常很大(如32位),但我们的波形表不可能有2^32那么大。因此需要截取高几位(例如高10位)作为查找表的地址。这里涉及到一个关键的精度权衡。
- 正弦波形查找表:一个只读存储器,存储了一个或多个完整周期的正弦波数字幅值。地址输入,幅值输出。
- 频率控制字输入:提供一个可配置的常数或端口,用于输入FTW,控制输出频率。
- 时钟与复位:整个系统的同步驱动源。
我们的设计目标是:在System Generator中实例化这些模块,正确连接,并配置合理的位宽和精度参数,最终在仿真中观察到一个纯净的正弦波数字序列。
3. 在System Generator中的详细实现步骤
3.1 环境搭建与基本设置
首先,确保你的机器上安装了Vivado Design Suite(包含System Generator)以及MATLAB/Simulink。建议使用较新的版本以获得更好的兼容性,例如Vivado 2022.1与MATLAB R2022a。启动MATLAB,在命令窗口输入sysgen启动System Generator组件浏览器。然后新建一个Simulink模型(.slx文件)。
在开始拖模块之前,有一项关键设置:定点数(Fixed-Point)数据类型。FPGA内部处理的是定点数,而非浮点数。System Generator默认会使用定点数,我们需要理解其配置。定点数格式通常表示为Fix_N_M,其中N是总位宽,M是整数部分位宽(对于有符号数,包含一位符号位)。例如,Fix_16_14表示一个16位的数,其中1位符号位,13位整数位,2位小数位?不,更常见的解读是:Fix_N_M的M指的是二进制小数点左边的位数(包括符号位)。Fix_16_14意味着总位宽16位,其中14位在二进制小数点左边(1位符号位 + 13位整数位),那么小数部分位宽就是16-14=2位。理解这个对控制数据精度和动态范围至关重要。
注意:在System Generator中,当你从Simulink库拖入一个常数或运算模块时,务必双击模块,在“基本”或“数据类型”选项卡中明确指定其定点数类型。盲目使用默认的“继承”类型,后期很容易出现位宽不匹配或精度丢失的诡异错误。
3.2 核心模块的选型与配置
现在,我们开始搭建核心链路。从System Generator工具库(Xilinx Blockset)中寻找并拖入以下模块:
Gateway In:这是信号从Simulink浮点世界进入System Generator定点硬件世界的“门户”。我们至少需要两个:一个用于输入时钟(
clk),一个用于输入频率控制字(ftw)。对于clk,数据类型通常设为布尔型(Boolean)即可。对于ftw,我们需要根据设计确定其位宽。假设我们相位累加器位宽N_acc为32位,那么ftw也应为32位无符号整数(UFix_32_0)。累加器(Accumulator)模块:在“Basic Elements”库里可以找到。这是实现相位累加的核心。
- 配置:双击模块。在“基本”选项卡,设置“复位类型”为“上升沿”,这样我们可以用复位信号清零。“累加器位宽”设为32(
N_acc)。在“实现”选项卡,确保“流水线”选项被勾选,这会在累加器输出插入寄存器,提高系统时序性能,是FPGA设计的好习惯。 - 连接:它的一个输入接
ftw的Gateway In,另一个add端口悬空(默认为加运算)。clk和rst端口连接到对应的全局时钟和复位源。
- 配置:双击模块。在“基本”选项卡,设置“复位类型”为“上升沿”,这样我们可以用复位信号清零。“累加器位宽”设为32(
切片器(Slice)模块:在“Basic Elements”库里。用于从32位的相位累加值中,截取出高若干位作为查找表地址。
- 配置:假设我们的正弦查找表深度为1024点(地址位宽10位)。我们截取累加器输出的最高位吗?不,应该截取最高有效位。对于相位累加,其输出可以看作一个整数,数值越大代表相位越大。我们取高10位(
[31:22])作为查找表地址。在Slice模块中,设置“位宽”为10,“偏移量”为22(从第22位开始),并选择“从最高位开始偏移”。 - 为什么取高位?因为高位代表了相位累加值中变化最慢、但决定大范围相位的部分,这正是我们需要的粗粒度相位信息。低位代表了相位细小的、快速变化的部分,它们贡献了频率的细微分辨率,但用于查表地址会产生巨大的、不必要的ROM容量开销。舍弃低位的过程称为“相位截断”,它会引入一定的相位噪声,这是DDS输出频谱中杂散成分的主要来源之一。
- 配置:假设我们的正弦查找表深度为1024点(地址位宽10位)。我们截取累加器输出的最高位吗?不,应该截取最高有效位。对于相位累加,其输出可以看作一个整数,数值越大代表相位越大。我们取高10位(
正弦波ROM(ROM)模块:在“Memory”库里找到“Dual Port ROM”或“Single Port ROM”。我们使用单口ROM即可。
- 关键配置——初始化:这是重中之重。双击ROM模块,在“基本”选项卡设置“地址宽度”为10(对应1024深度)。在“初始化”部分,点击“编辑”按钮,会弹出初始化向量设置。我们需要填入一个包含1024个点的、一个完整周期的正弦波数据向量。
- 生成初始化数据:最方便的方法是使用MATLAB命令预先生成。在MATLAB命令窗口输入:
然后将生成的% 生成一个周期的正弦波样本,幅度归一化到定点数范围 LUT_DEPTH = 1024; n = 0:LUT_DEPTH-1; sin_wave = sin(2*pi*n/LUT_DEPTH); % 浮点数,范围[-1, 1] % 量化为14位有符号定点数(假设输出位宽为14位) OUTPUT_BITS = 14; % 将[-1, 1]映射到有符号定点数范围:[-2^(OUTPUT_BITS-1), 2^(OUTPUT_BITS-1)-1] scale_factor = 2^(OUTPUT_BITS-1) - 1; % 例如14位,则为8191 sin_wave_fixed = round(sin_wave * scale_factor); % 确保数值在范围内 sin_wave_fixed = min(max(sin_wave_fixed, -scale_factor-1), scale_factor); % 转换为System Generator需要的格式:十进制整数,用空格或逗号分隔 init_vector = mat2str(sin_wave_fixed); % 或者手动复制这个数组sin_wave_fixed数组(例如一个1x1024的矩阵)复制粘贴到ROM的初始化向量框中。同时,在ROM的“输出”选项卡中,将“数据类型”设置为有符号定点数,例如Fix_14_13(14位总宽,13位二进制小数点左侧,即1位符号位,12位整数位,1位小数位?这里需要根据缩放因子调整。更简单的做法是设为Fix_14_0,然后通过Gateway Out后的增益模块来调整幅度)。 - 连接:地址端口(
addr)接Slice模块的输出。
Gateway Out:这是信号从System Generator定点世界返回到Simulink进行观察和分析的“门户”。将ROM的输出连接到Gateway Out模块。
- 配置:在Gateway Out的属性中,你可以选择“将定点数转换为双精度浮点数”,这样在Simulink Scope里看到的波形就是熟悉的浮点值了。
系统时钟与复位:还需要一个System Generator令牌(Token)。从Xilinx Blockset顶层拖入“System Generator”模块到你的模型中。双击它进行系统级配置。
- 配置:在“编译”选项卡,选择你的目标FPGA器件型号(例如xc7z020clg400-1)。设置“综合工具”为Vivado Synthesis,“硬件描述语言”为Verilog或VHDL。在“时钟”选项卡,设置“仿真周期”为你的时钟周期(例如,如果时钟频率是100MHz,周期就是10ns)。这个令牌会自动为你的设计提供全局时钟(
sysgen_clk)和复位(sysgen_rst)信号。你需要将这些信号连接到累加器、ROM等模块的时钟和复位引脚。
- 配置:在“编译”选项卡,选择你的目标FPGA器件型号(例如xc7z020clg400-1)。设置“综合工具”为Vivado Synthesis,“硬件描述语言”为Verilog或VHDL。在“时钟”选项卡,设置“仿真周期”为你的时钟周期(例如,如果时钟频率是100MHz,周期就是10ns)。这个令牌会自动为你的设计提供全局时钟(
3.3 连接与参数化
将上述模块按照“FTW输入 -> 累加器 -> 切片器 -> ROM -> 输出”的顺序连接起来。别忘了连接所有的时钟和复位线。一个良好的习惯是使用信号线命名和Goto/From标签来保持图纸清晰。例如,将全局时钟线命名为clk,复位线命名为rst。
为了灵活测试,我们可以将FTW输入模块(Gateway In)的常数值设置为一个参数。具体做法是:在MATLAB工作区定义一个变量,比如FTW_VALUE = 42949673;(这个值对应输出频率约为(FTW_VALUE * 100e6) / 2^32 ≈ 1.0 MHz,假设时钟为100MHz)。然后在Gateway In的常量值栏中填入FTW_VALUE。这样,只需在命令窗口改变FTW_VALUE并重新运行仿真,就能快速改变输出频率。
4. 仿真验证与频谱分析
4.1 搭建仿真测试平台
在System Generator设计外围,我们需要用Simulink搭建一个测试环境。
- 添加Scope:从Simulink库(Sinks)拖入一个“Scope”模块,连接到DDS输出的Gateway Out,用于观察时域波形。
- 添加频谱分析仪:为了更专业地分析输出信号质量,从DSP System Toolbox库中拖入“Spectrum Analyzer”模块。将DDS输出也连接到此模块。
- 设置仿真参数:在Simulink菜单栏,点击“仿真 -> 模型配置参数”。在“求解器”选项中,设置“仿真时间”为足够长以观察多个波形周期(例如0.001秒), “步长”可以设为“自动”。对于定点仿真,建议将“数据类型覆盖”设置为“双精度”,以避免Simulink自身定点运算可能带来的混淆。
- 运行仿真:点击运行按钮。你应该能在Scope中看到一个光滑的数字正弦波。在Spectrum Analyzer中,将“频谱单位”设为“dBm”,分辨率带宽(RBW)设得小一些(如100Hz),应该能看到一个清晰的主频谱峰,以及由于相位截断和幅度量化产生的杂散频率分量。
4.2 关键测试案例与结果分析
让我们进行几组测试,并解读结果:
- 测试1:基础频率输出。设置
FTW_VALUE = 1073741824(即2^30)。根据公式,输出频率应为(2^30 * 100e6) / 2^32 = 25 MHz。在Scope中测量周期应为40ns。频谱分析仪应显示一个干净的25MHz单频信号,其信噪比(SNR)和杂散动态范围(SFDR)取决于我们设计的位宽。 - 测试2:频率切换动态性能。我们可以用Simulink的“Signal Builder”或“Repeating Sequence”模块,产生一个在仿真中途变化的FTW信号(代替常数输入)。观察Scope,你会看到输出正弦波的频率在切换瞬间是相位连续的,这是DDS相比传统PLL的一大优点。
- 测试3:微小频率输出。设置一个很小的FTW,例如
FTW_VALUE = 42950,输出频率约为1kHz。观察时域波形,一个周期内应包含大量采样点,波形非常光滑。这展示了DDS极高的频率分辨率。
实操心得:仿真时,如果发现输出波形是“阶梯状”而非光滑正弦波,首先检查ROM的初始化数据是否正确(是否是一个完整的、平滑的正弦周期)。其次,检查Gateway Out的采样时间设置是否正确,应设置为-1(继承输入采样时间)。最后,确保Simulink Scope的采样时间足够高,能正确显示信号。
4.3 性能评估与优化方向
通过频谱分析仪,我们可以定量评估这个简易DDS的性能。主要关注两个指标:
- 信噪比:主要由输出幅度的量化位数(即ROM输出位宽)决定。理论SNR约为
6.02 * N + 1.76dB,其中N是幅度量化位数。我们用了14位,理论SNR约86dB。实测值会略低,因为还有相位截断噪声。 - 无杂散动态范围:指主信号幅度与最大杂散信号幅度的差值。这主要由相位截断的位数决定。我们截断了低22位,SFDR的理论值大约在
6.02 * PdB左右,其中P是保留的地址位宽(10位),但这是一个简化模型,实际会更复杂。
优化方向:
- 提高SFDR:可以增加查找表深度(用更多地址位),或者采用正弦相位差算法、角度旋转算法等更高级的技术,用较小的ROM实现高精度输出。System Generator库中甚至有现成的“DDS Compiler”IP核,它内部就集成了这些优化算法。
- 提高频率纯度:在ROM输出后添加一个数字滤波器(如FIR滤波器),滤除由于采样和量化产生的高频镜像噪声。这可以在System Generator中直接使用FIR Compiler模块实现。
- 多通道与调制:基于这个核心,可以扩展为多通道DDS(用于I/Q信号生成),或者在相位累加器前加入调制模块,实现调频或调相。
5. 常见问题、调试技巧与硬件协同仿真
5.1 搭建与仿真中的典型问题
即使按照步骤操作,你也可能会遇到一些坑。这里记录几个常见问题及其解决方法:
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 仿真时报错,提示“端口数据类型不匹配” | 模块间连线数据类型(定点格式)不一致。 | 1. 检查每个模块输入输出端口的数据类型(右键点击信号线 -> 信号属性)。 2. 确保上游模块的输出类型与下游模块的输入类型兼容。System Generator的“Gateway In”是源头,其输出类型决定了后续链路的类型。可以在需要转换的地方插入“Convert”(类型转换)模块。 |
| 输出波形为一条直线(零值) | ROM未正确初始化,或地址输入始终为固定值。 | 1. 双击ROM模块,确认初始化向量已正确载入,并且不是全零。 2. 用Scope探头同时观察Slice模块的输出(地址),在仿真运行时看地址是否在循环递增。如果地址不变,检查累加器的 clk和rst连接,以及FTW输入是否非零。 |
| 输出波形幅度异常(饱和或太小) | ROM输出数据的定点格式与后续处理或显示不匹配。 | 1. 检查ROM输出数据类型。如果设为Fix_14_13,其数值范围约为[-4096, 4095]。如果实际正弦波数据范围是[-8191,8191],就会饱和。2. 调整ROM输出的定点数格式,或者在后级Gateway Out之前添加一个“Scale”模块进行归一化。 |
| 仿真速度极慢 | 仿真时间或步长设置不当,或者模型中存在高采样率的模块。 | 1. 在“配置参数”中,将求解器类型改为“定步长”,并指定一个合适的步长(如1e-8秒,即10ns时钟周期)。 2. 确保System Generator令牌中的“仿真周期”与Simulink主步长一致。 |
| 无法生成HDL代码 | System Generator令牌配置错误,或设计中有不支持的Simulink模块。 | 1. 双击System Generator令牌,确认“编译”选项卡中的FPGA部件、综合工具等设置正确。 2. 确保设计中只使用了Xilinx Blockset和少数支持的Simulink模块(如Scope、Constant)。避免使用复杂的Simulink函数模块。 |
5.2 进阶:从仿真到硬件
仿真成功只是第一步。要让DDS在真实的FPGA板上跑起来,还需要以下步骤:
- 生成HDL代码:在System Generator令牌中,点击“生成”按钮。System Generator会自动将你的图形化设计转换为VHDL或Verilog代码,并创建一个Vivado工程。
- 添加I/O约束:在生成的Vivado工程中,你需要为时钟输入、复位输入(如果有)、以及DDS的数字输出信号分配具体的FPGA管脚。这通过编写XDC约束文件完成。
- 输出信号处理:我们的DDS输出是14位数字信号。要变成真正的模拟波形,你需要连接一个外部的DAC芯片。在FPGA端,可能需要根据DAC的接口(如并行、SPI、I2S)编写一个接口控制器模块。你也可以利用FPGA开发板上的音频编解码器(如果支持)来输出低频音频信号进行验证。
- 硬件协同仿真:这是更高级的验证手段。通过JTAG,可以将FPGA硬件纳入Simulink仿真环路。你可以在Simulink中实时改变FTW,并直接从FPGA抓取输出数据回Simulink分析。这需要在System Generator中配置“硬件协同仿真”选项,并连接好硬件平台。
5.3 扩展思考:从简易DDS到实用系统
这个简易DDS是一个完美的教学原型,但离一个 robust 的实用信号源还有距离。在实际项目中,你可能需要考虑:
- 动态配置:通过微处理器(如FPGA内部的MicroBlaze软核,或者外部的ARM Cortex)通过AXI总线动态配置FTW、相位偏移等参数。
- 波形存储与切换:将ROM扩展为双端口RAM,并设计一个波形加载机制,实现任意波形的发生。
- 自动幅度控制:在输出级添加一个数字乘法器(充当数字衰减器/放大器),实现输出幅度的可编程控制。
- 抖动注入技术:为了进一步改善SFDR,可以在相位累加器或ROM地址输入端,有规律地加入一个很小的随机抖动信号,将确定性杂散能量打散成底噪声。
通过System Generator实现这个简易DDS,你收获的不仅仅是一个能用的信号源模块,更重要的是一套在Simulink环境下进行FPGA DSP系统级设计和验证的方法论。它让你能更专注于算法和系统架构,而将繁琐的HDL编码和底层时序问题交给工具去处理,极大地加速了从概念到原型的进程。当你下次需要快速验证一个数字滤波器、一个调制解调方案时,不妨试试把System Generator作为你的第一站。