u-boot 启动流程分析

前言

写文章的目的是想通过记录自己的学习过程,以便以后使用到相关的知识点可以回顾和参考。

一、在链接脚本中找程序入口

在arch\arm\cpu\slsiap中找到u-boot.lds,里面可以看到ENTRY(_stext),即程序入口就是_stext了。

OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
OUTPUT_ARCH(arm)
ENTRY(_stext)
SECTIONS
{
	. = 0x00000000;

	. = ALIGN(4);
	.text :
	{
		*(.__image_copy_start)
		SOCDIR/start.o (.text*)
		SOCDIR/vectors.o (.text*)
		*(.text*)
	}

不过可以看到链接的地址是0x00000000,可在SecureCRT中显示uboot启动地址在0x43C00000
在这里插入图片描述
那么可以在ubuntu命令行输入grep -nR “0x43C00000”,就定位到0x43C00000的位置了,在
include\configs\x6818.h这个配置.h文件中:

#define CONFIG_RELOC_TO_TEXT_BASE												/* Relocate u-boot code to TEXT_BASE */

#define	CONFIG_SYS_TEXT_BASE 			0x43C00000
#define	CONFIG_SYS_INIT_SP_ADDR			CONFIG_SYS_TEXT_BASE					/* init and run stack pointer */

/* malloc() pool */
#define	CONFIG_MEM_MALLOC_START			0x44000000
#define CONFIG_MEM_MALLOC_LENGTH		32*1024*1024							/* more than 2M for ubifs: MAX 16M */

/* when CONFIG_LCD */
#define CONFIG_FB_ADDR					0x46000000
#define CONFIG_BMP_ADDR					0x47000000

/* Download OFFSET */
#define CONFIG_MEM_LOAD_ADDR			0x48000000

在后面拷贝u-boot到SDRAM会使用到CONFIG_SYS_TEXT_BASE 这个宏定义,即u-boot会在0x43C00000开始执行。

二、分析Start.S

在arch\arm\cpu\slsiap\s5p6818里面找到Start.S,打开可以看到:

	.globl	_stext
_stext:
	b 	reset
	ldr	pc, _undefined_instruction
	ldr	pc, _software_interrupt
	ldr	pc, _prefetch_abort
	ldr	pc, _data_abort
	ldr	pc, _not_used
	ldr	pc, _irq
	ldr	pc, _fiq

_undefined_instruction:	.word undefined_instruction
_software_interrupt:	.word software_interrupt
_prefetch_abort:	.word prefetch_abort
_data_abort:		.word data_abort
_not_used:		.word not_used
_irq:			.word irq
_fiq:			.word fiq

在_stext中执行的第一条代码是 b reset,那么先跳转到reset看看做了什么事情。

	.globl reset

reset:
	/*
	 * set the cpu to SVC32 mode
	 */
	mrs	r0, cpsr
	bic	r0, r0, #0x1f
	orr	r0, r0, #0xd3
	msr	cpsr,r0

	/* disable watchdog */
	ldr	r0, =0xC0019000
	mov	r1, #0
	str	r1, [r0]

	/* the mask ROM code should have PLL and others stable */
#ifndef CONFIG_SKIP_LOWLEVEL_INIT
	bl	cpu_init_cp15
	bl	cpu_init_crit
#endif

可以看到做的工作有:进入SVC32管理模式,关闭看门狗,然后进入cpu_init_cp15和cpu_init_crit。其中跳转到cpu_init_cp15代码,在下图:

ENTRY(cpu_init_cp15)
	/*
	 * Invalidate L1 I/D
	 */
	mov	r0, #0			@ set up for MCR
	mcr	p15, 0, r0, c8, c7, 0	@ invalidate TLBs
	mcr	p15, 0, r0, c7, c5, 0	@ invalidate icache
	mcr	p15, 0, r0, c7, c5, 6	@ invalidate BP array
#ifndef CONFIG_MACH_S5P6818
	mcr p15, 0, r0, c7, c10, 4	@ DSB
	mcr p15, 0, r0, c7, c5, 4	@ ISB
#endif

	/*
	 * disable MMU stuff and caches
	 */
	mrc	p15, 0, r0, c1, c0, 0
	bic	r0, r0, #0x00002000	@ clear bits 13 (--V-)
	bic	r0, r0, #0x00000007	@ clear bits 2:0 (-CAM)
	orr	r0, r0, #0x00000002	@ set bit 1 (--A-) Align
	orr	r0, r0, #0x00000800	@ set bit 11 (Z---) BTB
#ifdef CONFIG_SYS_ICACHE_OFF
	bic	r0, r0, #0x00001000	@ clear bit 12 (I) I-cache
#else
	orr	r0, r0, #0x00001000	@ set bit 12 (I) I-cache
#endif
	mcr	p15, 0, r0, c1, c0, 0
	mov	pc, lr			@ back to my caller
ENDPROC(cpu_init_cp15)

里面主要做的工作是:通过访问cp15协处理器有关寄存器关闭I/D cache,关闭TLB,关闭MMU。

接入跳转到cpu_init_crit看看里面做了什么,代码如下图:

ENTRY(cpu_init_crit)
	/*
	 * Jump to board specific initialization...
	 * The Mask ROM will have already initialized
	 * basic memory. Go here to bump up clock rate and handle
	 * wake up conditions.
	 */
	b	lowlevel_init		@ go setup pll,mux,memory
ENDPROC(cpu_init_crit)
#endif

继续跳转到lowlevel_init ,它定义在arch\arm\cpu\slsiap\s5p6818\low_init.S里

	.globl lowlevel_init
lowlevel_init:

	/* get cpu id */
  	mrc     p15, 0, r0, c0, c0, 5     	@ Read CPU ID register
  	ands    r0, r0, #0x03             	@ Mask off, leaving the CPU ID field
  	mov     r1, #0xF                  	@ Move 0xF (represents all four ways) into r1

	/* join SMP */
  	mrc     p15, 0, r0, c1, c0, 1   	@ Read ACTLR
  	mov     r1, r0
  	orr     r0, r0, #0x040          	@ Set bit 6
  	cmp     r0, r1
  	mcrne   p15, 0, r0, c1, c0, 1   	@ Write ACTLR

	/* enable maintenance broadcast */
	mrc     p15, 0, r0, c1, c0, 1      	@ Read Aux Ctrl register
	mov     r1, r0
    orr     r0, r0, #0x01              	@ Set the FW bit (bit 0)
	cmp     r0, r1
    mcrne   p15, 0, r0, c1, c0, 1      	@ Write Aux Ctrl register

	mov	 pc, lr							@ back to caller

#endif /* CONFIG_SKIP_LOWLEVEL_INIT */

里面好像是做了获取cpu ID,加入SMP,启用维护广播,貌似不是重点,那么reset就执行完成了,返回Start.S中继续分析。

reset之后的代码如下图:

#ifdef CONFIG_RELOC_TO_TEXT_BASE
relocate_to_text:
	/*
	 * relocate u-boot code on memory to text base
	 * for nexell arm core (add by jhkim)
	 */
	adr	r0, _stext				/* r0 <- current position of code   */
	ldr	r1, TEXT_BASE			/* test if we run from flash or RAM */
	cmp r0, r1              	/* don't reloc during debug         */
	beq clear_bss

	ldr	r2, _bss_start_ofs
	add	r2, r0, r2				/* r2 <- source end address         */

copy_loop_text:
	ldmia	r0!, {r3-r10}		/* copy from source address [r0]    */
	stmia	r1!, {r3-r10}		/* copy to   target address [r1]    */
	cmp	r0, r2					/* until source end addreee [r2]    */
	ble	copy_loop_text

	ldr	r1, TEXT_BASE			/* restart at text base */
	mov pc, r1

clear_bss:
#ifdef CONFIG_MMU_ENABLE
	bl	mmu_turn_on
#endif
	ldr	r0, _bss_start_ofs
	ldr	r1, _bss_end_ofs
	ldr	r4, TEXT_BASE			/* text addr */
	add	r0, r0, r4
	add	r1, r1, r4
	mov	r2, #0x00000000			/* clear */

clbss_l:str	r2, [r0]			/* clear loop... */
	add	r0, r0, #4
	cmp	r0, r1
	bne	clbss_l

	ldr	sp, =(CONFIG_SYS_INIT_SP_ADDR)
	bic	sp, sp, #7					/* 8-byte alignment for ABI compliance */
	sub	sp, #GD_SIZE				/* allocate one GD above SP */
	bic	sp, sp, #7					/* 8-byte alignment for ABI compliance */
	mov	r9, sp						/* GD is above SP */
	mov	r0, #0
	bl	board_init_f

	mov	sp, r9						/* SP is GD's base address */
	bic	sp, sp, #7					/* 8-byte alignment for ABI compliance */
	sub	sp, #GENERATED_BD_INFO_SIZE	/* allocate one BD above SP */
	bic	sp, sp, #7					/* 8-byte alignment for ABI compliance */

	mov	r0, r9						/* gd_t *gd */
	ldr r1, TEXT_BASE				/* ulong text */
	mov r2, sp						/* ulong sp */
	bl	gdt_reset

	/* call board_init_r(gd_t *id, ulong dest_addr) */
	mov	r0, r9							/* gd_t */
	ldr	r1,  =(CONFIG_SYS_MALLOC_END)	/* dest_addr for malloc heap end */
	/* call board_init_r */
	ldr	pc, =board_init_r				/* this is auto-relocated! */

#else	/* CONFIG_RELOC_TO_TEXT_BASE */

	bl	_main
#endif

因为x6818.h配置文件中有定义CONFIG_RELOC_TO_TEXT_BASE,则接下来就是代码重定位了。
TEXT_BASE 是上面提到的宏,即0x43C00000,先判断代码是否运行在0x43C00000,即SDRAM中,如果不是就需要代码重定位,拷贝结束后pc跳转到TEXT_BASE ,重新执行代码。重新运行到重定位的位置时,判断到
_stext = TEXT_BASE 不需要再重定位了,执行清BSS段:

clear_bss:
#ifdef CONFIG_MMU_ENABLE
	bl	mmu_turn_on
#endif
	ldr	r0, _bss_start_ofs
	ldr	r1, _bss_end_ofs
	ldr	r4, TEXT_BASE			/* text addr */
	add	r0, r0, r4
	add	r1, r1, r4
	mov	r2, #0x00000000			/* clear */

clbss_l:str	r2, [r0]			/* clear loop... */
	add	r0, r0, #4
	cmp	r0, r1
	bne	clbss_l

清BSS段结束后,代码如下:

ldr	sp, =(CONFIG_SYS_INIT_SP_ADDR)
	bic	sp, sp, #7					/* 8-byte alignment for ABI compliance */
	sub	sp, #GD_SIZE				/* allocate one GD above SP */
	bic	sp, sp, #7					/* 8-byte alignment for ABI compliance */
	mov	r9, sp						/* GD is above SP */
	mov	r0, #0
	bl	board_init_f

	mov	sp, r9						/* SP is GD's base address */
	bic	sp, sp, #7					/* 8-byte alignment for ABI compliance */
	sub	sp, #GENERATED_BD_INFO_SIZE	/* allocate one BD above SP */
	bic	sp, sp, #7					/* 8-byte alignment for ABI compliance */

	mov	r0, r9						/* gd_t *gd */
	ldr r1, TEXT_BASE				/* ulong text */
	mov r2, sp						/* ulong sp */
	bl	gdt_reset

	/* call board_init_r(gd_t *id, ulong dest_addr) */
	mov	r0, r9							/* gd_t */
	ldr	r1,  =(CONFIG_SYS_MALLOC_END)	/* dest_addr for malloc heap end */
	/* call board_init_r */
	ldr	pc, =board_init_r				/* this is auto-relocated! */

从代码中可以看出,在_stext 下面开辟了gd数据区,然后调用board_init_f 函数,board_init_f 定义在common\board_f.c中,代码如下:

void board_init_f(ulong boot_flags)
{
#ifdef CONFIG_SYS_GENERIC_GLOBAL_DATA
	/*
	 * For some archtectures, global data is initialized and used before
	 * calling this function. The data should be preserved. For others,
	 * CONFIG_SYS_GENERIC_GLOBAL_DATA should be defined and use the stack
	 * here to host global data until relocation.
	 */
	gd_t data;

	gd = &data;

	/*
	 * Clear global data before it is accessed at debug print
	 * in initcall_run_list. Otherwise the debug print probably
	 * get the wrong vaule of gd->have_console.
	 */
	zero_global_data();
#endif

	gd->flags = boot_flags;
	gd->have_console = 0;

	if (initcall_run_list(init_sequence_f))
		hang();

#if !defined(CONFIG_ARM) && !defined(CONFIG_SANDBOX)
	/* NOTREACHED - jump_to_copy() does not return */
	hang();
#endif
}

board_init_f 进行了清global_data空间,初始化外设,init_sequence_f是一系列的初始化函数,通过配置文件的宏(条件判断)来调用这些函数。

接着再往下分析,在gd数据区下面又开辟了bd数据区,然后调用gdt_reset函数,定义在arch\arm\cpu\slsiap\s5p6818\cpu.c中,代码如下:

void gdt_reset(gd_t *gd, ulong text, ulong sp)
{
	ulong text_start, text_end, heap_end;
	ulong bd;

	/* for smp cores */
	global_descriptor = gd;

	/* reconfig stack info */
	gd->relocaddr = text;
	gd->start_addr_sp = sp;
	gd->reloc_off = 0;

	/* copy bd info  */
	bd = (unsigned int)gd - sizeof(bd_t);
	memcpy((void *)bd, (void *)gd->bd, sizeof(bd_t));

	/* reset gd->bd */
	gd->bd = (bd_t *)bd;

	/* prevent dataabort, when access enva_addr + data (0x04) */
	gd->env_addr = (ulong)default_environment;

	/* get cpu info */
	text_start = (unsigned int)(gd->relocaddr);
	text_end = (unsigned int)(gd->relocaddr + _bss_end_ofs);
	heap_end = CONFIG_SYS_MALLOC_END;

#if defined(CONFIG_SYS_GENERIC_BOARD)
	/* refer initr_malloc (common/board_r.c) */
	gd->relocaddr = heap_end;
#endif
	flush_dcache_all();

#if defined(CONFIG_DISPLAY_CPUINFO)
	ulong pc;

	asm("mov %0, pc":"=r" (pc));
	asm("mov %0, sp":"=r" (sp));

	printf("Heap = 0x%08lx~0x%08lx\n", heap_end-TOTAL_MALLOC_LEN, heap_end);
	printf("Code = 0x%08lx~0x%08lx\n", text_start, text_end);
	printf("GLD  = 0x%08lx\n", (ulong)gd);
	printf("GLBD = 0x%08lx\n", (ulong)gd->bd);
	printf("SP   = 0x%08lx,0x%08lx(CURR)\n", gd->start_addr_sp, sp);
	printf("PC   = 0x%08lx\n", pc);

	printf("TAGS = 0x%08lx \n", gd->bd->bi_boot_params);
	#ifdef CONFIG_MMU_ENABLE
	ulong page_tlb =  (text_end & 0xffff0000) + 0x10000;
	printf("PAGE = 0x%08lx~0x%08lx\n", page_tlb, page_tlb + 0xc000 );
	#endif
	#ifdef CONFIG_SMP
	printf("SMPSP= 0x%08x (-0x%08x)\n", CONFIG_SYS_SMP_SP_ADDR, CONFIG_SYS_SMP_SP_SIZE*(NR_CPUS-1));
	#endif
	printf("MACH = [%ld]   \n", gd->bd->bi_arch_number);
	printf("VER  = %u      \n", nxp_cpu_version());
	printf("BOARD= [%s]    \n", CONFIG_SYS_BOARD);
#endif
}

可以看到里面对gd结构体的成员变量进行初始化,并打印出SDRAM的内存分配信息,即在SecureCRT中,启动u-boot时看到的信息:
在这里插入图片描述
接着继续在Start.S往下分析,看到调用了board_init_r函数,board_init_r定义正在common\board_r.c中,代码如下:

void board_init_r(gd_t *new_gd, ulong dest_addr)
{
#ifdef CONFIG_NEEDS_MANUAL_RELOC
	int i;
#endif

#ifndef CONFIG_X86
	gd = new_gd;
#endif

#ifdef CONFIG_NEEDS_MANUAL_RELOC
	for (i = 0; i < ARRAY_SIZE(init_sequence_r); i++)
		init_sequence_r[i] += gd->reloc_off;
#endif

	if (initcall_run_list(init_sequence_r))
		hang();

	/* NOTREACHED - run_main_loop() does not return */
	hang();
}

init_sequence_r也是一系列函数的集合,跟init_sequence_f差不多,进一步对外设进行初始化,里面最后一个函数是run_main_loop:

static int run_main_loop(void)
{
#ifdef CONFIG_SANDBOX
	sandbox_main_loop_init();
#endif
	/* main_loop() can return to retry autoboot, if so just run it again */
	for (;;)
		main_loop();
	return 0;
}

for (;;)是一个死循环,表示一直执行main_loop(),main_loop代码如下:

void main_loop(void)
{
	const char *s;

	bootstage_mark_name(BOOTSTAGE_ID_MAIN_LOOP, "main_loop");

#ifndef CONFIG_SYS_GENERIC_BOARD
	puts("Warning: Your board does not use generic board. Please read\n");
	puts("doc/README.generic-board and take action. Boards not\n");
	puts("upgraded by the late 2014 may break or be removed.\n");
#endif

	modem_init();
#ifdef CONFIG_VERSION_VARIABLE
	setenv("ver", version_string);  /* set version variable */
#endif /* CONFIG_VERSION_VARIABLE */

	cli_init();

	run_preboot_environment_command();

#if defined(CONFIG_UPDATE_TFTP)
	update_tftp(0UL);
#endif /* CONFIG_UPDATE_TFTP */

	s = bootdelay_process();
	if (cli_process_fdt(&s))
		cli_secure_boot_cmd(s);

	autoboot_command(s);

	cli_loop();
}

其中
->bootstage_mark_name是打印启动进度
->setenv(“ver”, version_string)是设置版本号环境变量
-> cli_init()跟命令初始化有关,初始化 hushshell 相关的变量
->run_preboot_environment_command()获取环境变量 perboot 的内容
->bootdelay_process 函数,此函数会读取环境变量 bootdelay 和 bootcmd 的内容, 然后将 bootdelay 的值赋值给全局变量stored_bootdelay,返回值为环境变量 bootcmd 的值
->autoboot_command 函数,此函数就是检查倒计时是否结束?倒计时结束之前有
没有被打断?,3秒倒计时后启动Linux内核的功能就是这里实现的,参数s保存着bootcmd 的值,即启动Linux命令,如果倒计时结束就会执行这个命令。如果在倒计时按下键盘上的按键,命令就不会执行,autoboot_command相当于一个空函数。
->cli_loop(),这个就是命令处理函数,负责接收好处理输入的命令,从此进入了u-boot的命令行。

cli_loop函数定义在common\cli.c,代码如下:

void cli_loop(void)
{
#ifdef CONFIG_SYS_HUSH_PARSER
	parse_file_outer();
	/* This point is never reached */
	for (;;);
#else
	cli_simple_loop();
#endif /*CONFIG_SYS_HUSH_PARSER*/
}

在x6818.h中定义了CONFIG_SYS_HUSH_PARSER,即执行parse_file_outer()函数,parse_file_outer定义在common\cli_hush.c中,代码如下:

#ifndef __U_BOOT__
static int parse_file_outer(FILE *f)
#else
int parse_file_outer(void)
#endif
{
	int rcode;
	struct in_str input;
#ifndef __U_BOOT__
	setup_file_in_str(&input, f);
#else
	setup_file_in_str(&input);
#endif
	rcode = parse_stream_outer(&input, FLAG_PARSE_SEMICOLON);
	return rcode;
}

其中parse_stream_outer函数的作用是负责接收命令行输入,然后解析并执行相应的命令,它定义在common\cli_hush.c中,代码如下:

static int parse_stream_outer(struct in_str *inp, int flag)
{

	struct p_context ctx;
	o_string temp=NULL_O_STRING;
	int rcode;
#ifdef __U_BOOT__
	int code = 0;
#endif
	do {
		ctx.type = flag;
		initialize_context(&ctx);
		update_ifs_map();
		if (!(flag & FLAG_PARSE_SEMICOLON) || (flag & FLAG_REPARSING)) mapset((uchar *)";$&|", 0);
		inp->promptmode=1;
		rcode = parse_stream(&temp, &ctx, inp, '\n');
#ifdef __U_BOOT__
		if (rcode == 1) flag_repeat = 0;
#endif
		if (rcode != 1 && ctx.old_flag != 0) {
			syntax();
#ifdef __U_BOOT__
			flag_repeat = 0;
#endif
		}
		if (rcode != 1 && ctx.old_flag == 0) {
			done_word(&temp, &ctx);
			done_pipe(&ctx,PIPE_SEQ);
#ifndef __U_BOOT__
			run_list(ctx.list_head);
#else
			code = run_list(ctx.list_head);
			if (code == -2) {	/* exit */
				b_free(&temp);
				code = 0;
				/* XXX hackish way to not allow exit from main loop */
				if (inp->peek == file_peek) {
					printf("exit not allowed from main input shell.\n");
					continue;
				}
				break;
			}
			if (code == -1)
			    flag_repeat = 0;
#endif
		} else {
			if (ctx.old_flag != 0) {
				free(ctx.stack);
				b_reset(&temp);
			}
#ifdef __U_BOOT__
			if (inp->__promptme == 0) printf("<INTERRUPT>\n");
			inp->__promptme = 1;
#endif
			temp.nonnull = 0;
			temp.quote = 0;
			inp->p = NULL;
			free_pipe_list(ctx.list_head,0);
		}
		b_free(&temp);
	/* loop on syntax errors, return on EOF */
	} while (rcode != -1 && !(flag & FLAG_EXIT_FROM_LOOP) &&
		(inp->peek != static_peek || b_peek(inp)));
#ifndef __U_BOOT__
	return 0;
#else
	return (code != 0) ? 1 : 0;
#endif /* __U_BOOT__ */
}

里面的do-while循环就是处理输入命令的:
->函数 parse_stream 进行命令解析
->调用 run_list 函数来执行解析出来的命令
->函数 run_list 会经过一系列的函数调用,最终通过调用 cmd_process 函数来处理命令

cmd_process函数 定义在common\command.c中,代码如下:

enum command_ret_t cmd_process(int flag, int argc, char * const argv[],
			       int *repeatable, ulong *ticks)
{
	enum command_ret_t rc = CMD_RET_SUCCESS;
	cmd_tbl_t *cmdtp;

	/* Look up command in command table */
	cmdtp = find_cmd(argv[0]);
	if (cmdtp == NULL) {
		printf("Unknown command '%s' - try 'help'\n", argv[0]);
		return 1;
	}

	/* found - check max args */
	if (argc > cmdtp->maxargs)
		rc = CMD_RET_USAGE;

#if defined(CONFIG_CMD_BOOTD)
	/* avoid "bootd" recursion */
	else if (cmdtp->cmd == do_bootd) {
		if (flag & CMD_FLAG_BOOTD) {
			puts("'bootd' recursion detected\n");
			rc = CMD_RET_FAILURE;
		} else {
			flag |= CMD_FLAG_BOOTD;
		}
	}
#endif

	/* If OK so far, then do the command */
	if (!rc) {
		if (ticks)
			*ticks = get_timer(0);
		rc = cmd_call(cmdtp, flag, argc, argv);
		if (ticks)
			*ticks = get_timer(*ticks);
		*repeatable &= cmdtp->repeatable;
	}
	if (rc == CMD_RET_USAGE)
		rc = cmd_usage(cmdtp);
	return rc;
}

首先通过调用函数 find_cmd 在命令表中找到指定的命令,命令表其实是cmd_tbl_t 结构体数组,在下一篇博客如何自定义命令会提及到。然后调用 cmd_call 函数执行具体的命令,cmd_call 函数其实就是调用cmd_tbl_t 结构体的 cmd成员——do_xxx命令函数,最终执行这个命令。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值