目录
- 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、TIM8 | APB2 | 通用定时器的活它全干,还多了刹车、死区、互补输出—— 专门用来控制电机(比如三相无刷电机),能安全地驱动大功率器件 |
| 通用定时器(够用) | TIM2~TIM5 | APB1 | 能数外部脉冲、测脉宽、输出 PWM、读编码器、几个定时器之间能联动 |
| 基本定时器(最朴素) | TIM6、TIM7 | APB1 | 就只会一件事:定时到点产生中断,顺便能触发 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"。
整张图连起来讲一个故事
按时间顺序:
- 一开始:PSC=0,预分频系数 = 1。CNT_EN 拉高后,CNT 开始跟着 CK_PSC 飞快地数:F7,F8,F9...
- 中间某一刻:你代码里往 PSC 写了个新值 1。PSC 寄存器立刻变成 1,但缓冲器还是 0,所以分频行为没变,CNT 继续飞快地数。
- CNT 溢出:从 FC 加到 00,冒了一个 UEV 脉冲。
- UEV 那一瞬:缓冲器才把新值 1 搬上去,预分频计数器切换成 "数到 1 才溢出" 模式。
- 从此刻开始: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 把它清掉,否则下次进中断又会重复触发。
整张图连起来讲一个瞬间
按时间顺序看那个跳变点:
- CNT=0x0036,CK_CENT 来一个脉冲。
- 硬件判断:CNT+1 = 0x0037 > ARR=0x0036 → 触发溢出。
- 三件事同时发生:
- CNT 被清成 0x0000。
- "计数器溢出" 脉冲冒一下。
- UEV 脉冲冒一下,UIF 标志位置 1。
- 如果中断开着,CPU 跳进
TIMx_IRQHandler()。 - 你在中断里做两件事:
- 该干嘛干嘛(翻转 LED、读传感器……)。
- 手动把 UIF 写 0 清掉(HAL 库的
HAL_TIM_IRQHandler()会帮你做)。
- 下一个 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 两个数字,硬件就自动按这个节奏无限循环跑下去。