linux USB 系统 (7)

1、 Linux中的USB设备驱动

我们再看看下面的图,我们基本了解了一下EHCI和如何将EHCI驱动起来,上EHCI驱动上面是USB核心,这一块是USB中最复杂的一块了,所幸他是与硬件无关的,作为一个普普通通的驱动工程师,只需要知道他提供了什么样的接口,以及如何使用这些接口,我们甚至不需要知道USB是如何工作的,只要放心大胆的使用这些USB核心层的API,把USB当作通路来使用。

当然这只是理想的状态,所谓的理想就是永远也无法实现的现实,当调试USB时我们还是需要从头到尾的把USB协议研究一遍。

只有站在USB核心层上,我们才能清晰的看到一般USB书籍中提到的端点,接口,通道和四种传输内型(控制传输,中断传输,批量传输,等时传输)等这些逻辑上的概念。

clip_image002

在linux的架构中,有一种数据数据叫URB封装了所有了这些概念,作为USB设备驱动,要使用USB通路来传递数据,我们只要操控URB就可以了。

typedef struct urb //USB Request Block,包含了建立任何 USB传输所需的所有信息,并贯穿于USB协议栈对数据处理的整个过程

{

       spinlock_t lock;         & nbsp;    // lock for the URB

       void *hcpriv;                // private data for host controller与主机控制器相关数据,对USB内核层是透明

       struct list_head urb_list;       // list pointer to all active urbs双向指针,用于将此URB连接到处于活动的URB双向链表中

       struct urb *next;             // pointer to next URB 指向下一个URB的指针

       struct usb_device *dev;       // pointer to associated USB device 接受此URB的USB设备指针

       unsigned int pipe;// pipe information表示设备的某个端点和客户端驱动程序之间的管道

       int status;          ;           ;  // returned status 返回状态

       unsigned int transfer_flags;       // USB_DISABLE_SPD | USB_ISO_ASAP | etc.

USB_DISABLE_SPD   //拒绝短数据包,即比最大传输包长度小的数据包

USB_ISO_ASAP     //用于实时传输,告诉主机控制器立即进行此请求的数据传输。如果没有置位,则需要给start_frame赋值,用来通知主机控制器该在哪个帧上开始此请求的数据传输

USB_ASYNC_UNLINK  //告诉USBD采用异步方式取消请求

USB_QUEUE_BULK    //表示批量请求可以排队,一般一个设备的批量请求端点只有一个URB

USB_NO_FSBR       //表示全速传输站用的带宽不要回收

USB_ZERO_PACKET //表示批量传输的数据长度等于端点的包最大长度时,主机控制器在发送完数据后,再发送一个零长度的包表示数据结束

USB_TIMEOUT_KILLED //本位只被HCD设置,表示发生了超时。客户驱动可以给URB的处理设置一个超时时间,如果处理超时,则要求USBD结束对此URB的处理,URB的返回信息中会反映该此状态。

       void *transfer_buffer ;           ;  // associated data buffer传输数据缓存区指针,接收或发送设备的数据,它必须是物理连续的,不可换页的内存块,用kmalloc(,GFP_KERNEL)分配
       int transfer_buffer_length;     // data buffer length缓存区长度

       int actual_length;      // actual data buffer length 实际数据长度

       int bandwidth;                  //此请求每次占用一帧的带宽,只适用实时/中断传输
       unsigned char *setup_packet;      //用于指向控制传输中控制命令的指针,只适用控制传输

       int start_frame;    //此请求所开始传输的帧号,只适用实时/中断传输。中断传输时,表示返回启动此请求的第一次中断传输的帧号。实时传输时,指明处理第一个实时请求数据报包的帧号,如果设置了USB_ISO_ASAP,此变量表示返回启动第一次实时传输的帧号。

       int number_of_packets;  // number of packets in this request (iso)此请求所包含的数据包数,只适合实时传输

       int interval; // polling interval (irq only) 中断传输的周期,1〈= interval〈=255

       int error_count;   // number of errors in this transfer (iso only)发生传输错误次数的累加值,只适用实时传输

       int timeout;       // timeout (in jiffies)      

       void *context;               // context for completion routine回调函数中的参数

       usb_complete_t complete;       // pointer to completion routine 指向回调函数的指针。当数据传输完成后,主机控制器驱动会回调该函数。

       iso_packet_descriptor_t iso_frame_desc[0]; 要进行实时传输的结构数组,每个结构表示一次数据传输

}

 

