usb驱动是linux内核中比较复杂的驱动之一,因此,大多数usb教程建议从usb-skeleton开始学习usb驱动。个人认为这是相当正确的,usb-sekelton提供了一个usb驱动开发的模板,而且代码量较少,很适合初学者的学习。

    记住,对于c语言的程序设计说,数据结构是整个程序的灵魂。因此,分析别人编写的代码的简洁的入口点就是高清代码中主要数据结构之间的关系。分析以usb-skeleton为例的完整的usb驱动框架,我们就从主要的几个数据结构入手。
 
一、usb驱动框架的主要数据结构
usb驱动框架主要包括设备,配置,接口和端点几个组成部分,相对应的数据结构为:
  1. struct usb_device;               //usb设备


  2. struct usb_host_config;           //usb配置

  3. struct usb_host_interface;       //usb接口

  4. struct usb_host_endpoit;          //usb端口
它们之间的关系可以形象的表示为图1。

上图描述的usb各组成部分的关系可以描述为:

(1)一个usb设备可以有多个usb配置,多个配置之间可以相互切换,某个时刻只能对应一个配置;

(2)一个usb配置可以有多个usb接口,一个usb接口代表了一个基本功能,对以一个usb客户端驱动程序;

(3)一个usb接口可以有多个usb端点;

就像我们平时程序设计经常使用的方法一样,一个对象由一个结构体来表示,但还会再用来一个结构体来描述这个对象的一些属性。usb驱动框架也采用了这样的设计思想,usb框架中每一个组成部分都用两个结构体来描述:一个结构体表示成员组成,另一个结构体表示属性组成。Linux-USB核心定义了4个usb描述符。

  1. struct usb_device_descriptor;<-->struct usb_device;
  2. struct usb_host_config;<-->struct usb_config_descriptor;
  3. struct usb_host_interface;<-->struct usb_interface_descriptor;
  4. struct usb_host_endpoint;<-->struct usb_endpoint_descriptor;

另外,还有一个比较特殊的数据结构是URB(USB Request Block),被USB协议栈使用,是USB数据传输机制的核心数据结构,具体的URB的类型,URB的使用步骤等读者可以参考《Linux设备驱动程序》一书,本文不做详细介绍。

二、USB设备的枚举过程

    USB设备是一种典型的热插拔设备,与PCI设备类似,当总线检测到有设备插入的时候,总线驱动程序就会去遍历总线上已经挂载的所有的设备驱动程序,查看有没有驱动程序与刚插入的设备匹配,如果匹配成功,则去执行驱动程序中的probe函数。这是Linux内核中经典的驱动和设备挂钩的方式之一。

三、分析代码的执行过程

    要理解代码的含义,最关键的是理清代码的执行路径,即代码中函数的调用关系。光靠source insight或eclipse代码分析工具有时候略显不够,对于那些没有直接调用关系的函数这些静态代码分析工具爱莫能助。最好方法是能看到代码一步一步的执行流程,那么,单步调试就是比较好的选择了。本文采用KVM+GDB的方式对usb-skeleton模块进行了单步调试。Linux内核调试环境的搭建参考文章《qemu+eclipse内核调试》,这里仅需要创建一个Makefile文件,编译usb-skeleton.c即可。KVM中usb设备的使用参考文章《KVM中使用usb设备》。需要注意的是,Linux内中默认的usb存储设备的驱动模块名称为usb-storage,在调试前,先卸载该模块。

四、usb-skeleton主要代码分析(Linux-2.6.35)

