Qt 串口编程-从入门到实战

0. Linux 串口开发

0.1 串行通信基本概念

0.1.1 数据通信的方式
  • 串行通信:计算机与 I/O 设备之间数据传输的各位是按顺序依次进行传送,通常数据在一根数据线上或者一对差分线上传输
    • 传输速度慢,但成本低
  • 并行通信:计算机与 I/O 设备之间通过多条传输线交换数据,数据的各位 bit 同时传送
    • 适合距离较短的通信,信号容易受干扰,传输速度快,成本高

在这里插入图片描述

0.1.2 串行通信涉及的常用术语
  • 单工、半双工和全双工
    • 单工(Simplex):仅能进行一个方向的数据传送
    • 半双工(Half Duplex):数据可以在两个方向上进行传送,但是这种传送绝不能同时进行
    • 全双工(Full Duplex):能够在两个方向同时进行数据传送
  • 数据传输率
    • 每秒传输的二进制位数,单位为 bps(bit per second) 也称波特率
  • 异步方式与同步方式
    • 同步方式:在发送数据信号的时候,会同时送出一根同步时钟信号,用来同步发送方和接收方的数据采样频率
    • 异步方式:数据发送方和数据接收方没有同步时钟,只有数据信号线,只不过发送端和接收端会按照固定频率来进行数据采样

在这里插入图片描述

0.1.3 数据通信传输模式
  • 轮询模式

    • 通过程序执行流,不停的检测状态寄存器的结果,如果当前可发送或接收,则发送或接收数据
      在这里插入图片描述
  • 中断模式

    • 当数据到达或数据可发送时产生中断,通知 CPU 去发送或接收数据
  • DMA 模式(效率大大提高)

    • 在 CPU 不干涉的情况下,DMA 硬件自动实现数据的转移和复制

0.2 UART 串口

  • UART(Universal Asynchronous Receiver and Transmitter) 通用异步接收器和发送器
0.2.1 为什么使用串口?
  • 大多数嵌入式设备没有显示屏,无法获取当前设备运行状况,通过串口和上位机之间打印输出嵌入式设备状态信息
  • 还可用于嵌入式设备的跟踪与调试
0.2.2 通信协议

在这里插入图片描述

  • 起始位
    • 先发出一个逻辑 ”0” 的信号,表示传输字符的开始
  • 数据位
    • 紧接着起始位之后。数据位的个数可以是 4、5、6、7、8 等,构成一个字符。通常采用 ASCII 码
  • 奇偶校验位
    • 数据位加上这一位后,使得 “1” 的位数应为偶数(偶校验)或奇数(奇校验),以此来校验资料传送的正确性
  • 停止位
    • 它是一个字符数据的结束标志。可以是 1 位、1.5 位、2 位的高电平
  • 空闲位
    • 处于逻辑 “1” 状态,表示当前线路上没有资料传送
  • 波特率
    • 数据传输的速率。有以下几个档位:300、600、1200、2400、4800、9600.19200、38400、43000、56000、57600、115200
0.2.3 硬件结构

在这里插入图片描述

0.2.4 Linux 串口
  • linux 中串口设备在 /dev 目录下,如 “/dev/ttyS0”、“/dev/ttyS1”
  • 在使用串口之前必须设置相关配置,包括:波特率、数据位、校验位、停止位等
  • 串口设置由下面结构体实现
    • c_cflag 最为重要,可设置波特率、数据位、校验位、停止位,在设置波特率时需在数字前加上 “B” 如:B9600、B19200
    • 输入模式 c_iflag 成员控制端口接收端的字符输入处理
    struct termios {
        tcflag_t c_iflag;
        tcflag_t c_oflag;
        tcflag_t c_cflag;
        tcflag_t c_lflag;
        cc_t c_cc[NCCS];
    };
    
  • 串口控制函数
    在这里插入图片描述

Linux 下串口调试示例

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <errno.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <termios.h>
#include <stdlib.h>

