<kernel>kernel 6.4 USB-之-usb_new_device()分析

<kernel>kernel 6.4 USB-之-usb_new_device()分析

kernel 6.4 USB系列文章如下:
<kernel>kernel 6.4 USB-之-hub_event()分析
<kernel>kernel 6.4 USB-之-port_event()分析
<kernel>kernel 6.4 USB-之-hub_port_connect_change()分析
<kernel>kernel 6.4 USB-之-hub_port_connect()分析
<kernel>kernel 6.4 USB-之-hub_port_init()分析
<kernel>kernel 6.4 USB-之-usb_new_device()分析

本文是基于linux kernel 6.4版本内核分析;源码下载路径:linux kernel
在这里插入图片描述

一、前言

在Linux内核的USB子系统中,用于处理新连接的USB设备的函数usb_new_device。它的主要作用是初始化新连接的USB设备,并将其添加到设备系统中。
这个函数接受一个指向struct usb_device的指针作为参数,这个结构体包含了新连接的USB设备的所有信息。
函数的主要步骤和作用如下:
(1) 如果USB设备不是根集线器,则禁用设备唤醒功能。
(2) 向运行时电源管理框架报告设备处于活动状态,并启用设备的自动挂起功能。
(3) 禁用所有设备的自动挂起功能。
(4) 调用usb_enumerate_device函数读取设备的描述符。
(5) 如果读取描述符失败,则跳转到错误处理部分。
(6) 将设备的设备号设置为USB设备的主设备号和次设备号的组合。
(7) 调用announce_device函数公告设备。
(8) 如果设备有序列号、产品名或制造商名,则将这些信息添加到设备的随机性中。
(9) 启用设备的异步挂起功能。
(10) 检查集线器或固件是否将此端口标记为不可移动。
(11) 调用device_add函数将设备注册到设备系统中。
(12) 如果设备有父设备,则在设备和USB端口设备之间创建链接文件。
(13) 调用usb_create_ep_devs函数为设备创建端点设备。
(14) 将设备标记为最后一次忙碌。

如果所有步骤都成功,则返回0;否则,将设备状态设置为未连接,并禁用运行时电源管理,然后返回错误代码。

二、usb_new_device()函数

usb_new_device()函数内容如下:

int usb_new_device(struct usb_device *udev)
{
	int err;

	if (udev->parent) {
		/* Initialize non-root-hub device wakeup to disabled;
		 * device (un)configuration controls wakeup capable
		 * sysfs power/wakeup controls wakeup enabled/disabled
		 */
		device_init_wakeup(&udev->dev, 0);
	}

	/* Tell the runtime-PM framework the device is active */
	pm_runtime_set_active(&udev->dev);
	pm_runtime_get_noresume(&udev->dev);
	pm_runtime_use_autosuspend(&udev->dev);
	pm_runtime_enable(&udev->dev);

	/* By default, forbid autosuspend for all devices.  It will be
	 * allowed for hubs during binding.
	 */
	usb_disable_autosuspend(udev);

	err = usb_enumerate_device(udev);	/* Read descriptors */
	if (err < 0)
		goto fail;
	dev_dbg(&udev->dev, "udev %d, busnum %d, minor = %d\n",
			udev->devnum, udev->bus->busnum,
			(((udev->bus->busnum-1) * 128) + (udev->devnum-1)));
	/* export the usbdev device-node for libusb */
	udev->dev.devt = MKDEV(USB_DEVICE_MAJOR,
			(((udev->bus->busnum-1) * 128) + (udev->devnum-1)));

	/* Tell the world! */
	announce_device(udev);

	if (udev->serial)
		add_device_randomness(udev->serial, strlen(udev->serial));
	if (udev->product)
		add_device_randomness(udev->product, strlen(udev->product));
	if (udev->manufacturer)
		add_device_randomness(udev->manufacturer,
				      strlen(udev->manufacturer));

	device_enable_async_suspend(&udev->dev);

	/* check whether the hub or firmware marks this port as non-removable */
	set_usb_port_removable(udev);

	/* Register the device.  The device driver is responsible
	 * for configuring the device and invoking the add-device
	 * notifier chain (used by usbfs and possibly others).
	 */
	err = device_add(&udev->dev);
	if (err) {
		dev_err(&udev->dev, "can't device_add, error %d\n", err);
		goto fail;
	}

	/* Create link files between child device and usb port device. */
	if (udev->parent) {
		struct usb_hub *hub = usb_hub_to_struct_hub(udev->parent);
		int port1 = udev->portnum;
		struct usb_port	*port_dev = hub->ports[port1 - 1];

		err = sysfs_create_link(&udev->dev.kobj,
				&port_dev->dev.kobj, "port");
		if (err)
			goto fail;

		err = sysfs_create_link(&port_dev->dev.kobj,
				&udev->dev.kobj, "device");
		if (err) {
			sysfs_remove_link(&udev->dev.kobj, "port");
			goto fail;
		}

		if (!test_and_set_bit(port1, hub->child_usage_bits))
			pm_runtime_get_sync(&port_dev->dev);
	}

	(void) usb_create_ep_devs(&udev->dev, &udev->ep0, udev);
	usb_mark_last_busy(udev);
	pm_runtime_put_sync_autosuspend(&udev->dev);
	return err;

fail:
	usb_set_device_state(udev, USB_STATE_NOTATTACHED);
	pm_runtime_disable(&udev->dev);
	pm_runtime_set_suspended(&udev->dev);
	return err;
}

