【STM32】基础定时器 TIM 详解:从极简原理到寄存器全解析


基础定时器 TIM 详解:从极简原理到寄存器全解析

1. 极简原理:把定时器当作一个"闹钟"

生活中带"定时"功能的场景:发条或电池驱动闹钟按秒计时,到了设定时间(闹钟响),执行既定事项(起床、去上学)。

定时器的本质功能:设定延时,然后超时后,通过超时触发完成某种工作。

要在单片机内实现类似效果,需要做到三点:

  1. 把时间本身进行量化(这样才能进行时间比较);
  2. 设定未来超时时间;
  3. 超时,触发完成特定工作。

1.1 量化时间(微观视角)

想象一个以固定时钟频率对外输出电脉冲的时钟源,再想象一个计数器硬件电路------每收到一个电脉冲(上升沿)就自增 1。两者结合,就是"时间量化"的最小系统:

① 时钟源的"节拍"必须是均匀的

假设时钟源输出 1000Hz 方波,那么每个时钟周期 = 1/1000s = 1ms,也就是每 1ms 产生一次上升沿。这个"周期"就是时间的"最小刻度"。

② 计数器按节拍自增

计数器每收到一次上升沿就 +1:收到 1 次 → 计数值 1;收到 123 次 → 计数值 123。

③ 数字即时间

因为节拍固定(1ms/次),所以:

复制代码
计数值 123  ←→  已经过去了 123ms

在固定频率的时钟周期下,计数器数字,就是时间。

④ 量化精度由时钟频率决定

  • 时钟 1000Hz → 1ms 计一次 → 时间分辨率 1ms,能分辨的最短时间差是 1ms;
  • 时钟 1MHz → 1us 计一次 → 分辨率 1us;
  • 时钟频率越高,时间分辨率越高,计时越精准(这正是后面为什么要引入"分频器"和追求高频时钟源的原因)。

1.2 设定未来时间

既然时间能被量化,当前时间和未来某个时间点当然也能被量化、能比较。

我们再新增一个用户可以设置的寄存器(表示未来某个时间点),计数器更新时和它比较是否相等:

  • 设定 500ms → 计数器每次自增都做一次比较;
  • 不相等 → 继续计数(本质就是在做延时);
  • 相等 → 说明定时器"到点"了,该"响"了。

这个"可设置的寄存器",就是后面 TIMx_ARR(自动重装载寄存器) 的雏形;

"比较是否相等"的电路,就是时基单元内部的比较器。

1.3 触发完成特定工作

当计数器数值等于或大于用户提前设定好的数字时,自动触发某种动作,完成具体工作。

这个"数字的比较"本质就是"时间的比较"------到达预定时间之前只做计数,本质就是在做延时。

这里关于"超时/溢出"的条件说得很模糊("等于还是大于"?),后面在定时器内部会专门阐述------结论是溢出条件是 CNT = ARR + 1("凡溢出,必超过"),先留个悬念。

1.4 小结:定时器"三要素"

要素 硬件载体 说明
量化时间 时钟源 + 计数器(CNT) 把时间变成数字
设定未来时间 可设寄存器(ARR) 设定"闹钟响"的时刻
超时触发 事件/中断/DMA 电路 到点后自动干活

2. 基础定时器是什么?

定时器的核心就是一个硬件计数器,它按固定节奏数数,数到你设定的数字时就触发一个动作。


3. 为什么要有基础定时器?

如果每隔 2s 让 LED 翻转一次、每隔 2s 打印一次数据,之前只能:

cpp 复制代码
while(1)
{
    HAL_Delay(XX);   // 软件延时
    // 做动作
}

软件延时看似简单,其实有三大痛点:

痛点 说明
占用 CPU HAL_Delay 期间 CPU 只能空转等待,什么活都干不了。 定时器一次只配 1 个 LED,如果配 10 个任务、每个都要延时,CPU 就被"卡死"在延时里
时间不精准 软件延时的计时基于 SysTick 中断,实际触发时间会受中断优先级、其他中断抢占、代码执行时间影响,误差不可控
无法并发 多个任务需要不同延时周期时,软件延时只能顺序执行,无法"并行计时"

硬件定时器的优势正好相反:

由硬件独立计数,CPU 零负担;

超时通过中断/事件/DMA 精准触发;

多个定时器可以并行工作,互不干扰。

类比:

软件延时 = 你亲自盯着秒表倒数;

硬件定时器 = 交给一个独立的人(硬件)看表,到点了他拍你肩膀(中断)或者直接替你干活(DMA/事件)。


4. STM32 定时器类别介绍

对于 STM32F103ZE 芯片,按功能分三类:

类别 定时器 功能特点
基础定时器 TIM6、TIM7 仅有基本的计时功能和 DAC 转换触发功能(本章主讲)
通用定时器 TIM2、TIM3、TIM4、TIM5 基础全部功能 + 输入捕获、输出比较、PWM 生成、正交编码器输入
高级定时器 TIM1、TIM8 前两者全部功能 + 互补 PWM 输出与死区控制、紧急刹车、主从模式(电机领域用)

器件一览(数据手册 STM32F103 功能配置表):

型号 Flash SRAM 通用定时器 高级定时器 基础定时器 GPIO
F103Rx 256K 48K 4(TIM2~5) 2(TIM1/8) 2(TIM6/7) 51
F103Vx 384K 64K 4 2 2 80
F103Zx 512K 64K 4 2 2 112

"多达 11 个定时器"的由来:

8 个片上定时器(TIM1~8)+ 系统定时器 SysTick + 2 个看门狗(IWDG 独立看门狗、WWDG 窗口看门狗)。

认知细节:

基础、通用、高级定时器看起来都是定时器,但最好把他们当作 3 个不同的外设。

这样可以用独立视角理解不同定时器,不容易把概念搞混。


5. 基础定时器原理

5.1 定位:片上外设

基础定时器(TIM6/TIM7)属于 MCU 内部的片上外设。

