STM32 定时器

版权声明:署名-非商业性使用-相同方式共享

@@ Tags: STM32 定时器
@@ Date: 2026-02-08 2202
@@ From: 嵌入式系统设计
@@ Note: ~

概念

定时器的核心就是一个计数器模块,可以进行加一或减一计数。每出现一个计数信号,计数器的值就自动加一或减一。当计数值从0递增到最大值或者从最大值递减到0时,定时器可以向处理器发送中断请求。

计数信号的来源可以选择非周期的外部输入信号或者周期性的内部时钟信号,这两种不同的计数信号决定了定时器的两种基本工作模式:计数模式定时模式

在衡量一个定时器的基本性能时,常常使用位宽进行描述,比如8位定时器或者16位定时器。这里的位宽代表了定时器内部的计数器的位数,它决定了定时器的最大计数范围或者最大定时时间。

  • 计数模式
    对引脚输入的外部脉冲信号进行计数。
  • 定时模式
    对处理器内部的周期性时钟信号进行计数。
  • 定时时钟
    在定时模式下,输入定时器的周期性时钟信号称为定时时钟。
  • 计数时间
    在定时模式下,定时器内部的计数单元记一次数所花费的时间称为计数时间(或者说两次计数的时间间隔),该值为定时时钟频率的倒数

根据定时时钟和计数时间的定义,我们可以得到定时器的定时时间计算公式:

  • 定时时间 = 计数值 × 计数时间
  • 定时时间 = 计数值 / 定时时钟频率

例如,当定时器的定时时钟为1MHz时,计数时间为1s。此时,16位计数器的最大定时时间为65536μs,即65.536ms

STM32-TIMER.png

STM32 定时器介绍

STM32的定时器的功能强大,种类也比较多。按照定时器在微控制器内部的位置可以划分为内核定时器外设定时器两大类。

其中,外设定时器按照功能又可以划分为专用定时器和常规定时器。具体的分类如图8-1所示。

在众多的定时器中,常规定时器的用途最为广泛。按照功能的不同,可以把常规定时
器分为基本定时器、通用定时器和高级定时器三类:

  • 基本定时器
    儿乎没有任何对外的输入/输出通道,常用作时间基准(时基),实现基本的定时功能。
  • 通用定时器
    具备多路独立的捕获/比较通道,可以完成定时/计数、输入捕获、输出比较等功能,还可以连接其他的传感器接口,如编码器和霍尔传感器。
  • 高级定时器
    高级定时器的功能最为强大,除了具备通用定时器的功能外,还增加了重复计数器带死区控制的互补信号输出等功能,可用于电机控制等领域。

以STM32F411芯片为例,片内集成了8个定时器:

STM32-TIMER-2.png

注意:STM32微控制器的各个定时器都是完全独立的,所拥有的硬件资源彼此独立,
互不干扰

定时器的定时时钟与定时器所挂接的外设总线时钟APB相关,外设总线的时钟配置如图8-2所示。

无论哪一种定时器,最基本的功能都是定时和计数,在这两个功能的基础上又衍生出其他的功能。在实际的工程应用中,最常用的定时器功能有以下三种:

  • 定时/计数功能:用于产生时间基准以及测量外部脉冲的个数。
  • 输出比较功能:包括PWM输出、电平翻转、单脉冲输出以及强制输出等功能。
  • 输入捕获功能:用于测量输人信号的脉冲宽度。

时钟源

定时器的两种工作模式的区别主要在于时钟源的选择。可以通过设置相关的寄存器,选择对应的时钟源后,该时钟源将作为时基单元的预分频时钟CK_PSC送入时基单元。

时钟源一共有四种选择:

1.内部时钟(CK_INT)

内部时钟CK_INT来自外设总线APB1APB2提供的定时时钟TIMx_CLK,如图8-2所示的 APBx Timer Clocks。

使用内部时钟作为时钟源时,定时器工作于定时模式,并衍生出输出比较和输入捕获等功能。

2.外部时钟模式1:外部输入引脚TIx(x表示引脚编号1~4,下同)

TIx (Timer Input), 时钟信号来自外部输入引脚TIx,定时器可以在时钟信号的每个上升沿或下降沿计数。

注意:

  • 当使用外部时钟模式1时,时钟信号实际上是由定时器的捕获/比较通道所对应的引脚TIMx_CHn(n表示通道编号1~4,下同)输人。
  • TIMx_CHn 是物理引脚,而 TIx 是内部路线。

3.外部时钟模式2:外部触发输入ETR

ETR (External Trigger Input), 时钟信号来自外部触发输入引脚TIMx_ETR。定时器可以在时钟信号的每个上升沿或下降沿计数

4.内部触发输入 ITRx

ITRx (Internal Trigger Input), 时钟信号来自芯片内部其他定时器的触发输出,可以实现定时器的同步或级联。例如,使用一个定时器作为另一个定时器的预分频器

时基单元

