移植u-boot2012.04.1 -》2440 (三)nandflash 识别

    board_init_r 函数中,两个重要的过程就是 norflash 的识别和 nandflash 的识别,norflash 的识别过程以及如何移植前边已经分析过,本文首先会分析 smdk2410 nandflash 的识别过程,根据 2410 与 2440 之间的差别,进行移植。

    在分析之前,先来回顾一下 nandflash 的操作。

一、nandflash 操作

  1、发命令


    CLE:commond latch enable 为高电平

    CE:片选 低电平

    ALE:address latch enable 为低电平

 2、发地址


    CLE:commond latch enable 为低电平

    CE:片选 低电平

    ALE:address latch enable 为高电平

    我的这个 nanflash 页大小为2K,地址发送需要5个周期

  3、发数据


    CLE:commond latch enable 为低电平

    CE:片选 低电平

    ALE:address latch enable 为低电平

  4、读ID


   先发出 0x90 命令,然后再发送地址 0x00 ,然后读5个周期,第一个周期是厂商ID,第二个周期是芯片ID 我这个是 0xDA,后面三个周期也是芯片相关的东西,比如页大小,重点看下 4th 读回的含义。


    正常我这个读出来是 0x95 1001 0101B,页大小 01 2KB,Block size 128KB,位宽 x8 等等

二、smdk2410 nandflash 识别过程

    前边移植过 norflash,它是先读ID,然后与已知的信息进行匹配,匹配成功则能成功识别,分析完 nandflash 的代码你会发现是一样的,先来看一下移植 Norflash 部分时,nandflash 部分的错误信息。


    搜索一下“NAND:”,发现在arch\arm\lib\board.c 中打印,紧跟着就是 nand_init()

nand_init()->
static void nand_init_chip(int i)
{
    struct mtd_info *mtd = &nand_info[i];
    struct nand_chip *nand = &nand_chip[i];

    mtd->priv = nand;//使 nand 作为 mtd 的私有数据
    //IO_ADDR_R IO_ADDR_W 指向 nandflash 相关寄存器的基地址
    nand->IO_ADDR_R = nand->IO_ADDR_W = (void  __iomem *)0x4E000000;

    if (board_nand_init(nand))

    if (nand_scan(mtd, 1))

    nand_register(i);
}

    这里定义了两个结构体,struct mtd_info 和 struct nand_chip 分别对应于两层,nand_chip 看到 "chip" 我们就应该知道它是"芯片"相关的底层函数,它知道操作哪些寄存器进行读写等,但是它却不知道发送读写哪些数据。相反 mtd_info 对应于上层的统一的接口,它知道读写哪些数据,间接调用底层 nand_chip 进行 nandflash 操作。这样分层是有好处的,对于大量的芯片,我们只需要创建或修改 nand_chip 结构,上层的 mtd 接口无需更改。

    drivers\mtd\nand\s3c2410_nand.c -> board_nand_init

