STM32入门教程(EXTI外部中断篇)

重要的内容写在前面:

  1. 该系列是以up主江协科技的STM32视频教程为基础写下去的,大部分内容都参考了老师的课件,对于一些个人认为比较重要但是老师仅口述的部分,笔者都有用文字的方式记录并标出了重点。
  2. 文中的图片基本都来源于老师的课件以及开发板和芯片的手册,粘贴过来是为了方便阅读。
  3. 如果有条件的可以先学习一些相关课程再去看STM32的教程,学起来会更加轻松(不太建议零基础开始直接STM32,听起来可能会有点困难,可以先学51单片机),相关课程有数字电路(强烈推荐先学数电,不然可能会有很多地方理解起来很困难)、模拟电路、计算机组成原理(像寄存器、存储器、中断等在这门课里有很详细的介绍)、计算机网络等。
  4. 如有错漏欢迎指出。

视频链接:[5-1] EXTI外部中断_哔哩哔哩_bilibili

一、中断系统概述

1、中断的一些概念

(1)中断:在主程序运行过程中,出现了特定的中断触发条件(中断源),使得CPU暂停当前正在运行的程序,转而去处理中断程序,处理完成后又返回原来被暂停的位置继续运行

(2)中断优先级:当有多个中断源同时申请中断时,CPU会根据中断源的轻重缓急进行裁决,优先响应更加紧急的中断源

(3)中断嵌套:当一个中断程序正在运行时,又有新的更高优先级的中断源申请中断,CPU再次暂停当前中断程序,转而去处理新的中断程序,处理完成后依次进行返回。

2、中断执行流程

二、STM32的中断

1、STM32的中断管理

(1)STM32中有68个可屏蔽中断通道,包含EXTI、TIM、ADC、USART、SPI、I2C、RTC等多个外设。

(2)使用NVIC统一管理中断,每个中断通道都拥有16个可编程的优先等级,可对优先级进行分组,进一步设置抢占优先级和响应优先级。(NVIC就相当于一个排队系统,根据各种中断的轻重缓急给它们排队)

2、NVIC的基本结构

3、NVIC优先级分组

(1)NVIC的中断优先级由优先级寄存器的4位(0~15)决定,这4位可以进行切分,分为高n位的抢占优先级和低4-n位的响应优先级(n的取值由用户在程序中进行选择)。

(2)抢占优先级高的可以中断嵌套(低优先级中断在占用CPU时,抢占优先级高的中断可以抢占低优先级中断的CPU,让低优先级中断暂停执行),响应优先级高的可以优先排队抢占优先级和响应优先级均相同的按中断号排队

分组方式(由程序选择)

抢占优先级

响应优先级

分组0

0位,取值为0

4位,取值为0~15

分组1

1位,取值为0~1

3位,取值为0~7

分组2

2位,取值为0~3

2位,取值为0~3

分组3

3位,取值为0~7

1位,取值为0~1

分组4

4位,取值为0~15

0位,取值为0

4、EXTI简介

(1)EXTI(Extern Interrupt)——外部中断,EXTI可以监测指定GPIO口的电平信号,当其指定的GPIO口产生电平变化时,EXTI将立即向NVIC发出中断申请,经过NVIC裁决后即可中断CPU主程序,使CPU执行EXTI对应的中断程序

(2)支持的触发方式:上升沿/下降沿/双边沿/软件触发。

(3)支持的GPIO口:所有GPIO口,但相同的Pin不能同时触发中断(比如PA0和PB0不能同时使用,不过PA0和PB1可以同时使用)。

(4)通道数:16个GPIO_Pin,外加PVD输出、RTC闹钟、USB唤醒、以太网唤醒。

(5)触发响应方式:

①中断响应:当外部中断检测到引脚电平变化时,申请中断,让CPU执行中断函数

②事件响应:当外部中断检测到引脚电平变化时,外部中断的信号不会通向CPU,而是通向其它外设,用来触发其它外设的操作。

5、EXTI基本结构

