文章目录
本文为野火教程学习笔记。
EXTI原理解释
EXTI 简介
EXTI (External interrupt/event controller) ——外部中断/事件控制器,其用于管理外设中GPIO和一些其他外设的中断。可以实现单独对某一线配置中断触发方式等操作。
EXTI原理框图解释
边沿检测器检测外部的信号,上升沿和下降沿触发选择寄存器具体的配置会决定边沿检测电路检测何种边沿。
检测到对应边沿后,检测电路输出1,软件中断事件寄存器的配置决定是否产生中断或事件,相当于小开关。
若配置响应这个边沿,则小开关输出1。下面由请求挂起寄存器和事件屏蔽寄存器确定产生中断还是事件。这两个相当于选择开关。
如果为中断,最后来到中断屏蔽寄存器,这相当于大开关。
从原理图中我们可以看出,从输入线输入开始到NVIC向内核发出中断,中间要经过边沿检测电路,小开关,请求寄存器,大开关等寄存器。各个寄存器的对应名称已经标注在图片上。
输入线
得知 EXTI 中断的过程后,探讨输入线部分内容。
EXTI可以管理20根外部输入源,EXTI和对应外部输入源匹配后的整体叫做输入线。
外部的输入源与中断线EXTI的对应关系如下图
EXTI的前16根线的输入源只能是GPIO端口,单独一根线允许连接GPIO的固定一种端口;如:EXTI0 只能连接 0 端口,如:PA0,PB0… 不能连接别的端口,如不可连接PA1,PB1…
小结:外部中断理论部分
要使用 EXTI 外部中断。对于中断过程中,要配置框图中的各种寄存器以达到目的。对于输入线,要配置输入源和中断线以组成输入线。
编程实例
实例简介
上面介绍了EXTI的原理,我们现在编写一个按键控制LED灯的程序,其中让EXTI来检测按键按下的动作,然后产生中断,在中断函数中反转LED灯。
实验的电路图
![](https://i-blog.csdnimg.cn/blog_migrate/88cfa4ca939d840e988ce00d6bb87b57.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/cd8868ac6eab0e3650265835df7749cc.jpeg)
从图中我们可以看出,按键默认是低电平。按下后为高电平。我们希望按键按下时有动作,所以我们要设置PA0上升沿触发,然后在中断函数中控制PB5反转。
开始编程(一)
首先我们在User文件夹下新建文件夹以存放C和H文件,并添加到USER下Group中,把H文件填入IncludePath.
初始化EXTI和GPIO
初始化EXTI
根据经验,固件库中一定有初始化的函数。打开stm32f10x_exti.h翻到最下面我们就可以看见这个头文件所有函数,果然有
void EXTI_Init(EXTI_InitTypeDef* EXTI_InitStruct);
一看就知道这是初始化函数。打开后发现只需要一个结构体的传入,F12这个结构体我们看到:
我们可以看见这个结构体提供了 :
EXTI_Line : 用来选定PXm 中的m
EXTI_Mode : 用来选定工作方式->中断/事件
EXTI_Tigger : 触发方式
EXTI_LineCmd : 开启开关。
这些成员变量的取值一定被枚举或宏定义好了。向上向下翻找就可以找到。
在这里你一定发现,通过初始化 EXTI ,可以确定中断线,但无法确定输入源。即只能指定 PXm 中的 m,无法确定其中的X。想要配置输入源 需要从GPIO库中的函数进行配置。
初始化GPIO
老习惯,去找找固件库有没有这样的函数。打开stm32f10x_gpio.h翻到对最下面,我们可以看到这条函数
void GPIO_EXTILineConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource);
十分的神似,F12进去看看函数说明;
通过函数的描述我们可以看见这就是用来配置EXTI中断的,并且同时指定了输入源和中断线。
GPIO_PortSource
指定输入源,GPIO_PinSource
指定中断线。
值得注意的是,这个函数已经在操作AFIO这个外设了,因此,开时钟时必须开启AFIO的时钟。
对于这个例子,我们无需初始化GPIO,因为 PA0 的输入信号会直接通过 EXTI0 内部电路完成中断。但对于一种情况,GPIO的初始化是必要的,在思考中会 体现这一点。
最后提醒一下,不要忘记打开时钟。
void EXTI_KEY_Config(void)
{
EXTI_InitTypeDef EXTI_InitStruct;
//这里还缺少一个配置中断优先级的函数,下面再说
//EXTI_NVIC_Config();
//初始化EXTI
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE); //千万别忘了打开时钟
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA,GPIO_PinSource0); //选择输入线
//配置EXTI初始化函数的结构体
EXTI_InitStruct.EXTI_Line = EXTI_Line0;
EXTI_InitStruct.EXTI_LineCmd = ENABLE;
EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Rising;//上升沿触发
EXTI_Init(&EXTI_InitStruct);
}
小结:外部中断寄存器配置部分
通过上一个理论小结,我们需要配置框图中的寄存器保证中断可被内核接收,配置输入线保证外部中断可以传入。
这里程序部分,初始化 EXTI 配置了框图中的寄存器,配置了中断线。初始化 GPIO和 AFIO配置了中断线和输入源,即完成了输入线的配置。
开始编程(二)
由上节STM32中断应用总结的 中断编程顺序 可知,我们上面的工作不过是完成了第一步,配置外设中断。
配置中断优先级分组
初始化NVIC时要配置好优先级的分组方式,这就要用到
void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup);
这个函数,其中的参数也可在上下的枚举中找到。我们设置优先级分组为1。
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
配置 NVIC 寄存器
配置这个寄存器需要完成对NVIC_InitTypeDef
这个结构体的配置。
其中结构体的成员变量NVIC_IRQChannel
的参数可以在IRQn_Type
中找到,F12进入
可以看见,IRQn_Type
事实上是一个枚举,而且其中使用宏定扩展对应了不同型号的F10x系列的芯片,值得注意的,EXTIm
中 m=0,1,2,3,4是所有芯片都有定义的,而且每个都有独立定义。但5-9,10-15就不是了,在下面大容量的宏定义中我们可以看见他们是被集体定义在一起的。在使用时要注意。
我们编写一个
EXTI_NVIC_Config()
用来对NVIC进行初始化。设置主优先级为1,子优先级为1。
static void EXTI_NVIC_Config(void)//static 说明此函数只可被这个.c文件的函数调用
{
NVIC_InitTypeDef NVIC_InitStruct;
//配置优先级分组方式
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
//配置初始化函数
NVIC_InitStruct.NVIC_IRQChannel = EXTI0_IRQn;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStruct);
}
至此所有的初始化工作已经完成,当被设置的端口有变化被检测到时,就会进入中断函数。
编写中断服务函数
所有的中断函数都必须被编写在 stm32f10x_it.c文件中以便于管理。并且中断函数的名字必须和启动文件。打开启动文件startup_stm32f10x_hd.s,我们找到名称。
找到这个函数名后,我们希望进入中断后再判断一下EXTI中断挂起寄存器PR是否真的置位了,可以用这个函数,在exti中可以找到
ITStatus EXTI_GetITStatus(uint32_t EXTI_Line);
他会返回ITStatus类型,即Set或Reset。
在完成操作后,我们必须把挂起寄存器里的置位清零,这样程序才能去运行下面的工作,而不是重复跳入中断函数。用于操作的函数在exti中也有。这一句不可缺少。
void EXTI_ClearITPendingBit(uint32_t EXTI_Line);
这两个对EXTI_PR寄存器操作的函数,所需的参数都是EXTI_Line。用用于指定函数操作的线。可以说EXTI_Line 和GPIO_PIN十分相似。
我们在stm32f10x_it.c中添加这个函数
/*这部分宏定义实现了LED反转的功能应当被定义在bsp_led.h中合适
这里为了便于观察附于此*/
#define LED_G_GPIO_PIN GPIO_Pin_0
#define LED_G_GPIO_RORT GPIOB
#define LED_G_TEOOGLE {LED_G_GPIO_RORT->ODR ^= LED_G_GPIO_PIN;}
void EXTI0_IRQHandler(void)
{
//先判断是否真的置位了
if(EXTI_GetITStatus(EXTI_Line0) !=RESET)
{
LED_G_TEOOGLE; //LED 反转
EXTI_ClearITPendingBit(EXTI_Line0); //清除中断标志位
}
}
附上之前已经用很多次的LED配置代码
void LED_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
RCC_APB2PeriphClockCmd(LED_G_GPIO_CLK, ENABLE);
GPIO_InitStruct.GPIO_Pin = LED_G_GPIO_PIN;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(LED_G_GPIO_RORT,&GPIO_InitStruct);
}
至此所以的准备就已经完成了,只需要在main中简单的调用就可以了。
int main(void)
{
LED_GPIO_Config();
EXTI_KEY_Config();
while(1){}
}
思考
本次的实例中,输入线由 EXTI0 作为中断线,PA0作为输入源;一个中断线连接了一个输入源。
但如果一个中断线连接了多个输入源,如PA0,PB0,PC0… 和 EXTI0 连接。这时 PA0,PB0,PC0… 中任何一个产生中断信号后,都是通过 EXTI0 这条中断线进入中断的。即这些线的中断信号最后都会由同一个中断服务函数处理。如何判断是那条线产生的中断呢?
这里需要在中断服务函数中编写程序手动判断。主要思想是利用外部电平变化时,GPIO的 IDR 寄存器会记录电平。通过判断 IDR 的变化来判断是哪一个中断源。这时,在程序中就需要初始化输入的GPIO为浮空输入模式。(虽然浮空GPIO默认下就是浮空输入模式,为了保险起见,还是要初始化)