stm32—中断

1. 引入

通过按键KEY去控制LED灯:

        需要CPU不停的去读取按键相应的GPIO口的电平状态

                高电平(1) ---> 弹起

                低电平(0) ---> 按下

        然后再根据GPIO引脚的电平状态,来判断按键的按下或弹起的状态,"通知" CPU,然后再去做相应的处理......

大概的代码就是:

while (1) {

    if (第一个按键按下) {
        LED1灯亮
    } else {
        LED1灯灭
    }
    
    ......

    if (第四个按键按下) {
        LED4灯亮
    } else {
        LED4灯灭
    }
}

因为需要一直不停的去判断按键对应的GPIO的电平状态,所以需要加上一个while(1),这种CPU的工作方式就称之为 "轮询"

"轮询":轮流询问,但是轮询有一些缺陷:

        (1) 浪费CPU

        (2) 占用总线,Bus is always busy

        (3) 轮询有一个时间差,事件响应不及时

有没有办法不让CPU主动去询问事件是否发生,而是当事件发生后,主动通知CPU呢?

        ===> 中断

2. 中断的概念

一般中断定义为打断CPU指令正常执行顺序的事件,转而处理紧急程序,然后返回原暂停的程序继续运行

当外部事件或内部异常需要被及时处理时,可以利用中断机制打断CPU的正常执行(Thread Mode),转而去执行事件处理(Handle Mode)

        中断其实就是打断CPU执行顺序,转而去执行另外的中断服务程序

        中断机制的出现主要是为了能够及时处理一些紧急的事件以及能够满足实时处理要求等

怎么确保中断被及时响应呢?

        为了尽量节省中断响应的时间,它事先为每一个中断事件设置一个编号,并且不同的中断的中断处理程序的地址,也要事先指定好(就如同现实中找东西,我们每次把东西放在一个固定的位置能很快找到)

        所以当产生了一个特定的中断事件时,CPU就可以以最快的速度到指定的地址去执行中断处理程序

        那么每一个中断处理程序所在的地址,都是根据中断编号的顺序存储在一个数组(函数指针数组)中的,这个数组就是我们的中断向量表


现代CPU架构为了能够及时响应外部或内部的一些紧急事件,都支持中断,并且会提供相应的中断响应机制

        ===> 中断机制

3. ARM Cortex-M4 中断机制

stm32的 嵌入向量中断控制器(NVIC) 管理着包括 Cortex_M4 内核异常等中断,其和 ARM 处理器核的接口紧密相连,可以实现 低延时 的中断处理,并有效地处理  晚到 中断  


当 嵌入向量中断控制器(NVIC) 通知CPU产生了某个事件,此时,CPU就会停止正在做的事情,转到处理模式去处理这个事件
而且M4给不同的中断一个唯一的编号(中断编号:用来区分不同的中断事件)

当不同的中断事件产生时,CPU会做不同的处理


CPU怎么根据不同的中断事件去做不同的处理呢?

        根据中断向量表


中断向量表是什么?

        就是一个数组,保存不同的中断事件处理函数的地址的数组
        所以中断向量表实际上就是一个函数指针数组

中断编号:实际上就是该数组的下标

        所以当某个事件发生时,中断产生,那么CPU就会根据此中断对应事件编号将其作为下标去调用该下标对应的那个中断处理函数

中断处理函数的格式一般为:

        void handler_func(void)

        {

        }

我们可以发现中断处理函数实际上没有返回值也不需要参数,why???

        首先我们要明白中断函数与普通函数的区别:

                普通函数:用户主动调用的,所以用户在调用时可以准备好参数,当然也可以解析返回值

                中断函数:CPU被动调用,不是用户主动调用的。是当有紧急事件发生时,CPU被动去执行中断函数。它可以在用户指令的任何时刻去调用它,因为中断在任何时刻都可能产生

        对于我们用户来讲,我们并不知道什么时候去调用中断函数。那么我们连什么时候调用都不知道,所以更无从去准备参数以及解析返回值

4. 中断向量表

不同的中断事件对应不同的中断函数,中断向量表保存了各个中断处理程序(函数)地址的数组

        比如:

                中断事件 0 --->  xxx_isr_0
                ......

                中断事件 x --->  xxx_isr_x

                ......

                中断事件 n --->  xxx_isr_n


中断函数的命名由用户指定,不是CPU指定。CPU负责的是将所有中断函数的地址放到一块连续的、固定的地址上面去。中断服务函数通常编写在 stm32f4xx_it.c 文件中,这是软件级的中断处理方式

中断向量表的实现:

typedef void (*pFunc_t) (void);
pFunc_t vectors[256]; // 最大256,因为 xPSR 只有八位用来保存中断编号
vectors[0] = xxx_isr_0;
...
vectors[x] = xxx_isr_x;
...
vectors[n] = xxx_isr_n;

当事件产生时,中断控制器(NVIC)就会打断CPU的执行,同时告诉CPU是x(中断的编号)号中断产生了,那么CPU会立即停止正在做的事情,转去执行vector[x]