从系统结构图看,它挂在 APB1 总线上,与 I2C1/2、SPI2、USART2/3、DAC 等"慢速外设"同住一屋。

后面研究基础定时器原理时,就认为是研究 TIM6 或 TIM7 这样的片上外设即可。

5.2 原理:基本定时器框图(图144)

整个结构框图(参考手册 stm32f103xx):

cpp 复制代码
来自RCC的TIMxCLK → 内部时钟CK_INT → 触发控制器 → 预分频器PSC → CK_CNT → 计数器CNT
                                                └→ 自动重装载寄存器ARR(预加载→影子)◄──┘(比较)
                                                             │
                                          CNT 溢出 → 事件 UEV → 中断和DMA输出 / TRGO至DAC

下面一部分一部分来看。

5.2.1 时钟源(时钟树全景)

定时器要根据固定频率更新计数器,就必须要有时钟源。

在 STM32F103ZET6 中,这个时钟源由 RCC 模块时钟树提供。

完整时钟路径(标准配置下):

bash 复制代码
HSE 外部晶振 8MHz
    → PLL 锁相环 ×9
    → SYSCLK 系统时钟 72MHz
    → AHB 预分频器(HCLK 分频=1)
    → HCLK 72MHz
    → APB1 预分频器(PPRE1=/2)
    → PCLK1 = 36MHz  (APB1 总线时钟)
    → 定时器时钟补偿 ×2
    → TIMxCLK = 72MHz

其中 APB1 预分频系数由 RCC_CFGR 寄存器的 PPRE12:0 位决定(000=不分频、100=/2、101=/4...)。我们不用手动配它(标准库/CubeMX 已配好),但要理解它的后果。

关键规则:

基础定时器挂接在 APB1 总线上,APB1 最大频率 36MHz。

输入到定时器内部的时钟频率和 APB1 预分频器配置系数有关:

复制代码
如果 APB1 分频系数 = 1,则 TIMxCLK = APB1
如果 APB1 分频系数 ≠ 1,则 TIMxCLK = 2 × APB1
场景 HCLK APB1 分频 APB1 频率 TIMxCLK
① 标准全速(最常见) 72MHz 2 36MHz 36MHz × 2 = 72MHz
② 低速运行(不翻倍) 36MHz 1 36MHz 36MHz
③ 大比例分频 72MHz 4 18MHz 18MHz × 2 = 36MHz

为什么会有这么奇怪的"×2"设定? 本质是对定时器时钟的精度补偿:

  • 定时器时钟源频率越高越好(计数精度、计时精度越高)------希望它跑 72MHz;
  • 但 APB1 总线上挂了很多 UART、I2C 等"慢速"外设,总线频率太高它们受不了、功耗激增------APB1 必须 ≤ 36MHz;
  • 于是规定"只要 APB1 分频 > 1,定时器时钟 = APB1 的 2 倍"------既满足慢速外设,又尽量不损失定时器精度。

三个时钟概念:

名称 含义
TIMxCLK 定时器外设时钟(来自 RCC),频率取决于挂载总线和 RCC 倍频设置
CK_INT 内部时钟。定时器选择"内部时钟源"模式时作为驱动源,基础定时器只会用 CK_INT
CK_PSC 预分频器输入时钟,真正输入到 PSC 的时钟

细节:

对基础定时器而言,TIMxCLK = CK_INT = CK_PSC,可以把这三个概念当成"一根导线"上的概念。

之所以存在三组概念,是为了手册表达方便和未来硬件扩展(比如通用定时器可以通过外部引脚输入时钟,那时 CK_INT 就不等于 TIMxCLK 了)。

5.2.2 时基单元(Time-Base Unit)

这是基础定时器的最核心部分,由三个 16 位寄存器组成:

寄存器 位数 取值范围 作用
TIMx_CNT (计数器) 16 位 0, 65535 时钟信息到来时自动计数,完成时间的量化
TIMx_ARR (自动重装载寄存器) 16 位 0, 65535 与未来超时时间相关:设置未来时间点,时基单元比较 CNT 与 ARR
TIMx_PSC (预分频器) 16 位 0, 65535 动态配置时间分辨率(分频)

① TIMx_PSC 预分频器(偏移 0x28,复位值 0x0000)

寄存器位定义:

复制代码
15  14  13  12  11  10   9   8   7   6   5   4   3   2   1   0
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│                        PSC[15:0]  (rw)                        │
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
  • 位 15:0 PSC[15:0]:预分频器数值;
  • 计数器的时钟频率 CK_CNT = fCK_PSC / (PSC[15:0] + 1);
  • 在每一次更新事件(UEV)时,PSC 的数值被传送到实际的预分频寄存器(影子)中。

为什么分频系数是 PSC+1 而不是 PSC?

因为 PSC=0 时至少要"1 分频"(不分频)。

如果公式是 /PSC,PSC=0 就会除以 0------所以硬件规定 实际分频系数 = PSC + 1:

需求 PSC 设置 实际分频系数
1 倍分频(不分频) PSC = 0 1
1000 倍分频 PSC = 999 1000
最大分频 PSC = 65535 65536
复制代码
实际分频系数 = PSC[15:0] + 1,范围 [1, 65536]
F_CK_CNT = F_CK_PSC ÷ (PSC[15:0] + 1)

② TIMx_CNT 计数器(偏移 0x24,复位值 0x0000)

寄存器位定义:

复制代码
15  14  13  12  11  10   9   8   7   6   5   4   3   2   1   0
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│                        CNT[15:0]  (rw)                        │
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
  • 位 15:0 CNT[15:0]:计数器数值;
  • 有了分频后的 CK_CNT,计数器每经历一个时钟周期就自动计数一次;
  • 基础定时器只支持向上计数(0,1,2,3,4...),其他计数方式(向下、中央对齐)是通用定时器的功能,不用考虑;
  • 为什么是 16 位?16 位最大 65535,配合分频器足以覆盖绝大多数定时需求(比如 72MHz 最高分辨率下,ARR=65535 时最长周期约 0.91ms,但加上 PSC 最大分频 65536,最长可达约 59.6s)。

