前言

在嵌入式系统开发中,定时器是最常用也最容易被低估的外设之一。很多开发者对定时器的理解仅仅停留在"定时中断"的层面,却不知道一个高级定时器所能实现的功能远远超出想象——它可以生成高精度 PWM 波形、精确测量脉冲信号、实现编码器接口、驱动步进电机和无刷电机,甚至可以不占用 CPU 资源完成复杂的波形生成。

STM32 的定时器系统设计得极其精巧,尤其是高级控制定时器(Advanced-control Timer),如 TIM1、TIM8 等,其内部包含了多达几十个寄存器,支持多种工作模式。对于电机控制这样的实时性要求极高的应用场景,高级定时器几乎是不可或缺的。

然而,正是因为其功能强大,高级定时器的学习曲线也相当陡峭。很多开发者对着参考手册上的寄存器描述看了几天,依然搞不清捕获/比较通道、互补输出、死区插入、刹车功能这些概念到底是怎么回事。更不用说将这些功能组合起来实现一个完整的 FOC(磁场定向控制)电机驱动了。

本文将从定时器的基本原理出发,层层深入,带你彻底理解 STM32 高级定时器的每一个功能模块。我们不仅会讲解理论,更会通过大量的代码示例,从简单的 PWM 输出开始,一步步实现基于高级定时器的 BLDC 无刷电机六步换向控制。

无论你是刚开始接触 STM32 的新手,还是希望深入理解定时器硬件原理的资深开发者,相信这篇文章都能给你带来新的收获。

一、STM32 定时器家族:不止是"计数"那么简单

很多人学习 STM32 定时器时的第一个困惑就是:为什么 STM32 要有这么多种定时器?从 TIM2 到 TIM17,编号一大堆,每个定时器的功能还都不太一样,很容易搞混。

实际上,ST 对定时器的分类是非常清晰的,按照功能从简单到复杂,可以分为以下几类:

1.1 基本定时器(Basic Timer):TIM6、TIM7

基本定时器正如其名,功能最简单,只有最核心的定时功能。它没有外部 IO 引脚,也没有捕获/比较通道,只能实现最基本的定时中断和 DAC 触发。

基本定时器的典型应用场景:

  • 实现固定间隔的定时中断(如 1ms 系统滴答)
  • 作为 DAC 的转换触发信号
  • 简单的延时功能

如果你只需要"多少时间后做什么事",基本定时器就足够了,它的资源占用也最小。

1.2 通用定时器(General-purpose Timer):TIM2 ~ TIM5、TIM9 ~ TIM14

通用定时器是使用最广泛的一类定时器。它们具有 4 个独立的捕获/比较通道,可以实现:

  • 输入捕获:测量外部脉冲的频率、占空比
  • 输出比较:生成各种波形
  • PWM 输出:生成电机控制所需的 PWM 信号
  • 单脉冲模式:生成精确的单脉冲输出
  • 编码器接口:对接正交编码器

通用定时器又可以细分为两个子类:

  • 16 位通用定时器(TIM9 ~ TIM14):功能相对简化,通常只有 2 个通道
  • 32 位通用定时器(TIM2、TIM5):计数器位数更多,可以实现更长的定时周期

对于大多数应用来说,通用定时器已经能满足需求了。比如你要驱动一个普通的直流电机,用通用定时器生成 PWM 就完全够用。

1.3 高级控制定时器(Advanced-control Timer):TIM1、TIM8

这才是本文的主角。高级控制定时器在通用定时器的基础上,增加了一系列专为电机控制和电源变换应用设计的高级功能:

  • 互补输出与死区插入:驱动 H 桥电路必备,防止上下桥臂直通
  • 中央对齐模式:生成对称的 PWM 波形,降低电机噪音
  • 刹车输入:紧急情况下快速关断输出,保护硬件
  • 重复计数器:更灵活的中断周期控制
  • 多个 DMA 请求:配合 DMA 实现无 CPU 干预的波形生成

可以说,如果你要做 BLDC 无刷电机、步进电机、开关电源这类应用,高级定时器是必不可少的。普通的通用定时器根本无法满足这些场景的复杂需求。

1.4 如何选择合适的定时器

这里给大家一个简单的选型指南:

