3742 字
19 分钟
[STM32] TIM 定时器
2025-12-24
2025-12-27
声明

该文档没有使用任何AIGC内容,主要参考了STM32F10xxx的参考手册。

硬件结构#

核心:时基单元#

该部分是实现”定时”功能的基础,虽然名字是”时基”,但应该被看作是一个计数器

其输入是来自触发控制器的时钟信号,可暂时先视作系统时钟信号。

硬件组成#

组件运行逻辑相关组件
16位预分频器 PSC遇到上升沿时输出一个脉冲给CNT,然后等待 N 个脉冲 (不输出)对应的影子寄存器
16位计数器 CNT每遇到一个上升沿,寄存器就执行一次计数 (有多种模式)“触发控制器”的CNT控制器
16位自动重装载寄存器 ARR根据向上/向下/中央对齐模式操作CNT,并产生更新事件与更新中断对应的影子寄存器

例如,有一个 72MHz 的时钟信号作为输入。若设置为向上计数,PSC = (7200-1),ARR = (10000-1)。则预分频器会输出72MHz7200=10000Hz\frac{72\text{MHz}}{7200} = 10000\text{Hz} 的信号,又由于ARR会在 CNT==(10000-1) 的时候产生更新事件,所以最终的效果就是每秒产生一次更新。

关于中断和事件

在此处,更新事件只会影响 CNT 与硬件电路(如 TRGO),而各个中断可以配置 NVIC 或 DMA 来处理

影子寄存器(预加载寄存器):在定时器运行的过程中,可能需要对PSC或ARR做修改,而计数器仍在运行,这可能会诱发不可预料的错误。而影子寄存器可以先记录要修改的目标值,在下一次触发更新事件时,再将修改同步到实际寄存器

影子寄存器需要手动激活

例如ARR的影子寄存器,需要使用TIM_ARRPreloadConfig()函数使能

向上计数模式#

计数器从 0 开始向上计数到 ARR,然后产生一个更新事件并将 CNT 置 0

向下计数模式#

计数器从 ARR 开始向下计数到 0,然后产生一个更新事件并将 CNT 置 ARR

中央对齐模式#

ToDo


通用定时器 的结构与运行逻辑#

对于通用定时器,应该将其视为一个“可编程的通用计数器”。其主要结构如下:

STM32 通用定时器框图

