
1. 为什么要在 Codesys 里盯住系统负载和总线周期做过 PLC 项目的人大概都有过这种经历设备在实验室跑得好好的一到现场运行两三周偶尔卡顿一下或者某个从站莫名其妙掉线重启又恢复正常。这种偶发问题最折磨人因为你抓不到现场。后来我养成了一个习惯——在程序里把系统负载、总线周期计数、任务名字这些运行时指标持续记录下来出问题时翻日志往往一眼就能定位到是哪个任务拖慢了扫描周期或者总线在哪个时间点出现了异常抖动。这次要聊的就是在 Codesys 环境下获取系统负载、总线周期计数以及任务名字这套组合拳。说白了它解决的是我的控制器现在到底累不累、总线跑得稳不稳、是哪个任务在捣乱这三个问题。适合已经能写基本 PLC 逻辑、想往诊断和运维方向深入的朋友也适合刚接手存量项目、需要快速摸清系统运行状况的工程师。哪怕你之前没怎么碰过 Codesys 的系统库跟着思路走也能落地。Codesys 本身提供了不少隐藏得比较深的系统接口SysTaskInfo、SysBus、CmpSysTask这些库函数就是典型代表。它们不像运动控制库那样有大量例程文档也偏底层但只要理解了数据结构和调用时机拿到的信息量非常大。下面我按设计思路—核心细节—实操落地—问题排查的顺序把这几块内容掰开揉碎讲清楚。2. 整体设计思路与方案选型拆解2.1 需求本质三个指标分别回答什么问题先把需求拆开看。系统负载CPU Load反映的是控制器 CPU 的忙闲程度通常以百分比表示它告诉你当前扫描周期里有多少时间被真正用于执行代码剩下的是空闲。这个值持续偏高说明程序太重或者任务分配不合理。总线周期计数Bus Cycle Count是现场总线每完成一次数据交换就累加的一个计数器它和总线周期时间配合看能判断总线是否在稳定节拍上运行掉站、重连、周期抖动都会在计数曲线上留下痕迹。任务名字则是把上面这些指标和具体任务对应起来的关键——光知道负载 80%没用得知道是哪个任务吃掉了这 80%。三者组合起来本质上是在构建一个轻量级的运行时健康画像。我在实际项目里通常会把它们做成一个循环记录的结构体数组配合时间戳形成一个黑匣子事后回放非常直观。2.2 为什么选系统库而不是自己算有人会问负载不能自己用定时器估算吗总线周期不能自己数吗理论上可以但坑很多。自己用TON估算负载精度受任务优先级和抖动影响误差可能到 10% 以上自己数总线周期你得精确知道总线任务的触发点稍有不慎就漏计或重计。Codesys 的CmpSysTask组件在系统层面已经维护了这些计数SysTaskInfo读出来的负载是运行时真实统计值精度和可靠性远高于应用层估算。选型上的核心考量是诊断代码本身不能成为负担。系统库的读取操作开销很小通常就是一次内存拷贝放在低优先级的后台任务里跑对主控逻辑几乎无影响。这也是我坚持用系统接口而不是应用层轮询的根本原因。2.3 数据采集的架构设计我的常规做法是分三层采集层、缓存层、输出层。采集层放在一个独立的低优先级任务里比如 100ms 或 500ms 触发一次调用系统函数把负载、周期计数、任务名读出来缓存层用一个环形缓冲区Ring Buffer存最近 N 条记录避免频繁写文件拖慢系统输出层按需把缓冲区内容落盘或者通过可视化界面展示。这个分层的好处是解耦——采集频率可以独立调整输出方式可以随时替换今天写 CSV明天改成写数据库或走 OPC UA互不影响。环形缓冲区的大小我一般取 1000 到 5000 条按 500ms 一条算能覆盖 8 到 40 分钟的历史足够定位大多数偶发问题。注意采集任务一定要设成低优先级且不要在采集任务里做任何阻塞操作比如同步写文件。我见过有人在采集任务里直接SysFileWrite结果磁盘慢的时候把整个任务卡住反而制造了新的负载问题。3. 核心细节解析与实操要点3.1 SysTaskInfo 结构体到底装了什么SysTaskInfo是获取任务信息的核心入口。调用SysTaskInfoGet不同版本库函数名可能略有差异常见的是通过CmpSysTask组件提供的接口能拿到一个任务信息结构里面包含任务名称、任务优先级、当前状态、周期时间、执行时间、以及我们最关心的 CPU 负载统计。这里有个容易踩的坑任务名字段是定长字符串通常是STRING(31)之类的固定长度读取后要做去空格处理否则后续做字符串比较或者写日志时会带一堆尾随空格看起来很难受。我一般用SysStrTrim或者自己写个简单的截断逻辑处理。另一个细节是负载字段的单位。有的版本返回的是千分比比如 800 表示 80%有的是百分比整数还有的是浮点。这个必须查对应版本的库文档确认不能想当然。我吃过一次亏按百分比理解结果数值差了 10 倍排查了半天才发现是单位问题。3.2 总线周期计数的获取路径总线周期计数不像任务负载那样有统一的SysTaskInfo入口它通常和具体总线类型绑定。以 EtherCAT 为例主站对象里有周期计数相关的诊断变量CANopen 主站也有类似的计数器。通用的做法是通过总线主站实例的诊断接口读取或者用SysBus相关函数遍历总线状态。实操中我倾向于直接访问主站实例的诊断结构因为路径最短、延迟最低。比如 EtherCAT 主站通常会暴露一个包含CycleCount、WorkingCounter、SlaveState等字段的诊断区周期计数就在里面。读取时要注意数据类型宽度——周期计数往往是UDINT甚至ULINT用INT接会溢出跑一段时间就变成负数非常隐蔽。3.3 任务名字与指标的关联映射单独拿到任务名字和单独拿到负载都不难难的是把它们正确对应起来。SysTaskInfoGet通常支持按索引遍历所有任务索引从 0 开始递增直到返回错误这样就能枚举出全部任务。每个任务条目里既有名字也有负载天然就是关联的。但要注意任务枚举的顺序不保证稳定不同编译版本、不同配置下顺序可能变化。所以做长期记录时不要用第 0 个任务是主任务这种假设而应该用任务名字做键。我一般会先枚举一遍建立名字→索引的映射表后续采集按名字查索引这样即使顺序变了也不影响。3.4 采集频率与精度的权衡采集频率不是越高越好。太高会增加系统开销而且很多指标本身是统计平均值采太密意义不大太低又会漏掉瞬时尖峰。我的经验值是负载采集 200ms 到 500ms 一次这个粒度既能捕捉到持续性的负载上升又不会漏掉持续几百毫秒的尖峰总线周期计数 100ms 到 200ms 一次因为总线抖动往往发生在毫秒级需要相对密一点的采样来还原趋势。如果确实需要捕捉极短时间的异常可以临时把采集频率提到 10ms 甚至更高但只作为诊断模式短期开启长期跑不建议。这就像示波器的高采样率模式适合抓瞬态不适合长时间录制。4. 实操过程与核心环节实现4.1 环境准备与库引用第一步是把需要的库加进工程。在 Codesys 的库管理器里通常需要引用SysTask或CmpSysTask对应的库、SysTypes2 Interfaces、以及总线相关的诊断库如 EtherCAT 的IoDrvEtherCAT诊断接口。不同版本的库名可能有出入以你本地安装的为准。引用完成后在全局变量区定义好数据结构。我习惯定义一个记录结构体TYPE ST_SystemHealth : STRUCT sTaskName : STRING(63); rCpuLoad : REAL; // 百分比 udiBusCycle : UDINT; // 总线周期计数 dtTimestamp : DT; // 采集时间戳 END_STRUCT END_TYPE然后定义一个环形缓冲区数组aHealthLog : ARRAY[0..1999] OF ST_SystemHealth;和一个写指针uiWriteIdx : UINT;。2000 条按 500ms 算大约覆盖 16 分钟够用。4.2 任务枚举与负载读取的实现采集任务的主体逻辑分两步。第一步枚举任务第二步逐个读取负载。伪代码逻辑大致如下// 枚举所有任务建立名字到索引的映射 FOR i : 0 TO MAX_TASKS - 1 DO IF SysTaskInfoGet(i, stTaskInfo) 0 THEN // 记录 stTaskInfo.sName 和 i 的对应关系 ELSE EXIT; // 索引越界枚举结束 END_IF END_FOR这里SysTaskInfoGet的返回值判断很关键——成功返回 0失败返回错误码遇到失败就说明枚举到头了。拿到任务列表后对每个关心的任务读取其负载字段。如果只想监控主任务和总线任务可以按名字过滤减少记录量。提示任务名字在结构体里可能是定长带填充的比较前务必做 trim。我一般写个F_TrimString辅助函数去掉首尾空格和不可见字符。4.3 总线周期计数的读取与换算总线周期计数的读取依赖具体总线。以常见的 EtherCAT 主站为例主站实例通常有一个诊断结构里面包含周期计数。读取方式类似// 假设 pMaster 是主站实例指针 udiCycle : pMaster^.diag.udiCycleCount;如果拿不到直接的诊断结构可以退而求其次用总线状态函数遍历。但要注意周期计数是累加的会溢出。UDINT最大约 42 亿按 1ms 周期算大约 49 天溢出一次。做长期记录时要么用ULINT要么在软件里做溢出处理——检测到当前值小于上次值时认为发生溢出累加一个偏移量。这个细节不做跑一个多月后曲线会突然掉到 0非常迷惑。4.4 时间戳与落盘策略每条记录都带时间戳用SysTimeGet或SysTimeRtcGet获取。落盘策略我推荐缓冲满或定时触发两种模式结合缓冲区写满一圈时批量落盘或者每隔固定时间比如 5 分钟落一次防止掉电丢数据。落盘格式用 CSV 最省事一行一条记录字段用逗号分隔Excel 直接能打开分析。写文件时用SysFileOpen、SysFileWrite、SysFileClose这一套注意写完要 close否则文件句柄泄漏跑久了会出问题。我一般把落盘放在独立的低优先级任务里和采集任务分开避免写盘阻塞采集。4.5 一个完整的采集周期示例把上面串起来一个采集周期的执行顺序是读时间戳 → 枚举任务并读负载 → 读总线周期计数 → 组装成结构体 → 写入环形缓冲区 → 指针递增取模→ 判断是否触发落盘。整个流程在 500ms 周期任务里跑实测 CPU 占用增加不到 1%对主控逻辑无感。这里有个实操心得指针递增用取模而不是 if 判断。uiWriteIdx : (uiWriteIdx 1) MOD 2000;一行搞定比IF uiWriteIdx 2000 THEN uiWriteIdx : 0;更简洁也不容易写错边界。5. 常见问题与排查技巧实录5.1 负载读数一直是 0 或恒定不变这是最常见的现象。原因通常有三类一是库函数调用失败但没检查返回值读到的是未初始化内存二是任务索引传错读到了不存在的任务三是单位理解错误实际值很小被当成 0。排查时先打印SysTaskInfoGet的返回值确认调用成功再打印原始字段值确认不是单位问题。我遇到过一次是库版本不匹配函数签名变了但编译没报错运行时静默失败换了库版本就好了。5.2 总线周期计数跳变或归零前面提过溢出问题UDINT约 49 天归零一次。如果周期更短就归零那可能是总线重连导致计数器重置。这时候要结合从站状态一起看——如果归零的同时有从站掉线记录那就是总线异常如果只是单纯归零且从站都正常大概率是溢出。处理办法就是做溢出检测和偏移累加。5.3 任务名字乱码或带大量空格定长字符串的经典问题。读取后必须 trim且要注意字符编码。Codesys 默认字符串是单字节如果任务名里有非 ASCII 字符可能显示异常。建议任务命名统一用英文加下划线避免中文和特殊符号从源头规避。5.4 采集任务本身拖慢系统这是设计问题。采集任务优先级设太高、采集频率太密、或者在采集任务里做了阻塞操作都会导致这个问题。解决办法是把采集任务降到最低优先级采集频率控制在 200ms 以上所有耗时操作写文件、通信挪到独立任务。我一般还会给采集任务加一个看门狗如果单次采集耗时超过预期记录一条告警方便发现性能退化。5.5 常见问题速查表现象可能原因排查方向解决方式负载恒为 0函数调用失败/单位错误检查返回值与原始值修正调用或单位换算周期计数归零溢出或总线重连结合从站状态判断溢出累加偏移/排查总线任务名乱码定长填充/编码问题打印原始字节trim 统一英文命名系统变慢采集任务过重看采集任务耗时降优先级/降频率/分离写盘落盘失败文件句柄泄漏/路径错误检查 close 与路径确保 close/校验路径注意所有诊断代码上线前务必在目标硬件上实测一轮模拟长时间运行至少跑满一个溢出周期或几天确认没有内存泄漏和计数器异常。实验室短时间测试发现不了溢出和句柄问题。6. 我在实际项目里踩过的几个坑说几个文档里不会写、但实际会遇到的细节。第一个是任务枚举的索引上限。不同控制器支持的最大任务数不同硬编码一个很大的上限去枚举虽然能工作但每次采集都白跑很多次失败调用浪费开销。我的做法是先枚举一次确定实际任务数缓存下来后续只遍历有效范围。第二个是时间戳的时区问题。SysTimeRtcGet拿到的是本地时间还是 UTC取决于配置。做跨设备日志对比时如果时区不一致时间线会对不上。我现在的习惯是统一用 UTC 时间戳记录展示时再转本地避免混乱。第三个是环形缓冲区的并发访问。如果采集任务和落盘任务同时访问缓冲区可能读到写了一半的数据。解决办法是用一个简单的标志位做互斥或者让落盘任务只读已经写满且不再变动的那部分区域。这个细节不做偶尔会出现日志里某条记录字段错位很难查。第四个是总线周期计数和负载的采样不同步。如果两者在不同任务里采时间戳会差几十毫秒做关联分析时会有偏差。我的做法是把它们放在同一个采集周期里顺序读取共用一个时间戳保证数据对齐。这套东西搭起来之后现场排查效率提升非常明显。以前遇到偶发卡顿只能靠猜现在翻日志看负载曲线和总线计数基本能锁定问题窗口。后续还可以扩展——比如把数据推到可视化面板做实时曲线或者加个阈值告警负载超过 85% 持续 10 秒就记一条警告。这些都是在现有采集框架上加点逻辑的事成本很低。