单片机程序调用子程序、进入中断后回到原来的位置,硬件错误中断定位(快速定位)产生的错误代码
单片机程序调用子程序、进入中断后回到原来的位置,硬件错误中断定位(快速定位)产生的错误代码
- 一、简介
- 1. 寄存器组
- 2.在 Thumb 状态下的 PC 寄存器(R15)
- 3.进入和退出中断/异常
- 1.进入
- 2.退出
- 4.特别说明
- 二、调用子程序
- 1分析寄存器
- 三、中断返回
- 1.分析反汇编
- 2.分析堆栈
- 四、硬件错误中断
- 1.实现一个硬件错误程序
- 2.定位错误位置
- 3.使用工具快速定位错误
一、简介
在编写程序时一般并不会在意在单片机中程序是如何执行的,在单片机中如何做到切换子程序后再回到原来的调用子程序的地方,在单片机中进入中断后和调用子程序回到原来的位置有什么区别,在编写程序后单片机运行时有时会进入硬件错误中断,时常为此抓破脑袋也不知是为何产生。
这篇文章将解决以下内容:
- 子程序和中断后如何回到原来的程序中及器区别;
- 出现硬件错误中断后如何定位(快速定位)发生错误的代码。
注意:这篇文章将不会深入讨论堆栈内容,所以子程序中将不会再有子程序调用。
解决以上问题将用到以下知识:
1. 寄存器组

具体的寄存器描述参照:CM3和CM4内核中的寄存器组R0-R15的作用,尽量详细+举例
2.在 Thumb 状态下的 PC 寄存器(R15)
在 CM3 中的指令至少是半字对齐的,所以 PC 的 LSB 总是读回 0。
在跳转程序时,直接写 PC 的值或者使用跳转指令,加载到 PC 的数值必须是奇数( LSB=1),用来表明是在Thumb 状态下执行。如果写入 PC 的数值中 LSB != 1,则视为企图转入 ARM 模式,CM3 将产生一个 fault 异常(这是因为CM3 只使用 Thumb‐2 指令集)。
3.进入和退出中断/异常
1.进入
响应异常/中断时,需要做以下动作:
- 1.寄存器(R0-R3、R12、LR、PC、xPSR)入栈;
- 2.取向量(从向量表中获取异常/中断的入口函数地址);
- 3.更新寄存器(更新堆栈指针SP,更新连接寄存器LR,更新程序计数器PC)
注意:在 异常/中断 中 LR 的值将被重新解释为 EXC_RETURN (一种新的值)
寄存器入栈的顺序:
2.退出
退出时只需要将 LR 的值送入 PC 即可,即启动处理器的中断返回序列:
- 1.出栈:将进入中断时压入栈中的寄存器恢复。
- 2.更新NVIC寄存器:随着异常的返回,它的活动位也被硬件清除。
4.特别说明
以下程序均使用 STM32F407VE 芯片,基于 RT-Thread Studio 平台的通用项目建立,并且部分使用 QEMU 仿真,硬件错误中断程序使用单片机实机运行。
二、调用子程序
编写以下内容(在同一文件)
/// 子程序
void subroutine(void)
{
int i = 0;
i++;
}
int main(void)
{
subroutine(); ///< 断点
return 0;
}
编译开始在线调试运行,并且在 main 函数的 subroutine() 函数调用处插入断点。
1分析寄存器
进入 main 函数后,寄存器中的值如下:

按下 F11 ,跳转进入 subroutine ,进入该函数后寄存器的值如下,需要特别关注的是 lr 寄存器的值,为 0x8002ca9 ,打开反汇编代码框显示如下图2,lr 中存储的即为在 main 函数中调用该函数的下一条命令 return 0 (地址为:为 0x8002ca8)。
至于 lr 中的值为为什么比真实的地址多1,请看 简介第2条


