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引脚来实现是否输出电源。