USB客户驱动简析

内核为我们提供了一个最基础的USB驱动,即USB骨架程序,是一个最简单的USB设备驱动实例

通过分析该驱动可以了解 USB设备驱动主要工作、如何读写数据。

drivers\usb\usb-skeleton.c

//定义厂商ID、产品ID
#define USB_SKEL_VENDOR_ID	0xfff0
#define USB_SKEL_PRODUCT_ID	0xfff0

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

//设备信息
struct usb_skel {
	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端点 : 向处理器提交数据
	struct urb		*bulk_in_urb;		/* the urb to read data with 用于读数据的URB*/
	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;		//缓冲区中的字节数
	size_t			bulk_in_copied;		//already copied to user space  已经拷贝到用户空间的字节数
	__u8			bulk_in_endpointAddr;	/* the address of the bulk in endpoint */
	
	//批量OUT端点 :接收处理器传递过来的数据
	__u8			bulk_out_endpointAddr;	/* 批量输出端点的地址*/
	
	int			errors;			/* the last request tanked */
	bool			ongoing_read;		/* a read is going on */
	spinlock_t		err_lock;		/* lock for errors */
	struct kref		kref;
	struct mutex		io_mutex;		/* synchronize I/O with disconnect */
	unsigned long		disconnected:1;
	wait_queue_head_t	bulk_in_wait;		/* to wait for an ongoing read */
};

//read操作 提交URB的回调函数
static void skel_read_bulk_callback(struct urb *urb)
{
	struct usb_skel *dev;
	unsigned long flags;

	dev = urb->context;

	spin_lock_irqsave(&dev->err_lock, flags);
	//求情失败
	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;
	}
	dev->ongoing_read = 0;
	spin_unlock_irqrestore(&dev->err_lock, flags);

	wake_up_interruptible(&dev->bulk_in_wait);
}	

// 填充URB,提交URB
static int skel_do_read_io(struct usb_skel *dev, size_t count)
{
	int rv;

	// 填充URB 准备读
	/*
	struct usb_skel 
		struct urb		*bulk_in_urb
	
	struct usb_skel
		struct usb_device	*udev;
	
	//创建IN方向的批量管道 看代码可以知道 所谓的管道 其实就是一个数字,理解为编号
		//参1 usb设备  参2 批量输入端点地址
	usb_sndbulkpipe(dev->udev, dev->bulk_in_endpointAddr)
	
	dev->bulk_in_buffer :  the buffer to receive data
	min(dev->bulk_in_size, count),: 准备读的数据大小
	skel_read_bulk_callback 读完回调函数:使用异步URB提交,提交完了就返回到调用它的线程,并不等待数据是否传成功传送到了应用。
	struct usb_skel 设备信息
	
	
		
												struct usb_skel
										+-------struct usb_device	*udev;
										|
	struct urb                          |
		struct usb_device *dev----------+
		unsigned int pipe
		//USB设备数据缓存区
		void *transfer_buffer
		//缓存大小
		u32 transfer_buffer_length;
		//回调函数
		usb_complete_t complete;
		//回调函数参数
		void *context;	
	
	
	*/
	usb_fill_bulk_urb(dev->bulk_in_urb,
			dev->udev,
			usb_rcvbulkpipe(dev->udev,
				dev->bulk_in_endpointAddr),
			dev->bulk_in_buffer,
			min(dev->bulk_in_size, count),
			skel_read_bulk_callback,
			dev);

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

	//异步提交URB
	/*
	执行后在回调中 查看  urb->status
		0 : 
			输入URB: 请求的数据被成功收到
			输出URB:数据被成功发送
		else 失败
	*/
	rv = usb_submit_urb(dev->bulk_in_urb, GFP_KERNEL);
	
	...
	
	return rv;
	
}

