跳到主要内容

C 特有语法糖 (C99+)

C 没有 OOP 语言那种类、扩展方法、属性、模式匹配, 但通过宏 + inline + GCC/Clang 扩展, 仍可以让 EtherCAT SDK 客户代码写得相当紧凑、可读。 这一节把 DarraEtherCAT C SDK 提供的全部 sugar 整理在一起, 供 C/C++ 工程师对照查阅。

DarraEtherCAT C SDK 在 include/sugar/ 下提供 7 个独立 helper 头文件, 全部为纯 inline + macro, 不引入额外链接符号、不增加运行时成本。 用户可直接引入伞形头:

#include "ethercat.h"
#include "sugar/sugar.h"

也可按需挑选单个:

#include "sugar/foreach.h"
#include "sugar/try_macro.h"
子头主题C 标准要求
sugar/foreach.h遍历宏C99
sugar/try_macro.h错误处理宏C89
sugar/inline_getters.h短名 inline 包装C99
sugar/cleanup.hRAII 风格自动清理GCC / Clang(EC_WITH_* 全编译器可用)
sugar/designated_init.h推荐配置预设 / compound literalC99
sugar/token_paste.h批量声明 / 枚举名字映射C89
sugar/generic.hC11 _Generic 类型分发C11

下面按文件分章, 给出语义、API 列表、典型示例。

结构指定初始化与 Compound Literal — sugar/designated_init.h​

C99 起, 结构体初始化可以指定字段名, 不再依赖字段顺序。 这是 C SDK 客户端代码可读性的最大单点改善。

直接使用指定初始化​

ec_startup_param_t sp = {
.index = 0x6060,
.sub_index = 0,
.data = { 8 },
.data_len = 1,
.transition = EC_TRANS_PS,
.timing = EC_TIMING_BEFORE,
.priority = 100,
.complete_access = 0,
.is_register_write= 0,
};

未指定的字段自动零初始化。 添加新字段时旧代码不会 break.

推荐配置预设​

designated_init.h 提供 4 个常用周期预设, 可直接作为初始化值:

ec_cycle_preset_t cfg = EC_PRESET_CYCLE_1MS;
cfg.cpu = 5; // 局部覆盖
  • EC_PRESET_CYCLE_4MS — 周期 4 ms, CPU 0, 优先级 10, DC 否, WDK 否
  • EC_PRESET_CYCLE_1MS — 周期 1 ms, CPU 3, 优先级 50, DC 是, WDK 是
  • EC_PRESET_CYCLE_500US — 周期 500 us, CPU 5, 优先级 80, DC 是, WDK 是
  • EC_PRESET_CYCLE_250US — 周期 250 us, CPU 7, 优先级 99, DC 是, WDK 是

每个预设还有 _AS_LITERAL 后缀的 compound literal 版本, 可作为 & 取址的临时值传给函数:

apply_cycle(&EC_PRESET_CYCLE_500US_AS_LITERAL);

速记宏 EC_CYCLE(...)​

apply_cycle(EC_CYCLE(.cycle_ns = 1000000, .cpu = 3, .use_dc = 1));

展开后是 ((ec_cycle_preset_t){ .cycle_ns = 1000000, .cpu = 3, .use_dc = 1 }), 是一个标准 C99 compound literal. 不需要先声明变量, 一行解决。

SDO 启动参数速记​

ec_startup_param_t sp = EC_SDO_DOWNLOAD_SHORT(0x6060, 0, 8, /*priority*/100);
AddStartupParameter(mi, si, &sp);

AddStartupParameter(master_index, slave_index, const ec_startup_param_t*) 把该条参数加入从站的启动参数管线, 在状态切换时由 master 自动下发; 配套接口还有 AddStartupParameterBatch / ClearStartupParameters / GetStartupParameterCount / ApplyStartupParameters. 宏自动把 32-bit 值切成 4 字节 little-endian, 设置 EC_TRANS_PS + EC_TIMING_BEFORE. 适合大量"PreOp 之前下发模式字"这种重复样板。

遍历宏 — sugar/foreach.h​

C 没有 foreach, 但用宏可以写得很自然。

基础: EC_FOREACH_SLAVE​

EC_FOREACH_SLAVE(mi, si) {
printf("[%u] vid=0x%08X pid=0x%08X\n",
si, GetSlaveVendorId(mi, si), GetSlaveProductCode(mi, si));
}

宏内部声明了独立的循环变量和上界缓存:

for (uint16_t si = 1, _ec_n_si = ec_foreach_slave_count_(mi);
si <= _ec_n_si; ++si) { ... }

ec_foreach_slave_count_(mi) 只在循环开始处调用一次(内部经 GetMasterState 读 slavecount), 不会每次迭代都打 SDK.

按组遍历: EC_FOREACH_SLAVE_IN_GROUP​

EC_FOREACH_SLAVE_IN_GROUP(mi, si, /*group=*/2) {
/* 写输出用 WriteSlaveOutputPdo,见 PDO 输入输出 */
}

只有 GetSlaveGroup(mi, si) == 2 的从站会进入循环体。 内部以 if (条件不满足) continue; else 过滤(continue 方式不产生悬挂 else 粘连,也不重写循环结构),break 仍能正常退出。

仅 OP 状态: EC_FOREACH_OP_SLAVE​

EC_FOREACH_OP_SLAVE(mi, si) {
/* 基础态为 OPERATIONAL 的从站才进循环 */
}

判定前先剥 AL 错误位(& 0x0F 后比较),因此 0x18(OP|ERROR)也会进入循环;要单独遍历带错误位的从站用 EC_FOREACH_ERROR_SLAVE。

拓扑遍历​

ec_topology_node_t nodes[64];
uint16_t roots[8];
TopologyBuild(mi, nodes, 64);
int nroot = TopologyGetRoots(mi, roots, 8);

EC_FOREACH_ROOT(roots, nroot, ri) {
uint16_t children[16];
int nc = TopologyGetChildren(mi, roots[ri], children, 16);
EC_FOREACH_CHILD(children, nc, ci) {
// children[ci] 是子从站索引
}
}

注意: EC_FOREACH_SLAVE_IN_GROUP / EC_FOREACH_OP_SLAVE 等过滤宏以 continue 过滤、尾随 else 接循环体, 多语句循环体必须写显式大括号, 避免与内部 continue / else 折叠粘连; 外层也不要直接用 if 包住这两个宏而不加大括号。

错误处理宏 — sugar/try_macro.h​

C 没有异常, 工业 SDK 代码常见的"goto fail 集中清理"模式可以缩成一行。

EC_OK / EC_ERR 命名​

if (SetState(mi, EC_STATE_OPERATIONAL) == EC_OK) { ... }

EC_OK 定义为 1 (对齐 BOOL TRUE), EC_ERR 为 0. 让 BOOL 返回的 SDK 函数读起来更像状态机。

EC_TRY(expr) — 失败 goto fail​

int do_init(const char* nic) {
uint16_t mi = 0;
mi = Initialize();
EC_TRY(mi != 0); // 失败跳 fail
EC_TRY(SetNetwork(mi, nic, "") != 0);
EC_TRY(SetStateSequence(mi, EC_STATE_OPERATIONAL, 5000));
Start(mi);
return 0;

fail:
if (mi) Dispose(mi);
return -1;
}

fail: 标签必须存在, 否则 GCC 会报 label 'fail' used but not defined. 这个约束反而强制工程师写出清理路径。

EC_TRY_LOG(expr, msg) — 失败时打日志​

EC_TRY_LOG(SetState(mi, EC_STATE_OPERATIONAL),
"SetState OPERATIONAL 失败");

默认实现用 fprintf(stderr, ...). 如果项目有自己的日志系统, 在 #include "sugar/try_macro.h" 之前定义 hook:

#define EC_TRY_LOG_HOOK(msg) my_logger("ec", msg)
#include "sugar/try_macro.h"

EC_RETURN_IF_FAIL(expr, retval)​

适合不需要清理的小函数:

int read_voltage(uint16_t mi, uint16_t si, uint32_t* out) {
EC_RETURN_IF_FAIL(out != NULL, -1);
EC_RETURN_IF_FAIL(ec_slave_count(mi) >= si, -2);
*out = GetSlaveVendorId(mi, si);
return 0;
}

EC_GOTO_IF_FAIL(expr, label) — 多级清理​

EC_GOTO_IF_FAIL(SetNetwork(mi, ...), cleanup_dispose);
EC_GOTO_IF_FAIL(SetStateSequence(...), cleanup_stop);
Start(mi);
return 0;

cleanup_stop: Stop(mi);
cleanup_dispose: Dispose(mi);
return -1;

EC_BREAK_IF_FAIL(expr) / EC_ASSERT_RANGE(v, lo, hi)​

EC_BREAK_IF_FAIL 在循环中失败 break. EC_ASSERT_RANGE 越界则 return -1(不是 EC_ERR —— 本 SDK 的 int 返回约定是 0 = 成功, 返回 EC_ERR(0)在 int 函数里等于谎报成功), 比 assert() 安全 (release 不会被去掉).

短名 Inline 包装 — sugar/inline_getters.h​

主 SDK 用 PascalCase 函数名 (GetSlaveVendorId 等)。 这里给一组 ec_* 短名, 仅作直接转发, 编译器会完全 inline 消除。

短名类别读写等价主 API返回类型
ec_slave_count(mi)主站状态只读GetMasterState() 读 slavecountuint16_t
ec_link(mi)链路只读(ec_link_state_t)GetLinkStatus(mi)ec_link_state_t
ec_vendor(mi, si)从站标识只读GetSlaveVendorId(mi, si)uint32_t
ec_product(mi, si)从站标识只读GetSlaveProductCode(mi, si)uint32_t
ec_revision(mi, si)从站标识只读GetSlaveRevision(mi, si)uint32_t
ec_serial(mi, si)从站标识只读GetSlaveSerialNumber(mi, si)uint32_t
ec_addr(mi, si)从站标识只读GetSlaveConfigAddr(mi, si)uint16_t
ec_alias(mi, si)从站标识只读GetSlaveAliasAddr(mi, si)uint16_t
ec_state(mi, si)从站状态只读(ec_state_t)GetSlaveState(mi, si)ec_state_t
ec_alstatus(mi, si)从站状态只读GetSlaveALStatusCode(mi, si)uint16_t
ec_is_op(mi, si)从站状态只读比较 EC_STATE_OPERATIONALint (1/0)
ec_has_error(mi, si)从站状态只读检查 EC_STATE_ERROR 位int (1/0)
ec_out_bytes(mi, si)过程数据只读GetSlaveOutputBytes(mi, si)uint32_t
ec_in_bytes(mi, si)过程数据只读GetSlaveInputBytes(mi, si)uint32_t
ec_parent(mi, si)拓扑只读GetSlaveParent(mi, si)uint16_t

还有两个调试日志辅助:

ec_state_t s = ec_state(mi, si);
printf("slave[%u] state=%s alstatus=0x%04X\n",
si, ec_state_name(s), ec_alstatus(mi, si));

printf("link=%s\n", ec_link_name(ec_link(mi)));

ec_state_name 返回 "INIT" / "PRE_OP" / "BOOT" / "SAFE_OP" / "OP" / "NONE", 其它值返回 "UNKNOWN"; 屏蔽错误位 (& 0x0F) 后判断。

Cleanup Attribute (GCC / Clang) — sugar/cleanup.h​

GCC 和 Clang 支持 __attribute__((cleanup(fn))), 当变量离开作用域时自动调 fn(&var). 这把 C 写出 RAII 风格成为可能。

EC_AUTO_DISPOSE​

void demo(const char* nic) {
EC_AUTO_DISPOSE uint16_t mi = Initialize();
if (!mi) return;
SetNetwork(mi, nic, "");
SetStateSequence(mi, EC_STATE_OPERATIONAL, 5000);
Start(mi);
// 函数返回时自动 Dispose(mi). return / goto 也会触发.
}

EC_AUTO_STOP_DISPOSE​

适合 Start() 后的资源, 退出时先 Stop 再 Dispose:

EC_AUTO_STOP_DISPOSE uint16_t mi = Initialize();
SetNetwork(mi, "eth0", "");
Start(mi);
// 离开作用域: Stop(mi); Dispose(mi);

EC_AUTO_FREE / EC_AUTO_FREE_SDK​

EC_AUTO_FREE      void* user_buf = malloc(4096);    // free()
EC_AUTO_FREE_SDK void* sdk_buf = SomeSdkAlloc(); // FreeMemory() (SDK 内分配)

全编译器替代: EC_WITH_* / EC_CLEANUP_*(推荐)​

cleanup attribute 不可用时(MSVC / 严格 C89)用 for 循环守卫或手动清理原语, 语义与 EC_AUTO_* 一致:

EC_WITH_DISPOSE(mi) { ... }        // 块结束时 Dispose(mi)
EC_WITH_STOP_DISPOSE(mi) { ... } // 块结束时 Stop(mi) + Dispose(mi)
EC_WITH_FREE(ptr) { ... } // 块结束时 free(ptr)
EC_WITH_FREE_SDK(ptr) { ... } // 块结束时 FreeMemory(ptr)

EC_CLEANUP_DISPOSE(mi) / EC_CLEANUP_STOP_DISPOSE(mi) / EC_CLEANUP_FREE(ptr) / EC_CLEANUP_FREE_SDK(ptr) 是同语义的显式调用版, 任意位置手动触发清理。

