跨小时统计在PLC项目里是个绕不开的硬需求。产线每小时产量、设备每小时能耗、空压机的运行时长这些数据背后都有一个共同的痛点——整点那一下的交接上一小时的数据怎么结存新一小时的数据从哪一秒开始算有没有可能丢料或者串料。最近在技术社区里又看到有人在讨论T#5S周期采样的写法跟R_TRIG沿触发的写法到底哪种稳。这个问题我前后也改过好几版踩过不少坑今天把两套方案的原理、误差边界和现场表现仔细捋一遍应该能把这事说透也让正在写统计程序的同行少走弯路。1. 先聊清楚跨小时统计到底难在哪1.1 整点交接的典型场景统计类程序在自动化项目里太常见了。我做过的一个包装线项目每条产线需要按小时统计产量报表给车间主任看用来核对班次效率和设备利用率。另一个项目是空压站需要统计每台空压机的每小时运行时长和耗电量数据直接进能源管理系统。这些场景的程序结构其实都差不多一个累加器每个扫描周期把新的瞬时值累加进去到了整点就把累加值存到对应小时的位置然后清零重来。关键是这个到了整点怎么判断。写快了容易出幺蛾子写慢了又怕把交接时刻错过。1.2 容易出事的三个细节第一个细节是归属时刻。整点那一秒新来的数据到底算上一小时还是下一小时如果算错了每个小时报表都会出现固定偏差看着不大但月度盘点对不上账的时候就很头疼。第二个细节是重复或者遗漏。交接逻辑如果被触发两次上一小时的数据会被覆盖如果一次都没触发那上一小时的数据就永远留在累加器里后面全部串位。第三个细节是异常场景。PLC停机、程序下载、系统校时、扫描周期抖动这些情况都会让时间判断出问题。真正稳的方案不只是正常工况下不出错还得在这些边界条件下也能合理处理。这三点就是判断哪种稳的试金石。下面我把两种方案分别拆开看。2. 两条路线R_TRIG沿触发与T#5S周期采样2.1 R_TRIG方案只认一次变化R_TRIG是标准功能块检测CLK输入端的上升沿。用在跨小时统计里的思路非常直接把当前小时不等于上次记录的小时这个条件当作CLK输入小时从9变成10的瞬间条件从FALSE跳成TRUER_TRIG输出一个扫描周期的TRUE脉冲程序在这个脉冲里完成归档和清零。R_TRIG最重要的特性是只响应一次。就算条件满足持续了好几个扫描周期R_TRIG也只在第一个周期输出Q为TRUE后面即使CLK还是TRUEQ也保持FALSE。只有等CLK先变回FALSE再变TRUE才会再触发一次。放在整点切换这个场景里这个特性简直是为我们量身定做的。因为小时值在一个小时内是稳定的从9变到10只在整点发生一次所以R_TRIG必然只触发一次数据交接不多不少。另外程序在检测到变化的那一拍就把上次小时值更新掉了下一扫描周期条件自动变回FALSE不存在自己反复触发自己的问题。2.2 T#5S方案定时看一眼T#5S方案用的是定时器思路常见写法是TON功能块预设时间5秒每5秒产生一个脉冲在脉冲里读取当前小时跟上次检查的小时做比较不一致才做交接。这套方案的哲学是轮询。程序不关心整点那一瞬间具体发生了什么而是每隔5秒去看一眼时间变了没有。好处是对信号稳定性要求低就算某一次读取结果不正常5秒后再看一眼也能恢复。坏处也明显——反应是滞后的。整点发生在10:00:00下一次采样可能在10:00:03那么从10:00:00到10:00:03这3秒累加的数据就会被归到上一小时。如果采样点在10:00:02误差就是2秒最坏的情况下误差可以达到整整5秒。2.3 两套代码一对比就清楚为了直观我把两套核心逻辑写出来对比。先看R_TRIG的思路// 每次扫描执行 iCurHour : CALC_CURRENT_HOUR(); // 获取当前小时 0~23 rTrigHour(CLK : (iCurHour iLastHour)); // 小时变化沿检测 IF rTrigHour.Q THEN // 整点交接 IF iLastHour 0 THEN rHourlyData[iLastHour] : rAccumValue; // 归档上一小时 END_IF; rAccumValue : 0.0; // 清零 iLastHour : iCurHour; // 更新基准小时 END_IF; rAccumValue : rAccumValue rNewValue; // 累加当前值再看T#5S的思路// 5秒定时采样 ton5s(IN : NOT ton5s.Q, PT : T#5S); // 自复位定时器 IF ton5s.Q THEN iCurHour : CALC_CURRENT_HOUR(); IF iCurHour iLastHour THEN // 发现小时变化 IF iLastHour 0 THEN rHourlyData[iLastHour] : rAccumValue; END_IF; rAccumValue : 0.0; iLastHour : iCurHour; END_IF; END_IF; rAccumValue : rAccumValue rNewValue; // 每周期累加注意两套代码里累加语句都放在交接判断后面这个顺序是有讲究的。如果反过来写整点那一扫描周期的新数据会被提前累加进去然后才触发交接归档新小时的第一笔数据就串到上一小时了。这个问题后面还会重点说。3. 稳定性对比精度、漏检、异常场景3.1 边界精度一个扫描周期 vs 最多5秒先算精度账。R_TRIG方案的误差上界是一个扫描周期。假设扫描周期20ms整点一到程序最迟20ms内就会检测到小时变化累加值里混入的跨小时数据最多就是这20ms的量。对于1秒生产1个产品的产线这20ms可能连一个产品都覆盖不到误差在实际工程里基本可以忽略。T#5S就完全不同了。每5秒采样一次整点落在两次采样中间时检测延迟0到5秒不等。如果上一小时的累加值在整点后第4秒才被归档那这4秒里的所有数据全部算进上一小时。举个数设备负载100kW电费1元/度。T#5S方案每小时的边界误差上限是5秒换算成电量是100 × 5 ÷ 3600 ≈ 0.14度一天24小时累计误差约3.3度。单看这个数值不大但能源管理系统的报表跟电表总表对账时这些误差会叠加到总差异里到时候说不清楚是表的问题还是程序的问题。所以从边界精度这个维度看R_TRIG是压倒性优势。它的误差跟扫描周期绑定而扫描周期在绝大多数PLC项目里都是毫秒级的。3.2 漏触发风险经典担心在这里不成立支持T#5S的同行经常提一个理由R_TRIG会漏沿脉冲宽度小于扫描周期就检测不到所以不如周期采样稳。这个担心本身没错但要看CLK信号从哪来。如果CLK是外部干接点硬件脉冲只有1ms扫描周期100ms那R_TRIG确实可能漏。但在跨小时统计这个场景里CLK不是外部脉冲而是PLC自己读系统时钟算出来的小时值。小时值从9变成10之后它会一直保持10不会自己弹回9也不会闪烁。程序只要在整点之后的任意一个扫描周期读到这个变化条件就是TRUER_TRIG就会触发。不存在脉冲已经过去了没赶上的情况因为这不是脉冲是个稳定电平。反过来T#5S方案反而有一个容易被忽视的隐患定时器复位逻辑如果写得不严谨采样周期会被意外拉长。我见过有人用TON的Q直接做采样标志然后又用了另一段逻辑把定时器复位掉结果Q的TRUE只持续了一个扫描周期程序还没来得及做小时比较周期就变成长短不一了。尤其是一个程序段里把TON复位另一个程序段里又来调TON这种写法很容易踩这个坑。3.3 异常场景重启、校时、周期抖动PLC刚下载完程序或者从STOP转RUN这是统计程序的第一次扫描。R_TRIG方案里只要把iLastHour初始化成-1第一拍iCurHour肯定不等于-1条件成立R_TRIG触发一次。因为iLastHour是-1归档语句会被跳过累加器清零然后程序正常开始新小时的累加。停机期间可能残留的脏数据被一次性清掉了非常干净利落。T#5S方案在第一拍不会触发采样它要先等第一个5秒周期结束。意味着开机后前5秒的数据虽然进了累加器但小时基准在5秒后才建立。如果开机时刻正好跨整点那归档归属会有点别扭。系统校时或者手动改时间导致小时跳变两套方案都会触发一次交接中间跳过的那些小时自然不会有数据这是正常的。倒是时间往回拨的情况值得注意如果从10点拨回9点两套方案用不等于比较都会检测到变化会把当前累加值归档到10点那一格然后重新开始。这个行为可以接受但如果你用了大于而不是不等于做判断时间回拨就会漏检这点在写代码时要留意。扫描周期抖动对R_TRIG几乎没影响因为它本来就是每个周期都比一次。T#5S就要看具体实现很多平台的TON是根据任务周期累加的扫描周期波动会让5秒间隔漂移成5.1秒甚至更夸张采样间隔不规则边界误差就变得不可控。有些平台提供系统周期时钟位那种一般比较稳定但可读性没有自己写的TON直观。4. 实操代码与移植要点4.1 R_TRIG标准实现首扫描保护和索引防越界完整的R_TRIG统计块建议这样写各部分作用我都标出来PROGRAM HourlyStats VAR rTrigHour : R_TRIG; // 上升沿检测实例 iLastHour : INT : -1; // 上次归档的小时-1代表首次运行 iCurHour : INT; // 当前小时 rAccumValue : REAL : 0.0; // 当前累加值 rHourlyData : ARRAY[0..23] OF REAL; // 24小时分布数据 xHourChanged : BOOL; END_VAR // 第一步换算当前小时。先获取系统时间距午夜的秒数再整除3600 iCurHour : (TIME_TO_INT(GET_TIME_OF_DAY()) / 3600) MOD 24; // 第二步沿检测 rTrigHour(CLK : (iCurHour iLastHour)); xHourChanged : rTrigHour.Q; // 第三步整点交接按 归档-清零-更新基准 的顺序执行 IF xHourChanged THEN IF (iLastHour 0) AND (iLastHour 23) THEN rHourlyData[iLastHour] : rAccumValue; END_IF; rAccumValue : 0.0; iLastHour : iCurHour; END_IF; // 第四步累加瞬时值必须在交接之后执行 rAccumValue : rAccumValue rNewValue;有几个细节值得强调。iLastHour初始值必须是-1而不是0否则首次扫描时0点的数据会被程序当作有效归档索引直接把还没建立的历史0点数据覆盖掉。归档前做一次索引范围判断是习惯问题虽然正常情况下iLastHour一定在0到23之间但多一层保护对现场调试有好处。GET_TIME_OF_DAY这个函数在不同平台名称不一样有些平台叫TimeOfDay()有些平台要通过库函数获取。它的核心作用是取系统时钟换算成从0点开始的累计秒数。没有现成函数的环境也可以用读取系统时间然后自己拆出小时的方式替代。4.2 T#5S实现定时器复位的两种写法T#5S采样方案也有两种常见写法区别在于定时器怎么复位。第一种是自复位方式前面代码里用的就是ton5s(IN : NOT ton5s.Q, PT : T#5S);Q输出TRUE时下一拍IN变成FALSE定时器复位然后Q变回FALSEIN恢复TRUE重新计时。这种写法简洁但有一个坑Q的TRUE只持续一个扫描周期如果你在程序其他地方也引用ton5s.Q可能会漏掉这个一拍脉冲。更稳妥的写法是用R_TRIG再抓一次Q的沿ton5s(IN : TRUE, PT : T#5S); rTrig5s(CLK : ton5s.Q); IF rTrig5s.Q THEN ton5s(IN : FALSE); // 手动复位准备下一轮 // 在这里做小时比较和交接 END_IF;注意这个写法里IF块内把IN置成FALSE复位定时器下一拍程序开头又置成TRUE重新计时。中间间隔一个扫描周期实际采样周期变成了5秒加一个扫描周期但这个误差可忽略。关键是rTrig5s.Q保证了采样动作只做一次程序逻辑清晰不会误触发。顺带说一句用R_TRIG去抓TON的Q本质上是说T#5S方案内部其实也离不开沿检测。所以严格意义上这两种方案不是完全对立的关系。周期采样负责定时看一眼沿检测负责看一眼只执行一次二者经常搭配使用。4.3 换个平台怎么改不同品牌PLC在标准IEC 61131-3的基础上有各自的系统库差异移植的核心工作就是把CALC_CURRENT_HOUR这个函数换成目标平台的时间获取方式。思路是统一的获取当前系统时间转成距离当天0点的秒数整除3600得到小时值。有些平台直接提供了TimeOfDay类型操作最简单有些平台返回的是日期时间结构体需要先取出时分秒再组装成小时还有些老平台支持的是系统实时时钟的BCD码寄存器那就要按位解析。不管哪种换算逻辑放在一个独立函数里统计主程序只调用函数后续移植只改函数内部实现这是最省事的做法。另外建议把统计逻辑放在固定周期的任务里不要放在事件中断或通讯任务里。固定扫描周期让R_TRIG的边界误差可控也让TON的计时更稳定。如果任务周期本身飘忽那讨论R_TRIG和T#5S谁更稳就没什么意义了地基先歪了。5. 调试实录与问题速查表5.1 现场最常见的三个坑第一个坑是累加语句写在交接判断前面。这是跨小时统计最经典的Bug。整点那一扫描周期新值先进累加器然后交接触发把包含新数据的累加值归档到上一小时新小时从零开始等于新小时的第一笔数据被截胡了。排查方法很简单在整点前后各监视一拍rAccumValue和rHourlyData[上一小时]如果发现上一小时最后一个值和累加器清零前的值不一致就说明顺序写反了。第二个坑是定时器复位竞态。T#5S方案里TON的复位信号如果由另一个程序段或中断产生复位时机不可控可能出现在Q为TRUE之前导致那一个采样周期被吞掉。现场表现就是统计报表的小时数据偶尔连续两小时相同缺了一小时的记录。我的处理习惯是永远用R_TRIG抓Q沿复位逻辑跟采样逻辑放在同一个IF块内不让外部信号干预定时器生命周期。第三个坑是归档索引越界。前面说过iLastHour初始值不能用0用-1才能在首次扫描跳过归档。有些同行图省事直接用0初始化结果每天0点的数据在开机那一刻会被脏数据覆盖一次而且这个覆盖发生在凌晨现场不一定有人盯着等发现时报表已经错了好几天。5.2 整点数据串小时的排查思路如果你发现每个小时的报表数据总是有固定偏差比如上一小时多了、下一小时少了按照这个顺序排查基本能定位第一确认交接顺序。看代码里累加语句和交接判断的相对位置顺序错了是最常见的原因修正也最快。第二看整点前后累加器的变化曲线。用在线监视或者趋势图记录rAccumValue在整点前后的值如果清零时刻晚了说明检测延迟大多半是T#5S采样间隔过长如果清零时刻正常但归档值里多了新小时的数据说明顺序问题或者整点前最后一个扫描周期的数据被算进了新小时。第三检查系统时间精度。PLC的实时时钟本身有漂移如果跟标准时间差了几秒整点交接看起来就总是提前或延后。这种情况不是程序逻辑问题需要给系统做时钟同步比如定期从上位机或时间服务器校准。我整理了一张速查表平时排查对着看就行现象可能原因处理思路上一小时数据偏多交接检测延迟跨小时数据进了旧桶缩短采样周期或改用R_TRIG沿触发每小时数据整体串一位首次运行iLastHour初始化为0改成-1首次扫描跳过归档某小时数据缺失TON复位竞态导致采样周期被吞Q沿用R_TRIG抓复位放同一IF块归档执行了多次CLK条件持续为TRUE且没用沿检测R_TRIG是必选项不能拿电平当触发清零后累加器不为0累加语句在交接判断之前调整代码顺序先交接后累加报表与电表总表对不上时钟漂移或统计边界本身有误差同步PLC时钟统计精度按扫描周期评估5.3 关于稳的一个容易被忽略的维度讨论哪种稳时很多人只盯着触发方式本身但整点交接的那一刻CPU可能同时要处理多个定时任务和通讯任务。如果你把统计程序放在优先级较低的任务里而通讯任务又在同一时刻抢占资源交接那拍可能被延迟好几个扫描周期。R_TRIG方案的检测点是程序实际执行到那一步的时刻不受其他任务影响只要它执行了就触发不会因为延迟而漏掉。T#5S方案则受任务调度影响更大如果采样任务被别的任务挤掉一两个周期5秒间隔就变成了5秒加几十毫秒。这一点在现场不容易复现但理论上确实存在。所以我个人写统计程序时习惯把交接检测放到周期任务最靠前的位置确保整点那拍不会被前面的长耗时逻辑拖住。6. 我的结论分场景选择别一竿子打死说了这么多直接给结论。单纯论跨小时统计哪个稳我的答案是R_TRIG更稳但这个稳要定义清楚——它赢在边界精度和交接确定性。毫秒级的误差、天然的防重复触发、首扫描保护干净这些特点都指向同一个事实R_TRIG方案在整点交接这个具体问题上是结构性的正确。T#5S周期采样也不是一无是处。如果你本来就需要每5秒做一次数据快照用于趋势曲线顺便用采样脉冲来检查小时变化这是一种省资源的组合。又或者你的小时基准不是来自PLC本机时钟而是来自外部通讯数据外部信号本身就带有毛刺和抖动那周期采样确实比直接沿触发更抗干扰。但你要清楚它的代价统计边界存在0到5秒的误差而且每隔5秒才反应一次无法做到严格意义上的整点交接。我在实际项目里的做法已经固定下来了统计边界永远用R_TRIG5秒采样快照只用于趋势记录和分析不参与归档判定。说白了两个东西解决的是不同层次的问题。R_TRIG管边界在哪5秒采样管过程曲线长什么样把两者分开想清楚程序就不会乱。最后再分享一个细节。程序里除了交接逻辑我还习惯给每个小时记录附带一个归档时刻的时间戳。这样事后查报表能看到每条数据是什么时候归档的一旦数据对不上可以直接从时间戳反推是时钟问题、扫描问题还是逻辑问题。这个习惯帮我排查过不少现场疑难杂症成本几乎为零收益却很实在。写统计程序的同行不妨也把这个时间戳加上。