环形队列是PLC数据缓冲的经典方案,它能高效管理连续流入的离散数据(如传感器采样值、通信报文),避免内存碎片和频繁搬移。下面从原理到代码,完整拆解实现过程。
核心原理:为什么选环形队列
普通数组存数据,取出时若搬移后续元素,时间开销随数据量线性增长。环形队列用"头尾指针循环"代替物理搬移,读写都是 $O(1)$ 操作。
想象一个固定容量的数组,写数据时 尾指针 向前移动,读数据时 头指针 向前移动。指针到达数组末尾时,绕回索引0继续。两指针之间的区域就是有效数据。
关键状态判断:
- 队空:
头指针 == 尾指针 - 队满:
(尾指针 + 1) % 容量 == 头指针(牺牲一个单元区分满/空)
硬件与软件环境
| 项目 | 配置示例 |
|---|---|
| PLC型号 | 西门子 S7-1200 / 三菱 FX5U / 欧姆龙 NX1P |
| 编程软件 | TIA Portal / GX Works3 / Sysmac Studio |
| 数据类型 | 以 INT(16位)为例,可替换为 REAL、BYTE 等 |
以下代码以西门子 SCL 语言为主,逻辑适用于任何支持结构化文本的PLC。
第一步:定义数据结构与变量
创建 一个 数据块(DB),命名为 RingBuffer_DB。
TYPE "RingBuffer_INT" // 定义环形队列结构体
STRUCT
Buffer : Array[0..99] of Int; // 存储区,容量100(可改)
Head : Int := 0; // 读指针(头)
Tail : Int := 0; // 写指针(尾)
Count : Int := 0; // 当前元素个数(辅助)
Size : Int := 100; // 总容量(常量)
IsFull : Bool := false; // 满标志
IsEmpty: Bool := true; // 空标志
END_STRUCT;
END_TYPE
声明 全局实例:
// 在全局DB中
Queue1 : "RingBuffer_INT"; // 实例化队列
TempData : Int; // 临时变量
WriteOK : Bool; // 写入成功标志
ReadOK : Bool; // 读取成功标志
第二步:初始化队列
编写 初始化函数 FB_InitQueue,PLC启动时调用一次。
FUNCTION_BLOCK "FB_InitQueue"
VAR_INPUT
Queue : InOut "RingBuffer_INT"; // 传入队列实例
END_VAR
BEGIN
// 清零所有元素
FOR #i := 0 TO #Queue.Size - 1 DO
#Queue.Buffer[#i] := 0;
END_FOR;
#Queue.Head := 0;
#Queue.Tail := 0;
#Queue.Count := 0;
#Queue.IsFull := false;
#Queue.IsEmpty := true;
END_FUNCTION_BLOCK
调用 方式(在 OB100 启动组织块中):
"FB_InitQueue_DB"(Queue := "RingBuffer_DB".Queue1);
第三步:入队操作(写数据)
创建 功能块 FB_Enqueue,处理数据写入。
WriteOK = false"] B -- "IsFull = false" --> D["Buffer[Tail] = Value"] D --> E["Tail = (Tail + 1) % Size"] E --> F["Count = Count + 1"] F --> G{"Count == Size?"} G -- "是" --> H["IsFull = true"] G -- "否" --> I["IsFull = false"] H --> J["IsEmpty = false"] I --> J J --> K["返回成功
WriteOK = true"]
代码实现:
FUNCTION_BLOCK "FB_Enqueue"
VAR_INPUT
Value : Int; // 待写入数据
END_VAR
VAR_OUTPUT
Done : Bool := false; // 完成标志
END_VAR
VAR_IN_OUT
Queue : "RingBuffer_INT";
END_VAR
VAR_TEMP
NextTail : Int;
END_VAR
BEGIN
IF #Queue.IsFull THEN
#Done := false; // 队列满,写入失败
RETURN;
END_IF;
// 写入数据到尾指针位置
#Queue.Buffer[#Queue.Tail] := #Value;
// 计算新尾指针: (Tail + 1) % Size
#NextTail := #Queue.Tail + 1;
IF #NextTail >= #Queue.Size THEN
#NextTail := 0;
END_IF;
#Queue.Tail := #NextTail;
// 更新计数和状态
#Queue.Count := #Queue.Count + 1;
#Queue.IsEmpty := false;
// 检查是否已满
IF #Queue.Count >= #Queue.Size THEN
#Queue.IsFull := true;
ELSE
#Queue.IsFull := false;
END_IF;
#Done := true;
END_FUNCTION_BLOCK
关键公式(模运算实现循环):
$$\text{NextTail} = (\text{Tail} + 1) \bmod \text{Size}$$
PLC中 % 运算符可能不支持,用 IF 判断替代。
第四步:出队操作(读数据)
创建 功能块 FB_Dequeue,处理数据读取并移除。
FUNCTION_BLOCK "FB_Dequeue"
VAR_OUTPUT
Value : Int := 0; // 读出的数据
Valid : Bool := false; // 数据有效标志
END_VAR
VAR_IN_OUT
Queue : "RingBuffer_INT";
END_VAR
VAR_TEMP
NextHead : Int;
END_VAR
BEGIN
IF #Queue.IsEmpty THEN
#Value := 0;
#Valid := false; // 队列空,读取失败
RETURN;
END_IF;
// 从头指针位置读取数据
#Value := #Queue.Buffer[#Queue.Head];
// 计算新头指针: (Head + 1) % Size
#NextHead := #Queue.Head + 1;
IF #NextHead >= #Queue.Size THEN
#NextHead := 0;
END_IF;
#Queue.Head := #NextHead;
// 更新计数和状态
#Queue.Count := #Queue.Count - 1;
#Queue.IsFull := false;
// 检查是否已空
IF #Queue.Count <= 0 THEN
#Queue.IsEmpty := true;
#Queue.Count := 0; // 防负数
ELSE
#Queue.IsEmpty := false;
END_IF;
#Valid := true;
END_FUNCTION_BLOCK
第五步:只读不出队(窥视操作)
有时需查看队首但不移除,添加 FB_Peek:
FUNCTION_BLOCK "FB_Peek"
VAR_OUTPUT
Value : Int := 0;
Valid : Bool := false;
END_VAR
VAR_IN_OUT
Queue : "RingBuffer_INT";
END_VAR
BEGIN
IF #Queue.IsEmpty THEN
#Valid := false;
ELSE
#Value := #Queue.Buffer[#Queue.Head];
#Valid := true;
END_IF;
END_FUNCTION_BLOCK
第六步:高级功能扩展
6.1 批量写入(数组入队)
传感器常以数组形式批量采样,创建 FB_EnqueueArray:
FUNCTION_BLOCK "FB_EnqueueArray"
VAR_INPUT
SrcArray : Array[0..9] of Int; // 源数组,长度10示例
SrcCount : Int := 10; // 实际写入个数
END_VAR
VAR_OUTPUT
Done : Bool := false;
Written : Int := 0; // 实际写入数量
END_VAR
VAR_IN_OUT
Queue : "RingBuffer_INT";
END_VAR
VAR_TEMP
i : Int;
SingleDone : Bool;
END_VAR
BEGIN
#Written := 0;
FOR #i := 0 TO #SrcCount - 1 DO
// 调用单元素入队
"FB_Enqueue_DB"(Value := #SrcArray[#i],
Done => #SingleDone,
Queue => #Queue);
IF NOT #SingleDone THEN // 队列满,停止
EXIT;
END_IF;
#Written := #Written + 1;
END_FOR;
#Done := (#Written == #SrcCount); // 全部写入才算完成
END_FUNCTION_BLOCK
6.2 清空队列
FUNCTION_BLOCK "FB_Clear"
VAR_IN_OUT
Queue : "RingBuffer_INT";
END_VAR
BEGIN
#Queue.Head := #Queue.Tail; // 头尾重合即空
#Queue.Count := 0;
#Queue.IsFull := false;
#Queue.IsEmpty := true;
// 数据区可选择清零或不处理(通常不清,覆盖即可)
END_FUNCTION_BLOCK
第七步:主程序调用示例
在 OB1 循环组织块中组织业务逻辑:
// ==================== 初始化 ====================
// OB100中已完成,此处省略
// ==================== 数据采集周期 ====================
// 假设每10ms执行一次,M0.0为脉冲标志
IF "10msPulse" AND "SensorReady" THEN
"FB_Enqueue_DB"(Value := "AI_Sensor1", // 模拟量输入
Done => "WriteOK",
Queue => "RingBuffer_DB".Queue1);
END_IF;
// ==================== 数据处理周期 ====================
// 假设每100ms处理一次,M0.1为脉冲标志
IF "100msPulse" THEN
// 先窥视,判断是否需要处理
"FB_Peek_DB"(Value => "TempData",
Valid => "PeekOK",
Queue => "RingBuffer_DB".Queue1);
IF "PeekOK" AND "TempData" > 100 THEN // 条件判断示例
// 正式出队并处理
"FB_Dequeue_DB"(Value => "ProcessData",
Valid => "ReadOK",
Queue => "RingBuffer_DB".Queue1);
// 触发后续运算或通信发送
"ProcessTrigger" := true;
END_IF;
END_IF;
// ==================== 溢出保护 ====================
// 队列满时报警或加快处理
IF "RingBuffer_DB".Queue1.IsFull THEN
"BufferOverflowAlarm" := true;
// 可选: 自动清空或加速出队
END_IF;
关键注意事项
| 问题 | 解决方案 |
|---|---|
模运算 % 不支持 |
用 IF 判断指针越界后归零 |
| 多任务竞争(读写冲突) | 用 MUTEX 标志或统一时序调度,确保同一周期只读或只写 |
大数据类型(如 LREAL) |
修改结构体中 Buffer 的元素类型,容量相应减小 |
| 断电保持 | 将队列DB设为 retentive,但指针状态需上电校验 |
| 调试观察 | 在线监控 Head、Tail、Count 三个核心变量 |
容量计算与性能估算
设队列容量为 $N$,采样周期 $T_s$,处理周期 $T_p$,则:
$$\text{最小需求容量} = N \geq \frac{T_p}{T_s} + \text{安全余量}$$
示例:10ms采样、100ms处理,理论需10个单元,实际配置20-50个防止突发堆积。
读写耗时固定为:
- 写入:$t_{write} \approx$ 3-5个PLC扫描周期(含边界判断)
- 读取:$t_{read} \approx$ 2-4个扫描周期
跨平台移植要点
| PLC品牌 | 主要修改点 |
|---|---|
| 西门子 S7-1200/1500 | 直接使用上述SCL代码 |
| 三菱 FX5U/Q系列 | 改为ST语言,数组下标从1开始,指针计算调整 |
| 欧姆龙 NJ/NX | 改为结构化文本,注意 % 运算符可用 |
| 罗克韦尔 ControlLogix | 改为Structured Text,数组维度用 DINT |
| 汇川/信捷等国产 | 参考SCL逻辑,确认是否支持自定义结构体 |
核心算法(头尾指针循环)完全通用,仅语法细节调整。
完整状态机总结
环形队列的实现本质就三层:指针循环计算、边界状态判断、读写原子保护。掌握这套框架,可快速适配任何PLC平台的缓冲需求。

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