三、中断返回
编写以下内容(在同一文件),这会触发一个 PendSV 异常并且进入PendSV 中断。
#define NVIC_INT_CTRL 0xE000ED04 /* interrupt control state register */
#define NVIC_PENDSVSET 0x10000000 /* value to trigger PendSV exception */
void PendSV_Handler(void)
{
int i=0; ///< 断点
i++;
}
void subroutine(void)
{
int i = 0;
i++;
}
void trigger_PendSV(void)
{
*((volatile unsigned long *)(NVIC_INT_CTRL)) |= NVIC_PENDSVSET;
}
int main(void)
{
trigger_PendSV();
subroutine();
return 0;
}
1.分析反汇编
在中断服务函数 PendSV_Handler 的反汇编中,首先执行(这里没有谈内核隐含的自动压栈操作,隐含的操作在 简介第 3 条 详细介绍)寄存器 R7 的压栈操作,接着将 SP 指针向下偏移12个地址长度。

2.分析堆栈
查看 SP 寄存器所指的内存空间,从反汇编的内容可知,需要将 SP 指针按 32 位 4 字节地址移动 4 个长度,即为中断之前的内核的隐含入栈内容,这个内容指示了在退出中断后应该返回的位置。


四、硬件错误中断
如上所见,中断均可以使用该方式来查找中断返回的地址,换句话说,可以找到发生中断的大致位置,同理也可以使用该方法找到发生硬件错误的原因。
1.实现一个硬件错误程序
编写以下内容(在同一文件),这会触发一个 fault 异常并且进入 HardFault_Handler 中断。
注意:以下代码在单片机平台运行,仿真平台不会触发除零异常,也不会进入 HardFault_Handler 中断。
void HardFault_Handler(void)
{
while(1);
}
int main(void)
{
int i =0, k,j=10;
//使能除0异常
*((volatile unsigned long *)(0xE000ED14)) |= 0x10;
k = j/i;
return 0;
}
2.定位错误位置
借助 《三、中断返回》 章节的分析过程,可以在 SP 指针中找到对应的堆栈内存,从内存中可以找到 LR 指针和 PC 指针的值,这将作为定位错误的主要方式。
为了结合简述中的 EXC_RETURN 功能,这里使用了 RT-Thraed 操作系统,所以在进入 HardFault_Handler 中断服务函数时 lr 的值是 0xfffffffd ,表示返回线程时使用线程堆栈(PSP)。

查看 PSP 堆栈数据,以及查看 PSP 指针所指向的内存空间。


在获取到 LR(0x08000B49) 和 PC(0x0800FEDA) 的值后,需要使用反汇编的内容查找对应的地址来定位发生错误(触发 fault 异常的地址),一般来讲只需要使用 PC 的值即可定位到错误地点。

3.使用工具快速定位错误
上述的定位错误方式需要使用调试器来实时调试获取错误信息,这会增加许多的工作量,不是一个最优的定位错误的方法,推荐使用工具:CmBacktrace: ARM Cortex-M 系列 MCU 错误追踪库
CmBacktrace (Cortex Microcontroller Backtrace)是一款针对 ARM Cortex-M 系列 MCU 的错误代码自动追踪、定位,错误原因自动分析的开源库。该库的移植说明请参照开源库的 README_ZH.md 文件,本文不再赘述。
但是本文并不会完全使用该库,只使用了如下内容:
/// cm_backtrace/fault_handler/gcc/cmb_fault.S
.syntax unified
.thumb
.text
/* NOTE: If use this file's HardFault_Handler, please comments the HardFault_Handler code on other file. */
.global HardFault_Handler
.type HardFault_Handler, %function
HardFault_Handler:
MOV r0, lr /* get lr */
MOV r1, sp /* get stack pointer (current is MSP) */
BL cm_backtrace_fault
Fault_Loop:
BL Fault_Loop /* while(1) */
-------------------------------------------------------------------------------------------
/// cm_backtrace/cm_backtrace.c
void cm_backtrace_fault(uint32_t fault_handler_lr, uint32_t fault_handler_sp)
按需修改以上代码后,当出现错误时会在控制台依次打印 R0-R3、R12、LR、PC、xPSR 的值,拿到这些值后在开源库的目录 tools/addr2line/win64/addr2line.exe 找到该工具,在终端中使用该工具。
注意:使用该工具前,必须生成 .elf 文件。


更多推荐


所有评论(0)