跳到主要内容

PDO 输入输出

通过 slave.PDO() 的映射视图与元素视图、slave.SlavePdo() 的整帧读写访问过程映像。​

PDO 就是过程映像里的一段数据,内核每周期自动收发。映射成结构体后,视图直接钉在过程映像上 —— 0 拷贝:映射本身不复制数据、不为整帧另建托管缓冲区,写下的下个周期发出;但 JNA 结构体的字段是 Java 侧缓存,读前必须 read()、改完必须 write(),否则读到的是旧值、写的不生效。

PDO 周期与邮箱的关系

邮箱(SDO/CoE/FoE 等)的读取附带在 PDO 帧中,不占用额外周期时间。邮箱响应延迟约 2-3 个 PDO 周期。

出站邮箱(如 SDO 写入)由系统根据 PDO 周期自适应调度,无需用户干预。

使用案例​

日常使用​

① 映射到类(0 拷贝,持续用):进 OP 后映射一次,拿到钉在过程映像上的 JNA Structure 视图,之后当变量读写。

import com.sun.jna.Structure;
import com.sun.jna.Structure.FieldOrder;

@FieldOrder({"statusWord", "actualPosition"}) // 字段顺序与 PDO 逐字节一致
public static class InPdo extends Structure {
public short statusWord;
public int actualPosition;
public InPdo() { super(ALIGN_NONE); } // ALIGN_NONE = Pack 1,必须
}

@FieldOrder({"controlWord", "targetPosition"})
public static class OutPdo extends Structure {
public short controlWord;
public int targetPosition;
public OutPdo() { super(ALIGN_NONE); }
}
import com.darra.ethercat.master.EtherCATMaster;
import com.darra.ethercat.data.EcState;
import com.darra.ethercat.slave.Slave;

EtherCATMaster.BuildResult r = EtherCATMaster.create()
.setENI("C:/EtherCAT/MyProject.deni") // DENI 一行加载
.build();
if (!r.success) throw new Exception(r.message);
EtherCATMaster master = r.master;
master.setState(EcState.OP); // 进 OP

Slave slave = master.getSlave(1);

InPdo inputs = slave.PDO().inputsMapping(InPdo.class); // 映射一次(进 OP 后)
OutPdo outputs = slave.PDO().outputsMapping(OutPdo.class);

inputs.read(); // 刷字段 = 当前周期输入
short statusWord = inputs.statusWord;
outputs.controlWord = 0x000F; // 改字段
outputs.targetPosition = inputs.actualPosition;
outputs.write(); // 写回 IOmap:下个周期发出

② 单次读取(一次性读):不要结构体,只取一份当前值。

// 单点:按偏移取元素视图,再在元素上读
int sw = slave.PDO().In().Get(0).AsUInt16(); // 偏移 0 的 u16(StatusWord)
int pos = slave.PDO().In().Get(2).AsInt32(); // 偏移 2 的 i32(ActualPosition)

// 整段:一份完整输入帧快照,同一拍一致,拷出来不会跟着总线变
byte[] frame = slave.SlavePdo().Inputs();
String name = new String(frame, 6, 16, java.nio.charset.StandardCharsets.US_ASCII)
.replace("\0", "").trim(); // 字符串字段:偏移 6 起 16 字节
System.out.printf("status=0x%04X pos=%d name=%s%n", sw, pos, name);

周期变化​

映射取一次就够,同一份映射放进你自己的周期循环直接用:

while (running) {
inputs.read(); // 本周期输入
outputs.controlWord = (inputs.statusWord & 0x0004) != 0 ? (short) 0x000F : (short) 0x0006;
outputs.targetPosition = inputs.actualPosition;
outputs.write(); // 写回 IOmap:本周期发出
Thread.sleep(1); // 你自己的节奏,例如 1 ms
}

内核每周期搬运过程数据:循环里读到的永远是当前周期、写下的下个周期发出,不需要任何额外调用,也不要每拍重新映射(close() / 重新进 OP 后旧视图才作废,重进 OP 时重取一次即可)。

结构体映射​

public <T extends Structure> T inputsMapping(Class<T> type)     // slave.PDO(),输入(TxPDO)
public <T extends Structure> T outputsMapping(Class<T> type) // slave.PDO(),输出(RxPDO)

进 OP 后把 packed Structure overlay 到过程映像:视图钉在内核 IOmap 上(0 拷贝,不是快照)。

PDO 与 SlavePdo 上是同一套实现,BindPdoStruct(type, isInput) 是 SlavePdo 上的兼容入口。