int board_nand_init(struct nand_chip *nand)
{
	u_int32_t cfg;
	u_int8_t tacls, twrph0, twrph1;
	struct s3c24x0_clock_power *clk_power = s3c24x0_get_base_clock_power();
	struct s3c2440_nand *nand_reg = s3c2440_get_base_nand();
	// 我们在串口看到的打印信息
	debug("board_nand_init()\n");
	// 使能nandflash 时钟
	writel(readl(&clk_power->clkcon) | (1 << 4), &clk_power->clkcon);

	/* CONFIG_S3C24XX_CUSTOM_NAND_TIMING 没有定义 */
#if defined(CONFIG_S3C24XX_CUSTOM_NAND_TIMING)
	tacls  = CONFIG_S3C24XX_TACLS;
	twrph0 = CONFIG_S3C24XX_TWRPH0;
	twrph1 =  CONFIG_S3C24XX_TWRPH1;
#else	// 执行这个分支,最好修改符合我们 nandflash 的时序
	tacls = 4;
	twrph0 = 8;
	twrph1 = 8;
#endif
	// 根据上面的三个参数,配置 nfconf nandflash 控制寄存器
	cfg = S3C2410_NFCONF_EN;
	cfg |= S3C2410_NFCONF_TACLS(tacls - 1);
	cfg |= S3C2410_NFCONF_TWRPH0(twrph0 - 1);
	cfg |= S3C2410_NFCONF_TWRPH1(twrph1 - 1);
	writel(cfg, &nand_reg->nfconf);

	/* initialize nand_chip data structure */
	nand->IO_ADDR_R = (void *)&nand_reg->nfdata;
	nand->IO_ADDR_W = (void *)&nand_reg->nfdata;

	nand->select_chip = NULL;

	/* read_buf and write_buf are default */
	/* read_byte and write_byte are default */
#ifdef CONFIG_NAND_SPL	// 这个宏没有定义
	nand->read_buf = nand_read_buf;
#endif

	/* hwcontrol 是最底层的操作函数 */
	nand->cmd_ctrl = s3c2410_hwcontrol;

	nand->dev_ready = s3c2410_dev_ready;

#ifdef CONFIG_S3C2410_NAND_HWECC	// 硬件 ECC 没有定义
	nand->ecc.hwctl = s3c2410_nand_enable_hwecc;
	nand->ecc.calculate = s3c2410_nand_calculate_ecc;
	nand->ecc.correct = s3c2410_nand_correct_data;
	nand->ecc.mode = NAND_ECC_HW;
	nand->ecc.size = CONFIG_SYS_NAND_ECCSIZE;
	nand->ecc.bytes = CONFIG_SYS_NAND_ECCBYTES;
#else	// 执行这个分支,采用软件 ECC
	nand->ecc.mode = NAND_ECC_SOFT;
#endif

#ifdef CONFIG_S3C2410_NAND_BBT	// 没有定义
	nand->options = NAND_USE_FLASH_BBT;
#else	// 执行这个分支
	nand->options = 0;
#endif

	debug("end of nand_init\n");

	return 0;
}
    这个函数主要的工作就是配置寄存器,初始化 nandflash 了,对于 2410 仅仅配置 nfconf 寄存器就可以了,2440 还有 nfcont 寄存器。那么,先做如下修改(不同的nandflash的时序要求不一样,看看自己以前的nandflash 实验的参数):
#if defined(CONFIG_S3C24XX_CUSTOM_NAND_TIMING)
	tacls  = CONFIG_S3C24XX_TACLS;
	twrph0 = CONFIG_S3C24XX_TWRPH0;
	twrph1 =  CONFIG_S3C24XX_TWRPH1;
#else
	//tacls = 4;
	//twrph0 = 8;
	//twrph1 = 8;
	tacls = 1;
	twrph0 = 1;
	twrph1 = 0;
#endif

	//cfg = S3C2410_NFCONF_EN;
	//cfg |= S3C2410_NFCONF_TACLS(tacls - 1);
	//cfg |= S3C2410_NFCONF_TWRPH0(twrph0 - 1);
	//cfg |= S3C2410_NFCONF_TWRPH1(twrph1 - 1);
	cfg = (tacls << 12)|(twrph0 << 8)|(twrph1 << 4);//新增
	writel(cfg, &nand_reg->nfconf);
	cfg = (1<<4)|(1<<1)|(1<<0);	//新增
	writel(cfg, &nand_reg->nfcont);
    这个函数除了配置 nfconf 和 nfcont 之外,还指定了两个底层的函数,首先是 s3c2410_hwcontrol
static void s3c2410_hwcontrol(struct mtd_info *mtd, int cmd, unsigned int ctrl)
{
	struct nand_chip *chip = mtd->priv;
	struct s3c2440_nand *nand = s3c2440_get_base_nand();

	debug("hwcontrol(): 0x%02x 0x%02x\n", cmd, ctrl);

	if (ctrl & NAND_CTRL_CHANGE) {
		ulong IO_ADDR_W = (ulong)nand;

		if (!(ctrl & NAND_CLE))
			IO_ADDR_W |= S3C2410_ADDR_NCLE;
		if (!(ctrl & NAND_ALE))
			IO_ADDR_W |= S3C2410_ADDR_NALE;

		chip->IO_ADDR_W = (void *)IO_ADDR_W;

		if (ctrl & NAND_NCE)
			writel(readl(&nand->nfconf) & ~S3C2410_NFCONF_nFCE,
			       &nand->nfconf);
		else
			writel(readl(&nand->nfconf) | S3C2410_NFCONF_nFCE,
			       &nand->nfconf);
	}

	if (cmd != NAND_CMD_NONE)
		writeb(cmd, chip->IO_ADDR_W);
}
   理解 uboot 作者的意图是至关重要的,首先看参数,cmd 命令,函数的最后写到 chip->IO_ADDR_W 里去,前面我们指定的 chip->IO_ADDR_W = (void *)&nand_reg->nfdata; 显然将命令发送到数据寄存器是错误,我们看到在发送之前 chip->IO_ADDR_W = (void *)IO_ADDR_W; IO_ADDR_W 这个变量是根据 ctrl 来计算的,它有以下取值

    ctrl :!NAND_CLE ,             S3C2410_ADDR_NCLE == 8    ->地址

    ctrl :!NAND_ALE ,              S3C2410_ADDR_NALE == 4    -> 指令

    ctrl : (!NAND_CLE) | (!NAND_ALE)    -> 8 | 4 == 12   -> 数据

