ARM Linux 驱动入门及手把手教你简单驱动书写

0. 本文会讲解的内容

  1. 自己写驱动需要的材料
  2. 简单的介绍编译驱动的Makefile文件
  3. 写一个简单的驱动,并告知如何查看是否正确加载
  4. 介绍驱动常用的几个回调指针
  5. 写一个驱动,使用ioctl,将应用态和内核态进行交互

1. 自己写驱动需要的材料

  • 对应平台的交叉工具链,本文中的工具链是(arm-at91-linux-gnueabi-gcc)
  • 对应平台的可编译的内核,即进行过menuconfig的内核源码。(与网上下的内核源码包的主要区别是menuconfig过后会生成一个.config文件)

2. 简单的介绍编译驱动的Makefile文件

驱动的Makefile不像是应用层makefile那么千奇百怪,可以说驱动的Makefile就是一个固定的模板,复制过来用就完事了。

驱动的Makefile

# Makefile
ifneq ($(KERNELRELEASE),)
        obj-m := xyy.o
else
        #KERNELDIR ?= /lib/modules/$(shell uname -r)/build
        KERNELDIR ?= /path_to_linux_src/linux-at91-linux-2.6.39-at91-20160713
        PWD := $(shell pwd)
default: clean
	make -C $(KERNELDIR) M=$(PWD) modules ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabi-
	rm -rf *.o .*.cmd .tmp_versions  *.mod.c modules.order Module.symvers *.ko.unsigned
clean:
	$(RM) -r *.ko *.o .*.cmd .tmp_versions  *.mod.c modules.order Module.symvers *.ko.unsigned
endif

简单讲讲这个Makefile如何用

这里需要修改的总共就三处

  1. 就是obj-m后面的.o的文件名字,改成对应的你自己驱动的.c文件的名字;
  2. KERNELDIR改成linux内核源码的文件夹地址即可,记住,要是menuconfig过的。
  3. CROSS_COMPILE这个变量改成你板子交叉编译工具链的前缀。

这三处改好之后,就可以直接在makefile的当前目录输入make执行了。

这里需要注意,为什么是obj-m:

  • obj-y +=xxx.o该模块编译到zImage

  • obj-m +=xxx.o该模块不会编译到zImage,但会生成一个独立的xxx.ko 静态编译

如果这里使用obj-y则make之后不会发生任何事情。

简单的讲讲为什么会是obj-m这个玩意

我们可以简单的看一个内核源码中的某个组件的Makefile文件,这里以/arch/arm/mach-at91/Makefile为例子:

# CPU-specific support
obj-$(CONFIG_ARCH_AT91RM9200)	+= at91rm9200.o at91rm9200_time.o at91rm9200_devices.o
obj-$(CONFIG_ARCH_AT91SAM9260)	+= at91sam9260.o at91sam926x_time.o at91sam9260_devices.o sam9_smc.o at91sam9_alt_reset.o
obj-$(CONFIG_ARCH_AT91SAM9261)	+= at91sam9261.o at91sam926x_time.o at91sam9261_devices.o sam9_smc.o at91sam9_alt_reset.o
obj-$(CONFIG_ARCH_AT91SAM9G10)	+= at91sam9261.o at91sam926x_time.o at91sam9261_devices.o sam9_smc.o at91sam9_alt_reset.o
obj-$(CONFIG_ARCH_AT91SAM9263)	+= at91sam9263.o at91sam926x_time.o at91sam9263_devices.o sam9_smc.o at91sam9_alt_reset.o
obj-$(CONFIG_ARCH_AT91SAM9RL)	+= at91sam9rl.o at91sam926x_time.o at91sam9rl_devices.o sam9_smc.o at91sam9_alt_reset.o
obj-$(CONFIG_ARCH_AT91SAM9G20)	+= at91sam9260.o at91sam926x_time.o at91sam9260_devices.o sam9_smc.o at91sam9_alt_reset.o
obj-$(CONFIG_ARCH_AT91SAM9G45)	+= at91sam9g45.o at91sam926x_time.o at91sam9g45_devices.o sam9_smc.o
obj-$(CONFIG_ARCH_AT91SAM9X5)	+= at91sam9x5.o at91sam926x_time.o at91sam9x5_devices.o sam9_smc.o
obj-$(CONFIG_ARCH_AT91SAM9N12)	+= at91sam9n12.o at91sam926x_time.o at91sam9n12_devices.o sam9_smc.o
obj-$(CONFIG_ARCH_AT91CAP9)	+= at91cap9.o at91sam926x_time.o at91cap9_devices.o sam9_smc.o
obj-$(CONFIG_ARCH_AT572D940HF)  += at572d940hf.o at91sam926x_time.o at572d940hf_devices.o sam9_smc.o
obj-$(CONFIG_ARCH_AT91X40)	+= at91x40.o at91x40_time.o