应用场景 推荐定时器类型
简单定时中断 基本定时器 TIM6/TIM7
普通 PWM 输出 通用定时器 TIM2~TIM5
输入捕获测量 通用定时器 TIM2~TIM5
正交编码器接口 通用定时器 TIM2~TIM5
BLDC 电机控制 高级定时器 TIM1/TIM8
开关电源/PFC 高级定时器 TIM1/TIM8
需要互补输出 高级定时器 TIM1/TIM8

理解了这些分类,你在设计硬件和编写代码时就不会选错定时器了。

二、高级定时器的硬件架构

在深入各个功能模块之前,让我们先从整体上理解高级定时器的硬件结构。很多人学定时器学不懂,就是因为一上来就钻进寄存器的细节里,没有建立起整体的认知。

STM32 高级定时器整体架构

2.1 时钟源:一切的起点

定时器的核心是一个计数器,而计数器需要时钟信号才能运转。高级定时器的时钟源可以来自多个地方:

  1. 内部时钟(CK_INT):最常用的时钟源,来自 APB 总线时钟。需要注意的是,当 APB 预分频器不为 1 时,定时器时钟是 APB 时钟的 2 倍。比如 APB1 时钟是 42MHz,那么 TIM2~TIM5 的时钟就是 84MHz。

  2. 外部时钟模式 1:通过外部引脚(TI1、TI2)输入的时钟信号,可以是编码器、外部振荡器等。

  3. 外部时钟模式 2:通过 ETR 引脚输入的外部时钟,支持更灵活的触发配置。

  4. 内部触发连接(ITR):一个定时器可以作为另一个定时器的时钟源,实现定时器级联。

对于大多数应用,我们使用内部时钟就足够了。

2.2 时基单元:定时器的心脏

时基单元是定时器最核心的部分,它由三个关键寄存器组成:

  • 预分频器(PSC):对输入时钟进行分频,16 位,可以设置 0~65535 的分频系数
  • 计数器(CNT):实际的计数器,高级定时器是 16 位的
  • 自动重装载寄存器(ARR):计数器的上限值,当 CNT 达到 ARR 时,发生溢出事件

这三个寄存器的组合决定了定时器的溢出频率:

定时器溢出频率 = 定时器时钟 / ((PSC + 1) * (ARR + 1))

举个例子,如果定时器时钟是 168MHz,我们设置 PSC=167,ARR=999,那么溢出频率就是:

168,000,000 / ((167+1) * (999+1)) = 168,000,000 / (168 * 1000) = 1000 Hz

也就是每 1ms 溢出一次。这个公式非常重要,几乎所有定时器配置都会用到。

2.3 捕获/比较通道:功能的扩展

高级定时器有 4 个独立的捕获/比较通道(CH1~CH4)。每个通道都有自己的捕获/比较寄存器(CCR),可以独立配置为输入捕获模式或输出比较模式。

这就是定时器设计的巧妙之处——同一个硬件通道,既可以用来接收外部信号(输入捕获),也可以用来输出波形(输出比较)。方向完全由软件配置决定。

每个通道还包含:

  • 输入滤波器:消除输入信号的抖动
  • 输入多路复用:选择信号来源
  • 输出控制:决定输出极性、使能等

理解捕获/比较通道的工作原理,是掌握定时器高级功能的关键。

2.4 高级功能模块

除了上述通用部分,高级定时器还增加了专属的功能模块:

  • 死区时间发生器:为互补输出插入死区,防止桥臂直通
  • 刹车电路:紧急停止功能,保护功率电路
  • 互补输出通道:CH1N~CH3N,与 CH1~CH3 形成互补对
  • 重复计数器:控制更新事件的产生频率

这些模块共同构成了完整的电机控制硬件加速方案。

(第一部分完,约2200字)

三、PWM 生成原理与配置

PWM(脉冲宽度调制)是定时器最常用的功能之一,也是电机控制的基础。通过改变 PWM 的占空比,我们可以精确控制电机的转速;通过改变 PWM 的相位和时序,我们可以实现各种复杂的电机换向逻辑。

3.1 PWM 的基本原理

PWM 本质上是一种周期性的方波信号,它有两个关键参数:

  • 周期:信号重复的频率,由 ARR 寄存器决定
  • 占空比:高电平时间占整个周期的比例,由 CCR 寄存器决定

