17. EXTI—外部中断/事件控制器

本章参考资料:《STM32F4xx中文参考手册》系统配置控制器以及中断和事件章节。

上一章我们已经详细介绍了NVIC,对STM32F4xx中断管理系统有个全局的了解,我们这章的内容是NVIC的实例应用,也是STM32F4xx控制器非常重要的一个资源。 学习本章时,配合《STM32F4xx中文参考手册》系统配置控制器以及中断和事件章节一起阅读,效果会更佳,特别是涉及到寄存器说明的部分。

特别说明,本书内容是以STM32F42x系列控制器资源讲解。

17.1. EXTI简介

外部中断/事件控制器(EXTI)管理了控制器的23个中断/事件线。每个中断/事件线都对应有一个边沿检测器,可以实现输入信号的上升沿检测和下降沿的检测。 EXTI可以实现对每个中断/事件线进行单独配置,可以单独配置为中断或者事件,以及触发事件的属性。

17.2. EXTI功能框图

EXTI的功能框图包含了EXTI最核心内容,掌握了功能框图,对EXTI就有一个整体的把握,在编程时就思路就非常清晰。见 EXTI功能框图

在图 EXTI功能框图 可以看到很多在信号线上打一个斜杠并标注“23”字样, 这个表示在控制器内部类似的信号线路有23个, 这与EXTI总共有23个中断/事件线是吻合的。所以我们只要明白其中一个的原理,那其他22个线路原理也就知道了。

EXTI功能框图

EXTI可分为两大部分功能,一个是产生中断,另一个是产生事件,这两个功能从硬件上就有所不同。

首先我们来看图 EXTI功能框图 中红色虚线指示的电路流程。 它是一个产生中断的线路,最终信号流入到NVIC控制器内。

编号1是输入线,EXTI控制器有23个中断/事件输入线,这些输入线可以通过寄存器设置为任意一个GPIO,也可以是一些外设的事件, 这部分内容我们将在后面专门讲解。输入线一般是存在电平变化的信号。

编号2是一个边沿检测电路,它会根据上升沿触发选择寄存器(EXTI_RTSR)和下降沿触发选择寄存器(EXTI_FTSR)对应位的设置来控制信号触发。 边沿检测电路以输入线作为信号输入端,如果检测到有边沿跳变就输出有效信号1给编号3电路,否则输出无效信号0。而EXTI_RTSR和EXTI_FTSR两个寄存器可以控制需要检测哪些类型的电平跳变过程,可以是只有上升沿触发、只有下降沿触发或者上升沿和下降沿都触发。

编号3电路实际就是一个或门电路,它的一个输入来自编号2电路,另外一输入来自软件中断事件寄存器(EXTI_SWIER)。EXTI_SWIER允许我们通过程序控制就可以启动中断/事件线, 这在某些地方非常有用。我们知道或门的作用就是有1就为1,所以这两个输入随便一个有有效信号1就可以输出1给编号4和编号6电路。

编号4电路是一个与门电路,它的一个输入编号3电路,另外一个输入来自中断屏蔽寄存器(EXTI_IMR)。与门电路要求输入都为1才输出1, 导致的结果如果EXTI_IMR设置为0时,那不管编号3电路的输出信号是1还是0,最终编号4电路输出的信号都为0;如果EXTI_IMR设置为1时, 最终编号4电路输出的信号才由编号3电路的输出信号决定,这样我们可以简单的控制EXTI_IMR来实现是否产生中断的目的。 编号4电路输出的信号会被保存到挂起寄存器(EXTI_PR)内,如果确定编号4电路输出为1就会把EXTI_PR对应位置1。

编号5是将EXTI_PR寄存器内容输出到NVIC内,从而实现系统中断事件控制。

接下来我们来看看绿色虚线指示的电路流程。它是一个产生事件的线路,最终输出一个脉冲信号。

产生事件线路是在编号3电路之后与中断线路有所不同,之前电路都是共用的。编号6电路是一个与门,它的一个输入编号3电路, 另外一个输入来自事件屏蔽寄存器(EXTI_EMR)。如果EXTI_EMR设置为0时,那不管编号3电路的输出信号是1还是0,最终编号6电路输出的信号都为0;如果EXTI_EMR设置为1时, 最终编号6电路输出的信号才由编号3电路的输出信号决定,这样我们可以简单的控制EXTI_EMR来实现是否产生事件的目的。