下面就对usb_new_device()函数内容详细分析:

2.1 第5-11行:初始化非根集线器(root hub)设备的唤醒状态

第5-11行:主要作用是初始化非根集线器(root hub)设备的唤醒状态。

这段代码的详细过程如下:

首先,检查udev(指向struct usb_device的指针,它包含了USB设备的所有信息)的parent成员。如果parent成员存在,那么说明这个设备不是根集线器(root hub)。

然后,调用device_init_wakeup函数来初始化这个设备的唤醒状态。这个函数的第一个参数是一个指向设备的指针,第二个参数是唤醒的初始状态。在这里,设备的唤醒状态被初始化为0,也就是禁用状态。

这段代码的主要作用是控制非根集线器设备的唤醒能力。在USB子系统中,设备的唤醒能力是可以被配置的。设备的唤醒能力被禁用时,设备在系统挂起(suspend)时无法唤醒系统。设备的唤醒能力被启用时,如果设备在系统挂起时有活动(例如,接收到数据),那么设备可以唤醒系统。

2.2 第13-17行:USB 设备进行电源管理

第13-17行:对 USB 设备进行电源管理的部分。

pm_runtime_set_active(&udev->dev); 这行告诉运行时电源管理框架,该设备是活动的。运行时电源管理框架是 Linux 内核中用于管理设备电源的子系统。当设备是活动的,意味着设备正在使用中,系统不会尝试将其电源关闭以节省能源。

pm_runtime_get_noresume(&udev->dev); 这行是获取设备的运行时引用,但不会尝试恢复设备。这意味着,如果设备当前处于挂起状态(即电源关闭状态),这个函数不会尝试将其唤醒。

pm_runtime_use_autosuspend(&udev->dev); 这行是启用设备的自动挂起功能。当设备启用了自动挂起后,如果一段时间内没有使用设备,系统会自动将其电源关闭以节省能源。

pm_runtime_enable(&udev->dev); 这行是启用设备的运行时电源管理。当设备的运行时电源管理被启用后,系统将会根据设备的使用情况,自动地调整设备的电源状态,以平衡能源消耗和性能。

总的来说,这段代码的作用是启用并配置 USB 设备的运行时电源管理,以达到节省能源的目的。

2.3 第22行:禁止 USB 设备(udev)的自动挂起功能

第22行:作用是禁止 USB 设备(udev)的自动挂起功能。

自动挂起是 Linux 内核中的一个电源管理机制,当设备在一段时间内没有被使用时,系统会自动将其电源关闭,以节省能源。然而,对于某些设备,我们可能不希望它们在空闲时被自动关闭电源,因此需要禁用它们的自动挂起功能。

这段代码的注释提到,“默认情况下,禁止所有设备的自动挂起。在绑定过程中,将允许对集线器使用此功能。” 这意味着在 USB 设备被初始化时,会禁止其自动挂起功能,但在后续的过程中,可能会根据需要启用集线器(即 USB 设备的一种)的自动挂起功能。

2.3.1 usb_disable_autosuspend()函数

路径:drivers\usb\core\driver.c
void usb_disable_autosuspend(struct usb_device *udev)
{
	pm_runtime_forbid(&udev->dev);
}
EXPORT_SYMBOL_GPL(usb_disable_autosuspend);

“这个函数阻止 @udev 设备被自动挂起,并且如果它已经被自动挂起,那么将唤醒它。调用者必须持有 @udev 设备的锁。” 这意味着,在调用这个函数之前,必须确保已经获取了设备的锁,以防止并发操作造成的问题。

路径:
void pm_runtime_forbid(struct device *dev)
{
	spin_lock_irq(&dev->power.lock);
	if (!dev->power.runtime_auto)
		goto out;

	dev->power.runtime_auto = false;
	atomic_inc(&dev->power.usage_count);
	rpm_resume(dev, 0);

 out:
	spin_unlock_irq(&dev->power.lock);
}
EXPORT_SYMBOL_GPL(pm_runtime_forbid);

其主要作用是阻止设备的运行时电源管理(runtime PM)。

struct device *dev 是函数的参数,表示要操作的设备。

spin_lock_irq(&dev->power.lock); 这行代码是获取设备电源管理的锁,防止并发操作。spin_lock_irq 是一个忙等待锁,如果锁已经被其他线程获取,那么调用线程会一直忙等待,直到获取到锁。irq 表示在获取锁的同时,禁止中断。

if (!dev->power.runtime_auto) goto out; 这行代码是检查设备的 runtime_auto 标志,如果该标志为 false,则直接跳转到 out 标签,释放锁。

dev->power.runtime_auto = false; 这行代码是将 runtime_auto 标志设置为 false,表示禁止设备的自动挂起。

atomic_inc(&dev->power.usage_count); 这行代码是原子地增加设备的使用计数。原子操作是一种不可中断的操作,保证了在多线程环境下的安全性。

rpm_resume(dev, 0); 这行代码是尝试恢复设备,即如果设备当前处于挂起状态,那么尝试唤醒设备。

spin_unlock_irq(&dev->power.lock); 这行代码是释放设备电源管理的锁,并允许中断。

EXPORT_SYMBOL_GPL(pm_runtime_forbid); 这行代码是将 pm_runtime_forbid 函数导出,使得其他的内核模块也可以调用这个函数。

总的来说,这个函数的作用是禁止设备的运行时电源管理,即使设备闲置,也不会自动挂起。

2.4 第24-32行:USB设备枚举过程中执行一些操作

第24-32行:在USB设备枚举过程中执行一些操作。

err = usb_enumerate_device(udev); 这行代码调用函数usb_enumerate_device,这个函数的作用是读取USB设备的描述符。这是USB设备枚举过程的一部分,其中设备将其能力和特性报告给主机。

if (err < 0) goto fail; 这行代码检查usb_enumerate_device函数的返回值。如果返回值小于0,表示函数执行失败,代码将跳转到fail标签处,这通常意味着出现错误处理。

dev_dbg(&udev->dev, “udev %d, busnum %d, minor = %d\n”, udev->devnum, udev->bus->busnum, (((udev->bus->busnum-1) * 128) + (udev->devnum-1))); 这行代码是用于调试的,它将USB设备的一些信息(设备编号,总线编号,次设备号)打印出来。

最后两行代码设置udev设备的设备类型和设备号。MKDEV宏用于创建一个设备号,这个设备号由主设备号(这里是USB_DEVICE_MAJOR)和次设备号组成。这里的次设备号是通过一个算式计算得到的,算式中使用了USB设备所在的总线编号和设备编号。这个设备号将用于创建设备节点,使得用户空间的程序(例如libusb)可以访问这个USB设备。

总的来说,这段代码的主要作用是在USB设备枚举过程中读取设备描述符,打印设备信息,并设置设备号,以便用户空间程序可以访问USB设备。

2.5 第35行:将USB设备的信息打印到日志中

第35行:announce_device函数的作用是将USB设备的信息打印到日志中。这通常用于调试和诊断问题,也有助于了解USB设备的状态和属性。

路径:drivers\usb\core\hub.c
static void announce_device(struct usb_device *udev)
{
	u16 bcdDevice = le16_to_cpu(udev->descriptor.bcdDevice);

	dev_info(&udev->dev,
		"New USB device found, idVendor=%04x, idProduct=%04x, bcdDevice=%2x.%02x\n",
		le16_to_cpu(udev->descriptor.idVendor),
		le16_to_cpu(udev->descriptor.idProduct),
		bcdDevice >> 8, bcdDevice & 0xff);
	dev_info(&udev->dev,
		"New USB device strings: Mfr=%d, Product=%d, SerialNumber=%d\n",
		udev->descriptor.iManufacturer,
		udev->descriptor.iProduct,
		udev->descriptor.iSerialNumber);
	show_string(udev, "Product", udev->product);
	show_string(udev, "Manufacturer", udev->manufacturer);
	show_string(udev, "SerialNumber", udev->serial);
}

2.6 第37-43行:USB设备枚举过程中执行一些操作

第37-43行:在USB设备枚举过程中执行一些操作。

这段代码的作用是将USB设备的一些属性(序列号、产品名称、制造商名称)添加到系统的熵池中。熵池是Linux内核用于生成随机数的一个数据结构。当需要生成一个随机数时,内核会从熵池中取出一些数据,并使用这些数据作为随机数生成算法的输入。

在这段代码中,add_device_randomness函数被用于将数据添加到熵池。这个函数的第一个参数是要添加的数据,第二个参数是数据的长度。这里的数据是USB设备的序列号、产品名称和制造商名称,这些数据都是字符串,所以使用strlen函数来获取它们的长度。

总的来说,这段代码的作用是增加系统的熵,以提高随机数的质量。这对于需要高质量随机数的应用(如加密应用)来说是非常重要的。

2.6.1 add_device_randomness()函数

路径:drivers\char\random.c
void add_device_randomness(const void *buf, size_t len)
{
	unsigned long entropy = random_get_entropy();
	unsigned long flags;

	spin_lock_irqsave(&input_pool.lock, flags);
	_mix_pool_bytes(&entropy, sizeof(entropy));
	_mix_pool_bytes(buf, len);
	spin_unlock_irqrestore(&input_pool.lock, flags);
}
EXPORT_SYMBOL(add_device_randomness);

用于向输入池添加设备或启动特定的数据,以帮助初始化输入池。

这段代码的工作流程如下:

调用 random_get_entropy 函数获取一些随机性(熵),并将其存储在 entropy 变量中。

使用 spin_lock_irqsave 函数锁定输入池,以防止其他进程在此过程中修改输入池。这个函数还会保存当前的中断状态,并在稍后恢复它。

调用 _mix_pool_bytes 函数两次,首先将 entropy 变量的内容混入输入池,然后将 buf 参数指向的数据混入输入池。

使用 spin_unlock_irqrestore 函数解锁输入池,并恢复之前保存的中断状态。

这段代码的主要作用是增加系统的随机性,以避免在大量相同的设备上,熵池具有相似的初始状态。需要注意的是,这里添加的数据并不增加熵池的熵,也就是说,它们不会增加熵池的随机性。这些数据只是用来改变熵池的初始状态,以防止在大量相同的设备上出现相同的初始状态。

2.7 第45行:启用设备的异步挂起

第45行:device_enable_async_suspend是一个内核函数,它的作用是启用设备的异步挂起(asynchronous suspend)功能。异步挂起是一种电源管理策略,它允许设备在系统空闲时进入低功耗状态,以节省电能。当设备被需要时,系统会自动将其唤醒。

在这段代码中,&udev->dev是一个指向USB设备结构体的指针。这个结构体包含了设备的各种信息,包括设备的类型、制造商、序列号等。调用device_enable_async_suspend函数后,这个USB设备就被设置为支持异步挂起。

总的来说,这段代码的作用是启用USB设备的电源管理功能,以节省系统的电能。

2.8 第48行:检查 USB 设备是否被标记为不可移除

第48行:用于检查 USB 设备是否被标记为不可移除。

set_usb_port_removable(udev); 这行代码调用了一个函数 set_usb_port_removable,该函数的参数是 udev,代表了一个 USB 设备。

set_usb_port_removable 函数的主要作用是设置 USB 设备的可移动性。具体来说,它会检查 USB 设备(或者固件)是否将这个端口标记为不可移动。如果是,那么在系统中,这个 USB 设备将被视为不可移动设备,即使物理上可以拔插。

这个函数的具体实现可能会依赖于具体的硬件和固件实现,可能会涉及到读取硬件寄存器或者固件设置等操作。

2.8.1 set_usb_port_removable()

static void set_usb_port_removable(struct usb_device *udev)
{
	struct usb_device *hdev = udev->parent;
	struct usb_hub *hub;
	u8 port = udev->portnum;
	u16 wHubCharacteristics;
	bool removable = true;

	dev_set_removable(&udev->dev, DEVICE_REMOVABLE_UNKNOWN);

	if (!hdev)
		return;

	hub = usb_hub_to_struct_hub(udev->parent);

	/*
	 * If the platform firmware has provided information about a port,
	 * use that to determine whether it's removable.
	 */
	switch (hub->ports[udev->portnum - 1]->connect_type) {
	case USB_PORT_CONNECT_TYPE_HOT_PLUG:
		dev_set_removable(&udev->dev, DEVICE_REMOVABLE);
		return;
	case USB_PORT_CONNECT_TYPE_HARD_WIRED:
	case USB_PORT_NOT_USED:
		dev_set_removable(&udev->dev, DEVICE_FIXED);
		return;
	default:
		break;
	}

	/*
	 * Otherwise, check whether the hub knows whether a port is removable
	 * or not
	 */
	wHubCharacteristics = le16_to_cpu(hub->descriptor->wHubCharacteristics);

	if (!(wHubCharacteristics & HUB_CHAR_COMPOUND))
		return;

	if (hub_is_superspeed(hdev)) {
		if (le16_to_cpu(hub->descriptor->u.ss.DeviceRemovable)
				& (1 << port))
			removable = false;
	} else {
		if (hub->descriptor->u.hs.DeviceRemovable[port / 8] & (1 << (port % 8)))
			removable = false;
	}

	if (removable)
		dev_set_removable(&udev->dev, DEVICE_REMOVABLE);
	else
		dev_set_removable(&udev->dev, DEVICE_FIXED);

}

这个函数的目的是设置USB设备的移除性。

函数首先获取USB设备的父设备和对应的USB集线器。然后,它设置设备的移除性为未知。

如果没有父设备,函数就会返回。否则,它会获取设备对应的USB集线器。

接着,函数会检查平台固件是否为该端口提供了信息。如果是,它会根据此信息来确定设备是否可移除。如果设备是热插拔类型,它会设置设备为可移除并返回。如果设备是硬连线类型或端口未使用,它会设置设备为固定并返回。

如果固件没有提供端口信息,函数会检查集线器是否知道端口是否可移除。它获取集线器的特性,并检查集线器是否复合。如果不是,函数会返回。

然后,函数会根据集线器的速度来确定设备是否可移除。对于超速集线器,如果设备可移除位图中的对应位被设置,它会设置设备为不可移除。对于高速集线器,如果设备可移除位图的对应位被设置,它也会设置设备为不可移除。

最后,函数会根据设备是否可移除来设置设备的移除性。如果设备可移除,它会设置设备为可移除。否则,它会设置设备为固定。

总的来说,这个函数的作用是根据设备和集线器的信息来设置USB设备的移除性。

2.9 第54-58行:注册 USB 设备

第54-58行:主要负责注册 USB 设备。

首先,它调用 device_add(&udev->dev) 函数试图将 USB 设备(由 udev 指针表示)注册到系统中。device_add 函数是设备驱动模型(device driver model)的一部分,它将设备添加到系统中并初始化设备所需的所有数据结构。

如果 device_add 函数成功执行,那么 USB 设备就被成功注册到系统中,设备驱动程序负责配置设备并调用添加设备通知链(被 usbfs 和可能的其他一些模块使用)。

如果 device_add 函数执行失败,它会返回一个错误代码。这个错误代码被存储在 err 变量中。然后,dev_err(&udev->dev, “can’t device_add, error %d\n”, err) 语句会将错误信息打印到日志中,其中 %d 是一个占位符,它会被 err 的值替换,表示具体的错误代码。最后,goto fail 语句会使程序跳转到 fail 标签所在的位置,执行错误处理的代码。

2.10 第61-80行: sysfs 文件系统中创建链接

第61-80行:主要负责在 sysfs 文件系统中创建链接,以表示 USB 设备(udev)和其父设备(一个 USB 集线器)之间的关系。

首先,它检查 udev 是否有父设备。如果没有,那么这段代码就不会执行。

如果 udev 有父设备,那么它首先获取这个父设备对应的 USB 集线器(hub)。然后,它获取 udev 所在的端口号(port1)。接着,它获取这个端口对应的 USB 端口设备(port_dev)。

接下来,它试图在 sysfs 中创建一个从 udev 到 port_dev 的链接,链接名为 “port”。如果创建失败,它会跳转到 fail 标签执行错误处理。

如果上一步成功,它会继续尝试在 sysfs 中创建一个从 port_dev 到 udev 的链接,链接名为 “device”。如果创建失败,它会首先删除刚刚创建的 “port” 链接,然后跳转到 fail 标签执行错误处理。

最后,它会检查 port1 是否在 hub 的 child_usage_bits 中被设置。如果没有被设置,那么它会设置这个位,并调用 pm_runtime_get_sync 函数增加 port_dev 的运行时 PM 引用计数。这是为了防止 port_dev 在 udev 还在使用它时被系统挂起或关闭。

总的来说,这段代码的作用是在 sysfs 中表示 USB 设备和其父设备(一个 USB 集线器)之间的关系,并在需要时防止父设备被系统挂起或关闭。

2.11 第82-85行:创建 USB 设备的端点设备,并处理设备的电源管理

第82-85行:主要负责创建 USB 设备的端点设备,并处理设备的电源管理。

首先,它调用 usb_create_ep_devs(&udev->dev, &udev->ep0, udev); 函数,该函数的目的是在 sysfs 文件系统中为 USB 设备的每个端点创建设备节点。这样,用户空间的程序就可以通过这些设备节点来访问 USB 设备的端点。在这里,&udev->dev 是 USB 设备的设备结构体,&udev->ep0 是 USB 设备的端点0的端点结构体,udev 是 USB 设备的设备结构体。

然后,它调用 usb_mark_last_busy(udev); 函数,该函数的目的是标记 USB 设备的最后一次活动时间。这个时间戳用于设备的电源管理,例如,如果设备在一段时间内没有活动,系统可能会将其挂起以节省电源。

接着,它调用 pm_runtime_put_sync_autosuspend(&udev->dev); 函数,该函数的目的是减少设备的运行时电源管理引用计数,并可能触发设备的自动挂起。在这里,&udev->dev 是 USB 设备的设备结构体。

最后,它返回 err,这是一个错误代码。如果前面的操作都成功,err 应该是 0。如果有任何操作失败,err 将是一个错误代码。

总的来说,这段代码的作用是创建 USB 设备的端点设备,并处理设备的电源管理。

2.12 第87-91行:处理 USB 设备连接失败

第87-91行:用于处理 USB 设备连接失败的情况。

首先,usb_set_device_state(udev, USB_STATE_NOTATTACHED); 这行代码将 USB 设备的状态设置为 USB_STATE_NOTATTACHED,表示设备没有被连接到系统。

接着,pm_runtime_disable(&udev->dev); 这行代码调用电源管理(Power Management,简称 PM)函数,禁用设备的运行时电源管理。这意味着设备将不会在运行时进入低功耗状态。

然后,pm_runtime_set_suspended(&udev->dev); 这行代码将设备的运行时 PM 状态设置为挂起(suspended)。这通常意味着设备已被关闭或者不再活动。

最后,return err; 这行代码返回一个错误代码,表示设备连接失败。

总的来说,这段代码的作用是在 USB 设备连接失败时,将设备状态设置为未连接,并禁用设备的运行时电源管理。

三、总结

usb_new_device()主要是将USB设备添加到系统中,通过device_add()添加到系统中,该函数会匹配对于USB的驱动函数 进一步调用设备驱动的probe函数,后续就是设备驱动的内容了。

USB插入的整体流程大部是在hub驱动中完成的。目前hub部分的内容基本分析完成。

后面会继续分析 usb_control_msg_send()等函数 与USB设备的通信过程,以及以U盘插入后匹配storage 驱动的内容等等。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

waterfxw

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

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

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

打赏作者

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

抵扣说明:

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

余额充值