STM32EXTI外部中断实战:对射式红外传感器和旋转编码器的计次应用(第5篇)

目录

一、中断系统

1、中断介绍

2、中断执行流程

二、STM32中断

1、STM32中断介绍

2、NVIC的基本结构

3、NVIC优先级分组

4、EXTI简介

5、EXTI基本结构

6、AFIO复用IO口

7、EXTI内部框图

三、硬件模块 

1、使用外部中断的硬件特性

2、旋转编码器

(1)旋转编码器介绍

(2)旋转编码器硬件电路

四、对射式红外传感器计次

1、硬件接线图 

2、AFIO外设常用函数

3、EXTI外设常用函数

4、NVIC外设常用函数

5、程序

6、实物展示

五、旋转编码器计次

1、硬件接线图

2、程序

3、实物展示


一、中断系统

1、中断介绍

中断的定义:在主程序运行过程中,出现了特定的中断触发条件(中断源),使得CPU暂停当前正在运行的程序,转而去处理中断程序,处理完成后又返回原来被暂停的位置继续运行,这个过程称为中断。

这里的中断触发条件,对于外部中断来说,可以是引脚发生了电平跳变,对于定时器来说,可以是定时的时间到了,对于串口通信来说,可以是接收到了数据。

当这些事件发生时,情况会比较紧急,比如外部中断来了,如果不处理,下一个跳变信号就跟着过来了,比如串口接收中断,如果不来读取接收到的数据,那下一个数据再过来,就会把原来的数据覆盖掉,所以我们希望当中断条件满足时,CPU能够立即停下当前执行的程序,转而去处理这些中断事件的程序。

所以如果外部中断来了,想要计次,那就变量++,串口中断来了,就把接收到的数据转存起来,当这些紧急事件处理完成后,CPU还能回到原来停下的地方继续运行,这就是中断的处理流程和用途。

使用中断系统,能够极大地提高程序的效率,如果没有中断系统,为了防止外部中断被忽略或者串口数据被覆  盖,那主程序就只能不断地查询是否有这些事件发生,不能再干其他事情了,比如如果没有定时器中断,那主程序只有靠Delay函数,才能实现定时的功能,但有了中断系统,主程序就可以放心执行其他事情,有中断的时候再去处理,这样效率就会大大提升。

中断优先级:当有多个中断源同时申请中断时,CPU会根据中断源的轻重缓急进行判断,优先响应更加紧急的中断源。

中断嵌套:当一个中断程序正在运行时,又有新的更高优先级的中断源申请中断,CPU再次暂停当前中断程序,转而去处理新的中断程序,处理完成后依次进行返回。

2、中断执行流程

左边的是中断程序的执行流程。主程序执行到某个地方,外设的中断条件满足时,无论主程序是在干什么事情,主程序都得立即暂停,程序由硬件电路自动跳转到中断程序中,当中断程序执行完后,程序再返回到被暂停的地方继续运行,这个被暂停的地方,我们就称它为断点。

为了程序能在中断返回后继续原来的工作,在中断执行前,会对程序的现场进行保护,中断执行完后,会再还原现场,这样可以保证主程序即使被中断后,回来之后也能继续运行。

我们用C语言编程时,保护现场和还原现场并不需要我们来做,编译器会自动帮我们做好。

右边这个图就是中断嵌套的执行流程。当主程序执行到断点,进入中断程序,这时又有优先级更高的中断来了,在中断程序中,又会再次打断,进入新的中断程序,新的中断程序执行结束后,在继续原来的中断,原来的中断程序结束,在继续主程序。

带有中断的程序如下图所示,上面是主函数,while(1)死循环里就是主程序,正常情况下,程序就是在主程序里不断循环执行,当中断条件满足时,主程序就会暂停,然后跳转到下面的中断程序中,中断程序执行完后,再返回主程序里继续执行。


二、STM32中断

1、STM32中断介绍

在STM32中,有68个可屏蔽中断通道(中断源),包含EXTI外部中断、TIM定时器、ADC模数转换器、USART串口、SPI通信、I2C通信、RTC实时时钟等多个外设。

STM32使用NVIC统一管理中断,每个中断通道都拥有16个可编程的优先等级,还可对优先级进行分组,进一步设置抢占优先级和响应优先级。

下面这个图就是STM32里的中断资源,灰色部分是内核的中断,其他部分就是STM32外设的中断了,内核部分的中断我们很少用到,了解即可。