编号7是一个脉冲发生器电路,当它的输入端,即编号6电路的输出端,是一个有效信号1时就会产生一个脉冲;如果输入端是无效信号就不会输出脉冲。

编号8是一个脉冲信号,就是产生事件的线路最终的产物,这个脉冲信号可以给其他外设电路使用,比如定时器TIM、模拟数字转换器ADC等等。

产生中断线路目的是把输入信号输入到NVIC,进一步会运行中断服务函数,实现功能,这样是软件级的。 而产生事件线路目的就是传输一个脉冲信号给其他外设使用,并且是电路级别的信号传输,属于硬件级的。

另外,EXTI是在APB2总线上的,在编程时候需要注意到这点。

17.3. 中断/事件线

EXTI有23个中断/事件线,每个GPIO都可以被设置为输入线,占用EXTI0至EXTI15,还有另外七根用于特定的外设事件,见表 EXTI中断/事件线

七根特定外设中断/事件线由外设触发,具体用法参考《STM32F4xx中文参考手册》中对外设的具体说明。

EXTI中断/事件线

中断/事件线

输入源

EXTI0

PX0(X可为A,B,C,D,E,F,G,H,I)

EXTI1

PX1(X可为A,B,C,D,E,F,G,H,I)

EXTI2

PX2(X可为A,B,C,D,E,F,G,H,I)

EXTI3

PX3(X可为A,B,C,D,E,F,G,H,I)

EXTI4

PX4(X可为A,B,C,D,E,F,G,H,I)

EXTI5

PX5(X可为A,B,C,D,E,F,G,H,I)

EXTI6

PX6(X可为A,B,C,D,E,F,G,H,I)

EXTI7

PX7(X可为A,B,C,D,E,F,G,H,I)

EXTI8

PX8(X可为A,B,C,D,E,F,G,H,I)

EXTI9

PX9(X可为A,B,C,D,E,F,G,H,I)

EXTI10

PX10(X可为A,B,C,D,E,F,G,H,I)

EXTI11

PX11(X可为A,B,C,D,E,F,G,H,I)

EXTI12

PX12(X可为A,B,C,D,E,F,G,H,I)

EXTI13

PX13(X可为A,B,C,D,E,F,G,H,I)

EXTI14

PX14(X可为A,B,C,D,E,F,G,H,I)

EXTI15

PX15(X可为A,B,C,D,E,F,G,H)

EXTI16

可编程电压检测器(PVD)输出

EXTI17

RTC闹钟事件

EXTI18

USB OTG FS唤醒事件

EXTI19

以太网唤醒事件

EXTI20

USB OTG HS(在FS中配置)唤醒事件

EXTI21

RTC入侵和时间戳事件

EXTI22

RTC唤醒事件

EXTI0至EXTI15用于GPIO,通过编程控制可以实现任意一个GPIO作为EXTI的输入源。由上表可知, EXTI0可以通过SYSCFG外部中断配置寄存器1(SYSCFG_EXTICR1)的EXTI0[3:0]位选择配置为PA0、PB0、PC0、PD0、PE0、PF0、PG0、PH0或者PI0, 见图 EXTI0输入源选择 。其他EXTI线(EXTI中断/事件线)使用配置都是类似的。

EXTI0输入源选择

17.4. EXTI初始化结构体详解

标准库函数对每个外设都建立了一个初始化结构体,比如EXTI_InitTypeDef,结构体成员用于设置外设工作参数,并由外设初始化配置函数, 比如EXTI_Init()调用,这些设定参数将会设置外设相应的寄存器,达到配置外设工作环境的目的。

初始化结构体和初始化库函数配合使用是标准库精髓所在,理解了初始化结构体每个成员意义基本上就可以对该外设运用自如了。 初始化结构体定义在stm32f4xx_exti.h文件中,初始化库函数定义在stm32f4xx_exti.c文件中,编程时我们可以结合这两个文件内注释使用。

代码清单:EXTI-1 EXTI初始化结构体
1
2
3
4
5
6
typedef struct {
    uint32_t EXTI_Line;                 // 中断/事件线
    EXTIMode_TypeDef EXTI_Mode;         // EXTI模式
    EXTITrigger_TypeDef EXTI_Trigger;   // 触发事件
    FunctionalState EXTI_LineCmd;       // EXTI控制
} EXTI_InitTypeDef;

1) EXTI_Line:EXTI中断/事件线选择, 可选EXTI0至EXTI22,可参考表 18‑1选择。

2) EXTI_Mode:EXTI模式选择, 可选为产生中断(EXTI_Mode_Interrupt)或者产生事件(EXTI_Mode_Event)。

3) EXTI_Trigger:EXTI边沿触发事件, 可选上升沿触发(EXTI_Trigger_Rising)、下降沿触发( EXTI_Trigger_Falling)或者上升沿和下降沿都触发( EXTI_Trigger_Rising_Falling)。

4) EXTI_LineCmd:控制是否使能EXTI线, 可选使能EXTI线(ENABLE)或禁用(DISABLE)。

17.5. 外部中断控制实验

中断在嵌入式应用中占有非常重要的地位,几乎每个控制器都有中断功能。中断对保证紧急事件得到第一时间处理是非常重要的

我们设计使用外接的按键来作为触发源,使得控制器产生中断,并在中断服务函数中实现控制RGB彩灯的任务。

17.5.1. 硬件设计

轻触按键在按下时会使得引脚接通,通过电路设计可以使得按下时产生电平变化,见图 按键电路设计

按键电路设计

17.5.2. 软件设计

这里只讲解核心的部分代码,有些变量的设置,头文件的包含等并没有涉及到,完整的代码请参考本章配套的工程。 我们创建了两个文件:bsp_exti.c和bsp_exti.h文件用来存放EXTI驱动程序及相关宏定义,中断服务函数放在stm32f4xx_it.h文件中。

17.5.2.1. 编程要点

  1. 初始化RGB彩灯的GPIO;

  2. 开启按键GPIO时钟和SYSCFG时钟;

  3. 配置NVIC;

  4. 配置按键GPIO为输入模式;

  5. 将按键GPIO连接到EXTI源输入;

  6. 配置按键EXTI中断/事件线;

  7. 编写EXTI中断服务函数。

17.5.2.2. 软件分析

按键和EXTI宏定义

代码清单:EXTI-2 按键和EXTI 宏定义
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
//引脚定义
/*******************************************************/
#define KEY1_INT_GPIO_PORT                GPIOA
#define KEY1_INT_GPIO_CLK                 RCC_AHB1Periph_GPIOA
#define KEY1_INT_GPIO_PIN                 GPIO_Pin_0
#define KEY1_INT_EXTI_PORTSOURCE          EXTI_PortSourceGPIOA
#define KEY1_INT_EXTI_PINSOURCE           EXTI_PinSource0
#define KEY1_INT_EXTI_LINE                EXTI_Line0
#define KEY1_INT_EXTI_IRQ                 EXTI0_IRQn

#define KEY1_IRQHandler                   EXTI0_IRQHandler

#define KEY2_INT_GPIO_PORT                GPIOC
#define KEY2_INT_GPIO_CLK                 RCC_AHB1Periph_GPIOC
#define KEY2_INT_GPIO_PIN                 GPIO_Pin_13
#define KEY2_INT_EXTI_PORTSOURCE          EXTI_PortSourceGPIOC
#define KEY2_INT_EXTI_PINSOURCE           EXTI_PinSource13
#define KEY2_INT_EXTI_LINE                EXTI_Line13
#define KEY2_INT_EXTI_IRQ                 EXTI15_10_IRQn

#define KEY2_IRQHandler                   EXTI15_10_IRQHandler

使用宏定义方法指定与电路设计相关配置,这对于程序移植或升级非常有用的。

嵌套向量中断控制器NVIC配置

