SpyGlass lint规则手册与参数调优:从报告降噪到团队基线

发布时间:2026/11/4 9:41:16
SpyGlass lint规则手册与参数调优:从报告降噪到团队基线

简介:面向IC设计与验证工程师的SpyGlass LintRule规则参考手册,对应Synopsys Verification Continuum平台Q-2020.03-SP1版本,专注Verilog/VHDL硬件描述语言的静态检查,帮助使用者在设计早期发现编码风格、时序约束、功耗优化及接口检查等方面的隐患。压缩包共1个PDF文件,约2.04MB,按规则ID逐条编排,每条规则给出描述、示例代码、修改方案与严重性分级,并附规则参数说明(如时钟条件、序列逻辑、总线检查等开关)以及配置方法,方便按项目规范启用或禁用条目、调整阈值与例外。文档同时收录版权、出口管制、免责声明与开源软件许可等条款。目前已有4495人学习,适合需要搭建lint检查流程、编写规则 waiver 或排查告警原因的初中级工程师当作随手查阅的手册使用。

1. 拿到一份三百多页的 spyglass lint 规则手册,先别从头读

第一次翻开 spyglass lint 的规则参考手册,大多数人的第一反应是“这不就是本字典”。三百多页,一百多个参数,从allow_clk_in_condition一路排到use_carry_bit,字母序排得整整齐齐。真按顺序读,读到第三个字母就忘了第一个参数是干什么的。

我的用法是反过来的:先让 lint 跑一遍,拿着报出来的规则 ID 回手册里查,查的时候只看两处——参数默认口径、豁免条件。这份 Q-2020.03-SP1 版的手册真正值钱的地方,不是它罗列了多少条规则,而是它把每个检查器的“口径”都摊开了:同一个check_latch,配latch_effort_level是 1 还是 3,报出来的东西完全是两码事。它对刚接手 RTL 静态检查的人友好,对想给团队定规则基线的人更有用。

2. 手册的骨相:一百多个参数背后是四类检查面

2.1 一条规则条目里,只有两段值得先看

手册里每条规则的排版是固定的:规则 ID、描述、示例代码、解决方案、严重性。新手最容易犯的错是从“描述”开始逐字读,读完五条就困了。实际排查时真正有用的是示例代码和解决方案这两段——示例告诉你这条规则在什么样的写法上会触发,解决方案告诉你改哪儿能消掉。描述那一段往往是写给评审看的,信息密度最低。

规则 ID 的命名有一套隐含规律:多数以字母开头加数字,部分带字母后缀表示变体。同一个数字主干下的不同后缀,检查的是同一类电路结构的不同写法,比如组合环、位宽截断这类。看到后缀不同的两条规则同时报在同一个文件上,先别急着分别改,大概率是同一处代码被两个视角扫到了,改一处两边都消。

严重性字段(Warning / Error / Info)在手册里是静态标注,但在工程里它是可以被覆盖的。这一点很多人没意识到,导致把 Error 当成不可协商的红线,把 Info 当成可以无视的噪音。真实项目里我一般按“这条违例会不会导致功能错”重新分级,而不是照抄手册的默认值。

2.2 参数不是开关,是检查器的口径

把手册里的参数按功能粗分,大致是四类,这个分类决定了你该怎么调它。

第一类是开关型:check_initialization_assignment、check_parameter、check_sequential这种,给 yes/no,决定某个检查器跑不跑。这类参数的坑在于名字相近,check_counter_assignment和check_counter_assignment_turbo是两个独立开关,turbo 版本对计数器识别更激进,报得更多也更容易误报,两个一起开会互相污染报告。

第二类是阈值型:casesize、check_concat_max_width、check_concat_max_width之外的位宽类参数,给一个数字,超过或低于才报。阈值型参数是调优的主战场,也是“为什么同事的工程不报我这里报”的最常见原因。

第三类是豁免型:所有ignore_开头的参数,ignore_genvar、ignore_local_variables、ignore_typedefs、ignore_inout、ignore_macro_to_nonmacro,一长串。这类参数本质是给检查器开天窗,用得好能把第三方 IP 和自动生成代码的噪音压下去,用滥了就是把真问题一起关掉。

