验证项目的第一步不是搭环境,而是读 Spec。08-10 已经冻结了 DUT 的边界:APB 从机 + 11 个寄存器、卷积(3×3/5×5, stride=1)、2×2 最大池化、全连接、ReLU、INT32 累加 + requant、特征图乒乓双缓冲、完成中断。本节把这些"功能描述"翻译成可执行、可追踪、可度量的验证点 —— 这张表会贯穿整个项目,也是面试里"你怎么从一个 Spec 开始验证"的标准答案。
是什么:需求拆解 = 逐行读 Spec,为每条功能/每类边界提出三个问题:①怎么激励它(写什么寄存器、灌什么数据)?②怎么检查它(和谁比、在哪一层检查)?③怎么度量它(哪个覆盖率 bin 标记"测过了")?拆解的产出就是验证点表格。
为什么:不拆解就开写用例,必然漏测"不显眼"的功能 —— 中断使能、读清除语义、非法层类型、N−1 编码边界(0 表示 1 通道),这些恰恰是流片后最难补的洞。拆解表还能当"验证完成"的裁判:表上每行都有对应的用例与覆盖 bin,全部绿了才算做完。
怎么做 —— 本项目 11 条验证点(可直接抄进你的验证计划):
| 点号 | DUT 特性(Spec 依据) | 验证方法 | 用例名 |
|---|---|---|---|
| V1 | 寄存器读写属性 / 复位值(11 个寄存器,VERSION=0x0001_0000) | RAL 前门读写 + 复位值检查 + bit-bash 序列 | reg_reset_test / reg_bitbash_test |
| V2 | 卷积 3×3 stride=1(+bias+requant) | 固定数据定向比对 golden(SV refmodel) | smoke_conv3x3_test |
| V3 | 卷积 5×5(k5=1) | 定向 + 随机尺寸/通道,refmodel 比对 | conv5x5_test |
| V4 | 2×2 最大池化(layer_type=001) | 定向已知矩阵(手算期望) | pool_max2x2_test |
| V5 | 全连接(layer_type=010) | 定向 1×N 向量 × 矩阵,refmodel 比对 | fc_test |
| V6 | ReLU 使能(relu_en 0/1) | 同一层数据开关 relu 各跑一次,负数输出应翻转 | relu_on_off_test |
| V7 | 乒乓切换 + back-to-back 层间接力 | 连续 start 两层,输出块地址交换正确、无读写冲突 | pingpong_b2b_test |
| V8 | 中断行为(ie 门控、电平 irq、done 读清除) | 定向中断序列 + SVA 断言(见 1.2) | irq_level_test |
| V9 | 异常配置:非法 layer_type(3'b011/1xx)、idle=0 时写 start、地址未对齐 | 异常用例:期待 pslverr / start 被忽略 / 无 panic | illegal_cfg_test |
| V10 | 边界尺寸:fm_in=1、1 通道、256 通道(N−1 满量程)、特征图 RAM 16K 边界 | 随机约束边界取值优先 + 定向极值 | boundary_test |
| V11 | 随机层组合整体行为(LeNet 五层全流水) | 约束随机回归 N 个 seed,refmodel 全比对 | rand_all_test(回归) |
是什么:搭环境前必须先回答"接口上每一根信号,验证要盯它什么"。本项目对外只有三组信号:APB 从机口(psel/penable/pwrite/paddr[7:0]/pwdata/prdata/pready/pslverr)、irq 中断、clk/rst_n。每组的检查点如下。
pslverr && psel && penable 采成带地址的错误事务,scoreboard 期待表比对 —— 错误响应本身也是被验证的功能,不是环境报错。paddr[1:0]==2'b00;RAM 通路里 BIAS_ADDR 必须按 INT32 4 字节对齐(08-10 寄存器表备注)。对齐违例用 SVA 断言最合适(见下)。// 三条最值钱的接口断言(SVA,写在 bind 到 npu_top 的 checker 里)
property p_apb_align;
@(posedge clk) disable iff (!rst_n)
(psel && penable) |-> (paddr[1:0] == 2'b00); // 地址 4 字节对齐
endproperty
property p_pslverr_only_in_access;
@(posedge clk) disable iff (!rst_n)
pslverr |-> (psel && penable); // 错误只准在 ACCESS 相报
endproperty
property p_irq_level_semantics;
@(posedge clk) disable iff (!rst_n)
(irq && rst_n) |=> (irq until_with (psel && penable && !pwrite &&
(paddr == 8'h04))); // 电平中断:读到 STATUS 才许撤
endproperty
// 使用:assert property (p_apb_align) else `uvm_error("SVA", "paddr 未对齐")
psel && penable && pready 的 ACCESS 相采样,SETUP 相的信号无意义;monitor 里写成 if (psel && penable && pready) 一个 if 覆盖读写两路。「为什么选电平中断?」—— 轮询与中断控制器都友好、不会因 CPU 关中断丢事件,代价是要有明确的清除动作(读 STATUS),这个"清除契约"必须进验证点 V8。uvm_fatal,异常用例根本没法跑 —— 它是期待中的响应;③断言写在 class 里编译不过(SVA 必须在 module/Checker/interface 中,用 bind 注入 DUT);④irq 电平语义验成"每层一个脉冲",与 Spec 契约不符。是什么:验证计划(vPlan/test plan)是验证项目的"施工图":一张表把 Spec 需求编号 → 验证点 → 激励/检查方法 → 用例名 → 覆盖率 bin → 状态 六列锁死,任何一行改动都可追踪、可 review。
| 列 | 内容示例(V8 行) | 为什么要有这列 |
|---|---|---|
| 需求编号 | REQ-06(08-10 Spec「完成以中断通知」) | 追溯到 Spec 原文,防"凭记忆验证" |
| 验证点 | ie 门控 / 电平 irq / done 读清除 | 把需求拆成可检查的最小单元 |
| 检查方法 | 定向序列 + SVA p_irq_level_semantics + scoreboard 记录 | 明确"谁负责判错" |
| 用例 | irq_level_test(ie=0 与 ie=1 两段) | 落到可执行的代码 |
| 覆盖率 | cg_irq:ie 2 bins × done 事件 cross | 落到可度量的 bin(闭环的终点) |
| 状态 | 未开始 / 已运行 / 通过 / 覆盖收敛 | 进度管理与每日站会汇报 |
review 流程怎么做:①计划评审(设计 + 验证 + 架构三方 30 分钟):逐行过表,设计者补"你没注意到的角"(如 start 的忽略语义),验证者补"你没验到的角";②用例评审:每个用例头部注释必须写清"对应哪条验证点、期待什么现象";③每日覆盖率评审:回归结果 + 覆盖率增量,绿了销项,红了排期。真实公司里 vPlan 常挂在 Cadence vManager / Synopsys Verdi-Manager,个人项目一张 Excel/Markdown 表足矣。
是什么:同一个环境上跑三种激励:定向(directed)是手工构造的精确场景;随机(constrained-random)是约束驱动的批量场景;回归(regression)是把前两者批量化的运行机制(多 seed、自动判成败、自动归档)。
| 维度 | 定向用例 | 随机用例(约束随机 CRV) |
|---|---|---|
| 干什么 | smoke 通路、每算子关键路径、异常/边界精确复现 | 参数空间扫荡:层类型×尺寸×通道×地址×relu/k5/requant |
| 为什么 | 确定性结果可手算、可 debug、环境坏了第一个发现 | 覆盖工程师想不到的组合,撞出 corner bug |
| 本项目 | V1~V9 的 smoke/算子/异常/边界用例 | V10/V11:rand_all_test + 边界加权约束 |
| 占比经验 | 约 20%,但决定环境能不能跑 | 约 80% 的仿真机时,决定覆盖能不能收敛 |
回归管理怎么做:①用 seed 控制可复现:./simv +UVM_TESTNAME=rand_all_test +ntb_random_seed=42,每个失败必须记录 seed;②夜间回归跑满随机用例 × N 个 seed(如 50),按 UVM report server 的 UVM_ERROR 计数自动判 pass/fail;③失败自动归档:脚本把失败的 log、波形、coverage 快照、seed、DUT/TB 的 git commit 号打包进 regr_fail/<date>_<test>_<seed>/,第二天只看归档目录 —— 没有这一步,夜间 50 个失败会淹没你的邮箱。
08-12 的最小环境在这里升格为"NPU 项目版":总线侧换成 APB agent(读写 11 个寄存器 + RAM 写通路),新增一路 irq_monitor 观察完成中断,新增 refmodel 行为级黄金模型与 scoreboard 逐块比对。组件树不变 —— 还是 test/env/agent 那棵树,变的只是"插在树上的零件"。
是什么:本项目环境共 7 类组件:apb_agent(active:sequencer + driver + monitor,负责全部 APB 事务)、irq_monitor(passive,采样 irq 与 done,广播"层完成事件")、refmodel(镜像寄存器与两块 RAM,算出期望输出块)、npu_scoreboard(期望块 vs 实际块逐字节比对)、npu_coverage(订阅完成事件采 covergroup)、npu_env(装配容器)、npu_base_test + 各 test(配置与激励策略)。
目录结构与编译(工程化最低配置):
npu_dv/ ← 与 RTL 仓库分开,dv = design verification
├── tb/ ← 顶层连接(静态世界)
│ ├── npu_if.sv ← APB + irq + clk/rst_n 接口,clocking block
│ └── tb_top.sv ← 例化 DUT/接口,config_db 注入,run_test()
├── sv/ ← 组件(动态类世界)
│ ├── npu_txn.sv ← apb_txn / irq_evt / exp_blk 三个事务类
│ ├── npu_ral.sv ← RAL 寄存器模型 + adapter(08-12 §7.2 直接复用)
│ ├── apb_agent/ ← apb_cfg / apb_driver / apb_sequencer / apb_monitor
│ ├── irq_monitor.sv
│ ├── npu_refmodel.sv ← 行为级黄金模型(本页 2.2)
│ ├── npu_scoreboard.sv ← (本页 2.3)
│ ├── npu_coverage.sv ← (本页 4.1)
│ ├── npu_env.sv
│ └── npu_tests/ ← smoke/算子/随机/异常 各用例(本页 2.4)
├── sim/
│ ├── filelist.f ← 文件清单,编译脚本只认它
│ ├── run_vcs.sh / Makefile ← 一键编译仿真
│ └── regr/ ← 夜间回归脚本 + 失败自动归档目录
└── doc/ ← 验证计划表 / 签核报告 / 复盘文档
# VCS(商用,验证岗主流):UVM 库 + 全覆盖率开起来一行
vcs -sverilog -ntb_opts uvm-1.2 -full64 \
-cm line+cond+fsm+tgl+branch -cm_hier cover.cfg \
-f sim/filelist.f -o simv -debug_access+all -l comp.log
./simv +UVM_TESTNAME=smoke_conv3x3_test -cm_dir cov/smoke.vdb -l sim.log
# iverilog 一句话差异:开源 iverilog(-g2012)支持大部分 SV 语法,
# 但没有官方 UVM 类库,跑不了标准 UVM 环境 —— 学习期用 EDA Playground
# 云端仿真器(Questa/VCS 可选)或学校/公司 license,本项目默认 VCS 语法。
是什么:refmodel(golden model)是一个不关心时序、只关心数值的软件模型:它和 DUT 听同样的配置(镜像寄存器与 RAM),算出"理论上正确的输出",交给 scoreboard 当比对基准。本项目两条实现路线:路线 A 纯 SystemVerilog 行为级(队列 + 数组 + 三重循环),路线 B DPI-C 调 Python/NumPy(复用 08-08 的量化脚本)。
| 维度 | 路线 A:SV 行为级 | 路线 B:DPI-C 调 Python/NumPy |
|---|---|---|
| 开发效率 | 三重循环几十行,和 RTL 结构一一对应,好教好查 | np.convolve/matmul 数行搞定,算法人一读就懂 |
| 数值契约 | INT8 饱和 / requant 算术右移用 $signed/>>> 精确复现 | 要自己复刻 INT8 饱和与舍入规则,NumPy 默认浮点易"对不上" |
| 环境依赖 | 零依赖,仿真器直接编译 | 需编译 C 壳 + Python 运行时,跨机回归要装环境 |
| 调试体验 | 与 DUT 同一波形窗口、同一日志系统,逐层可打印 | 两边调试割裂,需打印中间张量人工对照 |
| 适用场景 | 学习项目 / 中小 IP(推荐) | 大网络、算法团队已有一致性脚本(08-08)时可复用 |
怎么做 —— 路线 A 骨架(镜像 + 计算两段):
class npu_refmodel extends uvm_component;
`uvm_component_utils(npu_refmodel)
// —— 镜像区:与 DUT "记同一本账" ——
bit [31:0] regs [bit [7:0]]; // 寄存器镜像:按 0x00~0x28 偏移
bit [7:0] wram [bit [15:0]]; // 权重 RAM 镜像(64K×8)
bit [7:0] fram [bit [17:0]]; // 特征图 RAM 镜像(A/B 乒乓统一编址)
uvm_analysis_port #(npu_exp_blk) exp_ap;
uvm_analysis_imp #(apb_txn, npu_refmodel) bus_imp; // 订阅 APB 写
function void write(apb_txn t); // monitor 广播进来
if (!t.pwrite || !t.pready) return; // 只关心有效写
regs[t.paddr] = t.pwdata; // 寄存器通路
// RAM 写通路(0x80 段起):同步写 wram/fram 镜像,含 pslverr 拒绝逻辑
endfunction
task run_layer(int layer_no); // 收到"完成事件"前的期望计算
npu_exp_blk blk = npu_exp_blk::type_id::create("blk");
int fm_in = (regs[8'h20] & 32'hFF) + 1; // ★ N−1 编码还原
int fm_out = (regs[8'h20] >> 8 & 32'hFF) + 1;
int ch_in = (regs[8'h1C] & 32'hFF) + 1;
int ch_out = (regs[8'h1C] >> 8 & 32'hFF) + 1;
int K = (regs[8'h18] & 32'h10) ? 5 : 3; // k5 位选核
bit relu = (regs[8'h18] & 32'h08) != 0;
int mult = regs[8'h24] & 32'hFFFF;
int shift = (regs[8'h24] >> 16) & 32'h1F;
blk.type_name = LAYER_NAME[regs[8'h18] & 7]; // conv/pool/fc
blk.layer_no = layer_no;
// —— 逐输出点累加(队列/数组充当行缓冲,行为级不追周期) ——
for (int oy = 0; oy < fm_out; oy++)
for (int ox = 0; ox < fm_out; ox++)
for (int oc = 0; oc < ch_out; oc++) begin
int acc = 0;
for (int ic = 0; ic < ch_in; ic++)
for (int ky = 0; ky < K; ky++)
for (int kx = 0; kx < K; kx++)
acc += $signed(fram[ifm_addr(ox+kx, oy+ky, ic)]) *
$signed(wram [w_addr (kx, ky, ic, oc)]);
acc += $signed(regs[8'h14] ? bias_ld(oc) : 32'sd0); // INT32 bias
acc = (acc * mult) >>> shift; // requant 算术右移
if (acc > 127) acc = 127; // INT8 双向饱和
else if (acc < -128) acc = -128;
if (relu && acc < 0) acc = 0; // ReLU 在饱和之后
blk.dat[ofm_idx(ox, oy, oc)] = acc[7:0];
end
exp_ap.write(blk); // 期望块 → scoreboard
endtask
endclass
(acc*M)>>shift 是算术右移、饱和在 ReLU 之前还是之后要翻回 08-12/08-08 的 Spec,顺序错一位,randomize 一跑满屏 mismatch。>>> 且左操作数是 int;②饱和与 ReLU 顺序颠倒,负数边界值比对全挂;③镜像忘了处理 pslverr 被拒的写(DUT 拒收,模型却记账);④乒乓块地址换算错,A/B 在模型与 RTL 里编址不一致。是什么:scoreboard 是终审裁判。比对粒度有两种:逐事务/逐块(一个高层事务完成后,整块数据一次比对)与逐 beat/逐字节(数据流每拍到达即比)。本项目数据流是"配置 → start → done → 整块输出",天然适合层完成事件触发的逐块比对:irq_monitor 广播完成事件 → scoreboard 取 refmodel 期望块 → 与特征图 RAM 镜像(DUT 实际写回)逐字节扫。
| 策略 | 触发时机 | 适用接口 | 本项目取舍 |
|---|---|---|---|
| 逐事务/逐块比对 | 完成事件/握手后整块比对 | 带"完成"语义的加速器、帧协议 | 采用:done 事件触发,出错一次看全块 |
| 逐 beat 比对 | 每个数据拍即到即比 | AXI-Stream / FIFO 等流式接口 | 不采用:本 DUT 输出经片内 RAM,总线上看不到逐拍数据 |
| 端到端分数检查 | 全网五层跑完比对最终 10 类输出 | 回归冒烟、模型一致性 | 补充:rand_all_test 附加终检,快但定位粗 |
失败打印要"一屏定位":报错必须自带 层号、层类型、出错坐标、期望值、实际值、差值,配合波形文件行号直接跳现场:
function void npu_scoreboard::compare_layer(npu_exp_blk exp);
int unsigned first_err = exp.dat.size();
int err_cnt = 0;
foreach (exp.dat[i]) begin
bit [7:0] act = rm.fram[exp.ofm_base + i]; // DUT 实际写回的特征图 RAM
if (act !== exp.dat[i]) begin
int ox, oy, oc;
exp.decode_idx(i, ox, oy, oc); // 线性地址反解回 (x,y,channel)
`uvm_error("SCB", $sformatf(
"[layer#%0d %s] idx=%0d (ox=%0d,oy=%0d,oc=%0d) exp=%02h act=%02h diff=%0d",
exp.layer_no, exp.type_name, i, ox, oy, oc, exp.dat[i], act,
$signed({1'b0, act}) - $signed({1'b0, exp.dat[i]}))))
if (first_err == exp.dat.size()) first_err = i;
if (++err_cnt == 8) begin
`uvm_warning("SCB", "同类错误满 8 个,截断打印(先修首错)")
break;
end
end
end
err_total += err_cnt;
`uvm_info("SCB", $sformatf("layer#%0d 比对完成: %0d/%0d 字节错, 首错 idx=%0d",
exp.layer_no, err_cnt, exp.dat.size(), first_err), UVM_LOW)
endfunction
容差立场 —— INT8 量化网络必须位精确(exact):同一份输入 + 同一运算顺序,量化行为是确定的,!== 一个 bit 都不许差;容差(±1、相对误差)只属于浮点/训练网络。工程红线:发现 mismatch 就给比对加"允许差 1"的容差,等于亲手把裁判变成摆设 —— 差 1 的背后可能是 requant 移位错一位或饱和边界错,全是真 bug。
== 比数据遇到 x 值漏判,4 态比对用 !==。怎么做:先写一个 npu_base_test 集中装配(env 创建、vif 注入、公共 sequence 库),各用例只覆写"配置 + 激励策略"。四类用例的分工:smoke 保环境通路、功能用例保 Spec 每条算子、随机用例扫参数空间、异常用例保 Spec 的"拒绝语义"。
// —— 基类:所有用例的公共底座 ——
class npu_base_test extends uvm_test;
`uvm_component_utils(npu_base_test)
npu_env env;
function new(string name, uvm_component parent); super.new(name, parent); endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
env = npu_env::type_id::create("env", this); // vif 由 tb_top config_db 注入
endfunction
endclass
// —— ① smoke:单层 3×3 卷积固定数据,验"环境通路 + 中断 + 比对闭环" ——
class smoke_conv3x3_test extends npu_base_test;
`uvm_component_utils(smoke_conv3x3_test)
task run_phase(uvm_phase phase);
npu_direct_seq seq;
phase.raise_objection(this);
seq = npu_direct_seq::type_id::create("seq");
seq.fixed_layer(.type(3'd0), .k5(0), .fm_in(8), .ch_in(1),
.ch_out(2), .relu(1), .seed_data(8'h5A)); // 固定伪随机填充
seq.start(env.agt.sqr); // 配寄存器 → start → 等完成事件
phase.drop_objection(this);
endtask
function void report_phase(uvm_phase phase);
if (env.scb.err_total == 0 && env.irq_mon.done_events == 1)
`uvm_info("PASS", "smoke 通路 OK: 1 层完成 + 0 mismatch", UVM_NONE)
else `uvm_error("FAIL", $sformatf("err=%0d done_evt=%0d",
env.scb.err_total, env.irq_mon.done_events))
endfunction
endclass
// —— ② 功能用例:每算子一个,与验证点表 V2~V7 一一对应(以池化为例) ——
class pool_max2x2_test extends npu_base_test;
`uvm_component_utils(pool_max2x2_test)
task run_phase(uvm_phase phase);
npu_direct_seq seq;
phase.raise_objection(this);
seq = npu_direct_seq::type_id::create("seq");
seq.fixed_layer(.type(3'd1), .fm_in(8), .ch_in(2), .ch_out(2)); // 8×8 → 4×4
seq.start(env.agt.sqr);
phase.drop_objection(this);
endtask
endclass
// —— ③ 随机用例:约束 = "Spec 允许的参数空间",边界值加权出现 ——
class npu_rand_layer_seq extends uvm_sequence #(apb_txn);
`uvm_object_utils(npu_rand_layer_seq)
rand bit [2:0] layer_type;
rand int fm_in, ch_in, ch_out;
rand bit relu_en, k5;
constraint c_type { layer_type inside {3'd0, 3'd1, 3'd2}; } // 合法三类
constraint c_fm { fm_in inside {[1:32]};
layer_type == 3'd1 -> fm_in[0] == 1'b0; } // pool 需偶边长
constraint c_hot { ch_in dist {1 := 3, [2:15] := 1, 16 := 2, 256 := 2}; // 边界加权
ch_out dist {1 := 3, 6 := 2, [2:15] := 1, 256 := 2}; }
constraint c_k5 { layer_type == 3'd0 -> k5 inside {0, 1}; // k5 仅 conv
layer_type != 3'd0 -> k5 == 0; }
// body():逐条写寄存器 → 载入随机数据到镜像 RAM → start → 等完成
endclass
// —— ④ 异常用例:验"拒绝语义",每一步都写明期待 ——
class illegal_cfg_test extends npu_base_test;
`uvm_component_utils(illegal_cfg_test)
task run_phase(uvm_phase phase);
npu_err_seq seq;
phase.raise_objection(this);
seq = npu_err_seq::type_id::create("seq");
seq.start(env.agt.sqr);
phase.drop_objection(this);
endtask
endclass
class npu_err_seq extends uvm_sequence #(apb_txn);
`uvm_object_utils(npu_err_seq)
task body();
// 场景 1:非法层类型 layer_type=3'b110(Spec 只定义 000/001/010)
wr_reg(8'h18, 32'h0000_0006);
wr_reg(8'h00, 32'h0000_0001); // start
// 期待:FSM 拒绝启动(STATUS.idle 保持 1)或按约定 pslverr;不挂死、无 irq
// 场景 2:计算中再次 start(idle=0 期间)
wr_reg(8'h18, 32'h0000_0000); // 合法 conv 配置
wr_reg(8'h00, 32'h0000_0001); // 第一次 start → FSM 离开 IDLE
wr_reg(8'h00, 32'h0000_0001); // 第二次 start:期待被忽略
// 期待:只完成 1 层,irq_mon.done_events == 1,start 自动清零
// 场景 3:done 读清除契约(1.2 节电平中断)
// 读 STATUS 两次:第一次 done=1(同时清),第二次 done=0 且 irq 撤销
endtask
endclass
是什么:UVM 调试的三大仪器:report 日志(分级过滤,代替满屏 $display)、波形(RTL 信号级真相)、失败定位流程(按现象走固定路线,不靠玄学)。NPU 验证的失败现象 90% 归入三类:挂死、数据错、中断不来。
+UVM_VERBOSITY=UVM_MEDIUM;复跑失败用例提到 UVM_HIGH 看 driver/monitor 握手细节,不用改一行代码。控制总量:+UVM_MAX_QUIT_COUNT=10 让首个失败后尽早停止(满屏重复错只会淹没首错现场);按组件静音:env.scb.set_report_severity_id_override(...) 或命令行 +uvm_set_verbosity=<comp>,<id>,<verb>,<phase>。-debug_access+all 编译,仿真中经 ucli/verdi 控制 dump 范围(FSDB 格式 + Verdi 看);开源流程在 tb_top 里 $dumpfile("wave.vcd"); $dumpvars(0, tb_top);(VCD 全量,大设计慎用)。经验:失败用例只 dump 失败时段(时间窗 + 层次范围),波形文件可缩小一个量级。sim.log 首个 UVM_ERROR(scoreboard 的结构化打印给出层号/坐标/期望/实际)→ 带着坐标开波形,直接跳到该层完成时刻。where/Verdi 的 hang 分析看线程停在哪一行 —— 90% 停在 get_next_item(没人发激励)或 do @(vif.drv_cb) while(!pready)(DUT 没握手)。「数据错先查谁?」—— 先查 refmodel:尺子错了量什么都是错的;模型单测通过后,再用"六级对照法"在波形里找第一处与模型分叉的信号。uvm_fatal 用在可恢复错误上,现场没收集完仿真就退了;③VCD 忘了关,夜间回归磁盘被写爆;④修 bug 只验证了失败用例,没挂回全回归 —— 修好 A 环节坏了 B,是回归存在的全部意义。覆盖率回答一个问题:"验证做完了吗?"它把"我感觉差不多了"变成"两张报告 + 一张排除表"。本节依次解决:功能覆盖率怎么建模(covergroup)、代码覆盖率怎么开、两张报告怎么分析合并、以及什么条件才配说"签核"。
是什么:功能覆盖率是人写的度量:把 1.1 节验证点表里"什么算测过了"翻译成 covergroup/coverpoint/cross。它度量的是意图(Spec 的功能有没有被激励到),与 4.2 节度量实现(RTL 代码有没有被执行)的代码覆盖率互补。
为什么这样设计 bins:每个 coverpoint 对应寄存器/参数的一个维度,bins 划分遵循"边界值单独成 bin + 中间区间收敛 + 非法值进 illegal_bins" —— N−1 编码的 0(1 通道)、满量程 255(256 通道)是必中目标,中间值测一个和测一百个意义相同。
class npu_coverage extends uvm_subscriber #(irq_evt);
`uvm_component_utils(npu_coverage)
npu_refmodel rm; // env connect_phase 注入,采样时读镜像
bit [2:0] layer_type; bit relu_en, k5;
int fm_in, ch_in, ch_out; int addr_in, addr_out;
covergroup cg_layer @(posedge done_event); // 事件触发采样(见下)
option.per_instance = 1; option.at_least = 1;
cp_type: coverpoint layer_type {
bins conv = {3'd0}; bins pool = {3'd1}; bins fc = {3'd2};
illegal_bins bad = default; // 非法层类型出现即报警
}
cp_relu: coverpoint relu_en { bins off = {0}; bins on = {1}; }
cp_k5: coverpoint k5 { bins k3 = {0}; bins k5 = {1}; }
cp_fmin: coverpoint fm_in {
bins min1 = {1}; // 最小边界
bins small = {[2:8]};
bins mid = {[9:24]};
bins large = {[25:31]}; bins max32 = {32};
}
cp_chin: coverpoint ch_in {
bins one = {1}; // N−1:0 → 1 通道
bins mid = {[2:127]};
bins full = {256}; // 满量程(N−1 编码 255)
ignore_bins rsvd = {[257:65535]}; // Spec 不支持的值
}
cp_addr: coverpoint (addr_in[13:0] + blk_words(addr_in)) {
bins in_a = {0};
bins cross = {[1:16'h3FFF]};
bins hit_bd = {16'h4000}; // 触到 16K 边界
}
// 组合空间:层类型 × ReLU × 核尺寸 —— 乒乓接力要求的"模式全覆盖"
cx_mode: cross cp_type, cp_relu, cp_k5 {
ignore_bins pool_no_k5 = binsof(cp_k5.k5) && binsof(cp_type.pool);
ignore_bins fc_no_k5 = binsof(cp_k5.k5) && binsof(cp_type.fc);
}
cx_io: cross cp_fmin, cp_chin; // 尺寸 × 通道热区
endgroup
function void write(irq_evt e); // 订阅完成事件 → 读 rm 镜像 → sample
layer_type = rm.regs[8'h18] & 7; relu_en = rm.regs[8'h18][3];
k5 = rm.regs[8'h18][4]; fm_in = (rm.regs[8'h20] & 'hFF) + 1;
ch_in = (rm.regs[8'h1C] & 'hFF) + 1; ch_out = (rm.regs[8'h1C] >> 8) + 1;
cg_layer.sample();
endfunction
endclass
采样时机 —— 三选一,别混用:①covergroup ... @(event) 事件触发(本项目:irq_monitor 的层完成事件,最推荐 —— 一层一采,天然对齐"配置生效"时刻);②显式 cg.sample()(write() 里备好采样快照后调用,可控性最强);③coverpoint ... iff(cond) 条件内联采样。反面教材:在 driver 里采激励侧数据 —— 覆盖率必须采"观察到的 DUT 行为",不是"发出过的激励"。
| 覆盖点 | bins 划分 | 保护的验证点 |
|---|---|---|
| cp_type | conv/pool/fc + illegal_bins | V2~V5、V9(非法层类型有人盯) |
| cp_relu / cp_k5 | 各 0/1 两 bin | V3、V6(开关两态都试过) |
| cp_fmin | 1 / 2~8 / 9~24 / 25~31 / 32 | V10(边界尺寸单独成 bin) |
| cp_chin | 1 / 2~127 / 256(ignore 保留域) | V10(N−1 编码 0 与满量程) |
| cx_mode | type × relu × k5(剔除非法组合) | V11(模式组合全覆盖) |
| cx_io | fm_in × ch_in | V11(尺寸与通道的热区交叉) |
option.per_instance=1,多实例共享计数,报告看不出哪个 collector 采的;②在 build_phase 里 new covergroup 之外还在 class 里重复声明成员变量却没在 write() 里赋值 —— 采出来全是 0;③把 ignore_bins 写成 illegal_bins(或反之),前者是"不算",后者是"出现即错";④采样太早(配置还没写完就采),镜像值是旧配置。是什么:代码覆盖率是仿真器自动统计的"RTL 执行痕迹",五类指标各有打击面:
| 指标 | 统计什么 | 能抓什么 bug | 本项目例子 |
|---|---|---|---|
| 行 / 语句(line) | 每行 RTL 是否执行过 | 死代码 / 没跑到的分支体 | pslverr 拒写逻辑一行没执行 → 没测过"计算中写 RAM" |
| 分支(branch) | if/case 的每个走向 | 只走了 true 没走 false | relu_en 只测过 1,else 分支空白 |
| 条件(condition/expression) | 布尔子表达式的真假组合 | 短路组合隐藏的半个条件 | start && idle 只遇过 idle=1 的组合 |
| 翻转(toggle) | 每个 bit 0→1 与 1→0 | 常 0/常 1 的悬空位、位宽浪费 | STATUS[7:5] 从未翻转 → 保留域该确认 |
| FSM(state + transition) | 状态到达 + 状态跳转对 | 从未进入的状态、缺失的跳转 | 五态主 FSM 的 ERROR 态没到过 → V9 没生效 |
工具开关一句话:VCS 编译 -cm line+cond+fsm+tgl+branch(配 -cm_hier 排除 TB 层次),仿真 -cm_dir cov/<test>.vdb,出报告 urg -dir cov/*.vdb -report urgReport;Questa 对应 vsim -coverage + vcover merge/report。报告里真正要看的不是总百分比,而是 hits=0 的明细列表 —— 那是 4.3 节分析的输入。
| 维度 | 功能覆盖率 | 代码覆盖率 |
|---|---|---|
| 谁写的 | 工程师手写 covergroup(意图) | 工具自动插桩(实现) |
| 度量对象 | Spec 功能点有没有被激励到 | RTL 代码有没有被执行 |
| 100% 意味着 | 计划内功能场景全测过 | 所有代码/分支跑过(≠功能对!) |
| 盲区 | 忘了建模的功能点测一万遍也记 0 分 | 每行都跑过但结果错 —— 它不管对错 |
| 关系 | 双 100% 才敢谈签核;功能覆盖补"对不对、全不全",代码覆盖补"有没有漏执行的角落" | |
-cm_hier/exclude 文件剔除 TB 与 wrapper;②把代码覆盖率当签核唯一标准 —— 它不检查数据正确性;③toggle 覆盖把伪路径/保留位也要求翻转,永远缺一口(排除并注释)。怎么做 —— 收集与合并:每个用例跑完产出独立数据库(VCS 是 .vdb,Questa 是 .ucdb),回归脚本最后一步合并:urg -dir cov/(VCS)或 vcover merge total.ucdb run1.ucdb run2.ucdb ...(Questa)。合并后才谈覆盖率 —— 单用例覆盖 80% 没有意义,50 个 seed 合并后通常能到 90%+,剩下的是"难啃的骨头"。
未覆盖原因分析 —— 每行 hits=0 只有两种归宿:
是什么:签核(sign-off)是"验证完成"的正式宣告,靠清单不靠感觉;复盘是把项目经验变成个人资产的最后一环 —— 也是把这个项目写进简历、扛住面试追问的最后一公里。
| 检查项 | 签核标准(本项目) | 不达标怎么办 |
|---|---|---|
| 功能覆盖率 | 100%(所有 illegal_bins 之外全 hit) | 回到 4.3 闭环;exclusion 逐条 review |
| 代码覆盖率 | line/branch/cond/FSM > 95%,toggle > 90%,剩余逐条论证或排除 | 可达缺口补用例;不可达写注释排除 |
| 回归结果 | 全部用例 × N 个 seed,0 UVM_ERROR,0 UVM_FATAL | 失败归档 → 定位 → 修复 → 全回归 |
| 断言 | 1.2 节 SVA 全部通过,无未兑现的 assert | 断言失败即 bug,不许 "关断言跑绿" |
| 追踪完整性 | vPlan 每行状态 = 收敛;需求 → bin 无断链 | 补 bin 或补用例,禁止"先签核后补表" |
| 遗留项 | 无悬空 TODO / FIXME / 临时 workaround;已知问题(issues)全部登记并评估影响 | 能修则修,不能修转 issue 并写入签核报告"风险栏" |
复盘文档怎么写(一页,四个小节):①结果:验证点表完成率、双覆盖率最终数字、回归规模(X 用例 × Y seed)、抓到的 bug 清单(按引入阶段分类);②根因:每个 bug 复盘"哪条验证点漏了/约束缺了/断言没建" —— bug 不是白抓的,每一条都应反哺覆盖率模型;③工时与偏差:环境搭建/用例/收敛各花多久,哪段估计错了(常见:覆盖率最后 5% 的时间被低估 3 倍);④可复用资产:apb_agent、RAL、比对框架打包成小 IP,下个项目(比如给这个 NPU 加 AXI-Lite 口)直接搬。
以下资源均经核实可访问(2026-09);按"官方 → 教程 → 中文社区 → 开源代码 → 视频"排序,配合本页第 1~4 节对照食用:coverage 系列对应 4.1~4.3,APB/UVM 工程对应第 2 节,NPU 开源仓库对应贯穿项目本身。