上表中就是URB的具体内容,URB对USB协议解析得己经很清楚了,但是还是很复杂,我们需要更要更有利的工具,内核己经提供了这类操作URB的工具:

usb_fill_control_urb(struct urb *urb,

struct usb_device *dev,

unsigned int pipe,

unsigned char *setup_packet,

void *transfer_buffer,

int buffer_length,

usb_complete_t complete_fn,

void *context)

初始化控制URB结构,pipe是端点通道,setup_packet指向setup_packet数据,transfer_buffer指向transfer数据,

 

usb_fill_bulk_urb(struct urb *urb,

struct usb_device *dev,

unsigned int pipe,

void *transfer_buffer,

int buffer_length,

usb_complete_t complete_fn,

void *context)

初始化块传输的URB

 

usb_fill_int_urb(struct urb *urb,

struct usb_device *dev,

unsigned int pipe,

void *transfer_buffer,

int buffer_length,

usb_complete_t complete_fn,

void *context,

int interval)

初始化中断传输的URB

 

usb_control_msg(struct usb_device *dev, unsigned int pipe,

__u8 request, __u8 requesttype, __u16 value, __u16 index,

void *data, __u16 size, int timeout);

  

usb_interrupt_msg(struct usb_device *usb_dev, unsigned int pipe,

void *data, int len, int *actual_length, int timeout);

  

usb_bulk_msg(struct usb_device *usb_dev, unsigned int pipe,

void *data, int len, int *actual_length,

int timeout);

  

extern void usb_init_urb(struct urb *urb);

extern struct urb *usb_alloc_urb(int iso_packets, gfp_t mem_flags);

extern void usb_free_urb(struct urb *urb);

extern struct urb *usb_get_urb(struct urb *urb);

extern int usb_submit_urb(struct urb *urb, gfp_t mem_flags);

extern int usb_unlink_urb(struct urb *urb);

extern void usb_kill_urb(struct urb *urb);

extern void usb_poison_urb(struct urb *urb);

extern void usb_unpoison_urb(struct urb *urb);

extern void usb_kill_anchored_urbs(struct usb_anchor *anchor);

usb_poison_anchored_urbs(struct usb_anchor *anchor);

extern void usb_unpoison_anchored_urbs(struct usb_anchor *anchor);

extern void usb_unlink_anchored_urbs(struct usb_anchor *anchor);

extern void usb_anchor_urb(struct urb *urb, struct usb_anchor *anchor);

extern void usb_unanchor_urb(struct urb *urb);

extern int usb_wait_anchor_empty_timeout(struct usb_anchor *anchor,

unsigned int timeout);

extern struct urb *usb_get_from_anchor(struct usb_anchor *anchor);

extern void usb_scuttle_anchored_urbs(struct usb_anchor *anchor);

extern int usb_anchor_empty(struct usb_anchor *anchor);

  

对于pipe的创建及操作,内核中也有定义:

#define PIPE_ISOCHRONOUS 0

#define PIPE_INTERRUPT 1

#define PIPE_CONTROL 2

#define PIPE_BULK 3

#define usb_pipein(pipe)

#define usb_pipeout(pipe)

#define usb_pipedevice(pipe)

#define usb_pipeendpoint(pipe)

#define usb_pipetype(pipe)

#define usb_pipeisoc(pipe)

#define usb_pipeint(pipe)

#define usb_pipecontrol(pipe)

#define usb_pipebulk(pipe)

#define usb_gettoggle(dev, ep, out)

#define usb_dotoggle(dev, ep, out)

#define usb_settoggle(dev, ep, out, bit)

#define usb_sndctrlpipe(dev,endpoint)

#define usb_rcvctrlpipe(dev,endpoint)

#define usb_sndisocpipe(dev,endpoint)

#define usb_rcvisocpipe(dev,endpoint)

#define usb_sndbulkpipe(dev,endpoint)

#define usb_rcvbulkpipe(dev,endpoint)

#define usb_sndintpipe(dev,endpoint)

#define usb_rcvintpipe(dev,endpoint)

  

上面这些工具都是usb核心层提供给我们的,我们只需在逻辑层上把USB看作一个个的pipe就可以了,USB从设备中也会有这样的一些概念,我们其实不是与从设备的硬件直接打交道,而是和从设备中的USB固件(usb从控制器的驱动)打交道。

