PDO 输入输出
通过 GetIO 取过程映像指针,通过 PDOArrayFromFrame / PDOArrayItem 取过程数据元素视图。
PDO 就是过程映像里的一段数据,内核每周期自动收发。
热路径是把 GetIO 拿到的指针 reinterpret 成 packed 结构体 overlay —— 0 拷贝:
- 读写成员就是直接读写过程映像本身,不用拷贝、不用刷新。
- 读到的永远是当前周期,写下的下个周期发出。
需要按字节偏移取单个定宽标量时,用元素视图(ec_pdo_array_t / ec_pdo_data_item_t + PDOArrayItem + PDOItemRead* /
PDOItemWrite*)。
邮箱通信(SDO/CoE/FoE 等)自动附带在 PDO 周期中,不占用额外帧。邮箱响应延迟约 2 个 PDO 周期。
- 是视图,不是拷贝:视图只持
frame/size/offset/is_input四个字段,自己不存任何数据字节 —— 元素就是过程映像里那一个字节的句柄。稳态零分配,调用方通常放栈上。 - 视图不持有帧:这族函数不复制、不持有、不释放帧,帧的寿命与线程安全由调用方负责。live 切片在主站释放 / 换代后立即失效,禁止跨代缓存。
- 失败语义:元素访问器一律返回
FALSE且不改写出参 —— 不折叠成 0、不静默按 1 字节读、不报半个结果。 - 写侧只认输出:只有输出侧(RxPDO)窗口可写;对输入侧视图调用写访问器一律返回
FALSE(输入区由主站每周期覆盖,写它静默无效)。 - 索引越界与宽度越界分开报:
PDOArrayItem的字节索引越界与"元素起点处放不下该类型的宽度"是两种失败,便于定位是选错字节还是选错类型。
使用案例
日常使用
① 结构体映射(0 拷贝,持续用):进 OP 后 GetIO 取一次过程映像指针,reinterpret 成 packed
结构体指针,之后当变量读写。
uint16_t m = EcInitFromFile("C:\\EtherCAT\\MyProject.json"); /* JSON 一步初始化(缺省进 OP)*/
#pragma pack(push, 1)
typedef struct {
uint16_t status_word;
int32_t actual_position;
} in_pdo_t;
typedef struct {
uint16_t control_word;
int32_t target_position;
} out_pdo_t;
#pragma pack(pop)
int32_t out_size = 0, in_size = 0;
uint8_t *out_ptr = NULL, *in_ptr = NULL;
if (!ec_pdo_io_is_ok(GetIO(m, 1, &out_size, &out_ptr, &in_size, &in_ptr)))
return 1; /* 未进 OP 或过程映像不可用 */
if ((size_t)in_size < sizeof(in_pdo_t) || (size_t)out_size < sizeof(out_pdo_t))
return 1; /* 结构体比 IO 区大:会踩到别的从站 */
in_pdo_t *in = (in_pdo_t *)in_ptr; /* 钉在过程映像上(0 拷贝) */
out_pdo_t *out = (out_pdo_t *)out_ptr;
out->control_word = 0x000F; /* 写:下个周期发出 */
out->target_position = in->actual_position + 1000; /* 读:当前周期 */
② 元素视图(按偏移读一个定宽值):先取一份输入帧快照,再在快照上建数组视图、取元素视图。
uint8_t frame[64];
uint32_t copied = 0, required = 0;
uint8_t st = CopySlaveInputPdo(m, 1, frame, (uint32_t)sizeof frame, &copied, &required);
if (ec_pdo_io_is_ok(st)) {
ec_pdo_array_t arr;
ec_pdo_data_item_t item;
uint16_t sw = 0;
if (PDOArrayFromFrame(frame, (int32_t)copied, TRUE, &arr) /* TRUE = 输入侧:只读 */
&& PDOArrayItem(&arr, 0, &item) /* 偏移 0 处的元素视图 */
&& PDOItemReadUInt16(&item, &sw)) /* 小端 2 字节 */
printf("status=0x%04X\n", sw);
} else if (ec_pdo_io_is_buffer_too_small(st)) {
printf("输入帧 %u 字节,缓冲区扩容后重试\n", required);
}
③ 元素视图写输出:输出侧不能对 live 切片做局部读改写,走版本化事务(Copy → patch → Commit)。
uint8_t out_frame[64]; /* 缓冲不足时按 required 给出的完整帧长扩容 */
uint32_t copied = 0, required = 0;
uint64_t ticket = 0, deadline = 0, token = 0; /* 首次 Begin:票据 / 截止时间传 0 由 Core 签发 */
uint8_t st = CopySlaveOutputPdoVersioned(m, 1, out_frame, (uint32_t)sizeof out_frame,
&copied, &required, &ticket, &deadline, &token);
if (ec_pdo_io_is_ok(st)) {
ec_pdo_array_t arr;
ec_pdo_data_item_t item;
if (PDOArrayFromFrame(out_frame, (int32_t)copied, FALSE, &arr) /* FALSE = 输出侧:可写 */
&& PDOArrayItem(&arr, 2, &item)) {
if (!PDOItemWriteInt16(&item, 1500)) /* patch:只改帧缓冲,还没上总线 */
printf("patch 被拒:该元素放不下 2 字节\n");
}
uint64_t current = 0;
uint32_t written = 0, need = 0;
if (!ec_pdo_io_is_ok(CommitSlaveOutputPdoVersioned(m, 1, out_frame, copied, token,
ticket, deadline,
¤t, &written, &need)))
printf("提交失败:written=%u required=%u\n", written, need);
}
周期变化
映射取一次就够,同一份映射放进你自己的周期循环直接用:
while (running) {
out->control_word = (in->status_word & 0x0004) ? 0x000F : 0x0006;
out->target_position = in->actual_position;
wait_next_tick(); /* 你自己的节奏,例如 1 ms */
}
内核每周期搬运过程数据:循环里读到的永远是当前周期、写下的下个周期发出,不需要任何额外调用,也不要每拍重取
GetIO。Dispose / 主站换代 / 重新进 OP 后旧映射才作废,重进 OP 时重取一次即可。
元素视图反过来 —— 它绑的是你交给它的那段窗口:快照帧不会自己变,要新的一拍必须重新取帧;live 切片指向的就是过程映像本身,值跟着总线变,不必每拍重取,但释放 / 换代后必须重取。
过程映像访问
GetIO()
uint8_t GetIO(uint16_t master_index, uint16_t slave_index,
int32_t* out_byte_size, uint8_t** out_byte,
int32_t* in_byte_size, uint8_t** in_byte);
取从站的 live IOmap 指针:out_byte 是输出过程映像(写进去即下发),in_byte 是输入过程映像(总线刷新即更新)。这是 0
拷贝热路径的入口,配合 packed 结构体 overlay 使用。
参数:
master_index(uint16_t) — 主站索引,1-basedslave_index(uint16_t) — 从站索引,1-basedout_byte_size(int32_t*) — 出参,接收输出过程映像字节数out_byte(uint8_t**) — 出参,接收输出过程映像基址in_byte_size(int32_t*) — 出参,接收输入过程映像字节数in_byte(uint8_t**) — 出参,接收输入过程映像基址
返回值:
uint8_t— native 状态,成功是0x00;一律用ec_pdo_io_is_ok()判定,不要拿EC_PDO_IO_*名去比
说明:
- 失败(状态非成功或 shell 未就绪导致指针为
NULL)时绝不要解引用out_byte/in_byte。 - 拿到指针后按
*_byte_size核对结构体大小再 overlay。 - 指针钉在 IOmap 上,主站释放 / 换代后失效。
- 输入区只读:
in_byte指向的 I 区由主站每周期从从站覆盖,写它静默无效(下一周期即被冲掉)。只读消费(解码 / 判断 / 转发)用GetIORO,出参是const uint8_t**,类型上封住写口;写过程数据只走输出侧(out_byte/PDOSliceOutputPtr/WriteSlaveOutputPdo/ 版本化事务)。
示例:
int32_t out_size = 0, in_size = 0;
uint8_t *out_ptr = NULL, *in_ptr = NULL;
if (ec_pdo_io_is_ok(GetIO(m, 1, &out_size, &out_ptr, &in_size, &in_ptr))
&& out_ptr && in_ptr && in_size >= 2 && out_size >= 1) {
uint16_t sw = (uint16_t)(in_ptr[0] | (in_ptr[1] << 8)); /* 读输入:当前周期,小端 */
out_ptr[0] = 0x0F; /* 写输出:下个周期发出 */
printf("sw=0x%04X\n", sw);
}
ec_pdo_io_is_ok() / ec_pdo_io_is_buffer_too_small()
BOOL ec_pdo_io_is_ok(uint8_t status);
BOOL ec_pdo_io_is_buffer_too_small(uint8_t status);
过程数据状态码的唯一正确判据。整帧函数与 GetIO 的成功码是同一个 native 码 0x00;两个助手只是额外接受合同码(1
/ 2),判定一律只走它们。
参数:
status(uint8_t) —GetIO或整帧函数的返回值
返回值:
BOOL—ec_pdo_io_is_ok:native 成功码0x00或合同码1都判成功;ec_pdo_io_is_buffer_too_small:native0xFD或合同码2都判缓冲不足
说明:
- 缓冲不足时不会复制半帧,
required_size给出完整帧长度,按它扩容后重试即可。 - 运行期交回来的只有 native 码:成功
0x00,失败类0xFA..0xFF。1..6是ec_pdo_io_status_t(master/core.h)的合同取值,不要拿EC_PDO_IO_*名去比原始status——EC_PDO_IO_OK判不到 native 成功,而EC_PDO_IO_FAILED又与 native 成功同值0。 - 其它状态(长度不匹配、版本冲突、generation 已失效、IOmap 正忙)都是失败。
示例:
uint32_t copied = 0, required = 0;
uint8_t st = CopySlaveInputPdo(m, 1, frame, (uint32_t)sizeof frame, &copied, &required);
if (ec_pdo_io_is_ok(st))
printf("这一拍 %u 字节\n", copied);
else if (ec_pdo_io_is_buffer_too_small(st))
printf("需要 %u 字节的缓冲\n", required);
PDOSliceInputPtr()
BOOL PDOSliceInputPtr(uint16_t master_index, uint16_t slave_index,
int32_t offset, int32_t size,
const uint8_t** slice);
在 live 输入过程映像的字节偏移 offset 处取 size 字节的只读切片视图(0 拷贝)。
参数:
master_index(uint16_t) — 主站索引,1-basedslave_index(uint16_t) — 从站索引,1-basedoffset(int32_t) — 相对该从站输入过程映像起点的字节偏移size(int32_t) — 切片字节数,必须> 0slice(const uint8_t**) — 出参,接收切片基址
返回值:
BOOL— 成功TRUE- 越界(
offset < 0、size <= 0、offset + size超出该方向映像)或映像不可用返回FALSE,且*slice置NULL—— 绝不返回指向映像外的指针
- 越界(
说明: 返回的是 const 视图:输入区由主站每周期从从站覆盖,写它静默无效,要写过程数据走输出侧。
PDOSliceInputPtr 给的是 const uint8_t*。
要在它上面建元素视图需要显式去掉 const,并且只能用于读取 —— 视图 is_input = TRUE 会让所有写访问器返回
FALSE,只读约束仍由 SDK 强制。
示例:
const uint8_t* slice = NULL;
if (PDOSliceInputPtr(m, 1, 0, 4, &slice)) {
ec_pdo_array_t arr;
ec_pdo_data_item_t item;
uint32_t value = 0;
if (PDOArrayFromFrame((uint8_t *)(uintptr_t)slice, 4, TRUE, &arr)
&& PDOArrayItem(&arr, 0, &item)
&& PDOItemReadUInt32(&item, &value))
printf("value=%u\n", value); /* 每次访问都读当前映像 */
}
PDOSliceOutputPtr()
BOOL PDOSliceOutputPtr(uint16_t master_index, uint16_t slave_index,
int32_t offset, int32_t size,
uint8_t** slice);
在 live 输出过程映像的字节偏移 offset 处取 size 字节切片视图,改写即写回。越界判据与 PDOSliceInputPtr 相同。
参数:
master_index(uint16_t) — 主站索引,1-basedslave_index(uint16_t) — 从站索引,1-basedoffset(int32_t) — 相对该从站输出过程映像起点的字节偏移size(int32_t) — 切片字节数,必须> 0slice(uint8_t**) — 出参,接收切片基址
返回值:
BOOL— 成功TRUE;越界或映像不可用返回FALSE,且*slice置NULL
说明: 这是一个活的写窗口,适合整段 overwrite。禁止对 live 输出切片做局部读改写(RMW) —— 切片内的部分覆盖 / 位操作必须走版本化事务。
示例:
uint8_t* out_slice = NULL;
if (PDOSliceOutputPtr(m, 1, 0, 2, &out_slice)) {
ec_pdo_array_t arr;
ec_pdo_data_item_t item;
if (PDOArrayFromFrame(out_slice, 2, FALSE, &arr)
&& PDOArrayItem(&arr, 0, &item)
&& PDOItemWriteUInt16(&item, 0x000F))
printf("已写入输出切片\n");
}
帧来源与新鲜度
元素视图绑在调用方持有的一段过程映像字节窗口上。窗口有三种来源,元素访问器对三者一视同仁 —— 它只认窗口,不认来源:
- 快照帧(推荐):
CopySlaveInputPdo/CopySlaveOutputPdo拷出的整帧。这是 generation-scoped 的一致读:一次成功调用给出的整帧自洽,缓冲不足时不会复制半帧。 - 版本化输出帧:
CopySlaveOutputPdoVersioned取的帧,patch 之后必须由CommitSlaveOutputPdoVersioned提交,中间那一段只在你的缓冲里,没上总线。 - live 切片:
PDOSliceInputPtr/PDOSliceOutputPtr指向的过程映像本身,0 拷贝,读到的是"访问那一刻"的值。
新鲜度由窗口决定,不由访问器决定。 SDK 不替你取快照:一次访问器调用只是从绑定窗口上取一次值。
- 视图绑在快照帧上时,视图生命期内所有读都是同一帧 —— 多字段天然一致,但它不会自己变,要新的一拍必须重新取帧。
- 视图绑在 live 切片上时,每次访问器调用都直接读当前映像,因此同一个表达式里的两次访问可能落在两个不同的总线周期(例如把两个元素的读结果写进一个表达式,两次读之间总线可能已经刷新了一轮)。
需要一拍之内多字段互相自洽的视图,就用 CopySlaveInputPdo 取整帧快照,在这块缓冲上建数组视图;或者用 GetIO 的
packed 结构体 overlay,一次读完整个方向。
元素访问器不是 0 拷贝热路径。
0 拷贝读过程映像的入口是 GetIO(packed 结构体 overlay)与 PDOSliceInputPtr / PDOSliceOutputPtr(按偏移取切片)。
元素访问器是"按偏移解一个定宽标量"的工具:它本身不复制字节,但也不替你保证一致,读的就是你交给它的那段窗口。
需要对 live 映像做安全的整体视图,用 overlay,不要靠元素访问器逐个字段拼。
整帧快照与版本化事务
CopySlaveInputPdo() / CopySlaveOutputPdo()
uint8_t CopySlaveInputPdo(uint16_t master_index, uint16_t slave_index,
uint8_t* buffer, uint32_t buffer_size,
uint32_t* copied_size, uint32_t* required_size);
uint8_t CopySlaveOutputPdo(uint16_t master_index, uint16_t slave_index,
uint8_t* buffer, uint32_t buffer_size,
uint32_t* copied_size, uint32_t* required_size);
把一整个方向的 PDO 帧拷进你自己的缓冲,得到一份同一代、自洽的快照。
参数:
master_index(uint16_t) — 主站索引,1-basedslave_index(uint16_t) — 从站索引,1-basedbuffer(uint8_t*) — 调用方的接收缓冲buffer_size(uint32_t) — 缓冲字节数copied_size(uint32_t*) — 出参,实际拷贝的字节数required_size(uint32_t*) — 出参,完整帧长度(缓冲不足时按它扩容)
返回值:
uint8_t— 状态码,用ec_pdo_io_is_ok()/ec_pdo_io_is_buffer_too_small()判定
说明: 缓冲不足时不复制任何字节,required_size
给出权威长度。快照是你的内存,拷出来之后不跟着总线变,可以慢慢解析、跨线程传阅;这是"要一致的多字段视图"时的首选帧来源。
示例:
uint8_t frame[128];
uint32_t copied = 0, required = 0;
if (ec_pdo_io_is_ok(CopySlaveInputPdo(m, 1, frame, (uint32_t)sizeof frame, &copied, &required))) {
ec_pdo_array_t arr;
if (PDOArrayFromFrame(frame, (int32_t)copied, TRUE, &arr))
printf("这一拍 %d 字节的视图就绪\n", PDOArrayLength(&arr));
}
WriteSlaveOutputPdo()
uint8_t WriteSlaveOutputPdo(uint16_t master_index, uint16_t slave_index,
const uint8_t* data, uint32_t size,
uint32_t* written_size, uint32_t* required_size);
用手里的完整 RxPDO 帧整帧精确替换该从站的输出过程映像(单一实时 owner 的整帧写入口)。
参数:
master_index(uint16_t) — 主站索引,1-basedslave_index(uint16_t) — 从站索引,1-baseddata(const uint8_t*) — 要写入的完整输出帧size(uint32_t) — 帧字节数,必须等于当前完整帧长度written_size(uint32_t*) — 出参,实际写入的字节数required_size(uint32_t*) — 出参,当前的完整帧长度
返回值:
uint8_t— 状态码,用ec_pdo_io_is_ok()判定;size与当前完整帧长度不匹配时返回长度不匹配状态且不写任何字节
说明: 定长整帧替换,不做读改写 —— 只改其中几个字段时走版本化事务。
示例:
uint8_t frame[64]; /* 完整输出帧,长度须与当前帧一致 */
uint32_t written = 0, required = 0;
if (!ec_pdo_io_is_ok(WriteSlaveOutputPdo(m, 1, frame, (uint32_t)sizeof frame,
&written, &required)))
printf("写入被拒:written=%u required=%u\n", written, required);
CopySlaveOutputPdoVersioned() / CommitSlaveOutputPdoVersioned()
uint8_t CopySlaveOutputPdoVersioned(uint16_t master_index, uint16_t slave_index,
uint8_t* buffer, uint32_t buffer_size,
uint32_t* copied_size, uint32_t* required_size,
uint64_t* transaction_ticket,
uint64_t* native_deadline_us,
uint64_t* version_token);
uint8_t CommitSlaveOutputPdoVersioned(uint16_t master_index, uint16_t slave_index,
const uint8_t* data, uint32_t size,
uint64_t expected_token,
uint64_t transaction_ticket,
uint64_t native_deadline_us,
uint64_t* current_token,
uint32_t* written_size,
uint32_t* required_size);
乐观并发的 RxPDO 事务:Begin 取一份输出帧并带回版本令牌,在本地缓冲上 patch,再 Commit 提交。这是局部 / 类型化 / 位 / 批量读改写唯一的合法入口。
参数:
master_index(uint16_t) — 主站索引,1-basedslave_index(uint16_t) — 从站索引,1-basedbuffer(uint8_t*) /data(const uint8_t*) — Begin 的接收缓冲 / Commit 要提交的帧buffer_size(uint32_t) /size(uint32_t) — 缓冲字节数 / 提交帧长度copied_size(uint32_t*) /written_size(uint32_t*) — 出参,实际拷贝 / 实际写入字节数required_size(uint32_t*) — 出参,完整帧长度transaction_ticket(uint64_t*) — 出参(Begin)/ 传入(Commit),Core 签发的不透明事务票据native_deadline_us(uint64_t*) — 出参(Begin)/ 传入(Commit),Core 单调时钟的绝对截止值version_token(uint64_t*) — 出参(Begin)/ 经expected_token传入(Commit)- 高 32 位是 context generation,低 32 位是该从站输出 revision,两者都不为 0
current_token(uint64_t*) — 出参,权威的当前版本令牌
返回值:
uint8_t— 状态码,用ec_pdo_io_is_ok()判定;冲突 / 已失效 / 长度不匹配时written_size为 0 并返回权威的current_token/required_size
说明:
- 第一次 Begin 把
transaction_ticket与native_deadline_us都传 0,Core 同时签发非零票据并设置now + 5000 us的绝对截止时间。 - 同一笔读改写的尺寸查询、重新 Begin、失败重试与 Commit 必须原样复用这两个值,禁止清零续期或伪造。
- 只有 Core 可以裁决已产生副作用的 Commit 是否超时;Commit 返回成功后不得再由调用方本地时钟改判失败。
- 整帧精确替换(单一实时 owner 已持有完整权威 RxPDO)→
WriteSlaveOutputPdo;size与当前完整帧长度不匹配时返回长度不匹配状态且不写任何字节。 - 局部 / 类型化 / 位 / 批量读改写 → 本节这对版本化事务。
WriteSlaveOutput/WriteSlaveOutputByte/PDOWriteDirect/PDOBatchWrite都是退场入口:- 固定 fail-closed,不写任何输出。
- 前两个与真入口
WriteSlaveOutputPdo只差词尾。
- 读侧孪生
PDOReadDirect/PDOBatchRead是活入口:只把调用方缓冲填成一整帧输入快照,不写任何输出,不在退场之列。
示例:
uint64_t ticket = 0, deadline = 0, token = 0;
uint64_t current = 0;
uint32_t copied = 0, required = 0, written = 0, need = 0;
if (ec_pdo_io_is_ok(CopySlaveOutputPdoVersioned(m, 1, frame, (uint32_t)sizeof frame,
&copied, &required, &ticket, &deadline, &token))) {
/* 在 frame 上按元素视图 patch(见"元素视图"一节)... */
if (!ec_pdo_io_is_ok(CommitSlaveOutputPdoVersioned(m, 1, frame, copied, token,
ticket, deadline,
¤t, &written, &need)))
printf("提交失败:current=0x%llX written=%u required=%u\n",
(unsigned long long)current, written, need);
}
元素视图
PDOArrayFromFrame()
BOOL PDOArrayFromFrame(uint8_t* frame, int32_t size, BOOL is_input, ec_pdo_array_t* array);
在调用方持有的过程映像字节窗口上建立数组视图。本函数不复制、不持有、不释放帧。
参数:
frame(uint8_t*) — 窗口基址(快照帧缓冲或 live 切片),调用方持有size(int32_t) — 窗口可用字节数 = 本方向过程映像字节数;未进 OP / 未映射传0,此时任何索引都越界is_input(BOOL) —TRUE= 输入侧(TxPDO,只读);FALSE= 输出侧(RxPDO,可写)array(ec_pdo_array_t*) — 出参,接收数组视图
返回值:
BOOL— 成功TRUE;frame/array为NULL或size < 0返回FALSE
示例:
ec_pdo_array_t arr;
if (PDOArrayFromFrame(frame, (int32_t)copied, TRUE, &arr))
printf("输入过程映像 %d 字节\n", PDOArrayLength(&arr));
相关结构:
typedef struct {
uint8_t* frame; /* 窗口基址 (调用方持有; 不复制、不释放) */
int32_t size; /* 窗口可用字节数 = 本方向过程映像字节数; 未进 OP = 0 */
BOOL is_input; /* TRUE = 输入侧 (TxPDO, 只读) / FALSE = 输出侧 (RxPDO) */
} ec_pdo_array_t;
PDOArrayLength()
int32_t PDOArrayLength(const ec_pdo_array_t* array);
本方向过程映像的字节数。
参数:
array(const ec_pdo_array_t*) — 数组视图
返回值:
int32_t— 字节数;array为NULL、或未进 OP / 未映射时为 0
示例:
for (int32_t i = 0; i < PDOArrayLength(&arr); i++) {
ec_pdo_data_item_t item;
if (PDOArrayItem(&arr, i, &item))
printf("偏移 %d 可访问\n", i);
}
PDOArrayItem()
BOOL PDOArrayItem(const ec_pdo_array_t* array, int32_t index, ec_pdo_data_item_t* item);
按字节偏移取元素视图,index 相对本方向过程映像起点、从 0 起。元素视图只持句柄,不持数据字节。
参数:
array(const ec_pdo_array_t*) — 数组视图index(int32_t) — 字节偏移,取值范围[0, PDOArrayLength(array))item(ec_pdo_data_item_t*) — 出参,接收元素视图
返回值:
BOOL— 成功TRUE;array/item为NULL、窗口基址为NULL、或index越界返回FALSE
说明: 这里拒绝的是"字节偏移越界"。取到元素视图后,访问器还会再判一次"该宽度在这个元素起点放不下",两种失败刻意分开报。
示例:
ec_pdo_data_item_t item;
if (PDOArrayItem(&arr, 6, &item))
printf("偏移 6 的元素视图就绪\n");
相关结构:
typedef struct {
uint8_t* frame; /* 窗口基址 (同 ec_pdo_array_t) */
int32_t size; /* 窗口可用字节数 */
int32_t offset; /* 元素字节偏移 (相对 frame) */
BOOL is_input; /* TRUE = 输入侧 (TxPDO, 只读) */
} ec_pdo_data_item_t;
元素访问器
偏移式单标量接口(PdoRead* / PdoWrite* / PDOReadInput* / PDOWriteOutput*)的头内包装已从本 SDK 删除。
对应的薄壳 DarraEcat_* 导出仍在内部 ABI 里,序号不平移,不属公开入口,不要直调。
按偏移取单个值请统一走元素视图(PDOArrayItem + PDOItemRead* / PDOItemWrite*)。
PDOItemReadContent() / PDOItemWriteContent()
BOOL PDOItemReadContent (const ec_pdo_data_item_t* item, uint8_t* value);
BOOL PDOItemWriteContent(const ec_pdo_data_item_t* item, uint8_t value);
元素第一个字节的原始值(1 字节,无符号)。PDOItemReadUInt8 / PDOItemWriteUInt8 与它同义同字节。
参数:
item(const ec_pdo_data_item_t*) — 元素视图value(uint8_t* / uint8_t) — 读:出参,失败时不改写;写:要写入的字节
返回值:
BOOL— 成功TRUE;value为NULL、偏移越界、或输入侧视图(写)返回FALSE
示例:
uint8_t raw = 0;
if (PDOItemReadContent(&item, &raw))
printf("byte=0x%02X\n", raw);
if (!PDOItemWriteContent(&out_item, 0x0F)) /* 整字节覆盖,仅输出侧 */
printf("写入被拒:输入侧视图或偏移越界\n");
类型化读取(PDOItemRead*())
BOOL PDOItemReadInt8 (const ec_pdo_data_item_t* item, int8_t* value);
BOOL PDOItemReadUInt8 (const ec_pdo_data_item_t* item, uint8_t* value);
BOOL PDOItemReadInt16 (const ec_pdo_data_item_t* item, int16_t* value);
BOOL PDOItemReadUInt16(const ec_pdo_data_item_t* item, uint16_t* value);
BOOL PDOItemReadInt32 (const ec_pdo_data_item_t* item, int32_t* value);
BOOL PDOItemReadUInt32(const ec_pdo_data_item_t* item, uint32_t* value);
BOOL PDOItemReadInt64 (const ec_pdo_data_item_t* item, int64_t* value);
BOOL PDOItemReadUInt64(const ec_pdo_data_item_t* item, uint64_t* value);
BOOL PDOItemReadFloat (const ec_pdo_data_item_t* item, float* value);
BOOL PDOItemReadDouble(const ec_pdo_data_item_t* item, double* value);
从元素偏移起按定宽读一个标量,读侧对任何方向的视图都合法。
参数:
item(const ec_pdo_data_item_t*) — 元素视图value(int8_t* … double*) — 出参,接收读出的值;失败时不改写
返回值:
BOOL— 成功TRUE;value为NULL、元素窗口放不下该宽度、或偏移越界返回FALSE
说明: 多字节一律小端(EtherCAT 线序),本 SDK 不做任何字节颠倒。PDOItemReadInt8 按 8 bit
做二补数符号扩展;浮点是 IEEE754。
示例:
int16_t pos = 0;
uint16_t sw = 0;
if (PDOItemReadInt16(&item, &pos))
printf("pos=%d\n", pos); /* 元素偏移起 2 字节,小端 */
if (!PDOItemReadUInt16(&item, &sw))
printf("该元素放不下 2 字节\n"); /* 例如已是最后一个字节 */
类型化写入(PDOItemWrite*())
BOOL PDOItemWriteInt8 (const ec_pdo_data_item_t* item, int8_t value);
BOOL PDOItemWriteUInt8 (const ec_pdo_data_item_t* item, uint8_t value);
BOOL PDOItemWriteInt16 (const ec_pdo_data_item_t* item, int16_t value);
BOOL PDOItemWriteUInt16(const ec_pdo_data_item_t* item, uint16_t value);
BOOL PDOItemWriteInt32 (const ec_pdo_data_item_t* item, int32_t value);
BOOL PDOItemWriteUInt32(const ec_pdo_data_item_t* item, uint32_t value);
BOOL PDOItemWriteInt64 (const ec_pdo_data_item_t* item, int64_t value);
BOOL PDOItemWriteUInt64(const ec_pdo_data_item_t* item, uint64_t value);
BOOL PDOItemWriteFloat (const ec_pdo_data_item_t* item, float value);
BOOL PDOItemWriteDouble(const ec_pdo_data_item_t* item, double value);
把值按定宽小端写进元素所在字节。仅输出侧:输入侧视图一律返回 FALSE 且不改写任何字节。
参数:
item(const ec_pdo_data_item_t*) — 元素视图,必须来自输出侧窗口value(int8_t … double) — 要写入的值
返回值:
BOOL— 成功TRUE;输入侧视图、元素窗口放不下该宽度、或偏移越界返回FALSE
说明: 对 live
输出切片做部分覆盖(含位操作)属于局部读改写,不允许;整段覆盖整个切片可以。其余写要落在版本化事务取出的帧缓冲上,patch
完由 CommitSlaveOutputPdoVersioned 提交。
示例:
/* arr 建在版本化事务取出的输出帧上(is_input = FALSE) */
ec_pdo_data_item_t item;
if (PDOArrayItem(&arr, 2, &item)) {
if (!PDOItemWriteInt16(&item, 1500)) /* patch 帧缓冲 */
printf("写入被拒\n");
}
/* patch 完成,随后 Commit */
PDOItemReadBool() / PDOItemWriteBool()
BOOL PDOItemReadBool (const ec_pdo_data_item_t* item, BOOL* value);
BOOL PDOItemWriteBool(const ec_pdo_data_item_t* item, BOOL value);
整字节布尔访问器,语义是 bit0:读 (byte & 0x01) != 0,写 TRUE = 0x01 / FALSE =
0x00(整字节覆盖,不是位操作)。
参数:
item(const ec_pdo_data_item_t*) — 元素视图value(BOOL* / BOOL) — 读:出参,失败时不改写;写:要写入的布尔值
返回值:
BOOL— 成功TRUE;value为NULL、偏移越界、或输入侧视图(写)返回FALSE
说明:
- 布尔访问器只认最低位:字节
0x00读FALSE、0x01读TRUE,而0x02与0x80都读FALSE。 - 这不是"非 0 即真",很多人第一次会踩。
- 要按整字节 0 / 非 0 判真假,用
PDOItemReadContent取出原始字节自行比较 —— 本 SDK 不提供第二个"非 0 即真"的布尔访问器。
示例:
BOOL enable = FALSE;
if (PDOItemReadBool(&item, &enable))
printf("bit0 = %s\n", enable ? "true" : "false"); /* 字节 0x02 / 0x80 也是 false */
if (!PDOItemWriteBool(&out_item, TRUE)) /* 整字节写 0x01 */
printf("写入被拒\n");
PDOItemReadBit() / PDOItemWriteBit()
BOOL PDOItemReadBit (const ec_pdo_data_item_t* item, int bit_index, BOOL* value);
BOOL PDOItemWriteBit(const ec_pdo_data_item_t* item, int bit_index, BOOL value);
读 / 写本元素字节的第 bit_index 位,bit_index 取 0..7,0 = LSB。
参数:
item(const ec_pdo_data_item_t*) — 元素视图bit_index(int) — 位号0..7,0 = LSBvalue(BOOL* / BOOL) — 读:出参,失败时不改写;写:要写入的位
返回值:
BOOL— 成功TRUE;bit_index不在0..7、偏移越界、或输入侧视图(写)返回FALSE
说明:
- 位号是按字节而不是按元素宽度寻址:无论元素有多宽,
0..7都指向元素偏移处的那一个字节(多字节元素的第 0 字节就是小端低字节),也不会跨到下一个字节。 - 写在输出侧,并且只覆盖目标位 —— 同字节其余位保持原值。
示例:
BOOL ready = FALSE;
if (PDOItemReadBit(&item, 0, &ready)) /* 元素字节的 bit0 */
printf("ready=%d\n", ready);
if (!PDOItemWriteBit(&out_item, 0, TRUE)) /* 只置 bit0,同字节其余位不动 */
printf("写入被拒\n");
if (!PDOItemWriteBit(&out_item, 8, TRUE)) /* 位号越界:返回 FALSE,不改任何字节 */
printf("位号必须在 0..7\n");
PDOItemReadValue() / PDOItemWriteValue()
BOOL PDOItemReadValue (const ec_pdo_data_item_t* item,
ec_pdo_datatype_t type, ec_pdo_value_t* value);
BOOL PDOItemWriteValue(const ec_pdo_data_item_t* item,
ec_pdo_datatype_t type, const ec_pdo_value_t* value);
按 EtherCAT 数据类型读 / 写元素 —— 数据类型要到运行期才知道(例如按 ESI 映射表驱动)时的入口。
参数:
item(const ec_pdo_data_item_t*) — 元素视图type(ec_pdo_datatype_t) —EC_DT_*码值value(ec_pdo_value_t* / const ec_pdo_value_t*) — 读:出参,按type取对应成员,失败时不改写;写:按type读对应成员
返回值:
BOOL— 成功TRUE;type不在白名单、value为NULL、偏移 / 宽度越界、或输入侧视图(写)返回FALSE
说明:
- 白名单恰好 11 个:
EC_DT_BOOL、INT8、INT16、INT32、INT64、UINT8UINT16、UINT32、UINT64、REAL32、REAL64(多字节小端)
- 白名单之外一律返回
FALSE:EC_DT_BIT1..EC_DT_BIT8(含EC_DT_BIT8—— 位串与EC_DT_UINT8语义不同,有意不映射)EC_DT_INT24/EC_DT_INT40/EC_DT_INT48/EC_DT_INT56(以及对应的EC_DT_UINT24/EC_DT_UINT40/EC_DT_UINT48/EC_DT_UINT56)- 字符串(
EC_DT_VISIBLE_STRING/EC_DT_OCTET_STRING/EC_DT_UNICODE_STRING) EC_DT_TIME_OF_DAY/EC_DT_TIME_DIFFERENCE/EC_DT_DOMAIN/EC_DT_NONE
- 不猜宽度、不静默按 1 字节读。
联合体没有运行期类型标签,成员必须与 type 匹配:读 EC_DT_INT16 就取 i16,写 EC_DT_REAL32 就填 f32。
boolean 成员读时是 bit0 语义;写时按布尔取真值,落到总线上仍是整字节 0x01 / 0x00。
示例:
ec_pdo_value_t v;
if (PDOItemReadValue(&item, EC_DT_INT32, &v))
printf("i32=%d\n", v.i32);
v.f32 = 12.5f;
if (!PDOItemWriteValue(&out_item, EC_DT_REAL32, &v))
printf("写入被拒\n");
相关结构:
typedef union {
BOOL boolean; /* EC_DT_BOOL, bit0 语义 */
int8_t i8; /* EC_DT_INT8 */
uint8_t u8; /* EC_DT_UINT8 */
int16_t i16; /* EC_DT_INT16 */
uint16_t u16; /* EC_DT_UINT16 */
int32_t i32; /* EC_DT_INT32 */
uint32_t u32; /* EC_DT_UINT32 */
int64_t i64; /* EC_DT_INT64 */
uint64_t u64; /* EC_DT_UINT64 */
float f32; /* EC_DT_REAL32 */
double f64; /* EC_DT_REAL64 */
} ec_pdo_value_t;
位串与变长类型(ec_value_*)
位串(EC_DT_BIT1..EC_DT_BIT8)没有定宽标量访问器。直接对元素窗口调 ec_value_*,宽度取
ec_value_type_bit_size(1..8):
int bits = ec_value_type_bit_size(EC_DT_BIT3); /* 3 */
uint64_t raw = 0;
if (ec_value_try_extract_u64(item.frame, item.size,
ec_pdo_item_bit_offset(&item), bits, &raw))
printf("bit3=%llu\n", (unsigned long long)raw);
字符串 / EC_DT_DOMAIN 是变长类型:ec_value_type_bit_size 对它们返回 -1(不是可用宽度),ec_value_* 不适用。
这类字段用 CopySlaveInputPdo 取整帧快照后按字节自行解析。
ec_pdo_item_bit_offset(&item) 给出元素起点的 bit 偏移(字节偏移 × 8);它不做 NULL 检查,item 必须先由
PDOArrayItem 取到手。
写侧用 ec_value_insert_u64 / ec_value_insert_i64(返回 EC_VALUE_OK 表示成功),位域写入是读改写,区外位保持原值。
ec_value_* 不做方向检查:它不会替你拒绝输入侧窗口。只在对输出帧缓冲(版本化事务取出的帧)写入时才用它们,且写入发生在
CommitSlaveOutputPdoVersioned 之前。
完整示例
#include <stdio.h>
#include <stdint.h>
#include <ethercat.h>
#pragma pack(push, 1)
typedef struct { uint16_t status_word; int32_t actual_position; } in_pdo_t;
typedef struct { uint16_t control_word; int32_t target_position; } out_pdo_t;
#pragma pack(pop)
int main(void)
{
/* JSON 一步初始化:加载 + 进 OP */
uint16_t m = EcInitFromFile("C:\\EtherCAT\\MyProject.json");
if (m == 0) return 1;
/* ① 0 拷贝热路径:packed 结构体钉在过程映像上 */
int32_t out_size = 0, in_size = 0;
uint8_t *out_ptr = NULL, *in_ptr = NULL;
if (!ec_pdo_io_is_ok(GetIO(m, 1, &out_size, &out_ptr, &in_size, &in_ptr)))
return 1; /* 未进 OP 或过程映像不可用 */
if ((size_t)in_size < sizeof(in_pdo_t) || (size_t)out_size < sizeof(out_pdo_t))
return 1; /* 结构体比 IO 区大:会踩到别的从站 */
in_pdo_t *in = (in_pdo_t *)in_ptr;
out_pdo_t *out = (out_pdo_t *)out_ptr;
out->control_word = 0x000F; /* 写:下个周期发出 */
out->target_position = in->actual_position + 1000;
printf("status=0x%04X pos=%d\n", in->status_word, in->actual_position);
/* ② 元素视图:先取一拍输入快照,再按偏移读单个定宽标量 */
uint8_t frame[64];
uint32_t copied = 0, required = 0;
uint8_t st = CopySlaveInputPdo(m, 1, frame, (uint32_t)sizeof frame, &copied, &required);
if (ec_pdo_io_is_ok(st)) {
ec_pdo_array_t arr;
ec_pdo_data_item_t item;
uint16_t sw = 0;
BOOL ready = FALSE;
if (PDOArrayFromFrame(frame, (int32_t)copied, TRUE, &arr) /* 快照帧是输入侧:只读 */
&& PDOArrayItem(&arr, 0, &item)) {
if (PDOItemReadUInt16(&item, &sw)) /* 小端 2 字节 */
printf("快照 status=0x%04X\n", sw);
if (PDOItemReadBit(&item, 0, &ready)) /* 同一字节的 bit0 */
printf("ready=%d\n", ready);
}
/* 需要按偏移局部写输出时走版本化事务,见"使用案例 ③" */
} else if (ec_pdo_io_is_buffer_too_small(st)) {
printf("输入帧 %u 字节,缓冲区扩容后重试\n", required);
}
getchar();
EcClose(m); /* 一步关闭:Stop + Dispose */
return 0;
}