Linux USB驱动学习

USB系统硬件和软件框架

PC接入USB设备过程相关疑问

USB设备插入识别过程

USB设备插到电脑上接触到的对方设备是USB控制器,是USB控制器内嵌的root hub。接入USB设备后,USB总线驱动程序负责识别USB设备,给USB设备找到对应的驱动程序。

PC硬件监测USB设备

PC的USB口内部,D-和D+接有15K的下拉电阻,未接USB设备时为低电平 USB设备的USB口内部,D-或D+接有1.5K的上拉电阻;它一接入PC,就会把PC USB口的D-或D+拉高,从硬件的角度通知PC有新设备接入。

接入PC时如何识别USB设备的种类:

PC和USB设备都得遵守一些规范。比如:USB设备接入电脑后,PC机会发出"你是什么"?USB设备就必须回答"我是xxx", 并且回答的格式是固定的。USB总线驱动程序会发出某些命令想获取设备信息(描述符),USB设备必须返回"描述符"给PC。

PC如何分辨已存在的多个USB设备:

每一个USB设备接入PC时,USB总线驱动程序都会给它分配一个编号。PC机想访问某个USB设备时,发出的命令都含有对应的编号(地址)。且新接入的USB设备的默认编号是0,在未分配新编号前,PC使用0编号和它通信。

硬件框架

在USB系统中,有2个硬件概念:

  • USB Host:它跟处理器相连,处理器通过USB Host跟各类USB设备通信。USB Host中集成有一个root hub

  • USB Device:这分为两类设备

    Hub:用来扩展USB接口

    Function:就是普通的USB设备,比如U盘、声卡等

    hub与func是同级的,hub最多有6级,第7级一定是func。

软件框架

APP可以通过USB设备驱动 程序访问USB设备。也可以绕过USB设备驱动,直接通过USB控制器驱动 访问USB设备,通过控制器驱动访问硬件设备时在应用层有 libusb 库以简化访问。

USB电器信号

电子信号

USB连接线有4条:5V、D+、D-、GND。数据线D+、D-,只能表示4种状态。USB协议中,很巧妙地使用这两条线路实现了空闲(Idle)、开始(SOP)、传输数据(Data)、结束(EOP)等功能。

数据信号

以下对于低速/全速设备 举例讲解,

同步信号固定KJKJKJKK ,(K信号:D+高电平,D-低电平;J信号:D+低电平,D-高电平)host发送sync信号给USB设备,USB设备通过sync信号计算出时钟周期从而实现数据收发同步,USB使用的是一对差分信号线传输sync信号,不同于I2C、SPI使用CLK线同步时钟,也不同于串口需要双方约定波特率。

SOP:Start Of Packet,Hub驱动D+、D-这两条线路从Idle状态变为K状态。SOP中的K状态就是SYNC信号的第1位数据,SYNC格式为3对KJ外加2个K。

EOP :End Of Packet,由数据的发送方发出EOP,数据发送方驱动D+、D-这两条线路,先设为SE0状态并维持2位时间,再设置为J状态并维持1位时间,最后D+、D-变为高阻状态,这时由线路的上下拉电阻使得总线进入Idle状态。

NRZI与位填充

NRZI:Non Return Zero Inverted Code,反向不归零编码。NRZI的编码方位为:对于数据0,波形翻转;对于数据1,波形不变。

Bit Stuffing

使用NRZI时,如果传输的数据总是"1",会导致波形维持不变。如果电平长时间维持不变,比如传输100位1时,如果接收方稍有偏差,就可能认为接收到了99位1、101位1。而USB中采用了Bit-Stuffing位填充处理,即在连续发送6个1后面会插入1个0,强制翻转发送信号,从而让接收方调整频率,同步接收。而接收方在接收时只要接收到连续的6个1后,直接将后面的0删除即可恢复数据的原貌。

USB描述符

怎么描述设备、配置、接口、端点?使用描述符(Descriptors),有设备描述符、配置描述符、接口描述符、端点描述符。所谓描述符,就是一些格式化的数据,用来描述信息。 一个USB设备,

  • 只有一个设备描述符:用来表示设备的ID、它有多少个配置、它的端点0一次最大能传输多少字节数据
  • 可能有多个配置描述符:用来表示它有多少个接口、供电方式、最大电流
  • 一个配置描述符下面,可能有多个接口描述符:用来表示它是哪类接口、有几个设置(Setting)、有几个端点
  • 一个接口描述符符下面,可能有多个端点描述符:用来表示端点号、方向(IN/OUT)、类型(批量/中断/同步)

在虚拟机中查看

bash 复制代码
$ lsusb -v

libusb(应用态驱动)

介绍

libusb是一个使用C编写的库,它提供USB设备的通用的访问方法。APP通过它,可以方便地访问USB设备,无需编写USB设备驱动程序。

  • 可移植性:支持Linux、macOS、Windows、Android、OpenBSD等
  • 用户模式:APP不需要特权模式、也不需要提升自己的权限即可访问USB设备
  • 支持所有USB协议:从1.0到3.1都支持

libusb支持所有的传输类型(控制/批量/中断/实时),有两类API接口:同步(Synchronous,简单),异步(Asynchronous,复杂但是更强大)。它是轻量级的、线程安全的。还支持热拔插。

