1 数据结构说明
1.1
struct usb_device_id {
        /* which fields to match against? */
        __u16                match_flags;


        /* Used for product specific matches; range is inclusive */
        __u16                idVendor;
        __u16                idProduct;
        __u16                bcdDevice_lo;
        __u16                bcdDevice_hi;


        /* Used for device class matches */
        __u8                bDeviceClass;
        __u8                bDeviceSubClass;
        __u8                bDeviceProtocol;


        /* Used for interface class matches */
        __u8                bInterfaceClass;
        __u8                bInterfaceSubClass;
        __u8                bInterfaceProtocol;


        /* Used for vendor-specific interface matches */
        __u8                bInterfaceNumber;


        /* not matched against */
        kernel_ulong_t        driver_info
                __attribute__((aligned(sizeof(kernel_ulong_t))));
};
成员变量说明:
match_flags :需要匹配的类型(可以多个或起来),数据定义如下:
        #define USB_DEVICE_ID_MATCH_VENDOR                0x0001 //匹配PID
        #define USB_DEVICE_ID_MATCH_PRODUCT                0x0002 //匹配VID
        #define USB_DEVICE_ID_MATCH_DEV_LO                0x0004 //匹配产品编号低位
        #define USB_DEVICE_ID_MATCH_DEV_HI                0x0008 //匹配产品编号高位
        #define USB_DEVICE_ID_MATCH_DEV_CLASS                0x0010 //匹配设备描述符的class
        #define USB_DEVICE_ID_MATCH_DEV_SUBCLASS        0x0020 //匹配设备描述符的subclass
        #define USB_DEVICE_ID_MATCH_DEV_PROTOCOL        0x0040 //匹配设备描述符的protocol
        #define USB_DEVICE_ID_MATCH_INT_CLASS                0x0080 //匹配接口描述符的class
        #define USB_DEVICE_ID_MATCH_INT_SUBCLASS        0x0100 //匹配接口描述符的subclass
        #define USB_DEVICE_ID_MATCH_INT_PROTOCOL        0x0200 //匹配接口描述符的protocol
        #define USB_DEVICE_ID_MATCH_INT_NUMBER                0x0400 //匹配接口的数量
driver_info :用来保存用户的私有数据


1.2 
struct usb_driver {
        const char *name;


        int (*probe) (struct usb_interface *intf,
                      const struct usb_device_id *id);


        void (*disconnect) (struct usb_interface *intf);


        int (*unlocked_ioctl) (struct usb_interface *intf, unsigned int code,
                        void *buf);


        int (*suspend) (struct usb_interface *intf, pm_message_t message);
        int (*resume) (struct usb_interface *intf);
        int (*reset_resume)(struct usb_interface *intf);


        int (*pre_reset)(struct usb_interface *intf);
        int (*post_reset)(struct usb_interface *intf);


        const struct usb_device_id *id_table;


        struct usb_dynids dynids;
        struct usbdrv_wrap drvwrap;
        unsigned int no_dynamic_id:1;
        unsigned int supports_autosuspend:1;
        unsigned int disable_hub_initiated_lpm:1;
        unsigned int soft_unbind:1;
};
name:独一无二的设备的名字
probe:回调函数指针,在USB设备插入,且符合struct usb_device_id 定义的匹配条件是被调用。这是如果匹配成功返回0,
disconnect:回调函数指针,当设备拔出时被调用
unlocked_ioctl:
suspend : 设备休眠的时候被调用
resume:设备唤醒的时候被调用
reset_resume:设备在休眠的时候已经reset,唤醒的时候调用
pre_reset:在调用usb_reset_device()的时候这个回调函数被调用,这个函数在reset前调用,该函数必须等到没有有效的URBs才能返回。
post_reset:在调用usb_reset_device()的时候这个回调函数被调用,这个函数在reset完成后调用
id_table:需要匹配的设备信息
dynids:用来保持动态添加的设备列表
drvwrap:Driver-model core structure wrapper
no_dynamic_id:如果设置为1,内核将不允许添加动态设备列表
supports_autosuspend:如果设置为0 内核将不允许设备自动休眠,不知道为什么这个参数设置为0,有时会造成设备访问被拒绝
soft_unbind:如果设置为1 ,在设备拔出前,不允许禁用端点,杀掉URBs
disable_hub_initiated_lpm:
注意:这个结构必须要实现name、probe、disconnect、id_table,其他成员可以根据情况选择是否实现


1.3
struct usb_class_driver {
        char *name;
        char *(*devnode)(struct device *dev, umode_t *mode);
        const struct file_operations *fops;
        int minor_base;
};
name:设备节点的名称,也就是在dev目录下的设备,可以直接Open这个设备进行读写
devnode:??
fops:设备文件操作结构
minor_base:次设备号的基准值,


1.4
struct urb {
        /* private: usb core and host controller only fields in the urb */
        struct kref kref;                /* reference count of the URB */
        void *hcpriv;                        /* private data for host controller */
        atomic_t use_count;                /* concurrent submissions counter */
        atomic_t reject;                /* submissions will fail */
        int unlinked;                        /* unlink error code */


        /* public: documented fields in the urb that can be used by drivers */
        struct list_head urb_list;        /* list head for use by the urb's  * current owner */
        struct list_head anchor_list;        /* the URB may be anchored */
        struct usb_anchor *anchor;
        struct usb_device *dev;                /* (in) pointer to associated device */
        struct usb_host_endpoint *ep;        /* (internal) pointer to endpoint */
        unsigned int pipe;                /* (in) pipe information */
        unsigned int stream_id;                /* (in) stream ID */
        int status;                        /* (return) non-ISO status */
        unsigned int transfer_flags;        /* (in) URB_SHORT_NOT_OK | ...*/
        void *transfer_buffer;                /* (in) associated data buffer */
        dma_addr_t transfer_dma;        /* (in) dma addr for transfer_buffer */
        struct scatterlist *sg;                /* (in) scatter gather buffer list */
        int num_mapped_sgs;                /* (internal) mapped sg entries */
        int num_sgs;                        /* (in) number of entries in the sg list */
        u32 transfer_buffer_length;        /* (in) data buffer length */
        u32 actual_length;                /* (return) actual transfer length */
        unsigned char *setup_packet;        /* (in) setup packet (control only) */
        dma_addr_t setup_dma;                /* (in) dma addr for setup_packet */
        int start_frame;                /* (modify) start frame (ISO) */
        int number_of_packets;                /* (in) number of ISO packets */
        int interval;                        /* (modify) transfer interval
                                         * (INT/ISO) */
        int error_count;                /* (return) number of ISO errors */
        void *context;                        /* (in) context for completion */
        usb_complete_t complete;        /* (in) completion routine */
        struct usb_iso_packet_descriptor iso_frame_desc[0];
                                        /* (in) ISO ONLY */
};
以下的成员变量是私有的,只能由usb核心和主机控制器访问的字段 
        kref : urb引用计数 ,当计数引用为0时,系统释放urb
        hcpriv : 主机控制器私有数据 
        use_count :   并发传输计数  
        reject : 传输将失败


