中断EXTI、NVIC以及AFIO的一些总结

1.中断的介绍

        在主程序运行过程中,出现了特定的中断触发条件(中断源),使得CPU暂停当前正在运行的程序,转而去处理中断程序,处理完成后又返回原来被暂停的位置继续运行,同时需要知道中断优先级和中断嵌套的概念。

        中断优先级:当有多个中断源同时申请中断时,CPU会根据中断源的轻重缓急进行裁决,优先响应更加紧急的中断源,中断优先级又被分为抢占优先级以及响应优先级

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

        当执行一个程序,突然接收到中断时,该中断信号会进入NVIC(Nested vectoredinterrupt controller即嵌套向量中断控制器),NVIC是挂在在STM32内部的属于内核部分,经过NVIC的裁决,然后再将中断信号给到CPU,从而执行对应的中断。

        同时,在执行中断前,会对程序进行“现场保护”。例如,此时正在每秒计数值加一的方式计数,计数计到10的时候突然产生一个中断,中断标志位置起,中断函数所执行的动作可以自己设置,例如点亮一个LED2秒钟的灯,软件写入清除中断标志位,中断结束,回到主程序继续从10开始计数。

        中断嵌套,就是在原本正在执行中断的情况下,进来一个(抢占)优先级更高的中断,经NVIC裁决,就会先对原本执行的中断进行“现场保护”,然后执行那个优先级更高的中断,待优先级更高的中断执行完毕,也就是该中断标志位清除后再回到原本的中断函数继续中断操作。

        以上是中断执行的大致步骤,我们需要用到中断时,同样和GPIO一样,需要对中断进行配置,而对中断进行配置就需要用到NVIC以及EXTI(External interrupt/event controller)外部中断/事件控制器,接下来就对NVIC和EXTI内部的寄存器进行展开,因为所有的配置操作,实际上都是对该外设内部的寄存器进行置1或置0操作。

2.NIVC(Nested vectored interrupt controller嵌套向量中断控制器)

       中断向量表,68个可屏蔽中断通道,包含EXTI、TIM、ADC、USART、SPI、I2C、RTC等多个外设。下面面描述了所有的中断函数,其中未写明位置的灰色部分则是表示内核部分的异常(实际上中断和异常是等价的)函数也即内核中断函数,这些中断函数的优先级默认最高,换句话说,在任何情况这些异常中断函数的执行优先级都高于非灰色中断函数(可编程中断函数,可屏蔽中断)。

        然后是外设中断函数(非灰色中断函数),这里只列举了部分,基本上都是片上外设的中断函数,这些中断函数都是可以通过软件编程来调整优先级的大小,从而使程序先后执行中断函数。

        只有当两个中断函数优先级一致的时候,才会通过中断向量表的优先级大小来进行判断,优先级小的先执行。

        同时每一个中断函数都标有地址,这是因为,程序中的中断函数地址不固定的,而当需要执行下面某一个中断函数时,由于硬件限制,只能够跳转固定位置为了让硬件跳转到一个不固定的中断函数去执行它,这样就需要用到这些在内存中定义的固定地址。当中断发生时,就跳转到这些地址,然后通过编译器再加上一条跳转到中断函数的代码,这样就能够执行中断函数了。

        大致图解如下:

        首先,明确NVIC是属于内核外设,所以NVIC的时钟不需要由RCC进行时钟开启配置,也就是说,当STM32芯片上电后NVIC的时钟就已经开启了,它随着内核的启动而自动激活。

        值得一提的是,RCC开启时钟,开启的都是片上外设例如TIM1,USART等外设的时钟。

        因为NVIC属于内核外设所以再对NVIC的一些参数查找的时候,需要到内核描述的地方去寻找,例如NVIC的库函数描述在misc文件里头,并没有将该文件指明是NVIC

        而对NVIC寄存器的描述则在Start文件中的内核文件core_cm3里,

        接下来根据数据手册对这些寄存器一一进行介绍。

        同样的,需要在STM3210xxxx Cortex-M3手册里进行查找,该手册都是英文的,所以有需要的用到有道词典。

        为了提高软件效率,CMSIS简化了NVIC寄存器的表示,NVIC的寄存器Set-enable、Clear-enable、Set-pending、Clear-pending和Active Bit映射到32位整数数组,即用数组来进行表示,同时下表是中断到中断变量的映射表

        可以看到,Set-enable、Clear-enable、Set-pending、Clear-pending和Active Bit数组每个组都有3个对应的寄存器,0,1,2分别对应0~31、32~63、64~67位。

        下图是NVIC不同寄存器组对应位的分布情况

        不同的产品对应位的分布也就有所差异,我使用的SMT32103C8T6,中断寄存器有60位,每一位对应着一个中断,即总共可编程屏蔽中断。

        如此只需要用到前两个寄存器,寄存器[0]以及寄存器[1],同时可以观察到第一列的地址偏移,每一个寄存器组的位置偏移也是不一样的。

2.1Interrupt set-enable registers (NVIC_ISERx)中断使能寄存器组

        该寄存器是用来对可屏蔽中断进行使能以及失能的,需要注意的是,如果一个挂起的中断被启用,NVIC会根据它的优先级激活这个中断。如果中断未启用,但中断信号将中断状态更改为挂起,但NVIC永远不会激活中断,无论其优先级如何。

        换句话说,如果启动该中断,这个中断就会根据优先级进行排队,直到前面中断任务执行完毕才进行中断。如果将一个未使能的中断去排队,则该中断不会响应,除非使能该中断。

