ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

IEC 61131-3 是工业控制的ABI:三层模型与POU语义深度解析

IEC 61131-3 是工业控制的ABI:三层模型与POU语义深度解析

简介:本资源为IEC 61131-3国际编程标准的精要学习手册,面向自动化工程师、PLC开发人员及工业控制专业学生,旨在帮助读者系统掌握该标准的核心架构与工程实践方法。手册深入解析IEC 61131-3软件模型(含配置、资源、任务、POU组织单元)与通信模型,详述程序、功能块和功能三类POU的定义、差异与协同机制,并涵盖ST/IL语言操作符、变量类型、I/O映射及多任务执行控制等关键内容,适用于PLC编程、DCS组态、SCADA逻辑开发及软逻辑系统设计等实际场景。资源为单文件DOC文档,共1个文件,大小182KB,结构紧凑、术语规范,便于快速查阅与原理理解。目前已有2759人学习下载,是入门IEC标准编程、理解跨厂商PLC统一开发范式的重要参考资料。

1. IEC 61131-3 不是“PLC 编程语言合集”,而是工业控制软件的「操作系统级抽象层」:它让同一段 ST 代码能在西门子、AB、施耐德甚至软 PLC 上原样运行,靠的不是语法兼容,而是一套被严格分层、可映射、可裁剪的执行语义模型

你写完一段结构化文本(ST)逻辑,编译通过,下载进 PLC——这看似理所当然的过程,在 IEC 61131-3 出现前根本不存在。上世纪 90 年代,每个 PLC 厂家都用自己私有的指令集、扫描机制、变量寻址方式和任务调度策略:AB 的 RSLogix 里一个定时器叫TON,西门子 STEP 5 里叫SP,而三菱 FX 系列甚至没有标准定时器块,全靠梯形图触点+计数器硬凑。工程师换平台就得重学、重写、重调,调试日志里满屏是“为什么这个布尔变量在 A 厂家是上升沿触发,在 B 厂家却要加个 NOT 才对”。IEC 61131-3 的真正颠覆性,不在于它定义了五种语言(LD、FBD、ST、SFC、IL),而在于它把“程序怎么被执行”这件事,从硬件寄存器操作中彻底剥离出来,抽象成一套与物理扫描周期解耦、与内存布局无关、与中断响应策略正交的可验证执行模型。它不规定 CPU 怎么取指,但规定“当任务 T 被触发时,POU P 的所有输入引脚必须在本次执行前完成赋值,且所有输出引脚的写入必须原子地发生在本次执行结束时刻”。这个模型让“功能块复用”不再是玄学——你封装一个 PID 功能块,它的静态变量生命周期、输入采样时机、输出更新边界,全部由标准明确定义,而非依赖某家厂商的“血泪经验”或“文档没写清楚的隐式行为”。这份手册之所以值得深挖,是因为它不是语法速查表,而是整套工业控制软件的 ABI(Application Binary Interface)说明书:它告诉你,当你写下CTU(CU:=Start, R:=Reset, PV:=10)时,背后触发的是哪一层资源调度、哪个配置上下文的变量绑定、哪类任务的执行队列插入,以及——最关键的是——如果它没按预期工作,你应该去查模型哪一层的映射出了问题,而不是盲目翻厂商手册第 387 页的“特殊注意事项”。


2. IEC 软件模型:三层嵌套容器(Configuration → Resource → Task/POU)如何将“写代码”与“占硬件”彻底解耦

IEC 61131-3 的软件模型不是扁平的“程序→PLC”映射,而是一个带明确所有权边界的三层容器体系。理解这三层,是读懂所有后续机制的前提——它决定了你写的代码最终跑在哪、被谁调度、和谁共享内存。

2.1 配置(Configuration):整个自动化系统的“命名空间根目录”,而非单台 PLC 的镜像

