跳到主要内容

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
sugar/designated_init.h推荐配置预设 / compound literalC99
sugar/token_paste.h批量声明 / 枚举名字映射C89
sugar/generic.hC11 _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优先级DCWDK
EC_PRESET_CYCLE_4MS4 ms010
EC_PRESET_CYCLE_1MS1 ms350
EC_PRESET_CYCLE_500US500 us580
EC_PRESET_CYCLE_250US250 us799

每个预设还有 _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_GROUPEC_FOREACH_OP_SLAVEif/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_OPERATIONALint (1/0)
ec_has_error(mi, si)检查 EC_STATE_ERRORint (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() 后的资源, 退出时先 StopDispose:

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) — __inline MSVC 可用, GCC static 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