第四类是行为型:handle_large_bus、handle_large_expr、handle_shift_op、new_flow_width、strict、fast,这类参数改变的是分析引擎本身走哪条路径,不是简单的开和关。它们对运行时间和内存的影响远大于对报告条数的影响,属于“改之前先看机器扛不扛得住”的一类。

提示:阈值型和行为型参数一定要在项目配置文件里显式写死,不要依赖工具默认值。默认值会随 flow 和版本变化,你今天调好的口径,明天换个环境可能就变了。

2.3 把参数清单倒成一张可检索的表

手册是 PDF,PDF 最不友好的地方是不能按参数名快速跳转比对。我的做法是先把它转成纯文本,抽出所有参数名做成一张对照表,左边参数名右边一句话用途,团队里谁要加参数先查这张表,避免两个人加了两套互相打架的配置。

# 把手册转成保留版式的文本,-layout 很关键,否则参数名和说明会被打散 pdftotext -layout SpyGlass_LintRules_Reference.pdf rules.txt # 参数名基本是小写字母+下划线,单独成行的多半是条目名 grep -nE '^[a-z][a-z0-9_]{4,}$' rules.txt > params_raw.txt # 去重并按字母序排,方便和 prj 文件里的现有配置做差集 sort -u params_raw.txt | awk '{print $2}' | sort > params_all.txt wc -l params_all.txt # 再把你工程里已经用到的参数抽出来,做差集就知道还有哪些没评估过 grep -oE 'set_option[[:space:]]+[a-z_]+' ./proj/*.prj | awk '{print $2}' | sort -u > params_used.txt comm -23 params_all.txt params_used.txt

pdftotext -layout这个参数不能省,不加的话双栏排版会被从上到下串成一行,参数名和说明文字混在一起,后面所有 grep 都失效。comm -23输出的是“存在但没用过”的参数集合,这份清单才是有价值的——它告诉你还有哪些检查器你压根没评估过,而不是哪些已经开了。

这套流程跑完,一份三百页的手册就变成了一张一百多行的清单加一份待评估列表。真正的规则理解,是从这份清单里挑出和你设计风格相关的十几条,逐条去啃示例代码。

3. 从工程到报告:把 lint 目标跑出可定位的违例清单

3.1 工程文件的三层结构:源文件、约束、目标

lint 跑不起来,八成不是规则配错,是工程入口没搭对。一个能长期维护的 lint 工程,我一般拆成三层:源文件清单、约束文件、目标与参数。三层分开的好处是文件列表变动频率最高,参数变动频率最低,混在一个文件里改一次就要全量回归一次。

源文件清单单独放一个.f文件,一行一个路径,支持+incdir+这种编译选项。约束文件用工具自己的约束格式,主要写时钟定义和模块黑盒声明。工程主文件只负责把这两样读进来,然后挂目标、设参数。

# proj/rtl_lint.prj —— lint 工程入口,只放结构和口径,不放文件明细 set_option projectwdir ./work set_option top top_core set_option language_mode mixed set_option enableSV yes # 源文件与约束分别维护,改动互不影响 read_file -type sourcelist ./filelist/rtl.f read_file -type sgdc ./constraint/top.sgdc # 挂目标:RTL 结构类 lint current_goal lint/lint_rtl # 只显式覆盖需要调口径的参数,其余全部走默认 set_option casesize 8 set_option check_concat_max_width 32 set_option handle_large_bus yes run_goal # 两份报告分工:summary 看分布,moresimple 定位到行 write_report summary > ./report/lint_summary.rpt write_report moresimple > ./report/lint_moresimple.rpt

set_option和set_goal_option的区别要拎清楚:前者对全工程生效,后者只对current_goal生效。同一个参数两处都写,实际生效的是更靠近目标的那个。我在项目里遇到过参数写在 prj 顶部、但目标在文件后半段、结果被后面一条set_goal_option覆盖掉的情况,查了半天以为参数没生效,其实是生效了又被改回去了。

