IGKBoard(imx6ull)-Linux下TTY串口编程


一、TTY介绍

(1)理解tty

简单来说,tty 是 Teletype / Teletypewriter 的缩写。而 Teletype / Teletypewriter 的中文意思则是电传打字机。
大家可以参考这篇文章来理解:点击打开(对终端、命令行和shell等的理解)

终端 = tty = Teletype / Teletypewriter = 电传打字机(可以这样理解)

简单来说,tty 是终端的统称。早期的终端是电传字打印机(Teletype / Teletypewriter),英文缩写就是 tty。虽然终端设备已经不再限制于电传打字机了,但是 tty 这个名称还是就这么保留了下来。

(2)tty设备节点

我们到/dev/路径下看一下tty的设备节点,可以看见很多:

root@igkboard:~# ls /dev                   
autofs           gpiochip1     loop0    mmcblk1boot0  pps1   ram14   rtc     tty10  tty20  tty30  tty40  tty50  tty60    ttymxc6             vcs6   vcsu3
block            gpiochip2     loop1    mmcblk1boot1  ptmx   ram15   rtc0    tty11  tty21  tty31  tty41  tty51  tty61    ubi_ctrl            vcsa   vcsu4
bus              gpiochip3     loop2    mmcblk1p1     ptp0   ram2    rtc1    tty12  tty22  tty32  tty42  tty52  tty62    udev_network_queue  vcsa1  vcsu5
char             gpiochip4     loop3    mmcblk1p2     ptp1   ram3    shm     tty13  tty23  tty33  tty43  tty53  tty63    urandom             vcsa2  vcsu6
console          hwrng         loop4    mmcblk1rpmb   pts    ram4    snd     tty14  tty24  tty34  tty44  tty54  tty7     v4l                 vcsa3  vga_arbiter
cpu_dma_latency  i2c-1         loop5    mqueue        ram0   ram5    stderr  tty15  tty25  tty35  tty45  tty55  tty8     vcs                 vcsa4  vhci
disk             initctl       loop6    mxc_asrc      ram1   ram6    stdin   tty16  tty26  tty36  tty46  tty56  tty9     vcs1                vcsa5  video0
fd               input         loop7    net           ram10  ram7    stdout  tty17  tty27  tty37  tty47  tty57  ttymxc0  vcs2                vcsa6  watchdog
full             kmsg          mapper   null          ram11  ram8    tty     tty18  tty28  tty38  tty48  tty58  ttymxc1  vcs3                vcsu   watchdog0
fuse             log           mem      port          ram12  ram9    tty0    tty19  tty29  tty39  tty49  tty59  ttymxc2  vcs4                vcsu1  zero
gpiochip0        loop-control  mmcblk1  pps0          ram13  random  tty1    tty2   tty3   tty4   tty5   tty6   ttymxc3  vcs5                vcsu2

这些节点的总结如下(注:表格中的 X 代表数字编号)
在这里插入图片描述


二、tty串口应用编程

(1)串口基本操作

【1】打开串口

打开串口连接的时候,程序在open函数中除了Read+Write模式以外还需要指定O_NOCTTY选项:

fd=open("/dev/ttymxc0",O_RDWR|O_NOCTTY);

标志O_NOCTTY告诉系统这个程序不会成为这个端口上的“控制终端”。如果不这样做的话,所有的输入,比如键盘上过来的Ctrl+C中止信号等等,会影响到你的进程。

【1】读写数据

读数据的时候需要找准时机,需要知道串口何时有数据,可以使用linux下的轮询机制进行监控串口的文件描述符:

rv = read(fd, buf, 1024);

Linux下一切皆文件,写数据直接使用write、fputs等函数即可直接向串口发送数据:

rv= write(fd, buf, sizeof(buf));
【1】关闭串口

可以使用close系统调用关闭串口:

close(fd);

(2)termios 结构体(配置)

termios是面向所有终端设备的。termios 结构体:

struct termios
{
	tcflag_t c_iflag; /* input mode flags */
	tcflag_t c_oflag; /* output mode flags */
	tcflag_t c_cflag; /* control mode flags */
	tcflag_t c_lflag; /* local mode flags */
	cc_t c_line; /* line discipline */
	cc_t c_cc[NCCS]; /* control characters */
	speed_t c_ispeed; /* input speed */
	speed_t c_ospeed; /* output speed */
};

c_iflag

输入模式标志,控制终端输入方式。
在这里插入图片描述

c_oflag
输出模式标志,控制终端输出方式。
在这里插入图片描述

c_cflag
控制模式标志,指定终端硬件控制信息.。
在这里插入图片描述
c_lflag
本地模式标志,控制终端编辑功能。
在这里插入图片描述
c_cc[NCCS]
​ ​控制字符​​,用于保存终端驱动程序中的​ 特殊字符​​,如输入结束符等。
在这里插入图片描述

(3)终端控制API函数

具体函数参数等可以自行man看看,后面的代码中涉及到的也可以拿来参考。

tcgetattr      取属性(termios结构)
tcsetattr      设置属性(termios结构)
cfgetispeed    得到输入速度
cfgetospeed    得到输出速度
cfsetispeed    设置输入速度
cfsetospeed    设置输出速度
tcdrain        等待所有输出都被传输
tcflow         挂起传输或接收
tcflush        刷清未决输入和/或输出
tcsendbreak    送BREAK字符
tcgetpgrp      得到前台进程组ID
tcsetpgrp      设置前台进程组ID
cfmakeraw      将终端设置成原始模式
cfsetspeed     设置输入输出速度

其中设置输入输出波特率的时候需要注意,波特率可以设置如下:
B0​、B50​、B75​、B110​、B134​、B150​、B200​、B300​、B600​、B1200​、B1800​、B2400​、B4800​、B9600​、B19200​、B38400​、 B57600、 B115200、​ B230400

struct termios uart_cfg;memset(&uart_cfg,0,sizeof(struct termios));cfsetispeed(&uart_cfg,B115200); //设置输入波特率​
cfsetospeed(&uart_cfg,B115200); //设置输出波特率

(4)终端的三种工作模式

终端的三种工作模式,分别是规范模式 canonical mode,非规范模式 non-canonical mode 和原始模式 raw mode。

通过设置 c_lflag 设置 ICANNON 标志来定义终端是以规范模式还是非规范模式工作,默认为规范模式。

  • 规范模式

所有输入基于行进行处理。在用户输入一个行结束符(回车符、EOF【End Of File】等)之前,系统调用read()函数读不到用户输入的任何字符。其次,除了EOF之外的行结束符与普通字符一样会被read()函数读取到缓冲区中。一次调用read()只能读取一行数据。

  • 原始模式

是一种特殊的非规范模式,所有的输入数据以字节为单位被处理。即有一个字节输入时,触发输入有效。

  • 非规范模式

所有输入时即时有效的,不需要用户另外输入行结束符。
在非规范模式下,对参数 MIN(c_cc[VMIN])和 TIME(c_cc[VTIME])的设置决定 read 函数的调用方式,MIN 和 TIME 的取值不同,会有以下四种不同的情况:
在这里插入图片描述


三、tty串口应用编程实现

(1)硬件连接

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

(2)应用编程代码

/*********************************************************************************
 *      Copyright:  (C) 2023 WangDengtao<1799055460@qq.com>
 *                  All rights reserved.
 *
 *       Filename:  uart_test.c
 *    Description:  This file 
 *                 
 *        Version:  1.0.0(2023年04月08日)
 *         Author:  WangDengtao <1799055460@qq.com>
 *      ChangeLog:  1, Release initial version on "2023年04月08日 14时45分32秒"
 *                 
 ********************************************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <getopt.h>
#include <libgen.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <termios.h>
#include <signal.h>

#define	READ_FLAG 0			/*读标志*/
#define	WRITE_FLAG 1		/*写标志*/

struct uart_parameter {
    unsigned int    baudrate;       // 波特率 
    unsigned char   dbit;           // 数据位 
    char            parity;         // 奇偶校验 
    unsigned char   sbit;           // 停止位 
};

static struct termios   oldtio;     // 用于保存终端的配置参数
static int              fd_uart;  	// 串口终端对应的文件描述符

