前言

今天我们来学习外部中断(EXTI)的初始化及简单应用。可能会有读者感到奇怪,上一期不就是数码管么,怎么这一期又是?这一期虽然也是按键控制数码管,但这次是利用IO脚产生外部中断,执行中断服务函数来实现的。剧透一下:这次的实现效果和上一期按键扫描的效果不太一样,希望细心的你看过视频后可以发现。(文末公布答案)

一、前置知识——中断与STM32外部中断简述

1、中断的概念

因为这个概念非常基础,学习过51单片机或者学校有开微机原理与接口技术的同学应该都知道;所以我就简单通俗的讲一下就行。中断是相对于CPU的一个概念。CPU在执行正常的程序时,突然CPU内部发生一个错误,或是有其他的工作需要打断当前代码执行顺序,这时就发生了中断。迫使CPU停止当前正常代码执行流程的信号称为中断源,CPU离开正常代码而去执行的代码段被称为中断服务程序。

2、CPU响应中断时的工作流程(单层中断)

CPU响应了中断,也就是终端服务程序结束后还要返回原先被打断的地方继续执行正常程序的。因此,如果把响应中断这一事件从具体CPU中抽象出来,CPU的工作流程大致如下所示:

断点处下一条待执行指令的地址入栈--->CPU标志寄存器入栈--->CPU各寄存器值入栈--->关闭中断--->执行中断服务程序--->开启中断--->CPU各寄存器值出栈--->CPU标志寄存器出栈--->断点处下一条待执行指令的地址出栈

3、中断优先级

在早期的MCU(比如Intel 8051)或者MPU(比如Intel 8086/8088)中,中断优先级一般就只有一个含义,仅仅指优先级高的中断可以打断正在执行的、优先级低的中断(抢占优先级)。但是以ARM Cortex-M3为内核的STM32F103存在两种类型的中断优先级——抢占优先级和响应优先级。

抢占优先级是针对嵌套中断而言的。举个例子就很容易理解了。假如中断A比中断B的抢占优先级要高,B发生后正在执行B的中断服务程序;这时A也发生了,那么CPU将从B的中断服务程序中响应中断A,转而执行A的中断服务程序。

响应优先级则是指当两个抢占优先级相同的中断同时发生时,CPU将优先处理响应优先级高的中断。

4、中断与事件

有时候我们会混着讲“中断事件”,但其实二者是有区别的。二者的共同点在于都由中断触发,都可打断当前正常程序流程转而进行其他操作;区别在于中断是依赖于CPU的,必须通过中断服务程序起作用,而事件不通过CPU,由硬件电路直接联动实现。

5、NVIC——嵌套向量中断控制器

个人感觉这玩意的作用其实和8255A差不太多,都是用于管理中断并允许用户设置外部中断优先级的一个器件。用户可以通过库函数NVIC_PriorityGroupConfig(参数:NVIC_PriorityGroup_0/1/2/3/4)为NVIC设置五个不同的优先级组中的一个以配置外部中断的中断优先级。

6、用户程序使用外部中断的程序流程

这一部分内容太多,我就把我自己画的假流程图放上来吧。




esp32 中断 优先级_外部中断


二、实验效果


esp32 中断 优先级_stm32外部中断实验报告_02


https://www.zhihu.com/video/1238892513039425536

三、本实验的用户函数(硬件资源参见上一期即可)

1、NVIC初始化(设置优先级)


void NVICConfig(void){
	NVIC_InitTypeDef keyNVIC;
	/*PC8和PC9配置外部中断*/
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
	keyNVIC.NVIC_IRQChannel = EXTI9_5_IRQn;//设置产生外部中断的IO引脚为5-9(实际使用8和9)
	keyNVIC.NVIC_IRQChannelCmd = ENABLE;//开中断
	keyNVIC.NVIC_IRQChannelPreemptionPriority = 0;//设置抢占优先级(数字越小级别越高)
	keyNVIC.NVIC_IRQChannelSubPriority = 0;//设置响应优先级(数字越小级别越高)
	NVIC_Init(&keyNVIC);
	/*PD2配置外部中断*/
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
	keyNVIC.NVIC_IRQChannel = EXTI2_IRQn;//设置产生外部中断的IO引脚为2
	keyNVIC.NVIC_IRQChannelCmd = ENABLE;//开中断
	keyNVIC.NVIC_IRQChannelPreemptionPriority = 0;//设置抢占优先级(数字越小级别越高)
	keyNVIC.NVIC_IRQChannelSubPriority = 0;//设置响应优先级(数字越小级别越高)
	NVIC_Init(&keyNVIC);
}


2、外部中断初始化(配置外部中断工作模式)