时基单元是定时器的核心控制单元,负责时钟源的分频、计数和溢出重载等基本功能。它主要由三个模块组成:预分频模块、计数模块和自动重载模块。

时基单元的功能框 如图8-4所示

STM32-TIMER-3.png

预分频时钟CK_PSC是经过时钟源选择后输出的时钟信号。如果使用内部时钟CK_INT作为时钟源,CK_PSC的频率等于定时时钟TIMx_CLK的频率。如果使用外部时钟或者内部触发信号作为时钟源,CK_PSC的频率由外部引脚输入信号或者内部触发信号的频率决定。

CK_CNT是经过预分频模块后,送入计数模块的时钟,这里称为计数时钟

预分频模块

预分频模块由预分频计数器预分频寄存器TIMx_PSC组成:预分频计数器通过计数的方式对预分频时钟CK_PSC进行分频,而预分频寄存器用于存放预分频系数PSC

预分频主要用于将 CK_PSC 时钟频率进行预处理,使得可以在 16 位定时器上处理更长时间的定时任务(比如 100MHz 频率上处理 65.536ms 以上时长)。另外一方面可以将频率转换为更容易处理的时长单位,比如间隔时长为 1us

从图8-5可以看到:当预分频寄存器的内容为3时,预分频计数器从0开始计数,且计数值0会保持一个完整的计数脉冲。因此,实际的计数脉冲个数为4,即最终的预分频系数为:PSC+1。例如,要对内部时钟进行72分频,则预分频系数PSC应设置为71。

预分频时钟CK_PSC经过预分频模块后,得到计数时钟CK_CNT,它的计算公式如下:

$$
CK\_CNT=CK\_PSC / (PSC+1)
$$

计数模块

计数模块由核心计数器和计数器寄存器TIMx_CNT组成:核心计数器用来对预分频模块输出的计数时钟CK_CNT进行二次计数。计数时钟CK_CNT每输人一个脉冲,核心计数器的计数值就自动加一或减一(根据用户设置的不同计数模式来决定是加一还是减一)。计数器寄存器则用来存放核心计数器运行时的计数值,便于用户读取。

自动重载模块

自动重载模块由自动重载寄存器TIMx_ARR构成,用来设置计数器的计数终值或计数初值,决定计数脉冲的多少(计数模式)或定时周期(定时模式)的长短我们将·寄存器的内容记为自动重载值ARR。

计数模式

定时器的计数模块支特三种计数模式:递增计数递减计数中心对齐计数并产生溢出事件,作为定时器的更新中断(定时中断)

三种模式的主要区别在于:递增时计数器从0到ARR后产生上溢事件,递减时计数器冲ARR到0后产生下溢事件,中心对齐模式先从0到ARR-1后产生上溢事件,再递减到 1 产生下溢事件

定时时间计算公式

当定时器工作于定时模式时,预分频时钟CK_PSC等于定时时钟TIMx_CLK。定时时间由TIMx_CLK的频率、预分频系数PSC和自动重载值ARR三者决定。

假设PSC=1,ARR=36,采用递增计数模式,计数器寄存器的初值为0。定时器的时序如图8-7所示。

STM32-TIMER-4.png

至此我们可以推导出,定时时间的计算公式为:

$$
T=(PSC+1) * (ARR+1) / CK\_PSC
$$

  • 由于计数器寄存器从0开始计数,且计数值0会保持1个完整的计数脉冲。因此,实际的计数脉冲个数为:ARR+1
  • 公式的 (PSC+1) * (ARR+1) 部分可以简单看做: 触发溢出事件需要多少个 CK_PSC 脉冲数。
  • 需要脉冲数 / 频率 就可以求出触发溢出事件总共需要多少秒。

同样的,可以得到定时器溢出频率的计算公式:

$$
f=CK\_PSC / (PSC+1) * (ARR+1)
$$

外部脉冲计数

外部信号的输入有两个途径:

  • 一个是通过外部触发输入引脚 ETR,经过极性选择、分频和滤波以后,再输入到时基单元进行计数(工程常用)。
  • 一个是通过外部输入引脚 TIx,也就是定时器的捕获/比较通道TIMx_CHn,经过滤波和边沿检测后,再输入到时基单元进行计数。

外部脉冲信号的处理流程如图8-8所示。

STM32-TIMER-5.png

极性选择用于选择外部脉冲信号的触发极性,可以进行反相和不反相:触发极性不反相表示在脉冲信号的上升沿出现时计数;触发极性反相表示在脉冲信号的下降沿出现时计数。

HAL 库外设模块的编程

对于定时器(TIMER)、串口(UART)和模数转换器(ADC)等功能较为复杂的外设,HAL库设计了一个名为外设句柄的数据类型PPP_HandleTypeDef(PPP表示外设名称)。

#define TIM2 ((TIM_TypeDef *) TIM2_BASE)

