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 / 2b1 = -Kp + Ki * T / 2e(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值。
具体步骤:
- Ki先设为0,只保留Kp。从0开始慢慢增大Kp。
- 观察电流响应。当Kp增大到某个值时,电流开始出现等幅振荡(不衰减的正弦波)。记下这个Kp值,称为临界增益Kcr。
- 测量振荡周期。用示波器或者直接读定时器计数值,记下振荡周期Tcr。
- 查表计算。对于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 本章知识体系
为了让你更直观地理解电流环实现的整体流程,我画了一张图:
这张图把电流环实现的三个核心步骤串起来了。从离散化推导,到代码落地,再到参数整定,每一步都有坑,但也都有解法。
最后说一句:参数整定没有银弹。Ziegler-Nichols法给的是起点,不是终点。实际调试时,我会在这个基础上微调:如果超调太大,适当降Kp;如果响应太慢,适当升Ki。多试几次,手感就出来了。
公众号:蓝海资料掘金营,微信deep3321