本文作者:小嗷
微信公众号:aoxiaoji
吹比QQ群:736854977
本文你会找到以下问题的答案:
- 单片机的串口通信
- 从零开始开发QT软件思路
- 波特率、流控制、奇偶校验位、
- C++11中引入的auto
今天,有个做测试的小伙伴,问我上位机什么写?其实,我一开始想没有,快滚。不过,还是用当年的javaSwing写的串口软件,甩他脸算。算了,看看能不能QT利用搞一个串口软件给他,不能就叫他上网自己查。
业务需求:
- 使用单片机检测硬件
- 再通过单片机其他IO口连接上COM口(串口)
- 上传数据给PC端处理结果
- 最后,上传到服务器
2.1 我需要怎么做(产品经理的一般套路,看看别人产品):
如下图:
2.2 串口查一查英文单词是什么?
哦!seial port(其实,小嗷早就知道,英文是什么。只是为了配合那个测试小白)
2.3 打开QT软件 -> 欢迎 -> 示例 -> 搜serial,如下
鼠标移到其中一个小窗框上
英文意思:展示如何使用标准QSerialPort的API,在一个非图形用户界面思路上
从小窗口可以看出没有现成的,都是边边角角。
2.4 先不急打开看看示例,转移目标查QSerialPort的API
网址如下:
http://doc.qt.io/qt-5/qserialport.html#BaudRate-enum
打开图如下:
大概意思就是提供函数进入串口,如上图得出:
cpp需要导入:#include <QSerialPort>(Header)
pro需要导入:QT += serialport(qmake)
2.5 开始创建项目名QSerialPortTool,一路Next
创建成功后,在pro添加QT += serialport(qmake)
2.6 继续看网址的API介绍,发现如下
公共类型:
- BaudRate:波特率(点击BaudRate)
波特率是什么?介绍完API下方有介绍。
- DataBits:数据位(这个枚举描述使用的数据位的数量)
- Direction:用法(这个枚举描述了数据传输的可能方向。)
小嗷简单说说吧:只能允许输入/只能允许输出/同时允许输入输出(有点意思,小嗷不懂什么是同时输入输出。)
- FlowControl:这个枚举描述了所使用的流控制。
大概分为软硬件流控制,-1过时不建议使用。
同理,下方有流控制的解释
- Parity:奇偶校正(这个枚举描述了使用的奇偶校验方案。)
同理下方。
- PinoutSignal:针输出信号?(这个枚举描述了可能的RS-232 pinout信号。)
这参数不太清楚是什么,RS-232指的是:我们台式电脑的9Pin(9针的插头,电子专业俗称COM口),大概是定义COM口的那个针输出信号(小嗷猜的)
- SerialPortError:串口错误信息(这个枚举描述了串口::error属性所包含的错误。)
- StopBits:停止位(这个枚举描述了所使用的停止位的数量。)
用于表示单个包的最后一位。典型的值为1,1.5和2位。由于数据是在传输线上定时的,并且每一个设备有其自己的时钟,很可能在通信中两台设备间出现了小小的不同步。因此停止位不仅仅是表示传输的结束,并且提供计算机校正时钟同步的机会。适用于停止位的位数越多,不同时钟同步的容忍程度越大,但是数据传输率同时也越慢。
- BaudRate:波特率(点击BaudRate)
- DataBits:数据位(这个枚举描述使用的数据位的数量)
- Direction:用法(这个枚举描述了数据传输的可能方向。)
- FlowControl:这个枚举描述了所使用的流控制。
- Parity:奇偶校正(这个枚举描述了使用的奇偶校验方案。)
- PinoutSignal:针输出信号?(这个枚举描述了可能的RS-232 pinout信号。)
- SerialPortError:串口错误信息(这个枚举描述了串口::error属性所包含的错误。)
- StopBits:停止位(这个枚举描述了所使用的停止位的数量。)
再看看我们对标的产品图:
这时,小嗷大概了解的自己要做什么。即:对标产品的图,除了设置接收的编码方式,基本在QSerialPort类中,可以直接调用API函数(如:波特率等)进行相关设置。
在往下就是功能函数:其中,黄色部分有设置COM的序号什么,估计搞软件的时候需要用它来设置COM序号(如:COM 1-256)。
再往下就是重载函数(重载是什么?)
重载,简单说,就是函数或者方法有相同的名称,但是参数列表不相同的情形,这样的同名不同参数的函数或者方法之间,互相称之为重载函数或者方法。
好了,该补得都补了,下面第4点就来看看如何实现从零开始实现串口代码部分。
3.1 波特率
单片机或计算机在串口通信时的速率。指的是信号被调制以后在单位时间内的变化,即单位时间内载波参数变化的次数,如:
每秒钟传送240个字符,而每个字符格式包含10位(1个起始位,1个停止位,8个数据位),这时的波特率为240Bd,
比特率为10位*240个/秒=2400bps
1Bps=8bps
1Mbps=128KBps
下载速度最高为128KBps
每秒钟传送240个二进制位,这时的波特率为240Bd,比特率也是240bps。
3.2 流控制
用途:数据在传输过程中容易出现数据丢失的现象。
例如:两台计算机通过串口传输数据时,或者台式机与单片机之间进行通信时,可能由于两端计算机的处理速度不同,出现接收端的数据缓冲区已满,而发送端依然继续发送数据,则导致数据丢失。
流控制的出现就是为了解决这种数据丢失的问题。
工作原理:当接收端的数据缓冲区已满,无法处理数据来时,就发出”不再接收”的信号,发送端则停止发送,直到发送端收到”可以继续发送”的信号再发送数据。
计算机中常用的两种流控制分别是硬件流控制(RTS/CTS、DTR/DSR等)和软件流控制(XON/XOFF)。
硬件流控制 :硬件流控制必须将相应的电缆线连上。
硬件流控制常用方式为:RTS/CTS(请求发送/清除发送)流控制和DTR/DSR(数据终端就绪/数据设置就绪)流控制。
当用RTS/CTS流控制时,需将通讯两端的RTS、CTS线对应相连,数据终端设备(如计算机)使用RTS来启动调制解调器或其它数据通讯设备的数据流,而数据通讯设备(如调制解调器)则用CTS来启动和暂停来自计算机的数据流。
这种硬件握手方式的过程为:通过程序为接收端缓冲区大小设置一个高位标志(可为缓冲区大小的75%)和一个低位标志(可为缓冲区大小的25%),当缓冲区内数据量达到高位时,接收端将CTS线置低电平(送逻辑0),当发送端的程序检测到CTS为低后,就停止发送数据,直到接收端缓冲区的数据量低于低位而将CTS置高电平。RTS则用来标明接收设备有没有准备好接收数据。
DTR/DSR流控制的工作方式与RTS/CTS流控制类似,不再进行赘述。
简单讲讲就是
RTS:标明家属有没有准备好钱;
CTS:标明绑匪接不接受钱;
有点意思,和服务器的排他锁思想类似(学过服务器就知道)
3.3 奇偶校验位(Parity)
奇偶校验位(Parity),在数据存储和传输中,字节中额外增加一个比特位,用来检验错误。它常常是从两个或更多的原始数据中产生一个冗余数据,冗余数据可以从一个原始数据中进行重建。不过,奇偶校验数据并不是对原始数据的完全复制。被用在RAID的2、3、4、5级别中。
使用
由于它很简单,所以奇偶校验位用于许多计算机硬件中遇到麻烦时能够重新操作或者通过简单的错误检测就能起到很大作用的场合。例如SCSI总线使用奇偶校验位检测传输错误,许多微处理器的指令高速缓存中也包括奇偶校验位保护。因为指令缓存数据是主内存数据的副本,所以在发现错误的时候能够抛弃错误数据并且重新取回数据。
在串行数据通信中,常用的格式是 7 个数据位、1 个校验位、1 到 2 个停止位。这种格式用方便的 8 位字节巧妙地适应了所有的 7 位 ASCII 字符。也可以用其它的格式表示,8 位数据加上 1 个校验位可以传输任意的 8 位字节数据。
在串行通信中,奇偶校验位通常是由UART这样的接口硬件生成、校验的,在接收方,通过接口硬件中的寄存器的状态位传给 CPU 以及操作系统。错误数据的恢复通常是通过重新发送数据,这个过程通常由如操作系统输入输出程序这样的软件处理的。
奇偶校验块(其实,可以不讲。不过,个人觉得有点意思。涉及硬盘数据恢复原理)
一些冗余磁盘阵列(en:RAID)使用奇偶校验块实现冗余。如果阵列中的一块磁盘出现故障,工作磁盘中的数据块与奇偶校验块一起来重建丢失的数据。
下面每列表示一个磁盘,假设 A1 = 00000111、A2 = 00000101 以及 A3 = 00000000。A1、A2、A3 异或得到的 Ap 等于 00000010。如果第二个磁盘出现故障,A2 将不能被访问,但是可以通过 A1、A3 与 Ap 的异或进行重建:
A1 XOR A3 XOR Ap = 00000101
冗余磁盘阵列
A1 A2 A3
Ap B1 B2
Bp C1 C2
C3 C4 Cp
注意:数据块是格式 A#,奇偶校验块是 Ap。
其实,我们从零开始串口代码,有2条路选:
- 直接看官方的例子,用到啥补啥(如什么是波特率,流控?)
- 另外一种就是小嗷的方法,先查查相关产品图(对标别的产品。对标这个词,我第一次听是从腾讯工作的一位大水比听到的,我们称它吹比达人),再去看看相关的类,再补补不懂的地方,再反过来对比相关产品,再看看例子。这样做的话,就像我们学完高中三年相关知识,去高考(我们不一定复习完全部内容)。
两者的区别在于:
- 记忆的时效性而言1比2快忘记
- 1就算做完,心里也是没什么底气(开发软件,很少出现题海战术。就像高一开始,我只做历年真题,难度大。做完试卷,转个弯你未必能转过来【出现bug的时候】)
- 1比2优点,网上搜例子运气好的话。直接复制粘贴改一改就完事。就看别人开源给不给力。给力的话,用不了20分钟实现产品【一般像Java/Android/IOS/QT什么自带不能直接完事】。同理,要是别人开源代码挖几个坑,我相信你百分百走着走着掉下去。挖坑的人,基本不会填坑(为啥救你?救你要花钱买绳子吊你上来,再花钱买铲子填坑,耗时耗钱耗力)。
- 如果时间允许的话,我还是建议大家选择2.
网址如下:http://doc.qt.io/qt-5/qserialport.html#BaudRate-enum
4.1 点击Qt Serial Port
4.2 阅读英文翻译
得到的信息如下:
使用qt中的串口通信的时候需要用到的两个头文件分别为:
#include <QtSerialPort/QSerialPort>
#include <QtSerialPort/QSerialPortInfo>
除了加上面两个头文件之外,还需要在工程文件.pro中加下面一行代码(上面搞过):
QT += serialport
我们一般都需要先定义一个全局的串口对象,记得在自己的头文件中添加上 n :
QSerialPort *serial;
4.3 查一查QSerialPortInfo
QSerialPortInfo英文意思大概是串口信息,小嗷也不清楚是什么,查一查如下:
网址:http://doc.qt.io/qt-5/qserialportinfo.html#details
QSerialPortInfo Class:
提供关于现有串口的信息。
使用静态函数生成QSerialPortInfo对象列表。列表中的每个QSerialPortInfo对象都代表一个串行端口,并且可以查询港口名称、系统位置、描述和制造商。QSerialPortInfo类也可以用作QSerialPort类的setPort()方法的输入参数。
4.4 点击examples(例子)
网址:http://doc.qt.io/qt-5/qtserialport-index.html
如下信息:
网址:http://doc.qt.io/qt-5/qtserialport-examples.html
4.5 点击Blocking Master Example
小嗷其实就是任意点一个例子看看。当然,英文内容,小嗷还是读懂大概意思。
纳里,就是第一个搜索图的项目代码的讲解,很好很好,没有注解的代码,不是好代码。小嗷一般都不写注解,哈哈哈。小嗷估计其余项目例子都一样,
4.6 点击Terminal Example
小嗷再翻翻其他项目翻到Terminal Example,看看内容就知道可以动手搞定。项目如下:
网址如下:http://doc.qt.io/qt-5/qtserialport-terminal-example.html
4.6.1 在MainWindow主界面创建new QSerialPort(this)对象;
4.6.2 链接控件的回调函数(信号与槽,打个比方就是:按钮按一下,调用这个函数【槽】。当然不怎么专业,为了便于你们理解)
4.6.3 打开串口【设置什么波特率,几号串口等,再连接串口】
4.6.4 关闭串口,读串口数据和发送串口数据
基本上,拥有我们需要的功能:打开串口 -> 发送数据/接收数据 -> 关闭串口(当然,上面已经介绍的错误识别。具体情况,在实现过程,看看。)
5.1 添加到.pro
QT += serialport
5.2 写界面(Label + Combo Box【左键双击控件】)
只写比较关键操作
5.2.1 Combo Box【左键双击控件,添加值】,需要翻看上面的功能函数定义
相关控件命名:
5.3 头文件mainwindow.h
代码如下:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
//上图忘了添加QtSerialPort和QSerialPortInfo两个类
#include <QMainWindow>
#include <QtSerialPort/QSerialPort>
#include <QtSerialPort/QSerialPortInfo>
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
//创建按钮的回调函数(响应事件)和接收数据的回调函数
private slots:
void on_OpenSerialButton_clicked();
void ReadData();
void on_SendButton_clicked();
//定义一个QSerialPort的全局变量:serial
private:
Ui::MainWindow *ui;
QSerialPort *serial;
};
#endif // MAINWINDOW_H
5.4 解析源文件mainwindow.cpp
5.4.1 查找可用的串口
对标别人的产品,打开软件自动获取当前电脑的COM口的信息。
实现思路:
- 查看系统是否有用的串口
- 有用就添加到ComboBox这个组合框中(小嗷在Android中,习惯说下拉菜单)
步骤:
第一个问题: 怎么获取系统是否有用的串口?
小嗷记得之前有个QSerialPortInfo Class查一查,发现一个有用串口的东东,点击进入
网址: http://doc.qt.io/qt-5/qserialportinfo.html#availablePorts
英文翻译:返回一个在系统上有用的串口列表
第二个问题:怎么读取列表(list)中的内容
本来小嗷向上网查查怎么读取列表(list)中的内容。这个自动获取串口怎么这么眼熟?
打开QT提供的例子 -> 在“.cpp”中 ctrl + F 查找“availablePorts”关键字
C++11中引入的auto主要有两种用途:自动类型推断和返回值占位
const auto infos = QSerialPortInfo::availablePorts();
//for循环中的:符号是什么意思
//for(x:y)表示x属于y,并且遍历y中的所有元素
for (const QSerialPortInfo &info : infos)
serialPortComboBox->addItem(info.portName());
一套带走,搞定,嗷嗷嗷!
实现代码加强版如下(因为考虑到有的串口没有关闭的状态):
//查找可用的串口
const auto infos = QSerialPortInfo::availablePorts();
for (const QSerialPortInfo &info : infos)
{
QSerialPort serial;
serial.setPort(info);
//如果某个串口打开,读取正常,统统关闭
if(serial.open(QIODevice::ReadWrite))
{
ui->PortBox->addItem(info.portName());
serial.close();
}
}
设置选择的项,如:当我数据位下拉菜单选择8时,设置Data8
//设置数据位数
switch (ui->BitBox->currentIndex())
{
case 8:
serial->setDataBits(QSerialPort::Data8);//设置数据位8
break;
default:
break;
}
为啥小嗷不写全?
瞄了一眼,波特率要写8个,每个3行。大概从波特率到流控估计要写多60-N行代码。算了,直接写死。
5.5 mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
//查找可用的串口
const auto infos = QSerialPortInfo::availablePorts();
for (const QSerialPortInfo &info : infos)
{
QSerialPort serial;
serial.setPort(info);
//如果某个串口打开,读取正常,统统关闭
if(serial.open(QIODevice::ReadWrite))
{
ui->PortBox->addItem(info.portName());
serial.close();
}
}
//开机设置所有下拉菜单默认显示第3项(0为第一项,看项目需求)
ui->BaudBox->setCurrentIndex(2);
ui->BitBox->setCurrentIndex(2);
ui->ParityBox->setCurrentIndex(2);
ui->BitBox->setCurrentIndex(2);
ui->FlowBox->setCurrentIndex(2);
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::on_OpenSerialButton_clicked()
{
if(ui->OpenSerialButton->text() == tr("打开串口"))
{
serial = new QSerialPort;
//设置串口名
serial->setPortName(ui->PortBox->currentText());
//打开串口
serial->open(QIODevice::ReadWrite);
//设置波特率
serial->setBaudRate(QSerialPort::Baud115200);//设置波特率为115200
//设置数据位数
switch (ui->BitBox->currentIndex())
{
case 8:
serial->setDataBits(QSerialPort::Data8);//设置数据位8
break;
default:
break;
}
//设置校验位
switch (ui->ParityBox->currentIndex())
{
case 0:
serial->setParity(QSerialPort::NoParity);
break;
default:
break;
}
//设置停止位
switch (ui->BitBox->currentIndex())
{
case 1:
serial->setStopBits(QSerialPort::OneStop);//停止位设置为1
break;
case 2:
serial->setStopBits(QSerialPort::TwoStop);
default:
break;
}
//设置流控制
serial->setFlowControl(QSerialPort::NoFlowControl);//设置为无流控制
//关闭设置菜单使能
ui->PortBox->setEnabled(false);
ui->BaudBox->setEnabled(false);
ui->BitBox->setEnabled(false);
ui->ParityBox->setEnabled(false);
ui->StopBox->setEnabled(false);
ui->OpenSerialButton->setText(tr("关闭串口"));
//连接信号槽
QObject::connect(serial,&QSerialPort::readyRead,this,&MainWindow::ReadData);
}
else
{
//关闭串口
serial->clear();
serial->close();
serial->deleteLater();
//恢复设置使能
ui->PortBox->setEnabled(true);
ui->BaudBox->setEnabled(true);
ui->BitBox->setEnabled(true);
ui->ParityBox->setEnabled(true);
ui->StopBox->setEnabled(true);
ui->OpenSerialButton->setText(tr("打开串口"));
}
}
//读取接收到的信息
void MainWindow::ReadData()
{
QByteArray buf;
buf = serial->readAll();
if(!buf.isEmpty())
{
QString str = ui->textEdit->toPlainText();
str+=tr(buf);
ui->textEdit->clear();
ui->textEdit->append(str);
}
buf.clear();
}
//发送按钮槽函数
void MainWindow::on_SendButton_clicked()
{
//Latin1是ISO-8859-1的别名,有些环境下写作Latin-1。ISO-8859-1编码是单字节编码,向下兼容ASCII
//其编码范围是0x00-0xFF,0x00-0x7F之间完全和ASCII一致,0x80-0x9F之间是控制字符,0xA0-0xFF之间是文字符号。
serial->write(ui->textEdit_2->toPlainText().toLatin1());
}