本文档深入讲解 EvoMotorDriver 的实现原理与使用要点,涵盖其双总线兼容架构、MIT 阻抗控制数据打包协议、寄存器配置体系,以及 CAN-FD 场景下的批量一帧多机控制机制。阅读本文前,建议先掌握 MotorDriver 抽象基类与接口设计 中的通用状态机与回调模型,以便理解本文中 lock_motor、unlock_motor、init_motor 等生命周期方法的设计上下文。
Sources: evo_motor_driver.hpp, motor_driver.hpp
架构定位与双总线兼容
EvoMotorDriver 继承自 MotorDriver 抽象基类,是 SDK 中唯二同时支持 CAN 2.0 与 CAN-FD 的驱动实现(另一为 DM 系列)。构造函数通过 interface_type 参数("can" / "canfd" / "ethercanfd")在运行时决定通信后端:CAN 2.0 场景绑定 MotorsCAN 单例,CAN-FD 场景则绑定 MotorsCANFD 单例,并将实例指针注册到以 can_interface_ 为键的全局静态 bus_registry_ 中。该注册表在析构时自动清理,保证多电机共享同一总线时的生命周期安全。
classDiagram
class MotorDriver {
<<abstract>>
+lock_motor()
+unlock_motor()
+init_motor()
+motor_mit_cmd(f_p, f_v, f_kp, f_kd, f_t)
+motor_pos_cmd(pos, spd, ignore_limit)
+motor_spd_cmd(spd)
+set_motor_control_mode(mode)
}
class EvoMotorDriver {
-EVO_Motor_Model motor_model_
-EVO_Limit_Param limit_param_
-uint8_t motor_index_
-can_rx_cbk(rx_frame)
-canfd_rx_cbk(rx_frame)
-write_register_evo(rid, value)
+motor_mit_cmd(float*)
}
MotorsCAN --> EvoMotorDriver : CAN 2.0
MotorsCANFD --> EvoMotorDriver : CAN-FD
MotorDriver <|-- EvoMotorDriver
在 CAN-FD 模式下,电机 ID 必须落在 1~8 范围内,驱动内部将其映射为 motor_index_ = motor_id - 1,该索引直接决定批量 MIT 控制帧中的 8 字节槽位位置。若 ID 超出此范围,则强制归零,可能导致批量命令不匹配。
Sources: evo_motor_driver.cpp, evo_motor_driver.cpp
电机型号与运行限幅
EVO 系列目前内置三款电机的硬编码限幅参数,以全局数组 evo_limit_param 维护。驱动在构造时根据 motor_model_ 索引拷贝对应结构到成员 limit_param_,后续所有控制量饱和、反馈量反归一化均以此为准。
| 型号枚举 | 位置限幅 PosMax (rad) | 速度限幅 SpdMax (rad/s) | 力矩限幅 TauMax (N·m) | 外环 Kp 上限 OKpMax | 外环 Kd 上限 OKdMax |
|---|---|---|---|---|---|
EVO431040 |
12.5 | 20.0 | 18.0 | 500.0 | 5.0 |
EVO811825 |
12.5 | 10.0 | 50.0 | 250.0 | 50.0 |
EVO811832 |
12.5 | 10.0 | 50.0 | 250.0 | 50.0 |
这些参数不仅用于软件层面的 limit() 截断,更关键的是作为 range_map() 的物理量程边界——MIT 控制指令需要将浮点物理量映射到固定位宽的整型域,反馈解析时则反向映射。若实际电机型号与构造时传入的枚举不一致,会导致反馈值比例错误,表现为位置或力矩读数偏差。
Sources: evo_motor_driver.cpp, evo_motor_driver.hpp
数据打包协议:MIT 8 字节帧
EVO 的 MIT(阻抗控制)协议将 5 个浮点控制量 压缩进 8 字节 payload,这是整个驱动中位运算最密集的部分。打包规则如下:
- 位置 p:16 bit 无符号整型,量程
[-PosMax, PosMax]映射到[0, 0xFFFF] - 速度 v:12 bit 无符号整型,量程
[-SpdMax, SpdMax]映射到[0, 0x0FFF] - 刚度 kp:12 bit 无符号整型,量程
[0, OKpMax]映射到[0, 0x0FFF] - 阻尼 kd:12 bit 无符号整型,量程
[0, OKdMax]映射到[0, 0x0FFF] - 力矩 t:12 bit 无符号整型,量程
[-TauMax, TauMax]映射到[0, 0x0FFF]
字节布局采用紧凑拼接,无字节对齐填充:
data[0] = p[15:8] // 位置高 8 位
data[1] = p[7:0] // 位置低 8 位
data[2] = v[11:4] // 速度高 8 位
data[3] = v[3:0] | kp[11:8] // 速度低 4 位 + Kp 高 4 位
data[4] = kp[7:0] // Kp 低 8 位
data[5] = kd[11:4] // Kd 高 8 位
data[6] = kd[3:0] | t[11:8] // Kd 低 4 位 + 力矩高 4 位
data[7] = t[7:0] // 力矩低 8 位
代码实现中,motor_mit_cmd(float, float, float, float, float) 先通过 limit() 将物理量钳位到安全区间,再经 range_map() 完成量程转换,最后按上述位域写入 data[]。注意位置命令会自动扣除构造时传入的 motor_zero_offset_,实现软件零偏补偿。
Sources: evo_motor_driver.cpp
状态反馈解析
接收回调同样存在 CAN 与 CAN-FD 两条路径。can_rx_cbk 针对标准 CAN 2.0,canfd_rx_cbk 针对 CAN-FD;后者若检测到帧未携带 CANFD_FDF 标志,会降级转调到前者的解析逻辑,保证协议兼容性。
反馈帧结构:
| 字段 | CAN 2.0 数据源 | CAN-FD 数据源 | 位宽 | 物理映射 |
|---|---|---|---|---|
| 位置 | data[1..2] |
data[0..1] |
16 bit | range_map 到 [-PosMax, PosMax] |
| 速度 | data[3..4] 高12bit |
data[2..3] 高12bit |
12 bit | range_map 到 [-SpdMax, SpdMax] |
| 力矩/电流 | data[4] 低4bit + data[5] |
data[3] 低4bit + data[4] |
12 bit | range_map 到 [-TauMax, TauMax] |
| 错误码 | data[6] |
(data[6..7] >> 1) & 0x7F |
8/7 bit | 直接映射 EVOError |
| 温度 | data[7] |
data[5] - 40.0f |
8 bit | CAN: 原始值;CAN-FD: 摄氏度偏移 |
其中 CAN-FD 的温度采用 data[5] - 40.0f 的偏移编码,与 CAN 2.0 的原始值表示不同,这是双总线切换时最容易被忽略的差异点。错误码 error_id_ 在解析后若大于 0,驱动会通过 spdlog 自动输出 ERROR 级日志,包含总线名与电机 ID,便于现场快速定位故障节点。
Sources: evo_motor_driver.cpp
错误码体系
EVO 驱动定义了独立的错误枚举 EVOError,在 init_motor() 返回时用于向调用方传递上电自检结果。
| 错误码 | 含义 | 触发场景 |
|---|---|---|
0x00 |
无错误 | 正常运行 |
0x01 |
过压 (OVER_VOLTAGE) |
母线电压超过保护阈值 |
0x02 |
欠压 (UNDER_VOLTAGE) |
母线电压低于保护阈值 |
0x03 |
过流 (OVER_CURRENT) |
相电流超过锁定值 |
0x09 |
MOS 过温 (MOS_OVER_TEMP) |
驱动板功率管温度超限 |
0x0A |
线圈过温 (COIL_OVER_TEMP) |
电机绕组温度超限 |
0x0B |
编码器错误 (ENCODER_ERROR) |
位置传感器通信或校验失败 |
0x0F |
过载 (OVERLOAD) |
机械负载持续超限 |
0x10 |
通信丢失 (COMM_LOST) |
上位机超时未收到有效帧 |
0xFF |
未知错误 (UNKNOWN_ERROR) |
未定义故障码 |
init_motor() 的标准流程为:先 unlock_motor() 进入就绪态,再设置 MIT 模式,随后 lock_motor() 使能,最后 refresh_motor_status() 拉取实时状态并返回错误码。任何非零返回值都意味着当前电机不宜直接投入闭环控制。
Sources: evo_motor_driver.hpp, evo_motor_driver.cpp
寄存器配置与 Flash 操作
EVO 系列采用寄存器地址模型,通过 0x600 + motor_id_ 的扩展标识符进行参数读写。核心寄存器枚举 EVO_REG 覆盖了电流环 PI、死区、运动限幅、CAN 映射量程以及安全阈值等配置项。
寄存器写入采用统一的 8 字节协议模板:
data[0] = EVO_CMD_START_FLASH (0x67)
data[1] = rid // 寄存器地址
data[2..5] = value (float/int32 LE)
data[6] = EVO_CMD_WRITE_FLASH (0x15)
data[7] = EVO_CMD_END_FLASH (0x76)
驱动为此提供两个重载的私有方法 write_register_evo(uint8_t rid, float value) 与 write_register_evo(uint8_t index, int32_t value),分别对应浮点型和整型寄存器。save_register_evo() 则将当前内存配置固化到 Flash,模板中 data[6] = EVO_CMD_SAVE_FLASH (0x00)。不过当前版本中 write_motor_flash() 公共接口直接返回 true,实际持久化操作建议通过 set_motor_zero() 或显式寄存器调用完成。
Sources: evo_motor_driver.hpp, evo_motor_driver.cpp
批量 MIT 控制:CAN-FD 一帧多机
EVO 驱动在 CAN-FD 场景下支持高效的一帧多机批量控制,这是其区别于 CAN 2.0 实现的核心性能特性。motor_mit_cmd(float* f_p, float* f_v, float* f_kp, float* f_kd, float* f_t) 重载会构造一个 64 字节 payload 的 CAN-FD 帧,CAN ID 固定为 EVOFD_MIT_ID (0x20),每个电机占用 8 字节槽位,槽位索引等于 motor_index_。
sequenceDiagram
participant Host as 上位机
participant Bus as CAN-FD 总线
participant M1 as EVO #1 (slot 0)
participant M2 as EVO #2 (slot 1)
participant M8 as EVO #8 (slot 7)
Host->>Bus: 发送 64B 帧 (ID=0x20)
Note over Bus: slot0: M1 MIT 数据<br/>slot1: M2 MIT 数据<br/>...<br/>slot7: M8 MIT 数据
Bus->>M1: 提取 data[0..7]
Bus->>M2: 提取 data[8..15]
Bus->>M8: 提取 data[56..63]
实现上,方法先以全 0x7F/0xFF 填充全部 8 个槽位作为默认值,随后加锁遍历 bus_registry_[can_interface_],对注册到该总线的每个 EvoMotorDriver 实例,依据其 motor_index_ 和各自的 limit_param_ 计算并覆写对应槽位。这意味着同一总线上不同型号的 EVO 电机可以共存于同一批量帧,各自的物理限幅独立生效。若某槽位无对应电机实例,则保留默认值,电机端应忽略或保持上一帧状态。
该接口仅在 comm_type_ == CANFD 时启用;若为 CAN 2.0,会降级到单电机 motor_mit_cmd(float, ...),确保 API 语义一致。
Sources: evo_motor_driver.cpp
位置与速度模式
除 MIT 阻抗控制外,EVO 驱动同样支持独立的位置模式(POS)与速度模式(SPD)。两种模式均通过向电机基础 ID 发送 IEEE-754 float 原始字节实现。
- 位置模式:
motor_pos_cmd(float pos, float spd, bool ignore_limit)发送 8 字节,前 4 字节为pos的小端 float,后 4 字节为spd的小端 float。若当前不处于POS模式,则先调用set_motor_control_mode(POS)并立即返回,等下一控制周期再发实际位置指令。 - 速度模式:
motor_spd_cmd(float spd)仅发送前 4 字节速度值,同样存在模式切换的懒加载逻辑。
set_motor_control_mode 在 CAN 2.0 下会向寄存器 11 写入 0x02,随后更新成员变量 motor_control_mode_;CAN-FD 路径目前也执行相同操作。
Sources: evo_motor_driver.cpp, evo_motor_driver.cpp
零位标定与 ID 管理
set_motor_zero() 是 EVO 系列 commissioning 阶段最常用的操作之一。其实现流程为:发送 EVO_CMD_SET_ZERO (0xFE),等待 500 ms,刷新状态并读取当前位置,随后 unlock_motor()。若读取到的位置绝对值大于 judgment_accuracy_threshold (1e-2),则判定标定失败并返回 false,否则返回 true。该阈值来自基类静态常量,适用于全系列电机。
电机 ID 修改通过向寄存器 EVO_REG_MOTOR_ID (36) 写入新 ID 完成。set_motor_id(old_id, new_id) 与 reset_motor_id() 均调用 write_register_evo 实现,但修改后不会自动固化到 Flash,如需掉电保存,需额外调用保存流程。reset_motor_id() 将 ID 恢复为出厂默认值 1。
Sources: evo_motor_driver.cpp, evo_motor_driver.cpp
工厂创建与 Python 暴露
用户代码通常不直接调用 EvoMotorDriver 构造函数,而是通过基类工厂方法统一实例化:
auto motor = MotorDriver::create_motor(
motor_id, // uint16_t
"canfd", // interface_type
"can0", // can_interface
"EVO", // motor_type
EVO_Motor_Model::EVO431040, // motor_model
0, // master_id_offset (EVO 未使用)
0.0 // motor_zero_offset
);
Python 层通过 pybind11 暴露相同语义,create_motor 静态方法在 MotorDriver 基类上绑定,调用端无需感知具体子类。EVO 的枚举模型在 Python 中通过整型传递即可。
Sources: motor_driver.cpp, pybind_module.cpp
与其他驱动的关键差异总结
| 维度 | EVO | DM | LeadRobot |
|---|---|---|---|
| 总线支持 | CAN 2.0 + CAN-FD | CAN-FD 为主 | CAN 2.0 |
| 批量 MIT | CAN-FD 64B 一帧 8 机 | 单帧单电机 | 不支持 |
| 温度编码 | CAN-FD 带 -40°C 偏移 | 模型相关 | 原始值 |
| 零位命令 | 0xFE 独立命令 |
Flash 写寄存器 | 独立协议 |
| ID 范围约束 | CAN-FD 要求 1~8 | 较宽 | 较宽 |
| 错误码体系 | 独立 EVOError 枚举 |
独立 DMError |
独立 LROError |
Sources: evo_motor_driver.hpp
下一步阅读建议
若你已理解 EVO 驱动的通信协议与生命周期管理,建议继续深入以下关联主题:
- 若需理解 MIT 控制背后的阻抗/导纳控制数学原理,请阅读 MIT 阻抗控制原理与实现
- 若需对比 EVO 与另一主流品牌的协议差异,请阅读 达妙 DM 电机驱动详解
- 若需在机器人系统中同时调度多个 EVO 电机并优化总线负载,请回顾 SocketCAN 单例与无锁发送队列 中的多电机注册与回调分发机制
- 若计划通过 Python 进行快速原型开发,请参考 Python SDK 快速上手 中的调用示例