TIM 编码器接口-stm32入门

1. TIM 编码器接口简介

1.1 基本概念

Encoder Interface 编码器接口,基本上相当于使用了一个带有方向选择的外部时钟。
工作流程:编码器接口可接收增量(正交)编码器的信号,根据编码器旋转产生的正交信号脉冲,自动控制CNT自增或自减,从而指示编码器的位置、旋转方向和旋转速度

输出的两个方波信号相位相差 90°,超前 90° 或者滞后 90°,分别代表正转和反转,这就是正交编码器。

使用定时器的编码器接口,再配合编码器,就可以测量旋转速度和旋转方向了,这里编码器测速一般应用在电机控制的项目上。使用 PWM 驱动电机,再使用编码器测量电机的速度,然后再用 PID 算法进行闭环控制,这是一个比较常见的使用场景。一般电机旋转速度比较高,会使用无接触式的霍尔传感器或者光栅进行测速,我们这里为了方便,就使用这个触点式的旋钮编码器来演示。电机旋转就用人工旋转来模拟。当然实际使用的话,这个旋钮编码器和电机的霍尔、光栅编码器都是一样的效果。

实验现象:一个编码器有两个输出,一个是 A 相,一个是 B 相;然后接入到 STM32,定时器的编码器接口,编码器接口自动控制定时器时基单元中的 CNT 计数器进行自增或自减。比如初始化之后,CNT 初始值为 0,然后编码器右转,CNT 就 ++,右转产生一个脉冲,CNT 就加一次;比如右转产生十个脉冲,停下来,那么这个过程 CNT 就由 0 自增到 10,停下来;编码器左转,CNT 就 --,左转产生一个脉冲,CNT 减一次;比如编码器再左转产生 5 个脉冲,那 CNT 就在原来 10 的基础上自减 5,停下来。这个编码器接口,其实就相当于是一个带有方向控制的外部时钟。他同时控制着 CNT 的计数时钟和计数方向,这样的话,CNT 的值就表示了编码器的位置。如果我们每隔一段时间取一次 CNT 的值,再把 CNT 清零,是不是每次取出来的值就表示了编码器的速度。

这个编码器测速实际上就是测频法测正交脉冲的频率。CNT 计次,然后每隔一段时间取一次计次,这就是测频法的思路。只不过这个编码器接口计次更高级,它能根据旋转方向,不仅能自增计次,还能自减计次,是一个带方向的测速。

以上就是编码器接口的工作流程。

每个高级定时器和通用定时器都拥有1个编码器接口

这个编码器接口的资源还是比较紧张的,如果一个定时器配置成了编码器接口模式,那它基本上就干不了其他活了,我们这个 C8T6 芯片只有 TIM1、2、3、4 这 4 个定时器,所以最多只能接 4 个编码器,而且接完 4 个编码器,就没有定时器可以用了,所以如果你编码器比较多的话,需要考虑一下这个资源够不够用。不过实在不行的话,还是可以用外部中断来接编码器的,这样就是用软件资源来弥补硬件资源了。所以这里也可以看出,硬件资源和软件资源是互补的,硬件资源越多,软件资源就会越轻松,硬件不够,那就软件来凑。比如 PWM,我可以直接来个定时中断,然后在中断里手动计数,手动翻转电平。比如输入捕获,我可以来个外部中断,然后在中断里手动把 CNT 取出来,放在变量里,比如编码器接口,我也可以来个外部中断,然后在中断里手动自增或自减计数,这都可以实现功能。什么输出比较、输入捕获、编码器接口都不需要,但是这样,就是消耗软件资源了。所以一般有硬件资源的情况下,我们可以优先使用硬件资源,这样节约下来的软件资源,就可以去干更重要的事情。

两个输入引脚借用了输入捕获的通道 1 和通道 2。从结构框图可以看出来,编码器的两个输入引脚,就是定时器的 CH1 和 CH2 引脚,CH3 和 CH4 不能接编码器。

