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 把参数清单倒成一张可检索的表手册是 PDFPDF 最不友好的地方是不能按参数名快速跳转比对。我的做法是先把它转成纯文本抽出所有参数名做成一张对照表左边参数名右边一句话用途团队里谁要加参数先查这张表避免两个人加了两套互相打架的配置。# 把手册转成保留版式的文本-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.txtpdftotext -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.rptset_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 21 | 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、加法结果 W1 这种推导出来的宽度目标位宽是你声明的位宽。两者不等就有截断或扩位截断可能是设计意图饱和处理也可能是笔误。// 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; // 自然位宽 2W24, 赋给 16 位必然截断 if (sel) // 缺 else, sel0 时 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 yeslatch_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 yesignore_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 之后被读入且里面又写了默认 severityprj 里的覆盖就被打回去了。解决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 21 || echo 未识别: ${p} done used.txt5.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 配置不管改的是一个参数还是一整组规则都强制走一遍“改配置 → 跑自检 → 比对预期规则 → 再看真实报告”这个顺序一步不省。配置这种东西看不见摸不着唯一的后悔药就是自检。希望帮到你。本文还有配套的精品资源点击获取