资讯动态

STM32寄存器开发:中断与定时器白话手册,附工程示例

发布时间:2026/10/21 2:07:09 来源:尧图企业网站定制
搞STM32寄存器开发最劝退的不是GPIO而是中断和定时器。GPIO再复杂也就是查表配寄存器逻辑是直的中断和定时器这东西牵扯到时钟树、总线分频、NVIC、预分频、自动重装载名词一个接一个很多人在这个十字路口果断放弃寄存器路线转投库函数。我当年也是一路踩坑过来的。这篇“白话手册”第二章咱们就把STM32的中断系统和定时器掰开揉碎用大白话把寄存器配置讲清楚最后给出可以直接抄作业的工程级示例。这章适合两类人一类是刚点完LED、准备往深处走的新手一类是在库函数里绕了很久、始终觉得底层不踏实的老同学。读完你会明白中断服务函数的名字为什么不能乱起定时时间为什么总是差一倍PWM占空比到底改的是哪个寄存器。准备好了就往下看全是实操经验。1. 这一章我们要解决什么问题1.1 为什么第二章偏偏是中断和定时器第一章节通常会带着大家把GPIO玩熟点灯、按键扫描、I2C模拟时序都有了。但点灯只是单向输出真正的嵌入式系统是事件驱动的串口来了数据要立刻处理电机到了指定角度要马上停下按下按键系统要切换到另一个工作模式。这些场景靠死循环轮询当然也能做但CPU会被拖死而且实时性完全没保证。解决这个问题的两个硬件基础就是中断和定时器。中断解决的是“突发事件来了怎么办”定时器解决的是“到了指定时间该干什么”。两个配合起来才能搭起一个像样的嵌入式系统框架。我用寄存器而不推荐一上来就库函数原因很简单库函数封装得太严实了它一个函数帮你把PSC、ARR、NVIC全配好你不清楚里面发生了什么一旦定时时间不对、中断进不去只能干瞪眼。寄存器方式的好处是每一步都看得见摸得着你写下的每一个1、每一个0都对应手册里实实在在的位。出了错还能顺着配置序列倒推回去。1.2 寄存器思维的一次升级从轮询到事件驱动在GPIO时代CPU是主人外设是奴隶CPU主动去读外设状态寄存器这个动作叫轮询。到了中断和定时器角色反过来了外设变成主动方定时器计到数了自己发一个中断请求外部引脚电平跳变了自己触发一个事件。CPU被“打断”后暂停手头的任务跳去执行中断服务函数处理完再回来继续干活。这个“暂停-跳转-返回”的机制底层就是NVIC和向量表在做调度。很多人以为配置中断就是“使能一下”其实完整的使能链有三个环节外设自己要产生中断事件、NVIC这个调度室要允许这个事件进来、CPU内核要能从向量表里找到对应的处理函数。三个环节缺一个中断就静悄悄。这一步想通了后面看任何外设的中断配置都不再犯迷糊。2. 中断系统白话解剖2.1 一条中断从发生到被处理的完整路径我画过无数次这条路径现在用文字讲清楚。比如定时器2计数溢出它先把自身状态寄存器TIMx_SR的更新标志位置1告诉所有人“我数到头了”。接着这个标志位要经过TIMx_DIER里对应的中断使能位这道“门”门开着才允许往外发请求。请求信号到达NVIC后NVIC会看这个中断通道是否被ISER寄存器使能、当前优先级是否允许抢占综合判断后向CPU内核发送中断请求信号。CPU响应后会自动从内存顶部的向量表中取出对应的中断服务函数入口地址跳过去执行。执行完毕CPU恢复之前被打断的现场继续跑主循环。整个链条里外设标志位、中断使能位、NVIC通道三个点都要配通。我见过太多人只配了NVIC忘了开外设中断源结果中断从头到尾没影也见过只清了NVIC挂起位忘了清外设标志位导致中断函数一退出又立刻进来表现为“卡死在中断里”。记住外设标志位是源头NVIC挂起位是传输过程中的中间状态两个都要处理干净。2.2 NVIC到底配了什么NVIC的全称是嵌套向量中断控制器属于ARM Cortex-M内核自带的部件所有STM32芯片都靠它来管理中断。寄存器层面真正要操作的只有三类ISER中断使能寄存器向对应位写1打开某个中断通道。ICER中断除能寄存器向对应位写1关闭某个中断通道。IPR中断优先级寄存器配置抢占优先级和子优先级。以STM32F1为例TIM2的外部中断号是28所以使能TIM2中断就是往NVIC-ISER[0]的bit28写1。这里有个规律Cortex-M3的ISER每个寄存器是32位管理32个中断号ISER[0]管0到31号ISER[1]管32到63号。所以EXTI15_10中断号是40就要操作ISER[1]的bit8因为40减去32等于8。这个换算关系并不复杂但现场写代码时特别容易算错我建议直接在工程里定义一张中断号映射表写代码时对照查。优先级配置同样关键。Cortex-M3支持通过SCB-AIRCR的PRIGROUP位把优先级位拆分成抢占优先级和子优先级两部分。最常用的配置是全部作为抢占优先级也就是4位有效。IPR寄存器每个中断通道占一个字节但只有高4位有效低4位读出为0。数值越小优先级越高0是最高优先级。中断嵌套的规则是只有抢占优先级更高的中断才能打断当前正在执行的中断服务函数。把高频、紧急的中断优先级设高把低频、非关键的中断优先级设低这个习惯能省掉你很多查不出来的逻辑问题。2.3 中断服务函数的名字为什么不能乱写新手最容易栽的坑就在这里写了一个Timer2_Handler函数中断就是不执行。原因在于Cortex-M的启动文件里已经定义了一张向量表表里写明了每个中断号对应的默认函数名比如TIM2_IRQHandler。你写的函数必须叫这个名字链接器才会把函数地址填进向量表CPU响应中断时才能跳得进来。名字差一个字母、大小写不对中断来了CPU还是跳到启动文件里的默认空函数你的代码根本没机会跑。这个机制在编译原理上叫“弱符号”启动文件里那个默认处理函数是弱定义你只要写一个同名的全局函数链接器就会用你的强定义覆盖它。所以写中断服务函数前先去启动文件里查确切的名字这是最稳的做法。另外还有一个容易忽略的点中断服务函数里如果定义了大数组或者用到了浮点运算要留意栈空间是否足够。Cortex-M3的中断嵌套会消耗栈中断层级一多栈溢出就是灾难。3. 定时器计时原理与配置实操3.1 定时器时钟乌龙事APB1分频的那个坑先讲一个我实际调了两天才搞明白的问题。STM32F103的主时钟最大72MHzAPB1总线时钟默认是36MHzTIM2、TIM3、TIM4这些通用定时器都挂在APB1上。手册里有一句话特别容易忽略如果APB1预分频系数不是1那么定时器时钟频率等于APB1时钟频率的2倍。换句话说APB1是36MHz时TIM2的时钟实际是72MHz。我当年按36MHz去算预分频定时时间整整差了一倍查了半天才发现罪魁祸首是这句话。为什么会设计成这样因为APB1上的外设时钟被限制在36MHz以内但定时器需要更高的工作频率来保证计数精度芯片内部干脆给定时器单独做了个倍频器。这算是一种特殊的“照顾”。实际开发中别凭感觉猜定时器时钟去数据手册里查这篇芯片的时钟树把SYSCLK、AHB预分频、APB1预分频、APB2预分频一条条捋清楚写在代码注释里。多花两分钟看时钟树能省下两天调时间误差的功夫。3.2 定时器三大寄存器PSC、ARR、CNT通用定时器的核心逻辑就是计数器CNT在时钟脉冲驱动下不断加1加到头触发事件再重新开始。围着这个核心有三个寄存器要搞明白TIMx_PSC预分频寄存器把定时器输入时钟按比例降频。比如72MHz时钟PSC写7199实际分频是7200计数频率变成10kHz。TIMx_ARR自动重装载寄存器决定计数的目标值。CNT从0数到ARR再回来产生更新事件。TIMx_CNT计数器本身记录了当前计到哪了。定时时间公式是定时时间 (PSC 1) × (ARR 1) / 定时器时钟频率。为什么都要加1因为PSC和ARR都是从0开始数的PSC写0就是1分频ARR写0就是计1个数。这个1的细节也是新手最容易丢的丢了之后定时时间永远差一个倍数。我举个例子我要定时1秒定时器时钟72MHz先定PSC为7199分频后计数频率10kHz也就是每0.1ms计数一次再定ARR为9999计满10000次也就是1秒。两个值相乘刚好1秒。你用公式验证(71991) × (99991) / 72000000 1秒严丝合缝。3.3 定时中断的完整寄存器配置序列这里以TIM2为例完整走一遍配置流程代码可以直接抄。第一步开启TIM2的时钟。TIM2挂载在APB1总线上对应RCC-APB1ENR寄存器的bit0所以RCC-APB1ENR | (1 0); // 使能TIM2时钟第二步配置PSC和ARR。我经常看到有人把这两行顺序写反虽然不影响最终结果但规范起见还是先PSC再ARRTIM2-PSC 7200 - 1; // 72MHz / 7200 10kHz TIM2-ARR 10000 - 1; // 10kHz * 10000 1秒这里有个关键地方PSC和ARR写入后不会立刻生效而是先存在预装载寄存器里要等一次更新事件才会真正装载到影子寄存器中。所以第三步我通常手动产生一次更新事件让配置立刻生效TIM2-EGR | (1 0); // UG位置1立即产生更新事件 TIM2-SR 0; // 顺便把更新标志位清零第四步开启更新中断。TIM2-DIER的bit0是更新中断使能位写1打开TIM2-DIER | (1 0); // 允许更新事件触发中断第五步配置NVIC。TIM2中断号28优先级的抢占优先级设2NVIC-ISER[0] | (1 28); NVIC-IP[28] 2 4; // 高4位有效第六步最后让定时器跑起来。TIM2-CR1的bit0是计数器使能位写1启动TIM2-CR1 | (1 0);中断服务函数长这样void TIM2_IRQHandler(void) { if (TIM2-SR (1 0)) { TIM2-SR ~(1 0); // 清零更新标志位必须清 // 这里放你的周期任务比如翻转LED GPIOA-ODR ^ (1 5); } }注意注释里那句“必须清”不清标志位意味着中断标志一直挂起主程序刚退出中断标志还在马上又进一次看起来就像程序卡死。这个问题我排查过不下十次。3.4 还有一个轻量级定时器SysTickSysTick是Cortex-M内核自带的系统定时器严格来说不属于芯片外设定时器但它做1ms时基非常好用。寄存器就四个控制与状态寄存器CTRL、重装载值寄存器LOAD、当前值寄存器VAL、校准寄存器CALIB。配置一个1ms时基的SysTick只需要两行SysTick-LOAD 72000 - 1; // 72MHz时钟下72000个周期1ms SysTick-VAL 0; // 清空当前计数 SysTick-CTRL 0x07; // 使能、开启中断、选择HCLK时钟源中断服务函数名为SysTick_Handler在函数里累加一个毫秒计数器就能搭建系统的心跳volatile uint32_t ms_counter 0; void SysTick_Handler(void) { ms_counter; }SysTick和TIM系列各有分工。SysTick适合做操作系统节拍、delay延时、全局时基硬件定时器适合做PWM、输入捕获、编码器接口等需要复杂功能的场合。两者不冲突后文PWM的例子还要靠TIM来做。4. PWM输出把定时器用出花来4.1 PWM的四个寄存器模式、极性、比较值、预装载PWM本质上是定时器比较输出功能的扩展。计数器CNT不断累加和比较寄存器CCR里的值比较CNT小于CCR时输出一种电平大于CCR时输出另一种电平。占空比就是CCR和ARR的比值公式占空比 CCR / (ARR 1)。所以想要变占空比核心就是改CCR。配置PWM输出需要四个环节TIMx_CCMR1里的OC1M位设置通道1工作在PWM模式1还是模式2。TIMx_CCER里的CC1E位使能通道1的PWM输出。TIMx_CCR1寄存器写入比较值决定初始占空比。TIMx_CR1里的ARPE位使能自动重装载预装载避免更新瞬间出现占空比跳变。此外还要把对应的GPIO引脚设置成复用推挽输出。以TIM3_CH1默认映射到PA6为例GPIOA-CRL要配置PA6为复用推挽输出。GPIO寄存器里每4个位控制一个引脚高两位是CNF配置低两位是MODE模式。复用推挽输出对应的配置是CNF10、MODE11合起来0xBGPIOA-CRL ~(0xF 24); GPIOA-CRL | (0xB 24);4.2 呼吸灯实例改一个值就行呼吸灯就是让PWM的占空比从0%慢慢升到100%再从100%慢慢降回0%。频率固定1kHz占空比的变化频率大约1秒一个循环。TIM3挂载在APB1上时钟照样是72MHz。取PSC为71ARR为999PWM频率就是72MHz除以72再除以1000等于1kHz。// 配置TIM3为PWM模式1通道1输出 TIM3-PSC 72 - 1; TIM3-ARR 1000 - 1; TIM3-CCMR1 | (0x6 4); // OC1M110PWM模式1 TIM3-CCMR1 | (1 3); // OC1PE1输出比较预装载使能 TIM3-CCER | (1 0); // CC1E1使能通道1输出 TIM3-CR1 | (1 7); // ARPE1自动重装载预装载使能 TIM3-EGR | (1 0); // 立即产生更新事件 TIM3-CR1 | (1 0); // 使能计数器主循环里用SysTick提供的毫秒时基改变CCR值做线性渐变volatile uint16_t duty 0; volatile int8_t dir 1; while (1) { if (ms_tick()) { // 简单示意每毫秒执行一次 if (dir 1) { duty; if (duty 1000) dir -1; } else { duty--; if (duty 0) dir 1; } TIM3-CCR1 duty; // 修改比较值改变占空比 } }跑起来之后用示波器看PA6引脚能看到脉冲宽度在周期性地拉长再缩窄。这个例子虽然简单但PWM调占空比这套逻辑是以后驱动舵机、控制LED亮度、调速电机的共同基础。把CCR的那条赋值语句理解透其他PWM外设都是一个套路。5. 外部中断EXTI按键也配拥有姓名5.1 EXTI整体流程从引脚到中断服务函数EXTI模块负责处理外部引脚的中断触发。STM32把16个引脚分为EXTI0到EXTI15每条EXTI线对应一个引脚编号但同一时间只能有一个引脚映射到同一条线。这就意味着PA0和PB0不能同时使用EXTI0想都用就必须选两条不同的EXTI线。配置外部中断的完整链条是引脚上的电平变化经过GPIO和AFIO的引脚选择到达EXTI模块通过边沿检测电路识别到上升沿或下降沿置起挂起寄存器EXTI_PR的标志。这个标志再经过EXTI_IMR中断屏蔽寄存器放行送到NVICNVIC再向CPU发请求。所以配置寄存器至少有五个RCC使能AFIO时钟和GPIO时钟。AFIO_EXTICR选择具体引脚到EXTI线。EXTI_IMR设置不屏蔽对应线。EXTI_RTSR或者EXTI_FTSR设置触发边沿。NVIC使能对应中断通道。5.2 按键外部中断完整配置PA0按一下闪一下以PA0作为按键输入下降沿触发为例。禁言先把AFIO时钟和GPIOA时钟使能配置PA0为上拉输入然后选通AFIO的EXTICR1寄存器让EXTI0关联到PA0最后配置EXTI和NVIC。// 1. 开启时钟 RCC-APB2ENR | (1 2); // GPIOA时钟 RCC-APB2ENR | (1 0); // AFIO时钟 // 2. PA0配置为上拉输入 GPIOA-CRL ~(0xF 0); GPIOA-CRL | (0x8 0); // CNF10上拉/下拉输入MODE00输入模式 GPIOA-ODR | (1 0); // 上拉 // 3. AFIO选通EXTI0映射到PA0 AFIO-EXTICR[0] ~(0xF 0); // 4. EXTI配置 EXTI-IMR | (1 0); // 不屏蔽EXTI0 EXTI-FTSR | (1 0); // 下降沿触发 EXTI-PR | (1 0); // 初始化时清一次挂起标志 // 5. NVIC配置 NVIC-ISER[0] | (1 6); // EXTI0中断号6 NVIC-IP[6] 3 4; // 抢占优先级3中断服务函数里处理按键void EXTI0_IRQHandler(void) { if (EXTI-PR (1 0)) { EXTI-PR | (1 0); // 清挂起标志写1清除 flag_key_pressed 1; // 只置标志不在中断里做耗时操作 } }在外面加按键消抖的逻辑用Timer或者SysTick延时20ms再读取一次按键电平确认还是低电平才视为有效按压。注意绝对不要在中断服务函数里做长时间延时中断期间系统其他事情全部被晾着一次没关系按键频繁按下来系统就瘫痪了。5.3 按键抖动的软件消抖经验机械按键的抖动是硬件上客观存在的现象按下瞬间电平会来回跳变几十毫秒。用EXTI下降沿触发时一次按压可能触发三四次中断处理不好会出现一次按压系统响应三遍的怪毛病。软件消抖的思路是检测到第一次下降沿后先不立即处理业务逻辑而是记录触发时间随后用定时器每隔10到20ms查一次引脚电平如果持续确认低电平才判定为一次有效按键。最实用的方案是“定时器轮询消抖标志位”中断服务函数里把EXTI_PR清零后启动一个20ms的计时标志主循环里计时到期后读PA0确认还是低执行按键逻辑。这样既保证了中断响应的及时性又避开了在中断里延时。我踩过的坑是贪图省事在中断服务函数里直接写了几个delay_ms(20)结果系统调度直接乱掉排查半天才醒悟。6. 常见问题排查与经验教训6.1 中断没响应排查看哪里中断不进服务函数是寄存器开发里最常被问到的问题按顺序排查基本都能找到根源服务函数名是否和启动文件里完全一致。TIM2_IRQHandler少一个字母就静默失效。外设中断源是否打开。TIM要开DIEREXTI要开IMR串口要开CR1里的对应位。NVIC通道是否打开。外设能发中断NVIC不放行也没用。优先级是否配置合理。目标中断的抢占优先级如果比当前正在执行的中断还低它就得排队等着。中断标志位是否溢出在SR里。SR标志没清中断会反复进入但标志没置起来中断永远不会来。我自己的习惯是遇到中断不响应先只用“翻转LED”做测试进中断函数后翻转一次LED的IO电平。逻辑分析仪或者调试器观察IO波形比打印调试信息直观得多。中断这种偏底层的机制示波器、逻辑分析仪永远是最值得信赖的伙伴。6.2 定时时间不对的排查方向定时时间不对最常见的是时钟源和1问题。如果实测时间是预期时间的一半多半是APB1分频后忘了2倍这件事如果时间差了一个倍数多半是PSC或ARR忘记减1。还有一类隐蔽问题初始化顺序不对PSC和ARR更新发生在启停之后导致第一轮周期的数值是旧值。把EGR | (1 0)放在初始化末尾手动产生一次更新事件是最保险的做法。另外如果用了RTOS或者中断嵌套比较深的工程要注意定时器中断服务函数里的代码执行时间。如果代码执行时间超过了定时器周期下一次中断到来时上一次还没处理完时序就乱了。这种问题在现象上表现为“定时越来越慢”本质上不是定时器不准而是中断服务函数过载。6.3 调试中断和定时器的几个实用技巧第一点强调一遍中断服务函数里只做“置标志位、清标志位、读寄存器”这类短操作真正业务逻辑放主循环。这个原则能解决绝大多数系统卡死问题。第二点用调试器看外设寄存器窗口。几乎所有的IDE调试器都支持查看外设寄存器实时值比如看TIM2的SR位是否等于1、NVIC的ISER是否真的把目标位置起来了。开发阶段多开这个窗口比printf有用得多。第三点配置完寄存器后手动触发一次事件验证通路。比如定时器配置完后置位UG位观察SR的更新标志是否被置起EXTI配置完后可以用杜邦线手动触碰一下引脚模拟一次边沿。先验证硬件通路再跑业务逻辑定位问题会快很多。最后说点大实话我自己在摸索寄存器开发的阶段最大的收获不是背会了多少寄存器的位定义而是建立了一套“事件响应”的思维方式。中断和定时器这套东西最开始觉得复杂是因为它们同时牵扯到时钟树、外设寄存器、内核NVIC三层逻辑但一旦把路径图在脑子里画清楚再去看其他外设的中断配置、PWM配置、DMA配置都是一通百通。这章篇幅不短但如果你能自己动手跑完定时中断、PWM呼吸灯、按键中断这三个例程STM32寄存器开发的地基就算彻底打牢了。下一章如果继续写大概率要聊串口USART和DMA那又是一个让人又爱又恨的模块。实操中如果真遇到某个寄存器配置半天不生效别硬扛去看看参考手册对应章节的“寄存器描述”和“编程要点”大部分答案都写在里面。祝各位早日熟练驾驭寄存器回头再看库函数时会有一览众山小的感觉。

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

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

免费获取报价 →
↑