news 2026/10/6 12:30:23

TIM定时中断(第一节)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
TIM定时中断(第一节)

目录

  • TIM简介
  • 定时器类型
    • 一句话理解
    • 三档对比(从强到弱)
    • 几个名词翻译一下
    • 跟我们手上那块芯片的关系
    • 实际写代码时怎么选
    • 例 2:PWM 输出 —— 做 LED 呼吸灯
    • 例 3:输入捕获 —— 读红外遥控器的按键值
    • 三个例子串起来看
    • *底层逻辑(选看)
    • 先看整体信号链(一句话)
    • 第一层:时钟源 ——"让谁来数"
    • 第二层:预分频器 PSC——"把心跳放慢"
    • 第三层:计数器 CNT——"真正在数的那个东西"
    • 第四层:自动重装载 ARR——"数到几算一轮"
    • 第五层:更新事件 UEV——"数到头了,该干活了"
    • 第六层:输出比较(PWM 那一路)——"CNT 跟 CCR 对表"
    • 第七层:输入捕获(红外解码那一路)——"外部跳变把 CNT 抓下来"
    • 把整个流程串一遍(以 "1ms 中断一次" 为例)
    • 参考手册时的 "地图"
  • 定时中断基本结构
    • 整体方向:从左往右
    • 第一列:时钟源(谁来给定时器当心跳)
    • 第二列:黄色梯形块(时钟模式选择)
    • 第三列:粉色大框 "时基单元"—— 这才是心脏
    • 第四列:中断输出控制(深青绿色窄条)
    • 第五列:NVIC(最右边)
    • 把整张图连起来读一遍
    • 重难点讲解
  • 预分频时序
    • 先知道这张图在演示什么
    • 从上到下逐行翻译
    • 整张图连起来讲一个故事
    • 以后写代码会遇到的一个坑
    • 图下面那个公式
  • 计数器时序图
    • 先知道这张图在演示什么
    • 从上到下逐行翻译
    • 整张图连起来讲一个瞬间
    • 跟代码对应起来
    • 把两张图串起来看
    • 看代码时的对应关系

TL;DR(快速了解)

定时器本质是一个自动数节拍的计数器:给它一个时钟,它就一格一格往上加,加到设定值就触发中断或翻转引脚。STM32 的定时器按能力分成三档——高级定时器(TIM1/TIM8)最强,支持刹车、死区和互补输出,适合电机控制;通用定时器(TIM2~TIM5)够用,能输出 PWM、测脉宽、读编码器;基本定时器(TIM6/TIM7)最朴素,只会定时到点产生中断。在 STM32F103C8T6 这块芯片上,资源被砍了一刀:只有 TIM1、TIM2、TIM3、TIM4,没有 TIM5~TIM8,也没有基本定时器 TIM6/TIM7,所以做定时中断实验只能用 TIM2~TIM4 来凑。

TIM简介

详细图解参见:新应用

定时器类型

一句话理解

定时器本质就是个"自动数节拍的计数器"—— 给它一个时钟,它就一格一格往上加,加到我们设定的值就 "叮" 一下(触发中断 / 翻转引脚)。不同型号的定时器,计数器的 "手脚多少" 不一样,所以分成了三档。

三档对比(从强到弱)

表格

档位芯片里谁是挂在哪条总线能干什么
高级定时器(最猛)TIM1、TIM8APB2通用定时器的活它全干,还多了刹车、死区、互补输出—— 专门用来控制电机(比如三相无刷电机),能安全地驱动大功率器件
通用定时器(够用)TIM2~TIM5APB1能数外部脉冲、测脉宽、输出 PWM、读编码器、几个定时器之间能联动
基本定时器(最朴素)TIM6、TIM7APB1就只会一件事:定时到点产生中断,顺便能触发 DAC 输出电压波形

几个名词翻译一下

  • 总线(APB1 / APB2):可以理解成两条 "数据马路"。APB2 跑得快一点,所以高级定时器挂上面;APB1 慢一点但定时器大多挂这儿。不用纠结,知道 "挂在哪" 只是硬件连接,写代码时 CubeMX 会自动处理。
  • 重复计数器:高级定时器才有的,数 N 次才真正进一次中断,减少 CPU 负担。
  • 死区生成 + 互补输出:两个引脚输出反相的方波,中间留一点点 "都关断" 的间隙,防止 H 桥上下管直通烧管子。这是电机控制的标配。
  • 输入捕获:外部信号跳变时,自动把当前计数值锁下来 → 可以测频率、测脉宽。
  • 输出比较:计数值到某个数时,引脚自动翻转 / 置位 → 可以输出 PWM、做精确延时。
  • 编码器接口:直接接旋转编码器的 A/B 两相,硬件自动算方向和步数,不用你软件去数。

跟我们手上那块芯片的关系

在STM32F103C8T6 中,它引脚少、封装小,所以片上资源被砍了一刀:

  • ✅ 有:TIM1(高级)、TIM2、TIM3、TIM4(通用)
  • ❌ 没有:TIM5~TIM8

这意味着:这块板子上没有基本定时器 TIM6/TIM7,也没有第二个高级定时器 TIM8。所以平时做 "定时中断" 实验,只能用 TIM2~TIM4;想做最简单的 "纯定时",也得拿通用定时器来凑。

实际写代码时怎么选

翻译成代码心智:

这就是 "基本定时器" 该干的活,但因为F103上没有 TIM6/TIM7,所以拿通用定时器 TIM3 来干。


例 2:PWM 输出 —— 做 LED 呼吸灯

用谁:TIM2_CH1(通用定时器的一个通道,对应引脚 PA0)

事:LED 亮度从暗到亮再到暗,循环往复,不要 CPU 参与。

定时器内部发生了啥:

翻译成代码心智:

这就是 "输出比较" 功能,通用定时器 TIM2~4 都能干。蜂鸣器发出不同音调、舵机转到不同角度,原理一模一样。


例 3:输入捕获 —— 读红外遥控器的按键值

用谁:TIM4_CH1(通用定时器的输入捕获通道,对应 PB6)

事:红外接收头把遥控信号转成一串高低电平,想知道每一段高 / 低电平持续了多久,从而解出按了哪个键。

定时器内部发生了啥:

这就是 "输入捕获" 功能——CPU 全程不用盯着引脚,硬件帮你把时间戳记下来,精度比你软件去读 GPIO 高得多。


三个例子串起来看

表格

例子用到定时器的哪个能力对应F103上的
LED 闪烁定时中断(基本功能)TIM2/3/4 都行
呼吸灯 / PWM输出比较TIM2/3/4 的通道
红外解码输入捕获TIM2/3/4 的通道
电机控制(你没做)死区 + 互补输出TIM1

核心记忆点:

现在做的实验(LED、按键、蜂鸣器、OLED),99% 都用 TIM2/3/4 就够了,TIM1 暂时可以放一边。等以后想做循迹小车、平衡车,再回头啃 TIM1 的互补输出和刹车功能也不迟。

*底层逻辑(选看)

  • 只是想 "每 1ms 进一次中断点灯 / 扫描按键" → 随便挑 TIM2/3/4 都行
  • 想输出 PWM 调光 / 控制舵机 / 做呼吸灯 → 用 TIM2~TIM4 的通道
  • 想读红外遥控器脉冲、测方波频率 → 用 TIM2~TIM4 的输入捕获
  • 想做无刷电机 FOC、驱动半桥 → 才需要 TIM1(你这块板子有,够用)
  • 想要个 "免费" 的定时器顺带驱动 DAC → F103这块板子没有 TIM6/7,得用 TIM2~4 模拟

    例 1:最朴素的 "定时中断"—— 让 LED 每 500ms 闪一次

    定时器:TIM3(通用定时器,F103有)

    需要做的事:主程序该干嘛干嘛,每过 500ms 自动进一次中断函数,把 LED 翻转一下。

    定时器内部流程变化:

  • 定时器有个时钟输入(比如 72MHz),经过一个 "分频器" 把它降慢。
  • 假设我们想要 1ms 跳一格,就分频 72 倍 → 每 1ms 计数器加 1。
  • 计数器从 0 开始往上加,加到 "自动重装载值" 等于 500 时,就触发一次中断。
  • 进中断函数你把 LED 翻转,计数器自动清零,重新开始数。
  • 分频系数 PSC = 71 → 72MHz / (71+1) = 1MHz
  • 自动重装载 ARR = 500 → 数 500 次就是 500ms
  • 中断里写:HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
  • 还是先分频、再数到 ARR,跟例 1 一样。
  • 但这次不触发中断了,而是把 "当前计数值" 跟你设的一个 "比较值"(CCR)做比较:
    • 计数值 < CCR → 引脚输出高电平
    • 计数值 ≥ CCR → 引脚输出低电平
  • 这样一个周期里,高电平占多少比例,就由 CCR 决定 —— 这就是占空比。
  • 你只要在主循环里慢慢改 CCR 的值(比如从 0 加到 ARR,再减到 0),LED 就呼吸起来了。
  • 定时器周期还是定死的(比如 1kHz)
  • CCR = 0→ LED 全灭
  • CCR = ARR/2→ 一半亮度
  • CCR = ARR→ LED 全亮
  • CPU 只需要偶尔改一下 CCR,波形是硬件自动生成的。
  • 定时器照样在那儿数格子(比如 1μs 一格)。
  • 关键区别:不是 CPU 去读时间,而是当引脚上出现 "下降沿"(高变低)的瞬间,硬件自动把当前计数值锁存到一个寄存器里,同时触发一个中断。
  • 你在中断里把这个值读出来,跟上次锁存的值一减,就是这段电平持续了多少微秒。
  • 红外协议(比如 NEC)就是靠 "高电平 9ms + 低电平 4.5ms" 这种特定脉宽组合来表示 0 和 1 的,你把这些脉宽串起来就解出按键码了。
  • 计数器 + 分频器 是所有定时器的 "地基",三档都有。
  • 基本定时器:只会 "数到顶就喊一声"。
  • 通用定时器:还会 "盯着引脚跳变记时间" 和 "自动按规则改引脚电平"。
  • 高级定时器:在通用基础上再加 "安全刹车" 和 "成对输出反相波",为电机这种危险场合准备。

这次把定时器 "剖开" 看内部 —— 信号是怎么从一个时钟晶振,一步步变成引脚翻转和中断的。你写配置文件的时候,每一个参数背后都对应着里面某一个模块。


先看整体信号链(一句话)

时钟源 → 分频器 (PSC) → 计数器 (CNT) → 跟 ARR 比 → 触发更新事件 → 跟 CCR 比 → 改输出引脚

所有定时器都是这条流水线,区别只在于后面挂了多少个 "比较 / 捕获通道"。


第一层:时钟源 ——"让谁来数"

定时器得有个心跳才能数,心跳来源有四种:

内部时钟 (CK_INT):就是芯片主时钟(你板子上 72MHz),最常用。点灯、做 PWM 都用它。外部输入引脚 (TIx):从某个 GPIO 进来的方波,定时器跟着外部方波数。外部触发输入 (ETR):专门一个引脚,用来数高速外部脉冲。级联其他定时器:一个定时器溢出后,给下一个当心跳。比如 TIM1 溢出一次,TIM2 才加 1—— 两个定时器拼起来变成一个 "大计数器"。

代码选 "Clock Source = Internal Clock",选的就是这一层。


第二层:预分频器 PSC——"把心跳放慢"

晶振 72MHz 太快了,你根本数不了这么细。PSC 就是一个"除法器":

本质:PSC 是个 16 位寄存器,写啥值,时钟就被切成 (PSC+1) 份。

CubeMX 里那个 "Prescaler" 框,填的就是 PSC。


第三层:计数器 CNT——"真正在数的那个东西"

CNT 是一个寄存器,里面存着当前计数值,每来一个经过 PSC 分频后的脉冲,它就 + 1。

工作模式有三种(你现在只需要知道前两种):

关键点:CNT 是硬件自动加的,CPU 完全不用管。你任何时刻去读它,都能拿到 "现在数到几了"—— 这就是定时器的灵魂:不占用 CPU,硬件自己跑。


第四层:自动重装载 ARR——"数到几算一轮"

ARR 也是个寄存器,规定了 "数到几就清零重新开始"。

ARR 还有个影子寄存器的概念(暂时不用深究):你写 ARR 的时候,不是立刻生效,而是等下一次 "溢出" 才换上去,防止改一半导致时序错乱。


第五层:更新事件 UEV——"数到头了,该干活了"

当 CNT 加到等于 ARR 的那一刻,硬件自动做三件事:

这就是你例 1 里 "每 500ms 进一次中断" 的底层真相:不是定时器喊你,是它把 UIF 置 1,中断控制器看到后通知 CPU。

你代码里那句if(__HAL_TIM_GET_FLAG(&htim3, TIM_FLAG_UPDATE)),读的就是这个 UIF。


第六层:输出比较(PWM 那一路)——"CNT 跟 CCR 对表"

这是另一条并行的分支,跟上面的 "溢出" 无关,每个通道(CH1/CH2/CH3/CH4)独立一套:

每个通道都有一个CCR 寄存器(捕获比较寄存器),你往里写一个数值。

定时器内部有个比较器,每个时钟周期都在做一件事:

整个 ARR 周期里,高电平占 CCR/ARR 这么长一段 —— 这就是占空比。

PWM 模式下流程是这样的:

  • 你写PSC = 71,它就把时钟频率除以 (71+1) = 72。
  • 72MHz / 72 =1MHz,也就是每1 微秒计数器加 1。
  • 你写PSC = 999,就变成 72MHz/1000 = 72kHz,每 13.89μs 加 1。
  • 向上计数:0 → ARR → 清 0 → 再 0 → ARR…… 循环(最常用,例 1 例 2 都是这种)。
  • 向下计数:ARR → 0 → 重装 ARR……
  • 中央对齐:0 → ARR → 0,上下走,电机控制才用。
  • PSC 决定 "每格多少时间",ARR 决定 "一共多少格"。
  • 一次完整定时 = (PSC+1) × (ARR+1) / 时钟频率。
  • 比如 PSC=71, ARR=999:每格 1μs,数 1000 格 = 1ms,这就是 1ms 中断一次的由来。
  • CNT 清 0(向上计数模式下)。
  • 置一个标志位 UIF(在状态寄存器 SR 里)—— 告诉全世界:"我溢出了"。
  • 如果中断使能位 UIE 开着,就向 NVIC 申请一次中断,CPU 跳进你的TIM3_IRQHandler()。
  • CNT < CCR→ 引脚输出电平 A(比如高)
  • CNT ≥ CCR→ 引脚输出电平 B(比如低)
  • CNT 从 0 开始往上加。
  • CNT < CCR:引脚一直是高。
  • CNT 刚到 CCR:引脚 "啪" 翻成低。
  • CNT 加到 ARR:溢出、CNT 清 0。
  • CNT 从 0 重新开始:引脚又变回高。
  • 周而复始。

第七层:输入捕获(红外解码那一路)——"外部跳变把 CNT 抓下来"

这是反过来的方向:信号从引脚进来,定时器帮你记时间。

引脚跳变时内部流程:

关键:从 "引脚跳变" 到 "CNT 被锁进 CCR",是纯硬件完成的,中间没有任何 CPU 指令。哪怕你 CPU 正被别的中断占着,这个时间戳也不会丢 —— 只是你晚一点去读而已。这就是为什么输入捕获比你在主循环里while(GPIO_Read() == 0);这种软件计时要准得多。


把整个流程串一遍(以 "1ms 中断一次" 为例)

假设你配的是 TIM3,72MHz 时钟,PSC=71,ARR=999:

72MHz晶振 ↓ PSC分频器:72MHz / 72 = 1MHz(1μs一格) ↓ CNT每个μs+1:0,1,2,3,...,999 ↓ CNT加到999(=ARR)的那一刻 ├→ CNT自动清0,下一个μs重新从0开始 ├→ UIF标志置1 └→ 如果UIE开了,CPU跳进 TIM3_IRQHandler() ↓ 你写:翻转LED、读按键、扫描OLED……

如果同时你又开了 PWM 通道:

CNT在数:0,1,2,...,999 ↓ 每个μs跟CCR比一下 CNT < CCR=500 → PA0输出高 CNT ≥ 500 → PA0输出低 ↓ ARR到 → CNT清0 → 下一轮 ↓ PA0上自动冒出一串方波,占空比 500/1000 = 50%

参考手册时的 "地图"

STM32 参考手册里定时器那一章,本质就是把上面每个模块单独讲了一遍:

表格

你以后会看到的寄存器对应我上面讲的哪一层
PSC第二层 分频器
CNT第三层 计数器当前值
ARR第四层 数到几清零
CCR1/2/3/4第六 / 七层 比较值 / 抓到的时间戳
DIER(中断使能)第五 / 七层 要不要通知 CPU
SR(状态)第五 / 七层 UIF/CCxIF 标志位
CR1(控制)启动 / 停止、计数方向

你写 HAL 库代码的时候,HAL_TIM_Base_Start()干的事就是把 CR1 里的 "启动位" 拉高,从那一刻起 CNT 才开始动。在此之前,PSC、ARR、CCR 这些值你都可以慢慢配好,定时器一动不动等你发令。


一句话总结:定时器就是个全自动节拍器—— 你提前告诉它 "每格多久、数几格、到点干什么",剩下的事它硬件自己跑,CPU 该干嘛干嘛,到点它主动敲你门。这就是为什么单片机做 "同时干很多事" 特别顺手:一个定时器干一件事,几个定时器一起跑,互不打架。

呼吸灯效果的真相:主循环里每隔几毫秒改一下 CCR 的值(0→ARR→0),硬件就自动把 PWM 占空比改了,引脚电平翻转全程是定时器自己干的,CPU 一秒钟都没参与。

CubeMX 里 PWM 模式选 "PWM mode 1",配的就是这个比较器的翻转规则。


定时中断基本结构

  • 引脚上出现下降沿(或上升沿)。
  • 先经过一个滤波器(可以设几个周期延迟,防止毛刺误触发)。
  • 边沿检测器确认 "真的跳变了"。
  • 硬件自动把此刻的 CNT 值,复制到 CCR 寄存器里—— 这个动作叫 "捕获"。
  • 同时置一个捕获标志 CCxIF,如果开了中断,就跳进中断函数。
  • 你在中断里读 CCR 的值,跟上一次读的减一下,就是这段电平持续了多少个 "刻度"。

整体方向:从左往右

[左边:选心跳] → [中间粉色:真正在数的核心] → [右边:把结果通知CPU]

第一列:时钟源(谁来给定时器当心跳)

最左边这一排绿色 / 红色方块,都是 "候选心跳源",定时器从中挑一个用:

表格

图上的方块大白话对应之前讲的
RCC 内部时钟(红)芯片自己的 72MHz 晶振,最常用时钟源第 1 种
ETR 外部时钟(绿,接 GPIO)从一个专门引脚喂进来的外部方波时钟源第 3 种
ITRx 其他定时器(绿)别的定时器溢出一次,给我当一次心跳时钟源第 4 种(级联)
TIx 捕获通道(绿,接 GPIO)从捕获引脚进来的方波当心跳时钟源第 2 种

中间那个黄色块(梯形)就是一个多路开关—— 你写代码选哪种时钟源,就把哪一路信号接通到右边去。我们用得最多的是最上面那个红块(内部时钟),其他几路平时基本不碰。

小细节:TIx 那一路箭头同时也向右指向 "时基单元",意思是这个引脚既可以当心跳源,也能当输入捕获用 —— 一个引脚两种用途,由你配置决定。


第二列:黄色梯形块(时钟模式选择)

上面已经说了,它就是个 "多路选择开关",图上列了四种模式:

内部时钟模式 外部时钟模式 2(走 ETR 进来的) 外部时钟模式 1(走 TIx 进来的) 编码器模式(TI1+TI2 两相一起进,专门给旋转编码器用)

你不用记这些名字,知道它就是个二选一 / 多选一的开关就行。


第三列:粉色大框 "时基单元"—— 这才是心脏

这个粉色框圈起来的,就是我上次说的 "计数器 + 分频器 + ARR" 三件套,也是图里最重要的部分。

按信号流:

粉色框内部信号流:

时钟进来 → PSC砍慢 → CNT往上加 → CNT跟ARR比 ↓ 加到ARR那一刻 → 溢出 → CNT清0重来

第四列:中断输出控制(深青绿色窄条)

CNT 溢出的那一刻,除了清 0 重来,还会往外发一个 "信号"。这个信号先经过 "中断输出控制" 这个门 ——你这个门开着,它才允许出去;门关着(没开中断使能),信号就被拦下来。

对应我之前讲的:

以 "TIM3 每 1ms 进一次中断" 为例:

RCC内部时钟(72MHz) ↓ [黄色选择器] 选了"内部时钟模式" ↓ PSC预分频器:72MHz / 72 = 1MHz(1μs一格) ↓ CNT计数器:每μs+1 → 0,1,2,...,999 ↓ CNT 跟 ARR(=999) 比 → 加到999的瞬间 ├→ CNT清0,重新从0开始 └→ 往外发"溢出"信号 ↓ [中断输出控制](这个门开着) ↓ NVIC ↓ CPU跳进你写的 TIM3_IRQHandler()

底下那个 "运行控制" 方块,就是你调用HAL_TIM_Base_Start_IT(&htim3)的那一下 —— 把总开关拉上去,整条流水线才开始跑。


重难点详解

这张图你以后翻参考书时,先看粉色框—— 那是定时器的本体;左右两边都是 "入口" 和 "出口",是配角。

这个 "中断输出控制" 方块,就是那个 "门"。


第五列:NVIC(最右边)

NVIC 是芯片内部的 "中断总管"—— 所有外设(定时器、串口、外部引脚……)的中断申请都先汇到这里,它按优先级决定先处理谁、后处理谁,然后叫 CPU 跳进去执行对应的中断函数。

在代码里看到的 "NVIC 配置中断优先级",就是在管这个最右边的方块。


把整张图连起来读一遍

  • PSC 预分频器(左下橙色方块)
    • 左边进来的时钟,先经过它。
    • 它把时钟频率砍慢(比如 72MHz 砍成 1MHz)。
    • 就是之前讲的第二层。
  • CNT 计数器(右下橙色方块)
    • PSC 出来的每一下,CNT 就 + 1。
    • 它是核心 —— 硬件自动往上加。
    • 就是之前讲的第三层。
  • ARR 自动重装载器(右上橙色方块)
    • 跟 CNT 之间有个双向箭头。
    • 意思是:CNT 不停地拿自己跟 ARR 比,一旦加到等于 ARR,就触发 "溢出",然后 CNT 被 ARR 的值重新装填、清 0 重来。
    • 双向箭头表示 "两个寄存器互相影响"。
    • 就是之前讲的第四层。
  • 运行控制(最底下橙色方块,箭头向上指向 CNT)
    • 这是个总开关。你调用HAL_TIM_Base_Start()的时候,就是在抬这个开关 —— 从这一刻起 CNT 才开始动,之前 PSC、ARR 的值都配好了但 CNT 一动不动。
  • UIF 标志位被置上(在 SR 寄存器里)。
  • 中断使能位 UIE 开着,才真的往 CPU 递申请。
  • "为什么 TIM1 在 APB2、TIM2 在 APB1?"—— 图上没画出来,但 RCC 那个红块内部就分了两路总线,高级定时器走 APB2 那根 "快车道",通用和基本定时器走 APB1 那根。
  • "PSC、ARR、CNT 到底什么关系?"—— 粉色框里三个方块一目了然:PSC 管快慢、CNT 在数、ARR 管数到几停。
  • "外部时钟、编码器模式什么时候用?"—— 左边那几个绿块就是为这俩场景留的入口,你现在用不到,知道有这东西就行。

以 "TIM3 每 1ms 进一次中断" 为例:

RCC内部时钟(72MHz) ↓ [黄色选择器] 选了"内部时钟模式" ↓ PSC预分频器:72MHz / 72 = 1MHz(1μs一格) ↓ CNT计数器:每μs+1 → 0,1,2,...,999 ↓ CNT 跟 ARR(=999) 比 → 加到999的瞬间 ├→ CNT清0,重新从0开始 └→ 往外发"溢出"信号 ↓ [中断输出控制](这个门开着) ↓ NVIC ↓ CPU跳进你写的 TIM3_IRQHandler()

底下那个 "运行控制" 方块,就是你调用HAL_TIM_Base_Start_IT(&htim3)的那一下 —— 把总开关拉上去,整条流水线才开始跑。


重难点讲解

"为什么 TIM1 在 APB2、TIM2 在 APB1?"—— 图上没画出来,但 RCC 那个红块内部就分了两路总线,高级定时器走 APB2 那根 "快车道",通用和基本定时器走 APB1 那根。"PSC、ARR、CNT 到底什么关系?"—— 粉色框里三个方块一目了然:PSC 管快慢、CNT 在数、ARR 管数到几停。

这张图先看粉色框—— 那是定时器的本体;左右两边都是 "入口" 和 "出口"


预分频时序

先知道这张图在演示什么

  • 初始状态:PSC 寄存器里写的是 0 → 分频系数 = 0+1 = 1 → 不分频,CK_CNT = CK_PSC。
  • 中途发生一件事:在代码里往 TIMx_PSC 寄存器写了个新值 1。
  • 结果:分频系数变成 2 → CK_CENT 频率减半。

这张图就是把 "从写入新值到真正生效" 这一瞬间,硬件内部每个信号的变化都画出来了。


从上到下逐行翻译

第 1 行:CK_PSC

预分频器的输入时钟,就是那个连续跳变的方波。它一直在跑,不管你配什么,它都不会停。

第 2 行:CNT_EN

计数器使能信号。一开始是低(没启动),中间拉高一跳 —— 这就是你调用HAL_TIM_Base_Start()的那一下。从这一刻起,后面所有信号才开始动。

第 3 行:定时器时钟 = CK_CNT

真正喂给 CNT 计数器的时钟。

  • 前半段(PSC 还没改的时候):跟 CK_PSC 一模一样,一个跟着一个跳。
  • 后半段(新 PSC 生效后):频率变成一半——CK_PSC 跳两下,CK_CNT 才跳一下。

这就是 "分频" 的肉眼可见效果。

第 4 行:计数器寄存器(CNT 的值)

每个 CK_CNT 脉冲来一下,CNT 就 + 1。像上面标的:

F7 → F8 → F9 → FA → FB → FC → 00 → 01 → 02 → 03...

这是十六进制数。FC 加到 FF 再溢出就变 00—— 图上正好在 00 那里冒了一个 UEV 脉冲。

注意一个细节:

  • 前半段(CK_CENT 快的时候):F7,F8,F9,FA,FB,FC 这些数挤得很密,每跳一格 CNT 就 + 1。
  • 后半段(CK_CENT 慢了之后):00,01,02,03 之间间隔变宽了 —— 因为现在每来一个 CK_CENT 脉冲要等两个 CK_PSC。

第 5 行:更新事件 UEV

CNT 从 FC 溢出清 0 到 00 的那一瞬间,"啪" 冒出来一个窄脉冲。这就是之前讲的 "溢出事件"—— 它是个分水岭。

第 6 行:预分频控制寄存器

这就是手册里的TIMx_PSC 寄存器本体。

  • 前半段写的是 0。
  • 中间箭头指的地方变成 1—— 这就是你代码里写htim.Instance->PSC = 1;的动作。

第 7 行:在 TIMx_PSC 中写入新数值

这一行其实就是第 6 行那个箭头的注释,告诉你 "这里是 CPU 写寄存器的瞬间"。

第 8 行:预分频缓冲器(影子寄存器)

关键来了!

第 6 行 "预分频控制寄存器" 在箭头那一刻立刻就变成 1 了,但这一行的 "预分频缓冲器"并没有跟着立刻变—— 它还停在 0,一直等到 UEV 脉冲那个时刻,才 "啪" 地变成 1。

这就是我上次提到的影子寄存器(缓冲器)机制:

  • 你写 PSC 寄存器,写进去的是 "前台值"。
  • 真正起作用的是 "后台影子值"。
  • 影子值不是立刻换,而是等到下一次 UEV(更新事件)才把新值搬上来。

为什么要这样设计?防止你改 PSC 改到一半(前一个周期用老分频、后一个周期用新分频),导致时序错乱。STM32 故意让你 "写了不算,等下一个整周期才生效"。

第 9 行:预分频计数器

这是个藏在 PSC 内部的小计数器,范围是 0 到 PSC:

  • PSC=0 的时候:它只能数到 0 → 每来一个 CK_PSC 就溢出一次 → CK_CENT 跟 CK_PSC 同频。
  • PSC=1 的时候:它从 0 数到 1,数满 2 次才溢出一次 → CK_CENT 频率被砍一半。

图上后半段你能看到它在 0,1,0,1,0,1 之间来回跳 —— 这就是 "数到 1 就翻回 0,每两个 CK_PSC 产生一个 CK_CENT"。


整张图连起来讲一个故事

按时间顺序:

  1. 一开始:PSC=0,预分频系数 = 1。CNT_EN 拉高后,CNT 开始跟着 CK_PSC 飞快地数:F7,F8,F9...
  2. 中间某一刻:你代码里往 PSC 写了个新值 1。PSC 寄存器立刻变成 1,但缓冲器还是 0,所以分频行为没变,CNT 继续飞快地数。
  3. CNT 溢出:从 FC 加到 00,冒了一个 UEV 脉冲。
  4. UEV 那一瞬:缓冲器才把新值 1 搬上去,预分频计数器切换成 "数到 1 才溢出" 模式。
  5. 从此刻开始:CK_CENT 频率减半,CNT 变慢:01,02,03... 之间间隔变成原来的两倍。

以后写代码会遇到的一个坑

这个影子寄存器机制带来一个实际后果:

你运行中改 PSC/ARR 的值,不会立刻生效,要等下一次 UEV。

比如你定时器正在跑,你忽然写:

htim.Instance->PSC = 100;

这一瞬间 CNT 还在用老分频系数继续数,直到下一次溢出清 0 的那一刻才换新参数。

如果你想让它立刻生效,得手动触发一次更新事件:

htim.Instance->PSC = 100; SET_BIT(htim.Instance->EGR, TIM_EGR_UG); // 手动产生UEV,立刻刷新影子寄存器

但日常开发里99% 的情况你不需要这么干—— 都是在初始化时把 PSC/ARR 配好,启动后就不动了。


图下面那个公式

CK_CNT = CK_PSC / (PSC + 1)

这就是核心公式,记住这个比记图有用:

  • PSC=0 → 分频 1 倍 → 72MHz 直跑
  • PSC=71 → 分频 72 倍 → 1MHz(你最常用)
  • PSC=999 → 分频 1000 倍 → 72kHz

为什么是 PSC+1 而不是 PSC?因为 PSC 写 0 也得有意义 —— 写 0 就表示 "别分频",硬件内部算的时候自动给你 + 1。这是所有 STM32 寄存器的统一设计:写 0 代表 "1 倍"。


一句话总结这张图:PSC 是个有 "延迟生效开关" 的分频器 —— 你写下新分频值,它先记着不动,等定时器整周期走完一圈(UEV)才真正切换,避免半路改参数导致时序错乱。


计数器时序图

这张图比上一张简单,讲的是CNT 数到顶溢出那一瞬间,所有信号怎么配合。图标题写了 "分频因子为 2",就是说 PSC 已经配好了,CK_CENT 是 CK_INT 的一半频率。


先知道这张图在演示什么

  • PSC 已经固定(分频因子 = 2),CK_CENT 稳定跑着。
  • CNT 一路从 0 往上数,数到 ARR 后溢出。
  • 图上截到的瞬间是:CNT 从 0x0036 溢出回 0x0000 那一刻。
  • 也就是说,这张图里ARR = 0x0036(十进制 54),CNT 要从 0 数到 55 才算一轮。

从上到下逐行翻译

第 1 行:CK_INT

芯片内部时钟,一直在跳,跟你无关。

第 2 行:CNT_EN

计数器使能,拉高之后 CNT 才开始数。图上这个上升沿在很左边,说明早就启动了。

第 3 行:CK_CNT(定时器时钟)

经过 PSC 分频后的时钟。注意它跟 CK_INT 的关系:

  • CK_INT 跳一下,CK_CENT 不一定跳。
  • 因为分频因子 = 2,所以 CK_INT 跳两下,CK_CENT 才跳一下。
  • 你看方波的稀疏程度就能看出来。

第 4 行:计数器寄存器(CNT 的值)

每个 CK_CNT 脉冲来一下,CNT 就 + 1:

0034 → 0035 → 0036 → 0000 → 0001 → 0002 → 0003

重点看那个 "跳变点":从 0036 下一个 CK_CNT 来了,按理说应该是 0037,但它直接变成了0000—— 这就是溢出。

因为 ARR=0x0036,CNT 数到等于 ARR 之后,下一个脉冲就触发溢出,自动从 0 重新开始。

第 5 行:计数器溢出

CNT 刚从 0036 跳到 0000 的那一瞬间,"啪" 冒一个窄脉冲。这个脉冲叫 "溢出"。

第 6 行:更新事件(UEV)

跟 "计数器溢出" 几乎同时冒出来。UEV 就是 "溢出事件" 的正式名字 —— 硬件自动做这几件事:

  • CNT 清 0(刚才看到的 0000)。
  • PSC/ARR 的影子寄存器在这一刻刷新(就是上一张图讲的那个机制)。
  • 往右边发一个信号。

第 7 行:更新中断标志(UIF)

UEV 那一瞬,硬件自动把 SR 寄存器里的 UIF 位置 1。这就是 "通知 CPU" 的那张纸条:

  • 如果你开了更新中断(DIER 寄存器里 UIE=1),NVIC 就叫 CPU 跳进中断函数。
  • 如果你没开中断,UIF 就那么静静挂着,你可以随时去读它 —— 比如你不用中断,而是主循环里if(htim.Instance->SR & TIM_SR_UIF)来轮询。
  • 注意 UIF 一旦置 1 就不会自己清 0,必须你软件写 0 把它清掉,否则下次进中断又会重复触发。

整张图连起来讲一个瞬间

按时间顺序看那个跳变点:

  1. CNT=0x0036,CK_CENT 来一个脉冲。
  2. 硬件判断:CNT+1 = 0x0037 > ARR=0x0036 → 触发溢出。
  3. 三件事同时发生:
    • CNT 被清成 0x0000。
    • "计数器溢出" 脉冲冒一下。
    • UEV 脉冲冒一下,UIF 标志位置 1。
  4. 如果中断开着,CPU 跳进TIMx_IRQHandler()。
  5. 你在中断里做两件事:
    • 该干嘛干嘛(翻转 LED、读传感器……)。
    • 手动把 UIF 写 0 清掉(HAL 库的HAL_TIM_IRQHandler()会帮你做)。
  6. 下一个 CK_CENT 来,CNT=0x0001,继续数。

跟代码对应起来

假设你要 "1ms 进一次中断",这块板子 72MHz:

表格

你想配公式选值
每格多久?CK_CENT = CK_INT / (PSC+1)想要 1μs 一格 → PSC=71
数几格算一轮?溢出频率 = CK_CENT / (ARR+1)想要 1ms 一次 → ARR=999

代入图上的公式:

CK_CNT_OV = CK_CNT / (ARR+1) = CK_PSC / (PSC+1) / (ARR+1) = 72MHz / 72 / 1000 = 1000 Hz = 每1ms溢出一次

这就是你所有定时器中断实验的数学源头。


把两张图串起来看

你现在应该把上一张图和这张图合起来理解:

表格

上一张图(图 99)这一张图(图 102)
主角PSC 预分频器CNT 计数器
演示什么改 PSC 参数时,分频系数怎么切换CNT 数到 ARR 时,怎么溢出清 0
关键机制影子寄存器延迟生效溢出→UEV→UIF→中断
触发分水岭UEV 脉冲UEV 脉冲(同一个东西)

两张图里都出现了 UEV—— 它是整个定时器的 "节拍点"。所有跨周期的事(改 PSC、改 ARR、重新装 CNT),都等 UEV 这一刻才生效,保证永远不会半路乱。


看代码时的对应关系

// 这些你在 CubeMX 生成的 MX_TIM3_Init() 里都能看到 htim.Init.Prescaler = 71; // PSC:决定每格多久 htim.Init.CounterMode = TIM_COUNTERMODE_UP; // 向上数 htim.Init.Period = 999; // ARR:决定数到几 HAL_TIM_Base_Start_IT(&htim3); // 拉高CNT_EN,开始数

然后中断函数:

void TIM3_IRQHandler(void) { HAL_TIM_IRQHandler(&htim3); // 它自动读SR、判断UIF、清UIF、调回调 } void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM3) { // 这里就是你想让"每1ms干一次"的事 HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13); } }

整张图讲的就是这几行代码背后的硬件故事:CNT 在数→到顶→UIF 置 1→CPU 进中断→HAL 库帮你清 UIF→你的回调函数被调用。


一句话总结:定时器就是个 "数到顶就举手" 的计数器 ——PSC 决定每格多长,ARR 决定数几格,数满就冒 UEV、置 UIF、叫 CPU。你写代码时填 PSC 和 ARR 两个数字,硬件就自动按这个节奏无限循环跑下去。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/6 12:23:01

鸣潮后台自动战斗完全指南:ok-ww 一键日常与 4C 声骸刷取配置

鸣潮后台自动战斗完全指南&#xff1a;ok-ww 一键日常与 4C 声骸刷取配置 【免费下载链接】ok-wuthering-waves 鸣潮 后台自动战斗 自动刷声骸 一键日常 Automation for Wuthering Waves 项目地址: https://gitcode.com/GitHub_Trending/ok/ok-wuthering-waves 深夜想上…

作者头像 李华
网站建设 2026/10/6 12:07:00

CLIP改进工作串讲(上)

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/6 12:04:39

仪表读数识别技术拆解:从YOLO检测到椭圆拟合的完整方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华