EXTI外部中断(笔记)

中断系统

中断:在主程序运行中,出现了特定的中断触发条件[中断源],使得cpu暂停当前正在运行的程序,转而去处理中断程序,处理完成后又返回原来被暂停的位置继续执行

如:定时中断

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

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

stm32中断

68个可屏蔽中断通道{最多},包含EXTI、TIM、ADC、USART、SPI、I2C、RTC等多个外设

EXTI外部中断,TIM定时器、ADC数模转换器、USART串口、SPI通信、I2C通信、RTC实时时钟

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

NVIC{嵌套中断向量控制器}的基本结构

NVIC是一个内核外设,在内核芯片中与cpu呆在一块,

如果不使用NVIC进行适配的弊端:stm32的中断非常多,如果把这些中断全部直接接到CPU上,那CPU还需要引出很多线去进行适配,在设计上就很麻烦,并且,如果很多中断同时申请,或者中断很多产生了拥堵,CPU也会很难处理,毕竟CPU是用于运算的,所以就将中断分配的任务放到别的地方,进而使用NVIC

NVIC有很多输入口,你有多少个中断线路,都可以接上来

一个外设可能会占用多个中断通道,NVIC只有一个输出口,NVIC根据每个中断的优先级分配中断的优先顺序,通过这一个输出告诉CPU该处理哪个中断

所以依靠NVIC嵌套中断向量控制器对于中断先后顺序分配的任务,cpu不需要知道

NVIC的中断优先级由优先级寄存器4位决定,这4位可以进行切分,分为高n位的抢占优先级和低4-n位的响应优先级 (二进制四位数)

抢占优先级高的可以中断嵌套,响应优先级高的可以优先排队,抢占优先级和响应优先级均相同的按中断号进行排队(数值越小,优先级越高)

分组方式 抢占优先级 响应优先级
分组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~16 0位,取值范围0
EXTI(Extern Interrupt) 外部中断

EXTI可以检测指定的GPIO口的电平信号,当其指定的GPIO口产生电平变化时,EXTI将立即向NVIC提出中断申请,经过NVIC批准后即可中断CPU主程序,使得CPU执行EXTI对应的中断程序【引脚电平变化触发中断】

支持的触发方式:上升沿/下降沿/双边沿/软件触发

上升沿:电平从低电平变到高电平的瞬间触发中断

下降沿:电平从高电平变到低电平的瞬间触发中断

双边沿:上升沿和下降沿都可以触发中断

软件触发:与引脚无关,因程序执行而产生中断

支持的GPIO口:所有的IO口[任意的GPIO口都可以当作外部中断的引脚],但是相同的pin不能同时触发中断

如PA1、PB1、PC1这样,端口GPIO_Pin一样的不能同时用,只能选择其中的一个作为中断引脚

所以如果有多个中断引脚,要选择不同的pin的引脚

通道数:16个GPIO_Pin[对应GPIO_Pin_0~GPIO_Pin_15],外加PVD电源电压检测输出、RTC闹钟、USB唤醒、以太网唤醒[总共20个中断线路,16+4]

触发响应方式:中断响应/事件响应

中断响应:申请中断,让CPU执行中断函数

事件响应:当外部中断检测到引脚电平变化时,正常的流程是选择触发中断,但在STM32中也可以选择触发一个事件,如果选择触发事件,那么外部中断的信号就不会通向CPU了,而是通向其他的外设,用来触发其他外设的操作

EXTI外部中断的基本结构

前面我们说,EXTI只有16个GPIO的外设通道,但GPIOA,GPIOB,GPIOC每个GPIO外设都有16个引脚,如果每个引脚占用一个通道,那么EXTI的16个通道就肯定不够用,所以在接入EXTI之前,会接入一个AFIO中断引脚选择的电路模块,这个AFIO就是一个数据选择器,它可以在前方三个GPIO外设的16个引脚中选择一个连接到后面EXTI的通道中,所以前面说相同的pin不能同时触发中断,是因为AFIO会选择3种GPIO中其中的一个Pin来接入EXTI外部中断模块的通道。,如PA0,PB0,PC0经过AFIO选择后只有一个能连接到EXTI的通道0上

经过EXTI电路之后,分为了两种输出:

一部分接到NVIC用于触发中断,本来20路输入应该有20路的输入,但可嫩ST公司觉得这20个输出太多了,比较占用NVIC的通道资源所以就将其中外部中断的9~5和15~10给分配到一个通道中【也就是说外部中断的9~5会触发同一个中断函数,15~10也会触发同一个中断函数,在编程的时候,我们需要再根据标志位到底是哪一个中断进入的】

另有20路输出线路到了其他的外设,这就是用来触发其他的外设操作的,也就是我们刚才说的事件响应

AFIO复用IO口

AFIO主要用于引脚复用功能的选择和重定义

在STM32中,AFIO主要用于完成两个任务:复用功能引脚重映射、中断引脚选择

AFIO结构

EXTI框图

EXTI结构

EXTI的右边就是20根输入线,输入线首先进入边缘检测线路,在上面的上升沿寄存器和下降沿寄存器可以是选择上升沿触发、下降沿触发还是两个都触发【或门的作用】,接着触发信号就进入到这个或门的输入端了

或门:

只有输入全为零,输出才为0

与门:

只要有一个输入是低电平0,输出就是0

非门[一个输入,一个输出]:

输入0,输出1,输入1,输出0

触发信号通过或门后,就兵分两路,上一路是触发终断的,下一路是触发事件的,触发中断首先会置一个挂起寄存器,这相当于一个中断标志位了,我门可以读取这个寄存器判断是哪个通道触发的中断,如果中断挂起寄存器置1,它就会继续向左走,和中断屏蔽寄存器共同进入一个与门,然后是到达NVIC中断控制器,

这里的与门实际上起到一个开关的作用,因为对于与门来说,1与上任意数x,等于这个任意的数x,1&x=x,0与任意的数x都等于0,0&x=0

这就相当于中断寄存器给1,那另一个数入就是直接输出,也就是允许中断,中断屏蔽器给0,那么另一个输入无论是多少,输出都是0,达到屏蔽中断的目的

在这个图的下方的事件屏蔽寄存器和它对应的与门也是相同的原理

脉冲发生器就是给一个电平脉冲,用来触发其他外设的动作

上面的外设接口,和APB总线,我们可以通过总线访问这些寄存器,

什么时候需要用到外部中断呢

对于stm32来说,想要获取的信号是外部驱动的很快的突发信号,比如旋转编码器的输出信号,我可能很久都不会去拧它,这时不需要STM32做任何事,但是我一拧它,就会有很多脉冲波形需要STM32接收,这个信号是突发的stm32不知道什么时候它会来,同时它是外部驱动的,STM32只能被动读取,最后,这个波形非常快,STM32稍微晚一点来读取就会错过很多波形,像这种情况,就可以考虑使用STM32的外部中断了。

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

旋转编码器

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

类型:机械触点式/霍尔传感器式/光栅式

光栅式:

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

机械触点式:

内部是使用金属触点来进行通断的,所以它是一种机械触点式编码器,左右是两部分开关触点,其中内侧的这两根细的触点,都是和中间这个引脚连接的,像这样

中间是一个机械按键开关,引出两条线与上面的两个引脚相连

编码盘也是一系列像是光栅的东西,只不过光栅的孔洞换成了金属触点,旋转时,依次接通和断开两边的触点,这个金属盘的位置是经过设计过的,它能让两侧触点产生一个90度的相位差,也就是说一个触点连接一方的线路时,另一方的线路是没有被触点接通的

这个编码器的两个波形会呈现这样的相位差

当正转时,左边的引脚,也就是A相引脚,输出一个方波波形,右边的引脚,也就是B相引脚,也输出一个方波波形

B相输出是滞后A相位90度的

当正转时,相应的B相会提前09度

那么这样,正转反转就能够区分开来了,这种相位相差90度的波形就叫做正交波形,带正交波形信号输出的编码器是可以用来测方向的,这就是单相输出和两相正交输出的区别

当然还有的编码器不是输出正交波形,而是一个引脚输出方波信号代表转速,另一个输出高低电平代表旋转方向,这种不是正交输出的编码器但是还是可以测方向的

旋转编码器的内部硬件电路

旋转轴旋转时,这两个触点以相位差90度的方式交替导通,因为这只是一个开关电路,还需要配合外部电路才能输出高低电平。

