当我们按下一个按键,LED灯做出反转,再按另一个,蜂鸣器随之响起,如何做到这些,这一章就带你领略——按键输入。
对于按键输入有两种方式:
1、查询式(不断检测GPIO口电平变化)
2、中断式(触发中断进入中断服务程序)
这一章先讲查询式,学过中断之后再讲中断式。
基本思路如下:
1、开启GPIO口时钟;
2、配置按键输入方式;
3、扫描按键是否输入;
4、根据按键做出动作。
目录
- 一、电路连接
- 二、按键配置
-
- 1、时钟的开启
- 2、上拉下拉配置
- 3、按键检测
- 4、根据按键做动作
- 三、总程序
一、电路连接
四个按键:
WK_UP---->PA0
KEY2------->PE2
KEY1------->PE3
KEY0------->PE4
注意看自己开发板按键电路图
二、按键配置
1、时钟的开启
由电路图可得,按键涉及的IO口有PA,PE,那我们得开启GPIOA和GPIOE的时钟,关于开启外设时钟方法之前有讲到,故这里不再赘述,它需用到RCC_APB2ENR(外设时钟使能寄存器)。
2、上拉下拉配置
上拉是上拉至电源(高电平),下拉是拉至地(低电平)。那为什么要设置上拉下拉呢?我们的按键还没按下的时候,GPIO口输入的电平是不确定的,有可能高,有可能低,这时候外界对IO口可能会造成干扰,从而影响设备的动作,所以通过上拉下拉,把GPIO口电位钳置在高电平或低电平,来增强它的抗干扰能力。
根据电路图可以得知,WK_UP要设置为下拉,如果设置为上拉就是高电平,按键按下和松开都是高电平,没有变化,同理,KEY0、KEY1、KEY2要设置为上拉。
如图,设置上拉下拉我们还是用GPIOx_CRL(端口配置低寄存器),还有,就是最重要的,设置上拉,还需设置GPIOx_ODR(数据输出寄存器)。
3、按键检测
一个IO口需要输出高电平还是低电平,我们通过ODR来控制,IDR正好相反,这个寄存器就是用来检测该IO口是高电平还是低电平,我们具体来看使用方法:
- 现在假设我们用按键KEY2(PE2),KEY2是上拉模式,不按下时,PE2引脚是高电平,GPIOE_IDR第2位值读出为1,按键按下时,接地,PE2引脚是低电平,GPIOE_IDR第2位值读出为0。
这样分析,我们便有了按键检测的依据,可以如下定义KEY2
#define KEY2 ((GPIOE_IDR&(1<<2))?1:0) //KEY2->PE2
// (?:)这是一个三目运算符
// KEY2 ((GPIOE_IDR&(1<<2))?1:0)
//如果GPIOE_IDR&(1<<2)成立,那KEY2值为1,不成立则为0
四个按键都如此定义:
//---------------------按键配置---------------------------
#define WK_UP ((GPIOA_IDR&(1<<0))?1:0) //WK_UP->PA0
#define KEY0 ((GPIOE_IDR&(1<<4))?1:0) //KEY0->PE4
#define KEY1 ((GPIOE_IDR&(1<<3))?1:0) //KEY1->PE3
#define KEY2 ((GPIOE_IDR&(1<<2))?1:0) //KEY2->PE2
同样,所有IO口都可以检测,和上边方式一样
//--------------------------------------------------------
#define LED0 ((GPIOB_IDR&(1<<5))?1:0) //判断LED0状态
#define LED1 ((GPIOE_IDR&(1<<5))?1:0) //判断LED1状态
#define BEEP ((GPIOB_IDR&(1<<8))?1:0) //判断蜂鸣器状态
4、根据按键做动作
我们上边定义了按键,就可以根据返回的值是0还是1来判断按键有没有按下,这里强调一下,WK_UP是设置下拉,所以没按下时是0,按下了是1。
unsigned int KEY_SCANF()
{
static unsigned int key_up=1;
if(key_up&&((KEY2==0)||(KEY1==0)||(KEY0==0)||(WK_UP==1)))
{
SysTick_ms(10); //消抖
key_up=0;
if(KEY2==0)return 1; //控制LED0
else if(KEY1==0)return 2; //控制LED1
else if(KEY0==0)return 3; //控制蜂鸣器
else if(WK_UP==1)return 4; //LED反转
}
else if((KEY2==1)&&(KEY1==1)&&(KEY0==1)&&(WK_UP==0))
key_up=1;
return 0;
}
这里有个消抖,要重视一下,对于我们这种按键,在按下的时候,电信号会有一个抖动,如下图:
消抖有两种方式,硬件消抖和软件消抖,我们用的就是软件消抖,利用延时的方法,去除抖动。
根据返回值,我们就可以做相应的动作:
int main(void)
{
unsigned int key; //定义一个变量接收返回值
System_clock(9); //打开HSE高速时钟
LED(); //LED灯初始化
KEY(); //按键初始化
GPIOB_ODR&=~(1<<5); //打开LED0
while(1)
{
key=KEY_SCANF(); //获取键值
if(key)
{
switch(key)
{
case 1:
PB5=~(PB5); //LED灯取反,若亮就灭,灭就亮
break;
case 2:
PE5=~(PE5); //LED灯取反,若亮就灭,灭就亮
break;
case 3:
PB8=~(PB8); //蜂鸣器取反
break;
case 4:
PB5=~(PB5);
PE5=~(PE5);
break;
}
}
SysTick_ms(10);
}
}
三、总程序
//--------------APB2使能时钟寄存器------------------------
#define RCC_APB2ENR *((unsigned volatile int*)0x40021018)
//----------------GPIOA配置寄存器-------------------------
#define GPIOA_CRL *((unsigned volatile int*)0x40010800)
#define GPIOA_IDR *((unsigned volatile int*)0x40010808)
//----------------GPIOB配置寄存器-------------------------
#define GPIOB_CRL *((unsigned volatile int*)0x40010C00)
#define GPIOB_CRH *((unsigned volatile int*)0x40010C04)
#define GPIOB_ODR *((unsigned volatile int*)0x40010C0C)
//----------------GPIOE配置寄存器 ------------------------
#define GPIOE_CRL *((unsigned volatile int*)0x40011800)
#define GPIOE_ODR *((unsigned volatile int*)0x4001180C)
#define GPIOE_IDR *((unsigned volatile int*)0x40011808)
//--------------------位带定义--------------------------------
#define PB5 *((unsigned volatile int*)0x42218194)
#define PE5 *((unsigned volatile int*)0x42230194)
#define PB8 *((unsigned volatile int*)0x422181A0)
//------------------RCC时钟寄存器-------------------------
#define RCC_CR *((unsigned volatile int*)0x40021000)
#define RCC_CFGR *((unsigned volatile int*)0x40021004)
//--------------FLASH闪存存储器接口-----------------------
#define FLASH_ACR *((unsigned volatile int*)0x40022000)
//---------------------按键配置---------------------------
#define WK_UP ((GPIOA_IDR&(1<<0))?1:0) //WK_UP->PA0
#define KEY0 ((GPIOE_IDR&(1<<4))?1:0) //KEY0->PE4
#define KEY1 ((GPIOE_IDR&(1<<3))?1:0) //KEY1->PE3
#define KEY2 ((GPIOE_IDR&(1<<2))?1:0) //KEY2->PE2
//-----------------SysTick寄存器地址----------------------
#define SysTick_Base 0xE000E010
#define SysTick ((SysTick_Typedef*)SysTick_Base)
//-----------------SysTick寄存器定义----------------------
typedef struct
{
volatile unsigned long CTRL; //控制和状态寄存器
volatile unsigned long RELOAD; //重装载寄存器
volatile unsigned long VAL; //当前值寄存器
volatile unsigned long CALIB; //校准寄存器
}SysTick_Typedef;
//------------------系统时钟配置---------------------------
void System_clock(unsigned char PLL)
{
unsigned int Clock_OK;
RCC_CR|=1<<16; //开启HSE高速外部时钟
while(!(RCC_CR&(1<<17))); //等待HSE开启成功
RCC_CFGR|=4<<8; //0x00000400 AHB不分频;APB2不分频;APB1二分频
FLASH_ACR|=0x2; //FLASH缓冲
RCC_CFGR|=1<<16; //HSE输出作为PLL输入时钟
PLL=PLL-2; //选择PLL倍频2--9
RCC_CFGR|=PLL<<18; //PLL九倍频输出
RCC_CR|=1<<24; //PLL使能
while(!(RCC_CR&(1<<25))); //等待PLL使能成功
RCC_CFGR|=2<<0; //选择PLL为系统时钟
do //等待系统时钟设置成功
{
Clock_OK=RCC_CFGR&0x0c;
}
while(Clock_OK!=0x08);
}
//设置完成后系统时钟:SYSCLK=72MHZ;AHB:HCLK=72MHZ;APB2:PCLK=72MHZ;APB1:PCLK1=36MHZ
//----------------------滴答定时器---------------------------
void SysTick_ms(unsigned int time)
{
unsigned long num;
SysTick->VAL=0; //计数器清零
SysTick->RELOAD=9000*time; //重装载计数值
SysTick->CTRL|=1<<0; //定时器使能,打开定时器
do
{
num=SysTick->CTRL;
}
while((num&0x01)&&!(num&(1<<16))); //等待计数器到0
SysTick->CTRL&=~(1<<0); //关闭计数器
SysTick->VAL=0; //计数器清零
}
//-----------------------按键检测---------------------------
unsigned int KEY_SCANF()
{
static unsigned int key_up=1;
if(key_up&&((KEY2==0)||(KEY1==0)||(KEY0==0)||(WK_UP==1)))
{
SysTick_ms(10); //消抖
key_up=0;
if(KEY2==0)return 1; //控制LED0
else if(KEY1==0)return 2; //控制LED1
else if(KEY0==0)return 3; //控制蜂鸣器
else if(WK_UP==1)return 4; //LED反转
}
else if((KEY2==1)&&(KEY1==1)&&(KEY0==1)&&(WK_UP==0))
key_up=1;
return 0;
}
//-----------------------按键初始化---------------------------
void KEY(void)
{
RCC_APB2ENR|=1<<2; //GPIOA时钟
RCC_APB2ENR|=1<<6; //GPIOE时钟
GPIOA_CRL&=0xFFFFFFF0; //引脚初始化
GPIOA_CRL|=0x00000008; //PA0下拉
GPIOE_CRL&=0xFFF000FF; //引脚初始化
GPIOE_CRL|=0x00088800; //PE2、PE3、PE4上拉
GPIOE_ODR|=7<<2; //PE2上拉
}
//------------------------LED初始化---------------------------
void LED(void)
{
RCC_APB2ENR|=1<<3; //APB2-GPIOB外设时钟使能
RCC_APB2ENR|=1<<6; //APB2-GPIOE外设时钟使能
GPIOB_CRL&=0xFF0FFFFF; //设置位清零
GPIOB_CRL|=0x00200000; //PB5推挽输出
GPIOB_ODR|=1<<5; //设置初始灯为灭
GPIOE_CRL&=0xFF0FFFFF; //设置位清零
GPIOE_CRL|=0x00200000; //PE5推挽输出
GPIOE_ODR|=1<<5; //设置初始灯为灭
GPIOB_CRH&=0xFFFFFFF0; //引脚初始化
GPIOB_CRH|=0x00000002; //PB8推挽
GPIOE_ODR&=~(1<<8); //蜂鸣器不工作
}
//-------------------------主函数-----------------------------
int main(void)
{
unsigned int key; //定义一个变量接收返回值
System_clock(9); //打开HSE高速时钟
LED(); //LED灯初始化
KEY(); //按键初始化
GPIOB_ODR&=~(1<<5); //打开LED0
while(1)
{
key=KEY_SCANF(); //获取键值
if(key)
{
switch(key)
{
case 1:
PB5=~(PB5);
break;
case 2:
PE5=~(PE5);
break;
case 3:
PB8=~(PB8);
break;
case 4:
PB5=~(PB5);
PE5=~(PE5);
break;
}
}
SysTick_ms(10);
}
}
建议大家写程序的时候,养成一个好习惯,多用自定义函数,自定义函数就是把一个一个功能模块化了,在主函数只需要调用一下,代码看起来简洁,不杂乱,后期调试的时候也不影响别的模块,如果代码全写在主函数,在后期维护时,会带来很大的不便。