007——存储设备(基于liteos-a)

目录

存储设备驱动程序分析

1.1 字符设备和块设备

1.1.1 APP与驱动程序的交互

1. 字符设备驱动程序

2. 块设备驱动程序教

1.1.2 驱动程序结构体

1.1.3 注册函数

1. 字符设备驱动程序注册函数

2. 块设备驱动程序注册函数

1.2 MTD设备

1.3 块设备驱动程序为MTD开了一个后门

怎么用内存模拟Flash

2.1 指定要使用的内存地址、大小

2.2 实现MtdDev结构体

2.3 怎么使用块设备

2.4 移植过程


存储设备驱动程序分析

参考资料:vendor\democom\demochip\driver\mtd\spi_nor\src\common\spinor.c

我们的目录是samsung下的exynos4412不过内容都是这个内容下面需要源码的没给出也是这个

/*
 * Copyright (c) 2013-2019, Huawei Technologies Co., Ltd. All rights reserved.
 * Copyright (c) 2020, Huawei Device Co., Ltd. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, this list of
 *    conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice, this list
 *    of conditions and the following disclaimer in the documentation and/or other materials
 *    provided with the distribution.
 *
 * 3. Neither the name of the copyright holder nor the names of its contributors may be used
 *    to endorse or promote products derived from this software without specific prior written
 *    permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "errno.h"
#include "fs/fs.h"
#include "stdio.h"
#include "stdlib.h"
#include "spinor.h"
#include "mtd_common.h"
#include "spinor_common.h"
#include "mtd_dev.h"

struct MtdDev spinor_mtd;
void AddMtdList(char *type, struct MtdDev *mtd);

extern int get_mtd_info(const char *type);

void* GetMtd(const char *type)
{
	(void)type;
	return &spinor_mtd;
}

static int ramnor_erase(struct MtdDev *mtd, UINT64 start, UINT64 len, UINT64 *failAddr)
{
	unsigned char * rambase = (unsigned char *)mtd->priv;

    uint32_t offset = start;
    uint32_t length = len;

    if (offset + length > mtd->size) {
        return -EINVAL;
    }

    if (offset & (mtd->eraseSize - 1)) {
        return -EINVAL;
    }

    if (length & (mtd->eraseSize - 1)) {
        return -EINVAL;
    }

	memset((void *)(rambase+offset), 0xff, length);
    return 0;
}

static int ramnor_write(struct MtdDev *mtd, UINT64 start, UINT64 len, const char *buf)
{
	unsigned char * rambase = (unsigned char *)mtd->priv;
    uint32_t offset = start;
    uint32_t length = len;

    if ((offset + length) > mtd->size) {
        return -EINVAL;
    }

    if (!length) {
        return 0;
    }

    memcpy((void *)(rambase+offset), buf, length);
	return len;
}

static int ramnor_read(struct MtdDev *mtd, UINT64 start, UINT64 len, char *buf)
{
	unsigned char * rambase = (unsigned char *)mtd->priv;
    uint32_t offset = start;
    uint32_t length = len;
	//int i;

    if ((offset + length) > mtd->size) {
		PRINT_RELEASE("%s %s %d, memcpy: 0x%x, 0x%x, 0x%x\n", __FILE__, __FUNCTION__, __LINE__, (unsigned int)buf, (unsigned int)(rambase+start), (unsigned int)len); 	
        return -EINVAL;
    }

    if (!length) {
		PRINT_RELEASE("%s %s %d, memcpy: 0x%x, 0x%x, 0x%x\n", __FILE__, __FUNCTION__, __LINE__, (unsigned int)buf, (unsigned int)(rambase+start), (unsigned int)len); 	
        return 0;
    }

	//PRINT_RELEASE("%s %s %d, memcpy: 0x%x, 0x%x, 0x%x\n", __FILE__, __FUNCTION__, __LINE__, (unsigned int)buf, (unsigned int)(rambase+start), (unsigned int)len);		

    //return spinor->read(spinor, (uint32_t)from, (uint32_t)len, buf);
    memcpy(buf, (void *)(rambase+offset), length);

	return len;
}


void ramnor_register(struct MtdDev *mtd)
{
    //mtd->priv = (void *)DDR_RAMFS_VBASE;

    //mtd->size = DDR_RAMFS_SIZE;
    mtd->eraseSize = 0x10000;

    mtd->type = MTD_NORFLASH;

    mtd->erase = ramnor_erase;
    mtd->read = ramnor_read;
    mtd->write = ramnor_write;

}

/*---------------------------------------------------------------------------*/
/* spinor_node_register- spinor node register */
/*---------------------------------------------------------------------------*/
int spinor_node_register(struct MtdDev *mtd)
{
    int ret = 0;
    ret = register_blockdriver("/dev/spinor", &g_dev_spinor_ops, 0755, mtd);
    if (ret) {
        ERR_MSG("register spinor err %d!\n", ret);
    }

    return ret;
}

