平台无关的嵌入式通用按键管理器

平台无关的嵌入式通用按键管理器

本文代码仓库地址:https://gitee.com/holymiao/Platform-independent-Embedded-Universal-Key-Manager.git

本文是在《通用的按键代码(上)》和《通用按键代码(下)》两篇文章的基础上添加组合按键重新整理架构后编写而来;

中间用到了 双向链表库 ,使用方法可以参考《手搓算法(3) 双向链表 支持头尾遍历、排序、增删、正负数索引、查找》一文;

程序架构用到了 面向对象 的C语言设计方法,牵扯 结构体的继承、多态、封装、内部函数 等功能,技术点可以参考《面向对象的C语言编程

由于本代码跟上一篇程序代码的结构变化较大(这是本项目经理考虑不周导致,这也就是为啥让程序员添加一个小功能会得罪对方的原因 ),但也不是推翻重做,所以代码不讲也不合适,从头到尾再讲一遍也不合适。

所以这里 只编写使用方法,具体实现方法,大家可以根据上一篇文章的基础,再根据代码的注释自行分析。

1.按键库的功能和特点

功能:

  1. CPU、架构无关的按键驱动,只需提供基本接口即可畅游;
  2. 自带独立按键、矩阵按键、AD模拟按键功能的识别,需要用户提供基本的IO读写或AD值获取函数就能应用;
  3. 留出接口,方便用户扩展其他类型的按键
  4. 支持获取按键的 按下、弹开、双击、长按 的事件获取;支持 组合键
  5. 支持 中断模式 获取按键事件,也支持 轮询模式 查看按键值;
  6. 支持添加 按键字符对应表 ,方便按键和字符进行对应,可随时 注册,更改和卸载
  7. 支持 按键分组 ,可以把不同功能或者不同驱动方式的按键分组处理,让一个项目中支持多种按键项目,且可以随时注册和卸载按键组

2.跟上一篇使用方法的区别(总览)

大家可以根据下边的条目,对比《通用的按键代码(上)》和《通用按键代码(下)》分析代码的异同。

区别:

  1. 添加组合按键(导致程序结构大改)
  2. 把之前的按键组的事件回调删除,添加按键事件总驱动,统一处理单个按键和组合按键的问题;
  3. 按键值支持组合按键,故按键值从keyValue_t类型替换成keyValueArr_t类型,可以保存多个按键值;相关的按键事件标志从keyValue_t类型挪到keyValueArr_t类型中;
  4. 按键组获取按键值也可能是多个,故基类的getKeyValue的返回值改成void,并在基类中添加组合按键值combinKeyValues,getKeyValue()函数获取的值放到combinKeyValues中处理,对应的派生类相关的函数接口都要跟着变化;
  5. 按键管理器的初始化函数keyManagerInit添加组合按键识别时间参数,保证用户在按下组合按键稳定之后再识别组合事件;
  6. 添加组合按键值的添加删除函数,供程序或者继承类中相关程序调用;
  7. 因为添加组合按键,故独立按键必须知道当前组有多少个按键,所以必须重新设计独立按键类,添加按键个数count,根据索引获取IO电平的函数指针getKey两个类成员。相应的,独立按键组分配函数也要做相关修改;
  8. 按键事件分析函数keyManagerScankeyEvent在原程序的基础上添加了组合按键的识别代码。

3.独立按键的使用方法

这里使用stm32HAL库为例进行讲解,主要是我不想自己编写硬件初始化代码。 后边其他例子也做相关处理不再赘述。

3.1 外设设置准备:

  1. cubemx软件生成调试串口(并编写printf支持的程序,这里不再讲解);
  2. 生成20ms定时中断,当然也可以使用我之前编写的定时管理器,详情看这篇文章《通用的定时事件管理器》.
    在定时中断里边调用keyManagerScankeyEvent() 函数,用户驱动按键的状态识别。
