上周三下午,一个刚从后端转岗做 DFT 的同事把厂商发来的一个压缩包丢到共享盘上,转头问我:"Tessent_StdcellLib 这堆东西,到底哪些要读进 Tessent,哪些可以放着不管?"这个问题我前后回答过不下十遍。标准单元库(std cell library)是数字芯片的地基,但 DFT 流程对它的诉求,跟综合、时序分析完全不在一个频道上——综合关心驱动能力和延迟,Tessent 关心的是这颗触发器能不能被打断、这个门控单元的使能在移位阶段能不能被拉住、这个 tie cell 究竟输出高还是低。名字里的 Tessent_StdcellLib 听起来很直白,就是"面向 Tessent 的标准单元库",可落到真实工程里,它往往指三件不同的事,处理手法差得很远。
这篇内容写给三类人:第一次把厂商库接进 Tessent 的 DFT 新人、要同时维护好几个工艺节点库的库管理员、以及被"扫描链插不进去""覆盖率整块掉点"折磨过的验证工程师。我会先讲清楚 Tessent_StdcellLib 的三种典型含义,再说明一份库要满足哪些条件才算真正"能被 ATPG 读懂",然后给出一套可以直接照做的环境搭建、属性建模、报错回追和长期维护方法。所有命令和字段写法我会尽量给出可复制的片段,同时标注哪些地方跟工具版本强相关,避免你照抄之后在另一个版本上卡住。
1. Tessent_StdcellLib 的三种含义,先分清再动手
拿到这个名字,第一件事不是打开文件,而是判断你手上的目录属于哪一类。我见过太多次有人把厂商原始库直接塞进流程,结果读进去报一堆语法警告,然后花两天去查工具问题——其实方向从一开始就偏了。
1.1 含义一:厂商交付的、已经带 DFT 视图的标准单元库包
多数成熟工艺的库厂商,除了给综合用的.lib、给布局用的.lef、给 LVS 用的.cdl、给仿真用的.v,还会额外提供一份面向测试的视图。这份视图里最核心的东西是 Liberty 里的test_cell组,它告诉 Tessent 哪个引脚是扫描输入、哪个是扫描使能、哪个是扫描输出。有些厂商把它单独放在一个dft子目录,有些直接内嵌在功能.lib里。
如果你手上的目录里同时能翻到.lib、.lef、.gds、.v、.cdl,基本可以确定这是含义一。这类库原则上不该手改,改了就脱离了厂商的可追溯性,后面 tapeout 出问题会很难解释。
1.2 含义二:项目内部维护的 Tessent 库封装目录
第二种情况更常见:公司内部有一个目录就叫Tessent_StdcellLib,里面装的不是原始库,而是转换脚本、补丁文件、编译产物和一份说明文档。为什么要有这么一层?因为厂商给的库经常不完整——某些低功耗单元没建模、某些 mux 没有 DFT 属性、某些工艺角只给了典型角。库管理员会写一套 Tcl 或者 Perl 脚本,把厂商库读进来,补齐缺失的属性,再输出一份"Tessent 友好"的库。
这一层最大的价值是可复现。厂商库升级了,跑一遍脚本就能重新生成,不需要人工回忆上次改了哪几行。如果你的团队还没有这一层,强烈建议补上,哪怕只是几个 shell 脚本加一份 README。
1.3 用一张文件清单快速判定你手上的是哪一种
不想逐层翻目录的话,对着下面这张表扫一遍就能定性。表里的"缺失后果"一栏是我在实际项目里踩过或见过别人踩过的,不是理论推演。
| 文件/目录特征 | 典型归属 | 缺失后果 |
|---|---|---|
.lib带test_cell组 | 厂商完整库 | 扫描链插入阶段找不到扫描等价单元 |
仅有功能.lib,无测试视图 | 厂商基础库 | 需要人工补 DFT 属性 |
*.tcl+*.patch+README | 内部封装层 | 换库时无法复现历史改动 |
编译缓存目录(libc之类) | 工具生成物 | 不该进版本库,会污染 diff |
只有.db(二进制时序库) | 综合侧产物 | Tessent 读不了,必须拿回 Liberty |
提示:编译缓存目录不要提交到 Git。它是工具生成的二进制,跟工具版本、绝对路径强绑定,换台机器就跑不起来,还会把仓库撑大。
2. 一份库要被 Tessent 正确理解,缺哪几样东西
判断完含义,接下来是硬功夫:这份库到底缺什么。我习惯按"功能视图、扫描属性、扫描等价单元、特殊单元、低功耗单元"五个层次依次过一遍,顺序不能乱,因为后面的层次依赖前面的。
2.1 功能视图:别让工具把标准单元当黑盒
Tessent 要做 DRC 和 ATPG,必须知道每个单元的逻辑行为。如果某个单元没有功能模型,工具会把它当黑盒,随之而来的是一串"输出不可控""无法传播"的违规。很多新人以为.lib就够了,其实不然——Liberty 主要描述时序和功耗,逻辑功能要靠 Verilog 行为模型来补。
标准做法是把厂商提供的仿真模型(通常是.v或者带 UDP 定义的库文件)读进来,或者在库建模阶段为每个单元生成一份简化的功能描述。我一般会重点检查这几类:复合逻辑门(比如 AOI、OAI)、带复位或置位的触发器、时钟门控单元、以及各种 tie 单元。这几类一旦缺模型,DRC 的报错会非常密集,而且互相遮掩,很难一眼看出根因。
2.2 扫描属性:test_cell组决定扫描链能不能拼出来
这是整份库的灵魂。下面是一段简化的 Liberty 节选,展示带扫描端口的触发器该怎么描述。注意这是节选,实际字段比这多,具体写法以厂商模板为准:
cell (DFSQ) { area : 10.5 ; ff (IQ, IQN) { next_state : "D" ; clocked_on : "CK" ; } pin (D) { direction : input ; } pin (CK) { direction : input ; clock : true ; } pin (SI) { direction : input ; signal_type : test_scan_in ; } pin (SE) { direction : input ; signal_type : test_scan_enable ; } pin (Q) { direction : output ; function : "IQ" ; signal_type : test_scan_out ; } }关键就在于signal_type这几个取值。Tessent 读库的时候靠它识别扫描端口,识别不到,工具就会认为这颗触发器不可扫描,转头去找"扫描等价单元"——通常是一个 2:1 mux 加一颗普通触发器。找不到,扫描链就断在这里。
我建议在库评估阶段用最简单的命令先数一遍:
grep -c "^cell (" stdcell.lib grep -c "test_scan_in" stdcell.lib grep -c "test_scan_enable" stdcell.lib第一个数字和第二个数字的差距,基本就等于"需要插 mux 的触发器数量"。差距过大时,要么是厂商库没给全,要么是你们选的触发器型号本身就不带扫描端口,这两种情况的处理方式完全不同。
2.3 扫描等价单元:为什么库里必须有 mux,以及 mux 放在哪
很多人以为扫描触发器是"必须"的,实际上 Tessent 允许用普通触发器加 mux 来构造扫描单元,这就是所谓的扫描等价单元。这样做的代价是面积和延迟,收益是库的选择面更宽。
这里有个容易忽略的细节:mux 必须出现在库文件里,并且带有能标识它"可以作为扫描 mux"的属性,而不是靠工具去猜逻辑。如果库里只有 mux 的功能模型、没有测试属性,工具可能仍然识别,但识别结果不稳定,不同版本表现可能不一样。我的习惯是明确建模,不依赖推断。
另外,插入位置也有讲究。Tessent 倾向于在触发器前紧挨着插 mux,这样对时序影响最小,但会改变局部布线密度。如果你们的设计在扫描插入后经常出现局部拥塞,可以评估一下是不是插入策略导致的,这时候调整约束比改库更有效。
2.4 时钟门控、tie、三态 pad:三个最容易被忽略的建模点
时钟门控单元(ICG)是低功耗设计的标配,也是 DFT 踩坑的重灾区。移位阶段时钟必须持续翻转,这就要求门控单元的使能在移位时被强制打开。如果库里没有把使能引脚建模成工具能识别和控制的形态,Tessent 就没法在移位模式下拉住它,结果就是扫描链一动不动,波形上看时钟根本没有脉冲。
tie 单元的问题更隐蔽。它输出恒定值,工具必须知道这个值才能正确判断某条路径是不是常量。如果 tie 单元被当黑盒,工具会保守地认为该点可控,于是漏掉本该报出的违规,覆盖率也可能因此出现虚高。三态 pad 同理,双向端口在测试模式下必须被正确置成输入或输出,否则 DRC 会报不可控。
这三类单元我建议单独建立一份清单,每次换库都拿这份清单去核对,比漫无目的地扫.lib快得多。
2.5 多电压域下 isolation、level shifter、retention 的测试态处理
设计一旦上多电压域,库里就会多出隔离单元、电平转换单元和保持单元。这三类在测试模式下的行为必须明确:隔离单元在测试时要放行还是钳位、电平转换单元在测试时是否透明、保持单元在移位时是否保持。
这些东西光看 Liberty 是不够的,通常还要结合电源意图文件一起看。我的经验是把"测试模式下的电源状态"单独列一张表,跟库里的单元逐一对上,确认每个单元在测试态都有确定的行为。这一块一旦出错,往往是硅后才能发现,代价极高。
3. 从安装包到可用环境:把 Tessent 和库文件摆到正确的位置
库文件准备得再好,环境不对也白搭。这一节讲的是把工具和库摆到正确位置的具体做法,以及为什么某些看起来省事的做法后患无穷。
3.1 安装包形态与安装后的自检清单
从官方渠道拿到安装包之后,通常有图形化和静默两种安装方式。图形化适合个人工作站,静默安装适合服务器批量部署。这里不展开具体参数,因为不同年份的安装包形式差别不小,以随包附带的安装说明为准。
安装完别急着跑流程,先过一遍自检清单。下面这几项我每次都做,能省下大量"为什么工具起不来"的排查时间。
| 检查项 | 期望结果 | 不达标时的常见原因 |
|---|---|---|
| 命令行能启动 | 正常输出工具版本号 | 环境变量 PATH 没配 |
| 能进入交互模式 | 不报许可证缺失 | 许可证变量未设置或指向错误 |
| 库文件可读 | 读库命令返回正常,无 error | 路径权限或文件损坏 |
| 编译缓存生成 | 工作目录下出现编译库目录 | 工作目录只读或磁盘空间不足 |
| 版本匹配 | 工具版本不低于库语法要求 | 库用了新版语法,工具太老 |
后面两项经常被忽略。尤其是版本匹配,很多"库读不进去"的报错本质上是新版 Liberty 语法撞上旧版工具,这类问题改库没用,只能换工具或换库。
3.2 库目录怎么组织,为什么不要直接引用厂商原始路径
我见过最糟糕的做法,是流程脚本里直接写厂商解压目录的绝对路径。这么做的后果是:厂商升级一次库,所有脚本的路径全要改;同事在自己的机器上复现,路径对不上;半年后有人清理共享盘,整个流程崩掉。
比较稳的组织方式是给每个库建一个带版本号的目录,流程脚本只引用一个统一的软链接或环境变量。目录结构大致长这样:
lib/ stdcell_tt_1v0_25c_r3/ liberty/ verilog/ lef/ dft_patch/ MANIFEST.md stdcell_tt_1v0_25c -> stdcell_tt_1v0_25c_r3换库的时候只改软链接的指向,流程脚本一行都不用动。MANIFEST.md里记清楚这个版本相对上一版改了什么、什么时候改的、谁改的,出问题时能快速回溯。
3.3 编译库缓存与"库、工具、项目"三者的版本绑定
Tessent 读 Liberty 之后会在工作目录下生成编译缓存,后续运行直接复用,速度会快很多。但这带来一个隐患:缓存是跟工具版本绑定的。工具升级之后用旧缓存,可能报奇怪的错误,或者行为跟预期不一致。
我的做法是把缓存目录按"库版本 + 工具版本"命名,并在启动脚本里做一次校验:
CACHE_DIR=".libcache_${LIB_VER}_${TOOL_VER}" if [ ! -d "$CACHE_DIR" ]; then echo "首次使用该组合,需要重新编译库" fi别小看这一行判断,它能避免至少一类"昨天还好好的,今天怎么就不对了"的问题。版本组合这块,宁可多花几分钟重新编译,也不要赌缓存能用。
4. DFT 属性加在库级还是实例级:这是一个架构决定
补属性这一步,做法有很多种,但最关键的决策只有一个:属性加在库里,还是加在网表实例上。这个决定看起来是细节,实际上影响整个流程的可维护性。
4.1 库级建模的收益与代价
库级加属性的最大好处是"一次建模,全设计生效"。ATPG 是针对整颗芯片所有实例工作的,库级的属性对所有实例自动适用,包括后续新插入的单元。而且库一旦建好,多个项目可以共用,边际成本递减。
代价在于库变成了共享资产,改动需要走评审。曾经有同事为了赶进度,直接在库里改了一个时钟门控单元的属性,结果另一个项目复用了同一份库,测试模式下的行为全变了。所以我现在的规矩是:库改动必须记录、必须回归,不允许"临时改一下先跑通"。
4.2 用 Tcl 补属性的典型场景和写法
有些属性确实不适合放在库里,比如某个特定实例要特殊处理。这种情况用 Tcl 在流程脚本里补更合适。下面是一段骨架,具体命令名在不同版本里略有差别,以你们所用版本的命令手册为准:
# 读取标准单元库与设计网表 read_cell_library ./lib/stdcell_tt.lib read_cell_library ./lib/icg_tt.lib read_verilog ./netlist/top_scan.v # 声明测试模式下需要用到的基本信号 add_scan_mode shift_mode -scan_enable scan_en -scan_clock clk # 运行设计规则检查,先看库层面的问题 check_design_rules顺序上我坚持先读库、再读网表、最后加约束。反过来的话,工具在解析网表时拿不到单元的完整信息,会先报一轮"未知单元"的警告,把真正的问题淹掉。
4.3 黑盒、dont_use 与"库里有但设计里没用"的单元
库里通常会包含大量设计里根本没用的单元,比如各种驱动强度的缓冲器、各种变体触发器。这些单元留着不影响正确性,但会拖慢库解析速度。如果库里单元数量在四位数以上,可以考虑裁剪出一份项目专用子集。
dont_use的处理要小心。有些单元被标了dont_use,扫描插入时工具就不会选它,如果恰好某个 mux 被标了,扫描链就插不出来。遇到这种情况,先确认是厂商标错了还是确实不该用,别直接删掉标记了事。
黑盒单元的处理更直接:如果某类单元在测试模式下确实不需要被分析(比如某些模拟模块的接口单元),可以显式声明为黑盒,避免它污染 DRC 结果。但声明黑盒等于放弃对它的检查,这个口子要开得克制。
4.4 建模完必做的三项自检
库改完之后,至少跑这三项再往下走。第一项是库解析自检,确认没有 error 级别的报错,警告逐条看一眼。第二项是端口识别自检,抽查几个典型触发器,确认扫描端口被正确识别。第三项是小规模试插,用一个几十个触发器的测试模块跑一遍扫描链插入,看能不能拼出完整链。
这三项加起来通常不到十分钟,但能把绝大多数库层面的问题挡在正式流程之前。跳过它们直接跑全芯片,出问题之后定位成本要高一个数量级。
5. 报错往回追:从 DRC 清单定位到库文件的那一行
哪怕前面都做对了,还是会有问题。这一节讲的是出问题之后怎么往回追。核心思路是:不要一上来就改东西,先按症状分类,再顺着链路往回走。
5.1 症状、根因、动作对照表
下面这张表是这些年攒下来的,覆盖了八成以上的常见情况。表里的"动作"一栏我刻意写成可执行的操作,不是"检查一下"这种空话。
| 症状或报错 | 常见根因 | 处理动作 |
|---|---|---|
| 读库时 parse error | Liberty 语法超出工具版本支持 | 换用厂商低版本语法库,或升级工具,不要手改语法 |
| 找不到扫描等价单元 | 库里既无带 SI/SE 的触发器,也无可用 mux | 补 mux 的测试属性,确认 mux 未被 dont_use |
| 某引脚报不可控 | 该引脚由 tie 或 ICG 驱动,单元被当黑盒 | 确认对应单元有功能模型 |
| 某模块覆盖率整块为 0 | 该模块用的特殊单元未建模 | 单独补属性后重跑该模块 |
| 移位阶段波形不翻转 | 门控时钟在移位时未打开 | 检查 ICG 使能的识别与测试态约束 |
| 报重复定义的单元 | 同一单元被多个库文件重复定义 | 合并库或用库优先级明确覆盖关系 |
重复定义这一条特别值得说。项目里往往同时读入标准单元库、IO 库、存储器库、IP 库,不同库之间偶尔会出现同名单元。工具的行为取决于读入顺序,而读入顺序常常写在脚本深处,没人记得。遇到诡异的行为差异,先查库的读入顺序。
5.2 一次完整的排查链路:ICG 使能引脚被建模成内部节点
说个真实案例。当时的情况是扫描链能拼出来,DRC 也基本干净,但移位阶段仿真出来发现某一段链的数据不往前走。工具报告里没有任何显式错误,只有几条低优先级的警告,很容易被忽略。
第一步,确认时钟。抓了门控时钟的输入端和输出端波形,输入端有脉冲,输出端是平的,说明门控没打开。第二步,找使能。使能信号来自一个控制寄存器的输出,上电复位后是默认值。第三步,看测试模式约束。约束里确实有拉高该使能的操作,但作用于寄存器实例的端口上。第四步,追到库。翻出 ICG 单元的 Liberty,发现使能引脚被建模成了一个内部节点,外部端口映射到的是另一个名称。约束写在外部端口上,根本没生效。
根因是库建模时引脚命名跟约束里用的名字不一致。修法有两个:改库里的引脚命名,或者在约束里用工具实际识别的名字。我们选了前者,因为库命名应该跟厂商文档保持一致,约束里写"库里的真实名字"只是权宜之计。
这个案例的教训是:没有报错不代表没有问题。低优先级警告里经常藏着关键信息,尤其是跟端口识别、属性缺失相关的警告,值得逐条过一遍。
5.3 覆盖率掉点,先怀疑库还是先怀疑约束
覆盖率莫名其妙掉了几个点,大家第一反应通常是查激励和约束。我的经验是先把库排除掉,因为库的问题往往表现为"整块区域完全没有覆盖",而约束问题通常表现为"某个功能点没被激活",两者的分布形态很好区分。
判断方法是按模块统计覆盖率。如果某个模块覆盖率接近零,而且该模块用的单元类型比较特殊,八成是库的问题。如果各个模块覆盖率都还行,只是某些特定故障类型没覆盖,那更可能是约束或激励的问题。
还有一种情况是覆盖率虚高:应该报的违规没报出来,分数很好看但实际有风险。这种通常在 tie 单元或黑盒单元没建模时出现。所以看覆盖率不能只看数字,还要看违规清单是否合理。
6. 库的长期维护:多节点、多版本下的工程化做法
一次性把事情做对不难,难的是半年后换库、换工具、换人之后还能做对。这一节讲的是把前面这些工作沉淀成机制。
6.1 版本命名、校验和与变更记录
版本命名我建议包含工艺角、电压、温度、修订号四段信息,比如tt_1v0_25c_r3。这样从目录名就能判断适用场景,不用打开文件确认。每份库在入库时算一遍校验和,记在清单里,下游使用时可以做一次比对,防止文件在传输过程中损坏或被误改。
变更记录不要求写得多正式,但至少要回答三个问题:改了什么单元、为什么改、影响哪些项目。我见过太多"这个属性为什么是这么写的"没人知道的情况,最后只能靠猜。
6.2 换库后的回归清单
换库是高风险操作,必须走回归。回归清单不用很长,但每一项都要有明确的通过标准:
- 库解析无 error,警告数量与上一版对比无异常增长
- 典型触发器的扫描端口识别结果与上一版一致
- 小规模测试模块的扫描链插入成功,链上单元数量符合预期
- 设计规则检查的违规数量与上一版对比,新增项逐条确认
- 覆盖率基线对比,掉点超过阈值就停下来查
最后一项是关键。"新增的违规逐条确认"这句话的意思是,不允许出现"反正多了三条,应该没事"这种判断。每一条新增违规都要有明确结论:是库变化引起的合理变化,还是引入了新问题。
6.3 一小段自动化脚本省下的人力
前面这些检查,手工做一遍大概要半小时到一小时,而且容易漏。写个脚本把这些检查串起来,成本很低:
#!/bin/bash set -e LIB_DIR=${1:?请传入库目录} echo "== 库文件数量统计 ==" grep -c "^cell (" "$LIB_DIR"/liberty/*.lib echo "== 扫描端口统计 ==" grep -c "test_scan_in" "$LIB_DIR"/liberty/*.lib echo "== 校验和 ==" find "$LIB_DIR" -type f -name "*.lib" -exec md5sum {} \;这个脚本很粗糙,但它能在一分钟内给出三个关键数字,跟上一版的记录一比就知道有没有异常。真正有价值的不是脚本本身,而是它逼着你把"这一版和上一版差在哪"变成一个可以自动回答的问题。
我在实际操作中的体会是,标准单元库这块的工作,技术难度其实并不算高,难的是保持一致性——同一个属性今天这么写、三个月后还是这么写,换个人接手也是这么写。真正让项目出事的,往往不是某个复杂的建模技巧,而是一个被悄悄改掉的引脚名、一份没走回归的新库、一个以为"应该没事"的新增违规。把检查做成习惯,比把技巧学全更重要。另外分享一个小技巧:每次改库之前,先在版本库里打一个标签,改完跑完回归再合并,这样任何时候都能退回去对比,比事后靠记忆回忆改了什么靠谱得多。