1.2 正交编码器

正交编码器一般可以测量位置,或者带有方向的速度值。
在这里插入图片描述

它一般有两个信号输出引脚,一个是 A 相,一个是 B 相,当编码器的旋转轴转起来的时候,A 相和 B 相就会输出这样的方波信号,转的越快,这个方波的频率就越高。所以方波的频率就代表了速度,我们取出任意一相的信号来测频率,就能知道旋转速度了。但是只有一相的信号,无法测量旋转方向,因为无论正转还是反转,它都是这样的方波,想要测量方向,还必须要有另一根线的辅助。比如我可以不要这个 B 相,再定义一个方向输出脚,正转置高电平,反转置低电平,这是一种解决方案,但这样的信号并不是正交信号;另一种解决方案,就是我们本节所说的正交信号。当正转时,A 相提前了 B 相 90°,反转时,A 相滞后 B 相 90°,当然这个正转,是 A 相提前还是 A 相滞后并不是绝对的,这只是一个极性问题。毕竟正转和反转的定义也是相对的。总之就是朝一个方向转就是 A 相提前,另一个方向是 A 相滞后。

使用正交信号相比于单独定义一个方向引脚,有什么好处呢?

  1. 正交信号精度更高,因为 A、B 相都可以计次,相对于计次频率提高了一倍
  2. 正交信号可以抗噪声,因为正交信号,两个信号必须是交替跳变的,所以可以设计一个抗噪声电路。如果一个信号不变,另一个信号连续跳变,也就是产生了噪声,那这时计次值是不会变化的。

正交信号如何计次和区分旋转方向呢?(由右表分析)

  1. 首先观察波形特点。在正转的时候,第一个时刻,A 相上升沿,对应的 B 相此时是低电平,也就是表里第一行;第二个时刻,B 相上升沿,对应的 A 相此时是高电平,也就是表里第三行;第三个时刻,A 相下降沿,对应的 B 相此时是高电平,也就是表里第二行;第四个时刻,B 相下降沿,对应的 A 相此时是低电平,也就是表里第四行。再然后就是 A 相上升沿,B 相低电平了,和第一个状态重复。所以在正转的时候,我们总结了右边这个表,当出现这 4 种边沿时,对应另一项的状态是这 4 种。那反转呢?第一个时刻,B 相上升沿,对应的 A 相此时是低电平,也就是表里第三行;第二个时刻,A 相上升沿,对应的 B 相此时是高电平,也就是表里第一行;第三个时刻,B 相下降沿,对应的 A 相此时是高电平,也就是表里第四行;第四个时刻,A 相下降沿,对应的 B 相此时是低电平,也就是表里第二行。然后把这 4 种状态也列一个表。这里就可以发现,当 A、B 相出现这些边沿时,对应另一相的状态,正转和反转正好是相反的。比如 A 相上升沿时,正转,B 相就是低电平,反转,B 相就是高电平。
  2. 所以我们编码器接口的设计逻辑就是,首先把 A 相和 B 相的所有边沿作为计数器的计数时钟,出现边沿信号时,就计数自增或自减,那到底是增还是减呢?这个计数的方向有另一相的状态来确定,当出现某个边沿时,我们判断另一相的高低电平,如果对应另一相的状态出现在上面这个表里,那就是正转,计数自增;反之,另一相的状态出现在下面这个表里,那就是反转,计数自减。这样就能实现编码器接口的功能了,这也是 STM32 定时器编码器接口的执行逻辑。

1.3 定时器编码器接口的框图

