说明:如果有哪里说错了或者说得不好的话还请大家指出来,及时纠正错误,或者哪里有更好的解决方法也可以提出来,我们一起学习交流。
目录
一、编码电机
如果想要控制小车的速度就需要得到小车的速度,想要得到小车的速度就需要用到编码电机,下面先来大概看一下编码电机。
大概就是这个样子了,这里就不介绍它的原理了,可以去查阅相关资料。
二、单片机相关定时器的作用以及配置
本次用到了3个定时器,分别是TIM2、TIM3、TIM4,定时器2用于定时,也就是定时10ms产生中断,然后到中断服务函数中去计算速度,定时器3用于输出PWM波来控制小车的行走,定时器4用来配置成编码器模式,用来读取编码器的数据。
1、TIM2的配置
#include "encoderTim.h"
/*********************
通用定时器6初始化
arr:自动重装值。
psc:时钟预分频数
定时器溢出时间计算方法:Tout=((arr+1)*(psc+1))/Ft us.
Ft=定时器工作频率,单位:Mhz
************************/
void TIM2_Int_Init(u16 arr,u16 psc)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE); ///使能TIM时钟
TIM_TimeBaseInitStructure.TIM_Period = arr; //自动重装载值
TIM_TimeBaseInitStructure.TIM_Prescaler=psc; //定时器分频
TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up; //向上计数模式
TIM_TimeBaseInitStructure.TIM_ClockDivision=TIM_CKD_DIV1;
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStructure);//初始化TIM7
TIM_ITConfig(TIM2,TIM_IT_Update,ENABLE); //允许定时器6更新中断 定时器溢出中断
TIM_Cmd(TIM2,ENABLE); //使能定时器6 开始工作
NVIC_InitStructure.NVIC_IRQChannel=TIM2_IRQn; //定时器6中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=0x01; //抢占优先级1
NVIC_InitStructure.NVIC_IRQChannelSubPriority=0x03; //子优先级3
NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE; //中断使能
NVIC_Init(&NVIC_InitStructure);
}
TIM2中断服务函数
void TIM2_IRQHandler(void) //
{
if(TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET){ //检测是否发送中断
calc_motor_rotate_speed();
}
TIM_ClearITPendingBit(TIM2, TIM_IT_Update); //清除中断
}
TIM2用于定时10ms,然后进入中断服务函数,执行calc_motor_rotate_speed()函数,对于这个函数的作用,后面再说明。
2、TIM3的配置
#include "iopwm.h"
void TIM3_PWM_Init(u16 arr,u16 psc)
{
GPIO_InitTypeDef GPIO_InitStruct;
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;
TIM_OCInitTypeDef TIM_OCInitStruct;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //使能GPIOA外设
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE); //使能定时器3时钟
GPIO_InitStruct.GPIO_Mode=GPIO_Mode_AF_PP; //输出
GPIO_InitStruct.GPIO_Pin=GPIO_Pin_7|GPIO_Pin_6; // GPIOA.7;GPIOA.6
GPIO_InitStruct.GPIO_Speed=GPIO_Speed_50MHz; //TIM3_CH2 TIM3_CH1
GPIO_Init(GPIOA,&GPIO_InitStruct);//初始化GPIO
//定时器TIM3初始化
TIM_TimeBaseInitStruct.TIM_Period=arr;//设置在下一个更新事件装入活动的自动重装载寄存器周期的值
TIM_TimeBaseInitStruct.TIM_Prescaler=psc;//设置用来作为TIMx时钟频率除数的预分频值
TIM_TimeBaseInitStruct.TIM_ClockDivision=0;//设置时钟分割:TDTS = Tck_tim
TIM_TimeBaseInitStruct.TIM_CounterMode=TIM_CounterMode_Up;//TIM向上计数模式
TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStruct);//根据TIM_TimeBaseInitStruct中指定的参数初始化TIMx的时间基数单位
//初始化TIM3 Channel 2 PWM模式
TIM_OCInitStruct.TIM_OCMode=TIM_OCMode_PWM2;//选择定时器模式:TIM脉冲宽度调制模式2
TIM_OCInitStruct.TIM_OutputState=TIM_OutputState_Enable;//比较输出使能
TIM_OCInitStruct.TIM_OCPolarity=TIM_OCPolarity_Low;//输出极性:TIM输出比较极性
TIM_OC2Init(TIM3,&TIM_OCInitStruct);//根据T指定的参数初始化外设TIM3 OC2
TIM_OC2PreloadConfig(TIM3,TIM_OCPreload_Enable);使能TIM3在CCR2上的预装载寄存器
TIM_OC1Init(TIM3,&TIM_OCInitStruct);//根据T指定的参数初始化外设TIM3 OC1
TIM_OC1PreloadConfig(TIM3,TIM_OCPreload_Enable);使能TIM3在CCR1上的预装载寄存器
TIM_Cmd(TIM3,ENABLE);//使能定时器
}
TIM3主要就是输出PWM信号来控制电机
3、TIM4的配置
#include "encoder.h"
//TIM4 通道1通道2 正交编码器
void TIM4_ENCODER_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseStruct;
TIM_ICInitTypeDef TIM_ICInitStruct; //输入捕获
//PB6 ch1 A,PB7 ch2
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE);//使能TIM4时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);//使能GPIOA时钟
GPIO_StructInit(&GPIO_InitStructure);//将GPIO_InitStruct中的参数按缺省值输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//PA6 PA7浮空输入
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
/*时基初始化*/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE); /*使能定时器时钟 APB1*/
TIM_DeInit(TIM4);
//TIM_TimeBaseStructInit(&TIM_TimeBaseStruct);
TIM_TimeBaseStruct.TIM_Prescaler = ENCODER_TIM_PSC; /*预分频 0*/
TIM_TimeBaseStruct.TIM_Period = ENCODER_TIM_PERIOD; /*周期(重装载值)65535*/
TIM_TimeBaseStruct.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseStruct.TIM_CounterMode = TIM_CounterMode_Up; /*连续向上计数模式*/
TIM_TimeBaseInit(TIM4, &TIM_TimeBaseStruct);
/*编码器模式配置:同时捕获通道1与通道2(即4倍频),极性均为Rising*/
TIM_EncoderInterfaceConfig(TIM4, TIM_EncoderMode_TI12,TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);
TIM_ICStructInit(&TIM_ICInitStruct);
TIM_ICInitStruct.TIM_ICFilter = 0; /*输入通道的滤波参数*/
TIM_ICInit(TIM4, &TIM_ICInitStruct); /*输入通道初始化*/
TIM_SetCounter(TIM4, CNT_INIT); /*CNT设初值 0 */
TIM_ClearFlag(TIM4,TIM_IT_Update); /*中断标志清0*/
TIM_ITConfig(TIM4, TIM_IT_Update, ENABLE); /*中断使能*/
TIM_Cmd(TIM4,ENABLE); /*使能CR寄存器*/
}
//定时器4中断服务函数
void TIM4_IRQHandler(void)
{
if(TIM4->SR&0X0001){}//溢出中断
TIM4->SR&=~(1<<0);//清除中断标志位
}
TIM4主要就是配置成编码器模式,用来计数编码器脉冲个数
4、PID函数
#include "PIDEncoder.h"
#define P_DATA 1.5 //P参数
#define I_DATA 0.1 //I参数
#define D_DATA -0.4 //D参数
typedef struct
{
double Proportion; //比例常数 Proportional Const
double Integral; //积分常数 Integral Const
double Derivative; //微分常数 Derivative Const
int LastError; //Error[-1]
int PrevError; //Error[-2]
}PID;
static PID sPID;
static PID *sptr = &sPID;
void PID_Init(void)
{
sptr->LastError=0; //Error[-1]
sptr->PrevError=0; //Error[-2]
sptr->Proportion=P_DATA; //比例常数
sptr->Integral=I_DATA; //积分常数
sptr->Derivative=D_DATA; //微分常数
}
/**************
入口参数:NextPoint:当前输出值 SetPoint: 设定值
返回值:PID调整输出
***************/
int PID_Calc(int NextPoint,int SetPoint)
{
int iError,Outpid; //当前误差
iError=SetPoint-NextPoint; //增量计算
Outpid=(sptr->Proportion * iError) //E[k]项
-(sptr->Integral * sptr->LastError) //E[k-1]项
+(sptr->Derivative * sptr->PrevError); //E[k-2]项
sptr->PrevError=sptr->LastError; //存储误差,用于下次计算
sptr->LastError=iError;
return Outpid; //返回增量值
}
PID函数就是用来不断调整的,使当前值逐渐接近目标值
5、读定时器的计数值
// 读取定时器计数值
int read_encoder(void)
{
int encoderNum = 0;
encoderNum = (int)((int16_t)(TIM4->CNT)); /*CNT为uint32, 转为int16*/
TIM_SetCounter(TIM4, CNT_INIT);/*CNT设初值*/
return encoderNum;
}
6、calc_motor_rotate_speed()函数
calc_motor_rotate_speed()函数在TIM2的中断服务函数里面,10ms时间到了就执行这个函数
//计算电机转速(被另一个定时器100ms调用1次)
void calc_motor_rotate_speed()
{
/*读取编码器的值,正负代表旋转方向*/
encoderNum = read_encoder();
/* 转速(1秒钟转多少圈)=单位时间内的计数值/总分辨率*时间系数 */
// rotateSpeed = (int)encoderNum/TOTAL_RESOLUTION*10;
// Speed2 = (rotateSpeed * 6.0 * 3.14)/1.0; //速度 1秒钟转的圈数 X 一圈的距离(轮子周长)/1s
Speed = ((encoderNum/(48.4*4)) * 18.84)/0.1;//((总的脉冲数/电机一圈的脉冲*减数比*4倍频)*轮子周长)/10ms
para_L = PID_Calc(Speed,SetPoint); //,计数得到增量式PID的增量数值
if((para_L < -3) || (para_L > 3)){ // 不做 PID 调整,避免误差较小时频繁调节引起震荡。
Moto_Left += para_L;
}
if(Moto_Left>19000)Moto_Left=19000;//限幅
if(Moto_Left<-19000)Moto_Left=-19000;//限幅
TIM_SetCompare1(TIM3, Moto_Left);
TIM_SetCompare2(TIM3, 0);
OLED_ShowString(0,1,"hope");
OLED_ShowNum(32,1,SetPoint,8,16);
OLED_ShowString(97,1,"cm/s");
OLED_ShowString(0,4,"now:");
OLED_ShowNum(32,4,Speed,8,16);
OLED_ShowString(97,4,"cm/s");
}
函数中第一步获取编码器的值,第二步通过轮子的周长,减数比等一些参数计算出小车当前的实际速度,然后将计算出的实际速度和目标速度作为PID_Calc(Speed,SetPoint)函数的参数进行PID调整,然后得到相应的调整值,再将限幅后的值加载到电机上面,这样就进行了一次调整,然后在OLED屏幕上面进行显示目标速度和实际速度,可以看出调整的过程。
三、2020电赛C题
先来看一下2020年TI被电子设计大赛的C题
这里要求了小车到达终点的时间,而且木板的角度还要变化,可以发现小车行驶的路程始终是没有改变的,所以这里就可以使用编码电机得到小车实际的速度,然后还知道小车的路程,在规定的时间内到达终点,这样就可以通过速度 = 路程 /速度来设置相应的目标速度了,不管木板的角度如何变化,小车都会自己调整速度,在规定的时间内到达终点,从而实现了小车速度的闭环控制。这里还可以加一个陀螺仪进行精确的调整。
当然了不用编码电机也可以实现,把每一个角度的对应要输出多少PWM写死就好了,但是这样会很麻烦。
四、扩展
这个例程是利用编码器输出的脉冲计算得到电机的速度,设置一个目标速度,这样通过PID的调整就可以实现实际速度逐渐接近目标速度,从而实现速度的控制,
有了这个思想就可以扩展到其他方面了,比如对温度的控制,有些对温度要求很高的场所,温度要一直恒温,不能太高或者太低,如果是人来进行调整的话那就有点麻烦了,这里也可以通过这个方法来实现,只需要将速度改为温度即可,即得到当前的实际温度就可以了。当然还有控制水位高度那些都是类似的了。
这样是不是就实现了一个简单的恒温控制系统,或者水位控制系统。
五、效果展示
视频中的PID参数还没有调好,只能说大概是这个现象,作为参考。
STM32单片机实现对小车速度的控制
调得不好,调得不好,调得不好!!!