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>,负责检查点判断、调用任务、错误分支,不直接调用适配层。
  • 所有跨层调用必须检查返回值,禁止“调用后不管”。

下面这张图给出四层架构的依赖关系、配置与日志的贯穿方式,以及检查点在流程层与任务层之间的位置。

分层脚本架构与依赖方向 入口层 Entry 解析参数 · 合并配置 · 调用 flow_main 流程层 Flow 阶段顺序 · 检查点判断 · 错误分支 flow_synthesis / flow_place / flow_cts 任务层 Task 单一可复用任务 · 返回 0/非0 task_load_design / task_run_place 工具适配层 Adapter 翻译为具体工具命令 adapter_innovus_place / adapter_icc2_place 调用 调用 调用 配置管理 default.tcl + project.tcl + 命令行覆盖 ::CFG 只读 合并后冻结 任务层只读 禁止写回 注入 日志系统 DEBUG/INFO/WARN ERROR/FATAL log_write() 控制台 + 文件 $stage.log $stage.tool.log 带 stage/task 字段 贯穿 检查点 state/$task.done status=ok 则跳过 · 失败按可重试/不可重试分支

把这张图落到工程目录上,结构大致如下:

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,由流程层统一决定是重试、跳过还是终止。这样错误恢复策略集中在流程层,任务层保持纯粹,适配层保持无状态。配置、日志、检查点三者贯穿各层但不破坏依赖方向,整个脚本工程才具备可维护、可复用、可恢复的工程属性。