扇区快速定位算法:用A、B、C干掉除法
上一章我们讲了传统扇区判断——查表、算角度、做除法。说实话,那种方法在教科书上看着挺工整,但拿到嵌入式里跑,尤其是用STM32或者DSP做高频控制时,心里总有点不踏实。
为什么?因为除法太慢了。我早年做伺服驱动时,PWM频率跑到20kHz,一个控制周期才50微秒。你想想看,如果每次扇区判断都要来一次反正切或者除法,那剩下的时间还够做电流环吗?
所以这一章,我们来聊一个真正能在工程里落地的方案——利用中间变量A、B、C实现无除法扇区判断。说白了,就是只用加减法和比较,就能把扇区定下来。
从数学到代码:A、B、C的由来
先回忆一下,我们在Clark变换后得到了αβ轴上的电压分量Uα和Uβ。传统做法是用Uβ/Uα算角度,然后查表。但如果我们换个思路呢?
我定义一个中间变量:
A = Uβ
B = (√3/2) * Uα - (1/2) * Uβ
C = -(√3/2) * Uα - (1/2) * Uβ
等一下,这里是不是还有系数?没错,有√3和除法。但别急——在实际代码里,我们根本不会真的去算√3/2。我会在初始化时把这个系数算好存成常量,或者直接用定点数做缩放。
这三个变量其实对应的是三相电压在120°坐标系下的投影。你想想看,SVPWM的六个扇区,本质上就是把电压矢量投影到三个轴线上,然后看哪个轴的分量最大、哪个最小。
核心思想: 扇区判断不需要知道具体角度,只需要知道A、B、C三个值的正负关系。
扇区与A、B、C的对应关系
有了A、B、C之后,扇区判断就变成了一组简单的逻辑比较。我习惯用一个3位的二进制数来表示扇区:
| 条件 | 二进制位 | 扇区号 |
|---|---|---|
| A > 0 | bit0 = 1 | N = 4*C_sign + 2*B_sign + 1*A_sign |
| B > 0 | bit1 = 1 | |
| C > 0 | bit2 = 1 |
然后查一个预置的映射表,就能直接得到扇区号。我在项目中用过的一个经典映射是这样的:
// 扇区映射表
const uint8_t sector_map[8] = {
0, // 000 - 无效
1, // 001 - 扇区I
5, // 010 - 扇区V
0, // 011 - 无效
3, // 100 - 扇区III
2, // 101 - 扇区II
4, // 110 - 扇区IV
0 // 111 - 无效
};
嗯,这里要注意:000和111在正常情况下不会出现,因为三个变量不可能同时为负或同时为正。但我在调试时遇到过因为ADC采样异常导致全零的情况,所以映射表里留了0作为错误标志。
C语言实现:从理论到寄存器
直接上代码吧。这是我个人比较喜欢的一种写法,既清晰又高效:
// 扇区快速判断函数
// 输入:Ualpha, Ubeta(Q15格式,范围-1.0 ~ 1.0)
// 输出:扇区号 1~6,异常返回0
uint8_t svpwm_sector_fast(int16_t Ualpha, int16_t Ubeta)
{
int16_t A, B, C;
uint8_t sector_code = 0;
// 计算中间变量
A = Ubeta;
B = (int16_t)((int32_t)Ualpha * 0x6ED9 >> 15) - (Ubeta >> 1);
C = (int16_t)((int32_t)(-Ualpha) * 0x6ED9 >> 15) - (Ubeta >> 1);
// 符号判断,组合成3位二进制
if(A > 0) sector_code |= 0x01;
if(B > 0) sector_code |= 0x02;
if(C > 0) sector_code |= 0x04;
// 查表映射
return sector_map[sector_code];
}
优化技巧: 0x6ED9是√3/2的Q15定点数表示。我习惯在头文件里用宏定义:#define SQRT3_OVER_2_Q15 0x6ED9。这样代码可读性更好,编译器也能做常量折叠优化。
避坑指南:我曾经踩过的三个坑
这个算法看起来简单,但真正移植到不同平台上时,有几个地方特别容易出问题。我一个个说:
- 符号位溢出: 计算B和C时,Ualpha乘以0x6ED9会得到一个32位中间结果。如果Ualpha是满量程的负值,乘完后可能溢出。我建议用int32_t做中间计算,然后再截断回int16_t。
- 右移的符号扩展: 在C语言里,有符号整数的右移是算术右移还是逻辑右移,取决于编译器。我遇到过在ARM上没问题,换到C2000上就出错的案例。保险起见,可以用条件判断代替右移,或者显式做符号位处理。
- 查表越界: sector_code的范围是0~7,但映射表必须保证8个元素。我曾经手滑只写了7个元素,结果在扇区III时直接数组越界,电机抖得像筛子一样。
重要提醒: 如果你用的是浮点DSP,可以直接用float算A、B、C,然后比较。但如果是定点MCU,一定要确认好Q格式的精度。我建议至少用Q15,如果Uα和Uβ的动态范围大,可以考虑Q12。
性能对比:无除法到底快多少?
我在STM32F407上做过一个简单的对比测试,主频168MHz,优化等级-O2:
| 方法 | 执行周期 | 耗时(us) | 代码量(bytes) |
|---|---|---|---|
| 反正切+查表 | ~120 | 0.71 | 256 |
| 除法+比较 | ~85 | 0.51 | 192 |
| A、B、C法(本方法) | ~28 | 0.17 | 88 |
看到没?快了将近5倍。而且代码量只有88个字节,对于ROM紧张的老平台来说,这个优势非常明显。
SVG流程图:无除法扇区判断的完整流程
进阶优化:查表还能更快吗?
上面那个查表版本已经很快了,但如果你追求极致性能,还有两个小技巧:
- 直接计算扇区号: 不用查表,用公式 N = 4*(C>0) + 2*(B>0) + (A>0),然后直接映射。但这样会多几条比较指令,实际效果取决于编译器优化能力。
- 内联汇编: 在ARM Cortex-M上,可以用SSAT指令做饱和运算,一条指令搞定符号判断。不过可移植性会变差,我一般只在量产代码里用。
我的建议: 对于大多数项目,用纯C的查表版本就够了。除非你的控制周期真的紧到纳秒级,否则不要轻易上汇编。代码可维护性也是成本。