☰
STM32中断系统深度解析:NVIC、EXTI与SysTick寄存器实战
2026/10/25 1:09:48 网站建设 项目流程

1. 中断系统为什么是STM32的分水岭

1.1 从轮询到中断的思维转变

刚接触STM32那会儿,我最不理解的就是中断。之前玩51单片机,写个按键检测就是while(1)里不断读引脚电平,简单粗暴。到了STM32,教程一上来就讲NVIC、EXTI、优先级分组,一堆寄存器看得人头大。后来做项目踩了坑才明白:轮询方式在复杂系统里根本活不下去。

举个实际场景。你有一个系统要同时处理串口收数据、按键响应、定时采样ADC、PWM输出。如果用轮询,主循环里得挨个查标志位,串口来数据了没及时读就丢包,按键按快了就漏检。中断机制的本质是让CPU从“主动轮询”变成“被动响应”——外设有事了主动通知CPU,CPU放下手头的事去处理,处理完再回来。这个思维转变是STM32入门最关键的一步。

这一章要聊的三个东西——NVIC、EXTI、SysTick——恰好构成了STM32中断系统的铁三角。NVIC管优先级调度,EXTI管外部事件触发,SysTick管系统节拍。把这三个搞透了,后面学定时器中断、串口中断、DMA中断都是水到渠成的事。

1.2 三个模块的分工与协作关系

先理清它们各自的位置。NVIC是Cortex-M内核自带的中断控制器,所有中断最终都要经过它来排队和调度。EXTI是芯片外设,负责把GPIO引脚的电平变化、外设事件等转换成中断请求信号送给NVIC。SysTick也是内核自带的一个24位递减计数器,专门用来产生周期性的系统节拍中断。

它们的关系可以这样理解:EXTI是门卫,检测到有人来了就按门铃;NVIC是前台调度员,决定先接待谁后接待谁;SysTick是墙上的挂钟,每隔固定时间响一次提醒你该干活了。三者配合,才能让整个系统有条不紊地运转。

我见过不少初学者把EXTI和NVIC搞混,以为配置了EXTI中断就能用了,结果忘了在NVIC里使能对应的中断通道,调试半天找不到问题。这两个是串联关系,缺一不可。

1.3 学完这一章你能做什么

具体来说,学完这一章的内容,你应该能独立完成这些事:用寄存器方式配置任意GPIO引脚的外部中断,设置上升沿、下降沿或双边沿触发;理解并正确配置中断优先级分组,处理多个中断同时到来的竞争问题;用SysTick实现精准的毫秒级和微秒级延时函数,不再依赖不靠谱的for循环延时;搞清楚中断嵌套和抢占优先级的实际运行机制。

这些技能在实际项目里使用频率极高。比如做物联网网关,串口中断收数据、定时器中断做协议超时检测、EXTI中断处理按键配网,全都依赖本章的内容。可以说,中断系统是STM32从“玩具”走向“产品”的必经之路。

2. NVIC寄存器深度拆解

2.1 NVIC到底是什么,为什么它这么重要

NVIC全称Nested Vectored Interrupt Controller,嵌套向量中断控制器。这个名字里每个词都有含义:“嵌套”说明它支持中断嵌套,高优先级中断可以打断低优先级中断的处理;“向量”说明它通过中断向量表直接跳转到对应的处理函数,不需要软件查询中断源;“控制器”说明它是整个中断系统的调度中心。

NVIC位于Cortex-M内核内部,不是STM32芯片厂商加的外设。这意味着不管你用ST、GD还是其他厂商的M系列芯片,NVIC的编程模型都是一样的。它管理着芯片上所有中断源的使能、禁用、挂起、优先级设置等操作。

STM32F103系列有60个可屏蔽中断通道,每个通道在NVIC里都有一组对应的寄存器位。这些寄存器通过内存映射的方式暴露给程序员,地址在0xE000E000附近。CMSIS库把这些寄存器封装成了结构体,但用寄存器方式直接操作能让你更清楚地理解底层机制。