设备驱动想要使用usb总线和设备通信,一般先要初始化urb结构,把你所想要传送的数据用系统提供的urb操作工具填入urb中,然后用 usb_submit_urb向usb核心提交。

我们想要了解usb设备驱动层的数据是如何到达USB主控制器并发送到总线上去的,usb_submit_urb是一个很好的突破口。

usb_submit_urb中全调用usb_hcd_submit_urb,usb_hcd_submit_urb会找到预先指定的控制器驱动,即调用hcd->driver->urb_enqueue(),对ehci控制器而言, urb_enqueue就是ehci_hcd.c中的ehci_urb_enqueue(),数据走到ehci_urb_enqueue(),接下来的事情我们就能很清楚了,我们前介绍过itd/sitd/qh/qtd/fstn这几种在ehci sepc规范中定义的数据模型,也介绍了这几种数据模型在linux kernel中的表示,一句话,ehci_urb_enqueue()要作的事就是把设备驱动层交给urb的数据填充到itd/sitd/qh/qtd/fstn这几种数据结构中,并将其链成调度表。

我们来看看这个函数的代码:

static int ehci_urb_enqueue (

struct usb_hcd *hcd,

struct urb *urb,

gfp_t mem_flags

) {

struct ehci_hcd *ehci = hcd_to_ehci (hcd);

struct list_head qtd_list;

INIT_LIST_HEAD (&qtd_list);

switch (usb_pipetype (urb->pipe)) {

case PIPE_CONTROL:

/* qh_completions() code doesn't handle all the fault cases

* in multi-TD control transfers. Even 1KB is rare anyway.

*/

if (urb->transfer_buffer_length > (16 * 1024))

return -EMSGSIZE;

/* FALLTHROUGH */

/* case PIPE_BULK: */

default:

if (!qh_urb_transaction (ehci, urb, &qtd_list, mem_flags))

return -ENOMEM;

return submit_async(ehci, urb, &qtd_list, mem_flags);

case PIPE_INTERRUPT:

if (!qh_urb_transaction (ehci, urb, &qtd_list, mem_flags))

return -ENOMEM;

return intr_submit(ehci, urb, &qtd_list, mem_flags);

case PIPE_ISOCHRONOUS:

if (urb->dev->speed == USB_SPEED_HIGH)

return itd_submit (ehci, urb, mem_flags);

else

return sitd_submit (ehci, urb, mem_flags);

}

}

 

接触usb的人都知道USB的传输分为中断传输,控制传输,批量传输,等时传输。基本上所有的人都知道这几种传输的概念上的区别,但却没几个人能了解这种区别的具体实现,以及形成区别的原因,等时传输和中断传输为什么会有时效性,批量传输和等时传输在实现在有什么区别呢。

USB设备驱动URB数据直到流到了 ehci_urb_enqueue才各回各家,各找各妈。

控制传输数据由submit_async处理进入了qtd队列;

中断传输数据由intr_submit处理被打包成qtd挂在itd队列上。

等时数据则由itd_submit处理,打包成itd队列。如果是支持低/全速设备,还有一个sitd_submit的处理,生成sitd队列。

列表准备好了后,乘下的就是要对寄存器操作了,以submit_async为例,这个关键的动作由qh_link_async()这个函数完成:

static void qh_link_async (struct ehci_hcd *ehci, struct ehci_qh *qh)

{

__hc32 dma = QH_NEXT(ehci, qh->qh_dma);

struct ehci_qh *head;

/* (re)start the async schedule? */

head = ehci->async;

timer_action_done (ehci, TIMER_ASYNC_OFF);

if (!head->qh_next.qh) {

u32 cmd = ehci_readl(ehci, &ehci->regs->command);

if (!(cmd & CMD_ASE)) {

/* in case a clear of CMD_ASE didn't take yet */

(void)handshake(ehci, &ehci->regs->status,

STS_ASS, 0, 150);

cmd |= CMD_ASE | CMD_RUN;

ehci_writel(ehci, cmd, &ehci->regs->command);

ehci_to_hcd(ehci)->state = HC_STATE_RUNNING;

/* posted write need not be known to HC yet ... */

}

}

/* clear halt and/or toggle; and maybe recover from silicon quirk */

if (qh->qh_state == QH_STATE_IDLE)

qh_refresh (ehci, qh);

/* splice right after start */

qh->qh_next = head->qh_next;

qh->hw_next = head->hw_next;

wmb ();

head->qh_next.qh = qh;

head->hw_next = dma;

qh->qh_state = QH_STATE_LINKED;

/* qtd completions reported later by interrupt */

}

 