代码清单:EXTI-3 NVIC配置
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
static void NVIC_Configuration(void)
{
    NVIC_InitTypeDef NVIC_InitStructure;

    /* 配置NVIC为优先级组1 */
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);

    /* 配置中断源:按键1 */
    NVIC_InitStructure.NVIC_IRQChannel = KEY1_INT_EXTI_IRQ;
    /* 配置抢占优先级:1 */
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
    /* 配置子优先级:1 */
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
    /* 使能中断通道 */
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);

    /* 配置中断源:按键2,其他使用上面相关配置 */
    NVIC_InitStructure.NVIC_IRQChannel = KEY2_INT_EXTI_IRQ;
    NVIC_Init(&NVIC_InitStructure);
}

有关NVIC配置问题可参考上一章节内容,这里不做过多解释。

EXTI中断配置

代码清单:EXTI-4 EXTI中断配置
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
void EXTI_Key_Config(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    EXTI_InitTypeDef EXTI_InitStructure;

    /*开启按键GPIO口的时钟*/
    RCC_AHB1PeriphClockCmd(KEY1_INT_GPIO_CLK|KEY2_INT_GPIO_CLK ,ENABLE);

    /* 使能 SYSCFG 时钟 ,使用GPIO外部中断时必须使能SYSCFG时钟*/
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE);

    /* 配置 NVIC */
    NVIC_Configuration();

    /* 选择按键1的引脚 */
    GPIO_InitStructure.GPIO_Pin = KEY1_INT_GPIO_PIN;
    /* 设置引脚为输入模式 */
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN;
    /* 设置引脚不上拉也不下拉 */
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
    /* 使用上面的结构体初始化按键 */
    GPIO_Init(KEY1_INT_GPIO_PORT, &GPIO_InitStructure);

    /* 连接 EXTI 中断源 到key1引脚 */
    SYSCFG_EXTILineConfig(KEY1_INT_EXTI_PORTSOURCE,
                        KEY1_INT_EXTI_PINSOURCE);

    /* 选择 EXTI 中断源 */
    EXTI_InitStructure.EXTI_Line = KEY1_INT_EXTI_LINE;
    /* 中断模式 */
    EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
    /* 下降沿触发 */
    EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising;
    /* 使能中断/事件线 */
    EXTI_InitStructure.EXTI_LineCmd = ENABLE;
    EXTI_Init(&EXTI_InitStructure);

    /* 选择按键2的引脚 */
    GPIO_InitStructure.GPIO_Pin = KEY2_INT_GPIO_PIN;
    /* 其他配置与上面相同 */
    GPIO_Init(KEY2_INT_GPIO_PORT, &GPIO_InitStructure);

    /* 连接 EXTI 中断源 到key2 引脚 */
    SYSCFG_EXTILineConfig(KEY2_INT_EXTI_PORTSOURCE,
                        KEY2_INT_EXTI_PINSOURCE);

    /* 选择 EXTI 中断源 */
    EXTI_InitStructure.EXTI_Line = KEY2_INT_EXTI_LINE;
    EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
    /* 上升沿触发 */
    EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
    EXTI_InitStructure.EXTI_LineCmd = ENABLE;
    EXTI_Init(&EXTI_InitStructure);
}

首先,使用GPIO_InitTypeDef和EXTI_InitTypeDef结构体定义两个用于GPIO和EXTI初始化配置的变量,关于这两个结构体前面都已经做了详细的讲解。

使用GPIO之前必须开启GPIO端口的时钟;用到EXTI必须开启SYSCFG时钟。

调用NVIC_Configuration函数完成对按键1、按键2优先级配置并使能中断通道。

作为中断/时间输入线把GPIO配置为输入模式,这里不使用上拉或下拉,有外部电路完全决定引脚的状态。

SYSCFG_EXTILineConfig函数用来指定中断/事件线的输入源,它实际是设定SYSCFG外部中断配置寄存器的值,该函数接收两个参数, 第一个参数指定GPIO端口源,第二个参数为选择对应GPIO引脚源编号。

我们的目的是产生中断,执行中断服务函数,EXTI选择中断模式,按键1使用下降沿触发方式,并使能EXTI线。

按键2基本上采用与按键1相关参数配置,只是改为上升沿触发方式。

EXTI中断服务函数

代码清单:EXTI-5 EXTI中断服务函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
void KEY1_IRQHandler(void)
{
    //确保是否产生了EXTI Line中断
    if (EXTI_GetITStatus(KEY1_INT_EXTI_LINE) != RESET) {
        // LED1 取反
        LED1_TOGGLE;
        //清除中断标志位
        EXTI_ClearITPendingBit(KEY1_INT_EXTI_LINE);
    }
}

void KEY2_IRQHandler(void)
{
    //确保是否产生了EXTI Line中断
    if (EXTI_GetITStatus(KEY2_INT_EXTI_LINE) != RESET) {
        // LED2 取反
        LED2_TOGGLE;
        //清除中断标志位
        EXTI_ClearITPendingBit(KEY2_INT_EXTI_LINE);
    }
}

当中断发生时,对应的中断服务函数就会被执行,我们可以在中断服务函数实现一些控制。

一般为确保中断确实发生,我们会在中断服务函数调用中断标志位状态读取函数读取外设中断标志位并判断标志位状态。

EXTI_GetITStatus函数用来获取EXTI的中断标志位状态,如果EXTI线有中断发生函数返回“SET”否则返回“RESET”。实际上, EXTI_GetITStatus函数是通过读取EXTI_PR寄存器值来判断EXTI线状态的。

按键1的中断服务函数我们让LED1翻转其状态,按键2的中断服务函数我们让LED2翻转其状态。执行任务后需要调用EXTI_ClearITPendingBit函数清除EXTI线的中断标志位。

主函数

代码清单:EXTI-6 主函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
int main(void)
{
    /* LED 端口初始化 */
    LED_GPIO_Config();

    /* 初始化EXTI中断,按下按键会触发中断,
    *  触发中断会进入stm32f4xx_it.c文件中的函数
    *  KEY1_IRQHandler和KEY2_IRQHandler,处理中断,反转LED灯。
    */
    EXTI_Key_Config();

    /* 等待中断,由于使用中断方式,CPU不用轮询按键 */
    while (1) {
    }
}

主函数非常简单,只有两个任务函数。LED_GPIO_Config函数定义在bsp_led.c文件内,完成RGB彩灯的GPIO初始化配置。 EXTI_Key_Config函数完成两个按键的GPIO和EXTI配置。

17.5.3. 下载验证

保证开发板相关硬件连接正确,把编译好的程序下载到开发板。此时RGB彩色灯是暗的,如果我们按下开发板上的按键1, RGB彩灯变亮,再按下按键1,RGB彩灯又变暗;如果我们按下开发板上的按键2并弹开,RGB彩灯变亮,再按下开发板上的KEY2并弹开,RGB彩灯又变暗。

17.6. 补充说明

关于使用外部按键中断的理解补充,按教程与参考手册的中断/事件线与输入源的表格看,可以使用EXTI0至15线路对应到使用16个GPIO做外部中断, 需要注意的是这16个线路并非都有单独的中断源,从stm32f4xx.h查看对应芯片型号有EXTI0_IRQn、EXTI1_IRQn、EXTI2_IRQn、EXTI3_IRQn、EXTI4_IRQn、EXTI9_5_IRQn、EXTI15_10_IRQn, 再查看启动文件那边定义的中断函数同样也是EXTI0_IRQHandler、EXTI1_IRQHandler、EXTI2_IRQHandler、EXTI3_IRQHandler、EXTI4_IRQHandler、EXTI9_5_IRQHandler、EXTI15_10_IRQHandler。

除了前面4个线路有单独的中断函数,后面5至9和10至15线路使用 复用 的思想思考,区分出什么是可以唯一标识的,什么是复用的,EXTI_Lines在寄存器中都是一一对应状态标位,中断函数复用, 因此在EXTI9_5_IRQHandler和EXTI15_10_IRQHandler的中断函数里面使用多次EXTI_GetITStatus函数来判断出线路。

这里再次强调一下关于STM32中断应用概览章节的补充说明内容, NVIC_PriorityGroupConfig是整个程序中只需要设置一次 ,当设置好了中断优先级分组,其他各种外设对应的中断向量的中断优先级既是基于目前设置分组来解读。

在其他例程中很多将NVIC_PriorityGroupConfig写在了每个外设自己的中断配置函数里面,有些可能多个外设配置函数下重复了NVIC_PriorityGroupConfig()这句, 这里提醒用户后续编写自己程序时只需要调用一次即可,并且从代码布局逻辑来说适合放在main()函数中。