人形机器人一个关节 = 电机 + 减速器 + 编码器 + 驱动器。驱动器固件是关节的"神经系统":它负责读编码器、采电流、算 FOC、输出 PWM、走 CAN 总线。无论是直接用 SimpleFOC / ODrive,还是自研驱动,最终都要落到 MCU 固件这一层。先把硬件选型和外设角色搞清楚,后面学库才会"知其然也知其所以然"。
电机控制对 MCU 的核心要求是:高精度定时器(互补 PWM + 死区)、高速 ADC(同步采样)、足够的算力(FPU 最好有)。下面是两大家族在电机控制场景下的直接对比:
| 型号 | 内核 / 主频 | 电机控制关键外设 | 参考价格 | 社区生态 | 适合场景 |
|---|---|---|---|---|---|
| STM32F103C8T6 | Cortex-M3 @72MHz,无硬件浮点 | 基础定时器 / ADC,无高级电机控制外设 | 约 ¥5~10 | 教程海量 | 裸机入门、学习 MCU 基本功 |
| STM32F401/F411 | Cortex-M4F @84/100MHz,带 FPU | 通用定时器 / ADC / DMA | 约 ¥10~20 | 丰富 | 中小型项目、带 FPU 的过渡选择 |
| STM32G431(关节主力) | Cortex-M4F @170MHz + FPU + CORDIC | 高级定时器(互补 PWM/死区)、内部比较器、运放、FDCAN | 约 ¥15~25 | 电机控制资料多 | 关节 FOC 主力,众多商用关节的参考架构 |
| ESP32 | Xtensa 双核 @240MHz | 外设丰富,但无 CAN 控制器(需外接)、ADC 精度一般 | 约 ¥10~20 | 极活跃,Arduino/ESP-IDF | 学习 SimpleFOC、无线调试、原型验证 |
结论:学习 FOC 用 ESP32 + SimpleFOC 上手最快(库支持好、接线简单);做产品原型对标主流关节方案,选 STM32G431 这类带高级定时器 + 内部运放/比较器 + FDCAN 的型号,它几乎是"为 FOC 而生"。
| 外设 | 在电机控制中的角色 | 关键点 |
|---|---|---|
| 高级定时器(TIM1/8) | 生成三相六路互补 PWM 并插入死区;编码器正交解码(ABI);触发 ADC 同步采样 | 死区时间、PWM 频率(常见 10~40kHz)、中心对齐模式 |
| ADC | 采样相电流(低边采样电阻)与母线电压 | 必须由定时器触发、避开开关沿噪声;三相只需采两相 |
| PWM 输出 | 把 SVPWM 计算结果变成三相桥的开关信号 | 六路互补、死区插入、逐波限流(硬件比较器) |
| CAN / FDCAN | 关节间总线通信,把目标位置/速度/力矩下发给各关节 | 周期 ≤1ms;人形机器人多关节级联的事实标准;FDCAN 速率更高 |
FOC(Field Oriented Control,磁场定向控制)一句话概括:把三相交流电机在"与转子同步旋转的坐标系"里看成一个直流电机,然后像控制直流电机一样用两个 PID 控制电流。这样既能精确控制力矩,又能让电流正弦、效率高、噪声小。
其中第 2、3 步合称"电流解算",第 5、6 步合称"电压合成"。整个过程每个 PWM 周期(如 10kHz 时 100μs)执行一次,顺序不能乱。
力矩与电流成正比(τ = Kt · Iq),所以闭环控制力矩必须先"看见"电流。工程上最常见的是低边采样电阻 + 差分运放 → ADC。三相电流满足基尔霍夫定律 Ia + Ib + Ic = 0,因此只需采两相即可算出第三相。采样时刻必须在 PWM 中心对齐处、避开功率管开关瞬间的电流尖峰(详见第 6 节"常见坑")。
// 伪代码:定时器更新事件触发 ADC 中断,一次采样两相电流
void adc_isr(void) {
float ia = adc_read(CH_A); // A 相电流(经运放放大后换算)
float ib = adc_read(CH_B); // B 相电流
float ic = -(ia + ib); // 基尔霍夫: Ia + Ib + Ic = 0
// 交给后续 Clark 变换处理...
}
三相电流在空间上是 120° 分布的,直接处理麻烦。Clark 变换把它们投影到两个正交轴 α、β 上(等幅值形式):
Iα = Ia
Iβ = (Ia + 2·Ib) / √3
// C 实现(等幅值 Clark 变换)
#define SQRT3 1.7320508f
float ialpha = ia; // Iα = Ia
float ibeta = (ia + 2.0f * ib) / SQRT3; // Iβ = (Ia + 2·Ib)/√3
α/β 轴上的电流仍是正弦交流量,PID 对交流量无法做到无静差跟踪。Park 变换把 α/β 投影到与转子同步旋转的 d/q 坐标系上,得到两个近似直流的分量:Id(励磁分量)、Iq(力矩分量)。这里的 θ 是转子电角度——这正是编码器的用武之地。
Id = Iα·cos(θ) + Iβ·sin(θ)
Iq = −Iα·sin(θ) + Iβ·cos(θ)
// C 实现(Park 变换)
float ct = cosf(theta); // theta = 转子电角度
float st = sinf(theta);
float id = ialpha * ct + ibeta * st; // Id = Iα·cosθ + Iβ·sinθ
float iq = -ialpha * st + ibeta * ct; // Iq = −Iα·sinθ + Iβ·cosθ
得到直流量后,两个独立的 PID 分别控制 d 轴与 q 轴电流:q 轴目标来自速度/位置外环(要多少力矩),d 轴目标通常恒为 0(永磁电机最大转矩电流比,即 MTPA 的简化形式)。
// 以 q 轴电流环为例(每个控制周期执行一次)
float err = iq_ref - iq; // iq_ref 来自外环
integral += err * dt;
if (integral > limit) integral = limit; // 抗积分饱和
if (integral < -limit) integral = -limit;
float vq = kp * err + ki * integral + kd * (err - err_prev) / dt;
err_prev = err;
// vd 同理,通常 id_ref = 0
电流环响应要快(带宽 1~2kHz),所以它必须跑在最快的中断优先级上,直接由 PWM 周期驱动。
PID 输出的 vd、vq 还是旋转坐标系下的量,要变成实际的三相电压,先逆 Park 回 α/β 静止坐标系:
Vα = Vd·cos(θ) − Vq·sin(θ)
Vβ = Vd·sin(θ) + Vq·cos(θ)
// C 实现(逆 Park 变换)
float valpha = vd * ct - vq * st; // Vα = Vd·cosθ − Vq·sinθ
float vbeta = vd * st + vq * ct; // Vβ = Vd·sinθ + Vq·cosθ
SVPWM(空间矢量脉宽调制)把 (Vα, Vβ) 这个电压矢量用三相桥的 8 个基本开关状态(6 个非零矢量 + 2 个零矢量)来合成。每个 PWM 周期内,先判断矢量落在哪个扇区,再计算相邻两个基本矢量的作用时间 T1、T2,剩余时间 T0 用零矢量填充,最后按"七段式"中心对齐排布开关序列:
扇区 s 内,γ 为电压矢量相对扇区起始边的角度:
T1 = (√3·Ts / Vdc) · |V| · sin(60° − γ)
T2 = (√3·Ts / Vdc) · |V| · sin(γ)
T0 = Ts − T1 − T2
// 伪代码:SVPWM 主流程(每 PWM 周期一次)
int sector = sector_of(valpha, vbeta); // 由 Vα/Vβ 符号与比值判扇区 1..6
float t1 = calc_t1(sector, valpha, vbeta, Ts, Vdc);
float t2 = calc_t2(sector, valpha, vbeta, Ts, Vdc);
float t0 = Ts - t1 - t2; // 零矢量填充
update_compare_regs(sector, t1, t2, t0); // 更新比较寄存器,硬件输出六路互补 PWM
回到第 2.4 节:Park 变换和逆 Park 变换都需要转子电角度 θ,它是整个旋转坐标系的"基准"。没有 θ,FOC 就无从谈起:
编码器常见两种:增量式(ABI 正交输出或霍尔,便宜但上电不知道绝对位置,需找零)和绝对值(磁编码器如 AS5600/MT6701,上电即知位置,关节类产品几乎都用绝对值编码器)。
SimpleFOC 把上面整条流水线封装成非常清晰的两步,理解 FOC 原理后读它的源码毫无障碍:
// SimpleFOC 的主循环结构(简化示意)
void loop() {
motor.loopFOC(); // ① 读编码器 → 电流采样 → Clark → Park → 电流 PID → 逆 Park → SVPWM
motor.move(target); // ② 根据控制模式(力矩/速度/位置)计算电流目标,交给 loopFOC 执行
}
| FOC 数学步骤 | SimpleFOC 内部对应 | 你改的是什么 |
|---|---|---|
| 电流采样 | BLDCDriver3PWM / 电流检测类(取决于是否带电流采样) | 驱动板与引脚 |
| Clark / Park / 逆 Park | BLDCMotor 内部电流解算与电压合成 | 一般不用改 |
| 电流 PID | PIDController(电流环) | KP/KI 与输出限幅 |
| 编码器 → 电角度 | Encoder / MagneticSensorI2C 等传感器类 | 编码器类型与极对数 |
| SVPWM | 驱动层的 setPwm / writeDutyCycle | PWM 频率、死区(在 MCU 层) |
SimpleFOC 是目前最流行的开源 FOC 库:在 Arduino 生态上把"电机 + 驱动板 + 编码器"抽象成 BLDCMotor、BLDCDriver3PWM、Encoder 等类,内置力矩/速度/位置三种控制模式,以及开环模式。官方文档(docs.simplefoc.com)有完整的中英文教程,社区活跃度很高,是个人开发者进入 FOC 世界的第一站。
SimpleFOC 支持 Arduino 全系(UNO/Nano/Mega 等)、STM32(BluePill、Nucleo 系列,B-G431B-ESC1 这类电机专用板体验最佳)、ESP32(最推荐入门)、Teensy、Raspberry Pi Pico 等;编码器支持增量 ABI、霍尔、以及 AS5600 等 I2C 磁编码器。详细的板子清单以官方文档为准。
下面是一份完整的 SimpleFOC 示例(基于 ESP32):先跑开环验证电机能转,再接编码器切换电压闭环。注释里标了每一步的作用。
// SimpleFOC 最小可用示例:BLDC 无刷电机 · 开环 → 闭环
// 硬件:ESP32 + 三相无刷驱动板(如 L6234 模块/大疆 FOC 板)+ 无刷电机
// 接线:驱动板 VM/GND 接 12~24V 电源;三相输出接电机 A/B/C;
// 编码器(可选)接 GPIO36/39;电机每转一圈的极对数按实际填
#include <SimpleFOC.h>
// 电机对象:极对数(pole pairs),例如 7 —— 必须与电机实际一致
BLDCMotor motor = BLDCMotor(7);
// 三相 PWM 驱动器:引脚依次为 A、B、C(ESP32 示例,按你的接线改)
BLDCDriver3PWM driver = BLDCDriver3PWM(32, 33, 25);
// 增量编码器:ABI 两路引脚 + 每圈计数(CPR),没有编码器可先不启用
Encoder encoder = Encoder(36, 39, 2048);
// 编码器中断回调(ABI 编码器必须)
void doA(){ encoder.handleA(); }
void doB(){ encoder.handleB(); }
void setup(){
// 1) 驱动器:设置供电电压并初始化
driver.voltage_power_supply = 12.0f; // 与你的电源电压一致!
driver.init();
motor.linkDriver(&driver);
// 2) 编码器:初始化并接上电机(注释掉可先跑开环)
encoder.init();
encoder.enableInterrupts(doA, doB);
motor.linkSensor(&encoder);
// 3) 控制模式:先开环验证,再接编码器后改成 voltage / velocity / angle
motor.controller = MotionControlType::openloop; // 开环:不需要编码器
// motor.controller = MotionControlType::voltage; // 闭环:注释掉上一行,启用这行
// 4) 安全限幅:限制最大电压,防止误操作烧电机
motor.voltage_limit = 3.0f; // 先给 3V,能转再加
motor.init();
motor.initFOC(); // 编码器校准 + FOC 初始化(开环模式会自动跳过)
Serial.begin(115200);
delay(1000);
}
void loop(){
motor.loopFOC(); // 闭环时必须调用(开环模式内部会跳过)
motor.move(2.0f); // 开环:2V 电压矢量;闭环:2V 目标电压(≈力矩)
}
运行顺序建议:先保持 openloop 模式上电,电机应以 2V 小电压平稳旋转;再接入编码器、改成 voltage 模式重新上电,`initFOC()` 会自动执行编码器校准,成功后电机会"咔"地锁定,此时 `move()` 给的就是带反馈的力矩指令。
ODrive 是机器人圈最著名的开源高功率 FOC 驱动板:硬件、固件、上位机工具全部开源(GitHub: odriverobotics/ODrive)。经典板卡 v3.6 基于 STM32F405 + DRV8301 栅极驱动,支持 24~48V 母线电压、双轴驱动,单轴峰值电流可达数十安培量级(持续电流取决于散热,以官方手册为准)。它把"FOC 固件 + 三环控制 + 总线接口"做成即插即用的整板,是轮式机器人、直驱关节样机的标配。
# 终端 1:进入交互式工具(需要先安装: pip install odrive)
odrivetool
# 在 odrivetool 交互环境中(伪代码,以实际命令为准):
odrv0.axis0.motor.config.pole_pairs = 7 # 极对数
odrv0.axis0.motor.config.current_lim = 20 # 电流限制(A)
odrv0.axis0.encoder.config.cpr = 4096 # 编码器每圈计数
odrv0.axis0.config.startup_closed_loop = False # 上电不自动闭环(调试期)
odrv0.axis0.requested_state = AXIS_STATE_FULL_CALIBRATION_SEQUENCE # 执行校准
odrv0.save_configuration() # 保存配置
校准完成后,日常控制就是几行 Python。下面的脚本让 axis0 进入闭环并依次做速度、位置、力矩控制:
import odrive
from odrive.enums import *
# 找到并连接 ODrive(USB)
odrv0 = odrive.find_any(timeout=10)
axis = odrv0.axis0
# 进入闭环控制
axis.requested_state = AXIS_STATE_CLOSED_LOOP_CONTROL
axis.input_vel = 10.0 # 速度模式:目标 10 rad/s
axis.input_pos = 3.14 # 位置模式:目标 π rad
axis.input_torque = 0.5 # 力矩模式:目标 0.5 N·m
# 读取当前状态
print("pos:", axis.pos_estimate, "vel:", axis.vel_estimate)
# 保存配置(参数改动后调用)
odrv0.save_configuration()
input_* 同时存在,生效的是当前控制模式对应的那个。切换模式前先让电机停下,直接改 requested_state 前最好把目标清零。motoros 的仓库,但那是 Motoman 工业机械臂的 ROS 驱动,与小米无关。学习时请以小米官方开发者渠道和官方 GitHub 组织为准,警惕 AI 生成内容中的错误引用——本页不传播未经核实的"MotorOS"描述。小米在电机与机器人方向确实有实打实的开源动作,最典型的是 CyberGear 微伺服电机(2023 年发布):这是一颗开源准直驱关节模组,把电机、减速器、编码器、驱动器集成一体,首发价约 499 元,通过 CAN 总线(1Mbps)通信,社区资料齐全,极大拉低了人形/四足关节的入门门槛。围绕它形成了丰富的国产开源生态:
MIT Mini Cheetah 是麻省理工的开源四足机器人:电机放在关节处的准直驱设计 + 高带宽力控,让它能跑、能跳、能后空翻。其开源控制栈包含状态估计、运动规划与关节力矩分配(虚拟模型控制 / 凸优化),社区实现如 CHAMP(MIT Cheetah I 的完整开源实现)把整套控制拆成可读的模块。学习它的意义在于:单关节 FOC 只是"肌肉",Mini Cheetah 展示的是"神经"——如何把整机目标(跑/跳)实时分配成每个关节的力矩指令。
这些坑基本都可以在硬件设计 + 固件防护两层同时设防:硬件上保证采样电路和死区电路正确,固件上把限流、欠压、过温保护写进主循环,任何一层兜住都不至于炸板。