以下的成员变量是公共的,可以被驱动使用的字段 
        urb_list :链表头
        dev :关联的usb设备 
        pipe :端点消息,这个变量必须在这个 urb 被发送到 USB 核心之前被 USB 驱动初始化.
               要设置这个结构的成员, 驱动可以使用下面的函数来设置. 注意每个端点只可以是一个类型.
                unsigned int usb_sndctrlpipe(struct usb_device *dev, unsigned int endpoint)
                        指定一个控制 OUT 端点给特定的带有特定端点号的 USB 设备.
                unsigned int usb_rcvctrlpipe(struct usb_device *dev, unsigned int endpoint)
                        指定一个控制 IN 端点给带有特定端点号的特定 USB 设备.
                unsigned int usb_sndbulkpipe(struct usb_device *dev, unsigned int endpoint)
                        指定一个块传输 OUT 端点给带有特定端点号的特定 USB 设备
                unsigned int usb_rcvbulkpipe(struct usb_device *dev, unsigned int endpoint)
                        指定一个块传输 IN 端点给带有特定端点号的特定 USB 设备
                unsigned int usb_sndintpipe(struct usb_device *dev, unsigned int endpoint)
                        指定一个中断传输 OUT 端点给带有特定端点号的特定 USB 设备
                unsigned int usb_rcvintpipe(struct usb_device *dev, unsigned int endpoint)
                        指定一个中断传输 IN 端点给带有特定端点号的特定 USB 设备
                unsigned int usb_sndisocpipe(struct usb_device *dev, unsigned int endpoint)
                        指定一个同步传输 OUT 端点给带有特定端点号的特定 USB 设备
                unsigned int usb_rcvisocpipe(struct usb_device *dev, unsigned int endpoint)
                        指定一个同步传输 IN 端点给带有特定端点号的特定 USB 设备 
        status : urb的当前状态 
                0        urb 传送是成功的.
                -ENOENT  这个 urb 被 usb_kill_urb调用停止.
                -ECONNRESET urb 被usb_unlink_urb 调用去链, 并且 transfer_flags 变量被设置为 URB_ASYNC_UNLINK.
                -EINPROGRESS  这个 urb 仍然在被 USB 主机控制器处理中. 如果你的驱动曾见到这个值, 它是一个你的驱动中的 bug.
                -EPROTO       一个 bitstuff 错误在传送中发生. 硬件没有及时收到响应帧.
                 -EILSEQ       在这个 urb 传送中 CRC 不匹配.
                -EPIPE        这个端点现在被停止. 如果这个包含的端点不是一个控制端点, 这个错误可被清除通过一个对函数 usb_clear_halt 的调用.
                -ECOMM        在传送中数据接收快于能被写入系统内存. 这个错误值只对 IN urb.
                -ENOSR        在传送中数据不能从系统内存中获取得足够快, 以便可跟上请求的 USB 数据速率. 这个错误只对 OUT urb.
                -EOVERFLOW    这个 urb 发生一个"babble"错误. 一个"babble"错误发生当端点接受数据多于端点的特定最大报文大小.
                -EREMOTEIO    只发生在当 URB_SHORT_NOT_OK 标志被设置在 urb 的 transfer_flags 变量, 并且意味着 urb 请求的完整数量的数据没有收到.
                -ENODEV       这个 USB 设备现在从系统中消失.
                -EXDEV        只对同步 urb 发生, 并且意味着传送只部分完成. 为了决定传送什么, 驱动必须看单独的帧状态.
                -EINVAL       这个 urb 发生了非常坏的事情. USB 内核文档描述了这个值意味着什么:ISO 疯了, 如果发生这个: 退出并回家.
                      它也可发生, 如果一个参数在 urb 结构中被不正确地设置了, 或者如果在提交这个 urb 给 USB 核心的 usb_submit_urb 调用中, 有一个不正确的函数参数.
                -ESHUTDOWN    这个 USB 主机控制器驱动有严重的错误; 它现在已被禁止, 或者设备和系统去掉连接, 并且这个urb 在设备被去除后被提交. 它也可发生当这个设备的配置改变, 而这个 urb 被提交给设备.
                通常, 错误值 -EPROTO, -EILSEQ, 和 -EOVERFLOW 指示设备的硬件问题, 设备固件, 或者连接设备到计算机的线缆.
                
        transfer_flags:这个变量可被设置为不同位值, 根据这个 USB 驱动想这个 urb 发生什么. 可用的值如下:
                URB_SHORT_NOT_OK          当置位, 它指出任何在一个 IN 端点上可能发生的短读, 应当被 USB 核心当作一个错误. 这个值只对从 USB 设备读的 urb 有用, 不是写 urbs.
                URB_ISO_ASAP              如果这个 urb 是同步的, 这个位可被置位如果驱动想这个 urb 被调度, 只要带宽允许它这样, 并且在此点设置这个 urb 中的 start_frame 变量. 如果对于同步 urb 这个位没有被置位, 驱动必须指定 start_frame 值并且必须能够正确恢复, 如果没有在那个时刻启动. 见下面的章节关于同步 urb 更多的消息.
                URB_NO_TRANSFER_DMA_MAP   应当被置位, 当 urb 包含一个要被发送的 DMA 缓冲. USB 核心使用这个被 transfer_dma 变量指向的缓冲, 不是被 transfer_buffer 变量指向的缓冲.
                URB_NO_SETUP_DMA_MAP      象 URB_NO_TRANSFER_DMA_MAP 位, 这个位用来控制有一个 DMA 缓冲已经建立的 urb. 如果它被置位, USB 核心使用这个被 setup_dma 变量而不是 setup_packet 变量指向的缓冲.
                URB_ASYNC_UNLINK          如果置位, 给这个 urb 的对 usb_unlink_urb 的调用几乎立刻返回, 并且这个 urb 在后面被解除连接. 否则, 这个函数等待直到 urb 完全被去链并且在返回前结束. 小心使用这个位, 因为它可有非常难于调试的同步问题.
                URB_NO_FSBR               只有 UHCI USB 主机控制器驱动使用, 并且告诉它不要试图做 Front Side Bus Reclamation 逻辑. 这个位通常应当不设置, 因为有 UHCI 主机控制器的机器创建了许多 CPU 负担, 并且 PCI 总线被等待设置了这个位的 urb 所饱和.
                URB_ZERO_PACKET           如果置位, 一个块 OUT urb 通过发送不包含数据的短报文而结束, 当数据对齐到一个端点报文边界. 这被一些坏掉的 USB 设备所需要(例如一些 USB 到 IR 的设备) 为了正确的工作..
                URB_NO_INTERRUPT          如果置位, 硬件当 urb 结束时可能不产生一个中断. 这个位应当小心使用并且只在排队多个到相同端点的 urb 时使用. USB 核心函数使用这个为了做 DMA 缓冲传送.
        transfer_buffer : 发送数据到设备或从设备接收数据的缓冲区,这个缓冲区它必须被使用 kmalloc来创建, 不是在堆栈或者静态地址,所以不能定义一个大数组来作为缓冲区.
        transfer_dma : 用来以dma方式向设备传输数据的缓冲区 
        transfer_buffer_length : transfer_buffer或transfer_dma 指向缓冲区的大小 
        actual_length : urb结束后,发送或接收数据的实际长度 
        setup_packet : 指向控制urb的设置数据包的指针
        setup_dma :控制urb的设置数据包的dma缓冲区
        start_frame :等时传输中用于设置或返回初始帧
        number_of_packets :只对同步 urb 有效, 并且指定这个 urb 要处理的同步传送缓冲的编号. 这个值必须被 USB 驱动设置给同步 urb, 在这个 urb 发送给 USB 核心之前.
        interval : urb被轮询到的时间间隔(对中断和等时urb有效) 
        error_count : 等时传输错误数量 
        context :completion函数上下文 ,用来保存用户数据的指针
        complete : 回调函数指针,当urb被完全传输或发生错误时,被调用 
                usb_complete_t 类型定义 :typedef void (*usb_complete_t)(struct urb *, struct pt_regs *);
        iso_frame_desc[0] : 单个urb一次可定义多个等时传输时,描述各个等时传输 
        
        注意:urb 不能静态创建, 或者在另一个结构中, 必须使用 usb_alloc_urb 函数来创建,使用usb_free_urb来释放 。
        
2 函数说明


        int usb_register_driver(struct usb_driver *new_driver, struct module *owner,        const char *mod_name)
        函数功能:注册一个USB驱动
        参数说明:new_driver :struct usb_driver的结构指针 
                  owner      :模块的所有者名
                  mod_name   :模块名称
        返回值:0 成功 非0 失败
        说明 :这个函数通常在驱动的__init函数中调用注册一个USB驱动。为简化调用的参数,我们通常直接用 usb_register这个宏
               usb_register定义如下
               #define usb_register(driver)         usb_register_driver(driver, THIS_MODULE, KBUILD_MODNAME)        
         
               
        void usb_deregister(struct usb_driver * driver);
        函数功能:注销一个USB驱动,这个函数通常在驱动的__exit函数中调用
        参数说明:driver : struct usb_driver的结构指针 
        返回值:无
        
        
        int usb_register_dev(struct usb_interface *intf, struct usb_class_driver *class_driver)
        函数功能:注册一个USB设备,注册成功后可以在/dev目录下看到相应的设备节点,这个函数通常是在struct usb_driver中定义的probe回调函数中被调用
        参数说明:intf :USB接口描述符
                  class_driver : struct usb_class_driver 这个结构将指定设备的名称和  file_operations结构
        返回值:0成功 非0 失败 
        
        void usb_deregister_dev(struct usb_interface *intf, struct usb_class_driver *class_driver)
        函数功能: 注销一个USB设备,这个函数通常是在struct usb_driver中定义的disconnect回调函数中被调用
        参数说明:intf :USB接口描述符
                  class_driver : struct usb_class_driver 这个结构将指定设备的名称和  file_operations结构
        返回值: 无
        
        struct urb *usb_alloc_urb(int iso_packets, gfp_t mem_flags)
        函数功能:动态分配一个urb,并初始化
        参数说明:iso_packets : usb_iso_packet_descriptor的个数,这个参数只对同步传输有效。对于控制、中断、块传输,这个参数必须为0
                  mem_flags : 和kmalloc 函数调用来从内核分配内存的标志类型相同
        返回值:成功返回分配的urb的指针,失败返回NULL
        
        void usb_free_urb(struct urb *urb)
        函数功能:释放usb_alloc_urb分配的urb
        参数说明:urb : 要释放的urb
        返回值:无
        
        void 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)
        void 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)
        void 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
        参数说明:Urb:是要初始化的urb
                  Dev:表示消息要被发送到的USB设备
                  Pipe:表示消息被发送到的端点
                  transfer_buffer:表示发送或接收数据的缓冲区
                  length:就是transfer_buffer所表示的缓冲区大小
                  context:完成处理函数的上下文
                  complete_fn:传输完了之后的回调函数.
                  interval:中断传输被调度的间隔
                  setup_packet:将被发送到端点的设置数据包
        返回值:无
        
        int usb_submit_urb(struct urb *urb, gfp_t mem_flags)
        函数功能:用来递交urb,该函数以非阻塞的方式发送,也就是这个函数将不等发送完成就返回。
        参数说明:urb :指向urb的指针
                  mem_flags :可以有下面三种取值
                              GFP_ATOMIC:在中断处理函数、底半部、tasklet、定时器处理函数以及urb完成函数中,在调用者持有自旋锁或者读写锁时以及当驱动将current->state修改为非 TASK_ RUNNING时,应使用此标志。
                                GFP_NOIO:在存储设备的块I/O和错误处理路径中,应使用此标志;
                              GFP_KERNEL:如果没有任何理由使用GFP_ATOMIC和GFP_NOIO,就使用GFP_ KERNEL。
        返回值:0成功 非0 失败
        
        void usb_kill_urb(struct urb *urb)
        函数功能:取消一个urb传输,并且等待他结束
        参数说明:要取消的urb的指针
        返回值:无
        
        int usb_unlink_urb(struct urb *urb)
        函数功能:停止 urb传输. 这个函数 不等待这个 urb 完全停止就返回
        参数说明:要取消的urb的指针
        返回值:无
        
        int usb_control_msg(struct usb_device *dev, unsigned int pipe, u8 request, u8 requesttype, u16 value, u16 index, void *data, u16 size, int timeout)
        函数功能:以阻塞的方式进行控制传输,直到超时或urb传输完成函数才返回
        参数说明:Dev:表示消息要被发送到的USB设备
                  Pipe:表示消息被发送到的端点
                  request:命令的序号(其实就是命令)
                  requesttype:D7=0主机到设备, =1设备到主机;
                            D6D5 =00标准请求命令, 01 类请求命令,10用户定义的命令,11保留值
                            D4D3D2D1D0= 0表示接收者为设备,1表示接收者为接口,2表示接收者为端点,3表示接收者为其他
              value:2个字节,用来传送当前请求的参数,随请求不同而变。
              index:索引字段同样是2个字节,描述的是接口号 
              data :数据缓冲区的指针
              size:数据缓冲区的长度
              timeout:超时时间   
    返回值:0成功,非0 失败           
        
        int usb_interrupt_msg(struct usb_device *usb_dev, unsigned int pipe, void *data, int len, int *actual_length, int timeout)        
        int usb_bulk_msg(struct usb_device *usb_dev, unsigned int pipe, void *data, int len, int *actual_length, int timeout)
        函数功能:以阻塞的方式进行中断或者块传输,直到超时或urb传输完成函数才返回
        参数说明:Dev:表示消息要被发送到的USB设备
                  Pipe:表示消息被发送到的端点             
              data :数据缓冲区的指针
              len :数据缓冲区的长度
              actual_length :实际发送或者接收的数据长度
              timeout:超时时间   
    返回值:0成功,非0 失败  
    
    void usb_anchor_urb(struct urb *urb, struct usb_anchor *anchor)
    函数功能:当urb在处理的时候锁定该urb ,urb被使用时是不能被删除的。在删除一个urb时需要调用usb_wait_anchor_empty_timeout来等待urb传输完全结束
    参数说明:urb:要锁定的urb
              anchor:   
        返回值:无
        
        void usb_unanchor_urb(struct urb *urb) 
        函数功能:解锁一个urb
    参数说明:urb:要解锁的urb 
        返回值:无
        
        int usb_wait_anchor_empty_timeout(struct usb_anchor *anchor,unsigned int timeout)
        函数功能:等待urb传输完全结束
    参数说明:urb:要等待的urb
              anchor: 
              timeout :超时设置  
        返回值:0传输完成,非0 超时
        
        void usb_kill_anchored_urbs(struct usb_anchor *anchor)
        函数功能:撤销一个urb传输
        参数说明:anchor:
        
        
