09-定时器详解.md 27 KB

STM32 定时器详解

基于江科大(13, 14, 15, 16, 17, 19)、铁头山羊(40, 42, 44)三教程综合整理


1. 定时器概述

STM32F10x 系列有 最多 8 个定时器,分为三种类型:

类型 定时器 位数 特点
高级定时器 TIM1, TIM8 16 位 带死区互补输出、重复计数器、刹车功能,适合电机控制
通用定时器 TIM2~TIM5 16 位 时基单元、输入捕获、输出比较、编码器接口、PWM 生成
基本定时器 TIM6, TIM7 16 位 仅时基单元(定时中断 + DAC 触发),无输入输出通道

这三个笔记综合覆盖的是 通用定时器 TIM2~TIM5,这也是最常用、功能最丰富的定时器。


2. 时基单元

定时器中断基本结构 上图:STM32 定时器中断基本结构(含时基单元、预分频器、计数器、自动重装寄存器)(来源:STM32入门教程 PPT 第57页)

预分频器时序图 上图:预分频器工作时序(来源:STM32入门教程 PPT 第58页)

2.1 组成

每个通用定时器都有一个 16 位的时基单元,由三个寄存器组成:

CK_PSC(定时器时钟,如 72 MHz)
    │
    ▼
┌──────────┐     ┌────────────┐     ┌──────────┐
│  PSC     │ ──→ │  CNT 计数器 │ ──→ │  ARR    │
│预分频器  │     │  (16-bit)  │     │自动重装  │
│16-bit    │     │  0~65535   │     │寄存器    │
└──────────┘     └────────────┘     └──────────┘
寄存器 全称 作用
PSC Prescaler(预分频器) 对 CK_PSC 进行分频,得到 CK_CNT = CK_PSC / (PSC + 1)
CNT Counter(计数器) 在 CK_CNT 驱动下递增/递减,每来一个脉冲 ±1
ARR Auto-Reload Register(自动重装载寄存器) 当 CNT 达到 ARR 时溢出归零(向上计数),产生更新事件

2.2 工作模式

模式 说明 应用场景
向上计数 CNT 从 0 递增到 ARR,溢出后归 0 最常用,定时中断、PWM
向下计数 CNT 从 ARR 递减到 0,溢出后回到 ARR 偶尔使用
中央对齐 CNT 从 0 到 ARR 来回摆动 需要对称 PWM 时

2.3 定时时间的计算

CK_CNT = CK_PSC / (PSC + 1)
T_溢出 = (ARR + 1) / CK_CNT
T_溢出 = (ARR + 1) × (PSC + 1) / CK_PSC

示例:CK_PSC = 72 MHz,PSC = 7199,ARR = 9999

CK_CNT = 72M / 7200 = 10000 Hz = 10 kHz
T_溢出 = (9999 + 1) / 10000 = 1 秒

2.4 代码:定时中断初始化

void Timer_Init(void)
{
    // 1. 开启 TIM2 时钟(TIM2 在 APB1 上)
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
    
    // 2. 选择内部时钟(默认就是内部时钟,此函数可选)
    TIM_InternalClockConfig(TIM2);
    
    // 3. 时基单元配置
    TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
    TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;      // 滤波器分频
    TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;  // 向上计数
    TIM_TimeBaseInitStructure.TIM_Period = 10000 - 1;                // ARR = 9999
    TIM_TimeBaseInitStructure.TIM_Prescaler = 7200 - 1;              // PSC = 7199
    TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;             // 高级定时器用
    TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
    
    // 4. 清除更新标志位(TIM_TimeBaseInit 末尾会手动产生更新事件)
    TIM_ClearFlag(TIM2, TIM_FLAG_Update);
    
    // 5. 开启更新中断
    TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
    
    // 6. NVIC 配置
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    NVIC_InitTypeDef NVIC_InitStructure;
    NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
    NVIC_Init(&NVIC_InitStructure);
    
    // 7. 启动定时器
    TIM_Cmd(TIM2, ENABLE);
}

