1. 从零开始:为什么我们需要NCO IP核?

如果你刚开始接触FPGA数字信号处理,可能会好奇,为什么我们不直接用Verilog写个正弦波发生器,而是要去折腾一个叫NCO的IP核?这事儿我刚开始也琢磨过,后来踩了几个坑才明白,自己手搓一个高质量的正弦波,远没有想象中那么简单。

NCO,全称Numerically Controlled Oscillator,翻译过来就是数字控制振荡器。你可以把它理解成一个“数字化的信号发生器”。它的核心任务是根据你给的一个频率控制字(就是那个phi_inc_i),在每一个时钟周期,精准地计算出当前相位对应的正弦波幅度值。这听起来好像不难,但难点在于“精准”和“高效”。自己用查找表(LUT)实现,你得考虑存储深度(影响频率分辨率)、量化位数(影响信号质量)、还要处理相位累加器的溢出,更别提优化资源消耗了。而Altera(现在是Intel)提供的NCO IP核,把这些复杂的算法和优化都封装好了,你只需要像点菜一样配置几个参数,它就能给你生成一个性能相当不错的正弦波,省时省力还可靠。

在通信系统、音频处理、雷达信号生成这些领域,一个纯净、稳定的本地振荡信号是基础。NCO IP核生成的信号,其频率精度只取决于你的系统时钟和频率控制字的位数,非常稳定,不受环境温度、电压漂移的影响,这是模拟振荡器很难做到的。所以,学会使用并仿真验证这个IP核,是FPGA工程师迈向数字信号处理实战的重要一步。

这次,我就用Quartus Prime 18.0Modelsim这对经典组合,带你完整走一遍流程。从创建工程、配置IP核、编写测试平台,到最终在Modelsim里看到完美的正弦波形。更重要的是,我会重点解决那个让无数新手头疼的“拦路虎”——仿真时遇到的asj等库文件未定义的报错问题。这个坑我当年也掉进去过,折腾了大半天,网上资料零零碎碎,这次咱们把它彻底讲透,让你以后遇到类似问题都能从容应对。

2. 第一步:在Quartus 18.0中创建NCO IP核

万事开头难,但配置NCO IP核其实是个“傻瓜式”操作,关键在于理解每个参数背后的意义。打开你的Quartus 18.0,新建一个工程,这个步骤我就不赘述了。工程建好后,我们直奔主题,找到IP核的入口。

在Quartus的菜单栏,点击 Tools -> IP Catalog。在弹出的IP Catalog窗口中,你在搜索框里输入“NCO”,就能看到“NCO v18.0”这个IP核。双击它,会弹出一个配置向导。这个向导界面信息量不小,咱们一步步来。

首先,给你的IP核实例起个名字,比如nco_ip。重点在“Parameterization”这个标签页里:

  1. Implementation(实现方式):这里通常选择“Small ROM”。它意味着IP核会使用查找表(LUT)来存储正弦波形,这种方式在速度和资源消耗上比较平衡,适合大多数应用。如果你的系统对资源极其敏感,可以考虑“CORDIC”算法,但它计算速度会慢一些。
  2. Angular Precision(角度精度):这个参数决定了相位累加器的位数,直接影响频率分辨率。简单来说,位数越高,你能设置的最小频率步进就越精细。默认值往往够用,但如果你需要非常精细的频率调谐,比如软件无线电(SDR)应用,可以适当调高。
  3. Magnitude Precision(幅度精度):这就是输出正弦波fsin_o的位宽。位宽越高,波形的量化噪声越低,信号质量越好,但同时会占用更多的FPGA逻辑资源。对于一般的演示和测试,10位是一个很常用的值,它能在信号质量和资源占用间取得很好的平衡。我们这里就设为10。
  4. Phase Increment Value(相位增量值):这就是频率控制字phi_inc_i的输入位宽。通常设置为16位或32位。位宽越大,频率控制越精细。我们设为16位,这样输入范围是0到65535。
  5. Clock Rate(时钟频率)和Desired Output Frequency(期望输出频率):这两个参数是联动的。你输入你的系统时钟(比如50MHz),再输入你想得到的正弦波频率(比如1MHz),Quartus会自动帮你计算出对应的phi_inc_i初始值。这个功能非常贴心,省去了手动计算的麻烦。我们假设系统时钟clk是50MHz,想生成一个1MHz的正弦波,就在这里填好。

配置完这些核心参数,其他标签页如“Optional Input/Output Ports”可以保持默认。比如,确保clken(时钟使能)和reset_n(低电平复位)端口被勾选,这是我们控制IP核所必需的。最后点击“Generate HDL”,Quartus就会为你生成一个封装好的IP核文件(.v.vhd)以及一个例化模板。这个模板就是我们接下来写顶层模块的“菜谱”。

