百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

Linux系统USB有线和无线键鼠处理流程分析

ahcoder 2025-03-13 12:37 8 浏览

前段时间,项目组接到一个客户需求,需要开发星闪无线健鼠的功能。在评估需求时发现对整个处理流程比较模糊,所以花时间对有线键鼠和无线键鼠的处理流程从整体上进行了一次梳理,用来借鉴开发新项目。结果发现一个简单的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配置相关的接口,如:模式设置,扫描、广播、配对、回连、通信等接口。

以上即为无线和有线键鼠工作流程的简单分析,记下笔记,后期理解深入后再补充完善。

相关推荐

ARM64内核内存布局图(ARM64内核内存布局图解)

ARM64架构处理器采用48位物理寻址机制,最大可以寻找到256TB的物理地址空间。对于目前的应用来说已经足够了,不需要扩展到64位的物理地址寻址。虚拟地址也同样最大支持48位支持,所以在处理器的架构...

ARM64 linux 调试串口通信(ARM64 linux 调试串口通信实验报告)

ARM64linux调试串口通信随着国产机普及很多工作也转移到了新平台上,以前调试设备用的笔记本电脑也换成新国产ARM64架构的了。本文以绿联CM204USB-A转RJ45Console调试线...

Gentoo Linux 终止对 Itanium IA-64 体系的支持

GentooLinux是最后几个继续维护Itanium(IA-64)架构构建的Linux发行版之一,但现在这些已停产的英特尔处理器正在逐步淘汰。由于Linux6.7内核放弃了对Itan...

如何检查 Linux 系统是 32 位还是 64 位?这9个命令查的又快又准!

在Linux系统中,位数(bit)通常指的是CPU架构的位宽,即CPU一次能够处理的数据量。32位系统和64位系统在内存寻址能力、计算性能和软件支持上存在显著差异:「32位系统」:...

调出好画面!带你玩转飞凌嵌入式AM62x开发板的显示接口

来源:飞凌嵌入式官网“显示”是嵌入式开发板最为重要的功能之一,能够支持更多种类、更高规格的显示接口,意味着它能够应对的使用场景也更加广泛。每一款嵌入式开发板在出厂前都会做屏幕调试,但在客户的实际项目开...

带你玩转AM62x开发板的显示接口——LVDS的显示和修改方式

此前小编已为大家介绍过OK6254-C开发板的RGB显示和修改方式,今天将继续为大家介绍OK6254-C开发板的LVDS显示和修改方式。话不多说,我们进入正题。1、LVDS接口规格飞凌嵌入式OK62...

AM335x继任者?AM6254性能解析(am2361p)

飞凌嵌入式FET6254-C核心板基于TISitaraTMAM62x系列工业级处理器设计开发,采用ARMCortex-A53架构,主频最高可达1.4GHz;并集成了丰富的接口,可广泛应用于的工...

如何在 Linux 发行版中安装微信和 QQ?

很多人因为工作沟通的原因需要用到微信和QQ,那么如何在Linux发行版中安装微信和QQ呢?以下是一些尝试的解决方法。QQ上一个版本的QQLinux版还是在2009年,而在现在,基于N...

MySQL:物理备份工具XBK(mysql 备份方案)

XBK的优缺点:XBK(PerconaXtraBackup)优点:1.免费2.热备:备份期间不阻塞innodb和XtraDB表,但会阻塞Myisam表3.物理备份:备份恢复快XBK缺点:1.不支持远...

AMD锐龙9 9950X CPU AIDA64跑分曝光:比7950X最高快45%

IT之家6月26日消息,Anandtech论坛网友igor_kavinski本周一发布帖子,分享了AMD旗舰锐龙99950X处理器的AIDA64基准测试跑分,与当前基于Z...

qemu linux内核(5.10.209)开发环境搭建

版本信息宿主机:ubuntu20.04.6LTS(FocalFossa)虚拟机:ubuntu20.04.6LTS(FocalFossa)安装宿主机的步骤省略,和一般的在vmware中安...

iPhone 7成刷机神器,成功运行乌班图、Linux、安卓

在智能机刚开始流行的时候,很多手机发烧友都喜欢刷机,当时民间大神们制作了特别多优化的ROM。后来随着手机硬件的逐步提升,以及厂商们对系统的大力优化,让大家对于刷机的兴趣也越来越少。不知道大家还记得这部...

12 款最佳免费开源 Linux 渲染器 | 火狐浏览器 130.0 版本更新

12款最佳免费开源Linux渲染器Linux的一大优势在于其拥有丰富的开源软件,可以满足艺术家、摄影师、动画师和设计师的需求。凭借价格低廉的硬件、免费的软件以及少量的才能和灵感,任何人都可以创...

Linux中xargs 命令详解与实用场景

xargs是Linux系统中常用的命令行工具之一,它能够从标准输入构造参数列表并传递给其他命令使用,是处理批量数据操作时的重要利器。一、xargs的基本语法xargs[OPTION]...[C...

Linux 磁盘扩容(非LVM)方式(linux扩容lvm磁盘容量)

今天接到一个客户的需求,CentOS的/分区容量太小了,OA系统所有的数据都在这下面,由于当时前同事给客户安装系统时采用了标准分区,而不是LVM逻辑卷,所以不支持在线扩容。df-hT查看磁盘使...