看一下左边,这里接了一个10k的上拉电阻,默认没有旋转的情况下,这个点被上拉为高电平,通过R3这个电阻,输出到A端口的也就是高电平,当旋转时,内部触点导通,电压直接就被拉低到GND了,这个时候通过R3输出,到A的就是低电平

R3是一个输出限流电阻,他是为了防止模块引脚电流过大的,这个C1是输出滤波电容,可以防止一些输出信号抖动

配置函数

第一步:配置RCC,把我们这里所涉及的外设的时钟都打开,不打开时钟,外设是没办法工作的

第二步:配置GPIO,选择我们的端口为输入模式

第三步:配置AFIO,选择我们用的这一路GPIO,连接到后面的EXTI

第四步:配置EXTI,选择边沿触发方式,比如上升沿,下降沿,或者双边沿,还有选择触发响应方式,可以选择中断响应和事件响应

第五步:配置NVIC,给我们的这个中断选择一个合适的优先级,最后通过NVIC,外部中断信号就能进入CPU了,这样CPU才能收到中断信号,才能跳转到中断函数里执行中断程序

这五步就是配置外部中断的具体流程,这里涉及的外设较多,有RCC,GPIO,AFIO,EXTI,

第一步:RCC开启外部时钟

这一步,主要是三个开启时钟的函数

开启GPIOB的时钟

这里参数是APB_GPIOB的这个,函数也要用APB2的这个开启时钟函数

 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE)

如果这里用了APB1或者AHB的函数,然后填上APB2_GPIOB的参数,这样程序也是不会报错的,因为语法上并没有错误,后面这个参数只是一个宏定义,只是一个数的替换而已,所以编译器也不知道你写错了,但实际运行的话,这个程序肯定是存在问题的,所以这里要细心一点,注意函数和参数的这个APB2、APB1和AHB要对应起来

开启AFIO的时钟

AFIO也是APB2的外设,,那我们复制一下这个函数,然后把GPIO改成AFIO就完成了

如果不确定哪个外设是接在哪个总线上的,可以转到函数定义,看一下参数列表,看一下函数【如RCC_APB2PeriphClockCmd()】定义,上方的参数列表,在参数列表中的就是对应的总线上的外设

接着还有EXTI和NVIC两个外设

这两个外设的时钟是一直都打开着的,不需要我们再开启时钟了

EXTI作为一个独立外设,按理来说应该是需要开启时钟的,但是寄存器里面却没有EXTI时钟的控制位,

up推测可能是和EXTI唤醒有关,或者是其他的一些电路设计上的一些考虑,弹幕:EXTI模块是由NVIC模块直接控制的,并不需要单独的外设时钟。

NVIC也不需要开启时钟,因为NVIC是内核的外设,内核的外设都是不需要开启时钟的,它们和CPU待在一起,而RCC管的都是内核外的外设,所以RCC管不着NVIC

到这里时钟就配置完了

#include “stm32f10x.h”                  // Device header

void CounterSensor_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);//开启GPIOB的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);//开启AFIO的时钟
}

第二步:配置GPIO

首先定义结构体GPIO_InitYypeDef GPIO_InitStructure

然后复制结构体名字,将结构体都引出来

然后GPIO_Init(GPIOB,&GPIO_InitStructure);引用结构体,初始化GPIO外设

接着填写结构体参数,第一个填GPIO_Mode,对于外部中断来说要选择浮空输入,上拉输入,或者下拉输入,这其中的一个模式,

像这种其他外设使用GPIO的情况,如果不清楚应该配置为什么模式,可以看一下参考手册,在GPIO这一章找一下,有一个外设的GPIO的配置表

所以在这里,就给一个GPIO_Mode_IPU,上拉输入,默认为高电平的输入方式

然后GPIO_Pin,我们用的PB14号端口,所以就写GPIO_Pin_14

GPIO_Speed,这个不是很重要,我们还是GPIO_Speed-50MHz

#include “stm32f10x.h”                  // Device header

void CounterSensor_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
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);
}

第三步:配置AFIO

这个AFIO外设,ST公司并没有为它分配专门的库函数文件,它的库函数是和GPIO在一个文件里的,我们找一下GPIO的.h文件,翻到最下方,找到函数声明,一些是与GPIO有关的函数,一些函数则与EXTI有关

