京东-优惠雷达
箱包品类日
嗨翻暑期档
随心所动,自在安睡
自营热卖

13 . 外部中断实验

韶华向远 2月前   阅读数 35 0

外部中断实验

介绍了STM32F10x 的中断,就来学习下外部中断。要实现的功能与按键实验一样,即通过按键控制LED,只不过这里采用外部中断方式进行控制。

1. 外部中断介绍

EXTI 简介
STM32F10x 外部中断/事件控制器(EXTI)包含多达20 个用于产生事件/中断请求的边沿检测器。EXTI 的每根输入线都可单独进行配置,以选择类型(中断或事件)和相应的触发事件(上升沿触发、下降沿触发或边沿触发),还可独立地被屏蔽。
EXTI 结构框图
在这里插入图片描述
EXTI 框图包含了EXTI 最核心内容,掌握了此框图,对EXTI 就有一个全局的把握,在编程的时候思路就非常清晰。从图中可以看到,总计有EXTI中断线20根,有很多信号线上都有标号9 样的“20”字样,这个表示在控制器内部类似的信号线路有20 个,这与STM32F10x 的EXTI 总共有20 个中断/事件线是吻合的。因此我们只需要理解其中一个的原理,其他的19个线路原理都是一样的。
EXTI 分为两大部分功能,一个产生中断,另一个产生事件,这两个功能从硬件上就有所差别,这个在框图中也有体现。从图中标号3 的位置处就分出了两条线路,一条是3-4-5 用于产生中断,另一条是3-6-7-8 用于产生事件。下面我们就来介绍下这两条线路:
(1)首先看下产生中断的这条线路(1-2-3-4-5)
1.标号1 为输入线,EXTI 控制器有20 个中断/事件输入线,这些输入线可以通过寄存器设置为任意一个GPIO,也可以是一些外设的事件,这部分内容我们会在后面专门讲解。输入线一般是存在电平变化的信号。
2.边沿检测电路,EXTI 可以对触发方式进行选择,通过上升沿触发选择寄存器和下降沿触发选择寄存器对应位的设置来控制信号触发。边沿检测电路以输入线作为信号输入端,如果检测到有边沿跳变就输出有效信号1 给红色框3 电路,否则输出无效信号0。而上升沿和下降沿触发选择这两个寄存器可以控制需要检测哪些类型的电平跳变过程,可以是只有上升沿触发、只有下降沿触发或者上升沿和下降沿都触发。
3.其实就是一个或门电路,一端输入信号线由标号2 提供,一端由软件中断事件寄存器提供,只要有一个为有效信号1,标号3 电路则输出有效信号1,否则为无效信号0。软件中断事件寄存器允许我们使用软件来启动中断/事件线,这个在某些地方非常有用。
4.其实就是一个与门电路,一端输入信号线由标号3 电路输出提供,一端由中断屏蔽寄存器提供,只有当两者都为有效信号1,标号4 电路才会输出有效信号1,否则输出无效。这样我们就可以简单的控制中断屏蔽寄存器来实现是否产生中断的目的。当我们把中断屏蔽寄存器设置为1 时,标号4 输出就取决于标号3 电路的输出。标号3 电路输出的信号会被保存到挂起寄存器内,如果确定标号3 电路输出为1 就会把挂起寄存器对应位置1。

5.将挂起寄存器内容输入到NVIC 内,从而实现系统中断事件的控制。
(2)最后我们再来看下产生事件这条线路(1-2-3-6-7-8),前面1-2-3都是一样的,只是在3 的输出后产生分歧。
6.其实就是一个与门电路,一端来至标号3 电路的输出信号,一端来至事件屏蔽寄存器,只有两者都为有效电平1,标号6 输出才有效。当事件屏蔽寄存器设置为0 时,不管标号3 电路输出为1 还是0,标号6 电路输出均为0。当事件屏蔽寄存器设置为1 时,标号6 电路输出取决于标号3 电路输出,这样就可以简单的控制事件屏蔽寄存器来实现是否产生事件的目的。
7.脉冲发生器电路,其输入端只与标号6 电路输出有关,标号6 输出有效,脉冲发生器才会输出一个脉冲信号。
8.脉冲信号,由标号7 脉冲发生器产生,是事件线路的终端,此脉冲信号可供其他外设电路使用,比如定时器、ADC 等。这样的脉冲信号通常用来触发定时器、ADC 等开始转换。

