SV unique priority case:case 分支的仿真期违例检查与三选一决策
更新时间:2026-09-28。RTL 里的 case 语句谁都写,但多数仿真期诡异的「跑不通」和综合后「对不上」都能追到 case 分支没有写全或互相重叠。SystemVerilog 在 case/casez/casex 前面加了
unique、priority、unique0三个修饰符,目的不是改变逻辑功能,而是把「我对这个 case 的分支结构有什么假设」显式声明出来,让仿真器在运行期替你把关。这一篇从语义差异讲到违例检查行为,再落到和 lint 豁免与签核 的协作流程。
本文要回答的问题
- unique、priority、unique0 各自声明了什么假设,语义差异在哪?
- 仿真期发生分支重叠或漏分支时,三种修饰符分别报什么、报多狠?
- 加了 unique 之后综合器会做什么优化,这个优化带来什么风险?
- default 分支和 unique/priority 能共存吗,怎么搭配才安全?
- 在回归流程里,这些违例应该在 lint 阶段还是仿真阶段被拦下来?
一、裸 case 的两种隐蔽缺陷
先看不加任何修饰符的普通 case 语句会出什么问题。缺陷一:分支重叠。两个 case item 的条件同时为真,Verilog 规定按书写顺序取第一个匹配的分支,后面的静默失效。如果写代码时本意是互斥条件,只因为信号取值出了意外状态而重叠,仿真结果错了但一行 warning 都没有。缺陷二:漏分支。所有 case item 都不匹配、又没写 default,case 整体不执行任何分支。在组合逻辑 always 块里这直接推断出 latch;在时序块里输出保持旧值,功能错了,同样一声不吭。
always_comb begin
case (sel)
2'b00: y = a;
2'b01: y = b;
2'b1?: y = c; // 与 2'b01 在 sel=01 时重叠;sel=11 才真命中
endcase // sel=10 没有任何分支,也没 default:推断 latch
end一个真实规模的例子能说明这两种缺陷的代价。某 SoC 项目的外设控制器用一个 4 位 one-hot 状态选择子做数据通路译码,验证中期有人加了一个状态却忘了给对应 case item 补赋值分支,裸 case 加上没写 default,仿真照样全绿——因为该状态只在一种罕见中断嵌套时序下进入,回归里没覆盖。直到门级仿真带时序反标后行为偏移,才顺藤摸瓜找回这个 case。如果当初写的是 unique case 并配好 default,仿真器在进入该状态的第一拍就会打出零命中违例,缺陷的活期只有几个小时,而不是跨过一个里程碑。
这两种缺陷的共同点是「假设没有落在代码里」。工程师心里假设分支互斥且完备,但代码上什么都没说,工具无从检查。修饰符的作用就是把这个假设变成可执行的契约:仿真器按契约做运行期检查,综合器按契约做优化。

