基于linux的I2C驱动与调试(传统ID匹配方式)

1.Linux I2C驱动框架

I2C驱动框架可以分为四部分,I2C核心、I2C设备、I2C驱动、I2C适配器,其中I2C总线位于I2C核心中。
在这里插入图片描述

1.1. I2C驱动的主要对象

2C总线用于管理I2C设备和I2C驱动,维护一个设备链表和驱动链表,定义了设备和驱动的匹配规则,定义了匹配成功后的行为,其在内核中的定义如下。

1.1.1. I2C总线

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

1.1.2. I2C设备

I2C设备描述了I2C设备的硬件信息,例如I2C设备的地址、I2C设备在接在哪一个I2C控制器上,其结构体定义如下

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 i2c_driver *driver;	/* and our access routines	*/
	struct device dev;		/* the device structure		*/
	int irq;			/* irq issued by device		*/
	struct list_head detected;
};

1.1.3. I2C驱动

I2C驱动是I2C设备的驱动程序,用于匹配I2C设备,其结构体定义如下。

struct i2c_driver {
	...
	/* 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 *);
	int (*suspend)(struct i2c_client *, pm_message_t mesg);
	int (*resume)(struct i2c_client *);
	...
	struct list_head clients;
};

1.1.4. I2C适配器

I2C适配器是SOC上的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;
	...
	struct device dev;		/* the adapter device */
	...
};

其中的i2c_algorithm表示算法,用于向硬件设备传输数据,其定义如下。

struct i2c_algorithm {
	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 *);
};

1.1.5. 总结一下

I2C驱动的主要对象是I2C总线、I2C设备、I2C驱动、I2C适配器
I2C总线用于管理I2C设备和I2C驱动
I2C设备描述了I2C设备的硬件信息
I2C驱动是I2C设备对应的驱动程序
I2C适配器是SOC上的I2C控制器,其定义了算法,可以向I2C硬件设备传输数据
其中直接面向编写I2C设备驱动的开发者的是I2C设备和I2C驱动,I2C总线和I2C适配器是幕后工作者

1.2. I2C框架分析

I2C核心维护着一条I2C总线,提供了注册I2C设备、I2C驱动、I2C适配器的接口。
I2C总线维护着一条设备链表和驱动链表,当向I2C核心层注册设备时,会将其添加到总线的设备链表中,然后遍历总线上的驱动链表,查看二者是否匹配,如果匹配就调用驱动的probe函数。
当注册I2C驱动时,也会将其添加到I2C总线的驱动链表中,然后遍历总线的设备链表,查看二者是否匹配,如果匹配就调用驱动的probe函数。
在I2C驱动程序中,通过I2C适配器中的算法向I2C硬件设备传输数据。

1.3. I2C流程分析

在这里插入图片描述

2.Linux I2C驱动框架源码剖析

2.1.注册I2C设备

(1)注册I2C适配可以通过i2c_new_device,此函数会生成一个i2c_client,指定对应的总线为I2C总线,然后向总线注册设备。

struct i2c_client *
i2c_new_device(struct i2c_adapter *adap, struct i2c_board_info const *info)
{
	struct i2c_client	*client;
	client = kzalloc(sizeof *client, GFP_KERNEL);
	client->dev.bus = &i2c_bus_type;
	...
	status = device_register(&client->dev);
}

(2)看一下其中的i2c_bus_type对象,其表示I2C总线,定义了设备和驱动的匹配规则还有匹配成功后的行为。

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

(3)下面再来看看device_register向总线注册设备过程中会发生什么。device_register首先会将设备添加到总线的设备链表中,然后遍历总线的驱动链表,判断设备和驱动是否匹配,如果匹配就调用驱动的probe函数,下面看一看源码分析。

int device_register(struct device *dev)
{
	device_initialize(dev);
	return device_add(dev);
}
int device_add(struct device *dev)
{
	...
	error = bus_add_device(dev);
	...
	bus_probe_device(dev);
	....
}

(4)其中bus_add_device函数会将设备添加到总线的设备链表中,如下。

int bus_add_device(struct device *dev)
{
	....
	klist_add_tail(&dev->p->knode_bus, &bus->p->klist_devices);
	...
}

(5)bus_probe_device函数会遍历总线的驱动链表,如下。

