在机器人控制系统中,通信协议层是连接高层控制算法与底层物理硬件的「数字神经」。本页聚焦 roboto_motors 与 roboto_imu 两个模块中的通信协议实现,系统性地解析 CAN 2.0、CAN-FD 与串口三种总线的抽象架构、SocketCAN 内核驱动对接方式,以及为保障实时性所采用的线程调度与无锁队列设计。阅读本页前,建议先了解 RobotInterface 硬件抽象层 的整体定位。
Sources: motor_driver.hpp imu_driver.hpp
协议层总览与架构定位
项目将通信协议划分为ISO 抽象接口与后端实现两层。上层电机驱动(DM / EVO / LRO)与 IMU 驱动(HiPNUC)仅依赖抽象接口编程,而后端则通过 Linux SocketCAN、SocketCANFD 或 POSIX TTY 与内核交互。这种设计使得驱动代码可以在不修改业务逻辑的前提下,适配不同的物理总线或未来扩展 EtherCAT 等后端。
graph TB
subgraph 驱动层
MD[电机驱动<br/>DmMotorDriver / EvoMotorDriver / LroMotorDriver]
IMUD[IMU 驱动<br/>HipnucIMUDriver]
end
subgraph ISO 抽象层
MCAN[MotorsCAN]
MCANFD[MotorsCANFD]
IMUCAN[IMUSocketCAN]
IMUSER[IMUSerialPort]
end
subgraph 后端实现
SOC[MotorsSocketCAN]
SOCFD[MotorsSocketCANFD]
ISOC[IMUSocketCAN]
SER[IMUSerialPort]
end
subgraph Linux 内核
SKCAN[SocketCAN 子系统]
TTY[TTY 子系统]
end
MD --> MCAN
MD --> MCANFD
IMUD --> IMUCAN
IMUD --> IMUSER
MCAN --> SOC
MCANFD --> SOCFD
IMUCAN --> ISOC
IMUSER --> SER
SOC --> SKCAN
SOCFD --> SKCAN
ISOC --> SKCAN
SER --> TTY
从模块归属看,src/motors/src/protocol/ 负责电机相关的 CAN 与 CANFD 通信,src/imu/src/protocol/ 则同时提供 CAN 与串口两种选项,以兼容 HiPNUC IMU 的多接口特性。两个模块的协议层均独立编译为静态库(motors_can、motors_canfd、imu_protocol),再链接到各自的驱动库中。
Sources: CMakeLists.txt CMakeLists.txt CMakeLists.txt
ISO 抽象层:解耦驱动与后端
MotorsCAN 与 MotorsCANFD 接口
电机模块定义了两组对称的抽象基类。MotorsCAN 面向经典 CAN 2.0(最大 8 字节 payload),MotorsCANFD 面向 CAN-FD(最大 64 字节 payload)。两者均提供统一的传输、回调注册与 key extractor 定制接口。工厂方法 MotorsCAN::get(interface, backend) 与 MotorsCANFD::get(interface, backend) 目前仅实现 "socketcan" 后端,但预留了 "ethercat" 扩展点。
关键抽象包括:transmit() 负责发送单帧;add_can_callback() 按 CAN ID 注册接收回调;set_can_key_extractor() 允许驱动自定义回调键值提取逻辑——例如 DM 电机使用 master_id_ 而非原始 can_id 作为回调键,以区分多主节点场景。
Sources: can_iso.hpp canfd_iso.hpp
IMU 协议抽象
IMU 模块的通信需求与电机不同:CAN 模式下通常为单向接收传感器数据,串口模式下则需双向读写。IMUSocketCAN 提供基于 CAN ID 的回调注册,而 IMUSerialPort 以原始字节流回调暴露给上层,由 hipnuc_dec 等解码器完成协议解析。两者均通过 open() 工厂方法返回 shared_ptr,生命周期由驱动层管理。
Sources: socket_can.hpp serial_port.hpp
CAN 协议实现:SocketCAN
创建与初始化流程
MotorsSocketCAN 在构造时完成完整的 SocketCAN 初始化:创建 PF_CAN 原始套接字、通过 ioctl(SIOCGIFINDEX) 获取接口索引、bind() 绑定到指定网络接口(如 can0),随后通过 fcntl 设置为非阻塞模式。发送缓冲区被显式扩大到 1MB,以缓解高频率控制循环下的内核缓冲区溢出风险。
sequenceDiagram
participant D as 驱动层
participant S as MotorsSocketCAN
participant K as Linux Kernel
D->>S: get("can0")
S->>K: socket(PF_CAN, SOCK_RAW, CAN_RAW)
K-->>S: sockfd
S->>K: setsockopt(SO_SNDBUF, 1MB)
S->>K: ioctl(SIOCGIFINDEX, "can0")
S->>K: bind(sockfd, addr)
S->>K: fcntl(O_NONBLOCK)
S->>S: 启动 receiver_thread
S->>S: 启动 sender_thread
S-->>D: shared_ptr
Sources: socket_can.cpp
双线程收发模型
MotorsSocketCAN 内部维护独立的接收线程与发送线程,避免收发操作相互阻塞。
接收线程以 SCHED_FIFO 实时优先级 80 运行,通过 select() 等待套接字可读,随后在非阻塞循环中 read() 所有可用帧。每读到一帧,即调用 key_extractor_ 提取键值,并在 can_callback_list_ 中查找对应的回调函数执行。这种设计确保同一总线上多个电机驱动可以按 ID 精准分发报文,互不干扰。
发送线程同样具备实时优先级,从 boost::lockfree::queue 构成的 TX 队列中弹出待发送帧。若 write() 返回错误,则最多重试 3 次,每次间隔 1ms,防止在总线拥塞时陷入忙等。驱动层可通过 set_send_sleep() 在帧间插入额外微秒级延迟,以适配特定电机的接收带宽限制。
Sources: socket_can.cpp
IMU CAN 的只读简化
IMUSocketCAN 仅实现接收线程,没有独立发送线程。这是因为 HiPNUC IMU 作为传感器节点,主机侧通常只需监听其周期性广播的 J1939 报文;若需修改 IMU 配置(如波特率、输出内容),项目提供了独立的 init_imu.sh 脚本,通过 cansend 工具直接下发配置指令。
Sources: socket_can.cpp init_imu.sh
CANFD 协议实现:SocketCANFD
CANFD 实现 MotorsSocketCANFD 与经典 CAN 在代码结构上高度对称,核心差异体现在三点:
- CANFD 模式使能:创建套接字后,必须调用
setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_FD_FRAMES, &enable_canfd, ...),否则内核将拒绝收发 CANFD 帧。 - 帧结构差异:使用
canfd_frame替代can_frame,len字段最大支持 64,flags字段可携带CANFD_BRS(波特率切换)标志。 - MTU 差异:接收时按
CANFD_MTU(72 字节)读取,而非CAN_MTU(16 字节)。
EVO 与 DM 电机在 CANFD 模式下发送控制帧时,均显式设置 tx_frame.flags = CANFD_BRS,使数据段以高速率传输,提升控制带宽。LRO 电机目前仅支持 CANFD 接口,其驱动直接依赖 MotorsCANFD 抽象。
Sources: socket_canfd.cpp evo_motor_driver.cpp lro_motor_driver.hpp
串口协议实现:IMUSerialPort
termios 配置与流控策略
IMUSerialPort 使用标准 POSIX termios 接口配置串口。默认配置为:8 数据位、无校验、1 停止位(8N1),禁用硬件流控(~CRTSCTS)与软件流控(~IXON | ~IXOFF | ~IXANY),并关闭规范模式(~ICANON)和回显(~ECHO),以原始二进制流方式收发数据。波特率支持 9600 到 921600 共 8 档,通过 cfsetispeed / cfsetospeed 设置。
flowchart LR
A[open /dev/ttyUSB0] --> B[tcgetattr 备份]
B --> C{波特率匹配}
C --> D[cfsetispeed/ospeed]
D --> E[配置 c_cflag: CS8|PARENB|CSTOPB|CLOCAL|CREAD|~CRTSCTS]
E --> F[配置 c_lflag: ~ICANON|~ECHO|~ISIG]
F --> G[配置 c_iflag: ~IXON|~IXOFF|IGNBRK|...]
G --> H[VMIN=0, VTIME=1]
H --> I[tcsetattr TCSANOW]
I --> J[启动 RX 线程]
其中 VMIN = 0 与 VTIME = 1 的组合意味着:read() 在至少读到 1 字节或超时 100ms 后立即返回,避免阻塞控制循环。
Sources: serial_port.cpp
单线程接收模型
与 CAN 的双线程设计不同,串口仅维护一个 RX 线程。该线程同样以 SCHED_FIFO 优先级 80 运行,使用 select() + read() 组合读取字节流,并通过回调函数将原始数据交给 hipnuc_dec 解码器进行逐字节协议解析。串口发送当前采用同步直写(驱动层直接操作文件描述符),没有额外的 TX 队列与发送线程,这是因为 IMU 控制指令的发送频率远低于电机控制帧。
Sources: serial_port.cpp
实时性能优化设计
通信协议层的核心竞争力在于确定性延迟。本系统从线程调度、CPU 拓扑、IO 模型与内存结构四个维度进行优化:
| 优化手段 | CAN / CANFD | 串口 | 说明 |
|---|---|---|---|
| 实时优先级 | RX/TX 双线程 SCHED_FIFO 优先级 80 | RX 线程 SCHED_FIFO 优先级 80 | 避免被普通线程抢占 |
| CPU 亲和性 | 按接口名末位数字自动绑定核心 | 未显式绑定 | total_cores - 1 - port_num,实现多接口负载分散 |
| 非阻塞 IO + select | 全部使用 | 全部使用 | 防止 read/write 阻塞导致实时性劣化 |
| 无锁发送队列 | boost::lockfree::queue (4096) |
无 | 消除 TX 路径上的锁竞争 |
| 发送缓冲区扩容 | 1MB | N/A | 降低内核缓冲区满导致的丢帧概率 |
以 CPU 亲和性为例,MotorsSocketCAN 与 MotorsSocketCANFD 在初始化线程时,会提取接口名末尾数字(如 can0 取 0,can1 取 1),将其绑定到 total_cores - 1 - port_num 号逻辑核心。若系统有 8 核,则 can0 绑定到 Core 7,can1 绑定到 Core 6,依此类推,实现物理隔离与缓存亲和。
Sources: socket_can.cpp serial_port.cpp
驱动层集成模式
单例共享与总线生命周期
协议层实例按接口名单例化。对于电机驱动,MotorsCAN::get("can0") 或 MotorsCANFD::get("can1") 保证同一 CAN 总线上的多个电机共享同一个套接字与收发线程,避免重复创建内核资源。IMU 的 IMUSocketCAN::get_instance("can4") 同样遵循此模式。
回调注册与多电机分发
驱动在构造时注册接收回调,并在析构时注销。以 EVO 电机为例,其构造函数根据 interface_type 选择 CAN 或 CANFD 后端,通过 std::bind 将成员函数 canfd_rx_cbk 绑定到协议层的回调映射表中,键值为 motor_id_。当总线上出现该 ID 的反馈帧时,协议层自动路由到对应驱动实例。
EVO 与 LRO 驱动还引入了总线注册表(bus_registry_),在同一 CANFD 接口上维护所有已注册电机指针的列表。这使得后续实现「一对多」广播控制帧(如同时向同一总线上的全部电机发送使能指令)成为可能,无需遍历全部独立回调。
Sources: evo_motor_driver.cpp hipnuc_imu_driver.cpp
Key Extractor 的定制化
默认情况下,协议层以 frame.can_id 作为回调查找键。但某些电机协议(如 HiPNUC IMU 的 J1939)使用扩展帧或位掩码编码,此时驱动可通过 set_key_extractor() 提供自定义 lambda。HiPNUC 驱动即通过 frame.can_id & 0x7F 提取节点地址,确保无论 PDU 格式如何变化,都能正确匹配到 IMU 实例。
Sources: hipnuc_imu_driver.cpp
三种通信方式选型参考
| 维度 | CAN 2.0 | CAN-FD | 串口 |
|---|---|---|---|
| 典型用途 | DM 电机低速控制 | EVO / LRO 电机高速控制 | HiPNUC IMU 姿态采集 |
| 最大 Payload | 8 字节 | 64 字节 | 无理论上限(受波特率约束) |
| 控制带宽 | 中(1 Mbps) | 高(可达 5+ Mbps 数据段) | 低~中(115200~921600 bps) |
| 实时确定性 | 高(硬件仲裁 + 内核原生支持) | 高 | 中(TTY 子系统延迟波动略大) |
| 拓扑支持 | 多主多从总线 | 多主多从总线 | 点对点 |
| 项目实现线程 | RX + TX 双线程 | RX + TX 双线程 | 单 RX 线程 |
| 配置复杂度 | 需配置 ip link set canX up type can bitrate ... |
同上 + CANFD 参数 | 需确认设备节点与波特率 |
延伸阅读与下一步
通信协议层为上层驱动提供了稳定、低延迟的数据通道,但如何将协议帧解析为电机状态、又如何将控制指令编码为 MIT 模式报文,则属于驱动层的职责范畴。建议继续阅读:
- 电机驱动模块与 MIT 控制 —— 了解 DM、EVO、LRO 三种电机驱动如何封装协议帧
- IMU 驱动与姿态获取 —— 深入 HiPNUC 的 J1939 与自定义二进制协议解码
- 扩展新电机驱动类型 —— 若需接入新品牌电机,可参照本文的 ISO 抽象层模式实现新后端