🤖 roboto_origin_03 Wiki
首页 / 电机子模块 / CAN-FD扩展帧支持

CAN-FD 扩展帧(Extended Frame,29 位标识符)是本 SDK 在支持大容量、多节点电机控制场景下的重要传输能力。本文从协议抽象层、SocketCAN 后端实现、回调路由机制三个维度,系统阐述 SDK 如何透明地承载 29 位扩展帧,并解析 LeadRobot 驱动在多电机批控场景中对该能力的具体应用。阅读本文前,建议先了解 CAN/CANFD协议抽象接口 中的基础类设计。

Sources: canfd_iso.hpp, socket_canfd.hpp

CAN-FD 扩展帧与标准帧的差异

在 Linux SocketCAN 体系中,标准帧(Standard Frame)使用 11 位标识符(0x0000x7FF),扩展帧使用 29 位标识符(0x000000000x1FFFFFFF)。二者共享同一套 canfd_frame 结构体,内核通过 can_id 字段的最高位 CAN_EFF_FLAG0x80000000)区分帧类型。CAN-FD 物理层在扩展帧模式下依然支持最高 64 字节的有效载荷与比特率切换(BRS),因此扩展帧并未牺牲 CAN-FD 的核心带宽优势。

特性 标准 CAN-FD 帧 扩展 CAN-FD 帧
标识符长度 11 bit 29 bit
内核区分标志 CAN_EFF_FLAG
最大数据长度 64 byte 64 byte
典型应用场景 单电机点对点控制 广播/多电机聚合指令
ID 范围 0x00x7FF 0x00x1FFFFFFF

Sources: socket_canfd.cpp

协议抽象层的原生支持

SDK 的 CAN-FD 协议抽象层直接复用 Linux 内核头文件 <linux/can.h> 中定义的 canfd_frame,因此天然兼容扩展帧,无需额外的封装或转换逻辑。MotorsCANFD 接口以 canfd_frame 为最小传输单元,所有发送与接收 API 均直接操作该结构体,使得扩展帧的 29 位 can_idCANFD_BRSCANFD_FDF 等标志位在上下层之间无损传递。

// 回调函数直接接收内核原生 canfd_frame
using CanFdCbkFunc = std::function<void(const canfd_frame&)>;

// 抽象接口:发送与订阅均不区分标准/扩展帧
virtual void transmit(const canfd_frame& frame) = 0;
virtual void add_canfd_callback(const CanFdCbkFunc& callback, CanFdCbkId id) = 0;

由于 canfd_frame.can_id 的类型为 canid_t(即 __u32),驱动层在填充帧时可以直接执行 tx_frame.can_id = 0x8080 | CAN_EFF_FLAG,抽象层与后端对此完全无感知。

Sources: canfd_iso.hpp

SocketCAN 后端的透明传输

MotorsSocketCANFD 在初始化阶段通过 setsockopt 开启 CAN_RAW_FD_FRAMES,随后将原始套接字绑定到指定 CAN 接口。一旦内核启用 CAN-FD,标准帧与扩展帧的收发均由内核协议栈统一处理,用户态无需针对扩展帧编写额外的解析逻辑。

以下 Mermaid 图展示了扩展帧在 SocketCAN 后端中的完整数据流:

flowchart LR
    A[驱动层<br/>填充 canfd_frame] -->|transmit| B[无锁发送队列<br/>boost::lockfree::queue]
    B -->|sender_thread| C[SocketCAN 套接字<br/>write()]
    D[内核接收缓冲区] -->|receiver_thread| E[select + read]
    E -->|key_extractor| F[回调路由表<br/>CanFdCbkMap]
    F -->|命中| G[驱动回调<br/>canfd_rx_cbk]

在接收线程中,::read(sockfd_, &rx_frame, CANFD_MTU) 一次性读取完整 canfd_frame,随后调用 key_extractor_rx_frame.can_id 中提取回调键值。由于套接字工作在 CAN_RAW 模式,内核会原样上报所有帧的 can_id(含 CAN_EFF_FLAG),用户态代码只需关注业务解析。

Sources: socket_canfd.cpp, socket_canfd.hpp

回调路由与键提取机制

扩展帧在 SDK 中的主要约束并非来自传输层,而来自回调路由层的键值空间CanFdCbkId 被定义为 uint16_t,默认键提取器将 frame.can_id 直接强制转换为 uint16_t

CanFdCbkKeyExtractor key_extractor_ = [](const canfd_frame& frame) -> CanFdCbkId {
    return static_cast<CanFdCbkId>(frame.can_id);
};

