(嵌入式八股)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 是这样走的)【面试常考】
精简版
- 外设产生中断信号
- NVIC 检查是否开启 + 优先级是否允许
- NVIC 把当前寄存器上下文自动压栈
- 跳转至中断向量表对应的 IRQHandler
- 执行 IRQHandler 函数
- 退出中断:自动恢复现场(POP 寄存器)
- 回到主程序
👉 注意:上下文压栈/出栈由硬件自动完成,效率非常高。
详细版(如果面试能结合寄存器讲的详细一些会更好!)
处理器在以下条件满足时会接受异常请求:
- 处理器正在运行(未暂停或复位)。
- 异常处于使能状态(NMI 和 HardFault 总是使能)。
- 异常的优先级高于当前运行等级。
- 异常未被异常屏蔽寄存器(如 PRIMASK)屏蔽。
注意:SVC 异常若在某个异常处理中被意外调用,且该异常处理的优先级不低于 SVC,则会触发 HardFault。
异常进入流程
异常进入包括以下步骤:
- 压栈:多个寄存器和返回地址压入当前使用的栈(MSP 或 PSP),使异常处理可用普通 C 函数实现。
- 取向量:取出异常向量(异常处理入口地址),可能与压栈并行执行以减少延迟。
- 取指令:根据异常向量取出待执行的异常处理指令。
- 更新寄存器:更新 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 缓存结构。
什么是环形缓冲区?
环形缓冲区,也叫循环缓冲区或环形队列,是一种首尾相连的先进先出数据结构。你可以把它想象成一个圆环:
它有两个指针:
- 写指针:指向下一个可以写入数据的位置。
- 读指针:指向下一个可以读取数据的位置。
当指针移动到缓冲区的末尾时,它会自动绕回到缓冲区的开头,这就是“环形”的由来。
环形缓冲区的优势:
- 高效的数据缓冲:平滑了数据流,防止了数据丢失。
- 预分配固定内存:内存大小在初始化时就确定了,避免了动态内存分配的不确定性和碎片问题,这在资源受限的嵌入式系统中至关重要。
- 高效的内存使用:通过“绕回”机制,可以循环利用一块固定的内存区域。
- 线程/中断安全:通过简单的机制(如关中断)可以实现生产者/消费者之间的安全数据交换。
为什么在嵌入式系统中需要它?
环形缓冲区解决了嵌入式系统中一个非常常见的问题:数据生产者和消费者速度不匹配。
典型场景:
- UART 串口通信:数据以字节流的形式异步到达(生产者),而你的主程序可能正在处理其他任务,无法立即处理每一个字节(消费者)。如果没有缓冲区,数据就会丢失。
- 数据采集系统:ADC 以固定速率采样数据(生产者),而 CPU 需要批量处理这些数据(消费者)。
- 多任务/中断环境:在一个中断服务程序中接收数据(生产者),在主循环中处理数据(消费者)。直接在主循环中访问中断产生的数据是不安全的。
简单实现:环形缓冲区必须区分 满 和 空。
名称 | 作用 |
buffer[] | 固定长度的数组 |
head | 写指针(生产者) |
tail | 读指针(消费者) |
size | 数组总长度 |
count(可选) | 当前数据数量 |
保留一个空位(最常用、最有效)
- 规则:始终保证写指针指向的位置是空的。当 (write_ptr + 1) % SIZE == read_ptr 时,认为缓冲区已满。
- 判断条件:空:read_ptr == write_ptr满:(write_ptr + 1) % SIZE == read_ptr
- 优点:逻辑清晰,判断高效。
- 缺点:缓冲区实际可用的容量是 SIZE - 1。例如,你申请了一个 100 字节的数组,最多只能存 99 字节数据。
1.
typedef struct {
uint8_t *buffer; // 缓冲区指针
uint16_t size; // 缓冲区大小
volatile uint16_t head; // 写指针
volatile uint16_t tail; // 读指针
} RingBuffer_t;
void RingBuffer_Push(RingBuffer_t *rb, uint8_t data)
{
uint16_t next = (rb->head + 1) % rb->size;
if (next == rb->tail) {
// 缓冲区满
return; // 或覆盖、或丢弃
}
rb->buffer[rb->head] = data;
rb->head = next;
}
uint8_t RingBuffer_Pop(RingBuffer_t *rb)
{
if (rb->tail == rb->head) {
return 0; // 空
}
uint8_t data = rb->buffer[rb->tail];
rb->tail = (rb->tail + 1) % rb->size;
return data;
}
2.
#define BUFFER_SIZE 64 // 实际可用大小为63
typedef struct {
uint8_t buffer[BUFFER_SIZE];
volatile uint32_t read_index; // 读指针,使用volatile防止编译器优化
volatile uint32_t write_index; // 写指针
} ring_buffer_t;
// 初始化缓冲区
void ring_buffer_init(ring_buffer_t *rb) {
rb->read_index = 0;
rb->write_index = 0;
}
// 检查缓冲区是否为空
bool ring_buffer_is_empty(ring_buffer_t *rb) {
return (rb->read_index == rb->write_index);
}
// 检查缓冲区是否已满
bool ring_buffer_is_full(ring_buffer_t *rb) {
return ((rb->write_index + 1) % BUFFER_SIZE) == rb->read_index;
}
// 写入一个字节(在中断或生产者线程中调用)
bool ring_buffer_put(ring_buffer_t *rb, uint8_t data) {
if (ring_buffer_is_full(rb)) {
return false; // 缓冲区满,写入失败
}
rb->buffer[rb->write_index] = data;
rb->write_index = (rb->write_index + 1) % BUFFER_SIZE;
return true; // 写入成功
}
// 读取一个字节(在主循环或消费者线程中调用)
bool ring_buffer_get(ring_buffer_t *rb, uint8_t *data) {
if (ring_buffer_is_empty(rb)) {
return false; // 缓冲区空,读取失败
}
*data = rb->buffer[rb->read_index];
rb->read_index = (rb->read_index + 1) % BUFFER_SIZE;
return true; // 读取成功
}
注意事项
- 并发安全:
- 上面的简单实现不是线程安全的。如果 get 和 put 可能在不同的线程(或主循环和 ISR)中同时调用,需要保护。
- 在 ISR 和主循环共享时,最简单的保护方法是在主循环中操作缓冲区前临时关闭中断,操作完成后再打开中断。对于更复杂的 RTOS 多任务环境,需要使用互斥锁。
- 数据溢出:
- 当缓冲区满时,新数据会被丢弃。在实际应用中,你可能需要记录溢出次数、触发告警或者采用其他策略。head/tail 必须取模:% size
- 批量读写:
- 上面的例子是单字节读写。你可以实现更高效的批量读写函数,一次性读取/写入多个字节,减少函数调用和指针绕回检查的开销。其实实际的应用场景应该也是一次读取或写入多个字节。
2.4 Hardfault
这个最好自己根据自己的项目找一个问题,可以一直说的!这个问的几率超级无敌大!!!
“HardFault” 是 ARM Cortex-M 系列处理器(例如 STM32)中的一种异常类型,通常表示系统发生了严重错误。
定义:
HardFault 是一种不可屏蔽的异常,当系统发生某些严重错误时,处理器会触发 HardFault 异常。它通常表示系统已经无法正常运行,需要进行错误处理。
在面试或实际开发中,90% 的 HardFault 都是由以下几个原因造成的:
非法指令:执行了一条无效的指令(例如,指令地址错误或指令本身无效)。
堆栈溢出:
局部变量太大(例如在函数内定义了一个 uint8_t buffer[10240])。
递归调用层次太深,导致 Stack 空间被耗尽,覆盖了其他内存区域。
内存访问错误:
野指针/空指针: 访问了 NULL 指针,或者指针指向了不存在的内存地址(例如访问外设时外设时钟没开,或者访问了保留地址)。
数组越界: 写到了数组之外的内存,破坏了堆栈。
特权级别错误:执行了特权指令,但当前运行在非特权模式下(例如,普通任务执行了只有操作系统才能执行的指令)。
中断优先级错误:中断优先级配置错误,导致高优先级中断被低优先级中断打断。
其他硬件错误:如总线错误、数据错误、整数除以 0 会触发 Fault 等。
发生 HardFault 时,CPU 做了什么?
理解这个过程对于调试至关重要。当异常发生时,Cortex-M 内核会自动将 CPU 当前的重要寄存器压入栈 (Push to Stack),保存“案发现场”。
压栈顺序如下(从高地址到低地址):
- xPSR (程序状态寄存器)
- PC (程序计数器 - 最重要,指向出错时即将执行的下一条指令)
- LR (链接寄存器 - 指向出错函数的返回地址)
- R12
- R3, R2, R1, R0
只要找到这个栈的位置,就能读出 PC 值,从而在汇编代码中找到出错的那一行。

