MCU HardFault学习小记(二)——基于Arm Cortex-M系列

1 前言

MCU HardFault学习小记(一)——基于Arm Cortex-M系列中介绍了HardFault问题的背景和解决HardFault问题所需的理论知识,并介绍了两种HardFault分析法。本文将介绍两个HardFault实例现场,分别采用栈回溯法和CmBackTrace法对其进行分析,最后比较两种方法间的差异。

2 除零操作下的HardFault现场

编写该实例的环境配置见下表所示。

配置参数
处理器架构ARM Cortex-M3
操作系统环境裸机环境
预期实现功能自定义运算
引发HardFault类型除零操作
函数调用链1条

注意:在默认情况下执行除零操作时,Cortex-M3不会触发除零异常。所以代码在初始化时因使能除零异常触发,具体方法为设置SCB寄存器组的CCR(配置控制寄存器)的第4位为1,代码内容如下。

void fault_test_by_div0(void)
{
    volatile int *SCB_CCR = (volatile int *)0xE000ED14; // SCB->CCR
    uint8_t Value_Array[] = {8, 10, 2, 7, 5, 0, 6, 8, 9, 8};
    uint8_t sum;
    *SCB_CCR |= (1 << 4); /* bit4: DIV_0_TRP. */

    for (uint8_t i = 0; i < sizeof(Value_Array) / sizeof(Value_Array[0]); i++) {
        sum += 10 / Value_Array[i];
}

    printf("sum is %d\n", sum);
}

int func1(int a, int b)
{
    a += (a + b);
    b += (a - b);

    fault_test_by_div0();

    return a + b;
}

int func2(int a, int b)
{
    int c = func1(a, b);
    c++;

    if (c > 0) {
        printf("%d\n", c);
    }

    return c;
}

void func3(void *parameter)
{
    int c = 0;
    c = func2(7, 9);
    c++;

    if (c > 0) {
        printf("%d\n", c);
    }
}

2.1 栈回溯法

将开发板与调试器连接,烧录代码,进入调试模式,点击全速运行按钮,进入异常处理函数HardFault_Handler中。使用栈回溯法分析过程如下:

  1. 打开寄存器窗口,如下图所示。LR = 0xFFFFFFF9,bit2 = 0,可知进入异常前系统使用的是主栈指针MSP,从Banked栏获取MSP栈指针值为0x20000830。

![[Pasted image 20230702104442.png|300]]

  1. 打开memory窗口,输入栈指针值,获得函数调用栈,根据Cortex-M3压栈顺序,红圈标注即为PC值,PC = 0x08001A0A。打开汇编窗口,输入PC值跳转到进入HardFault异常的代码位置。

![[Pasted image 20230702104637.png|700]]

![[Pasted image 20230702104727.png|500]]

  1. 解析分散加载文件,计算代码地址范围。打开工程的分散加载文件,第一个运行时域存储着Code和RO-data,起始地址为0x08000000。终止地址 = 起始地址 + Code + RO-data,Code和RO-data占用大小可在编译输出窗口中查看。这里为0x000027C0,即代码地址区间为[0x08000000, 0x080027C0)。向下解析函数调用栈,栈值为奇数且在该地址区间的即为函数的返回地址LR,依次圈出在Memory窗口中。对各个函数现场的LR值进行反汇编,即可得到函数的调用关系:

m a i n ( ) − > f u n c 3 ( ) − > f u n c 2 ( ) − > f u n c 1 ( ) − > f a u l t _ t e s t _ b y _ d i v 0 ( ) main()->func3()->func2()->func1()->fault\_test\_by\_div0() main()>func3()>func2()>func1()>fault_test_by_div0()

![[Pasted image 20230702105305.png|800]]

![[Pasted image 20230702105355.png|800]]

  1. 打开Fault Report窗口,查看已被置位的状态位,如下图所示。图中用红圈标注的状态位显示为置位状态,该状态位表示系统企图执行除零操作,说明代码某处在执行除零操作时进入HardFault异常。

![[Pasted image 20230702105519.png|300]]

通过上述步骤,我们得到了函数的调用关系并分析出系统是在执行语句sum += 10 / Value_Array[i]时产生了除零异常。

2.2 CmBackTrace查找法

移植CmBackTrace开源库至项目工程中,烧录并运行代码,通过串口在上位机中观察信息输出,如下图所示。

