最新教程下载:http://www.armbbs.cn/forum.php?mod=viewthread&tid=93255

第19章       STM32F429的GPIO应用之按键FIFO

本章教程为大家介绍STM32F429的GPIO应用之按键FIFO,这个方案已经在实际项目中千锤百炼,比较实用。

目录

第19章       STM32F429的GPIO应用之按键FIFO

19.1 初学者重要提示

19.2 按键硬件设计

19.2.1 硬件设计

19.2.2 GPIO内部结构分析按键

19.3 按键FIFO的驱动设计

19.3.1 按键FIFO的原理

19.3.2 按键FIFO的实现

19.3.3 按键检测程序分析

19.3.4 按键检测采用中断方式还是查询方式

19.4 按键板级支持包(bsp_key.c)

19.4.1 函数bsp_InitKeyHard

19.4.2 函数bsp_GetKey

19.4.3 函数bsp_KeyScan10ms

19.5 按键FIFO驱动移植和使用

19.6 实验例程设计框架

19.7 实验例程说明(MDK)

19.8 实验例程说明(IAR)

19.9 总结


19.1 初学者重要提示

  1. 学习本章节前,务必保证已经学习了第15,16和17章。
  2. 按键FIFO驱动扩展和移植更简单,组合键也更好用。支持按下、弹起、长按和组合键。

19.2 按键硬件设计

V6开发板有三个独立按键和一个五向摇杆,下面是三个独立按键的原理图:

注意,K1(S1)、K2(S2)和K3(S3)按键的上拉电阻是接在5V电压上,因为这三个按键被复用为PS/2键盘鼠标接口,而PS/2是需要5V供电的(注,V5和V6开发板做了PS/2复用)。实际测试,K1、K2、K3按键和PS/2键盘是可以同时工作的。

下面是五向摇杆的原理图:

通过这个硬件设计,有如下两个知识点为大家做介绍:

19.2.1 硬件设计

按键和CPU之间串联的电阻起保护作用。按键肯定是存在机械抖动的,开发板上面的硬件没有做硬件滤波处理,即使设计了硬件滤波电路,软件上还是需要进行滤波。

  • 保护GPIO,避免软件错误将IO设置为输出,如果设置为低电平还好,如果设置输出的是高电平,按键按下会直接跟GND(低电平)连接,从而损坏MCU。
  • 保护电阻也起到按键隔离作用,这些GPIO可以直接用于其它实验。

19.2.2 GPIO内部结构分析按键

详细的GPIO模式介绍,请参考第15章的15.3小节,本章仅介绍输入模式。下面我们通过一张图来简单介绍GPIO的结构。

红色的线条是GPIO输入通道的信号流向,作为按键检测IO,这些需要配置为浮空输入。按键已经做了5V上拉,因此GPIO内部的上下拉电阻都选择关闭状态。

19.3 按键FIFO的驱动设计

bsp_key按键驱动程序用于扫描独立按键,具有软件滤波机制,采用FIFO机制保存键值。可以检测如下事件:

  • 按键按下。
  • 按键弹起。
  • 长按键。
  • 长按时自动连发。

我们将按键驱动分为两个部分来介绍,一部分是FIFO的实现,一部分是按键检测的实现。

bsp_key.c 文件包含按键检测和按键FIFO的实现代码。

bsp.c 文件会调用bsp_InitKey()初始化函数。

bsp.c 文件会调用bsp_KeyScan按键扫描函数。

bsp_timer.c 中的Systick中断服务程序调用 bsp_RunPer10ms。

中断程序和主程序通过FIFO接口函数进行信息传递。

函数调用关系图:

19.3.1 按键FIFO的原理

FIFO是First Input First Output的缩写,先入先出队列。我们这里以5个字节的FIFO空间进行说明。Write变量表示写位置,Read变量表示读位置。初始状态时,Read = Write = 0。

我们依次按下按键K1,K2,那么FIFO中的数据变为:

如果Write!= Read,则我们认为有新的按键事件。

我们通过函数bsp_GetKey读取一个按键值进行处理后,Read变量变为1。Write变量不变。

我们继续通过函数bsp_GetKey读取3个按键值进行处理后,Read变量变为4。此时Read = Write = 4。两个变量已经相等,表示已经没有新的按键事件需要处理。

有一点要特别的注意,如果FIFO空间写满了,Write会被重新赋值为0,也就是重新从第一个字节空间填数据进去,如果这个地址空间的数据还没有被及时读取出来,那么会被后来的数据覆盖掉,这点要引起大家的注意。我们的驱动程序开辟了10个字节的FIFO缓冲区,对于一般的应用足够了。

设计按键FIFO主要有三个方面的好处:

  • 可靠地记录每一个按键事件,避免遗漏按键事件。特别是需要实现按键的按下、长按、自动连发、弹起等事件时。
  • 读取按键的函数可以设计为非阻塞的,不需要等待按键抖动滤波处理完毕。
  • 按键FIFO程序在嘀嗒定时器中定期的执行检测,不需要在主程序中一直做检测,这样可以有效地降低系统资源消耗。

19.3.2 按键FIFO的实现

在bsp_key.h 中定了结构体类型KEY_FIFO_T。这只是类型声明,并没有分配变量空间。

#define KEY_FIFO_SIZE    10
typedef struct
{uint8_t Buf[KEY_FIFO_SIZE];        /* 键值缓冲区 */uint8_t Read;                    /* 缓冲区读指针1 */uint8_t Write;                 /* 缓冲区写指针 */uint8_t Read2;                 /* 缓冲区读指针2 */
}KEY_FIFO_T;

在bsp_key.c 中定义s_tKey结构变量, 此时编译器会分配一组变量空间。

static KEY_FIFO_T s_tKey;        /* 按键FIFO变量,结构体 */

一般情况下,只需要一个写指针Write和一个读指针Read。在某些情况下,可能有两个任务都需要访问按键缓冲区,为了避免键值被其中一个任务取空,我们添加了第2个读指针Read2。出厂程序在bsp_Idle()函数中实现的按K1K2组合键截屏的功能就使用的第2个读指针。

当检测到按键事件发生后,可以调用 bsp_PutKey函数将键值压入FIFO。下面的代码是函数的实现:

/*
*********************************************************************************************************
*    函 数 名: bsp_PutKey
*    功能说明: 将1个键值压入按键FIFO缓冲区。可用于模拟一个按键。
*    形    参: _KeyCode : 按键代码
*    返 回 值: 无
*********************************************************************************************************
*/
void bsp_PutKey(uint8_t _KeyCode)
{s_tKey.Buf[s_tKey.Write] = _KeyCode;if (++s_tKey.Write  >= KEY_FIFO_SIZE){s_tKey.Write = 0;}
}

