1、简介
学习usb驱动的难度比i2c、spi等又上升了一个台阶。因为它包含了太多内容。需要设置设备、配置、接口、端点四类描述符(usb_device_descriptor、usb_config_descriptor、usb_interface_descriptor、usb_endpoint_descriptor),它们之间的关系可以简单概括为下图。每个设备有不同的工作模式,不同的工作模式就可以理解为对应不同的配置。每种工作配置下,又可以又很多种usb接口设备(鼠标、键盘等等)同时工作。每个接口通过其下面的不同端点与主设备通信。端点又分为输入端点、输出端点、控制端点等等。
每类结构体下又有很多字段需要配置,以上还仅是usb驱动的一部分内容。USB驱动内容多到难以入手,看别人的驱动源码也不知所云。因此从应用层入手usb驱动未尝不是个选择。
2、libusb
libusb是一个开源的、跨平台的库,用于与USB设备进行交互。它提供了对USB硬件的底层访问,允许用户空间的应用程序与USB设备通信,而不需要编写内核驱动程序。
libusb库的安装方式很简单:
sudo apt-get install libusb-1.0-0-dev sudo apt-get install libusb-dev
使用时引用#include<libusb-1.0/libusb.h>头文件即可 。编译姿势(这里我是在内核为linux-5.10.66的开发板上编译):
aarch64-linux-gnu-gcc -o usb_test usb_test.c -lusb-1.0
再从网上粘一份代码下来测试(07_使用libusb读取鼠标数据 — Linux设备驱动开发教程中心 (100ask.net)):
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <libusb-1.0/libusb.h>
int main(int argc, char **argv)
{
int err;
libusb_device *dev, **devs;
int num_devices;
int endpoint;
int interface_num;
int found = 0;
int transferred;
int count = 0;
unsigned char buffer[16];
struct libusb_config_descriptor *config_desc;
struct libusb_device_handle *dev_handle = NULL;
/* libusb_init */
err = libusb_init(NULL);
if (err < 0) {
fprintf(stderr, "failed to initialise libusb %d - %s\n", err, libusb_strerror(err));
exit(1);
}
/* get device list */
if ((num_devices = libusb_get_device_list(NULL, &devs)) < 0) {
fprintf(stderr, "libusb_get_device_list() failed\n");
libusb_exit(NULL);
exit(1);
}
fprintf(stdout, "libusb_get_device_list() ok\n");
/* for each device, get config descriptor */
for (int i = 0; i < num_devices; i++) {
dev = devs[i];
/* parse interface descriptor, find usb mouse */
err = libusb_get_config_descriptor(dev, 0, &config_desc);
if (err) {
fprintf(stderr, "could not get configuration descriptor\n");
continue;
}
fprintf(stdout, "libusb_get_config_descriptor() ok\n");
for (int interface = 0; interface < config_desc->bNumInterfaces; interface++) {
const struct libusb_interface_descriptor *intf_desc = &config_desc->interface[interface].altsetting[0];
interface_num = intf_desc->bInterfaceNumber;
if (intf_desc->bInterfaceClass != 3 || intf_desc->bInterfaceProtocol != 2)
continue;
else
{
/* 找到了USB鼠标 */
fprintf(stdout, "find usb mouse ok\n");
for (int ep = 0; ep < intf_desc->bNumEndpoints; ep++)
{
if ((intf_desc->endpoint[ep].bmAttributes & 3) == LIBUSB_TRANSFER_TYPE_INTERRUPT ||
(intf_desc->endpoint[ep].bEndpointAddress & 0x80) == LIBUSB_ENDPOINT_IN) {
/* 找到了输入的中断端点 */
fprintf(stdout, "find in int endpoint\n");
endpoint = intf_desc->endpoint[ep].bEndpointAddress;
found = 1;
break;
}
}
}
if (found)
break;
}
libusb_free_config_descriptor(config_desc);
if (found)
break;
}
if (!found)
{
/* free device list */
libusb_free_device_list(devs, 1);
libusb_exit(NULL);
exit(1);
}
if (found)
{
/* libusb_open */
err = libusb_open(dev, &dev_handle);
if (err)
{
fprintf(stderr, "failed to open usb mouse\n");
exit(1);
}
fprintf(stdout, "libusb_open ok\n");
}
/* free device list */
libusb_free_device_list(devs, 1);
/* claim interface */
libusb_set_auto_detach_kernel_driver(dev_handle, 1);
err = libusb_claim_interface(dev_handle, interface_num);
if (err)
{
fprintf(stderr, "failed to libusb_claim_interface\n");
exit(1);
}
fprintf(stdout, "libusb_claim_interface ok\n");
/* libusb_interrupt_transfer */
while (1)
{
err = libusb_interrupt_transfer(dev_handle, endpoint, buffer, 16, &transferred, 5000);
if (!err) {
/* parser data */
printf("%04d datas: ", count++);
for (int i = 0; i < transferred; i++)
{
printf("%02x ", buffer[i]);
}
printf("\n");
} else if (err == LIBUSB_ERROR_TIMEOUT){
fprintf(stderr, "libusb_interrupt_transfer timout\n");
} else {
fprintf(stderr, "libusb_interrupt_transfer err : %d\n", err);
//exit(1);
}
}
/* libusb_close */
libusb_release_interface(dev_handle, interface_num);
libusb_close(dev_handle);
libusb_exit(NULL);
}
能够正常接收鼠标发上来的数据:
3、代码解读
下面我将用窝浅薄的知识对代码进行解读
首先是每个变量各代表什么意思。
int err; //函数返回值
libusb_device *dev, **devs; /*dev是单指针,用于引用单个USB设备
devs是双指针,用于引用多个USB设备*/
int num_devices; //存储libusb_get_device_list函数返回的设备数量,即系统中连接了多少个USB设备
int endpoint; //存储USB设备中端点的地址,每个端点都有唯一一个地址
int interface_num; //存储USB设备中接口的编号,每个接口都有唯一一个编号
int found = 0; //判断有没有找到一个鼠标设备
int transferred; //存储实际传输的数据量,以字节为单位
int count = 0; //统计接收的消息数量
unsigned char buffer[16]; //存储USB设备上发的数据
struct libusb_config_descriptor *config_desc; //存储USB设备的配置信息
struct libusb_device_handle *dev_handle = NULL; //存储打开的USB设备的句柄
接下来使用libusb_init(NULL)初始化libusb,创建供libusb使用的“上下文”。这个“上下文”包含了libusb库内部使用的全局数据,用于管理库的内部状态,包括设备列表、事件处理等。参数若为“NULL”表示由libusb库创建一个默认的“上下文”。
再通过libusb_get_device_list(NULL,&devs)获得连接到主机的usb设备。其包含两个参数,第一个参数是指向“上下文”的指针,此处为NULL表示使用默认的“上下文”。第二个参数是指向libusb_device的指针地址,用于存储libusb库返回的设备列表。
/* libusb_init */
err = libusb_init(NULL);//初始化,创建一个供libusb使用的默认“上下文”
if (err < 0) {
fprintf(stderr, "failed to initialise libusb %d - %s\n", err, libusb_strerror(err));
exit(1);
}
/* get device list */
if ((num_devices = libusb_get_device_list(NULL, &devs)) < 0) { //获取连接到计算机的USB设备列表
fprintf(stderr, "libusb_get_device_list() failed\n");
libusb_exit(NULL);
exit(1);
}
fprintf(stdout, "libusb_get_device_list() ok\n");
获得usb设备列表后,就在这个列表当中寻找有无“鼠标设备”。 首先使用libusb_get_config_descriptor(dev, 0, &config_desc)获取USB设备的配置描述符。第一个参数dev为指向libusb_device结构的指针,表示要获取配置描述符的USB设备。第二个参数指定要获取哪个配置的描述符,大多数设备通常只有一个配置,因此这个参数通常为0。第三个参数指向libusb_config_descriptor结构,用于存储获取到的配置描述符。
获取设备的配置描述符后,使用config_desc->bNumInterfaces遍历配置下的所有接口。通过&config_desc->interface[interface].altsetting[0]获得接口描述符。通过判断bInterfaceClass值是否等于3,以及bInterfaceProtocol是否等于2来判断此接口是否为鼠标。
/* for each device, get config descriptor *///寻找有无鼠标设备
for (int i = 0; i < num_devices; i++) {
dev = devs[i];
/* parse interface descriptor, find usb mouse */
err = libusb_get_config_descriptor(dev, 0, &config_desc); //获取设备配置描述符,其中包含了设备的接口、端点和其他配置信息
if (err) {
fprintf(stderr, "could not get configuration descriptor\n");
continue;
}
fprintf(stdout, "libusb_get_config_descriptor() ok\n");
for (int interface = 0; interface < config_desc->bNumInterfaces; interface++) { //遍历配置描述符中的所有接口描述符,寻找符合USB鼠标特征的接口
const struct libusb_interface_descriptor *intf_desc = &config_desc->interface[interface].altsetting[0];//获得接口描述符
interface_num = intf_desc->bInterfaceNumber; //获得接口序号
if (intf_desc->bInterfaceClass != 3 || intf_desc->bInterfaceProtocol != 2) //USB鼠标属于HID类,bInterfaceClass为3,bInterfaceProtocol为2
continue;
else
{
/* 找到了USB鼠标 */
fprintf(stdout, "find usb mouse ok\n"); //找到了USB鼠标对应的接口
for (int ep = 0; ep < intf_desc->bNumEndpoints; ep++) //对于找到的接口,遍历所有端点,寻找输入中断端点
{ //bmAttributes低2位为LIBUSB_TRANSFER_TYPE_INTERRUPT,bEndpointAddress最高位为1即LIBUSB_ENDPOINT_IN
if ((intf_desc->endpoint[ep].bmAttributes & 3) == LIBUSB_TRANSFER_TYPE_INTERRUPT ||
(intf_desc->endpoint[ep].bEndpointAddress & 0x80) == LIBUSB_ENDPOINT_IN) {
/* 找到了输入的中断端点 */
fprintf(stdout, "find in int endpoint\n");
endpoint = intf_desc->endpoint[ep].bEndpointAddress;//如果找到了输入的中断端点,就记录地址
found = 1;
break;
}
}
}
if (found)
break;
}
libusb_free_config_descriptor(config_desc);//释放配置描述符占用的内存
if (found)
break;
}
bInterfaceClass值不同,就对应不同种类的设备,下面是不同值和设备的对应关系。
#define USB_DEVICE_CLASS_RESERVED 0x00 //保留类,不用于特定类别的设备 #define USB_DEVICE_CLASS_AUDIO 0x01 //音频类设备,如:麦克风、扬声器等 #define USB_DEVICE_CLASS_COMMUNICATIONS 0x02 //通信设备,如:调制解调器、网络适配器 #define USB_DEVICE_CLASS_HUMAN_INTERFACE 0x03 //人机接口设备,如:键盘、鼠标等。 #define USB_DEVICE_CLASS_MONITOR 0x04 //监视设备,如:显示器 #define USB_DEVICE_CLASS_PHYSICAL_INTERFACE 0x05 //物理接口设备,如:读卡器 #define USB_DEVICE_CLASS_POWER 0x06 //电源设备,通常用于电源管理 #define USB_DEVICE_CLASS_IMAGE 0x06 //图像设备,如扫描仪、相机等 #define USB_DEVICE_CLASS_PRINTER 0x07 //打印设备,如:打印机 #define USB_DEVICE_CLASS_STORAGE 0x08 //存储设备,如硬盘驱动器,USB闪存 #define USB_DEVICE_CLASS_HUB 0x09 //集线器设备,用于连接多个USB #define USB_DEVICE_CLASS_CDC_DATA 0x0A //CDC数据设备,通常用于网络设备 #define USB_DEVICE_CLASS_SMART_CARD 0x0B //智能卡设备 #define USB_DEVICE_CLASS_CONTENT_SECURITY 0x0D //内容安全设备,用于数字版权管理 #define USB_DEVICE_CLASS_VIDEO 0x0E //视频设备,如摄像头、视频监控设备 #define USB_DEVICE_CLASS_PERSONAL_HEALTHCARE 0x0F //个人医疗保健设备 #define USB_DEVICE_CLASS_AUDIO_VIDEO 0x10 //音频、视频设备 #define USB_DEVICE_CLASS_BILLBOARD 0x11 //广告牌设备,用于显示信息 #define USB_DEVICE_CLASS_DIAGNOSTIC_DEVICE 0xDC //诊断设备 #define USB_DEVICE_CLASS_WIRELESS_CONTROLLER 0xE0 //无线控制器 #define USB_DEVICE_CLASS_MISCELLANEOUS 0xEF //杂项设备,用于不属于其他类别的设备 #define USB_DEVICE_CLASS_APPLICATION_SPECIFIC 0xFE //特定应用程序设备 #define USB_DEVICE_CLASS_VENDOR_SPECIFIC 0xFF //供应商特定设备
而bInterfaceProtocol是USB设备的接口描述符中的一个字段,它用于指定特定的接口的通信协议。通常与bInterfaceClass一起使用,以唯一标识设备上的一个接口。
随后根据设备查找结果进行错误处理和资源管理。
if (!found)//如果未找到设备,就释放资源
{
/* free device list */
libusb_free_device_list(devs, 1);
libusb_exit(NULL);
exit(1);
}
if (found)//如果找到设备则尝试打开设备以进行进一步操作
{
/* libusb_open */
err = libusb_open(dev, &dev_handle);
if (err)
{
fprintf(stderr, "failed to open usb mouse\n");
exit(1);
}
fprintf(stdout, "libusb_open ok\n");
}
/* free device list */
libusb_free_device_list(devs, 1);//释放设备列表
/* claim interface */
libusb_set_auto_detach_kernel_driver(dev_handle, 1); //自动分离与设备关联的内核驱动程序,以便libusb可以控制设备
err = libusb_claim_interface(dev_handle, interface_num);//声明设备的接口,以便libusb可以独占访问
if (err)
{
fprintf(stderr, "failed to libusb_claim_interface\n");
exit(1);
}
fprintf(stdout, "libusb_claim_interface ok\n");
随后使用libusb库进行USB中断传输。中断传输使用函数libusb_interrupt_transfer(dev_handle, endpoint, buffer, 16, &transferred, 5000);第一个参数dev_handle为设备句柄,用于标识要进行通信的设备。第二个参数endpoint为端点地址,指定从哪个端点进行数据传输。第三个参数buffer为数据缓冲区,用于存储设备接收到的数据。第四个参数为要传输数据的最大长度。第五个参transferred为指向整数变量的指针,用于存储实际传输的字节数。第六个参数为超时时间,以毫秒为单位。
如果接收没出错就把接收到的数据打印出来。
/* libusb_interrupt_transfer */
while (1)
{
err = libusb_interrupt_transfer(dev_handle, endpoint, buffer, 16, &transferred, 5000);//用于执行中断传输
if (!err) {
/* parser data */
printf("%04d datas: ", count++);
for (int i = 0; i < transferred; i++)//遍历接收到的数据,并打印
{
printf("%02x ", buffer[i]);
}
printf("\n");
} else if (err == LIBUSB_ERROR_TIMEOUT){
fprintf(stderr, "libusb_interrupt_transfer timout\n");
} else {
fprintf(stderr, "libusb_interrupt_transfer err : %d\n", err);
//exit(1);
}
}
/* libusb_close */ //关闭设备
libusb_release_interface(dev_handle, interface_num);
libusb_close(dev_handle);
libusb_exit(NULL);