工业控制现场里,Codesys 这套软 PLC 平台用得越深,越会碰到一个绕不开的需求:程序跑起来之后,我怎么知道它到底累不累、忙不忙、哪个任务在拖后腿。很多人第一反应是打开 IDE 在线监控看一圈,但真正上了产线、设备封在电柜里、现场只剩一个触摸屏或者远程维护通道的时候,在线调试那一套就不灵了。这时候你需要的是让 PLC 自己把运行状态"报"出来——系统负载多少、总线周期计数跑到哪了、当前是哪个任务在占用 CPU。这几个数据看着不起眼,却是做设备健康监测、故障回溯、性能调优的地基。
这篇内容就围绕 Codesys 里获取系统负载、读取总线周期计数、拿到任务名字这三件事展开。它适合已经能跑通基本工程、但想在诊断和运维层面再进一步的工控从业者,也适合刚接触软 PLC、想搞清楚"运行时到底暴露了哪些可读信息"的朋友。我会把每个数据从哪来、怎么读、读出来是什么量纲、现场用的时候容易踩什么坑,一条条拆开讲清楚。核心关键词就三个:系统负载、总线周期计数、任务名字,全文都围着它们转。
1. 先搞清楚 Codesys 运行时到底暴露了哪些可读状态
在动手写代码之前,得先建立一个认知:Codesys 的运行时(Runtime)不是黑盒,它内部维护着一套系统级的诊断信息,通过特定的库函数和系统接口对外暴露。你要读的负载、周期计数、任务名,本质上都是这套诊断体系里的成员。不理解这层关系,直接抄代码往往读出来一堆 0 或者莫名其妙的数值,然后就开始怀疑人生。
1.1 系统负载、总线周期、任务名分别属于哪一层信息
把 Codesys 运行时的信息粗略分三层来看会更清晰。最底层是操作系统与硬件层,CPU 占用率、内存使用量这类指标由运行时向底层系统查询后汇总,对应我们要的"系统负载"。中间层是总线与通信层,现场总线(比如 EtherCAT、PROFINET、CANopen 这类)每跑一个通信周期就会累加计数,这就是"总线周期计数"的来源,它反映的是总线通信的节拍和稳定性。最上层是任务调度层,Codesys 支持多任务配置,每个任务有名字、优先级、周期,运行时会把当前活动任务、各任务的执行情况暴露出来,这就是"任务名字"相关的信息。
理解这三层的意义在于:它们读取的方式、刷新频率、精度都不一样。系统负载通常是百分比、刷新较慢;总线周期计数是递增的整数、跟总线节拍同步;任务名是字符串、跟任务调度状态绑定。你不能指望用同一个函数一把梭全读出来,也不能假设它们同时刷新。现场做诊断页面的时候,如果把这几个值混在一个刷新周期里显示,很容易出现"负载跳变但周期计数没动"这种看起来像 bug 的现象,其实只是刷新节奏不同步。
1.2 为什么不能只靠 IDE 在线监控
IDE 的在线监控确实方便,变量表一拉、波形一开,什么都能看。但它有三个硬伤。第一,依赖调试连接,设备一旦交付、调试口封闭或者只留了受限的维护通道,在线监控就用不了。第二,侵入性强,在线监控本身会占用通信带宽和运行时资源,你测出来的负载可能有一部分就是监控自己造成的,数据失真。第三,无法长期记录,IDE 适合"看一眼",不适合 7×24 小时把负载曲线、周期计数趋势存下来做分析。
所以正确做法是让 PLC 程序自己周期性采集这些数据,存到内部变量或者写到文件、上报给上位机。这样既不依赖调试连接,又能长期运行,还能在故障发生时回溯"出事前那几分钟负载是不是已经飙上去了"。这也是为什么"获取系统负载"这类需求在实战里出现频率很高——它不是炫技,是运维刚需。
1.3 读取这些信息需要引入哪些库
Codesys 里读系统级信息,主要靠SysTask、SysTime、CmpSysExec这一类系统库,具体名字随版本和运行时类型略有差异。任务信息相关的函数大多在SysTask库里,比如按索引枚举任务、拿任务属性;系统负载有的版本通过CmpSysExec或者运行时提供的诊断接口获取;总线周期计数则往往跟具体总线主站库绑定,比如 EtherCAT 主站会提供从站或主站的周期计数、帧计数接口。
这里有个经验:不要凭记忆写库函数名。Codesys 版本迭代快,同一个功能在不同版本里函数签名可能变,库也可能拆并。正确姿势是打开库管理器,搜索关键词(比如 Task、Load、Cycle),看当前工程实际可用的库和函数原型,再对照帮助文档确认参数含义。我见过太多人从网上抄了一段代码,结果编译报"函数未定义",折腾半天才发现是版本对不上、库没装。先把库这一关过了,后面才谈得上读数据。
2. 系统负载的读取:从接口调用到数值可信度
系统负载是这三个数据里最"玄"的一个,因为它不是一个确定性的物理量,而是运行时对 CPU 忙碌程度的一个估算。读出来容易,读得准、读得有意义难。这一节把接口调用、量纲换算、可信度判断讲透。
2.1 负载接口的调用逻辑与返回值含义
系统负载的读取通常是一个函数调用,返回一个表示占用率的数值。不同运行时返回的量纲不一样,有的是 0 到 100 的百分比整数,有的是 0 到 1000 的千分比,还有的返回 0 到 1 的浮点。第一步永远是确认量纲,否则你显示个"负载 850"在触摸屏上,操作工以为设备要炸了,其实那是千分比的 85%。
调用逻辑一般是:声明一个输出变量接收返回值,在周期性任务里调用函数,把结果存到一个全局变量供 HMI 或其他任务读取。要注意的是,这个函数调用本身有开销,不要放在 1ms 的高频任务里每个周期都调,一般放在 100ms 到 1s 的慢任务里足够了。负载是个宏观指标,刷新太快没有意义,反而增加负担。
// 示意:在慢周期任务中采集系统负载 PROGRAM PRG_SysDiag VAR nCpuLoadRaw : INT; // 原始返回值 rCpuLoadPct : REAL; // 换算后的百分比 tonSample : TON; // 采样节拍控制 END_VAR tonSample(IN := NOT tonSample.Q, PT := T#500MS); IF tonSample.Q THEN // 具体函数名以当前工程库为准 nCpuLoadRaw := SysGetCpuLoad(); rCpuLoadPct := INT_TO_REAL(nCpuLoadRaw) / 10.0; // 假设千分比 END_IF上面这段是结构示意,函数名和换算系数一定要按你实际工程里的库文档来改。我特意把换算单独拎出来写成一行,就是为了让量纲这件事在代码里显式可见,而不是藏在某个魔法数字里。
2.2 负载数值为什么会有"毛刺",怎么处理
现场调试时经常遇到负载值偶尔蹦到很高又马上掉下来,看着像异常。这多半不是真故障,而是采样时刻恰好撞上了某个重任务执行。系统负载本质是滑动窗口内的平均值或者瞬时采样,如果采样点正好落在一次大数据拷贝、一次文件写入、一次通信突发上,读数就会偏高。
处理办法有两个方向。一是软件滤波,对连续几次采样做滑动平均,把单次毛刺抹平。二是看趋势不看瞬时值,HMI 上显示最近 10 次的平均值,同时记录最大值用于事后分析。我一般会在诊断程序里同时维护"当前值""10 次平均""历史峰值"三个变量,当前值用于快速响应,平均值用于展示,峰值用于判断设备是否曾经接近过载。
注意:滤波窗口不要设太大,否则真实的负载上升会被延迟发现。500ms 采样、10 次窗口,相当于 5 秒的平滑,对大多数设备够用;如果设备对过载极敏感,窗口要缩短。
2.3 负载读数的实际参考价值与误判场景
负载读数最大的价值不是"精确测量",而是"趋势判断"和"异常预警"。比如设备刚上线时负载稳定在 30%,运行半年后同样工况下变成 55%,这就提示你程序里有东西在累积——可能是内存没释放、可能是某个队列越积越长、可能是通信重试变多。这种趋势变化比绝对值更有诊断意义。
但也要警惕误判。负载高不一定是程序写得烂,也可能是任务配置不合理:把一个重计算任务和通信任务放在同一优先级、同一周期,互相抢 CPU,负载自然高。还有一种情况是负载读数本身受运行时版本影响,升级固件后同样的程序读数变了,这时候别急着改程序,先确认是不是运行时对负载的统计口径变了。我踩过一次坑:升级运行时后负载从 40% 跳到 70%,排查了半天程序,最后发现是新版本把某个后台服务也算进了负载统计,属于口径变化,不是真问题。
3. 总线周期计数:判断通信健康的一面镜子
总线周期计数这个数据,很多人不知道怎么用,觉得就是个不断增大的数字。其实它是判断现场总线通信是否稳定的利器——周期计数该涨的时候不涨、涨的速度不对、或者出现跳变,都对应着具体的通信问题。
3.1 周期计数的来源与递增规律
现场总线每完成一个通信周期,主站侧通常会维护一个计数器自增。这个计数器可能叫 CycleCount、FrameCount、WorkingCounter 相关的东西,具体看总线类型和主站库的实现。以常见的周期通信总线为例,主站按设定的周期(比如 1ms、2ms)向从站收发数据帧,每成功完成一轮,计数加一。所以理论上,计数增长速度应该等于 1 除以总线周期。1ms 周期的话,每秒应该涨 1000 左右。
这个规律就是判断依据:如果你读到的计数增长速度明显低于理论值,说明有周期被跳过了,可能是通信负载过重、从站响应慢、或者主站任务被更高优先级任务抢占导致错过了发送时机。如果计数出现大幅跳变或者回绕,那更要警惕。理解"理论增速"这个概念,是把这个数据用起来的前提。
3.2 读取周期计数的两种典型位置
周期计数一般有两个读取位置。一是主站对象,主站库通常暴露一个全局的周期计数或者统计结构,读它最直接,反映的是主站视角的通信节拍。二是从站或过程数据映像,某些总线允许从从站侧读到与通信相关的计数或状态字,这反映的是从站视角。
两者对比着看很有价值:主站计数正常涨、从站侧计数不涨或者对不上,说明问题出在链路中间或者从站本身;两边都正常但过程数据不更新,那可能是数据映射或者应用层逻辑的问题。我习惯在诊断程序里同时采集主站周期计数和一个关键从站的通信状态,两个值放一起看,定位问题的效率高很多。
// 示意:周期计数采集与增速计算 PROGRAM PRG_BusDiag VAR dwCycleCount : DWORD; // 当前周期计数 dwCycleCountOld : DWORD; // 上一采样点计数 diCycleDelta : DINT; // 采样间隔内的增量 rCycleRate : REAL; // 折算每秒周期数 tonRate : TON; END_VAR tonRate(IN := NOT tonRate.Q, PT := T#1S); IF tonRate.Q THEN // 读取当前周期计数,函数/变量名按实际主站库调整 dwCycleCount := BusGetCycleCounter(); diCycleDelta := DWORD_TO_DINT(dwCycleCount - dwCycleCountOld); rCycleRate := DINT_TO_REAL(diCycleDelta); // 1 秒窗口即每秒周期数 dwCycleCountOld := dwCycleCount; END_IF这段代码的关键在于用"差值"而不是"绝对值"来判断健康度。绝对值一直在涨,看不出问题;差值稳定在理论值附近,才说明通信节拍正常。
3.3 用周期计数差值定位通信抖动
实际排查通信抖动时,我会把每秒的周期增量记录下来,跟理论值对比。假设总线周期 2ms,理论每秒 500 个周期。如果某几秒的增量掉到 480、460,说明那几秒丢了周期,通信有抖动。这时候再结合系统负载看:如果丢周期的那几秒负载也偏高,那大概率是 CPU 忙不过来导致主站任务错过了发送窗口;如果负载正常但周期还是丢,那要往链路质量、从站响应、电磁干扰方向查。
这种"周期计数差值 + 系统负载"的联合分析,是我认为这套诊断数据最有价值的用法。单看任何一个都只能猜,两个放一起就能缩小范围。表格对比一下几种典型组合:
| 周期增量 | 系统负载 | 可能原因 | 排查方向 |
|---|---|---|---|
| 正常 | 正常 | 通信健康 | 无需处理 |
| 偏低 | 偏高 | CPU 抢占导致丢周期 | 优化任务优先级、拆分重任务 |
| 偏低 | 正常 | 链路或从站问题 | 查线缆、从站状态、干扰源 |
| 跳变/回绕 | 任意 | 计数溢出或主站重启 | 确认计数位宽、查主站日志 |
这张表不是万能公式,但能帮你快速建立排查方向,比盲目翻手册强。
4. 任务名字的获取:多任务工程里的"谁在干活"
Codesys 支持多任务,一个工程里可能同时跑高速运动任务、慢速逻辑任务、通信任务、HMI 刷新任务。出问题时,光知道"负载高"不够,还得知道"是哪个任务高"。任务名字的获取就是解决这个问题的入口。
4.1 任务信息的枚举方式与名字字段
运行时一般提供按索引枚举任务的接口:给一个索引,返回该任务的句柄或信息结构,结构里包含任务名字、优先级、周期、当前状态等字段。任务名字通常是字符串类型,长度有限制,读取时要准备好足够大的字符串缓冲区,否则会被截断。
枚举的逻辑是:从索引 0 开始,逐个读,直到接口返回"无效索引"或者错误码,说明枚举完了。这个过程适合在初始化阶段做一次,把任务列表和名字缓存下来,运行中只需要按缓存的句柄去查状态,不用每次都重新枚举。我见过有人在每个扫描周期都重新枚举一遍任务,纯属浪费,而且枚举接口本身可能有开销。
// 示意:枚举任务并缓存名字 PROGRAM PRG_TaskList VAR i : INT; sTaskName : STRING(63); bEnumDone : BOOL; aTaskNames : ARRAY[0..15] OF STRING(63); nTaskCount : INT; END_VAR IF NOT bEnumDone THEN FOR i := 0 TO 15 DO // 按索引取任务名,函数名以实际库为准 IF SysTaskGetNameByIndex(i, ADR(sTaskName), SIZEOF(sTaskName)) THEN aTaskNames[i] := sTaskName; nTaskCount := nTaskCount + 1; ELSE EXIT; // 无效索引,枚举结束 END_IF END_FOR bEnumDone := TRUE; END_IF这段是初始化枚举的骨架,重点是"枚举一次、缓存起来"这个思路。字符串缓冲区用STRING(63)是常见做法,够用且不浪费。
4.2 任务名与负载、周期的关联分析
拿到任务名字之后,真正的价值在于把"任务"和"负载/周期"关联起来。理想情况下,运行时能提供每个任务的执行时间统计,这样你就能算出哪个任务吃掉了最多 CPU。如果运行时只提供全局负载,那至少可以通过"任务状态 + 全局负载"做间接判断:比如某个任务配置的周期很短、优先级很高,全局负载又偏高,那它嫌疑最大。
关联分析的一个实用技巧是做任务执行时间的基线。设备正常运行时,记录各任务的典型执行时间或者全局负载基线;一旦负载偏离基线,对照任务列表看最近有没有新增任务、有没有任务周期被改短、有没有任务里加了重逻辑。这种"基线 + 变更对照"的方法,比单纯盯着负载数字有效得多。
4.3 任务名在故障回溯中的用法
故障回溯时,任务名是很好的"时间线锚点"。比如设备在某个时刻报警,你回看那段时间的诊断记录:负载在报警前 30 秒开始爬升,同时某个通信任务的周期计数增量开始下降。这时候任务名告诉你"是通信任务受影响",而不是笼统的"系统忙"。有了这个线索,再去查通信任务里最近改了什么、是不是加了大数据量收发,方向就明确了。
我一般会在诊断记录里给每条数据打上"采集时刻 + 任务上下文"的标签,虽然任务上下文不是每个数据都能直接带,但至少全局负载和周期计数能跟"当时哪个任务在主导"建立关联。这套记录在事后分析时,比一堆孤立的数字有用得多。
5. 把三样数据整合成一套可落地的诊断方案
单独读三个数据不难,难的是把它们组织成一套能长期跑、能指导运维的方案。这一节讲整合思路、采样节奏设计、存储与上报,以及现场部署时的注意事项。
5.1 采样节奏与任务划分的设计
三个数据的刷新特性不同,采样节奏要分开设计。系统负载变化慢,500ms 到 1s 采一次足够;总线周期计数要算增速,采样窗口至少 1s,否则差值太小、噪声大;任务名基本不变,初始化枚举一次即可,运行中只在任务状态变化时更新。
任务划分上,我建议单独建一个低优先级的诊断任务,周期设 500ms 或 1s,专门干采集和记录。这样做的好处是:诊断逻辑跟控制逻辑解耦,诊断任务出问题不影响控制;诊断任务优先级低,不会抢占控制任务的 CPU;采集节奏统一管理,好维护。千万别把诊断采集塞进高速控制任务里,那等于给控制循环增加不确定性。
5.2 数据存储:环形缓冲与掉电保持的取舍
诊断数据要能回溯,就得存。存哪里、存多久,是个取舍。内存里的环形缓冲适合存最近几分钟的高频数据,掉电就丢,但读写快、不磨损存储介质;文件或者持久化变量适合存关键节点和峰值,掉电保持,但写入有开销、频繁写会磨损 Flash。
我的做法是两层:内存环形缓冲存最近 5 到 10 分钟的秒级数据,用于实时查看和短期回溯;每小时把负载峰值、周期计数异常次数、任务状态变化这些"摘要"写一次持久化存储,用于长期趋势。这样既不会频繁写 Flash,又保留了长期分析需要的关键信息。写持久化时要注意不要在每个扫描周期写,用定时触发或者变化触发,否则存储介质寿命扛不住。
5.3 上报给上位机或 HMI 的接口设计
诊断数据最终要给人看,接口设计要考虑"谁看、看什么、多久看一次"。HMI 操作工关心的是"当前负载高不高、有没有报警",给他看当前值和状态灯就够;维护工程师关心趋势和异常,给他看曲线和异常记录;上位机系统可能要做集中监控,那就需要结构化的数据上报。
接口设计上,我习惯把诊断数据打包成一个结构体,字段命名清晰、量纲在名字里体现(比如rCpuLoadPct、dwBusCycleDelta),HMI 和上位机按需取字段。这样比散落一堆全局变量好维护得多。上报频率跟采样频率对齐,不要 HMI 每 100ms 刷一次而数据 1s 才更新一次,造成"数据不动"的错觉。
注意:上报结构体里最好带一个"数据有效"标志位,采集失败或者初始化未完成时置 FALSE,避免上位机把默认值 0 当成真实负载。
5.4 现场部署时最容易忽略的几个细节
第一个细节是运行时版本差异。不同版本的运行时,负载接口的量纲、任务枚举接口的签名、总线计数的可读性都可能不同。部署前一定要在目标设备同版本环境里验证一遍,别在开发机上跑通了就直接上现场。
第二个细节是诊断代码本身的资源占用。诊断逻辑再轻也有开销,尤其是字符串操作和文件写入。枚举任务名用了字符串拷贝,如果任务多、枚举频繁,开销不小。我的经验是枚举一次缓存、字符串操作尽量在初始化阶段做完,运行中只做数值比较和赋值。
第三个细节是异常情况的兜底。接口调用可能失败、返回值可能无效、周期计数可能回绕。代码里要有判断:读失败时保持上一次有效值并置无效标志,计数回绕时用差值计算而不是直接比较大小。这些兜底不做,现场一旦出现异常,诊断数据本身就成了误导源。
6. 几个真实场景下的排查思路复盘
光讲接口和代码不够,实际排查时的思路才是干货。这里复盘几个典型场景,展示怎么用这三样数据把问题揪出来。
6.1 负载缓慢爬升但周期计数正常
有个场景是设备运行几小时后负载从 35% 慢慢爬到 60%,但总线周期计数增速一直正常。这种情况基本可以排除通信问题,方向转向"程序内部有累积"。常见原因是动态内存分配没释放、队列或者缓冲越积越长、日志或者记录没做上限。排查时我会在诊断记录里加上内存使用量(如果运行时能读),跟负载曲线对照,如果内存也在爬,那基本锁定是资源泄漏。
这类问题的教训是:负载趋势要长期记录,只看当前值发现不了缓慢爬升。设备跑一天看不出问题,跑一周就暴露了。所以诊断方案里长期趋势记录不是可选项,是必需项。
6.2 周期计数增速下降伴随负载尖峰
另一个场景是周期计数每秒增量偶尔从 500 掉到 450,同时负载出现尖峰。这种组合指向"CPU 被短时抢占"。进一步查任务列表,发现某个低优先级任务里有一段重计算,虽然优先级低,但执行时间长,在某些时刻跟通信任务撞车。解决办法是把重计算拆成小块分多次执行,或者调整任务周期错开,避免长时间占用。
这个案例说明,任务名和周期计数要联合看。单看周期计数下降,你会怀疑通信;单看负载尖峰,你会怀疑程序;两个一起看,才能定位到"任务调度层面的冲突"。
6.3 任务枚举失败导致的诊断空白
还有一次是诊断页面上一片空白,负载和周期都不显示。查下来是任务枚举接口在某个运行时版本上签名变了,初始化时调用失败,导致整个诊断程序卡在初始化阶段没往下走。这个坑的教训是:初始化逻辑要有失败兜底,枚举失败不应该阻塞后续采集,最多是任务名显示不出来,负载和周期该采还得采。代码里把各采集项做成独立的、互不阻塞的模块,一个失败不影响其他,这是诊断程序健壮性的基本要求。
7. 一些让诊断数据真正好用的经验补充
最后补充几个零散但实用的经验,都是踩过坑之后总结的。
关于量纲和单位,永远在变量名里体现。rCpuLoadPct比rCpuLoad好,dwBusCycleDelta比dwBusCycle好。现场维护的人不一定懂你的代码,但看变量名能猜个八九不离十,这能省很多沟通成本。
关于采样与显示的分离,采集频率和显示刷新频率不要绑死。采集可以 500ms 一次,HMI 显示 1s 刷一次,中间用最新值填充。这样采集够密、显示不闪,两边都舒服。
关于诊断数据的自检,诊断程序本身也要能被诊断。比如加一个"诊断任务心跳"计数器,如果它不涨了,说明诊断任务本身挂了,这时候上位机应该报警"诊断失效"而不是显示一堆陈旧数据。这个自检机制在长期运行的设备上特别重要。
关于文档化,每个诊断变量的来源、量纲、刷新周期、有效范围,都该在工程文档里写清楚。我接手过别人做的诊断页面,变量名一堆缩写、量纲全靠猜,维护起来极其痛苦。自己做的方案,多花十分钟写清楚,后面省几小时。
这套获取系统负载、总线周期计数、任务名字的方案,说到底就是把 Codesys 运行时暴露的诊断信息用起来,让设备自己会"说话"。数据本身不复杂,难的是采样节奏的设计、量纲的把控、异常情况的兜底,以及把几个数据联合起来分析的能力。我在实际项目里用这套思路做过设备健康监测,也在故障回溯时靠它快速定位过通信抖动和任务冲突,实测下来,只要把量纲和刷新节奏这两件事做扎实,剩下的就是按需扩展的事了。