3. HAL库驱动开发:编码器模式API详解与中断配置
好,咱们进入编码器模式最核心的部分——HAL库的API调用。说实话,我刚接触STM32的编码器接口时,也被那一堆定时器寄存器搞得有点晕。后来发现,HAL库把这些封装得挺干净,你只要搞清楚几个关键函数,剩下的就是配置问题了。
3.1 核心API:启动与停止
编码器模式最常用的两个函数,就是启动和停止。我习惯把它们叫做“油门”和“刹车”。
| 函数原型 | 功能说明 | 典型返回值 |
|---|---|---|
HAL_TIM_Encoder_Start(TIM_HandleTypeDef *htim, uint32_t Channel) |
启动指定定时器的编码器模式,开始计数 | HAL_OK / HAL_ERROR |
HAL_TIM_Encoder_Stop(TIM_HandleTypeDef *htim, uint32_t Channel) |
停止编码器模式,计数器保持当前值 | HAL_OK / HAL_ERROR |
HAL_TIM_Encoder_Start_IT(TIM_HandleTypeDef *htim, uint32_t Channel) |
启动编码器模式并开启中断 | HAL_OK / HAL_ERROR |
HAL_TIM_Encoder_Start_DMA(TIM_HandleTypeDef *htim, uint32_t Channel, uint32_t *pData, uint16_t Length) |
启动编码器模式并开启DMA传输 | HAL_OK / HAL_ERROR |
关键点:启动函数中的Channel参数,不是指物理引脚,而是定时器的通道编号。比如TIM3的编码器模式,通常用TIM_CHANNEL_1和TIM_CHANNEL_2同时传入。我见过有人只启动一个通道,结果计数器根本不走——嗯,编码器必须两个通道一起用。
3.2 中断配置:什么时候需要它?
你可能会问:编码器模式不是硬件自动计数吗?为什么还要开中断?
说白了,中断是用来处理“边界情况”的。比如:
- 溢出中断:计数器从0xFFFF翻到0x0000,或者反过来。这时候你得知道发生了溢出,否则算出来的速度就错了。
- 捕获/比较中断:某些高级应用里,你想在特定计数值时触发动作。
我个人习惯,在电机测速项目里,溢出中断是必开的。为什么?因为编码器分辨率越高,定时器溢出得越快。举个例子:
经验数据:一个1000线的编码器,4倍频后每圈4000个脉冲。如果电机转速3000RPM,每秒就是200000个脉冲。16位定时器最大计数值65535,大约0.33秒就溢出了。你不处理溢出,测出来的速度就是错的。
3.3 溢出中断处理:核心逻辑
溢出中断的处理,其实就干一件事:记录溢出的次数。我常用的方法是定义一个全局变量,每次溢出中断里加1或减1。
// 全局变量,用于记录溢出次数
volatile int16_t encoder_overflow_cnt = 0;
// 定时器溢出中断回调
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM3)
{
// 判断计数方向
if (__HAL_TIM_IS_TIM_COUNTING_DOWN(htim))
{
encoder_overflow_cnt--; // 向下计数,溢出减1
}
else
{
encoder_overflow_cnt++; // 向上计数,溢出加1
}
}
}
我曾经踩过的坑:一开始我没判断计数方向,直接每次溢出都加1。结果电机反转时,速度值完全不对。后来才意识到,溢出方向必须和计数方向匹配。向上溢出是正,向下溢出是负,这个逻辑不能搞反。
3.4 DMA配置:什么时候用?
DMA在编码器模式里,主要用于连续采集计数值。比如你想以固定频率(1kHz)读取编码器值,用DMA可以做到零CPU开销。
配置步骤大致如下:
- 使能定时器的DMA请求(更新事件触发)
- 配置DMA通道,源地址为定时器的CNT寄存器
- 设置传输长度和循环模式
- 调用
HAL_TIM_Encoder_Start_DMA()
不过说实话,在大多数电机测速场景里,我更喜欢用定时器中断+软件读取的方式。DMA虽然省CPU,但处理溢出时反而更麻烦——你得在DMA传输完成中断里再处理一次溢出补偿,逻辑绕来绕去。
3.5 完整启动流程
下面是我在实际项目里用的启动代码,你可以参考:
// 编码器初始化与启动
void Encoder_Start(void)
{
// 1. 初始化定时器(MX_TIM3_Init() 由CubeMX生成)
MX_TIM3_Init();
// 2. 使能定时器更新中断(用于溢出处理)
__HAL_TIM_ENABLE_IT(&htim3, TIM_IT_UPDATE);
// 3. 启动编码器模式(两个通道都要)
HAL_TIM_Encoder_Start(&htim3, TIM_CHANNEL_1);
HAL_TIM_Encoder_Start(&htim3, TIM_CHANNEL_2);
// 4. 使能定时器(这一步容易忘!)
HAL_TIM_Base_Start(&htim3);
printf("编码器启动完成,当前计数值:%d\r\n", __HAL_TIM_GET_COUNTER(&htim3));
}
注意:第4步很多人会漏掉。HAL_TIM_Encoder_Start()只是配置了编码器模式,但定时器的时钟并没有真正开始跑。必须再调用HAL_TIM_Base_Start()才能让计数器动起来。我刚开始也在这卡了半天,计数器死活不动,后来翻参考手册才找到原因。
3.6 知识结构图:API调用与中断流程
下面这张图,把编码器模式从启动到读取的完整流程画出来了。你可以对照着看,每一步对应哪个API或中断。
3.7 避坑总结
最后,我把这些年踩过的坑整理了一下,你写代码时多留个心眼:
- 计数器初始值:启动前记得清零,否则第一次读数就是错的。用
__HAL_TIM_SET_COUNTER(&htim, 0)。 - 中断优先级:溢出中断的优先级不要设得太低,否则在高负载下可能丢中断。我一般设到1或2(0最高)。
- 变量类型:记录溢出次数的变量,建议用
int16_t或int32_t,别用uint16_t。因为反转时溢出是负的,无符号类型会出问题。 - 调试技巧:如果计数器不动,先检查GPIO配置——编码器引脚必须配置为复用开漏或复用推挽,不能是普通的输入模式。我在这上面浪费过半天时间。
我的习惯:每次写完编码器驱动,我都会先用手慢慢转动电机,同时通过串口打印计数值。如果数值稳定增加或减少,说明硬件连接和软件配置都没问题。这一步虽然简单,但能省下后面排查问题的很多时间。