搞PLC项目的朋友,应该都绕不开交通灯这个经典控制对象。尤其用博途(TIA Portal)配S7-1200 PLC,再带一块HMI触摸屏,把一套十字路口交通灯做出来并跑仿真,不仅适合练手,还能把定时器、状态机、HMI联动、PLCSIM仿真这一条完整链路一次性打通。这套“基于博途1200PLC+HMI的交通灯控制系统仿真程序”,我在学习阶段和后面做现场项目前前后后调了很多轮,今天就把其中能复现的细节、容易踩的坑和背后的设计逻辑全写出来。
这篇文章不是简单把某个成品程序甩给你,而是从需求拆解开始,一步一步走到PLC程序、HMI画面、仿真联调,最后再聊聊怎么把它扩展成一个更接近真实工程的东西。适合刚接触TIA Portal的初学者,也适合自己写过一段梯形图但还没系统做过HMI联调的人。不管是做课程设计,还是把交通灯当成状态机练手项目,下面这套思路都可以直接抄作业。
1. 交通灯控制的核心逻辑拆解
1.1 先认清需求:这不是让几个灯循环亮灭那么简单
很多人上来就在PLC里写定时器,定时器一到就切换输出,看起来好像没问题,但真到现场或者HMI仿真的时候会发现一堆毛病:灯号切换瞬间会跳变、主路和支路同时绿灯、急停之后恢复不了初始状态。这些问题的根源在于没有把控制对象抽象成“状态”,而是当成了一堆独立的定时器在硬拼。
交通灯控制本质是一个顺序控制过程。以典型的十字路口为例,主路和支路按固定相位轮流放行,每个相位里又包含绿灯、黄灯、红灯三种灯色。控制程序要保证在任何时刻,两个方向的实际放行灯不能互相冲突,也就是不能出现主路绿灯和支路绿灯同时亮的物理矛盾。
我的做法是先画一张相位图,把整个周期拆成四个状态:
- 状态S0:主路绿灯20秒,支路红灯
- 状态S1:主路黄灯3秒,支路红灯
- 状态S2:主路红灯25秒,支路绿灯
- 状态S3:主路红灯,支路黄灯3秒
这四段加起来一共51秒,形成一个循环。主路绿灯20秒是主干道放行时间,支路绿灯25秒是因为支路方向在S2和S3里红灯和黄灯都并着,实际放行更久一点?不对,这里我故意把支路绿灯设为25秒,是为了演示不同相位的时长可以独立调整。实际项目里你完全可以按路口的车流量把绿灯设置成可调,这就是后面要讲到的“绿灯时长参数化”。
1.2 状态机为什么比一连串定时器更靠谱
如果你用四个TON定时器分别控制四个状态,不可避免会碰到“定时器到时了但前一个状态还没退出”的竞争问题。比如主路绿灯定时器到20秒,立刻把主路绿灯输出清零,同时另一个定时器又把主路黄灯置位,如果这两个输出不是在同一个程序扫描周期内顺序执行的,就会出现某一个扫描周期里主路绿灯还没灭、黄灯已经亮了的情况。
更稳的做法是用一个整型变量表示当前状态,每个扫描周期只允许状态做一次迁移。状态迁移的条件就是对应相位的定时器完成位Q。输出信号不是通过单独触点去驱动,而是根据当前状态值统一计算。简单说,输出只是状态的一种“投影”,状态没有到哪个阶段,对应的灯就不可能亮。
这样设计的好处有三个:
- 输出电压天然互锁,因为同一状态只会给一组输出赋真值
- 状态迁移路径清晰,加减一个状态只改状态表,不用改动一大坨网络
- 仿真和HMI联调的时候,可以在HMI上直接显示当前状态值和剩余时间,非常直观
2. 博途工程搭建与硬件组态要点
2.1 我选用的软硬件环境
这个项目我用的环境是TIA Portal V16/V17均可,CPU选S7-1200系列里的CPU 1214C DC/DC/DC,HMI选KTP700 Basic或者KTP400 Basic都行。CPU 1214C对于这种小项目来说IO点数完全够,而且自带以太网口,仿真下载不需要额外转USB编程电缆。
有人问为什么不用Smart系列或者200 SMART,主要是因为S7-1200配合博途的PLCSIM仿真和HMI联动体验更好,WinCC Runtime Advanced对HMI变量的在线监控也更顺手。如果你手头只有S7-1200 PLC实体机没有HMI触摸屏,一样可以只装PLCSIM跑仿真,HMI画面用WinCC Runtime的集成仿真跑。这块我放在第四章详细说。
2.2 变量表规划比写程序更重要
交通灯控制本身不复杂,但涉及PLC和HMI通信,如果变量名乱七八糟,后面联调会非常痛苦。我的习惯是先建一张统一的PLC变量表,所有HMI变量、PLC程序变量都在这一张表里定义,地址以I/Q/M为主,中间状态变量用DB块或者M区都行,但尽量集中在DB块里,方便在线监控。
下面是这个程序的基础IO和内部变量规划,直接抄就行:
| 变量名 | 地址/类型 | 作用 |
|---|---|---|
| Start_PB | I0.0 Bool | 启动按钮(HMI与实体按钮并联) |
| Stop_PB | I0.1 Bool | 停止/急停按钮 |
| Run_Flag | M10.0 Bool | 运行标志,True表示系统运行 |
| Current_Step | M11.0 Int | 当前状态值 0~3 |
| Main_Green | Q0.0 Bool | 主路绿灯输出 |
| Main_Yellow | Q0.1 Bool | 主路黄灯输出 |
| Main_Red | Q0.2 Bool | 主路红灯输出 |
| Side_Green | Q0.3 Bool | 支路绿灯输出 |
| Side_Yellow | Q0.4 Bool | 支路黄灯输出 |
| Side_Red | Q0.5 Bool | 支路红灯输出 |
| Step_Remain | MW12 Int | 当前状态剩余秒数,HMI显示用 |
特别注意,PLC输出双线圈问题在交通灯程序里特别容易犯。如果你在OB1里用了“主路绿灯”这个输出线圈,又在某个FC里再次用“主路绿灯”线圈,博途编译可能不报错,但实际输出会以最后扫描到的那个网络为准,而且仿真时很难查。所以所有输出信号只允许在一个块里做最终赋值,其他位置只能用变量做运算,不能直接写Q点。
2.3 HMI组态中的网络连接设置
HMI添加完成后,第一步就是设置与PLC的连接。在博途左侧项目树里双击“设备与网络”,把PLC和HMI都拖到网络视图中,用以太网线连接起来。HMI侧选择集成以太网接口,PLC侧选择PROFINET接口即可。
这里有个新手常犯的错:PLC的IP地址和设备名称要和HMI的连接参数保持一致,不然下载完HMI画面之后,运行时HMI会报“连接中断”或者变量显示为“###”。我一般固定PLC IP为192.168.0.1,HMI IP为192.168.0.2,子网掩码255.255.255.0,并且把“允许从HMI访问PLC”的复选框勾上。
组态完连接后,还要确认HMI变量表里是否自动生成了连接变量。HMI上想操作PLC里面的M10.0启动位,必须在HMI变量表里新建一个变量指向PLC变量“Start_PB”或者直接指向M10.0。不要直接在画面对象的属性里复制粘贴PLC地址,那不靠谱。
3. PLC控制程序实现:状态机 + TON定时器
3.1 程序块结构怎么安排
我的程序结构非常简单但清晰:
- OB1主程序:负责调用控制函数块、处理启动停止
- FB1 交通灯控制函数块:核心状态机
- FB2 HMI数据接口函数块:把状态值和剩余时间映射到HMI变量(小型项目可以直接放OB1里,但拆开更整洁)
- DB1 数据块:存放所有内部变量
FB1内部使用静态变量,TON定时器可以直接在FB里声明成静态实例,不必每用一次定时器就建一个独立背景DB。这样变量都集中在一个背景数据块里,在线监控非常方便。
FC与FB的区别这里多说一句:FC没有背景数据块,中途断电停机后内部变量会丢;FB有背景DB,变量会被保留。交通灯这种顺序控制强烈建议用FB,因为状态值Current_Step绝对不能因为扫描周期的变化或者CPU停止启动就凭空丢成0。如果丢成0,灯号会瞬间回到主路绿灯,现场极可能出事故。
3.2 核心状态机SCL代码参考
下面这段是FB1里真正干活的SCL代码。如果你习惯梯形图,也可以按同样的逻辑转成梯形图网络,但SCL写顺序控制会更紧凑,我强烈建议新手试着读一遍。
// FB1: Traffic_Light_Control // 输入输出通过背景DB引用的方式定义,此处在属性里设置 IF NOT "Run_Flag" THEN // 停止状态:所有输出清零,状态回到0 "Main_Green" := FALSE; "Main_Yellow" := FALSE; "Main_Red" := FALSE; "Side_Green" := FALSE; "Side_Yellow" := FALSE; "Side_Red" := FALSE; "Current_Step" := 0; RETURN; ELSE // 根据当前状态计算输出 CASE "Current_Step" OF 0: "Main_Green" := TRUE; "Main_Yellow" := FALSE; "Main_Red" := FALSE; "Side_Green" := FALSE; "Side_Yellow" := FALSE; "Side_Red" := TRUE; 1: "Main_Green" := FALSE; "Main_Yellow" := TRUE; "Main_Red" := FALSE; "Side_Green" := FALSE; "Side_Yellow" := FALSE; "Side_Red" := TRUE; 2: "Main_Green" := FALSE; "Main_Yellow" := FALSE; "Main_Red" := TRUE; "Side_Green" := TRUE; "Side_Yellow" := FALSE; "Side_Red" := FALSE; 3: "Main_Green" := FALSE; "Main_Yellow" := FALSE; "Main_Red" := TRUE; "Side_Green" := FALSE; "Side_Yellow" := TRUE; "Side_Red" := FALSE; END_CASE; END_IF;等等,上面这段SCL只写了输出,还没写状态迁移,而且并不完整。真正完整的状态迁移可以有两种写法:一种是用四个TON实例分别计时,每步状态内激活对应的TON;另一种用一个秒脉冲计数器,用Current_Step配合Time_Count计数切换。我更推荐前者,因为你项目里如果每个相位时长都不同,TON的PT参数可以做成变量,后续在HMI上直接改绿灯时长才方便。
3.3 完整版状态迁移逻辑(推荐写法)
在FB1里声明四个TON定时器实例:
VAR Timer_S0 : TON; Timer_S1 : TON; Timer_S2 : TON; Timer_S3 : TON; Step_Time : Array[0..3] of Int := [20, 3, 25, 3]; // 各状态时长 END_VAR主逻辑如下:
// 状态迁移 CASE "Current_Step" OF 0: Timer_S0(IN := "Run_Flag", PT := UDINT_TO_TIME(INT_TO_UDINT(Step_Time[0]) * 1000)); IF Timer_S0.Q THEN "Current_Step" := 1; END_IF; 1: Timer_S1(IN := "Run_Flag", PT := UDINT_TO_TIME(INT_TO_UDINT(Step_Time[1]) * 1000)); IF Timer_S1.Q THEN "Current_Step" := 2; END_IF; 2: Timer_S2(IN := "Run_Flag", PT := UDINT_TO_TIME(INT_TO_UDINT(Step_Time[2]) * 1000)); IF Timer_S2.Q THEN "Current_Step" := 3; END_IF; 3: Timer_S3(IN := "Run_Flag", PT := UDINT_TO_TIME(INT_TO_UDINT(Step_Time[3]) * 1000)); IF Timer_S3.Q THEN "Current_Step" := 0; END_IF; END_CASE;注意这里为什么要把Step_Time转换成TIME类型,而不是直接在TON的PT里写T#20S?因为你一旦想把绿灯时长做成HMI可调,就必须让定时器的PT是变量。SCL里通常用毫秒数乘以1000再转成TIME,实务上很好用。
还有个关键点:TON的IN信号到底接什么。很多人把TON的IN接到当前状态激活位上,比如“Current_Step=0”,那状态一旦切走,定时器IN变False,定时器立即复位。下次再回到这个状态时,又从0重新计时,这正好是我们要的。但如果你把IN接到一个长期True的Run_Flag上,那定时器在整个周期里一直计时,到点之后必须手动复位,否则下次再回到该状态时定时器Q还是True,状态会连续跳好几次。所以我上面直接把IN接到Run_Flag,但依赖状态切换后重新给定时器复位?不对,TON在IN保持True时不会自动复位,Q会一直True,即使CASE已经离开当前状态,下一次扫描进入其他分支没有被调用,但如果T0.Q一直是True并且再次进入状态0时会立即跳到状态1。
这就是一个非常容易踩的坑。正确做法是让每个TON只在对应状态时计时,同时对其复位。比如:
Timer_S0(IN := ("Current_Step" = 0) AND "Run_Flag", PT := ...);IN为False时TON自动复位定时值和Q位。所以状态一切走,这个定时器就归零,下次再进状态0时从头开始计时。这比单独写RESET复位指令更可靠。
同理,Timer_S1、Timer_S2、Timer_S3的IN分别是("Current_Step"=1) AND Run_Flag、("Current_Step"=2) AND Run_Flag、("Current_Step"=3) AND Run_Flag。这样状态切换干净利落,绝对不会产生连跳问题。
3.4 输出不能直接放在状态迁移之前
我刚才把输出计算和状态迁移分开写了。实际运行时,我会把输出计算放在状态迁移的后面,也就是先判断当前状态是否需要切换,切换完Current_Step已经是新值,再用新值去刷新所有输出。如果在状态迁移之前刷新输出,那Current_Step还是旧状态,输出的就会是旧的灯色,等下一扫描周期才会更新到新灯色,虽然只差一个扫描周期,但HMI上看起来会有几毫秒的闪变,而且在线监控时会发现状态值和输出不对应,非常别扭。
标准顺序是:
- 读取当前状态Current_Step
- 根据当前状态激活对应TON定时器
- 如果定时器Q到点,则更新Current_Step
- 根据更新后的Current_Step计算所有输出
- 把Current_Step和Step_Remain写入HMI显示变量
Step_Remain怎么算?这里我常用一个小的技巧:用当前计时值和设定值做差。SCL里可以用TON的ET读取已经计时的时间,但ET是TIME格式,不好直接显示成秒数。我一般把Step_Time[Current_Step]减去ET除以1000后的毫秒取整,就能得到剩余秒数。
3.5 启动停止和急停逻辑
启动和停止处理放在OB1里最简单。启动按钮I0.0的上升沿把Run_Flag置True,停止按钮I0.1的上升沿把Run_Flag复位为False。同时如果系统处于停止状态,Current_Step被强制为0,所有输出清零,HMI上的灯全部熄灭。
这里有个细节,交通灯控制不能一停止就把所有灯全灭。真实路口如果全灭会导致交通混乱,所以工程上通常把停止按钮设计成“全红闪”或者“夜间黄灯闪”。我在仿真程序里留下了一个Night_Mode变量,如果为True,所有状态输出里只有黄灯以1Hz频率闪烁,其他灯全灭。具体实现可以用一个1Hz时钟脉冲M0.5,在输出计算时判断:
IF "Night_Mode" THEN "Main_Yellow" := "Clock_1Hz"; "Side_Yellow" := "Clock_1Hz"; "Main_Green" := FALSE; ... RETURN; END_IF;这样仿真的时候你可以通过HMI按钮切换到“夜间模式”,比单纯的启停演示更有工程味道。
4. HMI画面设计与仿真联调实操
4.1 HMI画面布局怎么做才直观
HMI画面我一般做一个十字路口俯视图,两条道路交叉,六个灯分布在道路两侧:主路方向上下各一组,支路方向左右各一组。每组灯用三个圆形对象叠放,分别对应红、黄、绿。每个圆形的“背景颜色”属性直接关联对应PLC变量,比如主路绿灯的圆形的“Background Color”关联Main_Green变量,取值为True时显示绿色,False时显示深灰色。
听起来很简单,但博途HMI的颜色动画有两种做法:一种是用“动画-外观”添加一条规则,变量为True时背景绿色,False时背景灰色;另一种是直接在每个圆的“闪烁”属性里关联变量。推荐使用动画外观,因为仿真时看到的颜色变化最直观。
除了灯之外,画面上还要放:
- Start按钮:Momentary型,按下时置位Start_PB
- Stop按钮:Momentary型,按下时置位Stop_PB
- 当前状态显示:用I/O域关联Current_Step,或用文本列表把0~3映射成“主路绿灯”“主路黄灯”等文字
- 剩余时间显示:I/O域关联Step_Remain
- 夜间模式切换开关:切换一个M位
这样整个HMI画面就不仅是“看灯”,还能操作、能监视状态,仿真的完整度一下就上来了。
4.2 PLCSIM仿真与HMI联调的正确步骤
很多人卡在这一步:PLC程序下载到PLCSIM没问题,但HMI点仿真运行之后连不上PLC。我总结了一套稳定流程:
- 在博途里编译整个项目,确保没有错误
- 先启动PLCSIM,把PLC程序下载进去,这时候能看到CPU的模块指示灯变成绿色,然后点击“运行”让CPU进入RUN状态
- 确认PLCSIM里的PLC IP地址和HMI组态的一致
- 找到HMI设备,右键选择“仿真”或“启动仿真”,而不是在项目树里直接点HMI运行
- 如果使用WinCC Runtime Advanced,会弹出一个独立HMI仿真窗口
- 如果HMI连不上,回到“设备与网络”视图,重新检查连接线的“子网”是否共享
用PLCSIM跑HMI联调最大的优势是,你不需要真实PLC和触摸屏就能把变量读写、画面动画、按钮操作全部验证一遍。而且PLCSIM里能直接在线修改PLC变量,比如把Step_Time数组里的值改成5秒,HMI上下一周期立刻就能看到绿灯时间变成5秒。
4.3 HMI仿真时常见连接错误
我在打样阶段遇到过最典型的一个错误是:HMI仿真窗口打开后,灯号一直显示“###”这种无效值。这通常是PLC地址在HMI变量里映射错了,或者PLC根本没运行。
还有一种情况是启动按钮按下去没有反应。检查HMI变量“Start_PB”的操作模式,必须设置为“按下”时置位,并且写入到PLC变量的地址要是I0.0。如果用的是M10.0这类内部位,仿真和实体PLC都能用,但按钮按下后没有保持,你需要一个置位一复位逻辑。我一般不用I0.0直接做HMI操作,而是定义两个内部位:HMI_Start和HMI_Stop,在HMI上写M20.0和M20.1,程序里对这两个位做上升沿。这样和实体按钮完全不冲突,也方便后面把实体板卡接上去。
5. 仿真调试中的典型坑与提速方法
5.1 状态连跳:为什么有时候黄灯一闪而过
这是我在调试中踩过最深的一个坑。表面上是定时器到点后状态跳过了某个中间状态,实际上是因为TON定时器的Q位没有及时复位。尤其是当CASE状态迁移写在定时器调用之前时,定时器Q还是True,下一次扫描又满足迁移条件,状态就会连续跳两次。
检查方法也简单:在博途的在线监视里,把四个TON的Q位和Current_Step同时放进监控表,然后盯着它们看。如果状态从0跳到1的瞬间,Timer_S0的Q位还保持True,那就要检查它的IN信号。只要IN只在对应状态为真,切换状态后IN变为False,定时器自动复位,这个问题就会消失。
5.2 输出互锁不可省
虽然状态机能保证同一状态下主路绿灯和支路绿灯不同时亮,但为了安全,工程上还是建议在最终输出网络里加一道硬件/软件互锁。软件互锁可以这样写:
"Main_Green" := ("Main_Green_Logic") AND NOT "Side_Green" AND NOT "Side_Yellow"; "Side_Green" := ("Side_Green_Logic") AND NOT "Main_Green" AND NOT "Main_Yellow";这种“双保险”在实际项目里非常实用。仿真程序可能看不出区别,但如果你以后把程序下载到真实PLC再接到继电器接触器上,万一接触器卡死或者输出回路短路,互锁能帮你兜住底。
5.3 如何把调试时间压缩一半
我强烈建议你在写程序之前就把HMI变量表和交通灯状态表格画出来。表格长这样:
| 状态 | 主路绿灯 | 主路黄灯 | 主路红灯 | 支路绿灯 | 支路黄灯 | 支路红灯 | 时长 |
|---|---|---|---|---|---|---|---|
| S0 | 亮 | 灭 | 灭 | 灭 | 灭 | 亮 | 20s |
| S1 | 灭 | 亮 | 灭 | 灭 | 灭 | 亮 | 3s |
| S2 | 灭 | 灭 | 亮 | 亮 | 灭 | 灭 | 25s |
| S3 | 灭 | 灭 | 亮 | 灭 | 亮 | 灭 | 3s |
把这张表打印出来放在手边,然后严格按照状态表去写CASE分支。调程序时一旦发现主路黄灯在S0就亮了,十有八九是状态表和输出语句不一致。这种表格化检查比盯着一大坨梯形图网络找问题快得多。
另一个提速方法是,在HMI上把所有Step_Time做成可写输入域。改一个绿灯持续时间,不需要重新编译下载,直接在HMI运行画面上改成5秒,下一个周期就能看到效果。这个特性在做“时序验证”时能省很多来回编译的时间。
6. 怎么把它从“课设”变成“能用的工程”
6.1 加入倒计时显示
真实路口都有倒计时数字屏,HMI仿真也能模拟。实现思路是在HMI画面中放一个I/O域,关联Step_Remain,再把当前状态的灯色也显示成文字。数字会从20、19、18往下跳,直到0然后切换状态。如果想让HMI上的数字秒级刷新,不需要额外写PLC程序,Step_Remain本来就用秒为单位更新,刷新周期默认为1秒,显示效果刚好。
如果你愿意更进一步,可以把倒计时也和灯色绑定。比如主路绿灯倒计时显示绿色,主路黄灯倒计时显示黄色,HMI动画里加一条规则根据Current_Step改变字体颜色即可。
6.2 增加传感器优先功能
一个固定时序的交通灯是“定时控制”,真实路口往往要加“感应控制”。这个项目可以继续扩展:在主路入口加一个车辆检测传感器,模拟输入I0.2表示有车等待。当主路绿灯已运行20秒,支路方向有车等待,且主路绿灯最小放行时间已满足,那就允许提前切换;如果主路一直有车,就延长绿灯最大到30秒。
这个逻辑本质上就是给状态0增加两个提前退出条件:一个是最小时间到,一个是有车等待。加上最大时间保护,避免某方向车辆连续放行导致另一个方向饥饿。这已经是非常接近工程实战的控制思路了。
6.3 把程序结构化,对接真实设备
仿真程序做完以后,我建议你把FB1的接口改成标准化的输入输出参数:Start、Stop、Night_Mode、Phase_Time_Array、Main_Lamp_Output、Side_Lamp_Output。这样将来换成真实PLC和真实信号灯板时,不需要改内部逻辑,只需要把实体输入输出地址和接口变量对上。
另外,HMI画面里的按钮、I/O域、灯号对象,尽量用PLC变量而不是HMI内部变量。虽然HMI内部变量不需要占用PLC地址,但工程里后期维护时会非常考验人的记忆。所有操作按钮、指示灯、报警都走PLC变量,以后谁接手都能通过PLC程序看懂HMI在做什么。
6.4 程序版本管理和记录
最后分享一个小习惯。我做交通灯这个项目时给每个版本建了一个简单的修改记录文本,放在博途项目文件夹里。比如V1.0完成四状态固定时序,V1.1增加夜间模式,V1.2增加HMI时长可调。每一次仿真验证后,如果发现时序、变量名、画面布局有改动,就同步更新记录。看上去有点土,但等你想回头找“上一个版本为什么没有夜间模式”的时候,这个记录比什么版本管理工具都好用。
交通灯控制系统的仿真程序,表面上是练手项目,实际上把博途的硬件组态、PLC编程、HMI组态、仿真联调和工程规范都串起来了。我做了这么多次之后最大的体会是,别急着写代码,先把状态表画清楚,把HMI和PLC变量对齐,剩下的就是按图施工。你在调这个小项目时磨出来的这套流程,以后做再复杂的设备,也还是这条路。