开发环境:
MDK:Keil 5.30
开发板:N32G4FRML-STB 开发板
MCU:N32G4FRMEL7
1 普通方式
1.1 普通方式工作原理
按键 GPIO 端口有两个方案可以选择,一是采用上拉输入模式,因为按键在没按下的时候,是默认为高电平的,采且内部上拉模式正好符合这个要求。第二个方案是直接采用浮空输入模式,因为按照硬件电路图,在芯片外部接了上拉电阻,其实就没必要再配置成内部上拉输入模式了,因为在外部上拉与内部上拉效果是一样的。
笔者本文将会使用S1。当检测到低电平按键按下。
1.2 普通方式实现
主函数代码如下:
int main(void)
{
ST_BSP_LED_Dev BSP_LED_Dev0 = LED_DEV0_CONFIG;
ST_BSP_LED_Dev BSP_LED_Dev1 = LED_DEV1_CONFIG;
ST_BSP_LED_Dev BSP_LED_Dev2 = LED_DEV2_CONFIG;
ST_BSP_Key_Dev BSP_KEY_Dev0 = KEY_DEV0_CONFIG;
SysTick_Init();
BSP_LED_Init(&BSP_LED_Dev0);
BSP_LED_Init(&BSP_LED_Dev1);
BSP_LED_Init(&BSP_LED_Dev2);
BSP_Key_Init(&BSP_KEY_Dev0);
RCC_EnableAPB2PeriphClk(RCC_APB2_PERIPH_AFIO, ENABLE);
GPIO_ConfigPinRemap(GPIO_RMP_SW_JTAG_NO_NJTRST, ENABLE);
while(1)
{
if(BSP_Key_Scan(&BSP_KEY_Dev0))
{
BSP_LED_Toggle(&BSP_LED_Dev0);
BSP_LED_Toggle(&BSP_LED_Dev1);
BSP_LED_Toggle(&BSP_LED_Dev2);
}
}
}
GPIO初始化代码如下:
void BSP_Key_Init(ST_BSP_Key_Dev *BSP_Key_Dev)
{
GPIO_InitType GPIO_InitStructure;
RCC_EnableAPB2PeriphClk ( BSP_Key_Dev->key_gpio_clk, ENABLE);
GPIO_InitStructure.Pin = BSP_Key_Dev->key_pin;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitPeripheral(BSP_Key_Dev->key_port, &GPIO_InitStructure);
}
BSP_Key_Init ()与 LED 的 GPIO 初始化函数 BSP_Key_Init ()类似,区别只是在这个函数中,要开启的 GPIO 的端口时钟不一样,并且把检测按键用的引脚 Pin 的模式设置为浮空输入。
uint8_t BSP_Key_Scan(ST_BSP_Key_Dev *BSP_Key_Dev)
{
uint8_t Down_state = SET;
if(GPIO_ReadInputDataBit(BSP_Key_Dev->key_port, BSP_Key_Dev->key_pin) == Down_state )
{
Key_Delay(10000);
if(GPIO_ReadInputDataBit(BSP_Key_Dev->key_port, BSP_Key_Dev->key_pin) == Down_state )
{
while(GPIO_ReadInputDataBit(BSP_Key_Dev->key_port, BSP_Key_Dev->key_pin) == Down_state);
return KEY_ON;
}
else
{
return KEY_OFF;
}
}
else
{
return KEY_OFF;
}
}
相信延时消抖的原理大家在学习其他单片机时就已经了解了,本函数的功能就是扫描输入参数中指定的引脚,检测其电平变化,并作延时消抖处理,最终对按键消息进行确认。
- 利用 GPIO_ReadInputDataBit () 读取输入数据,若从相应引脚读取的数据等于 0(KEY_ON),低电平,表明可能有按键按下,调用延时函数。否则返回 KEY_OFF,表示按键没有被按下。
- 延时之后再次利用 GPIO_ReadInputDataBit ()读取输入数据,若依然为低电平,表明确实有按键被按下了。否则返回 KEY_OFF,表示按键没有被按下。
- 循环调用GPIO_ReadInputDataBit () 一直检测按键的电平,直至按键被释放,被释放后,返回表示按键被按下的标志 KEY_ON。以上是按键消抖的流程,调用了一个库函数GPIO_ReadInputDataBit ()。输入参数为要读取的端口、引脚,返回引脚的输入电平状态,高电平为 1,低电平为 0。
2 EXTI方式
2.1 EXTI的工作原理
EXTI(External Interrupt) 就是指外部中断,通过 GPIO 检测输入脉冲,引起中断事件,打断原来的代码执行流程,进入到中断服务函数中进行处理,处理完后再返回到中断之前的代码中执行。
Cortex内核具有强大的异常响应系统,它把能够打断当前代码执行流程的事件分为异常(exception)和中断(interrupt),并把它们用一个表管理起来,编号为 0 ~ 15 的称为内核异常,而 16 以上的则称为外部中断(外是相对内核而言),这个表就称为中断向量表。
而N32 对这个表重新进行了编排,把编号从-3 至 6 的中断向量定义为系统异常,编号为负的内核异常不能被设置优先级,如复位(Reset)、不可屏蔽中断 (NMI)、硬错误(Hardfault)。从编号 7 开始的为外部中断,这些中断的优先级都是可以自行设置的。详细的N32中断向量表见下表。
……
完整向量表请参考《N32G4FR series 32-bit ARM® Cortex®-M4F microcontroller User manual V3.0》。
N32的中断如此之多,配置起来并不容易,因此我们需要一个强大而方便的中断控制器 NVIC (Nested Vectored Interrupt Controller)。NVIC 是属于 Cortex 内核的器件,不可屏蔽中断 (NMI)和外部中断都由它来处理,而 SYSTICK 不是由 NVIC 来控制的。
当我们要使用 NVIC 来配置中断时,自然想到固件库肯定也已经把它封装成库函数了。对 NVIC 初始化,首先要定义并填充一个 NVIC_InitType类型的结构体。这个结构体有 4 个成员,见下表。
结构体成员名称 |
描述 |
NVIC_IRQChannel |
需要配置的中断向量 |
NVIC_IRQChannelCmd |
使能或关闭相应中断向量的中断响应 |
NVIC_IRQChannelPreemptionPriority |
配置相应中断向量抢占优先级 |
NVIC_IRQChannelSubPriority |
配置相应中断向量的响应优先级 |
前面两个结构体成员都很好理解,首先要用 NVIC_IRQChannel 参数来选择将要配置的中断向量,用 NVIC_IRQChannelCmd 参数来进行使能(ENABLE)或关闭(DISABLE)该中断。在 NVIC_IRQChannelPreemptionPriority 成员要配置中断向量的抢占优先级,在NVIC_IRQChannelSubPriority 需要配置中断向量的响应优先级。对于中断的配置,最重要的便是配置其优先级,但N32 的同一个中断向量为什么需要设置两种优先级?这两种优先级有什么区别?
N32 的中断向量具有两个属性,一个为抢占属性,另一个为响应属性,其属性编号越小,表明它的优先级别越高。
抢占,是指打断其他中断的属性,即因为具有这个属性会出现嵌套中断(在执行中断服务函数 A 的过程中被中断 B 打断,执行完中断服务函数 B 再继续执行中断服务函数A),抢占属性由 NVIC_IRQChannelPreemptionPriority 的参数配置。
而响应属性则应用在抢占属性相同的情况下,当 两个中断向量的抢占优先级相同时,如 果两个中断同时到达,则先处理响应优先级高的中断,响应属性 由NVIC_IRQChannelSubPriority 参数配置。例如,现在有三个中断向量,见下表。
中断向量 |
抢占优先级 |
响应优先级 |
A |
0 |
0 |
B |
1 |
0 |
C |
1 |
1 |
若内核正在执行 C 的中断服务函数,则它能被抢占优先级更高的中断 A 打断,由于 B和 C 的抢占优先级相同,所以 C 不能被 B 打断。但如果 B 和 C 中断是同时到达的,内核就会首先响应响应优先级别更高的 B 中断。
在配置优先级的时候,还要注意一个很重要的问题,即中断种类的数量。NVIC 只可以配置 16 种中断向量的优先级,也就是说,抢占优先级和响应优先级的数量由一个 4 位的数字来决定,把这个 4 位数字的位数分配成抢占优先级部分和响应优先级部分。有 5 组分配方式 :
第 0 组: 所有 4 位用来配置响应优先级。即 16 种中断向量具有都不相同的响应优先级。
第 1 组:最高 1 位用来配置抢占优先级,低 3 位用来配置响应优先级。表示有 21=2 种级别的抢占优先级(0 级,1 级),有 23=8 种响应优先级,即在 16 种中断向量之中,有8 种中断,其抢占优先级都为 0 级,而它们的响应优先级分别为 0~7,其余 8 种中断向量的抢占优先级则都为 1 级,响应优先级别分别为 0~7。
第 2 组:2 位用来配置抢占优先级,2 位用来配置响应优先级。即 22=4 种抢占优先级,22=4 种响应优先级。
第 3 组:高 3 位用来配置抢占优先级,最低 1 位用来配置响应优先级。即有 8 种抢占优先级,2 种响应 2 优先级。
第 4 组:所有 4 位用来配置抢占优先级,即 NVIC 配置的 24 =16 种中断向量都是只有抢占属性,没有响应属性。
要配置这些优先级组,可以采用库函数 NVIC_PriorityGroupConfig(),可输入的参数为NVIC_PriorityGroup_0 ~ NVIC_PriorityGroup_4,分别为以上介绍的 5 种分配组。
于是,有读者觉得疑惑了, 如此强的N32, 所有GPIO都能够配置成外部中断,USART、ADC 等外设也有中断,而 NVIC 只能配置 16 种中断向量,那么在某个工程中使用超过 16 个中断怎么办呢?注意 NVIC 能配置的是 16 种中断向量,而不是16 个,当工程中有超过 16 个中断向量时,必然有两个以上的中断向量是使用相同的中断种类,而具有相同中断种类的中断向量不能互相嵌套。
N32的所有 I/O 端口都可以配置为 EXTI 中断模式,用来捕捉外部信号,可以配置为下降沿中断、上升沿中断和上升下降沿中断这三种模式。它们以图Figure 2‑1所示方式连接到 16 个外部中断 / 事件线上。
N32 的所有 GPIO 都引入到 EXTI 外部中断线上,使得所有的 GPIO 都能作为外部中断的输入源。GPIO 与 EXTI 的连接方式见下图。
观察图下可知,PA0 ~ PD0 连接到 EXTI0 、PA1 ~ PD1 连接到 EXTI1、……、PA15 ~ PD15 连接到 EXTI15。这里大家要注意的是 :PAx ~ PDx 端口的中断事件都连接到了 EXTIx,即同一时刻 EXTIx 只能响应一个端口的事件触发,不能够同一时间响应所有GPIO 端口的事件,但可以分时复用。它可以配置为上升沿触发、下降沿触发或双边沿触发。EXTI 最普通的应用就是接上一个按键,设置为下降沿触发,用中断来检测按键。
2.2 EXTI方式实现
现在我们重点分析BSP_Key_Init() 这个函数,它完成了配置一个 I/O 为 EXTI 中断的一般步骤,主要有以下功能 :
1)使能 EXTIx 线的时钟和第二功能 AFIO 时钟。
2)配置 EXTIx 线的中断优先级。
3)配置 EXTI 中断线 I/O。
4)选定要配置为 EXTI 的 I/O 口线和 I/O 口的工作模式。
5)EXTI 中断线工作模式配置。
void BSP_Key_Init(ST_BSP_Key_Dev *BSP_Key_Dev, EN_Key_Mode KeyMode, uint8_t PreemptionPriority, uint8_t SubPriority)
{
GPIO_InitType GPIO_InitStructure;
EXTI_InitType EXTI_InitStructure;
RCC_EnableAPB2PeriphClk ( BSP_Key_Dev->key_gpio_clk, ENABLE);
GPIO_InitStructure.Pin = BSP_Key_Dev->key_pin;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitPeripheral(BSP_Key_Dev->key_port, &GPIO_InitStructure);
if(KeyMode == KEY_MODE_EXTI)
{
RCC_EnableAPB2PeriphClk(RCC_APB2_PERIPH_AFIO, ENABLE);
GPIO_ConfigEXTILine(BSP_Key_Dev->key_exti_port_source, BSP_Key_Dev->key_exti_pin_source);
EXTI_InitStructure.EXTI_Line = BSP_Key_Dev->key_exti_line;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_InitPeripheral(&EXTI_InitStructure);
}
BSP_Key_NVIC_Configuration(BSP_Key_Dev->key_irqn, PreemptionPriority, SubPriority);
}
BSP_Key_Init() 代码中,配置好 NVIC 后,还要对 GPIOA 进行初始化,这部分和按键轮询的设置类似。
接下来,开启复用时钟,调用GPIO_ConfigEXTILine () 函数把 GPIOA、Pin4设置为 EXTI 输入线。选择好了 GPIO,开始填写 EXTI 的初始化结构体。从这些参数的名字,相信读者已经知道如何把它应用到按键检测中。
1).EXTI_Line = EXTI_LINE4 : 给 EXTI_Line 成员赋值。选择 EXTI_Line0 线进行配置,因为按键的 PA0 连接到了 EXTI_Line0。
2).EXTI_Mode = EXTI_Mode_Interrupt :给 EXTI_Mode 成员赋值。 把 EXTI_Line0的模式设置为中断模式(EXTI_Mode_Interrupt)。这个结构体成员也可以赋值为事件模式EXTI_Mode_Event ,这个模式不会立刻触发中断,而只是在 寄存器上把相应的事件标志位置 1,应用这个模式需要不停地查询相应的寄存器。
3).EXTI_Trigger = EXTI_Trigger_Falling :给 EXTI_Trigger 成员赋值。把触发方式(EXTI_Trigger)设置为下降沿触发(EXTI_Trigger_Falling)。
4).EXTI_LineCmd = ENABLE :给 EXTI_LineCmd 成员赋值。把 EXTI_LineCmd 设置为使能。
最后调用 EXTI_Init() 把 EXTI 初始化结构体的参数写入寄存器。
AFIO (alternate-function I/O),指 GPIO 端口的复用功能,GPIO 除了用作普通的输入输出(主功能),还可以作为片上外设的复用输入输出,如串口、ADC,这些就是复用功能。大多数 GPIO 都有一个默认复用功能,有的 GPIO 还有重映射功能。重映射功能是指把原来属于 A 引脚的默认复用功能,转移到 B 引脚进行使用,前提是 B 引脚具有这个重映射功能。
当把 GPIO 用作 EXTI 外部中断或使用重映射功能的时候,必须开启 AFIO 时钟,而在使用默认复用功能的时候,就不必开启 AFIO 时钟了。
static void BSP_Key_NVIC_Configuration(uint8_t IRQChannel, uint8_t PreemptionPriority, uint8_t SubPriority)
{
NVIC_InitType NVIC_InitStructure;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
NVIC_InitStructure.NVIC_IRQChannel = IRQChannel;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = PreemptionPriority;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = SubPriority;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
本代码中向 NVIC 初始化结构体写入参数 .NVIC_IRQChannel =EXTI0_IRQn,表示要配置的为 EXTI 第 1 线的中断向量。因为按键 PA4对应的 EXTI 线为EXTI0。这些可写入的参数可以在 stm32f4xx.h 文件的 IRQn 类型定义中查找到。然后配置抢占优先级和响应优先级,因为这个工程简单,就直接把它设置为最高级中断。填充完结构体,别忘记最后要调用 NVIC_Init() 函数来向寄存器写入参数。这里要注意的是,如果用的 IO 口是 IO0 ~ IO4,那么对应的中断向量是 EXTI0_IRQn ~ EXTI4_IRQn,如果用的 IO 是I05 ~ IO9 中的一个的话,对应的中断向量只能是 EXTI9_5_IRQn, 如果用的 IO 是 I010~IO15中的一个的话,对应的中断向量只能是 EXTI15_10_IRQn。举例:如果 PE5 或者 PE6 作为EXTI 中断口,那么对应的中断向量都是 EXTI9_5_IRQn,在同一时刻只能相应来自一个IO 的 EXTI 中断。
在这个 EXTI 设置中我们把 PA4 连接到内部的 EXTI4,GPIO 配置为上拉输入,工作在下降沿中断。在外围电路上我们将 PA4 接到了 key上。当按键没有按下时,PA4始终为高,当按键按下时 PA4变为低,从而 PA4上产生一个下降沿跳变,EXTI4 会捕捉到这一跳变,并产生相应的中断,中断服务程序在 n32g4fr_it.c 中实现。n32g4fr_it.c 文件是专门用来存放中断服务函数的。文件中默认只有几个关于系统异常的中断服务函数,而且都是空函数,在需要的时候自行编写。那么中断服务函数名是不是可以自己定义呢?不可以。中断服务函数的名字必须要与启动文件startup_n32g4fr.s 中的中断向量表定义一致。
EXTI4_.IRQHandler 表示为 EXTI0 中断向量的服务函数名。于是,我们就可以在n32g4fr_it.c文件中加入名为 EXTI4_IRQHandler() 的函数。
void EXTI4_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_LINE4) != RESET)
{
BSP_LED_Toggle(&BSP_LED_Dev0);
BSP_LED_Toggle(&BSP_LED_Dev1);
EXTI_ClrITPendBit(EXTI_LINE4);
}
}
其内容比较容易理解,进入中断后,调用库函数 EXTI_GetITStatus () 来重新检查是否产生了 EXTI_Line 中断,接下来把 LED 取反,操作完毕后,调用 EXTI_ClrITPendBit ()清除中断标志位再退出中断服务函数。
3 实验现象
编译好程序后,下载到板子上,不管是普通方式还是中断方式,当按在按键按按下时,LED或亮或灭。