跳到正文
Learn-SCL
SCL 示例库/SCL 配方导入导出:RecipeExport / RecipeImport

SCL 配方导入导出:RecipeExport / RecipeImport

TIA-only:用标准访问 DB + UDT 存 5 条配方,一个 FB 管理 CSV 导出、导入、Busy/Done/Error 和状态码。

综合 S7-1200 S7-1500 RecipeExport RecipeImport UDT CSV TIA-only 2 块 / 150 行 TIA 验证 TIA 已验证

一句话:配方 DB 用标准访问 + Array[1..5] of RecipeUDT 存配方;RecipeExport 把 DB 导成与 DB 同名的 CSV,RecipeImport 从 CSV 读回。FB 用 Mode 0/1/2 保证同一时间只有一个指令在跑,REQ 取上升沿,Done 单周期,Error 保持到 Reset。

导入到我的 TIA 项目

通过 TiaLink 把本示例的 2 个 SCL 块作为“外部源”导入你当前打开的 TIA Portal 项目:自动生成块、编译,并清理外部源。导入后不会自动保存;FB 示例导入后需要你在 OB 里调用。

TiaLink 设置

默认 http://localhost:8080。也可以在本页地址后加 ?tialink= 临时指定。

TiaLink 启动时若设置了 --api-token,这里要填相同的值;只保存在本机浏览器 localStorage,不会上传。

TIA-only 示例:RecipeExport / RecipeImport 是 TIA Portal 的文件系统指令,依赖 CPU 的存储卡、Web 服务器和配方目录;本地 SCL 仿真引擎只有硬件桩,不会真正生成 CSV,所以不提供云仿真场景。验证方式是导入 TIA Portal 编译。

配方(Recipe)是设备参数的一组命名数据:同一个程序,换一组配方就能换产品。S7-1200/1500 用 RecipeExport 把配方 DB 导出成 CSV,用 RecipeImport 把修改后的 CSV 读回 DB。这个示例用 5 条啤酒配方演示完整的导出/导入握手。

原工程 rezept_ex-import.scl 用 exportDataUDT / importDataUDT + Program_Alarm + Variant DB;本页把它整理成 FB_Recipe + DB_Recipe,保留 RecipeExport / RecipeImport 指令,并修正原工程“导入 REQ 误用导出 busy”的问题。

Recipe 配方 DB 的 5 条 UDT 记录与 RecipeExport/RecipeImport 状态机
DB_Recipe 存 5 条 RecipeUDT;FB_Recipe 在 0/1/2 状态间切换,导出和导入共用同一把忙锁。

概述

  • DB_Recipe:标准访问 DB,Recipes : Array[1..5] of "RecipeUDT",每条配方包含品名、水、麦芽、小麦、酒花、酵母、糖化温度、休止温度、休止时间。
  • RecipeUDT:定义在 FB_Recipe.scl 末尾(END_FUNCTION_BLOCK 之后),TIA 外部源会一次生成 FB_Recipe + RecipeUDT。
  • FB_Recipe:状态 0=空闲、1=导出、2=导入;ExportReq / ImportReq 取上升沿,Done 单周期,Error 保持到 Reset。

导入顺序必须是 FB_Recipe 先、DB_Recipe 后:FB 文件先生成 RecipeUDT,DB 才能引用这个 UDT。编译在所有文件导入之后进行。

你将学到

  • RecipeExport / RecipeImport 的参数和 CSV 生成规则;
  • 为什么 RECIPE_DB 必须是标准访问、基于 UDT 的 DB;
  • 为什么 UDT 和 DB 的导入顺序不能反;
  • 如何用一把 Mode 忙锁保证导出/导入不同时执行;
  • 原工程的一个真实 bug:导入 REQ 用了导出的 busy。

接口与变量

下表由构建器从 SCL 源码的接口声明自动生成;页面上展示的代码就是导入 TIA 的同一份源码。

FB_Recipe FB

