介绍
编码器(Encoder)是一种用于测量旋转运动或位置的设备。编码器通常与定时器模块一起使用,以便在微控制器中获取和计算旋转的脉冲数量,从而确定物体的运动方向和距离。编码器在许多应用中都很有用,例如机器人运动控制、电机位置反馈和位置传感。
STM32微控制器提供了多种类型的编码器支持,其中一种常见的编码器类型是增量编码器。增量编码器基于两个脉冲信号通道,通常称为A相和B相。当旋转发生时,这两个信号通道的相位关系会发生变化,从而可以计算出旋转方向和旋转的步数。STM32提供了硬件支持,使您可以使用定时器模块来捕获和计数编码器信号。以下是一些与STM32编码器相关的重要概念:
计数器值(Counter Value)
:这是定时器模块中的计数器,它记录了编码器信号的脉冲数量。计数器的增加和减少取决于编码器信号的变化。
计数方向(Counter Direction)
:计数器可以递增或递减,具体取决于编码器信号的变化。计数方向可以用于确定物体的旋转方向。
捕获
:定时器可以配置为捕获编码器信号的状态,以便在脉冲信号变化时记录计数器的值。捕获允许您测量脉冲的时间间隔,从而用于速度和位置计算。
编码器模式(Encoder Mode)
:这是定时器模块的一个设置,用于指定它将用作编码器。编码器模式配置定时器的输入通道,使其能够捕获编码器信号。
编码器计数器(Encoder Counter)
:某些STM32型号提供了特殊的硬件编码器计数器,可直接处理编码器的信号和计数。
AB相编码器
AB相编码器,也称为正交编码器或增量式编码器,是一种常用于测量旋转位置和方向的设备。它基于两个输出信号通道,通常称为A相和B相,这两个信号在相位上相差90度,用于确定旋转方向和计数。
!!网上好多人这些编码器区分不出来 下面是总结!!
增量式编码器:
增量式编码器通常有两个输出通道,分别称为A相和B相,以及一个索引信号通道(有时可能没有)。
当旋转运动发生时,A相和B相信号的脉冲数量发生变化。这两个信号相位差90度,可以通过监测它们的变化来确定旋转方向和计数值。
增量式编码器的输出是增量脉冲,需要进行计数和积分以获得位置信息。可以使用两个通道的信号来测量速度。
正交编码器:
正交编码器也有A相和B相两个输出通道,这两个通道的信号脉冲相位差90度,就像增量式编码器一样。
与增量式编码器不同的是,正交编码器的输出已经是经过积分的位置信息,因此可以直接获取旋转位置。
正交编码器还可以提供一个Z相或索引信号,用于标记一个完整的旋转周期。
原理介绍:
可以通过判断其中一相上升/下降沿时,另一相是高或者低电平判断转动方向
常用最后一个,这里面的内容就是对应AB相的判断。比如图中FP1上升 FP2高电平,若1为A相2为B相,即A在上升时B在高电平,对应反转,因此向下计数。
编码器接口判断是正反转,控制CNT++或–。ARR设置为65535,这样CNT=0时一自减就变为65535,也可以转为有符号型直接折半变为正负65535/2。
STM32配置
主要配置:
几个重要参数:
预分配
:可以设置编码器输出脉冲是否分频
滤波
:滤波消除毛刺,越大效果越好,看需求
下面是示例代码
#include "tim.h"
// 声明外部的 TIM_HandleTypeDef 结构体变量,该结构体变量在其他文件中定义
extern TIM_HandleTypeDef htim2;
// 初始化 TIM2 编码器
void MX_TIM2_Init(void)
{
// 定义 TIM_Encoder_InitTypeDef 结构体变量,并初始化为 0
TIM_Encoder_InitTypeDef sConfig = {
0};
// 定义 TIM_MasterConfigTypeDef 结构体变量,并初始化为 0
TIM_MasterConfigTypeDef sMasterConfig = {
0};
// 设置 htim2 结构体的相关参数
htim2.Instance = TIM2;
htim2.Init.Prescaler = 2;
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
htim2.Init.Period = 65536;
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
// 设置编码器模式和输入捕获相关参数
sConfig.EncoderMode = TIM_ENCODERMODE_TI12;
sConfig.IC1Polarity = TIM_ICPOLARITY_RISING;
sConfig.IC1Selection = TIM_ICSELECTION_DIRECTTI;
sConfig.IC1Prescaler = TIM_ICPSC_DIV1;
sConfig.IC1Filter = 15;
sConfig.IC2Polarity = TIM_ICPOLARITY_RISING;
sConfig.IC2Selection = TIM_ICSELECTION_DIRECTTI;
sConfig.IC2Prescaler = TIM_ICPSC_DIV1;
sConfig.IC2Filter = 15;
// 初始化编码器
if (HAL_TIM_Encoder_Init(&htim2, &sConfig) != HAL_OK)
{
Error_Handler();
}
// 设置主定时器的触发输出和主从模式
sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
if (HAL_TIMEx_MasterConfigSynchronization(&htim2, &sMasterConfig) != HAL_OK)
{
Error_Handler();
}
}
// 初始化 TIM2 编码器的外设
void HAL_TIM_Encoder_MspInit(TIM_HandleTypeDef* tim_encoderHandle)
{
GPIO_InitTypeDef GPIO_InitStruct = {
0};
if(tim_encoderHandle->Instance==TIM2)
{
// 使能 TIM2 外设时钟
__HAL_RCC_TIM2_CLK_ENABLE();
// 使能 GPIOA 外设时钟
__HAL_RCC_GPIOA_CLK_ENABLE();
// 配置 GPIOA 引脚 0 和 1 为输入模式
GPIO_InitStruct.Pin = GPIO_PIN_0|GPIO_PIN_1;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// 配置 TIM2 中断优先级并使能中断
HAL_NVIC_SetPriority(TIM2_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(TIM2_IRQn);
}
}
// 反初始化 TIM2 编码器的外设
void HAL_TIM_Encoder_MspDeInit(TIM_HandleTypeDef* tim_encoderHandle)
{
if(tim_encoderHandle->Instance==TIM2)
{
// 关闭 TIM2 外设时钟
__HAL_RCC_TIM2_CLK_DISABLE();
// 复位 GPIOA 引脚的配置
HAL_GPIO_DeInit(GPIOA, GPIO_PIN_0|GPIO_PIN_1);
// 关闭 TIM2 中断
HAL_NVIC_DisableIRQ(TIM2_IRQn);
}
}
主函数:
// 清除 TIM2 更新中断标志位
HAL_TIM_CLEAR_IT(&htim2, TIM_IT_UPDATE);
// 启动 TIM2 编码器模式,捕获编码器信号
HAL_TIM_Encoder_Start(&htim2, TIM_CHANNEL_ALL);
// 使能 TIM2 更新中断
__HAL_TIM_ENABLE_IT(&htim2, TIM_IT_UPDATE);
常用函数:
HAL_TIM_Encoder_GetState():
该函数用于获取编码器的状态,例如运行状态、停止状态等。
HAL_TIM_Encoder_GetValue():
获取当前编码器的计数值。这对于获得实时位置信息很有用。
HAL_TIM_Encoder_Stop():
停止编码器模式。这可以用于暂停编码器信号的捕获和处理。
HAL_TIM_Encoder_Start_DMA():
在编码器模式下,使用DMA(直接内存访问)来捕获编码器信号,从而减少CPU的负担。
HAL_TIM_IC_CaptureCallback():
如果您使用输入捕获模式来处理编码器信号,该回调函数将在捕获事件发生时被调用。
测距离
方式一每次一个中断判断
将CNT设置为1 满了就会产生溢出发生中端 在中断中写下下面的代码
判断方向 之后加或者减。
extern long long xtt;
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim == &htim2)
{
//判断正反转;
if((uint32_t)(__HAL_TIM_IS_TIM_COUNTING_DOWN(&htim2)))
{
xtt++;
}
else
{
xtt--;
}
}
}
这样写缺点:
速度和精度:如果编码器的旋转速度很快,这种在定时器中断中直接判断正反转的方法可能会导致误判。另外,由于使用定时器中断的方式,计数器更新的精度可能受到中断处理的时间影响。
中断频率和计算:您在每次定时器中断中都对计数器值进行判断和更新,这可能会导致频繁的中断发生,从而影响系统性能。
方式二主函数快速轮询 获取值后清零
将将CNT设置为65536 每次获取后强转为short类型就可以有正负值了 ,另外主函数快速轮询,获取值累加并清空。
轮询 :
#include "main.h"
#include "tim.h"
#include "usart.h"
#include "gpio.h"
void SystemClock_Config(void);
extern TIM_HandleTypeDef htim2;
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_TIM2_Init();
MX_USART1_UART_Init();
long long longs = 0;
__HAL_TIM_CLEAR_IT (&htim2 ,TIM_IT_UPDATE );
HAL_TIM_Base_Start_IT (&htim2);
if (HAL_TIM_Encoder_Start(&htim2, TIM_CHANNEL_ALL) != HAL_OK)
{
Error_Handler(); // 错误处理:编码器启动失败
}
while (1)
{
longs += Get_Count(); //累加当前的值 可能为正也可能为负
__HAL_TIM_SET_COUNTER(&htim2,0); //读取完成清空
printf("当前值:%lld\n\r",longs);
}
}
Get_Count函数:
short Get_Count(void)
{
return (short)(__HAL_TIM_GET_COUNTER(&htim2));
}
优点:
简单直接:这种方式直接读取编码器计数器的值,然后累加并清零。代码相对简单,易于理解和实现。
实时性:由于在主循环中快速轮询,您能够几乎实时地获取编码器计数器的值。
不依赖中断:您没有使用定时器中断来处理编码器信号,这可以减少中断处理带来的开销,特别适用于一些对实时性要求较高的场景。
缺点:
实时性问题:虽然您可以几乎实时地获取计数器的值,但由于在主循环中轮询,如果主循环的其他操作耗时较长,可能会影响到对计数器值的获取。特别是当处理其他任务或外设时,可能会导致计数器值的丢失。
精度问题:在主循环中轮询的方式可能会受到循环迭代的时间变化影响,从而影响到对编码器的计数。如果主循环迭代时间不稳定,计数器值可能会出现不准确的情况。
综上所述,这适用于一些实时性要求较高、要求简单和直接的应用场景。
方式三 中断里获取CNT寄存器里的值并判断方向
就是在中断里获取CNT寄存器里的值并判断方向也就是分辨出上溢出下溢出并记录上溢出加一下溢出减一 获取值的时候使用使用前面的值乘上重装的值加上当前计数器里的值。
这个方式看似非常完美,那我们就实现一下。
测试发现 中断很稳妥 也没出现跳跃式的变化。
long long xtt;
long long xxt = -1; //这里设置为-1消除第一次产生的中断
void TIM2_IRQHandler(void)
{
if (__HAL_TIM_GET_FLAG(&htim2, TIM_FLAG_UPDATE) == SET) //发生重置
{
uint32_t currentCounter = __HAL_TIM_GET_COUNTER(&htim2); //获取装载的上一次值
//下溢出
if(currentCounter == 0)
{
xxt++;
}
//上溢出
if(currentCounter == 65535)
{
xxt--;
}
}
__HAL_TIM_CLEAR_IT(&htim2, TIM_IT_UPDATE);
}
long long Get_Value(void)
{
return xxt*65535 + (__HAL_TIM_GET_COUNTER(&htim2));
}
方式四 再使用一个定时器定时获取
类似于方式二 但是另加一个定时器 定时获取 并且清空计数器。这样写缺点是定时器设置的定时中断时间很难控制,需要介于性能与速度直接考虑。
测速度
外加一个定时器 定时通过__HAL_TIM_GET_COUNTER(&htim2)
获取,速度 = 距离/时间 就OK了,这里就不演示了。
最后
1.总结
滤波器调整为自己合适: 由于编码器信号可能受到噪声干扰,您可以配置定时器的滤波器来平滑输入信号,以避免误判。索引信号这里没用上因为测距离小车可能会来回走动: 一些编码器支持索引信号(Z相信号),用于标记旋转的一个完整周期。索引信号可以用于校准和重置位置。DMA后续作项目可以加上: 某些情况下,您可能希望使用DMA来处理编码器信号的捕获,以减轻CPU的负担,提高效率。
2.欧姆龙E6B2-CWZ6C使用注意事项
我所用到的编码器是欧姆龙E6B2-CWZ6C这款编码器,注意ABZ相需要加上拉电阻,我这里测试1K即可。
3. __HAL_TIM_CLEAR_IT 和 __HAL_TIM_CLEAR_FLAG 的区别
都用于清除定时器中断或标志位,但它们的使用场景和功能略有不同
- __HAL_TIM_CLEAR_IT(&htim2, TIM_IT_UPDATE);
这个宏用于清除定时器的中断标志位。在HAL_TIM_PeriodElapsedCallback
函数中,当定时器产生周期性中断(比如定时器溢出中断)时,为了避免重复调用,您通常会在中断回调函数的开头使用该宏清除中断标志位。这样,在下一个周期时,如果再次发生中断,您可以确保中断回调函数仅被调用一次。 - __HAL_TIM_CLEAR_FLAG(&htim2, TIM_FLAG_UPDATE);
这个宏用于直接清除定时器的特定标志位,包括中断标志位和其他标志位。您可以使用这个宏来手动清除定时器的标志位,而不依赖于中断回调函数。这对于您在其他代码段中需要清除标志位的情况很有用。
总结来说,__HAL_TIM_CLEAR_IT
主要用于在中断回调函数中清除中断标志位,以避免重复调用。而__HAL_TIM_CLEAR_FLAG
则可以在任何需要的地方手动清除定时器的标志位,不限于中断回调函数。
4.HAL_TIM_PeriodElapsedCallback 和 TIMx_IRQHandler 的区别
都与定时器相关的中断处理有关,但它们在使用上有一些区别
- HAL_TIM_PeriodElapsedCallback:
HAL_TIM_PeriodElapsedCallback
是一个 HAL 库提供的回调函数,在定时器的周期性中断发生时被调用。它是通过 HAL 库的封装来实现的,适用于 STM32 HAL 库用户。您可以通过在应用代码中定义这个函数,并在其中处理定时器中断发生时的操作。这个函数在定时器的每个周期性中断时都会被调用。 - TIMx_IRQHandler:
TIMx_IRQHandler
是定时器的底层中断处理函数。对于每个定时器,CMSIS(Cortex Microcontroller Software Interface Standard)提供了相应的中断处理函数,例如TIM1_IRQHandler
、TIM2_IRQHandler
等。这些函数位于设备特定的启动文件或库中,负责处理底层硬件中断。您可以在这些函数中进行一些底层的中断处理,如清除中断标志位等。这种方式更接近硬件,并且通常需要在启动文件中进行中断向量的映射。
综上所述,HAL_TIM_PeriodElapsedCallback
是一个抽象的高级封装,适用于 STM32 HAL 库,而 TIMx_IRQHandler
则是底层的硬件中断处理函数,适用于进行更底层的中断操作。选择使用哪个函数取决于您的开发需求和使用的库。