PDO 输入输出
通过 slave.pdo 访问过程映像。
PDO 就是过程映像里的一段数据,内核每周期自动收发。
映射成 ctypes 结构体后,读写字段就是直接读写过程映像本身 —— 0 拷贝:不用拷贝、不用刷新,读到的永远是当前周期,写下的下个周期发出。
不想定义结构体时,用元素视图 input_array / output_array 按字节偏移单点读写。
邮箱通信(SDO/CoE/FoE 等)自动附带在 PDO 周期中,不占用额外帧。邮箱响应延迟约 2-3 个 PDO 周期。
- 必须
_pack_ = 1,字段顺序与 PDO 映射逐字节一致;存在可判定的填充字节(漏写_pack_)时映射抛RuntimeError,不静默错位。 - 不得大于对应方向的映像字节数(
slave.pdo.inputs_mapping_info["size"]/outputs_mapping_info["size"])。- 超尺寸、映像不可用、
offset越界都抛RuntimeError(消息带从站号、方向与字节数)。 - 小于合法,按偏移覆盖。
- 超尺寸、映像不可用、
- 字段只能是 ctypes 定宽类型;字符串用
ctypes.c_char * n,变长数据用整帧inputs解。 - 映射一次长期有效;
close()/ 重新进 OP 后旧视图作废,重进 OP 时重取。
使用案例
日常使用
① 映射到结构体(0 拷贝,持续用):进 OP 后映射一次,拿到钉在过程映像上的结构体实例,之后当变量读写。
import ctypes
from darra_ethercat import EtherCATMaster, EcState
class InPdo(ctypes.Structure):
_pack_ = 1 # 必须;字段顺序与 PDO 逐字节一致
_fields_ = [
("status_word", ctypes.c_uint16),
("actual_position", ctypes.c_int32),
]
class OutPdo(ctypes.Structure):
_pack_ = 1
_fields_ = [
("control_word", ctypes.c_uint16),
("target_position", ctypes.c_int32),
]
master = (EtherCATMaster()
.set_eni(r"C:\EtherCAT\MyProject.deni") # DENI 一行加载
.build())
master.set_state(EcState.OP) # 进 OP
slave = master[1] # 从站(1-based 索引)
inputs = slave.pdo.inputs_mapping(InPdo) # 只读视图(进 OP 后映射一次)
outputs = slave.pdo.outputs_mapping(OutPdo)
outputs.control_word = 0x000F # 写:下个周期发出
outputs.target_position = inputs.actual_position # 读:永远是当前周期
print(f"状态字=0x{inputs.status_word:04X} 位置={inputs.actual_position}")
② 元素视图(不定义结构体):按字节偏移取一个元素,元素的每个成员就是一个定宽标量读写口。
# 单点:索引越界抛 IndexError
pos = slave.pdo.input_array[2].as_int32 # 偏移 2 起 4 字节
sw = slave.pdo.input_array[0].as_uint16 # 偏移 0 起 2 字节(StatusWord)
slave.pdo.output_array[0].as_uint16 = 0x000F # 写:下个周期发出
slave.pdo.output_array[2].as_int32 = 10000
# 整段:一份完整输入帧快照,同一拍一致,同一份 bytes 里解多个字段
import struct
frame = slave.pdo.inputs # bytes;取不到帧返回 None
if frame is not None:
sw2, pos2 = struct.unpack_from("<Hi", frame, 0)
print(f"状态字=0x{sw2:04X} 位置={pos2}")
周期变化
映射取一次就够,同一份映射放进你自己的周期循环(或循环里的调度点)直接用:
import time
while running:
outputs.control_word = 0x000F if inputs.status_word & 0x0004 else 0x0006
outputs.target_position = inputs.actual_position
time.sleep(0.001) # 你自己的节奏,例如 1 ms
内核每周期搬运过程数据:循环里读到的永远是当前周期、写下的下个周期发出,不需要任何额外调用,也不要每拍重新映射(close()
/ 重新进 OP 后旧视图才作废,重进 OP 时重取一次即可)。
结构体映射(零拷贝)
inputs_mapping() / outputs_mapping()
def inputs_mapping(self, struct_type, offset=0) # TxPDO(从站→主站):只读
def outputs_mapping(self, struct_type, offset=0) # RxPDO(主站→从站):可读可写
把 ctypes 结构体钉在 GetIO live 过程映像指针上,映射一次后读字段即读当前周期值(0 拷贝,不是快照)。
参数:
struct_type(ctypes.Structure 子类) — 必须_pack_ = 1,字段顺序与该方向 PDO 映射逐字节一致offset(int) — 相对该方向过程映像起始的字节偏移(可选),默认0
返回值:
- 输入方向 — 钉在 live 输入过程映像上的结构体实例;只读,给字段赋值抛
RuntimeError - 输出方向 — 钉在 live 输出过程映像上的结构体实例;改字段即改 PDO,下个周期发出
说明:
- 进 OP 后调用。
- 下列情况一律抛
RuntimeError:GetIO失败(主站未启动 / 未进 OP / 该方向无映射)- 映像为空
offset为负offset + ctypes.sizeof(struct_type)超过映像字节数- 结构体存在可判定的填充字节
示例:
inputs = slave.pdo.inputs_mapping(InPdo) # 只映射一次(进 OP 后)
outputs = slave.pdo.outputs_mapping(OutPdo)
outputs.control_word = 0x000F # 写:下个周期发出
pos = inputs.actual_position # 读:当前周期值
inputs_slice_mapping() / outputs_slice_mapping()
从指定字节偏移处钉页,供 MDP 等按模块切段的设备使用。
def inputs_slice_mapping(self, struct_type, offset) # 输入切片:只读
def outputs_slice_mapping(self, struct_type, offset) # 输出切片:可写
参数:
struct_type(ctypes.Structure 子类) — 必须_pack_ = 1,字段顺序与 PDO 映射逐字节一致offset(int) — 相对该方向过程映像起始的字节偏移,必填(不接受默认0,避免把切片调用误写成整段调用)
返回值:
- 与
inputs_mapping/outputs_mapping同类型 — 钉在 live 过程映像offset处的结构体实例(非快照);输入方向只读
说明:
- 与
inputs_mapping(struct_type, offset)同一实现、同一越界口径。 offset为负、或offset + ctypes.sizeof(struct_type)超过映像字节数,抛RuntimeError。
示例:
# MDP 设备:模块 1 从偏移 0 开始,模块 2 从偏移 8 开始(模块各自定义 ctypes 结构体)
mod1 = slave.pdo.inputs_slice_mapping(Module1In, 0)
mod2 = slave.pdo.inputs_slice_mapping(Module2In, 8)
out2 = slave.pdo.outputs_slice_mapping(Module2Out, 8)
out2.setpoint = 1500
bind_pdo_struct()
def bind_pdo_struct(self, struct_type, is_input=True, offset=0)
按方向参数转调 inputs_mapping / outputs_mapping,供同时需要两个方向的调用点用一个入口。
参数:
struct_type(ctypes.Structure 子类) — 必须_pack_ = 1is_input(bool) —True取输入方向(只读),False取输出方向(可写),默认Trueoffset(int) — 相对该方向过程映像起始的字节偏移,默认0
返回值:
- 结构体实例 — 钉在 live 过程映像上(非快照);输入方向只读
示例:
input_ref = slave.pdo.bind_pdo_struct(InPdo, is_input=True)
print(input_ref.actual_position) # 每次读即最新值
inputs_mapping_info / outputs_mapping_info
@property
def inputs_mapping_info(self) -> dict # slave.pdo
@property
def outputs_mapping_info(self) -> dict
本方向过程映像的 live 指针与字节数,用来在映射前核对结构体大小。
返回值:
dict—{"pointer": int, "size": int, "slave_index": int};size是本方向过程映像字节数,未进 OP / 未映射时为 0
示例:
import ctypes
if ctypes.sizeof(InPdo) > slave.pdo.inputs_mapping_info["size"]:
raise RuntimeError("结构体大于输入 PDO")
直接访问(不映射也能用)
下面这组调用与结构体映射彼此独立:不映射可以直接用,映射了也常并用(结构体放不下的字段用它解)。
input_array / output_array
@property
def input_array(self) -> PDOArrayInstance # TxPDO(从站→主站):只读
@property
def output_array(self) -> PDOArrayInstance # RxPDO(主站→从站):可读可写
按字节偏移索引过程数据元素:input_array[i] 读输入,output_array[i] 读写输出,i 相对本方向过程映像起点。
返回值:
PDOArrayInstance— 按字节索引的过程映像视图;length是本方向过程映像字节数,未进 OP / 未映射时为 0(此时任何索引都抛IndexError)
说明:
input_array是 TxPDO,主站每周期覆盖,只读:写它的元素成员或input_array[i] = v一律抛RuntimeError。output_array是 RxPDO,每次成员赋值走一次版本化事务(Copy → 改帧 → 提交,冲突有界重试),成功的写下个周期发出。- 提交失败抛
RuntimeError。 - 元素视图按偏移缓存复用,读取每次取一份当前帧快照后按偏移解析 —— 与
slave.pdo.inputs同一份快照语义,不是inputs_mapping()的 0 拷贝钉页引用。 - 索引越界抛
IndexError;偏移或宽度落在本方向过程映像之外抛ValueError(与索引越界刻意分开,便于区分选错字节与选错类型)。 - 索引不是整数、整型成员(
content与as_int8…as_uint64)的赋值不是整数(bool按 0 / 1)抛TypeError。 as_float/as_double另收int/float/bool。- 快照取不到(未进 OP / 该方向无映射)抛
RuntimeError。
新鲜度:
- 一次成员访问 = 一份新快照。
item.as_int16与item.as_bool各读一次,两次之间可能已经过了一个总线周期,同一元素的两个字段可能来自不同拍。- 要多字段同拍一致,先取一份整帧
frame = slave.pdo.inputs,再从这一份bytes里按偏移解析。 - 要 0 拷贝的整段视图用
inputs_mapping(struct_type)。
字节序: 多字节一律小端(EtherCAT 线序),本层不做任何换序。
0 拷贝:
- 元素访问器不是 0 拷贝路径 —— 每次读取都先复制一份完整过程映像帧(
length字节)再按偏移解析。 - 0 拷贝只用以下入口(都钉在 live 过程映像上,不是快照):
inputs_mappingoutputs_mappinginputs_slice_mappingoutputs_slice_mappingbind_pdo_struct
相关结构:
class PDOArrayInstance: # slave.pdo.input_array / output_array
length # int,只读:本方向过程映像字节数;未进 OP / 未映射 = 0
def __getitem__(index) # → PDODataItem;index ∉ [0, length) 抛 IndexError
def __setitem__(index, value) # output_array[i] = v ≡ output_array[i].content = v
def __len__() # ≡ length
class PDODataItem: # 单个 PDO 项的视图:slave.pdo.input_array[i] / output_array[i]
content # int,读写:原始字节(无符号 8 位)
as_int8 # int,读写:有符号 8 位
as_uint8 # 与 content 同义同字节
as_int16 # 多字节一律小端(EtherCAT 线序)
as_uint16
as_int32
as_uint32
as_int64
as_uint64
as_float # IEEE754 单精度
as_double # IEEE754 双精度
as_bool # bool,读写:bit0 语义,只有最低位为 1 才是 True
def get_bit_in_byte(bit_index) # → bool;bit_index 0-7,0 = LSB
def set_bit_in_byte(bit_index, value) # 仅输出方向;只改目标位,同字节其余位不动
def get_bit(bit_index) # 旧名别名(DeprecationWarning)→ get_bit_in_byte
def set_bit(bit_index, value) # 旧名别名(DeprecationWarning)→ set_bit_in_byte
def as_value(data_type) # 按 EcDataType 读:只支持 11 个定宽类型
def write_value(data_type, value) # 按 EcDataType 写
def as_base_data(data_type) # → BaseData:位串 / 变长类型的出口
def write_from_base_data(data) # → bool;失败返回 False(不抛)
def __int__() # int(item) ≡ .content
as_bool 只认最低位:字节 0x02 与 0x80 都读 False。
写 as_bool 是整字节写:True 写 0x01、False 写 0x00(不是改单个位),该字节的 bit1–bit7
会被一并清掉;要按位读写请用 get_bit_in_byte() / set_bit_in_byte()。
要按整字节 0 / 非 0 判真假,读 content 自行比较 —— 没有第二个「非 0 即真」的口子。
as_value / write_value 只支持与定宽类型一一对应的 11 个数据类型:
BOOLEANINTEGER8/INTEGER16/INTEGER32/INTEGER64UNSIGNED8/UNSIGNED16/UNSIGNED32/UNSIGNED64REAL32/REAL64
位串(BIT1…BIT8)、24 / 40 / 48 / 56 位整数、字符串、DOMAIN 一律抛 ValueError。
示例:
item = slave.pdo.input_array[2] # 偏移 2 处的元素视图
pos = item.as_int32 # 读:当前帧快照
sw = slave.pdo.input_array[0].as_uint16 # 偏移 0 起 2 字节(StatusWord)
slave.pdo.output_array[2].as_int32 = 10000 # 写:版本化局部提交,下个周期发出
slave.pdo.output_array[0].set_bit_in_byte(0, True) # 只置 bit0,同字节其余位不动
slave.pdo.output_array[0] = 0x11 # ≡ output_array[0].content = 0x11
flag = slave.pdo.input_array[3].as_bool # bit0 语义
byte0 = int(slave.pdo.input_array[0]) # ≡ .content
for i in range(slave.pdo.output_array.length): # 遍历整个输出映像
slave.pdo.output_array[i].set_bit_in_byte(0, False)
get_bit_in_byte() / set_bit_in_byte()
读 / 写元素所在那个字节的第 bit_index 位。旧名 get_bit() / set_bit() 仍可用(转调新名,触发 DeprecationWarning),新代码用新名。
def get_bit_in_byte(self, bit_index) -> bool
def set_bit_in_byte(self, bit_index, value) -> None
参数:
bit_index(int) — 位号 0-7,0是最低位(LSB);不在 0-7 抛ValueErrorvalue(bool) — 目标位的新值(仅set_bit_in_byte)
返回值:
bool— 该位当前是否为 1(get_bit_in_byte)None—set_bit_in_byte无返回值
说明:
- 位操作按字节寻址,与元素宽度无关:无论元素按 1 / 2 / 4 / 8 字节哪种类型解读,
bit_index始终指向元素偏移处的那一个字节,不会扩展到相邻字节。 get_bit_in_byte输入 / 输出方向都可读。set_bit_in_byte只用于输出方向(输入方向抛RuntimeError),走一次版本化事务,只改目标位,同一字节其余位保持不变。offset超出当前帧抛ValueError,事务提交失败抛RuntimeError。
示例:
slave.pdo.output_array[0].set_bit_in_byte(0, True) # 偏移 0 那个字节的 bit0
slave.pdo.output_array[0].set_bit_in_byte(7, False) # 同一个字节的 bit7
ready = slave.pdo.input_array[4].get_bit_in_byte(2) # 偏移 4 那个字节的 bit2 —— 与元素宽度无关
as_value() / write_value()
def as_value(self, data_type) # → int | float | bool
def write_value(self, data_type, value) # 仅输出方向
按 ESI / ENI 里那条 PDO 映射条目的数据类型读写,不用自己记宽度。
参数:
data_type(EcDataType) — 该位置的映射类型,见 EcDataTypevalue— 要写入的值(仅write_value),按目标类型换算,不要求与data_type同类
返回值:
int/float/bool— 读到的值;BOOLEAN返回bool(bit0 语义)
说明:
- 白名单恰好 11 个定宽类型,其余一律抛
ValueError—— 不猜宽度、不静默按 1 字节读(BIT8有意不落到as_uint8:位串与无符号 8 位语义不同)。 write_value的值按目标类型换算:浮点转整数就近取偶,NaN / 无穷 / 超出目标类型取值范围抛OverflowError。value=None或来源类型不受支持抛TypeError。
示例:
from darra_ethercat import EcDataType
pos = slave.pdo.input_array[2].as_value(EcDataType.INTEGER32)
flag = slave.pdo.input_array[3].as_value(EcDataType.BOOLEAN) # bit0 语义
slave.pdo.output_array[2].write_value(EcDataType.INTEGER32, 10000)
as_base_data() / write_from_base_data()
宽类型出口:as_value / write_value 只覆盖 11 个定宽类型,位串与变长字段用这一对进出。
def as_base_data(self, data_type) -> BaseData
def write_from_base_data(self, data) -> bool
参数:
data_type(EcDataType) — 该位置的映射类型;字节宽度按数据类型表取(向上取整到字节),变长 / 未知类型按 1 字节处理data(BaseData) — 要写回的整段原始字节
返回值:
BaseData— 按data_type宽度从当前位置拷出的原始字节包装bool— 是否写成功;失败返回False(不抛),输入方向固定返回False
说明:
as_base_data在偏移 / 宽度超出当前帧时抛ValueError。write_from_base_data走输出 PDO 的版本化提交,下个周期发出。- 越界、传入对象不是
BaseData、数据为空都以False返回,不抛异常。
示例:
from darra_ethercat import BaseData, EcDataType
wide = slave.pdo.input_array[0].as_base_data(EcDataType.BIT3) # 位串:定宽标量覆盖不到
ok = slave.pdo.output_array[0].write_from_base_data(wide) # 失败返回 False(不抛)
blob = BaseData(b"\x01\x02", dtype=EcDataType.UNSIGNED16) # 也可以自己构造整段写回
slave.pdo.output_array[0].write_from_base_data(blob)
inputs / outputs / write_outputs()
按字节访问过程映像整帧,用于结构体放不下的字段(字符串、变长数据)。
@property
def inputs(self) -> Optional[bytes] # 输入整帧:每次访问返回一份独占快照
@property
def outputs(self) -> Optional[bytes] # 输出整帧快照
def write_outputs(self, data) -> bool # 整帧替换
返回值:
bytes— 快照,不是 0 拷贝:同一拍一致,拷出来不跟着总线变;取不到帧返回None,该方向长度为 0 时返回b""bool— 是否写入成功(write_outputs)
说明:
- 这三个成员声明在
slave.pdo上(slave.pdo.inputs),不在slave上。 write_outputs是单写者的权威整帧替换:长度与该方向过程映像字节数不一致时拒绝返回False,且不截断。- 它是整帧写,不要拿它做局部 / 批量的读改写(那类更新用元素视图成员,走版本化事务)。
示例:
frame = slave.pdo.inputs
if frame is not None:
name = frame[6:22].split(b"\x00", 1)[0].decode("ascii", "replace")
print(f"status=0x{frame[0] | (frame[1] << 8):04X} name={name}")
out = bytearray(slave.pdo.outputs or b"")
if out:
out[0:2] = (0x000F).to_bytes(2, "little")
slave.pdo.write_outputs(bytes(out)) # 整帧替换
batch_read() / batch_write()
一次取多个区间 / 一次提交多个区间。
def batch_read(self, requests) -> list # 一份快照里解多个区间
def batch_write(self, requests) -> int # 一个事务写多个区间
参数:
requests(list) —batch_read为(offset, size)元组列表;batch_write为(offset, data)元组列表offset(int) — 字节偏移,相对该方向过程映像起点
返回值:
list—batch_read返回bytes列表,失败项为Noneint—batch_write返回成功写入的项数(整批一个事务:全部成功返回总项数,任一项失败整批返回0)
说明:
batch_read一次取一份完整输入帧快照,多个区间从同一拍解出。batch_write是一个事务、一次提交:任一项越界即整批失败,不部分提交、不静默截断。- 单字段读写走元素视图(见 input_array / output_array):读
slave.pdo.input_array[0].as_uint16,写slave.pdo.output_array[0].as_int32 = 10000;越界抛异常,不返回默认值。 - 字符串等结构体放不下的字段用整帧
inputs/outputs自己解(见上文inputs / outputs / write_outputs())。
示例:
values = slave.pdo.batch_read([(0, 2), (2, 4)]) # 一次快照,多个字段
ok = slave.pdo.batch_write([(0, (0x000F).to_bytes(2, "little")),
(2, (10000).to_bytes(4, "little"))])
完整示例
import ctypes
import time
from darra_ethercat import EcDataType, EtherCATMaster, EcState
class InPdo(ctypes.Structure):
_pack_ = 1
_fields_ = [("status_word", ctypes.c_uint16),
("actual_position", ctypes.c_int32)]
class OutPdo(ctypes.Structure):
_pack_ = 1
_fields_ = [("control_word", ctypes.c_uint16),
("target_position", ctypes.c_int32)]
master = (EtherCATMaster()
.set_eni(r"C:\EtherCAT\MyProject.deni") # DENI 一行加载
.build())
master.set_state(EcState.OP) # 进 OP
slave = master[1] # 从站(1-based 索引)
# ① 0 拷贝:映射一次,结构体钉在 live 过程映像上
inputs = slave.pdo.inputs_mapping(InPdo) # 只读视图
outputs = slave.pdo.outputs_mapping(OutPdo)
# ② 元素视图:不定义结构体,按字节偏移单点读写
print(slave.pdo.input_array[0].as_uint16) # 偏移 0 起 2 字节,小端
slave.pdo.output_array[2].write_value(EcDataType.INTEGER32, 10000)
slave.pdo.output_array[0].set_bit_in_byte(0, True) # 只改偏移 0 那个字节的 bit0
# ③ 整帧快照:同一拍一致,用来解结构体放不下的字段
frame = slave.pdo.inputs
if frame is not None:
print(f"快照 {len(frame)} 字节, status=0x{frame[0] | (frame[1] << 8):04X}")
# 周期循环:读到的永远是当前周期,写下的下个周期发出
for _ in range(1000):
outputs.control_word = 0x000F if inputs.status_word & 0x0004 else 0x0006
outputs.target_position = inputs.actual_position
time.sleep(0.001) # 你自己的节奏,例如 1 ms
print(f"状态字=0x{inputs.status_word:04X} 位置={inputs.actual_position}")
master.close()