电机驱动模块👇
【STM32--平衡车】TB6612FNG详解[特殊字符]------用于电机驱动
https://blog.csdn.net/2301_76153977/article/details/155577282?spm=1001.2014.3001.5501本篇我们学习
编码器模块------用于测速

1. 介绍------定时器编码器模式
其实属于定时器内容,但是都放一块内容太多了~
想象编码器内有两个马盘,一个接入A相,一个接入B相,有所错开


正交编码器输出 A/B 两相脉冲(相位差 90°),TIM2/TIM3 的 CH1/CH2 引脚分别接 A/B 相,定时器会根据 A/B 相的上升 / 下降沿自动增减计数 ------ 正转时计数增加,反转时计数减少,无需软件中断处理,硬件自动完成,效率极高。
再复习一下定时器框图


2. encoder.c
2.1 核心逻辑


编码器要有两个,编码器一般和电机是放在一起的
- 配置 TIM2/TIM3 工作在编码器模式,对接两路正交编码器的 A/B 相脉冲;
- 初始化编码器对应的 GPIO 引脚(输入模式 + 上拉);
- 提供读取编码器计数值的接口(读取后清零,便于计算增量);
- 整体用于检测左右两个编码器的脉冲数(反映电机转动的方向和步数)。
2.2 写出encoder.c
全局变量定义
cpp
TIM_HandleTypeDef encoder_left_handle = {0};
TIM_HandleTypeDef encoder_right_handle = {0};
TIM_HandleTypeDef:STM32 HAL 库的定时器句柄结构体,封装了定时器的实例(如 TIM2)、初始化参数、状态等,是 HAL 库操作外设的核心载体;
全局定义的原因:编码器计数值需要在函数间共享,且 HAL 库的回调 / 操作函数需要传入句柄指针;
初始化={0}:避免未初始化的随机值导致 HAL 库函数异常(嵌入式中全局变量默认初始化为 0,但显式初始化更规范)。
编码器初始化函数(以encoder_left_init为例)
cpp
void encoder_left_init(void)
{
TIM_Encoder_InitTypeDef config = {0};
// 1. 定时器基础配置
encoder_left_handle.Instance = TIM2; // 绑定TIM2外设
encoder_left_handle.Init.Period = 65536 - 1; // 自动重装值:65535(16位定时器最大值)
encoder_left_handle.Init.Prescaler = 0; // 预分频器:不分频
encoder_left_handle.Init.CounterMode = TIM_COUNTERMODE_UP; // 计数模式:向上(编码器模式下实际由脉冲方向决定)
encoder_left_handle.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; // 时钟分频:不分频
// 2. 编码器模式专属配置
config.EncoderMode = TIM_ENCODERMODE_TI12; // 编码器模式:TI1/TI2两路脉冲都参与计数
// 通道1配置
config.IC1Polarity = TIM_ICPOLARITY_RISING; // 触发极性:上升沿
config.IC1Selection = TIM_ICSELECTION_DIRECTTI; // 输入捕获映射:直接映射到TI1引脚
config.IC1Prescaler = TIM_ICPSC_DIV1; // 输入预分频:不分频(每个脉冲都捕获)
config.IC1Filter = 0xF; // 输入滤波器:15个定时器时钟周期滤波
// 通道2配置(和通道1一致)
config.IC2Polarity = TIM_ICPOLARITY_RISING;
config.IC2Selection = TIM_ICSELECTION_DIRECTTI;
config.IC2Prescaler = TIM_ICPSC_DIV1;
config.IC2Filter = 0xF;
// 3. 应用配置并启动编码器计数
HAL_TIM_Encoder_Init(&encoder_left_handle, &config); // 初始化编码器模式
HAL_TIM_Encoder_Start(&encoder_left_handle, TIM_CHANNEL_ALL); // 启动所有通道计数
}
定时器编码器模式原理:
正交编码器输出 A/B 两相脉冲(相位差 90°),TIM2/TIM3 的 CH1/CH2 引脚分别接 A/B 相,定时器会根据 A/B 相的上升 / 下降沿自动增减计数 ------ 正转时计数增加,反转时计数减少,无需软件中断处理,硬件自动完成,效率极高。
TIM_ENCODERMODE_TI12:
编码器模式有 3 种:
TIM_ENCODERMODE_TI1:仅 TI1(A 相)脉冲触发计数;
TIM_ENCODERMODE_TI2:仅 TI2(B 相)脉冲触发计数;
TIM_ENCODERMODE_TI12:TI1 和 TI2 都触发(最常用,计数精度最高)。
Period = 65535:
16 位定时器的最大计数值是 65535,计数到 65535 后会溢出到 0(向上计数),或从 0 下溢到 65535(向下计数);如果需要更大的计数范围,需在软件中处理溢出(后面扩展会讲)。
IC1Filter = 0xF:
编码器脉冲可能有电磁干扰导致毛刺,滤波器会 "平滑" 输入信号 ------ 只有当引脚电平稳定 15 个定时器时钟周期后,才认为是有效脉冲,滤除短于 15 个周期的毛刺。
Prescaler = 0:
预分频器值为 0 表示 "不分频",定时器时钟 = APB1 时钟(STM32F4 中 APB1 最大 42MHz,F1 中 72MHz);如果设置为 1,则每 2 个脉冲才计数 1 次,用于降低计数频率。
左边编码器会了,右边编码器同理
cpp
void encoder_right_init(void)
{
TIM_Encoder_InitTypeDef config = {0};
encoder_right_handle.Instance = TIM3;
encoder_right_handle.Init.Period = 65536 - 1;
encoder_right_handle.Init.Prescaler = 0;
encoder_right_handle.Init.CounterMode = TIM_COUNTERMODE_UP;
encoder_right_handle.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
config.EncoderMode = TIM_ENCODERMODE_TI12;
config.IC1Polarity = TIM_ICPOLARITY_RISING;
config.IC1Selection = TIM_ICSELECTION_DIRECTTI;
config.IC1Prescaler = TIM_ICPSC_DIV1;
config.IC1Filter = 0xF;
config.IC2Polarity = TIM_ICPOLARITY_RISING;
config.IC2Selection = TIM_ICSELECTION_DIRECTTI;
config.IC2Prescaler = TIM_ICPSC_DIV1;
config.IC2Filter = 0xF;
HAL_TIM_Encoder_Init(&encoder_right_handle, &config);
HAL_TIM_Encoder_Start(&encoder_right_handle, TIM_CHANNEL_ALL);
}
MSP 初始化函数(HAL_TIM_Encoder_MspInit)
cpp
void HAL_TIM_Encoder_MspInit(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM2)
{
__HAL_RCC_TIM2_CLK_ENABLE(); // 使能TIM2时钟
__HAL_RCC_GPIOA_CLK_ENABLE(); // 使能GPIOA时钟(TIM2_CH1=PA0,CH2=PA1)
GPIO_InitTypeDef gpio_initstruct;
gpio_initstruct.Pin = GPIO_PIN_0 | GPIO_PIN_1; // TIM2_CH1=PA0,CH2=PA1
gpio_initstruct.Mode = GPIO_MODE_INPUT; // 输入模式(编码器脉冲是输入)
gpio_initstruct.Pull = GPIO_PULLUP; // 上拉电阻(编码器通常是开漏输出,需上拉)
gpio_initstruct.Speed = GPIO_SPEED_FREQ_HIGH; // 高速模式(适配编码器高频脉冲)
HAL_GPIO_Init(GPIOA, &gpio_initstruct);
}
else if(htim->Instance == TIM3)
{
__HAL_RCC_TIM3_CLK_ENABLE(); // 使能TIM3时钟
__HAL_RCC_GPIOA_CLK_ENABLE(); // 使能GPIOA时钟(TIM3_CH1=PA6,CH2=PA7)
GPIO_InitTypeDef gpio_initstruct;
gpio_initstruct.Pin = GPIO_PIN_6 | GPIO_PIN_7; // TIM3_CH1=PA6,CH2=PA7
gpio_initstruct.Mode = GPIO_MODE_INPUT;
gpio_initstruct.Pull = GPIO_PULLUP;
gpio_initstruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &gpio_initstruct);
}
}
HAL_TIM_Encoder_MspInit:
HAL 库把外设核心配置(如编码器模式、计数参数)和底层硬件配置(时钟、GPIO)分离
------HAL_TIM_Encoder_Init负责定时器功能配置,HAL_TIM_Encoder_MspInit负责 "MCU 硬件层" 配置(时钟、引脚),这个函数是 HAL 库的 "弱函数",用户需要重写实现具体硬件配置。
GPIO_PULLUP:
正交编码器的输出引脚通常是开漏型(无内部上拉),如果不加外部上拉,必须配置 GPIO 内部上拉,否则引脚电平会悬空,导致计数乱跳。
引脚映射:
TIM2_CH1=PA0、CH2=PA1;TIM3_CH1=PA6、CH2=PA7 是 STM32 的 "默认复用功能映射",如果需要改引脚,需配置 AFIO 复用映射(高级用法)。
编码器计数值读取函数
cpp
int16_t encoder_left_get(void)
{
int16_t temp = __HAL_TIM_GET_COUNTER(&encoder_left_handle); // 读取当前计数值
__HAL_TIM_SET_COUNTER(&encoder_left_handle, 0); // 清零计数器
return temp; // 返回本次读取的增量值
}
__HAL_TIM_GET_COUNTER:
宏定义,直接读取定时器的 CNT 寄存器(计数寄存器),是硬件寄存器级别的操作,效率极高;
读取后清零的设计:
这种方式获取的是 "两次读取之间的脉冲增量",适合计算电机的瞬时速度(速度 = 脉冲增量 / 时间间隔);如果需要累计位置,不能清零,需记录累计值并处理溢出。
同理,右边编码器
cpp
int16_t encoder_right_get(void)
{
int16_t temp = __HAL_TIM_GET_COUNTER(&encoder_right_handle);
__HAL_TIM_SET_COUNTER(&encoder_right_handle, 0);
return temp;
}
在main函数中使用
运行,打开串口调试助手
顺时针转,0→4→8→......变大,都是四的倍数

这是因为计数
要计A的上升沿、A的下降沿、B的上升沿、B的下降沿
逆时针转,从0→65532(满量程,+4溢出变成0)

我如果想 逆时针转 是负数呢?
把u去掉就好了

试一试
