I2C总线驱动——ap3216c光感传感器从寄存器手册开始入手的实战版(附思维导图)


最近公司项目用到I2C陀螺仪,之前也学习过I2C子系统这块,但是稍微淡忘了些,所以特地来补一下这块,顺便整理成博客。
自己画的思维导图:
image.png
关于I2C通信时序,可用直接看第三章节中的链接

1.I2C驱动框架简介

IIC驱动框架,也是一个标准的platform驱动,其中分为I2C总线驱动和I2C设备驱动
I2C总线驱动:( 适配器驱动 )一般是原厂维护,主要是提供读写等API
I2C设备驱动:是针对具体I2C设备所编写的驱动

IIC总线驱动也遵循驱动、设备、总线的匹配规则,设备和驱动匹配成功后,probe函数便会执行,probe函数在i2c_driver中,我们主要需要实现的也是driver这部分。
其中i2c_device_match完成IIC总线的设备和驱动匹配。
i2c_client:描述设备信息
i2c_driver:描述驱动信息
i2c_adapter:I2C总线驱动

这里借用一张其他地方看到的图,描述的比较清晰
image.png

1.1 I2C总线驱动(适配器驱动)

i2c总线驱动一般由原厂完成,主要工作是初始化i2c_adapter结构体,设置i2c_algorithm中的master_xfer函数(i2c适配器传输函数,该函数完成IIC通信),再通过i2c_add_adapter向系统注册i2c_adapter结构体。

1.1.1 重要结构体

i2c_adapter:i2c适配器

struct i2c_adapter {
    struct module *owner;
    unsigned int class;       /* classes to allow probing for */
    const struct i2c_algorithm *algo; /* the algorithm to access the bus */
    void *algo_data;

    /* data fields that are valid for all devices   */
    struct rt_mutex bus_lock;

    int timeout;            /* in jiffies */
    int retries;
    struct device dev;      /* the adapter device */

    int nr;
    char name[48];
    struct completion dev_released;

    struct mutex userspace_clients_lock;
    struct list_head userspace_clients;

    struct i2c_bus_recovery_info *bus_recovery_info;
    const struct i2c_adapter_quirks *quirks;
};

ii2c_algorithm:总线访问算法

struct i2c_algorithm {
	/* If an adapter algorithm can't do I2C-level access, set master_xfer
	   to NULL. If an adapter algorithm can do SMBus access, set
	   smbus_xfer. If set to NULL, the SMBus protocol is simulated
	   using common I2C messages */
	/* master_xfer should return the number of messages successfully
	   processed, or a negative value on error */
	int (*master_xfer)(struct i2c_adapter *adap, struct i2c_msg *msgs,
			   int num);
	int (*smbus_xfer) (struct i2c_adapter *adap, u16 addr,
			   unsigned short flags, char read_write,
			   u8 command, int size, union i2c_smbus_data *data);

	/* To determine what the adapter supports */
	u32 (*functionality) (struct i2c_adapter *);

};

functionality:返回I2C适配器支持什么样的通信协议

1.1.2 重要函数

/// 向Linux内核添加i2c适配器
/// 返回值0成功,负值失败
int i2c_add_adapter(struct i2c_adapter *adapter)	/// 动态总线号
int i2c_add_numbered_adapter(struct i2c_adapter *adap) /// 静态总线号
/// 删除Linux内核适配器
void i2c_del_adapter(struct i2c_adapter * adap)

1.2 I2C设备驱动

1.2.1 重要结构体

i2c_client:描述设备信息,每检测到一个i2c设备,分配一个i2c_client

struct i2c_client {
	unsigned short flags;		/* div., see below		*/
	unsigned short addr;		/* chip address - NOTE: 7bit	*/
					/* addresses are stored in the	*/
					/* _LOWER_ 7 bits		*/
	char name[I2C_NAME_SIZE];
	struct i2c_adapter *adapter;	/* the adapter we sit on	*/
	struct device dev;		/* the device structure		*/
	int irq;			/* irq issued by device		*/
	struct list_head detected;
#if IS_ENABLED(CONFIG_I2C_SLAVE)
	i2c_slave_cb_t slave_cb;	/* callback for slave mode	*/
#endif
};

