【Cortex-M】异常中断时的程序运行指针SP获取,及SCB寄存器错误类型获取

【Cortex-M】异常中断时的程序运行指针SP获取,及SCB寄存器错误类型获取

更新以gitee为准:
gitee

异常中断

当MCU因堆栈溢出等造成异常会 会进入异常中断
可以在startup.s中查看
在这里插入图片描述
通常引起的时HardFault中断(包括内存溢出、非法赋值、寄存器操作错误、堆栈溢出等)
譬如手动引起异常:

*(uint32_t *)0x45678912=1;

异常的程序运行指针SP获取

打个断点在异常代码处 可以看到地址为0x08003792
在这里插入图片描述
在HardFault_Handler中也打个断点 就能在运行到其中时停止
在这里插入图片描述
此时 LR寄存器为特殊标记值
0xFFFFFFE9对应的是要看MSP寄存器
0xFFFFFFFD对应的是要看PSP寄存器
具体根据芯片手册而定(cortex-M权威指南)
不过也有个更方便的方式是直接读取SP指针
通过汇编即可实现

__asm uint32_t MZ_CM_ReadReg13(void){
     MOV     R0, sp
     bx lr
}

那么 就可以通过此方式获取到0x200101B0
通过内存查看该地址:
在这里插入图片描述
第5和6个uint32值即为上一次异常程序地址 和如果正常运行的话 下一个程序地址
注意为小端格式 所以解析出来就是0x08003793和0x0800379A
在这里插入图片描述
但程序地址的最后一位是标志位 用于区分是否为thumb指令或ARM指令(cortex权威指南)

由于历史原因,最初的16位单片机用的指令是Thumb指令,为16位指令。后来,ARM推出了32位的ARM指令。16位的指令更适合空间有限的嵌入式处理器,而32位的指令则更加灵活、功能丰富。因此,有些单片机在运行时可以根据需求切换指令状态。如下图:
在这里插入图片描述

后来诞生了Thumb-2指令集,它实现了16位指令和32位指令的共存。而Cortex-M3架构则就是采用Thumb-2指令集。由于16位指令长度是2字节,32位指令长度是4字节,因此其PC指针地址是按照2字节(16位)对齐的。换言之其最低位一定是0。

另外 需要注意的是 如果在代码层面进行获取 不能包装子函数(子函数会导致LR和SP被重写) 所以直接用一个宏替代即可

#define MZ_CM_SP_UpdateStat() MZ_CM_SP_Stat.sp = MZ_CM_ReadReg13();MZ_CM_SP_Stat.spAddr = (uint32_t *)MZ_CM_SP_Stat.sp;MZ_CM_SP_Stat.lastFlashAddr = MZ_CM_SP_Stat.spAddr[5];MZ_CM_SP_Stat.nextFlashAddr = MZ_CM_SP_Stat.spAddr[6];

调用后 如下:
在这里插入图片描述
后一个地址正是如果正常运行的下一个程序语句地址
在这里插入图片描述

SCB寄存器错误类型获取

关键寄存器:

SCB->CFSR(可配置故障状态寄存器):判断故障类型(如内存访问错误、未对齐访问、指令执行错误)。

SCB->HFSR(硬件故障状态寄存器):确认是否为HardFault的直接触发原因。

SCB->MMFAR/SCB->BFAR:记录导致内存管理或总线故障的具体地址。

直接通过memcpy获取即可

硬件错误异常 Hard fault status register (SCB->HFSR)

在这里插入图片描述

存储器管理错误异常 SCB->CFSR中MMFSR位

在这里插入图片描述

总线错误异常 SCB->CFSR中BFSR位

在这里插入图片描述

使用错误异常 SCB->CFSR中UFSR位

在这里插入图片描述

附录:压缩字符串、大小端格式转换

压缩字符串

首先HART数据格式如下:
在这里插入图片描述
在这里插入图片描述
重点就是浮点数和字符串类型
Latin-1就不说了 基本用不到

浮点数

浮点数里面 如 0x40 80 00 00表示4.0f

在HART协议里面 浮点数是按大端格式发送的 就是高位先发送 低位后发送

发送出来的数组为:40,80,00,00

