ST报警系统构建:基于数组和结构体的循环报警记录功能
在工业现场,PLC(可编程逻辑控制器)需持续监控设备状态,一旦检测到异常(如电机过热、液位超限、通信中断),必须立即响应并留存可追溯的报警信息。传统做法常将报警标志位单独定义为布尔变量,但这种方式存在明显缺陷:无法记录发生时间、无法回溯历史、无法区分同一类报警的多次触发、难以实现报警确认与复位管理。本指南以结构化文本(ST,Structured Text)语言为核心,手把手教你构建一个轻量、稳定、可直接部署于主流PLC(如西门子S7-1200/1500、倍福TwinCAT、Codesys平台)的循环报警记录系统。全程不依赖HMI组态或上位软件,所有逻辑在PLC程序中闭环完成。
一、明确核心需求与设计原则
该系统需满足以下5项硬性要求:
- 自动循环覆盖:报警记录容量固定(例如100条),新报警写入时,最旧记录自动移出,避免内存溢出;
- 完整上下文留存:每条记录必须包含报警ID、发生时间戳(毫秒级)、确认状态、复位状态、附加描述文本(可选);
- 快速定位与读取:支持按索引随机访问任意一条记录,也支持按“最新未确认”条件顺序扫描;
- 低资源占用:不使用动态内存分配,全部基于静态数组,编译时确定大小;
- ST原生兼容:仅使用IEC 61131-3标准语法,禁用平台私有指令或非标函数块。
关键设计决策:
- 不用DB块嵌套引用:避免跨块寻址延迟与调试复杂度;
- 不用字符串数组存描述:PLC中字符串操作开销大且易越界,改用报警ID查表映射;
- 时间戳不依赖系统时钟读取:每次报警触发时,调用
GET_SYSTEM_TIME_HIGH_RES()函数获取64位纳秒级时间,再转换为毫秒整数,确保精度与一致性; - 确认/复位状态分离:
bConfirmed表示操作员已点击“确认”,bReset表示故障已消除且允许清除记录,二者独立控制。
二、定义报警结构体(UDT)
首先创建用户自定义类型(User-Defined Type),命名为ALARM_ENTRY。该类型封装单条报警的全部属性:
TYPE ALARM_ENTRY :
STRUCT
nID : INT; // 报警唯一编号(例:101=电机过载,102=泵停机)
dwTimestamp_ms : DWORD; // 发生时刻,单位:毫秒(从PLC启动起累计)
bActive : BOOL; // 当前是否仍处于激活态(故障未消失)
bConfirmed : BOOL; // 是否已被操作员确认
bReset : BOOL; // 是否已复位(故障已解除,记录可被覆盖)
nPriority : BYTE; // 优先级(0=低,1=中,2=高,3=紧急)
END_STRUCT
END_TYPE
说明:
dwTimestamp_ms使用DWORD(32位无符号整数)足够覆盖约49天毫秒计时,远超单次运行周期;若需长期运行,可升级为LINT,但多数场景无需;bActive与bReset逻辑解耦:即使故障已消失(bActive := FALSE),若未手动复位(bReset := FALSE),该记录仍保留在列表中,供审计;nPriority用于后续HMI排序或声光提示分级,此处仅存储,不参与核心逻辑。
三、声明循环报警数组与控制变量
声明一个固定长度的结构体数组,并配套索引管理变量:
// 报警记录主数组,容量100条
aAlarms : ARRAY[0..99] OF ALARM_ENTRY;
// 控制变量
iNextWriteIndex : INT := 0; // 下一条新报警将写入的索引位置(0~99循环)
iRecordCount : INT := 0; // 当前有效记录总数(≤100,新记录写入时+1,满后不再增加)
bAlarmTriggered : BOOL := FALSE; // 外部报警信号输入(由工艺逻辑置位)
nAlarmID : INT := 0; // 触发的报警ID(由工艺逻辑提供)
关键机制说明:
iNextWriteIndex采用模运算实现循环:写入后执行iNextWriteIndex := (iNextWriteIndex + 1) MOD 100;;iRecordCount仅在数组未满时递增,满后保持为100,避免误判“空记录”;bAlarmTriggered必须为上升沿触发,防止同一故障连续扫描导致重复写入。实际使用中,应接R_TRIG函数块的Q输出。
四、编写核心报警写入逻辑(ST代码段)
将以下代码放入主程序组织块(OB1)或专用报警处理FB中:
// 步骤1:检测报警上升沿
bAlarmEdge := bAlarmTriggered AND NOT bAlarmTriggered_PRV;
bAlarmTriggered_PRV := bAlarmTriggered;
// 步骤2:若检测到新报警,执行写入
IF bAlarmEdge THEN
// 初始化新记录
aAlarms[iNextWriteIndex].nID := nAlarmID;
aAlarms[iNextWriteIndex].dwTimestamp_ms :=
DWORD(REAL_TO_DWORD(GET_SYSTEM_TIME_HIGH_RES() / 1000000.0)); // 纳秒→毫秒
aAlarms[iNextWriteIndex].bActive := TRUE;
aAlarms[iNextWriteIndex].bConfirmed := FALSE;
aAlarms[iNextWriteIndex].bReset := FALSE;
// 设置优先级(此处以ID范围映射,可按需修改)
IF nAlarmID >= 100 AND nAlarmID <= 199 THEN
aAlarms[iNextWriteIndex].nPriority := 2; // 中优先级
ELSIF nAlarmID >= 200 AND nAlarmID <= 299 THEN
aAlarms[iNextWriteIndex].nPriority := 3; // 紧急
ELSE
aAlarms[iNextWriteIndex].nPriority := 1;
END_IF;
// 更新索引与计数
iNextWriteIndex := (iNextWriteIndex + 1) MOD 100;
IF iRecordCount < 100 THEN
iRecordCount := iRecordCount + 1;
END_IF;
END_IF;
注意:
bAlarmTriggered_PRV需声明为STATIC变量(在FB中)或全局VAR(在OB中),确保断电保持状态;- 时间转换使用
REAL_TO_DWORD而非DINT_TO_DWORD,因GET_SYSTEM_TIME_HIGH_RES()返回LINT,需先转REAL再截断; MOD运算是ST标准算符,所有符合IEC 61131-3的PLC均支持。
五、实现报警确认与复位逻辑
操作员通过HMI按钮发送确认/复位指令,对应PLC输入点(如bConfirmCmd、bResetCmd)。为防抖与单次触发,同样使用上升沿:
// 确认命令处理(对最新未确认报警操作)
IF bConfirmCmd AND NOT bConfirmCmd_PRV THEN
// 从最新记录向前扫描,找到第一条bActive=TRUE且bConfirmed=FALSE的记录
FOR i := 0 TO iRecordCount - 1 DO
idx := (iNextWriteIndex + 99 - i) MOD 100; // 逆序索引:最新在前
IF aAlarms[idx].bActive AND NOT aAlarms[idx].bConfirmed THEN
aAlarms[idx].bConfirmed := TRUE;
EXIT; // 找到即停
END_IF;
END_FOR;
END_IF;
bConfirmCmd_PRV := bConfirmCmd;
// 复位命令处理(对指定ID或全部已确认报警)
IF bResetCmd AND NOT bResetCmd_PRV THEN
// 方式1:复位当前ID对应的全部记录(常见于单点故障)
FOR i := 0 TO 99 DO
IF aAlarms[i].nID = nResetTargetID THEN // nResetTargetID由HMI传入
aAlarms[i].bReset := TRUE;
END_IF;
END_FOR;
// 方式2(备选):复位所有已确认且已消失的报警
(*
FOR i := 0 TO 99 DO
IF aAlarms[i].bConfirmed AND NOT aAlarms[i].bActive THEN
aAlarms[i].bReset := TRUE;
END_IF;
END_FOR;
*)
END_IF;
bResetCmd_PRV := bResetCmd;
说明:
- 确认逻辑采用逆序扫描,确保总是确认“最新未确认”的那一条,符合操作员直觉;
- 复位目标
nResetTargetID需由HMI显式传递,避免误清其他报警; - 注释掉的备选方式适用于“一键清除所有已解决报警”,按需启用。
六、提供安全的数据读取接口
外部模块(如HMI通信FB或数据归档FB)需安全读取报警列表。禁止直接暴露数组地址,必须通过受控函数:
// 函数:读取指定索引的报警记录(带有效性检查)
FUNCTION GetAlarmAt : ALARM_ENTRY
VAR_INPUT
iIndex : INT; // 请求索引(0~99)
END_VAR
VAR
bValid : BOOL;
END_VAR
bValid := (iIndex >= 0) AND (iIndex < 100) AND (iIndex < iRecordCount);
IF bValid THEN
GetAlarmAt := aAlarms[iIndex];
ELSE
// 返回空记录(ID=0, Timestamp=0, 其余FALSE)
GetAlarmAt.nID := 0;
GetAlarmAt.dwTimestamp_ms := 0;
GetAlarmAt.bActive := FALSE;
GetAlarmAt.bConfirmed := FALSE;
GetAlarmAt.bReset := FALSE;
GetAlarmAt.nPriority := 0;
END_IF;
此函数确保:
- 越界索引返回默认空值,不会引发运行时错误;
- 仅返回已写入的有效记录(
iIndex < iRecordCount); - HMI调用时可直接绑定结构体变量,无需额外判空。
七、报警生命周期状态流转图
为清晰理解各状态关系,以下是报警条目的状态机:
stateDiagram-v2
[*] --> Idle
state Active {
[*] --> ActiveMain
ActiveMain --> ActiveMain : 故障持续\n(bAlarmTriggered = TRUE)
ActiveMain --> Confirmed : 操作员确认\n(bConfirmCmd 上升沿)
ActiveMain --> Reset : 故障消失\n(bAlarmTriggered = FALSE)
}
state Confirmed {
[*] --> ConfirmedMain
ConfirmedMain --> ConfirmedMain : 无变化
ConfirmedMain --> Reset : 故障消失\n(bAlarmTriggered = FALSE)
}
state Reset {
[*] --> ResetMain
ResetMain --> [*] : 被新报警覆盖\n(iNextWriteIndex 指向此位置)
}
Idle --> Active : 检测到故障\n(bAlarmTriggered 上升沿)
Active --> Confirmed : 操作员确认\n(bConfirmCmd 上升沿)
Active --> Reset : 故障消失\n(bAlarmTriggered = FALSE)
Confirmed --> Reset : 故障消失\n(bAlarmTriggered = FALSE)
说明:
Idle为初始态(未触发);Reset态表示该记录已就绪被覆盖,但内容仍可读取,直至新报警写入同一索引;- 所有状态跃迁均由布尔信号上升沿驱动,杜绝毛刺干扰。
八、验证与调试要点
上线前必须验证以下5个关键点:
- 循环覆盖验证:连续触发101次报警,检查第1条记录是否被第101条覆盖,
iRecordCount是否稳定为100; - 时间戳一致性验证:用两路独立报警信号,对比其
dwTimestamp_ms差值是否等于信号间隔(毫秒级); - 确认逻辑验证:触发3次报警,依次确认,检查
bConfirmed标记是否按逆序(最新→次新→最旧)置位; - 复位隔离验证:设置ID=101与ID=102报警各2条,仅对ID=101发复位指令,确认ID=102记录不受影响;
- 边界索引读取验证:调用
GetAlarmAt(iIndex:=99),当iRecordCount=50时,必须返回空记录而非越界数据。
九、扩展建议(按需选用)
- 添加报警屏蔽功能:增加
bMasked : BOOL字段及屏蔽时段表(起始/结束时间戳),在写入前判断是否处于屏蔽期; - 支持报警抑制:当某主设备停机时,自动将关联子设备报警
bActive置FALSE,避免误报; - 导出CSV日志:在FB中集成文件写入逻辑,定时将
bReset=TRUE的记录打包为ALARM_YYYYMMDD.CSV; - 与SMS网关联动:当
nPriority=3且bConfirmed=FALSE持续超5分钟,调用通信FB发送短信。
所有扩展均基于现有结构体字段新增,无需重构数组或索引逻辑。

暂无评论,快来抢沙发吧!