STM32的中断和事件

学习单片机一贯的套路,搞完时钟和GPIO就要折腾中断了。

1. 中断和异常的区别

1.1 中断是指系统停止当前正在运行的程序转而其他服务,可能是程序接收了比自身高优先级的请求,或者是人为设置中断,中断是属于正常现象。
1.2 异常是指由于cpu本身故障、程序故障或者请求服务等引起的错误,异常属于不正常现象。

Cortex-M3内核总共支持256个中断,其中包含16个内核异常和240个外部中断,但是各个芯片产商在设计芯片的时候会对CM3内核的芯片进行精简设计,如STM32F103系列,所搭载的异常响应系统,包含10个系统异常和60个外部中断,用一张表将它们管理起来,编号0~15位系统异常,16以上称为外部中断。

系统异常清单:
STM32的中断和事件_第1张图片
外部中断清单:
STM32的中断和事件_第2张图片

STM32的中断和事件_第3张图片

外部中断信号从核外发出,信号最终要传递到NVIC(嵌套向量中断控制器)。NVIC跟内核紧密耦合,它控制着整个芯片中断的相关功能。

2. 中断优先级

STM32支持两种优先级:抢占优先级和子优先级。所有优先级可编程的中断源都需要指定这两种优先级。
抢占优先级决定是否可以产生中断嵌套,子优先级决定中断响应顺序,若两种优先级一样则看中断源在中断向量表中的偏移量,偏移量小的先响应。
对这句话的解释为:

(1)抢占优先级高的中断源可以中断抢占优先级低的中断处理函数,进而执行高优先级的中断处理函数,执行完毕后再继续执行被中断的低优先级的处理函数。
(2)当两个中断源的抢占优先级相同时,即这两个中断将没有嵌套关系,当一个中断到来后,若此时cpu正在处理另一个中断,则这个后到来的中
断就要等到前一个中断处理函数处理完毕后才能被处理,当这两个中断同时到达,则中断控制器会根据它们的子优先级决定先处理哪个。
(3)如果它们的抢占优先级和子优先级都相等,则根据它们在中断表中的排位顺序决定先处理哪一个。

每个中断源都需要被指定抢占优先级和子优先级,自然需要相应的寄存器来记录。在NVIC中有一个专门处理中断优先级的寄存器NICV_IPRx,用于配置中断源的优先级。IPR的宽度为8Bit,对于CM3内核来说,因为它支持的中断源为256个,那么原则上每个外部中断源可配置的优先级位0~255,数值越小优先级越高。但是因为绝大多数CM3芯片都会精简设计,所以不会使用到全部位,在STM32F103中只使用4Bit。
这里写图片描述
注意,个别系统系统的优先级是固定的,所以它们的中断优先级是不可编程的。

2.1 CM3核的优先级分组方式
CM3中定义了8个Bit用于设置中断源的优先级,这8个Bit可以分配为:

(1)8bit用于响应优先级
(2)最高1位用于指定抢占优先级,最低7位用于执行子优先级
(3)最高2位用于指定抢占优先级,最低6位用于执行子优先级
(4)最高3位用于指定抢占优先级,最低5位用于执行子优先级
(5)最高4位用于指定抢占优先级,最低4位用于执行子优先级
(6)最高5位用于指定抢占优先级,最低3位用于执行子优先级
(7)最高6位用于指定抢占优先级,最低2位用于执行子优先级
(8)最高7位用于指定抢占优先级,最低1位用于执行子优先级

CM3核的优先级分组方式,使用的设置函数

NVIC_SetPriorityGrouping()

在Libraries\CMSIS\CM3\CoreSupport\core_cm3.h文件中实现:

static __INLINE void NVIC_SetPriorityGrouping(uint32_t PriorityGroup)
{
  uint32_t reg_value;
  uint32_t PriorityGroupTmp = (PriorityGroup & 0x07);                         /* only values 0..7 are used          */

  reg_value  =  SCB->AIRCR;                                                   /* read old register configuration    */
  reg_value &= ~(SCB_AIRCR_VECTKEY_Msk | SCB_AIRCR_PRIGROUP_Msk);             /* clear bits to change               */
  reg_value  =  (reg_value                       |
                (0x5FA << SCB_AIRCR_VECTKEY_Pos) | 
                (PriorityGroupTmp << 8));                                     /* Insert write key and priorty group */
  SCB->AIRCR =  reg_value;
}

该函数写在.h文件中,且声明为内联函数(__INLINE),内联函数跟宏替换差不多,可以避免函数调用的压栈出栈等开销。PriorityGroup的取值为0~7。

2.2 STM32的优先级分组方式
CM3核的优先级分组方式是针对256个中断全部用上的场合,但是Cortex-M3也允许在具有较少中断源时用较少的寄存器位指定中断源的优先级。STM32并没有使用Cortex-M3内核嵌套向量中断全套东西,而是使用了它的一部分:

(1)STM32F103系列有16个内核异常和60个外部中断
(2)STM32F107系列有16个内核异常和68个外部中断

STM32的优先级分组使用标准库函数

NVIC_PriorityGroupConfig()

该函数在Libraries\STM32F10x_StdPeriph_Driver\src\misc.c中实现:

void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup)
{
  /* Check the parameters */
  assert_param(IS_NVIC_PRIORITY_GROUP(NVIC_PriorityGroup));

  /* Set the PRIGROUP[10:8] bits according to NVIC_PriorityGroup value */
  SCB->AIRCR = AIRCR_VECTKEY_MASK | NVIC_PriorityGroup;
}

可见,这个函数也是在设置SCB->AIRCR寄存器,只是这里的取值为:

(1)NVIC_PriorityGroup_0:0bit for 抢占优先级,4bit for 子优先级,即有24次方个子优先级
(2)NVIC_PriorityGroup_1:1bit for 抢占优先级,3bit for 子优先级,即有23次方个子优先级
(3)NVIC_PriorityGroup_2:2bit for 抢占优先级,2bit for 子优先级,即有22次方个子优先级
(4)NVIC_PriorityGroup_3:3bit for 抢占优先级,1bit for 子优先级,即有21次方个子优先级
(5)NVIC_PriorityGroup_4:4bit for 抢占优先级,0bit for 子优先级,即有20次方个子优先级

3. NVIC操作相关函数

NVIC的描述结构体在core_cm3.h中:

typedef struct
{
  __IO uint32_t ISER[8];        /* 中断使能寄存器(Interrupt Set Enable Register),Offset: 0x000 */
       uint32_t RESERVED0[24];                                   
  __IO uint32_t ICER[8];        /* 中断清除寄存器(Interrupt Clear Enable Register),Offset: 0x080 */
       uint32_t RSERVED1[24];                                    
  __IO uint32_t ISPR[8];        /* 中断使能挂起寄存器(Interrupt Set Pending Register),Offset: 0x100 */
       uint32_t RESERVED2[24];                                   
  __IO uint32_t ICPR[8];        /* 中断清除挂起寄存器(Interrupt Clear Pending Register),Offset: 0x180 */
       uint32_t RESERVED3[24];                                   
  __IO uint32_t IABR[8];        /* 中断有效位寄存器(Interrupt Active bit Register ),Offset: 0x200 */
       uint32_t RESERVED4[56];                                   
  __IO uint8_t  IP[240];        /* 中断优先级寄存器(Interrupt Priority Register),Offset: 0x300 (8Bit wide) */
       uint32_t RESERVED5[644];                                  
  __O  uint32_t STIR;           /* 软中断触发寄存器(Software Trigger Interrupt Register),Offset: 0xE00 */
}  NVIC_Type;

编程中常用的是ISER、ICER和IP这三个寄存器。ISER和ICER分别用于enable、disable中断,IP用于控制中断优先级。
同在core_cm3.h中,定义了对结构体成员的操作函数,这是针对Cortex-M3内核芯片都适用的函数:

(1)设置优先级分组寄存器: NVIC_SetPriorityGrouping(uint32_t PriorityGroup)
(2)从NVIC中断控制器得到优先级分组设置值: NVIC_GetPriorityGrouping(void)
(3)使能中断: NVIC_EnableIRQ(IRQn_Type IRQn) 
(4)失能中断: NVIC_DisableIRQ(IRQn_Type IRQn) 
(5)获取挂起中断编号: NVIC_GetPendingIRQ(IRQn_Type IRQn) 
(6)设置中断挂其位: NVIC_SetPendingIRQ(IRQn_Type IRQn) 
(7)清除中断挂起位: NVIC_ClearPendingIRQ(IRQn_Type IRQn) 
(8)NVIC_GetActive(IRQn_Type IRQn)
(9)设置中断源的中断优先级: NVIC_SetPriority(IRQn_Type IRQn, uint32_t priority) 
(10)获取中断源的中断优先级: NVIC_GetPriority(IRQn_Type IRQn) 
(11)编码一个中断的优先级,不知道干嘛: NVIC_EncodePriority (uint32_t PriorityGroup, uint32_t PreemptPriority, uint32_t SubPriority)
(12)解码一个中断的优先级,不知道干嘛: NVIC_DecodePriority (uint32_t Priority, uint32_t PriorityGroup, uint32_t* pPreemptPriority, uint32_t* pSubPriority) 
(13)系统复位: NVIC_SystemReset(void)