(1)每个GPIO外设有16个引脚,所以各有16根线连接AFIO(AFIO就是一个数据选择器),而EXTI模块只有16个GPIO_Pin的输入线,如果每个引脚都直接接到EXTI上是显然不可能的,所以二者之间有一个AFIO中断引脚选择的电路模块,它可以在前面GPIO外设的引脚中选16个不同号的引脚连接到后面EXTI的输入线上(PA0、PB0、PC0……中只能选择其中一个引脚接到EXTI的0号输入线,PA1、PB1、PC1……中只能选择其中一个引脚接到EXTI的1号输入线,以此类推)。(EXTI还有4条输入线分别连接PVD、RTC、USB、ETH)

(2)外部中断5-9的NVIC输入线被分配到同一个通道,也就是说当外部中断5-9中的其中一个发生时,会执行同一个中断函数,外部中断10-15同理

(3)除了通往NVIC的通道,EXTI还有20条输出线连接到了其它外设,它们均用于事件响应。

6、AFIO复用IO口

(1)AFIO主要用于引脚复用功能的选择和重定义。

(2)在STM32中,AFIO主要完成两个任务:复用功能引脚重映射、中断引脚选择。

(3)本节的重点为中断引脚选择,可以看到AFIO中有16个数据选择器,以0号数据选择器为例,它负责选择PA0、PB0……PG0中的其中一个引脚,在写程序时如果想选择PB0作为中断引脚,就需要对AFIO的0号数据选择器进行配置,让它选择GPIOB的0号引脚

7、EXTI框图

(1)如下图所示,右下角其实是20根输入线,输入线首先进入边沿检测电路,由上面的上升沿触发选择寄存器和下降沿触发选择寄存器选择是上升沿触发还是下降沿触发,亦或是双边沿触发,接着触发信号进入到或门的输入端,该或门的输入端同时还连接软件中断事件寄存器,该寄存器决定软件触发

(2)触发信号通过或门以后有两个去向,第一个去向是触发中断响应,第二个去向是触发事件响应。触发中断首先会将请求挂起寄存器的相应位置为1,这相当于一个中断标志位,用户可以通过读取这个寄存器的内容来判断哪条线路触发了中断,接着如果请求挂起寄存器的相应位被置为1,那么触发信号会从该寄存器的相应线路输出到NVIC中断控制器前的与门,这时是否能产生中断由中断屏蔽寄存器的相应位决定,如果中断屏蔽寄存器的相应位为1,说明该线路的中断未被屏蔽,信号通过该线路的与门前往NVIC中断控制器

8、需要使用外部中断的模块的特点

        STM32获取的信号是外部驱动产生的很快的突发信号(转瞬即逝的信号,随时都可能出现的信号),不过当没有信号时,STM32不需要对其进行任何处理。(51单片机教程中的红外遥控篇使用的也是外部中断;对于按键模块,虽然它的信号也是不定时产生,但是它不使用外部中断,因为其中的死循环代码会对程序造成极大的影响,该模块一般使用定时器中断进行处理

三、示例程序

1、对射式红外传感器计次

(1)下图所示的就是对射式红外传感器,给它进行通电后它的指示灯会亮,当使用不透光的遮挡物插在左边两个“黑块块”中间时指示灯会熄灭,两种状态分别对应低电平和高电平,该数字信号通过DO口传送到STM32的引脚上。

(2)按照下图所示接好线路,并将使用OLED屏进行显示的工程文件夹作为模板复制一份使用

(3)在项目的Hardware组中添加CountSensor.h文件和CountSensor.c文件用于封装对射式红外传感器模块的代码。

①CountSensor.h文件:

#ifndef __CountSensor_H
#define __CountSensor_H

void CountSensor_Init(void);
uint16_t CountSensor_Get(void);

#endif

②CountSensor.c文件:

#include "stm32f10x.h"                  // Device header

uint16_t CountSensor_Count = 0;    //记录挡光次数的变量

void CountSensor_Init(void)
{
	//传感器的DO口接在PB14上,需要使能GPIOB的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	//使能AFIO的时钟(中断引脚选择需要AFIO)
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
	//NVIC(内核中的外设)和EXTI的时钟一直是开着的,不需要程序打开
	
	//配置GPIO端口模式
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;  //手册建议EXTI的输入线(本例是PB14)可以配置为上拉输入
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB,&GPIO_InitStructure);
	
	//配置AFIO的14号数据选择器选择GPIOB的PB14
	GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource14);
	
	//配置EXTI
	EXTI_InitTypeDef EXTI_InitStructure;
	EXTI_InitStructure.EXTI_Line = EXTI_Line14;              //PB14对应14号输入线
	EXTI_InitStructure.EXTI_LineCmd = ENABLE;                //启动中断
	EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;      //配置为中断响应(与之对应的是事件响应)
	EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;  //下降沿触发
	EXTI_Init(&EXTI_InitStructure);
	
	//配置NVIC
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);      //分组方式2(2位抢占优先级,2位响应优先级)
	//在中断种数较少时,分组方式可以随意
	
	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn;      //10-15号输入线被合并到同一个通道中
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;           //开启中断
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //抢占优先级
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;        //响应优先级
	NVIC_Init(&NVIC_InitStructure);
}

uint16_t CountSensor_Get(void)
{
	return CountSensor_Count;  //返回挡光次数
}

void EXTI15_10_IRQHandler(void)  //中断函数(不用在头文件声明)
{
	if(EXTI_GetITStatus(EXTI_Line14) == SET)  //判断这个中断是不是由14号线触发
	{
		CountSensor_Count++;  //挡光次数+1
		EXTI_ClearITPendingBit(EXTI_Line14);  //清除14号线路的中断标志位
	}
}

        手册相关部分:

(4)在stm32f10x_gpio.h文件中除了GPIO相关的函数外还有AFIO的函数,比较重要的几个函数为下图红框框选的函数。

[1]GPIO_AFIODeInit函数:将AFIO外设复位,调用该函数后AFIO的配置全部被清除。

[2]GPIO_EventOutputConfig函数:用于配置AFIO的事件输出功能。

[3]GPIO_EventOutputCmd函数:用于配置AFIO的事件输出功能。

[4]GPIO_PinRemapConfig函数:用于进行引脚重映射,第一个参数为重映射方式,第二个参数为重映射后的状态。

[5]GPIO_EXTILineConfig函数:用于配置AFIO的数据选择器,选择中断引脚。

        本节重点需要使用GPIO_EXTILineConfig函数函数,其函数参数如下图所示,红框内为用户可选择的参数值,其中的x可选A~G/0~15。

(5)stm32f10x_exti.h文件的底部有关于exti的函数声明。

[1]EXTI_DeInit函数:清除EXTI的配置,恢复为上电时的默认状态。

[2]EXTI_Init函数:根据函数参数中结构体指针指向的结构体中的参数对EXTI进行配置。

[3]EXTI_StructInit函数:给函数参数中结构体指针指向的结构体赋一个默认值。

[4]EXTI_GenerateSWInterrupt函数:与软件触发中断有关,参数给出一条指定的中断线(见EXTI框图,有20条输入线,参数为0-19),调用该函数时软件触发一次外部中断,执行的中断函数为指定输入线对应的中断函数。

[5]EXTI_GetFlagStatus函数:在主程序中获取指定线路的标志位是否被置1。

[6]EXTI_ClearFlag函数:在主程序中对指定线路的标志位进行清除(置0)。

[7]EXTI_GetITStatus函数:在中断函数中获取指定线路的中断标志位是否被置1。

[8]EXTI_ClearITPendingBit函数:在中断函数中对指定线路的中断挂起标志位进行清除(置0)。

(6)NVIC的相关函数的声明在misc.h的底部。

[1]NVIC_PriorityGroupConfig函数:用于中断分组,参数为中断分组的方式(共5种)。

[2]NVIC_Init函数:根据结构体中指定的参数初始化NVIC。

[3]NVIC_SetVectorTable函数:设置中断向量表。

