前阵子帮一个项目组整理SMIC工艺下的Memory时序库,核心工作就是把Memory Compiler吐出来的一堆.lib文件,用Library Compiler批量换成综合和后端能直接吃进去的.db。整个过程听起来很简单,实际走下来有不少细节,稍不注意就是面积虚大、timing对不上、功耗表缺失这类问题。今天把这次实战从头到尾拆开讲清楚,从SMIC Memory配置到lc_shell脚本编写,再到坑点排查,一次性讲透。
做数字IC的兄弟都清楚,Memory在芯片里的地位很特殊。它通常占掉芯片面积的大头,又是时序和功耗分析的难点。Synopsys的Memory Compiler会生成.lib(Liberty时序库),这个文件是文本格式,人眼能看,但综合工具、STA工具不会直接拿它去跑。需要用Library Compiler(lc_shell)把它转成二进制格式的.db文件。这一步看起来只是格式转换,实际牵扯到库名、时序模型、功耗模型、面积、电压单位等一系列信息的解析和重构,出一点差错,后面DC综合和PT签核就会埋雷。
这篇文章适合正在做数字后端、前端集成或者PDK/库维护的工程师。如果你是刚接触Memory Compiler的应届生,或者从其他工艺转SMIC的老手,都能在里边找到可以直接抄作业的脚本和排查思路。
1. 项目概述:为什么要碰Memory的lib转db
1.1 Memory在数字流程中的特殊地位
几乎所有SoC里都有SRAM、ROM这类存储器,寄存器和触发器堆出来的memory在面积和功耗上没有竞争力。实际项目里,大块缓存、FIFO、寄存器堆,都是直接调用Memory Compiler生成的硬核macro。这些macro不是像标准单元那样的简单逻辑,它们内部有复杂的位线、字线、敏感放大器、时序控制电路,Cell library里对应一个完整的时序模型。
在做逻辑综合时,Design Compiler需要知道每个memory单元的面积、输入pin的cap、时序弧(setup/hold/output delay)、功耗特性。这些信息在Memory Compiler生成的.lib里都有。但DC默认加载.db格式,Synopsys工具链用二进制db去加速读取和优化。直接用.lib也能跑DC吗?早期版本某些流程能读,但正规流程都是先转db,一是有性能优势,二是方便库里统一管理,三是db在版本兼容和加密上更省心。
SMIC的Memory Compiler通常一次会生成多个corner的lib,比如typical、slow、fast,还可能分为ss/tt/ff更细分的PVT组合。这些lib文件需要统一转成对应的db,才能分别用于setup和hold分析。转db不是简单改个后缀,里面有很多工程层面的决策点。
1.2 lib和db到底差在哪
.lib是Liberty格式的文本文件,遵循IEEE 1481标准,里面描述了cell的name、pins、direction、capacitance、timing arcs、power模型。可以用文本编辑器打开,也可以直接用脚本解析。db是Synopsys的二进制格式,本质上是lib的编译后产物,解析速度更快,也支持base library之间的link操作。
从内容上说,db并没有增减lib的信息,它是一一对应的。Library Compiler在转库时也会做一致性检查,如果lib有语法错误、定义缺失、单位不明,转换就会报错或warning。所以很多人问“能不能直接用lib替代db”,技术上部分工具支持,但芯片流程里普遍的做法是db优先。特别是在multi-corner multi-mode流程里,db的加载效率比lib高很多,综合几千个标准单元加上几十个memory的库,文本解析的差距会非常明显。
1.3 这次实战我打算怎么讲
整条链路可以分成两段:前一段是SMIC Memory配置,决定生成什么样的.lib;后一段是Library Compiler转换,决定.lib如何变成.db。很多人在转库时报错,回头查了半天,根因其实在Memory Compiler配置那一步,比如输出寄存器选项没选对,或者功耗模型没打开,生成的lib本身就不完整。所以这篇文章不会只讲lc_shell那几行命令,而是把Memory配置和转库结合起来讲,让整个流程能高效闭环。
2. SMIC Memory配置环节:先把源文件做对
2.1 Memory Compiler选型与工艺映射
SMIC的Memory IP一般通过工艺厂或第三方IP公司提供的Compiler工具生成,比如自研的SRAM Compiler,或者用的Sage Micro/力旺等第三方编译器。选型时第一个要确认的是工艺节点和金属层。SMIC工艺有55nm、40nm、28nm、14nm等,不同手机/wire bonded封装还影响金属层选择,这些会直接决定memory macro的面积和速度。
Memory Compiler工具里一般会先选Library/Technology,然后进入SRAM/ROM类型选择。这一步务必和Foundry提供的PDK版本对齐。PDK版本对不齐,生成的lib在时序上和signoff标准可能有差异。我见过有人拿着新工艺的Compiler去生成旧PDK的库,结果db里的时序模型和后端signoff时拿到的SPICE仿真结果差得离谱。
配置界面里通常要填这些基本参数:Memory类型(Single Port、Dual Port、Two Port)、宽度(Word Width)、深度(Number of Words)、Column Mux Ratio、Output Register、功耗优化模式。每个选项背后都有一整套电路架构选择。
2.2 关键配置项逐条拆解
先说宽度和深度。Word Width和Number of Words是Memory的基本规格,一般直接来自芯片架构需求。要注意的是深度变化会显著影响macro面积和时序,同样的16Kx32和32Kx16,面积不是简单线性关系,而是取决于编译器内部的bank划分和bitcell排布。我们在实际项目里会让架构师先给需求,再让Memory Compiler跑一遍,看面积和时序报告是否满足约束,不满足就调整宽度、深度或mux ratio。
Column Mux Ratio,也叫列复用比,常见的有4、8、16。它的含义是同一根IO pin被多少列的bitcell共用。mux越大,单根IO对应的bitcell列数越多,位线负载越大,访问速度会变慢,但面积会更小,因为外围电路(sense amplifier、写驱动)可以减少。反过来mux越小,速度更快,面积更大。选mux时要结合工作频率和预算面积,通常先默认4或8,然后根据时序余量微调。
Output Register选项决定了memory是否带流水线寄存器输出。带output register的memory有更好的时序性能,能显著降低输出路径上的delay,但会增加一拍延迟,同时面积和功耗也会略高。很多前端工程师在RTL里已经有了reg输出,再用Compiler的output register就会造成两级寄存器,白白增加延迟和功耗。这里要提前和前端确认清楚,否则生成出来的lib在综合时时序会偏紧。
功耗优化模式一般有High Speed、Balance、Low Power几种。SMIC工艺下,Low Power模式通常通过降低位线摆幅、关断不需要的bank、调整cell电压等方式实现,代价是访问变慢。如果芯片是低功耗SoC,Memory数量多,这个选项值得仔细权衡。High Speed模式适合对频率敏感的关键buffer,但要做好功耗超标的风险评估。
2.3 配置时的面积、速度、功耗取舍
Memory配置不是简单地把参数填完就完事,本质上是一个多目标优化问题。面积和速度是直接矛盾的。速度越高,往往需要更大的bitcell,更强的sense amplifier,更精细的时序控制电路,这些都会推高面积。功耗和速度也有冲突,尤其是在近阈值电压或低电压场景下,功耗优化往往会导致cell电流能力下降,时序变差。
我的习惯是先在典型corner下做一个sweep:把深度、宽度固定,改变mux ratio、output register和功耗模式,生成几组lib,然后用Memory Compiler自带的报告对比面积、功耗和access time。选出最优组合后,再针对这个组合去生成全corner的lib。这个流程会花点时间,但比盲选再返工高效得多。
2.4 生成文件怎么检查
Memory Compiler生成完成后,除了.lib以外,通常还会生成.v(Verilog model)、.gds、.lef、.milkfile或.etm文件。对数字流程来说,最关心的是.lib和.v。在进入lc_shell之前,至少要做三件事:
用文本编辑器或grep打开.lib确认library name,一般是类似sram_16k*32_tt1p2v_25c.lib的命名,这里边包含了corner信息。检查文件头部有没有声明电压、温度、单位。常见错误是单位缺失,比如capacitive_load_unit没写,转换后电容值异常。
检查是否包含时序响应,尤其是有没有输出信号transition的查找表。有些简化lib只给了scalar值,虽然lc_shell能转,但到DC/PT里做尺寸优化时会因为缺少lookup table而非常粗糙。
用Memory Compiler生成的.v文件和lib做一次port对应检查。比如256x32的SRAM,端口应该有CLK、CEN、WEN、A[7:0]、D[31:0]、Q[31:0]。如果lib里pin定义少了,后端的connectivity就会出问题。
还有一个小技巧,我习惯先用文本工具搜索关键字段,比如pin, timing, power, index_1,确认lib不是空壳。曾经碰到过编译器生成一个warning,部分corner的lib只有一个空模板,没有任何timing信息,问题就是memory instance数量超过了工具试用limit,生成被截断。这种问题不提前发现,后面lc_shell照样能转出db,但dc使用时会直接报timing library没有arc。
3. Library Compiler转换:从lib到db的完整实操
3.1 工具环境和准备
Library Compiler是Synopsys的工具,一般在Linux服务器上以lc_shell方式运行。确认环境变量,确保lc_shell已经被添加到PATH。可以用which lc_shell验证,也可以直接用完整路径调用。
转库前先想清楚输出目录结构,建议按工艺/corner/类型分目录。比如smic55/sram/tt_1p2v_25c/和smic55/sram/ss_0p99v_125c/分开,避免db文件相互污染。数据库统一以后,DC的link_library、target_library指向会更干净。
在lc_shell脚本里通常需要设置一些变量,比如:
- search_path,指定lib所在路径。
- target_library,也就是最终生成的db要放到哪里。
- SHOW_LIBRARY_INFO,可用来快速查看库信息。
但lc_shell本身不需要复杂的setup,它和DC不同,不需要启动license那么久,但同样需要有效的Synopsys license feature。启动后可以直接交互输入命令,也可以执行脚本文件。
3.2 一条命令的转换脚本
最简单的方式如下:
set search_path [list . /path/to/inputs] read_lib sram_16k32_tt_1p2v_25c.lib set output_lib [get_object_name [current_design]] write_lib $output_lib -format db -output sram_16k32_tt_1p2v_25c.db在lc_shell里,read_lib读入lib后,当前design就是那个library,库名通常和lib文件里的library(name)一致。可以用current_design查看,也可以用get_libs列出所有已加载的library。write_lib命令的完整格式是:
write_lib sram_16k32_tt_1p2v_25c -format db -output /path/to/output/sram_16k32_tt_1p2v_25c.db这条命令会直接生成db文件。如果是第一次做,建议加上report_lib命令,先把库信息打印出来确认再写文件。比如:
report_lib sram_16k32_tt_1p2v_25creport_lib能看到library name、unit、创建时间、cell数量、pin数量等。如果cell数为0,基本就是没读进来或者lib有问题。
3.3 批量转换脚本
Memory一般不是一个两个,少则几十,多则上百个。手工一条条敲命令显然不行。我习惯写一个Tcl脚本,用foreach循环处理一个列表。
假设所有lib文件在libs/目录下,输出到dbs/目录,脚本可以这样写:
set lib_list [list \ sram_16k32_tt_1p2v_25c \ sram_16k32_ss_0p99v_125c \ sram_16k32_ff_1p32v_0c \ rom_64k16_tt_1p2v_25c \ ] set out_dir "/path/to/dbs" set lib_dir "/path/to/libs" foreach lib_name $lib_list { set lib_file "${lib_dir}/${lib_name}.lib" if {[file exists $lib_file]} { read_lib $lib_file write_lib $lib_name -format db -output ${out_dir}/${lib_name}.db puts "Converted ${lib_name}" } else { puts "Warning: ${lib_file} not found" } }如果corner多、memory多的场景,还可以用shell脚本自动扫描目录下所有.lib,生成对应.db,比如:
for lib in /path/to/libs/*.lib; do base=$(basename "$lib" .lib) echo "read_lib $lib" echo "write_lib $base -format db -output /path/to/dbs/${base}.db" done > convert.tcl然后在lc_shell里执行:
source convert.tcl这样一旦有新增memory,只要把新.lib丢进目录,重新生成脚本再跑一遍就行。实测下来几十个库的转换也就几分钟,瓶颈主要在license和服务器的IO上。
3.4 转换后的检查项
db生成后不要急着入库,先做一轮检查。最直接的是用lc_shell再读一次db,或者用report_lib查看。但要特别注意,db读取后报告的信息如果和lib一致,才能算转换成功。
我通常会检查以下几点:
- library name是否和期望一致。
- cell数量是否完整,不要漏cell。
- 每个memory cell的时序arc是否存在,查看某个cell下是否有rise/fall transition和setup/hold,用report_delay_calc? 更简单的做法是用grep在lib里对比,或者db后直接ltrace? lc_shell没有直接report_timing,但可以通过report_lib -cell xxx看到pin和arc信息。
在lc_shell里,如果只是快速确认arc,可以:
report_lib -cell sram_16k32_tt_1p2v_25c会列出每个cell的pins,如果只列了pin名没有timing信息,那这个lib可能是功能模型,不是时序模型。Memory lib的timing信息通常在cell下面以timing()内包含相关_pin和related_pin的方式存在。
另外,检查power模型,用report_lib是不是能看到power_consumption或internal_power信息。对于Memory,功耗模型如果缺失,跑IR drop或功耗分析就会缺数据。很多Memory Compiler默认生成的lib里,internal_power可能只有理论值,必须在功耗计算时配合activity factors,才能准确估算动态功耗。
最后,把db放到DC里做一次link和elaborate测试,直接用read_db或link_design加载它,看是否报错。这一步是终极验证,比任何检查都直接。
4. 实战中的坑和排查方法
4.1 read_lib阶段最常见的报错
read_lib报错有很多种,我挑几个高频的讲。
第一种是“Cannot find library file”或者路径问题。这通常不是lib不存在,而是search_path没设置好,或者文件名里带了特殊字符。建议在脚本开头用绝对路径,避免相对路径带来的麻烦。Memory Compiler生成的文件名有时候带+号或者空格,最好先用shell重命名成简洁的名字。
第二种是“Library name mismatch”或者“Cell count 0”。这往往是lib文件里的library(name)和文件名不一致。Library Compiler内部以lib里的name为主,但write_lib时如果名字写错,就会写不出来。解决办法是先read_lib,再用get_object_name [current_design]确认实际的library name,不要凭文件名猜。
第三种是“Undefined attribute”或者“Unsupported group”。这常见于lib版本格式太新或太旧,Library Compiler版本不兼容。比如新版Liberty里有的一些新属性,旧版lc_shell不认识但也不会报error,只是忽略,结果库信息不完整。遇到这种情况,要升级Library Compiler版本,或者回到Memory Compiler里的lib生成选项,选择兼容的Liberty格式。
还有一类是在Windows环境跑lc_shell,路径分隔符混乱。Library Compiler主要在Linux上跑,Windows上不是不能跑,但路径和license的问题很多。我的建议是别折腾,放到Linux机器上转。
4.2 转换结果和预期不符的问题
有时候转出来的db能用,但面积不对、timing曲线很怪,多半问题出在lib的默认值。比如单位定义。LC转db时如果不明确单位,会采用lib里设定的单位。如果lib里电阻单位写成kohm,电容单位是pF,db解析出来的load和transition可能和期望差几个数量级。这类问题没法在lc_shell直接看出来,要在DC中检查cell面积或pin cap。
0.1pf的库被当成1pf,综合时buffer sizing就会完全失真。所以转库后我习惯在DC里用report_cell或者report_units确认一下单位和数值范围。尤其是Memory这类大macro,面积动辄几万平方微米甚至更大,如果db里面积是整数数量级的错误,很容易发现;如果只是偏5%,就要怀疑是不是单位或scaling factor的问题。
另一个常见问题是功耗数据异常。Memory的功耗和activity强相关,lib里通常给出的是内部功耗查找表,包含能量值。如果lib模板没有internal_power,而只有leakage_power,那转换后功耗报告会缺失dynamic power。这种情况最好回Memory Compiler里检查有没有开启功耗库生成选项。
还有一些情况是多个corner的lib相互覆盖。比如tt corner和ss corner的library name都是sram_16k32,write_lib到同一个目录时,后写的覆盖前面的。这个坑我踩过,项目后期发现setup分析用的ss库其实是tt的数据。所以批量转换脚本里一定要用不同或有明确后缀的库名,输出文件也严格区分corner目录。
4.3 版本兼容和跨工艺注意事项
Library Compiler版本和Memory Compiler版本最好保持在同一个Synopsys工具版本周期内。跨大版本(比如2019到2022)读lib,大多数情况没问题,但偶尔会有attribute变化导致warning。warning太多时别忽略,尤其是“Ignoring attribute xxx”这种,说明lib携带的信息可能被丢弃。
另外要注意跨工艺复用库的问题。SMIC 55nm的memory db千万不要直接用在40nm的flow里,哪怕二者pin兼容,时序和功耗物理上完全不一致。有次我见有人为了省转库时间,把tt的db复制成ss的db,临时跑通仿真,后来被PT报了一堆violation,反而花更多时间排查。每个corner的db必须独立生成。
还有Memory Compiler的lib里经常包含带version信息的header,比如“liberty_version 2.0”或“2.1”。高版本Liberty支持更多属性,比如ccsn、ecsm,但Library Compiler如果有license限制,可能不解析这些扩展信息,转换出的db只有传统NLDM时序。如果项目需要做先进节点时序signoff,要用到ccs或ecsm库,就要确保Library Compiler版本和feature足够,否则精度不够。
5. 效率提升经验:把转库变成流水线
5.1 一次配置全流程自动化
转库这件事,看起来简单,但手工做很容易出错。我最后在自己环境里搭了一套流水线,大体分三层:Memory配置列表、转换脚本、验证脚本。
memory配置列表是一个文本文件,每行一个memory,字段包括名称、类型、宽度、深度、mux、output register、功耗模式、corner。这个文件既给Memory Compiler用,也给转库脚本用,保证两边参数一致。Memory Compiler生成的lib名从配置文件里派生,转库脚本读配置文件,自动生成lib_list,然后批量转db。
这样做的好处是,当架构改了某个memory深度,只需要改配置列表,然后重新跑一遍生成和转换脚本,所有corner的db自动更新。再也不用手工一个一个敲命令。
5.2 Memory db在综合阶段的验证闭环
转库完成后,我习惯先在一个测试顶层里做综合烟雾测试。DC里设置:
set target_library [list /path/to/dbs/sram_16k32_tt_1p2v_25c.db /path/to/stdcell/tt.db] set link_library [list * $target_library]然后read_verilog一个简单的wrapper,只例化几个memory模块,elaborate,link,检查有没有Unresolved reference,再跑一遍compile或update_timing。这一步能发现大部分库不匹配问题。
综合后还要看下面积报告和时序报告。Memory的面积应该和lib里cell面积一致,时序路径上和memory相关的delay应该在预期范围内。如果发现Q pin的transition特别大,可能是lib里output load模型不对,或者memory本身驱动能力不足。
5.3 我的一些个人习惯
最后聊几个个人习惯,不算标准做法,但能省不少事。
第一,给所有db文件加上时间戳或版本标记,比如sram_16k32_tt_1p2v_25c_r1.db。这样team里好追溯,不会出现拿错库跑结果的事。
第二,转换日志必须留档。脚本里用tee命令把lc_shell输出存成log,一旦后面出现异常,能快速定位是哪个库在哪一步出了问题。
第三,不要只盯着db文件的大小。很多memory的db几百KB,几个corner看起来差不多,实际上内容差异很大。要在DC里分别load并检查,才能确保corner切换正确。
第四,Memory Compiler配置和转库任务别手动执行,尽量写进Makefile或者持续集成(CI)里。我们后端flow每次跑回归前,都会自动检查所有memory库的生成时间,发现lib更新就会触发重新转库流程,避免因为源文件更新导致db过期。
做这行时间长了,越来越觉得“从lib到db”虽然只是一个小环节,但它卡在所有数字实现流程的最前面。源库质量不行,后面综合、STA、功耗分析全是白跑。把Memory配置和Library Compiler的转换流程理顺,配置文件收口,脚本自动化,回报率非常高。