STM32(7):中断方式让按键点亮LED

概述

上一节“STM32(5):轮训方式让按键点亮LED”实现了基于轮训的方式,实现点亮LED灯,本节将基于另外一种通信方式:中断方式,来实现点亮LED。

代码概览

void delay(unsigned int time)
{
        unsigned int i = 0;
        while (time--)
        {
                i = 1000000;
                while (i--)
                        ;
        }
}

u8 key_read()
{
        u8 result = 0;
        // delay(1);
        // 注意这里是读取InputDataBit
        result = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_12);

        return result;
}

void led_init()
{
        GPIO_InitTypeDef led;

        RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
        led.GPIO_Pin = GPIO_Pin_13;
        led.GPIO_Mode = GPIO_Mode_Out_PP;
        led.GPIO_Speed = GPIO_Speed_50MHz;
        GPIO_Init(GPIOC, &led);
        GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_SET);
}

void key_init()
{
        GPIO_InitTypeDef key;

        RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
        key.GPIO_Pin = GPIO_Pin_12;
        key.GPIO_Mode = GPIO_Mode_IPD;
        GPIO_Init(GPIOB, &key);
}

void led_opr(int opr)
{
        if (1 == opr)
        {
                GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_SET);
        }
        else
        {
                GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_RESET);
        }
}

void key_exti_init()
{
        EXTI_InitTypeDef key_exti;

        RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
        GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource12);

        key_exti.EXTI_Line = EXTI_Line12;
        key_exti.EXTI_LineCmd = ENABLE;
        key_exti.EXTI_Mode = EXTI_Mode_Interrupt;
        key_exti.EXTI_Trigger = EXTI_Trigger_Rising_Falling;

        EXTI_Init(&key_exti);
}

void key_nvid_init()
{
        NVIC_InitTypeDef key_nvic;
        key_nvic.NVIC_IRQChannel = EXTI15_10_IRQn; 
        key_nvic.NVIC_IRQChannelCmd = ENABLE;
        key_nvic.NVIC_IRQChannelPreemptionPriority = 1;
        key_nvic.NVIC_IRQChannelSubPriority = 0;

        NVIC_Init(&key_nvic);
}

void uart1_send(const u8 *data)
{
        while(*data)
        {
                USART_ClearFlag(USART1, USART_FLAG_TC);
                USART_SendData(USART1,*data++);
                while(USART_GetFlagStatus(USART1, USART_FLAG_TC)==RESET); 
                //delay_ms(1);
        }
}

int main(void)
{
        uart1_init();
        led_init();
        key_init();
        key_exti_init();
        key_nvid_init();

        uart1_send("main\r\n");
        while (1){
                //delay(1);
        }
}

static char flag = 0;

void EXTI15_10_IRQHandler()
{
        if (SET == EXTI_GetITStatus(EXTI_Line12))
        {
                if(0 ==flag){
                        uart1_send("get data: 0\r\n");
                        led_opr(0);
                        flag = 1;
                }else{
                        uart1_send("get data: 1\r\n");
                        led_opr(1);
                        flag = 0;
                }

                EXTI_ClearITPendingBit(EXTI_Line12);
        }
}

main函数

int main(void)
{
        uart1_init();
        led_init();
        key_init();
        key_exti_init();
        key_nvid_init();
        while (1){
                //delay(1);
        }
}

眼熟的函数

led_init,key_init在上一节“STM32(6):轮训方式让按键点亮LED”已经做了详细讲解,这里不再做赘述。

按键中断初始化

void key_exti_init()
{
        EXTI_InitTypeDef key_exti;

        RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
        GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource12);

        key_exti.EXTI_Line = EXTI_Line12;
        key_exti.EXTI_LineCmd = ENABLE;
        key_exti.EXTI_Mode = EXTI_Mode_Interrupt;
        key_exti.EXTI_Trigger = EXTI_Trigger_Rising_Falling;

        EXTI_Init(&key_exti);
}

定义中断初始化结构体

