QT实现串口通信步骤以及问题记录,小白文,大佬轻锤,欢迎指错。
下面是UI界面,主要需求:通过串口或网口方式收发数据,读取下位机状态以及对其进行控制。
串口部分主要控件:两个QTextBrowser记录收发数据,串口开关,QLabel制作开关指示灯
(控制界面还未全部完成)
1. 工程文件及头文件添加代码
工程文件xxx.pro中添加:
#串口通信
QT +=serialport
头文件xxx.h中添加:
//串口通信
#include <QSerialPort>
#include <QSerialPortInfo>
2. 参数设置
//串口变量↓
QSerialPort *serial; //定义全局的串口对象
QStringList baudList; //波特率
QStringList parityList; //校验位
QStringList dataBitsList; //数据位
QStringList stopBitsList; //停止位
QStringList flowControlList; //控制流
3. 串口初始化函数
void MainWindow::SerialPortInit()
{
serial = new QSerialPort; //申请内存,并设置父对象
// 获取计算机中有效的端口号,然后将端口号的名称给端口选择控件
foreach(const QSerialPortInfo &info,QSerialPortInfo::availablePorts())
{
serial->setPort(info); // 在对象中设置串口
if(serial->open(QIODevice::ReadWrite)) // 以读写方式打开串口
{
//ui->UI界面你添加的控件名->控件方法
ui->PortBox->addItem(info.portName()); // 添加计算机中的端口
qDebug() << "串口打开成功";
serial->close(); // 关闭
} else
{
qDebug() << "串口打开失败,请重试";
}
}
// 参数配置,波特率
serial->setBaudRate(QSerialPort::Baud19200);
// 校验位,校验默认选择无
serial->setParity(QSerialPort::NoParity);
// 数据位,数据位默认选择8位
serial->setDataBits(QSerialPort::Data8);
// 停止位,停止位默认选择1位
serial->setStopBits(QSerialPort::OneStop);
// 控制流,默认选择无
serial->setFlowControl(QSerialPort::NoFlowControl);
}
- 这里的参数配置 (如波特率数值19200) 需要与下位机一致。
4.开关按钮以及显示灯
void MainWindow::on_OpenSerialButton_clicked()
{
//用不上这段代码
#if 0
if(uSocket->isOpen()){
//按钮被点击时,无论当前状态,如果开启则关闭
uSocket->close();
// 关闭状态,按钮显示“打开串口”
ui->UDP_ConnectButton->setText("打开\n网口");
// 关闭状态,颜色为绿色
ui->UDP_ConnectButton->setStyleSheet("color: green;");
// 关闭,显示灯为红色
UDP_LEDChange(true);
ui->OperatorLog->append("<b><font color=\"#B22222\">"+TimeStamp()+"UDP已关闭!</b>");
ui->OperatorLog->append("<font color=\"#B22222\">原因:串口操作");
}
#endif
if(serial->isOpen()) // 如果串口打开,则将其关闭
{
serial->clear();
serial->close();
// 关闭状态,按钮显示“打开串口”
ui->OpenSerialButton->setText("打开\n串口");
// 关闭状态,字体颜色改为绿色
ui->OpenSerialButton->setStyleSheet("color: green;");
// 关闭,显示灯改为红色
LED(true);
ui->OperatorLog->append(TimeStamp()+"<b><font color=\"#B22222\">"+TimeStamp()+"串口已关闭 0</b>");
ui->ControlBox->setEnabled(false);
}
else // 如果串口关闭,则将其打开
{
//当前选择的串口名字
serial->setPortName(ui->PortBox->currentText());
//用ReadWrite 的模式尝试打开串口,无法收发数据时,发出警告
if(!serial->open(QIODevice::ReadWrite))
{
QMessageBox::warning(this,tr("提示"),tr("串口打开失败!"),QMessageBox::Ok);
return;
}
// 打开状态,按钮显示“关闭串口”
ui->OpenSerialButton->setText("关闭\n串口");
// 打开状态,字体颜色改为红色
ui->OpenSerialButton->setStyleSheet("color: red;");
// 打开,显示灯改为绿色
LED(false);
ui->OperatorLog->append(TimeStamp()+"<b><font color=\"#32CD32\">"+TimeStamp()+"串口已开启</b>");
ui->ControlBox->setEnabled(true);
}
}
// 开关显示灯
void MainWindow::LED(bool changeColor)
{
if(changeColor == false)
{
// 显示绿色
ui->LED->setStyleSheet("background-color: qradialgradient(spread:pad, cx:0.5, cy:0.5, radius:0.5, fx:0.5, fy:0.5, stop:0 rgba(0, 229, 0, 255), stop:1 rgba(255, 255, 255, 255));border-radius:12px;");
}
else
{
// 显示红色
ui->LED->setStyleSheet("background-color: qradialgradient(spread:pad, cx:0.5, cy:0.5, radius:0.5, fx:0.5, fy:0.5, stop:0 rgba(255, 0, 0, 255), stop:1 rgba(255, 255, 255, 255));border-radius:12px;");
}
}
- #if 1表示直到#endif之间的这段代码正常开启,#if 0表示关闭。 可以理解为高效将此代码块注释掉。(文中的#if 到#endif内容可以忽略)
- QMessageBox是提示窗口,需要在头文件或者源文件内添加#include <QMessageBox>
- TimeStamp()是自写的时间戳函数,返回当前时间,精确小数点后两位。
- SetStyleSheet字体样式设置,类似html中的格式,颜色可在rgb调色板中寻找。这是平时方找颜色时常用的网站
- 关于函数名on_OpenSerialButton_clicked(),这是个槽函数,前缀为on_,后缀为_clicked(),中间是控件的名称,这种格式写槽函数便可以省去写connect函数。
//时间戳
QString MainWindow::TimeStamp(){
QString str;
QTime time = QTime::currentTime();
QString msec = QString::number(time.msec());
//如果毫秒恰好为整数0,则添加个0,保持秒数为两位
if(msec.size()==1){
msec.append("0");
}
//msec.left(2)表示取毫秒数的前两位
str.append("◆"+time.toString("hh:mm:ss:")+msec.left(2)+"◆→ ");
return str;
}
5. 串口数据接收
(1)接收函数
//串口接收
void MainWindow::Serial_DataReceived()
{
QByteArray data;
//下位机发送
if(serial->bytesAvailable()>= 10 ) // 10为一次需要读取的字节数
{
data = serial->readAll(); // 读取数据
qDebug() << "initial data test"<< data;
//将接收到的QByteArray数据转为16进制,DataRecevied_Hex为QString全局变量
DataRecevied_Hex = data.toHex();
//注释
//ui->Serial_DataReceived->append( TimeStamp()+ DataFactory(DataRecevied_Hex));
//注释
//AngleCurrent();
//这里可以直接将读取到的数据显示到你指定的控件,例如ui->控件名->append(内容);
//注意内容类型,文中用的控件是QTextBrowser,append的内容需要是QString类型的
//控件有两种方式,一种是代码生成,一种是ui界面绘制,文中多数采取的是ui界面绘制。
}
}
(2)窗口内调用串口初始化函数 ,并写上串口接收信号槽,便能读取数据。
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
SerialPortInit();
/**********************************************
*
* ↓ 信号槽 ↓
*
**********************************************/
/* Serial */
// 串口接收数据
//信号槽,connect(发送方,发送的信号,接收方,触发的函数)
//下面这段既串口为发送方,信号则是串口接收到数据,接收方this,触发Serial_DataReceived函数
connect(serial,&QSerialPort::readyRead,this,&MainWindow::Serial_DataReceived);
//上文提到的on_OpenSerialButton_clicked,如果写成connect的方式,则为
//connect(OpenSerialButton,&QPushButton::clicked,this,&MainWindow::OpenSerialButton);
//既(控件名,控件被点击时发送信号,接收方this,触发函数),将触发函数写成on_控件名_clicked则可省略,方便许多。
}
//串口接收
void MainWindow::Serial_DataReceived()
{
QByteArray data;
if(serial->bytesAvailable()>= 10 ) // 10为一次需要读取的字节数
{
data = serial->readAll(); // 读取数据
qDebug() << "initial data test"<< data;
DataRecevied_Hex = data.toHex();
ui->Serial_DataReceived->append( TimeStamp()+ DataFactory(DataRecevied_Hex));
AngleCurrent();
}
}
接收数据方法有很多,但多数都存在一定问题,
或是数据读取不全,或是读取顺序不对、时快时慢。
最后在网上找到了这个办法,目前为止没出错。
问题记录:关于 字节 与 16进制(Hex)
之前一直没绕明白为什么1字节可以表示两个16进制数,饶了很久。
个人理解16进制是人类语言,需要转换成计算机能理解的0与1电信号。
1字节(Byte)= 8 比特(bit),既00000000。
想表示一个16进制数,需要至少16种(0~F)二进制组合来区分,
对应二进制就至少需要4个bit,即0000。
结论:1个16进制位=0.5个字节。
所以接收到的10字节内容,既80bit,每4位表示一个16进制,可以表示20位。
6. 串口数据发送
本文数据发送,需要将用户输入的高低角与方位角角度数值转为16进制QByteArray类型。
(1)角度更改按钮
void MainWindow::on_Control_AngleChange_clicked(){
...
}
(2)高低字节转换
QString MainWindow::gdwChange(QString s){
//初始0°或为空字符串
if(s=="0"||s==""){
s="0000";
}
//10进制转为16进制的String类型后,需要补位,2位前面补00以此类推。
//即使是1°,转换后也是200左右,需要至少2位16进制表示,所以除0°外size不会为1。
//append为添加至字符串末尾,prepend为添加至字符串开头,
//remove为移除部分字符串,例如F123,remove(2,1)则代表从第2位开始,移除1位,移除后为F13
if(s.size()==2){
s.append("00"); //填充+高低位位置变化
}else if(s.size()==3){
s.prepend("0");
QString ache;
ache.append(s.at(2));
ache.append(s.at(3));
s.remove(2,2);
s.prepend(ache);
}else if(s.size()==4){
QString ache;
ache=s.at(2);
ache.append(s.at(3));
s.remove(2,2);
s.prepend(ache);
}
return s;
}
(3)校验位
//校验位计算与转换
void MainWindow::Parity_DataSend(){
//本文接口协议校验方式为校验和计算,既末尾校验字节等于除校验位的所有字节相加,然后取后两位
//如ff b2 7f 02 d9 02 26 00 e1 14,则最后的14是通过ff + b2 + 7f + ...得出的。(414后两位)
int ache=0;
//↓除去校验位本身的长度
for(int i=0;i<DataSend_Hex.size()-2; i+=2){
//全局变量QString DataSend_Hex="ffb200000000000000000c000000";
//先将16进制QString转10进制QString,再转为int类型,最后根据基数和位权计算
ache += HexToDec(DataSend_Hex.at(i)).toInt()*16 +
HexToDec(DataSend_Hex.at(i+1)).toInt();
//at是Qstring的方法,读取字符串某一位,效率高,但只限读取无法更改。
//HexToDec是自定义函数,用于将16进制字符转为10进制
}
//最后得到的int数值再转为16进制的QString类型(转来转去只是为了计算,(╯▔皿▔)╯恼)
QString Parity =QString::number(ache, 16);
//判断校验位的位数,不足补0
if(Parity.size()==1){
Parity.prepend("000");
}
else if(Parity.size()==2){
Parity.prepend("00");
}
else if(Parity.size()==3){
Parity.prepend("0");
}
//将最后两位(最后一字节)替换。(协议规定发送时是发14字节,接收时接收10字节)
//replace是自带的字符串替换函数,replace(从第26位开始,取两位,替换内容)
Parity.remove(0,2);
DataSend_Hex.replace(26,2,Parity);
//qDebug可以理解为只显示于是控制台的cout,经常拿来测试运行后的数据值
qDebug() << "校验位更改:" << DataSend_Hex;
}
(4)进制转换
分别是16转2,16转10,2转16,只能一位一位的转,挺笨重的方法。
/* ↓ 进制转换 */
QString MainWindow::HexToBinary(QString s){
if(s=="0"){
s="0000";
}else if (s=="1") {
s="0001";
}else if (s=="2") {
s="0010";
}else if (s=="3") {
s="0011";
}else if (s=="4") {
s="0100";
}else if (s=="5") {
s="0101";
}else if (s=="6") {
s="0110";
}else if (s=="7") {
s="0111";
}else if (s=="8") {
s="1000";
}else if (s=="9") {
s="1001";
}else if (s=="a"||s=="A") {
s="1010";
}else if (s=="b"||s=="B") {
s="1011";
}else if (s=="c"||s=="C") {
s="1100";
}else if (s=="d"||s=="D") {
s="1101";
}else if (s=="e"||s=="E") {
s="1110";
}else if (s=="f"||s=="F") {
s="1111";
}
return s;
}
QString MainWindow::HexToDec(QString s){
if(s=="0"){
s="0";
}else if (s=="1") {
s="1";
}else if (s=="2") {
s="2";
}else if (s=="3") {
s="3";
}else if (s=="4") {
s="4";
}else if (s=="5") {
s="5";
}else if (s=="6") {
s="6";
}else if (s=="7") {
s="7";
}else if (s=="8") {
s="8";
}else if (s=="9") {
s="9";
}else if (s=="a"||s=="A") {
s="10";
}else if (s=="b"||s=="B") {
s="11";
}else if (s=="c"||s=="C") {
s="12";
}else if (s=="d"||s=="D") {
s="13";
}else if (s=="e"||s=="E") {
s="14";
}else if (s=="f"||s=="F") {
s="15";
}
return s;
}
QString MainWindow::BinaryToHex(QString s){
if(s=="0000"){
s="0";
}else if (s=="0001") {
s="1";
}else if (s=="0010") {
s="2";
}else if (s=="0011") {
s="3";
}else if (s=="0100") {
s="4";
}else if (s=="0101") {
s="5";
}else if (s=="0110") {
s="6";
}else if (s=="0111") {
s="7";
}else if (s=="1000") {
s="8";
}else if (s=="1001") {
s="9";
}else if (s=="1010") {
s="a";
}else if (s=="1011") {
s="b";
}else if (s=="1100") {
s="c";
}else if (s=="1101") {
s="d";
}else if (s=="1110") {
s="e";
}else if (s=="1111") {
s="f";
}
return s;
}
/* ↑ 进制转换 */
(5)发送方式
//最终数据发送[判断是 串口发送 or UDP发送]
void MainWindow::SendMode(){
QByteArray dataByte =QByteArray::fromHex(DataSend_Hex.toLatin1());
//发送方式判断,哪个端口打开了就用哪个发送,文中开关按钮设置了互斥,所以串口和网口不会同时打开。
if(serial->isOpen()){
//串口写入
qDebug() << "串口发送数据:"<< dataByte;
//数据写入
serial->write(dataByte);
//全局变量行间距const QString Line75Percent="<p style='line-height:75%'>";
ui->Serial_DataSend->append(Line75Percent+TimeStamp()+"<font color=\"#05BDFF\">"+DataFactory(DataSend_Hex)+"</font>"+"</p>");
}
#if 0
else if(uSocket->isOpen()){
qDebug() << "UDP发送数据:";
QString IPDestination = ui->IPDestination->text();
quint16 PortDestination = ui->PortDestination->text().toInt();
qDebug() << "IPDestination:"<<IPDestination;
qDebug() << "PortDestination:"<<PortDestination;
uSocket->writeDatagram(dataByte,QHostAddress(IPDestination),PortDestination);
ui->UDP_DataSend->append(Line75Percent+TimeStamp()+"<font color=\"#05BDFF\">"+DataFactory(DataSend_Hex)+"</font>"+"</p>");
}else{
qDebug() << "请打开串口或UDP";
}
#endif
};
(6)数据加工
DataFactory就是将内容加空格,并转为大写。
//数据加工[Data=DataReceived_Hex or DataSend_Hex]
QString MainWindow::DataFactory(QString Data){
QString str=Data;
//如果字符串不是20位或者28位,说明数据有误,直接打死。
if(str.size()!=20 && str.size()!=28){
qDebug() << "数据出错了";
return 0;
}
//加空格,这里要注意每次添加后,添加的空格本身也占长度,所以coefficient每次加3,而不是加2
int coefficient = 0;
for(int i=0;i<Data.size()/2;i++){
str.insert(coefficient," ");
coefficient +=3;
}
//字符串转大写,toLower是转小写,QString自带函数
str = str.toUpper();
//测试[接通下位机时应注释]
//str ="FF FF FF FF FF FF FF FF FF FF FF FF FF FF";
//str ="CC CC CC CC CC CC CC CC CC CC CC CC CC CC";
return str;
};
7. 总结
串口通信步骤:读取串口,设置参数,获得数据,数据类型转换,使用数据
感觉耗的大部分时间都在数据转换这一部分,真是把上学那会落得都补回来了🤷♂️
之后会总结数据转换的各类方法,先哭为敬。