void GPIO_DeInit(GPIO_TypeDef* GPIOx);
void GPIO_AFIODeInit(void);//用于复位AFIO外设的,调用一下这个函数,AFIO外设的配置就会全部清除
void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct);
void GPIO_StructInit(GPIO_InitTypeDef* GPIO_InitStruct);
uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
uint16_t GPIO_ReadInputData(GPIO_TypeDef* GPIOx);
uint8_t GPIO_ReadOutputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
uint16_t GPIO_ReadOutputData(GPIO_TypeDef* GPIOx);
void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
void GPIO_WriteBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, BitAction BitVal);
void GPIO_Write(GPIO_TypeDef* GPIOx, uint16_t PortVal);
void GPIO_PinLockConfig(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);//用来锁定GPIO配置的,调用这个函数,参数指定某个引脚,那这个引脚的配置就会被锁定防止意外更改
void GPIO_EventOutputConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource);//用来配置AFIO的事件输出功能
void GPIO_EventOutputCmd(FunctionalState NewState);//用来配置AFIO的事件输出功能的
void GPIO_PinRemapConfig(uint32_t GPIO_Remap, FunctionalState NewState);//用来进行引脚重映射,第一个参数可以选择要重映射的方式,第而个参数是新的状态
void GPIO_EXTILineConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource);//配置AFIO的数据选择器,来选择我们想要的中断引脚
void GPIO_ETH_MediaInterfaceConfig(uint32_t GPIO_ETH_MediaInterface);//与以太网有关

void GPIO_AFIODeInit()这个函数是用于复位AFIO外设的,调用一下这个函数,AFIO外设的配置就会全部清除

void GPIO_PinLockConfig()这个函数是用来锁定GPIO配置的,调用这个函数,参数指定某个引脚,那这个引脚的配置就会被锁定防止意外更改

void GPIO_EventOutputConfig()、void GPIO_EventOutputCmd()是用来配置AFIO的事件输出功能的,用的不多了解即可

void GPIO_PinRemapConfig()用来进行引脚重映射,第一个参数可以选择要重映射的方式,第而个参数是新的状态【重要】

void GPIO_EXTILineConfig()用于配置AFIO的数据选择器,来选择我们想要的中断引脚【重要】

现在我们想要配置AFIO外部中断引脚选择,就直接复制这个GPIO_EXYILineConfig函数放到函数中,右键跳转到定义,可以看到这个函数虽然是以GPIO开头内部却是操作的AFIO的寄存器,所以这个函数实际上是AFIO的函数,

/**
* @brief  Selects the GPIO pin used as EXTI Line.//选择GPIO Pin作为外部中断线
* @param  GPIO_PortSource: selects the GPIO port to be used as source for EXTI lines.//选择某个GPIO外设作为外部中断源
*   This parameter can be GPIO_PortSourceGPIOx where x can be (A..G).
* @param  GPIO_PinSource: specifies the EXTI line to be configured.
*   This parameter can be GPIO_PinSourcex where x can be (0..15).
* @retval None
*/
void GPIO_EXTILineConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource)
{
uint32_t tmp = 0x00;
/* Check the parameters */
assert_param(IS_GPIO_EXTI_PORT_SOURCE(GPIO_PortSource));
assert_param(IS_GPIO_PIN_SOURCE(GPIO_PinSource));

tmp = ((uint32_t)0x0F) << (0x04 * (GPIO_PinSource & (uint8_t)0x03));
AFIO->EXTICR[GPIO_PinSource >> 0x02] &= ~tmp;
AFIO->EXTICR[GPIO_PinSource >> 0x02] |= (((uint32_t)GPIO_PortSource) << (0x04 * (GPIO_PinSource & (uint8_t)0x03)));
}

简介里写的是:选择GPIO Pin作为外部中断线

第一个参数是GPIO_PortSource,选择某个GPIO外设作为外部中断源,这个参数可以是GPIO_PortSource GPIOX,其中x可以是A到G,那么我们就复制一下这个参数放到函数中,我们用的是PB14号引脚,所以把x改成B就可以了

第二个参数是GPIO_PinSourse指定要配置的外部中断线,这个参数可以是GPIO_PinSourcex,其中x可以是0到15,那么我们就复制一下这个参数放到函数中,我们用的是PB14号引脚,所以把x改成14,代表连接PB14号口的第14个中断线路

到这里,AFIO外部中断引脚选择配置就完成了,

当执行完这个函数后,AFIO的第14个数据选择器就拨好了,其中输入端被拨到了GPIOB外设上,对应的就是PB14号引脚,输出端固定连接的是EXTI的第14个线路,这样PB14号引脚的线路的电平信号就可以顺利通过AFIO,进入到后级EXTI电路了

#include “stm32f10x.h”                  // Device header

void CounterSensor_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
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_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource14);
}

第四步:配置EXTI

我们先看一下EXTI的库函数文件,看一下EXTI都有哪些库函数可用,我们找到exti.h的文件,打开,拖到最后,这些就是EXTI的所有库函数了

void EXTI_DeInit(void);//把EXTI的设置全部清除,恢复成上电默认的状态,
void EXTI_Init(EXTI_InitTypeDef* EXTI_InitStruct);//调用这个函数,就可以根据这个结构体里的参数配置EXTI外设
void EXTI_StructInit(EXTI_InitTypeDef* EXTI_InitStruct);//调用这个函数,可以把参数传递的结构体变量赋一个默认的值,
void EXTI_GenerateSWInterrupt(uint32_t EXTI_Line);//软件触发外部中断的,调用这个函数,参数给一个指定的中断线,就能软件触发一次这个外部中
FlagStatus EXTI_GetFlagStatus(uint32_t EXTI_Line);//获取指定的标志位是否置1
void EXTI_ClearFlag(uint32_t EXTI_Line);//对置1的标志位进行清除
ITStatus EXTI_GetITStatus(uint32_t EXTI_Line);//获取中断标志位是否被置1
void EXIT_ClearITPendingBit(cuint32_t EXTI_Line);//清除中断挂起标志位

首先第一个,EXTI_DeInit()调用它就可以把EXTI的设置全部清除,恢复成上电默认的状态,

然后第二个,EXTI_Init()调用这个函数,就可以根据这个结构体里的参数配置EXTI外设,我们初始化EXTI主要用的就是这函数,使用方法和GPIO_Init()相同

接着第三个,EXTI_StructInit()调用这个函数,可以把参数传递的结构体变量赋一个默认的值,

这前面三个函数基本所有的外设都有,就像是库函数的模板函数一样,基本每个外设都需要这些类型的函数

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

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

其中这前两个函数,EXTI_GetFlagStatus()可以获取指定的标志位是否置1了,EXTI_ClearFlag()可以对置1的标志位进行清除,

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

EXTI_GetITStatus(),获取中断标志位是否被置1了,EXIT_ClearITPendingBit(),清除中断挂起标志位

总结,如果想在主程序函数中查看和清除标志位,就用上面的两个函数,如果想在中断函数中查看和清除标志位,就用下面这两个函数

其实本质上,这四个函数都是对状态寄存器的读写,上面两个和下面的两个都是类似的功能,都是读写状态寄存器,只不过是下面这两个函数只能读写与中断有关的标志位,并且对中断是否允许做出了判断而上面的两个函数只是一般的读写标志位,没有额外的处理,能不能触发中断的标志位都能读取,所以建议在主程序里用上面两个,在中断函数里用下面两个,在中断中用上面两个也是没问题的,只不过是中断函数针对这两种场景,区分了这两种读写函数,

对于EXTI的初始化配置很明显,用这个EXTI_Init()函数就行了

调用EXTI_Init函数,里面只有一个参数,就是EXTI初始化的结构体,因为EXTI只有一个,所以不需要像GPIO那样,先制定要配置的哪个EXTI了

右键跳转到定义,看一下说明

/**
* @brief  Initializes the EXTI peripheral according to the specified
*         parameters in the EXTI_InitStruct.
* @param  EXTI_InitStruct: pointer to a EXTI_InitTypeDef structure
*         that contains the configuration information for the EXTI peripheral.
* @retval None
*/
void EXTI_Init(EXTI_InitTypeDef* EXTI_InitStruct)

函数简介写的是初始化EXTI外设,根据结构体里面指定的参数,第一个参数是EXTI_InitStruct指向EXTI_InitTypeDef类型的结构体指针包含 EXTI 外设的配置信息。复制结构体类型,粘贴类型名,然后起一个变量名叫EXTI_InitStructure,接着复制变量名EXTI_InitStructure在下面粘贴,引出结构体成员,最后把结构体变量放在EXTI_Init的参数里,前面加上取地址的符号&

接下来看参数,EXTI_Line右键跳转到定义,下方参数class选择member【因为结构体成员的英文是member】

然后看注释,这个参数可以是EXTI_Lines的随意组合我们选中EXTI_Lines,Ctrl+F搜索一下,Find Next

typedef struct
{
uint32_t EXTI_Line;               /*!< Specifies the EXTI lines to be enabled or disabled.
This parameter can be any combination of @ref EXTI_Lines 这个参数可以是EXTI_Lines的随意组合*/

EXTIMode_TypeDef EXTI_Mode;       /*!< Specifies the mode for the EXTI lines.
This parameter can be a value of @ref EXTIMode_TypeDef */

EXTITrigger_TypeDef EXTI_Trigger; /*!< Specifies the trigger signal active edge for the EXTI lines.
This parameter can be a value of @ref EXTIMode_TypeDef */

FunctionalState EXTI_LineCmd;     /*!< Specifies the new state of the selected EXTI lines.
This parameter can be set either to ENABLE or DISABLE指定选择的中断线的新状态,这个参数可以是ENABLE或者DISABLE */
}EXTI_InitTypeDef;

可以看到这个参数的取值,我们需要使用PB14所在的第14个线路,所以选择EXTI_Line14 复制,然后放到结构体成员对应参数位置中,第一个参数就完成了

接着第二个参数,EXTI_LineCmd,同样的方法,看一下注释,这里写的是,指定选择的中断线的新状态,这个参数可以是ENABLE或者DISABLE,我们肯定是要开启中断,所以我们选择ENABLE

接着第三个参数,EXTI_Mode,看一下注释,这里写的是,指定外部中断线的模式,这个参数可以是 EXTIMode_TypeDef 里面的一个值,选中,Ctrl+F搜索一下,Find Next,可以看到这是一个枚举,第一个是中断模式,第二个是事件模式,我们要用中断模式,所以复制第一个到参数位

typedef enum
{
EXTI_Mode_Interrupt = 0x00,//中断模式
EXTI_Mode_Event = 0x04//枚举模式
}EXTIMode_TypeDef;

接着第四个参数,EXTI_Trigger,,看一下注释,这里写的是,指定触发信号的有效边沿,这个参数可以是这个定义里的一个值,这个定义它写错了,这里应该是EXTITrigger_TypeDef,而不是EXTIMode_TypeDef,里面可以选择Rising上升沿触发,Falling下降沿触发,Rising_Falling双边沿触发,这里我们选择Falling下降沿触发

typedef enum
{
EXTI_Trigger_Rising = 0x08,//上升沿触发
EXTI_Trigger_Falling = 0x0C,//下降沿触发
EXTI_Trigger_Rising_Falling = 0x10//双边沿触发
}EXTITrigger_TypeDef;

这样,我们的外部中断配置就完成了

我们当前的配置是,将EXTI的第14个线路配置为中断模式,下降沿触发,然后开启中断,这样PB14的电平信号就能够通过EXTI通往下一级NVIC了

#include “stm32f10x.h”                  // Device header

void CounterSensor_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
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_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource14);

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

因为NVIC是内核外设,所以它的库函数是被ST发配到杂项里去了,我们打开misc.h文件,拖到最后,

void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup);//这个函数是用来中断分组的,参数是中断分组的方式
void NVIC_Init(NVIC_InitTypeDef* NVIC_InitStruct);//根据结构体里面指定的参数初始化NVIC
void NVIC_SetVectorTable(uint32_t NVIC_VectTab, uint32_t Offset);//设置中断向量表
void NVIC_SystemLPConfig(uint8_t LowPowerMode, FunctionalState NewState);//系统低功耗配置
void SysTick_CLKSourceConfig(uint32_t SysTick_CLKSource);

这里有NVIC的四个函数和Systick的一个函数

第一个,NVIC_PriorityGroupConfig(),这个函数是用来中断分组的,参数是中断分组的方式

然后第二个,NVIC_Init,根据结构体里面指定的参数初始化NVIC

下面NVIC_SetVectorTable(),设置中断向量表【用的不多】

