ARTICLE DETAIL

资讯详情

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

FPGA数字信号处理:NCO IP核配置与ModelSim仿真全流程详解

FPGA数字信号处理:NCO IP核配置与ModelSim仿真全流程详解 做FPGA数字信号处理的老哥应该都绕不开一个需求生成一路正弦波、余弦波或者给系统提供本地载波做混频、调制。不少人的第一反应是自己写一个DDS直接数字频率合成器模块但放到实际工程里我更推荐直接用Quartus Prime 18.0自带的NCO IP核。IP核把相位累加、相位截断、杂散抑制这些底层细节都处理好了你只需要填几个参数。这篇文章我会完整记录从创建NCO IP核、逐项配置参数、生成文件到在ModelSim里把仿真跑通的全过程重点讲那些不亲自踩一遍绝对想不到的坑——尤其是仿真波形一片红线的经典问题。内容针对Quartus Prime 18.0和ModelSim的联合仿真场景既有原理讲解也有保姆级操作步骤适合刚接触FPGA数字信号处理的初学者也适合想把NCO流程彻底跑通的项目攻城狮。1. DDS原理与NCO IP核选型先搞清楚配置的是什么1.1 相位累加器与查找表NCO的核心工作方式NCO本质上就是一个DDS核心结构由两部分组成相位累加器Phase Accumulator和相位-幅度查找表Look-Up TableLUT。相位累加器每个时钟周期累加一次频率控制字Phase Increment Word相当于在一根被均匀切分成2^N份的“相位圆”上迈步子累加结果的高位被截取出来作为查找表的地址查找表输出对应的正弦或余弦幅度值。说得再直白一点一个360°的圆被切成2^N个刻度每个时钟沿走一步步长就是频率控制字。步长越大绕完一整圈越快输出波形频率就越高。核心公式是f_out f_word × f_clk / 2^N其中f_clk是系统时钟频率N是相位累加器位宽f_word是频率控制字。这个公式在后面配置IP核和计算testbench参数时会反复用到建议直接记下来。1.2 为什么用NCO IP核而不是自己手写一个DDS模块网上有不少手写DDS的教程代码也就几十行为什么还要特意用IP核我在公司里也见过同事自己写DDS跑功能仿真没问题一上板测频谱就露馅杂散大、SFDR无杂散动态范围上不去排查半天发现是相位截断和ROM量化问题没处理好。NCO IP核算是对这些底层细节做了专门的优化主要体现在三个方面杂散抑制通过相位抖动Phase Dithering或泰勒级数校正等手段把相位截断造成的离散杂散打散成宽带噪声SFDR表现比自己随便写的好不少。资源可预估IP核生成时可以直接看到LUT、寄存器、乘法器的预估消耗工程上做资源评估很方便。接口标准化clk、clken、reset_n、phi_inc_i、out_valid这些端口都是固定的方便后续集成到AXI或自定义数据通路里。当然自己写DDS也不是没有优势比如想省掉LUT改用CORDIC算法、想彻底脱离供应商IP依赖、想做极端低延迟的定制实现。但绝大多数场景下用IP核是性价比最高的方案这也是本文选择NCO IP核的根本原因。1.3 相位累加器位宽到底影响什么频率分辨率在打开配置界面之前需要先理解“相位累加器位宽”这个参数。它直接决定了频率分辨率也就是输出频率能精细到什么程度。频率分辨率的计算公式是Δf f_clk / 2^N举个例子如果系统时钟是100MHz相位累加器位宽是32位那么频率分辨率就是100M / 2^32 ≈ 0.023Hz。这意味着你几乎可以输出任意频率的正弦波精度远高于普通晶振应用的需求。位宽加到40位分辨率还能再细4个数量级但累加器占用的寄存器资源也会相应增加。这里有个容易忽略的点NCO配置界面有时会让你直接填频率分辨率然后由工具反推位宽有时会让你填相位累加器位宽然后显示对应的分辨率。两种方式本质是一回事但填频率分辨率时要注意单位通常是Hz别填成MHz导致差6个数量级。2. 仿真环境准备ModelSim与Quartus的“库”才是关键2.1 用自带的ModelSim Starter还是独立安装ModelSim SEQuartus Prime 18.0安装时会提供一个ModelSim Starter Edition这个是Intel定制版功能和器件支持都做了裁剪跑个小工程没问题但工程大了、IP核多了速度和稳定性都一般。我的习惯是装独立的ModelSim SE 2020.4配合Quartus 18.0使用。注意Starter版与SE版在编译Altera仿真库这件事上流程是一样的只是SE版更通用命令行操作也更顺手。如果你装了ModelSim SE先确认环境变量里能不能直接敲vsim命令。敲不了就手动把ModelSim的win64目录加到PATH里或者直接在安装目录下打开ModelSim终端。这一步虽然基础但很多人卡在这一步后面所有命令都报“不是内部或外部命令”。2.2 不编译Altera仿真库后果就是一片红线Altera的IP核在Modelsim里做功能仿真时需要依赖供应商提供的仿真库比如altera_mf、altera_primitives、lpm、altera_lnsim。这些库里封装了IP核底层例化的LPM模块和原语仿真模型。如果不先编译这些库并映射到ModelSim环境里仿真器看到IP核内部模块时会直接报“Module not found”输出的信号自然全是未知状态反映在波形窗口里就是一片红色。有同学问我不就是把NCO生成的.v文件加到Modelsim里仿真吗为什么还非要编译这些东西因为NCO生成的.v只是一个包装壳底层例化了altera_mf库里的lpm_rom、lpm_add_sub等通用宏单元。仿真器不是编译器它不会自己去Quartus安装目录里找库所有依赖的库都得先编译好、映射好它才知道去哪里找模型。这个道理搞明白后面遇到类似IP核的红线问题就有思路了。2.3 手动编译仿真库的具体命令在ModelSim里手动编译Altera库是比较稳妥的做法即使以后换了IP核这套库也能继续用。先找到Quartus 18.0仿真库所在的路径默认是C:\intelFPGA\18.0\quartus\eda\sim_lib打开ModelSim先cd到你想存放仿真库的目录比如一个专门放库的文件夹然后逐条执行以下命令vlib work vmap altera_mf altera_mf vmap altera_primitives altera_primitives vmap lpm lpm vmap altera_lnsim altera_lnsim vlog -work altera_mf C:/intelFPGA/18.0/quartus/eda/sim_lib/altera_mf.v vlog -work altera_primitives C:/intelFPGA/18.0/quartus/eda/sim_lib/altera_primitives.v vlog -work lpm C:/intelFPGA/18.0/quartus/eda/sim_lib/lpm.v vlog -work altera_lnsim C:/intelFPGA/18.0/quartus/eda/sim_lib/altera_lnsim.sv注意altera_lnsim是SystemVerilog文件后缀是.sv所以用vlog编译时不需要额外加其他选项ModelSim能自动识别。如果编译过程中报语法错误多半是版本太老或文件路径不对检查一下路径分隔符是正斜杠反斜杠即可。另外vmap命令的意思是建立逻辑库名到物理目录的映射。这里把altera_mf映射到同名目录相当于告诉ModelSim“以后谁引用altera_mf库就去这个目录找”。这一步不做即使编译了.v文件仿真加载时依然找不到库。2.4 用NativeLink自动配置省心但有条件Quartus 18.0提供NativeLink功能可以在综合后自动调用ModelSim进行仿真并且自动完成库的编译和映射。设置路径是Tools - Options - EDA Tool Options把ModelSim的可执行文件路径填上然后在工程设置里Assignments - Settings - EDA Tool Settings - Simulation把工具选为ModelSim语言选Verilog。NativeLink确实省事但我个人更推荐手动建ModelSim工程尤其是在做纯功能仿真阶段。原因很简单NativeLink自动跑的时候会生成一堆脚本和映射一旦出错很难定位手动建工程虽然前期多敲几条命令但整个过程完全可控也能真正理解IP核仿真的底层逻辑。命令行方式在这个场景下反而更“保姆级”。3. 创建NCO IP核参数界面逐项设置与原理对应3.1 打开IP Catalog并设置工程基本信息在Quartus Prime 18.0里新建或打开一个工程后通过Tools - IP Catalog打开IP目录窗口在搜索框里输入NCO就能看到Numerically Controlled Oscillator。双击后弹出创建窗口第一件事是给IP核起个名字比如nco_top然后选择输出文件类型一般默认Verilog。这里提醒两点IP核名字不要用中文不要用数字开头更不要和工程名重复否则后续生成文件和管理都会出问题。另外如果界面提示选择目标器件族最好先确认你最终要用的FPGA型号比如Cyclone V、Cyclone 10、Arria 10等因为不同器件族能用的NCO实现方式不太一样。3.2 Implementation选型Small ROM、Large ROM、CORDICNCO配置界面第一个关键选项是Implementation也就是底层实现方式。不同版本Quartus里可能略有差异但核心是以下几种实现方式基本原理资源特点适用场景Small ROM用最小ROM存放压缩后的正弦表LUT资源少但地址译码逻辑多资源紧张、对性能要求不高Large ROM用一整块大ROM直接存正弦表单块RAM占用大但速度快常规选择性能稳定CORDIC用旋转迭代算法计算正弦余弦不依赖ROM用DSP/寄存器换取ROM精度可控不想用BRAM或需要高吞吐时如果是第一次上手建议直接选Large ROM。它最直观仿真资源消耗也适中不容易出现性能瓶颈。Small ROM虽然省LUT但反而引入额外的地址计算流水线新手调试起来容易懵。CORDIC适合有特殊需求的老手比如ROM资源非常紧张、或者需要无ROM实现时再考虑。3.3 频率分辨率的设置方式与相位累加器精度的关系进入参数页后会看到“Phase Accumulator Precision”或者“Frequency Resolution”之类的选项。有的版本让你直接填频率分辨率工具自动计算对应的位宽有的版本让你先定位宽工具显示分辨率。两种方式对应同一个核心关系频率分辨率 时钟频率 / 2^位宽。举个例子时钟设为100MHz、频率分辨率设为0.023Hz工具算出来的位宽就是32位。反过来如果位宽填32位界面会显示分辨率为0.023Hz。这里建议先把系统时钟和期望频率分辨率想好再填别反过来被默认值带偏。还要确认一个概念Frequency per Word每个字对应的频率就是前面说的频率分辨率Δf。如果你在这个页面设置了一个完整的“Phase Increment Value”或者频率控制字那么要注意它是无符号整数最终实际输出频率是整数和Δf的乘积。频率控制字取整会带来微小的频率偏差这个偏差在大多数通信系统里可以忽略但如果要做高精度载波恢复就需要考虑用更高位宽来摊薄误差。3.4 输出端口类型Sine、Cosine还是正交输出NCO可以只输出正弦Sine only、只输出余弦Cosine only或者同时输出正弦和余弦Sine and Cosine。做单音信号源时选一个就行做正交混频、QPSK调制解调时直接选Sine and Cosine省得外面再拼接90°相移网络。输出位宽Output Width决定了正弦波幅度值的量化位宽。比如16位输出不带符号时最大幅度是2^15-1也就是32767。输出位宽和SFDR直接相关粗略估算SFDR约为6.02 × 输出位宽 1.76 dB。16位输出理论上能到98dB左右的无杂散动态范围实际加上相位截断和量化噪声后通常会打折扣但也能到80~90dB以上。另外界面上还有一个选项叫“Target SFDR”或者“Spurious Free Dynamic Range”一般在输出位宽下方。填目标SFDR后工具可能会自动推荐是否开启相位抖动。这里建议先把目标SFDR设到和理论SFDR接近的值比如16位输出设90dB左右再看工具推荐的抖动策略。3.5 相位抖动Phase Dithering为什么它能压低杂散前面反复提到相位截断这里展开讲一下。当相位累加器位宽高于查找表地址位宽时低位的相位信息会被截断掉只留高位做查表。截断操作会让输出频谱在特定频率点上出现大的杂散分量也就是所谓的相位截断杂散。Phase Dithering的做法是在截断之前给相位累加器的低位加上一个伪随机序列把本来集中在几个频点上的杂散“打散”到整个频带里变成类似白噪声的底噪。这样最大的杂散幅度明显下降SFDR指标提升了代价是噪声基底会稍微抬高一点点。实际配置时如果界面上有“Enable Phase Dithering”选项建议直接勾上。尤其是在输出位宽和相位累加器位宽差距比较大的情况下比如32位累加器、16位输出不勾抖动的后果就是频谱上会出现几个刺眼的杂散峰。3.6 端口总结从界面看到最终信号配置完成后最后一页会列出NCO生成的端口常见的有clk系统时钟clken时钟使能高电平有效reset_n异步复位低电平有效phi_inc_i频率控制字输入只有在配置为可变频率模式时才出现fsin_o正弦输出fcos_o余弦输出只有选正交输出才出现out_valid输出有效指示高电平表示当前周期的输出数据有效这里要重点记住out_valid信号。NCO内部有流水线数据从输入到输出有固定延迟out_valid的作用就是告诉你“当前这个时钟周期的数据是可以用的”。仿真时如果忽略out_valid直接采fsin_o可能在数据还没有效的几个周期里采到垃圾值。4. 配置之后立刻做生成文件与例化模板4.1 生成的文件清单及用途点击Finish后Quartus会在工程目录下生成一组文件。常见文件包括文件后缀作用.v / .vhdIP核顶层文件可直接用于综合和仿真_bb.v黑盒声明文件用于第三方综合工具_inst.v / _inst.vhd例化模板可以直接复制到顶层模块.qipQuartus IP集成文件必须加入工程.sipSynopsys IP集成文件用于部分第三方流程.bsf原理图符号文件_tb.v部分版本会生成参考testbench生成文件后我建议先打开_inst.v看一眼例化模板因为它给了所有端口的连接方式写testbench时可以直接复制改一改。4.2 加入Quartus工程加.qip而不是直接加.v很多人在这里会犯一个错误直接把生成的.v文件Add to Project。这样做在个别版本里能跑但后面升级IP版本、重新生成IP时会很麻烦而且Quartus对IP的版本管理是基于.qip文件的。正确做法是Project - Add/Remove Files in Project选择生成的.qip文件。.qip文件记录了IP核的版本信息、文件清单和参数配置Quartus会根据它自动管理所有依赖文件。如果你只加了.v文件Quartus可能不会编译IP内部的其他资源文件综合时也会报错。在NativeLink仿真时.qip文件也是Quartus识别IP仿真模型的关键。总之一句话要么加.qip要么老老实实复制_inst.v里的例化代码别只加单个.v文件。4.3 查看例化模板并确认端口打开_inst.v文件会看到类似下面的代码nco_top u_nco_top ( .phi_inc_i (phi_inc_i), .clk (clk), .clken (clken), .reset_n (reset_n), .fsin_o (fsin_o), .fcos_o (fcos_o), .out_valid (out_valid) );在实际工程里把这段复制到自己的顶层模块然后定义对应的wire和reg信号即可。端口名和NCO IP配置页里的名字一一对应如果配置时不勾选正交输出fcos_o就不会出现在例化模板里写testbench时也别画蛇添足地连这个端口。另外如果配置时选了固定频率模式Fixed frequency例化模板里不会有phi_inc_i端口频率控制字直接固化在IP内部参数里。需要外部动态改频时必须选可变频率模式Variable frequency此时phi_inc_i就会以总线形式出现在例化模板里。做扫频信号源、跳频通信时这个端口非常关键。5. 从testbench到ModelSim完整仿真流程实操5.1 testbench骨架与频率控制字计算写testbench之前先确定几个参数系统时钟100MHz目标输出频率5MHz相位累加器位宽32位。计算频率控制字f_word f_out × 2^N / f_clk 5e6 × 2^32 / 1e8 214748364.8取整后得到214748365。由于取整实际输出频率会有一点点偏差计算一下f_out_actual 214748365 × 1e8 / 2^32 ≈ 5000000.0238 Hz偏差大约是0.024Hz几乎可以忽略。这就是32位累加器带来的高分辨率优势。5.2 完整testbench代码下面给出一份可以直接运行的testbench模块名按实际生成的IP核名替换timescale 1ns / 1ps module tb_nco; parameter CLK_PERIOD 10; // 100MHz时钟 parameter F_CLK 100_000_000; // 100MHz parameter F_OUT 5_000_000; // 5MHz parameter N 32; // 相位累加器位宽 parameter M 16; // 输出数据位宽 reg clk; reg clken; reg reset_n; reg [N-1:0] phi_inc_i; wire [M-1:0] fsin_o; wire [M-1:0] fcos_o; wire out_valid; initial begin phi_inc_i 32d214748365; // 5MHz 100MHz end initial clk 1b0; always #(CLK_PERIOD/2) clk ~clk; initial begin reset_n 1b0; #100; reset_n 1b1; end initial begin clken 1b0; #200; clken 1b1; end nco_top u_nco ( .phi_inc_i (phi_inc_i), .clk (clk), .clken (clken), .reset_n (reset_n), .fsin_o (fsin_o), .fcos_o (fcos_o), .out_valid (out_valid) ); initial begin $display(NCO Simulation Start); #10000; $display(NCO Simulation Done); $stop; end endmodule注意testbench里复位保持低电平100ns时钟使能clken在复位释放后再拉高。这样做是为了确保NCO内部寄存器先被复位再开始正常工作。如果clken和reset_n同时拉高部分仿真模型可能出现初始化不完全的红色未知态。5.3 在ModelSim中启动仿真的完整命令假设前面已经编译好了Altera库现在把NCO的.v文件、testbench文件放到同一个目录或分别指定路径打开ModelSim后执行vlib work vmap work work # 编译NCO IP核顶层文件 vlog nco_top.v # 编译testbench vlog tb_nco.v # 加载仿真并映射Altera仿真库 vsim -L altera_mf -L altera_primitives -L lpm -L altera_lnsim -L work work.tb_nco # 添加波形 add wave -hex /tb_nco/clk add wave -hex /tb_nco/reset_n add wave -hex /tb_nco/clken add wave -hex /tb_nco/phi_inc_i add wave -hex /tb_nco/fsin_o add wave -hex /tb_nco/fcos_o add wave -hex /tb_nco/out_valid # 运行 run -all这里的关键是vsim命令里的-L参数它指定了仿真库搜索路径。如果不带这些-L参数即使之前编译了库仿真器也不会自动去altera_mf等库里找模型结果还是红线。这一步是最容易遗漏的细节务必记住。如果不习惯命令行也可以在ModelSim GUI里手动操作Compile菜单编译文件Simulate菜单选Start Simulation然后在Library窗口把这几个库勾上。原理和命令行-L参数完全一样只是操作路径不同。5.4 观察波形时关注的信号顺序仿真跑起来后波形窗口里会看到时钟、复位、使能、phi_inc_i和fsin_o等信号。建议按这个顺序观察clk和reset_n确认时钟在翻转、复位信号在100ns后拉高。clken确认在复位释放后拉高。out_valid这个信号拉高后fsin_o/fsin_o上的数据才是有效的正弦采样点。fsin_oout_valid为高之后波形应该呈现规律的正弦阶梯状。如果out_valid一直为低说明复位或时钟使能可能有问题先去检查这两条线。NCO内部有流水线延迟从复位释放到首个out_valid脉冲会有若干个时钟周期的延迟这是正常的别慌。6. 波形是红线NCO仿真常见问题排查6.1 红线在ModelSim里代表什么ModelSim里红色波形通常表示信号处于X未知状态也就是仿真器无法确定它是0还是1。还有一种情况是高阻态Z通常显示为蓝绿色的线。在很多版本里红色X是最常见的异常信号。X状态一旦出现会像污染一样传播一个模块的输入是X输出大概率也是X所以经常看到一整片波形全红。NCO仿真出现红线基本可以归为四类原因仿真库没编译映射、复位时序异常、时钟/使能信号没正常工作、端口连接错误。下面逐个排查。6.2 排查链路从库到端口到复位的完整顺序遇到红线我建议按固定顺序排查不要东看一处西看一处看ModelSim的Transcript窗口有没有“Module not found”或“Failed to link”之类的错误提示。如果有基本就是库没编译好或-L参数没带全。看仿真工程里有没有把NCO生成文件全部编译进去只编译了顶层.v还是把IP依赖的其他.v文件也编译了IP核有时会有多个源文件漏一个就缺模块。检查NCO实例化端口有没有连错比如phi_inc_i位宽不匹配、out_valid接到了别的信号上、时钟使能悬空这些都会让仿真器对内部状态无从判断。检查复位逻辑reset_n是否在仿真初始阶段有明确的0→1跳变如果复位信号本身一直是X态那NCO内部所有寄存器都会保持X红线跑不了。检查clken有些NCO配置要求clken默认使能如果clken一直为0内部可能完全不工作输出保持X或0。6.3 我踩过的最典型的几个坑这里分享几个我在实际调试中真实遇到的案例按坑度排序。坑一忘记编译altera_mf库。第一次用NCO IP核时我以为编译testbench和NCO的.v就完事了结果一仿真全是红色报错里写着lpm_rom not found。折腾了半天才意识到要先把Altera的通用仿真库编译进去。这个坑相信不少人都踩过。坑二reset_n一直被拉低。在某次试验中我为了控制复位时序把reset_n写成了常低电平忙着去改phi_inc_i结果波形里输出一直是一条直线而且out_valid永远不出现。复盘才发现NCO内部有寄存器需要复位释放才能进入正常累加状态复位信号长期有效所有状态都被锁死在复位态。坑三clken没拉高。这个坑其实很蠢但容易在复制粘贴代码时发生testbench里定义了clken却忘了在initial块里给它赋值默认是X。X传播到NCO的时钟使能端整个IP核心就跟“假死”一样输出完全不确定。坑四phi_inc_i设成0。如果把频率控制字设成0NCO输出频率就是0Hzfsin_o会恒定为0或接近0的直流看起来不是红线而是直线。很多人看到直线就怀疑模块坏了其实只要把phi_inc_i改成非零值波形立刻出现。遇到直线输出先检查频率控制字。坑五用了IP核的旧仿真模型。如果之前项目里已经生成过一个旧版本NCO这次重新生成IP后Modelsim还在编译旧的.v文件端口列表对不上也会出现大量X态。检查一下当前编译的文件路径和生成时间可以避免这个问题。6.4 一个实用的定位技巧加$display或$monitor在波形窗口里看X态虽然直观但有时候信号太多红色一片反而不知道从哪查起。我的习惯是在testbench里加几条$display打印关键信号比如always (posedge clk) begin if (out_valid) $display(t%0t out_valid1 fsin_o%0d, $time, $signed(fsin_o)); end仿真过程中如果能看到打印输出且数值在变化说明NCO核心已经工作只是波形显示设置问题如果打印里fsin_o一直是X说明问题在NCO内部或输入端口再去查库和复位链路。靠打印信息可以快速缩小排查范围比盯着波形瞎猜高效得多。另外ModelSim里也可以在波形窗口右键信号 - Radix - Decimal把fsin_o改成十进制显示方便观察正弦采样点变化趋势。如果采样点呈现先增大后减小的规律基本可以判断NCO的输出逻辑是正常的只是显示比例问题让你看着像“红线”或者正常波形。7. 结果验证与参数调整频率、SFDR与资源消耗的平衡7.1 在ModelSim里验证输出频率仿真跑通后第一件事是验证输出频率是否正确。方法很简单在波形窗口里选中两个相邻正弦波峰或过零点看光标时间差这个时间差就是输出信号的周期。再用1除以周期得到频率与目标5MHz对比。以100MHz时钟、5MHz目标输出为例正弦周期是200ns。如果仿真显示周期确实是200ns或者200ns左右因为取整误差只有0.024Hz周期偏差基本看不出来说明配置正确。如果测出来是2μs那很可能phi_inc_i算错了一位小数点如果测出来是100ns那可能是输出频率设成了10MHz要回头检查参数。这里建议把testbench仿真时间拉长一点比如跑到50μs这样可以观察到几百个完整正弦周期频率测量的统计精度更高也更容易看出波形是否存在明显的幅度或相位异常。7.2 把数据导出来做FFT验证SFDR波形看频率只是第一步真正要验证NCO的输出质量还得看频谱。ModelSim本身不带FFT工具我的做法是在testbench里把输出数据写入文件然后导出到本地用Python或MATLAB做FFT分析。testbench里增加文件输出逻辑integer fd; initial begin fd $fopen(nco_output.txt, w); end always (posedge clk) begin if (out_valid) $fwrite(fd, %d\n, $signed(fsin_o)); end initial begin #100000; $fclose(fd); end仿真结束后用Python读取文件并做FFT代码大致如下import numpy as np import matplotlib.pyplot as plt data np.loadtxt(nco_output.txt) fs 100e6 N len(data) win np.blackman(N) spec np.fft.rfft(data * win) freq np.fft.rfftfreq(N, 1/fs) plt.figure(figsize(10, 5)) plt.plot(freq / 1e6, 20 * np.log10(np.abs(spec) / np.max(np.abs(spec)))) plt.xlabel(Frequency (MHz)) plt.ylabel(Amplitude (dBc)) plt.title(NCO Output Spectrum) plt.grid(True) plt.xlim(0, 50) plt.ylim(-120, 5) plt.show()在这张频谱图上主峰应该在5MHz附近其余频点如果杂散非常低比如低于-80dBc说明NCO配置合理。如果看到某个频点出现明显的杂散峰特别是与5MHz不对称的边带大概率是相位截断杂散这时候需要检查相位抖动是否开启。7.3 调整参数的工程经验最后总结一下我调NCO参数的经验供工程参考调整目标推荐参数变化需要注意的代价提高频率分辨率增大相位累加器位宽寄存器资源增加但通常不明显降低输出噪声/提高SFDR增大输出位宽后续数据通路位宽跟着变宽DSP资源增加压低相位截断杂散开启相位抖动噪声基底会略微抬高降低ROM资源占用改用CORDIC或Small ROM延迟增加逻辑资源可能上升动态调频改为Variable模式接入phi_inc_i需要增加外部控制逻辑时序约束更复杂如果是做常规信号源我建议的起点配置是100MHz时钟、32位相位累加器、16位输出、Large ROM实现、开启相位抖动。这个组合在资源、性能和仿真速度上比较均衡调参时容易观察效果。如果之后要做高SFDR的通信前端再把输出位宽提到18位或20位配合dithering一起看频谱。有一个细节容易被忽略NCO输出是有符号数具体表示的是补码还是原码要看IP设置的输出格式。在testbench里用$signed()读取数据可以避免符号解释错误如果在数据分析软件里发现正弦波形有跳变或符号异常优先检查这里。7.4 参数调整后的验证闭环参数调整不是调整完就结束了一定要重新走一遍“重新生成IP - 更新工程文件 - 重新编译仿真 - 看频谱”的闭环。特别是在改动相位累加器位宽或输出位宽后旧的testbench里位宽参数可能对不上编译时会直接报size mismatch警告。先把这类警告清干净再去看波形才是正确的排查顺序。我个人的习惯是每次调整完参数在testbench里固定打印一组关键参数时钟频率、频率控制字、采样点数这样换版本、换电脑时只要看打印信息就知道当前跑的是哪一套配置极大减少“明明改了配置但仿真结果没变”的困惑。这套NCO配置与ModelSim仿真流程我在多个项目里反复用过从一开始踩库编译的坑到后来把FFT频谱验证固化成标准操作形成了一条稳定的开发闭环。如果你也正在折腾NCO建议按本文顺序先跑通最小系统再去调参数之后做多路载波生成、变频处理都会顺很多。
返回列表