3注册一个USB驱动的流程
        1 定义一个 struct usb_device_id的结构。
        2 定义一个 struct usb_driver,并实现probe和disconnect函数
        3 在模块的init函数中调用usb_register函数注册一个驱动
        4 定义一个usb_class_driver 
        5 在probe函数中调用usb_register_dev注册一个USB设备,这样在dev目录下就可以看到我们的USB设备了
        
下面是示例代码:
。。。。
static const struct file_operations usbdev_fops = {
        .owner =        THIS_MODULE,
        .read =                usbdev_read,
        .write =        usbdev_write,
        .open =                usbdev_open,
        .release =        usbdev_release,
        .flush =        usbdev_flush,
        .llseek =        noop_llseek,
};
static struct usb_class_driver usbdev_class = {
        .name =                "USBTestDevice%d",//这里一次插上多个USB时候,将在dev目录下看到USBTestDevice0、USBTestDevice1 ....
        .fops =                &usbdev_fops,
        .minor_base =        USB_DEV_MINOR_BASE,
};
static int usbdev_probe(struct usb_interface *interface, const struct usb_device_id *id)
{
        ......
        retval = usb_register_dev(interface, &usbdev_class);
        ......
}
static void usbdev_disconnect(struct usb_interface *interface)
{
        ................
        usb_deregister_dev(interface, &usbdev_class);
        ................
}
static const struct usb_device_id usbdev_table[] = {
        { USB_DEVICE(USB_MATCH_VENDOR_ID, USB_MATCH_PRODUCT_ID) },
        { USB_DEVICE(USB_MATCH_VENDOR_ID, USB_MATCH_PRODUCT_ID_1) },
        { }                                        /* Terminating entry */
};
MODULE_DEVICE_TABLE(usb, usbdev_table);
static struct usb_driver usbdev_driver = {
        .name =                "usbtest",
        .probe =        usbdev_probe,
        .disconnect =        usbdev_disconnect,
        .id_table =        usbdev_table,
};
static int __init usbdev_init(void)
{
         
        return usb_register(&usbdev_driver);
         
}
module_init(usbdev_init);
static void __exit usbdev_exit(void)
{
        usb_deregister(&usbdev_driver);
}
module_exit(usbdev_exit);




4 urb的操作流程:
  1 调用usb_alloc_urb分配一个urb
  2 调用kmalloc分配缓冲区
  3 调用usb_fill_XXX_urb初始化urb
  4 调用usb_submit_urb提交
  5 在回调函数(complete)中处理,接收的数据,判断是否还要继续接收,如果还要接收,重复第三步。
    如果是发送数据,判断是否还有数据要发送,填充数据
  6 如果传输完成调用usb_free_urb释放urb,释放缓冲区
  
  下面是示例代码
  //回调函数