typedef struct
{
  uint32_t Prescaler;                         // 预分频系数 PSC, 即 TIMx_PSC 的内容
  uint32_t CounterMode;                       // 计数模式
  uint32_t Period;                            // 自动重装载值 ARR, 即 TIMx_ARR 的内容
  uint32_t ClockDivision;                     // 时钟分频
  uint32_t RepetitionCounter;                 // 重复计数器(仅高级定时器有效)
  uint32_t AutoReloadPreload;                 // 设置自动重载寄存器 TIMx_ARR 的预装载值
} TIM_Base_InitTypeDef;

typedef struct {
    TIM_TypeDef                *Instance;     // 定时器外设实例(寄存器基地址)
    TIM_Base_InitTypeDef       Init;          // 定时器时基初始化参数配置
    HAL_TIM_ActiveChannel      Channel;       // 捕获/比较通道(用于 PWM、输入捕获等)
    DMA_HandleTypeDef          *hdma[7];      // DMA 句柄数组
    HAL_LockTypeDef            Lock;          // 保护锁(防止多任务冲突)
    __IO HAL_TIM_StateTypeDef  State;         // 定时器状态(如 ready, busy, error)
} TIM_HandleTypeDef;

TIM_HandleTypeDef htim2;                      // 定义一个名为 htim2 的句柄
htim2.Instance         = TIM2;                // 指向 TIM2 的硬件地址
htim2.Init.Prescaler   = 71;                  // 预分频器
htim2.Init.Period      = 999;                 // 自动重装载值 (ARR)
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
                                              // ... 其他初始化
HAL_TIM_Base_Init(&htim2);                    // 调用库函数,把 Init 里的参数写进 Instance 寄存器

定时器有两种启动方式,轮询或中断。对于定时器来说这两种方式的区别在于设置的标志位不同,轮询时由用户自行检查。而中断则设置中断标志,从而触发中断处理程序。

注意:

定时器是一个片内外设,因此定时器在计数或输出PWM时,是完全独立于CPU工作的,完全不影响CPU的指令执行

轮询方式

// 1. 启动定时器
HAL_TIM_Base_Start(&htim2);

while (1)
{
    // 2. 轮询检查:UIF 标志位是否被置 1(说明计数溢出了)
    if (__HAL_TIM_GET_FLAG(&htim2, TIM_FLAG_UPDATE) != RESET)
    {
        // 3. 清除标志位(如果不清除,下次进来立刻又是 SET)
        __HAL_TIM_CLEAR_FLAG(&htim2, TIM_FLAG_UPDATE);

        // 4. 执行你的逻辑代码
        HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
    }
}

中断方式

中断方式使用 HAL_TIM_Base_Start_IT() 启动定时器, 当溢出事件发生时,会设置中断标志,从而触发中断运行中断处理函数,最后被 HAL_TIM_PeriodElapsedCallback 处理。

注意:

在调用接口函数 HAL_TIM_Base_Init() 初始化时基单元时,会执行一个软件更新语句:TIMx->EGR=TIM_EGR_UG,这个语句将触发更新事件,并置位更新中断标志。

如果采用中断方式启动定时器,一旦使能了更新中断,将立即进入更新中断服务程序。因此用户在使能定时器更新中断之前,必须先执行清除更新中断标志的操作。

定时与计数示例

定时闪烁指示灯

CubeMX 配置

  • STM32F411RET6 (100MHz)

    • Times -> TIM10:
      • Activated -> checked
      • Prescaler -> 9999
      • CounterPeriod -> 9999;
      • CounterMode -> UP
  • STM32F103C8T6 (72MHz)

    • Times -> TIM3:
      • Activated -> checked
      • Prescaler -> 7199
      • CounterPeriod -> 9999;
      • CounterMode -> UP
  • NVIC 启用 TIM10TIM3 中断

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_TIM3_Init();

  /* USER CODE BEGIN 2 */
  __HAL_TIM_CLEAR_IT(&htim3, TIM_IT_UPDATE);  // 清除更新中断标志, 避免定时器一启动就进入中断
  HAL_TIM_Base_Start_IT(&htim3);              // 中断方式启动定时器
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */

增加定时器函数

/* USER CODE BEGIN 4 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
    if (htim->Instance == TIM3) {
        HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
    }
}
/* USER CODE END 4 */

外部脉冲计数示例

监听键盘事件,每触按下一次键盘,就通过引脚输出一个周期为2ms的脉冲,送入定时器的外部触发输入引脚进行计数,并将计算结果通过串口发送到PC上显示。

  • STM32F411RET6 (100MHz)
    • Times -> TIM2: 外部输入计数设置
      • TriggerSource -> Disbale
      • ClockSource -> ETR2
      • Prescaler -> 0
      • CounterPeriod -> 65535;
      • CounterMode -> UP
      • ClockFilter -> 0
    • Connectivity -> USART1: 串口通讯设置
      • Mode -> Asynchronous
      • BaudRate -> 115200
      • Parity -> None
  • STM32F103C8T6 (72MHz)
    • Times -> TIM2: 外部输入计数设置
      • TriggerSource -> Disbale
      • ClockSource -> ETR2
      • Prescaler -> 0
      • CounterPeriod -> 65535;
      • CounterMode -> UP
      • ClockFilter -> 0
    • Connectivity -> USART1: 串口通讯设置
      • Mode -> Asynchronous
      • BaudRate -> 115200
      • Parity -> None

