STM32学习笔记(三)丨中断系统丨EXTI外部中断(对射式红外传感器计次、旋转编码器计次)

本篇文章包含的内容

  • 一、中断系统
    • 1.1 中断的定义
    • 1.2 中断优先级
    • 1.3 中断的嵌套
    • 1.4 STM32中的中断系统
      • 1.4.1 STM32的中断资源
      • 1.4.2 嵌套中断向量控制器 NVIC(Nested Vectored Interrupt Controller)
      • 1.4.3 NVIC与优先级分组
  • 二、EXTI 外部中断
    • 2.1 EXTI(Extern Interrupt)简介
      • 2.1.1 EXTI支持的触发方式
      • 2.1.2 EXTI支持监测的GPIO及其条件
      • 2.1.3 EXTI占用的通道
      • 2.1.4 EXTI触发的响应方式
    • 2.2 EXTI的工作原理
      • 2.2.1 AFIO(Alternate function I/O)复用功能输入输出进行中断引脚选择
      • 2.2.2 EXTI的边沿检测与控制功能
    • 2.3 EXTI的实际应用
      • 2.3.1 旋转编码器简介
      • 2.3.2 EXTI的配置流程和中断函数的定义
      • 2.3.3 对射式红外传感器计次
      • 2.3.3 旋转编码器计次
    • 2.4 中断函数编程的几点建议

​  本次课程采用单片机型号为STM32F103C8T6。
​  课程链接:江科大自化协 STM32入门教程


  往期笔记链接:
  STM32学习笔记(一)丨建立工程丨GPIO 通用输入输出
  STM32学习笔记(二)丨STM32程序调试丨OLED的使用


  本篇文章的内容我愿称之为史诗级副本(感觉非常难,但是非常重要)。


一、中断系统

  中断系统,是管理和执行中断的逻辑结构,而外部中断EXTI,定时中断TIM等外设都是能产生中断的外设。

1.1 中断的定义

  在主程序运行过程中,出现了特定的中断触发条件(中断源),使得CPU立即暂停当前正在运行的程序,转而去处理中断程序(程序由硬件电路自动跳转到中断程序),处理完成后又返回原来被暂停的位置继续运行。
STM32学习笔记(三)丨中断系统丨EXTI外部中断(对射式红外传感器计次、旋转编码器计次)_第1张图片
  中断触发的条件多种多样,对于外部中断EXTI来说,可以是引脚的电平发生了跳变;对于定时器TIM来说,可以是定时时间到;对于串口通信USART来说,可以是接收到了数据……当这些情况发生时,情况一般比较“紧急”,如果不加以处理,可能会造成电平信号或者数据流无法正常读取(对于串口通信USART,下一个数据到来会覆盖前一个数据),所以我们希望当这些事件发生时,CPU能立即停止当前正在执行的程序,转而去处理中断时间的程序;并且对于CPU来说,CPU并不知道这些事件什么时候发生,为了防止外部中断被忽略,或者串口数据被覆盖,主程序就只能不断地查询这些事件是否发生,而不能去执行其他程序了。
  为了程序能在中断返回后可以继续原来的工作,在中断执行前,会对程序的“现场”进行保护,中断执行后,会再还原“现场”。使用C语言编程时,这部分工作由编译器自动完成。如果使用汇编语言进行编程,就需要手动保护“现场”。

1.2 中断优先级

  当有多个中断源同时申请中断时,CPU会根据中断源的轻重缓急进行裁决,优先响应更加紧急的中断源。
  中断优先级是程序设计者根据程序设计的需求自己规定的(当优先级相同时,将按照一定的规则依次处理,具体可根据向量表查询)。

1.3 中断的嵌套

  当一个中断程序正在运行时,又有新的更高优先级的中断源申请中断,CPU再次暂停当前中断程序,转而去处理新的中断程序,处理完成后依次进行返回。
STM32学习笔记(三)丨中断系统丨EXTI外部中断(对射式红外传感器计次、旋转编码器计次)_第2张图片

1.4 STM32中的中断系统

1.4.1 STM32的中断资源

  本课程使用的STM32F103C8T6含有68个(这个数字由具体的芯片型号决定)可屏蔽中断通道(中断源),包含外部中断EXTI、定时器TIM、模数转换器ADC、串口通信USART、SPI通信、I2C通信、实时时钟RTC等多个外设。
  STM32(本课程使用的型号)中的中断资源如下表所示(表中灰色的部分是内核的中断资源,白色的部分是外设的中断资源):
