面向应用学习stm32(4)-EXTI外部中断

本文通过实例介绍如何在STM32单片机中配置外部中断(EXTI)和NVIC,实现按键按下松手的中断触发,实现实时按键计数。通过生活比喻阐述中断机制,并提供关键代码段,适合初学者快速入门单片机应用开发。

前导:本文的目的与,意在于面向应用的学习单片机,故不会涉及太多的原理知识,例如寄存器之类的。

主要目的在于面向应用的学习单片机,学会单片机的基础用法,开发板采取野火的指南者f103。

作者大二小白,写的不好的地方轻点喷,欢迎评论区交流

全部工程代码开源在Gitee仓库

1 中断简介

1.1 为什么需要中断?

​ 想象一个生活场景,在生活中,我是一个处理能力的主体,做饭,烧水,买菜,打游戏等等的,在没有任何提醒的下,我是只能做一件事情的。

​ 例如我在打游戏的时候,我只能打游戏,做饭的时候,只能做饭。

​ 显然这样的效率是十分低下的,但是在真实的生活中,是充斥着各自提示消息的,例如水烧开的时候,发出响声,我听到水烧开的声音,放下手头的工作,去把烧水壶拿开,然后接着回头干我自己的事情。

假设我不去听水烧开的声音,我只是看着水在那边开始冒热气,直到烧开,那这几分钟内我什么事情都干不了。

​ 同样,洗衣机洗衣服也是同理,我不会看着衣服洗完,我只会等待洗衣机洗好后发出滴滴声提醒我。

生活的存在着各种信息,提醒我们暂时放下手头的工作,转而完成另外一件事情,再回到手头工作

​ 而在计算机中,这种机制叫做中断处理机制

1.2 中断

中断机制生活例子对应代码
开始洗衣服时,我会注意洗衣机洗完的响声写代码绑定一个中断事件,进行监听
当洗衣机的响声和烧水壶的响声同时响起时,我会先去拿开烧水壶配置中断优先组,当两个中断同时发生的时候,优先处理优先级高的事情
听到响声配置中断触发条件
去拿走烧水壶将操作写进中断服务函数
处理完事件后,回到手头上正在做的事情清除标志位

中断整体工作流程如下

在这里插入图片描述

​ 注意,跳转到中断服务程序的时候,我需要问你一个问题,怎么跳转?程序怎么知道,我要跳转到哪里。

​ 这里要提及一个概念,函数在程序中,是有自己的地址的。程序是依靠着地址,跳转到函数,执行函数里的操作的。

​ 中断以及对应函数是由stm32提供的,自然,官方也就给我们准备好了各种中断函数。

​ 在**startup.s里有一系列的中断函数定义,也叫做异常向量组。**当某个中断发生的时候,就会跳转到对应的中断函数里,执行我们要做的操作。执行完再返回主函数。

​ 这意味着,中断函数的名字我们是不要去改变的,我们要去这个文件里复制对应的中断服务函数,然后在里面写代码

image-20220505184112147

1.3 EXTI外部中断事件

External interrupt / event controller 外部中断/事件控制器

管理了一组中断/事件线。每个中断/事件线都对应有一个边沿检测器,可以实现输入信号的上升沿检测和下降沿的检测。

在这里插入图片描述

平时我们会使用 EXTI0至 EXTI15对应的就是GPIO的0-15号引脚的中断

1.4 NVIC中断组

Nested vectoredinterrupt controller,即嵌套向量中断控制器。

嵌套的理解就是,多个中断接连发生,嵌套在一起的时候,该以什么顺序处理这些中断。

他具有五个分组,两个属性

分组部分:

记住整个程序只能配置一个分组,相当于是一个规则

img

这里的主和子,指的就是后面的抢占和优先的取值。

属性部分:

一个为抢占属性,另一个为响应属性,其属性编号越小,表明它的优先级别越高。

  • 抢占,是指打断其他中断的属性,即因为具有这个属性会出现嵌套中断(在执行中断服务函数A 的过程中被中断B 打断,执行完中断服务函数B 再继续执行中断服务函数A),抢占属性由NVIC_IRQChannelPreemptionPriority 的参数配置。
  • 响应,则应用在抢占属性相同的情况下,当两个中断向量的抢占优先级相同时,如果两个中断同时到达, 则先处理响应优先级高的中断, 响应属性由NVIC_IRQChannelSubPriority 参数配置。

image-20220505190558135

​ 若内核正在执行C 的中断服务函数,则它能被抢占优先级更高的中断A 打断

​ 由于B 和C 的抢占优先级相同,所以C 不能被B 打断

​ 但如果B 和C 中断是同时到达的,程序就会首先执行响应优先级别更高的B 中断

(高优先级的抢占优先级是可以打断正在进行的低抢占优先级中断的,而抢占优先级相同的中断,高优先级的响应优先级不可以打断低响应优先级的中断)。

优先级取值范围如下

在这里插入图片描述

1.5 代码编写步骤

说到编写步骤,不如先来看看,中断的整体执行流程(忽略我不好看的字)

image-20220505191649722

根据这张图,我们可以写出,代码大致的配置流程如下

  • 开启复用时钟(别问为什么哈哈)
  • 配置指定端口的中断线
  • 配置中断触发模式,中断触发的条件
  • 使能中断线,并初始化EXTI
  • 配置优先组,绑定中断源(也就是所要触发的中断线)
  • 配置抢占优先级,响应优先级
  • 使能并初始化

2 中断例子代码

2.1 按键中断1

2.1.1需求

  • 我需要在按键按下,并且没有松手的时候,程序能够正常运行,松手的时候能够正常记录按键按下次数。

显然,之前的程序是有问题的。

当我们在按键扫描的函数里,增加判断松手的时候,也就是

#define KEY1 GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0)
if(KEY1)
{
	Delay(2500000);
	while(KEY1);
	key = '1';
}

​ 会导致一个bug,也就是,当我们按下,但不松手的时候,主函数就什么都执行不了因为他疯狂的扫描按键1的电平,而我们一直按着,就一直卡在while(1)里导致死循环。

​ 就像是我最开始举的例子,我只能在水旁边看着它烧开。

​ 所以我们现在就需要检查“听水烧开的声音” 这个信号。

也就是检查按键按下然后松手时触发的“信号”,配置中断了。

2.1.2EXTI配置

按照我们前面所说的流程进行配置。开启复用时钟后。

去stm32f10x_exti.h里找到对应的初始化结构体。

image-20220505194432815

​ 可以发现里面就有中断线,中断模式,中断触发器(触发条件),中断线使能四个参数,这些参数对应的定义在头文件里也有

image-20220505194547941

image-20220505194629391

得到这些信息后,我们开始配置,可以复制一份之前LCD的工程,在那上面进行改进。(在KEY_Init里配置)

//声明EXTI初始化结构体
EXTI_InitTypeDef EXTI_InitStructure;
//开启复用时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
//配置PA0的中断线
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA,GPIO_PinSource0);
//选择0号中断线,因为是PA0
EXTI_InitStructure.EXTI_Line=EXTI_Line0;			
//配置模式为中断触发模式,一般只用这个。
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;	
//配置为下降沿触发,按键按下时是高电平,也就是上升沿,一松手就是下降沿了
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
//使能中断线
EXTI_InitStructure.EXTI_LineCmd = ENABLE;			
//初始化中断
EXTI_Init(&EXTI_InitStructure);	

到这,EXTI的配置部分就已经完成了。

2.1.3NVIC配置

NVIC的相关内容存在misc.h里,我们还是照例跳进去看。发现一个这样的结构体

image-20220505195147189

可以看到四个参数

  • 1:中断源的通道,也就是我们的中断线0
  • 2:抢占优先级
  • 3:响应优先级
  • 4:使能

代码编写,就像1.5里讲的一样,从组开始配置,我们查看misc.h的最下方,第一个函数。翻译过来就是

NVIC优先组配置。

image-20220505195958586

并且在上方会发现它的取值有以下四个

image-20220505200113604

代码编写如下(也可以在KEY_Init里配置)

//声明NVIC初始化结构体
NVIC_InitTypeDef NVIC_InitStructure;
//配置中断组1,我们就一个按键中断,这个组怎么配置其实都可以了
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
//配置中断源
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;//这个参数去stm32f10x.h里找	
//配置抢占优先级
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;	
//配置响应优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;		
//使能
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;	
//初始化NVIC
NVIC_Init(&NVIC_InitStructure);  	  

到这。NVIC的配置部分也完成了。然后,可以删掉我们的KEY_Scan函数了,因为用的不是扫描法

2.1.4逻辑代码

​ 所有的配置部分写完之后,我们就准备开始编写业务逻辑代码。首先要转换一下思路之前我们是利用GPIO_ReadInputDataBit不断的扫描读取按键上的电平。

就像是我们看着水,水烧开了再进行操作。

​ 现在因为我们刚刚配置了 下降沿的中断触发,也就是,每当我们按键按下时,松开时,产生的那个下降沿,就会被程序监听到。然后产生一个中断信号,然后跳转到中断函数中进行逻辑处理。

​ 因为按键按下是高电平,产生上升沿,松手后变回低电平,下降沿产生。中断捕获到。跳转到中断函数进行处理。

问题来了,中断服务函数从何而来?在我1.2的中断讲解里有提到。可以回去观察。会发现里面有一个

EXTI0_IRQHandler这就是EXTI0的中断服务函数的函数名了。

image-20220505201024524

KEY也是连接在PA0上的,自然配置的也就是EXTI0的中断,那么复制这行函数,写到stm32f10x_it.c里面。

it是interrupt中断的意,约定俗成的我们中断服务函数都会写到stm32f10x_it.c里,当然要写其他地方也行。就像这样,这里面要放的就是我们产生中断后要进行的处理了。

