1. Qt 串口通讯

Qt串行端口(Qt Serial Port)提供基本功能,包括配置、I/O操作、获取和设置RS-232引脚的控制信号。

本章主要讲解QSerialPort模块,参考Qt例程,写一个非常简陋的串口助手。

鲁班猫板卡的串口资源以及串口使用可以参考下:https://doc.embedfire.com/linux/rk356x/linux_base/zh/latest/linux_app/uart/uart.html

1.1. Qt Serial Port

串口模块中两个类 QSerialPortInfo 和 QSerialPort

  • QSerialPort 提供访问串行端口的功能,继承于QIODevice

  • QSerialPortInfo 提供有关现有串行端口的信息

QSerialPortInfo使用静态函数生成QSerialPortInfo对象的列表,列表中的每个QSerialPortInfo对象代表一个串行端口,可以查询该端口的名称,系统位置,描述和制造商。

QSerialPortInfo::availablePorts()获取的串口信息,可以被QSerialPort用来设置串口参数。 我们可以使用QSerialPort类提供的方法来设置串口的相关参数:

  • setBaudRate() 设置波特率

  • setDataBits() 设置数据位

  • setParity() 设置校验位

  • setStopBits() 设置停止位

  • setFlowControl() 设置流控制

设置串口参数后,可以使用open()方法以只读(r/o)、仅写(w/o)或可读可写(r/w)模式打开它。 成功打开后,串口可以使用read()、readLine()或者readAll()来读取数据,使用 write()来读写数据到串口设备,不使用串口时使用close()关闭串口。

注意:串口始终以独占访问方式打开(其他进程或线程不能访问已打开的串口)。

串口使用过程中,可以通过readyRead()信号, 一旦知道串口已准备好读取或写入,就可以使用read()或write()方法。另外,还可以调用readLine()和readAll()便捷方法。 如果不是一次读取所有数据,则在将新的传入数据附加到QSerialPort的内部读取缓冲区后,其余数据将可供以后使用。 您可以使用setReadBufferSize()限制读取缓冲区的大小。

还有些功能可用于实现阻塞串行端口:

waitForReadyRead()阻止调用,直到可以读取新数据为止。 waitForBytesWritten()阻止调用,直到将一个有效载荷数据写入串行端口为止。

如果waitForReadyRead()返回false,则表示串口连接已关闭或数据传输发生了错误。 如果在任何时间点发生错误,QSerialPort将发出errorOccurred()信号,还可以调用error()来查找上次发生的错误的类型。

更多的串口模块信息可以参考下官方文档:https://doc.qt.io/qt-5/qtserialport-module.html

1.2. 串口例程

测试例程根据Qt官方 Terminal Example 进行了简单修改。 测试例程串口通讯使用异步(非阻塞)方法,发送操作后,立即返回。

1.2.1. 串口例程编写

创建项目例程文件: - 创建SerialPort工程,基于QWidget - 界面设计,设计一个串口通信界面(MainWindow)和一个串口设置对话框(SettingsDialog) - 创建一个Console类,基于QPlainTextEdit,用于显示串口数据和输入发送数据

串口程序编写 : - 使用 QSerialPortInfo::availablePorts() 获取当前设备识别到的串口 - 新建一个串口实例 QSerialPort,设置它的端口号,波特率等参数 - 绑定 QSerialPort 的 readyRead() 信号,自动接受串口数据 - 使用 QSerialPort 的 open() 函数打开串口 - 调用 read() 或 write() 进行数据收发 - 使用 QSerialPort 的 close() 函数关闭串口。

1.2.2. 核心代码讲解

Qt串口模块为一个附加模块,使用串口相关的类时,需要在pro工程文件中添加 QT += serialport

工程中有四个用户类:Console,SettingsDialog,Settings,MainWindow:

  • Console负责控制台相关的业务,例如显示串口数据;

  • SettingsDialog是一个QDialog类,其目的是用于设置串口的参数;

  • Settings是用来保存当前串口的参数的类,SettingsDialog的一个成员类;

  • MainWindow则进行串口数据的读写,UI相关的业务。