下面来看看STM32外设的部分中断:

        第0个WWDG,窗口看门狗,这个是用来监测程序运行状态的中断,比如程序卡死了,没有及时喂狗,窗口看门狗就会申请中断,让程序跳到窗口看门狗的中断程序里,在中断程序中,就可以进行一些错误检查,看看出了什么问题了。

        第1个PVD电源电压监测,如果STM32供电电压不足,PVD电路就会申请中断,在中断程序中,就能知道现在供电不足,电池没电了,要赶紧保存一下重要资料。

其他中断也是类似的功能。

其中EXTI0~EXTI4,EXTI9_5,EXTI15_10,就是我们要学的外部中断对应的中断资源。

        这个表的最右边有一个中断的地址,由于我们程序中的中断函数,它的地址是由编译器来分配的,是不固定的,但是我们的中断跳转,由于硬件的限制,只能跳到固定的地址执行程序,所以为了能让硬件跳转到一个不固定的中断程序里,这里就需要在内存中,定义一个地址的列表。
        这个列表地址是固定的,中断发生后,就跳到这个固定的位置,然后在这个固定位置,由编译器,再加上一条跳转到中断函数的代码,这样中断跳转就可以跳转到任意位置了,这个中断地址的列表,就叫中断向量表,相当于中断跳转的一个跳板。

2、NVIC的基本结构

NVIC的名字叫做嵌套中断向量控制器,在STM32中,它是用来统一分配中断优先级和管理中断的,NVIC是一个内核外设,是CPU的小助手。

由于SMT32的中断非常多,如果把这些中断全都接到CPU上,那CPU还得引出很多线进行适配,设计上就很麻烦,并且如果很多中断同时申请,或者中断很多产生了拥堵,CPU也会很难处理,毕竟CPU主要是用来运算的,所以中断分配的任务尽量放到其他地方,如NVIC。

NVIC有很多输入口,有多少个中断线路,都可以接过来,比如图中所示,可以接到EXTI、TIM、ADC、USART等等,接线上划了一个/,上面有一个n,这个意思是一个外设可能会同时占用多个中断通道,所以这里有n条线。

NVIC只有一个输出口,NVIC会根据每个中断的优先级分配中断的先后顺序,之后,通过右边这一个输出口就告诉CPU,你该处理哪一个中断。对于中断先后顺序分配的任务,CPU不需要知道。

举个例子,如果CPU是一个医生,如果医院只有医生的话,当看病的人很多时,医生就得安排一下先看谁,后看谁,如果有紧急的病人,那还得先让紧急的病人最先来,这个安排先后次序的任务很繁琐,会影响医生看病的效率,所以医院就安排了一个叫号系统,来病人了,统一取号,并且根据病人的情况,分配一个看病的优先级,然后叫号系统看一下现在在排队的病人,优先叫号紧急情况的病人,最后叫号系统给医生输出的就是一个一个排好队的病人,医生就可以专心看病了,这里的叫号系统,就是NVIC。

3、NVIC优先级分组

为了处理不同形式的优先级,STM32的NVIC可以对优先级进行分组,分为抢占优先级和响应优先级,这两者有何区别,下面我们举个例子来说明。

对于紧急的病人,其实有两种形式的优先,一种是当一个病人正在看病,外面排队排了很多病人,当这个病人看完病后,紧急的病人即使是后来的,也会最先进去看病,这种相当于插队的优先级,就叫响应优先级,响应优先级高的,可以插队提前看病。

另外一种情况是如果这个病人非常紧急,并且此时已经正好有人在看病了,那他可以不等这个病人看完病,就可以直接冲到医生的屋里,让正在看病的病人先靠边站,先给这个情况非常紧急的人看病,等看完了,再让之前看病被暂停的人,继续看病,这种形式的优先,就是之前的中断嵌套,这种决定是否可以中断嵌套的优先级,就叫抢占优先级,抢占优先级高的,可以进行中断嵌套。

每个中断有16个优先级,为了把这个优先级再区分为抢占优先级和响应优先级,就需要对这16个优先级进行分组。

NVIC的中断优先级由优先级寄存器的4位二进制数(0~15)决定,这个优先级的数是值越小吗,优先级越高,0就是最高优先级,这4位可以进行切分,分为高n位的抢占优先级和底4-n位的响应优先级。总共4位,切分的可能情况有(0,4)、(1,3)、(2,2)、(3,1)、(4,0),如下图所示。

分组0就是抢占优先级分配0位,只能取值0,而响应优先级分配4位,取值可以是0~15;
分组1就是抢占优先级分配1位,能取值0~1,而响应优先级分配3位,取值可以是0~7;
分组2就是抢占优先级分配2位,能取值0~3,而响应优先级分配2位,取值可以是0~3;
分组3就是抢占优先级分配3位,能取值0~7,而响应优先级分配1位,取值可以是0~1;
分组4就是抢占优先级分配4位,能取值0~15,而响应优先级分配0位,取值只能是0。

这个分组方式是在程序中我们自己来选择的,选好分组方式以后,在配置优先级的时候,就要注意抢占优先级和响应优先级的取值范围了,不能超出表里规定的取值范围。

抢占优先级高的可以中断嵌套,响应优先级高的可以优先排队,抢占优先级和响应优先级均相同的,按中断号排队,这里的中断号是指上面中断向量表的第二列数字。

4、EXTI简介

EXTI(Extern Interrupt)外部中断

EXTI可以监测指定GPIO口的电平信号,当其指定的GPIO口产生电平变化时,EXTI将立即向NVIC发出中断申请,经过NVIC判断后,即可中断CPU主程序,使CPU执行EXTI对应的中断程序。

支持的触发方式:
上升沿——电平从低电平变到高电平的瞬间触发中断
下降沿——电平从高电平变到低电平的瞬间触发中断
双边沿——上升沿和下降沿都可以出发中断
软件触发——通过编程来在指定位置触发中断

外部中断支持的GPIO口:支持所有的GPIO口,即任意的GPIO口都可以当做外部中断的引脚,但是相同的Pin不能同时触发中断(PA0、PB0、PC0不能同时触发中断)。

外部中断占用的通道数:16个GPIO_Pin,这就对应GPIO_Pin_0到GPIO_Pin_15,外加PVD输出、RTC闹钟,USB唤醒、以太网唤醒,加起来总共有20个中断线路。
这里的16个GPIO_Pin是外部中断的主要功能,后面四个都是过来“蹭网”的,这是因为外部中断有个功能,就是从低功耗模式的停止模式下唤醒STM32,那对于PVD电源电压监测,当电源从电压过低恢复时,就需要PVD借助一下外部中断退出停止模式;对于RTC闹钟来说,有时候为了省电,RTC定一个闹钟之后,STM32会进入停止模式,等到闹钟响的时候再唤醒,这时候也需要借助外部中断,剩下的USB唤醒和以太网响应也是类似的作用。

外部中断的触发响应方式:
中断响应——申请中断,让CPU执行中断函数
事件响应——STM32对外部中断增加的一种额外功能,当外部中断监测到引脚电平变化时,正常的流程是选择触发中断,但是在STM32中,也可以选择触发一个事件,如果选择触发事件,那外部中断的信号就不会通向CPU了,而是通向其他外设,用来触发其他外设的操作,比如触发ADC转换、触发DMA等。

5、EXTI基本结构

上图就是外部中断的整体,最左边是GPIO口的外设,如GPIOA、GPIOB、GPIOC等等,每个GPIO外设有16个引脚,所以进来16根线,但是EXTI模块只有16个GPIO的通道,而这里每个GPIO外设都有16个引脚,如果每个引脚占用一个通道,那EXTI的16个通道显然就不够用了,因此在GPIO外设和EXTI之间有一个AFIO中断引脚选择的电路模块。

AFIO就是一个数据选择器,它可以在这前面的若干个GPIO外设的16个引脚里选择其中一个连接到后面EXTI的对应通道里,如选择了PA0,则将PA0街道EXTI的通道0上,选择了PC5,则将PC5接到EXTI的通道5上,所以这就是为什么相同的Pin不能同时触发中断,因为EXTI在相同的Pin中,只选择了其中一个连接。

然后经过AFIO选择之后的16个通道,就接到了EXTI边沿检测及控制电路上,同时,下面这四个蹭网的外设,也是并列连接进来的,这些加起来就组成了EXTI的20个输入信号。

然后经过EXTI电路之后,分为了两种输出,上面的大部分接到了NVIC,是用来触发中断的,本来20路输入,应该有20路中段的输出,但是ST公司可能觉得20个输出太多了,比较占用NVIC的通道资源,就把其中外部中断的9~5和15~10,分别分到了一个通道里,也就是说,外部中断的9~5会触发同一个中断函数,15~10也会触发同一个中断函数,在编程的时候,在这两个中断函数里,需要再根据标志位来区分到底是哪个中断进来的。
最下面那里,是另一种输出,共有20条输出线路到了其他外设,这就是用来触发其他外设操作的,也就是刚才说的事件响应。

6、AFIO复用IO口

上面这个图,就是AFIO选择中断引脚的结构图,这里就是一系列的数据选择器,第一个输入每个GPIO外设的Pin_0引脚,然后通过数据选择器,最终选择一个,连接到EXTI0上,在每一个数据选择器的上方,写的是配置这个寄存器的这些位,就可以决定选择哪个一输入,如在AFIO_EXTICR1寄存器的EXTI0[3:0]配置相应数据,即可选择左边Pin_0引脚的哪个输入。

AFIO主要用于引脚复用功能的选择和重定义,也就是数据选择器的作用。

在STM32中,AFIO主要完成两个任务,复用功能引脚重映射,中断引脚选择。
其中复用功能引脚重映射,就是最开始提到的引脚定义表,当我们想把这些默认复用功能的引脚换到重定义的这个位置来,就是用AFIO来完成的。
中断引脚选择就是上图所示功能。

 7、EXTI内部框图

EXTI内部结构如上图所示,在上图右下角,就是20根输入线,输入线进入EXTI后,首先进入边沿检测电路, 通过上面的上升沿寄存器和下降沿寄存器来选择是上升沿触发还是下降沿触发,或者两个都触发,通过边沿检测电路继续往里走,就进入到“或”门的输入端了,这里由两个输入,一个是刚刚由边沿检测电路过来的,另一个是由软件中断来控制的,因此我们说EXTI有四种触发方式,前者控制三种触发方式,后者控制软件触发方式。

继续往后走,往上就是触发中断,往下就是触发事件。

触发中断首先会置一个挂起寄存器,这相当于是一个中断标志位,我们可以读取这个寄存器判断是哪个通道触发的中断,如果中断挂起寄存器置1,它就会继续往左走,和中断屏蔽寄存器共同进入一个“与”门,然后至NVIC中断控制器,这里的“与”门实际上就是开关的作用,中断屏蔽寄存器给1,那另一个输入就是直接输出,也就是允许中断,中断屏蔽寄存器给0,那另一个输入无论是什么,输出都是0,相当于屏蔽了这个中断,这就是这个“与”门的作用,相当于一个开关控制。

接下来看触发事件部分,首先也是一个事件屏蔽寄存器进行开关控制,最后通过一个脉冲发生器,到其他外设。这个脉冲发生器 就是给一个电平脉冲,用来触发其他外设的动作。


三、硬件模块 

1、使用外部中断的硬件特性

对于STM32来说,想要获取的信号是外部驱动的很快的突发信号,比如旋转编码器的输出信号,可能很久不会拧它,这时不需要STM32做任何事,但是一拧旋转编码器,就会有很多脉冲波形需要STM接收,这个信号是突发的,STM32不知道什么时候回来,同时它是外部驱动的,STM32只能被动读取,最后这个信号非常快,STM32稍微晚来一些,就会错过很多波形,对于这种情况来说,就可以考虑使用STM32的外部中断了,有脉冲过来,STM32立即进入中断函数处理,没有脉冲的时候,STM32就专心做其他事情。

另外还有红外遥控接收头的输出,接收到遥控数据之后,它会输出一段波形,这个波形转瞬即逝,并且不会等你,所以就需要我们用外部中断来读取。

最后还有按键,虽然它的动作也是外部驱动的突发事件,但是并不推荐使用外部中断来读取按键,因为外部中断不好处理按键抖动和松手检测的问题,对于按键来说,它的输出波形也不是转瞬即逝的,所以要求不高的话可以在主程序中循环读取,如果不想主循环读取,可以考虑一下定时器中断读取的方式,这样既可以做到后台读取按键值,不阻塞主程序,也可以额很好地处理按键抖动和松手检测的问题。

2、旋转编码器

(1)旋转编码器介绍

旋转编码器:是用来测量位置、速度或旋转方向的装置,当其旋转轴旋转时,其输出端可以输出与旋转速度和方向对应的方波信号,读取方波信号的频率和相位信息,即可得知旋转轴的速度和方向。

旋转编码器的类型包括机械触触点式、霍尔传感器式、光栅式等类型。

旋转编码器的工作原理:下图是一种最简单的编码器样式,这里使用的也是对射式红外传感器来测速的,为了测速,还需要配合一个这样的光栅编码盘,当这个编码盘转动时,红外传感器的红外光就会出现遮挡、透过、遮挡、透过这样的现象,对应模块输出的电平就是高低电平交替的方波,方波的个数表示了转过的角度,方波的频率表示转速,那我们就可以用外部中断来捕获这个方波的边沿,以此判断位置和速度,这个模块只有一路输出,正转反转输出波形没办法区分,因此这种测速方法只能测位置和速度,不能测量旋转方向。

为了进一步测量方向,我们就可以用后面的几种编码器。

下面第一个图中,就是我们套件里面的旋转编码器,第二个图是它的内部拆解结构,它内部是用金属触点来进行通断的,所以它是一种机械触点式编码器,它一般是用来调节的,比如音响调节音量,因为它是触点接触的形式,所以不适合点击这种高速旋转的地方,另外几种都是非接触的形式,可以用于电机测速。

在内部结构图中,左右有两部分开关触点,其中两部分内侧细一点的触点,都是和中间引脚C相连,外侧触点,左边的接在A引脚,右边的接在引脚,这就是这些触点的连接方式。

然后中间这个圆的金属片是一个按键,这个旋转编码器的轴是可以按下去的,这个按键的两根线,就在上面引出来了,按键的轴按下,上面两根线连接,松手,上面两根线断开,就是个普通按键。

再看一下第三个图,编码盘,它也是一系列像光栅一样的东西,只不过这是金属触点,在旋转式时,依次接通和断开两边的触点。

并且还有一个关键的部分,这个金属盘的位置是经过设计的,它能让两侧触点的通断产生一个90度的相位差,最终配合外部电路,这个编码器的两个输出就会输出下图所示的波形。

正转时,左边的引脚,也是A相引脚,输出一个方波波形,同时右侧的引脚,就是B相引脚,输出一个和它相差90°的波形。

反转时,A相还是方波信号,B相就会提前90°,如下图所示。

这种相位相差90°的波形,就叫正交波形,带正交波形信号输出的编码器,是可以用来测方向的,这就是单相输出和两相正交输出的区别。

 下面这个图,是直接附在电机后面的编码器,称为霍尔传感器形式编码器,中间是一个圆形磁铁,边上有两个位置错开的霍尔传感器,当磁铁旋转时,通过霍尔编码器,就可以输出正交的方波信号了。

 下面这个图就是独立的编码器元件,它的输入轴转动时,输出就会有波形,这个也是可以测速和侧方向的。

(2)旋转编码器硬件电路

 上图是模块的电路图,中间部分就是旋转编码器,上面按键的两根线并没有使用,是悬空的,圆形按键下面就是编码器内部的两个触点,旋转轴旋转时,这两个触点以相位相差90°的方式交替导通,因为这只是开关信号,所以要配合外围电路,才能输出高低电平。

在R1下面的红点处,接了一个10k的上拉电阻,因此默认没有旋转的情况下,这个点被上拉为高电平,通过R3电阻输出到A端口的就也是高电平,当旋转时,内部触点导通,R1下方的红点就被中间接地部分拉到低电平,再通过R3输出,A端口就是低电平了。R3是一个输出限流电阻,它是为了防止模块引脚电流过大,C1是输出滤波电容,可以防止一些输出信号的抖动。右边电路和左边是一模一样的,这里就不再说明。

最后可以看到中间C端口是直接接GND的。

 上面VCC和GND接电源,下面A相输出和B相输出接到STM32的两个引脚上,比如PB0和PB1,注意引脚的GPIO_Pin编号不一样就可以,中间C引脚就是GND,暂时不用。


四、对射式红外传感器计次

1、硬件接线图 

2、AFIO外设常用函数

1.void GPIO_AFIODeInit(void);

这个函数是用来复位AFIO外设的,调用一下这个函数,AFIO外设的配置就会全部清除。

2.void GPIO_EventOutputConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource);

用来配置AFIO的事件输出功能

3.void GPIO_EventOutputCmd(FunctionalState NewState);

用来配置AFIO的事件输出功能

4.void GPIO_PinRemapConfig(uint32_t GPIO_Remap, FunctionalState NewState);

用来进行引脚重映射,第一个参数可以选择要重映射的方式,第二个参数是新的状态。

5.void GPIO_EXTILineConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource);

调用这个函数,就可以配置AFIO的数据选择器,来选择我们想要的中断引脚。

第一个参数是选择某个GPIO外设作为外部中断源,这个参数可以是GPIO_PortSourceGPIOx,其中x是A~G。

第二个参数是GPIO_PinSourcex,指定要配置的外部中断线,其中x是0~15。

GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource14);		
//AFIO外部中断引脚选择配置

第一个参数是选择GPIOB作为外部中断源,第二个参数表示选择EXTI连接PB14号口的第14个中断线路。

3、EXTI外设常用函数

1.void EXTI_DeInit(void);