2.5 中断服务函数

void TIM2_IRQHandler(void)
{
    if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)
    {
        // 用户代码:如变量自增、设置标志位等
        Num++;
        TIM_ClearITPendingBit(TIM2, TIM_IT_Update);  // 必须清除标志位
    }
}

2.6 HAL 库版本:定时中断

// HAL库 — 定时中断
TIM_HandleTypeDef htim2;

void MX_TIM2_Init(void)
{
    htim2.Instance = TIM2;
    htim2.Init.Prescaler = 7200 - 1;            // 72MHz/7200 = 10kHz
    htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
    htim2.Init.Period = 10000 - 1;              // 10kHz/10000 = 1Hz
    htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
    htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
    HAL_TIM_Base_Init(&htim2);

    HAL_NVIC_SetPriority(TIM2_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(TIM2_IRQn);

    HAL_TIM_Base_Start_IT(&htim2);              // 启动中断模式
}

// MSP 初始化:HAL 自动调用,负责底层 RCC/GPIO/NVIC
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM2) {
        __HAL_RCC_TIM2_CLK_ENABLE();
    }
}

// HAL 弱回调,需在用户文件中实现
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM2) {
        // 定时中断处理,如 Num++;
    }
}

注意:

  • HAL_TIM_Base_Init() 内部会调用 HAL_TIM_Base_MspInit() 开启时钟等底层配置。
  • AutoReloadPreload 使能后,ARR 在更新事件时更新,防止写入途中产生异常波形。
  • 中断回调使用弱函数 HAL_TIM_PeriodElapsedCallback,在 HAL 库里有一个默认 __weak 实现,用户需在同文件或主文件中覆盖。

3. 输出比较与 PWM

3.1 输出比较原理

输出比较(Output Compare)是定时器的输出功能,通过比较 CNTCCR(捕获/比较寄存器)的值,在引脚上输出特定的电平变化。

STM32 通用定时器有 4 个独立通道(CH1~CH4),每个通道有一个 CCR 寄存器。

3.2 PWM 模式

PWM(Pulse Width Modulation,脉冲宽度调制)是最常用的输出比较模式。

PWM 模式 1(向上计数):

  • CNT < CCR:输出有效电平(如高电平)
  • CNT ≥ CCR:输出无效电平(如低电平)
  • CNT 溢出 → CNT 归零,重新从 0 开始

           CCR  ARR
    CNT:  0───████████████████████████████████████████████████████████───0
    输出:████████████████ 低电平  ████████████████████████████████████
       ↑ 占空比 = CCR/ARR     ↑
    

3.3 PWM 参数计算

参数 公式 说明
频率 Freq = CK_PSC / (PSC+1) / (ARR+1) PWM 波的周期
占空比 Duty = CCR / (ARR+1) 高电平占整个周期的比例
分辨率 Res = 1 / (ARR+1) 占空比的调节步长

示例:72 MHz,PSC = 719,ARR = 99

Freq = 72M / 720 / 100 = 1000 Hz = 1 kHz
Duty(CCR=50) = 50 / 100 = 50%
Res = 1/100 = 1%

3.4 GPIO 配置注意

使用定时器 PWM 输出时,GPIO 必须设置为 复用推挽输出GPIO_Mode_AF_PP),因为引脚由外设控制而非 GPIO 模块直接控制。

3.5 代码:PWM 驱动 LED 呼吸灯

void PWM_Init(void)
{
    // 开启时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    
    // GPIO 复用推挽输出
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;       // TIM2_CH1 默认在 PA0
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    
    // 内部时钟
    TIM_InternalClockConfig(TIM2);
    
    // 时基单元:PSC=719, ARR=99 → Freq=1kHz
    TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
    TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
    TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseInitStructure.TIM_Period = 100 - 1;
    TIM_TimeBaseInitStructure.TIM_Prescaler = 720 - 1;
    TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
    TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
    
    // 输出比较配置:PWM 模式 1
    TIM_OCInitTypeDef TIM_OCInitStructure;
    TIM_OCStructInit(&TIM_OCInitStructure);               // 初始化结构体默认值
    TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
    TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
    TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
    TIM_OCInitStructure.TIM_Pulse = 0;                     // 初始 CCR=0
    TIM_OC1Init(TIM2, &TIM_OCInitStructure);
    
    // 启动定时器
    TIM_Cmd(TIM2, ENABLE);
}

