本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:本文深入探讨了Linux系统下USB驱动的工作原理和开发过程。USB驱动在Linux中由三部分组成:主机控制器驱动、设备类驱动和设备驱动。Linux USB驱动支持热插拔,允许用户空间通过文件操作接口与USB设备进行数据读写。文章通过分析”usb-skeleton.c”模板和”Makefile”来指导开发者理解USB驱动的构建和工作流程,掌握使用USB调试工具进行故障排查。
usb.rar_USB LINUX_linux  USB_linux read usb_linux usb_linux usb

1. Linux USB驱动架构概述

Linux操作系统以其开源特性和强大的社区支持,成为众多嵌入式设备和服务器的首选。在众多硬件接口中,USB因其便捷的连接方式和广泛的支持成为不可或缺的硬件接口之一。Linux为USB硬件提供了丰富的支持,本章将从整体上介绍Linux USB驱动的基本架构,以便读者可以对后续章节中关于USB热插拔、数据传输、驱动开发以及调试等内容有更深入的理解。

Linux USB驱动架构简介

Linux USB驱动架构是建立在USB子系统之上的,该子系统负责管理和执行USB设备相关的所有操作。USB子系统位于Linux内核的设备驱动层,它抽象了硬件细节,提供了一系列的API供上层驱动调用。架构上主要分为以下几个层次:

  • USB核心层 :负责USB主机控制器和USB设备之间的通信,维护设备状态,并且提供设备识别、配置和电源管理等基础功能。
  • USB核心驱动 :处理USB协议的细节,是与USB硬件直接通信的部分。
  • USB设备驱动 :位于用户空间,用来控制特定USB设备的操作。

Linux USB驱动的组成

Linux USB驱动主要由以下几个部分组成:

  • USB驱动程序 :识别和管理USB设备的软件模块。
  • USB设备接口 :通过sysfs和devfs等机制,向用户空间提供设备信息和控制接口。
  • USB核心API :内核提供的接口,用于USB驱动编写者实现特定的USB设备操作,如urb(USB Request Block)的创建和提交。

USB驱动与内核的关系

USB驱动在内核中的角色至关重要,它确保了USB设备的正常连接、数据传输和电源管理。驱动开发者需要了解USB协议以及内核提供的相关API,才能编写出稳定、高效的驱动程序。了解Linux USB驱动架构,是深入研究USB热插拔、数据传输等高级功能和进行调试的前提。随着接下来章节的深入,我们将逐步揭开Linux USB驱动的神秘面纱,揭示其在现代操作系统中的核心作用。

2. USB热插拔功能的实现原理

2.1 热插拔技术简介

2.1.1 热插拔的历史和发展

热插拔技术的历史始于个人计算机硬件扩展的需要。在早期的计算机系统中,添加或移除硬件设备通常需要关闭系统电源,然后打开机箱,手动进行物理连接或断开。这种做法不仅繁琐,而且容易因为操作不当而导致硬件损坏或数据丢失。随着计算机技术的发展,尤其是在硬件接口标准化之后,对硬件设备的热插拔支持成为了一种迫切需求。

热插拔技术的进步在很大程度上得益于USB接口的普及。自1996年USB 1.0规范发布以来,USB接口以其便捷性、高兼容性和即插即用的特性迅速成为PC和众多电子设备的标准接口。USB热插拔允许用户在不关闭系统电源的情况下连接或断开USB设备,极大地方便了用户操作,同时也减少了因硬件操作不当带来的风险。

2.1.2 热插拔在USB中的重要性

USB热插拔的引入不仅提高了用户操作的便捷性,还对整个计算机系统的稳定性和安全性产生了深远影响。热插拔功能使得设备可以随时连接或断开,无需重启系统,这意味着用户可以实时扩展或更新他们的计算机系统。例如,USB打印机、外部硬盘驱动器、摄像头等设备都可以在需要时即时添加到系统中,使用完毕后随时断开连接,不再占用系统资源。