pFunc_t isr = vectors[x];

isr();  or  (*isr)(); // 调用中断x的中断处理函数

实际上中断向量表一般是由汇编完成:

__Vectors
	DCD  xxx_isr_0
    ...
    DCD  xxx_isr_x
	...
	DCD  xxx_isr_n

为了让CPU能够更快的响应中断,M4约定将中断向量表存放在一个固定的位置,也就是存储器地址为0x0800 0000处(通过映射会将其映射到0x0000 0000处)  ---> RESET段
在STM32F4xx的固件库中,
startup_stm32f40xx.s 启动文件中早就定义好了中断向量表

产生中断后,CPU的执行过程如下(伪代码):
产生中断后,会产生一个中断编号,这个编号就保存在xPSR[7:0]中。接着:
MRS	R0, IPSR					;将中断编号传给R0
MOV 	R1, #0x00000000		    ;中断向量表基址
LDR 	R2, [R1, R0, LSL #2]	;R2 <- R1+(R0<<2)  加载中断处理函数入口地址
MOV  	PC, R2					;跳转到中断处理函数地址执行


比如:
在STM32F4xx的固件库中,startup_stm32f40xx.s 启动文件中早就定义好了中断向量表:

; Vector Table Mapped to Address 0 at Reset	// 重置时将中断向量表映射到地址为0处
        AREA    RESET, DATA, READONLY
        EXPORT  __Vectors
        EXPORT  __Vectors_End
       	EXPORT  __Vectors_Size
__Vectors		DCD     __initial_sp           ; Top of Stack
          		DCD     Reset_Handler          ; Reset Handler
                DCD     NMI_Handler            ; NMI Handler
                ......

__Vectors就是STM32F4xx的中断向量表
每一个DCD后面,对应的就是一个个的中断处理函数名(中断处理函数入口地址)

详情请见<STM32F4xx中断向量表>

5. STM32F4xx 的中断管理机制


任何中断的产生到CPU的响应,都要经过以下阶段:

  • 中断源阶段

        中断源是指产生中断的设备

        设备要想能够产生中断,就必须要有一根中断请求线(IRQ Line)

        并且这根中断请求线必须连接到中断控制器(NVIC)的中断输入引脚上

  • 中断控制器(NVIC)阶段

        中断控制器是对所有中断输入引脚进行管理和控制
        可以根据输入的中断请求给 CPU 内核一个中断信号,通知CPU某某设备产生了中断。外部硬件在通过 INTR 发送中断请求信号时,还要向CPU给出一个8位的中断编号。CPU在响应这个中断请求时,同时读取到了这个由外部硬件给出的中断编号,然后以这个中断编号为下标就会去对应的中断向量表中找到对应的元素,将元素中的值(地址)取出来后,跳转过去执行

        INTR:interrupt Request 的缩写,表示中断请求信号,当外部设备需要CPU处理某个事件时,可以通过将 INTR 信号置为高电平来请求中断,CPU 检测到 INTR 信号为高电平后,会立即停止当前指令的执行,并跳转到中断处理程序中执行相应的操作


一个设备产生了中断首先要经过"中断源"这一级,而"中断源"可以屏蔽或使能中断。即便外部设备产生了"事件",中断源也可以不向它的上一级中断控制器发起中断请求报告
 

中断控制器(NVIC)它也可以控制中断,当NVIC接收到中断请求后,也可以选择Enable(使能) / Disable(禁止)这个中断,意思是NVIC收到了请求,但是不报告给CPU

在STM32中,中断的种类以及数量较多,我们先从外部中断开始讲起

6. STM32F4xx 外部中断

外部中断(EXTI:EXTernal  Interrupt)是指GPIO的外部电路上产生的中断
        比如:在GPIO口的外部电路上产生一个上升沿(或下降沿)将可能会导致一个外部中断

F407上一共有23个外部中断

        记为:EXTI0、EXTI1、EXTI2、EXTI3......EXTI22

        EXTI0(外部中断0)的产生来源于所有编号为0的GPIO引脚:

                PA0、PB0、PC0......PI0

        EXTI1(外部中断1)的产生来源于所有编号为1的GPIO引脚:

                PA1、PB1、PC1......PI1

        ....

        其余的外部中断的来源情况可以查看:

        <STM32F4xx中文参考手册.pdf>中第243页(10.2.5  外部中断/事件线映射)



所以GPIO外部中断的路线图应该是:

        GPIO外部信号输入(高跳变 / 低跳变)

        --->         GPIO 控制器(此时GPIO控制器应该配置为输入状态)

        --->         SYSCFG 选择器(选择由哪个GPIO产生EXTI)

        --->         EXTI 外部中断控制器(边沿触发选择 / 外内部中断 / 屏蔽和使能)

        --->         NVIC 中断控制器

        --->         CPU 停止正常执行顺序

                ---> 1.     获取NVIC报告的中断编号(中断向量表下标)

                ---> 2.     中断向量表中查找中断处理函数地址

                ---> 3.     执行对应的中断处理函数

7. 外部中断的代码实现

GPIO外部中断的详细配置步骤:

0. main 函数里面指定抢占优先级占多少bits
    
    void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup);
         
        @NVIC_PriorityGroup: 指定抢占优先级占多少bits  0~4
	                 NVIC_PriorityGroup_0	抢占优先级0bits子优先级4bits
	                 NVIC_PriorityGroup_1	抢占优先级1bits子优先级3bits
	                 NVIC_PriorityGroup_2	抢占优先级2bits子优先级2bits
	                 NVIC_PriorityGroup_3	抢占优先级3bits子优先级1bits
	                 NVIC_PriorityGroup_4	抢占优先级4bits子优先级0bits
         
        这个函数一般在main里面调用一次就可以了
            (防止后续分配的优先级超过所占bits范围大小)
                1bit  ---> 0-1
                2bits ---> 0-3
                3bits ---> 0-7