// 设置占空比
void PWM_SetCompare1(uint16_t Compare)
{
    TIM_SetCompare1(TIM2, Compare);  // Compare: 0~100 对应占空比 0%~100%
}

主函数:呼吸灯效果

int main(void)
{
    PWM_Init();
    
    while (1)
    {
        // 渐亮
        for (uint8_t i = 0; i <= 100; i++)
        {
            PWM_SetCompare1(i);
            Delay_ms(10);
        }
        // 渐暗
        for (uint8_t i = 0; i <= 100; i++)
        {
            PWM_SetCompare1(100 - i);
            Delay_ms(10);
        }
    }
}

3.6 HAL 库版本:PWM 输出

// HAL库 — PWM 驱动 LED 呼吸灯
TIM_HandleTypeDef htim2;

void MX_TIM2_Init(void)
{
    TIM_OC_InitTypeDef sConfigOC = {0};

    htim2.Instance = TIM2;
    htim2.Init.Prescaler = 720 - 1;             // 72MHz/720 = 100kHz
    htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
    htim2.Init.Period = 100 - 1;                 // 100kHz/100 = 1kHz
    htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
    htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
    HAL_TIM_PWM_Init(&htim2);

    sConfigOC.OCMode = TIM_OCMODE_PWM1;
    sConfigOC.Pulse = 0;                          // 初始 CCR=0
    sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
    sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
    HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_1);

    HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);
}

void HAL_TIM_PWM_MspInit(TIM_HandleTypeDef *htim)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    if (htim->Instance == TIM2) {
        __HAL_RCC_TIM2_CLK_ENABLE();
        __HAL_RCC_GPIOA_CLK_ENABLE();
        GPIO_InitStruct.Pin = GPIO_PIN_0;
        GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;   // 复用推挽输出
        GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
        HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
    }
}

// 设置占空比(宏写法,与 GPIO 无关)
void PWM_SetCompare1(uint16_t Compare)
{
    __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, Compare);
}

与标准库对照: | 标准库 | HAL 库 | |--------|--------| | TIM_OC1Init(TIM2, &...) | HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_1) | | TIM_SetCompare1(TIM2, val) | __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, val) | | GPIO_Mode_AF_PP | GPIO_MODE_AF_PP | | TIM_Cmd(TIM2, ENABLE) | HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1) |

引脚重映射:如果 PA0 被占用,可以将 TIM2_CH1 重映射到其他引脚:

RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
GPIO_PinRemapConfig(GPIO_PartialRemap1_TIM2, ENABLE);  // 部分重映射
// 或完全重映射参考手册确定映射关系

4. 输入捕获

4.1 输入捕获原理

输入捕获(Input Capture)是定时器的输入功能,用于 测量外部信号的频率、周期、占空比

外部信号 → TIMx_CH1(PA6) → 输入滤波器 → 边沿检测器 → ICx → 捕获 CCR
                                           ↑
                                      触发信号(上升/下降沿)

当检测到指定边沿时,当前 CNT 的值被立即锁存到 CCR 寄存器,同时可以触发中断。

4.2 测频法 vs 测周法

方法 原理 适用频率
测频法 固定时间 Ts 内统计脉冲个数 N,f = N/Ts 高频信号
测周法 测量两个上升沿之间的周期 T,f = 1/T 低频信号

4.3 测周法的实现(从模式复位)

最常用的输入捕获方式:从模式复位,即 TI1 的上升沿触发 CNT 复位。

             第一个上升沿        第二个上升沿
                 │                  │
