学单片机的,相信对中断的概念都已经了如指掌了,中断具体是什么我在这里也就不再详细说明,不懂的上网找找也一大堆。那么在介绍实验之前我先跟大家简单讲讲STM32当中的NVIC(嵌套向量中断控制器)
材料
正点原子探索者开发板,芯片为STM32F407ZGT6
开发板的原理图
STM32CubeMX
keil MDK
NVIC
NVIC(嵌套向量中断控制器)。NVIC就是控制中断响应的。主要由三个参数,一个是中断使能,一个是抢占优先级,还有一个就是响应优先级。(优先级数值越小,优先级别越高)
中断使能很好理解,就是是否开启中断,如果开启中断,则满足中断触发条件时程序会跳到中断服务程序运行,否则不响应中断主程序继续运行。
抢占优先级是用来判断一个中断是否可以打断另外一个中断的中断服务程序抢先运行。例如A中断触发,正在运行A中断的服务程序,此时B中断也触发,如果B中断的抢占优先级比A的高,则程序会打断A的中断服务程序,去运行B的中断服务程序,即中断嵌套。等B的中断服务程序运行完后继续运行A的中断服务程序。如果B的抢占优先级没有高过A的抢占优先级,则程序不会打断A的中断服务程序,而是待定A的中断服务程序运行完成后才运行B的中断服务程序。
响应优先级是用来判断抢占优先级相同的几个中断那个中断会优先响应。如果几个抢占优先相同的中断同时触发,那么响应优先级高的最先运行。
判断中断的优先级,先看抢占优先级,抢占优先级高的中断优先级别高。抢占优先级相同的情况下,响应优先高的中断优先级别高。抢占优先级和响应优先级相同的情况下,更加中断向量表确定。
新建及配置工程
具体新建过程就不详细说了,还不懂的小朋友看《STM32CubeMX实战教程(一)——软件入门》。
那么首先进入时钟的配置,高低速时钟同样是选择外部晶振。
接下来配置一下时钟的分配系数,同样的本次实验对时钟频率要求不高,只需要选择好这两处并将频率配置在允许范围内即可。
那么接下来同样是打开我们STM32开发板的原理图,分别找到按键和LED的位置,可以看到有两个可编程LED,和四个按键,从中我们可以获得几点信息。
led接了上拉电阻,低电平导通
三个按键下拉,低电平有效
一个按键上拉,高电平有效
此处先不管WK_UP,也就是其余三个按键在按下的一瞬间均会产生一个下降沿,所以引脚应该配置成下降沿触发的中断
另外找到芯片对应的引脚号,LED分别是PF9、PF10,按键是PE2
二话不说,把PF9、PF10配置成GPIO_Output,把PE2配置成GPIO_EXIT2也就是外部中断模式,这里的2是指该GPIO是挂载在中断线2上的
然后进入GPIO配置界面,LED的配置就不多说了,不懂的可以看回《STM32CubeMX实战教程(二)——按键点个灯》接下来开始中断引脚PE2的配置,可以看到在PE2的配置中有三栏,其他两栏都已讲过,而这第一栏看得就有点晕头转向,点开GPIO mode居然有六个选项,其实这已经涵盖了所有外部中断事件的触发类型
上升沿触发的外部中断
下降沿触发的外部中断
上升/下降沿触发的外部中断
上升沿触发的事件中断
下降沿触发的事件中断
上升/下降沿触发的事件中断
我们在这里选择的是第二项
可能现在大家对事件中断和外部中断有什么区别还不太清楚,我这里就先简单讲一下,不懂也没关系,在后面的教程中我们将在实践中学习
外部中断和事件中断
从外部激励信号来看,中断和事件的产生源都可以是一样的。之所以分成2个部分,由于中断是需要CPU参与的,需要软件的中断服务函数才能完成中断后产生的结果;但是事件,是靠脉冲发生器产生一个脉冲,进而由硬件自动完成这个事件产生的结果,当然相应的联动部件需要先设置好, 比如引起DMA操作,AD转换等;
简单举例:外部I/O触发AD转换,来测量外部物品的重量;如果使用传统的中断通道,需要I/O触发产生外部中断,外部中断服务程序启动AD转换,AD转换完成中断服务程序提交最后结果;要是使用事件通道,I/O触发产生事件,然后联动触发AD转换,AD转换完成中断服务程序提交最后结果;相比之下,后者不要软件参与AD触发,并且响应速度也更块;要是使用事件触发DMA操作,就完全不用软件参与就可以完成某些联动任务了。
NVIC配置
配置完成GPIO之后我们进入NVIC的配置,可以直接点击GPIO界面内的NVIC来进入GPIO的中断控制器或是System Core下面的NVIC,进入全局中断控制器这里我先简要介绍一下该界面的功能
Priority Group是优先级分组 STM32以4个比特位表示中断的抢占优先级和响应优先级。中断优先级分组是为了给抢占式优先级和响应优先级在中断优先级寄丛器的四个比特位分配各个优先级数字所占的位数。例如3位用于抢占优先级(优先级有23=8种优先级),1位用于响应优先级(优先级有21=2种优先级)。这里因为只用到一个中断,所有如何设置问题都不大
下面就是所有中断的使能及相应优先级设置,前面几个默认已经使能的为系统必须的中断,不可取消,另外我们这里需要对外部中断线2进行使能,这样才能进入中断服务函数进行中断处理。
进入代码
配置完这些之后,目前来看是没什么问题了,那么生成代码,编译,认领两个鸭蛋。同样是来到main.h这里看一眼,几下已经我们定义好的引脚号。
/* Private defines -----------------------------------------------------------*/#define Key_Pin GPIO_PIN_2#define Key_GPIO_Port GPIOE#define Key2_EXTI_IRQn EXTI2_IRQn#define LED0_Pin GPIO_PIN_9#define LED0_GPIO_Port GPIOF#define LED1_Pin GPIO_PIN_10#define LED1_GPIO_Port GPIOF
回调函数
在写代码前,先来介绍一下回调函数的概念,用过STM32中断的应该都知道,单片机再执行中断服务函数之前,Contex-M4内核先将现在使用到的寄存器和主程序中断点的地址压入堆栈(保护现场)。然后程序在中断向量表中找到中断对应的地址。这个地址存储的为中断服务函数的入口地址。然后程序转跳到中断服务函数执行。
那么我们怎么去找到这个中断服务函数呢,这就需要我们打开32的启动文件,翻到这个位置。
显然,这里都是各种中断服务函数的句柄,那么哪个才是我们要的呢,很明显我们刚刚使能的是EXIT2,所以这里对应的就是EXIT2_IRQHandler,而在用STM32CubeMX开发的过程中,我们并不需要去写这么一个函数,这也是与标准库所不同的地方。在代码生成的过程中STM32CubeMX已经帮我们写好了,现在,我们可以右键go to the definition去看看到底帮我们写了什么。
于是,我们便进入了stm32f4xx_it.c这个文件里面,那么本次实验的代码也将在这里完成,那么是不是将我们需要的中断服务内容添加到里面就可以了呢。别急,还没看完呢,它又调用了一个 HAL_GPIO_EXTI_IRQHandler 这个函数,难道就不好奇里面是什么?带着这份好奇,我们接着往里追。
紧接着就来到了这里,很明显在清楚标志位后又进入了一个
HAL_GPIO_EXTI_Callback(GPIO_Pin) 这个函数,这里就有一点需要注意了,它已经把本该是我们要去清除的标志位给清除了,也就是说,我们在使用STM32CubeMX开发的过程中,使用的任何中断都不需要去关心标志位的问题。这也可以很明显的反应出一点,就是STM32CubeMX在试图让我们少碰,或是不需要关心32开发过程中硬件层的问题,只用做好应用层的开发就足够了,这其实是一大进步。
至于 HAL_GPIO_EXTI_Callback(GPIO_Pin) 呢,就是传说中的回调函数了,好我们进去看看。
看似一个很简单的函数却有着两个从没见过的用法,其实也很简单,__weak 是一个弱化标识,带有这个的函数就是一个弱化函数,什么意思呢,就是你可以在其他地方写一个名称和参数都一模一样的函数,编译器就会忽略这一个函数,而去执行你写的那个函数;而UNUSED(GPIO_Pin) ,这就是一个防报错的定义,当传进来的GPIO端口号没有做任何处理的时候,编译器也不会报出警告。
到这里,我就分析的一遍回调函数,其实总结一下就是:我们在开发的时候已经不需要去理会中断服务函数了,只需要找到这个中断回调函数并将其重写即可
而这个回调函数还有一点非常便利的地方这里没有体现出来,就是当同时有多个中断使能的时候,STM32CubeMX会自动地将几个中断的服务函数规整到一起并调用一个回调函数,也就是无论几个中断,我们只需要重写一个回调函数并判断传进来的端口号即可,还是非常方便的。
那么接下来我们就在stm32f4xx_it.c这个文件的最下面添加这段代码
/* USER CODE BEGIN 1 */void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin){ if(GPIO_Pin==Key_Pin) { HAL_Delay(100); if(HAL_GPIO_ReadPin(Key_Pin_Port,Key_Pin)==0) { HAL_GPIO_TogglePin(LED1_GPIO_Port,LED1_Pin); HAL_GPIO_TogglePin(LED0_GPIO_Port,LED0_Pin); } }}/* USER CODE END 1 */
重写回调函数
判断端口号
延时消抖
翻转LED状态
代码很简单,一目了然,编译一遍,没有任何问题,那么是不是就结束了呢,还为时过早,现在下载进单片机只会让它在按键按下后没有任何反应,这又是为什么呢,其实问题出在HAL_Delay()上
HAL_Delay()分析
这时候下载的代码,用keil单步执行不难看出,在进入回调函数之后就一直在HAL_Delay陷入了死循环中,怎么也出不来,小小的延时函数为什么会出现这种情况呢?百度一下HAL_Delay函数卡死,相关帖子层出不穷,真正说出原因并提供解决方法的几乎没有。但其实原因并不难,进来HAL_Delay函数看看就知道。
/** * @brief This function provides minimum delay (in milliseconds) based * on variable incremented. * @note In the default implementation , SysTick timer is the source of time base. * It is used to generate interrupts at regular time intervals where uwTick * is incremented. * @note This function is declared as __weak to be overwritten in case of other * implementations in user file. * @param Delay specifies the delay time length, in milliseconds. * @retval None */__weak void HAL_Delay(uint32_t Delay){ uint32_t tickstart = HAL_GetTick(); uint32_t wait = Delay; /* Add a freq to guarantee minimum wait */ if (wait 《 HAL_MAX_DELAY) { wait += (uint32_t)(uwTickFreq); } while((HAL_GetTick() - tickstart) 《 wait) { }}
这是延时函数的原型,同样,有着 __weak 的标识,意味着你可以自己写一个延时函数并用同样的函数名。但这不是重点,可以看到函数上方有说明,第二条注意事项清楚地说道:计时器的时基来源是 SysTick定时器,且在固定时间间隔内产生中断。Interrupt这一词是问题的关键,既然是中断,势必就有优先级,如果在中断里面HAL_Delay会卡死,而main函数则不会,那么有没有可能是Systick优先级太低造成的呢。带着这个问题我们回到STM32CubeMX中重新找到NVIC。
这时候我们注意到在默认使能的中断里面有一个System tick的中断,这就是给HAL_Delay函数提供时基的定时器中断了。显然,这个中断的抢占优先级和外部中断的抢占优先级是一样的,那么在外部中断触发时肯定不能接着触发systick中断了,问题已经找到,只需要简单地将外部中断的抢占优先级改低即可。
下载验证
重新生成代码编译就可以下载验证了,最后我的工程已上传,工程与讲解有些许不同,是初始化了三个按键的外部中断
非常抱歉由于CSDN官网上传的资源必须要设定积分,否则几乎无法通过审核,这里就没有办法免费开发给大家,不过源码在教程里已经非常详细了。
结语
非常感谢大家的阅读,如有不当或者错误的地方,欢迎指正,谢谢支持。小编一个字一个字敲出来不容易,如果觉对你有帮助的,别忘了点个赞~
祝大家事业蒸蒸日上!
学单片机的,相信对中断的概念都已经了如指掌了,中断具体是什么我在这里也就不再详细说明,不懂的上网找找也一大堆。那么在介绍实验之前我先跟大家简单讲讲STM32当中的NVIC(嵌套向量中断控制器)
材料
正点原子探索者开发板,芯片为STM32F407ZGT6
开发板的原理图
STM32CubeMX
keil MDK
NVIC
NVIC(嵌套向量中断控制器)。NVIC就是控制中断响应的。主要由三个参数,一个是中断使能,一个是抢占优先级,还有一个就是响应优先级。(优先级数值越小,优先级别越高)
中断使能很好理解,就是是否开启中断,如果开启中断,则满足中断触发条件时程序会跳到中断服务程序运行,否则不响应中断主程序继续运行。
抢占优先级是用来判断一个中断是否可以打断另外一个中断的中断服务程序抢先运行。例如A中断触发,正在运行A中断的服务程序,此时B中断也触发,如果B中断的抢占优先级比A的高,则程序会打断A的中断服务程序,去运行B的中断服务程序,即中断嵌套。等B的中断服务程序运行完后继续运行A的中断服务程序。如果B的抢占优先级没有高过A的抢占优先级,则程序不会打断A的中断服务程序,而是待定A的中断服务程序运行完成后才运行B的中断服务程序。
响应优先级是用来判断抢占优先级相同的几个中断那个中断会优先响应。如果几个抢占优先相同的中断同时触发,那么响应优先级高的最先运行。
判断中断的优先级,先看抢占优先级,抢占优先级高的中断优先级别高。抢占优先级相同的情况下,响应优先高的中断优先级别高。抢占优先级和响应优先级相同的情况下,更加中断向量表确定。
新建及配置工程
具体新建过程就不详细说了,还不懂的小朋友看《STM32CubeMX实战教程(一)——软件入门》。
那么首先进入时钟的配置,高低速时钟同样是选择外部晶振。
接下来配置一下时钟的分配系数,同样的本次实验对时钟频率要求不高,只需要选择好这两处并将频率配置在允许范围内即可。
那么接下来同样是打开我们STM32开发板的原理图,分别找到按键和LED的位置,可以看到有两个可编程LED,和四个按键,从中我们可以获得几点信息。
led接了上拉电阻,低电平导通
三个按键下拉,低电平有效
一个按键上拉,高电平有效
此处先不管WK_UP,也就是其余三个按键在按下的一瞬间均会产生一个下降沿,所以引脚应该配置成下降沿触发的中断
另外找到芯片对应的引脚号,LED分别是PF9、PF10,按键是PE2
二话不说,把PF9、PF10配置成GPIO_Output,把PE2配置成GPIO_EXIT2也就是外部中断模式,这里的2是指该GPIO是挂载在中断线2上的
然后进入GPIO配置界面,LED的配置就不多说了,不懂的可以看回《STM32CubeMX实战教程(二)——按键点个灯》接下来开始中断引脚PE2的配置,可以看到在PE2的配置中有三栏,其他两栏都已讲过,而这第一栏看得就有点晕头转向,点开GPIO mode居然有六个选项,其实这已经涵盖了所有外部中断事件的触发类型
上升沿触发的外部中断
下降沿触发的外部中断
上升/下降沿触发的外部中断
上升沿触发的事件中断
下降沿触发的事件中断
上升/下降沿触发的事件中断
我们在这里选择的是第二项
可能现在大家对事件中断和外部中断有什么区别还不太清楚,我这里就先简单讲一下,不懂也没关系,在后面的教程中我们将在实践中学习
外部中断和事件中断
从外部激励信号来看,中断和事件的产生源都可以是一样的。之所以分成2个部分,由于中断是需要CPU参与的,需要软件的中断服务函数才能完成中断后产生的结果;但是事件,是靠脉冲发生器产生一个脉冲,进而由硬件自动完成这个事件产生的结果,当然相应的联动部件需要先设置好, 比如引起DMA操作,AD转换等;
简单举例:外部I/O触发AD转换,来测量外部物品的重量;如果使用传统的中断通道,需要I/O触发产生外部中断,外部中断服务程序启动AD转换,AD转换完成中断服务程序提交最后结果;要是使用事件通道,I/O触发产生事件,然后联动触发AD转换,AD转换完成中断服务程序提交最后结果;相比之下,后者不要软件参与AD触发,并且响应速度也更块;要是使用事件触发DMA操作,就完全不用软件参与就可以完成某些联动任务了。
NVIC配置
配置完成GPIO之后我们进入NVIC的配置,可以直接点击GPIO界面内的NVIC来进入GPIO的中断控制器或是System Core下面的NVIC,进入全局中断控制器这里我先简要介绍一下该界面的功能
Priority Group是优先级分组 STM32以4个比特位表示中断的抢占优先级和响应优先级。中断优先级分组是为了给抢占式优先级和响应优先级在中断优先级寄丛器的四个比特位分配各个优先级数字所占的位数。例如3位用于抢占优先级(优先级有23=8种优先级),1位用于响应优先级(优先级有21=2种优先级)。这里因为只用到一个中断,所有如何设置问题都不大
下面就是所有中断的使能及相应优先级设置,前面几个默认已经使能的为系统必须的中断,不可取消,另外我们这里需要对外部中断线2进行使能,这样才能进入中断服务函数进行中断处理。
进入代码
配置完这些之后,目前来看是没什么问题了,那么生成代码,编译,认领两个鸭蛋。同样是来到main.h这里看一眼,几下已经我们定义好的引脚号。
/* Private defines -----------------------------------------------------------*/#define Key_Pin GPIO_PIN_2#define Key_GPIO_Port GPIOE#define Key2_EXTI_IRQn EXTI2_IRQn#define LED0_Pin GPIO_PIN_9#define LED0_GPIO_Port GPIOF#define LED1_Pin GPIO_PIN_10#define LED1_GPIO_Port GPIOF
回调函数
在写代码前,先来介绍一下回调函数的概念,用过STM32中断的应该都知道,单片机再执行中断服务函数之前,Contex-M4内核先将现在使用到的寄存器和主程序中断点的地址压入堆栈(保护现场)。然后程序在中断向量表中找到中断对应的地址。这个地址存储的为中断服务函数的入口地址。然后程序转跳到中断服务函数执行。
那么我们怎么去找到这个中断服务函数呢,这就需要我们打开32的启动文件,翻到这个位置。
显然,这里都是各种中断服务函数的句柄,那么哪个才是我们要的呢,很明显我们刚刚使能的是EXIT2,所以这里对应的就是EXIT2_IRQHandler,而在用STM32CubeMX开发的过程中,我们并不需要去写这么一个函数,这也是与标准库所不同的地方。在代码生成的过程中STM32CubeMX已经帮我们写好了,现在,我们可以右键go to the definition去看看到底帮我们写了什么。
于是,我们便进入了stm32f4xx_it.c这个文件里面,那么本次实验的代码也将在这里完成,那么是不是将我们需要的中断服务内容添加到里面就可以了呢。别急,还没看完呢,它又调用了一个 HAL_GPIO_EXTI_IRQHandler 这个函数,难道就不好奇里面是什么?带着这份好奇,我们接着往里追。
紧接着就来到了这里,很明显在清楚标志位后又进入了一个
HAL_GPIO_EXTI_Callback(GPIO_Pin) 这个函数,这里就有一点需要注意了,它已经把本该是我们要去清除的标志位给清除了,也就是说,我们在使用STM32CubeMX开发的过程中,使用的任何中断都不需要去关心标志位的问题。这也可以很明显的反应出一点,就是STM32CubeMX在试图让我们少碰,或是不需要关心32开发过程中硬件层的问题,只用做好应用层的开发就足够了,这其实是一大进步。
至于 HAL_GPIO_EXTI_Callback(GPIO_Pin) 呢,就是传说中的回调函数了,好我们进去看看。
看似一个很简单的函数却有着两个从没见过的用法,其实也很简单,__weak 是一个弱化标识,带有这个的函数就是一个弱化函数,什么意思呢,就是你可以在其他地方写一个名称和参数都一模一样的函数,编译器就会忽略这一个函数,而去执行你写的那个函数;而UNUSED(GPIO_Pin) ,这就是一个防报错的定义,当传进来的GPIO端口号没有做任何处理的时候,编译器也不会报出警告。
到这里,我就分析的一遍回调函数,其实总结一下就是:我们在开发的时候已经不需要去理会中断服务函数了,只需要找到这个中断回调函数并将其重写即可
而这个回调函数还有一点非常便利的地方这里没有体现出来,就是当同时有多个中断使能的时候,STM32CubeMX会自动地将几个中断的服务函数规整到一起并调用一个回调函数,也就是无论几个中断,我们只需要重写一个回调函数并判断传进来的端口号即可,还是非常方便的。
那么接下来我们就在stm32f4xx_it.c这个文件的最下面添加这段代码
/* USER CODE BEGIN 1 */void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin){ if(GPIO_Pin==Key_Pin) { HAL_Delay(100); if(HAL_GPIO_ReadPin(Key_Pin_Port,Key_Pin)==0) { HAL_GPIO_TogglePin(LED1_GPIO_Port,LED1_Pin); HAL_GPIO_TogglePin(LED0_GPIO_Port,LED0_Pin); } }}/* USER CODE END 1 */
重写回调函数
判断端口号
延时消抖
翻转LED状态
代码很简单,一目了然,编译一遍,没有任何问题,那么是不是就结束了呢,还为时过早,现在下载进单片机只会让它在按键按下后没有任何反应,这又是为什么呢,其实问题出在HAL_Delay()上
HAL_Delay()分析
这时候下载的代码,用keil单步执行不难看出,在进入回调函数之后就一直在HAL_Delay陷入了死循环中,怎么也出不来,小小的延时函数为什么会出现这种情况呢?百度一下HAL_Delay函数卡死,相关帖子层出不穷,真正说出原因并提供解决方法的几乎没有。但其实原因并不难,进来HAL_Delay函数看看就知道。
/** * @brief This function provides minimum delay (in milliseconds) based * on variable incremented. * @note In the default implementation , SysTick timer is the source of time base. * It is used to generate interrupts at regular time intervals where uwTick * is incremented. * @note This function is declared as __weak to be overwritten in case of other * implementations in user file. * @param Delay specifies the delay time length, in milliseconds. * @retval None */__weak void HAL_Delay(uint32_t Delay){ uint32_t tickstart = HAL_GetTick(); uint32_t wait = Delay; /* Add a freq to guarantee minimum wait */ if (wait 《 HAL_MAX_DELAY) { wait += (uint32_t)(uwTickFreq); } while((HAL_GetTick() - tickstart) 《 wait) { }}
这是延时函数的原型,同样,有着 __weak 的标识,意味着你可以自己写一个延时函数并用同样的函数名。但这不是重点,可以看到函数上方有说明,第二条注意事项清楚地说道:计时器的时基来源是 SysTick定时器,且在固定时间间隔内产生中断。Interrupt这一词是问题的关键,既然是中断,势必就有优先级,如果在中断里面HAL_Delay会卡死,而main函数则不会,那么有没有可能是Systick优先级太低造成的呢。带着这个问题我们回到STM32CubeMX中重新找到NVIC。
这时候我们注意到在默认使能的中断里面有一个System tick的中断,这就是给HAL_Delay函数提供时基的定时器中断了。显然,这个中断的抢占优先级和外部中断的抢占优先级是一样的,那么在外部中断触发时肯定不能接着触发systick中断了,问题已经找到,只需要简单地将外部中断的抢占优先级改低即可。
下载验证
重新生成代码编译就可以下载验证了,最后我的工程已上传,工程与讲解有些许不同,是初始化了三个按键的外部中断
非常抱歉由于CSDN官网上传的资源必须要设定积分,否则几乎无法通过审核,这里就没有办法免费开发给大家,不过源码在教程里已经非常详细了。
结语
非常感谢大家的阅读,如有不当或者错误的地方,欢迎指正,谢谢支持。小编一个字一个字敲出来不容易,如果觉对你有帮助的,别忘了点个赞~
祝大家事业蒸蒸日上!
举报