资讯动态

STM32寄存器编程:外部中断、定时器与PWM配置实战

发布时间:2026/10/14 16:51:44 来源:尧图企业网站定制
接着上一章的进度这次我们把手伸向中断系统和定时器。说实话这两块是很多人从“点灯”进阶到“干活”的分水岭也是寄存器操作里最容易让人一头雾水的地方。ST的手册写得非常严谨严谨到一句话里塞了三个缩写、两个限定条件、一个“取决于应用需求”翻开第N章的那一刻不少人直接选择了放弃。这一章的目标很明确不靠HAL库直接用寄存器把外部中断、定时中断和PWM这三件事调出来。你不需要把参考手册背下来只需要搞懂“哪几个寄存器必须碰、按什么顺序碰、碰错了会怎样”。等你把这一章的内容吃透再回头看官方手册的中断章节会觉得那些长篇大论突然都变得有血有肉了。这一章的内容适合已经能熟练操作GPIO、对时钟树有基本概念的读者如果你还停留在照抄例程的阶段跟着本文走一遍收获会比想象中大。1. 先从地图说起这一章到底要碰哪些寄存器1.1 中断链路从引脚到CPU中间隔着两道门很多人第一次调外部中断时习惯把“引脚检测到电平变化”和“CPU跳进中断函数”这两件事混为一谈然后就开始怀疑人生。其实从引脚到CPU之间隔着两道门第一道门叫EXTI外部中断控制器第二道门叫NVIC嵌套向量中断控制器。EXTI负责监视引脚电平边沿它检测到了变化会把一个挂起标志位置1然后向NVIC发出请求。NVIC相当于CPU门口的调度员只有它点头放行CPU才会停下当前任务跳到中断向量表里对应的那个函数。所以检查中断不生效时先别急着怀疑代码逻辑先确认这两道门是不是都打开了。有个很反直觉的点是EXTI并不直接挂在某个总线上配置它不需要单独开时钟但配置引脚和EXTI的对应关系需要用AFIO复用功能IO模块而AFIO是挂在APB2总线上需要开时钟的。这个坑我踩过后面第2章会专门提醒。1.2 定时器家族SysTick和TIMx先分清谁是谁定时器从功能上大致分两类一类是内核自带的SysTick另一类是芯片外设的TIMx系列。SysTick是一个24位递减计数器它的存在几乎就是为了操作系统做节拍或者延时用的简单、粗暴、不花哨。TIMx则复杂得多有基本定时器、通用定时器、高级定时器之分支持输入捕获、输出比较、PWM、编码器接口等花哨功能。寄存器手册里会把它们分成好几章分别介绍但实际上手的时候没必要分得太细。你只需要记住SysTick的寄存器非常少主要是CTRL、LOAD、VAL三个TIMx的寄存器虽然多但核心组合拳也就那么几个——PSC、ARR、CNT、CR1、SR、DIER、CCR、CCER。这就像学做饭调料架上有几十瓶料但日常做菜常用的就那五六种先把它们用熟其他的是后话。1.3 配置前的思维切换HAL封装掩盖了什么如果有人已经用HAL库调通过中断和定时器再回到寄存器会觉得很不适应因为HAL把你伺候得太好了。以定时器为例HAL库一个初始化函数内部会帮你做几十次寄存器读写包括时基结构体校验、时钟使能、中断使能、软件触发更新事件、清除标志位等等。这些操作本身没什么秘密但问题在于一旦出了故障你根本不知道是哪一步没生效。寄存器方式强迫你把这几十步拆开一步一步做。每步操作的因果关系变得非常清晰开了时钟定时器才能计数配置了PSC分频才生效设置了DIER更新中断才被允许上报写CR1的CEN位定时器才正式启动。这种思维切换可能让人初期觉得繁琐但一旦养成习惯排查问题会变得极其迅速。2. 外部中断实操从GPIO到NVIC的完整寄存器链2.1 第一步GPIO模式配置以一个按键为例按键接在PA0引脚低电平有效也就是说按下时引脚被拉到地。我们需要把PA0配置成输入模式并且开启上拉电阻这样平时引脚是高电平按下时变成低电平松开后自动回到高电平。在寄存器层面PA0属于端口A的低8位对应GPIOA的CRL寄存器每4位控制一个引脚的模式和配置。PA0的CNF[1:0]要设为10上拉/下拉输入MODE[1:0]设为00输入模式。具体代码如下RCC-APB2ENR | RCC_APB2ENR_IOPAEN; // 打开GPIOA的时钟 GPIOA-CRL ~(0xF 0); // 先清空PA0的4位配置 GPIOA-CRL | (0x8 0); // CNF10, MODE00 GPIOA-ODR | (1 0); // ODR写1选择上拉注意最后一行ODR的设置很多人会觉得输入模式不关心ODR其实对于上拉/下拉输入ODR就是决定上拉还是下拉的开关ODR对应位写1是上拉写0是下拉。这一步漏了你的按键可能永远读不到期望的电平。2.2 第二步EXTI的触发电平与屏蔽控制引脚模式配好了接下来要让EXTI知道你监视的是PA0并且只对下降沿感兴趣。这里有两组寄存器要操作AFIO-EXTICR是把某个引脚映射到某个EXTI线EXTI-RTSR/FTSR是配置触发边沿。PA0对应的映射关系比较特殊端口A的0号引脚正好对应EXTI0所以AFIO-EXTICR[0]的EXTI0[3:0]位段设为0000就能选中PA0。如果换成PB0这里就要设为0001。这个配置尤其坑人因为很多入门板子恰好有PA0-PA15的引脚而EXTI线只有16条多条引脚映射到同一条线上时只允许同时选中一个映射写错了中断根本不触发。接着配置EXTI。我们想要下降沿触发也就是电平从高变低的瞬间产生中断请求AFIO-EXTICR[0] ~(0xF 0); // EXTI0选择PA0 EXTI-IMR | (1 0); // 取消屏蔽EXTI0 EXTI-FTSR | (1 0); // 使能下降沿触发写这段代码时有一件事值得提醒IMR的全称是Interrupt Mask Register它是“屏蔽控制寄存器”不是“使能寄存器”。位写1是不屏蔽放行写0是屏蔽拦截。如果你习惯性地认为写0是禁止、写1是使能那没问题但如果你把逻辑反过来直接取反去写事情就变得很诡异。2.3 第三步NVIC的通道使能与优先级EXTI这边的请求已经准备好了但它还在门口等着因为NVIC这道门没开。每个中断源在NVIC里都有一个对应的通道号EXTI0对应的通道在向量表里通常排在IRQn的某个编号。使能这个通道需要两步在NVIC-ISER里把对应位置1在NVIC-IPR里设置抢占优先级和子优先级。NVIC-ISER[0] | (1 EXTI0_IRQn); // 使能EXTI0中断通道 NVIC-IPR[EXTI0_IRQn] (1 4) | (0 0); // 抢占优先级1子优先级0这里有一个初学者经常忽略的关键点NVIC的IPR寄存器每个通道占用8位但具体哪几位有效取决于你在NVIC优先级分组里配置了几位抢占、几位子优先级。默认情况下Cortex-M3内核的AIRCR寄存器里优先级分组可能没有被初始化这时软件优先级分组的效果并不明确。所以第4章我们会专门讨论优先级分组的配置顺序它在整个中断系统里扮演着老板的角色。2.4 中断服务函数与标志位处理通道使能后中断来了CPU就会跳转到中断服务函数。对于EXTI0服务函数名在启动文件里固定为EXTI0_IRQHandler你在任意C文件里实现同名函数即可void EXTI0_IRQHandler(void) { if (EXTI-PR (1 0)) // 检查EXTI0的挂起标志 { EXTI-PR | (1 0); // 清零挂起标志 // 这里写你的按键处理逻辑 } }注意最后一行往EXTI-PR的对应位写1才会清除挂起标志这是个“写1清0”的寄存器不是说写0就能清。如果你用HAL库时心里一直认为“写0是清除”的习惯在这里就会栽跟头。另外如果不清除这个标志即使NVIC已经响应过这次中断EXTI还会一直保持请求状态产生的后果就是中断退出后立刻再次进入淋漓尽致地表现出一只“复读机”的执着。2.5 排查顺序自己动手一次中断从头到尾的验证把代码烧进板子后发现按键没反应是很多人的噩梦。我调试这类问题通常按照“从外设往内核”的顺序排查先看GPIO是否配置正确、引脚电平是否正常再看EXTI-PR挂起位有没有置1然后看NVIC-ISER里对应位有没有写进去最后看CPU是否真的进入了中断函数。在调试器里直接查看这些寄存器是非常直观的方法比打印日志高效得多。另外有个细节程序刚复位时如果之前中断标志位没有清干净有可能在配置EXTI的瞬间就触发一次假中断。所以有些工程师习惯在配置EXTI之前先把PR寄存器写一遍清干净EXTI-PR 0xFFFFFFFF; // 复位所有挂起标志防止假触发这一步虽然不强制但在实际工程里能挡住不少奇怪问题建议养成习惯。3. 定时器寄存器组合拳PSC、ARR、CR1、SR是怎么配合的3.1 定时器的基本计数组件从纯寄存器角度看定时器就像一个带分频器的计数器。CNT是当前计数值ARR是自动重装值PSC是预分频值时钟经过PSC分频后变成CNT的计数脉冲CNT数到ARR时产生一次更新事件然后CNT清零重新开始。你用这个行为可以实现定时中断每隔固定时间进入一次中断函数。这里有个容易过得糊涂的细节PSC和ARR的取值是分频系数减1、重装值减1。比如系统时钟72MHzPSC写71那么定时器计数频率就是1MHz也就是每1微秒CNT加1ARR写999CNT从0数到999需要1000个计数脉冲所以定时周期是1毫秒。计算式很简单定时周期 (PSC 1) * (ARR 1) / 定时器输入时钟以72MHz、定时1ms为例(71 1) * (999 1) / 72MHz 1ms很好验证。这条公式是后续所有定时器应用的基石PWM的频率计算、输入捕获的周期换算全都绕不开它。3.2 定时中断的配置顺序寄存器之间是有依赖的定时中断的配置顺序跟第2章的外部中断配置有点相似但定时器自身的寄存器更多顺序更有讲究。我习惯按照“时钟 → 时基 → 中断允许 → 清除标志 → 启动计数”五步走RCC-APB1ENR | RCC_APB1ENR_TIM2EN; // 1. 打开TIM2时钟 TIM2-PSC 71; // 2. 预分频 TIM2-ARR 999; // 2. 自动重载值 TIM2-DIER | TIM_DIER_UIE; // 3. 允许更新中断 TIM2-SR ~TIM_SR_UIF; // 4. 清除更新标志 TIM2-CR1 | TIM_CR1_CEN; // 5. 启动计数 NVIC-ISER[0] | (1 TIM2_IRQn); // 6. 打开NVIC通道这个顺序里第3步和第5步的顺序可以互换但DIER最好在启动计数之前配好否则定时器已经开始跑了更新中断却没被允许要等完整跑完一圈才能生效。第4步清标志看似多余实际上是在防止启动瞬间产生一次意外的更新中断因为CR1的CEN置位后有些硬件会在计数器从零启动的瞬间产生一个更新事件。3.3 PWM输出CCR和CCER的角色如果说定时中断是“计时”那PWM输出就是“定时器的比较功能”。在输出比较模式下CNT会持续和某个比较寄存器CCR比较CNT小于CCR时输出一种电平CNT大于CCR时输出另一种电平于是引脚上就出现了我们想要的方波。这个方波的周期由PSCARR决定占空比由CCR决定。要输出PWM除了配置PSC、ARR还要把某个通道映射到引脚上并选择PWM模式。以TIM2的通道1为例它默认可以映射到PA0引脚。GPIO那边需要配置为复用推挽输出TIM2这边要设置CCMR1、CCERGPIOA-CRL ~(0xF 0); GPIOA-CRL | (0xB 0); // CNF11复用推挽, MODE11输出50MHz TIM2-CCMR1 | (6 4); // CH1设为PWM模式1即OC1M[2:0]110 TIM2-CCER | (1 0); // 使能CH1输出 TIM2-CCR1 500; // 占空比50%这里要注意CCER的配置不能忽略。很多刚上手的人会对着CCMR1一通猛调结果引脚上什么都没有就是因为CCER里的对应输出位没有打开。CCER就像是PWM输出通道的大门CCMR1是里面的控制台控制台调得再漂亮大门关着也没用。3.4 算一次完整的PWM频率别被“减1”坑了假设系统时钟72MHz想要一个20kHz的PWM占空比可调。按公式反推PSC1乘以ARR1要等于72MHz除以20kHz也就是3600。一种常见组合是PSC71ARR49这时代入计算72MHz / (711) / (491) 72MHz / 3600 20kHz这个组合下CCR的取值范围是0到49占空比可按1/50的步进调整。如果你想要更精细的分辨率可以把PSC调到71、把ARR调成3599这样频率就是72MHz/72/36001kHz占空比步进变成1/3600但代价是PWM频率降低了。同步率、分辨率、频率三者之间存在取舍没有一组万能的参数选型时必须先明确项目需求侧重哪一边这条经验几乎适用于所有定时器应用。配合上示波器观察波形调PWM是最直观理解寄存器协作的方式。我在第一次看到引脚上出现稳定方波时突然对CR1、CNT、CCR这三个寄存器之间的关系有了真正的体感这比看十遍参考资料都有用。4. 中断里的隐形规则优先级分组、标志位清除与致命习惯4.1 优先级分组整个系统的根一辈子只能设一次Cortex-M3的内核用AIRCR寄存器控制优先级分组也就是“抢占优先级占几位、子优先级占几位”。这个配置对总线上所有中断都生效而且NVIC_IPR寄存器里为每个中断写的值要与这个分组模式按位对齐优先级才会按你的预期工作。优先级分组必须在系统初始化阶段尽早设置而且不要中途改变。很多项目里的教训是某个模块的初始化函数里偷偷改了一次分组结果其他中断的优先级含义完全变了原本应该是高优先级的中断被硬生生降级系统出现随机卡顿查了半天才发现是这种瞎子摸象式的问题。最合理的做法是在main函数的最开始就设置一次分组然后整个工程都不再碰它。// 设置2位抢占优先级2位子优先级 SCB-AIRCR 0x05FA0000 | (0x5 8); // 优先级分组5对应22这个值不同芯片可能有不同的推荐写法0x05FA0000是写入AIRCR的钥匙是解锁写操作的关键。如果忘记带这把钥匙整个寄存器写入会被硬件拒绝调试时会出现“写入无效”的诡异现象。4.2 标志位清除该清哪里不该清哪里我把这个坑单独列出来是因为它在HAL库时代被完美掩盖了。在中断服务函数里处理完事件之后需要清除标志位但很多人不知道该清哪一个。以定时器更新中断为例需要清除的是TIMx-SR的UIF位不是NVIC里的某个ICPR位。NVIC的ICPR是用于清除挂起状态的它管的是“NVIC这一层的待处理请求”不是“外设事件标志”。如果你只清ICPR而不清外设SR外设的请求仍然持续有效中断会像顽固的牛皮糖一样反复触发反过来如果你只清SR而不清NVIC的挂起这一次中断请求可能还残留着下次会意外多触发一次。正确做法是先处理外设SR写1清零NVIC这一层通常在CPU响应中断后会自动清除挂起位不需要你手动干预。4.3 中断服务函数里尽量少做的事在中断函数里直接写大段业务逻辑是很多初学者容易掉进去的陷阱。中断服务函数的特点是它响应的是某个紧急事件事件处理期间同级和低优先级的中断都被阻塞。如果函数里有一个毫秒级的延时或者慢速的算法系统对外部事件的响应能力就会直线下降。我在实际调试中遇到过这种情况一个按键中断里放了LED扫描、蜂鸣器、滤波逻辑还有一条printf到串口的调试输出结果按键响应肉眼可见地迟钝偶然还会出现“按键一次动作两次”的现象。问题就出在中断函数执行时间太长第二下降沿被排队挂起等函数退出后立刻再次进入。建议的中断函数结构是“快进快出”中断里只做最必要的事比如清标志位、把数据塞到一个环形缓冲区或者置一个全局标志真正的处理逻辑放到主循环里慢慢做。这种设计几乎适用于所有低复杂度嵌入式项目。5. 手写一份最小工程骨架外加我平时怎么调试5.1 工程初始化顺序先把地基打牢把这一章所有的内容串起来你会发现初始化顺序有一个清晰的逻辑链。一个同时包含外部中断、定时器中断、PWM输出的最小系统初始化顺序大致如下int main(void) { // 第一步: 设置NVIC优先级分组只能一次 SCB-AIRCR 0x05FA0000 | (0x5 8); // 第二步: 配置时钟树SysTick, PLL等 // 这里省略基于前两章内容 // 第三步: 初始化GPIO和AFIO RCC-APB2ENR | RCC_APB2ENR_IOPAEN | RCC_APB2ENR_AFIOEN; // 第四步: 配置EXTI AFIO-EXTICR[0] ~(0xF 0); EXTI-IMR | (1 0); EXTI-FTSR | (1 0); // 第五步: 配置TIM2: 时基 RCC-APB1ENR | RCC_APB1ENR_TIM2EN; TIM2-PSC 71; TIM2-ARR 999; TIM2-DIER | TIM_DIER_UIE; TIM2-SR ~TIM_SR_UIF; TIM2-CR1 | TIM_CR1_CEN; // 第六步: 使能NVIC各通道 NVIC-IPR[EXTI0_IRQn] (1 4); NVIC-IPR[TIM2_IRQn] (2 4); NVIC-ISER[0] | (1 EXTI0_IRQn) | (1 TIM2_IRQn); while(1) { // 主循环处理中断里留下的标志 } }这份骨架刻意把“开时钟 → 配外设 → 开中断”的层次拉开了每一层之间都能用调试器验证状态。当你遇到一个复杂的功能时先不要急着把所有功能一起往上叠加只保留一个小功能和这条骨架跑通了再逐步加。5.2 调试三板斧断点、变量窗口、外设寄存器面板寄存器开发最大的好处就是调试时一切都在明面上。我调试中断类问题时最常用的手段是一个断点配合调试器的“外设寄存器”窗口。以EXTI为例在中断服务函数入口设一个断点运行后如果断点进来了说明NVIC和CPU链路是通的如果断点没进来就去看EXTI-PR和NVIC-ISER的当前值一眼就能判断是请求没产生还是门没开。变量窗口和内存窗口也是好帮手。在调试器里观察TIM2-PSC和TIM2-ARR的实时值能确认写入是否成功。有些开发者可能会忽略的是这些外设寄存器地址在不同型号的芯片上完全一致但具体的位定义可能有差异调试时要把芯片手册放在手边别凭着记忆硬猜。5.3 最后一点个人心得回到我自己的经验在使用寄存器开发的过程中我觉得最有价值的习惯是每调通一个功能模块就把它做成一个极简的可复用模板并注释上核心寄存器配置的计算过程。这样下次遇到类似需求时不需要重新翻参考手册只需要改参数和引脚编号。还有个小技巧想分享给读者调试定时器中断时间不准时不要一上来就怀疑PSC和ARR算错了先量一下实际波形或者改变ARR值看中断速度有没有跟着变化。如果ARR改了中断频率完全没变那多半是时钟源配置出错了比如定时器挂在APB1上但时钟分频系数没算上这个问题对一个经验丰富的开发者来说也是常见的坑。用这种“变量控制”的思路去排查往往比反复阅读手册更高效。

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

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

免费获取报价 →
↑