这几行是在讲 EtherCAT(工业以太网实时通讯协议)中时钟同步的可选方案,理解如下:

EtherCAT 可选时钟(optional clock options)

  1. TwinCAT clock(TwinCAT 时钟)
    • TwinCAT 是 Beckhoff 的工业自动化软件
    • 使用 TwinCAT 系统自身的内部时钟作为 EtherCAT 从站同步的参考
    • 优点:无需额外硬件,直接使用控制器内部时钟
  2. DC Ref. clock(Distributed Clock 参考时钟)
    • DC(Distributed Clocks,分布式时钟)是 EtherCAT 的一种同步机制
    • 使用网络中某个设备的 DC 作为全网参考时钟
    • EtherCAT 从站通过 DC 信号自动同步到主站的时间
    • 优点:精度高,适合实时控制
  3. 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)同步周期中事件的时间关系。我来帮你逐段解释:

关键元素

  1. Sync0
    • 这是 EtherCAT 同步信号(通常与控制周期绑定)。
    • Cycle Time (0x1C32.2/0x1C33.2):配置的周期时间(主站给从站定义的周期)。
    • 每个周期都由 Sync0 信号触发。
  2. Min Cycle Time (0x1C32.5/0x1C33.5)
    • 定义最小允许的周期时间。
    • 如果实际周期小于此值,可能导致设备无法及时处理数据。
  3. PDO_OutputMapping() / PDO_InputMapping()
    • PDO(Process Data Object)过程数据对象:EtherCAT 中循环交换的数据。
    • PDO_OutputMapping():主站将输出数据写入从站(即给设备下发控制指令)。
    • PDO_InputMapping():主站从从站读取输入数据(即采集设备状态/传感器数据)。
  4. ECAT_Application()
    • 应用层逻辑,即实际的用户程序(比如控制算法)。
    • 发生在 PDO 映射之后。
  5. Delay Time (0x1C32.9 / 0x1C33.9)
    • 输入或输出的延迟时间,决定信号在 Sync0 触发点之后的偏移。
  6. 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 同步信号和应用任务执行时序 的示意图。下面逐段解释:

关键要素

  1. Sync0 / Sync1
    • EtherCAT 分布式时钟机制提供的硬件同步脉冲信号。
    • Sync0:常用来触发周期性任务,比如驱动输出更新、控制环计算。
    • Sync1:可选的第二个同步脉冲,用于输入采样(Latch 输入)或双边沿控制。
  2. Cycle Time (0x1C32.2 / 0x1C33.2)
    • 周期时间寄存器,定义了 Sync0/Sync1 的周期。
    • 每个周期开始时触发 Sync 信号,保证主站和从站严格对齐。
  3. PDO 映射 (PDO_OutputMapping / PDO_InputMapping)
    • PDO_OutputMapping:主站写数据到从站的过程(输出有效时间点)。
    • PDO_InputMapping:从站把采样到的输入数据映射到 PDO,供主站读取。
  4. ECAT_Application()
    • EtherCAT 应用任务,即用户的控制算法或数据处理逻辑,运行在同步周期内。
  5. Calc and Copy Time (0x1C32.6 / 0x1C33.6)
    • 数据计算和拷贝的耗时。
    • 表示从收到数据到准备好输出之间所需的处理延迟。
  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 SSCC 库,因此在 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)。

总结

  1. 问题:如果直接用 SSC 的 process_app / MainLoop,代码会丑陋、难维护。
  2. 改进:通过 C++ 应用类slushie_app)把逻辑和协议栈解耦。
  3. 分层
    • 协议栈层(SSC):负责 EtherCAT 状态机、PDO 通信。
    • 适配层(extern “C” 函数)process_app,桥接 SSC 和 C++ 应用。
    • 应用层(C++ 类):业务逻辑、测试、模拟。
  4. 好处
    • 可测试(应用层不用依赖 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();
 };
}