![[Pasted image 20230702110837.png|900]]

在工程output文件夹中打开命令行,运行异常信息提示指令,得到结果如下图。

![[Pasted image 20230702111017.png|900]]

由此易得函数的调用关系:

m a i n ( ) − > f u n c 3 ( ) − > f u n c 2 ( ) − > f u n c 1 ( ) − > f a u l t _ t e s t _ b y _ d i v 0 ( ) main()->func3()->func2()->func1()->fault\_test\_by\_div0() main()>func3()>func2()>func1()>fault_test_by_div0()

产生除零异常的代码位置是在fault_test.c文件中的第48行,在项目文件中找到此位置对应内容如下。不难发现数组Value_Array中有一个元素为数值0,该元素作为除数参与了除法运算,因此产生了除零异常。

![[Pasted image 20230702111411.png|900]]

至此,我们使用CmBackTrace查找法成功定位到问题点和函数调用信息,CmBackTrace的信息输出还包含着线程堆栈信息和寄存器信息,可使用调试工具在调试状态下进行辅助分析。

3 访问非法地址的HardFault现场

编写该实例的环境配置见下表所示。

配置参数
处理器架构ARM Cortex-M3
操作系统环境FreeRTOS
预期实现功能设备读写操作
引发HardFault类型不精确的数据总线错误
函数调用链2条

代码内容如下。

/**
 * @brief     为了方便管理,所有的任务创建函数都放在这个函数里面
 */
static void AppTaskCreate(void)
{
    BaseType_t xReturn = pdPASS;

    taskENTER_CRITICAL();

    xReturn = xTaskCreate((TaskFunction_t)i2c_at24c02_read_task, (const char *)"AT24C02 TASK", (uint32_t)128, (void *)NULL, (UBaseType_t)3, (TaskHandle_t *)Task_Handle1);

    if (pdPASS == xReturn) {
        printf("AT24C02 READ任务创建成功\r\n");
    }

    xReturn = xTaskCreate((TaskFunction_t)sensor_read_task, (const char *)"SENSOR TASK", (uint32_t)128, (void *)NULL,
(UBaseType_t)4, (TaskHandle_t *)&Task_Handle2);

    if (pdPASS == xReturn) {
        printf("SENSOR READ任务创建成功\r\n");
    }

    /* 通常情况下任务是无法返回的,但任务创建函数只需要执行一次,所以需要删除该任务 */
    vTaskDelete(AppTaskCreate_Handle);

    taskEXIT_CRITICAL();
}

typedef uint32_t (*device_write)(int16_t param1, void *priv);
typedef uint32_t (*device_read)(uint16_t param1, uint8_t param2);

typedef struct {
    device_write write;
    device_read read;
} device_ops_t;

typedef struct {
    device_ops_t *ops;
    void *argc;
} device_t;

static uint32_t i2c_read(uint16_t data, uint8_t address)
{
    return SUCCESS;
}

static device_read func_get_addr(void)
{
    return i2c_read;
}

void fault_test_by_illegal_address(uint16_t data, uint8_t address)
{
    device_t *a;

    a = pvPortMalloc(sizeof(device_t));
	a->ops->read = func_get_addr();
	a->ops->read(data, address);
}

void i2c_mpu6050_read(void)
{
    uint16_t data;
    fault_test_by_illegal_address(data, 0x68);
    printf("data is %d", data);
}

void sensor_read_task(void *parameter)
{
    for ( ; ; ) {   
        i2c_mpu6050_read();
        vTaskDelay(200);
    }
}

void i2c_at24c02_read_task(void *parameter)
{
    uint16_t data;
    for ( ; ; ) {
        fault_test_by_illegal_address(data, 0x24);
        printf("data is %d", data);
        vTaskDelay(200);
    }
}

3.1 栈回溯法

将开发板与调试器连接,烧录代码,进入调试模式,点击全速运行按钮,进入异常处理函数HardFault_Handler中。使用栈回溯法分析过程如下:

  1. 打开寄存器窗口,如下图所示。LR = 0xFFFFFFFD,bit2 = 1,可知进入异常前系统使用的是线程栈指针PSP,从Banked栏获取PSP栈指针值为0x200013B0。

![[Pasted image 20230702113936.png|300]]

  1. 打开memory窗口,输入栈指针值,得到函数调用栈,根据Cortex-M3压栈顺序,红圈标注即为PC值,PC = 0x08001AC2。打开汇编窗口,输入PC值跳转到进入HardFault异常的代码位置。