/* 应用程序读 /dev/skel%d 
同样使用异步URB提交,提交完了就返回到调用它的线程,并不等待数据是否传成功上传给应用

1 获取usb设备信息
2 如果缓冲区有数据,copy_to_user拷贝数据到用户空间
3 如果缓存中没有数据
	3.1设置用于批量输入端点的URB,主要是:
		管道信息(批量输入端点地址)
		接收数据缓存地址 
		缓存大小 
		回调函数 
	3.2 提交URB,请求数据
4 调动回调
	如果请求的数据被成功收到,记录实际请求到的数据size

*/
static ssize_t skel_read(struct file *file, char *buffer, size_t count,
			 loff_t *ppos)
{
	struct usb_skel *dev;
	int rv;
	bool ongoing_io;

	//获取usb设备信息
	dev = file->private_data;

	/* if IO is under way, we must not touch things */
retry:

...

	/*
	 * if the buffer is filled we may satisfy the read
	 * else we need to start IO
	 如果缓冲区被填满,我们可以满足读取,否则我们需要开始IO
	 */

	//缓冲区有数据
	if (dev->bulk_in_filled) {
		//当前待读取数据量
		size_t available = dev->bulk_in_filled - dev->bulk_in_copied;
		size_t chunk = min(available, count);

		//没有数据可读
		if (!available) {
			//提交数据请求URB 拿数据
			rv = skel_do_read_io(dev, count);
			if (rv < 0)
				goto exit;
			else
				goto retry;
		}

		//拷贝数据到用户空间
		/*
		buffer : 目的地址
		dev->bulk_in_buffer + dev->bulk_in_copied :源地址
		chunk : 拷贝数据大小
		*/
		if (copy_to_user(buffer, dev->bulk_in_buffer + dev->bulk_in_copied, chunk))
			rv = -EFAULT;
		else
			rv = chunk;

		//已拷贝数据大小 ++
		dev->bulk_in_copied += chunk;
	
		//剩下可用数据小于用户需要的数据
		if (available < count){
			// 填充URB,提交URB
			skel_do_read_io(dev, count - chunk);
		}
	} else {
		//缓存中没有数据
		// 填充URB,提交URB  请求数据
		rv = skel_do_read_io(dev, count);
		if (rv < 0)
			goto exit;
		else
			goto retry;
	}
exit:
	mutex_unlock(&dev->io_mutex);
	return rv;
}



//应用程序写 /dev/skel%d 
/*
使用异步URB提交,提交完了就返回到调用它的线程,并不等待数据是否传成功传送到了外围设备。
1 创建一个URB
2 分配USB设备数据传输缓冲区,并将用户空间数据拷贝到该空间
3 填充URB
	如重点是 
		管道信息(批量输出端点地址)
		发送数据缓存地址 
		缓存大小
		回调函数 
		
4 提交URB,数据被成功发送
*/
static ssize_t skel_write(struct file *file, const char *user_buffer,
			  size_t count, loff_t *ppos)
{
	struct usb_skel *dev;
	int retval = 0;
	struct urb *urb = NULL;
	char *buf = NULL;
	size_t writesize = min(count, (size_t)MAX_TRANSFER);
	
	/* 获取当前USB设备信息
		 在 open()中绑定
	*/
	dev = file->private_data;

	/* verify that we actually have some data to write */
	if (count == 0)
		goto exit;

...

	/* 创建一个URB
		creates a new urb for a USB driver to use
	*/
	urb = usb_alloc_urb(0, GFP_KERNEL);


	/*  allocate a DMA-consistent buffer,  USB设备数据传输缓冲区 DMA缓存
	
	 * @dev: device the buffer will be used with
	 * @size: requested buffer size
	 * @mem_flags: affect whether allocation may block
	 * @dma: used to return DMA address of buffer
	*/
	buf = usb_alloc_coherent(dev->udev, writesize, GFP_KERNEL,
				 &urb->transfer_dma);

	//拷贝用户空间数据 到 DMA缓存
	if (copy_from_user(buf, user_buffer, writesize)) {
		retval = -EFAULT;
		goto error;
	}

...
	// 填充URB
	/*
	
	struct urb
	
	struct usb_skel
		struct usb_device	*udev;
	
	//创建OUT方向的批量管道 看代码可以知道 所谓的管道 其实就是一个数字,理解为编号
		//参1 usb设备  参2 批量输出端点地址
	usb_sndbulkpipe(dev->udev, dev->bulk_out_endpointAddr)
	
	buf : USB设备DMA缓存
	writesize: 准备写的数据大小
	写完回调函数:使用异步URB提交,提交完了就返回到调用它的线程,并不等待数据是否传成功传送到了外围设备。
	struct usb_skel 设备信息
	
	
												struct usb_skel
										+-------struct usb_device	*udev;
										|
	struct urb                          |
		struct usb_device *dev----------+
		unsigned int pipe
		//USB设备数据缓存区
		void *transfer_buffer
		//缓存大小
		u32 transfer_buffer_length;
		//回调函数
		usb_complete_t complete;
		//回调函数参数
		void *context;	
	*/
	usb_fill_bulk_urb(urb, dev->udev,  usb_sndbulkpipe(dev->udev, dev->bulk_out_endpointAddr), buf, writesize, skel_write_bulk_callback, dev);
	
	/*
	struct urb 
		//dma addr for transfer_buffer
		dma_addr_t transfer_dma;
		
	Transfer_dma是有效的,所以最好利用它进行数据传输
	*/
	urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
	
...

	/* send the data out the bulk port 从批量端口发送数据 */
	//异步提交URB 
	/*
		执行后在回调中 查看  urb->status
			0 : 
				输入URB: 请求的数据被成功收到
				输出URB:数据被成功发送
			else 失败
	*/
	retval = usb_submit_urb(urb, GFP_KERNEL);

...

	return writesize;
}


