杂谈

这篇博文写的时间确实有几天了,主要是想让需要的人更好地运用这一模块,同时将自己的使用经验分享给大家,就像当初迷茫的我,也是CSDN的 大佬们的指点迷津对我有了很大的帮助。

这几阶段,我主要是将一些模块知识的理解与运用,和一些项目的经验,后期打算深入编程语言与嵌入式相关技术。还有那一直想好好深入学习的数据结构,哈哈。

前言

编码器在项目、竞赛中被广泛运用。很多运动控制系统都是一个闭环系统,而在这个闭环系统中,各类型的电机必然是执行器,但只有电机,那这个运动控制只能是开环的,我们需要一个反馈值,用于实现电机控制的闭环结构。编码器被广泛应用于电机测速,实现电机闭环控制。

下篇博文是上次写的关于电机控制的博文
电机控制程序

一、何为编码器

编码器(encoder)是将信号(如比特流)或数据进行编制、转换为可用以通讯、传输和存储的信号形式的设备。
编码器把角位移或直线位移转换成电信号,前者称为码盘,后者称为码尺。

二、编码器的分类

按照读出方式,编码器可以分为接触式和非接触式两种。
按照工作原理,编码器可分为增量式和绝对式两类。

我们在通常使用中,常以工作原理的形式进行分类,以供选型使用。

1、增量式编码器

增量式编码器基本原理:

增量式编码器是将位移转换成周期性的电信号,再把这个电信号转变成计数脉冲,用脉冲的个数表示位移的大小。

先上图,留个印象。这是逐飞科技的一款正交编码器(有带方向的),(不是打广告)。
在这里插入图片描述

对于增量式的编码器,有几个基本的概念。
1、五根线,三根数据线A线、B线、Z线,还有VCC与GND。
其中,A、B均为数据输出线,输出的是脉冲数,用于数据加工处理。关于A、B线的不同之处,见下图。两者输出的脉冲存在90度的相位差。

Z线一般是零点信号的,就是当编码器旋转到零点位置时,它会发出一个脉冲用于表示,这个位置是生产商固定的,也可以称作机械零位。

对于增量式编码器的工作原理可见这篇博文:
增量式编码器详解

在这里插入图片描述

2、绝对式编码器

绝对式编码器基本原理:

与增量式通过计数脉冲数和判断脉冲的方向不同,绝对式编码器直接输出数字的传感器,更准确的说,是能给出与每个角位置相对应的完整的数宇量输出 。

所谓的位置信息大致可以这样理解,每个位置是唯一的,编码器旋转过程中,直接读取位置即可。编码器中有光码盘(可理解为下图)。

在它的圆形码盘上沿径向有若干同心码道,每条上由透光和不透光的扇形区相间组成,相邻码道的扇区数目是双倍关系,码盘上的码道数就是它的二进制数码的位数,在码盘的一侧是光源,另一侧对应每一码道有一光敏元件;当码盘处于不同位置时,各光敏元件根据受光照与否转换出相应的电平信号,形成二进制数。
这种编码器的特点是不要计数器,在转轴的任意位置都可读出一个固定的与位置相对应的数字码。显然,码道越多,分辨率就越高,对于一个具有 N 位二进制分辨率的编码器,其码盘必须有N 条码道。

在这里插入图片描述(光码盘)

绝对式编码器类别:单圈绝对编码器和多圈绝对编码器

**1、旋转单圈绝对编码器,**以转动中测量光电码盘各道刻线,以获取唯一的编码,当转动超过360度时,编码又回到原点,这样就不符合绝对编码唯一的原则,这样的编码只能用于旋转范围360度以内的测量,称为单圈绝对值编码器。

如果要测量旋转超过360度范围,就要用到多圈绝对编码器。

2、旋转多圈绝对编码器,编码器生产厂家运用钟表齿轮机械的原理,当中心码盘旋转时,通过齿轮传动另一组码盘(或多组齿轮,多组码盘),在单圈编码的基础上再增加圈数的编码,以扩大编码器的测量范围,这样的绝对编码器就称为多圈式绝对编码器,它同样是由机械位置确定编码,每个位置编码唯一不重复,而无需记忆。

