STM32——理解中断与中断配置

前言:本文将从”这是什么?“ ”为什么需要它?“ “如何配置操作它”三个角度展开讨论分析

目录

中断简介

 抢占优先级和子优先级

中断分组

配置要点

EXTI

EXTI框图讲解

信号产生过程

编程要点


中断简介

中断,即机器运行过程中出现某些意外情况,需要机器停止正在运行的程序并转入处理新情况的程序,处理完毕之后又返回原来被暂停的程序继续运行。

理解中断

想象一个这样的场景:

你在认真的敲代码,你妈喊你出房间去客厅吃饭,并且以不出来就拔网线为威胁。这时候你能怎么办?只能乖乖保存好现在的进度,然后出去恰饭,恰完之后再回来打开之前的文件继续敲咯。

我们分析一下上面的场景:

  • 正在敲代码——正在执行的事件
  • 你妈喊你吃饭——中断源
  • 你听到你妈喊你吃饭——接收到中断请求
  • 威胁你——事件优先级别高
  • 保存已经写好的部分——保存现场(中断响应)
  • 去吃饭——执行中断事件
  • 吃完饭回房间——中断返回
  • 打开文件继续敲代码——恢复现场

对于机器来说也是一样,无论是内核出现问题(异常),还是因为存在其他比当前正在执行的程序的优先级更高的事件发生(外部中断),都会打断当前运行的程序,去处理更加重要的事情。

毕竟,就算机器的运算速度很快,处理事件也有先后之分不是?

接下来,让我们再次回到刚刚那个场景,考虑下面两种情况:

  1. 当正在吃饭的时候,你忽然又想去WC解决下个人问题。这时你会停止吃饭的这个动作,然后去WC解决完之后再回来吃饭。吃完饭才会继续。
  2. 当正在吃饭的时候,你收到快递小哥的短信,告诉你丰巢的验证码可以取件的时候,你肯定会先吃完饭,然后去取完快递之后(也可能先在那放着不取)再回房间敲代码。

对于情况1而言,为了避免某些比较尴尬的事情发生,先去WC再回来吃饭几乎是必然选择。因为它比吃饭紧急。而对于情况2而言,取快递相对来说没那么急迫的需求,所以一般而言我们都会先吃晚饭,再去拿快递,最后再回房间敲代码。

当然如果敲代码比较着急,那么拿快递这件事情自然就得等到我们敲完代码之后再做了。

从上面两个场景中,我们很容易得发现中断时可以嵌套的,且它有一定的优先级概念。

很显然,我们的日常生活中也会有许多中断事件。我们人脑是怎么处理它们的?认真思考不难发现,其实对于很多事件,我们早就给他们定义了优先级。而定义这些事件的优先级并严格执行的就是我们的大脑。

对于CPU来说也一样,当中断事件很少的时候,不需要分门别类的整理,直接比较判断优先级就可以了。

而对于中断事件较多的复杂系统,对于事件的分组和优先级设置也就成了刚需。

下面进入到正题,以STM32F10x的中断系统为例,展开学习。

STM32中的中断设置

我们知道,STM32用的是Cortex-M3内核

而CM3内核支持256个中断,256级可编程设置(优先级设置),其中:

  • 内核中断16个
  • 外设中断240个

且以上配置属于STM32中的顶配,具体的STM32F103系列只有:

  • 内核中断16个
  • 外设中断44个

小结一下以上的数据,可以发现,实际中断事件的多少取决于芯片的定位。高端的,即外设多的,芯片中断必然多,反之必然少。

NVIC

而要处理那么多的中断事件,CM3内核给出的解决方案是——用NVIC(Nested Vectored Interrupt Controller)嵌套向量中断控制器

NVIC首先定义了一段地址区域,(一般从0地址开始)用于存放中断事件服务函数的地址。定义的这段地址就称为中断向量表。

preview

 

 以上是《stm32f10x中文参考手册》中的截图

可以看到它一个地址对应着一个中断事件,其中灰色部分是内核相关的中断的位置。每一条指令执行完之后,系统都会检查一遍看看有没有中断事件发生。如果有的话,就会在中断向量表中找到对应的中断事件,执行中断事件所在地址指向的函数(即中断函数)。

也就是因为中断事件所在的地址存的是一个地址,它指向的是该中断对应发生的事件(函数)。所以其实中断向量表这个名字起的很符合它的实际含义——表里的是一个个有指向的值

至于定义这些中断事件的优先级和它们具体是干嘛的,NVIC把设置的权利交给了开发者,它只给了可以完成相关操作的寄存器组。

/** @addtogroup CMSIS_CM3_NVIC CMSIS CM3 NVIC
  memory mapped structure for Nested Vectored Interrupt Controller (NVIC)
  @{
 */
typedef struct
{
  __IO uint32_t ISER[8];                      /*!< Offset: 0x000  Interrupt Set Enable Register           */
       uint32_t RESERVED0[24];                                   
  __IO uint32_t ICER[8];                      /*!< Offset: 0x080  Interrupt Clear Enable Register         */
       uint32_t RSERVED1[24];                                    
  __IO uint32_t ISPR[8];                      /*!< Offset: 0x100  Interrupt Set Pending Register          */
       uint32_t RESERVED2[24];                                   
  __IO uint32_t ICPR[8];                      /*!< Offset: 0x180  Interrupt Clear Pending Register        */
       uint32_t RESERVED3[24];                                   
  __IO uint32_t IABR[8];                      /*!< Offset: 0x200  Interrupt Active bit Register           */
       uint32_t RESERVED4[56];                                   
  __IO uint8_t  IP[240];                      /*!< Offset: 0x300  Interrupt Priority Register (8Bit wide) */
       uint32_t RESERVED5[644];                                  
  __O  uint32_t STIR;                         /*!< Offset: 0xE00  Software Trigger Interrupt Register     */
}  NVIC_Type;                                               
/*@}*/ /* end of group CMSIS_CM3_NVIC */

在固件库头文件core_cm3.h中可以找到以上NVIC的寄存器组映射

其中如果我们使用寄存器编程的话,最关心的三个寄存器是:

  • ISER:Interrupt Set Enable Register ,中断使能设置寄存器
  • ICER:Interrupt Clear Enable Register,中断使能清除寄存器
  • IP:Interrupt Priority Register,中断优先级寄存器

在这里满,中断优先级寄存器是需要我们着重研究的。有意思的是,固件库命名的时候对于该寄存器的简写直接只用IP,而不是IPR,害我一度怀疑这两个是不是同一个东西。直到我查了手册才确定是一个东西。

IPR中断优先级寄存器

原CM3设计中,NVIC_IPRx用于配置外部中断的优先级。IPR宽度为8bit,理论上可以配置的优先级为0-255,数值越小优先级越高。但和大多数的CM3芯片设计都会精简一样,STM32也做了相关的精简设计,只用了高4bit.也就是说,实际上STM32允许配置的优先级只有16个

 抢占优先级和子优先级

CM3把优先级划分成了抢占优先级和子优先级(又叫响应优先级)。即,对于同一个中断事件,它有抢占优先级和子优先级两个优先级。

为什么要这么设置?

因为如果只是简单的一种优先级的话,太过粗线条了。像CM3内核设置最多有256级,即使是stm32也有16级,级数太多,没有主次不方便使用和管理。

类比我们平时生活中的状态,就像社会上有256个人,每个的级别都一样,估计你会眼花缭乱,解决这个问题的办法就是把这256级的中断分类归层,层级内再分子级的分层管理形式,达到压缩层级的效果。如先分统治阶级,奴隶主、奴隶等,同一阶级在区分小级别。

对应于中断分组就是,把256级优先级根据本工程的需求,先规定了中断层数(即抢占优先级)和每层有多少小级别个数。

抢占优先级和子优先级比较

只有当抢占优先级相同的时候,子优先级才会发挥作用。若子优先级也相同,就去比较两个外设的硬件终端编号,即该中断在中断向量表中的位置,同样是编号越小,优先级越高的原则。

值得注意的是抢占式优先级和子优先级还有些许差别:

  • 抢占优先级:无关中断产生的先后,只比较优先级的高低。优先级高的中断信号即使比中断优先级低的中断信号后到,也可以直接中断优先级低的事件直接抢占系统资源。
  • 子优先级:当两个中断信号有相同的抢占优先级,分两种情况考虑:
    • 两个信号同时到达,子优先级高的中断先响应
    • 子优先级低的中断事件正在发生,子优先级高的中断信号刚到的时,需要等到当前中断完成后,在执行刚到的中断信号。

中断分组

从前面的讨论中我们可以知道,为了中断更加可控,CM3内核做了两种优先级的分层机制。

但是对于不同的需求,我们对于阶层和小阶级的数量要求也是不同的,以身份证为例,在发放身份证之前,我们要总体考虑全国要分多少个区域,然后确定地区需要设置几位数才够。熟悉计算机网络的朋友可以发现,这里其实和子网划分是一个道理。

到这里,我们也可以看到,在一个工程中,我们一般只会设置一次,且每次只设置一个分组。还是拿身份证举例,你不可能前一秒还说身份证前3位表示A省,下一秒就说前4位表示B省吧?编码格式统一是一切的基础。

所以,我们平时在设置分组时,若不是非常特殊的需求

一个工程只设置一次分组!!!

一个工程只设置一次分组!!!

一个工程只设置一次分组!!!

重要的事情说三遍!!!

这个分组管理的设置由内核外设SCB_AIRCR(应用程序中断及复位控制寄存器)的prigroup[10:8]控制。

因为它通过3bit控制,所以理论上可以分成8组(0-7组),其中每组可设置的抢占优先级和子优先级的位数时不同的。

 

如上图,可以看到,随着组号的增大,抢占优先级可设置的位越少,子优先级越多。

上面这种分组时针对IPR里的8bits都用上的情况设计的,而我们知道STM32的NVIC是CM3的NVIC的子集,只用了IPR的高4bits,所以分组自然而然就减少了。 

 有了以上的原理性的认知之后,我们可以进入到编程配置环节。

配置要点

  • 使能某个中断
  • 中断分组
  • 初始化NVIC_InitTypeDef结构体
    • 设置中断源
    • 设置抢着优先级和子优先级
    • 配置中断使能/失能
  • 编写中断服务函数(就是中断发生之后,告诉机器去干嘛)

上面的流程中提到的NVIC_InitTypeDef结构体,就是如下图中的:

** 
  * @brief  NVIC Init Structure definition  
  */

typedef struct
{
  uint8_t NVIC_IRQChannel;                    /*!< Specifies the IRQ channel to be enabled or disabled.
                                                   This parameter can be a value of @ref IRQn_Type 
                                                   (For the complete STM32 Devices IRQ Channels list, please
                                                    refer to stm32f10x.h file) */

  uint8_t NVIC_IRQChannelPreemptionPriority;  /*!< Specifies the pre-emption priority for the IRQ channel
                                                   specified in NVIC_IRQChannel. This parameter can be a value
                                                   between 0 and 15 as described in the table @ref NVIC_Priority_Table */

  uint8_t NVIC_IRQChannelSubPriority;         /*!< Specifies the subpriority level for the IRQ channel specified
                                                   in NVIC_IRQChannel. This parameter can be a value
                                                   between 0 and 15 as described in the table @ref NVIC_Priority_Table */

  FunctionalState NVIC_IRQChannelCmd;         /*!< Specifies whether the IRQ channel defined in NVIC_IRQChannel
                                                   will be enabled or disabled. 
                                                   This parameter can be set either to ENABLE or DISABLE */   
} NVIC_InitTypeDef;
 

它跟NVIC_Type结构体的区别在于,NVIC_Type只是寄存器组的映射,而NVIC_InitTypeDef是固件库抽象提炼出来帮助我们简化配置流程用的。

下面我们以一个实例体会上述的过程。

EXTI

EXTI(External interrupt/event controller)——外部中断/事件控制器,用于管理控制器的20个中断/事件线(互联型才有20根,其他类型只有19根)。

NVIC和EXTI的关系

我们首先来理一理这东西根NVIC的关系:

1、NVIC是CM3内核的东西,它用于整个整个芯片的中断控制。而EXTI是ST公司设计用于控制外设的,它不是内核里面的东西。

2、EXTI通过魔改引出的NVIC中的20根中断信号线,对外设事件的控制能力进一步增强了。

在这里,我们先理清三个概念:

  • 事件
  • 中断
  • 中断事件

比如一老师在教室里面给学生上课,课堂上的学生可能做出各种行为的动作,比如做笔记,打哈欠,翻书包,讲小话等,我们把这些行为统称为事件,其中有些行为老师往往只是视而不见,继续他的上课;而有些行为可能导致老师的上课中止,比方讲小话,并对学生的行为予以警告、批评或纠正等,然后继续上课。我们把老师因为学生的某些行为而中止授课,并产生后续动作,之后接着上课的这个过程理解为中断或中断响应。我们把可能导致老师上课中断的学生行为理解为中断事件。

EXTI框图讲解

下面,我们结合EXTI框图理解上述的概念。

首先,我们对上述的整个框图有个大概的认识:

  •  绿色+红色部分:跟外设连接在一起。绿色部分用于和蓝色部分信息交互,红色部分用于传输事件信号。
  • 蓝色部分:寄存器组,用于控制事件信号的采集和走向。
  • 黄色部分:两个输出端,用于事件信号的输出,中断信号去NVIC,非中断信号用于其他外设(如DMA)

信号产生过程

下面我们分析下两者的产生过程:

如上图所示,首先会经过边沿检测电路,这个检测电路会通过查看上边沿触发和下边沿触发寄存器的值,去判断信号到底在哪个边沿采集。(所谓上边沿和下边沿,即电平从低->高或者高->低变化时的那条竖直边)

经过边沿检测电路之后,就来到上图中的3位置,这里是个或门连接着输入信号和软件中断事件寄存器,也就是说,除了信号触发中断之外,我们还可以通过控制相关的寄存器从而产生中断。

我们先来看看中断的产生过程:

首先中断请求信号会先进入请求挂起寄存器。这个寄存器的存在意义在于,如果中断发生时,正在处理同级或高优先级中断,则中断不能立即得到响应,此时中断被悬起。悬挂意味着等待而不是舍去,当优先级高的或者同等级的先发生的中断执行完之后,被挂起的中断才会执行。

落实到STM32这里,可以看到标号4的位置,是一个与门,所以中断请求什么时候推送到NVIC控制器执行,就是中断屏蔽寄存器应该控制的事了。

同理,对于事件来收,事件是否生成,就是事件屏蔽器的事了。不同点在于,事件的产生没有优先级的概念,所以无需挂起寄存器这么个东西。

代码示例:这里我实现的事按键中断控制LED灯的亮灭

编程要点

  • 初始化要用来产生中断的GPIO;
  • 初始化EXTI
  • 配置NVIC
  • 编写中断服务函数
#include "bsp_exit.h"
void LED_GPIO_Config(void)
{
	/*定义一个GPIO_InitTypeDef类型的结构体*/
	GPIO_InitTypeDef GPIO_InitStruct;
	/*打开GPIO对应的时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
	/*设置为推挽输出*/
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
	/*选择要控制的GPIO引脚*/
	GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_5 | GPIO_Pin_1;
	/*设置引脚输出速率*/
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStruct);
	/*关闭所有的LED灯*/
	GPIOB->ODR |= 0x23;
}	


/*NVIC的配置*/
static void NVIC_Config(void)
{ 
	/*定义一个NVIC_InitTypeDef类型的结构体*/
	NVIC_InitTypeDef NVIC_InitStruct;
	
	/*配置NVIC为优先组1*/
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
	/*配置中断源:按键1 */
	NVIC_InitStruct.NVIC_IRQChannel = KEY1_EXTI_INT_IRQ;
	/*配置抢占优先级 */
	NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
	/*配置子优先级 */
	NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1;
	/*使能中断通道 */
	NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
	/*调用库函数,初始化NVIC*/
	NVIC_Init(&NVIC_InitStruct);
	/*配置中断源:按键2,其他使用上面一样的配置*/
	NVIC_InitStruct.NVIC_IRQChannel = KEY2_EXTI_INT_IRQ;
	NVIC_Init(&NVIC_InitStruct);

}
/*
  *@brief 配置 IO为EXTI中断口,并设置中断优先级
  *param  无
  *retval 无
*/

void KEY_INT_Config(void)
{
	/*定义GPIO_InitTypeDef和EXTI_InitTypeDef类型结构体*/
	GPIO_InitTypeDef KEY_InitStruct;
	EXTI_InitTypeDef EXIT_InitStruct;
	
	//开启外设按键和外部中断EXTI的时钟,这里可以简化的,源代码非常好
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
	RCC_APB2PeriphClockCmd(KEY1_INT_CLK, ENABLE);
	RCC_APB2PeriphClockCmd(KEY1_EXIT_INT_CLK,ENABLE);
	RCC_APB2PeriphClockCmd(KEY2_INT_CLK,ENABLE);
	/*配置NVIC中断*/
	NVIC_Config();
	/*————————KEY1-----------------配置*/
	/*选择按键用到的GPIO引脚*/
	KEY_InitStruct.GPIO_Pin	= KEY1_INT_PIN;
	/*配置为浮空输入,浮空输入的意思为由外部电路决定初始电平信号*/
	KEY_InitStruct.GPIO_Mode =  GPIO_Mode_IN_FLOATING;
	GPIO_Init(KEY1_INT_PORT, &KEY_InitStruct);
	
	/*选择EXTI的信号源*/
	GPIO_EXTILineConfig(KEY1_EXIT_INT_PORTSource, KEY1_EXIT_INT_PINSource);
	EXIT_InitStruct.EXTI_Line = EXTI_Line0;
	
	//EXTI设置为中断模式
	EXIT_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt;
	//上升沿中断
	EXIT_InitStruct.EXTI_Trigger = EXTI_Trigger_Rising;
	//使能中断通道
	EXIT_InitStruct.EXTI_LineCmd = ENABLE;
	//外部中断EXTI的初始化
	EXTI_Init(&EXIT_InitStruct);
	
	
	/*---------------------KEY2配置--------------*/
	/*选择按键用到的GPIO引脚*/
	KEY_InitStruct.GPIO_Pin = KEY2_INT_PIN;
	GPIO_Init(KEY2_INT_PORT, &KEY_InitStruct);
	
	/*选择EXTI的信号源*/
	GPIO_EXTILineConfig(KEY2_EXTI_INT_PORTSource, KEY2_EXTI_INT_PINSource);
	EXIT_InitStruct.EXTI_Line = KEY2_EXTI_INT_LINE;
	
	/*下降沿触发*/
	EXIT_InitStruct.EXTI_Trigger = EXTI_Trigger_Falling;
	/*EXTI为中断模式*/
	EXIT_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt;
	/*时钟中断通道*/
	EXIT_InitStruct.EXTI_LineCmd = ENABLE;
	EXTI_Init(&EXIT_InitStruct);

中断服务函数:

void KEY1_EXTI_Handler(void)
{
  if(EXTI_GetITStatus(KEY1_EXIT_INT_LINE) != RESET )
	{
			LED_G_TOGGLE;	
		EXTI_ClearITPendingBit(KEY1_EXIT_INT_LINE);  //清除中断标志位
	}
	
	
}

void KEY2_EXTI_Handler(void)	
{
if(EXTI_GetITStatus(KEY2_EXTI_INT_LINE) != RESET )
	{
		LED_R_TOGGLE;;
		EXTI_ClearITPendingBit(KEY2_EXTI_INT_LINE);
	}
	
}

  • 15
    点赞
  • 71
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值