精华内容
下载资源
问答
  • 2021-01-31 03:11:44

    一、串口通信简介

    串口通信,顾名思义也就是利用串行接口进行通信。串行接口指串口按位(bit)发送和接收字节。尽管比按字节(byte)传输的并行通信慢,但是串口可以在使用一根线发送数据的同时用另一根线接收数据。

    串口通信中比较重要的参数包括波特率、数据位、停止位及校验位,通讯双方需要约定一致的数据格式才能正常收发数据。串行通讯可以进一步分为单工、半双工和全双工三种。在串口通信中,常用的协议包括RS-232、RS-422和RS-485。它们的主要区别在于其各自的电平范围不相同。

    二、串行和并行通信

    刚说到串口通信是通过接口进行串行通讯。那么什么是串行通讯呢?

    1 串行通信:

    串行通信:计算机与I/O设备之间,同一时刻,只能传输一个bit位的信号。传输数据按顺序依次一bit位接一bit位进行传输,通常数据在一根数据线或一对差分线上传输。

    比如,当传输1字节信息时,并行通讯有8根信号线实现同时传输,假如耗时为1T,而串行是在一根信号线上,把数据排成一行、一位一位传输,需要传8次,因此耗时为8T。因此可总结出二者的特性:

    2 并行通信:

    并行通信是和串行通信相对的数据传输的方式。

    并行通信:计算机与I/O设备之间,通过多条传输线,可以同时传输多个bit位的信号。

    • 并行通讯的效率高,但是成本高、对信号线路要求高,一般应用于快速设备之间近距离传输,譬如CPU 与存储设备、存储器与存储器、主机与打印机等都采用并行通讯。

    • 串行通讯效率较低,但是对信号线路要求低,抗干扰能力强,同时成本也相对较低,一般用于计算机与计算机、计算机与外设之间远距离通讯。

    3 串口通信和串行通信的区别

    串口通信和串行通信的区别在于:串行通信是一种概念,串口通信是一种具体的通信手段。

    串行通信是一种概念,是指一比特一比特的收发数据,相对于并行通信可同时传输多个bit位而言。包括一般的的串口通信、I2C、SPI等等。

    串口通信是外设和计算机间的一种通信手段,是相对于以太网通信等通信手段而言的。

    二者一个是一种概念,一个是一种实际的通信方式。

    三、同步和异步通信

    在设备之间传送数据,不管是同步通信还是异步通信,都是为了保证数据被正确的发送和接收,即发送方和接收方的“同步”。即接收方可以确定什么时候发送方开始或者结束发送数据以及每一个数据单位(例如bit,字符)的开始和结束的位置,这样接收方才能在正确的时间对发送方的数据进行采样,以接收正确的数据,否则接收到的数据就是错误的。

    根据“同步方式”的不同,由此分出两种同步信号得方法:
    (1)同步通信(比特位同步)
    (2)异步通信(字符间同步,字符内比特位异步)

    同步通信会利用一根额外的信号线,其实也就是时钟信号线,它往往是发送设备提供的时钟信号,发送设备和接收设备在发送设备提供的同一时钟频率下完成同步。(实际上,基本所有的并行通信采用同步通信。)

    异步通信没有额外的一根信号线用于同步,接收者和发送者使用各自的时钟信号,接收者根据与发送者按事先约定的规来确定数据发送的开始与结束以及数据单位的持续时间。例如异步串行通信中,一般接收双方会确定一致的停止位,数据位的个数、波特率的大小以及是否采用奇偶校验位。接收方可以根据这些信息推测出准确的数据采样时间以接收正确的数据。如果是同步通信则不需要这些额外的用于同步的数据位(开始位,结束位,奇偶校验位)。

    1 同步通信

    同步通信要求发送和接收双方在进行数据传输时,保持完全的同步,因此,要求发收双方必须使用同频同相的同步时钟信号。只需在传送报文的最前面附加特定的同步字符,使发收双方建立同步,此后在同步时钟的控制下逐位发送/接收。这样,信息传输完全可以确定传输过程中每1位的位置。因此同步通信是一种比特同步通信技术。

    如下图所示:
    在这里插入图片描述

    同步通信是一种连续传送数据的通信方式,一次通信传送一帧数据,每个信息帧用同步字符作为开始,字符间不加标识位。(这里的数据帧比异步通信中的字符帧要大得多,通常含有若干个数据字符)。当检测到有一串数位和同步字符相匹配时,就认为开始一个信息帧,于是,把此后的数位作为实际传输信息来处理。

    同步通信以数据帧为单位,其格式包括:同步字符+数据+校验字符CRC。

    (a)单同步数据帧结构

    同步字符数据字符1数据字符2数据字符3数据字符nCRC1CRC1

    (b)双同步数据帧结构

    同步字符1同步字符2数据字符1数据字符2数据字符nCRC1CRC1

    没有数据发送时,接收方要时刻做好接收数据的准备。在每组信息(通常称为帧)传输的开始,发送方先发送一个或两个特殊字符,该字符称为同步字符。当接收方收到同步字符,并和发送方达到同步后,就可以以固定的节奏一个字符接一个字符地发送一大块数据,而不再需要用起始位和停止位了,这样可以明显地提高数据的传输速率。同步通信更加适合对速度要求高的传输,对时序的要求很高,当然对硬件要求也更高。

    在没有信息要传输时,要填上空字符,因为同步传输不允许有间隙。在同步传输过程中,一个字符可以对应5~8位。当然,对同一个传输过程,所有字符对应同样的数位,比如说n位。这样,传输时,收发双方用一个时钟进行协调,按每n位划分为一个时间片,发送端在一个时间片中发送一个字符,接收端则在一个时间片中接收一个字符,这样就可以确定传输中每一位的位置。接收数据时,接收方利用同步字符使内部时钟与发送方保持同步,然后将同步字符后面的数据逐位移入,并转换成并行格式,供CPU读取,直至收到结束符为止。

    2 异步通信

    异步通信是按字符帧传输的,相对于同步通信,异步通信在发送字符时,所发送的字符之间的时隙可以是任意的,接收方并不知道数据什么时候会到达,因此接收端必须时刻做好接收的准备(如果接收端主机的电源都没有加上,那么发送端发送字符就没有意义,因为接收端根本无法接收)。发送端可以在任意时刻开始发送字符,时间间隔可以是任意的,在一字符帧中的所有比特是连续发送的。

    发送端不需要在发送字符之前和接收端进行协调(不需要先进行比特同步)。接收设备在收到起始信号之后只要在一个字符的传输时间内能和发送设备保持同步就能正确接收。内部处理器在完成了相应的操作后,通过一个回调的机制,以便通知发送端发送的字符已经得到了回复。下一个字符起始位的到来又使同步重新校准(依靠检测起始位来实现发送与接收方的时钟自同步的字符间同步,字符内比特位异步)

    因此必须在每一个字符的开始和结束的地方加上标志,即加上开始位和停止位,以便使接收端能够正确地将每一个字符接收下来。通信双方需要对采用的信息格式(字符的位数、停止位的位数、有无校验位及校验方式等)和数据的传输速率作相同的约定。接收方是在数据的起始位和停止位的帮助下实现字符传送时的同步。这种传输通常是很小的分组,比如一个字符为一组,为这个组配备起始位和结束位。所以这种传输方式的效率是比较低的,毕竟额外加入了很多的辅助位作为负载,常用在低速的传输中。

    在这里插入图片描述
    异步通信以字符为单位,其格式包括:起始位+数据+奇偶校验位+停止位。
    以起止式异步协议为例,如下图所示
    在这里插入图片描述
      起止式异步通信的特点是:一个字符一个字符地传输,每个字符一位一位地传输,并且传输一个字符时,总是以"起始位"开始,以"停止位"结束,字符之间没有固定的时间间隔要求。每一个字符的前面都有一位低电平起始位(逻辑值0),字符本身由5-8位数据位组成,接着字符后面是一位校验位(也可以没有校验位),最后是一位或一位半或二位停止位,停止位后面是不定长的空闲位。停止位和空闲位都规定为高电平(逻辑值1),这样就保证起始位开始处一定有一个下跳沿。由此就可以标志一个字符传输的起始。而根据起始位和停止位也就很容易的实现了字符的界定和同步。
      如上图中所示,这种格式是靠起始位和停止位来实现字符的界定或同步的,故称为起止式协议。
      
    (1)起始位:发送数据时,先发持续一个bit时间的逻辑”0”信号,表示字符传输的开始,接收端可根据起始位使自己的接收时钟与发送方的数据同步。

    (2)数据位:起始位后是数据位,异步传送规定低位在前,高位在后,数据位的位数一般可以是5~8位。

    (3)奇偶校验位:奇偶位紧跟在数据最高位之后,占用一位(也可省去)。加上这一位后,使得逻辑“1”信号的位数得到偶校验或奇校验,以此来校验数据传送的正确性。
      如果是奇校验,需要保证传输的数据总共有奇数个逻辑高位;如果是偶校验,需要保证传输的数据总共有偶数个逻辑高位。举例来说,假设传输的数据位为01001100,如果是奇校验,则奇校验位为0(要确保总共有奇数个1),如果是偶校验,则偶校验位为1(要确保总共有偶数个1)。
      由此可见,奇偶校验位仅是对数据进行简单的置逻辑高位或逻辑低位,不会对数据进行实质的判断,这样做的好处是接收设备能够知道一个位的状态,有可能判断是否有噪声干扰了通信以及传输的数据是否同步。

    (4)停止位:数据发送完后,再发1位、1.5位、2位的高电平(逻辑”1”信号)代表停止位,表示一帧数据结束,同时为接收下一帧数据做准备。

    (5)空闲位:在没有数据发送时,即下一帧的起始位“0”到来之前,数据线保持默认的“1”状态,即由高电平来填充。

    异步通信字符帧格式总结如下表:

    逻辑信号数据位数
    起始位01位
    数据位0或15~8位
    校验位0或11位或无
    停止位11位,1.5位或2位
    空闲位1任意数量

    :位数的本质含义是信号持续的时间,故可有分数位,如停止位1.5位,1.5是它的长度,即停止位的电平保持1.5个单位时间长度。一个单位时间就是波特率的倒数,例如波特率为9600bps,则一个单位时间长为1/9600s,1.5个停止位,即停止位电平保持1.5/9600s。

    3 同步通信和异步通信比较

    (1)同步通信要求接收端时钟频率和发送端时钟频率一致;异步通信时不要求接收端时钟和发送端时钟同步。

    (2)同步通信数据传输是以字节块(多个字节)传输的;异步通信数据传输是以字符(一个字节)传输的。

    (3)同步通信传输效率高,但复杂、要求高,双方时钟的允许误差较小;异步通信传输效率低,但简单、要求低,双方时钟可允许一定误差。

    (4)同步通信的字节传输是没有间隔的发送端发送连续的比特流;异步通信字节传送的间隔是任意的,发送端发送完一个字节后,可经过任意长的时间间隔再发送下一个字节。

    同步通信和异步通信的差异总结如下表:

    同步通信异步通信
    传送单位信息帧(由若干字符组成的数据块)字符(由若干bit组成)
    单位格式同步字符+数据+校验字符CRC起始位+数据位+奇偶校验位+停止位
    传送间隔一个数据块(信息帧)内,字符与字符间无间隔相邻两字符之间隔任意长
    时钟信号时序要求高,使用同频同相的时钟线路时序要求较低,使用各自的时钟信号
    优点效率高简单,要求低
    缺点复杂,要求高效率低(传送一个字符,要增加约20%的附加信息位)
    更多相关内容
  • 串口通信(内含完整的C语言代码)
  • Qt实现串口通信示例 前言:以下串口通信示例并不完全属于原创,参考了现有网上前辈们的资源,最后结合部分个人的思想,所以下述博客会将实现的原理及代码的案例进行公开。 这里我们先上效果图: 一、串口通信...

                                                         Qt实现串口通信示例

    前言:以下串口通信示例,参考了现有网上前辈们的资源,最后结合部分个人的思想,所以下述博客会将实现的原理及代码的案例进行公开。

    这里我们先上效果图:

    一、串口通信简介

    串口通信是上下位机进行通信的一种常用的通信协议,大部分单片机中都有一到多个串口资源经过简单的配置就可以实现上下位机的通信,下图是串口通信协议中的一种形式。如果你不是用硬件描述语言去实现一个串口,这部分了解下即可。常用的是8位数据位加起始位加停止位,因此串口是只能发送0-255区间的数据。因此想要发送int或者float型的数据需要按高地位或者到内存中取出数据来发送,此时就需要通信协议来确保数据的准确性,接下来将依次进行介绍。

    二、串口通信各参数的含义

    了解完串口通信的大致含义,我们再编程时候比较关心的还是串口通信中波特率、奇偶校验、数据位以及停止位的含义。下面我们引用一位前辈作出的解释,有关本文中涉及到的引用会在本文最后附上原创的出处,为了美观,不在相应的地方附上版权声明了。

    简介

    串口是一种非常通用的设备通信的协议(不要与通用串行总线Universal Serial Bus(USB)混淆)。大多数计算机包含两个基于RS232的串口。串口同时也是仪器仪表设备通用的通信协议;很多GPIB兼容的设备也带有RS-232口。同时,串口通信协议也可以用于获取远程采集设备的数据。
    串口通信的概念非常简单,串口按位(bit)发送和接收字节。尽管比按字节(byte)的并行通信慢,但是串口可以在使用一根线发送数据的同时用另一根线接收数据。它很简单并且能够实现远距离通信。比如IEEE488定义并行通行状态时,规定设备线总长不得超过20米,并且任意两个设备间的长度不得超过2米;而对于串口而言,长度可达1200米。
    典型地,串口用于ASCII码字符的传输。通信使用3根线完成:(1)地线,(2)发送,(3)接收。由于串口通信是异步的,端口能够在一根线上发送数据同时在另一根线上接收数据。其他线用于握手,但不是必须的。串口通信最重要的参数是波特率、数据位、停止位和奇偶校验。对于两个进行通行的端口,这些参数必须匹配:
    波特率

    这是一个衡量符号传输速率的参数。它表示每秒钟传送的符号的个数。例如300波特表示每秒钟发送300个符号。当我们提到时钟周期时,我们就是指波特率,例如如果协议需要4800波特率,那么时钟是4800Hz。这意味着串口通信在数据线上的采样率为4800Hz。通常电话线的波特率为14400,28800和36600。波特率可以远远大于这些值,但是波特率和距离成反比。高波特率常常用于放置的很近的仪器间的通信,典型的例子就是GPIB设备的通信。
    数据位

    这是衡量通信中实际数据位的参数。当计算机发送一个信息包,实际的数据不会是8位的,标准的值是5、6、7和8位。如何设置取决于你想传送的信息。比如,标准的ASCII码是0~127(7位)。扩展的ASCII码是0~255(8位)。如果数据使用简单的文本(标准 ASCII码),那么每个数据包使用7位数据。每个包是指一个字节,包括开始/停止位,数据位和奇偶校验位。由于实际数据位取决于通信协议的选取,术语“包”指任何通信的情况。
    停止位

    用于表示单个包的最后一位。典型的值为1,1.5和2位。由于数据是在传输线上定时的,并且每一个设备有其自己的时钟,很可能在通信中两台设备间出现了小小的不同步。因此停止位不仅仅是表示传输的结束,并且提供计算机校正时钟同步的机会。适用于停止位的位数越多,不同时钟同步的容忍程度越大,但是数据传输率同时也越慢。
    奇偶校验位

    在串口通信中一种简单的检错方式。有四种检错方式:偶、奇、高和低。当然没有校验位也是可以的。对于偶和奇校验的情况,串口会设置校验位(数据位后面的一位),用一个值确保传输的数据有偶个或者奇个逻辑高位。例如,如果数据是011,那么对于偶校验,校验位为0,保证逻辑高的位数是偶数个。如果是奇校验,校验位为1,这样就有3个逻辑高位。高位和低位不是真正的检查数据,简单置位逻辑高或者逻辑低校验。这样使得接收设备能够知道一个位的状态,有机会判断是否有噪声干扰了通信或者是否传输和接收数据是否不同步。
    2.补充

    比特率
    在数字信道中,比特率是数字信号的传输速率,它用单位时间内传输的二进制代码的有效位(bit)数来表示,其单位为每秒比特数bit/s(bps)、每秒千比特数(Kbps)或每秒兆比特数(Mbps)来表示(此处K和M分别为1000和1000000,而不是涉及计算机存储器容量时的1024和1048576)。
    波特率
    波特率指数据信号对载波的调制速率,它用单位时间内载波调制状态改变次数来表示,其单位为波特(Baud)。 波特率与比特率的关系为:比特率=波特率X单个调制状态对应的二进制位数。
    显然,两相调制(单个调制状态对应1个二进制位)的比特率等于波特率;四相调制(单个调制状态对应2个二进制位)的比特率为波特率的两倍;八相调制(单个调制状态对应3个二进制位)的比特率为波特率的三倍;依次类推。
    RS232是要用在近距离传输上最大距离为30M
    RS485用在长距离传输最大距离1200M

    三、Qt中的串口通信

    qt中集成了QSerialport类,可以直接使用该类实现串口通信。这部分内容看到网上有的说在建立工程的时候要选择Qt Project  Setting ,不过我在使用Qt5.12建立工程的时候仍然按照普通项目工程建立(这里不再额外叙述怎么New 一个Project了),如果Qt版本低的话,工程失败情况下可以另行找工程建立方法。

    在qt中使用串口通信仍然比较简单,只需要在.pro文件中加入下述代码就完成了。

    QT       += core gui serialport
    

    然后在工程的头文件中引用相关的头文件。

    #include <QtSerialPort/QSerialPort>
    #include <QtSerialPort/QSerialPortInfo>
    

    接着在类中将串口类进行实例化。

        QSerialPort *serialPort;
    

    以上的过程,我们就已经准备好了串口通信所需要的必要条件,接下来要使用串口通信,少不了的要对串口实例化的对象进行参数的设定,下面我们对串口进行初始化。

    void Serial_port::InitPort()
    {
        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();
            }
        }
        QStringList baudList;   //波特率
        QStringList parityList; //校验位
        QStringList dataBitsList;   //数据位
        QStringList stopBitsList;   //停止位
        // 波特率    //波特率默认选择下拉第三项:9600
        baudList<<"1200"<<"2400"<<"4800"<<"9600"
               <<"14400"<<"19200"<<"38400"<<"56000"
              <<"57600"<<"115200";
        ui->BaudBox->addItems(baudList);
        ui->BaudBox->setCurrentIndex(3);
        // 校验      //校验默认选择无
        parityList<<"无"<<"奇"<<"偶";
        ui->ParityBox->addItems(parityList);
        ui->ParityBox->setCurrentIndex(0);
        // 数据位      //数据位默认选择8位
        dataBitsList<<"5"<<"6"<<"7"<<"8";
        ui->DataBox->addItems(dataBitsList);
        ui->DataBox->setCurrentIndex(3);
        // 停止位      //停止位默认选择1位
        stopBitsList<<"1"<<"2";
        ui->StopBox->addItems(stopBitsList);
        ui->StopBox->setCurrentIndex(0);
    }
    

    其中初始化函数 中首先获取计算机中有效的端口号,然后将端口号的名称给端口选择控件。相应的波特率等也是赋予选择控件相应的值。setCurrentIndex()是给控件设定默认下拉项的index。

    经过对串口对象指针的初始化,就已经完成串口通信所要求的各参数配置,一个完整的通信当然也少不了串口的收数了,下面给出串口数据的收报函数。

    void Serial_port::readData()
    {
        QByteArray buf;
        if (serialPort){
    
        buf = serialPort->readAll();
    
        if (!buf.isEmpty())
        {
    
            receBytes += buf.size();
            QString redata = QString("received:%1").arg(QString::number(receBytes));
            ui->sendlabel->setText(redata);
            QString myStrTemp = QString::fromLocal8Bit(buf); //支持中文显示
            if(ui->reDisplay->isChecked())
            {
                QString str = ui->textBrowser->toPlainText();
                str +=myStrTemp;
                ui->textBrowser->clear();
                ui->textBrowser->append(str);
            }
        }
        buf.clear();
        }
    
    }
    

    其实,从上面的函数中我们就能发现qt中串口的收报函数很简单,只是需要下面的一行命令,就能够完成了串口的收数功能。

        buf = serialPort->readAll();
    

    而相应的我们并不会满足这样单调的功能,所以在收报函数中加入接收总字节数显示以及显示收到的内容,这样的功能。

            receBytes += buf.size();
            QString redata = QString("received:%1").arg(QString::number(receBytes));
            ui->sendlabel->setText(redata);
    
                ui->textBrowser->append(str);
    

    说完了串口通信的收报,我们先不提发报的事情,从相关的串口助手上我们都知道,在显示接收的数据时候,很多情况下要求显示收到报文的16进制形式,所以这里我们再写另外一种收报函数(以16进制接收并显示)。

    void Serial_port::readToHex()
    {
        QByteArray temp = serialPort->readAll();
        auto isShow = ui->reDisplay->isChecked();         //接收显示?
        QDataStream out(&temp,QIODevice::ReadOnly);    //将字节数组读入
        while(!out.atEnd())
        {
               qint8 outChar = 0;
               out>>outChar;   //每字节填充一次,直到结束
               //十六进制的转换
               QString str = QString("%1").arg(outChar&0xFF,2,16,QLatin1Char('0'));
               if(isShow){
                   ui->textBrowser->insertPlainText(str.toUpper());//大写
                   ui->textBrowser->insertPlainText(" ");//每发送两个字符后添加一个空格
                   ui->textBrowser->moveCursor(QTextCursor::End);
               }
        }
    
    }
    

    最后让我们完成串口通信的发报功能,这部分其实很简单,但为了追求发报功能的完整性,所以我们丰富这部分函数的功能,同样的将发送功能分为文本发送和以16进制形式的发送。

    void Serial_port::on_sendButton_clicked()
    {
        //Latin1是ISO-8859-1的别名,有些环境下写作Latin-1。ISO-8859-1编码是单字节编码,向下兼容ASCII
       //其编码范围是0x00-0xFF,0x00-0x7F之间完全和ASCII一致,0x80-0x9F之间是控制字符,0xA0-0xFF之间是文字符号。
        QString str = ui->lineEdit->text();
    
        if(!str.isEmpty())
        {
            auto isHexSend = ui->sHexRadio->isChecked();
    
            int len = str.length();
            if(len%2 == 1)
            {
                str = str.insert(len-1,'0');
            }
            QByteArray senddata;
            if(isHexSend)
            {
                StringToHex(str,senddata);
                serialPort->write(senddata);
    
                if(serialPort->write(senddata)<0)
                {
                    QMessageBox::critical(this, tr("Error"), serialPort->errorString());
                }
            }
            else
            {
                if(serialPort->write(ui->lineEdit->text().toLocal8Bit())<0)
                {
                    QMessageBox::critical(this, tr("Error"), serialPort->errorString());
                }
            }
    
        }
    
    }
    

    字符转换为16进制函数如下:

    void Serial_port::StringToHex(QString str, QByteArray &senddata)
    {
        int hexdata,lowhexdata;
               int hexdatalen = 0;
               int len = str.length();
               senddata.resize(len/2);
               char lstr,hstr;
               for(int i=0; i<len; )
               {
                   //char lstr,
                   hstr=str[i].toLatin1();
                   if(hstr == ' ')
                   {
                       i++;
                       continue;
                   }
                   i++;
                   if(i >= len)
                       break;
                   lstr = str[i].toLatin1();
                   hexdata = convertHexChart(hstr);
                   lowhexdata = convertHexChart(lstr);
                   if((hexdata == 16) || (lowhexdata == 16))
                       break;
                   else
                       hexdata = hexdata*16+lowhexdata;
                   i++;
                   senddata[hexdatalen] = (char)hexdata;
                   hexdatalen++;
               }
               senddata.resize(hexdatalen);
    
    }
    
    char Serial_port::convertHexChart(char ch)
    {
        if((ch >= '0') && (ch <= '9'))
                    return ch-0x30;  // 0x30 对应 ‘0’
                else if((ch >= 'A') && (ch <= 'F'))
                    return ch-'A'+10;
                else if((ch >= 'a') && (ch <= 'f'))
                    return ch-'a'+10;
        //        else return (-1);
        else return ch-ch;//不在0-f范围内的会发送成0
    
    }
    

    四、解码

    对于串口通信来说,收报是重要的一步但也不是最后的一步,在进行开发时,我们不但要完成数据的接收,同样要根据协议双方的约定完成报文的解码,再进行相应算法的开发,解码的复杂程度依赖于协议的复杂程度,这里我们仅仅提供一种简单的解码校验方式。

    假设上图是我们接收的一个完整报的形式,其中ox55和0x53表示报文头,中间的校验位等暂不考虑,最后的sum表示和校验位。和校验即通过数据的累加和去验证数据的正确与否。上图是一帧需要发送的数据可以看从左到右分别为帧头,功能字,数据帧与和校验,还可以加上数据位数。帧头和功能字可以根据自己的需求进行定制,数据位即需要发送的数据,最后的SUM即为无符号char型数据,将SUM之前的数据求和(不用管数据的溢出)即可。下位机按这种方式将数据发送上来上位机只要找到帧头即可进行解码并且对数据进行验证,这里附一个简单的上位机解码的程序,之前用来解码下位机发送来的欧拉角数据使用的程序。

    void uart::ReadData()
    {
    	ui.buffSize->setText(QString::number(serialPort.bytesAvailable()));
    	int buffersize = ui.bufferSize->value();
    	if (serialPort.bytesAvailable()>buffersize){   //更改过滤个数,提高通信速率
    	requestData = serialPort.readAll().toHex();//转成 hex
                }
    	if (!requestData.isEmpty() )
    	{
    		QByteArray temp = requestData.mid(QString(requestData).indexOf("55"), 22);
    		unsigned char buffer[11] = { 0 };
    		unsigned char sum = 0;
    		for (int i = 0; i < 11; i++)
    		{
    			buffer[i] = (ByteArrayToHexchar(temp.at((i << 1) )) << 4 )+ ByteArrayToHexchar(temp.at((i << 1) + 1 ));
    			if (i<10)
    				sum = sum + buffer[i];
    		}
    		//sum = buffer[0] + buffer[1] + buffer[2] + buffer[3] + buffer[5] + buffer[6];
    		if ((buffer[0] == 0x55) && (buffer[1] == 0x53))
    		if (sum == buffer[10])
    		{
    			float x, y, z;
    			x = (buffer[3] << 8 | buffer[2]);
    			y = (buffer[5] << 8 | buffer[4]);
    			z = (buffer[7] << 8 | buffer[6]);
    			x = x / 32768 * 180;
    			y = y / 32768 * 180;
    			z = z / 32768 * 180;
    			ui.x->setText(QString::number(x, 'f', 2));
    			ui.y->setText(QString::number(y, 'f', 2));
    			ui.z->setText(QString::number(z, 'f', 2));
    			//ui.textEdit->append(QString::number(z, 'f', 2));
    			
    			requestData.clear();
    		}
    	}
    	
    	if (!timer.isActive())
    		timer.start(5000);//500ms定时器
    }
    

    当然如果不考虑和校验位置的话就更简单了,假设我们的协议规定了一个完整报的长度为40字节,报文头为ox03和0x66,然后我们要从第15位和第16位解码出一个int数据,这里将去掉大部分个人的内容。

            if(temp.size()==40)
            {
                if((recvdata[0]==0x03)&&(recvdata[1]==0x66))
                {
                    target_data.e=(int)(recvdata[15]+recvdata[16]*256);
                    
                }
            }
    

    五、发报模拟器

    这里我们嵌入到demo中作为其中的一个小功能了,其中路径后面的输入框,在进行单击时候会打开文件系统框要求你去选取txt文件,然后点击播放按钮将读取txt中的内容(这里我的txt所有的数据都在一行,如果是分行的txt需要你自己去更改相应的代码)然后我这里每次模拟发报40个字节。

    void Serial_port::handleLineEditClicked()
    {
        //QString curPath = QDir::currentPath();
        QString curPath = "../QSerial_port";
        //QFile file;
        QString aFileName = QFileDialog::getOpenFileName(this,QString("选择文件"),curPath,QString("TEXT(*.txt)"));
        ui->m_txt->setText(aFileName);
    
    }
    
    void Serial_port::on_Playbutton_clicked()
    {
        QString path = ui->m_txt->text();
        //qDebug()<<"是否成功获取"<<path_ce->text();
    
        if(path.isEmpty())
            return;
        QFile aFile(path);
        if(!aFile.exists())     //文件不存在
            qDebug() <<"the file not exist!!";
            //return;
        if(!aFile.open(QIODevice::ReadOnly | QIODevice::Text))
            qDebug() <<"文件无法打开";
            //return;
        QTextStream aStream(&aFile);    //用文件流读取文件
        while (!aStream.atEnd())
        {
            QString qstr = aStream.readLine();   //读取文件的一行文本
            //qDebug()<<qstr.length();
            int len = qstr.length();
            if(len%2 == 1)
            {
                qstr = qstr.insert(len-1,'0');
            }
            QByteArray senddata;
            StringToHex(qstr,senddata);
            //qDebug()<<senddata.size();
            for(int i=0;i<senddata.size();i++)
            {
                QByteArray tmpsenddata=senddata.mid(i,40);
                i=i+39;
                //qDebug()<<tmpsenddata.length();
                serialPort->write(tmpsenddata);
    
                if(!serialPort->waitForBytesWritten())   //这一句很关键,决定是否能发送成功
                {
                     qDebug()<<"serial write error";
                }
    
                Sleep(1000);
            }
    
        }
    
    }
    

    到这里基本就完成了串口通信的全部内容(上述代码设计到了工程的ui控件,请自行修改),另外额外提供一点,在模拟器中如果循环对串口数据进行发报,我们都知道,核心的代码就是

                serialPort->write(tmpsenddata);
    

    但是当我们在进行发报的时候,会发现哪怕程序执行了write功能,收报方仍然无法收到,这里有人提出qt中的串口通信在发送后必须要进行发送是否成功的判断才能够正常工作,当然,这一点在循环中执行write是最明显看到的,单个命令可能是正常的,所以这里我们必须要加上判断。

                if(!serialPort->waitForBytesWritten())   //这一句很关键,决定是否能发送成功
                {
                     qDebug()<<"serial write error";
                }
    

    好了到此Qt的串口通信就完全结束了,为了更直观的看到各功能的实现,下面给出完整的代码,如果还不明白,点击最后链接下载完整工程唠!

    Serial_port.h

    #ifndef SERIAL_PORT_H
    #define SERIAL_PORT_H
    
    #include <QMainWindow>
    #include <QtSerialPort/QSerialPort>
    #include <QtSerialPort/QSerialPortInfo>
    #include <QLabel>
    #include <QTimer>
    #include <windows.h>
    #include <QString>
    #include <QDebug>
    #include <dbt.h>
    #include <devguid.h>
    #include <setupapi.h>
    #include <initguid.h>
    #include <list>
    using namespace std;
    
    namespace Ui {
    class Serial_port;
    }
    
    class Serial_port : public QMainWindow
    {
        Q_OBJECT
    
    public:
        explicit Serial_port(QWidget *parent = nullptr);
        void InitPort();
        ~Serial_port();
    
    private slots:
        void readData();    //读取数据
        void timeToSend();  //定时发送
    
        void on_OpenButton_clicked();
    
        void on_r_clearButton_clicked();
    
        void on_s_clearButton_clicked();
    
        void on_sendButton_clicked();
    
        void StringToHex(QString str,QByteArray &senddata); //发送时进制转换
        char convertHexChart(char ch);  //16进制转换
        void readToHex();   //将读取的数据以16进制显示
        void Mdisplay();
        //void pasingData(QByteArray &array); //解析串口数据
    
        void handleLineEditClicked();
    
        void on_Playbutton_clicked();
    
    private:
        Ui::Serial_port *ui;
        QSerialPort *serialPort;
        QTimer *time;
        static   int sendBytes;
        static   int receBytes;
    };
    #endif // SERIAL_PORT_H
    

    Serial_port.cpp

    #include "Serial_port.h"
    #include "ui_Serial_port.h"
    #include <QMessageBox>
    #include "m_lineEdit.h"
    #include <QDebug>
    #include <QFileDialog>
    
    
    Serial_port::Serial_port(QWidget *parent) :
        QMainWindow(parent),
        ui(new Ui::Serial_port)
    {
        ui->setupUi(this);
        time = new QTimer(this);
        InitPort();
        //设置默认值
        ui->PortBox->setCurrentIndex(1);
        ui->BaudBox->setCurrentIndex(7);
        ui->StopBox->setCurrentIndex(0);
        ui->DataBox->setCurrentIndex(3);
        ui->ParityBox->setCurrentIndex(0);
    
        ui->sendButton->setEnabled(false);
        ui->sHexRadio->setEnabled(false);
        ui->sTextRadio->setChecked(true);
        ui->sTextRadio->setEnabled(false);
        ui->rTextRadio->setChecked(true);
        ui->rHexRadio->setChecked(false);
        ui->reDisplay->setChecked(true);
    
        connect(ui->reSendCheck, &QCheckBox::stateChanged,
                this, &Serial_port::timeToSend);         //自动重发
        connect(time, &QTimer::timeout, this, &Serial_port::on_sendButton_clicked);//
    
        connect(ui->rHexRadio, &QRadioButton::toggled, this, &Serial_port::Mdisplay);
    
        connect(ui->m_txt, SIGNAL(clicked()), this, SLOT(handleLineEditClicked()));
    
    
    }
    
    int Serial_port::sendBytes = 0;
    int Serial_port::receBytes = 0;
    
    void Serial_port::InitPort()
    {
        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());
                //qDebug()<<info.portName();
                serial.close();
            }
        }
        QStringList baudList;   //波特率
        QStringList parityList; //校验位
        QStringList dataBitsList;   //数据位
        QStringList stopBitsList;   //停止位
        // 波特率    //波特率默认选择下拉第三项:9600
        baudList<<"1200"<<"2400"<<"4800"<<"9600"
               <<"14400"<<"19200"<<"38400"<<"56000"
              <<"57600"<<"115200";
        ui->BaudBox->addItems(baudList);
        ui->BaudBox->setCurrentIndex(3);
        // 校验      //校验默认选择无
        parityList<<"无"<<"奇"<<"偶";
        ui->ParityBox->addItems(parityList);
        ui->ParityBox->setCurrentIndex(0);
        // 数据位      //数据位默认选择8位
        dataBitsList<<"5"<<"6"<<"7"<<"8";
        ui->DataBox->addItems(dataBitsList);
        ui->DataBox->setCurrentIndex(3);
        // 停止位      //停止位默认选择1位
        stopBitsList<<"1"<<"2";
        ui->StopBox->addItems(stopBitsList);
        ui->StopBox->setCurrentIndex(0);
    }
    
    Serial_port::~Serial_port()
    {
        delete serialPort;
        delete time;
        delete ui;
    }
    
    void Serial_port::readData()
    {
        QByteArray buf;
        if (serialPort){
    
        buf = serialPort->readAll();
    
        if (!buf.isEmpty())
        {
    
            receBytes += buf.size();
            QString redata = QString("received:%1").arg(QString::number(receBytes));
            ui->sendlabel->setText(redata);
            QString myStrTemp = QString::fromLocal8Bit(buf); //支持中文显示
            if(ui->reDisplay->isChecked())
            {
                QString str = ui->textBrowser->toPlainText();
                str +=myStrTemp;
                ui->textBrowser->clear();
                ui->textBrowser->append(str);
            }
        }
        buf.clear();
        }
    
    }
    
    void Serial_port::timeToSend()
    {
        if(ui->reSendCheck->isChecked())
        {
            if(time->isActive())
            {
                return;
            }
            else
            {
                int ms = ui->spinBox->value();
                time->start(ms);
            }
        }
        else
        {
            if(time->isActive())
            {
                time->stop();
            }
            else
            {
                return;
            }
        }
    
    }
    
    void Serial_port::on_OpenButton_clicked()
    {
        if (ui->OpenButton->text() == tr("打开串口"))
        {
            serialPort = new QSerialPort;
    
            serialPort->setPortName(ui->PortBox->currentText());
    
            if(serialPort->open(QIODevice::ReadWrite))
            {
                //qDebug()<<ui->BaudBox->currentIndex();
                switch (ui->BaudBox->currentIndex()) {
                case 0:
                    serialPort->setBaudRate(QSerialPort::Baud1200);
                    break;
                case 1:
                    serialPort->setBaudRate(QSerialPort::Baud2400);
                    break;
                case 2:
                    serialPort->setBaudRate(QSerialPort::Baud4800);
                    break;
                case 3:
                    serialPort->setBaudRate(QSerialPort::Baud9600);
                    break;
                case 4:
                    serialPort->setBaudRate(QSerialPort::Baud19200);
                    break;
                case 5:
                    serialPort->setBaudRate(QSerialPort::Baud38400);
                    break;
                case 6:
                    serialPort->setBaudRate(QSerialPort::Baud57600);
                    break;
                case 7:
                    serialPort->setBaudRate(QSerialPort::Baud115200);
                    break;
                default:
                    break;
                }
    
                switch (ui->StopBox->currentIndex()) {
                case 0:
                    serialPort->setStopBits(QSerialPort::OneStop);
                    break;
                case 1:
                    serialPort->setStopBits(QSerialPort::TwoStop);
                    break;
                default:
                    break;
                }
    
                switch (ui->DataBox->currentIndex()) {
                case 0:
                    serialPort->setDataBits(QSerialPort::Data5);
                    break;
                case 1:
                    serialPort->setDataBits(QSerialPort::Data6);
                    break;
                case 2:
                    serialPort->setDataBits(QSerialPort::Data7);
                    break;
                case 3:
                    serialPort->setDataBits(QSerialPort::Data8);
                    break;
                default:
                    break;
                }
    
                switch (ui->ParityBox->currentIndex()) {
                case 0:
                    serialPort->setParity(QSerialPort::NoParity);
                    break;
                case 1:
                    serialPort->setParity(QSerialPort::OddParity);
                    break;
                case 2:
                    serialPort->setParity(QSerialPort::EvenParity);
                    break;
                default:
                    break;
                }
    
                ui->OpenButton->setText(tr("关闭串口"));
                ui->PortBox->setEnabled(false);
                ui->BaudBox->setEnabled(false);
                ui->StopBox->setEnabled(false);
                ui->DataBox->setEnabled(false);
                ui->ParityBox->setEnabled(false);
                ui->sendButton->setEnabled(true);
                ui->sTextRadio->setEnabled(true);
                ui->sHexRadio->setEnabled(true);
                connect(serialPort, &QSerialPort::readyRead, this, &Serial_port::readData);
                connect(serialPort, &QSerialPort::readyRead, this, &Serial_port::readToHex);
            }
            else
            {
                QMessageBox::critical(this, tr("Error"), serialPort->errorString());
            }
        }
        else
        {
            serialPort->clear();
            serialPort->close();
            serialPort->deleteLater();
    
            ui->sendButton->setEnabled(false);
            ui->OpenButton->setText(tr("打开串口"));
            ui->PortBox->setEnabled(true);
            ui->BaudBox->setEnabled(true);
            ui->StopBox->setEnabled(true);
            ui->DataBox->setEnabled(true);
            ui->ParityBox->setEnabled(true);
            ui->sHexRadio->setEnabled(false);
            ui->sTextRadio->setEnabled(false);
    
        }
    
    }
    
    void Serial_port::on_r_clearButton_clicked()
    {
        ui->textBrowser->clear();
    
    }
    
    void Serial_port::on_s_clearButton_clicked()
    {
        ui->lineEdit->clear();
    }
    
    void Serial_port::on_sendButton_clicked()
    {
        //Latin1是ISO-8859-1的别名,有些环境下写作Latin-1。ISO-8859-1编码是单字节编码,向下兼容ASCII
       //其编码范围是0x00-0xFF,0x00-0x7F之间完全和ASCII一致,0x80-0x9F之间是控制字符,0xA0-0xFF之间是文字符号。
        QString str = ui->lineEdit->text();
    
        if(!str.isEmpty())
        {
            auto isHexSend = ui->sHexRadio->isChecked();
    
            int len = str.length();
            if(len%2 == 1)
            {
                str = str.insert(len-1,'0');
            }
            QByteArray senddata;
            if(isHexSend)
            {
                StringToHex(str,senddata);
                serialPort->write(senddata);
    
                if(serialPort->write(senddata)<0)
                {
                    QMessageBox::critical(this, tr("Error"), serialPort->errorString());
                }
            }
            else
            {
                if(serialPort->write(ui->lineEdit->text().toLocal8Bit())<0)
                {
                    QMessageBox::critical(this, tr("Error"), serialPort->errorString());
                }
            }
    
        }
    
    }
    
    void Serial_port::StringToHex(QString str, QByteArray &senddata)
    {
        int hexdata,lowhexdata;
               int hexdatalen = 0;
               int len = str.length();
               senddata.resize(len/2);
               char lstr,hstr;
               for(int i=0; i<len; )
               {
                   //char lstr,
                   hstr=str[i].toLatin1();
                   if(hstr == ' ')
                   {
                       i++;
                       continue;
                   }
                   i++;
                   if(i >= len)
                       break;
                   lstr = str[i].toLatin1();
                   hexdata = convertHexChart(hstr);
                   lowhexdata = convertHexChart(lstr);
                   if((hexdata == 16) || (lowhexdata == 16))
                       break;
                   else
                       hexdata = hexdata*16+lowhexdata;
                   i++;
                   senddata[hexdatalen] = (char)hexdata;
                   hexdatalen++;
               }
               senddata.resize(hexdatalen);
    
    }
    
    char Serial_port::convertHexChart(char ch)
    {
        if((ch >= '0') && (ch <= '9'))
                    return ch-0x30;  // 0x30 对应 ‘0’
                else if((ch >= 'A') && (ch <= 'F'))
                    return ch-'A'+10;
                else if((ch >= 'a') && (ch <= 'f'))
                    return ch-'a'+10;
        //        else return (-1);
        else return ch-ch;//不在0-f范围内的会发送成0
    
    }
    
    void Serial_port::readToHex()
    {
        QByteArray temp = serialPort->readAll();
        auto isShow = ui->reDisplay->isChecked();         //接收显示?
        QDataStream out(&temp,QIODevice::ReadOnly);    //将字节数组读入
        while(!out.atEnd())
        {
               qint8 outChar = 0;
               out>>outChar;   //每字节填充一次,直到结束
               //十六进制的转换
               QString str = QString("%1").arg(outChar&0xFF,2,16,QLatin1Char('0'));
               if(isShow){
                   ui->textBrowser->insertPlainText(str.toUpper());//大写
                   ui->textBrowser->insertPlainText(" ");//每发送两个字符后添加一个空格
                   ui->textBrowser->moveCursor(QTextCursor::End);
               }
        }
    
    }
    
    void Serial_port::Mdisplay()
    {
        if(ui->rHexRadio->isChecked())
        {
            disconnect(serialPort, &QSerialPort::readyRead, this, &Serial_port::readData);
            connect(serialPort, &QSerialPort::readyRead, this, &Serial_port::readToHex);
        }
        else
        {
            connect(serialPort, &QSerialPort::readyRead, this, &Serial_port::readData);
            disconnect(serialPort, &QSerialPort::readyRead, this, &Serial_port::readToHex);
        }
    
    }
    
    void Serial_port::handleLineEditClicked()
    {
        //QString curPath = QDir::currentPath();
        QString curPath = "../QSerial_port";
        //QFile file;
        QString aFileName = QFileDialog::getOpenFileName(this,QString("选择文件"),curPath,QString("TEXT(*.txt)"));
        ui->m_txt->setText(aFileName);
    
    }
    
    void Serial_port::on_Playbutton_clicked()
    {
        QString path = ui->m_txt->text();
        //qDebug()<<"是否成功获取"<<path_ce->text();
    
        if(path.isEmpty())
            return;
        QFile aFile(path);
        if(!aFile.exists())     //文件不存在
            qDebug() <<"the file not exist!!";
            //return;
        if(!aFile.open(QIODevice::ReadOnly | QIODevice::Text))
            qDebug() <<"文件无法打开";
            //return;
        QTextStream aStream(&aFile);    //用文件流读取文件
        while (!aStream.atEnd())
        {
            QString qstr = aStream.readLine();   //读取文件的一行文本
            //qDebug()<<qstr.length();
            int len = qstr.length();
            if(len%2 == 1)
            {
                qstr = qstr.insert(len-1,'0');
            }
            QByteArray senddata;
            StringToHex(qstr,senddata);
            //qDebug()<<senddata.size();
            for(int i=0;i<senddata.size();i++)
            {
                QByteArray tmpsenddata=senddata.mid(i,40);
                i=i+39;
                //qDebug()<<tmpsenddata.length();
                serialPort->write(tmpsenddata);
    
                if(!serialPort->waitForBytesWritten())   //这一句很关键,决定是否能发送成功
                {
                     qDebug()<<"serial write error";
                }
    
                Sleep(1000);
            }
    
        }
    
    }
    

    最后提示一点,ui中的lineedit为自定义控件,需要提升相应功能,及添加对应类。

    六、虚拟串口工具

    对于程序来说,最方便的测验程序的时候不需要接硬件就能测试,这里介绍给大家一款能够创建虚拟串口的工具Virtual Serial Port Driver,以及串口小助手,当然咱们自己写的就是小助手,为了严重功能确实好用,那也可以下载个网上的,或者运行两个,有关工具的介绍这里就不提了,百度上讲的已经很好了,具体的资源也很容易百度到,这里也不提供了,百度对该工具的使用介绍链接(点击)。

    七、参考文献

    1.文中第一部分,串口通信的简介、和校验解码、串口通信图片及报文示图片来源于CSDN博主「WAmani」的原创文章,链接为:https://blog.csdn.net/wamani/article/details/52849043

    2.文中第二部分,串口通信各参数的含义来源于CSDN博主「guomutian911」的原创文章,链接为:https://blog.csdn.net/guomutian911/article/details/47044603/

    3.文中一些功能的实现来源于CSDN博主「苍雨流痕」的原创文章,链接为https://blog.csdn.net/hellolru/article/details/80838824

    八、资源下载

    点击下载。或者访问链接:https://download.csdn.net/download/zijinmu69/12376049

     

    展开全文
  • 同步通信不像异步通信那样,靠起始位在每个字符数据开始时使发送和接收同步,而是通过同步字符在每个数据块传送开始时使收发双方同步。  同步通信的特点是:  ·以同步字符作为传送的开始,从而使收发同步;  ...
  • [通讯方式] 串口通信

    2022-03-07 16:02:49
    串口通信的重要概念 一. 前言 1、什么是通信? 发送方按照信息编码方式对有效信息进行编码(编成可以在通信线路上传输的信号形态); 编码后的信息在传输介质上进行传输,输送给接收方; 接收...
    • 📢博客主页:https://blog.csdn.net/weixin_43197380
    • 📢欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正!
    • 📢本文由 Loewen丶原创,首发于 CSDN,转载注明出处🙉
    • 📢现在的付出,都会是一种沉淀,只为让你成为更好的人✨

    一. 前言

    1、什么是通信?

    1. 发送方按照信息编码方式对有效信息进行编码(编成可以在通信线路上传输的信号形态);
    2. 编码后的信息在传输介质上进行传输,输送给接收方;
    3. 接收方接到编码信息后进行解码,解码后得到可以理解的有效信息

    可以理解为收发快递:卖家打包快递(编码)—— 快递小哥+运输车(传输介质)将打包好的快递给到买家手中 —— 买家拆开快递(解码),最终得到物品(有效信息)。

    2、同步通信和异步通信

    同步还是异步就看发送方和接收方的时钟频率是否一致:

    • [√] 同步通信:接收端发送端时钟信号频率一致,其发送端发送连续的比特流
    • [×] 异步通信:以字符为单位进行传输,接收端发送端时钟频率不要求同步,字符之间没有固定的时间间隔要求,但接收端必须时刻做好接收的准备(如果接收端主机的电源都没有加上,那么发送端发送字符就没有意义,因为接收端根本无法接收)。

    在这里插入图片描述
    举个例子:大耳朵图图去牛爷爷家

    胡英俊对图图说:

    ①你自己去牛爷爷家吧,我去上班了,到牛爷爷家了给我打一个电话(异步通信)。

    ②胡英俊先送图图去牛爷爷家,确认送到牛爷爷家了之后,再去上班(同步通信)。

    3、电平信号和差分信号

    电平信号和差分信号是用来描述通信线路传输方式的。也就是说如何在通信线路上表示1和0。

    电平信号的传输线中有一个参考电平线(一般是GND),然后信号线上的信号值是由信号线电平和参考电平线的电压差决定。电平信号的2根通信线之间的电平差异容易受到干扰,传输容易失败;

    差分信号的传输线中没有参考电平线,所有都是信号线,然后1和0的表达靠信号线之间的电压差。差分信号不容易受到干扰,因此传输质量比较稳定。现代通信一般都使用差分信号,电平信号几乎没有了。

    在电平信号下,”1根参考电平线+1根信号线“可以传递1位二进制;”1根参考电平线+2根信号线“可以同时发送2位二进制;如果想同时发送8位二进制就需要9根线。
    在差分信号下,2根线(彼此差分)可以同时发送1位二进制;如果需要同时发送8位二进制就需要16根线。

    总结: 在相同根数的通信线下, 表面上电平信号要比差分信号快;但是实际还是差分信号快,因为差分信号抗干扰能力强, 因此1个发送周期的时间耗时更少.

    4、串行接口和并行接口

    串行端口被描述为通道,而并行端口则表示有8条通道可同时传输8位(一个字节)的数据。

    在这里插入图片描述

    串行接口:
    串行接口,简称串口,也就是COM接口,是采用串行通信协议的扩展接口,数据传输率是115kbps~230kbps。

    串行接口是指数据是一位位地顺序传送出去的,其特点是通信线路简单,只要一对传输线就可以实现双向通信,并可以利用电话线。降低成本,适用于远距离通信,但传送速度慢

    并行接口:
    并行接口,简称并口,也就是LPT接口,是采用并行通信协议的扩展接口。并口的数据传输率比串口快8倍,标准并口的数据传输率为1Mbps。

    并行接口是指数据通过多条数据线同时传输出去,其特点是传输速度快(1Mbps),但当传输距离远、位数又多时,导致通信线路复杂且成本提高

    速度差异:
    ​通俗点讲,串口就像只有一条车道,而并口就是有8个车道,同一时刻能传送8位(一个字节数据)。但是并不是并口快,由于8位通道之间的互相干扰。传输速度就受到了限制。而且传输出错时,要同时重新传送8个位的数据。串口没有干扰,传输出错后重发以为就可以了。所以串口比并口快。串口硬盘就是这样被重视的,没有谁希望自己的机箱里出现一根胳膊粗的线束。

    历史上,工程师们确实是先做了串口,速度不够没办法只好含泪加电线上并口,直到他们发现了三大法宝(差分信号,时钟-数据恢复,和信道均一化)来提速,并口的动力就不那么强劲了。摘自:知乎:为什么串口比并口快?

    总结: 经过这么多年的发展,最终胜出的是: 异步、差分、串行, 譬如USB和网络通信。


    二. 串口通信的基本概念

    1、串口通信特点(异步、差分、串行通信)

    (1)异步

    串口通信的发送方和接收方之间没有统一的时钟信号,在异步通信中,收发双方取得同步是通过在字符格式中设置起始位和停止位的方法来实现,具体来说就是,在一个有效字符正式发送之前,发送器先发送一个起始位,然后在发送有效字符位,在字符结束时在发送一个停止位。

    (2)差分信号

    串口通信出现的时间较早、速率较低、传输的距离较近,所有干扰不太明显,因此当时使用了电平信号传输。后期出现的传输协议都改成了差分信号传输了。

    (3)串行通信

    将数据按位(bit)依次传输, 每位数据占据固定的时间长度,即可使用少数几条通信线路就可以完成系统间交换信息

    ps:注意区分串行通信和串口通信:

    • 串行通信是通信双方按位(bit)进行,遵守时序的一种通讯方式;
    • 串口通信是一种通信手段,对标于以太网方式、红外方式、蓝牙方式等通信手段而言。

    常用的串口接头有两种,一种是9针串口(简称DB-9),一种是25针串口(简称DB-25)。每种接头都有公头和母头之分,其中带针状的接头是公头,而带孔状的接头是母头。9针串口的外观如图2所示。

    在这里插入图片描述
    在9针串口接头中,公头和母头的管脚定义顺序是不一样,这一点需要特别注意。那么,这些管脚都有什么作用呢?9针串口和25针串口常用管脚的功能说明如图所示:
    在这里插入图片描述

    典型的串口通信使用3根线完成,分别是地线、发送、接收。由于串口通信是异步的,所以端口能够在一根线上发送数据同时在另一根线上接收数据。串口通信最重要的参数是波特率、数据位、停止位和奇偶的校验。对于两个需要进行串口通信的端口,这些参数必须匹配,这也是能够实现串口通讯的前提。

    2、波特率

    波特率指的是串口通信的速率,也就是串口通信时每秒钟可以传输的数据位数(多少个二进制位 / 秒),譬如,每秒钟可以传输9600个二进制位(传输一个二进制位需要的时间是1/9600秒,也就是104us),波特率就是9600。

    波特率不能随意指定,主要是因为:通信双方必须事先设定相同的波特率才能成功通信,如果发送方和接收方按照不同的波特率通信则根本收不到,因此波特率最好是大家熟知的而不是随意指定的;常用的波特率经过长久发展,就形成了共识,大家常用的就是9600或者115200。

    可以将波特率比喻为两个特工的联络暗号,只有联络暗号正确,才能进行后续信息的传送。


    3、起始位、数据位、奇偶校验位、停止位

    串口通信时,收发是一个周期一个周期进行的,每个周期传输n个二进制位。这一个周期就叫做一个通信单元,一个通信单元由:起始位+数据位+奇偶校验位+停止位组成的。

    在这里插入图片描述
    起始位: 就是数据开始的标志,由0开始,即低电平开始。

    数据位: 是一个通信单元中发送的有效信息位,是本次通信真正要发送的有效数据,串口通信一次发送多少位有效数据是可以设定的(可以是5,7,8位三种,通常选择8位数据位,因为一般通过串口发送的文字信息都是ASCII码编码,而ASCII码中一个字符刚好编码为8位)。

    奇偶校验位: 是用来校验数据传输过程中的数据位,以防止数据位出错的。

    停止位: 是发送方用来表示本通信单元结束标志的,停止位的定义是串口通信标准事先指定的,是由通信线上的电平变化来反映的。常见的有1位停止位、1.5位停止位、2位停止位等,一般使用的是1位停止位。

    总结:

    串口通信时因为是异步通信,所以通信双方必须事先约定好通信参数,这些通信参数包括:波特率、数据位、校验位、停止位(串口通信中起始位定义是唯一的,所以一般不用选择)。

    4、单工、半双工和全双工

    在通信中,相信大家也经常听说单工模式和双工模式,这是不同的通讯方式:

    • 单工模式:单方向收发数据,固定一方为发送端,一方为接收端。譬如,只能“A发送数据,B接收数据”。

    • 半双工模式(RS485):双方分时收发数据,譬如,“A发送数据,B接收数据”或者“A接收数据,B发送数据”,两个方向不能同时进行。

    • 全双功模式(RS232):双方同时收发数据,譬如,“A发送数据,B接收数据”同时“A接收数据,B发送数据”,两个方向同时进行。

    总结:

    半单工模式(RS485) 既可以使用一根数据线,又可以使用两根数据线,但是在切换数据传输方向时传输会有一些延时,故信息传输效率慢一些。全双工是两个单工通信的结合,参数一样的情况下,传输速度要比半双工高一些

    展开全文
  • 串口通信编程

    千次阅读 2021-07-18 20:23:45
    串口通信学习笔记

    一、串口通信基础知识

    1.1 串行通信与并行通信

    并行通信:在一些联络信号的控制下,一次将8位、16位或32位数据同时进行传送的通信方式。特点:通信速率高,但成本高,易受干扰。
    串行通信:只需一对传输线,数据的各位按照时间顺序一位一位地传送。特点:通信速率慢,但通信成本低,适合远距离通信。

    在这里插入图片描述

    1.2 串行通信的特点

    串行通信由于在一条传输线上既传输数据信息,又传输控制联络信息,这就需要一系列约定,从而识别条线上传送的信息流,哪一部分是数据信号,哪一部分是联络信号。

    串行通信的信息格式有异步和同步信息格式。与此对应,有异步串行通信和同步串行通信两种方式。

    1.2.1 异步传输

    在异步传输方式中,帧是数据传输单位。在通信的数据流中,帧之间异步,帧内部各位间同步。异步通信方式的“异步”主要体现在帧与帧之间通信没有严格的定时要求。在异步传输中,帧可以是连续地、一个个地发送,也可以是不连续地、随机地单独发送。
    请添加图片描述

    1.2.2 同步传输

    同步通信中,数据开始传送前用同步字符来指示(常约定1~2个),并由时钟来实现发送端和接收端的同步,即检测到规定的同步字符后,下面就连续按顺序传送数据,直到一块数据传送完毕。同步传送时,字符之间没有间隙,也不要起始位和停止位,仅在数据开始时用同步字符SYNC来指示。
    在这里插入图片描述

    1.2.3 硬件握手和软件握手

    握手信号实际上是控制信号,用来控制数据的传输。通过握手信号,发送端可以得知接收端是否有数据要发送。接收端通过握手信号通知发送端它是否已经准备好了接收信号。握手信号遵循某种协议。

    当发送端和接收端处理数据的速度不一样时,可能会造成数据丢失。在传输中,如果发送端的发送速度大于接收端的接收速度,同时若接收端处理数据的速度不够快的话,那么接收端的缓冲区必定在一定时间后溢出,从而造成以后发送过来的数据不能进入缓冲区而丢失。发送端何时可以继续发送数据,何时必须暂停发送,从而让接收端有时间处理数据,称为流量控制,必须靠握手信号来解决这个问题。

    1. 硬件握手

      在硬件握手中,发送端通过将某一个导线拉到高电平或者低电平,来表示发送端可以发送数据。接收端已经准备好接收数据之后,也把某一个导线拉到高电平或者是低电平,来通知发送端,发送端一直在检测这个信号。接收端可以在任何时候把这个信号变为无效,甚至是在接收一个数据块过程中。当发送端检测到这个信号变为无效之后,就必须停止本次发送,直到这个信号变为有效为止。

    2. 软件握手

      在软件握手中,以数据线上的数据信号来代替实际的硬件电路。这种方法用在直接连接或者通过调制解调器连接的两台计算机之间进行双向通信的场合。
      对于软件握手现在已经建立了一些标准协议,其中最常用的是通信协议。通信协议是指通信双方的一种约定,约定包括对数据格式、同步方式、传输速度、传输步骤、检/纠错方式以及控制字符定义等问题进行统一规定,通信双方必须共同遵守,也叫做通信控制规程或称传输控制规程,它属于OSI七层参考模型中的数据链路层。

    1.3 串行通信基本参数

    在传输进行的过程中,双方明确传输信息的具体方式,否则双方就会没有一套共同的译码方式,从而无法了解对方所传输过来的信息的意义。因此双方为了进行通信,必须遵守一定的通信规则,这个共同的规则就是串口的初始化。

    串口的初始化必须对以下几项参数进行设置。

    1.3.1 数据传输速度

    串行通信的传输受到通信双方配备性能及通信线路的特性所控制,收发双方必须按照同样的速率进行串行通信,即收发双方采用同样的波特率。我们通常将传输速度称为波特率,指的是串行通信中每秒所传输的数据位数,单位是b/s。我们经常可以看到仪器或Modem的规格书上都写着19200 b/s、38 400 b/s,…,所指的就是传输速度。

    1.3.2 起始位与停止位

    由于异步串行传输中并没有使用同步脉冲作为基准,故接收端完全不知道发送端何时将进行数据的传输。发送端准备要开始传输数据时,发送端会在所送出的字符前后分别加上高电位的起始位(逻辑0)及低电位的停止位(逻辑1),它们分别是所谓的起始位和停止位,也就是说,当发送端要开始传输数据时,便将传输线上的电位由低电位提升至高电位,而当传输结束后,再将电位降至低电位。接收端会因起始位的触发(因电压由低电位升至高电位)而开始接收数据;并因停止位的通知(因电压维持在低电位)而明确数据的字符信号已经结束;加入了起始位及停止位才能比较容易达到多字符的接收能力。起始位固定为1位,而停止位则有1、1.5、2位等多种选择,如何选择呢?只要通信双方协议通过即可,没有强制规定。

    1.3.3 校验位

    为了预防错误的产生,使用校验位作为检查的机制。校验位是用来检查所传输数据的正确性的一种核对码,又可分成奇校验与偶校验两种,分别是检查字符码中1的数目是奇数或偶数。以偶校验为例,“A"的ASCI码是41H(十六进制),将它以二进制表示时,是01000001,其中1的数目是2,因此校验位便是0,使1的数目保持偶数;同样地,校验位是奇校验时,“A”的校验位便是1,使1的数目保持在奇数。接收端重新计算奇偶校验位,如果新的计算值正确,那么表示正常。如果新的计算值错误,那么接收端就会发出一些指示,表示此次接收的数据有误。

    1.4 串行通信的传输方式

    1.4.1 信号传输方式

    • 近距离传输时,采用按信号原样传输的基波传输。
    • 远距离传输时,使用Modem将原信号调制为高频的模拟仿号,然后通过电话网络,进行远距离传输。

    1.4.2 线路传输方式

    • 单工方式:信息只能沿一个方向传输
    • 半双工方式:同一时刻,信息只能沿一个方向传输
    • 全双工方式:通信双方都能在同一时刻发送和接受

    1.5 串口通信及其标准

    1.5.1 RS-232C标准

    RS-232C是PC与通信工业中应用最广泛的一种串行接口。它适合于数据传输速度在 0~20000 b/s范围内的通信。这个标准对串行通信接口的有关问题,如信号电平、信号线功能、电气特性、机械特性等都作了明确的规定。

    由于RS-232C并未定义连接器的物理特性,因此,出现了 DB-25 和 DB-9 各种类型的连接器,其引脚的定义也各不相同。现在计算机上一般只提供 DB-9 连接器,都为公头。相应的连接线上的串口连接器也有公头和母头之分,见下图。请添加图片描述
    从功能上看,全部信号线从功能来看,全部信号线分为三类,即数据线(TXD,RXD)、地线(GND)和联络控制线(DSR,DTR,RI,DCD,RTS,CTS)

    当两台RS-232串口设备通信距离较近时(<15m),可以用电缆线直接将2台设备的RS-232端口连接,若通信距离较远(>15m)时,需附加调制解调器(Modem)。

    1.5.2 RS-422标准

    RS-422由RS-232发展而来,它是为了弥补RS-232的不足而提出的。为了改进RS-232抗干扰能力差、通信距离短、传输速度低等缺点,RS-422定义了一种平衡通信接口,将传输速度提高到10 Mb/s,速率低于100 kb/s时传输距离延长到4000英尺(1英尺=0.3048 m),并允许在一条平衡总线上连接最多10个接收器。RS-422是一种单机发送、多机接收的单向、平衡传输规范,被命名为TIA/EIA-428-A标准。

    1.5.3 RS-488标准

    为扩展应用范围,EIA又于1983年在RS-422基础上制定了RS-485标准,增加了多点、双向通信能力,即允许多个发送器连接到同一条总线上,同时增加了发送器的驱动能力和冲突保护特性,扩展了总线共模范围,后命名为TIA/EIA-488-A标准。由于EIA提出的建议标准都是以“RS”作为前缀的,所以在通信工业领域,仍然习惯将上述标准以RS作为前缀。

    1.5.4 参数比较

    RS-232,RS-422与RS-485标准只对接口的电气特性做出规定,而不涉及接插件、电缆或协议,在此基础上用户可以建立自己的高层通信协议。有关电气参数见下表。
    在这里插入图片描述

    1.6 个人计算机中的串口

    1.6.1 观察计算机上串口位置和几何特征

    在PC主机箱后面板上,有各种各样的接口,其中有个9针的接头区,这就是RS-232C串口。PC上的串行接口有多个名称:RS-232口、串口、通信口、COM口、异步口等。

    1.6.2 查看串口设备信息

    打开“设备管理器”。在列表中有端口COM和LPT设备信息,如图所示。
    在这里插入图片描述
    右键单击“通信端口(COM1)”,选择属性,在这里可以查看端口的低级设置,也可查看其资源。在“端口设置”选项卡中,可以看到缺省的波特率和其他设置,这些设置可以在这里改变,也可以在应用程序中很方便地修改。在“资源”选项卡中,可以看到,COM1口的地址分配信息(03F8-03FF)和中断请求号(04),如图所示。
    请添加图片描述

    二、Windows串口编程

    Windows API是由操作系统所提供的函数,这些函数可以为程序设计人员提供相当多的执行功能。Windows的API包括了串口通信的函数,因而在串口通信中使用调用API接口的方法,是实现在C中直接控制串口硬件的简便可行的解决方案。

    2.1 动态链接库与API函数

    Windows是多任务操作系统。在多任务环境中,应用程序共享内存资源,如果多个应用程序都调用库文件中相同的函数,则在链接时把该函数复制给每个应用程序,运行时在内存中生成同一函数的多个复制,造成内存资源的浪费;此外,如果修改了库中函数的代码,则必须对调用该函数的应用程序重新进行链接。因此,在Windows环境下,通常使用动态链接库(Dynamic Link Libaray,DLL)。动态链接库是一个函数库,是可被其他程序或DLL调用的函数集合组成的可执行文件模块。之所以被称为动态链接库,是因为DLL的代码并不是某个应用程序的组成部分,而是在运行时链接到应用程序中。

    动态链接分为两个阶段,即链接过程和装入过程。

    当应用程序调用动态链接库中的某个函数时,链接程序并不复制被调用函数的代码,而只是从引入库中复制一些指示信息,指出被调用函数属于哪个动态链接库(.DLL文件)。因此,在应用程序的可执行文件中,存放的不是被调用的函数的代码,而是DLL中该函数的内存地址。程序运行后,当需要调用该函数时,进入装入过程,把应用程序与DLL库一起装入内存,由Windows读入DLL中的函数并运行程序。

    可以看出,动态链接是在应用程序被装入到内存中进行的。这样,当多个应用程序调用库中的同一个函数时,不会在内存中有该函数的多个复制,而是只有一分复制,每个应用程序的可执行文件中装入的只是该函数的内存地址,程序运行时再把应用程序代码与被调用函数代码动态链接起来,从而可以节省内存资源。同时,由于DLL与应用程序分开,即使更新DLL,也不用修改已编译好的可执行文件。

    常用的动态链接库是安装在Windows目录下,随Windows软件包提供的DLL。这些动态链接库中所包含的函数就称为Windows API函数。

    2.2 与串口通信有关的API函数

    虽然串口是属于硬件层面,可Windows却将它当成一个文件来操作,因此打开串口时,就必须像打开文件那样操作。在使用完串口时,必须使用关闭文件的函数将串口关闭。

    与串口通信有关的API函数均在\Window\system32子目录下的USER.EXE动态链接库中,API中与串行通信相关的函数约有20个,本节只讨论经常使用的函数。

    2.2.1 打开串口

    Win32 中用于打开串口的API 函数为CreateFile,其原型为:

    HANDLE CreateFile (
    	LPCTSTR lpFileName, //将要打开的串口逻辑名,如COM1 或COM2
    	DWORD dwAccess, //指定串口访问的类型,可以是读取、写入或两者并列
    	DWORD dwShareMode, //指定共享属性,由于串口不能共享,该参数必须置为0
    	LPSECURITY_ATTRIBUTES lpsa, //引用安全性属性结构,缺省值为NULL
    	DWORD dwCreate, //创建标志,对串口操作该参数必须置为OPEN_EXISTING
    	DWORD dwAttrsAndFlags, //属性描述,用于指定该串口是否可进行异步操作,FILE_FLAG_OVERLAPPED:可使用异步的I/O
    	HANDLE hTemplateFile //指向模板文件的句柄,对串口而言该参数必须置为NULL
    );
    

    例如,以下程序用于以同步读写方式打开串口COM1:

    HANDLE hCom;
    DWORD dwError;
    hCom = CreateFile("COM1", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
    if (hCom == (HANDLE)0xFFFFFFFF)
    {
    	dwError = GetLastError();
    	MessageBox(dwError);
    }
    

    2.2.2 关闭串口

    利用API 函数实现串口通信时关闭串口非常简单,只需使用CreateFile 函数返回的句柄作为参数调用CloseHandle 即可:

    BOOL CloseHandle(
    	HANDLE hObject // handle to object to close
    );
    

    2.2.3 配置串口

    串口通信中,使用该数据结构存放通信参数。由于该结构包括的通信参数很多,逐个设置不方便。通常的做法是首先将串口的当前设置值读出到一个DCB结构中,然后根据需要修改该DCB结构中的某些参数,再以该结构来设置串口通信参数。

    接收缓冲区和发送缓冲区的大小可通过SetupComm 函数来设置。

    typedef struct _DCB { // dcb
    	DWORD DCBlength; // DCB结构的大小
    	DWORD BaudRate; // 通信波特率
    	DWORD fBinary: 1; // 是否允许二进制方式通信。只能设为TRUE,即使用二进制方式通信。因为WinAPI只支持二进制方式通信。若设为FALSE则通信不能正常工作。
    	DWORD fParity: 1; // 是否允许奇偶校验。若设为TRUE,则通信时执行奇偶校验检测,并且当出现奇偶校验错时,将会产生错误信息。
    	DWORD fOutxCtsFlow:1; // 指定串口输出数据时,是否使用CTS作为流控制信号。
    	DWORD fOutxDsrFlow:1; // 指定串口输出数据时,是否使用DSR作为流控制信号。
    	DWORD fDtrControl:2; // 指定DTR流控制信号的工作方式。可取的值:DTR_CONTROL_DISABLE,禁止DTR信号线,并保持禁止状态;DTR_CONTROL_ENABLE,允许DTR信号线,并保持允许状态;DTR_CONTROL_HANDSHAKE,允许DTR握手。此时不允许在程序中使用EscapeCommFunction()函数调整DTR信号线的状态。
    	DWORD fDsrSensitivity:1; // 指定通信驱动程序对DSR信号是否敏感。如果设为TRUE,则当DSR线为无效状态时,串口接收的任何数据将被忽略。
    	DWORD fTXContinueOnXoff:1; // 指定当接收缓冲区已满,并且驱动程序已经发出XoffChar字符时,发送是否停止。如果为FALSE则停止。fOutX:指定在发送串口数据时是否使用Xon/Xoff流控制。当该成员设为TRUE时,则率门在收到XofChar时传输将停止,直到接收到XonChar时,发送操作才继续进行。
    	DWORD fOutX: 1; // 指定在发送串口数据时是否使用Xon/Xoff流控制。当该成员设为TRUE时,则串口在收到XoffChar时传输将停止,直到接收到XonChar时,发送操作才继续进行。
    	DWORD fInX: 1; // finX:指定在接收审口数据时是否使用Xon/Xoff流控制。当该成员设为TRUE时,则在接收缓冲区中数据长度达到接收界限值(接收缓冲区长度与XofLim之差)时,驱动程序将发送XofrChar,以通知发送方停止发送数据。直到接收缓冲区中数据长度小于XonLim时,驱动程序才发送XonChar,以通知发送方可以继续发送数据。
    	DWORD fErrorChar: 1; // 当数据发生奇偶校验错时,指定是否用ErrorChar成员值替换该错误数据。若该成员值设为TRUE,则当fParity设为TRUE并且发生奇偶校验错时,将进行此替换操作。
    	DWORD fNull: 1; // 指定是否忽略接收数据中的空字符(ASCI1码值为0)。该成员值设为TRUE,则执行忽略的操作。
    	DWORD fRtsControl:2; // 指定RTS流控制方式。
    	DWORD fAbortOnError:1; // 指定当发生错误时,是否终止发送和接收操作。若设为TRUE,则发生通信错误时,将终止所有的发送和接收操作。此时,驱动程序不再响应任何通信操作。直到程序调用了ClearCommError()函数响应该错误,通信才恢复正常。
    	DWORD fDummy2:17; // 系统保留不使用
    	WORD wReserved; // 不使用,设为0
    	WORD XonLim; // 指定在发出Xon字符前,接收缓冲区中允许的最小字符数量。
    	WORD XoffLim; // 指定在发出Xoff字符前,接收缓冲区中允许的最大字符数量。该最大字符数量为接收缓冲区长度与XoffLim之差。
    	BYTE ByteSize; // 指定通信数据位数, 4-8
    	BYTE Parity; // 指定奇偶校验方式
    	BYTE StopBits; // 指定停止位数
    
    	char XonChar; // 指定通信时发送XON字符使用的字符值(ASCII码值)。
    	char XoffChar; // 指定通信时发送XOFF字符使用的字符值(ASCII码值)。
    	char ErrorChar; // 指定通信发生奇偶校验错误时,用于替换错误数据的字符值。
    	char EofChar; // 指定表示传输结束的字符值
    	char EvtChar; // 指定事件字符。当传输中出现该宁符时,将产生一个事件。
    	WORD wReserved1; // 系统保留不使用
    } DCB;
    

    而SetupComm 函数的原型则为:

    BOOL SetupComm(
    	HANDLE hFile, // 指向一个串口设备的句柄,该句柄由CreateFile()函数返回
    	DWORD dwInQueue, // 指示输入缓冲区大小,以字节为单位
    	DWORD dwOutQueue // 指示输出缓冲区大小,以字节为单位
    );
    

    要设置串行通信参数,首先定义一个DCB结构类型的变量。根据通信要求,设置该结构变量的数值,然后以该结构变量作为参数,执行SetCommState() 函数即可完成串口通信参数的设置。

    BOOL Setcomnstate(
    HANDLE hFile, // 指向要设置参数的串口句柄。该句柄通常是CreateFile 函数的返回值。
    LPDCB IpDCB ); //指向DCB结构变量的指针。该结构变量中存放着通信参数设置的具体数值。
    

    以下程序将串口设置为:波特率为9600,数据位数为7 位,停止位为2 位,偶校验,接收缓冲区和发送缓冲区大小均为1024 个字节,最后用PurgeComm 函数终止所有的后台读写操作并清空接收缓冲区和发送缓冲区:

    DCB dcb;
    dcb.BaudRate = 9600; //波特率为9600
    dcb.ByteSize = 7; //数据位数为7 位
    dcb.Parity = EVENPARITY; //偶校验
    dcb.StopBits = 2; //两个停止位
    dcb.fBinary = TRUE;
    dcb.fParity = TRUE;
    if (!SetCommState(hCom, &dcb))
    {
    	MessageBox("串口设置出错!");
    }
    SetupComm(hCom, 1024, 1024);
    PurgeComm(hCom, PURCE_TXABORT | PURGE_RXABORT | PURGE_TXCLEAR | PURGE_RXCLEAR);
    

    2.2.4 超时设置

    超时设置是通过改变COMMTIMEOUTS 结构体的成员变量值来实现的,COMMTIMEOUTS 的原型为:

    typedef struct _COMMTIMEOUTS
    {
    	DWORD ReadIntervalTimeout; //定义两个字符到达的最大时间间隔,单位:毫秒
    	//当读取完一个字符后,超过了ReadIntervalTimeout,仍未读取到下一个字符,就会发生超时
    	
    	DWORD ReadTotalTimeoutMultiplier;
    	DWORD ReadTotalTimeoutConstant;
    	//其中各时间所满足的关系如下:
    	//ReadTotalTimeout = ReadTotalTimeOutMultiplier* BytesToRead + ReadTotalTimeoutConstant
    	DWORD WriteTotalTimeoutMultiplier;
    	DWORD WriteTotalTimeoutConstant;
    	
    } COMMTIMEOUTS, *LPCOMMTIMEOUTS;
    

    设置超时的函数为SetCommTimeouts,其原型中接收COMMTIMEOUTS 的指针为参数:

    BOOL SetCommTimeouts(
    	HANDLE hFile, // handle to communications device
    	LPCOMMTIMEOUTS lpCommTimeouts // pointer to comm time-out structure
    );
    

    以下程序将串口读操作的超时设定为10 毫秒:

    COMMTIMEOUTS to;
    memset(&to, 0, sizeof(to));
    to.ReadIntervalTimeout = 10;
    SetCommTimeouts(hCom, &to);
    

    2.2.5 事件设置

    在读写串口之前,需要用SetCommMask ()函数设置事件掩模来监视指定通信端口上的事件,其原型为:

    BOOL SetCommMask(
    	HANDLE hFile, //标识通信端口的句柄
    	DWORD dwEvtMask //能够使能的通信事件
    );
    

    有了Set 当然还会有Get,与SetCommMask 对应的GetCommMask()函数的原型为:

    BOOL GetCommMask(
    	HANDLE hFile, //标识通信端口的句柄
    	LPDWORD lpEvtMask // address of variable to get event mask
    );
    

    串口上可以发生的事件可以是如下事件列表中的一个或任意组合:EV_BREAK、EV_CTS、EV_DSR、EV_ERR、EV_RING、EV_RLSD、EV_RXCHAR、EV_RXFLAG、EV_TXEMPTY。

    我们可以用WaitCommEvent()函数来等待串口上我们利用SetCommMask ()函数设置的事件:

    BOOL WaitCommEvent(
    	HANDLE hFile, //标识通信端口的句柄
    	LPDWORD lpEvtMask, // address of variable for event that occurred
    	LPOVERLAPPED lpOverlapped, // address of overlapped structure
    );
    

    WaitCommEvent()函数一直阻塞,直到串口上发生我们用所SetCommMask ()函数设置的通信事件为止。一般而言,当WaitCommEvent()返回时,程序员可以由分析*lpEvtMask 而获得发生事件的类别,再进行相应的处理。

    2.2.6 读串口

    对串口进行读取所用的函数和对文件进行读取所用的函数相同,读函数原型如下:

    BOOL ReadFile(
    	HANDLE hFile, // 指向串口句柄。该句柄由CreateFile()函数返回。
    	LPVOID lpBuffer, // 指向某个缓冲区的指针。该缓冲区存放接收到的二进制数据。
    	DWORD nNumberOfBytesToRead, // 要接收的字节数。
    	LPDWORD lpNumberOfBytesRead, // 实际接收到的字节数。
    	LPOVERLAPPED lpOverlapped // 指向重叠结构变量的指针。当使用重叠操作时,必须设置该成员,否则必须设为NULL
    );
    

    2.2.7 写串口

    对串口进行写入所用的函数和对文件进行写入所用的函数相同,写函数原型如下:

    BOOL WriteFile(
    	HANDLE hFile, // 指向串口句柄。该句柄由CreateFile 函数返回。
    	LPCVOID lpBuffer, // 指向某个缓冲区的指针。该缓冲区存放待发送的二进制数据。
    	DWORD nNumberOfBytesToWrite, // 要发送的字节数。
    	LPDWORD lpNumberOfBytesWritten, // 实际发送了的字节数。
    	LPOVERLAPPED lpOverlapped // 指向重叠结构变量的指针。当使用重叠操作时,必须设置该成员,否则,必须设为NULL。
    );
    

    参考资料:

    1. 曹卫杉 . C/C++串口通信典型应用实例编程实践 . 电子工业出版社 , 2009
    2. 深入浅出VC++串口编程
    3. 龚建伟 , 熊光明 . Visual C++/Turbo C串口通信编程实践 . 电子工业出版社, 2004
    4. 张筠莉 , 刘书智 . VISUAL C++实践与提高:串口通信与工程应用篇 . 中国铁道出版社
    5. 梦小羊博客:串口通信知识总结
    展开全文
  • UART串口通信

    千次阅读 2021-11-10 15:35:21
    串行接口简称“串口”,即采用串行通信方式的接口。 串行通信将数据字节分成一位一位的形式,在一条数据线上逐个传送。 串行通信的特点:通信线路简单,但传输速度较慢。 因此,串行接口广泛应用于对数据传输速度...
  • 目录:一、通信相关知识二、STM32串口三、常用串口寄存器四、串口库函数配置 一、通信相关知识   在计算机设备与设备之间或集成电路之间常常需要进行数据传输(通信)。 1.按数据传送的方式,通信可分为串行通信与...
  •  对于异步串行通信,只有在通信双方波特率相同时,才能实现数据的正确传输与接收;而一些系统总是希望能实现对各种波特率的兼容。通常的实现方法是,要求对方首先发出规定的字符或数据,系统收到该字符或数据后,...
  • 串口意思是串行通信接口,与之对应的是并行通信接口。下面具体来看下什么是串行通信和并行通信串行通信,就是数据是按时间先后顺序一串串发出去的。比如现在要发一个13(对应二进制是1101)的数据,那第一个时钟...
  • 同步通信复杂,双方时钟允许误差较小;异步通信简单,双方时钟可允许一定误差。 同步通信用于点对多点,异步通信只适用于点对点 2、电平信号和差分信号 2.1电平信号和差分信号的概念 电平信号和差分信号是用来描述...
  • ZigBee串口通信

    千次阅读 2021-05-27 17:54:43
    一、串口通信原理 1.什么是UART ​ UART : Universal Asynchronous Receiver/Transmitter 通用异步收发器 ​ 一种常用也是最简单的串行数据传输协议。数据线只需要两根就可以实现全双工。 ​ Tx:发送数据线 ​ Rx:...
  •  关键词:单片机 通用异步收发器(UART) 串口扩展 引 言 1 概 述  SCC2619是Philips公司推出的高集成、低能耗的全双工通用异步收发器UART。该芯片的接收与发送速度可以分别定义,接收器采用三倍缓冲方式,在...
  • 协议层主要规定通讯逻辑,统一收发双方的数据打包、解包标准。 2.协议层 串口通讯的数据包由发送设备通过自身的 TXD 接口传输到接收设备的 RXD 接口。在串口通讯的协议层中,规定了数据包的内容,它由启始位、主体...
  • 51单片机串口通信详解

    千次阅读 2021-05-06 11:05:13
    文章目录前言一、计算机通信简介二、串口通信简介1、简介2、同步通信和异步通信2.1 同步通信2.2 异步通信3、串行通信的传输方式4、串口通信硬件电路5、常见接口介绍三、串口相关寄存器详解1、特殊功能寄存器SCON2、...
  • STM32——串口通信及实验

    千次阅读 2022-01-03 21:47:24
    一般情况下,设备之间的通信方式可以分成并行通信串行通信两种。它们的区别是: 串行通信的分类 1、按照数据传送的方向,分为: 单工:数据传输只支持数据在一个方向上传输 半双工:允许数据在两个方向上传输...
  • 一.基本知识 1.串行通信: 串行通信是指通信双方按位进行,遵守时序的一种通信方式。串行通信中,将数据按位依次传输, 每位数据占据固定的...3.串行通信和串口通信的区别: 串行通信是指一比特一比特的收发数据,包
  • 串口通信基础知识(UART)

    千次阅读 2021-07-20 11:28:51
    一、首先是对于串口通信的具体分类: ​​​​​ 总结一下: 串口第一分类为并行通信和串行通信,而由于串行通信的优点以及对缺点的弥补,导致如今基本都采用串行通信; 对于串行通信,又按照是否有同步时钟和...
  • 浅析同步通信与异步通信

    千次阅读 2020-06-08 19:04:05
    同步通信: 发送端在发送串行数据的同时,提供一个时钟信号,并按照一定的约定(例如:在时钟信号的上升沿的时候,将数据发送出去)发送数据,接收端根据发送端提供的时钟信号,以及大家的约定,接收数据。...
  • 本文将从通信原理的角度,解析RS232串口通信过程中的每个环节,包括硬件和软件,在信源和信宿之间,实现离散的字符串的传送。 目录: 一. 什么是RS232通信 二. RS232实现离散字符串通信的基本需求框架 三. RS232...
  • 异步通信和同步通信

    2016-04-28 19:20:59
    异步通信“异步通信”是一种很常用的通信方式。异步通信在发送字符时,所发送的字符之间的时间间隔可以是任意的。当然,接收端必须时刻做好接收的准备(如果接收端主机的电源都没有加上,那么发送端发送字符就没有...
  • FPGA串口通信实验

    千次阅读 2021-03-07 08:41:51
    目录串口简介串口通信简介同步通信异步通信串口接头协议标准电气特性时序介绍设计思想 串口通信是FPGA较为基础的一个实验,本人在初学FPGA后决定将其整理一下并进一步加强自身理解。 串口简介 串口也称串行通信接口...
  • 单片机串口通信与同步异步通信

    千次阅读 2021-03-08 20:42:40
    文章目录一、串口通信二、异步通信三、同步通信四、串行通信的传输方向 一、串口通信 1、随着多微机系统的广泛应用和计算机网络技术的普及,计算机的通信功能愈来愈显得重要。计算机通信是指计算机与外部设备或...
  • 串口通信详解

    千次阅读 2021-10-09 13:23:56
    串口通信(Serial Communications)的概念非常简单,串口按位(bit)发送和接收字节。尽管比按字节(byte)的并行通信慢,但是串口可以在使用一根线发送数据的同时用另一根线接收数据。它很简单并且能够实现远距离通信。...
  • 串口通信协议

    千次阅读 2020-11-29 05:37:46
    串口通信(serial communication)是一种设备间非常常用的串行通信方式,大部分电子设备都支持,电子工程师再调试设备时也经常使用该通信方式输出调试信息。 2.讲到某一种通信协议,离不开的就是物理层,物理层主要...
  • 文章目录前言一、串口通信协议?二、使用步骤1.引入库2.读入数据总结 前言 之前的文章中介绍了与串口相关的各电气标准,上一篇文章主要是介绍如何用Verilog语言来完成串口接收功能。在前期查询资料的过程中,在...
  • MSP432的串行通信

    千次阅读 多人点赞 2021-07-23 15:43:53
    目录数字通信基本知识组成串行通信和并行通信同步通信和异步通信异步串行通信的通用基础知识异步串行通信的格式串行通信的波特率奇偶校验串行通信传输方式术语1.全双工(Full-duplex)2.半双工(Half-duplex)3.单工...
  • STM32串口通信详解

    千次阅读 多人点赞 2022-04-22 20:17:32
    串口通信协议,串口中断实验,用电脑端给单片机发送指令点亮LED灯
  • FPGA(二)串口通信

    2022-02-18 10:47:40
    通信过程分为3个步骤:首先,发送方按照信息编码方式对有效信息进行编码(编成可以在通信线路上传输的信号形态);然后,编码后的信息在传输介质上进行传输,输送给接收方;最后,接收方接到编码信息后进行解码,...
  • 串口通信实验

    千次阅读 2022-01-15 19:59:26
    串口通信实验

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 5,646
精华内容 2,258
关键字:

串行通信收发双方