在这里插入图片描述
编码器接口输入部分:
编码器接口在触发控制器中,有两个输入端,分别要接到编码器的 A 相和 B 相,然后 TI1FP1 和 TI2FP2 是两个网络标号,对应着输入滤波器和边沿检测器后的 TI1FP1 和 TI2FP2。可以看出,编码器接口的两个引脚借用了输入捕获单元的前两个通道,所以最终编码器的两个输入引脚就是定时器的 CH1 和 CH2 这两个引脚。信号的通路是,CH1 通过输入滤波器和边沿检测器后的 TI1FP1,通向编码器接口,CH2 通过输入滤波器和边沿检测器后的 TI2FP2,通向编码器接口,CH3 和 CH4 与编码器接口无关,其中 CH1 和 CH2 的输入滤波器和边沿检测器,编码器接口也有使用。但是后面的是否交叉,预分频器和 CCR 寄存器,与编码器接口无关。

编码器接口输出部分:其实就相当于从模式控制器了,去控制 CNT 的计数时钟和计数方向。输出执行流程是:按照我们之前总结的那个表,如果出现了边沿信号,并且对应另一相的状态为正转,则控制 CNT 自增,否则,控制 CNT 自减。

注意在这里,我们之前一直在使用的 72 MHz 内部时钟 和 我们在时基单元初始化时设置的计数方向,并不会使用。因为此时计数时钟和计数方向都处于编码器接口托管的状态,计数器自增和自减,受编码器控制,这就是编码器接口的电路结构了。

1.4 编码器接口基本结构

在这里插入图片描述
输入捕获的前两个通道,通过 GPIO 口接入编码器的 A、B 相,然后通过滤波器和边沿检测极性选择,产生 TI1FP1 和 TI2FP2,通向编码器接口。编码器接口通过预分频器控制 CNT 计数器的时钟,同时,编码器接口还根据编码器的旋转方向,控制 CNT 的计数方向,编码器正转时,CNT 自增,编码器反转是,CNT 自减,另外 ARR 也是有效的,一般我们会设置 ARR 为 65535,最大量程,这样的话,利用补码的特性,很容易得到负数,比如 CNT 初始为 0,正转,CNT自增,这都没问题,但是反转,CNT 自减,0 下一个数就是 65535,接着是 65534、65533 等等,这里负数不应该是 -1,-2 么?65535 是不是出问题了,但是没关系,我们会做一个操作,直接把 16 位的无符号数转换为 16 位的有符号数,根据补码的定义,这个 65535 对应 -1,65534 对应 -2,65533 对应 -3 等等,这样就可以直接得到负数,非常方便。这就是我们读出数据得到负数的一个小技巧。

1.5 工作模式

编码器接口的工作逻辑。TI1FP1 和 TI2FP2 接的就是编码器的 A 相和 B 相。
在这里插入图片描述
在 A、B 相的上升沿或者下降沿触发计数,到底是向上计数还是向下计数取决于边沿信号发生的这个时刻,另一相的电平状态。也就是相对信号的电平,就是另一相电平的意思。

编码器分了三种工作模式:

  1. 仅在 TI1 计数
  2. 仅在 TI2 计数

只在一相的边沿计数,另一相的边沿忽略,也就是不计数。

  1. TI1 和 TI2 都计数

两相都计数相较于只有一相计数 计次精度 要高一些。一般情况下我们都会使用 两相计数模式,因为这个模式计数精度最高,上面这两个模式,如果有需求的话,可以了解一下。

总结一句话就是:正转的状态都向上计数,反转的状态都向下计数,这就是编码器接口指向的逻辑。

1.6 实例图(TI1、TI2均不反相)

使用两个引脚的边沿都计数的模式
在这里插入图片描述
TI1 和 TI2 的时序信号和计数器值的变化情况。

  1. TI1 上升沿,TI2 低电平,查表对应向上计数,所以计数器自增。
  2. 正交编码器抗噪声的原理:TI2 没有变化,TI1 跳变了好几次,这不符合正交编码器的信号规律。显然这里是一个毛刺信号,而通过我们上面这个表的逻辑,就可以把这个噪声滤掉。比如 TI1 上升沿,TI2 低电平,查表得向上计数,所以计数器自增,而下一个状态,TI1 下降沿,TI2 低电平,查表得向下计数,所以计数器自减,所以如果出现了一个引脚不变,另一个引脚连续跳变多次的毛刺信号,计数器就会加、减、加、减来回摆动,最终计数值还是原来那个数,并不受毛刺噪声的影响。
  3. 反转的波形,查表对应向下计数,所以计数器自减。
  4. TI1 不断,TI2 多次跳变,计数值也是来回摆动,过滤噪声。
  5. 最后正转,向上计数,计数器自增。