可以看到有很多obj-${xxxx}的Makefile变量,而这些变量使用的宏,实际上就是在menuconfig里进行选择的时候进行了复制,我们可以在.config文档中找到端倪。

# Automatically generated make config: don't edit
# Linux/arm 2.6.39 Kernel Configuration
# Wed Mar 27 14:37:50 2019
#
CONFIG_ARM=y
CONFIG_SYS_SUPPORTS_APM_EMULATION=y
CONFIG_GENERIC_GPIO=y
# CONFIG_ARCH_USES_GETTIMEOFFSET is not set
CONFIG_GENERIC_CLOCKEVENTS=y
CONFIG_KTIME_SCALAR=y
CONFIG_HAVE_PROC_CPU=y
CONFIG_STACKTRACE_SUPPORT=y
CONFIG_HAVE_LATENCYTOP_SUPPORT=y
CONFIG_LOCKDEP_SUPPORT=y
CONFIG_TRACE_IRQFLAGS_SUPPORT=y
CONFIG_HARDIRQS_SW_RESEND=y
CONFIG_GENERIC_IRQ_PROBE=y

其实menuconfig的过程就是给Makefile里的这些值进行了赋值,将不同的目标文件进行有规则的分类。俗称内核裁剪,这部分不在这里过多赘述。

3. 写一个简单的驱动,并告知如何查看是否正确加载

/*
 * @Author: Adam Xiao
 * @Date: 2021-03-23 19:40:28
 * @LastEditors: Adam Xiao
 * @LastEditTime: 2021-03-25 10:39:18
 * @FilePath: /test/xyy.c
 */
// xyy.c
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/device.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/types.h>
#include <linux/errno.h>
#include <linux/slab.h>
#include <asm/gpio.h>
#include <asm/uaccess.h>  
#include <linux/delay.h>
#include <linux/spinlock.h>
#include <linux/miscdevice.h>
#include <linux/irq.h>
#include <linux/wait.h>
#include <linux/sched.h>
#include <linux/interrupt.h>
#include <linux/version.h>
#include <linux/timer.h>
#include <linux/timex.h>
#include <linux/rtc.h>
#include <linux/workqueue.h>
#include <asm/atomic.h>

struct miscdevice misc_dev =
{
	.minor = MISC_DYNAMIC_MINOR,
	.name = "Adam",
};

static int __init test_init(void)
{
	int ret = misc_register(&misc_dev);
	if (ret) {
		printk(KERN_ERR "[xyy]misc_register error\n");
	} else {
		printk(KERN_INFO "[xyy]test driver init\n");
	}
	return ret;
}

static void __exit test_exit(void)
{
	misc_deregister(&misc_dev);
	printk(KERN_INFO "[xyy]test driver exit\n");
}

module_init(test_init);
module_exit(test_exit);

MODULE_AUTHOR("XXXXXBBBBB Corporation");
MODULE_DESCRIPTION("XXXXXXXX machine main controller board IO test driver");
MODULE_LICENSE("Dual BSD/GPL");
MODULE_VERSION("1.0.0");

测试源码如上所述,直接进行make

xyy@test$make
rm -f -r *.ko *.o .*.cmd .tmp_versions  *.mod.c modules.order Module.symvers *.ko.unsigned
make -C /XXXXXXXXXXXXXXXXXXXXX/linux-at91-linux-2.6.39-at91-20160713 M=/XXXXXXXXXXXXXXXXXXXXX/test modules ARCH=arm CROSS_COMPILE=arm-at91-linux-gnueabi-
make[1]: Entering directory '/XXXXXXXXXXXXXXXXXXXXX/linux-at91-linux-2.6.39-at91-20160713'
  CC [M]  /XXXXXXXXXXXXXXXXXXXXX/test/xyy.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC      /XXXXXXXXXXXXXXXXXXXXX/test/xyy.mod.o
  LD [M]  /XXXXXXXXXXXXXXXXXXXXX/test/xyy.ko
make[1]: Leaving directory '/XXXXXXXXXXXXXXXXXXXXX/linux-at91-linux-2.6.39-at91-20160713'
rm -rf *.o .*.cmd .tmp_versions  *.mod.c modules.order Module.symvers *.ko.unsigned

令人期待的第一个驱动新鲜出炉了。这里学习三个简单的指令

  • insmod:加载对应驱动
  • rmmod:卸载对应驱动
  • dmesg:查看内核打印,-c是清除历史打印缓存,但是会显示最后一次的缓存,显示完后才会清除。

我们尝试一下,刚刚编译好的驱动xyy.ko

# insmod xyy.ko
# dmesg -c
[xyy]test driver init
# rmmod xyy.ko
# dmesg -c
[xyy]test driver exit

# ls /dev/Adam -laht
crw-rw----    1 root     root       10,  57 Mar 25 14:25 /dev/Adam

可以看到,这个驱动在加载进系统的时候,内核打印了我们编码的内容。在卸载驱动的时候,也打印了卸载驱动的相关内容。并且在/dev目录下,多了一个名为Adam的设备文件。

请注意10,57这个数字,后面会详细讲解这两个数字的含义。

4. 介绍驱动程序的接口,以及常用的几个回调指针

上面的驱动能够简单的运行起来,说明我们的框架是正确的。那么简单的讲解一下,内核里面的一些特有接口。

4.1 MODULE_AUTHOR系列宏

如同代码中最后部分有一些这样的宏,其实就是说明你的驱动的归属。

MODULE_AUTHOR("XXXXXBBBBB Corporation");
MODULE_DESCRIPTION("XXXXXXXX machine main controller board IO test driver");
MODULE_LICENSE("Dual BSD/GPL");
MODULE_VERSION("1.0.0");

都不是强制的,但是有说法是这样的:

不是严格要求的, 但是你的模块确实应当指定它的代码使用哪个许可. 做到这一点只需包含一行 MODULE_LICENSE: MODULE_LICENSE(“GPL”); 内核认识的特定许可有, “GPL”( 适用 GNU 通用公共许可的任何版本 ), “GPL v2”( 只适用 GPL 版本 2 ), “GPL and additional rights”, “Dual BSD/GPL”, “Dual MPL/GPL”, 和 “Proprietary”. 除非你的模块明确标识是在内核认识的一个自由许可下, 否则就假定它是私有的, 内核在模块加载时被"弄污浊"了。 象我们在第 1 章"许可条款"中提到的, 内核开发者不会热心帮助在加载了私有模块后遇到问题的用户。

总之就是鼓励大家还是有开源精神。

4.2 module_initmodule_exit

这两个本质也是宏。用C++的概念理解这个,可以当成构造函数和析构函数去理解,驱动加载的时候运行的函数,用module_init这个接口注册,驱动卸载的时候运行的函数,用module_exit这个接口注册。那么在驱动进行对应的操作的时候,对应的函数就会被运行。

这两个函数,相当于驱动的入口和出口函数。

4.3 misc_registermisc_deregister

这里先介绍两个概念,杂项设备(misc_device)和字符设备(char_device)。

几种设备类型

  • 杂项设备 misc_device

杂项设备也是在嵌入式系统中用得比较多的一种设备驱动。在 Linux内核的include/linux目录下有Miscdevice.h文件,要把自己定义的miscdevice从设备定义在这里。其实是因为这些字符设备不符合预先确定的字符设备范畴,所有这些设备采用主编号10,一起归于miscdevice,其实misc_register就是用主标号10调用register_chrdev()的。