STM32学习笔记(三)丨中断系统丨EXTI外部中断(对射式红外传感器计次、旋转编码器计次)_第3张图片
STM32学习笔记(三)丨中断系统丨EXTI外部中断(对射式红外传感器计次、旋转编码器计次)_第4张图片
STM32学习笔记(三)丨中断系统丨EXTI外部中断(对射式红外传感器计次、旋转编码器计次)_第5张图片
STM32学习笔记(三)丨中断系统丨EXTI外部中断(对射式红外传感器计次、旋转编码器计次)_第6张图片
  STM32中,每一个中断资源都有对应的地址(如上表所示)。在编程实现中,我们将中断要执行的操作放在一个子函数中,但是这个中断函数的地址由编译器动态分配,并不是固定的。但是由于硬件电路的限制,中断跳转时必须跳转到固定的地址执行程序,所以为了使硬件能够跳转到一个不固定的中断函数里,就需要在内存中定义一个固定的地址列表,中断条件满足后,就跳转到对应的固定位置,然后由编译器和一句跳转到中断函数位置的代码,这样中断就能跳转到任意位置了。在上述过程中,我们将中断地址的列表成为中断向量表,相当于中断跳转的一个跳板。
  应用C语言编程时,我们无需关心中断向量表的工作,这部分工作由编译器自动完成。

1.4.2 嵌套中断向量控制器 NVIC(Nested Vectored Interrupt Controller)

  STM32采用NVIC统一管理中断和分配优先级。每个中断通道都拥有16个可编程的优先等级,还可以对优先级进行分组处理,即进一步设置抢占优先级响应优先级
  NVIC的基本结构如下图所示:
STM32学习笔记(三)丨中断系统丨EXTI外部中断(对射式红外传感器计次、旋转编码器计次)_第7张图片
  NVIC是一个内核外设,相当于CPU的一个“秘书”,也相当于医院中为医生和患者服务的叫号系统。STM32的中断种类非常多,如果将这些中断全部连接给CPU,在设计上的复杂度将难以估量;斌且如果很多中断同时申请,或者很多中断产生了拥堵,也会增大CPU的处理难度,变相降低了CPU的运算效率。
  NVIC有很多输入口,可以连接不同外设的多条中断线路(通道)。上图中NVIC连接的每个外设线路上的 n n n 表示一个外设可能会同时占用多个中断通道。
  NVIC只有一个输出口,连接到CPU。NVIC根据每个中断的优先级分配中断的先后顺序,然后通过输出口告知CPU应该处理哪个中断。对于中断先后顺序分配的任务,CPU不需要知道。
  每个中断通道都拥有16个可编程的优先等级。 NVIC可以由此对优先级进行分组。

1.4.3 NVIC与优先级分组

  为了处理不同形式的优先级,NVIC可以对优先级进行分组。在STM32中,中断优先级分为抢占优先级响应优先级。以医院中的叫号系统作比喻,抢占优先级高的可以进行中断嵌套,即中断当前的中断程序;响应优先级高的可以优先排队,相当于“插队”;抢占优先级和响应优先级均相同的按中断号排队(向量表中的默认优先级)。
  每个中断都拥有16个可编程的优先等级,为了把优先级分为抢占优先级和响应优先级,NVIC通过优先级寄存器的4位决定(4位二进制数,可以表示16个数字,即 0-15,对应16个优先等级)。优先级寄存器代表的数值越小,优先级越高,0就是最高优先级。这4位可以进行切分,分为高 n n n 位(对应 2 n 2^n 2n 个抢占优先级)的抢占优先级和低 4 − n 4-n 4n 位(对应 2 4 − n 2^{4-n} 24n 个响应优先级)的响应优先级 (注意:这里的高低指优先级寄存器的高低位,不是优先级寄存器代表的数)所以STM32的中断不存在先来后到的排队方式,在任何时候,都是优先级高的中断先响应。
STM32学习笔记(三)丨中断系统丨EXTI外部中断(对射式红外传感器计次、旋转编码器计次)_第8张图片

二、EXTI 外部中断

2.1 EXTI(Extern Interrupt)简介

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