实现代码摘要如下, 其中(以STM32F103C8T6为基准, 其他板子引脚有所不同):

  • PC13 作为LED
  • PB12 作为按键
  • PC15 作为脉冲输出
  • PA0 作为TIM2_ETR
/* USER CODE BEGIN Includes */
#include "stdio.h"
/* USER CODE END Includes */

/* USER CODE BEGIN 0 */
typedef enum {
  KEY_UP = 0,
  KEY_DEBOUNCE,
  KEY_WAIT_RELEASE,
} KEY_STATE;

KEY_STATE        KeyState = KEY_UP;
volatile uint8_t KeyFlag  = 0;
uint8_t          Result   = 0;
/* USER CODE END 0 */

int main(void)
{
  // 省略...

  /* USER CODE BEGIN 2 */
  HAL_TIM_Base_Start_IT(&htim1); // 启动定时器1,用于按键检测
  HAL_TIM_Base_Start(&htim2);    // 启动定时器2,用于外部脉冲计数

  printf(" Timer count function test: \n");
  /* USER CODE END 2 */

  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */
    /* USER CODE BEGIN 3 */
      if (KeyFlag)
      {
        KeyFlag = 0;
        printf(" Button B12 has been target.\n");
        HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);  // 切换 LED 以便观察状态

        // 2ms 脉冲输出
        HAL_GPIO_WritePin(GPIOC, GPIO_PIN_15, GPIO_PIN_SET);
        HAL_Delay(1);
        HAL_GPIO_WritePin(GPIOC, GPIO_PIN_15, GPIO_PIN_RESET);
        HAL_Delay(1);

        Result = __HAL_TIM_GET_COUNTER(&htim2);  // 读取外部脉冲计数值
        printf(" count %d.\n", Result);          // 显示结果
      }
  }
  /* USER CODE END 3 */
}

/* USER CODE BEGIN 4 */
// printf 输出重定向到串口
int fputc(int ch, FILE *f)
{
  HAL_UART_Transmit(&huart1, (uint8_t*)&ch, 1, HAL_MAX_DELAY);
  return ch;
}

// 按键检测
void KeyScan()
{
  switch(KeyState) {
    case KEY_UP:
      if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_12) == GPIO_PIN_RESET) {
         KeyState = KEY_DEBOUNCE;
      }
      break;

    case KEY_DEBOUNCE:
      if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_12) == GPIO_PIN_RESET) {
         KeyState = KEY_WAIT_RELEASE;
         KeyFlag = 1;
      }
      else {
        KeyState = KEY_UP;
      }
      break;

    case KEY_WAIT_RELEASE:
      if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_12) == GPIO_PIN_SET) {
         KeyState = KEY_UP;
      }
      break;
  }
}

// 定时检测按钮状态
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  if(htim->Instance == htim1.Instance)
    KeyScan();
}
/* USER CODE END 4 */

注意:

  • 串口部分的代码,因为使用了 printf 因此要注意勾选 Use MicroLIB 设置, 否则直接运行可能不生效。
  • 外部脉冲计数方面, 相对来说比较简单, 在 CubeMX 中配置好定时器后, 仅需要启动 然后 读取计数值即可。

PWM 输出

脉冲宽度调制(pulse width modulation,PWM)是一种对模拟信号电平进行数字编码的方法。PWM技术广泛应用于机械、通信、功率控制等领域,如电机的转速控制、灯光的亮度调节、DC-DC转换器以及信号调制等场合。

PWM信号有两个重要的参数:周期和占空比。

  • 周期(Period)
    一个完整PWM波形所持续的时间。
  • 占空比(Duty)
    • 高电平持续时间(T)与周期(Period)的比值。
    • 占空比的计算公式如下:
      $Duty=(T/Period) * 100%$

图8-18 给出了50%、20%和80%三种不同占空比的PWM信号。

如图8-18所示,电压的峰值为 $3.3V$, $T_{on}$ 表示高电平持续时间,$T_{off}$ 表示信号关断时间,虚线表示PWM信号所对应的平均电压。

STM32-TIMER-PWM-1.png

根据平均电压的计算公式:平均电压=峰值×占空比,我们可以计算出不同占空比的PWM信号所对应的平均电压:

  • 20% 占空比的平均电压为 0.66V;
  • 50% 占空比的平均电压为 1.65V;
  • 80% 占空比的平均电压为 2.64V

因此,PWM信号能够进行电压调节的基本原理就是不同占空比的PWM信号等效于不同的平均电压。