③ TIMx_ARR 自动重装载寄存器(偏移 0x2C,复位值 0x0000)

寄存器位定义:

复制代码
15  14  13  12  11  10   9   8   7   6   5   4   3   2   1   0
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│                        ARR[15:0]  (rw)                        │
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
  • 位 15:0 ARR[15:0]:自动重装载数值;
  • ARR 的数值将传送到实际的自动重装载(影子)寄存器中;
  • 如果自动重装载数值为 0,则计数器停止;
  • 为什么名字这么怪("自动重装载")?------因为未来一旦超时,定时器会把预加载寄存器中的值自动重新装载(重装)到影子寄存器中,这个动作是硬件自动完成的,所以叫"自动重装载"。后面讲影子寄存器时会彻底明白,此处先当普通 16 位寄存器。
5.2.3 溢出与超时(重点概念,逐步拆解)

假设设置 ARR = 3、时钟 1000Hz(1ms 一次计数)。我们用表格跟踪计数器的一生:

脉冲序号 时间点 CNT 值 与 ARR=3 比较 是否触发动作
(启动) 0ms 0 未到 否
第 1 个脉冲 1ms 1 未到 否
第 2 个脉冲 2ms 2 未到 否
第 3 个脉冲 3ms 3 CNT == ARR 否(临界溢出,不算)
第 4 个脉冲 4ms 4(溢出) CNT == ARR+1 是!触发超时

关键认知:

  • CNT == ARR 在硬件角度并不是溢出,也不会触发任何动作,最多称之为临界溢出;
  • 按数字溢出的认识:凡溢出,必超过------计数器计到 ARR 设定值后,再计一个脉冲时发生的归零(或翻转)动作,才叫做定时器溢出;
  • 计数器溢出后自动从 4 变回 0(支持重新开始,可设置);同时时基单元硬件产生一个"溢出信号"触发后续动作。

结论:

超时条件是 CNT = ARR + 1。

当你设置 ARR=3、1000Hz 时,超时时间其实是 4ms------超时时间由 ARR+1 决定。

例:设置 ARR=999,表示 1000ms 之后超时;反过来,要 1000ms 超时,就设置 ARR=999。

概念同步:定时器视角里,定时器溢出 = 定时器超时,后续当做一个概念。

5.2.4 计算练习(公式推导 + 典型例题)

先把公式推导一遍(面试要能现场推):

bash 复制代码
第 1 步:CK_CNT = CK_PSC / (PSC + 1)        ← 分频后的计数时钟
第 2 步:单个周期时长 = 1 / CK_CNT = (PSC + 1) / CK_PSC   ← 计一次数花的时间
第 3 步:超时时间 = 单个周期时长 × (ARR + 1)  ← 共需 ARR+1 次计数才溢出
第 4 步:代入 → 超时时间 = ((PSC + 1) × (ARR + 1)) / CK_PSC  ← 总公式

练习一(正向计算):TIMxCLK=72MHz,分频 PSC=71,ARR=999,求单次超时时间和 1s 内超时次数?

复制代码
① 频率被切分:72 / (71+1) = 1MHz → 即 1us 触发一次计数
② 超时时间:(999+1) × 1us = 1000us = 1ms
③ 1s 内超时次数:1s / 1ms = 1000 次

练习二(逆向计算):需求每 500ms 触发一次超时,怎么设置参数?

复制代码
① 让定时器工作在最高频率 TIMxCLK = 72MHz
② 设定分频:期望每 100us 产生一次脉冲触发计数 → CK_CNT = 10kHz
③ CK_CNT = CK_PSC / (PSC+1) → PSC+1 = 72MHz / 10kHz = 7200 → PSC = 7199
   (预分频器 16 位最大 65535,7199 < 65535,设置合法)
④ 500ms / 100us = 5000 次计数 → ARR = 5000 - 1 = 4999
   (自动重装载 16 位最大 65535,4999 < 65535,设置合法)

练习三(综合):想获得 1s 周期、2us 分辨率,怎么配?

复制代码
① 分辨率 2us → CK_CNT = 500kHz → PSC+1 = 72MHz / 500kHz = 144 → PSC = 143
② 1s / 2us = 500000 次计数 → 但 ARR 最大只能 65535!
③ 结论:单靠基础定时器无法同时满足"高分辨率 + 长周期",需要换方案
   (提高分辨率放弃周期 / 降低分辨率 / 用 32 位计数器组合------这正是通用定时器/级联的意义)

小结:定时器配置的本质是用 PSC 定"刻度",用 ARR 定"总数",二者共同决定超时时间。

5.2.5 影子寄存器(关键机制,三阶段拆解)

自动重装载寄存器 TIMx_ARR(Auto-Reload Register)有些特殊,由 2 个寄存器组成:

  1. 预加载寄存器(Preload Register):程序员可以看见的、用于读/写操作的寄存器;
  2. 影子寄存器(Shadow / Active Register):程序员看不见,但在内部真正起作用的寄存器。

读写路径:

对 ARR 的读、写操作,实际是通过读写预加载寄存器实现的;

预加载寄存器中的数值会在特定时机(更新事件 UEV)自动传输到影子寄存器中(单向传输,就像通信主题里数据寄存器和移位寄存器)。

超时比较的对象:

超时比较,都是计数器 CNT 和影子寄存器比较**------影子寄存器才是硬件使用的寄存器。**

参考手册上看到的寄存器其实是预加载寄存器,而硬件内部参与比较的是影子寄存器。

未来一旦超时,基础定时器会自动:

  1. TIMx_ARR自动把预加载寄存器的值设置到影子寄存器中("自动重装载"名字的由来);
  2. 计数器 CNT 自动清 0,方便下次重新开始计数。

为什么需要影子寄存器?(三阶段场景)

假设:

时钟源 1ms 一个周期,计数器 1ms 自增一次,超时时间是 100ms 之后,当前计数器已经计数 60ms。

