深入理解USB设备驱动开发:使用USB_SKELETON框架
简介:USB Skeleton是Linux环境下USB设备驱动开发的起点,提供了一套基础框架以帮助开发者快速上手。框架包含基本的USB驱动结构和核心组件,如设备描述符、配置描述符、接口描述符和端点描述符,以及关键操作流程如枚举、配置设置、接口和端点操作、中断处理和设备断开处理。通过学习和使用USB Skeleton,开发者可以避免重复编写通用代码,专注于实现与特定USB设备交互的逻辑。
1. USB Skeleton框架介绍
在当今的计算机世界中,USB(通用串行总线)技术已经成为连接外围设备的标准方式。开发者在为各种USB设备编写驱动程序时,使用统一的框架可以大大简化开发过程。USB Skeleton框架,顾名思义,就是USB驱动开发的骨架代码,它提供了一个基础的模板,使得开发者可以在此基础上构建自己特定的驱动程序。
本章将介绍USB Skeleton框架的基础知识,以及它如何帮助开发者快速开始USB驱动开发。我们会从框架的目的、基本组成以及在Linux内核中的位置开始,逐步深入了解USB驱动开发的入门知识。
#include <linux/module.h> /* Needed by all modules */
#include <linux/kernel.h> /* Needed for KERN_INFO */
#include <linux/usb.h> /* Needed to use the USB core API */
/* USB设备结构体,包含产品ID和供应商ID */
static struct usb_device_id usb_table[] = {
{ USB_DEVICE(0x1234, 0x5678) }, /* 替换为你的设备ID */
{} /* Terminating entry */
};
MODULE_DEVICE_TABLE(usb, usb_table);
/* 插入(安装)函数,USB模块被加载时调用 */
static int usb_probe(struct usb_interface *interface, const struct usb_device_id *id)
{
printk(KERN_INFO "USB Skeleton Example: usb_probe\n");
/* 设备被识别时的处理逻辑 */
return 0;
}
/* 卸载函数,USB模块被移除时调用 */
static void usb_disconnect(struct usb_interface *interface)
{
printk(KERN_INFO "USB Skeleton Example: usb_disconnect\n");
/* 设备断开时的清理工作 */
}
static struct usb_driver usb_driver = {
.name = "example_usb_driver",
.id_table = usb_table,
.probe = usb_probe,
.disconnect = usb_disconnect,
};
/* 初始化函数,在模块加载时调用 */
static int __init usb_driver_init(void)
{
return usb_register(&usb_driver);
}
/* 清理函数,在模块卸载时调用 */
static void __exit usb_driver_exit(void)
{
usb_deregister(&usb_driver);
}
module_init(usb_driver_init);
module_exit(usb_driver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("USB Skeleton Driver Example");
以上代码展示了一个非常基础的USB驱动程序框架,开发者可以基于此骨架代码进行扩展,添加具体设备的初始化、数据处理和卸载逻辑。通过理解这个框架,开发者可以更快地开始USB设备的开发工作。
2. USB驱动开发层次结构
2.1 USB驱动的架构和组件
2.1.1 Linux内核中USB驱动的总体架构
Linux内核中的USB驱动架构是一个分层的模型,每一层都承担着特定的职责,以便于抽象硬件操作和提供通用的编程接口给上层应用。在核心层,USB核心代码负责处理USB协议的通用操作,例如设备的枚举、传输调度以及端点管理。它提供了一系列的API函数供更高级的驱动使用。
在USB驱动的层次结构中,除了核心层,还有主机控制器驱动(HCD)和USB设备驱动。HCD负责与计算机主机上的USB控制器通信,实现数据的传输。USB设备驱动则负责与特定类型的USB设备通信,通过使用核心层的API与硬件设备进行交互。
这种分层设计的好处是,驱动开发者可以专注于他们最关心的部分,而不必重写所有底层通信代码。例如,开发一个特定的USB打印机驱动时,开发者可以利用核心层提供的USB打印机类驱动框架,专注于实现与打印机相关的特定功能,而不需要处理底层的数据传输细节。
2.1.2 各层驱动组件的职责和功能
-
USB核心层(Core Layer) :这个层次位于整个USB驱动架构的中间,负责维护USB设备和驱动的通用数据结构,并提供设备管理和数据传输的接口给上层驱动和下层HCD。USB核心层处理诸如设备插入和移除、USB总线枚举、标准设备请求、配置管理以及接口和端点管理等任务。
-
主机控制器驱动(Host Controller Driver, HCD) :HCD是USB驱动架构中最接近硬件的一层,直接与计算机系统上的USB控制器硬件交互。HCD负责实现USB协议中的具体细节,如硬件中断处理、端点数据传输等,并将这些操作封装为通用接口供USB核心层和上层驱动调用。
-
USB设备驱动(USB Device Driver) :这是最顶层的驱动,直接与特定类型的USB设备交互。USB设备驱动实现特定设备功能,如读写数据、处理设备特定的事件等。设备驱动通常通过USB核心层提供的接口与USB设备通信,而不需要直接与HCD交互。
以上各层的驱动组件在USB通信过程中协同工作,提供了一个可靠、可扩展且易于维护的驱动框架,使得在Linux系统中开发和维护USB驱动变得更为简便和高效。
2.2 USB驱动与硬件通信的实现
2.2.1 USB设备的识别和枚举过程
USB设备识别和枚举是USB驱动开发中至关重要的步骤,是设备与主机系统开始通信的起点。当一个USB设备被连接到计算机上时,系统必须识别出该设备的类型、制造商以及它支持的功能。枚举过程可以分为以下几个阶段:
- 设备插入 :当USB设备连接到主机时,USB总线通过检测到电压变化来识别设备插入事件。
- 设备复位 :主机发送复位信号给新接入的设备,准备设备开始枚举过程。
- 设备地址分配 :主机分配一个唯一的地址给新设备,并通过该地址与设备进行后续通信。
- 设备描述符请求 :主机请求设备的各种描述符信息,包括设备类型、制造商、产品序列号以及支持的端点和传输类型等。
- 配置选择 :主机根据获取到的设备信息选择合适的配置,并请求配置描述符。
在Linux内核中,这一过程是通过USB核心层的API自动完成的。驱动开发者通常不需要深入到枚举过程的每一个细节,但是理解枚举过程可以帮助他们更好地编写设备驱动程序。
2.2.2 USB数据传输机制和传输类型
USB支持四种基本的传输类型:控制传输、批量传输、中断传输和同步传输(Isochronous)。每种传输类型有其特定的使用场景和特性:
-
控制传输 :用于设备的初始化和配置,以及设备请求的处理。控制传输通常用于发送命令和获取设备信息。
-
批量传输 :用于传输大量数据,且对数据的准确性有较高的要求。常见于打印机、大容量存储设备等。
-
中断传输 :用于传输少量且时间敏感的数据。例如,鼠标和键盘的输入数据是通过中断传输来处理的。
-
同步传输 :用于要求固定的带宽和定期传输数据的场合,如音频和视频设备。
Linux USB核心层为各种传输类型提供了通用的API。驱动开发者可以使用这些API来提交特定类型的传输请求,而无需关心底层细节。USB核心层会根据请求类型和设备能力选择最合适的传输方法。
下面是一段提交批量传输请求的代码示例:
urb = usb_alloc_urb(0, GFP_KERNEL);
if (!urb) {
/* 错误处理 */
}
usb_fill_bulk_urb(urb, dev, /* 端点信息 */, /* 缓冲区指针 */, /* 缓冲区大小 */,
/* 回调函数 */, /* 上下文信息 */);
ret = usb_submit_urb(urb, GFP_KERNEL);
if (ret) {
/* 提交失败处理 */
}
usb_free_urb(urb);
代码逻辑的逐行解读分析:
-
usb_alloc_urb函数用于分配一个未初始化的USB请求块(URB),它是进行USB传输的基础结构。 -
usb_fill_bulk_urb函数用于初始化URB结构,填充包括端点信息、缓冲区指针、数据大小、完成时的回调函数和上下文信息等参数。 -
usb_submit_urb函数将初始化好的URB提交给USB核心,进行异步传输。如果返回值不是0,则表示提交失败,需要进行相应的错误处理。 - 最后,使用
usb_free_urb函数释放之前分配的URB资源。
开发者必须确保他们正确地分配和释放URB资源,以避免内存泄漏和其他潜在的问题。
2.3 USB驱动的加载和卸载
2.3.1 驱动加载过程中的关键步骤
USB驱动的加载过程涉及到几个关键的步骤,包括初始化USB设备、分配和配置必要的资源以及注册USB设备驱动到系统中。在Linux内核中,这通常是通过使用 module_init() 宏指定的 init 函数来完成的。
加载过程可以分为以下步骤:
- 设备初始化 :分配和初始化USB设备所需的内存和数据结构。
- 资源分配 :请求并分配USB核心层所需的资源,如URB、设备ID、端点等。
- 驱动注册 :将USB设备驱动注册到USB核心层,这样核心层就可以将驱动和相应的USB设备关联起来。
- 设备匹配 :当有USB设备接入时,USB核心层将设备信息与注册的驱动进行匹配。如果匹配成功,则调用驱动提供的接口来处理该设备。
驱动加载过程中还需要考虑错误处理和资源清理,确保在发生错误或卸载驱动时,能够释放之前分配的资源,并且不会对系统造成影响。
2.3.2 驱动卸载时的资源释放和状态恢复
在USB驱动卸载时,需要进行适当的资源释放和状态恢复,以确保驱动卸载不会影响系统的稳定性和其他运行中的设备。卸载过程主要需要完成以下任务:
- 注销设备 :将与该驱动关联的USB设备从USB核心层注销。
- 释放资源 :释放之前加载时分配的所有资源,包括URB、设备ID、端点、内核内存等。
- 清理工作 :进行必要的清理工作,确保没有未完成的操作和潜在的内存泄漏。
在Linux内核中,这通常通过使用 module_exit() 宏指定的 exit 函数来完成。开发者必须确保卸载过程中不会导致系统崩溃或其他设备出现问题。例如,当USB设备被移除时,应该适当地处理所有的传输请求,避免系统挂起。
下面是一个简单的代码示例,展示了如何在驱动卸载函数中使用 usb_put_dev() 来释放USB设备引用:
static void usb_driver_exit(struct usb_driver *driver)
{
usb_deregister(driver);
/* 其他资源清理代码 */
}
static void usb_device_release(struct device *dev)
{
struct usb_device *usb_dev = to_usb_device(dev);
usb_put_dev(usb_dev);
}
static int __init usb_driver_init(void)
{
/* 注册USB驱动 */
return usb_register(&my_usb_driver);
}
static void __exit usb_driver_exit(void)
{
usb_driver_exit(&my_usb_driver);
/* 其他资源清理代码 */
}
这里 usb_deregister() 函数用于注销USB驱动, usb_put_dev() 函数用于减少USB设备的引用计数,当引用计数降到0时,将释放与该USB设备相关的所有资源。
整个USB驱动的加载和卸载过程必须谨慎处理,确保系统资源在任何时候都能被正确管理,从而保证系统的稳定性和设备的正常运行。
3. USB核心组件细节
USB驱动程序的开发离不开对USB核心组件的深刻理解。在这一章节中,我们将深入探讨USB的核心数据结构、设备类驱动框架、以及错误处理机制。这些知识将为开发者提供更深入的USB内部工作原理的理解,从而编写出更加高效和稳定的USB驱动。
3.1 USB核心数据结构
3.1.1 USB设备、接口和端点的内核表示
USB设备在内核中的表示主要依赖于一系列的数据结构,如 usb_device 、 usb_interface 、 usb_endpoint 等。这些结构体在内核的USB子系统中发挥着至关重要的作用,它们记录了设备的配置、接口和端点等信息。
-
usb_device代表了整个USB设备,它包含了设备的各种属性,如厂商ID、产品ID、设备版本号等信息。它也维护了指向设备所有接口的指针列表。 -
usb_interface代表了一个单独的USB接口,它用于实现特定的功能。一个USB设备可以有多个接口,每个接口可能包含多个端点。 -
usb_endpoint代表了一个端点,是USB通信的基本单位。每个端点有一个特定的传输类型和方向,并在配置时被分配一个唯一的地址。
在内核中,这些结构体通过指针相互链接,形成了一个层次化的数据结构。开发者在编写驱动时,需要根据这些数据结构来访问和管理USB设备。
3.1.2 USB请求块(URB)的作用和生命周期
USB请求块(USB Request Block,简称URB)是USB通信中重要的抽象,它用于在USB核心和USB驱动之间传递数据和控制信息。一个URB通常代表了一个USB事务,可以是数据传输、控制传输或者同步传输。
URB的生命周期从分配开始,开发者使用 usb_alloc_urb() 函数来创建一个URB实例。之后,在设置URB的各个字段后,通过 usb_submit_urb() 将其提交给USB核心。USB核心负责将URB请求加入队列,并在适当的时候调度给相应的USB设备处理。完成之后,USB核心会调用URB的完成回调函数,通知驱动URB已完成。最后,驱动负责释放URB资源,调用 usb_free_urb() 来完成URB的生命周期。
整个过程可以表示为下面的流程图:
graph LR
A[分配URB] --> B[设置URB]
B --> C[提交URB]
C --> D[执行数据传输]
D --> E[完成回调]
E --> F[释放URB资源]
3.2 USB设备类驱动的框架
3.2.1 设备类驱动的结构和接口定义
USB设备类驱动为同一类别的USB设备提供了一套通用的驱动框架。它定义了一组标准的接口,这些接口用于实现设备的初始化、配置、数据传输和清理等功能。通过定义和实现这些接口,USB设备类驱动可以为不同的设备提供统一的管理方式。
一个典型的USB设备类驱动框架包括以下几个部分:
-
usb_driver结构体:它包含了驱动的名称、ID表、以及驱动注册和注销的函数。 -
probe()函数:当USB核心发现一个匹配的设备时,会调用这个函数进行设备的初始化。 -
disconnect()函数:当USB设备被移除或者驱动被卸载时,会调用这个函数来执行清理工作。 - 其他如
suspend()和resume()等函数,用于处理USB设备的电源状态改变。
3.2.2 标准和特定类驱动的实现细节
USB设备类驱动的实现可以分为标准类驱动和特定类驱动两种。标准类驱动是由USB标准化组织定义的,用于处理如USB存储设备、USB音频设备等通用设备类型。特定类驱动则是为特定厂商或特定用途的设备定制的驱动。
标准类驱动提供了通用的接口和数据结构,这使得编写这类驱动更为简单,同时也可以让设备在不同的系统和平台上具有更好的兼容性。特定类驱动则需要详细定义设备的特定行为和属性,这通常需要对硬件规范有深入的了解。
3.3 USB驱动中的错误处理机制
3.3.1 错误检测和报告
USB驱动中的错误处理机制是确保USB设备稳定运行的关键。USB核心提供了错误检测机制,例如,当USB传输失败时,会返回一个非零值给驱动。错误报告机制通常包括错误码和错误文本描述,方便开发者诊断和修复问题。
错误处理机制的实现包括在驱动的适当位置检查错误码,并根据错误类型采取不同的处理策略。例如,对于暂时性的错误,驱动可能会选择重试操作;对于永久性的错误,则可能需要通知用户或执行一些清理工作。
3.3.2 错误恢复和预防策略
错误恢复和预防策略是USB驱动设计中不可忽视的一部分。错误恢复机制包括自动重试、回退到安全状态、以及通知用户等。预防策略则更多地侧重于避免错误的发生,例如,合理设置超时时间、进行适当的数据校验、以及保证USB数据传输的可靠性。
在实际开发中,错误处理的代码片段可能如下所示:
urb = usb_alloc_urb(0, GFP_KERNEL);
if (!urb) {
// 错误处理:分配URB失败
printk(KERN_ERR "Failed to allocate URB\n");
return -ENOMEM;
}
usb_fillBulkurb(urb, dev, usb_pipeendpoint(dev->pipe), buffer,
size, usb完成后回调函数, NULL);
urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
ret = usb_submit_urb(urb, GFP_KERNEL);
if (ret) {
// 错误处理:提交URB失败
printk(KERN_ERR "Failed to submit URB: %d\n", ret);
usb_free_urb(urb);
return ret;
}
// ... 在urb完成后回调函数中处理完成情况 ...
以上代码展示了在URB提交前后进行错误处理的逻辑,其中包含了对 usb_alloc_urb 和 usb_submit_urb 返回值的检查。这样的错误处理策略可以确保驱动程序在各种情况下都能正确地响应和处理错误。
以上所述内容涵盖了USB核心组件的详细解释,包括USB设备、接口、端点的内核表示,URB的生命周期,设备类驱动的结构和接口,以及错误处理机制的详细讨论。这些知识为编写和优化USB驱动提供了坚实的基础。在第四章中,我们将深入了解USB设备的枚举、配置、接口端点操作、中断处理、以及设备断开等关键操作流程,这些都是USB驱动开发中的重要环节。
4. 关键操作流程:枚举、配置、接口端点操作、中断处理、设备断开
4.1 USB设备的枚举流程详解
4.1.1 枚举过程中USB核心的交互机制
当USB设备插入主机后,USB核心负责与设备进行交互,开始枚举过程。枚举是USB设备与主机通信的初始化步骤,包括设备的识别、配置、接口及端点等信息的获取。
USB核心通过一系列的控制传输,与设备建立通信。首先是获取设备描述符,设备描述符包含了设备的基本信息如设备类、子类、协议以及设备支持的最大速度等。接着,USB核心会获取设备配置描述符,配置描述符包含了多个接口和端点的定义。每个接口可以支持不同的数据传输类型,而端点则定义了数据传输的方向和大小。
核心层还负责实现USB设备的状态管理和地址分配。设备初始状态是默认状态,核心层通过默认控制管道发送标准设备请求来获取设备信息,并为设备分配一个唯一的地址。之后,设备进入地址状态,核心层使用该地址来发送后续的请求。
枚举过程中,USB核心与USB驱动程序之间的交互主要是通过一系列的回调函数实现的。这些回调函数定义在USB驱动程序中,用于响应核心层的请求。例如,当核心层成功获取设备的配置描述符后,会调用驱动程序中注册的 probe 函数,该函数负责进一步配置设备,并绑定相应的接口驱动。
4.1.2 驱动如何响应枚举事件
USB驱动程序响应枚举事件主要依赖于驱动注册时提供的回调函数,包括:
-
probe函数:在枚举过程中核心层确认设备和驱动匹配后调用,用于初始化设备。这个函数中通常会检查硬件ID、初始化数据结构、注册设备操作函数等。 -
disconnect函数:当设备从系统中拔出或驱动被卸载时调用,用于进行清理工作,比如释放内存、注销设备操作函数等。 - 其他可选回调函数,如
idVendor和idProduct等,可以根据具体设备的需求进行定义。
当USB核心完成设备的基本信息采集后,会查询其驱动程序列表,找出与设备匹配的驱动。匹配成功后,调用该驱动的 probe 函数。驱动的 probe 函数需要填写必要的设备信息,并实现与设备通信的逻辑。例如,在一个USB存储设备的驱动中, probe 函数可能会分配一个请求队列,并挂载文件系统。
在实际的USB驱动开发中,开发者需要根据USB设备的具体协议和数据结构实现这些回调函数。例如,对于一个自定义的USB设备,驱动程序可能需要定义一套与之通信的请求协议,并在 probe 函数中实现这些请求的处理逻辑。
static int my_usb_device_probe(struct usb_interface *interface, const struct usb_device_id *id)
{
struct usb_device *udev = interface_to_usbdev(interface);
struct my_usb_device *myudev;
myudev = kzalloc(sizeof(struct my_usb_device), GFP_KERNEL);
if (!myudev)
return -ENOMEM;
myudev->udev = udev;
usb_set_intfdata(interface, myudev);
// 设备初始化代码...
return 0;
}
static void my_usb_device_disconnect(struct usb_interface *interface)
{
struct my_usb_device *myudev = usb_get_intfdata(interface);
// 设备清理代码...
usb_set_intfdata(interface, NULL);
kfree(myudev);
}
static struct usb_driver my_usb_driver = {
.name = "my_usb_driver",
.id_table = my_usb_table,
.probe = my_usb_device_probe,
.disconnect = my_usb_device_disconnect,
// 其他回调函数...
};
module_usb_driver(my_usb_driver);
在上述代码示例中, my_usb_device_probe 和 my_usb_device_disconnect 函数分别作为 probe 和 disconnect 回调函数被注册到了USB驱动中。当设备插入时,USB核心会调用 my_usb_device_probe 函数进行设备的初始化;当设备拔出时, my_usb_device_disconnect 函数会被调用以进行设备的清理工作。
4.2 USB设备的配置与接口操作
4.2.1 配置选择和接口激活过程
USB设备可能有多个配置,每个配置定义了一组接口,每个接口可以实现不同的功能。系统在枚举设备时,会默认选择第一个配置,并通过 set_configuration 请求激活它。如果需要激活其他配置,则需要驱动程序显式地请求。
在USB驱动中,通常会在 probe 函数中激活所需的配置,并且根据需要激活特定的接口。这一过程涉及发送 set_interface 请求,以选择和激活设备的特定接口。驱动程序通常会枚举所有可用的配置和接口,并根据需要进行选择。每个接口都有一个唯一的ID,设备的驱动程序根据其功能需求选择合适的接口。
USB驱动程序通常会使用 usb_set_interface 函数来激活特定的接口。该函数需要传入USB设备指针、接口号和备用接口设置号。
4.2.2 接口和端点的编程模型
USB设备的接口模型主要由接口(interface)、端点(endpoint)和配置(configuration)组成。接口是通信的抽象,而端点是实际进行数据传输的通道。在USB协议中,端点可以是输入端点或输出端点,并可以支持批量、中断、等时或控制传输类型。
USB驱动程序在实现时,需要考虑不同端点的传输类型和方向,为它们编写相应的数据处理代码。例如,一个中断端点可能会用于定期传输状态更新,而批量端点可能用于传输大量数据。
端点被定义在接口描述符中,而接口被定义在配置描述符中。每个端点都有唯一的地址(包括端口号和端点号),设备驱动程序通过这些地址来访问特定的端点。
在USB驱动程序中,端点通常使用USB请求块(URB)来访问。URB是一个核心结构,表示单个USB传输请求。驱动程序会填写URB结构,并将其提交给USB核心,核心负责调度传输。URB提交后,驱动程序可以在完成回调函数中处理完成后的逻辑。
struct urb *urb;
urb = usb_alloc_urb(0, GFP_KERNEL); // 分配一个URB
if (!urb) {
// 分配失败处理
}
usb_fill_bulk_urb(urb, myudev->udev, usb_rcvbulkpipe(myudev->udev, EPNUM),
my缓冲区, 数据长度, my_callback, myudev); // 填充URB
urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP; // 设置传输标志
int ret = usb_submit_urb(urb, GFP_KERNEL); // 提交URB
if (ret)
usb_free_urb(urb); // 提交失败,释放URB
// 回调函数
void my_callback(struct urb *urb)
{
if (urb->status == 0) {
// 传输成功处理
} else {
// 传输失败处理
}
// 其他清理工作...
}
在代码示例中,我们创建了一个URB,并使用 usb_fill_bulk_urb 来填充URB结构。然后提交URB给USB核心以进行数据传输。传输完成后,由 my_callback 函数处理完成后的逻辑。
4.3 USB中断和批量数据处理
4.3.1 中断传输的特点和实现方法
USB中断传输是一种低速数据传输方式,通常用于定时传递少量数据,例如键盘和鼠标设备。中断传输保证了较低的延迟,但带宽有限,通常不超过64字节。
USB驱动程序中,中断传输的实现依赖于提交URB给USB核心。当设备连接时,驱动程序会使用特定的端点号和端点类型,以及回调函数注册中断URB。一旦注册,核心会定期调度URB,以收集来自设备的中断数据。
中断处理函数通常在定时器中断的上下文中执行,因此驱动程序需要确保该函数尽可能高效。在中断处理函数中,开发者可以读取数据,然后根据需要进行处理,例如更新输入设备状态,或者将数据传递给更高层的协议栈。
4.3.2 批量传输的性能优化和流控制
批量传输是USB中用于传输大量数据的传输类型,例如读写USB存储设备。这种传输类型提供较高的数据吞吐率,但不保证数据的实时性。为了优化批量传输性能,USB驱动程序需要实现有效的流控制,以避免缓冲区溢出和数据丢失。
在Linux USB核心中,驱动程序可以通过URB调度来管理批量传输。开发者可以设计自己的缓冲区管理策略,比如环形缓冲区,来缓存从设备接收的数据。同时,也可以使用传输完成回调来处理发送和接收的确认,确保数据正确处理。
在批量传输中,还可以使用USB协议中的同步框架,以减少等待时间。例如,Linux内核支持异步无阻塞读写接口,这些接口可以减少线程阻塞,提高系统的吞吐量。
urb = usb_alloc_urb(0, GFP_KERNEL); // 分配URB
usb_fill_bulk_urb(urb, myudev->udev, usb_rcvbulkpipe(myudev->udev, EPNUM),
my缓冲区, 数据长度, my_callback, myudev); // 填充URB
ret = usb_submit_urb(urb, GFP_KERNEL); // 提交URB
if (ret) {
// 提交失败处理
}
// 在中断或完成回调中处理数据
void my_callback(struct urb *urb)
{
if (urb->status == 0) {
// 正常数据传输完成处理
} else if (urb->status == -ENOENT || urb->status == -ECONNRESET || urb->status == -ESHUTDOWN) {
// 传输取消或设备断开连接处理
} else {
// 其他错误处理
}
// 其他数据处理逻辑...
}
在上面的代码中,我们展示了如何使用URB来执行批量传输。使用 usb_rcvbulkpipe 来获取批量端点的管道信息,然后使用 usb_submit_urb 提交URB。在回调函数 my_callback 中,根据URB的状态来处理数据。这种模式适用于批量传输,特别是在需要高吞吐量且对数据延迟要求不是非常高的场景。
4.4 USB设备的断开和异常处理
4.4.1 设备移除时的同步和清理工作
USB设备的断开可以是热插拔,也可能是因为系统关机、崩溃等异常情况。不管哪种情况,驱动程序都需要确保在设备移除时执行必要的同步和清理工作。这包括释放设备分配的资源,比如内存、IO端口等,并确保所有未完成的URB被正确取消。
当设备从系统中移除时,USB核心会调用驱动中的 disconnect 函数。这个函数是实现清理逻辑的主要地方。驱动程序必须确保在这一步骤中完成所有必要的操作,以保证设备可以安全断开。例如,一个网络摄像头驱动程序可能需要释放分配的视频缓冲区,注销相关的网络接口等。
在 disconnect 函数中,应该停止所有在设备上的操作,并且释放所有与设备相关的资源。这可能包括从字符设备或块设备中注销设备文件,以及在某些情况下,向USB设备发送断开通知。
4.4.2 异常情况下的恢复策略和调试方法
异常情况可能包括设备数据包丢失、错误状态和协议违规等。为了处理这些异常情况,驱动程序需要实现错误恢复策略,并提供足够的调试信息以便快速定位问题。
当发生错误时,驱动程序需要能够识别错误的类型,并采取相应的恢复动作。例如,如果发生URB提交失败,驱动程序应取消URB,并在适当的时候重试。如果发生传输错误,驱动程序可能需要重新建立与设备的通信。
在调试时,Linux提供了一系列的工具来帮助开发者追踪和诊断问题。例如:
-
dmesg命令用于查看内核消息缓冲区中的信息,这里记录了USB核心和驱动程序的调试信息。 -
usbmon工具可以用来追踪USB总线上的所有活动,包括数据传输、设备事件和驱动程序的调试输出。
此外,开发者还可以通过修改驱动程序来增加额外的日志输出,或者在特定的代码位置插入断点,以便在调试器中逐步执行代码并分析问题所在。
// 检测到设备错误时的日志输出
if (urb->status != 0) {
dev_err(&myudev->udev->dev, "URB传输失败, 状态码: %d\n", urb->status);
// 错误恢复逻辑...
}
在上述代码中,我们展示了当URB传输失败时,如何在驱动程序中添加日志输出。这对于调试和定位USB传输错误非常有帮助。
在本章中,我们深入探讨了USB设备在Linux内核中的关键操作流程,包括设备枚举、配置与接口操作、中断和批量数据处理,以及设备断开和异常处理。每一步都涉及到与USB核心层的紧密交互,并依赖于精心设计的回调函数和资源管理策略。理解这些操作流程,对于开发稳定可靠的USB驱动程序至关重要。
5. 实际开发中的应用和修改
在探讨了USB驱动开发的基础知识和关键操作流程之后,本章将深入讨论如何在实际开发过程中应用这些理论知识,并对驱动进行必要的修改以满足特定的需求。
5.1 开发环境和工具链的搭建
为了有效地开发USB驱动,需要搭建一个强大的开发环境和选择合适的工具链。
5.1.1 交叉编译工具的配置和使用
交叉编译是一种在与目标系统架构不同的平台上编译代码的过程。在开发嵌入式设备的USB驱动时,由于目标硬件资源有限,通常采用交叉编译。
工具链安装
首先,需要选择合适的交叉编译工具链。例如,针对ARM架构的设备,可以使用 arm-linux-gnueabihf-gcc 工具链进行编译。
sudo apt-get install gcc-arm-linux-gnueabihf
编译参数设置
在编译USB驱动模块时,需要指定交叉编译工具链的路径和交叉编译器的前缀。
ARCH := arm
CROSS_COMPILE := arm-linux-gnueabihf-
使用 make 命令时,会通过这些参数调用正确的交叉编译器。
5.1.2 内核调试和分析工具的选择
调试USB驱动时,可以使用内核的调试工具如 printk 、 kgdb 以及 ftrace 。
printk
printk 函数用于在内核中输出调试信息,可以将调试信息打印到串口或通过 dmesg 命令查看。
printk(KERN_INFO "USB Driver: Device is connected\n");
kgdb
kgdb 是一个内核级别的调试器,需要与GDB结合使用。
- 启动内核时开启kgdb调试功能:
kgdboc=ttyS0,115200 kgdbwait
- 使用GDB连接到内核:
gdb vmlinux /proc/kcore
ftrace
ftrace 是一个强大的内核函数跟踪工具,可以用来监控函数调用。
echo function > /sys/kernel/debug/tracing/current_tracer
5.2 驱动代码的编写和调试技巧
驱动开发不仅仅是编码,还包括了编码后的调试。在编写驱动代码的过程中,会涉及到一些编写指导和规范。
5.2.1 驱动模块的编写指导和规范
编写驱动代码时,应遵循以下指导和规范:
- 遵循Linux内核编码风格。
- 使用内核提供的API进行内存分配和释放。
- 恰当地使用锁机制,避免死锁。
- 为驱动模块编写文档,说明驱动的功能、安装和卸载步骤。
5.2.2 调试过程中的常见问题及解决方法
在调试USB驱动时可能会遇到的问题包括设备无法枚举、数据传输错误等。
设备无法枚举
- 检查USB核心和USB设备驱动的初始化代码是否正确。
- 确保在驱动加载时内核日志中没有错误信息。
- 使用
usbhid-dump和lsusb等工具检查设备状态。
usbhid-dump -d <vendor_id>:<product_id>
lsusb -v
5.3 驱动的测试和性能优化
编写完USB驱动后,测试是必不可少的步骤。性能优化通常基于测试结果进行。
5.3.1 驱动的功能测试和性能评估
功能测试
功能测试主要是确保驱动能够正常工作,包括设备枚举、配置、接口操作和数据传输等。
性能评估
性能评估需要测量驱动在各种操作下的响应时间和吞吐量。
struct timeval start, end;
unsigned long elapsed;
gettimeofday(&start, NULL);
// Perform USB operation
gettimeofday(&end, NULL);
elapsed = (end.tv_sec - start.tv_sec) * 1000;
elapsed += (end.tv_usec - start.tv_usec) / 1000;
5.3.2 性能瓶颈的定位与优化策略
当性能不满足预期时,需要定位瓶颈并进行优化。
性能瓶颈定位
- 使用
perf工具分析CPU使用情况。 - 利用
ftrace跟踪函数调用。
perf record -g
perf report
优化策略
- 调整USB驱动中的缓冲区大小。
- 减少上下文切换和中断频率。
- 使用DMA进行数据传输。
urb->transfer_dma = dma_alloc_coherent(&intf->dev->dev, size, &urb->transfer_dma, GFP_KERNEL);
通过上述方法,开发人员可以有效地对USB驱动进行调试、测试和性能优化,确保最终产品的稳定性和效率。
6. 参考源码和文档以理解用法和原理
6.1 阅读和分析USB Skeleton源码
在Linux内核社区中,USB Skeleton是一个非常有用的资源,它为开发者提供了一个USB驱动程序的基本框架。通过分析USB Skeleton源码,开发者可以更好地理解USB驱动的结构和实现细节。
6.1.1 代码结构和实现细节解读
USB Skeleton的代码结构通常包括以下几个主要部分:
- 初始化和退出函数 :这部分代码负责模块的加载(
probe函数)和卸载(disconnect函数)。 - 设备操作函数 :定义了对USB设备进行各种操作的函数,如打开、释放、读写等。
- urb(USB请求块)处理 :urb是USB通信中的核心概念,用于提交和管理USB事务。
阅读这些部分的代码时,要注意函数之间的相互调用关系,以及数据结构是如何在这些函数中传递的。例如,在 probe 函数中,你可能会看到对 usb_register_dev 的调用,这表明驱动程序正在为设备注册一个设备号,使其能够出现在系统中。
6.1.2 如何通过源码学习USB驱动原理
学习USB驱动原理的一个好方法是跟随USB Skeleton的代码执行流程。从设备连接开始,逐步跟踪到数据的传输和设备的断开。理解USB设备枚举过程中内核是如何识别设备类型、配置接口、设置端点等的。
- 设备枚举 :识别和配置新连接的USB设备,是通过一系列的USB请求完成的,如获取设备描述符、配置描述符等。
- 数据传输 :数据传输涉及urb的创建和提交,以及在urb完成时如何处理数据。
- 错误处理 :通过分析如何响应urb的错误和如何重试或恢复失败的传输,了解错误处理机制。
通过这样的分析,你可以逐步构建起对USB驱动工作原理的全面理解。
6.2 利用官方文档深入学习USB规范
官方文档是学习USB技术的黄金标准。USB规范定义了USB技术的所有方面,包括硬件、协议和传输机制。
6.2.1 USB官方技术文档的获取和阅读
要获取官方USB技术文档,你可以访问USB-IF(USB Implementers Forum)的官方网站。在这个网站上,你可以找到不同版本的USB规范文档,例如USB 3.2和USB4规范。
6.2.2 规范中的关键概念与应用实例
在阅读规范时,重点理解以下几个关键概念:
- USB协议栈 :USB协议栈如何组织不同的层,包括USB事务处理层、USB传输层和USB主机控制层。
- 描述符 :设备、配置、接口和端点描述符的具体格式和含义。
- 传输类型 :控制传输、批量传输、中断传输和同步传输的特性及适用场景。
应用实例可以加深对规范的理解。例如,通过查看文档中关于如何处理HID(人机接口设备)的章节,你能理解到HID设备在操作系统中是如何识别和使用的。
6.3 结合实际案例提升开发技能
通过阅读源码和文档,你可以构建理论基础,但要将理论转化为实际技能,还需要分析实际的案例。
6.3.1 分析真实项目中的USB驱动代码
分析一个真实项目中的USB驱动代码,可以让你看到USB驱动在实际使用中的复杂性和多样性。例如,对于一个特定的打印机驱动,你可以看到它是如何处理不同打印任务的。
- 初始化和清理流程 :查看驱动是如何在系统启动和关闭时进行初始化和资源释放的。
- 事件处理和中断管理 :分析在设备状态改变或接收到数据时,驱动是如何响应和处理的。
6.3.2 学习如何处理复杂的USB设备兼容性问题
兼容性问题是USB驱动开发中的一大挑战。通过分析不同设备的驱动代码,你可以学习到开发者是如何解决不同设备之间可能出现的兼容性问题的。
- 设备特定的处理逻辑 :对于不同的硬件设备,可能需要特定的处理逻辑来确保驱动能够正确地与设备通信。
- 通用解决方案和技巧 :在解决特定兼容性问题时,开发者常常会分享一些通用的解决方案和技巧,如使用标准类驱动、编写设备特定的quirks等。
通过这些实际案例的学习,你可以提高解决实际问题的能力,并在自己的USB驱动开发中应用这些技巧。
简介:USB Skeleton是Linux环境下USB设备驱动开发的起点,提供了一套基础框架以帮助开发者快速上手。框架包含基本的USB驱动结构和核心组件,如设备描述符、配置描述符、接口描述符和端点描述符,以及关键操作流程如枚举、配置设置、接口和端点操作、中断处理和设备断开处理。通过学习和使用USB Skeleton,开发者可以避免重复编写通用代码,专注于实现与特定USB设备交互的逻辑。
更多推荐
所有评论(0)