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.h | RAII 风格自动清理 | GCC / Clang(EC_WITH_* 全编译器可用) |
sugar/designated_init.h | 推荐配置预设 / compound literal | C99 |
sugar/token_paste.h | 批量声明 / 枚举名字映射 | C89 |
sugar/generic.h | C11 _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() 读 slavecount | uint16_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_OPERATIONAL | int (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) —__inlineMSVC 可用, GCCstatic 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