static int uart_init(const char *device);//串口初始化
static int uart_configuration(const struct uart_parameter *para);//串口配置
static void async_io_init(void);//异步i/o初始化函数
static void io_handler(int sig, siginfo_t *info, void *context);//信号处理函数,当串口有数据可读时,会跳转到该函数执行
static void usage(char *progname);//提示信息

int main(int argc, char *argv[])
{
    struct uart_parameter   uart_para;
    char                    device[64];
    int                     rw_flag = -1;
    unsigned char           write_buf[10] = {0x11, 0x22, 0x33, 0x44,0x55, 0x66, 0x77, 0x88};    
    int                     n;
    int                     opt;
	char					*progname=NULL;

    memset(&uart_para, 0x0, sizeof(struct uart_parameter));
    memset(device, 0x0, sizeof(device));

    struct option           long_options[] = {
        {"device", required_argument, NULL, 'D'},
        {"type", required_argument, NULL, 'T'},
        {"brate", no_argument, NULL, 'b'},
        {"dbit", no_argument, NULL, 'd'},
        {"parity", no_argument, NULL, 'p'},
        {"sbit", no_argument, NULL, 's'},
        {"help", no_argument, NULL, 'h'},
        {NULL, 0, NULL, 0}
    };

    memset(&uart_para, 0x0, sizeof(struct uart_parameter));
	progname = (char *)basename(argv[0]);

    while((opt = getopt_long(argc, argv, "D:T:b:d:p:s:h", long_options, NULL)) != -1)
    {
        switch(opt)
        {
            case'D':
                strcpy(device, optarg);
                break;
                
            case'T':
                if (!strcmp("read", optarg))
                {
                    rw_flag = READ_FLAG;   
                }       
                else if (!strcmp("write", optarg))
                {
                    rw_flag = WRITE_FLAG;   
                }
                break;
                
            case'b':
                uart_para.baudrate = atoi(optarg);
                break;

            case'd':
                uart_para.dbit = atoi(optarg);
                break;

            case'p':
                uart_para.parity = *optarg;
                break;

            case's':
                uart_para.sbit = atoi(optarg);
                break;

            case'h':
                usage(progname);
                return 0;

            default:
                break;
        }
    }

    if (NULL == device || -1 == rw_flag) 
    {
        usage(progname);
        return -1;
    }

    /* 串口初始化 */ 
    if (uart_init(device))
    {
        printf("fail to execute uart_init\n");
        return -2;
    }

    /* 串口配置 */ 
    if (uart_configuration(&uart_para)) 
    {
        /* 恢复之前的配置 */
        tcsetattr(fd_uart, TCSANOW, &oldtio);   
        return -3;
    }

    /* 通过读写标志判断读写,然后进行读写 */
    switch (rw_flag) 
    {
        case 0:  // 读串口数据
            async_io_init();	// 我们使用异步 i/o 方式读取串口的数据,调用该函数去初始化串口的异步 i/o
            for ( ; ; )         // 进入休眠,等待有数据可读,有数据可读之后就会跳转到 io_handler() 函数
            {
                sleep(1);
            }
            break;
        case 1:   // 向串口写入数据
            for ( ; ; ) 
            {   		
                write(fd_uart, write_buf, 8); 	
                sleep(1);       	
            }
            break;
    }

    tcsetattr(fd_uart, TCSANOW, &oldtio);  
    close(fd_uart);
    
    return 0;
}

static int uart_init(const char *device)
{
    fd_uart = open(device, O_RDWR | O_NOCTTY);
    if (0 > fd_uart)
    {
        printf("fail to open uart file\n");
        return -1;
    }

    /* 获取串口当前的配置参数 */
    if (0 > tcgetattr(fd_uart, &oldtio))
    {
        printf("fail to get old attribution of terminal\n");
        close(fd_uart);
        return -2;
    }

    return 0;
}