![[Pasted image 20230702114249.png|700]]

![[Pasted image 20230702114412.png|500]]

  1. 计算代码地址范围。该例的分散加载文件同上例保持一致,故起始地址不变,为0x08000000。在编译窗口中可以查看Code和RO-data的占用大小,计算出的终止地址 = 起始地址 + Code +RO-data = 0x080034E4,即代码地址区间为[0x08000000, 0x080034E4)。向下解析函数调用栈,栈值为奇数且在该地址区间的即为函数的返回地址LR,依次圈出在Memory窗口中。对各个函数现场的LR值进行反汇编,即可得到函数的调用关系:

s e n s o r _ r a e d _ t a s k ( ) − > i 2 c _ m p u 6050 _ r e a d ( ) − > f a u l t _ t e s t _ b y _ i l l g e a l _ a d d r e s s ( ) sensor\_raed\_task()->i2c\_mpu6050\_read()->fault\_test\_by\_illgeal\_address() sensor_raed_task()>i2c_mpu6050_read()>fault_test_by_illgeal_address()

  1. 打开Fault Report窗口,查看已被置位的状态位,如下图所示。图中用红圈标注的状态位显示为置位状态,该状态位表示系统出现了数据总线错误,且堆栈返回的是合法的地址,与错误无关

![[Pasted image 20230702115128.png|300]]

通过上述一系列查找操作,我们得到了函数的调用关系并分析处系统是在a->ops->read(data, address)处产生了数据总线异常。结合代码上下文进一步分析,我们得知系统仅为ops指针分配了4字节内存,未给ops的结构体成员分配内存空间。系统将合法地址赋给了一块未初始化的区域,因而进入HardFault异常。

3.2 CmBackTrace查找法

移植CmBackTrace开源库至项目工程中,烧录并运行代码,通过串口在上位机中观察信息输出,如下图所示。

![[Pasted image 20230702115952.png|900]]

在工程output文件夹中打开命令行,运行异常信息提示指令,得到结果如下图所示。

![[Pasted image 20230702120118.png|900]]、、

由此易得函数的调用关系:

s e n s o r _ r a e d _ t a s k ( ) − > i 2 c _ m p u 6050 _ r e a d ( ) − > f a u l t _ t e s t _ b y _ i l l g e a l _ a d d r e s s ( ) sensor\_raed\_task()->i2c\_mpu6050\_read()->fault\_test\_by\_illgeal\_address() sensor_raed_task()>i2c_mpu6050_read()>fault_test_by_illgeal_address()

产生不精确的数据总线错误的代码位置是在fault_test.c文件中的第178行,在项目文件中找到此位置对应内容如下。进一步分析可以发现,系统仅为ops指针分配了4字节内存,未给结构体成员分配内存空间。系统将合法地址赋给了一块未初始化的区域,因而进入HardFault异常。

![[Pasted image 20230702120358.png|900]]

4 查找方法比较

在上文节介绍了两个HardFault实例现场并分别使用栈回溯法和CmBackTrace查找法定位和解决问题。两种方法均能有效地找到问题点和异常原因。

栈回溯法的优势在于不需要额外添加代码,节省编译固件空间。缺点为:(1)只能在仿真状态下调试,对环境要求高。(2)要求开发人员对程序调用压栈/出栈原理有清晰的理解。(3)分析偶发性HardFault问题效率低下,耗费大量时间。

相较于栈回溯法,CmBackTrace查找法的缺点在于移植代码过程较为繁琐,且需要针对不同类型工程编写不同的配置文件。优点显而易见:(1)无需在仿真状态下调试,可以简单高效地完成问题查找和分析。(2)对新手较为友好,无需了解复杂的处理器架构和压栈原理,极大简化了HardFault分析的流程。(3)该方法在系统正常状态下运行时仍可以使用,能高效定位偶发性HardFault问题。(4)该方法具有较大的提升空间。在实际开发中可以根据需求,将错误信息保存在Flash中或发送至云端,节省了维护成本,对工程领域而言有着很高的价值。

5 小结

本文编写了两个HardFault实例,采用栈回溯法和CmBackTrace查找法对HardFault现场进行分析,从分析过程、适用场合等角度比较两种方法之间的差异。

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

青渡QAQ

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值