在组织完qtd队列后,我们就把ehci的控制COMMAND寄存器的 CMD_ASE 和 CMD_RUN字段使能,ehci就开始调度(运行)了。

这里我们并没有看见让ASYNCLISTADDR指向qh队列头,这件事其实早就做好了,看下面的函数:

usb/host/ehci_hcd.c

 

/* start HC running; it's halted, ehci_init() has been run (once) */

static int ehci_run (struct usb_hcd *hcd)

{

struct ehci_hcd *ehci = hcd_to_ehci (hcd);

......

if ((retval = ehci_reset(ehci)) != 0) {

ehci_mem_cleanup(ehci);

return retval;

}

ehci_writel(ehci, ehci->periodic_dma, &ehci->regs->frame_list);

ehci_writel(ehci, (u32)ehci->async->qh_dma, &ehci->regs->async_next);

 

至此,看到对ASYNCLISTADDR和FRINDEX两个寄存器的操作,所有与EHCI控制器有关的疑问都应该解决了。我们可以放心的在设备驱动中使用usb总线了。

从设备中往往会有endpoint和pipe的概念,USB主机设备驱动层也使用endpoint和pipe的概念,这样主从之间就对上话了,通讯就没有问题了。

一般而言,数据通路被协议还可以理解,但USB规范中,设备驱动也被协议了。

我们看看前面那张表

1-audio:表示一个音频设 备。

 

2-communication device:通讯设备,如电话,moden等等。

 

3-HID:人机交互设备,如键盘,鼠标等。

 

6-image图象设备,如扫描仪,摄像头等,有时数码相 机也可归到这一类。

 

7-打印机类。如单向,双向打印机等。

 

8-mass storage海量存储类。所有带有一定存储功能的都可以归到这一类。如数码相机大多数都归这一类。

 

9-hub类。

 

11-chip card/smart card。

 

13 --Content Security

 

14--Video (Interface)

 

15--Personal Healthcare

 

220--Diagnostic Device

 

224--Wireless Controller (Interface)

 

239--Miscellaneous

 

254--Application Specific (Interface)

 

255-vendor specific.厂家的自定义类,主要用于一些特殊的设备。如接口转接卡等。

 

上表中的设备和接口在USB规范中都有规定,这可能是USB相关的东西调试起来很复杂吧,不像I2C/PCI那样好理解,所以干脆USB社区连设备的规范都定了下来,这样就可以生产出许多免驱动的设备了。

我们先看看linux中的usb设备的描述

struct usb_device {           ;  //代表一个USB设备
      int devnum;   ;           ;  //分配的设备地址,1-127

      enum {

               USB_SPEED_UNKNOWN = 0,               /* enumerating */

               USB_SPEED_LOW, USB_SPEED_FULL,              /* usb 1.1 */

               USB_SPEED_HIGH                       /* usb 2.0 */

       } speed;                         //设备速度,低速/全速/高速

       struct usb_device *tt;             /* usb1.1 device on usb2.0 bus */,事务处理解释器

       int ttport;          ;      /* device/hub port on that tt */设备所连接的具有事务处理解释器功能的集线器端口

       atomic_t refcnt;          ;   /* Reference count */引用计数

       struct semaphore serialize; //用于同步

       unsigned int toggle[2] ;           ;    /* one bit for each endpoint ([0] = IN, [1] = OUT) */用于同步切换的位图,每个端点占用1位,[0]表示输入,[1]输出

       unsigned int halted[2];        /* endpoint halts; one bit per endpoint # & direction;  [0] = IN, [1] = OUT */表示端点是否处于停止状态的位图

       int epmaxpacketin[16] ;           ;/* INput endpoint specific maximums */输入端点的最大包长

       int epmaxpacketout[16] ;           ;    /* OUTput endpoint specific maximums */输出端点的最大包长

       struct usb_device *parent;   //表示设备所连的上游集线器指针

       struct usb_bus *bus;        /* Bus we're part of */设备所属的USB总线系统

       struct usb_device_descriptor descriptor;/* Descriptor */ 设备描述符

       struct usb_config_descriptor *config; /* All of the configs */指向设备的配置描述符和其所包含的接口描述符,端点描述符的指针

       struct usb_config_descriptor *actconfig;/* the active configuration */当前的配置描述符指针

       char **rawdescriptors;   ;           ;/* Raw descriptors for each config */

       int have_langid;            /* whether string_langid is valid yet *// 是否有string_langid

       int string_langid;        /* language ID for strings */和字符描述符相关的语言ID

      void *hcpriv;   ;           ;    /* Host Controller private data */设备在HCD层占用的资源指针,对USB内核层是透明的

    /* usbdevfs inode list */ 设备在usbdevfs中的inode列表

       struct list_head inodes;

       struct list_head filelist;

       /*

        * Child devices - these can be either new devices

        * (if this is a hub device), or different instances

        * of this same device.

        *

        * Each instance needs its own set of data structures.

        */只对当前设备是集线器的情况有效

      int maxchild;           /* Number of ports if hub */ hub的下游端口数

       struct usb_device *children[USB_MAXCHILDREN]; hub所连设备指针

};

struct usb_bus { //USB总线系统

       int busnum;          ;           ;  /* Bus number (in order of reg) */当前总线系统的序列号,Linux支持多总线系统并为它们编号

#ifdef DEVNUM_ROUND_ROBIN

       int devnum_next;     /* Next open device number in round-robin allocation */

#endif /* DEVNUM_ROUND_ROBIN */给连接到子系统上的设备分配设备号的数据结构

       struct usb_devmap devmap;       /* Device map */给连接到子系统上的设备分配设备号的数据结构

       struct usb_operations *op;      /* Operations (specific to the HC) */HCD为USB内核提供的一系统函数集指针

       struct usb_device *root_hub;    /* Root hub */指向根Hub的指针

       struct list_head bus_list;       双向链表指针,USB内核用一个双向链表来维护系统中所有USB总线系统

       void *hcpriv; /* Host Controller private data */与主机控制器相关数据,对USB内核层是透明

       int bandwidth_allocated;       /* on this Host Controller; applies to Int. and Isoc. pipes; measured in microseconds/frame; range is 0..900, where 900 = 90% of a 1-millisecond frame */当前子系统的带宽使用情况,单位是微秒/帧,取值范围[0,900]

       int bandwidth_int_reqs;        ; /* number of Interrupt requesters */子系统中当前的中断传输的数量

       int bandwidth_isoc_reqs;       /* number of Isoc. requesters */子系统中当前的实时传输的数量

       /* usbdevfs inode list */ 在usbdevfs中的inode列表       struct list_head inodes;

       atomic_t refcnt;

};

struct usb_driver { //客户端驱动程序为USB内核提供的调用接口

       const char *name;    //客户端驱动程序的字符串名称,用于避免重复安装和卸载

       void *(*probe)(//给USB内核提供的函数,用于判断驱动程序是否能对设备的某个接口进行驱动,如能则分配资源

           struct usb_device *dev,     ;           ;/* the device */

           unsigned intf,   ;           ;    /* what interface */

           const struct usb_device_id *id   /* from id_table */

           );

       void (*disconnect)(struct usb_device *, void *);//给USB内核提供的函数,用于释放设备的某个接口所占用的资源

       struct list_head driver_list;//对应的双向指针,USB内核通过一个双向指针链表维护USB子系统中所用的客户端驱动程序

       struct file_operations *fops;

       int minor; 驱动的次版本号

       struct semaphore serialize;

       /* ioctl -- userspace apps can talk to drivers through usbdevfs */

       int (*ioctl)(struct usb_device *dev, unsigned int code, void *buf);

       /* support for "new-style" USB hotplugging

        * binding policy can be driven from user mode too

        */

       const struct usb_device_id *id_table;

       /* suspend before the bus suspends;

        * disconnect or resume when the bus resumes */

       // void (*suspend)(struct usb_device *dev);

       // void (*resume)(struct usb_device *dev);

};

 

看来,USB设备驱动其实走的也是老套路,重点还是要放在与设备的通讯上。

与设备相关的,usb规范又定义了许多东西,协议中把这些叫描述符,我觉得应该叫配置更合理些。如下图,设备的配置,配置(Configuration)的配置,接口的配置,端点的配置。

 
 clip_image004

USB规范把设备分成了许多类,特定类(class)的设备又可划分成子类描述符(subclass),划分子类的后软件就可以搜索总线并选择所有它可以支持的设备,一个设备只有一个(Device)描述符,它指明了设备所属的类,

每个设备可以有一个或多个配置描述符(Configuration),配置描述符用于定义设备的功能。如果某个设备有几种不同的功能,则每个功能都需要一个配置描述符。配置描述符(configuration)是接口描述符(interface)的集合。接口指定设备中的哪些硬件与USB交换数据。每一个与USB交换数据的硬件就叫做一个端点描述符(endpoint)。因此,接口是端点的集合。USB的设备类别定义(USB Device Class Definitions)定义特定类或子类中的设备需要提供的缺省配置、接口和端点.

USB设备驱动中在打通数据通路后,就要理清各种配置了,根据这些配置再与设备作下一步的交流。

USB设备的识别过程

有了上面的准备知识,我们可以看一看当一个USB设备接入后是如何被识别的。

USB系统是一种主从结构,所有的通讯都是host发起的,从设备永远处于被动状态,但在主设备还是需要一个中断来唤醒。

1)如果是OTG设备,接入USB从设备时,USB-ID线被拉低,OTG中断触发,OTG将控制器切换到host状态。