STM32 定时器支持多种 PWM 模式,其中最常用的是 PWM 模式 1 和 PWM 模式 2:

模式 向上计数时行为 向下计数时行为
PWM 模式 1 CNT < CCR 时有效,否则无效 CNT > CCR 时无效,否则有效
PWM 模式 2 CNT < CCR 时无效,否则有效 CNT > CCR 时有效,否则无效

所谓"有效"和"无效",还取决于输出极性的设置。如果设置为高电平有效,那么"有效"就对应高电平,“无效"对应低电平。

3.2 边沿对齐 vs 中央对齐

这是很多初学者容易忽视但对电机控制非常重要的一个概念。

边沿对齐模式(Edge-aligned):计数器只从 0 向上计数到 ARR,然后重置为 0 重新开始。PWM 的变化只发生在计数器的一个方向上。

中央对齐模式(Center-aligned):计数器先从 0 向上计数到 ARR,然后再从 ARR 向下计数到 0。这样每个周期内计数器会经过两次 CCR 值,PWM 波形是对称的。

为什么中央对齐模式对电机控制很重要?因为:

  1. 对称的 PWM 波形可以降低电机的电流纹波
  2. 减少电机运行时的噪音和振动
  3. 对于 FOC 控制,中央对齐模式可以让电流采样更精确

当然,中央对齐模式也有代价:在相同的 ARR 设置下,PWM 频率会是边沿对齐模式的一半,因为计数器需要走完一个来回才算一个周期。

3.3 PWM 配置代码示例

让我们来看一个具体的配置示例。假设我们使用 STM32F4 的 TIM1,时钟频率是 168MHz,想要生成 20kHz 的 PWM 用于电机控制:

// 计算 PWM 周期:1/20kHz = 50us
// ARR = 定时器时钟 / PWM 频率 - 1 = 168MHz / 20kHz - 1 = 8399
#define PWM_PERIOD    8399
#define PWM_DEAD_TIME 100  // 死区时间,单位:定时器时钟周期

void TIM1_PWM_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct;
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStruct;
    TIM_OCInitTypeDef TIM_OCInitStruct;
    TIM_BDTRInitTypeDef TIM_BDTRInitStruct;
    
    // 1. 使能时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE);
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA | RCC_AHB1Periph_GPIOB, ENABLE);
    
    // 2. 配置 GPIO 为复用功能
    // PA8: TIM1_CH1, PA9: TIM1_CH2, PA10: TIM1_CH3
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9 | GPIO_Pin_10;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_100MHz;
    GPIO_InitStruct.GPIO_OType = GPIO_OType_PP;
    GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_NOPULL;
    GPIO_Init(GPIOA, &GPIO_InitStruct);
    
    // PB13: TIM1_CH1N, PB14: TIM1_CH2N, PB15: TIM1_CH3N
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15;
    GPIO_Init(GPIOB, &GPIO_InitStruct);
    
    // 映射复用功能
    GPIO_PinAFConfig(GPIOA, GPIO_PinSource8, GPIO_AF_TIM1);
    GPIO_PinAFConfig(GPIOA, GPIO_PinSource9, GPIO_AF_TIM1);
    GPIO_PinAFConfig(GPIOA, GPIO_PinSource10, GPIO_AF_TIM1);
    GPIO_PinAFConfig(GPIOB, GPIO_PinSource13, GPIO_AF_TIM1);
    GPIO_PinAFConfig(GPIOB, GPIO_PinSource14, GPIO_AF_TIM1);
    GPIO_PinAFConfig(GPIOB, GPIO_PinSource15, GPIO_AF_TIM1);
    
    // 3. 配置时基单元
    TIM_TimeBaseStruct.TIM_Prescaler = 0;  // 不分频
    TIM_TimeBaseStruct.TIM_CounterMode = TIM_CounterMode_CenterAligned1;  // 中央对齐
    TIM_TimeBaseStruct.TIM_Period = PWM_PERIOD;
    TIM_TimeBaseStruct.TIM_ClockDivision = TIM_CKD_DIV1;
    TIM_TimeBaseStruct.TIM_RepetitionCounter = 0;
    TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStruct);
    
    // 4. 配置输出比较模式为 PWM
    TIM_OCInitStruct.TIM_OCMode = TIM_OCMode_PWM1;
    TIM_OCInitStruct.TIM_OutputState = TIM_OutputState_Enable;
    TIM_OCInitStruct.TIM_OutputNState = TIM_OutputNState_Enable;  // 使能互补输出
    TIM_OCInitStruct.TIM_Pulse = 0;  // 初始占空比为 0
    TIM_OCInitStruct.TIM_OCPolarity = TIM_OCPolarity_High;
    TIM_OCInitStruct.TIM_OCNPolarity = TIM_OCNPolarity_High;
    TIM_OCInitStruct.TIM_OCIdleState = TIM_OCIdleState_Reset;
    TIM_OCInitStruct.TIM_OCNIdleState = TIM_OCNIdleState_Reset;
    
    TIM_OC1Init(TIM1, &TIM_OCInitStruct);
    TIM_OC2Init(TIM1, &TIM_OCInitStruct);
    TIM_OC3Init(TIM1, &TIM_OCInitStruct);
    
    // 使能 CCR 预装载
    TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable);
    TIM_OC2PreloadConfig(TIM1, TIM_OCPreload_Enable);
    TIM_OC3PreloadConfig(TIM1, TIM_OCPreload_Enable);
    
    // 5. 配置死区时间和刹车功能
    TIM_BDTRInitStruct.TIM_OSSRState = TIM_OSSRState_Enable;
    TIM_BDTRInitStruct.TIM_OSSIState = TIM_OSSIState_Enable;
    TIM_BDTRInitStruct.TIM_LOCKLevel = TIM_LOCKLevel_OFF;
    TIM_BDTRInitStruct.TIM_DeadTime = PWM_DEAD_TIME;  // 死区时间
    TIM_BDTRInitStruct.TIM_Break = TIM_Break_Disable;  // 暂时禁用刹车
    TIM_BDTRInitStruct.TIM_BreakPolarity = TIM_BreakPolarity_High;
    TIM_BDTRInitStruct.TIM_AutomaticOutput = TIM_AutomaticOutput_Enable;
    TIM_BDTRConfig(TIM1, &TIM_BDTRInitStruct);
    
    // 使能 ARR 预装载
    TIM_ARRPreloadConfig(TIM1, ENABLE);
    
    // 6. 启动定时器
    TIM_Cmd(TIM1, ENABLE);
    
    // 7. 使能主输出(高级定时器必须设置这一步!)
    TIM_CtrlPWMOutputs(TIM1, ENABLE);
}

这里有一个非常重要的细节:高级定时器必须调用 TIM_CtrlPWMOutputs() 使能主输出,否则 PWM 信号不会出现在引脚上!这是很多新手踩过的坑——代码看起来都对,但就是没有波形输出。

配置完成后,我们就可以通过修改 CCR 寄存器的值来改变 PWM 占空比了:

// 设置通道 1 占空比为 50%
TIM_SetCompare1(TIM1, PWM_PERIOD / 2);

// 设置通道 2 占空比为 25%
TIM_SetCompare2(TIM1, PWM_PERIOD / 4);

// 设置通道 3 占空比为 75%
TIM_SetCompare3(TIM1, PWM_PERIOD * 3 / 4);

四、输入捕获:精确测量脉冲信号

除了输出波形,定时器的另一个重要功能是输入捕获——精确测量外部输入信号的频率、占空比、脉冲宽度等参数。

4.1 输入捕获的工作原理

输入捕获的基本思路很简单:当外部引脚上发生指定的跳变沿(上升沿或下降沿)时,硬件会自动将当前计数器的值锁存到 CCR 寄存器中,同时可以触发中断。

通过两次捕获之间的计数器差值,我们就能计算出:

  • 脉冲宽度:从上升沿到下降沿的时间
  • 信号周期:两次上升沿之间的时间
  • 信号频率:周期的倒数

4.2 输入滤波器:消除抖动

机械开关、霍尔传感器这类信号通常会有抖动,如果直接捕获可能会产生误触发。STM32 定时器的每个输入通道都带有数字滤波器,可以配置不同的滤波参数。

滤波器的工作原理是:只有当连续 N 个采样周期都检测到相同的电平时,才认为这是一个有效的电平变化。采样频率和 N 值都可以配置,滤波时间 = N / 采样频率。

