covergroup coverpoint bins:功能覆盖率三要素的语义与写法
更新时间:2026-09-25。本文是「集成电路大类 · 芯片设计 · 数字验证」实操序列的入门专题。验证计划 testplan 怎么写:从特性条目到功能覆盖率的可追溯映射 解决了「要覆盖什么」的问题,本篇回答下游一步:feature point 清单落到验证环境里,具体靠什么语法去度量。SystemVerilog 给出的答案就是三个层层嵌套的构件——covergroup、coverpoint、bins。覆盖率概念本身(行/翻转/状态机/功能覆盖的分工)在 代码功能覆盖率 里已有铺垫,本篇只聚焦功能覆盖率模型怎么写、每个 bin 的语义是什么。
本文要回答的问题
- covergroup 到底是什么,它在什么时刻采样?为什么说它是「时钟驱动的采样容器」?
- 一个 coverpoint 不写任何 bins 时会发生什么?自动 bins 为什么常常不够用?
- 显式 bins 怎么表达单值、范围、一组值,以及「先经过 A 再经过 B」这类转换序列?
- illegal_bins 和 ignore_bins 只差一个单词,它们在命中时的行为和对覆盖率分母的影响有什么本质区别?
- cross 交叉覆盖自动生成笛卡尔积,组合爆炸时怎么用 binsof/intersect 裁剪?多个实例、多个 seed 的覆盖率又怎么合并进同一份报表?
一、covergroup:时钟事件触发的采样容器
covergroup 是一种用户自定义类型,语义上接近「一个带采样时刻表的覆盖率容器」。它内部装若干 coverpoint 和 cross,本身不产生任何激励、不改变信号,唯一的职责是在规定时刻把信号值抄下来,分给内部的 coverpoint 判断命中了哪个 bin。
触发方式有两种。最常见的是把采样事件直接绑在时钟沿上:covergroup cg @(posedge clk); 之后每个时钟上升沿,容器内所有 coverpoint 在同一时刻采样一次,彼此天然对齐——这对后面做 cross 至关重要,交叉的两个维度必须来自同一次采样。第二种是调用内置的 sample() 方法,可以显式带参数,适合采样点不规则、或只想在 monitor 识别出某个事务(transaction)时才记一笔的场景:在总线事务级覆盖率里,通常一个事务只 sample 一次,而不是每周期记一笔。
图里最需要记住的是「一次事件、全组采样」:cross 的笛卡尔积之所以成立,是因为 cp_level 和 cp_rw 的值抄自同一个时钟沿。如果两个 coverpoint 分别在不同时刻采样,交叉结果没有物理意义。covergroup 可以定义在 module、interface 里,也可以定义在 class 中并像普通对象一样 new 出多份——比如 monitor 里每路接口例化一个 cg,这就引出后面 per_instance 与 merge_instances 的区分。
一个完整但最小的骨架长这样:
covergroup cg_rw @(posedge clk);
cp_level: coverpoint mon_level;
cp_rw: coverpoint ({wr_en, rd_en});
endgroup
cg_rw cov = new(); // 例化后自动开始在每个时钟沿采样事务级采样的写法值得给个具体样子:covergroup 声明时不带时钟事件,而是声明带参数的 sample,monitor 在识别出一个完整总线事务后手工调用,把事务字段作为参数传进去。这样覆盖率模型与协议时序解耦——无论一个事务跨越 2 拍还是 20 拍,都只记一次,报表里的 bin 数直接对应「发生过多少类事务」,不会被周期数稀释:
covergroup cg_txn with function sample(bit [3:0] op, bit err);
cp_op: coverpoint op;
cp_err: coverpoint err;
endgroup
// monitor 中:cg_txn.sample(txn.opcode, txn.slv_err);上面的 covergroup 没有写任何 bins,它仍然会工作——只是分箱方式由语言规则自动决定。下一节就来看自动行为是什么。
二、coverpoint 与自动 bins:默认分箱为什么常常不够用
coverpoint 是对「一个表达式」的覆盖度量。表达式可以是变量、位拼接、条件运算甚至函数调用:coverpoint ({wr_en, rd_en}) 度量的是两比特拼接值,coverpoint (addr inside {[...]}) 度量的是布尔结果。coverpoint 的取值空间被切成若干 bin,每来一个采样值,落入对应 bin 就给它计一次数;覆盖率就是「至少命中过一次的 bin 数 ÷ 总 bin 数」。
不写 bins 时,仿真器自动生成隐式 bins,规则分两种情况:
| 被采样表达式类型 | 自动 bins 行为 |
|---|---|
| enum 枚举类型 | 每个枚举命名常量各一个 bin,语义刚好 |
| 整型(logic/int 等) | 按 option.auto_bin_max(默认 64)把取值范围均分成至多 64 个等宽区间 |
枚举类型的自动行为正是想要的,所以状态机状态的 coverpoint 常常一行 bins 都不写。但整型自动分箱对实际项目几乎不可用:一个 16 位地址空间被均分成 64 段,边界值(0、最大值)和关键寄存器地址被淹没在大区间里,命中了区间内任意一个值就算整个 bin 命中,你无法回答「0 地址到底访问过没有」。更麻烦的是,综合外外设里大量关心的是非均匀的取值——边界几个值要单独成箱、中间一大片可以合并一箱,自动均分给不出这种结构。
图中两条路线对比的就是工程上的取舍:自动 bins 零成本但语义粗糙,显式 bins 要多写几行,却能把 testplan 里的 feature point 精确对应到具名 bin——覆盖率报表上看到的是 cp_level.full 而不是「第 63 号自动区间」,holes 分析才有意义。此外还有两个常用调节量:option.at_least = N 规定一个 bin 至少命中 N 次才算覆盖(默认只要求 1 次),coverpoint 的 weight 则控制它在整组覆盖率里的权重。
三、显式 bins:单值、范围与转换序列
显式 bins 是功能覆盖率模型里信息量最大的地方。它能表达三类东西:取值、取值集合的组织方式、以及取值之间的时序转换。
最基础的是单值与范围:
cp_level: coverpoint mon_level {
bins empty = {0}; // 单值成箱
bins one = {1};
bins mid = {[2:14]}; // 范围合并成一箱
bins full = {15, 16}; // 离散多值合并一箱
}{15, 16} 这种离散列举和 [2:14] 范围可以混用,落在箱内任意一个值都算命中。如果反过来希望集合里每个值各自成箱、又不想逐个手写,用数组形式的 bins:bins vals[] = {[0:3], 7}; 会生成 5 个独立 bin。这在度量操作码、中断号这类「每个编码都要单独见到」的维度上很常用。
第二类是转换 bins(transition bins),语法用 => 表示相邻两次采样之间的跳转:
cp_state: coverpoint fsm_state {
bins idle2act = (IDLE => ACT);
bins any2err = (IDLE, ACT => ERR); // 两个起点都算
bins long = (IDLE => ACT => DONE); // 三拍序列
bins act_hold = (ACT [*3]); // ACT 连续保持 3 次采样
bins loops = (ACT [*2:5]); // 连续 2 到 5 次
}这正是普通取值 bins 无法替代的能力:连续两拍采到同一个 ACT 值,只说明状态出现过;(IDLE => ACT) 才能证明「跳转」这件事发生过。, 在转换两侧表示「多个候选起点/终点」,链式 => 表达多拍序列,[*N] 与 [*N:M] 表达重复次数区间。FSM 的每条弧、握手协议的 valid 拉起时序,都应该用转换 bins 度量,写法可参照 FSM 状态跳转验证 的完整例子。
这张时序图提醒一个常见误解:转换 bins 的判定窗口随采样逐拍滑动,(IDLE => ACT => DONE) 要求连续三拍严格匹配,上图 t0 到 t3 因为中间多了一拍 ACT 而不命中——想表达「最终到 DONE,中间不限」要用通配写法。另外两个语法点一并记住:wildcard bins 允许值里写 x/z/? 做位掩码匹配;default 箱承接所有没有被其他 bins 覆盖到的取值,用来度量「意外值是否出现」,但它不参与正常覆盖率的分母计算。
四、illegal_bins 与 ignore_bins:一个报错,一个剔除
coverpoint 里除了普通 bins,还有两类特殊箱,它们字面相似、语义相反,是面试和实战中都高频踩坑的点:
| 构件 | 语义 | 命中时 | 对覆盖率分母 | 典型用途 |
|---|---|---|---|---|
| bins | 期望发生的合法取值/转换 | 计数,推动覆盖率 | 计入 | 正常场景 |
| illegal_bins | 协议禁止、出现即 DUT 出错 | 立刻报错(等同断言失败) | 计入 | 保留编码、非法状态跳转 |
| ignore_bins | 合法但本配置下不可达/不关心 | 静默丢弃该采样值 | 从分母剔除 | 配置深度之外的值、不支持的模式 |
差别可以用一句话概括:illegal_bins 表达「永远不许来」,ignore_bins 表达「来了也不算数」。illegal_bins 命中会直接污染回归结果,所以它和断言是近亲——一个非法操作码被采到,功能覆盖模型当场报错,不必等 scoreboard 兜。ignore_bins 则纯粹是数学口径:把不可能出现的值移出分母,否则覆盖率永远差一截且无法收敛;但每一条 ignore_bins 都必须在 验证计划 testplan 怎么写 里有书面理由(什么配置导致不可达),无理由的 ignore_bins 等于藏洞。
cp_op: coverpoint opcode {
bins legal[] = {[0:8]};
ignore_bins unsupported = {9, 10}; // 本裁剪版本不支持,分母中剔除
illegal_bins reserved = {[11:15]}; // 编码表保留区,出现即 RTL 违例
}两者也支持 wildcard 与转换形式:illegal_bins bad_jump = (ERR => IDLE); 禁止错误状态直接跳回空闲。语义表同样适用于 cross 内部——交叉覆盖里一样可以写 illegal_bins 和 ignore_bins,这正是下一节裁剪笛卡尔积的工具。
五、cross 交叉覆盖:笛卡尔积与 bins 裁剪
cross 把两个或更多 coverpoint 的 bins 做笛卡尔积,自动为每个组合生成一个交叉 bin。若 cp_level 有 4 个 bin、cp_rw 有 4 个 bin,cross cp_level, cp_rw 就产生 16 个交叉 bin,记录诸如「水位为 full 且当拍只写不读」这类多维度场景——这正是单维度 coverpoint 给不出的信息,feature point 里的组合类条目都靠它承接。
问题随维度数指数放大:三个各 16 箱的 coverpoint 交叉就是 4096 个 bin,而 testplan 真正关心的组合可能只有十几个。报表被大量永不出现的组合占满,覆盖率数字失真,holes 也无从分析。裁剪工具是 binsof() 配合 intersect:
cx_full_rw: cross cp_level, cp_rw {
ignore_bins not_full = binsof(cp_level) intersect {[0:14]};
// 只保留 full(15,16) × 四种使能 = 4 个交叉 bin
ignore_bins full_idle = binsof(cp_level) intersect {15,16}
&& binsof(cp_rw) intersect {2'b00};
}binsof(cp_level) 选中该 coverpoint 的 bin 集合,intersect {[0:14]} 再按底层值过滤,多个条件可用 &&/|| 组合。被 ignore_bins 裁掉的组合移出分母;把 ignore 换成 illegal,则表示「这个组合协议上禁止,一旦出现立即报错」——比写一堆等价断言更集中、报表也更直观。
图中漏斗形状就是 cross 设计的正确姿势:先小粒度定义单维 coverpoint,再在 cross 里用 ignore_bins 收敛到需求相关的组合,而不是反过来让成千上万个自动交叉箱长期挂 0%。需要直接点名保留某些组合时,还可以用显式 bins sel = binsof(...) && binsof(...) 只生成选中的交叉箱;option.cross_num_print_missing 控制报表里列出多少个未命中箱,避免报告本身被组合爆炸撑爆。
六、覆盖率合并选项与覆盖率驱动验证闭环
最后一组问题是工程口径:covergroup 被例化多份、回归在几十台机器上跑几百个 seed,分散的命中数怎么变成一份可签核的报表。
先区分实例级与类型级选项。option.per_instance = 1 让每个 covergroup 实例单独统计与显示——4 路端口各自的覆盖率一目了然;type_option.merge_instances = 1 则在保持实例化的同时,把同类型所有实例的命中向上合并到类型覆盖率里:只要 4 路端口中任意一路命中过某 bin,类型层面该 bin 就算覆盖。两者的取舍取决于验证问题:想要求「每一路端口都各自覆盖全」,用 per_instance 并逐实例看;只要求「系统整体见过所有场景」,用 merge_instances。相关的常用选项还有 type_option.weight(类型在总覆盖率中的权重)与 option.goal(该构件的收敛目标,默认 100)。另外,仿真过程中可以用内建方法实时读数:cg.get_coverage() 返回类型级覆盖率、cg.get_inst_coverage() 返回当前实例的覆盖率,scoreboard 或测试用例据此可以做条件收尾——比如某场景覆盖率已达标就提前结束,把仿真时长让给尚未命中的场景。选项设置的位置也要留意:type_option 写在 covergroup 定义体中、对整个类型生效;option 既可写在定义体中作默认值,也可在例化后对单个实例单独覆盖,两者的作用域关系和 class 的静态变量与成员变量完全同构。
图右半段的回路就是覆盖率驱动验证(CDV)的日常运转。两个层面的合并缺一不可:语言层面的 merge_instances 解决「多实例」,工具层面(仿真器附带的 urg/vcover 等命令)解决「多 seed、多机器」的数据库合并。纪律是先全量合并,再下钻 holes——只看单个 seed 会把大量「合计已命中、单看都没命中」的 bin 误判成空洞。对剩余的 0 命中 bin,按根因分流:合法但难出现的,回到 受约束随机验证 CRV 放松约束或加定向序列;物理不可达的,转 ignore_bins 并登记理由;协议禁止却出现了的,illegal_bins 已经替你把回归标红。收敛口径、阈值和签核流程的完整做法见 覆盖率收敛与签核。
覆盖率数字本身不证明 DUT 正确,它只证明「场景发生过」。所以每个关键 bin 仍然要和检查配对:场景由 bins 度量,行为正确性由 scoreboard 与 SVA 断言实战 中的断言保证,两者合起来才是一条完整证据。
这一篇在知识体系里的位置
本文处在「功能覆盖率怎么写」这一层:上游决定度量目标的是 验证计划 testplan 怎么写:从特性条目到功能覆盖率的可追溯映射,覆盖率概念与代码覆盖率分工见 代码功能覆盖率,激励侧如何配合填 bins 见 受约束随机验证 CRV,转换 bins 的 FSM 实例见 FSM 状态跳转验证,行为检查见 SVA 断言实战,下游把这些数字收敛成签核结论的流程见 覆盖率收敛与签核。
总结
covergroup coverpoint bins 是功能覆盖率的三层骨架:covergroup 靠时钟事件或 sample() 让全组在同一时刻采样,coverpoint 对一个表达式分箱,枚举可依赖自动 bins、整型则应写显式 bins 以保住边界与语义;显式 bins 用单值、范围、数组分箱表达取值,用 =>、链式序列和 [*N] 表达转换;illegal_bins 命中即报错、ignore_bins 静默剔除出分母,两者不可混用;cross 自动生成笛卡尔积并靠 binsof/intersect 裁剪掉需求之外的组合;多实例用 per_instance 与 type_option.merge_instances 区分口径,多 seed 先工具合并再做 holes 分析,按放松约束、定向序列、登记 waiver 分流——最终让每个 bin 都有来源、每个 holes 都有结论,这就是覆盖率驱动验证的闭环。