void bus_probe_device(struct device *dev)
{
	....
	if (bus->p->drivers_autoprobe) {
		ret = device_attach(dev);
		WARN_ON(ret < 0);
	}
	...
}
int device_attach(struct device *dev)
{
	...
	ret = bus_for_each_drv(dev->bus, NULL, dev, __device_attach);
	...
}

(6)bus_for_each_drv(dev->bus, NULL, dev, __device_attach);会遍历总线的驱动链表的每一项,然后调用__device_attach。

static int __driver_attach(struct device *dev, void *data)
{
	struct device_driver *drv = data;
	if (!driver_match_device(drv, dev))
		return 0;

	if (dev->parent)	/* Needed for USB */
		device_lock(dev->parent);
	device_lock(dev);
	if (!dev->driver)
		driver_probe_device(drv, dev);
	device_unlock(dev);
	if (dev->parent)
		device_unlock(dev->parent);

	return 0;
}

driver_match_device函数会判断设备和驱动是否匹配,如果匹配就调用driver_probe_device。

(7)首先来看一看driver_match_device函数的定义。

static inline int driver_match_device(struct device_driver *drv,
				      struct device *dev)
{
	return drv->bus->match ? drv->bus->match(dev, drv) : 1;
}

(8)发现它调用了总线的match函数,这里的总线在注册I2C设备的时候已经被设置为I2C总线了,定义如下。

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

(9)所以这里会调用到i2c_device_match函数,i2c_device_match会通过I2C驱动的id_table中每一的name和I2C设备的name进行匹配。

static const struct i2c_device_id *i2c_match_id(const struct i2c_device_id *id,
						const struct i2c_client *client)
{
	while (id->name[0]) {
		if (strcmp(client->name, id->name) == 0)
			return id;
		id++;
	}
	return NULL;
}

(10)如果匹配成功会调用driver_probe_device,下面再来看看driver_probe_device,driver_probe_device函数最终会先调用到I2C总线的probe函数,然后再调用I2C驱动的probe函数。

int driver_probe_device(struct device_driver *drv, struct device *dev)
{
	int ret = 0;

	if (!device_is_registered(dev))
		return -ENODEV;

	pr_debug("bus: '%s': %s: matched device %s with driver %s\n",
		 drv->bus->name, __func__, dev_name(dev), drv->name);

	pm_runtime_get_noresume(dev);
	pm_runtime_barrier(dev);
	ret = really_probe(dev, drv);
	pm_runtime_put_sync(dev);

	return ret;
}
static int really_probe(struct device *dev, struct device_driver *drv)
{
	...
}

(11)总线的probe函数为i2c_device_probe,此函数会调用驱动的probe函数

static int i2c_device_probe(struct device *dev)
{
	...
	status = driver->probe(client, i2c_match_id(driver->id_table, 
	...
}

2.2.注册I2C驱动

(1) 可以通过i2c_add_driver注册I2C驱动,该函数会指定驱动对应的总线为I2C总线,然后向总线注册驱动。

int i2c_register_driver(struct module *owner, struct i2c_driver *driver)
{
	...
	driver->driver.bus = &i2c_bus_type;
	...
	res = driver_register(&driver->driver);
	...
}

(2) i2c_bus_type的定义如下。

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

(3) driver_register函数遍历总线的设备链表进行操作,然后将驱动添加到总线的驱动链表中。

int driver_register(struct device_driver *drv)
{
	...
	ret = bus_add_driver(drv);
	...
}
int bus_add_driver(struct device_driver *drv)
{
	...
	if (drv->bus->p->drivers_autoprobe) {
		error = driver_attach(drv);
		if (error)
			goto out_unregister;
	}
	klist_add_tail(&priv->knode_bus, &bus->p->klist_drivers);
	
}
int driver_attach(struct device_driver *drv)
{
	return bus_for_each_dev(drv->bus, NULL, drv, __driver_attach);
}

(4)下面来看一看__driver_attach函数,此函数会判断设备和驱动是否匹配,如果匹配就调用驱动的probe函数。

static int __driver_attach(struct device *dev, void *data)
{
	...
	if (!driver_match_device(drv, dev))
		return 0;
	...
	if (!dev->driver)
		driver_probe_device(drv, dev);
}

2.3. I2C适配器的构建

对于三星平台,在drivers\i2c\busses\i2c-s3c2410.c文件中构建并注册了I2C适配器,这是三星平台I2C控制器的驱动。

static const struct i2c_algorithm s3c24xx_i2c_algorithm = {
	.master_xfer		= s3c24xx_i2c_xfer,
	.functionality		= s3c24xx_i2c_func,
};
static int s3c24xx_i2c_probe(struct platform_device *pdev)
{
	...
	i2c->adap.algo    = &s3c24xx_i2c_algorithm;
	...
	ret = i2c_add_numbered_adapter(&i2c->adap);
	...
}

其中的s3c24xx_i2c_algorithm中的s3c24xx_i2c_xfer就是通过操作寄存器来通过I2C控制器传输数据。

2.4. I2C数据传输

上面介绍I2C数据传输是通过I2C适配器完成的,下面来分析一下源码,在I2C驱动中,使用i2c_transfer来传输I2C数据,此函数肯定是通过I2C适配器的算法进行操作的,如下。

int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)
{
	....
	ret = adap->algo->master_xfer(adap, msgs, num);
	...
}

3.I2C背景了解

3.1.物理接口

1)SCL(serial clock ):时钟线,传输CLK信号,一般是I2C主设备向从设备提供时钟的通道;
2)SDA(serial data):数据线,通信数据都通过SDA线传输。