4.3 输入捕获代码示例

假设我们要测量霍尔传感器的脉冲信号频率:

volatile uint32_t capture_value = 0;
volatile uint32_t frequency = 0;
volatile uint8_t capture_done = 0;

void TIM2_IC_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct;
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStruct;
    TIM_ICInitTypeDef TIM_ICInitStruct;
    NVIC_InitTypeDef NVIC_InitStruct;
    
    // 1. 使能时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
    
    // 2. 配置 PA0 为复用功能(TIM2_CH1)
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_100MHz;
    GPIO_InitStruct.GPIO_OType = GPIO_OType_PP;
    GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_UP;
    GPIO_Init(GPIOA, &GPIO_InitStruct);
    
    GPIO_PinAFConfig(GPIOA, GPIO_PinSource0, GPIO_AF_TIM2);
    
    // 3. 配置时基:1MHz 计数频率
    TIM_TimeBaseStruct.TIM_Prescaler = 83;  // 84MHz / (83+1) = 1MHz
    TIM_TimeBaseStruct.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseStruct.TIM_Period = 0xFFFFFFFF;  // 32位自动重装载
    TIM_TimeBaseStruct.TIM_ClockDivision = TIM_CKD_DIV1;
    TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStruct);
    
    // 4. 配置输入捕获
    TIM_ICInitStruct.TIM_Channel = TIM_Channel_1;
    TIM_ICInitStruct.TIM_ICPolarity = TIM_ICPolarity_Rising;  // 上升沿捕获
    TIM_ICInitStruct.TIM_ICSelection = TIM_ICSelection_DirectTI;
    TIM_ICInitStruct.TIM_ICPrescaler = TIM_ICPSC_DIV1;
    TIM_ICInitStruct.TIM_ICFilter = 0x8;  // 开启滤波
    TIM_ICInit(TIM2, &TIM_ICInitStruct);
    
    // 5. 配置中断
    NVIC_InitStruct.NVIC_IRQChannel = TIM2_IRQn;
    NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0;
    NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1;
    NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStruct);
    
    // 使能捕获中断
    TIM_ITConfig(TIM2, TIM_IT_CC1, ENABLE);
    
    // 6. 启动定时器
    TIM_Cmd(TIM2, ENABLE);
}

// 中断服务函数
void TIM2_IRQHandler(void)
{
    static uint32_t last_capture = 0;
    uint32_t current_capture;
    
    if (TIM_GetITStatus(TIM2, TIM_IT_CC1) != RESET)
    {
        TIM_ClearITPendingBit(TIM2, TIM_IT_CC1);
        
        current_capture = TIM_GetCapture1(TIM2);
        
        if (last_capture != 0)
        {
            // 计算周期(注意处理计数器溢出)
            if (current_capture > last_capture)
            {
                capture_value = current_capture - last_capture;
            }
            else
            {
                capture_value = (0xFFFFFFFF - last_capture) + current_capture + 1;
            }
            
            // 计算频率:1MHz / 周期 = 频率(Hz)
            frequency = 1000000 / capture_value;
            capture_done = 1;
        }
        
        last_capture = current_capture;
    }
}

这样我们就能精确测量外部信号的频率了。对于占空比测量,可以配置为交替捕获上升沿和下降沿,计算高电平和低电平的时间比例。

(第二部分完,约2300字)

五、编码器接口:硬件正交解码

编码器是电机控制系统中获取位置和速度信息的关键传感器。增量式正交编码器输出两路相位差 90 度的脉冲信号(通常称为 A 相和 B 相),通过这两路信号的相对相位关系,我们不仅可以计算旋转的角度,还能判断旋转方向。

5.1 为什么要用硬件编码器接口

很多初学者一开始会想用外部中断来读取编码器信号——A 相一个中断,B 相一个中断,在中断里判断相位关系,累加计数。这种方法在低速时还能工作,但当电机转速提高后,问题就来了:

  1. 中断频率太高:一个 1000 线的编码器,电机每秒转 100 圈,就会产生 400,000 次中断(4 倍频),CPU 根本处理不过来
  2. 丢失脉冲:如果中断处理不及时,就会丢失脉冲,导致位置累积误差
  3. 占用 CPU 资源:大量的中断会占用 CPU 时间,影响其他实时任务

STM32 定时器的编码器接口模式就是为了解决这个问题设计的。硬件自动完成正交解码和计数,完全不需要 CPU 干预,CPU 只需要在需要的时候读取计数器的值就可以了。

5.2 编码器接口的工作原理

定时器的编码器接口模式使用 CH1 和 CH2 两个通道作为输入。当检测到 A 相或 B 相的跳变沿时,硬件会自动根据相位关系对计数器进行加 1 或减 1 操作。

根据计数方式的不同,编码器接口分为三种模式:

模式 计数时机 分辨率
TI1 仅计数 仅在 TI1 边沿计数 1 倍频
TI2 仅计数 仅在 TI2 边沿计数 1 倍频
TI1 和 TI2 都计数 两路信号的边沿都计数 4 倍频

实际应用中,我们几乎总是使用 4 倍频模式,这样可以获得最高的位置分辨率。

5.3 编码器接口配置代码

#define ENCODER_PPR  1000  // 编码器每转脉冲数

void TIM3_Encoder_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct;
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStruct;
    TIM_ICInitTypeDef TIM_ICInitStruct;
    
    // 1. 使能时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
    
    // 2. 配置 PA6(TIM3_CH1) 和 PA7(TIM3_CH2)
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_100MHz;
    GPIO_InitStruct.GPIO_OType = GPIO_OType_PP;
    GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_UP;
    GPIO_Init(GPIOA, &GPIO_InitStruct);
    
    GPIO_PinAFConfig(GPIOA, GPIO_PinSource6, GPIO_AF_TIM3);
    GPIO_PinAFConfig(GPIOA, GPIO_PinSource7, GPIO_AF_TIM3);
    
    // 3. 配置时基
    TIM_TimeBaseStruct.TIM_Prescaler = 0;
    TIM_TimeBaseStruct.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseStruct.TIM_Period = 0xFFFF;  // 16位最大值
    TIM_TimeBaseStruct.TIM_ClockDivision = TIM_CKD_DIV1;
    TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStruct);
    
    // 4. 配置编码器接口模式(4倍频)
    TIM_EncoderInterfaceConfig(TIM3, 
                               TIM_EncoderMode_TI12,  // 两路都计数
                               TIM_ICPolarity_Rising,
                               TIM_ICPolarity_Rising);
    
    // 5. 配置输入滤波
    TIM_ICInitStruct.TIM_Channel = TIM_Channel_1;
    TIM_ICInitStruct.TIM_ICPolarity = TIM_ICPolarity_Rising;
    TIM_ICInitStruct.TIM_ICSelection = TIM_ICSelection_DirectTI;
    TIM_ICInitStruct.TIM_ICPrescaler = TIM_ICPSC_DIV1;
    TIM_ICInitStruct.TIM_ICFilter = 0x6;  // 滤波
    TIM_ICInit(TIM3, &TIM_ICInitStruct);
    
    TIM_ICInitStruct.TIM_Channel = TIM_Channel_2;
    TIM_ICInit(TIM3, &TIM_ICInitStruct);
    
    // 6. 启动定时器
    TIM_Cmd(TIM3, ENABLE);
    
    // 初始计数器设为中间值,防止正负溢出
    TIM_SetCounter(TIM3, 0x8000);
}

// 读取编码器位置
int32_t Encoder_GetPosition(void)
{
    static uint16_t last_counter = 0x8000;
    static int32_t position = 0;
    uint16_t current_counter;
    int16_t delta;
    
    current_counter = TIM_GetCounter(TIM3);
    delta = (int16_t)(current_counter - last_counter);
    last_counter = current_counter;
    
    position += delta;
    
    return position;
}

// 读取电机速度(单位:RPM)
float Encoder_GetSpeed(uint32_t sample_time_ms)
{
    static int32_t last_position = 0;
    int32_t current_position;
    int32_t delta_position;
    float speed_rpm;
    
    current_position = Encoder_GetPosition();
    delta_position = current_position - last_position;
    last_position = current_position;
    
    // 4倍频下,每转脉冲数 = PPR * 4
    // 转速 = (脉冲数 / (PPR * 4)) * (60000 / 采样时间)
    speed_rpm = (float)delta_position / (ENCODER_PPR * 4.0f) * 
                (60000.0f / sample_time_ms);
    
    return speed_rpm;
}