2.2 中断使能寄存器ISER的位操作技巧

ISER全称Interrupt Set-Enable Register,中断使能寄存器。STM32F103的NVIC有ISER[0]到ISER[1]两个32位寄存器,总共可以管理64个中断通道。每个bit对应一个中断通道,写1使能,写0无效。

这里有个容易踩的坑:ISER只能写1使能,不能写0禁用。要禁用一个中断,得操作ICER寄存器。这个设计其实挺合理的,避免了读-改-写操作中可能出现的竞争问题。

用寄存器方式使能EXTI0中断的代码是这样的:

// EXTI0_IRQn的中断号是6 // 6 / 32 = 0,所以在ISER[0]中 // 6 % 32 = 6,所以操作bit6 NVIC->ISER[0] = (1 << 6);

更通用的写法是用CMSIS提供的宏:

NVIC_EnableIRQ(EXTI0_IRQn);

这个宏内部会自动计算操作哪个ISER寄存器、哪个bit位。我建议初学阶段先手动算几遍,搞清楚位号和中段号的关系,之后再用宏提高效率。

2.3 优先级寄存器IPR的配置逻辑

IPR全称Interrupt Priority Register,中断优先级寄存器。STM32F103的NVIC有IPR[0]到IPR[14]共15个寄存器,每个寄存器8位,但STM32只用了高4位,所以实际优先级范围是0到15,数值越小优先级越高。

这里要特别注意:STM32F103的优先级寄存器是按字节访问的,每个中断通道占一个字节。IPR[0]的bit7~bit4对应中断通道0的优先级,bit3~bit0保留未用。所以设置优先级的时候不能直接对整个32位寄存器赋值,得用字节操作。

// 设置EXTI0中断优先级为1 // EXTI0_IRQn = 6,对应IPR[1]的高4位 NVIC->IP[6] = (1 << 4); // 优先级1左移4位放到高4位

CMSIS同样提供了封装宏:

NVIC_SetPriority(EXTI0_IRQn, 1);

我个人的经验是,在项目初期就把所有中断的优先级规划好,列一个表格,避免后期中断多了之后优先级冲突导致奇怪的bug。规划原则后面会详细讲。

2.4 优先级分组:抢占优先级与子优先级的取舍

这是NVIC里最容易让人迷糊的部分。Cortex-M3/M4的中断优先级分为抢占优先级和子优先级两组。抢占优先级决定能不能打断正在执行的中断,子优先级决定同时挂起时谁先执行,但不能打断。

STM32F103通过SCB->AIRCR寄存器的PRIGROUP位来设置分组方式,共有5种分组:

分组PRIGROUP值抢占优先级位数子优先级位数抢占级数子级数
00x70040161
10x6003182
20x5002244
30x4001328
40x30004116

分组0表示全部4位都是抢占优先级,没有子优先级,支持16级嵌套。分组4表示没有抢占优先级,全是子优先级,不支持嵌套。

怎么选?我的经验是:如果你的系统中断不多,直接用分组0,全部用抢占优先级,简单明了。如果中断比较多,需要精细调度,用分组2,2位抢占2位子优先级,兼顾嵌套和排队。

设置分组的代码:

// 设置优先级分组为2 // 需要先写入密钥0x5FA到AIRCR的高16位 SCB->AIRCR = (0x5FA << 16) | (2 << 8);

或者用CMSIS宏:

NVIC_SetPriorityGrouping(2);

注意:优先级分组只需要在系统初始化时设置一次,运行中不要随意更改,否则已配置的中断优先级会错乱。

2.5 中断挂起与解挂的实用场景

NVIC还有两个比较冷门但很有用的寄存器:ISPR和ICPR。ISPR用于软件触发中断挂起,ICPR用于清除挂起状态。