但在C语言对浮点数的存储中 是按小端格式来存储的 也就是40在高位 00在低位
浮点数:4.0f
地址0x1000对应00
地址0x1001对应00
地址0x1002对应80
地址0x1003对应40

若直接使用memcpy函数 则需要进行大小端转换 否则会存储为:
地址0x1000对应40
地址0x1001对应80
地址0x1002对应00
地址0x1003对应00

大小端转换:

void swap32(void * p)
{
   uint32_t *ptr=p;
   uint32_t x = *ptr;
   x = (x << 16) | (x >> 16);
   x = ((x & 0x00FF00FF) << 8) | ((x >> 8) & 0x00FF00FF);

   *ptr=x;
}

压缩Packed-ASCII字符串

本质上是将原本的ASCII的最高2位去掉 然后拼接起来 比如空格(0x20)
四个空格拼接后就成了
1000 0010 0000 1000 0010 0000
十六进制:82 08 20
对了一下表 0x20之前的识别不了
也就是只能识别0x20-0x5F的ASCII表
在这里插入图片描述

压缩/解压函数后面再写:

//传入的字符串和数字必须提前声明 且字符串大小至少为str_len 数组大小至少为str_len%4*3 str_len必须为4的倍数
uint8_t Trans_ASCII_to_Pack(uint8_t * str,uint8_t * buf,const uint8_t str_len)
{
   if(str_len%4)
   {
      return 0;
   }
	 
   uint8_t i=0;
   memset(buf,0,str_len/4*3);	  
   for(i=0;i<str_len;i++)
   {
      if(str[i]==0x00)
      {
         str[i]=0x20;
      }
   }

   for(i=0;i<str_len/4;i++)
   {
      buf[3*i]=(str[4*i]<<2)|((str[4*i+1]>>4)&0x03);
      buf[3*i+1]=(str[4*i+1]<<4)|((str[4*i+2]>>2)&0x0F);
      buf[3*i+2]=(str[4*i+2]<<6)|(str[4*i+3]&0x3F);
   }

   return 1;
}

//传入的字符串和数字必须提前声明 且字符串大小至少为str_len 数组大小至少为str_len%4*3 str_len必须为4的倍数
uint8_t Trans_Pack_to_ASCII(uint8_t * str,uint8_t * buf,const uint8_t str_len)
{
   if(str_len%4)
   {
      return 0;
   }

   uint8_t i=0;

   memset(str,0,str_len);

   for(i=0;i<str_len/4;i++)
   {
      str[4*i]=(buf[3*i]>>2)&0x3F;
      str[4*i+1]=((buf[3*i]<<4)&0x30)|(buf[3*i+1]>>4);
      str[4*i+2]=((buf[3*i+1]<<2)&0x3C)|(buf[3*i+2]>>6);
      str[4*i+3]=buf[3*i+2]&0x3F;
   }

   return 1;
}


大小端转换

在串口等数据解析中 难免遇到大小端格式问题

什么是大端和小端

所谓的大端模式,就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。

所谓的小端模式,就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。

简单来说:大端——高尾端,小端——低尾端

举个例子,比如数字 0x12 34 56 78在内存中的表示形式为:

1)大端模式:

低地址 -----------------> 高地址

0x12 | 0x34 | 0x56 | 0x78

2)小端模式:

低地址 ------------------> 高地址

0x78 | 0x56 | 0x34 | 0x12

可见,大端模式和字符串的存储模式类似。

数据传输中的大小端

比如地址位、起止位一般都是大端格式
如:
起始位:0x520A
则发送的buf应为{0x52,0x0A}

而数据位一般是小端格式(单字节无大小端之分)
如:
一个16位的数据发送出来为{0x52,0x0A}
则对应的uint16_t类型数为: 0x0A52

而对于浮点数4.0f 转为32位应是:
40 80 00 00

以大端存储来说 发送出来的buf就是依次发送 40 80 00 00

以小端存储来说 则发送 00 00 80 40

由于memcpy等函数 是按字节地址进行复制 其复制的格式为小端格式 所以当数据为小端存储时 不用进行大小端转换
如:

uint32_t dat=0;
uint8_t buf[]={0x00,0x00,0x80,0x40};
   memcpy(&dat,buf,4);
   float f=0.0f;
   f=*((float*)&dat); //地址强转
   printf("%f",f);

或更优解:

   uint8_t buf[]={0x00,0x00,0x80,0x40};   
   float f=0.0f;
   memcpy(&f,buf,4);

而对于大端存储的数据(如HART协议数据 全为大端格式) 其复制的格式仍然为小端格式 所以当数据为小端存储时 要进行大小端转换
如:

uint32_t dat=0;
uint8_t buf[]={0x40,0x80,0x00,0x00};
   memcpy(&dat,buf,4);
   float f=0.0f;
   swap32(&dat); //大小端转换
   f=*((float*)&dat); //地址强转
   printf("%f",f);

或:

uint8_t buf[]={0x40,0x80,0x00,0x00};
   memcpy(&dat,buf,4);
   float f=0.0f;
   swap32(&f); //大小端转换
   printf("%f",f);

或更优解:

uint32_t dat=0;
uint8_t buf[]={0x40,0x80,0x00,0x00};
   float f=0.0f;
   dat=(buf[0]<<24)|(buf[0]<<16)|(buf[0]<<8)|(buf[0]<<0)
   f=*((float*)&dat);

总结

固 若数据为小端格式 则可以直接用memcpy函数进行转换 否则通过移位的方式再进行地址强转

对于多位数据 比如同时传两个浮点数 则可以定义结构体之后进行memcpy复制(数据为小端格式)

对于小端数据 直接用memcpy写入即可 若是浮点数 也不用再进行强转

对于大端数据 如果不嫌麻烦 或想使代码更加简洁(但执行效率会降低) 也可以先用memcpy写入结构体之后再调用大小端转换函数 但这里需要注意的是 结构体必须全为无符号整型 浮点型只能在大小端转换写入之后再次强转 若结构体内采用浮点型 则需要强转两次

所以对于大端数据 推荐通过移位的方式来进行赋值 然后再进行个别数的强转 再往通用结构体进行写入

多个不同变量大小的结构体 要主要字节对齐的问题
可以用#pragma pack(1) 使其对齐为1
但会影响效率

大小端转换函数

直接通过对地址的操作来实现 传入的变量为32位的变量
中间变量ptr是传入变量的地址

void swap16(void * p)
{
   uint16_t *ptr=p;
   uint16_t x = *ptr;
   x = (x << 8) | (x >> 8);

   *ptr=x;
}

void swap32(void * p)
{
   uint32_t *ptr=p;
   uint32_t x = *ptr;
   x = (x << 16) | (x >> 16);
   x = ((x & 0x00FF00FF) << 8) | ((x >> 8) & 0x00FF00FF);

   *ptr=x;
}

