【易开嵌入式】rt-thread+stm32f407+nandflash,实现RL-FLASHFS文件系统移植

版权声明:本版面文章皆为原创、或参考其他技术网站、博客后自己动手做实验所得,转载请注明出处。

鸣谢:感谢eric硬汉

商务合作:lz_kwok@foxmail.com

易开嵌入式工作室

 

基于单片机系统的nandflash文件系统,一直是比较头疼的问题。由于nandflash存在硬件坏块,所以ecc动态均衡校验就显得十分重要。目前开源的ftl算法,都或多或少存在一些问题(没问题的也没开源,反正我没找到)。如此一来,keil的rl-flashfs就显得十足珍贵。

 

先来看看rl-flashfs的特点:

1. RL-FlashFS本身支持擦写均衡,坏块管理,ECC和掉电保护。
2. RL-FlashFS是FAT兼容的文件系统。
3. RL-FlashFS的文件名仅支持ASCII,不支持中文,这点要特别注意。
4. 首次格式化后使用,读速度2.3MB/S左右,写速度3.2MB/S左右,配置不同的文件系统缓冲大小,速度有区别。
5. RL-FlashFS的函数是标准的C库函数,跟电脑端的文件系统使用方法一样。
6. RL-FlashFS与FatFS的区别,FatFS仅是一个FAT类的文件件系统,擦写均衡,坏块管理,ECC和掉电保护都不支持。
    这些都需要用户自己去实现。
7. UFFS,YAFFS这两款文件系统是不兼容FAT的,也就是无法在Windows端模拟U盘。

但是比较讨厌的是,在MDK4.74之后,rl-flashfs与keil自家的操作系统做了比较深的耦合。虽然eric硬汉哥提供了freertos的教程,但是有没有可能把rl-flashfs与目前国内比较火的操作系统rt-thread做兼容呢?虽然rt-thread自带的uffs文件系统也能用,但是占用ram太高,且不兼容FAT。

说干就干,经过两天的移植和测试,初步完成rl-flashfs在rt-thread操作系统下的移植。

 

硬件平台:stm32f407zgtx

NANDFLASH:W29N02GVSIAA(FSMC)

(有需要开发板合作的可以私信邮箱)

从官网下载rl-arm的源码包,如下:

rt-thread有非常棒的scons构建工具,根据该构建语法,编写对应的配置文件Kconfig和SConscript。编写完成后,利用rt-thread提供的Env工具,输入menuconfig,按照下图进行选择。注意,由于时间关系,并未和rtt原生的虚拟文件系统做兼容适配,故选择关闭Device virtual file system

选择完毕后,在Env工具中 输入scons --target=mdk5,可自动将我配置好的源码目录加入到MDK中(本人使用的是mdk5.23)。工程如下:

工程中可以看到三个重要文件,寄File_Config.c、FS_NAND_FlashPrg.c和FSN_CM3.lib,注意FSN_CM3.lib只能用于keil下,对于使用IAR的同学,只能说声拜拜。

File_Config配置如下:

在FS_NAND_FlashPrg.c文件中,需要根据自己的硬件环境,实现如下五个函数:

const NAND_DRV nand0_drv = {
  Init,
  UnInit,
  PageRead,
  PageWrite,
  BlockErase,
};

为了实现这五个函数,首先新建drv_nand.c文件,实现W29N02GVSIAA驱动,代码如下:

static rt_uint8_t FSMC_NAND_ReadStatus(void)
{
	rt_uint8_t ucData;
	rt_uint8_t ucStatus = NAND_BUSY;

	NAND_CMD_AREA = NAND_CMD_STATUS;
	ucData = *(__IO rt_uint8_t *)(Bank_NAND_ADDR);

	if((ucData & NAND_ERROR) == NAND_ERROR)
	{
		ucStatus = NAND_ERROR;
	}
	else if((ucData & NAND_READY) == NAND_READY)
	{
		ucStatus = NAND_READY;
	}
	else
	{
		ucStatus = NAND_BUSY;
	}

	return (ucStatus);
}