这部分可详细参考这篇博文。绝对式编码器

3、编码器运用实例

先看图,这是我在智能车赛测速用的编码器,WCHF103芯片与四个编码器之间的数据传输依靠的是SPI的“分时复用”,对于分时复用的概念,前几篇博文已经介绍过了。走SPI通信这种办法,确实有一定的优点,少用一个定时器,哈哈。

后来,我又写了这个编码器与32的芯片进行SPI通信的程序,有需要的可以评论区留下邮箱。

在这里插入图片描述

3、霍尔编码器

如下图。这个编码器挺好,便宜实用,上面两种有些小贵。霍尔编码器也是增量式编码器中的一种,有A、B相两相输出,与文章开篇的那种不同,但理解起来是一样的道理。

接下来我就讲讲自己对带编码器的直流减速电机的理解。

在这里插入图片描述

三、带编码器的直流减速电机详解

1、直流减速电机的概念

1)直流因为是直流电。
2)根据公式P=FV,功率相同条件下,力和速度成反比。
2)编码器主要用于测速。

在这里插入图片描述(图片更加形象生动)

2、如何运用编码器进行测速

在带霍尔传感器的直流电机的条件下,电机转动一圈,通过霍尔传感器的A、B两相输出一定数量的脉冲,我们可以根据一定时间内的脉冲数计算出电机的瞬时速度。
记脉冲数有两种方式:

1) 我们通过定时器的输入捕获或者GPIO引脚的外部中断来检测边沿变化,以此来检测脉冲数,但是会有毛刺,也就是错误的脉冲信号。

2) 我们以stm32芯片作为主控,利用32的定时器外设的输入捕获功能,配置相关输入通道为编码器接口模式,就可以进行脉冲数的计数。

在第二种方式中,错误的脉冲信号会被输入滤波器过滤掉,所以更推荐也更常用。

注意点如下:

1) 通用、高级定时器具有输入捕获功能,基本定时器没有。
2) 在高级、通用定时器中不全都有编码器接口模式,只有TIM1-5、8具有编码器接口模式。(以STM32F103ZET6为例

3) 一个定时器有四个独立的输入通道CH1-4,但只有CHI,CH2,也就是TI1、TI2,能作为编码器接口的输入通道。所以,用多少个霍尔编码器,就需要多少个定时器。
4) 编码器模式下,定时器可以理解为计数器。

在这里插入图片描述根据上图,编码器的A、B两相通过接线,接到定时器输入通道1、2对应的GPIO引脚上即可,这样,电机转动,就会通过编码器产生连续的脉冲输入到T1,T2中。

接下来需要详细分析一下如何利用编码器接口模式进行测速的。

首先我们应该理解脉冲计数的原理。对于计数,有三种模式,

1) 计数器在T1输出的脉冲的上升沿或下降沿计数。
2) 计数器在T2输出的脉冲的上升沿或下降沿计数。
3) 计数器在T1和T2的输出脉冲的上升沿与下降沿计数。

但我们经常用第三种模式,因为提高了采样精度。具体的解释,可以看这个大佬的解释,好理解且实用博文链接

三种模式以及相关的计数方向如下表。

在这里插入图片描述
这里我先对这个表进行分析,便于理解。先分析表格。以仅在T1计数为例,相对应信号的电平就是指TI2是高电平还是低电平。TI1FP1信号指TI1的脉冲,然后脉冲分为上升沿与下降沿两种,同理可理解相应的表中的TI1FP2。

表中第一行,可以理解为在TI2上的脉冲为高电平时,如果TI1的脉冲为上升沿,则计数器+1,;如果TI1的脉冲为下降沿,则计数器-1。对于TI2的脉冲不进行计数,因为已经使用过其高低电平了,并且规定了只对TI1计数了。

其他行亦是这个道理。

下面这个表便是第三种模式下,计数器的工作过程,从图中我们可以知道,可以通过计数器是递增还是递减,来判断电机的正反转。而且可以看到,毛刺没过滤了,计数器碰到没有计数。
在这里插入图片描述

