3、电流环实现:从连续到离散,再到代码落地

好,咱们接着聊电流环。前面我们把电流环的数学模型和PI控制器都理清了,但那些都是连续域的东西。你想想看,我们的MCU是数字芯片,它只能处理离散的信号。所以,这一步很关键:把连续域的电流环控制器,变成能在定时器中断里跑的离散算法

我个人习惯把这一步叫做「翻译」——把数学公式翻译成C代码。翻译得不好,电机就会抖,甚至啸叫。我早期一个项目就吃过这个亏,后面细说。

3.1 电流环的离散化:双线性变换

离散化的方法有好几种,前向差分、后向差分、双线性变换。我为什么偏爱双线性变换?

说白了,它精度高。前向差分搞不好会让系统不稳定,后向差分虽然稳但精度差点。双线性变换(也叫Tustin变换)相当于把整个s平面映射到z平面,映射关系是:

s = (2/T) * (z-1)/(z+1)

其中T是采样周期,也就是定时器中断的周期。

我们把PI控制器的传递函数拿过来:

G(s) = Kp + Ki/s

代入双线性变换,一顿推导(这里省略中间过程,直接给结果),得到离散域的差分方程:

u(k) = u(k-1) + b0 * e(k) + b1 * e(k-1)

其中:

  • b0 = Kp + Ki * T / 2
  • b1 = -Kp + Ki * T / 2
  • e(k) 是当前拍的电流误差
  • e(k-1) 是上一拍的电流误差
  • u(k) 是当前拍的输出(占空比或电压指令)

嗯,这里要注意:b0和b1是预先算好的常数,不要在中断里每次重新算,那样太费时间了。

核心要点:双线性变换得到的差分方程,只有乘法和加法,没有除法。这对定点MCU非常友好。

3.2 C代码实现:在定时器中断里跑起来

代码怎么写?我习惯把电流环放在一个固定的定时器中断里,比如50kHz或者20kHz。中断频率决定了电流环的带宽。

下面是我常用的电流环中断服务函数框架:

// 电流环PI控制器参数(预先计算好)
typedef struct {
    float b0;
    float b1;
    float u_prev;  // u(k-1)
    float e_prev;  // e(k-1)
    float out_max; // 输出限幅
    float out_min;
} CurrentPI_t;

// 初始化PI参数
void CurrentPI_Init(CurrentPI_t *pi, float Kp, float Ki, float T) {
    pi->b0 = Kp + Ki * T / 2.0f;
    pi->b1 = -Kp + Ki * T / 2.0f;
    pi->u_prev = 0.0f;
    pi->e_prev = 0.0f;
    pi->out_max = 1.0f;  // 占空比限幅,0~1
    pi->out_min = -1.0f;
}

// 电流环PI计算(在中断里调用)
float CurrentPI_Calc(CurrentPI_t *pi, float target, float feedback) {
    float error = target - feedback;
    float output = pi->u_prev + pi->b0 * error + pi->b1 * pi->e_prev;

    // 更新历史值
    pi->e_prev = error;
    pi->u_prev = output;

    // 输出限幅(抗积分饱和)
    if (output > pi->out_max) {
        output = pi->out_max;
        pi->u_prev = output;  // 这里我习惯把u_prev也限幅,防止积分饱和
    } else if (output < pi->out_min) {
        output = pi->out_min;
        pi->u_prev = output;
    }

    return output;
}

// 定时器中断服务函数(假设50kHz)
void TIM1_IRQHandler(void) {
    // 1. 读取ADC电流采样值
    float ia = GetPhaseCurrentA();
    float ib = GetPhaseCurrentB();

    // 2. Clarke + Park变换得到Iq, Id
    float i_alpha = Clarke(ia, ib);
    float i_beta = ...;
    float iq = Park(i_alpha, i_beta, theta);
    float id = ...;

    // 3. 电流环PI计算
    float vq = CurrentPI_Calc(&pi_q, iq_ref, iq);
    float vd = CurrentPI_Calc(&pi_d, id_ref, id);

    // 4. 反Park + SVPWM输出
    // ...
}

避坑指南:我曾经在输出限幅时忘了更新u_prev,结果积分项一直累积,电机突然失控。后来我养成了习惯:限幅的同时,把u_prev也钳位到限幅值。这叫「抗积分饱和」,非常重要。

3.3 电流环的调试与参数整定:Ziegler-Nichols法

代码写好了,参数怎么调?你可能会想,先随便给个Kp和Ki试试。我劝你别这么干,容易烧管子。

我推荐用Ziegler-Nichols频域整定法。这个方法的核心思想是:先让系统振荡起来,然后根据振荡参数算出合适的PI值。

具体步骤:

  1. Ki先设为0,只保留Kp。从0开始慢慢增大Kp。
  2. 观察电流响应。当Kp增大到某个值时,电流开始出现等幅振荡(不衰减的正弦波)。记下这个Kp值,称为临界增益Kcr
  3. 测量振荡周期。用示波器或者直接读定时器计数值,记下振荡周期Tcr。
  4. 查表计算。对于PI控制器,Ziegler-Nichols给出的经验公式是:
控制器类型 Kp Ki
P 0.5 * Kcr 0
PI 0.45 * Kcr 1.2 * Kp / Tcr
PID 0.6 * Kcr 2 * Kp / Tcr

举个例子。我在调试一个200W的伺服电机时,Kp调到15.2时电流开始等幅振荡,振荡周期Tcr是0.8ms。那么:

  • Kp = 0.45 * 15.2 = 6.84
  • Ki = 1.2 * 6.84 / 0.0008 = 10260

你看,Ki的值往往很大,因为Tcr很小。这个值看起来吓人,但实际效果很好。

警告:做临界振荡测试时,一定要把电流限幅设小一点(比如额定电流的30%),否则振荡起来电流峰值可能烧MOS管。我刚开始做时胆子大,直接满电流试,结果...嗯,换了三个管子才学会这个教训。

3.4 本章知识体系

为了让你更直观地理解电流环实现的整体流程,我画了一张图:

电流环实现知识体系 离散化(双线性变换) s = (2/T)*(z-1)/(z+1) C代码实现 定时器中断 + PI计算 参数整定 Ziegler-Nichols法 推导出差分方程 计算b0, b1系数 PI结构体定义 中断服务函数框架 抗积分饱和处理 寻找临界增益Kcr 测量振荡周期Tcr 查表计算Kp, Ki 最终目标:稳定、快速的电流响应 带宽 > 1kHz | 超调 < 5% | 稳态误差 < 1%

这张图把电流环实现的三个核心步骤串起来了。从离散化推导,到代码落地,再到参数整定,每一步都有坑,但也都有解法。

最后说一句:参数整定没有银弹。Ziegler-Nichols法给的是起点,不是终点。实际调试时,我会在这个基础上微调:如果超调太大,适当降Kp;如果响应太慢,适当升Ki。多试几次,手感就出来了。


公众号:蓝海资料掘金营,微信deep3321