3.2通信特征

1)I2C 属于串行通信,所有的数据以位为单位在SDA线上串行传输。

2)同步通信就是通信双方工作在同一个时钟下,一般是通信的A方通过一根CLK信号线传输自己的时钟给B,B工作在A传输的时钟下,所以同步通信的显著特征就是:通信线中有CLK。

3)非差分。因为I2C速率不高且通信双方距离很近,所以使用电平信号通信。

4)低速率。 I2C一般是用在板子上的2个IC之间的通信,而且用来传输的数据量不大,所以本身通信速率很低(几百KHz,不同的I2C芯片的通信速率可能不同,具体在编程的时候要看自己使用的设备允许的I2C通信最高速率,不能超过这个速率)

4. I2C时序图

4.1. I2C起始信号

1) SCL为高电平的时候,SDA由高电平向低电平跳变。

在这里插入图片描述

4.2. I2C终止信号

1) 终止信号:SCL为高电平的时候,SDA由低电平向高电平跳变。

在这里插入图片描述

4.3. I2C应答信号

1)I2C总线上的所有数据都是以8位字节传送的,发送器每发送一个字节,就在时钟脉冲9期间释放数据线,由接收器反馈一个应答信号。应答信号为低电平时,规定为有效应答位(ACK简称应答位),表示接收器已经成功地接收了该字节;应答信号为高电平时,规定为非应答位(NACK),一般表示接收器接收该字节没有成功,对于反馈有效应答位ACK的要求是,接收器在第9个时钟脉冲之前的低电平期间将SDA线拉低,并且确保在该时钟的高电平期间为稳定的低电平。如果接收器是主控器,则在它收到最后一个字节后,发送一个NACK信号,以通知被控发送器结束数据发送,并释放SDA线,以便主控接收器发送一个停止信号P。

在这里插入图片描述

4.4. I2C写时序

1) 开始信号:主机+从设备地址+写命令,从机应答,应答成功,表示有这个设备,然后主机+设备内部寄存器地址,此时不用再加写命令控制字,从机应答,应答成功,表示设备内有这个地址,主机写入数据,从机应答,是否继续发送,不发送的话,发送停止信号P。

在这里插入图片描述

4.5. I2C读时序

1) 首先要知道将要所读取设备的地址告诉从设备,从设备才能将数据放到(发送)SDA上使主设备读取,从设备将数据放入SDA上的过程,由硬件主动完成,不用人为的写入。所以首先先写入从机地址,然后+写控制命令,从机应答,应答成功,表示有这个设备,然后写入内部寄存器地址,此时不用再加写命令控制字,从机应答,应答成功,表示设备内有这个地址。然后主机继续发出:写入从机地址,然后+读命令,从机应答,应答成功,此时便可以读取数据了,从设备已经将数据放入到SDA上了。

在这里插入图片描述

5. I2C实际操作图解

1)  首先本次操作是通过I2C读写CPLD的一个实验。第一,我们进行一个写数据操作,I2C从设备地址位0x16,CPLD寄存器地址为0x13,写入CPLD的数据为0x2;然后在读取写入CPLD中寄存器的值。

5.1.写操作

在这里插入图片描述
由上图所示,我可以得到从设备的地址为0x16,为写操作写入的高八位数据为0x0。

在这里插入图片描述
由上图所示,我可以得到写操作写入的低八位数据为0x13,接着写入的高八位数据为0x00。

