建议提前学习:使用STM32CubeMX实现按下按键,电平反转;STM32中断与事件的理解
目录
EXTI
中断
中断的概念
抢占优先级与响应优先级
中断分组
事件
上升沿,下降沿以及双边沿触发
上升沿,下降沿以及双边沿的概念
上升沿,下降沿以及双边沿触发
EXTI中断/事件线
中断服务函数
NVIC
中断的流程框图
STM32CubeMX配置
电路图分析
引脚配置
K2中断配置
K1按键下拉输入
LED1和LED2配置为输出
生成文件
Keil编写程序
__HAL_GPIO_EXTI_GET_IT()
作用
__HAL_GPIO_EXTI_GET_IT()内部实现
使用说明
__HAL_GPIO_EXTI_CLEAR_IT()
作用
__HAL_GPIO_EXTI_CLEAR_IT()内部实现
使用
中断分组的函数在哪里
中断函数
哪里可以查看中断服务函数名字
STM32CubeMX为我们生成的中断服务函数在哪里
代码
宏定义部分
while(1)中代码
中断服务函数
STM32的EXTI有事件与中断两种形式。
推荐一个文档:事件与中断区别;
中断:一定会有一个中断函数需要执行。有CPU的参与。
事件:事件可以没有函数,他不需要CPU参与。事件可以触发一些操作,比如触发DMA,触发ADC采样等等。
首先,我们需要了解什么是中断。我们都知道,程序都是按照我们编写的顺序进行的,从上到下依次进行。但是很显然,这样会导致程序运行很死板,无法处理突发事件。于是就有了中断。
CPU你可以理解为一个非常听话的人,他会完美的按照你的指令执行任务,但是又过于死板。举个例子:
(1)假设你有一个小孩。你跟小孩说让他做完作业再出房间看电视。
(2)按照正常情况,他是先做完作业,然后出房间,最后看电视。但是你这个时候有紧急事情要处理,要出去有点事要处理,但是现在你在厨房做菜。如果你直接就折磨走了,显然会造成事故。怎么办呢?
(3)于是你就让小孩停止做作业,出来做饭。但是我说了,小孩认为做饭不是他的事情,不听你的。于是你拿出皮带,强行要求小孩做饭。(有点小残忍呀,嘿嘿)
这个打断小孩做作业,让他做饭,就叫做中断。
(4)然后小孩做完饭做了一半,你回来了。就让小孩再去做作业?你想吃饭时间再让他去做作业,很显然太残忍。于是你让他先看电视去休息。
这个打断小孩做饭(第一层中断),他还没做完饭(第一层中断还没有完成)就让他去看电视(第二层中断),叫做中断嵌套。
一个中断响应有抢占优先级与响应优先级。有什么区别呢?
抢占优先级:(概念)抢占优先级就和他的名字一样。很霸道,如果我的抢占优先级比你高,那么我就打断你现在的行为,让你做其他事情。不需要等待正在执行的中断执行完!
(举例)依旧拿上面这个小孩作为例子,打断小孩做饭(第一层中断),他还没做完饭就让他去看电视(第二层中断),这个叫做中断嵌套,同时也体现了,第二层中断的抢占优先级高于第一层中断。
响应优先级:(概念)响应优先级就是等待响应,他不会突然打断现在执行的中断。如果有一个中断正在执行,这个时候先后来了两个中断,这三个中断的抢占优先级是一样的。那么就先等待第一个正在执行的中断执行完,然后判断后来的两个中断响应优先级,响应优先级高的任务先执行。
(举例)假设你现在下班了,同时有两个选择,第一个是打一小时游戏,第二个是陪女友逛街。显然打游戏的优先级高于陪女友逛街。那么你就会先打一小时游戏,等一个小时之后再去陪女友逛街。这种先执行一个任务,再执行另外一个任务,这个就体现了响应优先级。
如果来了两个中断,首先我们判断它的抢占优先级(也可以叫做主优先级),如果抢占优先级一致。那么才开始判断响应优先级(也可以叫做从优先级)。
需要注意的一点是,抢占优先级0比抢占优先级1要大,响应优先级0比响应优先级1大。
stm32的中断是存在分组的,有5种分组方式。如果我们选择了分组2,那么抢占优先级只有0-3,不存在抢占优先级4。
推荐博客:STM32中断与事件的理解
依旧是上面这个例子
现在小孩(CPU)在看电视,但是突然来了一个上门快递。你(硬件)去开门签收快递。
这个过程小孩没有参与(CPU),全过程交给了你(硬件)。这个叫做事件。
我暂时还没用过事件,我所能想到的是通过事件触发ADC,然后利用DMA将数据存入SRAM。
此章节只讲中断!!!
上升沿:低电平到高电平的这个过程
下降沿:高电平到低电平的这个过程
双边沿:上升沿+下降沿
这个触发表示,发生上升沿或者下降沿才会触发中断。具体是怎么触发,这个需要我们自己配置。
EXTI 有 20 个中断/事件线,每个 GPIO 都可以被设置为输入线,占用 EXTI0 至 EXTI15,还有另外4根用于特定的外设事件。
我们需要注意一个点,就是如果我们PA0设置为外部中断引脚,那么PB0就不能为中断引脚。因为所有PIN0都公用同一根中断线。
外部中断虽然有16根中断,但是外部中断服务函数却只有7个。EXTI0—EXTI4五个中断线分别各占一个中断服务函数。但是EXTI5—EXTI9这5个中断线公用一个中断服务函数,EXTI10—EXTI15这6个中断线公用一个中断服务函数。
所有中断最后都是由NVIC才处理,这里涉及到内核相关知识了,属于我的知识盲区。感兴趣的可以去看CM3内核的NVIC部分内容。
总结:记住所有中断由NVIC处理即可,他与内核有关。
不知道与或非门概念的,看:二值逻辑变量与基本逻辑运算;
使用中断之前,我建议先了解一下STM32F103的中断机制,以上升沿触发为例,如下
看不懂没关系,接着往下看,你记住那几个函数什么时候使用就行。但是有基础的建议看完,之后接触其他型号的单片机上手更快。
(1)边缘检测电路检测输入线,如果检测到上升沿,上升沿触发选择寄存器将会置1。这里需要注意,我们这里使用的是外部中断,非软件中断,所以软件中断事件寄存器为0。(软件中断就是定时器中断,ADC中断等等)但是这是或门,所以有1出1。2处输出为1。
(2)因为我们是中断,而非事件,所以事件屏蔽寄存器位0。与门是有0出0,所以与门2输出为0。
(3)因为我们使用的是中断,所以中断屏蔽寄存器要设置为1。(这个在GPIO初始化的时候就已经设置好了)
(4)因为断屏蔽寄存器已经为1了。现在我们2处的或门输出为1,那么与门1输出1给NVIC,这个时候将会触发中断。
实验要体现中断,我打算利用,按下按键K1,绿灯电平反转,然后while等待。这个时候将K2配置为中断,按下K2,蓝灯和绿灯电平都反转。(注意,如果K2不是中断,K1没有松手,无论你怎么按K2都没有现象改变呀。因为会堵死在while循环里面,一直等待你K1松手)
首先我们看电路图都知道按下按键为高电平。那么我们可以设置K2为上升沿触发。K1为下拉输入。
(1)第一步,配置GPIO模式
配置中断的时候,GPIO mode里面有6个选项
(1)前面三个表示中断,后面三个是事件。
(2)第一个是上升沿触发中断,第二个是下升沿触发中断,第三个是双边升沿触发中断
(3)第四个是上升沿触发事件,第五个是下升沿触发事件,第六个是双边升沿触发事件
然后配置为下拉。注意
(1)在标准库里面,外部中断要配置成输入。这里HAL库虽然没说GPIO是输出还是输入,但是我们需要知道,外部中断是要配置成输入。
(2)
如果我们是上升沿触发,输入要配置为下拉或者无上下拉。
如果我们是下升沿触发,输入要配置为上拉或者无上下拉。
如果我们是双边升沿触发,上下拉配置随便。
(2)第二步,配置NVIC(这个专门负责中断的)
∆ 这里我们需要配置中断分组(这个随便你自己怎么配置,只要注意抢占优先级和响应优先级符合中断分组就行)
∆ 建议把按照抢占优先级和响应排序打开,这样我们就能直观的知道中断顺序。(这里需要注意一个点,我们只开启了PC13的中断,但是怎么会有折磨多中断呢?原因是因为其他中断是系统中断,不需要理会)
∆ 使能中断需要勾选,如果你没有勾选,中断也就不会产生。
∆ 5和6分别是配置抢占优先级和响应优先级的。
∆ 这里有一个东西需要注意!!!我是写写程序验证实验的时候才发现的!!!
Time base: System tick timer的抢占优先级要高于(不能等于)外部中断的优先级。否则会出现在外部中断中无法调用延时函数的情况。(HAL_Delay()这个函数是滴答定时器,使用的是滴答定时器中断实现的。只需了解)
因为LED是外接高电平,所以引脚初始化为高电平输出。否则你刚开机,灯就已经亮了。
GPIO配置
如果还不会生成文件的,看:STM32CubeMX新建工程并点亮一个LED;的文件生成部分
我们先介绍它的函数使用,再进行程序编写吧。此处我们只会讲解需要用到的函数,其他的不怎么常用的,等各位入门了自己可以研究。
这里会涉及到中断的流程框图的内容,如果没看懂的人,直接看函数的使用说明即可。
(1)获取中断线的是否发起中断请求。这个是什么意思呢?
(2)上面我说了,EXTI0—EXTI4五个中断线分别各占一个中断服务函数。但是EXTI5—EXTI9这5个中断线公用一个中断服务函数。那么就会出现一个问题,现在我PA5和PA7两个引脚都有中断,当其中一个引脚发生中断,他们会同时进入相同的中断服务函数EXTI9_5_IRQHandler。
(3)那么这个时候__HAL_GPIO_EXTI_GET_IT()这个函数的作用就有了,我们可以在EXTI9_5_IRQHandler这个中断函数里面先进行一个if判断,是哪一个中断线发生中断请求。
__HAL_GPIO_EXTI_GET_IT()其实不是一个函数,而是一个宏定义。如下
/**
* @brief Checks whether the specified EXTI line is asserted or not.
* @param __EXTI_LINE__: specifies the EXTI line to check.
* This parameter can be GPIO_PIN_x where x can be(0..15)
* @retval The new state of __EXTI_LINE__ (SET or RESET).
*/
#define __HAL_GPIO_EXTI_GET_IT(__EXTI_LINE__) (EXTI->PR & (__EXTI_LINE__))
(1)我们看这个宏定义,发现其实它是对PR这一个寄存器进行操作,那么我们可以尝试翻阅STM32F103数据手册。发现PR这一个寄存器可以查看中断是否被挂起。让我们让PR寄存器&(有0 出0)对应的PIN脚(比如GPIO_PIN_13就是0x2000,正好对应PR13),那么就可以获取这一个中断线是否有中断请求。(这里看不懂,直接看后面,这一部分是给有基础的人看的。我最后面会告诉你们什么时候需要使用,记住即可。)
(2)这个PR就是挂起寄存器,如果外部来了符合我们设置的脉冲,那么这个寄存器将会被挂起,也就是输出为1。(结合上面那个流程图)
这个PR有32bit,但是只有前20bit被使用,这一一对应了20根中断/事件线。如果是中断/事件线13发生中断响应,那么PR13将会被挂起(也就是1)
(3)我们可以通过读取PR,来确认到底是拿一根中断/事件线发生了中断响应。这样就很好的处理了EXTI5—EXTI9这5个中断线公用一个中断服务函数,EXTI10—EXTI15这6个中断线公用一个中断服务函数的尴尬问题。
//EXTI15_10的中断服务函数
void EXTI15_10_IRQHandler(void)
{
//确保是否产生了 EXTI Line 13中断
if (__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_13) != RESET)
{
}
}
(1)外部中断的PR寄存器发生了触发信号之后,会被置1。使用完之后PR寄存器并不会自己清0。
(2)这样将会导致一个问题,PR(挂起寄存器)一直为1,然后中断屏蔽寄存器在GPIO配置的时候就已经置1了。就会导致,会持续向NVIC中断控制器发起请求,中断会持续进行,进行完这次中断之后,再重新进行一次这个中断。
(3)因为我们只希望来一次触发信号,就执行一次中断。而不是来一次触发信号,就一直执行中断。所以我们就要使用__HAL_GPIO_EXTI_CLEAR_IT()来清除PR寄存器。
我们看下面__HAL_GPIO_EXTI_CLEAR_IT()的内部实现,发现很有意思的一点,就是__HAL_GPIO_EXTI_CLEAR_IT()不是一个函数!
我们发现是向PR寄存器相应位写入1,才是清除标志位,写0没反应。所以这里是等于!!!
/**
* @brief Clears the EXTI's line pending bits.
* @param __EXTI_LINE__: specifies the EXTI lines to clear.
* This parameter can be any combination of GPIO_PIN_x where x can be (0..15)
* @retval None
*/
#define __HAL_GPIO_EXTI_CLEAR_IT(__EXTI_LINE__) (EXTI->PR = (__EXTI_LINE__))
void KEY1_IRQHandler(void)
{
//确保是否产生了 EXTI Line 13中断
if (__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_13) != RESET)
{
...
//清除中断标志位
__HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_13);
}
}
很多人都是从学习标准库转到HAL库的,所以我这里说一下STM32CubeMX的中断分组在哪里。纯新手可以了解一下。
//1,点击这个函数,按F12
HAL_Init();
//2,跳转到这个函数之后,往下翻,会看见这一行。注意我配置的中断分组为4
/* Set Interrupt Group Priority */
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);
STM32的中断服务函数都是由指定名称的。比如外部中断线EXTI10到15引起中中断,会进入EXTI15_10_IRQHandler这个中断服务函数。那么我们应该如何寻找我们的对应中断服务函数呢?
现在各位应该已经了解了STM32的外部中断的。我就直接上代码
/* Private defines -----------------------------------------------------------*/
#define K2_Pin GPIO_PIN_13
#define K2_GPIO_Port GPIOC
#define K2_EXTI_IRQn EXTI15_10_IRQn
#define K1_Pin GPIO_PIN_0
#define K1_GPIO_Port GPIOA
#define LED_G_Pin GPIO_PIN_0
#define LED_G_GPIO_Port GPIOB
#define LED_B_Pin GPIO_PIN_1
#define LED_B_GPIO_Port GPIOB
while (1)
{
/* USER CODE END WHILE */
if(HAL_GPIO_ReadPin(K1_GPIO_Port,K1_Pin) == SET)
{
//软件消抖,延时20ms,如果按键并联了电容,就不用考虑这个
HAL_Delay(20);
if(HAL_GPIO_ReadPin(K1_GPIO_Port,K1_Pin) == SET)
{
//按下K1,绿灯亮
HAL_GPIO_TogglePin(LED_G_GPIO_Port, LED_G_Pin);
}
//等待松手
while(HAL_GPIO_ReadPin(K1_GPIO_Port,K1_Pin) == SET);
}
/* USER CODE BEGIN 3 */
}
void EXTI15_10_IRQHandler(void)
{
//确保是否产生了 EXTI Line 13中断
if (__HAL_GPIO_EXTI_GET_IT(K2_Pin) != RESET)
{
//再次强调!!!Time base: System tick timer的抢占优先级要高于(不能等于)外部中断的优先级
//软件消抖,延时20ms,如果按键并联了电容,就不用考虑这个
HAL_Delay(20);
//判断是不是K2被按下
if(HAL_GPIO_ReadPin(K2_GPIO_Port,K2_Pin) == SET)
{
//按下K2,绿灯灭,蓝灯亮,
HAL_GPIO_TogglePin(LED_G_GPIO_Port, LED_G_Pin);
HAL_GPIO_TogglePin(LED_B_GPIO_Port, LED_B_Pin);
}
//清除中断标志位
__HAL_GPIO_EXTI_CLEAR_IT(K2_Pin);
}
}