做自动化项目的兄弟应该都有印象,“跨小时统计”这四个字看着不起眼,做起来却最容易在验收时翻车。产量、能耗、流量累积,客户都是按小时拉着Excel对账的,一旦0点前后的数据对不齐,现场工程师的耳朵就要遭罪。最近技术交流群里又有人问:跨小时统计用T#5S稳,还是用R_TRIG稳?问的人显然是被边界数据折磨过。
先把结论放在前头:如果你的目标是“准确裁定哪个事件属于哪个小时”,那别犹豫,用R_TRIG配合系统时间比较来检测小时边界,比T#5S定时扫描稳得多。T#5S不是不能用,但它天生带着一个最长5秒的归属模糊窗口,用在边界判定上就是隐患。下面我会把误差来源、实现方式、踩坑经验全部拆开讲,不管你是梯形图老法师还是ST爱好者,应该都能抄点作业。
1. 先把场景讲清楚:跨小时统计到底难在哪
1.1 统计对象决定方案
跨小时统计最常见的对象有三类:产线上的产量计数(光电传感器、接近开关)、公用工程的能耗统计(电表脉冲、瞬时功率),以及水处理和热网的流量累积。它们表面上看都是“每小时出一个数”,但对信号的处理方式截然不同。
产量和电表脉冲属于离散事件,一个一个的脉冲,每个脉冲代表一个产品或者一个固定能量单位。这类信号必须逐事件捕捉,漏一个就少一个,没有任何“插值”的说法。瞬时流量和瞬时功率是连续量,随时间变化,需要周期采样再积分,才能得到累积值。很多工程师把这两类搞混,用5秒定时去“数脉冲”,或者试图用边沿触发去“积分”,方向一反,稳定性自然没保证。
所以你先要问自己一个问题:我要统计的是离散事件,还是连续量?这个问题想清楚,方案就定了大半。想不清楚,后面的选型都是扯淡。另外还要考虑MES报表通常需要给每个小时记录打上明确的时间标签,这个标签是按起始时间命名还是按结束时间命名,直接影响到跨小时时数据到底塞进哪个小时段,这一步在程序里就得定死,别等报表端再来猜。
1.2 跨小时切换的三个标准动作
不管统计对象是什么,跨小时统计在程序里都逃不过三个动作:锁存、清零、启动。锁存,是把上一小时累计值从工作寄存器存到历史记录区;清零,是把当前小时累计计数器归零;启动,是刷新小时起始时间戳,让新一小时的累计从零开始。
这三个动作必须在同一个触发信号下连续完成,而且不能被打断。比如先清零再锁存,锁存读到的就是零,上一小时的数据直接丢了;先锁存再清零,但忘了刷新时间戳,下一轮边界判断的基准还是旧小时,整个程序就会循环错位。这不仅是逻辑顺序问题,更是你有没有一个“可靠的、只发生一次的边界信号”的问题。很多现场报错的数据问题,最后查下来根本不是累加逻辑错了,而是触发切换的那个信号本身不可靠。
1.3 边界裁定是灵魂,不能靠运气
跨小时统计的核心不是累计逻辑本身,而是“什么时候执行锁存、清零、启动这三个动作”。一秒不差、不多不少,恰好在这个扫描周期里发现了整点,然后执行切换。这句话听起来简单,做起来最难。
有些程序写得很随意:PLC上电后启动一个5秒定时器,定时到就执行统计逻辑,逻辑里比较当前小时和上一小时,变了就切换。表面上看,小时一变,最多5秒内就能发现,好像没什么问题。但问题恰恰就出在这个“最多5秒”上。你愿意接受你的产品归属存在一个最长5秒的不确定窗口吗?如果甲方愿意接受,我也不拦着。在实际产线上,5秒可能意味着几十个产品归属不清,报表上一多一少,特别明显。
2. T#5S方案:误差模型和局限性
2.1 常见写法
用T#5S做跨小时统计,梯形图里大概是这么个意思:TON定时器定为5秒,定时到Q输出一个脉冲,触发统计逻辑;统计逻辑里读当前时间的小时字段,和上次记录的小时值比较,不等就执行切换;执行完重置定时器。ST里写出来大概是这样:
TON_inst(IN := NOT TON_inst.Q, PT := T#5S); IF TON_inst.Q THEN current_hour := READ_HOUR_REALTIME(); IF current_hour <> last_hour THEN history[last_hour] := product_count; product_count := 0; last_hour := current_hour; END_IF; TON_inst(IN := FALSE); END_IF;这种写法看起来很合理:5秒检查一次,一小时720次,误差最多5秒,听起来可以接受。这也是很多项目敢用它做边界裁定的原因。但把误差模型拆开看,问题比想象的多。接下来我逐个分析。
2.2 相位误差:定时器不是从整点开始的
T#5S定时器和小时整点之间没有任何相位同步关系。PLC上电后,定时器从某个时刻开始计时,它下一次触发时刻完全由启动时刻决定。举个例子:假设这个定时器上次触发是23:59:58,那下一次触发就是00:00:03。执行跨小时判断时,实际时间已经是00:00:03。
这5秒间隔里发生了什么?23:59:58到00:00:00这2秒内通过的产品,物理上属于上一小时,但计数器还没来得及锁存;00:00:00到00:00:03这3秒内通过的产品,物理上属于下一小时,却已经被算进了当前累计值。如果你采用“先锁存再清零”,锁存时刻是00:00:03,会把0点后3秒的数据一起记进上一小时;如果用“先清零再锁存”,0点后3秒的数据就被直接清掉了。无论哪种写法,都存在一个上一小时和下一小时之间归属模糊的窗口,这个窗口最长就是5秒。
2.3 定量算一笔账
假设产线每秒通过1个产品,最坏情况下归属误差是5秒乘以每秒1个,等于5个产品。如果产线每秒通过10个,那就是50个。5000瓶/小时的包装线,0点前后的统计误差可以达到正负50瓶,报表上直接体现为一小时多几十瓶、另一小时少几十瓶。现场对账的时候,这几十瓶的差异很难解释,因为你觉得程序逻辑没问题,但数据就是不对。
流量场景更直观。假设瞬时蒸汽流量是10吨/小时,5秒窗口对应的归属误差就是10吨除以3600秒再乘以5秒,大约14公斤。如果连续统计24小时,由于定时器相位是固定的,某些小时会持续偏多、某些持续偏少,MES报表上的小时流量曲线会出现规律性锯齿。这种锯齿一旦被客户看到,第一反应就是你采集程序有问题,解释成本极高。
2.4 扫描周期叠加的触发延迟
T#5S的定时精度本身还行,不少PLC的定时器使用系统时基,5秒不会差太多。但定时时间到之后,Q信号要到下一次程序扫描时才会被逻辑读取并执行切换。这个“下一次扫描”就是额外延迟。
如果扫描周期稳定在10ms,影响不大。但现场程序里往往挂着PID、通信、配方管理,扫描周期可能波动到几十毫秒。一旦通信卡顿,扫描周期可能到100ms以上,边界切换时刻会比真实整点晚100ms以上。这个误差虽然比5秒小,但它是随机的、不确定的,排查起来比固定相位误差更恶心。因为固定误差还能通过逻辑修正,随机延迟你根本没法补偿。
2.5 定时采样的积分截断误差
如果统计对象是连续量,用T#5S做采样积分是另一个常见做法:每5秒读一次瞬时流量,乘以5秒,然后累加。问题在于,T#5S的采样时刻不会恰好落在整点上。
上一小时最后一次采样可能是23:59:57,按采样时刻归属,23:59:57采到的瞬时值乘以5秒,这5秒区间会覆盖到00:00:02,其中0点后2秒的流量被算进了上一小时;下一小时的积分起点从00:00:02开始,相当于少了2秒。积分边界被截断,而且截断量随定时器相位不同而不同。这种截断误差在每个小时交界处都会出现,短时间看影响不大,累积起来会让人觉得数据总有那么一点说不清的偏差。
3. R_TRIG方案:为什么更稳
3.1 R_TRIG的本质和两种用法
R_TRIG是上升沿检测功能块,输入信号clk从FALSE变TRUE时,输出Q会输出一个扫描周期的TRUE脉冲。它的核心价值是捕捉“状态从0到1的跳变瞬间”,而不是周期性的时间到达。
在跨小时统计里,R_TRIG有两种典型用法,正好对应前面说的两类统计对象。第一种是检测系统时间的小时值变化,裁定自然小时边界;第二种是检测传感器脉冲信号上升沿,逐事件计数。下面分开讲清楚。
3.2 用法一:检测小时边界
通过系统时间函数读取当前小时字段,每个扫描周期和上一次的值比较。不一样了,说明系统时间跨过了整点。这里有个很容易犯的错:直接用比较结果这个电平信号去触发锁存和清零。问题在于,比较结果会持续TRUE一个甚至多个扫描周期,如果直接拿来当触发器,程序每个扫描周期都会执行一遍锁存清零,累计值永远攒不起来。
正确做法是用R_TRIG把这个“小时变化”电平信号转换成单周期脉冲:
current_hour := READ_HOUR_REALTIME(); hour_changed := current_hour <> last_hour; R_TRIG_hour(clk := hour_changed); IF R_TRIG_hour.Q THEN history[last_hour] := product_count; product_count := 0; last_hour := current_hour; END_IF;这里的hour_changed就是current_hour不等于last_hour的比较结果。R_TRIG保证切换动作只执行一次,而且执行时机和真实整点只差一个扫描周期。扫描周期10ms就是10ms,扫描周期1ms就是1ms,T#5S的5秒模糊窗口在这里被压缩了500倍以上。这个精度在现场是完全够用的。
3.3 用法二:逐脉冲计数
如果统计对象是离散事件,比如产品通过光电传感器的脉冲,每个脉冲都要加1。程序写法:
R_TRIG_sensor(clk := sensor_signal); IF R_TRIG_sensor.Q THEN product_count := product_count + 1; END_IF;只要传感器脉冲宽度大于扫描周期,每个脉冲都会被捕捉。扫描周期10ms,脉冲宽度20ms以上,就不会漏。如果脉冲太快,就要上硬件中断或者高速计数通道,那属于另一个话题。跨小时时,脉冲计数和边界锁存两个功能要配合:平时R_TRIG_sensor负责对每个脉冲加1;当R_TRIG_hour的Q为TRUE时,先把product_count锁存到历史记录,再清零,然后开始新小时累计。脉冲计数本身不会被T#5S的采样间隔影响,因为它是逐事件处理的,天然就没有“采样点错过整点”这个问题。
3.4 从误差模型看为什么稳
整理一下R_TRIG方案的误差来源。边界检测误差是一个扫描周期,毫秒级;脉冲捕捉误差,只要脉宽合规,零漏计数;连续量积分,用循环中断或系统累积功能,不依赖普通定时器;时间基准是系统实时时钟,与程序执行节奏无关。
这套方案的本质是事件驱动。程序只对“边界事件”和“脉冲事件”反应,扫描周期波动只会延迟检测,不会产生归属错误。就算某个扫描周期发生了100ms延迟,跨小时的判断也只是晚了一个周期,该属于上一小时的数据始终在计数器里,不会因为采样相位而错分。这一点是T#5S方案做不到的。T#5S是时间驱动,它只知道自己“该干活了”,至于现在离真实整点差多少,一无所知。
3.5 别忽视系统时间的前提
R_TRIG方案稳,有一个硬前提:系统时间必须准。实时时钟电池、上电自动同步、外部时钟源,这些都要到位。如果PLC时间在运行中跳变,不管是手动校时还是时间同步造成的跳变,小时字段的反复变化都会触发假边界。
建议在程序里做时间有效性判断:读取小时字段前,先检查系统时间是否有效、年份是否合理。时间无效时宁可保持原状,也不要执行清零。有一次现场校时,时间从23:59:58直接跳到00:00:00,如果边界逻辑没做防抖,统计会被连续触发两次清零。这个细节听起来小,真踩上的时候特别冤。
4. 选型建议和实操要点
4.1 对比表
| 对比维度 | T#5S定时扫描 | R_TRIG边界检测 |
|---|---|---|
| 触发本质 | 时间周期触发 | 状态跳变触发 |
| 边界精度 | 相位相关,最差5秒 | 扫描周期级,毫秒 |
| 离散事件计数 | 不适合,会漏脉冲 | 逐脉冲捕捉 |
| 连续量积分 | 可用但有截断误差 | 不适合直接积分 |
| 受扫描周期波动影响 | 大,触发延迟漂移 | 小,仅延迟检测 |
| 实现复杂度 | 低 | 中 |
| 建议角色 | 周期快照、趋势记录 | 边界锁存、事件计数 |
4.2 我验证过的组合方案
我个人推荐跨小时统计按下面这套组合来做,几个项目用下来,没有因为边界问题被甲方找过:
- 边界裁定:系统时间小时字段比较+R_TRIG,负责锁存、清零、启动。
- 离散事件统计:R_TRIG逐脉冲累加,计数器用保持型变量。
- 连续量统计:用循环中断做等周期积分,周期根据工艺定,比如100ms或1s,不建议用普通TON定时器做积分。
- 历史快照:MES如果还需要过程趋势,可以另用T#5S做5秒快照,但快照数据只用于趋势展示,不参与小时归属判定。
这套方案里,T#5S退回到它擅长的“周期采样”角色,边界这种需要精确裁定的活全部交给R_TRIG和系统时间。连续量统计不用TON定时器,是因为TON定时器的触发时刻依赖扫描周期,而循环中断有自己的硬件时基,积分更均匀,跨小时截断误差也更小。
4.3 一个可抄的ST示例
完整的一个小时切换逻辑,ST风格写出来是这样:
// 周期块中每个扫描周期执行 current_hour := READ_HOUR_REALTIME(); hour_changed := current_hour <> last_hour; R_TRIG_hour(clk := hour_changed); IF R_TRIG_hour.Q THEN // 1 锁存上一小时累计值 history[last_hour] := product_count; // 2 给上一小时记录打上时间段标签 history_hour[last_hour] := CONCAT(INT_TO_STRING(last_hour), ':00-', INT_TO_STRING(current_hour), ':00'); // 3 清零当前累计 product_count := 0; // 4 刷新小时基准 last_hour := current_hour; END_IF; // 脉冲计数,每个扫描周期执行 R_TRIG_sensor(clk := sensor_signal); IF R_TRIG_sensor.Q THEN product_count := product_count + 1; END_IF;注意第2步的小时标签用起始时间命名,比如“17:00-18:00”,这样报表端能清楚区分每个小时区间,避免“23点那段到底是23点还是0点”的歧义。如果不打标签,隔几天再回看历史数据,很容易把上一小时和下一小时搞混。
4.4 写代码的常见注意点
锁存、清零、启动的顺序一定要写在注释里,未来维护的人会感谢你。当前小时累计值用掉电保持变量,防止断电丢累计;锁存后的历史记录区也建议用保持区,或者第一时间上传MES,别让数据在PLC里裸奔。
R_TRIG的Q只保持一个扫描周期,如果同一扫描周期里有多个逻辑都要用这个边界信号,最好在Q为TRUE时置位一个中间变量,其他逻辑读取这个中间变量,而不是重复调用R_TRIG输出。调试的时候,把边界信号拉出来接到一个HMI指示灯或者示波器通道,验收时心里有底,数据对不上时也能快速判断边界触发是否正常。
5. 踩过的坑和排查记录
5.1 坑一:锁存和清零的顺序写反
某个项目的产量统计,0点后报表显示上一小时产量为0。查代码发现工程师先写了product_count清零,再写的锁存history赋值。清零在前,锁存自然锁了个0。这类问题不看代码很难发现,因为程序逻辑本身没有报错,数据看起来也有规律,就是每个整点后上一小时产量都是0。排查时第一件事就是检查锁存和清零的相对顺序,别先怀疑传感器和计数器。
5.2 坑二:用字符串比较时间导致偶发失灵
有工程师用字符串方式读取系统时间做小时比较,遇到格式对齐问题:“0:00:00”和“00:00:00”看起来一样,比较结果却可能不同,边界信号就会漏发。漏一次,那个小时的统计就整体偏移了。后来改成读取整数小时字段做比较,问题消失。这也是我坚持“比较整数,不比较字符串”的原因。字符串比较在不同平台上的实现细节差异很大,正经做统计逻辑,远离它。
5.3 坑三:脉冲宽度小于扫描周期漏计数
传感器脉冲持续时间只有1ms,扫描周期10ms,R_TRIG一次都捕捉不到。排查时先确认脉冲宽度和扫描周期这两个数,再谈方案。如果脉冲太窄,把信号接到带硬件锁存或中断功能的输入通道。现场没有条件改硬件时,也可以用影子位方案:输入为TRUE就置位一个影子位,程序周期性检测到影子位后计数加1再复位,但前提是脉冲宽度大于一个扫描周期,否则连影子位都采不到。如果产线速度极高,脉冲频率每秒几千个,R_TRIG这条路就不该走了,直接上高速计数器。
5.4 坑四:断电重启导致假边界
实时时钟电池失效,或者PLC没有时间同步源,重新上电后系统时间回到初始值,小时字段从某个值跳到初始值,比较结果变化,触发一次假跨小时,把统计清零。恢复逻辑建议加上时间有效性判断:读到的年份小于某个基准年时,不执行清零,只等待时间同步完成。统计值本身也要用保持寄存器,断电重启后继续原来的累计。
5.5 坑五:时区切换日的23小时和25小时
有些PLC系统配置了时区自动切换,在切换日会出现23个小时或者25个小时的情况。小时数值不会简单地从23跳到0,可能跳到别的值,也可能出现相同小时值连续出现两次。跨小时统计逻辑如果只按“小时值变化”判断,切换日的数据会出现缺失或重复。
这种场景虽然属于少数,但一旦碰上就很头疼。建议使用当日分钟数或者秒数做连续性判断,而不是用小时字段的非连续跳变来判断。比如计算当前时间换算成“当日第几分钟”,和上一周期的分钟数比较,差值大于1就说明出现了时间跳变,再结合跳变方向决定是否切换统计段。
5.6 坑六:T#5S方案的特征现象
如果现场已经用T#5S方案部署,想确认是否踩了相位坑,可以做一个小实验:连续记录一周每个小时头尾5秒的产量增量,然后把相邻两小时的数据合并起来看。你会发现单小时数据高低起伏,但两小时合并总量基本守恒。这是典型的边界窗口归属模糊特征。
解决办法就是把边界裁定从T#5S手里拿出来,换成系统时间加R_TRIG。改完之后再记录一周,单小时数据的起伏会明显减小,相邻小时也没有那种“你一多我就一少”的关联性了。这个实验既简单又直观,拿来判断问题根源很好用。
5.7 排查速查表
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 相邻小时数据一多一少,合并守恒 | T#5S边界窗口归属模糊 | 统计头尾5秒增量 |
| 0点数据整体归零 | 清零先于锁存 | 检查锁存清零顺序 |
| 偶发漏计数 | 脉冲过窄或扫描周期过大 | 测脉宽,查扫描周期 |
| 断电后统计错乱 | 时钟失效或变量非保持 | 查时钟电池、保持属性 |
| 切换日小时数据重复或缺失 | 时区切换导致小时非连续 | 改用分钟或秒级连续性判断 |
| 校时后统计被清零 | 时间跳变触发假边界 | 加时间有效性判断 |
我在实际项目里的习惯很简单:把“边界裁定”和“数据采集”彻底分开。数据采集该用定时器就用定时器,该用中断就用中断;但所有涉及“哪个小时的数据”的判断,一律交给系统时间加R_TRIG,不让普通定时器参与边界裁定。这套逻辑从调试到运维,基本没有因为跨小时统计被甲方找过麻烦。
最后再分享一个小技巧:切换动作最好用边沿触发后的单周期脉冲来做,不要在同一个扫描周期里既判断又操作还到处读写,扫描周期一旦波动,数据错位就会从这种拥挤的操作里冒出来。如果你现在正在为“T#5S还是R_TRIG”纠结,我的建议很简单——边界归R_TRIG,采样随便选,别让5秒定时器做它不擅长的裁判。