1. 配置GPIO
    (1) 使能GPIO分组时钟
    (2) 配置GPIO为通用输入模式
    
2. 选择GPIO引脚外部中断请求线(配置SYSCFG选择器)
    (1) 使能选择器时钟
    (2) 配置选择器:选择相应的GPIO引脚作为外部中断的输入引脚

3. 配置外部中断控制器(初始化 EXTI 外部中断控制器)
    (1) 指定要配置的外部中断请求线
    (2) 选择电平触发发送
    (3) 选择是中断模式
    (4) 使能中断

4. 初始化配置NVIC中断控制器
    (1) 指定中断输入通道
    (2) 配置抢占优先级和响应优先级
    (3) 使能相应中断通道

在STM32F4xx的固件库中,startup_stm32f40xx.s 启动文件中早就定义好了中断向量表

首先查看原理图:

        KEY0 ---> PA0 ---> EXTI0 ---> NVIC ---> CPU ---> EXTI0_IRQHandler

        KEY1 ---> PE2 ---> EXTI2 ---> NVIC ---> CPU ...

        KEY2 ---> PE3 ---> EXTI3 ...

        KEY4 ---> PE4 ---> EXTI4 ...


根据 GPIO 外部中断的路线图来看,首先要做的是配置 GPIO 控制器:

1)配置 GPIO 控制器为输入模式

        a)使能 GPIO 分组时钟

                RCC_AHB1PeriphClockCmd()

        b)初始化 GPIO 控制器

                GPIO_Init()

2)配置SYSCFG选择器

        选择器SYSCFG也相当于一个外设,因此也需要使能时钟:

        a)使能SYSCFG的时钟(外设SYSCFG处于APB2总线)  ---> stm32f4xx_rcc.c

参考<STM32F4xx中文参考手册.pdf> 2.3 存储器映射可知:SYSCFG 位于总线 APB2 上

void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph, FunctionalState NewState);

如:RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE);

        b)初始化SYSCFG(选择外部中断的输入信号来源) ---> stm32f4xx_syscfg.c

SYSCFG_EXTILineConfig ===> 选择外部中断线

void SYSCFG_EXTILineConfig(uint8_t EXTI_PortSourceGPIOx, 
                                    uint8_t EXTI_PinSourcex);

    @EXTI_PortSourceGPIOx: 选择GPIO分组(x = A,B,C,...,I)
                           EXTI_PortSourceGPIOA
                           EXTI_PortSourceGPIOB
                           EXTI_PortSourceGPIOC
                           ......
                           EXTI_PortSourceGPIOI

    @EXTI_PinSourcex: 选择产生外部中断的引脚(x = 0,1,2,3,...,15)
                           EXTI_PinSource0
                           EXTI_PinSource1
                           EXTI_PinSource2
                           ......
                           EXTI_PinSource15
----------------------------------------------------------------
比如:配置PA0产生外部中断0

SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOA, EXTI_PinSource0);

3)初始化 EXTI 外部中断控制器 ---> stm32f4xx_exti.c

// EXTI也是外设,也需要使能时钟,但是默认就是打开的

EXTI_Init: 用来初始化配置EXTI外部中断控制器
		
void EXTI_Init(EXTI_InitTypeDef *EXTI_InitStruct)
			
@EXTI_InitStruct: 指向EXTI配置的结构体

    结构体原型如下:
        typedef struct 
        {		
            uint32_t EXTI_Line;
                /*
				    指定要初始化的外部中断编号(可以位或多个)
					    EXTI_Line0
					    EXTI_Line1
					    ...
					    EXTI_Line15
                */

	        EXTIMode_TypeDef EXTI_Mode;
				/*
                    指定相应的外部中断的模式(二选一)
					    EXTI_Mode_Interrupt   中断模式
					    EXTI_Mode_Event		  事件模式
                */

		    EXTITrigger_TypeDef EXTI_Trigger; 
				/* 
                    指定外部中断线的边沿触发模式(三选一)
					    EXTI_Trigger_Rising		      
                            在上升沿的时候触发中断
					    EXTI_Trigger_Falling		  
                            在下降沿的时候触发中断
					    EXTI_Trigger_Rising_Falling	  
                            在上升沿和下降沿都触发中断(双边沿触发)
		  	    */

            FunctionalState EXTI_LineCmd;
                /* 
				    使能/禁止相应的外部中断
					    ENABLE
					    DISABLE
                */

		    } EXTI_InitTypeDef;
