对应手册编码器接口14.3.12
实现代码
实现旋转编码器计次,与之前的在定时器中断时实现的旋转编码器计次实现内容相同,但是方式不同,之前的是通过触发外部中断,通过中断函数来实现手动计次加一;这次不同,是通过定时器的编码器接口来自动计次,可以节约软件资源。
之前使用外部中断来实现旋转编码器计次,当电机高速旋转时,编码器每秒产生成千上万个脉冲,会频繁进入中断,而完成的内容却是简单的+1,这导致软件资源被这种简单而又低级的工作占用了,对于这种简单且需要频繁执行的任务,我们可以交给定时器的编码器接口来自动计次。
每隔一段时间来获取旋转编码器的计次值,就可以得到旋转编码器旋转的速度了
简介
使用编码器接口计次旋转编码器的速度实质上就是上一节所说的测频法的思路
正交编码器
当编码器的旋转轴转起来时,编码器就会输出如图的正交波,旋转得越快,方波频率就越高,则波的频率即是旋转的速度,A,B中任意一相就可测出旋转的速度,两相的目的是用于确定旋转的方向,当旋转为正向时,B相会滞后90°;反向旋转时,B相则会提前90°。这些并不是绝对的,这些是极性问题,正转和反转也是相对的。
双相都输出为正交信号有什么好处呢?首先是正交信号精度更高,因为AB相都是正交信号,都可以计次,计次频率提高了一倍;其次是正交信号可以抗噪声,比如A相信号不变,B相信号却发生很大的波动,连续跳变,会被判定为噪声被剔除掉,这时计次值是不会变化的。
设计逻辑:把AB相的所有边沿都作为计数器的计数时钟,出现边沿信号时,就计数自增或者自减,计数的方向由另一相的状态来决定。
电路结构
可见编码器接口接在CH1和CH2,同时也使用了输入滤波器和边沿检测器
编码器接口的输出部分,相当于从模式控制器了,去控制CNT的计数时钟和计数方向(此时这里不会用到72MHz内部时钟和在时基单元初始化时设置的计数方向,计数时钟和计数方向完全被编码器接口托管,由其决定计数方向)
编码器接口基本结构
如图所示, GPIO口接入编码器的AB相,然后通过滤波器和边沿检测极性选择,在分别通往编码器接口,编码器接口通过预分频器控制CNT计数器的时钟,同时编码器接口还会根据编码器的旋转方向控制CNT的计数方向。
一般我们设置ARR为最大值65535,这样可以利用补码的特性,容易得到负数。
(反转会导致CNT自减,0下一个数时65535,而不是负数,所以我们会做一个操作,直接把这个16位无符号数转换为16位有符号数,其中65535对应-1)
工作模式
如图,检测到TI1FP1正处于上升沿,则会继续检测TI1FP2是处于高电平还是低电平,若处于高电平,则向下计数,即计数器自减;若处于低电平,则向上计数,即计数器自增。
实例
(均不反相)
对抗噪声,如图所示,不符合规范的波会导致计数器自增一下自减一下,即不影响结果
TI1反相
反相,即极性选择中选择反相,TI1反相即把高低电平置换,会得出与不反相刚好相反的计数器计数,这种情况用于调整计数方向,当我们发觉计数方向和自己的需求不同时,采用这种方法得到自己想要的计数方向
代码实操
计划用TIM的CH1和CH2
我们需要新学的函数有
定时器编码器接口配置(定时器,编码器模式,CH1的极性,CH2的极性)
void TIM_EncoderInterfaceConfig(TIM_TypeDef* TIMx, uint16_t TIM_EncoderMode,
uint16_t TIM_IC1Polarity, uint16_t TIM_IC2Polarity);
1、开启GPIO和定时器的时钟
//开启时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
2、配置GPIO口,需要把PA6和PA7配置为输入模式
//配置GPIO
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);
3、配置时基单元(不分频)
//时基单元初始化
TIM_TimeBaseInitTypeDef TimeBaseInitStructure;
//指定时钟分频(与本次操作没太大关系)
TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
//计数器模式(编码器接手托管,与本次操作无关)
TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
//ARR(防止测量的频率太小,导致计数溢出)
TimeBaseInitStructure.TIM_Period = 65536 - 1;
//PSC(给0,不分频,编码器的时钟直接驱动计数器)
TimeBaseInitStructure.TIM_Prescaler = 1 - 1;
//重复计数器的值(高级计数器特有的,我们没有直接赋0)
TimeBaseInitStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM3, &TimeBaseInitStructure);
4、配置输入捕获单元
//配置输入捕获单元的滤波器和边沿检测极性选择
//用不到的参数由结构体初始化函数配置初始值
//CH1
TIM_ICInitTypeDef TIM_ICInitStruct;
TIM_ICStructInit(&TIM_ICInitStruct);
TIM_ICInitStruct.TIM_Channel = TIM_Channel_1;
TIM_ICInitStruct.TIM_ICFilter = 0x0F;
//边沿检测,极性选择(极性不反转)
TIM_ICInitStruct.TIM_ICPolarity = TIM_ICPolarity_Rising;
TIM_ICInit(TIM3, &TIM_ICInitStruct);
//CH2
TIM_ICStructInit(&TIM_ICInitStruct);
TIM_ICInitStruct.TIM_Channel = TIM_Channel_2;
TIM_ICInitStruct.TIM_ICFilter = 0x0F;
//边沿检测,极性选择(极性不反转)
TIM_ICInitStruct.TIM_ICPolarity = TIM_ICPolarity_Rising;
TIM_ICInit(TIM3, &TIM_ICInitStruct);
5、配置编码器接口模式
//配置编码器接口
TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);
在这我们会发现这里也定义了一边CH1和CH2引脚的极性选择,所以我们在配置输入捕获单元时,可以不用配置CH1和CH2的极性选择,在配置编码器接口模式中陪置即可
6、启动定时器
TIM_Cmd(TIM3, ENABLE);
总体(还加了一个获取CNT值的函数)
#include "stm32f10x.h" // Device header
void Encoder_Init(void)
{
//开启时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
//配置GPIO
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);
//编码器会托管时钟,所以就不用配置内部时钟了
//时基单元初始化
TIM_TimeBaseInitTypeDef TimeBaseInitStructure;
//指定时钟分频(与本次操作没太大关系)
TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
//计数器模式(编码器接手托管,与本次操作无关)
TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
//ARR(防止测量的频率太小,导致计数溢出)
TimeBaseInitStructure.TIM_Period = 65536 - 1;
//PSC(给0,不分频,编码器的时钟直接驱动计数器)
TimeBaseInitStructure.TIM_Prescaler = 1 - 1;
//重复计数器的值(高级计数器特有的,我们没有直接赋0)
TimeBaseInitStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM3, &TimeBaseInitStructure);
//配置输入捕获单元的滤波器和边沿检测极性选择
//用不到的参数由结构体初始化函数配置初始值
//CH1
TIM_ICInitTypeDef TIM_ICInitStruct;
TIM_ICStructInit(&TIM_ICInitStruct);
TIM_ICInitStruct.TIM_Channel = TIM_Channel_1;
TIM_ICInitStruct.TIM_ICFilter = 0x0F;
TIM_ICInit(TIM3, &TIM_ICInitStruct);
//CH2
TIM_ICStructInit(&TIM_ICInitStruct);
TIM_ICInitStruct.TIM_Channel = TIM_Channel_2;
TIM_ICInitStruct.TIM_ICFilter = 0x0F;
TIM_ICInit(TIM3, &TIM_ICInitStruct);
//配置编码器接口
TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);
TIM_Cmd(TIM3, ENABLE);
}
uint16_t Encoder_Get(void)
{
return TIM_GetCounter(TIM3);
}
在主函数中调用一下,看看CNT是否准确
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Timer.h"
#include "Encoder.h"
int main(void)
{
OLED_Init();
Encoder_Init();
OLED_ShowString(1,1,"CNT:");
while(1)
{
OLED_ShowNum(1, 5, Encoder_Get(), 5);
}
}
可以看到结果正确
但是反转自减从0开始会变65535,我们只需强制转换类型即可
int16_t Encoder_Get(void)
{
return TIM_GetCounter(TIM3);
}
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Timer.h"
#include "Encoder.h"
uint16_t Num;
int main(void)
{
OLED_Init();
Encoder_Init();
//Timer_Init();
OLED_ShowString(1,1,"CNT:");
while(1)
{
OLED_ShowSignedNum(1, 5, Encoder_Get(), 5);
}
}
这样反转就可以显示负数了
拓展
如果目前的正转方向和我们想要的不一样,我们有两种方法来修改正转的定义
1、直接更改接线,把旋转编码器的两根线调换即可
2、修改代码,在配置编码器接口时,我们定义了两个端口的极性选择,我们只需把其中的一个改为与其之前相反的极性即可
//原
TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);
//修改极性后
TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12, TIM_ICPolarity_Falling, TIM_ICPolarity_Rising);
使用编码器接口测速
要实现测量旋转编码器的速度,就需要设定一个闸门时间,在这段时间内旋转多少,再除以时间,即是速度。
首先每过一次闸门时间,CNT的计数都需要清零,由此我们需要改编获取CNT值的函数
int16_t Encoder_Get(void)
{
int16_t temp = TIM_GetCounter(TIM3);
TIM_SetCounter(TIM3, 0);
return temp;
}
main.c(使用Delay的方法,每过1s,获取一次CNT的值,即Speed=CNT/1)
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Timer.h"
#include "Encoder.h"
uint16_t Num;
int main(void)
{
OLED_Init();
Encoder_Init();
OLED_ShowString(1,1,"Speed:");
while(1)
{
OLED_ShowSignedNum(1, 7, Encoder_Get(), 5);
Delay_ms(1000);
}
}
这样做不太妥当,假如主函数中还有其他的程序需要运行,使用Delay函数可能会阻塞主循环的运行,所以我们可以通过定时中断来实现每隔一秒取一次CNT的值。
在定时中断中我们就已经设置好了是每个1s执行一次中断
#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();
Encoder_Init();
Timer_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);
}
}