2.2Interrupt clear-enable registers (NVIC_ICERx)中断除能寄存器组

        和上面的寄存器功能相反,失能中断函数。

        为什么不单单使用NVIC_ISERx寄存器来实现对中断函数的使能和使能呢?

答:

        目的明确性NVIC_ICERx的设计初衷就是用于禁用中断,即停止中断的触发和响应。而NVIC_ISERx则是用于使能中断的,其操作与中断的禁用(清除)相反。
        避免混淆:在编程和调试过程中,明确区分使能和禁用中断的寄存器有助于减少错误和混淆。使用NVIC_ICERx进行中断清除(禁用)可以使代码更加清晰易懂。
        寄存器功能划分:NVIC的设计将中断的使能和禁用功能明确划分到不同的寄存器中,这是为了提高系统的可维护性和可扩展性。

        同时注意到NVIC_ISERx寄存器位上标注的是rs也就是软件可以读也可以设置此位,写’0’对此位无影响。可以看到,该寄存器只有置1的功能,这样也能够解释NVIC_ICERx专门用来清除中断函数的目的。

2.3Interrupt set-pending registers (NVIC_ISPRx)中断挂起控制寄存器组

        中断挂起控制寄存器,顾名思义也就在有多个中断函数进行NVIC的优先级裁决后,优先级低的中断函数就被挂起,也就是“排队” 

        需要注意的是:

        如果某个中断已经被标记为挂起(pending)状态,即该中断的挂起位(在NVIC的挂起寄存器ISPx中)已经被设置,那么此时再向该中断对应的ISPRx寄存器中的位写入1(尝试再次将该中断标记为挂起),将不会产生任何效果。因为从逻辑上讲,一个中断如果已经处于挂起状态,就没有必要再次将其标记为挂起。NVIC的设计确保了挂起状态的唯一性和一致性,避免了不必要的重复设置。

        即使某个中断当前被禁用(即其使能位没有被设置),向该中断对应的ISPRx寄存器中的位写入1仍然会将该中断的挂起状态设置为挂起。这允许软件在中断被禁用时预先将中断标记为挂起,以便在未来某个时刻重新使能中断时能够立即处理这些挂起的中断。这种机制为中断管理提供了灵活性,使得中断可以在不立即处理的情况下被“保存”起来,等待后续的处理。

中断即使失能也能进入挂起状态,这样就能很好的解释上述的注意部分。

2.4Interrupt clear-pending registers (NVIC_ICPRx)中断解挂控制寄存器组     

        同样这是一个专门清除中断挂起的一个寄存器,和上面使能失能中断类似。

        将1写入ICPR位不会影响相应中断的活动状态。活动状态的中断,也就是正在被执行的中断,不会被NVIC_ICPRx)中断解挂控制寄存器所影响。

2.5Interrupt active bit registers (NVIC_IABRx)中断激活标志位寄存器组

        该寄存器置1就说明对应的中断函数正在被执行,也就是处于active活跃状态。只要是处于活跃状态,不管NVIC_ISPRx中断是否挂起,NVIC_IABRx中断激活标志位寄存器对应的位都是1。

2.6Interrupt priority registers (NVIC_IPRx)中断优先级控制的寄存器组

        该寄存器由240 个 8bit 的寄存器组成,每个可屏蔽中断占用 8bit,这样总共可以表示 240 个可屏蔽中断,每一位大小为8bit,实际上,只用到高四位,第四位不会用到:

        这四位又被分为两组:抢占优先级(pre-emption priority)、抢占优先级(也叫子优先级)(subpriority)

        对应的优先级就是NVIC_IPRx每一位的高4位进行配置的,因为抢占优先级和响应优先级是类似互补的形式对位进行抢占,例如,抢占优先级分配到4位(0~15),则响应优先级互补的分配到0位(0)。巧妙的这样只用了4个bit,就能够满足两种16位的优先级。

        同时,需要注意的是,进行以上分析,NVIC_IPRx的高4位进行一个中断优先级进行判断,所以也就是,一个中断函数同时拥有抢占优先级、抢占优先级只不过取值不同。

优先级的区分(抢占优先级和响应优先级)

        对于抢占优先级和响应优先级的优先级比较,参照了江科大的形象比喻:

        医院的叫号系统,一个紧急的病人A(响应优先级)去排队看病,由于情况略微紧急,将病人A排到队伍首位,也就是当正在看病的病人(正在执行的中断函数)看完病(执行完成),就让病人A(响应优先级)接上去看病(执行对应的中断函数);

        而一个更加紧急的病人B(抢占优先级),例如发生交通事故需要紧急看病,此时不等此时正在看病的病人C(正在执行的中断函数)看完病(执行完全),直接给病人B(抢占优先级)看病(执行对应的中断函数),待看完病人B(抢占优先级),在继续回去给病人C(先前在执行的中断函数)。

        抢占优先级属于中断嵌套,可以直接看病,而响应优先级是直接插队,还要等待一段时间,这样来看,抢占优先级。

        当抢占和响应相同时就需要通过中断向量表进行查看优先级的先后

         为实现上面对NVIC_IPRx的高4位的分组,宏定义组0,1,2,3,4分别为0x700(0x

 该寄存器是实现对中断进行分组,也就是下面函数:

void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup)

        跳转到定义

         以发现,实际上是对SCB内的AIRCR(应用程序中断/复位控制寄存器)寄存器进行写入配置。

        NVIC_PriorityGroup的参数如下

        这样就能够选择分组来实现,抢占和响应优先级分别分配到几位。

        NVIC的初始化参数配置在下文会具体介绍(抢占和响应优先级)配置。

