(3.6)一个按键所能涉及的:输入子系统

/* AUTHOR: Pinus

* Creat on : 2018-10-28

* KERNEL : linux-4.4.145

* BOARD : JZ2440(arm9 s3c2440)

* REFS : 韦东山视频教程第二期

               linux驱动子系统之输入子系统(1)

               我对linux驱动 输入子系统的理解

               Linux之输入子系统分析(详解)

*/

概述

前文自己的按键驱动的时候:

        实现模块init(入口)、exit(出口)函数,设置或自动生成设备主设备号,注册设备类、设备节点,实现file_operation结构具体操作。

        若有多个不同的驱动程序时,应用程序就要打开多个不同的驱动设备,如果仅仅是自己看或者使用当然会很清楚怎么使用修改,但实际上我们的代码是要给别人看或者使用的,别人来使用时就会很麻烦。因此Linux引入input(输入子系统), 使应用程序无需打开多个不同的驱动设备便能实现

什么是输入子系统?

        linux内核中自带了很多的驱动子系统,其中比较典型的有:input子系统,led子系统,framebuffer子系统(LCD), I2c子系统等,这些子系统它是通过一层一层的函数传递与封装,它实现了设备驱动的注册,以及定义了file-operations结构体里面的各种函数操作等,不需要在单独的设备驱动代码中进行注册,定义,直接调用相应的的子系统即可,内核提供的输入子系统是对分散的、多种不同类别的输入设备(键盘、鼠标、触摸屏、加速计、跟踪球、操纵杆等)进行统一处理的驱动程序。

输入子系统带来的好处:

抽象底层形态各异的硬件输入设备,为上层提供了统一的操作接口

提高了代码重用率和效率,减少了bug

输入子系统框架:

输入子系统的三层结构:

l  事件驱动层

         负责和应用程序的接口

l  核心层

         提供事件驱动层和设备驱动层所需的函数接口

l  设备驱动层

         负责和底层输入设备通信

事件驱动层和核心层都是通用的,我们需要实现的是设备驱动层。输入设备驱动会把硬件产生的事件信息用统一的格式(struct input_event)上报给核心层,然后核心层进行分类后,再上报给相应的事件处理驱动程序,最后通过事件层传递到用户空间,应用程序可以通过设备节点来获取事件信息。比如底层报上来的是一个按键事件,核心层会报给evdev来处理;如果报的是一个鼠标事件,核心层会报给mousedev来处理。

分析输入子系统

drivers/input/input.c 核心层

subsys_initcall(input_init); //input子系统的实现是类似模块的方法

module_exit(input_exit);

从input_init,分析【注册流程】

static LIST_HEAD(input_dev_list);              // 定义全局链表 : input device

static LIST_HEAD(input_handler_list);       // 定义全局链表 : input handler

 

input_init()      //入口函数

    |

    class_register(&input_class)     // 将class注册到内核中,同时创建/sys/class/下节点,类似class_create()

    input_proc_init()         // 创建 /proc下对应文件,导出设备信息到proc, 没啥用

        |

        proc_bus_input_dir = proc_mkdir("bus/input", NULL); // 创建文件夹"/proc/bus/input"

        // 创建节点"/proc/bus/input/devices"

        entry = proc_create("devices", 0, proc_bus_input_dir, &input_devices_fileops);

        // 创建节点"/proc/bus/input/handlers"

        entry = proc_create("handlers", 0, proc_bus_input_dir, &input_handlers_fileops);

        // 根据要求申请主设备号,主设备号INPUT_MAJOR == 13,这里只是申请了区域但还没有创建具体设备节点,属于创建设备的一步,这种创建设备的方式你前面也提过。

   register_chrdev_region(MKDEV(INPUT_MAJOR, 0), INPUT_MAX_CHAR_DEVICES, "input");

 

input handler层: evdev.c 【注册流程】

evdev_init()     // module_init(evdev_init)

    |

    input_register_handler(&evdev_handler)