static int uart_configuration(const struct uart_parameter *para)
{
    struct termios newtio;
    speed_t speed;

    /* 设置为原始模式
     * 配置为原始模式相当于已经对 newtio 做了如下配置
     * IGNBRK 忽略输入终止条件,BRKINT 检测到终止条件发送 SIGINT 信号,PARMRK 对奇偶校验做出标记
     * ISTRIP 裁剪数据位为 7 bit,去掉第八位,INLCR 换行符转换为回车符,IGNCR 忽略回车符
     * ICRNL 将回车符转换为换行符,IXON 启动输出流控
     * OPOST 启用输出处理功能
     * ECHO 使能回显,ICANON 规范模式,ISIG 收到信号产生相应的信号,IEXTEN 输入处理
     * CSIZE 数据位掩码,PARENB 使能校验,CS8 8 个数据位
     * termios_p->c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP| INLCR | IGNCR | ICRNL | IXON);
     * termios_p->c_oflag &= ~OPOST;
     * termios_p->c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
     * termios_p->c_cflag &= ~(CSIZE | PARENB);
     * termios_p->c_cflag |= CS8;
     */
    memset(&newtio, 0x0, sizeof(struct termios));
    cfmakeraw(&newtio);

    /* CREAD 使能接受 */
    newtio.c_cflag |= CREAD;

    /* 设置波特率 */
    switch (para->baudrate)
    {
        case 1200:
            speed = B1200;
            break;
        case 1800:
            speed = B1800;
            break;
        case 2400:
            speed = B2400;
            break;
        case 4800:
            speed = B4800;
            break;
        case 9600:
            speed = B9600;
            break;
        case 19200:
            speed = B19200;
            break;
        case 38400:
            speed = B38400;
            break;
        case 57600:
            speed = B57600;
            break;
        case 115200:
            speed = B115200;
            break;
        case 230400:
            speed = B230400;
            break;
        case 460800:
            speed = B460800;
            break;
        case 500000:
            speed = B500000;
            break;
        default:
            speed = B115200;
            printf("default baud rate is 115200\n");
            break;
    }

    /* cfsetspeed 函数,设置波特率 */
    if (0 > cfsetspeed(&newtio, speed))
    {
        printf("fail to set baud rate of uart\n");
        return -1;
    }

	/* 设置数据位大小
     * CSIZE 是数据位的位掩码,与上掩码的反,就是将数据位相关的比特位清零
     * CSX (X=5,6,7,8) 表示数据位位数
     */
    newtio.c_cflag &= ~CSIZE;
    switch (para->dbit)
    {
        case 5:
            newtio.c_cflag |= CS5;
            break;
        case 6:
            newtio.c_cflag |= CS6;
            break;
        case 7:
            newtio.c_cflag |= CS7;
            break;
        case 8:
            newtio.c_cflag |= CS8;
            break;
        default:
            newtio.c_cflag |= CS8;
            printf("default data bit size is 8\n");
            break;
   	}

	/* 设置奇偶校验
     * PARENB 用于使能校验
     * INPCK 用于对接受的数据执行校验
 	 * PARODD 指的是奇校验
     */
    switch (para->parity)
    {
        case 'N':   //无校验
            newtio.c_cflag &= ~PARENB;
            newtio.c_iflag &= ~INPCK;
            break;
        case 'O':   //奇校验
            newtio.c_cflag |= (PARODD | PARENB);
            newtio.c_iflag |= INPCK;
            break;
        case 'E':   //偶校验
            newtio.c_cflag |= PARENB;
            newtio.c_cflag &= ~PARODD;
            newtio.c_iflag |= INPCK;
            break;
        default:    //默认配置为无校验
            newtio.c_cflag &= ~PARENB;
            newtio.c_iflag &= ~INPCK;
            printf("default parity is N (no check)\n");
            break;
    }

	/* 设置停止位
     * CSTOPB 表示设置两个停止位
     */
    switch (para->sbit)
    {
        case 1:     //1个停止位
            newtio.c_cflag &= ~CSTOPB;
            break;
        case 2:     //2个停止位
            newtio.c_cflag |= CSTOPB;
            break;
        default:    //默认配置为1个停止位
            newtio.c_cflag &= ~CSTOPB;
            printf("default stop bit size is 1\n");
            break;
   	}

	/* 将 MIN 和 TIME 设置为 0,通过对 MIN 和 TIME 的设置有四种 read 模式
     * read 调用总是会立即返回,若有可读数据,则读数据并返回被读取的字节数,否则读取不到数据返回 0
     */
    newtio.c_cc[VTIME] = 0;
    newtio.c_cc[VMIN] = 0;

    /* 清空输入输出缓冲区 */
    if (0 > tcflush(fd_uart, TCIOFLUSH))
    {
        printf("fail to flush the buffer\n");
        return -3;
    }

    /* 写入配置,使配置生效 */
    if (0 > tcsetattr(fd_uart, TCSANOW, &newtio))
    {
        printf("fail to set new attribution of terminal\n");
        return -4;
    }

    return 0;
}