C 复制代码
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  keyManagerScankeyEvent();
}
  1. 把库中的"KeyManager.c"、"KeyManager.h"、"LinkList.c"、"LinkList.h"添加并导入到工程中,导入方法这里不再详解;
  2. 根据自己的开发板硬件电路,添加几个按键,这里以四个按键为例,名字分别改为"key1"、"key2"、"key3"、"key4"。
    注意,我这个板子key1~key3接低电平,故IO设置为输入上拉,而key4接高电平,故IO口需要设置成输入下拉,看代码时务必注意!!!

3.2 主程序中调用流程

咱们先讲主程序如何配置,然后再说需要哪些回调函数。

调用流程如下:

  1. 初始化串口、GPIO、定时器等相关的硬件;
C 复制代码
  	MX_GPIO_Init();
  	MX_TIM6_Init();
  	MX_USART1_UART_Init();
	HAL_TIM_Base_Start_IT(&htim6);
  1. 初始化按键管理器,注册超时时间及事件回调函数keyEvent和应用参数,这里"事件回调函数keyEvent"需要我们手动去编写
C 复制代码
//长按识别时间1s,双击识别时间100ms,组合按键识别事件100ms
//事件回调函数keyEvent,为NULL表示没有回调函数
//回调参数设置为NULL,表示没有,也可以设置成其他数据的指针
keyManagerInit(1000, 100, 100, keyEvent, NULL);
  1. 分配独立按键的设备空间,这里为了分组 (炫技),把四个按键分成两组,key1、key2一组,key3和key4一组,所以设备空间分成两个,每组俩个按键;
    这里出现两个"获取IO电平的回调函数getkeyGroup1、getkeyGroup2"需要我们手动去实现
C 复制代码
//每组两个按键,并注册回调函数
key_base_t *keyGroup1 = creatIndepKey(2, getkeyGroup1);
key_base_t *keyGroup2 = creatIndepKey(2, getkeyGroup2);
  1. 注册两个按键组,根据顺序,分配的ID分别是1号和二号,可以通过注册函数的返回值来确认;这里第一组按键值从1开始,第二组按键值从10开始;
C 复制代码
uint8_t id1 = keyManagerAddGroup(keyGroup1, 1);//按键值从1开始
uint8_t id2 = keyManagerAddGroup(keyGroup2, 10);//按键值从10开始
  1. 注册按键字符对应表,让按键对应字符,某些场合下适用。对应表分为全局对应表GlobalKeyCharTab和局部对应表KeyCharTab,可以共用,其中局部表优先级最高

另外,程序运行时调用下边的函数可以随时更新按键值,实现不同场合不同按键功能的作用。

出现的GlobalKeyCharTab、KeyCharTab两个数组需要我们手动去实现。

C 复制代码
RegistIdKeyCharTab(GlobalKeyCharTab, 2);
RegistKeyCharTab(keyGroup2, KeyCharTab, 2);
  1. 至此,初始化全部完成,期间需要我们实现一个事件函数 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 编写按键字符对应表

按键字符对应表有两种,可以同时使用(多个按键可以对应一个字符,某些按键也可以不设置字符)。

  1. 带ID的全局按键字符对应表,所有按键组都可以用一张表来对应,属于低优先级对应表
C 复制代码
//全局按键字符对应表
const IdKeyChar_t GlobalKeyCharTab[] = 
{
	{{1, 1}, 'a'},			
	{{1, 2}, 'b'},
	{{2, 10}, 'c'}, 		//第二组按键值从10开始
	{{2, 11}, 'd'},
};
  1. 不带ID的按键组内部字符对应表,只适用于某一组别内部按键字符识别,优先级高
C 复制代码
const KeyChar_t KeyCharTab[] = 
{
	{10, 'A'},		//第二组按键值从10开始
	{11, 'B'}
};

3.5 执行结果

编译下载后,按下按键,串口打印信息如下,大家对着事件函数keyEvent()自行分析

4. 矩阵按键的声明方法

