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;
}

运行与验证

  1. 编译:gcc -o usb_audio_enum usb_audio_enum.c $(pkg-config --cflags --libs libusb-1.0)
  2. 插入USB音频设备(如USB声卡),运行:sudo ./usb_audio_enum 0x1234 0x5678(替换为实际VID/PID,可用lsusb查看)
  3. 预期输出(以典型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识别