热插拔的重要性还体现在它对系统的资源管理上。系统通过热插拔事件能够及时识别新连接的设备,并根据设备类型自动加载相应的驱动程序。当设备不再使用时,系统也可以通过热插拔事件来执行清理工作,如卸载驱动程序和释放相关资源。这种动态管理机制有效提高了系统的资源利用效率,并增强了系统的扩展性和灵活性。

2.2 Linux系统中的热插拔机制

2.2.1 系统对热插拔的支持

Linux作为开源操作系统的代表,从早期版本开始就对热插拔提供了良好的支持。Linux内核中,热插拔事件由udev子系统处理,udev子系统是devfs的后继者,负责管理设备文件,包括设备的创建、命名和删除。udev的引入极大地增强了Linux系统对热插拔设备的支持能力。

在Linux系统中,当USB设备连接到计算机上时,内核会首先检测到硬件事件,并发送一个事件通知给udev。udev根据内核提供的信息创建设备文件,并可能加载相应的驱动程序。与此同时,udev还可以根据预设的规则来改变设备文件的权限,或者执行其他的自定义脚本,从而实现对热插拔事件的灵活处理。

2.2.2 热插拔事件的处理流程

热插拔事件的处理流程是Linux系统中的关键环节。当USB设备被插入或拔出时,系统会产生一个设备事件,并通过设备模型发送到用户空间。用户空间中,udev子系统监听这些设备事件,并根据其规则文件来决定如何响应。

udev处理热插拔事件的过程大致可以分为以下几个步骤:

  1. 事件生成 :当USB设备热插拔时,内核中的USB核心会捕获这个事件并生成一个热插拔事件,如插入事件(add)或移除事件(remove)。

  2. 事件传递 :内核通过netlink套接字将事件发送到用户空间。

  3. udev监听 :udev监听系统发出的热插拔事件。

  4. 规则匹配与执行 :udev根据其规则文件(通常是 /etc/udev/rules.d/ 目录下的文件)来匹配事件,并执行相应的规则命令。

  5. 设备文件创建 :根据规则中定义的动作,udev可能会创建对应的设备文件或符号链接,以及执行其他的用户空间脚本。

  6. 驱动程序加载 :如果事件对应的设备需要特定的驱动程序,udev会通知系统加载相应的驱动模块。

  7. 用户空间通知 :最后,udev可能还会向用户空间其他程序如 HAL (硬件抽象层)发送通知,以便执行更高级别的设备管理任务。

2.3 热插拔功能的编程实践

2.3.1 用户空间下的热插拔监控

在Linux系统中,除了udev负责的内核级热插拔监控外,用户空间程序也可以利用libudev库进行热插拔监控。libudev是一个用户空间库,它提供了与udev守护进程交互的API接口。使用libudev,开发人员可以编写自己的热插拔事件监控程序,实现对特定设备事件的捕获和响应。

使用libudev的程序通常包括以下几个关键步骤:

  1. 打开udev库 :使用libudev提供的函数初始化库环境,以便程序可以访问udev的API。

  2. 创建udev上下文 :udev上下文是监控事件的基础,它包含了udev守护进程的状态和环境信息。

  3. 创建udev监视器 :监视器用于监听设备事件,它是libudev中用于捕获热插拔事件的中心组件。

  4. 事件回调处理 :为监视器设置事件回调函数,当有热插拔事件发生时,udev会调用这个回调函数。

  5. 过滤事件 :可以设置过滤条件来筛选需要监听的事件类型或特定的设备。

  6. 事件循环 :将程序置于事件循环中,以便能够持续监听并处理热插拔事件。

  7. 关闭资源 :在程序退出或不再需要监听事件时,应正确关闭udev上下文和监视器,并释放所有资源。

下面是一个使用libudev进行热插拔监控的简单示例代码:

#include <libudev.h>
#include <stdio.h>

static int monitor_callback(struct udev *udev, struct udev_device *dev, void *arg) {
    printf("Device %s (%s) %s\n",
           udev_device_get_sysname(dev),
           udev_device_get_action(dev),
           udev_device_get_devpath(dev));
    return 0;
}