这个实例展示了什么时候向上计数、什么时候向下计数以及正交编码器抗噪声的原理。

1.7 实例图(TI1 反相、TI2 不反相)

在这里插入图片描述
这个实例展示的是极性的变化对技术的影响。

TI1 和 TI2 都会经过这个极性选择的部分,在输入捕获模式下这个极性选择是选择上升沿有效还是下降沿有效的。但是编码器接口显然始终都是上升沿和下降沿都有效的,上升沿和下降沿都需要计次,所以在编码器接口模式下,这里就不再是边沿的极性选择了,而是高低电平的极性选择,如果我们选择上升沿的参数,就是信号直通过来,高低电平极性不反转,如果选择下降沿的参数,就是信号通过一个非门进来,高低电平极性反转,所以极性选择部分会有两个控制极性的参数,选择要不要在这里加一个非门,反转一下极性。如果两个信号都不反转,就是均不反相;如果把 TI1 高低电平信号反转一下,就是 TI1 反相。

这里图上画的是输入信号,如果直接对照表,得到的计数方向就是相反的。

  1. 我们可以直接查表后将计数方向取反;
  2. 或者先将 TI1 高低电平取反,这才是反相后实际编码器接口的电平,然后再查表。

用途:比如你接一个编码器,发现它数据的加减方向反了,你想要正转方向,结果它自减了;你想要反转方向,结果它自增了。这时,就可以调整一下极性,把任意一个引脚反相,就能反转计数方向了。当然如果想直接改变计数方向的话,我们还可以直接把 A、B 相两相的引脚换一下,这也是可以的,有很多地方都可以调整极性,还是非常方便的。

2. 一个定时器编码器接口功能案例

2.1 编码器接口测速

2.1.1 硬件电路图

在这里插入图片描述
旋转编码器插在左边,VCC 和 GND 接上电源正负极,下面的 A 相输出,接到 PA6 引脚,B 相输出,接到 PA7 引脚,这里 PA6 和 PA7 两个引脚可以交换一下,就是正转和反转的极性不一样而已。

但是 PA6 和 PA7 这两个引脚不能随便更换,在引脚定义表中 PA6 和 PA7 是 TIM3 的通道 1 和 通道 2,我们计划用 TIM3 接编码器,所以需要接在 PA6 和 PA7 这两个引脚上。其他定时器也需要参考引脚定义表接在对应 TIM 的 CH1 和 CH2 这两个引脚的位置。

2.1.2 定时器编码器接口模块初始化

  • 定时器编码器接口模块的库函数
void TIM_EncoderInterfaceConfig(TIM_TypeDef* TIMx, uint16_t TIM_EncoderMode,
                                uint16_t TIM_IC1Polarity, uint16_t TIM_IC2Polarity);//配置定时器编码器接口函数(选择定时器,选择编码器模式,通道1 电平极性,通道 2 电平极性)选择 Rising 就是通道不反相,选择 Failing 就是通道反向
  • 配置定时器编码器接口模块
  1. RCC开启时钟,开启GPIO和定时器的时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);//开启 TIM3
  1. 配置 GPIO,把 PA6 和 PA7 配置成输入模式
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;//上拉输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);//GPIO 配置好默认低电平