上图是裸 case 的三种运行结果。两条虚线路径在没有任何修饰符时都不会产生任何报告,这正是后面三个修饰符要分别封堵的洞。
二、unique:声明「分支互斥且完备」
unique case 表达最强假设:任意时刻恰好有一个分支命中。仿真器据此做两类运行期检查:零个分支命中(且无 default)时报违例;多个分支同时命中时也报违例。两类违例默认都是 warning 级信息,打印时间戳和出问题的 case 位置,仿真继续跑。
always_comb begin
unique case (state)
IDLE: nxt = RUN;
RUN: nxt = DONE;
DONE: nxt = IDLE;
default: nxt = IDLE; // default 吸住漏分支,完备性检查交给它兜底
endcase
end有个容易误解的点:写了 default 之后,「零命中」违例就被 default 吸走不再触发(default 本身就是合法命中),但「多命中」违例仍然检查——default 救不了重叠。所以 unique + default 不是免死金牌,重叠分支照样被抓。
综合器对 unique 的反应比仿真器更激进:它把 unique 当作 full_case + parallel_case 双重授权,假设分支互斥且覆盖所有可能取值,于是丢掉优先级编码逻辑、丢掉默认保持路径,生成更小的并行译码。优化本身没问题,问题在于假设一旦是假的,仿真和综合的行为就分叉了:仿真器老老实实按顺序匹配,综合出的网表按并行逻辑算,同一个非法取值两者给出不同结果,这就是经典的前仿真后仿真不一致,定位极其痛苦。放 unique 的前提是分支真的互斥、真的完备,或者完备性由 default 兜住。
还有一个调试效率上的收益常被忽略:unique 违例的报告时间点是违例发生的当拍,而不是错误数据传播到 scoreboard 之后的某个时刻。case 类 bug 最难查的恰恰是「错误赋值发生后,信号经过若干级流水才在比对点失配」,往回追波形要翻几层组合逻辑。运行期违例把报警点直接钉在出事的 case 语句上,第一次失败就是根因现场。这也是为什么很多团队的编码规范强制组合逻辑译码必须带 unique:它不是风格洁癖,而是把定位成本从小时级压到分钟级的投资。
三、priority:只声明「至少一个命中」
priority case 的假设弱一档:它允许分支重叠(重叠时按书写顺序取第一个,语义和裸 case 一致),只要求至少有一个分支命中。仿真期只检查零命中违例,多命中不吭声。
always_comb begin
priority casez (req)
3'b1??: grant = 3'b100; // 最高位优先
3'b?1?: grant = 3'b010;
3'b??1: grant = 3'b001;
default: grant = 3'b000;
endcase
end固定优先级仲裁器是 priority 的标准场景:casez 分支天然重叠,靠书写顺序表达优先级,用 unique 会被海量重叠违例刷屏,用裸 case 又放弃了漏分支检查。priority 恰好卡在中间。综合侧它等价于 full_case:综合器假设未列出的取值不重要,可以当 don't care 优化掉,但保留优先级编码逻辑(不假设互斥)。所以 priority 不会像 unique 那样引入仿真综合语义分叉的互斥性风险,风险只在完备性上。
priority 的一个使用细节值得专门提醒:分支书写顺序本身就是功能的一部分,review 时不能只看「分支齐不齐」,还要看「顺序对不对」。把两条 casez item 上下互换,语法合法、lint 不报、unique 检查也不涉及(反正允许重叠),但仲裁结果整个反过来——高优先级请求被饿死。这类错误靠仿真期检查是抓不住的,只能靠定向激励(同时拉高所有请求位、验证 grant 顺序)或者断言把优先级关系写成属性。修饰符能替你查完备性,查不了「顺序即语义」这类约定。
unique 与 priority 的分工可以用一张表收住。
| 修饰符 | 声明的假设 | 仿真检查零命中 | 仿真检查多命中 | 综合推断 |
|---|---|---|---|---|
| (裸 case) | 无 | 否 | 否 | 保持优先级语义 |
| unique | 恰好一个命中 | 是(无 default 时) | 是 | full_case + parallel_case |
| priority | 至少一个命中 | 是(无 default 时) | 否 | full_case,保留优先级 |
| unique0 | 至多一个命中 | 否 | 是 | parallel_case(非 full) |
四、unique0:只查重叠、不查漏分支
unique0 是个容易被忽略的变体:它声明分支互斥但不完备——允许多个输入值落在所有分支之外(此时 case 什么都不做,和裸 case 一样),但不允许两个分支同时命中。仿真期只报多命中违例,零命中合法。
always_comb begin
unique0 case (op)
OP_ADD: result = x + y;
OP_SUB: result = x - y;
endcase
// 其余 op 值:result 保持/由别处赋值,属预期行为
end典型场景是「只解码关心的少数操作码,其余值走默认数据通路」。用 unique 会对每个未覆盖的 op 值刷违例,全加 waiver 噪音太大;priority 又只查漏不查重叠,恰好方向相反。unique0 正好补上这个角:重叠是 bug(两个操作码条件写串了),漏分支是设计意图。综合侧它等价于 parallel_case——授权互斥优化,但不授权完备性优化,未覆盖的取值仍要保留原有行为路径。

这张图把三者摆成从 unique 出发的两个放松方向:往 priority 走是放弃互斥、往 unique0 走是放弃完备。选型的思考顺序是反过来的——先问自己「这个 case 的分支是真互斥还是靠顺序定优先级」,互斥选 unique 系、顺序选 priority;再问「未覆盖取值是 bug 还是设计意图」,是 bug 用 unique,是意图用 unique0。
需要区分的是 unique0 与「裸 case + 人工小心」的差别。裸 case 下分支重叠同样静默取第一个,和 unique0 的功能行为完全一致;差别只在重叠时有没有告警。有人会说「我的解码表是工具生成的,不可能重叠」,那 unique0 就是免费的保险,写上没有任何运行成本。反过来,如果重叠是生成工具的已知行为(比如按前缀匹配的路由表),那说明场景本质上是 priority 语义,用 unique0 反而会刷噪音。修饰符选型永远跟着语义走,不跟着「能不能消 warning」走。
五、仿真期违例检查的实际表现
违例在仿真期的呈现形式,主流仿真器基本一致:时间戳 + 文件行号 + 违例类型 + 当前选择子取值。例如 unique case 在 t=1250ns 时 sel 同时匹配两个 item,会打出类似 RT Warning: More than one condition match in 'unique case' statement 的信息,仿真不中断。想让它变致命,一是仿真器选项把这类 warning 提升为 error,二是回归脚本按日志模式匹配计数,违例数非零直接判红——后一种更常见,因为提升为 error 会在海量违例的第一次就掐死仿真,反而丢掉其他违例的现场。
这里有一条回归管理的实战经验:unique 违例经常在 X 传播场景下批量出现——case 选择子采到了未初始化的寄存器,命中行为变得不可预期。处理顺序是先修 X 源头(复位覆盖、初始化检查),再回头看违例是不是自然消失,而不是对着几百条违例逐条加豁免。X 传播在 case 里的坑和 四态仿真与 X 传播 讲的门级问题是同一族:选择子里混进 X 之后,casez/casex 的通配匹配结果和直觉经常相反。