static void async_ctrl_callback(struct urb *urb)
{
        struct usb_ctrlrequest *req = (struct usb_ctrlrequest *)urb->context;
        int status = urb->status;


        if (status < 0)
        {
                printk("error\n");
        }
                 
        kfree(req);
        kfree(urb->transfer_buffer);
        usb_free_urb(urb);
}
        struct urb *urb;
        struct usb_ctrlrequest *req;
        void *data;
        urb = usb_alloc_urb(0, GFP_KERNEL);
        req = kmalloc(sizeof(struct usb_ctrlrequest), GFP_ATOMIC);
        /*根据协议设置reg*/
        req->bRequestType = RequestType;
        req->bRequest = Request;
        req->wValue = Value;
        req->wIndex = Index;
        req->wLength = Length;
        data = kmalloc(Length, GFP_KERNEL);
         
        usb_fill_control_urb(urb, dev, usb_sndctrlpipe(dev, 0),reg, data, Length,  usb_ctrl_complete, req);
        usb_submit_urb(urb, GFP_ATOMIC);
        
        对于其他的传输,方法差不多。
        最后需要注意的是中断传输接收,不需要每次调用usb_submit_urb,调用一次usb_submit_urb之后,系统会按照指定的间隔,不断接收数据的。
        
5 下面是完成的驱动的示例代码




#include <linux/kernel.h>
#include <linux/errno.h>
#include <linux/slab.h>
#include <linux/module.h>
#include <linux/kref.h>
#include <linux/uaccess.h>
#include <linux/usb.h>
#include <linux/mutex.h>
#include <linux/moduleparam.h>
#define BUF_PER_PAGE 10240
#define MAX_PAGE 5
   
typedef struct
{
        volatile int iReadIndex;
        volatile int iWriteIndex;
        unsigned char *PackgeBuf[MAX_PAGE];//[BUF_PER_PAGE];
        int iLen[MAX_PAGE];
}cachebuf  ;


 
/* Define these values to match your devices */
#define USB_MATCH_VENDOR_ID            0x1234
#define USB_MATCH_PRODUCT_ID        0x5678
 
/* table of devices that work with this driver */
static const struct usb_device_id usbdev_table[] = {
        { USB_DEVICE(USB_MATCH_VENDOR_ID, USB_MATCH_PRODUCT_ID) },
        { }                                        /* Terminating entry */
};
MODULE_DEVICE_TABLE(usb, usbdev_table);




/* Get a minor range for your devices from the usb maintainer */
#define USB_DEV_MINOR_BASE        192


/* our private defines. if this grows any larger, use your own .h file */
#define MAX_TRANSFER                (PAGE_SIZE - 512)
/* MAX_TRANSFER is chosen so that the VM is not stressed by
   allocations > PAGE_SIZE and the number of packets in a page
   is an integer 512 is the largest possible packet on EHCI */
#define WRITES_IN_FLIGHT        8
/* arbitrarily chosen */


/* Structure to hold all of our device specific stuff */
struct usb_devinfo {
        struct usb_device        *udev;                        /* the usb device for this device */
        struct usb_interface        *interface;                /* the interface for this device */
        struct semaphore        limit_sem;                /* limiting the number of writes in progress */
        struct usb_anchor        submitted;                /* in case we need to retract our submissions */
        struct urb                *bulk_in_urb;                /* the urb to read data with */
        unsigned char           *bulk_in_buffer;        /* the buffer to receive data */
        size_t                        bulk_in_size;                /* the size of the receive buffer */
        size_t                        bulk_in_filled;                /* number of bytes in the buffer */
        size_t                        bulk_in_copied;                /* already copied to user space */
        __u8                        bulk_in_endpointAddr;        /* the address of the bulk in endpoint */
        __u8                        bulk_out_endpointAddr;        /* the address of the bulk out endpoint */
        int                        errors;                        /* the last request tanked */


        spinlock_t                err_lock;                /* lock for errors */
        struct kref                kref;
        struct mutex                io_mutex;                /* synchronize I/O with disconnect */


        cachebuf UsbBuffer; 
        wait_queue_head_t   wqReadOK; /* buf is not empty*/
        struct work_struct work;
        struct mutex                read_mutex;
        int isOpen;
};
 


static struct usb_driver usbdev_driver;
static void usbdev_emptyurb(struct usb_devinfo *dev);
static int devusb_do_read_io(struct usb_devinfo *dev, size_t count);
 
static void work_func(struct work_struct *work)
{
        struct usb_devinfo *dev = container_of(work, struct usb_devinfo, work);
         
        if(dev->isOpen==0)
        {
                return ;
        }
        
        
        if(dev->bulk_in_filled>0)
        {
                int iWriteIndex = dev->UsbBuffer.iWriteIndex;
                mutex_lock(&dev->read_mutex); 
                //memcpy(dev->UsbBuffer.PackgeBuf[iWriteIndex],dev->bulk_in_buffer  ,dev->bulk_in_filled);
                 
                dev->UsbBuffer.iLen[iWriteIndex] = dev->bulk_in_filled;
                mutex_unlock(&dev->read_mutex); 
                iWriteIndex++;
                if(iWriteIndex>=MAX_PAGE)
                {
                        iWriteIndex = 0;
                }
                while(iWriteIndex ==dev->UsbBuffer.iReadIndex)
                {
                        if(iRetry==0)
                                printk(KERN_ALERT"usbdev_overflow(%d %d) \n",dev->UsbBuffer.iReadIndex,dev->UsbBuffer.iWriteIndex);
                         
                         
                }
                mutex_lock(&dev->read_mutex); 
                dev->UsbBuffer.iWriteIndex = iWriteIndex;
                mutex_unlock(&dev->read_mutex); 
                dev->bulk_in_filled = 0;
                //printk("write w=%d r=%d \n",dev->UsbBuffer.iWriteIndex,dev->UsbBuffer.iReadIndex);
                
                devusb_do_read_io(dev,BUF_PER_PAGE);
                wake_up_interruptible(&dev->wqReadOK);         
        }
         
         
}
static void usbdev_delete(struct kref *kref)
{
        struct usb_devinfo *dev = container_of(kref, struct usb_devinfo, kref);
        int i; 


        usb_free_urb(dev->bulk_in_urb);
        usb_put_dev(dev->udev);
        kfree(dev->bulk_in_buffer);
        for(i=0;i<MAX_PAGE;i++)
        {
                kfree(dev->UsbBuffer.PackgeBuf[i]);
        }
        kfree(dev);
        
        
}


static int usbdev_open(struct inode *inode, struct file *file)
{
        struct usb_devinfo *dev;
        struct usb_interface *interface;
        int subminor;
        int retval = 0;
        printk(KERN_ALERT"usbdev_open \n");
        subminor = iminor(inode);


        interface = usb_find_interface(&usbdev_driver, subminor);
        if (!interface) {
                pr_err("%s - error, can't find device for minor %d\n",
                        __func__, subminor);
                retval = -ENODEV;
                goto exit;
        }


        dev = usb_get_intfdata(interface);
        if (!dev) {
                retval = -ENODEV;
                goto exit;
        }


        retval = usb_autopm_get_interface(interface);
        if (retval)
                goto exit;


        /* increment our usage count for the device */
        kref_get(&dev->kref);
         
        
        /* save our object in the file's private structure */
        file->private_data = dev;
        dev->UsbBuffer.iReadIndex=dev->UsbBuffer.iWriteIndex=0;
        devusb_do_read_io(dev,BUF_PER_PAGE);
        dev->isOpen = 1 ;
exit:
        return retval;
}


static int usbdev_release(struct inode *inode, struct file *file)
{
        struct usb_devinfo *dev;
         
        dev = file->private_data;
        if (dev == NULL)
                return -ENODEV;


        /* allow the device to be autosuspended */
        mutex_lock(&dev->io_mutex);
        usbdev_emptyurb(dev);
        if (dev->interface)
                usb_autopm_put_interface(dev->interface);
        mutex_unlock(&dev->io_mutex);
        dev->isOpen = 0 ; 
        wake_up_interruptible(&dev->wqReadOK);         
        /* decrement the count on our device */
        kref_put(&dev->kref, usbdev_delete);
        return 0;
}


static int usbdev_flush(struct file *file, fl_owner_t id)
{
        struct usb_devinfo *dev;
        int res;


        dev = file->private_data;
        if (dev == NULL)
                return -ENODEV;


        /* wait for io to stop */
        mutex_lock(&dev->io_mutex);
        usbdev_emptyurb(dev);


        /* read out errors, leave subsequent opens a clean slate */
        spin_lock_irq(&dev->err_lock);
        res = dev->errors ? (dev->errors == -EPIPE ? -EPIPE : -EIO) : 0;
        dev->errors = 0;
        spin_unlock_irq(&dev->err_lock);


        mutex_unlock(&dev->io_mutex);


        return res;
}


static void usbdev_read_callback(struct urb *urb)
{
        struct usb_devinfo *dev;


        dev = urb->context;
        //printk(KERN_ALERT"usbdev_read_callback\n");
        spin_lock(&dev->err_lock);
        /* sync/async unlink faults aren't errors */
        if (urb->status) {
                if (!(urb->status == -ENOENT ||
                    urb->status == -ECONNRESET ||
                    urb->status == -ESHUTDOWN))
                        dev_err(&dev->interface->dev,
                                "%s - nonzero write bulk status received: %d\n",
                                __func__, urb->status);


                dev->errors = urb->status;
        } else {
                dev->bulk_in_filled = urb->actual_length;
                if(dev->isOpen==1)
                {
                        schedule_work(&(dev->work));
                }
                 
                
        }


        spin_unlock(&dev->err_lock);
        


}


static int devusb_do_read_io(struct usb_devinfo *dev, size_t count)
{
        int rv;
 
        /* prepare a read */
        usb_fill_bulk_urb(dev->bulk_in_urb,
                        dev->udev,
                        usb_rcvbulkpipe(dev->udev,
                                dev->bulk_in_endpointAddr),
                        dev->UsbBuffer.PackgeBuf[dev->UsbBuffer.iWriteIndex],//dev->bulk_in_buffer,//
                        min(dev->bulk_in_size, count),
                        usbdev_read_callback,
                        dev);
 
        /* tell everybody to leave the URB alone */
        //spin_lock_irq(&dev->err_lock);


        //spin_unlock_irq(&dev->err_lock);


        /* submit bulk in urb, which means no data to deliver */
        dev->bulk_in_filled = 0;
        dev->bulk_in_copied = 0;


        /* do it */
        rv = usb_submit_urb(dev->bulk_in_urb, GFP_KERNEL);
        if (rv < 0) {
                //dev_err(&dev->interface->dev,
                //        "%s - failed submitting read urb, error %d\n",
                //        __func__, rv);
                rv = (rv == -ENOMEM) ? rv : -EIO;
                spin_lock_irq(&dev->err_lock);


                spin_unlock_irq(&dev->err_lock);
        }
        //printk(KERN_ALERT"devusb_do_read_io\n");
        return rv;
}


static ssize_t usbdev_read(struct file *file, char *buffer, size_t count, loff_t *ppos)
{
        struct usb_devinfo *dev;
         
         
        int copyed=0;
        dev = file->private_data;
        if(dev->isOpen != 1 )
        {
                //printk(KERN_ALERT"close read err\n");
                return -1;
        }
        if ( !count)
                return 0;
        if(dev->UsbBuffer.iReadIndex==dev->UsbBuffer.iWriteIndex)
        {
                if (file->f_flags & O_NONBLOCK) 
                {
                        return copyed;
                }
                else
                {
                        
                        wait_event_interruptible(dev->wqReadOK, (dev->UsbBuffer.iReadIndex!=dev->UsbBuffer.iWriteIndex)||(dev->isOpen != 1 ) );
                }
        }
        if(dev->isOpen != 1 )
        {
                //printk(KERN_ALERT"close read err1\n");
                return -1;
        } 
        //下一步加锁
        mutex_lock(&dev->read_mutex); 
        while( dev->UsbBuffer.iReadIndex!=dev->UsbBuffer.iWriteIndex )
        {
                int iLen = dev->UsbBuffer.iLen[dev->UsbBuffer.iReadIndex];
                if( count>=iLen )
                {
                         
                         
                        if(copy_to_user(buffer+copyed, &dev->UsbBuffer.PackgeBuf[dev->UsbBuffer.iReadIndex][0],         iLen)!=0)
                        {
                                copyed = -EFAULT;
                                break;
                        }
                        dev->UsbBuffer.iReadIndex++;
                        if(dev->UsbBuffer.iReadIndex>=MAX_PAGE)
                        {
                                dev->UsbBuffer.iReadIndex = 0;
                        }
                        count-=iLen;
                        copyed+=iLen;
                        //printk("read r=%d w=%d\n",dev->UsbBuffer.iReadIndex,dev->UsbBuffer.iWriteIndex );
                        if(iLen<BUF_PER_PAGE)
                        {
                                break; 
                                
                        }
                         
                         
                }
                else
                {
                        break;
                }
                
        }
        mutex_unlock(&dev->read_mutex); 
        return copyed;
}


static void usbdev_write_callback(struct urb *urb)
{
        struct usb_devinfo *dev;


        dev = urb->context;


        /* sync/async unlink faults aren't errors */
        if (urb->status) {
                if (!(urb->status == -ENOENT ||
                    urb->status == -ECONNRESET ||
                    urb->status == -ESHUTDOWN))
                        dev_err(&dev->interface->dev,
                                "%s - nonzero write bulk status received: %d\n",
                                __func__, urb->status);


                spin_lock(&dev->err_lock);
                dev->errors = urb->status;
                spin_unlock(&dev->err_lock);
        }


        /* free up our allocated buffer */
        usb_free_coherent(urb->dev, urb->transfer_buffer_length,
                          urb->transfer_buffer, urb->transfer_dma);
        up(&dev->limit_sem);
}


static ssize_t usbdev_write(struct file *file, const char *user_buffer,
                          size_t count, loff_t *ppos)
{
        struct usb_devinfo *dev;
        int retval = 0;
        struct urb *urb = NULL;
        char *buf = NULL;
        size_t writesize = min(count, (size_t)MAX_TRANSFER);


        dev = file->private_data;
        if(dev->isOpen != 1 )
        {
                return -1;
        }
        /* verify that we actually have some data to write */
        if (count == 0)
                goto exit;


        /*
         * limit the number of URBs in flight to stop a user from using up all
         * RAM
         */
        if (!(file->f_flags & O_NONBLOCK)) {
                int ret = down_timeout(&dev->limit_sem,10);
                if (ret!=0) {
                        retval = -ERESTARTSYS;
                        goto exit;
                }
        } else {
                if (down_trylock(&dev->limit_sem)) {
                        retval = -EAGAIN;
                        goto exit;
                }
        }


        spin_lock_irq(&dev->err_lock);
        retval = dev->errors;
        if (retval < 0) {
                /* any error is reported once */
                dev->errors = 0;
                /* to preserve notifications about reset */
                retval = (retval == -EPIPE) ? retval : -EIO;
        }
        spin_unlock_irq(&dev->err_lock);
        if (retval < 0)
                goto error;


        /* create a urb, and a buffer for it, and copy the data to the urb */
        urb = usb_alloc_urb(0, GFP_KERNEL);
        if (!urb) {
                retval = -ENOMEM;
                goto error;
        }


        buf = usb_alloc_coherent(dev->udev, writesize, GFP_KERNEL,
                                 &urb->transfer_dma);
        if (!buf) {
                retval = -ENOMEM;
                goto error;
        }


        if (copy_from_user(buf, user_buffer, writesize)) {
                retval = -EFAULT;
                goto error;
        }


        /* this lock makes sure we don't submit URBs to gone devices */
        mutex_lock(&dev->io_mutex);
        if (!dev->interface) {                /* disconnect() was called */
                mutex_unlock(&dev->io_mutex);
                retval = -ENODEV;
                goto error;
        }


        /* initialize the urb properly */
        usb_fill_bulk_urb(urb, dev->udev,
                          usb_sndbulkpipe(dev->udev, dev->bulk_out_endpointAddr),
                          buf, writesize, usbdev_write_callback, dev);
        urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
        usb_anchor_urb(urb, &dev->submitted);


        /* send the data out the bulk port */
        retval = usb_submit_urb(urb, GFP_KERNEL); 
        mutex_unlock(&dev->io_mutex);
        if (retval) {
                dev_err(&dev->interface->dev,
                        "%s - failed submitting write urb, error %d\n",
                        __func__, retval);
                goto error_unanchor;
        }


        /*
         * release our reference to this urb, the USB core will eventually free
         * it entirely
         */
        usb_free_urb(urb);
         


        return writesize;


error_unanchor:
        usb_unanchor_urb(urb);
error:
        if (urb) {
                usb_free_coherent(dev->udev, writesize, buf, urb->transfer_dma);
                usb_free_urb(urb);
        }
        up(&dev->limit_sem);


exit:
        return retval;
}


static const struct file_operations usbdev_fops = {
        .owner =        THIS_MODULE,
        .read =                usbdev_read,
        .write =        usbdev_write,
        .open =                usbdev_open,
        .release =        usbdev_release,
        .flush =        usbdev_flush,
        .llseek =        noop_llseek,
};


/*
 * usb class driver info in order to get a minor number from the usb core,
 * and to have the device registered with the driver core
 */
static struct usb_class_driver usbdev_class = {
        .name =                "USBDev%d",
        .fops =                &usbdev_fops,
        .minor_base =        USB_DEV_MINOR_BASE,
};
static struct usb_class_driver usbdevother_class = {
        .name =                "USBOther%d",
        .fops =                &usbdev_fops,
        .minor_base =        USB_DEV_MINOR_BASE,
};
static int usbdev_probe(struct usb_interface *interface,
                      const struct usb_device_id *id)
{
        struct usb_devinfo *dev;
        struct usb_host_interface *iface_desc;
        struct usb_endpoint_descriptor *endpoint;
        size_t buffer_size;
        int i;
        int retval = -ENOMEM;
        int iInterfaceType=0;
        if(interface->cur_altsetting->desc.bInterfaceClass==0xff)
        {
                if(interface->cur_altsetting->desc.bInterfaceSubClass==0xff)
                {
                        iInterfaceType=1;
                }
                else
                {
                        iInterfaceType=2;
                }
        }
        if(iInterfaceType==0)
        {
                return ;
        } 
         
        /* allocate memory for our device state and initialize it */
        dev = kzalloc(sizeof(*dev), GFP_KERNEL);
        if (!dev) {
                dev_err(&interface->dev, "Out of memory\n");
                goto error;
        }
        kref_init(&dev->kref);
        sema_init(&dev->limit_sem, WRITES_IN_FLIGHT);
        mutex_init(&dev->io_mutex);
        spin_lock_init(&dev->err_lock);
        init_usb_anchor(&dev->submitted);




        dev->udev = usb_get_dev(interface_to_usbdev(interface));
        dev->interface = interface;


        /* set up the endpoint information */
        /* use only the first bulk-in and bulk-out endpoints */
        iface_desc = interface->cur_altsetting;
        for (i = 0; i < iface_desc->desc.bNumEndpoints; ++i) {
                endpoint = &iface_desc->endpoint[i].desc;


                if (!dev->bulk_in_endpointAddr &&
                    usb_endpoint_is_bulk_in(endpoint)) {
                        /* we found a bulk in endpoint */
                        buffer_size = usb_endpoint_maxp(endpoint);
                        dev->bulk_in_size = BUF_PER_PAGE;//buffer_size*100;
                        dev->bulk_in_endpointAddr = endpoint->bEndpointAddress;
                        dev->bulk_in_buffer = kmalloc(dev->bulk_in_size, GFP_KERNEL);
                        if (!dev->bulk_in_buffer) {
                                dev_err(&interface->dev,
                                        "Could not allocate bulk_in_buffer\n");
                                goto error;
                        }
                        dev->bulk_in_urb = usb_alloc_urb(0, GFP_KERNEL);
                        if (!dev->bulk_in_urb) {
                                dev_err(&interface->dev,
                                        "Could not allocate bulk_in_urb\n");
                                goto error;
                        }
                }


                if (!dev->bulk_out_endpointAddr &&
                    usb_endpoint_is_bulk_out(endpoint)) {
                        /* we found a bulk out endpoint */
                        dev->bulk_out_endpointAddr = endpoint->bEndpointAddress;
                }
        }
        if (!(dev->bulk_in_endpointAddr && dev->bulk_out_endpointAddr)) {
                dev_err(&interface->dev,
                        "Could not find both bulk-in and bulk-out endpoints\n");
                goto error;
        }


        /* save our data pointer in this interface device */
        usb_set_intfdata(interface, dev);


        /* we can register the device now, as it is ready */
        if(iInterfaceType==1)
        {
                retval = usb_register_dev(interface, &usbdev_class);
        }
        else
        {
                retval = usb_register_dev(interface, &usbdevother_class);
        }
        if (retval) {
                /* something prevented us from registering this driver */
                dev_err(&interface->dev,
                        "Not able to get a minor for this device.\n");
                usb_set_intfdata(interface, NULL);
                goto error;
        }


        memset(&dev->UsbBuffer,0,sizeof(cachebuf));
         init_waitqueue_head(&dev->wqReadOK);
         INIT_WORK(&(dev->work), work_func);
         mutex_init(&dev->read_mutex);
         dev->isOpen = 0 ;
         for(i=0;i< MAX_PAGE;i++)
         {
                 dev->UsbBuffer.PackgeBuf[i] = kmalloc(dev->bulk_in_size, GFP_KERNEL);
         }
        /* let the user know what node this device is now attached to */
        dev_info(&interface->dev,
                 "USB Skeleton device now attached to USBSkel-%d",
                 interface->minor);
         
        return 0;


error:
        if (dev)
                /* this frees allocated memory */
                kref_put(&dev->kref, usbdev_delete);
        return retval;
}


static void usbdev_disconnect(struct usb_interface *interface)
{
        struct usb_devinfo *dev;
        int minor = interface->minor;
        int iInterfaceType=0;
         
        if(interface->cur_altsetting->desc.bInterfaceClass==0xff)
        {
                if(interface->cur_altsetting->desc.bInterfaceSubClass==0xff)
                {
                        iInterfaceType=1;
                }
                else
                {
                        iInterfaceType=2;
                }
        }
        if(iInterfaceType==0)
        {
                return ;
        }
        
        dev = usb_get_intfdata(interface);
        dev->isOpen = 0 ;
         wake_up_interruptible(&dev->wqReadOK);         
        usb_set_intfdata(interface, NULL);


        /* give back our minor */
        if(iInterfaceType==1)
        {
                usb_deregister_dev(interface, &usbdev_class);
        }
        else
        {
                usb_deregister_dev(interface, &usbdevother_class);
        }


        /* prevent more I/O from starting */
        mutex_lock(&dev->io_mutex);
        dev->interface = NULL;
        mutex_unlock(&dev->io_mutex);


        usb_kill_anchored_urbs(&dev->submitted);


        /* decrement our usage count */
        kref_put(&dev->kref, usbdev_delete);
         
        dev_info(&interface->dev, "USB  #%d now disconnected", minor);
}


static void usbdev_emptyurb(struct usb_devinfo *dev)
{
        int time;


        time = usb_wait_anchor_empty_timeout(&dev->submitted, 1000);
        if (!time)
                usb_kill_anchored_urbs(&dev->submitted);
        usb_kill_urb(dev->bulk_in_urb);
}


static int usbdev_suspend(struct usb_interface *intf, pm_message_t message)
{
        struct usb_devinfo *dev = usb_get_intfdata(intf);


        if (!dev)
                return 0;
        usbdev_emptyurb(dev);
        return 0;
}


static int usbdev_resume(struct usb_interface *intf)
{
        return 0;
}


static int usbdev_pre_reset(struct usb_interface *intf)
{
        struct usb_devinfo *dev = usb_get_intfdata(intf);


        mutex_lock(&dev->io_mutex);
        usbdev_emptyurb(dev);


        return 0;
}


static int usbdev_post_reset(struct usb_interface *intf)
{
        struct usb_devinfo *dev = usb_get_intfdata(intf);


        /* we are sure no URBs are active - no locking needed */
        dev->errors = -EPIPE;
        mutex_unlock(&dev->io_mutex);


        return 0;
}


static struct usb_driver usbdev_driver = {
        .name =                "usbtest",
        .probe =        usbdev_probe,
        .disconnect =        usbdev_disconnect,
        .suspend =        usbdev_suspend,
        .resume =        usbdev_resume,
        .pre_reset =        usbdev_pre_reset,
        .post_reset =        usbdev_post_reset,
        .id_table =        usbdev_table,
        .supports_autosuspend = 1,
};


 
static int __init usbdev_init(void)
{
         
                return usb_register(&usbdev_driver);
        
}
module_init(usbdev_init);
static void __exit usbdev_exit(void)
{
        usb_deregister(&usbdev_driver);
}
module_exit(usbdev_exit);
MODULE_LICENSE("GPL");

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

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

更多推荐