从上面EXTI 框图可以看出,中断线路最终会输入到NVIC 控制器中,从而会运行中断服务函数,实现中断内功能,这个是软件级的。而事件线路最后产生的脉冲信号会流向其他的外设电路,是硬件级的。在EXTI 框图最顶端可以看到,其外设接口时钟是由PCLK2,即APB2 提供,所以在后面使能EXTI 时钟的时候一定要注意。

外部中断/事件线映射
STM32F10x 的EXTI 具有20 个中断/事件线,对应连接的外设说明如下表所示:
在这里插入图片描述
从上表可知,STM32F10x 的EXTI 供外部IO 口使用的中断线有16 根,但是我们使用的STM32F103 芯片却远远不止16 个IO 口, 那么STM32F103 芯片怎么解决这个问题的呢?因为STM32F103 芯片每个GPIO 端口(GPIOx)均有16 个管脚,因此把每个端口的16 个IO 对应那16 根中断线EXTI0-EXTI15 。
比如:
GPIOx.0-GPIOx.15(x=A,B,C,D,E,F,G)分别对应中断线EXTI0-EXTI15,这样一来每个中断线就对应了最多7 个IO 口,比如:GPIOA.0、GPIOB.0、GPIOC.0、GPIOD.0、GPIOE.0、GPIOF.0、GPIOG.0。但是中断线每次只能连接一个在IO 口上,这样就需要通过AFIO 的外部中断配置寄存器1 的EXTIx[3:0]位来决定对应的中断线映射到哪个GPIO 端口上,对于中断线映射到GPIO 端口上的配置函数在stm32f10x_gpio.c 和stm32f10x_gpio.h 中,所以使用到外部中断时要把这个文件加入到工程中。EXTI 的GPIO 映射图如图所示:
在这里插入图片描述
EXTI 配置步骤
接下来我们介绍下如何使用库函数对外部中断进行配置。这个也是在编写程序中必须要了解的。具体步骤如下:(EXTI 相关库函数在stm32f10x_exti.c 和stm32f10x_exti.h 文件中)
(1)使能IO 口时钟,配置IO 口模式为输入
由于本章使用开发板上4 个按键IO 口作为外部中断输入线,因此需要使能对应的IO 口时钟及配置IO 口模式,在按键实验章节中,我们就介绍过要把对应IO 口设置为输入模式,这部分配置与按键实验一样。
(2)开启AFIO 时钟,设置IO 口与中断线的映射关系
接下来我们需要将GPIO 映射到对应的中断线上,只要使用到外部中断,就必须先使能AFIO 时钟,前面已经说了它是挂接在APB2 总线上的,所以使能AFIO时钟库函数为:

RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);

然后,我们就可以把GPIO 映射到对应的中断线上,配置GPIO 与中断线映射的库函数如下:

void GPIO_EXTILineConfig(uint8_t GPIO_PortSource, uint8_tGPIO_PinSource);

比如我们将中断线0 映射到GPIOA 端口,那么就需要如下配置:

GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);

这时候GPIOA.0 管脚就与中断线0 连接起来,其他端口中断线的映射类似。
(3)配置中断分组(NVIC),使能中断我们知道EXTI 产生中断线路最终是流向NVIC 控制器的,由NVIC 调用中断服务函数,因此我们需要对NVIC 进行配置。NVIC 的配置在上一章介绍STM32 中断的时候已经讲过,这里就不重复,不清楚的朋友可以回过头看下。配置NVIC范例如下:

NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;//EXTI0 中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=2;//抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority =3; // 子优先级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ 通道使能
NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化VIC 寄存器

(4)初始化EXTI,选择触发方式
配置好NVIC 后,我们还需要对中断线上的中断初始化,EXTI 初始化库函数,如下:

void EXTI_Init(EXTI_InitTypeDef* EXTI_InitStruct);

函数形参是有一个结构体EXTI_InitTypeDef 类型的指针变量,EXTI_InitTypeDef 结构体成员变量如下:

typedef struct
{
uint32_t EXTI_Line; //中断/事件线
EXTIMode_TypeDef EXTI_Mode; //EXTI 模式
EXTITrigger_TypeDef EXTI_Trigger; //EXTI 触发方式
FunctionalState EXTI_LineCmd; //中断线使能或失能
}EXTI_InitTypeDef;