ISPR的典型用法是在调试阶段模拟中断触发,不用真的去按按键或接信号,直接写寄存器就能进入中断服务函数,方便验证逻辑。ICPR则用于清除那些因为毛刺信号误触发的中断挂起标志。

// 软件触发EXTI0中断 NVIC->ISPR[0] = (1 << 6); // 清除EXTI0中断挂起 NVIC->ICPR[0] = (1 << 6);

这两个寄存器在实际产品中用的不多,但调试时能省不少事。特别是当你的板子还没焊好按键,但想先调通中断处理逻辑的时候,ISPR就是救星。

3. EXTI外部中断的寄存器级配置

3.1 EXTI的架构与触发链路

EXTI全称External Interrupt/Event Controller,外部中断/事件控制器。STM32F103的EXTI有19条中断/事件线,其中EXTI0到EXTI15对应GPIO引脚,EXTI16到EXTI19对应PVD输出、RTC闹钟、USB唤醒等内部事件。

每条EXTI线可以独立配置为中断模式或事件模式。中断模式会向NVIC发送中断请求,CPU执行中断服务函数;事件模式不产生中断,而是产生一个脉冲信号触发其他外设(比如ADC启动转换、定时器启动等)。这个区别很关键,很多人配置完了发现中断不触发,就是因为模式选错了。

EXTI的触发链路是这样的:GPIO引脚信号经过边沿检测电路,如果检测到配置的边沿(上升沿/下降沿/双边沿),就置位挂起寄存器PR中的对应位。如果中断屏蔽寄存器IMR中对应位为使能状态,就向NVIC发送中断请求。NVIC再根据优先级决定是否响应。

3.2 从GPIO到EXTI的映射关系

STM32F103的GPIO和EXTI之间的映射不是随便选的。EXTI0只能映射到PA0、PB0、PC0等所有端口的第0号引脚,EXTI1只能映射到第1号引脚,以此类推。具体选哪个端口,由AFIO_EXTICR寄存器决定。

AFIO_EXTICR有4个寄存器,每个管理4条EXTI线,每个线用4个bit选择端口。比如EXTI0用AFIO_EXTICR1的低4位,写0选PA0,写1选PB0,写2选PC0。

// 使能AFIO时钟 RCC->APB2ENR |= (1 << 0); // 配置PA0为EXTI0源 AFIO->EXTICR[0] &= ~(0xF << 0); // 清除低4位 AFIO->EXTICR[0] |= (0 << 0); // 选择PA0

这里有个大坑:PA0和PB0不能同时用作EXTI0。如果你同时需要PA0和PB0的外部中断,只能一个用EXTI0,另一个用其他EXTI线,或者用轮询方式处理其中一个。这个限制在项目引脚规划阶段就要考虑清楚。

3.3 边沿检测与挂起寄存器的配合

EXTI的边沿检测由RTSR和FTSR两个寄存器控制。RTSR设置上升沿触发,FTSR设置下降沿触发。两个都设置就是双边沿触发。

// 配置EXTI0为下降沿触发 EXTI->FTSR |= (1 << 0); // 使能下降沿 EXTI->RTSR &= ~(1 << 0); // 禁止上升沿 // 使能EXTI0中断 EXTI->IMR |= (1 << 0);

当触发条件满足时,EXTI->PR寄存器的对应位会被硬件置1。这个标志位必须在中断服务函数里手动清除,否则会反复进入中断。清除方法是写1到对应位:

EXTI->PR = (1 << 0); // 写1清除

注意这里是写1清除,不是写0。这个设计在STM32里很常见,目的是避免读-改-写操作影响其他标志位。

3.4 完整的外部中断配置流程

把上面的步骤串起来,用寄存器方式配置PA0下降沿触发中断的完整流程如下:

第一步,使能GPIOA和AFIO时钟:

RCC->APB2ENR |= (1 << 2); // GPIOA时钟 RCC->APB2ENR |= (1 << 0); // AFIO时钟

