玩转STM32定时器:从基础原理到实战应用全解析(多任务并发+呼吸灯+高级技巧)

文章目录

  • 玩转STM32定时器:从基础原理到实战应用全解析(多任务并发+呼吸灯+高级技巧)
    • 一、定时器核心原理与分类
      • 1. 定时器分类(以STM32F103为例)
      • 2. 定时原理与关键参数
    • 二、定时器中断实现多任务并发(实战案例)
      • 1. 功能需求
      • 2. 硬件准备
      • 3. STM32CubeMX详细配置
        • (1)基础配置
        • (2)定时器配置(TIM2+TIM3)
          • TIM2(2秒定时):
          • TIM3(5秒定时):
      • 4. 代码实现与解析
        • (1)定时器中断回调函数
        • (2)主函数初始化
      • 5. 实现效果
    • 三、非定时器方案对比(轮询法的局限)
      • 1. 轮询法代码实现
      • 2. 轮询法的致命缺陷
    • 四、PWM原理与呼吸灯实战
      • 1. PWM核心概念
      • 2. 呼吸灯功能需求
      • 3. STM32CubeMX配置(TIM3+TIM4)
        • (1)TIM3_CH1(PA6)配置
        • (2)TIM4_CH2(PB7)配置
      • 4. 呼吸灯代码实现
      • 5. 实现原理
    • 五、定时器高级应用与常见问题排查
      • 1. 高级应用场景
      • 2. 常见问题与解决方案
    • 六、总结与拓展
    • 七、效果展示
    • 八、心得体会

一、定时器核心原理与分类

STM32的定时器是嵌入式开发中实现精准定时、脉冲计数、PWM输出的核心外设,其本质是可编程的计数器,通过预设时钟频率和计数规则实现多样化功能。

1. 定时器分类(以STM32F103为例)

类型 代表定时器 核心特点 典型应用场景
基本定时器 TIM6、TIM7 仅支持向上计数,无外部引脚,常用于触发DAC或作为时基 定时中断、DAC触发
通用定时器 TIM2-TIM5 支持向上/向下计数,带4个捕获/比较通道,可输出PWM、测量脉冲宽度 多任务并发、PWM输出、脉冲计数
高级定时器 TIM1、TIM8 具备通用定时器全部功能,支持互补PWM输出(带死区控制),适合电机驱动 电机控制、三相逆变

本实战重点使用通用定时器(TIM2-TIM5),兼顾定时中断和PWM功能。

2. 定时原理与关键参数

定时器的核心是“时钟源→预分频器→计数器→自动重装寄存器”的信号链路,关键参数:

  • 时钟源(CK_PSC):定时器的输入时钟,F103通用定时器默认挂载APB1(最大36MHz),若APB1预分频为1,则定时器时钟=APB1时钟;否则=APB1时钟×2(本文配置为72MHz)。
  • 预分频器(PSC):将时钟源分频,分频后频率 = 时钟源 / (PSC+1)(PSC范围0~65535)。
  • 自动重装值(ARR):计数器计数到该值后复位(溢出),溢出周期 = (PSC+1)×(ARR+1) / 时钟源频率
  • 计数模式:向上计数(从0到ARR)、向下计数(从ARR到0)、中央对齐(上下往返)。

二、定时器中断实现多任务并发(实战案例)

传统轮询方式通过HAL_Delay()阻塞CPU,无法同时处理多个定时任务,而定时器中断可实现“无阻塞并发”,大幅提升系统效率。

1. 功能需求

  • 任务1:2秒定时翻转LED(PA0引脚外接LED)
  • 任务2:5秒定时通过USART1发送“hello windows!”
  • 两个任务独立运行,互不干扰

2. 硬件准备

  • 主控:STM32F103C8T6最小系统板
  • 外设:外接LED(串联220Ω电阻)、USB转TTL模块(用于串口通信)
  • 工具:ST-Link下载器、Keil MDK、STM32CubeMX

3. STM32CubeMX详细配置

