ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Vivado中DDS信号发生器完整验证:从仿真到上板ILA实测

Vivado中DDS信号发生器完整验证:从仿真到上板ILA实测 以前我调DDS信号发生器习惯先开Matlab把相位累加器的数学模型敲一遍、看几眼正弦波、再回去写Verilog。后来发现这条路在项目节奏快的时候真不划算Matlab里的模型和FPGA里的定点实现毕竟是两套东西仿真和工程之间隔着一道翻译的工序。现在我基本是在Vivado里直接用Verilog做DDS的完整验证——行为仿真、综合实现、上板ILA抓波形一步到位。这篇文章全程记录我用Vivado给一个DDS信号发生器做全身体检的过程包括DDS核心里相位累加器和查找表怎么搭、testbench怎么写得能看、ILA上板实测怎么抓数据、以及波形质量体检报告里最容易被忽略的杂散与相位截断问题。适合正在学FPGA、想在Vivado里把DDS从仿真做到板级验证的同学也适合想放下Matlab、纯靠FPGA工具链走完整条验证链路的工程师参考。1. 为什么我决定在Vivado里直接验证DDS而不是先跑Matlab1.1 DDS到底在验证什么三层问题DDS信号发生器核心功能就是用参考时钟去合成一个用户想要的频率和波形。但验证一个DDS这件事远比看看能不能出正弦波复杂。我们在实际项目里验证DDS其实是在回答三个层面的问题第一层是功能对错输出频率对不对波形是不是标准正弦跳频的时候相位能不能保持连续第二层是性能好坏谐波多大杂散多大频率准确度和频率稳定度如何信噪比能不能满足后级电路的要求第三层是资源与实时性在FPGA上占多少LUT和BRAM任意跳频指令下发后需要多少个时钟周期才能稳定输出Matlab在回答第一层问题时特别顺手因为它的DDS模型天然是数学化的浮点运算、连续相位、理想正弦。但到了第二层和第三层Matlab模型如果想真正反映FPGA行为就得把定点化、截位、查找表量化、时序延迟全部建进去。这个工作量加起来往往比重写一份Verilog还大。1.2 Vivado仿真与Matlab仿真的本质差异Matlab里的DDS模型相位累加器是用double类型跑的正弦函数调用的是数学库。而FPGA里跑的是固定字长的二进制运算相位累加器会截位、查找表有量化误差、输出位宽也有限。这两者之间天然存在一个鸿沟。举个例子Matlab里你写sin(2pif*t)不管t是多少结果都精确到浮点精度。但Verilog里相位累加器的低十几位一旦被截断输出波形上就会出现细小的相位抖动这个抖动在时域里可能看不太出来在频域里却会表现为一对明显的杂散边带。更关键的差异在于Vivado的行为仿真直接运行你的RTL代码每个寄存器、每根连线都是真实存在的。你可以精确观察某个中间节点在哪个时钟沿翻转可以在时序仿真阶段加入布线延迟可以看到组合逻辑毛刺。这套验证体系虽然上手门槛比Matlab高一点但它的结论最终能直接回答这块FPGA跑起来是不是这么回事而不是数学上模型是不是正确。1.3 一条脱离Matlab的完整验证路径我现在做DDS项目验证流程固定如下在Vivado里用Verilog写好DDS RTL包含相位累加器和查找表编写testbench施加时钟复位和给定的频率控制字行为仿真确认地址跳转、波形数据输出正确综合实现后在板上用ILA接出DDS输出在真实时钟下采样原始数据结合ILA导出的数据做频域分析判断SFDR是否满足需求。整套流程不打开Matlab。如果确实需要生成正弦查找表或做复杂频谱分析我用Python脚本生成coe和hex文件用ILA导出的CSV配Python做FFT。Matlab在我这里彻底退出了DDS调试主链路。Vivado这套工具链已经覆盖了DDS验证所需的大部分环节省掉Matlab转Verilog、Verilog转板上实测的反复翻译过程效率反而更高。2. DDS核心里最关键的两个模块相位累加器和查找表2.1 相位累加器写法与频率控制字计算DDS的结构可以拆成两半一半是相位累加器也叫数控振荡器另一半是相位到幅度的映射也就是查找表。相位累加器的原理不复杂它就是一个在每个时钟周期累加频率控制字FCW的寄存器溢出即完成了2π的循环。下面这段代码是DDS核心模块的典型写法module dds_core #( parameter PHASE_WIDTH 28, parameter DATA_WIDTH 16, parameter ADDR_WIDTH 10 )( input wire clk, input wire rst_n, input wire [PHASE_WIDTH-1:0] fcw, output wire [DATA_WIDTH-1:0] sine_out, output wire [PHASE_WIDTH-1:0] phase ); reg [PHASE_WIDTH-1:0] phase_r; always (posedge clk or negedge rst_n) begin if (!rst_n) phase_r {PHASE_WIDTH{1b0}}; else phase_r phase_r fcw; end assign phase phase_r; // 取相位累加器的高10位作为查找表地址 wire [ADDR_WIDTH-1:0] addr; assign addr phase_r[PHASE_WIDTH-1 -: ADDR_WIDTH]; // 查找表例化 dds_lut #( .DATA_WIDTH(DATA_WIDTH), .ADDR_WIDTH(ADDR_WIDTH) ) u_lut ( .clk (clk), .addr (addr), .data (sine_out) ); endmodule频率控制字的计算是整个DDS设计中最基础也最容易出错的环节。输出频率和FCW的关系是f_out fcw × f_clk / 2^PHASE_WIDTH反过来就是fcw f_out × 2^PHASE_WIDTH / f_clk假设系统时钟50MHz相位累加器28位想输出1MHz正弦fcw 1e6 × 2^28 / 50e6 5,368,709.12在Verilog里fcw只能是整数所以取5368709实际输出频率是5368709 × 50e6 / 268435456 ≈ 999999.98Hz误差小到可以忽略。如果换成1kHz信号fcw 1e3 × 2^28 / 50e6 5368.709取5369。这种低频率、整数的约束下频率误差会被放大但仍在可接受范围内。关键是设计者要有这个概念fcw是整数相位累加器溢出时刻并不是严格等间隔的这种时间量化误差也是DDS杂散的一个来源。2.2 正弦查找表怎么生成才靠谱查找表本质上是把相位信息映射成幅度信息。DDS里最常用的做法是存一个完整正弦周期的定点采样值用相位累加器的高位去寻址。查找表的大小和位宽直接决定波形的质量。假设地址宽度是10位就有1024个采样点输出数据宽度16位每个值用16比特带符号数表示。那么存储容量是1024 × 16bit 2KB对FPGA的BRAM或分布式RAM来说都非常小。在Verilog里加载查找表比较干净的方式是$readmemhmodule dds_lut #( parameter DATA_WIDTH 16, parameter ADDR_WIDTH 10 )( input wire clk, input wire [ADDR_WIDTH-1:0] addr, output reg [DATA_WIDTH-1:0] data ); reg [DATA_WIDTH-1:0] mem [0:(1ADDR_WIDTH)-1]; initial begin $readmemh(sine_1024_16bit.hex, mem); end always (posedge clk) begin data mem[addr]; end endmodule这个sine_1024_16bit.hex文件需要提前生成。我习惯用Python生成因为写起来最直接import math n 1024 with open(sine_1024_16bit.hex, w) as f: for i in range(n): v int(round(32767 * math.sin(2 * math.pi * i / n))) f.write(f{v 0xFFFF:04x}\n)注意这里生成的是十六进制补码形式。32767对应0x7FFF-32768对应0x8000。用正弦幅度32767是为了避开-32768这个不对称点减少量化误差的边界效应。$readmemh在Vivado仿真和综合时都会读取这个hex文件所以路径一定要放对。更稳妥的做法是可以把hex文件放到和RTL文件同一目录或者使用工程的绝对路径。仿真时报readmem错误十有八九是路径问题。2.3 相位截断与输出位宽的关系我在代码里把28位的相位累加器只取了高10位给查找表这就引入了DDS中一个躲不开的现象相位截断Phase Truncation。因为地址总线只有10位所以低18位相位信息在每个时钟周期都被丢掉了。这个动作从时域看就是给真实正弦波叠加了一个周期性的相位误差在频域上就会表现为杂散边带。理论上相位截断造成的杂散抑制水平大约等于6.02 × ADDR_WIDTH dB。也就是说地址位宽10位时理论杂散抑制大约60dB12位大约72dB14位大约84dB。如果你的项目要求SFDR做到80dB以上地址位宽至少14位起步10位的查找表在频谱仪上一定会露馅。这引出一个设计权衡如果把相位累加器的全部28位都作为查找表地址存储量就是2^28 × 16bit 512MB这在FPGA里根本放不下。所以必须截断但怎么截、截多少位是性能和资源的核心博弈点。第三章后面会专门细说杂散问题。3. 给DDS做第一次体检testbench与Vivado仿真的完整流程3.1 一个能用的testbench该写哪些内容Vivado里的行为仿真本质上就是用一段testbench给RTL施加激励然后观察输出。这个testbench写得好不好直接影响体检结论靠不靠谱。我给上面的DDS核心模块配的testbench是这样的timescale 1ns / 1ps module dds_core_tb; reg clk; reg rst_n; reg [27:0] fcw; wire [15:0] sine_out; wire [27:0] phase; initial clk 0; always #10 clk ~clk; // 50MHz initial begin rst_n 0; fcw 28d0; #100; rst_n 1; fcw 28d5368709; // 1MHz 50MHz #500_000; // 仿真500us $finish; end dds_core #( .PHASE_WIDTH(28), .DATA_WIDTH (16), .ADDR_WIDTH (10) ) u_dds ( .clk (clk), .rst_n (rst_n), .fcw (fcw), .sine_out (sine_out), .phase (phase) ); endmodule这段testbench的核心逻辑是把时钟设为20ns周期复位拉低100ns后释放再给一个1MHz对应的频率控制字跑500us。为什么要跑500us因为要看到完整周期的正弦波。1MHz的周期是1us500us能看到500个完整周期足够判断频率是否准确、波形是否连续。如果你只想快速确认功能也可以把fcw设得大一点比如2MHz或5MHz波形周期更短仿真时间可以缩短。但注意fcw改变后要同步改频率期望值不然数周期时会数错。3.2 仿真波形怎么看频率、幅度、相位连续性打开Vivado的Simulation后把sine_out和phase都添加到波形窗口有几个检查点必须逐一确认幅度范围sine_out应在-32767到32767之间摆动最大振幅接近32767。如果峰值明显偏小或者不对称说明查找表生成或读取有问题。周期数量500us内如果输出1MHz信号应当看到约500个完整正弦周期。目测或数周期都行这能粗略判断频率准确度。相位连续性如果中途改变fcwsine_out的波形不应出现断崖式跳变。DDS的核心优势之一就是切换频率时相位连续这会直接体现在波形上。相位累加器phase波形呈现锯齿状从0增长到2^28-1后溢出归零。它的斜率等于fcw可以辅助验证频率控制字的计算。3.3 仿真中容易出现的假结果行为仿真阶段有一个很典型的假正常现象波形看起来完全没问题但不是你要的频率。原因通常是查找表地址和相位累加器的时序没对齐。我在前面的dds_lut模块输出加了寄存器目的是流水线化。但代价是sine_out比phase晚一个时钟周期。如果你把sine_out和phase连到同一个采样点去观察会发现幅度输出落后相位一拍。这在单看sine_out时完全看不出来波形依然光滑只有当你做更复杂的基于相位做解调、或者用ILA抓数据做FFT时这个延迟才会暴露。另一个常见问题是复位信号没拉高输出全是0。有些同学写完testbench后忘记把rst_n从0置成1仿真出来的波形就是一条直线还以为是DDS没有输出。还有一个坑是$readmemh的路径问题。Vivado仿真的工作目录和项目目录不一样如果hex文件路径写错查找表里全是不定态sine_out波形就是一团噪声而且不会报错只在日志里给warning。排查这类问题的方法是直接查看mem数组的内容或者把addr和data连到一起看——如果addr变化正常而data是X态基本就是readmem失败。4. 上板实测用ILA捕捉真实波形把仿真结论拉出来遛遛4.1 在Vivado里添加ILA的两种方式行为仿真通过后DDS只是纸面健康。真正要确认的是它在真实电路上的表现。这个环节我用的是Vivado的ILAIntegrated Logic Analyzer也就是集成逻辑分析仪核相当于把示波器探头做进FPGA里。在Vivado里加ILA有两条路路线一通过综合后的网表加探针。先做综合打开Synthesized Design选中要观察的sine_out或phase信号右键选择Set Up Debug按向导流程操作选择采样时钟和触发信号重新实现工程。这种做法的好处是不改动RTL缺点是后续如果改了代码探针经常要重新设置。路线二在RTL里直接例化ILA IP。在IP Catalog里搜ILA配置好探针数量和数据宽度后生成IP然后在顶层模块里例化ila_0 u_ila ( .clk (clk), .probe0 (sine_out), // 16位DDS输出 .probe1 (phase[27:18]) // 可选观察高10位地址 );我习惯用路线二因为探针和设计逻辑绑定在一起代码版本管理也更清晰。换一台机器、换一个工程重新打开就能看到同样的观察点。4.2 ILA参数设置和触发条件的选择ILA的采样深度和数据位宽是相乘的关系。资源够用的话建议采样深度至少4096这是做FFT的最低可用样本数。如果采样深度只有1024FFT后频率分辨率会非常差很难分辨杂散边带。采样时钟的选择比很多人想象的更重要。ILA的时钟必须和DDS输出信号同源否则你会看到数据在采样窗口里到处抖。如果DDS的时钟是PLL输出的ILA也用这个PLL时钟。这里的关键不是用系统时钟能不能看的问题而是跨时钟域采样会产生亚稳态导致时序分析中的建立保持时间违例抓回来的波形边缘会出现毛刺。触发条件方面最常见的做法是用fcw变化触发。当频率控制字从一个值跳变到另一个值时ILA窗口里既能看到跳变前的输出也能看到跳变后的输出直接评估跳频稳定时间。如果想聚焦观察稳态波形可以设置phase到达某个值作为触发条件这样每次抓到的都是波形同一相位点的前后数据方便做多次平均。4.3 实测波形与仿真对比哪些差异说明了什么把ILA抓到的sine_out用波形窗口展开你会看到一个真实的数字正弦序列。这个序列应该和仿真波形高度一致但仔细观察差异恰恰是体检最有价值的部分。有一次我在某款7系列FPGA上跑50MHz时钟DDSILA抓到的正弦波和仿真波形肉眼几乎无法分辨但导出数据后做FFT发现离主谱线约40kHz处有一个明显的杂散边带幅度只比主频低50多dB。这个杂散在时域波形里完全看不出来这就是为什么DDS验证不能只看时域。后来定位到原因是查找表地址位宽只有10位相位截断引入了周期性杂散。把地址位宽从10位提到12位之后同样条件下杂散降到68dB附近问题解决。这个经历让我养成了固定习惯凡是验证DDS时域波形只能算初筛频域分析才是确诊。5. 体检报告怎么看SFDR、杂散和相位截断的真相5.1 从时域到频域的一次认识升级做DDS验证如果只看示波器或ILA里的时域波形你八成会得出波形挺干净的结论。但真正的体检报告必须看频域。DDS信号发生器的核心性能指标不是波形长得多好看而是它的SFDR也就是无杂散动态范围Spurious-Free Dynamic Range。SFDR的定义是在指定频率范围内最大输出信号幅度与最大杂散分量幅度之比单位是dBc。DAC的SFDR通常能达到80dB以上但很多FPGA直接生成的DDS信号SFDR只有60dB上下。差距往往不在DAC而在FPGA内部DDS的相位截断和查找表量化上。5.2 相位截断杂散的计算和抑制方法相位截断杂散的机理是这样的相位累加器以28位步进但查找表只有10位地址所以相位值被量化为1024个离散档位。每个时钟周期真实相位和量化相位之间的差值构成一个周期性误差序列。这个误差序列虽然不是纯正弦但它的能量集中在某些离散频率点上反映在频谱上就是杂散。理论上相位截断造成的杂散抑制大约是6.02 × ADDR_WIDTH dB所以查找表地址位宽理论杂散抑制常见实测SFDR10位约60dBc55~65dBc12位约72dBc65~75dBc14位约84dBc75~85dBc实测值比理论值差原因是叠加了幅度量化噪声和时钟抖动。抑制相位截断杂散有几种常用招数。最直接的是增加地址位宽把10位提到12位或14位效果立竿见影。缺点是查找表容量翻倍但1024个点变4096个点也就是8KB和32KB的区别现代FPGA完全能承受。第二种是加抖动dithering。在相位进入查找表之前给相位低几位加一个伪随机扰动打破相位误差序列的周期性把离散杂散的能量摊平成底噪。这个做法不增加存储量但会抬高噪底适合对SFDR要求中等、对噪底要求不苛刻的场景。实现方式是用一个LFSR生成随机数叠加到相位累加器输出上// 伪随机数发生器 reg [15:0] lfsr; always (posedge clk or negedge rst_n) begin if (!rst_n) lfsr 16hACE1; else lfsr {lfsr[14:0], lfsr[15] ^ lfsr[13] ^ lfsr[12] ^ lfsr[10]}; end wire [PHASE_WIDTH-1:0] phase_d phase_r {16d0, lfsr[9:0]}; assign addr phase_d[PHASE_WIDTH-1 -: ADDR_WIDTH];第三种是误差反馈Error Feedback把相位截断误差寄存起来在下一个周期补偿回去。这个方案不会抬高噪底但逻辑复杂度比抖动高工程上用得不如前两种多。5.3 查找表幅度量化给SFDR带来的限制除了相位截断查找表的幅度量化也会限制SFDR。查找表输出是16位定点数幅值被量化到2^16个离散电平。理想DAC的量化信噪比SNR约等于6.02 × B 1.76dBB是输出位宽。16位输出理论SNR约98dB这个数量级远高于10位相位截断造成的60dB杂散所以当地址位宽只有10位时SFDR瓶颈在相位截断而非幅度量化。但如果你的地址位宽已经拉到14位、16位相位截断杂散被压到84dB以上这时16位的幅度量化就会成为新的瓶颈。想要SFDR超过90dB查找表输出位宽必须做到18位以上并且后级DAC也不能拖后腿。还需要注意查找表生成时的量化方式。如果用round取整而不是truncation截断量化误差是零均值且不相关的。用Python生成正弦表时round()函数的行为需要确认是四舍五入还是银行家舍入不同语言对负数round的规则不一样。我都是把正弦值放大到32767后用int(v 0.5)这样的正数偏移再截断确保量化误差可控。5.4 实测工程中提高DDS性能的几个做法综合多个项目的工程经验我整理了提高DDS性能的六条实操建议查找表地址位宽最少12位不要为了省BRAM把地址压到10位以下查找表读出的data要打一拍寄存让输出是纯时序逻辑避免组合逻辑毛刺直接进DAC相位累加器保持足够宽度建议24位以上太窄会导致频率分辨率不足如果输出经过DACDAC前一级加一个插值FIR或至少一个低通滤波器衰减带外镜像对SFDR要求高的场合用抖动破坏相位截断误差的周期性跳频应用里fcw更新要做同步处理不要异步改累加器输入否则瞬间可能出现一个错误的相位增量。这些做法单独拎出来都不复杂但它们叠加起来决定了一块板子上DDS方案的最终性能上限。6. 调试DDS时我最常遇到的三个坑6.1 现象输出完全不对先查时钟和复位DDS上板后最常见的现象是sine_out全0或者只是某个固定值。遇到这种问题不要急着怀疑DDS逻辑写错先检查两个最基本的信号时钟和复位。我在一个项目里遇到过PLL未锁定就把DDS使能的情况。PLL输出时钟在锁定前是不稳定的DDS在这种时钟下跑了一小段累加器状态全部乱掉。PLL锁定后时钟虽然正常了但累加器已经在一个随机状态输出波形不是期望的正弦。排查手法很简单在ILA里把PLL的locked信号和DDS的复位信号都接进来观察locked释放和rst_n拉高的先后关系。正确做法是把PLL的locked接到DDS的复位控制逻辑上locked为高后才释放DDS的复位。还有一个隐蔽的坑异步复位释放时如果刚好撞上时钟上升沿会导致触发器进入亚稳态部分寄存器复位成功、部分没有复位成功。稳妥的做法是在DDS内部用同步复位或者在外部先对异步复位做两级同步。6.2 现象波形有毛刺先查位宽和组合逻辑有次我把查找表输出直接综合成组合逻辑也就是data mem[addr]没有打寄存器out在ILA里看起来问题不大但接到高速DAC后输出正弦波上出现很多随机毛刺尤其当addr有多个bit同时翻转时特别明显。原因并不复杂查找表是分布式RAM或BRAM地址跳变时输出数据会有短暂的不稳定区间。波形仿真阶段由于没有加时序延迟组合逻辑毛刺不会体现在行为仿真里但综合实现后布线延时让毛刺变成真实可见的尖峰。解决方式就是把查找表输出加一级流水线寄存器用时钟沿把数据打一拍。代价是输出延迟一个周期但换来的是干净的数字输出这笔交易非常划算。需要留意的是查找表输出打拍之后整个DDS的流水线延迟会变成两个周期如果后级有同步逻辑要统一调整。6.3 现象上板波形和仿真不符合先查ILA采样率还有一个我能记一辈子的坑用系统25MHz时钟去抓DDS的125MHz时钟域信号抓到一屏波形像是被狗啃过的正弦。当时第一反应是DDS代码在综合后出了问题反复查了半天最后发现是ILA采样时钟和DDS时钟不同源。跨时钟域采样时如果采样时钟频率低于信号时钟频率或者采样时钟和信号时钟相位关系不稳定触发器的建立保持时间不断被违反采样回来的数据就是乱的。这不是DDS的问题是体检设备本身没校准。这个问题的排查顺序很关键。先确认ILA的采样时钟是否和DDS时钟完全同源。其次确认采样时钟频率是否满足奈奎斯特条件至少要大于被观察信号最高频率的两倍。DDS输出频率一般远低于它的系统时钟所以只要ILA采样时钟等于DDS系统时钟通常都满足。最后确认探针的数据宽度和实际信号位宽一致宽度不匹配也会导致波形显示异常。在实际项目中把ILA的采样时钟独立出来、和被测DDS系统时钟用同一个PLL输出是最省心的做法。跨时钟域的问题留给正式的数据交互逻辑去处理调试探针不要自己制造跨时钟域。最后再分享一个我自己用下来的习惯DDS接入DAC之前先用ILA把原始数字波形抓下来导出成CSV之后做一次FFT确认SFDR达标再往板上接DAC和模拟滤波电路。这一步能省掉很多后级滤波电路调参的时间。Vivado的ILA加Verilog定点仿真已经完全覆盖了Matlab能做的大部分DDS验证工作至少对我手里的项目来说是这样。如果你想从Matlab仿真过渡到纯FPGA验证从DDS这块开始试最合适因为它功能经典、结构清晰、问题可观测是练手数字化体检流程最好的试验田。
返回列表