void swap64(void * p)
{
   uint64_t *ptr=p;
   uint64_t x = *ptr;
   x = (x << 32) | (x >> 32);
   x = ((x & 0x0000FFFF0000FFFF) << 16) | ((x >> 16) & 0x0000FFFF0000FFFF);
   x = ((x & 0x00FF00FF00FF00FF) << 8) | ((x >> 8) & 0x00FF00FF00FF00FF);

   *ptr=x;
}
<think>我们正在讨论Cortex-M系列中的VTOR寄存器。根据引用[1],VTOR(向量表偏移寄存器)是Cortex-M3和Cortex-M4处理器中的一个可编程寄存器,用于实现向量表重定向功能。默认情况下,向量表从地址0开始,但通过VTOR,我们可以将向量表重定位到其他地址,例如SRAM或不同的Flash区域。 ### VTOR寄存器工作机制详解 1. **向量表的作用**: - 向量表是一个字(32位)数组,每个条目对应一个异常或中断的处理函数的入口地址。 - 第一个条目(索引0)是主堆栈指针(MSP)的初始值,第二个条目(索引1)是复位处理函数的地址,后续条目对应其他异常和中断。 2. **向量表的位置**: - 默认情况下,向量表位于地址0x00000000。但是,许多微控制器将启动存储器(如Flash)映射到地址0,因此向量表通常位于Flash的起始位置。 - 通过设置VTOR寄存器,可以将向量表重定位到其他地址。例如,在Bootloader跳转到应用程序,需要将向量表设置为应用程序的向量表地址。 3. **VTOR寄存器的结构**: - VTOR寄存器是一个32寄存器,其地址为0xE000ED08(在系统控制块SCB中)。 -寄存器分为两部分: - **TBLOFF**(位[31:7]):向量表基地址的高25位。由于向量表要求对齐(至少128字节,但通常要求256字节或512字节对齐),所以基地址的低7位(位[6:0])必须为0。因此,写入VTOR的地址必须是128字节(0x80)的倍数。 - **Reserved**(位[6:0]):保留,必须为0。 4. **向量表重定位步骤**(以从Flash重定位到SRAM为例): - **复制向量表**:将原始向量表(位于Flash)复制到新的位置(如SRAM的起始地址0x20000000)。 ```c #define HW32_REG(ADDRESS) (*((volatile unsigned long *)(ADDRESS))) #define VTOR_NEW_ADDR 0x20000000 for (int i = 0; i < 48; i++) { // 假设复制48个向量(根据实际异常和中断数量调整) HW32_REG(VTOR_NEW_ADDR + (i << 2)) = HW32_REG(i << 2); } ``` - **设置内存屏障**:确保复制操作完成。 ```c __DMB(); // 数据存储器屏障,确保写操作完成 ``` - **更新VTOR寄存器**: ```c SCB->VTOR = VTOR_NEW_ADDR; // 设置VTOR寄存器 __DSB(); // 数据同步屏障,确保新配置生效 ``` 5. **应用场景**: - **Bootloader与应用程序切换**:Bootloader在跳转到应用程序前,需要将VTOR设置为应用程序的向量表地址,这样当发生中断,处理器才能跳转到应用程序的中断服务程序。 - **动态更新中断服务程序**:在运行,如果需要改变中断服务程序,可以将向量表复制到RAM中,然后修改RAM中的向量表条目,再将VTOR指向RAM中的向量表。这样,中断发生就会跳转到新的服务程序。 6. **注意事项**: - **对齐要求**:向量表的基地址必须满足对齐要求。对齐大小取决于处理器支持的向量数量(例如,Cortex-M3/M4要求至少128字节对齐,但通常需要256或512字节对齐,具体请参考芯片手册)。 - **中断禁用**:在更新向量表,应禁用全局中断,以避免在更新过程中发生中断而导致不可预测的行为。 - **内存属性**:向量表所在的存储区域必须可被处理器访问(例如,如果向量表在Flash中,则Flash必须使能;如果在RAM中,则RAM必须初始化并可用)。 ### 示例:Bootloader跳转前设置VTOR 在Bootloader跳转到应用程序,除了设置堆栈指针和程序计数器外,还必须设置VTOR: ```c void jump_to_app(uint32_t app_address) { // 禁用中断 __disable_irq(); // 设置应用程序的向量表地址 SCB->VTOR = app_address; // 设置主堆栈指针(MSP)为应用程序向量表的第一个条目 __set_MSP(*(volatile uint32_t*)app_address); // 获取应用程序的复位处理函数地址(向量表的第二个条目) uint32_t reset_handler = *(volatile uint32_t*)(app_address + 4); // 跳转到应用程序 asm volatile ("BX %0" : : "r" (reset_handler)); } ``` ### 相关问题 1. 在Cortex-M0处理器中,是否支持VTOR寄存器?如果不支持,如何实现向量表重定向? 2. 如何计算向量表所需的大小?一个包含48个向量的向量表需要多少字节? 3. 在运行修改向量表条目(例如,改变某个中断服务程序的地址)需要注意哪些事项? 4. 为什么在更新VTOR后需要插入数据同步屏障(DSB)指令? 5. 在多核处理器(如Cortex-M7)中,VTOR寄存器是如何工作的? [^1]: 引用[1]中关于向量表重定向的说明。 [^2]: 引用[2]中关于控制寄存器的说明(虽然本问题未直接使用,但提供了系统控制相关背景)。</think>### Cortex-M系列VTOR寄存器工作机制详解 #### 1. VTOR寄存器基本功能 VTOR(Vector Table Offset Register)是Cortex-M系列处理器的核心系统控制寄存器,用于实现**向量表重定向**功能。其核心机制包括: - **地址映射**:物理地址固定为 `0xE000ED08`(通过SCB->VTOR访问) - **位域结构**: ``` [31:7] TBLOFF - 向量表基地址(高位) [6:0] Reserved - 必须为0(强制对齐) ``` - **对齐要求**:向量表地址必须满足 $2^{n+7}$ 对齐(如128/256/512字节)[^1] #### 2. 向量表工作机制 ```mermaid graph LR A[异常发生] --> B[计算向量地址] B --> C{VTOR=0?} C -->|是| D[使用0x00000000基址] C -->|否| E[使用VTOR值基址] E --> F[地址 = VTOR + 异常号×4] F --> G[获取中断处理程序地址] ``` 1. **复位状态**:VTOR复位值=0,处理器从 `0x00000000` 读取初始SP和复位向量 2. **重定向过程**: ```c // 将向量表重定向到0x20000000(SRAM) #define VECTOR_TABLE_SIZE 48 // 典型向量数量 // 复制向量表到新位置 for(uint32_t i=0; i<VECTOR_TABLE_SIZE; i++) { *((uint32_t*)(0x20000000 + i*4)) = *((uint32_t*)(i*4)); } __DSB(); // 数据同步屏障 SCB->VTOR = 0x20000000; // 更新VTOR __ISB(); // 指令同步屏障 ``` #### 3. 关键应用场景 1. **Bootloader跳转**: ```c void jump_to_app(uint32_t app_addr) { __disable_irq(); SCB->VTOR = app_addr; // 设置应用向量表 __set_MSP(*(volatile uint32_t*)app_addr); // 设置主堆栈 ((void(*)(void))(*(volatile uint32_t*)(app_addr + 4)))(); // 跳转 } ``` 2. **动态更新中断**: ```c // 运行修改UART中断向量 void update_uart_isr(void (*new_handler)(void)) { uint32_t vtor = SCB->VTOR; *((volatile uint32_t*)(vtor + 16 * 4)) = (uint32_t)new_handler; // UART向量位置 __DSB(); } ``` #### 4. 技术要点 1. **对齐限制**: - Cortex-M0/M0+:最小128字节对齐 - Cortex-M3/M4/M7:最小256字节对齐(需参考具体芯片手册) - 验证方法:`(SCB->VTOR & 0x7F) == 0`(必须成立) 2. **内存屏障必要性**: - `__DSB()`:确保所有存储操作完成 - `__ISB()`:清空处理器流水线 - 缺少屏障可能导致**指令预取错误**[^1] 3. **与异常返回的关联**: - 异常发生处理器自动使用VTOR定位处理程序 - 异常返回通过EXC_RETURN值决定堆栈使用(MSP/PSP)[^2] #### 5. 调试注意事项 1. **常见故障排查**: - HardFault后检查SCB->VTOR值 - 确认向量表区域内存属性(X/NX权限) - 验证向量地址是否4字节对齐 2. **特殊架构差异**: | 处理器 | VTOR支持 | 最小对齐 | 备注 | |------------|----------|----------|-----------------------| | Cortex-M0 | 可选 | 128字节 | 部分低端芯片未实现 | | Cortex-M3 | 必需 | 256字节 | 全系列支持 | | Cortex-M4 | 必需 | 256字节 | 与M3相同 | | Cortex-M7 | 必需 | 512字节 | 支持Cache一致性操作 | ### 相关问题 1. 在Cortex-M0芯片中如何实现无VTOR的向量表重定向? 2. VTOR寄存器更新为何需要数据同步屏障(DSB)指令? 3. 多核处理器(如Cortex-M7)中VTOR如何工作? 4. 如何验证向量表重定向后中断处理程序的正确性? 5. 向量表重定向对RTOS任务切换有何影响? [^1]: 向量表重定向机制及内存屏障要求 [^2]: 异常返回与堆栈选择机制
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

嵌入式拳铁编曲MikeZhou

光电帝国,光联万物!

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

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

打赏作者

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

抵扣说明:

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

余额充值