2.7Software trigger interrupt register (NVIC_STIR)软件触发中断寄存器

        

        NVIC_STIR软件触发中断寄存器,该寄存器允许在程序中通过软件方式直接触发中断,而无需等待硬件事件(如引脚电平变化或定时器溢出)来触发中断。

        写入操作:向NVIC_STIR写入一个特定的中断编号(通常是一个0到N的数值,N为可支持的最大中断数减一(STM32C8T6种支持70个中断,10个内核的不可编程中断,以及60个可编程外设),即可触发对应编号的中断。中断编号的具体范围和可用性取决于芯片设计。
        读取操作:NVIC_STIR的读取操作通常不用于触发中断,而是可能用于调试或状态检查。然而,由于NVIC_STIR主要是用于触发中断的,其读取操作的具体用途可能较为有限。

        中断优先级:软件触发的中断同样受到中断优先级规则的影响。如果多个中断同时发生,CPU将根据中断优先级来决定先处理哪个中断。
        中断屏蔽:如果目标中断被屏蔽(例如,通过中断使能/除能寄存器禁用),则即使通过NVIC_STIR触发了该中断,它也不会被CPU响应

2.8 NVIC的初始化配置

2.8.1 NVIC_IRQChannel参数的选择

        上述为NVIC初始化结构体的参数一览,可以看到基本上都是uint8_t类型的,正好对应NVIC_IPRx中断优先级控制的寄存器的每一位为uint8_t。

        首先看到NVIC_IRQChannel,通过注释可以知道,STM32的IRQ Channels参数并没有放在mish.c种,而是在stm32f10x.h文件中。

        而该文件基本上是对外设寄存器的描述,同时存放了大量的宏定义,感兴趣的可以去看看,在该文件的下半部分。

        通过上图,我们能够知道Cortex-M3内核的配置基本上都在这个文件里,同时对不同容量的产品定义也有差异。

        上图看起来虚白一样的颜色,实际上上述写法运用了#ifdef,单片机会自动判断该STM32芯片属于什么系列,然后用#ifdef STM32F103xxxx如果为真就会使用该系列下面的内容,注意要用#endif ,实际上对一个头文件(.h文件)定义就是使用的这样的方法。

        可以看到MD相比LD多了一些外设,所以在使用某个外设的时候需要查看该系列是否包含该外设。

        关于不同产品系列的分类如上图。

        

        回到对NVIC_IRQChannel参数的配置,最上一栏是内核的异常(异常等价中断)函数也就是内核中断,而下一栏就是我们需要的可编程中断

        可以发现参数IRQn是一个枚举变量,同时对应着不同外设的IRQn函数,每个外设的IRQn函数都对应着一个具体的值,这个就是前面中断向量表里面说的优先级,尽管数值有些许不同,作用是等价的。

        对比中断向量表以及IRQn的枚举可以发现,Reset复位以及NMI不可屏蔽中断并不在IRQn的枚举中。

        以下是原因:

  • Reset:复位是系统启动或恢复初始状态的一种方式。它通常不是由软件触发的中断,而是由外部事件(如电源上电、复位按钮按下等)或内部事件(如看门狗定时器超时)触发的。因此,复位不需要在IRQn枚举中定义,因为它不是通过软件来管理和配置的。
  • NMI:不可屏蔽中断是一种特殊的中断,其优先级高于所有其他中断,包括最高优先级的中断。NMI不能被软件禁用或屏蔽,因此它也不需要通过IRQn枚举来管理。NMI通常用于处理严重的系统错误或异常情况,这些错误或情况需要立即得到处理,而不管当前的系统状态如何。

        NVIC_IRQChannel的参数也就取决于你需要产生中断的外设,选择对应的枚举变量写入结构体NVIC_IRQChannel参数中,就完成选择中断对象这一步。

2.8.2 NVIC_IRQChannelPreemptionPriority以及NVIC_IRQChannelSubPriority参数的选择

        可以看到,抢占优先级和响应优先级(子优先级)的参数来源是相同的,这也能很好的解释这两个参数是在NVIC_IPRx中断优先级控制的寄存器中一同配置的。

        参数源就在该结构体的下方

        忘了说,在此之前要完成对中断的分组,通过下面这个函数(之前提到过)。

void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup)

        根据两种优先级的规则来满足程序的要求,规则也有先后顺序:

       一个中断A触发后,此时中断B正在执行中断,中断A会在NVIC进行裁决,首先会先检查抢占优先级的大小(中断A中断B比较),抢占优先级高的可以中断嵌套抢占优先级低的。

        若抢占优先级相同,此时比较响应优先级,当中断A响应大于中断B,中断A不能直接嵌套,只能够排队,和中断A前面排队的中断进行比较,响应优先级大的排前面。

        若抢占优先级相同,响应优先级也相同,此时需要根据中断向量表的编号(或者是优先级 作用效果是一样的),编号小的优先级越高。

        需要注意的是:0是优先级最高的,不要混淆了!!!                                                   

2.8.3NVIC_IRQChannelCmd参数的选择

        可以看到这是一个Functional State类型的参数,看不懂直接跳转定义

        可以看到这是一个枚举类型的状态位,通过typedef类型宏定义来更改枚举的初始化操作。

        参数很简单,DISABLE失能或是ENABLE使能。

        同时我们还能注意到上面的 FlagStatus、ITStatus,这两个分别是标志位状态以及中断标志位状态,同样的RESET或是SET和DISABLE失能或是ENABLE使能很类似,只不过是场合不一样。

        DISABLE失能或是ENABLE使能更多的用在使能外设的时钟、通道等;而RESET或是SET更多的用在标志位的检查,通过检查标志位来进行下一步操作,举个就近的粒子,例如中断函数,

        通过检查标志位是否被置SET,来决定是否需要执行中断函数,如果在数据手册没有提到会自动将写入标志位寄存器的位进行清除,同样的,需要对该标志位进行清除,否则该中断函数就会一直执行,无法跳出。

关于状态标志位的一些介绍

        同样的,我们也可以通过清除函数来大致推测是否需要对该标志位进行清除,实际上大多数存储标志位的寄存器都带有清除函数,甚至一些会进行自动清除标志位的寄存器都会带有清除函数。

这样做的好处

  1. 灵活性:软件清除标志位提供了更大的灵活性。开发者可以在适当的时候清除标志位,以确保程序的逻辑正确性和稳定性。
  2. 控制性:在某些情况下,开发者可能希望在中断服务例程之外清除标志位,或者基于特定的条件来清除标志位。这时,软件清除函数就显得尤为重要。
  3. 兼容性:STM32系列微控制器的外设和功能非常丰富,不同的型号和系列之间可能存在差异。软件清除函数提供了一种统一的方式来处理这些差异,提高了代码的兼容性和可移植性。

        同样的ErrorStatus,也是在特定的场合下使用的枚举类型,例如用于检验操作是否正确,通常被作为返回值的形式,以采取对应的措施。

        Functional State    FlagStatus、ITStatus     ErrorStatus,这三个枚举值运用在不同的场合,明确了某个状态,由于是在不同的场合下,所以极大的提高了程序的可读性,同时提高了效率,避免了不必要的程序开销,同时提高系统的稳定性。

        对于NVIC_IRQChannelCmd参数,只需要写上ENBALE(使能)就能够开启NVC_IRQChannel的中断通道,就是这么简单。

2.8.4 NVIC_Init(NVIC_InitTypeDef* NVIC_InitStruct)

        此函数是在前面配置NVIC_InitTypeDef结构体的基础上实现的,下面的描述实际上就是对相应的寄存器不同的的配置(例如:NVIC_ISERx中断使能寄存器组进行配置),学有余力的可以去理解,同时可以参考博客中断-NVIC与EXTI外设详解(超全面)-CSDN博客里面有详细的说明。

2.9 小结

        以上就是对NVIC的配置了,只需对NVIC_InitTypeDef结构体的参数进行配置,然后将该结构体导入NVIC_Init的配置函数中,还需强调,NVIC的位置属于内核,同时随着STM32芯片启动而打开时钟,故NVIC属于内核外设,虽然NVIC不直接依赖于某个时钟源,但STM32微控制器的其他部分(如CPU、外设等)的时钟配置对NVIC的性能和中断处理有间接影响。
        例如,系统时钟(SYSCLK)的频率会影响CPU执行指令的速度,从而间接影响NVIC处理中断的速度。

        而NVIC对于CPU存在的意义在于提供了高效、灵活且可靠的中断管理机制。它使得CPU能够高效地处理多个中断源,并根据中断的重要性和紧急性来合理分配处理器资源。这种机制对于实时系统和多任务操作系统来说尤为重要,因为它确保了系统能够稳定、可靠地运行。

3.EXTI(External Interrupt/Event Controller)外部中断/事件控制器

        EXTI(External Interrupt/Event Controller)外部中断/事件控制器,是STM32微控制器中用于处理外部中断和事件的硬件模块。它提供了多个中断/事件线,每个线都可以独立配置为中断或事件源,并可以配置为上升沿、下降沿或双边沿触发,同时每个线也可以独立的被屏蔽,也能被挂起保存对中断的访问。  

3.1AFIO(Alternate Function Input Output,复用功能输入输出)的介绍     

        最左边是对应的GPIOx端口,共计有16个GPIO端口也就对应GPIO_PIN_0~15。先进入AFIO(Alternate Function Input Output,复用功能输入输出),这个外设属于片上外设,例如通过查看他的基地址

#define AFIO_BASE             (APB2PERIPH_BASE + 0x0000)

        可以看到AFIO的基地址在APB2基地址的首位,也就是说AFIO是属于APB2上的片上外设,这样你需要使用AFIO的时候就需要先开启AFIO时钟,使用RCC_APB2PeriphClockCmd,参数填上对应的AFIO参数。

        AFIO有两个功能,分别是最主要的复用功能输入输出,以及对中断引脚的数据选择,这些功能本质上都属对其内部寄存器进行配置,接下来就对AFIO内寄存器进行展开。

3.1.1 事件控制寄存器(AFIO_EVCR)(Event Control Register)

        32位的寄存器,实际上也只有低八位才起作用,这是因为STM32单片机的设计往往需要在性能、功耗、成本和兼容性之间进行权衡。使用32位寄存器而不是更窄的位宽可以带来性能上的优势,同时保持与现有软件和硬件的兼容性。同时保留的地方能够为后续的功能拓展做准备,也就说明了这样做兼容性更强。

        最后续遇到寄存器中含有保存位,也要往兼容性这方面去考虑。

        回到AFIO_EVCR寄存器,第七位是EVOE允许事件输出,也就是当该位置1时,也就说明能够从GPIO端口中输入一个事件中断源,同时PROT[2:0]和PIN[3:0]被激活,分别用来选择对应的GPIOx,以及具体的GPIO_PIn_x。

        同时可以看到事件源输入GPIO的函数GPIO_EventOutputConfig,可以看到实际上是对AFIO中的EVCR寄存器进行写入。参数与GPIO端口的略微不同:

        可以发现GPIO端口的例如:GPIOA是由基地址偏移而来,同时属于一个GPIO_TypeDef的一个指针,而GPIO_PortSourceGPIOA直接就是一个uint8_t类型的确切的数值0x00

        在分别对比以下Pin之间的区别:

        明显的区别就是一个是uint8_t,而另一个是uint16_t,仔细观察发现,GPIO_PinSourcex是用0到F表示的,只需要用到低八位,而GPIO_Pin_x分别是低八位和高八位分别设置位1~8来表示Pin_0~15,换句话说就是,配置GPIO_Pin_x的16位寄存器,每一位对应着一个Pin端口。

        换成二进制理解就是:

        GPIO_PinSourcex:GPIO_PinSource0    (u8)0x00                  0000 0000

                                       ...

                                      GPIO_PinSource15  (u8)0x0F                  0000 11111

        GPIO_Pin_x:           GPIO_Pin_0    (u16)0x0001     0000 0000 0000 0001  

                                       ...

                                      GPIO_Pin_15   (u16)0x0001    1000 0000 0000 0000  

        这样区别就很明显了。

        总而言之,事件控制寄存器(AFIO_EVCR)就是在需要事件触发的场合需要得到相应的配置,例如:在某些低功耗模式下,微控制器可能处于休眠状态以节省电量。通过将GPIO引脚配置为事件输出,并连接到中断唤醒引脚,可以在外部事件发生时唤醒微控制器。这对于需要快速响应外部事件的应用非常关键。

3.1.2 复用重映射和调试I/O配置寄存器(AFIO_MAPR)(Alternate Function Input/Output)

        该寄存器对于不同类型的产品,寄存器内部也是略有不同的

        归根结底,是因为不同类型的产品适应不同的场合,而同时外设种类上也会随之改变。

       复用重映射和调试I/O配置寄存器(AFIO_MAPR)的作用就是通过引脚重映射来实现一个GPIO端口不同功能的实现。

AFIO重映射的介绍

        下面的函数就是通过对MAPR寄存器的配置来实现重映射的功能。                

        对第一个GPIO_Remp参数查看可取的值:

        该参数填写需要重定义后的功能,由于外设资源在不同产品的分配情况是不同的,所以一些小容量能够重定义功能较少。

        该图中的默认复用功能,虽然上面由复用两个字,实际上却不需要用到AFIO外设,相当于一个端口有这么多功能(默认复用功能),需要用到哪个功能只需配置对应的GPIO端口即可。

        例如:使用PA8和PA9中的USART1_TX和USART1_RX,直接配置PA8和PA9,然后接上对应的片外外设就可以实现串口发送和接收功能。

         本质上,需不需要用到AFIO外设就是看有没有用到AFIO内部的寄存器,对中断的输入进行选择需要用到外部中断配置寄存器 x(AFIO_EXTICRx),所以就在对中断的配置时,需要开启AFIO的时钟,同时需要用到引脚重定义功能时,会用到复用重映射和调试I/O配置寄存器(AFIO_MAPR)。

         在实际应用中,能不能使用引脚重定义功应参考相应型号的STM32参考手册进行具体配置。

        需要用到引脚重映射功能的场合:本应用默认复用功能引脚,当该引脚被其他外设占用,需要使用该功能。

        例如,想要使用PA8和PA9中的USART1_TX和USART1_RX,但是PA8和PA9被其他外设占用,此时通过查找手册,就可以通过开启AFIO重映射功能激活PB6和PB7来实现USART1_TX和USART1_RX串口发送接受的功能。

3.1.3 外部中断配置寄存器 1(AFIO_EXTICR1)(External Interrupt Configuration Register)

        该寄存器有四个,每一个寄存器只使用低16位每四位用来用来确定一个EXTIx引脚,而一个寄存器能够确定4个EXTIx引脚,寄存器按序号1~4由低到高依次确定(EXTI0、EXTI1、EXTI2、EXTI3)、(EXTI4、EXTI5、EXTI6、EXTI7)、(EXTI8、EXTI9、EXTI10、EXTI11)、(EXTI12、EXT13、EXTI14、EXTI15)。

        这样理解起来略微抽象,举个例子:配置外部中断配置寄存器1(AFIO_EXTICR1)的低三位EXTI0[3:0]位0000,也就是PA0端口接通到EXTI0进入中断。此时EXTI0就被占领,其他端口的0号就无法进入中断(PB0,PC0)。

        若在配置EXTI1[3:0]依旧位0000,也就是PA1通道进入EXTI1中断,其他PB1、PC1等也就无法进入中断了,下图可以形象的解释:

        每一号端口只能能有一个进入中断(PA0,PB0,PC0不能同时进入EXTI0)。

3.1.4 AFIO的小结

        对于AFIO时钟是否开启,也就是是否需要用到AFIO这个外设,取决于是否要用到AFIO内部的寄存器,无非两个地方:需要中断时,对中断引脚进行选择;以及需要用到引脚功能重映射的时候。