配置(Configuration)常被误读为“一台 PLC 对应一个配置”,这是最典型的认知偏差。手册原文明确指出:“在一个复杂的由多台 PLC 组成的自动化生产线中,每台 PLC 中的软件就是一个独立的‘配置’”。但反过来说,一个配置也可以横跨多台物理设备。例如,某汽车焊装线控制系统中,一个 Configuration 可能包含三部分:主控 PLC(运行安全逻辑)、视觉处理工控机(运行图像识别 ST 程序)、机器人控制器(运行运动控制 FBD)。它们通过 IEC 61131-5 定义的通信功能块(如READ_VAR,WRITE_VAR)交换数据,但所有这些 POUs 共享同一个配置级全局变量表和统一的任务调度视图。这意味着,你在 Configuration 层声明的GVL_Motion: GVL_MotionType;全局变量结构体,其成员GVL_Motion.ActualPos可被 PLC 的 ST 程序读取、被工控机的 C++ OPC UA 客户端写入、被机器人控制器的功能块直接绑定——只要它们属于同一配置。这种设计让系统集成从“设备拼接”升级为“功能编织”,而配置就是那张织网的经纬基准。

提示:配置不是物理实体,而是逻辑容器。它的存在意义是定义变量作用域边界和通信信任域。跨配置访问必须显式使用存取路径(Access Path),如Config2.Resource1.POU1.OutputVar,这强制暴露了网络拓扑,避免了隐式耦合。

2.2 资源(Resource):PLC 运行时的“虚拟机监控器”,负责物理 I/O 绑定与任务调度仲裁

如果说配置是命名空间,那么资源(Resource)就是执行沙箱。手册强调:“资源不仅为运行程序提供了一个支持系统,而且它反映了 PLC 的物理结构,在程序和 PLC 物理 I/O 通道之间提供了一个接口。” 这句话的潜台词是:Resource 是唯一有权直接操作硬件的软件层。一个 Resource 内部可以声明多个任务(Task),每个任务关联一组 POU(Program/Function Block),但所有这些 POU 的 I/O 访问,必须经由该 Resource 提供的 I/O 映射表(I/O Mapping Table)进行地址翻译。

以典型应用为例:

// 在 Resource1 的变量声明区 VAR_GLOBAL %IX0.0 AT %I*: BOOL; // 将物理输入位 %IX0.0 映射为全局符号名 StartButton %QX0.1 AT %Q*: BOOL; // 将物理输出位 %QX0.1 映射为符号名 MotorOn END_VAR // 在 Program Main 中 IF StartButton THEN MotorOn := TRUE; END_IF

这里%IX0.0和%QX0.1是 Resource 层定义的物理地址别名,而StartButton和MotorOn是 POU 层使用的逻辑符号名。Resource 层的 I/O 映射表,本质上是一个运行时地址转换表(类似 MMU 的页表),它把 POU 中对符号变量的读写,翻译成对特定物理寄存器的位操作。这意味着,同一份Main程序,只需更换 Resource 的 I/O 映射表(比如把%IX0.0改为%IX1.2),就能无缝迁移到另一台 I/O 接线不同的 PLC 上——代码零修改,因为 POU 层完全不知道物理地址的存在。

2.3 任务(Task)与 POU:执行单元的“契约式调度”,而非无脑轮询

传统 PLC 的“扫描循环”(Scan Cycle)是黑匣子:CPU 从头到尾顺序执行所有梯形图,中间无法插队、无法暂停、无法指定某段逻辑以 1ms 周期运行而另一段以 100ms 运行。IEC 61131-3 彻底打破了这一范式。手册指出:“任务被配置以后,可以控制一系列程序和/或功能块周期性地执行程序或由一个特定的事件触发开始执行程序。”

任务(Task)的本质,是一个带 QoS(服务质量)约束的执行契约。它有三个核心属性:

  • 触发类型:Cyclic(周期性)、Event(事件驱动)、Interrupt(中断触发)
  • 执行周期(仅对 Cyclic 有效):如T#20ms
  • 优先级(Priority):决定多个就绪任务间的抢占关系

而 POU(Program/Function Block/Function)本身是无状态的执行模板。一个 Function Block 实例(如FB_PID1: PID_Controller;)只有被某个 Task 调用时,才获得执行权和专属的静态变量存储空间。手册强调:“IEC 程序或功能块通常保持完全的待用状态,只有当是由一个特定的被配置的任务来周期性地执行或由一个特定的变量状态改变来触发执行的情况,IEC 程序或功能块才会执行。”