这个bsp_PutKey函数除了被按键检测函数调用外,还可以被其他底层驱动调用。比如红外遥控器的按键检测,也共用了同一个按键FIFO。遥控器的按键代码和主板实体按键的键值统一编码,保持键值唯一即可实现两套按键同时控制程序的功能。

应用程序读取FIFO中的键值,是通过bsp_GetKey函数和bsp_GetKey2函数实现的。我们来看下这两个函数的实现:

/*
*********************************************************************************************************
*    函 数 名: bsp_GetKey
*    功能说明: 从按键FIFO缓冲区读取一个键值。
*    形    参:  无
*    返 回 值: 按键代码
*********************************************************************************************************
*/
uint8_t bsp_GetKey(void)
{uint8_t ret;if (s_tKey.Read == s_tKey.Write){return KEY_NONE;}else{ret = s_tKey.Buf[s_tKey.Read];if (++s_tKey.Read >= KEY_FIFO_SIZE){s_tKey.Read = 0;}return ret;}
}/*
*********************************************************************************************************
*    函 数 名: bsp_GetKey2
*    功能说明: 从按键FIFO缓冲区读取一个键值。独立的读指针。
*    形    参:  无
*    返 回 值: 按键代码
*********************************************************************************************************
*/
uint8_t bsp_GetKey2(void)
{uint8_t ret;if (s_tKey.Read2 == s_tKey.Write){return KEY_NONE;}else{ret = s_tKey.Buf[s_tKey.Read2];if (++s_tKey.Read2 >= KEY_FIFO_SIZE){s_tKey.Read2 = 0;}return ret;}
}

返回值KEY_NONE = 0, 表示按键缓冲区为空,所有的按键时间已经处理完毕。按键的键值定义在 bsp_key.h文件,下面是具体内容:

typedef enum
{KEY_NONE = 0,            /* 0 表示按键事件 */KEY_1_DOWN,            /* 1键按下 */KEY_1_UP,                /* 1键弹起 */KEY_1_LONG,            /* 1键长按 */KEY_2_DOWN,            /* 2键按下 */KEY_2_UP,                /* 2键弹起 */KEY_2_LONG,            /* 2键长按 */KEY_3_DOWN,            /* 3键按下 */KEY_3_UP,                /* 3键弹起 */KEY_3_LONG,            /* 3键长按 */KEY_4_DOWN,            /* 4键按下 */KEY_4_UP,                /* 4键弹起 */KEY_4_LONG,            /* 4键长按 */KEY_5_DOWN,            /* 5键按下 */KEY_5_UP,                /* 5键弹起 */KEY_5_LONG,            /* 5键长按 */KEY_6_DOWN,            /* 6键按下 */KEY_6_UP,                /* 6键弹起 */KEY_6_LONG,            /* 6键长按 */KEY_7_DOWN,            /* 7键按下 */KEY_7_UP,                /* 7键弹起 */KEY_7_LONG,            /* 7键长按 */KEY_8_DOWN,            /* 8键按下 */KEY_8_UP,                /* 8键弹起 */KEY_8_LONG,            /* 8键长按 *//* 组合键 */KEY_9_DOWN,            /* 9键按下 */KEY_9_UP,                /* 9键弹起 */KEY_9_LONG,            /* 9键长按 */KEY_10_DOWN,            /* 10键按下 */KEY_10_UP,            /* 10键弹起 */KEY_10_LONG,            /* 10键长按 */
}KEY_ENUM;

必须按次序定义每个键的按下、弹起和长按事件,即每个按键对象(组合键也算1个)占用3个数值。我们推荐使用枚举enum, 不用#define的原因:

  • 便于新增键值,方便调整顺序。
  • 使用{ } 将一组相关的定义封装起来便于理解。
  • 编译器可帮我们避免键值重复。

我们来看红外遥控器的键值定义,在bsp_ir_decode.h文件。因为遥控器按键和主板按键共用同一个FIFO,因此在这里我们先贴出这段定义代码,让大家有个初步印象。

/* 定义红外遥控器按键代码, 和bsp_key.h 的物理按键代码统一编码 */
typedef enum
{IR_KEY_STRAT     = 0x80,IR_KEY_POWER     = IR_KEY_STRAT + 0x45,IR_KEY_MENU     = IR_KEY_STRAT + 0x47, IR_KEY_TEST     = IR_KEY_STRAT + 0x44,IR_KEY_UP     = IR_KEY_STRAT + 0x40,IR_KEY_RETURN    = IR_KEY_STRAT + 0x43,IR_KEY_LEFT    = IR_KEY_STRAT + 0x07,IR_KEY_OK        = IR_KEY_STRAT + 0x15,IR_KEY_RIGHT    = IR_KEY_STRAT + 0x09,IR_KEY_0        = IR_KEY_STRAT + 0x16,IR_KEY_DOWN    = IR_KEY_STRAT + 0x19,IR_KEY_C        = IR_KEY_STRAT + 0x0D,IR_KEY_1        = IR_KEY_STRAT + 0x0C,IR_KEY_2        = IR_KEY_STRAT + 0x18,IR_KEY_3        = IR_KEY_STRAT + 0x5E,IR_KEY_4        = IR_KEY_STRAT + 0x08,IR_KEY_5        = IR_KEY_STRAT + 0x1C,IR_KEY_6        = IR_KEY_STRAT + 0x5A,IR_KEY_7        = IR_KEY_STRAT + 0x42,IR_KEY_8        = IR_KEY_STRAT + 0x52,IR_KEY_9        = IR_KEY_STRAT + 0x4A,
}IR_KEY_E;

我们下面来看一段简单的应用。这个应用的功能是:主板K1键控制LED1指示灯;遥控器的POWER键和MENU键控制LED2指示灯。

#include "bsp.h"int main(void)
{uint8_t ucKeyCode;bsp_Init();IRD_StartWork();    /* 启动红外解码 */while(1){bsp_Idle();/* 处理按键事件 */ucKeyCode = bsp_GetKey();if (ucKeyCode > 0){/* 有键按下 */switch (ucKeyCode){case KEY_DOWN_K1:        /* K1键按下 */bsp_LedOn(1);        /* 点亮LED1 */break;case KEY_UP_K1:        /* K1键弹起 */bsp_LedOff(1);    /* 熄灭LED1 */break;                    case IR_KEY_POWER:        /* 遥控器POWER键按下 */bsp_LedOn(1);        /* 点亮LED2 */break;case IR_KEY_MENU:        /* 遥控器MENU键按下 */bsp_LedOff(1);    /* 熄灭LED2 */break;                    case MSG_485_RX:        /* 通信程序的发来的消息 *//* 执行通信程序的指令 */break;default:break;}}}
}

