定时器详解以及呼吸灯实现 — STM32(HAL库)

1. 定时器的分类

以下举例以STM32F103ZET6进行举例。

定时器分为3类,分别为基本定时器(2个),通用定时器(4个),高级定时器(2个)。

它们都是16位自动装载计数器(0 ~ 65535)

基本定时器只能配置为向上计数,通用和高级定时器可以配置为向上计数、向下计数、向上/向下计数。

基本定时器: 功能最少,只有计时的功能,Tim6Tim7

通用定时器: 除了可以进行计时之外,还可以输入捕获、输出比较、生成PWM方波,Tim2~5

高级定时器: 除了上述的所有功能外,还额外增加了死区时间插入功能、互补 PWM 输出、刹车(Break)功能、重复计数器(Repetition Counter) 等功能,Tim1Tim8

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 中,循环往复。"

一般使用系统滴定时器作为系统时基,简单的延时函数,不占用芯片外设资源,仅消耗内核资源。

相关推荐
逆小舟2 小时前
【STM32】定时器、PWM
stm32·单片机·嵌入式硬件
XH1.2 小时前
学习RT-thread(RT-thread定时器)
stm32·单片机·学习
QT 小鲜肉2 小时前
【个人成长笔记】在 Linux 系统下撰写老化测试脚本以实现自动压测效果(亲测有效)
linux·开发语言·笔记·单片机·压力测试
申克Lab2 小时前
STM32 串口概念 UART协议
stm32·单片机·嵌入式硬件
小莞尔3 小时前
【51单片机】【protues仿真】基于51单片机自动浇花系统
单片机·嵌入式硬件
沐欣工作室_lvyiyi4 小时前
基于51单片机的宠物喂食器的设计与实现(论文+源码)
单片机·嵌入式硬件·毕业设计·51单片机·宠物
hazy1k7 小时前
51单片机基础-最小系统设计
stm32·单片机·嵌入式硬件·mcu·51单片机·proteus
奋斗的牛马8 小时前
FPGA—ZYNQ学习spi(六)
单片机·嵌入式硬件·学习·fpga开发·信息与通信
清风6666668 小时前
基于单片机的智能高温消毒与烘干系统设计
数据库·单片机·嵌入式硬件·毕业设计·课程设计·期末大作业