本文大纲

  • 编码器的定义、作用与分类
  • 编码器的工作原理
  • 代码实现直流电机编码器计数

    欢迎移步我的哔哩哔哩账号观看相关内容的讲解视频。

本篇文章及视频参考了大量网络作品,包括csdn,知乎,哔哩哔哩等,不具有版权效应。

编码器的定义、作用与分类

    在实际操作中,为了保证电机驱动轮转动的速度和方向符合预期,我们需要对电机的转动速度与方向进行精确控制。

    而编码器的作用是,电机每转过一个角度,编码器获得一个脉冲信号,从而获得电机的实际转动参数(速度与方向)。

pkk0ka6.png

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

pkkwxGF.png

    我们在本篇blog及相关视频中采用增量式霍尔磁编码器进行演示。

  • 增量式:将位移转化为周期性电信号,继而转化成技术脉冲,用脉冲个数表示位移大小。
  • 霍尔磁:通过磁电转换将输出轴上的机械几何位移量转换成脉冲或数字量。

pkkwvPU.png

    霍尔编码器由霍尔码盘和霍尔元件组成。霍尔码盘是在一定直径的圆板上等分的不同的磁极,与电动机同轴。电动机旋转时,霍尔元件通过切割磁感线产生电流,输出若干脉冲信号,为判断电机转动方向,一般输出两组存在一定相位差的方波信号。

编译器的工作原理

我们可以简单地把编译器工作的流程表示为如下四个部分:

  1. 编码器生成脉冲信号
  2. 单片机捕获脉冲信号
  3. 公式计算速度
  4. 输出速度

同时补充一些后续实践可能用到的参数概念:

  • 分辨率:指编码器能够分辨的最小单位。对于增量式编码器,其分辨率表示为编码器转轴旋转一圈所产生的脉冲数,即脉冲数/转(Pulse Per Revolution或 PPR)。
  • 精度精度:是指编码器每个读数与转轴实际位置间的最大误差,通常用角度、角分或角秒来表示。
  • 最大响应频率:编码器每秒输出的脉冲数,单位是Hz。计算公式为:最大响应频率=分辨率*轴转速/60。
  • 信号输出形式:对于增量式编码器,每个通道的信号独立输出,输出电路形式通常有集电极开路输出、推挽输出、差分输出等。

下面是编码器信号采集方式:

单片机 采集方式 例子
自带编码器接口 输入捕获法(直接使用硬件计数) STM32
没有编码器接口 外部中断法(通过外部中断读取在跳变沿触发中断) Arduino

测速方法

    本篇教程中我们采用M法测量转速。前文我们提到,电机每转过一个角度,编码器获得一个脉冲信号,因此,单位时间内脉冲的个数可以用来表示这段时间内的平均速度,我们可以通过计量这个数值来估算平均速度,这就是M法测速,原理如图所示。

pkEdd6H.jpg

    我们可以给出如下公式:

n=mNT(r/s)=m60NT(r/min)

  • n——平均转速(r/min);

  • T——测速采样时间(s);

  • m——T时间内测得的编码器脉冲个数;

  • N——编码器每转脉冲数。

代码实现直流电机编码器计数

    我们现在需要解决的问题是,如何用代码实现定时接收编码器的脉冲并得到对应的参数,即T时间内测得的的编码器脉冲个数m。在单片机相关代码的编写中,我们一般通过外部中断或输入捕获实现,这里我先给出外部中断的具体方法,以后有时间会讲解后者。

    外部中断,是单片机实时地处理外部事件的一种内部机制。当外部事件发生时,单片机的中断系统将迫使CPU暂停正在执行的程序,转而去进行中断事件的处理,中断完毕后返回被中断的程序处,继续执行下去。

    这里引用江协科技STM32单片机EXTI外部中断教学视频的一例比较形象的比喻。医院看病一般是要排队的,先挂号的先看病,后挂号的后看病,这就对应了单片机在不中断情况下按顺序运行程序。但是如果有情况紧急的病人出现,医院就会暂停当前的问诊,优先去治疗他。EXTI外部中断就是这种“情况紧急”,它会迫使CPU终止当前程序(一般是main()),处理完中断事件后才能回到当时中断的位置继续执行。

一般格式如下

1
2
3
4
5
6
7
8
9
int mian(void) //主函数
{
while(1){
}
}
void EXTI0_IRQHandler(void) //中断函数
{

}

    在掌握了EXTI相关原理之后,我们可以开始编写函数来配置外部中断。

外部中断配置函数

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
/**************************************************************************
功能:应用外部中断方式采集编码器数据,使用M法测速反馈车轮实时速度
函数:Encoder_EXTIX_Init(void) EXTI15_10_IRQHandler(void)
引脚: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);


}

    配置环境是单片机使用过程中比较复杂且常见的步骤,这里我们不妨先将这冗长的函数封装起来,不求甚解地进入下一步骤。

外部中断服务函数

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
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
}
}

}

    随后我们编写脉冲获取函数。

脉冲获取函数

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
/**************************************************************************
功能:获取不同方式下的脉冲值
函数:int Read_Encoder(u8 TIMX)
参数: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)
参数: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);
}