static struct input_handler evdev_handler = {
    .event        = evdev_event,
    .events       = evdev_events,
    .connect      = evdev_connect,
    .disconnect   = evdev_disconnect,
    .legacy_minors = true,
    .minor        = EVDEV_MINOR_BASE,
    .name         = "evdev",
    .id_table     = evdev_ids,
};

        |

        INIT_LIST_HEAD(&handler->h_list);   //初始化链表头,把链表的前和后都指向它自己

        list_add_tail(&handler->node, &input_handler_list);  // 把 handler的 node 加到 input_handler_list这个双向链表

        list_for_each_entry(dev, &input_dev_list, node)    // 遍历input_dev_list链表,链表中每一个input device均尝试与当前的input handler(evdev)匹配

        input_attach_handler(dev, handler); // evdev可以与任何input device匹配,因为evdev的id_table[]为空

            |

            id = input_match_device(handler, dev); // 根据evdev的id_table[]进行匹配 - 满足id_table[]中的全部条件才能匹配成功

            error = handler->connect(handler, dev, id); // 匹配成功后调用handler中connect() -- .cevdev_connect,

        input_wakeup_procfs_readers(); // 将当前的handler加入到/proc/bus/input/handlers文件中

总结:

1. 注册了evdev_handler

2. 遍历input_dev_list,进行行匹配,匹配成功,调用handler中connect方法--- evdev_connect()

3. 内核有好几个input handler: evdev、mousedev、joydev、evbug等

4. 其中 evdev 可以处理所有的事件,触摸屏驱动,sensor就是用的这个。

 

input 驱动层: button.c 【注册流程】 示例解析

#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/sched.h>
#include <linux/pm.h>
#include <linux/slab.h>
#include <linux/sysctl.h>
#include <linux/proc_fs.h>
#include <linux/delay.h>
#include <linux/platform_device.h>
#include <linux/input.h>
#include <linux/gpio_keys.h>
#include <linux/workqueue.h>
#include <linux/gpio.h>
#include <linux/of.h>
#include <linux/of_platform.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
#include <linux/spinlock.h>

#include <asm/io.h>
#include <asm/irq.h>
#include <asm/uaccess.h>

#include <mach/regs-gpio.h>
#include <mach/gpio-samsung.h>

static struct input_dev *buttons_dev;  //声明一个输入子系统设备

struct jzpin_desc{
	int irq;
	char *name;
	unsigned int pin;
	unsigned int key_val;
};

struct jzpin_desc pins_desc[4] = {
	{IRQ_EINT0,  "S2", S3C2410_GPF(0),  KEY_L},
	{IRQ_EINT2,  "S3", S3C2410_GPF(2),  KEY_S},
	{IRQ_EINT11, "S4", S3C2410_GPG(3),  KEY_ENTER},
	{IRQ_EINT19, "S5", S3C2410_GPG(11), KEY_LEFTSHIFT},
};
static struct jzpin_desc *irq_pd;

static struct timer_list button_timer;  //定时器防抖动

static irqreturn_t button_irq_handle(int irq, void *dev_id)
{
	irq_pd = (struct intpin_desc *)dev_id;
	/* 10ms后启动定时器 */
	mod_timer(&button_timer, jiffies + HZ/100);
	return IRQ_HANDLED;
}

/* Handle the timer event */
static void button_expire_timeout(unsigned long unused)
{
	struct jzpin_desc * pindesc = irq_pd;
	unsigned int pinval;

	if (!pindesc)
			return 0;
	
	pinval = gpio_get_value(pindesc->pin);

        /*上报事件*/
	if (pinval){
		/*松开 : 最后一个参数表示:0-松开, 1-按下*/
		input_event(buttons_dev, EV_KEY, pindesc->key_val, 0);
		input_sync(buttons_dev);  //同步事件,表示上报完
	}else{
		/*按下*/
		input_event(buttons_dev, EV_KEY, pindesc->key_val, 1);
		input_sync(buttons_dev);
	}

}

