本次课程采用单片机型号为STM32F103C8T6。
课程链接:江科大自化协 STM32入门教程
往期笔记链接:
STM32学习笔记(一)丨建立工程丨GPIO 通用输入输出
STM32学习笔记(二)丨STM32程序调试丨OLED的使用
STM32学习笔记(三)丨中断系统丨EXTI外部中断
STM32学习笔记(四)丨TIM定时器及其应用(定时中断、内外时钟源选择)
STM32学习笔记(五)丨TIM定时器及其应用(输出比较丨PWM驱动呼吸灯、舵机、直流电机)
STM32学习笔记(六)丨TIM定时器及其应用(输入捕获丨测量PWM波形的频率和占空比)
如果上一篇笔记的内容为史诗级副本,本篇文章的内容我愿称之为传说级副本(四)。
编码器接口(Encoder Interface),可以接收正交编码器(又称为增量编码器)的信号,根据编码器旋转产生的正交信号脉冲,自动由硬件电路控制定时器时基单元中的计数器CNT自增或自减,从而指示编码器的位置、旋转方向和旋转速度。从原理上看,编码器接口输出的信号就相当于一个带有方向控制的外部时钟,同时控制CNT的计数时钟和计数方向。这时,CNT的值就代表了编码器的位置。如果每隔一段时间将CNT的值取出并清0,我们就能得到编码器在这一段时间内的旋转速度(注意,不是旋转速率,这里的速度只与这段时间的长短和始末位置有关),思路与测频法测正交脉冲的频率相似,只不过这里的计数器的计数方向由编码器接口决定。
正交(增量)编码器
两个输出引脚输出的方波信号相位相差90°的编码器称为正交(增量)编码器。一个方波信号相对于另一个方波信号超前或滞后90°代表正传或反转。
要测量编码器的旋转方向,还可以使用一个引脚输出方波表示旋转角度和速度,另一个引脚输出旋转方向:正传输出高电平,反转输出低电平。但相较于这一种方案,使用正交编码器的优势在于:
- 正交信号精度更高。正交信号的A、B相都可以计次,且相位相差90°,相当于计次频率(计数器的变化频率)提高了一倍。
- 正交信号可以在一定程度上免除噪声的干扰。对于正交信号而言,两个信号必须交替跳变,CNT的值才会自增或自减。如果一个信号不变,另一个信号连续跳变,则CNT的值是不会变化的。
每个高级定时器和通用定时器都拥有1个编码器接口,且功能上没有差别。编码器接口的两个输入引脚借用了输入捕获的通道1(TIMx_CH1)和通道2(TIMx_CH2)。由于STM32中定时器的数量有限,编码器接口占用的硬件资源还是很大的,如果一个定时器被配置为编码器接口模式,那这个定时器将很难完成其他功能。
STM32的软硬件资源是互补的。如果在实际应用中,出现了硬件资源不足的情况,可以使用软件资源作弥补,对于软件也是相同。例如,我们可以通过外部中断来实现类似编码器接口测速的功能;对于PWM,也可以在定时中断中手动翻转引脚电平来达到类似的效果;输入捕获的功能也可以由外部中断完成,可以在中断中手动将CNT取出并放在变量里,然后再清0……在有硬件资源的条件下,应该优先使用硬件资源,节约出的软件资源可以完成更重要的任务。
编码器接口借用了定时器的CH1和CH2通道,与CH3和CH4无关。且输入捕获前端的输入滤波和极性选择部分也有使用。编码器接口的输出部分,相当于一个从模式控制器,控制CNT的自增和自减。此时72Hz内部时钟和时基单元初始化时设置的计数方向都处于失效的状态,因为此时的计数时钟和计数方向都处于编码器接口托管的状态。 所以当一个定时器被配置为编码器接口模式,这个定时器就很难完成其他工作了,因为编码器接口直接控制时基单元的时钟来源。
在编码器接口模式下ARR也是有效的,一般将ARR设置为最大量程,这样利用补码的特性,很容易得到负数。
编码器接口的设计逻辑为:把A相和B相的所有边沿作为计数器的计数时钟,出现边沿信号时,就计数自增或自减,此时增减由另一相的状态确定。当然,也可以仅在一相进行计数,忽略另一相,只不过这样会牺牲一部分计数的精度。
TI1FP1和TI2FP2均不反相时,计数器的操作实例如下图所示。下图同时也展示了编码器接口的抗噪声原理:当一个信号不变,另一个信号连续跳变,计数器就会来回加减摆动,最终计数值不变。
TI1FP1反相,TI2FP2不反相时,计数器的操作实例如下图所示。在输入捕获模式下,极性选择模块是用来选择上升沿有效还是下降沿有效的,但编码器接口模式上升沿和下降沿都有效,所以在编码器接口模式下,极性选择不再是边沿的极性选择,而是高低电平的极性选择。
这里反相的作用是:如果在编码器模块的使用过程中,发现数据的加减方向反了,这时就可以将任意一个引脚反相,就可以反转计数方向了。当然,也可以直接把A相和B相的输入引脚更换一下。
本小节要实现的实验现象比较简单,这里采用编码器接口获取CNT中的值,在Encoder_Get
函数中手动将CNT清0,并且采用定时中断的方法,定时1s取一次CNT的值。
Timer.c
(使用TIM2定时,每1s触发一次定时中断)#include "stm32f10x.h" // Device header
void Timer_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
TIM_InternalClockConfig(TIM2);
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStructure.TIM_Period = 10000 - 1;
TIM_TimeBaseInitStructure.TIM_Prescaler = 7200 - 1;
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
TIM_ClearFlag(TIM2, TIM_FLAG_Update); // 避免刚初始化完成就进入中断函数的问题
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
// 初始化NVIC
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; // 响应优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; // 抢占优先级
NVIC_Init(&NVIC_InitStructure);
TIM_Cmd(TIM2, ENABLE);
}
Encoder.c
(使用TIM3配置为编码器接口模式)#include "stm32f10x.h" // Device header
void Encoder_Init(void)
{
// 1. RCC开启时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
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);
// 3. 配置时基单元
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; // 这里计数模式失效
TIM_TimeBaseInitStructure.TIM_Period = 65536 - 1;
TIM_TimeBaseInitStructure.TIM_Prescaler = 1 - 1;
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure);
// 4. 配置输入捕获单元
TIM_ICInitTypeDef TIM_ICInitStruct;
TIM_ICStructInit(&TIM_ICInitStruct); // 给结构体赋一个初始值防止未知问题
// 配置CH1
TIM_ICInitStruct.TIM_Channel = TIM_Channel_1;
TIM_ICInitStruct.TIM_ICFilter = 0xF;
// TIM_ICInitStruct.TIM_ICPolarity = TIM_ICPolarity_Rising;
// 这里的上升沿并不代表上升沿有效,这里代表高低电平极性不翻转
// 它与TIM_EncoderInterfaceConfig中的参数操作的是同一个寄存器,属于重复配置
TIM_ICInit(TIM3, &TIM_ICInitStruct);
// 配置CH2
TIM_ICInitStruct.TIM_Channel = TIM_Channel_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(TIM3, ENABLE);
}
int16_t Encoder_Get(void)
{
int16_t Temp;
Temp = TIM_GetCounter(TIM3); // 这里TIM_GetCounter返回一个uint16_t类型的值,利用补码很方便地将正数变为负数,例如65535变为-1
TIM_SetCounter(TIM3, 0);
return Temp;
}
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Timer.h"
#include "Encoder.h"
int16_t Speed;
int main()
{
OLED_Init();
Timer_Init();
Encoder_Init();
OLED_ShowString(1, 1, "Speed:");
while(1)
{
// 一般不建议以下做法,容易阻塞主程序
// OLED_ShowSignedNum(1, 5, Encoder_Get(), 5);
// Delay_ms(1000);
OLED_ShowSignedNum(1, 7, Speed, 5);
}
}
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)
{
Speed = Encoder_Get();
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}
原创笔记,码字不易,欢迎点赞,收藏~ 如有谬误敬请在评论区不吝告知,感激不尽!博主将持续更新有关嵌入式开发、机器学习方面的学习笔记~