逐项说明

  1. static void init();
    → 硬件初始化接口。
    通常用来配置 MCU 的外设、总线、GPIO、CAN 控制器、定时器等。
  2. static void tick();
    → 硬件的周期性“心跳”函数。
    在主循环或定时器中被调用,用来驱动底层硬件的周期任务(比如刷 CAN 缓冲区、更新状态寄存器)。
  3. static bool send_can_msg_obj(uint8_t const* data);
    → 发送一个 CAN 报文。
    data 指向待发送的数据(比如打包好的报文结构体),返回值表示是否成功。
  4. static std::optional<can_msg_pack_t> get_can_msg_obj();
    → 从硬件(CAN 接收缓冲区)获取一个 CAN 报文。
    • 如果有报文,就返回 can_msg_pack_t
    • 如果没有,就返回 std::nullopt
      std::optional 可以避免额外的标志位。
  5. static uint32_t get_debug_status_word();
    → 获取硬件的调试状态字(status word)。
    常见用法是把内部状态寄存器、错误码、计数器等打包成一个 32 位数,方便上层调试和 EtherCAT 上传。
  6. static void status_in_idle();
    → 设置当前硬件处于 空闲状态
    可能会更新 LED 指示灯、状态机标志位。
  7. static void status_in_op();
    → 设置当前硬件处于 运行状态(Operational)
    一般对应 EtherCAT OP 模式。
  8. static void status_have_error();
    → 标记硬件进入 错误状态
    通常在 CAN 错误、总线异常或安全停机时调用。
  9. static void status_clear_error();
    → 清除错误状态。
    让系统从错误状态恢复,可能需要主站控制触发。
  10. static void status_have_ecat_msg();
    → 标记已经收到一个 EtherCAT 消息。
    一般用于通知状态机“有新数据”,触发相应处理。

总结

这个 hardware 结构体就是一个 硬件抽象层接口

  • 输入/输出:通过 send_can_msg_obj / get_can_msg_obj 和外部总线(CAN、EtherCAT)交互。
  • 状态获取get_debug_status_word 提供调试信息。
  • 状态管理status_in_idlestatus_in_opstatus_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();
 }
}

逐行解释

  1. slushie_app<xmc::hardware::hardware> app;
    • 定义了一个应用对象 app,它的模板参数是 xmc::hardware::hardware,也就是说这个应用将运行在 XMC 硬件抽象层 之上。
    • 这样可以把业务逻辑(slushie_app)和底层硬件实现(hardware layer)解耦。
  2. 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,供主站读取。
  3. extern "C" { void MainLoop(void); }
    • 声明了一个 来自 SSC 的主循环函数 MainLoop
    • 这个函数在 Beckhoff 提供的 EtherCAT 从站协议栈里实现,负责处理 EtherCAT 状态机和通信。
  4. int main() { ... }
    程序入口:
    • app.init();
      调用应用的初始化函数 → 设置硬件、状态机、内部变量。
    • while (1U) { ... }
      无限循环,相当于嵌入式系统的主循环。
      循环内两件事:
      1. MainLoop();
        运行 EtherCAT 协议栈的循环处理,保持和主站的通信与状态机。
      2. app.app_loop();
        应用层自己的循环处理逻辑(比如 CAN 消息处理、状态机更新、业务逻辑)。

整体总结

这个程序的核心逻辑就是把 EtherCAT 从站协议栈(SSC)应用逻辑(slushie_app) 结合起来:

  1. EtherCAT 协议栈调用 process_app → 输入 PDO 交给应用处理,输出 PDO 由应用生成。
  2. 主函数 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:总线周期结果,有三种:
    1. waiting 等待新消息
    2. have_msg 收到新消息
    3. 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();
}

解释:
应用主循环,每个周期都执行一次:

  1. pre_app_loop():循环前准备。
  2. 根据 process_ecat_state_ 处理:
    • waiting:没新消息 → 给状态机发一个 tick 事件。
    • have_msg:有新消息 → 处理新命令,并更新 last_ecat_cmd_
    • invalid_cycle:检测到无效命令 → 给状态机发一个 invalid_ecat_msg 事件。
    • default:默认当作 tick
  3. 每次循环结束后都重置为 waiting
  4. 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);
}

解释:

  1. 尝试从硬件层获取一个新的 CAN 消息:
    • 如果有新 CAN 消息,就把它封装成 EtherCAT 状态返回。
  2. 如果没有新 CAN 消息:
    • 读取硬件状态字 hw_status_word
    • 默认状态模式是 hw_status1
    • 如果状态机处于 "safety" 状态,则返回 can_error 模式。
  3. as_status 封装成 status_out,供 EtherCAT 输出。

整体总结

slushie_app 是应用层的核心,它的职责是:

  1. 接收 EtherCAT 命令process_cyclic_data)。
  2. 运行应用逻辑状态机app_loop)。
  3. 生成 EtherCAT 输出状态get_cyclic_status)。
  4. 与底层硬件交互(通过 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();
}
  • 当出现异常或进入安全模式时:
    1. 通过硬件层发送 CAN 消息,把电机或设备关闭(motor_off)。
    2. 更新硬件状态,标记为 错误状态status_have_error())。

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 应用的状态机核心,负责:

  1. 定义状态和事件的转换表(Boost.SML 实现)。
  2. 统计运行周期和无效事件。
  3. 提供安全模式接口(move_to_safety()),确保异常情况下设备安全关闭。
  4. 通过 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. 事件与动作

