ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

定时器完全图解:从555到STM32,带你搞懂计数、PWM与捕获原理

2026/9/8 22:01:12 拓冰建站 浏览量
定时器完全图解:从555到STM32,带你搞懂计数、PWM与捕获原理 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 71ARR 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、13Timer1 控制 11、12Timer2 控制 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 ... CCR3: ‾‾‾‾‾‾‾‾‾‾___‾‾‾‾‾‾‾‾‾‾___ 高3低7 CCR7: ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾___‾‾‾‾‾ 高7低3PWM 的应用范围非常大。电机调速、LED 亮度控制、蜂鸣器音调、开关电源的驱动波形全都靠它。拿 Buck 开关电源来说电源芯片或者 MCU 输出的 PWM 控制开关管通断通过调节占空比来稳定输出电压这就是“开关电源工作模式”里最常见的一种控制方式。在功率较大的场景里还需要“死区”防止上下桥臂同时导通烧管子这项功能通常在高级定时器里由硬件直接支持。好多人在设置 PWM 时没有意识到一件事PWM 的频率和占空比与 PSC、ARR、CCR 是联动的。比如 72 MHz 时钟产生 20 kHz 的 PWM如果直接分频到 1 MHzARR 设成 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 MHzAPB1 预分频是 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 MHzARR 需要设成 49为了 20 kHz那么 1% 的调节精度对应 CCR 步进仅为 0.5不够用。所以更推荐把 PSC 设小一点让计数分辨率更高。比如 PSC 1即 2 分频到 36 MHzARR 1799那么 1% 占空比对应 CCR 约 18控制精度好很多。实际计算公式如下PWM频率 定时器时钟 / ((PSC1) * (ARR1)) 占空比 CCR / (ARR1)所以如果想出 75% 占空比ARR 1799CCR 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。这行代码如果出问题跑出来的周期永远是理论值的n1)/n 倍虽然差别不大但做精密时间基准时会有麻烦。6.3 锁屏定时器失效、后台被冻结操作系统比你想的更“抠门”Windows 锁屏的时候系统为了省电和降低后台活动会自动降低或者冻结部分定时器的精度导致一些计划任务在锁屏状态下不执行或者大幅延迟。很多写桌面工具的朋友都会遇到“锁屏定时器失效”的问题。解决办法通常是注册一个系统待机状态的处理或者把任务设计成每小时唤醒一次而不是依赖高精度定时器在锁屏时还精确定时。移动端的 App 后台冻结也是同一个道理后台定时器被系统暂停。所以在线时长统计、心跳包发送这类功能必须依赖系统级的后台任务机制而不是单纯地启动一个 1 秒的 JS 定时器。6.4 选型与调试的几条经验最后分享几点经验。第一能用硬件定时器解决的事情别用软件延时硬扛。不管是按键消抖还是脉冲生成硬件定时器加中断的方案会让 CPU 有大量时间去做更重要的事而且时间精度稳定得多。第二改定时器参数时PSC 和 ARR 的更新时机要想清楚。很多定时器只有在更新事件发生时才会装载新值如果你一边输出 PWM 一边改 ARR最好等待一次更新事件完成后再改否则会出现一帧奇怪波形。第三调试定时器最有效的工具永远是示波器或逻辑分析仪。别对着变量干瞪眼直接量引脚波形一看便知周期、占空比、边沿时机是不是符合预期。第四多平台移植时要警惕“看似同名实则不同”的寄存器。51 的 TH0/TL0 是手动写初值STM32 的 ARR 是自动重装载GD32 又多了时钟预分频配置Arduino 的 PWM 频率甚至由底层库默认决定。数据结构相似但细节差异很大照着别的平台代码抄的时候一定要逐个核对寄存器含义。定时器这个东西学的时候觉得简单用的时候才知道水有多深。我自己也是踩了无数次“明明配置了却没反应”“频率总是差一倍”的坑之后才慢慢形成一套“先查时钟树、再算寄存器、最后量波形”的调试思路。希望这篇图解能帮你少走一些弯路。