i2c_driver:描述驱动信息,重点处理的结构体,其中包括probe函数,remove函数等

struct i2c_driver {
	unsigned int class;
	/* Notifies the driver that a new bus has appeared. You should avoid
	 * using this, it will be removed in a near future.
	 */
	int (*attach_adapter)(struct i2c_adapter *) __deprecated;
	/* Standard driver model interfaces */
	int (*probe)(struct i2c_client *, const struct i2c_device_id *);
	int (*remove)(struct i2c_client *);
	/* driver model interfaces that don't relate to enumeration  */
	void (*shutdown)(struct i2c_client *);
	/* Alert callback, for example for the SMBus alert protocol.
	 * The format and meaning of the data value depends on the protocol.
	 * For the SMBus alert protocol, there is a single bit of data passed
	 * as the alert response's low bit ("event flag").
	 */
	void (*alert)(struct i2c_client *, unsigned int data);
	/* a ioctl like command that can be used to perform specific functions
	 * with the device.
	 */
	int (*command)(struct i2c_client *client, unsigned int cmd, void *arg);
	struct device_driver driver;
	const struct i2c_device_id *id_table;
	/* Device detection callback for automatic device creation */
	int (*detect)(struct i2c_client *, struct i2c_board_info *);
	const unsigned short *address_list;
	struct list_head clients;
};

i2c_msg:I2C传参结构体,用于i2c_transfer传输数据
device_driver:其中包含设备树匹配方式of_match_table(compatible属性在其中),以及acpi_match_table
id_table:未使用设备树的设备匹配ID表

1.2.2 重要函数

#define i2c_add_driver(driver) \
    i2c_register_driver(THIS_MODULE, driver)
/// 注册i2c driver,当已经注册过时,需要先进行del
int i2c_register_driver(struct module *owner, struct i2c_driver *driver);
/// 注销i2c_driver
void i2c_del_driver(struct i2c_driver *driver)
/// 发送I2C数据
int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)   

adap:所使用的 I2C 适配器,i2c_client 会保存其对应的 i2c_adapter。
msgs:I2C 要发送的一个或多个消息。
num:消息数量,也就是 msgs 的数量。
返回值:负值,失败,其他非负值,发送的 msgs 数量。

其中i2c_transfer 函数最终会调用 I2C 适配器中 i2c_algorithm 里面的 master_xfer 函数,i2c驱动具有中间大两边小的特点。中间你无论是哪个厂家,无论你使用什么算法,最终提供给驱动开发人员的接口一定是i2c_transfer()接口,向下使用的文件一定是i2c-algo-bit.c文件中的函数产生波形

1.3 I2C设备和驱动匹配过程

I2C设备和驱动的匹配过程由I2C总线来完成,drivers/i2c/i2c-core.c、i2c_bus_type是i2c总线的核心部分,调用其中的i2c_device_match函数完成匹配。

struct bus_type i2c_bus_type = {
	.name		= "i2c",
	.match		= i2c_device_match,
	.probe		= i2c_device_probe,
	.remove		= i2c_device_remove,
	.shutdown	= i2c_device_shutdown,
};

static int i2c_device_match(struct device *dev, struct device_driver *drv)
{
	struct i2c_client	*client = i2c_verify_client(dev);
	struct i2c_driver	*driver;

	if (!client)
		return 0;

	/* Attempt an OF style match */
	if (of_driver_match_device(dev, drv))
		return 1;

	/* Then ACPI style match */
	if (acpi_driver_match_device(dev, drv))
		return 1;

	driver = to_i2c_driver(drv);
	/* match on an id table if there is one */
	if (driver->id_table)
		return i2c_match_id(driver->id_table, client) != NULL;

	return 0;
}

依旧是匹配三部曲:设备树compatible属性匹配->acpi形式匹配->无设备树的id_table name属性方式匹配

2.I2C设备驱动编写

现在主流基本都会使用设备树来管理,如果不使用设备树的话,则是借助i2c_board_info结构体来描述I2C设备信息,当使用设备树则按照下面的步骤,一步步完成一个I2C设备驱动编写/适配!