上拉输入、下拉输入和浮空输入的选择原则:

  • 上拉和下拉如何选择,我们可以看一下接在这个引脚的外部模块输出的默认电平。
    • 如果外部模块空闲默认输出高电平,我们就选择上拉输入,默认输入高电平如果外部模块默认输出低电平,我们就选择下拉输入,默认输入低电平,和外部模块保持默认状态一致,防止默认电平打架,这是上拉和下拉的选择原则。
    • 不过一般来说,默认高电平,是一个习惯的状态,所以一般上拉输入用的比较多。
  • 如果你不确定外部模式输出的默认状态,或者外部信号输出功率非常小,这时就尽量选择浮空输入。浮空输入没有上拉电阻和下拉电阻去影响外部信号,但是缺点就是当引脚悬空时,没有默认电平了,输入就会受噪声干扰,来回不断地跳变。

这就是三种输入模式的选择原则。

  1. 配置时基单元,这里预分频器我们一般选择不分频,自动重装值一般给最大 65535,只需要 CNT 执行计数就行了
TIM_TimeBaseInitTypeDef TIMTimeBaseInitStructure;
TIMTimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;//指定时钟分频,决定滤波器的采样频率
TIMTimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;//计数器模式,这个参数没有作用,因为计数方向也是被编码器接口托管的。
TIMTimeBaseInitStructure.TIM_Period = 65536 - 1;//ARR:这里给最大值,也就是 16 位的计数器可以满量程计数,这样计数的范围是最大的,而且方便换算为负数
TIMTimeBaseInitStructure.TIM_Prescaler = 1 - 1;//PSC:这里给 0,就是不分频,编码器的时钟直接驱动 计数器
TIMTimeBaseInitStructure.TIM_RepetitionCounter = 0;//重复计数器的值(高级定时器才有),不需要用赋 0
TIM_TimeBaseInit(TIM3, &TIMTimeBaseInitStructure);
  • 编码器接口会托管时钟,编码器接口就是一个带有方向控制的外部时钟,所以内部时钟没有用
  • ARR:这里给最大值,也就是 16 位的计数器可以满量程计数,这样计数的范围是最大的,而且方便换算为负数
  • PSC:这里给 0,就是不分频,编码器的时钟直接驱动计数器
  • 计数器模式,这个参数没有作用,因为计数方向也是被编码器接口托管的。
  1. 配置输入捕获单元(只有滤波器和极性这两个参数有用,后面参数没有用到,与编码器无关)
TIM_ICInitTypeDef TIM_ICInitStructure;
TIM_ICStructInit(&TIM_ICInitStructure);目前结构体的配置是不完整的,为了防止结构体中出现不确定的值可能会造成的问题,最好用 structInit 给结构体赋一个初始值
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;//选择通道,因为 IC_Init 函数只有一个,所以要靠结构体这个参数来指定配置哪个通道
TIM_ICInitStructure.TIM_ICFilter = 0xF;//用来配置输入捕获的滤波器,如果信号有毛刺和噪声,可以增大滤波器参数,可以有效地避免干扰,数越大,滤波效果就越好
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;//极性,这里的上升沿参数代表的是高低电平极性不反转(对应不反相实例)
//TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;//触发信号分频器:没有用到
//TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;//选择触发信号从哪个引脚输入,用来配置数据选择器:没有用到
TIM_ICInit(TIM3, &TIM_ICInitStructure);//结构体变量的配置,调用 ICInit 函数之后,就写入到硬件的寄存器了,
	//所以 Init 之后,这个结构体我们可以换个值继续使用,不需要重新定义新的结构体
TIM_ICInitStructure.TIM_Channel = TIM_Channel_2;
TIM_ICInitStructure.TIM_ICFilter = 0xF;
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;
TIM_ICInit(TIM3, &TIM_ICInitStructure);

极性,这里的上升沿并不代表上升沿有效,因为编码器接口始终都是上升沿、下降沿都有效的,这里的上升沿参数代表的是高低电平极性不反转(对应不反相实例),下降沿参数代表的是高低电平极性反转(对应反相实例)。这个极性参数等会我们配置编码器接口的时候也有,属于重复配置了,这里这个其实可以删掉。

