TIA博途V15中用SCL实现先进后出堆栈算法

发布时间:2026/9/19 23:43:42
TIA博途V15中用SCL实现先进后出堆栈算法
简介面向TIA博途V15用户的PLC编程学习资料以SCL语言详细讲解堆栈算法先进后出的工程实现适合工业自动化工程师、电气设计人员及对高级语言编程感兴趣的PLC开发者。资源为一份docx文档约1.45MB内容从堆栈结构的基本原理出发逐步展示FB功能块的输入输出接口定义、StackData数组与StackPointer指针的变量设计、OB1主循环调用方法以及通过图形化界面进行入栈、出栈和清空重置的仿真测试。文档中还给出了程序流程图和管脚连接示意方便对照学习。已有1788人学习下载整体既覆盖数据结构核心概念又紧密贴合TIA博途V15实际编程是一份兼顾理论理解与工程落地的好资料。1. 在 TIA 博途 V15 中堆栈算法的应用需求与 SCL 定位生产线上经常会遇到“保存最近 N 个结果、事后倒着取出来”的需求视觉检测把每件产品的外观值写入暂存区后面工位要按反序回查或者机械手要按缓存顺序把来料倒推成卸垛顺序又或者把设备连续五次称重值放进一个先进后出结构用于计算趋势。PLC 不像 PC 那样可以动态分配内存TIA 博途 V15 对数组定义也是编译期内固定的所以“先进后出LIFO”这个看起来简单的堆栈算法到了 PLC 里必须靠固定数组加上一个栈顶指针手工实现。SCL 语言在这里比 LAD/FBD 更合适因为你会用到下标变量、循环和条件分支而 SCL 描述这些逻辑的密度远高于梯形图。这篇文章就按 TIA 博途 V15 的语法把堆栈算法的数据模型、FB 程序、边界保护和 PLCSIM 验证方法一次讲完。2. 堆栈算法先进后出的 SCL 存储模型与变量边界2.1 用固定数组模拟连续栈空间栈顶指针的含义TIA 博途 V15 的 SCL 不支持动态数组也不支持在 RUN 状态下重新分配 DB 长度。最常见的做法是在函数块 FB 的静态变量区里声明一个固定长度的数组把数组当作堆栈的存储体。数组的元素可以是 INT、REAL也可以是一个 PLC 数据类型UDT。下面是一个典型的栈元素结构用来保存产品序列号、称重值和判定结果TYPE StackItem : VERSION : 1.0 STRUCT SeqNo : DINT; // 产品序列号 Weight : REAL; // 称重值单位 kg Passed : BOOL; // 最终判定结果 END_STRUCT END_TYPE堆栈的核心是“后进先出”而控制后进先出的不是数据本身而是一个整数指针。这个指针有两种定义方式一种指向栈顶最后一个有效数据另一种指向下一个空闲位置。我常用后一种原因很简单压栈时先写入数组再指针加一弹栈时先指针减一再读取数组这样栈空的判断条件就是Top 0栈满的判断条件就是Top 数组上界 1。两种方式的对比见下表。栈顶指针语义空栈条件满栈条件压栈操作顺序弹栈操作顺序指向下一个空闲位置Top 0Top 最大深度先写StackArray[Top]再 Top1先 Top-1再读StackArray[Top]指向最后一个有效数据需要额外计数器需要额外计数器先 Top1再写入先读取再 Top-1采用“指向下一个空闲位置”的好处是少一个变量而且数组下标全部从 0 开始和 SCL 默认的数组下界一致在边界检查时不容易把人绕晕。下面这段代码声明了一个容量为 10 的栈// 在 FB_Stack 的 Static 区定义 StackArray : ARRAY[0..9] OF StackItem; // 10 个栈元素 Top : INT; // 栈顶指针指向下一个写入位置如果你需要修改堆栈容量必须同时修改ARRAY[0..9]的上界和程序里的比较值 10。容量一变数组边界和满栈条件都要跟着改这属于 TIA 博途 V15 编译期约束不必指望运行时动态扩容。2.2 为什么选择 SCL对比 LAD/FBD 后的优缺点取舍LAD 和 FBD 也能做数据搬移但当你的数据下标是由某个运行中的变量决定时梯形图会变得非常难读而且网络数量会随着分支增加成倍膨胀。SCL 语言的优点恰恰在于它能把数组下标、循环和条件判断写在一个紧凑的块里压栈与弹栈的逻辑一眼就能看全后期维护时也能快速找到每个分支的处理。但 SCL 也不是没有代价。首先是编译后的代码量通常比同等功能的 LAD 略大这在 S7-1200 上尤其明显如果是 10 个元素的栈影响可以忽略但如果你在中断 OB 里对数千个元素做遍历就要评估扫描周期其次SCL 没有动态内存你无法用New的方式创建对象整个栈的大小必须在编程阶段拍死第三SCL 的数组越界行为在优化块和标准块下并不完全一致后续章节会专门讲越界保护。因此在实际项目中我一般把 SCL 用在数据结构、配方管理、顺序控制这类“逻辑密度高、执行频率适中”的程序段里而不是用它去替 LAD 处理高速脉冲或运动控制。堆栈算法正属于前者。2.3 栈顶指针初始化与掉电保持策略栈顶指针Top是 FB 静态变量存储在背景数据块 DB 中。S7-1200/1500 的 DB 变量默认不具备保持性也就是说 CPU 断电重启后Top会被复位成 0但是数组里的元素内容会保持掉电前的二进制数据。这样会造成一个奇怪现象栈正确显示为空但数组中仍能看到之前的数据。所以如果你需要掉电保持比如在断电前把最近 10 个历史数据存住应该在 DB 属性中把StackArray和Top都设置为保持如果不需要则要在启动 OB 里主动调用一次复位操作。复位可以直接利用 FB 的Reset输入在 OB100 里写Stack_DB(Reset : TRUE);这段代码会把Top强制置 0。注意复位并不会清空数组里的数据因为清空数组需要循环执行 10 次赋值完全没有必要。只要Top为 0任何弹栈请求都会被拒绝旧数据只是一个二进制残留不会影响堆栈逻辑。3. 在 TIA 博途 V15 中编写 FB_Stack接口、SCL 代码与调用方法3.1 函数块接口定义输入、输出、静态变量在 TIA 博途 V15 中新建一个函数块 FB名字叫FB_Stack。接口参数按下面的表来定义其中Push和Pop需要使用上升沿触发DataIn压入的数据DataOut弹出的数据这几个参数后面写程序时要重点处理。变量名方向数据类型说明PushInputBool有上升沿时执行压栈PopInputBool有上升沿时执行弹栈ResetInputBool置 1 时栈顶指针清零DataInInputStackItem压栈的源数据DataOutOutputStackItem弹栈结果CountOutputINT当前栈内元素个数FullOutputBool栈满标志EmptyOutputBool栈空标志StackArrayStaticARRAY[0..9] OF StackItem栈存储TopStaticINT栈顶指针PushEdgeStaticBoolPush 输入的上一个扫描周期值PopEdgeStaticBoolPop 输入的上一个扫描周期值另外还需要在 Temp 区声明两个临时变量PushCmd和PopCmd它们表示“检测到了一次上升沿”每个扫描周期都会被重新计算不需要保持属性。3.2 压栈与弹栈的 SCL 实现V15 版本语法及参数说明FB 内部核心程序如下所有注释都按照 TIA 博途 V15 的 SCL 语法编写// 复位优先清零栈顶指针并清空弹栈输出 IF #Reset THEN #Top : 0; #DataOut.SeqNo : 0; #DataOut.Weight : 0.0; #DataOut.Passed : FALSE; END_IF; // 上升沿检测避免按钮或通讯位持续为 TRUE 时重复触发 #PushCmd : FALSE; IF #Push AND NOT #PushEdge THEN #PushCmd : TRUE; END_IF; #PushEdge : #Push; #PopCmd : FALSE; IF #Pop AND NOT #PopEdge THEN #PopCmd : TRUE; END_IF; #PopEdge : #Pop; // 压栈优先于弹栈如果同一周期两个上升沿同时到达只执行压栈 IF #PushCmd THEN IF #Top 10 THEN // 先写入后移动栈顶指针 #StackArray[#Top] : #DataIn; #Top : #Top 1; END_IF; ELSIF #PopCmd THEN // 弹栈先减指针再读数Top 归零时不能再弹 IF #Top 0 THEN #Top : #Top - 1; #DataOut : #StackArray[#Top]; END_IF; END_IF; // 统一刷新标志位避免上一次 Full/Empty 状态残留 #Full : (#Top 10); #Empty : (#Top 0); #Count : #Top;这里的10必须和Array[0..9]保持一致。压栈时先判断Top 10意思是还有空闲位置弹栈时先判断Top 0防止数组下标变成-1。Full和Empty最后统一计算而不是在分支里赋值这样即使压栈被拒外部程序也能根据Full知道当前容量已经满了。临时变量PushCmd和PopCmd只服务于本周期所以每次扫描周期开始先把它们置为 FALSE再根据上升沿逻辑置 TRUE。如果不做这一步PushCmd的上一周期状态会残留在临时区里造成同一个 Push 信号被反复解读。PushEdge和PopEdge必须放在 Static 区因为它们要保存上一周期的输入值。还有一点容易被忽略弹栈操作不会删除StackArray[#Top]里的数据。你只能依据Count或Empty判断栈内是否真的有有效数据不要试图通过读数组去判断数据是否被弹出残留数据长期存在是堆栈的正常行为。3.3 在 OB1 中调用 FB 与多重背景扩展FB 写好后在 OB1 中创建背景 DB 并调用。调用语法如下Stack_DB( Push : HMI_Push, Pop : HMI_Pop, Reset : HMI_Reset, DataIn : DB_Data.SourceItem, DataOut DB_Data.TargetItem, Count DB_Data.StackCount, Full DB_Data.FullFlag, Empty DB_Data.EmptyFlag );输入参数用:连接输出参数用连接。如果你只需要弹栈结果的某个成员也可以在调用时直接写DataOut.SeqNo 某变量SCL 支持这种成员级输出映射。不过为了可读性建议还是先放到临时变量里再拆分使用。如果一个设备上要同时维护多条线产品的缓存可以用多重背景实例。先在 FB 的属性里勾选“支持多重背景实例”然后在某个全局 DB 中声明// 在 DB_Context 的 Static 区 StackLineA : FB_Stack; StackLineB : FB_Stack;调用时直接使用实例前缀#StackLineA( Push : #PushA, Pop : #PopA, DataIn : #ItemA, DataOut #OutA ); #StackLineB( Push : #PushB, Pop : #PopB, DataIn : #ItemB, DataOut #OutB );这样两个堆栈共享同一个 FB 的代码区但各自的StackArray和Top互不干扰。注意多重背景实例的名字不能要求 TIA 博途自动生成背景 DB关注好实例前缀即可。4. 上溢、下溢与扫描周期堆栈算法在 PLC 现场最常见的 4 个坑4.1 栈指针下溢数据还在但下一次压栈会覆盖它弹栈操作最危险的地方不是数据读错而是Top被减成负数。假如在栈空时将Pop信号置高程序里没有IF #Top 0保护Top就会变成-1。在 TIA 博途 V15 中如果使用标准化访问的 DB数组下标为-1会直接触发编程错误CPU 可能进入 STOP如果使用优化访问的 DB行为更加隐蔽可能读取到数组前的其他静态变量。因此每个弹栈分支里都必须判断下界。此外Empty标志必须在Count 0的同一周期立即可用外部逻辑不要只在弹出的数据等于默认值时才判断栈空。4.2 上溢保护与容量边界为什么“满栈再压栈”必须放弃本次数据当堆栈满了以后如果再执行压栈程序只能选择“丢掉新数据”或者“覆盖最旧数据”。后者在语义上其实是队列 FIFO而不是堆栈 LIFO。因此 FB 在Top 10时拒绝写入同时把Full置 TRUE由外部程序决定怎么处理新数据。常见的做法是缓存到另一个故障区或者直接报警停机。千万不要在 FB 内部为了“省事”把新数据写入StackArray[9]覆盖旧数据那样整个先进后出顺序就从根上破坏了。如果你确实需要保留最旧数据应该改写为环形缓冲区而不是堆栈。4.3 沿信号与扫描周期按钮、通讯位和 HMI 置位的差异在 OB1 循环扫描的程序里HMI 按钮如果作为一个普通 Bool 输入直接接到Push操作员按一下按钮可能会持续十几个扫描周期而 FB 内部的上升沿检测保证只有第一次扫描周期会产生PushCmd后续周期PushCmd为 FALSE所以不会重复入栈。这个内置沿处理也适用于来自 Profinet 通讯的状态字只要信号从 0 变成 1就会触发一次操作。但要注意如果上位机或 HMI 的置位信号在上一个周期是 1这个周期又改成 0下一个周期再改成 1那么两次触发之间至少隔了一个周期这种操作是正常的不会丢数据。真正需要担心的是在中断 OB 中调用 FB如果同一个背景 DB 在 OB1 和中断 OB 中同时调用PushEdge和PopEdge可能被两个 OB 交替修改造成边沿误判。这种情况下应该在中断 OB 中只使用堆栈的弹栈输出而压栈操作统一放在 OB1 里或者为每个 OB 各建一个独立实例。4.4 在 OB1 中连续调用多个堆栈时的时序问题两个堆栈串联时调用顺序决定了一个扫描周期内数据的可见性。例如堆栈 A 弹栈的数据需要压入堆栈 B正确的顺序是先调用 A再把 A 的输出赋给 B 的输入A_DB(Pop : TRUE, DataOut #TempData); B_DB(Push : TRUE, DataIn : #TempData);SCL 程序自上而下执行第一行调用完 A 后#TempData已经是本周期弹栈结果第二行调用 B 时#TempData被当成当前输入压入。如果反过来先调用 B再调用 AB 压入的就是上一周期的#TempData数据整整晚了一个扫描周期。调试这类问题时不要只盯 FB 内部逻辑还要检查 OB1 中网络块的排列顺序。下面的表格总结了这四个典型坑的现场现象与对策。坑典型现象对策栈指针下溢弹栈时 PLC 进入 STOP或读出不正常数据弹栈前必须判断 Top 0栈满后丢失数据最旧或最新数据被覆盖顺序错乱满栈时拒绝压栈并置 Full按钮重复触发按一次 HMI 按钮栈里多了几十条记录使用上升沿检测多堆栈串联顺序错误数据总是慢一个周期或顺序颠倒按数据流方向排列调用顺序5. 用 PLCSIM 和监控表验证先进后出并减少堆栈块的开销5.1 在 PLCSIM 中构造 5 个压栈/弹栈用例把 FB_Stack 下载到 PLCSIM 后先在 OB1 中编写一段临时测试逻辑每 10 个扫描周期压入一个数据压入 5 个后再弹 3 个。测试代码的重点不是完整工程逻辑而是验证后进先出顺序。// OB1 中的测试逻辑仅用于验证堆栈顺序 IF #TestEnable THEN #CycleCounter 1; IF #CycleCounter 10 THEN #CycleCounter : 0; IF #PushCount 5 THEN #PushCount 1; #TestValue : #PushCount; Stack_DB( Push : TRUE, Pop : FALSE, DataIn : #TestValue, DataOut #ReadBack ); ELSIF #PopCount 3 THEN #PopCount 1; Stack_DB( Push : FALSE, Pop : TRUE, DataIn : #TestValue, DataOut #ReadBack ); END_IF; END_IF; END_IF;这里每一轮循环只调用一次 FB折合下来压栈操作之间间隔 10 个扫描周期足够在监控表中看清数据变化。压栈 5 个数据后再连续弹栈 3 次如果看到ReadBack依次返回 5、4、3说明堆栈的先进后出行为正确。DataIn在弹栈时虽然仍带着旧值但因为Pop优先逻辑FB 不会把DataIn压入栈中。5.2 用监控表观察数据顺序先看 Count再看数组下标在 TIA 博途 V15 的监控表中添加背景 DB 变量Stack_DB.StackArray[0]到Stack_DB.StackArray[4]以及Stack_DB.Top和Stack_DB.Count。压入 1、2、3 后Top等于 3数组下标 0、1、2 分别存放 1、2、3。紧接着弹栈一次Top变成 2但数组下标 2 的旧数据仍然等于 3。这时Count已经等于 2因此下游设备应该根据Count而不是数组残留值来判断有效元素个数。如果监控表中发现Top和Count不一致说明你改动了代码里#Count : #Top这一行务必保持两者同步更新。5.3 优化把输出标志集中成一个状态字减少接口变量数量当项目里需要多个堆栈时每个 FB 输出都用单独的Full、Empty和Count会占用大量接口变量和 DB 空间。更稳妥的做法是在 FB 的输出区增加一个State : DWORD用位 0 表示Empty位 1 表示Full位 2 表示Overflow然后把这几个标志的更新集中在最后两行#State.%X0 : (#Top 0); #State.%X1 : (#Top 10); #State.%X2 : (#PushCmd AND (#Top 10));这样外部程序只需要判断一个State变量缩短了调用时的实参列表也让监控界面更干净。如果你的 PLC 型号对 DB 容量敏感这个优化值得做如果只是单栈应用保留独立的Full和Empty反而更容易读程序。真正要避免的是同时保留Count和另一个Depth变量因为两个变量会漂移。永远只让Count作为唯一的事实来源弹栈压栈后都由它更新。本文还有配套的精品资源点击获取