目录
b.初始化SYSCFG选择器(选择外部中断具体的输入来源)syscfg
7.3 初始化EXTI外部中断控制器(EXTI也是外设,也需要使能时钟,但是默认就是打开的)exti
1.问题的引入
之前的练习是按键点灯
如果要去解决这个问题,需要CPU不断的去读取按键对应的那个引脚电平状态。
1-->松开 0-->按下
然后去做响应的事件的可以了。
大概的代码流程:
int main()
{
//初始化函数....
while(1)
{
if(GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0) == 0)//S1
{
}
if()
{
......
}
}
}
因为需要一直不停的去判断按键对应的GPIO的电平,加上一个while(1),这种工作方式称之为轮询。
轮询有缺陷:
1.浪费CPU
2.占用总线
3.有时差
有没有办法不让CPU主动去询问事件是否发生,而是当事件发生之后,主动通知CPU?--->中断
2.中断的概念
一般中断定义为打断CPU指令的正常执行顺序的事件
现代CPU架构为了能够及时响应外部或内部的一些紧急事件,都支持中断,并且会提供相应的中断响应机制-->中断机制
3.ARM Cortex-M4中断机制
当中断控制器(NVIC)通知CPU产生了某个事件,此时CPU就会停止正在做的事情,转而去执行中断
而且M4给不同的中断有一个唯一的编号(中断编号:用来区分不同的中断事件)
CPU怎么根据不同的中断事件去做不同的处理?根据中断向量表。
中断向量表是什么?
是一个函数指针数组,保存了所有的中断的处理函数的地址
中断编号实际上就是这个数组的下标
所以当某个事件发生的时候,产生中断,那么CPU就会根据此中断对应的中断编号将其作为下标去调用该下标在向量
表中对应的中断服务函数。
中断服务函数的格式:
void func1(void)
{
}
####中断函数为什么不需要参数和返回值?
首先明白中断函数与普通函数的区别:
普通函数:
用户主动调用的,所有用户在调用之前可以准备好参数,当然也可以解析返回值
中断函数:
CPU被动调用,不是主动调用。是当有紧急事件发生的时候,CPU被动去执行中断函数。它可以在用户指令的任何时刻去
调用--->中断在任何时刻都有可能发生
对于用户来说,无法提前准备参数,也无法解析返回值
4.中断向量表
不同的中断事件对应不同的中断函数。比如:
中断事件0 ----> xxx_isr0
......
中断函数的命名由用户指定,不是CPU指定。CPU负责的是把所有的中断函数的地址放到一块连续的,固定的地址上
中断向量表的实现:
void (*isr_addr[256])(void);
isr_addr[0] = isr_0;
isr_addr[1] = isr_1;
....
isr_addr[n] = isr_n;
当事件发生的时候,中断控制器(NVIC)就会打断CPU的执行,同时告诉了CPU是x号中断产生了,那么CPU就会执行isr_addr[x]
为了能够让CPU更快的响应中断,M4约定将中断向量表放在一个固定的位置,就是存储器地址为0x08000000处(通过映射
的方式将其映射到0x00000000处)、
5.STM32F4XX的中断管理机制
看图示
任何的中断能够被CPU响应,都要经过以下的阶段:
5.1 中断源阶段
中断源是指产生中断的设备
设备要想产生中断,就必须有一根中断请求线(IRQline)
并且这根中断请求线必须连接在中断控制器(NVIC)上
5.2 中断控制器阶段(NVIC)
中断控制器是对多有中断输入引脚进行管理和控制。
可以根据输入的中断请求给CPU内核一个中断信号,通知CPU某某设备产生了中断。外部硬件发送中断信号给NVIC的时候,同时
产生一个8位中断编号。CPU在响应这个中断请求的时候,同时也会得到这个中断编号,然后以这个中断编号为下标就会去对应
的中断向量表中找到对应的中断函数,跳转过去执行
一个设备产生了中断首先经过"中断源"这一级,而"中断源"可以屏蔽或使能中断。即便外部设备产生了中断,中断源可以
不告诉NVIC
中断控制器它也可以控制中断,当NVIC接收到中断请求后,也可以选择enbale或者disable这个中断。
在STM32中,中断的种类以及数量太多了,首先要从外部中断开始。
6. STM32F4xx外部中断
外部中断(EXTI)是指GPIO的外部电路上产生的中断
比如:在GPIO口的外部电路上产生一个上升沿或者下降沿可能会导致一个中断的产生
F407上一共有23个外部中断。
记成:EXTI0~EXTI22
其中只有EXTI0~EXTI15对应引脚
EXTI0
PA0
PB0
....
PI0
EXTI1
PA1
PB1
...
PI1
....
EXTI15
PA15
PB15
....
PI15
其余的外部中断来源可以查看中文手册
所以外部中断路线图:
GPIO引脚外部信号输入(高跳变/低跳变)
---->GPIO控制器(此时应该配置为输入状态)
---->SYSCFG选择器(选择具体是哪个GPIO产生EXTI)
---->EXTI外部中断控制器(边沿触发/外内部中断选择/屏蔽和使能)
---->NVIC控制器(中断的优先级/屏蔽和使能)
---->CPU停止正常执行顺序
--->获取中断编号(中断向量表的下标)
--->中断向量表中查找中断处理函数地址
--->执行中断服务函数
7.外部中断的代码实现:
首先还是通过原理图:
S1-->PA0-->EXTI0-->EXTI0_IRQHandler
S2-->PE2-->EXTI2-->EXTI2_IRQHandler
....
完全根据上述的路线图进行代码配置:
7.1 配置GPIO控制器为输入模式
a.使能时钟
b.初始化GPIO控制器
GPIO_Init
7.2 配置SYSCFG选择器
SYSCFG选择器也是一个外设,所以使用之前也需要使能时钟
a.使能时钟
经过手册的查询,SYSCFG是位于APB2时钟总线上 --rcc
RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph, FunctionalState NewState);
@RCC_APB2Periph:指定外设名
RCC_APB2Periph_SYSCFG
@NewState:使能还是禁止
ENABLE
DISABLE
b.初始化SYSCFG选择器(选择外部中断具体的输入来源)syscfg
void SYSCFG_EXTILineConfig(uint8_t EXTI_PortSourceGPIOx, uint8_t EXTI_PinSourcex);
@EXTI_PortSourceGPIOx:选择具体的GPIO分组
EXTI_PortSourceGPIOA
EXTI_PortSourceGPIOB
.....
@EXTI_PinSourcex:具体的引脚编号
EXTI_PinSource0
EXTI_PinSource1
.....
比如:配置PA0为EXTI0的输入来源
SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOA,EXTI_PinSource0);
7.3 初始化EXTI外部中断控制器(EXTI也是外设,也需要使能时钟,但是默认就是打开的)exti
a.定义一个结构体变量
EXTI_InitTypeDef EXTI_InitStruct;
b.结构体成员赋值
typedef struct
{
uint32_t EXTI_Line;//指定要初始化的外部中断线(可以位或多个)
EXTI_Line0
EXTI_Line1
....
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;
c.把刚才的结构体写进寄存器
EXTI_Init(&EXTI_InitStruct);
-------------------------------------------------------------------------------
需要注意的是:
外部中断触发之后,会在相应的外部中断寄存器中,设置"中断标志",而外部中断状态寄存器中的状态标志可以用来判断
是否产生了中断,并且这个中断标志必须经过软件(用户代码)清除
a.获取中断标志位
FlagStatus EXTI_GetFlagStatus(uint32_t EXTI_Line);//在中断函数外使用
ITStatus EXTI_GetITStatus(uint32_t EXTI_Line);//中断函数里面使用
@EXTI_Line:指定外部中断线
返回值:
SET 表示检测的外部中断线已经产生了中断
RESET 表示检测的外部中断线没有产生中断
b. 清除中断标志
void EXTI_ClearFlag(uint32_t EXTI_Line);
void EXTI_ClearITPendingBit(uint32_t EXTI_Line);
@EXTI_Line:指定外部中断线
注意:
在CPU执行完中断处理函数之后,就必须调用清除中断标志函数,否则,CPU认为你的中断尚未处理,也不会回归到
正常模式
所以中断函数的格式(以EXTI0的中断为例):
void EXTI0_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line0) == SET)
{
....执行的代码
}
EXTI_ClearITPendingBit(EXTI_Line0);
}
-------------------------------------------------------------------------------------------------
7.4 NVIC控制器
NVIC管理着所有的中断的优先级misc
a.定义一个结构体变量
NVIC_InitTypeDef NVIC_InitStruct;
b.结构体成员赋值
typedef struct
{
uint8_t NVIC_IRQChannel;//指定中断通道(不可以位或)
//STM32固件库中,给每一个中断一个唯一的编号,并且这个编号用枚举实现的
//存在stm32f4xx.h中
EXTI0_IRQn
EXTI1_IRQn
EXTI2_IRQn
EXTI3_IRQn
EXTI4_IRQn
EXTI9_5_IRQn
EXTI15_10_IRQn
这个中断通道就是中断编号
uint8_t NVIC_IRQChannelPreemptionPriority;//抢占优先级
uint8_t NVIC_IRQChannelSubPriority;//响应优先级
//NVIC允许用户给每一个通道分配优先级
两个优先级共占4bits,数字越小优先级越高
优先级4bits分:
抢占优先级占 x bits
响应优先级占 4-x bits
抢占优先级:
当一个中断A产生并且CPU已经在执行A的中断服务函数,此时产生中断B,发现中断B的抢占优先级
比A要高,此时CPU放弃执行中断A,转而去执行中断B,处理B之后再去处理A
响应优先级:
当两个或者多个中断同时产生并且它们的抢占优先级一样的情况下
响应优先级决定谁先执行
x具体是占多少,由用户调用函数去配置
中断优先级分组配置函数:
void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup);
@NVIC_PriorityGroup:指定抢占优先级占多少位
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函数里面前面调用一次即可
FunctionalState NVIC_IRQChannelCmd;//使能还是禁止
ENABLE
DISABLE
} NVIC_InitTypeDef;
c.把刚才的结构体写进寄存器
NVIC_Init(&NVIC_InitStruct);
7.5 编写中断服务函数
中断服务函数的格式一定不能乱改
中断服务函数的名字一定是自己去启动文件中复制
void EXTI0_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line0) == SET)
{
....执行的代码
}
EXTI_ClearITPendingBit(EXTI_Line0);
}
将上述的配置过程完成后,中断就配置好了,当中断产生,CPU就会自动去调用函数
那么主函数main只需要在完成各种初始化之后,陷入死循环即可。
int main()
{
//中断优先级分组
//各种初始化
while(1);
}
8.练习
将四个按键设置为中断
S1 ---> D1和D2
S2 ---> D3和D4
S3 ---> BEEP
S4 ---> 四个灯
我就在这里列出中断相关的代码,其他模块的我放在了另外几篇文章中
exti.c
#include "stm32f4xx.h"
#include "exti.h"
#include "key_lib.h"
#include "led_lib.h"
void EXTIX_Init(void)
{
EXTI_InitTypeDef EXTI_InitStruct;
NVIC_InitTypeDef NVIC_InitStruct;
key_lib_init();
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);
// 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_InitStruct.EXTI_Line = EXTI_Line0 | EXTI_Line2 | EXTI_Line3 | EXTI_Line4;
EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Falling;
EXTI_InitStruct.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStruct);
NVIC_InitStruct.NVIC_IRQChannel = EXTI0_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 2;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStruct);
NVIC_InitStruct.NVIC_IRQChannel = EXTI2_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 3;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStruct);
NVIC_InitStruct.NVIC_IRQChannel = EXTI3_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 2;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 2;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStruct);
NVIC_InitStruct.NVIC_IRQChannel = EXTI4_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 3;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStruct);
}
void EXTI0_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line0) == SET)
{
delay_ms(50); //延时消抖
if(KEY0 == 0) //判断按键KEY0是否被按下
{
GPIO_ToggleBits(GPIOF,GPIO_Pin_9);//翻转D1灯的状态
GPIO_ToggleBits(GPIOF,GPIO_Pin_10);//翻转D2灯的状态
}
}
EXTI_ClearITPendingBit(EXTI_Line0);
}
void EXTI2_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line2) == SET)
{
delay_ms(50); //延时消抖
if(KEY1 == 0) //判断按键KEY0是否被按下
{
GPIO_ToggleBits(GPIOE,GPIO_Pin_13);//翻转D3灯的状态
GPIO_ToggleBits(GPIOE,GPIO_Pin_14);//翻转D4灯的状态
}
}
EXTI_ClearITPendingBit(EXTI_Line2);
}
void EXTI3_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line3) == SET)
{
delay_ms(50); //延时消抖
if(KEY2 == 0) //判断按键KEY0是否被按下
{
GPIO_ToggleBits(GPIOF,GPIO_Pin_8);//翻转蜂鸣器的状态
}
}
EXTI_ClearITPendingBit(EXTI_Line3);
}
void EXTI4_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line4) == SET)
{
delay_ms(50); //延时消抖
if(KEY3 == 0) //判断按键KEY0是否被按下
{
GPIO_ToggleBits(GPIOF,GPIO_Pin_9);//翻转D1灯的状态
GPIO_ToggleBits(GPIOF,GPIO_Pin_10);//翻转D2灯的状态
GPIO_ToggleBits(GPIOE,GPIO_Pin_13);//翻转D3灯的状态
GPIO_ToggleBits(GPIOE,GPIO_Pin_14);//翻转D4灯的状态
}
}
EXTI_ClearITPendingBit(EXTI_Line4);
}
exti.h
#ifndef __EXTI_H__
#define __EXTI_H__
#include "stm32f4xx.h"
void EXTIX_Init(void);
#endif