一、 简介
本章讲解旋转编码器驱动方式,一种为普通GPIO边沿检测方式,一种为定时器编码器方式,各有优缺点,当资源不足时可以选择第一种但是精度不如定时器编码器方式,资源充足时建议时间定时器方式较优且可靠。
二、旋转编码器原理
2.1 介绍:
相信很多都知道旋转编码器这个东西,但是也不免新人见过但是没听过此名词故而不了解,因此请看下图!!! 这就是常见的旋转编码器,和电位器很像,但是原理和应用不一样,它一般用于音响系统、数控机床、机器人、自动化设备、伺服控制系统等。
2.2 模块电路原理:
可见外围电路还是相当简单,SW1引出的TRIM_A/B是编码器的AB相脉冲,PUSH是按下的按键电平翻转,这三个通道空闲都是上拉。
2.3 正反转检测原理:
编码器给出两相方波,可见下图的波形A/B相位差90°,我们就叫A/B通道,其中的一个通道给出与转速有关的信息,同时与另外一个通道对比,就能知道旋转的方向。
可见下图编码器盘上有两个通道(A 和 B),每个通道在不同位置有通断的状态,可见图中间的S1标识,如果 S1向右转动那么A 通道的信号先于 B 通道发生变化,则表示顺时针旋转,如果S1向左转动 B 通道的信号先于 A 通道发生变化,则表示逆时针旋转。
三、驱动方式一
3.1 简介:
此驱动方式以普通GPIO边沿检测方式,优点是不占用定时器资源,普通的GPIO配置成外部中断即可使用,缺点是响应慢、精度低、抗噪声能力差。
3.2 原理:
当脉冲来临时,触发外部中断进入服务函数,通过检测旋转编码器A通道和B通道的电平变化来确定编码器的旋转方向,并相应地增加或减少计数。当中断触发时,它检查当前通道的状态与之前保存的状态是否不同,以判断是否有旋转发生。然后,通过比较A通道和B通道的相对状态(即是否相同),来决定是正转还是反转:如果两个通道状态不同则认为是正转,增加计数;如果相同则认为是反转,减少计数。最后,更新上次的状态并清除中断标志位,以便处理下一次中断。
3.3 代码:
/**
**************************************************************************
** -------------------------------------------------------------------- **
** @name : Xz_scan
** @brief : 用于初始化外部中断以及中断向量控制器
** @param : None
** @retval : None
** @author data : 轩哥 2023-03-18
** @version : v1.0
** @attention : None
** -------------------------------------------------------------------- **
**************************************************************************
**/
void Xz_scan(void)
{
EXTI_InitTypeDef EXTI_InitStructure; //外部中断结构体声明
NVIC_InitTypeDef NVIC_InitStructure; //中断向量控制器声明
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE); //打开复用时钟
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA,GPIO_PinSource6|GPIO_PinSource7);//配置GPIO对应线
EXTI_InitStructure.EXTI_Line = XZ_A_PIN | XZ_B_PIN; //引脚线
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; //中断模式
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising_Falling;//上下边沿触发
EXTI_InitStructure.EXTI_LineCmd = ENABLE; //使能
EXTI_Init(&EXTI_InitStructure); //初始化
NVIC_InitStructure.NVIC_IRQChannel = EXTI9_5_IRQn; //中断源
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x00; //抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x01; //子优先级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能
NVIC_Init(&NVIC_InitStructure); //初始化NVIC
}
/**
**************************************************************************
** -------------------------------------------------------------------- **
** @name : EXTI9_5_IRQHandler
** @brief : 外部中断服务函数
** @param : None
** @retval : None
** @author data : 轩哥 2023-03-18
** @version : v1.0
** @attention : 它是下降沿触发,结合正反转时序图观察可见复合判断且不掉步
---顺时针-- ---逆时针--
| A | B | | A | B |
| 1 | 1 | | 1 | 1 |
| 0| 1 | | 1 | 0 |
| 0 | 0 | | 0 | 0 |
| 1 | 0 | | 0 | 1 |
----------- -----------
** -------------------------------------------------------------------- **
**************************************************************************
**/
void EXTI9_5_IRQHandler(void) {
static uint8_t lastEncoderState = 0; // 关于A通道的上一状态
static uint8_t lastEncoderState2 = 0; // 关于B通道的上一状态
if (EXTI_GetITStatus(EXTI_Line6) != RESET) //当边沿来了
{
uint8_t currEncoderState = GPIO_ReadInputDataBit(XZ_A_PORT,XZ_A_PIN); //将PA6电平保存
if (currEncoderState != lastEncoderState) //PA6当前电平状态与上一个电平状态比较 不相等代表被旋转
{
if (GPIO_ReadInputDataBit(XZ_B_PORT,XZ_B_PIN) != currEncoderState) //读取B通道电平和当前A通道状态
{
encoderCount++; //不同电平认为在正转
}
else {
encoderCount--; //相同认为在反转
}
lastEncoderState = currEncoderState; //将当前A通道电平保存为上一个电平
}
EXTI_ClearITPendingBit(EXTI_Line6); //清除标作位
}
if (EXTI_GetITStatus(EXTI_Line7) != RESET) //当边沿来了
{
uint8_t currEncoderState = GPIO_ReadInputDataBit(XZ_B_PORT,XZ_B_PIN); //读取B通道电平
if (currEncoderState != lastEncoderState2) //与上一次保存的电平不一致认为在转动
{
if (GPIO_ReadInputDataBit(XZ_A_PORT,XZ_A_PIN) != currEncoderState) //读取A通道的电平和B通道刚刚的电平比较
{
encoderCount--; //不相同认为在反转
}
else {
encoderCount++; //相同认为在正转
}
lastEncoderState2 = currEncoderState; //保存上一次电平状态
}
EXTI_ClearITPendingBit(EXTI_Line7); //清除标志位
}
}
四、驱动方式二
4.1 简介:
此驱动方式以定时器编码器检测方式,优点是能够更快速、更准确地响应编码器的脉冲,减少了因处理器忙碌或其他中断造成的延迟,抗噪声能力更强。缺点是占用片上外设资源。
4.2 原理:
通过监测连接到定时器输入捕获通道的旋转编码器A相和B相的信号变化,利用这两个信号之间的相位差来确定旋转方向,并根据每个通道的状态变化以四倍频的方式更新计数,从而精确地跟踪编码器的位置和旋转速度。定时器能够响应每个通道的上升沿和下降沿,提供高分辨率的位置测量,并且可以通过读取定时器的计数值来获取旋转信息。
4.3 代码:
uint8_t Encoder_sensi=1;//分频因子
TIM_HandleTypeDef TIM_Handle1; //输入结构体
/**************************************************************************
** -------------------------------------------------------------------- **
** @name : Encoder_Init
** @brief : 这是一个用于初始化旋转编码器GPIO的函数
** @param : None
** @retval : None
** @author data : 轩哥 2023-07-21
** @attention : None
** -------------------------------------------------------------------- **
**************************************************************************/
void Encoder_Init(void)
{
__HAL_RCC_TIM1_CLK_ENABLE(); //定时器1时钟打开
__HAL_RCC_GPIOA_CLK_ENABLE(); //打开GPIOA时钟打开
__HAL_RCC_GPIOE_CLK_ENABLE(); //打开GPIOE时钟打开
__HAL_RCC_GPIOC_CLK_ENABLE(); //打开GPIOC时钟打开
GPIO_InitTypeDef GPIO_InitStructure; //GPIO结构体声明
GPIO_InitStructure.Mode = GPIO_MODE_AF_PP; //设置内部上拉
GPIO_InitStructure.Pin = XZ_A_PIN ;
GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_VERY_HIGH; //设置翻转速度
GPIO_InitStructure.Alternate=GPIO_AF1_TIM1;
HAL_GPIO_Init(XZ_A_PORT, &GPIO_InitStructure); //初始化
GPIO_InitStructure.Mode = GPIO_MODE_AF_PP; //设置内部上拉
GPIO_InitStructure.Pin = XZ_B_PIN ;
GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_VERY_HIGH; //设置翻转速度
GPIO_InitStructure.Alternate=GPIO_AF1_TIM1;
HAL_GPIO_Init(XZ_B_PORT, &GPIO_InitStructure); //初始化
TIM_Encoder_InitTypeDef TIM_EncoderInitStructure; //定时器输入结构体 //定时器结构体声明
TIM_MasterConfigTypeDef TIM_MasterStructure;
TIM_Handle1.Instance=TIM1;
TIM_Handle1.Init.Prescaler = 0;
TIM_Handle1.Init.CounterMode = TIM_COUNTERMODE_UP;
TIM_Handle1.Init.Period = 65535;
TIM_Handle1.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
TIM_Handle1.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
TIM_EncoderInitStructure.EncoderMode = TIM_ENCODERMODE_TI2; // 四倍频模式
TIM_EncoderInitStructure.IC1Polarity = TIM_ICPOLARITY_RISING;
TIM_EncoderInitStructure.IC1Selection = TIM_ICSELECTION_DIRECTTI;
TIM_EncoderInitStructure.IC1Prescaler = TIM_ICPSC_DIV1;
TIM_EncoderInitStructure.IC1Filter = 0xF;
if (HAL_TIM_Encoder_Init(&TIM_Handle1, &TIM_EncoderInitStructure) != HAL_OK)
{
printf("TIM_EncoderInitStructure1初始化失败\r\n");
}
TIM_EncoderInitStructure.IC2Polarity = TIM_ICPOLARITY_RISING;
TIM_EncoderInitStructure.IC2Selection = TIM_ICSELECTION_DIRECTTI;
TIM_EncoderInitStructure.IC2Prescaler = TIM_ICPSC_DIV1;
TIM_EncoderInitStructure.IC2Filter = 0xF;
if (HAL_TIM_Encoder_Init(&TIM_Handle1, &TIM_EncoderInitStructure) != HAL_OK)
{
printf("TIM_EncoderInitStructure2初始化失败\r\n");
}
TIM_MasterStructure.MasterOutputTrigger = TIM_TRGO_RESET;
TIM_MasterStructure.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
if (HAL_TIMEx_MasterConfigSynchronization(&TIM_Handle1, &TIM_MasterStructure) != HAL_OK)
{
printf("TIM_MasterStructure初始化失败\r\n");
}
HAL_TIM_Encoder_Start(&TIM_Handle1, TIM_CHANNEL_ALL);
}
/**************************************************************************
** -------------------------------------------------------------------- **
** @name : Encoder_Get
** @brief : 这是一个获取编码器计数值的函数 (增加滤值)
** @param : None
** @retval : None
** @author data : 轩哥 2023-07-23
** @attention : None
** -------------------------------------------------------------------- **
**************************************************************************/
int8_t d0;// 定义一个全局变量 d0,用于存储上一次的编码器计数值(经过 Encoder_sensi 调整后的值)
int16_t Encoder_Get_Div4(void)
{
if(__HAL_TIM_GET_COUNTER(&TIM_Handle1)/Encoder_sensi - d0)// 检查当前计数值与上一次计数值之间的差值
{
int16_t temp = (__HAL_TIM_GET_COUNTER(&TIM_Handle1)/Encoder_sensi - d0);// 计算当前计数值与上一次计数值之间的差值
d0 = __HAL_TIM_GET_COUNTER(&TIM_Handle1)/Encoder_sensi;// 更新 d0 为当前计数值(经过 Encoder_sensi 调整后的值)
return temp*-1; //*-1方向调转
}
return 0;// 如果没有变化,则返回 0
}
五、实验示例
5.1 示例驱动方式一:
5.2 示例驱动方式二:
六、总结
通过以上学习我们完成了传感器驱动方法,可见方式一的精度、响应等都不如方式二,这是因为方式二这种模式下,硬件会自动处理编码器的A相和B相信号,并根据相位关系来确定旋转方向,同时以更高的分辨率(通常是四倍于编码器的基本分辨率)进行计数,因此有更快的响应与更加可靠。
标签:编码器,HAL,旋转,STM32,TIM,InitStructure,GPIO,EXTI From: https://blog.csdn.net/qq_39411256/article/details/145010154