状态表里的几类事件:

  1. event<tick>
    • 周期性触发(类似时钟节拍)。
    • 动作如 increment_tick(增加计数器)、tick_reset(计数器复位)。
  2. event<ecat_can_msg>
    • 收到 EtherCAT/CAN 消息。
    • 动作可能是 send_can_msg 或处理消息。
  3. event<ecat_idle_msg>
    • 收到空闲消息。
    • 动作如 enter_idlesend_idle_can_msg
  4. event<invalid_ecat_msg>
    • 收到无效消息。
    • 可以有条件 [too_many_invalid] 转到安全状态。
    • 否则继续在等待消息状态。
  5. event<disable_safety>
    • 关闭安全模式动作,如 safety_off
  6. event<reset>
    • 重置状态机。
    • 动作可能是 safety_modetick_reset

4. 转移逻辑示意

  1. 空闲状态 “idle”_s
    • 收到 tick → 保持在 “idle”,动作可能是 increment_tick
    • 收到 ecat_can_msg → 可以转到 "waiting_msg" 或保持在 “idle”。
    • 收到 disable_safety → 调用 safety_off
  2. 等待消息状态 “waiting_msg”_s
    • 收到 ecat_can_msg → 处理消息,保持在 "waiting_msg"
    • 收到 ecat_idle_msg → 发送空闲 CAN 消息,保持 "waiting_msg"
    • 收到 invalid_ecat_msgtoo_many_invalid → 转到 "safety"
    • 收到 invalid_ecat_msg → 发送空闲 CAN 消息,仍在 "waiting_msg"
  3. 安全状态 “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_entryevent<>、动作 / 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 后可以回到空闲状态。

总结逻辑

  1. "idle"_s:系统空闲,可以响应 CAN 消息进入 "waiting_msg"_s,也可以关闭安全模式。
  2. "waiting_msg"_s:系统在等待通信,处理有效消息,计数 tick 或无效消息,超过阈值则进入 "safety"_s
  3. "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 的参数列表:
    1. event:触发事件对象(这里用 /*event*/ 注释掉,表示没用到)。
    2. sm:状态机对象的引用,用来访问状态机内部状态。
    3. 其他两个参数是状态机的上下文或辅助参数,这里没用,所以命名为空。
{
    return sm.safety_enabled_
        && (sm.tick_count_ > sm.max_tick_count_);
};
  • 返回值:布尔值,判断是否触发状态转换。
  • 条件解释:
    1. sm.safety_enabled_:安全模式是否启用。
    2. 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 的参数:
    1. event:触发事件对象,这里是 ecat_can_msg,包含 CAN 消息数据 data
    2. sm:状态机对象的引用,可以访问状态机内部状态和硬件接口。
    3. 后面两个参数是状态机的上下文或辅助参数,这里没有用到。
{
    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&)
  • 定义了一个 constexpr lambda,名字叫 enter_idle
  • 在状态机中用作 状态进入动作(on_entry)
  • 参数:
    1. event:触发事件对象,这里没用到,所以注释掉。
    2. sm:状态机对象引用,可访问状态机内部状态和硬件接口。
    3. 后两个参数是上下文/辅助参数,这里没用。
{
    sm.hw_.send_can_msg_obj(slush_motor::can::motor_off);
    sm.hw_.status_in_idle();
    sm.reset_all_counters();
};

逐行解释:

  1. sm.hw_.send_can_msg_obj(slush_motor::can::motor_off);
    • 向硬件发送 电机停止命令 (motor_off)
    • 作用:进入空闲状态时确保电机停止。
  2. sm.hw_.status_in_idle();
    • 通知硬件或外部系统当前状态为 空闲(idle)
    • 作用:状态同步或状态指示。
  3. sm.reset_all_counters();
    • 重置状态机内部的所有计数器(如 tick_count_ 等)
    • 作用:清理状态,避免旧的计数影响下一次操作。

在状态机中的作用

"idle"_s 状态表中:

"idle"_s + sml::on_entry<_> / enter_idle
  • 理解:

每次进入空闲状态 "idle",自动执行 enter_idle

  1. 停止电机
  2. 更新状态为 idle
  3. 重置所有计数器

总结

  • entry 动作名enter_idle
  • 作用:进入 "idle" 状态时执行初始化和硬件动作
  • 效果
    1. 停止电机
    2. 设置状态指示为 idle
    3. 重置计数器

更多推荐