2)当控制器处于host状态开处于待机时(otg的ADP,ARP完成),因为设备的D+和D-线上有特殊的处理,低速设备上的D-上有下拉电阻,高速和全速设备的D+上有上拉电阻,集线器检测到这个变化上报中断给探制器。

3)主机使用0地址通道来向总线上的新设备进查询,取得设备的一些基本的信息。

4)主机发取得设备的基本信息后发出复位指令,并给设备分配一个地址。设备复位并有了新的地址。

5)主机用新的地址继续查询设备的各种描述符(配置),主机通过设备的这些配置为设备在主机上寻找一个驱动。

6)一旦找到设备的驱动程序,枚举过程就完成了,控制权交到设备驱动的手上,设备在设驱动的控制下工作。

上面的交互过程其实很复杂,还可以细分。我们需要注意的是在主机给设备分配地址前设备使用的是0号地址,这是个公共地址,任何设备在刚接入的时该都会使用这个地址,并且一次只能有一个设备在使用0地址,主要发出复位指令时才会给设备分配一个新的地址。设备枚举的过程主要是对设备的各种描述符进行解析,最后找到设备驱动程序。

USB设备驱动和USB gadge驱动应该是我们接触得最多的与usb相关的驱动,这也是初学者很容易混淆的两个概念,再看下面这张图。

 
 clip_image005

