Linux内核编程(二十一)USB驱动开发-键盘驱动

news/2025/1/22 15:23:22/

一、驱动类型

        USB 驱动开发主要分为两种:主机侧的驱动程序和设备侧的驱动程序。一般我们编写的都是主机侧的USB驱动程序。

        主机侧驱动程序用于控制插入到主机中的 USB 设备,而设备侧驱动程序则负责控制 USB 设备如何与主机通信。由于设备侧驱动程序通常与设备功能紧密相关,因此常常被称为 USB gadget 驱动程序。USB gadget 驱动程序的作用是定义如何通过 USB 协议栈与主机端进行通信,确保设备能够正确响应主机的请求。

        在 USB 系统中,主机侧和设备侧有各自不同的控制器。主机侧使用的是主机控制器(Host Controller),它负责在主机和设备之间建立数据传输连接,管理设备的连接和断开。设备侧则使用 USB 设备控制器(UDC),它用于控制设备如何响应主机的请求和进行数据传输。主机控制器和设备控制器分别在各自的系统中充当着至关重要的角色,确保 USB 设备和主机之间的有效通信。

        这两种驱动程序通过操作系统中的 USB 子系统进行协同工作,提供了广泛的设备支持,从键盘、鼠标等简单外设到复杂的存储设备、音频设备等多种类型的 USB 设备。

二、USB传输介质-URB

        USB通信和IIC类似,都要先构建数据包,然后使用对应的API函数进行传输。URB就是USB传输的介质。URB(USB请求块)是Linux内核中用于管理USB数据传输的结构体。在Linux中,USB数据传输的核心就是通过URB来进行的,它相当于I2C中的数据包封装,承担着数据传输的“容器”角色。URB用于描述一次USB传输的请求,包括传输方向、数据长度、目标端点等信息。

1. 根据数据传输的方式和协议类型,URB有不同的类型。

(1)控制传输URB:用于管理设备控制请求,如设备的初始化、配置等。使用usb_fill_control_urb()来填充控制传输的URB。

(2)批量传输URB:用于较大数据量的传输,通常用于数据的读取和写入。使用usb_fill_bulk_urb()来填充批量传输URB。

(3)等时传输URB:用于对实时性要求较高的传输,如音频、视频流等。使用usb_fill_int_urb()来填充等时传输URB。

(4)中断传输URB:用于短数据的周期性传输,如键盘、鼠标等设备。使用usb_fill_int_urb()来填充中断传输URB。

2. 结构体如下所示。

struct urb {struct list_head urb_list;          // 用于管理URB队列的链表unsigned int pipe;                  // 传输通道,即端点void *context;                      // 用户定义的上下文,用于回调函数中传递信息unsigned char *transfer_buffer;     // 数据缓冲区指针,指向传输数据的内存dma_addr_t transfer_dma;            // 用于DMA传输的物理地址unsigned int transfer_flags;        // 标志,指示URB的某些属性(例如同步、异步等)unsigned int status;                // 传输状态,成功或失败unsigned int actual_length;         // 实际传输的字节数unsigned int number_of_packets;     // 分包传输时的数据包数unsigned int timeout;               // 传输超时时间(毫秒)unsigned int start_frame;           // 传输开始的帧号unsigned int interval;              // 传输间隔时间(仅对中断传输有效)unsigned char *setup_packet;        // 指向控制传输的请求包(如果是控制传输时)struct urb *next;                   // 下一个URB,供链表使用unsigned int transfer_buffer_length; // 缓冲区的长度(即传输数据的最大字节数)struct usb_device *dev;             // USB设备指针,指向传输目标设备void (*complete)(struct urb *urb);   // 传输完成后的回调函数struct mutex *lock;                 // 用于同步URB访问的互斥锁unsigned int pipe_flags;            // 端点标志,描述端点的类型和特性
};

3. 相关API。

(1)构建URB对象。

        返回一个指向分配的URB对象的指针。如果分配失败,返回 NULL

struct urb *usb_alloc_urb(int iso_packets, gfp_t mem_flags);
/*
iso_packets:表示如果URB是用于等时传输,这个参数指定URB包含的数据包数量。如果不是等时传输,此参数应为0。
mem_flags:内存分配标志,通常使用 GFP_KERNEL 来分配内存。
*/

(2)释放一个URB对象。 

void usb_free_urb(struct urb *urb);

(3)填充控制传输URB。