触发器信号输入部分#

  • 内部时钟 CK_INT:来自RCC的 TIMxCLK
  • ETRF (处理后的外部时钟信号):由 ETR (外部触发引脚) 的信号经过 极性选择、边沿检测、预分频 (Prescaler) 得到 ETRP,再经过滤波得到 ETRF,发送到触发控制器用于外部时钟模式2(直接输入时基单元
  • ITR (Internal Trigger 内部触发信号):由四路 ITRx 信号经过 MUX (多路复用器)得到
  • TRC (Timer Remote Control 定时器遥控信号):由 ITR 与 TI1F_ED (TI1信号经过滤波和双边沿检测,常用于 三相无刷直流电机) 经过 MUX 得到
  • TRGI (Trigger Input 触发器输入信号):由 ETRF、TRC、TI1FP1、TI2FP2 经过 触发选择器(TS) 得到,作为从模式的输入信号

触发控制器#

作为整个定时器的信号路由,负责处理输入与输出,并控制CNT的工作。可以选择以下时钟源:

  • 内部时钟模式:(默认状态)使用系统时钟,CK_INT 信号。
  • 外部时钟模式2:直接使用 ETRF 信号,注意不是从 TRGI 得到的信号。

所选的时钟源的信号将被直接发送到CK_PSC,而不经过从模式控制器。这意味着它可以与从模式中的复位、门控、触发模式一起使用,这也是两种外部时钟模式的区别。

时钟源的配置

初始化定时器时,默认会使用内部时钟作为时钟源。

触发控制器可以同时开启两种模式:

  • 从模式 (Slave controller mode):接收外部信号 TRGI,控制CNT

    • 外部时钟模式1:来自TRGI的信号将被发送到 CK_PSC,作为时基单元的预分频器的输入。

    • 复位模式每次收到信号,CNT将被清零

    • 门控模式:只有在收到信号时,CNT才会计数

    • 触发模式:收到信号,CNT将被使能

    • 编码器模式:这是一种特殊的从模式,开启后从模式控制器将自动控制 CNT 进行自增或自减。相当于使用了一个带有方向选择的外部时钟。

      • 编码器模式1:2倍频。只在 A 相跳变时计数,看 B 相电平判方向
      • 编码器模式2:2倍频。只在 B 相跳变时计数,看 A 相电平判方向
      • 编码器模式3:4倍频 (最常用)。AB 两相跳变时都计数,看另一相电平判方向,精度最高
      关于编码器模式下的ARR

      通常将ARR设置为65535,这样就可以利用补码的特性来很好的处理正反转 例如,编码器接口收到了两个反转信号,CNT 变为65534。又收到两个正转信号,CNT 先加到65535,再加一次就是0

  • 主模式 (Master controller mode):将内部的事件输出为外部信号 TRGO(发往其它定时器实现级联,或发往 DAC/ADC)

    • 复位:复位事件发生时,发送一个正脉冲
    • 使能:将 CNT_EN 作为 TRGO,只要计数器在运行就是高电平
    • 更新:当更新事件发生时,发送一个正脉冲
    • 比较脉冲:在输入通道1 (IC1) 发生一次捕获或一次比较成功时,将要设置 CC1IF 标志时,发送一个正脉冲
    • 四种比较模式:对应四个通道 ICx,将 OCxREF (Output Compare x Reference Signal 输出比较 x 参考信号) 作为 TRGO

输入/输出部分#

该部分有四路,每路有一个输入捕获单元和一个输出比较单元(只能选择一个开启)。该部分的核心是 CCRx(捕获/比较寄存器),输入与输出都要操作该寄存器。 CCRx 有对应的影子寄存器(预装载寄存器),可配置开启。

输入捕获单元#

以第一路通道举例。信号从 TIMx_CH1 外部引脚输入,经过滤波和边沿检测得到 TI1,之后分为两路。TI1FP1 通往 IC1 的 MUX,TI1FP2 通往 IC2 的 MUX;经过 预分频 后得到触发信号,触发时 CNT 将被复制到 CCR1 (捕获/比较寄存器)。

关于编码器模式

而在编码器模式中,信号不经过边沿检测器就直接进入了编码器接口,因此 TIM_ICPolarity 在这里指的是 是否要对输入信号做反相处理。如果发现编码器接口中得到的数值与实际正好相反,就可以改变某一个 IC 的 TIM_ICPolarity,省去了换线的麻烦。

输出比较单元#

对于 OCx (OC1 ~ OC4),它会 根据 CNT、CCRx 与设定的输出比较模式,将结果输出到 OCxREF,并触发 CCxI 中断,若触发该中断时,中断标志位非0 (即之前的中断未被处理完),会将 CC1OF 置1(表示系统未能及时处理捕获信号)

有下面几种输出比较模式

  • 冻结:REF 保持原状态

  • 匹配时置有效电平:CNT==CCRx 时,REF 置有效电平(不适合输出连续信号)

    有效电平

    在通用计时器中,有效电平即为高电平。

  • 匹配时置无效电平:CNT==CCRx 时,REF 置无效电平(不适合输出连续信号)

  • 匹配时电平翻转:CNT==CCRx 时,REF 电平翻转

    输出一个占空比始终为50%的波形

    设置CNT=0,定时器就会在每次更新时翻转电平,因此周期为ARR的两倍,

  • 强制为无效电平:REF 强制为无效电平

  • 强制为有效电平:REF 强制为有效电平

  • PWM模式1

    • 向上计数CNT<CCRx\text{CNT} < \text{CCRx} 时,REF 置有效电平;CNTCCRx\text{CNT} \geq \text{CCRx} 时, REF 置无效电平
    • 向下计数CNTCCRx\text{CNT} \leq \text{CCRx} 时,REF 置有效电平;CNT>CCRx\text{CNT} > \text{CCRx} 时, REF 置无效电平
  • PWM模式2

    • 向上计数CNT<CCRx\text{CNT} < \text{CCRx} 时,REF 置有效电平;CNTCCRx\text{CNT} \geq \text{CCRx} 时, REF 置无效电平
    • 向下计数CNTCCRx\text{CNT} \leq \text{CCRx} 时,REF 置有效电平;CNT>CCRx\text{CNT} > \text{CCRx} 时, REF 置无效电平
    关于PWM模式

    模式2 与 模式1 的输出完全反相;一般较少使用向下计数。对于模式1的向上计数,有如下参数: PWM频率:  Freq = CK_PSC / (PSC + 1) / (ARR + 1) PWM占空比: Duty = CCR / (ARR + 1) PWM分辨率: Reso = 1 / (ARR + 1),也就是占空比最小变化步距,越小越好。

    快速使能模式

    仅当开启从模式并由外部信号触发,且使用PWM输出时,该模式才有用。 开启该模式后,会跳过部分同步逻辑,缩短信号的输出延迟(牺牲微小的逻辑同步稳定性) 使用TIM_OCxFastConfig开启

注意

输入捕获 和 输出比较 不能同时开启,因为它们共用 CCRx

输出控制#

负责将 REF 转化为 GPIO 的输出

  • 极性选择:设置有效电平极性(为高电平或低电平),参数为 TIM_OCPolarity
  • 输出使能:参数为 TIM_OutputState

基本定时器 的结构与运行逻辑#

基本定时器由一个最简单的触发控制器和一个时基单元组成,大概只能处理简单的定时任务。

STM32 基本定时器框图

高级定时器 的结构与运行逻辑#

相比通用定时器,其在时基单元后连接了 重复次数计数器;在输出部分增加了 DTG 死区生成电路;输出引脚变为了两个互补的输出,可用来控制三相无刷电机;接入了刹车输入时钟安全系统,用于在意外情况切断输出。

ToDo


基于标准库的代码#

展开代码

在下面的代码块中,点击 collapsed lines 即可展开代码

初始化一个最简单的通用定时器#

以下代码用于初始化使用内部时钟的,每秒触发一次中断的TIM2。

流程:

  • 初始化 TIM2 时钟
  • 初始化 TIM2
  • 打开 TIM2 的更新中断
  • 配置 NVIC 处理更新中断
void TIM_Config(){
20 collapsed lines
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
TIM_InternalClockConfig(TIM2); // 配置TIM2使用内部时钟
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 对定时器时钟的分频操作
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; // 向上计数
TIM_TimeBaseInitStructure.TIM_Prescaler = 0; // 预分频器
TIM_TimeBaseInitStructure.TIM_Period = 10 - 1; // 自动重装载寄存器的值
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
TIM_ClearFlag(TIM2, TIM_IT_Update); // 避免启用后立刻触发的中断
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_Init(&NVIC_InitStructure);
TIM_Cmd(TIM2, ENABLE);
}
注意设置NVIC优先级分组

NVIC_SetPriorityGrouping(NVIC_PriorityGroup_2);


初始化一个通用定时器,用于输出PWM#

该示例可用于驱动SG90舵机(让PA1输出周期为20ms的PWM)

流程:

  • 开启 GPIO 与 TIM 的时钟
  • 配置时基单元
  • 配置输出比较单元
  • 初始化 GPIO
  • 开启 TIM
void PWM_Config(){
28 collapsed lines
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
// 72MHz * 0.020 = 72*20000
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 配置时钟分频,此处设置为不分频
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; // 向上计数
TIM_TimeBaseInitStructure.TIM_Prescaler = 72-1; // 预分频 (PSC)
TIM_TimeBaseInitStructure.TIM_Period = 20000-1; // 周期 (ARR)
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; // PWM1模式
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; // 有效电平为高电平
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; // 输出使能
TIM_OCInitStructure.TIM_Pulse = 1500; // CCR初始值为1500 (高电平宽度 1.5ms)
TIM_OC2Init(TIM2, &TIM_OCInitStructure);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
TIM_OC2PreloadConfig(TIM2, TIM_OCPreload_Enable); // 开启CCR的影子寄存器
TIM_ARRPreloadConfig(TIM2, ENABLE); // 开启ARR的影子寄存器
TIM_Cmd(TIM2, ENABLE);
}

初始化一个通用定时器,用测周法测量 PWM#

测周法:在两个上升沿内,以标准频率 fcf_c 记次,得到 N,则频率 fx=fcNf_x = \frac{f_c}{N}

该示例中,fc=72MHz72=1MHzf_c = \frac{72\text{MHz}}{72} = 1\text{MHz}。 每个上升沿,IC1 都会将 CNT 复制到 CCR1。随即从模式控制器将 CNT 复位。因此只需要读取CCR1,就可以得到最新的N。 周期值(注意单位换算):TIM_GetCapture1(TIM3)+1 频率值:(1000000)/(TIM_GetCapture1(TIM3)+1)

注意核算可测量的最小频率值

此处的最小频率为 10000006553515.26Hz\frac{1000000}{65535} \approx 15.26\text{Hz}

同理,将 TI1FP2 信号输入 IC2,并在每个下降沿将 CNT 复制到 CCR2,就可以得到高电平在一个周期中的占比,通过计算可以得到占空比(此处计算结果为千分值) (TIM_GetCapture2(TIM3)+1)*1000/(TIM_GetCapture1(TIM3)+1)

流程:

  • 开启 GPIO 与 TIM 的时钟
  • 配置时基单元
  • 配置输入捕获单元
  • 选择从模式-复位模式
  • 选择输入触发信号的来源 (TRGI)
  • 初始化 GPIO
  • 开启 TIM
void PWMInput_Config(){
38 collapsed lines
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; // 注意此处使用了下拉输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 72MHz = 72*1000000 (1s)
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 配置时钟分频,此处设置为不分频
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; // 向上计数
TIM_TimeBaseInitStructure.TIM_Prescaler = 72-1; // 预分频 (PSC)
TIM_TimeBaseInitStructure.TIM_Period = 65536-1; // 周期 (ARR)
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure);
TIM_ICInitTypeDef TIM_ICInitStructure;
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1; // 配置IC1
TIM_ICInitStructure.TIM_ICFilter = 0xF; // 不启用滤波
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; // 上升沿触发
TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; // 不分频
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; // TI1 连接到 IC1(直连)
TIM_ICInit(TIM3, &TIM_ICInitStructure);
TIM_ICInitStructure.TIM_Channel = TIM_Channel_2;
TIM_ICInitStructure.TIM_ICFilter = 0xF;
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Falling;
TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_IndirectTI;
TIM_ICInit(TIM3, &TIM_ICInitStructure);
TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_Reset); // 选择从模式-复位模式
TIM_SelectInputTrigger(TIM3, TIM_TS_TI1FP1); // 选择 TI1FP1 作为 TRGI
TIM_ARRPreloadConfig(TIM3, ENABLE);
TIM_Cmd(TIM3, ENABLE);
}

