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开销。

配置步骤大致如下:

  1. 使能定时器的DMA请求(更新事件触发)
  2. 配置DMA通道,源地址为定时器的CNT寄存器
  3. 设置传输长度和循环模式
  4. 调用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或中断。

编码器模式API调用与中断处理流程 1. MX_TIMx_Init() 初始化 2. __HAL_TIM_ENABLE_IT(更新中断) 3. HAL_TIM_Encoder_Start(CH1 + CH2) 4. HAL_TIM_Base_Start() ⚠️易漏 5. 运行中:硬件自动计数 每N个脉冲 → 更新事件 → 触发溢出中断 6. HAL_TIM_PeriodElapsedCallback() 判断计数方向 更新 overflow_cnt

3.7 避坑总结

最后,我把这些年踩过的坑整理了一下,你写代码时多留个心眼:

  • 计数器初始值:启动前记得清零,否则第一次读数就是错的。用__HAL_TIM_SET_COUNTER(&htim, 0)。
  • 中断优先级:溢出中断的优先级不要设得太低,否则在高负载下可能丢中断。我一般设到1或2(0最高)。
  • 变量类型:记录溢出次数的变量,建议用int16_t或int32_t,别用uint16_t。因为反转时溢出是负的,无符号类型会出问题。
  • 调试技巧:如果计数器不动,先检查GPIO配置——编码器引脚必须配置为复用开漏或复用推挽,不能是普通的输入模式。我在这上面浪费过半天时间。

我的习惯:每次写完编码器驱动,我都会先用手慢慢转动电机,同时通过串口打印计数值。如果数值稳定增加或减少,说明硬件连接和软件配置都没问题。这一步虽然简单,但能省下后面排查问题的很多时间。