仲裁器验证:round-robin 与优先级仲裁器的公平性、无饥饿端到端验法
更新时间:2026-09-25。仲裁器(arbiter)是芯片里出现频率最高的小电路之一:多个 master 抢一个出口、DMA 抢总线、中断控制器抢上报口,背后都是同一个模型——N 个
request、N 个grant,每拍选一个。电路小,但它是典型的「仿真几十笔全对、上线才发现某个端口永远抢不到」的模块。这一篇按标准篇的深度,把仲裁器验证从 testplan、SVA(SystemVerilog Assertion,断言)、功能覆盖到 scoreboard 公平性统计完整走一遍,相关的断言与覆盖基础可回看 SVA 断言实践 与 代码功能覆盖。
本文要回答的问题
- round-robin(轮询)仲裁器和固定优先级仲裁器,行为差异到底落在哪几个场景上?
- 「公平」怎么变成可检查的数字,而不是波形上看着差不多?
- 「不会被饿死」这种 liveness(活性)性质,仿真和形式验证分别怎么验?
- atomic 操作和 burst 传输期间 grant 不能切换,断言应该怎么写?
- 多个请求同一拍一起拉高、轮转一圈、加权比例这些场景,怎么保证都被激励并覆盖到?
一、先把仲裁器模型钉死:接口与仲裁策略
验证前先和设计统一接口语义,否则后面每一条检查都可能是错的。最常见的组合逻辑仲裁器接口如下:req[N-1:0] 输入、grant[N-1:0] 输出,均为高有效;每拍 grant 至多有一 bit 为 1;被 grant 的 requester 通过自身的 valid/ready 或单独的 frame 信号占用资源,释放后仲裁器才重新仲裁。是否允许「没有 request 时 grant 保持上一个值」也要明确,多数规范要求无请求时 grant 全零。
仲裁策略决定参考模型,常见三种:
- 固定优先级(fixed priority):编号越低(或越高)优先级越固定。优点是延迟可预测,缺点是低优先级口在高优先级持续请求时永远拿不到 grant——这是设计本身允许的饥饿,验证要确认它「恰好如此」而不是「意外如此」。
- round-robin(轮询):维护一个指针(pointer),刚被服务过的 requester 优先级降到最低,指针向下推进。所有人持续请求时,grant 严格按 0→1→2→3→0 轮转。
- 加权轮询(WRR, Weighted Round Robin):每个 requester 带权重,一轮内按权重比例获得 grant,适合带宽分配场景。
| 维度 | 固定优先级 | round-robin | 加权轮询 |
|---|---|---|---|
| 持续满载时 grant 分布 | 全归最高优先级 | 各 1/N | 按权重比例 |
| 低优先级口 | 可能永久等待 | 至多等 N-1 次 grant | 上界与权重相关 |
| 关键参数 | 优先级编码 | 指针初值与更新点 | 权重寄存器 |
| 典型用途 | 低延迟控制口 | 对等 DMA、网卡队列 | QoS 带宽分配 |

这张轮转状态图是全篇的锚点:四请求仲裁器有四个指针状态,出边分两类——当前指针指向的 requester 被服务并释放,指针推进一格;它没发请求,则在同拍内跳过。注意「跳过」在实现里通常是组合逻辑一次性滑到下一个有效请求,而不是花多拍空转,这个细节直接决定波形预期:四个口全请求时一拍一换,只有 R0 和 R3 请求时序列是 R0、R3、R0、R3,中间看不到 R1、R2 的空 grant。
二、testplan:把「公平」拆成可验的功能点
仲裁器验证最容易写的 testplan 是「grant 能出来」,最容易漏的是统计性质。建议按下表组织功能点,并与覆盖点一一映射,方法见 testplan 与覆盖映射。
| 编号 | 功能点 | 验法 |
|---|---|---|
| F1 | grant 每拍 one-hot 或全零 | SVA 逐拍检查 |
| F2 | grant 必对应当拍有效 request | SVA 蕴含检查 |
| F3 | 四 requester 持续请求,grant 严格轮转 | 定向序列 + 顺序检查 |
| F4 | 任意 requester 持续请求,有限拍内必被 grant | liveness:cover 窗口 + 形式证明 |
| F5 | 无请求时 grant 全零,请求消失后 grant 当拍或次拍撤销 | SVA + 定向 |
| F6 | 多请求同拍有效时按指针裁决 | 请求向量全覆盖 |
| F7 | atomic/burst 持有期间 grant 不切换、指针冻结 | SVA 持锁检查 |
| F8 | 加权模式长期 grant 比例收敛于权重比 | scoreboard 窗口统计 |
| F9 | 复位与 FIFO 排空后指针回到初值 | 复位定向 |
激励侧建议用 N 个独立 requester sequence,各自可配三件事:请求发起概率、每次持有拍数(burst 长度)、是否发起 atomic 锁。随机时把「持有拍数」也随机化,因为公平性统计的陷阱大多出在长短 burst 混排上——比如 grant 计数公平但带宽(grant 拍数)严重倾斜,这是两个不同的指标,testplan 要明确验的是「服务次数」还是「占用拍数」,生产环境一般两个都收。

