在结构化文本(ST)编程中,ADR() 是获取变量地址的核心指令,pPointer := ADR(Variable); 这一行代码看似简单,却是实现动态数据访问、数组遍历、函数块参数传递、硬件寄存器映射等高级功能的基石。它不依赖于具体PLC品牌(如西门子SCL、倍福TwinCAT、施耐德IEC 61131-3 ST、罗克韦尔Structured Text),因为 ADR 是IEC 61131-3标准明确定义的地址运算符(Address Operator),其行为具有跨平台一致性。下面以“可直接上手执行”的方式,分步讲清:它到底在做什么、为什么必须这样写、常见错误如何排除、哪些场景离不开它,以及如何安全扩展使用。
一、先理解本质:什么是“地址”?什么又是“指针”?
在自动化控制中,所有变量(如 MotorSpeed : INT := 1200;)都存储在PLC内存的某个物理位置,例如起始地址为 DB1.DBW4(西门子)或 %MW100(施耐德)。这个位置编号就是地址——相当于快递收货的“门牌号”。
而指针(Pointer)本身是一个特殊变量,它的值不是数据(如1200),而是另一个变量的地址(如 DB1.DBW4 对应的数值 1004)。指针变量的数据类型必须声明为指针类型,例如:
pSpeed : POINTER TO INT;
这行声明表示:pSpeed 是一个能存放 INT 类型变量地址的容器,它自己占4字节(32位系统)或8字节(64位系统),但里面存的不是速度值,是“速度值住在哪”。
ADR(Variable) 就是把 Variable 的门牌号算出来,并交给指针变量。
所以 pPointer := ADR(Variable); 的真实含义是:
把 Variable 在内存中的精确起始地址读取出来,并存进 pPointer 这个指针变量里。
注意:ADR 不是函数调用,不执行任何计算,也不改变 Variable 的值;它只是编译器在编译阶段就确定好的“地址常量提取操作”。
二、正确书写 ADR() 的5个硬性规则(缺一不可)
以下每条都对应实际工程中高频报错原因:
-
Variable必须是已声明、具名、非临时的变量
❌ 错误示例:p := ADR(123); // 字面量无地址 p := ADR(MotorSpeed + 10); // 表达式结果无固定地址 p := ADR(ARRAY[0..9] OF INT); // 类型声明不是变量✅ 正确写法:
VAR MotorSpeed : INT := 1200; StatusFlags : ARRAY[0..7] OF BOOL; ConfigData : STRUCT MaxCurrent : REAL; TimeoutMS : TIME; END_STRUCT; END_VAR pSpeed := ADR(MotorSpeed); // ✔️ 单个变量 pFlags := ADR(StatusFlags); // ✔️ 整个数组(取首元素地址) pConfig := ADR(ConfigData); // ✔️ 结构体(取首成员地址) -
指针变量类型必须与目标变量类型严格匹配(TO 后类型一致)
POINTER TO INT只能指向INT、DINT等整数类型变量(因对齐和访问长度需兼容);不能指向REAL或STRING。
若需通用访问,必须用VOID POINTER(IEC 61131-3 标准支持),但需配合类型转换(见后文“安全扩展”部分)。 -
ADR()内部不能加括号嵌套或运算符
❌ 错误:p := ADR((MotorSpeed)); // 多余括号 → 编译失败 p := ADR(MotorSpeed[0]); // 数组索引 → 指向单个元素才合法,但写法错误✅ 正确(若要取数组第3个元素地址):
p := ADR(StatusFlags[2]); // 下标从0开始,取第3个BOOL地址 -
全局/局部作用域不影响
ADR,但影响指针有效性
若Variable是 FB(功能块)的VAR_INPUT,且未在调用时传入实参,则ADR(Variable)返回空地址(NULL),解引用会触发运行时错误。务必确保变量已初始化并驻留在有效内存区。 -
常量(CONST)不能取地址
IEC 61131-3 明确规定:ADR()仅适用于变量(VAR, VAR_GLOBAL, VAR_IN_OUT 等),不适用于CONST或VAR_ACCESS声明的符号。
❌CONST MaxVal : INT := 100; p := ADR(MaxVal);→ 编译报错。
三、三类典型应用场景(附可复制代码)
场景1:动态遍历数组(替代 FOR 循环硬编码)
问题:需对 SensorValues : ARRAY[0..15] OF REAL 的前 n 个元素求平均,但 n 由HMI输入实时变化。
传统做法需循环索引;用指针可消除索引变量,直接按地址偏移访问:
VAR
SensorValues : ARRAY[0..15] OF REAL := [0.0, 1.2, 2.3, ...];
nSamples : INT := 8;
pStart : POINTER TO REAL;
sum : REAL := 0.0;
i : INT;
END_VAR
// 1. 获取数组首地址
pStart := ADR(SensorValues);
// 2. 按字节偏移遍历(REAL 占 4 字节)
FOR i := 0 TO nSamples - 1 DO
sum := sum + pStart^[i]; // pStart^[i] = *(pStart + i * SIZEOF(REAL))
END_FOR
Average := sum / nSamples;
注:
pStart^[i]是标准解引用语法,等价于 C 语言*(pStart + i)。编译器自动按TO REAL类型计算偏移量(i * 4字节)。
场景2:将多个同类型变量打包传入函数块
问题:需将 Temp1, Temp2, Temp3(均为 REAL)一次性送入自定义滤波FB,避免写3个独立输入引脚。
// 自定义FB声明(FilterFB)
FUNCTION_BLOCK FilterFB
VAR_INPUT
pSource : POINTER TO REAL; // 接收首地址
nPoints : INT; // 数据点数
END_VAR
VAR
buffer : ARRAY[0..99] OF REAL;
i : INT;
END_VAR
// 内部逻辑:从 pSource 读取 nPoints 个 REAL 并滤波
FOR i := 0 TO nPoints - 1 DO
buffer[i] := pSource^[i] * 0.9 + (1.0 - 0.9) * buffer[i];
END_FOR
// 调用处
VAR
Temp1, Temp2, Temp3 : REAL;
temps : ARRAY[0..2] OF REAL;
pTemps : POINTER TO REAL;
END_VAR
// 构建临时数组并取地址
temps[0] := Temp1; temps[1] := Temp2; temps[2] := Temp3;
pTemps := ADR(temps);
FilterFB(pSource := pTemps, nPoints := 3);
场景3:访问硬件I/O映射地址(底层控制必需)
当PLC需要直接读写特定物理地址(如模拟量模块的配置寄存器),且该地址未被符号化时:
// 假设某模块基地址为 0x80000000(32位地址)
VAR
pHWReg : POINTER TO DWORD;
configValue : DWORD;
END_VAR
pHWReg := ADR(DWORD#16#80000000); // ⚠️ 注意:此写法仅在支持地址字面量的平台有效(如TwinCAT)
// 更通用写法(推荐):
// 声明一个 dummy 变量强制分配到目标地址(通过 AT 属性)
VAR_GLOBAL
HW_CONFIG_REG AT %MD0 : DWORD; // 强制映射到 %MD0(具体地址依硬件手册)
END_VAR
pHWReg := ADR(HW_CONFIG_REG); // 安全获取
// 读取配置
configValue := pHWReg^;
// 写入使能位(bit 0)
pHWReg^ := pHWReg^ OR 16#1;
四、必须规避的5个高危错误(附诊断方法)
| 错误现象 | 根本原因 | 快速诊断法 |
|---|---|---|
| 编译报错 “ADR operand not allowed” | 对 CONST、表达式、未声明变量使用 ADR |
检查 ADR() 内是否为纯变量名,右键变量名→查看声明位置 |
| 运行时崩溃或随机值 | 指针类型与目标类型不匹配(如 POINTER TO INT 指向 REAL) |
查看指针声明的 TO 类型,对比目标变量类型;启用编译器“类型严格检查” |
p^ 返回0或旧值 |
指针未初始化(p := 0;)或 ADR() 目标变量未分配内存(如FB未实例化) |
在线监控指针变量值:若为 0 或 NULL,说明 ADR 未成功赋值 |
| 数组越界无提示 | p^[i] 中 i 超出数组范围,但编译器不检查 |
手动添加边界判断:IF i < SIZEOF(Array)/SIZEOF(Element) THEN ... END_IF |
| 多任务中数据错乱 | 指针指向全局变量,但被多个任务并发读写 | 改用 VAR_IN_OUT 传参,或对共享内存加 CRITICAL_SECTION |
五、安全扩展:VOID POINTER 与类型转换
当需一个指针处理多种类型(如同时读 INT 和 REAL),用 VOID POINTER:
VAR
pVoid : POINTER TO VOID;
myInt : INT := 42;
myReal : REAL := 3.14;
END_VAR
pVoid := ADR(myInt);
// 转换为 INT 指针后解引用
INT_PTR : POINTER TO INT := pVoid;
valueInt := INT_PTR^; // 42
// 转换为 REAL 指针(⚠️ 确保内存内容确实是 REAL 格式!)
REAL_PTR : POINTER TO REAL := pVoid;
valueReal := REAL_PTR^; // 危险!此时读的是 INT 的二进制解释为 REAL → 得到乱码
✅ 安全用法:仅用于相同内存布局的类型间转换(如 INT ↔ WORD ↔ BYTE 数组),或配合 MOVE 指令做字节级搬运。
六、不同平台语法微调对照表
| 平台 | ADR 是否支持 |
指针声明示例 | 解引用符号 | 备注 |
|---|---|---|---|---|
| TwinCAT 3 (Beckhoff) | ✔️ 全面支持 | p : POINTER TO LREAL; |
p^ |
支持 ADR() 直接作用于 AT 地址变量 |
| 博途 SCL (Siemens) | ✔️ | pSpeed : P#INT; |
pSpeed^ |
P# 是西门子指针关键字,ADR() 用法完全一致 |
| Unity Pro (Schneider) | ✔️ | p : POINTER TO DINT; |
p^ |
需在“项目设置”启用指针功能 |
| Logix Designer (Rockwell) | ❌ 不支持 ADR() |
使用 @ 符号(如 @MyTag) |
@MyTag |
非IEC标准,此处不展开 |
所有符合 IEC 61131-3 的平台,
pPointer := ADR(Variable);的语法和语义完全一致。
七、性能真相:ADR() 几乎零开销
ADR() 在编译期完成,生成的机器码仅为一条地址加载指令(如 x86 的 LEA EAX, [Variable]),不消耗扫描周期时间。指针解引用 p^ 也仅比直接访问多1个内存寻址周期,在现代PLC中可忽略不计。真正影响性能的是不当的指针链式访问(如 p1^.[0].field.p2^.[5]^),应尽量扁平化。
八、调试技巧:3步定位指针问题
- 静态检查:编译前,将鼠标悬停在
ADR(Variable)上,IDE 应显示Variable的绝对地址(如DB1.DBX0.0); - 在线监控:运行时观察指针变量值,确认是否为非零有效地址(如
16#8000A000); - 交叉验证:用
MOVE指令将p^的值复制到中间变量,再与原始变量值比对是否一致。
pPointer := ADR(Variable); 不是炫技语法,而是让ST具备C语言级内存操控能力的关键开关。掌握它,意味着你能跳出“变量名直连”的思维定式,进入基于地址的灵活架构设计——从优化数据搬运,到对接第三方驱动,再到实现自定义通信协议,皆以此为起点。

暂无评论,快来抢沙发吧!