struct s3c2410_nand {
	u32	nfconf;
	u32	nfcmd;
	u32	nfaddr;
	u32	nfdata;
	u32	nfstat;
	u32	nfecc;
};
    2410寄存器的偏移量确实是吻合的,2410吻合了,2440呢?
struct s3c2440_nand {
	u32	nfconf;
	u32	nfcont;
	u32	nfcmd;
	u32	nfaddr;
	u32	nfdata;
	u32	nfeccd0;
	u32	nfeccd1;
	u32	nfeccd;
	u32	nfstat;
	u32	nfstat0;
	u32	nfstat1;
};
    显然,不修改的话,寄存器就都搞错了,作如下修改:
		if (!(ctrl & NAND_CLE))
			IO_ADDR_W |= 12;<span style="white-space:pre">	</span>//修改
		if (!(ctrl & NAND_ALE))
			IO_ADDR_W |= 8;         //<span style="font-family: Arial, Helvetica, sans-serif;">修改</span>
		if ((!(ctrl & NAND_CLE)) && (!(ctrl & NAND_ALE)))<span style="white-space:pre">	</span>//新增加
			IO_ADDR_W = IO_ADDR_W + 4;    //8|12 == 12 != 16 因此 + 4
    紧接着看下边
		if (ctrl & NAND_NCE)
			// 2410 nfconf bit 11 清零 选中片选
			writel(readl(&nand->nfconf) & ~S3C2410_NFCONF_nFCE,
			       &nand->nfconf);
		else
			writel(readl(&nand->nfconf) | S3C2410_NFCONF_nFCE,
			       &nand->nfconf);
    这个函数中还集成了片选和取消片选的功能,当 ctrl bit 0 为 1 时,选中片选,当然这也是 2410 的东西,做如下修改。
		if (ctrl & NAND_NCE)
			writel(readl(&nand->nfcont) & ~(1<<1), 

			       &nand->nfcont);
		else
			writel(readl(&nand->nfcont) | (1<<1),
			       &nand->nfcont);
    第二个函数是  s3c2410_dev_ready ,这个之前第一次编译时报错已经修改过了。    

    board_nand_init 函数结束,下面是 drivers\mtd\nand\nand.c -> nand_scan

int nand_scan(struct mtd_info *mtd, int maxchips)
{
	int ret;

	ret = nand_scan_ident(mtd, maxchips, NULL);
	if (!ret)
		ret = nand_scan_tail(mtd);
	return ret;
}
    先来看第一个函数,drivers\mtd\nand\nand_base.c -> nand_scan_ident
int nand_scan_ident(struct mtd_info *mtd, int maxchips,
		    const struct nand_flash_dev *table)
{
	int i, busw, nand_maf_id, nand_dev_id;
	struct nand_chip *chip = mtd->priv;
	const struct nand_flash_dev *type;

	/* chip->options == 0 ,busw == 0 */
	busw = chip->options & NAND_BUSWIDTH_16;
	/* 设置默认的操作函数 */
	nand_set_defaults(chip, busw);

	/* 读芯片类型 */
	type = nand_get_flash_type(mtd, chip, busw,
				&nand_maf_id, &nand_dev_id, table);
	/* 找不到时,打印下面的信息,跟我们刚开始时一样 */
	if (IS_ERR(type)) {
#ifndef CONFIG_SYS_NAND_QUIET_TEST
		printk(KERN_WARNING "No NAND device found!!!\n");
#endif
		chip->select_chip(mtd, -1);
		return PTR_ERR(type);
	}

	/* maxchips == 1 for 循环不执行 */
	for (i = 1; i < maxchips; i++) {
		...
	}
#ifdef DEBUG
	if (i > 1)
		printk(KERN_INFO "%d NAND chips detected\n", i);
#endif

	/* Store the number of chips and calc total size for mtd */
	chip->numchips = i;
	mtd->size = i * chip->chipsize;

	return 0;
}
    drivers\mtd\nand\n and_base.c -> nand_set_defaults
static void nand_set_defaults(struct nand_chip *chip, int busw)
{
	/* check for proper chip_delay setup, set 20us if not */
	if (!chip->chip_delay)
		chip->chip_delay = 20;

	/* check, if a user supplied command function given */
	if (chip->cmdfunc == NULL)
		chip->cmdfunc = nand_command;

	/* check, if a user supplied wait function given */
	if (chip->waitfunc == NULL)
		chip->waitfunc = nand_wait;

	if (!chip->select_chip)
		chip->select_chip = nand_select_chip;
	if (!chip->read_byte)
		chip->read_byte = busw ? nand_read_byte16 : nand_read_byte;
	if (!chip->read_word)
		chip->read_word = nand_read_word;
	if (!chip->block_bad)
		chip->block_bad = nand_block_bad;
	if (!chip->block_markbad)
		chip->block_markbad = nand_default_block_markbad;
	if (!chip->write_buf)
		chip->write_buf = busw ? nand_write_buf16 : nand_write_buf;
	if (!chip->read_buf)
		chip->read_buf = busw ? nand_read_buf16 : nand_read_buf;
	if (!chip->verify_buf)
		chip->verify_buf = busw ? nand_verify_buf16 : nand_verify_buf;
	if (!chip->scan_bbt)
		chip->scan_bbt = nand_default_bbt;
	if (!chip->controller)
		chip->controller = &chip->hwcontrol;
}
    在这里我们先看两个函数,nand_command 和 nand_select_chip ,关于读写的函数后边再说。
    drivers\mtd\nand\nand_base.c -> nand_select_chip

static void nand_select_chip(struct mtd_info *mtd, int chipnr)
{
	struct nand_chip *chip = mtd->priv;

	switch (chipnr) {
	case -1:
		chip->cmd_ctrl(mtd, NAND_CMD_NONE, 0 | NAND_CTRL_CHANGE);
		break;
	case 0:
		break;

	default:
		BUG();
	}
}
    很有意思,nand_select_chip(,-1)取消片选,没有其它功能了,因为我们片选的功能在  s3c2410_hwcontrol 中实现了,因此它要不要其实都行,不做修改。
    drivers\mtd\nand\nand_base.c -> nand_command ,很有意思,我们以读ID为例,分析这个函数。
    chip->cmdfunc(mtd, NAND_CMD_READID, 0x00, -1);