int main() {
    struct udev *udev;
    struct udev_monitor *monitor;

    udev = udev_new();
    if (!udev) {
        fprintf(stderr, "Can't create udev context\n");
        return 1;
    }

    monitor = udev_monitor_new_from_netlink(udev, "udev");
    udev_monitor_filter_add_match_subsystem_devtype(monitor, NULL, NULL);
    udev_monitor_enable_receiving(monitor);

    while (1) {
        struct udev_device *dev;
        fd_set fds;
        FD_ZERO(&fds);
        FD_SET(udev_monitor_get_fd(monitor), &fds);
        select(FD_SETSIZE, &fds, NULL, NULL, NULL);

        dev = udev_monitor_receive_device(monitor);
        if (dev)
            monitor_callback(udev, dev, NULL);
    }

    udev_monitor_unref(monitor);
    udev_unref(udev);
    return 0;
}

2.3.2 驱动程序中的热插拔事件响应

在USB设备驱动程序中,热插拔事件的响应通常依赖于驱动框架提供的钩子函数。USB设备驱动程序使用特定的API来注册设备的热插拔回调函数,当设备连接或断开时,这些回调函数将被内核调用。

编写驱动程序中的热插拔响应代码通常遵循以下步骤:

  1. 设备注册 :在驱动程序初始化时,使用 usb_register_driver() 函数向USB核心注册驱动程序。

  2. 热插拔钩子注册 :通过 usb_driver 结构体中的 probe 和 disconnect 函数指针注册设备连接和断开时的回调函数。

  3. probe函数实现 :在 probe 函数中实现设备连接时需要执行的初始化代码。

  4. disconnect函数实现 :在 disconnect 函数中实现设备断开时需要执行的清理代码。

  5. 设备移除处理 :当USB设备被移除时,USB核心会自动调用 disconnect 函数。

下面是一个USB驱动程序中热插拔事件响应的简单示例代码:

static int usb_my_driver_probe(struct usb_interface *interface,
                               const struct usb_device_id *id)
{
    // 设备连接时的初始化工作
    printk(KERN_INFO "USB device (%04X:%04X) plugged\n", id->idVendor, id->idProduct);
    return 0;
}

static void usb_my_driver_disconnect(struct usb_interface *interface)
{
    // 设备断开时的清理工作
    printk(KERN_INFO "USB device removed\n");
}

static struct usb_driver usb_my_driver = {
    .name = "my_usb_driver",
    .id_table = usb_my_id_table,
    .probe = usb_my_driver_probe,
    .disconnect = usb_my_driver_disconnect,
};

static int __init usb_my_driver_init(void)
{
    return usb_register(&usb_my_driver);
}

static void __exit usb_my_driver_exit(void)
{
    usb_deregister(&usb_my_driver);
}

module_init(usb_my_driver_init);
module_exit(usb_my_driver_exit);

在上述代码中, usb_my_driver_probe 函数是设备连接时的回调函数,而 usb_my_driver_disconnect 函数是设备断开时的回调函数。当设备插入或拔出时,内核会自动调用这些函数以响应热插拔事件。通过这种方式,驱动程序可以控制USB设备的正确初始化和清理过程,确保系统资源的有效管理。

3. USB数据读写机制详解

在深入理解了USB驱动架构和热插拔功能之后,我们将探讨USB数据读写机制。这是USB设备与主机系统交互的核心过程,涉及数据传输、编程实践、以及性能优化。

3.1 USB数据传输基础

3.1.1 USB传输模式和类型

USB支持四种数据传输模式:控制传输、批量传输、中断传输和同步传输。每种传输模式有其特定的用途和特性。

  • 控制传输 :主要用于USB设备的初始配置以及进行状态查询和设置。它使用端点0进行通信。
  • 批量传输 :用于数据量大、要求可靠传输但可以容忍延时的场景,比如打印机、扫描仪等设备。
  • 中断传输 :用于需要快速响应的小量数据传输,如键盘和鼠标。中断传输有固定的带宽和调度。
  • 同步传输 :为实时数据(如音频和视频)提供固定带宽的传输,具有较高的优先级。

