01 前言
如果我们想对电机进行速度或者转角的精确控制,需要使用到很多算法,比如非常经典的PID控制算法,或者一些只能算法,但这些算法都需要传感器来提供转速或转角的反馈值,对于电机来说,编码器是非常流行并且实用的电机配套传感器,本文使用STM32F103C8T6+L298N+MG513P30电机进行直流电机的编码器测速。
02 编码器原理
(资料图片仅供参考)
1.分类
光电式编码器的精准度比霍尔式要高,但是由于它需要红外线发生器和接收器,相对来说造价要贵一些。现在我们比较常用的是霍尔式增量编码器,有很多电机都会自带编码器。
2.测速方法分类
(1)M法测速
编码器输出的脉冲个数代表了位置,那么单位时间里的脉冲个数表示这段时间里的平均速度。因此,我们可以通过计量单位时间脉冲个数即可以估算出平均速度,称为M法测速(测脉冲个数)测速原理如图所示。
例如,若编码器每转产生N个脉冲,在T时间(单位s)产生m个脉冲,那么平均转速如下式所示:
式中 n——平均转速(r/min);
T——测速采样时间(s);
m——T时间内测得的编码器脉冲个数;
N——编码器每转脉冲数。
(2)T法测速
若用M法测速,在记录时间短、速度低的时候,只能记录几个脉冲,则分辨率降低。针对该问题,目前解决方法为:可以采用输出码盘脉冲为一个时间间隔,然后用计数器记录在这段时间里高速脉冲源发出的脉冲数。即通过采集到脉冲源脉冲数来计量编码器两个脉冲时间间隔,从而估算出速度,称为T法测速(测脉冲周期),测速原理图如图所示。
T法测速,利用编码器产生的脉冲用作门电路的触发信号;用已知频率f的时钟信号做输入。若控制门电路在编码器脉冲上升沿到来时开始导通,再次上升沿到来时关闭,即计数器只记录一个编码器脉冲周期内的时钟脉冲个数。若在编码器相邻脉冲之间记录的脉冲时钟个数为m,那么,可以计算两个编码器脉冲的时间间隔为m /f;若编码器每转有N个线脉冲输出,那么我们就知道编码器转过1/N转时需要时间m /f。据此,可计算与编码器同轴转速为公式所示。
式中 n——平均转速(r/min);
f——时钟脉冲频率(个/s);
m——两个编码器脉冲之间的时钟脉冲个数;
N——编码器每转脉冲数。
编码器一般会输出两路信号,分别称为A相和B相,它们相差90°,因此编码器也称为十字码盘,通过捕获两路输出信号可以测算出电机的转速和转向。
STM32使用编码器的方法有两种分别是外部中断法和输入捕获法,这两种方法都属于M法测速,两种方法比较来说外部中断法占用CPU资源较多,平时比较常用的是输入捕获法,但博主两种方法都调试出来了,因此记录下来跟大家分享一下。
03 外部中断法测速
对于外部中断的知识,各个讲STM32的教程都有,我就不过多赘述,外部中断的初始化都一样,主要是出发外部中断时需要进行的操作。
看这张图,正转方向是信号向右走,因为我们是同时捕获两路信号,有以下几种情况:
然后我们设置两个变量用来存储捕获的脉冲数,每捕获到一次脉冲信号根据上表进行判断,正转时使其加1;反转时使其减1;然后配置一个定时器,每隔一段时间反馈一次测速值。
1.外部中断配置
先编写一个函数初始化外部中断,使用PB12-15引脚复用为外部中断输入,外部中断配置步骤如下:
1.端口初始化:RCC_APB2PeriphClockCmd()、GPIO_Init()
2.使能复用功能时钟:RCC_APB2PeriphClockCmd()
3.设置IO口与中断线的映射关系:GPIO_EXTILineConfig()
4.初始化线上中断:EXTI_Init()
5.配置中断分组:NVIC_Init()
完整函数如下:
/**************************************************************************功能:应用外部中断方式采集编码器数据,使用M法测速反馈车轮实时速度函数:Encoder_EXTIX_Init(void) EXTI15_10_IRQHandler(void)作者:K.Fire日期:2022.01.30引脚:PB12(左轮A相) PB13(左轮B相) PB14(右轮A相) PB15(右轮B相)参数:void*************************************************************************/int Encoder_L_EXTI=0;int Encoder_R_EXTI=0;void Encoder_EXTIX_Init(void){ //1.端口初始化 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPD; GPIO_InitStruct.GPIO_Pin = GPIO_Pin_12 | GPIO_Pin_13 |GPIO_Pin_14 | GPIO_Pin_15; GPIO_Init(GPIOB,&GPIO_InitStruct); //2.使能复用功能时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE); //3.设置IO口与中断线的映射关系 GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource12); GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource13); GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource14); GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource15); //4.初始化线上中断 EXTI_InitTypeDef EXTI_InitStruct; EXTI_InitStruct.EXTI_Line = EXTI_Line12 | EXTI_Line13 | EXTI_Line14 | EXTI_Line15; EXTI_InitStruct.EXTI_LineCmd = ENABLE; EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt; EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Rising_Falling;//跳变沿触发 EXTI_Init(&EXTI_InitStruct); //5.配置中断分组 NVIC_InitTypeDef NVIC_InitStruct; NVIC_InitStruct.NVIC_IRQChannel = EXTI15_10_IRQn; NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE; NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0; NVIC_Init(&NVIC_InitStruct);}
2.外部中断服务函数
函数的判断逻辑与上表一致,外部中断捕获判断函数如下:
void EXTI15_10_IRQHandler(void){ if(EXTI_GetITStatus(EXTI_Line12) != RESET)//左轮A相 PB12 { EXTI_ClearITPendingBit(EXTI_Line12); //清除LINE上的中断标志位 if(PBin(12)==0) //这里判断检测到的是否是下降沿 { if(PBin(13)==0) Encoder_L_EXTI++;//B相的电平如果是低,电机就是正转加1 else Encoder_L_EXTI--;//否则就是反转减1 } else //上升沿 { if(PBin(13)==1) Encoder_L_EXTI++; //B相电平如果为高,电机就是正转加1 else Encoder_L_EXTI--;//否则就是反转减1 } } if(EXTI_GetITStatus(EXTI_Line13) != RESET)//左轮B相 PB13 { EXTI_ClearITPendingBit(EXTI_Line13); //清除LINE上的中断标志位 if(PBin(13)==0) //这里判断检测到的是否是下降沿 { if(PBin(12)==1) Encoder_L_EXTI++;//B相的电平如果是高,电机就是正转加1 else Encoder_L_EXTI--;//否则就是反转减1 } else //上升沿 { if(PBin(12)==0) Encoder_L_EXTI++; //B相电平如果为高,电机就是正转加1 else Encoder_L_EXTI--;//否则就是反转减1 } } if(EXTI_GetITStatus(EXTI_Line14) != RESET)//右轮A相 PB14 { EXTI_ClearITPendingBit(EXTI_Line14); //清除LINE上的中断标志位 if(PBin(14)==0) //这里判断检测到的是否是下降沿 { if(PBin(15)==0) Encoder_R_EXTI++;//B相的电平如果是低,电机就是正转加1 else Encoder_R_EXTI--;//否则就是反转减1 } else //上升沿 { if(PBin(15)==1) Encoder_R_EXTI++; //B相电平如果为高,电机就是正转加1 else Encoder_R_EXTI--;//否则就是反转减1 } } if(EXTI_GetITStatus(EXTI_Line15) != RESET)//右轮B相 PB15 { EXTI_ClearITPendingBit(EXTI_Line15); //清除LINE上的中断标志位 if(PBin(15)==0) //这里判断检测到的是否是下降沿 { if(PBin(14)==1) Encoder_R_EXTI++;//A相的电平如果是高,电机就是正转加1 else Encoder_R_EXTI--;//否则就是反转减1 } else //上升沿 { if(PBin(14)==0) Encoder_R_EXTI++; //A相电平如果为低,电机就是正转加1 else Encoder_R_EXTI--;//否则就是反转减1 } }}
注意一下,在使用引脚时不要重复,因为Px0(x=AB..F)都是使用的同一条外部中断线(EXIT0)
3.读取捕获脉冲数值
配置完定时器后,每过一段时间调用读取函数,通过公式可以计算出实际的脉冲值。
函数如下:
/**************************************************************************功能:获取不同方式下的脉冲值函数:int Read_Encoder(u8 TIMX)作者:K.Fire日期:2022.01.30参数:1:外部中断法左轮 2:外部中断法右轮 3:输入捕获法左轮 4:输入捕获法右轮**************************************************************************/int Read_Encoder(u8 TIMX){ int Encoder_TIM; switch(TIMX) { case 1:Encoder_TIM=Encoder_L_EXTI; Encoder_L_EXTI=0; break; case 2:Encoder_TIM=Encoder_R_EXTI; Encoder_R_EXTI=0; break; case 3:Encoder_TIM=TIM3 - > CNT; TIM3 - > CNT=0;break; case 4:Encoder_TIM=TIM4 - > CNT; TIM4 - > CNT=0;break; default:Encoder_TIM=0;break; } return Encoder_TIM;}/**************************************************************************功能:获取并打印输出实际速度值函数:void Get_SpeedNow(float* CurrentVelcity_L,float* CurrentVelcity_R)作者:K.Fire日期:2022.01.30参数:CurrentVelcity_L:左轮实时速度(地址) CurrentVelcity_R:右轮实时速度(地址)**************************************************************************/extern int Current_LN,Current_RN;//速度脉冲数void Get_SpeedNow(int* CurrentVelcity_L,int* CurrentVelcity_R){ /*外部中断方式*/ Current_LN = Read_Encoder(1); Current_RN = Read_Encoder(2); *CurrentVelcity_L = Current_LN * MPN * 100;//计算实际速度值 *CurrentVelcity_R = Current_RN * MPN * 100; printf("The Current Left Wheel Velocity is: %d mm/s.\\r\\n",*CurrentVelcity_L); printf("The Current Right Wheel Velocity is: %d mm/s.\\r\\n",*CurrentVelcity_R);}
4.测试结果
通过USART1串口向电脑输出实时速度值,结果如下:
04 输入捕获发测速
输入捕获是将TIM定时器的CHx通道配置为输入捕获模式,每捕获到一个信号会将响应定时器的CNT计数器的值加/减1,然后每隔一段时间提取并清空计数器的值就可以测算出电机的实时转速。
1.输入捕获模式配置
输入捕获模式的初始化,各个STM32的教学视频都有,只是在配置编码器模式时,需要用到一个编码器模式的配置函数:TIM_EncoderInterfaceConfig()
输入捕获模式的初始化步骤如下:
1.使能定时器和通道对应的时钟:RCC_APB1PeriphClockCmd()
2.初始化IO口:GPIO_Init()
3.初始化定时器:TIM_TimeBaseInit()
4.配置编码器模式:TIM_EncoderInterfaceConfig()
5.初始化输入捕获通道:TIM_ICInit()
6.开启更新中断:TIM_ITConfig()
7.使能定时器:TIM_Cmd()
完整的代码如下:
/**************************************************************************功能:应用输入捕获方式采集编码器数据,使用M法测速反馈车轮实时速度函数:void Encoder_CAP_Init()作者:K.Fire日期:2022.01.30引脚:PA6(左轮A相) PA6(左轮B相) PB6(右轮A相) PB7(右轮B相)参数:无**************************************************************************/void Encoder_CAP_Init_L(){ /*1.使能定时器和通道对应的时钟*/ RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); /*2.初始化IO口*/ GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPD;//推挽下拉输入 GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; GPIO_Init(GPIOA,&GPIO_InitStruct); /*3.初始化定时器*/ TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct; TIM_TimeBaseInitStruct.TIM_ClockDivision = 0; TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInitStruct.TIM_Period = 0xffff; TIM_TimeBaseInitStruct.TIM_Prescaler = 0x0; TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStruct); //配置编码器模式 TIM_EncoderInterfaceConfig(TIM3,TIM_EncoderMode_TI12,TIM_ICPolarity_Rising,TIM_ICPolarity_Rising); /*4.初始化输入捕获通道*/ TIM_ICInitTypeDef TIM_ICInitStruct; TIM_ICStructInit(&TIM_ICInitStruct); TIM_ICInitStruct.TIM_ICFilter = 10;//设置滤波器,重复检测10次,防止抖动,增加精度 TIM_ICInit(TIM3,&TIM_ICInitStruct); TIM_ClearFlag(TIM3, TIM_FLAG_Update);//清除TIM的更新标志位 /*5.开启更新中断*/ TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE); TIM_SetCounter(TIM3,0);//计数器置零 TIM3- >CNT = 0x7fff;// /*4.初始化输入捕获通道*/// TIM_ICInitTypeDef TIM_ICInitStruct;// TIM_ICInitStruct.TIM_Channel = TIM_Channel_1 | TIM_Channel_2;//通道1和2// TIM_ICInitStruct.TIM_ICFilter = 10;//设置滤波器,重复检测10次,防止抖动,增加精度// TIM_ICInitStruct.TIM_ICPolarity = TIM_ICPolarity_TIM_ICPolarity_Rising;//上升沿捕获// TIM_ICInitStruct.TIM_ICPrescaler = TIM_ICPSC_DIV1;//配置输入分频,不分频// TIM_ICInitStruct.TIM_ICSelection = TIM_ICSelection_DirectTI;//不设置重映射// TIM_ICInit(TIM2,&TIM_ICInitStruct);// // /*5.开启捕获中断,并配置中断*/// TIM_ITConfig(TIM2, TIM_IT_CC1 | TIM_IT_CC2, ENABLE);// // NVIC_InitTypeDef NVIC_InitStruct;// NVIC_InitStruct.NVIC_IRQChannel = TIM2_IRQn;// NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;// NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;// NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;// NVIC_Init(&NVIC_InitStruct); /*6.使能定时器*/ TIM_Cmd(TIM3,ENABLE); /*7.编写中断服务函数*/}
一个编码器需要占用一个定时器,这里我使用的是TIM3和TIM4
注意:在初始化IO口时,选择输入输出模式有点问题。
网上有的说选择输入浮空模式、有的说选择输入上拉模式,但是我调试的时候这两种都不出结果,我改成输入下拉模式的是才调试成功,大家在实际调试过程中可以几个输入模式都试一下。
2.读取捕获脉冲数值
这部分和上面外部中断法的函数一样,输入3和4参数使用输入捕获方式提取计数器的值。
/**************************************************************************功能:获取不同方式下的脉冲值函数:int Read_Encoder(u8 TIMX)作者:K.Fire日期:2022.01.30参数:1:外部中断法左轮 2:外部中断法右轮 3:输入捕获法左轮 4:输入捕获法右轮**************************************************************************/int Read_Encoder(u8 TIMX){ int Encoder_TIM; switch(TIMX) { case 3:Encoder_TIM=TIM3- >CNT-0x7fff; TIM3- >CNT=0x7fff;break; case 4:Encoder_TIM=TIM4- >CNT-0x7fff; TIM4- >CNT=0x7fff;break; default:Encoder_TIM=0;break; } return Encoder_TIM;}/**************************************************************************功能:获取并打印输出实际速度值函数:void Get_SpeedNow(float* CurrentVelcity_L,float* CurrentVelcity_R)作者:K.Fire日期:2022.01.30参数:CurrentVelcity_L:左轮实时速度(地址) CurrentVelcity_R:右轮实时速度(地址)**************************************************************************/extern int Current_LN,Current_RN;//速度脉冲数void Get_SpeedNow(int* CurrentVelcity_L,int* CurrentVelcity_R){ /*输入捕获方式*/ Current_LN = -Read_Encoder(3); Current_RN = Read_Encoder(4); *CurrentVelcity_L = (MPN * Current_LN * 1000 * 10)/T;//单位:mm/s *CurrentVelcity_R = (MPN * Current_RN * 1000 * 10)/T; printf("The Current Left Wheel Velocity is: %d mm/s.\\r\\n",*CurrentVelcity_L); printf("The Current Right Wheel Velocity is: %d mm/s.\\r\\n",*CurrentVelcity_R);}
3.测试结果
05 总结
注意:左轮电机和右轮电机是相反的,需要实际测试确定,比如我这里,向前走的时候,右轮是正转,这时左轮反转的时候才是前进,在编写程序时需要给左轮的脉冲值加个负号。
获取到了实时速度值后,就可以用PID算法对电机转速进行准确控制了。
关键词: