(嵌入式八股)No.3 STM32(2)

2.1 异常 vs 中断(务必掌握)

什么是异常?什么是中断(Interrupt)

中断就是 让 CPU 暂停当前任务,优先处理紧急事件 的机制。CM3 的所有中断机制都由 NVIC 实现。

虽然 CM3 是支持 240 个外中断的,但具体使用了多少个是由芯片生产商决定。CM3 还有一个 NMI(不可屏蔽中断)输入脚。当它被置为有效(assert)时,NMI 服务例程会无条件地执行。

类型

示例

优先级范围

调度者

系统异常(Exception)

Reset、HardFault、SysTick、PendSV

固定

内核

外设中断(IRQ)

USART、TIM、DMA、EXTI 等

可配置

NVIC

在 ARM Cortex-M 架构中,这两个概念常常混用,但有着严格的包含关系:

  • 异常 (Exception):是 CPU 改变正常执行流(指令顺序执行)去处理紧急事件的统称。
  • 中断 (Interrupt):是异常的一种子集,特指由片上外设(如定时器、串口)或外部引脚触发的异步事件。

编号规则:

编号 0不是异常! 它是 初始主堆栈指针 (Initial MSP) 的数值。

编号 1 ~ 15系统异常 (System Exceptions)

  • 定义者:由 ARM 架构 统一硬性定义,所有 Cortex-M 芯片都一样。

编号 16 ~ 255外部中断 (External Interrupts)

  • 定义者:由 芯片厂商 (二级厂商) 定义。ARM 只是预留了位置。
  • 内容:与具体的片上外设绑定,如 STM32 的 WWDG_IRQHandler (16), RTC_IRQHandler (19) 等。

不可编程优先级 (Fixed Priority)

由 ARM 内核锁死,用户无法修改,用于处理最紧急的系统事件。

  • Reset (复位):编号 1,优先级 -3 (最高)。
  • NMI (不可屏蔽中断):编号 2,优先级 -2。
  • HardFault (硬件错误):编号 3,优先级 -1。

可编程优先级 (Configurable Priority)

可以通过 NVIC 寄存器修改优先级(通常是 0 到 255 之间的正数)。

  • 部分系统异常:MemManage, BusFault, UsageFault, SVCall, SysTick, PendSV。
  • 所有外部中断:编号 16 以上的所有外设中断。

异常类型

向量表

CM3 通过“向量表查表机制”确定异常入口地址: 向量表本质上是一个 32 位整数 (WORD) 数组,存储了各个异常 Handler 的入口地址。虽然向量表的位置可通过 NVIC 重定位寄存器 (通常指 VTOR) 进行修改,但复位后该寄存器默认为 0,因此 地址 0 处必须预置一张向量表 以保证系统正常启动。

举个例子,如果发生了异常 11(SVC),则 NVIC 会计算出偏移移量是 11x4=0x2C,然后从那里取出服务例程的入口地址并跳入。0 号异常的功能则是个另类,它并不是什么入口地址,而是给出了复位后 MSP 的初值。

NVIC(Nested Vectored Interrupt Controller)是什么?

NVIC 是 中断控制器,负责:

  • 开启/关闭中断
  • 设置抢占优先级与响应优先级
  • 中断嵌套
  • 中断挂起/清除

中断优先级:STM32 最容易搞错的地方

在 STM32 中断优先级是两部分:[抢占优先级][子优先级]

优先级数值越小 → 优先级越高。

抢占优先级决定是否能打断别人;子优先级决定同级别中断的服务顺序。

子优先级被用来判断:两个中断同时发生时,谁先被处理。还是以 EXT0、EXT1 为例,

如果它们同时发生了,那么分组优先级高的中断先被处理;如果分组优先级相同,那么子

优先级高的先被处理;如果连子优先级也相同,那么编号小的 EXT0 先被处理。

NVIC_SetPriority(USART1_IRQn, 5);
NVIC_SetPriority(DMA1_Stream0_IRQn, 3);
DMA 的 3 < USART 的 5 → DMA 可以打断 USART。

三大中断屏蔽寄存器 (Mask Registers)

在嵌入式开发中,为了保证 “时间关键 (Time-critical)” 任务能在最后期限 (Deadline) 前完成,或者为了保证对全局变量/硬件资源操作的 原子性 (Atomicity),我们需要暂时关闭中断,这段时间称为 临界区 (Critical Section)

PRIMASK —— “全局开关” (The Global Switch)

  • 功能:这是最常用的“关总中断”。
  • 效果:置 1 后,将当前优先级提升到 0 (最高可编程优先级)。
  • 封杀范围:屏蔽所有可配置优先级的异常和中断。只剩下:Reset, NMI, HardFault 可以执行。
  • 汇编指令:CPSID I (关中断 / Set PRIMASK)CPSIE I (开中断 / Clear PRIMASK)
  • 应用:裸机程序中保护简短的临界代码。

FAULTMASK —— “Panic 按钮” (The Panic Button)

  • 功能:比 PRIMASK 更狠,通常用于处理极度严重的系统错误。
  • 效果:置 1 后,将当前优先级提升到 -1。
  • 封杀范围:连 HardFault 都能屏蔽。只剩下:Reset, NMI 可以执行。
  • 汇编指令:CPSID F (关 Fault / Set FAULTMASK)CPSIE F (开 Fault / Clear FAULTMASK)
  • 应用:极少在应用程序中使用。通常用于系统即将崩溃,需要在一个极其安全的环境下复位或记录日志,防止 HardFault 再次嵌套触发。

BASEPRI —— “精准过滤器” (The Threshold Filter)

  • 功能:这是 Cortex-M3/M4 相比 M0 最强大的升级点。它不搞“一刀切”,而是设立一个优先级阈值。
  • 效果:写入一个数值 N。
  • 封杀范围:屏蔽所有优先级数值≥ N 的中断。ARM 规则回顾:优先级数值越大,优先级越低。白话解释:比 N 地位低(或地位相等)的中断全部闭嘴;比 N 地位高的中断依然可以打断当前程序。
  • 应用:RTOS 的核心。FreeRTOS 的 taskENTER_CRITICAL() 就是通过操作 BASEPRI 来实现的。好处:RTOS 关闭了大部分普通任务的中断,但依然允许像“电机紧急停止”或“看门狗喂狗”这种超高优先级的中断触发,既保护了数据,又没牺牲系统的实时救命能力。

中断的执行流程(执行一次 IRQ 是这样走的)【面试常考】

精简版

  1. 外设产生中断信号
  2. NVIC 检查是否开启 + 优先级是否允许
  3. NVIC 把当前寄存器上下文自动压栈
  4. 跳转至中断向量表对应的 IRQHandler
  5. 执行 IRQHandler 函数
  6. 退出中断:自动恢复现场(POP 寄存器)
  7. 回到主程序

👉 注意:上下文压栈/出栈由硬件自动完成,效率非常高。

详细版(如果面试能结合寄存器讲的详细一些会更好!)

处理器在以下条件满足时会接受异常请求:

  • 处理器正在运行(未暂停或复位)。
  • 异常处于使能状态(NMI 和 HardFault 总是使能)。
  • 异常的优先级高于当前运行等级。
  • 异常未被异常屏蔽寄存器(如 PRIMASK)屏蔽。

注意:SVC 异常若在某个异常处理中被意外调用,且该异常处理的优先级不低于 SVC,则会触发 HardFault。

异常进入流程

异常进入包括以下步骤:

  1. 压栈:多个寄存器和返回地址压入当前使用的栈(MSP 或 PSP),使异常处理可用普通 C 函数实现。
  2. 取向量:取出异常向量(异常处理入口地址),可能与压栈并行执行以减少延迟。
  3. 取指令:根据异常向量取出待执行的异常处理指令。
  4. 更新寄存器:更新 NVIC 和内核寄存器(如 PSR、LR、PC、SP),设置挂起与活跃状态。LR 更新为 EXC_RETURN(高27位为1,低5位含状态信息)。PC 更新为异常处理入口地址。SP 根据压栈所用栈指针自动调整。

执行异常处理

异常执行期间:
  • 使用 主栈指针(MSP)。
  • 处理器处于 处理模式,具有特权访问等级。
异常嵌套与挂起:
  • 更高优先级异常可抢占当前处理。
  • 同级或更低优先级异常将挂起,直至当前处理完成。
异常退出:

在异常处理末尾,通过执行返回指令将 EXC_RETURN 载入 PC,触发异常返回机制。

异常返回

ARM Cortex-M 使用 EXC_RETURN 机制触发异常返回,该值在异常入口时存入 LR。将其写入 PC 即触发返回流程。

返回操作包括:
  • 出栈:恢复进入异常时压入栈的寄存器。
  • 更新寄存器:更新 NVIC 活跃状态及内核寄存器(PSR、SP、CONTROL 等)。

STM32 的中断配置步骤(官方推荐流程)

__HAL_UART_ENABLE_IT(&huart1, UART_IT_RXNE);       //① 使能外设本身中断  例如 USART:

HAL_NVIC_SetPriority(USART1_IRQn, 5, 0);            //② 配置 NVIC 优先级并使能 IRQ
HAL_NVIC_EnableIRQ(USART1_IRQn);

void USART1_IRQHandler(void)                        //③ 中断回调函数逻辑
{
    HAL_UART_IRQHandler(&huart1);
}

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{// 用户逻辑
}

常见中断类型(STM32 编程最常用)(了解)

1)GPIO EXTI 中断

上升沿/下降沿触发。

2)USART 串口中断

  • RXNE:收到数据
  • IDLE:空闲帧(DMA 串口强烈推荐使用)
  • TC:发送完成

3)DMA 中断

  • Half Transfer
  • Full Transfer

常用于 RingBuffer + DMA UART。

4)定时器中断(TIM)

  • 更新中断(周期事件)
  • Capture/Compare 中断

5)SysTick、PendSV(FreeRTOS 最常用)

RTOS 的调度依赖这两个:

  • SysTick:心跳节拍
  • PendSV:上下文切换

最常见的中断错误

❌ 1)不清除中断标志位 → 中断不断触发---->一定及时清除中断标志位

__HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0);

❌ 2)中断函数执行时间太长

导致:

  • 丢数据
  • 影响其他高优先级中断
  • FreeRTOS 抖动

正确做法:中断里只放最小逻辑,把数据放队列或标志位。最好是快进快出。

❌ 3)优先级设置错误导致 FreeRTOS 失效-----后面 RTOS 里面也会讲到

很多同学栽在这里。

FreeRTOS 要求:不能使用比 configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 更高的中断调用 RTOSAPI。

#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5
  则你的中断如果设置为 0,1,2,3,4 → 不能调用 FreeRTOS API。
   否则:系统崩溃。Hardfault

❌ 4)访问共享变量未加 volatile

尤其是:

  • 中断里修改标志位
  • 主循环读取标志位

必须:

volatile uint8_t flag = 0;

中断嵌套(掌握)

中断嵌套的核心在于中断优先级(Interrupt Priority)。当一个中断发生时,处理器会根据中断的优先级来决定是否中断当前正在执行的任务(包括普通任务或中断服务例程)。​

高优先级中断可以打断低优先级中断:如果一个高优先级的中断发生,而此时处理器正在执行一个低优先级的中断服务例程,处理器会暂停当前的中断服务例程,转而执行高优先级的中断服务例程。​

低优先级中断不能打断高优先级中断:如果一个低优先级的中断发生,而处理器正在执行一个高优先级的中断服务例程,低优先级的中断会被挂起,直到高优先级的中断服务例程执行完毕。

中断嵌套的实现​

中断嵌套的实现依赖于处理器的中断控制器(Interrupt Controller)。中断控制器会根据中断的优先级来调度中断服务例程的执行。以下是中断嵌套的典型实现步骤:​

中断请求(Interrupt Request):当一个中断源(如外部信号、定时器溢出等)触发中断时,中断控制器会记录该中断请求。​

中断优先级判断:中断控制器会比较当前正在执行的任务(或中断服务例程)的优先级与新中断的优先级。

中断嵌套处理:​

如果新中断的优先级高于当前正在执行的任务或中断服务例程的优先级处理器会保存当前上下文(如寄存器状态),然后跳转到新中断的中断服务例程。​

如果新中断的优先级低于或等于当前正在执行的任务或中断服务例程的优先级新中断会被挂起,直到当前任务或中断服务例程执行完毕。

中断嵌套的注意事项​

•中断服务例程的编写:中断服务例程应尽可能简短,避免长时间占用处理器。如果需要执行复杂任务,建议将任务放入队列,由主程序处理。​

优先级配置:合理配置中断优先级非常重要。高优先级的中断应处理紧急任务,低优先级的中断可以处理相对不紧急的任务。​

•避免死锁:如果多个中断服务例程之间存在共享资源(如全局变量、硬件资源等),需要使用互斥机制(如禁用中断、使用信号量等)来避免死锁。

•中断嵌套深度:虽然中断嵌套可以提高系统的实时性,但过多的嵌套层次可能导致系统复杂度增加。建议限制中断嵌套的深度,一般不超过2 - 3层。

2.2 DMA(了解原理即可)

STM32微控制器的DMA(Direct Memory Access,直接存储器访问)是一种非常强大的功能,可以实现数据在存储器之间或存储器与外设之间的高效传输,而无需CPU干预。这不仅可以减轻CPU的负担,还可以提高系统的整体性能。

DMA的基本概念​

DMA是一种硬件机制,允许数据在存储器和外设之间直接传输,而无需CPU的直接干预。在STM32中,DMA控制器可以管理多个数据传输请求,并根据配置的优先级和传输方向自动完成数据传输。​

•传输方向:DMA支持多种传输方向,包括从外设到存储器、从存储器到外设、存储器到存储器等。​

•数据源和目标:数据源和目标可以是外设的寄存器(如ADC、SPI、USART等)或存储器地址。​

•传输大小:可以配置传输的数据量(如传输的字节数或字数)。

STM32 DMA的主要特性​

多通道支持:STM32的DMA控制器通常支持多个通道(如DMA1和DMA2),每个通道可以独立配置和使用。​

•多种传输方向:支持从外设到存储器、从存储器到外设、存储器到存储器等多种传输方向。​

•数据宽度:支持字节(8位)、半字(16位)、字(32位)等多种数据宽度。​

•优先级配置:支持通道优先级配置,可以动态调整DMA通道的优先级。​

•中断支持:支持多种中断类型,包括传输完成、传输错误、半传输完成等。​

•循环传输:支持循环传输模式,可以在传输完成后自动重新开始传输。

2.3 彻底解决通讯数据覆盖造成的数据丢失 - 环形存储(Ring Buffer)

环形缓冲区是嵌入式开发中一个简单、高效且不可或缺的工具。它完美地解决了异步数据流处理中的速度匹配和临时存储问题,是构建稳定、可靠嵌入式系统的基石之一。理解并熟练运用它,是嵌入式工程师的基本功。环形缓冲区(Circular Buffer / Ring Buffer)是一种首尾相连的 FIFO 缓存结构。

什么是环形缓冲区?

环形缓冲区,也叫循环缓冲区或环形队列,是一种首尾相连的先进先出数据结构。你可以把它想象成一个圆环:

它有两个指针:

  • 写指针:指向下一个可以写入数据的位置。
  • 读指针:指向下一个可以读取数据的位置。

当指针移动到缓冲区的末尾时,它会自动绕回到缓冲区的开头,这就是“环形”的由来。

环形缓冲区的优势:

  1. 高效的数据缓冲:平滑了数据流,防止了数据丢失。
  2. 预分配固定内存:内存大小在初始化时就确定了,避免了动态内存分配的不确定性和碎片问题,这在资源受限的嵌入式系统中至关重要。
  3. 高效的内存使用:通过“绕回”机制,可以循环利用一块固定的内存区域。
  4. 线程/中断安全:通过简单的机制(如关中断)可以实现生产者/消费者之间的安全数据交换。

为什么在嵌入式系统中需要它?

环形缓冲区解决了嵌入式系统中一个非常常见的问题:数据生产者和消费者速度不匹配。

典型场景:

  • UART 串口通信:数据以字节流的

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

泻湖花园嵌入式Offer指南 文章被收录于专栏

从入门到上岸,一站式搞定求职! 本硕纯机械,无竞赛无论文,后转行嵌入式软件开发(因为课题组师哥转嵌入式拿到30Woffer之后狠狠心动),秋招最终收获35W+offer可以为27届或者28届的的UU们提供参考,可以关注一下!!!

全部评论
学习
1 回复 分享
发布于 03-03 23:39 山东
欢迎大家评论区交流嗷
1 回复 分享
发布于 03-03 22:39 山东

相关推荐

zbk1:学院本找嵌入式我觉得不太行,不要被培训班忽悠了,老老实实读个研吧。
点赞 评论 收藏
分享
评论
4
2
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务