有了硬件编码器接口,即使电机转速达到几万转,也不会丢失一个脉冲!

六、BLDC 电机六步换向完整实现

现在我们把前面学到的知识整合起来,实现一个完整的 BLDC 无刷电机六步换向控制系统。

6.1 六步换向的基本原理

BLDC 电机的定子有三相绕组,转子是永磁体。为了让电机持续转动,我们需要按照特定的顺序给三相绕组通电,产生一个旋转的磁场,拖动永磁体转子转动。

六步换向就是将一个电周期分为 6 个状态,每个状态导通不同的桥臂。通过霍尔传感器检测转子位置,在正确的时刻切换到下一个状态。

6.2 完整的驱动代码

#include "stm32f4xx.h"

#define PWM_PERIOD    8399
#define PWM_MAX       8000
#define PWM_MIN       0

// 霍尔传感器状态对应的换向表
// 索引 = (HALL_C << 2) | (HALL_B << 1) | HALL_A
// 值:对应需要导通的桥臂(AH, BH, CH, AL, BL, CL)
const uint8_t commutation_table[8] = {
    0,      // 000: 无效状态
    0x09,   // 001: AH + CL
    0x12,   // 010: BH + AL
    0x11,   // 011: BH + CL
    0x24,   // 100: CH + AL
    0x21,   // 101: AH + BL
    0x22,   // 110: CH + BL
    0       // 111: 无效状态
};

volatile uint16_t pwm_duty = 0;

// 设置 PWM 输出
void Set_PWM_Output(uint8_t state)
{
    // 先关闭所有输出
    TIM_SetCompare1(TIM1, 0);
    TIM_SetCompare2(TIM1, 0);
    TIM_SetCompare3(TIM1, 0);
    
    // 根据换向表设置相应的 PWM
    switch(state)
    {
        case 0x09:  // AH + CL
            TIM_SetCompare1(TIM1, pwm_duty);  // AH PWM
            TIM_SetCompare3(TIM1, PWM_PERIOD); // CL 全开
            break;
            
        case 0x12:  // BH + AL
            TIM_SetCompare2(TIM1, pwm_duty);  // BH PWM
            TIM_SetCompare1(TIM1, PWM_PERIOD); // AL 全开
            break;
            
        case 0x11:  // BH + CL
            TIM_SetCompare2(TIM1, pwm_duty);  // BH PWM
            TIM_SetCompare3(TIM1, PWM_PERIOD); // CL 全开
            break;
            
        case 0x24:  // CH + AL
            TIM_SetCompare3(TIM1, pwm_duty);  // CH PWM
            TIM_SetCompare1(TIM1, PWM_PERIOD); // AL 全开
            break;
            
        case 0x21:  // AH + BL
            TIM_SetCompare1(TIM1, pwm_duty);  // AH PWM
            TIM_SetCompare2(TIM1, PWM_PERIOD); // BL 全开
            break;
            
        case 0x22:  // CH + BL
            TIM_SetCompare3(TIM1, pwm_duty);  // CH PWM
            TIM_SetCompare2(TIM1, PWM_PERIOD); // BL 全开
            break;
    }
}

// 读取霍尔传感器状态
uint8_t Read_Hall_Sensors(void)
{
    uint8_t hall_a = GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_0);
    uint8_t hall_b = GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_1);
    uint8_t hall_c = GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_2);
    
    return (hall_c << 2) | (hall_b << 1) | hall_a;
}

// 换向中断服务函数
void EXTI0_IRQHandler(void)
{
    if (EXTI_GetITStatus(EXTI_Line0) != RESET ||
        EXTI_GetITStatus(EXTI_Line1) != RESET ||
        EXTI_GetITStatus(EXTI_Line2) != RESET)
    {
        EXTI_ClearITPendingBit(EXTI_Line0);
        EXTI_ClearITPendingBit(EXTI_Line1);
        EXTI_ClearITPendingBit(EXTI_Line2);
        
        // 读取霍尔状态并执行换向
        uint8_t hall_state = Read_Hall_Sensors();
        uint8_t pwm_state = commutation_table[hall_state];
        
        if (pwm_state != 0)
        {
            Set_PWM_Output(pwm_state);
        }
    }
}