外部信号  ───────┴────────  ────────┴────────
CNT       0────████████████████████████
                 ↑                   ↑
        捕获到 CCR1=0      捕获到 CCR1=N
        随即 CNT 归零       f = CK_CNT / N

4.4 代码:输入捕获测频率

void IC_Init(void)
{
    // 开启时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    
    // GPIO 上拉输入(TIM3_CH1 默认在 PA6)
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    
    // 内部时钟
    TIM_InternalClockConfig(TIM3);
    
    // 时基单元:PSC=71 → CK_CNT = 1MHz(测周法使用)
    TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
    TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
    TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseInitStructure.TIM_Period = 65536 - 1;    // ARR 最大
    TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1;     // CK_CNT = 1MHz
    TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
    TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure);
    
    // 输入捕获初始化:通道 1,上升沿,直通
    TIM_ICInitTypeDef TIM_ICInitStructure;
    TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;
    TIM_ICInitStructure.TIM_ICFilter = 0xF;                // 最大滤波
    TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;
    TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;  // 捕获不分频
    TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;
    TIM_ICInit(TIM3, &TIM_ICInitStructure);
    
    // 从模式复位:TI1FP1 上升沿触发 CNT 归零
    TIM_SelectInputTrigger(TIM3, TIM_TS_TI1FP1);
    TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_Reset);
    
    // 启动定时器
    TIM_Cmd(TIM3, ENABLE);
}

// 获取频率:f = CK_CNT / (CCR1 + 1)
uint32_t IC_GetFreq(void)
{
    return 1000000 / (TIM_GetCapture1(TIM3) + 1);
}

4.5 测频率 + 占空比(PWMI 模式)

双通道同时捕获:通道 1 测频率(上升沿触发复位),通道 2 测占空比(下降沿捕获)。

上升沿    下降沿  上升沿
  │         │      │
──┘─────────┴──────┘──
  ↑                 ↑
  CH1 捕获 CCR1     CH1 捕获 CCR1(从模式复位,CNT 归零)
           ↑
           CH2 捕获占用比(下降沿触发)
           
频率 f = CK_CNT / CCR1
占空比 Duty = CCR2 / CCR1

配置差别:将通道 2 设置为相反的极性(下降沿),选择交叉输入(TIM_ICSelection_IndirectTI)。

4.6 HAL 库版本:输入捕获测频率

// HAL库 — 输入捕获测频率
TIM_HandleTypeDef htim3;

void MX_TIM3_IC_Init(void)
{
    TIM_IC_InitTypeDef sConfigIC = {0};

    htim3.Instance = TIM3;
    htim3.Init.Prescaler = 72 - 1;               // 72MHz/72 = 1MHz
    htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
    htim3.Init.Period = 65536 - 1;               // ARR 最大
    htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
    htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
    HAL_TIM_IC_Init(&htim3);

    sConfigIC.ICPolarity = TIM_INPUTCHANNELPOLARITY_RISING;
    sConfigIC.ICSelection = TIM_ICSELECTION_DIRECTTI;
    sConfigIC.ICPrescaler = TIM_ICPSC_DIV1;
    sConfigIC.ICFilter = 0xF;
    HAL_TIM_IC_ConfigChannel(&htim3, &sConfigIC, TIM_CHANNEL_1);

    // 从模式复位:TI1FP1 上升沿触发 CNT 归零(HAL 无封装,直接用寄存器)
    TIM_SelectInputTrigger(TIM3, TIM_TS_TI1FP1);
    TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_Reset);

    HAL_NVIC_SetPriority(TIM3_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(TIM3_IRQn);

    HAL_TIM_IC_Start_IT(&htim3, TIM_CHANNEL_1);
}

void HAL_TIM_IC_MspInit(TIM_HandleTypeDef *htim)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    if (htim->Instance == TIM3) {
        __HAL_RCC_TIM3_CLK_ENABLE();
        __HAL_RCC_GPIOA_CLK_ENABLE();
        GPIO_InitStruct.Pin = GPIO_PIN_6;
        GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
        GPIO_InitStruct.Pull = GPIO_PULLUP;
        HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
    }
}

// 捕获回调:发生捕获时自动调用
uint32_t CapturedValue = 0;
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM3) {
        CapturedValue = HAL_TIM_ReadCapturedValue(&htim3, TIM_CHANNEL_1);
        // 频率 f = 1000000 / (CapturedValue + 1)
    }
}

PWMI 模式(测频率+占空比) HAL 库配置:

// 通道 1:上升沿,直通
sConfigIC.ICPolarity = TIM_INPUTCHANNELPOLARITY_RISING;
sConfigIC.ICSelection = TIM_ICSELECTION_DIRECTTI;
HAL_TIM_IC_ConfigChannel(&htim3, &sConfigIC, TIM_CHANNEL_1);

// 通道 2:下降沿,交叉输入
sConfigIC.ICPolarity = TIM_INPUTCHANNELPOLARITY_FALLING;
sConfigIC.ICSelection = TIM_ICSELECTION_INDIRECTTI;
HAL_TIM_IC_ConfigChannel(&htim3, &sConfigIC, TIM_CHANNEL_2);

// 从模式复位
TIM_SelectInputTrigger(TIM3, TIM_TS_TI1FP1);
TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_Reset);

HAL_TIM_IC_Start_IT(&htim3, TIM_CHANNEL_1);
HAL_TIM_IC_Start_IT(&htim3, TIM_CHANNEL_2);

// 回调中读取:
//   uint32_t Period   = HAL_TIM_ReadCapturedValue(&htim3, TIM_CHANNEL_1);
//   uint32_t DutyPulse = HAL_TIM_ReadCapturedValue(&htim3, TIM_CHANNEL_2);
//   频率 f = CK_CNT / Period
//   占空比 = DutyPulse / Period

5. 编码器接口

5.1 编码器模式原理

STM32 定时器的 编码器接口模式 可以直接连接 增量式旋转编码器(如 EC11) 的两路正交信号,自动完成正反转判断和计数

与 EXTI 方式(前面 Note 1 中的 5-2 示例)相比:

  • EXTI 方式:软件判断方向,仅能处理低速信号,占用两个中断
  • 定时器编码器模式:硬件自动处理,支持高速,不占用中断,是推荐方式

5.2 工作原理

编码器接法:TI1(PA6)接 A 相,TI2(PA7)接 B 相。

计数方向 TI1 电平(相对 TI2) TI2 电平(相对 TI1)
正转(递增) 超前 90° 滞后 90°
反转(递减) 滞后 90° 超前 90°

定时器根据两路信号的边沿顺序自动判断方向,对 CNT 进行 递增或递减

5.3 三种编码器模式

模式 TI1 边沿 TI2 边沿 单圈脉冲数
TI1 计数 忽略 1 倍
TI2 忽略 计数 1 倍
TI1+TI2(推荐) 计数 计数 2 倍 / 4 倍

4 倍频:TI1 和 TI2 的上升沿+下降沿都计数的模式。效果:编码器一圈 20 个脉冲 → 计数 80。

5.4 代码:编码器接口测速

void Encoder_Init(void)
{
    // 开启时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    
    // GPIO 上拉输入:PA6(TIM3_CH1), PA7(TIM3_CH2)
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    
    // 时基单元:PSC=0, ARR=最大(65535),自由运行
    TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
    TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
    TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseInitStructure.TIM_Period = 65536 - 1;
    TIM_TimeBaseInitStructure.TIM_Prescaler = 1 - 1;         // 不分频
    TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
    TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure);
    
    // 输入捕获初始化(为编码器模式做准备)
    TIM_ICInitTypeDef TIM_ICInitStructure;
    TIM_ICStructInit(&TIM_ICInitStructure);
    
    TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;
    TIM_ICInitStructure.TIM_ICFilter = 0xF;                  // 滤波去抖动
    TIM_ICInit(TIM3, &TIM_ICInitStructure);
    
    TIM_ICInitStructure.TIM_Channel = TIM_Channel_2;
    TIM_ICInitStructure.TIM_ICFilter = 0xF;
    TIM_ICInit(TIM3, &TIM_ICInitStructure);
    
    // 编码器模式配置(必须在输入捕获之后!)
    TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12,
        TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);
    // 注意:这里的 Rising/Falling 表示是否反相,而非真正的边沿
    
    // 启动定时器
    TIM_Cmd(TIM3, ENABLE);
}