在这里插入图片描述
由上图所示,我们可以得到写入的低八位数据为0x2,然后终止传输信号。

5.2.读操作

在这里插入图片描述

由上图所示,我可以得到从设备的地址为0x16,为写操作写入的高八位数据为0x0。

在这里插入图片描述
由上图所示,我可以得到写操作写入的低八位数据为0x13,然后重新发送起始型号,发送从设备地址0x16,然后读取数据。

在这里插入图片描述
由上图所示,我可以读取到高八位数据:0x0,接着为读取到的第八位数据:0x2,然后终止传输信号。

5.3. 驱动代码

  1. 以下代码为nst175温度传感器的代码.
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include <linux/device.h>
#include <linux/i2c.h>


#define I2C_MASTER_RD	0x0001	//read data, from slave to master
#define DEVICE_NAME "sensor" 		//设备名:srnsor
#define DEVICE_MINOR_NUM 1			//次设备号数		

struct i2c_nst175_command {
    u16   address;
    u16   data_buff;
};

struct nst175_device{ 
	dev_t devid; 				//设备ID	 
	struct cdev nst175_cdev; 	//字符设备结构体		
	struct class *nst175_class; //类	
	struct device *device; 		//创建设备
	int major; 					//主设备		 
	int minor; 					//从设备			
	int private_data[2]; 		//私有数据,用于存放从设备地址
};

static struct i2c_client *this_client;
static struct nst175_device nst175dev; 

//------------------------I2C读取NST175数据------------------------
static int i2c_nst175_rxdata(int saddr,char *regaddr,int regaddr_len,char *rxdata, int data_len)
{
	int	ret;
	
    struct i2c_msg msgs[] = {
        {
            .addr  = saddr,
            .flags = 0,
            .len   = regaddr_len,
            .buf   = regaddr,
        },
        {
            .addr  = saddr,
            .flags = I2C_MASTER_RD,
            .len   = data_len,
            .buf   = rxdata,
        },
    };

    ret = i2c_transfer( this_client->adapter, msgs, 2);
    if( ret < 0)
        printk( "read error:%d.\n", ret);
	
    return ret;	
	
}

//---------------------------打开操作---------------------------
static int nst175_open(struct inode *inode, struct file *filp) 
{ 
    return 0;  
}

//---------------------------控制操作---------------------------
static long nst175_ioctl( struct file *file, unsigned int cmd, unsigned long arg)
{
    struct i2c_nst175_command cmdframe;
    int ret = 0;
		
    switch( cmd) {
		//-----------------------------nst175_1(板子标识号u8)-----------------------------
        case 0:	
			copy_from_user((void *)&cmdframe, (void __user *)arg, sizeof( cmdframe));			
			ret = i2c_nst175_rxdata(nst175dev.private_data[0],(u8 *)&cmdframe.address,1,(u8 *)&cmdframe.data_buff,2);	
		    copy_to_user( (void __user *)arg, (void *)&cmdframe, sizeof( cmdframe));
			break;
		//-----------------------------nst175_2(板子标识号u9)-----------------------------	
		case 1:
            copy_from_user((void *)&cmdframe, (void __user *)arg, sizeof( cmdframe));			
			ret = i2c_nst175_rxdata(nst175dev.private_data[1],(u8 *)&cmdframe.address,1,(u8 *)&cmdframe.data_buff,2);	
		    copy_to_user( (void __user *)arg, (void *)&cmdframe, sizeof( cmdframe));
			break;

        default:
             printk( "invalid cmd %#x\n", cmd);
            return -EINVAL;
    }

    return ret;		
}

//---------------------------设备操作---------------------------
static struct file_operations nst175_fops = 
{
	.owner 			= THIS_MODULE, 
	.open   		= nst175_open, 
	.unlocked_ioctl = nst175_ioctl,
};