区块名称类型说明
输入ExportReqBool
输入ImportReqBool
输入ResetBool
输出ReadyBool
输出BusyBool
输出DoneBool
输出ErrorBool
输出StatusWord
输出LastStatusWord
输出ModeInt
静态ExportInst"RecipeExport"
静态ImportInst"RecipeImport"
静态ExportPrevBool
静态ImportPrevBool
静态ExportPulseBool
静态ImportPulseBool
静态ExportDoneIntBool
静态ExportBusyIntBool
静态ExportErrorIntBool
静态ExportStatusIntWord
静态ImportDoneIntBool
静态ImportBusyIntBool
静态ImportErrorIntBool
静态ImportStatusIntWord
静态StatusIntWord
静态LastStatusIntWord
静态ErrorLatchedBool
静态ModeIntInt

DB_Recipe DB

区块名称类型说明
静态RecipesARRAY[1..5] OF "RecipeUDT"

程序结构

导入顺序以下表为准:被依赖的块/类型先导入;本页已按依赖排好。

  1. FB_Recipe FB 导出/导入状态机,文件末尾定义 RecipeUDT · 141 行 · 4 段
  2. DB_Recipe DB 标准访问配方 DB,Array[1..5] of RecipeUDT · 9 行 · 1 段

FB_Recipe FB

导出/导入状态机,文件末尾定义 RecipeUDT · 141 行 · 源文件 blocks/FB_Recipe.scl