在Linux内核中,USB子系统抽象了这些传输模式,使得驱动开发者可以更专注于实现设备的特定逻辑,而不必担心底层细节。

3.1.2 Linux内核中的USB I/O架构

Linux内核中的USB I/O架构由几个主要组件构成:

  • USB核心 :作为USB子系统的核心,负责处理通用USB事务,如设备枚举、地址分配和数据传输。
  • USB驱动模型 :为USB设备提供了一种模型,包括USB设备、USB接口和USB配置的抽象。
  • USB主机控制器驱动 :与特定硬件平台的USB控制器进行交互。
  • USB设备驱动 :与特定USB设备进行交互,实现设备特定的逻辑。

USB I/O架构允许数据在USB设备与主机之间的通信基于标准的API进行。开发者通常会使用urb(USB请求块)来发起数据传输。

3.2 数据读写编程实践

3.2.1 编写USB设备的读写函数

在Linux USB驱动中,urb的创建和提交是数据传输的关键。以下是一个简单的urb提交示例:

#include <linux/usb.h>

struct urb *my_urb; // 定义一个urb指针

// 创建urb的函数
int create_urb(struct usb_device *dev, int pipe, unsigned char *buffer, int size) {
    my_urb = usb_alloc_urb(0, GFP_KERNEL); // 分配urb
    if (!my_urb) {
        return -ENOMEM;
    }

    usb_fillBulkurb(my_urb, dev, pipe, buffer, size, usb_callback, NULL); // 填充urb参数
    return usb_submit_urb(my_urb, GFP_KERNEL); // 提交urb
}

// urb的回调函数
void usb_callback(struct urb *urb) {
    // 处理urb完成后的回调逻辑
}

在创建urb时, usb_alloc_urb 用于分配urb内存, usb_fillBulkurb 填充urb的参数。 usb_submit_urb 函数将urb提交给USB核心进行处理。而 usb_callback 函数是在urb传输完成后会被调用的回调函数。

3.2.2 数据缓存和同步机制

在USB数据传输中,数据缓存和同步机制是保证数据一致性的关键。为了保证数据缓存的同步,可以使用各种锁机制,例如互斥锁、读写锁等。此外,Linux内核还提供了 usb_copy_to_user 和 usb_copy_from_user 等函数,以支持在用户空间和内核空间之间安全地传输数据。

3.3 错误处理与性能优化

3.3.1 常见的USB传输错误及处理

在USB数据传输过程中可能会遇到各种错误,例如:

  • -EPIPE :传输管道损坏,可能需要重新设置管道。
  • -ECONNRESET :设备被重置,需要重新提交urb。
  • -ESHUTDOWN :设备已经断开,传输无法完成。

错误处理通常需要根据错误类型采取相应的恢复策略。

3.3.2 提升USB数据传输效率的方法

为了提升USB数据传输效率,可以采取以下措施:

  • 批量提交urb :一次提交多个urb可以减少提交urb的开销。
  • 使用DMA :直接内存访问(DMA)可以减轻CPU负担,提高数据传输速度。
  • 适当使用中断传输 :中断传输在一些特定场景下比批量传输更有效率。

在Linux内核中,还可以利用 urb_split 标志来分割大数据包为多个较小的urb,这可以在某些USB主机控制器上提高传输性能。

// 代码段展示urb_split标志的使用
usb_fillBulkurb(my_urb, dev, pipe, buffer, size, usb_callback, NULL);
my_urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
my_urb->transfer_flags |= URB_NO_SETUP_DMA_MAP;
my_urb->transfer_buffer_length = 1024; // 分割为多个1024字节的urb

在上述代码中,通过设置 urb->transfer_flags 的相应位,可以控制urb在提交时是否进行DMA映射。这有助于改善性能,尤其是当数据需要在系统和USB设备间多次传输时。

接下来,我们将深入探究Linux内核中”usb-skeleton.c”模板的使用,该模板为USB设备驱动开发提供了一个实用的起点。

