基于s5pv-210开发板 usb设备侧驱动
2016-05-17 18:16
519 查看
在
USB 设备的逻辑组织中,包含设备、配置、接口和端点
4 个层次
每个USB
设备都提供了不同级别的配置信息,可以包含一个或多个配置,不同的配置使设备表现出不同的功能组合(在探测/连接期间需从其中选定一个),配置由多个接口组成。
在
USB 协议中,接口由多个端点组成,代表一个基本的功能,是
USB 设备驱动程序控制的对象,一个功能复杂的USB
设备可以具有多个接口。每个配置中可以有多个接口,而设备接口是端点的汇集(
collection)。例如,
USB
扬声器可以包含一个音频接口以及对旋钮和按钮的接口。一个配置中的所有接口可以同时有效,并可被不同的驱动程序连接。每个接口可以有备用接口,以提供不同质量的服务参数。
端点是
USB 通信的最基本形式,每一个
USB 设备接口在主机看来就是一个端点的集合,主机只能通过端点与设备进行通信,以使用设备的功能。在USB
系统中每一个端点都有惟一的地址,这是由设备地址和端点号给出的。每个端点都有一定的属性,其中包括传输方式、总线访问频率、带宽、端点号和数据包的最大容量等。一个USB
端点只能在一个方向承载数据,或者从主机到设备(称为输出端点),或者从设备到主机(称为输入端点),因此端点可看作一个单向的管道。端点0
通常为控制端点,用于设备初始化参数等。只要设备连接到
USB 上并且上电端点
0 就可以被访问。端点1、2
等一般用作数据端点,存放主机与设备间往来的数据。
usb 逻辑单元图:
![](http://img.blog.csdn.net/20160517091803629)
设备通常有一个或多个配置;
配置通常有一个或多个接口;
接口通常有一个或多个设置;
接口有零或多个端点。
设备结构体:
代表了一个插入的USB设备,在内核使用数据结构
struct usb_device来描述整个USB设备。(include/linux/usb.h)
和修订
ID,支持的设备类、子类和适用的协议以及默认端点的最大包大小等。在Linux
内核中,
USB 设备用usb_device
结构体来描述,
USB 设备描述符定义为
usb_device_descriptor结构体:
配置结构体:
一个USB设备可以有多个配置,并可在它们之间转换以改变设备的状态。比如一个设备可以通过下载固件(firmware)的方式改变设备的使用状态(我感觉类似FPGA或CPLD),那么USB设备就要切换配置,来完成这个工作。一个时刻只能有一个配置可以被激活。Linux使用结构
struct usb_host_config 来描述USB配置。我们编写的USB设备驱动通常不需要读写这些结构的任何值。可在内核源码的文件include/linux/usb.h中找到对它们的描述。
配置在内核中使用
usb_host_config结构体描述,
USB
配置描述符定义为结构体 usb_config_descriptor
接口结构体:
USB端点被绑为接口,USB接口只处理一种USB逻辑连接。一个USB接口代表一个基本功能,每个USB驱动控制一个接口。所以一个物理上的硬件设备可能需要一个以上的驱动程序。这可以在“晕到死
差屁”系统中看出,有时插入一个USB设备后,系统会识别出多个设备,并安装相应多个的驱动。
USB 接口可以有其他的设置,它是对接口参数的不同选择. 接口的初始化的状态是第一个设置,编号为0。 其他的设置可以以不同方式控制独立的端点。
USB接口在内核中使用 struct usb_interface 来描述。USB 核心将其传递给USB驱动,并由USB驱动负责后续的控制。
接口描述符:接口类、子类和适用的协议,接口备用配置的数目和端点数目。USB
接口在 内 核 中 使 用
usb_interface结 构 体 描 述 ,
USB
接 口 描 述 符 定 义 为 结 构 体usb_interface_descriptor
端点
USB 通讯的最基本形式是通过一个称为端点的东西。一个USB端点只能向一个方向传输数据(从主机到设备(称为输出端点)或者从设备到主机(称为输入端点))。端点可被看作一个单向的管道。
一个 USB 端点有 4 种不同类型, 分别具有不同的数据传送方式:
控制CONTROL
控制端点被用来控制对 USB 设备的不同部分访问. 通常用作配置设备、获取设备信息、发送命令到设备或获取设备状态报告。这些端点通常较小。每个 USB 设备都有一个控制端点称为"端点 0", 被 USB 核心用来在插入时配置设备。USB协议保证总有足够的带宽留给控制端点传送数据到设备.
中断INTERRUPT
每当 USB 主机向设备请求数据时,中断端点以固定的速率传送小量的数据。此为USB 键盘和鼠标的主要的数据传送方法。它还用以传送数据到 USB 设备来控制设备。通常不用来传送大量数据。USB协议保证总有足够的带宽留给中断端点传送数据到设备.
批量BULK
批量端点用以传送大量数据。这些端点常比中断端点大得多. 它们普遍用于不能有任何数据丢失的数据。USB 协议不保证传输在特定时间范围内完成。如果总线上没有足够的空间来发送整个BULK包,它被分为多个包进行传输。这些端点普遍用于打印机、USB Mass Storage和USB网络设备上。
等时ISOCHRONOUS
等时端点也批量传送大量数据, 但是这个数据不被保证能送达。这些端点用在可以处理数据丢失的设备中,并且更多依赖于保持持续的数据流。如音频和视频设备等等。
控制和批量端点用于异步数据传送,而中断和同步端点是周期性的。这意味着这些端点被设置来在固定的时间连续传送数据,USB 核心为它们保留了相应的带宽。
端点在内核中使用结构 struct usb_host_endpoint 来描述
usb_host_endpoint 所包含的真实端点信息在另一个结构中:struct usb_endpoint_descriptor(端点描述符,包含所有的USB特定数据)。
端点描述符:端点地址、方向和类型,支持的最大包大小,如果是中断类型的端点则还包括轮询频率。
字符串描述符:在其他描述符中会为某些字段提供字符串索引,它们可被用来检索描述性字符串,可以以多种语言形式提供。字符串描述符是可选的,有的设备有,有的设备没有,字符串描述符对应于usb_string_descriptor结构体
例如,笔者在
PC 上插入一个
SanDisk U 盘后,通过
lsusb 命令得到这个
U 盘相关的描述符,从中可以显示这个U
盘包含了一个设备描述符、一个配置描述符、一个接口描述符以及批量输入和批量输出两个端点描述符。呈现出来的信息内容直接对应于usb_device_descriptor、usb_config_
descriptor、usb_interface_descriptor、usb_endpoint_descriptor、usb_string_descriptor结构体
USB驱动程序框架:
app:
-------------------------------------------
USB设备驱动程序 // 知道数据含义
内核 --------------------------------------
USB总线驱动程序 // 1. 识别, 2. 找到匹配的设备驱动, 3. 提供USB读写函数 (它不知道数据含义)
-------------------------------------------
USB主机控制器
UHCI OHCI EHCI
硬件 -----------
USB设备
UHCI: intel, 低速(1.5Mbps)/全速(12Mbps)
OHCI: microsoft 低速/全速
EHCI: 高速(480Mbps)
USB总线驱动程序的作用
1. 识别USB设备
1.1 分配地址
1.2 并告诉USB设备(set address)
1.3 发出命令获取描述符
描述符的信息可以在include\linux\usb\Ch9.h看到
2. 查找并安装对应的设备驱动程序
3. 提供USB读写函数
usb设备驱动 ---鼠标
使用
usb_driver结构体描述一个
USB
设备驱动,usb_driver结构体的定义如下:
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 (*ioctl) (struct usb_interface*intf,
unsigned int code,
void *buf); /*I/O
控制函数*/
int (*suspend) (struct usb_interface*intf,
pm_message_t message);/*挂起函数*/
int (*resume) (struct usb_interface*intf);
/*恢复函数
*/
int (*reset_resume)(struct usb_interface*intf);
void (*pre_reset) (struct usb_interface*intf);
void (*post_reset) (struct usb_interface*intf);
const struct usb_device_id*id_table;/*usb_device_id表指针
*/
struct usb_dynids dynids;
struct usbdrv_wrap drvwrap;
unsigned
int no_dynamic_id:1;
unsigned int supports_autosuspend:1;
unsigned int soft_unbind:1;
};
usb_driver结构体中的
id_table成员描述了这个
USB
驱动所支持的 USB
设备列表,它指向一个usb_device_id数组,
usb_device_id结构体用于包含
USB
设备的制造商 ID、产品
ID、产品版本、设备类、接口类等信息及其要匹配标志成员match_flags(标明要与哪些成员匹配,包含DEV_LO、DEV_HI、DEV_CLASS、DEV_SUBCLASS、DEV_PROTOCOL、INT_CLASS、INT_SUBCLASS、INT_PROTOCOL)。可以借助下面一组宏来生成usb_device_id结构体的实例:
USB_INTERFACE_INFO(class,
subclass, protocol)
该宏用于创建一个匹配接口指定类型的
usb_device_id结构体实例。
USB_DEVICE_INFO(class, subclass, protocol)
该宏用于创建一个匹配设备指定类型的
usb_device_id结构体实例。
USB_DEVICE(vendor, product)
该宏根据制造商
ID 和产品
ID 生成一个
usb_device_id结构体的实例,在数组中增加该元素将意味着该驱动可支持匹配制造商ID、产品ID
的设备。
USB_DEVICE_VER(vendor,
product, lo, hi)
该宏根据制造商
ID、产品
ID、产品版本的最小值和最大值生成一个
usb_device_id结构体的实例,在数组中增加该元素将意味着该驱动可支持匹配制造商ID、产品ID
和
lo~hi范围内版本的设备。
当USB核心检测到某个设备的属性和某个驱动程序的usb_device_id结构体所携带的信息一致时,这个驱动程序的probe()函数就被执行。拔掉设备或者卸掉驱动模块后,USB核心就执行disconnect()函数来响应这个动作。
上述usb_driver结构体中的函数是
USB
设备驱动中 USB
相关的部分,而 USB
只是一个总线,真正的USB
设备驱动的主体工作仍然是
USB 设备本身所属类型的驱动,如字符设备、
tty设备、块设备、输入设备等。因此USB
设备驱动包含其作为总线上挂在设备的驱动和本身所属设备类型的驱动两部分。
与platform_driver类似,
usb_driver起到了“牵线”的作用,即在
probe()里注册相应的字符、tty等设备,在
disconnect()注销相应的字符、
tty
等设备,而原先对设备的注册和注销一般直接发生在模块加载和卸载函数中.
尽管USB
本身所属设备驱动的结构与其不挂在
USB 总线上时完全相同,但是在访问方式上却发生了很大的变化,例如,对于
USB
接口的字符设备而言,尽管仍然是 write()、
read()、
ioctl()这些函数,但是在这些函数中,贯穿始终的是称为
URB
的 USB
请求块。
我们把树根比作主机控制器,树叶比作具体的USB
设备,树干和树枝就是
USB
总线。树叶本身与树枝通过 usb_driver连接,而树叶本身的驱动(读写、控制)则需要通过其树叶设备本身所属类设备驱动来完成。树根和树叶之间的“通信”依靠在树干和树枝里“流淌”的URB
来完成。
urb结构体
USB
请求块( USB request block,urb)是USB
设备驱动中用来描述与
USB 设备通信所用的基本载体和核心数据结构,非常类似于网络设备驱动中的
sk_buff结构体。
struct urb
2 {
3 /* 私有的:只能由usb核心和主机控制器访问的字段 */
4 struct kref kref; /*urb引用计数 */
5 spinlock_t lock; /* urb锁 */
6 void *hcpriv; /* 主机控制器私有数据 */
7 int bandwidth; /* int/iso请求的带宽 */
8 atomic_t use_count; /* 并发传输计数 */
9 u8 reject; /* 传输将失败*/
10
11 /* 公共的: 可以被驱动使用的字段 */
12 struct list_head urb_list; /* 链表头*/
13 struct usb_device *dev; /* 关联的usb设备 */
14 unsigned int pipe; /* 管道信息 */
15 int status; /* urb的当前状态 */
16 unsigned int transfer_flags; /* urb_short_not_ok | ...*/
17 void *transfer_buffer; /* 发送数据到设备或从设备接收数据的缓冲区 */
18 dma_addr_t transfer_dma; /*用来以dma方式向设备传输数据的缓冲区 */
19 int transfer_buffer_length;/*transfer_buffer或transfer_dma 指向缓冲区的大小 */
20
21 int actual_length; /* urb结束后,发送或接收数据的实际长度 */
22 unsigned char *setup_packet; /* 指向控制urb的设置数据包的指针*/
23 dma_addr_t setup_dma; /*控制urb的设置数据包的dma缓冲区*/
24 int start_frame; /*等时传输中用于设置或返回初始帧*/
25 int number_of_packets; /*等时传输中等时缓冲区数据 */
26 int interval; /* urb被轮询到的时间间隔(对中断和等时urb有效) */
27 int error_count; /* 等时传输错误数量 */
28 void *context; /* completion函数上下文 */
29 usb_complete_t complete; /* 当urb被完全传输或发生错误时,被调用 */
30 struct usb_iso_packet_descriptor iso_frame_desc[0];
31 /*单个urb一次可定义多个等时传输时,描述各个等时传输 */
32 };
urb
处理流程
USB
设备中的每个端点都处理一个 urb
队列,在队列被清空之前,一个 urb
的典型生命周期如下:
(1)被一个USB
设备驱动创建。
创建
urb 结构体的函数为:
struct urb
*usb_alloc_urb(int
iso_packets, int mem_flags);
iso_packets是这个
urb
应当包含的等时数据包的数目,若为 0
表示不创建等时数据包。
mem_flags参数是分配内存的标志,和
kmalloc()函数的分配标志参数含义相同。如果分配成功,该函数返回一个urb
结构体指针,否则返回
0。
urb
结构体在驱动中不能静态创建,因为这可能破坏
USB 核心给
urb 使用的引用计数方法。
(2)初始化,被安排给一个特定USB
设备的特定端点。
对于中断
urb,使用
usb_fill_int_urb()函数来初始化urb:
urb参数指向要被初始化的
urb
的指针; dev
指向这个 urb
要被发送到的 USB
设备; pipe
是这个urb
要被发送到的
USB 设备的特定端点;
transfer_buffer是指向发送数据或接收数据的缓冲区的指针,和urb
一样,它也不能是静态缓冲区,必须使用
kmalloc()来分配;buffer_length是
transfer_buffer指针所指向缓冲区的大小;
complete
指针指向当这个 urb
完成时被调用的完成处理函数;context
是完成处理函数的“上下文”;
interval是这个
urb 应当被调度的间隔。
( 3)被 USB 设备驱动提交给 USB 核心。
在完成创建和初始化urb
后,
urb 便可以提交给
USB 核心,通过
usb_submit_urb()函数来完成,如下所示:
int usb_submit_urb(struct
urb *urb, int mem_flags);
urb
参数是指向 urb
的指针, mem_flags参数与传递给
kmalloc()函数参数的意义相同,它用于告知USB
核心如何在此时分配内存缓冲区。
在提交
urb 到
USB 核心后,直到完成函数被调用之前,不要访问
urb 中的任何成员。
usb_submit_urb()在原子上下文和进程上下文中都可以被调用,mem_flags变量需根据调用环境进行相应的设置,如下所示。
GFP_ATOMIC:在中断处理函数、底半部、tasklet、定时器处理函数以及urb
完成函数中,在调用者持有自旋锁或者读写锁时以及当驱动将
current→state修改为非
TASK_RUNNING时,应使用此标志。
GFP_NOIO:在存储设备的块I/O
和错误处理路径中,应使用此标志;
GFP_KERNEL:如果没有任何理由使用GFP_ATOMIC和
GFP_NOIO,就使用GFP_KERNEL。
如果
usb_submit_urb()调用成功,即urb
的控制权被移交给
USB 核心,该函数返回
0;否则,返回错误号。
(4)提交由USB
核心指定的
USB主机控制器驱动。
( 5)被USB
主机控制器处理,进行一次到
USB设备的传送。
第( 4) ~(5)步由USB
核心和主机控制器完成,不受
USB设备驱动的控制。
( 6)当urb
完成, USB主机控制器驱动通知
USB
设备驱动。
在如下3
种情况下,
urb 将结束,
urb 完成函数将被调用。
urb被成功发送给设备,并且设备返回正确的确认。如果
urb→status为
0,意味着对于一个输出urb,数据被成功发送;对于一个输入urb,请求的数据被成功收到。
如果发送数据到设备或从设备接收数据时发生了错误,urb→status将记录错误值。
urb被从
USB
核心“去除连接”,这发生在驱动通过 usb_unlink_urb()或usb_kill_urb()函数取消urb,或urb
虽已提交,而
USB 设备被拔出的情况下
USB 设备的逻辑组织中,包含设备、配置、接口和端点
4 个层次
每个USB
设备都提供了不同级别的配置信息,可以包含一个或多个配置,不同的配置使设备表现出不同的功能组合(在探测/连接期间需从其中选定一个),配置由多个接口组成。
在
USB 协议中,接口由多个端点组成,代表一个基本的功能,是
USB 设备驱动程序控制的对象,一个功能复杂的USB
设备可以具有多个接口。每个配置中可以有多个接口,而设备接口是端点的汇集(
collection)。例如,
USB
扬声器可以包含一个音频接口以及对旋钮和按钮的接口。一个配置中的所有接口可以同时有效,并可被不同的驱动程序连接。每个接口可以有备用接口,以提供不同质量的服务参数。
端点是
USB 通信的最基本形式,每一个
USB 设备接口在主机看来就是一个端点的集合,主机只能通过端点与设备进行通信,以使用设备的功能。在USB
系统中每一个端点都有惟一的地址,这是由设备地址和端点号给出的。每个端点都有一定的属性,其中包括传输方式、总线访问频率、带宽、端点号和数据包的最大容量等。一个USB
端点只能在一个方向承载数据,或者从主机到设备(称为输出端点),或者从设备到主机(称为输入端点),因此端点可看作一个单向的管道。端点0
通常为控制端点,用于设备初始化参数等。只要设备连接到
USB 上并且上电端点
0 就可以被访问。端点1、2
等一般用作数据端点,存放主机与设备间往来的数据。
usb 逻辑单元图:
设备通常有一个或多个配置;
配置通常有一个或多个接口;
接口通常有一个或多个设置;
接口有零或多个端点。
设备结构体:
代表了一个插入的USB设备,在内核使用数据结构
struct usb_device来描述整个USB设备。(include/linux/usb.h)
struct usb_device { int devnum; //设备号,是在USB总线的地址 char devpath [16]; //用于消息的设备ID字符串 enum usb_device_state state; //设备状态:已配置、未连接等等 enum usb_device_speed speed; //设备速度:高速、全速、低速或错误 struct usb_tt *tt; //处理传输者信息;用于低速、全速设备和高速HUB int ttport; //位于tt HUB的设备口 unsigned int toggle[2]; //每个端点的占一位,表明端点的方向([0] = IN, [1] = OUT) struct usb_device *parent; //上一级HUB指针 struct usb_bus *bus; //总线指针 struct usb_host_endpoint ep0; //端点0数据 struct device dev; //一般的设备接口数据结构 struct usb_device_descriptor descriptor; //USB设备描述符 struct usb_host_config *config; //设备的所有配置 struct usb_host_config *actconfig; //被激活的设备配置 struct usb_host_endpoint *ep_in[16]; //输入端点数组 struct usb_host_endpoint *ep_out[16]; //输出端点数组 char **rawdescriptors; //每个配置的raw描述符 unsigned short bus_mA; //可使用的总线电流 u8 portnum;//父端口号 u8 level; //USB HUB的层数 unsigned can_submit:1; //URB可被提交标志 unsigned discon_suspended:1; //暂停时断开标志 unsigned persist_enabled:1; //USB_PERSIST使能标志 unsigned have_langid:1; //string_langid存在标志 unsigned authorized:1; unsigned authenticated:1; unsigned wusb:1; //无线USB标志 int string_langid; //字符串语言ID /* static strings from the device */ //设备的静态字符串 char *product; //产品名 char *manufacturer; //厂商名 char *serial; //产品串号 struct list_head filelist; //此设备打开的usbfs文件 #ifdef CONFIG_USB_DEVICE_CLASS struct device *usb_classdev; //用户空间访问的为usbfs设备创建的USB类设备 #endif #ifdef CONFIG_USB_DEVICEFS struct dentry *usbfs_dentry; //设备的usbfs入口 #endif int maxchild; //(若为HUB)接口数 struct usb_device *children[USB_MAXCHILDREN];//连接在这个HUB上的子设备 int pm_usage_cnt; //自动挂起的使用计数 u32 quirks; atomic_t urbnum; //这个设备所提交的URB计数 unsigned long active_duration; //激活后使用计时 #ifdef CONFIG_PM //电源管理相关 struct delayed_work autosuspend; //自动挂起的延时 struct work_struct autoresume; //(中断的)自动唤醒需求 struct mutex pm_mutex; //PM的互斥锁 unsigned long last_busy; //最后使用的时间 int autosuspend_delay; unsigned long connect_time; //第一次连接的时间 unsigned auto_pm:1; //自动挂起/唤醒 unsigned do_remote_wakeup:1; //远程唤醒 unsigned reset_resume:1; //使用复位替代唤醒 unsigned autosuspend_disabled:1; //挂起关闭 unsigned autoresume_disabled:1; //唤醒关闭 unsigned skip_sys_resume:1; //跳过下个系统唤醒 #endif struct wusb_dev *wusb_dev; //(如果为无线USB)连接到WUSB特定的数据结构 };设备描述符:关于设备的通用信息,如供应商ID、产品ID
和修订
ID,支持的设备类、子类和适用的协议以及默认端点的最大包大小等。在Linux
内核中,
USB 设备用usb_device
结构体来描述,
USB 设备描述符定义为
usb_device_descriptor结构体:
struct usb_device_descriptor { __u8 bLength;//设备描述符的字节数大小,为0x12 __u8 bDescriptorType;//描述符类型编号,为0x01 __le16 bcdUSB;//USB版本号 __u8 bDeviceClass;//USB分配的设备类代码,0x01~0xfe为标准设备类,0xff为厂商自定义类型 //0x00不是在设备描述符中定义的,如HID __u8 bDeviceSubClass;//usb分配的子类代码,同上,值由USB规定和分配的 __u8 bDeviceProtocol;//USB分配的设备协议代码,同上 __u8 bMaxPacketSize0;//端点0的最大包的大小 __le16 idVendor;//厂商编号 __le16 idProduct;//产品编号 __le16 bcdDevice;//设备出厂编号 __u8 iManufacturer;//描述厂商字符串的索引 __u8 iProduct;//描述产品字符串的索引 __u8 iSerialNumber;//描述设备序列号字符串的索引 __u8 bNumConfigurations;//可能的配置数量 } __attribute__ ((packed));
配置结构体:
一个USB设备可以有多个配置,并可在它们之间转换以改变设备的状态。比如一个设备可以通过下载固件(firmware)的方式改变设备的使用状态(我感觉类似FPGA或CPLD),那么USB设备就要切换配置,来完成这个工作。一个时刻只能有一个配置可以被激活。Linux使用结构
struct usb_host_config 来描述USB配置。我们编写的USB设备驱动通常不需要读写这些结构的任何值。可在内核源码的文件include/linux/usb.h中找到对它们的描述。
struct usb_host_config { struct usb_config_descriptor desc; //配置描述符 char *string; /* 配置的字符串指针(如果存在) */ struct usb_interface_assoc_descriptor *intf_assoc[USB_MAXIADS]; //配置的接口联合描述符链表 struct usb_interface *interface[USB_MAXINTERFACES]; //接口描述符链表 struct usb_interface_cache *intf_cache[USB_MAXINTERFACES]; unsigned char *extra; /* 额外的描述符 */ int extralen; };配置描述符:此配置中的接口数、支持的挂起和恢复能力以及功率要求。USB
配置在内核中使用
usb_host_config结构体描述,
USB
配置描述符定义为结构体 usb_config_descriptor
struct usb_config_descriptor { __u8 bLength;//设备描述符的字节数大小,为0x12 __u8 bDescriptorType;//描述符类型编号,为0x01 __le16 wTotalLength;//配置所返回的所有数量的大小 __u8 bNumInterfaces;//此配置所支持的接口数量 __u8 bConfigurationValue;//Set_Configuration命令需要的参数值 __u8 iConfiguration;//描述该配置的字符串的索引值 __u8 bmAttributes;//供电模式的选择 __u8 bMaxPower;//设备从总线提取的最大电流 } __attribute__ ((packed));
接口结构体:
USB端点被绑为接口,USB接口只处理一种USB逻辑连接。一个USB接口代表一个基本功能,每个USB驱动控制一个接口。所以一个物理上的硬件设备可能需要一个以上的驱动程序。这可以在“晕到死
差屁”系统中看出,有时插入一个USB设备后,系统会识别出多个设备,并安装相应多个的驱动。
USB 接口可以有其他的设置,它是对接口参数的不同选择. 接口的初始化的状态是第一个设置,编号为0。 其他的设置可以以不同方式控制独立的端点。
USB接口在内核中使用 struct usb_interface 来描述。USB 核心将其传递给USB驱动,并由USB驱动负责后续的控制。
struct usb_interface { struct usb_host_interface *altsetting; /* 包含所有可用于该接口的可选设置的接口结构数组。每个 struct usb_host_interface 包含一套端点配置(即struct usb_host_endpoint结构所定义的端点配置。这些接口结构没有特别的顺序。*/ struct usb_host_interface *cur_altsetting; /* 指向altsetting内部的指针,表示当前激活的接口配置*/ unsigned num_altsetting; /* 可选设置的数量*/ /* If there is an interface association descriptor then it will list the associated interfaces */ struct usb_interface_assoc_descriptor *intf_assoc; int minor; /* 如果绑定到这个接口的 USB 驱动使用 USB 主设备号, 这个变量包含由 USB 核心分配给接口的次设备号. 这只在一个成功的调用 usb_register_dev后才有效。*/ /*以下的数据在我们写的驱动中基本不用考虑,系统会自动设置*/ enum usb_interface_condition condition; /* state of binding */ unsigned is_active:1; /* the interface is not suspended */ unsigned sysfs_files_created:1; /* the sysfs attributes exist */ unsigned ep_devs_created:1; /* endpoint "devices" exist */ unsigned unregistering:1; /* unregistration is in progress */ unsigned needs_remote_wakeup:1; /* driver requires remote wakeup */ unsigned needs_altsetting0:1; /* switch to altsetting 0 is pending */ unsigned needs_binding:1; /* needs delayed unbind/rebind */ unsigned reset_running:1; struct device dev; /* 接口特定的设备信息 */ struct device *usb_dev; int pm_usage_cnt; /* usage counter for autosuspend */ struct work_struct reset_ws; /* for resets in atomic context */ }; struct usb_host_interface { struct usb_interface_descriptor desc; //接口描述符 struct usb_host_endpoint *endpoint; /* 这个接口的所有端点结构体的联合数组*/ char *string; /* 接口描述字符串 */ unsigned char *extra; /* 额外的描述符 */ int extralen; };
接口描述符:接口类、子类和适用的协议,接口备用配置的数目和端点数目。USB
接口在 内 核 中 使 用
usb_interface结 构 体 描 述 ,
USB
接 口 描 述 符 定 义 为 结 构 体usb_interface_descriptor
struct usb_interface_descriptor { __u8 bLength;//设备描述符的字节数大小,为0x12 __u8 bDescriptorType;//描述符类型编号,为0x01 __u8 bInterfaceNumber;//接口的编号 __u8 bAlternateSetting;//备用的接口描述符编号 __u8 bNumEndpoints;//该接口使用端点数,不包括端点0 __u8 bInterfaceClass;//接口类型 __u8 bInterfaceSubClass;//接口子类型 __u8 bInterfaceProtocol;//接口所遵循的协议 __u8 iInterface;//描述该接口的字符串索引值 } __attribute__ ((packed));
端点
USB 通讯的最基本形式是通过一个称为端点的东西。一个USB端点只能向一个方向传输数据(从主机到设备(称为输出端点)或者从设备到主机(称为输入端点))。端点可被看作一个单向的管道。
一个 USB 端点有 4 种不同类型, 分别具有不同的数据传送方式:
控制CONTROL
控制端点被用来控制对 USB 设备的不同部分访问. 通常用作配置设备、获取设备信息、发送命令到设备或获取设备状态报告。这些端点通常较小。每个 USB 设备都有一个控制端点称为"端点 0", 被 USB 核心用来在插入时配置设备。USB协议保证总有足够的带宽留给控制端点传送数据到设备.
中断INTERRUPT
每当 USB 主机向设备请求数据时,中断端点以固定的速率传送小量的数据。此为USB 键盘和鼠标的主要的数据传送方法。它还用以传送数据到 USB 设备来控制设备。通常不用来传送大量数据。USB协议保证总有足够的带宽留给中断端点传送数据到设备.
批量BULK
批量端点用以传送大量数据。这些端点常比中断端点大得多. 它们普遍用于不能有任何数据丢失的数据。USB 协议不保证传输在特定时间范围内完成。如果总线上没有足够的空间来发送整个BULK包,它被分为多个包进行传输。这些端点普遍用于打印机、USB Mass Storage和USB网络设备上。
等时ISOCHRONOUS
等时端点也批量传送大量数据, 但是这个数据不被保证能送达。这些端点用在可以处理数据丢失的设备中,并且更多依赖于保持持续的数据流。如音频和视频设备等等。
控制和批量端点用于异步数据传送,而中断和同步端点是周期性的。这意味着这些端点被设置来在固定的时间连续传送数据,USB 核心为它们保留了相应的带宽。
端点在内核中使用结构 struct usb_host_endpoint 来描述
struct usb_host_endpoint { struct usb_endpoint_descriptor desc; //端点描述符 struct list_head urb_list; //此端点的URB对列,由USB核心维护 void *hcpriv; struct ep_device *ep_dev; /* For sysfs info */ unsigned char *extra; /* Extra descriptors */ int extralen; int enabled; };struct
usb_host_endpoint 所包含的真实端点信息在另一个结构中:struct usb_endpoint_descriptor(端点描述符,包含所有的USB特定数据)。
端点描述符:端点地址、方向和类型,支持的最大包大小,如果是中断类型的端点则还包括轮询频率。
struct usb_endpoint_descriptor { __u8 bLength; __u8 bDescriptorType; __u8 bEndpointAddress; /*这个特定端点的 USB 地址,这个8位数据包含端点的方向,结合位掩码 USB_DIR_OUT 和 USB_DIR_IN 使用, 确定这个端点的数据方向。*/ __u8 bmAttributes; //这是端点的类型,位掩码如下 __le16 wMaxPacketSize; /*端点可以一次处理的最大字节数。驱动可以发送比这个值大的数据量到端点, 但是当真正传送到设备时,数据会被分为 wMaxPakcetSize 大小的块。对于高速设备, 通过使用高位部分几个额外位,可用来支持端点的高带宽模式。*/ __u8 bInterval; //如果端点是中断类型,该值是端点的间隔设置,即端点的中断请求间的间隔时间,以毫秒为单位 /* NOTE: these two are _only_ in audio endpoints. */ /* use USB_DT_ENDPOINT*_SIZE in bLength, not sizeof. */ __u8 bRefresh; __u8 bSynchAddress; } __attribute__ ((packed));
字符串描述符:在其他描述符中会为某些字段提供字符串索引,它们可被用来检索描述性字符串,可以以多种语言形式提供。字符串描述符是可选的,有的设备有,有的设备没有,字符串描述符对应于usb_string_descriptor结构体
struct usb_string_descriptor { __u8 bLength;//设备描述符的字节数大小,为0x12 __u8 bDescriptorType;//描述符类型编号,为0x01 __le16 wData[1]; /* UTF-16LE encoded */ } __attribute__ ((packed));
例如,笔者在
PC 上插入一个
SanDisk U 盘后,通过
lsusb 命令得到这个
U 盘相关的描述符,从中可以显示这个U
盘包含了一个设备描述符、一个配置描述符、一个接口描述符以及批量输入和批量输出两个端点描述符。呈现出来的信息内容直接对应于usb_device_descriptor、usb_config_
descriptor、usb_interface_descriptor、usb_endpoint_descriptor、usb_string_descriptor结构体
USB驱动程序框架:
app:
-------------------------------------------
USB设备驱动程序 // 知道数据含义
内核 --------------------------------------
USB总线驱动程序 // 1. 识别, 2. 找到匹配的设备驱动, 3. 提供USB读写函数 (它不知道数据含义)
-------------------------------------------
USB主机控制器
UHCI OHCI EHCI
硬件 -----------
USB设备
UHCI: intel, 低速(1.5Mbps)/全速(12Mbps)
OHCI: microsoft 低速/全速
EHCI: 高速(480Mbps)
USB总线驱动程序的作用
1. 识别USB设备
1.1 分配地址
1.2 并告诉USB设备(set address)
1.3 发出命令获取描述符
描述符的信息可以在include\linux\usb\Ch9.h看到
2. 查找并安装对应的设备驱动程序
3. 提供USB读写函数
usb设备驱动 ---鼠标
/* * drivers\hid\usbhid\usbmouse.c */ #include <linux/kernel.h> #include <linux/slab.h> #include <linux/module.h> #include <linux/init.h> #include <linux/usb/input.h> #include <linux/hid.h> static struct input_dev *uk_dev; static char *usb_buf; static dma_addr_t usb_buf_phys; static int len; static struct urb *uk_urb; static struct usb_device_id usb_mouse_id_table [] = { { USB_INTERFACE_INFO(USB_INTERFACE_CLASS_HID, USB_INTERFACE_SUBCLASS_BOOT, USB_INTERFACE_PROTOCOL_MOUSE) }, { } /* Terminating entry */ }; MODULE_DEVICE_TABLE (usb, usb_mouse_id_table); static void usbmouse_as_key_irq(struct urb *urb) { int i; static int cnt = 0; printk("data cnt %d: ", ++cnt); for (i = 0; i < len; i++) { //printk("%02x ", usb_buf[i]); if(usb_buf[i] == 01) printk("left\n"); else printk("no left\n"); } printk("\n"); //data cnt 30: 01 ff 02 00 /* 重新提交urb */ usb_submit_urb(uk_urb, GFP_KERNEL); } static int usbmouse_as_key_probe(struct usb_interface *intf, const struct usb_device_id *id) { struct usb_device *dev = interface_to_usbdev(intf); //根据usb_interface指针intf获取usb_device的地址 struct usb_host_interface *interface; //这里可能要补充以下一些关于usb_interface_descriptor的知识,但因为内核源码对该结构体的注释不多,所以只能靠个人猜测。在一个usb_host_interface结构里面有一个usb_interface_descriptor叫做desc的成员,他应该是用于描述该interface的一些属性,其中bNumEndpoints是一个8位(b for byte)的数字,他代表了该接口的端点数。probe然后遍历所有的端点,检查他们的类型跟方向,注册到usb_skel中。 struct usb_endpoint_descriptor *endpoint; int pipe; interface = intf->cur_altsetting; //当前活跃的设置 endpoint = &interface->endpoint[0].desc; //本接口对应的端点 /* a. 分配一个input_dev */ uk_dev = input_allocate_device(); /* b. 设置 */ /* b.1 能产生哪类事件 */ set_bit(EV_KEY, uk_dev->evbit); set_bit(EV_REP, uk_dev->evbit); /* b.2 能产生哪些事件 */ set_bit(KEY_L, uk_dev->keybit); set_bit(KEY_S, uk_dev->keybit); set_bit(KEY_ENTER, uk_dev->keybit); /* c. 注册 */ input_register_device(uk_dev); /* d. 硬件相关操作 */ /* 数据传输3要素: 源,目的,长度 */ /* 源: USB设备的某个端点 */ //设置端点信息,其实pipe是一个int类型的数据。urb所发送的特定目标struct usb_device的端点信息 pipe = usb_rcvintpipe(dev, endpoint->bEndpointAddress); //bEndpointAddress 端点地址 /* 信息包长度: */ len = endpoint->wMaxPacketSize; /* 目的: */ usb_buf = usb_alloc_coherent(dev, len, GFP_ATOMIC, &usb_buf_phys); /* 使用"3要素" */ /* 分配usb request block(urb) */ uk_urb = usb_alloc_urb(0, GFP_KERNEL); /* 使用"3要素设置urb" */ //对于中断urb,使用usb_fill_int_urb初始化urb。 usb_fill_int_urb(uk_urb, dev, pipe, usb_buf, len, usbmouse_as_key_irq, NULL, endpoint->bInterval); uk_urb->transfer_dma = usb_buf_phys; uk_urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP; /* 使用URB */ usb_submit_urb(uk_urb, GFP_KERNEL); return 0; } static void usbmouse_as_key_disconnect(struct usb_interface *intf) { struct usb_device *dev = interface_to_usbdev(intf); //printk("disconnect usbmouse!\n"); usb_kill_urb(uk_urb); usb_free_urb(uk_urb); usb_free_coherent(dev, len, usb_buf, usb_buf_phys); input_unregister_device(uk_dev); input_free_device(uk_dev); } /* 1. 分配/设置usb_driver */ static struct usb_driver usbmouse_as_key_driver = { .name = "s5p210usbmouse", .probe = usbmouse_as_key_probe, .disconnect = usbmouse_as_key_disconnect, .id_table = usb_mouse_id_table, }; static int usbmouse_as_key_init(void) { /* 2. 注册 */ usb_register(&usbmouse_as_key_driver); return 0; } static void usbmouse_as_key_exit(void) { usb_deregister(&usbmouse_as_key_driver); } module_init(usbmouse_as_key_init); module_exit(usbmouse_as_key_exit); MODULE_LICENSE("GPL");
使用
usb_driver结构体描述一个
USB
设备驱动,usb_driver结构体的定义如下:
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 (*ioctl) (struct usb_interface*intf,
unsigned int code,
void *buf); /*I/O
控制函数*/
int (*suspend) (struct usb_interface*intf,
pm_message_t message);/*挂起函数*/
int (*resume) (struct usb_interface*intf);
/*恢复函数
*/
int (*reset_resume)(struct usb_interface*intf);
void (*pre_reset) (struct usb_interface*intf);
void (*post_reset) (struct usb_interface*intf);
const struct usb_device_id*id_table;/*usb_device_id表指针
*/
struct usb_dynids dynids;
struct usbdrv_wrap drvwrap;
unsigned
int no_dynamic_id:1;
unsigned int supports_autosuspend:1;
unsigned int soft_unbind:1;
};
/* 1. 分配/设置usb_driver */ static struct usb_driver usbmouse_as_key_driver = { .name = "s5p210usbmouse", .probe = usbmouse_as_key_probe, .disconnect = usbmouse_as_key_disconnect, .id_table = usb_mouse_id_table, };
<span style="font-family: SimSun; font-size: 10pt;">对<span style="font-family: TimesNewRomanPSMT; font-size: 10pt;">usb<span style="font-family: SimSun; font-size: 10pt;">_<span style="font-family: TimesNewRomanPSMT; font-size: 10pt;">driver <span style="font-family: SimSun; font-size: 10pt;">的注册和注销</span><br style="orphans: 2; text-align: -webkit-auto; widows: 2;" /></span></span></span></span> static int usbmouse_as_key_init(void) { /* 2. 注册 */ usb_register(&usbmouse_as_key_driver); return 0; } static void usbmouse_as_key_exit(void) { usb_deregister(&usbmouse_as_key_driver); }
usb_driver结构体中的
id_table成员描述了这个
USB
驱动所支持的 USB
设备列表,它指向一个usb_device_id数组,
usb_device_id结构体用于包含
USB
设备的制造商 ID、产品
ID、产品版本、设备类、接口类等信息及其要匹配标志成员match_flags(标明要与哪些成员匹配,包含DEV_LO、DEV_HI、DEV_CLASS、DEV_SUBCLASS、DEV_PROTOCOL、INT_CLASS、INT_SUBCLASS、INT_PROTOCOL)。可以借助下面一组宏来生成usb_device_id结构体的实例:
static struct usb_device_id usb_mouse_id_table [] = { { USB_INTERFACE_INFO(USB_INTERFACE_CLASS_HID, USB_INTERFACE_SUBCLASS_BOOT, USB_INTERFACE_PROTOCOL_MOUSE) }, { } /* Terminating entry */ }; MODULE_DEVICE_TABLE (usb, usb_mouse_id_table);
USB_INTERFACE_INFO(class,
subclass, protocol)
该宏用于创建一个匹配接口指定类型的
usb_device_id结构体实例。
USB_DEVICE_INFO(class, subclass, protocol)
该宏用于创建一个匹配设备指定类型的
usb_device_id结构体实例。
USB_DEVICE(vendor, product)
该宏根据制造商
ID 和产品
ID 生成一个
usb_device_id结构体的实例,在数组中增加该元素将意味着该驱动可支持匹配制造商ID、产品ID
的设备。
USB_DEVICE_VER(vendor,
product, lo, hi)
该宏根据制造商
ID、产品
ID、产品版本的最小值和最大值生成一个
usb_device_id结构体的实例,在数组中增加该元素将意味着该驱动可支持匹配制造商ID、产品ID
和
lo~hi范围内版本的设备。
当USB核心检测到某个设备的属性和某个驱动程序的usb_device_id结构体所携带的信息一致时,这个驱动程序的probe()函数就被执行。拔掉设备或者卸掉驱动模块后,USB核心就执行disconnect()函数来响应这个动作。
上述usb_driver结构体中的函数是
USB
设备驱动中 USB
相关的部分,而 USB
只是一个总线,真正的USB
设备驱动的主体工作仍然是
USB 设备本身所属类型的驱动,如字符设备、
tty设备、块设备、输入设备等。因此USB
设备驱动包含其作为总线上挂在设备的驱动和本身所属设备类型的驱动两部分。
与platform_driver类似,
usb_driver起到了“牵线”的作用,即在
probe()里注册相应的字符、tty等设备,在
disconnect()注销相应的字符、
tty
等设备,而原先对设备的注册和注销一般直接发生在模块加载和卸载函数中.
尽管USB
本身所属设备驱动的结构与其不挂在
USB 总线上时完全相同,但是在访问方式上却发生了很大的变化,例如,对于
USB
接口的字符设备而言,尽管仍然是 write()、
read()、
ioctl()这些函数,但是在这些函数中,贯穿始终的是称为
URB
的 USB
请求块。
我们把树根比作主机控制器,树叶比作具体的USB
设备,树干和树枝就是
USB
总线。树叶本身与树枝通过 usb_driver连接,而树叶本身的驱动(读写、控制)则需要通过其树叶设备本身所属类设备驱动来完成。树根和树叶之间的“通信”依靠在树干和树枝里“流淌”的URB
来完成。
urb结构体
USB
请求块( USB request block,urb)是USB
设备驱动中用来描述与
USB 设备通信所用的基本载体和核心数据结构,非常类似于网络设备驱动中的
sk_buff结构体。
struct urb
2 {
3 /* 私有的:只能由usb核心和主机控制器访问的字段 */
4 struct kref kref; /*urb引用计数 */
5 spinlock_t lock; /* urb锁 */
6 void *hcpriv; /* 主机控制器私有数据 */
7 int bandwidth; /* int/iso请求的带宽 */
8 atomic_t use_count; /* 并发传输计数 */
9 u8 reject; /* 传输将失败*/
10
11 /* 公共的: 可以被驱动使用的字段 */
12 struct list_head urb_list; /* 链表头*/
13 struct usb_device *dev; /* 关联的usb设备 */
14 unsigned int pipe; /* 管道信息 */
15 int status; /* urb的当前状态 */
16 unsigned int transfer_flags; /* urb_short_not_ok | ...*/
17 void *transfer_buffer; /* 发送数据到设备或从设备接收数据的缓冲区 */
18 dma_addr_t transfer_dma; /*用来以dma方式向设备传输数据的缓冲区 */
19 int transfer_buffer_length;/*transfer_buffer或transfer_dma 指向缓冲区的大小 */
20
21 int actual_length; /* urb结束后,发送或接收数据的实际长度 */
22 unsigned char *setup_packet; /* 指向控制urb的设置数据包的指针*/
23 dma_addr_t setup_dma; /*控制urb的设置数据包的dma缓冲区*/
24 int start_frame; /*等时传输中用于设置或返回初始帧*/
25 int number_of_packets; /*等时传输中等时缓冲区数据 */
26 int interval; /* urb被轮询到的时间间隔(对中断和等时urb有效) */
27 int error_count; /* 等时传输错误数量 */
28 void *context; /* completion函数上下文 */
29 usb_complete_t complete; /* 当urb被完全传输或发生错误时,被调用 */
30 struct usb_iso_packet_descriptor iso_frame_desc[0];
31 /*单个urb一次可定义多个等时传输时,描述各个等时传输 */
32 };
urb
处理流程
USB
设备中的每个端点都处理一个 urb
队列,在队列被清空之前,一个 urb
的典型生命周期如下:
(1)被一个USB
设备驱动创建。
创建
urb 结构体的函数为:
struct urb
*usb_alloc_urb(int
iso_packets, int mem_flags);
iso_packets是这个
urb
应当包含的等时数据包的数目,若为 0
表示不创建等时数据包。
mem_flags参数是分配内存的标志,和
kmalloc()函数的分配标志参数含义相同。如果分配成功,该函数返回一个urb
结构体指针,否则返回
0。
urb
结构体在驱动中不能静态创建,因为这可能破坏
USB 核心给
urb 使用的引用计数方法。
(2)初始化,被安排给一个特定USB
设备的特定端点。
/* 使用"3要素" */ /* 分配usb request block(urb) */ uk_urb = usb_alloc_urb(0, GFP_KERNEL); /* 使用"3要素设置urb" */ //对于中断urb,使用usb_fill_int_urb初始化urb。 usb_fill_int_urb(uk_urb, dev, pipe, usb_buf, len, usbmouse_as_key_irq, NULL, endpoint->bInterval); uk_urb->transfer_dma = usb_buf_phys; uk_urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
/* 使用URB */ usb_submit_urb(uk_urb, GFP_KERNEL);
对于中断
urb,使用
usb_fill_int_urb()函数来初始化urb:
urb参数指向要被初始化的
urb
的指针; dev
指向这个 urb
要被发送到的 USB
设备; pipe
是这个urb
要被发送到的
USB 设备的特定端点;
transfer_buffer是指向发送数据或接收数据的缓冲区的指针,和urb
一样,它也不能是静态缓冲区,必须使用
kmalloc()来分配;buffer_length是
transfer_buffer指针所指向缓冲区的大小;
complete
指针指向当这个 urb
完成时被调用的完成处理函数;context
是完成处理函数的“上下文”;
interval是这个
urb 应当被调度的间隔。
( 3)被 USB 设备驱动提交给 USB 核心。
在完成创建和初始化urb
后,
urb 便可以提交给
USB 核心,通过
usb_submit_urb()函数来完成,如下所示:
int usb_submit_urb(struct
urb *urb, int mem_flags);
urb
参数是指向 urb
的指针, mem_flags参数与传递给
kmalloc()函数参数的意义相同,它用于告知USB
核心如何在此时分配内存缓冲区。
在提交
urb 到
USB 核心后,直到完成函数被调用之前,不要访问
urb 中的任何成员。
usb_submit_urb()在原子上下文和进程上下文中都可以被调用,mem_flags变量需根据调用环境进行相应的设置,如下所示。
GFP_ATOMIC:在中断处理函数、底半部、tasklet、定时器处理函数以及urb
完成函数中,在调用者持有自旋锁或者读写锁时以及当驱动将
current→state修改为非
TASK_RUNNING时,应使用此标志。
GFP_NOIO:在存储设备的块I/O
和错误处理路径中,应使用此标志;
GFP_KERNEL:如果没有任何理由使用GFP_ATOMIC和
GFP_NOIO,就使用GFP_KERNEL。
如果
usb_submit_urb()调用成功,即urb
的控制权被移交给
USB 核心,该函数返回
0;否则,返回错误号。
(4)提交由USB
核心指定的
USB主机控制器驱动。
( 5)被USB
主机控制器处理,进行一次到
USB设备的传送。
第( 4) ~(5)步由USB
核心和主机控制器完成,不受
USB设备驱动的控制。
( 6)当urb
完成, USB主机控制器驱动通知
USB
设备驱动。
在如下3
种情况下,
urb 将结束,
urb 完成函数将被调用。
urb被成功发送给设备,并且设备返回正确的确认。如果
urb→status为
0,意味着对于一个输出urb,数据被成功发送;对于一个输入urb,请求的数据被成功收到。
如果发送数据到设备或从设备接收数据时发生了错误,urb→status将记录错误值。
urb被从
USB
核心“去除连接”,这发生在驱动通过 usb_unlink_urb()或usb_kill_urb()函数取消urb,或urb
虽已提交,而
USB 设备被拔出的情况下
相关文章推荐
- 对List中的某个属性进行排序
- HashMap、HashSet和Hashtable的区别
- Spark SQL下的Parquet使用最佳实践和代码实战
- WCF再学习小结
- iOS开发之诱导用户为自己的App评论功能
- 杭电 2054(指针 strchr函数)
- C# 自定义控件,自定义属性,自定义事件
- RabbitMQ(二)队列与消息的持久化
- Lucky Numbers (easy)(暴力解决)
- Android Studio使用Lambda表达式出错
- unity语音聊天--亲加通讯云(Android/iOS)---ios
- 0048 linux(ubuntu)中Spark的本地模式安装
- 继承中构造函数与析构函数
- 多线程-生产者消费者模式(2)
- 从今天开始写日记
- android调用webservice解析Gson数据
- 内部类总结
- 每日任务
- C代码中自定义static assert实现机制
- [存储]Android使用SQLiteOpenHelper简单存储