3.2EXTI寄存器

        下图为EXTI内部的所以寄存器

3.2.1 中断屏蔽寄存器(EXTI_IMR)(Mask Register)

        这个寄存器就是区分内核中断和外设中断,内核中断通常处理一些故障和异常,所以通常内核中断,不能被屏蔽。

        下图是中断线号EXTIx

        可以看到总共有EXTI0~19总共20个中断线,正好对应IMR屏蔽寄存器中的20位,一一对应。     

        实际上,中断的请求就是对中断标志位的判断,如果标志位为SET,也即执行对应的中断函数,而在判断之前,就将EXTI中断线与上IMR寄存器,来判断该EXTI通道是否被屏蔽。

        值得注意的是,在对EXTI进行初始化的时候会清除IMR寄存器,每次配置中断时都要配置一下IMR寄存器对应位为1,防止中断被屏蔽。

3.2.2 事件屏蔽寄存器(EXTI_EMR)(Event Mask Register)

        和上面的中断屏蔽中断寄存器IMR类似,只不过触发源改成了事件中断,这里不再赘述。

3.2.3 上升沿触发选择寄存器(EXTI_RTSR)(Rising Trigger Selection Register)

        该寄存器同样是低20位有效,写入高电平时,输入上升沿时会触发中断。

        需要注意的是,边沿触发的线路上不能出现毛刺信号(噪音扰动0),毛刺信号对边沿检测影响较大,所以需要避免毛刺信号。

3.2.4 下降沿触发选择寄存器(EXTI_FTSR)(Falling Trigger Selection Register)

        和上述的上升沿触发中断一样,配置低20位对应EXTI0~19,值得一提的是,无论上升还是下降沿选择触发寄存器实际上配置的是EXTI_InitTypeDef结构体变量的一个参数,接下来会对EXTI结构体配置进行展开。

3.2.5 软件中断事件寄存器(EXTI_SWIER) (Software Interrupt/Event Register)

        同样也是低20位有效,正好对应20给个EXTI通道。

        通过对SWIER寄存器的描述可知,该SWIER软件中断寄存器是和EXTI_PR寄存器配合使用的,所以先介绍挂起寄存器EXTI_PR寄存器这里。

3.2.6挂起寄存器(EXTI_PR)(Pending Register)

       我们同样能够发现该挂起寄存器也是低20位有效,正好配置EXTI的20个通道,在这里可以稍稍小结一下,几乎EXTI内部所有的寄存器都是低20位有效,一一对应EXTI的20个通道,此时回过头去看EXTI与其他外设之间的联系就一目了然了。

        先是选择GPIO对应的端口,然后接通至对应的EXIT,其中存在对应关系(GPIO_Pin_0对应EXTI0),EXTI边沿检测在中间,所以需要对EXTI的每个通道也就是20个进行配置,每一个寄存器都有20位来进行配置,最后进入NVIC进行裁决,经NVIC裁决后产生中断。

        回到PR挂起寄存器,Pending挂起的意思类比“排队”,也就是说需要排队等待,到”排队“完成时清除“排队”(挂起)的标志位。

关于标志位检测函数的一些介绍

        下面图中的函数就是对EXTI挂起标志位的检测,如果被挂起则PR寄存器中对应的位是1,则写入SET返回值。

        同样也存在清除函数

         类似的有下面两个函数

ITStatus EXTI_GetITStatus(uint32_t EXTI_Line);
void EXTI_ClearITPendingBit(uint32_t EXTI_Line);

        本质上都是对标志位的读取以及清除,只是所应用的场合有所不同:

        FlagStatus EXTI_GetFlagStatus(uint32_t EXTI_Line);
        这个函数用于获取指定外部线路(EXTI_Line)的中断或事件标志位的状态。         

        ITStatus EXTI_GetITStatus(uint32_t EXTI_Line);
        这个函数用于获取指定外部线路的中断状态。

        同样是获取标志位,只不过下面这个ITStatus EXTI_GetITStatus函数是在中断程序中进行判断的,同样在中断结束,需要对标志位进行清除,否则就一直处于中断状态。

        这里可能会对清除函数略感疑惑,为什么是将EXTI_PR寄存器存取EXTI_Line中的值就清除了bitstatus呢?

        因为如上图在挂起寄存器PR有这么一句话,该位第一次置1时表示发生了中后段触发请求也就是进入“排队",第二次对该位置1则是清除该位,也就是在排队完成的时执行。

        这就说的通了,再回到软件中断事件寄存器(EXTI_SWIER) , 同样也有一句话。

        类似的也是通过对PR挂起寄存器写1,来清除SWIER。

        掉用下面的函数,写入指定中断线,就能够进行软件触发一个中断,而不需要外部GPIO信号来触发中断。

        了解完EXTI寄存器,接下来了解EXTI的内部结构,EXTI的内部结构就是由一个个寄存器所构成的。