查看 / 复制完整块源码
FB_Recipe.scl
1FUNCTION_BLOCK "FB_Recipe"
2{ S7_Optimized_Access := 'TRUE' }
3VERSION : 0.1
4   VAR_INPUT
5      ExportReq : Bool;   // 上升沿导出 CSV
6      ImportReq : Bool;   // 上升沿导入 CSV
7      Reset : Bool;       // 清除错误和最后状态
8   END_VAR
9 
10   VAR_OUTPUT
11      Ready : Bool;       // 空闲且无未复位错误
12      Busy : Bool;        // 导出/导入进行中
13      Done : Bool;        // 本周期完成(单周期)
14      Error : Bool;       // 有未复位错误
15      Status : Word;      // 当前/最后状态码
16      LastStatus : Word;  // 最后一次完成或错误的状态码
17      Mode : Int;         // 0=空闲 1=导出 2=导入
18   END_VAR
19 
20   VAR
21      ExportInst : RecipeExport;
22      ImportInst : RecipeImport;
23      ExportPrev : Bool;
24      ImportPrev : Bool;
25      ExportPulse : Bool;
26      ImportPulse : Bool;
27      ExportDoneInt : Bool;
28      ExportBusyInt : Bool;
29      ExportErrorInt : Bool;
30      ExportStatusInt : Word;
31      ImportDoneInt : Bool;
32      ImportBusyInt : Bool;
33      ImportErrorInt : Bool;
34      ImportStatusInt : Word;
35      StatusInt : Word := 16#0000;
36      LastStatusInt : Word := 16#0000;
37      ErrorLatched : Bool := FALSE;
38      ModeInt : Int := 0;
39   END_VAR
40BEGIN
41   REGION 边沿与复位
42      // 导出/导入都只响应一次上升沿;REQ 给单周期脉冲,实例在后台继续执行。
43      #ExportPulse := #ExportReq AND NOT #ExportPrev;
44      #ExportPrev := #ExportReq;
45      #ImportPulse := #ImportReq AND NOT #ImportPrev;
46      #ImportPrev := #ImportReq;
47 
48      IF #Reset THEN
49         #ModeInt := 0;
50         #StatusInt := 16#0000;
51         #LastStatusInt := 16#0000;
52         #ErrorLatched := FALSE;
53      END_IF;
54 
55      #Done := FALSE;
56      #Busy := FALSE;
57   END_REGION
58 
59   REGION 导出
60      // 只有空闲且没有未复位错误时才允许开始新的导出。
61      IF #ExportPulse AND #ModeInt = 0 AND NOT #ErrorLatched THEN
62         #ModeInt := 1;
63         #StatusInt := 16#0000;
64      END_IF;
65 
66      #ExportInst(REQ := #ExportPulse,
67         RECIPE_DB := "DB_Recipe",
68         DONE => #ExportDoneInt,
69         BUSY => #ExportBusyInt,
70         ERROR => #ExportErrorInt,
71         STATUS => #ExportStatusInt);
72 
73      IF #ModeInt = 1 THEN
74         #Busy := #ExportBusyInt;
75         #StatusInt := #ExportStatusInt;
76         IF #ExportDoneInt THEN
77            #ModeInt := 0;
78            #Done := TRUE;
79            #LastStatusInt := #ExportStatusInt;
80         ELSIF #ExportErrorInt THEN
81            #ModeInt := 0;
82            #ErrorLatched := TRUE;
83            #LastStatusInt := #ExportStatusInt;
84         END_IF;
85      END_IF;
86   END_REGION
87 
88   REGION 导入
89      // 导入与导出共用 Mode,保证同一时间只有一个 Recipe 指令在跑。
90      IF #ImportPulse AND #ModeInt = 0 AND NOT #ErrorLatched THEN
91         #ModeInt := 2;
92         #StatusInt := 16#0000;
93      END_IF;
94 
95      #ImportInst(REQ := #ImportPulse,
96         RECIPE_DB := "DB_Recipe",
97         DONE => #ImportDoneInt,
98         BUSY => #ImportBusyInt,
99         ERROR => #ImportErrorInt,
100         STATUS => #ImportStatusInt);
101 
102      IF #ModeInt = 2 THEN
103         #Busy := #ImportBusyInt;
104         #StatusInt := #ImportStatusInt;
105         IF #ImportDoneInt THEN
106            #ModeInt := 0;
107            #Done := TRUE;
108            #LastStatusInt := #ImportStatusInt;
109         ELSIF #ImportErrorInt THEN
110            #ModeInt := 0;
111            #ErrorLatched := TRUE;
112            #LastStatusInt := #ImportStatusInt;
113         END_IF;
114      END_IF;
115   END_REGION
116 
117   REGION 状态输出
118      // Error 保持到 Reset;Done 只在完成的那一个扫描周期为 1。
119      #Error := #ErrorLatched;
120      #Ready := (#ModeInt = 0) AND NOT #ErrorLatched;
121      #Status := #StatusInt;
122      #LastStatus := #LastStatusInt;
123      #Mode := #ModeInt;
124   END_REGION
125END_FUNCTION_BLOCK
126 
127TYPE "RecipeUDT"
128VERSION : 0.1
129   STRUCT
130      ProductName : String[32];   // 配方名(CSV 第一列之后)
131      Water : Int;
132      Barley : Int;
133      Wheat : Int;
134      Hops : Int;
135      Yeast : Int;
136      WaterTmp : Int;
137      MashTmp : Int;
138      MashTime : Int;
139   END_STRUCT;
140END_TYPE
141 

FB_Recipe / 边沿与复位 — 一次请求只启动一次

FB_Recipe.scl · 边沿与复位
41   REGION 边沿与复位
42      // 导出/导入都只响应一次上升沿;REQ 给单周期脉冲,实例在后台继续执行。
43      #ExportPulse := #ExportReq AND NOT #ExportPrev;
44      #ExportPrev := #ExportReq;
45      #ImportPulse := #ImportReq AND NOT #ImportPrev;
46      #ImportPrev := #ImportReq;
47 
48      IF #Reset THEN
49         #ModeInt := 0;
50         #StatusInt := 16#0000;
51         #LastStatusInt := 16#0000;
52         #ErrorLatched := FALSE;
53      END_IF;
54 
55      #Done := FALSE;
56      #Busy := FALSE;
57   END_REGION

目的:把 HMI 的导出/导入电平转换成单周期 REQ,并处理 Reset。

条件 → 动作:ExportPulse := ExportReq AND NOT ExportPrev;ImportPulse 同理;Reset=1 时把 Mode=0、Status=0、LastStatus=0、ErrorLatched=FALSE;每周期先清 Done / Busy。

为什么:RecipeExport/Import 的 REQ 是上升沿触发,给一个周期就够;一直给 1 会反复启动。Reset 是清除错误锁存的唯一入口。

