本喵这次介绍的实验是按键实验,同样使用STM32F103ZE型号的芯片,在开发板上有4个按键,实验目的是每按一个按键就会让LED灯或者是蜂鸣器的工作状态发生反转,下面本喵来详细介绍。
IO口在单片机的使用中是最多的,此次实验用到了LED灯,蜂鸣器,以及3个按键,所以也使用到了IO口。
叫做端口数据输入寄存器(GPIOX_IDR),读取它的信息就能获得IO口上的状态,是作输入使用的。
这是它每一位的对应关系,可以看到,IDR寄存器是一个32位的寄存器,但是只有低16位在使用,而且是只读类型的,高16位保留,如果读的话结果就是0。低16位从0到15对应的该组IO口标号从0到15引脚上的数据信息,而且只能以16位的形式读取数据。
LED灯和蜂鸣器的使用会用到端口数据输出寄存器(GPIOX_ODR),在跑马灯实验中曾详细介绍过GPIO里一些寄存器的使用,有兴趣的小伙伴可以移步到跑马灯实验这篇文章中参考一下。
时钟的配置是使用任何一个外设都要设置的,这里也不例外,同样在跑马灯实验这篇文章中讲述过时钟使能的配置。
LED0与PB5相连,LED1与PE5相连,详情参考跑马灯实验这篇文章。
我们来看开发板原理图。
可以将BEEP看作是蜂鸣器,它与PB8相连。
这是它的具体电路,当BEEP是低电平时,蜂鸣器不会响,当BEEP是高电平时,通过三极管的放大作用,就会使蜂鸣器发出响声。
可以看到,按键KEY0与PE4相连,按键KEY1与PE3相连,按键KEY2与PE2相连。
按键WK_UP与PA0相连。
根据按键的电路图可以发现,KEY0,KEY1,KEY2这三个按键的另一端是和地相连接的,也就是当按键按下去时是低电平,未按下时是高电平,所以这3个键采用上拉输入模式。
WK_UP键的另一端连接的是3.3V的高电平,当键按下去时是高电平,未按下时是低电平,所以采用下拉输入模式。
当有键按下并且没有松开,只作一次键输入,不当作连续输入。
led.h中的代码
#ifndef __LED_H
#define __LED_H
void LED_Init(void);
#endif
led.c中的代码
#include "stm32f10x.h"//顶级头文件引用
#include "led.h"
void LED_Init()
{
GPIO_InitTypeDef GPIO_InitStruct;//创建GPIO结构体变量
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB|RCC_APB2Periph_GPIOE,ENABLE);//PB组和PE组IO口时钟使能
GPIO_InitStruct.GPIO_Mode=GPIO_Mode_Out_PP;//推挽输出
GPIO_InitStruct.GPIO_Pin=GPIO_Pin_5;//引脚5
GPIO_InitStruct.GPIO_Speed=GPIO_Speed_50MHz;//输出最大速度是50MHZ
GPIO_Init(GPIOB,&GPIO_InitStruct);//PB5初始化
GPIO_SetBits(GPIOB,GPIO_Pin_5);//PB5初始化为高电平,LED0灭
GPIO_Init(GPIOE,&GPIO_InitStruct);//PE5初始化
GPIO_SetBits(GPIOE,GPIO_Pin_5);//PE5初始化为高电平,LED1灭
}
beep.h中的代码
#ifndef __BEEP_H
#define __BEEP_H
void BEEP_Init(void);
#endif
beep.c中的代码
#include "stm32f10x.h"//顶级头文件引用
#include "beep.h"
void BEEP_Init()
{
GPIO_InitTypeDef GPIO_InitStruct;//创建GPIO结构体变量
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);//在LED初始化函数中已经将PB组IO口使能过了,这里可以不使能
GPIO_InitStruct.GPIO_Mode=GPIO_Mode_Out_PP;//推挽输出
GPIO_InitStruct.GPIO_Pin=GPIO_Pin_8;//引脚5
GPIO_InitStruct.GPIO_Speed=GPIO_Speed_50MHz;//输出最大速度是50MHZ
GPIO_Init(GPIOB,&GPIO_InitStruct);//PB8初始化
GPIO_ResetBits(GPIOB,GPIO_Pin_8);//PB5初始化为低电平,蜂鸣器不响
}
key.h中的代码
#ifndef __KEY_H
#define __KEY_H
#include "stm32f10x.h"//引用顶级头文件
#include "sys.h"//使用位带操作的头文件
#define KEY0 GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_4)
#define KEY1 GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_3)
#define KEY2 GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_2)
#define WK_UP GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0)//宏定义按键名就是IO的值
#define KEY0_PRES 1
#define KEY1_PRES 2
#define KEY2_PRES 3
#define WK_UP_PRES 4//定义键按下后返回的值
#define LED0 PBout(5)
#define LED1 PEout(5)
#define beep PBout(8)//使用位带操作加宏定义,直接对硬件名字进行操作
void KEY_Init(void);
uint8_t KEY_Scan(void);//键盘扫描函数
#endif
这里充分使用了宏定义,将每个硬件的名字都赋予了意义,在主程序中直接对硬件名字操作以及使用就可以。
为了方便改变LED灯以及蜂鸣器的状态,我们使用位带操作的方法,这样就可以直接对LED灯以及蜂鸣器的状态进行取反。
其中unit8_t类型的变量是STM32固件库中定义的变量类型。
typedef unsigned char uint8_t;
可以看到,它的本质是一个无符号的char类型,大小只有一个字节。因为STM32的空间比较小,尽量使用占内存较小的变量。
key.c中的代码
#include "stm32f10x.h"
#include "delay.h"
#include "key.h"
void KEY_Init()
{
GPIO_InitTypeDef GPIO_InitStruct;//创建GPIO结构体变量
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE|RCC_APB2Periph_GPIOA,ENABLE);//使能PE和PA组IO口
GPIO_InitStruct.GPIO_Mode=GPIO_Mode_IPU;//设置成上拉输入
GPIO_InitStruct.GPIO_Pin=GPIO_Pin_4|GPIO_Pin_3|GPIO_Pin_2;//PE4,PE3,PE2引脚,也就是KEY0,KEY1,KEY2按键
GPIO_Init(GPIOE,&GPIO_InitStruct);//按键初始化为上拉输入
GPIO_InitStruct.GPIO_Mode=GPIO_Mode_IPD;//设置为下拉输入
GPIO_InitStruct.GPIO_Pin=GPIO_Pin_0;//PA引脚,也就是WK_UP按键
GPIO_Init(GPIOA,&GPIO_InitStruct);//按键初始化为下拉输入
}
uint8_t KEY_Scan()
{
static uint8_t key = 1;//储存键的状态,没有键按下时是1
if(key&&(KEY0==0|KEY1==0|KEY2==0|WK_UP==1))//是否有键按下
{
delay_ms(10);//延时消抖
if(key&&(KEY0==0|KEY1==0|KEY2==0|WK_UP==1))//是否真的有键按下
{
key=0;//有键按下,储存为0
if(KEY0==0)
return KEY0_PRES;//KEY0按下
else if(KEY1==0)
return KEY1_PRES;//KEY1按下
else if(KEY2==0)
return KEY2_PRES;//KEY2按下
else if(WK_UP==1)//最好加上if,如果只有else的话可除了前面三个键的情况还可能有其他情况干扰WK_UP键的状态
return WK_UP_PRES;//WK_UP按下
}
}
else if(KEY0==1&&KEY1==1&&KEY2==1&&WK_UP==0)//为了会更加灵敏
key=1;//没有键按下,储存1
return 0;//没有键按下,返回0
}
键盘扫描函数的功能就是扫描每个与硬件相连按键的IO口的状况,然后通过这个状况来判断是否有键按下,不同的键按下对应着不同的处理。
注意:
判断按键是否按下时,需要进行消抖处理,通常消抖都是通过延时消抖,还可以采用消抖电路来消抖。
消抖的原因:
由于外界干扰,以及按键上的弹簧产生的机械作用,按键上的电平会在没有人按下的时候发生微小的变化,如果不作消抖处理,CPU就会将这些由其他因素引起的电平变化当作是键输入来处理,这样就产生了BUG,所以我们要进行消抖。
当检测到按键上有电平变化时,进行延时处理,一般10ms就够。如果是其他因素引起的电平变化,10ms之内就电平就会恢复,如果是用户按键导致的电平变化,那么在这10ms内就不会恢复到原来的电平状态,就可以认为是有键输入,并且进行相应的处理。
是不是有个疑问?
如果我在10ms内按下键并松开,是不是CPU就不会当作键输入处理?
是这样的,只要你手速够快,就可以骗过CPU,但是我们平常按键时,按一下并松开的时长肯定会超过10ms的。
让按键更加灵敏
else if(KEY0==1&&KEY1==1&&KEY2==1&&WK_UP==0)
这是判断没有键输入的语句,这种情况是与有键输入的情况对立的。如果写成只有一个else就笼统的概括没有键输入的情况,这样就会导致按键不灵敏,有时候需要按好几次才会出现对应的反应。
main.c中的代码
#include "led.h"
#include "beep.h"
int main()
{
delay_init();//延时初始化
LED_Init();//led初始化
BEEP_Init();//蜂鸣器初始化
KEY_Init();//按键初始化
while(1)
{
uint8_t ret=KEY_Scan();
if(ret)
{
switch(ret)
{
case KEY0_PRES:
LED0=!LED0;
LED1=!LED1;//同时反转俩个LED灯
break;
case KEY1_PRES:
LED0=!LED0;//反转LED0
break;
case KEY2_PRES:
LED1=!LED1;//反转LED1
break;
case WK_UP_PRES:
beep=!beep;//蜂鸣器状态反转
break;
}
}
else
delay_ms(10);//每隔10ms扫描一次键盘
}
}
当按下右键时,同时控制俩个LED灯的状态发生反转,当按下下键和左键则分别只控制一个LED灯的状态发生反转,当按下上键时,蜂鸣器的状态发生反转。
不支持连续按键
上电后,初始化状态是LED灯不亮,蜂鸣器不响,当有键输入后LED以及蜂鸣器的状态会发生改变。
连续按键也就是按着一个键不动的时候,LED以及蜂鸣器的状态不会发生改变,仅作一次键输入处理。
uint8_t KEY_Scan()
{
static uint8_t key = 1;//储存键的状态,没有键按下时是1
if(key&&(KEY0==0|KEY1==0|KEY2==0|WK_UP==1))//是否有键按下
{
delay_ms(10);//延时消抖
if(key&&(KEY0==0|KEY1==0|KEY2==0|WK_UP==1))//是否真的有键按下
{
//key=0;//有键按下,储存为0
if(KEY0==0)
return KEY0_PRES;//KEY0按下
else if(KEY1==0)
return KEY1_PRES;//KEY1按下
else if(KEY2==0)
return KEY2_PRES;//KEY2按下
else if(WK_UP==1)//最好加上if,如果只有else的话可除了前面三个键的情况还可能有其他情况干扰WK_UP键的状态
return WK_UP_PRES;//WK_UP按下
}
}
else if(KEY0==1&&KEY1==1&&KEY2==1&&WK_UP==0)
key=1;//没有键按下,储存1
return 0;//没有键按下,返回0
}
支持连续按只需要将key=0注释掉就可以,此时,键是否按下就不会参照前一次按键的状态,一只按照不放就是一只都有键输入处理,就像家里的遥控器调音量一样,按照音量键不放音量就会不停的发生变化。
支持连续按键
当连续按键,也就是按着一个键不放的时候,LED灯会有轻微的闪烁,其实LED灯的状态是在不停的连续变化的,由于变化的太快,只能看到轻微的闪烁。当按着控制蜂鸣器的按键不放时,可以听到蜂鸣器的响声不是连续的,而是有一些断续,这也说明蜂鸣器的状态在不停的发生改变,由于改变的太快,只能听到就轻微的间隔。
该实验使用的外设主要就是GPIO,LED灯和蜂鸣器是IO口的输出,按键是IO口的输入,掌握好IO口相关的知识就很容易做出来。