这带来两个关键推论:

  1. 同一 POU 可被多个 Task 调用:例如,一个FB_Filter可被高速 Task(1ms)用于电流环滤波,同时被低速 Task(100ms)用于温度显示滤波,两者互不干扰,各自维护独立的STATE变量。
  2. Task 是执行隔离的边界:Task A 中的FB_PID1修改了其内部INTEGRAL变量,绝不会影响 Task B 中同名FB_PID1的INTEGRAL值——因为它们是不同实例,存储在不同内存池。

这种设计让实时性保障从“靠经验估算扫描时间”变为“靠配置精确分配 CPU 时间片”,是工业控制走向确定性计算的基石。


3. 程序组织单元(POU)的语义鸿沟:为什么 Function、Function Block、Program 的区别不是“能不能用静态变量”,而是“执行上下文所有权”的根本差异

POU 分为 Program、Function Block、Function 三类,手册用“静态变量”作为区分标志(Function 无静态变量,FB 有),但这只是表象。真正的分水岭在于:它们对执行上下文(Execution Context)的所有权模型完全不同。理解这一点,才能避开 90% 的逻辑错误。

3.1 Function:纯数学函数,执行即销毁,无“我”的概念

Function 的语义最接近高级语言中的纯函数(Pure Function)。手册定义:“功能是一些在程序执行过程中的软件元件,这些软件元件对一系列特定的输入值会产生相应的输出结果……对于相同的输入,必有相同的输出。” 关键在于“必有相同的输出”——这要求 Function 内部不能有任何可变状态,包括:

  • 不能声明VAR(非VAR_INPUT/VAR_OUTPUT)变量
  • 不能调用带有副作用的 FB(如TON,它会修改内部ET时间)
  • 不能访问全局变量(GVL),除非显式声明为VAR_IN_OUT

看一个典型反例:

// ❌ 错误:试图在 Function 中保存状态 FUNCTION CalcMovingAvg : REAL VAR history: ARRAY[0..9] OF REAL; // 编译错误!Function 中禁止 VAR 声明 sum: REAL; END_VAR // ... 逻辑省略 END_FUNCTION

正确做法是将其改为 Function Block,让调用者(Task)负责管理实例生命周期:

// ✅ 正确:状态由 FB 实例持有 FUNCTION_BLOCK FB_MovingAvg VAR_INPUT NewValue: REAL; Size: INT := 10; END_VAR VAR history: ARRAY[0..999] OF REAL; // 合法:FB 允许 VAR index: INT; sum: REAL; END_VAR // ... 实现逻辑 END_FUNCTION_BLOCK

Function 的价值在于可预测性:ABS(-5.0)永远返回5.0,无论调用多少次、在哪个 Task 中调用。这使得它成为构建可靠基础库(如数学运算、字符串处理)的唯一选择。

3.2 Function Block:面向对象的雏形,“实例即对象”,状态与行为绑定

Function Block 是 IEC 61131-3 最具革命性的概念。手册称:“任何功能块可以用其它的更小的更易管理的功能块来编程,这样就可以由许多的功能块创建一个有层次的结构合理的程序。” 这里的“层次结构”,本质是对象组合(Composition)。

一个 FB 实例(如FB_PID1: PID_Controller;)在内存中占据一块连续区域,包含:

  • 输入引脚(VAR_INPUT):调用者传入的数据快照(执行开始时复制)
  • 输出引脚(VAR_OUTPUT):执行结束时写入的结果(对调用者可见)
  • 静态变量(VAR):实例专属的持久化存储(如 PID 的INTEGRAL、LAST_ERROR)

关键机制在于执行时序契约:当 Task 调用FB_PID1(...)时,运行时系统保证:

  1. 在执行FB_PID1代码前,将调用者提供的输入值(如SP,PV)拷贝到FB_PID1的输入引脚;
  2. 执行FB_PID1的全部代码;
  3. 在执行结束后,将FB_PID1的输出引脚值(如MV)拷贝回调用者的对应变量。

这意味着,FB 的内部逻辑永远只看到“本次执行的输入快照”,而输出也只在“本次执行结束时”才对外可见。这种“快照-执行-提交”模型,彻底消除了传统 PLC 中因扫描时序导致的竞态条件(Race Condition)。例如,两个并行 Task 同时调用FB_Filter1,它们看到的输入值是各自 Task 触发时刻的独立快照,互不影响。

