USB Gadget
+ -

IMX6UL USB Gadget支持麦克风扬声器f_uac1.c源代码

2025-09-23 0 0

该代码是经过本人修改后的源代码,在原来仅支持一路扬声器的基础上增加一路麦克风,并让麦克风的数据和扬声器的数据回环。

/*
 * f_audio.c -- USB Audio class function driver
  *
 * Copyright (C) 2008 Bryan Wu <cooloney@kernel.org>
 * Copyright (C) 2008 Analog Devices, Inc
 *
 * Enter bugs at http://blackfin.uclinux.org/
 *
 * Licensed under the GPL-2 or later.
 */

#include <linux/slab.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/device.h>
#include <linux/atomic.h>
#include <linux/string.h>

#include "u_uac1.h"

static int generic_set_cmd(struct usb_audio_control *con, u8 cmd, int value);
static int generic_get_cmd(struct usb_audio_control *con, u8 cmd);

/*
 * DESCRIPTORS ... most are static, but strings and full
 * configuration descriptors are built on demand.
 */

/*
 * We have two interfaces- AudioControl and AudioStreaming
 * TODO: only supcard playback currently
 */

#define MIC

#define F_AUDIO_AC_INTERFACE    0
#define F_AUDIO_AS_INTERFACE    1


/* B.3.1  Standard AC Interface Descriptor */
static struct usb_interface_descriptor ac_interface_desc = {
    .bLength =        USB_DT_INTERFACE_SIZE,
    .bDescriptorType =    USB_DT_INTERFACE,
    .bNumEndpoints =    0,
    .bInterfaceClass =    USB_CLASS_AUDIO,
    .bInterfaceSubClass =    USB_SUBCLASS_AUDIOCONTROL,
};

/*
 * The number of AudioStreaming and MIDIStreaming interfaces
 * in the Audio Interface Collection
 */
DECLARE_UAC_AC_HEADER_DESCRIPTOR(1);

#ifdef MIC

#define F_AUDIO_NUM_INTERFACES    2
#define UAC_DT_AC_HEADER_LENGTH    UAC_DT_AC_HEADER_SIZE(F_AUDIO_NUM_INTERFACES)
#define UAC_DT_TOTAL_LENGTH (UAC_DT_AC_HEADER_LENGTH + UAC_DT_INPUT_TERMINAL_SIZE  + UAC_DT_OUTPUT_TERMINAL_SIZE + UAC_DT_INPUT_TERMINAL_SIZE + UAC_DT_OUTPUT_TERMINAL_SIZE )

#else
#define F_AUDIO_NUM_INTERFACES    1
#define UAC_DT_AC_HEADER_LENGTH    UAC_DT_AC_HEADER_SIZE(F_AUDIO_NUM_INTERFACES)
#define UAC_DT_TOTAL_LENGTH (UAC_DT_AC_HEADER_LENGTH + UAC_DT_INPUT_TERMINAL_SIZE  + UAC_DT_OUTPUT_TERMINAL_SIZE + UAC_DT_FEATURE_UNIT_SIZE(0))

#endif

/* B.3.2  Class-Specific AC Interface Descriptor */
#ifdef MIC


unsigned char ac_header_desc[] =
{
    0x0A,
    0x24, 
    0x01, 
    0x00, 0x01, 
    __constant_cpu_to_le16(UAC_DT_TOTAL_LENGTH)&0xff, __constant_cpu_to_le16(UAC_DT_TOTAL_LENGTH)>>8,
    0x02,
    0x01,
    0x02
};

#else

static struct uac1_ac_header_descriptor_1 ac_header_desc = {
    .bLength = UAC_DT_AC_HEADER_LENGTH,
    .bDescriptorType = USB_DT_CS_INTERFACE,
    .bDescriptorSubtype = UAC_HEADER,
    .bcdADC = __constant_cpu_to_le16(0x0100),
    .wTotalLength = __constant_cpu_to_le16(UAC_DT_TOTAL_LENGTH),
    .bInCollection = F_AUDIO_NUM_INTERFACES,
    .baInterfaceNr = {
        /* Interface number of the first AudioStream interface */
            [0] = 1,
        }
};
#endif

#define INPUT_TERMINAL_ID    1
static struct uac_input_terminal_descriptor input_terminal_desc = {
    .bLength =        UAC_DT_INPUT_TERMINAL_SIZE,
    .bDescriptorType =    USB_DT_CS_INTERFACE,
    .bDescriptorSubtype =    UAC_INPUT_TERMINAL,
    .bTerminalID =        INPUT_TERMINAL_ID,
    .wTerminalType =    UAC_TERMINAL_STREAMING,
    .bAssocTerminal =    0,
    .wChannelConfig =    0x3,
};
DECLARE_UAC_FEATURE_UNIT_DESCRIPTOR(0);


#define FEATURE_UNIT_ID        2
static struct uac_feature_unit_descriptor_0 feature_unit_desc = {
    .bLength        = UAC_DT_FEATURE_UNIT_SIZE(0),
    .bDescriptorType    = USB_DT_CS_INTERFACE,
    .bDescriptorSubtype    = UAC_FEATURE_UNIT,
    .bUnitID        = FEATURE_UNIT_ID,
    .bSourceID        = INPUT_TERMINAL_ID,
    .bControlSize        = 2,
    .bmaControls[0]        = (UAC_FU_MUTE | UAC_FU_VOLUME),
};

static struct usb_audio_control mute_control = {
    .list = LIST_HEAD_INIT(mute_control.list),
    .name = "Mute Control",
    .type = UAC_FU_MUTE,
    /* Todo: add real Mute control code */
    .set = generic_set_cmd,
    .get = generic_get_cmd,
};

static struct usb_audio_control volume_control = {
    .list = LIST_HEAD_INIT(volume_control.list),
    .name = "Volume Control",
    .type = UAC_FU_VOLUME,
    /* Todo: add real Volume control code */
    .set = generic_set_cmd,
    .get = generic_get_cmd,
};

static struct usb_audio_control_selector feature_unit = {
    .list = LIST_HEAD_INIT(feature_unit.list),
    .id = FEATURE_UNIT_ID,
    .name = "Mute & Volume Control",
    .type = UAC_FEATURE_UNIT,
    .desc = (struct usb_descriptor_header *)&feature_unit_desc,
};

#define OUTPUT_TERMINAL_ID    3
static struct uac1_output_terminal_descriptor output_terminal_desc = {
    .bLength        = UAC_DT_OUTPUT_TERMINAL_SIZE,
    .bDescriptorType    = USB_DT_CS_INTERFACE,
    .bDescriptorSubtype    = UAC_OUTPUT_TERMINAL,
    .bTerminalID        = OUTPUT_TERMINAL_ID,
    .wTerminalType        = UAC_OUTPUT_TERMINAL_SPEAKER,
    .bAssocTerminal        = 1,//FEATURE_UNIT_ID,
    .bSourceID        = 1,//FEATURE_UNIT_ID,
};

/* B.4.1  Standard AS Interface Descriptor */
static struct usb_interface_descriptor as_interface_alt_0_desc = {
    .bLength =        USB_DT_INTERFACE_SIZE,
    .bDescriptorType =    USB_DT_INTERFACE,
    .bAlternateSetting =    0,
    .bNumEndpoints =    0,
    .bInterfaceClass =    USB_CLASS_AUDIO,
    .bInterfaceSubClass =    USB_SUBCLASS_AUDIOSTREAMING,
};

static struct usb_interface_descriptor as_interface_alt_1_desc = {
    .bLength =        USB_DT_INTERFACE_SIZE,
    .bDescriptorType =    USB_DT_INTERFACE,
    .bAlternateSetting =    1,
    .bNumEndpoints =    1,
    .bInterfaceClass =    USB_CLASS_AUDIO,
    .bInterfaceSubClass =    USB_SUBCLASS_AUDIOSTREAMING,
};

/* B.4.2  Class-Specific AS Interface Descriptor */
static struct uac1_as_header_descriptor as_header_desc = {
    .bLength =        UAC_DT_AS_HEADER_SIZE,
    .bDescriptorType =    USB_DT_CS_INTERFACE,
    .bDescriptorSubtype =    UAC_AS_GENERAL,
    .bTerminalLink =    INPUT_TERMINAL_ID,
    .bDelay =        1,
    .wFormatTag =        UAC_FORMAT_TYPE_I_PCM,
};

DECLARE_UAC_FORMAT_TYPE_I_DISCRETE_DESC(1);

static struct uac_format_type_i_discrete_descriptor_1 as_type_i_desc = {
    .bLength =        UAC_FORMAT_TYPE_I_DISCRETE_DESC_SIZE(1),
    .bDescriptorType =    USB_DT_CS_INTERFACE,
    .bDescriptorSubtype =    UAC_FORMAT_TYPE,
    .bFormatType =        UAC_FORMAT_TYPE_I,
    .bSubframeSize =    2,
    .bBitResolution =    16,
    .bSamFreqType =        1,
};

/* Standard ISO OUT Endpoint Descriptor */
static struct usb_endpoint_descriptor as_out_ep_desc = {
    .bLength = USB_DT_ENDPOINT_AUDIO_SIZE,
    .bDescriptorType = USB_DT_ENDPOINT,
    .bEndpointAddress = USB_DIR_OUT,
    .bmAttributes = USB_ENDPOINT_SYNC_ADAPTIVE | USB_ENDPOINT_XFER_ISOC,
    .wMaxPacketSize =  cpu_to_le16(UAC1_OUT_EP_MAX_PACKET_SIZE),
    .bInterval =        4,
};



/* Class-specific AS ISO OUT Endpoint Descriptor */
static struct uac_iso_endpoint_descriptor as_iso_out_desc = {
    .bLength =        UAC_ISO_ENDPOINT_DESC_SIZE,
    .bDescriptorType =    USB_DT_CS_ENDPOINT,
    .bDescriptorSubtype =    UAC_EP_GENERAL,
    .bmAttributes =     1,
    .bLockDelayUnits =    1,
    .wLockDelay =        __constant_cpu_to_le16(1),
};


#ifdef MIC
//mic control
//input
static struct uac_input_terminal_descriptor input_terminal_desc_mic = {
    .bLength = UAC_DT_INPUT_TERMINAL_SIZE,
    .bDescriptorType = USB_DT_CS_INTERFACE,
    .bDescriptorSubtype = UAC_INPUT_TERMINAL,
    .bTerminalID = 6,
    .wTerminalType = 0x402,
    .bAssocTerminal = 0,
    .wChannelConfig = 0x3,
};

//output
static struct uac1_output_terminal_descriptor output_terminal_desc_mic = {
    .bLength = UAC_DT_OUTPUT_TERMINAL_SIZE,
    .bDescriptorType = USB_DT_CS_INTERFACE,
    .bDescriptorSubtype = UAC_OUTPUT_TERMINAL,
    .bTerminalID = 7,
    .wTerminalType = 0x0101,
    .bAssocTerminal = 6,
    .bSourceID = 6,
};


//mic stream
static struct usb_interface_descriptor as_interface_alt_0_desc_mic = {
    .bLength = USB_DT_INTERFACE_SIZE,
    .bDescriptorType = USB_DT_INTERFACE,
    .bAlternateSetting = 0,
    .bNumEndpoints = 0,
    .bInterfaceClass = USB_CLASS_AUDIO,
    .bInterfaceSubClass = USB_SUBCLASS_AUDIOSTREAMING,
};

static struct usb_interface_descriptor as_interface_alt_1_desc_mic = {
    .bLength = USB_DT_INTERFACE_SIZE,
    .bDescriptorType = USB_DT_INTERFACE,
    .bAlternateSetting = 1,
    .bNumEndpoints = 1,
    .bInterfaceClass = USB_CLASS_AUDIO,
    .bInterfaceSubClass = USB_SUBCLASS_AUDIOSTREAMING,
};

static struct uac1_as_header_descriptor as_header_desc_mic = {
    .bLength = UAC_DT_AS_HEADER_SIZE,
    .bDescriptorType = USB_DT_CS_INTERFACE,
    .bDescriptorSubtype = UAC_AS_GENERAL,
    .bTerminalLink = 7,//mic input
    .bDelay = 0,
    .wFormatTag = UAC_FORMAT_TYPE_I_PCM,
};

static struct uac_format_type_i_discrete_descriptor_1 as_type_i_desc_mic = {
    .bLength = UAC_FORMAT_TYPE_I_DISCRETE_DESC_SIZE(1),
    .bDescriptorType = USB_DT_CS_INTERFACE,
    .bDescriptorSubtype = UAC_FORMAT_TYPE,
    .bFormatType = UAC_FORMAT_TYPE_I,
    .bSubframeSize = 2,
    .bBitResolution = 16,
    .bSamFreqType = 1,
};