程序启动第一步就是初始化MainWindow,MainWindow构造函数中,创建了上面的类和一个串口类(QSerialPort)的实例。

lubancat_qt_tutorial_code/SerialPort/mainwindows.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
QSerialPort *m_serial = nullptr;

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    m_ui(new Ui::MainWindow),
    m_status(new QLabel),
    m_console(new Console),
    m_settings(new SettingsDialog),
    m_serial(new QSerialPort(this))
{
    m_ui->setupUi(this);
    m_console->setEnabled(false);
    setCentralWidget(m_console);

    m_ui->actionConnect->setEnabled(true);
    m_ui->actionDisconnect->setEnabled(false);
    m_ui->actionQuit->setEnabled(true);
    m_ui->actionConfigure->setEnabled(true);

    m_ui->statusBar->addWidget(m_status);

    initActionsConnections();

    //关联信号与槽
    connect(m_serial, &QSerialPort::errorOccurred, this, &MainWindow::handleError);
    //关联串口readyRead信号和读取数据槽函数
    connect(m_serial, SIGNAL(readyRead()), this, SLOT(readData()));
    //关联信号与槽,用于串口发送Console输入的数据
    connect(m_console, &Console::getData, this, &MainWindow::writeData);

    //定时器,用于定时发送测试数据
    timer = new QTimer();
    connect(timer, SIGNAL(timeout()), this, SLOT(handletimeout()));
}

创建SettingsDialog实例的时候,会在构造函数中调用这个fillPortsInfo()函数, 此函数通过QSerialPortInfo::availablePorts()静态函数获取当前可获得的串口设备信息,将相关的端口号、可设置的参数,在SettingsDialog窗口显示出来:

serial000
lubancat_qt_tutorial_code/SerialPort/settingsdialog.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void SettingsDialog::fillPortsInfo()
{
    m_ui->serialPortInfoListBox->clear();
    QString description;
    QString manufacturer;
    QString serialNumber;
    //获取获取设备串口信息
    const auto infos = QSerialPortInfo::availablePorts();
    for (const QSerialPortInfo &info : infos) {
        QStringList list;
        description = info.description();
        manufacturer = info.manufacturer();
        serialNumber = info.serialNumber();
        list << info.portName()
             << (!description.isEmpty() ? description : blankString)
             << (!manufacturer.isEmpty() ? manufacturer : blankString)
             << (!serialNumber.isEmpty() ? serialNumber : blankString)
             << info.systemLocation()
             << (info.vendorIdentifier() ? QString::number(info.vendorIdentifier(), 16) : blankString)
             << (info.productIdentifier() ? QString::number(info.productIdentifier(), 16) : blankString);

        m_ui->serialPortInfoListBox->addItem(list.first(), list);
    }

    m_ui->serialPortInfoListBox->addItem(tr("Custom"));
}

当点击设置串口的设置后,点击窗口的 Apply 时,就会自动调用updateSettings()来更新Settings中的串口信息。

在前面构造函数的initActionsConnections()中,会关联很多Action和操作,其中actionConnect操作时,会调用MainWindow::openSerialPort()函数:

lubancat_qt_tutorial_code/SerialPort/mainwindows.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void MainWindow::openSerialPort()
{
    //从SettingsDialog获取到Settings
    const SettingsDialog::Settings p = m_settings->settings();
    //根据Settings中的串口参数,配置串口,并读写方式打开串口
    m_serial->setPortName(p.name);                 //串口名
    m_serial->setBaudRate(p.baudRate);             //波特率
    m_serial->setDataBits(p.dataBits);             //数据位
    m_serial->setParity(p.parity);                 //校验位
    m_serial->setStopBits(p.stopBits);             //停止位
    m_serial->setFlowControl(p.flowControl);       //流控
    if (m_serial->open(QIODevice::ReadWrite)) {
        m_console->setEnabled(true);
        m_console->setLocalEchoEnabled(p.localEchoEnabled); //回显
        m_ui->actionConnect->setEnabled(false);
        m_ui->actionDisconnect->setEnabled(true);
        m_ui->actionConfigure->setEnabled(false);
        showStatusMessage(tr("Connected to %1 : %2, %3, %4, %5, %6")
                        .arg(p.name).arg(p.stringBaudRate).arg(p.stringDataBits)
                        .arg(p.stringParity).arg(p.stringStopBits).arg(p.stringFlowControl));

        //timer->start(1000);
    } else {
        QMessageBox::critical(this, tr("Error"), m_serial->errorString());

        showStatusMessage(tr("Open error"));
    }

}