[4]NVIC_SystemLPConfig函数:系统低功耗配置。

        在调用NVIC_Init函数时,需要创建一个结构体变量,但是寻找其中可供用户选择的参数时可能需要到其它文件中查找,这时Ctrl+F仍然适用,不过要将搜索范围改成整个工程。(一般“@ref”后跟着的就是用于搜索的索引

        对于中断通道的选择,本教程使用的是MD,所以展开#ifdef STM32F10X_MD进行选择即可。

        对于抢占优先级和响应优先级的选择,在NVIC_InitTypeDef结构体定义的下方有说明,在中断种数较少的情况下设定可以稍微随意一点。

(7)中断系统配置好后需要有中断函数,中断函数具体如何命名(与51单片机不同,STM32中的中断函数需严格按照规定命名),可以在Start组中的.s文件中找,本例选择的是EXTI14号输入线(10-15号输入线合并在同一个通道)的中断,所以中断函数名为EXTI15_10_IRQHandler。(中断函数全部都是无参数无返回值;进入中断函数后需要判断该通道对应的中断标志位是否被置1,如果置1则执行中断函数,并将该通道对应的标志位置为0,否则该通道对应的中断函数会反复执行同名的中断函数最多只能有1个

(8)在main.c文件中添加如下代码,然后编译,将程序下载到开发板中进行测试。

#include "stm32f10x.h"                  // Device headerCmd
#include "OLED.h"
#include "CountSensor.h"

int main()
{
	OLED_Init();
	CountSensor_Init();
	
	OLED_ShowString(1,3,"Count:"); 
	
	while(1)
	{
		OLED_ShowNum(2,3,CountSensor_Get(),3);  //显示挡光次数
	}
}

(9)程序解释:

        PB14被程序配置为中断引脚。当传感器没有检测到有遮挡物时,DO口输出低电平,这时如果使用遮挡物进行遮挡,DO口输出高电平,也就是DO口产生了一个上升沿,不过程序中配置的是下降沿触发,所以这时还不会产生外部中断;当拿开遮挡物后,DO口从高电平变为低电平,产生一个下降沿,而DO口连接PB14,也就是说PB14接收一个下降沿,能够触发一次外部中断,该线路对应的中断标志位被置为1,接着程序进入相应的中断函数,确认该线路中断标志位为1(因为14号线是和其它几条线路合并成一条通道进入同一个中断函数,所以需要判断这个中断是不是由14号线触发),执行中断函数的内容——挡光次数+1,然后清除中断标志位,结束中断函数

        回到main函数中,主函数调用CountSensor_Get函数返回挡光次数并将它显示在OLED屏上。

2、旋转编码器计次

(1)旋转编码器是用来测量位置、速度或旋转方向的装置,当其旋转轴旋转时,其两个输出端可以输出与旋转速度和方向对应的方波信号,读取方波信号的频率和相位信息即可得知旋转轴的速度和方向

①旋转编码器的类型有机械触点式、霍尔传感器式和光栅式三种。

②下图中的“○”对应旋转编码器的旋转轴,当旋转轴旋转时,其左右两个触点会以相位相差90°的方式交替导通。以左边的A侧为例,左边接了一个10k的上拉电阻(连接VCC),当左边的触点没有导通时A点被上拉为高电平,当左边的触点导通时A点能直接连接GND,由于上拉电阻的作用,这时VCC不会将A点置为高电平,于是A点为低电平(电容的作用是过滤因抖动产生的电流;R3是一个输出限流电阻,防止模块中引脚电流过大)。

(2)按照下图所示接好线路,并将使用OLED屏进行显示的工程文件夹作为模板复制一份使用

(3)在项目的Hardware组中添加Encoder.h文件和Encoder.c文件用于封装旋转编码器模块的代码。

①Encoder.h文件:

#ifndef __Encoder_H
#define __Encoder_H

void Encoder_Init(void);
int16_t Encoder_Count_Get(void);

#endif

②Encoder.c文件:

#include "stm32f10x.h"                  // Device header

int16_t Encoder_Count = 0;  //记录旋转方向及深度变化

void Encoder_Init(void)
{
	//旋转编码器的A口和B口分别接在PB0和PB1上,需要使能GPIOB的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	//使能AFIO的时钟(中断引脚选择需要AFIO)
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
	//NVIC(内核中的外设)和EXTI的时钟一直是开着的,不需要程序打开
	
	//配置GPIO端口模式
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB,&GPIO_InitStructure);
	
	//配置AFIO的0号数据选择器选择GPIOB的PB0
	GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource0);
	//配置AFIO的1号数据选择器选择GPIOB的PB1
	GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource1);
	
	//配置EXTI
	EXTI_InitTypeDef EXTI_InitStructure;
	EXTI_InitStructure.EXTI_Line = EXTI_Line0 | EXTI_Line1;  //同时配置两条输入线
	EXTI_InitStructure.EXTI_LineCmd = ENABLE;                //启动中断
	EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;      //配置为中断响应(与之对应的是事件响应)
	EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;  //下降沿触发
	EXTI_Init(&EXTI_InitStructure);
	
	//配置NVIC
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);      //分组方式2(2位抢占优先级,2位响应优先级)
	//在中断种数较少时,分组方式可以随意
	
	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;          //0号输入线
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;           //开启中断
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //抢占优先级
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;        //响应优先级
	NVIC_Init(&NVIC_InitStructure);
	
	NVIC_InitStructure.NVIC_IRQChannel = EXTI1_IRQn;          //1号输入线
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;           //开启中断
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //抢占优先级
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;        //响应优先级
	NVIC_Init(&NVIC_InitStructure);
}