调试方法:面试的时候说一下自己的调试方法将是加分项!!!
查看 LR 寄存器 (确定栈指针)
在进入 HardFault_Handler 的瞬间,查看 LR (Link Register) 的值。
- 如果 LR = 0xFFFFFFF9:说明出错时使用的是 MSP (主堆栈指针)。
- 如果 LR = 0xFFFFFFFD:说明出错时使用的是 PSP (进程堆栈指针,常用于 RTOS 任务中)。
找到栈地址,提取 PC 指针
根据第 1 步确定的栈(MSP 或 PSP),在内存窗口(Memory View)中查看该指针指向的地址。
- 找到栈里的内容,数到第 7 个 32 位数据(偏移量 0x18),那就是 PC 值。
- 数到第 8 个 32 位数据,那就是 LR 值(调用者)。
使用 addr2line 或 IDE 反汇编(进阶)
拿到 PC 地址后(比如 0x08001A24):
- 方法 A (IDE): 在 IDE 的 Disassembly (反汇编) 窗口输入这个地址,直接就能看到是哪一行汇编指令出错了。
- 方法 B (Map 文件): 查看编译生成的 .map 文件,看这个地址落在哪个函数范围内。
分析故障状态寄存器 (SCB Registers) —— 高阶调试
Cortex-M3/M4 提供了详细的故障分析寄存器,能告诉你具体是什么类型的错误:
- HFSR (HardFault Status Register)
- CFSR (Configurable Fault Status Register) —— 最核心包含了 UFSR (Usage Fault), BFSR (Bus Fault), MMFSR (MemManage Fault)。例子: 如果 BFSR 中的 IMPRECISERR 位置 1,说明是总线写错误(比如写了不存在的 Flash 地址)。
(1)善用硬件特性与断言
- 开启 MPU (Memory Protection Unit)作用: 它是硬件级的“防火墙”。你可以设置某些内存区域为“只读”或“不可执行”。效果: 一旦代码试图去写受保护的区域,MPU 会立即触发 MemManage Fault(比 HardFault 更容易定位),而不是等到数据被破坏后才莫名其妙地 Crash。
- 使用断言 (ASSERT)开发阶段: 在 main.h 或配置中开启 USE_FULL_ASSERT。可以多看看源码,很多地方都用了断言!作用: 如果你传入的参数不对(比如 GPIO 端口号超范围),程序会直接报错并停在 assert 处,告诉你哪个文件哪一行有问题,而不是等到硬件出错。
- 外设时钟检查常识: 操作任何外设寄存器前,必须先开启该外设的时钟 (RCC)。现象: 如果没开时钟直接读写寄存器,总线会锁死或产生 BusFault,进而升级为 HardFault。
(2)检查堆栈空间
堆栈溢出是常见的 HardFault 原因之一。可以通过以下方法检查堆栈空间是否足够:
- 检查堆栈配置:确保堆栈大小配置合理。例如,在 FreeRTOS 中,可以通过 configTOTAL_HEAP_SIZE 配置堆栈大小。
- 检查堆栈使用情况:使用调试工具(如 Keil、IAR 或 STM32CubeIDE)查看堆栈的使用情况。如果堆栈指针(SP)接近堆栈的起始地址或结束地址,说明堆栈可能不足。
- 别在栈上开大数组大数组(Buffer)请使用全局变量(Global/Static)或动态分配(Heap,但也需谨慎)。
- 警惕递归 (Recursion)原则: 嵌入式开发通常严禁递归,或者严格限制递归深度。原因: 每次递归都会压栈保存寄存器,层数一多,瞬间爆栈。
- 栈溢出检测 (Stack Paint)技巧: 在系统启动时,把整个栈区域填充为特定魔术字(如 0xDEADBEEF 或 0xCC)。检查: 运行一段时间后,查看内存,看看栈顶的魔术字是否还完好。如果魔术字被改写了,说明栈空间不够用,需要调大。
(3)检查内存访问
内存访问错误也是 HardFault 的常见原因。可以通过以下方法检查内存访问是否正确:
- 检查指针初始化:确保所有指针在使用前都已正确初始化。例如,避免使用未初始化的指针访问内存。
- 检查数组访问:确保数组访问没有越界。例如,避免访问数组的负索引或超出数组范围的索引。
- 使用内存保护单元(MPU):如果硬件支持,可以启用内存保护单元(MPU)来限制内存访问。MPU 可以检测非法的内存访问并触发异常。
(4)检查中断优先级
中断优先级配置错误可能导致 HardFault。可以通过以下方法检查中断优先级是否正确:
- 检查中断优先级配置:确保中断优先级配置合理。例如,避免高优先级中断被低优先级中断打断。
- 使用中断优先级分组:如果硬件支持,可以使用中断优先级分组来管理中断优先级。例如,在 STM32 中,可以通过 NVIC 的优先级分组寄存器(PRIGROUP)配置中断优先级分组。在 RTOS 中一般只设置抢占优先级,不设置子优先级!
- 开启栈溢出钩子函数:在 FreeRTOSConfig.h 中定义 #define configCHECK_FOR_STACK_OVERFLOW 2。实现 vApplicationStackOverflowHook 函数,一旦任务爆栈,系统会自动跳进来。
(5)使用调试工具(这个调试一定要会!!!)
调试工具(如 Keil、IAR 或 STM32CubeIDE)提供了丰富的调试功能,可以帮助定位 HardFault 的原因。例如:
- 设置断点:在 HardFault 异常处理函数中设置断点,当 HardFault 发生时,调试器会停在断点处,可以查看寄存器的值和调用栈信息。
- 查看调用栈:通过调试器的调用栈窗口,可以查看触发 HardFault 时的调用栈信息,从而定位问题。
- 单步调试:使用单步调试功能,逐步执行代码,观察变量的值和寄存器的变化,从而找到问题所在。
举个例子
1.通过查看这些寄存器的值,可以获取到触发异常时的指令地址(pc)、返回地址(lr)等信息,从而定位问题。前提是得了解各个寄存的作用!!!(ARM架构里面)
__asm void HardFault_Handler(void)
{
IMPORT HardFault_C_Handler // 声明C函数名
TST lr, #4 // 检查LR.bit2
ITE EQ
MRSEQ r0, MSP // EQ: 用MSP
MRSNE r0, PSP // NE: 用PSP
B HardFault_C_Handler // 跳转到C函数,R0作为参数
}
// C函数:必须与原函数名不一致,且接受R0传递的指针
void HardFault_C_Handler(uint32_t *hardfault_args)
{
// 1. 正确读取栈帧(Cortex-M自动压栈顺序)
uint32_t stacked_r0 = hardfault_args[0];
uint32_t stacked_r1 = hardfault_args[1];
uint32_t stacked_r2 = hardfault_args[2];
uint32_t stacked_r3 = hardfault_args[3];
uint32_t stacked_r12 = hardfault_args[4];
uint32_t stacked_lr = hardfault_args[5];
uint32_t stacked_pc = hardfault_args[6]; // 关键:出错指令地址
uint32_t stacked_psr = hardfault_args[7];
// 2. 保存到全局变量(供调试器查看,作为printf崩溃的备份)
static struct {
uint32_t r0, r1, r2, r3, r12, lr, pc, psr;
uint32_t cfsr, hfsr, mmfar, bfar, afsr;
uint8_t printf_ok; // 标记printf是否成功
} fault_info = {0};
fault_info.r0 = stacked_r0;
fault_info.r1 = stacked_r1;
fault_info.r2 = stacked_r2;
fault_info.r3 = stacked_r3;
fault_info.r12 = stacked_r12;
fault_info.lr = stacked_lr;
fault_info.pc = stacked_pc;
fault_info.psr = stacked_psr;
// 3. 读取核心Fault状态寄存器(查找根本原因)
fault_info.cfsr = SCB->CFSR; // 0xE000ED28
fault_info.hfsr = SCB->HFSR; // 0xE000ED2C
fault_info.mmfar = SCB->MMFAR; // 0xE000ED34
fault_info.bfar = SCB->BFAR; // 0xE000ED38
// 4. 【危险操作】尝试printf(可能触发二次Fault)
// 如果printf崩溃,fault_info数据已保存,调试器可查
fault_info.printf_ok = 0; // 标记未成功
// 使用snprintf格式化到缓冲区,减少printf工作量
char buffer[256];
snprintf(buffer, sizeof(buffer),
"\r\n"
"========== HARD FAULT ==========\r\n"
"PC = 0x%08lX (故障指令地址)\r\n"
"LR = 0x%08lX (返回地址)\r\n"
"CFSR= 0x%08lX (可配置Fault状态)\r\n"
"HFSR= 0x%08lX (HardFault状态)\r\n"
"R0 = 0x%08lX\r\n"
"R1 = 0x%08lX\r\n"
"R2 = 0x%08lX\r\n"
"R3 = 0x%08lX\r\n"
"R12 = 0x%08lX\r\n"
"PSR = 0x%08lX\r\n"
"--------------------------------\r\n",
fault_info.pc, fault_info.lr, fault_info.cfsr, fault_info.hfsr,
fault_info.r0, fault_info.r1, fault_info.r2, fault_info.r3,
fault_info.r12, fault_info.psr
);
// 尝试输出(如果UART还能工作)
printf("%s", buffer);
fault_info.printf_ok = 1; // 标记成功
// 5. 自动解析故障原因(打印到串口)
uart_print_fault_reason(fault_info.cfsr, fault_info.hfsr);
// 6. 安全停止:触发调试断点 + 死循环
__asm volatile("BKPT #0");
while(1) {
// 可选:LED闪烁提示(如果printf失败,至少有个灯)
// HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
// HAL_Delay(500); // 注意:HAL_Delay可能依赖Systick,在HardFault中可能不准
}
}
// 辅助函数:解析并打印故障原因(不依赖printf)
void uart_print_fault_reason(uint32_t cfsr, uint32_t hfsr)
{
printf("---- 故障原因分析 ----\r\n");
// 解析CFSR(Configuable Fault Status Register)
if(cfsr & (1 << 0)) printf(" [X] IACCVIOL: 指令访问冲突(非法PC跳转)\r\n");
if(cfsr & (1 << 1)) printf(" [X] DACCVIOL: 数据访问冲突(非法内存读写)\r\n");
if(cfsr & (1 << 3)) printf(" [X] MUNSTKERR: 异常返回时堆栈错误\r\n");
if(cfsr & (1 << 7)) printf(" [X] STKERR: 异常压栈错误 -> 栈溢出!\r\n");
if(cfsr & (1 << 8)) printf(" [X] UNSTKERR: 异常出栈错误\r\n");
if(cfsr & (1 << 9)) printf(" [X] PRECISERR: 精确总线错误(地址在BFAR中)\r\n");
if(cfsr & (1 << 10)) printf(" [X] IMPRECISERR: 非精确总线错误(写缓冲导致)\r\n");
if(cfsr & (1 << 11)) printf(" [X] BFARVALID: 总线错误地址有效(见BFAR)\r\n");
// 解析HFSR(HardFault Status Register)
if(hfsr & (1 << 30)) printf(" [X] FORCED: 其他Fault被强制升级为HardFault\r\n");
if(hfsr & (1 << 31)) printf(" [X] DEBUGEVT: 调试事件(忽略)\r\n");
printf("请用Keil反汇编PC地址,定位故障代码\r\n");
}
然后去 Keil 的 Debug 仿真中去运行


当发生 HardFault 的时候
复制 PC 寄存器的值,0x08005A38,去查询改地址对应的函数,就可以查询到了。至于 PC 存放的值到底是什么意思,具体可见


MDK 自带的方法(出自硬汉嵌入式论坛)


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