场景一(无影子寄存器):突然想把超时时间从 100ms 改为 50ms。

复制代码
① 修改立即生效(无影子缓冲)
② 当前计数器已经是 60 > 新设定 50
③ 超时规则是"计数器 == 自动重装载值 + 1",可当前计数器已经超过了
④ 只能等计数器计数到 65535,回绕到 0,再重新计数到 50+1 才触发超时
→ 当前周期错乱,完全不满足要求!

场景二(有影子寄存器):同样突然把超时时间从 100ms 改为 50ms。

复制代码
① 用户修改 → 只改预加载寄存器(程序员看见的)
② 影子寄存器保持 100 不动 → 老的计数功能正常进行
③ 当前周期完整跑完(计数到 100+1 触发超时)
④ 超时瞬间 → 硬件自动把预加载寄存器的新值 50 装载到影子寄存器
⑤ 计数器清零,开启新的计数周期(按 50 跑)
→ 整个时序完全正确!

影子寄存器的本质意义:

把"参数修改的时刻"和"参数生效的时刻"解耦。

预加载寄存器本质是影子寄存器的缓存。

可以不用缓存吗?

可以。TIMx_CR1 的 ARPE 位(位7) 控制:

ARPE 行为 风险
0(禁用缓冲) 写入预加载寄存器的值直接传输到影子寄存器,修改立即生效 可能在计数器运行过程中打断当前周期
1(启用缓冲) 新值暂存预加载寄存器,只在更新事件 UEV 时才传到影子寄存器,下一周期生效 无(推荐)

PSC 预分频器也有影子寄存器,但不支持用户使能、默认就是开启的------你直接修改 PSC 即可,当定时器超时更新时,PSC 的影子寄存器也会被同步更新。

最佳实践:推荐使用带缓冲的定时器。

5.2.6 超时事件与中断(触发)

对定时器要有一个认知:定时器的核心功能之一就是"触发"。四类触发行为:

触发类型 说明 状态
触发自己更新 超时后:① 重装 ARR 预加载到影子寄存器(无新配置就继续用老的);② 计数器 CNT 重新清零;③ PSC 预分频器自动更新 Done
触发执行代码 基本定时器连接 NVIC,直接向 CPU 触发中断,CPU 执行定时器中断处理方法 Done
触发拷贝数据 基本定时器连接 DMA,超时时间触发时向 DMA 发起请求进行数据拷贝(定时器只是触发源,协助其他外设搬运) Done
触发其他硬件功能 计数器溢出,推动触发控制器,通过 TRGO 产生脉冲,触发其他硬件工作(最核心的是 DAC) 后面 DAC/ADC 章节讲

中断路径细节:

  • 基础定时器连接 NVIC(嵌套向量中断控制器),超时事件直接向 CPU 触发中断;
  • 中断号:TIM6_IRQn = 54、TIM7_IRQn = 55(向量表地址 0x0000_0118 / 0x0000_011C);
  • 在 HAL 库中,两个定时器共用同一个中断服务函数框架:TIM6_IRQHandler / TIM7_IRQHandler 内部调用 HAL_TIM_IRQHandler,最终回调 HAL_TIM_PeriodElapsedCallback------用户只需要重写这个回调函数即可。

DMA 路径细节:

定时器连接 DMA 只是未来作为触发源,协助其他片上外设进行数据搬迁(例如定时器定时把内存中的数据搬到 DAC 寄存器,实现波形输出);UP = Update,即"时间到了"的信号。

事件与中断(重要辨析):

  • 事件(广义)****:硬件状态的特定变化(如电平翻转、计数溢出),是内部逻辑电路产生的一个脉冲信号,代表"发生了什么"------是客观发生的事实;
  • 中断路径**:CPU 响应某个事件并暂停当前任务去处理它的过程------是 CPU 对特定事件的一种处理方式;**
  • 事件路径(狭义)****:事件脉冲不经过 CPU,直接送往其他硬件模块(触发 DAC/ADC 启动、触发 DMA 搬运数据),实现外设之间的自主协同------是其他硬件对特定事件的一种自主处理方式。

(类比:快递来了是"事件";你自己去取是"中断路径";让你舍友去取是"事件路径"。事件是客观发生的,处理方式可以选。)

5.2.7 使能相关(DIER / SR)

定时器超时后产生事件,可以触发 CPU 中断、DMA、DAC 等各种动作,但不一定每次全部生效------必须按需设置,相关配置寄存器就是 TIMx_DIER(DMA/中断使能寄存器,偏移 0x0C):

复制代码
15  14  13  12  11  10   9   8   7   6   5   4   3   2   1   0
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│       Reserved          │UDE│        Reserved        │     │UIE│
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
                          │rw                              │rw
位 名称 作用
位8 UDE 更新 DMA 请求使能:0 禁止 / 1 使能更新 DMA 请求
位0 UIE 更新中断使能:0 禁止 / 1 使能更新中断

(DIER = Timer DMA/Interrupt Enable Register。)

与事件相关的还有状态寄存器 TIMx_SR(偏移 0x10):

复制代码
15  14  13  12  11  10   9   8   7   6   5   4   3   2   1   0
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│                     Reserved                       │     │UIF│
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
                                                     │rc w0
  • 位 15:1:保留,始终读为 0;
  • 位 0 UIF(Update interrupt flag):更新中断标志,硬件在更新中断时设置该位,由软件清除;
  • 0 = 没有产生更新;1 = 产生了更新中断。下述情况下由硬件设置:
    • 计数器产生上溢/下溢并且 CR1 中 UDIS=0;
    • 如果 CR1 中 URS=0 且 UDIS=0,当使用 EGR 寄存器的 UG 位重新初始化计数器 CNT 时。

也就是说:

只要产生了事件,这个标志位就被设置了,和你是否使能了中断无关。

有的用户喜欢轮询检测这个标志位来确认是否有事件产生(虽然很少这么干),所以它叫"状态寄存器"。

轮询 vs 中断:

方式 原理 适用
轮询 主循环里不断读 SR.UIF,为 1 就处理并软件清零 简单场景、学习验证
中断 DIER.UIE=1,NVIC 使能,超时自动跳中断服务函数 实际工程主流做法
5.2.8 补充话题:两个关键控制寄存器

TIMx_CR1(控制寄存器1,偏移 0x00)------定时器行为的主控**:**

复制代码
15  14  13  12  11  10   9   8   7   6   5   4   3   2   1   0
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│                Reserved   │ARPE│Res│     │OPM│URS│UDIS│CEN│
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
                            │rw  │   │rw  │rw │rw  │rw │rw │
位 名称 作用
位0 CEN (Counter Enable) 计数器使能位 = 定时器"总开关"。 1 启动计数; 0 停止并保持当前值。 默认情况下时钟脉冲并不会让计数器立即开始计数,计数功能本身也要被使能(配置完 PSC 和 ARR 后,最后一步通常就是置 1)
位1 UDIS (Update Disable) 更新禁用位。 0 允许更新(溢出或软件 UG 触发 UEV,影子寄存器被加载); 1 禁用更新(修改的 PSC/ARR 不生效)。 常用于原子性精密配置(见下)
位2 URS (Update Request Source) 更新请求源选择。 0:溢出、软件 UG、从模式触发都会发中断/DMA 请求; 1:只有计数器溢出才发请求(防止写 UG 初始化时意外进中断)。 基础定时器只考虑溢出触发,此位通常设为 1
位3 OPM (One-Pulse Mode) 单脉冲模式。 0 循环计数(溢出后自动归零继续); 1 单次计数(下一次更新事件后硬件自动清 CEN,定时器停止)------一次性闹钟还是周期性闹钟的选择
位7 ARPE 自动重载预装载使能(即"是否使用缓存",前面讲过)

UDIS 的原子性应用场景(重点):

场景:

需要同时更改定时器的 PSC(频率)和 ARR(周期),且必须保证这两个值在同一个计数周期内生效**。**

风险:

如果先写了 PSC,此时定时器刚好溢出产生更新事件 UEV,那么下一周期会变成"新频率 + 旧周期"的错误组合,导致输出波形畸变。

锁定逻辑:

复制代码
① 先把 UDIS 设为 1(关门,禁止更新)
② 从容地写入新的 PSC
③ 从容地写入新的 ARR
④ 再把 UDIS 恢复为 0(开门)

这样,刚才写进去的两个值会等待下一次溢出时整整齐齐地一起生效。

URS 的场景:

当 URS=0 时,用软件写 UG 位也会发出中断/DMA 请求------这会导致"我只想手动刷一下参数,结果误进了一次中断"。

设 URS=1 后,只有计数器溢出才发请求,写 UG 就不会打扰中断逻辑。

基础定时器只需要考虑溢出触发,这个位通常也是 1。

OPM 的场景:

你设定闹钟的时候,不也有一次性闹钟和周期性闹钟?

OPM=1 就是一次性:下一次更新事件(溢出)后,硬件自动将 CEN 位清零,定时器停止工作;OPM=0 是循环:溢出后自动归零重新开始。


TIMx_EGR(事件产生寄存器,偏移 0x14)------定时器的"手动强制按钮" :

复制代码
15  14  13  12  11  10   9   8   7   6   5   4   3   2   1   0
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│                        Reserved                        │UG │
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
                                                        │w
  • 位 0 UG(Update Generation):只写(Write-only)的触发开关,该位由软件设置、由硬件自动清除;
  • 0:无作用;
  • 1:重新初始化定时器的计数器并产生对寄存器的更新(注意:预分频器也被清除,但预分频系数不变)。

写 UG=1 后的三个连带动作:

  1. 计数器归零:当前 CNT 立即复位为 0;
  2. 强制影子寄存器更新:修改过的 PSC/ARR 新值立刻生效,无需等待当前周期结束**;**
  3. 产生信号:若使能了中断(UIE=1),立即置位 UIF 并触发中断。

为什么需要它(问题背景):

STM32 的很多关键寄存器(PSC、ARR)都有影子寄存器机制。

如果在代码里改了频率,定时器必须等当前的这一轮(可能很长)数完了、产生溢出后新频率才会生效。

如果你不在意当前计数完、更在意立即让配置生效,就需要一种"手动让配置生效"的机制------就是 EGR 的 UG 位**。**

常见场景:

初始化时修改了 PSC 等参数,硬件通常要等一个计数周期结束才更新,导致第一次中断往往时间不对(依然使用默认旧参数)。

解决:

在启动定时器(CEN=1)前,手动将 EGR 的 UG 位置 1------强制新参数立即同步到影子寄存器并清零计数器,确保定时器从第一微秒起就按新配置运行。

5.2.9 触发控制器(TRGO / CR2)

如果把定时器比作"闹钟",TRGO(Trigger Output)就是闹钟响时往外拉出的一根"信号线":让定时器在特定时刻(溢出、复位)自动向其他硬件(DAC、ADC 或其他定时器)发送硬件级启动信号,无需 CPU 介入。

  • TRGO 的本质:一条内部连接线,直接把定时器与 ADC、DAC 或其他定时器互联;
  • 逻辑上的通知关系:触发控制器反而是被时基单元通知的------时基单元超时进行事件分发时,其中一路分发给触发控制器,用来控制其他片上外设。

相关寄存器 TIMx_CR2(控制寄存器2,偏移 0x04),只有一组 MMS2:0(主模式选择) 需要设置:

复制代码
15  14  13  12  11  10   9   8   7   6   5   4   3   2   1   0
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│                        Reserved              │ MMS[2:0] │Res│
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
                                               │   rw     │
MMS 模式 说明
000 复位 手动强刷(UG 位)时发送脉冲,用于多定时器中途强行对齐(基础定时器不考虑)
001 使能 只要计数器在跑就输出高电平,充当其他外设启停的同步总开关(不考虑)
010 更新 每次溢出时发送节拍脉冲,驱动 DAC/ADC 全自动定时转换的核心模式------最常用

