简介:《SpyGlass LintRules Reference Guide》是Synopsys官方发布的静态检查规则参考手册,对应Q-2020.03-SP1,也是2020年6月版。它面向数字前端设计、IC验证与Signoff工程师,以及需要为Verilog/VHDL代码建立编码规范检查流程的团队,帮助读者在RTL设计早期定位语法隐患、风格违规、时序与功耗相关风险以及接口约束问题,从而降低后期返工成本。压缩包内只有1个PDF文件,大小约2.04MB;正文逐条编排规则,每条都给出规则ID、用途描述、示例代码、修改建议,以及警告、错误、信息三级严重性,并保留版权、出口管制与免责声明原文。目前已有4495人学习下载。该手册覆盖设计风格、时序分析、功耗优化、设计约束、接口检查等规则方向,并列出allow_clk_in_condition、allviol、avoid_seq_logic等参数各自的取值与作用,说明如何按项目需要启用或停用规则、调整阈值或添加例外,因此可用于搭建自定义lint规则集、编写检查清单,也便于在规则报错时快速对照代码写法与修复思路,是日常SpyGlass lint排查的案头参考。
1. 一本被当成附录的规则手册,往往决定 lint 报告该不该信
跑完一轮 spyglass lint,报告里躺着几百条 W 级告警,逐条改还是整体压?大多数人第一反应是找老同事要一份 waiver 模板,能压的全压掉,剩下几十条再手工看。问题在于,压掉的那部分里到底有多少是真问题,谁也说不清——因为没人读过规则手册。
这份 Q-2020.03-SP1 的 lint 规则参考,正文部分就是一张一百多个参数的字母序清单,从allow_clk_in_condition一直排到use_carry_bit,每个参数后面跟着适用范围、默认值和语义说明。它不教你怎么搭工程,只回答一件事:当某条规则误报,或者需要把规则改用更严格的标准时,该改哪个开关,取值范围是什么。前端设计做完 RTL 想自检、验证同事要定团队规则基线、或者接手一个跑得动但结果很吵的老工程,都绕不开这本手册。下面按我自己实际的使用顺序拆一遍。
2. 手册的骨架:从参数表反查规则才是正确检索方向
拿到这份 PDF 的第一件事不是从前言读起,而是认清它和信息类文档的区别——这是一本字典,不是教程。字典的用法是碰到问题去查,不是通读。但查之前得搞清楚它的组织方式,否则打开目录会一脸茫然:为什么按check_开头排,而不是按规则 ID 排。
2.1 一条 lint 告警由四块信息拼成
理解规则手册之前,先在脑子里建立一个模型:每条 lint 告警实际由四块信息构成,缺一块就定位不到。
第一块是规则 ID,形如W164a、W71这种带字母后缀的编号。ID 是唯一标识,报告里按它聚合,waiver 里按它匹配。第二块是规则描述,说明这条规则在检查什么语义——比如"组合逻辑块中条件语句缺少 else 分支可能推断出锁存器"。第三块是违规定位,具体到文件、行号、信号名。第四块就是规则参数,也就是这本手册真正在讲的内容:这条规则的判定边界可以被哪些开关调整。
关键点在于,参数名和规则 ID 不是一一对应的。一个参数可能被多条规则共用,比如ignore_parameter这类过滤开关在很多规则里都会出现;一条规则也可能挂三四个参数,分别控制严格度、阈值和例外名单。所以查的顺序是倒过来的:先从报告里拿到规则 ID,进工具的规则说明窗口看它引用了哪些参数名,再回这本手册按字母序查参数语义和取值范围。直接翻手册正文找规则名,基本找不到。
我见过不止一个人把参数名当成规则名去搜,搜不到就以为手册缺页。这不是文档的问题,是查法的问题。
2.2 参数按语义分四类,处置方式完全不同
一百多个参数看着乱,其实按语义只有四类,每类的处置手法差别很大。
第一类是开关型,取值就是 yes / no 两态,比如allow_clk_in_condition(是否允许时钟出现在条件表达式里)、avoid_seq_logic、treat_latch_as_combinational、fast。这类参数没有中间地带,一改就是整片结果集变化,改之前必须先把改动前的报告存一份做基线,否则改完你都不知道是变好了还是变坏了。
第二类是阈值型,取值是数字或宽度,比如casesize(case 语句规模阈值)、check_concat_max_width(拼接最大宽度)、limit_task_function_scope、latch_effort_level。阈值型参数的好处是可以渐进调,代价是它天然带主观性——阈值设多少合理,取决于设计规范和团队容忍度,手册只给默认值,不给标准答案。
第三类是名单型,取值是一组名字,比如sign_extend_func_names、ignoreCellName、ignoreModuleInstance、ignore_in_ports。名单型最容易出问题,因为写错了名字工具不报错,只是静默不生效,你会以为开关没起作用。
第四类是作用域型,控制检查或报告的范围,比如ignore_hier_scope_var(按层次作用域过滤变量)、report_only_from_one_hierarchy、traverse_function、report_hierarchy。这类参数在层次化设计里价值最高,一个几千实例的顶层,不加作用域过滤,报告会直接淹掉。
| 参数类型 | 典型参数 | 取值范围 | 改动风险 |
|---|---|---|---|
| 开关型 | treat_latch_as_combinational、fast | yes / no | 高,结果集整体漂移 |
| 阈值型 | casesize、check_concat_max_width | 数值 / 位宽 | 中,需与规范对齐 |
| 名单型 | ignoreCellName、sign_extend_func_names | 名字列表 | 中,写错静默失效 |
| 作用域型 | ignore_hier_scope_var、traverse_function | 路径 / 层次 | 中高,易漏检 |
这张表我一般贴在工程文档首页,新人第一周就要求看一遍。因为绝大多数"参数设了没用"的问题,都能从这张表里找到原因:开关型改了没生效说明被上层配置覆盖,名单型改了没生效多半是名字写错。
2.3 用五分钟把 PDF 变成可检索的索引
PDF 最大的问题是搜不动。字母序参数表里,一个参数名的说明可能跨页,搜索命中后还得上下翻。我一般先把正文抽成纯文本,再用 grep 做索引,后面几个月都能省事。
# 保留排版抽取,参数名与说明的对应关系不会错行 pdftotext -layout SpyGlass_LintRules_Reference.pdf lint_ref.txt # 统计行数,确认没抽出乱码或空页 wc -l lint_ref.txt # 提取所有形如小写下划线标识符的独立行,即参数名条目 grep -nE '^[[:space:]]{0,4}[a-z][a-z0-9_]{5,}[[:space:]]*$' lint_ref.txt > param_index.txt wc -l param_index.txt第一行的-layout参数是必须的,不加的话表格会被打散成一行一个字,参数名和说明彻底错位。第三条正则限制缩进不超过四格、整行只有一个标识符,是为了过滤掉正文里恰好出现的小写单词,只留独占一行的参数标题。最后统计出来的行数应该在一百多,和手册目录里的参数数量对得上,对不上说明抽取有问题,得换工具重来。
索引文件建好之后,日常查参数就是grep -A 20 'handle_large_bus' param_index.txt一句话的事,比翻 PDF 快一个数量级。如果团队里有多人在用,把这个索引文件扔进内部文档库,比传 PDF 更实用。
3. 把参数落到工程里:工程文件、目标与批量开关
手册讲的是"能配什么",实际工程里要解决的是"配在哪"。这两件事经常脱节——参数语义看懂了,写进工程文件却没生效,或者生效了但下次跑又变回去。原因在于参数有覆盖层级,而且规则参数和工程选项是两套东西。
3.1 一个能跑通的最小工程骨架
先给一个最小骨架,能跑起来的工程才有讨论参数的意义。下面这份工程文件只用最基本的几行,把源码、约束、目标和方法学固定下来。
# lint.prj —— lint 目标的最小可用骨架 set_option projectwdir ./sg_work set_option language_mode mixed set_option enableSV yes set_option top core_top read_file -type sourcelist ./filelist.f read_file -type sgdc ./constraints/core.sgdc # 方法学决定默认启用哪些规则,必须先定 current_methodology $SPYGLASS_HOME/GuideWare/latest/block/rtl_handoff current_goal lint/lint_rtl -top core_top set_goal_option report_waived yes run_goal逐行说明。projectwdir指定工作目录,所有中间结果和报告都落在这里,不要用默认值,默认值会把产物散落在当前路径,CI 上跑几次就乱了。language_mode mixed允许 Verilog 与 SystemVerilog 混编,老工程里常见。enableSV yes打开 SystemVerilog 解析,不开的话logic、always_ff这些关键字会直接语法报错,报出来的还是无意义的解析错误,很费时间排查。
read_file -type sourcelist读文件列表,-type sgdc读约束文件。约束文件对 lint 的影响常被低估:时序约束缺失时,checksyncreset、report_inter_nba一类与复位和跨时钟相关的规则根本没法判,会给出大量无意义结果。
current_methodology是整套流程里最容易被忽略的一行。方法学决定默认规则集和默认参数值——同一个设计换一个方法学,报告条数能差三倍。所以团队内部必须把方法学版本写死在工程文件里,不要依赖环境变量。
current_goal lint/lint_rtl -top core_top选定目标。lint 下有多个目标粒度,lint_rtl是最常用的 RTL 级检查,功能 RTL 阶段还会用到更细的目标。目标选定之后,set_goal_option和规则参数才有效,写在current_goal之前的参数会被当成全局默认,行为可能和你预期的不一致。
3.2 参数的三层覆盖:默认值、工程级、目标级
参数生效顺序是新手最容易踩的坑。默认值来自方法学,工程级设置在current_goal之前,目标级设置在current_goal之后。三层同时存在时,后者覆盖前者,而且覆盖是静默的——工具不会告诉你某个参数被覆盖了,你只会看到结果和预期不符。
常见误用是这样:在工程文件开头写了set_option handle_large_bus yes,结果报告里还是有大量总线截断告警。原因是这条参数实际挂在具体规则上,工程级的全局设置对它不生效,必须在目标级按规则名单独设。这类"设了但没生效"的问题,我一般用两步确认:先看运行日志里回显的最终配置,确认参数是否被接受;再在报告目录里翻配置快照,确认生效值。
提示:任何参数改动之后,第一件事是去日志里找配置回显,确认值被接受。没回显的参数等于没写。
目标级设置的具体写法,不同版本命令名会有细微差异,稳妥做法是在交互 shell 里help一下确认命令签名,再写进工程文件。下面给的是常见的几类写法,含义比语法更重要:
# 目标级:按规则挂参数,规则 ID 必须写全 # 开关型:控制锁存器识别口径 treat_latch_as_combinational yes # 阈值型:case 规模超过该值就提示 casesize 16 # 名单型:这些单元名不参与某类检查 ignoreCellName "CLKBUF_X1 CLKBUF_X2" # 作用域型:只看指定层次下的变量 ignore_hier_scope_var "tb_top.u_dut.u_core.*"这四个例子恰好覆盖了 2.2 里的四种类型。注意第四行用了通配符,作用域型参数普遍支持路径通配,但通配符写错(比如少一个点、多一个层)不会报错,只会静默匹配不到,报告条数一点不减。这是我在项目里见过最多的一类问题。
3.3 用脚本生成开关矩阵,别手工改工程文件
参数一多,手工改工程文件必然出错。我的做法是维护一份 YAML 或 CSV 的参数表,用脚本生成每个目标对应的配置片段,工程文件里source进来。这样参数有版本管理,改动能 diff,回滚也方便。
import csv # 参数表三列:规则ID、参数名、取值;空规则ID表示全局参数 rows = list(csv.DictReader(open("params.csv", encoding="utf-8"))) lines = ["# 由 gen_params.py 自动生成,勿手改", ""] for r in rows: rule, name, val = r["rule"].strip(), r["param"].strip(), r["value"].strip() if not name or not val: continue if rule: # 目标级:挂到具体规则 lines.append(f"set_rule_parameter {rule} {name} {val}") else: # 工程级:全局默认 lines.append(f"set_option {name} {val}") open("params_gen.tcl", "w", encoding="utf-8").write("\n".join(lines) + "\n") print(f"生成 {len(lines) - 2} 条参数配置")这段脚本的核心是rule列空与非空的分流:空的走全局set_option,非空的走规则级set_rule_parameter。之所以要分流,就是 3.2 里说的覆盖层级问题——很多参数只在规则级生效,塞进全局设置里等于白写。
params.csv里建议再加两列:一列写"改动原因",一列写"责任人"。前者在评审时救命,半年后没人记得casesize为什么从 16 改成 8;后者在出问题时能快速找到人。这两列不参与生成,纯做记录。
生成之后在工程文件里加一行source ./params_gen.tcl,放在current_goal之后。每次参数调整只改 CSV,重跑脚本,再跑一遍目标,配置和结果就都是可复现的。
3.4 报告落在哪,日志里先看哪三行
跑完之后的问题定位效率,取决于你知不知道先看什么。报告目录一般在工作目录下按目标和顶层名分层,里面会有汇总报告和明细报告两类。汇总报告给你条数分布,明细报告给你逐条定位——先看汇总,再看明细,这个顺序反了会浪费大量时间。
日志先看三行:解析错误数、目标是否成功完成、被启用的规则数。解析错误不为零时,后面所有 lint 结论都不可信,先修语法;目标没跑完基本是约束文件路径或方法学路径写错;启用规则数和你预期的差异大,通常是方法学版本不一样。这三行看完,才轮到翻报告正文。
4. 典型规则族拆解:位宽、敏感表与锁存器三条主线
手册里一百多个参数,日常真正高频用到的集中在三个族:位宽与符号扩展、组合逻辑与敏感表、赋值语义与锁存器。这三族撑起了 lint 报告的大半篇幅,也是误报最集中的地方。下面按族拆,每族给一段典型代码和对应的参数组合。
4.1 位宽与符号扩展族:先分清自然位宽和 LRM 位宽
位宽类告警是 lint 里数量最多、也最容易被无脑压掉的一类。手册里相关参数密集得反常:check_lrm_and_natural_width、check_natural_width_of_multiplication、check_natural_width_for_static、check_static_natural_width、check_shifted_width、check_shifted_only、check_sign_extend、check_unsign_overflow、check_concat_max_width、concat_width_nf、new_flow_width、nocheckoverflow、ignore_mult_and_div、handle_shift_op、force_handle_shift_op、handle_lrm_param_in_shift。数量多说明一件事:位宽判定规则本身有分歧,工具给了你选择口径的余地。
口径分歧的根源是两套标准。LRM 位宽是语言标准规定的表达式结果宽度,自然位宽是"这个运算在数学上需要多少位才不丢信息"。两者在加法、移位、乘法上经常不一致。
wire [7:0] a, b; wire [8:0] s1 = a + b; // 9 位承接进位,自然位宽足够 wire [7:0] s2 = a + b; // 进位被截掉,典型的位宽告警点 wire [15:0] m = a * b; // 8x8 结果最多 16 位,刚好够 wire [7:0] m2 = a * b; // 高 8 位丢失,乘法位宽告警s2和m2这两行是标准的问题写法,但工程里也存在大量"故意截断"的场景,比如哈希、CRC、伪随机数生成。这种场景下去压规则是错的,正确做法是用nocheckoverflow关掉溢出类检查的局部触发,或者用ignore_mult_and_div把乘除运算整体排除在该规则之外。
new_flow_width是个影响面很大的开关,它切换位宽分析的整套流程。老工程升级工具版本后报告条数突然翻倍,八成是这条参数的默认值变了。我的处理原则是:不在同一个工程里混用两套流程,要么全走新流程把新增告警逐条过一遍,要么显式关掉保持旧行为,绝不半开半关。
check_shifted_only和check_shifted_width常被混淆。前者限定只检查移位运算的被移位操作数,后者控制移位结果的宽度检查。设计里如果有大量参数化移位(a >> shift_amount),把这两个参数混设会导致一部分移位完全不检查,而且不会有任何提示。
check_sign_extend配上ignore_signed_expressions是一组。有符号运算在 RTL 里本来就少,一旦出现几乎必有问题,我一般保持check_sign_extend开启,只在验证平台代码里用ignore_signed_expressions排除。sign_extend_func_names则用来声明哪些函数本身就是做符号扩展的,避免在函数内部再报一遍——这类名单型参数写漏一个函数名,函数体里就会冒出一堆重复告警。
4.2 敏感表与锁存器族:判定松紧直接决定报告噪声
组合逻辑相关的参数是第二密集区:check_implicit_senselist、simplesense、check_latch、latch_effort_level、treat_latch_as_combinational、disable_latch_crossing、reportLibLatch、avoid_seq_logic、allow_clk_in_condition、check_case_type、handle_case_select、ignore_if_case_statement、handle_static_caselabels、ignore_priority_case。
敏感表问题最典型的写法是漏信号:
always @(a or b) begin y = a & c; // c 不在敏感表里,仿真与综合结果可能不一致 end always @(*) begin if (en) q = d; // 无 else,组合逻辑推断出锁存器 end第一段的漏信号由check_implicit_senselist和simplesense联合判定。simplesense对简单敏感表(只有信号名、没有表达式)做额外检查,能抓到一些隐式敏感表漏掉的场景。两者都开的时候报告会重复,这是正常的,报告聚合时按信号去重即可。ignore_forloop_indexes和ignore_genvar用来放过 for 循环索引和 genvar 这类不可能出现在敏感表里的名字,不加的话每个 generate 块都会冒告警。
第二段的锁存器问题由check_latch控制,但真正的调节旋钮是latch_effort_level。这个参数决定工具花多大力气去证明"这个 if 分支虽然缺 else,但输出在其他路径被完整赋值了"。等级调高,误报少,运行时间涨;调低,跑得快,但报告里会混进大量实际不构成锁存器的写法。我一般对新模块调到中高档,先看清真实问题;等设计稳定、报告收敛之后降到中档,换取回归速度。
treat_latch_as_combinational是个语义级开关,它改变工具对锁存器的分类态度,影响的不只是check_latch一条规则,还会波及跨时钟域、复位相关的一批检查。改之前务必存基线,改完做报告 diff,否则很容易出现"锁存器告警没了,但别的地方多了二十条"这种局面。disable_latch_crossing则是关闭锁存器跨时钟域的专项检查,只在明确知道设计里有异步锁存器通路时开。
allow_clk_in_condition处理的是时钟出现在条件表达式里的场景。测试逻辑里用if (clk) ...做门控仿真很常见,生产 RTL 里出现就是设计问题。这个开关默认关闭是正确的,只有验证平台单独跑目标时才该打开。
case 相关的参数构成一个小族。casesize控制 case 语句规模阈值,超过阈值的长 case 会被提示考虑改写成查表或分段;check_case_type检查 case / casez / casex 的使用是否合适;handle_case_select影响 case 选择表达式本身的检查。ignore_priority_case和ignore_if_case_statement是纯过滤,前者放过 priority 修饰的 case,后者放过 if-else 链改写的 case 结构——这两个在有大量优先级编码逻辑的设计里必开,否则报告里一半是同类噪声。
4.3 赋值语义族:阻塞非阻塞、计数器与临时变量
第三族管赋值语义:checkblocking、checknonblocking、report_inter_nba、check_sequential、check_temporary_flop、check_initialization_assignment、check_counter_assignment、check_counter_assignment_turbo、ignore_counter_with_same_width、ignore_nonstatic_counter、check_const_selector、check_const_selector。
always @(posedge clk) begin a = b; // 时序块里用阻塞赋值,典型告警 c <= a; // 与上一行构成竞争,report_inter_nba 会命中 end always @(posedge clk) begin if (rst) cnt <= 4'd0; else cnt <= cnt + 1'b1; // 计数器自然位宽问题 end第一段的a = b由checkblocking抓,report_inter_nba抓的是同一块里阻塞与非阻塞混用导致的中途赋值竞争——这个组合是最危险的一类,仿真能过、综合后行为不同,属于典型的黑匣子问题。checknonblocking抓的是反向情况:组合块里用了非阻塞赋值。三条规则一起开,赋值语义这层基本就封死了。
第二段的计数器加法,问题在于cnt + 1'b1的自然位宽比cnt多一位。check_counter_assignment负责这类检查,check_counter_assignment_turbo是更激进的版本,能抓到跨语句的计数器位宽传递。ignore_counter_with_same_width放过那些"计数器宽度和步进宽度一致"的场景,ignore_nonstatic_counter放过非静态(运行时可配置)计数器。这两个过滤参数在带大量可配置计数器的设计里很关键,不开的话报告会被计数器淹没。
check_temporary_flop检查的是临时寄存器——那种为了对齐时序而在两级逻辑之间插入、但实际可以被优化掉的寄存器。这条规则的价值取决于综合策略,如果后端会做寄存器复制和合并,这类检查意义不大,可以关;如果要求 RTL 与网表严格对应,就得留着。
check_initialization_assignment和check_const_selector是一组偏"风格"的检查,前者看初始化赋值,后者看常量选择器。这两条在纯 ASIC 流程里通常只作为提示级,不参与门禁。
4.4 过滤参数:把噪声压下去,而不是把规则关掉
三族讲完,会有一个共同感受:噪声太多。正确做法不是关规则,而是用过滤参数精准排除。
过滤参数按作用对象分三档。第一档按名字排除:ignore_inout、ignore_in_ports、ignore_genvar、ignore_local_variables、ignore_parameter、ignore_typedefs、ignore_interface_locals、ignore_auto_function_return、ignore_function_init、ignore_macro_to_nonmacro、ignore_unique_and_priority、ignore_const_selector。这类参数的关键是名字匹配要精确,写ignore_local_variables yes和写具体名字列表是两回事,前者把整类变量全放过,后者只放过指定的。
第二档按结构排除:ignore_concat_expr、ignore_cond_having_identifier、ignore_multi_assign_in_genforblock、ignore_multi_assign_in_forloop、ignore_generatefor_index、report_semicolon、report_only_bitwise、report_only_overflow、report_only_from_one_hierarchy、report_only_from_one_hierarchy。
第三档按单元排除:ignoreCellName、ignoreModuleInstance、report_blackbox_inst、ignore_greybox_drivers。黑盒和灰盒处理是层次化设计的必需品,report_blackbox_inst控制是否报告黑盒实例的连接,ignore_greybox_drivers控制灰盒驱动是否被当成真实驱动源——后者设错会导致跨层次路径检查直接失效,很难发现。
注意:过滤参数本质是"缩小检查范围",每加一条就少一份保障。加过滤之前先在评审里写清楚为什么,别在工程文件里无声堆积。
5. 避坑:五条从日志里挖出来的经验
规则手册不可能写清楚的部分,只能靠踩。下面五条都是我在实际工程里反复遇到的,按「现象 → 原因 → 解决」记,遇到同类问题时对着查。
5.1 参数写进工程文件,报告一条没少
现象:把handle_large_bus yes写进工程文件开头,重跑之后超大总线的截断告警数量完全没变。
原因:这条参数挂在具体规则上,工程级的全局set_option对它无效。工具接受这条设置但不会应用到规则,日志里也没有告警。类似情况还有ignore_genvar、ignore_forloop_indexes这类过滤参数,全都必须走规则级设置。
解决:查手册确认参数归属的规则,改用规则级写法挂到对应规则;改完之后去报告目录翻配置快照,确认参数出现在最终生效列表里。这一步不做,任何参数改动都不可信。
5.2 名单型参数写了,检查还是报
现象:ignoreModuleInstance里加了三个模块实例路径,对应的告警一条没减。
原因:名单型参数的路径匹配是严格的层次路径,必须从顶层开始写全,中间少一层或者用错分隔符都匹配不到。更隐蔽的情况是通配符用错——*和.*在不同参数里语义不同,有的按字符匹配,有的按整段层次匹配。
解决:先只放一个最简单的、确定存在的实例路径,跑一遍确认能减掉;确认机制生效之后再往里加通配符。别一次写十条然后调试。
5.3 锁存器告警突然翻倍
现象:升级方法学版本后,锁存器相关告警从几十条涨到两百多条,逐条看大半是误报。
原因:新方法学默认提高了latch_effort_level相关的严格度,同时treat_latch_as_combinational的默认值也变了。两个变化叠加,导致大量"缺 else 但输出在其他路径完整赋值"的写法被判成锁存器。
解决:先把方法学版本回退,跑一份基线报告存档;再升级,做报告 diff,只处理新增条目。确认新增里确实有误报后,优先调latch_effort_level而不是关check_latch。整个过程中基线文件不能丢,这是唯一的对照物。
5.4 超大设计跑不动,改完参数结果不可信
现象:顶层综合前跑 lint,一次要跑六个多小时,为了赶时间把fast打开、handle_large_expr关掉,结果二十分钟跑完,但报告条数只有原来的三分之一。
原因:fast牺牲的是跨层次分析深度,handle_large_expr关掉之后大规模表达式直接跳过检查。两者都不是"优化",而是"少做事"。条数减少不是误报被过滤,是真问题被漏检。
解决:性能问题优先从范围入手,用report_only_from_one_hierarchy分模块跑,每个模块单独出报告再合并;其次检查是不是handle_large_bus没开导致反复重算。fast只在本地迭代时用,门禁必须用完整模式跑。
5.5 waiver 加了,下次跑又冒出来
现象:手工在报告上加了 waiver,当天结果很干净,第二天回归告警全部回来。
原因:大多数情况是 waiver 文件的路径写成了相对路径,而回归脚本在不同工作目录下启动,文件没被读到。少数情况是规则 ID 不匹配——waiver 里写的是主规则 ID,实际触发的是带后缀的变体,匹配不上。
解决:waiver 文件一律写绝对路径或者基于工程目录的路径,并且在工程文件里显式声明;每次加载后在日志里确认 waiver 条数,条数为零就是没读到。规则 ID 一律从报告里复制,不要手敲。
6. 让规则集活下来:waiver 治理与回归基线
规则参数调到能用的状态只是开始,真正难的是三个月后还能解释清楚每一条 waiver 为什么存在。我的做法是把 waiver 当代码管:一个文件、一个目录、一份清单,任何人新增都走同一条路。
waiver 文件在工程文件里显式声明,路径写死,不要依赖默认搜索:
# 工程文件里的 waiver 声明,路径基于工程根目录 set_option waiverfile ./waivers/lint_base.swl set_option waiverfile ./waivers/lint_tb.swl # 报告里同时体现已豁免条目,便于统计豁免率 set_goal_option report_waived yes分成两个文件是有意为之:lint_base放设计本身的豁免,lint_tb放验证平台的豁免。验证平台代码质量要求和设计不同,混在一起会导致评审时争论不清。report_waived yes必须开,否则你只知道当前告警数,不知道其中多少条是被压掉的——豁免率是衡量规则集健康度的核心指标,长期高于三成说明规则集和设计风格严重脱节,该重新审视参数而不是继续加豁免。
severity 分级用一个表固定下来,别让每条规则的级别散落在各人脑子里。set_message_severity这类参数的作用就是把特定规则的输出级别重定义:
| 规则族 | 默认级别 | 团队设定 | 理由 |
|---|---|---|---|
| 赋值语义(阻塞/非阻塞混用) | Warning | Error | 仿真综合不一致,必须拦 |
| 锁存器推断 | Warning | Error | 时序分析无法覆盖 |
| 位宽截断 | Warning | Warning | 存在合法截断场景 |
| 敏感表遗漏 | Warning | Error | 功能风险高 |
| 风格类(常量选择器等) | Info | Info | 不参与门禁 |
这张表的价值不在分级本身,而在于它把"为什么这条要拦"写下来了。新人看到 Error 会去问,看到表就自己明白了。表格建议跟着工程文件一起进版本库,改分级走评审。
回归基线这块,我的做法是每次方法学或参数变更后跑一份完整报告存档,命名带日期和参数表哈希。日常回归只做两件事:对比告警总数是否漂移超过阈值、对比新增规则 ID 是否出现在基线里。第一件事抓环境问题,第二件事抓真实回归。至于具体每条告警的内容,人工只在新增条目上看,存量不重复看——这是把人力花在增量上。
最后说个具体的验证技巧。参数调完之后,别只看总数,挑三条改动前后的代表性告警,手工追到 RTL 行号,确认工具的判断和你的理解一致。这一步花十分钟,能发现大部分"参数开了但口径理解错了"的问题。我有一次调check_shifted_only,报告条数降得很漂亮,手工追了三条才发现被排除的是整个移位结果检查,不是我以为的被移位操作数——如果不是随手追了一遍,这个问题会一直埋到后端。
从那以后我每次动规则参数,都强制走一遍「存基线 → 改一条 → diff → 手工追三条」的流程,绝不一次改五个参数然后看总数。希望帮到你。
本文还有配套的精品资源,点击获取