----------------------------------------------------------------------------
例子:配置按键KEY0(PA0)按下时,产生外部中断
			
/* 定义配置信息结构体变量 */
EXTI_InitTypeDef EXTI_InitStruct;
		
/* 给结构体变量赋值 */
// 指定要初始化的外部中断编号(可以位或多个)
EXTI_InitStruct.EXTI_Line =	EXTI_Line0;
// 指定相应的外部中断的模式(二选一) 中断模式
EXTI_InitStruct.EXTI_Mode =	EXTI_Mode_Interrupt;
// 指定外部中断线的边沿触发模式(三选一)
EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Falling;
// 使能相应的外部中断
EXTI_InitStruct.EXTI_LineCmd = ENABLE;				
			
/* 调用函数完成初始化配置 */
EXTI_Init(&EXTI_InitStruct);
----------------------------------------------------------------------------
需要注意的是:
    外部中断产生后,相应的外部中断状态寄存器(EXTI)中,就会设置"中断标志",
而外部中断状态寄存器中的状态标志,就可以用来判断是否产生了中断,
并且这个中断标志必须经过软件(用户自己写代码)清除

如下函数就是用来获取中断标志和清除中断标志用的:
    a.获取中断状态标志
        FlagStatus EXTI_GetFlagStatus(uint32_t EXTI_Line); // 在中断函数外使用
        ITStatus EXTI_GetITStatus(uint32_t EXTI_Line); // 在中断函数里面使用
		
            @EXTI_Line: 指定外部中断线
			                EXTI_Line0
			                ....
			                EXTI_Line15
		    返回值:
			    SET		表示获取的外部中断已经产生
			    RESET	表示获取的外部中断未产生

    b.清除中断状态标志
        void EXTI_ClearFlag(uint32_t EXTI_Line); // 在中断函数外使用
        void EXTI_ClearITPendingBit(uint32_t EXTI_Line); // 在中断函数里面使用

            @EXTI_Line: 指定外部中断线
						    EXTI_Line0
						    ....
						    EXTI_Line15
    注意:
        在CPU执行完中断处理函数后,就必须调用这些函数来清除中断标志
        否则,CPU会认为您的中断尚未处理,也就不会回到正常状态执行
-----------------------------------------------------------------------------
所以中断函数的格式(以EXTI0的中断为例):
void EXTI0_IRQHandler(void) {

    if (EXTI_GetlTStatus(EXTI_Line0) == SET) { // 产生了中断
        ...... 执行的代码
    }
    EXTI_ClearITPendingBit(EXTI_Line0); // 清除中断标志位
}

4)初始化配置 NVIC 中断控制器  -----> NVIC 管理所有的中断  ---> misc.c

NVIC_Init:用来初始化配置NVIC中断控制器

void NVIC_Init(NVIC_InitTypeDef *NVIC_InitStruct)
		
@NVIC_InitStruct: 指向配置信息结构体
	typedef struct
	{
		uint8_t NVIC_IRQChannel;
            指定中断输入通道(不可以位或多个)
		    STM32固件库中,给每一个中断一个唯一的编号,并且这个编号是用枚举来实现的
                枚举名格式如下:										
                    xxx_IRQn  ---> 这个就是中断通道(中断编号),xxx为外设中断名	
                      如:
                        外部中断0 ---> EXTI0_IRQn
                               1 ---> EXTI1_TRQn
                               2 ---> EXTI2_IRQn
                               3 ---> EXTI3_IRQn
                               4 ---> EXTI4_IRQn
                              5-9 ---> EXTI9_5_IRQn
                            10-15 ---> EXTI15_10_IRQn

					    串口中断1--->USART1_IRQn
                        ......

		uint8_t NVIC_IRQChannelPreemptionPriority;
            指定抢占优先级,数字越小优先级越高,根据所占 bits 给值
                如果抢占优先级占 2 bits,则可以给的值范围为:0-3
                如果抢占优先级占 3 bits,则可以给的值范围为:0-7
		uint8_t NVIC_IRQChannelSubPriority;		 	
            指定子优先级(响应优先级),数字越小优先级越高,根据所占 bits 给值
                如果子优先级占 2 bits,则可以给的值范围为:0-3
                如果子占优先级占 3 bits,则可以给的值范围为:0-7
                
            NVIC允许用户给每一个中断通道分配一个优先级,中断通道的优先级占4bits
				优先级4bits分为两类:
					抢占优先级占 x  bits
					子优先级占   4-x bits

				抢占优先级:
					当中断A产生并且在处理中断A时,如果此时产生了中断B,
并且中断B的抢占优先级高于中断A,那么系统会打断中断A的处理,转去处理中断B,
B处理完后,再继续处理A,A处理完后,才回到CPU正常执行
				
                响应优先级(子优先级):
					当两个或两个以上中断同时产生时,CPU预先处理谁
					如果中断A子优先级高于中断B,那么CPU先处理中断A
                
                x具体占多少,由用户调用函数配置
                void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup);
                
                @NVIC_PriorityGroup: 指定抢占优先级占多少bits  0~4
	                 NVIC_PriorityGroup_0	抢占优先级0bits子优先级4bits
	                 NVIC_PriorityGroup_1	抢占优先级1bits子优先级3bits
	                 NVIC_PriorityGroup_2	抢占优先级2bits子优先级2bits
	                 NVIC_PriorityGroup_3	抢占优先级3bits子优先级1bits
	                 NVIC_PriorityGroup_4	抢占优先级4bits子优先级0bits
                
                这个函数一般在main里面调用一次就可以了
                    (防止后面分配的优先级超过所占bits范围大小)
                        1bit  ---> 0-1
                        2bits ---> 0-3
                        3bits ---> 0-7
			
        FunctionalState NVIC_IRQChannelCmd; 
                使能/禁止相应中断通道
					ENABLE
					DISABLE		NVIC不会向CPU报告该中断

		} NVIC_InitTypeDef;	
