STM32学习之外部中断

STM32学习之外部中断 1. 中断常见的书上关于中断举的例子差不多就是这样“你正在看书厨房的水开了你起来去关火关了火以后继续看书”这种例子说他好吧差不多把中断的概念描述清楚了但是根本没办法和单片机的联系起来因为这种例子所描述的中断的概念光看“中断”这两个字也能猜个八九不离十说了等于没说实际上从正面去讲中断的概念也并不复杂我甚至觉得不举这种例子反而更好理解计算机程序正常情况都是从上到下依次执行的当发生一些内部或者外部事件的时候CPU本来应该继续向下执行的但是却没有继续向下执行反而从当前执行的代码处离开去执行别处的代码这个过程就叫中断这些内部或者外部的事件就叫做中断源。这里指讨论外部中断等后面学到了其他的中断在讨论其他的。1.1 NVIC与EXTI众所周知单片机的MCU是由内核和片上外设组成的内核就是一般说的某某内核的芯片比如战舰开发板采用 ARM Cortex-M3 内核内核就包括CPU核心以及一些内部外设内部外设的内是相对内核说的而片上外设就是内核外面的那些外设下图我框起来的部分Cortex-M3’s internal peripherals这部分映射的地址就是控制内部外设的寄存器地址而右边的那些(我的截图不完整只截了一部分)非常多的就是片上外设的寄存器地址了。NVIC是内部外设的一种EXTI则是片上外设他们两个搭配起来就构成了外部中断的完整配置了。1.1.1 外部中断与Cortex-M3内核所谓外部中断与M3内核的关系其实就是介绍M3内核的NVIC在启动文件中可以看到一个叫做g_pfnVectors的变量(我用的不是keil编译器有可能和keil的启动文件会有些许差异估计应该差不了很多)这个变量是指向中断向量表的指针中断向量表里存的就是中断服务函数的地址当我们进行了一些恰当的配置以后如果外部触发了某个中断CPU就会去这个表里找对应函数的地址然后去该地址执行相应的中断服务函数比如在正点原子的外部中断例程中有一个头文件是exti.h,里面有一个宏定义是#define KEY0_INT_IRQHandler EXTI4_IRQHandler这个宏的右边就是下图启动文件的第159行左边就是对应的中断服务函数因为C语言的函数名就是函数的地址所以这样写没啥问题这也能解释为什么最底层的中断函数无人调用了当中断触发的时候由CPU去自己找对应的函数。中断向量表的这些函数顺序是芯片公司出厂就定义好的我们只能服从不能随意修改。NVIC的寄存器介绍不在《STM32F10xxx参考手册》中需要去《Cortex-M3权威指南》中查找相关寄存器的介绍关于这部分寄存器在代码中的定义位于头文件core_cm3.h中代码中的名称和权威指南中的名称不太一样我在下面的表做个记录寄存器功能权威指南中的名称代码中的名称中断使能ISERSETENA中断失能ICERCLRENA中断挂起ISPRSETPEND中断解挂ICPRCLRPEND中断激活标志寄存器IABRACTIVE中断优先级寄存器IPPRI还有一个软件触发中断寄存器这个我目前还不知道在那些场合会用到估计是操作系统吧反正也和外部中断没关系后面遇到了再说。中断的使能与失能寄存器SETENAs和CLRENAs表示中断的使能和去使失能寄存器s可以取0到7,总共8对寄存器每个寄存器又有32位按照每一位来配置一个中断来看最多可以配置32*8256个中断而M3内核的SETENA7和CLRENA7只使用了16位所以M3最多可以支持240个中断而在STM32F103ZET6中被进一步裁减只使用了60位也就是这两组寄存器中s0,1的情况其他的外设寄存器使能和失能一般是写1使能写0失能NVIC比较特殊使能某个中断就给SETENAs对应的位写1失能就是给CLRENAs对应位写1,写0没啥用如果使能和失能寄存器的对应位都写1的话那就是哪个后执行哪个生效先失能后使能那中断有效先使能后失能那中断无效不过应该没人会这么干吧中断的挂起与解挂起寄存器中断在执行的时候来了高优先级的中断会打断当前中断而来了同优先级或者低优先级的中断那么中断就不能立即得到响应此时中断被挂起。中断的挂起状态可以通过“中断挂起寄存器(SETPEND)”和“中断解挂起寄存器(CLRPEND)”来读取还可以通过软件写入对应位来手动挂起中断不过使用情况不多一般都是硬件置位与清零。如果需要软件写入的话具体的使用方法和前面的使能和失能寄存器相同。中断激活标志寄存器当CPU进入了某个中断服务函数以后执行了第一条汇编指令之后中断激活标志寄存器的对应位就会被硬件置1这是一个只读寄存器通过读这个寄存器就能知道当前正在执行哪个中断。这类寄存器和之前一样也是总共8个除了最后一个寄存器使用了16位之外其余的每一位对应一个中断源。中断优先级配置寄存器通过配置这个寄存器就可以配置两个或以上的中断同时触发的时候先执行哪个再执行哪个这类寄存器总共有240个和之前一样STM32只用到了60个每一个寄存器对应一个中断源但是这类寄存器只有八位所以它们相邻的寄存器之间的地址偏移量是1上面提到的其他的寄存器偏移量是4。每个寄存器可以把寄存器的8位分成两组数来分别表示抢占优先级和响应优先级抢占优先级高的中断可以先执行并且抢占优先级高的中断可以打断抢占优先级低的中断抢占优先级相同的情况下响应优先级高的先执行抢占优先级相同但是响应优先级高的中断不能打断响应优先级低的中断如果这倆优先级都一样那就按自然优先级来决定执行顺序自然优先级就是在中断向量表里的先后顺序。抢占优先级和响应优先级的英文翻译是preempt priority和subpriority根据pre和sub这两个英文前缀也能看出来这两个优先级是什么意思不过我也不知道位啥要翻译成响应优先级这么怪的名字也许学到后面就懂了吧。1.1.2 外部中断与STM32F103ZET6NVIC是内核控制中断的外设所有具有cortex-M3内核的芯片都大差不差而如何把中断信息传递到NVIC就是片上外设的事情了在stm32f103xe.h这个头文件中定义了一个如下图所示的枚举82行的注释下面的部分总共有60个就是之前说的可以把中断信号传递到NVIC的60个外设其中第89到93行的EXTI就是根据外部的电平变化配置的中断也是这次学习的主要内容。EXTI是外部中断和事件控制器根据上面的图可以看出来这个芯片总共有五根EXTI线每根线之间相互独立可以分别进行不同的配置比如输入类型和触发事件触发事件指的是外部信号的上升沿触发下降沿触发或者双沿触发而输入类型这个名词是从正点原子给的开发指南上看到的我觉得更应该叫做EXTI的输出类型EXTI接收的信号是外部的电平变化信号它对这些信号处理了以后再进行产生脉冲或者传递信号至NVIC所以相比EXTI来说它更应该被叫做输出类型。来看参考手册中的这个外部中断/事件控制器框图![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/e95f730c32ac46758a245bac2d4e8795.png图片中的‘/20‘’20/‘之类的意思是EXTI是由 20 个产生事件/中断请求的边沿检测器组成总共20根并行的独立的线右下角的输入线三电平变化信号输入的地方它可以是任何一个GPIO口或者是一些其他外设事件进来之后经过一个边沿检测电路这个边沿检测电路上面的那倆寄存器就是用来配置是上升沿下降沿还是双沿触发的通过配置对应的寄存器就可以配置相应的中断源了然后经过一个与软件中断事件寄存器组成的或门电路这个寄存器一般也用不到配置一直输出0然后就到了EXTI的输出岔路了一条是与事件屏蔽寄存器组成的与门电路另一条传输至请求挂起寄存器先看和事件屏蔽寄存器组成的这个与门电路这个与门电路的输出是为了给其他外设电路产生脉冲信号比如定时器等通过配置事件屏蔽寄存器就可以屏蔽相应的外设事件从而不产生对应的脉冲信号起到屏蔽事件的作用另外一路传到请求挂起寄存器当信号传到这个寄存器的时候对应位就会被硬件置位然后告诉CPU现在来了个中断能不能处理如果此时有更高优先级的中断正在执行现在不能处理这个中断的话CPU就会把NVIC的挂起寄存器也置位然后这个中断就被挂起了如果可以执行的话CPU就把这个中断执行了但是这个请求挂起寄存器的对应位清零的工作就得通过代码来实现了一般都会放到中断服务函数里面来清零很多初学者一开始看到中断的代码里清标志位也许都会一头雾水至少我有一头雾水这里就解释了这个标志位的来源了然后信号经过中断屏蔽寄存器组成的与门之后进入NVIC这里和刚才说的事件屏蔽寄存器哪个与门意思差不多所以这里就是配置中断屏蔽寄存器就是字面意思了就是为了屏蔽某一路中断从而让这个与门输出0切断它与NVIC的联系。1.2 EXTI与GPIO配置刚才提到EXTI的输入可以是任何一个GPIO口或者一些其他的外设事件总结如下EXTI线对应连接0-15GPIO输入16PVD输出17RTC闹钟18USB唤醒19以太网唤醒除了前面的0-15是现在需要讨论的后面的都暂时不看因为我也不会而且战舰开发板不是互联型产品所以第19的以太网唤醒是没有的。stm32f103系列总共有GPIOxx A - G7组每组16个总共7*16112个GPIO上面对应的中断线只有16根显然不够但是注意到GPIO每组的个数和中断线的条数是一样的所以芯片公司的设计也是这样的第0根中断线对应的IO口可以选择GPIOA.0、 GPIOB.0、GPIOC.0、GPIOD.0、GPIOE.0、GPIOF.0 和 GPIOG.0中的一个第1根可以选择GPIOA.1、 GPIOB.1、GPIOC.1、GPIOD.1、GPIOE.1、GPIOF.1 和 GPIOG.1中的一个剩下的就同理了所以说GPIO能同时启用中断的IO口最多只有16个其实也很显然因为只有16根线嘛配置这个对应关系的寄存器是这些AFIO_EXTICR1 ~ AFIO_EXTICR4有没有感觉乱七八糟的打开芯片手册找到描述这些寄存器的地方就看AFIO_EXTICR2吧它指的是第4到7根中断线可选择的IO口每四位组成一个四位整数以此来选择GPIO的A-G其余的中断配置方法就在这个同名的其他寄存器这个比较简单就到此为止了。1.2 外部中断的配置整体来看外部中断配置的过程分为三个部分第一部分是把GPIO映射到对应的中断上使得中断信号能传到EXTI第二部分是配置EXTI使得信号能传递到NVIC最后一部分就是NVIC的配置了接下来就来点个灯吧。1.2.1 IO口的配置首先要通过看原理图来知道我们是从哪里获取中断源的下面这张图的四个开关就是获取中断源的当开关按下的时候MCU根据相应的电平变化来进入相应的中断服务。比如KEY_UP这个开关按下以后MCU的PA0就会变成3.3V的高电平电平变化是由低到高所以我们要把它对应的中断配制成上升沿触发其余的三个开关按下的时候对应的IO口就会变成低电平电平变化是由高到低所以要配制成下降沿触发。它是要接收外部信号的所以肯定配制成输入模式又因为要采集电平变化所以必然还得有一个上拉或者下拉来配制初始状态以KEY_UP为例看一下。voidgpio_init(void){WKUP_GPIO_CLK_ENABLE();/* WKUP时钟使能 */sys_gpio_set(WKUP_GPIO_PIN,SYS_GPIO_MODE_IN,SYS_GPIO_PUPD_PULLDOWN);/* WKUP引脚模式设置为下拉输入 */}1.2.2 EXTI的配置EXTI的配置就两步第一步就是配置AFIO来映射中断配置之前先使能AFIO时钟第二步是配置上升沿和下降沿。这个函数和正点原子会有点不同为了方便理解我把官方例程重新写了但是这样的结果就是每次只能配置一个IO口而官方例程可以同时配置同一组GPIO的多个口不过也没啥用即使官方可以同时配置多个它的例程也是写了多次而且这么多IO口在实际应用中应该也不会大量的使用同一组的GPIO的中断。voidsys_nvic_ex_config(GPIO_TypeDef*p_gpiox,uint16_tpinx,uint8_ttmode){uint8_tgpio_num0;/* gpio编号, 0~7, 代表GPIOA~GPIOG *//* 计算gpio编号,因为每一组GPIO之间的地址偏移相差0x400所以要除以它 */gpio_num((uint32_t)p_gpiox-(uint32_t)GPIOA)/0X400;RCC-APB2ENR|10;/* AFIO 1,使能AFIO时钟 *//* 配置AFIO寄存器*/switch(gpio_num){case0:case1:case2:case3:AFIO-EXTICR[0]~(0x000F(pinx%4));AFIO-EXTICR[0]|(gpio_num(pinx%4));break;case4:case5:case6:case7:AFIO-EXTICR[1]~(0x000F(pinx%4));AFIO-EXTICR[1]|(gpio_num(pinx%4));break;case8:case9:case10:case11:AFIO-EXTICR[2]~(0x000F(pinx%4));AFIO-EXTICR[2]|(gpio_num(pinx%4));break;case12:case13:case14:case15:AFIO-EXTICR[3]~(0x000F(pinx%4));AFIO-EXTICR[3]|(gpio_num(pinx%4));break;}/*配置上下拉触发模式tmode遵循官方例程定义*/if(tmode0x01)EXTI-FTSR|1gpio_num;if(tmode0x02)EXTI-RTSR|1gpio_num;}最后就是EXTI的初始化了直接调用上面的函数就行了voidextix_init(void){sys_nvic_ex_config(KEY0_INT_GPIO_PORT,WKUP_GPIO_PIN,SYS_GPIO_RTIR);/* WKUP配置为上升沿触发中断 */}1.2.3 NVIC的配置NVIC的配置就是配置一些中断优先级分组以及配置中断优先级之类的voidsys_nvic_init(uint8_tpprio,uint8_tsprio,uint8_tch,uint8_tgroup){uint32_ttemp;sys_nvic_priority_group_config(group);/* 设置分组不设置的话会使用默认的分组0 */temppprio(4-group);/*把pprio放到表示抢占优先级的高位*/temp|sprio(0x0fgroup);/*把pprio放到表示响应优先级的低位*/temp0xf;/* 取低四位 */NVIC-ISER[ch/32]|(1(ch%32));/* 使能中断位(要清除的话,设置ICER对应位为1即可) */NVIC-IP[ch]|temp4;/* 设置响应优先级和抢断优先级 */}然后就是NVIC初始化其实是没必要分这么细的就像官方例程那样这些初始化的东西就应该和上面的别的初始化内容放在一起。voidnvic_init(void){sys_nvic_init(3,2,WKUP_INT_IRQn,2);/* 抢占3子优先级2组2 */}最后就该写中断服务函数了这个WK_UP开关对应的是MCU的PA0所以对应第0根中断线也就是需要写一个上面启动文件中和89行同名的函数不过就像一开始说的一般都使用宏定义起更好听的名字了。#defineWKUP_INT_IRQHandlerEXTI0_IRQHandlervoidWKUP_INT_IRQHandler(void){delay_ms(20);/* 消抖 */EXTI-PRWKUP_INT_GPIO_PIN;/* 清除WKUP所在中断线 的中断标志位 */if(WK_UP1){BEEP_TOGGLE();/* LED2 状态取反 */}}通过看Cortex-M3权威指南发现中断远非上面写的这么简单不过对于日常的产品使用应该也勉强够用了来日方长慢慢学。