看到这里,想必你已经意识到bsp_PutKey函数的强大之处了,可以将不相关的硬件输入设备统一为一个相同的接口函数。

在上面的应用程序中,我们特意添加了一段红色的代码来解说更高级的用法。485通信程序收到有效的命令后通过 bsp_PutKey(MSG_485_RX)函数可以通知APP应用程序进行进一步加工处理(比如显示接收成功)。这是一种非常好的任务间信息传递方式,它不会破坏程序结构。不必新增全局变量来做这种事情,你只需要添加一个键值代码。

对于简单的程序,可以借用按键FIFO来进行少量的信息传递。对于复杂的应用,我们推荐使用bsp_msg专门来做这种任务间的通信。因为bsp_msg除了传递消息代码外,还可以传递参数结构。

19.3.3 按键检测程序分析

在bsp_key.h 中定了结构体类型KEY_T。

#define KEY_COUNT    10               /* 按键个数, 8个独立建 + 2个组合键 */typedef struct
{/* 下面是一个函数指针,指向判断按键手否按下的函数 */uint8_t (*IsKeyDownFunc)(void); /* 按键按下的判断函数,1表示按下 */uint8_t  Count;        /* 滤波器计数器 */uint16_t LongCount;    /* 长按计数器 */uint16_t LongTime;        /* 按键按下持续时间, 0表示不检测长按 */uint8_t  State;        /* 按键当前状态(按下还是弹起) */uint8_t  RepeatSpeed;    /* 连续按键周期 */uint8_t  RepeatCount;    /* 连续按键计数器 */
}KEY_T;

在bsp_key.c 中定义s_tBtn结构体数组变量。

static KEY_T s_tBtn[KEY_COUNT];
static KEY_FIFO_T s_tKey;        /* 按键FIFO变量,结构体 */

每个按键对象都分配一个结构体变量,这些结构体变量以数组的形式存在将便于我们简化程序代码行数。

使用函数指针IsKeyDownFunc可以将每个按键的检测以及组合键的检测代码进行统一管理。

因为函数指针必须先赋值,才能被作为函数执行。因此在定时扫描按键之前,必须先执行一段初始化函数来设置每个按键的函数指针和参数。这个函数是 void bsp_InitKey(void)。它由bsp_Init()调用。

/*
*********************************************************************************************************
*    函 数 名: bsp_InitKey
*    功能说明: 初始化按键. 该函数被 bsp_Init() 调用。
*    形    参: 无
*    返 回 值: 无
*********************************************************************************************************
*/
void bsp_InitKey(void)
{bsp_InitKeyVar();        /* 初始化按键变量 */bsp_InitKeyHard();        /* 初始化按键硬件 */
}

下面是bsp_InitKeyVar函数的定义:

/*
*********************************************************************************************************
*    函 数 名: bsp_InitKeyVar
*    功能说明: 初始化按键变量
*    形    参:  无
*    返 回 值: 无
*********************************************************************************************************
*/
static void bsp_InitKeyVar(void)
{uint8_t i;/* 对按键FIFO读写指针清零 */s_tKey.Read = 0;s_tKey.Write = 0;s_tKey.Read2 = 0;/* 给每个按键结构体成员变量赋一组缺省值 */for (i = 0; i < KEY_COUNT; i++){s_tBtn[i].LongTime = KEY_LONG_TIME;            /* 长按时间 0 表示不检测长按键事件 */s_tBtn[i].Count = KEY_FILTER_TIME / 2;        /* 计数器设置为滤波时间的一半 */s_tBtn[i].State = 0;                        /* 按键缺省状态,0为未按下 */s_tBtn[i].RepeatSpeed = 0;                    /* 按键连发的速度,0表示不支持连发 */s_tBtn[i].RepeatCount = 0;                    /* 连发计数器 */}/* 如果需要单独更改某个按键的参数,可以在此单独重新赋值 *//* 摇杆上下左右,支持长按1秒后,自动连发 */bsp_SetKeyParam(KID_JOY_U, 100, 6);bsp_SetKeyParam(KID_JOY_D, 100, 6);bsp_SetKeyParam(KID_JOY_L, 100, 6);bsp_SetKeyParam(KID_JOY_R, 100, 6);
}

注意下一下 Count 这个成员变量,没有设置为0。为了避免主板上电的瞬间,检测到一个无效的按键按下或弹起事件。我们将这个滤波计数器的初值设置为正常值的1/2。bsp_key.h中定义了滤波时间和长按时间。

/*按键滤波时间50ms, 单位10ms。只有连续检测到50ms状态不变才认为有效,包括弹起和按下两种事件即使按键电路不做硬件滤波,该滤波机制也可以保证可靠地检测到按键事件
*/
#define KEY_FILTER_TIME   5
#define KEY_LONG_TIME     100            /* 单位10ms, 持续1秒,认为长按事件 */

uint8_t KeyPinActive(uint8_t _id)(会调用函数KeyPinActive判断状态)函数就是最底层的GPIO输入状态判断函数。

/*
*********************************************************************************************************
*    函 数 名: KeyPinActive
*    功能说明: 判断按键是否按下
*    形    参: 无
*    返 回 值: 返回值1 表示按下(导通),0表示未按下(释放)
*********************************************************************************************************
*/
static uint8_t KeyPinActive(uint8_t _id)
{uint8_t level;if ((s_gpio_list[_id].gpio->IDR & s_gpio_list[_id].pin) == 0){level = 0;}else{level = 1;}if (level == s_gpio_list[_id].ActiveLevel){return 1;}else{return 0;}
}/*
*********************************************************************************************************
*    函 数 名: IsKeyDownFunc
*    功能说明: 判断按键是否按下。单键和组合键区分。单键事件不允许有其他键按下。
*    形    参: 无
*    返 回 值: 返回值1 表示按下(导通),0表示未按下(释放)
*********************************************************************************************************
*/
static uint8_t IsKeyDownFunc(uint8_t _id)
{/* 实体单键 */if (_id < HARD_KEY_NUM){uint8_t i;uint8_t count = 0;uint8_t save = 255;/* 判断有几个键按下 */for (i = 0; i < HARD_KEY_NUM; i++){if (KeyPinActive(i)) {count++;save = i;}}if (count == 1 && save == _id){return 1;    /* 只有1个键按下时才有效 */}        return 0;}/* 组合键 K1K2 */if (_id == HARD_KEY_NUM + 0){if (KeyPinActive(KID_K1) && KeyPinActive(KID_K2)){return 1;}else{return 0;}}/* 组合键 K2K3 */if (_id == HARD_KEY_NUM + 1){if (KeyPinActive(KID_K2) && KeyPinActive(KID_K3)){return 1;}else{return 0;}}return 0;
}

在使用GPIO之前,我们必须对GPIO进行配置,比如打开GPIO时钟,设置GPIO输入输出方向,设置上下拉电阻。下面是配置GPIO的代码,也就是bsp_InitKeyHard()函数:

/*
*********************************************************************************************************
*    函 数 名: bsp_InitKeyHard
*    功能说明: 配置按键对应的GPIO
*    形    参:  无
*    返 回 值: 无
*********************************************************************************************************
*/
static void bsp_InitKeyHard(void)
{    GPIO_InitTypeDef gpio_init;uint8_t i;/* 第1步:打开GPIO时钟 */ALL_KEY_GPIO_CLK_ENABLE();/* 第2步:配置所有的按键GPIO为浮动输入模式(实际上CPU复位后就是输入状态) */gpio_init.Mode = GPIO_MODE_INPUT;               /* 设置输入 */gpio_init.Pull = GPIO_NOPULL;                 /* 上下拉电阻不使能 */gpio_init.Speed = GPIO_SPEED_FREQ_VERY_HIGH;  /* GPIO速度等级 */for (i = 0; i < HARD_KEY_NUM; i++){gpio_init.Pin = s_gpio_list[i].pin;HAL_GPIO_Init(s_gpio_list[i].gpio, &gpio_init);    }
}

我们再来看看按键是如何执行扫描检测的。

按键扫描函数bsp_KeyScan10ms ()每隔10ms被执行一次。bsp_RunPer10ms函数在systick中断服务程序中执行。

我们再来看看按键是如何执行扫描检测的。
按键扫描函数bsp_KeyScan10ms ()每隔10ms被执行一次。bsp_RunPer10ms函数在systick中断服务程序中执行。
void bsp_RunPer10ms(void)
{bsp_KeyScan10ms();        /* 扫描按键 */
}

bsp_KeyScan10ms ()函数的实现如下:

/*
*********************************************************************************************************
*    函 数 名: bsp_KeyScan10ms
*    功能说明: 扫描所有按键。非阻塞,被systick中断周期性的调用,10ms一次
*    形    参: 无
*    返 回 值: 无
*********************************************************************************************************
*/
void bsp_KeyScan10ms(void)
{uint8_t i;for (i = 0; i < KEY_COUNT; i++){bsp_DetectKey(i);}
}

每隔10ms所有的按键GPIO均会被扫描检测一次。bsp_DetectKey函数实现如下:

/*
*********************************************************************************************************
*    函 数 名: bsp_DetectKey
*    功能说明: 检测一个按键。非阻塞状态,必须被周期性的调用。
*    形    参: IO的id, 从0开始编码
*    返 回 值: 无
*********************************************************************************************************
*/
static void bsp_DetectKey(uint8_t i)
{KEY_T *pBtn;pBtn = &s_tBtn[i];if (IsKeyDownFunc(i)){if (pBtn->Count < KEY_FILTER_TIME){pBtn->Count = KEY_FILTER_TIME;}else if(pBtn->Count < 2 * KEY_FILTER_TIME){pBtn->Count++;}else{if (pBtn->State == 0){pBtn->State = 1;/* 发送按钮按下的消息 */bsp_PutKey((uint8_t)(3 * i + 1));}if (pBtn->LongTime > 0){if (pBtn->LongCount < pBtn->LongTime){/* 发送按钮持续按下的消息 */if (++pBtn->LongCount == pBtn->LongTime){/* 键值放入按键FIFO */bsp_PutKey((uint8_t)(3 * i + 3));}}else{if (pBtn->RepeatSpeed > 0){if (++pBtn->RepeatCount >= pBtn->RepeatSpeed){pBtn->RepeatCount = 0;/* 常按键后,每隔RepeatSpeed * 10ms发送1个按键 */bsp_PutKey((uint8_t)(3 * i + 1));}}}}}}else{if(pBtn->Count > KEY_FILTER_TIME){pBtn->Count = KEY_FILTER_TIME;}else if(pBtn->Count != 0){pBtn->Count--;}else{if (pBtn->State == 1){pBtn->State = 0;/* 发送按钮弹起的消息 */bsp_PutKey((uint8_t)(3 * i + 2));}}pBtn->LongCount = 0;pBtn->RepeatCount = 0;}
}

对于初学者,这个函数看起来比较吃力,我们拆分进行分析。

pBtn = &s_tBtn[i];

读取相应按键的结构体地址,程序里面每个按键都有自己的结构体。

static KEY_T s_tBtn[KEY_COUNT];
if (IsKeyDownFunc(i))
{这个里面执行的是按键按下的处理
}
else
{这个里面执行的是按键松手的处理或者按键没有按下的处理
}

执行函数IsKeyDownFunc(i)做按键状态判断。

/*
**********************************************************************************
下面这个if语句主要是用于按键滤波前给Count设置一个初值,前面说按键初始化的时候
已经设置了Count = KEY_FILTER_TIME/2
**********************************************************************************
*/
if (pBtn->Count < KEY_FILTER_TIME)
{pBtn->Count = KEY_FILTER_TIME;
}/*
**********************************************************************************
这里实现KEY_FILTER_TIME时间长度的延迟
**********************************************************************************
*/
else if(pBtn->Count < 2 * KEY_FILTER_TIME)
{pBtn->Count++;
}
/*
**********************************************************************************
这里实现KEY_FILTER_TIME时间长度的延迟
**********************************************************************************
*/
else
{
/*
**********************************************************************************
这个State变量是有其实际意义的,如果按键按下了,这里就将其设置为1,如果没有按下这个
变量的值就会一直是0,这样设置的目的可以有效的防止一种情况的出现:比如按键K1在某个
时刻检测到了按键有按下,那么它就会做进一步的滤波处理,但是在滤波的过程中,这个按键
按下的状态消失了,这个时候就会进入到上面第二步else语句里面,然后再做按键松手检测滤波
,滤波结束后判断这个State变量,如果前面就没有检测到按下,这里就不会记录按键弹起。
**********************************************************************************
*/if (pBtn->State == 0){pBtn->State = 1;/* 发送按钮按下的消息 */bsp_PutKey((uint8_t)(3 * i + 1));}if (pBtn->LongTime > 0){if (pBtn->LongCount < pBtn->LongTime){/* 发送按钮持续按下的消息 */if (++pBtn->LongCount == pBtn->LongTime){/* 键值放入按键FIFO */bsp_PutKey((uint8_t)(3 * i + 3));}}else{if (pBtn->RepeatSpeed > 0){if (++pBtn->RepeatCount >= pBtn->RepeatSpeed){pBtn->RepeatCount = 0;/* 长按键后,每隔10ms发送1个按键 */bsp_PutKey((uint8_t)(3 * i + 1));}}}}
}

19.3.4 按键检测采用中断方式还是查询方式

检测按键有中断方式和GPIO查询方式两种。我们推荐大家用GPIO查询方式。

从裸机的角度分析

中断方式:中断方式可以快速地检测到按键按下,并执行相应的按键程序,但实际情况是由于按键的机械抖动特性,在程序进入中断后必须进行滤波处理才能判定是否有效的按键事件。如果每个按键都是独立的接一个IO引脚,需要我们给每个IO都设置一个中断,程序中过多的中断会影响系统的稳定性。中断方式跨平台移植困难。

查询方式:查询方式有一个最大的缺点就是需要程序定期的去执行查询,耗费一定的系统资源。实际上耗费不了多大的系统资源,因为这种查询方式也只是查询按键是否按下,按键事件的执行还是在主程序里面实现。

从OS的角度分析

中断方式:在OS中要尽可能少用中断方式,因为在RTOS中过多的使用中断会影响系统的稳定性和可预见性(抢占式调度的OS基本没有可预见性)。只有比较重要的事件处理需要用中断的方式。

查询方式:对于用户按键推荐使用这种查询方式来实现,现在的OS基本都带有CPU利用率的功能,这个按键FIFO占用的还是很小的,基本都在1%以下。

19.4 按键板级支持包(bsp_key.c)

按键驱动文件bsp_key.c主要实现了如下几个API:

  • KeyPinActive
  • IsKeyDownFunc
  • bsp_InitKey
  • bsp_InitKeyHard
  • bsp_InitKeyVar
  • bsp_PutKey
  • bsp_GetKey
  • bsp_GetKey2
  • bsp_GetKeyState
  • bsp_SetKeyParam
  • bsp_ClearKey
  • bsp_DetectKey
  • bsp_DetectFastIO
  • bsp_KeyScan10ms
  • bsp_KeyScan1ms

所有这些函数在本章的19.3小节都进行了详细讲解,本小节主要是把需要用户调用的三个函数做个说明。

19.4.1 函数bsp_InitKeyHard

函数原型:

/*
*********************************************************************************************************
*    函 数 名: bsp_InitKey
*    功能说明: 初始化按键. 该函数被 bsp_Init() 调用。
*    形    参: 无
*    返 回 值: 无
*********************************************************************************************************
*/
void bsp_InitKey(void)
{bsp_InitKeyVar();        /* 初始化按键变量 */bsp_InitKeyHard();        /* 初始化按键硬件 */
}

函数描述:

此函数主要用于按键的初始化。

使用举例:

底层驱动初始化直接在bsp.c文件的函数bsp_Init里面调用即可。

19.4.2 函数bsp_GetKey

函数原型:

/*
*********************************************************************************************************
*    函 数 名: bsp_GetKey
*    功能说明: 从按键FIFO缓冲区读取一个键值。
*    形    参: 无
*    返 回 值: 按键代码
*********************************************************************************************************
*/
uint8_t bsp_GetKey(void)
{uint8_t ret;if (s_tKey.Read == s_tKey.Write){return KEY_NONE;}else{ret = s_tKey.Buf[s_tKey.Read];if (++s_tKey.Read >= KEY_FIFO_SIZE){s_tKey.Read = 0;}return ret;}
}

函数描述:

此函数用于从FIFO中读取键值。

使用举例:

调用此函数前,务必优先调用函数bsp_InitKey进行初始化。

/*
*********************************************************************************************************
*    函 数 名: main
*    功能说明: c程序入口
*    形    参: 无
*    返 回 值: 错误代码(无需处理)
*********************************************************************************************************
*/
int main(void)
{uint8_t ucKeyCode;        /* 按键代码 */bsp_Init();        /* 硬件初始化 *//* 进入主程序循环体 */while (1){        /* 按键滤波和检测由后台systick中断服务程序实现,我们只需要调用bsp_GetKey读取键值即可。 */ucKeyCode = bsp_GetKey();    /* 读取键值, 无键按下时返回 KEY_NONE = 0 */if (ucKeyCode != KEY_NONE){switch (ucKeyCode){case KEY_DOWN_K1:            /* K1键按下 */printf("K1键按下\r\n");break;case KEY_DOWN_K2:            /* K2键按下 */printf("K2键按下\r\n");break;default:/* 其它的键值不处理 */break;}}}
}

19.4.3 函数bsp_KeyScan10ms

函数原型:

/*
*********************************************************************************************************
*    函 数 名: bsp_KeyScan10ms
*    功能说明: 扫描所有按键。非阻塞,被systick中断周期性的调用,10ms一次
*    形    参: 无
*    返 回 值: 无
*********************************************************************************************************
*/
void bsp_KeyScan10ms(void)
{uint8_t i;for (i = 0; i < KEY_COUNT; i++){bsp_DetectKey(i);}
}

函数描述:

此函数是按键的主处理函数,用于检测和存储按下、松手、长按等状态。

使用举例:

调用此函数前,务必优先调用函数bsp_InitKey进行初始化。

另外,此函数需要周期性调用,每10ms调用一次。

  • 如果是裸机使用,将此函数放在bsp.c文件的bsp_RunPer10ms函数里面即可,这个函数是由滴答定时器调用的,也就是说,大家要使用按键,定时器的初始化函数bsp_InitTimer一定要调用。
  • 如果是RTOS使用,需要开启一个10ms为周期的任务调用函数bsp_KeyScan10ms。

19.5 按键FIFO驱动移植和使用

按键移植步骤如下:

  • 第1步:复制bsp_key.c和bsp_key.c到自己的工程。
  • 第2步:根据自己使用的独立按键个数和组合键个数,修改几个地方。
