跨小时统计这件事,看着简单,翻车率却一直不低。我接过好几次这种“产量对不上、能耗每小时曲线中间突然鼓包”的活儿,最后都指向同一类问题:程序里用的到底是“隔5秒轮询一遍”的 T#5S 方案,还是用 R_TRIG 上升沿去抓整点切换。很多人问过我“这两种到底哪种稳”,我一般都先反问一句:你的边界信号保得住吗?如果保不住,两个都不稳;如果保得住,R_TRIG 比 T#5S 稳一个数量级。这篇就把两个方案从原理到边界情况从头捋一遍,顺便给一段可以直接抄的写法,免得哪天半夜被叫到现场看统计曲线。
1. 跨小时统计的两种主流做法
跨小时统计这个需求,说穿了就一句话:每小时归档一次数据,把上一小时的累计值存下来,然后清零重新累计。工业现场里管它叫班产量统计、小时能耗统计、设备运行时长统计,业务逻辑都一样,但不同人的写法千差万别。我先还原两个最典型的写法,你看完就知道为什么这个问题值得掰扯。
1.1 T#5S方案是怎么写的
所谓“T#5S方案”,就是在博途这类环境里用 T#5S 作为 TON 定时器的定时时间,每隔大约5秒触发一次统计逻辑。常见代码长这样:
// 自复位定时器,T#5S就是5秒 TON_5S(IN := NOT TON_5S.Q, PT := T#5S); IF TON_5S.Q THEN // 每5秒轮询一次,比较小时是否发生了变化 IF #curHour <> #lastHour THEN #archivedQty := #hourQty; // 归档上一小时的累计值 #archiveTime := #clock; // 记录归档时的系统时间 #lastHour := #curHour; // 更新上一小时记录 #hourQty := 0; // 清零,开始新的一小时累计 END_IF; END_IF;从思路上看,它不赌“整点瞬间能不能抓到信号”,而是每隔几秒主动看一眼“小时变了没”。小时变了就做归档,没变就继续等。这种方案在老旧项目特别常见,因为没有额外的时间脉冲信号源,也不需要精心设计边沿检测,一个自复位定时器加一次比较就完事。
但问题也藏在“每隔几秒看一眼”这句话里。看一眼这个动作,天然引入了一个“看的时间窗”,而这个时间窗会让统计边界变得模糊。具体有多模糊,后面第二部分我会拿三个误差点详细说。
1.2 R_TRIG方案是怎么写的
R_TRIG 是上升沿检测指令,作用是捕捉一个 Bool 信号从 0 变 1 的那个瞬间。用它的思路是:不靠轮询,靠触发,只要整点边界到了,程序立刻执行归档。
最直觉的写法是把“小时值”作为触发源,但这会踩一个大坑,我先把错误示范写出来:
// 错误示范:直接把curHour当CLK // R_TRIG的CLK要求是BOOL,整型小时值不能直接接 // 所以要先转成“小时是否变化”的BOOL #hourChanged := (#curHour <> #lastHour); R_TRIG_Hour(CLK := #hourChanged, Q => #hourRise); IF #hourRise THEN #archivedQty := #hourQty; #archiveTime := #clock; #hourQty := 0; END_IF;这段代码乍一看没毛病:小时从 8 变 9 时,hourChanged 从 0 变 1,R_TRIG 抓到一个上升沿,归档一次。但你要是把这条程序跑过一整天,尤其跑过每晚 23 点到 0 点的切换,你就会发现它漏了——具体为什么漏,第三部分专门讲。
也有一种更讲究的 R_TRIG 写法,用整点脉冲信号做 CLK:
#isFullHour := (#curMinute = 0) AND (#curSecond = 0); R_TRIG_Pulse(CLK := #isFullHour, Q => #pulseRise); IF #pulseRise THEN // 归档、清零 END_IF;这种方案就比“小时值变化”稳得多,因为它不依赖小时值的上升还是下降,只看“分钟等于0且秒等于0”这个状态有没有从 0 变 1。这个信号每天都会准时来,不存在跨日失效的问题。
1.3 决定稳定性的到底是什么
看到这里你应该已经有点感觉了:T#5S 和 R_TRIG 的稳定性差异,表面是指令不一样,本质是“定时轮询”和“事件通知”两种获取边界的方式不一样。
T#5S 属于定时轮询:它每隔5秒检查一次当前状态,发现问题就处理,处理时机存在最多5秒的滞后。R_TRIG 属于事件通知:它时刻盯着输入信号,信号变化的那一个扫描周期内就做出反应,滞后通常只有几十毫秒。从“反应速度”和“边界准确度”看,事件通知天然比轮询高一个量级。
但 R_TRIG 有一个轮询没有的软肋:它必须依托一个“边界信号”存在。如果这个信号本身不可靠,比如取了小时值这种会在跨日时反向跳变的变量,那事件通知反而会漏,漏得比轮询还彻底。这个软肋,是很多工程师在代码里用 R_TRIG 却翻车的根源。
2. T#5S定时轮询:用得最多,却藏了三个误差
T#5S 方案看起来稳,因为它逻辑简单、不依赖额外信号,但它有三个误差源头是很多人没意识到的。我把这三个误差一个个拆开讲。
2.1 TON自复位的真实周期是5秒加一个扫描周期
先看 TON 定时器的工作原理。TON 的计时逻辑是:只要输入 IN 为 TRUE,内部时间就累加,直到达到 PT 设定值后把 Q 置 TRUE;IN 变为 FALSE 后,时间清零。用IN := NOT TON_5S.Q这种写法实现自复位,效果是 Q 每隔约 5 秒亮一个扫描周期。
问题在于,Q 亮起来之后,程序要等到下一个扫描周期才会看到 Q 为 TRUE,才把 IN 拉低。也就是说,定时器的完整周期是“PT + 一个扫描周期 + 程序处理时间”。如果扫描周期是 10ms,那实际周期就是 5.01 秒;如果项目里扫描周期被中断块、通信任务拖到 50ms,那就是 5.05 秒。
单次误差看起来小,可统计程序是长期运行的。一天有 86400 秒,按 5.01 秒的周期算,一天会少触发很多次轮询,跨小时归档的“名义时刻”也会跟着往后漂。虽然比较小时值不一定漏,但归档动作的发生时刻会逐小时后移,统计时间戳和真实整点之间的偏差会累积。
2.2 最迟5秒才能察觉跨小时,统计会整体平移
这是 T#5S 方案最大的实际误差:当整点到来时,比如 10:00:00,程序可能刚好错过那一刻,要到 10:00:05 之后才第一次发现小时值变了。
这5秒里生产线可没停。假设你的产线节拍是每秒产出两个产品,那 10:00:00 到 10:00:05 之间的 10 个产品,全都会被归档到 9 点那一小时里;下午 16 点整也一样,4点开头这5秒的产量会被记进3点。每个小时的统计边界都在“向后平移最多5秒”,而且每个小时都会重复出现,不是偶尔发生。
如果你是统计累计电表读数,问题会更明显。T#5S 在整点后5秒才采样归档,就会把整点后5秒的消耗算进上一小时,同时少掉整点前最后一瞬间的快照,形成“起止两端各差一点”的双重误差。对要求不高的产量看板,5秒误差可能可以接受;可一旦要拿小时数据做成本核算、做设备OEE,5秒的边界偏差会搞得你很被动。
2.3 扫描周期抖动会让轮询时刻漂移
前面两点讲的还是理想情况,实际现场更麻烦。CPU 扫描周期不是恒定的,它受程序复杂度、通信负载、中断块频率影响。比如你在程序里加了一个通信指令,每500ms扫一次,负载高的时候扫描周期从10ms涨到30ms,TON 累计时间的精度就会跟着变,轮询触发时刻的漂移也更明显。
如果你用的是 S7-1200/1500 这类带系统时钟的 CPU,有一个比 TON 稳得多的替代思路:不去定时脉冲,而是直接读系统秒值,用“秒数对5取模等于0”来判断是否到轮询点。这样轮询时刻锚定系统时钟,不会因为扫描周期变化而累积漂移。这种方法本质还是每5秒看一眼,但比 TON 控制出来的 5 秒准得多。
不过,无论用 TON 还是系统秒值,T#5S 方案的“最多5秒滞后”这个本质问题依然存在。只要采用“每隔几秒看一眼”的思路,边界就不可能像事件触发那样精准。所以我才说,T#5S 适合粗需求,不适合对整点边界敏感的场景。
3. R_TRIG上升沿触发:比想象中更容易踩坑
R_TRIG 听起来高级,很多人用起来却更慌,因为它的坑往往不是报错,而是“平时看不出问题,月底结账时对不上”。这一节把 R_TRIG 的底层逻辑和最典型的几个坑都摊开说。
3.1 R_TRIG内部到底在做什么
R_TRIG 的本质是一个存储型检测器:它内部保存上一次输入的“快照”,当前输入为 TRUE 且上一次输入为 FALSE 时,输出 Q 置 TRUE,而且只保持一个扫描周期。用公式表达就是:
上升沿 = 当前输入 AND NOT 上一次输入所以 R_TRIG 只能检测“从0到1”的变化,不能检测“从1到0”的变化。后者是 F_TRIG 的活。很多人只记住“用R_TRIG抓边沿”,却忘了它只抓上升边沿,这是所有 R_TRIG 应用翻车的总根源。
另外一个细节也要注意:R_TRIG 的 CLK 必须是 Bool 类型。你没法把一个整型小时值直接接到 CLK 上,必须先通过比较运算把它变成一个 Bool 信号,比如#curHour <> #lastHour。也就是说,用 R_TRIG 检测小时变化时,真正被检测的是“小时变化比较结果的上升沿”,而不是小时值本身。
3.2 最大的坑:直接拿小时值当CLK,跨日那天23点会翻车
前面那行“小时变化”的Bool信号看起来合理,因为一天之中绝大多数小时切换都是递增的:8点变9点、9点变10点,直到22点变23点,这些都是上升沿。R_TRIG 都能抓到。
但每天有一刻是特例:23点变0点。这是从23跳到0,是下降而非上升。你程序里那个“小时变化”的比较结果,虽然从“False”变“True”仍然是正向跳变,看起来 R_TRIG 应该能触发?问题恰恰出在这里。
我仔细解释一下这个“看起来能触发但实际会漏”的弯。假设#lastHour在上一轮已经被更新为 23,当系统时间跳到 0 点,#curHour变成 0,比较结果#curHour <> #lastHour从 False 变 True,确实是一个上升沿,R_TRIG 确实会触发。所以“跨日23点翻车”的说法是不是不严谨?并不是,真正的风险在另一个组合场景里。
真正的翻车场景是这样的:如果#lastHour的初始值是 0,或者程序在上电时忘记把#lastHour同步成当前小时,那么当系统时间处于非0小时时,第一次扫描比较结果就是 True,R_TRIG 会误触发一次归档;而当系统时间真的处于0点时,#lastHour恰好也是0,比较结果是 False,当小时从0变1时才正常触发。这些异常在日积月累中会让归档记录错位,最典型的就是跨日那个小时的数据被并到了下一天或者丢失。
更干净的说法是:如果直接用“小时值变化”的Bool信号给R_TRIG用,你必须额外处理跨日0点的初始化,否则逻辑会因为上电初始值和边界值重叠而不可靠。这也是很多人觉得R_TRIG“时灵时不灵”的常见原因。
3.3 不是不能用,关键是CLK信号设计
那 R_TRIG 到底能不能用在跨小时统计?能,但关键不在指令本身,而在 CLK 信号的设计。只要满足两个条件,R_TRIG 就是最稳的方案:
第一,CLK 信号每天必须且只能出现一次正向变化。第二,这个正向变化与真实整点之间的偏差必须可控。
满足这两个条件的典型信号就是“整点脉冲”:
// 分钟为0且秒为0时,Bool为TRUE // 这个状态每秒只出现一次(秒值为0的那一秒),每天出现一次 #isFullHour := (#curMinute = 0) AND (#curSecond = 0);这个信号不会因为跨日反向跳变而失效,因为它不依赖小时值的方向,只看秒和分钟的组合。每天23:59:59到00:00:00,分钟从59变0、秒从59变0,但“分=0且秒=0”这个状态在00:00:00之后照样出现,R_TRIG 能准时抓到。
3.4 断电重启后的首次扫描误触发
R_TRIG 还有一个现场特别容易踩的坑:断电重启后的第一次扫描,内部“上一次输入”快照是初始值 False。如果你此时 CLK 信号恰好为 True,比如设备在整点附近上电,R_TRIG 会立刻误触发一次归档,把上一小时的累计值重复归档一遍或者把刚上电的空值归档出去。
解决方法是上电初始化。在 PLC 的初始化组织块里,把用于比较的跟踪变量赋为当前值,比如:
// 初始化组织块(例如OB100)中执行 #lastHour := #curHour;这样上电后的第一次扫描不会误判为“小时刚变化”。如果你用整点脉冲方案,也要在初始化时把 R_TRIG 的状态复位,或者用一个“上电后延时使能”的中间变量,让统计逻辑在系统稳定后再参与归档。这一点在旧项目改造时特别容易被忽略——因为旧PLC程序里没有初始化块,新增的R_TRIG一上电就来一次假归档,数据直接错一小时。
4. 边界实测:最考验稳定性的三个时刻
说了这么多原理,不如放一组模拟对比结果。我做这类验证一般直接在仿真环境里把系统时间调快,或者用 PLC 的测试功能给几个关键时间点打标记。重点看三个边界时刻:正常整点、跨日零点、断电重启。
4.1 测试条件怎么设
测试对象是四套逻辑:T#5S 轮询、R_TRIG 检测“小时变化”、R_TRIG 检测“整点脉冲”、以及普通“小时不等检测”(不经过 R_TRIG,直接比较当前小时和上一小时)。每种逻辑都配一个计数器模拟产线累计值,每小时固定累计 1000 件,观察归档出来的数值差多少。
扫描周期故意设成 20ms,模拟中等负载;定时器用 TON 自复位方式;系统时间在测试前直接拨到 23:59:55,让它跑过跨日零点。断电重启测试则模拟在整点后 2 秒上电。
4.2 实测结果对比表
下面这张表是我按测试结果整理的,数值表示归档值与真实 1000 件的误差,或者边界处理是否出错:
| 测试场景 | T#5S轮询 | R_TRIG检测小时变化 | R_TRIG检测整点脉冲 | 小时不等检测 |
|---|---|---|---|---|
| 正常整点(如18点) | 误差≤最大5秒内产量 | 正常触发,误差一个扫描周期 | 正常触发,误差一个扫描周期 | 正常触发,误差一个扫描周期 |
| 跨日零点(23点→0点) | 能触发,但边界延迟最多5秒 | 初始化和快照处理不当就会漏/错 | 能触发,误差一个扫描周期 | 能触发,误差一个扫描周期 |
| 断电后重启(0点后2秒上电) | 上电后首次轮询比较,可能会补一次归档,也可能漏掉断电窗口 | 易误触发或漏触发,取决于初始化 | 会直接漏掉00:00脉冲,需配合掉电保持和开机补采 | 初始化做好的情况下,第一次比较能发现差异并补采 |
| 扫描周期拉大(20ms→50ms) | 定时周期漂移更明显,边界误差变大 | 影响不大,只延迟一个扫描周期 | 影响不大 | 影响不大 |
结论很清楚:R_TRIG 检测“小时变化”看着简洁,但在跨日、断电这类边界场景里不够稳;T#5S 轮询在全场景都能跑,但误差是“秒级”;整点脉冲 R_TRIG 和小时不等检测在边界精度上是最稳的两套,重活推荐从这两个里面选。
4.3 哪种稳:分场景给结论
现在可以直接回答标题里的问题了。
如果“稳”指的是“不管怎么跑都不会漏归档”,那 R_TRIG 不能随便用,必须配合干净的整点脉冲信号和初始化逻辑;如果“稳”指的是“逻辑简单、随便写也不出大错”,T#5S 更稳,因为哪怕边界判断晚几秒,程序也不会漏掉“小时发生变化”这个事实。
如果“稳”指的是“归档时刻要准、误差越小越好”,那答案很明显:T#5S 不如 R_TRIG,但这里的 R_TRIG 得是“整点脉冲”那一路,而不是“小时值变化”那一路。无论哪种“稳”,T#5S 都赢不了“事件触发”在延迟上的天然优势。
5. 我推荐的最稳写法(带代码)
这一节给出两套经过现场验证的写法,都是按“跨日安全 + 断电可恢复 + 误差小”的原则来设计的。你直接改成自己项目的变量名就能用。
5.1 方案A:整点脉冲+R_TRIG(精确到扫描周期)
这是我最推荐的方案,归档时刻的误差只有一个扫描周期,通常几十毫秒以内。核心是把“整点状态”作为触发源:
// 每扫描周期读取系统时间,这一步要放到统计逻辑之前执行 RD_LOC_T(RET_VAL := #retVal, OUT => #dtl); IF #retVal = 0 THEN #curHour := #dtl.HOUR; #curMinute := #dtl.MINUTE; #curSecond := #dtl.SECOND; END_IF; // 整点状态:分钟和秒都为0 #isFullHour := (#curMinute = 0) AND (#curSecond = 0); // 用R_TRIG捕捉整点信号的上升沿 R_TRIG_FullHour(CLK := #isFullHour, Q => #fullHourRise); IF #fullHourRise THEN // 归档上一小时,清零新小时 #archivedQty := #hourQty; #archiveTime := #dtl; #hourQty := 0; END_IF;需要说明的是,秒值为0这个状态会保持1秒。如果扫描周期是20ms,等于这个信号在扫描层面保持了50个周期,R_TRIG 正常只会在第一次变化时输出一次Q,不会重复触发。但如果你的扫描周期大于1秒,理论上可能漏掉这个脉冲,这种情况在常规PLC里几乎不会发生,真遇到了只能说明扫描周期本身已经异常,首先要解决的是负载问题,不是统计逻辑。
5.2 方案B:小时不等检测(最简单,跨日安全)
如果你不想引入 R_TRIG,也不想依赖秒级脉冲,最稳妥的替代方案就是“小时不等检测”。代码反而更短:
// 每次扫描都执行一次比较 IF #curHour <> #lastHour THEN #archivedQty := #hourQty; #archiveTime := #dtl; #lastHour := #curHour; #hourQty := 0; END_IF;这个方案不区分上升沿和下降沿,只要小时值发生变化就处理。23点到0点的变化照样能触发,因为比较的是“不等于”,不是“大于”。它的缺点是“什么时候发现变化”取决于扫描周期,不会像整点脉冲那样在整点后第一次扫描就精准触发,但误差仍然只有扫描周期量级,比 T#5S 的5秒误差小得多。
每次扫描都做一次比较,会多占一点CPU时间。但比较两个整型值对现代PLC来说几乎可以忽略,不用为了省这点资源去换复杂信号。项目里追求“可读性”和“可维护性”,我反而建议用这个简单的方案。
5.3 别忽略的配套细节
最后提三个配套细节,都是现场踩过坑才总结出来的。
第一,统计值要放掉电保持区。如果设备在23:59断电,你 hourQty 里存的是最后几分钟的累计值,上电后要是清零了,那一小时的数据就少了。把 hourQty、lastHour、archiveTime 都放到掉电保持区,能让系统在断电重启后接着累计。只是要注意,掉电期间产线实际可能还在跑(比如由另一套系统控制),PLC无法感知这期间的产量,所以做精确统计时最好配合现场仪表的累计值做二次校验。
第二,归档时间戳千万不要用“检测到跨小时的那一瞬间”,而是要根据业务需求决定。整点脉冲方案里,检测瞬间就是整点后一个扫描周期,误差可忽略,直接打系统时间没问题。但 T#5S 方案如果用检测时刻打时间戳,打出来的时间会晚最多5秒,日积月累会让数据分析和MES侧产生误解。这种情况宁可自己算:检测到跨小时后,用当前小时减1秒或者减5秒作为归档时间,把边界拉回整点。
第三,读取系统时间的指令有同步时钟和本地时钟之分,做跨小时统计用本地时钟更直观,但前提是本地时钟本身要校时。如果PLC挂在上位机校时系统下,要确认校时策略不会在运行中途往回跳。时钟往回跳会导致 curHour 出现非预期的变化,小时不等检测可能会误触发——这种情况虽然少见,但我在现场遇到过,排查时一度以为是程序逻辑错了。
做了这么多年自动化和数据统计,我个人最大的体会是:跨小时统计的稳定性,七分在边界信号设计,三分在断电恢复。T#5S 和 R_TRIG 之争只是表象,真正决定结果的是你有没有想清楚“小时边界到底靠什么通知程序”。T#5S 适合粗需求、快速交付;整点脉冲 R_TRIG 和小时不等检测适合真正要拿数据做分析的项目。建议你新项目直接上方案A或方案B,别在轮询的边缘误差上反复纠结。