从图中可以看出,USB设备驱动和USB gadge驱动处在平等对话的位置,usb主机侧的usb核心层和USB设备侧的gadge层都是为了给这两者能够对上话提供服务。我们完全可以把USB核 心层和gadge层以下的部份当作一个黑盒子,这样USB设备驱动就和其它的设备驱动没有什么区别了,只是数据通路走的是USB总线。

在linux中,使用usb总线作为数据通路最重要的一点就是控制urb,在驱动层,对usb的控制就是操控urb,控制urb的api前面己经列举过,有两点需要特别注意,一是等时传输的urb要手动初始化。二是中断传输的urb中有一个回调函数,这个回调函数会在有中断传输数据到达时被usb核心调用。内核中有一个drivers/usb/usb-skeleton.c的例子,是一个很好的参照。

usb-gadge就要稍稍复杂一点,除了通路的问题,你可能还要准备好各种配置(设备描述符,配置描述符,端点描述符等)。最后不要忘了,USB设备驱动和gadge驱动有本质的区别,USB设备驱动是运行在主机侧,向用户层提供设备接口,而usb-gadge驱动则是作为一个设备的固件运行在设备侧。这是最基本的,如果这一点不清楚最好先不要碰USB。

usb-gadge驱动的技术和usb设备固件技术有很多相同之处,这也是中国研发人员投入精力最多的地方,市面上的很多USB的资料都会讲这一块,也只会讲这一块,很多资料中提到的USB控制器其实都是从控制器,用来设计USB设备的,这一点要特别注意。

 

注:转载请注明出处 datangsoc@hotmail.com

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值