Linux下的USB通信
什么是USB
Usb全称为通用串行总线,即通用串行总线。Usb是一个标准,用于在计算机和外部设备之间传输数据。
什么是USB设备
USB设备是指通过USB接口连接到计算机的外部设备,如键盘、鼠标、打印机、扫描仪、摄像头、U盘、移动硬盘等。
什么是USB驱动
USB驱动是操作系统中用于管理USB设备的软件,它负责识别、配置和控制USB设备。
USB的标准和速率
1996 年,USB1.0 标准发布。理论速度 1.5Mbit/s。(当时一个 3.5 寸的软盘容量在 140M 左右)
1998 年,USB1.1 标准发布。理论速度 12Mbit/s。
2000年,USB2.0标准发布。理论速度达480Mbit/s。
2008年,USB3.0标准,5Gbit/s
2013 年,USB3.1 标准,10Gbit/s,随后又发布了USB3.2标准,20Gbit/s
2019年,USB4.0 标准,40Gbit/s
USB的HOST和SLAVE
USB 设备分为 HOST(主设备)和 SLAVE(从设备),只有当一台 HOST 与一台 SLAVE 连接时才能实现数据的传输。
USBHOST(USB 主设备):
USB HOST是指具有 USB 主机功能的设备。USB 主设备是控制和管理 USB 总线的设备,USB主设备通常是计算机或其他主机设备,如平板电脑、笔记本台式机等。
USB Slave(USB 从设备):
USB Slave 是指具有 USB 从设备功能的设备。USB 从设备是受 USB 主机控制的设备,USB 从设备依赖于 USB 主机设备以进行数据传输和通信。USB 从设备可以是各种外围设备,如键盘、鼠标、U盘等
USB OTG(USB On-The-Go)(2014年开始普及):
USB OTG是指支持 USB OTG 功能的设备(即插即用),USB OTG 允许设备在主设备和从设备之间进行切换,从而能够直接与其他 USB 设备进行通信,无需传统的 USB 主机设备。如通过 OTG 可以实现将摄像机和打印机直接连接。
USB的OTG模式
USB的OTG结构图
USB的OTG模式
USB3_OTGO_VBUSDET 是 OTG 和 Device(SLAVE) 模式检测脚,高有效。
OTG 模式可以设置以下三种模式:
OTG 模式:
根据 ID 脚状态自动切换是 device 模式或 HOST 模式, ID 高为 device, ID拉低为 HOST,处在 device 模式时,还会判断 VBUSDET 脚是否为高,如果为高,才会拉高 DP,开始枚举
Device 模式:
设置为这个模式时,无需ID 脚,只需判断 VBUSDET 脚是否为高,如果为高,才会拉高 DP,开始枚举
HOST 模式:
设置为这个模式时, ID 和 VBUSDET 状态都无需要关心。 (如果产品只需要 HOST 模式,但是由于仅 USB3 OTGO DP/USB3 OTGO DM是系统固件烧写口, 在调试与生产过程都需要用这个口,烧写和adb 调试时,需要设置成device 模式,因此USB3 OTGO VBUSDET 信号也必须接)。在 uboot,默认为 device 模式,进 uboot,后,可根据实际需求配置这三种模式。
USB的拓扑结构
USB扩展器
但是所有从机都必须经过集线器(hub)
才能与主机连接。也就是设备不能直接和主机想连接。
根集线器 : 与主机直接连接的集线器叫做根集线器,用户外接的叫做普通集线器
集线的作用: 集线器是为了扩展更多的接口。但是并不是可以无休止的可以扩展。USB2.0协议中最多扩展七层。每一层所有设备相加不能超过127个(包括集线器)
USB设备如何被识别
在 USB2.0 协议中定义了设备的6中状态,分别是上电状态(Powered)
,默认状态(Default)
,地址状态(Address
),配置状态(Configured)
,连接状态(Attached)
和挂起状态(Suspend)
)。👇
USB2.0下如何判断设备已经插入
在 hub 一侧,数据线 D+和 D-都有一个阻值在 15k 的下拉电阻 Rpd(Pull-down Resistors)。
而在设备端,D+(全速,高速)和 D-(低速)上有一个 1.5k 的上拉电阻 Rpu(Pull-up Resistor )。
当设备插入时,设备会向 D+或 D-上拉电阻 Rpu(Pull-up Resistors)供电,当设备上电后,D+或 D-上拉电阻 Rpu(Pull-up Resistors)会向 hub 的下拉电阻 Rpd(Pull-down Resistors)供电,hub 会检测到这个变化,从而判断设备已经插入。
不同低速和全速设备,USB高速设备
先被识别为全速设备,然后通过 HOST 去检测 DEVICE是不是高速设备,并且和 HOSTDEVICE 两者之间要互相确认,再切换到高速模式的。在高速模式下是电流传输模式,要把 D+上的上拉电阳断开
USB描述符
USB 描述符主要有设备描述符(Device Descriptor)
,配置描述符(Configuration Descriptor)
,接口描述符(lnterface Descriptor)
,端点描述符(Endpoint Descriptor)
。
这些描述符共同构建了 USB 设备的特征和功能,通过读取这些描述符,系统可以配置驱动和参数,使得操作系统可以正确识别设备和通信。
设备描述符:包含设备的基本信息,比如设备的厂商ID,产品ID等。设备描述符是设备连接到主机时第一个被请求和返回的信息。他提供了设备的基本特征。
配置描述符:描述设备支持的不同配置。
接口描述符:描述了配置中的一个接口。
端点描述符:描述接口中的一个端点。
端点是数据在设备和主机之间传输的终点。一个具体的端点只能属于四种传输模式中的一种。
注意:
一个USB 设备有1个设备描述符;
一个 USB 设备有1个或多个配置描述符;
一个 USB 配置有1个或多个接口描述符;
一个 USB接口有0个或多个端点描述符(不包括端点 0)
USB的传输模式
USB 的四种传输模式有控制模式,实时模式,中断模式,批量式。
- 批量传输:一般用到数据量比较大,对实时性要求不高的场景,比如U盘。
- 中断传输:和硬件中断不太一样,这里的中断指的是主机在一定的时间间隔内进行一次传
输,主要用在少量的,不可以预测的数据,比如鼠标。 - 实时传输:一般用到数据量大,对实时性要求高,但是可以允许有少量数据错误的场景,
比如音视频传输。 - 控制传输:主要进行查询,配置和给 USB 设备发送通用的命令,控制传输是双向传输,数
据量通常较小。
USB数据格式
USB 通信数据格式由域,包,事务,传输组成。多个域组成包,多个包组成事务,多个事务组成传输。所以域是 USB 数据最小的单位
域: 域可分为七个类型,分别是同步域,地址域,端点域,号域,标识域,数据域,校验域。这七种不同类型的域由1个或者多个可以组成包。
包: 包分为令牌包(Token)、数据包(Data)、握手包(Handshake)和特殊包(Special),不同的包的域结构不同。几种包在一起组合又组合成了事务。
事务: 分由令牌包、数据包、握手包组成。其中数据包和握手包是可选的。事务是最基本的单位,所以对于我们开发人员来说关注事务和传输就可以了。
USB枚举
- 主机发送 获取设备描述符的请求,设备返回设备描述符。(主机使用默认的地址0和端点0)
- 主机给设备分配一个唯一的地址
- 主机以新的地址再次获取设备描述符,再以新的地址获取配置描述符
- 根据获得的描述符信息,主机给设备分配配置,并使设备进入配置状态
USB的应用开发
USB的libusb库
libusb,是一个使用C语言编写的开源库,通过这个库可以方便的编写应用程序操作 usb,设备无需了解底层的 USB 协议。
特点: 可移植性好:跨平台 APl,在 Linux、macOs、Windows 等均可使用。
简单易用: libusb 提供了一套简单的 AP1,支持所有版本的 USB 协议,从 USB1.0 到 USB3.1,并且 API接口保持不变。方便开发。
交叉编译移植
- 下载 libusb 源码
wget https://github.com/libusb/libusb/releases/download/v1.0.27/libusb-1.0.27.tar.bz2
tar -vxf libusb-1.0.27.tar.bz2
- 交叉编译
./configure --host=arm-rockchip830-linux-uclibcgnueabihf --prefix=$(pwd)/output CC=arm-rockchip830-linux-uclibcgnueabihf-gcc CXX=arm-rockchip830-linux-uclibcgnueabihf-g++ --disable-udev
make
make install
libUSB的应用
libusb 实现 lsusb 命令
列出当前系统上所有 usb 设备的信息。
#include <stdio.h>
#include "libusb.h"
int main(int argc, char *argv[]){
int ret = 0 ;
ret = libusb_init(NULL); // Initialize the libusb library
if(ret < 0){
fprintf(stderr, "Failed to initialize libusb\n");
return ret;
}
libusb_device **devs = NULL;
ssize_t cnt = libusb_get_device_list(NULL, &devs); // Get the list of devices , return the number of devices
if(cnt < 0){
fprintf(stderr, "Failed to get device list\n");
libusb_exit(NULL);
return (int)cnt;
}
printf("Found %zd devices\n", cnt);
for(int i = 0; i < cnt; i++){
libusb_device *dev = devs[i];
struct libusb_device_descriptor desc;
int r = libusb_get_device_descriptor(dev, &desc); // Get the device descriptor for the every dev
if(r < 0){
fprintf(stderr, "Failed to get device descriptor\n");
return r;
}
printf("Device %d: Vendor ID: %04x, Product ID: %04x\n", i, desc.idVendor, desc.idProduct);
}
libusb_free_device_list(devs, 1); // Free the device list
libusb_exit(NULL); // Exit the libusb library
return 0;
}
libusb 实现键盘接收的应用程序
同步模式
一个USB设备有一个设备描述符,一个或多个配置描述符,每个配置描述符包含一个或多个接口描述符,每个接口描述符包含一个或多个端点描述符。
#include <stdio.h>
#include "libusb.h"
int main(int argc, char *argv[]){
int ret = 0 ;
int exit_flag = 0;
int keyboard_endpoint = 0;
unsigned char buffer[8];
int actual_length = 0;
libusb_device *dev = NULL;
const struct libusb_interface_descriptor *interface_desc = NULL; // 接口描述符
const struct libusb_endpoint_descriptor *endpoint_desc = NULL; // 端点描述符
struct libusb_device_handle *dev_handle = NULL; // 设备句柄
ret = libusb_init(NULL); // Initialize the libusb library
if(ret < 0){
fprintf(stderr, "Failed to initialize libusb\n");
return ret;
}
libusb_device **devs = NULL;
ssize_t cnt = libusb_get_device_list(NULL, &devs); // Get the list of devices , return the number of devices
if(cnt < 0){
fprintf(stderr, "Failed to get device list\n");
libusb_exit(NULL);
return (int)cnt;
}
printf("Found %zd devices\n", cnt);
struct libusb_config_descriptor *config_desc = NULL;
for(int i = 0; i < cnt; i++){
dev = devs[i];
// usb device 的配置描述符有多个,0 是第一个配置描述符 , 键盘设备比较单一,只有一个配置描述符
ret = libusb_get_config_descriptor(devs[i], 0, &config_desc);// 0 is the config index ,config_desc is the config descriptor
if(ret < 0){
// Get the config descriptor for the every dev
fprintf(stderr, "Failed to get config descriptor\n");
return ret;
}
// 配置描述符中包含多个接口描述符,遍历配置描述符中的接口描述符
for(int k = 0; k < config_desc->bNumInterfaces; k++){
interface_desc = &config_desc->interface[k].altsetting[0];
printf("Interface %d: Class: %02x, Subclass: %02x, Protocol: %02x\n", k, interface_desc->bInterfaceClass, interface_desc->bInterfaceSubClass, interface_desc->bInterfaceProtocol);
// 判断设备是否是人机交互设备 , 键盘设备是 HID 设备 ,bInterfaceProtocol 来判断是键盘设备(1),还是鼠标设备(2)
if(interface_desc->bInterfaceClass == LIBUSB_CLASS_HID || interface_desc->bInterfaceProtocol == 1){
for(int j = 0; j < interface_desc->bNumEndpoints; j++){
endpoint_desc = &interface_desc->endpoint[j];
printf("Endpoint %d: Address: %02x, Type: %02x, Transfer Type: %02x, Max Packet Size: %02x\n", j, endpoint_desc->bEndpointAddress, endpoint_desc->bmAttributes, endpoint_desc->bDescriptorType, endpoint_desc->wMaxPacketSize);
// 判断端点是否是中断传输,或是是输入端点
if((interface_desc->endpoint[j].bmAttributes & 3 ) == LIBUSB_TRANSFER_TYPE_INTERRUPT || (interface_desc->endpoint[j].bmAttributes & 0x80 ) == LIBUSB_ENDPOINT_IN){
//找到键盘设备的端点描述符后将其保存,用于后续传输
keyboard_endpoint = interface_desc->endpoint[j].bEndpointAddress;
exit_flag = 1;
break;
}
}
}
if(exit_flag == 1){
break;
}
}
if(exit_flag == 1){
break;
}
}
printf("keyboard_endpoint: %02x\n", keyboard_endpoint);
// 打开设备
ret = libusb_open(dev, &dev_handle);
if(ret < 0){
fprintf(stderr, "Failed to open device\n");
return ret;
}
// 设置自动分离内核驱动,剥离内核驱动对改设备的控制,否则在传输数据时会报错
ret = libusb_set_auto_detach_kernel_driver(dev_handle,