void usb_fill_control_urb(struct urb *urb, struct usb_device *dev,unsigned int pipe, unsigned char *setup_packet,void *transfer_buffer, int buffer_length,usb_complete_t complete_fn, void *context);
/*urb:要填充的URB对象。dev:目标USB设备。pipe:USB管道(端点),可以通过usb_sndctrlpipe()和usb_rcvctrlpipe()等函数获取。setup_packet:指向控制请求的setup包指针,包含请求的控制信息(如请求码、值、索引等)。transfer_buffer:指向传输数据缓冲区的指针。如果是控制传输的输入数据,数据会存储在这            里;输出数据也通过此缓冲区发送。buffer_length:传输缓冲区的长度。complete_fn:传输完成后的回调函数。context:用于回调函数的用户定义的上下文数据。可以在回调函数中使用。
*/

 (4)填充批量传输URB。

void usb_fill_bulk_urb(struct urb *urb, struct usb_device *dev,unsigned int pipe, void *transfer_buffer,int buffer_length, usb_complete_t complete_fn,void *context);
/*urb:要填充的URB对象。dev:目标USB设备。pipe:USB管道(端点),可以通过usb_sndbulkpipe()和usb_rcvbulkpipe()等函数获取。transfer_buffer:指向传输数据缓冲区的指针。buffer_length:传输缓冲区的长度。complete_fn:传输完成后的回调函数。context:用于回调函数的用户定义的上下文数据。
*/

 (5)填充中断传输URB。

void usb_fill_int_urb(struct urb *urb, struct usb_device *dev,unsigned int pipe, void *transfer_buffer,int buffer_length, usb_complete_t complete_fn,void *context, unsigned int interval);
/*urb:要填充的URB对象。dev:目标USB设备。pipe:USB管道(端点),可以通过usb_sndintpipe()和usb_rcvintpipe()等函数获取。transfer_buffer:指向传输数据缓冲区的指针。buffer_length:传输缓冲区的长度。complete_fn:传输完成后的回调函数。context:用于回调函数的用户定义的上下文数据。interval:传输间隔,单位为帧(适用于中断传输)。
*/

 (6)提交URB进行传输。

        如果提交成功,返回 0;如果失败,返回一个负数错误码。

int usb_submit_urb(struct urb *urb, gfp_t mem_flags);
/*urb:要提交的URB对象。mem_flags:内存分配标志,通常使用 GFP_KERNEL。
*/

(7)取消一个URB的传输。

void usb_kill_urb(struct urb *urb);

三、USB主机侧驱动开发(键盘驱动)

1. 操作流程。

(1)编写USB驱动框架。

(2)完善probe函数。

(3)在退出函数中释放掉内存以及相关注销。

2. 编写USB驱动框架。

①在函数入口函数中注册usb驱动,在出口函数中注销usb驱动。

②填充usb驱动信息,例如名称、probe函数等。

#include <linux/module.h>
#include <linux/usb.h>
#include <linux/init.h>
//设备连接时执行
static int my_usb_probe(struct usb_interface *intf, const struct usb_device_id *id) {return 0;
}//设备断开时执行
static void my_usb_disconnect(struct usb_interface *intf) {}static const struct usb_device_id my_usb_id_table[] = {{ USB_INTERFACE_INFO(USB_INTERFACE_CLASS_HID, USB_INTERFACE_SUBCLASS_BOOT, USB_INTERFACE_PROTOCOL_KEYBOARD) },{ }, // 空结构体,表示数组结束
};static struct usb_driver my_usb_driver = {.name = "my_usb",.probe = my_usb_probe,.disconnect = my_usb_disconnect,.id_table = my_usb_id_table,
};static int my_usb_init(void) {int ret = usb_register(&my_usb_driver);return 0;
}static void my_usb_exit(void) {usb_deregister(&my_usb_driver);
}module_init(my_usb_init);
module_exit(my_usb_exit);
MODULE_LICENSE("GPL");

3. 完善probe函数。

①由于键盘属于输入设备,则在probe函数中先为输入子系统开辟空间,并填充信息。

②设置键盘的按键事件以及键值。

③注册输入子系统到驱动。

④构建URB对象,开辟空间并填充中断传输URB函数中的参数。

⑤提交URB对象,用于数据传输。

⑥在填充URB的回调函数中,上报键盘按键事件。

⑦在退出函数中释放内存。