static void nand_command(struct mtd_info *mtd, unsigned int command,
			 int column, int page_addr)
{
	register struct nand_chip *chip = mtd->priv;
	int ctrl = NAND_CTRL_CLE | NAND_CTRL_CHANGE;
	uint32_t rst_sts_cnt = CONFIG_SYS_NAND_RESET_CNT;

	/* commmand == NAND_CMD_READID */
	if (command == NAND_CMD_SEQIN) {
		...
	}
	/*
	 * ctrl ==  NAND_CTRL_CLE | NAND_CTRL_CHANGE
	 * #define NAND_CTRL_CLE (NAND_NCE | NAND_CLE)
	 * 选中片选,发送读ID 命令 0x90
	 */
	chip->cmd_ctrl(mtd, command, ctrl);

	ctrl = NAND_CTRL_ALE | NAND_CTRL_CHANGE;
	if (column != -1) {
		if (chip->options & NAND_BUSWIDTH_16)//8bit 不执行
			column >>= 1;
		/*
		 * ctrl ==  NAND_CTRL_ALE | NAND_CTRL_CHANGE
		 * 发送 0x00 到地址寄存器
		 */
		chip->cmd_ctrl(mtd, column, ctrl);
		ctrl &= ~NAND_CTRL_CHANGE;
	}
	if (page_addr != -1) {	// 不执行
		...
	}
	/* 取消片选 */
	chip->cmd_ctrl(mtd, NAND_CMD_NONE, NAND_NCE | NAND_CTRL_CHANGE);

	...
	/* Apply this short delay always to ensure that we do wait tWB in
	 * any case on any machine. */
	ndelay(100);

	nand_wait_ready(mtd);
}
    看来 nand_command 对于我们读 ID 来说是不需要修改的,其实这个函数对应于小页的 nandflash ,后边正确识别之后会判断页大小,大页的 nandflash 会使用 nand_command_lp 函数。
    真正的识别过程从这里开始 drivers\mtd\nand\nand_base.c -> nand_get_flash_type

	/* 读ID的过程刚分析完毕 */
	chip->cmdfunc(mtd, NAND_CMD_READID, 0x00, -1);
	/* Read manufacturer and device IDs */
	*maf_id = chip->read_byte(mtd);
	*dev_id = chip->read_byte(mtd);
	/* 有些时候读一次不靠谱,所以多读几次 */
	chip->cmdfunc(mtd, NAND_CMD_READID, 0x00, -1);
	printf("maf_id:%x, dev_id:%x\n", *maf_id, *dev_id);
	for (i = 0; i < 2; i++)
		id_data[i] = chip->read_byte(mtd);

	if (id_data[0] != *maf_id || id_data[1] != *dev_id) {
		printk(KERN_INFO "%s: second ID read did not match "
		       "%02x,%02x against %02x,%02x\n", __func__,
		       *maf_id, *dev_id, id_data[0], id_data[1]);
		return ERR_PTR(-ENODEV);
	}
	/* 所有已知的 nandflash 参数都在 nand_flash_ids 中 */
	if (!type)
		type = nand_flash_ids;
	/* 查找匹配 */
	for (; type->name != NULL; type++)
		if (*dev_id == type->id)
			break;
    根据芯片手册,我这款nandflash deviceid 是 0xDA ,nand_flash_ids 数组中已经存在了,如果没有的话,自己照葫芦画瓢添加。简单修改到这,make 烧写看看效果。

    OK,nandflash 的识别是已经没有问题了。执行nand dump nand read/write 等函数都没啥问题。

    在后边烧写 yaffs 文件系统时,发现这个版本的 uboot 对于 yaffs 文件系统的烧写是有 Bug 的,下面来分析一下。

    首先 nand write.yaffs 这个命令不被识别,在 smdk2440.h 中增加一个定义

        #define CONFIG_CMD_NAND_YAFFS

    增加完这个功能还没完事,烧写还有 bug 继续分析。 

    我们在命令行输入 nand dump/read/write/yaffs 等命令时,调用到 common\cmd_nand.c -> do_nand

    
               if (!strcmp(s, ".yaffs")) {
			if (read) {
				printf("Unknown nand command suffix '%s'.\n", s);
				return 1;
			}
			ret = nand_write_skip_bad(nand, off, &rwsize,
						(u_char *)addr, WITH_YAFFS_OOB);

    在 do_nand 函数中,如果我们输入了 nand write.yaffs 则会调用 nand_write_skip_bad 函数。

    drivers\mtd\nand\nand_util.c   -> nand_write_skip_bad

	need_skip = check_skip_len(nand, offset, *length);
	if (need_skip < 0) {
		printf ("Attempt to write outside the flash area\n");
		*length = 0;
		return -EINVAL;
	}

	if (!need_skip && !(flags & WITH_DROP_FFS)) {
		rval = nand_write (nand, offset, length, buffer);
		if (rval == 0)
			return 0;

		*length = 0;
		printf ("NAND write to offset %llx failed %d\n",
			offset, rval);
		return rval;
	}

    如果 nandflash 中没有坏块,那么 if (!need_skip && !(flags & WITH_DROP_FFS))条件成立,则使用 nand_write 进行烧写,而且烧写完成之后直接 return。

static inline int nand_write(nand_info_t *info, loff_t ofs, size_t *len, u_char *buf)
{
	return info->write(info, ofs, *len, (size_t *)len, buf);
}
static int nand_write(struct mtd_info *mtd, loff_t to, size_t len,
			  size_t *retlen, const uint8_t *buf)
{
	struct nand_chip *chip = mtd->priv;
	int ret;

	/* Do not allow writes past end of device */
	if ((to + len) > mtd->size)
		return -EINVAL;
	if (!len)
		return 0;

	nand_get_device(chip, mtd, FL_WRITING);

	chip->ops.len = len;
	chip->ops.datbuf = (uint8_t *)buf;
	chip->ops.oobbuf = NULL;

	ret = nand_do_write_ops(mtd, to, &chip->ops);

	*retlen = chip->ops.retlen;

	nand_release_device(mtd);

	return ret;
}
    在 nand_write 函数中 chip->ops.oobbuf = NULL ,压根就不会写 oob ,因此没有坏快时直接用 nand_write 烧写是不行的。真正的烧写函数在后边。
if (flags & WITH_YAFFS_OOB) {
			int page, pages;
			size_t pagesize = nand->writesize;
			size_t pagesize_oob = pagesize + nand->oobsize;
			struct mtd_oob_ops ops;

			ops.len = pagesize;
			ops.ooblen = nand->oobsize;
			ops.mode = MTD_OOB_AUTO;
			ops.ooboffs = 0;

			pages = write_size / pagesize_oob;
			for (page = 0; page < pages; page++) {
				WATCHDOG_RESET();

				ops.datbuf = p_buffer;
				ops.oobbuf = ops.datbuf + pagesize;

				rval = nand->write_oob(nand, offset, &ops);
				if (!rval)
					break;

				offset += pagesize;
				p_buffer += pagesize_oob;
			}
		}
    因此,我们需要做的就是,及时没有坏块时也不直接用 nand_write 来烧写。

    修改 :if (!need_skip && !(flags & WITH_DROP_FFS))
    改为 :if (!need_skip && !(flags & WITH_DROP_FFS) &&!(flags & WITH_YAFFS_OOB))

    就算改完,实验过程中烧写瞬间完成,显然不靠谱!还需要修改一个地方

			ops.len = pagesize;
			ops.ooblen = nand->oobsize;
			ops.mode = MTD_OOB_RAW;  //修改
			ops.ooboffs = 0;

			pages = write_size / pagesize_oob;
			for (page = 0; page < pages; page++) {
				WATCHDOG_RESET();

				ops.datbuf = p_buffer;
				ops.oobbuf = ops.datbuf + pagesize;

				rval = nand->write_oob(nand, offset, &ops);
				if (rval)  //修改
					break;

				offset += pagesize;
				p_buffer += pagesize_oob;
			}

    这才算大功告成。










  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
u-boot是用于启动和管理嵌入式系统的开源引导加载程序。而Zynq MP(Zynq UltraScale+ MPSoC)是Xilinx公司推出的一款高性能多核处理器系统级芯片,结合了处理器和可编程逻辑资源,常被用于嵌入式系统的开发。 u-boot与Zynq MP的结合是为了更好地管理和启动基于Zynq MP的嵌入式系统。通过u-boot,我们可以加载和启动操作系统(如Linux)以及其他应用程序,同时还能提供一些硬件控制和配置的功能。 具体来说,u-boot在Zynq MP中的主要功能如下: 1. 引导加载:u-boot可以从不同的存储介质(如SD卡、NAND闪存)中加载启动镜像,并将控制权交给操作系统。 2. 配置管理:u-boot提供了一系列命令和结构来配置和管理Zynq MP的各种外设、时钟和内存设置等。 3. bootargs设置:u-boot可以设置操作系统启动参数,如内核命令行参数、设备树等,以便操作系统正确地初始化硬件和驱动。 4. 安全性支持:u-boot还支持一些安全性功能,如Secure Boot、认证和加密等,以保护系统免受恶意代码和未经授权的访问。 5. 调试和故障排除:u-boot可以提供一些调试和故障排除的功能,如使用调试器进行硬件/软件的调试,显示系统状态信息等。 总之,u-boot为Zynq MP提供了一个灵活可定制的引导加载程序,使得开发者可以更好地管理和控制嵌入式系统的启动和初始化过程。同时,通过u-boot的功能,我们可以更方便地进行系统调试、配置和故障排除。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值