环境结构上没有特别之处,关键是 monitor 必须同时采三类信号:req、grant、占用/锁信号。只看 req 和 grant 写不出 F7 的检查,也无法区分「grant 了一拍」和「grant 后持续占用了八拍」。
三、逐拍正确性:SVA 三件套
第一组是安全性质(safety),任何仿真里都应常驻:
// F1: grant 合法编码——one-hot 或全零
a_grant_legal: assert property (@(posedge clk) disable iff (!rst_n)
$onehot0(grant));
// F2: grant 不能凭空发给没请求的口
a_grant_implies_req: assert property (@(posedge clk) disable iff (!rst_n)
|grant |-> (grant & req) == grant);
// F5: 无请求必无 grant
a_no_req_no_grant: assert property (@(posedge clk) disable iff (!rst_n)
req == '0 |-> grant == '0);
// F7: atomic/burst 持有期间不切换,frame 拉低前 grant 保持
a_grant_stable_during_frame: assert property (@(posedge clk) disable iff (!rst_n)
frame && |grant |=> $stable(grant));第二组是轮转顺序性质,利用局部变量记住上一个被 grant 的口:
// F3: 满载时,下一个 grant 必须是严格下一号(N=4)
property p_round_robin_full;
integer prev;
@(posedge clk) disable iff (!rst_n)
($onehot(grant), prev=$clog2(grant)) ##1 ($onehot(grant))
|-> $clog2(grant) == (prev+1) % 4;
endproperty
a_rr_order_full: assert property(p_round_robin_full);这条断言只在满载下成立;部分口空闲时允许跳跃,跳跃合法性交给 scoreboard 的参考模型判,不要硬写进断言,否则大量合法行为被误报。第三组是活性性质(liveness)。直接写「请求最终必被 grant」在仿真里无法判定——仿真可以一直跑下去。实用做法是加一个有界窗口:某 requester 连续请求,在至多 K 拍(RR 下 K 取 N × 最大burst 再加裕量)内必须出现它的 grant。
// F4 的仿真版:R0 持续请求,K 拍内必被服务
a_no_starve_r0: assert property (@(posedge clk) disable iff (!rst_n)
req[0] |-> ##[1:K] grant[0]);更彻底的无饥饿证明交给形式工具:把 burst 长度约束为有界,证明「每个 req 最终 grant」,这正是 形式化验证应用 文档里仲裁器验证一节的标准用法。仿真验统计、形式证明无饥饿,两者在仲裁器验证里是互补而非二选一。
四、关键场景波形:同拍并发与持锁
下面这张多请求并发的时序图覆盖 F3、F6 的核心拍序:四个口在第 1 拍同时拉高请求,grant 按指针逐拍推进;第 4 拍 R1 提前撤销请求,指针在第 5 拍跳过 R1 直接给到 R2。

这张图值得在 testplan 评审时对着设计逐拍确认,因为它同时暴露三个易错点:grant 与 req 是否同拍组合输出(还是延后一拍,时序断言的拍点要跟着改);撤销请求的当拍是否还能被 grant;跳过时指针的落点。
这里单独说 grant 的时序模式,因为它是最常见的接口误解。组合输出型(combinational grant)当拍看到 req、当拍给出 grant,requester 在同一个上升沿后就能动作,延迟低但容易在 req 来自寄存器输出时形成长组合路径;寄存输出型(registered grant)在拍 N 采样 req、拍 N+1 才给出 grant,握手天然错开一拍。两种模式的断言不能共用:寄存型要把 a_grant_implies_req 写成 grant |-> $past(req),无请求撤销也整体延后一拍。更麻烦的是混用模式——grant 寄存、但持锁期间的保持是组合的,这种接口必须让设计给出逐拍的时序表,scoreboard 按表配置延迟参数,切忌在参考模型里写死某一种。评审时直接问一句「grant 相对 req 延迟几拍、撤销延迟几拍」,答不上来就说明接口规范本身还没收敛。另一个必须定向构造的是 atomic 持锁波形:R2 获得 grant 后 frame/lock 持续 8 拍,期间 R0、R1 即便请求,grant 必须保持 0100,且 8 拍结束后指针从 R2 继续向后推进,而不是回到 R0。a_grant_stable_during_frame 守前半句,scoreboard 守后半句。
五、功能覆盖:请求向量交叉 grant 与「轮转一圈」
安全断言证明不出错,但证明不了场景都走到。仲裁器的覆盖模型建议包含三层。
第一层是请求向量与裁决结果的交叉:req 的 2^N 种取值(N=4 即 16 种)bin 化,与当拍 grant 交叉,重点看「多请求同拍有效」的组合是否每种都被仲裁过。只收 req 不收交叉,会漏掉「R1、R3 同请求时永远裁给 R3」这类偏向。第二层是轮转完整圈覆盖:定义 cover 序列,满载下连续采到 R0→R1→R2→R3 四次 grant 算一圈,要求仿真中出现至少一圈,且从不同指针初值(复位后初值、随机初值)各出现。第三层是持锁与饥饿边界:burst 长度 bin(1、最长、跨边界)、atomic 期间其他口请求、某口等待拍数接近 K 的窗口。
covergroup arb_cg @(posedge clk);
req_vec: coverpoint req {
bins all15[] = {[1:(1<<N)-1]};
bins multi = {[3:(1<<N)-1]} with ($countones(item) > 1);
}
grant_id: coverpoint grant {
bins g[] = {1, 2, 4, 8};
}
req_x_grant: cross req_vec, grant_id;
endgroup
// 轮转一圈的 cover property
c_full_turn: cover property (@(posedge clk) disable iff (!rst_n)
grant[0] ##1 grant[1] ##1 grant[2] ##1 grant[3]);加权模式额外收「长期比例」:以 1000 次 grant 为窗口,收每个口实际占比的 bin,期望比例(如权重 2:1:1:1 即 50%/25%/…)附近落 bin,远离期望的窗口要能在报告里单独冒出来。
六、scoreboard:把公平性变成计数器与容差
scoreboard 分两半。前一半是逐拍参考模型:testbench 内用一份行为级仲裁器(指针、权重、锁规则照规范独立实现,不抄 RTL),每拍给出期望 grant,与实际逐拍比对,并校验「服务释放后指针才推进」。后一半是统计模型,也是仲裁器验证区别于普通协议验证的地方:

统计上有三个工程细节。其一,公平性是长期性质,不能按单次 grant 判,窗口也不能只在仿真末尾切一个——建议每 W 次服务出一组比例,任一组超阈值都报错,这样「前 5 万拍偏心、后面靠大量均衡流量摊平」的 bug 不会被总比例藏住。其二,容差给数字不给感觉:四 requester RR、窗口 1000 次时,正常抖动落在约 ±2% 以内,阈值收紧到 1% 会误报、放宽到 10% 等于没查;阈值随窗口大小换算,换 N 要重定。其三,部分空闲场景天然不公平——R1 不请求时它的占比当然是零,统计模型必须按「实际发起请求的窗口机会」归一化,或者干脆只在所有 requester 都持续请求的专门用例里检查等比例,随机混合流量只检查无饥饿和顺序,两类场景分开判,结论才干净。
给一个具体算例感受阈值怎么工作。四 requester 等权 RR,窗口 W=1000 次服务,某次回归四个窗口的计数依次是 (252,248,249,251)、(261,240,250,249)、(310,210,255,225)、(249,250,251,250)。前两组最大偏差 2.1%,在 ±2% 抖动的正常范围边缘;第三组偏差 11%,明确报错;第四组回归正常。若只在仿真末尾看 4000 次的总量 (1072,948,1005,975),最差偏差被摊到 2.6%,第三窗口的偏心几乎被藏住。这就是坚持滑动窗口、且每个窗口独立判过的原因。报告里建议把每个窗口的四元组直接打印出来,偏差超标的窗口标红,回归趋势一目了然;跨多次回归对比同一 requester 的偏差方向,还能提前发现「总是 R2 略少」这类系统性偏移——单次看在容差内,方向一致就是缺陷的信号。
加权模式同理,把期望从 1/N 换成 weight_i / Σweight,并增加动态改权重的用例:权重寄存器更新后,从下一个轮转周期起比例应随之变化,更新当拍的在途 burst 不受影响。这类「配置生效边界」是 WRR 实测最常出问题的地方。
七、仿真之外:形式验证补 liveness 与边界穷尽
仿真统计再久也只是「没看到饥饿」,形式验证可以给出「不存在饥饿」的证明,做法在 形式化验证应用 中有完整流程。对仲裁器,假设(assume)三条:grant 被服务后 requester 会在有限拍内释放 frame;输入 req 不做 X/metastable 假设;权重合法(非零)。证明(prove)的结论包括:任意口持续请求时该口必被 grant(无饥饿)、grant one-hot、持锁期间不切换、WRR 下每个口每轮获得份额与权重一致。状态空间小(N=4 的仲裁器只有指针加少量计数器),证明通常分钟级收敛。
混合流量的协议级回归则可以套用 协议与总线验证实用指南 的方法:仲裁器放在总线互连里时,上游用真实协议 VIP(Verification IP,验证 IP)、下游加延迟反压,检查仲裁公平性在反压下是否仍成立——很多仲裁器单验全绿,挂到总线上因为各口 ready 时序不同而隐性偏心,问题不在仲裁逻辑而在 req 生成时机,单口级 testbench 根本构不出这种场景。
八、常见坑
- 把「grant 次数公平」当成全部:长短 burst 混排时次数公平不等于占用带宽公平,两个计数器都要收。
- liveness 写成无界
s_eventually:仿真无法判定,改用有界窗口断言或进形式工具。 - 在部分空闲流量上卡等比例:必然误报,等比例检查限定在满载专用序列。
- 只看末尾总比例:长期摊平会掩盖前期偏心,按滑动窗口逐段判。
- 忽略撤销请求的拍点:req 与 grant 同拍还是延后一拍,断言、参考模型、覆盖三处必须用同一口径。
- atomic 结束后指针落点没查:锁期间不切换只验了一半,释放后必须从锁定口继续轮转。
- 权重更新生效点含糊:在途 burst 是否受影响要写进规范并定向验。
- 单口级验完就收尾:挂总线后 ready 反压造成的隐性偏心,需要互连级回归兜底。
这一篇在知识体系里的位置
- 向上承接:SVA 断言实践 与 代码功能覆盖 提供断言与覆盖语法基础,本篇把它们组合在一个具体模块上。
- 方法支撑:testplan 与覆盖映射 给出功能点到覆盖点的映射纪律。
- 横向相关:形式化验证应用 证明无饥饿,协议与总线验证实用指南 覆盖互连级场景。
总结
仲裁器验证以「逐拍正确性靠断言、长期公平靠统计、无饥饿靠形式」三层分工展开:SVA 守住 one-hot、grant 对应 request、持锁不切换,覆盖穷尽请求向量交叉与轮转整圈,scoreboard 用参考模型逐拍比对并按滑动窗口统计 grant 比例与容差——固定优先级验其「恰好可饥饿」,round-robin 验其严格轮转与等比例,加权轮询再验权重生效边界。