矩阵键盘的声明方法跟独立按键方法大同小异。在独立按键的代码基础上直接添加

  1. cubemx中添加四个行IO,名字分别是row0~row3,设置成 输出模式 ;添加四个列IO,名字分别是col0~col3,设置成 输入上拉模式
  2. 在主程序中为矩阵按键分配设备空间,这里出现两个回调函数 setRow, getCol
C 复制代码
//行列都是4个IO,注册设置行IO函数setRow、读取列IO函数getCol
key_base_t *keyGroup3 = creatMatrixKey(4, 4, setRow, getCol);
  1. 把矩阵键盘组注册到按键管理器中,按键值从1开始。
C 复制代码
//矩阵键盘按键值从1开始,这就跟上边第一组独立按键键值重叠
//不用担心,两个组的ID不一样
uint8_t id3 = keyManagerAddGroup(keyGroup3, 1);
  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);
  1. 至此初始化工作已完成,需要实现两个回调函数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模拟按键的声明方法跟上边的代码类似,在矩阵代码的基础上添加如下内容:

  1. cubemx分配一个AD引脚,进型适当的配置,软件触发或定时触发都没问题,这里以软件触发为例。在主程序上初始化AD模块,按键值;
  2. 给AD模块分配内存,注册AD值获取函数getAD、电压识别范围表ADtab;
C 复制代码
//这里分配8个按键
key_base_t *keyGroup4 = creatADKey(getAD,  ADtab, 8);
  1. 把AD按键注册到按键管理器中:
C 复制代码
//按顺序来说,id为4,按键值从1开始
uint8_t id4 = keyManagerAddGroup(keyGroup4, 1);
  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模拟按键是目前最常用的三种按键,可满足九成以上的嵌入式需求。

但架不住还有一些特殊按键,比如电磁炉的触摸按键等,该如何自定义添加呢?

思路如下:

  1. 想办法实现按键基类中的 getKeyValue 函数,比如独立按键的readIndepKey()、矩阵键盘的scanMatrixKey()、AD键盘的scanADKey();
  2. 为了实现上边的函数必须注册相关用户处理函数回调,还要注册按键个数,比如独立按键的getKey()、矩阵键盘的getCol()和setRow()、AD按键的getAD();且按键个数和用户回调都要放到继承类里边;
  3. 继承类中添加必要的其他参数,例如AD键盘的类的ADTab识别数组;

大家可以根据KeyManager.c中独立按键、矩阵按键、AD按键的代码体会其中的方法,编写自己的按键程序(比如触摸按键识别程序)。

相关推荐
三佛科技-134163842122 小时前
FT8353系列(FT8353A/B/C/CD/DD/K/KD/PD)隔离型LED恒流驱动IC芯片 典型应用电路
单片机·物联网·智能家居·pcb工艺
阿拉斯攀登4 小时前
嵌入式-硬件基础:了解三极管
单片机·嵌入式硬件·三极管
逐步前行4 小时前
C51_74HC165并口转串口
单片机·51单片机
HarrySunCn4 小时前
如何使用VSCode开发Arduino项目
ide·vscode·单片机·编辑器
喵了meme5 小时前
C语言实战2
c语言·开发语言·网络
嵌入式的飞鱼5 小时前
SD NAND 焊接避坑指南:LGA-8 封装手工焊接技巧与常见错误
人工智能·stm32·单片机·嵌入式硬件·tf卡
三佛科技-134163842125 小时前
LN8K05A/B/C_5V非隔离AC-DC电源芯片 典型应用场景、典型电路、与阻容降压的对比分析
单片机·嵌入式硬件·物联网·智能家居·pcb工艺
ACP广源盛139246256735 小时前
GSV6155@ACP#6155产品规格详解及产品应用分享
嵌入式硬件·计算机外设·音视频
大唐荣华6 小时前
灵巧手 - 绳驱(钢丝/绳索驱动)、连杆(Linkage)和直驱(Direct Drive)的技术对比
嵌入式硬件·机械·灵巧手