#include <linux/init.h>
#include <linux/module.h>
#include <linux/usb.h>
#include <linux/usb/input.h>
#include <linux/hid.h>
#include <linux/slab.h>struct input_dev *myusb_inputdev = NULL;  // 输入设备
struct urb *myusb_urb = NULL;  // USB请求块(URB)
unsigned char *myusb_buf = NULL;  // 数据缓冲区
unsigned char myusb_buf_old[8];  // 数据缓冲区int myusb_size = 0;  // 缓冲区大小
dma_addr_t myusb_dma;  // DMA地址// 键盘的键码表,包含标准键盘的按键映射
static const unsigned char usb_keyboardcode[256] = {0, 0, 0, 0, 30, 48, 46, 32, 18, 33, 34, 35, 23, 36, 37, 38,3, 50, 49, 24, 25, 16, 19, 31, 20, 22, 47, 17, 45, 21, 44, 2,4, 5, 6, 7, 8, 9, 10, 11, 28, 1, 14, 15, 57, 12, 13, 26,27, 43, 43, 39, 40, 41, 51, 52, 53, 58, 59, 60, 61, 62, 63, 64,65, 66, 67, 68, 87, 88, 99, 70, 119, 110, 102, 104, 111, 107, 109, 106,105, 108, 103, 69, 98, 55, 74, 78, 96, 79, 80, 81, 75, 76, 77, 71,72, 73, 82, 83, 86, 127, 116, 117, 183, 184, 185, 186, 187, 188, 189, 190,191, 192, 193, 194, 134, 138, 130, 132, 128, 129, 131, 137, 133, 135, 136, 113,115, 114, 0, 0, 0, 121, 0, 89, 93, 124, 92, 94, 95, 0, 0, 0,122, 123, 90, 91, 85, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,0, 0, 29, 42, 56, 125, 97, 54, 100, 126, 164, 166, 165, 163, 161, 115,114, 113, 150, 158, 159, 128, 136, 177, 178, 176, 142, 152, 173, 140
};//回调函数执行的条件是 URB传输完成,无论是成功还是失败。
static void myusb_func(struct urb *urb)  //填充中断URB的回调函数
{//上报特殊按键for (i = 0; i < 8; i++) {input_report_key(myusb_inputdev, usb_keyboardcode[i + 224], (myusb_buf[0] >> i) & 1);}//上报普通按键for (i = 2; i < 8; i++) {if (myusb_buf_old[i] > 3 && memscan(myusb_buf + 2, myusb_buf_old[i], 6) == myusb_buf + 8) {if (usb_keyboardcode[myusb_buf_old[i]]) {input_report_key(myusb_inputdev, usb_keyboardcode[myusb_buf_old[i]], 0);}}if (myusb_buf[i] > 3 && memscan(myusb_buf_old+ 2, myusb_buf[i], 6) == myusb_buf_old+ 8) {if (usb_keyboardcode[myusb_buf[i]]) {input_report_key(myusb_inputdev, usb_keyboardcode[myusb_buf[i]], 1);}}}input_sync(myusb_inputdev); //同步事件,即上报完毕。memcpy(myusb_buf_old, myusb_buf, 8); //myusb_buf复制给myusb_buf_oldusb_submit_urb(myusb_urb , GFP_ATOMIC); //提交URB
}// 设备连接时执行
static int my_usb_probe(struct usb_interface *intf, const struct usb_device_id *id) {int i;int ret;struct usb_device *myusb_dev = interface_to_usbdev(intf); // 获取USB设备指针struct usb_endpoint_descriptor *endpoint;// 1. 为输入设备分配内存myusb_inputdev = input_allocate_device();if (!myusb_inputdev) {pr_err("Failed to allocate input device\n");return -ENOMEM;}myusb_inputdev->name = "myusb_input";   // 设置设备名称// 2. 设置事件类型:按键事件和重复事件set_bit(EV_KEY, myusb_inputdev->evbit);set_bit(EV_REP, myusb_inputdev->evbit);for (i = 0; i < 255; i++) {  // 设置按键位图set_bit(usb_keyboardcode[i], myusb_inputdev->keybit);}clear_bit(0, myusb_inputdev->keybit);  // 清除无效的按键位// 3. 注册输入设备ret = input_register_device(myusb_inputdev);if (ret) {input_free_device(myusb_inputdev);return ret;}
// 4. URB分配内存myusb_urb = usb_alloc_urb(0, GFP_KERNEL); // 分配一个URB,ISO数据包数为0,内存分配标志为GFP_KERNEL// 获取端点描述符并获取端点最大数据包大小endpoint = &intf->cur_altsetting->endpoint[0].desc;myusb_size = endpoint->wMaxPacketSize;// 分配一致性内存缓冲区用于数据传输myusb_buf = usb_alloc_coherent(myusb_dev, myusb_size, GFP_ATOMIC, &myusb_dma);// 获取接收中断管道unsigned int pipe = usb_rcvintpipe(myusb_dev, endpoint->bEndpointAddress);// 填充URBusb_fill_int_urb(myusb_urb, myusb_dev, pipe, myusb_buf, myusb_size, myusb_func, 0, endpoint->bInterval);// 5. 提交URB进行数据传输ret = usb_submit_urb(myusb_urb, GFP_KERNEL);mykbd_urb->transfer_dma = mykbd_dma;  // 设置URB的DMA地址mykbd_urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP; // 设置URB的标志:不使用DMA映射return 0;
}// 设备断开时执行
void myusb_disconnect(struct usb_interface *intf) {usb_kill_urb(myusb_urb);  // 取消URB传输usb_free_urb(myusb_urb);  // 释放URBusb_free_coherent(myusb_dev, myusb_size, myusb_buf, myusb_dma);  // 释放一致性内存input_unregister_device(myusb_inputdev);    // 注销输入设备input_free_device(myusb_inputdev);    // 释放输入设备
}

 问题1:上述代码中按键数组中的数据代表什么呢?

