第二章约束环境建模

为什么需要约束

形式验证会穷尽所有可能的输入组合。但在实际设计中,许多输入组合是不合法的(例如协议违规的输入)。如果不约束这些输入,形式验证工具会报告大量虚假的反例(false negative)。

Assume 约束用于定义合法输入空间,告诉工具"输入只会是这样的"。

约束输入

使用 SVA 的 assume property 编写输入约束:

// 示例:假设 valid 信号在 reset 后初始为低
asm_reset_valid: assume property (@(posedge clk) $fell(rstN) |=> !valid);

// 示例:假设 ready 最终会拉高(避免死锁)
asm_ready_eventually: assume property (@(posedge clk) valid |-> s_eventually ready);

// 示例:假设 op 编码只使用 0-3
asm_op_valid: assume property (@(posedge clk) op inside {[0:3]});

黑盒与白盒

黑盒(Black Box)

将某个模块声明为黑盒后,其输出变为自由变量(不受约束),内部逻辑被忽略。这在以下场景有用:

# 按模块名黑盒(可重复使用该开关,或用通配符)
elaborate -top top -bbox_m unimplemented_module

# 按实例黑盒
elaborate -top top -bbox_i top.u_pll

# 把缺失模块自动当作黑盒(0 = 缺失模块报错,为默认行为)
elaborate -top top -bbox 1

白盒(White Box)

默认情况下所有模块都是白盒——内部信号和逻辑都可见。JasperGold 可以引用白盒内的任意内部信号用于断言。

初始状态设定

默认情况下,JasperGold 从复位后的状态开始证明。初始状态由 reset 命令的相关开关设定:

# 用一个复位快照文件设定初始状态
reset -init_state <file_name>

# 把不可复位的寄存器初始化为指定常数
reset -expression ~rstN -non_resettable_regs 0
初始状态是通过文件(复位快照)来指定的,没有"给某个寄存器逐个赋初值"的命令形式。此外 abstract -init_value <register_name_tcl_list> 可以把指定寄存器的初值抽象掉——被抽象初值的信号,其复位值不再纳入分析。

设计复杂度评估

get_design_info 命令输出设计的复杂度指标:

get_design_info

Design Information 用于洞察设计的形式计算复杂度。它提供:

-list 支持在一条命令里给出多个枚举类型,例如 get_design_info -instance myInstance -list output input。常用的枚举包括 floplatchgateregistercounterfsmfifoinputoutputstopatbbox_modbbox_inst 等。不指定任何参数时,返回顶层实例的信息。

还有一个深度计算用法,可以数出某个信号或属性周围若干层内的寄存器:

get_design_info (-signal <name>+ | -property <name>+) \
                 -list (flop | latch | register) -depth <N> [-boundary]
注意这里的 -depth 是按 flop 层数来算的(-list flop 即"以 flop 层为单位做深度计算"),它不是"组合逻辑深度"这种时序指标。get_design_info 并不报告组合逻辑深度,那属于 HAL / Superlint 的 lint 检查范畴。

还可以指定作用域(scope)来限定统计范围:-instance-module-signal-property-task。例如可以只取某个 task 中所有属性的传递扇入里的 flop 列表。加 -silent 可只返回 Tcl 值而不打印,便于在脚本里使用。

过约束(Overconstraint)问题

过约束(overconstraint)的准确定义是:施加在设计上的约束缩小了设计的运行模式(即可达状态空间),以致于得到的任何证明都不是完整证明(full proof)。文档把过约束归类为部分证明(partial proof)的两种成因之一——另一种是有界证明(只检查从复位状态起 N 拍以内的 bug)。

过约束与空洞证明(property vacuity)密切相关,但两者定义不同。空洞证明指的是:属性中包含一个 precondition(前提),属性证明通过了,但那个 precondition 因为不可达而从未真正发生。例如在某种配置下 req 根本不可能为 1 时:

// SystemVerilog Assertion
assert property (@(posedge clk) req |=> ack);

这条断言会"证明通过",但它什么也没验证。工具会在 proven / unreachable 属性的状态旁边显示 vacuity 指示符;当整个 task 被过约束时,同样会看到这个指示符,鼠标悬停可以看到该 task 在第几拍被过约束。

检查过约束的方法:

区分两件事check_assumptions 针对的是"约束过强";而"约束彼此矛盾/与设计矛盾"是另一回事——它会以 error 有效性状态呈现出来(含义是"属性编译超时,或证明过程发现 task 中的假设与设计、或假设彼此之间不一致")。此外,当一个 task 被过约束时,工具会在其 proven 和 unreachable 属性上显示 vacuity(空洞)指示符,鼠标悬停可以看到该 task 在第几拍被过约束。

约束方法论:定义合法的输入空间

约束(assume)是形式验证中最容易被忽视、也是最容易出错的部分。约束决定了形式引擎在什么输入空间内验证设计。

约束的本质

形式引擎会尝试所有可能的输入组合来寻找反例。但在真实芯片中,输入并非完全自由——上游模块、协议规范、物理限制都会限制输入的合法取值。约束的作用就是告诉工具这些限制,让引擎只在合法的输入空间内搜索。

约束过强 vs 约束过弱

⚠️ 约束过强(Over-constrain)

症状:属性全部 proven,但实际芯片有 bug

原因:约束把合法的输入场景也排除了,引擎只在过小的空间内验证,bug 所在的场景被约束挡住

案例:约束了"req 永远不会连续拉高",但实际设计中连续请求是合法的,导致仲裁器在连续请求时的 bug 被掩盖

🔴 约束过弱(Under-constrain)

症状:大量反例,全是"不可能发生"的输入组合

原因:缺少必要约束,引擎产生了设计规范不允许的输入组合

案例:没有约束"复位期间不能发请求",导致引擎在复位期间驱动有效请求,产生大量假反例

约束验证:用 cover 证明约束合理

怎么知道约束是否恰到好处?核心方法是写 cover 属性来"探测"合法场景:

验证约束的最佳实践:在 prove 之前,先单独 run cover 属性。如果所有合法场景的 cover 都能击中,说明约束空间足够大;如果有 cover 无法击中,需要分析是约束过强还是设计问题。

常见约束模式

模式SVA 示例用途
有效电平assume {valid |-> ready};valid 和 ready 的协议关系
时序关系assume {req |-> ##[1:3] gnt};请求到授权的延迟约束
互斥assume {$onehot0(mode)};模式信号最多选一个
稳定信号assume property (@(posedge clk) !SCmdAccept |=> $stable(MCmd));握手未被接受时,命令信号在下一拍保持不变
常量信号asm_my_tag_constant: assume property (@(posedge clk) $stable(my_tag));把某个信号约束为全程恒定
最终响应(liveness)req |=> ##[0:$] ack;req 拉高后,ack 最终会在将来某一拍拉高
区间内保持(~write throughout (read & test_addr==addr)[->1])在"读到该地址"这件事发生之前的整段区间内,write 始终为低
throughout 的右操作数必须是一个 sequence,不能是一个裸的布尔信号。上面这条来自官方 Proof Accelerator 的 RAM 模型(jasper_model_ram),右边用的是 (read & test_addr==addr)[->1]——[->1]goto repetition(形如 a[->3]),表示"一直走到该条件第 1 次成立为止"。整条的含义是:在这段区间内 ~write 必须全程成立。写成 $stable(data) throughout valid 这种右边是裸布尔量的形式是不成立的。
注意 ##[0:$] 是无上界的$ 表示"将来的某个时刻",不含任何时间上限。它表达的是 liveness(最终会发生),不能用来表达"在 N 拍之内"。要限定窗口必须写成有界形式,如 ##[1:3]。文档也提醒:这类关于设计行为的笼统陈述,往往写成更具体的 safety property 会更好。

约束调试技巧

当你看到一个"不合理"的反例时(波形看起来很奇怪):

  1. 检查输入端口的波形——是不是出现了规范不允许的组合?
  2. 如果是 → 添加 assume 排除该组合
  3. 如果输入看起来合法 → 这可能是真的 bug
  4. 对每个新加的 assume,用 cover 确认它没有排除合法场景
迭代过程:约束和断言是交替完善的。先加基本约束 → prove → 看反例 → 补充约束或修改断言 → 再 prove。这个迭代过程是正常的,通常需要几轮才能得到干净的证明结果。

来源文档

  • jaspergold_apps_userguide.pdf(Appendix B:有效性状态与 vacuity 指示符)
  • AN_scripting.pdf
  • jaspergold_command_reference.pdf(assume / elaborate -bbox_* / reset -init_state / abstract / get_design_info / check_cov)
  • glossary.pdf(overconstraint、liveness property)