注意:不同 flow 下set_option和set_goal_option的优先级、可覆盖范围会有差异,写之前用help查一下当前 flow 的支持列表,别照抄另一套 flow 的脚本。

3.2 报告怎么读:先看 summary 的分布,再看行号

第一次跑完,报告动辄几千条。直接打开 moresimple 报告往下滚,滚到第一百行就失去判断力了。正确顺序是先看 summary:按规则 ID 聚合,看每条规则各报了多少条。排在前三位的规则,通常贡献了 80% 的报告量,这三条处理掉,报告立刻从几千条降到几百条。

降下来之后再打开 moresimple,这时候每一条都能认真看。报告里一般给出规则 ID、文件名、行号,有些还能给到信号名。定位到行之后,回到手册查这条规则的示例代码,对照自己的写法判断是真违例还是误报。误报的处理不是改代码,是加豁免或调阈值,这两件事的后果完全不同,下面第 5 章会细说。

3.3 豁免要落到料号级别,不能整条规则关

豁免的正确粒度是“文件 + 行 + 规则”,并且必须带理由和责任人。整条规则关掉是最省事也最危险的做法,因为下一个人不知道你为什么要关,也不敢开回来。

# waive.tcl —— 每条豁免必须带 comment,否则三个月后没人敢动 waive -rule W18 -file ./rtl/datapath/alu.v -line 142 \ -comment "低 8 位截断由下游饱和模块处理, 责任人 A" waive -rule W164 -file ./rtl/thirdparty/*.v \ -comment "外部 IP 不参与本次风格检查, 责任人 B"

-file支持通配符,第三方 IP 整个目录豁免是合理的;但自研代码用通配符豁免就是给自己挖坑。我一般要求自研目录下的豁免必须精确到行,一条一行,评审时能逐条看。

3.4 批处理与增量:让 lint 进每次提交

交互式点界面跑一遍,只能解决“今天这份报告长什么样”。要它真正起作用,得挂到脚本里做成批处理,每次提交跑增量。

#!/usr/bin/env bash # run_lint.sh —— 批处理入口,退出码直接给 CI 用 set -euo pipefail WORK=${1:-./proj} LOG=${WORK}/report/lint_$(date +%Y%m%d_%H%M%S).log # -batch 无界面执行, -licqueue 在多任务并发时排队等 license spyglass -project "${WORK}/rtl_lint.prj" -batch -licqueue 2>&1 | tee "${LOG}" # 把 Error 级别单独抽出来, 有就返回非零, 拦住合并 grep -E '^\s*Error' "${WORK}/report/lint_summary.rpt" > err_only.txt || true if [ -s err_only.txt ]; then echo "发现 Error 级违例: $(wc -l < err_only.txt) 条" exit 1 fi exit 0

-batch关掉图形界面,-licqueue在并发场景下排队等 license 而不是直接失败——这条在多分支同时提交时非常关键,我最早没加,一到大版本合并就有一半的流水线因为抢不到 license 挂掉,排查了半天以为是脚本写错。退出码的设计也要注意:只把 Error 级别拦在合并前,Warning 允许通过但记录,否则团队会因为一堆风格告警而绕过整条流水线。

4. 三类高频规则的参数调优:位宽、锁存器、计数器

4.1 位宽类:自然位宽和截断宽度是两回事

位宽类规则是 lint 报得最多、也最容易被一刀切关掉的一类。手册里相关参数密集得吓人:check_natural_width_of_multiplication、check_static_natural_width、check_natural_width_for_static、check_shifted_width、check_shifted_only、check_sign_extend、concat_width_nf、new_flow_width、check_unsign_overflow。

这一堆参数其实在回答同一个问题:一个表达式算出来的“自然位宽”和它被赋值的目标位宽不匹配时,报不报。自然位宽是乘法结果 2W、加法结果 W+1 这种推导出来的宽度,目标位宽是你声明的位宽。两者不等就有截断或扩位,截断可能是设计意图(饱和处理),也可能是笔误。

