前言
在嵌入式系统开发中,定时器是最常用也最容易被低估的外设之一。很多开发者对定时器的理解仅仅停留在"定时中断"的层面,却不知道一个高级定时器所能实现的功能远远超出想象——它可以生成高精度 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 |
理解了这些分类,你在设计硬件和编写代码时就不会选错定时器了。
二、高级定时器的硬件架构
在深入各个功能模块之前,让我们先从整体上理解高级定时器的硬件结构。很多人学定时器学不懂,就是因为一上来就钻进寄存器的细节里,没有建立起整体的认知。
2.1 时钟源:一切的起点
定时器的核心是一个计数器,而计数器需要时钟信号才能运转。高级定时器的时钟源可以来自多个地方:
-
内部时钟(CK_INT):最常用的时钟源,来自 APB 总线时钟。需要注意的是,当 APB 预分频器不为 1 时,定时器时钟是 APB 时钟的 2 倍。比如 APB1 时钟是 42MHz,那么 TIM2~TIM5 的时钟就是 84MHz。
-
外部时钟模式 1:通过外部引脚(TI1、TI2)输入的时钟信号,可以是编码器、外部振荡器等。
-
外部时钟模式 2:通过 ETR 引脚输入的外部时钟,支持更灵活的触发配置。
-
内部触发连接(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 波形是对称的。
为什么中央对齐模式对电机控制很重要?因为:
- 对称的 PWM 波形可以降低电机的电流纹波
- 减少电机运行时的噪音和振动
- 对于 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 相一个中断,在中断里判断相位关系,累加计数。这种方法在低速时还能工作,但当电机转速提高后,问题就来了:
- 中断频率太高:一个 1000 线的编码器,电机每秒转 100 圈,就会产生 400,000 次中断(4 倍频),CPU 根本处理不过来
- 丢失脉冲:如果中断处理不及时,就会丢失脉冲,导致位置累积误差
- 占用 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 关键注意事项
-
死区时间的重要性:绝对不能让同一相的上下桥臂同时导通,否则会造成电源短路,烧毁 MOS管和电机。死区时间就是在切换桥臂时插入的一小段延迟,确保一个桥臂完全关断后,另一个桥臂才导通。
-
霍尔传感器的安装相位:霍尔传感器的安装位置必须正确,否则换向表就不对。如果电机抖动或不转,首先检查霍尔信号的相位顺序。
-
启动策略:BLDC 电机静止时,霍尔传感器能给出初始位置,但如果是无感驱动,就需要特殊的启动算法。
-
电流保护:实际产品中必须添加过流保护,可以通过 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 管选型是否合适
八、进阶方向
掌握了基本的定时器功能和六步换向之后,你可以继续探索更高级的电机控制技术:
-
FOC(磁场定向控制):通过坐标变换实现对电机定子电流的矢量控制,具有更高的效率和更低的噪音,是高性能电机驱动的标准方案。
-
无感 BLDC 驱动:不使用霍尔传感器,通过反电动势过零检测来判断转子位置,降低系统成本。
-
位置环和速度环控制:结合 PID 算法实现精确的位置和速度闭环控制。
-
高级 PWM 技术:如 SVPWM(空间矢量 PWM)、DPWM 等,进一步提高驱动效率。
总结
STM32 的定时器系统虽然看起来复杂,但只要理解了它的模块化设计思想,就能逐步掌握每一个功能。从最基本的定时中断,到 PWM 输出、输入捕获、编码器接口,再到高级定时器的互补输出和死区插入,每一个功能都是为了解决实际的工程问题。
本文我们从定时器的基本架构开始,深入讲解了 PWM 生成原理、输入捕获、编码器接口等核心功能,并通过完整的代码示例展示了如何使用高级定时器实现 BLDC 电机的六步换向控制。希望这些内容能帮助你真正掌握 STM32 定时器,从"会用"到"用好”。
嵌入式开发就是这样,看似复杂的外设,只要你深入理解了它的设计思路和工作原理,就能发挥出它的最大潜力。而定时器,正是你从单片机入门走向嵌入式进阶的第一道门槛,也是最重要的一块基石。
(全文完,约7200字)