ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

PLC数据缓存利器:SCL实现循环队列FIFO功能块(TIA博途)

PLC数据缓存利器:SCL实现循环队列FIFO功能块(TIA博途) 简介本资源是面向西门子TIA博途PLC开发工程师与自动化系统集成人员的SCL高级编程实践组件聚焦于工业场景下高效、可靠的数据缓存与顺序处理需求提供开箱即用的循环队列FIFO算法功能块FB实现。资源以标准TIA博途项目库形式封装共9个文件含6个XML定义FB接口、变量及逻辑结构、1个PLFPLC功能块编译数据、1个IDX索引文件支持快速加载和1个AL15本地化语言资源整体压缩包仅255KB轻量易集成。已有1596人学习下载表明其在实际工程中具备较高复用价值。用户可直接导入TIA博途V15及以上版本在OB或FC中调用该FB完成入队、出队、状态查询等完整操作代码严格遵循SCL语法规范内置队空/队满判断、索引循环计算与边界容错机制避免下标越界与数据覆盖风险显著降低自研底层数据结构的开发成本与调试难度。1. 先搞清楚问题PLC里的FIFO队列到底要干啥搞过西门子S7-1200/1500程序的人多半会遇到“数据排队”的需求。机械手从A工位抓完料B工位还没加工完这时候来料的数据得先放到一边排队上位机通过Profinet或者TCP发来一批订单数据PLC一个扫描周期处理不完也得先缓存起来再慢慢消化。排队最讲究的是顺序先到的先走先入先出这就是FIFOFirst In First Out算法要解决的核心问题。很多人一开始的思路是建一个大数组来一个数据就往末尾塞要用的时候从数组第一个元素开始找找到之后把后面所有数据整体往前挪一格。数据量小的时候比如队列里就三五个元素这么干没问题扫描周期也看不出什么影响。但队列一旦堆到二三十个元素每取出一条数据就要把后面所有元素全部移动一遍几十次赋值操作挤在一个扫描周期里CPU时间全耗在这上面了。如果这个FB还在1毫秒或者几毫秒的高速中断OB里被调用那问题更严重现场可能直接出现看门狗超时PLC报故障停机。所以这篇要分享的不是这种“暴力挪数组”的写法而是在TIA博途TIA Portal里用SCL语言实现的一个循环队列FIFO功能块FB。它不移动数据只移动“队头”和“队尾”的位置读写都只和两个索引打交道不管队列里存了多少条数据单次入队和单次出队的开销永远是固定的。文章会从算法原理讲到SCL代码再讲到怎么在TIA博途里建FB、仿真验证、封装成库文件最后附上我在实际项目里踩过的坑和容量估算方法。适合谁来参考呢正在用S7-1200/1500做程序、被数据缓存和顺序处理问题困扰的工控工程师想学SCL语言但找不到合适案例的PLC程序员还有刚入门想搞懂FIFO在自动化场景里怎么落地的新手。循环队列不是什么高深算法就是一套非常朴素的索引管理逻辑但用好了能解决产线上很多棘手的时序问题。1.1 为什么不用普通数组硬挪先仔细拆一下“普通数组FIFO”为什么不好用。假设定义了一个长度为50的数组里面已经存了30条记录现在要取出一条标准的“数组左移”逻辑是// 伪代码示意普通数组左移 FOR i : 1 TO 29 DO queueData[i - 1] : queueData[i]; END_FOR;每取出一条循环29次赋值。如果再写入一条还要定位到当前末尾。当数据量再大一点数组里50个元素全部存满取出一个元素就要循环49次。这还只是一次出队操作如果PLC一个周期里既要出队又要入队再加上其他业务逻辑扫描周期直接被拖垮。更隐蔽的问题是数组移动本身涉及“读改写”的过程如果在移动过程中来了中断新数据一旦插进来导致数组元素错位排查起来非常痛苦数据错乱在产线上是最难定位的故障之一。我早年在一条装配线上就吃过这个亏。当时设备有几个工位要把检测结果按顺序发给后续工位进行扫码比对数组里存了二十多条记录结果发现OB1扫描周期从5毫秒涨到了快20毫秒而且还是间歇性跳变。后来把数组移动那段逻辑摘出去一测问题立竿见影就是数组左移带来的。从那时候起我对“通过批量元素移动来模拟队列”的方案就彻底放弃了。1.2 循环队列的核心思想循环队列的思路和普通数组完全不一样不再把数据物理地搬来搬去而是维护两个“指针”——队头索引head和队尾索引tail。入队的时候数据写到tail指向的位置然后tail向后走一格出队的时候从head指向的位置读数据然后head向后走一格。走到数组边界怎么办绕回来从头继续。这就是“循环”二字的由来。打个比方就像回转寿司店里的传送带。厨房把寿司放到传送带上的某个位置传送带一直往前转用餐的人只在取餐口从这里拿走寿司。传送带上的盘子本身没有换来换去只是位置在转圈。PLC里的循环队列也是一样内存区域就是那条传送带head和tail就是两个标记位置的指针我们通过改变索引的位置来模拟先进先出数据本身在数组里几乎不怎么动。两个索引怎么移动靠取模运算。在SCL里就是(index 1) MOD 长度。比如数组长度是10tail当前是9写入一个元素后tail不是变成10而是(9 1) MOD 10 0重新指向数组开头。这样整个数组就形成一个环形结构空间可以被反复利用不会出现“数组前面的格子空着后面的格子却放不下数据”的情况。2. 为什么用SCL写FB而不是LAD/FBD确定要用循环队列之后下一个问题就是用什么语言来实现。很多人习惯用LAD梯形图和FBD功能块图但说实话用这些图形化语言实现数组、取模、循环这类数据结构逻辑体验非常痛苦。2.1 SCL最适合表达数据结构逻辑LAD擅长的是继电器逻辑、互锁、起保停画个串并联电路很顺手。但你要是想在LAD里写一个数组循环先得拖一个FOR循环块然后在里面塞各种比较指令和计算指令图面密密麻麻维护起来简直是一场灾难。尤其是指针索引和MOD取模这种数学运算在LAD里表达极其繁琐一个索引加一的逻辑可能就要拖三四个功能块别人接手你的程序时大概率会一脸茫然。SCL是文本化编程语言语法接近Pascal和C可以用IF、FOR、WHILE、MOD、数组下标这些直接表达算法逻辑。写循环队列这种带状态、带索引运算的功能SCL天然合适代码读起来就像在看一段简洁的英文逻辑描述。TIA博途V13开始SCL已经比较成熟到V15以上用起来很流畅补全、语法检查、在线监视都做得不错。另外有一个从LAD转过来的朋友容易踩的坑SCL里赋值符号是:不是取模不是%而是MOD每个条件分支都要对应END_IF、END_FOR这样的结束关键词。这些语法差异看着小写顺手之后基本无感但刚开始容易漏掉结束关键词编译直接报错。2.2 FB、FC和库文件的关系FIFO队列必须保持记忆状态——上次写到哪个位置、读到哪个位置、队列里还剩多少数据这些信息要一直在。FC函数没有独立的背景数据块局部变量在调用结束后就丢了无法维持队列状态。如果非要用FC只能把状态变量都放到一个全局DB里然后手动管理这样做的缺点是每建一个队列就得复制一份DB和对应逻辑程序一多就乱成一锅粥。FB功能块自带背景数据块每个实例都有自己独立的状态空间。这就意味着我只需要写一个FIFO功能块然后在多个地方分别调用每个调用点生成各自的背景DB队列之间互不干扰。比如一条产线上有三台包装机需要三个独立的缓存队列我只需要调用三次FB各配一个DB实例就行逻辑只维护一份改动一次三处全部生效。这就是FB的模板化优势。写完FB之后还可以把它拖入TIA博途的全局库保存成库文件常见扩展名是.zal或者新版.zap开头的格式下次新建项目直接拖出来就能用不需要重新写一遍。这也是为什么网上的资源共享基本都是以“FB库文件”的形式出现。我习惯在FB里把接口写清楚、把注释写规范然后沉淀到公司标准库里新项目直接调用效率能提升不少。3. SCL实现循环队列完整代码与逐段讲解下面直接进入正题。我在TIA博途里新建了一个FB语言选SCL块名取FB_QueueFIFO然后实现了完整的循环队列FIFO算法。先看接口定义再看代码主体。3.1 接口设计输入输出和静态变量怎么定FB的接口决定了外部怎么调用它、内部怎么保持状态。我的设计如下变量名方向数据类型说明executePush输入Bool写入使能上升沿有效pushData输入Int待写入的数据executePop输入Bool读出使能上升沿有效reset输入Bool复位队列电平有效popData输出Int读出的数据isEmpty输出Bool队列空标志isFull输出Bool队列满标志amount输出Int当前队列内数据个数fullWarn输出Bool写入时队列已满报警emptyWarn输出Bool读出时队列为空报警headIdx静态保持Int队头索引tailIdx静态保持Int队尾索引itemCount静态保持Int当前元素个数pushOld静态保持Bool写入沿检测的上一周期值popOld静态保持Bool读出沿检测的上一周期值queueData静态保持ARRAY[0..9] OF Int队列存储区关于接口设计有几个点要特别说明。第一写入和读出为什么用上升沿而不是电平。OB1是循环扫描执行的同一个FB在每一个扫描周期都会被调用一次。如果直接用executePush的电平状态作为写入条件只要这个信号保持为TRUEFB就会在每个扫描周期都执行一次入队一条数据会被重复写入好几次。上升沿上升沿检测上升沿触发可以确保这个信号从FALSE变为TRUE的那一瞬间只执行一次写入动作。这是PLC编程里FIFO队列比较关键的一个细节。第二为什么用一个itemCount来记录队列里有多少数据。这是计数器方式。判断空和满最直观的方式就是看itemCount是0还是到达上限。另外还有经典实现方式——牺牲一个存储单元当tail head时为空当(tail 1) MOD N head时为满。这种方式更省一个变量但理解起来不如计数器直接。现代PLC内存和性能都够用我更推荐计数器方式空、满、存量一眼就能看出来调试的时候特别方便。3.2 完整代码下面的代码就是我在FB主体里完整粘贴的SCL实现。队列容量默认是10个元素如果你想改成20个把数组边界改成ARRAY[0..19]同时把QUEUE_LEN的赋值改成20即可。// 循环队列 FIFO 功能块 // 队列容量10索引范围0..9 // 入队executePush 上升沿 pushData // 出队executePop 上升沿数据出现在 popData // 复位reset 置 TRUE清空队列 VAR CONSTANT QUEUE_LEN : Int : 10; END_VAR VAR_INPUT executePush : Bool : FALSE; // 写入使能上升沿有效 pushData : Int : 0; // 待写入数据 executePop : Bool : FALSE; // 读出使能上升沿有效 reset : Bool : FALSE; // 复位队列电平有效 END_VAR VAR_OUTPUT popData : Int : 0; // 读出数据 isEmpty : Bool : TRUE; // 队列空 isFull : Bool : FALSE; // 队列满 amount : Int : 0; // 当前元素数量 fullWarn : Bool : FALSE; // 入队时队列已满 emptyWarn : Bool : FALSE; // 出队时队列为空 END_VAR VAR queueData : ARRAY[0..9] OF Int; headIdx : Int : 0; tailIdx : Int : 0; itemCount : Int : 0; pushOld : Bool : FALSE; popOld : Bool : FALSE; END_VAR VAR_TEMP pushRise : Bool; popRise : Bool; END_VAR // 上升沿检测 pushRise : executePush AND NOT pushOld; pushOld : executePush; popRise : executePop AND NOT popOld; popOld : executePop; // 复位优先 IF reset THEN headIdx : 0; tailIdx : 0; itemCount : 0; popData : 0; fullWarn : FALSE; emptyWarn : FALSE; END_IF; // 入队 IF pushRise AND NOT reset THEN IF itemCount QUEUE_LEN THEN queueData[tailIdx] : pushData; tailIdx : (tailIdx 1) MOD QUEUE_LEN; itemCount : itemCount 1; ELSE fullWarn : TRUE; END_IF; END_IF; // 出队 IF popRise AND NOT reset THEN IF itemCount 0 THEN popData : queueData[headIdx]; headIdx : (headIdx 1) MOD QUEUE_LEN; itemCount : itemCount - 1; ELSE emptyWarn : TRUE; END_IF; END_IF; // 状态输出 isEmpty : itemCount 0; isFull : itemCount QUEUE_LEN; amount : itemCount;3.3 关键代码的逻辑拆解沿着代码从上到下捋一遍几个关键点单独拎出来讲。上升沿检测这段标准写法就是pushRise : executePush AND NOT pushOld;然后pushOld : executePush。意思是当前周期信号是TRUE且上一个周期是FALSE那说明刚刚发生了由0到1的跳变这就是一个上升沿。这里也可以直接用系统自带的R_TRIG上升沿触发器块但手写这个逻辑的好处是不需要额外生成背景DB而且逻辑完全透明出了故障一眼就能看到沿检测的每一段状态。入队操作中IF itemCount QUEUE_LEN判断队列是否有空间。有空间就写入queueData[tailIdx]然后tailIdx前进一格并通过MOD取模实现绕回。itemCount加一。如果队列满了就置位fullWarn数据不写入。这个策略是“满了就丢弃新数据”。实际项目中还有另一种选择是“覆盖最旧数据”但绝大多数工艺场景下丢弃新数据并且报警更安全因为新数据如果是关键的上位机应该等待而不是强塞。出队操作与入队对称。IF itemCount 0判断队列非空。非空则从queueData[headIdx]读出数据赋给popDataheadIdx前进一格同样用MOD绕回itemCount减一。如果队列为空还去读那就置位emptyWarn。我特意把复位逻辑放在入队出队之前并且入队出队条件里都加了NOT reset这样复位信号在上升沿到来的同一周期也能优先生效避免出现“这边刚复位完那边又立刻执行了入队”的竞态问题。这个顺序看起来不起眼实际调试的时候真的很重要。3.4 从Int扩展到任意数据类型代码里queueData是ARRAY[0..9] OF Int所以比较基础。实际项目中往往要入队的是一个结构体——产品追溯数据、检测结果、订单号、字符串等这时候直接把数组元素的类型改成对应的UDT用户自定义数据类型就行入队和出队的逻辑一句都不用改。具体做法是在“PLC数据类型”里新建一个UDT比如UDT_TraceData里面定义产品批号、经度位置、检测结果、时间戳等成员。然后把FB接口里的queueData改成ARRAY[0..9] OF UDT_TraceData。这样每条队列元素就是一个完整的结构体入队时pushData也要改成同类型。我用过几次之后最大的体会是FB这个“写一次、到处用”的模板特性配上UDT之后通用性会大大提升一个队列FB可以存储任意复杂的数据对象不用为了不同数据结构写好几个版本的FIFO。4. 在TIA博途里实操建块、写码、仿真验证代码逻辑清楚了但最终还是要落到TIA博途这个工具里把FB建出来、编译通过、仿真跑通才算真正完成。我按自己在S7-1500上的操作流程一步步写下来S7-1200的步骤基本相同。4.1 新建FB和背景DB打开TIA博途新建项目添加一个S7-1500或者S7-1200设备。然后在左侧项目树中展开“程序块”双击“添加新块”选择“功能块”语言选“SCL”块名称写FB_QueueFIFO编号会自动生成。进入编程界面后先不要急着写代码把接口区按前面表格里列的内容定义好。TIA的SCL编辑器界面分上下两个区域上面是接口区下面是程序代码区。接口区有三个页签输入、输出、静态变量把executePush、pushData、executePop、reset填到输入页签popData、isEmpty、isFull、amount、fullWarn、emptyWarn填到输出页签queueData、headIdx、tailIdx、itemCount、pushOld、popOld填到静态变量页签。接口定义好之后把第三章的代码粘贴到程序区然后点击编译。如果代码没有语法问题编译一般会直接通过。随后在OB1里调用这个FB调用时会弹窗让你选择背景数据块可以自动生成我习惯于给它起名DB_QueueFIFO_Instance一看就知道是哪个FB的实例。这里有一个新手容易困惑的点FB的背景DB到底是怎么创建的。其实只要你把FB拖到OB1的程序段里TIA就会自动要求你分配一个背景DB。这个DB不需要手动一个个定义变量FB的输入输出和静态变量会自动出现在DB里。也就是说你在HMI或者监控表里可以直接看到DB_QueueFIFO_Instance.amount、DB_QueueFIFO_Instance.isEmpty这些变量非常方便调试。4.2 在OB1里调用并写一个模拟测试OB1里调用我一般这样写DB_QueueFIFO_Instance( executePush : HMI_Push, pushData : HMI_PushValue, executePop : HMI_Pop, reset : HMI_Reset, popData HMI_PopValue, isEmpty HMI_IsEmpty, isFull HMI_IsFull, amount HMI_Amount, fullWarn HMI_FullWarn, emptyWarn HMI_EmptyWarn );调用格式有一点要注意输入参数用:输出参数用。这个箭头方向如果写反了编译会直接报错第一次用SCL的人经常会在这里卡一下。为了验证FB逻辑是否正确我会先在仿真软件里做模拟测试而不是直接上真机。打开PLCSIM下载应用到仿真PLC然后通过监控表或者数据块监控窗口强制变量。测试步骤我整理成一个表格方便照着操作步骤操作预期结果1初始状态isEmpty TRUEamount 02HMI_PushValue 100HMI_Push 给一次上升沿amount 1isEmpty FALSE3HMI_PushValue 200又一次上升沿amount 2popData仍为04HMI_PushValue 300又一次上升沿amount 35HMI_Pop 给一次上升沿popData 100amount 26再次 HMI_Pop 上升沿popData 200amount 17连续给10次入队上升沿第10次后 isFull TRUEamount 108再给一次入队上升沿fullWarn TRUEamount仍为109HMI_Reset 置TRUEisEmpty TRUEamount 0isFull FALSE按照这个表跑一遍基本可以验证FIFO的入队、出队、空满判断、复位逻辑都正确。尤其是第7、8步是验证“满”状态最重要的环节第11条数据不能入队只能报警。4.3 用PLCSIM和监控表观察指针行为除了看输出信号我建议再深入一点直接观察内部指针的变化这能帮你真正理解循环队列的运行过程。在监控表里添加背景DB的内部变量比如DB_QueueFIFO_Instance.headIdx和DB_QueueFIFO_Instance.tailIdx以及queueData数组的每一个元素。当你写入第一条数据时tailIdx从0变成1queueData[0]里存了数据写入第二条时tailIdx从1变成2。读了第一条之后headIdx从0变成1。然后继续写入直到tailIdx从9绕回到0你会发现数组并没有被搬来搬去只是两个索引绕了一圈回来了。这个现象亲眼看过一次之后循环队列的原理就再也忘不了了。用PLCSIM跑仿真比真机调试舒服的地方在于你可以随时暂停扫描周期然后逐周期逐步执行观察沿检测的每一拍变化。真机上如果信号闪得太快根本来不及看仿真环境里可以把过程放得很慢特别适合理解上升沿触发到底是怎么工作的。4.4 封装成库文件供后续项目复用FB调试通过之后把它封装到全局库是一件收益很高的事。选中项目树里的FB_QueueFIFO鼠标拖到“全局库”区域TIA会自动创建库副本。然后在库上右键选择“保存库”或者“从库生成版本”按TIA版本选择对应的库文件格式保存。旧版TIA的全局库扩展名一般是.zal新版TIA V16以上可能是.zap开头的压缩格式。分享给别人的时候通常会把库文件再压成一个.rar包这样传输更稳定。别人拿到之后打开TIA博途在“全局库”区域点击“打开全局库”选择解压出来的库文件FB就会出现在库里直接拖到自己的程序块中就能调用。有几点封装经验供参考库文件版本和TIA版本有对应关系。V15的项目库拿到V16里打开一般能升级但V16的库拿到V15里是打不开的。所以分享库文件时一定注明你的TIA版本。如果FB中用了自定义UDT要把UDT也拖进库。否则别人引用FB时会提示缺少类型定义。建议把背景DB的编号命名规则写进FB的注释里比如“每个实例请使用独立DB避免多工位共用一个实例导致数据互相覆盖”后续使用者一眼就能看到规范。5. 常见坑与调参经验代码和流程都讲完了这一章我专门写实际项目中遇到的高频问题和对应的排查思路每一个都是我真实经历过的有些坑印象还挺深。5.1 队列容量怎么选才合适队列容量没有一个放之四海而皆准的数值要根据工艺节拍和缓存需求来算。给一个我自己常用的估算方法假设上游每T_up秒产出一个数据下游每T_down秒处理一个数据那么在下游处理一件的时间窗口里上游新产生的数据量大约是T_down / T_up件再留出至少1件的余量。所以最小容量可以按下式估算N_min 取整( T_down / T_up ) 1举例上游机械手每2秒放一个工件到缓存位下游检测设备每6秒处理一件。代入公式T_down / T_up 6 / 2 3所以N_min 3 1 4件。如果实际运行时上游来料有波动或者下游偶尔有短暂故障需要多缓存一会儿我会把容量再放大1.2到1.5倍。按这个例子我会直接定到8件留足安全余量。如果是通信数据缓存容量取决于上位机最大突发帧数和PLC的处理能力。比如上位机一次突发30条报文PLC每条处理时间5毫秒那队列容量至少30条否则就会丢报文。这个场景下我不太建议靠FIFO硬扛最好在上位机侧做流控队列容量只做兜底。5.2 空、满、索引错乱问题排查先列一个高频问题速查表方便直接对照定位现象可能原因排查方法写入后 amount 一直不变executePush 没有产生上升沿用监控表观察 pushRise检查 executePush 是否为常通信号读出数据总是最后一个不是最早那个队列不工作直接覆盖了同一位置检查 headIdx 和 tailIdx 是否一直在某个固定值索引没往前走复位后还能写入并且旧数据出现reset 与 push 同一周期竞争确认代码中复位优先级最高push 条件里加了 NOT resetisFull 一直为TRUE写不进去QUEUE_LEN 和数组边界不一致确认 QUEUE_LEN10 时数组是 ARRAY[0..9]别写成1..10数据偶发丢失仿真又复现不了executePush 脉冲宽度小于扫描周期沿没检测到改用置位复位信号锁存或者把扫描周期调短这里面最值得展开的是第5条。PLC扫描是周期性的如果外部信号给过来的脉冲宽度太窄——比如传感器信号只有0.5毫秒而PLC扫描周期是5毫秒那PLC可能压根没看到这个脉冲从FALSE变TRUE的过程上升沿检测直接失效。解决办法有两个一是把信号源改成电平保持由PLC内部确认后再复位二是把调用这个FB的OB放到更快的中断组织块里比如循环中断OB32。但改中断优先级会引入数据一致性风险如果不是必须我还是建议先确保外部信号脉宽足够宽。5.3 背景DB别共用这是一个血的教训FB的模板复用能力很强但有个前提每个队列实例必须用独立的背景DB。我有一次图省事两个工位调用了同一个FB没有单独生成实例而是直接把同一个背景DB的两个调用点连到了一起。结果工位A写入的数据工位B也能读出来两个工位的缓存完全混在一起现场产品追溯数据全部错乱。之后我形成了一个习惯每个FB调用点都单独生成背景DB并在DB名称里体现用途比如DB_Queue_Line1_A、DB_Queue_Line1_B。虽然程序块列表看起来多了几个DB但逻辑清晰了很多排查问题的时候一眼就能看出数据存在哪里。自动化产线最怕“隐性耦合”多个设备共享同一份队列数据出了问题很难快速定位。5.4 沿检测方式与扫描周期的关系我在代码里用了自己写的沿检测pushRise : executePush AND NOT pushOld。这是最基础、也最好理解的写法。TIA博途里也提供R_TRIG和F_TRIG上升沿/下降沿触发块使用起来更加标准化。不过有一个区别要清楚R_TRIG在FB里使用也要占用一个背景数据块的静态变量而手写沿检测只需要两个BOOL变量代码更透明。我个人在FB库代码里更倾向于手写因为别人拿到你的库打开代码能直接看明白每一拍发生了什么不需要额外去查R_TRIG的调用链。还有一点关于扫描周期的提醒。如果你把FIFO放在OB1里而OB1扫描周期是10毫秒那入队的执行频率最多也就是10毫秒一次。如果外部数据来得比这个速度快队列就会出现“来得多、处理得少”的堆积。判断队列容量是否满足要求时不能只看工艺节拍还要把调用FB的OB周期一起算进去。这也是为什么我前面强调容量估算要留出安全余量不然现场稍微有点波动isFull报警就会出现然后再去调参数就很被动了。5.5 代码效率和安全一致性循环队列的好处是读写开销恒定但代码里有一个点也要注意MOD取模运算在PLC里虽然不慢但如果调用频率极高比如在1毫秒中断里执行还是有极小的时间开销。好在现代S7-1500的整数取模速度很快基本不用在这个级别纠结。真正要关注的是数据一致性如果FIFO在多个中断优先级不同的OB里被调用就要考虑数据会不会被中断嵌套破坏。比如低优先级OB正在执行入队高优先级OB插进来执行出队队列索引可能被中间改写导致数据混乱。解决办法有几个层面一是尽量减少跨中断调用同一个FB实例二是TIA博途里可以用_disable和_enable指令临时关闭中断但尽量别用这种粗暴的方式三是如果数据一致性要求非常高可以考虑把FIFO放到固定优先级的循环中断OB里让读写都在同一优先级下执行。我在实际项目里一般遵循“一个队列只在一个OB中被处理”的原则不同实时性要求的数据分到不同队列而不是试图在一个队列里兼容所有中断等级。最后再分享一个实际经验前面说了很多原理、代码和坑最后聊一个我自己的使用习惯。我在做一个汽车零部件追溯项目的时候需要把每个工位的检测结果按顺序发给中央数据采集再和追溯码绑定。队列里存的不是单个整数而是一个包含产品批号、工位号、检测时间、OK/NG结果的结构体。最初我直接把这个UDT塞进FIFO逻辑跑得很顺。后来我发现光有FIFO还不够当NG品出现时现场操作人员希望看到的是“队列里现在有几件、最早那一件的追溯码是什么”。于是我又在FB的输出里加了一个peekData端口作用是不出队、只看队头数据。这个功能在调试和HMI展示时都非常有用比读一次再“还回去”靠谱得多因为读后写回很容易引入并发问题。所以如果你拿这个FB作为自己的基础模板我建议你在实现基本FIFO之后顺手加上一个peek接口。代码改动不大popData : queueData[headIdx]改成peekData : queueData[headIdx]但功能上能解决很多实际问题。这个FB我已经用在了好几个项目里每次新项目需要排队缓存我都是直接把库里的FB拖出来改一下UDT类型其余逻辑基本不动。库文件这个东西早建早轻松积累个三五个常用FB后面做项目真的能快不少。本文还有配套的精品资源点击获取
返回列表