@FieldOrder({"statusWord", "actualPosition", "actualVelocity"})
public static class ServoInput extends Structure {
public short statusWord;
public int actualPosition;
public int actualVelocity;
public ServoInput() { super(ALIGN_NONE); } // Pack 1
}

@FieldOrder({"controlWord", "targetPosition", "targetVelocity"})
public static class ServoOutput extends Structure {
public short controlWord;
public int targetPosition;
public int targetVelocity;
public ServoOutput() { super(ALIGN_NONE); }
}

ServoInput inputs = slave.PDO().inputsMapping(ServoInput.class); // 只映射一次
ServoOutput outputs = slave.PDO().outputsMapping(ServoOutput.class);

inputs.read(); // 要最新值就 read()
int pos = inputs.actualPosition;
outputs.controlWord = 0x000F;
outputs.targetPosition = pos + 1000;
outputs.write(); // 改完必须 write(),否则不生效
  • 结构体必须 extends Structure + 构造器 super(ALIGN_NONE)(= Pack 1)+ @FieldOrder 显式列字段。
  • size() 不得超过该方向字节数(slave.Ibytes() / slave.Obytes()),否则抛 IllegalStateException。
  • 字段是 Java 侧缓存:读前必须 inputs.read()、改完必须 outputs.write(),否则读的是旧值、写的不生效。
  • 对齐契约不做运行时校验(只有 size() 越界会抛)。
  • 字段错位只会静默读到脏数据。
  • 输入映射挂在只读指针上,对输入视图调 write() / Pointer.setXxx 抛 UnsupportedOperationException。
  • 视图不必每拍重新映射;close() / 重新进 OP 后旧视图作废,必须重取。

元素视图​

slave.PDO().In() / slave.PDO().Out() 是按字节偏移取单个 PDO 项的路径:Get(index) 取到第 index 个字节处的元素视图 PDODataItem,再用元素上的定宽访问器按类型读写。它和结构体映射读的是同一份过程映像 —— 同一份数据的第三条路径,差别是不必预先声明整段布局,只需按偏移取用到的少数几个字段。

Get(index) 返回的是句柄不是拷贝:元素只持五个字段(指针、字节偏移、方向、边界、所属 PDO),自己不存任何数据字节。同一偏移重复 Get 返回同一个元素对象(按偏移缓存)。

每次访问都是一次独立的过程映像访问:读按次取 generation 整帧快照,写是一次版本化事务。

public PDOArrayInstance In()                   // slave.PDO(),输入(TxPDO),只读
public PDOArrayInstance Out() // slave.PDO(),输出(RxPDO),可写

public PDODataItem Get(int index) // PDOArrayInstance,index = 字节偏移
public void Set(int index, byte value) // PDOArrayInstance,等价于 Get(index).Content(value)
public int Length() // PDOArrayInstance,本方向过程映像字节数

public PDODataItem(Pointer basePtr, int offset, boolean isInput) // 裸指针逃生口,宽度上限回落协议上限
public PDODataItem(Pointer basePtr, int offset, boolean isInput, int maxIomapSize) // 同上,宽度上限显式传入

读取​

In / Out 均可读。多字节成员一律小端(EtherCAT 线序)。

public byte    Content()                     // 单字节原始值,与 AsInt8() / AsUInt8() 完全同义
public byte AsInt8() // 有符号 8 位
public byte AsUInt8() // 无符号 8 位;Java 的 byte 有符号,取无符号值请自行 & 0xFF
public short AsInt16()
public int AsUInt16() // 返回 int 以表示无符号
public int AsInt32()
public long AsUInt32() // 返回 long 以表示无符号
public long AsInt64()
public long AsUInt64() // 返回 long 的原始 64 位;无符号解释用 Long 的 Unsigned 系列
public float AsFloat()
public double AsDouble()
public boolean AsBool() // bit0 语义,见下
public boolean GetBit(int bitIndex) // bitIndex 0-7,0 = LSB
public Object AsValue(EcDataType type) // 按 PDO 映射类型取装箱值
public BaseData AsBaseData(int dataType) // 该类型长度的字节副本(小端),不是 live 视图

一次读 = 一份独立快照:同一表达式里的两次访问不保证同拍;单次多字节访问内部不会撕裂。

AsBaseData 拿到的也是这个方向元素读的一次 generation 字节副本。

byte    raw  = slave.PDO().In().Get(0).Content();     // 偏移 0 的原始字节
short sw = slave.PDO().In().Get(0).AsInt16(); // 偏移 0..1 的 i16(小端)
int pos = slave.PDO().In().Get(2).AsInt32(); // 偏移 2..5 的 i32
float vel = slave.PDO().In().Get(6).AsFloat(); // 偏移 6..9 的 f32
boolean rdy = slave.PDO().In().Get(0).GetBit(2); // 偏移 0 的 bit2
boolean en = slave.PDO().In().Get(10).AsBool(); // 偏移 10 的 bit0

