【迅为电子】RK3568驱动指南|第十七篇 串口-第202章 串口编程

瑞芯微RK3568芯片是一款定位中高端的通用型SOC,采用22nm制程工艺,搭载一颗四核Cortex-A55处理器和Mali G52 2EE 图形处理器。RK3568 支持4K 解码和 1080P 编码,支持SATA/PCIE/USB3.0 外围接口。RK3568内置独立NPU,可用于轻量级人工智能应用。RK3568 支持安卓 11 和 linux 系统,主要面向物联网网关、NVR 存储、工控平板、工业检测、工控盒、卡拉 OK、云终端、车载中控等行业。


【公众号】迅为电子

【粉丝群】258811263(加群获取驱动文档+例程)

【视频观看】嵌入式学习之Linux驱动(第十七篇 串口_全新升级)_基于RK3568

【购买链接】迅为RK3568开发板瑞芯微Linux安卓鸿蒙ARM核心板人工智能AI主板


第202章 串口编程

为了控制串口通信,我们需要进行串口编程。在Linux系统中,可以使用文件I/O操作和ioctl操作来进行串口编程。文件I/O操作可以用于读取和写入串口数据,ioctl操作可以用于设置串口参数、控制流控制和获取串口状态等操作

202.1串口设备节点介绍 

在嵌入式设备中,串口设备通常以字符设备节点的形式出现在Linux系统中。在Linux系统中,每个设备都由一个设备节点(device node)来表示,设备节点是与设备相关联的一个文件,以/dev目录下的文件形式存在。

串口设备节点通常以tty开头,具体命名方式根据串口的类型和数量不同而不同。开发板系统启动之后,使用以下命令打印终端设备节点,如下图所示:

ls /dev/tty*

dev/ttyX(X 是一个数字编号,譬如 0、1、2、3 等)设备节点:tty是teletype 的简称,在 Linux 中,/dev/ttyX 代表的都是本地终端, Linux 内核在初始化时所生成的 63 个本地终端,包括/dev/tty1~/dev/tty63 一共63 个本地终端,可以是连接到开发板的LCD 显示器、键盘和鼠标等。

串口终端设备节点:从开发板原理图可以了解到,在iTOP-3568开发板上有四个串口,分别UART2、UART4、UART7、UART9,其中UART2为串口调试终端,对应的设备节点为/dev/ttyFIQ0,其他三个串口UART4、UART7、UART9分别对应/dev/ttyS4、/dev/ttyS7、/dev/ttyS9。

基于USB的虚拟串口:ttyGS0以及ttyUSBX(X 是一个数字编号,譬如 0、1、2、3 等)都是USB的虚拟串口,其中ttyGS0为烧写usb虚拟出的串口,在系统启动之后可以在windows终端通过“adb shell”命令进入开发板控制台。ttyUSBX在这里为4G模块的虚拟串口。

202.2 struct termios结构体

struct termios 是 Linux 内核中用于描述终端设备(包括串口设备)参数的结构体。它定义在 <linux/termios.h> 头文件中,包含了多个字段,用于配置和管理终端设备的属性和行为,包括了输入输出波特率、数据位、校验位、停止位等。其定义如下:

struct termios {
    tcflag_t c_iflag;  // 输入模式标志
    tcflag_t c_oflag;  // 输出模式标志
    tcflag_t c_cflag;  // 控制模式标志
    tcflag_t c_lflag;  // 本地模式标志
    cc_t c_cc[NCCS];   // 控制字符数组
};

下面是 struct termios 结构体的一些重要字段:

  • tcflag_t c_iflag:该字段包含了输入模式标志,用于配置终端设备的输入行为,如输入控制字符、输入数据处理等。
  • tcflag_t c_oflag:该字段包含了输出模式标志,用于配置终端设备的输出行为,如输出数据处理、输出控制字符等。
  • tcflag_t c_cflag:该字段包含了控制模式标志,用于配置终端设备的控制参数,如波特率、数据位数、停止位数、校验位等。
  • tcflag_t c_lflag:该字段包含了本地模式标志,用于配置终端设备的本地操作以及输入输出行为。
  • cc_t c_cc[NCCS]:该字段包含了特殊控制字符数组,用于配置终端设备的控制字符,如终端的擦除字符、结束字符、停止字符等。

 

202.2.1 输入模式

在这个结构体中,最重要的c_cflag,可以控制模式控制终端设备的硬件特性,譬如对于串口来说,该字段比较重要,可设置串口波特率、数据位、校验位、停止位等硬件特性。通过设置 struct termios 结构中 c_cflag 成员的标志对控制模式进行配置。可用于 c_cflag 成员的标志如下所示:

成员

对应成员的含义

IGNBRK

忽略输入终止条件

BRKINT

当检测到输入终止条件时发送 SIGINT 信号

IGNPAR

忽略帧错误和奇偶校验错误

PARMRK

对奇偶校验错误做出标记

INPCK

对接收到的数据执行奇偶校验

ISTRIP

将所有接收到的数据裁剪为 7 比特位、也就是去除第八位

INLCR

将接收到的 NL(换行符)转换为 CR(回车符)

IGNCR

忽略接收到的 CR(回车符)

ICRNL

将接收到的 CR(回车符)转换为 NL(换行符)

IUCLC

将接收到的大写字符映射为小写字符

IXON

启动输出软件流控

202.2.2 输出模式

输出模式控制输出字符的处理方式,即由应用程序发送出去的字符数据在传递到串口或屏幕之前是如何处理的。可用于 c_oflag 成员的宏如下所示:

成员

对应成员的含义

OPOST

启用输出处理功能,如果不设置该标志则其他标志都被忽略

OLCUC

将输出字符中的大写字符转换成小写字符

ONLCR

将输出中的换行符(NL '\n')转换成回车符(CR '\r')

OCRNL

将输出中的回车符(CR '\r')转换成换行符(NL '\n')

ONOCR

