1 硬件连接
1.1 mini
1.2 战舰
1.3 探索者
KEY0->PE4 上拉输入 KEY1->PE3 上拉输入 KEY2->PE2 上拉输入 WK_UP->PA0 下拉输入
按键输入最关键的是对按键的初始化和输入判断。这里按键的初始化与LED的初始化不同的是GPIO的模式不一样了,LED是推挽输出,按键则相反,是输入,这个时候要考虑是哪一种输入方式,这里当我的板子上按键是共阴极的,当按键按下的时候IO口输入的低电平,所以我需要在IO口接上拉电阻,使用上拉输入模式;这里当我的板子上按键是共阳极的,当按键按下的时候IO口输入的高电平,所以我需要在IO口接下拉电阻,使用下拉输入模式。
2 GPIO输操作说明
读取IO口输入电平调用库函数:uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
读取IO口输入电平操作寄存器:GPIOx_IDR:端口输入寄存器
使用位带操作读取IO口输入电平:PEin(4) ------------读取GPIOE.4口电平 PEin(n)------------读取GPIOE.n口电平
3 按键输入实验
按键消抖:我们采用延时消抖的思想,因为按键是机械开关,所以在按下的时候会产生电平的抖动,这在51单片机中经常使用。我个人认为延时消抖应该是一种思想。
第一步先检查按键是否按下,如果按下了,那么就延时20毫秒,再次判断按键是否按下,确保其不是因为机械抖动造成的误判断。两次判断以后,其结果基本确定,如果按键按下了,就点亮LED灯,然后再加入一个死循环,等待按键被松开。就是通过一个等待按键松开的函数来跳出循环,达到按键锁存的目的。
对于按键输入来说,怎么判断是连续按,还是不连续按,这是一个重点。以前是将两种情况分开考虑,写成了两个函数,然后判断,其实,这个可以写成一个函数,只需添加一个选择形参,然后对形参进行判断操作即可。
按键扫描(支持连续按)的一般思路:如果我要实现:按键按下,没有松开,只能算按下一次,这个函数无法实现。
u8 KEY_Scan(void) u8 key_Scan(u8 mode)
{ {
if(KEY按下) if(KEY0 == 0 || KEY1 == 0|| WK_UP == 1) //key按键按下
{ {
delay_ms(10);/*延时10-20ms,防抖。*/ delay_ms(10);
if(KEY确实按下) if(KEY0 == 0) // if(key 确实按下)
{ return KEY0_PRES; // 返回按键值
return KEY_Value; else if(KEY1 == 0)
} return KEY1_PRES;
return 无效值; else if(WK_UP == 1)
} return WKUP_PRES;
} }
else if(KEY1 == 1 && KEY0 == 1 && WK_UP == 0)
return 0; //没有按键按下 返回无效值
}
按键扫描(不支持连续按)的一般思路:不支持连续按:就是说,按键按下了,没有松开,只能算一次。
u8 KEY_Scan(void) u8 key_Scan(u8 mode)
{ {
static u8 key_up=1; static u8 key_up = 1; //按键松开标志
if(key_up && KEY按下) if(key_up && (KEY0 == 0 || KEY1 == 0|| WK_UP == 1))
{ {
delay_ms(10);//延时,防抖 delay_ms(10);
key_up=0;//标记这次key已经按下 key_up = 0;
if(KEY确实按下) if(KEY0 == 0) //if(key确实按下)
{ return KEY0_PRES; // return 键值
return KEY_VALUE; else if(KEY1 == 0)
} return KEY1_PRES;
}else if(KEY没有按下) key_up=1; else if(WK_UP == 1)
return 没有按下 return WKUP_PRES;
} }
else if(KEY1 == 1 && KEY0 == 1 && WK_UP == 0)
key_up = 1;
return 0;
}
按键扫描(两种模式合二为一)的一般思路:
u8 KEY_Scan(u8 mode) u8 key_Scan(u8 mode)
{ {
static u8 key_up=1; static u8 key_up = 1; //按键松开标志
if(mode==1) key_up=1;//支持连续按 if(mode)
if(key_up && KEY按下) key_up = 1;
{ if(key_up && (KEY0 == 0 || KEY1 == 0|| WK_UP == 1))
delay_ms(10);//延时,防抖 {
key_up=0;//标记这次key已经按下 delay_ms(10);
if(KEY确实按下) key_up = 0;
{ if(KEY0 == 0)
return KEY_VALUE; return KEY0_PRES;
} else if(KEY1 == 0)
}else if(KEY没有按下) key_up=1; return KEY1_PRES;
return 没有按下 else if(WK_UP == 1)
} return WKUP_PRES;
}
else if(KEY1 == 1 && KEY0 == 1 && WK_UP == 0)
key_up = 1;
return 0;
}
在主函数中 执行到这条 key=KEY_Scan(0)命令 ,也就是mode=0,那么开始执行该函数。定义一个关键字static 变量 key_up,并给其赋值为1。if(mode)key_up=1; 不执行。执行if(key_up&&(KEY0==0||KEY1==0||WK_UP==1)) ,假设此时按下KEY1且不松开,则执行该if后面的语句。delay_ms(10);目的是去抖动。将key_up赋值为0,然后函数返回值为1(KEY0_PRES在宏定义中表示为1),跳出函数,继续执行主函数,使得LED1灯亮。程序继续走,再一次走到key=KEY_Scan(0),调用KEY_Scan函数,这时候由于key_up在上一次被赋值为0了,故此时if(key_up&&(KEY0==0||KEY1==0||WK_UP==1)) 也不执行,直接执行else if(KEY0==1&&KEY1==1&&KEY2==1&&KEY3==0)key_up=1;此时key_up才重新变为1,然后return 0;那么主函数中key=0,则不执行if(key);故此时LED1灯状态不变,依然亮。这样就实现了第一种模式,即按键连续按的时候,灯/蜂鸣器的状态不改变。(其他的形式类似)
下面贴一个正点原子论坛中一位老哥的解释:key_up只是作为一个标志,if(key_up&&(KEY0==0||KEY1==0||KEY2==0||KEY3==1))这里边判断按键按下是要跟key_up相与的,也就是当你有键按下时还需要key_up为1才能让程序读取到按键值。key_up作为静态变量只会被初始化一次(也就是static u8 key_up=1只会执行一次),每一次读取到一个按键值后key_up就会被置0,让它重新变为1只有两种情况:
(1)else if(KEY0==1&&KEY1==1&&KEY2==1&&KEY3==0)key_up=1;这里就并不是连续按了,只要你按键松开了,下次继续读取没问题。
(2)你如果是想连续按着不动又希望程序能多次读取到的话,必须通过if(mode)key_up=1; 这一句就能让key_up恢复1,这样就实现了不需要松开按键就让它恢复1,即可实现连续按键并且程序连续读取。
(1)使能按键对应IO口时钟。调用函数:RCC_APB2PeriphClockCmd();
(2)初始化IO模式:上拉/下拉输入。调用函数:GPIO_Init();
(3)扫描IO口电平(库函数/寄存器/位操作)。
/************mian.c************/
#include "sys.h"
#include "delay.h"
#include "led.h"
#include "key.h"
int main(void)
{
vu8 key=0;
delay_init(); //延时函数初始化
LED_Init(); //LED端口初始化
KEY_Init(); //初始化与按键连接的硬件接口
BEEP_Init(); //初始化蜂鸣器端口
LED0=0; //先点亮红灯
while(1)
{
key=KEY_Scan(0); //得到键值
if(key)
{
switch(key)
{
case WKUP_PRES: //控制蜂鸣器
BEEP=!BEEP;
break;
case KEY2_PRES: //控制LED0翻转
LED0=!LED0;
break;
case KEY1_PRES: //控制LED1翻转
LED1=!LED1;
break;
case KEY0_PRES: //同时控制LED0,LED1翻转
LED0=!LED0;
LED1=!LED1;
break;
}
}else delay_ms(10);
}
}
/************key.h************/
#ifndef __KEY_H
#define __KEY_H
#include "sys.h"
//#define KEY0 PEin(4) //PE4
//#define KEY1 PEin(3) //PE3
//#define KEY2 PEin(2) //PE2
//#define WK_UP PAin(0) //PA0 WK_UP
#define KEY0 GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_4) //读取按键0
#define KEY1 GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_3) //读取按键1
#define KEY2 GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_2) //读取按键2
#define WK_UP GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0) //读取按键3(WK_UP)
#define KEY0_PRES 1 //KEY0按下
#define KEY1_PRES 2 //KEY1按下
#define KEY2_PRES 3 //KEY2按下
#define WKUP_PRES 4 //KEY_UP按下(即WK_UP/KEY_UP)
void KEY_Init(void); //IO初始化
u8 KEY_Scan(u8); //按键扫描函数
#endif
/************key.c************/
#include "stm32f10x.h"
#include "key.h"
#include "sys.h"
#include "delay.h"
//按键初始化函数
void KEY_Init(void) //IO初始化
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOE,ENABLE);//使能PORTA,PORTE时钟
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2|GPIO_Pin_3|GPIO_Pin_4;//KEY0-KEY2
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //设置成上拉输入
GPIO_Init(GPIOE, &GPIO_InitStructure);//初始化GPIOE2,3,4
//初始化 WK_UP-->GPIOA.0 下拉输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; //PA0设置成输入,默认下拉
GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.0
}
//按键处理函数
//返回按键值
//mode:0,不支持连续按;1,支持连续按;
//0,没有任何按键按下
//1,KEY0按下
//2,KEY1按下
//3,KEY2按下
//4,KEY3按下 WK_UP
//注意此函数有响应优先级,KEY0>KEY1>KEY2>KEY3!!
u8 KEY_Scan(u8 mode)
{
static u8 key_up=1;//按键按松开标志
if(mode)key_up=1; //支持连按
if(key_up&&(KEY0==0||KEY1==0||KEY2==0||WK_UP==1))
{
delay_ms(10);//去抖动
key_up=0;
if(KEY0==0)return KEY0_PRES;
else if(KEY1==0)return KEY1_PRES;
else if(KEY2==0)return KEY2_PRES;
else if(WK_UP==1)return WKUP_PRES;
}else if(KEY0==1&&KEY1==1&&KEY2==1&&WK_UP==0)key_up=1;
return 0;// 无按键按下
}
注意此函数有响应优先级:KEY0>KEY1>KEY2>KEY_UP !!
4 C语言关键字 :static
4.1 局部变量
普通局部变量是再熟悉不过的变量了,在任何一个函数内部定义的变量(不加static修饰符)都属于这个范畴。编译器一般不对普通局部变量进行初始化,也就是说它的值在初始时是不确定的,除非对其显式赋值。普通局部变量存储于进程栈空间,使用完毕会立即释放。静态局部变量使用static修饰符定义,即使在声明时未赋初值,编译器也会把它初始化为0。且静态局部变量存储于进程的全局数据区,即使函数返回,它的值也会保持不变。变量在全局数据区分配内存空间;编译器自动对其初始化;其作用域为局部作用域,当定义它的函数结束时,其作用域随之结束。静态局部变量的效果跟全局变量有一拼,但是位于函数体内部,就极有利于程序的模块化了。
4.2 全局变量
全局变量定义在函数体外部,在全局数据区分配存储空间,且编译器会自动对其初始化。普通全局变量对整个工程可见,其他文件可以使用extern外部声明后直接使用。也就是说其他文件不能再定义一个与其相同名字的变量了(否则编译器会认为它们是同一个变量)。静态全局变量仅对当前文件可见,其他文件不可访问,其他文件可以定义与其同名的变量,两者互不影响。在定义不需要与其他文件共享的全局变量时,加上static关键字能够有效地降低程序模块之间的耦合,避免不同文件同名变量的冲突,且不会误使用。
4.3 Static变量
Static的用途主要有两个:
一是用于修饰存储类型使之成为静态存储类型
二是用于修饰链接属性使之成为内部链接属性。
1)静态存储类型:
在函数内定义的静态局部变量,该变量存在内存的静态区,所以即使该函数运行结束,静态变量的值不会被销毁,函数下次运行时能仍用到这个值。
在函数外定义的静态变量——静态全局变量,该变量的作用域只能在定义该变量的文件中,不能被其他文件通过extern引用。
2) 内部链接属性
静态函数只能在声明它的源文件中使用。
下例中,每次调用getValue函数之后,返回值是多少?
int getValue(void) int getValue(void)
{ {
int flag=0; static int flag=0;
flag++; flag++;
return flag; return flag;
} }