和NVIC_SystemLPConfig,系统低功耗配置【用的不多】

使用上面的两个函数就行,在配置中断之前,先指定一下中断的分组,然后使用NVIC_Init()初始化一下NVIC就行了,复制函数NVIC_PriorityGroupConfig,粘贴,右键,跳转定义,查看注释

/**
* @brief  Configures the priority grouping: pre-emption priority and subpriority.
* @param  NVIC_PriorityGroup: specifies the priority grouping bits length.
*   This parameter can be one of the following values:
*     @arg NVIC_PriorityGroup_0: 0 bits for pre-emption priority
*                                4 bits for subpriority
*     @arg NVIC_PriorityGroup_1: 1 bits for pre-emption priority
*                                3 bits for subpriority
*     @arg NVIC_PriorityGroup_2: 2 bits for pre-emption priority
*                                2 bits for subpriority
*     @arg NVIC_PriorityGroup_3: 3 bits for pre-emption priority
*                                1 bits for subpriority
*     @arg NVIC_PriorityGroup_4: 4 bits for pre-emption priority
*                                0 bits for subpriority
* @retval None
*/

这个简介说,这是配置优先级分组:抢占优先级和响应优先级,这个参数可以取列表里面的值,bits for pre-emption priority几位抢占,bits for subpriority几位响应,这个具体要选哪个,其实是根据我们的需求来的,一般中断不多,很难导致中断冲突,对于优先级分组来说,就比较随意了,哪个都行,这里选择第二个分组,2位抢占,2位响应,复制这个参数NVIC_PriorityGroup_2粘贴,

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

接下来直接调用NVIC_Init函数,右键跳转定义,直接复制结构体类型名NVIC_InitTypeDef,粘贴,取名NVIC_InitStructure,引出结构体成员,最后把结构体变量放在NVIC_Init的参数里,前面加上取地址的符号&

依次看参数,右键跳转定义

typedef struct
{
uint8_t NVIC_IRQChannel;                    /*!< Specifies the IRQ channel to be enabled or disabled.
This parameter can be a value of @ref IRQn_Type
(For the complete STM32 Devices IRQ Channels list, please
refer to stm32f10x.h file) */

uint8_t NVIC_IRQChannelPreemptionPriority;  /*!< Specifies the pre-emption priority for the IRQ channel
specified in NVIC_IRQChannel. This parameter can be a value
between 0 and 15 as described in the table @ref NVIC_Priority_Table */

uint8_t NVIC_IRQChannelSubPriority;         /*!< Specifies the subpriority level for the IRQ channel specified
in NVIC_IRQChannel. This parameter can be a value
between 0 and 15 as described in the table @ref NVIC_Priority_Table */

FunctionalState NVIC_IRQChannelCmd;         /*!< Specifies whether the IRQ channel defined in NVIC_IRQChannel
will be enabled or disabled.
This parameter can be set either to ENABLE or DISABLE */
} NVIC_InitTypeDef;

第一个NVIC_IRQChannel,查看注释指定要启用或禁用的中断通道。 此参数可以是IRQn_Type里的一个值。下面有个括号,写的是对于完整的STM32中断通道列表,请参考stm32f10x.h文件,这个意思是,这个IRQn_Type的定义不在这个文件,你要到stm32f10x.h文件中去找,

Ctrl+F搜索一下,是搜索不到的,在第而个框中将搜索范围从当前文件换成当前工程,然后再搜索,就会跳转到stm32f10x.h文件中去,定义就再这里,我们可以在上面的列表选择,可以看到有非常多的中断通道,

因为这个库函数可以兼容所有F1系列的芯片,但是不同的芯片的中断通道列表是不一样的,所以这里有很多条件编译,用来选择你使用的芯片的中断通道列表,我们可以点击条件编译左边的减号将所有的条件编译都折叠起来,我们芯片用的都是MD中等密度的,所以只需要选择展开这个MD的条件编译即可

在这个表中,我们可以找到这个EXTI15_10_IRQn,STM32的EXTI10到EXTI15都融合到了这个通道中,所以我们复制这一个,放到这里,通道就指定好了

接着下一个,NVIC_IRQChannelCmd跳转定义,解释是,指定通道是使能还是失能,参数可以是ENABLE或DISABLE,我们选择ENABLE

