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 表达,而不能写死。
二、参数与变量:区分“设定值”和“计算值”
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 不是越复杂越好,而是要让工艺工程师能看懂、让引擎能安全执行、让版本治理能追溯。