----------------------------------------------------------------------------
比如:配置KEY0中断使能
		
NVIC_InitTypeDef NVIC_InitStruct;

NVIC_InitStruct.NVIC_IRQChannel	= EXTI0_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority =	2;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 2;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStruct);

中断优先级是指在一个系统中,不同中断之间的执行顺序和竞争关系
在许多嵌入式系统中,中断优先级通常包括抢占优先级(Preemption Priority)
和响应优先级(Subpriority)

1. 抢占优先级(Preemption Priority,pre) 
    抢占优先级决定了中断在发生时是否可以打断正在执行的其他中断或任务
高抢占优先级的中断可以打断低抢占优先级的中断。在同一抢占优先级下,
响应优先级决定执行的顺序

2. 响应优先级(Subpriority,sub): 
    当两个中断具有相同的抢占优先级时,响应优先级用于决定哪个中断先执行
高响应优先级的中断先执行,但它们不会相互打断。在同一响应优先级下,
自然优先级决定执行的顺序

3. 自然优先级: 
    自然优先级是中断向量表中每个中断对应的优先级 ---> 硬件中断编号(向量表的数组下标越
小,优先级越高),在没有特别设置抢占和响应优先级的情况下,自然优先级越高的中断会先执行

4. 数值越小表示优先级越高: 
    中断优先级通常使用一个数值表示,数值越小表示优先级越高
例如,在STM32中,优先级值为0表示最高优先级,而值越大表示优先级越低

-----------------------------------------------------------------------------

抢占优先级不同,会涉及到中断嵌套,抢占优先级高的会优先抢占优先级低的,优先得到执行
抢占优先级相同,不涉及到中断嵌套,响应优先级不同,响应优先抢高的先响应
抢占优先级和响应优先级都相同,则比较它们的硬件中断编号,中断编号越小,优先级越高

        将上述步骤完成后,就配置好了中断,当配置的那个中断产生,CPU就会自动去调用对应的中断处理函数
        因此,在配置完中断后,开发者应该要编写对应的中断处理函数,以供CPU在产生中断后调用

 在编写中断函数的时候,中断函数名官方已经指定,从汇编文件  startup_stm32f40xx.s 的中断向量表_Vectors中可以查询到。当然实际上也可以修改中断函数名

比如:配置了外部中断0,需要编写外部中断0的处理函数
		
void EXTI0_IRQHandler(void) {
			
    // 这里面就是你对外部中断0的处理
    // 当中断后你想做什么事情,就可以写代码在这里
			
	// 处理完中断后清除中断标志
	EXTI_ClearITPendingBit(EXTI_Line0);
}

8. 练习

将四个按键设置为中断 
        KEY1 --->  LED1 和 LED2 
        KEY2 --->  LED3 和 LED4 
        KEY3 --->  BEEP 
        KEY4 --->  四个灯


exti.h

#ifndef __EXTI_H__
#define __EXTI_H__

#include "stm32f4xx.h"

// 假延时  mydelay(100)近似10ms 
void mydelay(int ms);

/*
	KEY1:PA0
	KEY2:PE2
	KEY3:PE3
	KEY3:PE4
*/

/*
	key1:PA0
	配置PA0作为外部中断EXTI0的输入线
*/
void EXTI0_Init(void);

/*
	KEY2:PE2
	配置PE2作为外部中断EXTI0的输入线
*/
void EXTI2_Init(void);

/*
	KEY3:PE3
	配置PA0作为外部中断EXTI0的输入线
*/
void EXTI3_Init(void);

/*
	KEY3:PE4
	配置PA0作为外部中断EXTI0的输入线
*/
void EXTI4_Init(void);

/* 
	一步到位

    将与按键相关的外部中断进行初始化
		KEY0 --> PA0 --> EXTI0
		KEY1 --> PE2 --> EXTI2
		KEY2 --> PE3 --> EXTI3
		KEY3 --> PE4 --> EXTI4
*/
void KEY_EXTI_Init(void);