static int __init buttons_init(void)
{
	int i;
	
	/* 1. 分配一个input_dev结构体 */
	buttons_dev = input_allocate_device();
	/* 2. 设置 */
		//2.1 产生哪一类事件
		//2.2 产生哪一些事件 L,S,ENTER,LEFTSHIFT
	__set_bit(EV_REP, buttons_dev->evbit);
	input_set_capability(buttons_dev, EV_KEY, KEY_L);
	input_set_capability(buttons_dev, EV_KEY, KEY_S);
	input_set_capability(buttons_dev, EV_KEY, KEY_ENTER);
	input_set_capability(buttons_dev, EV_KEY, KEY_LEFTSHIFT);
	/* 3.注册 */
	input_register_device(buttons_dev);
	/* 4.硬件相关操作 */
	init_timer(&button_timer);
	setup_timer(&button_timer, button_expire_timeout, 0);
	add_timer(&button_timer);
	
	for (i=0; i<4; i++){
		request_irq(pins_desc[i].irq, 	button_irq_handle, IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING, pins_desc[i].name, &pins_desc[i]);  //注册中断
	}
	
	return 0;
}

static void __exit buttons_exit(void)
{
	int i;
	for (i=0; i<4; i++){
		free_irq(pins_desc[i].irq, &pins_desc[i]);
	}
	del_timer(&button_timer);
	input_unregister_device(buttons_dev);
	input_free_device(buttons_dev);
} 

module_init(buttons_init);
module_exit(buttons_exit);

MODULE_AUTHOR("pinus0716@163.com");
MODULE_LICENSE("GPL");

分析【注册流程】

input_register_device(buttons_dev);

    |

    error = device_add(&dev->dev);    // 创建设备节点

    list_add_tail(&dev->node, &input_dev_list); // 重要,把input device挂到全局的链表input_dev_list上

    // 核心重点,遍历input_handler_list链表,链表中每一个input handler均尝试与当前的input device匹配

    list_for_each_entry(handler, &input_handler_list, node)

    input_attach_handler(dev, handler); // evdev可以与任何input device匹配,适应所有设备

        |

        id = input_match_device(handler, dev); // 根据evdev的id_table[]进行匹配 - 满足id_table[]中的全部条件才能匹配成功

        error = handler->connect(handler, dev, id); // 匹配成功后调用handler中connect() -- evdev_connect

当input_dev和input_handler匹配成功

evdev_connect(struct input_handler *handler, struct input_dev *dev, const struct input_device_id *id)

    |

    // 分配次设备号,找到一个尚未被使用的最小次设备号,从64开始,65,66

    minor = input_get_new_minor(EVDEV_MINOR_BASE, EVDEV_MINORS, true);

    evdev = kzalloc(sizeof(struct evdev), GFP_KERNEL); // 实例化一个evdev对象

    INIT_LIST_HEAD(&evdev->client_list); // 多个应用打开同一个device时,每次open都生成一个clinet,挂载到client_list,数据上报时遍历链表,copy到所有成员的buffer中

    init_waitqueue_head(&evdev->wait);  // 等待队列用于完成阻塞,read()的时候,没数据(缓存队列头等于尾)就睡眠,唤醒条件为有数据(缓存队列头不等于尾),input_sync()显示唤醒

    ====

    等待队列的实现:

    1 wait_queue_head_t mywq_head;

    2 init_waitqueue_head(&mywq_head);

    3 wait_event_interruptible( mywq_head, fs210_btn_device->btn_state); // 条件不满足,就把调用进程挂起

    4 wake_up_interruptible(&mywq_head); // 唤醒等待队列 - 等待队列需要显式唤醒

    ====

    dev_set_name(&evdev->dev, "event%d", dev_no); // 创建设备文件/dev/input/event0/1/2 以下代码与device_create()一样

    evdev->dev.devt = MKDEV(INPUT_MAJOR, minor);

    evdev->dev.class = &input_class;

    evdev->dev.parent = &dev->dev;

    evdev->dev.release = evdev_free;

    device_initialize(&evdev->dev);

    evdev->handle.dev = input_get_device(dev); // 利用handle记录input device和input handler(经过匹配后的)

    evdev->handle.handler = handler;

    evdev->handle.private = evdev; // 后面evdev_events(struct input_handle *handle,)根据handle 拿到evdev

    error = input_register_handle(&evdev->handle);  // 将handle与input device关联,互相可以找到

        |

        list_add_tail_rcu(&handle->d_node, &dev->h_list); // 将handle与input device关联,互相可以找到

        list_add_tail_rcu(&handle->h_node, &handler->h_list); // 将handle与input handler关联,互相可以找到

    cdev_init(&evdev->cdev, &evdev_fops); // 初始化并注册字符设备cdev,完成fops,为用户提供文件io接口

    cdev_add(&evdev->cdev, evdev->dev.devt, 1);

