4.3 STM32单片机中断编程

中断是计算机和嵌入式系统中的一个十分重要的概念,在现代计算机和嵌入式系统中毫无例外地都要采用中断技术。什么是中断呢?可以举一个日常生活中的例子来说明,假如你正在看书,电话铃响了。这时,你放下书,去接电话。通话完毕,再继续看书。这个例子就表现了中断及其处理过程:电话铃声使你暂时中止当前的工作,而去处理更为急需处理的事情(接电话),把急需处理的事情处理完毕之后,再回头来继续原来的事情。在这个例子中,电话铃声称为“中断请求”,你暂停写信去接电话叫做“中断响应”,接电话的过程就是“中断处理”。

在计算机执行程序的过程中,由于出现某个特殊情况(或称为“事件”),使得CPU中止现行程序,而转去执行处理该事件的处理程序(俗称中断处理或中断服务程序),待中断服务程序执行完毕,再返回断点处继续执行原来的程序,这个过程称为中断。

计算机为什么要采用中断

为了说明这个问题,再举一例子。假设你有一个朋友来拜访你,但是由于不知道何时到达,你只能在大门等待,于是什么事情也干不了。如果在门口装一个门铃,你就不必在门口等待而去干其他的工作,朋友来了按门铃通知你,你这时才中断你的工作去开门,这样就避免等待和浪费时间。计算机也是一样,如打印输出,CPU传送数据的速度高,而打印机打印的速度低,如果不采用中断技术,CPU将经常处于等待状态,效率极低。而采用了中断方式,CPU可以进行其他的工作,只在打印机缓冲区中的当前内容打印完毕发出中断请求之后,才予以响应,暂时中断当前工作转去执行向缓冲区传送数据,传送完成后又返回执行原来的程序。这样就大大地提高了计算机系统的效率。

中断是单片机实时地处理内部或外部事件的一种内部机制。当某种内部或外部事件发生时,单片机的中断系统将迫使CPU暂停正在执行的程序,转而去进行中断事件的处理,中断处理完毕后,又返回被中断的程序处,继续执行下去。也就是说,中断是一种发生了一个事件时调用相应的处理程序的过程。在一定条件下,CPU响应中断后,暂停源程序的执行,转至为这个事件服务的中断处理程序。

中断是由于软件的或硬件的信号,使得CPU放弃当前的任务,转而去执行另一段子程序。可见中断是一种可以人为参与(软件)或者硬件自动完成的,使CPU发生的一种程序跳转。通常外部中断是由外部设备通过请求引脚向CPU提出的。中断信号也可以是CPU内部产生的,如定时器、实时时钟等。

在STM32单片机复位期间和刚复位后,复用功能未开启,I/O端口被配置成浮空输入模式。所有端口都有外部中断能力。为了使用外部中断线,端口必须配置成输入模式。

STM32单片机的中断系统

相对于ARM7使用的外部中断控制器,Cortex-M3内核中集成了中断控制器和中断优先级控制寄存器,支持256个中断(16个内核+240个外部)和可编程256 级中断优先级的设置。NVIC使用的是基于堆栈的异常模型。在处理中断时,将程序计数器,程序状态寄存器,链接寄存器和通用寄存器压入堆栈,中断处理完成后,再恢复这些寄存器。堆栈处理是由硬件完成的,无须在中断服务程序中进行堆栈操作。使用尾链(tail-chaining)连续中断技术只需消耗三个时钟周期,相比于32个时钟周期的连续压、出堆栈,大大降低了延迟,提供了确定的、低延迟的中断处理,提高了性能。

STM32单片机并没有使用ARM Cortex-M3内核全部的东西(如内存保护单元MPU、8位中断优先级等),因此它的NVIC是ARM Cortex-M3内核的NVIC的子集。STM32F10×系列单片机的嵌套中断向量控制器(NVIC)支持68个可屏蔽中断通道(不包含16个Cortex-M3内核的中断线),具有16级可编程中断优先级的设置(仅使用中断优先级设置8bit中的高4位),见表4.1和表4.2。

表4.1 ARM Cortex-M3内核的16个中断通道对应的中断向量表

表4.2 STM32F10×系列单片机的可屏蔽中断通道对应的中断向量表

续表

续表

嵌套中断向量控制器(NVIC)相关寄存器管理STM32 单片机所有中断开关和中断优先级。NVIC共支持1~240个外部中断输入。除了个别中断的优先级被固定外,其他中断的优先级都是可设置的。其中,对于所有的ARM Cortex-M3内核处理器(包括STM32),256个中断(异常)中的前面16个(0~15号)内核中断都是一样的,而240个外部中断具体的数值由芯片厂商在设计芯片时决定。一般地,各种芯片的中断源数目常常不到240 个,并且优先级的位数也由芯片厂商决定。对于STM32F10×系列单片机而言,其嵌套中断向量控制器(NVIC)支持68个可屏蔽中断通道(不包含16个Cortex-M3内核的中断线),具有16级可编程中断优先级的设置(仅使用中断优先级设置8bit中的高4位)。

编写STM32单片机的中断服务程序,首先要知道stm32f10x_it.c这个文件。打开固件库目录(\library\src)下的这个文件,可以看到***_IRQHandler函数的实现,虽然说是实现,但是几乎都是空的。这些函数就是要开发者填写的中断服务(处理)函数,如果你用到了哪个中断来做相应的处理,你就要填写相应的中断处理函数。这需要根据STM32单片机开发板(嵌入式产品)各外设的实际情况来填写,但是一般都会有关闭和开启中断,以及清除中断标记。在这个文件中还有很多系统相关的中断处理函数,如系统时钟SysTickHandler。

中断通道

每个中断对应一个外围设备,但外围设备通常具备若干个可以引起中断的中断源或中断事件,而该设备的所有的中断都只能通过该指定的“中断通道”向内核申请中断。STM32系列单片机可以支持的68个外部中断通道,已经固定地分配给相应的外部设备。

STM32单片机的外部中断

STM32单片机80个通用I/O端口连接到19个外部中断/事件源上。图4.12是STM32单片机通用I/O与外部中断的映射关系:PAx, PBx, PCx, PDx和PEx端口对应的是同一个外部中断/事件源EXTIx(x:0~15)。另外三个外部中断/事件控制器的连接如下:

图4.12 STM32单片机通用I/O与外部中断的映射关系

● 外部中断/事件源EXTI 16连接到PVD电源电压检测输出;

● 外部中断/事件源EXTI 17连接到RTC闹钟事件;

外部中断/事件源EXTI 18连接到从USB待机唤醒事件。

将教学开发板上4个按键中的一个按键(PC9端口)设置成可以中断,以实现下面的按键中断程序。

任务五 按键中断

例程:KeyWithEINT.c

      #include "stm32f10x_heads.h"
      #include "HelloRobot.h"
      int main(void)
      {
        BSP_Init(); //开发板初始化函数
        USART_Configuration();
        printf("Program Running!\n");
        while (1);//等待中断到来
      }

在固件库文件stm32f10x_it.c中,编写中断服务函数代码:

      #include "stm32f10x_it.h"
      extern void delay_nms(unsigned long n);
      …
      void EXTI9_5_IRQHandler(void)
      {
        if(GPIO_ReadOutputDataBit(GPIOB, GPIO_Pin_9)==0)
        GPIO_SetBits(GPIOB, GPIO_Pin_9);
        else
        GPIO_ResetBits(GPIOB, GPIO_Pin_9);
        delay_nms(10);  // 去抖动
        EXTI_ClearITPendingBit( EXTI_Line9) ;//中断结束时清中断标志位
        EXTI_ClearITPendingBit( EXTI_Line5) ;//中断结束时清中断标志位
      }
      /* BSP_Init()开放了EINT4中断,如不用,为了防止干扰信号,加入以下代码 */
      void EXTI4_IRQHandler(void)
      {
        EXTI_ClearITPendingBit(EXTI_Line4) ;//中断结束时清中断标志位
      }

中断的初始化工作放在开发板初始化函数BSP_Init()中(下面将介绍),主程序执行到while(1)等待中断的到来。当中断到来时,转而跳转到中断处理函数***_IRQHandler()中去。

该你了——编写、下载并执行这个程序

按下PC9端口对应的按键,相应的发光二极管的亮灭状态会交替变化。

到这里,你可能会有个疑问:当一个中断到来时,是怎样关联到STM32固件库里面的***_IRQHandler(void)中断服务(处理)函数呢?即STM32单片机的中断执行过程是怎样的?这就需要了解STM32单片机的中断结构和相关寄存器的配置。

STM32单片机的中断机制

(1)STM32单片机外部中断/事件控制器结构

STM32单片机80个通用I/O端口连接到19个外部中断/事件源上,如图4.13所示。外部中断/事件控制器由19个产生事件/中断要求的边沿检测器组成。每个输入线可以独立地配置输入类型(脉冲或挂起)和对应的触发事件(上升沿或下降沿或者双边沿都触发)。每个输入线都可以被独立的屏蔽,由“登记请求寄存器”保持着状态线的中断请求。“登记请求寄存器”也叫“挂起请求寄存器”,指的是同一个寄存器。

图4.13 STM32单片机外部中断/事件控制器结构图

图4.13中信号线上划有一条斜线,旁边标志19字样的注释,表示这样的线路共有19套。在图上部的APB总线和外设模块接口,是每一个功能模块都有的部分,CPU通过这样的接口访问各个功能模块。

图中的实线箭头,标出了外部中断信号的传输路径,首先外部信号从编号1 的芯片引脚进入,经过编号2的边沿检测电路,通过编号3的或门进入中断“登记(挂起)请求寄存器”,最后经过编号4的与门输出到M3内核的“NVIC中断控制器”。在这个通道上有4个控制部分。

① 外部的信号首先经过边沿检测电路,这个边沿检测电路受“上升沿选择寄存器”或“下降沿选择寄存器”控制,用户可以使用这两个寄存器控制需要哪一个边沿产生中断,因为选择上升沿或下降沿是分别受2个独立的寄存器控制,所以可以同时选择上升沿或下降沿,而如果只有一个寄存器控制,那么只能选择一个边沿了。

② 接下来是编号3的或门,这个或门的另一个输入是“软件中断/事件寄存器”,从这里可以看出,软件可以优先于外部信号请求一个中断或事件,即当“软件中断/事件寄存器”的对应位为“1”时,不管外部信号如何,编号3的或门都会输出有效信号。

③ 一个中断或事件请求信号经过编号3的或门后,进入“登记(挂起)请求寄存器”,到此,中断和事件的信号传输通路都是一致的,也就是说,“登记(挂起)请求寄存器”中记录了外部信号的电平变化。

④ 外部请求信号最后经过编号4的与门,向Cortex-M3内核的“NVIC中断控制器”发出一个中断请求,如果“中断屏蔽寄存器”的对应位为‘0’,则该请求信号不能传输到与门的另一端,从而实现了中断的屏蔽。

上述是外部中断的请求机制,下面介绍事件的请求机制。图4.13中虚线箭头,标出了外部事件信号的传输路径,外部请求信号经过编号3的或门后,进入编号5的与门,这个与门的作用与编号4的与门类似,用于引入“事件屏蔽寄存器”的控制;最后脉冲发生器把一个跳变的信号转变为一个单脉冲,输出到芯片中的其他功能模块。

从图4.13可以知道,从外部激励信号来看,中断和事件是没有分别的,只是在芯片内部分开,一路信号会向CPU产生中断请求,另一路信号会向其他功能模块发送脉冲触发信号,其他功能模块如何相应这个触发信号,则由对应的模块自己决定。

STM32单片机通用I/O与外部中断的映射关系

在图4.12中的STM32单片机通用I/O与外部中断的映射关系中,AFIO_EXTICRx(x:1~4)寄存器映像和复位值见表4.3。其中包含了EXTI0[3:0]~ EXTI15[3:0]。举例说明:

表4.3 AFIO寄存器映像和复位值

AFIO_EXTICR1寄存器的EXTI0[3:0]位的含义:

0000:代表PA0引脚;0001:代表PB0引脚;0010:代表PC0引脚;0011:代表PD0引脚;0100:代表PE0引脚。

AFIO_EXTICR1寄存器的EXTI1[3:0]位的含义:

0000:代表PA1引脚;0001:代表PB1引脚;0010:代表PC1引脚;0011:代表PD1引脚;0100:代表PE1引脚。依次类推。

这19个外部中断/事件源所对应的控制器寄存器映像和复位值见表4.4,存储器映射首地址为:0x40010400。下面简单介绍这些寄存器的含义。

表4.4 外部中断/事件控制器寄存器映像和复位值表

硬件中断选择通过下面的过程来配置19个线路做为中断源,相关的寄存器配置包括:

● 配置19个中断线的中断屏蔽寄存器(EXTI_IMR:Interrupt Mask Register);

● 配置触发方式:上升沿触发选择寄存器(EXTI_RTSR:Rising Trigger Selection Register)和下降沿触发选择寄存器(EXTI_FTSR:Falling Trigger Selection Register);

● 配置那些控制I/O映像到外部中断控制器(EXTI)的NVIC中断通道的使能和屏蔽位,使得19个中断线中的请求可以被正确地响应。

硬件事件选择通过下面的过程来配置19个线路作为事件源,相关的寄存器配置包括:

● 配置19个事件线的事件屏蔽寄存器(EXTI_EMR:Event Mask Register);

● 配置触发方式:上升沿触发选择寄存器(EXTI_RTSR)和下降沿触发选择寄存器(EXTI_FTSR)。

软件中断/事件的选择使19个线路可以被配置成软件中断/事件线,相关寄存器的配置包括:

● 配置19个中断/事件线的中断屏蔽寄存器和事件屏蔽寄存器(EXTI_IMR, EXTI_EMR);

● 配置软件中断/事件寄存器的请求位(EXTI_SWIER:Software Interrupt Event Register)。

如要产生中断,中断线须事先配置好并被激活。这是根据需要的边沿检测通过设置2 个“触发选择寄存器”,并在“中断屏蔽寄存器”的相应位写“1”来允许中断请求。当需要的边沿在外部中断线上发生时,将产生一个中断请求,对应的“登记(挂起)请求寄存器”相应位被置1。

注意通过写“1”到“中断登记(挂起)寄存器”,而不是写“0”清除该中断请求标记。ARM9和ARM11处理器也是这样。

为产生事件触发,事件连接线须事先配置好并被激活。这是根据需要的边沿检测通过设置2 个“触发选择寄存器”,并在“事件屏蔽寄存器”的相应位写“1”来允许事件请求。当需要的边沿在事件连线上发生时,将产生一个事件请求脉冲,对应的“登记(挂起)请求寄存器”相应位不被置1。

通过在“软件中断/事件寄存器”写“1”,一个中断/事件请求也可以通过软件来产生。即软件可以优先于外部信号请求一个中断或事件,当“软件中断/事件寄存器”的对应位为“1”时,不管外部信号如何,也会产生一个中断/事件请求,对应的“登记(挂起)请求寄存器”相应位也被置1。

嵌套向量中断控制器(NVIC)

在图4.13中,中断信号经屏蔽寄存器控制后,送至ARM Cortex-M3内核中的“嵌套向量中断控制器(NVIC)”。我们知道NVIC可以支持240个外部中断输入。这240个中断的使能与禁止(除能)分别使用各自的寄存器来控制。这与传统的、使用单一比特的两个状态来设置中断使能与禁止是不同的。

NVIC中有240对使能位/禁止位,每个中断拥有一对。这240个对分布在8对32位寄存器中(最后一对没有用完),分别是SETENA0~SETENA7和CLRENA0~CLRENA7,对应地址是:0xE000E100~0xE000E11C和0xE000E180~0xE000E19C,见表4.5。

表4.5 SETENAx和CLRENAx寄存器表

NVIC中欲使能一个中断,需写“1”到SETENAx对应位;欲禁止(除能)一个中断,需写“1”到CLRENAx对应位。通过这种方式,使能/禁止中断时只需把相应位写成“1”,其他的位可以全部为零,从而实现每个中断都可以独立地设置。

SETENA位和CLRENA位共有240对,对应的32位寄存器只需要8对。需要注意的是,在特定的芯片中,只有该芯片实现了的中断,其对应位才有意义。因此,如果你使用的芯片支持32个中断,则只有SETENA0/CLRENA0才需要使用。SETENA/CLRENA可以按字/半字/字节的方式来访问。前16个异常已经分配给系统异常(见表4.1),故而中断0的异常号是16。

现在我们再回到STM32的中断中来,STM32支持68个外部中断,既IRQ0~IRQ42(窗口看门狗中断~USB从挂起唤醒中断)。以串口1中断为例,如果要使能串口1中断,就要找到串口中断号,查看表4.2所示的中断向量表得知串口1(UART1)的中断号为37,那么将寄存器SETENA1中的位5置“1”就可使能UART1中断。

(2)外部中断相关寄存器的配置

外部中断相关寄存器的配置在“HelloRobot.h”文件的GPIO配置函数GPIO_Configuration和中断控制配置函数NVIC_Configuration中。以配置PC9按键作为外部中断口为例,配置外部中断首先需要打开相应的I/O口配置和时钟等,见GPIO配置函数:

      void RCC_Configuration(void)         //以配置GPIOC_9口作为外部中断口为例
      {  …
      RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC , ENABLE);
      }
      void GPIO_Configuration(void)        //以配置GPIOC_9口作为外部中断口为例
      {  …
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
         GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IN_FLOATING;
         GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
         GPIO_Init(GPIOC,&GPIO_InitStructure);
        }

下面,再看看中断控制配置函数NVIC_Configuration:

      NVIC_InitTypeDef NVIC_InitStruct;
      EXTI_InitTypeDef EXTI_InitStructure;
      …
      void NVIC_Configuration(void)                         //以配置GPIOC_9口作为外部中断口为例
      {
      …
GPIO_EXTILineConfig(GPIO_PortSourceGPIOC, GPIO_PinSource9);
      /*调用固件库中的GPIO_EXTILineConfig函数,其中2个参数分别是中断口和中端口对应的引脚号*/
      EXTI_InitStructure.EXTI_Line=EXTI_Line9;             // 将中断映射到中断/事件源Line9
      EXTI_InitStructure.EXTI_Mode=EXTI_Mode_Interrupt;    //中断模式
      EXTI_InitStructure.EXTI_Trigger=EXTI_Trigger_Falling;//设置为下降沿中断
      EXTI_InitStructure.EXTI_LineCmd=ENABLE;              //中断使能,即开中断
      EXTI_Init(&EXTI_InitStructure);
      /* 调用EXTI_Init固件库函数,将结构体写入EXTI相关寄存器中 */
      NVIC_InitStruct.NVIC_IRQChannel = EXTI9_5_IRQChannel;
      /* 选通通道9~5,即选择Px5,Px6,Px7,Px8,Px9作为中断源 */
      NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority=0; //0级抢占式优先级
      NVIC_InitStruct.NVIC_IRQChannelSubPriority=0;        //0级副优先级
      NVIC_InitStruct.NVIC_IRQChannelCmd=ENABLE;           //使能引脚作为中断源
      NVIC_Init(&NVIC_InitStruct);                         //调用NVIC_Init固件库函数进行设置
      …
      }

“HelloRobot.h”文件中的中断控制配置函数NVIC_Configuration完整代码如下:

      NVIC_InitTypeDef NVIC_InitStruct;
      EXTI_InitTypeDef EXTI_InitStructure;
      …
      void NVIC_Configuration(void)
      {
        NVIC_InitTypeDef NVIC_InitStructure;
      #ifdef  VECT_TAB_RAM
        /* Set the Vector Table base location at 0x20000000 */
        NVIC_SetVectorTable(NVIC_VectTab_RAM, 0x0);
      #else  /*VECT_TAB_FLASH  */
        /* Set the Vector Table base location at 0x08000000 */
        NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x0);
      #endif
        /* Configure the NVIC Preemption Priority Bits[配置优先级组] */
        NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0);
        /* Enable the TIM1 gloabal Interrupt [允许TIM1全局中断]*/
        NVIC_InitStructure.NVIC_IRQChannel = TIM1_UP_IRQChannel;
        NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
        NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
        NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
        NVIC_Init(&NVIC_InitStructure);
        /* Enable the TIM2 gloabal Interrupt [允许TIM2全局中断]*/
        NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQChannel;
        NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
        NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
        NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
        NVIC_Init(&NVIC_InitStructure);
        /* Enable the RTC Interrupt */
        NVIC_InitStructure.NVIC_IRQChannel = RTC_IRQChannel;
        NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
        NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
        NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
        NVIC_Init(&NVIC_InitStructure);
        /*Configure INT IO  PC9 enable exti9_5*/
        GPIO_EXTILineConfig(GPIO_PortSourceGPIOC, GPIO_PinSource9);
        EXTI_InitStructure.EXTI_Line=EXTI_Line9;
        EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
        EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
        EXTI_InitStructure.EXTI_LineCmd = ENABLE;
        EXTI_Init(&EXTI_InitStructure);
        NVIC_InitStruct.NVIC_IRQChannel = EXTI9_5_IRQChannel;
        NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority =0;
        NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
        NVIC_InitStruct.NVIC_IRQChannelCmd =ENABLE;
        NVIC_Init(&NVIC_InitStruct);
        /*Configure INT IO  PE4 enable exti4*/
        GPIO_EXTILineConfig(GPIO_PortSourceGPIOE, GPIO_PinSource4);
        EXTI_InitStructure.EXTI_Line=EXTI_Line4;
        EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
        EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
        EXTI_InitStructure.EXTI_LineCmd = ENABLE;
        EXTI_Init(&EXTI_InitStructure);
        NVIC_InitStruct.NVIC_IRQChannel = EXTI4_IRQChannel;
        NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority =0;
        NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
        NVIC_InitStruct.NVIC_IRQChannelCmd =ENABLE;
        NVIC_Init(&NVIC_InitStruct);
        /*Configure INT IO  PE5 enable exti9_5*/
        GPIO_EXTILineConfig(GPIO_PortSourceGPIOE, GPIO_PinSource5);
        EXTI_InitStructure.EXTI_Line=EXTI_Line5;
        EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
        EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
        EXTI_InitStructure.EXTI_LineCmd = ENABLE;
        EXTI_Init(&EXTI_InitStructure);
        NVIC_InitStruct.NVIC_IRQChannel = EXTI9_5_IRQChannel;
        NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority =0;
        NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
        NVIC_InitStruct.NVIC_IRQChannelCmd =ENABLE;
        NVIC_Init(&NVIC_InitStruct);
      }

这个函数进行了如下中断配置:

① 定时器1(TIM1)中断;

② 定时器2(TIM2)中断;

③ 实时时钟(RTC)中断;

④ PC9按键中断;

⑤ PE4中断和PE5中断(I/O引脚扩展)。

其中,NVIC_InitTypeDef结构在固件库文件“stm32f10x_nvic.h”中定义,EXTI_InitTypeDef结构在固件库文件“stm32f10x_exti.h”中定义。

      typedef struct
      {
        u8 NVIC_IRQChannel;
        u8 NVIC_IRQChannelPreemptionPriority;
        u8 NVIC_IRQChannelSubPriority;
        FunctionalState NVIC_IRQChannelCmd;
      } NVIC_InitTypeDef;
      typedef struct
      {
        u32 EXTI_Line;
        EXTIMode_TypeDef EXTI_Mode;
        EXTITrigger_TypeDef EXTI_Trigger;
        FunctionalState EXTI_LineCmd;
      }EXTI_InitTypeDef;

在固件库文件“stm32f10x_nvic.h”中还定义了STM32单片机68个外部可屏蔽中断通道号:

      /* IRQ Channels ------------------------------------------------------*/
      #define WWDG_IRQChannel         ((u8)0x00)  /*Window WatchDog Interrupt*/
      …
      #define EXTI0_IRQChannel        ((u8)0x06)  /*EXTI Line0 Interrupt*/
      #define EXTI1_IRQChannel        ((u8)0x07)  /*EXTI Line1 Interrupt*/
      #define EXTI2_IRQChannel        ((u8)0x08)  /*EXTI Line2 Interrupt*/
      #define EXTI3_IRQChannel        ((u8)0x09)  /*EXTI Line3 Interrupt*/
      #define EXTI4_IRQChannel        ((u8)0x0A)  /*EXTI Line4 Interrupt*/
      …
      #define ADC_IRQChannel           ((u8)0x12)  /*ADC global Interrupt*/
      …
      #define CAN_RX1_IRQChannel       ((u8)0x15)  /*CAN RX1 Interrupt*/
      #define CAN_SCE_IRQChannel       ((u8)0x16)  /*CAN SCE Interrupt*/
      #define EXTI9_5_IRQChannel       ((u8)0x17)  /*External Line[9:5]Interrupts*/
      #define TIM1_BRK_IRQChannel      ((u8)0x18)  /*TIM1 Break Interrupt*/
      #define TIM1_UP_IRQChannel       ((u8)0x19)  /*TIM1 Update Interrupt*/
      #define TIM1_TRG_COM_IRQChannel  ((u8)0x1A)  /*TIM1 Trigger and Commutation Interrupt*/
      #define TIM1_CC_IRQChannel       ((u8)0x1B)  /*TIM1 Capture Compare Interrupt*/
      #define TIM2_IRQChannel          ((u8)0x1C)  /*TIM2 global Interrupt*/
      #define TIM3_IRQChannel          ((u8)0x1D)  /*TIM3 global Interrupt*/
      #define TIM4_IRQChannel          ((u8)0x1E)  /*TIM4 global Interrupt*/
      …
      #define USART1_IRQChannel        ((u8)0x25)  /*USART1 global Interrupt*/
      #define USART2_IRQChannel        ((u8)0x26)  /*USART2 global Interrupt*/
      #define USART3_IRQChannel        ((u8)0x27)  /*USART3 global Interrupt*/
      #define EXTI15_10_IRQChannel     ((u8)0x28)  /*External Line[15:10]Interrupts*/
      #define RTCAlarm_IRQChannel      ((u8)0x29)  /*RTC Alarm through EXTI Line Interrupt*/
      #define USBWakeUp_IRQChannel     ((u8)0x2A)  /* USB WakeUp from suspend through EXTI Line
Interrupt */
      …

(3)中断服务(处理)函数

比如,当我们用PE4端口作为外部中断源时,就要配置这些外部中断/事件源寄存器的bit4位,它所对应的中断服务函数为EXTI4_IRQHandler。

注意:在STM32系列微控制器的固件库中,EXTI5~EXTI9这几个外部中断/事件源分成同一组,它们共用一个中断服务函数EXTI9_5_IRQHandler;EXTI10~EXTI15这几个外部中断/事件源分成同一组,它们共用一个中断服务函数EXTI15_10_IRQHandler。基于ARM9和ARM11的嵌入式处理器也有同样的特性。

中断服务函数的实现在固件库文件stm32f10x_it.c文件中。以PC9按键中断为例,它对应的中断通道为:EXTI9_5_IRQChannel,中断服务函数为EXTI9_5_IRQHandler。这样当PC9按键按下产生中断时,可以在中断服务函数中编写相应处理代码,比如:

      void EXTI9_5_IRQHandler(void)           //中断服务函数的入口
      {
          printf("Intterrupt is coming!\n");  //中断到来时打印字符
          delay_nms(100) ;
          EXTI_ClearITPendingBit(EXTI_Line9); //中断结束时清中断标志位
      }

中断服务函数是指当中断到来时,程序停止执行正在执行的语句,转而跳转到的函数。这个函数的执行也叫中断响应,中断响应结束时,应该清除中断标志位,也叫做清中断源,这样可以防止在中断返回后再次进入中断函数,出现死循环。

这里要注意:如果没有清除中断标志,则程序会反复进入中断,跳不出来。这种用软件方法清除中断标志的情况,在ARM9和ARM11系统中也是如此。

(4)STM32单片机中断服务函数关联机制

STM32单片机中断服务函数的声明没有像C51那样的特定格式。那么,当一个中断到来时,这些中断服务(处理)函数是如何被中断请求调用的呢?这是在设置NVIC(嵌套向量中断控制器)的时候将中断关联到了中断向量表。在文件“stm32f10x_nvic.c”的初始化NVIC函数NVIC_Init(&NVIC_InitStruct)中,结构体NVIC_InitStruct中一个成员是NVIC_IRQChannel。下面是设置使能NVIC_IRQChannel的语句:

      /* Enable the Selected IRQ Channels */
        NVIC->Enable[(NVIC_InitStruct->NVIC_IRQChannel >> 0x05)] =
          (u32)0x01 << (NVIC_InitStruct->NVIC_IRQChannel & (u8)0x1F);

同时,在工程文件下有个library目录,里面除了有固件库C语言文件外,还有2 个*.s文件,这是2 个启动代码,用汇编语言写的,主要完成处理器的初始化工作,其作用在第1章介绍了。其中,启动文件stm32f10x_vector.s的作用有:初始化堆栈、定义程序启动地址、中断向量表和中断服务程序入口地址,以及系统复位启动时,从启动代码跳转到用户main函数入口地址。

stm32f10x_vector.s文件里就有中断向量表(中断地址表),它指向有中断服务函数,名称已经定义好,写中断服务程序时要与这里的名字一致。

      ;************************************************************************
      ; Fill-up the Vector Table entries with the exceptions ISR address
      ;************************************************************************
                    AREA    RESET,DATA,READONLY
                    EXPORT  __Vectors
      __Vectors       DCD  __initial_sp            ;Top of Stack
                    DCD  Reset_Handler
                    DCD  NMIException
                    DCD  HardFaultException
                    DCD  MemManageException
                    DCD  BusFaultException
                    DCD  UsageFaultException
                    DCD  0               ;Reserved
                    DCD  0               ;Reserved
                    DCD  0               ;Reserved
                    DCD  0               ;Reserved
                    DCD  SVCHandler
                    DCD  DebugMonitor
                    DCD  0               ;Reserved
                    DCD  PendSVC
                    DCD  SysTickHandler
                    DCD  WWDG_IRQHandler
                    DCD  PVD_IRQHandler
                    DCD  TAMPER_IRQHandler
                    DCD  RTC_IRQHandler
                    DCD  FLASH_IRQHandler
                    DCD  RCC_IRQHandler
                    DCD  EXTI0_IRQHandler
                    DCD  EXTI1_IRQHandler
                    DCD  EXTI2_IRQHandler
                    DCD  EXTI3_IRQHandler
                    DCD  EXTI4_IRQHandler
                    DCD  DMAChannel1_IRQHandler
                    DCD  DMAChannel2_IRQHandler
                    DCD  DMAChannel3_IRQHandler
                    DCD  DMAChannel4_IRQHandler
                    DCD  DMAChannel5_IRQHandler
                    DCD  DMAChannel6_IRQHandler
                    DCD  DMAChannel7_IRQHandler
                    DCD  ADC_IRQHandler
                    DCD  USB_HP_CAN_TX_IRQHandler
                    DCD  USB_LP_CAN_RX0_IRQHandler
                    DCD  CAN_RX1_IRQHandler
                    DCD  CAN_SCE_IRQHandler
                    DCD  EXTI9_5_IRQHandler
                    DCD  TIM1_BRK_IRQHandler
                    DCD  TIM1_UP_IRQHandler
                    DCD  TIM1_TRG_COM_IRQHandler
                    DCD  TIM1_CC_IRQHandler
                    DCD  TIM2_IRQHandler
                    DCD  TIM3_IRQHandler
                    DCD  TIM4_IRQHandler
                    DCD  I2C1_EV_IRQHandler
                    DCD  I2C1_ER_IRQHandler
                    DCD  I2C2_EV_IRQHandler
                    DCD  I2C2_ER_IRQHandler
                    DCD  SPI1_IRQHandler
                    DCD  SPI2_IRQHandler
                    DCD  USART1_IRQHandler
                    DCD  USART2_IRQHandler
                    DCD  USART3_IRQHandler
                    DCD  EXTI15_10_IRQHandler
                    DCD  RTCAlarm_IRQHandler
                    DCD  USBWakeUp_IRQHandler
                    …

上面就是中断向量表,每一个item对应一个中断或异常处理,这里item的填写要和STM32数据手册中的Interrupt and exception vectors列表中的顺序一致。

那么这个向量表是放在何处呢?它被链接器放到了一个地址上:如果是存放在RAM中,地址是0x20000000;如果是存放在FLASH中,地址是0x08000000。

      #ifdef  VECT_TAB_RAM
        /* Set the Vector Table base location at 0x20000000 */
        NVIC_SetVectorTable(NVIC_VectTab_RAM, 0x0);
      #else  /*VECT_TAB_FLASH  */
        /* Set the Vector Table base location at 0x08000000 */
        NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x0);
      #endif

函数NVIC_SetVectorTable在“stm32f10x_nvic.c”文件中:

      /*************************************************************************
      *Description   :Sets the vector table location and Offset.
      *Input         :-NVIC_VectTab:specifies if the vector table is in RAM or
      *                 FLASH memory.
      *                 This parameter can be one of the following values:
      *                    -NVIC_VectTab_RAM
      *                    -NVIC_VectTab_FLASH
      *                -Offset:Vector Table base offset field.
      *                        This value must be a multiple of 0x100.
      *************************************************************************/
      void NVIC_SetVectorTable(u32 NVIC_VectTab, u32 Offset)
      {
        /* Check the parameters */
        assert(IS_NVIC_VECTTAB(NVIC_VectTab));
        assert(IS_NVIC_OFFSET(Offset));
        SCB->ExceptionTableOffset = NVIC_VectTab | (Offset & (u32)0x1FFFFF80);
      }

SCB表示系统控制块(System Control Block),其结构定义在文件stm32f10x_map.h中:

      typedef struct
      {
        vu32 CPUID;                   //CPUID Base Register
        vu32 IRQControlState;         //Interrupt Control State Register
        vu32 ExceptionTableOffset;    //Vector Table Offset Register
        vu32 AIRC;                    //Application Interrupt/Reset Control Register
        vu32 SysCtrl;                 //System Control Register
        vu32 ConfigCtrl;              //Configuration Control Register
        vu32 SystemPriority[3];       //System Handlers Priority Register
        vu32 SysHandlerCtrl;          //System Handler Control and State Register
        vu32 ConfigFaultStatus;       //Configurable Fault Status Registers
        vu32 HardFaultStatus;         //Hard Fault Status Register
        vu32 DebugFaultStatus         //Debug Fault Status Register
        vu32 MemoryManageFaultAddr;   //Mem Manage Address Register
        vu32 BusFaultAddr;            //Bus Fault Address Register
      } SCB_TypeDef;

这个中断向量表的地址存放在SCB->ExceptionTableOffset中。这个结构被映射到一个物理地址上,如果是存放在RAM中,地址是0x20000000;如果是存放在FLASH中,地址是0x08000000。只要保证这一处的地址不能被别的程序代码占用就行了。中断向量表里存放的地址就是中断服务函数***_IRQHandler(void)的入口地址,即函数指针。中断被接收之后,处理器通过内部总线接口从向量表中获取地址。向量表复位时指向零,编程控制寄存器可以使向量表重新定位。

中断优先级

中断优先级的概念是针对“中断通道”,当该中断通道的优先级确定后,也就确定了该外围设备的中断优先级,并且该设备所能产生的所有类型的中断,都享有相同的通道中断优先级。而设备本身产生的多个中断的执行顺序,则取决于中断服务程序。

ARM Cortex-M3内核中有两个优先级的概念:抢先(占)式优先级和子优先级,子优先级也称做:响应优先级、副优先级或亚优先级,每个中断源都需要被指定这两种优先级。具有高抢占式优先级的中断可以在具有低抢占式优先级的中断处理过程中被响应,即中断嵌套,或者说高抢占式优先级的中断可以嵌套低抢占式优先级的中断。

既然每个中断源都需要被指定这两种优先级,就需要有相应的寄存器位记录每个中断的优先级;在Cortex-M3中定义了8个比特位用于设置中断源的优先级,这8个比特位可以有8种分配方式,分别如下:

● 所有8位用于指定响应优先级;

● 最高1位用于指定抢占式优先级,最低7位用于指定响应优先级;

● 最高2位用于指定抢占式优先级,最低6位用于指定响应优先级;

● 最高3位用于指定抢占式优先级,最低5位用于指定响应优先级;

● 最高4位用于指定抢占式优先级,最低4位用于指定响应优先级;

● 最高5位用于指定抢占式优先级,最低3位用于指定响应优先级;

● 最高6位用于指定抢占式优先级,最低2位用于指定响应优先级;

● 最高7位用于指定抢占式优先级,最低1位用于指定响应优先级。

Cortex-M3内核允许具有较少中断源时使用较少的寄存器位指定中断源的优先级,因此在STM32系列单片机中,每个中断通道都具备自己的中断优先级控制字节PRI_n(8位,STM32只使用高4位),每4个通道的8位中断优先级控制字(PRI_n)构成一个32位的优先级寄存器(Priority Register)。68个通道的优先级控制字至少构成17个32位的优先级寄存器,它们是NVIC寄存器中的一个重要部分。

这4bit的中断优先级控制位分成2组:从高位开始,前面是定义抢先式优先级的位,后面用于定义子优先级。4bit的组合形式见表4.6。

表4.6 STM32系列单片机中断优先级分配说明表

函数void NVIC_Configuration(void)中,下面的语句就是进行优先级配置:

        /* Configure the NVIC Preemption Priority Bits[配置优先级组] */
        NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0);

可以通过调用STM32固件库中的函数NVIC_PriorityGroupConfig()选择使用哪种优先级分组方式,这个函数的参数有5种,定义在文件“stm32f10x_nvic.h”中:

      /* Preemption Priority Group */
      #define NVIC_PriorityGroup_0  ((u32)0x700)/*0bits for pre-emption priority
                                      4bits for subpriority */
      #define NVIC_PriorityGroup_1  ((u32)0x600)/*1bits for pre-emption priority
                                      3bits for subpriority */
      #define NVIC_PriorityGroup_2  ((u32)0x500)/*2bits for pre-emption priority
                                      2bits for subpriority */
      #define NVIC_PriorityGroup_3  ((u32)0x400)/*3bits for pre-emption priority
                                      1bits for subpriority */
      #define NVIC_PriorityGroup_4  ((u32)0x300)/*4bits for pre-emption priority
                                      0bits for subpriority */

在一个系统中,通常只使用上面5种分配情况的一种,具体采用哪一种,需要在初始化时写入到一个32位寄存器,即应用程序中断与复位控制寄存器(Application Interrupt and Reset Control Register,AIRC)的第[10:8]这3个位中。这3个bit位叫:PRIGROUP(优先级组)。例如,将0x05写到AIRC的[10:8]中,那么也就规定了你的系统中只有4个抢先式优先级,相同的抢先式优先级下还可以有4个不同级别的子优先级。

AIRC寄存器的第[7:0]低8位用于设置优先级,见表4.7。

表4.7 优先级设置说明表

例如,在某系统中使用了TIM2(中断通道28)和EXTI0(中断通道6)两个中断,要求TIM2中断必须优先响应,而且当系统在执行EXTI0中断服务时也必须打断(抢先、嵌套),就必须设置TIM2的抢先优先级比EXTI0的抢先优先级要高(数目小)。假定EXTI0为2号抢先优先级,那么TIM2就必须设置成0或1号抢先优先级。确定了整个系统所具有的优先级个数后,再分别对每个中断通道(设备)进行设置。

2种优先级的确定和嵌套规则如下:

● 高抢先优先级的中断可以打断低抢先优先级的中断服务,构成中断嵌套。抢先式优先级别相同的中断源之间没有嵌套关系。

● 当2(n)个相同抢先优先级的中断出现,它们之间不能构成中断嵌套关系,但STM32首先响应子优先级高的中断。当一个中断到来后,如果STM32正在处理另一个中断,这个后到来的中断就要等到前一个中断处理完之后才能被处理,注意:此时与响应优先级大小无关,即子优先级不可以中断嵌套,只能在当前中断完成之后再响应优先级最高的。

● 当2(n)个相同抢先优先级和相同子优先级的中断出现,STM32首先响应中断通道所对应的中断向量地址低(中断向量表靠前)的那个中断。

也就是说,0号抢先优先级的中断,可以打断任何中断抢先优先级为非0号的中断;1号抢先优先级的中断,可以打断任何中断抢先优先级为2、3、4号的中断;……构成中断嵌套。如果两个中断的抢先优先级相同,谁先出现,就先响应谁,不构成嵌套。如果一起出现(或挂在那里等待),就看它们两个谁的子优先级高了,如果子优先级也相同,就看它们的中断向量位置了。

系统上电复位后,AIRC寄存器中PRIGROUP[10:8]的值为0(编号0),因此,此时系统使用16个抢先优先级,无子优先级。另外由于所有外部中断通道的优先级控制字PRI_n也都是0,所以根据上面的定义可以得出,此时68个外部中断通道的抢先优先级都是0号,没有子优先级的区分。故此时不会发生任何的中断嵌套行为,谁也不能打断当前正在执行的中断服务。当多个中断出现后,则看它们的中断向量地址:地址越低,中断级别越高,STM32优先响应。

注意此时内部中断的抢先优先级也都是0号,由于它们的中断向量地址比外部中断向量地址都低,所以它们的优先级比外部中断通道高,但如果此时正在执行一个外部中断服务,它们也必须排队等待,只是可以插队,当正在执行的中断完成后,它们可以优先得到执行。另外,如果指定的抢占式优先级别或响应优先级别超出了选定的优先级分组所限定的范围,将可能得到意想不到的结果。

总中断控制

在基于ARM Cortex-M3内核的STM32单片机中是通过改变CPU的当前优先级来允许或禁止中断。其中,PRIMASK位用于允许NMI和hard fault异常,其他中断/异常都被屏蔽(当前CPU优先级=0)。FAULTMASK位用于允许NMI,其他所有中断/异常都被屏蔽(当前CPU优先级=-1)。在STM32固件库中(stm32f10x_nvic.c和stm32f10x_nvic.h)定义了四个函数操作PRIMASK位和FAULTMASK位,改变CPU的当前优先级,从而达到控制所有中断的目的。

下面两个函数等效于关闭总中断:

      void NVIC_SETPRIMASK(void);
      void NVIC_SETFAULTMASK(void);

下面两个函数等效于开放总中断:

      void NVIC_RESETPRIMASK(void);
      void NVIC_RESETFAULTMASK(void);

上面两组函数要成对使用,不能交叉使用。常采下面的方法:

      NVIC_SETPRIMASK(); //关闭总中断
      NVIC_RESETPRIMASK();//开放总中断

另一种方法是:

      NVIC_SETFAULTMASK(); //关闭总中断
      NVIC_RESETFAULTMASK();//开放总中断

这四个函数的具体实现在文件“cortexm3_macro.s”中,它定义Cortex-M3的一些宏指令操作,这些宏指令操作可供用户在C中调用,这里不再赘述。

任务六 中断方式测试机器人触觉

中断是在计算机或者单片机执行期间,发生了任何非寻常的或非预期的急需处理事件,这时CPU暂时中断当前正在执行的程序而转去执行相应的事件处理程序。待处理完毕后又返回原来被中断处继续执行的过程。因此,在非寻常或非预期的急需处理事件发生时,常采用中断方法执行。比如,机器人小车遇到障碍物时,采用中断可以提高避障程序执行的实时性。

在“HelloRobot.h”文件的端口初始化配置函数GPIO_Configuration中,我们已将PE4和PE5端口配置成输入模式,并且在中断控制配置函数NVIC_Configuration中将PE4和PE5端口配置成中断模式。下面的程序是采用中断方法,利用PE4和PE5端口依次检测机器人小车的右边和左边触觉(胡须)是否检测到了障碍物。

例程:TestWhiskersWithEINT.c

      #include "stm32f10x_heads.h"
      #include "HelloRobot.h"
      int main(void)
      {
        BSP_Init(); //开发板初始化函数
        USART_Configuration();
        printf("Program Running!\r\n");
        while (1);//等待中断到来
      }

在固件库文件stm32f10x_it.c中,编写中断服务函数代码:

      #include "stm32f10x_it.h"
      #include "stdio.h"
      extern    void delay_nms(unsigned long n);  //延时n ms
      …
      void EXTI4_IRQHandler(void)
      {
        printf("右边胡须检查到障碍 \r\n");
        if(GPIO_ReadOutputDataBit(GPIOB, GPIO_Pin_9)==0)
        GPIO_SetBits(GPIOB, GPIO_Pin_9);
        else
        GPIO_ResetBits(GPIOB, GPIO_Pin_9);
        delay_nms(200);
        EXTI_ClearITPendingBit( EXTI_Line4) ;//中断结束时清中断标志位
      }
      void EXTI9_5_IRQHandler(void)  //中断处理函数PC9
      {
        printf("左边胡须检查到障碍 \r\n");
        if(GPIO_ReadOutputDataBit(GPIOC, GPIO_Pin_13)==0)
        GPIO_SetBits(GPIOC, GPIO_Pin_13);
        else
        GPIO_ResetBits(GPIOC, GPIO_Pin_13);
        delay_nms(200) ;
        EXTI_ClearITPendingBit( EXTI_Line5) ;//中断结束时清中断标志位
      /* BSP_Init()开放了EINT9中断,为了防止干扰信号,加入以下代码 */
      EXTI_ClearITPendingBit( EXTI_Line9) ;//中断结束时清中断标志位
      }

该你了——编写、下载并执行这个程序。

通过串口调试软件检测胡须是否被触动,如同任务二那样。也可以利用两个发光二极管,参考任务一的程序,来指示左右两个胡须是否被触动。程序运行效果如图4.14所示。

图4.14 程序运行效果

判断是否进入中断也可用GPIO_WriteBit(GPIOB, GPIO_Pin_9, (BitAction) (1-GPIO_Read OutputDataBit (GPIOB, GPIO_Pin_9)))语句来控制LED发生交替亮灭。

尝试一下,使用光敏电阻和10kΩ电阻设计一个光引导机器人小车,如图4.15所示。在一个较暗的环境下,通过手电筒引导机器人小车寻光。

图4.15 利用光敏电阻设计的寻光机器人小车