#endif

exti.c

#include "exti.h"
#include "led.h"
#include "beep.h"
#include "key.h"

// 假延时  mydelay(100)近似10ms 
void mydelay(int ms) {
	while (ms--) {
		for (int i = 0; i < 0x2000; i++);
	}
}

/*
	用中断的方式来检测按键
    
    0. main 函数里面指定抢占优先级占多少bits

	1. 配置GPIO
		(1) 使能GPIO分组时钟
		(2) 配置GPIO为通用输入模式
    
	2. 选择GPIO引脚外部中断请求线(配置SYSCFG选择器)
		(1) 使能选择器时钟
		(2) 配置选择器:选择相应的GPIO引脚作为外部中断的输入引脚

	3. 配置外部中断控制器(初始化 EXTI 外部中断控制器)
		(1) 指定要配置的外部中断请求线
		(2) 选择电平触发发送
		(3) 选择是中断模式
		(4) 使能中断

	4. 初始化配置NVIC中断控制器
*/

/*
	key1:PA0
	配置PA0作为外部中断EXTI0的输入线
*/
void EXTI0_Init(void) {
	
	// 1. 配置GPIO
	// (1)使能GPIO分组时钟
	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
	// (2)配置GPIO为通用输入模式
	GPIO_InitTypeDef g;
	g.GPIO_Pin = GPIO_Pin_0;
	g.GPIO_Mode = GPIO_Mode_IN;
	g.GPIO_PuPd = GPIO_PuPd_NOPULL;
	GPIO_Init(GPIOA, &g);
	
	// 2. 选择GPIO引脚外部中断请求线(配置SYSCFG选择器)
	// (1) 使能选择器时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE);
	// (2) 配置选择器:选择相应的GPIO引脚作为外部中断的输入引脚
	SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOA, EXTI_PinSource0);
	
	// 3. 配置外部中断控制器(初始化 EXTI 外部中断控制器)
	EXTI_InitTypeDef e;
	e.EXTI_Line = EXTI_Line0;
	e.EXTI_Mode = EXTI_Mode_Interrupt;
	e.EXTI_Trigger = EXTI_Trigger_Falling; // 下降沿触发
	e.EXTI_LineCmd = ENABLE;
	EXTI_Init(&e);
	
	// 4. 初始化配置NVIC中断控制器
	NVIC_InitTypeDef n;
	n.NVIC_IRQChannel = EXTI0_IRQn;
	n.NVIC_IRQChannelPreemptionPriority = 2;
	n.NVIC_IRQChannelSubPriority = 2;
	n.NVIC_IRQChannelCmd = ENABLE;
	NVIC_Init(&n);
}

/*
	KEY2:PE2
	配置PE2作为外部中断EXTI0的输入线
*/
void EXTI2_Init(void) {
	
	// 1. 配置GPIO
	// (1)使能GPIO分组时钟
	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOE, ENABLE);
	// (2)配置GPIO为通用输入模式
	GPIO_InitTypeDef g;
	g.GPIO_Pin = GPIO_Pin_0;
	g.GPIO_Mode = GPIO_Mode_IN;
	g.GPIO_PuPd = GPIO_PuPd_NOPULL;
	GPIO_Init(GPIOA, &g);
	
	// 2. 选择GPIO引脚外部中断请求线(配置SYSCFG选择器)
	// (1) 使能选择器时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE);
	// (2) 配置选择器:选择相应的GPIO引脚作为外部中断的输入引脚
	SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOE, EXTI_PinSource2);
	
	// 3. 配置外部中断控制器(初始化 EXTI 外部中断控制器)
	EXTI_InitTypeDef e;
	e.EXTI_Line = EXTI_Line2;
	e.EXTI_Mode = EXTI_Mode_Interrupt;
	e.EXTI_Trigger = EXTI_Trigger_Falling; // 下降沿触发
	e.EXTI_LineCmd = ENABLE;
	EXTI_Init(&e);
	
	// 4. 初始化配置NVIC中断控制器
	NVIC_InitTypeDef n;
	n.NVIC_IRQChannel = EXTI2_IRQn;
	n.NVIC_IRQChannelPreemptionPriority = 2;
	n.NVIC_IRQChannelSubPriority = 2;
	n.NVIC_IRQChannelCmd = ENABLE;
	NVIC_Init(&n);
}