/* 异步 i/o 初始化函数 */
static void async_io_init(void)
{
    struct sigaction    sigatn;
    int                 flag;

    /* 使能异步 i/o,获取当前进程状态,并开启当前进程异步通知功能 */
    flag = fcntl(fd_uart, F_GETFL);
    flag |= O_ASYNC;
    fcntl(fd_uart, F_SETFL, flag);

    /* 设置异步 i/o 的所有者,将本应用程序进程号告诉内核 */
    fcntl(fd_uart, F_SETOWN, getpid());

    /* 指定实时信号 SIGRTMIN 作为异步 i/o 通知信号 */
    fcntl(fd_uart, F_SETSIG, SIGRTMIN);

    /* 为实时信号 SIGRTMIN 注册信号处理函数
     * 当串口有数据可读时,会跳转到 io_handler 函数
     */
    sigatn.sa_sigaction = io_handler;
    sigatn.sa_flags = SA_SIGINFO;

    /* 初始化信号集合为空 */
    sigemptyset(&sigatn.sa_mask);

    /* sigaction 的功能是为信号指定相关的处理程序,但是它在执行信号处理程序时
     * 会把当前信号加入到进程的信号屏蔽字中,从而防止在进行信号处理期间信号丢失
     */
    sigaction(SIGRTMIN, &sigatn, NULL);
}

/* 信号处理函数,当串口有数据可读时,会跳转到该函数执行 */
static void io_handler(int sig, siginfo_t *info, void *context)
{
    unsigned char   buf[10];
    int             ret;
    int             n;

    memset(buf, 0x0, sizeof(buf));

    if(SIGRTMIN != sig)
    {
        return;
    }

    /* 判断串口是否有数据可读 */
    if (POLL_IN == info->si_code)
    {
        ret = read(fd_uart, buf, 8);
        printf("[ ");
        for (n = 0; n < ret; n++)
        {
            printf("0x%hhx ", buf[n]);
        }
        printf("]\n");
    }
}

static void usage(char *progname)
{
	printf("Usage: %s [OPTION]...\n", progname);

	printf("The usage has been configured\n");
	printf("%s -D device -T write/read\n", progname);
	printf("If you want a different configuration, see below:\n");
    printf("-b brate\n");
    printf("-d dbit\n");
    printf("-p parity\n");
    printf("-s sbit\n");
    printf("-h help\n");

    return;
}

Makefile:

CC=arm-linux-gnueabihf-gcc
APP_NAME=uart_test

all:clean
	@${CC} ${APP_NAME}.c -o ${APP_NAME} -D_GNU_SOURCE

clean:
	@rm -f ${APP_NAME}

然后将生成的可执行文件下载到我们的开发板上。

如果开发板上没有使能我们的设备树插件,需要使能,然后重启即可。

root@igkboard:/run/media/mmcblk1p1# cat config.txt 
#Enble UART overlays
dtoverlay_uart=2 3 4 7

(3)运行结果

root@igkboard:~# tftp -gr uart_test 192.168.137.91
root@igkboard:~# chmod a+x uart_test 
root@igkboard:~# ./uart_test -h
Usage: uart_test [OPTION]...
The usage has been configured
uart_test -D device -T write/read
If you want a different configuration, see below:
-b brate
-d dbit
-p parity
-s sbit
-h help

打开我们的串口调试助手,连接上与转换器的接口。