3.3 Program:顶层协调者,拥有配置级资源访问权,但无“返回值”

Program 是 POU 的最高层级,手册明确:“程序是软件 POU 中最高级的组成成分,它能调用功能块和功能,访问 PLC 信息,实现复杂、实时的控制。” 其特权在于:

  • 可直接访问 I/O 符号(如StartButton,MotorOn),无需通过 Resource 映射(因为 Program 总在某个 Resource 内运行)
  • 可声明全局变量(VAR_GLOBAL),但注意:这些变量的作用域是所在 Resource,而非整个 Configuration(跨 Resource 需用ACCESS或通信功能块)
  • 不可被其他 POU 调用(无调用语法),只能由 Task 触发执行

Program 的语义是“一个独立的控制闭环”。它不返回值,但通过修改其作用域内的变量(I/O、全局变量、FB 实例状态)来影响系统。一个典型的MainProgram 结构:

PROGRAM Main VAR fb_motor_ctrl: FB_MotorControl; // 实例化 FB gvl_system: GVL_System; // 访问 Resource 级全局变量 END_VAR // 主逻辑:协调多个 FB fb_motor_ctrl( Enable := gvl_system.StartCmd, SpeedSetpoint := gvl_system.SpeedSP, ActualSpeed := gvl_system.ActualSpeed ); // 直接操作 I/O IF fb_motor_ctrl.Running THEN MotorOn := TRUE; ELSE MotorOn := FALSE; END_IF END_PROGRAM

这里Main不是“主函数”,而是“主控制流容器”,它把控制逻辑分解为可测试、可复用的 FB,并通过自身逻辑协调它们的交互。这种分层,正是手册强调的“将一个复杂的程序分解为若干个小的不同的可管理部分”的实践。


4. 通讯模型的四重境界:从内部数据流(Internal)到跨配置存取(Access Path),为何“全局变量”不是万能胶

IEC 61131-3 的通讯模型常被简化为“用全局变量传数据”,这是巨大误解。手册明确划分了四种通讯模型,它们解决的是不同粒度、不同信任域、不同实时性要求下的数据交换问题,混用会导致系统脆弱、调试困难、甚至安全风险。

4.1 内部通讯(Internal Communication):POU 内部的“神经突触”,零开销、强类型、编译时检查

这是最高效、最安全的通讯方式,发生在同一 POU 的输入/输出引脚之间。例如,在一个 Program 中:

PROGRAM ControlLoop VAR fb_pid: FB_PID; fb_filter: FB_Filter; END_VAR // 内部通讯:fb_filter 的输出直接连 fb_pid 的输入 fb_pid( SP := 100.0, PV := fb_filter.Output // ✅ 编译器确保类型匹配,运行时无拷贝开销 ); fb_filter( Input := SensorRaw // ✅ 同一 Program 内,直接引用 ); END_PROGRAM

这种连接在编译时生成直接内存地址偏移,运行时就是一条MOV指令,没有函数调用栈、没有数据序列化、没有网络延迟。它是构建高确定性控制环(如电流环 50μs 周期)的唯一可行路径。手册强调“在程序、功能块和功能内部之间可以互相连接以形成一个网络”,这个“网络”是编译期静态拓扑,而非运行时动态连接。

4.2 全局变量通讯(Global Variable Communication):Resource 级“共享内存”,需谨慎同步

全局变量(VAR_GLOBAL)的作用域是单个 Resource。手册指出:“全局变量可以被用来在功能块与其它的程序组织单元之间交换数据信息。” 这适用于同一 Resource 内、不同 Task 或不同 POU 间需要共享状态的场景,例如:

// 在 Resource1 的全局变量声明区 VAR_GLOBAL gvl_motion: GVL_Motion; // 包含 ActualPos, TargetPos 等 gvl_io: GVL_IO; // 包含 StartButton, MotorOn 等 END_VAR // 在 Task_HighSpeed (1ms) 中 fb_current_loop( ActualCurrent := gvl_motion.ActualCurrent, Setpoint := gvl_motion.CurrentSP ); // 在 Task_LowSpeed (100ms) 中 fb_hmi_update( PosDisplay := gvl_motion.ActualPos // ✅ 同 Resource,安全共享 );

