1. 定时器的分类
以下举例以STM32F103ZET6进行举例。
定时器分为3类,分别为基本定时器(2个),通用定时器(4个),高级定时器(2个)。
它们都是16位自动装载计数器(0 ~ 65535)
基本定时器只能配置为向上计数,通用和高级定时器可以配置为向上计数、向下计数、向上/向下计数。
基本定时器: 功能最少,只有计时的功能,Tim6 和Tim7
通用定时器: 除了可以进行计时之外,还可以输入捕获、输出比较、生成PWM方波,Tim2~5
高级定时器: 除了上述的所有功能外,还额外增加了死区时间插入功能、互补 PWM 输出、刹车(Break)功能、重复计数器(Repetition Counter) 等功能,Tim1 和Tim8
2. 定时器框图

STM32的众多定时器中我们使用最多的是高级定时器和通用定时器,高级定时器功能较复杂,日常开发中通用定时器使用更广泛,下面我们就以通用定时器为例进行讲解,其功能和特点包括:
- 位于低速的APB1总线上(APB1) (注意:高级定时器位于高速的APB2总线上)
- 16 位向上、向下、向上/向下(中心对齐)计数模式,自动装载计数器(TIMx_CNT)。
- 16 位可编程(可以实时修改)预分频器(TIMx_PSC),计数器时钟频率的分频系数 为 0~65535 之间的任意数值。
- 4 个独立通道(TIMx_CH1~4),这些通道可以用来作为:
|--------------------------------|--------------------------|
| 输入捕获 | 测量外部信号的周期、占空比(如编码器、红外信号) |
| 输出比较 | 控制 GPIO 的翻转时间点(如精确延时) |
| PWM 生成 | 电机驱动、LED 调光、音频输出 |
| 单脉冲模式(One Pulse Mode, OPM) | 用于一次性的脉冲输出(如超声波测距) |
- 可使用外部信号(TIMx_ETR)控制定时器和定时器级连(可以用 1 个定时器控制另外一个定时器)的同步电路。
- 在 STM32 中,每个定时器的核心功能模块是相互独立的 ,可以独立配置、独立运行,互不干扰。但在系统层面的资源(如时钟总线、中断向量)存在部分共享。
大部分情况,内部时钟源足够使用。不配置时钟源的情况下,默认选择的就是内部时钟源。
3. 时基单元
定时器框图中,使用黄色框圈住部分就是定时器的时基单元,包含3 个部分的内容:预分频寄存器(PSC)、计数器寄存器(CNT)、自动重装载寄存器(ARR)。
- 计数器寄存器:可以通过配置,控制其计数方式为向上计数、向下计数、向上/向下计数(中心对齐计数)。
- 预分频寄存器:是 16 位寄存器,取值范围为 0~65535 ,而实际的分频系数计算公式为 "分频系数 = PSC + 1"。
- 自动重装载寄存器:是 16 位寄存器,取值范围为 0~65535,对应的计数范围为1~65536(既ARR + 1)
eg:如果配置为向上计数,每来一个计数时钟,CNT便会加1,当 CNT 的值等于 ARR 时,此时还未产生更新事件,只是达到了预设的最大计数值,下一个计数时钟到来时 ,CNT 无法继续递增(因 ARR 是最大值),会溢出并重置为 0, 溢出的同时,硬件自动触发 "更新事件"(Update Event);若已使能更新中断(通过TIM_ITConfig),则会产生中断。
4. 使用通用定时器实现1s触发一次中断(Hal库 - Tim2举例)
使用定时器实现led灯定时亮灭
- 定时器配置(通过CubeMX软件,配置Tim2的时钟源为内部时钟,PSC为7199以及ARR为9999 LED灯的引脚这里选择PA1,根据电路的原理图配置 ------ 生成代码,自动生成如下内容)
cpp
TIM_HandleTypeDef htim2;
/* TIM2 init function */
void MX_TIM2_Init(void)
{
/* USER CODE BEGIN TIM2_Init 0 */
/* USER CODE END TIM2_Init 0 */
TIM_ClockConfigTypeDef sClockSourceConfig = {0};
TIM_MasterConfigTypeDef sMasterConfig = {0};
/* USER CODE BEGIN TIM2_Init 1 */
/* USER CODE END TIM2_Init 1 */
htim2.Instance = TIM2;
htim2.Init.Prescaler = 7199;
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
htim2.Init.Period = 9999;
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
if (HAL_TIM_Base_Init(&htim2) != HAL_OK)
{
Error_Handler();
}
sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
if (HAL_TIM_ConfigClockSource(&htim2, &sClockSourceConfig) != HAL_OK)
{
Error_Handler();
}
sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
if (HAL_TIMEx_MasterConfigSynchronization(&htim2, &sMasterConfig) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN TIM2_Init 2 */
/* USER CODE END TIM2_Init 2 */
}
- 中断服务函数(通过重写回调函数实现小灯引脚的电平翻转)
cpp
void TIM2_IRQHandler(void) {
HAL_TIM_IRQHandler(&htim2); // 调用HAL库中断处理
}
// 当前的中断回调函数触发的条件为:更新事件(CNT 溢出)
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM2)
{
// 每秒执行一次翻转LED灯
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
}
}
- 调用API启动定时器中断(在main函数中的MX_TIM2_Init();之后调用启动定时器中断的API)
cpp
if (HAL_TIM_Base_Start_IT(&htim2) != HAL_OK) // 启动TIM2并使能更新中断
{
Error_Handler();
}
5. 使用通用定时器生成PWM(Tim5_CH2为例)
使用PWM方波实现呼吸灯的效果
PWM:脉冲宽度调制, "Pulse Width Modulation"的缩写,简称脉宽调制
- 周期、频率、占空比(高电平占整个周期的比例)
- 一般用于具有惯性的场景(比如LED亮度调节,控制电机)
周期:连续两个上升沿或者连续两个下降沿之间的宽度,使用T表示
频率:1 / T(周期)
一般不改变频率和周期,通过改变占空比达到对外输出的有效电压的值
- 定时器配置(除了上面的配置之外,选择通道2 --- PWM Generation CH1,配置PSC为71,ARR为99,PWM为mode1,脉冲为50,高电平为有效电平 ------ 生成代码)
cpp
TIM_HandleTypeDef htim5;
/* TIM5 init function */
void MX_TIM5_Init(void)
{
/* USER CODE BEGIN TIM5_Init 0 */
/* USER CODE END TIM5_Init 0 */
TIM_ClockConfigTypeDef sClockSourceConfig = {0};
TIM_MasterConfigTypeDef sMasterConfig = {0};
TIM_OC_InitTypeDef sConfigOC = {0};
/* USER CODE BEGIN TIM5_Init 1 */
/* USER CODE END TIM5_Init 1 */
htim5.Instance = TIM5;
htim5.Init.Prescaler = 71;
htim5.Init.CounterMode = TIM_COUNTERMODE_UP;
htim5.Init.Period = 99;
htim5.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim5.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
if (HAL_TIM_Base_Init(&htim5) != HAL_OK)
{
Error_Handler();
}
sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
if (HAL_TIM_ConfigClockSource(&htim5, &sClockSourceConfig) != HAL_OK)
{
Error_Handler();
}
if (HAL_TIM_PWM_Init(&htim5) != HAL_OK)
{
Error_Handler();
}
sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
if (HAL_TIMEx_MasterConfigSynchronization(&htim5, &sMasterConfig) != HAL_OK)
{
Error_Handler();
}
sConfigOC.OCMode = TIM_OCMODE_PWM1;
sConfigOC.Pulse = 50;
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
if (HAL_TIM_PWM_ConfigChannel(&htim5, &sConfigOC, TIM_CHANNEL_2) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN TIM5_Init 2 */
/* USER CODE END TIM5_Init 2 */
HAL_TIM_MspPostInit(&htim5);
}
- 启动 TIM5 通道 2 的 PWM 输出(否则无波形输出)
cpp
// 注意:在main函数,MX_TIM5_Init();之后,while循环之前调用
HAL_TIM_PWM_Start(&htim5, TIM_CHANNEL_2);
- 实现呼吸灯的逻辑在while循环中(也可以单独创建一个函数,放在函数中实现)
cpp
// 通过改变脉冲值来改变占空比,实现呼吸灯的效果
// 原理:在PWM mode 1下,计数器CNT < 脉冲值pulse,则输出有效电平(这里配置为高电平,可根据需求修改极性)
uint16_t pulse = 0;
for (pulse = 0; pulse <= 99; pulse++)
{
__HAL_TIM_SET_COMPARE(&htim5, TIM_CHANNEL_2, pulse);
HAL_Delay(10);
for (pulse = 99; pulse > 0; pulse--)
{
__HAL_TIM_SET_COMPARE(&htim5, TIM_CHANNEL_2, pulse);
HAL_Delay(10);
}
这样就实现了呼吸灯的效果了,但是这样会导致阻塞主循环(如果程序中还包含其他的逻辑,那么就推荐使用中断的方式去修改脉冲值会更好)
6. 呼吸灯优化(使用中断回调方式)
思路:
方案一:当前使用Tim5来输出PWM,可以使用另一个定时器固定周期触发一次中断,每次中断修改定时器5的脉冲值Pluse,控制占空比递增/递减,形成呼吸效果(但是这样就需要使用到两个定时器,如果定时器资源较为紧张,那么则推荐使用方案二)
方案二:复用 PWM 定时器自身的更新中断,调整 ARR 同时兼顾 PWM 频率和中断周期
由于现实场景中方案二使用更多,所以通过方案二举例:
调整
ARR同时兼顾 PWM 频率和中断周期:中断周期 10ms,PWM 频率 1kHz(仍无频闪),配置如下:
PSC=71(计数时钟 = 72MHz/(71+1)=1MHz);ARR=999(PWM 周期 = 1000×1μs=1ms,中断周期 = 1ms,即每 1ms 触发一次更新中断)。
- 开启定时器5的中断(在CubeMX中进行勾选,设置ARR为999,其他内容保持不变)
cpp
// 开启了定时器5的中断之后,系统会自动调用中断处理函数,如下:
void TIM5_IRQHandler(void)
{
/* USER CODE BEGIN TIM5_IRQn 0 */
/* USER CODE END TIM5_IRQn 0 */
HAL_TIM_IRQHandler(&htim5);
/* USER CODE BEGIN TIM5_IRQn 1 */
/* USER CODE END TIM5_IRQn 1 */
}
- 通过定时器中断去改变定时器的脉冲值(逻辑为:每触发10次中断 --- 10ms,对脉冲值pluse进行加1或者减1的操作,并更新脉冲值)
cpp
// 首先声明全局变量
uint16_t pwm_pulse = 0;
int8_t pulse_step = 1;
// 同样在main函数的MX_TIM5_Init();之后,调用
HAL_TIM_PWM_Start_IT(&htim5, TIM_CHANNEL_2);
// 重写中断回调函数(可以写在任意位置 - 不要写在某一个函数里面)
// 这个中断回调函数触发的条件是:比较匹配事件(CNT == CCRx)比之前的中断回调函数更合适
void HAL_TIM_OC_DelayElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM5)
{
static uint8_t interrupt_count = 0;
if (++interrupt_count >= 10)
{
interrupt_count = 0;
pwm_pulse += pulse_step;
if (pwm_pulse >= 999)
pulse_step = -1;
if (pwm_pulse <= 0)
pulse_step = 1;
__HAL_TIM_SET_COMPARE(&htim5, TIM_CHANNEL_2, pwm_pulse);
}
}
}
补充内容:"系统定时器(滴答定时器)属于 CM3 内核,内嵌在 NVIC 中,是一个 24bit 的向下递减计数器。计数器寄存器 VAL 每计数一次的时间为 1/SYSCLK(一般配置 SYSCLK 为 72MHz)。当 VAL 递减到 0 时,产生一次中断,随后自动将重装载寄存器 LOAD 的值加载到 VAL 中,循环往复。"
一般使用系统滴定时器作为系统时基,简单的延时函数,不占用芯片外设资源,仅消耗内核资源。