#define HARD_KEY_NUM    8                 /* 实体按键个数 */
#define KEY_COUNT   (HARD_KEY_NUM + 2)   /* 8个独立建 + 2个组合按键 */
  • 第3步:根据使用的引脚时钟,修改下面函数:
/* 使能GPIO时钟 */
#define ALL_KEY_GPIO_CLK_ENABLE() {    \__HAL_RCC_GPIOB_CLK_ENABLE();    \__HAL_RCC_GPIOC_CLK_ENABLE();    \__HAL_RCC_GPIOG_CLK_ENABLE();    \__HAL_RCC_GPIOH_CLK_ENABLE();    \__HAL_RCC_GPIOI_CLK_ENABLE();    \};
  • 第4步:根据使用的具体引脚,修改如下函数,第3列参数低电平表示按下或者高电平表示按下:
/* GPIO和PIN定义 */
static const X_GPIO_T s_gpio_list[HARD_KEY_NUM] = {{GPIOI, GPIO_PIN_8, 0},        /* K1 */{GPIOC, GPIO_PIN_13, 0},    /* K2 */{GPIOH, GPIO_PIN_4, 0},        /* K3 */{GPIOG, GPIO_PIN_2, 0},        /* JOY_U */    {GPIOF, GPIO_PIN_10, 0},    /* JOY_D */{GPIOG, GPIO_PIN_3, 0},        /* JOY_L */    {GPIOG, GPIO_PIN_7, 0},        /* JOY_R */    {GPIOI, GPIO_PIN_11, 0},    /* JOY_OK */
};    
  • 第5步:根据使用的组合键个数,在函数IsKeyDownFunc里面添加相应个数的函数:
    /* 组合键 K1K2 */if (_id == HARD_KEY_NUM + 0){if (KeyPinActive(KID_K1) && KeyPinActive(KID_K2)){return 1;}else{return 0;}}

第2行ID表示HARD_KEY_NUM + 0的组合键,HARD_KEY_NUM + 1表示下一个组合键,以此类推。

另外就是,函数KeyPinActive的参数是表示检测哪两个按键,设置0的时候表示第4步里面的第1组按键,设置为1表示第2组按键,以此类推。

  • 第6步:主要用到HAL库的GPIO驱动文件,简单省事些可以添加所有HAL库.C源文件进来。
  • 第7步:移植完整,应用方法看本章节配套例子即可。

特别注意,别忘了每10ms调用一次按键检测函数bsp_KeyScan10ms。

19.6 实验例程设计框架

通过程序设计框架,让大家先对配套例程有一个全面的认识,然后再理解细节,本次实验例程的设计框架如下:

  第1阶段,上电启动阶段:

  • 这部分在第14章进行了详细说明。

  第2阶段,进入main函数:

  • 第1部分,硬件初始化,主要是HAL库、系统时钟、滴答定时器、按键等。
  • 第2部分,应用程序设计部分,实现了一个按键应用。
  • 第3部分,按键扫描程序每10ms在滴答定时中断执行一次。

19.7 实验例程说明(MDK)

配套例子:

V6-003_按键检测(软件滤波,FIFO机制)

实验目的:

  1. 学习按键的按下,弹起,长按和组合键的实现。

实验内容:

  1. 启动一个自动重装软件定时器,每100ms翻转一次LED2。

实验操作:

  1. 3个独立按键和5向摇杆按下时均有串口消息打印。
  2. 5向摇杆的左键和右键长按时,会有连发的串口消息。
  3. 独立按键K1和K2按键按下,串口打印消息。

上电后串口打印的信息:

波特率 115200,数据位 8,奇偶校验位无,停止位 1

程序设计:

系统栈大小分配:

硬件外设初始化

硬件外设的初始化是在 bsp.c 文件实现:

/*
*********************************************************************************************************
*    函 数 名: bsp_Init
*    功能说明: 初始化所有的硬件设备。该函数配置CPU寄存器和外设的寄存器并初始化一些全局变量。只需要调用一次
*    形    参:无
*    返 回 值: 无
*********************************************************************************************************
*/
void bsp_Init(void)
{/* STM32H429 HAL 库初始化,此时系统用的还是F429自带的16MHz,HSI时钟:- 调用函数HAL_InitTick,初始化滴答时钟中断1ms。- 设置NVIV优先级分组为4。*/HAL_Init();/* 配置系统时钟到168MHz- 切换使用HSE。- 此函数会更新全局变量SystemCoreClock,并重新配置HAL_InitTick。*/SystemClock_Config();/* Event Recorder:- 可用于代码执行时间测量,MDK5.25及其以上版本才支持,IAR不支持。- 默认不开启,如果要使能此选项,务必看V5开发板用户手册第8章*/
#if Enable_EventRecorder == 1  /* 初始化EventRecorder并开启 */EventRecorderInitialize(EventRecordAll, 1U);EventRecorderStart();
#endifbsp_InitKey();        /* 按键初始化,要放在滴答定时器之前,因为按钮检测是通过滴答定时器扫描 */bsp_InitTimer();      /* 初始化滴答定时器 */bsp_InitUart();    /* 初始化串口 */bsp_InitExtIO();   /* 初始化扩展IO */bsp_InitLed();        /* 初始化LED */
}

每10ms调用一次按键检测:

按键检测是在滴答定时器中断里面实现,每10ms执行一次检测。

/*
*********************************************************************************************************
*    函 数 名: bsp_RunPer10ms
*    功能说明: 该函数每隔10ms被Systick中断调用1次。详见 bsp_timer.c的定时中断服务程序。一些处理时间要求
*              不严格的任务可以放在此函数。比如:按键扫描、蜂鸣器鸣叫控制等。
*    形    参: 无
*    返 回 值: 无
*********************************************************************************************************
*/
void bsp_RunPer10ms(void)
{bsp_KeyScan10ms();
}

主功能:

主功能的实现主要分为两部分:

  • 启动一个自动重装软件定时器,每100ms翻转一次LED2
  • 按键消息的读取,检测到按下后,做串口打印。
/*
*********************************************************************************************************
*    函 数 名: main
*    功能说明: c程序入口
*    形    参: 无
*    返 回 值: 错误代码(无需处理)
*********************************************************************************************************
*/
int main(void)
{uint8_t ucKeyCode;        /* 按键代码 */bsp_Init();        /* 硬件初始化 */PrintfLogo();    /* 打印例程名称和版本等信息 */PrintfHelp();    /* 打印操作提示 */bsp_StartAutoTimer(0, 100); /* 启动1个100ms的自动重装的定时器 *//* 进入主程序循环体 */while (1){bsp_Idle();        /* 这个函数在bsp.c文件。用户可以修改这个函数实现CPU休眠和喂狗 *//* 判断定时器超时时间 */if (bsp_CheckTimer(0))    {/* 每隔100ms 进来一次 */  bsp_LedToggle(2);            }/* 按键滤波和检测由后台systick中断服务程序实现,我们只需要调用bsp_GetKey读取键值即可。 */ucKeyCode = bsp_GetKey();    /* 读取键值, 无键按下时返回 KEY_NONE = 0 */if (ucKeyCode != KEY_NONE){switch (ucKeyCode){case KEY_DOWN_K1:            /* K1键按下 */printf("K1键按下\r\n");break;case KEY_UP_K1:                /* K1键弹起 */printf("K1键弹起\r\n");break;case KEY_DOWN_K2:            /* K2键按下 */printf("K2键按下\r\n");break;case KEY_UP_K2:                /* K2键弹起 */printf("K2键弹起\r\n");break;case KEY_DOWN_K3:            /* K3键按下 */printf("K3键按下\r\n");break;case KEY_UP_K3:                /* K3键弹起 */printf("K3键弹起\r\n");break;case JOY_DOWN_U:            /* 摇杆UP键按下 */printf("摇杆上键按下\r\n");break;case JOY_DOWN_D:            /* 摇杆DOWN键按下 */printf("摇杆下键按下\r\n");break;case JOY_DOWN_L:            /* 摇杆LEFT键按下 */printf("摇杆左键按下\r\n");break;case JOY_LONG_L:            /* 摇杆LEFT键长按 */printf("摇杆左键长按\r\n");break;case JOY_DOWN_R:            /* 摇杆RIGHT键按下 */printf("摇杆右键按下\r\n");break;case JOY_LONG_R:            /* 摇杆RIGHT键长按 */printf("摇杆右键长按\r\n");break;case JOY_DOWN_OK:            /* 摇杆OK键按下 */printf("摇杆OK键按下\r\n");break;case JOY_UP_OK:            /* 摇杆OK键弹起 */printf("摇杆OK键弹起\r\n");break;case SYS_DOWN_K1K2:            /* 摇杆OK键弹起 */printf("K1和K2组合键按下\r\n");break;default:/* 其它的键值不处理 */break;}}}
}

19.8 实验例程说明(IAR)

配套例子:

V6-003_按键检测(软件滤波,FIFO机制)

实验目的:

  1. 学习按键的按下,弹起,长按和组合键的实现。

实验内容:

  1. 启动一个自动重装软件定时器,每100ms翻转一次LED2。

实验操作:

  1. 3个独立按键和5向摇杆按下时均有串口消息打印。
  2. 5向摇杆的左键和右键长按时,会有连发的串口消息。
  3. 独立按键K1和K2按键按下,串口打印消息。

上电后串口打印的信息:

波特率 115200,数据位 8,奇偶校验位无,停止位 1

程序设计:

系统栈大小分配:

硬件外设初始化

硬件外设的初始化是在 bsp.c 文件实现:

/*
*********************************************************************************************************
*    函 数 名: bsp_Init
*    功能说明: 初始化所有的硬件设备。该函数配置CPU寄存器和外设的寄存器并初始化一些全局变量。只需要调用一次
*    形    参:无
*    返 回 值: 无
*********************************************************************************************************
*/
void bsp_Init(void)
{/* STM32H429 HAL 库初始化,此时系统用的还是F429自带的16MHz,HSI时钟:- 调用函数HAL_InitTick,初始化滴答时钟中断1ms。- 设置NVIV优先级分组为4。*/HAL_Init();/* 配置系统时钟到168MHz- 切换使用HSE。- 此函数会更新全局变量SystemCoreClock,并重新配置HAL_InitTick。*/SystemClock_Config();/* Event Recorder:- 可用于代码执行时间测量,MDK5.25及其以上版本才支持,IAR不支持。- 默认不开启,如果要使能此选项,务必看V5开发板用户手册第8章*/
#if Enable_EventRecorder == 1  /* 初始化EventRecorder并开启 */EventRecorderInitialize(EventRecordAll, 1U);EventRecorderStart();
#endifbsp_InitKey();        /* 按键初始化,要放在滴答定时器之前,因为按钮检测是通过滴答定时器扫描 */bsp_InitTimer();      /* 初始化滴答定时器 */bsp_InitUart();    /* 初始化串口 */bsp_InitExtIO();   /* 初始化扩展IO */bsp_InitLed();        /* 初始化LED */
}

每10ms调用一次按键检测:

按键检测是在滴答定时器中断里面实现,每10ms执行一次检测。

/*
*********************************************************************************************************
*    函 数 名: bsp_RunPer10ms
*    功能说明: 该函数每隔10ms被Systick中断调用1次。详见 bsp_timer.c的定时中断服务程序。一些处理时间要求
*              不严格的任务可以放在此函数。比如:按键扫描、蜂鸣器鸣叫控制等。
*    形    参: 无
*    返 回 值: 无
*********************************************************************************************************
*/
void bsp_RunPer10ms(void)
{bsp_KeyScan10ms();
}

主功能:

主功能的实现主要分为两部分:

  • 启动一个自动重装软件定时器,每100ms翻转一次LED2
  • 按键消息的读取,检测到按下后,做串口打印。