MMS 的本质:

决定 TRGO 输出信号的生成规则------把哪种内部事件"翻译"成对外信号。

最佳实践:

CR2 最长设置的就是 010。

当时基单元超时的时候,自动让 TRGO 触发脉冲信号,触发下级设备工作,最常见外设就是 DAC。

典型应用:

DAC 波形发生器------定时器定时触发 DAC 转换,DMA 定时搬运采样点数据到 DAC 数据寄存器,三硬件全自动协作,CPU 全程不参与,输出正弦波/三角波。这部分后面学 DAC/ADC 会细说。


6. 总结:基础定时器功能框图(寄存器视角)+ 初始化流程

6.1 完整功能框图

复制代码
APB1 总线 ← RCC_TIMxCLK → 触发控制器/控制逻辑 → CK_PSC → 预分频器 PSC(/N) → CK_CNT → 计数器 CNT
                                                                                          │
                                         自动重装载寄存器 ARR(预加载 → 影子,设定计数值)◄─┘
                                                                                          │ CNT == ARR+1 → 溢出
                                     ┌───────────────── 更新事件 UEV(溢出脉冲)────────────────┐
                                     ▼                ▼                 ▼                ▼                ▼
                              更新自身配置       触发外设 TRGO      数据搬运 DMA        状态标记 SR      申请中断 NVIC
                            (影子寄存器同步)  (CR2 MMS=010→DAC)  (DIER UDE=1)    (置位 UIF)    (DIER UIE=1)

核心机制:时基单元溢出脉冲(Update Event)会同步触发 5 种动作:

  1. 更新自身配置(影子寄存器):将 ARR 和 PSC 的预装载值立即同步到物理执行寄存器,确保下一周期参数生效且不产生频率抖动;
  2. 触发外设(TRGO):根据 CR2 配置(如 MMS=010)将溢出信号映射为 TRGO 脉冲,直接驱动 DAC 转换或 ADC 采样;
  3. 数据搬运(DMA):产生 DMA 请求,全自动从内存将下一个采样点数据传送到 DAC 寄存器,无需 CPU 参与;
  4. 状态标记(SR):在状态寄存器中挂起更新标志位 UIF;
  5. 申请中断(NVIC):若使能了 DIER 中的中断位,则通知 CPU 执行回调函数。

触发 DMA 或中断是可配置的,其他都会自动触发完成。

6.2 基础定时器初始化流程(寄存器级,可背)

复制代码
① 使能时钟:RCC 打开 TIM6/TIM7 时钟(APB1 上)
② 配置分频:写 TIMx_PSC = 分频系数 - 1          ← 定"刻度"(分辨率)
③ 配置周期:写 TIMx_ARR = 计数次数 - 1           ← 定"总数"(超时时间)
④ 决定缓冲:CR1.ARPE = 1(推荐带缓冲)
⑤ 强制生效:写 TIMx_EGR 的 UG = 1               ← 让新参数立即同步到影子寄存器
⑥ 使能中断:DIER.UIE = 1;NVIC 使能 TIM6/TIM7 中断并配优先级
⑦ 启动计时:CR1.CEN = 1                        ← 计数器开始跑
⑧ 处理超时:在 HAL_TIM_PeriodElapsedCallback 里写业务代码(或轮询 SR.UIF)

附录 A:定时器存储器映像

定时器属于片上外设,TIM2~TIM7 挂在 APB1(基址 0x4000 0000 区段):

外设 地址范围
TIM2 0x4000 0000 - 0x4000 03FF
TIM3 0x4000 0400 - 0x4000 07FF
TIM4 0x4000 0800 - 0x4000 0BFF
TIM5 0x4000 0C00 - 0x4000 0FFF
TIM6 0x4000 1000 - 0x4000 13FF
TIM7 0x4000 1400 - 0x4000 17FF
TIM1 0x4001 2C00(挂 APB2)

(寄存器偏移 + 外设基址 = 寄存器绝对地址,如 TIM6->CR1 = 0x4000 1000 + 0x00。)

附录 B:基础定时器寄存器速查表(必背)

寄存器 偏移 复位值 关键位
TIMx_CR1 0x00 0x0000 CEN(0)、UDIS(1)、URS(2)、OPM(3)、ARPE(7)
TIMx_CR2 0x04 0x0000 MMS2:0
TIMx_DIER 0x0C 0x0000 UIE(0)、UDE(8)
TIMx_SR 0x10 0x0000 UIF(0)
TIMx_EGR 0x14 0x0000 UG(0)
TIMx_CNT 0x24 0x0000 CNT15:0
TIMx_PSC 0x28 0x0000 PSC15:0
TIMx_ARR 0x2C 0x0000 ARR15:0

附录 C:定时器超时公式(推导结论)

复制代码
1. CK_CNT = CK_PSC / (PSC + 1)
2. 单个周期时长 = 1 / CK_CNT = (PSC + 1) / CK_PSC
3. 定时器超时时间 = 单个周期时长 × (ARR + 1)
4. 定时器超时时间 = ((PSC + 1) × (ARR + 1)) / CK_PSC

基础定时器 TIM 面试题

一、概念题

Q1. 定时器的本质功能是什么?它是怎么把"时间"变成可比较的"数字"的?时间分辨率由什么决定?

A:

本质功能是设定延时,超时后触发完成某种工作。

通过"固定频率时钟源 + 计数器"实现:时钟源每触发一次脉冲,计数器自动 +1;因为时钟周期固定(如 1000Hz=1ms/次),计数器数字就对应时间(123 → 123ms)。

时间分辨率 = 时钟周期的长度,时钟频率越高分辨率越高(1MHz → 1us)。


Q2. STM32F103ZE 的定时器分哪三类?分别有哪些?基础定时器只有什么功能?

A:

① 基础定时器(TIM6、TIM7):仅基本计时功能和 DAC 转换触发;

② 通用定时器(TIM2~TIM5):基础功能 + 输入捕获、输出比较、PWM 生成、正交编码器输入;

