电机控制中的坐标系与机械域如何显式建模
目录
在只有电机和编码器的简单系统中,position、velocity、torque 似乎已经足够表达控制量。但当系统加入减速器、负载侧编码器、同步带或丝杠之后,同一个 position 可能表示:
- 电机轴角度,单位为
rad; - 减速器输出轴角度,单位仍为
rad; - 直线滑台位置,单位为
m。
它们在 C++ 中都可以用 float 表示,数值也可能恰好接近,却不能直接互换。许多难以复现的控制问题,并不是算法本身错误,而是某个模块把“负载侧位置”当成“电机侧位置”,或者把“米”当成“弧度”继续计算。
解决这类问题的关键,不是增加更多注释,而是让机械含义成为数据模型的一部分。
1. 两个容易被混为一谈的维度
一个机械状态至少要回答两个相互独立的问题:
- 它属于旋转系统还是直线系统?
- 它描述电机侧还是负载侧?
可以将这两个维度分别定义为机械域(domain)与机械坐标(coordinate)。
enum class MechanicalDomain : std::uint8_t {
rotary, // position [rad], velocity [rad/s], effort [N*m]
linear, // position [m], velocity [m/s], effort [N]
};
enum class MechanicalCoordinate : std::uint8_t {
motor, // 传动机构之前的电机侧
load, // 传动机构之后的负载侧
};
这会形成四种理论组合:
| 机械域 | 机械坐标 | 典型含义 |
|---|---|---|
rotary | motor | 电机轴角度、转速和转矩 |
rotary | load | 减速器输出轴角度、转速和转矩 |
linear | motor | 直线电机动子的位置、速度和推力 |
linear | load | 丝杠滑台或执行器末端的位置、速度和推力 |
这里最重要的一点是:域与坐标不是同一件事。减速器只改变坐标和比例,不会把旋转运动变成直线运动;丝杠则同时跨越了电机侧/负载侧和旋转/直线两个维度。
flowchart LR
A["电机侧<br/>rotary / motor"] -->|减速器或同步带| B["负载侧<br/>rotary / load"]
A -->|丝杠| C["负载侧<br/>linear / load"]
2. 为什么变量名和注释不够
一种常见写法是给变量增加后缀:
float motor_position_rad;
float load_position_rad;
float load_position_m;
这比统一使用 position 更清楚,但约束仍然只存在于人的阅读过程里。函数参数依旧只是 float,编译器无法阻止以下错误:
position_controller.update(load_position_m, target_position_rad);
代码能够编译,控制器也会输出一个看似正常的数值,错误往往直到实机运行才暴露。对于电机控制系统,这类错误可能表现为:
- 闭环增益相差一个减速比;
- 速度或位置突然跳变;
- 转矩限幅作用在错误的一侧;
- 负载侧编码器被要求提供不存在的电气角度;
- 控制器在旋转和直线单位之间静默运行。
因此,命名规范应该保留,但不能作为唯一防线。更可靠的做法是把单位、机械域和机械坐标一起传递。
3. 将状态建模为带标签的数据
旋转状态与直线状态应使用不同的字段名称,避免仅凭上下文解释单位:
struct RotaryMotionState {
float position_rad{0.0F};
float velocity_rad_per_s{0.0F};
};
struct LinearMotionState {
float position_m{0.0F};
float velocity_m_per_s{0.0F};
};
struct MotionState {
MechanicalDomain domain{MechanicalDomain::rotary};
MechanicalCoordinate coordinate{MechanicalCoordinate::motor};
RotaryMotionState rotary{};
LinearMotionState linear{};
bool valid{false};
float confidence{0.0F};
std::uint32_t flags{0U};
};
在资源充足的上位机软件中,可以使用 std::variant 表达互斥载荷;在要求静态内存、固定布局和可预测执行时间的嵌入式控制中,也可以像上面这样把两种载荷内联存储,再由 domain 指明当前有效成员。
后一种做法会多占用少量内存,但它具有几个实际优势:
- 数据结构可以保持 trivially copyable;
- ISR 与慢速任务之间可以通过固定容量队列传递;
- 不需要堆分配、锁或虚调用;
- 遥测协议能够使用稳定的数据布局。
无论采用哪种容器,规则都应一致:只有 domain 选中的载荷有效,消费者在使用数据前必须检查标签和有效性。
4. 电气状态与机械状态需要分开
FOC 所需的电气角度并不等同于机械位置。电气角度与电机极对数有关,而负载侧编码器通常只能提供机械状态。
struct ElectricalState {
float angle_rad{0.0F};
float velocity_rad_per_s{0.0F};
bool valid{false};
};
struct AxisControlState {
ElectricalState electrical;
MotionState motion;
bool valid{false};
};
将两者分开后,反馈源的职责会更清楚:
- 换向反馈负责电气角度;
- 电机侧编码器可以提供电机机械位置;
- 负载侧编码器可以提供负载机械位置;
- 估算器可以只提供速度;
- 冗余传感器不必伪装成主反馈源。
尤其不要为了复用旧接口,让负载侧编码器“补出”一个电气角度。不存在的数据应该保持无效,并由反馈路由选择真正能够承担该角色的来源。
5. 命令与遥测必须使用相同契约
仅在反馈状态中增加标签还不够。控制命令、配置和遥测也必须携带相同的 domain 与 coordinate:
struct RotaryAxisCommand {
float torque_nm{0.0F};
float target_velocity_rad_per_s{0.0F};
float target_position_rad{0.0F};
};
struct LinearAxisCommand {
float force_n{0.0F};
float target_velocity_m_per_s{0.0F};
float target_position_m{0.0F};
};
struct AxisCommand {
MechanicalDomain domain{MechanicalDomain::rotary};
MechanicalCoordinate coordinate{MechanicalCoordinate::motor};
RotaryAxisCommand rotary{};
LinearAxisCommand linear{};
};
假设某个轴被配置为 rotary / load 闭环,那么它只应接受 rotary / load 命令,也只应消费同一契约下的机械反馈:
bool validate_command(const AxisCommand& command,
MechanicalDomain expected_domain,
MechanicalCoordinate expected_coordinate) {
return command.domain == expected_domain &&
command.coordinate == expected_coordinate;
}
不匹配时应直接拒绝命令,而不是在控制周期中猜测调用者的意图。遥测同样需要带上标签,否则上位机仍可能把电机侧数据当作负载侧数据显示或记录。
6. 传动关系只在明确的边界转换
显式坐标模型并不意味着系统内部到处进行换算。更好的原则是:
控制器始终工作在配置指定的坐标中,只在物理执行量或传感器量跨越传动边界时转换。
6.1 旋转到旋转
定义减速比 (N) 为“电机转数 / 负载转数”,方向系数 (s \in {-1, 1}),正向效率为 (\eta)。电机侧到负载侧的位置与速度为:
忽略惯量与动态损耗,只考虑给定的正向效率时,负载转矩为:
如果控制器输出的是负载侧目标转矩,则电流环最终需要的电机侧转矩为:
例如,减速比为 (10)、效率为 (0.9) 时,请求 (9,\mathrm{N\cdot m}) 的负载转矩,对应理想化模型下约 (1,\mathrm{N\cdot m}) 的电机转矩。这个换算应集中在传动模型中,而不是散落在控制器、协议适配器和上位机里各写一遍。
6.2 旋转到直线
对于丝杠,若导程 (L) 的单位为 m/rev,丝杠之前还存在减速比 (N),则:
对应的负载推力为:
这组公式说明,直线执行器不是简单地把字段名从 torque 改为 force。位置、速度、输出量、控制器增益和限幅的单位都发生了变化。
7. 对暂未实现的机械域要失败关闭
提前建模一种能力,不等于已经实现这种能力。
系统可以先定义 linear 状态和命令,以稳定上层接口;但如果底层只有旋转电机的正弦 FOC 和旋转位置环,就不应把直线命令塞进旋转控制器继续执行。正确行为是:
Status validate_axis_command(const AxisCommand& command) {
if (command.domain == MechanicalDomain::linear) {
return Status::not_supported;
}
// 继续检查坐标、单位对应的数值范围和控制模式
return Status::ok;
}
这就是失败关闭(fail closed):无法证明命令与当前实现兼容时,保持功率输出关闭并返回明确错误。相比“先跑起来再说”,这种策略更适合会驱动物理机构的系统。
8. 在实时控制路径中落地
显式模型最终需要进入轴的完整数据流,而不是停留在公共头文件中。
flowchart TD
A["通信或应用层命令"] --> B{"检查 domain / coordinate"}
B -->|不匹配| X["拒绝命令并报告状态"]
B -->|匹配| C["固定容量命令队列"]
C --> D["PWM / ADC ISR"]
E["电气与机械反馈源"] --> F{"检查反馈标签与有效性"}
F -->|无效| Y["故障或禁止输出"]
F -->|有效| D
D --> G["坐标边界换算"]
G --> H["电流环与功率级"]
D --> I["带标签的遥测队列"]
一个可执行的落地顺序是:
- 在轴配置中固定控制域与控制坐标;
- 初始化时校验传动类型、减速比、方向、效率和丝杠导程;
- 命令入队前检查其标签是否与轴配置一致;
- ISR 获取反馈后再次检查反馈标签与有效性;
- 只在控制坐标与电机物理坐标之间转换输出量;
- 遥测原样携带域、坐标、单位明确的字段和错误状态。
高速路径仍然可以保持无堆分配、无锁和固定执行上界。显式建模增加的是少量标签与比较,却把原本隐藏在文档和调用约定中的假设变成了可验证的运行时契约。
9. 设计时可以自查的问题
在评审一个电机控制接口时,可以逐项回答:
position的单位是否体现在字段名或类型中?- 数据描述的是电机侧还是负载侧?
- 控制目标、反馈和遥测是否使用同一坐标?
- 负载侧编码器是否被错误地当成电气角度来源?
- 减速比、方向和效率是否只有一个权威配置来源?
- 坐标转换是否集中在明确的边界?
- 不支持的域或传动形式是否会明确拒绝?
- 错误命令能否在进入功率输出之前被拦截?
如果其中任何一项只能回答“调用者应该知道”,说明这个假设还没有真正进入系统模型。
总结
电机控制中的“坐标系问题”并不只是公式问题,更是接口契约问题。将模型拆成两个正交维度,可以得到清晰的表达:
MechanicalDomain决定物理量及其单位:旋转或直线;MechanicalCoordinate决定物理量所在的位置:电机侧或负载侧;- 传动模型负责在两个坐标之间进行唯一、可审查的转换;
- 命令、反馈与遥测共同携带标签,并在边界处校验;
- 尚未实现的组合明确返回不支持,而不是静默复用错误单位。
这种设计不会让控制算法变得更复杂。相反,它把复杂度从调试现场提前搬到了类型、配置和校验中,让错误更早出现,也让后续加入减速器、负载编码器和直线执行器时更容易保持系统一致。