下面就来介绍下结构体内成员的意义:
EXTI_Line:EXTI 中断/事件线选择,可配置参数为EXTI0-EXTI20,可参考上表。
EXTI_Mode:EXTI 模式选择,可以配置为中断模式EXTI_Mode_Interrupt 和事件模式EXTI_Mode_Event。
EXTI_Trigger : 触发方式选择, 可以配置为上升沿触发EXTI_Trigger_Rising、下降沿触发EXTI_Trigger_Falling、上升沿和下降沿触发EXTI_Trigger_Rising_Falling。
EXTI_LineCmd:中断线使能或者失能,配置ENABLE 为使能,DISABLE 为失能,我们这里要使用外部中断,所以需使能。
(5)编写EXTI 中断服务函数
所有中断函数都在STM32F1 启动文件中,不知道中断函数名的可以打开启动文件查找。这里我们使用到的是外部中断,其函数名如下:
EXTI0_IRQHandler
EXTI1_IRQHandler
EXTI2_IRQHandler
EXTI3_IRQHandler
EXTI4_IRQHandler
EXTI9_5_IRQHandler
EXTI15_10_IRQHandler
从函数名可以看到,前面0-4 个中断线都是独立的函数,中断线5-9 共用一个函数EXTI9_5_IRQHandler , 中断线10-15 也共用一个函数EXTI15_10_IRQHandler,所以要在编写对应中断服务函数时要注意。

硬件设计

硬件电路依然使用开发板上的4 个按键,其电路如图所示:
在这里插入图片描述
在这里插入图片描述
当按键按下时,对应的IO 口电平会发生变化,这时只要配置好对应端口的外部中断触发方式就可以触发中断。

软件设计

要实现外部中断方式控制LED,程序框架如下:
(1)初始化对应端口的EXTI
(2)编写EXTI 中断函数
(3)编写主函数
在前面介绍EXTI 配置步骤时,就已经讲解如何初始化EXTI。

EXTI 初始化函数
要使用外部中断,我们必须先对它进行配置。EXTI 初始化代码如下:

/****************************************************************
***************
* 函数名: My_EXTI_Init
* 函数功能: 外部中断初始化
* 输入: 无
* 输出: 无
*****************************************************************
**************/
void My_EXTI_Init(void)
{
NVIC_InitTypeDef NVIC_InitStructure;
EXTI_InitTypeDef EXTI_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOE, GPIO_PinSource2);//选择GPIO 管脚用作外部中断线路
GPIO_EXTILineConfig(GPIO_PortSourceGPIOE, GPIO_PinSource3);//选择GPIO 管脚用作外部中断线路
GPIO_EXTILineConfig(GPIO_PortSourceGPIOE, GPIO_PinSource4);//选择GPIO 管脚用作外部中断线路
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);//选择GPIO 管脚用作外部中断线路

//EXTI0 NVIC 配置
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;//EXTI0 中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=2;// 抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority =3; //子优先级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能
NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化VIC 寄存器

//EXTI2 NVIC 配置
NVIC_InitStructure.NVIC_IRQChannel = EXTI2_IRQn;//EXTI2 中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=2;// 抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority =2; //子优先级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能
NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化VIC 寄存器

//EXTI3 NVIC 配置
NVIC_InitStructure.NVIC_IRQChannel = EXTI3_IRQn;//EXTI3 中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=2;// 抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority =1; //子优先级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能
NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化VIC 寄存器

//EXTI4 NVIC 配置
NVIC_InitStructure.NVIC_IRQChannel = EXTI4_IRQn;//EXTI4 中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=2;// 抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority =0; //子优先级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能
NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化VIC 寄存器

EXTI_InitStructure.EXTI_Line=EXTI_Line0;
EXTI_InitStructure.EXTI_Mode=EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger=EXTI_Trigger_Rising;
EXTI_InitStructure.EXTI_LineCmd=ENABLE;
EXTI_Init(&EXTI_InitStructure);

EXTI_InitStructure.EXTI_Line=EXTI_Line2|EXTI_Line3|EXTI_Line4;
EXTI_InitStructure.EXTI_Mode=EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger=EXTI_Trigger_Falling;
EXTI_InitStructure.EXTI_LineCmd=ENABLE;
EXTI_Init(&EXTI_InitStructure);
}