void KeyInterruptInit(void){
	EXTI_InitTypeDef key;
	NVICConfig();//配置NVIC
	GPIO_EXTILineConfig(GPIO_PortSourceGPIOC,GPIO_PinSource8);//PC8配置为中断源
	GPIO_EXTILineConfig(GPIO_PortSourceGPIOC,GPIO_PinSource9);//PC9配置为中断源
	GPIO_EXTILineConfig(GPIO_PortSourceGPIOD,GPIO_PinSource2);//PD2配置为中断源
	/*配置PC8工作模式*/
	key.EXTI_Line = EXTI_Line8;//中断源发生线为EXTI8
	key.EXTI_LineCmd = ENABLE;//允许响应中断
	key.EXTI_Mode = EXTI_Mode_Interrupt;//设置为中断模式
	key.EXTI_Trigger = EXTI_Trigger_Rising;//设置为上升沿触发
	EXTI_Init(&key);
	/*配置PC9工作模式*/
	key.EXTI_Line = EXTI_Line9;
	key.EXTI_LineCmd = ENABLE;
	key.EXTI_Mode = EXTI_Mode_Interrupt
	key.EXTI_Trigger = EXTI_Trigger_Rising;
	EXTI_Init(&key);
	/*配置PD2工作模式*/
	key.EXTI_Line = EXTI_Line2;
	key.EXTI_LineCmd = ENABLE;
	key.EXTI_Mode = EXTI_Mode_Interrupt;
	key.EXTI_Trigger = EXTI_Trigger_Rising;
	EXTI_Init(&key);
}


3、中断服务程序


extern intr_flag;//全局变量,用于识别哪一个中断发生
/*KEY0与KEY1*/
void EXTI9_5_IRQHandler(void){
	int n = 100000;
	while(n--);//延时,消抖
	if(EXTI_GetITStatus(EXTI_Line8) != RESET){
		intr_flag = 1;//PC8上发生中断(KEY0被按下)
		EXTI_ClearITPendingBit(EXTI_Line8);//清除CPU内部中断标志位
	}
	if(EXTI_GetITStatus(EXTI_Line9) != RESET){
		intr_flag = 2;
		EXTI_ClearITPendingBit(EXTI_Line9);
	}
}

/*KEY2*/
void EXTI2_IRQHandler(void){
	int n = 100000;
	while(n--);
	if(EXTI_GetITStatus(EXTI_Line2) != RESET){
		intr_flag = 3;
		EXTI_ClearITPendingBit(EXTI_Line2);
	}
}


4、数码管显示程序

主体


void ShowNumDynamicByEXTI(void){
	static int countA=0, countB=0, countC=0;
	HC595_WriteData(num[17]);//消影
	switch(intr_flag){
		case 1:
			countA++;//按键1号计数器自增
		        intr_flag = 0;//重置中断发生标志位
			break;
		case 2:
			countB++;
			intr_flag = 0;
			break;
		case 3:
			countC++;
			intr_flag = 0;
			break;
		default:
			break;
	}
	show(&countA, &countB, &countC);//调用辅助函数显示数字
}


辅助函数(考虑到这部分的语句和上一期的显示语句完全相同,我就把这部分抽象出来写了个函数,减少重复代码的情况)


void show(int* a, int* b, int* c){
	/*KEY0*/
	if(*a<=15)
				HC595_WriteData(num[*a]);
	else{
				HC595_WriteData(num[0]);
				*a = 0;
	}
	GPIO_ResetBits(GPIOC, 0x1c00)
	delay();
	/*KEY1*/
	if(*b<=15)
				HC595_WriteData(num[*b]);
	else{
				HC595_WriteData(num[0]);
				*b = 0;
	}
	GPIO_ResetBits(GPIOC, GPIO_Pin_12 | GPIO_Pin_11);
	GPIO_SetBits(GPIOC, GPIO_Pin_10);
	delay();
	/*KEY2*/
	if(*c<=15)
				HC595_WriteData(num[*c]);
	else{
				HC595_WriteData(num[0]);
				*c = 0;
	}
	GPIO_ResetBits(GPIOC, GPIO_Pin_10 | GPIO_Pin_12);
	GPIO_SetBits(GPIOC, GPIO_Pin_11);
	delay();
}


四、总结

相信细心的你已经发现了,上一期和这一期有两点不同。

1、数字越界后的处理

上一期数字越界后直接亮的小数点,数字部分熄灭;这次在越界后又从0开始进入下一个轮回。仔细观察show()函数就会发现,我传入了三个指针,这三个指针用于修改三个按键计数器的值——一旦发生越界,即清0相应的计数器。

2、按键切换数字时,数码管不再熄灭一小段时间

上一期中,我们每按下一次按键,所有数码管都要熄灭,直到我们松开按键才会被重新点亮;而这次无论松开还是按下按键,数码管都是被点亮的。

还记得上一期的按键扫描程序吗?在函数KeyScan()中,我们有一句while(GPIO_ReadInputDataBit(GPIOx, Pin) == ON);这句的意思就是等待按键松开后再进行后续操作。当按键松开后,该函数返回一个值给显示数字的ShowNumDynamic()函数,收到返回值后才开始进行GPIO的相关操作,点亮数码管。因此,只要我们一直按住按键,这个while循环一直不会结束,一直不会有返回值,“关闭所有显示”后的if语句一直都不会执行结束,数码管也就会一直熄灭了。

而这期呢,我们得从外部中断初始化那儿说起。在KeyInterruptInit()中,我们将三个中断源都配置为“上升沿触发”。这就是关键所在。在我们按下按键后,对应的GPIO由高电平——>低电平,产生一个下降沿,是不会触发中断的;我们一直按着按键,对应的GPIO一直是低电平,中断也没有被触发;直到我们松开按键,产生了一个由低电平到高电平的上升沿跳变,成功触发了中断!也就是说,在我们松开按键的那一瞬间之前的时间里,由于中断一直没有被触发,在执行ShowNumDynamicByEXTI()时switch-case结构是一直被跳过的,直接执行了用于点亮数码管的show()函数。因此数码管一直都被点亮,没有熄灭。

五、结语

STM32真好玩!!!!!!!!!!!