新闻中心

EEPW首页 > 嵌入式系统 > 牛人业话 > 【单片机到嵌入式之路】序列之4:你的按键还活着么?

【单片机到嵌入式之路】序列之4:你的按键还活着么?

作者:时间:2015-05-24来源:网络收藏

  硬件平台单片机、ARM等等

本文引用地址:http://www.eepw.com.cn/article/274621.htm

  编译环境MDK5.0

  硬件工具配合MCU

  主要文件无

  作 者@量子CPU(747764222)

  本节我们将学习让你恼火的按键,主要是从下面4个方面进行讲解:

  1.按键触发方式迫在眉睫

  2.按键扫描

  3.中断扫描

  4.按键状态机制

  一、按键触发方迫在眉睫

  如果你在公司上班,抑或你在学校,当你做的项目任务进度比较多的时候,你还在使用按键扫描,也许老板会说你SB,老师说你无能。这就是现实,单片机和ST序列的目前还是单核的,怎么可能一直在做扫描,你大才小用了吧。你把精华浪费在做无聊的事情上面,现在你评价一下你自己吧,我不多说。O(∩_∩)O哈哈~

  亲,你的按键还活着么?你的按键会苦恼你么?如果你还在考虑按键用扫描方式,那可能有一天要被人骂了,如果想要面子,又让你的按键有活力。Follow me !!!

  你不会按键的三种方式,你妈知道么?(*^__^*) 嘻嘻……

  下面以单个独立按键来讲解,矩阵按键可以思想是一样的。

  

 

  二、按键扫描方式

  按键扫描的方式,这应该是大家比较熟悉的,因为大部分单片机入门的时候,碰到按键的时候,都是通过扫描方式来实现。

  按键扫描的原理:CPU需要不停的工作,来判断IO口是否被拉低或者置高,效率比较低。按键扫描主要是处理消抖。

  STM32为例讲解

  /***************************************

  * 函数描述:按键初始化函数

  * 输入参数:No

  * 返 回 值:No

  * 说 明:初始化按键

  * 修改记录:

  ****************************************/

  void KEY_Init(void)

  {

  GPIO_InitTypeDef GPIO_InitStruct;

  RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOA, ENABLE);

  GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0; //按键PA0

  GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN; //输入模式

  GPIO_InitStruct.GPIO_Speed = GPIO_Speed_Level_2;

  GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_DOWN; //下拉输入

  GPIO_Init(GPIOA, &GPIO_InitStruct);

  }

  /***************************************

  * 函数描述:按键扫描函数

  * 输入参数:No

  * 返 回 值:No

  * 说 明:扫描按键

  * 修改记录:

  ****************************************/

  uint8_t KEY_Scan(GPIO_TypeDef* GPIOx,uint16_t GPIO_Pin)

  {

  if(GPIO_ReadInputDataBit(GPIOx,GPIO_Pin) == 0 ) //检测是否有按键按下

  {

  Delay(10000); //延时消抖

  if(GPIO_ReadInputDataBit(GPIOx,GPIO_Pin) == 0 )

  {

  while(GPIO_ReadInputDataBit(GPIOx,GPIO_Pin) == 0); //等待按键释放 */

  return 0 ;

  }

  else

  return 1;

  }

  else

  eturn 1;

  }

  /***************************************

  * 函数描述:部分主函数

  * 输入参数:No

  * 返 回 值:No

  * 说 明:

  * 修改记录:

  ****************************************/

  while(1)

  {

  if( KEY_Scan (GPIOA,GPIO_Pin_0) ==0)//判定按键是否按下

  {

  GPIO_WriteBit(GPIOC, GPIO_Pin_9,

  (BitAction)((1-GPIO_ReadOutputDataBit(GPIOC, GPIO_Pin_9))));//反转led2灯

  }

  }

  这就是按键扫描,至于缺点不言而喻。这不就是苦逼的CPU的么?O(∩_∩)O哈哈~http://bbs.ickey.cn/group-topic-id-13062.html

  三、中断扫描

  在你对单片机比较熟悉的时候,在你考虑多任务的时候,你应该被按键扫描烦死了,这时候,你绞尽脑汁,终于想到了—————————— 中断扫描。

  中断扫描的原理:中断控制效率很高,一旦系统IO口出现上升或者下降沿电平就会触发执行中断内的程序。这样MCU就无需一直在扫描,这样你就可以干其他的时候,而且不会觉得按键不灵活。

  同样以STM32为例讲解:

  说明:STM32用IO口外部中断的一般步骤:

  1.初始化IO口为输入;

  2.开启IO口时钟,设置 IO 口与中断线的映射关系;

  3.初始化线上中断,设置触发条件等;

  4.配置中断分组(NVIC),并使能中断;

  5.编写中断服务函数。

  /***************************************

  * 函数描述:按键初始化函数

  * 输入参数:No

  * 返 回 值:No

  * 说 明:按键初始化

  * 修改记录:

  ****************************************/

  void KEY_Init(void)

  {

  GPIO_InitTypeDef GPIO_InitStruct;

  RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOA, ENABLE);

  GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0; //按键PA0

  GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN; //输入模式

  GPIO_InitStruct.GPIO_Speed = GPIO_Speed_Level_2;

  GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_DOWN; //下拉输入

  GPIO_Init(GPIOA, &GPIO_InitStruct);

  }

  /***************************************

  * 函数描述:外部中断初始化

  * 输入参数:No

  * 返 回 值:No

  * 说 明:

  * 修改记录:

  ****************************************/

  void EXTI_KEY_Init(void)

  {

  EXTI_InitTypeDef EXTI_InitStruct;

  NVIC_InitTypeDef NVIC_InitStruct;

  KEY_Init();

  RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE); //使能系统时钟配置

  SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOA, EXTI_PinSource0);

  //连接EXTI0给GPIOA0

  SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOA, EXTI_PinSource0);

  //配置GPIO与中断线的映射关系

  EXTI_InitStruct.EXTI_Line = EXTI_Line0; //中断线标号0

  EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt; //外部中断模式

  EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Falling; //下降沿中断

  EXTI_InitStruct.EXTI_LineCmd = ENABLE; //中断线使能

  EXTI_Init(&EXTI_InitStruct);

  NVIC_InitStruct.NVIC_IRQChannel = EXTI0_1_IRQn;

  NVIC_InitStruct.NVIC_IRQChannelPriority = 0x00;

  NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;

  NVIC_Init(&NVIC_InitStruct);

  }

  /***************************************

  * 函数描述:外部中断0服务程序

  * 输入参数:No

  * 返 回 值:No

  * 说 明:

  * 修改记录:

  ****************************************/

  void EXTI0_1_IRQHandler(void)

  {

  if(EXTI_GetITStatus(EXTI_Line0) != RESET)

  //判断线0上的中断是否发生,可以理解为标志位

  {

  /* Toggle LED1and LED2 */

  GPIO_WriteBit(GPIOC, GPIO_Pin_8,

  (BitAction)((1-GPIO_ReadOutputDataBit(GPIOC, GPIO_Pin_8))));

  GPIO_WriteBit(GPIOC, GPIO_Pin_9,

  (BitAction)((1-GPIO_ReadOutputDataBit(GPIOC, GPIO_Pin_9))));

  /* Clear the EXTI line 0 pending bit */

  EXTI_ClearITPendingBit(EXTI_Line0);//清除LINE0上的中断标志位

  }

  }

  /***************************************

  * 函数描述:主函数

  * 输入参数:No

  * 返 回 值:No

  * 说 明:

  * 修改记录:

  ****************************************/

  int main(void)

  {

  SystemInit(); //系统初始化

  LED_Init(); //LED灯初始化

  GPIO_ResetBits(GPIOC,GPIO_Pin_8);

  KEY_Init(); //按键初始化

  EXTI_KEY_Init(); //外部中断初始化

  while(1)

  {

  }

  }

  按键中断的有点是不是不言而喻!!!这不就拯救了苦逼的CPU么?O(∩_∩)O哈哈~

  http://bbs.ickey.cn/group-topic-id-13062.html

  四、按键状态机制

  网上看见一片讲状态机制很经典的文章。借用并转之!!!

  首先按键程序进入初始状态S1,在这个状态下,检测按键是否按下,如果有按下,则进入按键消抖状态2,在下一次执行按键程序时候,直接由按键消抖状态进入按键按下状态3,在此状态下检测按键是否按下,如果没有按键按下,则返回初始状态S1,如果有则可以返回键值,同时进入长按状态S4,在长按状态下每次进入按键程序时候对按键时间计数,当计数值超过设定阈值时候,则表明长按事件发生,同时进入按键连_发状态S5。如果按键键值为空键,则返回按键释放状态S6,否则继续停留在本状态。在按键连_发状态下,如果按键键值为空键则返回按键释放状态S6,如果按键时间计数超过连_发阈值,则返回连_发按键值,清零时间计数后继续停留在本状态。

  看了这么多,也许你已经有一个模糊的概念了,下面让我们趁热打铁,一起来动手编写按键驱动程序吧。

  下面是我使用的硬件的连接图。

  

 

  硬件连接很简单,四个独立按键分别接在P3^0------P3^3四个I/O上面。

  因为51单片机I/O口内部结构的限制,在读取外部引脚状态的时候,需要向端口写1.在51单片机复位后,不需要进行此操作也可以进行读取外部引脚的操作。因此,在按键的端口没有复用的情况下,可以省略此步骤。而对于其它一些真正双向I/O口的单片机来说,将引脚设置成输入状态,是必不可少的一个步骤。

  下面的程序代码初始化引脚为输入。

  void KeyInit(void)

  {

  io_key_1 = 1 ;

  io_key_2 = 1 ;

  io_key_3 = 1 ;

  io_key_4 = 1 ;

  }

  根据按键硬件连接定义按键键值

  #define KEY_VALUE_1 0x0e

  #define KEY_VALUE_2 0x0d

  #define KEY_VALUE_3 0x0b

  #define KEY_VALUE_4 0x07

  #define KEY_NULL 0x0f

  下面我们来编写按键的硬件驱动程序。

  根据第一章所描述的按键检测原理,我们可以很容易的得出如下的代码:

  static uint8 KeyScan(void)

  {

  if(io_key_1 == 0)return KEY_VALUE_1 ;

  if(io_key_2 == 0)return KEY_VALUE_2 ;

  if(io_key_3 == 0)return KEY_VALUE_3 ;

  if(io_key_4 == 0)return KEY_VALUE_4 ;

  return KEY_NULL ;

  }

  其中io_key_1等是我们按键端口的定义,如下所示:

  sbit io_key_1 = P3^0 ;

  sbit io_key_2 = P3^1 ;

  sbit io_key_3 = P3^2 ;

  sbit io_key_4 = P3^3 ;

  KeyScan()作为底层按键的驱动程序,为上层按键扫描提供一个接口,这样我们编写的上层按键扫描函数可以几乎不用修改就可以拿到我们的其它程序中去使用,使得程序复用性大大提高。同时,通过有意识的将与底层硬件连接紧密的程序和与硬件无关的代码分开写,使得程序结构层次清晰,可移植性也更好。对于单片机类的程序而言,能够做到函数级别的代码重用已经足够了。

  在编写我们的上层按键扫描函数之前,需要先完成一些宏定义。

  //定义长按键的TICK数,以及连_发间隔的TICK数

  #define KEY_LONG_PERIOD 100

  #define KEY_CONTINUE_PERIOD 25

  //定义按键返回值状态(按下,长按,连_发,释放)

  #define KEY_DOWN 0x80

  #define KEY_LONG 0x40

  #define KEY_CONTINUE 0x20

  #define KEY_UP 0x10

  //定义按键状态

  #define KEY_STATE_INIT 0

  #define KEY_STATE_WOBBLE 1

  #define KEY_STATE_PRESS 2

  #define KEY_STATE_LONG 3

  #define KEY_STATE_CONTINUE 4

  #define KEY_STATE_RELEASE 5

  接着我们开始编写完整的上层按键扫描函数,按键的短按,长按,连按,释放等等状态的判断均是在此函数中完成。对照状态流程转移图,然后再看下面的函数代码,可以更容易的去理解函数的执行流程。完整的函数代码如下:

  void GetKey(uint8 *pKeyValue)

  {

  static uint8 s_u8KeyState = KEY_STATE_INIT ;

  static uint8 s_u8KeyTimeCount = 0 ;

  static uint8 s_u8LastKey = KEY_NULL ; //保存按键释放时候的键值

  uint8 KeyTemp = KEY_NULL ;

  KeyTemp = KeyScan() ; //获取键值

  switch(s_u8KeyState)

  {

  case KEY_STATE_INIT :

  {

  if(KEY_NULL != (KeyTemp))

  {

  s_u8KeyState = KEY_STATE_WOBBLE ;

  %3

linux操作系统文章专题:linux操作系统详解(linux不再难懂)

51单片机相关文章:51单片机教程


单片机相关文章:单片机教程


单片机相关文章:单片机视频教程


单片机相关文章:单片机工作原理




关键词: 嵌入式

评论


相关推荐

技术专区

关闭