// alu.v —— 一段同时踩中位宽和锁存器两类规则的写法 module alu #(parameter W = 12) ( input wire [W-1:0] a, input wire [W-1:0] b, input wire sel, output reg [7:0] y ); reg [15:0] tmp; always @(*) begin tmp = a * b; // 自然位宽 2W=24, 赋给 16 位必然截断 if (sel) // 缺 else, sel=0 时 y 保持 -> 推断锁存器 y = tmp[7:0]; end endmodule

这段代码里tmp = a * b会触发乘法自然位宽检查,if (sel)缺分支会触发锁存器检查,y = tmp[7:0]又可能触发截断检查。三条规则从一个模块里一起冒出来,这是新手最慌的场景。

调参思路是分设计域走:数据通路(datapath)里的截断大多是设计意图,ignore_mult_and_div打开,把乘除法纳入豁免;控制通路(control path)里的位宽不匹配基本都是笔误,strict打开,一个不漏。手册里还有个datapath_or_control参数,作用就是让工具自己去判这段代码属于哪一类,我一般先让它自判一轮,把明显判错的模块记下来,再改成手工分组。

new_flow_width这个参数值得单独说。它切换的是位宽分析走新流程还是老流程,两者对复杂表达式的推导能力不一样,报出来的条数可能差好几倍。切之前一定要留一份旧报告做 diff,别看条数降了就以为问题解决了,很可能只是新流程对某类表达式不再深挖。

4.2 锁存器类:档位决定查得多深

锁存器相关的参数是一组:check_latch、latch_effort_level、disable_latch_crossing、treat_latch_as_combinational、reportLibLatch。

treat_latch_as_combinational这个参数我第一次看到时理解错了,以为是把锁存器当组合逻辑报,实际是让分析把锁存器按组合路径处理,用于排查锁存器后面接的那条路径上的时序和环问题。这个开关打开后报告条数会明显上升,因为它把原本被截断的路径接上了。

# latch.tcl —— 锁存器检查的三档配置,按项目成熟度选 # 档位一:新项目,宽进严出,只要疑似就报 set_option check_latch yes set_option latch_effort_level 3 set_option disable_latch_crossing no # 档位二:稳定项目,只报跨时钟域的锁存器 # set_option latch_effort_level 1 # set_option disable_latch_crossing yes

latch_effort_level的数字档位越大,工具对隐式锁存的推导越激进,包括那些藏在 case 缺 default、函数返回值未全覆盖里的锁存器。档位开到 3 时,误报会明显增多,典型误报是带复位优先判断的 always 块。所以新项目用 3 先把隐患扫一遍,稳定后降到 1,只保留高置信度的。

disable_latch_crossing控制的是跨层次边界的锁存器传播。层次化设计里锁存器藏在子模块内部、端口上体现不出来,这个开关关了之后,跨模块的锁存器路径就断在边界上。做顶层集成检查时必须打开,做单模块检查时可以关掉省时间。

4.3 计数器与赋值类:两个变体不要同时开

check_counter_assignment和check_counter_assignment_turbo是同一族检查的两个精度。普通版按常规模式识别计数器赋值(自增、自减、带条件的加减),turbo 版对复杂条件、多路赋值、非标准写法的识别更激进。两个都打开的结果是同一个计数器被报两次,报告里出现成对的行,处理起来非常烦。

# counter.tcl —— 二选一, 不要并列打开 set_option check_counter_assignment yes set_option check_counter_assignment_turbo no set_option ignore_counter_with_same_width yes # 位宽一致的计数器不报 set_option ignore_nonstatic_counter yes # 非静态边界的计数器不报 # 配合阻塞/非阻塞检查一起用, 计数器赋值方式错了会同时踩两条 set_option checkblocking yes set_option checknonblocking yes set_option check_temporary_flop yes

ignore_counter_with_same_width是我最喜欢的一个参数。它让工具跳过那些赋值两端位宽一致的计数器,这类计数器基本不会出问题,跳过能砍掉一大片报告。ignore_nonstatic_counter处理的是边界不固定的计数器,这类计数器的静态分析本来就不准,强行报出来只会增加噪音。

check_temporary_flop检查的是临时寄存器,也就是那种为了打拍临时声明、后面没再用到的 reg。这类寄存器在综合时会被优化掉,但仿真波形里还在,容易误导调试。打开它能提前发现一批“仿真对、综合不对”的隐患。

