)
这一章主要学习小车控制的PID算法。如图是PID的基本概念。因为我的专业是自动化对这些基本概念比较清楚就不对一些很常见的概念做一些叙述了。这幅图是一个烧水壶的例子用来说明PID控制的效果。右上角出现震荡是因为加热具有延时会产生一段超调。小车项目里不是使用传统的调参而是建模分析根据指标计算KPKIKD。第一部分 控制系统的结构和关键指标控制系统使用控制器控制执行器让被控对象的输出跟踪设定值。控制系统的组成上图是系统的关键指标的通俗易懂的解释。v其中对于准的概念上图具体做了一个解释因为水壶本身会散热所以最终的实际温度到不了设定温度造成稳态误差是系统自身的缺陷需要额外加大力度调整才能消除。上图是对控制系统的关键指标的专业术语解释。主要要记住几个量的专业名词方面后面编程、第二部分 电机的电学方程这是控制系统各个部分对应小车项目的现实部分。简易框图如下更简化如下想确定PID的参数首先要确定电机的数学模型。首先复习一下 理想的电机模型的相关参数。其中重要的是反电动势根据感应电动势推倒出的公式因为B和L是定值我们可以近似认为角速度和电源电压成正比。根据电学知识列出上述公式写出微分方程。第三部分 电机的机械方程除了知道电机的电学方程还需要知道电机的机械方程。电机转速主要受到安培力大小转子惯性还有摩擦力的影响。电机转动可以等效成刚体转动和刚体平动相关参数对比如下其中平动已经非常熟悉了刚体转动的扭矩计算方法如下转动惯量的计算如下用一道例题巩固知识点求出Jw就能求出角加速度了。以上是J的计算公式用微积分求出等效模型的转动惯量。以上计算过程是对等效圆环求转动惯量。那么最后就可以求出Jw的值了进而求出角加速度最后求出时间把电路部分和机械部分联动起来得到下面这张图联立等式就可以求出机械的微分方程。这就是两个方程电机的电学方程和机械方程。第四部分 拉普拉斯建模拉普拉斯变换将时域变换成复频域求解问题更加简单。首先对电学方程进行套用规则保持常数不变。接着是机械方程之后就可以通过这两个方程联立求出传递函数了。第五部分 结构图化简首先介绍了传递函数的概念求出输出与输入的拉氏变换之比就是传递函数。对上次的电学微分方程化简如图是电机的结构框图输入是Ua输出是ws。此处忽略Tl为了计算方便再对两个方程联立求解传递函数消去Ias就可以得到Uas和ws的关系。计算过程如图。用计算好的结果代入得到如图的结果。上面是通过方程组的方式得到传递函数另外还有一种画出结构图的方式来获取传递函数。上图两段蓝色的有笔误应该除以跟等式一样的式子。将相同的部分连接起来就行了接下去进行化简。上图的反馈环节有错误分子上面是G1s。第一步忽略TL对化简影响太大了。最终通过结构图化简的方式同样得出了传递函数。第六部分 电机参数测量这部分是测量基本参数的涉及大量公式可以不掌握直接获取参数即可。第七部分 波特图如图波特图是用来表示系统在3个频段的性能求出原始电机的性能加上PID的性能矫正就能得到PID控制好的系统。绘制步骤如下标准型型别看幂次比如2次就是2型。图上的分母括号里代表尾1型表示加的常数是1。例题画波特图的步骤如上。引入自动控制理论里的波特图是一种特殊的坐标图极点画叉零点画圈负号省略。先在w1处确定20logk的数值然后确定穿过的斜率由型别决定。例如在基准线的基础上遇到极点斜率减20遇到零点斜率加20。根据上节课跳过的数据把传递函数表示出来以后绘制电机模型的波特图。补充除了幅频特性曲线还有相频特性曲线。与幅频特性相关相频特性表示将xt经过Gs后得到的yt相位的值。首先看型别v是几次方就滞后几个90°在零点处超前90°形状类似于arctanx的函数在极点处滞后90°形状类似于arctanx的函数。因为没有零点所以画两条极点对应的曲线合成后大概是图上红线的形状。第八部分 PID参数整定这部分包含三段论的知识如下图低频是第一个极点左边的部分中频是曲线穿过x轴附近的区域剩下是高频段这三段分别和一些指标有关系。理想的波特图的状况如上。低频段k越大w1对应的值越大越陡稳态误差越小。中频段尽量以-20db穿过且穿越的范围尽可能大范围越大超调量越小。wc也就是穿过x轴的交点的值也是越大越好振荡越快越容易趋于稳定。高频段尽量低下降越快越好抗噪声能力越好。用以上指标分析原有电机模型低频段斜率为0需要变大成-20或者-40中频段穿过的点最好在频段的中心需要上移高频段斜率大衰减能力强。综上所述需要改进的位置在低频段和中频段。利用PID控制器叠加在原有电机上改进电机的性能如图。在低频段不仅变陡了而且w1的位置上升了。在中频段wc的位置右移了振荡加快且中频段穿越x轴两边的部分尽可能相等了。在高频段因为原本性能不搓所以没什么变动。想画出PID的波特图步骤如下将PID传递函数画出波特图各种不同的波特图如图。根据上面的练习设计电机的PID此处的数据为估算大致让改进后的系统达到要求而已。对于小车来说有3个现实缺陷导致电机控制非线性1.电机控制轮胎存在减速差因为变速齿轮存在间隙。2.编码器测速波形并非理想不光滑。3.启动扭矩很大。综上所述增益K需要减小一些这些数字都是up经过实验得出的。第九部分 PID控制器代码编写大致的PID控制流程在代码的体现如下创建一个结构体用来保存PID参数里面需要长期保存的参数。新建一个pid.c和pid.h的文件在头文件里创建结构体#ifndef PID_H #define PID_H #includestm32f10x.h typedef struct { float Kp;//比例系数 float Ki;//积分项的系数 float Kd;//微分项的系数 float SP;//用户的设定值 }PID_TypeDef; #endif接着创建两个函数用来初始化PID和改变设定值SP// //简介对PID控制器进行初始化 //参数 kp - 比例系数 //参数 ki - 积分项系数 //参数 kd - 微分项系数 void PID_Init(PID_TypeDef *PID,float Kp,float Ki,float Kd) { PID-KpKp; PID-KiKi; PID-KdKd; PID-SP0.0f; PID-t_k_10; PID-err_k_10; PID-err_int_k_10; } // //简介改变设定值SP //参数 SP - 新的设定值 // void PID_ChangeSP(PID_TypeDef *PID,float SP) { PID-SPSP; }创建一个计算参数用于执行一次PID计算里面的参数都需要经过运算得出。将PID的比例积分微分项累积就可以得出结果了。但是积分和微分比较难实现需要借助误差的图像。上图解释了积分和微分的图解法。具体算法如下用三角形的斜率求微分用梯形的面积求积分// //简介执行一次PID运算 //参数 FB - 反馈的值也就是传感器采回的值 //返回值PID控制器计算的结果 // float PID_Compute(PID_TypeDef *PID,float FB) { float errPID-SP - FB; uint64_t t_kGetUs(); float deltaT(t_k - PID-t_k_1)* 1.0e-6f; float err_dev(err - PID-err_k_1) / deltaT; float err_intPID-err_int_k_1 (PID-err_k_1 err)*deltaT * 0.5f; float COperr*PID-Kp; float COierr*err_int; float COderr*err_dev; float COCOp COi COd; //更新 PID-t_k_1t_k; PID-err_int_k_1err_int; PID-err_k_1err; return CO; }注意在执行完一次计算后需要迭代更新下一次计算的值。第十部分 PID测试上图是PID控制的思路其中compute函数上一个部分已经完成编写了。把所有的电机调速代码写在app_motor.h和app_motor.c里面。依照先前的思路先对左右电机进行初始化再反复执行电机调速的进程函数。声明左右两个电机的结构体然后将前面算好的PID的3个参数传进去完成PID控制器左右电机的初始化。static PID_TypeDef pid_motor_L;//左电机调速系统的PID控制器 static PID_TypeDef pid_motor_R;//右电机调速系统的PID控制器 // //简介初始化左右电机的调速系统 // void App_motor_Init(void) { PID_Init(pid_motor_L,0.5,7,0); PID_Init(pid_motor_R,0.5,7,0); }接着编写进程函数// //简介电机调速系统的进程函数 // void App_motor_Proc(void) { PERIODIC(1); //#1.通过编码器获取左右电机旋转的角速度 float omega_LApp_Encoder_Getspeed_L(); float omega_RApp_Encoder_Getspeed_L(); //#2.计算PID控制器的输出 float ua_LPID_Compute(pid_motor_L,omega_L); float ua_RPID_Compute(pid_motor_R,omega_R); //#3.将电压Ua设置到电机两端 //获取电池电压Vbat float Vbat App_Bat_Get(); //计算左右电机的PWM信号的占空比 float duty_Lua_L/Vbat*100.0f; float duty_Rua_R/Vbat*100.0f; App_PWM_Set_L(duty_L); App_PWM_Set_R(duty_R); }其中设置电压的部分之前的学习中是通过设置占空比来间接设置的。上图是占空比和Ua的转换关系。最后需要给用户留一个设置左右电机转速的接口// //简介用来设置左右电机的转速omega_L和omega_R的值 //参数omega - 表示电机的转速单位是rad/s // void App_motor_SetOmega_L(float omega) { PID_ChangeSP(pid_motor_L,omega); } void App_motor_SetOmega_R(float omega) { PID_ChangeSP(pid_motor_R,omega); }写完以后对上面的测试代码进行测试。每隔1s加大电机转速让电机实际转速跟踪设定转速观察跟踪是否紧密。实验思路如下具体测试的初始化和进程函数我已经卸载main,c里面了。下面是呈现阶梯状转速的思路两个整数相除会忽略余数示例中13.5s的时候首先是毫秒单位13500ms转换成13s然后取余数3s乘2结果变成6rad/s。下面是根据上面的思路编写的测试函数#include stm32f10x.h #include bat_test.h #include app_bat.h #include app_button.h #include app_pwm.h #include pwm_test.h #include encoder_test.h #include MPU6050_test.h #include app_encoder.h #include app_motor.h #include delay.h #include app_usart2.h #include task.h static float targetOmega; static void USART2_Proc(void); int main(void) { NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0); MPU6050_EularAngleTest(); // MPU6050_Test(); // Encoder_T_Method_Test(); // Encoder_Test(); // bat_test(); // pwm_test(); App_USART2_Init(); App_Bat_Init(); App_Button_Init(); App_PWM_Init(); App_Encoder_Init(); App_motor_Init(); while(1) { targetOmega(GetTick()/1000)%10*2.0f; App_motor_SetOmega_L(targetOmega); App_motor_SetOmega_R(targetOmega); App_Bat_Proc(); App_Button_Proc(); App_motor_Proc(); USART2_Proc(); } } static void USART2_Proc(void) { PERIODIC(10) float omega_lApp_Encoder_Getspeed_L(); float omega_rApp_Encoder_Getspeed_R(); My_USART_Printf(USART2,%.3f,%.3f,%.3f\n,targetOmega,omega_l,omega_r); }测试代码完成后在PID控制器方面还有缺陷需要改进后再进行测试。第十一部分 PID改进改进的方面主要有以上四个方面输出限幅主要限制CO的值要根据被控对象的现实物理条件进行合理的限制。比如Ua的输出值由于Vbat的限制控制0~100%的占空比范围在-8.4~8.4超过这个范围的都应该被强制拉到最高或者最低值。上图是代码实现的具体思路。先在结构体里定义这两个变量在初始化的时候设置好上下限图上表示无穷大和无穷小。再加入一个设置上下限的接口。在计算PID的函数里再对输出值进行判断进行限幅。具体的代码呈现和上图代码差不多。积分限幅方面如下// //简介执行一次PID运算 //参数 FB - 反馈的值也就是传感器采回的值 //返回值PID控制器计算的结果 // float PID_Compute(PID_TypeDef *PID,float FB) { float errPID-SP - FB; uint64_t t_kGetUs(); float deltaT(t_k - PID-t_k_1)* 1.0e-6f; float err_dev(err - PID-err_k_1) / deltaT; float err_intPID-err_int_k_1 (PID-err_k_1 err)*deltaT * 0.5f; float COperr*PID-Kp; float COierr*err_int; float COderr*err_dev; float COCOp COi COd; //更新 PID-t_k_1t_k; PID-err_int_k_1err_int; PID-err_k_1err; //输出限幅 if(COPID-upperlimit) CO PID-upperlimit; if(COPID-lowerlimit) CO PID-lowerlimit; //积分限幅 if(PID-err_int_k_1PID-upperlimit) PID-err_int_k_1PID-upperlimit; if(PID-err_int_k_1PID-lowerlimit) PID-err_int_k_1PID-lowerlimit; return CO; }在compute函数里加入输出限幅和积分限幅。PID复位就是通过小车上的按钮控制电机的开关时需要在小车重启的时候把相关参数复位避免残留数据对新的运行产生影响。主要是这些临时存储的数据需要清零。// //简介对PID控制器进行复位 // void PID_RESET(PID_TypeDef *PID) { PID-err_int_k_10.0f; PID-err_k_10.0f; PID-t_k_10; }编写一个复位函数// //简介开关电机 //参数: on - 控制电机的开关0 - 关闭 非零 - 开启 // void App_motor_Cmd(uint8_t on) { App_PWM_Cmd(on); //在开关电机的同时需要对PID控制器进行复位 PID_RESET(pid_motor_L); PID_RESET(pid_motor_R); }并且在按下按钮的时候调用复位函数回到之前编写的按钮相关函数里面static void OnUserKey_Clicked(uint8_t clicks) { if(clicks1) { //翻转电机状态 if(pwm_on0) { pwm_on1; } else { pwm_on0; } App_motor_Cmd(pwm_on); } }根据状态量控制电机的状态。这里之前写的是App_pwm_Cmd(pwm_on)现在把这个嵌入在motor的Cmd里面了。最后的改进首次运行时忽略积分和微分。第一次运行的时候只有积分项没有微分项和积分项所以要忽略。解决的办法是判断一下三个临时参数是不是0就知道是不是第一次运行了。uint64_t t_k_1;//t[k-1],上次运行PID的时间 float err_k_1;//err[k-1],上次运行PID时的误差 float err_int_k_1;//err_int[k-1],上次运行PID时的积分值就是如图的这三项float err_dev0.0f; float err_int0.0f; if(PID-t_k_1!0) { err_dev(err - PID-err_k_1) / deltaT; err_intPID-err_int_k_1 (PID-err_k_1 err)*deltaT * 0.5f; }首先先令积分项和微分项为0然后判断一下是否为0第一次为0就不进行运算了。接着测试一下效果。测试结果如下PID控制器能够正常跟踪。