图里刻意把「回归判红」画在仿真器之外:违例检查是仿真器的职责,把告警升级为失败是回归流程的职责,两层职责分开,仿真才能在一次运行里收集到尽量多的违例现场。
不同仿真器在违例措辞和严重级别上略有差异:有的把 unique 违例默认归为 warning 且可被 -assert 类选项统一升级,有的允许按消息 ID 单独控制。团队落地时要做两件事:其一,在公共仿真选项文件里固定这些消息的处理策略,避免不同工程师本地跑出不同严重级;其二,把违例消息 ID 写进回归日志的白名单清单——注意是白名单不是黑名单,即「除了已知豁免的消息 ID 之外,任何 unique/priority 消息都判红」。黑名单式维护(列已知问题、其余放行)会随代码演进不断漏进新违例,白名单才能保持默认收紧。
另一个实际问题是第三方 VIP 和复用 IP 内部也会带 unique case,它们的违例不该由你修,但会混在日志里抬高噪音。处理办法是在白名单里按「消息 ID + 层次路径前缀」豁免,精确到 IP 实例路径,而不是按消息 ID 全局豁免——后者会把自己 RTL 里的同类违例一并放跑。
六、与 lint 的分工及落地建议
仿真期检查有个天然短板:只覆盖仿真实际跑到的输入组合。选择子从未取到违例取值,检查就是摆设。lint 工具在静态层补齐这一块:分析 case item 的条件表达式,直接报告可重叠的分支对和未覆盖的取值范围,不需要任何激励。所以健康流程是双层的——lint 在编码提交时拦结构性问题(分支重叠、明显漏分支),仿真在运行期拦动态问题(X 传播引起的意外命中、时序相关取值)。两层都有漏网之鱼时,问题性质已经偏向 FSM 状态跳转验证 要处理的领域:非法状态进入、状态编码冲突,需要定向用例或断言收尾。
落地建议归拢成四条。第一,状态机译码、操作码译码这类真互斥场景一律 unique + default,default 里放安全态或 $error,不要把 default 当成隐藏 bug 的垃圾桶。第二,优先级仲裁、中断判选这类靠顺序表达优先级的场景用 priority,绝不为了「消 warning」改成 unique。第三,只解码部分取值的场景用 unique0,比裸 case 多一层重叠保护。第四,casez/casex 的选择子表达式里杜绝带 ? 通配的常量以外的 X 来源——选择子本身含 X 时,unique 的互斥假设在仿真语义里已经不成立,违例报告会指向分支而不是真正的病根。四条都做不到的场景,老实写裸 case 并在注释里说明原因,比加一个经不起推敲的修饰符诚实得多。
| 场景 | 推荐修饰符 | 理由 |
|---|---|---|
| FSM 状态译码 | unique + default | 状态互斥,default 兜非法态 |
| 固定优先级仲裁 | priority | 分支重叠是设计意图 |
| 部分操作码解码 | unique0 | 漏分支合法,重叠是 bug |
| 选择子可能含 X 的探测逻辑 | 裸 case(注释说明) | 修饰符假设在 X 下不成立 |
上面这张速查表背后的判定顺序,画成流程更不容易记错:

按图走一遍:第一问砍掉 priority 系和裸 case 的模糊地带,第二问在 unique 与 unique0 之间定界,含 X 的特殊场景单独落到裸 case。判定流程的价值在于 review 时可以逐问核对,而不是对着表格猜场景归属。
这一篇在知识体系里的位置
- 向上承接:SystemVerilog testbench 基础 的编码约定,本篇把 case 语义细化到可检查层面。
- 静态检查侧:lint 豁免与签核 处理同一类问题的静态层,与仿真期检查互补。
- 动态行为侧:四态仿真与 X 传播 解释 unique 违例批量出现时的 X 根源。
- 验证对象侧:FSM 状态跳转验证 是 unique case 最典型的应用对象;断言层收尾见 SVA 断言实践。
总结
SV unique priority case 三个修饰符本质是把「分支恰好一个命中 / 至少一个命中 / 至多一个命中」的假设写成可检查契约:unique 查零命中与重叠、priority 只查零命中、unique0 只查重叠,仿真器在运行期把关、lint 在静态层补齐,选型的唯一依据是这个 case 的分支结构本身——互斥用 unique 系、靠顺序定优先级用 priority、部分解码用 unique0。