目录
GPIO_EventOutputConfig和GPIO_EventOutputCmd
GPIO_PinRemapConfig和GPIO_EXTILineConfig
NVIC_SetVectorTable和NVIC_SystemLPConfig
声明:本专栏是本人跟着B站江科大的视频的学习过程中记录下来的笔记,我之所以记录下来是为了方便自己日后复习。如果你也是跟着江科大的视频学习的,可以配套本专栏食用,如有问题可以QQ交流群:963138186
本节我们来写一下外部中断的代码。
第一个代码:对射式红外传感器计次
先看一下接线图
图中DO数字输出端接到B14端口,当挡光片或者编码盘在这个对射式红外传感器中间经过时,DO就会输出电平跳变的信号,然后这个电平跳变的信号触发STM32 PB14号口的中断,我们在中断函数里执行变量++的程序,然后主循环里调用OLED显示这个变量,这样第一个程序就完成了。
给这节复制一个工程
接下来我们就开始写第一个代码了
一般来说我们对一个模块写模块化程序时,第一个函数都是初始化函数。然后第一步是外部中断配置。
外部中断配置步骤
配置外部中断时,我们要看一下这个图,使用外部中断要配置哪些东西就很清晰了。
把这张图刻在脑子里,以后应用外部中断的时候如果忘记了就回来看看!
简单来说,我们只需要把这个外部中断从GPIO 到NVIC这一路中出现的外设模块都配置好,把这条信号电路给打通就行了。
具体步骤就是:
第一步,配置RCC,把我们这里涉及的外设的时钟都打开,这个别忘了不打开时钟外设是没法工作的。
第二步,配置GPIO ,选择我们的端口为输入模式。
第三步,配置AFIO ,选择我们用的这一路GPIO ,连接到后面的EXTI。
第四步,配置EXTI,选择边沿触发方式,比如上升沿、下降沿或者双边沿,还有选择触发响应方式,可以选择中断响应和事件响应。当然我们一般都是中断响应。
第五步,配置NVIC,给这个中断选择一个合适的优先级。通过NVIC,外部中断信号就能进入CPU。了。这样CPU才能收到中断信号,才能跳转到中断函数里执行中断程序。
这五步就是外部中断的配置流程,这里涉及的外设比较多。
CountSensor.c
第一步,配置RCC
GPIOB和AFIO都是APB2的外设,注意函数和参数要对应起来。如果不确定哪个外设是接在哪个总线上的,可以转到RCC_APB2PeriphClockCmd函数的定义,看一下上面这个参数列表,有的话就是这个总线上的外设。
接着还有EXTI和NVIC两个外设,这两个外设的时钟是一直都打开着的,不需要我们再开启时钟了。EXTI作为一个独立外设,按理说应该是需要开启时钟的,但是寄存器里面却没有EXTI时钟的控制位,具体原因暂时不用管。
另外NVIC也不需要开启时钟,因为NVIC是内核的外设,内核的外设都是不需要开启时钟的,人家跟CPU一起,都是住在皇宫里的,而RCC管的都是内核外的外设,所以RCC管不着NVIC。
到这里时钟就配置完了。
第二步,配置GPIO
GPIO 这一部分我们之前也学过,也还是一样的流程,不再赘述。
注意模式配置的问题:
GPIO_Mode对于外部中断来说,要选择浮空输入,上拉输入或者下拉输入这其中的一个模式。当然像这种其他外设使用GPIO 的情况,如果你不清楚该配置为什么模式,可以看一下这个参考手册GPIO 这一章
找到外设的GPIO配置表,
里面有写每个外设的各个引脚都需要配置为什么模式。
比如最后就有EXTI输入线,它给的推荐配置就是浮空上拉或者下拉。
所以在这里我们可以配置为上拉输入,默认为高电平的输入方式
第三步,配置AFIO
针对AFIO 外设,ST公司并没有给它分配专门的库函数文件,它的库函数是和GPIO 在一个文件里的,即gpio.h文件。
AFIO的库函数
上前那些之前都讲过了,下面这些就是AFIO的库函数:
GPIO_AFIODeInit
这个函数是用来复位AFIO 外设,调用一下这个函数,AFIO 外设的配置就会全部清除。
GPIO_PinLockConfig
这个函数是用来锁定GPIO配置的,调用这个函数,参数指定某个引脚,这个引脚的配置就会被锁定,防止意外更改。这个也是GPIO外设的函数,之前没有讲,这里再提一下,但是用的不多,大家了解一下即可。
GPIO_EventOutputConfig和GPIO_EventOutputCmd
这两函数是用来配置AFIO的事件输出功能的,用的也不多,大家了解一下。
GPIO_PinRemapConfig和GPIO_EXTILineConfig
这两个函数就比较重要了。
GPIO_PinRemapConfig可以用来进行引脚重映射,第一个参数可以选择你要重新设的方式,第二个参数是新的状态。(调用方式以后再演示)。
然后GPIO_EXTILineConfig就是我们本节外部中断需要用的函数,调用这个函数就可以配置AFIO 的数据选择器来选择我们想要的中断引脚。
GPIO_ETH_MediaInterfaceConfig
这个是和以太网有关的,我们这个芯片没有以太网外设,所以也用不到。
我们现在想要配置AFIO 外部中断引脚选择,就直接调用GPIO_EXTILineConfig函数。这个函数虽然是GPIO 开头,但实际上里面操作的是AFIO 的寄存器,所以这个函数实际上是AFIO 的函数。
这个函数的简介写的是选择GPIO _Pin作为外部中断线。
第一个参数GPIO_PortSource,选择某个GPIO外设作为外部中断源,这个参数可以是GPIO_PortSourceGPIOx,其中x可以是A到G。
第二个参数是GPIO_PinSource,指定配置的外设中断线。这个参数可以是GPIO_PinSourcex,其中x可以是0到15。
到这里,AFIO 外设中断引脚选择配置就完成了。
当执行完这个函数后,AFIO 的第14个数据选择器就拨好了。其中输入端被拨到了GPIOB 外设上,对应的就是PB14号引脚,输出端固定连接的是EXTI的第14个中断线路。
这样PB14号引脚的电平信号就可以顺利通过AFIO 进入后级EXTI电路了。
第四步,配置EXTI
EXTI的库函数
我们先看一下EXTI的库函数文件,看一下EXTI都有哪些库函数可以用,我们找到EXTI点h的文件,打开拖到最后,这些就是EXTI的所有库函数
EXTI_DeInit
调用它就可以把EXTI的配置都清除,恢复成上电默认的状态。
EXTI_Init
调用这个函数就可以根据这个结构体里的参数配置EXTI外设,我们初始化EXTI主要用的就是这个函数,使用方法和GPIO_Init也是一样的。
EXTI_StructInit
调用这个函数可以把参数传递的结构体变量赋一个默认值。
这三个函数基本所有的外设都有,就像指库函数的模板函数一样,基本每个外设都需要这些类型的函数。这些模板函数使用方法和意思也都是一样的,会使用一个之后再见到这种函数。就能很容易的上手。所以STM32的库函数是不是比寄存器方便多了,这就是库函数的好处。
EXTI_GenerateSWInterrupt
这个函数是用来软件触发外部中断的,调用这个函数,参数给一个指定的中断线,就能软件触发一次这个外部中断。如果你程序中需要用到这个功能的话,可以使用这个函数。如果你只需要外部引脚触发中断,就不需要用这个函数了。
剩下的四个函数,也是库函数的模板函数,很多模块都有这四个函数,因为在外设运行的过程中,会产生一些状态标志位。比如外部中断来了,会有一个挂起寄存器置了一个标志位,对于其他外设,比如串口收到数据,会置标志位,定时器时间到也会置标志位。这些标志位都是放在状态寄存器的当程序的,当程序想要想要看这些标志位时,就可以用到这四个函数。
EXTI_GetFlagStatus
EXTI_GetFlagStatus可以获取指定的标志位是否被置1了
EXTI_ClearFlag
EXTI_ClearFlag可以对置1的标志位进行清除。
对于这些标志位,有的比较紧急,在置标志位后会触发中断。在中断函数里,如果你想查看标志位和清除标志位,就用下面两个函数。
EXTI_GetITStatus
EXTI_GetITStatus获取中断标志位是否被置1了。
EXTI_ClearITPendingBit
EXTI_ClearITPendingBit清除中断挂起标志位。
总结:如果你想在主程序里查看和清除标志位,就用这两个函数
如果你想在中断函数里查看和清除标志位就用下面这两个函数
其实本质上这四个函数都是对状态寄存器的读写。上面两个和下面两个都是类似的功能,都是读写状态,寄存器,只不过是下面这两个函数,只能读写与中断有关的标志位,并且对中断是否允许做出的判断。而上面的这两个函数只是一般的读写标志位,没有额外的处理。能不能触发中断的标志位都能读取。
所以建议在主程序里用上面两个,中断程序里用下面两个。当然你如果非要在中断里用上面两个,其实也是没问题的。只不过是库函数针对这两种场景,区分了这两类读写函数。
到这里,这个EXTI的库函数就都看完了。库函数的作用记不住没关系,用到的时候再回来翻翻看。
初始化EXTI就用EXTI_Init函数就行了,里面只有一个参数,就是EXTI初始化的结构体。因为EXTI只有一个,所以不需要像GPIO样先指定要配置哪个EXTI了。
这个函数简介写的是根据结构体里面指定的参数(即结构体成员)初始化EXTI外设。
函数的参数是EXTI_InitStruct,它是一个EXTI_InitTypeDef类型的结构体指针,包含了对于EXTI外设的配置信息。
结构体的第一个成员是EXTI_Line,指定我们要配置的中断线。
这些就是这个成员的取值(关于这样查看结构体成员的取值之前也都演示过了,在这里不再赘述,不懂的可以翻之前的博客)
我们需要用PB14所在的第14个线路,所以选择EXTI_Line14
第二个成员是EXTI_LineCmd,指定选择的中断线的新状态,取值可以是enable或disable,我们肯定要开启中断,所以是enable。
接着第三个EXTI_Mode,指定外部中断线的模式,取值可以是这些:
第一个是中断模式。第二个是事件模式,我们要用中断模式。
第四个成员EXTI_Trigger,指定触发信号的有效边沿。这个成员的定义解释有点小错误,这里应该是EXTITrigger_TypeDef
EXTI_Trigger的取值可以是下面这些值
可以选择EXTI_Trigger_Rising上升沿触发、EXTI_Trigger_Falling下降沿触发、EXTI_Trigger_Rising_Falling上升沿和下降沿都触发,根据你的实际需求来,我们就选择falling下降沿触发了。
这样我们的外部中断就配置完成了。我们当前的配置是将EXTI的第14个线路配置为中断模式。
下降沿出发,然后开启中断。这样PB14的电平信号就能通过EXTI通向下一级NVIC了。
第五步配置,NVIC
NVIC的库函数
先看一下库函数文件里的函数,因为NVIC是内核外设,所以它的库函数是被ST发配到杂项这里来了,我们打开misc.h,拖到最后。这里有NVIC的四个函数和SysTick的一个函数。
NVIC_PriorityGroupConfig
这个函数是用来中断分组的,参数是中断分组的方式。
NVIC_Init
根据结构体里面指定的参数初始化NVIC。
NVIC_SetVectorTable和NVIC_SystemLPConfig
NVIC_SetVectorTable设置中断向量表和NVIC_SystemLPConfig系统低功耗配置,这两个函数用的不多,大家可以先不看。
这样这个NVIC的库函数就看完了,只需要用最上面的两个函数就行了。
在配置中断之前,先指定一下中断的分组,然后使用NVIC_Init初始化一下NVIC就行了。
我们调用一下NVIC_PriorityGroupConfig这个函数,它的简介里介绍是配置优先级分组,先占优先级和从占优先级,这里先占优先级pre-emption priority就是抢占优先级,重占优先级subpriority就是响应优先级,英文表达也要熟悉一下。
然后下面这个参数可以取这个列表里的一个值,根据我们的实际需求来的。
一般的话,中断不多很难导致中断冲突,对优先级分组来说就比较随意,哪个都行。
这里我们选择第二个分组,两位抢占两位响应,这个比较平均一些。
另外注意一下这个分组方式,整个芯片只能用一种。所以这个分组的代码,整个工程只需要执行一次就行了。如果你把它放在模块里面进行分组,你要确保每个模块分组都选的是同一个。
也可以把这个代码放在主函数的最开始,这样模块里就不用再进行分组了。这里我们就直接放在模块里进行分组了。
这样分组就完成了。
我们来看一下这个结构体的成员
NVIC_IRQChannel,指定中断通道来开启或关闭。这个成员的取值要到stm32f10x.h里面去找,
但是我们也可以选中它
然后按住Ctrl+F搜索一下,然后把下面的搜索范围由当前文件换成当前工程,然后再搜索。
直接就跳到这个定义了
我们可以在这上面的这个列表选择NVIC_IRQChannel这个成员的取值。
可以看到这里有非常多的中断通道,因为这个库函数可以兼容所有的F1系列芯片,但是不同的芯片中断通道列表是不一样的,所以这里有很多条件编译,用来选择你使用芯片的中断通道列表:
我们可以点一下这左边的减号,把所有的条件编辑都折叠起来。
然后我们芯片用的是md中等密度的,所以只需要展开这个md的条件编译即可,其他的就不用看了。
在这个表里我们就可以找到这个
STM32的EXTI10到EXTI15都是合并到了这个通道里,这个在上一篇也讲过
所以我们复制这一个。然后放到这里,这样通道就指定好了。
NVIC_IRQChannelCmd指定中断通道是使能还是失能,取值可以是enable或者disable,我们选择enable。
接着下面两个成员NVIC_IRQChannelPreemptionPriority和NVIC_IRQChannelSubPriority就是指定所选通道的抢占优先级和响应优先级了。取值可以是0到15,具体的值可以参考这个表里的描述
这里给出了每个分组对应抢占优先级和响应优先级的取值范围。这个表和我们之前看到的这个表是一样的
我们选择了分组2,抢占优先级和响应优先级的取值范围就都是0到3。
因为我们演示的程序的中断只有一个,所以中断优先级的配置也是非常随意的。
这里我们就给抢占优先级和响应优先级都设置为1,优先级是在多个中断源同时申请,产生拥挤时才有作用,这只有一个中断优先级就随便了。
到这里NVIC就配置好了,整个外部中断的配置也就结束了。
外部中断的信号。从GPIO到AFIO 再到EXTI,再到NVIC,最终通向CPU,这样才能让CPU由主程序跳到中断程序执行。
中断函数
接下来就到中断程序部分。
中断函数的名字
在STM32中中断函数的名字都是固定的,每个中断通道都对应一个中断函数。中断函数的名字我们可以参考一下启动文件。我们找到启动文件,打开看一下,这些以EXTI2_IRQHandler结尾的字符就是中断函数的名字。
我们找到这一项,这就是EXTI15-10的中断函数
复制过来作为中断函数的名字,中断函数都是无参无返回值的。
然后在中断函数里,一般都是先进行一个中断标志位的判断,确保是我们想要的中断源触发的这个函数。因为这个函数EXTI10到EXTI15都能进来,所以要先判断一下是不是我们想要的EXTI14进来的,这时候我们就需要到EXTI.h里看一下,调用一下EXTI_GetITStatus函数
EXTI_GetITStatus的第一个参数我们写EXTI_Line14,返回值是set或reset,判断一下返回值是否是set,如果是的话,我们就可以执行中断程序了。
最后,中断程序结束后,一定要再调用一下清除中断标志位的函数。因为只要中断标志位置1了程序就会跳到中断函数。如果你不清除中断标志位,它就会一直申请中断,这样程序就会不断响应中断执行中断函数,程序就卡死在中断函数里了。所以我们每次中断程序结束后,都应该清除一下中断标志位。
调用这个函数就可以清除标志位:
参数我们也写EXTI_Line14,将中断标志位清除,这样中断的全部逻辑,我们就写好了。
注意:中断函数不需要调用,也不需要在头文件声明,它是自动执行的。
接下来我们需要一个数字来统计中断触发的次数。所以定义了一个全部变量
只要触发一次中断,在中断函数里就计数一次
然后写一个函数来返回这个CountSensor_Count记录的总次数
CountSensor.c的完整代码如下:
#include "stm32f10x.h" // Device header
uint16_t CountSensor_Count; //全局变量,用于计数
/**
* 函 数:计数传感器初始化
* 参 数:无
* 返 回 值:无
*/
void CountSensor_Init(void)
{
/*开启时钟*/
//开启GPIOB的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
//开启AFIO的时钟,外部中断必须开启AFIO的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure); //将PB14引脚初始化为上拉输入
/*AFIO选择中断引脚*/
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource14);
//将外部中断的14号线映射到GPIOB,即选择PB14为外部中断引脚
/*EXTI初始化*/
EXTI_InitTypeDef EXTI_InitStructure; //定义结构体变量
EXTI_InitStructure.EXTI_Line = EXTI_Line14; //选择配置外部中断的14号线
EXTI_InitStructure.EXTI_LineCmd = ENABLE; //指定外部中断线使能
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; //指定外部中断线为中断模式
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; //指定外部中断线为下降沿触发
EXTI_Init(&EXTI_InitStructure); //将结构体变量交给EXTI_Init,配置EXTI外设
/*NVIC中断分组*/
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //配置NVIC为分组2
//即抢占优先级范围:0~3,响应优先级范围:0~3
//此分组配置在整个工程中仅需调用一次
//若有多个中断,可以把此代码放在main函数内,while循环之前
//若调用多次配置分组的代码,则后执行的配置会覆盖先执行的配置
/*NVIC配置*/
NVIC_InitTypeDef NVIC_InitStructure; //定义结构体变量
NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn; //选择配置NVIC的EXTI15_10线
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //指定NVIC线路使能
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //指定NVIC线路的抢占优先级为1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //指定NVIC线路的响应优先级为1
NVIC_Init(&NVIC_InitStructure); //将结构体变量交给NVIC_Init,配置NVIC外设
}
/**
* 函 数:获取计数传感器的计数值
* 参 数:无
* 返 回 值:计数值,范围:0~65535
*/
uint16_t CountSensor_Get(void)
{
return CountSensor_Count;
}
/**
* 函 数:EXTI15_10外部中断函数
* 参 数:无
* 返 回 值:无
* 注意事项:此函数为中断函数,无需调用,中断触发后自动执行
* 函数名为预留的指定名称,可以从启动文件复制
* 请确保函数名正确,不能有任何差异,否则中断函数将不能进入
*/
void EXTI15_10_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_Line14) == SET) //判断是否是外部中断14号线触发的中断
{
/*如果出现数据乱跳的现象,可再次判断引脚电平,以避免抖动*/
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_14) == 0)
{
CountSensor_Count ++; //计数值自增一次
}
EXTI_ClearITPendingBit(EXTI_Line14); //清除外部中断14号线的中断标志位
//中断标志位必须清除
//否则中断将连续不断地触发,导致主程序卡死
}
}
CountSensor.h
#ifndef __COUNT_SENSOR_H
#define __COUNT_SENSOR_H
void CountSensor_Init(void);
uint16_t CountSensor_Get(void);
#endif
最后在OLED上显示出来记录的总数
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "CountSensor.h"
int main(void)
{
/*模块初始化*/
OLED_Init(); //OLED初始化
CountSensor_Init(); //计数传感器初始化
/*显示静态字符串*/
OLED_ShowString(1, 1, "Count:"); //1行1列显示字符串Count:
while (1)
{
OLED_ShowNum(1, 7, CountSensor_Get(), 5); //OLED不断刷新显示CountSensor_Get的返回值
}
}
这样运行之后的结果就是:
只要挡一下再移开传感器,数值就加1。我们用的是下降沿触方法,在移开挡光片的时候触发中断。
也可以改下这里变为上升沿出发,
这样就是遮挡的时候数字加1。如果这里改成上升沿下降沿都触发,就是遮挡和移开的时候都加1。
运行结果:
STM32-对射式红外传感器计次
这就是各种触发方式的现象,到这里第一个程序就写完了。
第二个代码:旋转编码器计次
先看一下接线图
如果接线图模糊看不清,可以直接看文件压缩包的接线图,领取链接:
链接:https://pan.baidu.com/s/1C9LkEb7iB9Bl3plfW3b6xw
提取码:ggjl
接好线后复制一个工程文件夹改名
Encoder.c
初始化函数
我们先写一个初始化函数Encoder_Init,在这里面我们初始化一下PB0和PB1两个GPIO口的外部中断。
当然这里只初始化一个外部中断,其实也是可以完成功能的。因为对于这个编码器而言,正向旋转时,A、B相输出的是这样的波形
反向旋转时,输出的是这样的波形
如果把一相的下降沿用作触发中断,在中断时刻读取另一相的电平,则正转时读取到的就是高电平,反转读取到的就是低电平,这样就能区分旋转方向了。
只不过这样在操作上有一些小瑕疵,比如正转的时候,由于A相先出现下降沿,所以刚开始动就进中断了。
而反转是A相后出现下降沿,所以就是转到位了才进入中断。这样实际上也没问题,就是有点不太好。
所以我们准备的就是A、B相都触发中断,只有在B相下降沿和A相低电平时才判断为正转。在A相下降沿和B相低电平时才判断为反转。
这样就能保证正转反转都是转到位了才执行数字加减的操作。
这个旋转编码器的初始化代码和上面初始化对射式红外传感器的初始化代码有点类似,只需要稍微修改一下。
/**
* 函 数:旋转编码器初始化
* 参 数:无
* 返 回 值:无
*/
void Encoder_Init(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //开启GPIOB的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); //开启AFIO的时钟,外部中断必须开启AFIO的时钟
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure); //将PB0和PB1引脚初始化为上拉输入
/*AFIO选择中断引脚*/
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource0);//将外部中断的0号线映射到GPIOB,即选择PB0为外部中断引脚
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource1);//将外部中断的1号线映射到GPIOB,即选择PB1为外部中断引脚
/*EXTI初始化*/
EXTI_InitTypeDef EXTI_InitStructure; //定义结构体变量
EXTI_InitStructure.EXTI_Line = EXTI_Line0 | EXTI_Line1; //选择配置外部中断的0号线和1号线
EXTI_InitStructure.EXTI_LineCmd = ENABLE; //指定外部中断线使能
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; //指定外部中断线为中断模式
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; //指定外部中断线为下降沿触发
EXTI_Init(&EXTI_InitStructure); //将结构体变量交给EXTI_Init,配置EXTI外设
//这样就能同时把第0条线路和第1条线路都初始化为中断模式、下降沿触发了。
/*NVIC中断分组*/
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //配置NVIC为分组2
//即抢占优先级范围:0~3,响应优先级范围:0~3
//此分组配置在整个工程中仅需调用一次
//若有多个中断,可以把此代码放在main函数内,while循环之前
//若调用多次配置分组的代码,则后执行的配置会覆盖先执行的配置
/*NVIC配置*/
NVIC_InitTypeDef NVIC_InitStructure; //定义结构体变量
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn; //选择配置NVIC的EXTI0线
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //指定NVIC线路使能
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //指定NVIC线路的抢占优先级为1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //指定NVIC线路的响应优先级为1
NVIC_Init(&NVIC_InitStructure); //将结构体变量交给NVIC_Init,配置NVIC外设
NVIC_InitStructure.NVIC_IRQChannel = EXTI1_IRQn; //选择配置NVIC的EXTI1线
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //指定NVIC线路使能
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //指定NVIC线路的抢占优先级为1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2; //指定NVIC线路的响应优先级为2
NVIC_Init(&NVIC_InitStructure); //将结构体变量交给NVIC_Init,配置NVIC外设
}
//注:当两个中断同时响应时,抢占优先级谁高谁先中断;
//当抢占优先级相同时,谁的响应优先级高谁先响应;
//当抢占优先级相同时,低响应优先级已经进入中断函数,高响应优先级不可打断低优先级)
/**
* 函 数:旋转编码器获取增量值
* 参 数:无
* 返 回 值:自上此调用此函数后,旋转编码器的增量值
*/
int16_t Encoder_Get(void)
{
/*使用Temp变量作为中继,目的是返回Encoder_Count后将其清零*/
/*在这里,也可以直接返回Encoder_Count
但这样就不是获取增量值的操作方法了
也可以实现功能,只是思路不一样*/
int16_t Temp;
Temp = Encoder_Count;
Encoder_Count = 0;
return Temp;
}
这样初始化函数就改好了,接下来改中断函数
中断函数
注意,因为我们用的是EXTI0和EXTI1这两个中断所以我们需要写两个中断函数,如果你使用的是9~5和15~10这些中断,那就只能写一个中断函数,然后把两个判断标志位的语句并列放到一个函数里就行了。
接下来我们就开始实现功能的代码了
先定义一个全局变量,用于计数旋转编码器的增量值
然后在中断函数里EXTI0里先判断另一个引脚的电平,如果GPIO_Pin_0,也就是A相为0的话就是正转,这是我们一开始自己定义好的,当然你也可以自己定义正反转的标志。如果是正转我们让Encoder_Count记录一次,即Encoder++。
然后我们需要再定义一个函数,把这个计数的这个变量再返回回去。
这里我们就不直接返回Encoder_Count这个变量了,我们返回每次调用这个Encoder_Get函数之后,返回count的变化值,用于外部加减一个变量。
所以在这里需要返回Encoder_Count,然后把Encoder_Count清零。因为返回Encoder_Count之后,函数就结束了,没法清零了。所以先定义一个临时变量temp把Encoder_Count先赋值给temp。再把Encoder_Count清零,最后返回temp,这样就完成了。
Encoder.h
#ifndef __ENCODER_H
#define __ENCODER_H
void Encoder_Init(void);
int16_t Encoder_Get(void);
#endif
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Encoder.h"
int16_t Num; //定义待被旋转编码器调节的变量
int main(void)
{
/*模块初始化*/
OLED_Init(); //OLED初始化
Encoder_Init(); //旋转编码器初始化
/*显示静态字符串*/
OLED_ShowString(1, 1, "Num:"); //1行1列显示字符串Num:
while (1)
{
Num += Encoder_Get(); //获取自上此调用此函数后,旋转编码器的增量值,并将增量值加到Num上
OLED_ShowSignedNum(1, 5, Num, 5); //显示Num
}
}
运行结果:
STM32-旋转编码器计次
关于中断编程的几点建议
最后再给大家提几个中断编程的建议:
第一个就是在这个中断函数里,最好不要执行耗时过长的代码。中断函数要简短快速,别刚进中断就执行一个delay多少毫秒这样的代码。因为中断是处理突发的事情,如果你为了处理一个突发的事情,待在中断里不出来了,主程序就会受到严重的阻塞。
另外就是最好不要在中断函数和主函数调用相同的函数,或者操作同一个硬件,尤其是硬件相关的函数,比如OLED显示函数,如果你既在主函数里调用OLED,又在中断里调用OLED,OLED就会显示错误。因为在主程序里,OLED刚显示一半,然后进中断了,结果中断里还是OLED显示函数,OLED就挪到其他地方显示了,这时还没有问题,但当中断结束之后,需要继续原来的显示,这时就出问题了。因为硬件的显示位置被挪到其他地方来。所以再回来的时候,继续显示的内容就会跟着跑到其他地方去。这就会造成问题。虽然在中断进入和退出的时候,会有保护现场和恢复现场,但这只能保证CPU程序能正常返回不出问题。对于外部硬件的话,并没有在进入中断时进行现场保护,所以中断返回后就出问题了。为了避免这样的问题,就最好不要在主程序和中断程序里操作可能产生冲突的硬件。
在实现功能的时候,可以中断里操作变量或者标志位。当中断返回时,我再对这个变量进行显示和操作。这样既能保证中断函数的简短快速,又能保证不产生冲突的硬件操作。
这就是中断程序设计的注意事项。
在其他地方大家也都可以多用用变量或者标志位来减少代码之间的耦合性,让各部分代码相互独立,仅使用变量标志位或者函数作为接口,这样能让程序更加清晰,代码更加强健
本节外部中断的部分到这里就结束了,下篇继续。
QQ交流群:963138186
本篇就到这里,下篇继续!欢迎点击下方订阅本专栏↓↓↓