3.3EXIT内部结构

        如上图,EXTI的结构,每一根线上标/20说明有20根线,对应的20个EXTI通道,输入线也就是经过AFIO中断线选择后得到的,有16个通道再加上PVD、RTC、USB、ETH共20根线。

        先是对上升下降沿寄存器边沿检测,然后或上软件中断事件寄存器SWIER输出,可以看到触发中断的方式可以是软件触发,也可以是边沿触发。输出的结果有两个去向,一个是与事件屏蔽寄存器与上输出至脉冲发生寄存器。

        根据数字电路的基础知识,月牙形状的逻辑门是属于或门,只要有一个输入为1,输出就为1;而两外一个拱门形状的逻辑门属于与门,只要有一个输入为0,输出就为0,相当于开关,只要开关的那个输入为0,无论另一个输入为什么值,都输出0,而事件屏蔽寄存器就是类比开关来使用的。

        然后脉冲发生寄存器可以输出至其他外设,进行后续操作,如下图产生的脉冲信号进行ADC信号采集,这一路也就是事件触发。

        显而易见,另一路是中断触发,进入PR挂起寄存器对应位置1,同样与上一个“开关”(中断屏蔽寄存器),结果输出值NVIC,经过NVIC的裁决最终才会由CPU执行。

中断和事件的差别

         同时上面也提到事件和中断的概念,这两种情况在EXTI结构里分别输出互不影响,接下来对其进行简单的描述:

        中断:需要CPU参与,需要调用软件的中断服务函数才能完成中断后产生的结果

        事件:靠脉冲发生器产生一个脉冲,进而由硬件自动完成这个事件产生的结果,当然相应的联动部件需要先设置好,触发TIM计时,AD转换等,事件不要软件的参与,降低了CPU的负荷,而且硬件速度快于软件速度。

        以上就是对EXTI内部结构的大致介绍以及简单的流程介绍,接下来对中断函数配置的具体实现进行描述。

4.中断配置

        想要配置中断,就需要打通一条如下图的通道。

        接下来就根据图中的导通顺序来配置中断函数,本次配置选择打通GPIOB中的GPIO_Pin_7。

4.1时钟开启

        要想要对应的外设工作则需要先打开该外设的时钟,在这里需要注意的是,AFIO外设也参与到了中断的配置中,所以开启时钟需要开启GPIO以及AFIO的时钟。 

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOx, ENABLE);		
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);

        当然了,也可以写成        

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB|RCC_APB2Periph_AFIO, ENABLE);		

4.2 GPIO的配置

	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);						

        其中需要注意的是GPIO_Mode的配置,可以查阅手册STM32F103xxxx

        浮空、上拉或下拉输入,这里要求不高,上拉输入即可。

4.3AFIO中断引脚选择

GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource7);

        这个函数在之前介绍AFIO提到过,通外部中断配置寄存器EXTICR来进行映射,也即GPIOB到GPIO_PortSourceGPIOB以及GPIO_Pin_7到GPIO_PinSource7的映射。

        接下来是对EXTI的配置,同样是由一个结构体引出多个参数的方式来配置的。

4.4EXTI边沿检测及控制

    EXTI_InitTypeDef EXTI_InitStructure;						//定义结构体变量
	EXTI_InitStructure.EXTI_Line = EXTI_Line7;					//选择配置外部中断的7号线
	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外设

        首先是对结构变量名称的定义,查看结构体的参数:

        然后是EXTI_Line,查看参数:

        里面放着EXTI0~19共20个中断线,GPIO_PinSource7就对应EXTI_Line7。

        接着是EXTI的模式:

        里面由事件触发和中断触发两种,我们这里选择中断触发,对中断和事件还模糊的可以去参考资料【转】1-单片机STM32---中断与事件的区别 - Engraver - 博客园 (cnblogs.com)

        然后触发检测模式:

        分为上升沿触发,下降沿触发以及双边沿触发,这里对一些精度要求较高的地方比较有考究,这里就选择下降沿触发。

        最后是使能对应的EXTI_Line7通道:

FunctionalState EXTI_LineCmd(ENABLE);

        填上ENBALE使能即可。

        然后将配置好的结构体传到EXTI_Init函数中:

        这样EXTI初始化函数会自动将结构体中的参数配置到对应寄存器中。

        同时我们能够注意到每次使能EXIT_LineCmd后,都会对两个屏蔽寄存器清除、以及配置边沿检测的寄存器。

4.5NVIC的配置

        如果对NVIC有些遗忘,可以回到之前对NVIC的描述进行查看。

NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);	

        首先需要对NVIC进行分组,NVIC属于内核外设,不需要由RCC开启时钟,而是由芯片的开启而开启,而对NVIC的分组是在SCB外设中的AIRCR(应用程序中断/复位控制寄存器)进行配置的,如下图。

        接下来是对NVIC结构体参数进行配置:

	NVIC_InitTypeDef NVIC_InitStructure;						//定义结构体变量
	NVIC_InitStructure.NVIC_IRQChannel = EXTI9_5_IRQn;		    //选择配置NVIC的EXTI9_5线
	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外设

        前面已经对结构体的参数介绍过了,这里就不再赘述。

        需要注意的是:

        由EXTI到NVIC并不是对应每一个EXTI都有一条通道与之对应的,而是将EXTI5到EXTI9合并位单独的一根线EXTI9_5,EXTI10到EXTI15同样的合并为EXTI15_10。

        这种合并方式使得MCU能够以较少的硬件资源提供更多的外部中断源,同时降低了设计的复杂性和成本,通过合并外部中断线,可以在不牺牲其他引脚功能的前提下,提供更多的外部中断源,减少功耗

        以上就是对中断的配置,接下来需要对中断函数进行配置。

4.6中断函数的配置

        ST公司推荐将函数放到上图的位置,stm32f10x.it.c文件中,实际上最好是将中断函数放到需要用到中断函数的地方,例如主函数的下面。