捕获/比较通道

捕获/比较通道负责输入捕获功能输出比较功能。每个定时器最多可以拥有4个捕获/比较通道,每个通道都有对应的寄存器进行控制,也有对应的GPIO引脚作为通道的输入和输出接口。

捕获/比较通道的功能框架如图8-19所示。

STM32-TIMER-PWM-2.png

1.输入捕获单元

用于捕获外部脉冲信号,捕获方式可以设置为上升沿捕获、下降沿捕获和双边沿捕获。发生捕获事件时,将计数器的当前计数值锁存到捕获/比较寄存器中,以供用户读取,同时可以产生捕获中断。输入捕获主要用于信号测量,可以测量信号的周期、频率和占空比等参数,测量所使用的参考时间就是捕获发生时锁存到寄存器中的值(详见:输入捕获功能)。

2.捕获/比较寄存器

捕获/比较寄存器 TIMx_CCR 是捕获/比较通道中最重要的寄存器。

  • 在输入捕获模式下用于存放发生捕获事件时的计数值;
  • 在输出比较模式下用于存放预设的比较值
  • 该寄存器具备预装载功能,由影子寄存器和预装载寄存器组成,预装载功能可由软件选择开启或关闭。

3.输出比较单元

输出比较单元用于信号输出。定时器通过将预设的比较值与计数器的计数值做匹配比较,从而实现各类输出,如PWM输出、电平翻转、单脉冲输出和强制输出。预设的比较值存放在捕获/比较寄存器TIMx_CCR中。

使用捕获/比较通道时,需要注意以下几点:

  • 输入捕获功能和输出比较功能都是由定时功能衍生而来。因此,定时器工作于定时模式,时钟源为内部时钟CK_INT,时基单元的预分频时钟CK_PSC等于定时器的定时时钟TIMx_CLK
  • 每个定时器具备1~4个独立的捕获/比较通道,每个通道具有独立的输入捕获单元、捕获/比较寄存器和输出比较单元,但共享同一个时基单元
  • 每个捕获/比较通道都可以独立设置为捕获通道(用于输入捕获)或者比较通道(用于输出比较),但是两种功能只能选择其中之一。
  • 每个捕获/比较通道都有对应的通道引脚作为通道的输入/输出接口,如TIMx_CHn(n表示通道号1~4,下同)。这些通道引脚与GPIO引脚复用,用户可以在CubeMX软件的引脚分配图上选择GPIO引脚的功能为通道引脚。

PWM 实现原理

PWM信号输出,需要用到三个寄存器:

  • TIMx_ARR 自动重载寄存器
  • TIMx_CCRn 捕获/比较寄存器(n表示通道编号1~4,下同)
  • TIMx_CNT 计数器寄存器,并通过通道引脚TIMx_CHn输出PWM信号。

整个PWM信号的输出过程如图8-20所示。

启动定时器后,计数器从0开始计数。计数过程中,不断将计数值CNT与捕获/比较值CCR以及自动重载值ARR相比较。

  • CNT小于CCR时,输出高电平;
  • CNT等于CCR时,输出电平翻转,变为低电平。
  • CNT等于ARR时,输出电平再次翻转,变为高电平。并触发重载, 开始下一个周期的输出。

根据PWM信号的输出过程,我们可以知道:自动重载寄存器TIMx_ARR用于控制PWM信号的周期,捕获/比较寄存器TIMx_CCRn用于控制PWM信号的占空比。

综上所述,我们可以得到PWM信号周期及占空比的计算公式:

  • $T=(PSC+1)*(ARR+1)/TIMx\_CLK$
  • $Duty=CCR/(ARR+1)* 100%$

STM32-TIMER-PWM-3.png

假设定时器的定时时钟TIM_CLK100MHz,要求输出周期为1ms,占空比为47.5%的PWM信号,输出波形如图8-21所示。

要产生这样一个PWM信号,根据公式,我们可以设置:

  • PSC 预分频系数为99,
  • ARR 自动重载值为999,从而得到1ms的PWM周期。
  • CCR 捕获/比较值为475。

定时器的每个通道都具有独立的输入捕获单元、捕获/比较寄存器和输出比较单元,可以分别输出PWM信号。对于同一个定时器而言,由于它的多个通道共享同一个自动重载寄存器,而自动重载寄存器的内容决定了PWM信号的周期。因此,对于同一个定时器的多个通道而言,可以同时输出占空比不同,但周期相同的PWM信号

输出 PWM 信号示例

输出周期为2s,占空比为50%的PWM信号控制开发板上的指示灯LED。