image-20220505201619113

​ 接下来,我们可以写我们的逻辑了,思考一下,在中断函数里需要什么

​ 既然已经跳转到中断函数里了,那么证明这个中断被触发了,也就是,按键产生下降沿了,意味着我们已经松手了,松手后需要的就是。 按键次数++

​ 我们现在这里 extern 一个主函数的按键次数变量。
c语言中,其他的c文件要使用不属于自己的.c文件时,需要使用extern,具体规则可以查一下csdn。

extern int key1_count;
void EXTI0_IRQHandler(void)
{
		Delay(2500000);//同样也需要消抖,否则还是有可能多次触发
		key1_count++;
}

可,这样可以了吗?

答案是否定的,计算机不像人那么聪明,听到水烧开,就去拿开水壶,拿开水壶后就回去接着干自己的事情。

​ 计算机会进入这个中断,是因为产生中断信号后,某个标志位变为1了,标志为1的时候就会跳转到中断函数中。这段代码里,我们并没有把标志位清0,也就是说,在我们一次松手后,这个中断还是会不断的触发。因为进入这个中断的标志位一直为1。

​ 解决方案也很简单,清除标志位就好,stm32官方肯定也考虑到了。在stm32f10x_exti.h里有这样一行函数

void EXTI_ClearITPendingBit(uint32_t EXTI_Line);

​ 很明显,参数为中断线,效果为清除中断线的标志,那么最终代码改造如下

extern int key1_count;
void EXTI0_IRQHandler(void)
{
		Delay(2500000);//同样也需要消抖,否则还是有可能多次触发
		key1_count++;
		EXTI_ClearITPendingBit(EXTI_Line0); //产生中断后清除标志位
}

此时,我们加入流水灯和液晶屏进行测试。显示方面就不说了,上一章提到过了。

#include "stm32f10x.h"
#include "./lcd/bsp_ili9341_lcd.h"
#include "led.h"
#include "key.h"
#include <stdio.h>

char disp[20];
int key1_count = 0;

void LCD_Show();

int main (void)
{
	KEY_Init();//LED初始化
	LED_Init();//按键初始化
	
	ILI9341_Init();//液晶初始化    
	LCD_SetColors(BLACK,WHITE);//设置白底黑字
	LCD_SetFont(&Font8x16);//设置字体大小
	ILI9341_Clear(0,0,LCD_X_LENGTH,LCD_Y_LENGTH);//清屏
  	ILI9341_GramScan ( 6 );//设置显示模式
	
	LED_Color(LED_OFF);//关灯
	while (1)
	{
		LED_Flashing(LED_RED);
		LCD_Show();
	}
}

void LCD_Show()
{
	sprintf(disp,"KEY1_Count:%3d",key1_count);
	ILI9341_DispStringLine_EN(LINE(4),disp);
}

这时我们就会发现了,当按键按下,并且不松手的时候,流水灯不会卡死。而松手的时候,次数将会被记录并显示。

2.2 按键中断2

2.2.1 需求

配置KEY2的中断触发。

首先我们会想到,KEY2连接的是PC13,就会去寻找Exti_Line13,幸运的,我们找到了。

2.2.2EXTI配置

	GPIO_EXTILineConfig(GPIO_PortSourceGPIOC,GPIO_PinSource13);
	EXTI_InitStructure.EXTI_Line=EXTI_Line13;
	EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;	
	EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
	EXTI_InitStructure.EXTI_LineCmd = ENABLE;
	EXTI_Init(&EXTI_InitStructure);	 

2.2.3NVIC配置

​ 和之前的一样,我们去寻找EXTI13_IRQn中断源。运气不好的是,我们发现里面并没有EXTI13_IRQn!!!,可以理解为因为芯片并没有办法为我们提供那么多的线路去连接。

但是我们在stm32f10x.h里会找到这个家伙,EXTI15_10IRQn,好家伙,10-15的中断源被定义在这一个里面了

image-20220505204345227

那么我们如下改造

	NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn;			
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;	
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;					
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;							
	NVIC_Init(&NVIC_InitStructure);  

2.2.4逻辑代码

又是和上次一样的坑,并没有13号中断线的中断服务函数。取而代之的是EXTI15_10_IRQHandler

image-20220505204614996

所以这次我们在stm32f10x_it.c里就用这个函数写,写出如下代码

extern int key2_count;
void EXTI15_10_IRQHandler(void)
{
		Delay(2500000);
		key2_count++;
		EXTI_ClearITPendingBit(EXTI_Line13); 
}

接下来,在LCD测试的地方增加入key2按键次数的显示就可以了

void LCD_Show()
{
	sprintf(disp,"KEY1_Count:%3d",key1_count);
	ILI9341_DispStringLine_EN(LINE(2),disp);
	
	sprintf(disp,"KEY2_Count:%3d",key2_count); //增加按键2
	ILI9341_DispStringLine_EN(LINE(4),disp);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

这里煤球

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值