static rt_uint8_t FSMC_NAND_GetStatus(void)
{
	rt_uint32_t ulTimeout = 0x10000;
	rt_uint8_t ucStatus = NAND_READY;

	ucStatus = FSMC_NAND_ReadStatus();

	while ((ucStatus != NAND_READY) &&( ulTimeout != 0x00))
	{
		ucStatus = FSMC_NAND_ReadStatus();
		if(ucStatus == NAND_ERROR)
		{
			return (ucStatus);
		}
		ulTimeout--;
	}

	if(ulTimeout == 0x00)
	{
		ucStatus =  NAND_TIMEOUT_ERROR;
	}

	return (ucStatus);
}



//读取NAND FLASH的ID
//不同的NAND略有不同,请根据自己所使用的NAND FALSH数据手册来编写函数
//返回值:NAND FLASH的ID值
static rt_uint32_t NAND_ReadID(void)
{
    NAND_IDTypeDef nand_id;

    HAL_NAND_Read_ID(&NAND_Handler,&nand_id);

    NAND_DEBUG("ID[%X,%X,%X,%X]\n",nand_id.Maker_Id,nand_id.Device_Id,nand_id.Third_Id,nand_id.Fourth_Id);

    return 0;
}  

//复位NAND
//返回值:0,成功;
//    其他,失败
static rt_uint8_t NAND_Reset(void)
{ 
    NAND_CMD_AREA = NAND_RESET;	//复位NAND
    if(FSMC_NAND_GetStatus()==NAND_READY)
        return 0;           //复位成功
    else 
        return 1;			//复位失败
} 


void rt_hw_mtd_nand_deinit(void)
{
    HAL_NAND_DeInit(&NAND_Handler);
}


//初始化NAND FLASH
rt_uint8_t rt_hw_mtd_nand_init(void)
{
    if(&NAND_Handler != NULL){
        rt_hw_mtd_nand_deinit();
    }
    FMC_NAND_PCC_TimingTypeDef ComSpaceTiming,AttSpaceTiming;
                                              
    NAND_Handler.Instance               = FMC_NAND_DEVICE;
    NAND_Handler.Init.NandBank          = FSMC_NAND_BANK2;                             //NAND挂在BANK2上
    NAND_Handler.Init.Waitfeature       = FSMC_NAND_PCC_WAIT_FEATURE_DISABLE;           //关闭等待特性
    NAND_Handler.Init.MemoryDataWidth   = FSMC_NAND_PCC_MEM_BUS_WIDTH_8;                //8位数据宽度
    NAND_Handler.Init.EccComputation    = FSMC_NAND_ECC_DISABLE;                        //不使用ECC
    NAND_Handler.Init.ECCPageSize       = FSMC_NAND_ECC_PAGE_SIZE_2048BYTE;             //ECC页大小为2k
    NAND_Handler.Init.TCLRSetupTime     = 1;                                            //设置TCLR(tCLR=CLE到RE的延时)=(TCLR+TSET+2)*THCLK,THCLK=1/180M=5.5ns
    NAND_Handler.Init.TARSetupTime      = 1;                                            //设置TAR(tAR=ALE到RE的延时)=(TAR+TSET+2)*THCLK,THCLK=1/180M=5.5n。   

    ComSpaceTiming.SetupTime        = 2;        //建立时间
    ComSpaceTiming.WaitSetupTime    = 5;        //等待时间
    ComSpaceTiming.HoldSetupTime    = 3;        //保持时间
    ComSpaceTiming.HiZSetupTime     = 1;        //高阻态时间
    
    AttSpaceTiming.SetupTime        = 2;        //建立时间
    AttSpaceTiming.WaitSetupTime    = 5;        //等待时间
    AttSpaceTiming.HoldSetupTime    = 3;        //保持时间
    AttSpaceTiming.HiZSetupTime     = 1;        //高阻态时间
    
    HAL_NAND_Init(&NAND_Handler,&ComSpaceTiming,&AttSpaceTiming); 
    NAND_Reset();       		        //复位NAND
    rt_thread_mdelay(100);

    return 0;
}

