HardFault 问题解决方案记录
问题概述
现象: 系统运行过程中随机触发 HardFault,导致单片机重启
错误信息:
HardFault_Handler
mcause: 0x00000004 (RISC-V Load Address Misaligned)
mtval: 0x2000000f (出错的内存地址 - 奇数地址,未对齐)
mepc: 0x000028c2 (出错指令 PC,位于 swap_endian 函数内)
问题分析
1. 异常类型分析
mcause = 0x04: RISC-V 异常码 #4,表示 "Load Address Misaligned"(加载地址未对齐)
mtval = 0x2000000f: 触发异常的地址,末位为 0xf(奇数),不是 2 字节或 4 字节对齐地址
mepc = 0x28c2: 异常指令位于 swap_endian 函数内,反汇编显示指令为 lhu a5,0(a0)(加载半字)
2. 代码追踪
通过分析汇编代码和 Map 文件,定位问题调用链:
user_mb_app_m.c:76
slavePollingCallbacksRouter(pucRegBuffer)
→ parse_polling_request_solve()
→ parse_polling_request_solve_bit16()
→ swap_endian(data, info->quantity) ← 崩溃点
3. 根本原因
类型不匹配导致未对齐访问:
user_mb_app_m.c 中调用 slavePollingCallbacksRouter(pucRegBuffer)
参数 pucRegBuffer 类型为 uint8_t*(单字节指针),地址可能是奇数(如 0x2000000f)
函数内部将 uint8_t* 强制当作 uint16_t* 处理
swap_endian 函数直接解引用 uint16_t*:data[i] = ...
RISC-V 架构要求 16 位访问必须 2 字节对齐,奇数地址触发 HardFault
4. 为什么之前没发现问题
Modbus 数据缓冲区通常从偶数地址开始分配,问题被掩盖
流程引擎加入后,内存使用模式改变,缓冲区可能落在奇数地址
RISC-V 架构对内存对齐要求严格(ARM Cortex-M 可以配置允许非对齐访问)
解决方案
修改方案
文件: ModbusTCPtoRTUGateway/User/gateway.c
原代码:
static void swap_endian(uint16_t *data, size_t length)
{
for (size_t i = 0; i < length; ++i)
{
// 直接解引用 uint16_t*,要求地址 2 字节对齐
data[i] = ((data[i] >> 8) & 0xFF) | ((data[i] << 8) & 0xFF00);
}
}
修复后代码:
// 【修复】使用逐字节处理,避免未对齐访问
static void swap_endian(uint16_t *data, size_t length)
{
uint8_t *p = (uint8_t *)data;
for (size_t i = 0; i < length; ++i)
{
// 逐字节读取(安全,不依赖对齐)
uint8_t b0 = p[i * 2];
uint8_t b1 = p[i * 2 + 1];
// 交换后写回
p[i * 2] = b1;
p[i * 2 + 1] = b0;
}
}
修复要点
逐字节访问: 将 uint16_t* 强制转为 uint8_t*,按字节访问
无对齐要求: 字节访问可以在任意地址进行,不受对齐限制
功能等价: 仍然完成大小端交换的功能,只是实现方式更安全
验证测试
测试步骤
重新编译固件
烧录到设备
运行可视化流程,触发 Modbus 读写操作
观察是否还有 HardFault
预期结果
不再出现 mcause: 0x00000004 错误
流程引擎可以正常执行多个周期
Modbus 读写操作正常工作
相关修改记录
本次修改文件
文件
修改内容
原因
gateway.c
修改 swap_endian 函数实现
避免未对齐内存访问
历史相关修改(未完全解决问题)
文件
修改内容
原因
flow_engine.c
使用 memcpy 复制节点到 RAM
解决 Flash XIP packed 结构体未对齐问题
flow_storage.h
添加安全访问辅助函数
提供安全的节点字段访问接口
flow_engine.h
增加任务栈大小到 640
防止栈溢出(后发现不是根本原因)
经验总结
1. RISC-V 架构注意事项
严格对齐要求: 16 位访问必须 2 字节对齐,32 位访问必须 4 字节对齐
与 ARM 差异: ARM Cortex-M 可以配置允许非对齐访问,RISC-V 通常不行
编译器行为: __attribute__((packed)) 结构体中的多字节字段可能未对齐,访问时需要特殊处理
2. 调试技巧
mcause/mepc/mtval: 这三个寄存器是定位 HardFault 的关键
Map 文件: 通过 mepc 地址查找对应函数,缩小问题范围
反汇编: 查看具体指令(如 lhu)确认访问类型和寄存器
3. 防御性编程建议
类型安全: 避免 uint8_t* 和 uint16_t* 混用,必要时使用逐字节访问
结构体定义: packed 结构体中的多字节字段,访问时使用 memcpy 或逐字节读取
代码审查: 检查所有类型强制转换,特别是涉及硬件寄存器或通信缓冲区的地方
附录:相关代码片段
调用链详情
// FreeModbus/port/user_mb_app_m.c:76
eMBErrorCode eMBMasterRegInputCB(..., UCHAR *pucRegBuffer, ...)
{
// pucRegBuffer 是 uint8_t*,可能为奇数地址
slavePollingCallbacksRouter(pucRegBuffer); // ← 传入未对齐指针
}
// gateway.c:2278
void slavePollingCallbacksRouter(uint16_t *data) // ← 被当作 uint16_t*
{
parse_polling_request_solve(receive_info, data);
}
// gateway.c:761
void parse_polling_request_solve_bit16(..., uint16_t *data)
{
swap_endian(data, info->quantity); // ← 解引用触发 HardFault
}
汇编指令分析
28c2: lhu a5,0(a0) # 加载半字(16位),要求 a0 寄存器值为偶数
当 a0 = 0x2000000f(奇数)时,触发 Load Address Misaligned 异常。
记录日期: 2026-04-04
记录人: SHEN.ZT
状态: 已解决,已验证