SCL 示例库 / SCL 数据日志:DataLogCreate / Write / Close
SCL 数据日志:DataLogCreate / Write / Close
TIA-only:用 DB 定义日志表头和数据列,一个 FB 管理创建、写入、关闭和错误状态,不依赖云仿真。
综合
S7-1200
S7-1500
DataLog
DB
状态机
TIA-only
2 块 / 178 行
TIA 验证
TIA 已验证
一句话: DataLog 用 DB 定义日志的 HEADER 和 DATA 列,用 DataLogCreate 创建文件并取得 ID,DataLogWrite 按 ID 写记录,DataLogClose 关闭;FB 用 0/10/20/30/40/90 状态机管理 REQ/DONE/BUSY/ERROR/STATUS,所有实例每周期都要调用。
导入到我的 TIA 项目
通过 TiaLink 把本示例的 2 个 SCL 块作为“外部源”导入你当前打开的 TIA Portal 项目:自动生成块、编译,并清理外部源。导入后不会自动保存;FB 示例导入后需要你在 OB 里调用 。
导入到我的 TIA 项目
TiaLink 设置
目标 PLC
自动(第一个 PLC)
TIA-only 示例 :本页使用 TIA Portal 的 DataLog 文件系统指令,依赖 CPU 的存储卡/Web 服务器环境;本地 SCL 仿真引擎只有硬件桩,不会真正写文件,所以不提供云仿真场景。验证方式是导入 TIA Portal 编译。
数据日志(DataLog)是 S7-1200/1500 把过程数据写成 CSV 文件的系统功能。它不像普通数组那样“写内存”,而是由 CPU 的文件系统管理:先创建日志文件,拿到一个 ID,之后每次写入都用这个 ID,最后关闭文件。
原工程 LogMsg.scl 是一个 49KB 的日志框架,包含创建、写入、关闭、打开、新建文件、删除、缓冲队列和状态文本。本页抽取最常用的 Create → Write → Close 主链路,整理成 DB_DataLog + FB_DataLog 两个块,既能在 TIA 里编译,也方便逐段理解。
DB 定义列;FB 在 0/10/20/30/40/90 状态间推进,DataLogCreate → Write → Close 都按 ID 操作。
概述
DB_DataLog:定义日志文件的表头 Header 和数据列 Data。Data 里的字段就是 CSV 的列:ID、Value、Text。
FB_DataLog:管理日志生命周期,状态码:
状态
含义
0
空闲,可以创建
10
正在创建(DataLogCreate)
20
已创建,可以写入
30
正在写入(DataLogWrite)
40
正在关闭(DataLogClose)
90
错误,等待关闭/复位
Create / Write / Close 都取上升沿,避免 HMI 电平在一个扫描周期里重复触发。每次操作成功,Done 只在一个扫描周期为 1;Error 在状态 90 保持,Status 保存最后一次错误码。
你将学到
DataLogCreate / DataLogWrite / DataLogClose 的参数和调用顺序;
为什么 HEADER / DATA 必须放在 DB 里;
如何用状态机管理 REQ / DONE / BUSY / ERROR / STATUS;
为什么所有 DataLog 实例必须每个扫描周期都调用;
日志 ID 和写入计数的保存方式。
本页目录
接口与变量
下表由构建器从 SCL 源码的接口声明自动生成;页面上展示的代码就是导入 TIA 的同一份源码。
DB_DataLog DB
区块 名称 类型 说明 静态 HeaderSTRUCT静态 DataSTRUCT
FB_DataLog FB
区块 名称 类型 说明 输入 CreateBool输入 WriteBool输入 CloseBool输入 FileNameSTRING[64]输入 RecordIDUInt输入 RecordValueReal输入 RecordTextSTRING[64]输出 ReadyBool输出 BusyBool输出 DoneBool输出 ErrorBool输出 StatusWord输出 LogIDDWord输出 WrittenUInt输出 StateInt静态 CreateInst"DataLogCreate"静态 WriteInst"DataLogWrite"静态 CloseInst"DataLogClose"静态 CreatePrevBool静态 WritePrevBool静态 ClosePrevBool静态 CreatePulseBool静态 WritePulseBool静态 ClosePulseBool静态 CreateDoneIntBool静态 CreateBusyIntBool静态 CreateErrorIntBool静态 CreateStatusIntWord静态 WriteDoneIntBool静态 WriteBusyIntBool静态 WriteErrorIntBool静态 WriteStatusIntWord静态 CloseDoneIntBool静态 CloseBusyIntBool静态 CloseErrorIntBool静态 CloseStatusIntWord静态 LogIDIntDWord静态 WrittenIntUInt静态 StateIntInt静态 StatusIntWord
程序结构
导入顺序以下表为准:被依赖的块/类型先导入;本页已按依赖排好。
DB_DataLog DB 日志表头和数据列 · 16 行 · 1 段 FB_DataLog FB 创建/写入/关闭状态机 · 162 行 · 5 段
DB_DataLog DB
日志表头和数据列 · 16 行 · 源文件 blocks/DB_DataLog.scl
查看 / 复制完整块源码
DB_DataLog.scl 复制
1 DATA_BLOCK "DB_DataLog"
2 { S7_Optimized_Access := 'TRUE' }
3 VERSION : 0.1
4 VAR
5 Header : Struct
6 Site : String [ 32 ] ;
7 END_STRUCT ;
8 Data : Struct
9 ID : UInt ;
10 Value : Real ;
11 Text : String [ 64 ] ;
12 END_STRUCT ;
13 END_VAR
14 BEGIN
15 END_DATA_BLOCK
16
DB_DataLog / 整体逻辑 — 日志的列定义
DB_DataLog.scl · 整体逻辑 复制
15 END_DATA_BLOCK
目的 :给 DataLogCreate 提供表头和数据列。
条件 → 动作 :Header 里放 Site / Line,Data 里放 ID / Value / Text;FB 在写入前把值填进 "DB_DataLog".Data。
为什么 :DataLogCreate 的 HEADER 和 DATA 参数要求指向 DB 中的结构;如果传局部变量或临时变量,运行时会报“指向本地内存”的错误(例如 16#8B24 / 16#8C24)。
注意 :Data 的字段顺序就是 CSV 的列顺序。增删字段后,已经存在的日志文件定义会不匹配,DataLogWrite 可能返回 16#8093;这时要新建日志文件。
FB_DataLog FB
创建/写入/关闭状态机 · 162 行 · 源文件 blocks/FB_DataLog.scl
查看 / 复制完整块源码
FB_DataLog.scl 复制
1 FUNCTION_BLOCK "FB_DataLog"
2 { S7_Optimized_Access := 'TRUE' }
3 VERSION : 0.1
4 VAR_INPUT
5 Create : Bool ;
6 Write : Bool ;
7 Close : Bool ;
8 FileName : String [ 64 ] := 'LearnSCL_Log' ;
9 RecordID : UInt ;
10 RecordValue : Real ;
11 RecordText : String [ 64 ] ;
12 END_VAR
13
14 VAR_OUTPUT
15 Ready : Bool ;
16 Busy : Bool ;
17 Done : Bool ;
18 Error : Bool ;
19 Status : Word ;
20 LogID : DWord ;
21 Written : UInt ;
22 State : Int ;
23 END_VAR
24
25 VAR
26 CreateInst : DataLogCreate;
27 WriteInst : DataLogWrite;
28 CloseInst : DataLogClose;
29 CreatePrev : Bool ;
30 WritePrev : Bool ;
31 ClosePrev : Bool ;
32 CreatePulse : Bool ;
33 WritePulse : Bool ;
34 ClosePulse : Bool ;
35 CreateDoneInt : Bool ;
36 CreateBusyInt : Bool ;
37 CreateErrorInt : Bool ;
38 CreateStatusInt : Word ;
39 WriteDoneInt : Bool ;
40 WriteBusyInt : Bool ;
41 WriteErrorInt : Bool ;
42 WriteStatusInt : Word ;
43 CloseDoneInt : Bool ;
44 CloseBusyInt : Bool ;
45 CloseErrorInt : Bool ;
46 CloseStatusInt : Word ;
47 LogIDInt : DWord := 0 ;
48 WrittenInt : UInt := 0 ;
49 StateInt : Int := 0 ;
50 StatusInt : Word := 16#0000 ;
51 END_VAR
52 BEGIN
53 REGION 边沿与复位
54 #CreatePulse := #Create AND NOT #CreatePrev ;
55 #CreatePrev := #Create ;
56 #WritePulse := #Write AND NOT #WritePrev ;
57 #WritePrev := #Write ;
58 #ClosePulse := #Close AND NOT #ClosePrev ;
59 #ClosePrev := #Close ;
60
61 #Done := FALSE ;
62 #Busy := FALSE ;
63 #CreateDoneInt := FALSE ;
64 #WriteDoneInt := FALSE ;
65 #CloseDoneInt := FALSE ;
66 END_REGION
67
68 REGION 创建日志
69 IF #CreatePulse AND #StateInt = 0 THEN
70 #StateInt := 10 ;
71 #StatusInt := 16#0000 ;
72 END_IF ;
73
74 #CreateInst ( REQ := #StateInt = 10 ,
75 RECORDS := 1000 ,
76 FORMAT := 1 ,
77 TIMESTAMP := 4 ,
78 NAME := #FileName ,
79 ID := #LogIDInt ,
80 HEADER := "DB_DataLog" . Header,
81 DATA := "DB_DataLog" . Data,
82 DONE => #CreateDoneInt ,
83 BUSY => #CreateBusyInt ,
84 ERROR => #CreateErrorInt ,
85 STATUS => #CreateStatusInt ) ;
86
87 IF #StateInt = 10 THEN
88 #Busy := #CreateBusyInt ;
89 IF #CreateDoneInt THEN
90 #StateInt := 20 ;
91 #Done := TRUE ;
92 ELSIF #CreateErrorInt THEN
93 #StateInt := 90 ;
94 #StatusInt := #CreateStatusInt ;
95 END_IF ;
96 END_IF ;
97 END_REGION
98
99 REGION 写入记录
100 IF #WritePulse AND #StateInt = 20 THEN
101 "DB_DataLog" . Data. ID := #RecordID ;
102 "DB_DataLog" . Data. Value := #RecordValue ;
103 "DB_DataLog" . Data. Text := #RecordText ;
104 #StateInt := 30 ;
105 END_IF ;
106
107 #WriteInst ( REQ := #StateInt = 30 ,
108 ID := #LogIDInt ,
109 DONE => #WriteDoneInt ,
110 BUSY => #WriteBusyInt ,
111 ERROR => #WriteErrorInt ,
112 STATUS => #WriteStatusInt ) ;
113
114 IF #StateInt = 30 THEN
115 #Busy := #WriteBusyInt ;
116 IF #WriteDoneInt THEN
117 #StateInt := 20 ;
118 #WrittenInt := #WrittenInt + 1 ;
119 #Done := TRUE ;
120 ELSIF #WriteErrorInt THEN
121 #StateInt := 90 ;
122 #StatusInt := #WriteStatusInt ;
123 END_IF ;
124 END_IF ;
125 END_REGION
126
127 REGION 关闭日志
128 IF #ClosePulse AND ( #StateInt = 20 OR #StateInt = 90 ) THEN
129 #StateInt := 40 ;
130 END_IF ;
131
132 #CloseInst ( REQ := #StateInt = 40 ,
133 ID := #LogIDInt ,
134 DONE => #CloseDoneInt ,
135 BUSY => #CloseBusyInt ,
136 ERROR => #CloseErrorInt ,
137 STATUS => #CloseStatusInt ) ;
138
139 IF #StateInt = 40 THEN
140 #Busy := #CloseBusyInt ;
141 IF #CloseDoneInt THEN
142 #StateInt := 0 ;
143 #LogIDInt := 0 ;
144 #WrittenInt := 0 ;
145 #Done := TRUE ;
146 ELSIF #CloseErrorInt THEN
147 #StateInt := 90 ;
148 #StatusInt := #CloseStatusInt ;
149 END_IF ;
150 END_IF ;
151 END_REGION
152
153 REGION 状态输出
154 #Ready := #StateInt = 20 ;
155 #Error := #StateInt = 90 ;
156 #Status := #StatusInt ;
157 #LogID := #LogIDInt ;
158 #Written := #WrittenInt ;
159 #State := #StateInt ;
160 END_REGION
161 END_FUNCTION_BLOCK
162
FB_DataLog / 边沿与复位 — 一次请求只执行一次
FB_DataLog.scl · 边沿与复位 复制
53 REGION 边沿与复位
54 #CreatePulse := #Create AND NOT #CreatePrev ;
55 #CreatePrev := #Create ;
56 #WritePulse := #Write AND NOT #WritePrev ;
57 #WritePrev := #Write ;
58 #ClosePulse := #Close AND NOT #ClosePrev ;
59 #ClosePrev := #Close ;
60
61 #Done := FALSE ;
62 #Busy := FALSE ;
63 #CreateDoneInt := FALSE ;
64 #WriteDoneInt := FALSE ;
65 #CloseDoneInt := FALSE ;
66 END_REGION
目的 :把 HMI 的 Create / Write / Close 电平转换成单扫描周期脉冲,并清理上一周期的 Done / Busy 标志。
条件 → 动作 :CreatePulse := Create AND NOT CreatePrev,每个周期更新 CreatePrev;WritePulse / ClosePulse 同理;每个周期先把 Done、Busy 和三个实例的 DONE 中间变量清 0。
为什么 :DataLog 指令是异步的,REQ 只需要一个周期;如果一直给 REQ=1,实例会反复启动,状态机无法判断“这一次操作什么时候完成”。
注意 :CreatePrev / WritePrev / ClosePrev 必须是静态变量;放 VAR_TEMP 会每个周期重建,边沿永远检测不到。
FB_DataLog / 创建日志 — DataLogCreate
FB_DataLog.scl · 创建日志 复制
68 REGION 创建日志
69 IF #CreatePulse AND #StateInt = 0 THEN
70 #StateInt := 10 ;
71 #StatusInt := 16#0000 ;
72 END_IF ;
73
74 #CreateInst ( REQ := #StateInt = 10 ,
75 RECORDS := 1000 ,
76 FORMAT := 1 ,
77 TIMESTAMP := 4 ,
78 NAME := #FileName ,
79 ID := #LogIDInt ,
80 HEADER := "DB_DataLog" . Header,
81 DATA := "DB_DataLog" . Data,
82 DONE => #CreateDoneInt ,
83 BUSY => #CreateBusyInt ,
84 ERROR => #CreateErrorInt ,
85 STATUS => #CreateStatusInt ) ;
86
87 IF #StateInt = 10 THEN
88 #Busy := #CreateBusyInt ;
89 IF #CreateDoneInt THEN
90 #StateInt := 20 ;
91 #Done := TRUE ;
92 ELSIF #CreateErrorInt THEN
93 #StateInt := 90 ;
94 #StatusInt := #CreateStatusInt ;
95 END_IF ;
96 END_IF ;
97 END_REGION
目的 :创建日志文件并取得 ID。
条件 → 动作 :状态 0 收到 CreatePulse → 状态 10;调用 DataLogCreate,REQ := State=10,RECORDS := 1000、FORMAT := 1、TIMESTAMP := 4、NAME := FileName、HEADER := "DB_DataLog".Header、DATA := "DB_DataLog".Data、ID := LogIDInt;DONE → 状态 20,ERROR → 状态 90。
为什么 :ID 是后续 Write/Close 的句柄,必须在 Create 成功后保存;RECORDS 是文件容量,满了以后 DataLogWrite 会返回 16#0001(覆盖最旧记录)。
注意 :DataLogCreate 实例要每个周期调用,即使 REQ=0;TIA 需要在每个扫描周期更新实例状态。创建失败时把 STATUS 保存到 StatusInt,不要只闪一个周期。
FB_DataLog / 写入记录 — DataLogWrite
FB_DataLog.scl · 写入记录 复制
99 REGION 写入记录
100 IF #WritePulse AND #StateInt = 20 THEN
101 "DB_DataLog" . Data. ID := #RecordID ;
102 "DB_DataLog" . Data. Value := #RecordValue ;
103 "DB_DataLog" . Data. Text := #RecordText ;
104 #StateInt := 30 ;
105 END_IF ;
106
107 #WriteInst ( REQ := #StateInt = 30 ,
108 ID := #LogIDInt ,
109 DONE => #WriteDoneInt ,
110 BUSY => #WriteBusyInt ,
111 ERROR => #WriteErrorInt ,
112 STATUS => #WriteStatusInt ) ;
113
114 IF #StateInt = 30 THEN
115 #Busy := #WriteBusyInt ;
116 IF #WriteDoneInt THEN
117 #StateInt := 20 ;
118 #WrittenInt := #WrittenInt + 1 ;
119 #Done := TRUE ;
120 ELSIF #WriteErrorInt THEN
121 #StateInt := 90 ;
122 #StatusInt := #WriteStatusInt ;
123 END_IF ;
124 END_IF ;
125 END_REGION
目的 :把一条记录写进已经创建的日志。
条件 → 动作 :状态 20 收到 WritePulse → 把 RecordID / RecordValue / RecordText 填进 "DB_DataLog".Data → 状态 30;调用 DataLogWrite,REQ := State=30、ID := LogIDInt;DONE → 状态 20,WrittenInt := WrittenInt + 1;ERROR → 状态 90。
为什么 :DataLogWrite 不接收数据参数,它写的是创建时 DATA 指向的 DB 当前值;所以必须先填 DB,再给 REQ。
注意 :不要在 REQ=1 的同一个周期修改 "DB_DataLog".Data 的字段顺序或类型;DataLog 的列定义在创建时已经固定。写入失败时状态 90,需要先 Close 再重新 Create。
FB_DataLog / 关闭日志 — DataLogClose
FB_DataLog.scl · 关闭日志 复制
127 REGION 关闭日志
128 IF #ClosePulse AND ( #StateInt = 20 OR #StateInt = 90 ) THEN
129 #StateInt := 40 ;
130 END_IF ;
131
132 #CloseInst ( REQ := #StateInt = 40 ,
133 ID := #LogIDInt ,
134 DONE => #CloseDoneInt ,
135 BUSY => #CloseBusyInt ,
136 ERROR => #CloseErrorInt ,
137 STATUS => #CloseStatusInt ) ;
138
139 IF #StateInt = 40 THEN
140 #Busy := #CloseBusyInt ;
141 IF #CloseDoneInt THEN
142 #StateInt := 0 ;
143 #LogIDInt := 0 ;
144 #WrittenInt := 0 ;
145 #Done := TRUE ;
146 ELSIF #CloseErrorInt THEN
147 #StateInt := 90 ;
148 #StatusInt := #CloseStatusInt ;
149 END_IF ;
150 END_IF ;
151 END_REGION
目的 :关闭文件,释放日志 ID。
条件 → 动作 :状态 20 或 90 收到 ClosePulse → 状态 40;调用 DataLogClose,REQ := State=40、ID := LogIDInt;DONE → 状态 0,LogIDInt := 0、WrittenInt := 0;ERROR → 状态 90。
为什么 :关闭会刷新文件系统缓冲;不关闭就断电,最后几条记录可能丢失。错误状态也允许 Close,避免日志一直占着 ID。
注意 :Close 之后 LogIDInt 清 0;下一次 Create 会生成新的 ID。不要用旧的 ID 继续 Write。
FB_DataLog / 状态输出 — 给 HMI 的汇总
FB_DataLog.scl · 状态输出 复制
153 REGION 状态输出
154 #Ready := #StateInt = 20 ;
155 #Error := #StateInt = 90 ;
156 #Status := #StatusInt ;
157 #LogID := #LogIDInt ;
158 #Written := #WrittenInt ;
159 #State := #StateInt ;
160 END_REGION
目的 :把内部状态翻译成 Ready、Busy、Done、Error、Status、LogID、Written。
条件 → 动作 :Ready := State=20;Error := State=90;Status := StatusInt;LogID := LogIDInt;Written := WrittenInt;State := StateInt。
为什么 :HMI 不需要理解 0/10/20/30/40/90,只需要“能不能写”“有没有错”“写了多少条”。
注意 :Done 是单周期脉冲,HMI 要用上升沿或自锁;Error 是状态 90 的电平,直到 Close/重新 Create 才消失。
仿真验证
本示例依赖 TIA Portal 系统库或硬件环境,暂不在云端仿真中执行。请使用上方 TiaLink 面板导入 TIA,在真实编译/仿真环境中验证。
关键 SCL 语法
写法
含义
DataLogCreate / DataLogWrite / DataLogClose
TIA 数据日志指令,实例放在静态 VAR
REQ := State = 10
用比较表达式给指令的请求输入
DONE => #DoneInt
指令输出用 => 接变量
ID := #LogIDInt
日志句柄,Create 后保存,Write/Close 复用
"DB_DataLog".Data.ID := #RecordID
写 DB 的列值
String[64]
文件名/文本列的字符串长度
REGION ... END_REGION
代码分段
常见坑
把 HEADER/DATA 放在局部变量 :运行时报“指向本地内存”;必须用 DB 中的结构。
不每个周期调用实例 :DataLog 实例需要周期调用才能更新 BUSY/DONE;只在 REQ 时调用会卡住。
REQ 一直为 1 :实例反复启动,状态机永远等不到稳定的 DONE;必须用单周期脉冲。
Create 后不保存 ID :Write/Close 用 0 或旧 ID,报“日志不存在”。
写入前不填 DB :CSV 里全是上一次的旧值;DataLogWrite 不接收数据参数。
不关闭就断电 :文件缓冲没有刷新,最后几条记录可能丢失。
改 DB 列定义后继续写旧文件 :列不匹配,DataLogWrite 返回 16#8093;重新 Create 新文件。
变体与扩展
自动换文件 :原工程在 DataLogWrite.STATUS = 16#0001(文件满)时触发 DataLogNewFile;本页只保留 Create/Write/Close,换文件可以按同样状态机扩展。
缓冲队列 :原工程先用数组缓冲日志记录,再批量写入,避免高频事件拖慢扫描;本页直接把单条记录写进 DB,适合低频日志。
多日志文件 :把 LogIDInt 做成 Array[0..N] of DWord,每个文件一个 ID;Create/Write/Close 用索引选择。
删除/清空 :TIA 还有 DataLogDelete / DataLogClear;本页不展开,避免权限和文件系统差异。
真实项目里要先在 CPU 属性中启用 Web 服务器并配置访问权限,否则 DataLog 文件无法通过浏览器下载。
常见问题
为什么这个示例不能在云仿真里跑? DataLogCreate/Write/Close 是 TIA Portal 的文件系统指令,依赖 CPU 的存储卡/Web 服务器环境;本地 SCL 仿真引擎只有硬件桩,不会真正写文件。因此本页只做 TIA 导入编译验证,不提供 scl_run_scenario 场景。
DataLogCreate 的 DATA 为什么必须放在 DB 里? DataLogCreate 的 HEADER 和 DATA 参数要求指向 DB 中的结构,不能指向临时变量或局部变量;本页用 DB_DataLog 的 Header/Data 两个 STRUCT 提供列定义,FB 只负责在写入前把值填进 Data。
验证状态
项目 结果 日期 证据 本地仿真场景 未启用 — TIA-only 示例,不提供仿真请求 PLC 云仿真 未跑 — `scl_run_scenario`(托管 MCP) TIA 导入编译 PASS(0 error / 0 warning) 2026-11-01 TIA-only 示例:本地/云仿真未启用;TIA Portal V19 + TiaLink 1.4.0.0 导入 PLC_2,DB_DataLog + FB_DataLog 编译 0 错误 / 0 警告,已保存。
本地仿真引擎验证语法、结构和逐扫描行为;TIA 编译验证在 Windows + TIA Portal + TiaLink 上完成。两者不能互相替代。
来源与许可
本页示例改编自 github.com/lopez-dev/Siemens-SCL-Source-Files 的 LogMsg/LogMsg.scl,原作者 Daniel Cáceres López ,以 MIT 许可发布。中文注释、接口调整、仿真场景与页面讲解由 Learn-SCL 编写。
原文件是 49KB 的日志框架,依赖 DataLogCreate/Write/Close/Open/NewFile/Delete;本站抽取 create/write/close 主链路,整理成 DB + FB 的最小可编译版本。
示例源码以 MIT 许可发布(上游仓库);中文讲解与页面内容版权归汇智网(Hubwiz)。官方参考:Siemens TIA Portal 。