初始化一个通用定时器,用标准库的PWMI模式测PWM#

PWMI模式在硬件层面是不存在的,是标准库为了简化程序而引入的函数。如果使用它,则上节代码中配置输入捕获的部分可以改为:

TIM_ICInitTypeDef TIM_ICInitStructure;
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;
TIM_ICInitStructure.TIM_ICFilter = 0xF;
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;
TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;
TIM_PWMIConfig(TIM3, &TIM_ICInitStructure);
关于 TIM_PWMIConfig()

只需要传入一个通道的初始化结构体,函数会自动将另一个通道设置成对应的配置。

例如:传入 通道1,直连,上升沿。函数就会自动配置 通道2,交叉,下降沿

但注意不能传入通道3或通道4


初始化一个通用定时器,读取编码器#

流程:

  • 开启 GPIO 与 TIM 的时钟
  • 初始化 GPIO 为输入模式
  • 配置时基单元(不分频)
  • 配置输入捕获单元
  • 配置编码器接口模式
  • 开启 TIM
void EncoderInput_Config(){
28 collapsed lines
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 注意此处使用了上拉输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseStructInit(&TIM_TimeBaseInitStructure);
TIM_TimeBaseInitStructure.TIM_Prescaler = 0; // 预分频 (PSC)
TIM_TimeBaseInitStructure.TIM_Period = 0xFFFF; // 周期 (ARR)
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure);
TIM_ICInitTypeDef TIM_ICInitStructure;
TIM_ICStructInit(&TIM_ICInitStructure);
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;
TIM_ICInitStructure.TIM_ICFilter = 0xF; // 滤波拉满
TIM_ICInit(TIM3, &TIM_ICInitStructure);
TIM_ICInitStructure.TIM_Channel = TIM_Channel_2;
TIM_ICInitStructure.TIM_ICFilter = 0xF; // 滤波拉满
TIM_ICInit(TIM3, &TIM_ICInitStructure);
// 开启TIM3的编码器接口,模式3,两个通道均不反相
TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);
TIM_Cmd(TIM3, ENABLE);
}
如何测速

由于硬件限制,是无法做到硬件层面的测速的。最佳的方法就是再配置一个定时器,定时去读取编码器测量的结果,并计算速度。

[STM32] TIM 定时器
https://blog.yschain.top/posts/stm32-tim/
作者
YSChain
发布于
2025-12-24
许可协议
CC BY-NC-SA 4.0