/*
当应用程序打开 /dev/skel%d 设备文件的时候,调用 skel_open()
工作: 根据文件次设备号,获取usb设备接口信息,再通过接口信息 获取 usb设备信息,将其绑定到 file

	struct file
		//needed for tty driver, and maybe others 
		void	*private_data; ------------------------- struct usb_skel 
*/
static int skel_open(struct inode *inode, struct file *file)
{
	struct usb_skel *dev;
	struct usb_interface *interface;
	int subminor;
	int retval = 0;

	//文件所对应的次设备号
	subminor = iminor(inode);

	//获取接口
	interface = usb_find_interface(&skel_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;
	}
...
	/* save our object in the file's private structure */
	/*
	struct file
		//needed for tty driver, and maybe others 
		void	*private_data; ------------------------- struct usb_skel 
	*/
	file->private_data = dev;

exit:
	return retval;
}




//字符设备操作集
static const struct file_operations skel_fops = {
	.owner =	THIS_MODULE,
	.read =		skel_read,
	.write =	skel_write,
	.open =		skel_open,
	.release =	skel_release,
	.flush =	skel_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 skel_class = {
	.name =		"skel%d",
	.fops =		&skel_fops,
	.minor_base =	USB_SKEL_MINOR_BASE,
};

prob工作:
在这里插入图片描述

/*
当集线器驱动完成枚举动作,唤醒 khubd线程,该线程就会执行 该USB设备驱动的 prob()
工作:
根据设备接口信息 获取接口下的目标 批量输入端点 批量输出端点等信息,并注册该usb设备
*/
static int skel_probe(struct usb_interface *interface,
		      const struct usb_device_id *id)
{
	//设备信息
	struct usb_skel *dev;
	//端点描述符
	struct usb_endpoint_descriptor *bulk_in, *bulk_out;
	int retval;

	/* allocate memory for our device state and initialize it */
	dev = kzalloc(sizeof(*dev), GFP_KERNEL);
	if (!dev)
		return -ENOMEM;

	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);
	init_waitqueue_head(&dev->bulk_in_wait);

	/* 拿到目标 usb_device 和 usb接口
	
	struct usb_skel
		
	
	*/
	dev->udev = usb_get_dev(interface_to_usbdev(interface));
	dev->interface = usb_get_intf(interface);

	/* set up the endpoint information */
	/* use only the first bulk-in and bulk-out endpoints */
	
	/* 
	遍历该接口下的所有端点,我们当前只查找一个批量输入端点和一个批量输出端点 拿到后存储返回
	批量输入端点 : 存放到  bulk_in
	批量输出端点 :  存放到  bulk_out
	
	struct usb_interface
		struct usb_host_interface *cur_altsetting;
	
	struct usb_endpoint_descriptor *bulk_in
	
	struct usb_endpoint_descriptor *bulk_out
	*/
	retval = usb_find_common_endpoints(interface->cur_altsetting,
			&bulk_in, &bulk_out, NULL, NULL);
	if (retval) {
		dev_err(&interface->dev,
			"Could not find both bulk-in and bulk-out endpoints\n");
		goto error;
	}

	/*
	struct usb_skel	
		size_t	bulk_in_size; //the size of the receive buffer
		__u8	bulk_in_endpointAddr; //the address of the bulk in endpoint
		unsigned char	*bulk_in_buffer;	//the buffer to receive data
	*/
	dev->bulk_in_size = usb_endpoint_maxp(bulk_in);//获取批量输入端点的最大数据包大小
	dev->bulk_in_endpointAddr = bulk_in->bEndpointAddress;//批量输入端点地址
	dev->bulk_in_buffer = kmalloc(dev->bulk_in_size, GFP_KERNEL);//接收数据缓存
	if (!dev->bulk_in_buffer) {
		retval = -ENOMEM;
		goto error;
	}
	
	//创建一个新的urb供驱动程序使用
	/*
	struct usb_skel	
		struct urb	*bulk_in_urb; // the urb to read data with 
	*/
	dev->bulk_in_urb = usb_alloc_urb(0, GFP_KERNEL);
	if (!dev->bulk_in_urb) {
		retval = -ENOMEM;
		goto error;
	}

	/* 设置批量输出端点地址
	struct usb_skel
		__u8	bulk_out_endpointAddr;	//the address of the bulk out endpoint 
	*/
	dev->bulk_out_endpointAddr = bulk_out->bEndpointAddress;

	/* save our data pointer in this interface device */
	/*
	struct usb_skel
	|	struct usb_interface	*interface;	//the interface for this device 
	|		struct device dev;
	+-----------void	*driver_data;

	*/
	usb_set_intfdata(interface, dev);

	/* we can register the device now, as it is ready
	   注册 USB设备,将 字符设备 /dev/skel%d 提供给用户空间, 应用程序操作 /dev/skel%d 和 该USB设备交换数据
		注意:
						//全局 字符设备操作集数组
						static const struct file_operations *usb_minors[MAX_USB_MINORS];
														 |
														 |
														 |
	static struct usb_class_driver skel_class            |
		.fops =		&skel_fops---------------------------+
	
	struct usb_skel
		struct usb_interface	*interface;	//the interface for this device 
			int minor;	//数组下标
	*/
	retval = usb_register_dev(interface, &skel_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;
	}

	/* 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:
	/* this frees allocated memory */
	kref_put(&dev->kref, skel_delete);

	return retval;
}