也就是说,misc设备其实也就是特殊的字符设备,可自动生成设备节点。

  • 字符设备(char_device)

使用register_chrdev(LED_MAJOR,DEVICE_NAME,&dev_fops)注册字符设备驱动程序时,如果有多个设备使用该函数注册驱动程序,LED_MAJOR不能相同,否则几个设备都无法注册。如果模块使用该方式注册并且LED_MAJOR为0(自动分配主设备号),使用insmod命令加载模块时会在终端显示分配的主设备号和次设备号,在/dev目录下建立该节点,比如设备leds,如果加载该模块时分配的主设备号和次设备号为253和0,则建立节点时使用如下语句:
mknod leds c 253 0

使用register_chrdev(LED_MAJOR,DEVICE_NAME,&dev_fops)注册字符设备驱动程序时要手动建立节点,否则在应用程序无法打开该设备。

主要区别体现

杂项设备注册和加载较为方便,不用手动指定主次设备号:

insmod xyy.ko

这样就完成加载了

字符设备注册的时候,需要手动指定设备号,并且加载的时候,需要调用mknod建立节点:

insmod Adam.ko
mknod /dev/Adam c 234 0

mknod 的标准形式为: mknod DEVNAME {b | c} MAJOR MINOR

  1. DEVNAME是要创建的设备文件名,如果想将设备文件放在一个特定的文件夹下,就需要先用mkdir在dev目录下新建一个目录;
  2. b和c 分别表示块设备和字符设备:
    b表示系统从块设备中读取数据的时候,直接从内存的buffer中读取数据,而不经过磁盘;
    c表示字符设备文件与设备传送数据的时候是以字符的形式传送,一次传送一个字符,比如打印机、终端都是以字符的形式传送数据;
  3. MAJOR和MINOR分别表示主设备号和次设备号:
    为了管理设备,系统为每个设备分配一个编号,一个设备号由主设备号和次设备号组成。主设备号标示某一种类的设备,次设备号用来区分同一类型的设备。linux操作系统中为设备文件编号分配了32位无符号整数,其中前12位是主设备号,后20位为次设备号,所以在向系统申请设备文件时主设备号不好超过4095,次设备号不好超过2^20 -1。

这里需要注意的是,mknod的主次设备号需要与驱动代码中注册的时候,保持一致。

5. 写一个驱动,使用ioctl,将应用态和内核态进行交互

上面的驱动虽然是正确的,但是实际上这个驱动什么都没有做。

回到驱动的本质,驱动是要干什么呢?

驱动是要用来操控硬件的

因为用户层没有权限直接操控硬件,只有内核才能做这个事情,所以我们需要一个桥梁,来通知内核,我想操控什么硬件,用这个硬件来读取或者写入什么东西。

一般来说,读取设备或者读取芯片的值之类的东西,厂家都会给接口样例,例如:

at91_set_gpio_value(CPLD_ADDR_4,1);
at91_set_gpio_value(CPLD_ADDR_1,1);

类似这样的接口其实一般字面意思就能猜出来是干了什么,上面的两个接口就是给gpio的某个管脚设置了1这个值,反正不是拉高就是拉低了电平嘛。

这种接口只能在内核态用,所以需要写一个驱动,完成一定的功能,然后把这些获取/设置的值给应用态的程序。每个驱动都是做这个事情的。

5.1 先上源码

这个debug.h主要是用来辅助打印的,将应用层和内核层的打印统一一致。

//debug.h
#ifndef __DEBUG_H__
#define __DEBUG_H__


#define COL_DEF "\033[m"
#define COL_RED "\033[0;32;31m"
#define COL_GRE "\033[0;32;32m"
#define COL_BLU "\033[0;32;34m"
#define COL_YEL "\033[1;33m"

#ifdef __KERNEL__	//for linux driver
	#define ERR(fmt, ...) printk(KERN_ERR COL_RED "driver error function[%s]:"\
				COL_YEL fmt COL_DEF "\n", __func__, ##__VA_ARGS__)
	#define INFO(fmt, ...) printk(KERN_INFO COL_GRE "driver information:"\
				COL_YEL	fmt COL_DEF "\n", ##__VA_ARGS__)

	#ifdef DEBUG
	#define DBG(fmt, ...) printk(KERN_DEBUG COL_BLU "debug function[%s]:"\
				COL_DEF	fmt, __func__, ##__VA_ARGS__)
	#else
	#define DBG(fmt, ...) ({0;})
	#endif
#else		//for linux application
	#define ERR(fmt, ...) printf(COL_RED "error function[%s]:"\
				COL_YEL fmt COL_DEF "\n", __func__, ##__VA_ARGS__)
	#define INFO(fmt, ...) printf(COL_GRE "information:"\
				COL_YEL	fmt COL_DEF "\n", ##__VA_ARGS__)

	#ifdef DEBUG
	#define DBG(fmt, ...) printf(COL_BLU "debug function[%s]:"\
				COL_DEF	fmt, __func__, ##__VA_ARGS__)
	#else
	#define DBG(fmt, ...) ({0;})
	#endif
#endif
#endif

这里是驱动的核心代码。

/*
 * @Author: Adam Xiao
 * @Date: 2021-03-23 19:40:28
 * @LastEditors: Adam Xiao
 * @LastEditTime: 2021-03-25 17:04:13
 * @FilePath: /test/xyy.c
 */
// xyy.c
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/device.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/types.h>
#include <linux/errno.h>
#include <linux/slab.h>
#include <asm/gpio.h>
#include <asm/uaccess.h>  
#include <linux/delay.h>
#include <linux/spinlock.h>
#include <linux/miscdevice.h>
#include <linux/irq.h>
#include <linux/wait.h>
#include <linux/sched.h>
#include <linux/interrupt.h>
#include <linux/version.h>
#include <linux/timer.h>
#include <linux/timex.h>
#include <linux/rtc.h>
#include <linux/workqueue.h>
#include <asm/atomic.h>
#include "debug.h"


static int io_open(struct inode *inode, struct file *filp)
{
	INFO("[xyy] Open called!!!");
	return 0;
}

static int io_release(struct inode *inode, struct file *file)
{
	INFO("[xyy] close called!!!");
	return 0;
}

struct test {
	int a;
	short b;
	char c;
};

long io_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
	INFO("cmd = %#x\n", cmd);
	INFO("arg:%p\n", (void *)arg);
	struct test t = {0, 0, 0};
	int ret = copy_from_user(&t, (struct test *)arg, sizeof(t));
	INFO("ret = %d, t.a = %d, t.b = %d, t.c = %d\n", ret, t.a, t.b, t.c);

	t.a = 111;
	t.b = 222;
	t.c = 33;
	(void) copy_to_user((struct test *)arg, &t, sizeof(t));
	return 0;
}

struct file_operations io_ops = 
{
    .owner = THIS_MODULE,
    .release = io_release,
    .open = io_open,
#if 0
	.read = irq_read,
#endif
#if LINUX_VERSION_CODE > KERNEL_VERSION(2,6,18)
	.unlocked_ioctl = io_ioctl, 
#else
    .ioctl = io_ioctl,
#endif
};

struct miscdevice misc_dev =
{
	.minor = MISC_DYNAMIC_MINOR,
	.name = "Adam",
	.fops = &io_ops,
};

static int __init test_init(void)
{
	int ret = misc_register(&misc_dev);
	if (ret) {
		printk(KERN_ERR "[xyy]misc_register error\n");
	} else {
		printk(KERN_INFO "[xyy]test driver init\n");
	}
	return ret;
}

static void __exit test_exit(void)
{
	misc_deregister(&misc_dev);
	printk(KERN_INFO "[xyy]test driver exit\n");
}

module_init(test_init);
module_exit(test_exit);

MODULE_AUTHOR("XXXXXBBBBB Corporation");
MODULE_DESCRIPTION("XXXXXXXX machine main controller board IO test driver");
MODULE_LICENSE("Dual BSD/GPL");
MODULE_VERSION("1.0.0");