第一行代码定义了中断初始化结构体:EXTI_InitTypeDef ,其中EXTI是EXTernal Interrupt的缩写;即外部中断,什么是外部中断呢?要了解外部中断首先要了解中断(Interrupt),首先中断描述的是操作系统响应外部硬件请求的过程;中断响应需要在操作系统层面暂停当前正在运行的程序,转而到响应中断,并执行中断函数,当中断处理完毕后,还会恢复之前的服务/程序运行环境,继续执行。 就像你在写代码,突然老板叫你去一趟办公室,你需要保存一下程序,锁屏,然后来到老板办公室,聊了聊工作进度,聊完后回到座位,解锁屏幕,继续开发;老板叫你去办公室,就是中断,你保存代码,就是保存现场;回来继续编码就是中断响应完毕后继续执行之前的服务。 中断有硬中断和软中断,我们说的中断,一般都是指硬中断,即CPU相应硬件发出的中断请求,本质是硬件发出的(中断)信号;在STM32里面,软中断则是在软件层面的发出的中断,即通过调用EXTI_GenerateSWInterrupt()来进行触发,写软件中断寄存器(software Interrupt event register)实现中断,流程和机制和硬件中断类似,目的就是提高执行的优先级。 STM32的外部中断(EXTI),就是指硬中断,即由外设发起的中断。

启动AFIO

第二行代码RCC_APB2PeriphClockCmd则是启动了AFIO;

RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);

AFIO的全称Alternative fuction I/O and debug configuration,可选IO功能以及调试配置。所谓的AFIO其实就是重用GPIO引脚;当需要GPIO和某个特定功能进行系统级别绑定的时候,就需要使用AFIO;比如,如果想要把正常PC13是和LED灯亮灭功能绑定在一起的,如果想要让PC13响应中断,相当于把PC13的功能进行重新绑定,这个时候就需要配置AFIO;当然AFIO并不限于中断,还可以把某个引脚和定时器(后面会讲到)功能绑定,也是需要AFIO;不过如果是配置引脚响应外部中断,必须要使能AFIO; 总之,需要把GPIO引脚和某个常规的外设功能进行绑定,所谓常规功能是指片内外设的功能,片外外设功能,此时就需要AFIO;不过如果你自己想要使用某个引脚,其实是不需要AFIO,因为自己实现的功能并不属于“外设的常规功能”,比如上一节通过轮训的方式来点亮LED,就是使用PB123,并不需要使用AFIO;

配置中断触发引脚

第三行代码是配置中断响应引脚,这里配置的是PB12,在“STM32(5):轮询方式让按键点亮LED”小节中,PB12被配置为周期性的读取,根据高低电平来控制LED等,在本节PB12被作为中断触发的引脚。

GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource12);

中断配置

key_exti.EXTI_Line = EXTI_Line12;        
key_exti.EXTI_LineCmd = ENABLE;
key_exti.EXTI_Mode = EXTI_Mode_Interrupt; 
key_exti.EXTI_Trigger = EXTI_Trigger_Rising_Falling;

EXTI_Init(&key_exti);

中断配置第一个配置的就是中断线,因为触发中断的是引脚,在EXTI里面基于引脚来对中断是分组的,每一个组称之为中断线,如下图所示,所有的下标为0的分到EXTI0线,比如PA0,PB0,PC0... ...都是EXTI0线(组)的;下标为1的,PA1,PB1,PC1... ...都被分到EXTI1线;每个程序每组只能有一个引脚被成功配置为触发中断,否则就会覆盖。引脚编号都是16个,所以中断线也是16组,之所以要对中断线进行分组,主要目的是为了分配中断号,下面内容将会看到中断号的设计是将几组中断线分配到了一个中断号(这样设计可以减少系统中断数,减小系统复杂度): STM32(7):中断方式让按键点亮LED 第二行配置的使能中断线,即清空中断线的状态;

key_exti.EXTI_LineCmd = ENABLE;

第三行配置的是中断模式,这里配置的是Interrupt,另外一个选项是“事件”,中断有两种,一种是需要软件层面介入,这种被称之为Interrupt,而另外一种是不需要软件层面介入,直接硬件层面全全处理,这种称之为事件。“中断模式”需要和“软中断”以及“硬中断”区别开来,软硬中断都是属于“Interrupted”,因为他们都是需要软件介入,即都需要执行软件层面的中断函数,差别只是在于软中断需要在软件层面手动写入“软件中断事件寄存器”,而硬中断则是在有硬件层面基于边沿触发中断。

key_exti.EXTI_Mode = EXTI_Mode_Interrupt; 

接着配置项是触发边沿,这里采用的上升沿和下降沿都触发,或者说上升沿和下降沿都“敏感”;之所以要这样配置,是因为我们的设定是按键按下灯亮,按键松开,灯灭,其中按键按下是低电平,即由高电平到低电平,是下降沿,按键松开电平由低电平到高电平,是上升沿;所以中断触发是上升沿和下降沿都是要触发(关于上升沿和下降沿,我们会在后面的番外篇做介绍)。 key_exti.EXTI_Mode = EXTI_Mode_Interrupt;

最后调用EXTI_Init函数,来写入寄存器,使得配置生效。

EXTI_Init(&key_exti);

NVIC配置

void key_nvid_init()
{
        NVIC_InitTypeDef key_nvic;
        key_nvic.NVIC_IRQChannel = EXTI15_10_IRQn;
        key_nvic.NVIC_IRQChannelCmd = ENABLE;
        key_nvic.NVIC_IRQChannelPreemptionPriority = 1;
        key_nvic.NVIC_IRQChannelSubPriority = 0;

        NVIC_Init(&key_nvic);
}

什么是NVIC?Nested Vectored Interrupt Controller,内嵌向量中断控制器。NVIC定义了处理中断的优先级,在NVIC里面中断是有两个级别,第一个级别是抢占权限,即A中断发生了,如果B中断的抢占优先级高于A,那么A将会被中断,转而执行B,等待B执行完毕后,再来执行A,就是嵌套/递归的感觉,这个就是为什么这种中断处理机制被称之为Nested Vectored(递归向量)。 那么什么是Vectored?知乎上面有关于这个问题的大讨论:(6 条消息) 中断向量为什么叫中断向量? - 知乎 (zhihu.com);主要是两大阵营,一个认为Vector描述的是“容器”的意思;我个人觉得第二种解释,Vector的意思是“指针”更加贴切,因为从“中断向量表”这个名称来看,说明这个表中每一项都是一个“中断向量”,此时Vector再翻译为容器,似乎说不通。 再拉回代码:

key_nvic.NVIC_IRQChannel = EXTI15_10_IRQn;
key_nvic.NVIC_IRQChannelCmd = ENABLE;

第一行代码是配置中断通道(中断线),这里配置EXTI15_10_IRQn,代表就是配置响应的中断线范围是1015;第二行代码则是使能中断线,之所以要把几个中断线捏在一起,目的就是减少中断号,从而简化系统; 我们看一下定义,EXTI15_10_IRQn是定义在stm32f10x.h文件中;我截取了一部分,可以看到枚举值EXTI15_10_IRQn是用来的定义中断号的(Interrupt Numbers),除了EXIT,还有TIM,I2C等等中断的中断号,外部中断有6个中断号,第一个是中断组59(12行),第二个就是本次代码中出现的1015(18行);中断组14是具有独立的中断号(4~8行):

typedef enum IRQn{
/******  STM32 specific Interrupt Numbers *********************************************************/
... ...
    EXTI0_IRQn = 6,      /*!< EXTI Line0 Interrupt                                 */
    EXTI1_IRQn = 7,      /*!< EXTI Line1 Interrupt                                 */
    EXTI2_IRQn = 8,      /*!< EXTI Line2 Interrupt                                 */
    EXTI3_IRQn = 9,      /*!< EXTI Line3 Interrupt                                 */
    EXTI4_IRQn = 10,     /*!< EXTI Line4 Interrupt 
... ...
#ifdef STM32F10X_MD
    ... ...
    EXTI9_5_IRQn = 23,     /*!< External Line[9:5] Interrupts                        */
    TIM1_BRK_IRQn = 24,     /*!< TIM1 Break Interrupt                                 */
    ... ...
    I2C1_EV_IRQn = 31,     /*!< I2C1 Event Interrupt                                 */
    ... ...
    ... ...
    EXTI15_10_IRQn = 40,     /*!< External Line[15:10] Interrupts                      */
#endif /* STM32F10X_MD */  
}

为什么中断号配置“EXTI15_10_IRQn”呢?因为在配置key的EXTI的时候,配置的就是12号中断线:

    void key_exti_init()
    {
            ... ...
            key_exti.EXTI_Line = EXTI_Line12;         // 指定是12号中断线
            ... ...
    }

而在STM32里面,内置中断号包含有12号的,只有EXTI15_10_IRQHandler。

NVIC的优先级

接下来的两行代码则是配置中断的优先级:

key_nvic.NVIC_IRQChannelPreemptionPriority = 1;
key_nvic.NVIC_IRQChannelSubPriority = 0;

NVIC里面的C是controller,代表要基于NVIC的规则来对中断进行管理。NVIC定义了那些规则?

  1. 抢占优先级&嵌套,所谓的嵌套是指A中断在执行,B中断来了,A要停下啦,让B先执行,B完事后,再有A来做;即中断嵌套,这个也是为什么叫NVIC的原因,N是Nested,嵌套之意(V:Vector,I:Interrupt,C:Controller,全译就是:嵌套向量中断控制器;B之所以能够让A暂停是因为B的级别要高,这个可以嵌套执行中断的等级称之为Preemption Priority,抢占优先级;
  2. 执行优先级,用于当两个中断的抢占优先级(Preemption Priority)相同的时候,那么就是排队,按照先来先执行,执行完一个再来执行下一个;同时来的话,那么就由执行优先级来决定先执行谁,这就没有中断嵌套的概念了;来晚了就是要排队,即使你的优先级高也没有;你的优先级只是当同时到来发生冲突的时候,才有用,这个在STM32里面称之为Sub Priority,子优先级;
  3. 向量表位置优先级,对于抢占优先级相同,执行优先级相同的两个向量同时来了怎么办?根据中断向量表的定义的位置来决定位置;

中断向量表

上一小节中提到了中断向量表,这个小节我们就来关注一下中断向量表;本次实验使用EXTI15_10_IRQHandler这个中断函数,通过下面截取的代码,可以看出来,所谓的“中断向量表”,其实就是定义了中断函数的指针,实现了指定的函数,当指定的中断触发,就会执行指定的中断函数,中断号40对应的中断向量就是“EXTI15_10_IRQHandler”:

__Vectors       DCD     __initial_sp               ; Top of Stack
... ...
                                DCD     EXTI0_IRQHandler           ; EXTI Line 0
... ...
                                DCD     DMA1_Channel1_IRQHandler   ; DMA1 Channel 1
... ...
                                DCD     USART1_IRQHandler          ; USART1
... ...                
                                DCD     EXTI15_10_IRQHandler       ; EXTI Line 15..10
... ...                

再进一步分析一下,会发现其实对于NVIC的优先级配置(包括后面会提到的优先级组PriorityGroup),其实都是针对这个“中断号40的”的中断进行的配置。 NVIC函数最后调用的NVIC的初始化函数,来生效配置;

NVIC_Init(&key_nvic);

中断函数

static char flag = 0;
void EXTI15_10_IRQHandler()
{
        if (SET == EXTI_GetITStatus(EXTI_Line12))
        {
                if(0 ==flag){
                        led_opr(0);
                        flag = 1;
                }else{
                        led_opr(1);
                        flag = 0;
                }

                EXTI_ClearITPendingBit(EXTI_Line12);
        }
}

中断线配置

首先是if分支代码,“EXTI_GetITStatus”代表判断触发的是否是12号中断线,做此判断是因为中断线10~15号的中断都会触发此函数,所以首先判断一下该响应的中断线号:

if (SET == EXTI_GetITStatus(EXTI_Line12))
{
        ... ...
}

亮灭判断

因为在配置边沿触发场景的时候,配置的是上升沿,下降沿都触发,所以按下以及抬起来都是会触发中断,一个是亮灯一个灭灯,这里设置了一个全局变量flag,按下(高电平到低电平,下降沿)灯亮,松开(低电平到高电平,上升沿)灯亮,其中led_opr里面封装的即使控制PC13引脚的高低电平,可见封装的好处,就是调用的时候,直接call函数,传参数即可:

if(0 ==flag){
                led_opr(0);
                flag = 1;
        }else{
                led_opr(1);
                flag = 0;
        }
}    

清空寄存器状态

中断的最后,需要清空一下寄存器位内容,虽然中断本身不再需要轮训,但是对于寄存器位的读取,在STM32中采用的轮询机制,所以如果不把寄存器位置清空,发生了一次中断,将会不断的进行触发,所以每次响应完毕中断后,需要将(中断线)寄存器的位清空,这里是清空12号中断线的位:

EXTI_ClearITPendingBit(EXTI_Line12);

中断函数里面函数一定要尽快执行完毕,不能有延时或者耗时的操作,如果有,则需要需要跳出中断函数之后再处理,比如,基于生产者消费者模式,放到缓存中,然后再进行后续的处理,这种处理也被称之为“下半段”处理,即将耗时的操作后续放到内核中处理,但是不要放在中断函数中。

中断寄存器操作流程

那么,STM32里面负责中断都有哪些寄存器,他们又是如何进行交互的呢? STM32(7):中断方式让按键点亮LED 中断信息首先从输入线(Input Line)进入到处理电路:

  1. 首先是检测边沿,检测的结果将会写入到上升沿寄存器(Rising trigger selection register)或者下降沿寄存器(Falling trigger selection register); STM32(7):中断方式让按键点亮LED
  2. 然后是进入到后续“或门”,或门处理的是否有“硬件中断”(上升/下降触发)或者“软件中断”,简单讲就是检测是否有中断,是否有“软件中断”是根据“软中断事件寄存器(Software Interrupt event register)”状态来进行判断的; STM32(7):中断方式让按键点亮LED
  3. 接着是来到了“与门”,这个与门判断是中断屏蔽寄存器和中断事件做与运算,只有有一个为假就没有后续处理了,所谓的中断屏蔽寄存器(Interrupt mask register)就是配置那些中断线要被屏蔽掉,即不做中断响应,所以如果从输入线(Input Line)中响应的中断号是被屏蔽的,那么,就没有后续了; STM32(7):中断方式让按键点亮LED
  4. 如果存在中断,且中断号没有被屏蔽,那么就会写入到挂起请求寄存器(Pending request register),NVIC中断控制器将会轮询的方式访问该寄存器,来判断是否有中断到来,如果有则根据中断线号,来调用相应的中断函数。 STM32(7):中断方式让按键点亮LED
  5. 另外一个分支是根据是否有中断以及事件屏蔽寄存器来共同判断是否要产生脉冲,这个分支和本次中断无关,所以略过。 STM32(7):中断方式让按键点亮LED 我们在中断函数中最后EXTI_ClearITPendingBit就是将挂起请求寄存器(Pending Request Register)数据清空。 上面提到了中断处理函数要尽快处理,就是因为pending寄存器是会被覆盖的风险的,如果中断函数处理时间长了,后续到来的中断可能就会被再后面到来的中断给覆盖掉,所以要尽快处理到来的中断,避免pending的中断被覆盖。

附录

上升沿/下降沿选择寄存器说明

在上升沿和下降沿选择寄存器的说明中,有如下的内容: The external wakeup lines are edge triggered, no glitches must be generated on these lines.If a rising edge on external interrupt line occurs during writing of EXTI_RTSR register, thepending bit will not be set. Rising and Falling edge triggers can be set for the same interrupt line. In this configuration,both generate a trigger condition. 大意是:

  1. 外部唤醒都是边缘触发的,不能有毛刺波形,即窄脉冲,如果有毛刺波形,将会影响唤醒;
  2. 在写入上升沿/下降沿中断寄存器的时候,如果此时有上升沿/下降沿触发中断,该中断将会被忽略;
  3. 一个中断线(号)可以同时配置上升沿触发和下降沿触发,其实我们按键这个例子就是同时配置了上升沿和下降沿。

中断触发流程

下图是ARM CPU的触发中断的流程: STM32(7):中断方式让按键点亮LED

  1. 中断是由片内外设(比如Timer,UART)/外设(比如按键)触发指定引脚而产生的的,即触发是由STM32负责的;
  2. 由引脚触发进而产生中断;
  3. 然后STM32会把中断的请求丢给ARM CPU的NVIC来处理;NVIC将会对中断进行调度,NVIC决定了如何响应中断(比如是否抢断正在执行的中断,如果两个抢占优先级相同的中断同时到来如何来决定响应哪一个);
  4. 然后再根据中断向量表中绑定的中断函数;
  5. 调用指定的中断函数。
上一篇:入门单片机:点亮一个LED


下一篇:第一节 使用LED灯