(1)基础配置
  • 芯片选型:STM32F103C8T6
  • 时钟配置:HSE(外部晶振)→ PLL倍频至72MHz → APB1=36MHz,APB2=72MHz(定时器时钟=72MHz)
  • LED引脚:PA0配置为GPIO_Output(推挽输出,初始高电平)
  • 串口配置:USART1(PA9=TX,PA10=RX),波特率115200,无校验位
(2)定时器配置(TIM2+TIM3)
TIM2(2秒定时):
  • 模式:Internal Clock(内部时钟)
  • 参数计算:
    目标定时2秒 = 2000ms,时钟源72MHz
    设分频后频率=10kHz(计数精度0.1ms),则PSC=72000000/10000 -1=7199
    计数次数=2000ms / 0.1ms=20000 → ARR=20000-1=19999
  • 中断配置:NVIC Settings中勾选“TIM2 global interrupt”,抢占优先级1(高于主循环)
TIM3(5秒定时):
  • 模式与分频:同TIM2(PSC=7199,分频后10kHz)
  • 参数计算:5秒=5000ms → 计数次数=5000/0.1=50000 → ARR=50000-1=49999
  • 中断配置:勾选“TIM3 global interrupt”,抢占优先级1

4. 代码实现与解析

(1)定时器中断回调函数

重写HAL_TIM_PeriodElapsedCallback函数(定时器溢出时自动调用):

/* USER CODE BEGIN 4 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
  // 判断中断来源(哪个定时器)
  if (htim->Instance == TIM2) { 
    // 任务1:2秒翻转LED
    HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_0);
  } else if (htim->Instance == TIM3) {
    // 任务2:5秒发送串口数据
    uint8_t send_data[] = "hello windows!\r\n";
    // 发送数据(超时时间0xFFFF表示无限等待)
    HAL_UART_Transmit(&huart1, send_data, sizeof(send_data)-1, 0xFFFF);
  }
}
/* USER CODE END 4 */
(2)主函数初始化
int main(void) {
  // 初始化HAL库、系统时钟、外设
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_USART1_UART_Init();
  MX_TIM2_Init();
  MX_TIM3_Init();
  
  // 启动定时器中断(关键步骤)
  HAL_TIM_Base_Start_IT(&htim2);  // 启动TIM2中断
  HAL_TIM_Base_Start_IT(&htim3);  // 启动TIM3中断
  
  while (1) {
    // 主循环可执行其他任务(如按键检测),不被定时任务阻塞
    // 示例:每100ms闪烁一次板载LED(PC13)
    HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
    HAL_Delay(100);
  }
}

5. 实现效果

  • 外接LED(PA0)每2秒翻转一次(亮→灭→亮)
  • 串口每5秒输出一次“hello windows!”
  • 板载LED(PC13)每100ms快速闪烁,不受前两个任务影响

核心优势:三个任务并发执行,定时器中断优先级高于主循环,确保定时精准。

三、非定时器方案对比(轮询法的局限)

若不使用定时器中断,可通过HAL_GetTick()(系统滴答定时器的时间戳)实现伪并发,但存在明显缺陷。

1. 轮询法代码实现

int main(void) {
  uint32_t led2_prev = 0;  // 2秒任务的上一次执行时间
  uint32_t uart5_prev = 0; // 5秒任务的上一次执行时间
  uint32_t current_time;   // 当前时间戳

  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_USART1_UART_Init();

  while (1) {
    current_time = HAL_GetTick();  // 获取当前毫秒级时间戳

    // 任务1:2秒翻转LED
    if (current_time - led2_prev >= 2000) {
      HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_0);
      led2_prev = current_time;  // 更新时间戳
    }

    // 任务2:5秒发送串口数据
    if (current_time - uart5_prev >= 5000) {
      uint8_t send_data[] = "hello windows!\r\n";
      HAL_UART_Transmit(&huart1, send_data, sizeof(send_data)-1, 0xFFFF);
      uart5_prev = current_time;  // 更新时间戳
    }

    // 主循环其他任务(若耗时过长,会影响定时精度)
    // HAL_Delay(500);  // 若添加此句,2秒任务会变成约2.5秒
  }
}

2. 轮询法的致命缺陷

  • 精度依赖主循环速度:若主循环存在耗时操作(如HAL_Delay(500)),定时误差会累积(2秒任务可能变成2.5秒)。
  • CPU利用率低:本质是“循环等待”,CPU在空转中浪费资源。
  • 实时性差:高优先级任务(如紧急中断)无法抢占,适合简单场景。

四、PWM原理与呼吸灯实战

PWM(脉冲宽度调制)通过周期性改变“高电平占空比”模拟“模拟电压输出”,是控制LED亮度、电机转速的核心技术。

1. PWM核心概念

  • 频率:PWM波形的周期倒数(如1kHz表示每秒1000个周期),LED控制需≥50Hz(避免肉眼闪烁)。
  • 占空比:一个周期内高电平的时间占比(0%~100%),占空比越高,LED越亮。
  • STM32实现:通用定时器的捕获/比较通道可输出PWM,通过设置“比较值(CCR)”控制占空比:占空比 = (CCR+1)/(ARR+1)×100%

2. 呼吸灯功能需求

  • 两个LED(PA6外接LED、PB7板载LED)同时实现“呼吸效果”(亮度从暗→亮→暗循环)。
  • 呼吸周期2秒(从最暗到最亮1秒,再到最暗1秒)。

3. STM32CubeMX配置(TIM3+TIM4)

(1)TIM3_CH1(PA6)配置
  • 定时器模式:PWM Generation CH1(PWM生成通道1)
  • 时钟源:Internal Clock,预分频PSC=71(72MHz/(71+1)=1MHz)
  • 自动重装值ARR=99(周期=100/1MHz=0.1ms → 频率=10kHz)
  • PWM模式:Mode 1(计数器<CCR时输出高电平)
  • 初始比较值CCR=0(初始占空比0%,LED熄灭)
(2)TIM4_CH2(PB7)配置
  • TIM4默认通道2为PB6,需通过“重映射”将其映射到PB7:
    进入TIM4配置→“Channel2”→“Configuration”→“TIM4 Remap”选择“Partial Remap”
  • 其他参数同TIM3(PSC=71,ARR=99,PWM Mode 1)

4. 呼吸灯代码实现

通过动态修改CCR值(比较值)改变占空比,实现亮度渐变:

// 全局变量
uint8_t pwm_duty = 0;    // 占空比(0~99,对应0%~99%)
uint8_t direction = 1;   // 变化方向(1:递增,0:递减)

int main(void) {
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_TIM3_Init();
  MX_TIM4_Init();
  
  // 启动PWM输出
  HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);  // TIM3_CH1(PA6)
  HAL_TIM_PWM_Start(&htim4, TIM_CHANNEL_2);  // TIM4_CH2(PB7)
  
  while (1) {
    // 占空比从0→99→0循环变化
    if (direction) {
      pwm_duty++;
      if (pwm_duty >= 99) direction = 0;  // 达到最大占空比,反向
    } else {
      pwm_duty--;
      if (pwm_duty <= 0) direction = 1;   // 达到最小占空比,反向
    }
    
    // 更新两个通道的PWM占空比
    __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, pwm_duty);
    __HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_2, pwm_duty);
    
    HAL_Delay(10);  // 控制变化速度(10ms×100步=1秒完成一次渐变)
  }
}

5. 实现原理

  • 定时器频率10kHz(周期0.1ms),人眼无法察觉闪烁;
  • pwm_duty从0递增到99(占空比0%→99%),LED逐渐变亮;
  • 再从99递减到0,LED逐渐变暗,形成“呼吸”效果;
  • HAL_Delay(10)控制每步间隔,确保2秒完成一个周期。

五、定时器高级应用与常见问题排查