// 启动电机
void Motor_Start(void)
{
    uint8_t hall_state = Read_Hall_Sensors();
    uint8_t pwm_state = commutation_table[hall_state];
    
    if (pwm_state != 0)
    {
        Set_PWM_Output(pwm_state);
    }
}

// 停止电机
void Motor_Stop(void)
{
    TIM_SetCompare1(TIM1, 0);
    TIM_SetCompare2(TIM1, 0);
    TIM_SetCompare3(TIM1, 0);
}

// 设置转速(0-100%)
void Motor_SetSpeed(uint8_t speed_percent)
{
    if (speed_percent > 100) speed_percent = 100;
    
    pwm_duty = (uint16_t)((uint32_t)speed_percent * PWM_MAX / 100);
}

6.3 关键注意事项

  1. 死区时间的重要性:绝对不能让同一相的上下桥臂同时导通,否则会造成电源短路,烧毁 MOS管和电机。死区时间就是在切换桥臂时插入的一小段延迟,确保一个桥臂完全关断后,另一个桥臂才导通。

  2. 霍尔传感器的安装相位:霍尔传感器的安装位置必须正确,否则换向表就不对。如果电机抖动或不转,首先检查霍尔信号的相位顺序。

  3. 启动策略:BLDC 电机静止时,霍尔传感器能给出初始位置,但如果是无感驱动,就需要特殊的启动算法。

  4. 电流保护:实际产品中必须添加过流保护,可以通过 ADC 采样母线电流,超过阈值时触发刹车功能。

七、常见问题与调试技巧

7.1 PWM 没有输出?

这是最常见的问题,检查清单:

  • 是否调用了 TIM_Cmd() 启动定时器?
  • 高级定时器是否调用了 TIM_CtrlPWMOutputs()
  • GPIO 是否正确配置为复用功能?
  • 是否正确配置了复用功能映射?
  • 是否设置了 CCR 寄存器的值(默认为 0)?

7.2 电机抖动但不转?

  • 检查霍尔传感器接线是否正确
  • 检查换向表是否正确(可能需要尝试不同的组合)
  • 检查三相绕组的相序是否正确
  • 检查 PWM 频率是否合适(太低会产生 audible noise)

7.3 电机反转?

  • 交换任意两相的接线
  • 或者修改换向表的顺序
  • 编码器模式下可以交换 A/B 相

7.4 MOS 管发热严重?

  • 检查死区时间是否足够
  • 检查 MOS 管驱动电压是否足够
  • 检查 PWM 频率是否过高
  • 检查 MOS 管选型是否合适

八、进阶方向

掌握了基本的定时器功能和六步换向之后,你可以继续探索更高级的电机控制技术:

  1. FOC(磁场定向控制):通过坐标变换实现对电机定子电流的矢量控制,具有更高的效率和更低的噪音,是高性能电机驱动的标准方案。

  2. 无感 BLDC 驱动:不使用霍尔传感器,通过反电动势过零检测来判断转子位置,降低系统成本。

  3. 位置环和速度环控制:结合 PID 算法实现精确的位置和速度闭环控制。

  4. 高级 PWM 技术:如 SVPWM(空间矢量 PWM)、DPWM 等,进一步提高驱动效率。

总结

STM32 的定时器系统虽然看起来复杂,但只要理解了它的模块化设计思想,就能逐步掌握每一个功能。从最基本的定时中断,到 PWM 输出、输入捕获、编码器接口,再到高级定时器的互补输出和死区插入,每一个功能都是为了解决实际的工程问题。

本文我们从定时器的基本架构开始,深入讲解了 PWM 生成原理、输入捕获、编码器接口等核心功能,并通过完整的代码示例展示了如何使用高级定时器实现 BLDC 电机的六步换向控制。希望这些内容能帮助你真正掌握 STM32 定时器,从"会用"到"用好”。

嵌入式开发就是这样,看似复杂的外设,只要你深入理解了它的设计思路和工作原理,就能发挥出它的最大潜力。而定时器,正是你从单片机入门走向嵌入式进阶的第一道门槛,也是最重要的一块基石。

(全文完,约7200字)