【江协STM32】6-7/8 TIM编码器接口、编码器接口测速
1. 编码器接口简介
2. 正交编码器
可以测量位置,或带有方向的速度值。
方波的频率代表速度,方向可以根据右侧表格判断。
3. 编码器接口基本结构
输入部分:编码器接口的两个引脚使用了输入捕获单元的CH1和CH2
输出部分:相当于从模式控制器,控制CNT的计数时钟和计数方向。此时,不会使用72MHz内部时钟和时基单元初始化时设置的计数方向,因为此时计数时钟和计数方向均处于编码器接口托管的状态,计数器的自增和自减受编码器控制
编码器接口根据编码器的旋转方向控制CNT的计数方向,编码器正转时CNT自增,编码器反转时CNT自减。ARR一般设置为最大量程65535,负数可以通过补码获得。
4. 编码器接口的工作模式
可以参考“2.正交编码器”中的图表。如果表中所有情况都计数,就是“在TI1和TI2上计数”;如果只在A相的上升和下降沿计数,可以是“仅在TI1计数”,则只在B相的上升和下降沿计数就为“仅在TI2计数”。

输入信号经过滤波器后,都会经过极性选择的部分。在输入捕获模式下,极性选择用来选择上升沿有效或下降沿有效。编码器接口,始终是上升沿和下降沿都有效,所以在此模式下为高低电平的极性选择,如果选择上升沿的参数,就是信号直通,高低电平极性不反转;如果选择下降沿的参数,就是信号通过一个非门,高低电平极性反转。
如果把TI1的高低电平反转一下,如下图。分析时需要先把TI1的高低电平取反,再查表。
极性反转的作用:比如接一个编码器,发现数据的加减方向反了,想要正转的方向结果程序自减,这时,就可以调整极性,把任意一个引脚反相,就能反转计数的方向了。当然如果想改变计数方向,还可以直接把A、B相两个引脚换一下。

5. 编码器接口测速
与旋转编码器计次代码(【江协STM32】5 EXTI外部中断,第2.2节)实现的功能基本一致,本节所写代码本质上也是旋转编码器计次。但本节代码是通过定时器的编码器接口实现自动计次,可以节约软件资源。而之前的代码是通过触发外部中断,然后在中断函数中手动计次,当电机高速旋转时,编码器每秒产生大量脉冲,程序会频繁进入中断,占用软件资源。
5.1 代码现象
OLED显示旋转速度,向右慢速旋转,数值为正,计次比较小,向右快速旋转,计次增大,向左数值为负。
5.2 接线图
旋转编码器的A相接PA6(TIM3_CH1),B相接PA7(TIM3_CH2)
5.3 代码
编码器初始化步骤:
- RCC开启GPIO和定时器的时钟
- 配置GPIO,需要把PA6和PA7配置成输入模式
- 配置时基单元,PSC一般选择不分频,ARR一般给65535,只需要CNT执行计数
- 配置输入捕获单元,这里只有滤波器和极性两个参数有用
- 配置编码器接口模式
- 调用TIM_Cmd启动定时器
GPIO的模式可以选择上拉、下拉或浮空。对于上拉和下拉的选择,一般可以看引脚的外部模块输出的默认电平,如果外部模块空闲默认输出高电平,就选择上拉输入,默认输入高电平;如果外部模块默认输出低电平,配置下拉输入,默认输入低电平。如果不确定外部模块输出的默认状态,或者外部信号输出功率非常小,尽量选择浮空输入。
Encoder.c
#include "stm32f10x.h" // Device header
void Encoder_Init(void)
{
// 1、初始化时钟RCC TIM3
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);// TIM2是APB1总线的外设
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// 2、配置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_InternalClockConfig(TIM3);// 因为定时器上电后默认就是使用内部时钟,所以此行可省略
// 3、配置时基单元
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;// 指定时钟分频,设置的是输入滤波的采样频率,与时基单元关系不大。这里随便选一个
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;// 计数器模式,选择向上计数
// 计数器溢出频率:CK_CNT_OV=CK_CNT/(ARR+1)=CK_PSC/(PSC+1)/(ARR+1)=72M/(PSC+1)/(ARR+1)
// 注意:PSC和ARR的取值都要在0~65535之间
TIM_TimeBaseInitStructure.TIM_Period = 65536 - 1;// 自动重装器(ARR)的值
TIM_TimeBaseInitStructure.TIM_Prescaler = 1 - 1;// 预分频器(PSC)的值,不分频
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;// 重复计数器的值(高级定时器才有,不是CNT),不需要用,直接给0
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure);// 刚初始化完会生成一次更新事件,会使Num刚复位就显示1
// 4、初始化输入捕获单元
TIM_ICInitTypeDef TIM_ICInitStruct;
TIM_ICStructInit(&TIM_ICInitStruct);
TIM_ICInitStruct.TIM_Channel = TIM_Channel_1;// 配置通道1
TIM_ICInitStruct.TIM_ICFilter = 0xF;// 配置输入捕获的滤波器
// TIM_ICInitStruct.TIM_ICPolarity = TIM_ICPolarity_Rising;// 可以删掉,在配置编码器接口时也设置了该属性。边沿检测,选择极性。高低电平极性不反转。
TIM_ICInit(TIM3, &TIM_ICInitStruct);
TIM_ICInitStruct.TIM_Channel = TIM_Channel_2;// 配置通道2
TIM_ICInitStruct.TIM_ICFilter = 0xF;// 配置输入捕获的滤波器
// 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);
// 6、调用TIM_Cmd启动定时器
TIM_Cmd(TIM3, ENABLE);
}
// 函数类型uint16_t(无符号数)时,旋转编码器从0反向转时会从65535减小
// 把函数类型换成int16_t(有符号数)时,旋转编码器从0反向转时就会显示负数(借用补码特性)
// 同时OLED显示函数需要由OLED_ShowNum换成OLED_ShowSignedNum
int16_t Encoder_Get(void)
{
// 如果用编码器测速,可以在固定的闸门时间读一次CNT,然后把CNT清零
int16_t Temp;
Temp = TIM_GetCounter(TIM3);
TIM_SetCounter(TIM3, 0);// 清零CNT
return Temp;
}
Encoder.h
#ifndef __ENCODER_H
#define __ENCODER_H
void Encoder_Init(void);
int16_t Encoder_Get(void);
#endif
main.c
#include "stm32f10x.h" // Device
#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_ShowSignedNum(1, 5, Encoder_Get(), 5);
Delay_ms(1000);// 闸门时间1s。每隔一段时间Encoder_Get一次。如果主程序有其他代码,会堵塞程序的执行,因此最好用中断的方式。
}
}
上述主函数使用延时函数来控制闸门时间,可以实现功能。如果主程序有其他代码,会堵塞程序的执行,因此最好用中断的方式。优化后的代码如下:
main.c
#include "stm32f10x.h" // Device
#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);
}
}
// 定时器2中断函数,定时1s
void TIM2_IRQHandler(void)
{
if(TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)// 检查中断标志位
{
Speed = Encoder_Get();// 每隔1s读取一下速度,存在Speed变量里
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);// 清除中断标志位
}
}
其他引用的头文件和c代码可在此处查阅:OLED.h(【江协STM32】4 OLED调试工具,第5节)、 Delay.h(【江协STM32】3-2 LED闪烁&LED流水灯&蜂鸣器,第1.3节)、Timer.h(【江协STM32】6-1/2 TIM定时中断、定时器定时中断&定时器外部时钟,第2.2节)
作者:冰糖雪莲IO