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

一、驱动类型

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. 结构体如下所示。

cpp 复制代码
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

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

(2)释放一个URB对象。

cpp 复制代码
void usb_free_urb(struct urb *urb);

(3)填充控制传输URB。

cpp 复制代码
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。

cpp 复制代码
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。

cpp 复制代码
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;如果失败,返回一个负数错误码。

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

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

cpp 复制代码
void usb_kill_urb(struct urb *urb);

三、主机侧驱动开发框架

1. 操作流程。

(1)编写USB驱动框架。

(2)完善probe函数。

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

2. 编写USB驱动框架。

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

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

cpp 复制代码
#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的回调函数中,上报事件。

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

cpp 复制代码
#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;  // 数据缓冲区
int myusb_size = 0;  // 缓冲区大小
dma_addr_t myusb_dma;  // DMA地址

// 键盘的键码表,包含标准键盘的按键映射
static const unsigned char usb_keyboardcode[256] = {
    0, 0, 30, 48, 46, 32, 18, 33, 34, 35, 23, 36, 37, 38, 0, 50,
    49, 24, 25, 16, 19, 31, 20, 22, 47, 17, 45, 21, 44, 2, 3, 5,
    6, 7, 8, 9, 10, 11, 28, 1, 14, 15, 57, 12, 13, 26, 4, 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, 6, 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
};

//回调函数执行的条件是 URB传输完成,无论是成功还是失败。
static void myusb_func(struct urb *urb)  //填充中断URB的回调函数
{
    if (urb->status) {
        pr_err("URB transfer failed with status %d\n", urb->status);
    } else {
        pr_info("URB transfer completed successfully\n");
    }
}

// 设备连接时执行
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);
    // 填充URB
    usb_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);  // 释放URB
    usb_free_coherent(myusb_dev, myusb_size, myusb_buf, myusb_dma);  // 释放一致性内存
    input_unregister_device(myusb_inputdev);    // 注销输入设备
    input_free_device(myusb_inputdev);    // 释放输入设备
}
相关推荐
Xudde.1 小时前
100条Linux命令汇总
linux·运维·笔记·学习
戴国进2 小时前
Linux磁盘空间不足,12个详细的排查方法
linux·运维·服务器
m0_748171582 小时前
云原生作业(四)
linux·运维·服务器
A charmer2 小时前
探秘 Linux 进程状态:解锁系统运行的密码
linux·服务器·网络
Mr.kanglong3 小时前
【Linux】网络层
linux·服务器·网络
hao_wujing3 小时前
Linux 高级路由与流量控制-用 tc qdisc 管理 Linux 网络带宽
linux·网络·智能路由器
丿无缘梦丨4 小时前
ubuntu,vmware虚拟机共享文件夹配置
linux·ubuntu
azoon.top4 小时前
centos设置开机自启的几种方案(frp为例)
linux·运维·服务器·centos·frp·开机自启
m0_748254094 小时前
Linux下MySQL的简单使用
linux·mysql·adb