STM32基本定时器入门:原理、寄存器配置与定时中断实战

发布时间:2026/10/21 23:56:55
STM32基本定时器入门:原理、寄存器配置与定时中断实战 搞嵌入式开发的人迟早都要跟定时器打交道。我见过不少刚入门的朋友一听到“STM32定时器”就觉得头大又是预分频又是自动重装载还有一堆寄存器要配到底从哪下手。其实如果真想弄懂基本定时器就是最好的切入点。它把定时器最核心的概念——时钟分频、计数、溢出更新、中断——全都包括了却又没有输入捕获、输出比较、PWM这类花哨功能来干扰视线。这篇内容就围绕STM32的基本定时器展开把原理、配置、中断、实战和踩坑一起过一遍目标是让看完的人能独立写出一个定时中断程序并且明白每一行配置为什么这么写。适合刚学STM32、被定时器绕晕的入门读者也适合想快速回顾定时器基础的开发者。先说清楚这篇不打算讲什么PWM、输入捕获、输出比较、编码器模式这些通用定时器和高级定时器的功能这篇统统不碰。原因很简单基本定时器本身就没有这些功能硬塞进来只会让新手混淆。把基本定时器吃透后面的内容会自然衔接。1. 先弄清楚基本定时器在STM32定时器家族里的位置1.1 三类定时器一张表看明白打开STM32的参考手册定时器部分一般会分成基本定时器、通用定时器和高级定时器三类。很多教材喜欢一上来就把高级定时器的PWM讲得天花乱坠结果新手连计数器怎么跑都没理解直接放弃。我觉得合理的路径是先认清家族关系再逐个击破。这里先给一张简单对照表类型常见实例核心能力典型用途基本定时器TIM6、TIM7只定时产生更新中断/事件定时触发、延时基准、节拍心跳通用定时器TIM2、TIM3、TIM4、TIM5定时输入捕获输出比较PWM测频率、PWM输出、编码器接口高级定时器TIM1、TIM8通用定时器全部能力互补输出刹车等电机控制、逆变电源、互补PWM从这张表能看出基本定时器真的非常“纯粹”。它没有外部引脚用来做输入捕获也没有通道可以输出PWM它存在的意义就是“数数”数到设定值就通知你一次。用生活化的比喻来说基本定时器就是那个只会到点响一声的闹钟通用定时器是带秒表、能计圈、能输出信号的运动手表高级定时器则是赛车仪表盘各种辅助功能全都有但日常大多数人用不上那么多。1.2 为什么说基本定时器是入门的最好起点学习曲线讲究由浅入深嵌入式学习路线也是一样。基本定时器的代码量极简关掉花哨功能之后只剩时钟使能、预分频器、自动重装载和更新中断这几个要素。你只要把这几件事弄明白后面再看通用定时器会发现它只是在这些基础之上增加了捕获/比较通道而已骨架是完全一样的。而且基本定时器在实战里用处并不小。系统的软件定时器、非阻塞延时、时间片轮转调度的时钟节拍用TIM6/TIM7就能跑得很好。这是很多实时操作系统的心跳机制雏形学它绝对不亏。位置关系搞清楚了下面进入最关键的部分基本定时器内部到底是怎么把时间算出来的。2. 基本定时器的内部原理三个寄存器就把定时讲清楚了2.1 PSC、CNT、ARR各管什么基本定时器的核心是三个寄存器预分频寄存器PSC、计数器寄存器CNT、自动重装载寄存器ARR。它们配合外部送进来的时钟信号工作。可以这样理解整个定时器像一条流水线PSC负责把输入时钟进行分频CNT负责真正的一格格计数ARR则是一张“到站表”。工作流程是这样的时钟源产生的脉冲先进入PSCPSC每累计到设定值就输出一个脉冲给计数器CNTCNT每收到一个脉冲就加1当CNT的值等于ARR时这一轮计数结束产生一次更新事件。然后CNT自动清零重新从0开始下一轮。就这样周而复始定时中断就是周期性触发的。这里要强调一个很多新手都会记错的点PSC是分频系数减1ARR也是计数值减1。如果你希望72MHz的时钟被分成1MHz需要PSC71而不是72如果你希望CNT从0数到999触发需要ARR999不是1000。原因是计数包含0这个状态这个“减一”是定时器配置里最容易翻车的细节之一。2.2 一条公式算出定时时间定时中断的周期可以用一条统一公式表达T (PSC 1) × (ARR 1) / 定时器输入时钟频率比如定时器输入时钟为72MHz要得到一个1ms的中断周期可以这样配先把PSC设为71也就是72分频计数频率为1MHz也就是每1微秒CNT加1再设ARR为999CNT要数1000个脉冲才触发更新。那么T 72 × 1000 / 72MHz 1ms。这个组合在工程里非常常用建议记下来PSC71、ARR999就是72MHz主频下最经典的1ms定时组合。目标时间PSCARR实际时间1us7101us1ms719991ms10ms71999910ms100ms7199999100ms1s719999991s注意PSC和ARR都是16位寄存器最大值65535。上表中的1s配置已经把ARR用到了9999完全在范围内单次定时的理论最长长度约为59.7秒再长就要靠软件计数累加了。在72MHz输入时钟下单次定时的最大长度大概是(655351)×(655351)/72MHz约等于59.7秒。也就是说单靠基本定时器一轮计数最长能接近60秒才产生一次更新事件一般应用完全够用。如果还想要更长时间可以通过软件计数累加实现“分钟级”“小时级”的定时。2.3 为什么有人算出来的时间总是差一倍在我接触过的初学者中“定时时间算出来是1ms实测却是2ms”是最常见的问题。原因十有八九出在时钟树上。STM32内部的定时器时钟往往不是直接等于你看到的APB1外设时钟而是遵循一个规则如果APB1预分频系数为1定时器时钟等于APB1如果APB1预分频系数不为1定时器时钟等于APB1的两倍。很多开发板把系统主频设在72MHzAPB1分频为2此时APB1外设时钟是36MHz但挂在这条总线上的定时器时钟却是72MHz。如果你想当然地把36MHz代入公式算出来的时间自然翻倍。排查时间问题时第一步永远是把时钟树打开确认定时器的真实输入时钟频率再计算。3. 动手配置寄存器版和HAL库版3.1 寄存器版最小代码先看寄存器版能帮助你把原理和硬件行为对应起来。以某系列芯片的TIM6为例最小配置如下// 1. 使能TIM6所在总线的时钟 RCC-APB1ENR | RCC_APB1ENR_TIM6EN; // 2. 配置预分频72MHz输入分频72倍计数频率1MHz TIM6-PSC 71; // 3. 配置自动重装载值计数1000次定时1ms TIM6-ARR 999; // 4. 使能更新中断 TIM6-DIER | TIM_DIER_UIE; // 5. 清一次更新标志避免一启动就误进中断 TIM6-SR 0; // 6. 启动计数器 TIM6-CR1 | TIM_CR1_CEN;这里第4步的DIER寄存器控制中断使能对应的事件是“更新事件”所以使用的是更新中断使能位UIE。第6步CR1的CEN是计数器使能位。配置顺序上建议先把参数写到位最后再启动计数这样不会出现先用旧参数跑了几拍的尴尬。寄存器版的逻辑非常直白你给我正确的PSC和ARR我就能按公式跑周期。缺点是不同芯片的库定义和位名称可能不同比如有的芯片TIM6的中断向量叫TIM6_DAC_IRQn有的直接叫TIM6_IRQn写NVIC配置前一定要查自己的芯片头文件。3.2 HAL库版最小代码现在工程实践中用HAL库的更多配置方式略有不同。HAL库把定时器封装成了一个句柄结构体你只需要填充几个字段TIM_HandleTypeDef htim6; void MX_TIM6_Init(void) { htim6.Instance TIM6; // 选择基本定时器实例 htim6.Init.Prescaler 71; // 72分频计数频率1MHz htim6.Init.CounterMode TIM_COUNTERMODE_UP; // 基本定时器只能向上计数 htim6.Init.Period 999; // 计数1000次定时1ms htim6.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; htim6.Init.AutoReloadPreload TIM_AUTORELOAD_PRELOAD_ENABLE; if (HAL_TIM_Base_Init(htim6) ! HAL_OK) { Error_Handler(); } }这里的CounterMode字段虽然在基本定时器上只能配“向上计数”但HAL库为了跟通用定时器共用一套接口还是要求你填上。AutoReloadPreload表示ARR寄存器是否启用预装载功能启用后你在运行期修改ARR不会立刻生效而是等本次计数结束才加载新值这样可以避免计数中途被改值。对学习阶段来说开不开影响不大但养成开预装载的习惯对以后做PWM调频很有帮助。3.3 初始化底层发生了什么注册了定时器句柄不代表定时器已经开始跑。HAL库把“初始化结构体”和“启动定时器”分成了两个动作MX_TIM6_Init只负责配置参数和注册底层真正开始计数需要调用另一个接口。这个设计理念和寄存器版先配参数再置位CEN是一致的目的都是防止定时器用不完整的配置提前跑起来。另外还需要注意底层时钟的问题。HAL库提供了一个回调函数HAL_TIM_Base_MspInit通常在这个函数里使能定时器时钟并配置NVIC中断优先级。如果你用的是图形化配置工具自动生成代码这个函数由工具生成不要手动删掉。手动写代码的话千万不要忘记在这一步做RCC时钟使能否则后果就是定时器寄存器写啥都像石沉大海读出来全是默认值。这个问题我在后面故障排查部分还会展开。4. 让定时中断真正跑起来4.1 从初始化到中断触发还差三步把定时器初始化做完只相当于装好了一个闹钟但闹钟的铃声还没接线。要让更新事件真的触发CPU中断至少还差三步第一步在MspInit里使能定时器时钟第二步配置NVIC使能定时器中断线并设置优先级第三步调用启动接口让定时器动起来。如果用HAL库启动接口需要区分两个常用函数HAL_TIM_Base_Start和HAL_TIM_Base_Start_IT。前者只启动计数器不进中断后者启动计数器并打开更新中断。很多人写代码时偷懒一律用Start_IT或者反过来把两者搞混导致定时器一直在跑但中断就是不进来这种问题查起来非常耗时。我建议在代码里明确注释需要中断就用Start_IT只是后台计数就用Start。NVIC配置在HAL库中一般是这样HAL_NVIC_SetPriority(TIM6_DAC_IRQn, 1, 0); HAL_NVIC_EnableIRQ(TIM6_DAC_IRQn);优先级分组在工程初始化时已经设好这里指定抢占优先级为1、子优先级为0。基本定时器中断是相对轻量的中断优先级不宜设得太低否则会被高频通信中断反复抢占导致定时节奏偶尔抖动。但也不建议设成最高0除非你的系统里没有比它更紧急的中断源。4.2 回调函数多个定时器共用同一个入口HAL库在处理中断时把所有定时器的“周期结束”事件都送进了同一个回调函数HAL_TIM_PeriodElapsedCallback。这意味着如果你的系统里同时跑了TIM6和TIM7两者的更新事件都会进入这个函数你必须在函数开头判断到底是哪个定时器触发的void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM6) { // 定时到了做该做的事 } }这一步很多新手会漏结果TIM6和TIM7共用同一个处理逻辑数据串了还找不到原因。另外HAL库要求回调函数体不要写耗时操作。中断服务函数执行时间越长对主循环和其他中断的阻塞就越严重。你如果在里面放一个HAL_Delay或者一段长时间阻塞的串口发送轻则系统卡顿重则触发看门狗复位。一般原则是回调里只做置标志位、翻转IO、计数累加这类微秒级操作真正的业务逻辑放到主循环里处理。4.3 一个LED翻转Demo把前面的内容串起来一个完整的定时中断Demo大概是这样的TIM6产生1ms周期中断在回调里翻转一个LED引脚于是LED以2ms为周期翻转也就是每秒切换500次肉眼基本看不出来但用示波器或逻辑分析仪可以清楚看到方波。如果想让LED肉眼可见地每秒闪一次可以在回调里加一个软件计数器static uint16_t tick_1000 0; void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM6) { tick_1000; if (tick_1000 500) // 1ms触发一次累计500次即500ms { tick_1000 0; HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); } } }这个Demo虽然简单但它验证了整条链路时钟使能、定时器参数、NVIC、启动接口、回调分发、标志位累加。链路中任何一环出错LED都不会按预期闪烁这就成了很好的排查工具。5. 玩法升级用基本定时器做非阻塞延时和简易调度5.1 非阻塞延时的实现思路很多人刚接触STM32时用的延时都是HAL_Delay它本质上是一个阻塞式延时在延时期间整个程序卡在那里干不了任何别的事。这在简单Demo里无所谓但只要系统里同时要处理按键、通信、显示等多个任务阻塞延时就会成为灾难。基本定时器可以给出一个非常优雅的替代方案维护一个全局“自增计数”每次更新中断加1主循环读取这个值算出经过的时间。比如我在工程里常用这样的模式volatile uint32_t system_tick 0; // 1ms递增一次 // 在定时器更新回调里 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM6) { system_tick; } } // 非阻塞延时函数 void non_block_delay(uint32_t ms) { uint32_t start system_tick; while ((system_tick - start) ms) { // 主循环在这里可以执行其他任务 } }注意这里system_tick声明为volatile是因为它在中断里被修改、在主循环里被读取编译器有可能把它优化到寄存器里导致主循环永远读不到最新值。加上volatile才能保证每次都从内存取值。这是一个很多教程不会专门讲的细节但实际踩过坑的人都知道少了它有多痛苦。用32位无符号数做差值判断还有一个好处即使计数器发生回绕也能正确判断时间不必自己处理溢出。5.2 主循环里的时间片调度雏形有了1ms的系统节拍你在主循环里可以非常方便地做多任务时间片轮转。比如想每10ms扫描一次按键每50ms刷新一次显示每200ms上报一次数据完全不用互相等uint32_t last_key 0, last_disp 0, last_upload 0; while (1) { if (system_tick - last_key 10) { last_key system_tick; scan_key(); } if (system_tick - last_disp 50) { last_disp system_tick; refresh_display(); } if (system_tick - last_upload 200) { last_upload system_tick; upload_data(); } }这种写法本质上就是一个极简的调度器也是很多轻量级嵌入式系统的心跳雏形。它在可读性、可维护性和实时性上比一堆HAL_Delay嵌套好太多。当你感觉到多个功能开始抢着用延时的时候就应该考虑重构到以系统节拍为核心的模型上来了。基本定时器在这里扮演的角色就是那个稳定、精确、永不疲劳的节拍源。6. 现场问答定时器用不起来的几类典型故障6.1 定时器不进中断先查这几处大家提问时出现频率最高的问题就是“定时器配置好了但中断就是不触发”。我通常按下面的顺序排查先确认RCC时钟有没有使能。可以单步执行到启动函数查看定时器对应的SR寄存器是否有更新标志如果读出来一直是0大概率时钟没开或者根本没启动。再确认NVIC是否使能了对应的中断。只启动定时器不打开NVIC定时器自己会数数但你永远等不到中断函数执行。然后是中断处理函数或回调函数的名字。HAL库中回调函数写得不对编译器不报错中断事件却不会经过你写的处理逻辑这是最隐蔽的一类错误。最后检查是否用了HAL_TIM_Base_Start但实际需要Start_IT。把这四个点过一遍基本能解决九成以上的“不进中断”问题。6.2 时间不对先查时钟树再查“减一”定时时间算出来和实测不一致优先看时钟树。上面说过APB1分频系数不为1时定时器时钟是APB1的两倍很多教材里的“72MHz定时器时钟”就是这么来的。如果确认时钟没问题再检查PSC和ARR是不是忘了减1。举个例子你配PSC71、ARR999实际是72分频和1000个计数周期。如果有人把ARR写成1000那定时时间会变成1.001ms虽然偏差不大但在追求精确的场合这就是Bug。养成先按公式推算出期望值、再反推PSC和ARR的习惯能省很多定位时间。6.3 中断里做了耗时操作系统“假死”我在早期调试时有一段时间把日志打印直接写在定时器中断回调里结果只要中断一触发系统就表现迟缓按键反应迟钝通信频繁超时。当时我一度以为是外部干扰查了半天才发现是中断嵌套阻塞导致主循环饿死。后来把串口发送改成环形缓冲回调里只负责往缓冲区丢数据主循环慢慢处理问题立刻消失。这也是在4.2里反复强调回调要做轻量操作的原因。如果你现在也遇到“系统过一会儿就像卡住”不妨把回调里的代码全部注释掉看现象是否恢复。这个方法排查起来非常快。6.4 调试建议用逻辑分析仪或示波器代替猜最后分享一个调试心得。验证定时器是否按预期工作最直观的方法是让某个IO口在回调里翻转然后用逻辑分析仪或示波器看这个引脚的波形周期。数字上算出来1ms示波器上测出1ms说明整条链路没问题测出2ms立刻想到时钟树两倍关系测出不定长再考虑是不是中断被高优先级事件卡住。这些信息比盲改代码可靠得多。手头没有专业仪器的话用单片机另一个引脚做翻转输出配合定时器也勉强能测但精度和可靠性都远不如逻辑分析仪。实际工作中我还有一个习惯每次新配置一个定时器都先在Demo工程里跑一遍最小中断确认回调能进、时间正确再往上加业务代码。这个习惯帮我避开了大量“到底是定时器问题还是业务代码问题”的纠缠。这段学习走到基本定时器这一篇算是把嵌入式里最核心的时间概念立住了。接下来再往上走就是通用定时器的PWM输出、输入捕获这些更进阶的内容但那些功能再怎么花哨骨子里的核心依然是预分频、自动重装载和更新事件这套东西。把这一篇里的每个细节都亲手验证一遍后面就顺多了。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询