调用这个函数,就可以把EXTI的配置都清除,恢复成上电时默认的状态。

2.void EXTI_Init(EXTI_InitTypeDef* EXTI_InitStruct);

调用这个函数,就可以根据这个结构体里的参数来配置EXTI外设,来初始化EXIT,使用方法和GPIO类似。

	EXTI_InitTypeDef EXTI_InitStructure;
	EXTI_InitStructure.EXTI_Line = EXTI_Line14;							//要配置的中断线
	EXTI_InitStructure.EXTI_LineCmd = ENABLE;							//指定选择中断线的新状态
	EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;					//外部中断线的模式
	EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;				//中断触发方式选择
	EXTI_Init(&EXTI_InitStructure );		

这里的结构体参数有四个,作用分别为:

EXTI_Line,是选择配置的中断线,由于我们使用的是PB14,所以需要用到EXTI14中断线,共有20根中断线可以选择。

EXTI_LineCmd,指定选择中断线的新状态,可以选择ENABLE和DISABLE,这里我们是要配置中断,所以肯定是要选择ENABLE。

EXTI_Mode,外部中断线的模式,有中断响应和事件响应两种,这里我们要让CPU执行中断,因此选择中断响应EXTI_Mode_Interrupt。

EXTI_Trigger,中断触发方式选择,有四种,分别是上升沿、下降沿、双边沿,因为之前的GPIO口是选的上拉模式,即没信号影响的时候那个口是高电平,那有信号来的就对应的是电平从高到低,即下降沿,因此我们这里选择下降沿触发EXTI_Trigger_Falling。

在对射式红外传感器计次程序中,下降沿触发是在移开挡光片的时候触发中断,如果是上升沿触发,则是放入挡光片的时候触发中断,如果是双边沿触发,则放入挡光片和移开挡光片的时候都会触发中断,也就是放一次挡光片就会触发两次中断。

3.void EXTI_StructInit(EXTI_InitTypeDef* EXTI_InitStruct);

调用这个函数,可以把参数传递的结构体变量EXTI_InitStruct,赋一个默认值,

4.void EXTI_GenerateSWInterrupt(uint32_t EXTI_Line);

这个函数是用软件来触发外部中断的,调用这个函数,参数给一个指定的中断线,就能软件触发一次这个外部中断。

在外设运行过程中,会产生一些状态标志位,比如外部中断来了,会在挂起寄存器使一个标志位置1,对于其他外设,如串口收到数据,也会置标志位,定时器时间到,也会置标志位,这些标志位都是放在状态寄存器内的,当程序想要看到这些标志位时,就可以用到下面四个函数。

5.FlagStatus EXTI_GetFlagStatus(uint32_t EXTI_Line);

可以获取指定的标志位是否被置1了。

6.void EXTI_ClearFlag(uint32_t EXTI_Line);

可以对置1的标志位进行清除。

如果想在主程序查看和清除标志位,就用上面两个函数。

对于有些比较紧急的标志位,在置标志位后,会触发中断,在中断函数中,如果想查看标志位和清除标志位,那就用下面两个函数。

7.ITStatus EXTI_GetITStatus(uint32_t EXTI_Line);

获取中断标志位是否被置1。

EXTI_GetITStatus(EXTI_Line14);

上面代码就是查看EXTI14的中断标志位是否为1,返回值是SET or RESET,若返回值是SET,则说明EXTI14的中断标志位为1,若返回值是RESET,说明EXTI14的中断标志位还是为0。

8.void EXTI_ClearITPendingBit(uint32_t EXTI_Line);

清除中断挂起标志位。

EXTI_ClearITPendingBit(EXTI_Line14)

在每次中断程序结束后,一定要再调用一下清除中断标志位的函数,因为只要中断标志位置1了,程序就会跳转到中断函数, 如果不清除中断标志位,那它就会一直申请中断,这样程序就会不断响应中断,执行中断函数,那程序就会卡死在中断函数中,所以每次中断程序结束后,都应该清除一下中断标志位。

上面这段程序的作用就是清除EXTI14的中断标志位。

4、NVIC外设常用函数

1.void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup);

这个函数是用来中断分组的,参数是中断分组的方式。

NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);			//中断优先级分组
	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn;				//选择指定通道
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;						//开启指定通道	
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;					//给响应优先级分配优先级程度
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;			//给抢占优先级分配优先级程度
	NVIC_Init(&NVIC_InitStructure);

 函数简介是配置优先级分组:抢占优先级和响应优先级

