在结构化文本(ST)编程中,DEREF 函数是访问指针所指向内存地址内容的核心操作。它不是语法糖,而是 IEC 61131-3 标准明确定义的唯一标准方式,用于安全、明确地执行“指针解引用”。许多初学者误以为 pPointer^ 或 pPointer[0] 可用,或直接写 Value := pPointer;,这些写法在 ST 中全部非法,会导致编译错误或运行时未定义行为。本文将手把手说明如何正确使用 DEREF 实现指针取值,覆盖从基础声明、地址获取、类型匹配、边界检查到典型故障排查的全流程。
一、理解指针本质:不是“变量”,而是“地址容器”
在 ST 中,指针不是普通变量,而是一个存储内存地址的特殊数据类型。它的值本身是某个变量在 PLC 内存中的物理/逻辑地址(如 #16#A00428),而非该地址处的数据。因此:
pPointer的值 = 地址(例如ADR(MyInt))DEREF(pPointer)的值 = 该地址中存放的实际数据(例如MyInt当前的整数值)
这与 C 语言中 int* p; *p 的语义完全一致,但语法强制要求显式调用 DEREF(),杜绝隐式解引用带来的歧义和安全隐患。
二、正确声明指针变量:必须指定目标类型
ST 中所有指针都必须带类型限定,格式为 POINTER TO <数据类型>。没有“通用指针”或“void*”概念。
-
声明一个指向 INT 的指针
pToInt : POINTER TO INT; -
声明一个指向结构体的指针
TYPE MyStruct : STRUCT State : BOOL; Count : DINT; END_STRUCT VAR stInst : MyStruct; pToStruct : POINTER TO MyStruct; END_VAR -
声明一个指向数组首元素的指针(常用)
arrData : ARRAY[0..9] OF REAL; pToRealArray : POINTER TO REAL; // 指向 REAL 类型,非 ARRAY 类型
⚠️ 错误示例(编译不通过):
pBad : POINTER; // ❌ 缺少 TO 子句,语法错误
pWrong : POINTER TO; // ❌ TO 后无类型,语法错误
三、获取有效地址:只能用 ADR(),禁用间接计算
指针必须指向一个已声明且生命周期有效的变量。获取地址的唯一合法方式是 ADR(变量名) 函数。
-
对普通变量取地址
MyValue : INT := 123; pToInt := ADR(MyValue); // ✅ 正确:pToInt 现在指向 MyValue 的内存位置 -
对数组元素取地址
arr[0..4] OF BYTE; pToByte := ADR(arr[2]); // ✅ 指向第3个元素(索引2) -
对结构体成员取地址
pToState := ADR(stInst.State); // ✅ 指向结构体内布尔成员
❌ 绝对禁止的操作:
p := 16#A00428;—— 直接赋数值地址(PLC 地址空间受保护,且不可移植)p := ADR(arr) + 2;—— 指针算术(ST 不支持+/-运算符作用于指针)p := ADR(tempVar);——tempVar是临时变量(如 FB 内部未保留的 TEMP),地址在扫描周期结束即失效
✅ 替代方案(安全偏移):用 OFFSET 函数配合 ADR()
pToThird := OFFSET(ADR(arr[0]), SIZEOF(BYTE) * 2); // ✅ 等效于 ADR(arr[2])
四、执行取值:DEREF() 是唯一合法方式
语法:DEREF(<指针变量>)
返回值类型 = 指针声明时 TO 后的类型。
-
基本取值
MyValue : INT := 42; pToInt : POINTER TO INT; Result : INT; pToInt := ADR(MyValue); Result := DEREF(pToInt); // ✅ Result = 42 -
取结构体成员值
stInst.State := TRUE; pToState := ADR(stInst.State); bFlag := DEREF(pToState); // ✅ bFlag = TRUE -
取数组元素值(通过指针偏移)
arrData : ARRAY[0..4] OF REAL := [1.1, 2.2, 3.3, 4.4, 5.5]; pToReal : POINTER TO REAL; pToReal := ADR(arrData[0]); val1 := DEREF(pToReal); // ✅ = 1.1 pToReal := OFFSET(pToReal, SIZEOF(REAL)); // 移动到下一个 REAL val2 := DEREF(pToReal); // ✅ = 2.2
⚠️ 常见错误写法(全部报错):
| 错误写法 | 原因 |
|----------|------|
| Value := pToInt^; | ^ 是 Pascal 风格,ST 不识别 |
| Value := pToInt[0]; | ST 中 [] 仅用于数组索引,指针不支持 |
| Value := pToInt; | 类型不匹配:POINTER TO INT ≠ INT |
| Value := DEREF(ADR(MyValue)); | ❌ ADR() 返回值不可直接传入 DEREF();必须先赋给指针变量 |
五、类型安全:DEREF 的类型检查规则
DEREF() 在编译期强制校验指针类型与目标变量类型是否一致。这是 ST 安全性的核心保障。
假设:
iVar : INT := 100;
rVar : REAL := 3.14;
pInt : POINTER TO INT;
pReal : POINTER TO REAL;
| 表达式 | 是否允许 | 原因 |
|---|---|---|
DEREF(pInt) |
✅ | 类型匹配 |
DEREF(pReal) |
✅ | 类型匹配 |
DEREF(ADR(iVar)) |
❌ 编译错误 | ADR(iVar) 是 POINTER TO INT 类型,但未声明为变量,无法作为 DEREF 参数 |
pInt := ADR(rVar); |
❌ 编译错误 | ADR(rVar) 是 POINTER TO REAL,不能赋给 POINTER TO INT |
pInt := ADR(iVar); Value := DEREF(pInt); |
✅ | 全流程类型一致 |
💡 提示:若需跨类型访问(如把 REAL 当作 4 字节 INT 解析),必须用 UNION 或 BYTE 数组 + ADR() + DEREF() 组合,并确保硬件字节序一致。
六、空指针防护:运行时必须检查有效性
ST 中指针初始值为 0(空指针)。对空指针调用 DEREF() 会导致运行时错误(如 PLC 停机、诊断缓冲区报 Access violation)。
必须添加空指针检测:
IF pToInt <> 0 THEN
Value := DEREF(pToInt);
ELSE
Value := 0; // 或触发报警、设默认值
END_IF
更严谨写法(兼容所有指针类型):
IF NOT ISVALID(pToInt) THEN
// 处理无效指针
ELSE
Value := DEREF(pToInt);
END_IF
注:
ISVALID()是部分厂商扩展函数(如 CODESYS、TwinCAT),非 IEC 标准,但被广泛支持。若 PLC 不支持,用pToInt <> 0是通用替代。
七、典型应用场景与完整代码示例
场景 1:动态选择多个传感器数据源
VAR
Sensor1, Sensor2, Sensor3 : REAL;
pActiveSensor : POINTER TO REAL;
ActiveIndex : INT; // 1, 2 or 3
CurrentReading : REAL;
END_VAR
CASE ActiveIndex OF
1: pActiveSensor := ADR(Sensor1);
2: pActiveSensor := ADR(Sensor2);
3: pActiveSensor := ADR(Sensor3);
ELSE pActiveSensor := 0;
END_CASE
IF pActiveSensor <> 0 THEN
CurrentReading := DEREF(pActiveSensor);
ELSE
CurrentReading := 0.0;
// 触发“无效传感器索引”报警
END_IF
场景 2:遍历字符串缓冲区(BYTE 数组)
VAR
Buffer : ARRAY[0..255] OF BYTE;
pBuf : POINTER TO BYTE;
i : INT;
Len : INT := 12;
END_VAR
pBuf := ADR(Buffer[0]);
FOR i := 0 TO Len - 1 DO
IF pBuf <> 0 THEN
// 将每个字节转为 ASCII 字符处理
ProcessByte(DEREF(pBuf));
pBuf := OFFSET(pBuf, SIZEOF(BYTE));
END_IF
END_FOR
场景 3:FB 内部缓存指针(带初始化防护)
FUNCTION_BLOCK DataLogger
VAR
pData : POINTER TO DINT;
bInitialized : BOOL;
LastValue : DINT;
END_VAR
METHOD Initialize : BOOL
VAR_INPUT
pSource : POINTER TO DINT;
END_VAR
IF pSource <> 0 THEN
pData := pSource;
bInitialized := TRUE;
ELSE
bInitialized := FALSE;
END_IF
Initialize := bInitialized;
METHOD ReadValue : DINT
IF bInitialized THEN
ReadValue := DEREF(pData);
ELSE
ReadValue := 0;
END_IF
调用方式:
logger : DataLogger;
myData : DINT := 999;
// 初始化时传入有效地址
logger.Initialize(ADR(myData));
// 后续任意时刻读取
current := logger.ReadValue(); // = 999
八、调试技巧与常见故障定位
| 现象 | 最可能原因 | 排查步骤 |
|---|---|---|
| 编译失败:“Expected ‘)’” 或 “Invalid operand type” | DEREF() 参数不是指针变量,或类型不匹配 |
检查 DEREF() 内是否为已声明的 POINTER TO X 变量;确认 ADR() 赋值语句存在且无拼写错误 |
运行时 PLC 停机,诊断缓冲区显示 Deref on NULL pointer |
指针为 0 时未检查直接 DEREF |
在所有 DEREF 前加 IF pX <> 0 THEN;用在线监控查看指针变量实时值 |
DEREF() 返回值始终为 0 或随机值 |
指针指向了 TEMP 变量或已释放内存 | 确认 ADR() 作用对象是 VAR、VAR_GLOBAL 或 FB 的 VAR RETAIN 成员;避免对 FOR 循环内声明的临时变量取地址 |
值正确但后续修改不生效(如 DEREF(p) := 123 不起作用) |
使用了只读地址(如常量、函数返回值)或硬件映射区写保护 | DEREF(p) 左值赋值需目标地址可写;检查变量声明是否含 CONST 或 AT %Q* 等只写区 |
✅ 快速验证指针有效性方法:
- 在线监控模式下,右键点击指针变量 → “Go to Address” → 查看该地址处实际存储的数值是否与预期变量值一致。
- 用
SIZEOF(DEREF(p))验证类型大小(如SIZEOF(DEREF(pToInt))应返回2)。
九、性能与资源注意事项
DEREF()是单周期指令,无额外开销,性能等同直接访问变量。- 指针变量本身占用内存:32 位 PLC 占 4 字节,64 位占 8 字节。
- 频繁使用
OFFSET()+DEREF()遍历大数组时,比直接用arr[i]略慢(因多一次地址计算),但差异通常小于 0.1μs,可忽略。 - 避免在每周期都重复调用
ADR()(如DEREF(ADR(x))放在循环内)——应提前赋值给指针变量,复用地址。
Value := DEREF(pPointer); 这行代码背后,是 ST 对内存安全的刚性约束与工程可靠性的底层承诺。它拒绝一切隐式、猜测和侥幸——地址必须显式获取,类型必须严格匹配,空值必须主动防御。写好这一行,不是学会一个函数,而是真正开始以 PLC 工程师的思维控制物理世界。

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