跳到主要内容

PDO 输入输出

通过 slave.PDO 访问过程映像。​

PDO 就是过程映像里的一段数据,内核每周期自动收发。映射成结构体后,读写成员就是直接读写过程映像本身 —— 0 拷贝:不用拷贝、不用刷新,读到的永远是当前周期,写下的下个周期发出。

PDO 周期与邮箱的关系

邮箱通信(SDO/CoE/FoE 等)自动附带在 PDO 周期中,不占用额外帧。邮箱响应延迟约 2 个 PDO 周期。

结构体契约
  • 必须 [StructLayout(LayoutKind.Sequential, Pack = 1)],字段顺序与 PDO 映射逐字节一致;漏写 Pack = 1 会带对齐填充、字段全部错位(能编译,静默读到脏数据)。
  • 不得大于对应方向的字节数(slave.InputsByteCount / slave.OutputsByteCount)。
    • InputsMapping<T>() / OutputsMapping<T>() 超尺寸抛 InvalidOperationException。
    • Slice 变体按偏移越界抛 ArgumentOutOfRangeException。
    • 带 onChanged 的重载映射时不校验尺寸,不符要到后续读取时才暴露。
    • 小于合法,按偏移覆盖。
  • 只能放定长值类型:含引用字段(string / 数组 / 委托)抛 InvalidOperationException(带 onChanged 的重载不做这项校验),字符串用整帧 Inputs 解。
  • bool 字段不是 bit0(0 拷贝 ref 映射路):结构体字段由 C# 自己解释,走语言原生"非 0 即真"。字节 0x02 / 0x80 这种非规范值,ref 映射里读 true,元素视图 AsBool 读 false。要 bit0 就用 slave.PDO.In[i].AsBool, 或者按位 GetBit / SetBit。例外:ValueSnapshot 快照路会把 bool 字节按 bit0 归一后再解释,与 AsBool 同口径。
  • 映射一次长期有效;Close / 换代后旧 ref 作废,重进 OP 时重取。
    • 换代没有通知,旧 ref 看不出自己已失效。取 ref 时把 slave.ProcessImageGeneration 一并存下,用之前比对:值变了就是换过代,旧 ref 必须丢弃、重新映射。
    • 该值单调递增(初值 1,永不返回 0),是纯托管计数器 —— 读取零锁、零分配,任意线程可用;Close 与重新应用 DC / ENI 配置时 +1。协议栈在 PreOp→SafeOp 内部重算 FMMU 时不递增它,所以重进 OP 一律主动重取。

使用案例​

日常使用​

① 映射到结构体(0 拷贝,持续用):进 OP 后映射一次,拿到钉在过程映像上的 ref,之后当变量读写。

using System.Runtime.InteropServices;
using DarraEtherCAT_Master;

[StructLayout(LayoutKind.Sequential, Pack = 1)] // Pack = 1 必须;字段顺序与 PDO 逐字节一致
public struct InPdo { public ushort StatusWord; public int ActualPosition; }