2.1.1 EXTI支持的触发方式

  • 上升沿:引脚电平从低电平上升为高电平
  • 下降沿:引脚电平从高电平下降为低电平
  • 双边沿:引脚电平发生变化
  • 软件触发:引脚电平不发生变化,由软件代码执行中断程序

2.1.2 EXTI支持监测的GPIO及其条件

  EXTI支持检测所有的GPIO口,但相同的Pin不能同时触发中断。“不能同时”的意思是EXTI不能同时监测例如PA0、PB0,或者PA1、PA2、PA3这样的GPIO_Pin相同的端口。所以如果要使用多个中断引脚,就要选择GPIO_Pin不同的引脚。

2.1.3 EXTI占用的通道

  GPIO占用20个中断输入通道。其中包括16个GPIO_Pin,外加PVD(电源电压监测)输出、RTC闹钟、USB唤醒、ETH以太网唤醒。在通道中,16个GPIO_Pin是EXTI的主要功能,其余四个是“蹭网”功能。
  EXTI的另一个功能是从低功耗的停止模式下唤醒STM32。 对于电源电压检测PVD,当电源从电压过低恢复时,就需要PVD借助外部中断推出停止模式。对于RTC闹钟来说,为了省电,RTC定一个闹钟之后,STM32会进入停止模式,等到闹钟响的时候再唤醒。USB唤醒和ETH以太网唤醒也是类似的作用。

2.1.4 EXTI触发的响应方式

  • 中断响应:申请中断,让CPU执行中断函数
  • 事件响应:这是STM32对于EXTI增加的一种额外的功能。当EXTI检测到引脚电平的变化时,正常的流程是选择触发中断,但在STM32中,也可以选择触发一个事件。这时外部中断的信号不会通向CPU,而是通向其他外设,用来触发其他外设的操作。比如触发ADC转换,触发DMA等。事件响应不会触发中断,而是触发别的外设操作,相当于外设之间的联合工作

2.2 EXTI的工作原理

  EXTI的基本结构及工作方式如下图所示:
STM32学习笔记(三)丨中断系统丨EXTI外部中断(对射式红外传感器计次、旋转编码器计次)_第9张图片

2.2.1 AFIO(Alternate function I/O)复用功能输入输出进行中断引脚选择

  AFIO和GPIO相对,分别称为复用功能输入输出和通用功能输入输出。AFIO主要用于引脚复用功能的选择和重定义。 在STM32中,AFIO主要执行以下两个任务:

  • 复用引脚功能重映射:将引脚的默认复用功能替换为重定义功能
  • 中断引脚选择

  每个GPIO外设有16个引脚,但是EXTI中只有16个GPIO通道,如果每个GPIO的引脚都占用一个EXTI中的GPIO通道,那么EXTI中的GPIO通道明显不够用。所以 需要AFIO实现中断引脚选择的电路模块。 AFIO实际上就是一个 数据选择器,它可以选择所有GPIO的其中的16个引脚接入到EXTI的通道中。它的选择规则是 :对于PA0、PB0、PC0、…、PG0这样的相同的GPIO_Pin引脚,AFIO只能选择一个接入到EXTI的通道0中,类似的,PA1、PB1、PC1、…、PG1中,只有一个能接入EXTI的通道1中。这就是所有的GPIO口都能触发中断,但是相同的GPIO_Pin的端口不能同时触发中断的原因。上述过程如下图所示:
STM32学习笔记(三)丨中断系统丨EXTI外部中断(对射式红外传感器计次、旋转编码器计次)_第10张图片

2.2.2 EXTI的边沿检测与控制功能

  20个输入信号(16个GPIO_Pin通道、PVD、RTC、USB、ETH)经过EXTI后分成两路输出:通往NVIC触发中断响应通往其他外设触发事件响应。在通往NVIC的通道中,按理来说应该有20路输出,但ST公司为了节省NVIC的中断资源,将EXTI的5 ~ 9通道合并称一个通道输出(EXTI9_5),将10 ~ 15合并到一个通道输出(EXTI15_10),在编程的时候,在这两个中断函数中,需要再根据标志位来区分到底是哪个端口的中断条件触发。
STM32学习笔记(三)丨中断系统丨EXTI外部中断(对射式红外传感器计次、旋转编码器计次)_第11张图片
  那么EXTI如何实现边沿的检测、中断的响应以及事件的响应呢?EXTI的内部结构框图如下图所示:
STM32学习笔记(三)丨中断系统丨EXTI外部中断(对射式红外传感器计次、旋转编码器计次)_第12张图片
  首先,由20条输入信号线输入的信号首先通过边沿检测电路。这个边沿检测电路的检测方式由上升沿触发选择寄存器和下降沿触发选择寄存器控制,由这两个寄存器可以将EXTI的监测的触发方式控制为上升沿触发、下降沿触发或双边沿触发;之后输出信号和软件中断事件寄存器通过一个或门直接输出(只要软件中断有效,或门的输出将为“1”,将立即触发中断,与外部电平无关)。输出信号兵分两路,一路连接到EXTI中断控制器触发中断响应,另一路连接到其他外设触发事件响应。

  • 触发中断响应:或门的输出信号通过一个请求挂起寄存器,再和中断屏蔽寄存器的输出信号通过一个与门输出到NVIC中断控制器。读取请求挂起寄存器就相当于读取中断的标志位,可以通过读取判断是哪个通道发生的中断。如果中断屏蔽寄存器允许中断(输出“1”),挂起寄存器输出的“1”将会输出到NVIC中断控制器中。
  • 触发事件响应:或门的输出信号与事件屏蔽寄存器的输出信号通过一个与门接入一个脉冲发生器,之后连接到其他外设。

  上图中的中断屏蔽寄存器和事件屏蔽寄存器都相当于电子开关的作用。对于它们与图中的寄存器,我们可以通过外设接口和总线访问这些寄存器。

2.3 EXTI的实际应用

  在实际应用中,到底什么样的设备需要使用外部中断,使用外部中断又有什么好处?使用外部中断的模块输出的信号有如下特性:

  • 外部:由外部模块驱动,STM32只能被动读取
  • 突发:STM32不知道信号什么时候来
  • 快速:如果STM32没有及时读取,就会错过很多信号(波形)

  例如,旋转编码器和红外接收模块输出的信号就具有以上特性。开关信号是其中的一个特例。 虽然按键按下也是外部驱动的突发事件,但是并不推荐使用外部中断来读取按键,原因是外部中断不好处理按键抖动和松手检测的问题;对于按键而言,由于按下按键的物理动作较长和按键抖动的问题,它的输出波形也不一定是转瞬即逝的,所以要求不高的情况下可以在主程序中循环读取,要求较高的情况下可以采用定时器TIM中断读取的方式,以达到后台读取按键值,不阻塞主程序,还可以处理按键抖动和松手检测的问题。

2.3.1 旋转编码器简介

  旋转编码器是用来测量旋转的位置、速度、方向的装置。当其旋转轴旋转时,其输出端可以输出与旋转速度和方向对应的方波信号,读取方波信号的频率和相位信息即可得知旋转轴的速度和方向。读取两路输出信号的相位差,或者一路方波信号对应位置和速度,另一路高低电平信号代表方向的输出信号即可得知旋转的方向。旋转编码器主要由以下四种类型:

  1. 光栅式旋转编码器:通过对射式红外传感器测速。当光栅编码盘转动时,输出口就会输出一个对应的方波,方波的个数对应旋转的角度(位置),方波的频率对应转速(速度)。它的缺点是无法确认旋转的方向。
    STM32学习笔记(三)丨中断系统丨EXTI外部中断(对射式红外传感器计次、旋转编码器计次)_第13张图片

  2. 机械触点式旋转编码器(本次课程使用):一般用于调节某一变量,例如调节音量。如下图所示,两侧金属触点的外侧分别连接A、B引脚,内测都连接到C引脚。中间的金属片是一个按键,控制上方的两个引脚的通断(本次实验没有用到)。当编码盘旋转时,依次接通两侧的金属触点,且可以让两侧金属触点的通断产生一个 π 2 \frac {\pi} 2 2π的相位差,通过检测A、B引脚的相位差,就可以在测量旋转角度、速度的基础上测量旋转的方向了(编码盘正传时,B滞后A π 2 \frac {\pi} 2 2π,编码盘反转时,B超前A π 2 \frac {\pi} 2 2π)。这种相位相差 π 2 \frac {\pi} 2 2π的波形称为正交波形。 带正交波形输出信号的编码器是可以输出旋转方向的。
    STM32学习笔记(三)丨中断系统丨EXTI外部中断(对射式红外传感器计次、旋转编码器计次)_第14张图片
    旋转编码器的硬件电路如下图所示(本次实验中C引脚没有使用 ):
    STM32学习笔记(三)丨中断系统丨EXTI外部中断(对射式红外传感器计次、旋转编码器计次)_第15张图片

  3. 霍尔传感器式旋转编码器:直接连接在电机后,通过圆形磁铁和两个霍尔传感器,同样可以输出正交的方波信号。原理与上一种机械触点式旋转编码器类似。
    STM32学习笔记(三)丨中断系统丨EXTI外部中断(对射式红外传感器计次、旋转编码器计次)_第16张图片

  4. 独立的旋转编码器元件:当输入转动时,输出就会有波形。详细用法请参考手册。
    STM32学习笔记(三)丨中断系统丨EXTI外部中断(对射式红外传感器计次、旋转编码器计次)_第17张图片

