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来说它更应该被叫做输出类型。来看参考手册中的这个外部中断/事件控制器框图{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权威指南发现中断远非上面写的这么简单不过对于日常的产品使用应该也勉强够用了来日方长慢慢学。