4.4 规模阈值:报不报取决于你设的线

阈值类参数的调法和其他参数不同,它们没有对错,只有合不合适。

参数作用对象我的常用值调大/调小的后果
casesizecase 语句分支数8调小会漏掉大 case 里的重复分支
check_concat_max_width拼接表达式宽度32调大会让宽拼接逃过检查
handle_large_bus超宽总线分析yes关了之后宽总线上的位宽问题全漏
handle_large_expr复杂表达式分析yes关了省内存,但表达式类误报会上升
limit_task_function_scopetask/function 分析深度视规模调小会让深层调用里的问题漏报

casesize设成 8 的意思是分支数超过 8 的 case 语句不参与某些检查,因为大 case 往往是从寄存器表自动生成的,逐分支比对既慢又容易误报。这个值设太小会漏掉手写的大 case 里的真问题,我一般按项目里手写 case 的最大分支数往上加 2。

handle_large_bus和handle_large_expr这两个关掉能显著降低内存占用,代价是宽总线和复杂表达式上的检查全部跳过。有过一次血泪经验:为了在一台内存吃紧的机器上跑通,把这两个都关了,结果一个 64 位总线的位宽截断问题溜到了后端才被发现。从那以后这两个参数我都不再关,宁可分批跑。

5. 避坑记录:五条把 lint 报告跑成噪音的常见操作

5.1 一上来全规则全开,报告直接失控

现象:第一次在新工程上跑 lint,为了“不留死角”把所有规则集都挂上,跑完出来两万多条违例,报告打开就卡,团队看了两天没人愿意接手。

原因:规则集是按通用设计风格写的,不是按你这个项目的编码规范写的。里面大量规则针对的是特定命名习惯、特定模块结构,你的项目根本不遵循那套,全开等于让工具拿别人的尺子量你的代码。

解决:分三轮挂。第一轮只挂和功能性错误直接相关的(锁存器、组合环、位宽截断、多驱动),目标是先消灭真问题;第二轮挂和可综合性相关的;第三轮才挂风格类。每轮之间把上一轮的豁免整理成文件,作为下一轮的基线。两万条降到两百条,靠的不是改代码,是分批。

5.2ignore_*参数顺手就加,漏检了都不知道

现象:某个模块报了几十条ignore_local_variables相关的违例,嫌烦直接把这个参数打开,报告立刻清净。三个月后这个模块的一个局部变量被优化掉导致功能异常,回头查发现当初那批违例里就有它。

原因:ignore_类参数不是“降噪”,是“关检查器”。它的作用范围是全局的,不是你以为的某个文件。加在工程级配置里,所有模块的局部变量检查一起没了。

解决:能用 waive 解决的不用ignore_。waive 有文件、行号和理由,三个月后还能追溯;ignore_只有一行配置,没人知道当初为什么加。只有在整个目录都是自动生成代码或第三方 IP 时,才用ignore_或目录级 waive。这条我踩过一次之后,现在评审任何配置变更都先搜一遍ignore_关键字。

5.3 severity 覆盖写错位置,改了跟没改一样

现象:某条规则在项目里被判定为“可以接受但不能新增”,于是把它的严重性从 Error 降到 Warning。改完重跑,报告里还是 Error,反复确认参数名没写错。

原因:severity 覆盖有位置优先级。写在 prj 顶部、写在 goal 之后、写在 waive 文件里,三处生效顺序不同,后生效的覆盖先生效的。如果 waive 文件在 goal 之后被读入且里面又写了默认 severity,prj 里的覆盖就被打回去了。

解决:set_message_severity统一集中在一个文件里,放在所有read_file之后、run_goal之前。改完用write_report summary看一眼实际生效的分级,别只看配置文件。有条件的在脚本里加一步校验:把预期分级和报告里的实际分级做 diff,不一致就报错退出。

# severity.tcl —— 集中管理,放在 read_file 之后、run_goal 之前 set_message_severity -rule W164 -severity Warning set_message_severity -rule W18 -severity Error # 改完必须重跑并核对 summary, 配置写了不等于生效了

