本文大纲
- 编码器的定义、作用与分类
- 编码器的工作原理
- 代码实现直流电机编码器计数
本篇文章及视频参考了大量网络作品,包括csdn,知乎,哔哩哔哩等,不具有版权效应。
编码器的定义、作用与分类
    在实际操作中,为了保证电机驱动轮转动的速度和方向符合预期,我们需要对电机的转动速度与方向进行精确控制。
    而编码器的作用是,电机每转过一个角度,编码器获得一个脉冲信号,从而获得电机的实际转动参数(速度与方向)。

    编码器主要分为如下几类,可以先看一下留个印象,对编码器分类和原理的理解对后续的实践影响不大。

    我们在本篇blog及相关视频中采用增量式霍尔磁编码器进行演示。
- 增量式:将位移转化为周期性电信号,继而转化成技术脉冲,用脉冲个数表示位移大小。
- 霍尔磁:通过磁电转换将输出轴上的机械几何位移量转换成脉冲或数字量。

    霍尔编码器由霍尔码盘和霍尔元件组成。霍尔码盘是在一定直径的圆板上等分的不同的磁极,与电动机同轴。电动机旋转时,霍尔元件通过切割磁感线产生电流,输出若干脉冲信号,为判断电机转动方向,一般输出两组存在一定相位差的方波信号。
编译器的工作原理
我们可以简单地把编译器工作的流程表示为如下四个部分:
- 编码器生成脉冲信号
- 单片机捕获脉冲信号
- 公式计算速度
- 输出速度
同时补充一些后续实践可能用到的参数概念:
- 分辨率:指编码器能够分辨的最小单位。对于增量式编码器,其分辨率表示为编码器转轴旋转一圈所产生的脉冲数,即脉冲数/转(Pulse Per Revolution或 PPR)。
- 精度精度:是指编码器每个读数与转轴实际位置间的最大误差,通常用角度、角分或角秒来表示。 
- 最大响应频率:编码器每秒输出的脉冲数,单位是Hz。计算公式为:最大响应频率=分辨率*轴转速/60。
- 信号输出形式:对于增量式编码器,每个通道的信号独立输出,输出电路形式通常有集电极开路输出、推挽输出、差分输出等。
下面是编码器信号采集方式:
| 单片机 | 采集方式 | 例子 | 
| 自带编码器接口 | 输入捕获法(直接使用硬件计数) | STM32 | 
| 没有编码器接口 | 外部中断法(通过外部中断读取在跳变沿触发中断) | Arduino | 
测速方法
    本篇教程中我们采用M法测量转速。前文我们提到,电机每转过一个角度,编码器获得一个脉冲信号,因此,单位时间内脉冲的个数可以用来表示这段时间内的平均速度,我们可以通过计量这个数值来估算平均速度,这就是M法测速,原理如图所示。

    我们可以给出如下公式:
- n——平均转速(r/min); 
- T——测速采样时间(s); 
- m——T时间内测得的编码器脉冲个数; 
- N——编码器每转脉冲数。 
代码实现直流电机编码器计数
    我们现在需要解决的问题是,如何用代码实现定时接收编码器的脉冲并得到对应的参数,即T时间内测得的的编码器脉冲个数m。在单片机相关代码的编写中,我们一般通过外部中断或输入捕获实现,这里我先给出外部中断的具体方法,以后有时间会讲解后者。
    外部中断,是单片机实时地处理外部事件的一种内部机制。当外部事件发生时,单片机的中断系统将迫使CPU暂停正在执行的程序,转而去进行中断事件的处理,中断完毕后返回被中断的程序处,继续执行下去。
    这里引用江协科技STM32单片机EXTI外部中断教学视频的一例比较形象的比喻。医院看病一般是要排队的,先挂号的先看病,后挂号的后看病,这就对应了单片机在不中断情况下按顺序运行程序。但是如果有情况紧急的病人出现,医院就会暂停当前的问诊,优先去治疗他。EXTI外部中断就是这种“情况紧急”,它会迫使CPU终止当前程序(一般是main()),处理完中断事件后才能回到当时中断的位置继续执行。
一般格式如下
| 12
 3
 4
 5
 6
 7
 8
 9
 
 | int mian(void) {
 while(1){
 }
 }
 void EXTI0_IRQHandler(void)
 {
 
 }
 
 | 
    在掌握了EXTI相关原理之后,我们可以开始编写函数来配置外部中断。
外部中断配置函数
| 12
 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
 
 | 
 
 
 
 
 int Encoder_L_EXTI=0;
 int Encoder_R_EXTI=0;
 
 void Encoder_EXTIX_Init(void)
 {
 
 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);
 
 
 RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
 
 
 GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource12);
 GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource13);
 GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource14);
 GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource15);
 
 
 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);
 
 
 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);
 
 
 }
 
 | 
    配置环境是单片机使用过程中比较复杂且常见的步骤,这里我们不妨先将这冗长的函数封装起来,不求甚解地进入下一步骤。
外部中断服务函数
| 12
 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
 
 | void EXTI15_10_IRQHandler(void){
 if(EXTI_GetITStatus(EXTI_Line12) != RESET)
 {
 EXTI_ClearITPendingBit(EXTI_Line12);
 if(PBin(12)==0)
 {
 if(PBin(13)==0)   Encoder_L_EXTI++;
 else             Encoder_L_EXTI--;
 }
 else
 {
 if(PBin(13)==1)  Encoder_L_EXTI++;
 else             Encoder_L_EXTI--;
 }
 }
 
 if(EXTI_GetITStatus(EXTI_Line13) != RESET)
 {
 EXTI_ClearITPendingBit(EXTI_Line13);
 if(PBin(13)==0)
 {
 if(PBin(12)==1)   Encoder_L_EXTI++;
 else             Encoder_L_EXTI--;
 }
 else
 {
 if(PBin(12)==0)  Encoder_L_EXTI++;
 else             Encoder_L_EXTI--;
 }
 }
 
 if(EXTI_GetITStatus(EXTI_Line14) != RESET)
 {
 EXTI_ClearITPendingBit(EXTI_Line14);
 if(PBin(14)==0)
 {
 if(PBin(15)==0)   Encoder_R_EXTI++;
 else             Encoder_R_EXTI--;
 }
 else
 {
 if(PBin(15)==1)  Encoder_R_EXTI++;
 else             Encoder_R_EXTI--;
 }
 }
 
 if(EXTI_GetITStatus(EXTI_Line15) != RESET)
 {
 EXTI_ClearITPendingBit(EXTI_Line15);
 if(PBin(15)==0)
 {
 if(PBin(14)==1)   Encoder_R_EXTI++;
 else             Encoder_R_EXTI--;
 }
 else
 {
 if(PBin(14)==0)  Encoder_R_EXTI++;
 else             Encoder_R_EXTI--;
 }
 }
 
 }
 
 | 
    随后我们编写脉冲获取函数。
脉冲获取函数
| 12
 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
 
 | 
 
 
 
 
 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;
 }
 
 
 
 
 
 
 
 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);
 }
 
 |