3. 编写顶层模块与测试平台

IP核生成好了,它就像一个封装好的芯片,我们需要把它“焊接”到我们自己的电路板(顶层模块)上,并给它提供测试信号(测试平台)。

3.1 顶层模块:连接IP核与外部世界

顶层模块的作用很简单:把FPGA的物理引脚(或者仿真环境的信号)连接到IP核的接口上。下面是我写的一个示例,你可以直接参考:

module nco_test (
    input wire clk,        // 系统时钟,比如50MHz
    input wire reset_n,    // 低电平有效的全局复位
    input wire clken,      // 时钟使能,高电平时IP核才工作
    input wire [15:0] phi_inc_i, // 16位频率控制字
    output wire [9:0] fsin_o,    // 10位有符号正弦波输出
    output wire out_valid        // 输出数据有效标志
);

    // 例化由Quartus生成的NCO IP核
    nco_ip u_nco_inst (
        .clk        (clk),        // 连接时钟
        .reset_n    (reset_n),    // 连接复位
        .clken      (clken),      // 连接使能
        .phi_inc_i  (phi_inc_i),  // 连接频率控制字
        .fsin_o     (fsin_o),     // 获取正弦波输出
        .out_valid  (out_valid)   // 获取有效信号
    );

endmodule

这段代码非常直观。nco_ip就是IP核的名字,u_nco_inst是我们给这个实例起的别名。端口一一对应连接即可。注意,fsin_o是10位有符号数,在Modelsim中查看波形时,我们需要将其设置为“有符号十进制”格式,才能看到正确的负值波形。

3.2 测试平台:给IP核“喂”数据

测试平台(Testbench)就是模拟一个真实的运行环境,产生时钟、复位、控制信号,并观察IP核的输出。写好Testbench是成功仿真的关键。

`timescale 1ns / 1ps  // 定义仿真时间单位/精度

module nco_test_tb();

    // 定义连接到被测模块的信号
    reg clk;
    reg reset_n;
    reg clken;
    reg [15:0] phi_inc_i;

    wire [9:0] fsin_o;
    wire out_valid;

    // 例化被测模块
    nco_test uut (
        .clk        (clk),
        .reset_n    (reset_n),
        .clken      (clken),
        .phi_inc_i  (phi_inc_i),
        .fsin_o     (fsin_o),
        .out_valid  (out_valid)
    );

    // 生成50MHz时钟,周期20ns
    initial begin
        clk = 0;
        forever #10 clk = ~clk; // 每10ns翻转一次,周期20ns
    end

    // 产生测试激励
    initial begin
        // 初始化所有输入
        reset_n = 0;  // 开始时复位有效
        clken = 0;
        phi_inc_i = 16'd0;

        #100; // 等待100ns,让系统稳定(非必须,但是个好习惯)

        // 释放复位
        reset_n = 1;
        #20;

        // 使能IP核,并设置频率控制字
        clken = 1;
        // 计算phi_inc_i: (期望频率 * 2^相位累加器位数) / 系统时钟频率
        // 假设我们想要1MHz输出,系统时钟50MHz,相位累加器32位(IP核内部默认,我们输入是16位,但IP核会处理)
        // 简化计算:对于16位控制字,最大值65535对应输出频率为 Fclk/2。
        // 想要1MHz,phi_inc_i ≈ (1e6 / 50e6) * 65536 ≈ 1310.72,取整1311
        phi_inc_i = 16'd1311;

        // 让仿真运行足够长的时间,以便观察多个周期的波形
        #50000; // 仿真50us,可以看到多个1MHz周期

        $stop; // 停止仿真
    end

endmodule

这个测试平台做了几件事:生成了一个50MHz的时钟;先让系统复位,然后释放;接着使能IP核并设置频率控制字为1311(对应约1MHz输出);最后仿真一段时间后停止。$stop语句很重要,不然仿真会一直跑下去。

4. 协同仿真的两种路径与那个恼人的“asj”错误

代码准备就绪,激动人心的仿真环节来了。用Modelsim仿真Quartus的IP核,通常有两条路可以走,我两种都试过,各有优劣。

4.1 方法一:Modelsim独立工程法(推荐给新手)

这种方法思路直接,避开了很多Quartus的复杂设置,是我更推荐新手使用的方法。它的核心思想是:直接用Quartus综合后的网表文件进行仿真

具体操作如下:

  1. 在Quartus中,对你的工程进行全编译(Start Compilation)。编译成功后,去你的工程目录下找,会发现一个名为 simulation 的文件夹。
  2. 在这个文件夹里,有一个非常重要的文件:<你的工程名>.vo。这个.vo文件是门级网表文件,它已经把我们的Verilog代码、IP核的内部结构都翻译成了基本的逻辑门和Altera底层原语。用这个文件仿真,就相当于对实际烧录到FPGA里的电路进行仿真,非常准确。
  3. 打开Modelsim,新建一个工程。
  4. 向这个工程中添加仅两个文件:一个是你的测试平台文件 nco_test_tb.v,另一个就是刚才找到的 .vo 文件。
  5. 编译这两个文件。这里有个小细节:编译.vo文件时,Modelsim可能会提示缺少一些Altera的库。这时你需要手动添加这些库的路径。库的路径通常在Quartus安装目录下,例如 intelFPGA_lite/18.0/quartus/eda/sim_lib。把 altera_mf.vcycloneive_atoms.v(根据你的FPGA器件系列选择)等库文件也添加到工程并编译。
  6. 编译无误后,切换到“Library”标签,找到你的测试平台模块 nco_test_tb,右键点击“Simulate”。
  7. 添加需要观察的信号到波形窗口,运行仿真。如果一切顺利,你就能在波形窗口看到 fsin_o 端口输出一个完美的正弦波了。

这种方法的好处是稳定,不依赖Quartus的仿真设置,尤其适合IP核仿真。因为.vo文件已经包含了IP核的全部实现细节。

4.2 方法二:Quartus直接调用Modelsim(及“asj”陷阱)

另一种方法是在Quartus中设置好仿真工具,然后一键调用Modelsim。听起来很美好,但新手极易在这里翻车,错误提示通常是一连串的:

** Error: (vsim-3033) .../.../nco_ip/simulation/submodules/asj_nco_xxx.v(50): Instantiation of 'asj_nco_xxx' failed. The design unit was not found.

这个 asj 文件未定义的错误,我当初遇到时也是一头雾水。问题的根源在于:NCO等复杂的IP核,其仿真模型并不是单个.v文件,而是一组文件,并且依赖于特定的仿真库。当你让Quartus调用Modelsim时,Quartus需要告诉Modelsim去哪里找这些文件。

解决这个问题的关键,在于正确生成并添加 .sip 文件.sip文件是一个“仿真信息包”文件,它里面记录了该IP核仿真所需的所有源文件路径和依赖关系。

正确的操作步骤是:

  1. 回到Quartus的IP核配置界面(就是之前生成IP核的那个窗口)。
  2. 在生成IP核的最后一步,有一个 “Generate”“Generation” 的标签页。
  3. 在这个页面的 “Simulation” 区域,你会看到一个创建仿真模型或生成.sip文件的选项。务必勾选上“Generate Simulation Model”或类似的选项。
  4. 更重要的是,在下面的文件列表里,确保选中所有与仿真相关的文件(通常包括.v.vo.sip文件)。很多教程只提了勾选,但没强调要“全选”,这里漏选任何一个都可能导致后续错误。
  5. 点击“Generate”重新生成IP核。这次,Quartus除了生成硬件用的文件,还会在输出目录(通常是 <工程目录>/simulation/modelsim)下生成一个 .sip 文件。
  6. 在Quartus主界面,进入 Assignments -> Settings -> EDA Tool Settings -> Simulation
  7. 在“NativeLink settings”下,找到“Compile test bench”或“More EDA Netlist Writer Settings”,点击进入。
  8. 你需要在这里添加测试平台文件,并且关键一步:在“Library”或“Search Path”设置中,添加包含.sip文件和IP核仿真文件的目录路径。或者,更简单的方法是,确保你在生成IP核时,输出目录被添加到了Modelsim的仿真库搜索路径中。
  9. 设置完成后,在Quartus中点击 Tools -> Run Simulation Tool -> RTL Simulation,Quartus就会自动启动Modelsim,并加载所有必要的文件(包括那个关键的.sip文件),这时仿真就能顺利进行了。

如果还是报错,你可以手动在Modelsim中检查:点击菜单栏的 Simulate -> Start Simulation,在“Libraries”标签页里,点击“Add”,将IP核生成目录下的仿真库文件夹(里面包含asj等子目录)添加进来。这相当于手动给Modelsim指路。

5. 仿真结果分析与波形解读

无论你通过哪种方法,当仿真成功运行后,在Modelsim的波形窗口里,你应该能看到类似下图的信号:

此处为描述,实际文章中无图片

  • clk:规律的方波时钟。
  • reset_n:仿真初期为低电平(复位),之后拉高。
  • clken:拉高后,IP核开始工作。
  • phi_inc_i:保持为我们设置的常数值(如1311)。
  • out_valid:在clken有效后不久,会持续输出高电平,表示数据有效。
  • fsin_o:这是主角!它应该显示为一个规则的、周期性的模拟波形。你需要将其显示格式设置为“有符号十进制”(Signed Decimal)或“模拟波形”(Analog),才能看到连续的正弦曲线,而不是一堆跳变的数字。

如何验证我们的正弦波是否正确?

  1. 测周期:测量fsin_o两个相邻波峰(或波谷)之间的时间差。对于1MHz的信号,这个周期应该是 1微秒(1000ns)。在50MHz时钟下,一个周期内应该包含50个时钟点。你可以放大波形仔细数一数,或者用Modelsim的测量工具。
  2. 看幅度:我们设置的是10位有符号输出,其理论输出范围是-512到+511。观察波形的峰值和谷值,应该在这个范围内。由于NCO IP核内部可能进行了缩放或处理,实际最大值可能不是严格的511,但应该非常接近。
  3. 观察启动过程:注意reset_n释放、clken有效后,到fsin_o输出第一个稳定数据,以及out_valid拉高,这中间是有几个时钟周期延迟的。这是IP核内部流水线处理的正常现象,在编写实际系统时序时要考虑这个延迟。

6. 仿真流程优化与高效调试技巧

走通了整个流程,算是入门了。但想提高效率,避免每次仿真都手忙脚乱,还得掌握一些优化技巧。

首先,建立并复用仿真库。 每次新建工程都手动添加Altera的仿真库文件(如altera_mf.v)太麻烦了。你可以在Modelsim中一次性编译这些库到一个指定目录(比如altera_lib),然后以后每个新工程都可以直接映射这个库。具体命令是在Modelsim的Transcript窗口输入:

vlib altera_lib
vmap altera_lib ./altera_lib
vlog -work altera_lib <你的Quartus安装路径>/quartus/eda/sim_lib/altera_mf.v
vlog -work altera_lib <你的Quartus安装路径>/quartus/eda/sim_lib/cycloneive_atoms.v # 根据你的器件系列选择

这样,在你工程的modelsim.ini文件里,或者直接在仿真设置中,添加这个库的引用即可。

其次,编写自动化的仿真脚本(.do文件)。 用鼠标点来点去加载设计、添加波形、运行仿真,既慢又容易出错。我们可以用Tcl脚本把所有操作自动化。创建一个sim.do文件,内容如下:

# 清空现有库和工作区
vlib work
vmap work work

# 编译设计文件(如果你用方法一,这里编译.vo和testbench)
vlog ../source/nco_test_tb.v
vlog ../output_files/nco_test.vo
# 或者编译库文件
vlog -sv ../altera_lib/*.v

# 启动仿真,指定顶层测试模块
vsim -L altera_lib -t ns work.nco_test_tb

# 添加感兴趣的信号到波形窗口
add wave -position insertpoint sim:/nco_test_tb/*
add wave -position insertpoint sim:/nco_test_tb/uut/*

# 对fsin_o信号以模拟波形形式显示
configure wave -signalnamewidth 1
configure wave -timelineunits ns
wave zoom full

# 运行仿真
run 100us

以后每次仿真,只需要在Modelsim的Transcript窗口输入 do sim.do,一切就自动完成了,波形也添加好了,非常高效。

最后,善用Quartus的SignalTap II进行板上调试。 仿真通过,只代表设计在理论上正确。实际下载到FPGA后,信号质量如何?有没有时序违规?这时就需要SignalTap这款片上逻辑分析仪。你可以在Quartus中嵌入SignalTap,选择要观察的信号(比如fsin_o),编译后下载到FPGA。在系统运行时,实时捕获这些信号并传回电脑查看。这对于调试IP核在实际硬件中的行为,验证时钟、复位是否干净,至关重要。仿真和实测相结合,才能确保万无一失。

整个流程走下来,从IP核配置、代码编写、解决仿真环境问题,到最终波形验证和效率优化,其实就是一个典型的FPGA开发闭环。遇到asj文件这类问题不要慌,本质都是仿真工具找不到对应的模型文件,要么像方法一那样直接用综合后的网表(.vo)避开它,要么像方法二那样,确保仿真模型(.sip及相关文件)被正确生成和引用。多操作几次,把环境配顺了,以后再用到其他IP核,比如PLL、FIFO,思路都是相通的。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