3. 脚本工程架构设计
当数字后端自动化脚本从“一个 Tcl 文件跑完 P&R”演进到“支撑多个项目、多种工艺、多套 EDA 工具”时,最先崩溃的往往不是工具,而是脚本本身。一个典型的失控信号是:run_flow.tcl 里同时出现 read_lef、set_app_options、report_timing、send_mail 和 exit 1,任何人想改一处约束都要通读两千行。分层脚本架构要解决的核心问题,就是把“流程编排”“任务实现”“工具调用”“配置数据”这四类关注点拆开,让每一层只对一种变化负责。
四层职责与依赖方向
推荐的分层自顶向下为:入口层(Entry)、流程层(Flow)、任务层(Task)、工具适配层(Adapter)。依赖方向严格单向向下,下层不得反向调用上层,也不得跨层直接读取全局变量。
- 入口层:只做三件事——解析命令行参数、加载配置、调用流程层主函数。它不包含任何 EDA 命令,典型入口是
main.tcl或run.pl。 - 流程层:定义“先做什么、后做什么、失败怎么办”,例如
flow_synthesis、flow_place、flow_cts。它调用任务层函数,负责阶段顺序、检查点、恢复策略。 - 任务层:实现单一可复用任务,如
task_load_design、task_apply_constraints、task_run_place。它知道“要做什么”,但不关心具体工具命令怎么写。 - 工具适配层:把任务层的抽象请求翻译成具体工具命令,如 Innovus 的
place_opt_design、ICC2 的place_opt。同一任务在不同工具下由不同适配器实现。
这种划分带来的直接收益是:换工具只改适配层,换流程只改流程层,调参数只改配置,而任务层和入口层几乎不动。
配置管理:单一数据源与分层覆盖
配置不应散落在脚本各处。建议采用“默认配置 + 项目配置 + 命令行覆盖”的三级合并策略,最终生成一个只读的全局配置字典 ::CFG。默认配置放在 config/default.tcl,项目配置放在 config/$PROJECT.tcl,命令行用 -project、-stage、-run_dir 等参数覆盖。
# config/default.tcl
set CFG(project) "demo"
set CFG(tech_node) "n5"
set CFG(clock_period) 0.75
set CFG(place_density) 0.72
set CFG(max_cores) 16
set CFG(log_level) "INFO"
# config/demo.tcl 覆盖默认值
set CFG(clock_period) 0.68
set CFG(place_density) 0.70
合并逻辑放在入口层,合并后立即冻结:array set ::CFG [merge_config $default $project $cli]。任务层只读 ::CFG,不允许写回,避免“跑到一半配置被改”的隐蔽 bug。对于必须随阶段变化的参数,用 ::CFG(stage) 区分,而不是在任务里硬编码判断。
日志系统:分级、分文件、可追溯
日志不是 puts 的堆砌。一个可用的日志系统至少要有级别、时间戳、模块名和输出目标。级别建议 DEBUG / INFO / WARN / ERROR / FATAL,由 ::CFG(log_level) 控制。每条日志格式统一为:
[2025-03-18 14:22:07] [INFO ] [flow_place] stage=place start, density=0.70
[2025-03-18 14:41:33] [WARN ] [task_run_place] congestion overflow=0.8%, retry with density=0.68
[2025-03-18 15:02:11] [ERROR] [adapter_innovus] place_opt_design returned 1
实现上,log_info、log_warn、log_error 统一走一个 log_write 函数,同时写入控制台和 $RUN_DIR/logs/$stage.log。工具原始输出重定向到 $RUN_DIR/logs/$stage.tool.log,便于出错时对照。日志中必须带 stage 和 task 字段,否则多阶段并行时无法定位。
错误恢复:检查点与幂等重入
数字后端流程动辄数小时,不能一出错就从头再来。错误恢复的关键是检查点(checkpoint)和幂等重入。每个任务完成后保存设计数据库和状态文件:
# task_run_place 末尾
saveDesign $RUN_DIR/db/place.enc
set fp [open $RUN_DIR/state/place.done w]
puts $fp "status=ok"
puts $fp "density=$CFG(place_density)"
close $fp
流程层在调用任务前先检查 state/$task.done 是否存在且 status=ok,存在则跳过。若任务失败,流程层根据错误类型决定:可重试错误(如拥塞导致 place 失败)自动调整参数重跑一次;不可重试错误(如网表缺失)直接 log_error 并退出。重试必须改变输入参数,否则只是浪费机时。
模块间接口规范
层与层之间只通过函数签名和返回值通信,禁止共享可变全局状态。约定如下:
- 任务层函数统一命名
task_<verb>_<object>,返回0表示成功,非0表示失败,失败原因写入::ERR_MSG。 - 适配层函数统一命名
adapter_<tool>_<action>,只接受标量或列表参数,不读::CFG,由任务层把配置值作为参数传入。 - 流程层函数统一命名
flow_<stage>,负责检查点判断、调用任务、错误分支,不直接调用适配层。 - 所有跨层调用必须检查返回值,禁止“调用后不管”。
下面这张图给出四层架构的依赖关系、配置与日志的贯穿方式,以及检查点在流程层与任务层之间的位置。
把这张图落到工程目录上,结构大致如下:
script_root/
├── main.tcl # 入口层
├── config/
│ ├── default.tcl
│ └── demo.tcl
├── flow/
│ ├── flow_synthesis.tcl # 流程层
│ ├── flow_place.tcl
│ └── flow_cts.tcl
├── task/
│ ├── task_load_design.tcl # 任务层
│ ├── task_apply_constraints.tcl
│ └── task_run_place.tcl
├── adapter/
│ ├── adapter_innovus.tcl # 工具适配层
│ └── adapter_icc2.tcl
├── lib/
│ ├── log.tcl # 日志系统
│ └── config_merge.tcl # 配置合并
└── run/
└── $PROJECT/$STAGE/
├── db/ # 检查点数据库
├── state/ # *.done 状态文件
└── logs/ # 分级日志
adapter/ 下加文件,新增一个阶段只需要在 flow/ 下加文件,而 task/ 和 main.tcl 不动,说明分层是有效的。source 工具适配文件并调用工具命令;流程层直接读 ::CFG 拼命令;适配层反向调用 log_error 之外的流程函数。这些都会让依赖方向断裂,最终退化成“分层只是目录分层”。最后强调接口规范中的返回值约定。任务层失败时不要用 exit 直接终止,而应返回非零并设置 ::ERR_MSG,由流程层统一决定是重试、跳过还是终止。这样错误恢复策略集中在流程层,任务层保持纯粹,适配层保持无状态。配置、日志、检查点三者贯穿各层但不破坏依赖方向,整个脚本工程才具备可维护、可复用、可恢复的工程属性。