int spinor_init(void)
{
/*xin.TODO*/
#if 0
    spinor_mtd.priv = (void *)DDR_RAMFS_VBASE;
	spinor_mtd.size = DDR_RAMFS_SIZE;
#else
    spinor_mtd.priv = (void *)0;
	spinor_mtd.size = 0;
#endif 
    /* ramnor register */
    ramnor_register(&spinor_mtd);
	PRINT_RELEASE("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);		
//    AddMtdList("spinor", &spinor_mtd);
    if (spinor_node_register(&spinor_mtd)) {
        PRINT_RELEASE("spinor node register fail!\n");
        return -1;
    }
    return get_mtd_info("spinor") ;
}

1.1 字符设备和块设备

Linux中设备驱动程序分为3类:字符设备、块设备、网络设备。 所谓字符设备就是LED、按键、LCD、触摸屏这些非存储设备,APP可以直接调用驱动函数去操作它们。 而块设备就是Flash、磁盘这些存储设备,APP读写普通的文件时,最终会由驱动程序访问硬件。 为什么叫块设备?以前的磁盘读写时,是以块为单位的:即使只是读写一个字节,也需要读写一个块。 主要差别在于:

  • 字符设备驱动程序里,可以读写任意长度的数据

  • 块设备驱动程序里,读写数据时以块(扇区)为单位

1.1.1 APP与驱动程序的交互

1. 字符设备驱动程序

        这个是liteos的字符设备驱动模型 

2. 块设备驱动程序教

 这个是liteos的块设备驱动模型 

1.1.2 驱动程序结构体

从上面的图形可以看到,无论是字符设备还是块设备,都要提供open/read/write/ioctl这些函数。 它们的驱动程序核心是类似的:

  • 字符设备驱动程序:file_operations_vfs

  • 块设备驱动程序:block_operations

1.1.3 注册函数

1. 字符设备驱动程序注册函数
int register_driver(FAR const char *path, FAR const struct file_operations_vfs *fops, mode_t mode, FAR void *priv);

示例:

int ret = register_driver("/dev/hello", &g_helloDevOps, 0666, NULL);

2. 块设备驱动程序注册函数
int register_blockdriver(FAR const char *path,
                         FAR const struct block_operations *bops,
                         mode_t mode, FAR void *priv);

示例:

int ret = register_blockdriver("/dev/spinor", &g_dev_spinor_ops, 0755, mtd);

1.2 MTD设备

在各类电子产品中,存储设备类型多种多样,比如Nor Flash、Nand Flash,这些Flash又有不同的接口:比如SPI接口等等。 这些不同Flash的访问方法各有不同,但是肯定有这三种操作:

  • 擦除

那么可以抽象出一个软件层:MTD,含义为Memory Technology Device,它封装了不同Flash的操作。主要是抽象出一个结构体:

struct MtdDev {
    VOID *priv;
    UINT32 type;
​
    UINT64 size;
    UINT32 eraseSize;
​
    int (*erase)(struct MtdDev *mtd, UINT64 start, UINT64 len, UINT64 *failAddr);
    int (*read)(struct MtdDev *mtd, UINT64 start, UINT64 len, const char *buf);
    int (*write)(struct MtdDev *mtd, UINT64 start, UINT64 len, const char *buf);
};

不同的Flash要提供它自己的MtdDev结构体。

1.3 块设备驱动程序为MTD开了一个后门

为什么说开了个后门呢,正常操作驱动设备都是read,write,但是我们跳过去会发现都是空的

(不知道是什么原因source突然卡的不行,我用vs截图吧)

我们使用mtd专门的read,write去操作。

在JFFS2文件系统中,是直接使用MTD的,没有使用block_operations。 比如:third_party\Linux_Kernel\fs\jffs2\read.c

/*
 * JFFS2 -- Journalling Flash File System, Version 2.
 *
 * Copyright © 2001-2007 Red Hat, Inc.
 *
 * Created by David Woodhouse <dwmw2@infradead.org>
 *
 * For licensing information, see the file 'LICENCE' in this directory.
 *
 */

#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/pagemap.h>
#include <linux/compiler.h>
#include <mtd_dev.h>
#include "nodelist.h"
#include "compr.h"
#include "los_crc32.h"
#include "user_copy.h"

int jffs2_read_dnode(struct jffs2_sb_info *c, struct jffs2_inode_info *f,
		     struct jffs2_full_dnode *fd, unsigned char *buf,
		     int ofs, int len)
{
	struct jffs2_raw_inode *ri;
	size_t readlen;
	uint32_t crc;
	unsigned char *decomprbuf = NULL;
	unsigned char *readbuf = NULL;
	int ret = 0;

	ri = jffs2_alloc_raw_inode();
	if (!ri)
		return -ENOMEM;

	ret = jffs2_flash_read(c, ref_offset(fd->raw), sizeof(*ri), &readlen, (char *)ri);
	if (ret) {
		jffs2_free_raw_inode(ri);
		pr_warn("Error reading node from 0x%08x: %d\n",
			ref_offset(fd->raw), ret);
		return ret;
	}
	if (readlen != sizeof(*ri)) {
		jffs2_free_raw_inode(ri);
		pr_warn("Short read from 0x%08x: wanted 0x%zx bytes, got 0x%zx\n",
			ref_offset(fd->raw), sizeof(*ri), readlen);
		return -EIO;
	}
	crc = crc32(0, ri, sizeof(*ri)-8);

	jffs2_dbg(1, "Node read from %08x: node_crc %08x, calculated CRC %08x. dsize %x, csize %x, offset %x, buf %p\n",
		  ref_offset(fd->raw), je32_to_cpu(ri->node_crc),
		  crc, je32_to_cpu(ri->dsize), je32_to_cpu(ri->csize),
		  je32_to_cpu(ri->offset), buf);
	if (crc != je32_to_cpu(ri->node_crc)) {
		pr_warn("Node CRC %08x != calculated CRC %08x for node at %08x\n",
			je32_to_cpu(ri->node_crc), crc, ref_offset(fd->raw));
			jffs2_dbg_dump_node(c, ref_offset(fd->raw));
		ret = -EIO;
		goto out_ri;
	}
	/* There was a bug where we wrote hole nodes out with csize/dsize
	   swapped. Deal with it */
	if (ri->compr == JFFS2_COMPR_ZERO && !je32_to_cpu(ri->dsize) &&
	    je32_to_cpu(ri->csize)) {
		ri->dsize = ri->csize;
		ri->csize = cpu_to_je32(0);
	}

	D1(if(ofs + len > je32_to_cpu(ri->dsize)) {
			pr_warn("jffs2_read_dnode() asked for %d bytes at %d from %d-byte node\n",
				len, ofs, je32_to_cpu(ri->dsize));
		ret = -EINVAL;
		goto out_ri;
	});

	if (ri->compr == JFFS2_COMPR_ZERO) {
		ret = LOS_UserMemClear(buf, len);
		goto out_ri;
	}

	/* Cases:
	   Reading whole node and it's uncompressed - read directly to buffer provided, check CRC.
	   Reading whole node and it's compressed - read into comprbuf, check CRC and decompress to buffer provided
	   Reading partial node and it's uncompressed - read into readbuf, check CRC, and copy
	   Reading partial node and it's compressed - read into readbuf, check checksum, decompress to decomprbuf and copy
	*/
	if (ri->compr == JFFS2_COMPR_NONE && len == je32_to_cpu(ri->dsize)) {
		readbuf = kmalloc(je32_to_cpu(ri->dsize), GFP_KERNEL);
		if (!readbuf) {
			ret = -ENOMEM;
			goto out_ri;
		}
	} else {
		readbuf = kmalloc(je32_to_cpu(ri->csize), GFP_KERNEL);
		if (!readbuf) {
			ret = -ENOMEM;
			goto out_ri;
		}
	}
	if (ri->compr != JFFS2_COMPR_NONE) {
		decomprbuf = kmalloc(je32_to_cpu(ri->dsize), GFP_KERNEL);
		if (!decomprbuf) {
			ret = -ENOMEM;
			goto out_readbuf;
		}
	} else {
		decomprbuf = readbuf;
	}

	jffs2_dbg(2, "Read %d bytes to %p\n", je32_to_cpu(ri->csize),
		  readbuf);
	ret = jffs2_flash_read(c, (ref_offset(fd->raw)) + sizeof(*ri),
			       je32_to_cpu(ri->csize), &readlen, (char *)readbuf);

	if (!ret && readlen != je32_to_cpu(ri->csize))
		ret = -EIO;
	if (ret)
		goto out_decomprbuf;

	crc = crc32(0, readbuf, je32_to_cpu(ri->csize));
	if (crc != je32_to_cpu(ri->data_crc)) {
		pr_warn("Data CRC %08x != calculated CRC %08x for node at %08x\n",
			je32_to_cpu(ri->data_crc), crc, ref_offset(fd->raw));
		jffs2_dbg_dump_node(c, ref_offset(fd->raw));
		ret = -EIO;
		goto out_decomprbuf;
	}
	jffs2_dbg(2, "Data CRC matches calculated CRC %08x\n", crc);
	if (ri->compr != JFFS2_COMPR_NONE) {
		jffs2_dbg(2, "Decompress %d bytes from %p to %d bytes at %p\n",
			  je32_to_cpu(ri->csize), readbuf,
			  je32_to_cpu(ri->dsize), decomprbuf);
		ret = jffs2_decompress(c, f, ri->compr | (ri->usercompr << 8), readbuf, decomprbuf, je32_to_cpu(ri->csize), je32_to_cpu(ri->dsize));
		if (ret) {
			pr_warn("Error: jffs2_decompress returned %d\n", ret);
			goto out_decomprbuf;
		}
	}

	if (LOS_CopyFromKernel(buf, len, decomprbuf + ofs, len) != 0) {
		ret = -EFAULT;
	}
 out_decomprbuf:
	if(decomprbuf != buf && decomprbuf != readbuf)
		kfree(decomprbuf);
 out_readbuf:
	if(readbuf != buf)
		kfree(readbuf);
 out_ri:
	jffs2_free_raw_inode(ri);

	return ret;
}

int jffs2_read_inode_range(struct jffs2_sb_info *c, struct jffs2_inode_info *f,
			   unsigned char *buf, uint32_t offset, uint32_t len)
{
	uint32_t end = offset + len;
	struct jffs2_node_frag *frag;
	int ret;

	jffs2_dbg(1, "%s(): ino #%u, range 0x%08x-0x%08x\n",
		  __func__, f->inocache->ino, offset, offset + len);

	frag = jffs2_lookup_node_frag(&f->fragtree, offset);

	/* XXX FIXME: Where a single physical node actually shows up in two
	   frags, we read it twice. Don't do that. */
	/* Now we're pointing at the first frag which overlaps our page
	 * (or perhaps is before it, if we've been asked to read off the
	 * end of the file). */
	while(offset < end) {
		jffs2_dbg(2, "%s(): offset %d, end %d\n",
			  __func__, offset, end);
		if (unlikely(!frag || frag->ofs > offset ||
			     frag->ofs + frag->size <= offset)) {
			uint32_t holesize = end - offset;
			if (frag && frag->ofs > offset) {
				jffs2_dbg(1, "Eep. Hole in ino #%u fraglist. frag->ofs = 0x%08x, offset = 0x%08x\n",
					  f->inocache->ino, frag->ofs, offset);
				holesize = min(holesize, frag->ofs - offset);
			}
			jffs2_dbg(1, "Filling non-frag hole from %d-%d\n",
				  offset, offset + holesize);
			ret = LOS_UserMemClear(buf, holesize);
			if (ret != 0) {
				return ret;
			}
			buf += holesize;
			offset += holesize;
			continue;
		} else if (unlikely(!frag->node)) {
			uint32_t holeend = min(end, frag->ofs + frag->size);
			jffs2_dbg(1, "Filling frag hole from %d-%d (frag 0x%x 0x%x)\n",
				  offset, holeend, frag->ofs,
				  frag->ofs + frag->size);
			ret = LOS_UserMemClear(buf, holeend - offset);
			if (ret != 0) {
				return ret;
			}
			buf += holeend - offset;
			offset = holeend;
			frag = frag_next(frag);
			continue;
		} else {
			uint32_t readlen;
			uint32_t fragofs; /* offset within the frag to start reading */

			fragofs = offset - frag->ofs;
			readlen = min(frag->size - fragofs, end - offset);
			jffs2_dbg(1, "Reading %d-%d from node at 0x%08x (%d)\n",
				  frag->ofs+fragofs,
				  frag->ofs + fragofs+readlen,
				  ref_offset(frag->node->raw),
				  ref_flags(frag->node->raw));
			ret = jffs2_read_dnode(c, f, frag->node, buf, fragofs + frag->ofs - frag->node->ofs, readlen);
			jffs2_dbg(2, "node read done\n");
			if (ret) {
				jffs2_dbg(1, "%s(): error %d\n",
					  __func__, ret);
				(void)LOS_UserMemClear(buf, readlen);
				return ret;
			}
			buf += readlen;
			offset += readlen;
			frag = frag_next(frag);
			jffs2_dbg(2, "node read was OK. Looping\n");
		}
	}
	return 0;
}

int jffs2_flash_direct_read(struct jffs2_sb_info *c, loff_t ofs, size_t len,
			size_t *retlen, const char *buf)
{
	int ret;
	ret = c->mtd->read(c->mtd, ofs, len, (char *)buf);
	if (ret >= 0) {
		*retlen = ret;
		return 0;
	}
	*retlen = 0;
	return ret;
}

怎么用内存模拟Flash

正常是用emmc启动的但是他太难了我们从内存中划出一块模拟flash

2.1 指定要使用的内存地址、大小

        这个函数的来源是韦东山老师在华为的时候根据这个文件金星的修改所以名字还没改但是内容是和spi没啥关系了

2.2 实现MtdDev结构体

源码:vendor\democom\demochip\driver\mtd\spi_nor\src\common\spinor.c

2.3 怎么使用块设备

  • 添加分区

    • /dev/spinor表示整个块设备

    • /dev/spinorblk0表示里面的第0个分区

    • 不添加分区也可以,可以直接挂载/dev/spinor

  • mount

2.4 移植过程

这里定义文件系统的内存大小

怎么理解呢

定义0分区的大小和虚拟内存大小

这个是路径

之前注释掉的挂载代码也可以放出来了

然后修改相关的函数名

#include "los_base.h"
#include "los_config.h"
#include "los_process_pri.h"
#include "lwip/init.h"
#include "lwip/tcpip.h"
#include "sys/mount.h"
#include "mtd_partition.h"
#include "console.h"

UINT32 OsRandomStackGuard(VOID)
{
    return 0;
}

static void exynos4412_mount_rootfs()
{
#if 1
#if 1	
    dprintf("register parition ...\n");
    if (add_mtd_partition("spinor", 0, 0x4000000, 0))
    {
        PRINT_ERR("add_mtd_partition fail\n");
    }
    
    dprintf("mount /dev/spinorblk0 / ...\n");
    //if (mount("/dev/spinorblk0", "/", "jffs2", MS_RDONLY, NULL))
    if (mount("/dev/spinorblk0", "/", "jffs2", 0, NULL))
    {
        PRINT_ERR("mount failed\n");
    }
#else
    dprintf("mount /dev/ramdisk / ...\n");
    //if (mount("/dev/spinorblk0", "/", "jffs2", MS_RDONLY, NULL))
    if (mount("/dev/ramdisk", "/", "vfat", 0, NULL))
    {
        PRINT_ERR("mount failed\n");
    }
#endif
#endif
}

static void exynos4412_driver_init()
{
#if 0	
	extern int my_ramdisk_init(void);
    if (my_ramdisk_init())
    {
        PRINT_ERR("my_ramdisk_init failed\n");
    }

#else	
    extern int spinor_init(void);
    dprintf("spinor_init init ...\n");
    if (!spinor_init())
    {
        PRINT_ERR("spinor_init failed\n");
    }
#endif

#ifdef LOSCFG_DRIVERS_VIDEO
    dprintf("imx6ull_fb_init init ...\n");
	extern int imx6ull_fb_init(void);
    if (imx6ull_fb_init())
    {
        PRINT_ERR("imx6ull_fb_init failed\n");
    }
#endif	

}


void SystemInit()
{
#ifdef LOSCFG_FS_PROC
    dprintf("proc fs init ...\n");
    extern void ProcFsInit(void);
    ProcFsInit();
#endif

#ifdef LOSCFG_DRIVERS_MEM
    dprintf("mem dev init ...\n");
    extern int mem_dev_register(void);
    mem_dev_register();
#endif
    exynos4412_driver_init();
    exynos4412_mount_rootfs();

#ifdef LOSCFG_DRIVERS_HDF
	    extern int DeviceManagerStart(void);
		PRINT_RELEASE("DeviceManagerStart start ...\n");	
		if (DeviceManagerStart()) {
			PRINT_ERR("No drivers need load by hdf manager!");
		}
		dprintf("DeviceManagerStart end ...\n");
#endif

    extern int uart_dev_init(void);
    uart_dev_init();

    if (virtual_serial_init("/dev/uartdev-0") != 0)
    {
        PRINT_ERR("virtual_serial_init failed");
    }

    if (system_console_init(SERIAL) != 0)
    {
        PRINT_ERR("system_console_init failed\n");
    }

    if (OsUserInitProcess())
    {
        PRINT_ERR("Create user init process faialed!\n");
    }
}

我们之前注释的现在也可以放出来了

这里的大小改成刚刚定义的分区0的宏

报错

 先放出来

发现一个事,有定义加U的

但是为什么没用呢神奇,不然之前也不会报那个错误了

有可能这个值找到不到填上现在就没错了


 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

宇努力学习

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

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

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

打赏作者

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

抵扣说明:

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

余额充值