static struct usb_endpoint_descriptor as_in_ep_desc_mic = {
    .bLength = USB_DT_ENDPOINT_AUDIO_SIZE,
    .bDescriptorType = USB_DT_ENDPOINT,
    .bEndpointAddress = USB_DIR_IN,
    .bmAttributes = USB_ENDPOINT_SYNC_ADAPTIVE | USB_ENDPOINT_XFER_ISOC,
    .wMaxPacketSize = cpu_to_le16(192),// cpu_to_le16(UAC1_OUT_EP_MAX_PACKET_SIZE),
    .bInterval = 4,
};

static struct uac_iso_endpoint_descriptor as_iso_in_desc_mic = {
    .bLength = UAC_ISO_ENDPOINT_DESC_SIZE,
    .bDescriptorType = USB_DT_CS_ENDPOINT,
    .bDescriptorSubtype = UAC_EP_GENERAL,
    .bmAttributes = 1,
    .bLockDelayUnits = 1,
    .wLockDelay = __constant_cpu_to_le16(1),
};
#endif



static struct usb_descriptor_header *f_audio_desc[] = {
    (struct usb_descriptor_header *)&ac_interface_desc,
#ifdef MIC
    (struct usb_descriptor_header*)ac_header_desc,
#else
    (struct usb_descriptor_header *)&ac_header_desc,
#endif
    (struct usb_descriptor_header *)&input_terminal_desc,
    (struct usb_descriptor_header *)&output_terminal_desc,
//    (struct usb_descriptor_header *)&feature_unit_desc,

#ifdef MIC
    (struct usb_descriptor_header*)&input_terminal_desc_mic,
    (struct usb_descriptor_header*)&output_terminal_desc_mic,
#endif

    (struct usb_descriptor_header *)&as_interface_alt_0_desc,
    (struct usb_descriptor_header *)&as_interface_alt_1_desc,
    (struct usb_descriptor_header *)&as_header_desc,
    (struct usb_descriptor_header *)&as_type_i_desc,
    (struct usb_descriptor_header *)&as_out_ep_desc,
    (struct usb_descriptor_header *)&as_iso_out_desc,

#ifdef MIC
    (struct usb_descriptor_header*)&as_interface_alt_0_desc_mic,
    (struct usb_descriptor_header*)&as_interface_alt_1_desc_mic,
    (struct usb_descriptor_header*)&as_header_desc_mic,
    (struct usb_descriptor_header*)&as_type_i_desc_mic,
    (struct usb_descriptor_header*)&as_in_ep_desc_mic,
    (struct usb_descriptor_header*)&as_iso_in_desc_mic,
#endif

    NULL,
};




enum {
    STR_AC_IF,
    STR_INPUT_TERMINAL,
    STR_INPUT_TERMINAL_CH_NAMES,
    STR_FEAT_DESC_0,
    STR_OUTPUT_TERMINAL,
    STR_AS_IF_ALT0,
    STR_AS_IF_ALT1,
};

static struct usb_string strings_uac1[] = {
    [STR_AC_IF].s = "AC Interface",
    [STR_INPUT_TERMINAL].s = "Input terminal",
    [STR_INPUT_TERMINAL_CH_NAMES].s = "Channels",
    [STR_FEAT_DESC_0].s = "Volume control & mute",
    [STR_OUTPUT_TERMINAL].s = "Output terminal",
    [STR_AS_IF_ALT0].s = "AS Interface",
    [STR_AS_IF_ALT1].s = "AS Interface",
    { },
};

static struct usb_gadget_strings str_uac1 = {
    .language = 0x0409,    /* en-us */
    .strings = strings_uac1,
};

static struct usb_gadget_strings *uac1_strings[] = {
    &str_uac1,
    NULL,
};

/*
 * This function is an ALSA sound card following USB Audio Class Spec 1.0.
 */

/*-------------------------------------------------------------------------*/
struct f_audio_buf {
    u8 *buf;
    int actual;
    struct list_head list;
};

static struct f_audio_buf *f_audio_buffer_alloc(int buf_size)
{
    struct f_audio_buf *copy_buf;

    copy_buf = kzalloc(sizeof *copy_buf, GFP_ATOMIC);
    if (!copy_buf)
        return ERR_PTR(-ENOMEM);

    copy_buf->buf = kzalloc(buf_size, GFP_ATOMIC);
    if (!copy_buf->buf) {
        kfree(copy_buf);
        return ERR_PTR(-ENOMEM);
    }

    return copy_buf;
}

static void f_audio_buffer_free(struct f_audio_buf *audio_buf)
{
    kfree(audio_buf->buf);
    kfree(audio_buf);
}

/*-------------------------------------------------------------------------*/

struct f_audio {
    struct gaudio            card;

    /* endpoints handle full and/or high speeds */
    struct usb_ep            *out_ep;
    struct usb_ep            *in_ep;//20250916
    spinlock_t            lock;
    struct f_audio_buf *copy_buf;
    struct work_struct playback_work;
    struct list_head play_queue;

    /* Control Set command */
    struct list_head cs;
    u8 set_cmd;
    struct usb_audio_control *set_con;
};

static inline struct f_audio *func_to_audio(struct usb_function *f)
{
    return container_of(f, struct f_audio, card.func);
}

/*-------------------------------------------------------------------------*/

static void f_audio_playback_work(struct work_struct *data)
{
    struct f_audio *audio = container_of(data, struct f_audio,playback_work);
    struct f_audio_buf *play_buf;

    spin_lock_irq(&audio->lock);

    //no data to play
    if (list_empty(&audio->play_queue)) {
        spin_unlock_irq(&audio->lock);
        return;
    }

    play_buf = list_first_entry(&audio->play_queue,struct f_audio_buf, list);
    list_del(&play_buf->list);
    spin_unlock_irq(&audio->lock);

    u_audio_playback(&audio->card, play_buf->buf, play_buf->actual);
    f_audio_buffer_free(play_buf);
}

static int f_audio_out_ep_complete(struct usb_ep *ep, struct usb_request *req)
{
    struct f_audio *audio = req->context;
    struct usb_composite_dev *cdev = audio->card.func.config->cdev;
    struct f_audio_buf *copy_buf = audio->copy_buf;
    struct f_uac1_opts *opts;
    int audio_buf_size;
    int err;

    opts = container_of(audio->card.func.fi, struct f_uac1_opts,func_inst);
    audio_buf_size = opts->audio_buf_size;

    if (!copy_buf)
        return -EINVAL;

//    printk("audio_buf_size=%d copy_buf->actual=%d req->actual=%d\n", audio_buf_size, copy_buf->actual,req->actual);

    /* Copy buffer is full, add it to the play_queue */

    //if buffer not enough,so insert to tail and to play ,and malloc new buffer to storage pcm data 
    if (audio_buf_size - copy_buf->actual < req->actual) {
        list_add_tail(&copy_buf->list, &audio->play_queue);
        schedule_work(&audio->playback_work);

        copy_buf = f_audio_buffer_alloc(audio_buf_size);
        if (IS_ERR(copy_buf))
            return -ENOMEM;
    }

    memcpy(copy_buf->buf + copy_buf->actual, req->buf, req->actual);
    copy_buf->actual += req->actual;

    //lastest data buffer to play
    audio->copy_buf = copy_buf;

    err = usb_ep_queue(ep, req, GFP_ATOMIC);
    if (err)
        ERROR(cdev, "%s queue req: %d\n", ep->name, err);

    return 0;

}

static void f_audio_complete(struct usb_ep *ep, struct usb_request *req)
{
    struct f_audio *audio = req->context;
    int status = req->status;
    u32 data = 0;
    struct usb_ep *out_ep = audio->out_ep;
    struct usb_ep* in_ep = audio->in_ep;
    struct usb_request* mreq;
    switch (status) {

    case 0:                /* normal completion? */
        if (ep == out_ep) 
        {
            mreq = usb_ep_alloc_request(in_ep, GFP_ATOMIC);
            if (mreq)
            {
                mreq->buf = kzalloc(req->actual, GFP_ATOMIC);
                if (mreq->buf)
                {
                    mreq->context = audio;
                    mreq->complete = f_audio_complete;
                    mreq->length = req->actual;
                    memcpy(mreq->buf, req->buf, req->actual);
                    usb_ep_queue(in_ep, mreq, GFP_ATOMIC);
                }
            }
            f_audio_out_ep_complete(ep, req);
        }
        else if (ep == in_ep)
        {
            kfree(req->buf);
            usb_ep_free_request(ep, req);
        }
        else if (audio->set_con)
        {
            memcpy(&data, req->buf, req->length);
            audio->set_con->set(audio->set_con, audio->set_cmd,    le16_to_cpu(data));
            audio->set_con = NULL;
        }
        break;
    default:
        //free_ep_req(ep, req);
        break;
    }
}

static int audio_set_intf_req(struct usb_function *f,const struct usb_ctrlrequest *ctrl)
{
    struct f_audio        *audio = func_to_audio(f);
    struct usb_composite_dev *cdev = f->config->cdev;
    struct usb_request    *req = cdev->req;
    u8            id = ((le16_to_cpu(ctrl->wIndex) >> 8) & 0xFF);
    u16            len = le16_to_cpu(ctrl->wLength);
    u16            w_value = le16_to_cpu(ctrl->wValue);
    u8            con_sel = (w_value >> 8) & 0xFF;
    u8            cmd = (ctrl->bRequest & 0x0F);
    struct usb_audio_control_selector *cs;
    struct usb_audio_control *con;

    DBG(cdev, "bRequest 0x%x, w_value 0x%04x, len %d, entity %d\n",ctrl->bRequest, w_value, len, id);

    list_for_each_entry(cs, &audio->cs, list) {
        if (cs->id == id) {
            list_for_each_entry(con, &cs->control, list) {
                if (con->type == con_sel) {
                    audio->set_con = con;
                    break;
                }
            }
            break;
        }
    }

    audio->set_cmd = cmd;
    req->context = audio;
    req->complete = f_audio_complete;

    return len;
}

static int audio_get_intf_req(struct usb_function *f,const struct usb_ctrlrequest *ctrl)
{
    struct f_audio        *audio = func_to_audio(f);
    struct usb_composite_dev *cdev = f->config->cdev;
    struct usb_request    *req = cdev->req;
    int            value = -EOPNOTSUPP;
    u8            id = ((le16_to_cpu(ctrl->wIndex) >> 8) & 0xFF);
    u16            len = le16_to_cpu(ctrl->wLength);
    u16            w_value = le16_to_cpu(ctrl->wValue);
    u8            con_sel = (w_value >> 8) & 0xFF;
    u8            cmd = (ctrl->bRequest & 0x0F);
    struct usb_audio_control_selector *cs;
    struct usb_audio_control *con;

    DBG(cdev, "bRequest 0x%x, w_value 0x%04x, len %d, entity %d\n",    ctrl->bRequest, w_value, len, id);

    list_for_each_entry(cs, &audio->cs, list) {
        if (cs->id == id) {
            list_for_each_entry(con, &cs->control, list) {
                if (con->type == con_sel && con->get) {
                    value = con->get(con, cmd);
                    break;
                }
            }
            break;
        }
    }

    req->context = audio;
    req->complete = f_audio_complete;
    len = min_t(size_t, sizeof(value), len);
    memcpy(req->buf, &value, len);

    return len;
}

static int audio_set_endpoint_req(struct usb_function *f,const struct usb_ctrlrequest *ctrl)
{
    struct usb_composite_dev *cdev = f->config->cdev;
    int            value = -EOPNOTSUPP;
    u16            ep = le16_to_cpu(ctrl->wIndex);
    u16            len = le16_to_cpu(ctrl->wLength);
    u16            w_value = le16_to_cpu(ctrl->wValue);

    DBG(cdev, "bRequest 0x%x, w_value 0x%04x, len %d, endpoint %d\n",ctrl->bRequest, w_value, len, ep);

    switch (ctrl->bRequest) {
    case UAC_SET_CUR:
        value = len;
        break;

    case UAC_SET_MIN:
        break;

    case UAC_SET_MAX:
        break;

    case UAC_SET_RES:
        break;

    case UAC_SET_MEM:
        break;

    default:
        break;
    }

    return value;
}

