Linux那些事儿 之 戏说USB(15)设备

struct usb_device结构冗长而又杂乱

include/linux/usb.h

struct usb_device {
	int		devnum;
	char		devpath[16];
	u32		route;
	enum usb_device_state	state;
	enum usb_device_speed	speed;

	struct usb_tt	*tt;
	int		ttport;

	unsigned int toggle[2];

	struct usb_device *parent;
	struct usb_bus *bus;
	struct usb_host_endpoint ep0;

	struct device dev;

	struct usb_device_descriptor descriptor;
	struct usb_host_bos *bos;
	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;

	unsigned short bus_mA;
	u8 portnum;
	u8 level;

	unsigned can_submit:1;
	unsigned persist_enabled:1;
	unsigned have_langid:1;
	unsigned authorized:1;
	unsigned authenticated:1;
	unsigned wusb:1;
	unsigned lpm_capable:1;
	unsigned usb2_hw_lpm_capable:1;
	unsigned usb2_hw_lpm_besl_capable:1;
	unsigned usb2_hw_lpm_enabled:1;
	unsigned usb2_hw_lpm_allowed:1;
	unsigned usb3_lpm_enabled:1;
	int string_langid;

	/* static strings from the device */
	char *product;
	char *manufacturer;
	char *serial;

	struct list_head filelist;

	int maxchild;

	u32 quirks;
	atomic_t urbnum;

	unsigned long active_duration;

#ifdef CONFIG_PM
	unsigned long connect_time;

	unsigned do_remote_wakeup:1;
	unsigned reset_resume:1;
	unsigned port_is_suspended:1;
#endif
	struct wusb_dev *wusb_dev;
	int slot_id;
	enum usb_device_removable removable;
	struct usb2_lpm_parameters l1_params;
	struct usb3_lpm_parameters u1_params;
	struct usb3_lpm_parameters u2_params;
	unsigned lpm_disable_count;
};
devnum,设备的地址。此地址非彼地址,和咱们写程序时说的地址是不一样的,devnum只是usb设备在一条usb总线上的编号。你的usb设备插到hub上时,hub观察到这个变化,于是来了精神,会在一个漫长而又曲折的处理过程中调用一个名叫choose_address的函数,为你的设备选择一个地址。就像在那个浪漫的季节的一个温馨的下午,你去吃港汇下边儿的那个必胜客,同样会领取一个属于自己的编号陪伴自己度过一个漫长的过程。有人说我没有用hub,我的usb设备直接插到主机的usb接口上了。我哭,即使你没有用hub,也总要明白主机里还会有个叫root hub的东东吧,不管是一般的hub还是root hub,你的usb设备总要通过一个hub才能在usb的世界里生活。

现在来认识一下usb子系统里面关于地址的游戏规则。在usb世界里,一条总线就是大树一棵,一个设备就是叶子一片。为了记录这棵树上的每一个叶子节点,每条总线设有一个地址映射表,即struct usb_bus结构体里有一个成员struct usb_devmap devmap,

include/linux/usb.h

/* USB device number allocation bitmap */
struct usb_devmap {
	unsigned long devicemap[128 / (8*sizeof(unsigned long))];
};
什么是usb_bus?前面不是已经有了一个struct bus_type类型的usb_bus_type了么?没错,在usb子系统的初始化函数usb_init里已经注册了usb_bus_type,不过那是让系统知道有这么一个类型的总线。而一个总线类型和一条总线是两码子事儿。从硬件上来讲,一个host controller就会连出一条usb总线,而从软件上来讲,不管你有多少个host controller,或者说有多少条总线,它们通通属于usb_bus_type这么一个类型,只是每一条总线对应一个struct usb_bus结构体变量,这个变量在host controller的驱动程序中去申请。

上面的devmap地址映射表就表示了一条总线上设备连接的情况,假设unsigned long=4bytes,那么unsigned long devicemap[128/(8*sizeof(unsigned long)]]就等价于unsigned long devicemap[128/(8*4)],进而等价于unsigned long devicemap[4],而4bytes就是32个bits,因此这个数组最终表示的就是128个bits。而这也对应于一条总线可以连接128个usb设备。之所以这里使用sizeof(unsigned long),就是为了跨平台应用,不管unsigned long到底是几,总之这个devicemap数组最终可以表示128位,也就是说每条总线上最多可以连上128个设备。

3行,devpath [16],它显然是用来记录一个字符串的,这个字符串啥意思?给你看个直观的东西
localhost:~ # ls /sys/bus/usb/devices/
1-0:1.0 2-1 2-1:1.1 4-0:1.0 4-5:1.0 usb2 usb4
2-0:1.0 2-1:1.0 3-0:1.0 4-5 usb1 usb3

Sysfs文件系统下,我们看到这些乱七八糟的东西,它们都是啥?usb1/usb2/usb3/usb4表示哥们的计算机上接了4条usb总线,即4个usb主机控制器,事物多了自然就要编号,就跟我们中学或大学里面的学号一样,就是用于区分多个个体,而4-0:1.0表示什么?4表示是4号总线,或者说4号Root Hub,0就是这里我们说的devpath,1表示配置为1号,0表示接口号为0。也即是说,4号总线的0号端口的设备,使用的是1号配置,接口号为0。那么devpath是否就是端口号呢?显然不是,这里我列出来的这个例子是只有Root Hub没有级联Hub的情况,如果在Root Hub上又接了别的Hub,然后一级一级连下去,子又生孙,孙又生子,子又有子,子又有孙。子子孙孙,无穷匮也。那么如何在sysfs里面来表征这整个大家族呢?这就是devpath的作用,顶级的设备其devpath就是其连在Root Hub上的端口号,而次级的设备就是其父Hub的devpath后面加上其端口号,即如果4-0:1.0如果是一个Hub,那么它下面的1号端口的设备就可以是4-0.1:1.0,2号端口的设备就可以是4-0.2:1.0,3号端口就可以是4-0.3:1.0。总的来说,就是端口号一级一级往下加。这个思想是很简单的,也是很朴实的。

5行,state,设备的状态。这是个枚举类型

include/uapi/linux/usb/ch9.h

enum usb_device_state {
	/* NOTATTACHED isn't in the USB spec, and this state acts
	 * the same as ATTACHED ... but it's clearer this way.
	 */
	USB_STATE_NOTATTACHED = 0,

	/* chapter 9 and authentication (wireless) device states */
	USB_STATE_ATTACHED,
	USB_STATE_POWERED,			/* wired */
	USB_STATE_RECONNECTING,			/* auth */
	USB_STATE_UNAUTHENTICATED,		/* auth */
	USB_STATE_DEFAULT,			/* limited function */
	USB_STATE_ADDRESS,
	USB_STATE_CONFIGURED,			/* most functions */

	USB_STATE_SUSPENDED

	/* NOTE:  there are actually four different SUSPENDED
	 * states, returning to POWERED, DEFAULT, ADDRESS, or
	 * CONFIGURED respectively when SOF tokens flow again.
	 * At this level there's no difference between L1 and L2
	 * suspend states.  (L2 being original USB 1.1 suspend.)
	 */
};
上面定义了9种状态,spec里只定义了6种,Attached,Powered,Default,Address,Configured,Suspended,对应于Table 9.1。
Attached表示设备已经连接到usb接口上了,是hub检测到设备时的初始状态。那么这里所谓的USB_STATE_NOTATTACHED就是表示设备并没有Attached。
Powered是加电状态。USB设备的电源可以来自外部电源,协议里叫做self-powered,也可以来自hub,叫bus-powered。尽管self-powered的USB设备可能在连接上USB接口以前已经上电,但它们直到连上USB接口后才能被看作是Powered的,你觉得它已经上电了那是站在你的角度看,可是现在你看的是usbcore,所以要放弃个人的成见,团结在core的周围。
Default缺省状态,在Powered之后,设备必须在收到一个复位(reset)信号并成功复位后,才能使用缺省地址回应主机发过来的设备和配置描述符的请求。
Address状态表示主机分配了一个唯一的地址给设备,此时设备可以使用缺省管道响应主机的请求。真羡慕这些usb设备,住的地方都是包分配的,哪像咱们辛辛苦苦一路小跑着也不一定能达到Address状态。
Configured状态表示设备已经被主机配置过了,也就是协议里说的处理了一个带有非0值的SetConfiguration()请求,此时主机可以使用设备提供的所有功能。
Suspended挂起状态,为了省电,设备在指定的时间内,3ms吧,如果没有发生总线传输,就要进入挂起状态。此时,usb设备要自己维护包括地址、配置在内的信息。
USB设备从生到死都要按照这么几个状态,遵循这么一个过程。

6行,speed,设备的速度,这也是个枚举变量

include/uapi/linux/usb/ch9.h

/* USB 2.0 defines three speeds, here's how Linux identifies them */

enum usb_device_speed {
	USB_SPEED_UNKNOWN = 0,			/* enumerating */
	USB_SPEED_LOW, USB_SPEED_FULL,		/* usb 1.1 */
	USB_SPEED_HIGH,				/* usb 2.0 */
	USB_SPEED_WIRELESS,			/* wireless (usb 2.5) */
	USB_SPEED_SUPER,			/* usb 3.0 */
};
地球人都知道,USB设备有三种速度,低速,全速,高速。USB1.1那会儿只有低速,全速,后来才出现了高速,就是所谓的480Mbps/s。这里还有个USB_SPEED_WIRELESS,是无线USB的,号称usb 2.5,还在发展中。USB_SPEED_UNKNOWN只是表示现阶段还不知道这个设备究竟什么速度。USB_SPEED_SUPER表示USB 3.0。
tt,ttport。知道tt干嘛的吗?tt 叫做transaction translator。你可以把它想成一块特殊的电路,是hub里面的电路,确切的说是高速hub中的电路,我们知道usb设备有三种速度的,分别是low speed,full speed,high speed。即所谓的低速/全速/高速,抗日战争那会儿,这个世界上只有low speed/full speed的设备,没有high speed的设备,后来解放后,国民生产力的大幅度提升催生了一种 high speed 的设备,包括主机控制器,以前只有两种接口的,OHCI/UHCI,这都是在usb spec 1.0 的时候,后来2.0推出了EHCI,高速设备应运而生。Hub也有高速hub和过去的hub,但是这里就有一个兼容性问题了,高速的hub是否能够支持低速/全速的设备呢?一般来说是不支持的,于是有了一个叫做TT 的电路,它就负责高速和低速/全速的数据转换,于是,如果一个高速设备里有这么一个TT,那么就可以连接低速/全速设备,如不然,那低速/全速设备没法用,只能连接到OHCI/UHCI那边出来的hub口里
toggle[2],这个数组只有两个元素,分别对应IN和OUT端点,每一个端点占一位。似乎这么说仍是在雾中看花,黑格尔告诉我们,存在就是有价值的,那么这个数组存在的价值是什么?一言难尽,说来话长,那就长话长说好了。
咱们前边儿说,你要想和你的usb通信,创建一个urb,为它赋好值,交给咱们的usb core就可以了。这个urb是站在咱们的角度,实际上在usb cable里流淌的根本就不是那么回事儿,咱们提交的是urb,usb cable里流淌的是一个一个的数据包(packet),就像咱们吃的是社会主义的粮,身体里流淌的是无产阶级的鲜血。咱们无产阶级的鲜血里,虽说不包括房产财产什么的,但是还是有许多的成分一定的结构的,usb底层传输的packets也一样。

咱们凄苦的人生是从第一声哭开始,所有的packets都从一个SYNC同步字段开始,SYNC是一个8位长的二进制串,只是用来同步用的,它的最后两位标志了SYNC的结束和PID(Packet Identifer)的开始,就像咱们的大四标志了梦想的结束和现实的开始。PID也是一个8位的二进制串,前四位用来区分不同的packet类型,后面四位只是前四位的反码,校验用的。packet的类型主要有四种,在spec的Table 8-1里有说明


主机和设备都是纯理性的东东,完全通过PID来判断送过来的packet是不是自己所需要的。PID之后紧跟着的是地址字段,每个packet都需要知道自己要往哪里去,它们是一个一个目的明确的精灵,行走在usb cable里。这个地址实际上包括两部分,7位表示了总线上连接的设备或接口的地址,4位表示端点的地址,这就是为什么前面说每条usb总线最多只能有128个设备,即使是高速设备每个接口除了0号端点也最多只能有15个in端点和15个out端点。地址字段再往后是11位的帧号(frame number),值达到7FFH时归零,像一个个无聊的夜晚一样循环往复。这个帧号并不是每一个packet都会有,它只在每帧或微帧(Mircoframe)开始的SOF Token包里发送。帧是对于低速和全速模式来说的,一帧就是1ms,对于高速模式的称呼是微帧,一个微帧为125微妙,每帧或微帧当然不会只能传一个packet。帧号再往后就是千呼万唤始出来的Data字段了,它可以有0到1024个字节不等。最后还有CRC校验字段来做扫尾工作。

咱们要学习packet,做一个有理想有目标的人,所以这里只看看Data类型的packet。前面的Table 8-1里显示,有四种类型的Data包,DATA0,DATA1,DATA2和MDATA。存在就是有价值的,这里分成4种数据包自然有里面的道理,其中DATA0和DATA1就可以用来实现data toggle同步,看到toggle,好像有点接近不久之前留下的疑问了。

对于批量传输、控制传输和中断传输来说,数据包最开始都是被初始化为DATA0的,然后为了传输的正确性,就一次传DATA0,一次传DATA1,一旦哪次打破了这种平衡,主机就可以认为传输出错了。对于等时传输来说,data toggle并不被支持。USB就是在使用这种简单的哲学来判断对于错。


我们的struct usb_device中的数组unsigned int toggle[2]就是为了支持这种简单的哲学而生的,它里面的每一位表示的就是每个端点当前发送或接收的数据包是DATA0还是DATA1。
parent,struct usb_device结构体的parent自然也是一个struct usb_device指针。对于Root Hub,前面说过,它是和Host Controller是绑定在一起的,它的parent指针在Host Controller的驱动程序中就已经赋了值,这个值就是NULL,换句话说,对于Root Hub,它不需要再有父指针了,这个父指针就是给从Root Hub连出来的节点用的。USB设备是从Root Hub开始,一个一个往外面连的,比如Root Hub有4个口,每个口连一个USB设备,比如其中有一个是Hub,那么这个Hub有可以继续有多个口,于是一级一级的往下连,最终连成了一棵树。
bus,没什么说的,设备所在的那条总线。
ep0,端点0的特殊地位决定了她必将受到特殊的待遇,在struct usb_device对象产生的时候它就要初始化。
dev,嵌入到struct usb_device结构里的struct device结构。
desc,设备描述符,四大描述符的第三个姗姗而来。它在include/uapi/linux/usb/ch9.h里定义
/* USB_DT_DEVICE: Device descriptor */
struct usb_device_descriptor {
	__u8  bLength;
	__u8  bDescriptorType;

	__le16 bcdUSB;
	__u8  bDeviceClass;
	__u8  bDeviceSubClass;
	__u8  bDeviceProtocol;
	__u8  bMaxPacketSize0;
	__le16 idVendor;
	__le16 idProduct;
	__le16 bcdDevice;
	__u8  iManufacturer;
	__u8  iProduct;
	__u8  iSerialNumber;
	__u8  bNumConfigurations;
} __attribute__ ((packed));

#define USB_DT_DEVICE_SIZE		18
bLength,描述符的长度,可以自己数数,或者看紧接着的定义USB_DT_DEVICE_SIZE。
bDescriptorType,这里对于设备描述符应该是USB_DT_DEVICE,0x01。
bcdUSB,USB spec的版本号,一个设备如果能够进行高速传输,那么它设备描述符里的bcdUSB这一项就应该为0200H。
bDeviceClass,bDeviceSubClass,bDeviceProtocol,和接口描述符的意义差不多,前面说了这里就不再罗唆了。
bMaxPacketSize0,端点0一次可以处理的最大字节数,端点0的属性却放到设备描述符里去了,更加彰显了它突出的江湖地位,它和机器人公敌里的机器人Sonny,交大南门外的老赵烤肉一样特别一样独一无二。
前面说端点的时候说了端点0并没有一个专门的端点描述符,因为不需要,基本上它所有的特性都在spec里规定好了的,然而,别忘了这里说的是“基本上”,有一个特性则是不一样的,这叫做maximum packet size,每个端点都有这么一个特性,即告诉你该端点能够发送或者接收的包的最大值。对于通常的端点来说,这个值被保存在该端点描述符中的wMaxPacketSize这一个field,而对于端点0就不一样了,由于它自己没有一个描述符,而每个设备又都有这么一个端点,所以这个信息被保存在了设备描述符里,所以我们在设备描述符里可以看到这么一项, bMaxPacketSize0。而且spec还规定了,这个值只能是8,16,32或者64这四者之一,如果一个设备工作在高速模式,这个值还只能是64,如果是工作在低速模式,则只能是8,取别的值都不行。
idVendor,idProduct,分别是厂商和产品的ID。
bcdDevice,设备的版本号。
iManufacturer,iProduct,iSerialNumber,分别是厂商,产品和序列号对应的字符串描述符的索引值。
bNumConfigurations,设备当前速度模式下支持的配置数量。有的设备可以在多个速度模式下操作,这里包括的只是当前速度模式下的配置数目,不是总的配置数目。
这就是设备描述符,它和spec Table 9-8是一一对应的。咱们回到struct usb_device,config,actconfig,分别表示设备拥有的所有配置和当前激活的,也就是正在使用的配置。usb设备的配置用struct usb_host_config 结构来表示,下节再说。
ep_in[16],ep_out[16],除了端点0,一个设备即使在高速模式下也最多只能再有15个IN端点和15个OUT端点,端点0太特殊了,对应的管道是Message管道,又能进又能出特能屈能伸的那种,所以这里的ep_in和ep_out数组都有16个值。
rawdescriptors,这是个字符指针数组,数组里的每一项都指向一个使用GET_DESCRIPTOR请求去获得配置描述符时所得到的结果。考虑下,为什么我只说得到的结果,而不直接说得到的配置描述符?不是请求的就是配置描述符么?这是因为你使用GET_DESCRIPTOR去请求配置描述符时,设备返回给你的不仅仅只有配置描述符,它把该配置所包括的所有接口的接口描述符,还有接口里端点的端点描述符一股脑的都塞给你了。第一个接口的接口描述符紧跟着这个配置描述符,然后是这个接口下面端点的端点描述符,如果有还有其它接口,它们的接口描述符和端点描述符也跟在后面,这里面,专门为一类设备定义的描述符和厂商定义的描述符跟在它们对应的标准描述符后面。
这里提到了GET_DESCRIPTOR请求,就顺便简单提一下USB的设备请求(device request)。协议里说了,所有的设备通过缺省的控制管道来响应主机的请求,既然使用的是控制管道,那当然就是控制传输了,这些请求的底层packet属于Setup类型,前面的那张表里也可以看到它,在Setup包里包括了请求的各种参数。协议里同时也定义了一些标准的设备请求,并规定所有的设备必须响应它们,即使它们还处于Default或Address状态。这些标准的设备请求里,GET_DESCRIPTOR就赫然在列。
bus_mA,这个值是在host controller的驱动程序中设置的,通常来讲,计算机的usb端口可以提供500mA的电流。
portnum,不管是root hub还是一般的hub,你的USB设备总归要插在一个hub的端口上才能用,portnum就是那个端口号。当然,对于root hub这个usb设备来说它本身没有portnum这么一个概念,因为它不插在别的Hub的任何一个口上。所以对于Root Hub来说,它的portnum在Host Controller的驱动程序里给设置成了0。
level,层次,也可以说是级别,表征usb设备树的级连关系。Root Hub的level当然就是0,其下面一层就是level 1,再下面一层就是level 2,依此类推。
discon_suspended,Disconnected while suspended。
have_langid,string_langid,usb设备里的字符串描述符使用的是UNICODE编码,可以支持多种语言,string_langid就是用来指定使用哪种语言的,have_langid用来判断string_langid是否有效。
product,manufacturer,serial,分别用来保存产品、厂商和序列号对应的字符串描述符信息。
usbfs相关的,不可知的未来说usbfs的时候再聊它们。
maxchild,hub的端口数,注意可不包括上行端口。
children[USB_MAXCHILDREN],USB_MAXCHILDREN是include/uapi/linux/usb/ch11.h里定义的一个宏,值为31
/* This is arbitrary.
 * From USB 2.0 spec Table 11-13, offset 7, a hub can
 * have up to 255 ports. The most yet reported is 10.
 *
 * Current Wireless USB host hardware (Intel i1480 for example) allows
 * up to 22 devices to connect. Upcoming hardware might raise that
 * limit. Because the arrays need to add a bit for hub status data, we
 * use 31, so plus one evens out to four bytes.
 */
#define USB_MAXCHILDREN		31
其实hub可以接一共255个端口,不过实际上遇到的usb hub最多的也就是说自己支持10个端口的,所以31基本上够用了。
pm_usage_cnt,struct usb_interface结构里也有,想知道吗?想知道回那儿看吧。
quirks,祭起我们法宝金山词霸看看,怪僻的意思,白了说就是大家的常用语“毛病”。本来指定usb spec就是让大家团结一致好办事,但总是有些厂商不太守规矩,拿出一些有点毛病的产品给我们用,你说它大毛病吧,也不是,就像俺这儿的厦X彩电一样,绝对能看,只是动不动就罢次工。不说远了,总之这里的quirk就是用来判断这些有毛病的产品啥毛病的。谁去判断?你的设备接哪儿哪儿负责,也就是说hub去判断,就不用咱费心了。
看到#ifdef CONFIG_PM这个标志,我们就知道从这里直到最后的那个#endif都是关于电源管理的。让我们先大胆的忽略它们,struct usb_device这个结构已经够让我们疲惫了,还是换换口味吧。
1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值