结构体变量的配置,调用 ICInit 函数之后,就写入到硬件的寄存器了,所以 Init 之后,这个结构体我们可以换个值继续使用,不需要重新定义新的结构体

  1. 配置编码器接口模式,直接调用一个库函数就可以了
TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);

后两个参数和上面两个参数是一样的,实际效果确实是一样的,这两个地方的参数,其实都配置的是同一个寄存器,属于重复配置了,后配置的参数会覆盖前面的参数,所以我们可以把这前面这个极性的参数也删掉。只使用后面这个函数来配置极性。不过要注意,一定要保证 Encoder 函数位于 ICInit 函数下面,否则就是 ICInit 覆盖 Encoder 函数的配置了,最后 ICInit 参数就只剩一个滤波器还有用了。

  1. 调用 TIM_Cmd,启动定时
TIM_Cmd(TIM3, ENABLE);

2.1.3 示例代码

Timer.h

#ifndef __TIMER_H
#define __TIMER_H

void Timer_Init(void);
uint16_t Timer_GetCount(void);

#endif

Timer.cpp

#include "stm32f10x.h"                  // Device header

void Timer_Init(void) {
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);//TIM2 是 APB1 总线的外设

	TIM_InternalClockConfig(TIM2);

	TIM_TimeBaseInitTypeDef TIMTimeBaseInitStructure;
	TIMTimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;//指定时钟分频,决定滤波器的采样频率
	TIMTimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;//计数器模式,向上计数
	//定时 1 s(记公式/理解工作流程)
	TIMTimeBaseInitStructure.TIM_Period = 10000 - 1;//ARR 的值(取值范围0~65535)
	TIMTimeBaseInitStructure.TIM_Prescaler = 7200 - 1;//PSC 的值(取值范围0~65535)
	TIMTimeBaseInitStructure.TIM_RepetitionCounter = 0;//重复计数器的值(高级定时器才有),不需要用赋 0
	TIM_TimeBaseInit(TIM2, &TIMTimeBaseInitStructure);
	TIM_ClearFlag(TIM2, TIM_FLAG_Update);//手动清除更新中断标志位

	TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);//配置更新中断输出

	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;//定时器2 在 NVIC 中的通道
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
	NVIC_Init(&NVIC_InitStructure);

	TIM_Cmd(TIM2, ENABLE);
}

uint16_t Timer_GetCount(void) {
	return TIM_GetCounter(TIM2);
}

Encoder.h

#ifndef __ENCODER_H
#define __ENCODER_H

void Encoder_Init(void);
int16_t Encoder_Get(void);

#endif

Encoder.cpp

#include "stm32f10x.h"                  // Device header

int16_t Encoder_Count;

void Encoder_Init(void) {
//1.RCC开启时钟,开启GPIO和定时器的时钟
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);//开启 TIM3

//2.配置 GPIO,把 PA6 和 PA7 配置成输入模式
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;//上拉输入
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);//GPIO 配置好默认低电平
	
//3.配置时基单元,这里预分频器我们一般选择不分频,自动重装值一般给最大 65535,只需要 CNT 执行计数就行了
	TIM_TimeBaseInitTypeDef TIMTimeBaseInitStructure;
	TIMTimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;//指定时钟分频,决定滤波器的采样频率
	TIMTimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;//计数器模式,这个参数没有作用,因为计数方向也是被编码器接口托管的。
	TIMTimeBaseInitStructure.TIM_Period = 65536 - 1;//ARR:这里给最大值,也就是 16 位的计数器可以满量程计数,这样计数的范围是最大的,而且方便换算为负数
	TIMTimeBaseInitStructure.TIM_Prescaler = 1 - 1;//PSC:这里给 0,就是不分频,编码器的时钟直接驱动 计数器
	TIMTimeBaseInitStructure.TIM_RepetitionCounter = 0;//重复计数器的值(高级定时器才有),不需要用赋 0
	TIM_TimeBaseInit(TIM3, &TIMTimeBaseInitStructure);
	