//USB设备拔出时 调用
static void skel_disconnect(struct usb_interface *interface)
{
	struct usb_skel *dev;
	int minor = interface->minor;

	dev = usb_get_intfdata(interface);
	usb_set_intfdata(interface, NULL);

	/* give back our minor */
	usb_deregister_dev(interface, &skel_class);

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

	usb_kill_anchored_urbs(&dev->submitted);

	/* decrement our usage count */
	kref_put(&dev->kref, skel_delete);

	dev_info(&interface->dev, "USB Skeleton #%d now disconnected", minor);
}



//当USB核心监测到某个设备的属性和该 usb_device_id信息一致时,就会执行 该驱动的 probe()
static const struct usb_device_id skel_table[] = {
	{ USB_DEVICE(USB_SKEL_VENDOR_ID, USB_SKEL_PRODUCT_ID) },
	{ }					/* Terminating entry */
};
MODULE_DEVICE_TABLE(usb, skel_table);

//usb 设备驱动
static struct usb_driver skel_driver = {
	.name =		"skeleton",
	.probe =	skel_probe,
	.disconnect =	skel_disconnect,
	.suspend =	skel_suspend,
	.resume =	skel_resume,
	.pre_reset =	skel_pre_reset,
	.post_reset =	skel_post_reset,
	.id_table =	skel_table,//USB设备的ID信息
	.supports_autosuspend = 1,
};


//向内核注册 USB 驱动
module_usb_driver(skel_driver);

drivers\usb\core\usb.c

/**
 * usb_find_common_endpoints() -- look up common endpoint descriptors
 * @alt:	alternate setting to search
 * @bulk_in:	pointer to descriptor pointer, or NULL
 * @bulk_out:	pointer to descriptor pointer, or NULL
 * @int_in:	pointer to descriptor pointer, or NULL
 * @int_out:	pointer to descriptor pointer, or NULL
 *
 * Search the alternate setting's endpoint descriptors for the first bulk-in,
 * bulk-out, interrupt-in and interrupt-out endpoints and return them in the
 * provided pointers (unless they are NULL).
 *
 * If a requested endpoint is not found, the corresponding pointer is set to
 * NULL.
 *
 * 如果找到所有请求的描述符,则返回:0,否则返回-ENXIO。
 */
 	/* 
	
遍历该接口下的所有端点,我们当前只查找一个大容量输入端点和一个大容量输出端点 拿到后存储返回
	
	struct usb_interface
		struct usb_host_interface *cur_altsetting;//备用设置
	
	struct usb_endpoint_descriptor *bulk_in  批量输入端点描述符 数组
	
	struct usb_endpoint_descriptor *bulk_out 批量输出端点描述符 数组
	
	NULL 中断输入端点描述符 数组
	
	NULL 中断输出端点描述符 数组
	
	*/
int usb_find_common_endpoints(struct usb_host_interface *alt,
		struct usb_endpoint_descriptor **bulk_in,
		struct usb_endpoint_descriptor **bulk_out,
		struct usb_endpoint_descriptor **int_in,
		struct usb_endpoint_descriptor **int_out)
{
	struct usb_endpoint_descriptor *epd;
	int i;

	if (bulk_in)
		*bulk_in = NULL;
	if (bulk_out)
		*bulk_out = NULL;
	if (int_in)
		*int_in = NULL;
	if (int_out)
		*int_out = NULL;