可以看到,和之前的代码相比,我们主要在杂项设备里多注册了一个file_operations这个结构体,这个结构体是干嘛的呢?我们知道,linux的精神就是一切东西都是文件,对于驱动的设备也一样,所以也是当做一个文件来操控。

struct file_operations io_ops = 
{
    .owner = THIS_MODULE,
    .release = io_release,
    .open = io_open,
#if 0
	.read = irq_read,
#endif
#if LINUX_VERSION_CODE > KERNEL_VERSION(2,6,18)
	.unlocked_ioctl = io_ioctl, 
#else
    .ioctl = io_ioctl,
#endif
};

这里看成员的定义就能大胆的猜想,无非是注册了很多个动作对应的函数嘛,例如open这个设备的时候,会执行io_open这个函数,ioctl的时候,会执行io_ioctl这个函数,close的时候,会执行io_release这个函数。

我们简单的写个测试程序验证一下我们的猜想:

/*
 * @Author: Adam Xiao
 * @Date: 2021-03-22 16:48:27
 * @LastEditors: Adam Xiao
 * @LastEditTime: 2021-03-25 16:20:37
 * @FilePath: /test/test_Adam.c
 */
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

struct test {
	int a;
	short b;
	char c;
};

int main(void)
{
	int fd = open("/dev/Adam", O_RDONLY);
	if (fd < 0) {
		puts("open fail");
		return -1;
	}
	
	struct test t = {100, 20, 10};
	printf("tt:%p\n", &t);
	int ret = ioctl(fd, 0x11223344, &t);
	if (ret != 0) {
		puts("ioctl fail");
	} else {
		printf("ret = %d, t.a = %d, t.b = %d, t.c = %d\n", ret, t.a, t.b, t.c);
	}
	close(fd);
	return 0;
}

可以看到,我们打开了我们驱动添加的这个/dev/Adam这设备,并对他进行了一次ioctl。看看这个时候,都有什么结果。

# ./test_Adam
tt:0xbeecdbd0
ret = 0, t.a = 111, t.b = 222, t.c = 33

# dmesg
[xyy]test driver init
driver information:[xyy] Open called!!!
driver information:cmd = 0x11223344

driver information:arg:beecdbd0

driver information:ret = 0, t.a = 100, t.b = 20, t.c = 10

driver information:[xyy] close called!!!

可以看到,当我们open自己刚刚加载的驱动,/dev/Adam这设备的时候,内核的确有打印Open Called!!!。和我们的预期一样。这些函数的确都注册成功了,具体的还有read等函数可以注册,深入的知识这里就不展开了,大家可以自行查阅file_operations这个结构体,所有对设备能进行的操作,这里都有对应的函数可以进行注册。

5.2 copy_from_usercopy_to_user

细心的同学也许注意到了,驱动层面有了这两个函数非要转换一下,不知道是什么意思。

这里涉及到驱动的另一个重要概念,内核态是不能直接访问应用态数据的地址的,那么怎么办呢?内核提供了copy_from_usercopy_to_user这两个接口,作用就是用来在内核态和用户态传递数据。这里就不具体展开了,和我们常用的strcpy的作用其实是类似的,无非是从内核态搬数据到用户态罢了。

6. 后续要做的概述

完成了上面的例子,其实已经把驱动是如何在系统注册,如何在应用态打开,如何在应用态设置,如何传递到内核里,然后内核如何把想传递的东西返回给应用。这个过程简单的演示了一下,但是绝大部分驱动其实做的都是这样的事情,无非里面调用的厂家接口更多,完成的业务更复杂而已。

例如点了一个灯,或者从某个接口读出来了数据要返回出来。那么也都是在file_operations里注册好对应的动作,无论是ioctl也罢,read也罢。注册好对应的函数,然后实现功能,通过copy_to_user传递给应用态罢了。

7. 参考文档

  1. MODULE_AUTHOR宏(二)

  2. 杂项设备和字符设备的区别

  • 3
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值