4. “usb-skeleton.c”模板深入分析

4.1 模板结构和关键代码解析

4.1.1 “usb-skeleton.c”文件概述

usb-skeleton.c 是Linux内核提供的一个USB驱动程序模板,它为开发者提供了一个创建USB驱动程序的基础框架。这个模板包含了一个USB驱动程序所必需的标准组件,如USB设备注册、注销、probe和disconnect函数,以及数据传输的实现方法。

该模板帮助开发者快速开始他们的USB驱动程序开发工作,减少了重复编写相同代码的需要,并为理解USB驱动架构提供了一个清晰的实例。此外,模板还提供了一定的灵活性,允许开发者修改和扩展代码以适应他们的特定硬件和需求。

4.1.2 模板中的核心函数和数据结构

在模板中,有几个核心的函数和数据结构是理解USB驱动程序所必须的:

  • module_init() 和 module_exit() 宏:分别用来定义模块加载和卸载时调用的初始化函数。
  • usb_driver 结构体:包含了一系列USB驱动的注册信息,如驱动的名称、probe和disconnect函数指针。
  • probe() 函数:在USB设备被系统识别时调用,用于初始化USB设备。
  • disconnect() 函数:在USB设备从系统中断开时调用,用于清理资源。

一个典型的 usb_driver 结构体定义如下:

static struct usb_driver my_usb_driver = {
    .name = "my_usb_driver",
    .id_table = id_table,
    .probe = usb_probe,
    .disconnect = usb_disconnect,
    .suspend = usb_suspend,
    .resume = usb_resume,
};

在 probe 函数中,通常会进行设备检测、初始化设备数据结构、注册设备操作函数等操作。相反,在 disconnect 函数中,会注销之前注册的设备操作函数,并释放设备数据结构占用的资源。

4.2 模板在开发中的应用

4.2.1 基于模板的USB设备驱动开发

基于 usb-skeleton.c 的模板开发USB设备驱动非常直接。开发者首先复制模板代码到新的文件中,然后根据自己的USB设备信息修改 id_table 以匹配设备ID。接下来,实现 probe() 和 disconnect() 函数以及其他需要的特定操作函数。

例如,以下代码展示了如何在probe函数中初始化数据结构:

static int usb_probe(struct usb_interface *interface, const struct usb_device_id *id)
{
    struct my_usb_dev *my_dev;
    struct usb_device *dev = interface_to_usbdev(interface);

    // 分配设备结构体
    my_dev = kzalloc(sizeof(struct my_usb_dev), GFP_KERNEL);
    if (!my_dev) {
        printk(KERN_ERR "Out of memory\n");
        return -ENOMEM;
    }

    // 初始化设备结构体中的USB设备句柄和其他成员
    usb_set_intfdata(interface, my_dev);

    return 0;
}

4.2.2 模板的修改与优化实例

针对特定的硬件设备或者性能优化需求,开发者可能需要对模板进行相应的修改。例如,如果需要更高效的数据传输,可能需要优化urb(USB请求块)的使用方式,或者使用DMA(直接内存访问)来减少CPU负载。

优化后的代码可能包含如下内容:

urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP; // 使用DMA映射
urb->transfer_buffer = dma_alloc_coherent(&dev->dev, buffer_size, &urb->transfer_dma, GFP_KERNEL);
urb->transfer_buffer_length = buffer_size;

此外,对于特定的性能问题,可能还需要考虑使用批量传输或中断传输等不同的传输类型,或者重新设计数据处理逻辑以减少延迟。

在代码逻辑的逐行解读分析中,可以进一步解释每一步操作的目的和背后的工作原理,确保读者能够清楚地理解每一个函数调用和数据结构操作的意图。通过这种方式,”usb-skeleton.c”模板不仅提供了一个USB驱动开发的基础,还为开发者提供了一个学习和优化的平台。

5. “Makefile”配置与编译的技巧

5.1 “Makefile”基础

5.1.1 “Makefile”在Linux驱动编译中的作用