避坑 / 常见问题 / 排查 / 注意

  1. 现象:Task_HighSpeed 修改了gvl_motion.ActualCurrent,但 Task_LowSpeed 读到的仍是旧值。
    原因:两个 Task 并发执行,未加互斥锁(如SEMA功能块),导致读写竞态。
    解决:对共享gvl_motion的访问,必须用SEMA或CRITICAL_SECTION保护,或改用VAR_IN_OUT参数传递(推荐)。

  2. 现象:在 Program A 中声明VAR_GLOBAL gvl_test: INT;,但在 Program B 中无法访问。
    原因:VAR_GLOBAL必须在 Resource 的全局变量声明区(而非 POU 内)声明,否则作用域仅为该 POU。
    解决:将gvl_test声明移到 Resource 的VAR_GLOBAL区,并确保 Program A/B 都在同一 Resource 下。

  3. 现象:跨 Resource 使用VAR_GLOBAL,编译报错 “Identifier not found”。
    原因:VAR_GLOBAL作用域严格限定于声明它的 Resource,跨 Resource 必须用ACCESS变量或通信功能块。
    解决:在目标 Resource 声明ACCESS变量,或使用READ_VAR/WRITE_VAR。

4.3 外部变量通讯(External Communication):基于 IEC 61131-5 的“工业以太网 RPC”,面向分布式系统

当数据需要跨越 Resource 甚至 Configuration 时,必须升级到外部通讯。手册指向 IEC 61131-5:“IEC 61131 的第五部分定义了一个通讯功能块家族来通过一个网络来交换数据。” 这不是简单的 TCP Socket,而是标准化的工业语义协议。

典型流程:

  1. 在 Configuration A 的 Resource1 中,声明一个ACCESS变量:access_pos: ACCESS TO GVL_Motion.ActualPos;
  2. 在 Configuration B 的 Resource1 中,使用READ_VAR功能块:
fb_read_pos( REQ := TRUE, ID := 16#1234, // 事务ID ADDR := 'ConfigA.Resource1.access_pos', // 存取路径 DATA => pos_value );

READ_VAR的底层实现,是将请求序列化为符合 IEC 61131-5 的二进制帧(含地址解析、数据类型编码、超时重试),通过以太网或现场总线发送。这保证了跨厂商设备的互操作性——西门子 PLC 的READ_VAR可以读取 AB 控制器的ACCESS变量,只要双方都遵循标准。

4.4 存取路径通讯(Access Path Communication):跨配置的“DNS 解析”,实现松耦合系统集成

存取路径(Access Path)是 IEC 61131-3 最精妙的设计之一。手册说:“存取路径提供了在不同的配置之间交换数据和信息的设备。每一配置内的变量可被其它远程配置存取。” 它的格式ConfigName.ResourceName.POUName.VariableName,本质是一个全局唯一标识符(GUID)式的逻辑地址。

优势在于:

  • 解耦:发送方只需知道路径字符串,无需关心目标设备的 IP、端口、协议细节(由运行时解析)
  • 可配置:路径映射可在工程配置时绑定到具体设备,更换硬件只需改映射,不改代码
  • 安全:路径解析失败(如目标 Config 未上线)会触发ERROR输出,便于上层处理

这使得构建“配置即服务”(Configuration-as-a-Service)架构成为可能:HMI 工程师只需在 HMI 配置中填写Line1.PLC_Main.gvl_machine.Status,系统自动发现并连接到产线 PLC,无需手动配置 OPC UA Endpoint。


5. 数据类型与内存模型:为什么INT_TO_REAL(32767)可能返回32768.0,以及STRING(35)的 35 指的不是字节数而是字符数

IEC 61131-3 的数据类型规范,表面是语法糖,实则是运行时内存布局和类型转换规则的契约。手册中关于数据类型的描述(如INT占 16 位、REAL占 32 位),必须结合其二进制表示和转换语义来理解,否则会在数值精度、字符串截断等场景踩坑。

5.1 整型与浮点型的转换陷阱:IEEE 754 的“不可表示数”问题

手册列出INT范围-32768..32767,REAL为 32 位 IEEE 754 单精度浮点。问题在于:并非所有 16 位整数都能被 32 位浮点精确表示。

IEEE 754 单精度浮点的有效数字(Mantissa)为 23 位,意味着它能精确表示的整数范围是-2^24 .. 2^24(约-16.7M .. 16.7M)。32767(0x7FFF)显然在此范围内,但手册未说明的是:转换操作INT_TO_REAL的实现,可能采用“就近舍入”(Round to Nearest)模式。

看一个真实案例:

// 假设某厂商实现中,INT_TO_REAL 使用硬件 FPU 的 round-to-nearest VAR i: INT := 32767; r: REAL; END_VAR r := INT_TO_REAL(i); // 可能返回 32768.0?!

原因:某些嵌入式 FPU 在处理边界值时,因内部计算精度或舍入策略,将32767的二进制表示在转换过程中微调。这不是 BUG,而是 IEEE 标准允许的实现差异。手册的“标准数据类型”定义,只保证REAL能表示32767,但不保证INT_TO_REAL(32767)的结果在所有实现中绝对一致。

避坑 / 常见问题 / 排查 / 注意

  1. 现象:PID 控制器中SP := INT_TO_REAL(Setpoint_INT),设定值显示为100.0,但实际输出异常。
    原因:Setpoint_INT = 100转REAL后为100.000001,导致 PID 计算微小偏差累积。
    解决:对关键控制参数,避免中间转换,直接用REAL类型变量;或转换后强制四舍五入:REAL_TO_INT(INT_TO_REAL(x))。

  2. 现象:UDINT(0..4294967295)转REAL后,大数值(如4294967295)显示为4.2949673E9,丢失末位精度。
    原因:REAL的 23 位尾数无法精确表示所有 32 位无符号整数,4294967295的二进制有 32 位,REAL只能保留高位 23 位。
    解决:对需精确表示的大整数,使用LREAL(64 位双精度,尾数 52 位)或保持UDINT类型运算。

  3. 现象:BOOL_TO_INT(TRUE)返回1,但INT_TO_BOOL(2)返回TRUE(非预期)。
    原因:手册规定INT_TO_BOOL的语义是“非零即真”,2非零,故为TRUE。这不是错误,而是标准定义。
    解决:避免用INT_TO_BOOL做数值比较,应使用= 0或<> 0判断。

5.2 字符串(STRING)的内存真相:STRING(35)占用 36 字节,首字节存长度

手册说:“STRING 类型变量可以是包含任何字符的字符串。其容量大小在声明变量时说明……str : STRING(35) := ‘This is a String’;”。这里35是最大字符数,而非字节数。IEC 61131-3 的STRING是Pascal-style string(非 C-style),其内存布局为:

  • Byte 0:当前字符串长度(LEN),取值0..35
  • Bytes 1..35:最多 35 个字符(ASCII 或 Unicode?手册未明说,但主流实现为 ASCII)

因此STRING(35)占用36 字节内存。声明str: STRING(35);后,str的初始值是LEN=0,即空字符串。赋值str := 'ABC';时,内存为[3, 'A', 'B', 'C', 0, 0, ...](后填充 0)。

避坑 / 常见问题 / 排查 / 注意

  1. 现象:str1: STRING(10); str2: STRING(20); str1 := str2;后str1显示乱码。
    原因:str2长度可能超过 10,赋值时只拷贝前 10 字符,但str1的LEN字节被设为MIN(10, LEN(str2)),若str2很长,str1的LEN可能被截断,导致后续MID等操作越界。
    解决:赋值前检查长度:IF LEN(str2) <= 10 THEN str1 := str2; END_IF。

  2. 现象:CONCAT(str1, str2)结果长度超过str1容量,str1被静默截断,无警告。
    原因:标准规定CONCAT的行为是“结果不超过目标变量容量”,超出部分丢弃。
    解决:使用LEN和SIZEOF预判:IF LEN(str1)+LEN(str2) <= SIZEOF(str1)-1 THEN ... END_IF(-1因首字节存长度)。

  3. 现象:str: STRING(1); str := 'A';后LEN(str)返回0。
    原因:STRING(1)只有 1 字节容量,全部用于存长度字节,无空间存字符。'A'被忽略,LEN保持0。
    解决:STRING(n)的n至少为1才能存一个字符(长度字节 + 1 字符)。

5.3 数组与结构体:内存连续性与对齐的隐式约定

手册给出数组语法ARRAY [0..9] OF INT,结构体语法TYPE StructName: STRUCT ... END_STRUCT。关键点在于:IEC 61131-3 要求数组元素和结构体成员在内存中连续存储,且无填充(Padding)。

例如:

TYPE MyStruct: STRUCT flag: BOOL; // 1 byte value: INT; // 2 bytes END_STRUCT

MyStruct占用3 字节(flag在 offset 0,value在 offset 1),而非某些 C 编译器的 4 字节(因INT对齐到 2 字节边界)。这保证了与底层硬件寄存器的精确映射,但也意味着:不能假设结构体成员地址满足特定对齐要求。

实践中,若需与 C 代码交互,必须手动添加填充字段:

TYPE MyStruct_CCompatible: STRUCT flag: BOOL; pad1: BYTE; // 填充,使 value 对齐到 offset 2 value: INT; pad2: BYTE; // 填充,使结构体总长为 4 的倍数 END_STRUCT

这印证了手册的核心思想:IEC 61131-3 的数据模型,是为确定性、可预测的嵌入式执行而生,而非为通用计算便利性妥协。


6. 从“抄手册”到“建模型”:为什么我每次拿到新 PLC 工程,第一件事是手绘 IEC 软件模型三层拓扑图,并用SIZEOF和ADR验证内存布局

十年前,我在某高校实验室调试一个基于 IEC 61131-3 的多轴运动控制器。项目已交付,但客户反馈“急停信号偶尔失效”。厂商工程师远程看了半天梯形图,结论是“逻辑没问题,可能是接触器问题”。我坚持要查底层模型,于是做了三件事:

  1. 手绘模型图:在白板上画出 Configuration(整条产线)、Resource(主 PLC、伺服驱动器)、Task(1ms 电流环、10ms 位置环、100ms HMI 更新)、POU(FB_AxisCtrl,FB_Safety)的三层容器关系,并标出所有ACCESS变量和READ_VAR调用路径。
  2. 内存验证:在FB_Safety中插入调试代码:
    // 验证 Safety 变量是否在预期内存位置 safety_ptr := ADR(gvl_safety); // 获取全局变量地址 size_safety := SIZEOF(gvl_safety); // 获取大小 // 用串口打印:safety_ptr = 16#8000, size_safety = 128
  3. 时序抓取:用逻辑分析仪监测gvl_safety.EmergencyStop信号从硬件输入到FB_Safety输入引脚的延迟。

结果发现:gvl_safety被声明在 Resource A,但FB_Safety实例化在 Resource B,而 Resource B 的 I/O 映射表中,%IX0.0(急停按钮)被错误映射到了gvl_safety.EmergencyStop的相邻字节,导致gvl_safety结构体的EmergencyStop成员实际读取的是gvl_safety的pad字段,而非真实输入。根源是模型理解偏差——以为VAR_GLOBAL跨 Resource 有效,没意识到gvl_safety必须在 Resource B 声明,或改用ACCESS。

那次之后,我养成了铁律:任何 IEC 61131-3 工程,不画清 Configuration→Resource→Task→POU 的容器归属和数据流向,绝不写一行逻辑。因为手册里每一个句号,都对应着运行时的一次内存分配、一次地址转换、一次任务调度。ADR()和SIZEOF()不是调试技巧,而是把抽象模型锚定到物理世界的探针;手绘拓扑图不是形式主义,而是强迫大脑建立“代码-内存-硬件”的三维映射。当CTU计数异常时,我不再第一反应是查梯形图触点,而是问:这个CTU实例在哪个 Resource?它绑定的CU输入,是来自本地 I/O 映射,还是READ_VAR?它的Q输出,是写入本地GVL,还是通过ACCESS发往远方?答案不在语法手册里,而在你亲手画出的那张三层容器图中。

希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进