int set_opt(int fd, int nSpeed, int nBits, char nEvent, int nStop) {
    struct termios newtio, oldtio;
    if (tcgetattr(fd, &oldtio) != 0) {
        perror("SetupSerial 1");
        return -1;
    }
    bzero(&newtio, sizeof(newtio));
    newtio.c_cflag |= CLOCAL | CREAD;
    newtio.c_cflag &= ~CSIZE;

    switch (nBits) {
    case 7:
        newtio.c_cflag |= CS7;
        break;
    case 8:
        newtio.c_cflag |= CS8;
        break;
    }

    switch (nEvent) {
    case '0':
        newtio.c_cflag |= PARENB;
        newtio.c_cflag |= PARODD;
        newtio.c_iflag |= (INPCK | ISTRIP);
        break;
    case 'E':
        newtio.c_iflag |= (INPCK | ISTRIP);
        newtio.c_cflag |= PARENB;
        newtio.c_cflag &= ~PARODD;
        break;
    case 'N':
        newtio.c_cflag &= ~PARENB;
        break;
    }

    switch (nSpeed) {
    case 2400:
        cfsetispeed(&newtio, B2400);
        cfsetospeed(&newtio, B2400);
        break;
    case 4800:
        cfsetispeed(&newtio, B4800);
        cfsetospeed(&newtio, B4800);
        break;
    case 9600:
        cfsetispeed(&newtio, B9600);
        cfsetospeed(&newtio, B9600);
        break;
    case 115200:
        cfsetispeed(&newtio, B115200);
        cfsetospeed(&newtio, B115200);
        break;
    default:
        cfsetispeed(&newtio, B115200);
        cfsetospeed(&newtio, B115200);
        break;
    }

    if (nStop == 1) {
        newtio.c_cflag &= ~CSTOPB;
    } else if (nStop == 2) {
        newtio.c_cflag |= CSTOPB;
    }

    newtio.c_cc[VTIME] = 0;
    newtio.c_cc[VMIN] = 0;
    tcflush(fd, TCIFLUSH);

    // 设置新的终端属性
    if ((tcsetattr(fd, TCSANOW, &newtio)) != 0) {
        perror("Error setting terminal attributes");
    
        // 回滚到先前的终端属性
        if (tcsetattr(fd, TCSANOW, &oldtio) == -1) {
            perror("Error restoring terminal attributes");
        }

        exit(EXIT_FAILURE);
    }
    printf("set done!\n");
    return 0;
}

int open_port(int fd, int comport) {
    char *dev[] = {"dev/ttyS0", "dev/ttyS1", "dev/ttyS2"};
    long int vdisable;

    if (comport == 1) {
        fd = open("dev/ttyS0", O_RDWR | O_NOCTTY | O_NDELAY);
        if (-1 == fd) {
            perror("Can't open serial port!");
            return -1;
        } else {
            printf("Open ttyS0...\n");
        }
    }
    if (comport == 2) {
        fd = open("dev/ttyS1", O_RDWR | O_NOCTTY | O_NDELAY);
        if (-1 == fd) {
            perror("Can't open serial port!");
            return -1;
        } else {
            printf("Open ttyS1...\n");
        }
    }
    if (comport == 3) {
        fd = open("dev/ttyS2", O_RDWR | O_NOCTTY | O_NDELAY);
        if (-1 == fd) {
            perror("Can't open serial port!");
            return -1;
        } else {
            printf("Open ttyS2...\n");
        }
    }
    if (fcntl(fd, F_SETFL, 0) < 0) {
        printf("fcntl failed!\n");
    } else {
        printf("fcntl = %d\n", fcntl(fd, F_SETFL, 0));
    }
    if (isatty(STDIN_FILENO) == 0) {
        printf("standard input is not a terminal device\n");
    } else {
        printf("isaty success!\n");
    }
    printf("fd - open = %d\n", fd);

    return fd;
}

int main(int argc, char *argv[]) {
    int fd;
    int nread, i;
    char buff[] = "Hello World!\n";

    if ((fd = open_port(fd, 2)) < 0) {
        perror("open_port error");
        return -1;
    }
    if ((i = set_opt(fd, 115200, 8, 'N', 1)) < 0) {
        perror("set_opt error");
        return -1;
    }

    printf("fd = %d\n", fd);
    nread = write(fd, buff, sizeof(buff));
    printf("write = %d, %s\n", nread, buff);
    sleep(5);
    close(fd);

    return 0;
}

1. Qt 串口通信流程解析

1.1 串行通信和并行通信对比

  • 并行通信适合距离较短的通信,且信号容易受干扰,成本高
  • 串口通讯-设备(蓝牙, wifi, gprs, gps)

在这里插入图片描述

1.2 Qt 串口通信具体流程

  • 1. 创建 QSerialPort 对象
  • 2. 配置属性(波特率, 数据位, 停止, 校验位)
  • 3. 打开设备
  • 4. 发送数据到串口 write
  • 5. 在槽函数中读取数据(当串口有数据可读的时候会发送 readyRead 信号)
1.2.1 serialapp.pro
QT       += core gui serialport
1.2.2 serialapp.h
#ifndef SERIALAPP_H
#define SERIALAPP_H

#include <QWidget>
#include <QSerialPort>
#include <QSerialPortInfo>
#include <QIODevice>
#include <QByteArray>
#include <QString>
#include <QDebug>

QT_BEGIN_NAMESPACE
namespace Ui { class SerialApp; }
QT_END_NAMESPACE

class SerialApp : public QWidget {
    Q_OBJECT

public:
    SerialApp(QWidget *parent = nullptr);
    ~SerialApp();

private slots:
    void on_openBt_clicked();
    void on_sendBt_clicked();
    void read_data();

private:
    Ui::SerialApp *ui;
    // 1、创建 QSerialPort 对象
    QSerialPort mSerial;
};
#endif // SERIALAPP_H
1.2.3 serialapp.cpp
#include "serialapp.h"
#include "ui_serialapp.h"

SerialApp::SerialApp(QWidget *parent) : QWidget(parent), ui(new Ui::SerialApp) {
    ui->setupUi(this);

    // 2、配置设备,波特率,数据位,停止位,校验位
    mSerial.setPortName("COM1");  // 选择对应的端口号
    mSerial.setBaudRate(QSerialPort::Baud115200);
    mSerial.setDataBits(QSerialPort::Data8);
    mSerial.setStopBits(QSerialPort::OneStop);
    mSerial.setParity(QSerialPort::NoParity);

    connect(&mSerial, &QSerialPort::readyRead, this, &SerialApp::read_data);
}

SerialApp::~SerialApp() {
    delete ui;
}

// 3、打开设备 (读写)
void SerialApp::on_openBt_clicked() {
    if (mSerial.open(QIODevice::ReadWrite)) {
        qDebug() << "open success!";
    } else {
        qDebug() << "open failed!";
    }
}

// 4、发送数据到串口 write
void SerialApp::on_sendBt_clicked() {
    QString data = ui->textEdit->toPlainText();
    mSerial.write(data.toUtf8());
}

// 5、读取串口数据 read
void SerialApp::read_data() {
    QByteArray array = mSerial.readAll();
    ui->textBrowser->append(QString(array));
}
1.2.4 serialapp.ui

在这里插入图片描述

2. Qt 虚拟串口调试

2.1 VSPD 创建虚拟串口

  • VSPD (Virtual Serial Port Driver) 是一个虚拟串口驱动程序

    • 它可以模拟多个串口设备,使得应用程序可以通过虚拟串口与物理串口设备进行通信
    • 使用 VSPD 可以方便地进行串口调试、数据采集、数据转发等操作
    • VSPD 还支持多种协议,例如模拟 GPS 设备、模拟调制解调器、与虚拟机通信等
  • VSPD虚拟串口软件安装及使用

在这里插入图片描述

2.2 SecureCRT 连接虚拟串口

  • SecureCRT 是一款安全的终端模拟器,常用于远程访问服务器和网络设备
    • 它可以让用户通过 SSH、Telnet、Rlogin 或者串口等协议连接到远程设备,并在本地进行命令行操作
    • SecureCRT 还提供了多重会话管理、脚本编写、自动登录、加密通信等多种功能
  • SecureCRT安装教程

在这里插入图片描述

2.3 Qt 虚拟串口实现

  • serialapp.h

    #ifndef SERIALAPP_H
    #define SERIALAPP_H
    
    #include <QWidget>
    #include <QSerialPort>
    #include <QSerialPortInfo>
    #include <QIODevice>
    #include <QByteArray>
    #include <QString>
    #include <QDebug>
    #include <QList>
    
    QT_BEGIN_NAMESPACE
    namespace Ui { class SerialApp; }
    QT_END_NAMESPACE
    
    class SerialApp : public QWidget {
        Q_OBJECT
    
    public:
        SerialApp(QWidget *parent = nullptr);
        ~SerialApp();
    
    private slots:
        void on_openBt_clicked();
        void on_sendBt_clicked();
        void read_data();
    
    private:
        Ui::SerialApp *ui;
        // 创建 QSerialPort 对象
        QSerialPort mSerial;
    };
    #endif // SERIALAPP_H
    
  • serialapp.cpp

    #include "serialapp.h"
    #include "ui_serialapp.h"
    
    SerialApp::SerialApp(QWidget *parent) : QWidget(parent), ui(new Ui::SerialApp) {
        ui->setupUi(this);
    
        // 获取当前设备上的所有串口
        QList<QSerialPortInfo> list =  QSerialPortInfo::availablePorts();
        for (int i = 0; i < list.size(); i++) {
            ui->comboBox->addItem(list.at(i).portName());
        }
    
        // 配置设备,波特率,数据位,停止位,校验位
        //mSerial.setPortName("COM1");
        mSerial.setBaudRate(QSerialPort::Baud115200);
        mSerial.setDataBits(QSerialPort::Data8);
        mSerial.setStopBits(QSerialPort::OneStop);
        mSerial.setParity(QSerialPort::NoParity);
    
        connect(&mSerial, &QSerialPort::readyRead, this, &SerialApp::read_data);
    }
    
    SerialApp::~SerialApp() {
        delete ui;
    }
    
    // 打开设备 (读写)
    void SerialApp::on_openBt_clicked() {
        if (mSerial.isOpen()) {
            mSerial.close();
        }
        mSerial.setPortName(ui->comboBox->currentText());  // 设置端口
        if (mSerial.open(QIODevice::ReadWrite)) {
            qDebug() << "open success!";
        } else {
            qDebug() << "open failed!";
        }
    }
    
    // 发送数据到串口 write
    void SerialApp::on_sendBt_clicked() {
        QString data = ui->textEdit->toPlainText();
        mSerial.write(data.toUtf8());
    }
    
    // 读取串口数据 read
    void SerialApp::read_data() {
        QByteArray array = mSerial.readAll();
        ui->textBrowser->append(QString(array));
    }
    
  • serialapp.ui

在这里插入图片描述

2.4 Qt 与 SecureCRT 建立虚拟串口连接

在这里插入图片描述

3. Qt 编写串口调试工具

在这里插入图片描述

3.1 serialportapp.h

#ifndef SERIALPORTAPP_H
#define SERIALPORTAPP_H

#include <QWidget>
#include <QSerialPort>
#include <QSerialPortInfo>
#include <QList>
#include <QStringList>
#include <QString>
#include <QIODevice>
#include <QDebug>
#include <QByteArray>
#include <QMessageBox>
#include <QTimerEvent>
#include <QFileDialog>
#include <QFile>

QT_BEGIN_NAMESPACE
namespace Ui { class SerialPortApp; }
QT_END_NAMESPACE

class SerialPortApp : public QWidget {
    Q_OBJECT

public:
    SerialPortApp(QWidget *parent = nullptr);
    ~SerialPortApp();

    void timerEvent(QTimerEvent *event);

private slots:
    void on_openBt_clicked();
    void on_closeBt_clicked();
    void on_sendBt_clicked();
    void on_autoCheckBox_clicked(bool checked);
    void on_clearSendSizeBt_clicked();
    void on_sendHexCb_clicked(bool checked);
    void on_recvHexCb_clicked(bool checked);

    void read_data();

    void on_clearRecvSizeBt_clicked();
    void on_selectfileBt_clicked();
    void on_sendfileBt_clicked();

    void send_file_text(quint64 size);

private:
    Ui::SerialPortApp *ui;
    QSerialPort mSerial;
    int timerid;
    qint32 sendsize;
    qint32 recvsize;
    QFile file;  // 发送文件
    qint32 sendfilesize;
};
#endif // SERIALPORTAPP_H

3.2 serialportapp.cpp

#include "serialportapp.h"
#include "ui_serialportapp.h"

SerialPortApp::SerialPortApp(QWidget *parent) : QWidget(parent), ui(new Ui::SerialPortApp) {
    ui->setupUi(this);

    // 遍历获取当前设备上的所有串口
    QList<QSerialPortInfo> infos = QSerialPortInfo::availablePorts();
    for (int i = 0; i < infos.size(); i++) {
        ui->comCb->addItem(infos.at(i).portName());
    }

    // 设置波特率显示
    QStringList list;
    list << "1200" << "2400" << "4800" << "9600" << "19200" << "38400" << "57600" << "115200";
    ui->rateCb->addItems(list);
    ui->rateCb->setCurrentIndex(7);  // 设置默认波特率为 115200
    list.clear();

    // 设置数据位
    list << "5" << "6" << "7" << "8" << "-1";
    ui->dataCb->addItems(list);
    ui->dataCb->setCurrentIndex(3);  // 设置默认数据位为 8
    list.clear();

    // 设置停止位
    list << "1" << "3" << "2" << "-1";
    ui->stopCb->addItems(list);
    list.clear();

    // 设置校验位
    list << "None" << "NULL" << "Even" << "Odd" << "Space" << "Mark";
    ui->priCb->addItems(list);
    list.clear();

    // 把关闭按钮设置失效
    ui->closeBt->setEnabled(false);

    // 当串口有数据可读时会发送 readyRead 信号
    connect(&mSerial, &QSerialPort::readyRead, this, &SerialPortApp::read_data);

    // 初始化发送、接收的字节数记录
    sendsize = recvsize = 0;
}

SerialPortApp::~SerialPortApp() {
    delete ui;
}

// 打开
void SerialPortApp::on_openBt_clicked() {
    // 配置端口,波特率,数据位,停止位,校验位
    mSerial.setPortName(ui->comCb->currentText());
    mSerial.setBaudRate(ui->rateCb->currentText().toInt());
    mSerial.setDataBits((QSerialPort::DataBits)ui->dataCb->currentText().toInt());
    mSerial.setStopBits((QSerialPort::StopBits)ui->stopCb->currentText().toInt());
    mSerial.setParity((QSerialPort::Parity)ui->priCb->currentText().toInt());

    // 打开设备
    if (mSerial.open(QIODevice::ReadWrite)) {
        ui->closeBt->setEnabled(true);
        ui->openBt->setEnabled(false);
    }
}

// 关闭
void SerialPortApp::on_closeBt_clicked() {
    // 关闭设备
    mSerial.close();
    ui->closeBt->setEnabled(false);
    ui->openBt->setEnabled(true);
}

// 手动发送数据
void SerialPortApp::on_sendBt_clicked() {
    QString data = ui->sendText->toPlainText();
    if (ui->sendHexCb->isChecked()) {
        // 转十六进制:data = 4142 --> 0x41 0x42
        QByteArray array;
        if (data.size() % 2 != 0) {
            data.insert(0, '0');
        }
        for (int i = 0; i < data.size() / 2; i++) {
            QString t = data.mid(2*i, 2);
            bool ok = false;
            int ihex = t.toInt(&ok, 16);
            array.append(ihex);
        }

        int size = mSerial.write(array);  // 发送数据
        sendsize += size;                 // 累计发送的字节数
    } else {
        int size = mSerial.write(data.toUtf8());  // 发送数据
        sendsize += size;
    }
    // 设置显示已发送的字节数
    ui->sendsizelabel->setText(QString::number(sendsize));
}

// 定时自动发送数据
void SerialPortApp::on_autoCheckBox_clicked(bool checked) {
    if (checked) {
        // 获取定时发送周期
        int ms = ui->autotimeEdit->text().toInt();
        if (ms < 100) {
            QMessageBox::warning(this, "time hint", "time should > 100ms");
            ui->autoCheckBox->setChecked(false);

            return;
        }

        // 启动定时器事件
        timerid = this->startTimer(ms);
    } else {
        // 关闭定时器事件
        this->killTimer(timerid);
    }
}

