ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

FPGA开发中的DDS信号发生器:用Vivado和Verilog完成全流程验证

FPGA开发中的DDS信号发生器:用Vivado和Verilog完成全流程验证 FPGA开发里做信号发生器绕不开DDS直接数字频率合成这个话题。以前我做DDS验证习惯先在Matlab里把波形、频谱、杂散一通仿真觉得没问题了再写Verilog然后到Vivado里再走一遍仿真。后来我发现这套流程有个很尴尬的问题Matlab里的模型和RTL里的行为经常对不上模型再漂亮到了RTL里一个位宽截断或者一个时序问题就能让波形变味。所以后来我干脆把DDS的整个体检流程全部搬进Vivado用Verilog搭测试平台用XSim做仿真用导出的波形数据做频谱分析直接在一套工具链里完成从RTL设计到性能验证的全部工作。这篇内容围绕一个全身体检思路展开DDS信号发生器不只测能不能出波形还要测频率准不准、频谱干不干净、抗干扰稳不稳定、相位切换连不连续。我会把设计思路、RTL代码、测试平台搭建、仿真结果分析和常见坑位都拆开讲一遍适合正在学FPGA的中级选手也适合想把自己DDS设计验证流程精简掉的工程师参考。1. 为什么要给DDS做全身体检从Matlab到XSim的一次工具链迁移1.1 DDS信号发生器到底在测什么指标DDS的核心原理是相位累加器加上查找表。N位相位累加器在每个时钟沿累加一个频率控制字FTWFrequency Tuning Word累加结果的高位作为查找表地址索引进而输出正弦波的幅度值。输出频率由FTW * f_clk / 2^N决定改变FTW就能快速改变频率而相位累加器本身是连续运转的所以频率切换天然就是相位连续的。但能出波形只是最基础的一层。一个能交付的DDS信号发生器至少要体检这几个指标频率精度实际输出频率和设定的目标频率差多少FTW取整带来的误差能不能接受。杂散与无杂散动态范围由于相位截断和幅度量化输出频谱里除了主频还有杂散分量SFDR够不够高。输出幅度一致性在不同频率设定下输出正弦波的幅度会不会明显波动主要是查找表和DAC相关的问题但RTL层面也能看到截断的影响。相位切换连续性从一个频率切到另一个频率时波形是不是平滑过渡有没有跳变或毛刺。长时间稳定性仿真跑长一点输出波形有没有漂移相位累加器有没有溢出或异常。在Matlab里验证这些指标相对理想——模型是数学化的时钟是完美的没有亚稳态没有复位时序问题。但一旦到了RTL里时钟树、复位释放、组合逻辑延迟、位宽截断都会真实地跑起来。与其在Matlab里建一套理想模型再在RTL里重写一套不如直接在Vivado里用Verilog搭一套接近真实芯片行为的验证环境一步到位。1.2 Matlab联合仿真的三个痛点我最早做DDS验证走的是Matlab生成查找表 - Verilog读入ROM - XSim跑仿真 - 把仿真数据导出来 - 再回Matlab画频谱这条路。这套流程有很明显的三个问题第一个痛点是数据来回导出太繁琐。XSim跑完仿真后要把波形数据通过$fopen、$fwrite写成文本文件再写一段Matlab脚本读取才能画图和做FFT。一次两次还好一旦要调整参数重新仿真整个流程要全走一遍时间全耗在数据搬运上了。第二个痛点是模型不一致。Matlab里的DDS模型和RTL实现之间有位宽差异、截断差异、初始化顺序差异。经常出现Matlab仿真出来的SFDR是90dBcRTL仿真一测只有70dBc然后要花很长时间排查差异出在哪里。第三个痛点其实是Vivado自带的工具能力被低估了。很多人不知道XSim导出的数据可以很方便地做频谱分析Vivado也自带Tcl脚本接口可以在仿真结束后自动做FFT、计算SFDR、生成报告。把这些脚本写好之后每次仿真跑完报告自动生成不需要再手动搬运数据。所以我后来完全改成了纯VivadoVerilog的路线RTL用一个参数化DDS模块Testbench用Verilog搭建激励环境仿真结束自动输出文本格式的采样值Tcl脚本自动做频谱分析并打印SFDR、频率误差、幅度误差。除了线性度、谐波失真这类需要更专业数学分析的场景日常DDS体检完全不需要再打开Matlab。1.3 Vivado自带的体检设备够不够用Vivado的XSim仿真器和Matlab比确实没有那么多现成的信号处理函数但DDS的频谱分析本质上就三件事采样值导出、加窗FFT、峰值搜索。这三件事用Tcl或一段Python脚本都能做Vivado本身也支持在仿真完成后通过Tcl脚本处理波形数据库。实际使用下来XSim对于DDS级别的设计几百门到几千门的RTL模块仿真速度非常快几十微秒的仿真时间通常只需要几十秒。相比Questa或VCSXSim胜在集成度高和Vivado工程无缝衔接跑完仿真直接打开波形界面放大看细节发现异常立刻回改RTL再跑迭代速度很快。如果你的设计里用了Vivado的DDS Compiler IP核XSim还能直接支持IP核的行为级仿真模型不需要额外的编译配置这也是我用Vivado做纯RTL验证的重要原因之一。2. 设计一个参数化DDS模块RTL实现与查找表生成2.1 相位累加器位宽怎么定这一步是整个DDS设计里最关键也最容易草率的地方。相位累加器的位宽N直接决定了频率分辨率和杂散性能。我见过很多新手直接用24位、16位理由是够用了但这往往会在后续验证时被频率误差打脸。频率分辨率 f_clk / 2^N。比如系统时钟100MHzN24位时分辨率大概是5.96HzN32位时分辨率是0.023Hz。表面看24位也够用但如果你的DDS要支持到0.1Hz级别的精密调频24位就不够了。所以我的习惯是直接把N做成参数默认32位需要更高频率精度时改成48位甚至64位。Verilog里参数化写起来并不复杂后续改位宽只需要改一个参数。相位累加器的位宽确定后查找表地址位宽A是独立决定的。A决定了查找表的深度2^A个采样点一般取10到16位。此时要注意相位累加器的高A位会被用来索引查找表低N-A位会在累加过程中被截断丢弃这就是相位截断杂散的来源。A越大相位截断误差越小SFDR越高但查找表的存储资源会指数增长。16位地址的查找表需要65536个点在FPGA里通常用Block RAM来存储资源占用还好10位地址1024个点则可以用分布式RAM。我在实际项目中常用的组合是N32、A14或A16。A14时SFDR理论值能到80dBc以上A16时能到90dBc级别。再往上走性价比就不高了因为幅度量化的16位输出已经是另一个瓶颈。2.2 查找表是手工算还是用IP核查找表本质上是一组正弦波采样值。生成方式有三种思路我分别说下优缺点第一种是Vivado自带的DDS Compiler IP核。优点是完全图形化配置支持相位抖动dithering来改善SFDR输出接口标准和XSim的协同也最顺畅。缺点是IP核有它自己的配置体系很多参数比如相位累加器位宽、输出位宽是否带符号、是否开启AxiStream接口不够直观不熟悉IP核配置的话容易把它当成一个黑盒出了问题反而不好排查。第二种是外部脚本生成 .coe 或 .mem 文件。用Python、Matlab或者其他脚本语言按sin(2*pi*i/2^A)计算每个地址对应的正弦值量化成你想要的位宽然后写成Vivado ROM初始化文件.coe或.mem在RTL里用$readmemh或$readmemb加载。这种方式胜在高度可控参数调整灵活比如可以顺便生成余弦查找表或者自定义任意波形表。我自己的项目里一般用这种方式因为后续如果想扩展成任意波形发生器只需要替换查找表文件就行。第三种是直接在RTL里用case语句写死采样点。这种方式只适用于查找表特别小的场景比如16个点不建议在DDS里用会让代码膨胀且难以维护。如果走第二种路线我有一个小建议生成查找表时用带符号数而不是无符号数。原因后面在RTL代码里会详细解释但简单说就是带符号数能更好地匹配后续DAC或示波器显示的逻辑避免直流偏置带来的困惑。2.3 RTL代码示例与端口设计下面是我在实际项目中用的DDS核心模块代码。这个模块支持参数化配置相位累加器位宽、查找表地址位宽和输出数据位宽。module dds_core #( parameter PHASE_WIDTH 32, parameter LUT_ADDR_WIDTH 14, parameter DATA_WIDTH 16 )( input wire clk, input wire rst_n, input wire [PHASE_WIDTH-1:0] ftw, // Frequency Tuning Word output reg [DATA_WIDTH-1:0] sine_out, output wire clk_out // optional ); reg [PHASE_WIDTH-1:0] phase_acc; wire [LUT_ADDR_WIDTH-1:0] lut_addr; wire [DATA_WIDTH-1:0] sine_w; // Phase accumulator always (posedge clk or negedge rst_n) begin if (!rst_n) phase_acc {PHASE_WIDTH{1b0}}; else phase_acc phase_acc ftw; end // Use upper bits as LUT address assign lut_addr phase_acc[PHASE_WIDTH-1 -: LUT_ADDR_WIDTH]; // sine lookup table (memory file) (* rom_style block *) reg [DATA_WIDTH-1:0] lut_mem [0:2**LUT_ADDR_WIDTH-1]; initial begin $readmemh(sine_lut.mem, lut_mem); end always (posedge clk) begin sine_out lut_mem[lut_addr]; end endmodule这段代码里有三个地方值得注意。第一个是ROM初始化方式。$readmemh在仿真里很直接只需要保证sine_lut.mem文件的路径能被仿真器找到。综合时综合器会把lut_mem推断成Block RAM或分布式RAMrom_style属性可以指定存储结构。如果你用的是纯仿真没有约束文件路径的问题但如果是综合上板记得确认一下sine_lut.mem被加进了工程的文件列表。第二个是ROM输出加了寄存器。sine_out lut_mem[lut_addr]这行看起来简单但重要它是在时钟沿后采样ROM输出可以有效避免组合逻辑输出带来的毛刺。ROM读取本身在FPGA里有相应的延迟Block RAM的读延迟一般是1到2个时钟周期所以带寄存器的写法既符合硬件特性也让后续的时序分析更干净。第三个是相位累加器不带单独的输出寄存器。有些设计会在累加器后面再插入一级寄存器来优化时序但DDS这类模块Fmax要求通常不高在时钟频率100MHz级别时累加器直接接ROM地址是没问题的。如果你的时钟频率跑到300MHz以上可以考虑把累加器输出打一拍再做ROM地址。为了直观理解这个模块的行为我放一张仿真的时序示意图时钟: __|‾‾|__|‾‾|__|‾‾|__|‾‾|__|‾‾|__ ftw: [ 429496730 ] (10MHz 100MHz) phase: 逐周期增加高14位作为ROM地址 sine_out: 输出16位带符号正弦波数值只需要提供ftw f_out * 2^PHASE_WIDTH / f_clk相位累加器就会每个时钟周期累加一次查找表输出对应的正弦波形采样值。3. 用XSim搭建仿真体检台Testbench实战3.1 激励环境设计要点DDS的Testbench最核心的工作包括生成时钟、释放复位、配置FTW、拉长仿真时间观察波形、导出采样值。下面是我的Testbench模板也是我所有DDS验证的基础框架。timescale 1ns / 1ps module tb_dds_core; parameter PHASE_WIDTH 32; parameter LUT_ADDR_WIDTH 14; parameter DATA_WIDTH 16; parameter F_CLK 100_000_000; // 100MHz reg clk; reg rst_n; reg [PHASE_WIDTH-1:0] ftw; wire [DATA_WIDTH-1:0] sine_out; // Clock generation initial clk 0; always #5 clk ~clk; // 10ns period // DUT dds_core #( .PHASE_WIDTH(PHASE_WIDTH), .LUT_ADDR_WIDTH(LUT_ADDR_WIDTH), .DATA_WIDTH(DATA_WIDTH) ) u_dut ( .clk(clk), .rst_n(rst_n), .ftw(ftw), .sine_out(sine_out) ); // Stimulus initial begin rst_n 0; ftw 32d0; #100; rst_n 1; #50; // 10MHz sine wave ftw 32d429496730; // 10e6 * 2^32 / 100e6 #20_000; // run for 20us // Frequency switch to 5MHz ftw 32d214748365; // 5e6 * 2^32 / 100e6 #20_000; // Close simulation $display(Simulation finished); $finish; end endmodule这段Testbench有几个细节一定要处理好。第一个是时钟生成方式。always #5 clk ~clk是比较常见的写法但如果你需要100MHz的精确时钟更严谨的写法是用forever #5 clk ~clk;放在initial块里这样不会出现仿真0时刻时钟状态不确定的问题。另外要注意timescale 1ns / 1ps里的时间精度1ps是仿真精度过高的精度会拖慢仿真速度如果只需要观察微秒级别波形1ns精度完全够了。第二个是复位释放时序。DDS模块对复位没有特别苛刻的要求但有一个思路值得学习复位释放的时机要等时钟稳定后再做。上面代码里#100是在仿真开始后100ns释放复位这个时间远大于10ns时钟周期能保证所有寄存器都处于已知状态。如果你用了MMCM/PLL生成时钟通常还要额外关注locked信号不过DDS模块一般不用PLL直接使用系统时钟就好。第三个是FTW的设置时机。我推荐复位释放后至少空出几十纳秒再设置第一个FTW必要时可以先ftw 0让相位累加器稳定累加0值也就是输出固定值而不是正弦波再切到目标频率。这个习惯能帮你排查复位和初始化问题避免一上来就怀疑DDS模块本身有BUG。3.2 频率控制字计算与测试用例规划FTW的计算公式是FTW round(f_out * 2^PHASE_WIDTH / f_clk)。比如f_clk 100MHzf_out 10MHzPHASE_WIDTH 32FTW round(10e6 * 2^32 / 100e6) round(429496729.6) 429496730这个取整会引入约0.39Hz的频率误差在大多数应用场景下可以忽略。但如果你的应用需要极低频率或极高的频率精度记得在Testbench里把这个误差算出来并作为体检参考值之一。我的测试用例规划一般包含用例编号测试内容FTW值预期输出频率TC1基础正弦波42949673010MHzTC2低频输出42951kHzTC3频率边界接近Nyquist214748364850MHzTC4频率切换连续性先10MHz再5MHz观察切换瞬间TC5复位恢复复位后重新设置FTW观察初始相位TC1和TC2用于验证频率精度和查找表初始化是否正确TC3用来观察奈奎斯特边界附近的输出波形质量这个用例往往能暴露一些问题当时钟频率只是输出频率的两倍时波形重建质量会明显下降。TC4是关键用例用来验证相位累加器连续工作不跳变TC5则用来确认复位置位后DDS能重新正常输出。3.3 波形查看与导出FFT频谱Testbench写好之后在Vivado里选择Run Simulation - Run Behavioral Simulation就能直接打开仿真波形窗口。看波形的第一步是确认sine_out是否按预期输出正弦形状。放大波形后如果看到的是逐周期平滑变化的正弦曲线说明DDS基本工作正常。如果看到跳变、毛刺、直流偏移、幅值异常就要回到代码里排查。XSim的波形窗口里可以直接添加模拟信号显示形式把sine_out改为Analog格式看起来就很直观。我个人习惯把sine_out、phase_acc、ftw这几个信号全部添加进去观察它们的联动关系。但光看波形不能量化评估频率精度和SFDR。这时候需要导出采样值到文件然后用脚本做频谱分析。在Testbench里加一段代码integer file_handle; initial begin file_handle $fopen(sine_samples.txt, w); wait (rst_n 1); #1000; // skip transient forever begin (posedge clk); if ($time 20000) begin // capture 20us of data $fwrite(file_handle, %0d\n, $signed(sine_out)); end end end这段代码会把从20us时间点后的每个时钟沿的sine_out数值写入文本文件。导出后用Python脚本或Tcl脚本做FFT。我提供一个简化的Python分析脚本import numpy as np fs 100e6 # sample rate samples np.loadtxt(sine_samples.txt) N len(samples) # Windowing and FFT window np.hanning(N) spectrum np.fft.rfft(samples * window) freqs np.fft.rfftfreq(N, d1/fs) # Find major peak mag np.abs(spectrum) peak_bin np.argmax(mag[10:]) 10 # skip DC f_measured freqs[peak_bin] print(fMeasured frequency: {f_measured/1e6:.6f} MHz) # SFDR: the ratio between fundamental and largest spur fund_mag mag[peak_bin] spur_bins [i for i in range(1, len(mag)) if i ! peak_bin] spur_mag max(mag[spur_bins]) sfdr_db 20 * np.log10(fund_mag / spur_mag) print(fSFDR: {sfdr_db:.2f} dBc)这个脚本能直接告诉你测量频率和SFDR。对于LUT_ADDR_WIDTH 14的配置SFDR通常在80dBc左右如果只有60dBc甚至更低说明相位截断或幅度量化出了问题需要优化。4. 体检报告怎么解读仿真结果分析与参数权衡4.1 输出频率误差怎么评估我在实际测试中10MHz设定值的典型测量结果是9.9999999xx MHz误差主要来自FTW取整。假设FTW429496730实际频率是429496730 * 100e6 / 2^32 ≈ 9.99999974MHz误差约0.26Hz。这个误差在绝大多数情况下都可以忽略。但如果你的目标频率是40MHz、50MHz这样的值取整误差会被放大。比如50MHz对应的FTW理论值是2147483648恰好是2^31能精确表示所以50MHz输出非常准。反过来看那些不能整除的频率误差就会在频谱上体现为一个略微偏移的峰值中心。评估频率误差的时候别只盯着FFT峰值频率还要用计数器实测一下。测试方法是在DDS输出端做一个过零检测统计一定时间内发生的过零次数用过零次数/2 / 时间计算出实际频率。这个方法的精度取决于统计时间长度统计时间越长越准。在仿真环境里这个方法很容易实现。4.2 相位截断与杂散抑制SFDR的实际影响相位累加器的低位被截断是DDS杂散的主要来源。虽然查找表ROM是按2^14个点存储的但相位累加器是32位的低18位每周期都在变化截断后相邻采样点对应的相位误差是一个锯齿波这个锯齿波调制在正弦波上就产生杂散。减小杂散的标准做法有两个第一大查找表深度增加LUT_ADDR_WIDTH第二是引入相位抖动dithering在相位累加器输出给ROM之前把LSB位上的随机小扰动加到相位上把杂散能量打散成宽带噪声从而抬高SFDR。Vivado的DDS Compiler IP核里就提供dithering选项手写RTL的话需要自己实现一个伪随机数发生器来生成抖动。我在手写RTL时曾尝试过加一个简单的LFSR做相位抖动效果挺明显SFDR从82dBc提升到91dBc。但代价是输出底噪声略微抬高。所以体检时SFDR不是越高越好要根据系统整体信噪比要求来决定。如果整个信号链路的噪声底是75dBc那把DDS的SFDR做到90dBc意义不大反而浪费资源和逻辑。还有一个容易被忽略的地方FFT分析时加窗的选择会直接影响SFDR读数。矩形窗的主瓣宽、旁瓣高容易掩盖附近的杂散汉宁窗Hanning是测量SFDR的常用选择主瓣窄、旁瓣衰减快。上面Python脚本里用的就是汉宁窗。如果你看到两个频率靠得非常近的谱线可能不是两个独立的信号而是同一个信号被窗函数展宽了这时候要加更长的采样长度再测。4.3 相位切换连续性的验证方法相位切换连续性测试是我必做的一个体检项目。DDS频率切换时由于相位累加器一直在累加不需要重新初始化所以理论上输出波形是连续的。但如果在RTL里对FTW做了打拍处理、异步复位、或者跨时钟域同步连续性可能就被破坏了。测试方法是TC4用例里让DDS先输出10MHz跑稳定后在某个时钟沿把FTW换成5MHz的值然后在波形窗口里放大切换点附近的波形仔细检查有没有波形幅值跳变说明ROM地址异常出现半周期毛刺说明FTW换入过程中组合逻辑竞争输出短暂为0或异常值说明ROM在切换时读到了未初始化地址我在第一次做切换连续性测试时就踩过一个坑FTW的更新方式用的是异步信号直接驱动导致在时钟沿附近改FTW时相位累加器有一次累加使用了新旧FTW混合的中间值波形上出现了一个明显的窄毛刺。后来我把FTW输入打了一拍在时钟同步后再更新到相位累加器里问题就消失了。所以如果MTTF测量法太复杂最简单的连续性评估方法是看切换点附近的瞬态波形。如果切换后第一个输出值和切换前的最后一个输出值之差的绝对值远小于正弦波最大斜率即正常情况下应该相邻的差值那连续性是没问题的。5. 踩过的坑和排查思路Vivado仿真十大问题速查5.1 仿真跑不起来的常见原因先说说点下Run Simulation之后什么都没有的情况。这个问题我见过太多次了新手和老手都会遇到。第一个原因是时钟没生成。如果你在Testbench里用initial clk 0; always #5 clk ~clk;这种写法有时会因为initial和always块之间的初始化顺序问题让clk在仿真0时刻处于X状态。解决办法是写个独立的时钟生成块initial begin clk 0; forever #5 clk ~clk; end。第二个原因是测试块里用了#100之类的时间延迟但timescale不一致。Testbench文件和RTL文件的timescale如果不一致时间单位换算出错会导致延迟极短或极长表现就是仿真立即结束或者仿真长时间卡住。解决方法是所有文件统一用timescale 1ns / 1ps。第三个原因是$fopen写文件路径错误。在Windows上Vivado工程中的相对路径和当前工作目录不一定对应建议在Testbench里使用绝对路径或./sim/子目录并提前创建好目录。否则文件打不开仿真可能报warning但继续跑数据却没写进去。5.2 RTL行为异常怎么定位如果仿真能跑但波形显示sine_out一直输出0或者恒定值优先查ROM初始化文件有没有被正确加载。打开Vivado的Tcl控制台运行report_memory -all查看ROM实例有没有成功初始化。如果显示NO INIT或者初始化失败多半是.mem文件路径不对或者文件格式和$readmemh期望的格式不匹配。如果sine_out输出的是锯齿波而不是正弦波说明ROM地址和存储内容对不上。检查你的查找表生成脚本确认采样点数量和ROM地址位宽一致。我犯过一次错误生成的.mem文件里数据数量比2^14多了几个导致最后的地址访问到未定义区域输出数据变成X状态。后来在生成脚本里加了一个断言数据数量必须是2^LUT_ADDR_WIDTH的整数倍再没出过这个问题。如果输出波形有毛刺优先检查时钟约束和异步处理。RTL里DDS模块对时钟要求相对宽松但如果和外部逻辑之间有跨时钟域信号毛刺就可能传播进来。排查方法是在毛刺出现的仿真时刻回看时钟沿和FTW更新是否在同一时刻如果是考虑加一级同步或打拍。5.3 Vivado编译/实现报错处理DRC、时序很多人在RTL仿真通过之后进入综合/实现阶段会遇到各种报错。最典型的就是[DRC RTSTAT-2]这个错误通常出现在约束不足或者布局布线资源紧张时。DDS模块本身逻辑简单DRC报错大概率是时钟约束没写对。我的排查思路是先看工程有没有有效的XDC约束文件确认system clock被正确约束再看实现报告里有没有大的setup或hold违例如果有检查是否在RTL里写了组合逻辑环。另一个常见问题是implement design变红也就是布局布线后时序收敛不了。DDS模块是高扇出的模块特别是ROM地址总线和输出数据总线如果LUT_ADDR_WIDTH设得过大比如18位甚至20位Block RAM的延迟和布线延迟会叠加导致Fmax上不去。这时候的优化思路是把ROM地址打一拍再进ROM或者把相位累加器拆成两级流水换取更高Fmax。DDS对输出延迟不敏感增加两三级流水完全没问题。还有个容易被忽略的问题Vivado在综合阶段会报[Synth 8-3331] design has unconnected port之类警告。DDS模块在Testbench里可能只接了sine_out其他可选端口比如相位输出没接线这本身不致命。但如果警告太多建议写一个接口完整性检查脚本避免某些端口漏接。5.4 仿真速度优化技巧DDS仿真动辄跑几十万甚至上百万个时钟周期如果每个周期都全量记录波形XSim的波形文件会非常庞大仿真速度会被拖慢到无法接受。我有几个优化技巧第一个技巧是只记录需要的信号。XSim默认记录工程里所有信号你可以在仿真设置里选择只记录顶层Testbench的少数关键信号或者用$dumpvars指定需要导出的变量。这样可以大幅减小波形文件体积。第二个技巧是关闭不必要的断言和监控代码。Testbench里如果加了$monitor或$display每拍都打印信息仿真速度会明显下降。我在调试阶段会打开详细打印但回归测试阶段会全部关掉只保留$finish前的统计报告。第三个技巧是合理设置仿真时间。有些同学习惯跑完10ms甚至100ms的仿真来验证长期稳定性这在纯RTL仿真里是可行的但要认识到仿真时间和仿真事件数量成正比。100MHz时钟跑10ms等于100万个时钟周期XSim处理起来可能需要几十分钟。所以我的建议是功能性验证用短仿真几微秒到几十微秒长期稳定性验证则用统计采样方法比如每1000个周期采样一次检查输出包络而不是全量记录。还有一个小技巧如果你发现XSim仿真速度特别慢检查一下你的timescale是不是设成了1ns / 1ps精度越高仿真越慢。如果不需要皮秒级别的调试精度可以把Testbench的精度改成1ns / 1ns速度能提升不少。6. 全流程实操从零跑通一次体检啰嗦了这么多原理和坑最后给一个可以照着做的完整流程。这个流程基本上就是我新接一个DDS项目时的工作流。第一步搭工程和目录结构。Vivado工程目录下我会建rtl/、sim/、mem/、scripts/四个子目录分别放RTL代码、Testbench、查找表文件和分析脚本。这样做的好处是查找表文件路径可以在Testbench里用相对路径明确引用脚本独立成目录也不会被综合误认为是设计文件。第二步生成查找表。用Python脚本生成sine_lut.mem格式如下// sine LUT, 16384 points, signed 16-bit 0000 0003 0006 ...关键参数是位宽16位、带符号、数据数量等于2^LUT_ADDR_WIDTH。脚本里最好加上数据数量验证和峰值检查防止写出的.mem文件格式不对。第三步写好RTL和Testbench跑行为仿真。先跑TC1确认基础波形正常。然后依次跑TC2到TC5每个用例跑完后导出采样文件用Python分析频率和SFDR。所有用例都过了RTL层面就算完成体检了。第四步如果是上板项目再做综合和实现。实现后基本不需要再改RTL除非遇到时序问题。上板后用逻辑分析仪或者片上逻辑分析仪抓实际波形和仿真结果对比。这一步通常能看到和纯仿真不一样的细节比如真实信号的波形毛刺会比仿真多一些但大部分在可接受范围内。整个流程走下来一个100MHz主频、14位查找表的DDS模块从零到频率精度和SFDR都验证完毕大约半天到一天时间。相比以前MatlabVerilog两条腿走路这个流程省掉了大量的数据搬运时间也让仿真模型和RTL行为保持一致排查问题明显更快。最后再分享一个我觉得特别实用的习惯在我现在的日常开发里每次仿真跑完我会顺手把FFT分析脚本和Tcl脚本一起放进工程目录的scripts文件夹并且在Testbench里自动生成一份report.txt里面记录本次仿真的FTW、目标频率、实测频率、SFDR、波形文件名。这样连续改版、回归测试时可以快速对比不同参数下的结果一眼看出改动是变好还是变差。如果你是从Matlab迁移过来的初期可能会不习惯没有那一堆现成的信号处理工具箱。但实际用下来你会发现DDS的验证核心需求也就那几个而在Vivado里用Verilog把这些需求全部自动化之后你得到的是一套完全可复现、可回归、可交接的开发流程。这套流程的稳定性是Matlab手动来回导数据永远比不了的。
返回列表