总结:

1. 分配evdev,并初始化,创建handle 记录 相匹配的input device和input handler的节点

2. 创建设备节点/dev/input/event0/1/2

3. 注册cdev,并实现fops

4. 关系:

    多个input device可以对应一个input handler

    一个input device对应一个evdev,对应一个设备节点:/dev/input/event0/1/2

    一个input device可以被多个应用打开,每次打开生成一个clinet,挂载到evdev中的client_list链表

5. 所有设备节点被调用open(),read(),write()文件io的时候

    实际上都是调用cdev中fops的各个接口:

static const struct file_operations evdev_fops = {

    .owner = THIS_MODULE,
    .read = evdev_read,
    .write = evdev_write,
    .poll = evdev_poll,
    .open = evdev_open,
    ...
};

 

按键采用定时器延时,所以上报在定时器处理函数中

/* Handle the timer event */
static void button_expire_timeout(unsigned long unused)
{
    struct jzpin_desc * pindesc = irq_pd;
    unsigned int pinval;
 
    if (!pindesc)
        return 0;

    pinval = gpio_get_value(pindesc->pin);
    if (pinval){
        /*松开 : 最后一个参数表示:0-松开, 1-按下*/
        input_event(buttons_dev, EV_KEY, pindesc->key_val, 0);
        input_sync(buttons_dev); //同步事件,表示上报完
    }else{
        /*按下*/
        input_event(buttons_dev, EV_KEY, pindesc->key_val, 1);
        input_sync(buttons_dev);
    }
}

底层驱动可以通过input_event上报事件信息,上层应用也可以通过此结构体来获取事件信息

struct input_event {

         structtimeval time;   /* 事件产生时间 */

         __u16type;               /* 事件类型 */

         __u16code;              /* 事件代码 */

         __s32value;             /* 事件值 */

};

n  报告输入事件

// 报告按键值

voidinput_report_key(struct input_dev *dev, unsigned int code, int value)

// 报告相对坐标

voidinput_report_rel(struct input_dev *dev, unsigned int code, int value)

//报告绝对坐标

void input_report_abs(struct input_dev *dev, unsigned int code, int value)

//用于事件同步 ,防止数据混乱

void input_sync(struct input_dev *dev)

以上都是对Input_event的封装

void input_event(struct input_dev *dev,unsigned int type, unsigned int code, intvalue)

实际使用程序流程

1. 应用程序调用open()

根据以前的经验我们最终会最终调用了evdev_open()

evdev_open(struct inode *inode, struct file *file)

    |

   struct evdev *evdev = container_of(inode->i_cdev, struct evdev, cdev); // inode->i_cdev就是connect()中住的的cdev

    unsigned int bufsize = evdev_compute_buffer_size(evdev->handle.dev); // 通过handle找到 input device,根据input device 获取缓冲区的大小(几个input event),但是我们驱动中未给定缓冲区大小,系统会自动给定一个

    unsigned int size = sizeof(struct evdev_client) + bufsize * sizeof(struct input_event);  // size包含了很多个input event

    struct evdev_client *client;

    client = kzalloc(size, GFP_KERNEL | __GFP_NOWARN); // 分配一个client对象,用来描述一个缓冲队列,存放的就是input_event

    client->bufsize = bufsize; // client中有一个缓冲区

    spin_lock_init(&client->buffer_lock);

    client->evdev = evdev;   // evdev_client中记录evdev

    evdev_attach_client(evdev, client); // 将client 加入到evdev中的一个小链表中

        |

        list_add_tail_rcu(&client->node, &evdev->client_list);

        file->private_data = client;  // evdev_client记录到file中,方便其他接口调用(这里是open(),其他接口还有read()、write())

总结:

1. 为输入设备分配一个缓冲区evdev_client,用于存放input device层上报的数据

2. evdev_client中记录evdev

3. evdev_client记录到file中,方便其他read() write() 等接口使用