串口调试助手中需要选择HEX(16进制)显示,不然会显示乱码:串口的Hex/AscII发送与显示

在这里插入图片描述

【1】发送数据

在命令上执行:

./uart_test -D /dev/ttymxc1 -T write

在这里插入图片描述

【2】接收数据

在命令上执行:

./uart_test -D /dev/ttymxc1 -T read

在这里插入图片描述


四、Linux下异步I/O:O_ASYNC标志(信号驱动I/O)

在上面的代码中,我们用到了 O_ASYNC 标志来完成I/O异步通信。什么为异步I/O,假设两个设备通信,其中一个设备正在接受发送过来的数据,但是同时也需要自己发送出去,这样就叫异步I/O,如果同步的话,这个设备发送完数据之后去接受,数据已经错过了。

(1)信号驱动I/O

在IO多路复用中,进程是通过系统调用(select、epoll)来检测文件描述符上是否可以执行IO。而在信号驱动IO中,进程请求内核当文件描述符上可执行IO操作时为自己发送一个信号。之后进程就可以执行任何其他的任务直到IO就绪为止。下面来说明一下上述代码中所使用信号驱动IO步骤:

1、得到文件描述符的状态标志集,为该状态标志集添加一个 O_ASYNC 属性:

flag = fcntl(fd_uart, F_GETFL);
/*
后面两步可以简化为一步:
fcntl(fd_uart, F_SETFL, flag | O_ASYNC);
*/
flag |= O_ASYNC;
fcntl(fd_uart, F_SETFL, flag);

2、用fcntl函数来设置一个用来接受信号的进程:

fcntl(fd_uart, F_SETOWN, getpid());

3、指定实时信号 SIGRTMIN 作为异步 i/o 通知信号,默认情况下,这个通知信号为SIGIO:

sigatn.sa_sigaction = io_handler;

4、为信号设置一个处理函数,用来读取并处理位于输入缓存中的数据:

sigatn.sa_sigaction = io_handler;

(2)信号处理函数

/* 信号处理函数,当串口有数据可读时,会跳转到该函数执行 */
static void io_handler(int sig, siginfo_t *info, void *context)
{
    unsigned char   buf[10];
    int             ret;
    int             n;

    memset(buf, 0x0, sizeof(buf));

    if(SIGRTMIN != sig)
    {
        return;
    }

    /* 判断串口是否有数据可读 */
    if (POLL_IN == info->si_code)
    {
        ret = read(fd_uart, buf, 8);
        printf("[ ");
        for (n = 0; n < ret; n++)
        {
            printf("0x%hhx ", buf[n]);
        }
        printf("]\n");
    }
}

相信大家看不懂的也只有这一行:POLL_IN == info->si_code

我们来看一下 siginfo_t 结构体:

siginfo_t {
    int si_signo; /* Signal number */
    int si_errno; /* An errno value */
    int si_code; /* Signal code */
    int si_trapno; /* Trap number that caused hardware-generated signal(unused on most
    architectures) */
    pid_t si_pid; /* Sending process ID */
    uid_t si_uid; /* Real user ID of sending process */
    int si_status; /* Exit value or signal */
    clock_t si_utime; /* User time consumed */
    clock_t si_stime; /* System time consumed */
    sigval_t si_value; /* Signal value */
    int si_int; /* POSIX.1b signal */
    void *si_ptr; /* POSIX.1b signal */
    int si_overrun; /* Timer overrun count; POSIX.1b timers */
    int si_timerid; /* Timer ID; POSIX.1b timers */
    void *si_addr; /* Memory location which caused fault */
    long si_band; /* Band event (was int in glibc 2.3.2 and earlier) */
    int si_fd; /* File descriptor */
    short si_addr_lsb; /* Least significant bit of address(since Linux 2.6.32) */
    void *si_call_addr; /* Address of system call instruction(since Linux 3.5) */
    int si_syscall; /* Number of attempted system call(since Linux 3.5) */
    unsigned int si_arch; /* Architecture of attempted system call(since Linux 3.5) */
}

我们着重看 si_code
在这里插入图片描述


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值