int16_t Encoder_Count_Get(void)
{
	int16_t Temp = Encoder_Count;
	Encoder_Count = 0;
	return Temp;  //返回旋转深度变化量
}

void EXTI0_IRQHandler(void)
{
	if(EXTI_GetITStatus(EXTI_Line0) == SET)  //判断0号线路的中断标志位是否被置1
	{
		if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0)  //PB0下降沿触发外部中断,读取另一侧的电平(判断正转)
		{
			Encoder_Count++;  //深度变化量+1(正转)
		}
		EXTI_ClearITPendingBit(EXTI_Line0);  //清除0号线路的中断标志位
	}
}

void EXTI1_IRQHandler(void)
{
	if(EXTI_GetITStatus(EXTI_Line1) == SET)  //判断1号线路的中断标志位是否被置1
	{
		if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == 0)  //PB1下降沿触发外部中断,读取另一侧的电平(判断反转)
		{
			Encoder_Count--;  //深度变化量-1(反转)
		}
		EXTI_ClearITPendingBit(EXTI_Line1);  //清除1号线路的中断标志位
	}
}

(4)在main.c文件中粘贴以下代码,然后进行编译,将程序下载到开发板中。

#include "stm32f10x.h"                  // Device headerCmd
#include "OLED.h"
#include "Encoder.h"

int8_t Num = 0;  //记录旋转深度

int main()
{
	OLED_Init();
	Encoder_Init();
	
	OLED_ShowString(1,1,"Num:"); 
	
	while(1)
	{
		Num += Encoder_Count_Get();    //修改当前旋转深度
		OLED_ShowSignedNum(1,5,Num,5);
	}
}

(5)旋转旋转编码器的旋转轴,可以看到OLED屏上会显示旋转深度和方向。

(6)程序解释:

每转动一次旋转轴,两个中断函数都会触发一次EXTI0_IRQHandler函数用于判断正转,当PB0(A侧)下降沿触发中断时,如果是正转,那么此时B侧应为低电平,正转深度+1,如果是反转,此时B侧为高电平,旋转深度不变;EXTI1_IRQHandler函数用于判断反转,当PB1(B侧)下降沿触发中断时,如果是反转,那么此时A侧应为低电平,反转深度+1,如果是正转,此时A侧为高电平,旋转深度不变。

正反转方向不固定,由程序员决定,这里描述的下图是左边正转、右边反转每次旋转总有且只有一个中断函数会改变旋转深度

  • 24
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Zevalin爱灰灰

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

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

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

打赏作者

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

抵扣说明:

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

余额充值