嵌入式Linux设备驱动程序开发指南20(Linux USB设备驱动)——读书笔记

二十、Linux USB设备驱动

20.1 USB简介

USB(通用串行总线)最早的USB总线速率包括低速(1.5Mbps)、高速(480Mbps),USB 3.0规范出现后,速率达到4.8Gbps。USB最大优点是它支持动态连接和移除,一种称”即插即用“的接口。

20.1.1 USB2.0总线拓扑

20.1.2 USB总线枚举和设备布局

USB主机控制轮询总线,其中所有十五均由USB主机发起。
端点、描述符。

20.1.3 USB数据传输

一旦枚举完成,主机和设备就可以自由地进行通信。

四种不同类型的传输:
控制传输;
批量数据传输;
中断数据传输;
等时数据传输;

20.1.4 USB设备类别

常见的有HID、打印机、成像设备、大容量存储设备和通信设备。

20.1.5 USB描述符

设备描述符
配置描述符
接口描述符
端点描述符

20.2 Linux USB 子系统

Linux USB是一类特定API实现,用于支持USB外设和主机控制器。
Linux USB API支持对控制消息和批量消息的同步调用。

20.3 编写Linux USB设备驱动程序

20.3.1 注册设备驱动

注册USB设备驱动程序,usb_driver结构体定义在,如下:

/linux/driver/misc/usbsevseg.c

20.3.2 Linux 主机端数据类型

USB设备驱动程序实际上绑定到接口,而不是绑定到设备。

20.3.3 USB请求块

20.4 USB LED模块

USB HID设备固件,该设备能够使用HID报告来发送和接收数据。

20.4 USB LED驱动代码


#include <linux/slab.h>
#include <linux/module.h>
#include <linux/usb.h>

//创建ID表示支持热插拔
#define USBLED_VENDOR_ID	0x04D8	
#define USBLED_PRODUCT_ID	0x003F	

/* table of devices that work with this driver */
static const struct usb_device_id id_table[] = {
	{ USB_DEVICE(USBLED_VENDOR_ID, USBLED_PRODUCT_ID) },
	{ }
};
MODULE_DEVICE_TABLE(usb, id_table);

//创建一个结构来存储驱动程序数据
struct usb_led {
	struct usb_device *udev;
	u8 led_number;
};

static ssize_t led_show(struct device *dev, struct device_attribute *attr,
			  char *buf)
{
	struct usb_interface *intf = to_usb_interface(dev);
	struct usb_led *led = usb_get_intfdata(intf);			
									
	return sprintf(buf, "%d\n", led->led_number);
}

static ssize_t led_store(struct device *dev, struct device_attribute *attr,
			 const char *buf, size_t count)
{
	struct usb_interface *intf = to_usb_interface(dev);
	struct usb_led *led = usb_get_intfdata(intf);
    	u8 val;
    	int error, retval;
        dev_info(&intf->dev, "led_store() function is called.\n");
    
    	/* transform char array to u8 value */
	error = kstrtou8(buf, 10, &val);
	if (error)
		return error;
    
    	led->led_number = val;

	if (val == 1 || val == 2 || val == 3)
        dev_info(&led->udev->dev, "led = %d\n", led->led_number);
    	else {
        	dev_info(&led->udev->dev, "unknown led %d\n", led->led_number);
        	retval = -EINVAL;
        	return retval;
    	}

	/* Toggle led */
	retval = usb_bulk_msg(led->udev, usb_sndctrlpipe(led->udev, 1),
				 &led->led_number, 
				 1,
				 NULL, 
				 0);
	if (retval) {
		retval = -EFAULT;
		return retval;
	}
	return count;
}
static DEVICE_ATTR_RW(led);