写入​

只有 Out(RxPDO,主站→从站)可写。

public void Content(byte value)                    // 单字节
public void AsInt8(byte value)
public void AsUInt8(byte value)
public void AsInt16(short value)
public void AsUInt16(int value)
public void AsInt32(int value)
public void AsUInt32(long value)
public void AsInt64(long value)
public void AsUInt64(long value)
public void AsFloat(float value)
public void AsDouble(double value)
public void AsBool(boolean value) // 写整个字节
public void SetBit(int bitIndex, boolean value) // 只改该位,同字节其余位不动
public void WriteValue(EcDataType type, Object value)
public boolean WriteFromBaseData(BaseData data) // 整段字节写回,成功 true,失败 false
slave.PDO().Out().Get(6).Content((byte) 0x11);        // 偏移 6 的整字节
slave.PDO().Out().Get(0).AsInt16((short) 0x000F); // 偏移 0..1 的 i16(小端)
slave.PDO().Out().Get(2).AsInt32(pos + 1000); // 偏移 2..5 的 i32
slave.PDO().Out().Get(0).SetBit(3, true); // 只置偏移 0 的 bit3

写入不直接落过程映像。每次 setter 调用 = 一次版本化事务:Begin 复制整帧并签发 token,帧上贴字节,Commit 原子替换整帧。

没有结构体路径那样要调用方另调的 write() 步骤 —— 提交在 setter 内完成。但它不是"改完即发":写下的值在提交那一刻随整帧原子替换,与主站发帧不产生竞态。

取舍:整帧热路径仍用 outputsMapping(0 拷贝)。元素视图只适合单点或少量字段的改动,不适合放进 1 ms 周期(原因见下)。

语义与边界​

  • 字节序:多字节成员一律小端(EtherCAT 线序),显式按小端组装/拆分字节,不依赖 JVM 或平台的默认字节序。
  • AsBool() 是 bit0 语义:只有最低位为 1 才是 true —— 0x01 → true,0x02 → false,0x80 → false。不是"非 0 即真";要按整字节判真假,请读 Content() 自行比较(本类刻意不提供第二个布尔口径)。
  • AsBool(boolean) 写整个字节:true → 0x01、false → 0x00,非规范字节不保留(写 false 即整字节清零)。
  • SetBit 是位域读改写:只改 bitIndex 那一位,同字节其余位保持原值。
  • bitIndex 取 0-7(0 = LSB),越界抛 IllegalArgumentException。
  • 写 In 抛 UnsupportedOperationException:I 区由主站每周期从从站覆盖,写它静默无效,故类型上直接拒绝。元素上的写成员与 Set 都会抛;WriteValue 例外 —— 它先转换值,值本身非法时先抛 IllegalArgumentException。要写过程数据一律用 Out。
  • 元素写的事务失败抛 IllegalStateException:未进 OP、冲突或忙重试耗尽、5 ms 预算耗尽都会走到这里。它绝不静默丢写,调用方要自己决定重试还是放弃(Set 同样抛)。
  • 异常分层(三类刻意分开报,便于定位是选错字节、选错类型还是状态未就绪):
    • index 落在 [0, Length()) 之外 → IndexOutOfBoundsException;
    • 偏移 + 宽度超出本方向过程映像 → IllegalArgumentException;
    • 过程映像不可用(未进 OP / 未映射 / 指针为空)→ IllegalStateException。
  • Length() 是本方向过程映像的字节数;未进 OP 或该从站无该方向 PDO 时为 0,此时任何索引都越界。
  • 视图生命周期:In() / Out() 每次调用都重新解析一次过程映像指针,指针或长度变化时重建实例。
  • 旧实例仍可用,但边界不会更新:元素带着所属 PDO,读按次取快照,所以旧实例不再有"指向旧指针"的读风险。但长度边界是 In() / Out() 当时捕获的 —— 跨 close() / 重新进 OP 仍必须重新取用。
  • AsValue / WriteValue 只支持 11 种定宽类型:BOOLEAN / INTEGER8·16·32·64 / UNSIGNED8·16·32·64 / REAL32 / REAL64;其余(Bit1..Bit8、Integer24·40·48·56、字符串、Domain)没有 Java 对标量类型,一律抛 IllegalArgumentException。WriteValue 越界即抛,不静默截断。
  • 裸指针逃生口:new PDODataItem(...) 接受调用方自备的缓冲基址。元素直接对该内存按偏移读写,不走快照、不走版本化提交。宽度上限由传入的 maxIomapSize 决定(省略则回落协议上限)。只有这块内存是调用方自持、不与主站发帧共享时才可用;正常用法一律经 In() / Out()(长度与方向由 native GetIO 填入)。