//4.配置输入捕获单元(只有滤波器和极性这两个参数有用,后面参数没有用到,与编码器无关)
	TIM_ICInitTypeDef TIM_ICInitStructure;
	TIM_ICStructInit(&TIM_ICInitStructure);//给结构体赋一个初始值
	TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;//选择通道,因为 IC_Init 函数只有一个,所以要靠结构体这个参数来指定配置哪个通道
	TIM_ICInitStructure.TIM_ICFilter = 0xF;//用来配置输入捕获的滤波器,如果信号有毛刺和噪声,可以增大滤波器参数,可以有效地避免干扰,数越大,滤波效果就越好
	TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;//极性,这里的上升沿参数代表的是高低电平极性不反转(对应不反相实例)
	//TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;//触发信号分频器:没有用到
	//TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;//选择触发信号从哪个引脚输入,用来配置数据选择器:没有用到
	TIM_ICInit(TIM3, &TIM_ICInitStructure);//结构体变量的配置
	TIM_ICInitStructure.TIM_Channel = TIM_Channel_2;
	TIM_ICInitStructure.TIM_ICFilter = 0xF;
	TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;
	TIM_ICInit(TIM3, &TIM_ICInitStructure);
	
//5.配置编码器接口模式,直接调用一个库函数就可以了
	TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);
	
//6.调用 TIM_Cmd,启动定时
	TIM_Cmd(TIM3, ENABLE);
}

int16_t Encoder_Get(void) {//要求读完后清零 CNT
	int16_t tmp;
	tmp = TIM_GetCounter(TIM3);
	TIM_SetCounter(TIM3, 0);
	return tmp;
}

电路初始化完成之后,CNT 就会随着编码器旋转而自增自减,如果想要测量编码器的位置,那直接读出 CNT 的值就行了。如果想要测量编码器的速度和方向,那就需要每隔一段固定的闸门时间,取出一次 CNT,然后再把 CNT 清零,这样就是测频法测量速度了

main.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "Timer.h"
#include "Encoder.h"

int16_t Speed;

int main(void) {
	OLED_Init();
	Timer_Init();
	Encoder_Init();
	
	OLED_ShowString(1, 1, "Speed:");
	
	while(1){
		OLED_ShowSignedNum(1, 7, Speed, 5);//显示有符号的整数
		//Delay_ms(1000);//闸门时间
	} 
}

void TIM2_IRQHandler(void) {
	if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET) {
		Speed = Encoder_Get();
		
		TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
	}
}

闸门时间:如果是电机飞速旋转的话,闸门时间就可以给短点,这样可以提高速度刷新的频率,而且防止计数器溢出。
注意事项:最好不要在主循环加入过长的 delay,这样会阻塞主循环的执行,比较好的方法是使用定时中断。

旋钮编码器每转一格,A 相产生一个低电平的脉冲,B 相产生一个相位差 90° 的脉冲,提前还是滞后,取决于正转还是反转,如果连续转动,就和实例图的波形是一样的了。那可以看出,现在编码器转动一格,A、B 相各出现了一个下降沿和上升沿,所以计次总共加/减了四次,当然如果你是电机的编码器,那就不会有这个段落感。

反转到 0 后再反转一格,可以看到, 0 再自减,计数器反向溢出,回到自动重装值,65535,再继续往下减,这就是目前我们使用 uint16_t 数据的现象,那如果我们想要 0 之后变为 -1,就直接把 uint16_t 类型强制转换成 int16_t ,在显示时显示有符号的整数就行了。

目前我们这个代码是编码器测量位置,如果需要测量位置的话,就直接 GetCounter 就行了。如果我们想用这个编码器来测速的话,就可以在固定的闸门时间,读一次 CNT,然后把 CNT 清零。

极性修改:

  1. 在硬件层面,我们可以把 A、B 相两根线换一下。
  2. 在软件层面,我们可以修改两个输入通道的极性,把任意一个极性反转一下,方向就会反过来;如果两个极性都反转,那极性还是保持不变。
  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值