static int led_probe(struct usb_interface *interface,
		     const struct usb_device_id *id)
{
	struct usb_device *udev = interface_to_usbdev(interface);
	struct usb_led *dev = NULL;
	int retval = -ENOMEM;

	dev_info(&interface->dev, "led_probe() function is called.\n");

	dev = kzalloc(sizeof(struct usb_led), GFP_KERNEL);
	if (!dev) {
		dev_err(&interface->dev, "out of memory\n");
        retval = -ENOMEM;
		goto error;
	}

	dev->udev = usb_get_dev(udev);

	usb_set_intfdata(interface, dev);
    
    	retval = device_create_file(&interface->dev, &dev_attr_led);
	if (retval)
		goto error_create_file;

	return 0;

error_create_file:
	usb_put_dev(udev);
	usb_set_intfdata(interface, NULL);
error:
	kfree(dev);
	return retval;
}

static void led_disconnect(struct usb_interface *interface)
{
	struct usb_led *dev;

	dev = usb_get_intfdata(interface);

	device_remove_file(&interface->dev, &dev_attr_led);
	usb_set_intfdata(interface, NULL);
	usb_put_dev(dev->udev);
	kfree(dev);

	dev_info(&interface->dev, "USB LED now disconnected\n");
}

static struct usb_driver led_driver = {
	.name =		"usbled",
	.probe =	led_probe,
	.disconnect =	led_disconnect,
	.id_table =	id_table,
};

//将驱动注册到USB总线
module_usb_driver(led_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR(" ");
MODULE_DESCRIPTION("This is a synchronous led usb controlled module");

20.5 USB LED 和开关模块

20.5.1 代码

#include <linux/slab.h>
#include <linux/module.h>
#include <linux/usb.h>

#define USBLED_VENDOR_ID	0x04D8	
#define USBLED_PRODUCT_ID	0x003F	

static void led_urb_out_callback(struct urb *urb);
static void led_urb_in_callback(struct urb *urb);

/* table of devices that work with this driver */
static const struct usb_device_id id_table[] = {
	{ USB_DEVICE(USBLED_VENDOR_ID, USBLED_PRODUCT_ID) },
	{ }
};
MODULE_DEVICE_TABLE(usb, id_table);

struct usb_led {
	struct usb_device *udev;
	struct usb_interface *intf;
	struct urb 	  *interrupt_out_urb;
	struct urb 	  *interrupt_in_urb;
	struct usb_endpoint_descriptor *interrupt_out_endpoint;
	struct usb_endpoint_descriptor *interrupt_in_endpoint;
	u8		  irq_data;
	u8		  led_number;
	u8 		  ibuffer;
	int		  interrupt_out_interval;
	int ep_in;
	int ep_out;
};

static ssize_t led_show(struct device *dev, struct device_attribute *attr,
			  char *buf)
{
	struct usb_interface *intf = to_usb_interface(dev);
	struct usb_led *led = usb_get_intfdata(intf);			\
									\
	return sprintf(buf, "%d\n", led->led_number);
}

static ssize_t led_store(struct device *dev, struct device_attribute *attr,
			 const char *buf, size_t count)
{
	/* interface: related set of endpoints which present a single feature or function to the host */
	struct usb_interface *intf = to_usb_interface(dev);
	struct usb_led *led = usb_get_intfdata(intf);
    	u8 val;
    	int error, retval;
    
        dev_info(&intf->dev, "led_store() function is called.\n");
    
    	/* transform char array to u8 value */
	error = kstrtou8(buf, 10, &val);
	if (error)
		return error;
    
    	led->led_number = val;
	led->irq_data = val;

	if (val == 0)
		dev_info(&led->udev->dev, "read status\n");
	else if (val == 1 || val == 2 || val == 3)
        	dev_info(&led->udev->dev, "led = %d\n", led->led_number);
    	else {
        	dev_info(&led->udev->dev, "unknown value %d\n", val);
        	retval = -EINVAL;
        	return retval;
    	}
	
	/* send the data out */
	retval = usb_submit_urb(led->interrupt_out_urb, GFP_KERNEL);
	if (retval) {
        	dev_err(&led->udev->dev,
			"Couldn't submit interrupt_out_urb %d\n", retval);
		return retval;
	}

	return count;
}
static DEVICE_ATTR_RW(led);

static void led_urb_out_callback(struct urb *urb)
{
	struct usb_led *dev;

	dev = urb->context;

	dev_info(&dev->udev->dev, "led_urb_out_callback() function is called.\n");

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

static void led_urb_in_callback(struct urb *urb)
{
	int retval;
	struct usb_led *dev;

	dev = urb->context;

	dev_info(&dev->udev->dev, "led_urb_in_callback() function is called.\n");

	if (urb->status) {
		if (!(urb->status == -ENOENT ||
		    urb->status == -ECONNRESET ||
		    urb->status == -ESHUTDOWN))
			dev_err(&dev->udev->dev,
				"%s - nonzero write status received: %d\n",
				__func__, urb->status);
	}

	if (dev->ibuffer == 0x00)
		pr_info ("switch is ON.\n");
	else if (dev->ibuffer == 0x01)
		pr_info ("switch is OFF.\n");
	else
		pr_info ("bad value received\n");

	retval = usb_submit_urb(dev->interrupt_in_urb, GFP_KERNEL);
	if (retval) 
        	dev_err(&dev->udev->dev,
			"Couldn't submit interrupt_in_urb %d\n", retval);
}

static int led_probe(struct usb_interface *intf,
		     const struct usb_device_id *id)
{
	struct usb_device *udev = interface_to_usbdev(intf);
	struct usb_host_interface *altsetting = intf->cur_altsetting;
	struct usb_endpoint_descriptor *endpoint;
	struct usb_led *dev = NULL;
	int ep;
	int ep_in, ep_out;
	int retval, size, res;
	retval = 0;

	dev_info(&intf->dev, "led_probe() function is called.\n");

	res = usb_find_last_int_out_endpoint(altsetting, &endpoint);
	if (res) {
		dev_info(&intf->dev, "no endpoint found");
		return res;
	}

	ep = usb_endpoint_num(endpoint); /* value from 0 to 15, it is 1 */
	size = usb_endpoint_maxp(endpoint);

	/* Validate endpoint and size */
	if (size <= 0) {
		dev_info(&intf->dev, "invalid size (%d)", size);
		return -ENODEV;
	}

	dev_info(&intf->dev, "endpoint size is (%d)", size);
	dev_info(&intf->dev, "endpoint number is (%d)", ep);

	ep_in = altsetting->endpoint[0].desc.bEndpointAddress;
	ep_out = altsetting->endpoint[1].desc.bEndpointAddress;

	dev_info(&intf->dev, "endpoint in address is (%d)", ep_in);
	dev_info(&intf->dev, "endpoint out address is (%d)", ep_out);

	dev = kzalloc(sizeof(struct usb_led), GFP_KERNEL);

	if (!dev) 
		return -ENOMEM;

	dev->ep_in = ep_in;
	dev->ep_out = ep_out;

	dev->udev = usb_get_dev(udev);

	dev->intf = intf;

	/* allocate int_out_urb structure */
	dev->interrupt_out_urb = usb_alloc_urb(0, GFP_KERNEL);
	if (!dev->interrupt_out_urb)
		goto error_out;

	/* initialize int_out_urb */
	usb_fill_int_urb(dev->interrupt_out_urb, 
			dev->udev, 
			usb_sndintpipe(dev->udev, ep_out), 
			(void *)&dev->irq_data,
			1,
			led_urb_out_callback, dev, 1);

	/* allocate int_in_urb structure */
	dev->interrupt_in_urb = usb_alloc_urb(0, GFP_KERNEL);
	if (!dev->interrupt_in_urb)
		goto error_out;

	/* initialize int_in_urb */
	usb_fill_int_urb(dev->interrupt_in_urb, 
			dev->udev, 
			usb_rcvintpipe(dev->udev, ep_in), 
			(void *)&dev->ibuffer,
			1,
			led_urb_in_callback, dev, 1);

	usb_set_intfdata(intf, dev);
    
    	retval = device_create_file(&intf->dev, &dev_attr_led);
	if (retval)
		goto error_create_file;

	retval = usb_submit_urb(dev->interrupt_in_urb, GFP_KERNEL);
	if (retval) {
        	dev_err(&dev->udev->dev,
			"Couldn't submit interrupt_in_urb %d\n", retval);
		device_remove_file(&intf->dev, &dev_attr_led);
		goto error_create_file;
	}
	
	dev_info(&dev->udev->dev,"int_in_urb submitted\n");

	return 0;

error_create_file:
	usb_free_urb(dev->interrupt_out_urb);
	usb_free_urb(dev->interrupt_in_urb);
	usb_put_dev(udev);
	usb_set_intfdata(intf, NULL);

error_out:
	kfree(dev);
	return retval;
}

static void led_disconnect(struct usb_interface *interface)
{
	struct usb_led *dev;

	dev = usb_get_intfdata(interface);

	device_remove_file(&interface->dev, &dev_attr_led);
	usb_free_urb(dev->interrupt_out_urb);
	usb_free_urb(dev->interrupt_in_urb);
	usb_set_intfdata(interface, NULL);
	usb_put_dev(dev->udev);
	kfree(dev);

	dev_info(&interface->dev, "USB LED now disconnected\n");
}

static struct usb_driver led_driver = {
	.name =		"usbled",
	.probe =	led_probe,
	.disconnect =	led_disconnect,
	.id_table =	id_table,
};

module_usb_driver(led_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR(" ");
MODULE_DESCRIPTION("This is a led/switch usb controlled module with irq in/out endpoints");

20.6 连接到USB多显LED的I2C模块

20.6.1 简介

使用芯片LTC3206 I2C 多显LED控制器。

20.6.2 代码

#include <linux/module.h>
#include <linux/slab.h>
#include <linux/usb.h>
#include <linux/i2c.h>

/* i2cset -y 4 0x1b 0x00 0xf0 0x00 i -> this is a full I2C block write blue and toggle the leds
i2cset -y 4 0x1b 0xf0 0x00 0x00 i -> red full 
i2cset -y 4 0x1b 0x10 0x00 0x00 i -> red low
i2cset -y 4 0x1b 0x00 0x0f 0x00 i -> green full
i2cset -y 4 0x1b 0x00 0x0f 0x0f i -> sub and green full
i2cset -y 4 0x1b 0x00 0x00 0xf0 i -> main full */

#define DRIVER_NAME	"usb-ltc3206"

#define USB_VENDOR_ID_LTC3206		0x04d8
#define USB_DEVICE_ID_LTC3206		0x003f

#define LTC3206_OUTBUF_LEN		3	/* USB write packet length */
#define LTC3206_I2C_DATA_LEN		3

/* Structure to hold all of our device specific stuff */
struct i2c_ltc3206 {
	u8 obuffer[LTC3206_OUTBUF_LEN];	/* USB write buffer */
	/* I2C/SMBus data buffer */
	u8 user_data_buffer[LTC3206_I2C_DATA_LEN];
	int ep_out;              	/* out endpoint */
	struct usb_device *usb_dev;	/* the usb device for this device */
	struct usb_interface *interface;/* the interface for this device */
	struct i2c_adapter adapter;	/* i2c related things */
	/* wq to wait for an ongoing write */
	wait_queue_head_t usb_urb_completion_wait;
	bool ongoing_usb_ll_op;		/* all is in progress */
	struct urb *interrupt_out_urb;
};

/*
 * Return list of I2C supported functionality
 */
static u32 ltc3206_usb_func(struct i2c_adapter *a)
{
	return I2C_FUNC_I2C | I2C_FUNC_SMBUS_EMUL |
	       I2C_FUNC_SMBUS_READ_BLOCK_DATA | I2C_FUNC_SMBUS_BLOCK_PROC_CALL;
}

/* usb out urb callback function */
static void ltc3206_usb_cmpl_cbk(struct urb *urb)
{
	struct i2c_ltc3206 *dev = urb->context;
	int status = urb->status;
	int retval;

	switch (status) {
	case 0:			/* success */
		break;
	case -ECONNRESET:	/* unlink */
	case -ENOENT:
	case -ESHUTDOWN:
		return;
	/* -EPIPE:  should clear the halt */
	default:		/* error */
		goto resubmit;
	}

	/* 
	 * wake up the waiting function
	 * modify the flag indicating the ll status 
	 */
	dev->ongoing_usb_ll_op = 0; /* communication is OK */
	wake_up_interruptible(&dev->usb_urb_completion_wait);
	return;

resubmit:
	retval = usb_submit_urb(urb, GFP_ATOMIC);
	if (retval) {
		dev_err(&dev->interface->dev,
			"ltc3206(irq): can't resubmit intrerrupt urb, retval %d\n",
			retval);
	}
}

static int ltc3206_ll_cmd(struct i2c_ltc3206 *dev)
{
	int rv;

	/* 
	 * tell everybody to leave the URB alone
	 * we are going to write to the LTC3206
	 */
	dev->ongoing_usb_ll_op = 1; /* doing USB communication */

	/* submit the interrupt out ep packet */
	if (usb_submit_urb(dev->interrupt_out_urb, GFP_KERNEL)) {
		dev_err(&dev->interface->dev,
				"ltc3206(ll): usb_submit_urb intr out failed\n");
		dev->ongoing_usb_ll_op = 0;
		return -EIO;
	}

	/* wait for its completion, the USB URB callback will signal it */
	rv = wait_event_interruptible(dev->usb_urb_completion_wait,
			(!dev->ongoing_usb_ll_op));
	if (rv < 0) {
		dev_err(&dev->interface->dev, "ltc3206(ll): wait interrupted\n");
		goto ll_exit_clear_flag;
	}

	return 0;

ll_exit_clear_flag:
	dev->ongoing_usb_ll_op = 0;
	return rv;
}

//分配并初始化用于主机和设备之间通信的中断输出URB
static int ltc3206_init(struct i2c_ltc3206 *dev)
{
	int ret;

	/* initialize the LTC3206 */
	dev_info(&dev->interface->dev,
		 "LTC3206 at USB bus %03d address %03d -- ltc3206_init()\n",
		 dev->usb_dev->bus->busnum, dev->usb_dev->devnum);

	dev->interrupt_out_urb = usb_alloc_urb(0, GFP_KERNEL);
	if (!dev->interrupt_out_urb){
		ret = -ENODEV;
		goto init_error;
	}

	usb_fill_int_urb(dev->interrupt_out_urb, dev->usb_dev,
				usb_sndintpipe(dev->usb_dev,
						  dev->ep_out),
				(void *)&dev->obuffer, LTC3206_OUTBUF_LEN, 
				ltc3206_usb_cmpl_cbk, dev,
				1);

	ret = 0;
	goto init_no_error;

init_error:
	dev_err(&dev->interface->dev, "ltc3206_init: Error = %d\n", ret);
	return ret;

init_no_error:
	dev_info(&dev->interface->dev, "ltc3206_init: Success\n");
	return ret;
}

static int ltc3206_i2c_write(struct i2c_ltc3206 *dev,
					struct i2c_msg *pmsg)
{
	u8 ucXferLen;
	int rv;
	u8 *pSrc, *pDst;
	
	if (pmsg->len > LTC3206_I2C_DATA_LEN)
	{
		pr_info ("problem with the lenght\n");
		return -EINVAL;
	}

	/* I2C write lenght */
	ucXferLen = (u8)pmsg->len;

	pSrc = &pmsg->buf[0];
	pDst = &dev->obuffer[0];
	memcpy(pDst, pSrc, ucXferLen);

	pr_info("oubuffer[0] = %d\n", dev->obuffer[0]);
	pr_info("oubuffer[1] = %d\n", dev->obuffer[1]);
	pr_info("oubuffer[2] = %d\n", dev->obuffer[2]);
		
	rv = ltc3206_ll_cmd(dev);
	if (rv < 0)
		return -EFAULT;

	return 0;
}

/* device layer */
static int ltc3206_usb_i2c_xfer(struct i2c_adapter *adap,
		struct i2c_msg *msgs, int num)
{
	struct i2c_ltc3206 *dev = i2c_get_adapdata(adap);
	struct i2c_msg *pmsg;
	int ret, count;

	pr_info("number of i2c msgs is = %d\n", num);

	for (count = 0; count < num; count++) {
		pmsg = &msgs[count];
		ret = ltc3206_i2c_write(dev, pmsg);
		if (ret < 0)
			goto abort;
	}

	/* if all the messages were transferred ok, return "num" */
	ret = num;
abort:
	return ret;
}

static const struct i2c_algorithm ltc3206_usb_algorithm = {
	.master_xfer = ltc3206_usb_i2c_xfer,
	.functionality = ltc3206_usb_func,
};

static const struct usb_device_id ltc3206_table[] = {
	{ USB_DEVICE(USB_VENDOR_ID_LTC3206, USB_DEVICE_ID_LTC3206) },
	{ }
};
MODULE_DEVICE_TABLE(usb, ltc3206_table);

static void ltc3206_free(struct i2c_ltc3206 *dev)
{
	usb_put_dev(dev->usb_dev);
	usb_set_intfdata(dev->interface, NULL);
	kfree(dev);
}

static int ltc3206_probe(struct usb_interface *interface,
			    const struct usb_device_id *id)
{
	struct usb_host_interface *hostif = interface->cur_altsetting;
	struct i2c_ltc3206 *dev;
	int ret;

	dev_info(&interface->dev, "ltc3206_probe() function is called.\n");

	/* allocate memory for our device state and initialize it */
	dev = kzalloc(sizeof(*dev), GFP_KERNEL);
	if (dev == NULL) {
		pr_info("i2c-ltc3206(probe): no memory for device state\n");
		ret = -ENOMEM;
		goto error;
	}

	/* get ep_out */
	dev->ep_out = hostif->endpoint[1].desc.bEndpointAddress;

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

	init_waitqueue_head(&dev->usb_urb_completion_wait);

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

	/* setup i2c adapter description */
	dev->adapter.owner = THIS_MODULE;
	dev->adapter.class = I2C_CLASS_HWMON;
	dev->adapter.algo = &ltc3206_usb_algorithm;
	i2c_set_adapdata(&dev->adapter, dev);

	snprintf(dev->adapter.name, sizeof(dev->adapter.name),
		 DRIVER_NAME " at bus %03d device %03d",
		 dev->usb_dev->bus->busnum, dev->usb_dev->devnum);

	dev->adapter.dev.parent = &dev->interface->dev;

	/* initialize ltc3206 i2c device */
	ret = ltc3206_init(dev);
	if (ret < 0) {  
		dev_err(&interface->dev, "failed to initialize adapter\n");
		goto error_init;
	}

	/* and finally attach to i2c layer */
	ret = i2c_add_adapter(&dev->adapter);
	if (ret < 0) {
		dev_info(&interface->dev, "failed to add I2C adapter\n");
		goto error_i2c;
	}

	dev_info(&dev->interface->dev,
			"ltc3206_probe() -> chip connected -> Success\n");
	return 0;

error_init:
	usb_free_urb(dev->interrupt_out_urb);

error_i2c:
	usb_set_intfdata(interface, NULL);
	ltc3206_free(dev);
error:
	return ret;
}

static void ltc3206_disconnect(struct usb_interface *interface)
{
	struct i2c_ltc3206 *dev = usb_get_intfdata(interface);

	i2c_del_adapter(&dev->adapter);

	usb_kill_urb(dev->interrupt_out_urb);
	usb_free_urb(dev->interrupt_out_urb);

	usb_set_intfdata(interface, NULL);
	ltc3206_free(dev);

	pr_info("i2c-ltc3206(disconnect) -> chip disconnected");
}

static struct usb_driver ltc3206_driver = {
	.name = DRIVER_NAME,
	.probe = ltc3206_probe,
	.disconnect = ltc3206_disconnect,
	.id_table = ltc3206_table,
};

module_usb_driver(ltc3206_driver);

MODULE_AUTHOR(" ");
MODULE_DESCRIPTION("This is a usb controlled i2c ltc3206 device");
MODULE_LICENSE("GPL");


感谢阅读,祝君成功!
-by aiziyou

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Jack.Jia

感谢打赏!

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

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

打赏作者

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

抵扣说明:

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

余额充值