2. Recipe数据模型设计

Recipe 引擎的核心不是“把参数存下来”,而是把一套可执行、可校验、可版本化的工艺逻辑表达成结构化数据。半导体设备 Recipe 通常同时包含三类信息:工艺意图(要做什么)、执行结构(按什么顺序做)、数据绑定(用哪些值做)。如果只把 Recipe 设计成一张“步骤—参数”平表,很快就会在条件分支、循环、子 Recipe 复用和变量计算上失控。因此,数据模型必须从“步骤表”升级为“可执行语法树 + 参数绑定 + 版本元数据”的组合。

一、从平表到树:Recipe 的六类建模对象

一个可落地的 Recipe Schema,至少需要六类对象:Step(步骤)、Parameter(参数)、Variable(变量)、Expression(表达式)、ControlFlow(控制流)、SubRecipe(子 Recipe)。它们的关系不是并列的,而是有明确的引用方向:Step 引用 Parameter 和 ControlFlow;Parameter 的值可以绑定 Variable 或 Expression;ControlFlow 内部又嵌套 Step 或 SubRecipe;SubRecipe 是一个可被多次实例化的 Recipe 片段。

以典型的 PECVD 设备为例,一个 Recipe 可能包含:抽本底真空、加热、气体稳定、RF 起辉、沉积、吹扫、回填等步骤。其中“沉积”步骤的时长可能依赖前一步实测的腔体压力,这就必须用 Variable 和 Expression 表达,而不能写死。

Recipe 数据模型:对象引用与嵌套关系 Recipe(根对象) recipeId / version / schemaVersion Step(步骤序列) stepId / type / order / next Parameter name / unit / range ControlFlow if / loop / branch Variable scope / type / default Expression AST / operator / ref SubRecipe ref / params / return 执行语义:从数据模型到运行时 参数绑定 literal / variable / expression 类型检查 + 范围校验 表达式求值 依赖拓扑排序 循环引用检测 单位一致性 控制流展开 if → 条件分支 loop → 迭代展开 sub → 实例化 版本治理 schemaVersion diff / 兼容性检查 发布 / 回滚

二、参数与变量:区分“设定值”和“计算值”

Parameter 是设备可执行的设定项,例如 RF_Power=300W、Pressure=2.5Torr、Time=45s。Variable 则是 Recipe 内部或跨步骤传递的中间量,例如 basePressure、stepIndex、accumulatedTime。关键设计原则是:Parameter 的值可以是字面量、变量引用或表达式,但 Parameter 本身必须声明类型、单位、上下限和是否可被覆盖。

下面是一个简化的 JSON Schema 片段,展示 Parameter 如何绑定 Expression:

{
  "stepId": "deposition",
  "type": "process",
  "parameters": [
    {
      "name": "RF_Power",
      "unit": "W",
      "type": "number",
      "range": [0, 2000],
      "value": { "expr": "basePower * (1 + rfTrim)" }
    },
    {
      "name": "Time",
      "unit": "s",
      "type": "number",
      "range": [1, 600],
      "value": { "var": "depositionTime" }
    }
  ]
}

这里 basePower 和 rfTrim 是 Variable,depositionTime 可能来自前一步的实测值或上位系统下发。Schema 必须能表达“这个值是算出来的”,而不是只存一个数字。

三、表达式:用 AST 而不是字符串拼接

表达式如果只存字符串,运行时再解析,会带来两个问题:一是无法在编辑期做类型检查和单位校验;二是无法安全地做依赖分析。推荐做法是把表达式建模为抽象语法树(AST),节点类型包括:Literal、VariableRef、BinaryOp、UnaryOp、FunctionCall。例如 basePower * (1 + rfTrim) 可以表示为:

{
  "op": "*",
  "left": { "var": "basePower" },
  "right": {
    "op": "+",
    "left": { "literal": 1 },
    "right": { "var": "rfTrim" }
  }
}

AST 的好处是:可以在保存时做循环引用检测、单位推导和范围传播。例如 basePower 单位是 W,rfTrim 无量纲,则结果单位仍是 W,可以直接与 Parameter 的 unit 做一致性校验。

四、条件分支与循环:控制流也是数据

条件分支不能写成“如果压力大于阈值就跳转”,而应建模为 IfNode,包含 condition(Expression)、thenBranch(Step 列表)、elseBranch(可选)。循环则建模为 LoopNode,包含 loopVar、iterable 或 whileCondition、body、maxIterations。其中 maxIterations 是安全上限,防止 Recipe 因条件永远为真而无限循环。

以“压力稳定等待”为例:

{
  "type": "loop",
  "loopVar": "waitCount",
  "whileCondition": { "op": "<", "left": { "var": "pressureError" }, "right": { "literal": 0.1 } },
  "maxIterations": 30,
  "body": [
    { "stepId": "wait_1s", "type": "delay", "parameters": [{ "name": "Time", "value": { "literal": 1 }, "unit": "s" }] },
    { "stepId": "read_pressure", "type": "measure", "parameters": [{ "name": "Sensor", "value": { "literal": "chamber_pressure" } }] }
  ]
}

注意 pressureError 必须在循环体内被更新,否则条件永远不变。Schema 应要求循环体至少有一个对条件变量的写操作,或在编辑期给出警告。

五、子 Recipe:复用与参数化

子 Recipe 是把重复工艺片段抽出来的关键机制。例如“抽本底真空”在多个 Recipe 中都会出现,可以定义为一个 SubRecipe,对外暴露参数 targetPressure 和 timeout。主 Recipe 通过 SubRecipeRef 引用它,并传入实参。Schema 需要区分定义和调用:定义包含 subRecipeId、parameters、body;调用包含 ref、arguments、returnBindings。

这样设计后,版本治理也有了抓手:SubRecipe 可以独立版本化,主 Recipe 锁定某个 SubRecipe 版本,避免“改了一个子 Recipe 导致所有主 Recipe 行为漂移”。

六、Schema 设计实践:校验规则与版本兼容

一个可用的 Recipe Schema 至少应包含以下校验层:结构校验(必填字段、类型、枚举)、引用校验(Variable 是否已声明、SubRecipe 是否存在)、语义校验(单位一致、范围合法、循环有上限)、版本校验(schemaVersion 是否被当前引擎支持)。建议在 Schema 中显式声明 schemaVersion,并在引擎侧维护一个版本迁移表。当字段新增时,旧 Recipe 应能通过默认值补齐;当字段语义变更时,必须提升主版本号并拒绝旧版本直接执行。

实践建议:把 Recipe 的“编辑期校验”和“执行期校验”分开。编辑期做静态检查,尽量在保存时就报错;执行期做动态检查,例如变量实际值是否越界、SubRecipe 调用深度是否超限。两者共用同一套 Schema 定义,避免规则漂移。

最后,数据模型设计要服务于可读性和可维护性。一个 Recipe 文件如果超过几百个步骤,就应该考虑拆分为多个 SubRecipe,并用清晰的命名和注释说明每个分支的工艺意图。Schema 不是越复杂越好,而是要让工艺工程师能看懂、让引擎能安全执行、让版本治理能追溯。