首页 关于
树枝想去撕裂天空 / 却只戳了几个微小的窟窿 / 它透出天外的光亮 / 人们把它叫做月亮和星星
目录

外部中断(EXTI)控制LED灯

上一篇文章中,我们介绍了芯片引脚的结构, 然后根据参考手册中关于GPIO的寄存器,定义了一系列的结构体和联合体用来方便的读写寄存器,控制GPIO端口,最后通过GPIO模块驱动LED灯。

本文我们仍然控制LED灯,只是控制方式改为外部中断的方式。中断是操作系统中上下文(context)切换或者说是进程切换的主要方式。 关于STM32F407内核的中断机制详细可以参考Cortex-M4的异常和中断系统

虽然外部中断通常不会被用作触发上下文切换的中断,但是考虑到它比较简单,我们在这里以外部中断为例介绍触发中断后处理器都作了什么事情, 为我们在《一个嵌入式操作系统的实现》中实现上下文切换作准备。

1. 外部中断

STM32F407允许我们直接通过I/O引脚的上升沿或者下降沿触发中断,这种中断的触发方式就是外部中断。 这种中断方式为我们提供了一种扩展机制,可以为MCU片上外设之外的各种接入设备提供中断支持。 所谓的MCU片上外设是指芯片上自带的诸如GPIO、USART、SPI等外设(peripheral)。 接入设备则是指电路板上满足某种功能的设备,可以是我们自己设计的也可以是市场上购买的。 当接入设备关注的某个事件发生了或者说是某个条件满足了,接入设备可以直接通过I/O引脚触发外部中断,请求处理器处理这一事件。

外部中断主要由外部中断/事件控制器(External interrupt/event controller, EXTI)控制,它管理了外部中断或者事件的使能与否、触发方式等功能。 图1中描述了EXTI的结构图,我们从右向左解释这一结构图。

图1 外部中断/事件控制器(EXTI)结构图

在F407中EXTI一共提供了23条中断线(input line)。EXTI中有一个边沿检测电路(edge detect circuit)监视着中断线, 并分别与上升沿和下降沿选择寄存器(Rising/Falling trigger selection register)对比。 如果在这两个寄存器中相应的中断线检测开启了,那么当中断线上有上升沿或者下降沿时边沿检测电路就会产生一个事件触发信号给后继的或门。

除了边沿检测电路的输出外,或门还接受一个软件中断事件寄存器(Software interrupt event register)的输入。 软件中断事件寄存器的存在使得我们可以通过软件的形式直接触发某一个中断线上的事件。

或门的输出接到了两个与门上,一方面与中断掩码寄存器(Interrupt mask register)求与触发中断, 另一方面与事件掩码寄存器(Event mask register)求与触发事件。 关于在STM32中事件和中断的详细解释参考这里。 中断掩码寄存器控制了相应的中断是否开启了,如果开启了中断将会产生一个中断触发信号,置位中断请求寄存器(pending request register), 同时将中断触发信号提交给中断控制器(NVIC)。 同样的道理,事件掩码寄存器控制事件是否开启,如果开启则直接产生一个脉冲通知后继的功能模块处理事件,例如通知DMA读写内存等。

在F407中所有的I/O端口都可以配置用作外部中断。F407中最多可以有140个I/O端口,而EXTI只提供了23条中断线,在这些中断线中前16条被映射到了I/O端口上。 STM32把I/O端口分成了A到I组GPIO,每组GPIO对应16个引脚,相应的在EXTI中前16条中断线分别对应所有组的各个引脚。 至于具体是哪个GPIO组需要通过设置系统配置控制器(SYSCFG)决定,如下图2所示,SYSCFG的EXTICR1的EXTI[3:0]对GPIOA到GPIOI的第0脚进行选择输出作为EXTI0。 其余15个中断线也是相同的配置。

图2 系统配置控制器中关于外部中断线0的设置

剩下7条中断线的连接情况如下:

表1 中断线连接形式