使用libusb编写应用程序意味着可以跳过设备驱动程序直接使用host驱动程序去操作usb设备,那就需要应用编写者去阅读设备规范、R/W、endpoint、解析Data。

用法

可以通过libusb访问USB设备,不需要USB设备端的驱动程序,需要移除原来的驱动程序。然后就可以直接访问USB控制器的驱动程序,使用open/read/write/ioctl/close这些接口来打开设备、收发数据、关闭设备。 libusb封装了更好用的函数,这些函数的使用可以分为5个步骤:

  • 初始化
  • 打开设备
    使用 libusb_get_config_descriptor() 就获取了设备的接口描述符和端点描述符。
    设备描述符无需释放,它保存在list的设备里。
  • 移除原驱动/认领接口
    移除设备驱动使用libusb访问设备,认领只是一个逻辑操作。
    方式一:设置自动移除标记位,在claim时先判断标记位再执行移除操作。
    方式二:直接移除再claim。
  • 传输
    同步传输:接口内部发起传输并等待传输完成、等待超时或错误。
    异步传输:先分配传输结构体(alloc);设置传输结构体用于保存是哪个endpoint、保存数据的buffer或数据长度len(fill);接着提交结构体启动传输(submit);启动后等待传输完成(completed\handle);释放传输结构体(free_transfer)。可在循环里执行传输。在异步接口函数里,启动传输和设置回调函数后就立刻返回。等"读到数据"或"得到回应"后,回调函数被调用。发送数据传输的线程无需休眠等待结果,支持"多endpoint"的操作,也支持取消传输。
  • 关闭设备
    对应前面的认领接口、打开设备、初始化libusb执行反向操作。

    最后的 libusb_close 改成 libusb_exit 。

API

1.初始化/反初始化

c 复制代码
int API_EXPORTED libusb_init(libusb_context **ctx);

初始化libusb,参数是一个"a context pointer"的指针,如果这个参数为NULL,则函数内部会创建一个"default context"。所谓"libusb context"就是libusb上下文,就是一个结构体,里面保存有各类信息,比如:libusb的调试信息是否需要打印、各种互斥锁、各类链表(用来记录USB传输等等)。

在关闭USB设备(libusb_close)后、程序退出前,调用如下函数:

c 复制代码
void API_EXPORTED libusb_exit(libusb_context *ctx);

2.获取设备

c 复制代码
ssize_t API_EXPORTED libusb_get_device_list(libusb_context *ctx,
	libusb_device ***list);

调用此函数后,所有设备的信息存入list,然后遍历list,找到想操作的设备。这个函数内部会分配list的空间,所以用完后要释放掉,使用以下函数释放:

c 复制代码
void API_EXPORTED libusb_free_device_list(libusb_device **list,
	int unref_devices);

如果参数unref_devices为1, 则list中每个设备的引用计数值减小1

3.打开/关闭设备

使用libusb_get_device_list得到设备列表后,可以选择里面的某个设备,然后调用libusb_open:

c 复制代码
int API_EXPORTED libusb_open(libusb_device *dev,libusb_device_handle **dev_handle);

使用libusb_open函数打开USB设备后,可以得到一个句柄:libusb_device_handle。以后调用各种数据传输函数时,就是使用libusb_device_handle。

使用完毕后,调用libusb_close关闭设备,函数原型如下:

c 复制代码
void API_EXPORTED libusb_close(libusb_device_handle *dev_handle);

关闭设备句柄, 在程序退出之前应该使用它去关闭已经打开的句柄,相较于libusb_open()增加设备的引用计数,在libusb_close()中会减小设备的引用计数。

4.根据ID打开设备 :

如果知道设备的VID、PID,那么可以使用libusb_open_device_with_vid_pid来找到它、打开它。这个函数的内部,先使用libusb_get_device_list列出所有设备,然后遍历它们根据ID选出设备,接着调用libusb_open打开它,最后调用libusb_free_device_list释放设备。

libusb_open_device_with_vid_pid函数原型如下:

c 复制代码
DEFAULT_VISIBILITY
libusb_device_handle * LIBUSB_CALL libusb_open_device_with_vid_pid(
	libusb_context *ctx, uint16_t vendor_id, uint16_t product_id);

5.描述符相关函数:

  • 获得设备描述符
c 复制代码
int API_EXPORTED libusb_get_device_descriptor(libusb_device *dev,
	struct libusb_device_descriptor *desc);
  • 获得/释放配置描述符
c 复制代码
/** \ingroup libusb_desc
 * 获得指定的配置描述符
 *
 * 参数:
 * dev - 哪个设备
 * config_index - 哪个配置
 * config - 输出参数, 用来保存配置描述符, 使用完毕要调用libusb_free_config_descriptor()释放掉
 * 
 * 返回值:
 * 0 - 成功
 * LIBUSB_ERROR_NOT_FOUND - 没有这个配置
 * 其他LIBUSB_ERROR错误码
 */
int API_EXPORTED libusb_get_config_descriptor(libusb_device *dev,
	uint8_t config_index, struct libusb_config_descriptor **config);

/** \ingroup libusb_desc
 * Free a configuration descriptor obtained from
 * 前面使用libusb_get_active_config_descriptor()或libusb_get_config_descriptor()获得配置描述符,
 * 用完后调用libusb_free_config_descriptor()释放掉
 */
