1. 中断
常见的书上关于中断举的例子,差不多就是这样:“你正在看书,厨房的水开了,你起来去关火,关了火以后继续看书”,这种例子说他好吧,差不多把中断的概念描述清楚了,但是根本没办法和单片机的联系起来,因为这种例子所描述的中断的概念光看“中断”这两个字也能猜个八九不离十,说了等于没说,实际上从正面去讲中断的概念也并不复杂,我甚至觉得不举这种例子反而更好理解,计算机程序正常情况都是从上到下依次执行的,当发生一些内部或者外部事件的时候,CPU本来应该继续向下执行的,但是却没有继续向下执行,反而从当前执行的代码处离开去执行别处的代码,这个过程就叫中断,这些内部或者外部的事件就叫做中断源。这里指讨论外部中断,等后面学到了其他的中断在讨论其他的。
1.1 NVIC与EXTI
众所周知,单片机的MCU是由内核和片上外设组成的,内核就是一般说的某某内核的芯片,比如战舰开发板采用 ARM Cortex-M3 内核,内核就包括CPU核心以及一些内部外设,内部外设的内是相对内核说的,内部外设的外是相对CPU处理器说的,而片上外设就是内核外面的那些外设,下图我框起来的部分,Cortex-M3’s internal peripherals这部分映射的地址就是控制内部外设的寄存器地址,而右边的那些(我的截图不完整,只截了一部分)非常多的就是片上外设的寄存器地址了。
NVIC是内部外设的一种,EXTI则是片上外设,他们两个搭配起来就构成了外部中断的完整配置了。
1.1.1 外部中断与Cortex-M3内核
所谓外部中断与M3内核的关系其实就是介绍M3内核的NVIC,在启动文件中可以看到一个叫做g_pfnVectors的变量(我用的不是MDK编译器,有可能和MDK的启动文件会有些许差异,估计应该差不了很多),这个变量是指向中断向量表的指针,中断向量表里存的就是中断服务函数的地址,当我们进行了一些恰当的配置以后,如果外部触发了某个中断,CPU就会去这个表里找对应函数的地址,然后去该地址执行相应的中断服务函数,比如在正点原子的外部中断例程中有一个头文件是exti.h
,里面有一个宏定义是#define KEY0_INT_IRQHandler EXTI4_IRQHandler
,这个宏的右边就是下图启动文件的第159行,左边就是对应的中断服务函数,因为C语言的函数名就是函数的地址,所以这样写没啥问题,这也能解释为什么最底层的中断函数无人调用了,当中断触发的时候由CPU去自己找对应的函数。中断向量表的这些函数顺序是芯片公司出厂就定义好的,我们只能服从,不能随意修改。
NVIC的寄存器介绍不在《STM32F10xxx参考手册》中,需要去《Cortex-M3权威指南》中查找相关寄存器的介绍,关于这部分寄存器在代码中的定义位于头文件core_cm3.h
中,代码中的名称和权威指南中的名称不太一样,我在下面的表做个记录:
寄存器功能 | 权威指南中的名称 | 代码中的名称 |
---|---|---|
中断使能 | ISER | SETENA |
中断失能 | ICER | CLRENA |
中断挂起 | ISPR | SETPEND |
中断解挂 | ICPR | CLRPEND |
中断激活标志寄存器 | IABR | ACTIVE |
中断优先级寄存器 | IP | PRI |
还有一个软件触发中断寄存器,这个我目前还不知道在那些场合会用到,估计是操作系统吧,反正也和外部中断没关系,后面遇到了再说。
- 中断的使能与失能寄存器
SETENAs和CLRENAs表示中断的使能和去使失能寄存器,s可以取0到7,总共8对寄存器,每个寄存器又有32位,按照每一位来配置一个中断来看,最多可以配置32*8=256个中断,而M3内核的SETENA7和CLRENA7只使用了16位,所以,M3最多可以支持240个中断,而在STM32F103ZET6中被进一步裁减,只使用了60位,也就是这两组寄存器中s=0,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 外部中断与STM32F103ZET6
NVIC是内核控制中断的外设,所有具有cortex-M3内核的芯片都大差不差,而如何把中断信息传递到NVIC就是片上外设的事情了,在stm32f103xe.h
这个头文件中,定义了一个如下图所示的枚举,82行的注释下面的部分总共有60个,就是之前说的可以把中断信号传递到NVIC的60个外设,其中第89到93行的EXTI就是根据外部的电平变化配置的中断,也是这次学习的主要内容。
EXTI是外部中断和事件控制器,根据上面的图可以看出来这个芯片总共有五根EXTI线,每根线之间相互独立,可以分别进行不同的配置,比如输入类型和触发事件,触发事件指的是外部信号的上升沿触发,下降沿触发或者双沿触发,而输入类型这个名词是从正点原子给的开发指南上看到的,我觉得更应该叫做EXTI的输出类型,EXTI接收的信号是外部的电平变化信号,它对这些信号处理了以后再进行产生脉冲或者传递信号至NVIC,所以相比EXTI来说,它更应该被叫做输出类型。来看参考手册中的这个外部中断/事件控制器框图:
,然后信号经过中断屏蔽寄存器组成的与门之后进入NVIC,这里和刚才说的事件屏蔽寄存器哪个与门意思差不多,所以这里就是配置中断屏蔽寄存器就是字面意思了,就是为了屏蔽某一路中断,从而让这个与门输出0,切断它与NVIC的联系。
1.2 EXTI与GPIO配置
刚才提到EXTI的输入可以是任何一个GPIO口或者一些其他的外设事件,总结如下:
EXTI线 | 对应连接 |
---|---|
0-15 | GPIO输入 |
16 | PVD输出 |
17 | RTC闹钟 |
18 | USB唤醒 |
19 | 以太网唤醒 |
除了前面的0-15是现在需要讨论的,后面的都暂时不看,因为我也不会,而且战舰开发板不是互联型产品,所以第19的以太网唤醒是没有的。
stm32f103系列总共有GPIOx(x = A - G)7组,每组16个,总共7*16=112个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为例看一下。
void gpio_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的中断。
void sys_nvic_ex_config(GPIO_TypeDef *p_gpiox, uint16_t pinx, uint8_t tmode)
{
uint8_t gpio_num = 0; /* gpio编号, 0~7, 代表GPIOA~GPIOG */
/* 计算gpio编号,因为每一组GPIO之间的地址偏移相差0x400,所以要除以它 */
gpio_num = ((uint32_t)p_gpiox - (uint32_t)GPIOA) / 0X400 ;
RCC->APB2ENR |= 1 << 0; /* AFIO = 1,使能AFIO时钟 */
/* 配置AFIO寄存器*/
switch (gpio_num) {
case 0:
case 1:
case 2:
case 3:
AFIO->EXTICR[0] &= ~(0x000F << (pinx%4));
AFIO->EXTICR[0] |= (gpio_num << (pinx%4));
break;
case 4:
case 5:
case 6:
case 7:
AFIO->EXTICR[1] &= ~(0x000F << (pinx%4));
AFIO->EXTICR[1] |= (gpio_num << (pinx%4));
break;
case 8:
case 9:
case 10:
case 11:
AFIO->EXTICR[2] &= ~(0x000F << (pinx%4));
AFIO->EXTICR[2] |= (gpio_num << (pinx%4));
break;
case 12:
case 13:
case 14:
case 15:
AFIO->EXTICR[3] &= ~(0x000F << (pinx%4));
AFIO->EXTICR[3] |= (gpio_num << (pinx%4));
break;
}
/*配置上下拉触发模式,tmode遵循官方例程定义*/
if (tmode & 0x01) EXTI->FTSR |= 1 << gpio_num;
if (tmode & 0x02) EXTI->RTSR |= 1 << gpio_num;
}
最后就是EXTI的初始化了,直接调用上面的函数就行了
void extix_init(void)
{
sys_nvic_ex_config(KEY0_INT_GPIO_PORT, WKUP_GPIO_PIN, SYS_GPIO_RTIR); /* WKUP配置为上升沿触发中断 */
}
1.2.3 NVIC的配置
NVIC的配置就是配置一些中断优先级分组,以及配置中断优先级之类的
void sys_nvic_init(uint8_t pprio, uint8_t sprio, uint8_t ch, uint8_t group)
{
uint32_t temp;
sys_nvic_priority_group_config(group); /* 设置分组,不设置的话会使用默认的分组0 */
temp = pprio << (4 - group); /*把pprio放到表示抢占优先级的高位*/
temp |= sprio & (0x0f >> group); /*把pprio放到表示响应优先级的低位*/
temp &= 0xf; /* 取低四位 */
NVIC->ISER[ch / 32] |= (1 << (ch % 32)); /* 使能中断位(要清除的话,设置ICER对应位为1即可) */
NVIC->IP[ch] |= temp << 4; /* 设置响应优先级和抢断优先级 */
}
然后就是NVIC初始化,其实是没必要分这么细的,就像官方例程那样,这些初始化的东西就应该和上面的别的初始化内容放在一起。
void nvic_init(void)
{
sys_nvic_init( 3, 2, WKUP_INT_IRQn, 2); /* 抢占3,子优先级2,组2 */
}
最后就该写中断服务函数了,这个WK_UP开关对应的是MCU的PA0,所以对应第0根中断线,也就是需要写一个上面启动文件中和89行同名的函数,不过就像一开始说的,一般都使用宏定义起更好听的名字了。
#define WKUP_INT_IRQHandler EXTI0_IRQHandler
void WKUP_INT_IRQHandler(void)
{
delay_ms(20); /* 消抖 */
EXTI->PR = WKUP_INT_GPIO_PIN; /* 清除WKUP所在中断线 的中断标志位 */
if (WK_UP == 1)
{
BEEP_TOGGLE(); /* LED2 状态取反 */
}
}
通过看Cortex-M3权威指南,发现中断远非上面写的这么简单,不过对于日常的产品使用应该也勉强够用了,来日方长,慢慢学。