前段时间,项目组接到一个客户需求,需要开发星闪无线健鼠的功能。在评估需求时发现对整个处理流程比较模糊,所以花时间对有线键鼠和无线键鼠的处理流程从整体上进行了一次梳理,用来借鉴开发新项目。结果发现一个简单的USB键鼠输入设备,整体框架功能和底层实现还是比较复杂的,涉及到的知识点很多。
以下以usb接口的有线和蓝牙键鼠为例来介绍具体的实现流程。
一. USB有线键盘
1. 框图如下:
2.模块介绍:
1)适配器端(键盘芯片):
---机械键盘矩阵: 键盘矩阵由多个行和列组成,按键与矩阵的交叉点连接在一起。当按下某个按键时,电路会识别出对应的行和列信息;
---键盘控制器: 键盘控制器通过扫描键盘矩阵来检测按键的状态变化, 并将对应的按键编码经过特殊协议的封装再发送给USB接口控制器;
---HID协议转换: 将采集的按健编码封装成HID协议格式再通过USB接口传输给计算器;HID协议定义了标准的数据格式、命令结构以及传输方式,使得不同的输入设备间能相互识别和兼容。
---USB接口控制器: USB接口控制器则将按键编码封装成USB协议格式,通过USB接口传输给计算机。
2)主控端:
---USB Host控制器;用于和usb从设备的底层数据通信。USB Host提供了主机控制器的驱动程序,并管理USB Hub以及在Hub上连接的USB设备;
---USB core总线协议:USB Core是内核设计的一个抽象层,目的是将Class Driver和USB Host控制器 Driver分隔开,使两者都依赖一个稳定的中间层;USB Core向上提供通信接口,向下统一管理USB设备,同时完成USB设备和USB Driver的匹配工作;
---HID协议转换:基于USB协议的实现。用于解析HID设备封装的用户操作和输入信息;HID协议定义了标准的数据格式、命令结构以及传输方式,使得不同厂商生产的输入设备可以被操作系统识别和兼容。
---input子系统: input子系统处理输入事务,任何输入设备的驱动程序都可以通过input 输入子系统提供的接口注册到内核,利用子系统提供的功能来与用户空间交互。 input 子系统用到了驱动分层模型,编写驱动程序的时只需要关注中间的驱动层、核心层和事件层。
a.驱动层:输入设备的具体驱动程序,比如按键驱动程序,向内核层报告输入内容
b. 核心层:承上启下,为驱动层提供输入设备注册和操作接口。通知事件层对输入事件进行处理,链接其他两个层之间的纽带与桥梁,向下提供驱动层的接口, 向上提供事件处理层的接口。
c.事件层:主要和用户空间进行交互,将硬件驱动层传来的事件报告给用户程序
各层之间通信的基本单位是事件, 任何一个输入设备的动作都可以抽象成一种事件, 如键盘的按下,触摸屏的按下, 鼠标的移动等。 事件有三种属性: 类型(type), 编码(code),值(value), input 子系统支持的所有事件都定义在 input.h 中, 包括所有支持的类型, 所属类型支持的编码等。
事件传送的方向:硬件驱动层-->子系统核心-->事件处理层-->用户空间。 在节点/dev/input 下面则是输入设备的节点。
---usb设备驱动: 提供设备识别的初始化工作和设备具体操作的接口方法,类似于这个设备的使用说明书。以及读取数据后,会做一些键值映射和转换的功能,注册input子系统,满足标准的input执行流程;
其中除usb设备驱动需要自己开发外,其它模块linux内核都已经实现.
3.流程说明:
下面来详细说明每一步的执行过程:
1)USB设备的枚举和识别过程:
a.主机端的USB集线器监视着它的每个端口的信号线的电压,当USB设备插入主机时,信号线的电平会发生变化,此时主机知道有新设备插入;
b.当主机检测到设备的插入后会重启这个设备,接着主机发出Get_Port_Status请求来验证设备是否已经重启,设备重启后主机通过检测根信号线的电平状态判断设备的速度;
c. 主机发送第一次Get_Descriptor(wValue字段的高字节为0x01,表示设备描述符)请求取得设备描述符,设备描述符提供了设备的多种信息, 包括:设备通讯终端0的最大包的大小,设备支持的配置号以及有关这个设备的其它信息,主机通过对这些信息的分析以确定接下来的通信动作;
d.设备描述符里规定了设备一个或多个配置描述符,主机再次或多次发出Get_Descriptor(wValue字段的高字节为0x02,表示配置描述符)指令来读取这些配置描述符,第一次只读出配置描述符的前9个字节,这9个字节里包含了配置描述符和它的所有从属描述符(接口描述符、端点描述符)的总长度, 然后主机根据这个长度读出设备的所有配置描述符(包括其所有从属描述符)。
e. 在读取完配置描述符后,若之间读取的设置描述中指定了相关字符串描述符(用来描述厂商、产品和设备序列号信息的)的索引, 主机将发出若干次Get_Descriptor(wValue字段的高字节为0x03,表示字符串描述符)命令来获得这些字符串描述,此时主机将会弹出窗口, 展示发现新设备的信息,产商、产品描述、型号等。
f. 在主机已经从它的描述符中知道了能够知道的所有信息后,便开始调用match函数通过PID/VID为这个设备匹配对应的驱动,如果没有找到,则会停止接下来的流程;
g.加载完USB设备驱动,执行probe函数初始化完成后,主机发送Set_Configuration命令请求为该设备选择一个合适的配置。
至此,USB设备枚举过程结束,USB通道建立,设备可以正常使用了。
2) USB设备和驱动的匹配说明;
linux系统的USB驱动模型是由三部分组成:设备(结构体device)、驱动(结构体device_driver)和总线(结构体bus_type)。 总线维护着设备和驱动两个不同的队列,bus_type是两者的中间纽带,usb device 和driver间的通信都是通过bus_type提供的注册接口,两者在注册到总线的过程时都会触发总线的匹配流程。
3)设备端键值数据采集和USB输入;
主要由两部分组成:键盘驱动电路和键盘控制器;
a.键盘驱动电路负责将键盘的电信号转换为数字信号,以便键盘控制器能够正确解析扫描码, 键盘驱动电路通常包括滤波电路、键盘编码电路等;
b.键盘通过扫描矩阵的方式来检测按键的状态,键盘控制器负责将按键的状态转换为扫描码, 并经过USB HID协议封装后发送给USB主控端;
4)获取HID报告并解析(
drivers/hid/usbhid/hid-core.c):
主机端收USB数据后判断为hid格式数据后,通过hid-core提供的方法,解析出具体的键值数据;
具体可看 hid_irq_in(struct urb *urb)函数中的 hid_input_report(urb->context, HID_INPUT_REPORT,urb->transfer_buffer, urb->actual_length, 1)方法
5)设备驱动中断接收处理和键值转换;
a. USB鼠标驱动程序(
drivers/hid/usbhid/usbmouse.c):
通过中断获取urb数据,并根据设备类型按其格式通过input子模块进行数据上报。
b. USB键盘驱动程序(
drivers/hid/usbhid/usbkbd.c):
内核中的键盘驱动程序负责接收键盘发送的扫描码(经过HID解析后),并将上述扫描码通过键盘映射表转换为操作系统能够理解的字符编码。 不同的操作系统可能有不同的键盘驱动程序,但其工作原理大致相同。
6)通过input子系统上报给用户空间;
---input_dev 注册过程:
①使用 input_allocate_device 函数申请一个 input_dev
②初始化 input_dev的事件类型以及事件值
③使用 input_register_device 函数向 Linux 系统注册初始化好的 input_dev
④先使用input_unregister_device函数注销掉注册的input_dev,然后使用 input_free_device 函数释放掉申请的 input_dev
---上报输入事件
在 input 设备驱动中申请、 注册完成 input_dev 结构体后, 还不能正常使用 input 子系统, 因为 input设备是输入一些信息,但是 Linux 内核还不清楚输入的信息表示什么意思, 所以需要驱动获取到具体的输入值, 或者说输入事件, 然后将输入事件上报给 Linux 内核。比如按键设备, 需要在按键产生后将按键值上报给 Linux 内核, Linux 内核获取到具体的按键值后, 才会执行相应的功能。 不同的事件上报的函数不同。
input_event 函数用于上报指定的事件以及对应的值
void input_event(struct input_dev *dev,unsigned int type,unsigned int code,int value)
---dev:需要上报的 input_dev
---type: 上报的事件类型,比如 EV_KEY
---code: 事件码,也就是注册的按键值,比如 KEY_0、 KEY_1
---value:事件值,比如 1 表示按键按下, 0 表示按键松开
常用的事件上报函数:
void input_report_key(struct input_dev *dev,unsigned int code, int value)
void input_report_rel(struct input_dev *dev, unsigned int code, int value)
void input_report_abs(struct input_dev *dev, unsigned int code, int value)
void input_report_ff_status(struct input_dev *dev, unsigned int code, int value)
void input_report_switch(struct input_dev *dev, unsigned int code, int value)
void input_mt_sync(struct input_dev *dev)
当上报事件以还需要使用 input_sync 函数来通知Linux 内核 input 子系统上报结束,本质是上报一个同步事件
void input_sync(struct input_dev *dev)
7)用户空间键值获取、解析和处理;
(1)Linux 内核使用 input_event 这个结构体来表示所有的输入事件,定义在
include/uapi/linux/input.h 文件中
struct input_event
{
struct timeval time;
__u16 type;
__u16 code;
__s32 value;
};
---tv_sec 和 tv_usec 这两个成员变量都为 long 类型,32位,event 事件上报数据的超时时间;
---type: 事件类型,比如 EV_KEY,表示此次事件为按键事件,此成员变量为 16 位。
---code: 事件码,比如在 EV_KEY 事件中 code 就表示具体的按键码,如: KEY_0、 KEY_1这些按键。此成员变量为 16 位
---value: 按键值,比如 EV_KEY 事件中 value 就是按键值,表示是否有按键被按下,1:说明按键按下;0:没有按键按下或者按键松开;
所有的输入设备都是按照 input_event 结构体传递给用户,用户应用程序可以通过 input_event 来获取到具体的输入事件或相关的值。在加载驱动模块之后,会在/dev/input 目录下event2文件;
(2)示例程序(测试键值):
#include
#include <sys/types.h>
#include <sys/stat.h>
#include
#include
#include <linux/input.h>
int main(int argc, char const *argv[])
{
//打开设备文件
int fd;
int retval;
fd_set readfds;
struct timeval tv;
if((fd = open("/dev/input/event0", O_RDONLY)) == -1)
{
perror("open error");
return -1;
}
//读取文件内容
struct input_event mykey;
while(1){
FD_ZERO(&readfds);
FD_SET(fd, &readfds);
if((retval = select(fd+1, &readfds, NULL, NULL, &tv)) == 1)
{
if(read(fd, &mykey, sizeof(mykey)) == sizeof(mykey)){
// 事件类型 鼠标或者按键
if(mykey.type == EV_KEY)
{
printf("--------------------\n");
printf("type = %u.\n", mykey.type);
printf("code = %u.\n", mykey.code);
printf("value = %u.\n", mykey.value); /* 按键是按下还是释放,0释放、1按下、2长按 */
switch (inputevent.type)
{
case EV_KEY:
if (inputevent.code < BTN_MISC) /* 键盘键值 */
{
printf("key %d %s\r\n", mykey.code, mykey.value ? "press" : "release");
}
else
{
printf("button %d %s\r\n", mykey.code, mykey.value ? "press" : "release");
}
break;
/* 其他类型的事件,自行处理 */
case EV_REL:
break;
case EV_ABS:
break;
case EV_MSC:
break;
case EV_SW:
break;
}
}
}
}
return 0;
}
二. 蓝牙usb无线键鼠:
1. 简介:
通过蓝牙技术或2.4GHz无线技术,无线键鼠实现了与电脑的无线连接和通信。
具体工作原理如下:
1) 无线键盘和鼠标内部搭载了2.4GHz蓝牙发射模块,把检测到的用户输入信号或键值经过HID协议封装后,再通过蓝牙发送到主机接收端;
2) 主机端有usb接口的蓝牙接收器,主要用于接收无线鼠标或键盘发送的数据,并通过USB接口与主机通信;
2. 框图如下:
3. 与有线usb键鼠区别:
从两个框图对比可以看出,USB无线鼠标和有线鼠标两部分的主要区别在适配器部分的工作量,以及主机host端 HID协议和driver有区别,其它部分基本一样,区别如下:
1)输入键值的采集和hid协议封装是在蓝牙发射端处理的,无线usb接收端仅做数据透传处理,不会进行hid封装;
2)主机会把usb 蓝牙接收器识别成普通usb设备,而不是hid设备;
3)hid协议的解析是在net/bluetooth/hidp子系统下处理的,有线usb键鼠hid协议则是在/driver/usbhid模块下执行;
4). 有线USB键鼠driver1主要工作是接收到经过HID协议解析后的数据,再根据健值转换并封装成满足input event需要数据格式,并调用input 子系统上报给用户空间;
5) 无线USB键鼠中的driver2部分主要工作除了有线鼠标driver1的功能外,还有就是需要完成ble配置相关的接口,如:模式设置,扫描、广播、配对、回连、通信等接口。
以上即为无线和有线键鼠工作流程的简单分析,记下笔记,后期理解深入后再补充完善。