// 获取增量值并清零
int16_t Encoder_Get(void)
{
    int16_t Temp;
    Temp = TIM_GetCounter(TIM3);
    TIM_SetCounter(TIM3, 0);
    return Temp;
}

测速主函数(结合定时中断,每隔固定时间读取一次):

int16_t Speed;

void TIM2_IRQHandler(void)
{
    if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)
    {
        Speed = Encoder_Get();        // 每 1 秒(或定时间隔)读一次增量值
        TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
    }
}

int main(void)
{
    OLED_Init();
    Timer_Init();      // TIM2 每隔 1 秒中断一次
    Encoder_Init();    // TIM3 编码器模式
    
    while (1)
    {
        OLED_ShowSignedNum(1, 7, Speed, 5);
    }
}

5.5 HAL 库版本:编码器接口

// HAL库 — 编码器接口测速
TIM_HandleTypeDef htim3;
TIM_Encoder_InitTypeDef sEncoder = {0};

void MX_TIM3_Encoder_Init(void)
{
    htim3.Instance = TIM3;
    htim3.Init.Prescaler = 1 - 1;                // 不分频
    htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
    htim3.Init.Period = 65536 - 1;               // ARR 最大,自由运行
    htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
    htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;

    sEncoder.EncoderMode = TIM_ENCODERMODE_TI12;  // TI1+TI2 双沿计数
    sEncoder.IC1Polarity = TIM_ICPOLARITY_RISING;
    sEncoder.IC1Selection = TIM_ICSELECTION_DIRECTTI;
    sEncoder.IC1Prescaler = TIM_ICPSC_DIV1;
    sEncoder.IC1Filter = 0xF;
    sEncoder.IC2Polarity = TIM_ICPOLARITY_RISING;
    sEncoder.IC2Selection = TIM_ICSELECTION_DIRECTTI;
    sEncoder.IC2Prescaler = TIM_ICPSC_DIV1;
    sEncoder.IC2Filter = 0xF;

    HAL_TIM_Encoder_Init(&htim3, &sEncoder);

    HAL_TIM_Encoder_Start(&htim3, TIM_CHANNEL_ALL);
}

void HAL_TIM_Encoder_MspInit(TIM_HandleTypeDef *htim)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    if (htim->Instance == TIM3) {
        __HAL_RCC_TIM3_CLK_ENABLE();
        __HAL_RCC_GPIOA_CLK_ENABLE();
        GPIO_InitStruct.Pin = GPIO_PIN_6 | GPIO_PIN_7;
        GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
        GPIO_InitStruct.Pull = GPIO_PULLUP;
        HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
    }
}

// 获取增量值并清零
int16_t Encoder_Get(void)
{
    int16_t Temp = (int16_t)__HAL_TIM_GET_COUNTER(&htim3);
    __HAL_TIM_SET_COUNTER(&htim3, 0);
    return Temp;
}

注意:

  • HAL_TIM_Encoder_Init() 内部同时完成了时基单元 + 编码器配置 + 通道滤波,无需额外调用 TIM_ICInit
  • 编码器模式下 AutoReloadPreload 通常设为 DISABLE,因为需要 CNT 对 ARR 立即响应。
  • __HAL_TIM_GET_COUNTER / __HAL_TIM_SET_COUNTER 是宏,代替标准库的 TIM_GetCounter / TIM_SetCounter

6. 定时器关键函数速查

基础操作