CubeMX 配置如下:

  • STM32F103C8T6 (72MHz)
    • Times -> TIM2:
      • Mode:
        • ClockSource -> Internal Clock
        • Channel1 -> PWM Generation CH1(PA0引脚)
      • Parameter Settings:
        • Prescaler(预分频系数PSC):7199
        • Counter Mode(计数模式):Up(递增计数)
        • Counter Period(自动重载值ARR):1999
        • Internal Clock Division(时钟分频):No Division(不分频)
        • auto-reload preload(预装载功能):Disable(预装载功能关闭)
        • PWM Generation Channel1(PWM输出通道1)部分进行如下的参数设置:
        • Mode(输出模式):PWM mode1(输出模式1)
        • Pulse(占空比):10000(50%占空比)
        • Output compare preload(输出比较预装载功能):Enable(预装载功能开启)
        • Fast Mode(快速输出模式):Disable(不使能快速输出模式)
        • CH Polarity(输出极性):High(输出有效电平为高电平)

注意:

由于 LED 所在的引脚在 PC13, 而该引脚没有定时器比较通道, 因此使用 PA0, 当测试时通过镊子短接 PA0 ~ PC13 即可。

代码如下(在 CubeMX 生成的基础上添加一行代码即可):

/* USER CODE BEGIN 2 */
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);
/* USER CODE END 2 */

PWM 实现呼吸灯

利用PWM信号控制LED指示灯。设置PWM周期为20ms,占空比从0%开始,步进为10%。递增到100%后,又从0%开始,并重复整个过程。占空比修改的时间间隔为100ms。

配置修改如下:

  • Prescaler(预分频系数PSC):7199
  • Counter Period(自动重载值ARR):199

代码如下:

/* USER CODE BEGIN 0 */
uint16_t Duty = 0;    // 占空比
uint16_t Step = 10;   // 步进值
/* USER CODE END 0 */

/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_TIM2_Init();
/* USER CODE BEGIN 2 */
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);
/* USER CODE END 2 */

/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
  /* USER CODE END WHILE */

  /* USER CODE BEGIN 3 */
  for(Duty = 0; Duty <= 200; Duty += Step)
  {
      __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, Duty);
      HAL_Delay(200);
  }
}
/* USER CODE END 3 */

输入捕获功能

输入捕获单元结构

输入捕获功能利用定时器的捕获/比较通道实现,其基本原理是:输入捕获单元不断检测通道上的外部输入信号,当检测到外部信号的有效边沿(上升沿、下降沿或双边沿)时,将计数器寄存器TIMx_CNT的当前值锁存到对应通道的捕获/比较寄存器TIMx_CCRn

STM32-TIMER-IC-0.png

笔记

当检测到外部信号有效时,将计数器中的数值(参考时间),存储到 TIMx_CCRn,并可选的触发中断。当触发了两次捕获之后,我们就可以计算出输入信号的周期、频率、占空比等信息。

输入捕获通道

外部输入信号通过输入捕获通道x对应的引脚TIMx_CHx产生输入信号TIx;

输入滤波器

输入滤波器用于对输入信号的采样滤波,滤波后的信号为TIF。当输入信号存在高频干扰信号时,我们可以通过设置滤波长度,对信号进行多次采样。如果连续N次采样检测都是高电平,则说明这是一个有效的电平信号,从而滤除高频干扰信号。

输入滤波器的工作原理如图8-29所示。从图中可以看到,当输入信号设置为上升沿捕获,滤波长度设置为4时。在捕获到输入信号的上升沿后,输入滤波器将按照采样时钟DTS(采样时钟DTS的频率由内部时钟CK_INT分频获得,分频系数可以是1/2/4)的频率,连续采样4次输入信号。如果都是高电平,才说明这是一次有效的触发信号TIF。如果没有做到连续4次采样都是高电平,则该次触发无效。

STM32-TIMER-IC-1.png

预分频器

捕获信号IC1经过预分频器之后,得到最终的捕获信号IC1PS。在最终的捕获信号IC1PS的作用下,将计数器寄存器TIMx_CNT的当前计数值锁存到捕获/比较寄存器TIMx_CCR1,同时可以发出捕获中断申请CC1I(Capture/Compare 1 interrupt)。

注意:捕获信号的输入有两种方式。

  • 直接输入表示通过本通道对应的通道引脚输入捕获信号;
  • 间接输入表示通过其他通道对应的通道引脚输入捕获信号。

例如:

  • TI1FP1 是捕获通道1的直接输入信号,由捕获通道1对应的通道引脚TIMx_CH1输入。
  • TI2FP1 是捕获通道1的间接输入信号,由捕获通道2对应的通道引脚TIMx_CH2输入。

信号测量原理

我们假设定时器采用上升沿捕获和递增计数模式,具体的测量过程如下:

  • 启动定时器后,在定时时钟TIMx_CLK的作用下(注意:最终起作用的时钟是经过预分频模块分频后的计数时钟CK_CNT),计数器从0开始递增计数。
  • 假设在 $t_b$ 时刻,被测信号出现了一个上升沿,触发第一次捕获。此时,计数器寄存器TIMx_CNT的内容将锁存到捕获/比较寄存器TIMx_CCRn中,并触发捕获中断。在中断回调中,可以保存 $t_b$ 时刻的捕获值。
  • 在中断处理过程中,定时器的计数模块不会暂停,将继续计数。
  • 假设在 $t_d$ 时刻,被测信号又出现了一个上升沿,触发第二次捕获。再次将TIMx_CNT的内容锁存到TIMx_CCRn中。并在捕获中断回调中,保存 $t_d$ 时刻的捕获值。
  • 将两次捕获值相减,再乘以计数时间,就可以计算出信号的周期。