void API_EXPORTED libusb_free_config_descriptor(
	struct libusb_config_descriptor *config);

6.detach/attach驱动:

使用libusb访问USB设备时,需要先移除(detach)设备原来的驱动程序,然后认领接口(claim interface)。有两种办法:

  • 方法一:
c 复制代码
// 只是设置一个标记位表示libusb_claim_interface
// 使用libusb_claim_interface时会detach原来的驱动
libusb_set_auto_detach_kernel_driver(hdev, 1);  

// 标记这个interface已经被使用认领了
libusb_claim_interface(hdev, interface_number);
  • 方法二:
c 复制代码
// detach原来的驱动
libusb_detach_kernel_driver(hdev, interface_number);

// 标记这个interface已经被使用认领了
libusb_claim_interface(hdev, interface_number);

使用完USB设备后,在调用libusb_close之前,应该libusb_release_interface释放接口:

c 复制代码
int API_EXPORTED libusb_release_interface(libusb_device_handle *dev_handle,
	int interface_number);

调用上面接口卸载前面claim的特殊驱动。

7.同步传输函数

  • 控制传输
c 复制代码
/** \ingroup libusb_syncio
 * 启动控制传输
 *
 * 传输方向在bmRequestType里
 * wValue,wIndex和wLength是host-endian字节序
 *
 * 参数:
 * dev_handle - 设备句柄
 * bmRequestType - setup数据包的bmRequestType域
 * bRequest      - setup数据包的bRequest域
 * wValue        - setup数据包的wValue域
 * wIndex        - setup数据包的wIndex域
 * data          - 保存数据的buffer, 可以是in、out数据
 * wLength       - setup数据包的wLength域
 * timeout       - 超时时间(单位ms),就是这个函数能等待的最大时间; 0表示一直等待直到成功
 * 
 * 返回值:
 * 正整数 - 成功传输的数据的长度
 * LIBUSB_ERROR_TIMEOUT - 超时
 * LIBUSB_ERROR_PIPE  - 设备不支持该请求
 * LIBUSB_ERROR_NO_DEVICE - 设备未连接
 * LIBUSB_ERROR_BUSY - 如果这个函数时在事件处理上下文(event handling context)里则返回这个错误
 * LIBUSB_ERROR_INVALID_PARAM - 传输的字节超过OS或硬件的支持
 * the operating system and/or hardware can support (see \ref asynclimits)
 * 其他LIBUSB_ERROR错误码
 */
int API_EXPORTED libusb_control_transfer(libusb_device_handle *dev_handle,
	uint8_t bmRequestType, uint8_t bRequest, uint16_t wValue, uint16_t wIndex,
	unsigned char *data, uint16_t wLength, unsigned int timeout);
  • 批量传输
c 复制代码
/** \ingroup libusb_syncio
 * 启动批量传输
 * **传输方向在endpoint的"方向位"里表示**
 *
 * 对于批量读,参数length表示"期望读到的数据最大长度", 实际读到的长度保存在transferred参数里
 *
 * 对于批量写, transferred参数表示实际发送出去的数据长度
 *
 * 发生超时错误时,也应该检查transferred参数。
 * libusb会根据硬件的特点把数据拆分为一小段一小段地发送出去,
 * 这意味着发送满某段数据后可能就发生超时错误,需要根据transferred参数判断传输了多少数据。
 *
 * 参数:
 * dev_handle - 设备句柄
 * endpoint - 端点
 * data          - 保存数据的buffer, 可以是in、out数据
 * length  - 对于批量写,它表示要发送的数据长度; 对于批量读,它表示"要读的数据的最大长度"
 * transferred - 输出参数,表示实际传输的数据长度
 * timeout       - 超时时间(单位ms),就是这个函数能等待的最大时间; 0表示一直等待直到成功
 *
 * 返回值:
 * 0 - 成功,根据transferred参数判断传输了多少长度的数据
 * LIBUSB_ERROR_TIMEOUT - 超时, 根据transferred参数判断传输了多少长度的数据
 * LIBUSB_ERROR_PIPE - 端点错误,端点被挂起了
 * LIBUSB_ERROR_OVERFLOW - 溢出,设备提供的数据太多了
 * LIBUSB_ERROR_NO_DEVICE - 设备未连接
 * LIBUSB_ERROR_BUSY - 如果这个函数时在事件处理上下文(event handling context)里则返回这个错误
 * LIBUSB_ERROR_INVALID_PARAM - 传输的字节超过OS或硬件的支持
 * 其他LIBUSB_ERROR错误码
 */
int API_EXPORTED libusb_bulk_transfer(libusb_device_handle *dev_handle,
	unsigned char endpoint, unsigned char *data, int length,
	int *transferred, unsigned int timeout);

上图endpoint参数bit7为0,表示是一个输出端点。

  • 中断传输
c 复制代码
/** \ingroup libusb_syncio
 * 启动中断传输
 * 传输方向在endpoint的"方向位"里表示
 *
 * 对于中断读,参数length表示"期望读到的数据最大长度", 实际读到的长度保存在transferred参数里
 *
 * 对于中断写, transferred参数表示实际发送出去的数据长度,不一定能发送完全部数据。
 *
 * 发生超时错误时,也应该检查transferred参数。
 * libusb会根据硬件的特点把数据拆分为一小段一小段地发送出去,
 * 这意味着发送满某段数据后可能就发生超时错误,需要根据transferred参数判断传输了多少数据。
 *
 * 参数:
 * dev_handle - 设备句柄
 * endpoint - 端点
 * data          - 保存数据的buffer, 可以是in、out数据
 * length  - 对于批量写,它表示要发送的数据长度; 对于批量读,它表示"要读的数据的最大长度"
 * transferred - 输出参数,表示实际传输的数据长度
 * timeout       - 超时时间(单位ms),就是这个函数能等待的最大时间; 0表示一直等待直到成功
 *
 * 返回值:
 * 0 - 成功,根据transferred参数判断传输了多少长度的数据
 * LIBUSB_ERROR_TIMEOUT - 超时, 根据transferred参数判断传输了多少长度的数据
 * LIBUSB_ERROR_PIPE - 端点错误,端点被挂起了
 * LIBUSB_ERROR_OVERFLOW - 溢出,设备提供的数据太多了
 * LIBUSB_ERROR_NO_DEVICE - 设备未连接
 * LIBUSB_ERROR_BUSY - 如果这个函数时在事件处理上下文(event handling context)里则返回这个错误
 * LIBUSB_ERROR_INVALID_PARAM - 传输的字节超过OS或硬件的支持
 * 其他LIBUSB_ERROR错误码
 */
int API_EXPORTED libusb_interrupt_transfer(libusb_device_handle *dev_handle,
	unsigned char endpoint, unsigned char *data, int length,
	int *transferred, unsigned int timeout);

8.异步传输函数:

使用libusb的异步函数时,有如下步骤:

  • 分配:分配一个libusb_transfer结构体
  • 填充:填充libusb_transfer结构体,比如想访问哪个endpoint、数据buffer、长度等等
  • 提交:提交libusb_transfer结构体启动传输
  • 处理事件:检查传输的结果,调用libusb_transfer结构体的回调函数
  • 释放:释放资源,比如释放libusb_transfer结构体

9.分配transfer结构体

c 复制代码
/** \ingroup libusb_asyncio
 * 分配一个libusb_transfer结构体,
 * 如果iso_packets不为0,还会分配iso_packets个libusb_iso_packet_descriptor结构体
 * 使用完毕后需要调用libusb_free_transfer()函数释放掉
 *
 * 对于控制传输、批量传输、中断传输,iso_packets参数需要设置为0
 *
 * 对于实时传输,需要指定iso_packets参数,
 * 这个函数会一起分配iso_packets个libusb_iso_packet_descriptor结构体。
 * 这个函数返回的libusb_transfer结构体并未初始化,
 * 你还需要初始化它的这些成员:
 *   libusb_transfer::num_iso_packets
 *   libusb_transfer::type
 *
 * 你可以指定iso_packets参数,意图给实时传输分配结构体,
 * 但是你可以把这个结构题用于其他类型的传输,
 * 在这种情况下,只要确保num_iso_packets为0就可以。
 *
 * 参数:
 * iso_packets - 分配多少个isochronous packet descriptors to allocate
 *
 * 返回值:
 * 返回一个libusb_transfer结构体或NULL
 */
DEFAULT_VISIBILITY
struct libusb_transfer * LIBUSB_CALL libusb_alloc_transfer(
	int iso_packets);

填充控制传输

c 复制代码
/** \ingroup libusb_asyncio
 * 构造控制传输结构体
 *
 * 如果你传入buffer参数,那么buffer的前面8字节会被当做"control setup packet"来解析,
 * buffer的最后2字节表示wLength,它也会被用来设置libusb_transfer::length
 * 所以,建议使用流程如下:
 * 1. 分配buffer,这个buffer的前面8字节对应"control setup packet",后面的空间可以用来保存其他数据
 * 2. 设置"control setup packet",通过调用libusb_fill_control_setup()函数来设置
 * 3. 如果是要把数据发送个设备,把要发送的数据放在buffer的后面(从buffer[8]开始放)
 * 4. 调用libusb_fill_bulk_transfer
 * 5. 提交传输: 调用libusb_submit_transfer()
 *
 * 也可以让buffer参数为NULL,
 * 这种情况下libusb_transfer::length就不会被设置,
 * 需要手工去设置ibusb_transfer::buffer、ibusb_transfer::length
 *
 * 参数:
 * transfer - 要设置的libusb_transfer结构体
 * dev_handle - 设备句柄
 * buffer - 数据buffer,如果不是NULL的话,它前面8直接会被当做"control setup packet"来处理,
 *          也会从buffer[6], buffer[7]把length提取出来,用来设置libusb_transfer::length
 *           这个buffer必须是2字节对齐
 * callback - 传输完成时的回调函数
 * user_data - 传给回调函数的参数
 * timeout - 超时时间(单位: ms)
 */
static inline void libusb_fill_control_transfer(
	struct libusb_transfer *transfer, libusb_device_handle *dev_handle,
	unsigned char *buffer, libusb_transfer_cb_fn callback, void *user_data,
	unsigned int timeout);

填充批量传输

c 复制代码
/** \ingroup libusb_asyncio
 * 构造批量传输结构体
 *
 * 参数:
 * transfer - 要设置的libusb_transfer结构体
 * dev_handle - 设备句柄
 * endpoint - 端点
 * buffer - 数据buffer
 * length - buffer的数据长度
 * callback - 传输完成时的回调函数
 * user_data - 传给回调函数的参数
 * timeout - 超时时间(单位: ms)
 */
static inline void libusb_fill_bulk_transfer(struct libusb_transfer *transfer,
	libusb_device_handle *dev_handle, unsigned char endpoint,
	unsigned char *buffer, int length, libusb_transfer_cb_fn callback,
	void *user_data, unsigned int timeout);

填充中断传输

c 复制代码
/** \ingroup libusb_asyncio
 * 构造中断传输结构体
 *
 * 参数:
 * transfer - 要设置的libusb_transfer结构体
 * dev_handle - 设备句柄
 * endpoint - 端点
 * buffer - 数据buffer
 * length - buffer的数据长度
 * callback - 传输完成时的回调函数
 * user_data - 传给回调函数的参数
 * timeout - 超时时间(单位: ms)
 */
static inline void libusb_fill_interrupt_transfer(
	struct libusb_transfer *transfer, libusb_device_handle *dev_handle,
	unsigned char endpoint, unsigned char *buffer, int length,
	libusb_transfer_cb_fn callback, void *user_data, unsigned int timeout);

填充实时传输

c 复制代码
/** \ingroup libusb_asyncio
 * 构造实时传输结构体
 *
 * 参数:
 * transfer - 要设置的libusb_transfer结构体
 * dev_handle - 设备句柄
 * endpoint - 端点
 * buffer - 数据buffer
 * length - buffer的数据长度
 * num_iso_packets - 实时传输包的个数
 * callback - 传输完成时的回调函数
 * user_data - 传给回调函数的参数
 * timeout - 超时时间(单位: ms)
 */
static inline void libusb_fill_iso_transfer(struct libusb_transfer *transfer,
	libusb_device_handle *dev_handle, unsigned char endpoint,
	unsigned char *buffer, int length, int num_iso_packets,
	libusb_transfer_cb_fn callback, void *user_data, unsigned int timeout);

10.提交传输

c 复制代码
/** \ingroup libusb_asyncio
 * 提交传输Submit,这个函数会启动传输,然后立刻返回
 *
 * 参数:
 * transfer - 要传输的libusb_transfer结构体
 *
 * 返回值:
 * 0 - 成功
 * LIBUSB_ERROR_NO_DEVICE - 设备未连接
 * LIBUSB_ERROR_BUSY - 这个传输已经提交过了
 * LIBUSB_ERROR_NOT_SUPPORTED - 不支持这个传输
 * LIBUSB_ERROR_INVALID_PARAM - 传输的字节超过OS或硬件的支持
 * 其他LIBUSB_ERROR错误码
 */
int API_EXPORTED libusb_submit_transfer(struct libusb_transfer *transfer);

11.处理事件

c 复制代码
int API_EXPORTED libusb_handle_events_timeout_completed(libusb_context *ctx,struct timeval *tv, int *completed);

completed参数可以避免多线程间的竞争关系,本函数获得"event handling lock"后,会判断completed指向的数值,如果这个数值非0(表示别的线程已经处理了、已经completed了),则本函数会立刻返回(既然都completed了,当然无需再处理)。timeout参数可以设置超时时间。

12.释放transfer结构体

传输完毕,需要释放libusb_transfer结构体,如果要重复利用这个结构体则无需释放。

c 复制代码
/** \ingroup libusb_asyncio
 * 释放libusb_transfer结构体
 * 前面使用libusb_alloc_transfer()分配的结构体,要使用本函数来释放。
 *
 * 如果libusb_transfer::flags的LIBUSB_TRANSFER_FREE_BUFFER位非0,
 * 那么会使用free()函数释放ibusb_transfer::buffer
 * 
 * 不能使用本函数释放一个活动的传输结构体(active, 已经提交尚未结束)
 * 
 */
void API_EXPORTED libusb_free_transfer(struct libusb_transfer *transfer);

USB设备驱动模型

BUS/DRV/DEV模型

"USB接口(usb interface)"是逻辑上的USB设备,我们编写的usb_driver驱动程序,支持的是"USB接口":

  • USB控制器或Hub识别出USB设备后,会创建、注册usb_deive
  • usb_device被"drivers\usb\core\generic.c"驱动认领后,会选择、设置某个配置
  • 这个配置下面的接口,都会分配、设置、注册一个usb_interface
  • 左边的usb_driver和右边的usb_interface如果匹配,则调用usb_driver.probe

接口函数

在USB设备驱动程序中,能使用的USB函数都在这个头文件里:include\linux\usb.h。

pipe

操作系统软件抽象句柄,用来代表主机与某端点的通信通路。

使用这些接口函数的主要目的是传输数据,传输数据的对象是USB设备里的某个endpoint,这被称为pipe:

c 复制代码
/* Create various pipes... */
#define usb_sndctrlpipe(dev, endpoint)	\
	((PIPE_CONTROL << 30) | __create_pipe(dev, endpoint))
#define usb_rcvctrlpipe(dev, endpoint)	\
	((PIPE_CONTROL << 30) | __create_pipe(dev, endpoint) | USB_DIR_IN)
#define usb_sndisocpipe(dev, endpoint)	\
	((PIPE_ISOCHRONOUS << 30) | __create_pipe(dev, endpoint))
#define usb_rcvisocpipe(dev, endpoint)	\
	((PIPE_ISOCHRONOUS << 30) | __create_pipe(dev, endpoint) | USB_DIR_IN)
#define usb_sndbulkpipe(dev, endpoint)	\
	((PIPE_BULK << 30) | __create_pipe(dev, endpoint))
#define usb_rcvbulkpipe(dev, endpoint)	\
	((PIPE_BULK << 30) | __create_pipe(dev, endpoint) | USB_DIR_IN)
#define usb_sndintpipe(dev, endpoint)	\
	((PIPE_INTERRUPT << 30) | __create_pipe(dev, endpoint))
#define usb_rcvintpipe(dev, endpoint)	\
	((PIPE_INTERRUPT << 30) | __create_pipe(dev, endpoint) | USB_DIR_IN)

发送和接收的方向是在host的角度来看的。

同步传输函数

对于控制传输、批量传输、中断传输,有3个同步函数可以用来直接发起传输。这些函数内部会创建、填充、提交 一个URB("usb request block") ,并等待它完成或超时,与libusb用法相似。

函数如下:

c 复制代码
//控制传输
/*
host与哪个设备dev的哪个管道通信pipe;数据保存在data,数据大小size,可以设置一个超时事件timeout。

令牌包(Token Packet):USB 底层包,包含 PID (SETUP/IN/OUT)(包类型标识符)、设备地址、端点号、CRC5校验,由pipe、dev参数解析生成;
SETUP 令牌包:控制传输 Setup 阶段专用,OUT 令牌包:主机→设备写数据,IN 令牌包:设备→主机读数据。

Setup 数据包(Setup Data,8 字节):紧跟 SETUP 令牌包后的 DATA0 载荷,由函数request/requesttype/value/index/size五个参数直接组装,对应标准struct usb_ctrlrequest,这是控制传输的命令载体。
*/
int usb_control_msg(struct usb_device *dev, unsigned int pipe, __u8 request,
		    __u8 requesttype, __u16 value, __u16 index, void *data,
		    __u16 size, int timeout);
//批量传输
int usb_bulk_msg(struct usb_device *usb_dev, unsigned int pipe,
		 void *data, int len, int *actual_length, int timeout);
//中断传输
int usb_interrupt_msg(struct usb_device *usb_dev, unsigned int pipe,
		      void *data, int len, int *actual_length, int timeout);

异步传输函数

使用URB进行传输时,它是异步方式:需要先分配、构造、提交一个URB("usb request block"),当传输完成后,它的回调函数被调用。

关键就在于需要填充URB:

  • dev:跟谁传输数据
  • pipe:跟哪个pipe传输数据
  • buffer:里面存有要发送的数据,或者用来接收要读取的数据
  • 数据长度
  • 回调函数(complete_fn)
    填充URB完成后系统调用回调函数

1.分配和释放URB

c 复制代码
struct urb *usb_alloc_urb(int iso_packets, gfp_t mem_flags);
void usb_free_urb(struct urb *urb);

2.分配/释放DMA Buffer

发起USB传输时,数据保存在buffer里。这个buffer可以是一般的buffer,也可以是DMA Buffer。

对于一般的buffer,在提交URB时会临时分配一个DMA Buffer:

  • 发送数据时:函数内部会先从一般buffer中把数据复制到DMA Buffer,在提交给USB控制器
  • 读取数据时:USB控制器先把数据传到DMA Buffer,函数内部在把DMA Buffer的数据复制到一般buffer
  • 中间增加了一次数据的拷贝,效率低

我们可以直接使用DMA Buffer,函数原型如下:

c 复制代码
void *usb_alloc_coherent(struct usb_device *dev, size_t size, gfp_t mem_flags,
			 dma_addr_t *dma);
void usb_free_coherent(struct usb_device *dev, size_t size, void *addr,
		       dma_addr_t dma);

虚拟地址所映射物的理地址可能是不连续的,但是USB Controller是硬件,识别离散的物理地址有难度,所有在USB Device传输数据时要么给数据直接分配一块物理上连续的buffer(高效)。要么先将数据发从离散的物理buffer 拷贝到一块物理上连续的buffer,再由 USB Controller 读取,数据方向反过来同样需要进过这个物理连续buffer。

3.填充URB

对于控制传输、批量传输、中断传输,分别有如下函数:

c 复制代码
static inline 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);
c 复制代码
static inline 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);
c 复制代码
static inline 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,
				    int interval);

如果URB使用DMA Buffer,那么还需要设置一个flag表明这点:

c 复制代码
urb->transfer_dma = DMA address of buffer; // usb_alloc_coherent的输出参数,将物理连续的buffer地址告知URB
urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP; //设置标记位,告知urb之后无需做临时buffer不需要做数据拷贝

4.提交URB

构造好URB后,需要提交到USB系统里,才能启动传输。

c 复制代码
int usb_submit_urb(struct urb *urb, gfp_t mem_flags);

5.取消URB

已经提交的URB,可以取消它,有2个函数:

  • usb_kill_urb:这是一个同步函数,它会等待URB结束
  • usb_unlink_urb:这是一个异步函数,它不会等待URB结束,USB控制器驱动会调用它的回调函数
c 复制代码
void usb_kill_urb(struct urb *urb);
c 复制代码
int usb_unlink_urb(struct urb *urb);

