STM32异常与中断过程详解

1. 异常与中断概念引入

异常主要是指来自CPU内部的意外事件,比如执行了未定义指令、算术溢出、除零运算等发生在CPU内部的意外事件,这些异常的发生,会引起CPU运行相应的异常处理程序;中断一般来自硬件(如片上外设、外部I/O输入等)发生的事件,当这些硬件产生中断信号时,CPU会暂停当前运行的程序,转而去处理相关硬件的中断服务程序。但无论是异常还是中断,都会引起程序执行偏离正常的流程,转而去执行异常/中断的处理函数。

下面对异常和中断的介绍,如果中断信号的产生原因来自CPU内部,则称之为异常;如果中断信号来自CPU外部,则称之为中断。有些场合如果没有明确指出是异常还是中断,就统称为中断。

1.1 为什么需要中断

在网上看到一个非常有意思的例子,这里引用下类似的例子来说明为什么需要中断。当你正在看一部很喜欢的电影时,这个时候你觉得有点口渴了,需要去烧一壶开水,假设水烧开需要10分钟。那么请问你如何知道水烧开了呢?也许你会有这两种选择:

  1. 每隔一小会你就跑去看一下这壶水有没有被烧开,然后回来接着看电影;
  2. 你可以等到水壶发出水被烧开的声音才去看一下,这期间你一直在看电影。

第一种情况的处理方式,实际上就是不断的查询水是否被烧开了,如果烧开了就关火,没烧开就接着继续看电影,这种处理方式可能会累死你,而且你也会觉得自己有点笨。写程序描述如下:

while (1)
{
	see a film(看电影)
    check water boiling(查看水是否烧开)
    if (水还没开)
        return(继续看电影)
    else
        关火    
}

第二种方式可以看作是烧开水的声音发出了一个信号给你,你接收到这个信号,然后就知道了我该去关火了,也就是说这个信号中断了你看电影的过程,而在这之前你可以一直享受看电影的过程。写程序描述如下:

while (1)
{
	see a film(看电影)   
}

中断服务程序()
{
    关火
}

这个例子类比于CPU的话,CPU也是可以选择这两种方式去处理意外事件的,查询方式或者中断方式。对于查询方式很明显会使得CPU的资源无法得到充分的利用,因为CPU的速度是远远大于外设的速度的,CPU在查询外设的状态时就需要等待外设的响应,使得CPU做了很多无用功。而对于中断方式,当外部事件还没达到就绪状态时(类比就是水还没烧开这件事),CPU可以专心的做其他任务,一旦CPU接收到中断信号时,转而去处理中断请求,CPU处理完毕之后再接着执行原来的任务。

从这个类比的例子可以看出,中断机制使得CPU具有了异步处理能力。有了中断机制之后,CPU可以一直专心的执行它的主任务,不用一直去查询设备的状态。设备本身如果达到了就绪状态,需要CPU去处理的时候,此时设备发出一个中断信号给CPU,通知CPU说:“我要你来处理一下了,赶紧来吧”!CPU收到通知之后,先把主任务暂停一会,然后跳转到相应外设的中断服务函数处理该外设的中断请求,处理完之后CPU再继续回去执行主任务。

1.2 ARM体系如何使用中断

在ARM体系中,使用中断的过程一般有如下步骤:

  1. 中断初始化

    1.1 设置中断源,让某个外设可以产生中断;

    1.2 设置中断控制器,使能/屏蔽某个外设的中断通道,设置中断优先级等;

    1.3 使能CPU中断总开关

  2. CPU在运行正常的程序

  3. 产生中断,比如用户按下了按键 —> 中断控制器 —> CPU

  4. CPU每执行完一条指令都会检查是否有异常/中断产生

  5. 发现有异常/中断产生,开始处理:

    5.1 保存现场

    5.2 分辨异常/中断,调用对应的异常/中断处理函数

    5.3 恢复现场

2. STM32异常与中断处理流程

2.1 概述

上面讲到当CPU产生异常/中断时,CPU会暂停当前程序的运行,转而去处理异常/中断的处理函数。但是这里有一些疑问:CPU如何调用异常/中断的处理函数?调用完之后如何返回?在运行中断处理函数之后,如何保证原来被打断的运行环境不被中断处理函数所改变?这几个问题是异常与中断处理流程中非常关键的问题,这些问题会在下面进行讨论,并给出解答。

当异常/中断发生时,ARM架构对于异常/中断的处理流程基本都是:

  1. 保存现场
  2. 分辨异常/中断,调用对应的异常/中断处理函数
  3. 恢复现场

但是细分到不同的架构(比如M3和A7架构)时,具体的处理流程还是有一些差异的,而对于STM32来说,中断的整个处理流程硬件帮我们做了大部分的工作,比如保存现场、分别异常/中断,跳转执行、恢复现场等工作,都是硬件帮我们实现了的。那么我们要做的事情是什么?我们只需要编写中断服务函数中具体想要实现的逻辑代码即可,发生异常/中断后,CPU跳转执行的就是我们实现的这部分代码。

2.2 异常向量表

对于STM32,当某一个外设的中断发生时,CPU如何去调用相应外设的中断服务函数?这就需要引入异常向量表的概念了。STM32的中断向量表可以看成是一个指针数组,这个数组里面存放的就是一个个的中断服务函数的入口地址(向量表首地址规定是栈顶指针)。当CPU检查到某个中断产生时,硬件会根据我们提供的中断号自动跳转到向量表中与这个中断号对应的这个中断服务函数的入口地址,从而执行相应的中断服务函数。

比如发生了Reset异常,那么CPU会从异常向量表的第1项找到Reset_Handler函数的地址,然后跳转到该函数执行;如果发生其他中断,CPU同样可以从表格里面找到对应的中断服务函数的地址,然后跳转到该函数执行。

STM32F103的中断向量表如下所示:

__Vectors       DCD     __initial_sp               ; Top of Stack
                DCD     Reset_Handler              ; Reset Handler
                DCD     NMI_Handler                ; NMI Handler
                DCD     HardFault_Handler          ; Hard Fault Handler
                DCD     MemManage_Handler          ; MPU Fault Handler
                DCD     BusFault_Handler           ; Bus Fault Handler
                DCD     UsageFault_Handler         ; Usage Fault Handler
                DCD     0                          ; Reserved
                DCD     0                          ; Reserved
                DCD     0                          ; Reserved
                DCD     0                          ; Reserved
                DCD     SVC_Handler                ; SVCall Handler
                DCD     DebugMon_Handler           ; Debug Monitor Handler
                DCD     0                          ; Reserved
                DCD     PendSV_Handler             ; PendSV Handler
                DCD     SysTick_Handler            ; SysTick Handler

                ; External Interrupts
                DCD     WWDG_IRQHandler            ; Window Watchdog
                DCD     PVD_IRQHandler             ; PVD through EXTI Line detect
                DCD     TAMPER_IRQHandler          ; Tamper
                DCD     RTC_IRQHandler             ; RTC
                DCD     FLASH_IRQHandler           ; Flash
                DCD     RCC_IRQHandler             ; RCC
                DCD     EXTI0_IRQHandler           ; EXTI Line 0
                DCD     EXTI1_IRQHandler           ; EXTI Line 1
                DCD     EXTI2_IRQHandler           ; EXTI Line 2
                DCD     EXTI3_IRQHandler           ; EXTI Line 3
                DCD     EXTI4_IRQHandler           ; EXTI Line 4
                DCD     DMA1_Channel1_IRQHandler   ; DMA1 Channel 1
                DCD     DMA1_Channel2_IRQHandler   ; DMA1 Channel 2
                DCD     DMA1_Channel3_IRQHandler   ; DMA1 Channel 3
                DCD     DMA1_Channel4_IRQHandler   ; DMA1 Channel 4
                DCD     DMA1_Channel5_IRQHandler   ; DMA1 Channel 5
                DCD     DMA1_Channel6_IRQHandler   ; DMA1 Channel 6
                DCD     DMA1_Channel7_IRQHandler   ; DMA1 Channel 7
                DCD     ADC1_2_IRQHandler          ; ADC1 & ADC2
                DCD     USB_HP_CAN1_TX_IRQHandler  ; USB High Priority or CAN1 TX
                DCD     USB_LP_CAN1_RX0_IRQHandler ; USB Low  Priority or CAN1 RX0
                DCD     CAN1_RX1_IRQHandler        ; CAN1 RX1
                DCD     CAN1_SCE_IRQHandler        ; CAN1 SCE
                DCD     EXTI9_5_IRQHandler         ; EXTI Line 9..5
                DCD     TIM1_BRK_IRQHandler        ; TIM1 Break
                DCD     TIM1_UP_IRQHandler         ; TIM1 Update
                DCD     TIM1_TRG_COM_IRQHandler    ; TIM1 Trigger and Commutation
                DCD     TIM1_CC_IRQHandler         ; TIM1 Capture Compare
                DCD     TIM2_IRQHandler            ; TIM2
                DCD     TIM3_IRQHandler            ; TIM3
                DCD     TIM4_IRQHandler            ; TIM4
                DCD     I2C1_EV_IRQHandler         ; I2C1 Event
                DCD     I2C1_ER_IRQHandler         ; I2C1 Error
                DCD     I2C2_EV_IRQHandler         ; I2C2 Event
                DCD     I2C2_ER_IRQHandler         ; I2C2 Error
                DCD     SPI1_IRQHandler            ; SPI1
                DCD     SPI2_IRQHandler            ; SPI2
                DCD     USART1_IRQHandler          ; USART1
                DCD     USART2_IRQHandler          ; USART2
                DCD     USART3_IRQHandler          ; USART3
                DCD     EXTI15_10_IRQHandler       ; EXTI Line 15..10
                DCD     RTCAlarm_IRQHandler        ; RTC Alarm through EXTI Line
                DCD     USBWakeUp_IRQHandler       ; USB Wakeup from suspend
                DCD     TIM8_BRK_IRQHandler        ; TIM8 Break
                DCD     TIM8_UP_IRQHandler         ; TIM8 Update
                DCD     TIM8_TRG_COM_IRQHandler    ; TIM8 Trigger and Commutation
                DCD     TIM8_CC_IRQHandler         ; TIM8 Capture Compare
                DCD     ADC3_IRQHandler            ; ADC3
                DCD     FSMC_IRQHandler            ; FSMC
                DCD     SDIO_IRQHandler            ; SDIO
                DCD     TIM5_IRQHandler            ; TIM5
                DCD     SPI3_IRQHandler            ; SPI3
                DCD     UART4_IRQHandler           ; UART4
                DCD     UART5_IRQHandler           ; UART5
                DCD     TIM6_IRQHandler            ; TIM6
                DCD     TIM7_IRQHandler            ; TIM7
                DCD     DMA2_Channel1_IRQHandler   ; DMA2 Channel1
                DCD     DMA2_Channel2_IRQHandler   ; DMA2 Channel2
                DCD     DMA2_Channel3_IRQHandler   ; DMA2 Channel3
                DCD     DMA2_Channel4_5_IRQHandler ; DMA2 Channel4 & Channel5
__Vectors_End

一般来说向量表都会被链接到一个程序存储的最前面的位置的,对于STM32F103来说,程序运行和加载的起始地址都是0x08000000,所以STM32的异常向量表也会会被加载到0x08000000地址处的。

对于ARM架构,规定向量表的基地址默认是0x00000000或0xFFFF0000位置的(对于STM32向量表的默认基地址就是0地址),而STM32向量表的基地址是被链接在了0x08000000的地址处,这样当中断发生时,不就跳转到了错误的函数入口地址了吗?为了解决这个问题,ARM允许可以修改异常向量表的基地址,这样就可以把异常向量表的基地址重新修改在内部Flash的起始地址0x08000000位置了。完成这一工作的是官方固件库的SystemInit函数,相关代码如下:

#define FLASH_BASE       ((uint32_t)0x08000000) /*!< FLASH base address in the alias region */
#define VECT_TAB_OFFSET  0x0 /*!< Vector Table base offset field. This value must be a multiple of 0x200. */
#define SCB              ((SCB_Type *)           SCB_BASE)         /* !< SCB configuration struct         */

void SystemInit (void)
{
    /****************此处省略部分代码****************/
#ifdef VECT_TAB_SRAM
  SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET;  /* Vector Table Relocation in Internal SRAM.  */
#else
  SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal FLASH. */
#endif 
}

其中 SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; 这一行代码就是把异常向量表基地址更改到了0x08000000的地址中去了。

补充:我曾自己写过简单的启动文件测试,并没有去调用SystemInit函数,自己也没有在其他地方对向量表的基地址更改到0x08000000的地址中去,但是当CPU检查到有中断发生时,还是可以跳转到正确的中断服务函数的入口地址处。为什么会这样?我查阅了下相关的资料,了解到当STM32从Flash启动时,实际上会把Flash的那段存储空间0x08000000 ~ 0x0807FFFF映射到0x00000000 ~ 0x0007FFFF这一段地址的存储空间,也就是说当CPU从Flash启动时,CPU从0地址处开始执行指令和从0x08000000地址处开始执行指令是一样的。所以,这个时候我没有去更改异常向量表的基地址,CPU仍然可以跳转到正确的中断服务程序。