//---------------------------探针函数----------------------------
static int sensor_nst175_probe(struct i2c_client *client, const struct i2c_device_id *id)
{	
	int i,err;
	int devno;
	//-------------------------注册字符设备驱动---------------------------------

	if (nst175dev.major) //创建设备号
	{
		//静态注册设备号
		nst175dev.devid = MKDEV(nst175dev.major, 0);
		register_chrdev_region(nst175dev.devid, DEVICE_MINOR_NUM,id->name); 
	} 
	else 
	{ 
		//动态注册设备号
		alloc_chrdev_region(&nst175dev.devid, 0, DEVICE_MINOR_NUM,id->name); /* 申请设备号 */ 
		nst175dev.major = MAJOR(nst175dev.devid); /* 获取主设备号 */ 
		nst175dev.minor = MINOR(nst175dev.devid); /* 获取次设备号 */ 
	} 

	//创建类
	nst175dev.nst175_class = class_create(THIS_MODULE,id->name);
	if (IS_ERR(nst175dev.nst175_class)) {
		return PTR_ERR(nst175dev.nst175_class); 
	}

	//设备初始化{
	//数据初始化
	nst175dev.nst175_cdev.owner = THIS_MODULE;
	cdev_init(&nst175dev.nst175_cdev,&nst175_fops);
	
	devno = MKDEV(nst175dev.major,nst175dev.minor+id->driver_data);
	
	//注册到系统
	err = cdev_add(&nst175dev.nst175_cdev,devno, 1);
	if(err){
		printk(KERN_EMERG "cdev_add is fail! %d\n",err);
	}
	else{
		printk(KERN_EMERG "cdev_add %d is success!\n",nst175dev.minor+id->driver_data);
	}
		
	//创建设备节点
	device_create(nst175dev.nst175_class,NULL,devno,NULL,"%s",id->name);	
		
	this_client = client;	
		
	return 0;
}

//--------------------------移除字符函数---------------------------
static int sensor_nst175_remove(struct i2c_client *client) 
{ 
	cdev_del(&nst175dev.nst175_cdev);						//删除字符设备
	unregister_chrdev_region(nst175dev.devid, 1); 			//注销字符设备
	
	//注销掉类和设备 
	device_destroy(nst175dev.nst175_class, nst175dev.devid); 
	class_destroy(nst175dev.nst175_class); 
	
	return 0; 
}

//-----------------------传统匹配方式ID列表------------------------
static const struct i2c_device_id sensor_nst175_id[] = {
	{"nst175_1", 0}, 
	{"nst175_2", 1},
	{/* END OF LIST */}	
};

//-------------------------I2C板载设备信息-------------------------
static struct i2c_board_info sensor_nst175_i2c_board_info[] = {
	{I2C_BOARD_INFO("nst175_1", 0x49),},
	{I2C_BOARD_INFO("nst175_2", 0x4a),},

};

//--------------------------i2c驱动结构体--------------------------
static struct i2c_driver sensor_nst175_driver = {
	.probe = sensor_nst175_probe, 
	.remove = sensor_nst175_remove,
	.id_table = sensor_nst175_id,	
	.driver = {
		.owner = THIS_MODULE, 
		.name = "sensor_nst175", 
	}, 
	
};

//----------------------------加载函数-----------------------------
static int __init sensor_nst175_init(void) 
{
	int ret = 0,i;
	struct i2c_adapter *i2c_adap = NULL;
	struct i2c_client *client = NULL;
	
	//获取adapter总线上的相应的I2C设备:I2C(0)
	i2c_adap = i2c_get_adapter(0);

	if(i2c_adap < 0)
	{
		printk("i2c_get_adapter() failed...\n");
	}
	
	//添加新的I2C设备
	for(i=0;i<sizeof(sensor_nst175_i2c_board_info)/\
			 sizeof(sensor_nst175_i2c_board_info[0]);++i)
	{
		client = NULL;
		client = i2c_new_device(i2c_adap, &sensor_nst175_i2c_board_info[i]);
		if (!client)
		{
			printk("i2c_new_device() failed...\n");
			i2c_put_adapter(i2c_adap);	
			return -1;
		}
		nst175dev.private_data[i] = client->addr;
	}
	
	//添加I2C驱动
	ret = i2c_add_driver(&sensor_nst175_driver);
	if(ret)
	{
		printk("i2c_add_driver() failed...\n");	
	}
	
	return ret;	
}

//-----------------------------卸载函数-----------------------------
static void __exit sensor_nst175_exit(void) 
{
	printk(KERN_EMERG "sensor_nst175_exit!\n");	
	i2c_del_driver(&sensor_nst175_driver);
}


module_init(sensor_nst175_init); 		//入口函数
module_exit(sensor_nst175_exit);		//出口函数

MODULE_LICENSE("GPL");
MODULE_AUTHOR("author:shzn");


6.总结

最后关于I2C的调试过程讲解就到这了,主要是想讲述一下调试中的过程,同时附带了驱动代码,欢迎大家一起来学习。。。

  • 6
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值