到这里想必对原理已经有较为清楚的理解了。

接下来,我们需要理解在编码过程中两个重要的参数。很重要!!!

1)重装载值
2)预分频系数

这两个系数在定时、PWM输出中都被运用到,且具有不同的含义,但这里我就详细讲一下在编码器计数中如何设定。

重装载值:

这里我们需要明白一点,电机转一圈输出的脉冲数是固定的,一般为360,具体的看情况,速度快了,也就可以理解为相同时间,转的圈数增多了。
遇到一个脉冲,计数器计数一次,当计数达到重装载值,就会产生溢出中断,计数器并清零重新计数,所以重装载值就是计数器脉冲计数的最大值。

预分频系数

原先定时器用作定时或者PWM输出,我们的预分频系数分的是32芯片的内部时钟,就是这个公式:CK_CNT=TIMxCLK/(PSC+1),这里的PSC就是预分频系数。但定时器用作编码器测速,这时时钟是外部时钟,也就是编码器采集的电机产生的脉冲。

预分频分的就是外部时钟的频率。

举个例子,如果你预分频系数为2,假设电机旋转一圈产生100个脉冲,则此时你单片机只能记录50个脉冲。

这两个因素应该理解的差不多了。

3、脉冲数转变成速度值方法

1、那我们如何计算转速呢,设重装载值为ARR,预分频系数置PSC,电机线数为360

那么电机转一圈,编码器采集到360*4 / PSC 个脉冲,因为预分频系数为PSC。再用定时器定时m秒,记录m秒内溢出的次数为n次,得到速度为 v=( n * ARR+当前的计数值) / (360 * 4 / PSC )/ m 。

总的来说,就是算每秒转了几圈。

2、预分频系数设置为 0 ,自动重装载值设为65535。然后在周期中断里每次去读取TIMx->CNT的值,并清零,以便于下一次读取。之所以在周期中断中,是因为我们读取的CNT的值相当于当前编码器的角度信息,以5ms的时间周期去读取,就可以看做相应时间编码器的变化,类似于测的效果。

下面这篇这篇博文可以作为借鉴。

博文链接

4、程序代码

第一种测速方式为例。

这段测速的思路引自这位大佬,具体文章和代码见下面链接。
测速方式一原文

int Encoder_Timer_Overflow;                                      //编码器溢出次数(每389*4溢出一次)
u16 Previous_Count;                                              //上次TIM3->CNT的值

