文章目录

PLC中数据缓冲区的环形队列实现

发布于 2026-03-23 04:19:27 · 浏览 6 次 · 评论 0 条

环形队列是PLC数据缓冲的经典方案,它能高效管理连续流入的离散数据(如传感器采样值、通信报文),避免内存碎片和频繁搬移。下面从原理到代码,完整拆解实现过程。


核心原理:为什么选环形队列

普通数组存数据,取出时若搬移后续元素,时间开销随数据量线性增长。环形队列用"头尾指针循环"代替物理搬移,读写都是 $O(1)$ 操作。

想象一个固定容量的数组,写数据时 尾指针 向前移动,读数据时 头指针 向前移动。指针到达数组末尾时,绕回索引0继续。两指针之间的区域就是有效数据。

关键状态判断:

  • 队空头指针 == 尾指针
  • 队满(尾指针 + 1) % 容量 == 头指针(牺牲一个单元区分满/空)

硬件与软件环境

项目 配置示例
PLC型号 西门子 S7-1200 / 三菱 FX5U / 欧姆龙 NX1P
编程软件 TIA Portal / GX Works3 / Sysmac Studio
数据类型 INT(16位)为例,可替换为 REALBYTE

以下代码以西门子 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,处理数据写入。

graph TD A["开始: 数据Value待写入"] --> B{"队列已满?"} B -- "IsFull = true" --> C["返回失败
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,但指针状态需上电校验
调试观察 在线监控 HeadTailCount 三个核心变量

容量计算与性能估算

设队列容量为 $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逻辑,确认是否支持自定义结构体

核心算法(头尾指针循环)完全通用,仅语法细节调整。


完整状态机总结

stateDiagram-v2 [*] --> Empty: 初始化 Empty --> Normal: 写入数据 Normal --> Normal: 写入/读取 Normal --> Full: Count达到Size Full --> Normal: 读取数据 Normal --> Empty: 读取至Count=0 Empty --> [*]: 可选销毁

环形队列的实现本质就三层:指针循环计算、边界状态判断、读写原子保护。掌握这套框架,可快速适配任何PLC平台的缓冲需求。

评论 (0)

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

扫一扫,手机查看

扫描上方二维码,在手机上查看本文