Innovus中如何一次性抓取module下所有寄存器clock pin

发布时间:2026/10/8 17:59:58
Innovus中如何一次性抓取module下所有寄存器clock pin
1. 为什么捞全一个 module 的 reg clock pin这件事值得单独拎出来说做数字后端的人大概都有过这种经历跑完 place 或者 CTS 之后想确认某个 module 里所有寄存器的时钟引脚到底连到了哪根 clock net 上结果打开 Innovus 的 GUI 一个个点点到手酸还容易漏。更麻烦的是如果这个 module 是层次化设计里的一个子模块寄存器可能散落在好几层里光靠眼睛看根本捞不全。这个需求听起来很小但实际工作中出现的频率极高。比如做 clock tree 综合之前你需要知道某个 module 下所有 sink pin 的分布情况才能合理规划 CTS 的约束比如做 power 分析时你要确认某个 module 里寄存器的 clock pin 是不是都挂在同一根 net 上避免出现意外的 clock domain 交叉再比如做 ECO 的时候你要快速定位某个 module 下所有受影响的寄存器时钟端才能评估改动范围。关键词里提到的module、reg、clock pin、Innovus这四个词基本就把场景锁死了在 Innovus 这个数字后端实现工具里针对一个层次化 module把所有寄存器的 clock pin 一次性抓出来。注意这里的关键词是一次捞全不是找到几个也不是大概看看而是要求结果完整、可复现、能直接用于后续脚本处理。我见过太多人在这件事上走弯路。有人用 GUI 的 highlight 功能手动选有人写了个半吊子的 Tcl 脚本结果漏掉了层次边界上的寄存器还有人干脆把整个 design 的 clock pin 都 dump 出来再拿 grep 过滤效率低不说还容易因为命名规则不一致而漏掉目标。其实 Innovus 提供了足够强大的命令组合只要理解清楚它的对象模型和层次遍历逻辑这件事可以用几行 Tcl 干净利落地解决。这篇文章就是把这个看似简单但暗坑不少的操作彻底讲透。不管你是刚入行的后端新人还是做了几年但一直用 GUI 混日子的老手看完之后应该都能写出一个稳定可靠的捞 pin脚本。我会从 Innovus 的数据模型讲起解释为什么有些看似能用的命令实际上会漏对象然后给出经过实测的完整方案最后分享几个我在实际项目中踩过的坑和对应的规避方法。2. Innovus 里 pin、terminal、port 这几个概念到底怎么区分2.1 从数据模型理解为什么捞 pin容易捞错在 Innovus 的数据库里一个寄存器的时钟端在不同语境下可能被称作 pin、terminal 或者 port这三个词经常被混用但它们在工具内部的指向并不完全一样。如果你不搞清楚这层关系写出来的脚本很可能在某个层次上就断了。简单来说pin通常指 instance 上的连接点也就是你看到的寄存器实例的 clock 引脚terminal更偏向于 net 的端点概念强调连接关系port则多用于 module 的边界表示层次化设计的输入输出接口。Innovus 的 Tcl 命令体系里get_pins拿到的是 instance pinget_terminals拿到的是 terminal 对象get_ports拿到的是 module port。这三者在层次化设计里是有关联的但绝不是一回事。为什么这个区分重要因为当你想要一个 module 下所有 reg 的 clock pin时你的目标对象其实是instance pin而且这些 instance 必须满足两个条件第一它们属于目标 module 的层次范围第二它们是寄存器的时钟输入引脚。如果你用get_ports去抓拿到的只是 module 边界的 port根本碰不到内部的寄存器如果你用get_terminals但不加层次过滤可能会把整条 clock net 上所有 terminal 都捞出来远远超出你的目标范围。2.2 寄存器 clock pin 的识别特征Innovus 并不会直接告诉你这个 pin 是寄存器的 clock pin。你需要通过几个特征来间接判断。最常用的方法是看这个 pin 所属 instance 的 reference 类型——也就是它的 cell 类型。时序单元sequential cell通常有明确的命名规律比如以DFF、SDFF、DFFR等开头或者你可以在 library 里查到哪些 cell 是 sequencial 的。另一个方法是看 pin 的方向和名字。寄存器的时钟引脚通常命名为CK、CLK、CP、C等方向是 input。但这个方法不够可靠因为不同工艺库、不同 vendor 的命名习惯不一样有些库可能用G表示 clock gate 的使能端容易混淆。最稳妥的做法是结合两者先通过 cell 类型筛选出时序单元再在这些单元的 pin 里找时钟引脚。Innovus 提供了get_cells命令可以按 reference 过滤也支持用-filter表达式做更精细的筛选。理解这些过滤机制是写出可靠脚本的前提。2.3 层次化设计带来的额外复杂度如果设计是扁平的那事情很简单遍历所有 instance筛出寄存器再找它们的 clock pin。但实际项目几乎都是层次化的一个 module 可能包含子 module子 module 里还有更小的 module寄存器可能分布在任意一层。这时候你就面临一个选择是只抓当前 module 直接包含的寄存器还是把整个子树里的寄存器都抓出来这两种需求都存在。有时候你只关心当前层次的寄存器因为子模块的时钟可能已经在上一层被处理过了有时候你需要整个子树的完整清单比如做 CTS 约束时需要知道所有 sink。Innovus 的get_cells和get_pins都支持-hierarchical选项但这个选项的行为需要仔细理解——它会递归遍历所有层次但返回的对象命名方式和你预期的可能不一样。3. 用 get_pins 配合层次过滤一次性抓取的正确姿势3.1 基础命令组合与参数含义最核心的命令组合是这样的set target_module u_core/u_alu set clk_pins [get_pins -hierarchical -filter directionin is_clocktrue $target_module/*]但这条命令能不能直接用取决于你的 Innovus 版本和数据库状态。is_clock这个属性并不是所有版本都支持而且它判断的是 pin 是否被识别为 clock pin依赖于工具对 clock 的定义。更通用的做法是用 cell 类型来筛set all_cells [get_cells -hierarchical $target_module/*] set reg_cells [filter_collection $all_cells ref_name ~ *DFF* || ref_name ~ *SDFF*] set clk_pins [get_pins -of_objects $reg_cells -filter directionin]这里有几个关键点需要解释。get_cells -hierarchical会递归遍历$target_module下的所有层次返回所有 instance。filter_collection用正则匹配ref_name把寄存器类型的 cell 筛出来。get_pins -of_objects则是拿到这些 cell 上的所有 pin再用directionin过滤出输入方向的 pin。但这样拿到的输入 pin 可能不止 clock pin还包括数据输入、使能、复位等。所以还需要进一步筛选。如果你的工艺库命名规范可以用 pin name 来过滤set clk_pins [get_pins -of_objects $reg_cells -filter directionin name ~ *CK*]或者更严谨一点用is_clock属性如果版本支持set clk_pins [get_pins -of_objects $reg_cells -filter directionin is_clocktrue]3.2 为什么 -hierarchical 不是万能的-hierarchical选项看起来很方便但它有一个容易被忽略的行为它返回的对象名字是完整层次路径而不是相对于你指定 module 的相对路径。这意味着如果你后续要用这些 pin 名做其他操作可能需要额外处理路径前缀。更麻烦的是-hierarchical在某些 Innovus 版本里对get_pins的支持并不完整。我遇到过这样的情况用get_pins -hierarchical抓某个 module 下的 pin结果只返回了当前层次的 pin子层次的完全没出来。后来查手册才发现那个版本里get_pins的-hierarchical需要配合-leaf选项才能递归到叶子层。所以我的建议是不要盲目依赖-hierarchical而是显式地先抓 cell 再抓 pin。这样每一步的结果都可验证出了问题也容易定位是哪一层断了。3.3 处理 module 边界上的特殊情况有一种情况特别容易漏寄存器本身就在 module 的边界上它的 clock pin 连接的是 module 的 input port。这时候如果你只抓 instance pin可能会发现这个 pin 的 net 是空的或者连到了 port 上需要额外处理。还有一种情况是 clock gating cell。有些设计里寄存器的 clock 不是直接来自 clock net而是经过了一个 ICGintegrated clock gatingcell。这时候你抓到的 clock pin 可能连到 ICG 的输出而不是主 clock net。如果你的目标是找所有受时钟控制的寄存器那这些也要算进去如果你只关心直接挂在某根 clock net 上的寄存器那就要把 ICG 后面的排除掉。处理这些边界情况的方法是在脚本里加判断逻辑。比如检查 pin 的 net 是否存在如果不存在就往上追到 port或者检查 pin 的 driver 类型如果是 ICG 就特殊标记。这些逻辑不复杂但需要你对设计结构有清晰的预期。4. 一个经过实测的完整 Tcl 脚本拆解4.1 脚本整体结构与设计思路下面这个脚本是我在实际项目中反复用过的版本核心思路是分步筛选、每步可验证proc get_reg_clk_pins {module_path} { # Step 1: 抓取目标 module 下所有层次 cell set all_cells [get_cells -hierarchical ${module_path}/*] if {[llength $all_cells] 0} { puts WARNING: No cells found under $module_path return {} } # Step 2: 筛选寄存器类型 cell set reg_cells {} foreach cell $all_cells { set ref [get_attribute $cell ref_name] if {[regexp -nocase {DFF|SDFF|DFFR|DFFS} $ref]} { lappend reg_cells $cell } } if {[llength $reg_cells] 0} { puts WARNING: No register cells found under $module_path return {} } # Step 3: 抓取这些 cell 的 clock pin set clk_pins {} foreach cell $reg_cells { set pins [get_pins -of_objects $cell -filter directionin] foreach pin $pins { set pin_name [get_attribute $pin name] if {[regexp -nocase {CK|CLK|CP} $pin_name]} { lappend clk_pins $pin } } } puts INFO: Found [llength $clk_pins] clock pins under $module_path return $clk_pins }这个脚本的好处是每一步都有明确的中间结果你可以单独跑某一步来验证。比如先跑 Step 1 看看 cell 数量对不对再跑 Step 2 看看寄存器筛出来多少最后看 clock pin 的数量是否合理。4.2 关键步骤的参数调优与验证方法Step 1 里的-hierarchical如果行为不符合预期可以改成手动递归proc get_all_cells_recursive {module_path} { set cells [get_cells ${module_path}/*] set sub_modules [get_cells -hierarchical ${module_path}/* -filter is_hierarchicaltrue] foreach sub $sub_modules { set sub_name [get_attribute $sub full_name] set cells [concat $cells [get_all_cells_recursive $sub_name]] } return $cells }Step 2 里的正则匹配需要根据你的工艺库调整。如果你不确定库里寄存器 cell 的命名规律可以先 dump 一批出来看看set sample_cells [lrange $all_cells 0 20] foreach cell $sample_cells { puts [get_attribute $cell name] - [get_attribute $cell ref_name] }Step 3 里的 pin name 匹配也是同理。有些库的 clock pin 叫C有些叫CK有些叫CLK。你可以先把所有输入 pin 的名字打印出来确认规律后再写正则。4.3 输出结果的格式化与后续利用抓到 pin 之后通常需要输出成某种格式供后续使用。最简单的就是打印完整路径foreach pin $clk_pins { puts [get_attribute $pin full_name] }如果需要输出成 CSV 或者给其他脚本用可以加上 cell 名字和 net 名字puts cell_name,pin_name,net_name foreach pin $clk_pins { set cell [get_attribute $pin cell] set net [get_attribute $pin net] puts [get_attribute $cell full_name],[get_attribute $pin name],[get_attribute $net full_name] }这些结果可以直接喂给 CTS 约束脚本或者用来做 clock tree 的 sink 分析。5. 那些年我在捞 pin上踩过的坑5.1 层次路径写错导致返回空列表这是最常见的错误。Innovus 的层次路径分隔符是/但如果你从 GUI 里复制路径有时候会带上多余的斜杠或者空格。更隐蔽的是有些设计的 module 名字里本身包含特殊字符比如[、]、.这些在 Tcl 里需要转义。我的习惯是在脚本开头先验证 module 是否存在if {[llength [get_cells $module_path]] 0} { puts ERROR: Module $module_path does not exist return {} }另外如果你不确定路径怎么写可以用get_cells -hierarchical *先列出所有层次 cell找到目标 module 的完整路径。5.2 寄存器类型判断遗漏了特殊 cell有些工艺库里有特殊的寄存器类型比如带 scan 的SDFF、带 enable 的DFFE、带 reset 的DFFR甚至还有DFFRS、DFFRE这种组合。如果你的正则只写了DFF可能会漏掉一些。更稳妥的做法是查 library 里的is_sequential属性set reg_cells [filter_collection $all_cells is_sequentialtrue]但这个属性也不是所有版本都支持。如果不行就只能老老实实把所有可能的命名规律都列出来。5.3 clock pin 名字匹配的陷阱前面提到过不同库的 clock pin 命名不一样。我遇到过最坑的一种情况是某个库的寄存器 clock pin 叫C但同一个库里还有个 combinational cell 的 pin 也叫C。如果你只用 pin name 过滤就会把 combinational cell 的 pin 也捞进来。解决办法是先用 cell 类型筛出寄存器再在寄存器范围内找 clock pin。这样即使 pin name 有歧义也不会跨类型误抓。5.4 性能问题大设计上的遍历效率如果你的设计很大get_cells -hierarchical可能会很慢。我试过一个百万门级的设计全层次遍历一次要几十秒。如果脚本里反复调用累积起来就很可观。优化方法是尽量用 Innovus 原生的过滤命令而不是在 Tcl 里循环。比如set reg_cells [get_cells -hierarchical ${module_path}/* -filter ref_name ~ *DFF*]这样过滤在工具内部完成比 Tcl 循环快很多。如果还是慢可以考虑分块处理每次只抓一个子模块。6. 从捞 pin延伸到 clock tree 分析的实用技巧6.1 用捞到的 pin 做 clock tree sink 统计抓到 clock pin 之后一个常见的后续操作是统计这些 pin 的分布。比如按 clock net 分组array set net_pins {} foreach pin $clk_pins { set net [get_attribute $pin net] set net_name [get_attribute $net full_name] lappend net_pins($net_name) $pin } foreach net_name [array names net_pins] { puts $net_name: [llength $net_pins($net_name)] pins }这个统计可以帮你快速判断某个 module 的 clock 是否集中有没有意外的 clock domain 交叉。6.2 结合 clock tree 报告做交叉验证Innovus 的report_clock_tree命令会输出 clock tree 的详细结构包括每个 sink 的位置和延迟。你可以把捞到的 pin 列表和报告里的 sink 列表做交叉验证确认没有遗漏。具体做法是把 pin 的 full name 和报告里的 sink name 做比对。如果报告里有但你的列表里没有说明漏了反之说明多抓了。这个方法虽然笨但在关键项目上很有效。6.3 把脚本封装成可复用的 proc最后建议把这个功能封装成一个通用的 proc放在你的个人工具库里。参数除了 module path还可以加上可选的过滤条件比如只抓某个 clock domain 的 pin或者只抓某个类型的寄存器。proc get_reg_clk_pins {module_path {clk_filter } {cell_filter DFF|SDFF}} { # ... 实现逻辑 }这样下次遇到类似需求直接调用就行不用重新写一遍。我在实际项目里用这套方法处理过各种规模的设计从几万门的小模块到几百万门的 SoC基本都能稳定工作。唯一需要根据项目调整的就是 cell 类型和 pin name 的匹配规则这个没有通用解只能根据具体工艺库来定。但只要你理解了 Innovus 的对象模型和层次遍历逻辑剩下的就是查手册和试错的事了。