下面两个参数,就是指定所选通道的抢占优先级和响应优先级了,查看定义,这两个值可以是0到15,具体的值,可以查看表NVIC_Priority_Table里的描述

因为我们这个程序的优先级只有一个,所以,中断优先级的配置也是非常随意的,这里都设置为1,优先级是在多个中断同时申请,产生拥挤时才有作用,这只有一个中断,优先级就随便了,

到这里NVIC就配置好了,整个外部中断的配置就结束了

外部中断的信号从GPIO到AFIO,再到EXTI,再到NVIC,最终通向CPU,这样才能让主程序跳转到中断程序执行

那么中断程序应该放在哪里呢,这就需要我们写一个中断函数了,在STM32中中断函数的名字都是固定的,每个中断通道都对应一个中断函数,中断函数的名字我们可以参考一下启动文件startup_stm32f10x_md.s,我们找到启动文件,打开看一下,在这里找一下,可以看到定义的中断向量表【应该是61开始】,这里面以IRQHandler结尾的字符串就是中断函数的名字,我们可以找到这个EXTI15_10_IRQHandler这一项,这就是EXTI15_10的中断函数,复制

在原来的结尾写函数

void EXTI15_10_IRQHander(void)
{

}

在中断函数中一般都是先进行一个中断标志位的判断,确保是我们想要的中断源触发的这个函数,因为这个函数EXTI10到EXTI15都能进来,所以要先判断一下是不是我们想要的EXTI14进来的,这时我们就需要到exti.h中看一下

复制一下这个EXTI_GetITStatus(),粘贴到函数中,看一下参数

/**
* @brief  Checks whether the specified EXTI line is asserted or not.
* @param  EXTI_Line: specifies the EXTI line to check.
*   This parameter can be:
*     @arg EXTI_Linex: External interrupt line x where x(0..19)
* @retval The new state of EXTI_Line (SET or RESET).//返回值是SET or RESET
*/

第一个参数,复制一下这个EXTI_Linex,然后x改为14,作为参数,判断EXTI14的中断位是否为1,返回值是SET 或者 RESET,那么我们就if判断一下返回值是否为SET,是的话,我们就能执行中断程序了,中断程序结束后,一定要再调用一下清除中断标志位的函数,因为只要中断标志位置1了,程序就会跳转到中断函数,如果不清除中断标志位,那么它就会一直申请中断,这样程序就会不断响应中断 ,程序就卡死在中断函数中了,所以我们每次中断程序结束后,都应该清除一下中断标志位,复制EXTI_ClearITPendingBit,放到if函数最后,参数与判断函数相同

这样中断的全部逻辑就写好了

#include “stm32f10x.h”                  // Device header

void CounterSensor_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
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_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource14);

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_IRQChannelPreemptionPriority=1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority=1;
NVIC_Init(&NVIC_InitStructure);
}
void EXTI15_10_IRQHander(void)
{
if(EXTI_GetITStatus(EXTI_Line14)==SET)
{

EXTI_ClearITPendingBit(EXTI_Line14);
}
}

复制初始化函数第一行,放在头文件里声明一下,中断函数不需要声明,它不需要调用,它是自动执行的

写完函数最好编译一下,不然有可能显示不出来我们新写的函数

main头文件包含CountSensor.h

然后在主循环前调用CountSensor_Init函数初始化,然后就可以编译了,我们想要计次,可以在模块最前方定义一个变量,类型unit16_t, 名字可以是CountSensor_Count,然后在中断函数里写CountSensor_Count++这样就行了【全局函数变量默认为零,这个变量主函数不能直接用,所以得用函数返回给主函数】

然后定义一个函数,返回该变量到主函数,

uint16_t CountSensor_Get(void)
{
return CountSensor_Count;
}

在头文件中声明该函数

然后在主文件的led函数中,显示这个数

#include “stm32f10x.h”                  // Device header
#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);
}
}

就ok了

红外对射计次的错误
  • 若遮断后放开计数两次,可以尝试将上拉输入改为下拉输入
  • 也可以在计次函数中延时一些
  • 若模块无法被找到,路径什么均正确,可能在存文件的时候,文件没有存进对应的文件夹
  • 中断函数最好复制
说点什么
支持Markdown语法
好耶,沙发还空着ヾ(≧▽≦*)o
Loading...