过程数据
通过 device.ProcessData 读写过程数据。
过程数据就是设备与控制器每周期互换的那一段字节。映射成结构体后,读写字段就是直接读写过程映像本身 —— 0 拷贝:不用拷贝、不用刷新,读到的永远是当前周期,写下的下个周期发出。
方向:写 Q(设备 → 控制器),读 I(控制器 → 设备)。I 区只读 —— 控制器每周期整段覆盖,写它静默无效。
总线周期由内核完成,不必自写周期循环;任意线程可同时用。
GSDML 里那 20 字节的 Input(设备 → 控制器)就是 SDK 的 Q;GSDML 里那 32 字节的 Output(控制器 → 设备)就是 SDK 的 I。
两个词在配置文件和 SDK 里是反的,别按字面理解。SDK 一律按数据流的方向说:写 Q、读 I。
- 必须
[StructLayout(LayoutKind.Sequential, Pack = 1)]。偏移按声明顺序紧排自算,不是Marshal的布局(Marshal里bool是 4 字节BOOL,映射按 1 字节算)。- 一个属性都不标:抛
ArgumentException,消息里说"必须标注[StructLayout(LayoutKind.Sequential, Pack=1)]"。 - 只写
Sequential不写Pack:Pack为 0,抛ArgumentException,消息里说"必须Pack=1(无对齐填充)"。
- 一个属性都不标:抛
- 可用字段:数值(
sbyte/byte/short/ushort/int/uint/long/ulong/float/double)、bool(1 字节)、枚举(按底层类型宽度)、定长byte[](必须标[MarshalAs(UnmanagedType.ByValArray, SizeConst = N)])、嵌套结构体(递归,同样Sequential Pack = 1)。 string字段抛NotSupportedException(改用定长byte[]);char字段抛NotSupportedException(改用ushort)。结构体含引用字段(string/ 类 / 数组 / 委托 / 接口)抛InvalidOperationException("结构体含引用字段,不能映射到过程映像")。- 结构体大小必须 1 - 65536 字节,否则抛
ArgumentException,消息形如"结构体大小非法 (须在 1 - 65536 字节): 类型全名"。 - 多字节字段是网络大端(PROFINET 网络序)。直接当宿主
short/int读会字节颠倒,要宿主数值走Read<T>/Write<T>(大端编解码)。 - I / Q 分两套结构体,不要混在一个里。
真正的 0 拷贝只有整幅映像的结构体映射 —— ProcessData.InputsMapping<T>() / ProcessData.OutputsMapping<T>()。
In[i] / Out[i] 那一族、Inputs / Outputs、ReadArea / WriteArea、Read<T> / Write<T> 全都是拷贝:每次取值都拷一小段字节再解释。
别把「按偏移取到的元素」当成活视图 —— [i] 定的只是偏移,值仍然是那一瞬间的快照。
读写如实报错,不会静默降级。
使用案例
日常使用
① 映射到结构体(0 拷贝,持续用):Start 之后映射一次,拿到钉在过程映像上的 ref,之后当变量读写。
using System.Runtime.InteropServices;
using Darra.PnS;
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct FromPlc // I:控制器 → 设备
{
public byte Present; // 偏移 0
public short Recipe; // 偏移 1:多字节是网络大端
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct ToPlc // Q:设备 → 控制器
{
public byte Ready; // 偏移 0
public byte Busy; // 偏移 1
}
using (var device = new DarraPnS())
{
PnSErrorCode rc = device.Start();
if (rc != PnSErrorCode.Success)
return;
ref readonly FromPlc inp = ref device.ProcessData.InputsMapping<FromPlc>(); // 映射一次
ref ToPlc outp = ref device.ProcessData.OutputsMapping<ToPlc>();
byte present = inp.Present; // 读:当前周期
outp.Ready = 1; // 写:下个周期发出,没有提交调用
device.Stop();
}
② 单次读取(一次性取):不要结构体,只取一份当前值。
byte[] i = device.ProcessData.Inputs; // I 整段:一次读出,同一拍自洽
short recipe = device.ProcessData.In[2].AsInt16; // 按字节偏移类型化读一个值
int counter = device.ProcessData.In[4].AsInt32;
字符串字段在过程映像里就是一段定长字节:映射不收引用字段,用快照一次读出。
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct FromPlcText
{
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)]
public byte[] PartNo;
}
var text = new FromPlcText();
PnSErrorCode rc = device.ProcessData.Read(ref text); // I 区快照读出(大端解码)
string partNo = System.Text.Encoding.ASCII.GetString(text.PartNo).TrimEnd('\0');
③ 按 TIA 地址读一个值:地址写法和 PLC 里一样,不用自己数字节偏移。
device.ReadBool("I0.0", out bool done); // 位
device.ReadInt16("IW2", out short recipe); // 字
device.ReadInt32("ID4", out int counter); // 双字
device.WriteBool("Q0.0", true); // 写 Q
device.WriteInt16("QW2", 123);
周期变化
映射取一次就够,同一份映射放进你自己的周期循环(或循环里的调度点)直接用:
ref readonly FromPlc inp = ref device.ProcessData.InputsMapping<FromPlc>();
ref ToPlc outp = ref device.ProcessData.OutputsMapping<ToPlc>();
while (running) // 你自己的周期任务,节拍自定
{
outp.Ready = (byte)(inp.Present + 1); // 读当前拍 → 算 → 写下拍发出
Thread.Sleep(10);
}
循环里读到的永远是当前周期、写下的下个周期发出,不需要任何额外调用。SDK 没有用户周期回调,调度(循环 / 定时器 / 事件)由你自己定。
映射放在循环外取一次仍然是推荐用法,但要知道它换来的是什么:省掉每次解析,代价是偏移一旦漂移就是静默读错位(见下面 InputsMapping<T>() 的说明)。
结构体映射(零拷贝)
映射没有偏移参数:起点永远是净荷第 0 字节。要映射中段,在结构体开头放一段占位 byte[] 顶开;或改用 In[i] / ReadArea 按偏移取。
InputsMapping<T>()
把 I 区(控制器 → 设备)的净荷钉成只读结构体,0 拷贝。映射一次,之后读字段就是读当前周期。
public unsafe ref readonly T InputsMapping<T>() where T : struct
返回值:
ref readonly T— 钉在过程映像净页上的只读引用,任意线程可读
说明:
- 只在
Start()之后可用。 服务没Start或已Stop,抛InvalidOperationException,消息是「过程数据输入映像不可用: 原因」。原因取LastServiceError,例如「服务未 Start 或已 Stop: 过程映像不可用, 请先 Start()」;没有原因时只留基础文案,不带尾部冒号。 - 通道没映射:
LastServiceError是「过程数据通道未映射, 过程映像不可用 (请先 Start() 建立过程数据通道)」,抛出的异常同上。 - 结构体含引用字段:
InvalidOperationException("结构体含引用字段,不能映射到过程映像")。 - 结构体大于净荷窗口:
InvalidOperationException("结构体大于输入过程映像")。 - 结构体布局非法:
ArgumentException(未标Sequential/Pack != 1/ 大小越界 / 字段类型不支持,见上面的结构体契约)。 - I 区只读:控制器每周期整段覆盖,写了下一拍就被冲掉,所以返回的是
ref readonly T,类型上直接禁写。要写过程数据用OutputsMapping<T>()。 - 起点不是常量。 净荷起点是每次映射时按当前区内容扫出来的:先剥 2 个 IOPS 前缀,再看净荷开头连续有几个
0x80,最多再剥 4 个。所以同一个结构体在不同时刻可能钉在差几个字节的位置上,能用的窗口比ProcessData.Inputs.Length小 2-6 字节。 - 映射放循环外取一次是推荐用法:省掉每次解析。代价是上面那条 —— 偏移一旦漂移就是静默读错位,不抛异常,尺寸门也拦不住(尺寸门只保证不越出净荷窗口、不戳出映射页,不校验主站侧 GSDML 配的长度;结构体大于主站实际下发的 I 长度时,尾部字段读到的是没刷新的字节,SDK 如实返回、不伪造)。
- 生命周期:
ref的有效期 = 映射存续期。Stop()只停刷新、不卸映射(读回旧值);Dispose()卸映射后ref立即失效,再读是未定义内存,不会抛异常提醒。 - 任意线程可读。
示例:
ref readonly FromPlc inp = ref device.ProcessData.InputsMapping<FromPlc>();
byte present = inp.Present; // 当前周期值
InputsMapping<T>(onChanged)
映射成结构体实例并挂数据变化回调。不是 0 拷贝 —— 它是 1 ms 轮询快照;金路径还是上面那个无参重载。
public InputProcessDataInstance<T> InputsMapping<T>(Action onChanged) where T : struct
public InputProcessDataInstance<T> InputsMapping<T>(Action<ProcessDataStructChangedEventArgs<T>> onChanged) where T : struct
返回值:
InputProcessDataInstance<T>— 轮询封装;.ValueSnapshot是最新一份快照(不会自己跟着总线变),.PollingError是后台失败原因
回调参数:
public sealed class ProcessDataStructChangedEventArgs<T> : EventArgs where T : struct
{
public T Previous { get; } // 变化前的快照
public T Current { get; } // 变化后的快照
}
带参数的重载给 Previous / Current;不带参数的重载只通知"变了"。两个重载其它行为一样。
说明:
- 构造时就先读一次快照。 没
Start就构造,抛InvalidOperationException("读取输入过程数据初始快照失败: rc=InvalidHandle")。所以回调不可能在Start之前响。 - 后台线程名叫
Darra.PnS.InputProcessData(IsBackground = true),每 1 ms 读一次快照,只在内容变了才回调(逐字段深度比较,不是按字节比)。 - 回调跑在那个后台线程上,且在锁外调用 —— 回调里读
ValueSnapshot不会自锁。 - 回调里抛异常 = 轮询永久停掉。 异常被吞进
PollingError,只写一条Trace.TraceError,之后再也不回调;再读ValueSnapshot才把原因抛出来:InvalidOperationException("输入过程数据后台轮询已停止", 内层异常)。 - 快照读失败(例如中途
Stop了)同样把InvalidOperationException("读取输入过程数据快照失败: rc=...")记进PollingError并停轮询。 PollingError没失败时为null。- 想停就
Dispose():幂等;最多等 1000 ms 收线程,超时把TimeoutException记进PollingError。 - Q 侧没有对应的回调重载。
- 这不是总线周期通知,也不能当 0 拷贝用。
示例:
InputProcessDataInstance<FromPlc> watch = device.ProcessData.InputsMapping<FromPlc>(
(ProcessDataStructChangedEventArgs<FromPlc> e) =>
Console.WriteLine($"{e.Previous.Recipe} -> {e.Current.Recipe}"));
FromPlc now = watch.ValueSnapshot; // 最新一份快照
watch.Dispose(); // 停轮询
OutputsMapping<T>()
把 Q 区(设备 → 控制器)的净荷钉成可写结构体,0 拷贝。映射一次,改字段即改过程映像。
public unsafe ref T OutputsMapping<T>() where T : struct
返回值:
ref T— 可写引用,钉在输出净荷上;改完字段下个周期发出
说明:
- 异常与
InputsMapping<T>()一一对应,只把"输入"换成"输出":InvalidOperationException的消息分别是「过程数据输出映像不可用: 原因」和「结构体大于输出过程映像」;含引用字段、布局非法同样抛。 - 没有任何提交调用。 内核周期 DPC 每周期原样取走输出区组帧,不按脏标记门控 —— 改字段就够了。
- 起点同样是扫出来的;Q 侧固定剥 3 字节前缀,所以能用的窗口正好等于
ProcessData.Outputs.Length。 - 钉页是单 owner:同一映像同一时刻只允许一个可写映射。读可并发,写请调用方自行串行化。
Dispose()卸映射后ref失效;Stop()只停刷新、不卸映射。- 旧名
OutputsMapping<T>(ref T)已删除。它是快照提交,和这个钉页同名不同义,现在叫SubmitStruct<T>(ref T),没留[Obsolete]转发。新代码别写旧名。
示例:
ref ToPlc outp = ref device.ProcessData.OutputsMapping<ToPlc>();
outp.Ready = 1; // 下个周期发出
outp.Busy = 0;
Read<T>(ref T)
【快照】I 区 → 结构体,大端解码。要 0 拷贝请用 InputsMapping<T>()。
public PnSErrorCode Read<T>(ref T data) where T : struct
返回值:
PnSErrorCode— 成功是Success;失败见下
说明:
- 未
Start→InvalidHandle。 - 结构体大于 I 区 →
LengthMismatch。这里的边界是整段ProcessData.Inputs.Length,比映射窗口宽松。 - 通道未就绪(映射掉了 / 读异常)→
NativeError,原因写在LastServiceError。 - 结构体布局非法 →
ArgumentException(快照和映射走同一套布局校验)。 - 含引用字段/
string/char→NotSupportedException或ArgumentException,与映射同一套判据。 bool字段按 bit0 编解码:解码时只有最低位算真(0x02/0x80解出false),编码时只写出1/0。跟元素口的AsBool同一口径。- 不是 0 拷贝:每次调用拷一份当前快照再解,拿到的实例不会跟着总线变。
device.ReadStruct<T>(ref x)与它同义。
示例:
FromPlc rx = new FromPlc();
PnSErrorCode rc = device.ProcessData.Read(ref rx);
Write<T>(ref T)
【快照】结构体 → Q 区,大端编码。要 0 拷贝请用 OutputsMapping<T>()。
public PnSErrorCode Write<T>(ref T data) where T : struct
返回值:
PnSErrorCode—Success表示整段已当场提交到共享区
说明:
- 未
Start→InvalidHandle。 - 结构体大于 Q 区 →
LengthMismatch。边界就是ProcessData.Outputs.Length。 - 通道未就绪/提交失败 →
NativeError,原因写在LastServiceError。提交失败时那片脏字节留在本地影子不丢,下一次写会带上,同时IoStatus.OutputValid如实为false(不冒充已送达)。 - 结构体布局非法 →
ArgumentException;定长byte[]字段的实际长度和SizeConst不符 →ArgumentException,消息形如「byte[] 字段 X 长度必须为 N 字节」。 - I 区没有对应的入口(写 I 静默无效,所以不给)。
device.WriteStruct<T>(ref x)与它同义。
示例:
ToPlc tx = new ToPlc { Ready = 1, Busy = 0 };
PnSErrorCode rc = device.ProcessData.Write(ref tx);
SubmitStruct<T>(ref T)
Write<T> 的别名,一字不差转发到 Write。
public PnSErrorCode SubmitStruct<T>(ref T data) where T : struct
返回值:
PnSErrorCode— 同Write<T>
说明:
- 失败形态和
Write<T>完全一样(未启动InvalidHandle/ 越界LengthMismatch/ 提交失败NativeError/ 布局非法ArgumentException)。 - 它是旧名
OutputsMapping<T>(ref T)的取代者。新代码写SubmitStruct<T>或Write<T>都行,两者同义。
示例:
device.ProcessData.SubmitStruct(ref tx);
GetFieldAddress<T>(fieldName)
把结构体字段名翻成 PLC 风格地址,省得自己数字节。
public string GetFieldAddress<T>(string fieldName) where T : struct
public string GetFieldAddress<T>(string fieldName, PnSAddressArea area) where T : struct
返回值:
string— 地址字符串;不带area时默认按 I 区(与Read同方向)
格式按字段类型定:
bool→I<偏移>.0(该字节的第 0 位,和ReadBool("I0.0")读的是同一个字节位)- 2 字节(
short/ushort/ 2 字节枚举)→IW<偏移> - 4 字节(
int/uint/float/ 4 字节枚举)→ID<偏移> - 1 字节 / 8 字节 / 定长数组 / 嵌套结构体 →
I<偏移>(起始字节地址;地址化 API 没有 8 字节直读) - 方向由
area定:PnSAddressArea.Input出I,PnSAddressArea.Output出Q
说明:
- 字段名区分大小写(按
Ordinal比)。找不到抛ArgumentException("结构体 类型名 中不存在字段: 名字")。 - 字段名空 / 全空白:
ArgumentException("字段名不能为空")。 area不是Input/Output:ArgumentException("仅支持 Input (I 区) / Output (Q 区) 地址")。- 结构体布局非法:
ArgumentException(判据同映射)。 - 生成出来的地址可能未对齐(偏移就是声明偏移),因为地址化读写不强制对齐,两者自洽。
示例:
string addr = device.ProcessData.GetFieldAddress<FromPlc>("Recipe"); // "IW1"
string qAddr = device.ProcessData.GetFieldAddress<ToPlc>("Ready", PnSAddressArea.Output); // "Q0"(Ready 是 1 字节,不带 .位号)
short recipe;
device.ReadInt16(addr, out recipe);
直接访问(不映射也能用)
下面这组调用与结构体映射彼此独立:不映射可以直接用,映射了也常并用。
In / Out
public ProcessDataArrayInstance In { get; } // I 区(控制器 → 设备):只读
public ProcessDataArrayInstance Out { get; } // Q 区(设备 → 控制器):可读可写
返回值:
ProcessDataArrayInstance— 字节偏移访问器;每次访问属性都新建一个,且是"还没选偏移"的状态
说明:
In是 I 区,控制器每周期覆盖 → 只读。对In赋值或写它的元素成员一律抛InvalidOperationException。Out是 Q 区,每次成员赋值都当场提交一次,成功的写下个周期发出。- 刚拿到手、还没写
[i]就用它的成员:InvalidOperationException("请先通过 ProcessData.In[index] 或 ProcessData.Out[index] 选择字节偏移")。
示例:
byte first = device.ProcessData.In[0].Content;
In[i] / Out[i]
public ProcessDataArrayInstance this[int index] { get; }
选择过程映像里的绝对字节偏移。i 是字节地址,不是元素序号 —— 这一点和别的数组不一样。
参数:
index(int) — 本方向的字节偏移,相对本方向过程映像起点
返回值:
ProcessDataArrayInstance— 带偏移的访问器;本身不搬数据,取值时才读
说明:
index小于 0 或大于 65535 抛ArgumentOutOfRangeException。- 索引合法但那一拍读不出来(没
Start/ 偏移超出当前区长度 / 通道没起来):取值时抛InvalidOperationException,消息里带偏移和错误码。 - 元素口没有
Length属性。长度从ProcessData.Inputs.Length/ProcessData.Outputs.Length拿。 - 8 字节字段用
AsInt64/AsUInt64按偏移取;或直接走结构体映射。 Out[i]逐字段写是每次一次提交;要一次改多个字段,用OutputsMapping<T>()钉页 —— 改字段不需要提交调用。
相关结构:
public sealed class ProcessDataArrayInstance // ProcessData.In[i] / Out[i] 的类型
{
// 下标与原始字节
public ProcessDataArrayInstance this[int index] { get; } // 绝对字节偏移;< 0 或 > 65535 抛 ArgumentOutOfRangeException
public byte Content { get; set; } // 原始字节;In 只读,Out 可写
// 定宽标量(多字节一律网络大端,PROFINET 网络序)
public sbyte AsInt8 { get; set; } // 有符号 8 位,1 字节
public byte AsUInt8 { get; set; } // 无符号 8 位,1 字节;与 Content 同一个字节
public short AsInt16 { get; set; } // 有符号 16 位,2 字节
public ushort AsUInt16 { get; set; } // 无符号 16 位,2 字节
public int AsInt32 { get; set; } // 有符号 32 位,4 字节
public uint AsUInt32 { get; set; } // 无符号 32 位,4 字节
public long AsInt64 { get; set; } // 有符号 64 位,8 字节
public ulong AsUInt64 { get; set; } // 无符号 64 位,8 字节
public float AsFloat { get; set; } // IEEE754 单精度,4 字节
public double AsDouble { get; set; } // IEEE754 双精度,8 字节
public bool AsBool { get; set; } // bit0 语义;写整字节 1 / 0,不是只改 bit0
// 位
public bool GetBit(int bitIndex); // 0-7,0 = 最低位;In / Out 都可读
public void SetBit(int bitIndex, bool value); // 只改这一位,同字节其余位不动;仅 Out
// 按数据类型进出
public object AsValue(PnSDataType type); // 按 PnSDataType 读 boxed 值
public void WriteValue(PnSDataType type, object value); // 按 PnSDataType 写;仅 Out
}
每个口的失败形态:
- 读失败统一抛
InvalidOperationException。单字节口(Content/AsInt8/AsUInt8/AsBool)的消息形如「读取 I 区过程数据字节失败: index=0, rc=LengthMismatch」;多字节口(AsInt16及以上)形如「读取 I 区过程数据失败: index=2, length=2, rc=LengthMismatch」。方向是 Q 时消息里的字母就是 Q。 - 写失败(Q 侧):单字节口「写入 Q 区过程数据字节失败: index=0, rc=...」,多字节口「写入 Q 区过程数据失败: index=2, length=2, rc=...」。
- 写 I 侧:
In[i]上的任何写口(Content/AsXxx/SetBit)一律抛InvalidOperationException("ProcessData.In 对应 I 区 (控制器→设备),只允许读取"),一个都不落。 - 每次取值都是一份新拷贝:读出来的
byte/short/ ... 是那一瞬间的值,不会跟着总线变。
AsBool 是 bit0 语义:字节 0x02 / 0x80 这类非规范值读 false,0x00 / 0x01 才是 false / true。要按整字节 0 / 非 0 判真假,读 Content 自己比。
device.ProcessData.Out[i].AsBool = value 把该字节整体写成 1 或 0,不是只改 bit0 ——
device.ProcessData.Out[0].AsBool = true 会把同一字节的 bit1-7 一起清掉。
同一字节里既有布尔位又有其它标志位时,用 SetBit(i, value):它只覆盖目标位,其余位保持不变。
示例:
byte b = device.ProcessData.In[0].Content; // 读:当前拍
short iw = device.ProcessData.In[2].AsInt16; // 大端解码
int id = device.ProcessData.In[4].AsInt32;
bool f = device.ProcessData.In[0].AsBool; // bit0 语义
device.ProcessData.Out[0].Content = 0x11; // 写:下个周期发出
device.ProcessData.Out[2].AsInt16 = 123;
GetBit(bitIndex) / SetBit(bitIndex, value)
单独用同一个字节里的某一位。
public bool GetBit(int bitIndex); // In / Out 都可读
public void SetBit(int bitIndex, bool value); // 仅 Out
参数:
bitIndex(int) — 位号 0-7,0 = 最低位(LSB)value(bool) — true 置 1,false 清 0
返回值:
bool— 该位是否为 1
说明:
bitIndex不在 0-7:抛ArgumentOutOfRangeException,消息是「位下标必须在 0-7」。这条检查在最前面,对In调用也一样先报位号错。- 位号合法、但方向是
In:SetBit走到写入时抛InvalidOperationException("ProcessData.In 对应 I 区 (控制器→设备),只允许读取")。 SetBit是读-改-写同一字节:先读当前字节,改掉目标位,再整字节写回,同字节其余位不动。- 读不到字节时抛的是
Content那两条读失败异常。
示例:
bool run = device.ProcessData.In[3].GetBit(0); // 读 I 的第 3 字节 bit0
device.ProcessData.Out[0].SetBit(0, true); // 只置 Q 的第 0 字节 bit0
AsValue(type) / WriteValue(type, value)
按数据类型枚举进出,适合类型在运行期才定的场合(表格驱动、变量联想表)。
public object AsValue(PnSDataType type); // In 读 I 区;Out 读 Q 影子
public void WriteValue(PnSDataType type, object value); // 仅 Out
参数:
type(PnSDataType) — 过程数据类型value(object) — 要写入的值,须能转成对应的 CLR 类型
返回值:
object— 装箱值:bool/sbyte/short/int/long/byte/ushort/uint/ulong/float/double
支持的取值(宽度与字节序同上面的定宽口):
Bool→bool(bit0 语义)Integer8/Integer16/Integer32/Integer64→sbyte/short/int/longUnsigned8/Unsigned16/Unsigned32/Unsigned64→byte/ushort/uint/ulongReal32/Real64→float/doubleByte/Word/DWord/LWord分别是Unsigned8/16/32/64的别名,同一个宽度
说明:
type不受支持:ArgumentOutOfRangeException,消息形如「不受支持的过程数据类型: xxx」。WriteValue的value为null:ArgumentNullException。value转不过去:Convert自己抛(InvalidCastException/FormatException/OverflowException)。- 对
In调WriteValue:落到写口时抛InvalidOperationException("ProcessData.In 对应 I 区 (控制器→设备),只允许读取")。 - 读不到字节时抛的是
Content那两条读失败异常。
示例:
object v = device.ProcessData.In[4].AsValue(PnSDataType.Integer32);
device.ProcessData.Out[4].WriteValue(PnSDataType.DWord, 10000);
整帧与整区
映射放不下的字段(字符串、变长数据、整批快照)走这几个口。全是拷贝 —— 每次调用取到的都是那一刻的快照:单次调用返回的这份内部自洽,跨多次调用不保证同一拍。要同一拍就一次读够。
Inputs / Outputs
public byte[] Inputs { get; } // I 整段:每次访问都给一份新的快照
public byte[] Outputs { get; set; } // Q 整段:get 给快照;set 提交整段
返回值:
byte[]— 那一刻的快照,不是 0 拷贝;拷出来不会跟着总线变
说明:
- 这两个属性声明在
device.ProcessData上,不在device上。 Inputs每次访问都新分配一个byte[]。读不出来(没Start/ 通道没起来)返回空数组,不抛 —— 判断"读到没有"要看Length,不要用异常。Outputs的 get 同理,读不出来返回空数组。Outputs的 set 是短写:只覆盖前 N 字节,不要求长度等于整区。赋null抛ArgumentNullException;提交失败抛InvalidOperationException("写输出过程数据失败 rc=" + 错误码)。Inputs已经剥掉开头的 IOPS 前缀,净荷从下标 0 起,别自己再加偏移;Inputs.Length按 I 区整段长度算(前缀字节仍计入长度、尾部补 0),所以有效净荷只有前Length - 前缀字节。Outputs.Length是输出区长度减 3(净荷窗)。见下面「字节数怎么拿」。- 高频循环里别用这两个属性:每次访问都新分配一个大数组。要么用上面的映射,要么改用
In[i]/Out[i]按需取。
示例:
byte[] frame = device.ProcessData.Inputs;
string name = System.Text.Encoding.ASCII.GetString(frame, 0, 16).TrimEnd('\0');
ReadArea(area, out data) / WriteArea(area, data)
按区域一次性读 / 写整段,替代逐地址读写的来回。
public PnSErrorCode ReadArea(PnSAddressArea area, out byte[] data);
public PnSErrorCode WriteArea(PnSAddressArea area, byte[] data);
参数:
area(PnSAddressArea) —Input= I 区(只读)/Output= Q 区(只写)。Memory/Db/Unknown一律InvalidArgument:不支持 M 区、不支持 DB。
返回值:
PnSErrorCode—Success才是成功;data是那一刻的区域快照
ReadArea 的失败形态:
- 区域不是
Input/Output→InvalidArgument。 - 未
Start→InvalidHandle。 - 通道未就绪、或读共享区抛异常 →
NativeError,原因写进LastServiceError。 - 返回的
data长度就是ProcessData.Inputs.Length(I 区)/ProcessData.Outputs.Length(Q 区),整区全量,没有"越界"这一说。
WriteArea 的失败形态:
- 写 I 区 →
NotSupported(不抛)。这一条在未启动检查之前,没Start也一样是NotSupported。 - 区域不是
Output(例如Memory/Db)→InvalidArgument。 - 未
Start→InvalidHandle。 data为null或空数组 →InvalidArgument。data.Length和ProcessData.Outputs.Length不一致 →LengthMismatch。长度必须完全相等,不允许多也不允许少。- 提交失败 →
NativeError。
说明:
ReadArea走共享区现值,不阻塞。WriteArea写入 Q 影子后当场单次提交:Success就是整区已经落在共享区。- 一次调用读写整区,比逐个
Read*/Write*省往返。
示例:
device.ReadArea(PnSAddressArea.Input, out byte[] i); // I 整段
byte[] q = new byte[device.ProcessData.Outputs.Length];
Array.Copy(i, q, q.Length);
device.WriteArea(PnSAddressArea.Output, q); // Q 整段,长度必须相等
Read(out fromPlc) / Write(toPlc)
区域整段的便捷包装,语义同上。
public PnSErrorCode Read(out byte[] fromPlc); // = ReadArea(Input), 读 I 区
public PnSErrorCode Write(byte[] toPlc); // 写 Q 区, 允许短写
参数:
toPlc(byte[]) — 要发出去的 Q 区字节,允许短写(只覆盖前 N 字节)
返回值:
PnSErrorCode— 失败码同ReadArea/WriteArea
说明:
Read就是ReadArea(PnSAddressArea.Input, out fromPlc),失败形态完全一样。Write与WriteArea(Output, ...)的差别只有一个:Write允许长度小于整区(短写),WriteArea要求长度完全相等。toPlc为null或空数组 →InvalidArgument。- 长度超出当前 Q 区 →
LengthMismatch。 - 未
Start→InvalidHandle;提交失败 →NativeError。
示例:
device.Write(new byte[] { 0x01, 0x00 }); // 短写 Q:只覆盖前 2 字节
device[offset](indexer)
按绝对字节偏移读写一个字节的语法糖。
public byte this[int offset] { get; set; }
参数:
offset(int) — 绝对字节偏移
返回值:
byte— 该偏移的字节
说明:
- 不是视图。 get 是「读整段 I 区 → 取第
offset个字节」,每次取值都整区拷一份。要高频用别走这里。 - get:
offset小于 0 抛ArgumentOutOfRangeException;读失败或offset超出当前区长度抛InvalidOperationException,消息形如「读过程数据失败 offset=N rc=错误码 服务端原因」。 - set:
offset小于 0 或大于 65535 抛ArgumentOutOfRangeException;写失败抛InvalidOperationException,消息形如「写过程数据失败 offset=N rc=错误码 服务端原因」。 - get 读 I 区、set 写 Q 区,一个属性两个方向,偏移语义各自独立(不是同一个区的读回)。
示例:
byte b = device[0]; // 读 I 区偏移 0
device[0] = 0x11; // 写 Q 区偏移 0
按地址读写(区域 + 字节 + 位)
不想自己算偏移、想照着 TIA 里的地址写时用这几个口:区域字母 + 字节偏移 + 位。
device.ReadBool("I0.0", out bool done); // 位
device.ReadInt16("IW2", out short recipe); // 字
device.ReadInt32("ID4", out int counter); // 双字
device.WriteBool("Q0.0", true);
device.WriteInt16("QW2", 123);
device.WriteInt32("QD4", 10000);
| 地址写法 | 是什么 |
|---|---|
| I0.0 / Q0.0 | 位地址:第 0 字节的第 0 位(位号只能 0-7) |
| IB0 / QB0 | 字节(1 字节) |
| IW2 / QW2 | 字(2 字节) |
| ID4 / QD4 | 双字(4 字节) |
共用约定:
- 不强制 2 / 4 对齐。
IW3/ID2都合法,偏移就是字节偏移 —— 和GetFieldAddress生成的未对齐地址自洽。 - 不支持 M 区 / DB 区。
- 读口是
PnSErrorCode+out取值;写口返回PnSErrorCode,不抛。 - 地址格式错不抛异常,一律返回
InvalidArgument(内部解析器自己抛的ArgumentException被吃掉,换成错误码)。 - 写口都是读-改-写:先读同地址的当前值,改掉目标字节 / 位,再整段写回。读不回来就直接返回那次的失败码,不会写一半。
ReadBool("Q0.0")读的是 Q 区本地影子(最近写进去的值),不是线上回读。
整区 WriteArea 与地址化写(WriteBool / WriteInt16 / WriteInt32)遇到 I 侧不抛,返回 PnSErrorCode.NotSupported —— 记得判返回值,别以为没异常就写成功了。
只有元素口(In[i].Content = ... 以及 In[i] 上的各类型化赋值)才抛 InvalidOperationException。
ReadBool(address, out value)
读一个位。
public PnSErrorCode ReadBool(string address, out bool value)
参数:
address(string) — 位地址,如I0.0/Q0.0;位号 0-7,0 = 最低位value(out bool) — 读到的位值;失败时为false
返回值:
PnSErrorCode— 成功是Success
说明:
- 地址解析不了(格式错 / M 区 / DB 区)→
InvalidArgument。 - 地址是字节 / 字 / 双字(没有
.位号)→InvalidArgument。 - 未
Start→InvalidHandle;偏移超出当前区 →LengthMismatch;通道未就绪 →NativeError。 - 位号超出 0-7 的地址(如
I0.8)在解析阶段就被拒 →InvalidArgument。 I读的是 I 区 CPM;Q读的是 Q 区本地影子(最近写入值)。- 不抛异常。要看原因读
LastServiceError。
示例:
if (device.ReadBool("I0.0", out bool done) == PnSErrorCode.Success && done)
device.WriteBool("Q0.0", true);
ReadInt16(address, out value) / ReadInt32(address, out value)
读一个字 / 双字,大端解码。
public PnSErrorCode ReadInt16(string address, out short value)
public PnSErrorCode ReadInt32(string address, out int value)
参数:
address(string) —IW2/QW2(16 位)、ID4/QD4(32 位)value(out short / out int) — 读到的值;失败时为 0
返回值:
PnSErrorCode— 成功是Success
说明:
- 宽度必须对得上:
ReadInt16要 2 字节地址,ReadInt32要 4 字节地址。- 给了位地址(
I0.0)→InvalidArgument。 ReadInt16("IB0")(1 字节)、ReadInt16("ID0")(4 字节)→InvalidArgument。
- 给了位地址(
- 地址解析不了(格式错 / M 区 / DB 区)→
InvalidArgument。 - 未
Start→InvalidHandle;偏移加宽度超出当前区 →LengthMismatch;通道未就绪 →NativeError。 - 偏移不要求对齐:
IW3读的是第 3、4 两个字节,大端拼成一个short。 ReadInt32返回int(有符号),无符号场景自己按位解释。
示例:
device.ReadInt16("IW3", out short recipe); // 未对齐也合法
device.ReadInt32("ID4", out int counter);
WriteBool(address, value) / WriteInt16(address, value) / WriteInt32(address, value)
写一个位 / 字 / 双字,大端编码。
public PnSErrorCode WriteBool(string address, bool value)
public PnSErrorCode WriteInt16(string address, short value)
public PnSErrorCode WriteInt32(string address, int value)
参数:
address(string) — 位地址Q0.0、字地址QW2、双字地址QD4value— 要写入的值
返回值:
PnSErrorCode—Success表示已经落共享区
说明:
- 写 I 区不抛,返回
NotSupported(I 只读)。注意顺序:这三个口是读-改-写,未Start时前一步"读同地址现值"先失败,返回的是InvalidHandle;NotSupported只在已Start之后出现。 - 写 Q 区都是读-改-写:先读同地址现值,只改这一段,再整段写回。读不回来就返回那次的码,不会写一半。
WriteBool只动目标位,同字节其余位保持不变(不走AsBool那条"整字节 1 / 0"的路)。- 地址解析不了(格式错 / M 区 / DB 区)→
InvalidArgument;宽度对不上 →InvalidArgument;未Start→InvalidHandle;偏移加宽度超出 Q 区 →LengthMismatch;提交失败 →NativeError。
示例:
device.WriteBool("Q0.0", true); // 只改 Q 区第 0 字节的 bit0
device.WriteInt16("QW2", 123);
device.WriteInt32("QD4", 10000);
PnSAddress.Parse(address)
上面那些口用的地址解析器,也可以自己拿去先解析、再校验。
public static PnSAddress Parse(string address)
返回值:
PnSAddress— 解析结果;成员有Area/ByteOffset/BitNumber/DataType/DataLengthBytes/SpanBytes/IsBit/Original/DbNumber
public sealed class PnSAddress
{
public PnSAddressArea Area { get; } // Input(I 区)/ Output(Q 区)
public int ByteOffset { get; } // 字节偏移
public int BitNumber { get; } // 位号 0-7;仅 DataType 为 Bit 时有效
public PnSAddressDataType DataType { get; } // Bit / Byte / Word / DWord
public int DataLengthBytes { get; } // 位 = 0,字节 = 1,字 = 2,双字 = 4
public int SpanBytes { get; } // 访问跨度:位算 1 字节,其余同 DataLengthBytes
public bool IsBit { get; }
public int DbNumber { get; } // 保留字段;不支持 DB,恒为 0
public string Original { get; } // 原始地址字符串
public void ValidateRange(int imageLength); // 越界检查
}
说明: 这个口会抛异常(和 Read* / Write* 返回错误码的口径不同)。消息都是源码原文:
- 地址为空 / 全空白:
ArgumentException("地址不能为空") DB开头:ArgumentException("不支持 DB")M开头:ArgumentException("不支持 M 区")- 区域字母不是
I/Q/M:ArgumentException("不支持的地址区域: 原地址") - 位地址不成形(如
I0./I.a):ArgumentException("地址格式非法 (位地址应为 I0.0 形式): 原地址") - 区域后面没有数字:
ArgumentException("地址缺少偏移量: 原地址") - 偏移或位号不是纯数字 / 为负:
ArgumentException("字节偏移非法: 原地址")或ArgumentException("位号非法: 原地址") - 位号超出 0-7(如
I0.8):ArgumentException("位号必须在 0-7 之间: 原地址") - 不带类型字符的地址(
I0/Q7)按字节处理,和 HSL 惯例一致;位地址必须写.位号。
ValidateRange(imageLength) 另外抛 ArgumentOutOfRangeException,消息形如「地址 IW7 需要 2 字节, 超过过程映像长度 4」。
示例:
PnSAddress a = PnSAddress.Parse("IW3");
int off = a.ByteOffset; // 3,未对齐也照样解析
a.ValidateRange(device.ProcessData.Inputs.Length);
下标与索引空间
字节下标
过程映像用字节偏移当下标:ProcessData.In[i] / Out[i],i 就是字节地址,不是元素序号。偏移不强制 2 / 4 对齐,In[3] 和 IW3 指的是同一个位置。
映射的结构体从净页第 0 字节起排,所以「按偏移取元素」和「结构体字段」是两把尺子:结构体布局对上了,两者的偏移就一致(这也是 GetFieldAddress 能生成地址的原因)。
记录索引空间
诊断侧的记录是另一个地址轴,和过程映像的字节偏移没关系。它分两段,走两个不同的入口:
- 用户区
0x0000-0x7FFF——device.Records(GET /api/records)。控制器写下来的用户记录都在这一段里。字段与用法见诊断。 - I&M
0xAFF0-0xAFF4——device.ImData(GET /api/im)。设备标识记录,由协议栈自己管理,不进用户区记录表。
所以在 Records 里翻 0xAFF0 是找不到的:用户区记录表只覆盖 0x0000-0x7FFF,I&M 走 ImData 这条路。两个端点别当成一张表。
字节数怎么拿
长度由运行配置决定,不要写死 20 / 32(改过组态就变)。没有 InputsByteCount / OutputsByteCount 这类口。
| 想拿 | 走哪 |
|---|---|
| I 侧整段长度 | device.ProcessData.Inputs.Length |
| Q 侧整段长度 | device.ProcessData.Outputs.Length |
| 两个方向的配置长度 | device.DiagSnapshot.InputAreaLength / OutputAreaLength(uint) |
Inputs的快照已剥掉开头的 IOPS 前缀(净荷从下标 0 起);Inputs.Length仍按整区长度算,前缀字节计入长度、尾部补 0。Outputs.Length= 输出区长度 减 3(净荷窗)。- 结构体映射能用的窗口又比
Inputs.Length小 2-6 字节:起点是每次映射时扫出来的(先剥 2 个 IOPS 前缀,再看净页开头连续有几个0x80,最多再剥 4 个)。 ReadArea(Input)返回的长度等于Inputs.Length;WriteArea(Output, data)要求的长度等于Outputs.Length。
所以「映射放得下」和「Inputs.Length 够大」不是一回事 —— 结构体大小要按映射窗口算,别照着 Inputs.Length 卡。拿不准就让映射自己报错:超了会抛 InvalidOperationException("结构体大于输入过程映像")。