平台无关的嵌入式通用按键管理器
本文代码仓库地址:https://gitee.com/holymiao/Platform-independent-Embedded-Universal-Key-Manager.git
本文是在《通用的按键代码(上)》和《通用按键代码(下)》两篇文章的基础上添加组合按键重新整理架构后编写而来;
中间用到了 双向链表库 ,使用方法可以参考《手搓算法(3) 双向链表 支持头尾遍历、排序、增删、正负数索引、查找》一文;
程序架构用到了 面向对象 的C语言设计方法,牵扯 结构体的继承、多态、封装、内部函数 等功能,技术点可以参考《面向对象的C语言编程》
由于本代码跟上一篇程序代码的结构变化较大(这是本项目经理考虑不周导致,这也就是为啥让程序员添加一个小功能会得罪对方的原因 ),但也不是推翻重做,所以代码不讲也不合适,从头到尾再讲一遍也不合适。
所以这里 只编写使用方法,具体实现方法,大家可以根据上一篇文章的基础,再根据代码的注释自行分析。
1.按键库的功能和特点
功能:
- CPU、架构无关的按键驱动,只需提供基本接口即可畅游;
- 自带独立按键、矩阵按键、AD模拟按键功能的识别,需要用户提供基本的IO读写或AD值获取函数就能应用;
- 留出接口,方便用户扩展其他类型的按键;
- 支持获取按键的 按下、弹开、双击、长按 的事件获取;支持 组合键;
- 支持 中断模式 获取按键事件,也支持 轮询模式 查看按键值;
- 支持添加 按键字符对应表 ,方便按键和字符进行对应,可随时 注册,更改和卸载;
- 支持 按键分组 ,可以把不同功能或者不同驱动方式的按键分组处理,让一个项目中支持多种按键项目,且可以随时注册和卸载按键组。
2.跟上一篇使用方法的区别(总览)
大家可以根据下边的条目,对比《通用的按键代码(上)》和《通用按键代码(下)》分析代码的异同。
区别:
- 添加组合按键(导致程序结构大改)
- 把之前的按键组的事件回调删除,添加按键事件总驱动,统一处理单个按键和组合按键的问题;
- 按键值支持组合按键,故按键值从keyValue_t类型替换成keyValueArr_t类型,可以保存多个按键值;相关的按键事件标志从keyValue_t类型挪到keyValueArr_t类型中;
- 按键组获取按键值也可能是多个,故基类的getKeyValue的返回值改成void,并在基类中添加组合按键值combinKeyValues,getKeyValue()函数获取的值放到combinKeyValues中处理,对应的派生类相关的函数接口都要跟着变化;
- 按键管理器的初始化函数keyManagerInit添加组合按键识别时间参数,保证用户在按下组合按键稳定之后再识别组合事件;
- 添加组合按键值的添加删除函数,供程序或者继承类中相关程序调用;
- 因为添加组合按键,故独立按键必须知道当前组有多少个按键,所以必须重新设计独立按键类,添加按键个数count,根据索引获取IO电平的函数指针getKey两个类成员。相应的,独立按键组分配函数也要做相关修改;
- 按键事件分析函数keyManagerScankeyEvent在原程序的基础上添加了组合按键的识别代码。
3.独立按键的使用方法
这里使用stm32HAL库为例进行讲解,主要是我不想自己编写硬件初始化代码。 后边其他例子也做相关处理不再赘述。
3.1 外设设置准备:
- cubemx软件生成调试串口(并编写printf支持的程序,这里不再讲解);
- 生成20ms定时中断,当然也可以使用我之前编写的定时管理器,详情看这篇文章《通用的定时事件管理器》.
在定时中断里边调用keyManagerScankeyEvent() 函数,用户驱动按键的状态识别。
C
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
keyManagerScankeyEvent();
}
- 把库中的"KeyManager.c"、"KeyManager.h"、"LinkList.c"、"LinkList.h"添加并导入到工程中,导入方法这里不再详解;
- 根据自己的开发板硬件电路,添加几个按键,这里以四个按键为例,名字分别改为"key1"、"key2"、"key3"、"key4"。
注意,我这个板子key1~key3接低电平,故IO设置为输入上拉,而key4接高电平,故IO口需要设置成输入下拉,看代码时务必注意!!!

3.2 主程序中调用流程
咱们先讲主程序如何配置,然后再说需要哪些回调函数。
调用流程如下:
- 初始化串口、GPIO、定时器等相关的硬件;
C
MX_GPIO_Init();
MX_TIM6_Init();
MX_USART1_UART_Init();
HAL_TIM_Base_Start_IT(&htim6);
- 初始化按键管理器,注册超时时间及事件回调函数keyEvent和应用参数,这里"事件回调函数keyEvent"需要我们手动去编写;
C
//长按识别时间1s,双击识别时间100ms,组合按键识别事件100ms
//事件回调函数keyEvent,为NULL表示没有回调函数
//回调参数设置为NULL,表示没有,也可以设置成其他数据的指针
keyManagerInit(1000, 100, 100, keyEvent, NULL);
- 分配独立按键的设备空间,这里为了分组 (炫技),把四个按键分成两组,key1、key2一组,key3和key4一组,所以设备空间分成两个,每组俩个按键;
这里出现两个"获取IO电平的回调函数getkeyGroup1、getkeyGroup2"需要我们手动去实现。
C
//每组两个按键,并注册回调函数
key_base_t *keyGroup1 = creatIndepKey(2, getkeyGroup1);
key_base_t *keyGroup2 = creatIndepKey(2, getkeyGroup2);
- 注册两个按键组,根据顺序,分配的ID分别是1号和二号,可以通过注册函数的返回值来确认;这里第一组按键值从1开始,第二组按键值从10开始;
C
uint8_t id1 = keyManagerAddGroup(keyGroup1, 1);//按键值从1开始
uint8_t id2 = keyManagerAddGroup(keyGroup2, 10);//按键值从10开始
- 注册按键字符对应表,让按键对应字符,某些场合下适用。对应表分为全局对应表GlobalKeyCharTab和局部对应表KeyCharTab,可以共用,其中局部表优先级最高。
另外,程序运行时调用下边的函数可以随时更新按键值,实现不同场合不同按键功能的作用。
出现的GlobalKeyCharTab、KeyCharTab两个数组需要我们手动去实现。
C
RegistIdKeyCharTab(GlobalKeyCharTab, 2);
RegistKeyCharTab(keyGroup2, KeyCharTab, 2);
- 至此,初始化全部完成,期间需要我们实现一个事件函数 keyEvent 、两个回调函数 keyGroup1、keyGroup2 ,两个对应表 GlobalKeyCharTab、KeyCharTab。
3.3 编写按键事件回调函数
因为回调函数的原型如下:
C
/// @brief 按键对应的事件函数指针类型
/// @param value 按键值,内含事件类型、组别、按键值
/// @param data 用户传入数据的地址,需要用户自行管理指针类型
typedef void (*fun_keyEvent_t)(keyValueArr_t* value, void *data);
故需要在用户程序中按照这个规则编写回调函数。
在此函数中,分析是否是组合键,分析按键的组别ID和键值,并显示此按键对应的字符。
(实际应用的时候 不要把按下事件当做按键的处理事件,因为信息太杂了)
C
void keyEvent(keyValueArr_t* values, void *data)
{
if(values->state == KEY_EVENT_COMBIN) { //识别是组合键
printf("组合按键,分别是");
for(int i = 0; i < values->count; i++) { //逐次显示组合键信息
printf("id:%d,value:%d;",values->valueArr[i].id,values->valueArr[i].keyValue);
}
} else { //不是组合键,则只需处理第一个数据即可
//分析按键的事件类型
switch(values->state) {
case KEY_EVENT_DOWN:printf("按下事件,");break;
case KEY_EVENT_UP:printf("弹开事件,");break;
case KEY_EVENT_LONG:printf("长按事件,");break;
case KEY_EVENT_DOUBLE:printf("双击事件,");break;
}
//显示组别ID和键值
printf("id:%d,value:%d;",values->valueArr[0].id,values->valueArr[0].keyValue);
//显示按键对应的字符
printf("按键字符是%c", GetKeyChar(values->valueArr[0]));
}
printf("\r\n");
}
3.3 编写IO读取函数
独立按键在分配空间时,需要用到如下回调原型:
C
/// @brief 独立按键获取IO状态的函数
/// @param index IO索引,从0开始计数
/// @return 返回IO按下状态,IO_pressed_state表示按下,IO_unpressed_state表示未按下
typedef IO_state_t (*fun_getIO_t)(uint16_t index);
按照这个规则编写独立按键IO读取函数。
这里把四个按键分成两组,key1、key2一组,key3和key4一组,(再次强调,key4另一端连接高电平,其状态跟其他按键正好相反),所以需要两个IO读取函数。
C
//第一组,识别key1和key2
IO_state_t getkeyGroup1(uint16_t index)
{
GPIO_PinState retValue = GPIO_PIN_SET;
switch(index) {
case 0: retValue = HAL_GPIO_ReadPin(key1_GPIO_Port, key1_Pin); break;
case 1: retValue = HAL_GPIO_ReadPin(key2_GPIO_Port, key2_Pin); break;
}
//hal库的GPIO_PinState和按键库的IO_state_t类型是兼容的,故可以强制转换,下同
return (IO_state_t)retValue;
}
//第一组,识别key3和key4
IO_state_t getkeyGroup2(uint16_t index)
{
GPIO_PinState retValue = GPIO_PIN_SET;
switch(index) {
case 0: retValue = HAL_GPIO_ReadPin(key3_GPIO_Port, key3_Pin); break;
//注意!!!
//key4按键按下时为高电平,未按时为低电平,跟其他按键状态相反,故这里需要取反
case 1: retValue = (GPIO_PinState)(!HAL_GPIO_ReadPin(key4_GPIO_Port, key4_Pin)); break;
}
return (IO_state_t)retValue;
}
3.4 编写按键字符对应表
按键字符对应表有两种,可以同时使用(多个按键可以对应一个字符,某些按键也可以不设置字符)。
- 带ID的全局按键字符对应表,所有按键组都可以用一张表来对应,属于低优先级对应表:
C
//全局按键字符对应表
const IdKeyChar_t GlobalKeyCharTab[] =
{
{{1, 1}, 'a'},
{{1, 2}, 'b'},
{{2, 10}, 'c'}, //第二组按键值从10开始
{{2, 11}, 'd'},
};
- 不带ID的按键组内部字符对应表,只适用于某一组别内部按键字符识别,优先级高;
C
const KeyChar_t KeyCharTab[] =
{
{10, 'A'}, //第二组按键值从10开始
{11, 'B'}
};
3.5 执行结果
编译下载后,按下按键,串口打印信息如下,大家对着事件函数keyEvent()自行分析

4. 矩阵按键的声明方法
矩阵键盘的声明方法跟独立按键方法大同小异。在独立按键的代码基础上直接添加
- cubemx中添加四个行IO,名字分别是row0~row3,设置成 输出模式 ;添加四个列IO,名字分别是col0~col3,设置成 输入上拉模式;
- 在主程序中为矩阵按键分配设备空间,这里出现两个回调函数 setRow, getCol:
C
//行列都是4个IO,注册设置行IO函数setRow、读取列IO函数getCol
key_base_t *keyGroup3 = creatMatrixKey(4, 4, setRow, getCol);
- 把矩阵键盘组注册到按键管理器中,按键值从1开始。
C
//矩阵键盘按键值从1开始,这就跟上边第一组独立按键键值重叠
//不用担心,两个组的ID不一样
uint8_t id3 = keyManagerAddGroup(keyGroup3, 1);
- 编写字符对应表,可以修改全局表GlobalKeyCharTab,也可以重新注册矩阵表;这里选择后者:
C
//4*4计算器小键盘字符对应表
const KeyChar_t MatrixKeyCharTab[] =
{
{1, '7'}, {2, '8'}, {3, '9'}, {4, '+'},
{5, '4'}, {6, '5'}, {7, '6'}, {8, '-'},
{9, '1'}, {10, '2'}, {11, '3'}, {12, '*'},
{13, 'C'}, {14, '0'}, {15, '='}, {16, '/'},
};
RegistKeyCharTab(keyGroup3, MatrixKeyCharTab, 16);
- 至此初始化工作已完成,需要实现两个回调函数setRow, getCol;
4.1 设置行IO高低电平的函数setRow
函数原型如下:
C
/// @brief 设置行IO电平状态(根据上下拉不一样,高低电平也不一样,一般来说,用户将"列"设置上拉,则行设置成IO_pressed_state表示为低电平)
/// @param index 行索引,从0开始
/// @param state 行IO状态,IO_pressed_state表示设置为识别电平(若"列"IO为上拉,则此处为低电平),IO_unpressed_state表示设置为未识别电平(若"列"IO为上拉,则此处为高电平)
typedef void (*fun_setRow_t)(uint16_t index, IO_state_t state);
由此编写的函数如下:
C
void setRow(uint16_t index, IO_state_t state)
{
switch(index) {
case 0: HAL_GPIO_WritePin(row0_GPIO_Port, row0_Pin, (GPIO_PinState)state); break;
case 1: HAL_GPIO_WritePin(row1_GPIO_Port, row1_Pin, (GPIO_PinState)state); break;
case 2: HAL_GPIO_WritePin(row2_GPIO_Port, row2_Pin, (GPIO_PinState)state); break;
case 3: HAL_GPIO_WritePin(row3_GPIO_Port, row3_Pin, (GPIO_PinState)state); break;
}
}
4.2 获取列IO电平函数getCol
函数原型如下:
C
/// @brief 获取列IO高低电平的状态
/// @param index 列IO索引,从0开始
/// @return IO状态,IO_pressed_state表示此列被识别按下(若列IO为上拉,则此处低电平为识别状态),IO_unpressed_state表示此列识别未按下(若列IO为上拉,则此处高电平为未识别状态)
typedef IO_state_t (*fun_getCol_t)(uint16_t index);
由此编写的函数如下:
C
IO_state_t getCol(uint16_t index)
{
GPIO_PinState retValue = GPIO_PIN_SET;
switch(index) {
case 0: retValue = HAL_GPIO_ReadPin(col0_GPIO_Port, col0_Pin); break;
case 1: retValue = HAL_GPIO_ReadPin(col1_GPIO_Port, col1_Pin); break;
case 2: retValue = HAL_GPIO_ReadPin(col2_GPIO_Port, col2_Pin); break;
case 4: retValue = HAL_GPIO_ReadPin(col3_GPIO_Port, col3_Pin); break;
}
return (IO_state_t)retValue;
}
4.3 总结
编译运行下载,其实验现象跟独立按键一模一样,这里不再赘述。
5. AD模拟按键的声明方法
AD模拟按键的声明方法跟上边的代码类似,在矩阵代码的基础上添加如下内容:
- cubemx分配一个AD引脚,进型适当的配置,软件触发或定时触发都没问题,这里以软件触发为例。在主程序上初始化AD模块,按键值;
- 给AD模块分配内存,注册AD值获取函数getAD、电压识别范围表ADtab;
C
//这里分配8个按键
key_base_t *keyGroup4 = creatADKey(getAD, ADtab, 8);
- 把AD按键注册到按键管理器中:
C
//按顺序来说,id为4,按键值从1开始
uint8_t id4 = keyManagerAddGroup(keyGroup4, 1);
- 给模拟按键添加字符识别数组,这里方法跟上边一样,不再赘述;
5.1 获取AD的回调函数
函数原型:
C
//获取AD值的回调函数
typedef uint16_t (*fun_GetAD_t)(void);
由此编写代码:
C
uint16_t getAD(void)
{
HAL_ADC_Start(&hadc1); //软件触发ADC
HAL_ADC_PollForConversion(&hadc1,1);//查询函数
return HAL_ADC_GetValue(&hadc1); //得到ADC的
}
5.2 模拟按键的电压范围识别表
先分析下边的图,假如配置的是12位AD,则最大值是4096;

