基于STM32的简易增量式离散PI控制器

PID作为最简单但是也是最常用的控制器之一,具有性能优异、实现简单等优点,本文介绍了如何在STM32上使用编码器直流电机实现PI闭环的转速控制器。

由于单片机本身就是属于离散控制的范畴,因此这个控制器必然也是离散控制器。使用PI控制器,可以增强系统的响应速度,适合于电机驱动这种对响应速度要求很高的环节。

要实现闭环控制,就必须得到电机当前的转速,因此需要选用带编码器的直流电机。对于直流电机的驱动,由于方案众多,这里不再赘述。这种电机具有安装在电机主轴上的电磁编码器。电磁编码器的原理类似于光电编码器,区别仅在于实现的方法不同,因此原理介绍部分就以光电编码器为例。

https://img.yuanze.wang/posts/stm32-pi-controller/motor.jpg
带电磁编码器的直流电机

正交编码器,顾名思义,输出的信号一定是“正交”的。这个正交,体现在输出的两路A与B信号,它们的频率均正比于电机的转速,只不过两路信号的波形总是差半个周期,并且通过这两路信号的先后,可以判断出当前电机转动的方向。正交编码器输出的波形一般都是如下图所示的两路方波信号。

https://img.yuanze.wang/posts/stm32-pi-controller/wave.gif
正交编码器输出波形图

由于编码器输出的信号频率与电机转速成正比,因此通过对这个信号计数即可得到电机轴转动的角度,若是以固定的时间进行采样,就可以得到单位时间内电机转动的角度,也就是电机的转速。当然,由于电机转速越高,这个信号的频率就越高,同时两路正交信号的时间先后也是判断电机转速的重要条件,因此使用软件对这两路信号进行采集不但精度不高,也容易出现占用处理器时间过大的问题;编码器输出频率较高时,使用中断也会导致大量处理器时间被用来响应中断。这时,STM32定时器的编码器模式就派上用场了。

STM32的高级定时器与通用定时器都具有正交编码器输入模式。这个模式使用定时器的1与2通道,两个通道分别接到编码器的A与B相,再对定时器的参数进行少许配置,即可让定时器自动处理正交编码器的输出。定时器在正交编码器输入模式下,CNT寄存器即作为计数值,当电机正转时,每一个脉冲都会让CNT自增;电机反转时,每一个脉冲都会让CNT自减。这样,只要用足够短的时间周期性读取CNT定时器,就可以获取到电机的脉冲转速。我购买的电机,主轴每旋转一圈,编码器产生1040个脉冲,也即是每个脉冲代表电机主轴转过0.35°,精度非常高。将STM32的定时器初始化为编码器的代码如下所示。

void Encoder_Init(void)
{
 GPIO_InitTypeDef GPIO_InitStructure;
 TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
 TIM_ICInitTypeDef TIM_ICInitStructure;
 
 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);//开启GPIOB时钟
 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE);//开启TIM4时钟
 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//编码器自带上拉电阻 浮空输入
 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;//PB6.PB7
 GPIO_Init(GPIOB,&GPIO_InitStructure);
 
 TIM_DeInit(TIM4);
 TIM_TimeBaseStructInit(&TIM_TimeBaseStructure);
 TIM_TimeBaseStructure.TIM_Period = 0xFFFF;//ARR为最大值
 TIM_TimeBaseStructure.TIM_Prescaler = 0;
 TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;//时钟不分频
 TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;//向上计数模式
 TIM_TimeBaseInit(TIM4, &TIM_TimeBaseStructure);
 TIM_EncoderInterfaceConfig(TIM4,TIM_EncoderMode_TI12,TIM_ICPolarity_Falling,TIM_ICPolarity_Falling);//初始化定时器为编码器模式
 TIM_ICStructInit(&TIM_ICInitStructure);//获取默认输入捕获结构体成员变量
 TIM_ICInitStructure.TIM_ICFilter = 6;//修改输入滤波器为6
 TIM_ICInit(TIM4, &TIM_ICInitStructure);//初始化输入捕获模块
 
 TIM_Cmd(TIM4,ENABLE);//使能定时器4
}

除此之外,还需要使能一个定时器,来产生PWM波,控制直流电机的转速。接下来就是产生一个周期性的定时器中断,这个周期即为离散控制的采样周期。程序中的设置值为5ms。定时器中断的代码如下。

void TIM3_IRQHandler(void)
{
 static int16_t error,motorPWM,error_last=0;
 
 if(TIM_GetITStatus(TIM3,TIM_IT_Update) != RESET)
 {
  encoder = TIM4->CNT;//读取编码器值
  TIM4->CNT = 0;//编码器值归零
  error = encoder - targetSpeed;//计算偏差
  motorPWM += Ki*(error-error_last) + Kp*error;//增量式PI控制器
  error_last = error;//保存上一次偏差 
  
  Motor_Set(motorPWM);//输出电机值
  
  TIM_ClearITPendingBit(TIM3,TIM_IT_Update);
 }
}

PI控制的代码均在这段定时器中断的函数中。首先,通过全局变量给定当前的目标值,目标值为采样周期内预计的编码器脉冲数。然后在函数中,首先读取编码器的值,也就是定时器4的CNT寄存器并将其清0,这样encoder变量即正比于上个5ms内电机主轴转过的角度。接下来,程序计算当前值与目标值的偏差,存储到error变量中,然后执行PI命令,将本次与上次的误差做差乘以Ki,并将本次的误差乘以Kp,最后保存本次的误差供下次使用并输出电机PWM驱动变量。

 STM32工程