前导:本文的目的与,意在于面向应用的学习单片机,故不会涉及太多的原理知识,例如寄存器之类的。
主要目的在于面向应用的学习单片机,学会单片机的基础用法,开发板采取野火的指南者f103。
作者大二小白,写的不好的地方轻点喷,欢迎评论区交流
全部工程代码开源在Gitee仓库
文章目录
1 中断简介
1.1 为什么需要中断?
想象一个生活场景,在生活中,我是一个处理能力的主体,做饭,烧水,买菜,打游戏等等的,在没有任何提醒的下,我是只能做一件事情的。
例如我在打游戏的时候,我只能打游戏,做饭的时候,只能做饭。
显然这样的效率是十分低下的,但是在真实的生活中,是充斥着各自提示消息的,例如水烧开的时候,发出响声,我听到水烧开的声音,放下手头的工作,去把烧水壶拿开,然后接着回头干我自己的事情。
假设我不去听水烧开的声音,我只是看着水在那边开始冒热气,直到烧开,那这几分钟内我什么事情都干不了。
同样,洗衣机洗衣服也是同理,我不会看着衣服洗完,我只会等待洗衣机洗好后发出滴滴声提醒我。
生活的存在着各种信息,提醒我们暂时放下手头的工作,转而完成另外一件事情,再回到手头工作
而在计算机中,这种机制叫做中断处理机制
1.2 中断
| 中断机制生活例子 | 对应代码 |
|---|---|
| 开始洗衣服时,我会注意洗衣机洗完的响声 | 写代码绑定一个中断事件,进行监听 |
| 当洗衣机的响声和烧水壶的响声同时响起时,我会先去拿开烧水壶 | 配置中断优先组,当两个中断同时发生的时候,优先处理优先级高的事情 |
| 听到响声 | 配置中断触发条件 |
| 去拿走烧水壶 | 将操作写进中断服务函数 |
| 处理完事件后,回到手头上正在做的事情 | 清除标志位 |
中断整体工作流程如下

注意,跳转到中断服务程序的时候,我需要问你一个问题,怎么跳转?程序怎么知道,我要跳转到哪里。
这里要提及一个概念,函数在程序中,是有自己的地址的。程序是依靠着地址,跳转到函数,执行函数里的操作的。
中断以及对应函数是由stm32提供的,自然,官方也就给我们准备好了各种中断函数。
在**startup.s里有一系列的中断函数定义,也叫做异常向量组。**当某个中断发生的时候,就会跳转到对应的中断函数里,执行我们要做的操作。执行完再返回主函数。
这意味着,中断函数的名字我们是不要去改变的,我们要去这个文件里复制对应的中断服务函数,然后在里面写代码。

1.3 EXTI外部中断事件
External interrupt / event controller 外部中断/事件控制器
管理了一组中断/事件线。每个中断/事件线都对应有一个边沿检测器,可以实现输入信号的上升沿检测和下降沿的检测。

平时我们会使用 EXTI0至 EXTI15对应的就是GPIO的0-15号引脚的中断。
1.4 NVIC中断组
Nested vectoredinterrupt controller,即嵌套向量中断控制器。
嵌套的理解就是,多个中断接连发生,嵌套在一起的时候,该以什么顺序处理这些中断。
他具有五个分组,两个属性
分组部分:
记住整个程序只能配置一个分组,相当于是一个规则

这里的主和子,指的就是后面的抢占和优先的取值。
属性部分:
一个为抢占属性,另一个为响应属性,其属性编号越小,表明它的优先级别越高。
- 抢占,是指打断其他中断的属性,即因为具有这个属性会出现嵌套中断(在执行中断服务函数A 的过程中被中断B 打断,执行完中断服务函数B 再继续执行中断服务函数A),抢占属性由
NVIC_IRQChannelPreemptionPriority的参数配置。 - 响应,则应用在抢占属性相同的情况下,当两个中断向量的抢占优先级相同时,如果两个中断同时到达, 则先处理响应优先级高的中断, 响应属性由
NVIC_IRQChannelSubPriority参数配置。

若内核正在执行C 的中断服务函数,则它能被抢占优先级更高的中断A 打断。
由于B 和C 的抢占优先级相同,所以C 不能被B 打断。
但如果B 和C 中断是同时到达的,程序就会首先执行响应优先级别更高的B 中断
(高优先级的抢占优先级是可以打断正在进行的低抢占优先级中断的,而抢占优先级相同的中断,高优先级的响应优先级不可以打断低响应优先级的中断)。
优先级取值范围如下

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

根据这张图,我们可以写出,代码大致的配置流程如下
- 开启复用时钟(别问为什么哈哈)
- 配置指定端口的中断线
- 配置中断触发模式,中断触发的条件
- 使能中断线,并初始化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里找到对应的初始化结构体。

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


得到这些信息后,我们开始配置,可以复制一份之前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里,我们还是照例跳进去看。发现一个这样的结构体

可以看到四个参数
- 1:中断源的通道,也就是我们的中断线0
- 2:抢占优先级
- 3:响应优先级
- 4:使能
代码编写,就像1.5里讲的一样,从组开始配置,我们查看misc.h的最下方,第一个函数。翻译过来就是
NVIC优先组配置。

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

代码编写如下(也可以在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的中断服务函数的函数名了。

KEY也是连接在PA0上的,自然配置的也就是EXTI0的中断,那么复制这行函数,写到stm32f10x_it.c里面。
it是interrupt中断的意,约定俗成的我们中断服务函数都会写到stm32f10x_it.c里,当然要写其他地方也行。就像这样,这里面要放的就是我们产生中断后要进行的处理了。

接下来,我们可以写我们的逻辑了,思考一下,在中断函数里需要什么?
既然已经跳转到中断函数里了,那么证明这个中断被触发了,也就是,按键产生下降沿了,意味着我们已经松手了,松手后需要的就是。 按键次数++
我们现在这里 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的中断源被定义在这一个里面了

那么我们如下改造
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

所以这次我们在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);
}
本文通过实例介绍如何在STM32单片机中配置外部中断(EXTI)和NVIC,实现按键按下松手的中断触发,实现实时按键计数。通过生活比喻阐述中断机制,并提供关键代码段,适合初学者快速入门单片机应用开发。
997

被折叠的 条评论
为什么被折叠?