	/* 遍历该接口下的所有端点

	struct usb_interface
		struct usb_host_interface *cur_altsetting;//备用设置
			//接口描述符
			struct usb_interface_descriptor	desc;
				//端点数量
				__u8  bNumEndpoints;
	*/
	for (i = 0; i < alt->desc.bNumEndpoints; ++i) {
		/* 获取端点描述符
		struct usb_interface
			struct usb_host_interface *cur_altsetting;//备用设置
				//端点数组
				struct usb_host_endpoint *endpoint;
					//端点描述符
					struct usb_endpoint_descriptor	desc;
		*/
		epd = &alt->endpoint[i].desc;

		/* 判断目标 端点描述符传输方向类型,并存存放到对应数组
			输入端点 : 存放到  bulk_in[0]
			输出端点 :  存放到  bulk_out[0]
			
		端点描述符
		struct usb_endpoint_descripto
		
		struct usb_endpoint_descriptor *bulk_in  批量输入端点描述符 数组
	
		struct usb_endpoint_descriptor *bulk_out 批量输出端点描述符 数组
		
		NULL 中断输入端点描述符 数组
	
		NULL 中断输出端点描述符 数组
		
		*/
		if (match_endpoint(epd, bulk_in, bulk_out, int_in, int_out))
			return 0;
	}

	return -ENXIO;
}
EXPORT_SYMBOL_GPL(usb_find_common_endpoints);




/*
端点描述符
struct usb_endpoint_descripto

struct usb_endpoint_descriptor *bulk_in  批量输入端点描述符 数组

struct usb_endpoint_descriptor *bulk_out 批量输出端点描述符 数组

NULL 中断输入端点描述符 数组

NULL 中断输出端点描述符 数组

*/
static bool match_endpoint(struct usb_endpoint_descriptor *epd,
		struct usb_endpoint_descriptor **bulk_in,
		struct usb_endpoint_descriptor **bulk_out,
		struct usb_endpoint_descriptor **int_in,
		struct usb_endpoint_descriptor **int_out)
{
	/*
	获取当前端点的传输类型,并存存放到对应数组
	*/
	switch (usb_endpoint_type(epd)) {
		
	//如果是批量传输类型端点
	case USB_ENDPOINT_XFER_BULK:
		/*
		判断该端点方向
			输入端点 : 存放到  bulk_in[0]
			输出端点 :  存放到  bulk_out[0]
		*/
		if (usb_endpoint_dir_in(epd)) {
			if (bulk_in && !*bulk_in) {
				//获取该
				*bulk_in = epd;
				break;
			}
		} else {
			if (bulk_out && !*bulk_out) {
				*bulk_out = epd;
				break;
			}
		}

		return false;
...
	default:
		return false;
	}
	return (!bulk_in || *bulk_in) && (!bulk_out || *bulk_out) &&
			(!int_in || *int_in) && (!int_out || *int_out);
}

kernel\include\linux\usb.h

/**
 * usb_fill_bulk_urb - macro to help initialize a bulk urb
 * @urb: pointer to the urb to initialize.
 * @dev: pointer to the struct usb_device for this urb.
 * @pipe: the endpoint pipe
 * @transfer_buffer: pointer to the transfer buffer
 * @buffer_length: length of the transfer buffer
 * @complete_fn: pointer to the usb_complete_t function
 * @context: what to set the urb context to.
 *
 * Initializes a bulk urb with the proper information needed to submit it
 * to a device.
 */
 
/*

struct urb

struct usb_skel
	struct usb_device	*udev;

//创建OUT方向的批量管道 看代码可以知道 所谓的管道 其实就是一个数字,理解为编号
usb_sndbulkpipe(dev->udev, dev->bulk_out_endpointAddr)

buf :USB设备DMA缓存
writesize: 准备写的数据大小
写完回调函数:使用异步URB提交,提交完了就返回到调用它的线程,并不等待数据是否传成功传送到了外围设备。
struct usb_skel 设备信息

*/
static inline 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)
{
	
	/*
	
											struct usb_skel
										+-------struct usb_device	*udev;
										|
	struct urb                          |
		struct usb_device *dev----------+
		unsigned int pipe
		//USB设备数据缓存区
		void *transfer_buffer
		//缓存大小
		u32 transfer_buffer_length;
		//回调函数
		usb_complete_t complete;
		//回调函数参数
		void *context;			
				
	*/
	urb->dev = dev;
	urb->pipe = pipe;
	urb->transfer_buffer = transfer_buffer;
	urb->transfer_buffer_length = buffer_length;
	urb->complete = complete_fn;
	urb->context = context;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Linux老A

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值