2.3.2 EXTI的配置流程和中断函数的定义

  使用EXTI的配置顺序如下:

  1. 打开GPIO和AFIO的外设时钟。EXTI和NVIC的时钟自动开启,不需要手动开启。按理来说EXTI作为一个独立外设需要开启时钟,但是寄存器中没有EXTI时钟的控制位,原因可能与EXTI的唤醒功能或者电路设计考量有关。NVIC是内核的外设,内核的外设都不需要手动开启时钟(RCC也只能管理内核外的外设,对内核里的外设没有操作权限)。

  2. 配置GPIO

    像这种外设使用GPIO的情况,如果不清楚GPIO应该配置为什么模式,可以参考手册。EXTI输入线推荐配置为浮空输入、带上拉输入或带下拉输入,所以配置哪一种都可以。
    STM32学习笔记(三)丨中断系统丨EXTI外部中断(对射式红外传感器计次、旋转编码器计次)_第18张图片

  3. 配置AFIO。对于AFIO外设,ST公司没有给它分配专门的库函数文件,它的库函数和GPIO在同一个库函数中。打开stm32f10x_gpio.h文件的最后可以找到以下函数(有些函数在GPIO一节没有讲解到,但是也不常用,这里作统一了解):

    // 复位AFIO外设,调用后将清除AFIO的全部配置
    void GPIO_AFIODeInit(void);	
    
    // 锁定某个GPIO端口的配置
    void GPIO_PinLockConfig(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);	
    
    // 下面两个函数用来配置AFIO的事件输出功能
    void GPIO_EventOutputConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource);
    void GPIO_EventOutputCmd(FunctionalState NewState);
    
    // 进行引脚功能重映射
    void GPIO_PinRemapConfig(uint32_t GPIO_Remap, FunctionalState NewState);
    
    // 配置AFIO实现EXTI中断引脚选择
    void GPIO_EXTILineConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource);
    
    // 配置以太网需要用到的函数(本课程没有涉及)
    void GPIO_ETH_MediaInterfaceConfig(uint32_t GPIO_ETH_MediaInterface);
    

    对于本节课使用的GPIO_EXTILineConfig函数,它虽然以GPIO开头,但实际上操作的是AFIO的寄存器:

    /**
      * @brief  Selects the GPIO pin used as EXTI Line.
      * @param  GPIO_PortSource: selects the GPIO port to be used as source for EXTI lines.
      *   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)));
    }
    
    
  4. 配置EXTI。首先来学习EXTI的库函数及其作用(有些不常用,仅作了解即可):

    // 下面三个函数是一个外设的“模板函数”,即基本所有的外设都拥有以下三个函数,定义和用法都类似
    // 清除EXTI的所有配置,清除为上电默认状态
    void EXTI_DeInit(void);
    // 用结构体变量配置EXTI
    void EXTI_Init(EXTI_InitTypeDef* EXTI_InitStruct);
    // 给用来参数配置的结构体变量赋一个默认值
    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 EXTI_ClearITPendingBit(uint32_t EXTI_Line);	// 清除中断挂起标志位
    
  5. 配置NVIC。NVIC是内核的外设,其库函数定义在misc.h中。库函数定义如下所示:

    void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup);	// 对中断进行分组,配置抢占优先级pre-emption和响应优先级subpriority
    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时,需要按照以下步骤配置(以下面的对射式红外传感器计次实验为例):

    1. 对中断优先级进行进行分组,使用NVIC_PriorityGroupConfig函数,具体参数详见函数定义注释。
    2. 定义NVIC_Init需要的结构体变量并给结构体的成员变量赋值。
      NVIC_InitTypeDef NVIC_InitStructure;
      NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn;		// 配置中断通道需要参考stm32f10x.h文件中的中断列表
      // 列表中包含所有的f1芯片,不同的芯片中断通道列表不同,故需要根据芯片型号选择STM32F10X_MD的中断列表
      NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;	// 指定中断通道是使能还是失能
      // 下面两个参数的取值范围需要参考NVIC_Priority_Table中不同优先级分组的取值范围
      NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;	// 将中断定义为NVIC抢占优先级中的取值
      NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;	// 将中断定义NVIC响应优先级中的取值
      

  定义中断函数需要注意:STM32中中断函数的名称都是固定的。在启动文件startup_stm32f10x_md.s中的中断向量表中可以找到以IRQHandler结尾的就是中断函数的名称。在中断函数中,我们还需要对中断标志位进行判断。因为如果选择EXTI15_10_IRQHandler函数,EXTI15到EXTI10都能进入函数,如果连接EXTI14,我们需要确定是我们想要的中断源(EXTI14)触发了这个函数。使用EXTI_GetITStatus函数判断中断标志位,在中断函数中完成相关功能后还需要使用EXTI_ClearITPendingBit函数清除中断挂起标志位。形式如下所示:

void EXTI15_10_IRQHandler(void)
{
	if (EXTI_GetITStatus(EXTI_Line14) == SET)	// 检查EXTI_Line14的中断标志位是否为1
	{
		/*
		中断函数中需要执行的操作...
		*/
		EXTI_ClearITPendingBit(EXTI_Line14);	// 清除EXTI_Line14的中断标志位	
	}
}