这意味着:

  1. 标准帧场景can_id 位于 0x0000x7FF,直接映射为 uint16_t,无信息损失。
  2. 扩展帧场景can_id 包含 CAN_EFF_FLAG(第 31 位)与最高 29 位 ID。强制转换后仅保留低 16 位,CAN_EFF_FLAG 被截断,高于 16 位的 ID 段亦丢失。
帧类型 原始 can_id 转换后 CanFdCbkId 信息损失
标准帧 0x123 0x00000123 0x0123
扩展帧 0x8080|CAN_EFF_FLAG 0x80008080 0x8080 丢失标志位
扩展帧 0x1A0B0C0D|CAN_EFF_FLAG 0x9A0B0C0D 0x0C0D 丢失高 13 位及标志位

在本 SDK 的现有驱动实现中,扩展帧仅被用于广播发送(如 LeadRobot 的 0x8080 | CAN_EFF_FLAG),而电机回传帧仍使用各自的标准 ID。因此默认提取器足以完成回调路由。若未来需要基于完整 29 位扩展 ID 做回调匹配,可调用 set_canfd_key_extractor() 注入自定义提取逻辑。

Sources: socket_canfd.hpp, socket_canfd.cpp

驱动层实践:LeadRobot 多电机批控

LeadRobot(LRO)驱动是目前唯一显式使用 CAN-FD 扩展帧的模块。在其多电机 MIT 批控接口 motor_mit_cmd(float* ...) 中,驱动将 8 台电机的控制字聚合到单帧 64 字节 payload 中,并通过扩展帧标识符 0x8080 | CAN_EFF_FLAG 进行总线广播:

canfd_frame tx_frame;
tx_frame.can_id = 0x8080 | CAN_EFF_FLAG;  // 29 位扩展 ID
tx_frame.len = 64;                          // CAN-FD 满负荷
tx_frame.flags = CANFD_BRS;

for (uint8_t slot = 0; slot < 8; ++slot) {
    // 每个 slot 8 字节,依次写入对应 motor_index_ 的 MIT 控制字
}

该设计的核心价值在于总线效率:一次传输即可完成 8 台电机的指令下发,显著降低多轴高速控制环的通信抖动。各电机在总线上接收到同一扩展帧后,根据帧内 slot 偏移(由 motor_index_ 决定)提取属于自己的 8 字节数据。回传阶段,各电机仍使用独立的标准 ID 向主站返回状态,因此回调路由不受影响。

对比之下,EVO 驱动的多电机批控虽然同样使用 64 字节 CAN-FD,但采用的是标准 ID 0x20EVOFD_MIT_ID),说明扩展帧的选择取决于电机厂商的协议规范,而非 SDK 本身的硬性要求。

Sources: lro_motor_driver.cpp, evo_motor_driver.cpp, evo_motor_driver.hpp

扩展帧使用边界与定制方法

尽管 SocketCAN 后端对扩展帧完全透明,开发者在使用时仍需注意以下边界条件:

1. 回调键冲突风险 若总线上同时存在标准 ID 0x8080 与扩展 ID 0x00008080 的帧,二者经默认提取器后均得到键值 0x8080,可能导致回调误触发。解决方案是在注册回调前调用 set_canfd_key_extractor(),例如将扩展帧的键值与标志位分离:

canfd->set_canfd_key_extractor([](const canfd_frame& frame) -> CanFdCbkId {
    if (frame.can_id & CAN_EFF_FLAG) {
        return 0x8000 | (frame.can_id & 0x7FFF);  // 自定义编码,避免与标准 ID 冲突
    }
    return static_cast<CanFdCbkId>(frame.can_id);
});

2. 发送端标志位显式设置 扩展帧的发送方必须显式置位 CAN_EFF_FLAG,否则内核将按标准帧发送,导致接收端解析错误。LeadRobot 驱动的实现已遵循此规范。

3. 接口层兼容性 SocketCAN 接口需在 bring-up 时启用 CAN-FD(fd on),扩展帧才能与长数据段同时工作。若接口仅配置为标准 CAN,内核将拒绝发送 CAN-FD 帧。

Sources: socket_canfd.cpp, socket_canfd.hpp

小结

本 SDK 对 CAN-FD 扩展帧的支持建立在 Linux 内核 canfd_frame 的原生能力之上:协议抽象层与 SocketCAN 后端均不对 can_id 做标准/扩展的二次区分,从而实现透明传输。回调路由层以 uint16_t 为键空间,默认策略对现有电机驱动(扩展帧仅用于广播发送、回传仍为标准 ID)已足够。LeadRobot 驱动充分利用扩展帧 + 64 字节 payload 的聚合优势,实现了单帧 8 电机的批控下发。若需支持更复杂的 29 位 ID 回调匹配,可通过 set_canfd_key_extractor() 扩展路由逻辑而不必改动后端实现。

继续深入阅读: