FOC 入门最大的障碍不是公式难,而是一上来就被电流环、观测器、补偿网络一起淹没。第十二期我们把问题拆小:先做电机开环控制,让转子转起来。开环控制不依赖编码器、不需要精确的转子初始位置,代码量比完整 FOC 闭环少一个量级,却能逼着你把 PWM 输出、三相全桥驱动、电压矢量、母线电压采集、电流采样通道全部打通。这一期跑通之后,后面再做电流环和速度环,只是在这条已经工作的电路上不断增加闭环逻辑而已。
本文会沿着“原理 -> 硬件准备 -> 代码实现 -> 实测验证 -> 常见问题”这条线展开,重点回答几个很实际的疑问:开环控制为什么能让电机转起来?三相占空比怎么算?电流采样为什么要放在下桥?Clarke 变换为什么只需要两相电流?开环和闭环之间怎么衔接?如果你是刚开始学 FOC、手上有 BLDC 或 PMSM 开发板但还没转过电机,这一期可以直接照着做。
1. 电机开环控制核心能力速览
| 能力项 | 说明 |
|---|---|
| 项目类型 | 电机控制算法教程,FOC 系列第十二期 |
| 核心主题 | 在 FOC 框架下实现电机开环控制 |
| 主要功能 | V/F 速度开环、电角度生成、三相 PWM 输出、正反转控制、软启动 |
| 建议硬件 | 带定时器 PWM 的 MCU、三相全桥驱动、BLDC/PMSM、直流稳压电源 |
| 编码器依赖 | 不依赖,开环可以“盲转” |
| 软件基础 | C 语言、定时器中断、ADC 采样、串口打印 |
| 是否支持接口调用 | 嵌入式项目一般没有 REST API,但可以设计串口调试协议 |
| 是否支持批量任务 | 不是互联网服务,但可以用上位机脚本批量标定电压-转速曲线 |
| 适合场景 | FOC 入门、驱动电路验证、PWM 相序检查、电流采样调试、无感算法前期准备 |
| 不适合场景 | 高精度转速控制、负载突变场合、需要大力矩锁止的场合 |
2. 适用场景与使用边界
开环控制最适合做两件事:第一,验证硬件链路是否正常;第二,给闭环控制铺路。
很多板子焊完之后,第一优先不是纸上谈兵聊 FOC 算法,而是确认单片机能不能输出六路 PWM、驱动芯片能不能正确换相、电机相线有没有接反、母线电压采样是否准确。这些问题用开环控制排查,比一上来就调电流环快得多。只要给一个固定电压矢量,电机会跟着定子磁场方向旋转,转得对不对、电流有没有异常,一眼就能看出来。
开环控制不适合做需要精确保持转速的任务。因为它本质上是“开环给定电压,不测量实际转速,也不修正误差”。负载增大时,电机转速会下降,但你程序里给出的电角度频率不会自动补偿。这个特性决定了它只适合空载或轻载实验,不适合做产品级伺服控制。
还需要强调安全边界:电机转动时,轴端、联轴器、齿轮都可能高速旋转,实验台要固定好,手边要放急停开关。电源要有限流功能,避免驱动板异常时母线短路。三相全桥驱动在调试阶段可能因为程序 bug 导致上下桥直通,烧管子是常见事故,所以第一轮测试建议用小功率电机、低母线电压和限流电源。
涉及商业开源代码时也要注意协议边界,比如 VESC 源码可以作为无感 FOC 学习的参考,但商用前要检查授权要求,不能直接拷贝了事。
3. 环境准备与前置条件
3.1 硬件清单
| 硬件 | 作用 | 备注 |
|---|---|---|
| MCU 开发板 | 运行控制算法 | STM32、GD32、TC264 等带高级定时器更好 |
| 三相全桥驱动板 | 将 PWM 信号转成功率驱动 | 检查是否自带母线电容、采样电阻、死区电路 |
| BLDC 或 PMSM 电机 | 被控对象 | 优先选择功率小、额定电压低的电机 |
| 直流稳压电源 | 提供母线电压 | 必须有电流限制功能 |
| 编码器或霍尔传感器 | 可选 | 开环控制阶段可以不用 |
| 逻辑分析仪或示波器 | 看 PWM 波形和相电流 | 排查问题时很关键 |
| USB 转串口模块 | 打印调试数据和下发指令 | 建议至少保留一路串口 |
3.2 软件环境
- 开发环境:Keil MDK、IAR 或 VSCode + GCC 都可以,选你熟悉的。
- 库:如果使用 STM32,可以用 HAL 库或直接寄存器操作。FOC 对实时性有要求,建议把核心控制在定时器中断里执行。
- 代码版本管理:建议开一个 git 仓库,每次改完 PWM 逻辑先提交一次。开环实验经常改参数,有历史版本方便回退。
- 数据观察:串口助手或 Python + pyserial 都可以,用于打印目标转速、电角度、母线电压、电流采样值。
3.3 接线检查清单
- 先不接电机,测量母线电压是否正常。
- 确认驱动板逻辑电源和功率电源分开供电。
- 确认单片机 PWM 输出的地线和驱动板逻辑地共地。
- 确认电机三相线 U/V/W 和驱动板三相输出一一对应。
- 确认 ADC 采样通道没有被大电流干扰源紧贴走线。
- 确认死区时间配置合理,尤其是 6 路互补 PWM 输出时。
4. 开环控制原理
4.1 为什么先写开环控制
FOC 闭环的最终目标是把定子电流矢量控制到与转子磁场成固定的关系,这样才能产生恒定力矩。但一开始转子位置不可知,观测器又不成熟,强行闭环只会出现启动失败或者电流失控。
开环控制的做法更直白:不需要知道转子在哪个角度,我直接给一个旋转的定子磁场,让转子被这个磁场拖起来。这就好比你在冰面上拉一块重物,不关心它朝哪个方向,只要拉力的方向不断转圈,重物就会跟着转。开环 FOC 正是用这种方式完成“启动”这个过程。
4.2 电角度、机械角度和极对数
电机每转一圈,电角度并不一定转一圈,中间相差一个极对数p。公式是:
theta_e = theta_m * p
其中theta_m是机械角度,theta_e是电角度,p是极对数。常见的多极电机,极对数可能是 4、7、14,电角度转速会比机械转速高很多。
给定目标转速后,电角速度的计算方法是:
omega_e = omega_m * p
omega_m = 2 * pi * rpm / 60
也就是说,程序里要先把用户设定的rpm换算成机械角速度,再乘以极对数得到电角速度。这一步做错了,电机转速会和你预期差很多。
4.3 三相电压矢量的生成
开环 FOC 中最简单的实现是正弦电压驱动。假设我们维持一个幅值为U的电压矢量,让电角度theta_e按电角速度匀速累加,三相电压可以写成:
ua = U * sin(theta_e) ub = U * sin(theta_e - 2*pi/3) uc = U * sin(theta_e + 2*pi/3)三相电压相差 120 度,合成出来的定子磁场就是旋转的。剩下的事情就是把这三个电压值换算成 PWM 占空比,输出到三相全桥驱动。
严格来说,FOC 中更常用的是 SVPWM,它对母线电压的利用率更高,谐波更小。但开环阶段先用正弦 PWM 映射占空比,代码简单,容易验证“电机转不转”这个核心问题。后面换 SVPWM 时,输入仍然是电压矢量,只是占空比计算方式不一样。
4.4 开环控制的电压选择
开环启动时,电压幅值不能给得太小,否则定子磁场拖不动转子;也不能给得太大,否则电流会飙高,甚至烧毁功率管。
通常做法是让电压幅值随目标频率平滑上升,也就是 V/F 控制。低速启动时电压小,转速上升后逐步加大电压,让电机在额定磁链附近运行。只给一个固定电压,电机也能转,但低速力矩不够,高速又可能过流。
5. 代码实现:开环 FOC 控制
下面以常见的 C 语言工程为例,核心只做三件事:
- 用定时器中断产生固定控制周期。
- 在中断里累加电角度。
- 根据电角度算三相占空比并更新 PWM 比较寄存器。
5.1 开环角度生成
// 定义开环 FOC 结构体 typedef struct { float pole_pairs; // 极对数 float speed_rpm; // 目标转速 float angle_e; // 当前电角度,单位 rad float angle_speed_e; // 电角速度,单位 rad/s float voltage_q; // 电压幅值,单位 V } OpenLoopFOC;初始化函数:
void openloop_init(OpenLoopFOC *foc, float pole_pairs, float speed_rpm, float voltage_q) { foc->pole_pairs = pole_pairs; foc->speed_rpm = speed_rpm; foc->voltage_q = voltage_q; // 机械角速度 float speed_mech = 2.0f * 3.1415926f * speed_rpm / 60.0f; // 电角速度 foc->angle_speed_e = speed_mech * pole_pairs; foc->angle_e = 0.0f; }控制周期中断函数:
void openloop_tick(OpenLoopFOC *foc, float dt) { // 电角度累加 foc->angle_e += foc->angle_speed_e * dt; // 限制在 0 ~ 2*pi 范围内 if (foc->angle_e > 2.0f * 3.1415926f) { foc->angle_e -= 2.0f * 3.1415926f; } // 三相正弦电压 float ua = foc->voltage_q * sinf(foc->angle_e); float ub = foc->voltage_q * sinf(foc->angle_e - 2.0944f); float uc = foc->voltage_q * sinf(foc->angle_e + 2.0944f); // 写入 PWM pwm_set_duty_abc(ua, ub, uc); }注意:这里的dt必须和定时器中断周期一致。如果控制周期是100us,那么dt = 0.0001f。如果中断周期不准确,电机转速会有明显误差。
5.2 三相电压转 PWM 占空比
ua、ub、uc是电压值,不能直接写到定时器 CCR 寄存器。需要根据母线电压做一次归一化。这里给出一个简化版本,实际工程会使用 SVPWM 来获得更高的母线利用率。
void pwm_set_duty_abc(float ua, float ub, float uc) { float vbus = get_bus_voltage(); // 实际母线电压 float offset; // 中点偏移计算,保证三相输出电压平衡 offset = (ua + ub + uc) / 3.0f; float da = (ua - offset) / vbus + 0.5f; float db = (ub - offset) / vbus + 0.5f; float dc = (uc - offset) / vbus + 0.5f; // 限幅 if (da < 0.0f) da = 0.0f; if (da > 1.0f) da = 1.0f; if (db < 0.0f) db = 0.0f; if (db > 1.0f) db = 1.0f; if (dc < 0.0f) dc = 0.0f; if (dc > 1.0f) dc = 1.0f; // 写入定时器通道比较寄存器 tim_set_ccr(da, db, dc); }如果你的定时器是中心对齐模式,占空比0.5f对应 50% 高电平。这个转换关系要结合定时器计数周期换算成 CCR 值。
5.3 启动斜坡
上电瞬间直接给目标转速,电流冲击很大。更稳妥的做法是用斜坡让转速从 0 逐步上升到目标。
float current_speed = 0.0f; float target_speed = 600.0f; // 目标转速,单位 rpm float speed_step = 200.0f; // 加速度,单位 rpm/s void speed_ramp_periodic(float dt) { if (current_speed < target_speed) { current_speed += speed_step * dt; if (current_speed > target_speed) { current_speed = target_speed; } } openloop_update_speed(current_speed); }斜坡可以有效减少开环启动失败的概率。转速也不能升得太快,否则转子跟不上定子磁场旋转速度,会出现失步、抖动甚至反转。
5.4 简单电流保护
开环控制虽然没有电流环,但一定要有过流保护。可以在每个 PWM 周期用 ADC 采样相电流或母线电流,超过阈值后立即关闭 PWM 输出。
void fault_check(void) { float current = get_current_sensor(); if (current > CURRENT_LIMIT) { pwm_disable(); set_error_flag(ERROR_OVERCURRENT); } }过流保护要独立于控制逻辑,最好同时用硬件比较器做第二道保险。只靠软件 ADC 保护,反应时间可能不够。
6. 功能测试与效果验证
6.1 测试前静态检查
接好电机,先不运行 PWM。万用表测量母线电压是否正常,确认驱动板没有短路。然后给一个极小的占空比输出,比如三相都是 50%,用示波器观察 PWM 波形是否正常、死区是否生效。
6.2 低速空载旋转测试
把目标转速设为100rpm或200rpm,电压幅值先取母线电压的 10% 到 20%。如果电机开始平滑旋转,说明三相相序和代码逻辑匹配。
如果电机抖动但不转,多半是三相电压的相序反了。交换任意两相即可。比如把ub和uc的计算顺序对调,或者调换物理接线。
6.3 正反转与转速修改
通过串口下发目标转速,观察:
- 正转和反转是否对称。
- 改变目标转速后,实际转速是否跟随。
- 在低速到中速范围内,相电流波形是否平滑。
- 是否有明显的失步噪声。
开环控制下,实际转速不一定等于目标转速。所以这次测试的重点不是“精确”,而是观察系统趋势是否正常。
6.4 堵转与异常测试
用小力矩堵住电机轴,观察母线电流是否上升。开环控制没有力矩环,堵转时电流会持续增长,最终触发过流保护。这个测试可以验证保护逻辑是否可靠,但要注意控制时间和电流阈值,避免烧管子。
堵转之后 PWM 关闭,重新使能前要把目标转速拉回 0,等电机停稳后再启动。
6.5 串口调试接口与批量标定
嵌入式项目一般没有 REST API,但可以用串口做一个小型调试协议。这样写出的开环控制才能方便后续标定电压-转速曲线。
void uart_command_handler(uint8_t cmd, float param) { switch (cmd) { case 'S': openloop_set_speed(param); // 设置目标转速 break; case 'V': openloop_set_voltage(param); // 设置电压幅值 break; case 'T': pwm_disable(); // 紧急停车 break; default: break; } }上位机可以用 Python 脚本批量扫点:
import serial import time ser = serial.Serial("COM3", 115200, timeout=0.1) for rpm in [100, 200, 300, 400, 500, 600]: ser.write(f"S{rpm}\n".encode()) time.sleep(3) data = ser.readline().decode().strip() print(rpm, data)这样可以把不同电压、不同转速下的电流和母线电压数据保存下来,用于后续设计 V/F 曲线。
7. 关键工程问题:FOC 电流采样为什么要设置在下桥
这个问题的答案很实际:FOC 需要知道任意时刻的三相电流,但并不是所有时刻都能采样。
7.1 上桥采样和下桥采样的区别
| 采样方式 | 优点 | 缺点 |
|---|---|---|
| 下桥采样 | 采样电阻一端接在低边 MOSFET 源级,参考地稳定,共模干扰小,成本低 | 当该相上桥导通时无法采样该相电流 |
| 上桥采样 | 理论上可以直接测任意相电流 | 共模电压高,需要差分放大和隔离电路,电路复杂,成本高 |
大多数低成本 FOC 驱动板都采用下桥采样。电机相电流在 PWM 周期内的不同时间段,会流过不同桥臂。只有当下桥 MOSFET 导通时,电流才会流过下桥采样电阻。因此,电流采样通常被安排在 PWM 周期的特定时刻,也就是“下桥导通窗口”内。
7.2 为什么不能随便采样
如果 PWM 上桥导通,该相电机端子被接到母线高边,这时候测量下桥采样电阻读到的电流与相电流不对应。甚至在死区时间内,续流路径和采样窗口会发生偏移。如果采样点选错,ADC 读到的不是真实相电流,电流环再努力也没用。
更具体的做法是:
- 配置定时器为中央对齐模式。
- 在计数器计数到周期中心或特定计数值时,触发 ADC 采样。
- 避开死区时间。
- 采样触发后,等待一小段稳定时间再读值。
7.3 下桥采样的窗口限制
下桥采样的代价是:当某相占空比接近 100% 时,该相下桥导通时间很短,采样窗口不足。常见解决方法是把 PWM 周期设置足够短,同时使用双电阻或三电阻采样方案,并在扇区边界时刻重新选择可用采样窗口。
“电流采样放在下桥”不是某个人的个人习惯,而是低成本 FOC 驱动器的工程取舍。理解了这一点,你就知道为什么 FOC 调试经常要先看 PWM 波形和采样触发点,而不是直接调 PID。
8. 从开环走向闭环:为电流环、速度环、无感 FOC 做准备
开环控制稳定之后,下一步就是逐步加入反馈闭环。
8.1 为什么 Clarke 变换只需要两相电流
三相电流ia、ib、ic并不是完全独立的。对于常见的星形连接三相电机,如果没有中线,基尔霍夫电流定律给出:
ia + ib + ic = 0
所以知道任意两相电流,第三相可以直接求出来:
ic = -ia - ib
因此 FOC 驱动板上常见的电流采样设计是两相采样,而不是装三个采样通道,这样能节省 ADC 通道和采样电阻成本。
从实现上看,采样哪两相不一定要求必须是 A/B 相,任意两相都可以。实际工程中,两相采样时通常会选择当前 PWM 周期内下桥导通窗口更宽的相,方便 ADC 触发。
8.2 Park 变换与 iq 产生磁场
有了两相电流后,通过 Clarke 变换得到静止坐标系下的ialpha和ibeta,再通过 Park 变换得到旋转坐标系下的id和iq。
关键点在于iq才是真正产生力矩的电流分量。因为 FOC 控制的是转子坐标下的电流方向,iq与转子磁场垂直,自然能产生最大力矩。id和转子磁场同轴,一般被控制在 0,除非做弱磁控制。
很多初学者搞不清楚“iq 是怎么产生磁场的”:实际上 d 轴和 q 轴上的电流都会产生定子磁动势,q 轴电流产生与转子磁链正交的交轴磁场,交轴磁场和永磁体磁场相互作用,就产生了电机力矩。
8.3 转子初始位置检测
开环阶段不需要转子位置,但闭环阶段必须先知道转子在哪。
- 带编码器:上电直接读编码器角度,乘以极对数得到电角度。
- 带霍尔传感器:读取三相霍尔状态,得到 60 度或 120 度间隔的粗略位置。
- 无感方案:高频注入法、预定位法、开环强拖启动等都是常见方法。
- 开环强拖:先用固定电角度速度把转子带动,转速达到一定值后切换到无感观测器。
第十二期做开环控制的意义就在这里:它是无感 FOC 启动流程的第一步。
8.4 速度环和电流环频率配置
完整 FOC 通常有三环:
| 控制环 | 常见频率参考 | 作用 |
|---|---|---|
| 电流环 | 10kHz 到 20kHz | 控制 d/q 轴电流,产生力矩 |
| 速度环 | 1kHz 到 2kHz | 控制转速 |
| 位置环 | 100Hz 到 500Hz | 控制机械位置 |
这个配置只是常见参考,具体频率要以电机和工程需求为准。开环控制阶段不需要三环,但你需要先让定时器中断跑到一个稳定频率,方便后续直接插入电流环。
8.5 弱磁控制
当电机高速运行到电压极限时,逆变器输出电压已经达到母线电压上限,再提高转速就需要减小反电动势。弱磁控制通过给 d 轴一个负电流,对永磁磁场进行去磁,等效降低反电动势,从而在高速区获得更高转速。
弱磁控制是在电流环稳定后才能继续深入的方向,第十二期先知道这个概念即可。
9. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 电机完全不转 | PWM 没输出或驱动板无使能 | 用示波器看六路 PWM、检查驱动板 EN 脚 | 补齐使能信号或改正 PWM 配置 |
| 电机抖动、卡顿 | 三相相序反了 | 交换任意两相相序 | 程序里交换 ub、uc 输出,或调换电机线 |
| 转速比预期快很多 | 极对数设置错误 | 确认电机铭牌或编码器数据 | 修改pole_pairs |
| 转速上升缓慢 | 电压幅值太小 | 观察母线电流和相电压 | 增加电压幅值或调整 V/F 曲线 |
| 母线电压瞬间跌落 | 电源限流或者启动电流过大 | 用限流电源观察电流值 | 增加斜坡时间,降低目标电压 |
| 电流波形很乱 | 采样窗口选错或死区干扰 | 用示波器对比 PWM 和 ADC 触发点 | 调整 ADC 采样触发点 |
| 发热严重 | 开关频率过高或死区不足 | 看驱动芯片温度、PWM 波形 | 降低 PWM 频率或增加死区时间 |
| 堵转保护不触发 | 电流阈值太高或 ADC 采样不准 | 打印采样值对比万用表 | 修正电流换算系数和阈值 |
| 重新启动时需要手动复位 | 故障标志未清除 | 检查错误状态机 | 增加故障复位逻辑 |
| 切换到闭环后电机乱飞 | 角度方向不对 | 检查编码器方向和电角度映射 | 修正机械角到电角度的方向 |
10. 最佳实践与下一步
开环控制阶段的工程质量,直接决定闭环阶段好不好调。建议从一开始就建立起几个好习惯:
- 第一次上电先用最低电压空载测试,别用大电机和大电压调 bug。
- 所有关键参数用结构体集中管理,不要散落全局变量,比如极对数、母线电压、PWM 频率、电流阈值。
- 串口日志至少要打印目标转速、实际角度、三相占空比、母线电压、电流采样值,后续调闭环全靠这些数据。
- 斜坡启动是标配,不要直接跳变目标转速。
- 每个功能验证通过后,立刻提交一个 git 版本。改坏代码时可以快速回退。
- 过流保护必须独立于主循环,同时用硬件和软件两层保护。
- 高压电机实验要注意隔离,不要用手触摸母线端子,电机旋转时保持安全距离。
- 涉及开源算法源码时,确认开源协议后再学习或商用。
这一期的终点是把电机“转起来”,并且确认 PWM、驱动、采样、保护这条链路是健康的。下一阶段可以直接把电流采样、Clarke 变换、Park 变换加到现有工程里,从开环 V/F 模式平滑切换到闭环电流环模式。最开始跑通的那段斜坡启动和电压矢量生成逻辑,会成为整个 FOC 工程里最底层的基石。