本文档面向需要在 roboto_motors 框架中接入全新电机硬件的高级开发者。你将从架构原理出发,系统掌握如何通过继承 MotorDriver 抽象基类、注册工厂方法、绑定通信协议后端,以及可选地实现多电机批处理(One-to-Many)模式,从而将一款全新的电机(例如 XYN 系列)无缝集成到现有机器人软件栈中。阅读前建议先理解 RobotInterface 硬件抽象层 与 通信协议层:CAN/CANFD 与串口 的设计逻辑。
Sources: motor_driver.hpp
驱动架构总览
整个电机驱动子系统采用抽象基类 + 静态工厂 + 协议后端代理的三层架构。MotorDriver 定义了所有电机驱动必须实现的统一接口,涵盖锁止/解锁、初始化、MIT 阻抗控制、位置/速度模式、参数烧录与错误清除等生命周期方法。具体的电机品牌(DM、EVO、LRO)通过派生类实现这些纯虚函数,并在内部持有 MotorsCAN 或 MotorsCANFD 的共享实例来完成真实总线通信。工厂方法 MotorDriver::create_motor() 根据传入的 motor_type 字符串分发到对应的派生类构造器,上层业务代码(如 Python SDK 或推理节点)因此无需关心底层硬件差异。
下图展示了现有类的继承与组合关系:
classDiagram
class MotorDriver {
+create_motor()$
+lock_motor()* void
+unlock_motor()* void
+init_motor()* uint8_t
+deinit_motor()* void
+motor_mit_cmd(float...)* void
+motor_mit_cmd(float*)* void
+motor_pos_cmd(float, float, bool)* void
+motor_spd_cmd(float)* void
+set_motor_control_mode(uint8_t)* void
+refresh_motor_status()* void
+clear_motor_error()* void
+set_motor_zero()* bool
+write_motor_flash()* bool
+get_motor_param(uint8_t)* void
+set_motor_id(uint8_t, uint8_t)* void
+reset_motor_id()* void
}
class DmMotorDriver {
+can_rx_cbk()
+canfd_rx_cbk()
-can_ : shared_ptr~MotorsCAN~
-canfd_ : shared_ptr~MotorsCANFD~
}
class EvoMotorDriver {
+can_rx_cbk()
+canfd_rx_cbk()
-can_ : shared_ptr~MotorsCAN~
-canfd_ : shared_ptr~MotorsCANFD~
-bus_registry_$
}
class LroMotorDriver {
+canfd_rx_cbk()
-canfd_ : shared_ptr~MotorsCANFD~
-bus_registry_$
}
class MotorsCAN {
+transmit(can_frame)
+add_can_callback(...)
}
class MotorsCANFD {
+transmit(canfd_frame)
+add_canfd_callback(...)
}
MotorDriver <|-- DmMotorDriver
MotorDriver <|-- EvoMotorDriver
MotorDriver <|-- LroMotorDriver
DmMotorDriver --> MotorsCAN
DmMotorDriver --> MotorsCANFD
EvoMotorDriver --> MotorsCAN
EvoMotorDriver --> MotorsCANFD
LroMotorDriver --> MotorsCANFD
Sources: motor_driver.cpp, lro_motor_driver.hpp, dm_motor_driver.hpp
核心设计模式
在动手编码前,必须理解三个贯穿所有现有驱动的核心设计模式,它们决定了新驱动的代码组织结构。
静态工厂分发。create_motor() 是唯一的对象创建入口,它根据 motor_type 字符串进行条件分支。新增驱动时,必须在这里追加一条 else if 分支,将字符串映射到你的派生类构造器。这个模式保证了 ROS2 节点和 Python SDK 在配置文件中只需修改 motor_type 字段即可完成硬件切换。
flowchart LR
A["调用 create_motor<br/>motor_type='XYN'"] --> B{工厂判断}
B -->|"DM"| C[构造 DmMotorDriver]
B -->|"EVO"| D[构造 EvoMotorDriver]
B -->|"LRO"| E[构造 LroMotorDriver]
B -->|"XYN"| F[构造 XynMotorDriver]
协议后端透明化。MotorsCAN 与 MotorsCANFD 是通信层的抽象接口,底层目前由 Linux SocketCAN 实现。电机驱动类通过 MotorsCAN::get(interface) 或 MotorsCANFD::get(interface) 获取共享实例,再调用 transmit() 发送原始帧。这意味着你的新驱动只需组装正确的 can_frame 或 canfd_frame,无需关心套接字文件描述符或 EtherCAT 桥接细节。
总线注册表(Bus Registry)与多电机批处理。EVO 与 LRO 驱动支持 motor_mit_cmd(float* ...) 指针重载,用于推理循环中的高效率批处理控制。实现该功能需要在每个驱动实例的构造函数中,将 this 指针注册到以 CAN 接口名为键的静态 bus_registry_ 映射表中;析构时注销。发送批处理帧时,遍历同一接口下的所有实例,按 motor_index_ 将每个电机的 8 字节控制字写入 64 字节 CAN-FD 报文的对应槽位,最后调用一次 transmit() 完成整条总线上最多 8 个电机的同步下发。
flowchart TD
A["推理线程调用<br/>motor_mit_cmd(float*)"] --> B[遍历 bus_registry_]
B --> C["按 motor_index_ 将各电机<br/>控制字写入 64 字节帧"]
C --> D["单次 canfd_->transmit<br/>完成整批下发"]
D --> E["各电机实例的<br/>canfd_rx_cbk 独立解析回帧"]
Sources: motor_driver.cpp, canfd_iso.hpp, lro_motor_driver.cpp
扩展实践:以 XYN 电机为例
以下步骤演示如何从零开始接入一款假设的 XYN 系列电机。现有代码库中已预留了 xyn 的注释占位,可作为实际工程参考。
第一步:新建目录与构建配置
在 src/motors/src/drivers/ 下新建 xyn/ 目录,并创建其 CMakeLists.txt。参照 EVO 与 LRO 的写法,将其编译为静态库,并显式链接所需的协议后端。如果 XYN 仅使用 CAN-FD,则只需依赖 motors_canfd;若同时兼容经典 CAN,则一并链接 motors_can。
新增文件 src/motors/src/drivers/xyn/CMakeLists.txt:
AUX_SOURCE_DIRECTORY(. SOURCE_LIST_XYN_MOTORS)
add_library(xyn_motors
STATIC
${SOURCE_LIST_XYN_MOTORS}
)
target_include_directories(xyn_motors PUBLIC ./ ${PROJECT_SOURCE_DIR}/include ${PROJECT_SOURCE_DIR}/src)
target_link_libraries(xyn_motors PUBLIC ${PUBLIC_DEPENDENCIES} motors_canfd)
随后将 xyn 加入驱动聚合构建。修改 src/motors/src/drivers/CMakeLists.txt,追加一行:
add_subdirectory(xyn)
Sources: drivers/CMakeLists.txt, evo/CMakeLists.txt
第二步:定义协议常量与型号枚举
为保持代码自描述性,应将 XYN 电机的错误码、型号、控制模式、配置命令与反馈类型定义为强类型枚举或宏常量。参考 LRO 的头部设计,这些定义与类声明放在同一个头文件中。
| 枚举/结构 | 用途 | 参照实现 |
|---|---|---|
XYNError |
电机错误码解析(过温、过流、编码器故障等) | LROError |
XYN_Motor_Model |
支持的型号列表,末尾以 Num_Of_Motor 结束,用于索引限参表 |
LRO_Motor_Model |
XYNMotorMode |
控制模式字节(MIT、POS、SPD、CUR 等) | LROMotorMode |
XYNSetupCmd |
0x7FF 广播 ID 下的配置命令(设零、改 ID、使能/失能) | LROSetupCmd |
XYN_Limit_Param |
各型号的物理极限参数结构体(PosMax, SpdMax, TauMax, OKpMax, OKdMax) | LRO_Limit_Param |
在头文件中还需要声明派生类 XynMotorDriver,它公有继承自 MotorDriver。除重写所有纯虚函数外,若计划支持批处理 MIT,需额外声明 bus_registry_mutex_、bus_registry_ 静态成员以及 motor_index_ 字段。
Sources: lro_motor_driver.hpp
第三步:实现核心虚函数
构造函数是驱动的初始化核心。你需要完成以下动作:
- 校验
interface_type是否被本驱动支持; - 根据
motor_model从全局参数表中加载limit_param_; - 保存
motor_id_、can_interface_与motor_zero_offset_; - 实例化协议后端(如
MotorsCANFD::get(can_interface)); - 注册接收回调(
add_canfd_callback),使得总线回帧能路由到本实例的canfd_rx_cbk; - (可选)若支持批处理,将
this加入bus_registry_[can_interface]。
析构函数则需对称执行:移除回调、从注册表中擦除自身,防止悬空指针。
必须实现的纯虚函数清单如下:
| 方法 | 功能要求 | 典型实现要点 |
|---|---|---|
lock_motor() |
电机使能 | 按协议组装使能帧并 transmit |
unlock_motor() |
电机失能 | 组装失能帧并 transmit |
init_motor() |
上电初始化序列 | 通常先 unlock,设模式为 MIT,再 lock,最后 refresh_motor_status 检查错误 |
deinit_motor() |
下电清理 | 调用 unlock_motor() |
set_motor_zero() |
当前位置设为零点 | 发送设零命令,延时后读取位置,判断是否在阈值 judgment_accuracy_threshold 内 |
write_motor_flash() |
参数持久化 | 部分电机自动保存(如 LRO),部分需显式发送烧录命令(如 EVO) |
get_motor_param() |
查询内部参数 | 发送查询帧,结果通常在 canfd_rx_cbk 中异步解析 |
motor_pos_cmd() |
位置模式控制 | 将弧度转换为电机协议单位,进行 range_map 限幅后打包 |
motor_spd_cmd() |
速度模式控制 | 同上,注意单位换算(rad/s → RPM 或内部单位) |
motor_mit_cmd(float...) |
单电机 MIT 控制 | 将 f_p/f_v/f_kp/f_kd/f_t 按协议位域压入 8 字节数据区;注意先减去 motor_zero_offset_ |
motor_mit_cmd(float*) |
多电机批处理 MIT | 遍历 bus_registry_,按 motor_index_ 写入 64 字节帧槽位,统一发送 |
set_motor_control_mode() |
切换控制模式 | 更新 motor_control_mode_;建议切到 MIT 时先发送零指令防止跃变 |
refresh_motor_status() |
请求实时状态 | 可发送零 MIT 指令触发反馈,或发送专用状态查询帧 |
clear_motor_error() |
清除错误标志 | 通常通过先 disable 再 enable 实现,或发送专用清错命令 |
set_motor_id() |
修改电机硬件 ID | 使用 0x7FF 广播 ID 发送带旧 ID 与新 ID 的配置帧 |
reset_motor_id() |
恢复出厂 ID | 广播重置命令 |
对于接收回调 canfd_rx_cbk,你需要解析 canfd_frame 的 data[] 字节,提取位置、速度、电流/力矩、温度与错误码,并写入对应的 std::atomic 成员变量(如 motor_pos_、error_id_)。MotorDriver 基类已为这些状态量提供了原子存储,确保多线程下的读安全。
Sources: lro_motor_driver.cpp, lro_motor_driver.cpp, lro_motor_driver.cpp
第四步:注册工厂与 Python 绑定
完成派生类实现后,还需在三处进行注册,才能让上层代码感知到新驱动。
1. 工厂注册:在 src/motors/src/motor_driver.cpp 中包含新头文件,并在 create_motor() 中追加分支:
#include "xyn_motor_driver.hpp"
// ...
} else if (motor_type == "XYN") {
return std::make_shared<XynMotorDriver>(motor_id, interface_type, interface,
static_cast<XYN_Motor_Model>(motor_model), motor_zero_offset);
}
2. 顶层构建链接:在 src/motors/CMakeLists.txt 中,将 xyn_motors 加入 motors 主库与安装目标:
target_link_libraries(motors PUBLIC ${PUBLIC_DEPENDENCIES}
dm_motors evo_motors lro_motors xyn_motors
motors_can motors_canfd
)
install(TARGETS motors dm_motors evo_motors lro_motors xyn_motors motors_can motors_canfd ...)
ament_export_libraries(motors dm_motors evo_motors lro_motors xyn_motors motors_can motors_canfd)
3. Python 绑定注册:若需 Python SDK 支持,修改 src/motors/src/pybind_module.cpp,包含头文件并在模块中暴露相关枚举(如有):
#include "drivers/xyn/xyn_motor_driver.hpp"
Python 侧通过 MotorDriver.create_motor(..., motor_type="XYN", ...) 即可创建实例,无需额外绑定代码,因为 MotorDriver 基类的接口已由 pybind11 统一暴露。
Sources: motor_driver.cpp, CMakeLists.txt, pybind_module.cpp
多电机批处理模式详解
在强化学习推理循环中,每毫秒需要对十几个关节同时下发 MIT 指令。如果逐个调用单电机接口,总线开销和上下文切换将成为瓶颈。因此,强烈建议新驱动实现 motor_mit_cmd(float* f_p, float* f_v, float* f_kp, float* f_kd, float* f_t) 重载。
实现该模式的关键在于 bus_registry_。这是一个以 std::string(CAN 接口名)为键、std::vector<YourMotorDriver*> 为值的静态哈希表。同一 can0 接口上的所有 XYN 实例在构造时都会把自身指针加入该向量。当调用批处理 MIT 时,驱动代码会:
- 锁定
bus_registry_mutex_,查找当前接口对应的电机列表; - 遍历列表,根据每个实例的
motor_index_(通常由motor_id_ - 1决定,范围 0–7)计算其在 64 字节 CAN-FD 帧中的字节偏移slot * 8; - 对每个电机,将指针数组中对应索引的控制量进行限幅与
range_map,按协议位域打包成 8 字节; - 将组装好的 64 字节帧通过
canfd_->transmit(tx_frame)一次性发出。
此机制实现了逻辑多对象到物理单帧的映射。需要注意的是,批处理帧通常使用特殊的广播 CAN ID(如 EVO 使用 0x20,LRO 使用 0x8080 | CAN_EFF_FLAG),而非常规的电机独立 ID。
Sources: lro_motor_driver.cpp, evo_motor_driver.cpp
接口类型与通信协议选择
MotorDriver 的设计允许同一电机品牌根据硬件部署场景选择不同的物理层通信方式。CommType 枚举当前定义了 CAN、CANFD 与 ETHERCAT 三种类型。在构造函数中,你应当根据传入的 interface_type 字符串进行分支:
| interface_type | 协议后端类 | 说明 |
|---|---|---|
"can" |
MotorsCAN |
经典 CAN 2.0,单帧 8 字节 |
"canfd" |
MotorsCANFD |
原生 CAN-FD,单帧最大 64 字节 |
"ethercanfd" |
MotorsCANFD |
EtherCAT 桥接的 CAN-FD,API 与原生一致 |
"ethercat" |
(预留) | 未来原生 EtherCAT 接口,目前 LRO 已预留占位 |
若你的新电机仅支持经典 CAN(如早期 DM 系列),则只实例化 MotorsCAN 并操作 can_frame;若支持 CAN-FD,则优先使用 MotorsCANFD,以便利用 64 字节payload实现批处理。基类中的 comm_type_ 字段用于记录当前实例所选的后端类型,在后续命令发送时进行分支判断。
Sources: motor_driver.hpp, dm_motor_driver.hpp
调试与验证清单
完成编码后,按以下顺序进行验证,确保新驱动在单机与整机环境中均稳定工作。
| 阶段 | 检查项 | 通过标准 |
|---|---|---|
| 编译 | colcon build 或 cmake --build 无报错,新静态库 xyn_motors 被正确链接到 motors 与 motors_py |
构建成功 |
| 单电机初始化 | Python SDK 中调用 create_motor(id, "canfd", "can0", "XYN", model),随后 init_motor() |
init_motor() 返回 0,无错误码 |
| 状态回读 | 执行 refresh_motor_status() 后调用 get_motor_pos() / get_motor_spd() / get_motor_current() |
读数与电机实际状态一致 |
| MIT 控制 | 发送小幅度正弦位置指令,观察电机是否平滑跟随 | 运动无抖动、无啸叫 |
| 批处理下发 | 在总线上挂载 2–4 个电机,调用指针重载 motor_mit_cmd |
所有电机同时响应,单次总线抓包仅见一帧 64 字节报文 |
| 零点标定 | 调用 set_motor_zero(),完成后读取位置 |
返回值约为 0,偏差小于 1e-2 |
| 错误恢复 | 手动触发过流/过温保护后调用 clear_motor_error() |
错误码清零,电机可重新使能 |
若批处理模式下发现个别电机不响应,首先检查 motor_index_ 是否与 motor_id_ 的映射关系正确,其次确认 bus_registry_ 中该实例是否成功注册且未被提前析构。
Sources: motor_driver.hpp, lro_motor_driver.cpp
延伸阅读与上下游关联
扩展新电机驱动只是硬件适配的第一步。完成驱动开发后,通常还需要在 URDF 与电机映射关系 中更新关节名称与 ID 的对应表,并在 配置文件系统详解 里添加新电机的型号枚举与限参配置。如果涉及强化学习策略的输入输出维度变化,请同步查阅 接入自定义强化学习策略。