1. 先把“定时器”说透:它到底在计什么
“定时器”这个名字,其实是嵌入式开发里最容易被低估的外设。刚学单片机那会儿,我也觉得它不就是个秒表吗?后来做产品踩了一圈坑才明白,几乎所有实用功能的地基都是它:按键消抖、呼吸灯、电机调速、编码器测速、串口超时判断、操作系统的时钟节拍,哪个也离不开定时器。这篇文章我就用尽量多的图解,把定时器的原理和工作模式从头到尾捋一遍,顺便把 555、51、STM32、GD32、Arduino、Linux、Cron、PLC 这些五花八门场景里的定时器统一起来看。
1.1 定时器的物理本质:其实就是一个“数数的家伙”
用一句话概括:定时器就是一个计数器,它靠固定的“节拍”数数,数到某个目标值就做一件事。这里的“节拍”可以是内部的时钟脉冲,也可以是外部输入的脉冲,甚至是编码器反馈回来的两路脉冲。把“计数器”这个概念画出来大概是这样的:
计数时钟 : __|‾|__|‾|__|‾|__|‾|__|‾|__|‾|__|‾| 计数值 : 0 1 2 3 4 5 6 ... 比较点 : ▲ 达到目标值,触发中断或翻转引脚 溢出点 : ▲ 计满回0,产生更新事件从硬件结构上看,一个典型定时器包括:时钟源、预分频器(PSC)、计数器(CNT)、自动重装载寄存器(ARR),以及围绕它们的捕获/比较逻辑。时钟源决定了“多久数一下”,预分频器负责把时钟降到合适的速度,计数器负责累加,自动重装载寄存器则规定了数到多少才算一个周期。
很多初学者第一次接触定时器时,脑袋里只有“延时”两个字。但实际上,定时器的价值远不止延时:它可以输出精确的 PWM,可以捕获外部脉冲的频率和脉宽,可以解码编码器转过的角度,可以在溢出时触发 ADC 采样,甚至可以级联多个定时器,把整个系统的执行顺序编排得像交响乐一样精确。
1.2 时基、预分频和重载值:定时器参数的“铁三角”
理解定时器,最关键的是把三个参数的关系理清楚:时钟频率、预分频系数 PSC、自动重装载值 ARR。
拿 STM32 举例,如果定时器输入时钟是 72 MHz,而我们希望计数器每 1 微秒数一次,那么就需要把 72 MHz 分频成 1 MHz。这里的做法是:
- 预分频系数 PSC 设为 72 - 1 = 71,实际分频倍数等于 PSC + 1,也就是 72 倍。
- 分频后计数频率:72 MHz / 72 = 1 MHz,即每 1 μs 计数一次。
- 如果我想让定时器每 1 ms 产生一次中断,那就让计数器数 1000 个数,ARR 设为 1000 - 1 = 999。
所以最终的时间计算公式是:
定时周期 T = (PSC + 1) × (ARR + 1) / 定时器时钟频率举个例子:72 MHz 时钟,PSC = 71,ARR = 999,那么 T = 72 × 1000 / 72 MHz = 1000 μs = 1 ms。
有的朋友会问,为什么很多例程里 PSC 和 ARR 都要减 1?因为这两个寄存器的值是从 0 开始计数的,74 个脉冲对应编号 0 到 73,所以“分频 74 倍”就要写 73。这个看起来很小的细节,经常导致时间算出来正好差一倍,排查半天发现是这里没减。
在 51 单片机里,16 位定时器的用法本质上也是这套逻辑。比如 12 MHz 晶振、12T 模式下机器周期是 1 μs,让 TH0/TL0 从 65536 - 50000 开始数,数到 65536 溢出,这中间恰好是 50000 μs,也就是 50 ms。手动装载初值和硬件自动重装载的差别,只是“谁来把初值写回去”而已。
1.3 图解一个周期的诞生:从时钟到中断的完整链路
把一次定时器中断的完整过程拆开看,大约是这样的:
时钟源产生脉冲 │ ▼ 预分频器(PSC)降频 ─────► 计数器(CNT)从0开始累加 │ ▼ CNT 与 ARR 比较 │ ┌──────────┴──────────┐ ▼ ▼ 相等 未到 │ │ ▼ ▼ 更新事件/溢出中断 继续累加 │ ▼ CNT 回到0(或装载初值)这里面最容易忽略的是“更新事件”和“更新中断”的区别。更新事件除了可以触发中断,还可以触发 DMA、触发 ADC、触发其他定时器,而更新中断只是 CPU 的其中一个中断源。很多高性能玩法,比如让定时器溢出时自动搬运一段数据到 DAC,靠的就是更新事件 + DMA,而不是更新中断,因为 DMA 不需要 CPU 介入,效率高得多。
2. 从 555 到 PLC:各类定时器的脾气秉性
很多爱好者在学定时器时会有一个困惑:这些东西在 51 上叫“定时器”,在 STM32 上叫“高级定时器、通用定时器、基本定时器”,在 PLC 里又叫“TON、TOF”,在 Linux 里还有“高精度定时器”,它们到底是不是同一个东西?
答案是:原理上都是“计数 + 比较/捕获”,但在具体形态和使用方式上,差别巨大。了解这些差别,才能在选型时做到心里有数。
2.1 经典 555:模拟世界里的定时神器
555 定时器是一颗经典的模拟+数字混合芯片,很多入门教材都会拿它做方波发生器。它内部有两个比较器、一个 RS 触发器、一个放电三极管和一个分压电阻网络。外部只需要接电阻和电容,就能实现定时。
它最常用的三种接法是:单稳态、双稳态和无稳态(多谐振荡)。无稳态接法可以持续输出方波,这就是很多课程里“555 定时器产生方波”和“555 定时器构成多谐振荡器”的由来。振荡周期大概满足:
T ≈ 0.693 × (R1 + 2 × R2) × C不过要注意,这个公式算出来的是近似值,而且 555 的工作频率和电阻电容精度直接挂钩,温漂也不小。所以工业产品里几乎没人用它做精密时钟源,它更适合做简单控制、报警音发生、简易呼吸灯这类对精度不敏感的场景。相比之下,单片机定时器由晶振驱动,精度要高好几个数量级,还能随时改参数。
2.2 MCU 定时器:51、STM32、GD32、Arduino 的资源观
51 单片机的定时器数量一般只有 2 到 3 个,但功能不简单。以 8051 为例,Timer0 和 Timer1 都是 16 位,可以配置成 13 位、16 位、8 位自动重装载等模式。其中一个典型用途是作为串口波特率发生器,因为串口通信需要非常稳定的时间基准。
STM32ZET6 这类大容量芯片,定时器资源就丰富多了。它内部通常包括:
- 2 个高级定时器 TIM1/TIM8:支持 PWM 互补输出、死区插入、刹车功能,专门驱动电机和电源变换器。
- 多个通用定时器 TIM2/TIM3/TIM4/TIM5:支持定时、计数、PWM、输入捕获、编码器接口。
- 多个基本定时器 TIM6/TIM7:只能做时基和触发 DAC,没有引脚输出。
- 一个 SysTick 滴答定时器:专门给操作系统做时基。
GD32F450 的定时器架构和 STM32 非常像,但它有一个很值得注意的点:定时器时钟需要通过rcu_timer_clock_prescaler_config来配置。比如函数参数RCU_TIMER_PSC_MUL2表示 APB1 预分频不为 1 时,定时器时钟自动加倍。这个细节如果不处理,定时器实际跑出来的时间可能和理论值差一倍,后面我会专门讲。
Arduino 的 Mega2560 用的是 ATmega2560 芯片,有 6 个定时器,其中 Timer0 用于millis()和micros(),Timer1 是 16 位且常用于舵机控制,Timer2 则常被用来做低功耗唤醒。在 Mega2560 上做 PWM 时,要注意每个定时器对应的引脚号:Timer0 控制 4、13,Timer1 控制 11、12,Timer2 控制 9、10,剩下的 Timer3、Timer4、Timer5 对应另外一批引脚。选错引脚,PWM 信号就出不来。
2.3 PLC 与软件环境里的定时器:换个形态,本质不变
PLC 里的定时器,本质上是一个由扫描周期驱动的软定时器。以施耐德 PLC 的 ST 语言为例,常用的有三种:TON(延时接通)、TOF(延时断开)、TP(脉冲输出)。它们不像单片机定时器那样直接占用一个硬件计数器和引脚,而是由 PLC 操作系统在每个扫描周期里统一刷新计时状态。
再往软件层面看,8086 时代用的 8253/8254 可编程间隔定时器(PIT)是 PC 平台上最经典的定时器,它有三个独立的 16 位计数通道,后来演变成了现代操作系统里的系统时钟源。现在的 Linux 定时器、Windows 定时器、Cron 表达式,本质上都是“软件管理 + 硬件时基”的组合:硬件提供一个固定频率的 tick,软件在 tick 里检查有没有到期的任务。
所以你看,从 555 到 PLC 再到 Linux,定时器的内核始终是同一个:一个稳定的时间源,加上一套计数/比较机制,最后在规定时刻触发动作。理解了这条主线,后面学任何具体平台的定时器都会轻松很多。
3. MCU 定时器六大工作模式图解
很多芯片手册会把定时器模式列得很吓人,比如 PWM 模式 1、PWM 模式 2、输入捕获模式、编码器模式、外部时钟模式 1、外部时钟模式 2、单脉冲模式、触发同步模式……其实归纳起来,MCU 定时器无非就六类核心工作模式。
3.1 定时/计数模式:最基础也是最常用的底子
定时模式用的是内部时钟,计数器按预分频后的频率递增,到 ARR 后溢出,产生更新事件。计数模式则把时钟源换成外部引脚脉冲,比如用外部信号驱动计数器,数出脉冲个数。
这里要提一下“GPT 工作模式”。在 GD32、STM32 的文档里,GPT 通常指 General-Purpose Timer,也就是通用定时器。通用定时器的价值在于“一专多能”:既能做时基,又能做 PWM、捕获和编码器,不像基本定时器只能做时基,也不像高级定时器那样为了电机控制堆了一堆复杂功能。对大多数项目来说,通用定时器是最常用的。
在代码层面,定时/计数模式最典型的配置是:
// 以 HAL 库为例,1ms 定时器中断 htim3.Init.Prescaler = 72 - 1; // 72MHz / 72 = 1MHz,即 1us 计数一次 htim3.Init.CounterMode = TIM_COUNTERMODE_UP; htim3.Init.Period = 1000 - 1; // 计数 1000 次,得到 1ms HAL_TIM_Base_Init(&htim3); HAL_TIM_Base_Start_IT(&htim3);中断回调里判断来源时,要注意多个定时器共用一个回调函数:
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM3) { // 1ms 到了,更新标志位 } }3.2 PWM 输出模式:让引脚“自动翻转”
PWM 模式是定时器的“明星功能”。它的原理很简单:计数器从 0 递增到 ARR,中间有个比较值 CCR,当 CNT 小于 CCR 时输出高电平,大于等于 CCR 时输出低电平。改变 CCR 就改变了占空比,改变 ARR 就改变了周期。
CNT: 0 1 2 3 4 5 6 7 8 9 0 1 2 ... CCR=3: ‾‾‾‾‾‾‾‾‾‾___‾‾‾‾‾‾‾‾‾‾___ (高3低7) CCR=7: ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾___‾‾‾‾‾ (高7低3)PWM 的应用范围非常大。电机调速、LED 亮度控制、蜂鸣器音调、开关电源的驱动波形,全都靠它。拿 Buck 开关电源来说,电源芯片或者 MCU 输出的 PWM 控制开关管通断,通过调节占空比来稳定输出电压,这就是“开关电源工作模式”里最常见的一种控制方式。在功率较大的场景里,还需要“死区”,防止上下桥臂同时导通烧管子,这项功能通常在高级定时器里由硬件直接支持。
好多人在设置 PWM 时没有意识到一件事:PWM 的频率和占空比与 PSC、ARR、CCR 是联动的。比如 72 MHz 时钟产生 20 kHz 的 PWM,如果直接分频到 1 MHz,ARR 设成 50,那么 1% 的占空比对应的 CCR 变化量只有 0.5,精度根本不够。正确的思路是先确定需要的分辨率和频率,再反推 PSC 和 ARR,不能随手填。
3.3 输入捕获模式:测量外部信号的频率和脉宽
输入捕获模式的思路是:当引脚出现指定边沿时,硬件把当前 CNT 值“咔嚓”一下存到捕获寄存器里。只要比较前后两次捕获值的差,再除以计数频率,就能算出脉冲宽度或周期。
测频率的经典方法是“等精度测量”:
上升沿1到来 -> CCR1 = 当前的 CNT 值(比如 1000) 上升沿2到来 -> CCR2 = 当前的 CNT 值(比如 4000) 两次间隔 = 4000 - 1000 = 3000 个计数 如果计数频率是 1MHz,那么周期 = 3000us,频率 ≈ 333Hz如果要测更高频率,通常可以先把时钟频率提高,或者使用“外部时钟模式”直接对外部脉冲计数,统计 1 秒内来了多少个脉冲,这样频率就是计数值本身。这个操作在 STM32 上被称为“定时器捕获测频率”,在 CubeMX 里配置一个输入捕获通道,然后在回调里计算差值,实现起来很快。
3.4 编码器模式:电机转了多少,定时器知道
编码器模式对做机器人、伺服控制的朋友来说是刚需。增量式编码器输出 A、B 两路相位相差 90° 的方波,电机的转动方向决定了 A、B 谁领先谁。定时器的编码器模式会直接根据 A、B 的相位关系自动加/减计数,不需要 CPU 干预。
正转:A领先B 90° -> 计数器递增 反转:B领先A 90° -> 计数器递减这样配置之后,你在任意时刻读 CNT,就能知道电机从开机到现在走了多少个脉冲。再结合编码器每圈脉冲数,就能换算成角度或位移。CubeMX 里设置编码器模式时,通常把两个通道都配置为“TI1FP1 + TI2FP2”,极性和滤波参数看实际编码器手册。需要注意的是,当计数器计到 ARR 或 0 时会发生溢出,这个溢出通常需要在中断里记录,否则角度会“记断片”。
3.5 看门狗、系统节拍与外部触发:定时器的隐藏身份
其实还有一批定时器并不直接面向外部信号,而是承担系统的“守门员”角色。看门狗定时器就是典型,它要的是“喂狗”:程序必须在设定时间内重置计数器,否则系统复位。STM32 里的独立看门狗 IWDG 和窗口看门狗 WWDG 不一样,IWDG 是自由运行的,WWDG 则必须在特定窗口期内喂狗,提前或晚了都会复位,适合对程序执行时序要求更严格的产品。
SysTick 滴答定时器则是一种特殊的 24 位向下计数器,它不输出 PWM、不捕获输入,唯一的工作就是每固定周期产生一次异常,让操作系统能切换任务、更新系统 tick。裸机环境下,它也常被用来做简单的软件定时调度。
此外,高级定时器还有外部触发和从模式,可以实现“一个定时器触发另一个定时器开始计数”,或者“外部信号让定时器立刻复位”。这种模式在逆变电源、电机控制的换相逻辑里很常用,因为硬件同步比软件在中断里手动触发可靠得多,延迟也小。
4. 图解实操:手把手配置一个定时器
理论讲了这么多,最终还是要落到代码。这里我以常见的 STM32 和 GD32 平台为例,把定时器配置的完整逻辑走一遍。很多人用 CubeMX 点点点就生成代码了,但如果不知道背后发生了什么,出了问题根本没法排查。
4.1 时钟树怎么走:为什么定时器时钟会多出两倍
先看一个典型问题:STM32F1 的定时器时钟,为什么系统时钟是 72 MHz,APB1 预分频是 2,定时器时钟却可能变成 72 MHz 而不是 36 MHz?这是因为芯片设计时做了补偿:当 APB 预分频不为 1 时,定时器时钟自动乘以 2。
GD32F450 里对应的接口就是:
rcu_timer_clock_prescaler_config(RCU_TIMER_PSC_MUL2);如果这里配错了,定时器实际计数频率就会差一倍。很多人的定时器中断“看起来”慢了一半或快了一倍,先别怀疑代码逻辑,去查时钟树配置。这类坑最隐蔽,因为它不是语法错误,而是跑起来才能发现的“倍频/二分频”问题。
4.2 用 CubeMX 生成一次定时器中断:从图形界面到寄存器
CubeMX 里配置定时器中断的步骤大约是:
- 把一个通用定时器(比如 TIM3)的 Clock Source 选为 Internal Clock。
- Prescaler 填写 71,自动重装载计数器填写 999,得到 1 ms 周期。
- 在 NVIC Settings 里勾选 TIM3 global interrupt。
- 生成代码后,调用
HAL_TIM_Base_Start_IT(&htim3);,然后在回调里写业务逻辑。
但要注意,CubeMX 只负责生成初始化代码,中断回调函数体还是要自己写。HAL 库的中断回调有个特点:它是全局的,所有定时器共用同一个回调函数,所以必须判断htim->Instance。
这里还要顺带提一下 GPIO 的工作模式。定时器要输出 PWM,引脚不能配成普通推挽输出,而是必须配置为“复用推挽输出”。STM32 的 GPIO 有 8 种工作模式,具体包括:模拟输入、浮空输入、上拉输入、下拉输入、开漏输出、推挽输出、复用开漏输出、复用推挽输出。定时器通道对应的引脚,一定要选“复用输出”类模式,否则 PWM 信号根本送不出来。很多新人用 HAL 库配置了定时器,但忘了把 GPIO 初始化成GPIO_MODE_AF_PP,最后发现引脚上永远是高电平或低电平,就是这个原因。
4.3 定时器输出 PWM 的图解配置:百分比到底怎么算
以输出 20 kHz、50% 占空比的 PWM 为例,设定 72 MHz 定时器时钟,如果先分频到 1 MHz,ARR 需要设成 49(为了 20 kHz),那么 1% 的调节精度对应 CCR 步进仅为 0.5,不够用。所以更推荐把 PSC 设小一点,让计数分辨率更高。比如 PSC = 1,即 2 分频到 36 MHz,ARR = 1799,那么 1% 占空比对应 CCR 约 18,控制精度好很多。
实际计算公式如下:
PWM频率 = 定时器时钟 / ((PSC+1) * (ARR+1)) 占空比 = CCR / (ARR+1)所以如果想出 75% 占空比,ARR = 1799,CCR = 1350。如果你需要动态调整频率和占空比,就得先改 ARR 再改 CCR,顺序不要反,因为很多定时器在更新 ARR 时会有重装载时机,处理不好会出现一次毛刺波形。
4.4 输入捕获和编码器模式:像做实验一样看波形
做输入捕获测频率时,我建议先用信号发生器输出一个已知频率的方波,再用逻辑分析仪或示波器观察引脚波形,最后看打印出来的测量值。如果测出来总是 0 或者数值跳变,通常不是代码写法问题,而是:
- 捕获边沿选错了,上升沿还是下降沿。
- 两次捕获之间计数值溢出了,但这个溢出没有处理。
- 信号没有经过电平转换,超过引脚耐压。
编码器模式调试更简单:用手慢慢转电机,打印 CNT 值。正转让 CNT 增加,反转让 CNT 减小,如果相反就把 A、B 相交换,或者把极性配置反过来。如果 CNT 乱跳,优先检查编码器供电电压和 A/B 相有没有加上拉电阻。
5. 软件定时器与表达式:没有硬件也要有时间轴
嵌入式之外,我们还会和很多“软件化”的定时器打交道。它们没有独立的计数器硬件,但本质仍然复用底层的硬件 tick,只是在上面包了一层软件调度。
5.1 Linux 定时器、timerfd 与高精度定时器
Linux 里有多种定时器接口,比如基于timer_list的内核定时间器,基于timerfd的进程定时器,还有高精度的hrtimer。它们都依赖内核时基,由内核维护一套到期任务链表,在时钟中断里检查并执行回调。用timerfd_create可以创建一个文件描述符,配合epoll统一监听,这样定时器事件就能和网络事件共用一套事件循环,非常优雅。
高精度定时器的意义在于,它不再依赖固定的 jiffies 节拍,而是直接利用硬件定时器提供纳秒级的到期精度,适合多媒体播放、实时控制这类对时间敏感的场合。
5.2 Cron 表达式:服务器世界的“前端定时组件”
Cron 是服务器领域最经典的定时任务表达式,它由五个字段加一个可选的用户名组成:
分 时 日 月 周 */5 * * * *这个表达式表示每 5 分钟执行一次任务。如果要每天 3 点执行,就是0 3 * * *。项目里如果要做定时任务配置界面,通常会做一个 Cron 表达式前端组件,把分、时、日、月、周做成下拉框或输入框,自动生成表达式。前端的难点在于合法性校验和中文提示,因为普通人看到*/15的时候,十有八九不明白是什么意思。
5.3 PLC ST 语言定时器:TON、TOF、TP 的工业用法
施耐德 PLC 的 ST 语言里,定时器功能块使用起来非常直观。以 TON(延时接通)为例:
TON_1(in := StartSignal, PT := T#5S, Q => OutputFlag, ET => ElapsedTime);这段代码的意思是:当StartSignal变为 TRUE 后,开始计时 5 秒;5 秒后OutputFlag变为 TRUE;如果StartSignal在中途变 FALSE,计时器立刻复位。和单片机定时器最大的不同是,PLC 的定时器依赖于扫描周期,所以定时精度通常在毫秒到十几毫秒级别,不适合做微秒级控制。如果做运动控制,还是得靠专用运动控制模块或硬件中断。
5.4 UI 定时器和游戏网络里的闲置定时器:离用户最近也最容易卡
桌面开发里,C# 的DispatcherTimer和 Windows Forms 的Timer特别容易踩坑。很多人的第一反应是把定时器周期缩短到几十毫秒去刷新界面,结果界面反而越来越卡。这是因为 UI 定时器的回调是在 UI 线程上执行的,你在回调里做任何耗时操作,界面都会被阻塞。
正确的做法是:耗时计算放到线程池或后台任务里,定时器回调里只做“把结果丢给界面”这一个小动作。如果你看到“C# UI 定时器刷新卡顿”,十有八九是回调函数里有数据库查询、文件读写或复杂循环。
操作系统里的“闲置定时器”也是如此,比如很多网络框架、游戏服务器里会设置一个“不活动定时器”,如果客户端超过一定时间没有发数据,服务器就自动断开连接,避免资源被无效连接占着。这类定时器不像 PWM 一样有波形,但它同样是整个系统中“时间轴”的一部分。
6. 定时器“翻车”记录:常见问题排查与避坑手册
最后这部分,我把这几年遇到过的定时器问题集中整理一下。这些坑单独看都不大,但每一个都可能让你白熬好几个通宵。
6.1 为什么 PWM 输出到 100% 占空比就异常
有时候我们需要把占空比加到 100%,结果发现输出反而变成常低或干脆没波形。原因通常是设置 PWM 时,CCR 被设成了 ARR + 1,也就是比较值超出了自动重装载值。不少定时器在这种情况下会直接输出无效电平。
解决办法是:如果要做 100% 占空比,把 CCR 设为 ARR + 1 在某些芯片上有效,在另一些芯片上无效;最稳妥的做法是 CCR 设为 ARR(在边沿对齐模式下),或者干脆把通道输出强制到一个确定电平。不同芯片的边界行为不一样,做产品前一定要查芯片参考手册里的“比较值等于 ARR 或超过 ARR”一节。
6.2 定时器中断不触发或者频率差一倍
中断不触发的排查顺序大致是:
- 先确认时钟树里定时器时钟实际是多少,尤其是 GD32/STM32 的 APB 分频补偿。
- 再确认 NVIC 有没有使能,HAL 库中
HAL_TIM_Base_Start_IT有没有调用。 - 最后用示波器量一下定时器引脚上有没有波形,如果有波形但没有中断,大概率是中断注册出问题了。
频率差一倍,最常见的原因就是 4.1 节说的 APB 分频补偿没配置对。还有一种是预分频和重载值的“减一”问题:计算时想的是 72 倍分频,结果 PSC 填了 72 而不是 71;想数 1000 次,结果 ARR 填了 1000 而不是 999。这行代码如果出问题,跑出来的周期永远是理论值的(n+1)/n 倍,虽然差别不大,但做精密时间基准时会有麻烦。
6.3 锁屏定时器失效、后台被冻结:操作系统比你想的更“抠门”
Windows 锁屏的时候,系统为了省电和降低后台活动,会自动降低或者冻结部分定时器的精度,导致一些计划任务在锁屏状态下不执行或者大幅延迟。很多写桌面工具的朋友都会遇到“锁屏定时器失效”的问题。解决办法通常是注册一个系统待机状态的处理,或者把任务设计成每小时唤醒一次,而不是依赖高精度定时器在锁屏时还精确定时。
移动端的 App 后台冻结也是同一个道理:后台定时器被系统暂停。所以在线时长统计、心跳包发送这类功能,必须依赖系统级的后台任务机制,而不是单纯地启动一个 1 秒的 JS 定时器。
6.4 选型与调试的几条经验
最后分享几点经验。
第一,能用硬件定时器解决的事情,别用软件延时硬扛。不管是按键消抖还是脉冲生成,硬件定时器加中断的方案,会让 CPU 有大量时间去做更重要的事,而且时间精度稳定得多。
第二,改定时器参数时,PSC 和 ARR 的更新时机要想清楚。很多定时器只有在更新事件发生时才会装载新值,如果你一边输出 PWM 一边改 ARR,最好等待一次更新事件完成后再改,否则会出现一帧奇怪波形。
第三,调试定时器最有效的工具永远是示波器或逻辑分析仪。别对着变量干瞪眼,直接量引脚波形,一看便知周期、占空比、边沿时机是不是符合预期。
第四,多平台移植时,要警惕“看似同名实则不同”的寄存器。51 的 TH0/TL0 是手动写初值,STM32 的 ARR 是自动重装载,GD32 又多了时钟预分频配置,Arduino 的 PWM 频率甚至由底层库默认决定。数据结构相似,但细节差异很大,照着别的平台代码抄的时候,一定要逐个核对寄存器含义。
定时器这个东西,学的时候觉得简单,用的时候才知道水有多深。我自己也是踩了无数次“明明配置了却没反应”“频率总是差一倍”的坑之后,才慢慢形成一套“先查时钟树、再算寄存器、最后量波形”的调试思路。希望这篇图解能帮你少走一些弯路。