单片机软件常用设计分享(一)驱动设计之按键设计
前言
本人从事单片机软件设计工作多年,既从事过裸机系统的设计,也在小型嵌入式实时操作系统下进行过设计。因在工作当中发现好多人对单片机软件的设计非常随意,尤其是在驱动方面,其考虑问题过于单一,并且应用层与底层驱动之间耦合度较大。基于此,本人整理工作当中做过常用的最基本设计分享到这里,希望对需要的人有所帮助或参考。当然,可能我的设计方法并不是很好,所以也算是一种学习交流。
在整理的过程中,可能会缺乏统一规划,仅先整理一部分常用的驱动设计。至于其它部分的内容,待今后跟进需要再逐一补充。
《驱动设计–按键驱动》
好多人对单片机的按键设计,往往是只作简单的I/O检测或A/D检测,并未考虑到防抖及其它更多功能的问题,同时也因为耦合度较高带来可移植性差的问题。
一般来说,应该将按键驱动进行分层设计,包括底层初始化、底层硬件扫描、按键扫描逻辑处理、功能码输出、功能解析等等。并需要设计扫描码与功能码(设计扫描码可以很方便的处理组合按键),除防抖设计外,同时需要考虑功能码输出时具有多种按键类型(包括按键按下、长按及长按时间、按键释放等)。甚至,必要的按键解析处理也应该一并考虑,只是这部分具体应该如何处理,则需要留给应用层决定。
说明:以下在描述时只列出了相关部分代码,但会在最后给出完整代码。
另外,在完成数码屏驱动调试后,对此驱动做了修改,主要是不再依赖应用层必须提供const uint32_t FunCode[]定义。
一、按键扫描方式
按键一般有2种扫描方式,I/O电平检测与AD电压检测,按键的扫描函数应该由具体的应用层去处理(根据具体的硬件电路进行设计),这也是降低耦合度的关键之一。
1.I/O电平检测
I/O电平检测又分为2种。
1)一个按键的一端连接到一个普通I/O口上,另外一端接地。这种方式仅适用于按键较少的情况;
2)一个按键的一端连接到一个行扫描I/O上,另外一端连接到列扫描I/O口上,这样I/O口将分为行扫描组与列扫描组。这种方式适用于按键较多的情况;
2.AD电压检测
这种方式实际上是将一个电压通过多个电阻进行均等分压,每个分压点接入一个按钮的一端,按钮的另一端接地。这种方式在一个AD口不适合连接太多的按钮,因为其受分压间距的影响,如果间距太小,则会出现检测误差或错误,甚至这种方式本身就依赖于电压的稳定性。同时,这种方式对于组合按键的处理一般不会太理想,这需要将硬件分压电阻设计的非常合理。
二、按键驱动数据结构
按键驱动数据结构设计,包括按键数据、按键参数、按键扫描执行状态、按键扫描驱动函数等等,以下分别描述各个部分内容。
1.按键数据
按键数据包括:扫描码、功能码,驱动数据结构设计,以下分别描述各个部分内容;
1)扫描码
扫描码的设计至少可以使用两种方式。一种是设计为1个bit对应1个按键,这样一个字节可以对应独立的8个按键,同时还可以表示众多的组合按键;另一种是设计为枚举类型,每一个按键或组合按键对应一个枚举值;后一种方式在生成的扫描码上不能做组合按键,必须在扫描函数中完成,并且此时的扫描码与功能码区别不是很大(严格来讲这个扫描码已经是功能码了);
这里以第一种方式设计扫描码,如有5个按键:上(bit0)、下(bit1)、左(bit2)、右(bit3)、确定(bit4)。具体定义如下(实际可设计为1–4个字节<这里以4个字节举例>);
注:扫描码应该由具体的应用层去设计,因为每个项目可能存在区别。
//扫描码定义<生成32bit表示的扫描码宏定义> [需要根据项目实际情况修改定义]
#define BitShift(key) (0x00000001<<(key))
//按键序号定义(可定义从0--31)
#define KEY_SERIAL_UP 0
#define KEY_SERIAL_DN 1
#define KEY_SERIAL_LF 2
#define KEY_SERIAL_RT 3
#define KEY_SERIAL_EN 4
//基本扫描码
//#define KEY_SCAN_NO 0x00 //扫描码-无按键
#define KEY_SCAN_UP BitShift(KEY_SERIAL_UP) //扫描码-按键上
#define KEY_SCAN_DN BitShift(KEY_SERIAL_DN) //扫描码-按键下
#define KEY_SCAN_LF BitShift(KEY_SERIAL_LF) //扫描码-按键左
#define KEY_SCAN_RT BitShift(KEY_SERIAL_RT) //扫描码-按键右
#define KEY_SCAN_EN BitShift(KEY_SERIAL_EN) //扫描码-按键确认
#define KEY_SCAN_MAX KEY_SCAN_EN
//组合扫描码(使用位定义扫描码生成组合按键非常容易)
#define KEY_SCAN_UPDN (BitShift(KEY_SERIAL_UP)|BitShift(KEY_SERIAL_DN))//扫描码-按键上+按键下(组合键)
#define KEY_SCAN_LFRT (BitShift(KEY_SERIAL_LF)+BitShift(KEY_SERIAL_RT))//扫描码-按键左+按键右(组合键)
2)功能码
功能码可以表示4种类型,按键按下、短释放、按键长按、长释放,之所以设计这4种类型的功能码,是考虑到软件可能会使用不同的按键状态来做处理。比如,检测按键执行某个功能,是以按下时执行还是按下弹起后执行,或者是长按执行还是长按释放执行,甚至是长按几秒后执行等等,这些都与需求或用户体验为基础进行考虑并设计。以上4种类型的功能码将以基本功能码为基础,使用宏定义生成。基本功能码完全可以使用自然数来定义,如有5个按键如上描述,使用4个字节表示功能码。
注:功能码应该也由具体的应用层去设计。
A,基本功能码定义
//基本功能码
//#define KEY_NO 0x00
#define KEY_UP 0x01 //基本功能码-按键上
#define KEY_DN 0x02 //基本功能码-按键下
#define KEY_LF 0x03 //基本功能码-按键左
#define KEY_RT 0x04 //基本功能码-按键右
#define KEY_EN 0x05 //基本功能码-键按确认
#define KEY_UPDN 0x06 //基本功能码-按键上+按键下(组合键)
#define KEY_LFRT 0x07 //基本功能码-按键左+按键右(组合键)
#define KEY_MAX KEY_LFRT
B,功能码宏定义
使用功能码的高4位来表示类型,低24位为基本功能码。
/*---------------------------------------------------------------------------------------------------
生成功能码宏定义<功能码定义四种类型>
1, 按键按下
2, 按键短释放
3, 按键长按
4, 按键长释放
---------------------------------------------------------------------------------------------------*/
#define SHORT_PRESS 0x00
#define SHORT_BREAK 0x01
#define LONG_PRESS 0x02
#define LONG_BREAK 0x03
#define SHPKEY(key) ((key)+(SHORT_PRESS<<24)) //按键按下
#define SHBKEY(key) ((key)+(SHORT_BREAK<<24)) //按键短释放
#define LGKEY(key) ((key)+(LONG_PRESS<<24)) //按键长按
#define LGBKEY(key) ((key)+(LONG_BREAK<<24)) //按键长释放
C,功能码定义
功能码的定义在一个应用中应该是可以统一的。
#define KEY_NO_KEY 0x00 //无功能码
//按键按下
#define KEY_UP_PRESS SHPKEY(KEY_UP)
#define KEY_DN_PRESS SHPKEY(KEY_DN)
#define KEY_LF_PRESS SHPKEY(KEY_LF)
#define KEY_RT_PRESS SHPKEY(KEY_RT)
#define KEY_EN_PRESS SHPKEY(KEY_EN)
#define KEY_UPDN_PRESS SHPKEY(KEY_UPDN)
#define KEY_LFRT_PRESS SHPKEY(KEY_LFRT)
//按键短释放
#define KEY_UP_BREAK SHBKEY(KEY_UP)
#define KEY_DN_BREAK SHBKEY(KEY_DN)
#define KEY_LF_BREAK SHBKEY(KEY_LF)
#define KEY_RT_BREAK SHBKEY(KEY_RT)
#define KEY_EN_BREAK SHBKEY(KEY_EN)
#define KEY_UPDN_BREAK SHBKEY(KEY_UPDN)
#define KEY_LFRT_BREAK SHBKEY(KEY_LFRT)
//按键长按
#define KEY_UP_LONG LGKEY(KEY_UP)
#define KEY_DN_LONG LGKEY(KEY_DN)
#define KEY_LF_LONG LGKEY(KEY_LF)
#define KEY_RT_LONG LGKEY(KEY_RT)
#define KEY_EN_LONG LGKEY(KEY_EN)
#define KEY_UPDN_LONG LGKEY(KEY_UPDN)
#define KEY_LFRT_LONG LGKEY(KEY_LFRT)
//按键长释放
#define KEY_UP_LONG_BREAK LGBKEY(KEY_UP)
#define KEY_DN_LONG_BREAK LGBKEY(KEY_DN)
#define KEY_LF_LONG_BREAK LGBKEY(KEY_LF)
#define KEY_RT_LONG_BREAK LGBKEY(KEY_RT)
#define KEY_EN_LONG_BREAK LGBKEY(KEY_EN)
#define KEY_UPDN_LONG_BREAK LGBKEY(KEY_UPDN)
#define KEY_LFRT_LONG_BREAK LGBKEY(KEY_LFRT)
2.按键参数
按键参数主要是扫描参数,其涉及到扫描按键时各个状态或阶段的执行时间。
其中有两个参数被放在了tScan结构中。
typedef struct
{
uint16_t scanUnit; //按键扫描时间单位
uint16_t jitterPressCntMax; //按键按下抖动检查时间
uint16_t jitterReleaseCntMax; //按键弹起抖动检查时间
uint16_t keepCntEnsure; //按键按下首次持续时间
uint16_t keepCntLongStart; //按键按下首次判断长按时间
uint16_t keepCntLongKeep; //按键按下持续长按时间间隔
}tScanParam;//扫描参数
typedef struct
{
uint8_t state; //扫描状态
uint8_t pressCnt; //扫描到同时按键个数
uint8_t keepCnt; //按键按下计时器,向下计数,单位为扫描时间,如10ms
uint8_t jitterPressCnt; //按下抖动计时器,向上计数
uint8_t jitterReleaseCnt; //释放抖动计时器,向上计数
uint16_t curKey; //当前扫描码
uint16_t prevKey; //上次扫描码
}tScan;//按键扫描
3.按键扫描执行状态
按键扫描分为几个状态:按键按下、抖动检测、确认按下、长按键、等待释放、按键释放;
按键按下:在按键释放状态,检测到有任意按键按下时进入此状态;
抖动处理:在按键按下或释放时检测到相反状况,进入此状态进行抖动处理(这是一个可并行的状态);
确认按下:按键按下并持续一定时间后,则进入此状态,产生按键按下功能码(基本功能码);
长按键:确认按键已经按下,并在一定时间之后,检测到按键仍然按下,则进入此状态,并产生长按功能码;
等待释放:在检测到按键弹起并执行抖动处理成功后,进入此状态,并在结束此状态时产生短按键释放或长按键释放功能码(这是一个可并行的状态);
按键释放:按键按下时抖动处理失败,或等待释放成功,设置到此状态,并可重新开始检测新的按键按下检测;
各个扫描状态的定义如下:
#define SKEY_STATE_RELEASE 0 //按键释放
#define SKEY_STATE_PUSH 1 //按键按下
#define SKEY_STATE_PRESS 2 //确认按下
#define SKEY_STATE_PRESS_LONG 3 //长按键
#define SKEY_STATE_JITTER 0x40 //抖动处理
#define SKEY_STATE_WAITRELEASE 0x80 //等待释放
#define SKEY_STATE_MASK 0x0f //互斥的状态屏蔽字
4.按键扫描驱动函数
1)扫描码与基本功能码默认映射
扫描码映射到基本功能码,可以使用最简单的查表映射方法,但这个方法在按键个数较多时进行填表就有些繁琐,并且占用FLASH较多。当然,这个方法的优点也显而易见,也就是算法简单的不能再简单了。如果你的应用有好的算法,也可以传递你应用层的映射算法函数给驱动即可。所以,这也是降低耦合度的一个设计。
FunCode同样也应该由应用层定义
const uint32_t FunCode[KEY_SCAN_MAX+1]=
{
//以[0xdd]方式表示的,扫描码对应功能码就存在,反之没有
KEY_NO,KEY_UP,KEY_DN,KEY_UPDN,KEY_LF, //0x00,[0x01],[0x02],[0x03],[0x04]
KEY_NO,KEY_NO,KEY_NO,KEY_RT,KEY_NO, //0x05,0x06,0x07,[0x08],0x09
KEY_NO,KEY_NO,KEY_LFRT,KEY_NO,KEY_NO, //0x0a,0x0b,[0x0c],0x0d,0x0e
KEY_NO,KEY_EN, //0x0f,[0x10]
};
//定义默认的扫描码映射函数
uint32_t KeyMapDef(uint32_t scanCode)
{
return FunCode[scanCode];
}
2)由应用层提供的安装函数(原型)
typedef void(*tKeyInit)(void); //按键底层初始化及去初始化函数原型
/*---------------------------------------------------------------------------------------------------
按键底层初始化及去初始化函数原型
* 摘要: 执行按键的底层硬件初始化功能或去初始化
* 参数: 无
* 返回: 无
* 说明: 如果应用层希望自行控制底层初始化等操作,则将其设置为NULL即可.
---------------------------------------------------------------------------------------------------*/
typedef tKeyMsg*(*tKeyPollFunc)(void); //按键POLL函数原型
/*---------------------------------------------------------------------------------------------------
按键POLL函数原型
* 摘要: 执行按键的POLLING功能,驱动的主要功能均在此完成
* 参数: 无
* 返回: tKeyMsg* 返回的按键消息.
---------------------------------------------------------------------------------------------------*/
*/
typedef uint8_t(*tKeyScanFunc)(uint32_t*); //按键扫描函数原型
/*---------------------------------------------------------------------------------------------------
按键扫描函数原型
* 摘要: 执行按键的硬件扫描功能
* 参数: uint32_t* 接收扫描码结果寄存器指针
* 返回: uint8_t 扫描到的按键数量
* 说明: 此功能应用层必须提供,否则无法执行按键扫描
---------------------------------------------------------------------------------------------------*/
typedef void(*tKeyParseFunc)(tKeyMsg*); //按键解析函数原型
/*---------------------------------------------------------------------------------------------------
按键解析函数原型
* 摘要: 执行按键的功能处理
* 参数: tKeyMsg* 按键消息指针
* 返回: 无
* 说明: 应用层如希望直接在驱动内执行按键处理,则需要将按键处理函数传递给驱动.如其执行时间较长,则最好
* 放在应用层处理.
---------------------------------------------------------------------------------------------------*/
typedef uint32_t(*tKeyMapFunc)(uint32_t); //按键扫描码映射函数原型
/*---------------------------------------------------------------------------------------------------
按键扫描码映射函数原型
* 摘要: 执行从扫描码映射到基本功能码的操作
* 参数: uint32_t 按键扫描码
* 返回: uint32_t 按键基本功能码
* 说明: 应用层如果直接使用驱动的默认映射方式,则只需要将其设置为NULL.如果希望自行编写映射方式,或执行更
* 多的操作,则可以将编写的映射函数设置到此.
---------------------------------------------------------------------------------------------------*/
5.按键驱动数据结构
1)按键值
按键值包括扫描码与功能码,其定义如下:
typedef struct
{
uint32_t scan; //扫描码
uint32_t func; //功能码
}tKeyValue;//按键值
2)按键消息
按键消息包括按键长按时间与按键值,其定义如下:
typedef struct
{
uint32_t time;