CppCon 2021 学习:Using C libraries in your Modern C++ Embedded Project EtherCAT Stack, C++17, and Mode
这几行是在讲 EtherCAT(工业以太网实时通讯协议)中时钟同步的可选方案,理解如下:
EtherCAT 可选时钟(optional clock options)
- TwinCAT clock(TwinCAT 时钟)
- TwinCAT 是 Beckhoff 的工业自动化软件
- 使用 TwinCAT 系统自身的内部时钟作为 EtherCAT 从站同步的参考
- 优点:无需额外硬件,直接使用控制器内部时钟
- DC Ref. clock(Distributed Clock 参考时钟)
- DC(Distributed Clocks,分布式时钟)是 EtherCAT 的一种同步机制
- 使用网络中某个设备的 DC 作为全网参考时钟
- EtherCAT 从站通过 DC 信号自动同步到主站的时间
- 优点:精度高,适合实时控制
- ext. Ref. clock(外部参考时钟)
- 使用外部设备提供的参考时钟(如 GPS、PTP、外部信号源)
- EtherCAT 主站或从站根据外部时钟同步
- 优点:多系统统一时间,适合多设备、多网络协同
总结:
- EtherCAT 可以选择不同的时钟源来保证从站设备与主站的同步
- TwinCAT clock:软件内部时钟
- DC Ref. clock:EtherCAT 网络内部分布式时钟
- ext. Ref. clock:外部硬件参考时钟
Sync0 Cycle Time (0x1C32.2/0x1C33.2) Sync0
┃◀━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━▶┃
┃ ┃
┃ ┃ ┃
Frame ┃ Min Cycle Time (Ox1C32.5/0x1C33.5) ┃ Frame ┃
┃◀━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━▶┃ ┃
┃ ┃ ┃
┃ DO_InputMapping() ┃ ┃
┃ PDO_OutputMapping()┃ ECAT_Application() ┃ ┃ ┃
┃ ┃ │ ┃ ┃ ┃
┃◀━━━▲━━━▶ ◀━━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┿━━━━━━━━━━▶┃◀━━━━▲━━━▶┃ ┃
┃ │ ▲ ┃ │ ▲ ┃ │ ┃ ┃
┃ │ │ ┃ start Input │ ┃ │ ┃ ┃
┃ │ │ ┃ Latch │ ┃ │ ┃ ┃
┃ │ │ ┃ ┌────────┘ ┃ │ ┃ ┃
┃ │ └──┐ ┃ │ ┃ │ ┃ ┃
┃ │ │Outputs valid │ ┃ │ ┃ ┃
│ │ ┃ Delay Time ┃ │ ┃
│ │ (0x1C33.9) │
Calc and Copy │ │
Time (Ox1C32.6) Delay Time │
(0x1C32.9) │
Calc and
Copy Time
(0x1C33.6)
这张图描述的是 EtherCAT 分布式时钟(Distributed Clocks, DC)同步周期中事件的时间关系。我来帮你逐段解释:
关键元素
- Sync0
- 这是 EtherCAT 同步信号(通常与控制周期绑定)。
Cycle Time (0x1C32.2/0x1C33.2):配置的周期时间(主站给从站定义的周期)。- 每个周期都由 Sync0 信号触发。
- Min Cycle Time (0x1C32.5/0x1C33.5)
- 定义最小允许的周期时间。
- 如果实际周期小于此值,可能导致设备无法及时处理数据。
- PDO_OutputMapping() / PDO_InputMapping()
- PDO(Process Data Object)过程数据对象:EtherCAT 中循环交换的数据。
- PDO_OutputMapping():主站将输出数据写入从站(即给设备下发控制指令)。
- PDO_InputMapping():主站从从站读取输入数据(即采集设备状态/传感器数据)。
- ECAT_Application()
- 应用层逻辑,即实际的用户程序(比如控制算法)。
- 发生在 PDO 映射之后。
- Delay Time (0x1C32.9 / 0x1C33.9)
- 输入或输出的延迟时间,决定信号在 Sync0 触发点之后的偏移。
- Calc and Copy Time (0x1C32.6 / 0x1C33.6)
- 计算和拷贝时间:用于处理过程数据(拷贝到内存、完成数据交换)。
- 通常加上延迟时间后,确定最终数据有效的时刻。
执行顺序解读
- 周期开始(Sync0 触发):进入新的 EtherCAT 周期。
- PDO_OutputMapping():主站把本周期的控制输出写给从站。
- ECAT_Application():应用层逻辑运行(例如运动控制计算)。
- PDO_InputMapping():在设定的时间点,采集从站的输入数据。
- start Input Latch:触发输入锁存(记录外部事件)。
- Outputs valid:输出数据被应用到设备,开始对外生效。
- Delay Time + Calc/Copy Time:补偿处理和通讯延迟,确保时序一致。
- Frame:EtherCAT 帧在周期中传输,与 PDO 数据映射保持同步。
总结
这张图展示了 EtherCAT 同步周期 (Cycle) 内,
- 输出数据 (Output PDO) 在什么时候下发并生效,
- 应用程序 (ECAT Application) 在什么时候运行,
- 输入数据 (Input PDO) 在什么时候采集,
- 延迟时间、计算时间 如何影响输入/输出的有效时刻。
Sync0/Sync1
Sync1
Sync0 Cycle Time (0x1C32.2/0x1C33.2) ┃ Sync0
┃◀━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━▶┃
┃ ┃ ┃
┃ ┃ ┃
Frame ┃ ┃ Frame ┃
┃ ┃ ┃
┃ ┃ PDO_InputMapping() ┃
┃ ┃ ┃
┃ PDO_OutputMapping()┃ ECAT_Application() ┃ ┃ ┃
┃ ┃ ┃ ┃ ┃
┃◀━━━▲━━━▶ ◀━━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━▶┃◀━━━━▲━━━▶┃◀━━━━▲━━━▶ ┃
┃ │ ▲ ┃ ┃ │ ┃ │ ┃
┃ │ │ ┃ ┃ │ ┃ │ ┃
┃ │ │ ┃ ┃ │ └────┐ ┃
┃ │ │ ┃ ┃ │ Inputs latch │ ┃
┃ │ └──┐ ┃ ┃ │ │ ┃
┃ │ │Outputs valid ┃ │ │ ┃
│ │ ┃ ┃ │ │
│ │ │ │
Calc and Copy │ │ │
Time (Ox1C32.6) Delay Time │ │
(0x1C32.9) │ │
Delay Time Calc and Copy
(0x1C33.9) Time(0x1C33.6)
这个图是 EtherCAT Sync0/Sync1 同步信号和应用任务执行时序 的示意图。下面逐段解释:
关键要素
- Sync0 / Sync1
- EtherCAT 分布式时钟机制提供的硬件同步脉冲信号。
- Sync0:常用来触发周期性任务,比如驱动输出更新、控制环计算。
- Sync1:可选的第二个同步脉冲,用于输入采样(Latch 输入)或双边沿控制。
- Cycle Time (0x1C32.2 / 0x1C33.2)
- 周期时间寄存器,定义了 Sync0/Sync1 的周期。
- 每个周期开始时触发 Sync 信号,保证主站和从站严格对齐。
- PDO 映射 (PDO_OutputMapping / PDO_InputMapping)
- PDO_OutputMapping:主站写数据到从站的过程(输出有效时间点)。
- PDO_InputMapping:从站把采样到的输入数据映射到 PDO,供主站读取。
- ECAT_Application()
- EtherCAT 应用任务,即用户的控制算法或数据处理逻辑,运行在同步周期内。
- Calc and Copy Time (0x1C32.6 / 0x1C33.6)
- 数据计算和拷贝的耗时。
- 表示从收到数据到准备好输出之间所需的处理延迟。
- Delay Time (0x1C32.9 / 0x1C33.9)
- 输出或输入相对 Sync 信号的延迟补偿时间。
- 用于保证输出信号在 Sync 边沿之后的一致时刻生效,或输入采样在期望时刻进行。
图中逻辑解读
- 左半部分(Sync0)
- 周期开始,触发 Sync0。
- 主站写输出 PDO → 从站应用(ECAT_Application)开始运行 → 输出数据在延迟时间后变为有效。
- 右半部分(Sync1)
- Sync1 在同一周期的后半部分触发。
- 常用于输入锁存(Latch Input),即在 Sync1 边沿精确采集输入信号。
- 输入数据经过延迟补偿后,进入 PDO_InputMapping(),等待下一个周期传输给主站。
总结
- Sync0 → 保证输出数据在固定时刻更新(驱动、PWM、控制指令)。
- Sync1 → 保证输入数据在固定时刻采样(编码器、传感器信号)。
- Delay Time / Calc and Copy Time → 用来对齐应用处理时间,确保输入/输出在严格的时序下交换。
在现代 C++ 嵌入式项目中使用 C 库 ,重点放在 Beckhoff EtherCAT SSC(Slave Stack Code)。
主要点:
- 系统抖动(System jitter)
小于 1 微秒,说明系统定时精度非常高,适合实时控制。 - 优先级与权衡(Priorities and trade-offs)
在嵌入式开发中,通常需要在 时间、成本、资源 之间做取舍。 - Beckhoff SSC (Slave Stack Code)
- 由 Beckhoff 提供的 EtherCAT 从站协议栈。
- 完全用 ANSI C 编写。
- 结构简单、模块化,便于快速开发从站设备。
extern "C"
当在 C++ 项目中集成 Beckhoff SSC(C 库)时,需要用extern "C"来避免 C++ 名字修饰(name mangling),保证 C++ 能正确链接到 C 接口。- 应用场景
例如在 Infineon XMC 等 MCU 上,把 Beckhoff SSC(C 实现)与 C++ 应用层结合使用。
Plugins 插件层
用户(开发者)需要在 主循环 和 硬件抽象接口 中提供一些功能:
- 调用 主循环 (MainLoop)
- 提供 处理过程数据的接口 (process data handling)
- 提供 中断控制的接口
- 在中断发生时调用接口
- 提供从 硬件具体实现 → 抽象接口 的桥梁
接口设计 (The Interface)
这是 C 库(Beckhoff SSC)暴露出来的关键接口,C++ 应用需要实现它们:
// 在主循环中被调用,驱动 SSC 的处理
void MainLoop(void);
// 当 SSC 有周期性数据(PDOs)时调用
void process_cycle(TOBJ7000 *pdo_in, TOBJ6000 *pdo_out);
MainLoop:相当于系统的心跳循环,驱动协议栈工作。process_cycle:当 EtherCAT SSC 准备好输入/输出 PDO 数据时,回调应用层函数。
函数重载 (Overloads)
- 在 C++ 里可以函数重载:
int func(); int func(int); float func(float); float func(int, float); - 在 C 里不能重载函数,只能通过不同函数名来区分:
int func(); int funci(int); float funcf(float); float funcif(int, float);
语言链接 (Language Linkage)
extern "C" 关键字用来指定 语言链接属性,主要解决:
- 调用约定 (calling convention)
- 名字修饰 (name mangling)
例子:
C 函数在 C++ 中调用
// C 代码
int get_radio_id() {
/** .... **/
}
// C++ 代码
extern "C" {
int get_radio_id(); // 防止 C++ 名字修饰
}
auto radio_id = get_radio_id();
C++ 函数在 C 中调用
// C++ 代码
StateMachineA sm_a;
StateMachineB sm_b;
extern "C" void UserLoop() {
sm_a.step();
sm_b.step();
}
// C 代码
UserLoop(); // 在 C 里直接调用
总结:
- Beckhoff SSC 是 C 库,因此在 C++ 中需要
extern "C"来保证函数签名保持 C 风格(避免 name mangling)。 - C++ 应用层 可以用
extern "C"导出接口给 SSC 调用,从而在 C 库 ↔ C++ 应用层 之间双向交互。 - 插件化设计让用户在主循环和中断点接入逻辑,而底层 SSC 负责 EtherCAT 协议处理。
这段 Beckhoff SSC + C++ 应用层的集成模式 整理并成理解:
核心问题
- SSC(Slave Stack Code)是 C 写的 EtherCAT 从站协议栈,要求用户实现 主循环和周期数据处理函数。
- 但是如果我们直接把逻辑写进 C 接口里,代码会变得“丑陋”、难以测试和模拟。
- 目标是通过 分层设计(Layered) 和 声明式接口(Declarative Division),把硬件相关和业务逻辑解耦。
第一阶段(ugly code 示例)
extern "C" {
void process_app(TOBJ7000 *pdo_in, TOBJ6000 *pdo_out) {
// 丑陋代码:直接操作 PDO 输入输出
}
}
extern "C" {
void MainLoop(void); // SSC 提供的主循环
}
int main() {
init();
while (1U) {
MainLoop(); // 驱动 EtherCAT 协议栈
// 这里还有一堆“丑陋代码”,逻辑散乱
}
}
问题:
- 逻辑和协议栈耦合太紧。
- 难以测试/模拟,因为全都依赖真实硬件。
第二阶段(分层设计,应用类 app)
slushie_app<xmc::hardware::hardware> app;
extern "C" {
void process_app(TOBJ7000 *pdo_in, TOBJ6000 *pdo_out) {
auto config = to_config_in(pdo_in); // 把 PDO 输入转换成配置结构
app.process_cyclic_data(config); // 交给 C++ 应用逻辑处理
auto status = app.get_cyclic_status(); // 获取处理结果
fill_pdo_status(status, pdo_out); // 写回 PDO 输出
}
}
extern "C" {
void MainLoop(void); // 仍然来自 SSC
}
int main() {
app.init(); // 初始化应用
while (1U) {
MainLoop(); // 协议栈
app.app_loop(); // 应用层自己的循环逻辑
}
}
改进点:
- 用 C++ 类 (
slushie_app) 管理应用逻辑,硬件相关的通过模板参数注入。 process_app成为 协议栈 ↔ 应用层 的桥梁。MainLoop处理 EtherCAT 协议状态机,而app.app_loop()管理业务逻辑。
第三阶段(PDO 输入结构转化)
ecat_data::config_in to_config_in(TOBJ7000 *pdo) {
return ecat_data::config_in {
.freeze_boost =
(uint32_t(pdo->Freeze_boost_h) << 16) |
uint32_t(pdo->Freeze_boost_l),
.outside_temperature = int32_t(pdo->Outside_temperature),
.do_clean = bool(pdo->Do_clean),
.flash_sign = bool(pdo->Flash_sign),
};
}
extern "C" {
void process_app(TOBJ7000 *pdo_in, TOBJ6000 *pdo_out) {
auto config = to_config_in(pdo_in); // 把 PDO 输入转成强类型 config_in
app.process_cyclic_data(config); // 交给 C++ 应用处理
auto status = app.get_cyclic_status();
fill_pdo_status(status, pdo_out); // 输出结果回写
}
}
改进点:
- 结构化输入:把原始 PDO(协议栈定义的原始内存布局)转换为 C++ 类型
config_in。 - 应用层操作的是类型安全的对象,而不是生硬的寄存器字段。
- 更容易测试(可以直接构造
config_in而不依赖 EtherCAT)。
总结
- 问题:如果直接用 SSC 的
process_app/MainLoop,代码会丑陋、难维护。 - 改进:通过 C++ 应用类(
slushie_app)把逻辑和协议栈解耦。 - 分层:
- 协议栈层(SSC):负责 EtherCAT 状态机、PDO 通信。
- 适配层(extern “C” 函数):
process_app,桥接 SSC 和 C++ 应用。 - 应用层(C++ 类):业务逻辑、测试、模拟。
- 好处:
- 可测试(应用层不用依赖 EtherCAT 硬件)。
- 可模拟(直接给
config_in输入即可)。 - 硬件无关,业务逻辑独立。
hardware layer 的接口设计。
namespace xmc::hardware {
struct hardware {
static void init();
static void tick();
static bool send_can_msg_obj(uint8_t const* /*data*/);
static std::optional<can_msg_pack_t> get_can_msg_obj();
static uint32_t get_debug_status_word();
static void status_in_idle();
static void status_in_op();
static void status_have_error();
static void status_clear_error();
static void status_have_ecat_msg();
};
}
逐项说明
static void init();
→ 硬件初始化接口。
通常用来配置 MCU 的外设、总线、GPIO、CAN 控制器、定时器等。static void tick();
→ 硬件的周期性“心跳”函数。
在主循环或定时器中被调用,用来驱动底层硬件的周期任务(比如刷 CAN 缓冲区、更新状态寄存器)。static bool send_can_msg_obj(uint8_t const* data);
→ 发送一个 CAN 报文。data指向待发送的数据(比如打包好的报文结构体),返回值表示是否成功。static std::optional<can_msg_pack_t> get_can_msg_obj();
→ 从硬件(CAN 接收缓冲区)获取一个 CAN 报文。- 如果有报文,就返回
can_msg_pack_t。 - 如果没有,就返回
std::nullopt。
用std::optional可以避免额外的标志位。
- 如果有报文,就返回
static uint32_t get_debug_status_word();
→ 获取硬件的调试状态字(status word)。
常见用法是把内部状态寄存器、错误码、计数器等打包成一个 32 位数,方便上层调试和 EtherCAT 上传。static void status_in_idle();
→ 设置当前硬件处于 空闲状态。
可能会更新 LED 指示灯、状态机标志位。static void status_in_op();
→ 设置当前硬件处于 运行状态(Operational)。
一般对应 EtherCAT OP 模式。static void status_have_error();
→ 标记硬件进入 错误状态。
通常在 CAN 错误、总线异常或安全停机时调用。static void status_clear_error();
→ 清除错误状态。
让系统从错误状态恢复,可能需要主站控制触发。static void status_have_ecat_msg();
→ 标记已经收到一个 EtherCAT 消息。
一般用于通知状态机“有新数据”,触发相应处理。
总结
这个 hardware 结构体就是一个 硬件抽象层接口:
- 输入/输出:通过
send_can_msg_obj/get_can_msg_obj和外部总线(CAN、EtherCAT)交互。 - 状态获取:
get_debug_status_word提供调试信息。 - 状态管理:
status_in_idle、status_in_op、status_have_error等方法用于切换和报告硬件状态。 - 生命周期:
init初始化,tick周期性更新。
这样上层slushie_app就不需要关心具体 MCU 或驱动细节,而只用调用这些统一的接口。
代码内容
slushie_app<xmc::hardware::hardware> app;
extern "C" {
void process_app(TOBJ7000 *pdo_in, TOBJ6000 *pdo_out) {
auto config = to_config_in(pdo_in);
app.process_cyclic_data(config);
auto status = app.get_cyclic_status();
fill_pdo_status(status, pdo_out);
}
}
extern "C" {
void MainLoop(void);
}
int main() {
app.init();
while (1U) {
MainLoop();
app.app_loop();
}
}
逐行解释
slushie_app<xmc::hardware::hardware> app;- 定义了一个应用对象
app,它的模板参数是xmc::hardware::hardware,也就是说这个应用将运行在 XMC 硬件抽象层 之上。 - 这样可以把业务逻辑(slushie_app)和底层硬件实现(hardware layer)解耦。
- 定义了一个应用对象
extern "C" { void process_app(...) }- 定义了一个 C 接口函数
process_app。 - EtherCAT SSC(Slave Stack Code)是用 C 写的,所以需要用
extern "C",这样 C++ 编译器不会改函数名(避免 name mangling)。 - 这个函数的作用:
- 输入:
pdo_in→ 从 EtherCAT 主站收到的 输入 PDO 数据。 - 输出:
pdo_out→ 从从站反馈给主站的 输出 PDO 数据。
执行流程:
- 输入:
to_config_in(pdo_in)→ 把原始的 EtherCAT PDO 输入数据转换成内部应用可用的配置结构config。app.process_cyclic_data(config)→ 应用处理这一周期的输入命令。app.get_cyclic_status()→ 获取应用当前状态。fill_pdo_status(status, pdo_out)→ 把状态结果写回 EtherCAT 输出 PDO,供主站读取。
- 定义了一个 C 接口函数
extern "C" { void MainLoop(void); }- 声明了一个 来自 SSC 的主循环函数
MainLoop。 - 这个函数在 Beckhoff 提供的 EtherCAT 从站协议栈里实现,负责处理 EtherCAT 状态机和通信。
- 声明了一个 来自 SSC 的主循环函数
int main() { ... }
程序入口:app.init();
调用应用的初始化函数 → 设置硬件、状态机、内部变量。while (1U) { ... }
无限循环,相当于嵌入式系统的主循环。
循环内两件事:MainLoop();
运行 EtherCAT 协议栈的循环处理,保持和主站的通信与状态机。app.app_loop();
应用层自己的循环处理逻辑(比如 CAN 消息处理、状态机更新、业务逻辑)。
整体总结
这个程序的核心逻辑就是把 EtherCAT 从站协议栈(SSC) 和 应用逻辑(slushie_app) 结合起来:
- EtherCAT 协议栈调用
process_app→ 输入 PDO 交给应用处理,输出 PDO 由应用生成。 - 主函数
main循环里:MainLoop()保证 EtherCAT 协议栈运行。app.app_loop()保证应用逻辑运行。
最终形成一个分层架构:
EtherCAT 主站 <-> Beckhoff SSC (C) <-> process_app (C++ bridge) <-> slushie_app (C++ 应用逻辑) <-> hardware layer
应用层 (slushie_app) 的实现逻辑。它把 EtherCAT 周期性数据(命令和状态)与硬件层、状态机结合起来。下面我帮你逐块和解释。
1. slushie_app 模板类
template<typename HardwareLayer>
class slushie_app {
public:
void init();
void process_cyclic_data(ecat_data::cmd_in const& cmd);
ecat_data::status_out get_cyclic_status();
void app_loop();
private:
void process_new_ecat_msg();
void pre_app_loop();
void post_app_loop();
};
解释:
这是应用的核心类,基于模板参数 HardwareLayer(硬件层)来实现。
- public 接口:
init():初始化硬件。process_cyclic_data():处理从 EtherCAT 收到的输入命令(PDO 输入)。get_cyclic_status():生成应用的状态,作为 PDO 输出。app_loop():应用的主循环逻辑,每周期调用一次。
- private 内部函数:
process_new_ecat_msg():处理新收到的 EtherCAT 消息。pre_app_loop()/post_app_loop():循环执行前后的准备和收尾动作。
2. 内部成员
template<typename HardwareLayer>
class slushie_app {
private:
ecat_data::cmd_in last_ecat_cmd_ = {};
ecat_data::cmd_in new_ecat_cmd_ = {};
ecat_data::status_out current_status_ = {};
uint8_t status_toggle_ = 0x01;
uint16_t app_loop_count_ = 0;
uint16_t app_loop_count_shadow_ = 0xdead;
enum class bus_cycle_result : uint8_t { waiting, have_msg, invalid_cycle };
bus_cycle_result process_ecat_state_ = bus_cycle_result::waiting;
using ecat_slushie_machine_t = ecat_slushie_machine::machine<HardwareLayer>;
boost::sml::sm<ecat_slushie_machine_t> ecat_slushie_sm_;
};
解释:
last_ecat_cmd_:上一个周期的 EtherCAT 命令。new_ecat_cmd_:本周期收到的新 EtherCAT 命令。current_status_:当前状态输出缓存。status_toggle_:状态切换标志(通常用于避免主站误判状态没更新)。app_loop_count_:应用循环计数器。app_loop_count_shadow_:计数器影子值(初始化为 0xdead,便于调试)。bus_cycle_result:总线周期结果,有三种:waiting等待新消息have_msg收到新消息invalid_cycle收到无效消息
process_ecat_state_:当前总线周期的状态。ecat_slushie_sm_:应用的状态机(基于 Boost.SML)。
3. 初始化
void init() {
HardwareLayer::init();
}
解释:
调用硬件层的初始化函数,完成底层外设、CAN、EtherCAT 等的启动。
4. 应用循环
void app_loop() {
pre_app_loop();
switch (process_ecat_state_) {
case bus_cycle_result::waiting:
ecat_can_sm_.process_event(ecat_can_machine::tick{});
break;
case bus_cycle_result::have_msg:
process_new_ecat_msg();
last_ecat_cmd_ = new_ecat_cmd_;
break;
case bus_cycle_result::invalid_cycle:
ecat_can_sm_.process_event(ecat_can_machine::invalid_ecat_msg{});
break;
default:
ecat_can_sm_.process_event(ecat_can_machine::tick{});
break;
}
process_ecat_state_ = bus_cycle_result::waiting;
post_app_loop();
}
解释:
应用主循环,每个周期都执行一次:
pre_app_loop():循环前准备。- 根据
process_ecat_state_处理:- waiting:没新消息 → 给状态机发一个
tick事件。 - have_msg:有新消息 → 处理新命令,并更新
last_ecat_cmd_。 - invalid_cycle:检测到无效命令 → 给状态机发一个
invalid_ecat_msg事件。 - default:默认当作
tick。
- waiting:没新消息 → 给状态机发一个
- 每次循环结束后都重置为
waiting。 post_app_loop():循环后处理。
5. 处理 EtherCAT 输入命令
void process_cyclic_data(ecat_data::cmd_in const& cmd) {
if (cmd.mode == last_ecat_cmd_.mode) {
process_ecat_state_ = bus_cycle_result::invalid_cycle;
} else {
new_ecat_cmd_ = cmd;
process_ecat_state_ = bus_cycle_result::have_msg;
}
}
解释:
- 如果新命令
cmd.mode和上一条命令last_ecat_cmd_一样,认为是无效周期(重复命令)。 - 如果不一样,更新
new_ecat_cmd_,标记为收到新命令。
6. 获取应用状态
ecat_data::status_out get_cyclic_status() {
using ecat_data::as_status;
auto can_msg_in = HardwareLayer::get_can_msg_obj();
if (can_msg_in) {
return as_status(toggle_status(ecat_data::status_mode::can_msg), *can_msg_in);
}
uint32_t hw_status_word = HardwareLayer::get_debug_status_word();
ecat_data::status_mode status_mode = ecat_data::status_mode::hw_status1;
using boost::sml::operator""_s;
if (ecat_can_sm_.is("safety"_s)) {
status_mode = ecat_data::status_mode::can_error;
}
return as_status(toggle_status(status_mode), app_loop_count_shadow_, hw_status_word);
}
解释:
- 尝试从硬件层获取一个新的 CAN 消息:
- 如果有新 CAN 消息,就把它封装成 EtherCAT 状态返回。
- 如果没有新 CAN 消息:
- 读取硬件状态字
hw_status_word。 - 默认状态模式是
hw_status1。 - 如果状态机处于
"safety"状态,则返回can_error模式。
- 读取硬件状态字
- 用
as_status封装成status_out,供 EtherCAT 输出。
整体总结
slushie_app 是应用层的核心,它的职责是:
- 接收 EtherCAT 命令(
process_cyclic_data)。 - 运行应用逻辑状态机(
app_loop)。 - 生成 EtherCAT 输出状态(
get_cyclic_status)。 - 与底层硬件交互(通过
HardwareLayer静态接口)。
它相当于把 EtherCAT 输入命令 → 应用逻辑 → 硬件操作 → EtherCAT 输出状态 串联起来,保证周期性任务按总线时序运行。
应用层的状态机实现(machine 模板类),它和 slushie_app 结合控制 EtherCAT 总线和 CAN 总线。下面我帮你解释:
machine template <typename HardwareLayer>
struct machine {
auto operator()() const {
using namespace boost::sml;
return make_transition_table(
// ....
);
}
void reset_all_counters() { /* ... */ }
void move_to_safety() {
hw_.send_can_msg_obj(slurp_motor::can::motor_off);
hw_.status_have_error();
}
bool safety_enabled_ = true;
uint32_t tick_count_ = 0;
uint32_t invalid_count_ = 0;
HardwareLayer hw_;
};
1. 模板定义
template <typename HardwareLayer>
struct machine {
HardwareLayer是硬件层模板参数,允许状态机调用底层硬件接口(例如 CAN 消息发送、读取状态字)。
2. 状态机操作符
auto operator()() const {
using namespace boost::sml;
return make_transition_table(
// ....
);
}
- 重载了
operator()(),返回 状态机的转换表。 - 使用 Boost.SML 库实现状态机。
make_transition_table定义了状态机的 状态、事件、动作 之间的映射关系(这里省略了具体实现)。
3. 重置计数器
void reset_all_counters() { /* ... */ }
- 用于把内部计数器或状态清零(例如
tick_count_、invalid_count_),常用于初始化或复位。
4. 安全模式
void move_to_safety()
{
hw_.send_can_msg_obj(slurp_motor::can::motor_off);
hw_.status_have_error();
}
- 当出现异常或进入安全模式时:
- 通过硬件层发送 CAN 消息,把电机或设备关闭(
motor_off)。 - 更新硬件状态,标记为 错误状态(
status_have_error())。
- 通过硬件层发送 CAN 消息,把电机或设备关闭(
5. 内部成员
bool safety_enabled_ = true;
uint32_t tick_count_ = 0;
uint32_t invalid_count_ = 0;
HardwareLayer hw_;
safety_enabled_:是否允许安全模式,默认为true。tick_count_:状态机周期计数器,用于追踪运行周期。invalid_count_:无效周期计数,用于统计异常命令或错误消息。hw_:硬件层对象,调用具体硬件接口(CAN 发送/接收、状态读写等)。
整体总结
machine 类是 EtherCAT/CAN 应用的状态机核心,负责:
- 定义状态和事件的转换表(Boost.SML 实现)。
- 统计运行周期和无效事件。
- 提供安全模式接口(
move_to_safety()),确保异常情况下设备安全关闭。 - 通过
HardwareLayer调用底层硬件操作,实现与实际总线、设备的交互。
可以理解为 应用层逻辑的大脑,slushie_app通过ecat_slushie_sm_(Boost.SML 状态机实例)来控制总线和设备行为。
Boost.SML 状态机的 Transition Table(转换表),它描述了状态机在不同事件下的状态转移、动作和条件。下面我帮你理解:
1. 表头解释
Source State | Event [Guard] | Action | Dest State
- Source State:源状态,当前状态。
- Event [Guard]:事件和可选守卫条件(Guard),事件触发状态转移时检查条件。
- Action:执行的动作(函数调用、操作等)。
- Dest State:目标状态,事件发生后状态机转到的状态。
2. 示例状态
状态机包含几个主要状态:
"idle"_s:空闲状态。"waiting_msg"_s:等待消息状态。"safety"_s:安全模式状态。
3. 事件与动作
状态表里的几类事件:
event<tick>- 周期性触发(类似时钟节拍)。
- 动作如
increment_tick(增加计数器)、tick_reset(计数器复位)。
event<ecat_can_msg>- 收到 EtherCAT/CAN 消息。
- 动作可能是
send_can_msg或处理消息。
event<ecat_idle_msg>- 收到空闲消息。
- 动作如
enter_idle、send_idle_can_msg。
event<invalid_ecat_msg>- 收到无效消息。
- 可以有条件
[too_many_invalid]转到安全状态。 - 否则继续在等待消息状态。
event<disable_safety>- 关闭安全模式动作,如
safety_off。
- 关闭安全模式动作,如
event<reset>- 重置状态机。
- 动作可能是
safety_mode或tick_reset。
4. 转移逻辑示意
- 空闲状态 “idle”_s
- 收到
tick→ 保持在 “idle”,动作可能是increment_tick。 - 收到
ecat_can_msg→ 可以转到"waiting_msg"或保持在 “idle”。 - 收到
disable_safety→ 调用safety_off。
- 收到
- 等待消息状态 “waiting_msg”_s
- 收到
ecat_can_msg→ 处理消息,保持在"waiting_msg"。 - 收到
ecat_idle_msg→ 发送空闲 CAN 消息,保持"waiting_msg"。 - 收到
invalid_ecat_msg且too_many_invalid→ 转到"safety"。 - 收到
invalid_ecat_msg→ 发送空闲 CAN 消息,仍在"waiting_msg"。
- 收到
- 安全状态 “safety”_s
- 进入安全状态时触发
sml::on_entry<_>→ 进入动作safety_mode。 - 收到
reset→ 可转回"idle"或继续在"safety"。
- 进入安全状态时触发
5. 总结
- 状态机由 空闲、等待消息、安全 三个核心状态组成。
- 事件驱动状态转移,并带动作(比如发送 CAN 消息、处理周期计数器)。
- 无效消息会触发安全模式,安全模式需要显式重置才能恢复到
"idle"。 - Boost.SML 的写法允许用守卫
[too_many_invalid]来判断是否真的触发安全。 - 这张表本质上就是 状态机的完整逻辑描述,可以在代码中直接用
make_transition_table构建。
| Source State | Event [Guard] | Action | Dest State |
|---|---|---|---|
| idle | tick | — | idle |
| idle | ecat_can_msg | send_can_msg | waiting_msg |
| idle | disable_safety | safety_off | — |
| idle | sml::on_entry<_> | enter_idle | — |
| waiting_msg | ecat_can_msg | send_can_msg | waiting_msg |
| waiting_msg | ecat_idle_msg | send_idle_can_msg | waiting_msg |
| waiting_msg | invalid_ecat_msg [too_many_invalid] | — | safety |
| waiting_msg | invalid_ecat_msg | send_idle_can_msg | waiting_msg |
| waiting_msg | reset | — | idle |
| waiting_msg | tick [too_many_ticks] | — | safety |
| waiting_msg | tick | increment_tick | — |
| waiting_msg | sml::on_entry<_> | tick_reset | — |
| safety | reset | — | idle |
| safety | sml::on_entry<_> | safety_mode | — |
这段代码是一个**状态机(State Machine)**的状态转移表,用的是 C++ 的 Boost.SML 库的风格(sml::on_entry、event<>、动作 / Action 等)。我帮你用详细解释一下每一部分的意思:
状态机的状态
"idle"_s:空闲状态"waiting_msg"_s:等待消息状态"safety"_s:安全模式状态
"idle"_s 状态的行为
| 事件 | 条件/Guard | 动作 | 下一个状态 |
|---|---|---|---|
event<tick> |
无条件 | 空动作 [] {} |
保持 "idle"_s |
event<ecat_can_msg> |
无条件 | send_can_msg |
转到 "waiting_msg"_s |
event<disable_safety> |
无条件 | safety_off |
保持 "idle"_s |
sml::on_entry<_> |
无条件 | enter_idle |
保持 "idle"_s |
| 解释: |
- 当在空闲状态时,如果收到
tick,什么都不做。 - 收到以太网/控制器消息
ecat_can_msg时,发送 CAN 消息并进入等待消息状态。 - 收到
disable_safety,关闭安全模式。 - 进入
"idle"_s时,执行enter_idle动作(初始化空闲状态的一些行为)。
"waiting_msg"_s 状态的行为
| 事件 | 条件/Guard | 动作 | 下一个状态 |
|---|---|---|---|
event<ecat_can_msg> |
无条件 | send_can_msg |
保持 "waiting_msg"_s |
event<ecat_idle_msg> |
无条件 | send_idle_can_msg |
保持 "waiting_msg"_s |
event<invalid_ecat_msg> |
[too_many_invalid] |
— | "safety"_s |
event<invalid_ecat_msg> |
无条件 | send_idle_can_msg |
保持 "waiting_msg"_s |
event<reset> |
无条件 | — | "idle"_s |
event<tick> |
[too_many_ticks] |
— | "safety"_s |
event<tick> |
无条件 | increment_tick |
保持 "waiting_msg"_s |
sml::on_entry<_> |
无条件 | tick_reset |
保持 "waiting_msg"_s |
| 解释: |
- 在等待消息状态,收到新的 CAN 消息或空闲消息,会响应并保持在当前状态。
- 如果连续收到太多无效消息 (
too_many_invalid),切换到安全模式。 - 普通无效消息,则发送空闲消息继续等待。
- 收到
reset,回到空闲状态。 - 如果连续 tick 太多 (
too_many_ticks),也进入安全模式。 - 否则每次 tick 就累加计数器 (
increment_tick)。 - 进入等待消息状态时重置 tick 计数 (
tick_reset)。
"safety"_s 状态的行为
| 事件 | 条件/Guard | 动作 | 下一个状态 |
|---|---|---|---|
event<reset> |
无条件 | — | "idle"_s |
sml::on_entry<_> |
无条件 | safety_mode |
保持 "safety"_s |
| 解释: |
- 进入安全模式时执行
safety_mode动作(可能是关闭系统或报警)。 - 收到
reset后可以回到空闲状态。
总结逻辑
"idle"_s:系统空闲,可以响应 CAN 消息进入"waiting_msg"_s,也可以关闭安全模式。"waiting_msg"_s:系统在等待通信,处理有效消息,计数 tick 或无效消息,超过阈值则进入"safety"_s。"safety"_s:系统进入安全模式,必须 reset 才能回到"idle"_s。
events
namespace ecat_can_machine {
//---------------------------------
// events
struct tick {};
struct timer_expired {};
struct reset {};
struct ecat_can_msg {
uint8_t const* data;
};
struct ecat_idle_msg {};
struct invalid_ecat_msg {};
struct disable_safety {};
}
事件(events)定义,用于前面状态机的触发条件。下面逐个解释:
命名空间
namespace ecat_can_machine {
ecat_can_machine:命名空间,把所有事件都放在里面,避免与其它代码冲突。
事件定义
struct tick {};
- tick:定时器事件,表示系统的“时钟滴答”或周期性触发。
- 用途:前面状态机中用于计数、检查
too_many_ticks。
struct timer_expired {};
- timer_expired:定时器到期事件。
- 用途:可能用于触发某些超时动作(在你贴的状态机表里暂时没出现)。
struct reset {};
- reset:复位事件。
- 用途:状态机收到这个事件会回到
"idle"_s状态,清理状态。
struct ecat_can_msg {
uint8_t const* data;
};
- ecat_can_msg:EtherCAT CAN 消息事件,携带消息数据指针
data。 - 用途:表示收到有效 CAN 通信消息,用于
"idle"_s→"waiting_msg"_s的状态转换。
struct ecat_idle_msg {};
- ecat_idle_msg:EtherCAT 空闲消息事件,没有额外数据。
- 用途:状态机中
"waiting_msg"_s收到这个事件会发送空闲 CAN 消息并保持当前状态。
struct invalid_ecat_msg {};
- invalid_ecat_msg:无效 EtherCAT 消息事件。
- 用途:如果收到过多无效消息 (
too_many_invalid),会切换到"safety"_s。
struct disable_safety {};
- disable_safety:关闭安全模式事件。
- 用途:在
"idle"_s状态下触发safety_off动作,关闭安全保护。
总结
这些事件就是状态机的“触发器”,每个事件对应系统可能发生的动作或外部输入:
| 事件 | 意义 | 对应动作/状态机用途 |
|---|---|---|
tick |
时钟滴答 | 计数,检查超时 |
timer_expired |
定时器到期 | 可触发超时处理 |
reset |
复位 | 回到 "idle"_s |
ecat_can_msg |
有效 EtherCAT CAN 消息 | 进入等待消息状态,发送 CAN |
ecat_idle_msg |
空闲消息 | 保持等待消息状态,发送空闲 CAN |
invalid_ecat_msg |
无效消息 | 超过阈值进入安全模式 |
disable_safety |
关闭安全模式 | 执行 safety_off |
constexpr auto too_many_ticks = [](auto const& /*event*/, auto& sm, auto const&, auto const&) {
return sm.safety_enabled_
&& (sm.tick_count_ > sm.max_tick_count_);
};
这段代码是一个 Guard 条件(布尔函数),用在状态机里判断某个事件是否满足特定条件,从而决定状态转换是否触发。我们逐行解释:
constexpr auto too_many_ticks =
- 定义了一个
constexpr的 lambda(编译期常量),名字叫too_many_ticks。 - 在状态机中会作为 Guard 使用,即事件触发时判断条件是否成立。
[](auto const& /*event*/,
auto& sm,
auto const&,
auto const&)
- lambda 的参数列表:
event:触发事件对象(这里用/*event*/注释掉,表示没用到)。sm:状态机对象的引用,用来访问状态机内部状态。- 其他两个参数是状态机的上下文或辅助参数,这里没用,所以命名为空。
{
return sm.safety_enabled_
&& (sm.tick_count_ > sm.max_tick_count_);
};
- 返回值:布尔值,判断是否触发状态转换。
- 条件解释:
sm.safety_enabled_:安全模式是否启用。sm.tick_count_ > sm.max_tick_count_:tick 计数是否超过允许的最大值。
- 逻辑:当安全模式启用 并且 tick 计数超限时,返回
true,表示条件满足。
在状态机中的作用
- 在
"waiting_msg"_s状态下,对event<tick>使用了这个 Guard:"waiting_msg"_s + event<tick>[too_many_ticks] = "safety"_s - 理解:
如果在等待消息状态时,安全模式启用且 tick 超过最大允许值,就触发状态机跳转到
"safety"_s。
简单总结: too_many_ticks= “tick 超过上限并且安全模式启用”。- 用途 = 判断是否进入安全模式。
constexpr auto send_can_msg = [](auto const& event, auto& sm, auto const&, auto const&) {
sm.hw_.send_can_msg_obj(event.data);
};
这段代码是 状态机中的动作(Action)定义,表示当某个事件触发状态转换时要执行的具体操作。我们逐行解析:
constexpr auto send_can_msg =
- 定义了一个
constexpr的 lambda,名字叫send_can_msg。 - 在状态机中作为 动作(Action) 使用。
[](auto const& event, auto& sm, auto const&, auto const&)
- lambda 的参数:
event:触发事件对象,这里是ecat_can_msg,包含 CAN 消息数据data。sm:状态机对象的引用,可以访问状态机内部状态和硬件接口。- 后面两个参数是状态机的上下文或辅助参数,这里没有用到。
{
sm.hw_.send_can_msg_obj(event.data);
};
- 执行动作:
- 调用状态机内部的硬件接口
hw_的send_can_msg_obj方法,把事件携带的data发送出去。 - 实际效果 = 发送一条 CAN 消息到总线。
- 调用状态机内部的硬件接口
在状态机中的作用
在 "idle"_s 或 "waiting_msg"_s 状态中:
"idle"_s + event<ecat_can_msg> / send_can_msg = "waiting_msg"_s
"waiting_msg"_s + event<ecat_can_msg> / send_can_msg = "waiting_msg"_s
- 理解:
收到 EtherCAT CAN 消息后,执行
send_can_msg将数据发出去,并根据当前状态决定是否切换状态。
总结
- 动作名:
send_can_msg - 作用:把事件里的 CAN 数据通过硬件接口发送出去
- 参数:事件对象、状态机对象
- 状态机用途:当收到
ecat_can_msg时触发
constexpr auto enter_idle = [](auto const& /*event*/, auto& sm, auto const&, auto const&) {
sm.hw_.send_can_msg_obj(slush_motor::can::motor_off);
sm.hw_.status_in_idle();
sm.reset_all_counters();
};
你贴的这段代码是 状态机状态的 entry 动作(进入动作),也就是每次进入某个状态时自动执行的操作。我们逐行解释:
constexpr auto enter_idle = [](auto const& /*event*/, auto& sm, auto const&, auto const&)
- 定义了一个
constexprlambda,名字叫enter_idle - 在状态机中用作 状态进入动作(on_entry)
- 参数:
event:触发事件对象,这里没用到,所以注释掉。sm:状态机对象引用,可访问状态机内部状态和硬件接口。- 后两个参数是上下文/辅助参数,这里没用。
{
sm.hw_.send_can_msg_obj(slush_motor::can::motor_off);
sm.hw_.status_in_idle();
sm.reset_all_counters();
};
逐行解释:
sm.hw_.send_can_msg_obj(slush_motor::can::motor_off);- 向硬件发送 电机停止命令 (
motor_off) - 作用:进入空闲状态时确保电机停止。
- 向硬件发送 电机停止命令 (
sm.hw_.status_in_idle();- 通知硬件或外部系统当前状态为 空闲(idle)
- 作用:状态同步或状态指示。
sm.reset_all_counters();- 重置状态机内部的所有计数器(如
tick_count_等) - 作用:清理状态,避免旧的计数影响下一次操作。
- 重置状态机内部的所有计数器(如
在状态机中的作用
在 "idle"_s 状态表中:
"idle"_s + sml::on_entry<_> / enter_idle
- 理解:
每次进入空闲状态
"idle",自动执行enter_idle:
- 停止电机
- 更新状态为 idle
- 重置所有计数器
总结
- entry 动作名:
enter_idle - 作用:进入
"idle"状态时执行初始化和硬件动作 - 效果:
- 停止电机
- 设置状态指示为 idle
- 重置计数器
更多推荐



所有评论(0)