粘贴过来的代码都乱了,贴个原文链接吧: https://blog.csdn.net/D_SEngineer?spm=1011.2124.3001.5113 不定时发送福利,欢迎小伙伴们交流学习。 |
终于到实战阶段了,前面学习的理论部分着实令人乏味呀。今天来看看增量式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); }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 编码器输入
不懂的可以仔细看看代码注释,注释也看不懂的,可以翻翻我之前的帖子,代码如下:
//输入捕获 通用定时器2 PA0 PA1void 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
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 按键和屏幕显示
按键很简单,我注释的也很详细,这是放在主函数中循环检测的,这个也不用多说了吧,直接代码:
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
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 增量式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”
获取源码。
 调试过程本来想写在这个帖子上的,不过一看时间有点晚了,明天再开一贴吧,明天还有工作呢,不熬夜,早睡早起。晚安,小伙伴们。
|