零基础制作平衡小车【连载】9---增量式PID实战(附源码)

停更一个月了,这一个月有点忙,没顾得上调试PID和写帖子,其实这篇文章一个月前就写的差不多了,一直没整理好,今天抽个空整理一下代码和帖子。好了,废话不多说,看帖。

终于到实战阶段了,前面学习的理论部分着实令人乏味呀。今天来看看增量式PID怎么在STM32中实现控制电机恒速运行的。

硬件连接

我用的战舰开发板和配套的屏幕,电机是带编码器的电机,第一章有关于硬件的介绍,这里就不多少了。
1.使用通用定时器3 通道1 PA6引脚输出PWM
2.编码器输入捕获 用的是 通用定时器2 PA0 PA1引脚
3.电机引脚如下图
在这里插入图片描述
电机电源正负极接到TB6612驱动的AO1和AO2(或者BO1和BO2,看你驱动器左侧接的是AIN1、AIN2还是BIN1、BIN2了),编码器电源接5V电源,黄绿信号线接单片机PA0和PA1,如果接反系统会出现异常,可以仿真看下接收到的脉冲值是不是很大(本例中的count),很大说明接反了。将线反接即可。另外就是驱动的PWMA(或者PWMB)接到单片机的PA6引脚。驱动器的STBY接到5V电源上,BIN1和BIN2一个接高一个接低即可。
接线很简单,不会的直接问淘宝卖家。其实看了代码只要知道PWM输出引脚和编码器输入引脚就可以接线了,其他都是固定接线方式。下图是我的硬件图,很乱,小车还没组装好,都是散件,凑合看吧。

在这里插入图片描述

代码实现的功能

  1. 按键设定目标转速
  2. stm32通过pwm控制电机维持目标转速运转
  3. 并且具备显示功能,把重要参数显示在屏幕上

代码思路

  1. 要先把PWM调通,实现PWM控制电机旋转
  2. 在实现PWM控制电机旋转之后,加上按键调节占空比
  3. 将编码器检测加进去,能实现检测到电机反馈的脉冲数
  4. 将编码器检测的脉冲数显示出来,或者串口打印出来,总之要实时知道这个值,方便后面调试PID参数
  5. 之后再将PID加进去
  6. 整理代码

部分重要源码讲解

  1. PWM
    PWM的生成之前章节已经讲解过了,在这里就不复述了,直接上代码。在这里需要注意一点就是之前PWM设置的是1Khz,这里我们设置成20Khz。大家可以看看注释,理解一下怎么设置成不同的频率。
/*
使用通用定时器3  通道1  PA6引脚输出PWM

------------ PWM信号 周期和占空比的计算 ---------------
 ARR :自动重装载寄存器的值
 CLK_cnt:计数器的时钟,等于 Fck_int / (psc+1) = 72M/(psc+1)
 PWM 信号的周期 T = ARR * (1/CLK_cnt) = ARR*(PSC+1) / 72M
 占空比P=CCR/(ARR+1)
 

时钟分频71,系统总线时钟72M,可以算出定时器时钟为72/(3+1)=18M
在这里我们需要设定PWM频率为20Khz=0.05ms,因此自动重装载的值ARR=900.因为定时器时钟为18M=0.0556us,
因此只需要累加900次,就等于0.05ms=20Khz了。

因此我们配置如下
TIM_TimeBaseInitTypeStructure.TIM_Prescaler = 3;                   //定时器时钟分频
TIM_TimeBaseInitTypeStructure.TIM_Period = (900 - 1);                //自动重装载的值 

*/
void PWM_Init(void)
{
    //基本定时器初始化部分
    
    GPIO_InitTypeDef GPIO_InitStructure;
    TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
    TIM_OCInitTypeDef TIM_OCInitStructure;
    //初始化定时器
    TIM_DeInit(TIM3);
    
    //GPIO初始化部分
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO,ENABLE);
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA,&GPIO_InitStructure);
    
    //使能定时器时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);
    
    //死区时间时钟源,该例程只进行PWM输出,没有用到死区,因此不设定
    TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
    //向上计数模式
    TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
    //自动重装载的值设定为1000
    TIM_TimeBaseInitStructure.TIM_Period = (900 - 1);
    //定时器周期分频数
    TIM_TimeBaseInitStructure.TIM_Prescaler = 3;

    TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
    
    //初始化定时器3
    TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStructure);
    
    
    //PWM初始化部分
    //pwm1模式
    TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
    //初始化电平
    TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
    //使能输出比较
    TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
    //初始化PWM占空比为0
    TIM_OCInitStructure.TIM_Pulse = 0;
    //初始化pwm
    TIM_OC1Init(TIM3,&TIM_OCInitStructure);
    
    //使能TIM3在CCR2上的预装载寄存器
    TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable); 
    //使能定时器3
    TIM_Cmd(TIM3,ENABLE);    
}
  1. 编码器输入
    不懂的可以仔细看看代码注释,注释也看不懂的,可以翻翻我之前的帖子,代码如下:
//输入捕获  通用定时器2  PA0  PA1
void Advance_TIM_Init(void)
{
    //GPIO初始化
    
    GPIO_InitTypeDef GPIO_InitStructure;
    TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
    TIM_ICInitTypeDef  TIM_ICInitStructure;
    NVIC_InitTypeDef NVIC_InitStructure; 
    	
		// 设置中断来源
    NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; 	
		// 设置抢占优先级  		
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;	 
	  // 设置子优先级
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;	
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);
    
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
    
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    
    GPIO_Init(GPIOA,&GPIO_InitStructure);
    
    
    //基本定时器初始化部分
    //使能定时器时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
    TIM_TimeBaseStructInit(&TIM_TimeBaseStructure);//设置缺省值
    // 自动重装载寄存器的值,累计TIM_Period+1个频率后产生一个更新或者中断
	TIM_TimeBaseStructure.TIM_Period = 60000;	
	// 驱动CNT计数器的时钟 = Fck_int/(psc+1)
	TIM_TimeBaseStructure.TIM_Prescaler = 0;	
	// 时钟分频因子 ,配置死区时间时需要用到
	TIM_TimeBaseStructure.TIM_ClockDivision=TIM_CKD_DIV1;		
	// 计数器计数模式,设置为向上计数
	TIM_TimeBaseStructure.TIM_CounterMode=TIM_CounterMode_Up;		
	// 重复计数器的值,没用到不用管
	TIM_TimeBaseStructure.TIM_RepetitionCounter=0;	
	// 初始化定时器
	TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
    
    //编码器模式
    TIM_EncoderInterfaceConfig(TIM2,TIM_EncoderMode_TI12,TIM_ICPolarity_Rising,TIM_ICPolarity_Rising);
	
    TIM_ICStructInit(&TIM_ICInitStructure);
    TIM_ICInitStructure.TIM_ICFilter = 10;		//10经验值,不必纠结,起滤波作用
    TIM_ICInit(TIM2, &TIM_ICInitStructure);
    
    
    //使能中断
    TIM_ITConfig(TIM2,TIM_IT_Update, ENABLE);	
    
    // 清除中断标志位
	TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
    
    // 使能高级控制定时器,计数器开始计数
    TIM_Cmd(TIM2, ENABLE);
}
  1. 按键和屏幕显示
    按键很简单,我注释的也很详细,这是放在主函数中循环检测的,这个也不用多说了吧,直接代码:

 if(GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_4) == 0) 
 {
     SetPoint=160;           //设定目标
     z=1;
 }
 if(GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_2) == 0)
 {
     SetPoint=120;           //设定目标
     z=2;
 }
 
 if(GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_3) == 0)
 {
     SetPoint=20;           //设定目标
     z=3;
 }
 

 if(z != j)
 {
     LCD_Clear(WHITE);  
     //delay_ms(120);
 }
 j = z ;
 
 LCD_Fill(0,SetPoint,lcddev.width,SetPoint+2,BLUE);	//目标值设定,一条直线
  	//如果FLAG=1时,说明200ms到了,将采样的count值打印在屏幕上, 	//k是x轴坐标,200MS+1,conunt是y轴坐标,代表当前时刻的采样的脉冲值
 if(flag == 1)   
 {
     k++;
     LCD_Fill(k,count,k+2,count+2,BLUE);
     flag = 0;
 }

  1. 增量式PID
    重点来了,学了这么多理论,你会发现,其实代码就一点点,两三行就完事了。先看代码:
/********************增量式PID控制设计************************************/
int IncPIDCalc(int PresentPoint) 
{
    int iError , iIncpid;       //iIncpid增量值   iError当前误差
    iError = SetPoint-PresentPoint;                    //增量计算
    iIncpid =(Proportion * iError)                  //E[k]项
              -(Integral * LastError)     //E[k-1]项
              +(Derivative * PrevError);  //E[k-2]项
    PrevError = LastError;
    LastError=iError;
    return (iIncpid);
}

是不是很简单,就这么几行代码,但是我却写了好几篇帖子来学习它的理论部分。过来之后就能看清庐山真面目了。

根据前面推导的公式,这里只是将这些公式用C写出来。首先定义一个当前误差iError和一个计算出来的增量值iIncpid。当前误差iError=目标值SetPoint-传感器采集的当前值PresentPoint,之后进行增量式PID运算,得出增量值iIncpid。
在这里插入图片描述
将式子和程序对比下,是不是一样的?再看看还是不一样。上面式子中没有减号,代码中却有;上面式子中明明微分项是𝐄𝐤−𝟐𝐄(𝐤−𝟏)+𝐄(𝐤−𝟐),即便将Kp*Td/T看成Kd也还是和程序不一样呀。程序中确直接乘上一个上上次误差PrevError,你说一样吗?你心中有没有这样的疑问?有的话也不奇怪,我朋友看了也有(斜眼笑)。

解惑
在这里插入图片描述
看完上面的推导,应该能解决你心中的疑惑了吧。

源码

昨天开通了一个公众号,以后代码就放到公众号了,后续我会将之前的代码也都放到公众号上,欢迎小伙伴们交流学习哈。关注公众号回复
“增量式PID”
获取源码。
在这里插入图片描述

调试过程

本来想写在这个帖子上的,不过一看时间有点晚了,明天再开一贴吧,明天还有工作呢,不熬夜,早睡早起。晚安,小伙伴们。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值