USB鼠标驱动程序示例

1.向USB总线注册一个USB接口(interface)驱动

c 复制代码
// linux-2.6.22.6/drivers/hid/usbhid/usbmouse.c

static struct usb_driver usb_mouse_driver = {
    .name       = "usbmouse",
    .probe      = usb_mouse_probe,
    .disconnect = usb_mouse_disconnect,
    .id_table   = usb_mouse_id_table,
};

static int __init usb_mouse_init(void)
{
    int retval = usb_register(&usb_mouse_driver);
    if (retval == 0)
        info(DRIVER_VERSION ":" DRIVER_DESC);
    return retval;
}

2.USB接口(interface)设备的创建

当一个USB 鼠标(Confugration )设备插入后,主机USB控制器检测到后,触发USB设备集线器中的"中断"处理函数hub_irq。在hub_irq中会获取USB鼠标设备的设备描述符,根据设备描述符创建USB接口设备,从而和这边的USB接口(Interface)驱动匹配,调用其probe函数,通过USB总线驱动程序(USB Core和USB HCD)和USB鼠标设备建立联系,进而操作(读写控制)该设备。

c 复制代码
hub_irq
    kick_khubd // 唤醒hub_thread线程
        hub_thread
            hub_events // 处理USB设备插入事件
                hub_port_connect_change

                    udev = usb_alloc_dev(hdev, hdev->bus, port1);
                                dev->dev.bus = &usb_bus_type;

                    choose_address(udev); // 给新设备分配编号(地址)                                       
                    hub_port_init   // usb 1-1: new full speed USB device using s3c2410-ohci and address 3

                        hub_set_address  // 把编号(地址)告诉USB设备

                        usb_get_device_descriptor(udev, 8); // 获取设备描述符
                        retval = usb_get_device_descriptor(udev, USB_DT_DEVICE_SIZE);

                        usb_new_device(udev)   
                            err = usb_get_configuration(udev); // 把所有的描述符都读出来,并解析
                            usb_parse_configuration

                            device_add  // 把device放入usb_bus_type的dev链表, 
                                        // 从usb_bus_type的driver链表里取出usb_driver,
                                        // 把usb_interface和usb_driver的id_table比较
                                        // 如果能匹配,调用usb_driver的probe

3.USB接口驱动和接口设备的匹配

USB设备插入后根据获取到的设备描述符所创建的USB 接口设备和开发的USB接口驱动匹配: 对于设备: 将获取到的USB设备描述符信息保存在其id_table中。 对于驱动: 驱动的id_table中存放,期望该驱动适用的USB设备。

c 复制代码
// linux-2.6.22.6/drivers/hid/usbhid/usbmouse.c

static struct usb_device_id usb_mouse_id_table [] = {  
    /*
    匹配 HID 设备
    USB 设备中有一大类就是 HID 设备,即 Human Interface Devices,人机接口设备。
    这类设备包括鼠标、键盘等,主要用于人与计算机进行交互。 
    它是 USB 协议最早支持的一种设备类。 
    HID 设备可以作为低速、全速、高速设备用。
    由于 HID 设备要求用户输入能得到及时响应,故其传输方式通常采用中断方式。
    */
    { USB_INTERFACE_INFO(USB_INTERFACE_CLASS_HID, USB_INTERFACE_SUBCLASS_BOOT,
        USB_INTERFACE_PROTOCOL_MOUSE) },
    { } /* Terminating entry */
};

匹配成功后调用该驱动的probe函数,和USB总线驱动建立联系。

probe函数原型如下:

c 复制代码
int (*probe) (struct usb_interface *intf,
              const struct usb_device_id *id);
  • 第1个参数是"struct usb_interface *"类型,表示匹配到的"USB逻辑设备"。

  • 第2个参数是"struct usb_device_id *"类型,它是usb_driver的id_table中的某项,表示第1个参数就是跟这个usb_device_id匹配的。有必要的话,probe函数里可以从id->driver_info得到驱动相关的一些信息。

  • 在probe函数,一般要记录intf信息,以后发起USB传输时会用到intf信息。

id_table是一个usb_device_id数组,结构体定义如下:

c 复制代码
struct usb_device_id {
	/* which fields to match against? */
	__u16		match_flags;

	/* Used for product specific matches; range is inclusive */
	__u16		idVendor;
	__u16		idProduct;
	__u16		bcdDevice_lo;
	__u16		bcdDevice_hi;

	/* Used for device class matches */
	__u8		bDeviceClass;
	__u8		bDeviceSubClass;
	__u8		bDeviceProtocol;

	/* Used for interface class matches */
	__u8		bInterfaceClass;
	__u8		bInterfaceSubClass;
	__u8		bInterfaceProtocol;

	/* Used for vendor-specific interface matches */
	__u8		bInterfaceNumber;

	/* not matched against */
	kernel_ulong_t	driver_info
		__attribute__((aligned(sizeof(kernel_ulong_t))));
};

4.创建数据传输管道(pipe)

对于USB鼠标设备使用中断传输方式。

c 复制代码
// linux-2.6.22.6/drivers/hid/usbhid/usbmouse.c

struct usb_device *dev = interface_to_usbdev(intf);
struct usb_host_interface *interface;
struct usb_endpoint_descriptor *endpoint;
struct usb_mouse *mouse;
struct input_dev *input_dev;
int pipe, maxp;
int error = -ENOMEM;

