第7章 直流有刷电机编码器测速​


本章我们主要来学习编码器测速的原理,并实现直流有刷电机的速度检测。在实际的电机控制中,我们时常需要关注电机的转速,并以此来了解电机的运行状态,因此,如何获得一个准确的电机速度至关重要。

本章分为如下几个小节:

7.1 编码器简介

7.2 编码器测速原理

7.3 编码器测速实现

7.1 编码器简介

1)概念:编码器是一种能将直线位移、角位移数据转换为脉冲信号、二进制编码的设备。它本质上就是一个传感器,可以把角位移或直线位移转换成电信号,并反馈给控制器,使控制器知道当前机械运动的位置、角度等信息。编码器的实物图如图7.1.1所示:

《DMF407电机控制专题教程》第7章 直流有刷电机编码器测速_寄存器


7.1.1 编码器实物

2)特点:精度高、结构简单、体积小、使用可靠、性价比高,等等。

3)应用场景:数控机床、机器人、雷达、光电经纬仪、地面指挥仪、高精度闭环调速系统、伺服系统等诸多领域。

4)编码器的分类:编码器的分类方式有很多,我们这里列举两种分类方式:按检测原理和编码类型,详见下图:

《DMF407电机控制专题教程》第7章 直流有刷电机编码器测速_编码器_02


7.1.2 编码器分类

从图7.1.2中可以看到,编码器按照检测原理可以分为光电式和磁电式;按照编码类型可分为增量式和绝对式。在实际的应用中,这四类编码器并不是相对独立的,它们经过组合后,就变成了光电绝对式、光电增量式、磁电绝对式和磁电增量式这四种编码器。

5)编码器原理:我们这里主要介绍磁电增量式、光电增量式以及光电绝对式这3种常用编码器的工作原理。

磁电增量式

原理:利用霍尔效应,将位移转换成计数脉冲,用脉冲个数计算位移和速度。

磁电增量式编码器的具体工作原理如图7.1.3所示:

《DMF407电机控制专题教程》第7章 直流有刷电机编码器测速_编码器_03


7.1.3 磁电增量式编码器工作原理

7.1.3中,磁电增量式编码器的结构包含:磁盘、霍尔传感器以及信号转换电路3个部分,其中,磁盘是由一个个交替排布的S极和N极磁极组成;霍尔传感器可以把磁场的变化转换成电信号的变化,它通常有AB两相(有的还有Z相),这两相的安装位置形成一定的夹角,这使得输出的AB两相信号有90°的相位差;信号转换电路可以把电信号转换成脉冲信号。

在实际应用中,磁盘会装在电机的转轴上,它会随着电机的转轴旋转,而磁盘上面的S极和N极就会交替地经过霍尔传感器的AB两相,霍尔传感器就可以把磁盘上的磁场变化转换为电信号的变化,输入到信号转换电路中,经过信号的转换之后,我们就可以得到AB两相脉冲信号了。从上图中可以看到,AB两相脉冲信号存在90°的相位差,而磁盘的正反转方向就决定了是A相信号在前还是B相在前。

光电增量式

原理:利用光电系统,将位移转换成计数脉冲,用脉冲个数计算位移和速度。

光电增量式编码器的具体工作原理如图7.1.4所示:

《DMF407电机控制专题教程》第7章 直流有刷电机编码器测速_寄存器_04


7.1.4 光电增量式编码器工作原理

7.1.4中,光电增量式编码器的结构包含:光电码盘、光源、透镜、受光元件以及信号转换电路5个部分,其中,光电码盘上有一个个均匀排布的透光孔;光源和透镜形成一个聚光系统;受光元件可以把光线的变化转换成电信号的变化,它通常有AB两相(有的还有Z相),这两相的安装位置形成一定的夹角,这使得输出的AB两相信号有90°的相位差;信号转换电路可以把电信号转换成脉冲信号。

在实际应用中,光电码盘会装在电机的转轴上,它会随着电机的转轴旋转,而码盘上面的透光孔会间歇性地经过AB两相,受光元件就可以把光线变化转换为电信号的变化,输入到信号转换电路中,经过信号的转换之后,我们就可以得到AB两相脉冲信号了。从上图中可以看到,AB两相脉冲信号存在90°的相位差,而码盘的正反转方向就决定了是A相信号在前还是B相在前。

光电绝对式

原理:当码盘处于不同位置(角度)时,光敏元件根据受光与否转换出相应的电平信号,最后转换成二进制数输出。

光电绝对式编码器的结构总体来说和光电增量式很类似,都是由光电码盘、光源、透镜、受光元件以及信号转换电路5个部分,但是它们码盘结构和输出信号含义不同。光电绝对式编码器的自然二进制码盘如图7.1.5所示:

《DMF407电机控制专题教程》第7章 直流有刷电机编码器测速_寄存器_05


7.1.5 自然二进制码盘

7.1.5中,光电绝对式编码器的二进制码盘上有很多圈线槽,我们称为码道。上图中的二进制码盘有4个码道,按照自然二进制的方式排列,这个码盘一共被分为2^4 = 16个区域,这些区域中,黑块不透光,代表1;白块透光,代表0。当码盘随着电机转轴旋转,光线会照射到不同的区域,受光元件就能感受到不同的光线情况,最后经过信号的处理,就可以直接输出该区域对应的二进制码了,而我们通过这个二进制码即可得出码盘(电机转轴)的当前位置(角度)。大家需要注意:二进制码盘的每一个位置对应一个确定的二进制码,因此这一类编码器常被应用于位置以及角度测量。

上述的自然二进制码盘读数很方便直观,但是它在实际应用中容易造成读数偏差很大,例如:当码盘停止旋转时,光线照射在00001111这两个相邻的区域之间,此时输出的二进制数可能是0000~1111中的任何一个,此时的读数和码盘的实际位置可能就相差很远了。为了避免读数和实际位置出现巨大偏差,我们可以改进一下二进制码的排列方式,使用格雷码形式,如图7.1.6所示:

《DMF407电机控制专题教程》第7章 直流有刷电机编码器测速_寄存器_06


7.1.6 格雷码码盘

7.1.6中,格雷码盘有4个码道,同样的也能表示16个二进制数,但是任意相邻的两个区域之间的二进制码只有一位不同。当我们采用格雷码盘时,如果码盘停止旋转,光线照射到码盘相邻两个区域之间,其最终输出的二进制数最多只会相差一位,此时位置的偏差范围就很小了。

6)编码器基本参数:

分辨率:编码器每个计数单位之间产生的距离,它是编码器可以测量到的最小的距离。对于增量式编码器,分辨率表示为编码器的转轴每旋转一圈所输出的脉冲数(PPR),也称为多少线,直流有刷电机教程中所使用的编码器是11线的

精度:编码器分辨率和精度是两个独立的概念,精度是指编码器输出的信号数据与实际位置之间的误差,常用角分′、角秒″表示。

最大响应频率:编码器每秒能输出的最大脉冲数,单位Hz,也称为PPS

最大转速:指编码器机械系统所能承受的最高转速。

7.2 编码器测速原理

本章节中我们所使用的编码器是磁电增量式编码器,它安装在直流有刷电机的尾部,实物图如图7.2.1所示:

《DMF407电机控制专题教程》第7章 直流有刷电机编码器测速_复用_07


7.2.1 直流有刷电机编码器实物

7.2.1中,直流有刷电机的编码器有AB两相,它们会输出两个相位差为90°的脉冲。当电机正转时,A相脉冲在前;当电机反转时,则是B相脉冲在前。

有了AB两相脉冲信号之后,我们应该如何去处理这些信号,把它们转换成电机的转速呢?这里就涉及到一个非常重要的功能:定时器编码器接口模式。

STM32定时器的编码器接口模式就相当于带有方向选择的外部时钟,也就是说,在此模式下,外部输入的脉冲信号可以作为计数器的时钟,而计数的方向则是由脉冲相位的先后所决定的。定时器编码器接口模式的原理如图7.2.2所示:

《DMF407电机控制专题教程》第7章 直流有刷电机编码器测速_复用_08


7.2.2 定时器编码器接口原理

7.2.2中,当电机(编码器)正转时,输出两相脉冲信号,A相脉冲在前,此时编码器接口把脉冲信号作为计数器的脉冲,计数方式为递增计数;当电机(编码器)反转时,计数方式就变成了递减计数。

上述的内容只是对编码器接口的原理进行简单的介绍,让大家有一个感性的认识,接下来我们深入研究一下编码器接口的原理:

1)编码器接口框图

我们首先看编码器的接口框图部分,了解一下脉冲信号进入编码器接口的途径,这里以通用定时器为例,具体框图如图7.2.3所示:

《DMF407电机控制专题教程》第7章 直流有刷电机编码器测速_复用_09


7.2.3 通用定时器编码器接口框图

7.2.3中,AB两相脉冲信号从TIMx_CH1TIMx_CH2这两个通道输入,经过滤波器和边沿检测器(可以设置滤波和反相)的处理,进入到编码器接口控制器中。大家需要注意,TIMx_CH3TIMx_CH4是不支持编码器接口模式的。

2)编码器接口计数原理

编码器接口可以利用输入脉冲的边沿进行计数,我们通过计数值的变化量,就可以算出输入脉冲信号的变化量,也就可以进一步计算出电机的转速了。接下来我们看一下编码器接口是如何根据脉冲边沿计数的,它的计数方向与输入脉冲信号的关系如表7.2.1所示:

有效边沿

相反信号的电平

TI1FP1对应TI2,

TI2FP2对应TI1

TI1FP1信号

TI2FP2信号

上升

下降

上升

下降

仅在TI1处计数

递减

递增

不计数

不计数

递增

递减

不计数

不计数

仅在TI2处计数

不计数

不计数

递增

递减

不计数

不计数

递减

递增

TI1TI2处均计数

递减

递增

递增

递减

递增

递减

递减

递增

7.2.1 计数方向与输入脉冲信号的关系

7.2.1来自于《STM32F4xx参考手册_V4(中文版).pdf》中的表75(第415页),上表中的有效边沿我们可以通过代码去配置,一共有3种边沿检测方式,其中仅在TI1处计数表示只检测TI1上的脉冲边沿变化,这时候的计数方向需要结合TI2FP2上的电平情况来确定,其他的两种边沿检测方式原理也是一样的,下面我们以一个实例来理解这个表格的内容:

假设我们把A相接在CH1TI1),B相接在CH2TI2),选择仅在TI1处计数(仅检测A相边沿)。此时编码器接口计数方向和输入脉冲信号的关系如下表:

有效边沿

B相信号

A相信号

上升

下降

仅在A相边沿计数

递减

递增

递增

递减

7.2.2 仅在A相边沿计数

编码器输出的AB两相脉冲信号如图7.2.4所示:

《DMF407电机控制专题教程》第7章 直流有刷电机编码器测速_编码器_10


7.2.4 AB相脉冲信号

7.2.4中,AB两相输出的脉冲信号有两种情况:当编码器正转,A相在前;当编码器反转,B相在前,我们选择仅在TI1处计数,也就是只检测A相的边沿。接下来我们分别介绍这两种情况下的计数方向:

正转:当A相上升沿到来时(图中①处),我们需要关注B相的电平高低,从图7.2.4中可看到B相此时是低电平,结合表7.2.2,可以得知此时计数方向为递增计数;当A相下降沿到来时(图中②处),从图中可以看到B相此时是高电平,结合表7.2.2,可以得知此时计数方向为递增计数;当A相上升沿再次到来时(图中③处),同理可得此时计数方向为递增计数。综上所得,我们可以知道此时编码器正转对应的计数方向就是递增计数。

反转:当A相上升沿到来时(图中④处),我们需要关注B相的电平高低,从图7.2.4中可看到B相此时是高电平,结合表7.2.2,可以得知此时计数方向为递减计数;当A相下降沿到来时(图中⑤处),从图中可以看到B相此时是低电平,结合表7.2.2,可以得知此时计数方向为递减计数;当A相上升沿再次到来时(图中⑥处),同理可得此时计数方向为递减计数。综上所得,我们可以知道此时编码器反转对应的计数方向就是递减计数。

上述的就是仅在TI1处计数的实例分析,其他两种边沿计数方式的原理是一样的,大家可以举一反三,我们这里就不再展开分析。

注意1、选择仅在TI1或者TI2处计数,就相当于对脉冲信号进行了2倍频(两个边沿),此时如果编码器输出10个脉冲信号,那么就会计数20次。2、选择的是在TI1TI2处均计数,就相当于对脉冲信号进行了4倍频,此时如果编码器输出10个脉冲信号,那么就会计数40次。因此,我们通过计数次数来计算电机速度的时候,需要除以相应的倍频系数。

至此,AB两相脉冲信号的变化就转换成了定时器的计数变化。接下来我们就可以通过一分钟内计数的变化量来计算电机的速度,具体公式如下:

电机转速 = 一分钟内计数变化量 / 倍频系数 / 编码器线数 / 减速比

到这里,编码器的测速原理就介绍完了,接下来我们开始实现直流有刷电机的测速实验。

7.3 编码器测速实现

本小节我们来学习使用直流有刷电机的编码器测速功能,关于编码器测速的原理大家可以回顾上一小节的内容。我们这里选择通用定时器的编码器接口来实现测速功能,而高级定时器则用于电机的基础驱动。

7.3.1 TIM2/TIM3/TIM4/TIM5寄存器

下面介绍TIM2/TIM3/TIM4/TIM5的几个与编码器测速相关且重要的寄存器,相关内容可以参考《STM32F4xx参考手册_V4(中文版).pdf》定时器相关章节

  • 控制寄存器 1TIMx_CR1

TIM2/TIM3/TIM4/TIM5的控制寄存器1描述如图7.3.1.1所示:

《DMF407电机控制专题教程》第7章 直流有刷电机编码器测速_复用_11


7.3.1.1 TIMx_CR1寄存器

上图中我们只列出了本实验需要用的位:CEN位,此位用于使能计数器的工作,必须要设置该位为1,才可以开始计数;DIR位,在编码器模式下可通过读取该位来判断计数方向。

  • 捕获/比较模式寄存器1TIMx_CCMR1该寄存器在编码器接口模式中作为输入功能,其描述如图7.3.1.2所示:
  • 《DMF407电机控制专题教程》第7章 直流有刷电机编码器测速_寄存器_12


  • 7.3.1.2 TIMx_CCMR1寄存器
    大家需要注意:TIMx_CCMR1寄存器对应于通道1和通道2的设置,CCMR2寄存器对应通道3和通道4,而通道3和通道4是不支持编码器接口模式的,所以这里只用到TIMx_CCMR1
    上图中的CC1S[1:0]这两个位用于CCR1的通道配置,这里我们设置IC1S[1:0]=01,也就是配置IC1映射在TI1上。
    输入捕获1预分频器IC1PSC[1:0],这个比较好理解。我们是1次边沿就触发1次计数,所以选择00就是了。
    输入捕获1滤波器IC1F[3:0],这个用来设置输入采样频率和数字滤波器长度。其中,fCK_INT是定时器时钟源频率,按照例程的配置为84Mhz,而fDTS则是根据TIMx_CR1CKD[1:0]的设置来确定的,如果CKD[1:0]设置为00,那么fDTS=fCK_INTN值采样次数,举个简单的例子:假设IC1F[3:0]=0011,并设置IC1映射到TI1上。表示以fCK_INT为采样频率,当连续8次都是采样到TI1为高电平或者低电平,滤波器才输出一个有效输出边沿。当8次采样中有高有低,那就保持原来的输出,这样可以滤除高频干扰信号,从而达到滤波的效果
  • 捕获/比较使能寄存器(TIMx_CCERTIM2/TIM3/TIM4/TIM5的捕获/比较使能寄存器,该寄存器控制着各个输入输出通道的开关和极性。TIMx_CCER寄存器描述如图7.3.1.3所示:
  • 《DMF407电机控制专题教程》第7章 直流有刷电机编码器测速_寄存器_13

  • 7.3.1.3 TIMx_CCER寄存器
    我们要用到这个寄存器的4个位,分别是CC1ECC1PCC2ECC2P位,上图中只列出了CCIECCIP位,其他两个位同理。要使能编码器接口模式,必须设置CC1ECC2E1,而CC1PCC2P设置的是边沿触发的方向。
  • 从模式控制寄存器(TIMx_SMCR

TIM2/TIM3/TIM4/TIM5的从模式控制寄存器描述如图7.3.1.4所示

7.3.1.4 TIMx_SMCR寄存器

该寄存器的SMS[2:0]位,用于从模式选择,其实就是选择计数器输入时钟的来源。在编码器模式中,如果需要仅在 TI1边沿处计数,则设置SMS[2:0]=010;如果需要仅在 TI2边沿处计数,则设置SMS[2:0]=001;如果需要在 TI1TI2边沿处都计数,则设置SMS[2:0]=011。本实验我们在TI1TI2边沿处均计数,也就是对脉冲信号进行4倍频。

7.3.2 硬件设计

1. 例程功能

1、本实验以电机开发板的直流有刷电机驱动接口1为例,基于电压温度电流采集实验,加入编码器测速功能,利用TIM3_CH1(由PC6复用)和TIM3_CH2(由PC7复用)作为编码器脉冲信号输入接口。

2、当按键0按下,就增大PWM的比较值变量,电机将加速,当按键1按下,就减小PWM的比较值变量,电机将减速,当比较值变量为正数时电机正转,反之电机反转,按下按键2则马上停止电机。

3、定时器6中断里面计算电机速度。

4、屏幕显示按键功能信息以及电机转速。

5、串口打印驱动板电压、电流、温度和转速信息。

6LED0闪烁指示程序运行。

2. 硬件资源

1LED

LED0 PE0

2)独立按键

KEY0 PE2

KEY1 PE3

KEY2 PE4

3)定时器136

TIM1正常输出通道 PA8

TIM1互补输出通道 PB13

TIM3 编码器A相输入通道 PC6

TIM3 编码器B相输入通道 PC7

4SD(刹车)信号输出 PF10

5ADC

ADC1通道9 PB1(电压)

ADC1通道0 PA0(温度)

ADC1通道8 PB0(电流)

6)串口1

USART1_TX PB6(发送)

USART1_RX PB7(接收)

3. 原理图

7.3.2.1 直流有刷电机接口原理图

7.3.2.1就是我们DMF407电机开发板的直流有刷电机接口1原理图,本实验我们除了用到基础驱动以及电压温度电流采集所需的引脚,还需要用到PM1_ENCAPC6)、PM1_ENCBPC7)这两个引脚,它们分别用于连接编码器的AB两相

本实验的硬件接线部分和上一章节一模一样,这里不再赘述,大家可以回顾上一章节的实验内容。

7.3.3 程序设计

7.3.3.1. 定时器的HAL库驱动

定时器在HAL库中的驱动代码在前面已经介绍了部分,这里我们再介绍几个本实验用到的函数。

1. HAL_TIM_Encoder_Init函数

定时器的编码器接口初始化函数。其声明如下:

HAL_StatusTypeDef HAL_TIM_Encoder_Init(TIM_HandleTypeDef *htim, TIM_Encoder_InitTypeDef *sConfig);

  • 函数描述:用于初始化编码器接口模式。
  • 函数形参:形参1TIM_HandleTypeDef结构体类型指针变量,用于配置定时器基本参数。形参2TIM_Encoder_InitTypeDef结构体类型指针变量,用于配置编码器模式参数。重点了解一下TIM_Encoder_InitTypeDef结构体指针类型,其定义如下:
typedef struct​
{​
uint32_t EncoderMode; /* 编码器模式 */​
uint32_t IC1Polarity; /* IC1输入信号的极性 */​
uint32_t IC1Selection; /* 输入通道映射 */​
uint32_t IC1Prescaler; /* 输入捕获预分频器 */​
uint32_t IC1Filter; /* 输入捕获滤波器 */​
uint32_t IC2Polarity; /* IC2输入信号的极性 */​
uint32_t IC2Selection; /* 输入通道映射 */​
uint32_t IC2Prescaler; /* 输入捕获预分频器 */​
uint32_t IC2Filter; /* 输入捕获滤波器 */​
} TIM_Encoder_InitTypeDef;

  • 该结构体成员用于配置编码器接口的参数。成员变量EncoderMode用来设置编码器模式,我们可以选择仅在TI1 / TI2处计数或者在TI1TI2处均计数。本实验中我们选择在TI1TI2处均计数,也就是4倍频。
    成员变量IC1Polarity在编码器模式下用于设置输入信号是否反相,它设定的是 TIMx_CCER 寄存器的 CCxNP 位和 CCxP 位。本实验我们不需要输入信号反相。
    成员变量ICSelection用来设置通道映射关系,它设定 TIMx_CCMRx 寄存器的 CCxS[1:0] 位的值。在编码器接口模式下,此成员设置为输入模式即可(其他型号芯片不一定),本实验我们选择TIM_ICSELECTION_DIRECTTI
    成员变量ICPrescaler用来设置输入捕获的时钟分频系数,本实验不需要分频,所以选择TIM_ICPSC_DIV1
    成员变量ICFilter用来设置滤波器长度,可选设置 0x0 0x0F。它设定 TIMx_CCMRx 寄存器ICxF[3:0] 位的值。
  • 函数返回值:HAL_StatusTypeDef枚举类型的值。2. HAL_TIM_Encoder_Start函数定时器的编码器接口模式启动函数,其声明如下
HAL_StatusTypeDef HAL_TIM_Encoder_Start(TIM_HandleTypeDef *htim, ​
uint32_t Channel);
  • 函数描述:用于启动定时器的编码器接口模式。
  • 函数形参:形参1TIM_HandleTypeDef结构体类型指针变量。
    形参2是定时器通道,范围:TIM_CHANNEL_1TIM_CHANNEL_4
  • 函数返回值:

HAL_StatusTypeDef枚举类型的值。

定时器的编码器模式配置步骤 ​

1)开启TIMx和输入通道的GPIO时钟,配置该IO口的复用功能输入

首先开启TIMx的时钟,然后配置GPIO为复用功能。本实验我们默认用到定时器3通道1和通道2,对应IOPC6PC7,它们的时钟开启方法如下:

__HAL_RCC_TIM3_CLK_ENABLE();  /* 使能定时器3 */​
__HAL_RCC_GPIOC_CLK_ENABLE(); /* 开启GPIOC时钟 */

IO口复用功能是通过函数HAL_GPIO_Init来配置的。

2)初始化TIMx,设置TIMxARRPSC等参数

使用定时器的输入捕获功能时,通过HAL_TIM_Encoder_Init函数来初始化定时器ARRPSC等参数。

注意:该函数会调用:HAL_TIM_Encoder_MspInit函数,我们可以通过后者存放定时器和GPIO时钟使能、GPIO初始化、中断使能以及优先级设置等代码

3)设置TIMx_CHy的编码器模式,开启编码器通道

HAL库中,定时器的编码器接口模式是通过HAL_TIM_Encoder_Init函数来设置定时器的编码器通道,包括映射关系,输入滤波和输入分频等。

4)使能定时器更新中断,配置定时器中断优先级

通过__HAL_TIM_ENABLE_IT函数使能定时器更新中断。

通过HAL_NVIC_EnableIRQ函数使能定时器中断。

通过HAL_NVIC_SetPriority函数设置中断优先级。

编码器在电机运行时会一直旋转并输出脉冲信号,如果时间较长,那么定时器计数就会溢出,我们必须对溢出必须做处理,否则电机速度的计算结果就不准了。

5)编写中断服务函数

定时器3中断服务函数为:TIM3_IRQHandler,当发生中断的时候,程序就会执行中断服务函数。HAL库为了使用方便,提供了一个定时器中断通用处理函数HAL_TIM_IRQHandler,该函数会调用一些定时器相关的回调函数,用于给用户处理定时器中断到了之后,需要处理的程序。本实验我们用到更新(溢出)中断回调函数HAL_TIM_PeriodElapsedCallback详见本例程源码。

7.3.3.2 程序流程图​

《DMF407电机控制专题教程》第7章 直流有刷电机编码器测速_复用_14


7.3.3.2.1 编码器测速程序流程图

7.3.3.3 程序解析​

这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。关于基础驱动和电压温度电流采集的代码我们不再赘述,大家可以回顾相应的章节

注意:本实验需要用到TIM3的编码器模式和TIM6的更新中断,相关的定时器驱动源码包括两个文件:dcmotor_tim.cdcmotor_tim.h

首先看dcmotor_tim.h头文件的几个宏定义:

/* 通用定时器 定义 */​
#define GTIM_TIMX_ENCODER_CH1_GPIO_PORT GPIOC​
#define GTIM_TIMX_ENCODER_CH1_GPIO_PIN GPIO_PIN_6​
#define GTIM_TIMX_ENCODER_CH1_GPIO_CLK_ENABLE() ​
do{ __HAL_RCC_GPIOC_CLK_ENABLE(); }while(0) /* PC口时钟使能 */​

#define GTIM_TIMX_ENCODER_CH2_GPIO_PORT GPIOC​
#define GTIM_TIMX_ENCODER_CH2_GPIO_PIN GPIO_PIN_7​
#define GTIM_TIMX_ENCODER_CH2_GPIO_CLK_ENABLE() ​
do{ __HAL_RCC_GPIOC_CLK_ENABLE(); }while(0) /* PC口时钟使能 */​

/* TIMX 引脚复用设置​
* 因为PC6/PC7, 默认并不是TIM3的功能脚, 必须开启复用, 才可以用作TIM3的CH1/CH2功能​
*/​
#define GTIM_TIMX_ENCODERCH1_GPIO_AF GPIO_AF2_TIM3 ​
#define GTIM_TIMX_ENCODERCH2_GPIO_AF GPIO_AF2_TIM3 ​

#define GTIM_TIMX_ENCODER TIM3 ​
#define GTIM_TIMX_ENCODER_INT_IRQn TIM3_IRQn​
#define GTIM_TIMX_ENCODER_INT_IRQHandler TIM3_IRQHandler​

#define GTIM_TIMX_ENCODER_CH1 TIM_CHANNEL_1 /* 通道1 */​
#define GTIM_TIMX_ENCODER_CH1_CLK_ENABLE() ​
do{ __HAL_RCC_TIM3_CLK_ENABLE(); }while(0) /* TIM3 时钟使能 */​

#define GTIM_TIMX_ENCODER_CH2 TIM_CHANNEL_2 /* 通道2 */​
#define GTIM_TIMX_ENCODER_CH2_CLK_ENABLE() ​
do{ __HAL_RCC_TIM3_CLK_ENABLE(); }while(0) /* TIM3 时钟使能 */​

/* 基本定时器 定义 ​
* 捕获编码器值,用于计算速度​
*/​
#define BTIM_TIMX_INT TIM6​
#define BTIM_TIMX_INT_IRQn TIM6_DAC_IRQn​
#define BTIM_TIMX_INT_IRQHandler TIM6_DAC_IRQHandler​
#define BTIM_TIMX_INT_CLK_ENABLE() ​
do{ __HAL_RCC_TIM6_CLK_ENABLE(); }while(0) /* TIM6 时钟使能 */​

/* 编码器参数结构体 */​
typedef struct ​
{​
int encode_old; /* 上一次计数值 */​
int encode_now; /* 当前计数值 */​
float speed; /* 编码器速度 */​
} ENCODE_TypeDef;​

extern ENCODE_TypeDef g_encode; /* 编码器参数变量 */

可以把上面的宏定义分成四部分:

第一部分是通用定时器3编码器模式输入通道对应的IO口宏定义;

第二部分是通用定时器3编码器模式输入通道的相应宏定义;

第三部分是定时器6更新中断的相应宏定义;

第四部分是编码器参数的结构体,这个结构体用于管理编码器的计数值和速度。

下面看dcmotor_tim.c的程序,首先是通用定时器的编码器接口模式初始化函数

/**​
* @brief 通用定时器TIMX 通道Y 编码器接口模式 初始化函数​
* @note​
* 通用定时器的时钟来自APB1,当PPRE1 ≥ 2分频的时候​
* 通用定时器的时钟为APB1时钟的2倍, 而APB1为42M, 所以定时器时钟 = 84Mhz​
* 定时器溢出时间计算方法: Tout = ((arr + 1) * (psc + 1)) / Ft us.​
* Ft=定时器工作频率,单位:Mhz​
*​
* @param arr: 自动重装值。​
* @param psc: 时钟预分频数​
* @retval 无​
*/​
void gtim_timx_encoder_chy_init(uint16_t arr, uint16_t psc)​
{ ​
g_timx_encode_chy_handle.Instance = GTIM_TIMX_ENCODER; /* 定时器x */​
g_timx_encode_chy_handle.Init.Prescaler = psc; /* 定时器分频 */​
g_timx_encode_chy_handle.Init.Period = arr; /* 自动重装载值 */​
g_timx_encode_chy_handle.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; ​
/* TI1、TI2都检测,4倍频 */​
g_timx_encoder_chy_handle.EncoderMode = TIM_ENCODERMODE_TI12; ​
/* 输入极性,非反向 */ ​
g_timx_encoder_chy_handle.IC1Polarity = TIM_ICPOLARITY_RISING; ​
/* 输入通道选择 */ ​
g_timx_encoder_chy_handle.IC1Selection = TIM_ICSELECTION_DIRECTTI; ​
g_timx_encoder_chy_handle.IC1Prescaler = TIM_ICPSC_DIV1; /* 不分频 */​
g_timx_encoder_chy_handle.IC1Filter = 10; /* 滤波器设置 */​

/* 输入极性,非反向 */​
g_timx_encoder_chy_handle.IC2Polarity = TIM_ICPOLARITY_RISING;​
/* 输入通道选择 */​
g_timx_encoder_chy_handle.IC2Selection = TIM_ICSELECTION_DIRECTTI; ​
g_timx_encoder_chy_handle.IC2Prescaler = TIM_ICPSC_DIV1; /* 一分频 */​
g_timx_encoder_chy_handle.IC2Filter = 10; /* 滤波器设置 */​
HAL_TIM_Encoder_Init(&g_timx_encode_chy_handle, ​
&g_timx_encoder_chy_handle); /* 初始化定时器x编码器 */​
/* 使能编码器通道1、2 */​
HAL_TIM_Encoder_Start(&g_timx_encode_chy_handle,GTIM_TIMX_ENCODER_CH1);​
HAL_TIM_Encoder_Start(&g_timx_encode_chy_handle,GTIM_TIMX_ENCODER_CH2);​
/* 使能更新中断、清除中断标志位 */​
__HAL_TIM_ENABLE_IT(&g_timx_encode_chy_handle,TIM_IT_UPDATE);​
__HAL_TIM_CLEAR_FLAG(&g_timx_encode_chy_handle,TIM_IT_UPDATE);​
}

HAL_TIM_Encoder_Init初始化定时器3的基础工作参数和编码器接口,如:ARRPSC、编码器模式、滤波器等,第二部分则是调用HAL_TIM_Encoder_Start函数开启编码器通道。最后是使能更新中断和清除中断标志位。通道对应的IO、时钟开启和NVIC的初始化都在HAL_TIM_Encoder_MspInit函数里编写,其定义如下:

/**​
* @brief 定时器底层驱动,时钟使能,引脚配置​
此函数会被HAL_TIM_Encoder_Init()调用​
* @param htim:定时器句柄​
* @retval 无​
*/ ​
void HAL_TIM_Encoder_MspInit(TIM_HandleTypeDef *htim)​
{​
if (htim->Instance == GTIM_TIMX_ENCODER)​
{​
GPIO_InitTypeDef gpio_init_struct;​
GTIM_TIMX_ENCODER_CH1_GPIO_CLK_ENABLE(); /* 开启IO时钟 */​
GTIM_TIMX_ENCODER_CH2_GPIO_CLK_ENABLE();​
GTIM_TIMX_ENCODER_CH1_CLK_ENABLE(); /*开启定时器时钟*/​
GTIM_TIMX_ENCODER_CH2_CLK_ENABLE();​

gpio_init_struct.Pin = GTIM_TIMX_ENCODER_CH1_GPIO_PIN; /* 通道1的IO */​
gpio_init_struct.Mode = GPIO_MODE_AF_PP; /* 复用推挽输出 */​
gpio_init_struct.Pull = GPIO_NOPULL; /* 上拉 */​
gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH; /* 高速 */​
gpio_init_struct.Alternate = GTIM_TIMX_ENCODERCH1_GPIO_AF; /* 端口复用 */​
HAL_GPIO_Init(GTIM_TIMX_ENCODER_CH1_GPIO_PORT, &gpio_init_struct); ​

gpio_init_struct.Pin = GTIM_TIMX_ENCODER_CH2_GPIO_PIN; /* 通道2的IO */​
gpio_init_struct.Mode = GPIO_MODE_AF_PP; /* 复用推挽输出 */​
gpio_init_struct.Pull = GPIO_NOPULL; /* 上拉 */​
gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH; /* 高速 */​
gpio_init_struct.Alternate = GTIM_TIMX_ENCODERCH2_GPIO_AF;​
HAL_GPIO_Init(GTIM_TIMX_ENCODER_CH2_GPIO_PORT, &gpio_init_struct);​

HAL_NVIC_SetPriority(GTIM_TIMX_INT_IRQn, 2, 0); /* 优先级设置 */​
HAL_NVIC_EnableIRQ(GTIM_TIMX_INT_IRQn); /* 开启中断 */​
}​
}

该函数调用HAL_GPIO_Init函数初始化定时器输入通道对应的IO,并且开启GPIO时钟,使能定时器。其中要注意IO口复用功能的选择一定要选对了。最后配置中断抢占优先级和响应优先级,以及打开定时器中断。

接着我们看基本定时器6的中断初始化函数及其底层初始化函数。

/**​
* @brief 基本定时器TIMX定时中断初始化函数​
* @note​
* 基本定时器的时钟来自APB1,当PPRE1 ≥ 2分频的时候​
* 基本定时器的时钟为APB1时钟的2倍, 而APB1为42M, 所以定时器时钟 = 84Mhz​
* 定时器溢出时间计算方法: Tout = ((arr + 1) * (psc + 1)) / Ft us.​
* Ft=定时器工作频率,单位:Mhz​
*​
* @param arr: 自动重装值。​
* @param psc: 时钟预分频数​
* @retval 无​
*/​
void btim_timx_int_init(uint16_t arr, uint16_t psc)​
{​
timx_handler.Instance = BTIM_TIMX_INT; /* 基本定时器X */​
timx_handler.Init.Prescaler = psc; /* 设置预分频器 */​
timx_handler.Init.CounterMode = TIM_COUNTERMODE_UP; /* 向上计数器 */​
timx_handler.Init.Period = arr; /* 自动装载值 */​
timx_handler.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; ​
HAL_TIM_Base_Init(&timx_handler);​

HAL_TIM_Base_Start_IT(&timx_handler); /* 使能基本定时器x及其更新中断 */​
__HAL_TIM_CLEAR_IT(&timx_handler,TIM_IT_UPDATE); /* 清除中断标志位 */​
}​

/**​
* @brief 定时器底册驱动,开启时钟,设置中断优先级​
此函数会被HAL_TIM_Base_Init()函数调用​
* @param 无​
* @retval 无​
*/​
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef *htim)​
{​
if (htim->Instance == BTIM_TIMX_INT)​
{​
BTIM_TIMX_INT_CLK_ENABLE(); /*使能TIM时钟*/​
HAL_NVIC_SetPriority(BTIM_TIMX_INT_IRQn, 1, 3);​
HAL_NVIC_EnableIRQ(BTIM_TIMX_INT_IRQn); /*开启ITM3中断*/​
}​
}

这一部分函数和基本定时器中断实验是一样的,这里不再赘述,我们使用定时器6来定时计算电机速度,本实验中配置溢出时间为1ms,也就是1ms进入一次定时器6的更新中断。

下面开始看中断服务函数的逻辑程序,HAL_TIM_IRQHandler函数会调用下面的回调函数,我们的逻辑代码就是放在回调函数里,函数定义如下:

/**​
* @brief 定时器更新中断回调函数​
* @param htim:定时器句柄指针​
* @note 此函数会被定时器中断函数共同调用的​
* @retval 无​
*/​
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)​
{​
if (htim->Instance == TIM3)​
{​
/* 判断CR1的DIR位 */​
if(__HAL_TIM_IS_TIM_COUNTING_DOWN(&g_timx_encode_chy_handle))​
{​
g_timx_encode_count--; /* DIR位为1,也就是递减计数 */​
}​
else​
{​
g_timx_encode_count++; /* DIR位为0,也就是递增计数 */​
}​
}​
else if (htim->Instance == TIM6)​
{​
int Encode_now = gtim_get_encode(); /* 获取编码器值,用于计算速度 */​
/* 中位平均值滤除编码器抖动数据,50ms计算一次速度 */​
speed_computer(Encode_now, 50);​
}​
}

进入更新中断回调函数后,先判断是不是定时器3的寄存器基地址,如果是则读取CR1寄存器的DIR位,判断计数溢出的方向,如果DIR位为1,就是递减计数,说明是向下溢出,我们让变量g_timx_encode_count自减1;如果DIR位为0,就是递增计数,说明是向上溢出,我们让变量g_timx_encode_count自增1,变量g_timx_encode_count将用于计算总的计数值。

如果是定时器6的寄存器基地址,就先获取编码器的计数总值,然后每隔50ms计算一次电机速度,其中所涉及到的两个函数我们后续再介绍。

接下来看编码器计数总值的计算函数。

/**​
* @brief 获取编码器的值​
* @param 无​
* @retval 编码器值​
*/​
int gtim_get_encode(void)​
{​
/* 当前计数值+之前累计编码器的值=总的编码器值 */​
return (int32_t)__HAL_TIM_GET_COUNTER(&g_timx_encode_chy_handle) + ​
g_timx_encode_count * 65536; ​
}

代码中通过调用__HAL_TIM_GET_COUNTER函数来获取当前的计数值,然后进行编码器计数总值的计算,这里需要用到变量g_timx_encode_count,具体的计算公式如下:

计数总值 = 当前计数值 + g_timx_encode_count * 65536

获取到计数总值之后,我们就可以计算电机的速度了,本实验中电机速度计算相关源码包括两个文件:dc_motor.cdc_motor.h

首先看dc_motor.h头文件的几个宏定义:

#define ROTO_RATIO 44 /* 线数*倍频系数,即11*4=44 */​
#define REDUCTION_RATIO 30 /* 减速比30:1 */​

/* 电机参数结构体 */​
typedef struct ​
{​
uint8_t state; /*电机状态*/​
float current; /*电机电流*/​
float volatage; /*电机电压*/​
float power; /*电机功率*/​
float speed; /*电机实际速度*/​
int32_t motor_pwm; /*设置比较值大小 */​
} Motor_TypeDef;​

extern Motor_TypeDef g_motor_data; /*电机参数变量*/

ROTO_RATIOREDUCTION_RATIO是用于速度计算的系数,ROTO_RATIO等于编码器的线数*倍频系数,教程中采用的直流有刷电机编码器线数为11,也就是说编码器每旋转一圈就输出11个脉冲信号;REDUCTION_RATIO代表电机的减速比,我们算出编码器的速度之后,就可以根据减速比计算电机的速度了。Motor_TypeDef这个结构体是用于管理电机的参数的,我们本实验中只需要用到speed(实际速度)这个成员。

下面看dcmotor_tim.c的程序,我们只看电机速度计算函数。

/**​
* @brief 电机速度计算​
* @param encode_now:当前编码器总的计数值​
* ms:计算速度的间隔,中断1ms进入一次,例如ms = 5即5ms计算一次速度​
* @retval 无​
*/​
void speed_computer(int32_t encode_now, uint8_t ms)​
{​
uint8_t i = 0, j = 0;​
float temp = 0.0;​
static uint8_t sp_count = 0, k = 0;​
static float speed_arr[10] = {0.0}; /* 存储速度进行滤波运算 */​

if (sp_count == ms) /* 计算一次速度 */​
{​
/* 计算电机转速 ​
第一步 :计算ms毫秒内计数变化量​
第二步 ;计算1min内计数变化量:g_encode.speed * ((1000 / ms) * 60 ,​
第三步 :除以编码器旋转一圈的计数次数(倍频倍数 * 编码器分辨率)​
第四步 :除以减速比即可得出电机转速​
*/​
g_encode.encode_now = encode_now; /* 取出编码器当前计数值 */​
/* 计算编码器计数值的变化量 */​
g_encode.speed = (g_encode.encode_now - g_encode.encode_old); ​
/* 保存一次电机转速 */​
speed_arr[k++] = (float)(g_encode.speed * ((1000 / ms) * 60.0) / ​
(uint16_t)(REDUCTION_RATIO * ROTO_RATIO));​
/* 保存当前编码器的值 */​
g_encode.encode_old = g_encode.encode_now;​

if (k >= 10) /* 累计10次速度值,后续进行滤波*/​
{ ​
for (i = 10; i >= 1; i--) /* 冒泡排序*/​
{​
for (j = 0; j < (i - 1); j++) ​
{​
if (speed_arr[j] > speed_arr[j + 1]) /* 数值比较 */​
{ ​
temp = speed_arr[j]; /* 数值换位 */​
speed_arr[j] = speed_arr[j + 1];​
speed_arr[j + 1] = temp;​
}​
}​
}​
temp = 0.0;​
for (i = 2; i < 8; i++) /* 去除两边高低数据 */​
{​
temp += speed_arr[i]; /* 将中间数值累加 */​
}​
temp = (float)(temp / 6); /* 求速度平均值 */​
/* 一阶低通滤波​
* 公式为:Y(n)= qX(n) + (1-q)Y(n-1)​
* 其中X(n)为本次采样值;Y(n-1)为上次滤波输出值;Y(n)为本次滤波输出值,​
q为滤波系数​
* q值越小则上一次输出对本次输出影响越大,整体曲线越平稳,​
但是对于速度变化的响应也会越慢​
*/​
g_motor_data.speed = (float)((g_motor_data.speed * (float)0.52) + ​
((float)0.48 * temp));​
k = 0;​
}​
sp_count = 0;​
}​
sp_count ++;​
}

电机的速度计算步骤如下:

第一步,计算50毫秒内计数变化量;

第二步,计算1min内计数变化量:g_encode.speed * ((1000 / ms) * 60

第三步,除以编码器旋转一圈的计数次数(REDUCTION_RATIO);

第四步,除以减速比(ROTO_RATIO)即可得出电机转速;

我们累计计算10次电机速度,然后进行冒泡排序,把10次电机速度值从小到大排序,接着将中间的6次速度值累加求平均值,最后再进行一阶低通滤波。由于冒泡排序和一阶低通滤波的详细介绍篇幅过长,我们这里不做展开,大家感兴趣的话可以去网上搜索相关的内容。

main.c里面编写如下代码:

int main(void)​
{ ​
uint8_t key;​
uint16_t t;​
int32_t motor_pwm = 0;​
uint16_t init_adc_val; ​
HAL_Init(); /* 初始化HAL库 */​
sys_stm32_clock_init(336, 8, 2, 7); /* 设置时钟,168Mhz */​
delay_init(168); /* 延时初始化 */​
usart_init(115200); /* 串口初始化为115200 */​
led_init(); /* 初始化LED */​
lcd_init(); /* 初始化LCD */​
key_init(); /* 初始化按键 */​
atim_timx_cplm_pwm_init(8400 - 1, 0); /* 168Mhz的计数频率 */​
dcmotor_init(); /* 初始化电机 */​
adc_nch_dma_init(); /* ADC DMA传输初始化 */​
gtim_timx_encoder_chy_init(0XFFFF, 0); /* 不分频直接84M的计数频率 */​
btim_timx_int_init(1000 - 1 , 84 - 1); /* 基本定时器初始化,1ms计数周期 */​

g_point_color = WHITE;​
g_back_color = BLACK;​
lcd_show_string(10, 10, 200, 16, 16, "DcMotor Test", g_point_color);​
lcd_show_string(10, 30, 200, 16, 16, "KEY0:Start forward", g_point_color);​
lcd_show_string(10, 50, 200, 16, 16, "KEY1:Start backward", g_point_color);​
lcd_show_string(10, 70, 200, 16, 16, "KEY2:Stop", g_point_color);​

delay_ms(20);​

/* init_adc_val存储电流测量对应的参考电压ADC值,这里进行滤波 */​
init_adc_val = g_adc_val [2]; /* 取出第一次得到的值 */​
for(t=0;t<1000;t++)​
{​
init_adc_val += g_adc_val [2]; /* 现在的值和上一次存储的值相加 */​
init_adc_val /= 2; /* 取平均值 */​
delay_ms(1);​
}​

while (1)​
{​
key = key_scan(0); /* 按键扫描 */​
if(key == KEY0_PRES) /* 当key0按下 */​
{​
/* 因为不同的电机最小启动电压不同,可能在第一次增加的时候电机还不能转起来 */​
motor_pwm += 400;​
if (motor_pwm == 0) ​
{​
dcmotor_stop(); /* 停止则立刻响应 */​
motor_pwm = 0;​
} ​
else ​
{​
dcmotor_start(); /* 开启电机 */​
if (motor_pwm >= 8400) /* 限速 */​
{​
motor_pwm = 8400;​
} ​
}​
motor_pwm_set(motor_pwm); /* 设置电机方向、转速 */​
}​

else if(key == KEY1_PRES) /* 当key1按下 */​
{​
motor_pwm -= 400;​
if (motor_pwm == 0) ​
{​
dcmotor_stop(); /* 停止则立刻响应 */​
motor_pwm = 0;​
} ​
else ​
{​
dcmotor_start(); /* 开启电机 */​
if (motor_pwm <= -8400) /* 限速 */​
{​
motor_pwm = -8400;​
} ​
}​
motor_pwm_set(motor_pwm); /* 设置电机方向、转速 */​
}​

else if(key == KEY2_PRES) /* 当key2按下 */​
{​
LED1_TOGGLE();​
dcmotor_stop(); /* 关闭电机 */​
motor_pwm = 0;​
motor_pwm_set(motor_pwm); /*设置电机方向、转速 */ ​
}​

delay_ms(10);​
t++;​
if(t % 20 == 0)​
{ ​
lcd_dis(); /* 显示电机速度信息 */​
LED0_TOGGLE(); /* LED0(红灯) 翻转 */​

printf("KEY0:增加比较值,KEY1:减小比较值,KEY2:停止电机\r\n\r\n");​
printf("Valtage:%.1fV \r\n",g_adc_val[0]*ADC2VBUS); /* 打印电压值 */​
printf("Temp:%.1fC \r\n",get_temp(g_adc_val[1])); /* 打印温度值 */​
printf("Current:%.1fmA \r\n",abs(g_adc_val[2]-​
init_adc_val)*ADC2CURT); /* 打印电流值 */​
printf("电机速度:%.1f RPM\r\n\r\n",g_motor_data.speed);/* 打印速度值 */​
}​
}​
}

我们只看本实验新增的内容,先看gtim_timx_encoder_chy_init(0XFFFF, 0)这个语句,这两个形参分别设置自动重载寄存器的值为65535,以及预分频器寄存器的值为0。定时器316位的计数器,这里设置为最大值65535。预分频系数,我们设置为分频,定时器3计数频率是84MHz。接着看btim_timx_int_init(1000 - 1 , 84 - 1)这个语句,这里设置了定时器6的计数频率为84M/84=1M Hz,计数周期为1ms,也就是1ms进入一次更新中断。最后就是在串口以及屏幕上输出电机速度值了。

7.3.4 下载验证​

下载代码后,可以看到LED0在闪烁,说明程序已经正常在跑了,LCD上显示按键功能以及电机速度信息,当我们按下KEY0,比较值变量motor_pwm将增大;按下KEY1,比较值变量motor_pwm将减小;按下KEY2,电机将停止。比较值变量motor_pwm为正数时,电机正转,反之电机反转,其绝对值越大,电机的速度越快。我们再打开串口调试助手,选择对应的串口端口,我这边是COM3,可以看到串口打印的按键信息、电压、温度、电流以及电机速度值,如图7.3.4.1所示:

《DMF407电机控制专题教程》第7章 直流有刷电机编码器测速_复用_15


7.3.4.1打印按键信息、电压、温度、电流以及速度值