在第 0 列不输出回车符(CR

ONLRET

不输出回车符

OFILL

发送填充字符以提供延时

OFDEL

如果设置该标志,则表示填充字符为 DEL 字符,否则为 NULL字符

 

202.2.3 控制模式

在这个结构体中,最终要的就是c_cflag ,可以控制模式控制终端设备的硬件特性,譬如对于串口来说,该字段比较重要,可设置串口波特率、数据位、校验位、停止位等硬件特性。通过设置 struct termios 结构中 c_cflag 成员的标志对控制模式进行配置。可用于 c_cflag 成员的标志如下所示:

c_cflag支持的常量名称

CBAUD

波特率的位掩码

B0

0波特率(放弃DTR)

B1800

1800波特率

B2400

2400波特率

B4800

4800波特率

B9600

9600波特率

B19200

19200波特率

B38400

38400波特率

B57600

57600波特率

B115200

115200波特率

CSIZE

数据位的位掩码

CS5

5个数据位

CS6

6个数据位

CS7

7个数据位

CS8

8个数据位

CSTOPB

2个停止位(不设则是1个停止位)

CREAD

接收使能

PARENB

校验位使能

PARODD

使用奇校验而不使用偶校验

HUPCL

最后关闭时挂线(放弃DTR)

CLOCAL

本地连接(不改变端口所有者)

LOBLK

块作业控制输出

CNET_CTSRTS

硬件流控制使能

202.2.4 本地模式

本地模式用于控制终端的本地数据处理和工作模式。通过设置 struct termios 结构体中 c_lflag 成员的标 志对本地模式进行配置。可用于 c_lflag 成员的标志如下所示:

c_iflag支持的常量名称

INPCK

奇偶校验使能

IGNPAR

忽略奇偶校验错误

PARMRK

奇偶校验错误掩码

ISTRIP

除去奇偶校验位

IXON

启动出口硬件流控

IXOFF

启动入口软件流控

IXANY

允许字符重新启动流控

IGNBRK

忽略中断情况

BRKINT

当发生中断时发送SIGINT信号

INLCR

将NL映射到CR

IGNCR

忽略CR

ICRNL

将CR映射到NL

ICANON

启用规范模式

202.2.5 特殊控制字符

特殊控制字符是一些字符组合,如 Ctrl+C、Ctrl+Z 等,当用户键入这样的组合键,终端会采取特殊处理方式。struct termios 结构体中 c_cc 数组将各种特殊字符映射到对应的支持函数。每个字符位置(数组下标)由对应的宏定义的,如下所示

c_cc 支持的常量名称

VKILL

删除行,对应键为CTRL+U

VEOF

位于文件结尾,对应键为CTRL+D

VEOL

 位于行尾,对应键为Carriage return(CR)

VEOL2

 位于第二行尾,对应键为Line feed(LF)

VMIN

指定了最少读取的字符数

VTIME

指定了读取每个字符的等待时间

VINTR

中断控制,对应键为CTRL+C

VQUIT

退出操作,对应键为CRTL+Z

VERASE

删除操作,对应键为Backspace(BS)

在以上所列举的这些宏定义中,TIME 和 MIN 值只能用于非规范模式,可用于控制非规范模式下 read()调用的一些行为特性,后面再向大家介绍。

202.2 常用串口控制函数

串口控制函数是用于管理和控制串口设备的函数,在 Linux 系统中主要通过 ioctl() 系统调用来实现。以下是一些常用的串口控制函数及其功能:

202.2.1 tcgetattr()函数

tcgetattr():该函数用于获取终端设备的属性,包括输入模式、输出模式、控制模式和本地模式等。通过该函数可以获取当前串口设备的配置参数。

tcsetattr() 函数是在 Linux 中用于设置终端设备的属性的函数,通常用于对串口设备的参数进行配置。该函数的原型如下:

int tcsetattr(int fd, int optional_actions, const struct termios *termios_p);

其中,参数说明如下:

fd:是指向打开的串口设备文件描述符的指针,用于标识要进行属性设置的串口设备。

optional_actions:表示对属性的设置动作,可以选择以下几个值:

  • TCSANOW:立即改变属性。
  • TCSADRAIN:等待输出完成后改变属性。
  • TCSAFLUSH:清空输入和输出缓冲区,然后改变属性。

termios_p:是一个指向 struct termios 结构体的指针,包含了要设置的终端属性。struct termios 结构体中包含了串口的配置参数,如波特率、数据位、停止位、校验位、流控制等。

202.2.2 tcsetattr()函数

tcsetattr() 函数是 Linux 系统中用于设置终端设备属性的函数,常用于配置串口设备的参数。该函数的声明如下:

int tcsetattr(int fd, int optional_actions, const struct termios *termios_p);

参数解释:

fd:是指向已打开串口设备的文件描述符的指针,用于指定要设置属性的串口设备。

optional_actions:表示属性设置的动作选项,可以是以下值之一:

  • TCSANOW:立即改变属性。
  • TCSADRAIN:等待输出缓冲区数据发送完毕后再改变属性。
  • TCSAFLUSH:清空输入输出缓冲区后再改变属性。

termios_p:是一个指向 struct termios 结构体的指针,包含了要设置的终端属性,包括波特率、数据位、停止位、校验位、流控制等。

通过调用 tcsetattr() 函数,可以实现对串口设备参数的灵活配置。常见的终端属性设置包括:

  • 设置波特率:通过在 termios_p 结构体中设置正确的输入和输出波特率来指定串口通信的速率。
  • 设置数据位、停止位和校验位:通过在 termios_p 结构体中设置正确的数据位、停止位和校验位来匹配串口设备的配置。
  • 配置流控制:通过设置硬件流控制和软件流控制来控制串口数据的流动。

在调用 tcsetattr() 函数时,需要注意指定合适的 optional_actions 参数,以确保属性设置的动作符合需求。正确地配置串口设备的属性可以保障数据通信的成功和稳定。

202.2.4 cfgetispeed()和cfgetospeed()函数

在 Linux 系统中,没有标准的 cfgetispeed() 和 cfgetospeed() 函数。这两个函数通常是在类 Unix 系统中的 <termios.h> 头文件中定义。它们的作用是用于获取终端设备的输入和输出波特率。这两个函数的功能介绍如下:

cfgetispeed(const struct termios *termios_p)

该函数用于获取终端设备的输入波特率。

参数 termios_p 是一个指向 struct termios 结构体的指针,包含了终端设备的属性信息。

返回值为表示输入波特率的整数值(波特率常用的单位是波特/秒)。

cfgetospeed(const struct termios *termios_p)

该函数用于获取终端设备的输出波特率。

参数 termios_p 是一个指向 struct termios 结构体的指针,包含了终端设备的属性信息。

返回值为表示输出波特率的整数值。

这两个函数用于从终端属性结构体中提取输入和输出波特率的值,以便应用程序可以了解当前终端设备的波特率设置。在使用这两个函数时,需要注意传入正确的 struct termios 结构体指针,以确保获取准确的波特率信息。

在 Linux 系统中,获取终端设备的波特率信息通常通过其他方式实现,如结合使用 tcgetattr() 函数获取终端属性结构体,并从中提取波特率信息。

202.2.3 cfsetispeed()和cfsetospeed()函数

cfsetispeed() 和 cfsetospeed():这两个函数分别用于设置输入波特率和输出波特率。可以通过这两个函数来指定串口设备的通信速率。

cfsetispeed()函数的作用如下:

cfsetispeed(struct termios *termios_p, speed_t speed):

该函数用于设置终端设备的输入波特率。

参数 termios_p 是指向 struct termios 结构体的指针,包含了终端设备的属性信息。

参数 speed 是指定的输入波特率值。

通过调用 cfsetispeed() 函数可以将输入波特率设置为指定的值,以指定串口设备接收数据的速率。

cfsetospeed()函数的作用如下:

cfsetospeed(struct termios *termios_p, speed_t speed):

该函数用于设置终端设备的输出波特率。

参数 termios_p 是指向 struct termios 结构体的指针,包含了终端设备的属性信息。

参数 speed 是指定的输出波特率值。

通过调用 cfsetospeed() 函数可以将输出波特率设置为指定的值,以指定串口设备发送数据的速率。

这两个函数通常与 struct termios 结构体一起使用,通过设置输入和输出波特率来配置串口设备的通信速率。正确地设置输入和输出波特率可以确保串口通信双方的数据传输速率匹配,从而实现稳定的数据交换。

202.2.4 tcflush()和tcflow()函数

tcflush() 和 tcflow() 函数是用于控制终端设备输入输出数据流的函数,常用于串口设备的操作。以下是详细介绍:

tcflush(int fd, int queue_selector)

该函数用于刷新终端设备的输入输出缓冲区。

参数 fd 是指向已打开的终端设备文件描述符的指针,用于标识要操作的终端设备。

参数 queue_selector 是刷新缓冲区的方式,可以是以下值之一:

  1. TCIFLUSH:刷新输入缓冲区,丢弃未读取的数据。
  2. TCOFLUSH:刷新输出缓冲区,丢弃未发送的数据。
  3. TCIOFLUSH:同时刷新输入输出缓冲区。

通过 tcflush() 函数可以根据需求清空终端设备的输入缓冲区、输出缓冲区或同时清空两者。

tcflow(int fd, int action)

该函数用于控制数据流的暂停和恢复。

参数 fd 是指向已打开的终端设备文件描述符的指针,用于标识要操作的终端设备。

参数 action 可以指定以下值之一:

  • TCOOFF:暂停数据的传输,停止发送数据。
  • TCOON:恢复数据的传输,继续发送数据。
  • TCIOFF:暂停数据的传输,停止接收数据。
  • TCION:恢复数据的传输,继续接收数据。

tcflush() 和 tcflow() 函数通常在串口通信中用于控制和管理数据的流动。tcflush() 可以清空缓冲区中的数据,而 tcflow() 可以控制数据的流动状态,暂停或恢复数据的传输。正确使用这两个函数有助于确保串口通信操作的稳定性和准确性。

202.3 串口操作流程

202.3.1设置串口的波特率 

在编写串口应用程序时,设置串口波特率是必须的步骤之一,它决定了数据传输的速率。在 Linux 中,设置波特率通常使用 cfsetspeed() 函数,cfsetspeed()函数所需头文件和函数原型如下所示:

所需头文件

函数原型

#include <termios.h>

#include <unistd.h>

int cfsetspeed(struct termios *termios_p, speed_t speed);

它接受一个 struct termios 结构体作为输入参数,同时返回一个整数值,表示操作是否成功。该函数实际上是对 c_cflag 字段中的波特率进行设置。例如,如果要将波特率设置为 115200,可以调用以下代码: 

struct termios options;
// 获取当前终端配置
tcgetattr(fd, &options);

// 设置波特率为 115200
cfsetspeed(&options, B115200);

// 将新的终端配置写入终端
tcsetattr(fd, TCSANOW, &options);

需要注意以下几点:

(1)需要先打开串口设备文件并且获取串口的属性,包括波特率在内的其他属性。可以使用 tcgetattr() 函数获取属性值,并存储在一个 termios 结构体变量中。

(2)设置波特率时需要调用 cfsetspeed() 函数,并传入一个波特率常量作为参数。常用的波特率常量包括 B9600、B115200 等。这些常量可以在头文件 termios.h 中找到。

(3)设置完成后,需要使用 tcsetattr() 函数将属性值写回到串口设备中。

202.3.2设置数据位大小

在串口通讯中,数据位指的是每个字符(byte)中实际包含的数据位数。通常情况下,一个字符包含8个位(即8个0或1),但有时也可能为7个位或其他值。

在编写串口应用程序时,需要使用 struct termios 结构体中的 c_cflag 成员变量来设置数据位大小。具体来说,需要将 c_cflag 成员变量中与数据位相关的位清零,然后再根据需要设置相应的值。

清零操作通常使用按位与(&)运算符和位运算中的“非”运算符(~),具体步骤如下:

new_cfg.c_cflag &= ~CSIZE; // 将数据位相关的比特位清零

其中,CSIZE 是一个宏定义,表示数据位的位掩码。宏定义通常定义在 termios.h 头文件中,其取值如下:

#define CSIZE   0x00000300  /* 字符长度掩码 */

通过清零操作,将数据位相关的位全部置为0。接下来,可以使用按位或(|)运算符和宏定义来设置具体的数据位数,例如:

new_cfg.c_cflag |= CS8;  // 将数据位数设置为8位

此时,CS8 宏定义将会被解释为一个包含8位的位掩码,通过按位或运算,将其设置到 c_cflag 成员变量中,从而完成数据位的设置。

202.3.3设置奇偶校验位 

串口的奇偶校验位配置一共涉及到 struct termios 结构体中的两个成员变量:c_cflag 和 c_iflag。首先对于 c_cflag 成员,需要添加 PARENB 标志以使能串口的奇偶校验功能,只有使能奇偶校验功能之后才会对输出数据产生校验位,从而对输入数据进行校验检查;同时对于 c_iflag 成员来说,还需要添加 INPCK 标志,这样才能对接收到的数据执行奇偶校验,代码如下所示:

奇校验使能:

new_cfg.c_cflag |= (PARODD | PARENB); // 设置为奇校验
new_cfg.c_iflag |= INPCK; // 使能奇偶校验

偶校验使能:

new_cfg.c_cflag &= ~PARODD;  // 设置为偶校验
new_cfg.c_cflag |= PARENB;   // 使能奇偶校验
new_cfg.c_iflag |= INPCK;    // 对输入数据执行奇偶校验

无校验:

new_cfg.c_cflag &= ~PARENB;  // 禁用奇偶校验
new_cfg.c_iflag &= ~INPCK;   // 不执行奇偶校验

202.3.4设置停止位

在串口通信中,停止位用于指定每个数据帧的结束位置。在传输一个完整的数据字节后,通常需要一个或多个停止位,以便接收端能够确定一个数据帧的结束。停止位的数量通常为1位或2位,其中1位停止位被广泛使用,而2位停止位则较少使用。

在Linux中,通过在 struct termios 结构体中设置 c_cflag 成员变量的 CSTOPB 标志位来控制停止位的数量。当 CSTOPB 为0时,仅使用1位停止位;当 CSTOPB 为1时,则使用2位停止位。

例如,以下代码将串口的停止位设置为1位:

new_cfg.c_cflag &= ~CSTOPB; // 设置停止位为1位

202.4 串口应用编程

在前面了解了串口编程需要的函数知识,接下来让我们动手实践,编写串口应用程序编写好的测试程序存放位置为:iTOP-RK3568开发板【底板V1.7版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux驱动配套资料\04_Linux驱动例程\123_uart_app

#include <stdio.h>
#include <termios.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>

/* 设置串口参数的函数 */
int set_uart(int fd, int speed, int bits, char check, int stop) {
    struct termios newtio, oldtio;

    // 步骤一:保存原来的串口配置
    if(tcgetattr(fd, &oldtio) != 0) {
        printf("tcgetattr oldtio error\n");
        return -1;
    }

    bzero(&newtio, sizeof(newtio));
    
    // 步骤二:设置控制模式标志
    newtio.c_cflag |= CLOCAL | CREAD;
    newtio.c_cflag &= ~CSIZE;

    // 步骤三:设置数据位
    switch(bits) {
        case 7:
            newtio.c_cflag |= CS7;
            break;
        case 8:
            newtio.c_cflag |= CS8;
            break;
    }
    
    // 步骤四:设置奇偶校验位
    switch(check) {
        case 'O': // 偶校验位
            newtio.c_cflag |= PARENB;
            newtio.c_cflag |= PARODD;
            newtio.c_iflag |= (INPCK | ISTRIP);
            break;
        case 'E': // 奇校验位
            newtio.c_cflag |= PARENB;
            newtio.c_cflag &= ~PARODD;
            newtio.c_iflag |= (INPCK | ISTRIP);
            break;
        case 'N': // 无校验
            newtio.c_cflag &= ~PARENB;
            break;
    }
    
    // 步骤五:设置波特率
    switch(speed) {
        case 9600:
            cfsetispeed(&newtio, B9600);
            cfsetospeed(&newtio, B9600);
            break;
        case 115200:
            cfsetispeed(&newtio, B115200);
            cfsetospeed(&newtio, B115200);
            break;
    }
    
    // 步骤六:设置停止位
    switch(stop) {
        case 1:
            newtio.c_cflag &= ~CSTOPB; // 1位停止位
            break;
        case 2:
            newtio.c_cflag |= CSTOPB; // 2位停止位
            break;
    }
    
    // 步骤七:刷新输入队列
    tcflush(fd, TCIFLUSH);
    
    // 步骤八:设置配置立刻生效
    if (tcsetattr(fd, TCSANOW, &newtio) != 0) {
        printf("tcsetattr newtio error\n");
        return -2;
    }
    
    return 0;
}

int main(int argc, char *argv[]) {
    int fd;
    char buf[128];
    int count;
    
    // 步骤九:打开串口设备
    fd = open("/dev/ttyS9", O_RDWR | O_NOCTTY | O_NDELAY);
    
    if (fd < 0) {
        printf("open error \n");
        return -1;
    }

    // 设置串口参数
    set_uart(fd, 115200, 8, 'N', 1);
    
    // 写入数据
    write(fd, argv[1], strlen(argv[1]));
    sleep(1);
    
    // 读取数据
    count = read(fd, buf, sizeof(buf));
    buf[count] = '\0';
    
    // 输出读取的数据
    printf("read message is %s\n", buf);
    
    // 关闭串口设备
    close(fd);
    
    return 0;
}

202.5 运行测试

202.5.1编译应用程序

首先进行应用程序的编译,因为测试APP是要在开发板上运行的,所以需要aarch64-linux-gnu-gcc来编译,输入以下命令,编译完成以后会生成一个app的可执行程序,如下图所示:

 aarch64-linux-gnu-gcc app.c -o app

 

然后将编译完成的可执行程序app拷贝到开发板上。

202.5.2 运行测试

202.5.2.1 串口硬件连接

本实验使用的串口是串口9,串口9在开发板底板的背面,使用杜邦线将下图中的UART9_TX_M1和UART9_RX_M1连接起来。

 

202.5.2.2 运行测试

将编译完成的可执行程序app拷贝到开发板上,如下所示:

然后输入以下命令运行应用程序

./app 123456abc

 

如上图所示,在运行这段程序时,如果一切正常,程序将尝试打开串口设备 /dev/ttyS9,配置串口参数(波特率、数据位、校验位、停止位),然后尝试从串口设备中读取数据并将其输出。

至此,串口应用编程实验完成!

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值