/*
*********************************************************************************************************
*    函 数 名: main
*    功能说明: c程序入口
*    形    参: 无
*    返 回 值: 错误代码(无需处理)
*********************************************************************************************************
*/
int main(void)
{uint8_t ucKeyCode;        /* 按键代码 */bsp_Init();        /* 硬件初始化 */PrintfLogo();    /* 打印例程名称和版本等信息 */PrintfHelp();    /* 打印操作提示 */bsp_StartAutoTimer(0, 100); /* 启动1个100ms的自动重装的定时器 *//* 进入主程序循环体 */while (1){bsp_Idle();        /* 这个函数在bsp.c文件。用户可以修改这个函数实现CPU休眠和喂狗 *//* 判断定时器超时时间 */if (bsp_CheckTimer(0))    {/* 每隔100ms 进来一次 */  bsp_LedToggle(2);            }/* 按键滤波和检测由后台systick中断服务程序实现,我们只需要调用bsp_GetKey读取键值即可。 */ucKeyCode = bsp_GetKey();    /* 读取键值, 无键按下时返回 KEY_NONE = 0 */if (ucKeyCode != KEY_NONE){switch (ucKeyCode){case KEY_DOWN_K1:            /* K1键按下 */printf("K1键按下\r\n");break;case KEY_UP_K1:                /* K1键弹起 */printf("K1键弹起\r\n");break;case KEY_DOWN_K2:            /* K2键按下 */printf("K2键按下\r\n");break;case KEY_UP_K2:                /* K2键弹起 */printf("K2键弹起\r\n");break;case KEY_DOWN_K3:            /* K3键按下 */printf("K3键按下\r\n");break;case KEY_UP_K3:                /* K3键弹起 */printf("K3键弹起\r\n");break;case JOY_DOWN_U:            /* 摇杆UP键按下 */printf("摇杆上键按下\r\n");break;case JOY_DOWN_D:            /* 摇杆DOWN键按下 */printf("摇杆下键按下\r\n");break;case JOY_DOWN_L:            /* 摇杆LEFT键按下 */printf("摇杆左键按下\r\n");break;case JOY_LONG_L:            /* 摇杆LEFT键长按 */printf("摇杆左键长按\r\n");break;case JOY_DOWN_R:            /* 摇杆RIGHT键按下 */printf("摇杆右键按下\r\n");break;case JOY_LONG_R:            /* 摇杆RIGHT键长按 */printf("摇杆右键长按\r\n");break;case JOY_DOWN_OK:            /* 摇杆OK键按下 */printf("摇杆OK键按下\r\n");break;case JOY_UP_OK:            /* 摇杆OK键弹起 */printf("摇杆OK键弹起\r\n");break;case SYS_DOWN_K1K2:            /* 摇杆OK键弹起 */printf("K1和K2组合键按下\r\n");break;default:/* 其它的键值不处理 */break;}}}
}

19.9 总结

这个方案在实际项目中已经经过千锤百炼,大家可以放心使用。建议熟练掌握其用法。

【STM32F429开发板用户手册】第19章 STM32F429的GPIO应用之按键FIFO相关推荐

  1. 【STM32F407开发板用户手册】第19章 STM32F407的GPIO应用之按键FIFO

    最新教程下载:http://www.armbbs.cn/forum.php?mod=viewthread&tid=93255 第19章       STM32F407的GPIO应用之按键FIF ...

  2. 【STM32H7教程】第19章 STM32H7的GPIO应用之按键FIFO

    完整教程下载地址:http://www.armbbs.cn/forum.php?mod=viewthread&tid=86980 第19章       STM32H7的GPIO应用之按键FIF ...

  3. 【STM32H7教程】第19章 STM32H7的GPIO应用之按键FIFO

    完整教程下载地址:http://forum.armfly.com/forum.php?mod=viewthread&tid=86980 第19章       STM32H7的GPIO应用之按键 ...

  4. 【STM32F429开发板用户手册】第46章 STM32F429的DMA2D应用之刷色块,位图和Alpha混合

    最新教程下载:http://www.armbbs.cn/forum.php?mod=viewthread&tid=93255 第46章       STM32F429的DMA2D应用之刷色块, ...

  5. 【STM32F429开发板用户手册】第33章 STM32F429的SPI总线应用之驱动DAC8563(双通道,16bit分辨率,正负10V)

    最新教程下载:http://www.armbbs.cn/forum.php?mod=viewthread&tid=93255 第33章       STM32F429的SPI总线应用之驱动DA ...

  6. 【STM32F429开发板用户手册】第26章 STM32F429的定时器应用之TIM1-TIM14的中断实现

    最新教程下载:http://www.armbbs.cn/forum.php?mod=viewthread&tid=93255 第26章       STM32F429的定时器应用之TIM1-T ...

  7. 【STM32F429开发板用户手册】第35章 STM32F429的FMC总线应用之驱动AD7606(8通道同步采样, 16bit, 正负10V)

    最新教程下载:http://www.armbbs.cn/forum.php?mod=viewthread&tid=93255 第35章       STM32F429的FMC总线应用之驱动AD ...

  8. 【STM32F429开发板用户手册】第41章 STM32F429的LTDC应用之LCD汉字显示和2D图形显示

    最新教程下载:http://www.armbbs.cn/forum.php?mod=viewthread&tid=93255 第41章       STM32F429的LTDC应用之LCD汉字 ...

  9. 【STM32F429开发板用户手册】第18章 STM32F429的GPIO应用之跑马灯

    最新教程下载:http://www.armbbs.cn/forum.php?mod=viewthread&tid=93255 第18章       STM32F429的GPIO应用之跑马灯 本 ...

最新文章

  1. MySQL 里的 Timestrap 和 DateTime 和 Java 中的 Date
  2. 好程序员web前端CSS选择符(选择器):表示要定义样式的对象
  3. ECharts x时间轴不连续实现
  4. linux在所有文件中查找某一个字符
  5. 看风水用什么罗盘最好_兰花用什么花盆栽植最好?
  6. React开发(166):ant design form 设置值
  7. 最短路径 一 Dijkstra 模板(O(n^2))
  8. python 获取照片拍摄时间_Python实现获取照片拍摄日期并重命名的方法
  9. html5三角函数怎么用,HTML5(五)html5<canvas路径和三角函数的故事>(下)
  10. 高手对中科院考博英语的体会
  11. FreeRTOS-TaskNotify
  12. 适用于 Windows 10 的触摸板手势
  13. 如何用艺术字制作水印?教你一招快速制作专属水印
  14. 2021-04-15 三级管npn和pnp的区别
  15. 运行时 Entry name .... .. collided
  16. 在vscode中使用latex高效书写论文教程
  17. 禁止input密码自动填充及浏览器记住密码完整解决方案
  18. word怎么删除空白页,Word Mac删除空白页面的图文方法
  19. Unbuntu远程电脑死机怎么解决
  20. 激光雷达赛道“白刃战”?硅光芯片级FMCW技术进入量产周期

热门文章

  1. 计算机制作请柬,婚礼纪能用电脑制作电子请柬吗?电子请柬有哪些注意事项?...
  2. 0002 计算长方形周长和面积
  3. 下一个倒下的是不是Genesis
  4. 完美世界怎么修改服务器经验,三招教你快速升级
  5. 计算机语言学专业排名,语言学专业排名
  6. MusicPlay播放器
  7. 五个老司机才知道的手机APP 你手机上有吗
  8. 灵魂拷问:TCP 四次挥手,可以变成三次吗?
  9. TCP三次握手和四次挥手的全过程
  10. Gbase8a数据库安装与使用