第二步,配置PA0为上拉输入:

GPIOA->CRL &= ~(0xF << 0); // 清除PA0配置 GPIOA->CRL |= (0x8 << 0); // 输入模式,上拉/下拉 GPIOA->ODR |= (1 << 0); // 上拉

第三步,配置EXTI0映射到PA0:

AFIO->EXTICR[0] &= ~(0xF << 0); // 选择PA0

第四步,配置EXTI0触发方式和使能:

EXTI->FTSR |= (1 << 0); // 下降沿触发 EXTI->IMR |= (1 << 0); // 使能中断

第五步,配置NVIC:

NVIC_SetPriorityGrouping(2); NVIC_SetPriority(EXTI0_IRQn, 1); NVIC_EnableIRQ(EXTI0_IRQn);

第六步,编写中断服务函数:

void EXTI0_IRQHandler(void) { if (EXTI->PR & (1 << 0)) { EXTI->PR = (1 << 0); // 清除标志 // 你的处理逻辑 } }

这个流程看起来步骤多,但每一步都有明确的目的。我建议初学者至少手动用寄存器写三遍,把每个寄存器的地址和位含义记牢,后面再用库函数就会觉得特别清晰。

3.5 中断服务函数的编写规范

中断服务函数有几个铁律必须遵守。第一,函数名必须和启动文件里的向量表名字完全一致,大小写都不能错。STM32F103的EXTI0中断函数名是EXTI0_IRQHandler,写成EXTI0_Handler就不会被调用。

第二,执行时间要尽可能短。中断服务函数里不要做延时、不要等标志位、不要调用耗时函数。如果需要处理复杂逻辑,置一个标志位让主循环去处理。

第三,共享变量要加volatile修饰。中断里修改的全局变量,主循环里读取时如果不加volatile,编译器可能优化掉读取操作,导致主循环看不到变量的变化。

volatile uint8_t key_pressed = 0; void EXTI0_IRQHandler(void) { if (EXTI->PR & (1 << 0)) { EXTI->PR = (1 << 0); key_pressed = 1; } }

第四,如果中断里调用了可能被其他中断也调用的函数,要考虑重入问题。最简单的办法是关中断保护临界区,但关中断时间不能太长。

4. SysTick系统滴答定时器实战

4.1 SysTick的定位与核心价值

SysTick是Cortex-M内核自带的一个24位递减计数器,位于内核的私有外设区,地址在0xE000E010。它的主要用途是给操作系统提供一个周期性的系统节拍,也可以用来做精确延时。

为什么不用通用定时器做延时?因为SysTick是内核自带的,不占用芯片厂商的外设资源,而且编程模型在所有M系列芯片上通用。用SysTick做延时函数,代码移植性最好。

SysTick有4个寄存器:CTRL控制寄存器、LOAD重装载值寄存器、VAL当前值寄存器、CALIB校准寄存器。结构非常简洁,比通用定时器好学得多。

4.2 四个寄存器的功能与配置方法

CTRL寄存器控制SysTick的开关和模式。bit0是使能位,写1启动,写0停止。bit1是中断使能位,写1则计数到0时产生中断。bit2是时钟源选择位,写0选择HCLK/8,写1选择HCLK。bit16是计数到0的标志位,读取为1表示已经归零。

LOAD寄存器存放重装载值,24位有效,最大0xFFFFFF。每次计数到0后,VAL会自动重新加载这个值。

VAL寄存器是当前计数值,写任何值都会清零,同时清除CTRL的COUNTFLAG标志。

CALIB寄存器用于校准,一般用不到。

配置1ms周期的SysTick中断,假设系统时钟72MHz:

// 重装载值 = 72000000 / 1000 - 1 = 71999 SysTick->LOAD = 71999; SysTick->VAL = 0; // 清空当前值 SysTick->CTRL = (1 << 2) | // 时钟源HCLK (1 << 1) | // 使能中断 (1 << 0); // 启动计数

对应的中断服务函数是SysTick_Handler,这个函数名是固定的,在启动文件里已经注册好了。

4.3 用SysTick实现精准延时函数

用SysTick做延时有两种方式:中断方式和查询方式。中断方式适合做系统节拍,查询方式适合做短延时。

查询方式实现微秒级延时的思路是:设置LOAD为需要延时的时钟数,启动计数,然后轮询COUNTFLAG直到计数结束。注意这种方式不需要开中断。

void delay_us(uint32_t us) { SysTick->LOAD = 72 * us - 1; // 72MHz下1us=72个时钟 SysTick->VAL = 0; SysTick->CTRL = (1 << 2) | (1 << 0); // HCLK,不使能中断 while (!(SysTick->CTRL & (1 << 16))); // 等待COUNTFLAG SysTick->CTRL = 0; // 关闭 }

这个函数有个限制:us不能太大,因为LOAD是24位,最大0xFFFFFF,72MHz下最多延时约233ms。做毫秒级延时需要循环调用或者用中断方式。

中断方式做毫秒延时需要一个全局变量计数:

volatile uint32_t ms_tick = 0; void SysTick_Handler(void) { ms_tick++; } void delay_ms(uint32_t ms) { uint32_t start = ms_tick; while ((ms_tick - start) < ms); }

这里用减法而不是直接比较大小,是为了处理计数器溢出的情况。ms_tick是32位变量,溢出周期很长,但用减法写法更安全。

4.4 SysTick在裸机系统中的调度应用

SysTick最大的价值不是做延时,而是做任务调度。在裸机系统里,可以用SysTick产生1ms节拍,然后在主循环里根据节拍执行不同周期的任务。

volatile uint32_t tick_1ms = 0; void SysTick_Handler(void) { tick_1ms++; } int main(void) { // 初始化... SysTick_Config(72000); // 1ms中断 uint32_t last_task1 = 0; uint32_t last_task2 = 0; while (1) { if ((tick_1ms - last_task1) >= 10) // 10ms任务 { last_task1 = tick_1ms; task1(); } if ((tick_1ms - last_task2) >= 100) // 100ms任务 { last_task2 = tick_1ms; task2(); } } }

这种时间片轮询的架构在简单项目里非常好用,比上RTOS轻量得多。我做过好几个小项目都是用这种方式,代码量小,调试方便,实时性也够用。

4.5 SysTick与FreeRTOS的配合

如果项目用了FreeRTOS,SysTick会被RTOS接管作为系统节拍。这时候你不能再自己写SysTick_Handler,否则会和RTOS的移植层冲突。

FreeRTOS的移植文件里已经定义了xPortSysTickHandler,通过宏映射到SysTick_Handler。RTOS用这个节拍来做任务调度和时间管理。如果你还需要额外的定时功能,应该用软件定时器或者通用定时器,不要动SysTick。

我见过有人在FreeRTOS项目里自己写了SysTick_Handler,结果编译报重复定义,或者更隐蔽的——编译通过了但RTOS调度异常。这个坑要注意。

5. 中断优先级规划与实战避坑

5.1 多中断系统的优先级分配原则

当系统里有多个中断时,优先级分配就成了一个需要认真设计的问题。我的经验是遵循这几条原则:

第一条,实时性要求最高的中断给最高抢占优先级。比如电机控制里的过流保护中断,必须在最短时间内响应,给抢占优先级0。

第二条,通信类中断给中等优先级。串口、SPI、I2C这些中断不能太慢,但也不要求微秒级响应,给抢占优先级1到3。

第三条,人机交互类中断给低优先级。按键、触摸屏这些,几十毫秒的延迟人根本感觉不到,给最低优先级。

第四条,同一个外设的多个中断源用子优先级区分。比如串口的接收中断和发送中断,抢占优先级相同,用子优先级决定谁先处理。

一个典型的优先级规划表:

中断源抢占优先级子优先级理由
过流保护00安全相关,最高响应
SysTick10系统节拍,影响调度
串口接收20通信实时性要求
定时器捕获21与串口同级,子优先级低
EXTI按键30人机交互,要求低

5.2 中断嵌套的实际运行机制

中断嵌套的规则是:高抢占优先级的中断可以打断低抢占优先级的中断服务函数。同抢占优先级的中断不能互相打断,即使子优先级不同。

举个例子。假设串口中断抢占优先级2正在执行,此时过流保护中断抢占优先级0触发,CPU会保存串口中断的现场,转去执行过流保护中断。过流保护执行完后,回到串口中断继续执行。

如果串口中断执行时,定时器捕获中断(抢占优先级2,子优先级1)触发,因为抢占优先级相同,不会嵌套,定时器中断会挂起等待串口中断执行完。

这里有个容易忽略的点:中断嵌套会增加栈的使用。每嵌套一层,就要保存一组寄存器现场。如果栈空间不够,就会栈溢出,表现为程序跑飞或硬件异常。我一般会在启动文件里把栈大小从默认的0x400增加到0x800甚至0x1000,给嵌套留足余量。

5.3 中断标志清除的时机与陷阱

中断标志清除的时机很讲究。清早了可能丢失后续事件,清晚了可能重复进入中断。

正确的做法是:进入中断服务函数后,先判断标志位,处理完业务逻辑后再清除标志。但有些外设的标志清除有特殊要求,比如STM32的串口接收中断,读DR寄存器就会自动清除RXNE标志,不需要手动清。

EXTI的标志清除要注意:必须在处理逻辑之前或之后清除PR寄存器,但不能在判断标志之前清除。如果先清标志再判断,判断永远为假。

// 正确写法 void EXTI0_IRQHandler(void) { if (EXTI->PR & (1 << 0)) // 先判断 { // 处理逻辑 EXTI->PR = (1 << 0); // 后清除 } } // 错误写法 void EXTI0_IRQHandler(void) { EXTI->PR = (1 << 0); // 先清除 if (EXTI->PR & (1 << 0)) // 永远为假 { // 永远不会执行 } }

5.4 常见问题速查与排查思路

现象可能原因排查方法
中断完全不触发NVIC未使能检查ISER寄存器
中断触发一次后不再触发标志未清除检查PR寄存器
中断频繁误触发边沿抖动加硬件RC滤波或软件消抖
中断嵌套后程序跑飞栈溢出增大栈空间
优先级设置无效分组未设置检查AIRCR的PRIGROUP
中断响应延迟大高优先级中断占用太久优化中断服务函数
编译报重复定义中断函数名冲突检查启动文件和库文件

我踩过最坑的一个问题是:EXTI中断配置好了,但按键按下去没反应。查了半天发现是GPIO时钟使能了,但AFIO时钟忘了开。AFIO时钟不使能,EXTICR寄存器写不进去,EXTI线就映射不到正确的GPIO。这个坑很隐蔽,因为代码编译没问题,运行也不报错,就是功能不对。

还有一个经典问题:两个中断优先级配置相同,但期望一个能打断另一个。这违反了中断嵌套的基本规则,同抢占优先级不能嵌套。解决办法是给需要嵌套的中断更高的抢占优先级。

5.5 调试中断的实用技巧

调试中断有几个好用的方法。第一,在中断服务函数里翻转一个GPIO,用示波器或逻辑分析仪看波形,能直观地看到中断触发频率和执行时间。

第二,用变量记录中断进入次数,在主循环里打印出来,判断中断是否按预期触发。

第三,如果怀疑中断被阻塞,可以读取NVIC的IABR寄存器(中断活跃状态寄存器),看看哪些中断正在执行。

第四,用调试器单步跟踪中断服务函数,但要注意单步会改变时序,有些实时性问题单步时看不出来。

第五,SysTick的COUNTFLAG是个好东西,可以用它来测量代码执行时间。在代码段前后读SysTick->VAL,差值就是执行的时钟数。

6. 寄存器操作的核心心法与进阶方向

6.1 寄存器编程的思维模式

用寄存器编程和用库函数编程,最大的区别不是代码量,而是思维模式。库函数是别人帮你封装好的,你只需要知道调什么函数;寄存器编程要求你知道每个bit的含义,知道为什么要这样配置。

这种思维模式的好处是,遇到问题时你能深入到最底层去排查。比如串口通信不正常,用库函数你只能检查初始化参数;用寄存器编程,你可以直接读SR寄存器看各个错误标志位,快速定位是帧错误、噪声错误还是溢出错误。

我的建议是:初学阶段用寄存器方式写代码,把每个外设的配置流程都手动走一遍。等工作后再用库函数或HAL库提高开发效率。但底层调试时,寄存器知识就是你的杀手锏。

6.2 从寄存器到HAL库的平滑过渡

掌握了寄存器操作后,看HAL库的源码会特别通透。比如HAL_GPIO_Init函数,内部就是在操作CRL、CRH、ODR这些寄存器。你知道每个参数最终写到哪个bit,就能理解为什么有些配置组合是无效的。

HAL库的中断处理也值得研究。HAL库把中断服务函数分成了两层:底层的中断入口函数和上层的回调函数。底层函数负责清除标志、判断中断源,然后调用回调函数。这种分层设计让用户只需要关注业务逻辑,不用管底层细节。

但HAL库也有性能开销。如果项目对中断响应时间要求极高,比如做无刷电机控制,建议还是用寄存器或LL库。HAL库的函数调用层次深,执行时间长,可能满足不了微秒级的响应要求。

6.3 后续可以深入的方向

中断系统学完之后,可以往这几个方向深入。第一个是DMA,DMA传输完成也会产生中断,配合中断可以实现高效的数据搬运,不占用CPU时间。第二个是定时器的高级功能,包括PWM输入捕获、编码器接口、死区控制等,这些在电机控制和电源管理里非常有用。第三个是RTOS,理解RTOS如何利用SysTick和SVC异常实现任务切换,能让你对中断系统有更深的认识。

第四个方向是低功耗。STM32的低功耗模式(Sleep、Stop、Standby)都和中断唤醒密切相关。比如Stop模式下,EXTI中断可以把芯片从停止模式唤醒。做电池供电的产品,低功耗设计是必修课。

第五个方向是中断安全。在功能安全相关的项目里,中断的优先级配置、栈溢出检测、中断响应时间监控都有严格的要求。这些内容在普通教程里很少涉及,但实际产品中很重要。

6.4 我个人的几条经验总结

最后分享几条我在实际项目中总结的经验。第一条,中断服务函数里尽量只做标志位设置和数据搬运,复杂逻辑放到主循环。中断执行时间越短,系统的实时性越好。

第二条,共享变量一定要加volatile,并且在读写多字节变量时考虑原子性。比如主循环读一个32位变量,中断里修改它,如果读操作被打断,可能读到一半新一半旧的数据。解决办法是关中断保护或者用原子操作。

第三条,优先级分组在项目初期就定好,不要中途改。我吃过这个亏,项目做了一半发现优先级不够用,改分组后所有中断的优先级都要重新算,很容易出错。

第四条,SysTick的延时函数在中断里调用要小心。如果中断优先级高于SysTick中断,在中断里调用delay_ms会导致死等,因为SysTick中断进不来,ms_tick不增加。解决办法是在中断里用查询方式的delay_us,或者用硬件定时器做延时。

第五条,调试中断问题时,先确认时钟使能,再确认NVIC使能,最后确认标志清除。这三个是最常见的出错点,按这个顺序排查能解决大部分问题。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询