☰
PLC跨小时统计方案对比:R_TRIG沿触发与周期采样谁更稳
2026/10/19 11:37:59 网站建设 项目流程

跨小时统计在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跳成TRUE,R_TRIG输出一个扫描周期的TRUE脉冲,程序在这个脉冲里完成归档和清零。

R_TRIG最重要的特性是"只响应一次"。就算条件满足持续了好几个扫描周期,R_TRIG也只在第一个周期输出Q为TRUE,后面即使CLK还是TRUE,Q也保持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,也不会闪烁。程序只要在整点之后的任意一个扫描周期读到这个变化,条件就是TRUE,R_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变回FALSE,IN恢复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_TRIG,5秒采样快照只用于趋势记录和分析,不参与归档判定。说白了,两个东西解决的是不同层次的问题。R_TRIG管"边界在哪",5秒采样管"过程曲线长什么样",把两者分开想清楚,程序就不会乱。

最后再分享一个细节。程序里除了交接逻辑,我还习惯给每个小时记录附带一个归档时刻的时间戳。这样事后查报表,能看到每条数据是什么时候归档的,一旦数据对不上,可以直接从时间戳反推是时钟问题、扫描问题还是逻辑问题。这个习惯帮我排查过不少现场疑难杂症,成本几乎为零,收益却很实在。写统计程序的同行,不妨也把这个时间戳加上。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询