rt_uint8_t FSMC_NAND_ReadPage(rt_uint8_t *_pBuffer, rt_uint32_t _ulPageNo, rt_uint16_t _usAddrInPage, rt_uint16_t NumByteToRead)
{
	rt_uint32_t i;

    NAND_CMD_AREA = NAND_AREA_A;
    //发送地址
    NAND_ADDR_AREA = _usAddrInPage;
	NAND_ADDR_AREA = _usAddrInPage >> 8;
	NAND_ADDR_AREA = _ulPageNo;
	NAND_ADDR_AREA = (_ulPageNo & 0xFF00) >> 8;
    NAND_ADDR_AREA = (_ulPageNo & 0xFF0000) >> 16;

    NAND_CMD_AREA = NAND_AREA_TRUE1;

	 /* 必须等待,否则读出数据异常, 此处应该判断超时 */
	for (i = 0; i < 20; i++);
    while(rt_pin_read(NAND_RB)==0);

	/* 读数据到缓冲区pBuffer */
	for(i = 0; i < NumByteToRead; i++)
	{
		_pBuffer[i] = NAND_DATA_AREA;
	}

	return RT_EOK;
}


rt_uint8_t FSMC_NAND_WritePage(rt_uint8_t *_pBuffer, rt_uint32_t _ulPageNo, rt_uint16_t _usAddrInPage, rt_uint16_t NumByteToRead)
{
	rt_uint32_t i;
  rt_uint8_t ucStatus;

	NAND_CMD_AREA = NAND_WRITE0;
  //发送地址
 	NAND_ADDR_AREA = _usAddrInPage;
	NAND_ADDR_AREA = _usAddrInPage >> 8;
	NAND_ADDR_AREA = _ulPageNo;
	NAND_ADDR_AREA = (_ulPageNo & 0xFF00) >> 8;
    NAND_ADDR_AREA = (_ulPageNo & 0xFF0000) >> 16;
	for (i = 0; i < 20; i++);

	for(i = 0; i < NumByteToRead; i++)
	{
        NAND_DATA_AREA = _pBuffer[i];
	}

	NAND_CMD_AREA = NAND_WRITE_TURE1; 

	for (i = 0; i < 20; i++);
	
  	ucStatus = FSMC_NAND_GetStatus();
	if(ucStatus == NAND_READY)   
	{
		ucStatus = RTV_NOERR;
	}
	else if(ucStatus == NAND_ERROR)
	{
		ucStatus = ERR_NAND_PROG;		
	}
	else if(ucStatus == NAND_TIMEOUT_ERROR)
	{
		ucStatus = ERR_NAND_HW_TOUT;		
	}
	
	return (ucStatus);
}

//擦除一个块
//BlockNum:要擦除的BLOCK编号,范围:0-(block_totalnum-1)
//返回值:0,擦除成功
//    其他,擦除失败
rt_uint8_t NAND_EraseBlock(rt_uint32_t _ulBlockNo)
{
    rt_uint8_t ucStatus;
	
	NAND_CMD_AREA = NAND_ERASE0;

	_ulBlockNo <<= 6;	

    NAND_ADDR_AREA = _ulBlockNo;
    NAND_ADDR_AREA = _ulBlockNo >> 8;
    NAND_ADDR_AREA = _ulBlockNo >> 16;

	NAND_CMD_AREA = NAND_ERASE1;

	ucStatus = FSMC_NAND_GetStatus();
	if(ucStatus == NAND_READY)   
	{
		ucStatus = RTV_NOERR;
	}
	else if(ucStatus == NAND_ERROR)
	{
		ucStatus = ERR_NAND_PROG;		
	}
	else if(ucStatus == NAND_TIMEOUT_ERROR)
	{
		ucStatus = ERR_NAND_HW_TOUT;		
	}
	
	return (ucStatus);
} 

//全片擦除NAND FLASH
void NAND_EraseChip(void)
{
    rt_uint8_t status;
    rt_uint16_t i=0;
    for(i=0;i<2048;i++)     //循环擦除所有的块
    {
        status=NAND_EraseBlock(i);
        if(status)
            NAND_DEBUG("Erase %d block fail!!,ERRORCODE %d\r\n",i,status);//擦除失败
    }
}

实现FS_NAND_FlashPrg.c中五个函数后,还需要对rt-thread原生的stubs.c文件进行修改,根据rl-flashfs重新映射IO输入输出,代码如下:

/*
 * Copyright (c) 2006-2018, RT-Thread Development Team
 *
 * SPDX-License-Identifier: Apache-2.0
 *
 * Change Logs:
 * Date           Author       Notes
 * 2012-11-23     Yihui        The first version
 * 2013-11-24     aozima       fixed _sys_read()/_sys_write() issues.
 * 2014-08-03     bernard      If using msh, use system() implementation
 *                             in msh.
 */

#include <string.h>
#include <rt_sys.h>

#include "rtthread.h"
#include "libc.h"

#ifdef RT_USING_DFS
#include "dfs_posix.h"
#endif

#ifdef RT_USING_RL_FLASHFS
#include <File_Config.h>
struct __FILE { int handle; /* Add whatever you need here */ };
#endif

#ifdef __CLANG_ARM
__asm(".global __use_no_semihosting\n\t");
#else
#pragma import(__use_no_semihosting_swi)
#endif

/* Standard IO device handles. */
#define STDIN       0x8001
#define STDOUT      0x8002
#define STDERR      0x8003

/* Standard IO device name defines. */
const char __stdin_name[]  = "STDIN";
const char __stdout_name[] = "STDOUT";
const char __stderr_name[] = "STDERR";

/**
 * required by fopen() and freopen().
 *
 * @param name - file name with path.
 * @param openmode - a bitmap hose bits mostly correspond directly to
 *                     the ISO mode specification.
 * @return  -1 if an error occurs.
 */
FILEHANDLE _sys_open(const char *name, int openmode)
{
#ifdef RT_USING_DFS
    int fd;
    int mode = O_RDONLY;
#endif

    /* Register standard Input Output devices. */
    if (strcmp(name, __stdin_name) == 0)
        return (STDIN);
    if (strcmp(name, __stdout_name) == 0)
        return (STDOUT);
    if (strcmp(name, __stderr_name) == 0)
        return (STDERR);

#ifndef RT_USING_DFS
    #ifdef RT_USING_RL_FLASHFS
        return (__sys_open (name, openmode));
    #else
        return -1;
    #endif
#else
    /* Correct openmode from fopen to open */
    if (openmode & OPEN_PLUS)
    {
        if (openmode & OPEN_W)
        {
            mode |= (O_RDWR | O_TRUNC | O_CREAT);
        }
        else if (openmode & OPEN_A)
        {
            mode |= (O_RDWR | O_APPEND | O_CREAT);
        }
        else
            mode |= O_RDWR;
    }
    else
    {
        if (openmode & OPEN_W)
        {
            mode |= (O_WRONLY | O_TRUNC | O_CREAT);
        }
        else if (openmode & OPEN_A)
        {
            mode |= (O_WRONLY | O_APPEND | O_CREAT);
        }
    }

    fd = open(name, mode, 0);
    if (fd < 0)
        return -1;
    else
        return fd;
#endif
}

int _sys_close(FILEHANDLE fh)
{
#ifndef RT_USING_DFS
    #ifdef RT_USING_RL_FLASHFS
        if (fh > 0x8000) {
            return (0);
        }
        return (__sys_close (fh));
    #else
        return 0;
    #endif
#else
    if (fh <= STDERR) return 0;

    return close(fh);
#endif
}

/*
 * Read from a file. Can return:
 *  - zero if the read was completely successful
 *  - the number of bytes _not_ read, if the read was partially successful
 *  - the number of bytes not read, plus the top bit set (0x80000000), if
 *    the read was partially successful due to end of file
 *  - -1 if some error other than EOF occurred
 *
 * It is also legal to signal EOF by returning no data but
 * signalling no error (i.e. the top-bit-set mechanism need never
 * be used).
 *
 * So if (for example) the user is trying to read 8 bytes at a time
 * from a file in which only 5 remain, this routine can do three
 * equally valid things:
 *
 *  - it can return 0x80000003 (3 bytes not read due to EOF)
 *  - OR it can return 3 (3 bytes not read), and then return
 *    0x80000008 (8 bytes not read due to EOF) on the next attempt
 *  - OR it can return 3 (3 bytes not read), and then return
 *    8 (8 bytes not read, meaning 0 read, meaning EOF) on the next
 *    attempt
 *
 * `mode' exists for historical reasons and must be ignored.
 */