5.4 换了 flow 或版本,参数静默失效

现象:本地跑得好好的配置,换到另一台机器或者升级了 flow 之后,某些参数像是完全没起作用,但脚本没报任何错,工具照常跑完出报告。

原因:不同 flow 支持的参数集合不完全一致,不支持的参数有的会报 warning,有的直接忽略。手册对应的是某一个具体版本(这份是 Q-2020.03-SP1),和别的版本比对时参数名可能已经改了拼写或者被合并。

解决:每次换环境,先用工具的help或属性查询确认关键参数在当前 flow 下是否被识别,把结果存档。批量校验可以写个脚本,把所有用到的参数名逐个查一遍,输出不支持列表。

# 检查 prj 里用到的参数在当前环境下是否都被识别 grep -oE 'set_(goal_)?option[[:space:]]+[a-z_]+' ./proj/*.prj \ | awk '{print $2}' | sort -u > used.txt while read -r p; do # help 输出里能查到该参数说明即视为受支持 spyglass -batch -cmd "help ${p}" >/dev/null 2>&1 || echo "未识别: ${p}" done < used.txt

5.5 黑盒模块没声明,报告里全是越界违例

现象:层次化设计里某个子模块以网表形式集成,lint 跑出来大量“未驱动信号”“端口未连接”的违例,全指向这个子模块的边界。

原因:工具看不到子模块内部实现,把所有输入端口当成悬空、所有输出当成无驱动。这不是设计问题,是分析边界没划清。

解决:在约束文件里把这些模块声明为黑盒,同时打开report_blackbox_inst确认黑盒列表正确。还要注意considerInoutAsOutput、ignore_inout、ignore_in_ports这几个参数在黑盒边界上的行为,inout 端口在默认处理下会同时被当成输入和输出,导致重复报。

6. 把手册变成团队基线:规则分层与自检注入

手册查得再熟,落到团队协作上还是得有一份写死的基线,否则每个人调出来的口径都不一样,报告没法横向比。

我的做法是把规则分三层。第一层是门禁层,只放那些一旦违反必然导致功能错误的规则——锁存器、组合环、多驱动、位宽截断,severity 锁死在 Error,任何豁免都需要两人评审。第二层是观察层,可综合性、复位策略、跨时钟域相关的规则,severity 定为 Warning,允许通过但要统计趋势,连续三个版本上升就介入。第三层是参考层,命名、注释、代码风格类,只出报告不拦流程,谁想清理谁去清。

层级典型参数severity豁免策略
门禁层check_latch、check_unsign_overflow、check_complete_designError精确到行,双人评审
观察层checkblocking、checksyncreset、check_temporary_flopWarning目录级,需记录理由
参考层check_case_type、report_semicolon、命名类Info不强制处理

分层定完之后,最关键的一步是注入自检。配置改完不能只看“报告条数变少了”,得证明检查器真的还在工作。我会在 RTL 目录之外单独放一个selftest/目录,里面故意写几个必然违规的小模块,每个模块对应一条门禁规则,然后让流水线每次跑 lint 时把这个目录一起读进去。

// selftest/bad_latch.v —— 故意缺 else, 用来验证 check_latch 没被关掉 module bad_latch (input wire en, input wire d, output reg q); always @(*) begin if (en) q = d; // 预期触发锁存器规则, 若没报说明检查器失效 end endmodule

流水线里的判断逻辑很简单:selftest目录下每个文件预期命中的规则 ID 写在一张表里,跑完之后比对实际报告,少一条就说明对应的检查器被谁关掉了,直接失败。这招在一次配置合并事故里救过我——有人为了赶进度把一整组check_*参数设成了 no,报告条数断崖式下降还被当成“优化成果”,是自检模块把这事捅出来的。

从那以后我每次动 lint 配置,不管改的是一个参数还是一整组规则,都强制走一遍“改配置 → 跑自检 → 比对预期规则 → 再看真实报告”这个顺序,一步不省。配置这种东西,看不见摸不着,唯一的后悔药就是自检。希望帮到你。

本文还有配套的精品资源,点击获取