void TIM3_Encode_init(u16 arr,u16 psc)
{
	GPIO_InitTypeDef GPIO_InitStructure;
	NVIC_InitTypeDef  NVIC_InitStructure;
  TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
  TIM_ICInitTypeDef TIM_ICInitStructure;    

  RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);    
  RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);  
		
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;          //GPIOA6和GPIOA7
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;                    //复用模式
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;              //速度100MHz
  GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;                //浮空	
  GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;                  //推挽复用输出
  GPIO_Init(GPIOA, &GPIO_InitStructure);                          //初始化PA6和PA7

  GPIO_PinAFConfig(GPIOA,GPIO_PinSource6,GPIO_AF_TIM3);           //GPIOA6复用为定时器3通道1
	GPIO_PinAFConfig(GPIOA,GPIO_PinSource7,GPIO_AF_TIM3);           //GPIOA7复用为定时器3通道2
	
  TIM_TimeBaseStructure.TIM_Period = arr; 	                      //(编码器线数-1)*4	四倍频原理
	TIM_TimeBaseStructure.TIM_Prescaler=psc;                        //定时器分频
	TIM_TimeBaseStructure.TIM_CounterMode=TIM_CounterMode_Up;       //向上计数模式
	TIM_TimeBaseStructure.TIM_ClockDivision=TIM_CKD_DIV1;           //时钟分频因子,不分频
	TIM_TimeBaseInit(TIM3,&TIM_TimeBaseStructure);                  //初始化TIM3
	
	TIM_ICInitStructure.TIM_Channel=TIM_Channel_1;                  //选择输入端IC1映射到TI1上
  TIM_ICInitStructure.TIM_ICPolarity=TIM_ICPolarity_Rising;	      //上升沿捕获
  TIM_ICInitStructure.TIM_ICSelection=TIM_ICSelection_DirectTI;   //映射到TI1上
  TIM_ICInitStructure.TIM_ICPrescaler=TIM_ICPSC_DIV1;	            //配置输入分频,不分频 
  TIM_ICInitStructure.TIM_ICFilter =6;                            //配置输入滤波器
  TIM_ICInit(TIM3,&TIM_ICInitStructure);
	
	TIM_ICInitStructure.TIM_Channel=TIM_Channel_2;                  //选择输入端IC2映射到TI2上
  TIM_ICInitStructure.TIM_ICPolarity=TIM_ICPolarity_Rising;	      //上升沿捕获
  TIM_ICInitStructure.TIM_ICSelection=TIM_ICSelection_DirectTI;   //映射到TI2上
  TIM_ICInitStructure.TIM_ICPrescaler=TIM_ICPSC_DIV1;	            //配置输入分频,不分频 
  TIM_ICInitStructure.TIM_ICFilter=6;                             //配置输入滤波器
  TIM_ICInit(TIM3,&TIM_ICInitStructure);
	
	TIM_EncoderInterfaceConfig(TIM3,TIM_EncoderMode_TI12,TIM_ICPolarity_Rising,TIM_ICPolarity_Rising );//编码器配置(定时器、编码模式、上升沿、上升沿)
		
  NVIC_InitStructure.NVIC_IRQChannel=TIM3_IRQn;                   //定时器3中断分组配置
	NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;                   //使能
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=0x01;      //抢占优先级1
	NVIC_InitStructure.NVIC_IRQChannelSubPriority =0x02;            //响应优先级2
	NVIC_Init(&NVIC_InitStructure);                                 //配置定时器3
		
	TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE);                        //允许定时器3更新中断
	TIM_Cmd(TIM3,ENABLE);                                           //使能定时器3
}

void TIM3_IRQHandler(void)                                        //定时器3中断服务函数
{
	if(TIM_GetITStatus(TIM3,TIM_IT_Update)==SET)                    //溢出中断
	{   
		Encoder_Timer_Overflow++;     		
	}
	TIM_ClearITPendingBit(TIM3,TIM_IT_Update);                      //清除中断标志位
}

u32 Read_Encoder(void)
{
  u32 Count;                                                      //一段时间内转过的脉冲数
  u16 Current_Count;                                              //当前TIM3->CNT的值
	u16 Enc_Timer_Overflow_one;	                                   

  Enc_Timer_Overflow_one=Encoder_Timer_Overflow;                  
  Current_Count = TIM_GetCounter(TIM3);                           //获得当前TIM3->CNT的值
  Encoder_Timer_Overflow=0;                                       //清零,方便下次计算
	if((TIM3->CR1&0x0010) == 0x0010)                                //如果反转
    Count = (u32)((Enc_Timer_Overflow_one)* -1*(4*ENCODER_PPR) - (Current_Count - Previous_Count));   //计算出一个时间转过的脉冲数
	else                                                            //如果正转
		Count = (u32)(Current_Count - Previous_Count + (Enc_Timer_Overflow_one) * (4*ENCODER_PPR));       //计算出一个时间转过的脉冲数
  Previous_Count = Current_Count;  
  return(Count);
}
//中间省略定时5的配置
void TIM5_IRQHandler(void)                          //定时器5中断服务函数
{
	if(TIM_GetITStatus(TIM5,TIM_IT_Update)==SET)      //溢出中断
	{  
		encode=Read_Encoder();                          //获得编码器的值
		printf("编码器脉冲数为:%d\r\n",encode);
		speed=encode/390.0f/4.0f/0.05f;
		printf("电机转速为:%f\r\n",speed);		
	}
	TIM_ClearITPendingBit(TIM5,TIM_IT_Update);        //清除中断标志位
}

总结

文章有些冗长,笔者我想尽量将各方面解释具体到位,不妥之处或者有疑问,可以在评论区留言,我会及时回复的。