这一篇在干嘛?

上一篇末尾给了段「抄走就能用」的例化代码——真抄走跑综合,立刻炸出一屏报错:信号未声明、模块是黑盒、约束文件不认、仿真找不到库。这不是巧合,而是每个 FPGA 新手必经的坑:手册上的代码是骨架,工程里的名字要对齐你实际生成的 IP。这一篇完整记录从报错到仿真全绿的调试过程,五个坑个个真实,踩过一遍你就出师了。

报错一:'clk_100m' is not declared

综合器给的第一记闷棍:

ERROR: 'clk_100m' is not declared in new.v(13)
ERROR: ignore module module due to previous errors in new.v(1)

翻回上一篇的例化代码看,问题有三个:

  1. 端口列表是空的:写成了 module new( );clk_50msys_rst_n 这些信号既不是端口也没声明;
  2. 第 13 行用了没声明的 clk_100malways @(posedge clk_100m ...) 里这个信号整个文件里凭空出现,连「默认推断成 wire」的机会都没有,直接 ERROR;
  3. 名字错位:PLL 输出接到了 clk_c0,下游逻辑用的却是 clk_100m——两个名字对不上,就算补了声明时钟也传不过去。

修好的完整顶层(这才是能跑的版本):

module new (
    input  wire clk_50m,      // 50MHz 晶振,来自 GCLK 管脚
    input  wire sys_rst_n,    // 低有效复位按键
    output wire rst_100m_n    // 同步释放后的 100MHz 域复位(低有效)
);
 
wire clk_100m;                // PLL C0 输出的 100MHz 系统时钟
wire pll_locked;              // PLL 锁定指示,高 = 输出可信
 
PLL_0 u_pll (
    .refclk   (clk_50m),      // 来自 GCLK 管脚的晶振时钟
    .reset    (~sys_rst_n),   // 复位,高有效,注意取反
    .extlock  (pll_locked),   // 锁定指示,高 = 输出可信
    .clk0_out (clk_100m)      // C0 输出 100MHz
);
 
// 用 extlock 门控下游逻辑的复位释放
reg [3:0] rst_sync;
always @(posedge clk_100m or negedge pll_locked) begin
    if (!pll_locked) rst_sync <= 4'b0000;               // 未锁定:保持复位
    else             rst_sync <= {rst_sync[2:0], 1'b1}; // 锁定后同步释放
end
assign rst_100m_n = rst_sync[3];
 
endmodule

坑在这里

Verilog 里每个信号都必须有出处:要么是端口,要么显式 wire/reg 声明。裸用一个没声明的名字,有的工具会善意推断,有的直接报错——别赌。另外顶层如果是真实的芯片设计,clk_50msys_rst_n 的引脚约束也一个都不能少(后面 BIT-8123 就是这笔账)。

报错二:pll_100m is a black box

信号声明补齐后,第二刀来了:

ERROR: pll_100m is a black box in ../../new.v(1)
CRITICAL-WARNING: instantiate unknown module pll_100m in ../../new.v(11)

「黑盒」= 综合器在整个工程里找不到这个模块的定义。原因很简单:IP 生成的模块根本不叫 pll_100m。怎么查真名?两个办法:

  • 打开 Hierarchy(层次)窗口,能看到 pll_0 - rtl (al_ip/PLL0.ipc)——真名就写在这儿;
  • 直接打开 al_ip/PLL_0.v,第一行 module PLL_0 (...) 和端口表一目了然。

对照生成文件,端口名也和想当然的不一样:输出端口叫 clk0_out,不是 clkcclkc 是 IP 内部原语的连线名)。正确的例化:

PLL_0 u_pll (
    .refclk   (clk_50m),
    .reset    (~sys_rst_n),
    .extlock  (pll_locked),
    .clk0_out (clk_100m)     // 注意:是 clk0_out,不是 clkc
);

顺手核对一下 IP 配置有没有配错——打开 al_ip/PLL_0.ipc 能看到 input_frequency = 50MHzclock_frequency = 100MHz,和设计意图一致,放心用。

坑在这里

例化的模块名、端口名,永远以 IP 生成文件为准,别凭记忆或者抄别家的代码。生成器每次生成的名字跟随你创建 IP 时起的名字(这次叫 PLL_0,下次你建个 pll_100m 的 IP 它才真叫 pll_100m)。例化前花 10 秒打开生成的 .v 文件扫一眼 module 头,能省半小时综合报错。

报错三:BIT-8123 —— 引脚没约束

综合过了,布局布线又拦住你:

TD Log Tree:Physical failed 弹窗,BIT-8123 未约束 IO 错误,以及 6 条可无视的 HDL-5001 警告

ERROR: Bitgen is prohibited as there is(are) unconstrained io(s) in design.

顶层三个端口 clk_50msys_rst_nrst_100m_n 都还没分配 FPGA 实际引脚——工具不知道把信号引到芯片哪条腿上,拒绝生成码流。这个错不该等跑完综合才发现:引脚约束本该在设计初期就定下来。

安路 TD 的约束文件是 .adc 后缀的文本文件,内容是一条条 set_pin_assignment

set_pin_assignment { clk_50m } { LOCATION = R7; IOSTANDARD = LVCMOS33; PULLTYPE = NONE; }
set_pin_assignment { sys_rst_n } { LOCATION = A2; IOSTANDARD = LVCMOS33; PULLTYPE = PULLUP; }
set_pin_assignment { rst_100m_n } { LOCATION = A4; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }

三个语法要点:

  • 属性大写LOCATION / IOSTANDARD / PULLTYPE),属性之间用分号分隔;
  • 输入脚可以加 PULLTYPE = PULLUP 内部上拉(复位键常用),输出脚可以指定驱动强度 DRIVESTRENGTH
  • 写完在工程里 Constraints → Add ADC File(s) 添加进来,重新跑 Physical。

写约束时还踩了一个隐藏坑——一开始在文件头加了说明注释:

TD Log Tree:USR-8168 报错,.adc 文件第 1 行的 // 注释被判定为非法 ADC 命令

ERROR: // new.v 顶层引脚约束 (EG4S20BG256) in new.adc(1) is not a valid ADC command

.adc 文件不支持注释

ADC 解析器逐行解析,// 开头的行会被当成非法命令报错(USR-8168)。想让约束文件可读,可以在网络命名上下功夫,或者把说明写在工程 README 里——就是别写进 .adc

这些警告可以无视:HDL-5001

跑通之后 Log Tree 里还挂着 6 条黄色警告:

WARNING: Contains anonymous inst(s) and/or net(s) that have not been renamed

这是说设计里存在没有显式命名的实例/连线(工具自动起了 PLL_0_open0PLL_0_open12 这类占位名)。打开 al_ip/PLL_0.v 一看便知:这堆「匿名」全是 IP 生成器自己产的中间连线,不是你的代码问题。它只影响你看网表时名字不好认,功能、时序、布局布线零影响——IP 生成的设计报这个几乎是必然,直接无视。

仿真关:Module 'PLL_0' is not defined

上板前先仿真。把测试平台加进 ModelSim 一跑:

** Error: (vsim-3001) Module 'PLL_0' is not defined.

原因:ModelSim 工程里只编译了 new_tb.vnew.v,没有 PLL_0 的定义。把 al_ip/PLL_0.v 加进去就行?没那么快——它内部例化的是 EG_PHY_PLLEG_LOGIC_BUFG 这样的厂商原语,ModelSim 必须有安路官方仿真库才能解析这些原语。

这里就要把「IP 的两层黑盒」讲清楚:

  • 包装层透明PLL_0.v 只是个壳,把原语按你的配置参数(输入 50MHz、反馈 ×2、C0 分频 10 → 100MHz)例化起来,源码随便看;
  • 原语层黑盒EG_PHY_PLL 对应芯片里真实的模拟电路(压控振荡器、电荷泵),Anlogic 不公开内部实现。综合时它直接映射到芯片里的专用 PLL 硬件;仿真时需要官方仿真库提供的行为级模型——那个模型也只模拟「复位后延迟一段时间锁定、输出时钟出现」的外显行为,真实锁定的几百微秒耗时、抖动、相噪都不在内。

对 PLL 这类模拟硬核,你该验证的从来不是它本身(那是厂商验证过的事),而是你的逻辑围绕它工作得对不对。所以最实用的做法是写一个行为级模型替身:

// sim_pll_0.v —— 仅用于仿真,勿加入 FPGA 工程
`timescale 1ns / 1ps
module PLL_0 (
    input  wire refclk,
    input  wire reset,
    output reg  extlock,
    output wire clk0_out
);
    // 100MHz = 10ns 周期(行为级自由振荡,不关心相位)
    reg clk100;
    initial clk100 = 1'b0;
    always #5 clk100 = ~clk100;
    assign clk0_out = clk100;
 
    // 锁定行为:reset(高有效)释放后随 refclk 拉高 extlock
    initial extlock = 1'b0;
    always @(posedge refclk or posedge reset) begin
        if (reset) extlock <= 1'b0;
        else       extlock <= 1'b1;
    end
endmodule

ModelSim 里用这个文件代替 al_ip/PLL_0.v 参与编译(vlog new.v sim_pll_0.v new_tb.v),整个设计就能仿真了。注意一个细节:模型里 reset 是高有效,正好接顶层送进来的 ~sys_rst_n,和真实 IP 行为一致。

判定不靠肉眼:自检型 Testbench

仿真跑起来后,「对不对」怎么判?靠肉眼对着波形量周期?可以,但每次改一行代码都量一遍,迟早量花眼。更好的做法是让仿真器自己当裁判——把预期写成检查逻辑,跑完只看 PASS/FAIL:

integer errors;
 
// 检查 1/2:量时钟周期,必须精确 20ns / 10ns
real t50_last;
initial t50_last = 0.0;
always @(posedge clk_50m) begin
    if (t50_last > 0.0)
        if (($realtime - t50_last) != 20.0) begin
            errors = errors + 1;
            $display("[FAIL] %0t : clk_50m period = %t, expect 20000ps",
                     $time, ($realtime - t50_last));
        end
    t50_last = $realtime;
end
 
// 检查 4:锁定后 rst_100m_n 应在 3~5 个 clk_100m 周期内同步释放
integer clk100_after_lock;
initial clk100_after_lock = 0;
always @(posedge u_dut.clk_100m)
    if (u_dut.pll_locked) clk100_after_lock = clk100_after_lock + 1;
 
always @(posedge rst_100m_n) begin
    if ((clk100_after_lock < 3) || (clk100_after_lock > 5)) begin
        errors = errors + 1;
        $display("[FAIL] %0t : reset released after %d clk_100m cycles, expect ~4",
                 $time, clk100_after_lock);
    end else
        $display("[PASS] %0t : rst_100m_n released after %d clk_100m cycles",
                 $time, clk100_after_lock);
end
 
// 结束时给出总判定
initial begin
    #2000;
    if (errors == 0) $display("RESULT: ALL CHECKS PASSED (0 errors)");
    else             $display("RESULT: FAILED, %0d errors", errors);
    $finish;
end

完整的检查一共五项:① clk_50m 周期 = 20ns;② clk_100m 周期 = 10ns(验证 2 倍频);③ 复位释放前 rst_100m_n 保持有效;④ 锁定后 3~5 拍内同步释放;⑤ 释放后不再掉回 0。

跑出来的 Transcript 长这样:

ModelSim 仿真结果:PASS 与 ALL CHECKS PASSED,0 errors

[PASS] 145000 : rst_100m_n released after 4 clk_100m cycles
RESULT: ALL CHECKS PASSED (ended at 2000000, 0 errors)

数字逐个解读:复位 100ns 释放 → PLL 模型在 110ns 拉高 extlock(复位释放后第一个 refclk 上升沿)→ rst_sync 随 100MHz 时钟逐拍移位,145ns 时第 4 拍打满rst_100m_n 同步拉高。五项检查零错误,复位同步链路验证闭环。

文本判定 vs 看波形,怎么分工?

文本自检是判定,波形是调试。日常迭代改一行代码,重跑 TB 看 ALL CHECKS PASSED 十秒确认没改坏;只有报 FAIL 的时候才打开波形窗口,看具体哪一拍翻转异常。专业流程里成百上千条用例过夜回归,全靠这种自检型 TB;波形窗口是诊断工具,不是验收工具。

小结:五个坑的避坑清单

调试路径回顾

  1. 信号必须声明:端口或 wire/reg,别裸用名字;PLL 输出和下游逻辑要用同一个信号名;
  2. 例化对齐生成文件:模块名(PLL_0)和端口名(clk0_out)打开 al_ip/*.v 核对,别凭记忆写;
  3. 引脚约束趁早.adc 文件、set_pin_assignment、大写属性加分号;不支持 // 注释(USR-8168);没约束 Bitgen 直接 BIT-8123 拒生成;
  4. HDL-5001 警告无视:IP 自动命名的连线,无害;
  5. 仿真用行为级替身:原语是黑盒,行为级模型验证「你的逻辑围绕 IP 对不对」;验证交给自检 TB,看 PASS/FAIL 不用肉眼量波形。

还有一个认知层面的收获:调试不是「代码写错了很丢人」,而是你和工具链的对话。每条报错都在精确告诉你它缺什么——学会读报错,比学会写代码更快。

自测