在mainwindows构造函数中,之后会绑定串口的errorOccurred()和readyRead()信号, 当串口接收到数据的时候,就会触发readyRead()信号,随即调用readData()槽函数,使用readAll()去读取串口数据,并显示到控制台。 串口发生故障的时候,会触发errorOccurred(),程序通过handleError()显示错误,并关闭串口。

lubancat_qt_tutorial_code/SerialPort/mainwindows.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
connect(m_serial, &QSerialPort::errorOccurred, this, &MainWindow::handleError);
connect(m_serial, &QSerialPort::readyRead, this, &MainWindow::readData);
//connect(m_serial, SIGNAL(readyRead()), this, SLOT(readData()));

void MainWindow::readData()
{
    const QByteArray data = m_serial->readAll();
    m_console->putData(data);
}

void MainWindow::handleError(QSerialPort::SerialPortError error)
{
    if (error == QSerialPort::ResourceError) {
        QMessageBox::critical(this, tr("Critical Error"), m_serial->errorString());
        closeSerialPort();
    }
}

串口接收到数据时,就会自动触发readyRead()信号,控制台就会显示串口接受到的数据了。 关闭UI时,会调用MainWindow::closeSerialPort()关闭串口:

lubancat_qt_tutorial_code/SerialPort/mainwindows.cpp
1
2
3
4
5
6
7
void MainWindow::closeSerialPort()
{
    if (m_serial->isOpen())
        m_serial->close();

    showStatusMessage(tr("Disconnected"));
}

1.2.3. 编译构建

build001
  • Ubuntu20.04主机上 选择 Desktop Qt 5.15.2 GCC 64bit 套件

  • LubanCat板卡上 选择 LubanCat-RK

注意 当两种构建套件进行切换时,请重新构建项目或清除之前项目。

build002

1.2.4. 运行测试

编译构建后,复制可自行程序到LubanCat板卡上运行,需要将程序拷贝到LubanCat开发板中,可通过NFS或SCP命令, NFS环境搭建参考:https://doc.embedfire.com/linux/imx6/linux_base/zh/latest/linux_app/mount_nfs.html

SCP命令如下:

# 复制可执行程序到板卡(这里测试的是lubancat2板卡)
scp SerialPort cat@192.168.103.110:/home/cat/qt

教程测试使用lubancat2板卡,使用USB转串口线将lubancat2的串口3连接到PC。 同时开启下板卡的串口3,串口的开启参考下 《串口通讯教程》

在LubanCat上运行串口例程:

# 在debian10 带桌面的系统中运行,测试使用xcb
# 注意程序打开设备文件,需要root权限
sudo  ./SerialPort -platform xcb

先点击界面的 tools-->config 可以对串口进行设置,点击串口的下拉框选择我们开启了的LubanCat2串口3(ttyS3):

serial003

串口3的标识为ttyS3,我们设置波特率设置为9600。 然后点击连接,打开串口即可进行数据收发。

测试板卡数据发送,点击到Console上,输入需要发送的数据,然后回车,就会发送数据, 或者在窗口的工具栏有一个定时发送的测试功能,点击 Test -> start timer,运行在pc上的串口助手(需要设置一致的串口参数)就会定时收到 https://firebbs.cn:

serial003

测试板卡数据接收测试,在PC串口助手上发送消息,在板卡上的串口工具就会显示:

serial003