注意:ExportPrev / ImportPrev 必须是静态变量;Done 是单周期脉冲,HMI 要自锁或用上升沿。

FB_Recipe / 导出 — RecipeExport

FB_Recipe.scl · 导出
59   REGION 导出
60      // 只有空闲且没有未复位错误时才允许开始新的导出。
61      IF #ExportPulse AND #ModeInt = 0 AND NOT #ErrorLatched THEN
62         #ModeInt := 1;
63         #StatusInt := 16#0000;
64      END_IF;
65 
66      #ExportInst(REQ := #ExportPulse,
67         RECIPE_DB := "DB_Recipe",
68         DONE => #ExportDoneInt,
69         BUSY => #ExportBusyInt,
70         ERROR => #ExportErrorInt,
71         STATUS => #ExportStatusInt);
72 
73      IF #ModeInt = 1 THEN
74         #Busy := #ExportBusyInt;
75         #StatusInt := #ExportStatusInt;
76         IF #ExportDoneInt THEN
77            #ModeInt := 0;
78            #Done := TRUE;
79            #LastStatusInt := #ExportStatusInt;
80         ELSIF #ExportErrorInt THEN
81            #ModeInt := 0;
82            #ErrorLatched := TRUE;
83            #LastStatusInt := #ExportStatusInt;
84         END_IF;
85      END_IF;
86   END_REGION

目的:把 DB_Recipe 的 5 条配方写成 CSV。

条件 → 动作:空闲、无未复位错误、收到 ExportPulse → Mode=1;调用 RecipeExport,REQ := ExportPulse、RECIPE_DB := "DB_Recipe";DONE → Mode=0、Done=TRUE、LastStatus=STATUS;ERROR → Mode=0、ErrorLatched=TRUE。

为什么:CSV 文件名与配方 DB 同名(DB_Recipe.csv),列顺序由 RecipeUDT 的成员顺序决定;导出前把配方值写进 DB 即可。

注意:导出错误常见于文件名非法(16#8090)、DB 结构不可处理(16#8091)、DB 超过 5000 字节(16#8092)、存储卡写保护(16#80B4)。STATUS 只在一个周期有效,所以本页用 LastStatus 锁存。

FB_Recipe / 导入 — RecipeImport

FB_Recipe.scl · 导入
88   REGION 导入
89      // 导入与导出共用 Mode,保证同一时间只有一个 Recipe 指令在跑。
90      IF #ImportPulse AND #ModeInt = 0 AND NOT #ErrorLatched THEN
91         #ModeInt := 2;
92         #StatusInt := 16#0000;
93      END_IF;
94 
95      #ImportInst(REQ := #ImportPulse,
96         RECIPE_DB := "DB_Recipe",
97         DONE => #ImportDoneInt,
98         BUSY => #ImportBusyInt,
99         ERROR => #ImportErrorInt,
100         STATUS => #ImportStatusInt);
101 
102      IF #ModeInt = 2 THEN
103         #Busy := #ImportBusyInt;
104         #StatusInt := #ImportStatusInt;
105         IF #ImportDoneInt THEN
106            #ModeInt := 0;
107            #Done := TRUE;
108            #LastStatusInt := #ImportStatusInt;
109         ELSIF #ImportErrorInt THEN
110            #ModeInt := 0;
111            #ErrorLatched := TRUE;
112            #LastStatusInt := #ImportStatusInt;
113         END_IF;
114      END_IF;
115   END_REGION

目的:把编辑后的 CSV 读回 DB_Recipe。

条件 → 动作:空闲、无未复位错误、收到 ImportPulse → Mode=2;调用 RecipeImport,REQ := ImportPulse、RECIPE_DB := "DB_Recipe";DONE → Mode=0、Done=TRUE、LastStatus=STATUS;ERROR → Mode=0、ErrorLatched=TRUE。

为什么:导入前必须先删除旧的 CSV(或在 Web 服务器里覆盖),否则 CPU 可能读到旧文件;CSV 的列结构必须和 RecipeUDT 完全一致。

注意:原工程的导入 REQ 写的是 #importDataUDT.req AND NOT #exportDataUDT.busy——用导出的 busy 来判断导入,导出运行时导入会被错误阻塞,导入运行时也可能重复触发。本页改成共用 Mode 忙锁,并让导入只检查自己的状态。

FB_Recipe / 状态输出 — Ready / Busy / Done / Error

FB_Recipe.scl · 状态输出
117   REGION 状态输出
118      // Error 保持到 Reset;Done 只在完成的那一个扫描周期为 1。
119      #Error := #ErrorLatched;
120      #Ready := (#ModeInt = 0) AND NOT #ErrorLatched;
121      #Status := #StatusInt;
122      #LastStatus := #LastStatusInt;
123      #Mode := #ModeInt;
124   END_REGION

目的:给 HMI 一个稳定的状态集合。

条件 → 动作:Error := ErrorLatched;Ready := (Mode=0) AND NOT ErrorLatched;Status := StatusInt;LastStatus := LastStatusInt;Mode := ModeInt。

为什么:Done 只闪一个周期,HMI 用 LastStatus 显示“上次结果”;Error 保持到 Reset,避免操作员错过错误。

注意:Ready=0 不代表一定有错;导出/导入进行中 Busy=1、Ready=0。HMI 应同时看 Busy 和 Error。

DB_Recipe DB

标准访问配方 DB,Array[1..5] of RecipeUDT · 9 行 · 源文件 blocks/DB_Recipe.scl

查看 / 复制完整块源码
DB_Recipe.scl
1DATA_BLOCK "DB_Recipe"
2{ S7_Optimized_Access := 'FALSE' }
3VERSION : 0.1
4   VAR
5      Recipes : Array[1..5] of "RecipeUDT";   // 最多 5 条配方
6   END_VAR
7BEGIN
8END_DATA_BLOCK
9 

DB_Recipe / 整体逻辑 — 标准访问 + UDT 数组

DB_Recipe.scl · 整体逻辑
8END_DATA_BLOCK

目的:给 RecipeExport/Import 提供一个可被 VARIANT 引用的配方 DB。

条件 → 动作:S7_Optimized_Access := 'FALSE';Recipes : Array[1..5] of "RecipeUDT";每条记录包含 9 个字段。

为什么:RECIPE_DB 是 VARIANT,TIA 只接受基于 PLC 数据类型(UDT)或系统数据类型的标准访问 DB。匿名 STRUCT 或优化访问 DB 会报 24/51 错误。

注意:数组上限 1..5 就是最大配方数;CSV 里的记录数不能超过它。修改 RecipeUDT 的成员顺序/类型后,旧 CSV 不再兼容,必须重新导出模板。

仿真验证

本示例依赖 TIA Portal 系统库或硬件环境,暂不在云端仿真中执行。请使用上方 TiaLink 面板导入 TIA,在真实编译/仿真环境中验证。

关键 SCL 语法

写法 含义
RecipeExport / RecipeImport TIA 配方文件指令,实例放静态 VAR
RECIPE_DB := "DB_Recipe" 指向标准访问、基于 UDT 的配方 DB
Array[1..5] of "RecipeUDT" 5 条配方的数组
TYPE "RecipeUDT" ... END_TYPE PLC 数据类型,定义 CSV 的列
REQ := #ExportPulse 上升沿启动,单周期
DONE => #DoneInt 指令输出接变量
VARIANT RecipeExport/Import 的 InOut 参数类型

常见坑

  • 配方 DB 用优化访问:VARIANT 报“Only data blocks based on a PLC data type…”,必须 S7_Optimized_Access := 'FALSE'。
  • DB 用匿名 STRUCT:即使标准访问,也不满足“基于 UDT”的要求;必须 Array[1..5] of "RecipeUDT"。
  • 先导入 DB 再导入 UDT:DB 生成时找不到 RecipeUDT,外部源报“数据类型未知”;必须先导入带 UDT 的 FB 文件。
  • 导入 REQ 用了导出的 busy:原工程 bug;导入应检查导入自己的 BUSY,并用共用忙锁防止两个指令同时跑。
  • CSV 列结构和 UDT 不一致:导入报 16#8091 或数据错位;改 UDT 后要重新导出模板。
  • 文件名/DB 名不一致:RecipeExport 生成的 CSV 名就是 DB 名;在 Web 服务器里找不到文件时先检查 DB 名。
  • 没有启用 Web 服务器/权限:CSV 文件无法下载或覆盖;需要在 CPU 属性里启用 Web 服务器并配置读写权限。
  • 把配方当安全参数:RecipeExport/Import 不适合需要高保护的数据;高保护场景用 HMI 配方管理。

变体与扩展

  • 多配方 DB:每个产品族一个 DB,用 RecipeDB 选择;或者把 Array[1..N] 扩大。
  • 状态报警:原工程用 Program_Alarm 把导出/导入状态发成报警;本页用 Status / LastStatus 输出,减少系统库依赖。
  • HMI 配方管理:HMI 配方视图可以直接编辑 CSV;PLC 只负责导出/导入和握手。
  • 自动备份:导出成功后把 CSV 复制到另一张存储卡/网盘;PLC 侧只保证 DONE 和 STATUS。
  • 真实项目要先在 CPU 属性中启用 Web 服务器,并配置配方目录的读写权限,否则 CSV 无法下载/覆盖。

常见问题

为什么配方 DB 必须用标准访问?

RecipeExport/RecipeImport 的 RECIPE_DB 是 VARIANT,TIA 只接受基于 PLC 数据类型(UDT)或系统数据类型的标准访问 DB。优化访问的匿名 STRUCT DB 会报“Only data blocks based on a PLC data type or a system data type can be addressed with the data type VARIANT”。

为什么 FB 文件里还带着 RecipeUDT?

本站的构建器每个 .scl 只解析一个块,不能单独放 TYPE 文件;把 RecipeUDT 追加在 FB 后面,TIA 外部源会一次生成 FB + UDT,DB 再引用这个 UDT。导入顺序必须先 FB 再 DB,且编译放在所有文件导入之后。

验证状态

项目结果日期证据
本地仿真场景未启用—TIA-only 示例,不提供仿真请求
PLC 云仿真未跑—`scl_run_scenario`(托管 MCP)
TIA 导入编译PASS(0 error / 0 warning)2026-11-01TIA-only 示例:本地/云仿真未启用;TIA Portal V19 + TiaLink 1.4.0.0 导入 PLC_2,FB_Recipe + RecipeUDT + DB_Recipe 编译 0 错误 / 0 警告,已保存。

本地仿真引擎验证语法、结构和逐扫描行为;TIA 编译验证在 Windows + TIA Portal + TiaLink 上完成。两者不能互相替代。

来源与许可

本页示例改编自 github.com/lopez-dev/Siemens-SCL-Source-Files 的 rezeptExportUndImport/rezept_ex-import.scl,原作者 Daniel Cáceres López,以 MIT 许可发布。中文注释、接口调整、仿真场景与页面讲解由 Learn-SCL 编写。

原文件用 exportDataUDT/importDataUDT + Program_Alarm + Variant DB;本站改成 DB_Recipe + FB_Recipe,修正导入 REQ 误用导出 busy 的问题,并保留 RecipeExport/RecipeImport 指令。

示例源码以 MIT 许可发布(上游仓库);中文讲解与页面内容版权归汇智网(Hubwiz)。官方参考:Siemens TIA Portal。

把这个示例带进你的工程

TiaLink

把 TIA Portal 接到你的智能体:导入、编译、读回核对、PLCSIM 仿真。本地运行,工程数据不出厂。

了解 TiaLink →

PLC 云仿真

不装博途,把 SCL 放到云端逐扫描执行:校验、仿真、场景验收。新用户 100 次免费。

打开云仿真 →

Learn-LAD

同一个团队的梯形图示例库:逐网络讲解、变量表、TiaLink 一键导入。

浏览 LAD 示例 →