参数可以选择NVIC_PriorityGroup_0~4,共有5种分组情况,如果中断不多,很难导致中断冲突,对优先级分组来说,哪个分组情况都可以,这里选择第三个分组,也就是NVIC_PriorityGroup_2,2位抢占,2位响应,比较平均一些。

注意:分组方式整个芯片只能用一种,所以这个分组的代码,整个工程只需要执行一次就好了,如果在模块里面进行分组,要确保每个模块分组都选的是同一个,或者也可以把这个分组代码放在主函数的最开始,这样模块里就不用再分组了。

2.void NVIC_Init(NVIC_InitTypeDef* NVIC_InitStruct);

根据结构体里面指定的参数初始化NVIC。

NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn;		//选择指定通道
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;			    //开启指定通道	
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;			//给响应优先级分配优先级程度
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;	//给抢占优先级分配优先级程度
NVIC_Init(&NVIC_InitStructure);

这里的结构体参数有四个,作用分别为:

NVIC_IRQChannel,选择指定通道,它的通道列表在STM32F10x.h文件里,STM32单片机芯片是MD中等密度的,因此找到STM32F10x_MD,往下翻,就能看到EXTI15_10_IRQn中断通道,STM32的EXTI10到EXTI15都是合并到了这个通道里。

IRQChannelCmd,使能或失能指定通道,这里我们肯定是要打开中断通道的,因此我们选择ENABLE。

NVIC_IRQChannelSubPriority,给响应优先级分配优先级程度,这里我们中断分组是2,因此响应优先级和抢占优先级的取值范围都是0~3,有因为中断只有一个,所以取值1即可。

NVIC_IRQChannelPreemptionPriority,给抢占优先级分配优先级程度,取值1。

3.void NVIC_SetVectorTable(uint32_t NVIC_VectTab, uint32_t Offset);

设置中断向量表。

4.void NVIC_SystemLPConfig(uint8_t LowPowerMode, FunctionalState NewState);

系统低功耗配置

5、程序

主函数

#include "stm32f10x.h"                  						// Device header
#include "Delay.h" 
#include "OLED.h"
#include "CountSensor.h"

int main(void)													
{
	OLED_Init();
	CountSensor_Init();
	
	OLED_ShowString(1, 1 , "Count:");		

	while(1)
	{
		OLED_ShowNum(1, 7, CountSensor_Get(), 5);
	}
}

CountSensor对射式红外传感器模块,EXTI15_10_IRQHandler中断函数不需要在.h文件中声明

#include "stm32f10x.h"                  // Device header

uint16_t CountSensor_Count;				//红外传感器被遮挡次数


/**
  * @brief  对射式红外传感器初始化函数
  * @param  无
  * @retval 无
  */

void CountSensor_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);				//打开GPIOB和AFIO的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);		

	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);								//GPIO引脚配置
	
	GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource14);		//AFIO外部中断引脚选择配置

	EXTI_InitTypeDef EXTI_InitStructure;
	EXTI_InitStructure.EXTI_Line = EXTI_Line14;							//要配置的中断线
	EXTI_InitStructure.EXTI_LineCmd = ENABLE;							//指定选择中断线的新状态
	EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;					//外部中断线的模式
	EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;				//中断触发方式选择
	EXTI_Init(&EXTI_InitStructure );	

	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);						//中断优先级分组
	
	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn;				//选择指定通道
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;						//开启指定通道	
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;					//给响应优先级分配优先级程度
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;			//给抢占优先级分配优先级程度
	NVIC_Init(&NVIC_InitStructure);
	
}


/**
  * @brief  用来返回红外传感器被遮挡次数
  * @param  无
  * @retval CountSensor_Count,中断函数每执行1次便+1
  */

uint16_t CountSensor_Get(void)					
{
	return CountSensor_Count;
}
	

/**
  * @brief  EXTI10~15中断线的中断函数,中断函数的名字是固定的,根据中断源的地方来更改名字,
			PB14有电平变化时即会触发中断,便会跳到这个函数。
  * @param  无
  * @retval 无
  */

void EXTI15_10_IRQHandler(void)					
{
	if(EXTI_GetITStatus(EXTI_Line14) == SET)	//检测EXTI14中断标志是否置1,置1才执行下面程序
	{
		CountSensor_Count++;					//每次红外传感器被遮挡,便计次+1
		EXTI_ClearITPendingBit(EXTI_Line14);	//EXTI14中断线中断标志清零
	}
	
}	

6、实物展示

对射式红外传感器计次


五、旋转编码器计次

1、硬件接线图

2、程序

