2. STM32定时器编码器模式:从原理到寄存器配置

编码器模式,说白了就是让定时器自己读懂正交编码信号。你不需要在中断里数脉冲,硬件全帮你干了。我第一次用这个功能时,还傻傻地在EXTI中断里计数,后来发现定时器自带这个能力,真是相见恨晚。

2.1 正交编码信号基础

先说说什么是正交编码信号。电机编码器通常输出两路方波:TI1和TI2。这两路信号相位差90度,所以叫正交。

  • TI1(Channel 1):A相信号,通常接定时器的CH1输入
  • TI2(Channel 2):B相信号,通常接定时器的CH2输入

为什么用两路?因为单路只能测速度,不能判方向。两路配合,谁先跳变,谁后跳变,方向就出来了。

核心概念:编码器模式本质上是把TI1和TI2的边沿事件当作计数时钟。每检测到一个有效边沿,计数器就加1或减1。方向由两路信号的相位关系决定。

2.2 TI1、TI2计数逻辑详解

STM32的编码器模式支持三种计数方式,我习惯叫它们:

  • 模式1:只在TI1边沿计数(TI2判方向)
  • 模式2:只在TI2边沿计数(TI1判方向)
  • 模式3:TI1和TI2边沿都计数(四倍频)

你想想看,模式3相当于把编码器分辨率翻了4倍。比如编码器本身是1000线,模式3下就是4000个计数/圈。我在做AGV小车时用过这个模式,低速下也能读到稳定的速度值。

具体计数逻辑是这样的:

TI1电平 TI2电平 TI1上升沿 TI1下降沿 TI2上升沿 TI2下降沿
高 低 +1 -1 -1 +1
高 高 -1 +1 +1 -1
低 高 +1 -1 -1 +1
低 低 -1 +1 +1 -1

这张表看着复杂,其实规律很简单:TI1和TI2电平不同时,TI1边沿使计数器加1;电平相同时,TI1边沿使计数器减1。TI2的边沿逻辑正好相反。

2.3 计数方向判定

方向判定是编码器模式的核心。定时器内部有个DIR位(在TIMx_CR1寄存器里),硬件自动更新这个位。

  • DIR = 0:正转(计数器递增)
  • DIR = 1:反转(计数器递减)

我曾经踩过一个坑:电机正转时计数器递增,但反转时计数器递减到0后会溢出到ARR值。如果你用uint16_t读计数器,方向判断就会出错。后来我改用int16_t来读,负值直接表示反转,省事多了。

经验之谈:读取计数器值时,建议用(int16_t)TIMx->CNT强制转换为有符号数。这样正反转一目了然,不用额外判断DIR位。

2.4 配置寄存器详解

配置编码器模式,主要操作三个寄存器:TIMx_SMCR、TIMx_CCMR1和TIMx_CCER。我按配置顺序来讲。

2.4.1 TIMx_SMCR(从模式控制寄存器)

这个寄存器决定定时器的工作模式。编码器模式属于从模式的一种。

// 编码器模式配置(模式3:TI1和TI2都计数)
TIMx->SMCR &= ~TIM_SMCR_SMS;      // 先清零
TIMx->SMCR |= TIM_SMCR_SMS_0 | TIM_SMCR_SMS_1;  // SMS = 011,模式3

SMS[2:0]位取值:

SMS值 模式 计数边沿
001 模式1 仅TI1
010 模式2 仅TI2
011 模式3 TI1和TI2

我个人习惯用模式3,分辨率最高。但要注意,模式3下计数器更新频率是编码器线数的4倍,如果你的电机转速很高,要确保定时器不会溢出。

2.4.2 TIMx_CCMR1(捕获/比较模式寄存器1)

这个寄存器配置TI1和TI2的输入滤波和映射。

// 配置TI1为输入,映射到IC1
TIMx->CCMR1 &= ~TIM_CCMR1_CC1S;
TIMx->CCMR1 |= TIM_CCMR1_CC1S_0;   // CC1S = 01,TI1映射到IC1

// 配置TI2为输入,映射到IC2
TIMx->CCMR1 &= ~TIM_CCMR1_CC2S;
TIMx->CCMR1 |= TIM_CCMR1_CC2S_0;   // CC2S = 01,TI2映射到IC2

这里有个细节:IC1F[3:0]和IC2F[3:0]位用于输入滤波。如果编码器信号有毛刺,可以设置滤波长度。