/*
	KEY3:PE3
	配置PA0作为外部中断EXTI0的输入线
*/
void EXTI3_Init(void) {
	
	// 1. 配置GPIO
	// (1)使能GPIO分组时钟
	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOE, ENABLE);
	// (2)配置GPIO为通用输入模式
	GPIO_InitTypeDef g;
	g.GPIO_Pin = GPIO_Pin_3;
	g.GPIO_Mode = GPIO_Mode_IN;
	g.GPIO_PuPd = GPIO_PuPd_NOPULL;
	GPIO_Init(GPIOA, &g);
	
	// 2. 选择GPIO引脚外部中断请求线(配置SYSCFG选择器)
	// (1) 使能选择器时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE);
	// (2) 配置选择器:选择相应的GPIO引脚作为外部中断的输入引脚
	SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOE, EXTI_PinSource3);
	
	// 3. 配置外部中断控制器(初始化 EXTI 外部中断控制器)
	EXTI_InitTypeDef e;
	e.EXTI_Line = EXTI_Line3;
	e.EXTI_Mode = EXTI_Mode_Interrupt;
	e.EXTI_Trigger = EXTI_Trigger_Falling; // 下降沿触发
	e.EXTI_LineCmd = ENABLE;
	EXTI_Init(&e);
	
	// 4. 初始化配置NVIC中断控制器
	NVIC_InitTypeDef n;
	n.NVIC_IRQChannel = EXTI3_IRQn;
	n.NVIC_IRQChannelPreemptionPriority = 2;
	n.NVIC_IRQChannelSubPriority = 2;
	n.NVIC_IRQChannelCmd = ENABLE;
	NVIC_Init(&n);
}

/*
	KEY3:PE4
	配置PA0作为外部中断EXTI0的输入线
*/
void EXTI4_Init(void) {
	
	// 1. 配置GPIO
	// (1)使能GPIO分组时钟
	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOE, ENABLE);
	// (2)配置GPIO为通用输入模式
	GPIO_InitTypeDef g;
	g.GPIO_Pin = GPIO_Pin_4;
	g.GPIO_Mode = GPIO_Mode_IN;
	g.GPIO_PuPd = GPIO_PuPd_NOPULL;
	GPIO_Init(GPIOA, &g);
	
	// 2. 选择GPIO引脚外部中断请求线(配置SYSCFG选择器)
	// (1) 使能选择器时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE);
	// (2) 配置选择器:选择相应的GPIO引脚作为外部中断的输入引脚
	SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOE, EXTI_PinSource4);
	
	// 3. 配置外部中断控制器(初始化 EXTI 外部中断控制器)
	EXTI_InitTypeDef e;
	e.EXTI_Line = EXTI_Line4;
	e.EXTI_Mode = EXTI_Mode_Interrupt;
	e.EXTI_Trigger = EXTI_Trigger_Falling;
	e.EXTI_LineCmd = ENABLE;
	EXTI_Init(&e);
	
	// 4. 初始化配置NVIC中断控制器
	NVIC_InitTypeDef n;
	n.NVIC_IRQChannel = EXTI4_IRQn;
	n.NVIC_IRQChannelPreemptionPriority = 2;
	n.NVIC_IRQChannelSubPriority = 2;
	n.NVIC_IRQChannelCmd = ENABLE;
	NVIC_Init(&n);
}

/* 
	一步到位

    将与按键相关的外部中断进行初始化
		KEY0 --> PA0 --> EXTI0
		KEY1 --> PE2 --> EXTI2
		KEY2 --> PE3 --> EXTI3
		KEY3 --> PE4 --> EXTI4
*/
void KEY_EXTI_Init(void) {
	
	// 1. 配置GPIO
	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE); // PA0
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOE, ENABLE); // PE2 PE3 PE4
	
	GPIO_InitTypeDef g;    
	g.GPIO_Pin = GPIO_Pin_0;
	g.GPIO_Mode = GPIO_Mode_IN;
	g.GPIO_PuPd = GPIO_PuPd_NOPULL;
	GPIO_Init(GPIOA, &g);
	
	g.GPIO_Pin = GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4;
	GPIO_Init(GPIOA, &g);
	
	// 2. 选择GPIO引脚外部中断请求线(配置SYSCFG选择器)
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE);

    SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOA, EXTI_PinSource0);
    SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOE, EXTI_PinSource2);
    SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOE, EXTI_PinSource3);
    SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOE, EXTI_PinSource4);
	
	// 3. 配置外部中断控制器(初始化 EXTI 外部中断控制器)
	EXTI_InitTypeDef e;
	e.EXTI_Line = EXTI_Line0;
	e.EXTI_Mode = EXTI_Mode_Interrupt;
	e.EXTI_Trigger = EXTI_Trigger_Falling;
	e.EXTI_LineCmd = ENABLE;
	EXTI_Init(&e);
	
	e.EXTI_Line = EXTI_Line2 | EXTI_Line3 | EXTI_Line4;
	EXTI_Init(&e);
	
	// 4. 初始化配置NVIC中断控制器
	NVIC_InitTypeDef n;
	n.NVIC_IRQChannel = EXTI0_IRQn;
	n.NVIC_IRQChannelPreemptionPriority = 2;
	n.NVIC_IRQChannelSubPriority = 2;
	n.NVIC_IRQChannelCmd = ENABLE;
	NVIC_Init(&n);
	
	n.NVIC_IRQChannel = EXTI2_IRQn;
	NVIC_Init(&n);
	
	n.NVIC_IRQChannel = EXTI3_IRQn;
	NVIC_Init(&n);
	
	n.NVIC_IRQChannel = EXTI4_IRQn;
	n.NVIC_IRQChannelPreemptionPriority = 0; // 优先级最高,可抢占其他中断
	n.NVIC_IRQChannelSubPriority = 2;
	NVIC_Init(&n);
}