1 skel_probe函数

  1. static int skel_probe(struct usb_interface*interface,
  2.          const struct usb_device_id *id)
  3. {
  4.     struct usb_skel *dev;
  5.     struct usb_host_interface *iface_desc;
  6.     struct usb_endpoint_descriptor *endpoint;
  7.     size_t buffer_size;
  8.     int i;
  9.     int retval = -ENOMEM;

  10.     /* allocate memory for our device state and initialize it */
  11.     dev = kzalloc(sizeof(*dev), GFP_KERNEL);
  12.     if (!dev){
  13.         err("Out of memory");
  14.         goto error;
  15.     }
  16.     kref_init(&dev->kref);
  17.     sema_init(&dev->limit_sem, WRITES_IN_FLIGHT);
  18.     mutex_init(&dev->io_mutex);/IO操作互斥锁,在进行IO操作时,不允许进行其他操作,如数据拷贝,后面会提到/
  19.     spin_lock_init(&dev->err_lock);
  20.     init_usb_anchor(&dev->submitted);
  21.     init_completion(&dev->bulk_in_completion);/*comletion同步机制,即IO操作和数据拷贝的同步*/
  22.    
  23.     /*数据结构之间的转换,这里是usb_skel和usb_host_interface两个结构体之间的转换,相当于container_of宏*/
  24.     dev->udev= usb_get_dev(interface_to_usbdev(interface));
  25.     dev->interface= interface;

  26.     /* set up the endpoint information */
  27.     /* use only the first bulk-in and bulk-out endpoints */
  28.     /* 初始化dev设备的bulk-in和bulk-out相关的数据成员,两种数据类型只初始化一个*/
  29.     iface_desc = interface->cur_altsetting;
  30.     for (i= 0; i < iface_desc->desc.bNumEndpoints;++i){
  31.         endpoint = &iface_desc->endpoint[i].desc;

  32.         if (!dev->bulk_in_endpointAddr&&
  33.          usb_endpoint_is_bulk_in(endpoint)){
  34.             /* we found a bulk in endpoint */
  35.             buffer_size = le16_to_cpu(endpoint->wMaxPacketSize);
  36.             dev->bulk_in_size= buffer_size;
  37.             dev->bulk_in_endpointAddr= endpoint->bEndpointAddress;
  38.             dev->bulk_in_buffer= kmalloc(buffer_size, GFP_KERNEL);
  39.             if (!dev->bulk_in_buffer){
  40.                 err("Could not allocate bulk_in_buffer");
  41.                 goto error;
  42.             }
  43.             dev->bulk_in_urb= usb_alloc_urb(0, GFP_KERNEL);
  44.             if (!dev->bulk_in_urb){
  45.                 err("Could not allocate bulk_in_urb");
  46.                 goto error;
  47.             }
  48.         }

  49.         if (!dev->bulk_out_endpointAddr&&
  50.          usb_endpoint_is_bulk_out(endpoint)){
  51.             /* we found a bulk out endpoint */
  52.             dev->bulk_out_endpointAddr= endpoint->bEndpointAddress;
  53.         }
  54.     }
  55.     if (!(dev->bulk_in_endpointAddr&& dev->bulk_out_endpointAddr)){
  56.         err("Could not find both bulk-in and bulk-out endpoints");
  57.         goto error;
  58.     }

  59.     /* save our data pointer in this interface device */
  60.     /*不同上下文之间结构体传递的一种方式,在probe和open等其他函数之间通过usb_set_intfdata和usb_get_intfdata两个函数来保存和获取局部变量de*/
  61.     usb_set_intfdata(interface, dev);

  62.     /* we can register the device now, as it is ready */
  63.     /*执行完usb_register_dev之后,接口interface就会对应一个此设备号,至此与该接口对应的驱动注册完成,其他一些操作将会等到open时再进行*/
  64.     retval = usb_register_dev(interface,&skel_class);
  65.     if (retval){
  66.         /* something prevented us from registering this driver */
  67.         err("Not able to get a minor for this device.");
  68.         usb_set_intfdata(interface,NULL);
  69.         goto error;
  70.     }

  71.     /* let the user know what node this device is now attached to */
  72.     dev_info(&interface->dev,
  73.          "USB Skeleton device now attached to USBSkel-%d",
  74.          interface->minor);
  75.     return 0;

  76. error:
  77.     if (dev)
  78.         /* this frees allocated memory */
  79.         kref_put(&dev->kref, skel_delete);
  80.     return retval;
  81. }

此函数主要完成usb用户态驱动的注册即usb_class_driver的注册,即skel_class结构体的注册。另外,需要注意的是在驱动程序设计中probe()和open()两个函数的区别,即它们各自应该实现什么功能?个人认为主要理解以下几点:

(1)对每个驱动来讲probe函数只会执行一次,执行时机为驱动加载或设备枚举的时候,用来实现设备和驱动的匹配。从这方面来讲,probe函数的执行函数应该尽可能的短,因此,操作越少越好。

(2)open()函数是每次打开某设备时都要执行的函数,如果系统中有多个进程都在使用某个设备,那么,open()函数就有可能执行多次,从这个角度来讲,open()函数主要应该坐与可冲入相关的操作,即让每个进程看来都是像第一次打开设备一样,其他进程对设备的某些值的修改不应该被其他进程看到。即相当于每次都虚拟了一个实际的硬件设备。

从这两方面将,如果不考虑probe的执行时间,如果不会存在多个使用同一设备的进程,完全可以将probe和open合并。

2 skel_open()函数

  1. static int skel_open(struct inode*inode, struct file *file)
  2. {
  3.     struct usb_skel *dev;
  4.     struct usb_interface *interface;
  5.     int subminor;
  6.     int retval = 0;

  7.     subminor = iminor(inode);
  8.     /*通过驱动和此设备号去查找对应的设备,类此pci设备,根据驱动结构体,在总线的device链表上查找这个驱动对应的设备,在usb驱动架构中即指对应的借口。*/
  9.     interface = usb_find_interface(&skel_driver, subminor);
  10.     if (!interface){
  11.         err("%s - error, can't find device for minor %d",
  12.          __func__, subminor);
  13.         retval = -ENODEV;
  14.         goto exit;
  15.     }
  16.     /*获得在probe函数中保存的局部变量usb_skel dev*/
  17.     dev = usb_get_intfdata(interface);
  18.     if (!dev){
  19.         retval = -ENODEV;
  20.         goto exit;
  21.     }

  22.     /* increment our usage count for the device */
  23.     kref_get(&dev->kref);

  24.     /* lock the device to allow correctly handling errors
  25.      * in resumption */
  26.     mutex_lock(&dev->io_mutex);

  27.     if (!dev->open_count++){/*与电源管理相关的代码,可以暂时不去分析*/
  28.         retval = usb_autopm_get_interface(interface);
  29.             if (retval) {
  30.                 dev->open_count--;
  31.                 mutex_unlock(&dev->io_mutex);
  32.                 kref_put(&dev->kref, skel_delete);
  33.                 goto exit;
  34.             }
  35.     } /* else { //uncomment this block if you want exclusive open
  36.         retval = -EBUSY;
  37.         dev->open_count--;
  38.         mutex_unlock(&dev->io_mutex);
  39.         kref_put(&dev->kref, skel_delete);
  40.         goto exit;
  41.     } */
  42.     /* prevent the device from being autosuspended */

  43.     /* save our object in the file's private structure */
  44.     /*这里需要注意,前面讲过在probe和open等函数之间传递私有数据用的是两个函数usb_set_intfdata()和usb_get_intfdata, 那么,在open函数和其他函数如read/write之间传递私有数据就是通过file->private_data变量来实现的*/
  45.     file->private_data= dev;
  46.     mutex_unlock(&dev->io_mutex);

  47. exit:
  48.     return retval;
  49. }

从上面的代码我们可以发现,open函数归根结底其实只做了一件事情:保存了私有变量usb_skel dev,是的其他文件操作函数可用。

