软件设计指南
软件设计指南
前面我们完成了所有的硬件设计,下面我们开始进行软件设计,软件设计内容较多,不可能完全讲解,在此只提部分核心内容。
功能设计
功能分解
HID键盘功能
- 按键消抖以及长按、短按、弹起状态识别
- 六键无冲按键事件报文生成,考虑按键冲突
- HID复合设备
密码管理功能
- 按键消抖长按、短按、连续按、弹起状态识别
- LCD显示屏驱动以及GUI界面
- 指纹模块驱动编写
代码环境搭建
- STM32F405RGT6+FreeRTOS+CubeMx
部分重要设计说明
对于键盘来说HID虽然比较高深,但是我并不认为它是键盘的核心所在,因为毕竟如何高深,USB也不过是一种通信手段而已,更重要的是如何处理按键事件、并在最后通过USB通信上报按键事件。
按键状态识别算法
一般来说按键事件都或多或少的伴随着按键抖动的问题,而按键抖动的时间一般为ms级别,其中机械轴体的抖动要比普通按键小一些,因此比较常规的方法是,中断方式判断第一次按键按下,然后等待5-20ms之后再次确定按键电平是否为按键按下,则标记按键状态为真实按下,但是此方法效率较低,且不容易处理多个按键,那么我在此提出一种按键扫描算法,它可以在高效率处理
按键消抖
的同时,获取按键按下的状态,包括按键的长按
、短按
、连续按
、以及按键弹起
,算法应当在定时器中周期运行,而不需要依赖任何的延时。
普通按键按下电压波形
对于按键抖动的问题是否可以从硬件上完美解决,我认为简单的加电容是不可以解决的,如果只依靠电容来达到比较完美的按键按下波形或许确实可以达到,但是达到那种程度的波形,极大程度上会造成电平触发的延时问题,而且复用率极低,同时不如算法来的稳定,其中按键按下的状态本质上就是有抖动存在的,它并不是一个始终连接的线路,并且依靠改变输出电平触发,所以硬件消抖只可能滤掉特定带宽的抖动来适用不同场合的轴体和按键,其复用率,扩展性较差,而且不能从根本上处理问题,或许这才是很少使用硬件消抖的原因,
按键扫描算法简图.
其中 time_count
记录检测按键按下电平次数, res_count
记录按键弹起电平次数,同时次数自增具有最大值限制,此部分不在流程图中显示, time_count
记录次数超过短按判断次数时,认为按键按下,首次进入将按键标记为短按状态,并将 res_count
清零, click_num
自增1,当 time_count
记录次数超过长按判断次数时认为按键为长按,首次进入标记为长按状态,并将 click_num
、 res_count
清零,当 res_count
大于按键弹起判断时间时,将按键标记为按键弹起, time_count
清零,当 res_count
大于连续按超时时间时获取按键按下次数 click_state_num=click_num
,并将 click_num
清零。按键扫描在定时器或任务中进行,其它线程只需关心 states
和 click_state_num
。
对于按键算法图中的按键状态的赋值操作都只有在非此状态时可以赋值,部分细节处理过程并未在流程图中展示,而且键盘的按键算法省略了按键次数的统计。
编码器按键信息结构体
typedef enum
{
KEY_TASK_UPSPRING, //按键弹起
KEY_TASK_PRESS, //按键按下
KEY_TASK_PRESS_L_TIME, //按键长按
} key_task_states;
typedef struct
{
key_task_states states; //按键状态
uint8_t click_state_num; //连续短按次数
uint16_t time_count; //按键按下时间计数
uint16_t res_count; //按键弹起时间计数
uint8_t click_num; //按键状态为短按计数
} key_task_time_info;
typedef struct
{
GPIO_TypeDef *key_gpio; //定义的按键GPIO
uint16_t key_gpio_pin; //定义的按键GPIO
key_task_time_info key_info; //按键算法信息
} mx_key_task_info;
经过按键算法处理,我们可以得到需要使用的按键消息
states
和click_state_num
这两个可供使用的变量,其中states
表示按键为长按、短按、弹起三种状态,而click_state_num
为连续按结束(最后一次按下后弹起超过0.5s后更新连续的短按次数)之后得到的连续按下次数,对于按键长按不计入次数、并且视为退出连续按。
按键扫描算法
for (key_count = 0; key_count < MAX_KEY_NUM; key_count++)
{
//读取按键电平
key[key_count] = key_task_scan(key_enc_info.key_t[key_count]);
if (key[key_count] == KEY_TASK_PRESS_S)
{
//按键按下计数
if (key_enc_info.key_t[key_count].key_info.time_count < KEY_PRESS_MAX_COUNT)
{
key_enc_info.key_t[key_count].key_info.time_count++;
}
}
else
{
//按键弹起计数
if (key_enc_info.key_t[key_count].key_info.res_count < KEY_RES_MAX_COUNT)
{
key_enc_info.key_t[key_count].key_info.res_count++;
}
}
//到达长按时间
if (key_enc_info.key_t[key_count].key_info.time_count >= KEY_TASK_TIME_L_COUNT)
{
if (key_enc_info.key_t[key_count].key_info.states == KEY_TASK_PRESS)
{
key_enc_info.key_t[key_count].key_info.states = KEY_TASK_PRESS_L_TIME;
key_enc_info.key_t[key_count].key_info.click_num = 0;
key_enc_info.key_t[key_count].key_info.click_state_num = 0;
//按键复位计数清零
key_enc_info.key_t[key_count].key_info.res_count = 0;
}
}
//短按时间
else if (key_enc_info.key_t[key_count].key_info.time_count >= KEY_TASK_TIME_P_COUNT)
{
//首次更新按键事件
if (key_enc_info.key_t[key_count].key_info.states == KEY_TASK_UPSPRING)
{
key_enc_info.key_t[key_count].key_info.states = KEY_TASK_PRESS;
//记录按键按下次数
if (key_enc_info.key_t[key_count].key_info.click_num < KEY_MAX_NUM)
{
key_enc_info.key_t[key_count].key_info.click_num++;
}
//按键复位计数清零
key_enc_info.key_t[key_count].key_info.res_count = 0;
}
}
//连续按超时
if (key_enc_info.key_t[key_count].key_info.res_count >= KEY_RES_NUM_COUNT)
{
if (key_enc_info.key_t[key_count].key_info.click_num != 0)
{
key_enc_info.key_t[key_count].key_info.click_state_num =
key_enc_info.key_t[key_count].key_info.click_num;
key_enc_info.key_t[key_count].key_info.click_num = 0;
}
}
//按键复位弹起
else if (key_enc_info.key_t[key_count].key_info.res_count >= KEY_RES_COUNT)
{
if (key_enc_info.key_t[key_count].key_info.states != KEY_TASK_UPSPRING)
{
key_enc_info.key_t[key_count].key_info.states = KEY_TASK_UPSPRING; //按键弹起复位
//按键按下计数清零
key_enc_info.key_t[key_count].key_info.time_count = 0; //按键计数清零
}
}
}
osDelayUntil(&peroid, KEY_TASK_TIME_CYCLE); //任务延时
对于矩阵按键扫描并不能直接读取GPIO电平来处理,需要进行一些简单的处理,来模拟成80个按键的真实电平,具体的实现方式是行列扫描,此处定义
6行
14列
矩阵键盘,其中6行
对于6个输出GPIO,14列
对应14个输入GPIO,之后是6行中控制一行输出高电平,其余行输出低电平,然后再读取对应行的14个输入GPIO电平确定本行按下的按键,其余行操作类似,只是各行的切换需要一定的延时,不能同时操作,那么具体操作如下:
开一个定时器定时 50us以上
在定时器中操作如下
循环读取对应行中14列按键的状态,并标记状态
如果行控制增加到大于按键扫描周期定义时间(定义时间>=行数*定时器周期,如75us周期扫描6行,可以定义为6即为450us扫描周期,为方便计算,定义为8即600us周期),根据记录80个按键状态进行按键按下计数、和按键复位计数,以及调用按键算法,该部分和上文所述按键算法类似,只是为了区分行列按键在数组定义上有所区别。
行控制自增1
控制对应行为高电平输出,其余行输出低电平,当下一次定时器中断时读取本行按键状态,即控制输出电平与读取电平时间相差一个定时器周期的时间。
对于上述80配列按键扫描算法,我定义的实际计算一轮80配列按键需要时间为600us,并且定义短按计数次数为10,即进行6ms的按键消抖,也就是说6ms内可以比较准确的得出80个按键的扫描结果,这个也对应于下面的HID报文周期
六键无冲处理
对于键盘来说通过修改HID描述符实现全键无冲应该不算是一件很难得事,在此我为了无线HID和有线HID报文得尽可能统一,使用了HID复合设备,其中包括鼠标、键盘、媒体,其中键盘是常规的六键无冲方案,报文4ms周期性发送,没必要太高,哪些打着响应1ms级别键盘的应该也只是上报时间是1ms周期(严重一点如果你的按键扫描结果是每100ms更新一次,那么每1ms上报一次报文就毫无意义可言),然而这个周期其实1ms-20ms之内我认为并没有实质性的区别。
键盘真正需要做好的是在5-20ms内准确获取矩阵按键内每个按键的状态(要准确获取按键的状态,无论是从硬件上还是软件上都至少需要5-20ms,进行按键消抖),这才是关键,本方案会在6ms准确的计算出所有按键的状态,如果5-20ms内按键扫描准确无误,那么下一次上报的会是你更新后的六个按键,在此我不相信有人真的可以在5-20ms内同时操作6个以上按键,注意是六个普通按键,组合键的
CTRL
、SHIFT
等特殊按键是不计算在内的,这部分按键是独立字节上报的,报文可以直接对应键值,而对于游戏要求需要全键无冲其实只是为了组合键,来满足必须同时按下六个以上普通按键的要求。
对于六键无冲处理,需要定义
uint8_t used_it[6]
和uint8_t used_key[6]
,其中used_it[6]
用于标记对应报文是否使用,used_key[6]
则用于存储上报键值,循环中首先检测used_it[6]
,即判断报文是否在使用,如果在使用,则后续判断used_key[6]
对应的按键是否还保持按下状态,当该按键为释放弹起状态时标记used_it[6]
为0, 然后后续根据按键按下情况依次将新按键键值送入used_it[6]
为0的used_key[6]
,当然还需要判断一下六个缓冲中都不包含此键值,特殊按键为移位操作,特殊处理。
六键无冲报文处理
void key_update(uint8_t *pTxbuf, key_info_t *key_cfg)
{
static uint8_t res_hid = 0;
static uint16_t res_hid_l_press[6] = {0};
uint8_t i;
for (i = 0; i < 8; i++)
pTxbuf[i] = 0;
//重复,一帧有效,一帧弹起,如果不发送弹起,高频率发送无法准确控制频率
/*特殊按键特殊处理,特殊按键不需要长短按区分,否则电脑可能有滞带键提示*/
key_info_get(key_cfg);
if (key_cfg->keyboard.byte0_off.Left_Control == 1)
{
pTxbuf[0] |= 1 << Left_Control;
key_cfg->keyboard.byte0_off.Left_Control_used_it = 1;
}
if (key_cfg->keyboard.byte0_off.Left_Shift == 1)
{
pTxbuf[0] |= 1 << Left_Shift;
key_cfg->keyboard.byte0_off.Left_Shift_used_it = 1;
}
if (key_cfg->keyboard.byte0_off.Left_Alt == 1)
{
pTxbuf[0] |= 1 << Left_Alt;
key_cfg->keyboard.byte0_off.Left_Alt_used_it = 1;
}
if (key_cfg->keyboard.byte0_off.Left_GUI == 1)
{
pTxbuf[0] |= 1 << Left_GUI;
key_cfg->keyboard.byte0_off.Left_Alt_used_it = 1;
}
if (key_cfg->keyboard.byte0_off.Right_Control == 1)
{
pTxbuf[0] |= 1 << Right_Control;
key_cfg->keyboard.byte0_off.Left_Alt_used_it = 1;
}
if (key_cfg->keyboard.byte0_off.Right_Shift == 1)
{
pTxbuf[0] |= 1 << Right_Shift;
key_cfg->keyboard.byte0_off.Left_Alt_used_it = 1;
}
if (key_cfg->keyboard.byte0_off.Right_Alt == 1)
{
pTxbuf[0] |= 1 << Right_Alt;
key_cfg->keyboard.byte0_off.Left_Alt_used_it = 1;
}
if (key_cfg->keyboard.byte0_off.Right_GUI == 1)
{
pTxbuf[0] |= 1 << Right_GUI;
key_cfg->keyboard.byte0_off.Left_Alt_used_it = 1;
}
pTxbuf[1] = 0;
if (res_hid == 0)
{
/*BYTE2--BYTE7 普通按键 需要区分长按短按*/
for (i = 0; i < 6; i++)
{
if ((key_cfg->keyboard.used_it[i] == 0) && (key_cfg->keyboard.keypad_status[i] == KEYBOARD_PRESS))
{
pTxbuf[i + 2] = key_cfg->keyboard.keypad[i];
key_cfg->keyboard.used_it[i] = 1;
res_hid_l_press[i] = 0;
}
if (key_cfg->keyboard.keypad_status[i] == KEYBOARD_PRESS_L_TIME)
{
if (res_hid_l_press[i] >= TIME_PRESS_L_HID_COUNT)
{
pTxbuf[i + 2] = key_cfg->keyboard.keypad[i];
res_hid_l_press[i] = 0;
}
res_hid_l_press[i]++;
}
}
}
res_hid++;
if (res_hid > 1)
{
res_hid = 0;
}
}
六键无冲报文生成
void key_info_get(key_info_t *key_cfg)
{
uint8_t i, point = 0;
/*记录正在使用的按键*/
static uint8_t used_it[6] = {KEY_NO_USER, KEY_NO_USER, KEY_NO_USER, KEY_NO_USER, KEY_NO_USER, KEY_NO_USER};
static uint8_t used_key[6] = {KEY_NO_USER, KEY_NO_USER, KEY_NO_USER, KEY_NO_USER, KEY_NO_USER, KEY_NO_USER};
/*按键松手处理*/
for (i = 0; i < 6; i++)
{
/*如果此缓冲区还在使用*/
if (used_it[i] == KEY_USER)
{
/*检查按键是否松手*/
if (key_cfg->key_is_press[used_key[i]].press_it.states == KEY_UPSPRING)
{
/*如果已经松手,清空缓冲区,六键无冲释放*/
used_it[i] = KEY_NO_USER;
used_key[i] = KEY_NO_USER;
key_cfg->keyboard.keypad[i] = 0;
key_cfg->keyboard.keypad_status[i] = 0;
key_cfg->keyboard.used_it[i] = 0;
}
}
}
/*特殊按键*/
if (key_cfg->key_is_press[KEY_LEFT_ALT].press_it.states != KEY_UPSPRING)
{
key_cfg->keyboard.byte0_off.Left_Alt = 1;
}
else
{
key_cfg->keyboard.byte0_off.Left_Alt = 0;
key_cfg->keyboard.byte0_off.Left_Alt_used_it = 0;
}
key_cfg->keyboard.byte0_off.Left_Alt_Status = key_cfg->key_is_press[KEY_LEFT_ALT].press_it.states;
if (key_cfg->key_is_press[KEY_LEFT_CONTROL].press_it.states != KEY_UPSPRING)
{
key_cfg->keyboard.byte0_off.Left_Control = 1;
}
else
{
key_cfg->keyboard.byte0_off.Left_Control = 0;
key_cfg->keyboard.byte0_off.Left_Control_used_it = 0;
}
key_cfg->keyboard.byte0_off.Left_Control_Status = key_cfg->key_is_press[KEY_LEFT_CONTROL].press_it.states;
if (key_cfg->key_is_press[KEY_LEFT_WIN].press_it.states != KEY_UPSPRING)
{
key_cfg->keyboard.byte0_off.Left_GUI = 1;
}
else
{
key_cfg->keyboard.byte0_off.Left_GUI = 0;
key_cfg->keyboard.byte0_off.Left_GUI_used_it = 0;
}
key_cfg->keyboard.byte0_off.Left_GUI_Status = key_cfg->key_is_press[KEY_LEFT_WIN].press_it.states;
if (key_cfg->key_is_press[KEY_LEFT_SHIFT].press_it.states != KEY_UPSPRING)
{
key_cfg->keyboard.byte0_off.Left_Shift = 1;
}
else
{
key_cfg->keyboard.byte0_off.Left_Shift = 0;
key_cfg->keyboard.byte0_off.Left_Shift_used_it = 0;
}
key_cfg->keyboard.byte0_off.Left_Shift_Status = key_cfg->key_is_press[KEY_LEFT_SHIFT].press_it.states;
if (key_cfg->key_is_press[KEY_RIGHT_ALT].press_it.states != KEY_UPSPRING)
{
key_cfg->keyboard.byte0_off.Right_Alt = 1;
}
else
{
key_cfg->keyboard.byte0_off.Right_Alt = 0;
key_cfg->keyboard.byte0_off.Right_Alt_used_it = 0;
}
key_cfg->keyboard.byte0_off.Right_Alt_Status = key_cfg->key_is_press[KEY_RIGHT_ALT].press_it.states;
if (key_cfg->key_is_press[KEY_RIGHT_CONTROL].press_it.states != KEY_UPSPRING)
{
key_cfg->keyboard.byte0_off.Right_Control = 1;
}
else
{
key_cfg->keyboard.byte0_off.Right_Control = 0;
key_cfg->keyboard.byte0_off.Right_Control_used_it = 0;
}
key_cfg->keyboard.byte0_off.Right_Control_Status = key_cfg->key_is_press[KEY_RIGHT_CONTROL].press_it.states;
if (key_cfg->key_is_press[KEY_RIGHT_WIN].press_it.states != KEY_UPSPRING)
{
key_cfg->keyboard.byte0_off.Right_GUI = 1;
}
else
{
key_cfg->keyboard.byte0_off.Right_GUI = 0;
key_cfg->keyboard.byte0_off.Right_GUI_used_it = 0;
}
key_cfg->keyboard.byte0_off.Right_GUI_Status = key_cfg->key_is_press[KEY_RIGHT_WIN].press_it.states;
if (key_cfg->key_is_press[KEY_RIGHT_SHIFT].press_it.states != KEY_UPSPRING)
{
key_cfg->keyboard.byte0_off.Right_Shift = 1;
}
else
{
key_cfg->keyboard.byte0_off.Right_Shift = 0;
key_cfg->keyboard.byte0_off.Right_Shift_used_it = 0;
}
key_cfg->keyboard.byte0_off.Right_Shift_Status = key_cfg->key_is_press[KEY_RIGHT_SHIFT].press_it.states;
/*六键无冲按键处理*/
for (i = 0; i < MAX_KEY; i++)
{
if ((i != KEY_LEFT_ALT) && (i != KEY_LEFT_CONTROL) && (i != KEY_LEFT_WIN) && (i != KEY_LEFT_SHIFT) && (i != KEY_RIGHT_ALT) && (i != KEY_RIGHT_CONTROL) && (i != KEY_RIGHT_WIN) && (i != KEY_RIGHT_SHIFT))
{
if (key_cfg->key_is_press[i].press_it.states != KEY_UPSPRING)
{
//如果该按键按下,遍历6个缓冲区,如果未使用,则使用此缓冲区
for (point = 0; point < 6; point++)
{
/*如果六个缓冲区都未包含此按键*/
if ((used_key[0] != i) && (used_key[1] != i) && (used_key[2] != i) && (used_key[3] != i) && (used_key[4] != i) && (used_key[5] != i))
{
if (used_it[point] == KEY_NO_USER)
{
used_it[point] = KEY_USER; //标记此按键已经使用
used_key[point] = i; //记录使用此缓冲区的按键
key_cfg->keyboard.keypad[point] = key_cfg->key_is_press[i].key_char;
key_cfg->keyboard.keypad_status[point] = key_cfg->key_is_press[i].press_it.states;
point = 6; //如果找到未使用的缓冲区,直接退出循环
}
}
/*以下部分加入长短按识别*/
else if (used_key[0] == i)
{
key_cfg->keyboard.keypad_status[0] = key_cfg->key_is_press[i].press_it.states;
}
else if (used_key[1] == i)
{
key_cfg->keyboard.keypad_status[1] = key_cfg->key_is_press[i].press_it.states;
}
else if (used_key[2] == i)
{
key_cfg->keyboard.keypad_status[2] = key_cfg->key_is_press[i].press_it.states;
}
else if (used_key[3] == i)
{
key_cfg->keyboard.keypad_status[3] = key_cfg->key_is_press[i].press_it.states;
}
else if (used_key[4] == i)
{
key_cfg->keyboard.keypad_status[4] = key_cfg->key_is_press[i].press_it.states;
}
else if (used_key[5] == i)
{
key_cfg->keyboard.keypad_status[5] = key_cfg->key_is_press[i].press_it.states;
}
}
}
}
}
}
HID复合设备
标准的USB设备有5种USB描述符:设备描述符、配置描述符、字符串描述符、接口描述符、端点描述符,要详细说明这些比较复杂,这里我们更关系USB中的HID设备,对于HID还有HID特有的描述符。
以下是STM32CubeMx基础之上实现HID复合设备流程
- 配置CubeMX 你可以直接生成一个HID设备,不过只包含鼠标功能
- 生成的鼠标HID工程默认只包含一个输出端点,在此需要修改设备描述符添加一个输入端点、用于PC向键盘汇报LED灯状态
- 添加输入端点、配置HID中断、回调等
- 修改HID描述符中HID报告描述符定义长度
- 修改HID报告描述符添加键盘(包括LED)、鼠标、媒体的HID报告描述符
- 为三个报告描述符添加 Report ID
- 根据 Report ID封装HID上报函数
指纹模块驱动
本方案使用了BM2166指纹模块,模块提供了简单的DEMO
简单的DEMO仅仅可以用于测试模块是否正常,其demo中大量的
delay
延时函数极大的占用了MCU的CPU资源,如何理解CPU的多线程管理原理、RTOS的任务调度、和RTOS的伪延时
、以及STM32系统的中断,在此不难搭建出更高效、稳定、以及更低占用的指纹模块驱动方案。
单核心的CPU其实只有一个线程,也就是真实情况下它也是单线程的,而多线程的概念只是由操作系统模拟出的多线程、在此用一个简单的示例解释CPU是如何进行多线程的,在此我们有四个应用程序,并将其分别命名为应用1、应用2、应用3、应用4,那么如果没有操作系统,你应该如何去分配CPU去执行这四个功能?
- 当然如果所有任务都可以充分利用CPU资源,而不会浪费CPU资源的话、在此可以应用1、应用2、应用3、应用4依次处理。
- 然而CPU一般处理速度都会远大于应用需要的处理速度,对于没有操作系统而言的系统但凡使用了延时、以及while循环等待的系统都存在系统资源的浪费。
- 继续我们假设
应用1
~应用4
分别需要的系统占用为 10%、20%、30%和50%,其中每个应用都需要延时、我们应该如何去处理这些应用? - 在此我们不难求出
10
、20
、30
、50
、的最大公约数为10
,而所有应用之和为100
,因此我们可以设计一个周期为100/10
的计数周期,来进行多线程的模拟。
void cpu_task(void)
{
static uint8_t tack_count=1;
while(1)
{
switch(tack_count)
{
case (1):run_task1();break;
case (2):
case (3):run_task2();break;
case (4):
case (5):
case (6):run_task3();break;
case (7):
case (8):
case (9):
case (10):run_task4();break;
case (11):tack_count = 0;break;
}
tack_count++;
}
- 在此结合嵌入式程序而言,涉及外设的通信以及响应等一般都在毫秒级别,同时当外设响应后CPU的数据处理过程往往只是us级到ms级别区间,因此上述方案还需要进行一定程度优化,我们设计一个定时执行的函数,并且假设定时为0.1ms。
- 对于你的应用程序,我推荐你可以使用第二种定时器方式,提供应用程序的轮询,因为这样你可以开多个定时器以分配为不同级别的应用程序轮询,而不会造成任何的CPU资源浪费,这种伪并行的方式,在我在按键扫描算法中很容易体现。
void timer_irq(void)
{
static uint8_t tack_count=1;
switch(tack_count)
{
case (1):run_task1();break;
case (2):
case (3):run_task2();break;
case (4):
case (5):
case (6):run_task3();break;
case (7):
case (8):
case (9):
case (10):run_task4();break;
case (11):tack_count = 0;break;
}
tack_count++;
}
- 那么上述程序的含义就是1ms内,0.1ms的时间处理
应用1
、0.2ms的时间处理应用2
、0.3ms的时间处理应用3
、0.5ms的时间处理应用4
,在此所有应用的结果处理都需要在0.1ms内完成,同时处理完成之后CPU会立即释放,并去等待其余的应用,那么实际等效下来就是每1ms,CPU会同时处理这四个应用程序,并得出执行结果。
在此我们尝试构建状态机来进行模块串口消息的处理,所有的指令都有消息回传,在发送消息后、CPU资源需要释放,等待模块处理完成并回传消息,在此原模块为插入超时等待模块返回,而无法释放CPU资源,因此消息方式必须得到改善
改善的消息方式为使用串口DMA发送指纹模块控制消息,使用串口空闲中断DMA双缓冲接收进行消息回传处理,构建消息处理状态机
BM_ACK_NO
,BM_ACK_WAIT
,BM_ACK_OK
BM_ACK_NO
>> 发送消息,并将状态切换为BM_ACK_WAIT
>> 释放CPU资源 >> 串口空闲中断 >> 切换状态为BM_ACK_OK
>>处理回传数据,切换状态为BM_ACK_NO
>> 下一轮数据收发
在指纹识别、指纹录入过程中牵涉到多条命令的协同处理,在此也需要构建状态机,进行命名的切换处理,合理的利用状态机不但可以理清应用逻辑,还可以很好的规范程序框架。
在此我们需要牢记,无论是工作还是代码、一种高效率的工作方式都不会去一直等待对方做好后处理,而是构建一个协同的状态机,当对方完成了此项工作,会将对应状态标记完成,而你要做的只是当你完成其它任务以及在你以固定的周期频率去检查它是否完成,如果未完成那么你继续去完成你的其它工作,如果完成了,你则快速的在它完成的基础之上完成后续工作。
显示屏驱动
商家提供了屏幕的驱动程序,只需要简单适配SPI驱动就可以实现刷屏,不过为了提高刷屏效率,在此需要简单修改驱动程序,以适应DMA方式刷屏,图形界面使用LVGL
键盘上报字符串
这个比较简单,因为以及实现按键的HID上报,在此只需要将ASSIC编码转为键盘的编码,并依次上报即可,上报时短暂剥夺按键上报权力,字符串上报完成后恢复,在此主要需要注意的是,键盘编码有很多共用的如大小写,而共用区分需要通过 SHIFT
的值区分。