interface = intf->cur_altsetting;

if (interface->desc.bNumEndpoints != 1)
    return -ENODEV;

endpoint = &interface->endpoint[0].desc;
if (!usb_endpoint_is_int_in(endpoint))
    return -ENODEV;

// 端点是USB设备数据传输对象
pipe = usb_rcvintpipe(dev, endpoint->bEndpointAddress);
maxp = usb_maxpacket(dev, pipe, usb_pipeout(pipe));

5.分配urb

urb(USB Request Block)是Linux内核中USB驱动实现上的一个数据结构,用于组织每一次的USB设备驱动的数据传输请求。

c 复制代码
mouse->irq = usb_alloc_urb(0, GFP_KERNEL);
if (!mouse->irq)
    goto fail2;

6.urb初始化

usb鼠标使用中断传输方式。接口详情在上一章。

c 复制代码
usb_fill_int_urb(mouse->irq, dev, pipe, mouse->data,
             (maxp > 8 ? 8 : maxp),
             usb_mouse_irq, mouse, endpoint->bInterval);
mouse->irq->transfer_dma = mouse->data_dma;
mouse->irq->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;

7.提交USB请求块

调用usb_submit_urb接口以获取USB设备数据。

c 复制代码
// linux-2.6.22.6/drivers/hid/usbhid/usbmouse.c

static int usb_mouse_open(struct input_dev *dev)
{
    struct usb_mouse *mouse = input_get_drvdata(dev);

    mouse->irq->dev = mouse->usbdev;
    if (usb_submit_urb(mouse->irq, GFP_KERNEL))
        return -EIO;

    return 0;
}

OTG硬件检测电路

1.OTG接口与转换器

OTG是"On The Go"的英文缩写,字面上可以理解为"安上即可用"。USB传输是主从结构,一切USB传输都有Host发起。比如在开发板上可以插入U盘,这时开发板作为USB Host。但是开发板要跟PC通信,开发板就要作为USB Device。开发板要作为USB Host、USB Device两种角色,可以使用OTG插口:它可以根据硬件电路 自动识别自己的角色,切换为USB Host或USB Deivce。OTG接口工作于host模式对外供电,工作于device模式接收电源。

OTG插口有多种形态,常用的有Micro USB、Type C,如下:

1.1 Micro USB

对于Micro USB插座,它有5条引脚:

引脚作用如下表所示:

开发板作为USB Device时跟PC上的USB相连,PC的USB接口只有VBUS、DM、DP、GND,所以开发板的ID引脚跟PC的USB口并无连接,它被板子上的上拉电阻拉高。

开发板作为USB Host时,需要接入一个"OTG转换器",如下图黑色的转换器:

OTG转换器的内部电路很简单:

这个转换器插入开发板的OTG口之后,OTG口上的ID引脚就被拉低,软件转换为USB Host。

1.2 Type C

Type C插座里面有两组完全一样的信号,Type C数据线无论正插、反插,都可以使用:

Type C插座有如下信号(参考:https://blog.csdn.net/qq_37659014/article/details/124479125),在USB2.0协议里我们只关心红框里的信号:

开发板作为USB Device时跟PC上的USB相连,PC的USB接口只有VBUS、DM、DP、GND,所以开发板的CC1、CC2引脚跟PC的USB口并无连接,它被板子上的上拉电阻拉高。

开发板作为USB Host时,需要接入一个"OTG转换器",如下图黑色的转换器:

如果不考虑兼容USB 3.0协议,上述转换器的电路图很简单,把Type C插头里面的CC引脚连接5.1K欧姆电阻到GND即可。如下图所示(参考:https://www.elecfans.com/connector/20180309645002_a.html):

2. OTG接口电路

开发板上的OTG接口需要实现两个功能:

  • 检测ID引脚(使用Type C接口的话是CC1、CC2引脚),引入主控芯片:软件根据它设置USB控制器的角色(Host或Device)

  • 根据ID引脚(或者CC1、CC2)决定VBUS是否输出电源:硬件电路自动实现,OTG接口工作于host模式对外供电,工作于device模式接收电源。

2.1 Micro USB

平时USB_OTG1_ID引脚处于悬空状态,在主控芯片内部ID引脚是有上拉电阻的,Q3 MOS管导通,SY6280AAAC芯片的EN为低电平,OUT端口无输出,既开发板处于Device状态,不会对外供电。

当接入转换线,将ID引脚拉低SY6280AAAC芯片的EN为高电平,电源就可以到VBUS,开发板就可以对外供电。 +

2.2 Type C

如果不考虑兼容USB 3.0协议,可以使用如下精简电路:CC1、CC2作为ID引脚。

如果要兼容USB 3.0协议,则需要加入专用的芯片

U14专用芯片会监测CC1、CC2,USB-OTG接PC和转换器CC1、CC2不同,芯片检测到CC1、CC2发生变化时会产生中断(INT#),这个中断导致专用芯片的驱动程序被调用,在驱动程序中发起i2c传输来分辨CC1、CC2的状态,得到OTG接口应该工作在host或device模式。再通过软件控制PF12引脚来实现是否输出电源。

学习内容来源:

https://zhuanlan.zhihu.com/p/558716468

https://ldd.100ask.net/zh/12_USB/02_1.html