把西门子博图(TIA Portal)里的自学Demo升级成真正能稳定运行在现场的大型项目程序,这个跨度比很多人想象的大得多。我接手过一套超市储藏环境自动控制项目,程序里二十多个FB、上百个DB变量、五路Modbus轮询,还要同时处理掉电保存和现场电表的数据读取。那段时间最大的感受是:1200PLC虽然是紧凑型控制器,但把它当成“大型项目程序”的载体去规划时,考验的根本不是指令会不会写,而是数据往哪放、缓存怎么做、通信怎么不把扫描周期拖垮。这篇文章就把实战里踩出来的经验拆开讲,适合已经从点亮一盏灯、跑通一个模拟量,准备真正上手多设备、多状态、多通信项目的工程师。这算是“开启自动化编程新征程”的第一课吧。
1. 大型项目的第一道坎:先做数据规划,再谈程序逻辑
1.1 从IO表到符号表:“拉完网络再说”为什么行不通
很多人写1200PLC程序是拿到点位表就开写,梯形图一拉一大片,只要设备能动作就算交差。这个思路在几十个点的项目里问题不大,但一旦进入大型项目,设备数量、传感器数量、模式切换、报警分级全部堆在一起,没有一张干净的IO清单和符号表,后面每个程序块都会因为命名混乱而反复返工。
我做仓储环境控制项目的第一步,是把所有IO信号整理成一张表,包含信号名称、PLC地址、信号类型、量程、来源设备、安全状态六列。不要嫌这一步麻烦,这张表决定了后续的符号名、中间变量、报警文本甚至触摸屏变量能不能对得上。博图的PLC变量表可以按IO地址自动排序,但“哪个信号属于哪台设备”这种业务关系,变量表不会替你想清楚,必须自己在规划阶段分层。
以一套常见的冷库储藏间为例,IO点表大致长这样:
| 区域 | 信号名称 | 地址 | 类型 | 量程/说明 | 安全状态 |
|---|---|---|---|---|---|
| 1号储藏间 | 温度传感器 | AI0 | 4-20mA | -50~50℃ | 超限报警 |
| 1号储藏间 | 湿度传感器 | AI1 | 4-20mA | 0~100%RH | 超限报警 |
| 1号储藏间 | 制冷接触器反馈 | I0.0 | DI | 常开 | 反馈丢失报警 |
| 1号储藏间 | 制冷接触器输出 | Q0.0 | DO | 继电器输出 | 断开为安全 |
| 1号储藏间 | 加热接触器输出 | Q0.1 | DO | 继电器输出 | 断开为安全 |
| 1号储藏间 | 排风机运行反馈 | I0.1 | DI | 常开 | 反馈丢失报警 |
| 1号储藏间 | 排风机输出 | Q0.2 | DO | 继电器输出 | 断开为安全 |
地址规划建议按设备或区域分段。I0.0到I5.7留给第一套制冷机组和风机,Q0.0到Q2.7留给执行机构,模拟量通道按AI模块顺序排。这样调试时只看地址段就能猜出大概是什么设备,比反复翻符号表快得多。博图“从设备组态生成变量”功能能省一部分工作,但它只适合生成IO变量,中间过程变量必须自己手动规划。
1.2 存储区规划:把DB块当成数据库来设计
大型项目的数据量远超直觉。一套仓储系统把温度、湿度、报警、设备状态、通信帧缓存、掉电保持参数全部加起来,经常超过几百个变量。如果全部放在M存储区,不仅地址容易冲突,在线监视时也不好归类。S7-1200的M区地址空间并不富裕,所以我在中型以上项目里的习惯是:业务数据全部放进DB块,M区只放临时标志位和系统状态。
DB块怎么分?我的固定套路是五类:
- 配置类DB:人工设定的目标温度、报警阈值、通信参数。这类数据需要掉电保持,单独放一个DB。
- 过程数据DB:传感器实时值、设备状态、模拟量工程值。不需要保持,上电后自动重新采集。
- 统计类DB:累计运行时间、故障次数、能耗数据。低频写入但需要保持,单独一个DB。
- 通信DB:和外部设备交换的原始寄存器值、解析后的工程量。独立出来,查通信问题会非常快。
- 配方/场景类DB:常温库、冷藏库、速冻库各一套环境参数预案,按需装载到配置DB。
这样分完之后,哪怕新接手的工程师打开项目看DB名字,也能知道数据在哪、掉电会不会丢、改参数该动哪里。这一步必须在写任何逻辑之前完成。
此外,S7-1200支持UDT(用户自定义类型)。如果现场有十几台同型号的制冷机组,千万不要为每一台复制一套变量。定义一个“机组数据结构”UDT,然后在DB里建一个数组,下标对齐设备编号。后面用循环处理设备逻辑会舒服很多。顺便提醒一个坑:S7-1200的标准访问DB数组,不允许用变量做下标,只能写常量;而优化访问的DB没有这个限制。所以环形缓冲区这类需要在运行时用变量下标访问的数组,一定要放在优化访问的DB里。
1.3 程序块的分工:OB、FB、FC、DB各干各的
大型程序必须按功能划分程序块。博图里S7-1200的程序结构,一般是OB1做主扫描,OB30这类循环中断OB做周期性任务,FB封装设备控制逻辑,FC封装公用算法,DB保存数据。
我的划分原则很简单:控制一台设备的完整状态逻辑用FB,因为FB自带背景数据块,能记住每台设备的内部状态;纯计算、纯转换用FC,它不占背景数据,适合做模拟量换算、数组处理、校验计算这类无状态操作。不要在OB1里堆几十个网络,即使能跑,后期维护会非常痛苦。
实际项目中,我会为每类设备建一个FB:制冷机组FB、风机组FB、加湿除湿FB,然后通过全局DB统一管理这些FB的实例。这样修改某台设备的控制策略,只动对应的FB,其他设备实例会自动更新,不会因为复制改漏而出现“1号机组正常、2号机组行为诡异”的现象。
2. 数据缓存与掉电保存:S7-1200里“数据放哪”的实战选择
2.1 先分清“运行缓存”和“掉电保持”
“1200PLC怎么写存储数据缓存程序”这个问题被问得很多,但很多提问者没搞清楚自己到底要解决哪类问题。我一般把存储需求拆成三种场景:
- 场景A:触摸屏或上位机读取速度慢,PLC侧数据变化太快,需要暂存一段历史供稍后读取。
- 场景B:通信对方(比如电表、扫码枪)一次只能处理一帧数据,PLC要把多条数据排队发送,需要一个发送缓冲队列。
- 场景C:现场突然断电,但设备运行数据(配方号、累计产量、停机原因)要保留,下次上电能恢复。
前两种属于“运行缓存”,用数组或FIFO就能解决;第三种属于“掉电保持”,靠的是保持性存储区的正确配置。很多人把两者混为一谈,所以不管怎么写都别扭。
运行缓存最典型的实现是环形缓冲区。它解决的核心问题是:写入端和读取端的节奏不一致时,新数据不能把还没处理的旧数据覆盖掉,读取端每次拿到最旧的一笔数据,这就是FIFO语义。
2.2 环形缓冲区:用SCL实现一个FIFO
我用SCL写过一个通用环形缓冲区,思路很简单:定义一块数组作为存储区域,用两个指针分别记录写入位置和读取位置,数据写满时置“满”标志,读空时置“空”标志。
这里用一个简化版本说明核心逻辑。假设数据结构已经通过UDT定义好了:
// UDT: 一条存储记录 TYPE "MyDataStruct" VERSION : 0.1 STRUCT timestamp : DTL; // 记录时间 value : Real; // 测量值 status : Int; // 状态字 END_STRUCT END_TYPEFB内部定义一个深度为100的数组和两个指针:
VAR buffer : ARRAY[0..99] OF "MyDataStruct"; writePtr : Int := 0; readPtr : Int := 0; itemCount : Int := 0; full : Bool := FALSE; empty : Bool := TRUE; END_VAR写入逻辑:
IF enWrite AND NOT full THEN buffer[writePtr] := newData; IF writePtr >= 99 THEN writePtr := 0; ELSE writePtr := writePtr + 1; END_IF; itemCount := itemCount + 1; empty := FALSE; IF itemCount >= 100 THEN full := TRUE; END_IF; END_IF;读取逻辑:
IF enRead AND NOT empty THEN dataOut := buffer[readPtr]; IF readPtr >= 99 THEN readPtr := 0; ELSE readPtr := readPtr + 1; END_IF; itemCount := itemCount - 1; full := FALSE; IF itemCount <= 0 THEN empty := TRUE; END_IF; END_IF;这个示例里的MOD运算我故意换成了IF判断,因为虽然SCL支持MOD,但用IF在PLC上执行效率更高一点。缓冲区深度不是越高越好,每增加一条记录,DB占用就多一块,要根据通信数据量和需求计算。做电表数据缓存时,我一般把深度设成200条左右,足够覆盖上位机10分钟不读取的极端情况。
2.3 掉电保持的取舍:配置Retain前先想清楚写次数
掉电保持是另一个大坑。S7-1200的DB变量可以在DB编辑器里单独设置“保持”属性,但保持性存储区域是有写次数寿命的。如果把累计运行时间这种每秒都在变的量设成保持,PLC会反复擦写非易失存储区,时间一长这块区域就可能损坏,扫描周期也会被拖慢。
我的分级方案是:
- 必须保持且低频写入的数据:配方号、温度阈值、通信参数、设备选型配置。这几类可以放心设Retain。
- 需要保存但高频变化的数据:累计运行时间、故障次数、能耗累计。不要直接写保持区,建议用数据日志周期写到存储卡,或者每次停机时做一次“暂存”。
- 无需掉电保存的数据:传感器实时值、设备状态、中间计算结果。上电后重新采集建立即可。
博图仿真器在“掉电保持”这块和真实PLC有差异,仿真里看着是保持了,真机上不一定。所以掉电恢复测试一定要在实体CPU上做验证,不要拿仿真结果直接交付。
3. 通信实战:1200PLC读取多功能电表参数
3.1 RS485接线与端口参数,稳定通信的一半在现场
多功能电表接口绝大多数是RS485,支持Modbus-RTU。S7-1200本体没有RS485口,需要挂CM1241 RS485通信模块或CB1241通信板。接线看起来简单,A接A、B接B,但现场最容易出问题的恰恰是这里。
必须用屏蔽双绞线,屏蔽层单端接地。总线上最远的两台设备要接终端电阻,如果模块支持软件启用终端电阻,记得在组态里打开。波特率、数据位、校验位必须和电表手册完全一致,常见默认是9600, 8, E, 1,但也有不少电表出厂是9600, 8, N, 1,接上后先读一遍设备型号,确认参数再继续。
通信线千万不要和动力电缆走同一个线槽。我遇到过现场怎么调都超时,最后发现是通信线跟变频器输出线平行走了十几米,屏蔽层还没接地。重新走管之后,连续跑了一个月没掉过线。这种问题,组态和程序怎么优化都解决不了,只能从物理层下手。
3.2 寄存器映射与MB_MASTER调用
博图V4.0以上固件的S7-1200,Modbus-RTU主站指令一般用MB_COMM_LOAD初始化端口,MB_MASTER发起请求。MB_MASTER的REQ要用沿触发,不要一直置TRUE。DATA_PTR指向专门建好的通信DB,数据类型是VARIANT,可以直接把优化访问DB里的数组符号地址传进去。
不同厂家电表的寄存器表完全不同,但通信逻辑大同小异。以常见的多功能电表为例,读取电压、电流、功率、电能时,通常会用到03功能码读保持寄存器,电能这类32位数据由两个连续16位寄存器组成,解析时要按高低字拼成DWord。
| 参数 | 功能码 | 寄存器地址示例 | 数据类型 | 换算说明 |
|---|---|---|---|---|
| A相电压 | 03 | 0x0000 | UInt | 除以10得到实际电压 |
| A相电流 | 03 | 0x0002 | UInt | 除以1000得到实际电流 |
| 有功功率 | 03 | 0x0004 | UInt | 除以10得到实际功率 |
| 正向有功电能 | 03 | 0x0006 | DWord | 两个寄存器拼合,除以100 |
注意:上表是示例,实际地址和系数要以电表出厂手册为准。读取到的原始值放进通信DB后,再做工程量换算,换算后的数据供给监视和报警使用。
3.3 轮询状态机,避免多个MB_MASTER打架
新手最容易犯的错,是把读取多个参数的MB_MASTER全部放在OB1里,每个REQ都置TRUE。Modbus-RTU是半双工总线,同一时间只允许一个请求在线,这样全部会超时报错。
正确做法是做一个轮询状态机:每个周期只发一个请求,收到响应、超时或错误后,再切换下一个请求。状态可以用Int型编号实现:
CASE state OF 0: // 读1号电表电压组 mbReq := TRUE; IF mbDone THEN state := 1; mbReq := FALSE; ELSIF mbError THEN state := 1; mbReq := FALSE; // 记录报警,尝试次数加1 END_IF; 1: // 读1号电表电能组 mbReq := TRUE; IF mbDone THEN state := 2; mbReq := FALSE; ELSIF mbError THEN state := 2; mbReq := FALSE; // 记录报警,尝试次数加1 END_IF; 2: // 读2号电表电压组 // ... 依次类推 END_CASE;每步的间隔建议用定时器控制,或者放到OB30循环中断里周期执行,不要每个扫描周期都触发。这样即使某台电表掉线,程序也只是记录报警并跳过,继续轮询其他设备,不会因为一台设备故障导致整个通信网络卡死。
3.4 扩展:把PLC数据送给其他系统
很多项目不只要读电表,还要把PLC的数据送给能源管理平台或另一套PLC。常见做法有两种:一是PLC作为Modbus TCP服务器,用MB_SERVER指令开放寄存器映射,外部系统按地址读取;二是用S7通信(PUT/GET)做PLC与PLC的数据交换。
MB_SERVER的关键是CONNECT参数的连接描述和寄存器映射表,外部系统感知到的就是一组保持寄存器。用这种方式,上位机或第三方系统对接成本最低,也不用装西门子专用驱动。但要注意开放哪些寄存器、权限怎么控制,现场总线安全这根弦不能松。
4. 实战拆解:超市储藏环境自动控制系统里的大型程序
4.1 需求拆解成IO点表和控制目标
超市储藏环境控制,说白了是控制几个储藏间的温度、湿度、通风、照明和报警。但“控制储藏间”五个字展开后,涉及的东西一点也不少:多路温湿度传感器、制冷机组和加热器、排风机和进风机、门状态、照明、本地触摸屏、远程监控预留。
我把需求拆成三类:
- 环境调节类:温度超高开制冷,温度超低开加热,湿度偏高考虑通风除湿。
- 安全保护类:制冷和加热必须互锁,压缩机避免频繁启停,传感器断线和通信故障要报警。
- 运维管理类:记录设备运行时间、故障次数、门开关状态,方便追溯。
IO点表在前面列过一部分,实际项目里每台设备还要单独做故障反馈。如果制冷接触器吸合了但没有运行反馈,要在几秒内判断反馈丢失并报警停机,避免设备带病运行。
4.2 三层代码结构:模式层、策略层、执行层
我写这套系统时,把程序严格分成三层:
- 模式层:手动、自动、检修、停止。模式决定策略层的请求信号能不能生效。
- 策略层:根据温湿度偏差、时间表、门状态生成“请求制冷”“请求通风”这类逻辑请求。
- 执行层:把策略层的请求映射到物理输出,处理互锁、延时、故障拍停。
这种分层最大的好处是现场调试时逻辑非常清晰。如果制冷机没动作,先看模式层是不是在自动,再看策略层有没有生成制冷请求,最后看执行层的互锁条件是不是被别的信号卡住。每一步都有状态变量可以查,不需要在线反复猜。
温湿度控制算法有两种选择:滞回控制和PID控制。仓储环境这种偏开关量的系统,我建议用滞回控制。低于设定值减去回差启动制冷,高于设定值停止。回差一般设1~2℃,太小会导致设备频繁启动,太大则温度波动明显。PID控制虽然连续,但参数整定和维护成本对小项目来说往往是负担。
4.3 互锁、延时和保护逻辑
制冷和加热的互锁必须做双重:电气上接触器加互锁,程序上策略层也做判断,任何情况下这两个输出不能同时为TRUE。这不是“保证逻辑正确”的问题,而是一旦同时接通,现场可能烧设备甚至出安全事故。
压缩机还要做两款延时保护:
- 启动后最短运行时间:比如至少运行5分钟才能停下,防止刚启动就被温度回差关掉。
- 停机后再启动延时:比如停机后必须等待3分钟才能再次启动,保护压缩机避免频繁启停损坏。
这两个延时不能简单用TON叠加,要做成设备启停状态机。我的做法是在制冷机组FB内部维护一个运行状态字,用状态切换触发计时,状态没到就不允许下一步动作。
故障处理逻辑也有讲究。设备过载或反馈丢失属于重故障,输出要立即断开,并且故障清除后需要手动复位,不能自动恢复。通信临时超时这类轻故障,允许自动恢复,但要把故障次数记录下来,方便统计设备质量。
4.4 现场调试时的验证路径
这套系统到现场后,我调试的顺序是:先单点测试IO输出,确认每个接触器和阀门的动作方向;再切手动模式,逐台操作设备,确认反馈信号;然后切自动模式,用模拟信号改变温度输入,观察策略层请求和执行层输出是否正确;最后做故障注入测试,比如拔掉一个温度传感器,确认报警和停机逻辑能可靠触发。
传感器断线这个点特别容易遗漏。4-20mA模拟量,如果断线电流掉到0mA,程序会把0当成正常温度来处理,这是很危险的。所以模拟量模块断线诊断必须打开,或者程序里判断电流是否低于3.6mA、高于20.8mA,超出范围直接置故障。
5. 大型程序调试里躲不开的坑
5.1 间歇性故障不能靠眼睛盯:诊断缓冲区与数据日志
大型程序调试时,在线监视只能看到当前值。间歇性故障的特点是你不盯它不出现,你一转身它就报警。做仓储环境项目时,有一台制冷机组偶尔会报过热,等了半天都复现不了。最后是在故障触发点加了一段历史记录逻辑,把故障前5秒的传感器值、输出状态、电网电压全部写入存储卡数据日志,第二天翻日志才发现是同一回路两套设备同时启动造成电压跌落。
S7-1200的诊断缓冲区记录了错误事件、时间戳和错误代码,这是排查故障的第一手资料。但要追溯业务层面的数据变化,就得靠数据日志或主动上报。大型程序从一开始就要设计好“关键状态可回溯”,别等出了故障再补。
5.2 强制(Force)变量的正确用法
在线监视时,右键变量可以“修改”或“强制”。修改只生效一个扫描周期,之后会被程序重新写入;强制是持续锁定变量值,直到手动取消。强制用不好会出现“程序逻辑明明不对,设备却一直在动”的诡异现象。
我的习惯是:强制只用于确认IO接线和模块映射是否正常。真正的逻辑调试用监控表的“修改”功能配合程序块在线监视来做。每次下载新程序前,强制变量列表必须清空并检查一遍,防止现场留下隐形的强制变量,交付后出问题。
5.3 版本管理:博图版本不兼容的教训
博图项目只能从低版本升级到高版本,不能从高版本降回低版本。V15项目用V16打开后,V15就打不开了。
我吃过一次亏:办公电脑装的是V15.1,现场调试电脑装的是V16,现场改完程序后回到办公室想打开归档文件做备份,结果直接报“项目由更高版本创建,无法打开”。最后只好又跑一趟现场,用那台电脑重新归档。后来强制规定:每个项目的TIA版本、CPU固件版本、归档文件路径必须写进项目README,所有工程师统一版本,改完程序立即归档并附版本变更记录。
另外要区分项目归档和在线备份。项目归档包含全部组态和库文件,可以整体恢复;在线备份是从CPU上上传程序,但程序块如果设置了Know-how保护,上传回来只能看接口,看不了内部逻辑。源程序一定要在公司服务器上留好,别指望在线备份能救命。
5.4 扫描周期超限的定位与优化
大型程序逻辑多了之后,OB1扫描周期会变长。S7-1200的CPU属性里,扫描周期监控时间默认是150ms,一旦超过会触发OB80。
遇到循环时间超限,不要一上来就乱改代码。先在在线诊断里看循环时间的历史最大值和最小值,定位是哪个OB占用了大量时间。常见的大户是模拟量滤波、字符串处理、大数据量数组循环,这些可以移到OB30这类循环中断里执行,降低调用频率,而不是每周期都跑一遍。通信轮询和模拟量采集我通常放500ms的循环中断,OB1只做设备控制逻辑,这样扫描周期能控制得很稳。
程序优化到最后,稳定性和可维护性比代码技巧更重要。写大型项目不像做算法题,不是越精简越好。一个能让人三个月后还能轻松上手的程序,才是真正合格的大型项目程序。