扇区快速定位算法:用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流程图:无除法扇区判断的完整流程

无除法扇区判断流程图 输入 Uα, Uβ 计算中间变量 A = Uβ B = √3/2·Uα - 1/2·Uβ C = -√3/2·Uα - 1/2·Uβ 符号判断 A>0 → bit0=1 | B>0 → bit1=1 | C>0 → bit2=1 组合成3位编码 N 查表 → 扇区号 1~6 关键优化点 • 无除法、无三角函数 • 仅需加减法和比较 • 定点数实现,适合MCU • 查表映射,避免分支预测

进阶优化:查表还能更快吗?

上面那个查表版本已经很快了,但如果你追求极致性能,还有两个小技巧:

  1. 直接计算扇区号: 不用查表,用公式 N = 4*(C>0) + 2*(B>0) + (A>0),然后直接映射。但这样会多几条比较指令,实际效果取决于编译器优化能力。
  2. 内联汇编: 在ARM Cortex-M上,可以用SSAT指令做饱和运算,一条指令搞定符号判断。不过可移植性会变差,我一般只在量产代码里用。

我的建议: 对于大多数项目,用纯C的查表版本就够了。除非你的控制周期真的紧到纳秒级,否则不要轻易上汇编。代码可维护性也是成本。