Linux usb子系统(一) _写一个usb鼠标驱动
来源:互联网 发布:网络教育统考报名网 编辑:程序博客网 时间:2024/03/29 02:02
USB总线是一种典型的热插拔的总线标准,由于其优异的性能几乎成为了当下大小设备中的标配。
USB的驱动可以分为3类:SoC的USB控制器的驱动,主机端USB设备的驱动,设备上的USB Gadget驱动,通常,对于USB这种标准化的设备,内核已经将主机控制器的驱动编写好了,设备上的Gadget驱动通常只运行固件程序而不是基于Linux, 所以驱动工程师的主要工作就是编写主机端的USB设备驱动。
USB子系统框架
下图表示了Linux中USB子系统的框架结构,和i2c一样,USB子系统也可分为三层:**设备驱动层--USB核心--控制器驱动层*
作为热插拔总线, USB和非热插拔总线最大的区别就是总线无法事前获知设备的信息以及设备何时被插入或拔出,所以也就不能使用任意一种形式将设备信息事前写入内核。
为了解决由于热插拔引起的设备识别问题,USB总线通过枚举的方式来获取一个接入总线的USB设备的设备信息——一个由device->config->interface->endpoint逐级描述的设备,基于分离的思想,USB子系统中设计了一组结构来描述这几个维度的设备信息,相比之下,i2c总线只要一个i2c_client即可描述一个设备.
USB总线上的所有通信都是由主机发起的,所以本质上,USB都是采用轮询的方式进行的。USB总线会使用轮询的方式不断检测总线上是否有设备接入,如果有设备接入相应的D+D-就会有电平变化。然后总线就会按照USB规定的协议与设备进行通信,设备将存储在自身的设备信息依次交给主机,主机将这些信息按照4层模型组织起来。上报到内核,内核中的USB子系统再去匹配相应的驱动,USB设备驱动是面向interface这一层次的信息的
作为一种高度标准化的设备, 虽然USB本身十分复杂, 但是内核已经为我们完成了相当多的工作, 下述的常用设备驱动在内核中已经实现了。很多时候, 驱动的难度不是看设备的复杂程度, 而是看标准化程度
- 音频设备类
- 通信设备类
- HID设备类
- 显示设备类
- 海量存储设备类
- 电源设备类
- 打印设备类
- 集线器设备类
核心结构和方法简述
核心结构
基于分离的思想,USB子系统也提供了描述一个USB设备的结构,只不过基于USB协议,完整描述一个USB设备信息需要9个结构,这些结构中,前4个用来描述一个USB设备的硬件信息,即设备本身的信息,这些信息是写入到设备的eeprom的,在任何USB主机中看到的都一样,这些信息可以使用lsusb -v命令来查看; 后5个描述一个USB设备的软件信息,即除了硬件信息之外,Linux为了管理一个USB设备还要封装一些信息,是OS-specific的信息; USB设备硬件信息和软件信息的关系类似于中断子系统中的硬件中断和内核中断,只不过更复杂一点。
- usb_device_descriptor来描述一个USB设备的device信息
- usb_config_descriptor来描述一个device的config信息
- usb_interface_descriptor来描述一个config的interface信息
usb_endpoint_descriptor来描述一个interface的endpoint信息
- usb_device描述一个USB的device的软件信息,包括usb_device_descriptor
- urb_host_config描述一个USB设备config的软件信息,包括usb_config_descriptor
- usb_interface描述一个接口信息
- usb_host_interface描述一个interface的设置信息,包括usb_interface_descriptor,我们编写驱动就是针对这一层次的
usb_host_endpoint描述一个interdace的endpoint信息,包括usb_endpoint_descriptor,这是USB通信的最小单位,我们读写一个设备就是针对一个endpoint
- usb_driver描述一个usb设备驱动, 也就是USB设备驱动开发的核心结构
- usb_driver_id用来标识一个usb设备, 其实例id_table就是usb_driver中的一个域, 由于usb总线中描述一个设备的复杂性, 构造这样一个对象的方法也多种多样
- urb (usb request block)是在USB通信过程中的数据载体, 相当于i2c子系统中的i2c_msg, 网络设备驱动中的sk_buff
usb_hcd描述一个SoC中的USB控制器驱动
核心方法
- usb_fill_int_urb是注册urb的API, 是整个USB通信的核心数据封装
核心结构和方法详述
首先说的是那9个描述设备信息的结构, 其中的硬件信息是相互独立的, 分别使用 这些结构在内核"include/uapi/linux/usbch9.h"有定义, 我就不贴代码了
usb_device_descriptor
//include/uapi/linux/usbch9.h258 struct usb_device_descriptor { 259 __u8 bLength;260 __u8 bDescriptorType;262 __le16 bcdUSB;263 __u8 bDeviceClass;264 __u8 bDeviceSubClass;265 __u8 bDeviceProtocol;266 __u8 bMaxPacketSize0;267 __le16 idVendor;268 __le16 idProduct;269 __le16 bcdDevice;270 __u8 iManufacturer;271 __u8 iProduct;272 __u8 iSerialNumber;273 __u8 bNumConfigurations;274 } __attribute__ ((packed));
struct usb_device_descriptor
--263-->设备类别
--264-->设备子类
--265-->通信协议
--267-->销售商
--268-->产品ID
--272-->序列号
usb_config_descriptor
//include/uapi/linux/usbch9.h314 struct usb_config_descriptor {315 __u8 bLength;316 __u8 bDescriptorType;317 318 __le16 wTotalLength;319 __u8 bNumInterfaces;320 __u8 bConfigurationValue;321 __u8 iConfiguration;322 __u8 bmAttributes;323 __u8 bMaxPower;324 } __attribute__ ((packed));
usb_interface_descriptor
//include/uapi/linux/usbch9.h351 struct usb_interface_descriptor {352 __u8 bLength;353 __u8 bDescriptorType;354 355 __u8 bInterfaceNumber;356 __u8 bAlternateSetting;357 __u8 bNumEndpoints;358 __u8 bInterfaceClass;359 __u8 bInterfaceSubClass;360 __u8 bInterfaceProtocol;361 __u8 iInterface;362 } __attribute__ ((packed));
usb_endpoint_descriptor
//include/uapi/linux/usbch9.h369 struct usb_endpoint_descriptor {370 __u8 bLength;371 __u8 bDescriptorType;372 373 __u8 bEndpointAddress;374 __u8 bmAttributes;375 __le16 wMaxPacketSize;376 __u8 bInterval;377 378 /* NOTE: these two are _only_ in audio endpoints. */379 /* use USB_DT_ENDPOINT*_SIZE in bLength, not sizeof. */380 __u8 bRefresh;381 __u8 bSynchAddress;382 } __attribute__ ((packed));
usb_device
//include/linux/usb.h 510 struct usb_device { 511 int devnum; 512 char devpath[16]; 513 u32 route; 522 struct usb_device *parent; 523 struct usb_bus *bus; 524 struct usb_host_endpoint ep0; 526 struct device dev; 528 struct usb_device_descriptor descriptor; 529 struct usb_host_bos *bos; 530 struct usb_host_config *config; 532 struct usb_host_config *actconfig; 557 char *product; 558 char *manufacturer; 559 char *serial; 561 struct list_head filelist; 563 int maxchild; 568 unsigned long active_duration; 569 584 };
struct usb_device
--522-->这个设备的父设备, 通常就是usb塔形结构的上一个节点设备
--523-->所属的总线是usb总线
--526-->这是一个device, 会挂接到相应的链表
--528-->这个软件device结构包含的硬件device对象
--530-->软件device拥有的所有软件config对象, 对应硬件device拥有的所有硬件config
--531-->当下, 这个device正在使用的config
--557-->产品名
--558-->产品制造商
--559-->产品序列号
--561-->在这个设备上打开的usbfs文件的链表节点
--563-->子设备的最大数量
urb_host_config
//include/linux/usb.h 275 struct usb_host_config { 276 struct usb_config_descriptor desc; 278 char *string; /* iConfiguration string, if present */ 282 struct usb_interface_assoc_descriptor *intf_assoc[USB_MAXIADS]; 286 struct usb_interface *interface[USB_MAXINTERFACES]; 290 struct usb_interface_cache *intf_cache[USB_MAXINTERFACES]; 292 unsigned char *extra; /* Extra descriptors */ 293 int extralen; 294 };
struct usb_host_config
--276-->软件config对象包含的硬件config对象
--278-->config的名称
--282-->这个config上关联的Interface Association Descriptor
--283-->这个config上关联的下一级的软件interface数组,
usb_interface
下面这个就是与驱动直接匹配的描述
160 struct usb_interface { 163 struct usb_host_interface *altsetting; 165 struct usb_host_interface *cur_altsetting; 167 unsigned num_altsetting; /* number of alternate settings */ 171 struct usb_interface_assoc_descriptor *intf_assoc; 173 int minor; 175 enum usb_interface_condition condition; /* state of binding */ 176 unsigned sysfs_files_created:1; /* the sysfs attributes exist */ 177 unsigned ep_devs_created:1; /* endpoint "devices" exist */ 178 unsigned unregistering:1; /* unregistration is in progress */ 179 unsigned needs_remote_wakeup:1; /* driver requires remote wakeup */ 180 unsigned needs_altsetting0:1; /* switch to altsetting 0 is pending */ 181 unsigned needs_binding:1; /* needs delayed unbind/rebind */ 182 unsigned reset_running:1; 183 unsigned resetting_device:1; /* true: bandwidth alloc after reset */ 185 struct device dev; /* interface specific device info */ 186 struct device *usb_dev; 187 atomic_t pm_usage_cnt; /* usage counter for autosuspend */ 188 struct work_struct reset_ws; /* for resets in atomic context */ 189 };
struct usb_interface
--163-->这个interface包含的所有的setting
--164-->这个interface当前正在使用的setting
--165-->如果这个interface与一个使用了主设备号的驱动绑定了, 这个域就是interface的次设备号; 反之则没用. 驱动应该在probe中设置这个参数
usb_host_interface
77 struct usb_host_interface { 78 struct usb_interface_descriptor desc; 80 int extralen; 81 unsigned char *extra; /* Extra descriptors */ 86 struct usb_host_endpoint *endpoint; 88 char *string; /* iInterface string, if present */ 89 };
struct usb_host_interface
--78-->这个interface对应的硬件interface对象
--86-->拥有的描述软件endpoint信息的usb_host_endpoint数组
--88-->interface名称
usb_host_endpoint
endpoint是USB设备IO的基本单元
64 struct usb_host_endpoint { 65 struct usb_endpoint_descriptor desc; 66 struct usb_ss_ep_comp_descriptor ss_ep_comp; 67 struct list_head urb_list; 68 void *hcpriv; 69 struct ep_device *ep_dev; /* For sysfs info */ 71 unsigned char *extra; /* Extra descriptors */ 72 int extralen; 73 int enabled; 74 };
struct usb_host_endpoint
--65-->这个usb_host_endpoint对应的硬件endpoint信息
--67-->读写这个endpoint的usb链表, 由usb核心层(drivers/usb/core/file.c)维护
--73-->这个endpoint是否被使能了
每一个硬件信息对象都包含在一个软件信息对象中, 而软件信息对象是层层包含的, 所以虽然驱动是基于interface描述的, 但是我们可以使用"list_entry()"很容易的向上找到config和device描述, 使用其中的域很容易的找到endpoint描述, 这9个描述设备的结构关系如下图所示:
urb
与platform或i2c总线不同的是,usb总线不允许设备发起通信,所以作为设备驱动, 只有将需要的"材料"准备好经由核心层提交到usb控制器驱动,让控制器驱动带着这些"材料"去轮询设备并将应答带回。这些"材料"就是urb和相应的注册参数。当usb_driver和usb设备匹配上后,我们准备一个urb对象通过usb_fill_int_urb()/_bulk_/_control_注册到总线,并在合适的时机通过usb_submit_urb向控制器驱动发出发送这个urb对象的命令,总线的控制器驱动收到我们的发送命令之后,会根据我们注册时设置的周期不断向匹配的设备发送请求,如果子设备响应了我们的请求,控制器驱动就会将我们注册的urb对象填充好,并回调我们的注册函数。
对于海量存储USB设备,如果要让设备正常工作, 除了这些流程,还需要加上对缓存区的管理,这部分我们下篇说。
usb_fill_int_urb
初始化并注册一个中断urb。函数原型如下:
static inline 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)
这个函数参数比较多, urb表示我们要注册的urb对象; dev表示这个urb对象的目的设备; pipe表示读写管道, 使用usb_sndintpipe()和usb_rcvintpipe()获取; transfer_buffer表示传递数据的缓冲区首地址;buffer_length表示缓冲区长度;complete_fn表示如果我们发出的urb有了回应, 就回调这个函数; context是回调函数的参数, 由用户定义, 相当于request_irq中的void *dev; interval就是发送周期, 核心层会以这个参数为周期通过usb控制器驱动轮询设备,
usb_alloc()
urb和xxx一样,要用内核分配函数,其中会做一些初始化的工作
usb_fill_bulk_urb
初始化并注册一个海量存储urb
usb_fill_control_urb
初始化并注册一个控制urb
usb_submit_urb
通知内核发送urb对象
usb_driver
1048 struct usb_driver {1049 const char *name;1051 int (*probe) (struct usb_interface *intf,1052 const struct usb_device_id *id);1054 void (*disconnect) (struct usb_interface *intf);1056 int (*unlocked_ioctl) (struct usb_interface *intf, unsigned int code,1057 void *buf);1059 int (*suspend) (struct usb_interface *intf, pm_message_t message);1060 int (*resume) (struct usb_interface *intf);1061 int (*reset_resume)(struct usb_interface *intf);1063 int (*pre_reset)(struct usb_interface *intf);1064 int (*post_reset)(struct usb_interface *intf);1066 const struct usb_device_id *id_table;1068 struct usb_dynids dynids;1069 struct usbdrv_wrap drvwrap;1070 unsigned int no_dynamic_id:1;1071 unsigned int supports_autosuspend:1;1072 unsigned int disable_hub_initiated_lpm:1;1073 unsigned int soft_unbind:1;1074 };
struct usb_driver
--1049-->usb设备的名字
--1051-->探测函数, 当usb_driver的id_table和usb设备信息匹配的时候会执行, 主要的工作是申请资源, 初始化, 提供接口
--1054-->当驱动模块被卸载时或设备被拔出时会被执行
--1066-->功能依然是匹配一样, 只是usb的设备信息由4个维度描述, 所以id_table可以填充的内容也多种多样
usb_register
注册一个usb_driver到内核
usb_deregister
注销一个usb_driver
id_table
内核提供了如下的宏来构造一个usb_device_id对象, 其实也就是对usb_device_id中的不同域进行了填充, 由于设备的差异性, 不同的USB设备会上报不同的设备信息, 但无论上报哪些信息, 一定属于下面这些宏的一种封装. 可以先使用lsusb -v查看设备的硬件信息, 再根据其提供的硬件信息确定id_table编写相应的驱动
USB_DEVICE
811 #define USB_DEVICE(vend, prod) \ 812 .match_flags = USB_DEVICE_ID_MATCH_DEVICE, \ 813 .idVendor = (vend), \ 814 .idProduct = (prod)
USB_DEVICE_VER
825 #define USB_DEVICE_VER(vend, prod, lo, hi) \ 826 .match_flags = USB_DEVICE_ID_MATCH_DEVICE_AND_VERSION, \ 827 .idVendor = (vend), \ 828 .idProduct = (prod), \ 829 .bcdDevice_lo = (lo), \ 830 .bcdDevice_hi = (hi)
USB_DEVICE_INTERFACE_CLASS
841 #define USB_DEVICE_INTERFACE_CLASS(vend, prod, cl) \ 842 .match_flags = USB_DEVICE_ID_MATCH_DEVICE | \ 843 USB_DEVICE_ID_MATCH_INT_CLASS, \ 844 .idVendor = (vend), \ 845 .idProduct = (prod), \ 846 .bInterfaceClass = (cl)
USB_DEVICE_INTERFACE_PROTOCOL
857 #define USB_DEVICE_INTERFACE_PROTOCOL(vend, prod, pr) \ 858 .match_flags = USB_DEVICE_ID_MATCH_DEVICE | \ 859 USB_DEVICE_ID_MATCH_INT_PROTOCOL, \ 860 .idVendor = (vend), \ 861 .idProduct = (prod), \ 862 .bInterfaceProtocol = (pr)
USB_DEVICE_INTERFACE_NUMBER
873 #define USB_DEVICE_INTERFACE_NUMBER(vend, prod, num) \ 874 .match_flags = USB_DEVICE_ID_MATCH_DEVICE | \ 875 USB_DEVICE_ID_MATCH_INT_NUMBER, \ 876 .idVendor = (vend), \ 877 .idProduct = (prod), \ 878 .bInterfaceNumber = (num)
USB_DEVICE_INFO
889 #define USB_DEVICE_INFO(cl, sc, pr) \ 890 .match_flags = USB_DEVICE_ID_MATCH_DEV_INFO, \ 891 .bDeviceClass = (cl), \ 892 .bDeviceSubClass = (sc), \ 893 .bDeviceProtocol = (pr) 894
USB_INTERFACE_INFO
904 #define USB_INTERFACE_INFO(cl, sc, pr) \ 905 .match_flags = USB_DEVICE_ID_MATCH_INT_INFO, \ 906 .bInterfaceClass = (cl), \ 907 .bInterfaceSubClass = (sc), \ 908 .bInterfaceProtocol = (pr)
USB_DEVICE_AND_INTERFACE_INFO
924 #define USB_DEVICE_AND_INTERFACE_INFO(vend, prod, cl, sc, pr) \ 925 .match_flags = USB_DEVICE_ID_MATCH_INT_INFO \ 926 | USB_DEVICE_ID_MATCH_DEVICE, \ 927 .idVendor = (vend), \ 928 .idProduct = (prod), \ 929 .bInterfaceClass = (cl), \ 930 .bInterfaceSubClass = (sc), \ 931 .bInterfaceProtocol = (pr)
USB_VENDOR_AND_INTERFACE_INFO
946 #define USB_VENDOR_AND_INTERFACE_INFO(vend, cl, sc, pr) \ 947 .match_flags = USB_DEVICE_ID_MATCH_INT_INFO \ 948 | USB_DEVICE_ID_MATCH_VENDOR, \ 949 .idVendor = (vend), \ 950 .bInterfaceClass = (cl), \ 951 .bInterfaceSubClass = (sc), \ 952 .bInterfaceProtocol = (pr) 953
id_table实例
下面是内核"drdrivers/hid/usbhid/usbmouse.c"中的ib_table填写方式, 可以看出, 不仅构造使用了宏, 由于USB鼠标是标准设备, 它的属性值也有标准的宏来标识
230 static struct usb_device_id usb_mouse_id_table [] = {231 { USB_INTERFACE_INFO(USB_INTERFACE_CLASS_HID, USB_INTERFACE_SUBCLASS_BOOT,232 USB_INTERFACE_PROTOCOL_MOUSE) },233 { } /* Terminating entry */234 };
USB鼠标实例
内核"drivers/hid/usbhid/usbmouse.c"就是一个usb鼠标的驱动, 这里我根据自己的理解写了一个, 采用的是"usb中断设备驱动+input子系统"框架
#define BUF_SIZE 8MODULE_LICENSE("GPL");//面向对象, 根据需求封装类struct xj_mouse { char name[128]; struct usb_device *dev; struct urb *msg; struct input_dev *input; signed char *buf;};struct xj_mouse *mouse;static int usb_mouse_open(struct input_dev *dev){ struct xj_mouse *mouse = input_get_drvdata(dev); mouse->msg->dev = mouse->dev; if (usb_submit_urb(mouse->msg, GFP_KERNEL)) return -EIO; return 0;}static void usb_mouse_close(struct input_dev *dev){ struct xj_mouse *mouse = input_get_drvdata(dev); usb_kill_urb(mouse->msg);}static int init_input(struct usb_interface * intf){ int err=0; struct usb_device *dev = mouse->dev; struct input_dev *input_dev = mouse->input; input_dev->name = mouse->name; usb_to_input_id(dev, &input_dev->id); input_dev->dev.parent = &intf->dev; input_dev->evbit[0] = BIT_MASK(EV_KEY) | BIT_MASK(EV_REL); input_dev->keybit[BIT_WORD(BTN_MOUSE)] = BIT_MASK(BTN_LEFT) |BIT_MASK(BTN_RIGHT) | BIT_MASK(BTN_MIDDLE); input_dev->relbit[0] = BIT_MASK(REL_X) | BIT_MASK(REL_Y); input_dev->keybit[BIT_WORD(BTN_MOUSE)] |= BIT_MASK(BTN_SIDE) |BIT_MASK(BTN_EXTRA); input_dev->relbit[0] |= BIT_MASK(REL_WHEEL); input_set_drvdata(input_dev, mouse); input_dev->open = usb_mouse_open; input_dev->close = usb_mouse_close; err = input_register_device(mouse->input); return 0;}static void completion(struct urb * msg){ int status; signed char *buf = mouse->buf; struct input_dev *input = mouse->input; input_report_key(input, BTN_LEFT, buf[0] & 0x01); input_report_key(input, BTN_RIGHT, buf[0] & 0x02); input_report_key(input, BTN_MIDDLE, buf[0] & 0x04); input_report_key(input, BTN_SIDE, buf[0] & 0x08); input_report_key(input, BTN_EXTRA, buf[0] & 0x10); input_report_rel(input, REL_X, buf[1]); input_report_rel(input, REL_Y, buf[2]); input_report_rel(input, REL_WHEEL, buf[3]); input_sync(input); status = usb_submit_urb (msg, GFP_ATOMIC);}static int probe(struct usb_interface *intf, const struct usb_device_id *id){ int pipe; struct usb_host_interface *interface; struct usb_endpoint_descriptor *endpoint; //分配、初始化个性结构 mouse = (struct xj_mouse *)kzalloc(sizeof(struct xj_mouse),GFP_KERNEL); mouse->dev=interface_to_usbdev(intf); mouse->msg=usb_alloc_urb(0,GFP_KERNEL); mouse->input=input_allocate_device(); mouse->buf=(void *)kzalloc(BUF_SIZE,GFP_KERNEL); if (mouse->dev->manufacturer){ strlcpy(mouse->name, mouse->dev->manufacturer, sizeof(mouse->name)); mouse->input->name = mouse->name; } //初始化input设备 init_input(intf); //获取pipe interface=intf->cur_altsetting; endpoint=&interface->endpoint[0].desc; /* 使用dev和endpoint获取端点地址 */ pipe = usb_rcvintpipe(mouse->dev,endpoint->bEndpointAddress); //注册usb驱动 usb_fill_int_urb(mouse->msg,mouse->dev,pipe,mouse->buf,BUF_SIZE,completion,mouse->msg,endpoint->bInterval); return 0;}static void disconnect(struct usb_interface *intf){ struct xj_mouse *tmp_mouse = usb_get_intfdata (intf); usb_set_intfdata(intf, NULL); if (tmp_mouse) { usb_kill_urb(tmp_mouse->msg); input_unregister_device(tmp_mouse->input); usb_free_urb(tmp_mouse->msg); kfree(tmp_mouse->buf); kfree(tmp_mouse); }}static struct usb_device_id id_table [] ={ { USB_INTERFACE_INFO(USB_INTERFACE_CLASS_HID, USB_INTERFACE_SUBCLASS_BOOT,USB_INTERFACE_PROTOCOL_MOUSE) }, {},};struct usb_driver mouse_drv = { .name = "xj_mouse_drv", .probe = probe, .disconnect = disconnect, .id_table = id_table,};module_usb_driver(mouse_drv);
- Linux usb子系统(一) _写一个usb鼠标驱动
- Linux usb子系统(一) _写一个usb鼠标驱动
- Linux usb子系统(一) _写一个usb鼠标驱动
- Linux usb子系统(一) _写一个usb鼠标驱动
- Linux usb子系统(一) _写一个usb鼠标驱动
- Linux usb子系统(一) _写一个usb鼠标驱动
- Linux usb子系统(一) _写一个usb鼠标驱动
- Linux usb子系统(一) _写一个usb鼠标驱动
- Linux usb子系统(一) _写一个usb鼠标驱动
- linux驱动子系统--USB
- Linux-USB鼠标驱动
- Linux设备驱动子系统 - USB
- linux USB子系统(一)
- USB驱动--USB鼠标
- Linux usb子系统(一):子系统架构
- Linux usb子系统(一):子系统架构
- Linux驱动之usb鼠标
- linux驱动由浅入深系列:usb子系统之四(android平台鼠标驱动代码分析)
- 什么,听说3分钟入门Cython??
- 特征选择与降维总结
- 【第16题】360校园招聘2015届技术类笔试题
- grep命令
- 收录SQL常用语句大全
- Linux usb子系统(一) _写一个usb鼠标驱动
- 配置描述符介绍
- POJ 1979 Red and Black
- 【第17题】360校园招聘2015届技术类笔试题
- java并发与多线程总结
- Tensorflow在Android上的应用(Windows、Mac、Linux)
- 在angular-cli项目中使用Scss和Pug(Jade)模板引擎
- 使用Ecplise git commit时出现"There are no stages files"
- python入门(四)