停更一个月了,这一个月有点忙,没顾得上调试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输出引脚和编码器输入引脚就可以接线了,其他都是固定接线方式。下图是我的硬件图,很乱,小车还没组装好,都是散件,凑合看吧。
代码实现的功能
- 按键设定目标转速
- stm32通过pwm控制电机维持目标转速运转
- 并且具备显示功能,把重要参数显示在屏幕上
代码思路
- 要先把PWM调通,实现PWM控制电机旋转
- 在实现PWM控制电机旋转之后,加上按键调节占空比
- 将编码器检测加进去,能实现检测到电机反馈的脉冲数
- 将编码器检测的脉冲数显示出来,或者串口打印出来,总之要实时知道这个值,方便后面调试PID参数
- 之后再将PID加进去
- 整理代码
部分重要源码讲解
- 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);
}
- 编码器输入
不懂的可以仔细看看代码注释,注释也看不懂的,可以翻翻我之前的帖子,代码如下:
//输入捕获 通用定时器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);
}
- 按键和屏幕显示
按键很简单,我注释的也很详细,这是放在主函数中循环检测的,这个也不用多说了吧,直接代码:
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;
}
- 增量式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”
获取源码。
调试过程
本来想写在这个帖子上的,不过一看时间有点晚了,明天再开一贴吧,明天还有工作呢,不熬夜,早睡早起。晚安,小伙伴们。