③ 高级定时器(TIM1、TIM8):再增加互补 PWM 与死区控制、紧急刹车、主从模式。

"11 个定时器" = 8 个片上定时器 + SysTick + IWDG + WWDG。


Q3. 软件延时(HAL_Delay)有哪些缺点?为什么需要硬件定时器?

A:

① 占用 CPU:延时期间 CPU 空转;

② 时间不精准:受中断抢占、代码执行时间影响;

③ 无法并发:多任务不同周期无法并行计时。硬件定时器由硬件独立计数,CPU 零负担、超时精准触发、多定时器可并行。

二、时钟题

Q4. TIMxCLK 和 APB1 的关系是什么?为什么有"×2"的设定?

A:

APB1 分频系数 = 1 时,TIMxCLK = APB1;

分频系数 ≠ 1 时,TIMxCLK = 2 × APB1。

原因是对定时器时钟的精度补偿:APB1 总线上挂着 UART/I2C 等慢速外设,总线频率不能太高(≤36MHz),而定时器希望跑在最高频(72MHz)保证计时精度------所以只要分频就自动翻倍。


Q5. 描述 STM32F103 标准配置下的时钟树路径(从晶振到定时器时钟)。

A:

HSE 8MHz → PLL ×9 → SYSCLK 72MHz → AHB(HCLK=72MHz)→ APB1 预分频 /2(PCLK1=36MHz)→ 定时器时钟补偿 ×2 → TIMxCLK=72MHz。

APB1 分频由 RCC_CFGR 的 PPRE12:0 决定。


Q6. TIMxCLK、CK_INT、CK_PSC、CK_CNT 的关系?

A:

对基础定时器,TIMxCLK = CK_INT = CK_PSC(一根导线上的三个名字);

CK_CNT = CK_PSC/(PSC+1) 才是真正驱动计数器的时钟。

三、寄存器与计算题(高频)

Q7. TIMx_PSC、TIMx_CNT、TIMx_ARR 各自的作用、取值范围?为什么分频系数是 PSC+1?

A:

三者都是 16 位寄存器,范围 0,65535。

PSC 调整时间分辨率,实际分频系数 = PSC+1(1,65536),因为 PSC=0 时必须"至少 1 分频",公式不能除以 0;

CNT 被 CK_CNT 驱动向上计数(基础定时器只支持向上计数),量化时间;

ARR 设定未来超时时间,与影子寄存器配合。


Q8. 什么是定时器"溢出"?超时条件到底是什么?"临界溢出"指什么?

A:

溢出 = 计数器超过 ARR 设定值后再计一个脉冲时发生的归零动作。

硬件上 CNT == ARR 不是溢出(最多叫临界溢出),超时条件是 CNT = ARR + 1("凡溢出,必超过")。

设置 ARR=3、1000Hz 时超时时间是 4ms------超时时间由 ARR+1 决定。


Q9. 计算:TIMxCLK=72MHz、PSC=71、ARR=999,求单次超时时间和 1s 内超时次数。

A:

CK_CNT = 72/(71+1) = 1MHz(1us/次);

超时时间 = (999+1)×1us = 1ms;

1s 内超时 1000 次。


Q10. 计算:要求每 500ms 触发一次超时(TIMxCLK=72MHz),给出一种配置。

A:

取 CK_CNT=10kHz(100us/次)→ PSC+1 = 72MHz/10kHz = 7200 → PSC=7199;500ms/100us=5000 次 → ARR=4999。(均 < 65535 合法。)


Q11. 背出定时器超时总公式,并现场推导。

A:

定时器超时时间 = ((PSC+1) × (ARR+1)) / CK_PSC。

推导:CK_CNT = CK_PSC/(PSC+1) → 单周期时长 = 1/CK_CNT = (PSC+1)/CK_PSC → 溢出需 (ARR+1) 次计数 → 超时时间 = 单周期时长 × (ARR+1)。


Q12. 想同时获得高分辨率(2us)和长周期(1s),基础定时器能做到吗?为什么?

A:

不能。2us 分辨率 → PSC+1=144;1s 需要 500000 次计数,ARR 最大只有 65535。基础定时器单靠 16 位 ARR 无法兼顾"高分辨率 + 长周期",需要降分辨率、或用通用定时器级联等方案。

四、影子寄存器题(重点难点)

Q13. 什么是影子寄存器?为什么 ARR 需要它?

A:

ARR 由预加载寄存器(程序员可读写)和影子寄存器(实际生效、用户不可见)组成。写 ARR 实际写的是预加载寄存器,在更新事件 UEV 时才传到影子寄存器,硬件比较的是 CNT 与影子寄存器。

原因:

无影子时中途改 ARR 立即生效,若当前计数器已超过新值,要等回绕一整圈才能触发超时,当前周期错乱;

有影子时当前周期完整执行完、新周期才生效------本质是把"参数修改时刻"和"参数生效时刻"解耦。


Q14. ARPE 位的作用?

A:

自动重载预装载使能(CR1 位7)。

ARPE=0:ARR 无缓冲,修改立即生效(可能打断当前周期);

ARPE=1:ARR 有缓冲,新值在下一个 UEV 才生效,保证当前周期完整。

最佳实践:推荐带缓冲。


Q15. PSC 有影子寄存器吗?和 ARR 的影子有什么区别?

A:

有。但 PSC 的影子寄存器不支持用户使能、默认就是开启的------直接修改 PSC 即可,超时更新时自动同步。ARR 的影子则由 ARPE 位控制是否启用缓冲。

五、事件与中断题

Q16. 事件(广义)、中断、事件路径(狭义)的区别?

A:

事件是硬件状态的客观变化(计数溢出产生的脉冲),代表"发生了什么";

中断是 CPU 响应事件并暂停当前任务去处理(自己取快递);

**事件路径(狭义)**是事件脉冲不经过 CPU、直接送往其他硬件(DMA/DAC/ADC)实现外设自主协同(让舍友取快递)。


Q17. 定时器超时后(溢出脉冲)会同步触发哪些动作?哪些可配置?

A:

五路:

① 更新自身配置(影子寄存器同步 + CNT 清零)------自动;

② TRGO 触发外设(如 DAC,CR2.MMS=010)------自动;

③ DMA 请求搬运(DIER.UDE)------可配置;

④ 置位 SR.UIF 状态标志------自动;

⑤ 申请 NVIC 中断(DIER.UIE)------可配置。


Q18. TIMx_DIER 和 TIMx_SR 里分别有哪些关键位?UIF 有什么特性?

A:

DIER(0x0C):位8 UDE(更新 DMA 请求使能)、位0 UIE(更新中断使能);

SR(0x10):位0 UIF(更新中断标志,硬件置位、软件清除)。

只要产生事件 UIF 就被置位,与是否使能中断无关------所以可以轮询 SR.UIF 判断事件。

六、控制与实战题

Q19. CR1 寄存器中 CEN、UDIS、URS、OPM 分别是什么?

A:

CEN(位0)= 计数器使能"总开关",默认时钟脉冲不会让计数器计数,配置完 PSC/ARR 后最后一步置 1;

UDIS(位1)= 更新禁用,用于原子性配置(先关门→写 PSC/ARR→开门,避免"新频率+旧周期"错配);

URS(位2)= 更新请求源选择,1 表示只有溢出才发请求(防写 UG 意外进中断);

OPM(位3)= 单脉冲模式,0 循环、1 单次(溢出后自动清 CEN)。


Q20. 什么场景下需要用到 UDIS?简述"原子性配置"的步骤和原因。

A:

需要同时修改 PSC 和 ARR 且保证同一周期生效时。

风险:

先写 PSC 时若定时器刚好溢出,下一周期变成"新频率+旧周期"的错配组合。

步骤:

① UDIS=1 关门(禁止更新);

② 写新 PSC;

③ 写新 ARR;

④ UDIS=0 开门------两个值等下一次溢出时整整齐齐一起生效。


Q21. EGR 寄存器的 UG 位是什么?初始化时为什么要先写它?写它有三个什么连带动作?

A:

UG(位0)是只写触发开关,写 1 强制立即产生 UEV:

① 计数器归零;

② 强制影子寄存器更新(PSC/ARR 新值立刻生效);

③ 若 UIE=1 则置位 UIF 并触发中断。

初始化时在 CEN=1 前先写 UG=1,能让新参数从第一微秒起就生效,否则第一次中断会按旧参数运行。


Q22. CR2 的 MMS 是什么?为什么 010(更新)最常用?

A:

MMS(主模式选择,CR2 位 4:6)决定 TRGO 输出信号的生成规则:000 复位(UG 强刷时发脉冲)、001 使能(计数器跑就输出高电平)、010 更新(每次溢出发节拍脉冲)。

010 最常用是因为它让定时器在每次溢出时自动给 DAC/ADC 发触发脉冲,实现全自动定时转换(如定时器+DMA+DAC 三硬件协作输出波形),无需 CPU 参与。


Q23. 口述题:描述基础定时器从配置到产生一次定时中断的完整流程。

A(答题要点):

① RCC 时钟树产生 TIMxCLK(基础定时器 = APB1 ×2,72MHz);

② 配置 PSC 分频得到 CK_CNT(如 PSC=71 → 1MHz);

③ 配置 ARR 设定超时时间(如 999 → 1ms);

④ 若用影子缓冲,配置 CR1.ARPE=1;

⑤ 写 EGR.UG=1 强制新参数立即同步到影子寄存器;

⑥ 配置 DIER.UIE=1 使能更新中断;

⑦ CR1.CEN=1 启动计数器;

⑧ CK_CNT 驱动 CNT 向上计数,CNT 达到 ARR+1 时溢出 → 产生 UEV:影子寄存器重装、CNT 清零、SR.UIF 置位、NVIC 触发中断 → CPU 跳转 TIM6/TIM7 中断服务函数,调用 HAL_TIM_PeriodElapsedCallback,清除标志,循环往复。


Q24. 如果 UIF 标志产生后不软件清除,会发生什么?

A:

UIF 是硬件置位、软件清除。

若中断模式不清理,中断服务函数退出后会再次进入中断(因为中断挂起标志一直有效),导致中断死循环、主程序被饿死。

所以 HAL 库回调里/中断处理中必须读 SR 后写 0 清除 UIF(HAL 库的 HAL_TIM_IRQHandler 会自动 __HAL_TIM_CLEAR_FLAG 处理)。


相关推荐
DS小龙哥2 小时前
从呼吸心率到离床报警:60G雷达睡眠仪全栈开发实录(硬件+MQTT+小程序)DIY一台全功能睡眠监护仪
stm32·单片机·嵌入式硬件·物联网·小程序·智能家居
lhbweilai3 小时前
深挖FreeRTOS内核实现(二)
c语言·汇编·stm32·单片机
黑妹天下第一乖3 小时前
第 05 讲:阿加犀 AidCV 图像处理加速与 OpenCV 一致开发实战
开发语言·图像处理·人工智能·嵌入式硬件·数码相机·opencv·计算机视觉
这个人很懒67673 小时前
【无标题】
单片机·嵌入式硬件
ebiobiz4 小时前
Zig 工具链编译 STM32 开发指南
驱动开发·stm32·嵌入式硬件
国科安芯4 小时前
卫星推进与轨道维持系统中脉冲调制控制器的精确时序与抗辐射可靠性研究
嵌入式硬件·抗辐射·卫星姿轨控·as32x601·在轨服务
小刘max4 小时前
从半导体视角理解 NVM 与 Flash:存储单元、阵列架构与应用差异
mcu
Zwawa4 小时前
反馈:让系统知道自己做得对不对
c语言·单片机·嵌入式
黑妹天下第一乖4 小时前
第 04 讲:阿加犀 AIMO 模型优化平台与 Model Farm 模型广场实战
人工智能·嵌入式硬件·矩阵·架构·iot