概要
在嵌入式系统中,按键和显示屏是最基本、最常见的用户交互方式。然而,如何设计和实现不同界面之间的交互逻辑,并让用户能够方便、快捷地完成各种操作,是一个需要深思熟虑的问题。本文将从代码的角度出发,介绍如何优化按键和显示屏之间的交互设计,以及如何利用各种技巧和策略来提升产品的市场竞争力。如果您对交互设计有兴趣,或者想要提升自己在嵌入式软件领域的技能和素质,那么这篇文章一定会对您有所帮助。
项目简介
一直说要整理下按键驱动、OLED/LCD显示驱动,小编平时自由时间不多,也是一直拖拖拖,最近刚好做项目中用到了按键、OLED屏,所以趁着这次机会就整理下,分享下我在做不同屏幕下的按键交互以及按键的各种状态检测是怎么实现的,怎么让复杂的交互逻辑代码上变得简化。
硬件介绍
本项目中使用的是Cortex-M0内核的MCU,主频48MHz,32K ROM,8K的RAM,按键使用的是三个独立按键,即每路按键用一个GPIO去检测状态,OLED屏幕使用常用的0.96'' I2C接口SSD1315驱动的128x64分辨率的屏幕。
软件框架
1、OLED的GUI使用开源的SimpleGUI,这套GUI里面集成了基础绘图功能以及能实现一些复杂的动态显示效果(需配合定时器),足够我们在项目上使用。
2、按键检测这里应项目需求实现了按键的短按、长按、双击三种状态,相对来说还是比较简单的,于是小编就自己写了一套逻辑实现按键的检测,使用这个构件只需要把对应的结构体成员完善后就能适配到任意平台,既解耦又便于移植。
3、主控这边原本打算跑一个操作系统的,等把操作系统移植完之后,再加上各种OLED的取模、字库等RAM直接就不够用了,因此又回到了裸机开发,好在项目不算复杂,裸机完全能处理,只是显示的内容比较多。
设计思想
本项目中需要有多个显示界面,在不同的显示界面下还会有不同的按键操作,单按键、多按键组合等,设计之初为了更好的管理按键和OLED屏幕之间的交互逻辑,就采用了消息队列来做,使用消息队列的好处能大大的简化按键和OLED之间各种复杂的交互,当有按键按下时,检测到按键的状态后向消息队列发送一条按键消息,这样我们在不同的显示界面只要接收这个按键消息就知道当前界面下是什么按键被按下,进而处理复杂的交互逻辑。这里消息队列使用的就是之前公众号文章中的环形缓冲区实现的(原文链接)。
按键检测逻辑
我们知道OLED的显存刷新是需要一定的时间的,当代码中需要频繁刷新OLED的显存时,此时裸机去做按键的检测势必会出现延时,让用户觉得按键不灵敏,甚至出现丢键的问题。因此项目中使用定时器做按键扫描,设置扫描周期20ms,每隔20ms对按键进行扫描,同时使用Systick做系统运行时间的统计。下面让我们来看看是怎么利用状态机基于这些逻辑实现按键的短按、长按、双击事件的检测的吧。
我把定时器的初始化以及定时器的中断放在BSP层,下面就来看下bsp_tim.c的代码吧,主要实现定时器的初始化以及中断优先级的配置。
#include "bsp_tim.h" pTimerIntCallback Timer3_IntCallback; /* * @ brief : Timer initialization, Configure prescaling factors and reload values. * @ param : {uint16_t} Prescaler: Prescaler value. {uint16_t} Period : Period value. * @ return: None * @ author: bagy * @ modify: None */ static void BSP_TimerParam_Config(TIM_Module* TIM_ID, uint16_t Prescaler, uint16_t Period) { TIM_TimeBaseInitType Timer_InitStr; Timer_InitStr.Prescaler = Prescaler - 1; Timer_InitStr.Period = Period - 1; Timer_InitStr.ClkDiv = TIM_CLK_DIV1; Timer_InitStr.CntMode = TIM_CNT_MODE_UP; TIM_InitTimeBase(TIM_ID, &Timer_InitStr); /* Clear the update interrupt flag bit */ TIM_ClearFlag(TIM_ID, TIM_FLAG_UPDATE); /* Enable timer update interrupt */ TIM_ConfigInt(TIM_ID, TIM_FLAG_UPDATE, ENABLE); /* Enable Timer */ TIM_Enable(TIM_ID, ENABLE); } /* * @ brief : Timer NVIC configuration, configure interrupt priority. * @ param : None * @ return: None * @ author: bagy * @ modify: None */ static void BSP_Timer_NVIC_Config(void) { NVIC_InitType NVIC_InitStr; NVIC_InitStr.NVIC_IRQChannel = TIM3_IRQn; NVIC_InitStr.NVIC_IRQChannelPriority = TIM3_INT_PRIORITY; NVIC_InitStr.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStr); } /* * @ brief : Timer 3 initialization, Configure prescaling factors and reload values. * @ param : {uint32_t} time : Timing duration, in ms. {pTimerIntCallback} TimerIntCallback: Timer interrupt callback function. * @ return: None * @ author: bagy * @ modify: None */ void BSP_Timer_Config(uint32_t time, pTimerIntCallback TimerIntCallback) { /* Timer peripheral configuration initialization */ BSP_TimerParam_Config(TIM3, TIM_PRESCALER, time * 1000); /* Timer nvic config */ BSP_Timer_NVIC_Config(); Timer3_IntCallback = TimerIntCallback; log_i("Timer config complete. Set timer: %dms.\r\n", time); } /* * @ brief : Timer 3 NVIC configuration, configure interrupt priority. * @ param : None * @ return: None * @ author: bagy * @ modify: None */ void TIM3_IRQHandler(void) { if(TIM_GetIntStatus(TIM3, TIM_INT_UPDATE) != RESET) { /* Clear the update interrupt flag bit */ TIM_ClrIntPendingBit(TIM3, TIM_INT_UPDATE); Timer3_IntCallback(); } }
在BSP的上一层,drv_keyscan.c中会调用上述的初始化接口,并设置回调函数,我们就在回调函数中检测按键的变化。在drv_keyscan.c中需要完善一个结构体,该结构体描述了按键扫描驱动中的所需要的接口以及状态。结构体如下:
typedef struct { uint8_t Key_Down; /* 按键按下的电平状态 */ uint8_t Key_Up; /* 按键抬起的电平状态 */ uint32_t (*pGetSysTime)(void); /* 当前系统时间 */ void (*pBSP_BSP_KeyConfig)(void); /* 按键GPIO的初始化函数 */ uint8_t (*pBSP_ReadKeyStatus)(uint8_t); /* 读取按键的状态接口 */ void (*pBSP_TimerConfig)(uint32_t, pTimerIntCallback); /* 按键扫描定时器的初始化函数 */ } KeyInterface_t;
重点来了,看下是怎么利用上述结构体提供的接口用状态机的思想,实现按键的检测的。
先来看下drv_keyscan.h中的实现
typedef enum { KEY_STATE_IDLE = 0, /* Key idle status */ KEY_STATE_PRESS_DOWN, /* Key press status */ KEY_STATE_RELEASE_UP, /* Key lift status */ KEY_STATE_SHORT_PRESS, /* Key short press state */ KEY_STATE_LONG_PRESS, /* Button long press status */ KEY_STATE_DOUBLE_CLICK /* Key double click status */ } eKeyStatus; typedef struct { uint8_t key_num; eKeyStatus state; /* Key Status */ uint32_t press_time; /* Key press time */ uint32_t release_time; /* Key lift time */ uint32_t last_press_time; /* Last key press time */ } KeyInfo_t;
drv_keyscan.c中的实现
/* 在这里可以添加新的按键进来 */ KeyInfo_t Key_List[] = { {KEY1, KEY_STATE_IDLE, 0, 0, 0}, {KEY2, KEY_STATE_IDLE, 0, 0, 0}, {KEY3, KEY_STATE_IDLE, 0, 0, 0}, }; /* Function declaration */ void drv_Key_DetectLoop(void); /* * @ brief : Pushbutton GPIO configuration initialization and initialize the message queue for the keys, Register the callback function for key scanning. * @ param : None * @ return: None * @ author: bagy * @ modify: None */ void drv_KeyConfig(void) { /* Key hardware interface initialization */ KeyInterface.pBSP_BSP_KeyConfig(); /* Creating a Keystroke Message Queue */ if(Queue_Creat(&KeyMsgQueue, KEY_MSG_MAX) < 0) { log_i("Keystroke message queue creation failure"); while(1); } /* Set the key scan period and callback function */ KeyInterface.pBSP_TimerConfig(KEY_SCAN_CYCLE, drv_Key_DetectLoop); } /* * @ brief : Key scan driver that sends different key messages according to different key states. * @ param : None * @ return: None * @ author: bagy * @ modify: None */ static void drv_KeyScan(KeyInfo_t *pKeyInfo) { static uint32_t curr_time = 0; uint32_t time_diff = 0; /* Get system time */ curr_time = KeyInterface.pGetSysTime(); switch(pKeyInfo->state) { case KEY_STATE_IDLE: { /* Press the button */ if(KeyInterface.pBSP_ReadKeyStatus(pKeyInfo->key_num) == KeyInterface.Key_Down) { pKeyInfo->state = KEY_STATE_PRESS_DOWN; pKeyInfo->press_time = curr_time; return; } }break; case KEY_STATE_PRESS_DOWN: { time_diff = curr_time - pKeyInfo->press_time; /* Button Lift */ if(KeyInterface.pBSP_ReadKeyStatus(pKeyInfo->key_num) == KeyInterface.Key_Up) { pKeyInfo->state = KEY_STATE_RELEASE_UP; pKeyInfo->release_time = curr_time; if(time_diff > KEY_LONG_PRESS_TIME) { pKeyInfo->state = KEY_STATE_LONG_PRESS; } } }break; case KEY_STATE_RELEASE_UP: { time_diff = curr_time - pKeyInfo->press_time; if (time_diff > KEY_DOUBLE_CLICK_TIME) { pKeyInfo->state = KEY_STATE_SHORT_PRESS; } else { if(KeyInterface.pBSP_ReadKeyStatus(pKeyInfo->key_num) == KeyInterface.Key_Down) { /* The button is pressed to enter the double click state */ pKeyInfo->state = KEY_STATE_DOUBLE_CLICK; pKeyInfo->last_press_time = pKeyInfo->press_time; pKeyInfo->press_time = curr_time; } } }break; case KEY_STATE_SHORT_PRESS: { uint8_t KeyMsg_Event = KEY_NONE_EVENT; switch(pKeyInfo->key_num) { case KEY1: KeyMsg_Event = KEY1_SHORT_PRESS_EVENT; break; case KEY2: KeyMsg_Event = KEY2_SHORT_PRESS_EVENT; break; case KEY3: KeyMsg_Event = KEY3_SHORT_PRESS_EVENT; break; } Queue_Send(&KeyMsgQueue, &KeyMsg_Event, 1); log_i("Key %d short press.\r\n", pKeyInfo->key_num); pKeyInfo->state = KEY_STATE_IDLE; }break; case KEY_STATE_LONG_PRESS: { uint8_t KeyMsg_Event = KEY_NONE_EVENT; switch(pKeyInfo->key_num) { case KEY1: KeyMsg_Event = KEY1_LONG_PRESS_EVENT; break; case KEY2: KeyMsg_Event = KEY2_LONG_PRESS_EVENT; break; case KEY3: KeyMsg_Event = KEY3_LONG_PRESS_EVENT; break; } Queue_Send(&KeyMsgQueue, &KeyMsg_Event, 1); log_i("Key %d long press.\r\n", pKeyInfo->key_num); pKeyInfo->state = KEY_STATE_IDLE; }break; case KEY_STATE_DOUBLE_CLICK: { if(KeyInterface.pBSP_ReadKeyStatus(pKeyInfo->key_num) == KeyInterface.Key_Up) { /* The button is lifted and enters the lifted state */ pKeyInfo->state = KEY_STATE_RELEASE_UP; pKeyInfo->release_time = curr_time; if (curr_time - pKeyInfo->last_press_time < KEY_DOUBLE_CLICK_TIME) { uint8_t KeyMsg_Event = KEY_NONE_EVENT; switch(pKeyInfo->key_num) { case KEY1: KeyMsg_Event = KEY1_DOUBLE_CLICK_EVENT; break; case KEY2: KeyMsg_Event = KEY2_DOUBLE_CLICK_EVENT; break; case KEY3: KeyMsg_Event = KEY3_DOUBLE_CLICK_EVENT; break; } Queue_Send(&KeyMsgQueue, &KeyMsg_Event, 1); log_i("Key %d double click.\r\n", pKeyInfo->key_num); pKeyInfo->state = KEY_STATE_IDLE; } } }break; default: break; } } /* * @ brief : Key scan detection cycle. * @ param : None * @ return: None * @ author: bagy * @ modify: None */ static void drv_Key_DetectLoop(void) { /* Scan all keys in the key list */ for(uint16_t i = 0; i < GET_BUFF_SIZE(Key_List); i++) { drv_KeyScan(&Key_List[i]); } }
怎么样,后面移植起来是不是很简单,和下层模块的耦合也降低了。
OLED交互逻辑
其实到这里,相信大家都都有所感悟了,剩下的就更简单了,举个简单的例子。
static void OLED_Display_Screen1(void) { uint8_t KeyMsg = KEY_NONE_EVENT; /* Receive keystroke messages */ Queue_Received(&KeyMsgQueue, &KeyMsg, 1); switch(KeyMsg) { case KEY1_SHORT_PRESS_EVENT: break; case KEY1_LONG_PRESS_EVENT: break; case KEY1_DOUBLE_CLICK_EVENT: break; case KEY2_SHORT_PRESS_EVENT: break; case KEY2_LONG_PRESS_EVENT: break; case KEY2_DOUBLE_CLICK_EVENT: break; case KEY3_SHORT_PRESS_EVENT: break; case KEY3_LONG_PRESS_EVENT: break; case KEY3_DOUBLE_CLICK_EVENT: break; default: break; } KeyMsg = KEY_NONE_EVENT; /* Update Video Memory */ OLED_Interface.fnSyncBuffer(); } /* * @ brief : Jump to the target screen display. * @ param : {eOLED_ScreenType} Obj_Screen: Target display screen. * @ return: None. * @ author: bagy * @ modify: None. */ void JumpToScreen(eOLED_ScreenType Obj_Screen) { static eOLED_ScreenType Lsat_Screen; /* Clear the screen first when switching the page display */ if(Lsat_Screen != Obj_Screen) OLED_Interface.fnClear(); switch(Obj_Screen) { case OLED_DISPLAY_SCREEN1: OLED_Display_Screen1(); break; case OLED_DISPLAY_SCREEN2: OLED_Display_Screen2(); break; case OLED_DISPLAY_SCREEN3: OLED_Display_Screen3(); break; default: break; } Lsat_Screen = Obj_Screen; }
以此类推,在不同界面下接收消息队列中的按键状态,就能在不同的显示界面下,简单的实现各种复杂的处理逻辑。在主循环中就可以使用JumpToScreen()这个接口跳转到任意界面显示。是不是很方便呢。
(注意:本文涉及到的代码只是一个Demo,提供一种编程逻辑,各位如需用到自己的项目上还要添加软件完善代码。)