根据上述信号测量的过程,我们可以总结出信号周期的计算公式:

$T=Diff * (PSC+1)/TIMx\_CLK$

其中 $Diff$ 表示两次捕获值相减后的差值。如果被测信号的周期不大于定时器的一个完整计数周期(从0到ARR)时。假设两次连续的捕获值分别为CCRn_1CCRn_2,则捕获差值 $Diff$ 可以按照如下方法计算:

  • 当CCRn_1 < CCRn_2: 捕获差值 Diff = CCRn_2 - CCRn_1
  • 当CCRn_1 > CCRn_2: 捕获差值 Diff = (ARR+1 - CCRn_1) + CCRn_2

注意:如果被测信号的周期较长,大于定时器的一个完整计数周期时,就需要考虑计数溢出的问题。这时,我们可以使能定时器的更新中断,在更新中断回调函数中记录溢出次数 m,最终的捕获差值 Diff = m * (ARR+1) - CCRn_1 + CCRn_2。

信号测量示例

利用定时器2的通道1来测量一个外部脉冲信号的周期、频率、高电平脉冲宽度和占空比,外部脉冲信号由另一个引脚产生并输入,并将测量结果通过串口发送到PC上显示。

CubeMX 配置如下:

  • STM32F103C8T6 (72MHz)
    • Times
      • TIM2:
        • Mode:
          • ClockSource -> Internal Clock
          • Channel1 -> Input Capture direct mode(PA0引脚)
        • Parameter Settings:
          • Counter Setting:
            • Prescaler(预分频系数PSC):0
            • Counter Mode(计数模式):Up(递增计数)
            • Counter Period(自动重载值ARR):65535
            • Internal Clock Division(时钟分频):No Division(不分频)
            • auto-reload preload(预装载功能):Disable(预装载功能关闭)
          • Input Capture Channel 1:
            • Polarity Selection(捕获边沿极性选择):Rising Edge(上升沿)
            • IC Selection(捕获通道选择):Direct(直接输入方式)
            • Prescaler Division Ratio(输人信号分频比):No division(不分频)
            • Input Filter(滤波长度):O(不进行滤波)
      • TIM3:(10kHZ 占空比10%)
        • Prescaler(预分频系数PSC):719
        • Counter Period(自动重载值ARR):9
        • Pulse(占空比):1
/* USER CODE BEGIN PV */
uint32_t          CapVal[3]   = {0}; // 存放捕获值
uint32_t          CapIndex    = 0;   // 捕获状态指示:0表示没有开始捕获,1表示完成一次捕获,2表示完成两次捕获
volatile uint32_t CapFlag     = 0;   // 捕获完成标志:0表示未完成,1表示完成
uint32_t          Period      = 0;   // 存放信号周期
uint32_t          HighTime    = 0;   // 存放高电平脉冲宽度
/* USER CODE END PV */

/* USER CODE BEGIN 2 */
printf("/*Timer Capture Function*/\n");       // 发送提示信息
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);     // 启动定时器3通道1输出PwM信号
HAL_Delay(1000);
HAL_TIM_IC_Start_IT(&htim2,TIM_CHANNEL_1);    // 启动定时器2通道1上升沿捕获
/* USER CODE END 2 */

/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
  /* USER CODE END WHILE */

  /* USER CODE BEGIN 3 */
  if (CapFlag)                                      // 捕获完成
  {
    if(CapVal[2] >= CapVal[0])                      // 信号在一个周期内
    {
      Period = CapVal[2] - CapVal[0];
    }
    else
    {
      Period = 65535 + 1 - CapVal[0] + CapVal[2];
    }

    printf("Period:%.2fms\n", Period/72000.0);      // 计算信号周期(毫秒), 72MMHz/s=72kHz/ms
    printf("Frequency:%dHz\n", 72000000/Period);    // 计算信号频率, 一秒内计数器触发多少次 / 一个周期的计数值 = 频率

    if(CapVal[1] >= CapVal[0])                      // 信号高电平宽度在同一个计数周期内
    {
      HighTime = CapVal[1] - CapVal[0];
    }
    else                                            // 信号高电平宽度不在同一个计数周期内
    {
      HighTime = 65535 + 1 - CapVal[0] + CapVal[1];
    }

    printf("High %.2fms\n", HighTime/72000.0);      // 计算高电平时间
    printf("Duty:%.1f%%\n", HighTime*100.0/Period); // 计算占空比
    printf("****************************\n");
    CapFlag = 0;                                    // 清除捕获完成标志

    // 启动下一次信号测量
    HAL_Delay(1000);
    HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_1);
  }
}
/* USER CODE END 3 */