2.1 确认原理图引脚及pinctrl子系统引脚配置信息

这里我使用的I2C设备是AP3216C,一款光感sensor,原理图如下。
image.png
image.png
其中sensor使用的是I2C1_SCL以及I2C1_SDA,所以先确定这两个引脚的引脚复用,也就是pinctrl子系统配置是否正确。

&i2c1 {
	clock-frequency = <100000>;
	pinctrl-names = "default";
	pinctrl-0 = <&pinctrl_i2c1>; ///< pinctrl设备节点为pinctrl_i2c1
	status = "okay";
    ..... //省略
}

在imx6ull-14x14-emmc-4.3-480x272-c.dts设备树文件下搜索后能看到引脚复用信息如下,已经复用为I2C1_SCL以及I2C1_SDA,所以不用修改。而如果这里的引脚复用未配置,则需要进行修改,需根据自己的实际情况。

pinctrl_i2c1: i2c1grp {
    fsl,pins = <
        MX6UL_PAD_UART4_TX_DATA__I2C1_SCL 0x4001b8b0
        MX6UL_PAD_UART4_RX_DATA__I2C1_SDA 0x4001b8b0
    >;
};

2.2 确认设备树I2C节点信息

&i2c1 {
	clock-frequency = <100000>;
	pinctrl-names = "default";
	pinctrl-0 = <&pinctrl_i2c1>;
	status = "okay";
        codec: wm8960@1a {
                compatible = "wlf,wm8960";
                reg = <0x1a>;
                clocks = <&clks IMX6UL_CLK_SAI2>;
                clock-names = "mclk";
                wlf,shared-lrclk;
        };
	mag3110@0e {
		compatible = "fsl,mag3110";
		reg = <0x0e>;
		position = <2>;
		status = "disabled";
	};
	ap3216c@1e {
		compatible = "alientek,ap3216c";
		reg = <0x1e>;
	};
};

添加ap3216c节点信息,ap3216c@1e是节点名字,0e/1e这种则是I2C器件地址,compatibel属性为ap3216c,用这个属性进行设备和驱动的匹配,reg属性也是I2C器件地址,时钟频率为100KHz。
如果设备树编写的没问题,在替换完dtb文件到开发板后,能cat到相应的属性。这里我是从虚拟机拷贝dtb文件到板子的emmc上,具体操作需要根据情况而定。
scp imx6ull-14x14-emmc-4.3-480x272-c.dtb root@169.254.113.91:/run/media/mmcblk1p1

root@ATK-IMX6U:/run/media/mmcblk1p1# cat /sys/bus/i2c/devices/0-001e/name
ap3216c

其中001e是设备树中器件的节点地址,name为ap3216c

2.3 编写主体框架代码

主要是实现module_init、module_exit、probe函数、remove函数、以及file_operations操作函数等,这个几乎所有驱动框架是通用的

2.4 实现module_init、exit

在module_init时,进行i2c_add_driver,exit时,则是进行i2c_del_driver的操作

/// 传统方式匹配
static const struct i2c_device_id ap3216c_id[] = {
    {"alientek,ap3216c", 0}, 
    {}
};

/// 设备树方式匹配
static const struct of_device_id ap3216c_of_match[] = {
    {.compatible = "alientek,ap3216c"}
};

static struct i2c_driver ap3216c_driver = {
    .probe = ap3216c_probe,
    .remove = ap3216c_remove,
    .driver = {
        .owner = THIS_MODULE,
        .name = "ap3216c",
        .of_match_table = ap3216c_of_match,
    },
    .id_table = ap3216c_id,
};

static int __init ap3216c_init(void)
{
    int ret = 0;

    printk("ap3216c driver init begin\n");
    ret = i2c_add_driver(&ap3216c_driver);
    printk("ap3216c driver init done, ret:%d\n", ret);

    return ret;
}

static int __exit ap3216c_exit(void)
{
    printk("ap3216c driver exit begin\n"); 
    i2c_del_driver(&ap3216c_driver);
    printk("ap3216c driver exit done\n"); 

    return 0;
}

