【QT Creator学习记录】上位机与下位机串口通信

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. 总结

串口通信步骤:读取串口,设置参数,获得数据,数据类型转换,使用数据

感觉耗的大部分时间都在数据转换这一部分,真是把上学那会落得都补回来了🤷‍♂️

之后会总结数据转换的各类方法,先哭为敬。

  • 22
    点赞
  • 154
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
qt串口升级上位机程序是一种能够实现通过串口对设备进行固件升级的上位机控制程序。上位机程序一般运行在计算机上,通过串口与待升级的设备进行通信,实现固件升级的功能。 首先,在qt串口升级上位机程序中,需要实现串口的打开和关闭功能。通过串口打开函数,我们可以选择要进行升级的设备,以及设置相应的串口参数,如波特率、数据位、校验位等。 其次,在与设备建立串口通信之后,需要进行固件升级相关的操作。这通常包括发送固件升级指令给设备、向设备传输固件数据以及监控设备升级状态等。在程序中,我们可以使用串口发送函数将指令发送给设备,通过不断读取设备返回的数据来监控升级进度和状态。 另外,为了提高用户体验和程序的稳定性,上位机程序还可以实现一些附加功能。比如,在升级过程中可以显示当前升级进度的进度条,用户可以随时了解升级状态;还可以记录升级日志,便于之后的排查和分析;在升级完成后,还可以提供升级结果的提示信息,以及相应的操作建议。 综上所述,qt串口升级上位机程序是一种能够通过串口与设备进行通信,实现设备固件升级的上位机程序。它具有打开和关闭串口、发送升级指令和数据、监控升级状态等基本功能,并可以结合其他附加功能,提高用户体验和程序的稳定性。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值