捕获中断:

/* USER CODE BEGIN 4 */
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
  if (htim->Instance == TIM2) // 判断发生捕获中断的定时器
  {
    if (htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1) // 判断发生捕获中断的通道
    {
      switch (CapIndex)
      {
        // 存放第一次上升沿捕获时的捕获值,修改捕获方式为下降沿,并修改捕获指示
        case 0:
        {
          CapVal[0] = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
          __HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1,
                                        TIM_INPUTCHANNELPOLARITY_FALLING);
          CapIndex = 1;
          break;
        }

        // 存放第一次下降沿捕获时的捕获值,修改捕获方式为上升沿,并修改捕获指示
        case 1:
        {
          CapVal[1] = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
          __HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1,
                                        TIM_INPUTCHANNELPOLARITY_RISING);
          CapIndex = 2;
          break;
        }

        // 存放第二次上升沿捕获时的计数器初值,停止捕获,重置捕获指示,设置捕获完成标志
        case 2:
        {
          CapVal[2] = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
          HAL_TIM_IC_Stop_IT(htim, TIM_CHANNEL_1);
          CapIndex = 0;
          CapFlag = 1;
          break;
        }

        // 错误状态
        default:
        {
          Error_Handler();
          break;
        }
      }
    }
  }
}
/* USER CODE END 4 */

捕获过程示意图如下:

STM32-TIMER-IC-2.png

笔记

STM32F103C8T6 遇到一个有趣的现象, 当输出 PWM 占空比为 10% 的时候, 第一次测量总是比后面的周期大两倍, 后面就正常了。但是占空比设置为 40% 这个现象就消失了。

下面是出现问题的输出:

/*Timer Capture Function*/
CapVal   : 2752,10672,17152
Period   : 0.20ms(14400)
Frequency: 5000Hz
High     : 0.11ms
Duty     : 55.0%
****************************
CapVal   : 21406,22126,28606
Period   : 0.10ms(7200)
Frequency: 10000Hz
High     : 0.01ms
Duty     : 10.0%
****************************
CapVal   : 32534,33254,39734
Period   : 0.10ms(7200)
Frequency: 10000Hz
High     : 0.01ms
Duty     : 10.0%
****************************
/*Timer Capture Function*/      - 重新上电, 现象依旧
CapVal   : 2752,10672,17152
Period   : 0.20ms(14400)
Frequency: 5000Hz
High     : 0.11ms
Duty     : 55.0%
****************************
CapVal   : 21414,22134,28614
Period   : 0.10ms(7200)
Frequency: 10000Hz
High     : 0.01ms
Duty     : 10.0%

分析记录,示意图如下:

   ┌┐┌┐┌┐┌┐┌┐┌┐┌┐┌┐┌┐┌┐
   ┌┐                  ┌┐                  ┌┐
   ││                  ││                  ││
───┘└──────────────────┘└──────────────────┘└───
   | 720 + 6480 = 7200 |
  • 72,000,000Hz / (719 + 1) = 100,000Hz # 计时器分频后的频率
  • 100,000Hz / 10 = 10,000Hz # PWM 每个周期的频率
  • PWM 每个周期 10 计数点, 每个计数点 720 个时钟点, 一个周期计数点 7200 个时钟点(timer ticks)
  • PWM 每个周期占空比为 10%, 即 高电平 720 个时钟点, 低电平 6480 个时钟点
  • 根据首次测量记录: 2752,10672,17152:
  • 10672 - 2752 = 7920 第一次上升沿到下降沿的计数点大于一个周期的计数点 7200 个时钟点
  • 17152 - 10672 = 6480 下降沿到第二次上升沿的计数点符号正常的低电平 6480 个时钟点
  • 7920 = 6480 + 720 + 720 分析结构大概是这样 ┘└──────────────────┘└
  • 那么可能的原因可能是 当成功捕获到第一个上升沿后, 触发了中断, 然后中断中修改为捕获下降沿, 但此时很可能已经到达低电平了,毕竟上升沿只有720个时钟点(10us), 因此会跳过一个上升沿直到下一个下降沿才会触发。这样也能后解释为什么占空比设置为 40% 这个现象就消失了。
  • 但为什么在第一次之后的测量中这个现象也不会出现了,还没有分析出来。

捕获的流程如下, 这个时间窗口大概可能需要 几十到上百个CPU周期, 而高电平只有 (720 timer ticks) 因此错过的概率很高。

- 边缘检测, 捕获到上升沿
- 置位CC1IF
- NVIC触发中断
- ISR中断服务程序
- HAL_TIM_IC_CaptureCallback
- 极性切换

后续优化可以尝试使用两个通道同时检测上升沿与下降沿, 减少极性切换导致的缺陷。