资讯动态

寄存器版STM32定时器中断实战:PSC、ARR与NVIC优先级配置

发布时间:2026/10/14 16:56:37 来源:尧图企业网站定制
如果你已经跟着某本寄存器风格的手册把GPIO点亮LED、时钟树复位这类前两章内容吃透了那第三章就是你从“点灯工程师”迈向“真正理解单片机”的第一个陡坡。这一章通常集中讲定时器和中断尤其是定时中断的完整链路硬件定时器怎么计数、怎么分频、怎么产生更新事件中断怎么被CPU响应、优先级又在哪里被仲裁。很多人用库函数写定时器回调感觉很简单但一换到寄存器就发懵原因就在于定时器是个独立外设中断控制器又是另一套体系两者之间的衔接层被库函数藏得太深了。这篇文章就直接对着寄存器手册的第三章视角来拆解重点讲清楚定时器核心寄存器、中断优先级分组、以及一个可复现的定时中断工程。不是复读数据手册而是把我实际调试中踩过的坑和验证过的顺序写出来给正在走寄存器路线的人省点时间。无论你是刚学完GPIO想进阶的新手还是被HAL库的抽象搞到头大想回头看看硬件的人这一章的内容都能帮你把“中断”和“定时”这两个概念真正落地。1. 这一章为什么一定会讲到定时器和中断1.1 定时器在嵌入式开发里的“承上启下”地位在寄存器系列教材的编排里第三章几乎逃不开定时器不是没有原因的。前两章解决的是“让单片机动起来”的问题时钟配好、GPIO能输出高低电平。但从“动起来”到“按节奏动起来”中间就差一个高精度的时间基准。你当然可以写delay循环耗着但那会让CPU完全卡死什么都干不了。定时器存在的意义就是让硬件自己数数数到预设值就通知你一声CPU该干嘛干嘛去。这个“通知你一声”的机制就是中断。所以定时器和中断在第三章被绑在一起讲本质上是因为它们在实际工程里就是绑在一起用的。你把定时器配好却不开中断那计数器只是在后台默默翻转你永远不知道它什么时候溢出了你把中断开了却不会配优先级那多个中断同时来的时候程序的行为就会变得随机。这两个东西少了谁另一方的价值都体现不出来。1.2 这章内容对谁最有用如果你是那种习惯用HAL库做开发但总想搞清楚HAL_TIM_Base_Start_IT背后到底发生了什么的人这章会很有帮助。寄存器手写一遍定时器初始化你会瞬间理解为什么HAL的句柄里要维护那些回调函数指针、为什么更新中断标志要在中断服务函数里面手动清。因为硬件上更新事件到了中断标志位置1但如果你不去清零它就会反复触发中断。对于学完GPIO正准备学定时器的纯新手这章也能直接当上手教程看。我不写HAL不写标准库就从芯片存储器映射的角度把寄存器地址、位定义、配置顺序一步步拆开。你只要跟着搭一遍再回看库函数的实现会发现原来那些API只是对寄存器的“翻译”而已。思路一旦通了后面学PWM、输入捕获、编码器接口都是同一个套路。2. 定时器最核心的四个寄存器拿出来单聊2.1 PSC分频寄存器——把72MHz降到你敢用的程度定时器刚上电时它的时钟源是挂在APB1总线上的很多STM32平台默认给到72MHz。如果直接用这个频率去做1秒定时计数器得数72000000次才能到1秒自动重装载寄存器根本存不下这么大的数。所以有了PSC预分频器它的作用就是“先降频再计数”。PSC寄存器是一个16位的值实际分频系数是PSC 1。也就是说你往PSC里写71得到的就是72MHz除以72等于1MHz写7199得到的是10kHz。这里有个新人最常掉的坑PSC的16位上限是65535所以PSC1最高到65536如果你需要的时基比72MHz/65536还慢就得分两级处理要么把APB1的时钟本身预分频要么就不要追求单纯一次分频搞定。我自己的习惯是先把定时器时钟降到1MHz也就是PSC71这样ARR里写多少就是多少微秒。1MHz的计数频率对大多数应用来说精度够用又不会因为计数器翻转太快导致频繁进出中断占用CPU。2.2 ARR自动重装载寄存器——计数的终点线ARR决定了计数器要数到哪里才触发更新事件。向上计数模式下CNT从0开始递增当CNT和ARR相等时硬件会置更新标志位然后CNT归零重新开始。所以定时周期的公式是溢出时间 (PSC 1) × (ARR 1) / 定时器时钟频率注意这里两个地方都有“1”。PSC是因为分频系数从1算起ARR是因为计数器从0开始数。如果你想定时1秒在前面已经选定1MHz计数频率的前提下ARR要写999999。有些新手想当然地写1000000结果定时就成了1.000001秒短期看不出来累计久了时间就漂了。而且ARR也是16位寄存器最大值65535。这就意味着1MHz计数频率下最长一次溢出约65.5毫秒。要做更长周期怎么办往下看第五节可以用软件变量去“翻倍”。2.3 控制寄存器CR1和状态寄存器SR——启停与事件判断CR1里最重要的位就是CEN写1启动计数器写0停止。这个位简单但CR1里还有一个位在写PWM和定时中断时容易被忽略ARPE位自动重装载预装载使能。ARPE为0时你写ARR的值会立刻生效ARPE为1时写进去的值要等到本次溢出才生效。对于定时中断来说如果你在中断服务函数里动态改了ARR想调整频率建议把ARPE置1否则可能在计数器运行途中突然改了终点导致第一次周期是非预期的值。SR寄存器最重要的是UIF更新中断标志位。计数器溢出时硬件把UIF置1如果你开了更新中断CPU就会跳去中断服务函数。但标志位必须由软件清零硬件不会自动清。很多人第一次写定时中断程序遇到“进一次中断就再也出不来了”的现场九成是没清UIF。清零的方式也有讲究直接往SR寄存器的UIF位写0。因为SR是按位写0清标志的寄存器写1反而没效果这和GPIO的输出配置逻辑完全相反容易绕晕。还有一个EGR寄存器值得提一嘴它对调试特别有用。往EGR的UG位写1可以软件触发一次更新事件效果是CNT归零、SR里的UIF被置1。这意味着你可以在初始化流程里通过写UG来预触发一次更新确保首次计数周期的起点是对的而不用等硬件自然跑完第一个周期。3. 中断优先级NVIC寄存器里藏着全局仲裁逻辑3.1 中断使能的核心开关定时器产生更新事件之后信号不会自动进到CPU中间还隔着一个NVIC嵌套向量中断控制器。NVIC里面负责使能中断的寄存器是ISER全称中断使能寄存器。每个定时器通道对应的中断源在ISER里都有固定的位号不是你想用哪个数就用哪个数必须查手册里的中断向量表。这里就出现了一个新手最容易困惑的点你已经配置了定时器产生更新事件的硬件条件也没让SR里的UIF置1但程序就是不进中断服务函数。排查了一圈才发现原来是NVIC里对应的ISER位没写1。反过来说如果ISER位写了1但定时器那边的更新中断没有使能中断同样不会来。定时器和NVIC像两扇串联的门任何一扇没打开都走不通。3.2 抢占优先级和子优先级的坑NVIC不止管能不能进中断还管进了中断之后谁说了算。这里有两个概念抢占优先级和子优先级。抢占优先级高数值小的中断可以打断正在执行的抢占优先级低的中断这就是所谓中断嵌套而同抢占优先级的情况下子优先级决定了两个同时到达的中断谁先被处理但它不能打断正在执行的同级抢占优先级中断。STM32的优先级分组通过AIRCR寄存器里的PRIGROUP位段来配置。比如常见的主流说法是“2位抢占优先级、6位子优先级”实际效果就是把优先级编码空间切成了不同层次。你必须在程序早期就配置好分组因为一旦有中断在处理再变更分组会带来不确定的优先级重组这在产品代码里会造成很难查的偶发问题我自己就吃过这个亏。另外要特别留神很多平台的库函数默认把所有中断的优先级设成相同且抢占优先级和子优先级都按芯片复位值处理。如果你在多个外设中断里共享数据又没有好好设置抢占优先级表面上看每个中断都正常但一旦时序紧张低优先级的处理可能会被无限制地挤到后面造成“明明进了中断服务函数却感觉反应很慢”的现象。3.3 中断服务函数的名字和位置在寄存器开发里中断服务函数的名字不是随便起的。它必须和启动文件里的中断向量表定义的符号保持一致。比如某个定时器的更新中断在启动文件里叫TIMx_IRQHandler你写的函数也必须是这个名字否则链接时向量表还是指向库里面那个空函数你写的代码永远不执行。这个坑非常隐蔽因为编译器不会报错只是把你写的函数当成一个普通函数没有任何链接关系。解决方案有两个。要么直接按启动文件里的原样写要么在启动文件里把函数名改成你习惯的。我建议新手直接照抄启动文件的名字少折腾。在中断服务函数里第一件事就是判断标志位是不是你要的那个第二件事清标志位第三件事再处理业务逻辑。顺序不能乱先清标志位再处理业务在某些极端时序下能避免一个周期内丢失事件。4. 一个可以直接抄的定时中断工程LED按秒翻转4.1 第一步打开定时器时钟很多人上来就配定时器的寄存器结果写进去一点反应都没有。原因很简单定时器的时钟还没打开。在STM32里绝大多数外设的时钟默认是关闭的你得先在外设时钟使能寄存器里把对应位置1。// 假设总线时钟为72MHz定时器挂在APB1上 // 相应的时钟使能寄存器位需要按型号查手册设置 RCC-APB1ENR | RCC_APB1ENR_TIM2EN;这一步不做后面所有对TIM2寄存器的写入都是写了个寂寞因为根本没有任何时钟驱动这块数字逻辑。这也是寄存器开发的特点一切都要主动配置不会有库函数偷偷帮你把时钟开了。4.2 第二步配置PSC、ARR和更新事件时钟打开了接下来按顺序初始化定时器。我习惯的顺序是先设分频再设自动重装载值然后强制产生一次更新事件最后清掉更新标志。为什么要强制产生一次更新事件因为PSC和ARR的预装载缓冲机制需要一次更新事件才能让新值安全载入计数器否则首次启动计数器时用的可能是复位默认值。TIM2-PSC 71; // 72MHz / (711) 1MHz TIM2-ARR 999999; // 1MHz计数溢出周期1秒 TIM2-EGR | TIM_EGR_UG; // 软件触发更新事件装载PSC和ARR TIM2-SR ~TIM_SR_UIF; // 清掉更新标志位 TIM2-CR1 | TIM_CR1_ARPE; // 自动重装载预装载使能有人会把ARR的1算错我再强调一下ARR是终点数值CNT从0数到ARR总共经历了ARR1个计数周期。所以1MHz下要1秒ARR得是999999不是1000000。这一步错了定时周期永远是1.000001秒这个“差一点”的状态。4.3 第三步把事件接到NVIC定时器侧已经能产生更新事件了现在要打开NVIC侧的门。使能对应的中断通道并设置优先级。这里我建议在配置完所有时钟和定时器之后再打开NVIC。开早了也没事但如果你用的调试工具在初始化早期就被中断干扰分析启动流程时会很痛苦。NVIC_SetPriorityGrouping(0x03); // 实际位段分配取决于系统AIRCR配置 NVIC_SetPriority(TIM2_IRQn, 1); // 这里使用库函数只是为了让代码简单 NVIC_EnableIRQ(TIM2_IRQn);如果你手头没有CMSIS提供的这些便捷函数也可以直接写AIRCR和ISER寄存器但注意AIRCR有个VECTKEY字段必须写成固定值否则写入会失效。这属于那种手册不会特意强调但实际一定会踩的细节。4.4 第四步启动计数器和中断总开关回到定时器侧把CR1的CEN位置1计数器开始跑。这里还有个CPU全局中断的开关要打开通常是一个叫PRIMASK的寄存器对应CMSIS里的__enable_irq()。如果你把NVIC该配的都配了但忘了开这个总开关中断同样不会执行。这三个层级关系是这样CPU默认关着全局中断NVIC只是仲裁器定时器UIF只是来源三层全部打通中断服务函数才会被调用。TIM2-CR1 | TIM_CR1_CEN; __enable_irq();中断服务函数里做LED翻转void TIM2_IRQHandler(void) { if (TIM2-SR TIM_SR_UIF) { TIM2-SR ~TIM_SR_UIF; GPIO-ODR ^ (1 5); // 假设某GPIO口的PIN5接LED } }这段代码已经是我实际用过的最小可跑示例把LED接在对应引脚烧录进去就应该看到1Hz翻转。如果你确实看到那个“一秒一亮一灭”跑起来了恭喜你中断和定时器这两大件你已经打通了。5. 新手最容易踩的五个坑以及排查顺序5.1 中断不触发先查时钟再查NVIC遇到定时中断不触发我建议按这个顺序排查先查定时器的时钟有没有开再看CNT到底动没动然后查SR的UIF有没有置1之后查NVIC的ISER对应位有没有使能最后查全局中断有没有开。把这个顺序记住比你对着寄存器手册发呆半小时有效率得多。我见过一个现场是代码写得天衣无缝就是定时器时钟用错了使能位。MCU挂了两条总线有些型号的TIM2在APB1有些在APB2如果复位默认定的定时器模块不同开的始终是APB1的位实际跑的TIM2根本没上电。这种就是硬件连接层面的错纯查软件永远查不出来。5.2 时间算不准八成是“1”没加PSC和ARR各自都有“1”的问题我已经反复强调了但真正容易错的不是这一层而是总线的时钟倍数。很多平台默认APB1的定时器时钟不是直观的72MHz而是总线时钟经过了一个倍频器后的结果。比如APB1预分频不为1时挂在其上的定时器时钟会翻倍。这样你明明算出来PSC71对应1MHz实际却可能是2MHz时间直接快了一倍。解决起来很简单把那个倍频关系搞清楚。开场先做一个闪烁LED的实验用示波器或逻辑分析仪实测一下周期确认计数频率再继续往下写。不要偷懒假定复位时钟树就是最大频率那样后面所有定时计算都建立在沙子上。5.3 中断标志位清不掉方向反了前面说过SR的清标志方式是写0清除。但很多人在GPIO那边写惯了习惯性往SR里写1去“置位”某个位来清除中断结果就是标志位纹丝不动程序卡死在中断里。这种问题看起来玄学其实就是读写方向和硬件设计逻辑的关系。定时器SR这类寄存器的一个特点是写1不清零写0才清零这和常规思维正好相反你一定要在配置里形成肌肉记忆。另外关于“到底什么时候清标志”也有讲究。如果你在进中断服务函数之后、处理业务之前清可以防止极端情况下新事件和你的清零操作交错导致丢中断。但如果你在业务处理之后清就得注意处理过程耗时不能超过一个定时周期否则你就会看到定时器明明还在中断里UIF又置位了程序永远跳不出去。5.4 优先级配置顺序不对系统跑飞了优先级分组一旦在运行期间改动会有极低概率导致中断向量错乱或者优先级反转。正规产品代码里优先级分组应该放在系统初始化最开始就在时钟配置旁边。之后所有外设中断的优先级分配都基于这个分组不变来设置。如果你把分组放在某外设初始化之后后续中断一旦发生硬件按旧分组解析优先级软件按新分组往寄存器里写两边对不上实时系统就会出现不可复现的故障。5.5 调试中断程序时硬件断点惹的祸这是调试器使用上的坑。你在中断服务函数里打了硬件断点单片机触发断点时定时器还在后台跑。等你点继续按说一切应该恢复正常但很多调试器在恢复那一刻会产生一个短暂的暂停脉冲正好让一个更新事件被错过或者是断点长时间停留导致UIF多次置位只能再进一次。表面上就是你“什么都没动”但程序行为变了。换成软件断点或者直接在中断里加一个全局变量自增逻辑再通过调试器观察变量能规避掉这个问题。6. 从定时中断出发还能延伸出哪些玩法6.1 把更新事件换成输出比较就有了PWM定时器的更新事件只是它能力的一小部分真正在电机控制里用得更多的是输出比较模式。那时就不是自己清UIF等中断了而是让定时器在CNT达到某个比较值的时候自动翻转某个引脚的电平这就是PWM的原始雏形。寄存器手册第三章作为基础一般只会讲更新事件但如果你能理解CNT和ARR的计数机制再去看CCR比较寄存器和CCMR输出模式配置难度会小很多。PWM频率由PSC和ARR一起决定占空比由CCR和ARR的比值决定本质还是那几个“1”的问题。6.2 换一个方向就能做输入捕获测频率如果你把定时器的输入引脚配置好再把CCMR的输入捕获模式打开定时器就能测量外部信号的脉宽或周期。原理是信号边沿到了捕获引脚硬件就把当前CNT的值锁存到CCR寄存器里之后你去读CCR就知道两次边沿之间的计数值再结合PSC频率就能算出信号频率。不少非接触式测速、遥控接收解码都是这个套路。这也是沿着第三章内容继续学下去自然会碰到的方向。6.3 第四章乃至更后面会逐渐进入现代MCU的复杂外设等定时器、中断、PWM、输入捕获都滚了一遍后面的章节大概率就是通信外设了串口、I2C、SPI。这几个外设的寄存器虽然各有各的怪脾气但配置思路仍然是那几步开时钟、配引脚复用、配控制寄存器、配中断、使能。你如果在第三章就把这个套路练熟后面无非是照着手册把寄存器的名字换一换而已。我看过很多学寄存器开发的人卡就卡在第三章这一个坎上。前两天还在点灯突然就要面对定时器、中断优先级、NVIC和一堆ITM调试概念信息量一下涌过来确实容易消化不良。但只要你在板子上把那个LED秒翻转跑通了前面所有的抽象概念都会突然有了附着点。这就是嵌入式的特点理论堆一堆不如实际烧一板寄存器动手跑通一次比看十遍手册都管用。说到底嵌入式开发的底层拼的就是这种对寄存器硬件的理解沉淀。你以后可能还是会回到库函数甚至HAL开发但带着这层理解去写定位问题、优化时序的能力完全不一样。第三章是分水岭翻过去之后前面就是一片开阔地。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