void EXTI9_5_IRQHandler(void)
{
	if (EXTI_GetITStatus(EXTI_Line7) == SET)		//判断是否是外部中断14号线触发的中断
	{
		/*如果出现数据乱跳的现象,可再次判断引脚电平,以避免抖动*/
		if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_7) == 0)
		{
    	    if (GPIO_ReadOutputDataBit(GPIOA, GPIO_Pin_1) == 0)		//获取输出寄存器的状态,如果            当前引脚输出低电平
	        {
		        GPIO_SetBits(GPIOA, GPIO_Pin_1);					//则设置PA1引脚为高电平
	        }
	        else													//否则,即当前引脚输出高电平
	        {
		        GPIO_ResetBits(GPIOA, GPIO_Pin_1);					//则设置PA1引脚为低电平
	        }
		}
		EXTI_ClearITPendingBit(EXTI_Line7);		//清除外部中断14号线的中断标志位
													//中断标志位必须清除
													//否则中断将连续不断地触发,导致主程序卡死
	}
}

        上述的中断函数是对PA1的LED进行电平跳转的程序,PB7则接上一个按键。

        上面的对PB7进行消抖处理,是为了防止毛刺影响到边沿检测:

       也就是按键按下之后会进行震动,同时松手时也会进行抖动,需要进行延时处理,但对于要求不高的情况下也可忽视。

        同样,需要对LED进行配置对应的GPIO端口,这里就不再进行赘述。

        最终的中断执行流程就是,按下PB7的按键,产生中断,PA1的LED灯由灭变亮,再次按下PB7,产生中断,PA1的LED灯就由亮便灭,注意在每次结束中断时都会清除中断标志位

5.总结

        实际上这篇文章也是耗费时间挺多的,写道一半的时候心烦意乱,想着怎么样快点结束,看起来略显煎熬,实际上,也正是写道一半的时候,各种知识盲区例如PR挂起寄存器两次置1是为什么,略显头疼,好在凭借鄙人的毅力坚持了下来,才完成此片文章,同时本来想将AFIO单独出一片文章的,可有感觉不妥,就让它在那了。同时欢迎讨论,以及指正错误,OVO。


 文章参考:中断-NVIC与EXTI外设详解(超全面)-CSDN博客

[5-1] EXTI外部中断_哔哩哔哩_bilibili

  • 33
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
STM32是一款非常强大的微控制器,其包含了大量的IO口和外设,能够广泛应用于各种嵌入式系统中。在实际应用中,我们通常会使用中断来响应外部事件,如按键、定时器等。本文将介绍STM32中断的顺序,包括初始化IO口、开启AFIO时钟、EXTI配置、NVIC配置以及编写中断服务函数等。 1. 初始化IO口 在使用STM32的IO口前,我们首先需要对其进行初始化。具体而言,需要配置GPIO端口模式、输出类型、速度、上下拉等参数。以PA0引脚为例,其初始化代码如下: ```c GPIO_InitTypeDef GPIO_InitStructure; //开启GPIOA时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //配置PA0为浮空输入模式 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure); ``` 2. 开启AFIO时钟 AFIO(Alternate Function I/O)是STM32中一种特殊的I/O功能,可以将某些I/O口用于其它功能,如复用为定时器、串口等外设。在使用AFIO前,需要先开启其时钟。代码如下: ```c RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); ``` 3. EXTI配置 EXTI(External Interrupt)是STM32中一种外部中断功能,可以用于响应外部事件,如按键、定时器等。在使用EXTI前,需要配置其相关参数,如GPIO端口、触发方式等。以PA0引脚为例,其EXTI配置代码如下: ```c EXTI_InitTypeDef EXTI_InitStructure; //选择GPIOA作为EXTI输入源 GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0); //配置EXTI线路为中断模式 EXTI_InitStructure.EXTI_Line = EXTI_Line0; EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising_Falling; EXTI_InitStructure.EXTI_LineCmd = ENABLE; EXTI_Init(&EXTI_InitStructure); ``` 4. NVIC配置 NVIC(Nested Vectored Interrupt Controller)是STM32中一种中断控制器,可以对所有中断进行优先级管理和嵌套管理。在使用中断前,需要配置其中断优先级和使能NVIC。以PA0引脚为例,其NVIC配置代码如下: ```c NVIC_InitTypeDef NVIC_InitStructure; //配置EXTI0中断优先级为0 NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); ``` 5. 编写中断服务函数 最后,我们需要编写中断服务函数来处理中断事件。以PA0引脚为例,其中断服务函数代码如下: ```c void EXTI0_IRQHandler(void) { if(EXTI_GetITStatus(EXTI_Line0) != RESET) { //中断处理代码 EXTI_ClearITPendingBit(EXTI_Line0); } } ``` 中断服务函数需要在NVIC中配置的中断号对应,如EXTI0_IRQn对应PA0引脚的中断号。在函数中,我们可以编写中断处理代码来响应中断事件,如读取按键状态、改变LED灯状态等。最后,需要调用EXTI_ClearITPendingBit函数清除中断标志位,以便下一次中断发生时能够再次触发中断服务函数。 综上所述,STM32中断的顺序包括初始化IO口、开启AFIO时钟、EXTI配置、NVIC配置以及编写中断服务函数等。通过以上步骤,我们可以有效地使用STM32的中断功能,实现各种外部事件响应。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值