
搞PLC项目的朋友应该都绕不开交通灯这个经典控制对象。尤其用博途TIA Portal配S7-1200 PLC再带一块HMI触摸屏把一套十字路口交通灯做出来并跑仿真不仅适合练手还能把定时器、状态机、HMI联动、PLCSIM仿真这一条完整链路一次性打通。这套“基于博途1200PLCHMI的交通灯控制系统仿真程序”我在学习阶段和后面做现场项目前前后后调了很多轮今天就把其中能复现的细节、容易踩的坑和背后的设计逻辑全写出来。这篇文章不是简单把某个成品程序甩给你而是从需求拆解开始一步一步走到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/DCHMI选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_PBI0.0 Bool启动按钮HMI与实体按钮并联Stop_PBI0.1 Bool停止/急停按钮Run_FlagM10.0 Bool运行标志True表示系统运行Current_StepM11.0 Int当前状态值 0~3Main_GreenQ0.0 Bool主路绿灯输出Main_YellowQ0.1 Bool主路黄灯输出Main_RedQ0.2 Bool主路红灯输出Side_GreenQ0.3 Bool支路绿灯输出Side_YellowQ0.4 Bool支路黄灯输出Side_RedQ0.5 Bool支路红灯输出Step_RemainMW12 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.1HMI 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_Step0”那状态一旦切走定时器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_Step1) AND Run_Flag、(Current_Step2) AND Run_Flag、(Current_Step3) 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_PBStop按钮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亮灭灭灭灭亮20sS1灭亮灭灭灭亮3sS2灭灭亮亮灭灭25sS3灭灭亮灭亮灭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变量对齐剩下的就是按图施工。你在调这个小项目时磨出来的这套流程以后做再复杂的设备也还是这条路。