// 中断处理函数:产生中断请求就会自动调用,不需要声明
void EXTI0_IRQHandler(void) {
	// 中断产生
	if (EXTI_GetITStatus(EXTI_Line0) == SET) {
		
        if (KEY_ReadInputData(KEY1_GPIO, KEY1_Pin) == 0) {
			
			// 假延时 10ms(消抖)
			mydelay(100);
			
			if (KEY_ReadInputData(KEY1_GPIO, KEY1_Pin) == 0) { // 确定是人为按下
				LED1_reversal_status();
				LED2_reversal_status();
			}
			while (KEY_ReadInputData(KEY1_GPIO, KEY1_Pin) == 0);
		}
		// 清除相应的中断标志
		EXTI_ClearITPendingBit(EXTI_Line0);
	}
}

// 中断处理函数:产生中断请求就会自动调用,不需要声明
void EXTI2_IRQHandler(void) {
	// 中断产生
	if (EXTI_GetITStatus(EXTI_Line2) == Bit_SET) {
 		
        if (KEY_ReadInputData(KEY2_GPIO, KEY2_Pin) == 0) {
			
			// 假延时 10ms(消抖)
			mydelay(100);
			
			if (KEY_ReadInputData(KEY2_GPIO, KEY2_Pin) == 0) { // 确定是人为按下
				LED3_reversal_status();
				LED4_reversal_status();
			}
			while (KEY_ReadInputData(KEY2_GPIO, KEY2_Pin) == 0);
		}
		// 清除相应的中断标志
		EXTI_ClearITPendingBit(EXTI_Line2);
	}
}

// 中断处理函数:产生中断请求就会自动调用,不需要声明
void EXTI3_IRQHandler(void) {
	// 中断产生 
	if (EXTI_GetITStatus(EXTI_Line3) == Bit_SET) {
		
        if (KEY_ReadInputData(KEY3_GPIO, KEY3_Pin) == 0) {
			
			// 假延时 10ms(消抖)
			mydelay(100);
			
			if (KEY_ReadInputData(KEY3_GPIO, KEY3_Pin) == 0) { // 确定是人为按下
				BEEP_reversal_status();
			}
			while (KEY_ReadInputData(KEY3_GPIO, KEY3_Pin) == 0);
		}
		// 清除相应的中断标志
		EXTI_ClearITPendingBit(EXTI_Line3);
	}
}

// 中断处理函数:产生中断请求就会自动调用,不需要声明
void EXTI4_IRQHandler(void) {
	// 中断产生
	if (EXTI_GetITStatus(EXTI_Line4) == Bit_SET) {
		
		if (KEY_ReadInputData(KEY4_GPIO, KEY4_Pin) == 0) {
			
			// 假延时 10ms(消抖) 
			mydelay(100);
			
			if (KEY_ReadInputData(KEY4_GPIO, KEY4_Pin) == 0) { // 确定是人为按下
				LED1_reversal_status();
				LED2_reversal_status();
				LED3_reversal_status();
				LED4_reversal_status();
			}
			while (KEY_ReadInputData(KEY4_GPIO, KEY4_Pin) == 0);
		}
		// 清除相应的中断标志
		EXTI_ClearITPendingBit(EXTI_Line4);
	}
}

main.c

#include "stm32f4xx.h"
#include "systick.h"
#include "led.h"
#include "beep.h"
#include "key.h"
#include "exti.h"

int main(void) {
	
	// 指定抢占优先级占多少bits  0~4
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	
	/* LED1 */
	LED1_Clock();
	LED_Init(LED1_GPIO, LED1_Pin);
	LED1_OFF();
	
	/* LED2 */
	LED2_Clock();
	LED_Init(LED2_GPIO, LED2_Pin);
	LED2_OFF();
	
	/* LED3 */
	LED3_Clock();
	LED_Init(LED3_GPIO, LED3_Pin);
	LED3_OFF();
	
	/* LED4 */
	LED4_Clock();
	LED_Init(LED4_GPIO, LED4_Pin);
	LED4_OFF();
	
	/* KEY1 */
	KEY1_Clock();
	KEY_Init(KEY1_GPIO, KEY1_Pin);
	
	/* KEY2 */
	KEY2_Clock();
	KEY_Init(KEY2_GPIO, KEY2_Pin);
	
	/* KEY3 */
	KEY3_Clock();
	KEY_Init(KEY3_GPIO, KEY3_Pin);
	
	/* KEY4 */
	KEY4_Clock();
	KEY_Init(KEY4_GPIO, KEY4_Pin);
	 
	/* BEEP */
	BEEP_Clock();
	BEEP_Init(BEEP_GPIO, BEEP_Pin);
	BEEP_OFF();
	
//	EXTI0_Init();
//	
//	EXTI2_Init();
//	
//	EXTI3_Init();
//	
//	EXTI4_Init();
	
	KEY_EXTI_Init();
	
	while (1);
}
  • 19
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值