static int audio_get_endpoint_req(struct usb_function *f,const struct usb_ctrlrequest *ctrl)
{
    struct usb_composite_dev *cdev = f->config->cdev;
    int value = -EOPNOTSUPP;
    u8 ep = ((le16_to_cpu(ctrl->wIndex) >> 8) & 0xFF);
    u16 len = le16_to_cpu(ctrl->wLength);
    u16 w_value = le16_to_cpu(ctrl->wValue);

    DBG(cdev, "bRequest 0x%x, w_value 0x%04x, len %d, endpoint %d\n",
            ctrl->bRequest, w_value, len, ep);

    switch (ctrl->bRequest) {
    case UAC_GET_CUR:
    case UAC_GET_MIN:
    case UAC_GET_MAX:
    case UAC_GET_RES:
        value = len;
        break;
    case UAC_GET_MEM:
        break;
    default:
        break;
    }

    return value;
}

static int f_audio_setup(struct usb_function *f, const struct usb_ctrlrequest *ctrl)
{
    struct usb_composite_dev *cdev = f->config->cdev;
    struct usb_request    *req = cdev->req;
    int            value = -EOPNOTSUPP;
    u16            w_index = le16_to_cpu(ctrl->wIndex);
    u16            w_value = le16_to_cpu(ctrl->wValue);
    u16            w_length = le16_to_cpu(ctrl->wLength);

    /* composite driver infrastructure handles everything; interface
     * activation uses set_alt().
     */
    switch (ctrl->bRequestType) {
    case USB_DIR_OUT | USB_TYPE_CLASS | USB_RECIP_INTERFACE:
        value = audio_set_intf_req(f, ctrl);
        break;

    case USB_DIR_IN | USB_TYPE_CLASS | USB_RECIP_INTERFACE:
        value = audio_get_intf_req(f, ctrl);
        break;

    case USB_DIR_OUT | USB_TYPE_CLASS | USB_RECIP_ENDPOINT:
        value = audio_set_endpoint_req(f, ctrl);
        break;

    case USB_DIR_IN | USB_TYPE_CLASS | USB_RECIP_ENDPOINT:
        value = audio_get_endpoint_req(f, ctrl);
        break;

    default:
        ERROR(cdev, "invalid control req%02x.%02x v%04x i%04x l%d\n",ctrl->bRequestType, ctrl->bRequest,w_value, w_index, w_length);
    }

    /* respond with data transfer or status phase? */
    if (value >= 0) {
        DBG(cdev, "audio req%02x.%02x v%04x i%04x l%d\n",ctrl->bRequestType, ctrl->bRequest,w_value, w_index, w_length);
        req->zero = 0;
        req->length = value;
        value = usb_ep_queue(cdev->gadget->ep0, req, GFP_ATOMIC);
        if (value < 0)
            ERROR(cdev, "audio response on err %d\n", value);
    }

    /* device either stalls (value < 0) or reports success */
    return value;
}

static int f_audio_set_alt(struct usb_function *f, unsigned intf, unsigned alt)
{
    struct f_audio        *audio = func_to_audio(f);
    struct usb_composite_dev *cdev = f->config->cdev;
    struct usb_ep *out_ep = audio->out_ep;
    struct usb_ep* in_ep = audio->in_ep;
    struct usb_request *req;
    struct f_uac1_opts *opts;
    int req_buf_size, req_count, audio_buf_size;
    int i = 0, err = 0;
    int k;
    unsigned char* p;

    opts = container_of(f->fi, struct f_uac1_opts, func_inst);
    req_buf_size = opts->req_buf_size;
    req_count = opts->req_count;
    audio_buf_size = opts->audio_buf_size;

    printk("f_audio_set_alt intf=%d alt=%d req_buf_size=%d\n", intf, alt, req_buf_size);
    if (in_ep->driver_data)
    {
        usb_ep_disable(in_ep);
        in_ep->driver_data = NULL;
    }

    if (intf == 1 || intf == 2) 
    {
        if (alt == 1)
        {
            if (intf == 1)
            {
                err = config_ep_by_speed(cdev->gadget, f, out_ep);
                printk("config_ep_by_speed err=%d \n", err);

                err = usb_ep_enable(out_ep);
                printk("usb_ep_enable err=%d \n", err);

                out_ep->driver_data = audio;

                audio->copy_buf = f_audio_buffer_alloc(audio_buf_size);
                if (IS_ERR(audio->copy_buf))
                    return -ENOMEM;
            }
            else
            {
                err = config_ep_by_speed(cdev->gadget, f, in_ep);
                printk("config_ep_by_speed err=%d \n", err);

                err = usb_ep_enable(in_ep);
                printk("usb_ep_enable err=%d \n", err);

                in_ep->driver_data = audio;
            }


            if (err)
                return err;

            /*
             * allocate a bunch of read buffers
             * and queue them all at once.
             */
            if (intf == 2)
            {
                req_count = 0;
            }
            for (i = 0; i < req_count && err == 0; i++) {
                if (intf == 1)
                {
                    req = usb_ep_alloc_request(out_ep, GFP_ATOMIC);
                //    printk("[%d] out usb_ep_alloc_request: %p\n", intf, req);
                }
                else
                {
                    req = usb_ep_alloc_request(in_ep, GFP_ATOMIC);
                //    printk("%d/%d [%d]in usb_ep_alloc_request:\n",i,req_count, intf);
                }

                if (req) {
                    req->buf = kzalloc(req_buf_size,GFP_ATOMIC);
                    if (req->buf) {            
                        req->context = audio;
                        req->complete = f_audio_complete;
                        req->length = req_buf_size;
                        if (intf == 1)
                        {
                            err = usb_ep_queue(out_ep, req, GFP_ATOMIC);
                        //    printk("[%d ]out queue req: %d\n", intf, err);
                        }
                        else if (intf == 2)//mic
                        {
                            req->length = 192;
                            p = (unsigned char*)req->buf;
                            for(k=0;k< req_buf_size;k++)
                                p[k] =  0x0+i;
                            err = usb_ep_queue(in_ep, req, GFP_ATOMIC);
                            //printk("[%d] in- queue req: %d\n", intf, err);
                        }
                    } else
                        err = -ENOMEM;
                } else
                    err = -ENOMEM;
            }

        } else {
            struct f_audio_buf *copy_buf = audio->copy_buf;
            if (copy_buf) {
                list_add_tail(&copy_buf->list,&audio->play_queue);
                schedule_work(&audio->playback_work);
            }
        }
    }

    return err;
}

static void f_audio_disable(struct usb_function *f)
{
    return;
}

/*-------------------------------------------------------------------------*/