MSVC 注意​

MSVC 不支持 cleanup attribute. 头文件在非 GCC/Clang 下不定义 EC_AUTO_*(直接使用会编译报错, 不是退化为空), 此时改用上面的 EC_WITH_* / EC_CLEANUP_*, 或手动清理。 跨平台代码可用预处理判断:

#if EC_HAS_CLEANUP_ATTR
EC_AUTO_DISPOSE uint16_t mi = Initialize();
#else
uint16_t mi = Initialize();
/* ... 手动 Dispose */
#endif

Token Paste 批量声明 — sugar/token_paste.h​

C 预处理器的 ## 操作符可以在编译期拼接 token, 适合批量生成样板代码。

EC_DECLARE_GETTER(name, type, fn) — 批量声明 typed getter​

EC_DECLARE_GETTER(slave_vendor,  uint32_t, GetSlaveVendorId)
EC_DECLARE_GETTER(slave_product, uint32_t, GetSlaveProductCode)
EC_DECLARE_GETTER(slave_state, uint8_t, GetSlaveState)
EC_DECLARE_GETTER(slave_alias, uint16_t, GetSlaveAliasAddr)

展开后, 每个声明都生成一个 inline:

static inline uint32_t ec_get_slave_vendor(uint16_t mi, uint16_t si) {
return GetSlaveVendorId(mi, si);
}

EC_DECLARE_GETTER1(name, type, fn) 是单参版 (master 索引).

EC_BEGIN_NAME_MAP / EC_NAME_MAP_ENTRY / EC_END_NAME_MAP​

枚举到字符串的查表函数:

EC_BEGIN_NAME_MAP(my_state, ec_state_t)
EC_NAME_MAP_ENTRY(EC_STATE_INIT, "INIT")
EC_NAME_MAP_ENTRY(EC_STATE_PRE_OP, "PRE_OP")
EC_NAME_MAP_ENTRY(EC_STATE_SAFE_OP, "SAFE_OP")
EC_NAME_MAP_ENTRY(EC_STATE_OPERATIONAL, "OP")
EC_END_NAME_MAP("UNKNOWN")

生成函数 static inline const char* my_state_name(ec_state_t v); 可直接调用。 限制: 仅适合 < 64 项的小型枚举。

实用工具宏​

  • EC_STRINGIFY(x) — token 转字符串字面量 (双层间接, 宏会展开)
  • EC_LOC — __FILE__ ":" __LINE__ 字符串, 适合日志
  • EC_CONCAT(a, b) / EC_CONCAT3(a, b, c) — 拼接 token (会展开宏)
  • EC_ARRAY_LEN(arr) — 静态数组长度, (int)(sizeof(arr)/sizeof((arr)[0]))
  • EC_UNUSED(x) — (void)(x), 抑制未使用变量警告

C11 _Generic 类型分发 — sugar/generic.h​

C11 起, _Generic 关键字可根据表达式静态类型选择不同实现, 实现"伪重载".

仅在 C11+ 编译器下生效。 C99 模式所有宏退化为兼容版本。

ec_byte_size(x) — 类型字节宽度​

uint32_t a; int8_t b; double c;
printf("%d %d %d\n", ec_byte_size(a), ec_byte_size(b), ec_byte_size(c));
// 输出: 4 1 8

C99 退化为 (int)sizeof(x). 表内整数/浮点类型两边行为一致; 其它类型(指针、结构体)在 C11 _Generic 分支落到 default: 0, 与 sizeof 退化不一致, 别用它量非标量宽度。

ec_print_value(label, v) — 类型安全打印​

uint32_t vendor = ec_vendor(mi, 1);
ec_print_value("vendor", vendor);
// 输出: vendor = 305419896

宏内部用 _Generic + <inttypes.h> 的 PRId32 等格式宏, 不需要手写 %u/%lu/%hd 等。 C99 退化为 ((void)0), 调用者自行用 printf.

ec_max(a, b) / ec_min(a, b)​

简版宏, 使用三元表达式。 注意会 double-eval 参数, 不要在参数中放副作用表达式。

不透明句柄 (Opaque Handle) 模式​

DarraEtherCAT C SDK 用 uint16_t 的 master 索引作为不透明句柄, 用户拿到的不是结构指针, 而是数字索引(各 API 形参名 master_index / mi)。 这个模式不需要 sugar 头文件支持, 但值得在文档里说明:

uint16_t mi = Initialize();              // 不透明句柄
SetNetwork(mi, "eth0", ""); // 所有 API 都接收 mi
/* ... */
Dispose(mi);

