1、首先回答下什么是中断?
中断就是让芯片停下当前正执行的程序,去执行另一个程序,举一个形象一点的例子,就是你正在家里面打游戏,突然有人敲门,这时,你不得不暂停游戏,去开门,这个过程就叫做中断,敲门可以理解为中断触发(中断源)。
Cortex-M3在内核上搭载了一个异常响应系统,支持为数众多的系统异常和外部中断。其中,编号为1-15的对应系统异常(注意:没有编号为0的异常,即使为0也不执行异常),大于等于16的则全是外部中断。除了个别异常的优先级被定死外,其它异常的优先级都是可编程的。
每个外设都可以产生中断,可以将中断与异常等价,不要深究它们到底有什么区别
16个系统异常清单如下:
60个外部中断:
有关具体的系统异常和外部中断可在标准库文件stm32f10x.h 这个头文件查询到,在IRQn_Type (中断/异常类型)这个结构体里面包含了F103 系列全部的异常声明。
下图为STM32的外部中断系统有哪些部分组成?
1. 先来讲解NVCI,在系统内部中如何处理中断?
NVIC(Nested Vectored Interrupt Controller):嵌套向量中断控制器,不可屏蔽中断(NMI)和外部中断都由它处理,但是SYSTICK(系统滴答定时器)却不是由NVIC来控制。
NVIC的作用就是:控制中断是否产生,比如你打游戏正上头,突然有人敲门,但是你却不想去开门,也就没有产生中断,这里人就相当与一个中断控制器,决定着中断是否产生。
理解了中断控制器,那就开始像配置GPIO那样来配置NVIC吧。
首先要了解下NVIC的构成,这样才能对每一项参数进行设定。
下图是在ST官方提供的固件库文件(misc.h)中找到的:
下面通过一幅图来理解NVIC中断控制器每一个成员所扮演的角色可能更加形象:
下面为EXTI功能框图,就不做过多说明,其实说的就是EXTI有两大功能,传递中断和事件,软件本质上是通过控制或门或者与门(硬件)来实现对中断和事件的控制。
下面再对NVIC结构体中出现的其他生词进行阐述:
①IRQ(Interrupt Request ):中断请求。
比如想要执行“紧急事件”必须向处理器提出申请(发一个电脉冲信号),要求“中断”,即要求处理器先停下“自己手头的工作”先去处理“紧急事件”,这一“申请”过程,称为——中断请求。
②什么叫抢占优先级和响应优先级(子优先级)?
这两个优先级发挥的作用就是决断,触发中断后,决断哪个更重要(优先级更高)就优先执行哪一个,也就是优先重要不紧急的事,这也是一种高效率做事的方式。
STM32 的中断向量具有两个属性,一个为抢占属性,另一个为响应属性,其属性编号越小,表明它的优先级别越高。
抢占:是指打断其他中断,执行优先级更高的程序,所以会出现嵌套中断,即在执行中断服务函数A 的过程中被中断B 打断,那么此时就要停下来,开始执行中断服务函数B,执行完B后再继续执行原来的中断服务函数A,抢占属性由NVIC_IRQChannelPreemptionPriority 的参数配置。
响应:在抢占优先级相同且同时到达的情况下,则先处理响应优先级高的中断, 响应属性由NVIC_IRQChannelSubPriority 参数配置。
总结一下:就是中断一次性可能有多个发生,就比如你正在打游戏,可能让你停下打游戏的突发事情有很多,而且还可能同时出现,那么此时就要理清先做哪一个,后做哪一个,其实本质还是优先“重要不紧急”的事。
③优先级是如何定义的呢?
在NVIC 有一个专门的寄存器:中断优先级寄存器NVIC_IPRx,用来配置外部中断的优先级,IPR宽度为8bit,原则上每个外部中断可配置的优先级为0~255,即存在256个中断,优先级数值越小,优先级越高。但是绝大多数CM3 芯片都会精简设计,以致实际上支持的优先级数减少,上面F103的外部中断清单图可知,F103只有60个外部中断,同时在F103 中,只使用了高4bit,即高四位有效。
用于表达优先级的这4bit,又被分组成抢占优先级和子优先级。如果有多个中断同时响应,抢占优先级高的就会抢占抢占优先级低的优先得到执行,如果抢占优先级相同,就比较子优先级。如果抢占优先级和子优先级都相同的话,就比较他们的硬件中断编号,编号越小,优先级越高。
中断优先级比较:多个中断触发时,先比较它们的抢占优先级,再比较响应优先级,最后比较编号(一定不会相同)。
优先级被分为了5组,通过这5组可以将中断设置为主或者子优先级属性,也就是可编程的。
在这里对表格做个解释,不然这里很容易存在一个误区:就是错把0bit当作是第0位,4bit当作是第4位,其实这是错误的,在官方手册的编程说明它会有bit和bits的区分,在这里主-0bit指的是高四位中没有一个可以配置主优先级,子-4bit指的是这四位都可以用来配置子优先级
第0组:所有4位用于配置响应优先级
第1组:最高1位用于配置抢占式优先级,最低3位用于配置响应优先级
第2组:最高2位用于配置抢占式优先级,最低2位用于配置响应优先级
第3组:最高3位用于配置抢占式优先级,最低1位用于配置响应优先级
第4组:所有4位用于配置抢占式优先级
这里要求我们学会调用官方库文件就可以,设置优先级分组可调用库函数NVIC_PriorityGroupConfig() 实现,有关NVIC 中断相关的库函数都在库文件misc.c 和misc.h 中。
misc文件是存放杂文件的固件库
3. 解释EXTI:
EXTI:External interrupt/event controller,外部中断/事件控制器,管理了控制器的20 个中断/事件线。每个中断/事件线都对应有一个边沿检测器,可以实现输入信号的上升沿检测和下降沿的检测。
①那么EXTI如何接收中断信号呢?
在STM32中,所用的GOIO都与EXTI外部中断线相连,因此所有的GPIO都可以配置成外部中断,即通过GPIO可以接收中断信号源(按键按下去这一信息)。
由上图可知,PA0~PG0连接到EXTI0;以此类推,PA1-PG1连接到EXTI1、…PA15-PG15连接到EXTI15。
EXTI 有20 个中断/事件线,每个GPIO 都可以被设置为输入线,占用EXTI0 至EXTI15
还有另外四根用于特定的外设事件:
由GPIO和EXTI连接图可知,这里要使用GPIO的复用功能(AFIO),将GPIO引脚设置为接收中断的引脚,通过AFIO_EXTICRx配置GPIO线上的外部中断/事件,首先必须先使能AFIO时钟。
所以这里也要配置EXTI,选择GPIO的复用功能,根据EXTI初始化结构体,可知它包含哪些参数,并对其进行设定。
下图EXTI_InitTypeDef结构体包含的函数参数:
typedef struct {
uint32_t EXTI_Line; // 中断/事件线
EXTIMode_TypeDef EXTI_Mode; // EXTI 模式
EXTITrigger_TypeDef EXTI_Trigger; // 触发类型
FunctionalState EXTI_LineCmd; // EXTI 使能
} EXTI_InitTypeDef;
下面实验案例为:按键控制中断实现点灯,按下按键,触发中断,LED灯亮(与前一篇按键控制点灯实现功能一样),按键原理图如下:
先针对EXTI代码中的函数名做个简单的阐述:
GPIO_EXTILineConfig()函数的作用是:引脚选择,指明当前系统中使用哪一个引脚作为外部中断触发的引脚,就是EXTIx要与哪一个GPIOx连到一起。
什么是中断服务函数?
当中断发生时,要执行的就是中断服务函数中的程序,存放在stm32f10x_it.c文件中,需要自己编写。
注意:中断服务函数名是不可以自己定义的,必须要与启动文件startup_stm32f10x_hd.s中断向量表定义的一致。
具体内容我已在官方提供的固件库文件中标了出来:
下面我直接上代码进行实验,每一行代码都有相应的解释,没有解释的,那就说明在以前的文章中有解释,想知道为什么可以看前面写的文章,也随时欢迎与我进行探讨:
exti.h
#ifndef __EXTI_H
#define __EXTI_H
#include "stm32f10x.h"
//引脚定义
#define KEY1_INT_GPIO_CLK (RCC_APB2Periph_GPIOA|RCC_APB2Periph_AFIO) //GPIOA的时钟,GPIOA的复用
#define KEY1_INT_GPIO_PORT GPIOA //按键中断GPIO组
#define KEY1_INT_GPIO_PIN GPIO_Pin_0
#define KEY1_INT_EXTI_PORTSOURCE GPIO_PortSourceGPIOA //选择GPIO组
#define KEY1_INT_EXTI_PINSOURCE GPIO_PinSource0 //选择GPIO引脚
#define KEY1_INT_EXTI_LINE EXTI_Line0 //选择EXTI线
#define KEY1_INT_EXTI_IRQ EXTI0_IRQn //中断通道
#define KEY1_IRQHandler EXTI0_IRQHandler //中断服务函数
#define KEY2_INT_GPIO_CLK (RCC_APB2Periph_GPIOC|RCC_APB2Periph_AFIO) //GPIOA的时钟,GPIOA的复用
#define KEY2_INT_GPIO_PORT GPIOC //按键中断GPIO组
#define KEY2_INT_GPIO_PIN GPIO_Pin_13
#define KEY2_INT_EXTI_PORTSOURCE GPIO_PortSourceGPIOC //选择GPIO组
#define KEY2_INT_EXTI_PINSOURCE GPIO_PinSource13 //选择GPIO引脚
#define KEY2_INT_EXTI_LINE EXTI_Line13 //选择EXTI线
#define KEY2_INT_EXTI_IRQ EXTI15_10_IRQn //中断通道
#define KEY2_IRQHandler EXTI15_10_IRQHandler //中断服务函数
void EXTI_KEY_Config(void);
#endif
exti.c
#include "exti.h"
/*配置嵌套向量中断控制器NVIC*/
static void NVIC_Config(void)
{
//1.引入两个结构体
NVIC_InitTypeDef NVIC_InitStructure;
//2.配置NVIC为优先级组1
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
//3.配置中断源:按键1+按键2
NVIC_InitStructure.NVIC_IRQChannel=KEY1_INT_EXTI_IRQ;
//4.配置抢占优先级
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1;
//5.配置响应优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority=1;
//6.使能中断
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
//7.初始化NVIC中断
NVIC_Init(&NVIC_InitStructure);
/* 配置中断源:按键2,其他使用上面相关配置 */
NVIC_InitStructure.NVIC_IRQChannel = KEY2_INT_EXTI_IRQ;
NVIC_Init(&NVIC_InitStructure);
}
/*配置IO口为EXTI中断口,并设置中断优先级*/
void EXTI_KEY_Config(void)
{
//1.引入GPIO和EXTI两个结构体
GPIO_InitTypeDef GPIO_InitStructure;
EXTI_InitTypeDef EXTI_InitStructure;
//2.打开GPIO的时钟
RCC_APB2PeriphClockCmd(KEY1_INT_GPIO_CLK,ENABLE);
//3.引入NVIC函数
NVIC_Config();
//4.配置KEY1
GPIO_InitStructure.GPIO_Pin=KEY1_INT_GPIO_PIN;//选择按键引脚
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IN_FLOATING; //选择引脚为浮空输入模式
GPIO_Init(KEY1_INT_GPIO_PORT,&GPIO_InitStructure);//调用库函数,初始化GPIO
GPIO_EXTILineConfig(KEY1_INT_EXTI_PORTSOURCE,KEY1_INT_EXTI_PINSOURCE);//选择EXTI信号源,也就是EXTI要连接GPIOtx
EXTI_InitStructure.EXTI_Line=KEY1_INT_EXTI_LINE;//初始化EXTI_LINE
EXTI_InitStructure.EXTI_Mode=EXTI_Mode_Interrupt;//EXTI设置为中断模式
EXTI_InitStructure.EXTI_Trigger=EXTI_Trigger_Rising;//设置EXTI为上升沿触发中断
EXTI_InitStructure.EXTI_LineCmd=ENABLE;//使能中断
EXTI_Init(&EXTI_InitStructure);//初始化中断
//5.配置KEY2
GPIO_InitStructure.GPIO_Pin=KEY2_INT_GPIO_PIN;//选择按键引脚
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IN_FLOATING; //选择引脚为浮空输入模式
GPIO_Init(KEY2_INT_GPIO_PORT,&GPIO_InitStructure);//调用库函数,初始化GPIO
GPIO_EXTILineConfig(KEY2_INT_EXTI_PORTSOURCE,KEY2_INT_EXTI_PINSOURCE);//选择EXTI信号源,也就是EXTI要连接GPIOtx
EXTI_InitStructure.EXTI_Line=KEY2_INT_EXTI_LINE;//初始化EXTI_LINE
EXTI_InitStructure.EXTI_Mode=EXTI_Mode_Interrupt;//EXTI设置为中断模式
EXTI_InitStructure.EXTI_Trigger=EXTI_Trigger_Falling;//设置EXTI为下降沿触发中断
EXTI_InitStructure.EXTI_LineCmd=ENABLE;//使能中断
EXTI_Init(&EXTI_InitStructure);//初始化中断
}
led.h
#ifndef __LED_H
#define __LED_H
#include "stm32f10x.h"
//R-红色
#define LED1_GPIO_CLK RCC_APB2Periph_GPIOB
#define LED1_GPIO_PORT GPIOB
#define LED1_GPIO_PIN GPIO_Pin_5
//G-绿色
#define LED2_GPIO_CLK RCC_APB2Periph_GPIOB
#define LED2_GPIO_PORT GPIOB
#define LED2_GPIO_PIN GPIO_Pin_0
//B-蓝色
#define LED3_GPIO_CLK RCC_APB2Periph_GPIOB
#define LED3_GPIO_PORT GPIOB
#define LED3_GPIO_PIN GPIO_Pin_1
#define ON 0
#define OFF 1
/* 使用标准的固件库控制IO*/
#define LED1(a) if (a) \
GPIO_SetBits(LED1_GPIO_PORT,LED1_GPIO_PIN);\
else \
GPIO_ResetBits(LED1_GPIO_PORT,LED1_GPIO_PIN)
#define LED2(a) if (a) \
GPIO_SetBits(LED2_GPIO_PORT,LED2_GPIO_PIN);\
else \
GPIO_ResetBits(LED2_GPIO_PORT,LED2_GPIO_PIN)
#define LED3(a) if (a) \
GPIO_SetBits(LED3_GPIO_PORT,LED3_GPIO_PIN);\
else \
GPIO_ResetBits(LED3_GPIO_PORT,LED3_GPIO_PIN)
/* 直接操作寄存器的方法控制IO */
#define digitalHi(p,i) {p->BSRR=i;} //输出为高电平
#define digitalLo(p,i) {p->BRR=i;} //输出低电平
#define digitalToggle(p,i) {p->ODR ^=i;} //输出反转状态
/* 定义控制IO的宏 */
#define LED1_TOGGLE digitalToggle(LED1_GPIO_PORT,LED1_GPIO_PIN)
#define LED1_OFF digitalHi(LED1_GPIO_PORT,LED1_GPIO_PIN)
#define LED1_ON digitalLo(LED1_GPIO_PORT,LED1_GPIO_PIN)
#define LED2_TOGGLE digitalToggle(LED2_GPIO_PORT,LED2_GPIO_PIN)
#define LED2_OFF digitalHi(LED2_GPIO_PORT,LED2_GPIO_PIN)
#define LED2_ON digitalLo(LED2_GPIO_PORT,LED2_GPIO_PIN)
#define LED3_TOGGLE digitalToggle(LED3_GPIO_PORT,LED3_GPIO_PIN)
#define LED3_OFF digitalHi(LED3_GPIO_PORT,LED3_GPIO_PIN)
#define LED3_ON digitalLo(LED3_GPIO_PORT,LED3_GPIO_PIN)
void LED_GPIO_Config(void);
#endif
led.c
#include "led.h"
void LED_GPIO_Config(void)
{
//1.引入两个GPIO结构体
GPIO_InitTypeDef GPIO_InitStructure;
//2.打开GPIO时钟
RCC_APB2PeriphClockCmd(LED1_GPIO_CLK|LED2_GPIO_CLK|LED3_GPIO_CLK,ENABLE);
//3.选择引脚
GPIO_InitStructure.GPIO_Pin=LED1_GPIO_PIN|LED2_GPIO_PIN|LED3_GPIO_PIN;
//4.配置引脚模式
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;
//5.配置引脚速度
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
//6.初始化GPIO
GPIO_Init(LED1_GPIO_PORT,&GPIO_InitStructure);
GPIO_Init(LED2_GPIO_PORT,&GPIO_InitStructure);
GPIO_Init(LED3_GPIO_PORT,&GPIO_InitStructure);
//7.关闭所有LED灯,从最初状态开始运行
GPIO_SetBits(GPIOB,GPIO_Pin_All);
}
stm32f10x_it.c
#include "led.h"
#include "exti.h"
void KEY1_IRQHandler(void)
{
if(EXTI_GetITStatus(KEY1_INT_EXTI_LINE)!=RESET) //获得中断位,重新检查是否产生中断,
{
LED1_TOGGLE;//LED1取反,点亮LED
//清除中断位,退出中断
EXTI_ClearITPendingBit(KEY1_INT_EXTI_LINE);
}
}
void KEY2_IRQHandler(void)
{
if(EXTI_GetITStatus(KEY2_INT_EXTI_LINE)!=RESET) //获得中断位,重新检查是否产生中断,
{
LED2_TOGGLE;//LED2取反,点亮LED
//清除中断位,退出中断
EXTI_ClearITPendingBit(KEY2_INT_EXTI_LINE);
}
}
main.c
#include "stm32f10x.h"
#include "led.h"
#include "exti.h"
int main(void)
{
LED_GPIO_Config();//调用LED函数
LED3(ON);//点亮蓝灯,可以实现R/G/B混合成白灯,这里使用库函数操作,ON=0
EXTI_KEY_Config(); //调用KEY函数
/* 等待中断,由于使用中断方式,CPU不用轮询按键 */
while(1)
{
}
}
再说一下关于魔术棒配置:
STM32F10X_HD,USE_STDPERIPH_DRIVER这两个宏定义代表的含义:
USE_STDPERIPH_DRIVER:是告诉编译器,我们需要使用ST官方库;
STM32F10X_HD:表示我们使用的芯片是大容量的stm32,添加之后就可以使用库文件里面专为大容量芯片定义的寄存器。
注意:两个宏定义之间是英文逗号。
最后,我还是有些地方没有搞清楚:比如为什么要用空while等待中断,如果去掉这一行,灯则不会亮;
对使用官方库函数的控制GPIO端口解释一下:
这里有一点不理解的是:\的作用是什么?如果去掉的话,则会出错
说明:因为我使用的是野火开发板,所以选取的资料大多来源于野火,一般我都是自己弄懂之后才会写出来,这样也助于提高自己,而且这篇文章的目的也不是点灯,而是要通过按键点灯学会中断,因为中断的用途非常大,经常使用,所以一定要学会使用中断。
作者能力水平有限,文章难免存在错误和纰漏,请大佬不吝赐教,非常欢迎大家与小白杨进行技术交流,希望在此能遇到志同道合的朋友,一起学习技术。