==============================================

2.应用程序调用read()

read(fd, &event, sizeof(struct input_event));

-----------------------------------------

vfs

sys_read(); // 系统调用

file->f_ops->read(); // fd就是file数组的下表,通过传入的fd找到file,其中的f_ops在open()的时候已经获取并保存

-----------------------------------------

static ssize_t evdev_read(struct file *file, char __user *buffer,
			  size_t count, loff_t *ppos)
{
	struct evdev_client *client = file->private_data; // 获取open() 分配的缓冲区
	struct evdev *evdev = client->evdev;   // 获取到evdev
	struct input_event event;	// 表示一个数据包,要给用户
	size_t read = 0;
	int error;

	if (count != 0 && count < input_event_size())
		return -EINVAL;

	for (;;) {
		if (!evdev->exist || client->revoked)
			return -ENODEV;

		// 实现非阻塞 -- 队列为空,且为非阻塞模式,直接返回again
		if (client->packet_head == client->tail &&   // 队列的头跟尾位置一样 == 队列为空
		    (file->f_flags & O_NONBLOCK))   // 非阻塞
			return -EAGAIN;

		/*
		 * count == 0 is special - no IO is done but we check
		 * for error conditions (see above).
		 */
		if (count == 0)
			break;

		while (read + input_event_size() <= count &&    // 这里判断要取的数据个数,count是要取得数据个数
		       evdev_fetch_next_event(client, &event)) {  // 1. 从client的缓冲区取数据,放到event中

			if (input_event_to_user(buffer + read, &event))  // 2. 把数据给用户空间
				return -EFAULT;

			read += input_event_size();    // 3. 统计上报多少数据
		}

		if (read)
			break;

		if (!(file->f_flags & O_NONBLOCK)) {   // 如果当前不是非阻塞模式,即阻塞模式
			error = wait_event_interruptible(evdev->wait,   // 休眠 - 条件不满足就睡眠:
					client->packet_head != client->tail ||  // 队列头不等于尾 -> 有数据
					!evdev->exist || client->revoked);
			if (error)
				return error;
		}
	}

	return read;
}

总结:

1. 如果没数据,就休眠等待

2. 如果有数据,就会从缓冲区client->buffer[client->tail++]拿数据,通过copy_to_user上报给用户

疑问:

1. 数据到底是如何存放在缓冲区的

2. 等待队列是谁唤醒的

==============================================

上报流程:

input_event(buttons_dev, EV_KEY, pindesc->key_val, 0);

    |

    input_handle_event(dev, type, code, value);

        |

        if (disposition & INPUT_PASS_TO_HANDLERS) { // input device数据交给input handler处理

            struct input_value *v;

            v = &dev->vals[dev->num_vals++]; // 将input device获取到的数据暂存到dev->vals

            v->type = type;

            v->code = code;

            v->value = value;

           input_pass_values(dev, dev->vals, dev->num_vals);

                |

                list_for_each_entry_rcu(handle, &dev->h_list, d_node) // 通过inpit device中与handle建立连接的 h_list 成员找到 handle

                if (handle->open)

                       input_to_handler(handle, vals, count);

                             |

                             struct input_handler *handler = handle->handler; // 通过出入的handle找到input handler(这里是evdev)

                             if (handler->events) // 首选events(), 没有才调用event()

                                     handler->events(handle, vals, count); // 调用events()

                             else if (handler->event)

                                     for (v = vals; v != end; v++)

                                             handler->event(handle, v->type, v->code, v->value);

static struct input_handler evdev_handler = {
    .event = evdev_event,
    ...
};
/*
 * Pass incoming events to all connected clients.
 */
static void evdev_events(struct input_handle *handle,
			 const struct input_value *vals, unsigned int count)
{
	struct evdev *evdev = handle->private; // 从handle中拿到evdev -- connect()中保存了:evdev->handle.private = evdev;
	struct evdev_client *client;
	/*	
		如果多个应用进程打开了同一个input device, 每次open()都会生成一个evdev_client
		evdev_client挂载到evdev的client_list链表中
		input_report_abs()时,handler会把数据copy到client_list所有的evdev_client的buffer中
		input_mt_sync(),逐一唤醒
		*/
	ktime_t ev_time[EV_CLK_MAX];

	ev_time[EV_CLK_MONO] = ktime_get();
	ev_time[EV_CLK_REAL] = ktime_mono_to_real(ev_time[EV_CLK_MONO]);
	ev_time[EV_CLK_BOOT] = ktime_mono_to_any(ev_time[EV_CLK_MONO],
						 TK_OFFS_BOOT);

	rcu_read_lock();

	client = rcu_dereference(evdev->grab);

        // 向每一个打开设备的程序发送数据包
	if (client)
		evdev_pass_values(client, vals, count, ev_time);  
	else
		list_for_each_entry_rcu(client, &evdev->client_list, node)
			evdev_pass_values(client, vals, count, ev_time);

	rcu_read_unlock();

}

evdev_pass_values(client, vals, count, ev_time);

static void evdev_pass_values(struct evdev_client *client,
			const struct input_value *vals, unsigned int count,
			ktime_t *ev_time)
{
	struct evdev *evdev = client->evdev;  // 通过client 获取到 evdev
	const struct input_value *v;
	struct input_event event;    // 数据包
	bool wakeup = false;

	if (client->revoked)
		return;

	event.time = ktime_to_timeval(ev_time[client->clk_type]);  // 填充数据包中的时间戳

	/* Interrupts are disabled, just acquire the lock. */
	spin_lock(&client->buffer_lock);

	for (v = vals; v != vals + count; v++) {  // 将input device上报的数据封装成 input_event对象
		if (__evdev_is_filtered(client, v->type, v->code))
			continue;

		// 唤醒等待队列 -- 如果调用了input_sync() --  input_event(dev, EV_SYN, SYN_REPORT, 0);
		if (v->type == EV_SYN && v->code == SYN_REPORT) { 
			/* drop empty SYN_REPORT */
			if (client->packet_head == client->head)
				continue;

			wakeup = true;
		}

		event.type = v->type;
		event.code = v->code;
		event.value = v->value;
		__pass_event(client, &event);
	}

	spin_unlock(&client->buffer_lock);

	if (wakeup)
		wake_up_interruptible(&evdev->wait);  // 唤醒等待队列
}
static void __pass_event(struct evdev_client *client,
			 const struct input_event *event)
{
	client->buffer[client->head++] = *event;  // 将input event数据放入缓冲区
	client->head &= client->bufsize - 1;

	if (unlikely(client->head == client->tail)) {
		/*
		 * This effectively "drops" all unconsumed events, leaving
		 * EV_SYN/SYN_DROPPED plus the newest event in the queue.
		 */
		client->tail = (client->head - 2) & (client->bufsize - 1);

		client->buffer[client->tail].time = event->time;
		client->buffer[client->tail].type = EV_SYN;
		client->buffer[client->tail].code = SYN_DROPPED;
		client->buffer[client->tail].value = 0;

		client->packet_head = client->tail;
	}

	if (event->type == EV_SYN && event->code == SYN_REPORT) { 
		client->packet_head = client->head;
		kill_fasync(&client->fasync, SIGIO, POLL_IN);
	}
}

  总结:
    1. 数据到底是如何存放在缓冲区的
        input_event()将数据交给handler,调用events(),将数据放入缓冲区client->buffer[client->head++] = *event;
    2. 等待队列是谁唤醒的
        input_sync() 显式唤醒等待队列 wake_up_interruptible(&evdev->wait);

测试:
1.
hexdump /dev/event1  (open(/dev/event1), read(), )
           秒        微秒    类  code    value
0000000 0bb2 0000 0e48 000c 0001 0026 0001 0000
0000010 0bb2 0000 0e54 000c 0000 0000 0000 0000
0000020 0bb2 0000 5815 000e 0001 0026 0000 0000
0000030 0bb2 0000 581f 000e 0000 0000 0000 0000

2. 如果没有启动QT:
cat /dev/tty1
按:s2,s3,s4
就可以得到ls

或者:
exec 0</dev/tty1
然后可以使用按键来输入


3. 如果已经启动了QT:
可以点开记事本
然后按:s2,s3,s4

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值