int _sys_read(FILEHANDLE fh, unsigned char *buf, unsigned len, int mode)
{
#ifdef RT_USING_DFS
    int size;
#endif

    if (fh == STDIN)
    {
#ifdef RT_USING_POSIX
        size = libc_stdio_read(buf, len);
        return len - size;
#else
        /* no stdin */
        return -1;
#endif
    }
#ifndef RT_USING_RL_FLASHFS
    if ((fh == STDOUT) || (fh == STDERR))
        return -1;
#endif

#ifndef RT_USING_DFS
#ifdef RT_USING_RL_FLASHFS
    if (fh > 0x8000) {
        return (-1);
    }
    return (__sys_read (fh, buf, len));
#else
    return 0;
#endif
#else
    size = read(fh, buf, len);
    if (size >= 0)
        return len - size;
    else
        return -1;
#endif
}

/*
 * Write to a file. Returns 0 on success, negative on error, and
 * the number of characters _not_ written on partial success.
 * `mode' exists for historical reasons and must be ignored.
 */
int _sys_write(FILEHANDLE fh, const unsigned char *buf, unsigned len, int mode)
{
#ifdef RT_USING_DFS
    int size;
#endif

    if ((fh == STDOUT) || (fh == STDERR))
    {
#if !defined(RT_USING_CONSOLE) || !defined(RT_USING_DEVICE)
        return 0;
#else
#ifdef RT_USING_POSIX
        size = libc_stdio_write(buf, len);
        return len - size;
#else
        if (rt_console_get_device())
        {
            rt_device_write(rt_console_get_device(), -1, buf, len);
            return 0;
        }

        return -1;
#endif
#endif
    }
#ifndef RT_USING_RL_FLASHFS
    if (fh == STDIN) return -1;
#endif
#ifndef RT_USING_DFS
#ifdef RT_USING_RL_FLASHFS
    if (fh > 0x8000) {
        return (-1);
    }
    return (__sys_write (fh, buf, len));
#else
    return 0;
#endif
#else
    size = write(fh, buf, len);
    if (size >= 0)
        return len - size;
    else
        return -1;
#endif
}

/*
 * Move the file position to a given offset from the file start.
 * Returns >=0 on success, <0 on failure.
 */
int _sys_seek(FILEHANDLE fh, long pos)
{
#ifndef RT_USING_RL_FLASHFS
    if (fh < STDERR)
        return -1;
#endif
#ifndef RT_USING_DFS
#ifdef RT_USING_RL_FLASHFS
    if (fh > 0x8000) {
        return (-1);
    }
    return (__sys_seek (fh, pos));
#else
    return -1;
#endif
#else

    /* position is relative to the start of file fh */
    return lseek(fh, pos, 0);
#endif
}

/**
 * used by tmpnam() or tmpfile()
 */
int _sys_tmpnam(char *name, int fileno, unsigned maxlength)
{
#ifdef RT_USING_RL_FLASHFS
    return 1;
#else
    return -1;
#endif
}

char *_sys_command_string(char *cmd, int len)
{
#ifdef RT_USING_RL_FLASHFS
    return cmd;
#else
    /* no support */
    return RT_NULL;
#endif
}

/* This function writes a character to the console. */
void _ttywrch(int ch)
{
#ifdef RT_USING_CONSOLE
    char c;

    c = (char)ch;
    rt_kprintf(&c);
#endif
}

RT_WEAK void _sys_exit(int return_code)
{
    /* TODO: perhaps exit the thread which is invoking this function */
    while (1);
}

/**
 * return current length of file.
 *
 * @param fh - file handle
 * @return file length, or -1 on failed
 */
long _sys_flen(FILEHANDLE fh)
{
    struct stat stat;
#ifndef RT_USING_RL_FLASHFS   
    if (fh < STDERR)
        return -1;
#endif
#ifndef RT_USING_DFS
#ifdef RT_USING_RL_FLASHFS
    if (fh > 0x8000) {
        return (0);
    }
    return (__sys_flen (fh));
#else
    return -1;
#endif
#else
    fstat(fh, &stat);
    return stat.st_size;
#endif
}

int _sys_istty(FILEHANDLE fh)
{
#ifdef RT_USING_RL_FLASHFS
    if (fh > 0x8000) {
        return (1);
    }
    return (0);
#else
    if((STDIN <= fh) && (fh <= STDERR))
        return 1;
    else
        return 0;
#endif
}

