题目:
今天写一个很像现场排队叫号的小功能:环形队列。
想象一条输送线,前面扫码器不断扫到工件编号,后面机械手一次只能处理一个工件。扫码速度有时快,机械手动作有时慢。如果程序不做缓存,任务就可能丢、乱、覆盖,最后现场就开始问:刚才那个件去哪了?
这种场景在自动化项目里不少见:扫码结果排队、分拣任务排队、AGV 请求排队、缓存线工件 ID 排队、检测结果排队。
本例用 SCL 写一个 16 个位置的环形队列。新任务从队尾入队,执行机构从队头取任务,先进先出,不插队。
设计分析:
队列的特点很简单:先进先出。
第一个来的任务,应该第一个被取走。就像食堂排队,谁先来谁先打饭。设备程序里也是一样,扫码先到的工件,通常就应该先被处理。
环形队列比普通数组移动更适合 PLC 小缓存。普通写法可能每取走一个元素,就把后面的数组整体往前搬一次。工位少还好,任务多了以后,程序既啰嗦,也容易写错。
环形队列不搬数组,只移动两个指针:
head:队头,下一个要取出的任务位置。
tail:队尾,下一个要写入的新任务位置。
count:当前队列里有多少个任务。
指针走到数组末尾后,再回到 1。看起来像绕圈,所以叫环形队列。
创建功能块:
本例创建一个 FB,命名为:
RingTaskQueue
为什么用 FB?
因为队列需要记住 buffer、head、tail、count 这些内部状态。它不是一次性计算,而是一个持续运行的小缓存,所以更适合用 FB。
定义接口变量:
定义输入变量:
enable:功能使能。
reset:清空队列和报警。
clearAlarm:只清除上溢、下溢报警。
enqueueReq:入队请求。
enqueueValue:要写入队列的任务值,例如工件编号或任务编号。
dequeueReq:出队请求。
capacity:实际使用的队列容量,最大 16。
定义输出变量:
enqueueDone:本扫描周期入队成功。
dequeueDone:本扫描周期出队成功。
outputValid:取出值是否有效。
dequeueValue:本次取出的任务值。
queueCount:当前队列任务数量。
full:队列已满。
empty:队列为空。
overflowAlarm:满队列时又收到入队请求。
underflowAlarm:空队列时又收到出队请求。
headIndex:当前队头指针,方便 HMI 和监控表观察。
tailIndex:当前队尾指针,方便 HMI 和监控表观察。
程序代码:
调用方法:
可以建立一个全局 DB,命名为:
GdbTaskQueue
在 DB 中定义实参变量:
在 OB1 或设备主 FB 中调用:
一个小例子:
假设扫码器依次扫到 101、102、103 三个工件编号。
每次扫码完成后,程序给 enqueueReq 一个上升沿,同时把工件编号写到 enqueueValue。
队列里就会按顺序保存:101、102、103。
后面机械手空闲时,给 dequeueReq 一个上升沿。第一次取出 101,第二次取出 102,第三次取出 103。
这就是先进先出。谁先来,谁先被处理,不会因为后面来了一个新任务就把前面的任务挤掉。
代码测试:
调试时可以用监控表手动给 EnqueueReq 和 DequeueReq 脉冲。建议做下面几组测试:
连续入队 101、102、103,确认 QueueCount 从 0 变到 3。
连续出队 3 次,确认 DequeueValue 依次为 101、102、103。
Capacity 设置为 3,再入队第 4 个任务,确认 OverflowAlarm 置 TRUE。
队列为空时出队,确认 UnderflowAlarm 置 TRUE。
指针走到 Capacity 末尾后继续入队,确认 TailIndex 会回到 1。
这个例子主要看 headIndex、tailIndex 和 queueCount。只要这三个值跑得对,环形队列基本就成功了。
工程注意事项:
第一,入队和出队请求要用上升沿。
如果 enqueueReq 一直为 TRUE,而程序每个扫描周期都入队一次,那一个扫码结果可能被塞进队列几十次。现场看起来就像任务自己复制了自己。
第二,队列满了不能硬写。
满队列时继续写入,会覆盖还没处理的任务。宁可报警,也不要悄悄覆盖。任务丢了以后,再查就很麻烦。
第三,队列空了不能硬取。
空队列出队,取出来的值没有意义。outputValid 一定要配合 dequeueValue 使用。
第四,环形队列保存的是任务,不是动作过程。
队列只负责缓存和顺序。真正执行任务时,还要有顺控状态机、超时报警、互锁和反馈确认。
常见问题:
第一,用数组平移实现队列。
每取出一个任务,就把数组后面的元素全部往前搬。这个写法能用,但不优雅,点位一多就又慢又容易错。
第二,不限制容量。
PLC 程序必须有边界。数组最大 16,就要把 capacity 限制在 16 以内,别相信 HMI 参数永远不会填错。
第三,报警不锁存。
OverflowAlarm 和 UnderflowAlarm 最好留下痕迹。否则任务曾经满过、空取过,调试人员根本看不到。
第四,HMI 只显示 QueueCount。
QueueCount 只能告诉你有几个任务。调试时最好把 headIndex、tailIndex、full、empty、overflowAlarm、underflowAlarm 都显示出来,问题会清楚很多。
老炮儿建议:
扫码、视觉、分拣、AGV 这类异步任务,优先考虑用队列做缓存。
任务值如果不止一个编号,可以扩展成 UDT 队列,例如包含 ID、目标工位、优先级、时间戳。
如果任务允许插队,那就不是普通 FIFO 队列了,要单独设计优先级队列。
把队列逻辑封装成 FB,不要散写在顺控里。顺控负责执行,队列负责排队。
总结:
本例用 SCL 写了一个环形队列任务缓存。
它的核心是三个状态:head、tail、count。入队写 tail,出队读 head,指针到末尾后回到 1。
这个小算法很适合 PLC 工程里的任务缓存场景。算法尽管很简单朴素,但能让扫码、分拣、缓存、执行之间的节奏变得清楚。
程序写到一定阶段,我们会发现:现场很多麻烦不是动作不会动,而是任务来了以后没有一个可靠的地方先排好队。
实例分享链接:
https://pan.quark.cn/s/931f680241a6