module_init(ap3216c_init);
module_exit(ap3216c_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("xuzhangxin");

2.5 实现probe、remove函数

probe中进行申请设备号,cdev_init、cdev_add、class_create、device_create等

static struct file_operations ap3216c_fops = {
    .owner = THIS_MODULE,
    .open = ap3216c_open,
    .release = ap3216c_close,
    .read = ap3216c_read,
};

int ap3216c_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
    printk("ap3216c driver probe begin\n");

    ///1.申请设备号
    if (ap3216c_dev.major) {
        ap3216c_dev.devid = MKDEV(ap3216c_dev.major, 0);
        register_chrdev_region(ap3216c_dev.devid, 1, AP3216C_NAME);
    } else {
        alloc_chrdev_region(&ap3216c_dev.devid, 0, 1, AP3216C_NAME);
        ap3216c_dev.major = MAJOR(ap3216c_dev.devid);
    }

    ///2.cdev_init、cdev_add
    cdev_init(&ap3216c_dev.m_cdev, &ap3216c_fops);
    cdev_add(&ap3216c_dev.m_cdev, ap3216c_dev.devid, 1);

    ///3.class_create
    ap3216c_dev.m_class = class_create(THIS_MODULE, AP3216C_NAME);
    if (IS_ERR(ap3216c_dev.m_class)) {
        return PTR_ERR(ap3216c_dev.m_class);
    }

    ///4.device_create    
    ap3216c_dev.m_dev = device_create(ap3216c_dev.m_class, NULL, ap3216c_dev.devid, NULL, AP3216C_NAME);
    if (IS_ERR(ap3216c_dev.m_dev)) {
        return PTR_ERR(ap3216c_dev.m_dev);
    }

    ap3216c_dev.client = client;

    printk("ap3216c driver probe done\n");
    return 0;
}

int ap3216c_remove(struct i2c_client *client)
{
    printk("ap3216c driver remove begin\n");
    ///cdev_del
    cdev_del(&ap3216c_dev.m_cdev);

    ///注销设备号
    unregister_chrdev(ap3216c_dev.devid, AP3216C_NAME);

    ///device_destroy
    device_destroy(ap3216c_dev.m_class, ap3216c_dev.devid);
    
    ///class_desroy
    class_destroy(ap3216c_dev.m_class);
    
    printk("ap3216c driver remove done\n");
    return 0;
}

2.6 实现open、close、read等操作函数

从这边开始的话,就要去看器件相应的寄存器手册了,这里我将整个的思路都尽可能写清楚,强化自己也方便大家,不需要了解这么详细的话也可以直接看下面的源码。

1)实现写i2c数据接口
寄存器手册中,有描述该器件I2C 写数据的协议。
首先是发送1bit开始信号+7bit的slave address(从设备地址),
此时从设备回1 bitACK,主设备继续发送8bit register address(寄存器地址),每收到一个字节数据,从设备拉低数据线产生一个应答,最后主设备再发送停止位
image.png
根据这里的i2c时序,可以先封装出一个i2c_write_reg的函数,用于设置System Configuration寄存器

static int ap3216c_write_reg(struct ap3216c_dev_t *dev, u8 reg, u8 *data, u16 len)
{
    int ret = 0;
    struct i2c_client* client = (struct i2c_client*)dev->client;
    struct i2c_msg msg;

    msg.addr = client->addr;            ///< 设备地址
    msg.flags = 0;                      ///< 写数据
    msg.buf[0] = reg;                   ///< 操作寄存器地址
    memcpy(&msg.buf[1], data, len);     ///< 写寄存器的数据
    msg.len = len + 1 ;                 ///< 1byte寄存器地址+data的长度    
    return i2c_transfer(client->adapter, &msg, 1);
}

2)实现读i2c数据接口
1bit开始信号+7bit的slave address(从设备地址)+1bit写数据位,此时从设备拉低数据线,给一个应答位,再发送8it的reg addr寄存器地址,从设备应答,主设备再次发送1bit的开始信号+7bit slave address+1bit读数据位+8bit寄存器地址,最后主设备作为接收方,收到最后一个字节数据后,主设备拉高SDA发送一个NACK信号,通知发送端结束数据发送,最后发送一个停止信号,完成数据的读取
image.png