2.3 保存现场

2.3.1 概念引入

我们在Keil里面写一段非常简单的代码,实现 a = a + b; 这个式子。当然我们并不是要去关心这个式子本身,而是利用Keil的仿真功能,去查看一下CPU执行这个式子后,其内部寄存器的变化。仿真截图如下:
在这里插入图片描述
从上面这个仿真的汇编代码可以看出,CPU要完成 a = a + b; 的运算,实际上是先去读取a, b的内存所保存的数据到内部寄存器r0, r1中,然后再执行r0 = r0 + r1的运算,最后把r0的值写回到内存a中,这样才完成了一次a = a + b; 的运算。另外,可以观察到CPU在运行程序的过程中,它内部的r0 - r15,还有程序状态寄存器xPSR的值,都是在不断的变化中的。

那么问题来了,假设CPU在运行 a = a + b; 这个式子的中途,程序运行过程被中断信号打断了。要想这个式子可以得到正确的结果,那么就要确保中断前CPU的这几个寄存器的值以及中断返回后这些寄存器的值都要保持不变,还有保存在内存空间a, b的值也不会被改变。这样运行这个式子得到的最终结果才是正确的。

对于保持a, b内存的值不变,这个好办,中断程序中不要去改写a, b的值即可。但是我们如何保持中断前后CPU内部寄存器的值不被改变呢?因为中断程序的运行肯定也是需要用到CPU内部的寄存器的,所以必须得有一种机制去保持中断前后CPU内部寄存器的值不被改变。也就是说,所谓的现场,实际上就是CPU运行到某一时刻时,CPU内部寄存器的值

那么如何保存现场和恢复现场呢?这就需要用到栈。CPU在跳转到异常/中断程序前,就需要把这些不希望被中断程序所改变的寄存器的值保存到栈中,这就是保存现场。在异常/中断处理完之后,从栈中恢复这些寄存器的值,这就是恢复现场

2.3.2 保存哪些寄存器的值

上面说了,所谓保存现场就是保存CPU内部一些寄存器的值到栈中。CPU内部常用的寄存器有 r0 ~ r15,再加上程序状态寄存器,一共16个(当然其他模式下可能还有对应的其他寄存器)。下图就是STM32F103内部寄存器示例(M3内核不具有浮点运算单元,这里不讨论浮点运算单元的寄存器了):
在这里插入图片描述
当发生异常/中断时,CPU跳转之前,就需要把这些寄存器的值保存到栈中,那么我们需要把CPU的这16个寄存器都保存到栈中吗?很显然是不需要的。

ARM公司推出了一个ATPCS标准,即ARM-THUMB procedure call standard(ARM-Thumb过程调用标准)。在这份标准里面,都规定了这16个寄存器的使用规则,如下表所示:

寄存器别名使用规则
r15pc程序计数器
r14lr连接寄存器
r13sp数据栈指针
r12ip子程序内部调用的scratch寄存器
r11v8ARM状态局部变量寄存器8
r10v7、s1ARM状态局部变量寄存器7、在支持数据栈检查的ATPCS中为数据栈限制指针
r9v6、sbARM状态局部变量寄存器6、在支持RWPI的ATPCS中为静态基址寄存器
r8v5ARM状态局部变量寄存器5
r7v4、wrARM状态局部变量寄存器4、Thumb状态工作寄存器
r6v3ARM状态局部变量寄存器3
r5v2ARM状态局部变量寄存器2
r4v1ARM状态局部变量寄存器1
r3a4参数/结果/scratch 寄存器4
r2a3参数/结果/scratch 寄存器3
r1a2参数/结果/scratch 寄存器2
r0a1参数/结果/scratch 寄存器1

其中,r0 ~ r3是函数调用时用来传递参数或者保存函数返回值的,r4 ~ r11是用来保存局部变量的,r12 ~ r15是特殊功能寄存器。这些寄存器一般会被拆分成2部分:调用者保存的寄存器(r0-r3, r12, lr, psr)被调用者保存的寄存器(r4-r11)

比如函数A调用函数B,那么函数A就是调用者,函数B就是被调用者。那么根据这份ATPCS规则,函数A在调用函数B之前,函数A就应该要保存r0-r3, r12, lr, psr这几个调用者要保存的寄存器;而对于r4-r11这几个寄存器,调用函数B的前后,函数B本身会保证调用前后这些寄存器的值保持不变。

那么,如果函数B是中断服务函数,因为函数B本身会保证r4-r11的值不会被改变,所以保存现场也就是保存r0-r3, r12, lr, psr这几个寄存器的值即可。

2.3.3 STM32如何保存现场

我们已经知道了什么是现场,保存现场要保存哪些寄存器的值了。那么对于STM32来说,是如何保存这些寄存器的值到栈中的呢?

非常幸运的是,STM32保存现场的工作是CPU的硬件自动帮我们完成的(我们不要想当然的认为所有架构的硬件都会帮我们完成这部分工作,对于Cortex-A7架构来说就不是硬件帮我们完成的),我们不需要自己写代码去把中断前需要保存的寄存器的值存放到栈中。具体过程如下图所示:
在这里插入图片描述

2.4 恢复现场

恢复现场就是指CPU执行完异常/中断服务函数后,返回到LR寄存器所指示的地址处(就是中断前的下一条指令的地址),并且把保存在栈中寄存器的值恢复到寄存器中去。

对于如何返回到中断前下一条指令的地址,其实很容易就可以实现,只要我们把下一条指令的地址存放到LR寄存器即可,CPU返回的话就是把LR寄存器的值赋值给PC程序计数器,这样就可以返回到了中断前下一条指令中继续运行程序了。但是如果这样做的话,硬件帮我们保存在栈中的寄存器的值,返回去是怎么恢复那些寄存器的值?

对于这个问题,STM32帮我们实现了一套机制。CPU进入异常/中断服务程序时,LR寄存器保存的并不是中断前下一条指令的地址,而是会保存一个特殊的数值,被称为EXC_RETURN。对于EXC_RETURN位域的定义和具体的数值含义,可以查看《ARM Crotex-M3与Cortex-M4权威指南》一书中第八章的描述。这里截图如下作为参考:
在这里插入图片描述
所以,当异常/中断返回时,LR寄存器赋值给PC的值是一个被称为EXC_RETURN的值,而一旦CPU识别到PC的值等于EXC_RETURN的话,那么就会触发异常/中断返回机制,这个机制会帮我们把保存在栈中r0-r3, r12, lr, psr的寄存器的值恢复回去。

  • 65
    点赞
  • 231
    收藏
    觉得还不错? 一键收藏
  • 15
    评论
STM32中,中断是一种机制,用于实现异步事件处理,例如外部设备的输入/输出操作。当外设产生一个中断请求时,处理器会暂停当前执行的任务,并跳转到中断服务程序(中断处理函数)中执行相应的处理中断函数是特殊的函数,用于处理中断事件。它必须遵循一定的规则,例如使用特定的命令定义函数,以确保正确地处理中断。 在STM32中,中断分为两种类型:外部中断和内部中断。外部中断是由外部设备产生的中断请求,例如GPIO引脚电平变化、串口接收数据等。内部中断是由处理器内部产生的中断请求,例如时钟中断、DMA传输完成中断等。 为了处理中断事件,需要在程序中注册中断服务程序(中断处理函数),并将其与相应的中断源关联。在STM32中,可以使用“中断向量表”来管理中断服务程序。中断向量表是一个特殊的表格,它包含了所有中断服务程序的入口地址,当中断事件发生时,处理器会根据中断向量表中相应中断向量的地址跳转到相应的中断服务程序中执行。 在编写中断服务程序时,需要注意以下几点: 1. 中断服务程序必须使用特殊的命令定义。在STM32中,使用“void __attribute__((interrupt)) <interrupt_name>(void)”语句定义中断服务程序。 2. 中断服务程序需要尽可能快地执行完毕,以确保及时处理下一个中断事件。因此,在中断服务程序中应尽量避免使用较耗时的操作,例如延时函数、大量的数据处理等。 3. 在中断服务程序中,应尽量减少对全局变量和静态变量的访问,以避免数据冲突的发生。如果必须使用全局变量和静态变量,应尽可能使用volatile关键字修饰,以确保变量的可见性和正确性。 4. 中断服务程序应尽可能短小精悍,以避免中断嵌套和中断重入的发生。如果必须使用嵌套中断或者中断重入,必须采取相应的措施,例如关闭中断或者使用互斥量等。 总之,在STM32中,中断是一种非常重要的机制,可以实现异步事件处理和提高系统的响应速度。因此,熟练掌握中断的原理和使用方法,对于STM32开发人员来说是非常必要的。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值