答:如下图所示不同的键值对应不同的按键,数组中的0元素就是保留位,也就是未分配的值。

 问题2:URB回调函数中的代码是什么意思?

答:是用于上报键盘上各个按键事件。USB每次传输都是8个字节的数据!对于键盘来说,这8个字节的数据分别对应下面的按键类型。 如下面两张图所示:

这张图是USB键盘按下后读取的8个字节的值。

下面这张图是读取的8个字节的值的解析。

4.验证

(1) 取消开发板上之前默认的USB驱动(键盘)。将内核源码重新配置编译到开发板。

(2)将自己写的驱动的ko文件上传至开发板,并将USB键盘连接开发板。

(3)使用命令:exec 0</dev/ttyl,按下按键如果没有反应,则证明此时没有键盘驱动了。由于按键现在用不了,所以重新启动开发板即可。

(4)安装ko驱动模块,然后重新使用命令:exec 0</dev/ttyl,这时按下按键就会有响应,如下所示:

五、USB设备侧驱动开发

31.Gadget驱动:把开发板模拟成USB网卡_哔哩哔哩_bilibili


http://www.ppmy.cn/news/1565246.html

相关文章

窥探QCC518x/308x系列与手机之间的蓝牙HCI记录与分析 - 手机篇

今天要介绍给大家的是, 当我们在开发高通耳机时如果遇到与手机之间相容性问题, 通常会用Frontline或Ellisys的Bluetooth Analyzer来截取资料分析, 如果手边没有这样的仪器, 要如何窥探Bluetooth的HCI log.这次介绍的是手机篇. 这次跟QCC518x/QCC308x测试的手机是Samsung S23 U…

【科研建模】Pycaret自动机器学习框架使用流程及多分类项目实战案例详解

Pycaret自动机器学习框架使用流程及项目实战案例详解 1 Pycaret介绍2 安装及版本需求3 Pycaret自动机器学习框架使用流程3.1 Setup3.2 Compare Models3.3 Analyze Model3.4 Prediction3.5 Save Model4 多分类项目实战案例详解4.1 ✅ Setup4.2 ✅ Compare Models4.3 ✅ Experime…

微服务学习-Seata 解决分布式事务

1. 为什么要使用分布式事务&#xff1f; 1.1. 问题重现 使用微服务架构&#xff0c;当账户余额为 0 时&#xff0c;还可以继续下单&#xff0c;而且扣减库存&#xff1b;或者当库存不足时&#xff0c;也可以下单继续扣减余额等问题&#xff0c;造成数据不一致。 1.2. 新的需…

阿里云服务器在Ubuntu上安装redis并使用

1、redis安装 sudo apt install lsb-release curl gpgcurl -fsSL https://packages.redis.io/gpg | sudo gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpgecho "deb [signed-by/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.…

自动驾驶---方案从有图迈进无图

1 背景 近两年&#xff0c;自动驾驶量产领域&#xff0c;有一句话出现的频率很高&#xff1a;“无图也能开”&#xff0c;到底什么是有图&#xff0c;什么是无图呢&#xff1f;简单来说就是有高精地图&#xff08;High Definition Map&#xff09;和没有高精地图&#xff08;但…

使用tritonserver完成clip-vit-large-patch14图像特征提取模型的工程化。

1、关于clip-vit-large-patch14模型 关于openapi开源的clip-vit-large-patch14模型的特征提取&#xff0c;可以参考之前的文章&#xff1a;Elasticsearch向量检索需要的数据集以及768维向量生成这篇文章详细介绍了模型的下载地址、使用方式、测试脚本&#xff0c;可以让你一步…

小白爬虫——selenium入门超详细教程

目录 一、selenium简介 二、环境安装 2.1、安装Selenium 2.2、浏览器驱动安装 三、基本操作 3.1、对页面进行操作 3.1.1、初始化webdriver 3.1.2、打开网页 3.1.3、页面操作 3.1.4、页面数据提取 3.1.5、关闭页面 ?3.1.6、综合小案例 3.2、对页面元素进行操作 3…

【深度学习】Huber Loss详解

文章目录 1. Huber Loss 原理详解2. Pytorch 代码详解3.与 MSELoss、MAELoss 区别及各自优缺点3.1 MSELoss 均方误差损失3.2 MAELoss 平均绝对误差损失3.3 Huber Loss 4. 总结4.1 优化平滑4.2 梯度较好4.3 为什么说 MSE 是平滑的 1. Huber Loss 原理详解 Huber Loss 是一种结合…