在My_EXTI_Init()函数中,首先使能AFIO 时钟,并将4 个按键端口映射到对应中断线上,4 个按键连接端口是PA0、PE2、PE3、PE4。然后配置相应的NVIC并使能对应中断通道,由于4 个按键的IO 口占用了4 个中断线,属于不同的中断通道,需要分别对其配置,从NVIC 配置代码中可以看到4 个中断通道的响应优先级为0-3,所以NVIC 被分成了2 组,分组代码放在了主函数,只有一条语句:

NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //中断优先级分组分2 组。

最后就是对EXTI 初始化,通过配置EXTI_InitStructure 结构体成员值实现EXTI 的配置,从代码中可以看到,中断线0(EXTI_Line0)被配置为上升沿触发,中断线2-4 被配置为下降沿触发,这是因为我们的按键K_UP 是高电平有效,而其他3 个按键是低电平有效,这个在按键实验中已经介绍过。其实如果你会配置一个中断线的EXTI,那么其他中断线都是类似的。

EXTI 中断函数

初始化EXTI 后,中断就已经开启了,当任意按键按下后会触发一次中断,这时程序就会进入中断服务函数执行,所以我们还需要编写对应的EXTI 中断函数,这里我们以PA0 管脚的K_UP 按键进行讲解,其他的按键的中断函数类似,具体代码如下:

/****************************************************************
***************
* 函数名: EXTI0_IRQHandler
* 函数功能: 外部中断0 函数
* 输入: 无
* 输出: 无
*****************************************************************
**************/
void EXTI0_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line0)==1)
{
delay_ms(10);
if(K_UP==1)
{
led2=0;
}
}
EXTI_ClearITPendingBit(EXTI_Line0);
}

因为PA0 管脚对应的中断线是EXTI0,所以其中断函数为EXTI0_IRQHandler,这个从名字来看就很好理解。进入中断函数后,为了确保中断是否真的发生,我们还会对其中断标志位状态进行判断,获取EXTI 中断标志位状态函数如下:

EXTI_GetITStatus(EXTI_Line0);

函数参数EXTI_Line0 是所要判断的中断线, 可以为EXTI_Line0-EXTI_Line20。如果EXTI 中断线有中断发生,函数返回SET,否则返回RESET。SET 也可以用1 表示,RESET 可以用0 表示。在结束中断服务函数前,我们还会清除中断标志位,函数如下:

EXTI_ClearITPendingBit(EXTI_Line0);

在库函数内,还提供了两个函数用来判断外部中断状态以及清除外部状态标志位的函数EXTI_GetFlagStatus 和EXTI_ClearFlag,他们的作用和前面两个函数的作用类似。只是在EXTI_GetITStatus 函数中会先判断这种中断是否使能,使能了才去判断中断标志位,而EXTI_GetFlagStatus 直接用来判断状态标志位。在中断函数内,还调用了delay_ms 函数,用于软件消抖,如果K_UP 确实按下了,就点亮D2 指示灯。

主函数
编写好EXTI 初始化和中断服务函数后,接下来就可以编写主函数了,代码如下:

/****************************************************************
***************
* 函数名: main
* 函数功能: 主函数
* 输入: 无
* 输出: 无
*****************************************************************
**************/
int main()
{
u8 i;
SysTick_Init(72);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //中断优先级分组分2 组
LED_Init();
KEY_Init();
My_EXTI_Init(); //外部中断初始化
while(1)
{
i++;
if(i%20==0)
{
led1=!led1;
}
delay_ms(10);
}
}

主函数实现的功能很简单, 首先对NVIC 进行分组, 这里我们调用NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2)函数,将NVIC 分为2 组即抢占优先级和响应优先级都占2 位,本套教程所有中断分组都采用这种设置,后面就不做重复。再对使用到的硬件端口时钟和IO 口初始化,然后调用我们前面编写的EXTI 的初始化函数,最后进入while 循环语句,不断让D1 指示间隔200ms
闪烁。有的朋友就会问,在主函数中怎么没有看到按键对LED 的控制呢?因为我们在My_EXTI_Init()函数内就已经把按键管脚映射到中断线上,并配置了相应的触发方式,当有按键按下,即会进入对应中断服务函数执行相应的功能程序,LED的控制就在中断函数内完成的。


注意:本文归作者所有,未经作者允许,不得转载

全部评论: 0

    我有话说: