RK3568驱动指南|第七篇-设备树-第68章 ranges属性实验

瑞芯微RK3568芯片是一款定位中高端的通用型SOC,采用22nm制程工艺,搭载一颗四核Cortex-A55处理器和Mali G52 2EE 图形处理器。RK3568 支持4K 解码和 1080P 编码,支持SATA/PCIE/USB3.0 外围接口。RK3568内置独立NPU,可用于轻量级人工智能应用。RK3568 支持安卓 11 和 linux 系统,主要面向物联网网关、NVR 存储、工控平板、工业检测、工控盒、卡拉 OK、云终端、车载中控等行业。


【公众号】迅为电子

【粉丝群】824412014(加群获取驱动文档+例程)

【视频观看】嵌入式学习之Linux驱动(第七期_设备树_全新升级)_基于RK3568

【购买链接】迅为RK3568开发板瑞芯微Linux安卓鸿蒙ARM核心板人工智能AI主板


第68章 ranges属性实验

68.1 platform_get_resource 获取设备树资源

在上个章节中讲解了使用of操作函数来获取设备树的属性,由于设备树在系统启动的时候都会转化为platform设备,那我们能不能直接在驱动中使用在53.1小节中讲解的platform_get_resource函数直接获取platform_device资源呢?

68.1.1 驱动程序编写

带着疑惑我们这里仍旧以65章的驱动程序为原型,在probe函数中加入使用platform_get_resource函数获取reg资源的函数,添加完成的驱动程序内容如下所示:

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/mod_devicetable.h>
// 平台设备的初始化函数
struct resource *myresources;
static int my_platform_probe(struct platform_device *pdev)
{
    printk(KERN_INFO "my_platform_probe: Probing platform device\n");

    // 获取平台设备的资源
    myresources = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    if (myresources == NULL) {
        // 如果获取资源失败,打印value_compatible的值
        printk("platform_get_resource is error\n");
}
printk("reg valus is %llx\n" , myresources->start); 

    return 0;
}

// 平台设备的移除函数
static int my_platform_remove(struct platform_device *pdev)
{
    printk(KERN_INFO "my_platform_remove: Removing platform device\n");

    // 清理设备特定的操作
    // ...

    return 0;
}


const struct of_device_id of_match_table_id[]  = {
	{.compatible="my devicetree"},
};

// 定义平台驱动结构体
static struct platform_driver my_platform_driver = {
    .probe = my_platform_probe,
    .remove = my_platform_remove,
    .driver = {
        .name = "my_platform_device",
        .owner = THIS_MODULE,
		.of_match_table =  of_match_table_id,
    },
};

// 模块初始化函数
static int __init my_platform_driver_init(void)
{
    int ret;

    // 注册平台驱动
    ret = platform_driver_register(&my_platform_driver);
    if (ret) {
        printk(KERN_ERR "Failed to register platform driver\n");
        return ret;
    }

    printk(KERN_INFO "my_platform_driver: Platform driver initialized\n");

    return 0;
}

// 模块退出函数
static void __exit my_platform_driver_exit(void)
{
    // 注销平台驱动
    platform_driver_unregister(&my_platform_driver);

    printk(KERN_INFO "my_platform_driver: Platform driver exited\n");
}

module_init(my_platform_driver_init);
module_exit(my_platform_driver_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("topeet");

编译成模块之后,放到开发板上进行加载,打印信息如下(图 68-1)所示:

图 218-1

可以看到使用platform_get_resource函数获取reg资源的函数失败了,在下一个小节中将分析获取资源失败的原因。

68.1.2 获取资源失败源码分析

platform_get_resource定义在内核源码目录下的"/drivers/base/platform.c"目录下,具体内容如下所示:

struct resource *platform_get_resource(struct platform_device *dev,
				       unsigned int type, unsigned int num)
{
	u32 i;

	for (i = 0; i < dev->num_resources; i++) {
		struct resource *r = &dev->resource[i];

		if (type == resource_type(r) && num-- == 0)
			return r;
	}
	return NULL;
}

该函数返回NULL符合第一小节中的情况,返回NULL的情况有两种可能性,一种是没进入上面的for循环直接返回了NULL,另外一种是进入了for循环,但是类型匹配不正确,跳出for循环之后再返回NULL。这里的类型一定是匹配的,所以我们就来寻找为什么没有进入for循环,这里只有一种可能,也就是dev->num_resources为0。

所以现在的目标来到了寻找dev->num_resources是在哪里进行的赋值,前面已经讲解过了由设备树转换为platform的过程,而且在系统启动后,在对应目录下也有了相应的节点:

图 68-2

证明转换是没问题的,所以继续寻找中间转换过程中有关资源数量的相关函数,定位到了of_platform_device_create_pdata函数,该函数定义在内核源码目录下的“drivers/of/platform.c”文件中,具体内容如下所示:

static struct platform_device *of_platform_device_create_pdata(
					struct device_node *np,
					const char *bus_id,
					void *platform_data,
					struct device *parent)
{
	struct platform_device *dev;

	/* 检查设备节点是否可用或已填充 */
	if (!of_device_is_available(np) ||
	    of_node_test_and_set_flag(np, OF_POPULATED))
		return NULL;

	/* 分配平台设备结构体 */
	dev = of_device_alloc(np, bus_id, parent);
	if (!dev)
		goto err_clear_flag;

	/* 设置平台设备的一些属性 */
	dev->dev.coherent_dma_mask = DMA_BIT_MASK(32);
	if (!dev->dev.dma_mask)
		dev->dev.dma_mask = &dev->dev.coherent_dma_mask;
	dev->dev.bus = &platform_bus_type;
	dev->dev.platform_data = platform_data;
	of_msi_configure(&dev->dev, dev->dev.of_node);
	of_reserved_mem_device_init_by_idx(&dev->dev, dev->dev.of_node, 0);

	/* 将平台设备添加到设备模型中 */
	if (of_device_add(dev) != 0) {
		platform_device_put(dev);
		goto err_clear_flag;
	}

	return dev;

err_clear_flag:
	/* 清除设备节点的已填充标志 */
	of_node_clear_flag(np, OF_POPULATED);	return NULL;
}

第15行:函数调用 of_device_alloc 分配一个平台设备结构体,并将设备节点指针、设备标识符和父设备指针传递给它,正是该函数决定的resource.num,然后找到该函数的定义,如下所示:

struct platform_device *of_device_alloc(struct device_node *np,
				  const char *bus_id,
				  struct device *parent)
{
	struct platform_device *dev;
	int rc, i, num_reg = 0, num_irq;
	struct resource *res, temp_res;

	dev = platform_device_alloc("", PLATFORM_DEVID_NONE);
	if (!dev)
		return NULL;

	/* count the io and irq resources */
	while (of_address_to_resource(np, num_reg, &temp_res) == 0)
		num_reg++;
	num_irq = of_irq_count(np);

	/* Populate the resource table */
	if (num_irq || num_reg) {
		res = kcalloc(num_irq + num_reg, sizeof(*res), GFP_KERNEL);
		if (!res) {
			platform_device_put(dev);
			return NULL;
		}

		dev->num_resources = num_reg + num_irq;
		dev->resource = res;
		for (i = 0; i < num_reg; i++, res++) {
			rc = of_address_to_resource(np, i, res);
			WARN_ON(rc);
		}
		if (of_irq_to_resource_table(np, res, num_irq) != num_irq)
			pr_debug("not all legacy IRQ resources mapped for %pOFn\n",
				 np);
	}

	dev->dev.of_node = of_node_get(np);
	dev->dev.fwnode = &np->fwnode;
	dev->dev.parent = parent ? : &platform_bus;

	if (bus_id)
		dev_set_name(&dev->dev, "%s", bus_id);
	else
		of_device_make_bus_id(&dev->dev);

	return dev;
}

在第26行出现了for循环的dev->num_resources = num_reg + num_irq;reg的number和irq的number,由于在设备树中并没有添加中断相关的属性num_irq为0,那这里的num_reg是哪里确定的呢。

我们向上找到14、15行,具体内容如下所示:

	/* count the io and irq resources */
	while (of_address_to_resource(np, num_reg, &temp_res) == 0)
		num_reg++;

然后跳转到while循环中的of_address_to_resource函数,该函数定义在内核源码目录的drivers/of/address.c文件中,具体内容如下所示:

int of_address_to_resource(struct device_node *dev, int index,
			   struct resource *r)
{
	const __be32	*addrp;
	u64		size;
	unsigned int	flags;
	const char	*name = NULL;

	addrp = of_get_address(dev, index, &size, &flags);
	if (addrp == NULL)
		return -EINVAL;

	/* Get optional "reg-names" property to add a name to a resource */
	of_property_read_string_index(dev, "reg-names",	index, &name);

	return __of_address_to_resource(dev, addrp, size, flags, name, r);
}

第9行,获取reg属性的地址、大小和类型,在设备树中reg属性已经存在了,所以这里会正确返回。

第14行,读取reg-names属性,由于设备树中没有定义这个属性,所以该函数不会有影响。

最后具有决定性作用的函数就是返回的__of_address_to_resource函数了,跳转到该函数的定义如下所示:

static int __of_address_to_resource(struct device_node *dev,
		const __be32 *addrp, u64 size, unsigned int flags,
		const char *name, struct resource *r)
{
	u64 taddr;

	if (flags & IORESOURCE_MEM)
		taddr = of_translate_address(dev, addrp);
	else if (flags & IORESOURCE_IO)
		taddr = of_translate_ioport(dev, addrp, size);
	else
		return -EINVAL;

	if (taddr == OF_BAD_ADDR)
		return -EINVAL;
	memset(r, 0, sizeof(struct resource));

	r->start = taddr;
	r->end = taddr + size - 1;
	r->flags = flags;
	r->name = name ? name : dev->full_name;

	return 0;
}

reg属性的flags为IORESOURCE_MEM,所以又会执行第9行的of_translate_address函数,跳转到该函数,该函数的定义如下所示:

u64 of_translate_address(struct device_node *dev, const __be32 *in_addr)
{
	struct device_node *host;
	u64 ret;

	ret = __of_translate_address(dev, in_addr, "ranges", &host);
	if (host) {
		of_node_put(host);
		return OF_BAD_ADDR;
	}

	return ret;
}

该函数的重点在第6行,上述函数实际上是__of_translate_address函数的封装,其中传入的第三个参数“ranges”是我们要关注的重点,继续跳转到该函数的定义,具体内容如下所示:

static u64 __of_translate_address(struct device_node *dev,
				  const __be32 *in_addr, const char *rprop,
				  struct device_node **host)
{
	struct device_node *parent = NULL;
	struct of_bus *bus, *pbus;
	__be32 addr[OF_MAX_ADDR_CELLS];
	int na, ns, pna, pns;
	u64 result = OF_BAD_ADDR;

	pr_debug("** translation for device %pOF **\n", dev);

	/* Increase refcount at current level */
	of_node_get(dev);

	*host = NULL;
	/* Get parent & match bus type */
	parent = of_get_parent(dev);
	if (parent == NULL)
		goto bail;
	bus = of_match_bus(parent);

	/* Count address cells & copy address locally */
	bus->count_cells(dev, &na, &ns);
	if (!OF_CHECK_COUNTS(na, ns)) {
		pr_debug("Bad cell count for %pOF\n", dev);
		goto bail;
	}
	memcpy(addr, in_addr, na * 4);

	pr_debug("bus is %s (na=%d, ns=%d) on %pOF\n",
	    bus->name, na, ns, parent);
	of_dump_addr("translating address:", addr, na);

	/* Translate */
	for (;;) {
		struct logic_pio_hwaddr *iorange;

		/* Switch to parent bus */
		of_node_put(dev);
		dev = parent;
		parent = of_get_parent(dev);

		/* If root, we have finished */
		if (parent == NULL) {
			pr_debug("reached root node\n");
			result = of_read_number(addr, na);
			break;
		}

		/*
		 * For indirectIO device which has no ranges property, get
		 * the address from reg directly.
		 */
		iorange = find_io_range_by_fwnode(&dev->fwnode);
		if (iorange && (iorange->flags != LOGIC_PIO_CPU_MMIO)) {
			result = of_read_number(addr + 1, na - 1);
			pr_debug("indirectIO matched(%pOF) 0x%llx\n",
				 dev, result);
			*host = of_node_get(dev);
			break;
		}

		/* Get new parent bus and counts */
		pbus = of_match_bus(parent);
		pbus->count_cells(dev, &pna, &pns);
		if (!OF_CHECK_COUNTS(pna, pns)) {
			pr_err("Bad cell count for %pOF\n", dev);
			break;
		}

		pr_debug("parent bus is %s (na=%d, ns=%d) on %pOF\n",
		    pbus->name, pna, pns, parent);

		/* Apply bus translation */
		if (of_translate_one(dev, bus, pbus, addr, na, ns, pna, rprop))
			break;

		/* Complete the move up one level */
		na = pna;
		ns = pns;
		bus = pbus;

		of_dump_addr("one level translation:", addr, na);
	}
 bail:
	of_node_put(parent);
	of_node_put(dev);

	return result;
}

第18行,获取父节点和匹配的总线类型

第24行,获取address-cell和size-cells

然后是一个for循环,在76行使用of_translate_one函数进行转换,其中rprop参数表示要转换的资源属性,该参数的值为传入的“ranges”,然后我们继续跳转到该函数,该函数的具体内容如下所示:

static int of_translate_one(struct device_node *parent, struct of_bus *bus,
			    struct of_bus *pbus, __be32 *addr,
			    int na, int ns, int pna, const char *rprop)
{
	const __be32 *ranges;
	unsigned int rlen;
	int rone;
	u64 offset = OF_BAD_ADDR;

	/*
	 * Normally, an absence of a "ranges" property means we are
	 * crossing a non-translatable boundary, and thus the addresses
	 * below the current cannot be converted to CPU physical ones.
	 * Unfortunately, while this is very clear in the spec, it's not
	 * what Apple understood, and they do have things like /uni-n or
	 * /ht nodes with no "ranges" property and a lot of perfectly
	 * useable mapped devices below them. Thus we treat the absence of
	 * "ranges" as equivalent to an empty "ranges" property which means
	 * a 1:1 translation at that level. It's up to the caller not to try
	 * to translate addresses that aren't supposed to be translated in
	 * the first place. --BenH.
	 *
	 * As far as we know, this damage only exists on Apple machines, so
	 * This code is only enabled on powerpc. --gcl
	 */
	ranges = of_get_property(parent, rprop, &rlen);
	if (ranges == NULL && !of_empty_ranges_quirk(parent)) {
		pr_debug("no ranges; cannot translate\n");
		return 1;
	}
	if (ranges == NULL || rlen == 0) {
		offset = of_read_number(addr, na);
		memset(addr, 0, pna * 4);
		pr_debug("empty ranges; 1:1 translation\n");
		goto finish;
	}

	pr_debug("walking ranges...\n");

	/* Now walk through the ranges */
	rlen /= 4;
	rone = na + pna + ns;
	for (; rlen >= rone; rlen -= rone, ranges += rone) {
		offset = bus->map(addr, ranges, na, ns, pna);
		if (offset != OF_BAD_ADDR)
			break;
	}
	if (offset == OF_BAD_ADDR) {
		pr_debug("not found !\n");
		return 1;
	}
	memcpy(addr, ranges + na, 4 * pna);

 finish:
	of_dump_addr("parent translation for:", addr, pna);
	pr_debug("with offset: %llx\n", (unsigned long long)offset);

	/* Translate it into parent bus space */
	return pbus->translate(addr, offset, pna);
}

在该函数的第26行使用of_get_property函数获取“ranges”属性,但由于在我们添加的设备树节点中并没有该属性,所以这里的ranges值就为NULL,第27行的条件判断成立,也就会返回1。

接下来再根据这个返回值继续分析上级函数:

of_translate_one函数返回1之后,上一级的_of_translate_address的返回值就为OF BAD ADDR,再上一级的of_translate_address返回值也是OF BAD _ADDR,继续向上查找_of_address_to_resource函数会返回EINVAL,of address_ to resource 返回EINVAL,所以num_reg 为0;到这里关于为什么platform_get_resource函数获取资源失败的问题就找到了,只是因为在设备树中并没有这个名为ranges这个属性,所以只需要对设备树进行ranges属性的添加即可,要修改的设备树为arch/arm64/boot/dts/rockchip/rk3568-evb1-ddr4-v10-linux.dts,修改完成如下(图 68-3)所示:

图 228-3

然后重新编译内核,将编译生成的boot.img烧写进开发板之后重新加载上面编写的驱动程序,可以看到之前获取失败的打印就消失了,而且成功打印出了reg属性的第一个值,如下图(图 68-4)所示:

图 238-4

虽然这里的问题解决了,但引起的思考并没有结束,那我们在这里添加的ranges属性的作用是啥呢,带着疑问,开始下一小节的学习吧。

68.2 ranges属性

68.2.1 ranges属性介绍

ranges 属性是一种用于描述设备之间地址映射关系的属性。它在设备树(Device Tree)中使用,用于描述子设备地址空间如何映射到父设备地址空间。设备树是一种硬件描述语言,用于描述嵌入式系统中的硬件组件和它们之间的连接关系。

设备树中的每个设备节点都可以具有 ranges 属性,其中包含了地址映射的信息。下面是一个常见的格式:

ranges = <child-bus-address parent-bus-address length>;

 或者

ranges;

然后对上述格式中每个部分进行解释:

child-bus-address:子设备地址空间的起始地址。它指定了子设备在父设备地址空间中的位置。具体的字长由 ranges 所在节点的 #address-cells 属性决定。

parent-bus-address:父设备地址空间的起始地址。它指定了父设备中用于映射子设备的地址范围。具体的字长由 ranges 的父节点的 #address-cells 属性决定。

length:映射的大小。它指定了子设备地址空间在父设备地址空间中的长度。具体的字长由 ranges 的父节点的 #size-cells 属性决定。

当 ranges 属性的值为空时,表示子设备地址空间和父设备地址空间具有完全相同的映射,即1:1映射。这通常用于描述内存区域,其中子设备和父设备具有相同的地址范围。

当 ranges 属性的值不为空时,按照指定的映射规则将子设备地址空间映射到父设备地址空间。具体的映射规则取决于设备树的结构和设备的特定要求。

然后以下面的设备树为例进行ranges属性的讲解,设备树内容如下所示:

/dts-v1/;

/ {
  compatible = "acme,coyotes-revenge";
  #address-cells = <1>;
  #size-cells = <1>;
  ....
  external-bus {
    #address-cells = <2>;
    #size-cells = <1>;
    ranges = <0 0 0x10100000 0x10000
              1 0 0x10160000 0x10000
              2 0 0x30000000 0x30000000>;
    // Chipselect 1, Ethernet
    // Chipselect 2, i2c controller
    // Chipselect 3, NOR Flash
.......

这里以ranges的第一个属性值为例进行具体解释如下:

在 external-bus 节点中#address-cells 属性值为2表示child-bus-address由两个值表示,也就是0和0,父节点的 #address-cells 属性值和#size-cells 属性值为1,表示parent-bus-address和length都由一个表示,也就是0x10100000和0x10000,该ranges值表示将子地址空间(0x0-0xFFFF)映射到父地址空间0x10100000 - 0x1010FFFF,这里的例子为带参数ranges属性映射,不带参数的ranges属性为1:1映射,较为简单,这里不再进行举例。

在嵌入式系统中,不同的设备可能连接到相同的总线或总线控制器上,它们需要在物理地址空间中进行正确的映射,以便进行数据交换和通信。例如,一个设备可能通过总线连接到主处理器或其他设备,而这些设备的物理地址范围可能不同。ranges 属性就是用来描述这种地址映射关系的。

68.2.2设备分类

根据上面讲解的映射关系可以将设备分为两类:内存映射型设备和非内存映射型设备。

1内存映射型设备:
内存映射型设备是指可以通过内存地址进行直接访问的设备。这类设备在物理地址空间中的一部分被映射到系统的内存地址空间中,使得CPU可以通过读写内存地址的方式与设备进行通信和控制。

特点:

(1)直接访问:内存映射型设备可以被CPU直接访问,类似于访问内存中的数据。这种直接访问方式提供了高速的数据传输和低延迟的设备操作。

(2)内存映射:设备的寄存器、缓冲区等资源被映射到系统的内存地址空间中,使用读写内存的方式与设备进行通信。

(3)读写操作:CPU可以通过读取和写入映射的内存地址来与设备进行数据交换和控制操作。

在设备树中,内存映射型设备的设备树举例如下所示:

/dts-v1/;
/ {
    #address-cells = <1>;
    #size-cells = <1>;
    ranges;

    serial@101f0000 {
        compatible = "arm,pl011";
        reg = <0x101f0000 0x1000>;
    };

    gpio@101f3000 {
        compatible = "arm,pl061";
        reg = <0x101f3000 0x1000
                0x101f4000 0x10>;
    };

    spi@10115000 {
        compatible = "arm,pl022";
        reg = <0x10115000 0x1000>;
    };
};

第5行的ranges属性表示该设备树中会进行1:1的地址范围映射。

2非内存映射型设备:
非内存映射型设备是指不能通过内存地址直接访问的设备。这类设备可能采用其他方式与CPU进行通信,例如通过I/O端口、专用总线或特定的通信协议。

特点:

(1)非内存访问:非内存映射型设备不能像内存映射型设备那样直接通过内存地址进行访问。它们可能使用独立的I/O端口或专用总线进行通信。

(2)特定接口:设备通常使用特定的接口和协议与CPU进行通信和控制,例如SPI、I2C、UART等。

(3)驱动程序:非内存映射型设备通常需要特定的设备驱动程序来实现与CPU的通信和控制。

在设备树中,非内存映射型设备的设备树举例如下所示:

/dts-v1/;

/ {
  compatible = "acme,coyotes-revenge";
  #address-cells = <1>;
  #size-cells = <1>;
  ....
  external-bus {
    #address-cells = <2>;
    #size-cells = <1>;
    ranges = <0 0 0x10100000 0x10000
              1 0 0x10160000 0x10000
              2 0 0x30000000 0x30000000>;
    // Chipselect 1, Ethernet
    // Chipselect 2, i2c controller
    // Chipselect 3, NOR Flash

    ethernet@0,0 {
      compatible = "smc,smc91c111";
      reg = <0 0 0x1000>;
    };

    i2c@1,0 {
      compatible = "acme,a1234-i2c-bus";
      #address-cells = <1>;
      #size-cells = <0>;
      reg = <1 0 0x1000>;

      rtc@58 {
        compatible = "maxim,ds1338";
        reg = <0x58>;
      };
    };
  } ;
}; 

68.2.3 映射地址计算

接下来以上面列举的非内存映射型设备的设备树中的ethernet@0节点为例,计算该网卡设备的映射地址。

首先,找到ethernet@0所在的节点,并查看其reg属性。在给定的设备树片段中,ethernet@0的reg属性为<0 0 0x1000>。在根节点中,#address-cells的值为1,表示地址由一个单元格组成。

接下来,根据ranges属性进行地址映射计算。在external-bus节点的ranges属性中,有三个映射条目:

第一个映射条目为“0 0 0x10100000 0x10000”,表示外部总线的地址范围为0x10100000到0x1010FFFF。该映射条目的第一个值为0,表示与external-bus节点的第一个子节点(ethernet@0,0)相关联。

第二个映射条目:“1 0 0x10160000 0x10000”,表示外部总线的地址范围为0x10160000到0x1016FFFF。该映射条目的第一个值为1,表示与external-bus节点的第二个子节点(i2c@1,0)相关联。

第三个映射条目:“2 0 0x30000000 0x30000000”,表示外部总线的地址范围为0x30000000到0x5FFFFFFF。该映射条目的第一个值为2,表示与external-bus节点的第三个子节点相关联。

由于ethernet@0与external-bus的第一个子节点相关联,并且它的reg属性为<0 0 0x1000>,我们可以进行以下计算:

ethernet@0的物理地址 = 外部总线地址起始值 + ethernet@0的reg属性的第二个值
= 0x10100000 + 0x1000
= 0x10101000

因此,ethernet@0的物理起始地址为0x10101000,又根据0x1000的地址范围可以确定ethernet@0的结束起始地址为0x10101FFF,至此,关于映射地址的计算就讲解完成了,大家可以根据同样的方法计算i2c@1的物理地址。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值