这一篇在干嘛?
上一篇末尾给了段「抄走就能用」的例化代码——真抄走跑综合,立刻炸出一屏报错:信号未声明、模块是黑盒、约束文件不认、仿真找不到库。这不是巧合,而是每个 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)
翻回上一篇的例化代码看,问题有三个:
- 端口列表是空的:写成了
module new( );,clk_50m、sys_rst_n这些信号既不是端口也没声明; - 第 13 行用了没声明的
clk_100m:always @(posedge clk_100m ...)里这个信号整个文件里凭空出现,连「默认推断成 wire」的机会都没有,直接 ERROR; - 名字错位: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_50m和sys_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,不是 clkc(clkc 是 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 = 50MHz、clock_frequency = 100MHz,和设计意图一致,放心用。
坑在这里
例化的模块名、端口名,永远以 IP 生成文件为准,别凭记忆或者抄别家的代码。生成器每次生成的名字跟随你创建 IP 时起的名字(这次叫
PLL_0,下次你建个pll_100m的 IP 它才真叫pll_100m)。例化前花 10 秒打开生成的.v文件扫一眼 module 头,能省半小时综合报错。
报错三:BIT-8123 —— 引脚没约束
综合过了,布局布线又拦住你:

ERROR: Bitgen is prohibited as there is(are) unconstrained io(s) in design.
顶层三个端口 clk_50m、sys_rst_n、rst_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。
写约束时还踩了一个隐藏坑——一开始在文件头加了说明注释:

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_open0、PLL_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.v 和 new.v,没有 PLL_0 的定义。把 al_ip/PLL_0.v 加进去就行?没那么快——它内部例化的是 EG_PHY_PLL、EG_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
endmoduleModelSim 里用这个文件代替 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 长这样:

[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;波形窗口是诊断工具,不是验收工具。
小结:五个坑的避坑清单
调试路径回顾
- 信号必须声明:端口或
wire/reg,别裸用名字;PLL 输出和下游逻辑要用同一个信号名;- 例化对齐生成文件:模块名(
PLL_0)和端口名(clk0_out)打开al_ip/*.v核对,别凭记忆写;- 引脚约束趁早:
.adc文件、set_pin_assignment、大写属性加分号;不支持//注释(USR-8168);没约束 Bitgen 直接 BIT-8123 拒生成;- HDL-5001 警告无视:IP 自动命名的连线,无害;
- 仿真用行为级替身:原语是黑盒,行为级模型验证「你的逻辑围绕 IP 对不对」;验证交给自检 TB,看 PASS/FAIL 不用肉眼量波形。
还有一个认知层面的收获:调试不是「代码写错了很丢人」,而是你和工具链的对话。每条报错都在精确告诉你它缺什么——学会读报错,比学会写代码更快。
自测
自测
ERROR: 'clk_100m' is not declared的三个可能原因是什么?本例里实际中了哪三个?- 「black box」报错说明什么?怎么快速查出一个 IP 生成的真名和端口表?
- BIT-8123 为什么会发生?
.adc约束文件里给复位输入脚加PULLTYPE = PULLUP是什么意思?.adc文件里能不能写//注释?会报哪个错误码?- 为什么 ModelSim 直接编译
al_ip/PLL_0.v会失败?行为级仿真模型和真实芯片的差距主要在哪?- 自检型 TB 里「锁定后 3~5 拍内释放复位」这个检查,为什么允许一个范围而不是精确 4 拍?如果换用官方仿真库(真实锁定时间几百 µs),这个检查还能通过吗?