3.skel_read()函数


  1. static ssize_t skel_read(structfile *file,char *buffer,size_t count,

  2. loff_t *ppos)

  3. {

  4. struct usb_skel *dev;

  5. int rv;

  6. bool ongoing_io;



  7. dev = (struct usb_skel*)file->private_data;



  8. /* if we cannot read at all, return EOF */

  9. if (!dev->bulk_in_urb|| !count)

  10. return 0;



  11. /* no concurrent readers */

  12. /*I/O操作的互斥锁,每次读操作前先去获得此锁,防止读操作和IO之间的并发进行。即如果读操作获得了此锁,IO操作就不能进行,同样的,如果该锁已经被IO操作获得,则当前执行流程睡眠,直到IO操作完成,释放此锁*/

  13. rv = mutex_lock_interruptible(&dev->io_mutex);

  14. if (rv < 0)

  15. return rv;



  16. if (!dev->interface){ /* disconnect() was called */

  17. rv = -ENODEV;

  18. goto exit;

  19. }



  20. /* if IO is under way, we must not touch things */

  21. retry:

  22. spin_lock_irq(&dev->err_lock);

  23. ongoing_io = dev->ongoing_read;/*获得当前的IO状态*/

  24. spin_unlock_irq(&dev->err_lock);



  25. if (ongoing_io){/*如果usb的I/O操作正在进行中,要等待I/O操作执行完成*/

  26. /* nonblocking IO shall not wait */

  27. if (file->f_flags& O_NONBLOCK){

  28. rv = -EAGAIN;

  29. goto exit;

  30. }

  31. /*

  32. * IO may take forever

  33. * hence wait in an interruptible state

  34. */
  35. /*睡眠等待IO操作完成*/
  36. rv = wait_for_completion_interruptible(&dev->bulk_in_completion);

  37. if (rv < 0)

  38. goto exit;

  39. /*

  40. * by waiting we also semiprocessed the urb

  41. * we must finish now

  42. */

  43. dev->bulk_in_copied= 0;/*如果正在进行IO操作,说明当前URB对应的缓冲区没有数据可用了,所以copied=0*/

  44. dev->processed_urb= 1;/*z执行到这的时候IO操作已经完成了,这里要标记下开始处理URB了*/

  45. }



  46. if (!dev->processed_urb){/*等于0时,说明这个URB还没被处理过,即第一次读取这个URB*/

  47. /*

  48. * the URB hasn't been processed

  49. * do it now

  50. */

  51. /*这里为什么还要等待,什么情况下需要等待?????*/

  52. wait_for_completion(&dev->bulk_in_completion);

  53. dev->bulk_in_copied= 0;

  54. dev->processed_urb= 1;

  55. }



  56. /* errors must be reported */

  57. rv = dev->errors;

  58. if (rv < 0) {

  59. /* any error is reported once */

  60. dev->errors= 0;

  61. /* to preserve notifications about reset */

  62. rv = (rv== -EPIPE) ? rv: -EIO;

  63. /* no data to deliver */

  64. dev->bulk_in_filled= 0;

  65. /* report it */

  66. goto exit;

  67. }



  68. /*

  69. * if the buffer is filled we may satisfy the read

  70. * else we need to start IO

  71. */



  72. if (dev->bulk_in_filled){/*不是第一次读,当前缓冲区中的数据的字节数*/

  73. /* we had read data */

  74. size_t available = dev->bulk_in_filled- dev->bulk_in_copied;/*当前URB还有多少数据没有拷贝*/

  75. size_t chunk =min(available,count);



  76. if (!available){/*当前URB对应的缓冲区中没有数据了,要执行IO操作读入*/

  77. /*

  78. * all data has been used

  79. * actual IO needs to be done

  80. */
  81. /*执行IO操作,主要包括初始化一个URB和向usb core提交这个URB两个操作,真正的IO操作是由usb core驱动代码来完成的*/
  82. rv = skel_do_read_io(dev,count);

  83. if (rv < 0)

  84. goto exit;

  85. else

  86. goto retry;

  87. }

  88. /*

  89. * data is available

  90. * chunk tells us how much shall be copied

  91. */

  92. /*从当前URB对应的缓冲区中拷贝chunk字节数据到用户空间*/

  93. if (copy_to_user(buffer,

  94. dev->bulk_in_buffer+ dev->bulk_in_copied,

  95. chunk))

  96. rv = -EFAULT;

  97. else

  98. rv = chunk;



  99. dev->bulk_in_copied+= chunk;/*增加已拷贝数据的字节数*/



  100. /*

  101. * if we are asked for more than we have,

  102. * we start IO but don't wait

  103. */

  104. /*当前缓冲区拷贝完成后,还没有完成用户指定的数据拷贝量,要继续执行I/O操作,填充URB*/

  105. if (available< count)

  106. skel_do_read_io(dev,count - chunk);

  107. } else {

  108. /* no data in the buffer */

  109. /*当前缓冲区已经没有数据了,要执行I/O操作来填充URB对应的缓冲区*/

  110. rv = skel_do_read_io(dev,count);

  111. if (rv < 0)

  112. goto exit;

  113. else if(!(file->f_flags& O_NONBLOCK))

  114. goto retry;

  115. rv = -EAGAIN;

  116. }

  117. exit:

  118. mutex_unlock(&dev->io_mutex);

  119. return rv;

  120. }

    1. /*执行完I/O操作后,要去唤醒usb用户驱动中正在睡眠等待的读拷贝操作过程*/

    2. static void skel_read_bulk_callback(struct urb*urb)

    3. {

    4. struct usb_skel *dev;

    5. dev = urb->context;

    6. spin_lock(&dev->err_lock);

    7. /* sync/async unlink faults aren't errors */

    8. if (urb->status){

    9. if (!(urb->status== -ENOENT ||

    10. urb->status== -ECONNRESET ||

    11. urb->status== -ESHUTDOWN))

    12. err("%s - nonzero write bulk status received: %d",

    13. __func__, urb->status);



    14. dev->errors= urb->status;

    15. } else {

    16. dev->bulk_in_filled= urb->actual_length;

    17. }

    18. dev->ongoing_read= 0; /*执行完IO操作通知usb客户端驱动*/

    19. spin_unlock(&dev->err_lock);


    20. /*执行唤醒操作*/
    21. complete(&dev->bulk_in_completion);

    22. }



    23. static int skel_do_read_io(struct usb_skel*dev, size_t count)

    24. {

    25. int rv;



    26. /* prepare a read */

    27. /*在执行I/O操作前,先准备号一个URB*/

    28. usb_fill_bulk_urb(dev->bulk_in_urb,

    29. dev->udev,

    30. usb_rcvbulkpipe(dev->udev,

    31. dev->bulk_in_endpointAddr),

    32. dev->bulk_in_buffer,

    33. min(dev->bulk_in_size,count),

    34. skel_read_bulk_callback,

    35. dev);

    36. /* tell everybody to leave the URB alone */

    37. spin_lock_irq(&dev->err_lock);

    38. dev->ongoing_read= 1; /*标记现在要进行I/O操作了*/

    39. spin_unlock_irq(&dev->err_lock);



    40. /* do it */

    41. /*交由usb core模块去执行真正的IO操作*/

    42. rv = usb_submit_urb(dev->bulk_in_urb, GFP_KERNEL);

    43. if (rv < 0) {

    44. err("%s - failed submitting read urb, error %d",

    45. __func__, rv);

    46. dev->bulk_in_filled= 0;

    47. rv = (rv== -ENOMEM) ? rv: -EIO;

    48. spin_lock_irq(&dev->err_lock);

    49. dev->ongoing_read= 0;

    50. spin_unlock_irq(&dev->err_lock);

    51. }

    52. return rv;

    53. }

该函数比较容易理解,就是一个从usb设备读数据到用户程序的过程,这里涉及到一个主要的数据结构URB,对于usb客户驱动来讲,只需调用usb core提供的API即可,因此,usb驱动开发者只需了解这个API的功能和如何使用即可。详见代码注释。

4 skel_write()操作

  1. static ssize_t skel_write(structfile *file,const char *user_buffer,

  2.              size_t count, loff_t *ppos)

  3. {

  4.     struct usb_skel *dev;

  5.     int retval = 0;

  6.     struct urb *urb = NULL;

  7.     char *buf= NULL;

  8.     /*确定每次写操作可以写的数据量,最大值为PAGE_SIZE - 512 字节*/

  9.     size_t writesize = min(count,(size_t)MAX_TRANSFER);



  10.     dev = (struct usb_skel*)file->private_data;



  11.     /* verify that we actually have some data to write */

  12.     if (count== 0)

  13.         goto exit;



  14.     /*

  15.      * limit the number of URBs in flight to stop a user from using up all

  16.      * RAM

  17.      */

  18.     if (!(file->f_flags& O_NONBLOCK)){

  19.         if (down_interruptible(&dev->limit_sem)){

  20.             retval = -ERESTARTSYS;

  21.             goto exit;

  22.         }

  23.     } else{

  24.         if (down_trylock(&dev->limit_sem)){

  25.             retval = -EAGAIN;

  26.             goto exit;

  27.         }

  28.     }



  29.     spin_lock_irq(&dev->err_lock);

  30.     retval = dev->errors;

  31.     if (retval< 0) {

  32.         /* any error is reported once */

  33.         dev->errors= 0;

  34.         /* to preserve notifications about reset */

  35.         retval = (retval ==-EPIPE) ? retval : -EIO;

  36.     }

  37.     spin_unlock_irq(&dev->err_lock);

  38.     if (retval< 0)

  39.         goto error;



  40.     /* create a urb, and a buffer for it, and copy the data to the urb */

  41.     /*在usb用户驱动的读程序中,并没有URB的分配,因为在usb_skel中包含了一个URB数据成员*/

  42.     urb = usb_alloc_urb(0, GFP_KERNEL);

  43.     if (!urb){

  44.         retval = -ENOMEM;

  45.         goto error;

  46.     }



  47.     /*因为这里是由usb用户驱动负责分配的URB,因此,也应负责分配URB的成员指向的内存*/

  48.     buf = usb_alloc_coherent(dev->udev, writesize, GFP_KERNEL,

  49.                  &urb->transfer_dma);

  50.     if (!buf){

  51.         retval = -ENOMEM;

  52.         goto error;

  53.     }

  54.        /*把用户空间的数据拷贝到URB对应的DMA内存中*/

  55.     if (copy_from_user(buf, user_buffer, writesize)){

  56.         retval = -EFAULT;

  57.         goto error;

  58.     }



  59.     /* this lock makes sure we don't submit URBs to gone devices */

  60.     /*在URB没有准备好之前,不允许进行实际的I/O操作*/

  61.     mutex_lock(&dev->io_mutex);

  62.     if (!dev->interface){ /* disconnect() was called */

  63.         mutex_unlock(&dev->io_mutex);

  64.         retval = -ENODEV;

  65.         goto error;

  66.     }



  67.     /* initialize the urb properly */

  68.     usb_fill_bulk_urb(urb, dev->udev,

  69.              usb_sndbulkpipe(dev->udev, dev->bulk_out_endpointAddr),

  70.              buf, writesize, skel_write_bulk_callback, dev);

  71.     urb->transfer_flags|= URB_NO_TRANSFER_DMA_MAP;

  72.     /*把urb添加到另外一条链表上,以便在其他地方对其进行访问*/

  73.     usb_anchor_urb(urb,&dev->submitted);



  74.     /* send the data out the bulk port */

  75.     retval = usb_submit_urb(urb, GFP_KERNEL);

  76.     mutex_unlock(&dev->io_mutex);

  77.     if (retval){

  78.         err("%s - failed submitting write urb, error %d",__func__,

  79.          retval);

  80.         goto error_unanchor;

  81.     }



  82.     /*

  83.      * release our reference to this urb, the USB core will eventually free

  84.      * it entirely

  85.      */

  86.     usb_free_urb(urb);





  87.     return writesize;

  88. }

在函数与skel_read非常类似,唯一不同的是在写操作过程中需要usb客户驱动程序来显示的分配和初始化用于写操作的URB,之所以在读操作过程中看不到这个过程,是因为,在struct usb_skel dev中已经为写操作内嵌了一个数据成员:

  1. /* Structure to hold all of our device specific stuff */
  2. struct usb_skel {
  3.     struct usb_device    *udev;            /* the usb device for this device */
  4.     struct usb_interface    *interface;        /* the interface for this device */
  5.     struct semaphore    limit_sem;        /* limiting the number of writes in progress */
  6.     struct usb_anchor    submitted;        /* in case we need to retract our submissions */
  7.     struct urb        *bulk_in_urb;        /* the urb to read data with */
  8.     unsigned char*bulk_in_buffer;    /* the buffer to receive data */
  9.     size_t            bulk_in_size;        /* the size of the receive buffer */
  10.     size_t            bulk_in_filled;        /* number of bytes in the buffer */
  11.     size_t            bulk_in_copied;        /* already copied to user space */
  12.     __u8            bulk_in_endpointAddr;    /* the address of the bulk in endpoint */
  13.     __u8            bulk_out_endpointAddr;    /* the address of the bulk out endpoint */
  14.     int            errors;            /* the last request tanked */
  15.     int            open_count;        /* count the number of openers */
  16.     bool            ongoing_read;        /* a read is going on */
  17.     bool            processed_urb;        /* indicates we haven't processed the urb */
  18.     spinlock_t        err_lock;        /* lock for errors */
  19.     struct kref        kref;
  20.     struct mutex        io_mutex;        /* synchronize I/O with disconnect */
  21.     struct completion    bulk_in_completion;    /* to wait for an ongoing read */
  22. };

而且这个结构体的很多数据成员都与读操作有关,具体为什么这么设计,暂时还没有搞明白,希望大家指教!

 

总结:

1. usb客户驱动还是比较简单的,主要是因为很多功能都由usb core驱动程序事先完成了,usb客户端驱动程序仅仅是调用一些API即可。

2. usb客户驱动程序的开发比较固定,直接套用usb-skeleton的代码基本就可以完成用户自定义驱动的编写


GitHub 加速计划 / li / linux-dash
6
1
下载
A beautiful web dashboard for Linux
最近提交(Master分支:3 个月前 )
186a802e added ecosystem file for PM2 4 年前
5def40a3 Add host customization support for the NodeJS version 4 年前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