1. 高级应用场景

  • 输入捕获:用TIM2_CH1测量外部脉冲的周期(如超声波传感器的回响信号)。
  • 编码器接口:通过TIM3的编码器模式读取电机转速(A、B相脉冲)。
  • 互补PWM:用高级定时器TIM1输出带死区的互补PWM,驱动H桥电机。

2. 常见问题与解决方案

问题现象 可能原因 解决方案
定时时间不准 时钟树配置错误(APB1分频错误) 重新计算APB1时钟,确保定时器时钟频率正确
中断不触发 未启动定时器中断(未调用Start_IT) 添加HAL_TIM_Base_Start_IT(&htimx)
PWM无输出 未启动PWM(未调用Start_PWM) 添加HAL_TIM_PWM_Start(&htimx, TIM_CHANNEL_x)
呼吸灯闪烁(非渐变) PWM频率过低(<50Hz) 减小ARR值(如ARR=99,频率10kHz)

六、总结与拓展

本文系统讲解了STM32定时器的核心应用:

  1. 定时器中断:通过TIM2和TIM3实现多任务并发,解决了轮询法的阻塞问题,适合需要精准定时的场景;
  2. PWM输出:利用TIM3和TIM4的PWM功能实现呼吸灯,通过动态调整占空比模拟亮度变化,掌握了__HAL_TIM_SET_COMPARE函数的用法。

拓展方向

  • 尝试用定时器输入捕获测量按键按下时间;
  • 结合FreeRTOS,用定时器作为任务调度器的时基;
  • 实现PWM占空比的串口调节(通过上位机发送指令修改亮度)。

七、效果展示

串口

串口plus

实物

小灯plus

八、心得体会

这次 STM32 定时器实战让我深刻体会到 “原理先行” 的重要性。起初配置 2 秒定时时,直接照搬网上的 PSC 和 ARR 参数,结果定时误差高达几百毫秒,直到回头梳理 “时钟源→预分频器→计数器” 的链路,才发现是误将 APB1 时钟当成了定时器时钟,忽略了 “APB1 分频非 1 时定时器时钟翻倍” 的规则。重新计算后(72MHz 时钟源、PSC=7199、ARR=19999),定时精度瞬间达标。这让我明白,嵌入式开发中任何参数配置都不是 “凭经验”,而是基于底层原理的推导,只有把定时器的计数逻辑、时钟链路摸透,才能在多任务并发等场景中实现精准控制,而非盲目试错。

定时器中断与轮询法的对比实践,让我学会了 “根据场景选方案”。轮询法虽然代码简单,但主循环中的耗时操作会直接导致定时不准,比如添加 500ms 延时后,2 秒定时变成了 2.5 秒;而定时器中断完全摆脱了主循环的束缚,即使主循环执行其他任务,2 秒 LED 翻转和 5 秒串口发送依然精准无误,CPU 利用率也大幅提升。后续的 PWM 呼吸灯实战更让我感受到定时器的灵活性 —— 通过动态修改比较值就能实现亮度渐变,无需复杂的模拟电路。这让我意识到,嵌入式开发的核心不是 “实现功能”,而是 “优化实现方式”:简单场景用轮询,精准并发用中断,模拟输出用 PWM,选对方案才能让系统既稳定又高效。

定时器开发的调试过程,让我收获了 “从现象倒推原因” 的排查思维。比如最初配置 PWM 时,PA6 引脚始终无输出,反复检查代码后发现是漏调用了HAL_TIM_PWM_Start函数;而呼吸灯出现闪烁而非渐变,是因为 PWM 频率设置过低(仅 50Hz),调整 ARR 值将频率提升到 1kHz 后问题解决。还有中断不触发的问题,排查后发现是 NVIC 优先级配置冲突,调整抢占优先级后恢复正常。这些经历让我明白,嵌入式开发的细节里藏着很多 “隐形坑”,一个函数的遗漏、一个参数的错误、一次优先级的冲突,都会导致功能失效。调试时不能急于求成,而要按照 “硬件连接→配置参数→代码逻辑→中断优先级” 的顺序逐步排查,凭借耐心和逻辑缩小问题范围,才能最终找到根源。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