在Linux内核开发中,”Makefile”文件是编译自动化的核心。它的主要作用是定义编译规则和依赖关系,以自动化的方式完成源代码到可执行文件的转换。对于驱动程序,”Makefile”文件尤为重要,因为它们通常包括多个源文件,依赖关系复杂,并且需要链接到内核的特定部分。一个精心编写的”Makefile”可以极大地简化开发过程,并提高代码编译的效率和准确性。

5.1.2 标准”Makefile”模板解析

一个标准的”Makefile”模板通常包括以下几个部分:

  1. Makefile的头部 :定义一些全局变量和目标,例如 obj-m 、 模块名-y 等。
  2. 编译器设置 :指定使用的是哪种编译器,例如 CC 变量用于指定C编译器。
  3. 编译选项 :根据需要添加特定的编译选项,如 -Wall 、 -Werror 等。
  4. 构建规则 :定义编译内核模块的规则,例如使用 $(MAKE) -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules 。
  5. 清理目标 :定义清理编译产物的规则,通常是一个 clean 目标。

以下是一个简化的”Makefile”模板示例:

obj-m += usb_driver.o

all:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

5.2 “Makefile”高级配置与优化

5.2.1 多目标和模块化编程的”Makefile”

在实际项目中,”Makefile”可能会变得更为复杂,涉及到多个目标文件和模块化编程。对于这种情况,”Makefile”需要支持模块化编程,能够单独编译、清理或者安装每个模块。

以下是一个支持模块化编程的”Makefile”示例:

# 定义模块名变量
MODULES = usb_driver another_module

all: $(MODULES)

# 模块的编译规则
$(MODULES):
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

# 单独编译模块
%_module:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) $@

# 单独清理模块
clean_%:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

5.2.2 调试与发布版”Makefile”的区别

在开发过程中,为了确保代码的稳定性和性能,开发者会使用不同的编译参数。”Makefile”中通常会包含调试模式和发布模式的不同编译选项。

调试模式会加入大量的调试信息,禁用优化,例如:

CFLAGS += -g -O0

发布模式会启用优化,减少代码体积和运行时的开销,例如:

CFLAGS += -O2 -fomit-frame-pointer

5.3 实际项目中的”Makefile”应用

5.3.1 面向特定硬件的”Makefile”定制

对于特定硬件的驱动开发,”Makefile”可能需要包含硬件平台的特定参数和编译指令。例如,针对ARM架构的处理器,可能需要指定交叉编译工具链。

定制”Makefile”以适应特定硬件平台可能包括:

  • 交叉编译器的设置 :通过设置 CROSS_COMPILE 变量指定交叉编译器。
  • 硬件平台特定的内核配置选项 :使用 ARCH 变量指定目标架构。
  • 额外的编译选项 :根据硬件要求添加特定的优化和调试选项。

5.3.2 “Makefile”在持续集成中的使用

在持续集成(CI)的环境下,”Makefile”可以被用来自动化测试和构建过程。例如,在一个git仓库中,”Makefile”可以定义以下目标:

  • test :运行单元测试和代码质量检查。
  • build :执行模块的编译过程。
  • package :将编译好的模块打包为发行包。

通过这样的”Makefile”,开发者可以在每次代码提交时自动执行这些目标,从而提高开发效率和代码质量。

test:
    # 运行测试套件的命令
    ./run_tests.sh

build:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

package:
    # 打包编译好的模块的命令
    ./package_module.sh

在持续集成系统中, make 命令可以与其他工具结合,例如Jenkins或GitHub Actions,来实现更复杂的自动化流程。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:本文深入探讨了Linux系统下USB驱动的工作原理和开发过程。USB驱动在Linux中由三部分组成:主机控制器驱动、设备类驱动和设备驱动。Linux USB驱动支持热插拔,允许用户空间通过文件操作接口与USB设备进行数据读写。文章通过分析”usb-skeleton.c”模板和”Makefile”来指导开发者理解USB驱动的构建和工作流程,掌握使用USB调试工具进行故障排查。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