1. USB音频设备枚举与描述符解析
任务目标
本案例实现USB音频设备在主机侧的枚举流程:读取设备描述符、配置描述符集合,解析其中的接口描述符、端点描述符及音频类特定描述符(音频控制接口、音频流接口、同步端点等),最终输出结构化的设备拓扑信息。
环境与依赖
- Linux主机(或任意支持libusb的平台)
- libusb-1.0开发库:
sudo apt install libusb-1.0-0-dev - 编译:
gcc -o usb_audio_enum usb_audio_enum.c $(pkg-config --cflags --libs libusb-1.0)
关键完整代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <libusb-1.0/libusb.h>
#define AUDIO_CLASS 0x01
#define AUDIOCONTROL_SUBCLASS 0x01
#define AUDIOSTREAMING_SUBCLASS 0x02
#define CS_INTERFACE 0x24
#define CS_ENDPOINT 0x25
#define HEADER_DESCRIPTOR 0x01
#define INPUT_TERMINAL_DESCRIPTOR 0x02
#define OUTPUT_TERMINAL_DESCRIPTOR 0x03
#define FEATURE_UNIT_DESCRIPTOR 0x06
static const char* ep_type_str(uint8_t attr) {
switch (attr & 0x03) {
case 0: return "Control";
case 1: return "Isochronous";
case 2: return "Bulk";
case 3: return "Interrupt";
default: return "Unknown";
}
}
static void parse_audio_cs_interface(const uint8_t* p, int len) {
if (len < 2) return;
switch (p[2]) {
case HEADER_DESCRIPTOR:
if (len >= 7) printf(" [AC] Header: bcdADC=%x.%02x, wTotalLength=%u\n",
p[3] >> 8, p[3] & 0xff, p[4] | (p[5] << 8));
break;
case INPUT_TERMINAL_DESCRIPTOR:
if (len >= 8) printf(" [AC] Input Terminal: ID=%u, Type=0x%04x\n", p[3], p[4] | (p[5] << 8));
break;
case OUTPUT_TERMINAL_DESCRIPTOR:
if (len >= 8) printf(" [AC] Output Terminal: ID=%u, Type=0x%04x\n", p[3], p[4] | (p[5] << 8));
break;
case FEATURE_UNIT_DESCRIPTOR:
if (len >= 6) printf(" [AC] Feature Unit: ID=%u, SourceID=%u\n", p[3], p[4]);
break;
default:
printf(" [AC] CS Interface desc type=0x%02x len=%d\n", p[2], len);
}
}
static void parse_audio_cs_endpoint(const uint8_t* p, int len) {
if (len < 3) return;
printf(" [AS] CS Endpoint: type=0x%02x, attr=0x%02x\n", p[2], p[3]);
}
static void parse_interface(const uint8_t* p, int len) {
int i = 0;
while (i + 2 <= len) {
int dlen = p[i];
int dtype = p[i+1];
if (dlen == 0 || i + dlen > len) break;
if (dtype == 0x04) { // Interface Descriptor
printf(" Interface: #%u alt=%u class=%u subclass=%u protocol=%u ep_count=%u\n",
p[i+2], p[i+3], p[i+5], p[i+6], p[i+7], p[i+4]);
} else if (dtype == 0x05) { // Endpoint Descriptor
printf(" Endpoint: addr=0x%02x attr=0x%02x (%s) maxpkt=%u interval=%u\n",
p[i+2], p[i+3], ep_type_str(p[i+3]), p[i+4] | (p[i+5] << 8), p[i+6]);
} else if (dtype == CS_INTERFACE) {
parse_audio_cs_interface(p + i, dlen);
} else if (dtype == CS_ENDPOINT) {
parse_audio_cs_endpoint(p + i, dlen);
}
i += dlen;
}
}
static void parse_config_descriptor(const uint8_t* buf, int len) {
int i = 0;
while (i + 2 <= len) {
int dlen = buf[i];
int dtype = buf[i+1];
if (dlen == 0 || i + dlen > len) break;
if (dtype == 0x02) {
printf("Configuration: %u interfaces, attr=0x%02x, power=%umA\n",
buf[i+4], buf[i+7], buf[i+8] * 2);
} else if (dtype == 0x04) {
parse_interface(buf + i, dlen);
}
i += dlen;
}
}
int main(int argc, char* argv[]) {
libusb_device** devs;
ssize_t cnt;
int ret = 0;
if (argc < 3) {
fprintf(stderr, "Usage: %s <vid_hex> <pid_hex>\n", argv[0]);
return 1;
}
uint16_t vid = (uint16_t)strtol(argv[1], NULL, 16);
uint16_t pid = (uint16_t)strtol(argv[2], NULL, 16);
if (libusb_init(NULL) < 0) { fprintf(stderr, "libusb_init failed\n"); return 1; }
libusb_set_option(NULL, LIBUSB_OPTION_LOG_LEVEL, LIBUSB_LOG_LEVEL_WARNING);
cnt = libusb_get_device_list(NULL, &devs);
if (cnt < 0) { fprintf(stderr, "get_device_list failed\n"); libusb_exit(NULL); return 1; }
libusb_device* target = NULL;
for (ssize_t i = 0; i < cnt; i++) {
struct libusb_device_descriptor dd;
if (libusb_get_device_descriptor(devs[i], &dd) == 0) {
if (dd.idVendor == vid && dd.idProduct == pid) {
target = devs[i];
printf("Device: VID=0x%04x PID=0x%04x class=%u subclass=%u protocol=%u\n",
dd.idVendor, dd.idProduct, dd.bDeviceClass, dd.bDeviceSubClass, dd.bDeviceProtocol);
break;
}
}
}
if (!target) {
fprintf(stderr, "Device not found\n");
libusb_free_device_list(devs, 1);
libusb_exit(NULL);
return 1;
}
libusb_device_handle* handle;
if (libusb_open(target, &handle) != 0) {
fprintf(stderr, "Failed to open device (permission?)\n");
libusb_free_device_list(devs, 1);
libusb_exit(NULL);
return 1;
}
// Read config descriptor (full, including audio class specific descriptors)
struct libusb_config_descriptor* config;
if (libusb_get_active_config_descriptor(target, &config) == 0) {
// libusb's config descriptor only contains standard descriptors.
// To get class-specific descriptors, we must read raw config data.
unsigned char raw[1024];
int r = libusb_get_descriptor(handle, 0x02, config->bConfigurationValue, raw, sizeof(raw));
if (r >= 4) {
int total = raw[2] | (raw[3] << 8);
if (total <= r) {
parse_config_descriptor(raw, total);
} else {
// Need larger buffer
unsigned char* big = malloc(total);
r = libusb_get_descriptor(handle, 0x02, config->bConfigurationValue, big, total);
if (r == total) parse_config_descriptor(big, total);
else fprintf(stderr, "Short read: %d/%d\n", r, total);
free(big);
}
}
libusb_free_config_descriptor(config);
} else {
fprintf(stderr, "Failed to get config descriptor\n");
ret = 1;
}
libusb_close(handle);
libusb_free_device_list(devs, 1);
libusb_exit(NULL);
return ret;
}
运行与验证
- 编译:
gcc -o usb_audio_enum usb_audio_enum.c $(pkg-config --cflags --libs libusb-1.0) - 插入USB音频设备(如USB声卡),运行:
sudo ./usb_audio_enum 0x1234 0x5678(替换为实际VID/PID,可用lsusb查看) - 预期输出(以典型USB音频设备为例,具体值取决于设备):
Device: VID=0x1234 PID=0x5678 class=0 subclass=0 protocol=0
Configuration: 1 interfaces, attr=0x80, power=100mA
Interface: #0 alt=0 class=1 subclass=1 protocol=0 ep_count=0
[AC] Header: bcdADC=1.00, wTotalLength=57
[AC] Input Terminal: ID=1, Type=0x0101
[AC] Output Terminal: ID=2, Type=0x0301
[AC] Feature Unit: ID=3, SourceID=1
Interface: #0 alt=1 class=1 subclass=2 protocol=0 ep_count=1
Endpoint: addr=0x81 attr=0x05 (Isochronous) maxpkt=192 interval=1
[AS] CS Endpoint: type=0x01, attr=0x00
判定标准
- 程序能正确打印设备VID/PID及配置描述符中的接口数量
- 音频控制接口(class=1, subclass=1)被识别,且其内部CS接口描述符(Header、Terminal、Feature Unit)被逐条解析输出
- 音频流接口(class=1, subclass=2)被识别,同步端点(attr=0x05或0x0D)及其CS端点描述符被正确解析
- 若设备无音频类描述符或VID/PID不匹配,程序给出明确错误提示而非崩溃
常见问题
- 权限不足:打开设备失败时使用
sudo运行,或添加udev规则 - raw描述符读取长度不足:代码已处理首次读取后按wTotalLength重新分配缓冲区的场景
- 复合设备:若设备同时包含音频和非音频接口,程序会打印所有接口,音频相关接口通过class=1识别