好处:

  • 不暴露内部结构布局, ABI 稳定 (新增字段不破坏 SDK 客户)
  • 跨进程 / 跨 DLL 边界传值安全 (整数, 不是指针)
  • 同样支持多实例 (InitializeSpecificMaster(N))

master_index = 0 永远表示"无效" / 未初始化。 大部分 SDK API 会显式判断 master_index != 0.

完整示例​

把上面所有 sugar 串起来, 一个完整的 PreOp -> SafeOp -> OP 启动流程:

#include "ethercat.h"
#include "sugar/sugar.h"

#include <stdio.h>

int main(int argc, char** argv) {
if (argc < 2) {
fprintf(stderr, "usage: %s <nic>\n", argv[0]);
return 1;
}

/* C99 指定初始化 */
ec_cycle_preset_t cfg = EC_PRESET_CYCLE_1MS;
cfg.cpu = 5;

/* 自动 Dispose (GCC/Clang); MSVC 下需手动 Dispose */
EC_AUTO_DISPOSE uint16_t mi = Initialize();
EC_RETURN_IF_FAIL(mi != 0, 2);

/* try-fail 模式 */
EC_TRY_LOG(SetNetwork(mi, argv[1], "") != 0,
"SetNetwork 失败, 检查 NIC 名称");
EC_TRY_LOG(SetStateSequence(mi, EC_STATE_OPERATIONAL, 5000),
"状态序列未达到 OP");

/* 启动 RT 周期 */
if (cfg.use_wdk) {
StartWdkRT(mi, cfg.cycle_ns / 1000, cfg.cpu);
} else {
Start(mi);
}

/* 短名 + 遍历 + 状态字符串 */
printf("link=%s, slaves=%u\n",
ec_link_name(ec_link(mi)), ec_slave_count(mi));

EC_FOREACH_OP_SLAVE(mi, si) {
printf(" [%u] vid=0x%08X pid=0x%08X state=%s\n",
si, ec_vendor(mi, si), ec_product(mi, si),
ec_state_name(ec_state(mi, si)));
}

/* 清理 — Stop 显式调用, Dispose 由 EC_AUTO_DISPOSE 接管 */
Stop(mi);
return 0;

fail:
/* EC_TRY_LOG 失败时跳到这里 */
return 3;
}

FAQ​

Q: Sugar 会不会拖慢运行时? A: 不会。 所有内联函数都标 static inline, 编译器会完全消除调用。 宏在预处理阶段展开, 0 运行时开销。

Q: 不用 sugar 可以吗? A: 完全可以。 主 SDK API 与 sugar 头解耦, 不引入任何 sugar 依赖。 sugar 只是给愿意用的工程师一个更简洁的写法。

Q: 我项目用 C89, 哪些 sugar 可用? A:

  • 错误处理 (try_macro.h) — 宏全部可用; 同头的失败现场 inline 在 GCC 需 C99 / gnu 扩展(MSVC __inline 可用)
  • 遍历 (foreach.h) — 不可用 (依赖 for (uint16_t si = 1, ...) 这种 C99 块内多变量声明)
  • inline 包装 (inline_getters.h) — __inline MSVC 可用, GCC static inline 需 C99
  • cleanup — 与 C 标准无关, 看编译器是否支持; 不支持时用 EC_WITH_* / EC_CLEANUP_*
  • 指定初始化 — 需 C99
  • token paste — C89 可用
  • _Generic — 需 C11

Q: MSVC 项目能用 cleanup 吗? A: EC_AUTO_* 不能。 MSVC 不实现 GCC cleanup attribute, 头文件在 MSVC 下不定义 EC_AUTO_*(用了直接编译报错)。 改用全编译器的 EC_WITH_DISPOSE / EC_CLEANUP_DISPOSE 等(见 Cleanup 一节), 或手动调 Dispose / Stop / free。

Q: 这些 sugar 影响 ABI 吗? A: 不影响。 全部是头文件内的宏 + inline, 不导出新符号, 不修改主 SDK 的二进制布局。

文件总览​

include/sugar/
├── sugar.h — 伞形头文件 (包含下面全部)
├── foreach.h — 遍历宏
├── try_macro.h — 错误处理 + EC_OK/EC_ERR
├── inline_getters.h — 短名 inline 包装
├── cleanup.h — GCC/Clang 自动清理
├── designated_init.h — 推荐配置预设 + compound literal
├── token_paste.h — 批量声明 + 名字映射
└── generic.h — C11 _Generic