别把元素视图放进 1 ms 周期热路径

Get(index) 本身稳态不分配(同一偏移复用同一视图对象)。但每次标量读取 = 一份整帧快照,长度 = 本方向过程映像字节数。

写还要加一次事务。原生调用次数是可数的:读 3 次(长度探测 + 整帧拷贝 + 解码),写 4 次(编码 + Begin 的探测与拷贝 + Commit)。

事务冲突会重试:每轮再 +3 次,最多 3 轮 ⇒ 最坏 10 次。

整帧 / 批量 / 周期热路径请用结构体映射(inputsMapping / outputsMapping)或 copyInputPdo 一次取整帧再本地解析。

按偏移读写单个值​

知道偏移、只要几个字段,就用元素视图(见 元素视图):按偏移取到元素,再在元素上按类型读写。

int sw  = slave.PDO().In().Get(0).AsUInt16();        // 读偏移 0 的 u16
int pos = slave.PDO().In().Get(2).AsInt32(); // 读偏移 2 的 i32
slave.PDO().Out().Get(0).AsInt16((short) 0x000F); // 写偏移 0..1 的 i16,下个周期发出
slave.PDO().Out().Get(0).SetBit(3, true); // 只置偏移 0 的 bit3

无符号值一律挑 AsUInt*:AsUInt16() 返回 int、AsUInt32() 返回 long,线值 0xFFFF 读出来就是 65535,不会变成负数。 AsInt16() / AsInt32() 是有符号的,只在值确实落在窄范围内时用。

整帧、批量、周期热路径不走元素视图,走下面这些:

byte[]  in  = slave.SlavePdo().Inputs();                                  // 输入整帧快照
boolean ok = slave.SlavePdo().Outputs(new byte[]{0x0F, 0x00}); // 输出整帧提交(长度须等于输出映射)
int n = slave.PDO().WriteOutputDataDirect(new byte[]{0x0F, 0x00}); // 写输出帧,返回写入字节数
int m = slave.PDO().ReadInputDataDirect(buf); // 读输入帧,返回读出字节数
boolean ok2 = slave.PDO().WriteFromTyped(new byte[]{0x0F, 0x00}, 0); // 按偏移贴一段字节

ByteBuffer[] bufs = slave.PDO().BatchRead(new int[]{0, 2}, new int[]{2, 4}); // 一次读多个偏移

copyInputPdo / copyOutputPdo / writeOutputPdo 是同一件事的完整形态:除了数据本身,还回传实际长度和需要长度。 周期热路径仍用结构体映射(inputsMapping / outputsMapping,0 拷贝),取舍见 元素视图 末尾。

完整示例​

import com.darra.ethercat.master.EtherCATMaster;
import com.darra.ethercat.data.EcState;
import com.darra.ethercat.slave.Slave;
import com.sun.jna.Structure;
import com.sun.jna.Structure.FieldOrder;

@FieldOrder({"statusWord", "actualPosition"})
public static class InPdo extends Structure {
public short statusWord;
public int actualPosition;
public InPdo() { super(ALIGN_NONE); }
}

@FieldOrder({"controlWord", "targetPosition"})
public static class OutPdo extends Structure {
public short controlWord;
public int targetPosition;
public OutPdo() { super(ALIGN_NONE); }
}

EtherCATMaster.BuildResult r = EtherCATMaster.create()
.setENI("C:/EtherCAT/MyProject.deni") // DENI 一行加载
.build();
if (!r.success) throw new Exception(r.message);

try (EtherCATMaster master = r.master) {
master.setState(EcState.OP); // 进 OP

Slave slave = master.getSlave(1);

// 映射一次:视图钉在过程映像上(0 拷贝)
InPdo inputs = slave.PDO().inputsMapping(InPdo.class);
OutPdo outputs = slave.PDO().outputsMapping(OutPdo.class);

inputs.read(); // 读:当前周期
outputs.controlWord = 0x000F; // 写:下个周期发出
outputs.targetPosition = inputs.actualPosition + 1000;
outputs.write();
System.out.printf("status=0x%04X pos=%d%n", inputs.statusWord & 0xFFFF, inputs.actualPosition);

// 单次读取:整帧快照(同一拍一致)+ 单点元素视图
byte[] frame = slave.SlavePdo().Inputs();
System.out.printf("快照 %d 字节, 单点读 pos=%d%n",
frame.length, slave.PDO().In().Get(2).AsInt32());

System.in.read();
}