标准库 HAL 库(宏/函数) 用途
TIM_Cmd(TIMx, ENABLE/DISABLE) HAL_TIM_Base_Start/Stop(&htim) 启动/停止定时器
TIM_InternalClockConfig(TIMx) HAL 初始化默认内部时钟 内部时钟
TIM_ITConfig(TIMx, TIM_IT_Update, ENABLE) HAL_TIM_Base_Start_IT(&htim) 使能更新中断
TIM_SetCounter(TIMx, Val) __HAL_TIM_SET_COUNTER(&htim, Val) 写 CNT
TIM_GetCounter(TIMx) __HAL_TIM_GET_COUNTER(&htim) 读 CNT
TIM_ClearFlag(TIMx, flag) __HAL_TIM_CLEAR_FLAG(&htim, flag) 清除标志

输出比较 / PWM

标准库 HAL 库(宏/函数) 用途
TIM_SetCompare1(TIMx, Val) __HAL_TIM_SET_COMPARE(&htim, CH, Val) 设置 CCR
TIM_GetCompare1(TIMx) __HAL_TIM_GET_COMPARE(&htim, CH) 读取 CCR
TIM_OC1PreloadConfig(TIMx, ENABLE) sConfigOC.OCFastMode + AutoReloadPreload 预装载
TIM_OCStructInit(&s) 直接给 TIM_OC_InitTypeDef 成员赋值 初始化结构体

输入捕获

标准库 HAL 库(宏/函数) 用途
TIM_GetCapture1(TIMx) HAL_TIM_ReadCapturedValue(&htim, CH) 读取捕获值
TIM_SelectInputTrigger(TIMx, src) 寄存器直接操作(HAL 无封装) 选择触发源
TIM_SelectSlaveMode(TIMx, mode) 寄存器直接操作(HAL 无封装) 从模式配置

编码器

标准库 HAL 库 用途
TIM_EncoderInterfaceConfig(...) HAL_TIM_Encoder_Init(&htim, &sEncoder) 配置编码器模式
TIM_ICInit(TIMx, &s)TIM_EncoderInterfaceConfig(...) HAL_TIM_Encoder_Init() 一次完成 一步到位

时钟源切换

函数 用途
TIM_InternalClockConfig(TIMx) 内部时钟(CK_INT)
TIM_ITRxExternalClockConfig(TIMx, TIM_TS_ITR0~3) 其他定时器输出作为时钟
TIM_TIxExternalClockConfig(TIMx, ...) 外部引脚的时钟
TIM_ETRClockMode2Config(TIMx, ...) ETR 引脚外部时钟

7. 常见问题

7.1 定时时间不准

  • 检查 APB1 分频:TIM2~TIM7 在 APB1≠1 时时钟 = APB1×2
  • PSC 和 ARR 的范围是 0~65535(16 位),写入值需要 -1
  • 计算:T = (PSC+1)×(ARR+1) / CK_PSC

7.2 进入中断后停不下来

症状:中断持续触发,主循环无法正常运行。

原因:中断标志位没有清除。

解决

TIM_ClearITPendingBit(TIMx, TIM_IT_Update);
// 或 EXTI 中断:
EXTI_ClearITPendingBit(EXTI_LineX);

7.3 TIM_TimeBaseInit 后立即触发中断

TIM_TimeBaseInit() 函数末尾会手动产生一次更新事件(UG 位)。如果在初始化之前开启了中断,CNT=0 时的 ARR=0 状态也会导致立即触发。

解决

TIM_ClearFlag(TIMx, TIM_FLAG_Update);  // 在 TIM_ITConfig 之前清除

7.4 编码器模式与输入捕获的顺序

必须先调用 TIM_ICInit() 初始化通道,再调用 TIM_EncoderInterfaceConfig() 配置编码器模式。反过来会使得输入捕获的配置覆盖编码器模式。

7.5 TIM_OCStructInit 的必要性

如果 TIM_OCInitStructure 中的某些成员(如 TIM_OCIdleState, TIM_OCNIdleState)没有显式赋值,在高级定时器上可能会得到意外结果。使用 TIM_OCStructInit() 可以给所有成员赋默认值,避免不确定行为。