[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct OutPdo { public ushort ControlWord; public int TargetPosition; }

using var master = new DarraEtherCAT()
.SetENI(@"C:\EtherCAT\MyProject.deni") // DENI 一行加载
.Build()
.Master;
master.SetState(EcState.OP, out _); // 进 OP

var slave = master.Slaves[0];

ref readonly InPdo input = ref slave.PDO.InputsMapping<InPdo>(); // 映射一次(进 OP 后)
ref OutPdo output = ref slave.PDO.OutputsMapping<OutPdo>();

output.ControlWord = 0x000F; // 写:下个周期发出
output.TargetPosition = 10000;
Console.WriteLine($"status=0x{input.StatusWord:X4} pos={input.ActualPosition}"); // 读:当前周期

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

// 单点:按字节偏移取元素视图(索引越界抛 IndexOutOfRangeException)
int pos = slave.PDO.In[2].AsInt32; // 偏移 2 起 4 字节
ushort sw = slave.PDO.In[0].AsUInt16; // 偏移 0 起 2 字节(StatusWord)

// 整段:一份完整输入帧快照,同一拍一致,拷出来不会跟着总线变
byte[] frame = slave.PDO.Inputs;
string name = System.Text.Encoding.ASCII.GetString(frame, 6, 16).TrimEnd('\0'); // 字符串字段
Console.WriteLine($"status=0x{frame[0] | (frame[1] << 8):X4} name={name}");

周期变化​

映射取一次就够,同一份映射放进你自己的周期循环(或循环里的调度点)直接用:

using System.Threading;

while (running)
{
output.ControlWord = (input.StatusWord & 0x0004) != 0 ? (ushort)0x000F : (ushort)0x0006;
output.TargetPosition = input.ActualPosition;
Thread.Sleep(1); // 你自己的节奏,例如 1 ms
}

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

结构体映射(零拷贝)​

InputsByteCount / OutputsByteCount​

当前从站输入 / 输出过程映像的字节数,用来在映射前核对结构体大小。

public int InputsByteCount  { get; }
public int OutputsByteCount { get; }

返回值:

  • int — 输入 / 输出过程映像字节数;未进 OP 或未映射时为 0

说明: 这两个属性声明在 Slave 上(slave.InputsByteCount),不在 slave.PDO 上。数值有约 50 ms 节流,状态切换后短期内可能仍是上一拍的缓存值。

示例:

if (ValueCodec.StructOverlaySize<InPdo>() > slave.InputsByteCount)
throw new InvalidOperationException("结构体大于输入 PDO");

InputsMapping<T>()​

把输入过程映像(TxPDO)映射成钉在内核 IOmap 上的只读结构体引用,映射一次后读字段即读当前周期值(0 拷贝,不是快照)。

public ref readonly T InputsMapping<T>() where T : struct

返回值:

  • ref readonly T — 钉在过程映像上的只读引用,任意线程可读

说明:

  • 进 OP 后调用。
  • I 区由主站每周期覆盖,写它静默无效;要写过程数据用 OutputsMapping<T>()。
  • 过程映像不可用(未进 OP)抛 InvalidOperationException。
  • Slave 已释放(Close() 后)抛 ObjectDisposedException。
  • 结构体大于输入 PDO 抛 InvalidOperationException。

示例:

ref readonly InPdo input = ref slave.PDO.InputsMapping<InPdo>();
ushort status = input.StatusWord; // 当前周期值

InputsMapping<T>(onChanged)​

映射成结构体实例并挂数据变化回调。只想拿 ref 时不带 onChanged 即可,无需额外绑定 API。

public Slave.InputPdoInstance<T> InputsMapping<T>(Action<Slave.PdoStructChangedEventArgs<T>> onChanged) where T : struct
public Slave.InputPdoInstance<T> InputsMapping<T>(Action onChanged) where T : struct

返回值:

  • InputPdoInstance<T> — .Value 是钉在过程映像上的 ref readonly T(0 拷贝);.ValueSnapshot 是当前值的一份托管快照(不会自己变)

回调参数:

public sealed class PdoStructChangedEventArgs<T> : EventArgs where T : struct
{
public ushort SlaveIndex { get; } // 从站索引
public bool IsInput { get; } // 是否为输入 PDO
public T Previous { get; } // 变化前的值
public T Current { get; } // 变化后的值
public DateTime Timestamp { get; } // 时间戳
}

说明: 内部按约 1 ms 后台轮询比较完整帧,不在 PDO 周期内触发;仅结构体值变化时才调用回调。

示例:

var input = slave.PDO.InputsMapping<InPdo>(e =>
Console.WriteLine($"位置变化: {e.Previous.ActualPosition} -> {e.Current.ActualPosition}"));

int pos = input.Value.ActualPosition; // 0 拷贝,当前周期
InPdo snap = input.ValueSnapshot; // 托管快照,不会跟着总线变

InputsSliceMapping<T>(int offset)​

从输入过程映像的指定字节偏移处映射只读结构体引用,供 MDP 等按模块切段的设备使用。

public ref readonly T InputsSliceMapping<T>(int offset) where T : struct

参数:

  • offset (int) — 相对本从站输入过程映像起始的字节偏移

返回值:

  • ref readonly T — 钉在该偏移处的只读引用(0 拷贝)

说明: offset 为负、或 offset + ValueCodec.StructOverlaySize<T>() 越界抛 ArgumentOutOfRangeException(尺寸按托管布局计算,含 bool/char 的结构体不要用 Marshal.SizeOf 代替)。

相关结构:

using System.Runtime.InteropServices;

[StructLayout(LayoutKind.Sequential, Pack = 1)] // Pack = 1 必须;漏写会有对齐填充、字段错位
public struct Module1In // 模块 1:占输入映像偏移 0-7(共 8 字节)
{
public ushort StatusWord; // 偏移 0
public ushort ErrorCode; // 偏移 2
public int Value; // 偏移 4
}

[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct Module2In // 模块 2:输入映像从偏移 8 起
{
public ushort StatusWord; // 偏移 8
public ushort ErrorCode; // 偏移 10
public int Value; // 偏移 12
}

[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct Module2Out // 模块 2:输出映像从偏移 8 起
{
public ushort ControlWord; // 偏移 8
public int Setpoint; // 偏移 10
}

每段结构体的字节宽度必须与对应模块的 PDO 长度一致;这里模块 1 占 8 字节,模块 2 才从偏移 8 开始。

示例:

// MDP 设备:模块 1 从偏移 0 开始,模块 2 从偏移 8 开始
ref readonly Module1In mod1 = ref slave.PDO.InputsSliceMapping<Module1In>(0);
ref readonly Module2In mod2 = ref slave.PDO.InputsSliceMapping<Module2In>(8);

InputsSliceMapping<T>(int offset, onChanged)​

从输入过程映像的指定偏移处映射结构体实例并挂数据变化回调。

public Slave.InputPdoInstance<T> InputsSliceMapping<T>(int offset, Action<Slave.PdoStructChangedEventArgs<T>> onChanged) where T : struct
public Slave.InputPdoInstance<T> InputsSliceMapping<T>(int offset, Action onChanged) where T : struct

参数:

  • offset (int) — 相对本从站输入过程映像起始的字节偏移
  • onChanged — 数据变化回调,形参同上

示例:

var mod2 = slave.PDO.InputsSliceMapping<Module2In>(8, e =>
Console.WriteLine($"模块 2 数据变化: {e.Previous} -> {e.Current}"));

ref readonly Module2In m2 = ref mod2.Value; // 0 拷贝

OutputsMapping<T>()​

把输出过程映像(RxPDO)映射成结构体引用,改字段即写总线。

public ref T OutputsMapping<T>() where T : struct

返回值:

  • ref T — 可写引用,钉在过程映像上(0 拷贝);改字段后下个周期发往从站

说明: 结构体大于输出 PDO 抛 InvalidOperationException;过程映像不可用或 Slave 已释放同 InputsMapping<T>()。

示例:

ref OutPdo output = ref slave.PDO.OutputsMapping<OutPdo>();
output.ControlWord = 0x000F; // 下个周期发出

OutputsSliceMapping<T>(int offset)​

从输出过程映像的指定字节偏移处映射可写结构体引用。

public ref T OutputsSliceMapping<T>(int offset) where T : struct

参数:

  • offset (int) — 相对本从站输出过程映像起始的字节偏移

示例:

ref Module2Out mod2 = ref slave.PDO.OutputsSliceMapping<Module2Out>(8);
mod2.Setpoint = 1500;

直接访问(不映射也能用)​

下面这组调用与结构体映射彼此独立:不映射可以直接用,映射了也常并用(结构体放不下的字段用它解)。

In[i] / Out[i]​

public PDOArrayInstance In  { get; }   // TxPDO(从站→主站):只读
public PDOArrayInstance Out { get; } // RxPDO(主站→从站):可读可写

按字节偏移索引过程数据元素:In[i] 读输入,Out[i] 读写输出,i 相对本方向过程映像起点。

返回值:

  • PDOArrayInstance — 按字节索引的过程映像视图;Length 是本方向过程映像字节数,未进 OP / 未映射时为 0(此时任何索引都抛 IndexOutOfRangeException)

说明:

  • In 是 TxPDO,主站每周期覆盖,只读:对 In 赋值或写它的元素成员一律抛 InvalidOperationException。
  • Out 是 RxPDO,每次成员赋值走一次版本化事务(Copy → patch → commit,冲突有界重试),成功的写下个周期发出。
  • 提交失败抛 InvalidOperationException。
  • 元素按偏移缓存复用,读取每次取一份当前帧快照后按偏移解析 —— 与 slave.PDO.Inputs 同一份快照语义,不是 InputsMapping<T>() 的 0 拷贝钉页引用。
  • 索引越界抛 IndexOutOfRangeException。
  • 选错字节 / 选错类型的越界分两道守卫,异常类型不同:
    • 第一道按元素视图的边界(创建时的 Length,即带约 50 ms 节流的方向字节数)判:偏移加宽度超出抛 ArgumentOutOfRangeException —— 与索引越界刻意分开,便于区分选错字节与选错类型。
    • 第二道按本次实际拿到的帧长判(读取取到的快照 / 写事务租到的帧):与元素边界不一致抛 ArgumentException。缓存过期、或过程映像换代缩小后才可能命中。
  • GetBit / SetBit 的 bitIndex 不在 0-7、AsValue / WriteValue 的类型不受支持时抛 ArgumentOutOfRangeException。
  • SetBit 先判方向再验索引:对 In 调用(无论索引是否合法)抛的是 InvalidOperationException;只有 Out 方向才可能因 bitIndex 越界抛 ArgumentOutOfRangeException。
  • Slave 已释放抛 ObjectDisposedException。

相关结构:

public sealed class PDODataItem        // slave.PDO.In[i] / Out[i] 返回的元素视图
{
public byte Content { get; set; } // 原始字节(无符号 8 位)
public sbyte AsInt8 { get; set; } // 有符号 8 位
public byte AsUInt8 { get; set; } // 与 Content 同义同字节
public short AsInt16 { get; set; } // 以下多字节均为小端(EtherCAT 线序)
public ushort AsUInt16 { get; set; }
public int AsInt32 { get; set; }
public uint AsUInt32 { get; set; }
public long AsInt64 { get; set; }
public ulong AsUInt64 { get; set; }
public float AsFloat { get; set; }
public double AsDouble { get; set; }
public bool AsBool { get; set; } // 读 bit0 语义;写整字节 1 / 0(不是改单个位)

public bool GetBit(int bitIndex); // bitIndex 0-7,0 = LSB
public void SetBit(int bitIndex, bool value); // 仅 Out;同字节其余位不动
public object AsValue(EcDataType type); // 按映射类型读 boxed 值
public void WriteValue(EcDataType type, object value); // 按映射类型写
public BaseData AsBaseData(EcDataType type); // 位串 / 宽整数 / 变长类型的出口
public bool WriteFromBaseData(BaseData data); // 整段写回;失败返回 false
public static implicit operator byte(PDODataItem item); // byte x = slave.PDO.In[0]; ≡ .Content
}

AsBool 读只认最低位:字节 0x02 与 0x80 都读 false。要按整字节 0 / 非 0 判真假,读 Content 自行比较。

反过来说,0 拷贝 ref 映射里的 bool 字段是非 0 即真(见上面「结构体契约」)。同一个非规范字节, ref 映射字段和 AsBool 读出来不一样;ValueSnapshot 快照已按 bit0 归一,两者一致。

GetBit(i) / SetBit(i, value) 是同一个字节的按位入口:i 取 0-7,0 = 最低位;GetBit 读该位(In / Out 都可读),SetBit 只改该位、同字节其余位不动。

AsBool 的 setter 写的是整个字节

slave.PDO.Out[i].AsBool = value 把该字节整体写成 1 或 0,不是只改 bit0 —— slave.PDO.Out[0].AsBool = true 会把同一字节的 bit1–7 一起清掉。 同一字节里既有布尔位又有其它标志位时,用 SetBit(i, value):它只覆盖目标位,其余位保持不变。

AsValue / WriteValue 只支持与 CLR 定宽类型一一对应的数据类型:Boolean、Integer8/16/32/64、Unsigned8/16/32/64、Real32/Real64。

位串、24/40/48/56 位整数、字符串、Domain 一律抛 ArgumentOutOfRangeException;要按字节进出宽类型和位串,用 AsBaseData()(见下)。宽类型经 AsBaseData() 之后也能取值:Content 按映射类型解码(24 位整数回 int / uint,40/48/56 位回 long / ulong),不必自己拼字节。

示例:

short sw = slave.PDO.In[0].AsInt16;        // 读:当前周期
slave.PDO.Out[2].AsInt16 = 123; // 写:版本化局部提交,下个周期发出
slave.PDO.Out[0].SetBit(0, true); // 只置 bit0,同字节其余位不动
bool flag = slave.PDO.In[3].AsBool; // bit0 语义
for (int i = 0; i < slave.PDO.Out.Length; i++)
slave.PDO.Out[i].SetBit(0, false); // 遍历整个输出映像
byte same = slave.PDO.In[0]; // 隐式转换:等同 .Content

TryWriteOutputBatch(patches)​

public bool TryWriteOutputBatch(IReadOnlyList<PdoOutputPatch> patches)

把多个输出字节段放进一次版本化事务提交:一次成功只产生一次 native commit。多个字段要同时更新时用它,别逐个 Out[i].X = ...(那是每字段一次事务)。

参数:

  • patches (IReadOnlyList<PdoOutputPatch>) — 要提交的字节段列表,偏移相对输出过程映像起点

返回值:

  • bool — 是否提交成功;false 表示 3 轮内、5 ms 预算内没抢到(冲突 / 忙),这次调用不写入

相关结构:

public readonly struct PdoOutputPatch
{
public PdoOutputPatch(int offset, ReadOnlyMemory<byte> data); // offset 为负抛 ArgumentOutOfRangeException
public int Offset { get; } // 相对本从站输出过程映像起点的字节偏移
public ReadOnlyMemory<byte> Data { get; } // 该段要写入的字节
}

说明:

  • 提交前每段都拷成自有副本,重试期间不受调用方后续改动影响。
  • patches 为 null 抛 ArgumentNullException;某段 Data 为空、或偏移 / 长度越界抛 ArgumentException。
  • 空列表返回 true。
  • 同一方法在 slave 上也有一份(slave.TryWriteOutputBatch),语义相同。

示例:

var patches = new[]
{
new PdoOutputPatch(0, new byte[] { 0x0F, 0x00 }), // ControlWord
new PdoOutputPatch(2, BitConverter.GetBytes(10000)), // TargetPosition
};
bool ok = slave.PDO.TryWriteOutputBatch(patches); // 一次事务,一次 native commit

AsBaseData() / WriteFromBaseData()​

public BaseData AsBaseData(EcDataType type);
public bool WriteFromBaseData(BaseData data);

宽类型出口:AsValue / WriteValue 只覆盖定宽标量,位串、变长字段用这一对进出。

参数:

  • type (EcDataType) — 该位置的映射类型。字节宽度取自 native 数据类型表:Integer40 / Unsigned40 = 5 字节,Integer48 / Unsigned48 / TimeOfDay / TimeDifference = 6 字节,Integer56 / Unsigned56 = 7 字节,其余定宽类型按位宽向上取整;字符串与 Domain 等变长类型、以及未登记的类型按 1 字节处理
  • data (BaseData) — 要写回的整段原始字节

返回值:

  • BaseData — 按 type 宽度从当前位置拷出的原始字节包装;GetRawData() 取这一段字节,WriteFromBaseData 原样写回
  • bool — 是否写成功;失败返回 false(不抛),In 方向固定返回 false

说明:

  • AsBaseData 在偏移 / 宽度越界时抛 ArgumentOutOfRangeException。
  • 两道越界检查之间过程映像可能换代、窗口缩小;后一道按实际帧长校验的守卫抛的是 ArgumentException(不是 ArgumentOutOfRangeException)。
  • type 的宽度非法(超出协议上限)抛 ArgumentException;帧里切下来的字节数与 BaseData 对该类型的宽度不一致时同样抛 ArgumentException —— 宽度对不上不静默降级。
  • 快照不可用(未进 OP)抛 InvalidOperationException。
  • WriteFromBaseData 走输出 PDO 的版本化提交,下个周期发出;写入失败以 false 返回,不抛异常。
  • 40/48/56 位整数没有 CLR 对标量,装箱类型统一取更宽的一族:Integer40 / Integer48 / Integer56 读写 long,Unsigned40 / Unsigned48 / Unsigned56 读写 ulong(40 位已放不进 int / uint,与 Integer64 / Unsigned64 同族,也与 Rust 侧 I64 / U64 同一规则)。
  • BaseData.Content 对 40/48/56 位整数给出真实值(不再抛 NotSupportedException):AsBaseData(EcDataType.Integer48).Content 直接返回解码后的 long,GetRawData() 仍给 6 字节原始内容(40/56 位为 5 / 7 字节)。读出来的值可以直接写回同类型。

示例:

using DarraEtherCAT_Master.Utils;

BaseData wide = slave.PDO.In[0].AsBaseData(EcDataType.Integer48); // 按 native 表宽度取 6 字节
byte[] raw = wide.GetRawData(); // 原生 6 字节内容
bool ok = slave.PDO.Out[0].WriteFromBaseData(wide); // 失败返回 false(不抛)

Inputs / Outputs​

按字节访问过程映像整帧,用于结构体放不下的字段(字符串、变长数据)。

public byte[] Inputs  { get; }        // 输入整帧:每次访问返回调用方独占的快照
public byte[] Outputs { get; set; } // 输出整帧:get 返回快照;set 提交完整帧

返回值:

  • byte[] — 快照,不是 0 拷贝:同一拍一致,拷出来不跟着总线变

说明:

  • 这两个属性声明在 slave.PDO 上(slave.PDO.Inputs),不在 slave 上。
  • 每次访问都新分配一个 byte[];高频循环里改用 slave.CopyInputPdo(span, out copied, out required) / slave.CopyOutputPdo(...) 拷进自备缓冲(无托管分配,返回 PdoIoStatus:只有 Success 算成功,BufferTooSmall 会带出所需帧长)。
  • 整帧提交用 slave.WriteOutputPdo(span, out _, out _)。
  • Outputs 的 setter 长度必须与 OutputsByteCount 匹配,否则抛 InvalidOperationException。
  • 两个 getter 在 Slave 已释放(Close() 后)时抛 ObjectDisposedException。
  • 拿不到完整帧时抛 InvalidOperationException(不返回空数组)。

示例:

byte[] frame = slave.PDO.Inputs;
string name = System.Text.Encoding.ASCII.GetString(frame, 6, 16).TrimEnd('\0');

PdoConsistentReader​

public sealed class PdoConsistentReader
{
public PdoConsistentReader(ushort masterIndex); // 本从站所属主站号
public ulong CycleCount { get; } // 已完成的周期序号(内核每周期 +1)
public T Read<T>(Func<ulong, T> reader, int maxRetries = 8);
public T ReadInput<T>(Slave slave, int offset, int maxRetries = 8) where T : struct;
public int ReadInt32(Slave slave, int offset, int maxRetries = 8);
public short ReadInt16(Slave slave, int offset, int maxRetries = 8);
public byte ReadByte(Slave slave, int offset, int maxRetries = 8);
}

在一个逻辑帧里读多个字段时用它:读前读后各取一次周期序号,两次相同才算这一拍读完;跨了周期就整体重试。只读一个字段用 In[i] 就够。

说明:

  • 一致性靠周期序号包夹检测,读的是托管快照;不钉页、不返回原生指针。
  • 与每次取 slave.PDO.Inputs 相比,不产生整帧数组分配。
  • 传进来的从站不属于构造时绑定的那个主站抛 ArgumentException(slave 为 null 抛 ArgumentNullException)。
  • 重试 maxRetries 次(默认 8)仍拿不到一致快照抛 InvalidOperationException:缩短读的 lambda,或减少一次读的字段数。
  • 带 IntPtr 的旧重载已废弃,调用即抛 NotSupportedException —— 原生 PDO 指针不能跨代保持有效。

示例:

var reader = new PdoConsistentReader(masterIndex: 1);   // 本从站所属主站号

InPdo frame = reader.Read(_ => new InPdo
{
StatusWord = slave.PDO.In[0].AsUInt16,
ActualPosition = slave.PDO.In[2].AsInt32,
}); // 两个字段要么同属一拍,要么整体重试