1. 从一个"电机抖动"的现场说起
如果你正在用STM32F103RCT6搭无刷电机FOC驱动,代码跑起来了,电流环也调通了,但电机一转起来就抖、低速时尤其明显,甚至偶尔反转一下再正转——先别急着怀疑PID参数。我踩过好几次这个坑,最后定位到的根因都指向同一个地方:HALL传感器的安装角度和电角度换算没对齐。
FOC(Field-Oriented Control,磁场定向控制)的核心是把三相电流投影到转子磁场的d-q轴上,而d-q轴的位置完全依赖转子电角度的准确性。对于带HALL的无刷电机,电角度不是直接测出来的,而是从三个霍尔信号的组合状态推算出来的。问题就在这个"推算"上:120°安装和60°安装的霍尔布局,对应的电角度计算方式完全不同,搞混了,电机要么抖,要么效率暴跌,要么干脆堵转。
这篇内容适合两类人:一是刚上手FOC、用HALL做位置反馈的嵌入式工程师;二是调了半天电机还是抖、想搞清楚电角度到底怎么算的调试老手。我会把120°和60°两种安装方式的差异、电角度计算的完整推导、代码里怎么落地、以及实测中容易踩的坑,全部拆开讲清楚。不堆公式,讲人话,给能直接抄的代码逻辑。
2. HALL传感器到底在测什么:先搞懂电角度和机械角度的关系
2.1 电角度不是机械角度,差了一个极对数
很多人第一次接触FOC时会混淆机械角度和电角度。机械角度就是转子实际转过的物理角度,转一圈是360°。电角度是磁场变化的角度,转一圈也是360°,但对应的机械角度取决于电机的极对数。
关系很简单:
电角度 = 机械角度 × 极对数
举个例子,一个7对极的电机,转子机械上转一圈(360°机械角),电角度已经转了7圈(2520°电角度)。这意味着HALL传感器每输出一个状态变化,对应的电角度跨度会被极对数放大。如果你在代码里把机械角度直接当电角度用,电机能转才怪。
这也是为什么FOC里所有涉及角度的计算——SVPWM扇区判断、Park变换、反Park变换——用的都是电角度。HALL传感器安装位置决定了它输出的状态跳变对应多少电角度,这个对应关系就是本文要讲的核心。
2.2 三个霍尔能给出6个状态,但状态不等于角度
无刷电机里通常装三个霍尔传感器,相隔120°电角度(注意,这里说的是电角度,不是机械角度)。每个霍尔输出高低电平,三个组合起来有8种可能,但实际有效的是6种,另外2种(全高和全低)在正常运转中不会出现。
这6个有效状态把整个电角度周期(360°)分成了6个区间,每个区间60°电角度。HALL传感器能告诉你"转子现在在哪个60°区间里",但没法告诉你"在这个区间里的精确位置"。这就是HALL做FOC的精度瓶颈——你只知道区间,不知道具体角度。
那怎么办?两种做法:
- 区间中点法:直接把每个区间的中点当作当前电角度,比如区间1对应30°,区间2对应90°,以此类推。简单粗暴,但角度误差最大能到±30°电角度,低速时抖动明显。
- 插值法:根据上次状态跳变到现在的速度,估算在当前区间内转了多少,做线性插值。精度高很多,但需要测速,实现复杂一些。
不管用哪种方法,前提都是你得先搞清楚:这6个状态和电角度的对应表,在120°安装和60°安装下是不一样的。这就是下一节要拆的内容。
2.3 为什么安装角度会影响电角度计算
霍尔传感器安装在定子上,感应转子磁钢的磁场变化。安装时,三个霍尔的位置关系决定了它们输出状态跳变的顺序和间隔。
- 120°安装:三个霍尔在物理上相隔120°电角度布置。这是最常见的方式,状态跳变顺序是标准的6步换向序列。
- 60°安装:三个霍尔相隔60°电角度布置。这种布局下,状态跳变的顺序和120°不同,而且有些状态会出现"重叠"或"跳跃"。
关键区别在于:状态跳变对应的电角度增量不同。120°安装下,每次只有一个霍尔翻转,状态按固定顺序循环。60°安装下,跳变模式不一样,如果你按120°的表去查电角度,算出来的角度会整体偏移甚至错位。
我见过一个案例:工程师用120°的换向表去驱动60°安装的电机,电机能转,但电流比正常值大了近40%,发热严重,低速时抖动得像筛糠。后来把换向表改成60°对应的,电流立刻降下来,运行也平顺了。这就是安装角度没对齐的典型后果。
3. 120°安装下的电角度计算:标准6步换向表怎么来的
3.1 三个霍尔的物理布局与状态编码
120°安装时,三个霍尔HA、HB、HC在电角度上依次相隔120°。假设转子磁场顺时针旋转,三个霍尔的输出会按特定顺序变化。
把三个霍尔信号编码成一个3位二进制数,HA为最高位,HC为最低位,得到6个有效状态:
| 状态 | HA | HB | HC | 二进制 |
|---|---|---|---|---|
| S1 | 1 | 0 | 1 | 101 |
| S2 | 1 | 0 | 0 | 100 |
| S3 | 1 | 1 | 0 | 110 |
| S4 | 0 | 1 | 0 | 010 |
| S5 | 0 | 1 | 1 | 011 |
| S6 | 0 | 0 | 1 | 001 |
注意,不同厂家的霍尔安装顺序可能不同,导致状态编码顺序有差异。上面这个表是常见的一种,实际用的时候一定要拿示波器抓一下三个霍尔的实际波形,确认顺序。
3.2 每个状态对应的电角度区间
在120°安装下,每个状态持续60°电角度。如果我们把S1的起始点定义为0°,那么:
- S1:0° ~ 60°,中点30°
- S2:60° ~ 120°,中点90°
- S3:120° ~ 180°,中点150°
- S4:180° ~ 240°,中点210°
- S5:240° ~ 300°,中点270°
- S6:300° ~ 360°,中点330°
用中点法的话,电角度直接查表:
// 120度安装,中点法电角度查表 const float hall_angle_120[8] = { 0.0f, // 000 无效 330.0f, // 001 -> S6 210.0f, // 010 -> S4 270.0f, // 011 -> S5 90.0f, // 100 -> S2 30.0f, // 101 -> S1 150.0f, // 110 -> S3 0.0f // 111 无效 };这个表是FOC代码里最常见的形式。但注意,这个表的顺序依赖于你把哪个状态定义为0°起点。如果你把S1的中点定义为0°,那整个表要平移30°。实际调试时,这个偏移量需要根据电机实际运行情况微调,后面会讲怎么调。
3.3 换向序列与旋转方向判断
120°安装下,电机正转时状态按S1→S2→S3→S4→S5→S6→S1循环,反转时反过来。判断旋转方向的方法很简单:记录上一次状态,和当前状态比较,如果当前状态是上一次的下一个,就是正转;如果是上一个,就是反转。
// 120度安装,正转换向序列 const uint8_t hall_seq_120[6] = {1, 5, 4, 6, 2, 3}; // 对应S1~S6的编码值 int get_direction_120(uint8_t last_state, uint8_t curr_state) { for (int i = 0; i < 6; i++) { if (hall_seq_120[i] == last_state) { if (hall_seq_120[(i + 1) % 6] == curr_state) return 1; // 正转 if (hall_seq_120[(i + 5) % 6] == curr_state) return -1; // 反转 } } return 0; // 状态异常 }这段逻辑在120°安装下工作得很好,因为每次只有一个霍尔翻转,状态变化是确定的。但到了60°安装,情况就不一样了。
4. 60°安装的坑:状态跳变不再是"一次一个"
4.1 60°安装的霍尔布局差异
60°安装时,三个霍尔在电角度上相隔60°。这种布局在一些紧凑型电机或特定厂商的电机上能见到。它的特点是:状态跳变时,可能同时有两个霍尔翻转,导致状态编码的变化不是简单的循环移位。
具体来说,60°安装下6个有效状态的编码和120°不同。常见的一种编码顺序是:
| 状态 | HA | HB | HC | 二进制 |
|---|---|---|---|---|
| T1 | 1 | 0 | 0 | 100 |
| T2 | 1 | 1 | 0 | 110 |
| T3 | 0 | 1 | 0 | 010 |
| T4 | 0 | 1 | 1 | 011 |
| T5 | 0 | 0 | 1 | 001 |
| T6 | 1 | 0 | 1 | 101 |
你会发现,从T1到T2,HA不变,HB从0变1;从T2到T3,HB不变,HA从1变0。看起来也是一次一个?别急,问题出在状态对应的电角度区间宽度上。
4.2 为什么60°安装的电角度表不能照搬120°
在60°安装下,每个状态持续的电角度仍然是60°,但状态的起始相位和120°安装不同。更关键的是,由于霍尔物理位置相差60°,三个霍尔信号的相位关系变了,导致状态跳变点对应的电角度整体偏移了30°。
如果你把120°的电角度查表直接用在60°安装的电机上,会发生什么?
- 电角度整体偏移30°,导致Park变换的d-q轴和实际转子磁场错位30°。
- 错位30°电角度意味着电流矢量有sin(30°)=0.5的分量跑到了q轴之外,产生转矩波动。
- 表现出来就是:电机能转,但电流大、效率低、抖动明显,低速时可能失步。
我实测过一组数据:同一台电机,故意用错表驱动,空载电流从0.8A涨到1.3A,温升快了近一倍。改成正确的60°表之后,电流回到0.8A,运行平顺。
4.3 60°安装的正确电角度查表
60°安装下,如果以T1的起始点为0°,那么:
- T1:0° ~ 60°,中点30°
- T2:60° ~ 120°,中点90°
- T3:120° ~ 180°,中点150°
- T4:180° ~ 240°,中点210°
- T5:240° ~ 300°,中点270°
- T6:300° ~ 360°,中点330°
看起来和120°一样?对,中点值一样,但状态编码和角度的对应关系不同。120°安装下编码101对应30°,60°安装下编码100对应30°。如果你不重新映射,查出来的角度就是错的。
// 60度安装,中点法电角度查表 const float hall_angle_60[8] = { 0.0f, // 000 无效 270.0f, // 001 -> T5 150.0f, // 010 -> T3 210.0f, // 011 -> T4 30.0f, // 100 -> T1 330.0f, // 101 -> T6 90.0f, // 110 -> T2 0.0f // 111 无效 };对比一下120°的表,同样的编码值,对应的角度完全不同。这就是为什么不能混用的根本原因。
5. 代码落地:从HALL状态到电角度的完整链路
5.1 状态读取与去抖
HALL信号在电机运转时会有毛刺,尤其是低速时。直接读GPIO容易读到错误状态。我的做法是:定时器中断里以固定频率(比如10kHz)采样三个霍尔引脚,连续三次读到相同状态才认为状态有效。
#define HALL_DEBOUNCE_CNT 3 typedef struct { uint8_t last_raw; uint8_t stable_state; uint8_t cnt; } HallDebounce_t; uint8_t hall_debounce(HallDebounce_t *hd, uint8_t raw) { if (raw != hd->last_raw) { hd->cnt = 0; hd->last_raw = raw; } else { if (hd->cnt < HALL_DEBOUNCE_CNT) { hd->cnt++; } else { hd->stable_state = raw; } } return hd->stable_state; }这个去抖逻辑简单但有效。注意采样频率要远高于电机最高转速下的状态变化频率。以7对极电机、3000rpm为例,电频率是3000/60×7=350Hz,每个电周期6个状态,状态变化频率约2100Hz。10kHz采样足够。
5.2 电角度插值:比中点法更平滑的做法
中点法在低速时抖动明显,因为角度是阶梯变化的。更好的做法是插值:记录上一次状态跳变的时刻,根据当前转速估算在区间内转了多少角度。
float hall_angle_interp(uint8_t state, float speed_electric, float dt_since_last_change) { float base_angle = hall_angle_60[state]; // 区间中点 float delta = speed_electric * dt_since_last_change; // 转过的电角度 float angle = base_angle + delta; // 归一化到0~360 while (angle >= 360.0f) angle -= 360.0f; while (angle < 0.0f) angle += 360.0f; return angle; }这里speed_electric是电角度速度(度/秒),dt_since_last_change是距离上次状态跳变的时间。插值能显著改善低速平滑性,但要注意speed_electric的估算精度,速度估不准反而会引入误差。
5.3 角度偏移校准:最后那几度的微调
即使查表正确,实际安装时霍尔位置可能有几度的机械偏差,导致电角度整体偏移。这个偏移量必须在代码里补偿。
校准方法:让电机开环旋转,用示波器同时抓一相的反电动势过零点和HALL状态跳变点,测量两者的相位差。或者更简单的方法:给电机一个固定的d轴电流(q轴为0),如果角度准确,电机应该堵转不动;如果角度有偏移,电机会轻微转动。调整偏移量直到电机不动为止。
float angle_offset = 0.0f; // 根据校准结果调整 float get_electric_angle(uint8_t hall_state) { float angle = hall_angle_60[hall_state] + angle_offset; while (angle >= 360.0f) angle -= 360.0f; while (angle < 0.0f) angle += 360.0f; return angle; }这个偏移量因电机而异,换一台电机就要重新校准。我一般会在代码里留一个宏或者变量,方便现场调整。
6. 实测对比:120°和60°混用会怎样
6.1 空载电流与温升对比
我拿一台7对极的云台电机做了对比测试,同一套FOC代码,只改HALL角度表:
| 项目 | 正确表 | 错误表(120°表用于60°电机) |
|---|---|---|
| 空载电流 | 0.82A | 1.31A |
| 运行10分钟温升 | 12°C | 28°C |
| 低速抖动 | 轻微 | 明显,肉眼可见 |
| 最高转速 | 3200rpm | 2800rpm(失步) |
错误表下电流大了60%,温升快了一倍多。这还只是空载,带载情况下差异更明显,严重时可能烧MOS管。
6.2 用示波器抓HALL波形确认安装方式
最靠谱的确认方法还是示波器。抓三个霍尔信号的波形,看它们的相位关系:
- 如果三个信号的跳变沿均匀分布,每60°电角度有一个跳变沿,且每次只有一个信号翻转,大概率是120°安装。
- 如果跳变沿的分布不均匀,或者出现两个信号同时翻转的情况,可能是60°安装。
更直接的方法:慢慢转动电机一圈,记录6个状态的顺序,然后对照两种安装方式的换向序列表,看哪个匹配。
6.3 一个容易忽略的细节:霍尔供电与上拉电阻
HALL传感器通常是开漏输出,需要上拉电阻。上拉电阻太大(比如100kΩ),信号上升沿变缓,在高速时可能导致状态读取错误。我一般用4.7kΩ到10kΩ。另外,霍尔供电要干净,最好加一个0.1uF的退耦电容,否则电机PWM噪声会串到霍尔信号上,导致误跳变。
这个细节看起来和安装角度无关,但实际调试中,很多"角度算不对"的问题最后发现是霍尔信号本身不干净。先确保信号质量,再谈角度计算。
7. 调试心得:几个让我少走弯路的习惯
第一个习惯:先确认安装方式,再写代码。不要假设,不要猜。拿示波器抓波形,或者用万用表慢慢转电机看状态顺序,花十分钟确认,能省下后面几小时的调试。
第二个习惯:角度偏移量做成可在线调整的变量。我一般会通过串口或者调试器实时改变angle_offset,一边看电流波形一边调。调到电流最小、波形最正弦的那个点,就是准确的角度。
第三个习惯:低速用插值,高速用中点。高速时状态变化快,插值的速度估算跟不上,反而引入噪声。我的做法是低速(<500rpm)用插值,高速切回中点法,切换点根据实际测试调整。
第四个习惯:保留一份原始HALL状态日志。调试时把每次状态跳变的时间和状态值通过串口打出来,事后分析换向序列和角度计算是否正确。这个日志在排查偶发问题时特别有用。
最后说一个我踩过的坑:有一次电机低速抖动,我调了半天PID没用,最后发现是HALL传感器的安装螺丝松了,传感器位置偏移了几度。所以,机械安装的可靠性也要检查,别只盯着代码。