4. 核心算法实现(一):Clark变换与Park变换的C代码实现、定点数与浮点数选择、查表法优化三角函数

好,咱们直接进入正题。

FOC 控制里,Clark 变换和 Park 变换是绕不开的两道坎。说白了,它们就是把电机三相电流这种“物理世界”的量,映射到“控制世界”的旋转坐标系里。你想想看,三相电流是正弦波,不好控;但转到 dq 轴之后,就变成了直流分量,PID 调起来就顺手多了。

我个人习惯,在讲代码之前,先理清楚数学关系。不然你抄完代码,出了问题都不知道怎么查。

4.1 Clark 变换:从三相到两相

Clark 变换,也叫 3s/2s 变换。它的任务是把 Ia、Ib、Ic 这三相电流,投影到 αβ 两相静止坐标系上。

公式其实很简单:

Iα = Ia
Iβ = (Ia + 2*Ib) / √3

注意,这里有个前提:三相电流是平衡的,即 Ia + Ib + Ic = 0。所以 Ic 其实可以不用采样,算出来就行。我在项目中遇到过采样电阻不够的情况,就只采两相,第三相用公式补,省了一个 ADC 通道。

来看 C 代码实现:

typedef struct {
    float Ialpha;
    float Ibeta;
} Clark_Output;

Clark_Output clark_transform(float Ia, float Ib) {
    Clark_Output out;
    out.Ialpha = Ia;
    out.Ibeta = (Ia + 2.0f * Ib) * 0.577350269f;  // 1/√3
    return out;
}

嗯,这里要注意:0.577350269f 是 1/√3 的浮点近似。如果你用定点数,就得把这个值放大成 Q 格式。

小技巧: 如果你的 MCU 没有 FPU,浮点运算会非常慢。这时候可以考虑把系数预先算好,存成定点数。我早期在 STM32F103 上做 FOC,就是靠定点数硬扛下来的。

4.2 Park 变换:从静止到旋转

Clark 变换完了,我们得到的是 αβ 轴上的正弦波。接下来 Park 变换把它转到 dq 旋转坐标系,变成直流。

公式:

Id = Iα * cos(θ) + Iβ * sin(θ)
Iq = -Iα * sin(θ) + Iβ * cos(θ)

这里的 θ 是转子电角度,通常由编码器或霍尔传感器提供。

代码实现:

typedef struct {
    float Id;
    float Iq;
} Park_Output;

Park_Output park_transform(float Ialpha, float Ibeta, float theta) {
    Park_Output out;
    float sin_t = sinf(theta);
    float cos_t = cosf(theta);
    out.Id = Ialpha * cos_t + Ibeta * sin_t;
    out.Iq = -Ialpha * sin_t + Ibeta * cos_t;
    return out;
}

看到 sinfcosf 了吗?这是最直接的写法,但也是性能瓶颈。为什么?因为三角函数计算太慢了。

避坑指南: 我曾经在一个项目中直接调用了标准库的 sinf/cosf,结果 PWM 频率一高,CPU 就满负荷了。后来一分析,三角函数占了 40% 的运算时间。所以,必须优化。

4.3 定点数与浮点数:选哪个?

这是个老生常谈的问题。我直接给结论:

特性 浮点数 定点数
精度 高,动态范围大 受 Q 格式限制,需小心溢出
速度 有 FPU 时快,无 FPU 时极慢 整数运算,通常很快
代码可读性 好,直接写数学公式 差,需要手动处理缩放
适用场景 带 FPU 的 MCU(如 STM32F4/F7/H7) 低成本 MCU(如 STM32F1、C2000 部分型号)

我的建议是:能用浮点就用浮点。现在带 FPU 的 MCU 已经很便宜了,没必要在定点数上折磨自己。但如果你确实要用定点数,记住三点:

  • 选好 Q 格式。比如电流用 Q12,角度用 Q15。
  • 乘法后记得右移,防止溢出。
  • 加法和减法一般没问题,但乘法和除法要小心。

4.4 查表法优化三角函数

好,回到三角函数的问题。不用标准库,那用什么?查表法

思路很简单:把 0 到 360 度的 sin 值预先算好,存成一个数组。运行时直接查表,或者查表加线性插值。

代码示例:

#define TABLE_SIZE 256
float sin_table[TABLE_SIZE];

void init_sin_table(void) {
    for (int i = 0; i < TABLE_SIZE; i++) {
        sin_table[i] = sinf(2.0f * 3.1415926f * i / TABLE_SIZE);
    }
}

float fast_sin(float theta) {
    // theta 范围:0 ~ 2*PI
    int index = (int)(theta * TABLE_SIZE / (2.0f * 3.1415926f)) % TABLE_SIZE;
    return sin_table[index];
}

这个实现精度一般,但速度极快。如果你需要更高精度,可以加线性插值:

float fast_sin_lerp(float theta) {
    float frac = theta * TABLE_SIZE / (2.0f * 3.1415926f);
    int index = (int)frac;
    float delta = frac - index;
    float s0 = sin_table[index % TABLE_SIZE];
    float s1 = sin_table[(index + 1) % TABLE_SIZE];
    return s0 + delta * (s1 - s0);
}

你看,这样既保留了速度,精度也够用。我在一个 72MHz 的 STM32F103 项目里,用查表法把 Park 变换的耗时从 12μs 降到了 2μs,效果非常明显。

核心要点: 查表法不是万能的。如果你的角度分辨率要求很高(比如伺服驱动),表太大反而浪费内存。这时候可以考虑 CORDIC 算法,但那是另一个话题了。

4.5 本章知识体系

为了让你更直观地理解,我画了一张图:

FOC 核心算法流程 三相电流 Ia, Ib, Ic Clark 变换 3s → 2s Iα, Iβ 静止坐标系 Park 变换 2s → 2r 电角度 θ 来自编码器/霍尔 Id, Iq 旋转坐标系 优化要点 • 定点数 vs 浮点数选择 • 三角函数查表法 • 线性插值提升精度 • 避免标准库 sinf/cosf • 预计算系数表

这张图把 Clark 变换、Park 变换以及优化要点串在了一起。你写代码的时候,脑子里要有这个流程。

好了,这一章就到这里。Clark 和 Park 是 FOC 的基石,代码虽然简单,但优化空间很大。下一章我们会继续深入,看看反 Park 变换和 SVPWM 怎么实现。

专注资料整理