中断线 连接外设
EXTI line 16 可编程电压检测器(Programmable Voltage Detector, PVD)输出
EXTI line 17 实时时钟(Real-Time Clock)报警(Alarm)事件
EXTI line 18 USB OTG FS唤醒(Wakeup)事件
EXTI line 19 以太网唤醒(Ethernet Wakeup)事件
EXTI line 20 USB OTG HS唤醒(Wakeup)事件
EXTI line 21 RTC Tamper and TimeStamp事件
EXTI line 22 RTC唤醒事件

关于EXTI和SYSCFG的寄存器,详细请参见Reference Manual。

2. 外部中断控制LED灯

下面我们用外部中断控制LED灯, 至于外部中断所涉及的EXTI和SYSCFG的寄存器访问方法可以参考通用IO端口驱动LED灯, 这里不再赘述。这里打包了本文示例中所用的代码和工程文件。

在开发板上有四个按键,如图3所示,WK_UP接到了3.3V的电源上了,剩下的三个按键KEY0, KEY1, KEY2则接到了地上。它这样稍微有点不合理, 要是能够在WK_UP上接一个上拉电阻到3.3V,在KEY0,KEY1,KEY2上接一个下拉电阻到地的话要更好一些。这样能够保证在芯片内部引脚配置不合理的情况下, 引脚上有一个明确的电平信号。

图3 按键原理图

根据原理图,我们配置芯片引脚PA0为下拉输入,KEY0,KEY1,KEY2分别为上拉输入,这样在按键没有按下的情况下PA0默认输入0, 其余三个按键默认输入1。下面是对这四个引脚进行初始化的函数init_key():

        void init_key() {
            RCC->AHB1ENR.bits.gpioa = 1;
            RCC->AHB1ENR.bits.gpioe = 1;
        
            GPIOA->MODER.bits.pin0 = GPIO_Mode_In;   
            GPIOA->PUPDR.bits.pin0 = GPIO_Pull_Down;
            GPIOA->OSPEEDR.bits.pin0 = GPIO_OSpeed_Very_High;
            
            GPIOE->MODER.bits.pin2 = GPIO_Mode_In;   
            GPIOE->PUPDR.bits.pin2 = GPIO_Pull_Up;
            GPIOE->OSPEEDR.bits.pin2 = GPIO_OSpeed_Very_High;
        
            GPIOE->MODER.bits.pin3 = GPIO_Mode_In;   
            GPIOE->PUPDR.bits.pin3 = GPIO_Pull_Up;
            GPIOE->OSPEEDR.bits.pin3 = GPIO_OSpeed_Very_High;
        
            GPIOE->MODER.bits.pin4 = GPIO_Mode_In;   
            GPIOE->PUPDR.bits.pin4 = GPIO_Pull_Up;
            GPIOE->OSPEEDR.bits.pin4 = GPIO_OSpeed_Very_High;
        }

下面是main函数,在其中我们先对LED、KEY进行初始化,以通过GPIO直接控制相应IO端口的电平逻辑进而控制LED,或者读取KEY的输入电平。 然后在init_exti()函数中对SYSCFG, EXTI和NVIC进行配置以开启外部中断。接着关闭LED进入一个死循环,在死循环中只进行了一段时间的延时。

        int main(void) {
            init_led();
            init_key();
            init_exti();

            // 关闭LED
            GPIOF->ODR.bits.pin9 = 1;
            GPIOF->ODR.bits.pin10 = 1;
   
            while(1) {
                delay(100);
            }
        }

对LED和KEY进行初始化主要是针对GPIO寄存器的操作,详细参见通用IO端口驱动LED灯, 下面给出了init_exti()的初始化函数,这里我们选择按键WK_UP来控制两个LED灯点亮和熄灭。

首先,我们配置SYSCFG确定以GPIOA作为EXTI0的信号源,在配置之前我们先通过RCC开启了SYSCFG的时钟。SYSCFG是系统配置控制器, System configuration controller,主要是用来重映射代码段的内存位置,选择以太网PHY接口,以及外部中断线信号源。 在STM32的系统时钟设置中我们提到, 为了降低系统的功耗,默认情况下系统外设的时钟是关闭的,在启用某一外设之前必选先打开其时钟。这里SYSCFG模块挂载在APB2总线上, 所以我们对RCC的APB2ENR寄存器进行配置。

接着,我们配置EXTI的寄存器,开启外部中断线0并关闭了事件请求,开启上升沿触发机制。这里之所以使用上升沿机制, 是因为默认情况下WK_UP是低电平,按下按键时电平变化为高,就会产生一个上升沿。

最后,我们通过中断控制器NVIC赋予外部中断EXTI0最高的优先级,向IPR寄存器中写0,同时向ISER寄存器中相应位写1开启外部中断。 需要说明的是,EXTI15_10指的是外部中断线10到15共用同一个中断号(具体中断号的分配情况参见Reference Manual的第12章), 至于是EXTI15还是其他外部中断被触发了需要在中断服务函数中进行判断。

        void init_exti() {
            // 配置GPIOA为EXTI0的信号源
            RCC->APB2ENR.bits.syscfg = 1;
            SYSCFG->EXTICR.bits.exti0 = Exti_PortSource_A;
            // 开启外部中断关闭事件
            EXTI->IMR.bits.tr0 = 1;
            EXTI->EMR.bits.tr0 = 0;
            // 上升沿触发
            EXTI->RTSR.bits.tr0 = 0;
            EXTI->FTSR.bits.tr0 = 1;
            // 外部中断的优先级和使能
            NVIC->IPR.bits.EXTI0 = 0x00;
            NVIC->ISER.bits.EXTI0 = 1;
        }

我们这里使用的EXIT0,如下是该中断的服务函数,函数名为EXTI0_IRQHandler是由启动文件"startup_stm32f40_41xxx.S"中定义的向量表决定的,两处定义的函数名应当是一样的。

在服务函数中,我们先根据EXTI的中断请求寄存器(Pending Register, PR)判定中断线0是否触发了外部中断。若判定为真, 我们先延迟一段时间再读取ARM_KEY的输入电平,以消除按键的抖动。如果延迟一段时间后,仍然读入为高电平,我们认为按下了一次WK_UP键并更新状态LED灯。

        void EXTI0_IRQHandler(void) {
            if (EXTI->PR.bits.tr0) {
                delay(1000);
                if (KEY) {
                    if (0 == times) {
                        LED_0 = ON;
                        LED_1 = OFF;
                        times = 1;
                    } else if (1 == times) {
                        LED_0 = OFF;
                        LED_1 = ON;
                        times = 0;
                    }
                }
                EXTI->PR.bits.tr0 = 1;
            }
        }

3. 中断过程分析

Cortex-M4的异常和中断系统中,我们提到一个典型的中断处理流程如下:

这里,特定事件是指一次按键被按下,中断服务函数则是指EXTI0_IRQHander,它们的作用和实现原理在前两节中已经进行了充分的阐述了。 现在我们关心处理器如何挂起当前正在执行的任务进入中断服务函数,以及从中断服务函数返回时如何恢复到原来的任务中。

在回答这两个问题之前,我们需要先了解一下Cortex-M4的工作模式与访问权限。 Cortex-M4有Thread和Handler两种工作模式,一般情况下处理器是工作在Thread模式下的,当系统进入中断服务函数处理异常时,处理器是则进入了Handler模式。 这点我们可以在调试模式下通过仿真器观察得到,下图4中(a)和(b)是对Keil的截图。图4(a)记录了处理器刚进入main函数时的状态,其中红色圆框中标记了当前的工作模式为Thread, 按下按键后,系统进入EXTI0的中断处理函数,此时我们可以从图4(b)中的红色圆框中看到处理器进入了Handler的模式。

图4(a) Thread模式下内核寄存器 图4(b) Handler模式下内核寄存器

根据图5所示的中断状态时序简图,我们可以看出处理器在进入Handler模式时,经过了"Stacking & Vector fetch"这一过程。

处理器开始处理中断之前会先把当前系统信息相关的一些寄存器压入栈空间,这一过程称为Stacking。 对比图4(a)(b)中两个红色方框中标记的栈指针寄存器SP的值,我们发现在进入中断服务函数前后, SP的值减小了108个Byte,对应为27个Word(1 Word = 4 Byte),也就是说一共压入了27个寄存器的值。 参考编程手册(PM0214)第42页的图12, 对于F407这款具有FPU的处理器,产生中断时除了需要压入R0~R3、R12五个通用寄存器,链接寄存器(LR)、程序计数器(PC)和系统状态寄存器(xPSR)外, 还需要压入16个FPU寄存器(S0~S15)和一个FPU状态寄存器(FPSCR)。另外还有一个空白的Word,一共会压入26个Word。 因为我们在中断服务函数中还调用了一个延时函数delay,LR寄存器会被修改,为了保证退出中断服务函数时系统能够恢复到原来的状态, 中断服务函数额外又压入了LR寄存器一次。

根据ARM的程序调用规则, 我们知道R0~R3、R12、LR、xPSR是由调用者保存的寄存器(Caller Saved Registers)。 它们需要在函数被调用之前保存到栈空间中,在函数调用结束后从栈空间中取出,在被调用的函数中可以任意的修改这些寄存器。 R4~R11则是由被调用者保存的寄存器(Callee Saved Registers),它们由被调用的函数负责保存和恢复。 类似的对于FPU,S0~S15和S16~S31分别是Caller/Callee Saved Register。 所以在执行中断服务函数之前,需要由系统保存Caller Saved Registers,它们被称为栈帧(stack frame)。

图5 中断状态的时序简图

Vector fetch是指在处理器接收到中断请求以后,从中断向量表中查询中断服务函数, 进而处理中断事件。系统状态寄存器xPSR的低9位记录了当前正在处理的中断的编号。 如图4(b)中所示xPSR的值在进入中断后变成了0x81000016,低9位记录的中端编号为22,对应CMSIS的编号则是6,与Reference Manual中描述的EXTI0的编号一致。

在进入中断服务函数时,LR被赋予了一个特殊的值EXC_RETURN,该值用于从中断服务函数中退出时出发异常返回机制(exception return mechanism)。 从图4(b)中刚进入中断服务函数时的LR值我们可以看到0xFFFFFFE9,这个值就应该是所谓的EXC_RETURN,其位定义如表2所示。

表2 EXC_RETURN的位定义

Bits 描述 取值
31:18 EXC_RETURN标识 0xF
27:5 保留 0xEFFFFF(23位全为1)
4 栈帧类型 1:8个Word,FPU关闭
0:26个Word,FPU开启。
3 返回模式 1:系统恢复到Thread模式
0:系统恢复到Handler模式
2 使用的栈指针寄存器 1:使用PSP
2:使用MSP
1 保留 0
0 保留 1

4. 总结

在本文中,我们先介绍了外部中断的结构和特性, 接着,实现了一个通过按键触发外部中断进而控制LED灯的例程。 最后,在调试模式下分析了中断处理过程。

在STM32中,所有的I/O端口都可以用来触发外部中断。外部中断由EXTI模块管理,它提供了23条中断线,其中前16条用于I/O端口。 我们还需要设置SYSCFG寄存器来指定这16条中断线的中断源具体是哪个GPIO端口的。此外,EXTI还允许我们设定是上升沿还是下降沿或者是两者都用的触发机制。 剩下的7条中断线则接到了USB、RTC等外设上了。

处理器接收到中断请求后,会先从中断向量表中查找中断服务函数的入口。 在进入中断服务函数时,将会向栈空间中压入25个寄存器(包括5个通用寄存器、LR、PC、xPSR、16个FPU寄存器和一个FPU状态寄存器)和1个空白的Word。 同时也会把LR写为EXC_RETURN值,以保证从中断服务函数返回时,从栈空间弹出26个Word到各个寄存器中。




Copyright @ 高乙超. All Rights Reserved. 京ICP备16033081号-1