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 |
sugar/designated_init.h | 推荐配置预设 / compound literal | C99 |
sugar/token_paste.h | 批量声明 / 枚举名字映射 | C89 |
sugar/generic.h | C11 _Generic 类型分发 | C11 |
下面按文件分章, 给出语义、API 列表、典型示例.
1. 结构指定初始化与 Compound Literal — sugar/designated_init.h
C99 起, 结构体初始化可以指定字段名, 不再依赖字段顺序. 这是 C SDK 客户端代码可读性的最大单点改善.
1.1 直接使用指定初始化
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.
1.2 推荐配置预设
designated_init.h 提供 4 个常用周期预设, 可直接作为初始化值:
ec_cycle_preset_t cfg = EC_PRESET_CYCLE_1MS;
cfg.cpu = 5; // 局部覆盖
| 宏 | 周期 | CPU | 优先级 | DC | WDK |
|---|---|---|---|---|---|
EC_PRESET_CYCLE_4MS | 4 ms | 0 | 10 | 否 | 否 |
EC_PRESET_CYCLE_1MS | 1 ms | 3 | 50 | 是 | 是 |
EC_PRESET_CYCLE_500US | 500 us | 5 | 80 | 是 | 是 |
EC_PRESET_CYCLE_250US | 250 us | 7 | 99 | 是 | 是 |
每个预设还有 _AS_LITERAL 后缀的 compound literal 版本, 可作为 & 取址的临时值传给函数:
apply_cycle(&EC_PRESET_CYCLE_500US_AS_LITERAL);
1.3 速记宏 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. 不需要先声明变量, 一行解决.
1.4 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 之前下发模式字"这种重复样板.
2. 遍历宏 — sugar/foreach.h
C 没有 foreach, 但用宏可以写得很自然.
2.1 基础: 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 = GetSlaveCount(mi);
si <= _ec_n_si; ++si) { ... }
GetSlaveCount(mi) 只在循环开始处调用一次, 不会每次迭代都打 SDK.
2.2 按组遍历: EC_FOREACH_SLAVE_IN_GROUP
EC_FOREACH_SLAVE_IN_GROUP(mi, si, /*group=*/2) {
WriteSlaveOutputByte(mi, si, 0, 0xFF);
}
只有 GetSlaveGroup(mi, si) == 2 的从站会进入循环体. 内部用 if () {} else 折叠不重写循环结构, break 仍能正常退出.
2.3 仅 OP 状态: EC_FOREACH_OP_SLAVE
EC_FOREACH_OP_SLAVE(mi, si) {
/* 安全访问 PDO, 因为只迭代 OPERATIONAL 从站 */
}
2.4 拓扑遍历
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 含 if/else 折叠, 在 C99 模式可正确嵌套, 但避免在外层加额外的 if 防止与展开的 else 关联错误. 写显式大括号最安全.
3. 错误处理宏 — sugar/try_macro.h
C 没有异常, 工业 SDK 代码常见的"goto fail 集中清理"模式可以缩成一行.
3.1 EC_OK / EC_ERR 命名
if (SetState(mi, EC_STATE_OPERATIONAL) == EC_OK) { ... }
EC_OK 定义为 1 (对齐 BOOL TRUE), EC_ERR 为 0. 让 BOOL 返回的 SDK 函数读起来更像状态机.
3.2 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. 这个约束反而强制工程师写出清理路径.
3.3 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"
3.4 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(GetSlaveCount(mi) >= si, -2);
*out = GetSlaveVendorId(mi, si);
return 0;
}
3.5 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;
3.6 EC_BREAK_IF_FAIL(expr) / EC_ASSERT_RANGE(v, lo, hi)
EC_BREAK_IF_FAIL 在循环中失败 break. EC_ASSERT_RANGE 越界则 return EC_ERR, 比 assert() 安全 (release 不会被去掉).
4. 短名 Inline 包装 — sugar/inline_getters.h
主 SDK 用 PascalCase 函数名 (GetSlaveVendorId 等) 与 C# 类库一致. 这里给一组 ec_* 短名, 仅作直接转发, 编译器会完全 inline 消除.
| 短名 | 等价主 API | 返回类型 |
|---|---|---|
ec_slave_count(mi) | GetSlaveCount(mi) | 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", 屏蔽错误位 (& 0x0F) 后判断.
5. Cleanup Attribute (GCC / Clang) — sugar/cleanup.h
GCC 和 Clang 支持 __attribute__((cleanup(fn))), 当变量离开作用域时自动调 fn(&var). 这把 C 写出 RAII 风格成为可能.
5.1 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 也会触发.
}
5.2 EC_AUTO_STOP_DISPOSE
适合 Start() 后的资源, 退出时先 Stop 再 Dispose:
EC_AUTO_STOP_DISPOSE uint16_t mi = Initialize();
SetNetwork(mi, "eth0", "");
Start(mi);
// 离开作用域: Stop(mi); Dispose(mi);
5.3 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 内分配)
5.4 MSVC 注意
MSVC 不支持 cleanup attribute. 头文件检测到 MSVC 会把所有 EC_AUTO_* 退化为空, 用户必须手动清理. 跨平台代码可用预处理判断:
#if EC_HAS_CLEANUP_ATTR
EC_AUTO_DISPOSE uint16_t mi = Initialize();
#else
uint16_t mi = Initialize();
/* ... 手动 Dispose */
#endif
6. Token Paste 批量声明 — sugar/token_paste.h
C 预处理器的 ## 操作符可以在编译期拼接 token, 适合批量生成样板代码.
6.1 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 索引).
6.2 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 项的小型枚举.
6.3 实用工具宏
| 宏 | 作用 |
|---|---|
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), 抑制未使用变量警告 |
7. C11 _Generic 类型分发 — sugar/generic.h
C11 起, _Generic 关键字可根据表达式静态类型选择不同实现, 实现"伪重载".
仅在 C11+ 编译器下生效. C99 模式所有宏退化为兼容版本.
7.1 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), 行为相同.
7.2 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.
7.3 ec_max(a, b) / ec_min(a, b)
简版宏, 使用三元表达式. 注意会 double-eval 参数, 不要在参数中放副作用表达式.
8. 不透明句柄 (Opaque Handle) 模式
DarraEtherCAT C SDK 已经使用 master_index_t (实际是 uint16_t) 作为不透明句柄, 用户拿到的不是结构指针, 而是数字索引. 这个模式不需要 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.
9. 综合示例
把上面所有 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;
}
10. FAQ
Q: Sugar 会不会拖慢运行时?
A: 不会. 所有内联函数都标 static inline, 编译器会完全消除调用. 宏在预处理阶段展开, 0 运行时开销.
Q: 不用 sugar 可以吗? A: 完全可以. 主 SDK API 与 sugar 头解耦, 不引入任何 sugar 依赖. sugar 只是给愿意用的工程师一个更简洁的写法.
Q: 我项目用 C89, 哪些 sugar 可用? A:
- 错误处理 (
try_macro.h) — 全部可用 - 遍历 (
foreach.h) — 不可用 (依赖for (uint16_t si = 1, ...)这种 C99 块内多变量声明) - inline 包装 (
inline_getters.h) —__inlineMSVC 可用, GCCstatic inline需 C99 - cleanup — 与 C 标准无关, 看编译器是否支持
- 指定初始化 — 需 C99
- token paste — C89 可用
- _Generic — 需 C11
Q: MSVC 项目能用 cleanup 吗?
A: 不能. MSVC 不实现 GCC cleanup attribute. 头文件检测到 MSVC 后会把 EC_AUTO_* 退化为空, 用户必须手动调 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