在外设库misc.h定义了针对STM32的NVIC的初始化描述结构体:

typedef struct
{
  uint8_t NVIC_IRQChannel;                    /* 中断源 */
  uint8_t NVIC_IRQChannelPreemptionPriority;  /* 抢占优先级 */
  uint8_t NVIC_IRQChannelSubPriority;         /* 子优先级 */
  FunctionalState NVIC_IRQChannelCmd;         /* 中断使能或者失能 */   
} NVIC_InitTypeDef;

misc.c也定义了针对STM32的NVIC的操作函数:

(1)设置优先级分组寄存器: NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup)
(2)初始化NVIC_InitTypeDef类的结构体: NVIC_Init(NVIC_InitTypeDef* NVIC_InitStruct)
(3)设置中断向量表位置和偏移: NVIC_SetVectorTable(uint32_t NVIC_VectTab, uint32_t Offset)
系统可以选择从SRAM启动,也可以选择从flash启动,对应的启动地址会映射到0地址处,而中断向量表是要被放在0地址处的,所以要将中      断向量表放在SRAM/flash的起始位置。函数参数一的取值为NVIC_VectTab_RAM/NVIC_VectTab_FLASH,参数二的取值必须是0x200的整数倍(STM32就是这么规定的)。
(4)选择进入低功耗模式的条件: NVIC_SystemLPConfig(uint8_t LowPowerMode, FunctionalState NewState) 
参数一取值NVIC_LP_SEVONPEND/NVIC_LP_SLEEPDEEP/NVIC_LP_SLEEPONEXIT,参数二取值ENABLE/DISABLE

4. EXTI–外部中断和事件控制器

EXTI有20个中断/事件线,每个GPIO都可以被设置为中断/事件的输入线,占用EXTI0~EXTI15,还有另外4根用于特定的外设事件的EXTI16~EXTI19:
STM32的中断和事件_第4张图片
注意,EXTIx与GPIOx的对应关系,EXTI0只能和P[x]0绑定(x = A、B、C、D…),
STM32的中断和事件_第5张图片
实现绑定操作的函数声明位于标准库Libraries\STM32F10x_StdPeriph_Driver\inc\stm32f10x_gpio.h中:

GPIO_EXTILineConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource)
参数一GPIO_PortSource的取值为GPIO_PortSourceGPIOx (x = A..G),
参数二GPIO_PinSource的取值为GPIO_PinSourcex(x = 0..15)

这个函数在一般初始化EXTI寄存器时候调用。因为外部中断是GPIO引脚的复用功能,所以同时要开启GPIO复用功能的时钟:

RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE)

5. EXTI描述结构体的初始化

EXTI描述结构体声明在标准外设库Libraries\STM32F10x_StdPeriph_Driver\inc\stm32f10x_exti.h中:

typedef struct
{
  uint32_t EXTI_Line;               /* 中断事件线 */
  EXTIMode_TypeDef EXTI_Mode;       /* EXTI模式,事件/中断 */
  EXTITrigger_TypeDef EXTI_Trigger; /* 触发类型 */
  FunctionalState EXTI_LineCmd;     /* EXTI使能 */ 
}EXTI_InitTypeDef;

与EXTI操作相关的函数有:

(1)去除EXTI_InitTypeDef结构体的初始化:EXTI_DeInit(void)
(2)初始化EXTI_InitTypeDef结构体: EXTI_Init(EXTI_InitTypeDef* EXTI_InitStruct) 
(3)默认初始化:EXTI_StructInit(EXTI_InitTypeDef* EXTI_InitStruct) 
(4)EXTI_GenerateSWInterrupt(uint32_t EXTI_Line) 产生一个软件中断
(5)获取产生中断的标志:EXTI_GetFlagStatus(uint32_t EXTI_Line) 
                   EXTI_GetITStatus(uint32_t EXTI_Line)
(6)清除中断产生标志:EXTI_ClearFlag(uint32_t EXTI_Line) 
                  EXTI_ClearITPendingBit(uint32_t EXTI_Line)

获取/清除产生中断的标志的实现是一样的,但是为什么要分成两组函数?
也许这是STM32标准外设库设计者出自于为兼容性考虑吧。有的ARM芯片的中断体系分为两层,也就是说中断信号要抵达NVIC需要两层筛选,同理清除中断标志也需要清除两层。但是在CM3核的ARM只设计了一层,STM32为了兼容其他芯片,依旧还是将函数设计成两层,只不过这两层的实现体是一致的。

6. 编程实现按键中断

下来编程操作STM32的中断。未能免俗,还是以按键中断为例。
实验采用正点原子miniSTM32硬件平台,
STM32的中断和事件_第6张图片

按键KEY0(PC5)和KEY1(PA15)的原理图:
STM32的中断和事件_第7张图片
STM32的中断和事件_第8张图片
这里写图片描述

LED0(PA8)和LED1(PD2)的原理图:
STM32的中断和事件_第9张图片
STM32的中断和事件_第10张图片

实验实现按键产生外部中断,在中断处理函数中实现反向控制LED灯。编程的要点为:

(1)初始化用来产生中断信号的GPIO
(2)初始化中断/事件控制器EXTI
(3)配置NVIC
(4)编写中断服务函数

EXTI用于设置中断源的触发方式、中断/事件类型和具体是哪一个中断源。
中断信号产生后最终传递到NVIC,NVIC控制中断源优先级、中断线通道等,以便比对中断信号、根据优先级调用中断服务函数。
实验采用MDK4集成开发环境,工程的目录结构如下:
STM32的中断和事件_第11张图片

exti_led.h声明实验中用到的函数:

#ifndef __EXTI_LED_H__
#define __EXTI_LED_H__

#include "stm32f10x_conf.h"

void TimeDelay(void);
void Led_CfgInit(void);
void Exti_CfgInit(void);
void NVIC_CfgInit(void);
void Key_CfgInit(void);

#endif /* __EXTI_LED_H__ */

mian.c实现各个功能模块:
(1)初始化外接LED的GPIO引脚

//PA8-->LED0,PD2-->LED1
void Led_CfgInit(void)
{
    GPIO_InitTypeDef GPIO_InitTypeStu;

    //开启PA和PD端口的时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOD, ENABLE);

    //初始化PA8引脚为推挽输出
    GPIO_InitTypeStu.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitTypeStu.GPIO_Pin = GPIO_Pin_8;
    GPIO_InitTypeStu.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitTypeStu);
    GPIO_SetBits(GPIOA,GPIO_Pin_8);         //LED0初始状态为灭

    //初始化PD2引脚为推挽输出
    GPIO_InitTypeStu.GPIO_Pin = GPIO_Pin_2;
    GPIO_Init(GPIOD, &GPIO_InitTypeStu);
    GPIO_SetBits(GPIOD,GPIO_Pin_2);         //LED1初始状态为灭
}

(2)初始化外接按键的引脚

void Key_CfgInit(void)
{
    GPIO_InitTypeDef GPIO_InitTypeStu;

    //开启PC和PA的时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOA, ENABLE);

    //初始化PC5为上拉输入
    GPIO_InitTypeStu.GPIO_Mode = GPIO_Mode_IPU; // GPIO_Mode_IN_FLOATING;   
    GPIO_InitTypeStu.GPIO_Pin = GPIO_Pin_5;
    GPIO_InitTypeStu.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOC, &GPIO_InitTypeStu);

    //初始化PA15为上拉输入
    GPIO_InitTypeStu.GPIO_Mode = GPIO_Mode_IPU; // GPIO_Mode_IN_FLOATING;   
    GPIO_InitTypeStu.GPIO_Pin = GPIO_Pin_15;
    GPIO_InitTypeStu.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitTypeStu);
}

一开始我是设置PC5和PA15为浮空输入的,因为只需要考虑到按键按下是低电平,但是实验表明,浮空输入并不能确定引脚状态,即使在按键按下以后也不能触发中断,所以还是要将它们设置为上拉/下拉输入。

(3)初始化EXTI

void Exti_CfgInit(void)
{
    EXTI_InitTypeDef EXTI_InitStu;  
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);

    //EXTI5
    GPIO_EXTILineConfig(GPIO_PortSourceGPIOC, GPIO_PinSource5); //将具体GPIO和外部中断事件线绑定
    EXTI_InitStu.EXTI_Line = EXTI_Line5;
    EXTI_InitStu.EXTI_Mode = EXTI_Mode_Interrupt;               //中断模式
    EXTI_InitStu.EXTI_Trigger = EXTI_Trigger_Rising;            //上升沿触发
    EXTI_InitStu.EXTI_LineCmd = ENABLE;
    EXTI_Init(&EXTI_InitStu);

    //EXTI15
    GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource15);    
    EXTI_InitStu.EXTI_Line = EXTI_Line15;
    EXTI_InitStu.EXTI_LineCmd = ENABLE;
    EXTI_Init(&EXTI_InitStu);
}

GPIO用于传输外部中断信号属于GPIO复用部分的功能,所以需要打开RCC_APB2Periph_AFIO的时钟。

(4)初始化NVIC

void NVIC_CfgInit(void)
{
    NVIC_InitTypeDef NVIC_InitTypeStu;

    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);         //设置中断分组
    NVIC_InitTypeStu.NVIC_IRQChannel = EXTI9_5_IRQn;        //外部中断线EXTI5属于共享中断
    NVIC_InitTypeStu.NVIC_IRQChannelPreemptionPriority = 1; //抢占优先级
    NVIC_InitTypeStu.NVIC_IRQChannelSubPriority = 1;        //子优先级
    NVIC_InitTypeStu.NVIC_IRQChannelCmd = ENABLE;   
    NVIC_Init(&NVIC_InitTypeStu);

    NVIC_InitTypeStu.NVIC_IRQChannel = EXTI15_10_IRQn;      //外部中断线EXTI15属于共享中断
    NVIC_InitTypeStu.NVIC_IRQChannelSubPriority = 2;
    NVIC_Init(&NVIC_InitTypeStu);
}

(5)延时函数,这里只是简单延时,并没有精准计算

void TimeDelay(void)
{
    int i, j;

    for (i = 0; i < 100; i++)
        for (j = 0; j < 1000; j++);
}

(6)main函数

int main(void)
{
    Led_CfgInit();
    Key_CfgInit();
    Exti_CfgInit();
    NVIC_CfgInit();

    while(1);
    return 0;
}

当用户按下KEY0时,会进入EXTI9_5_IRQHandler()处理函数中,按下KEY1则进入EXTI15_10_IRQHandler()处理函数。这两个函数名是在启动文件写的,详细可参考文章http://blog.csdn.net/qq_29344757/article/details/74932235。处理函数的实现如下:

void EXTI9_5_IRQHandler(void)
{   
    //int i = 50;
    TimeDelay();    //延时去抖动

    if (!GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_5) && EXTI_GetFlagStatus(EXTI_Line5))
    {
        GPIO_WriteBit(GPIOA, GPIO_Pin_8, ((BitAction)!GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_8)));

        //清中断
        //EXTI_ClearFlag(EXTI_Line5);
        EXTI_ClearITPendingBit(EXTI_Line5);
    }
}

void EXTI15_10_IRQHandler(void)
{   
    TimeDelay();    //延时去抖动

    if (!GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_15) && EXTI_GetFlagStatus(EXTI_Line15))
    {
        GPIO_WriteBit(GPIOD, GPIO_Pin_2, ((BitAction)!GPIO_ReadInputDataBit(GPIOD, GPIO_Pin_2)));

        //清中断
        //EXTI_ClearFlag(EXTI_Line15);
        EXTI_ClearITPendingBit(EXTI_Line15);
    }
}

函数可在用户自定义的文件实现,也可以在标准库提供的工程模板文件stm32f10x_it.c中实现。由于是共享中断,所以需要调用EXTI_GetFlagStatus()函数来判断是否为目的中断源。其实在其他很多非共享中断的场合也有使用EXTI_GetFlagStatus()函数判断目的中断源,无关紧要了。

STM32的中断系统非常强大,每一个外设都可以产生中断。关于STM32中断/事件部分的学习就先告一段落,详细内容可参考STM32F10X-中文参考手册》、《 Cortex-M3 内核编程手册》的4.3章节。下一个学习任务–系统定时器SysTick。

你可能感兴趣的:(STM32单片机)