- 当s1按下时,ADC_KEY口电压为0V,AD值是 0;
- S2按下时,分压150/(150+1000),对应的AD值是4096*150/(150+1000)=534;
- S3按下时,分压(150+240)/(150+240+1000),对应的AD值是4096*(150+240)/(150+240+1000)=1390;
- S4按下时,对应的AD值是4096*(150+240+360)/(150+240+360+1000)=1755;
- S5按下时,对应的* AD值是4096*(150+240+360+620)/(150+240+360+620+1000)=2367;
- S6按下时,对应的AD值是4096*(150+240+360+620+1000)/(150+240+360+620+1000+1000)=2880;
- S7按下时,对应的AD值是4096*(150+240+360+620+1000+3600)/(150+240+360+620+1000+3600+1000)=3508;
- S8按下时,对应的AD值是4096*(150+240+360+620+1000+3600+30000)/(150+240+360+620+1000+3600+30000+1000)=3985;
由此我们得到每个按键的AD值识别范围:
C
const ADKeyTab_t ADTab[] = {
{0, 200}, {300, 700}, {1000, 1550}, {1600, 2000},
{2100, 2500}, {2600, 3000}, {3300, 3700}, {3800, 4000}};
5.3 总结
模拟按键的初始化工作已经全部完成,实验结果跟上边一致。
注意:AD按键内部不支持组合键,因为两个按键按下后,输出电压值只有一个;但AD按键可以跟其他组按键进行组合识别。
6. 自定义按键的添加方法
独立按键、矩阵键盘、AD模拟按键是目前最常用的三种按键,可满足九成以上的嵌入式需求。
但架不住还有一些特殊按键,比如电磁炉的触摸按键等,该如何自定义添加呢?
思路如下:
- 想办法实现按键基类中的 getKeyValue 函数,比如独立按键的readIndepKey()、矩阵键盘的scanMatrixKey()、AD键盘的scanADKey();
- 为了实现上边的函数必须注册相关用户处理函数回调,还要注册按键个数,比如独立按键的getKey()、矩阵键盘的getCol()和setRow()、AD按键的getAD();且按键个数和用户回调都要放到继承类里边;
- 继承类中添加必要的其他参数,例如AD键盘的类的ADTab识别数组;
大家可以根据KeyManager.c中独立按键、矩阵按键、AD按键的代码体会其中的方法,编写自己的按键程序(比如触摸按键识别程序)。