// 定时器事件
void SerialPortApp::timerEvent(QTimerEvent *event) {
    on_sendBt_clicked();
}

// 清空已发送的字节数
void SerialPortApp::on_clearSendSizeBt_clicked() {
    sendsize = 0;
    ui->sendText->clear();
    ui->sendsizelabel->setText("0");
}

// 发送端:十六进制和十进制转换
void SerialPortApp::on_sendHexCb_clicked(bool checked) {
    if (checked) {  // 十进制 --> 十六进制
        QString data = ui->sendText->toPlainText();
        QByteArray array = data.toUtf8().toHex();
        ui->sendText->setText(QString(array));
    } else {  // 十六进制 --> 十进制
        QString data = ui->sendText->toPlainText();
        QByteArray array;
        if (data.size() % 2 != 0) {
            data.insert(0, '0');
        }
        for (int i = 0; i < data.size() / 2; i++) {
            QString t = data.mid(2*i, 2);
            bool ok = false;
            int ihex = t.toInt(&ok, 16);
            array.append(ihex);
        }
        ui->sendText->setText(QString(array));
    }
}

// 接收端:十六进制和十进制转换
void SerialPortApp::on_recvHexCb_clicked(bool checked) {
    if (checked) {  // 十进制 --> 十六进制
        QString data = ui->recvText->toPlainText();
        QByteArray array = data.toUtf8().toHex();
        ui->recvText->setText(QString(array));
    } else {  // 十六进制 --> 十进制
        QString data = ui->recvText->toPlainText();
        QByteArray array;
        if (data.size() % 2 != 0) {
            data.insert(0, '0');
        }
        for (int i = 0; i < data.size() / 2; i++) {
            QString t = data.mid(2*i, 2);
            bool ok = false;
            int ihex = t.toInt(&ok, 16);
            array.append(ihex);
        }
        ui->recvText->setText(QString(array));
    }
}

// 接收串口数据
void SerialPortApp::read_data() {
    // 读到的数据是一个个字节
    QByteArray array = mSerial.readAll();
    recvsize += array.size();  // 显示已接收到的字节数

    if (ui->recvHexCb->isChecked()) {
        ui->recvText->append(array.toHex());
    } else {
        ui->recvText->append(array);
    }

    // 设置显示已接收到的字节数
    ui->recvsizelabel->setText(QString::number(recvsize));
}

// 清空已接收的字节数
void SerialPortApp::on_clearRecvSizeBt_clicked() {
    recvsize = 0;
    ui->recvText->clear();
    ui->recvsizelabel->setText("0");
}

// 选择要发送的文件
void SerialPortApp::on_selectfileBt_clicked() {
    QString path = QFileDialog::getOpenFileName(this);
    ui->filepathEdit->setText(path);
}

// 发送文件
void SerialPortApp::on_sendfileBt_clicked() {
    // 当数据发送完毕后会发出一个信号 &QSerialPort::bytesWritten
    // 每当有效载荷的数据写入到设备当前的写入通道时,就会发出这个信号
    connect(&mSerial, &QSerialPort::bytesWritten, this, &SerialPortApp::send_file_text);

    // 打开文件
    file.setFileName(ui->filepathEdit->text());
    if (!file.open(QIODevice::ReadOnly)) {
        return;
    }

    // 获取文件大小
    int filesize = file.size();
    ui->progressBar->setMaximum(filesize);

    // 设置进度条显示
    QByteArray array = file.read(128);  // 每次读取 128 字节内容
    sendfilesize = mSerial.write(array);
    ui->progressBar->setValue(sendfilesize);
}

// 循环(每 128 字节)发送文件
void SerialPortApp::send_file_text(quint64 size) {
    // 设置进度条显示
    QByteArray array = file.read(128);
    quint64 mSize = mSerial.write(array);
    sendfilesize += mSize;
    ui->progressBar->setValue(sendfilesize);

    // 判断文件是否发送完毕
    if (sendfilesize == ui->progressBar->maximum()) {
        file.close();
        disconnect(&mSerial, &QSerialPort::bytesWritten, this, &SerialPortApp::send_file_text);
    }
}

3.3 serialportapp.ui

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值