注意:滤波值越大,抗干扰能力越强,但会引入延迟。高速电机建议用较小的滤波值(如ICF=0或1),低速电机可以适当加大(ICF=4~8)。我在做伺服驱动时,电机转速3000rpm,滤波设到4就够用了。

2.4.3 TIMx_CCER(捕获/使能寄存器)

这个寄存器使能输入通道,并配置极性。

// 使能CC1和CC2,极性不反转
TIMx->CCER &= ~(TIM_CCER_CC1P | TIM_CCER_CC2P);  // 不反转
TIMx->CCER |= TIM_CCER_CC1E | TIM_CCER_CC2E;     // 使能

如果编码器A/B相反了,不用改硬件,把CC1P或CC2P置1就行。这个技巧我在现场调试时用过很多次。

2.5 完整配置流程

嗯,这里要注意配置顺序。我总结了一个标准流程:

  1. 使能定时器时钟:RCC->APB1ENR或APB2ENR
  2. 配置GPIO:TI1和TI2对应的引脚设为复用功能
  3. 配置CCMR1:设置输入映射和滤波
  4. 配置CCER:使能通道,设置极性
  5. 配置SMCR:选择编码器模式
  6. 配置ARR和PSC:设置计数上限和预分频
  7. 使能计数器:CR1的CEN位置1

下面是一个完整的初始化代码示例:

void Encoder_Init(TIM_TypeDef* TIMx) {
    // 1. 使能时钟(以TIM3为例)
    RCC->APB1ENR |= RCC_APB1ENR_TIM3EN;
    
    // 2. GPIO配置(PA6=TI1, PA7=TI2)
    GPIOA->MODER &= ~(GPIO_MODER_MODER6 | GPIO_MODER_MODER7);
    GPIOA->MODER |= GPIO_MODER_MODER6_1 | GPIO_MODER_MODER7_1;  // 复用功能
    
    // 3. 配置CCMR1
    TIMx->CCMR1 &= ~(TIM_CCMR1_CC1S | TIM_CCMR1_CC2S);
    TIMx->CCMR1 |= TIM_CCMR1_CC1S_0 | TIM_CCMR1_CC2S_0;  // TI1->IC1, TI2->IC2
    
    // 4. 配置CCER
    TIMx->CCER &= ~(TIM_CCER_CC1P | TIM_CCER_CC2P);
    TIMx->CCER |= TIM_CCER_CC1E | TIM_CCER_CC2E;
    
    // 5. 配置SMCR(模式3)
    TIMx->SMCR &= ~TIM_SMCR_SMS;
    TIMx->SMCR |= TIM_SMCR_SMS_0 | TIM_SMCR_SMS_1;
    
    // 6. 配置ARR(假设编码器1000线,模式3下4000计数/圈)
    TIMx->ARR = 4000 - 1;
    TIMx->PSC = 0;  // 不分频
    
    // 7. 使能计数器
    TIMx->CR1 |= TIM_CR1_CEN;
}

2.6 知识结构图

下面这张图展示了编码器模式的核心知识体系,从信号输入到寄存器配置再到计数逻辑:

STM32定时器编码器模式知识结构图 编码器模式知识体系 正交编码信号 TI1 / TI2 输入 TIMx_SMCR SMS位选择模式 TIMx_CCMR1 CC1S/CC2S + 滤波 TIMx_CCER 使能 + 极性 计数逻辑核心 模式1:仅TI1边沿计数 | 模式2:仅TI2边沿计数 | 模式3:双沿计数(四倍频) 方向判定:TI1与TI2电平比较 → DIR位自动更新 计数器值 (TIMx_CNT) 递增 → 正转 | 递减 → 反转 💡 配置顺序:GPIO复用 → CCMR1输入映射 → CCER使能 → SMCR选择模式 → ARR/PSC → CEN启动 读取建议:使用 (int16_t)TIMx->CNT 直接获取有符号计数值

2.7 避坑指南

最后分享几个我实际项目中遇到的坑:

  • 计数器溢出问题:编码器模式没有自动重装载中断,计数器从ARR减到0或从0加到ARR时,不会产生更新事件。你需要用定时器的更新中断来辅助处理。
  • 初始值不确定:上电时计数器值不确定。我习惯在初始化后读一次CNT作为零点,后续值都减去这个零点。
  • GPIO速度要匹配:编码器信号频率高时,GPIO速度要设到High或Very High。我曾经用Low速度,结果丢了不少脉冲。

调试技巧:用逻辑分析仪同时抓TI1、TI2和CNT值(通过DAC或串口输出),可以直观看到计数逻辑是否正确。这个办法帮我解决过好几个疑难问题。