在嵌入式开发中,一旦程序跳转到 HardFault_Handler 并停止响应,通常意味着发生了严重的运行时错误。
HardFault 本质
启动文件中可以看到:
HardFault_Handler\
PROC
EXPORT HardFault_Handler [WEAK]
B .
ENDP
B . 是死循环指令,程序执行到这里就会一直停住。
常见原因
数组越界
这是最常见的错误类型。
典型场景:
uint8_t buffer[10];
void process_data(uint8_t index)
{
buffer[index] = 0x55; // 如果 index >= 10,就异常了
}
实际项目中更常见的情况:
void uart_rx_handler(void)
{
static uint8_t rx_buffer[64];
static uint8_t rx_index = 0;
rx_buffer[rx_index++] = UART_DR; // 没做边界检查!
if (rx_index >= 64) {
// 下次写入就越界了
process_packet(rx_buffer);
rx_index = 0;
}
}
解决方法:
- 在数组访问前后加边界检查的断言
- 用调试器看 PC 指针停在哪,反汇编找到对应的 C 代码
- 检查数组索引变量的值,是否超出合理范围
防御性写法:
#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof(arr[0]))
void safe_write(uint8_t *buf, uint32_t size, uint32_t index, uint8_t value)
{
if (index < size) {
buf[index] = value;
} else {
// 记录错误日志,或者触发断言
while(1);
}
}
栈溢出
栈溢出的问题在于它往往不会立即触发 HardFault,而是改写其他内存区域的数据,导致程序在完全不相关的地方崩溃。
典型场景:
void deep_function(void)
{
char temp_buffer[512]; // 在栈上开大数组
memset(temp_buffer, 0, sizeof(temp_buffer));
// ...
}
void another_function(void)
{
deep_function(); // 栈空间不够用了
}
递归调用过深也会导致栈溢出:
int factorial(int n)
{
if (n <= 1) return 1;
return n * factorial(n - 1); // 如果 n 很大,栈就爆了
}
判断栈溢出的方法:
- 查看 Map 文件,确认栈空间分配大小
- 在栈底填充特殊值(如
0xDEADBEEF),运行一段时间后检查是否被改写
- 使用调试器查看 MSP(主栈指针)的值,是否超出栈空间范围
解决方法:
在启动文件中增大栈空间:
Stack_Size EQU 0x00001000 ; 4KB 栈空间
添加栈使用监控:
#define STACK_CANARY_VALUE 0xDEADBEEF
extern uint32_t __initial_sp;
extern uint32_t __StackLimit;
void check_stack_usage(void)
{
uint32_t *stack_bottom = &__StackLimit;
uint32_t used_words = 0;
while (*stack_bottom != STACK_CANARY_VALUE) {
used_words++;
stack_bottom++;
}
uint32_t used_bytes = used_words * 4;
// 打印栈使用量
}
野指针
野指针是最难排查的问题之一,因为它可能指向任何地址,症状千奇百怪。
未初始化的指针
void bad_function(void)
{
int *ptr; // 未初始化,指向随机地址
*ptr = 100; // 崩!
}
返回局部变量地址
uint8_t *get_buffer(void)
{
uint8_t local_buf[10];
return local_buf; // 返回局部变量的地址!
}
void caller(void)
{
uint8_t *buf = get_buffer();
buf[0] = 0x55; // local_buf 已经失效了
}
排查方法:
- 查看 LR(链接寄存器)的值,找到调用函数
- 检查指针的值是否合理(是否为 NULL,或指向有效地址)
- 在可疑的指针操作前后加打印,观察指针值
防御性写法:
void safe_ptr_write(int **pptr, int value)
{
if (pptr == NULL || *pptr == NULL) {
return; // 或者触发错误处理
}
**pptr = value;
}
// 指针初始化为 NULL,用完也置 NULL
int *ptr = NULL;
// 使用前检查
if (ptr != NULL) {
*ptr = 100;
}
对齐访问错误
典型场景:
void misaligned_access(void)
{
uint8_t buffer[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
// 地址不是 4 的倍数!
uint32_t *ptr = (uint32_t *)(buffer + 1);
uint32_t value = *ptr; // 可能触发 HardFault
}
结构体强制转换时也容易出现这个问题:
typedef struct {
uint8_t cmd;
uint32_t data; // 在内存中可能不是 4 字节对齐
} __attribute__((packed)) Packet_t;
void process_packet(uint8_t *raw_data)
{
Packet_t *pkt = (Packet_t *)raw_data;
uint32_t d = pkt->data; // 如果 raw_data 不是 4 的倍数,可能出问题
}
排查方法:
- 查看 CFSR(可配置故障状态寄存器)的 UNALIGNED 位
- 检查指针地址是否能被访问类型的大小整除
正确的做法:
uint32_t read_unaligned_u32(void *ptr)
{
uint8_t *p = (uint8_t *)ptr;
return ((uint32_t)p[0]) |
((uint32_t)p[1] << 8) |
((uint32_t)p[2] << 16) |
((uint32_t)p[3] << 24);
}
或使用 __packed 关键字(Keil)或 __attribute__((packed))(GCC),但要注意性能影响。
HardFault 处理
与其让程序死循环,不如打印有用的调试信息:
void HardFault_Handler(void)
{
uint32_t *sp;
__asm volatile(
"TST LR, #4\n"
"ITE EQ\n"
"MRSEQ %0, MSP\n"
"MRSNE %0, PSP\n"
: "=r" (sp)
);
printf("\n=== HardFault ===\n");
printf("PC = %08X\n", sp[6]);
printf("LR = %08X\n", sp[5]);
printf("PSR = %08X\n", sp[7]);
printf("CFSR= %08X\n", SCB->CFSR);
printf("HFSR= %08X\n", SCB->HFSR);
if (SCB->CFSR & 0x80000000) {
printf("BFAR= %08X\n", SCB->BFAR);
}
if (SCB->CFSR & 0x00800000) {
printf("MMFAR=%08X\n", SCB->MMFAR);
}
while (1);
}
减少 HardFault 的发生
- 数组访问做边界检查 — 不要假设输入数据总是合法的
- 栈空间开大一些 — RAM 不够时优化其他地方
- 指针初始化为 NULL — 使用前检查,使用后置 NULL
- 注意内存对齐 — 特别是结构体强制转换和 DMA 操作
- 开启编译器警告 — 把 warning 当作 error 处理
- 使用静态分析工具 — PC-lint、Coverity 等工具能发现很多隐患