static void f_audio_build_desc(struct f_audio *audio)
{
    struct gaudio *card = &audio->card;
    u8 *sam_freq;
    int rate;

    /* Set channel numbers */
    input_terminal_desc.bNrChannels = u_audio_get_playback_channels(card);
    as_type_i_desc.bNrChannels = u_audio_get_playback_channels(card);

    /* Set sample rates */
    rate = u_audio_get_playback_rate(card);
    sam_freq = as_type_i_desc.tSamFreq[0];
    memcpy(sam_freq, &rate, 3);

#ifdef MIC
    //set mic
    input_terminal_desc_mic.bNrChannels = u_audio_get_playback_channels(card);
    as_type_i_desc_mic.bNrChannels = u_audio_get_playback_channels(card);
    sam_freq = as_type_i_desc_mic.tSamFreq[0];
    memcpy(sam_freq, &rate, 3);
#endif
    /* Todo: Set Sample bits and other parameters */

    return;
}

/* audio function driver setup/binding */
static int
f_audio_bind(struct usb_configuration *c, struct usb_function *f)
{
    struct usb_composite_dev *cdev = c->cdev;
    struct f_audio        *audio = func_to_audio(f);
    struct usb_string    *us;
    int            status;
    struct usb_ep        *ep = NULL;
    struct f_uac1_opts    *audio_opts;

    audio_opts = container_of(f->fi, struct f_uac1_opts, func_inst);
    audio->card.gadget = c->cdev->gadget;
    /* set up ASLA audio devices */
    if (!audio_opts->bound) {
        status = gaudio_setup(&audio->card);
        if (status < 0)
            return status;
        audio_opts->bound = true;
    }
    us = usb_gstrings_attach(cdev, uac1_strings, ARRAY_SIZE(strings_uac1));
    if (IS_ERR(us))
        return PTR_ERR(us);

    //setup string
    //audio control
    ac_interface_desc.iInterface = us[STR_AC_IF].id;

    input_terminal_desc.iTerminal = us[STR_INPUT_TERMINAL].id;
    input_terminal_desc.iChannelNames = us[STR_INPUT_TERMINAL_CH_NAMES].id;
    feature_unit_desc.iFeature = us[STR_FEAT_DESC_0].id;
    output_terminal_desc.iTerminal = us[STR_OUTPUT_TERMINAL].id;
    as_interface_alt_0_desc.iInterface = us[STR_AS_IF_ALT0].id;
    as_interface_alt_1_desc.iInterface = us[STR_AS_IF_ALT1].id;

#ifdef MIC
    input_terminal_desc_mic.iTerminal = us[STR_INPUT_TERMINAL].id;
    input_terminal_desc_mic.iChannelNames = us[STR_INPUT_TERMINAL_CH_NAMES].id;
    output_terminal_desc_mic.iTerminal = us[STR_OUTPUT_TERMINAL].id;
    as_interface_alt_0_desc_mic.iInterface = us[STR_AS_IF_ALT0].id;
    as_interface_alt_1_desc_mic.iInterface = us[STR_AS_IF_ALT1].id;
#endif

    //setup audio descriptor parameter from card.playback device
    f_audio_build_desc(audio);

    /* allocate instance-specific interface IDs, and patch descriptors */
    status = usb_interface_id(c, f);//allocate an unused interface ID
    if (status < 0)
        goto fail;
    ac_interface_desc.bInterfaceNumber = status;

    status = usb_interface_id(c, f);
    if (status < 0)
        goto fail;
    as_interface_alt_0_desc.bInterfaceNumber = status;
    as_interface_alt_1_desc.bInterfaceNumber = status;
    printk("speaker interface = %d\n", status);

#ifdef MIC
    status = usb_interface_id(c, f);
    if (status < 0)
        goto fail;
    as_interface_alt_0_desc_mic.bInterfaceNumber = status;
    as_interface_alt_1_desc_mic.bInterfaceNumber = status;
    printk("mic interface=%d\n", status);
#endif

    status = -ENODEV;

    /* allocate instance-specific endpoints */
    ep = usb_ep_autoconfig(cdev->gadget, &as_out_ep_desc);
    if (!ep)
        goto fail;
    audio->out_ep = ep;
    audio->out_ep->desc = &as_out_ep_desc;
    ep->driver_data = cdev;    /* claim */

#ifdef MIC
    ep = usb_ep_autoconfig(cdev->gadget, &as_in_ep_desc_mic);
    if (!ep)
        goto fail;
    audio->in_ep = ep;
    audio->in_ep->desc = &as_in_ep_desc_mic;
    ep->driver_data = cdev;    /* claim */
#endif

    status = -ENOMEM;

    /* copy descriptors, and track endpoint copies */
    status = usb_assign_descriptors(f,NULL, f_audio_desc, NULL);//FS,HS,SS
    printk("f_audio_bind status=%d\n", status);
    if (status)
        goto fail;

    return 0;

fail:
    gaudio_cleanup(&audio->card);
    if (ep)
        ep->driver_data = NULL;
    return status;
}

/*-------------------------------------------------------------------------*/

static int generic_set_cmd(struct usb_audio_control *con, u8 cmd, int value)
{
    con->data[cmd] = value;

    return 0;
}

static int generic_get_cmd(struct usb_audio_control *con, u8 cmd)
{
    return con->data[cmd];
}

/* Todo: add more control selecotor dynamically */
static int control_selector_init(struct f_audio *audio)
{
    INIT_LIST_HEAD(&audio->cs);
    list_add(&feature_unit.list, &audio->cs);

    INIT_LIST_HEAD(&feature_unit.control);
    list_add(&mute_control.list,    &feature_unit.control);
    list_add(&volume_control.list,    &feature_unit.control);

    volume_control.data[UAC__CUR] = 0xffc0;
    volume_control.data[UAC__MIN] = 0xe3a0;
    volume_control.data[UAC__MAX] = 0xfff0;
    volume_control.data[UAC__RES] = 0x0030;

    return 0;
}

static inline struct f_uac1_opts *to_f_uac1_opts(struct config_item *item)
{
    return container_of(to_config_group(item), struct f_uac1_opts,
                func_inst.group);
}

CONFIGFS_ATTR_STRUCT(f_uac1_opts);
CONFIGFS_ATTR_OPS(f_uac1_opts);

static void f_uac1_attr_release(struct config_item *item)
{
    struct f_uac1_opts *opts = to_f_uac1_opts(item);

    usb_put_function_instance(&opts->func_inst);
}

static struct configfs_item_operations f_uac1_item_ops = {
    .release    = f_uac1_attr_release,
    .show_attribute    = f_uac1_opts_attr_show,
    .store_attribute = f_uac1_opts_attr_store,
};

#define UAC1_INT_ATTRIBUTE(name)                    \
static ssize_t f_uac1_opts_##name##_show(struct f_uac1_opts *opts,    \
                     char *page)            \
{                                    \
    int result;                            \
                                    \
    mutex_lock(&opts->lock);                    \
    result = sprintf(page, "%u\n", opts->name);            \
    mutex_unlock(&opts->lock);                    \
                                    \
    return result;                            \
}                                    \
                                    \
static ssize_t f_uac1_opts_##name##_store(struct f_uac1_opts *opts,    \
                      const char *page, size_t len)    \
{                                    \
    int ret;                            \
    u32 num;                            \
                                    \
    mutex_lock(&opts->lock);                    \
    if (opts->refcnt) {                        \
        ret = -EBUSY;                        \
        goto end;                        \
    }                                \
                                    \
    ret = kstrtou32(page, 0, &num);                    \
    if (ret)                            \
        goto end;                        \
                                    \
    opts->name = num;                        \
    ret = len;                            \
                                    \
end:                                    \
    mutex_unlock(&opts->lock);                    \
    return ret;                            \
}                                    \
                                    \
static struct f_uac1_opts_attribute f_uac1_opts_##name =        \
    __CONFIGFS_ATTR(name, S_IRUGO | S_IWUSR,            \
            f_uac1_opts_##name##_show,            \
            f_uac1_opts_##name##_store)

UAC1_INT_ATTRIBUTE(req_buf_size);
UAC1_INT_ATTRIBUTE(req_count);
UAC1_INT_ATTRIBUTE(audio_buf_size);

#define UAC1_STR_ATTRIBUTE(name)                    \
static ssize_t f_uac1_opts_##name##_show(struct f_uac1_opts *opts,    \
                     char *page)            \
{                                    \
    int result;                            \
                                    \
    mutex_lock(&opts->lock);                    \
    result = sprintf(page, "%s\n", opts->name);            \
    mutex_unlock(&opts->lock);                    \
                                    \
    return result;                            \
}                                    \
                                    \