static int ap3216c_read_reg(struct ap3216c_dev_t *dev, u8 reg, u8 *data, u16 len)
{
    int ret = 0;
    struct i2c_client* client = (struct i2c_client*)dev->client;
    struct i2c_msg msg[2];

    msg[0].addr = client->addr;             ///< 设备地址
    msg[0].flags = 0;                       ///< 写数据
    msg[0].buf = &reg;                    ///< 操作寄存器地址
    msg[0].len = 1 ;                        ///< 1byte寄存器地址 

    msg[1].addr = client->addr;            ///< 设备地址
    msg[1].flags = I2C_M_RD;               ///< 读数据
    msg[1].buf = data;                     ///< 读取数据缓冲区
    msg[1].len = len;                      ///< 读取数据长度 

    return i2c_transfer(client->adapter, &msg, 2);
}

3)实现fops->open接口,接口中配置设备工作模式
主要看我们需要采集哪些数据,来决定配置的模式
首先找到System Configuration寄存器,这个寄存器用于配置器件的工作模式,bit0-bit2有效,bit3-bit7无效image.png
image.png
image.png
关于它的不同工作模式,我找到的这篇博客:https://developer.aliyun.com/article/1083178
直接选用ALS+PS+IR 模式,同时对光强度及接近程度测量,也就是把mode配置为011
另外根据寄存器手册中的说明,设置完SW reset后,需要等待10ms,再设置真正的工作模式
image.png
所以最终open的代码如下:

static int ap3216c_open(struct inode *node, struct file *file)
{
    u8 value = 0x00;
    printk("ap3216c driver open\n");
    file->private_data = (void *)&ap3216c_dev; ///< 设置私有数据
    
    
    ap3216c_write_reg(file->private_data, AP3216C_SYSTEMCONG, &value, 1);  ///< set sw reset
    mdelay(20); ///< 最少等待10ms
    value = 0x03;
    ap3216c_write_reg(file->private_data, AP3216C_SYSTEMCONG, &value, 1);  ///< 设置为ALS and PS+IR模式

    printk("ap3216c driver open success\n");
    return 0;
}

4)实现fpos->close接口

static int ap3216c_close(struct inode *node, struct file *file)
{
    u8 value = 0x00;
    printk("ap3216c driver close\n");
    file->private_data = (void *)&ap3216c_dev; ///< 设置私有数据
    
    
    ap3216c_write_reg(file->private_data, AP3216C_SYSTEMCONG, &value, 1);  ///< set sw reset
    
    printk("ap3216c driver close success\n");
    return 0;
}

5)实现fops->read接口
读以下几个寄存器,获取IR、ALS、PS寄存器的值,其中若IR Data Low寄存器中bit7的值为1,则代表IR数据无效,否则代表数据有效。
image.png
image.png
image.png
image.png
所以这里读出IR Data Low寄存器的值val & 0x80,判断最高位是否为1,为1则IR寄存器的数据无效,为0则数据有效。PS寄存器同样也有这种机制,判断方法是一样的

static ssize_t ap3216c_read(struct file *file, char __user *buf, size_t cnt, loff_t *off)
{
    u8 data[3] = {0};
    u8 read_buf[AP3216C_DATA_REG_NUM] = {0};
    u8 i = 0;
    int ret = 0;
    printk("ap3216c driver read\n");

    file->private_data = (void *)&ap3216c_dev; ///< 设置私有数据

    /// 循环将IR、ALS、PS中的寄存器数据读取
    for (i = 0; i < AP3216C_DATA_REG_NUM; i++) {
        ret = ap3216c_read_reg(file->private_data, AP3216C_IRDATALOW + i, read_buf, 6);
        printk("ap3216c driver read ret:%d, reg:%x, value:%d\n", ret, AP3216C_IRDATALOW + i, read_buf[i]);
    }

    if (read_buf[0] & 0x80) {
        data[0] = 0;
        printk("ap3216c driver read ir data error\n");
    } else {
        data[0] = (read_buf[1] << 2) | read_buf[0];
        printk("ap3216c driver read ir data success, data:%x\n", data[0]);
    }

    data[1] = (read_buf[3] << 8) | read_buf[2];
    printk("ap3216c driver read als data success, data:%x\n", data[1]);


    if (read_buf[4] & 0x80) {
        data[2] = 0;
        printk("ap3216c driver read ps data error\n");
    } else {
        data[2] = ((read_buf[5] & 0x3f) << 4) | (read_buf[4] & 0x0f);
        printk("ap3216c driver read ir data success, data:%x\n", data[0]);
    }

    printk("ap3216c driver read als data success, data:%x\n", data[1]);

    if (copy_to_user(buf, data, sizeof(data) / sizeof(data[0])) != 0) {
        return -1;
    }


    printk("ap3216c driver read sucess, ret:%d\n", ret);
    return ret;
}

