STM32学习之外部中断

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中,代码中的名称和权威指南中的名称不太一样,我在下面的表做个记录:

寄存器功能权威指南中的名称代码中的名称
中断使能ISERSETENA
中断失能ICERCLRENA
中断挂起ISPRSETPEND
中断解挂ICPRCLRPEND
中断激活标志寄存器IABRACTIVE
中断优先级寄存器IPPRI

还有一个软件触发中断寄存器,这个我目前还不知道在那些场合会用到,估计是操作系统吧,反正也和外部中断没关系,后面遇到了再说。

  1. 中断的使能与失能寄存器
    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的话,那就是哪个后执行哪个生效,先失能后使能,那中断有效,先使能后失能,那中断无效,不过应该没人会这么干吧?
  2. 中断的挂起与解挂起寄存器
    中断在执行的时候来了高优先级的中断会打断当前中断,而来了同优先级或者低优先级的中断,那么中断就不能立即得到响应,此时中断被挂起。中断的挂起状态可以通过“中断挂起寄存器(SETPEND)”和“中断解挂起寄存器(CLRPEND)”来读取,还可以通过软件写入对应位来手动挂起中断,不过使用情况不多,一般都是硬件置位与清零。如果需要软件写入的话,具体的使用方法和前面的使能和失能寄存器相同。
  3. 中断激活标志寄存器
    当CPU进入了某个中断服务函数以后,执行了第一条汇编指令之后,中断激活标志寄存器的对应位就会被硬件置1,这是一个只读寄存器,通过读这个寄存器就能知道当前正在执行哪个中断。这类寄存器和之前一样,也是总共8个,除了最后一个寄存器使用了16位之外,其余的每一位对应一个中断源。
  4. 中断优先级配置寄存器
    通过配置这个寄存器,就可以配置两个或以上的中断同时触发的时候先执行哪个再执行哪个,这类寄存器总共有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来说,它更应该被叫做输出类型。来看参考手册中的这个外部中断/事件控制器框图:
![在这里插入图片描述](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系列总共有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权威指南,发现中断远非上面写的这么简单,不过对于日常的产品使用应该也勉强够用了,来日方长,慢慢学。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值