2.3.3 对射式红外传感器计次

  建立工程并添加工程文件CountSensor.c/.h后,首先要在CountSensor_Init函数中配置实现该功能需要的全部外设。

  硬件连接图和源码如下所示:
STM32学习笔记(三)丨中断系统丨EXTI外部中断(对射式红外传感器计次、旋转编码器计次)_第19张图片

  • CountSensor.c
#include "stm32f10x.h"                  // Device header

uint16_t CountSensor_Conut;

void CountSensor_Init(void)
{
	// 开启所需要使用的外设的时钟,EXTI和NVIC的时钟自动开启
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	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);
	
	// 配置AFIO
	GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource14);
	
	// 配置EXTI(将EXTI的第14条线路配置为中断触发模式)
	EXTI_InitTypeDef EXTI_InitStructure;	// 定义结构体变量
	EXTI_InitStructure.EXTI_Line = EXTI_Line14;		// EXTI对应的输入线路
	EXTI_InitStructure.EXTI_LineCmd = ENABLE;		// 是否开启中断
	EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;		// EXTI配置为中断触发模式(与事件触发模式相对)
	EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;	// 下降沿触发(该参数的定义在官方文件中是错误的,应该在枚举变量EXTITrigger_TypeDef中选择)
	EXTI_Init(&EXTI_InitStructure);
	
	// 配置NVIC(分组方式整个芯片只能用同一种)
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);		// NVIC中断优先级分组,整个工程只能使用一个分组,如果在模块中配置需要保证每个模块中的分组方式都相同
	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;	// 指定中断通道是使能还是失能
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;	// NVIC抢占优先级
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;	// NVIC响应优先级
	NVIC_Init(&NVIC_InitStructure);
}

uint16_t CountSensor_Get(void)
{
	return CountSensor_Conut;
}

void EXTI15_10_IRQHandler(void)
{
	if (EXTI_GetITStatus(EXTI_Line14) == SET)	// 检查EXTI_Line14的中断标志位是否为1
	{
		if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_14) == 0)	// 如果数字乱跳,可以在这里再次检查端口的电平信号
		{
			CountSensor_Conut ++;
		}
		EXTI_ClearITPendingBit(EXTI_Line14);	// 清除EXTI_Line14的中断标志位	
	}
}

  • main.c
#include "stm32f10x.h"                  // Device header
#include "OLED.h"
#include "CountSensor.h"

int main()
{
	OLED_Init();
	OLED_ShowString(1, 1, "Count:");	// 显示一个字符串
	CountSensor_Init();
	
	while(1)
	{
		OLED_ShowNum(1, 7, CountSensor_Get(), 5);
	}
}

2.3.3 旋转编码器计次

  硬件连接图和源码如下所示:
STM32学习笔记(三)丨中断系统丨EXTI外部中断(对射式红外传感器计次、旋转编码器计次)_第20张图片

  • Encoder.c
#include "stm32f10x.h"                  // Device header

int16_t Encoder_Count;

void Encoder_Init(void)
{
	// 开启所需要使用的外设的时钟,EXTI和NVIC的时钟自动开启
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
	
	// 配置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);
	
	// 配置AFIO
	GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource0);		// 将AFIO的第0个数据选择器拨到GPIOB上
	GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource1);		// 将AFIO的第1个数据选择器拨到GPIOB上
	
	// 配置EXTI(将EXTI的第0和1条线路配置为中断触发模式)
	EXTI_InitTypeDef EXTI_InitStructure;	// 定义结构体变量
	EXTI_InitStructure.EXTI_Line = EXTI_Line0 | EXTI_Line1;		// EXTI对应的输入线路,该变量可以时EXTI_Linex的任意组合
	EXTI_InitStructure.EXTI_LineCmd = ENABLE;		// 是否开启中断
	EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;		// EXTI配置为中断触发模式
	EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;	// 下降沿触发
	EXTI_Init(&EXTI_InitStructure);
	
	// 配置NVIC(分组方式整个芯片只能用同一种)
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);		// NVIC中断优先级分组
	// 对两个通道都进行优先级设置
	NVIC_InitTypeDef NVIC_InitStructure;	// 这个变量可以重复使用
	
	NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;	// 指定中断通道是使能还是失能
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;	// NVIC配置抢占优先级
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;	// NVIC配置响应优先级
	NVIC_Init(&NVIC_InitStructure);
	
	NVIC_InitStructure.NVIC_IRQChannel = EXTI1_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;	// 指定中断通道是使能还是失能
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;	// NVIC配置抢占优先级
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;	// NVIC配置响应优先级
	NVIC_Init(&NVIC_InitStructure);
}

int16_t Encoder_Get(void)
{
//	return Encoder_Count;
	// 这里也可不不直接返回Encoder_Count,选择返回Encoder_Count的变化量
	int16_t Temp;
	Temp = Encoder_Count;
	Encoder_Count = 0;
	return Temp;
}

void EXTI0_IRQHandler(void)
{
	if (EXTI_GetITStatus(EXTI_Line0) == SET)	// 检查EXTI_Line0的中断标志位是否为1
	{
		if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == 0)	// 检查PB0是否稳定在0,方式数字乱跳
		{			
			if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0)	// 反转(A的下降沿时B为低电平)
			{
				Encoder_Count --;
			}
		}
		EXTI_ClearITPendingBit(EXTI_Line0);	// 清除EXTI_Line0的中断标志位	
	}
}

void EXTI1_IRQHandler(void)
{
	if (EXTI_GetITStatus(EXTI_Line1) == SET)	// 检查EXTI_Line1的中断标志位是否为1
	{
		if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0)	// 检查PB1是否稳定在0,方式数字乱跳
		{			
			if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == 0)	// 正转(B的下降沿时A为低电平)
			{
				Encoder_Count ++;
			}
		}
		EXTI_ClearITPendingBit(EXTI_Line1);	// 清除EXTI_Line1的中断标志位	
	}
}

  • main.c
#include "stm32f10x.h"                  // Device header
#include "OLED.h"
#include "Encoder.h"

int16_t Num = 0;

int main()
{
	OLED_Init();
	OLED_ShowString(1, 1, "Num:");	// 显示一个字符串
	Encoder_Init();
	
	while(1)
	{
		Num += Encoder_Get();
		OLED_ShowSignedNum(1, 5, Num, 5);
	}
}

2.4 中断函数编程的几点建议

  1. 在中断函数中,最好不要执行耗时过长的代码,否则有可能会严重阻塞主程序
  2. 不要在中断函数和主函数中调用相同的函数或操作同一个硬件。中断执行前编译器对现场的保护只能保证主程序不出错,但是如果对于外部的函数和硬件没有添加保护措施,就很容易出错。

  下一篇:STM32学习笔记(四)丨TIM 定时中断 ……正在更新中……


  欢迎点赞,收藏~ 如有谬误敬请在评论区不吝告知,感激不尽!博主将持续更新有关嵌入式开发、机器学习方面的学习笔记~

你可能感兴趣的:(STM32,学习笔记,stm32,单片机,学习)