2.7 测试

这里写了一个测试代码

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"



int main(int argc, char **argv)
{
    char r_buf[3] = {0};
    int count = 100;
    int fd = open("/dev/ap3216c", O_RDWR);
    if (fd < 0)
    {
        printf("open ap3216c failed\n");
        return -1;
    }
    printf("open ap3216c success\n");
    while (count--) {
        read(fd, r_buf, sizeof(r_buf) / sizeof(r_buf[0]));
        printf("read ap3216c ir:%d\n", r_buf[0]);
        printf("read ap3216c als:%d\n", r_buf[1]);
        printf("read ap3216c ps:%d\n", r_buf[2]);
        usleep(200 * 1000);
    }
    close(fd);


    return 0;
}
insmod i2c_ap3216.ko
./test

image.png
这里还有另外一种调试方法,可以借助cat 和echo来简单的完成调试,不需要应用测试代码,可以参考下面这篇博客:
使用DEVICE_ATTR实现cat、echo指令调试驱动

3.i2c通信时序

这里暂时自己还没有写相关博客,可以参考下其他人写的
I2C通信时序

4.思维导图

  • 26
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是一个简单的示例代码,展示了如何在Linux系统上编写i2c驱动程序以控制AP3216C传感器: ```c #include <linux/i2c-dev.h> #include <linux/i2c.h> #define AP3216C_I2C_ADDR 0x1e static int ap3216c_read(struct i2c_client *client, u8 reg, u8 *data, int len) { struct i2c_msg msgs[2] = { { .addr = client->addr, .flags = 0, .len = 1, .buf = &reg, }, { .addr = client->addr, .flags = I2C_M_RD, .len = len, .buf = data, }, }; return i2c_transfer(client->adapter, msgs, 2); } static int ap3216c_write(struct i2c_client *client, u8 reg, u8 *data, int len) { u8 *buf = kzalloc(len + 1, GFP_KERNEL); int ret; if (!buf) return -ENOMEM; buf[0] = reg; memcpy(buf + 1, data, len); struct i2c_msg msg = { .addr = client->addr, .flags = 0, .len = len + 1, .buf = buf, }; ret = i2c_transfer(client->adapter, &msg, 1); kfree(buf); return ret; } static int ap3216c_probe(struct i2c_client *client, const struct i2c_device_id *id) { int ret; u8 data[2]; ret = ap3216c_read(client, 0x00, data, 2); if (ret < 0) { dev_err(&client->dev, "Failed to read AP3216C chip ID\n"); return ret; } dev_info(&client->dev, "AP3216C chip ID: %x\n", data[1]); /* TODO: Write initialization sequence here */ return 0; } static const struct i2c_device_id ap3216c_id[] = { { "ap3216c", 0 }, { } }; MODULE_DEVICE_TABLE(i2c, ap3216c_id); static struct i2c_driver ap3216c_driver = { .driver = { .name = "ap3216c", }, .probe = ap3216c_probe, .id_table = ap3216c_id, }; module_i2c_driver(ap3216c_driver); ``` 这个示例代码定义了两个函数:`ap3216c_read`和`ap3216c_write`,分别用于读和写AP3216C寄存器。该驱动程序在初始化过程中会通过I2C总线读取AP3216C芯片的ID,并可以在该函数中添加其他初始化代码。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值