比例积分微分控制,简称PID控制,由于其 算法简单、***鲁棒性好***和 可靠性高,被广泛应用于工业过程控制,至今仍有 90% 左右的控制回路具有PID结构。
简单的说,根据给定值和实际输出值构成控制偏差,将偏差按比例
、积分
和微分
通过线性组合构成控制量,对被控对象进行控制。
所谓串级控制,就是采用两个控制器串联工作,外环控制器的输出作为内环控制器的设定值,由内环控制器的输出去操纵控制阀,从而对外环被控量具有更好的控制效果。
万能的PID控制听起来很陌生,但是在我们的生活中随处可见。比如空调的温度控制,无人机的精准悬停,机械手臂的运动系统,人脸跟踪,甚至飞机和火箭的姿态调整等等,都要到这这个最基础的自动控制算法。
一定要认真理解PID控制算法是一种闭环系统!!!
首先我们先不引入过多的复杂概念,先结合一个具体的生活实例来形象生动的讲述PID的各个环节。
众所周知,某校西区宿舍洗澡是要调控冷热水比例才能达到令人舒服的水温。我有个师兄,现在这位师兄已经放出了冷水,只需要再加入50个单位的热水就可以肆无忌惮的洗澡了。一般地,我们会先设置一个达成目标的时间。比如说5秒,那么这位师兄操纵机械臂旋转热水阀的平均速度是10单位/秒。按照这个速度匀速转动开关5s就可以完成目标了。而这种控制方法叫做***开放回路控制系统***,模式图如下:
这种控制系统的可以理解成是一种流水线的结构,输入是恒定的值,输出也是对应得到的恒定值,且输出值对输入值是没有影响的
然而要知道,机械臂有可能因为沾上了水而容易发生偏移,又或者机械臂转动速度并不是那么精准,是无法达到他洗澡要求的50单位热水。况且这位老同志突然想改成40单位热水或者60单位热水,那么我们又需要重新给这个系统赋予新的输入值,这是件很麻烦的事情,总不能老同志洗澡的时候,我们闯进去计算输入值给机械臂吧!
在这里我们更需要一种***闭环回路控制系统***。在这样一个系统中,输入值不在是一个恒定的值,而是会受输出值的影响。根据当前的热水总量来重新调整机械臂的转动力,模式图如下:
这就是PID控制的基本模式图
那么这种调整是如何做出来的呢? 我们引入一个误差值
来表示当前值与目标值的差值。当这个误差值越大时,机械臂的转动力就越大。并且我们把误差值
与转动开关的力
的关系表示为一个线性关系,同时误差
是关于时间
的函数
F = K p ∗ e r r o r (1) F=K_p*error\tag{1} F=Kp∗error(1)
e r r o r = f ( t ) (2) error=f(t)\tag{2} error=f(t)(2)
其中的 kp 是一个系数,由此公式我们可以看出在最初状态时,机械臂旋转力量是最大的,随着误差越来越小,旋转力也逐渐减小,热水量逐渐接近目标值。这就是PID算法中的 “P” 部分,即***比例部分,它表示的是转动速度v与误差error之间的比例关系***。用图像表示即为
同时通过图像,我们也注意到逐渐接近目标值时,虽然转动力最终会为零,但因为存在着 ***“惯性”***,在接近目标时难免会产生一点溢出,产生额外的误差。就好比在目标值处加速度虽然为零,但速度仍然存在,是会超过目标值的。说白了,就是春洲师兄手抖抖抖抖抖抖抖了。 况且这种振荡误差会因为接近目标值速度过快,即kp过大而愈发明显。 这是我们就需要介绍PID算法中的 ***“D”,算法,即微分算法。***。我们对误差进行微分就是造成震荡溢出的速度
,通过这个运算我们可以抵消掉振荡产生的速度。加入微分运算
后PID的简易数学表达为
F = K p ∗ e r r o r + K d ∗ d ( e r r o r ) d t (3) F=K_p*error+K_d*{d(error)\over dt}\tag{3} F=Kp∗error+Kd∗dtd(error)(3)
K d = K p ∗ T d (4) K_d=K_p*T_d\tag{4} Kd=Kp∗Td(4)
其中 Kd 为微分系数, Td 为微分时间常数。基本上通过 P 、D 两种运算我们就能得到一个平稳的图像。
不过细心一些我们就不难发现即使经过了这两种运算,误差也是不能被消除的。随着误差减小,春洲师兄的机械臂转动力也减小,最终会和阻力平衡,这时开关不会在转动,误差仍然存在,也就是说我们还需要抵消掉阻力造成的误差。 在这里,我们通过累积所有时间段的误差,得到一个均值
再乘以某个系数来提供给春洲师兄更大的转动力来弥补阻力。数学表达式如下:
F = K p ∗ e r r o r + K d ∗ d ( e r r o r ) d t + K i ∗ ∫ 0 t ( e r r o r ) d t (5) F=K_p*error+K_d*{d(error)\over dt}+K_i*\int_0^t (error)dt \tag{5} F=Kp∗error+Kd∗dtd(error)+Ki∗∫0t(error)dt(5)
K i = K p T i (6) K_i={K_p\over T_i}\tag{6} Ki=TiKp(6)
其中 Ti 为积分时间常数,Ki 为积分系数。综合以上六个式子,我们就得到了PID算法完整的数学表达式:
F = K p ∗ e r r o r + K d ∗ d ( e r r o r ) d t + K i ∗ ∫ 0 t ( e r r o r ) d t F=K_p*error+K_d*{d(error)\over dt}+K_i*\int_0^t (error)dt F=Kp∗error+Kd∗dtd(error)+Ki∗∫0t(error)dt
K i = K p T i K_i={K_p\over T_i} Ki=TiKp
K d = K p ∗ T d K_d=K_p*T_d Kd=Kp∗Td
简易模式图:
比例作用是针对系统当前误差进行控制,积分作用则针对系统误差的历史,而微分作用则反映了系统误差的变化趋势,这三者的组合是“过去、现在、未来”的完美结合
其实从上面的概念理解中,我们就很容易通过比例、积分、微分三个环节的作用来衡量这个PID控制是否优越。
从最开始到稳定值的时间
以及震荡的程度
在稳定值时与目标值之间的差距
稳定值调整到目标值所需时间
于是就产生了四个衡量PID系统的指标:
1、上升时间 t r t_r tr:从最开始到稳定值的时间
2、超调量 σ % \sigma\% σ%:震荡的程度
3、稳态误差 e s s e_{ss} ess:在稳定值时与目标值之间的差距
4、调节时间 t e t_e te:稳定值调整到目标值所需时间
我们就是通过这四个指标来衡量一个PID控制系统性能好坏,而影响这些指标的即是PID算法公式中的三个系数。所以我们需要通过对这几个参数进行整定来提高系统的性能。
我们把目光重新放到PID的数学表达式上,我们不难发现 Kd、Ki的值是和Kp有关的。因此这三个系数的值不是独立的,当Kp值最优时,另外两个不一定就是最优的值。我们寻求的不是局部最优,而是三个系数组合起来的全局最优。
Kp使得控制器的输入输出成比例关系,为了尽量减小偏差,同时也为了加快响应速度
,缩短调节时间
,就需要增大Kp。但比例作用过大会使系统有较大惯性,造成溢出
。
Ki作用的引入有利于消除稳态误差
,但使系统的稳定性下降
。尤其在大偏差阶段的积分往往会使系统产生过大的超调,调节时间变长
。
Kd作用的引入使系统能够根据偏差变化的趋势做出反应,适当的微分作用可加快系统响应
,有效地减小超调
,改善系统的动态特性,增加系统的稳定性。不利之处是微分作用对干扰敏感,使系统抑制干扰能力降低
。
因此,PID控制器的参数选取必须兼顾动态与静态性能指标要求,只有合理地整定Kp、Ki、Kd三个参数,才能获得比较满意的控制性能。
下面我们介绍正定参数的方法。
最省事儿的办法就是先增大 Kp ,当能看到明显振荡时,引入Kd并适当减小Kp。如果一直打不到预期值,则引入 Ki并慢慢增大。
还有一种是正规的方法,临界比例度法(Z-N法)。先只引入比例算法,从大到小逐渐改变调节器的比例度,得到等幅振荡的过渡过程,此时的比例度称为临界比例度
,用 δ k {\delta_k} δk表示。相邻两个波峰间的时间间隔,称为临界振荡周期
,用 T K {T_K} TK表示。用这两个值根据表格通过计算即可求出调节器的三个整定参数。
鉴于代码量繁杂,以下只展示关键代码部分。我们通过运用PID控制算法来让电机匀速稳定转动 。下面代码参考“平衡小车之家”的测试代码,其中串口的收发数据帧协议是由正点原子提供
1、主函数
u8 flag_Stop=1; //收发数据的停止标志位
int Encoder; //编码器脉冲计数
int moto; //电机PWM变量
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //中断分组
Stm32_Clock_Init(9); //系统时钟设置函数
delay_init(); //延时函数初始化
LED_Init(); //初始化LED的GPIO口
uart_init(115200); //串口初始化
MOTO_Init(); //初始化电机所接GPIO口
pwm_Init(7199,0); //初始化PWM波
Encoder_Init_TIM2(); //初始化定时器
TIM3_Int_Init(99,7199); //初始化中断函数 每10ms一次中断
while(1)
{
printf("%d\r\n",Encoder); //通过串口打印脉冲数来形象看到PID控制
delay_ms(10);
}
}
2、串口的初始化与串口收发数据函数
#if EN_USART1_RX //如果使能了接收
//串口1中断服务程序
//注意,读取USARTx->SR能避免莫名其妙的错误
u8 USART_RX_BUF[USART_REC_LEN];
//接收缓冲区最大存储USART_REC_LEN个字节.
//接收状态
//bit15, 接收完成标志
//bit14, 接收到0x0d
//bit13~0, 接收到的有效字节数目
u16 USART_RX_STA=0; //接收状态标记
void uart_init(u32 bound){
//GPIO端口设置
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA, ENABLE); //使能USART1,GPIOA时钟
//USART1_TX GPIOA.9
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //PA.9
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.9
//USART1_RX GPIOA.10初始化
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;//PA10
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入
GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.10
//Usart1 NVIC 配置
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3 ;//抢占优先级3
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; //子优先级3
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能
NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化VIC寄存器
//USART 初始化设置
USART_InitStructure.USART_BaudRate = bound;//串口波特率
USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长为8位数据格式
USART_InitStructure.USART_StopBits = USART_StopBits_1;//一个停止位
USART_InitStructure.USART_Parity = USART_Parity_No;//无奇偶校验位
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//无硬件数据流控制
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; //收发模式
USART_Init(USART1, &USART_InitStructure); //初始化串口1
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启串口接受中断
USART_Cmd(USART1, ENABLE); //使能串口1
}
void USART1_IRQHandler(void) //串口1中断服务程序
{
u8 Res;
#if SYSTEM_SUPPORT_OS //如果SYSTEM_SUPPORT_OS为真,则需要支持OS.
OSIntEnter();
#endif
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) //接收中断(接收到的数据必须是0x0d 0x0a结尾)
{
Res =USART_ReceiveData(USART1); //读取接收到的数据
if((USART_RX_STA&0x8000)==0)//接收未完成
{
if(USART_RX_STA&0x4000)//接收到了0x0d
{
if(Res!=0x0a)USART_RX_STA=0;//接收错误,重新开始
else USART_RX_STA|=0x8000; //接收完成了
}
else //还没收到0X0D
{
if(Res==0x0d)USART_RX_STA|=0x4000;
else
{
USART_RX_BUF[USART_RX_STA&0X3FFF]=Res ;
USART_RX_STA++;
if(USART_RX_STA>(USART_REC_LEN-1))USART_RX_STA=0;//接收数据错误,重新开始接收
}
}
}
}
#if SYSTEM_SUPPORT_OS //如果SYSTEM_SUPPORT_OS为真,则需要支持OS.
OSIntExit();
#endif
}
#endif
3、初始化定时器2为编码器接口模式
void Encoder_Init_TIM2(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_ICInitTypeDef TIM_ICInitStructure;
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);//使能定时器2的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);//使能PA端口时钟
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_1; //端口配置
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //浮空输入
GPIO_Init(GPIOA, &GPIO_InitStructure); //根据设定参数初始化GPIOB
TIM_TimeBaseStructInit(&TIM_TimeBaseStructure);
TIM_TimeBaseStructure.TIM_Prescaler = 0x0; // 预分频器
TIM_TimeBaseStructure.TIM_Period = ENCODER_TIM_PERIOD-1; //设定计数器自动重装值
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;//选择时钟分频:不分频
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_CenterAligned1;TIM向上计数
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
TIM_EncoderInterfaceConfig(TIM2, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);//使用编码器模式3
TIM_ICStructInit(&TIM_ICInitStructure); //把TIM_ICInitStruct 中的每一个参数按缺省值填入
TIM_ICInitStructure.TIM_ICFilter = 10; //设置滤波器长度
TIM_ICInit(TIM2, &TIM_ICInitStructure);//根据 TIM_ICInitStruct 的参数初始化外设 TIMx
TIM_ClearFlag(TIM2, TIM_FLAG_Update);//清除TIM的更新标志位
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);//使能定时器中断
TIM_SetCounter(TIM2,0);//设置TIMx 计数器寄存器值
TIM_Cmd(TIM2, ENABLE); //使能定时器
}
4、单位时间读取脉冲数
int Read_Encoder(u8 TIMX)//读取计数器的值
{
int Encoder_TIM;
//printf("%d\r\n", TIM2->CNT);
switch(TIMX)
{
case 2:Encoder_TIM=(short)TIM2->CNT; TIM2 -> CNT=0;
break;
case 3:Encoder_TIM=(short)TIM3->CNT; TIM3 -> CNT=0;
break;
case 4:Encoder_TIM=(short)TIM4->CNT; TIM4 -> CNT=0;
break;
default: Encoder_TIM=0;
}
return Encoder_TIM;
}
5、定时器3中断函数(对脉冲计数进行处理)
nt Target_velocity=50; //设定速度控制的目标速度为50个脉冲每10ms
void TIM3_IRQHandler(void)
{
if(TIM_GetFlagStatus(TIM3,TIM_FLAG_Update)==!RESET)
{
TIM_ClearITPendingBit(TIM3,TIM_IT_Update); //===清除定时器1中断标志位
Encoder=Read_Encoder(2); //取定时器2计数器的值
Led_Flash(100); //LED闪烁
moto=Incremental_PI(Encoder,Target_velocity); //===位置PID控制器
Xianfu_Pwm();
Set_Pwm(moto);
}
}
6、PID算法核心代码(其实代码很简单)
float Incremental_PI (int Encoder,int Target)
{
float Kp=75,Ki=10;
static float Bias,Pwm,Last_bias;
Bias=Encoder-Target; //计算偏差
Pwm+=Kp*(Bias-Last_bias)+Ki*Bias; //增量式PI控制器
Last_bias=Bias; //保存上一次偏差
return Pwm; //增量输出
}
7、其他辅助实现PID的函数
void Set_Pwm(int moto)//赋值给PWM寄存器
{
if(moto>0) AIN1=0, AIN2=1;
else AIN1=1, AIN2=0;
PWMA=myabs(moto);
}
void Xianfu_Pwm(void) //限制幅度的函数
{
int Amplitude=7100; //===PWM满幅是7200 限制在7100
if(moto<-Amplitude) moto = -Amplitude;
if(moto>Amplitude) moto = Amplitude;
}
int myabs(int a) //取绝对值
{
int temp;
if(a<0) temp=-a;
else temp=a;
return temp;
}
我们每隔10ms就将脉冲值通过串口打印出来,并使用一款可以将串口数据绘制成图像的软件(SerialPlot)来形象感受PID。
通过图像 我们不难看到响应时间较短,静差几乎消除,稳定性较好。这是一个性能较为优越的PID系统。