static ssize_t f_uac1_opts_##name##_store(struct f_uac1_opts *opts,    \
                      const char *page, size_t len)    \
{                                    \
    int ret = -EBUSY;                        \
    char *tmp;                            \
                                    \
    mutex_lock(&opts->lock);                    \
    if (opts->refcnt)                        \
        goto end;                        \
                                    \
    tmp = kstrndup(page, len, GFP_KERNEL);                \
    if (tmp) {                            \
        ret = -ENOMEM;                        \
        goto end;                        \
    }                                \
    if (opts->name##_alloc)                        \
        kfree(opts->name);                    \
    opts->name##_alloc = true;                    \
    opts->name = tmp;                        \
    ret = len;                            \
                                    \
end:                                    \
    mutex_unlock(&opts->lock);                    \
    return ret;                            \
}                                    \
                                    \
static struct f_uac1_opts_attribute f_uac1_opts_##name =        \
    __CONFIGFS_ATTR(name, S_IRUGO | S_IWUSR,            \
            f_uac1_opts_##name##_show,            \
            f_uac1_opts_##name##_store)

UAC1_STR_ATTRIBUTE(fn_play);
UAC1_STR_ATTRIBUTE(fn_cap);
UAC1_STR_ATTRIBUTE(fn_cntl);

static struct configfs_attribute *f_uac1_attrs[] = {
    &f_uac1_opts_req_buf_size.attr,
    &f_uac1_opts_req_count.attr,
    &f_uac1_opts_audio_buf_size.attr,
    &f_uac1_opts_fn_play.attr,
    &f_uac1_opts_fn_cap.attr,
    &f_uac1_opts_fn_cntl.attr,
    NULL,
};

static struct config_item_type f_uac1_func_type = {
    .ct_item_ops    = &f_uac1_item_ops,
    .ct_attrs    = f_uac1_attrs,
    .ct_owner    = THIS_MODULE,
};

static void f_audio_free_inst(struct usb_function_instance *f)
{
    struct f_uac1_opts *opts;

    opts = container_of(f, struct f_uac1_opts, func_inst);
    if (opts->fn_play_alloc)
        kfree(opts->fn_play);
    if (opts->fn_cap_alloc)
        kfree(opts->fn_cap);
    if (opts->fn_cntl_alloc)
        kfree(opts->fn_cntl);
    kfree(opts);
}

static struct usb_function_instance *f_audio_alloc_inst(void)
{
    struct f_uac1_opts *opts;

    opts = kzalloc(sizeof(*opts), GFP_KERNEL);
    if (!opts)
        return ERR_PTR(-ENOMEM);

    mutex_init(&opts->lock);
    opts->func_inst.free_func_inst = f_audio_free_inst;

    config_group_init_type_name(&opts->func_inst.group, "", &f_uac1_func_type);

    opts->req_buf_size = UAC1_OUT_EP_MAX_PACKET_SIZE;//200
    opts->req_count = UAC1_REQ_COUNT;            //256
    opts->audio_buf_size = UAC1_AUDIO_BUF_SIZE;//48000

    opts->fn_play = FILE_PCM_PLAYBACK;//    /dev/snd/pcmC0D0p
    opts->fn_cap = FILE_PCM_CAPTURE;//        /dev/snd/pcmC0D0c
    opts->fn_cntl = FILE_CONTROL;    //        /dev/snd/pcmC0D0p
    return &opts->func_inst;
}

static void f_audio_free(struct usb_function *f)
{
    struct f_audio *audio = func_to_audio(f);
    struct f_uac1_opts *opts;

    gaudio_cleanup(&audio->card);
    opts = container_of(f->fi, struct f_uac1_opts, func_inst);
    kfree(audio);
    mutex_lock(&opts->lock);
    --opts->refcnt;
    mutex_unlock(&opts->lock);
}

static void f_audio_unbind(struct usb_configuration *c, struct usb_function *f)
{
    usb_free_all_descriptors(f);
}

static struct usb_function *f_audio_alloc(struct usb_function_instance *fi)
{
    struct f_audio *audio;
    struct f_uac1_opts *opts;

    /* allocate and initialize one new instance */
    audio = kzalloc(sizeof(*audio), GFP_KERNEL);
    if (!audio)
        return ERR_PTR(-ENOMEM);

    audio->card.func.name = "g_audio";

    opts = container_of(fi, struct f_uac1_opts, func_inst);
    mutex_lock(&opts->lock);
    ++opts->refcnt;
    mutex_unlock(&opts->lock);
    INIT_LIST_HEAD(&audio->play_queue);
    spin_lock_init(&audio->lock);

    audio->card.func.bind = f_audio_bind;
    audio->card.func.unbind = f_audio_unbind;
    audio->card.func.set_alt = f_audio_set_alt;
    audio->card.func.setup = f_audio_setup;
    audio->card.func.disable = f_audio_disable;
    audio->card.func.free_func = f_audio_free;

    control_selector_init(audio);

    INIT_WORK(&audio->playback_work, f_audio_playback_work);

    return &audio->card.func;
}

DECLARE_USB_FUNCTION_INIT(uac1, f_audio_alloc_inst, f_audio_alloc);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Bryan Wu");

0 篇笔记 写笔记

USB-UAC麦克风 音频控制接口
音频控制接口描述符描述了设备的结构(拓扑结构),并通过特定类请求对音频的控制。UAC麦克风 音频控制接口描述符偏移地址字段长度值描述0bLength10x09接口描术符的长度1bDescriptorType10x04描述符的类型接口描述符2bInterfaceNumber10x00接口ID号3bA......
USB音箱 UAC设备描述符分析
这里看到,USB规范采用的是USB1.1版本,不过字符串中又显示的是”USB2.0 Device”,所以这里就有点迷~关于设备描述符各字段的详细解释,详见USB规范中设备描述符一节。从bDeviceClass,bDeviceSubClass和bDeviceSubClass都被置为0来看,这是一个典......
UAC 类特定音频控制接口头描述符
UAC类特定音频控制头接口描述符这个名字有点绕,其实这个描述符是前接标准的音频控制接口描述符,后续关于音频控制的所有相关描述符,起着承上起下的作用。当然也可以认为是音频控制相关描述符的前导。这是因为UAC类特定音频控制接口描述符含有一个关键的字段wTotalLength,用于包含音频控制所有接口描述......
华为UAC耳机 音频控制接口
音频控制接口占用接口ID=0,音频控制接口的描述符结构布局如下:USB标准接口描述符UAC音频控制接口头描述符IDSId描述    音频控制输入终端描述符1USB Streaming   ......
华为UAC耳机的工作过程数据分析
华为UAC耳机工作时,首先需要打开设备,然后读取数据,播放过程后,需要关闭设备。这里我们使用BusHound抓包(省略掉重复的数据包:由于这些特定类请求是发给UAC音频控制终端/实体或接口的,所以我们先回顾一下UAC音频控制单元的一些ID,然后对照其选择子进行分析。UAC音频控制接口头描述符ID描述......
UAC 文档下载
UAC规范1.0文档下载地址:https://www.usb.org/sites/default/files/audio10.pdfUAC规范2.0文档下载地址:https://www.usb.org/sites/default/files/Audio2.0_final.zipUAC规范3.0及3.......
UAC 音频数据格式FORMAT_TYPE_3
下面来介绍USB Audio Data Formats 的第一类音频数格式 FORMAT_TYPE_III = 0x03Audio Data Format Type III Codes 其下又分为5种,分别为:NamewFormatTagTYPE_III_UNDEFINED0x2000IEC1937......
UAC 音频控制
一个USB设备可能包含多个配置。像手机一样,当手机通过USB线缆接入PC机后,会弹出一个选择对话框:让用户选择。当然一个USB设备只能工作在一种配置描述符下。对于每一个USB配置描述符,可能含有多个USB接口描述描述符,同时这些接口描述符可能每个接口描述符又包含多个转换接口描述符。这些接口描述符可能......
USB音箱 UAC Speaker 概述
手头有一个USB Speaker,插入电脑后在设备管理器中如下:从设备管理器中来看,这是一个单一功能的Usb Speaker。其硬件设备ID如下:USBVID_1908&PID_2070&REV_0100(USB Composite Device)USBVID_1908&......
USB音箱 UAC配置描述符分析
该USB音箱的配置描述符和普通USB设备描述符结构体一致,并无特别区别。该配置描述符的总长度(包括后续的所有其它描述符)为110字节,这里相对华为耳机的要少了很多,这是因为少了像麦克风和HID。从配置描述符的字段bmAttributes来看,并不支持远程唤醒功能。 ------------......
华为UAC耳机 配置描述符
配置描述符的大小其实不光包括配置描述符自身,也包括后续的所有描述符。这里我们只先介绍一下配置描述符,后续的接口描述符和其它UAC相关描述符在后续一节介绍。配置描述符的内容如下: ------------------ Configuration Descriptor --------------......
UAC 其它速率配置描述符
UAC规范中,并没有定义特别的其它速率配置描述符,故UAC音频设备的其它速率描述符应符合USB规范中的其它速率配置描述符。......
UAC Feedback端点
下面转一段对USB feedback的理解:这段时间一直在做USB Audio Device(UAC)设备的开发工作。由于UAC采用的是isochronous endpoint来传输数据,对时钟的要求较高。但无奈我们的嵌入式平台的时钟并不准,数据同步就成了问题。经过研究,发现只能使用异步模式来解决这......
USB-UAC麦克风 配置描述符
UAC麦克风采用的配置描述符结构和USB配置描述符的结构一致.偏移地址字段长度值描述0bLength10x09配置描述符的长度1bDescriptorType10x02描述符类型,配置描述符类型2wTotalLength20x0064配置描述符的总长度(包括后续的UAC音频控制和UAC音频流接口描述......
华为UAC麦克风的工作过程数据分析
看完了耳机的数据分析,再来分析麦克风,就相对来说很简单了,这是因为:第一:麦克风没有音频控制特效单元描述符,所以少了很多特定类请求。第二:麦克风只有一个音频流转换接口,且只支持采样率为48000HZ 16位。具体的过程见下:Length Phase Data-------- --......
关注公众号
  • HID人机交互
  • Linux&USB
  • UAC音频
  • CDC
  • TYPE-C
  • USB规范
  • USB大容量存储
  • USB百科
  • USB周边
  • UVC摄像头
  • Windows系统USB
  • 音视频博客
  • 取消
    感谢您的支持,我会继续努力的!
    扫码支持
    扫码打赏,你说多少就多少

    打开支付宝扫一扫,即可进行扫码打赏哦

    您的支持,是我们前进的动力!