int _sys_ensure (FILEHANDLE fh) {
  if (fh > 0x8000) {
    return (-1);
  }
  return (__sys_ensure (fh));
}

int remove(const char *filename)
{
#ifndef RT_USING_DFS
    return -1;
#else
    return unlink(filename);
#endif
}

#if defined(RT_USING_FINSH) && defined(FINSH_USING_MSH) && defined(RT_USING_MODULE) && defined(RT_USING_DFS)
/* use system(const char *string) implementation in the msh */
#else
int system(const char *string)
{
    RT_ASSERT(0);
    for (;;);
}
#endif

#ifdef __MICROLIB
#include <stdio.h>

int fputc(int c, FILE *f) 
{
    char ch[2] = {0};

    ch[0] = c;
    rt_kprintf(&ch[0]);
    return 1;
}

int fgetc(FILE *f) 
{
#ifdef RT_USING_POSIX
    char ch;

    if (libc_stdio_read(&ch, 1) == 1)
        return ch;
#endif

    return -1;
}
#endif

到这里,移植工作告一段落。

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------

rt-thread吸引我的,除了强大配置功能之外,还有类似于linux的msh功能。既然是对文件系统移植,那就自定义几个shell命令来玩玩吧。参考linux的shell脚本,稍作修改:

static void glz_nand(int argc, char **argv)
{
    /* If the number of arguments less than 2 */
    if (argc < 2)
    {
help:
        rt_kprintf("\n");
        rt_kprintf("glz_nand [OPTION] [PARAM ...]\n");
        rt_kprintf("         ls                           显示指定工作目录下之内容\n");
        rt_kprintf("         cat        <filename>        显示文件内容\n");
        rt_kprintf("         mkdir      <docname>         创建文件夹\n");
        rt_kprintf("         rm         <filename>        删除文件\n");
        rt_kprintf("         formatall                    磁盘格式化\n");
		rt_kprintf("         df                           显示磁盘空间\n");
        return ;
    }
    else if (!strcmp(argv[1], "ls"))
    {
		if(argv[2]  != NULL){
			ViewRootDir(argv[2]);
		}else{
			ViewRootDir(NULL);
		}
    }
    else if (!strcmp(argv[1], "cat"))
    {
        if (argc < 3)
        {
            rt_kprintf("The input parameters are too few!\n");
            goto help;
        }
		ReadFileData(argv[2]);
    }
    else if (!strcmp(argv[1], "echo"))
    {
        if (argc < 4)
        {
            rt_kprintf("The input parameters are too few!\n");
            goto help;
        }
		if(!strcmp(argv[3], ">")){
			EchotextFile(argv[2],argv[4]);
		}else{
			rt_kprintf("bad parameters\n");
		}
    }
    else if (!strcmp(argv[1], "formatall"))
    {
		Formatflash();
    }
	else if (!strcmp(argv[1], "df"))
    {
		ViewNandCapacity();
    }
	else if (!strcmp(argv[1], "df"))
    {
		ViewNandCapacity();
    }
	else if (!strcmp(argv[1], "mkdir"))
    {
		if (argc < 2)
        {
            rt_kprintf("The input parameters are too few!\n");
            goto help;
        }
		CreateNewFile(argv[2]);
    }
	else if (!strcmp(argv[1], "rm"))
    {
		if (argc < 2)
        {
            rt_kprintf("The input parameters are too few!\n");
            goto help;
        }
		DeleteDirFile(argv[2]);
    }
    else
    {
        rt_kprintf("Input parameters are not supported!\n");
        goto help;
    }
}
MSH_CMD_EXPORT(glz_nand, GLZ nand RL-FLASHFS test function);

实现上述所有函数后,编译、下载,我们来看下效果撒。

首先,demo板上电,从串口输出log:

在msd中输入glz_nand,回车,可以看到命令提示:

先试下df,看下磁盘空间:

可以看到 磁盘空间大小为256MB。

下面分别实现其他指令操作:

好了,再也不用担心在单片机下对nandflash进行操作了。。。。。。。。结束!

评论 3 您还未登录,请先 登录 后发表或查看评论

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

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页

打赏作者

lz_kwok

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

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

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

打赏作者

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

抵扣说明:

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

余额充值