2.APM32-GPIO-按键检测
效果展示
GPIO-按键状态判断之扫描式
硬件原理图
LED模块
该电路和流水灯中的一样就不再赘述。
按键模块
由该原理图可知,按键按下时按键输出低电平,为了检测按键输出的高低电平所以APM32的GPIO要设置为输入模式,同时为了防止外部干扰,GPIO模式可设置成上拉模式,也可以在电路原理图上加一个上拉电阻,这样当按键没按下时就呈现高电平。
注意该电路有短路的危险,当IO不小心设置为输出模式,输出高电平并且按下按键容易造成短路并且烧毁IO口,为了解决这个问题可以在按键的一端接上限流电阻。
蜂鸣器模块
该电路使用NPN三极管控制蜂鸣器,当PB13端口输出高电平时三极管导通,蜂鸣器响,输出低电平,三极管截止,蜂鸣器关闭。R13为下拉电阻,使PB13端口默认电压为低电平,LL4148为续流电阻,由于蜂鸣器是电感性元件,所以当三极管处于截止状态时需要二极管LL4148进行续流,否则可能会烧毁器件。
源代码
LED源代码
LED的源代码和流水灯中的一样就不再赘述。
蜂鸣器源代码
#ifndef __BSP_BEEP_H__
#define __BSP_BEEP_H__
/* 包含头文件 ----------------------------------------------------------------*/
#include "apm32f10x_gpio.h"
#include "apm32f10x_rcm.h"
/**
* @brief BEEP-->PB13
*/
#define BEEP_RCM_CLOCK_GPIO RCM_APB2_PERIPH_GPIOB
#define BEEP_GPIO_PORT GPIOB
#define BEEP_GPIO_PIN GPIO_PIN_13
// 打开蜂鸣器
#define BEEP_ON GPIO_SetBit(BEEP_GPIO_PORT, BEEP_GPIO_PIN)
// 关闭蜂鸣器
#define BEEP_OFF GPIO_ResetBit(BEEP_GPIO_PORT, BEEP_GPIO_PIN)
// 翻转蜂鸣器状态
#define BEEP_TOGGLE \
{ \
BEEP_GPIO_PORT->ODATA ^= BEEP_GPIO_PIN; \
}
/* 函数声明 ------------------------------------------------------------------*/
void BEEP_GPIO_Config(void);
#endif /* __BSP_BEEP_H__ */
#include "bsp_beep.h"
/**
* @brief 板载蜂鸣器IO引脚初始化,
* 如需修改引脚修改bsp_beep.h文件中宏定义即可
*/
void BEEP_GPIO_Config(void)
{
// 定义IO硬件初始化结构体变量
GPIO_Config_T GPIO_ConfigStruct;
// 使能(开启)蜂鸣器引脚对应IO端口时钟
RCM_EnableAPB2PeriphClock(BEEP_RCM_CLOCK_GPIO);
// 配置蜂鸣器引脚为推挽输出模式
GPIO_ConfigStruct.mode = GPIO_MODE_OUT_PP;
// 配置蜂鸣器引脚
GPIO_ConfigStruct.pin = BEEP_GPIO_PIN;
// 配置蜂鸣器引脚速度
GPIO_ConfigStruct.speed = GPIO_SPEED_2MHz;
// 配置蜂鸣器引脚
GPIO_Config(BEEP_GPIO_PORT, &GPIO_ConfigStruct);
// 设置引脚输出为低电平,此时蜂鸣器不响
GPIO_ResetBit(BEEP_GPIO_PORT, BEEP_GPIO_PIN);
}
按键源代码
#ifndef __BSP_KEY_H__
#define __BSP_KEY_H__
/* 包含头文件 ----------------------------------------------------------------*/
#include "apm32f10x_rcm.h"
#include "apm32f10x_gpio.h"
/* 宏定义 --------------------------------------------------------------------*/
/**
* @brief PE7-->KEY4, PE8-->KEY3, PE9-->KEY2, PE10-->KEY1
*/
#define KEY_UP 0
#define KEY_DOWN 1
#define KEY1_GPIO_CLOCK RCM_APB2_PERIPH_GPIOE
#define KEY1_GPIO_PORT GPIOE
#define KEY1_GPIO_PIN GPIO_PIN_10
#define KEY1_DOWN_LEVEL 0 // 这边具体看原理图
#define KEY2_GPIO_CLOCK RCM_APB2_PERIPH_GPIOE
#define KEY2_GPIO_PORT GPIOE
#define KEY2_GPIO_PIN GPIO_PIN_9
#define KEY2_DOWN_LEVEL 0 // 这边具体看原理图
#define KEY3_GPIO_CLOCK RCM_APB2_PERIPH_GPIOE
#define KEY3_GPIO_PORT GPIOE
#define KEY3_GPIO_PIN GPIO_PIN_8
#define KEY3_DOWN_LEVEL 0// 这边具体看原理图
#define KEY4_GPIO_CLOCK RCM_APB2_PERIPH_GPIOE
#define KEY4_GPIO_PORT GPIOE
#define KEY4_GPIO_PIN GPIO_PIN_7
#define KEY4_DOWN_LEVEL 0 // 这边具体看原理图
/* 函数声明 ------------------------------------------------------------------*/
void KEY_GPIO_Config(void);
uint8_t KEY1_StateRead(void);
uint8_t KEY2_StateRead(void);
uint8_t KEY3_StateRead(void);
uint8_t KEY4_StateRead(void);
#endif /* __BSP_KEY_H__ */
/* 包含头文件 ----------------------------------------------------------------*/
#include "bsp_key.h"
/**
* @brief 板载按键IO引脚初始化
*/
void KEY_GPIO_Config(void)
{
GPIO_Config_T GPIO_ConfigStruct;
// 开启KEY1、KEY2、KEY3、KEY4引脚时钟
RCM_EnableAPB2PeriphClock(KEY1_GPIO_CLOCK);
RCM_EnableAPB2PeriphClock(KEY2_GPIO_CLOCK);
RCM_EnableAPB2PeriphClock(KEY3_GPIO_CLOCK);
RCM_EnableAPB2PeriphClock(KEY4_GPIO_CLOCK);
// 设定KEY1对应引脚IO为上拉输入模式,这边具体看是否需要上拉或下拉
GPIO_ConfigStruct.mode = GPIO_MODE_IN_PU;
// 设定KEY1对应引脚IO编号
GPIO_ConfigStruct.pin = KEY1_GPIO_PIN;
// 设定KEY1对应引脚IO操作速度
GPIO_ConfigStruct.speed = GPIO_SPEED_2MHz;
GPIO_Config(KEY1_GPIO_PORT, &GPIO_ConfigStruct);
GPIO_ConfigStruct.pin = KEY2_GPIO_PIN;
GPIO_Config(KEY2_GPIO_PORT, &GPIO_ConfigStruct);
GPIO_ConfigStruct.pin = KEY3_GPIO_PIN;
GPIO_Config(KEY3_GPIO_PORT, &GPIO_ConfigStruct);
GPIO_ConfigStruct.pin = KEY4_GPIO_PIN;
GPIO_Config(KEY4_GPIO_PORT, &GPIO_ConfigStruct);
}
/**
* @brief 按键扫描延时,简单粗暴,不精确
*/
static void KEY_ScanDelay(void)
{
uint32_t i, j;
for (i = 0; i < 100; ++i)
for (j = 0; j < 1000; ++j)
{
}
}
/**
* @brief 读取按键KEY1的状态
* @return uint8_t
* @retval KEY_DOWN 按键按下
* KEY_UP 按键没按下
*/
uint8_t KEY1_StateRead(void)
{
// 读取此时按键值并判断是否是被按下状态,如果是被按下状态进入函数内
if (GPIO_ReadInputBit(KEY1_GPIO_PORT, KEY1_GPIO_PIN) == KEY1_DOWN_LEVEL)
{
// 延时一小段时间,消除抖动
KEY_ScanDelay();
// 延时时间后再来判断按键状态,如果还是按下状态说明按键确实被按下
if (GPIO_ReadInputBit(KEY1_GPIO_PORT, KEY1_GPIO_PIN) == KEY1_DOWN_LEVEL)
{
// 等待按键弹开才退出按键扫描函数
while (GPIO_ReadInputBit(KEY1_GPIO_PORT, KEY1_GPIO_PIN) == KEY1_DOWN_LEVEL)
;
// 按键扫描完毕,确定按键被按下,返回按键被按下状态
return KEY_DOWN;
}
}
return KEY_UP;
}
/**
* @brief 读取按键KEY2的状态
* @return uint8_t
* @retval KEY_DOWN 按键按下
* KEY_UP 按键没按下
*/
uint8_t KEY2_StateRead(void)
{
// 读取此时按键值并判断是否是被按下状态,如果是被按下状态进入函数内
if (GPIO_ReadInputBit(KEY2_GPIO_PORT, KEY2_GPIO_PIN) == KEY2_DOWN_LEVEL)
{
// 延时一小段时间,消除抖动
KEY_ScanDelay();
// 延时时间后再来判断按键状态,如果还是按下状态说明按键确实被按下
if (GPIO_ReadInputBit(KEY2_GPIO_PORT, KEY2_GPIO_PIN) == KEY2_DOWN_LEVEL)
{
// 等待按键弹开才退出按键扫描函数
while (GPIO_ReadInputBit(KEY2_GPIO_PORT, KEY2_GPIO_PIN) == KEY2_DOWN_LEVEL)
;
// 按键扫描完毕,确定按键被按下,返回按键被按下状态
return KEY_DOWN;
}
}
return KEY_UP;
}
/**
* @brief 读取按键KEY3的状态
* @return uint8_t
* @retval KEY_DOWN 按键按下
* KEY_UP 按键没按下
*/
uint8_t KEY3_StateRead(void)
{
// 读取此时按键值并判断是否是被按下状态,如果是被按下状态进入函数内
if (GPIO_ReadInputBit(KEY3_GPIO_PORT, KEY3_GPIO_PIN) == KEY3_DOWN_LEVEL)
{
// 延时一小段时间,消除抖动
KEY_ScanDelay();
// 延时时间后再来判断按键状态,如果还是按下状态说明按键确实被按下
if (GPIO_ReadInputBit(KEY3_GPIO_PORT, KEY3_GPIO_PIN) == KEY3_DOWN_LEVEL)
{
// 等待按键弹开才退出按键扫描函数
while (GPIO_ReadInputBit(KEY3_GPIO_PORT, KEY3_GPIO_PIN) == KEY3_DOWN_LEVEL)
;
// 按键扫描完毕,确定按键被按下,返回按键被按下状态
return KEY_DOWN;
}
}
return KEY_UP;
}
/**
* @brief 读取按键KEY4的状态
* @return uint8_t
* @retval KEY_DOWN 按键按下
* KEY_UP 按键没按下
*/
uint8_t KEY4_StateRead(void)
{
// 读取此时按键值并判断是否是被按下状态,如果是被按下状态进入函数内
if (GPIO_ReadInputBit(KEY4_GPIO_PORT, KEY4_GPIO_PIN) == KEY4_DOWN_LEVEL)
{
// 延时一小段时间,消除抖动
KEY_ScanDelay();
// 延时时间后再来判断按键状态,如果还是按下状态说明按键确实被按下
if (GPIO_ReadInputBit(KEY4_GPIO_PORT, KEY4_GPIO_PIN) == KEY4_DOWN_LEVEL)
{
// 等待按键弹开才退出按键扫描函数
while (GPIO_ReadInputBit(KEY4_GPIO_PORT, KEY4_GPIO_PIN) == KEY4_DOWN_LEVEL)
;
// 按键扫描完毕,确定按键被按下,返回按键被按下状态
return KEY_DOWN;
}
}
return KEY_UP;
}
主程序
/**
* @file main.c
* @brief PE7-->KEY4, PE8-->KEY3, PE9-->KEY2, PE10-->KEY1
*/
/* 包含头文件 ----------------------------------------------------------------*/
#include "bsp_led.h"
#include "bsp_beep.h"
#include "bsp_key.h"
/* 私有类型定义 --------------------------------------------------------------*/
/* 私有宏定义 ----------------------------------------------------------------*/
/* 私有变量 ------------------------------------------------------------------*/
/* 扩展变量 ------------------------------------------------------------------*/
/* 私有函数原形 --------------------------------------------------------------*/
static void Delay(uint32_t time);
/**
* @brief 主函数
* @return 无
*/
int main(void)
{
// 初始化板载LED灯
LED_GPIO_Config();
// 初始化板载蜂鸣器
BEEP_GPIO_Config();
// 初始化板载按键
KEY_GPIO_Config();
while (1)
{
if (KEY1_StateRead() == KEY_DOWN) // KEY1按下,蜂鸣器响,LED1翻转
{
BEEP_ON;
LED1_TOGGLE;
}
if (KEY2_StateRead() == KEY_DOWN) // KEY2按下,蜂鸣器关闭,LED2翻转
{
BEEP_OFF;
LED2_TOGGLE;
}
if (KEY3_StateRead() == KEY_DOWN) // KEY3按下,蜂鸣器响,LED3翻转
{
BEEP_ON;
LED3_TOGGLE;
}
if (KEY4_StateRead() == KEY_DOWN) // KEY4按下,蜂鸣器关闭,LED1-3关闭
{
BEEP_OFF;
LED1_OFF;
LED2_OFF;
LED3_OFF;
}
Delay(100);
}
}
/**
* @brief 简单粗暴的延时函数,不精确
* @param time 延时时间设置
*/
static void Delay(uint32_t time)
{
uint32_t i, j;
for (i = 0; i < time; ++i)
{
for (j = 0; j < 10000; ++j)
{
// 空循环,什么都不做
}
}
}
代码分析
bsp_beep.h
#define BEEP_RCM_CLOCK_GPIO RCM_APB2_PERIPH_GPIOB
#define BEEP_GPIO_PORT GPIOB
#define BEEP_GPIO_PIN GPIO_PIN_13
这段代码是宏定义和流水灯中的一样都是为了方便程序的移植、维护和使用。
// 打开蜂鸣器
#define BEEP_ON GPIO_SetBit(BEEP_GPIO_PORT, BEEP_GPIO_PIN)
// 关闭蜂鸣器
#define BEEP_OFF GPIO_ResetBit(BEEP_GPIO_PORT, BEEP_GPIO_PIN)
// 翻转蜂鸣器状态
#define BEEP_TOGGLE \
{ \
BEEP_GPIO_PORT->ODATA ^= BEEP_GPIO_PIN; \
}
这段代码也和流水灯中控制LED的代码差不多,无非就是换了名字和GPIO,具体也不再赘述。
bsp_beep.c
/**
* @brief 板载蜂鸣器IO引脚初始化,
* 如需修改引脚修改bsp_beep.h文件中宏定义即可
*/
void BEEP_GPIO_Config(void)
{
// 定义IO硬件初始化结构体变量
GPIO_Config_T GPIO_ConfigStruct;
// 使能(开启)蜂鸣器引脚对应IO端口时钟
RCM_EnableAPB2PeriphClock(BEEP_RCM_CLOCK_GPIO);
// 配置蜂鸣器引脚为推挽输出模式
GPIO_ConfigStruct.mode = GPIO_MODE_OUT_PP;
// 配置蜂鸣器引脚
GPIO_ConfigStruct.pin = BEEP_GPIO_PIN;
// 配置蜂鸣器引脚速度
GPIO_ConfigStruct.speed = GPIO_SPEED_2MHz;
// 配置蜂鸣器引脚
GPIO_Config(BEEP_GPIO_PORT, &GPIO_ConfigStruct);
// 设置引脚输出为低电平,此时蜂鸣器不响
GPIO_ResetBit(BEEP_GPIO_PORT, BEEP_GPIO_PIN);
}
蜂鸣器引脚初始化和流水灯中初始化LED一样,先是声明一个GPIO的结构体,打开GPIO对应的时钟,配置结构体,将结构体填入初始化函数中。
bsp_key.h
#define KEY_UP 0
#define KEY_DOWN 1
#define KEY1_GPIO_CLOCK RCM_APB2_PERIPH_GPIOE
#define KEY1_GPIO_PORT GPIOE
#define KEY1_GPIO_PIN GPIO_PIN_10
#define KEY1_DOWN_LEVEL 0 // 这边具体看原理图
KEY_UP
和KEY_DOWN
两定义分别表示按键松开和按键按下。
#define KEY1_DOWN_LEVEL 0
该定义表示按键按下,按键输出什么电平,若按键按下输出低电平这边就定义为0,输出高电平就定义为1,提高程序的移植性。
剩下的宏定义就和流水灯中类似,不在赘述。
bsp_key.c
/**
* @brief 板载按键IO引脚初始化
*/
void KEY_GPIO_Config(void)
{
GPIO_Config_T GPIO_ConfigStruct;
// 开启KEY1、KEY2、KEY3、KEY4引脚时钟
RCM_EnableAPB2PeriphClock(KEY1_GPIO_CLOCK);
RCM_EnableAPB2PeriphClock(KEY2_GPIO_CLOCK);
RCM_EnableAPB2PeriphClock(KEY3_GPIO_CLOCK);
RCM_EnableAPB2PeriphClock(KEY4_GPIO_CLOCK);
// 设定KEY1对应引脚IO为上拉输入模式,这边具体看是否需要上拉或下拉
GPIO_ConfigStruct.mode = GPIO_MODE_IN_PU;
// 设定KEY1对应引脚IO编号
GPIO_ConfigStruct.pin = KEY1_GPIO_PIN;
// 设定KEY1对应引脚IO操作速度
GPIO_ConfigStruct.speed = GPIO_SPEED_2MHz;
GPIO_Config(KEY1_GPIO_PORT, &GPIO_ConfigStruct);
GPIO_ConfigStruct.pin = KEY2_GPIO_PIN;
GPIO_Config(KEY2_GPIO_PORT, &GPIO_ConfigStruct);
GPIO_ConfigStruct.pin = KEY3_GPIO_PIN;
GPIO_Config(KEY3_GPIO_PORT, &GPIO_ConfigStruct);
GPIO_ConfigStruct.pin = KEY4_GPIO_PIN;
GPIO_Config(KEY4_GPIO_PORT, &GPIO_ConfigStruct);
}
上述代码是初始化按键相关GPIO的,和流水灯中初始化LED相关GPIO只有GPIO_ConfigStruct.mode = GPIO_MODE_IN_PU;
不同,其他都是一样的。
GPIO_ConfigStruct.mode = GPIO_MODE_IN_PU;
表示设置GPIO为输入上拉模式。
在介绍其他函数之前,我们先来看下APM32标准库中读取电平的函数
GPIO_ReadInputBit()
我们先来看下该函数的源码。
/*!
* @brief Reads the specified input port pin
*
* @param port: Select the GPIO port.
* This parameter can be one of GPIOx( x can be from A to G).
*
* @param pin : specifies pin to read.
* This parameter can be one of GPIO_PIN_x( x can be from 0 to 15).
*
* @retval The input port pin value
*/
uint8_t GPIO_ReadInputBit(GPIO_T* port, uint16_t pin)
{
uint8_t ret;
ret = (port->IDATA & pin) ? BIT_SET : BIT_RESET;
return ret;
}
从函数的注释可以知道
-
这个函数功能是读取指定引脚输入的电平。
-
port
可选GPIOA到GPIOG -
pin
可选GPIO_PIN_0到GPIO_PIN_15
ret = (port->IDATA & pin) ? BIT_SET : BIT_RESET;
这个是C语言的三目运算符写法,是if else
的一种简写,如果(port->IDATA & pin)
为真,则ret=BIT_SET
否则为BIT_RESET
。
我们在来看下APM32参考手册,第116页
该寄存器的低16位用来储存GPIOx的输入数据,第y位对应GPIO_PIN_y。IDATA & pin
这样的与运算就是读取GPIO_PIN_y的电压,0表示输入信号位低,1表示输入信号为高。
GPIO_ReadInputPort()
GPIO_ReadInputBit()
是读取单个引脚的输入信号的,如果想读取GPIOx端口16个引脚的电压就要用GPIO_ReadInputPort()
,我们来看下该函数的原代码。
/*!
* @brief Reads the specified GPIO input data port
*
* @param port: Select the GPIO port.
* This parameter can be one of GPIOx( x can be from A to G).
*
* @retval GPIO input data port value
*/
uint16_t GPIO_ReadInputPort(GPIO_T* port)
{
return ((uint16_t)port->IDATA);
}
这个函数的代码和GPIO_ReadInputBit()
很像,就是少了与运算,直接将IDATA寄存器的值返回出去了。
看完上面两个函数我们继续看按键检测的代码,代码如下
/**
* @brief 读取按键KEY1的状态
* @return uint8_t
* @retval KEY_DOWN 按键按下
* KEY_UP 按键没按下
*/
uint8_t KEY1_StateRead(void)
{
// 读取此时按键值并判断是否是被按下状态,如果是被按下状态进入函数内
if (GPIO_ReadInputBit(KEY1_GPIO_PORT, KEY1_GPIO_PIN) == KEY1_DOWN_LEVEL)
{
// 延时一小段时间,消除抖动
KEY_ScanDelay();
// 延时时间后再来判断按键状态,如果还是按下状态说明按键确实被按下
if (GPIO_ReadInputBit(KEY1_GPIO_PORT, KEY1_GPIO_PIN) == KEY1_DOWN_LEVEL)
{
// 等待按键弹开才退出按键扫描函数
while (GPIO_ReadInputBit(KEY1_GPIO_PORT, KEY1_GPIO_PIN) == KEY1_DOWN_LEVEL)
;
// 按键扫描完毕,确定按键被按下,返回按键被按下状态
return KEY_DOWN;
}
}
return KEY_UP;
}
可以看到,在代码中我们先读取按键的状态,延时了一会又去读按键的状态,这是为什么?因为由于按键的物理特性,按键按下的一瞬间不可能就变成按下状态,在这个过程中按键会抖动,如下图
等待按键到稳定状态我们再去读取它的电平,根据读取到的电平返回按键的状态是按下还是没按下。
其他几个按键的检测如法炮制即可。
注意这种按键检测的方式是阻塞的,很浪费MCU的资源,也有非阻塞的检测方式,后续再说。
main.c
/**
* @file main.c
* @brief PE7-->KEY4, PE8-->KEY3, PE9-->KEY2, PE10-->KEY1
*/
/* 包含头文件 ----------------------------------------------------------------*/
#include "bsp_led.h"
#include "bsp_beep.h"
#include "bsp_key.h"
/* 私有类型定义 --------------------------------------------------------------*/
/* 私有宏定义 ----------------------------------------------------------------*/
/* 私有变量 ------------------------------------------------------------------*/
/* 扩展变量 ------------------------------------------------------------------*/
/* 私有函数原形 --------------------------------------------------------------*/
static void Delay(uint32_t time);
/**
* @brief 主函数
* @return 无
*/
int main(void)
{
// 初始化板载LED灯
LED_GPIO_Config();
// 初始化板载蜂鸣器
BEEP_GPIO_Config();
// 初始化板载按键
KEY_GPIO_Config();
while (1)
{
if (KEY1_StateRead() == KEY_DOWN) // KEY1按下,蜂鸣器响,LED1翻转
{
BEEP_ON;
LED1_TOGGLE;
}
if (KEY2_StateRead() == KEY_DOWN) // KEY2按下,蜂鸣器关闭,LED2翻转
{
BEEP_OFF;
LED2_TOGGLE;
}
if (KEY3_StateRead() == KEY_DOWN) // KEY3按下,蜂鸣器响,LED3翻转
{
BEEP_ON;
LED3_TOGGLE;
}
if (KEY4_StateRead() == KEY_DOWN) // KEY4按下,蜂鸣器关闭,LED1-3关闭
{
BEEP_OFF;
LED1_OFF;
LED2_OFF;
LED3_OFF;
}
Delay(100);
}
}
/**
* @brief 简单粗暴的延时函数,不精确
* @param time 延时时间设置
*/
static void Delay(uint32_t time)
{
uint32_t i, j;
for (i = 0; i < time; ++i)
{
for (j = 0; j < 10000; ++j)
{
// 空循环,什么都不做
}
}
}
这段段代码很简单,先是初始化LED、BEEP、KEY,然后根据不同的按键按下做相应的事。