主程序

#include "stm32f10x.h"                  						// Device header
#include "Delay.h" 
#include "OLED.h"
#include "Encoder.h"

int16_t Num;

int main(void)													
{
	OLED_Init();
	
	OLED_ShowString(1, 3, "Num:");		

	while(1)
	{
		Num += Encoder_Get();			//Encoder_Get()返回的是编码器旋转的变化值
										//这样Num每次只需要进行变化值的加减即可,之前的旋转值就会累积
		OLED_ShowSignedNum(1, 5, Num, 5);
	}
}

Encoder模块

#include "stm32f10x.h"                  // Device header

int16_t Encoder_Count;					//转动编码值


/**
  * @brief  旋转编码器初始化函数
  * @param  无
  * @retval 无
  */

void Encoder_Init(void)
{
	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);				//打开GPIOB和AFIO的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);		

	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);								//GPIO引脚配置
	
	GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource0);			//AFIO外部中断引脚选择配置
	GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource1);			

	
	EXTI_InitTypeDef EXTI_InitStructure;
	EXTI_InitStructure.EXTI_Line = EXTI_Line0 | EXTI_Line1;				//要配置的中断线
	EXTI_InitStructure.EXTI_LineCmd = ENABLE;							//指定选择中断线的新状态
	EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;					//外部中断线的模式
	EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;				//中断触发方式选择
	EXTI_Init(&EXTI_InitStructure );	

	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);						//中断优先级分组
	
	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;				//选择指定通道
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;						//开启指定通道	
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;					//给响应优先级分配优先级程度
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;			//给抢占优先级分配优先级程度
	NVIC_Init(&NVIC_InitStructure);

	NVIC_InitStructure.NVIC_IRQChannel = EXTI1_IRQn;				
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;						
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;					
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;			
	NVIC_Init(&NVIC_InitStructure);
	
}


/**
  * @brief  每次调用这个函数,返回的是Encoder_Count的变化值
			既然是变化值,那么就得清零,所以就得另外定义一个值Temp
  * @param  无
  * @retval Temp,Encoder_Count的变化值
  */


int16_t Encoder_Get(void)
{
	int16_t Temp;
	Temp = Encoder_Count;
	Encoder_Count = 0;
	return Temp;
}

/**
  * @brief  EXTI0中断线的中断函数,PB0有上升沿则跳转到这个中断函数
			然后检测另外一个相位是否是低电平,是低电平则说明是反转
  * @param  无
  * @retval 无
  */

void EXTI0_IRQHandler()
{
	if(EXTI_GetITStatus(EXTI_Line0) == SET)
	{
		if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0)		//读取PB1的输入值
		{
			Encoder_Count--;
		}
		
		EXTI_ClearITPendingBit(EXTI_Line0);
	}
}

	/**
  * @brief  EXTI1中断线的中断函数,PB1有上升沿则跳转到这个中断函数
			然后检测另外一个相位是否是低电平,是低电平则说明是正转
  * @param  无
  * @retval 无
  */

void EXTI1_IRQHandler()
{
	if(EXTI_GetITStatus(EXTI_Line1) == SET)
	{
		if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == 0)		//读取PB0的输入值
		{
			Encoder_Count++;
		}
		
		EXTI_ClearITPendingBit(EXTI_Line1);
	}
}
	


3、实物展示

旋转编码器计次

中断函数使用建议:

(1)在中断函数里,不要执行耗时过长的代码,中断函数要简短快速,别刚进中断就执行一个Delay多少毫秒这样的代码,因为中断是处理突发的事情,如果为了一个突发的事情待在中断里不出来了,主程序就就会受到严重的阻塞。

(2)最好不要在中断函数和主函数调用相同的函数或者操作同一个硬件,尤其是硬件相关的函数,比如OLED显示函数,如果既在主函数中调用OLED,又在中断里调用OLED,OLED就会显示错误,因为在主程序中,OLED刚显示一半,这时程序进中断了,结果中断里还是OLED显示函数,那OLED就会被挪到中断里进行显示,但是当中断结束后,需要继续原来的显示,这时就出问题了,虽然在中断进入和推出的时候,会有保护现场和恢复现场,但是这只能保证CPU程序能正常返回不出问题,对于外部硬件的话,并没有在进入中断时,进行现场保护,所以中断返回后,就会出现问题。

作者:剑鞘的流苏

物联沃分享整理
物联沃-IOTWORD物联网 » STM32EXTI外部中断实战:对射式红外传感器和旋转编码器的计次应用(第5篇)

发表评论