串口通信工程笔记

2024-05-31 05:08
文章标签 笔记 串口 通信工程

本文主要是介绍串口通信工程笔记,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一、串口API

1.  打开串口

  使用CreateFile函数可以打开串口。通常有两种方式可以打开,一种是同步方式(NonOverlapped),另外一种异步方式(Overlapped)。

        HANDLE hComm;

        hComm = CreateFile(   gszPort,                                                    //串口名

                                           GENERIC_READ|GENERIC_WRITE          //读写

                                           0,                        //注意:串口为不可共享设备,本参数须为0

                                           0,

                                           OPEN_EXISTING,

                                           FILE_FLAG_OVERLAPPED,                      //异步方式

                                           0);

        if(hComm == INVALID_HANDLE_VALUE)     //打开串口失败处理

               ······

2.   配置串口

  DCB(Device Control Block)结构定义了串口通信设备的控制设置,有3种方式可以初始化DCB。

  • 通过GetCommState函数得到DCB的初始值:

      DCB dcb;

               memset(&dcb, 0, sizeof(dcb));

               if(!GetCommState(hComm, &dcb))     ……        //错误处理

               else ……                                                         //已准备就绪

  • 用BuildCommDCB函数初始化DCB结构:

      DCB dcb;

      memset(&dcb, 0, sizeof(dcb));

      dcb.DCBlength = sizeof(dcb);

      if(!BuildCommDCB(“9600,n,8,1”,  &dcb))       ……     //参数配置错误

      else ……                                                                //已准备就绪

  • 用SetCommState函数手动设置DCB初值:

      DCB dcb;

      memset(&dcb, 0, sizeof(dcb));

      if(!GetCommState(hComm, &dcb))     return FALSE;

      dcb.BaudRate = CBR_9600;

3.  流控设备

  流控制有如下两种设置:

  • 硬件流控制:硬件流控有两种,DTE/DSR方式和RTS/CTS方式。这与DCB结构的初始化有关系,建议采用标准流行的流控方式,采用硬件流控时,DTE、DSR、RTS、CTS的逻辑位直接影响到数据的读写及收发数据的缓冲区控制。
  • 软件流控制:串口通信中采用特殊字符XON和XOFF作为控制串口数据的收发。

注意:在不设置流控制方式或软件流控的情况下,基本上不会出现什么问题,但在硬件流控下,规范的RTS_CONTROL_HANDSHAKE流控方式的含义本来是当缓冲区快满的时候RTS会自动OFF通知对方暂停发送,当缓冲区重新空出来的时候,RTS会自动ON,但很多时候当RTS变OFF以后即使已经清空了缓冲区,RTS也不会自动的ON,造成对方停在那里不发送了。所以,如果要用硬件流控制的话,还要在接收后最好加上检测缓冲区大小的判断,具体做法是使用ClearCommError后返回COMSTAT.cbInQue,当缓冲区已经空出来的时候,要使用invoke(EscapeCommFunction,hComm,SETRTS)重新将RTS设置为ON。

4.  串口读写操作

  串口读写有两种方式:同步方式(NonOverlapped)和异步方式(Overlapped)。同步方式指必须完成了读写操作,函数才返回,这可能会使程序无响应,因为如果在读写时发生了错误,永远不返回就会出错,可能线程将停在原地。而异步方式则灵活的多,一旦读写不成功,就将读写挂起,函数直接返回,可以通过GetLastError函数得知读写未成功的原因,所以串口读写常常采用异步方式操作。

ReadFile()函数用于完成读操作,异步方式的读操作为:

  DWORD dwRead;

  BOOL fWaitingOnRead = FALSE;

  OVERLAPPED osReader;

  memset(&osReader, 0, sizeof(osReader));

  osReader.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);

  if(osReader.hEvent == NULL)      ……       //错误处理

  if(!fWaitingOnRead)

  {

          if(!ReadFile(hComm, lpBuf, READ_BUF_SIZE, &dwRead, &osReader))              //读串口

          {

                 if(GetLastError() != ERROR_IO_PENDING)     ……       //报告错误

                 else fWaitingOnRead = TRUE;

     }

  }

  else

  {

    //读取完成,不必在调用GetOverlappedResults函数

    HandleASuccessfulRead(lpBuf, dwRead);

        }

 

  //如果读操作被挂起,可以调用WaitForSingleObject()函数或

  //WaitForMuntilpleObjects()等待读操作完成或者超时发生,

  //再调用GetOverlappedResult()得到想要的信息。

  if(fWaitingOnRead)

  {

    dwRes = WaitForSingleObject(osReader.hEvent, READ_TIMEOUT);

    switch(dwRes)

    {

    case WAIT_OBJECT_0:        //完成读操作

           if(!GetOverlappedResult(hComm, &osReader, &dwRead, FALSE))   …… //错误

           else ……        //全部读取成功

           HandleASuccessfulRead(lpBuf, dwRead);

             fWaitintOnRead = FALSE;

             break;

    case WAIT_TIMEOUT:         //操作尚未完成

           …….                           //处理其他任务

           break;

    default:

           ……              //出现错误

           break;

    }

  }

  注意上述代码在处理多线程串口在windows系列下存在一些问题,修改完成后代码参考1.4节。

5.  关闭串口

  程序结束或需要释放串口资源时,必须正确关闭串口。调用CloseHandle函数关闭串口的句柄即可,

         CloseHandle(hComm);

  值得注意的是,在关闭串口前必须保证读写串口线程已经退出,否则会引起误操作,一般采用的办法是使用事件驱动机制,启动一事件,通知串口读写线程强制退出。

6. 其他问题

  串口通信中其他必须处理的问题主要有如下几个:

  • 检测通信事件:用SetCommMask()设置想要得到的通信事件的掩码,再调用WaitCommEvent()检测通信事件的发生。可设置事件标志有EV_BREAK \ EV_VTS \ EV_DSR \ EV_ERR \ EV_RING \ EV_RLSD \ EV_RXCHAR \ EV_RXFLAG \ EV_TXEMPTY。
  • 处理通信超时:在通信中,超时是一个很重要的考虑因素,因为数据接收过程中由于某种原因突然中断或停止,如果不采取超时控制机制,将会使得I/O线程被挂起或无限阻塞。超时设置分两步,首先设置COMMTIMEOUTS结构的5个变量,然后调用SetcommTimeouts()设置超时值,对于使用异步方式读写的操作,如果操作挂起后,异步成功完成了读写,WaitForSingleObject()或WaitForMultipleObjects()将返回WAIT_OBJECT_0,GetOverlappedResult()返回TRUE。其实还可以用GetCommTimeouts()得到系统初始值。
  • 错误处理和通信状态:在串口通信中,可以会产生很多的错误,使用ClearCommError()可以检测错误并且清除错误条件。
  • WaitCommEvent()返回时,只是指出了如CTS等等状态有变化。但要了解具体变化情况必须使用GetCommModemStatus()获得串口线路状态更详细的信息。

 

二、串口操作方式

1.  同步方式

  同步(NonOverlapped)方式是比较简单的一种方式,编写代码长度明显少于异步(Overlapped)方式。同步方式中,读串口的函数试图在串口的接收缓冲区中读取规定数据的数据,直到规定数据的数据全部被读出或设定超时时间已到时才返回。例如:

       COMMTIMEOUTS timeOver;

       memset(&timeOver, 0, sizeof(timeOver));

       DWORD timeMultiplier, timeConstant;

       ……

       timeOver.ReadTotalTimeoutMultiplier = timeMultiplier;

       timeOver.ReadTotalTimeoutConstant = timeConstant;

       SetCommTimeouts(hComm, &timeOver);

       ……

       ReadFile(hComm, inBuffer, nWantRead, &nRealRead, NULL); //NULL指采用同步文件读写

  如果所规定的待读取数据的数目nWantRead较大且设定的超时时间较长,而接收缓冲区中数据较少,则可能引起线程阻塞。解决这一问题的方法是检查COSTAT结构的cbInQue成员,该成员的大小即为接收缓冲区中处于等待状态的实际个数。如果令nWantRead的值等于COMSTAT.cbInQue,就能很好的防止线程阻塞。                       

2. 异步方式

  在异步方式中,利用Windows的多线程结构,可以让串口的读写操作在后台进行,而应用程序的其他部分在前台执行。例如:

       OVERLAPPED wrOverlapped;

       COMMTIMEOUTS timeOVer;

       memset(&timeOver, 0, sizeof(timeOver));

       DWORD timeMultiplier, timeConstant;

       ……       //给timeMultiplier, timeConstant赋值

       timeOver.ReadTotalTimeoutMultiplier = timeMultiplier;

       timeOver.ReadTotalTimeoutConstant = timeConstant;

       SetCommTimeouts(hComm, &timeOver);

       wrOverlapped.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);

       ……

       ReadFile(hComm, inBuffer, nWantRead, &nRealRead,  &wrOverlapped);

       GetOverlappedResult(hComm, &wrOverlapped, &nRealRead,TRUE);

       ……

       ResetEvent(wrOverlapped.hEvent);

  上面代码中的ReadFile由于采用了异步方式,所以只返回数据是否已经开始读入的状态,并不返回实际的读入数据,即ReadFile中的nRealRead无效。实际读入的数据由GetOverlappedResult返回的,该函数的最后一个参数值为TRUE,表示它等待异步操作结束后才返回到应用程序,此时,GetOverlappedResult与WaitForSingleObject函数无效。

3. 查询方式

  即一个进程中的某一线程定时地查询串口的接收缓冲区,如果缓冲区中有数据,就读取数据;若缓冲区没有数据,该线程将继续执行,因此会占用大量的CPU时间,它实际上是同步方式的一种派生。例如:

              COMMTIMEOUTS timeOver,

              Memset(&timeOver, 0, sizeof(timeOver));

              timeOver.ReadIntervalTimeout = MAXWORD;

              SetCommTimeouts(hComm, &timeOver);

              ……ReadFile(hComm, inBuffer, nWantRead, &nRealRead, NULL);

  除了COMMTIMEOUTS结构的变量timeOver设置不同外,查询方式与同步方式在程序代码方面很类似,但二者的工作方式却差别很大。尽管ReadFile采用的也是同步文件读写方式,但由于timeOver的区间超过时间设置为MAXWORD,所以ReadFile每次将读出接收队列中的所有处于等待状态的数据,一次最多可读出nWantRead个字节的数据。

4.  事件驱动方式

  若对端口数据的响应时间要求较严格,可采用事件驱动方式。事件驱动方式通过设置事件通知,当所希望的事件发生时,Windows发出该事件已经发生的通知。Windows定义了9中串口通信事件,常用的有以下3中:

  • EV_RXCHAR:接收到一个字节,并放入输入缓冲区。
  • EV_TXEMPTY:输出缓冲区中的最后一个字符,发送出去。
  • EV_RXFLAG:接收到事件字符(DCB结构中的EvtChar成员),放入输入缓冲区。

  在用SetCommMask()制定了有用的事件后,应用程序可调用WaitCommEvent()来等待事件的发生。SetCommMask可使WaitCommEvent()中止。例如:

              COMSTAT comStat;

              DWORD dwEvent;

              SetCommMask(hComm, EV_RXCHAR);

              ……

              if(WaitCommEvent(hComm, &dwEvent, NULL))

                     if((dwEvent & EV_RXCHAR) && comstat.cbInQue)

                            ReadFile(hComm, inBuffer, comstat.cbInQue, &nRealRead, NULL);

5. 总结

  一般要求情况下,查询方式是一种最直接的读串口的方式。但定时查询存在一个致命的弱点,即查询是定时发生的,可能发生的过早或过晚。在数据变化较快的情况下,特别是主控计算机的串口通过扩展板扩展多个时,需定时对所有串口轮流查询,容易发生数据的丢失。虽然定时间隔越小,数据的实时性越高,但系统的资源也被占用越多。

  Windows中提出文件读写的异步方式,主要是针对文件IO相对较慢的速度而进行的改进,它利用了系统的多线程结构,虽然在Windows中没有实现任何对文件IO的异步操作,但它却能对串口进行异步操作。采用异步方式,可以提高系统整体性能,在对系统强壮性要求高的场合,建议采用这种方式。

  事件驱动方式是一种高效的串口读方式。这种方式实时性较高,特别对扩展了多个串口的情况,并不要求像查询方式那样定时地对所有串口轮询,而像中断方式那样,只有当设定的事件发生时,应用程序得到windows操作系统发出的消息后,才进行相应处理,以免数据丢失。

这篇关于串口通信工程笔记的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1017422

相关文章

【学习笔记】 陈强-机器学习-Python-Ch15 人工神经网络(1)sklearn

系列文章目录 监督学习:参数方法 【学习笔记】 陈强-机器学习-Python-Ch4 线性回归 【学习笔记】 陈强-机器学习-Python-Ch5 逻辑回归 【课后题练习】 陈强-机器学习-Python-Ch5 逻辑回归(SAheart.csv) 【学习笔记】 陈强-机器学习-Python-Ch6 多项逻辑回归 【学习笔记 及 课后题练习】 陈强-机器学习-Python-Ch7 判别分析 【学

系统架构师考试学习笔记第三篇——架构设计高级知识(20)通信系统架构设计理论与实践

本章知识考点:         第20课时主要学习通信系统架构设计的理论和工作中的实践。根据新版考试大纲,本课时知识点会涉及案例分析题(25分),而在历年考试中,案例题对该部分内容的考查并不多,虽在综合知识选择题目中经常考查,但分值也不高。本课时内容侧重于对知识点的记忆和理解,按照以往的出题规律,通信系统架构设计基础知识点多来源于教材内的基础网络设备、网络架构和教材外最新时事热点技术。本课时知识

论文阅读笔记: Segment Anything

文章目录 Segment Anything摘要引言任务模型数据引擎数据集负责任的人工智能 Segment Anything Model图像编码器提示编码器mask解码器解决歧义损失和训练 Segment Anything 论文地址: https://arxiv.org/abs/2304.02643 代码地址:https://github.com/facebookresear

数学建模笔记—— 非线性规划

数学建模笔记—— 非线性规划 非线性规划1. 模型原理1.1 非线性规划的标准型1.2 非线性规划求解的Matlab函数 2. 典型例题3. matlab代码求解3.1 例1 一个简单示例3.2 例2 选址问题1. 第一问 线性规划2. 第二问 非线性规划 非线性规划 非线性规划是一种求解目标函数或约束条件中有一个或几个非线性函数的最优化问题的方法。运筹学的一个重要分支。2

【C++学习笔记 20】C++中的智能指针

智能指针的功能 在上一篇笔记提到了在栈和堆上创建变量的区别,使用new关键字创建变量时,需要搭配delete关键字销毁变量。而智能指针的作用就是调用new分配内存时,不必自己去调用delete,甚至不用调用new。 智能指针实际上就是对原始指针的包装。 unique_ptr 最简单的智能指针,是一种作用域指针,意思是当指针超出该作用域时,会自动调用delete。它名为unique的原因是这个

查看提交历史 —— Git 学习笔记 11

查看提交历史 查看提交历史 不带任何选项的git log-p选项--stat 选项--pretty=oneline选项--pretty=format选项git log常用选项列表参考资料 在提交了若干更新,又或者克隆了某个项目之后,你也许想回顾下提交历史。 完成这个任务最简单而又有效的 工具是 git log 命令。 接下来的例子会用一个用于演示的 simplegit

记录每次更新到仓库 —— Git 学习笔记 10

记录每次更新到仓库 文章目录 文件的状态三个区域检查当前文件状态跟踪新文件取消跟踪(un-tracking)文件重新跟踪(re-tracking)文件暂存已修改文件忽略某些文件查看已暂存和未暂存的修改提交更新跳过暂存区删除文件移动文件参考资料 咱们接着很多天以前的 取得Git仓库 这篇文章继续说。 文件的状态 不管是通过哪种方法,现在我们已经有了一个仓库,并从这个仓

忽略某些文件 —— Git 学习笔记 05

忽略某些文件 忽略某些文件 通过.gitignore文件其他规则源如何选择规则源参考资料 对于某些文件,我们不希望把它们纳入 Git 的管理,也不希望它们总出现在未跟踪文件列表。通常它们都是些自动生成的文件,比如日志文件、编译过程中创建的临时文件等。 通过.gitignore文件 假设我们要忽略 lib.a 文件,那我们可以在 lib.a 所在目录下创建一个名为 .gi

取得 Git 仓库 —— Git 学习笔记 04

取得 Git 仓库 —— Git 学习笔记 04 我认为, Git 的学习分为两大块:一是工作区、索引、本地版本库之间的交互;二是本地版本库和远程版本库之间的交互。第一块是基础,第二块是难点。 下面,我们就围绕着第一部分内容来学习,先不考虑远程仓库,只考虑本地仓库。 怎样取得项目的 Git 仓库? 有两种取得 Git 项目仓库的方法。第一种是在本地创建一个新的仓库,第二种是把其他地方的某个

Git 的特点—— Git 学习笔记 02

文章目录 Git 简史Git 的特点直接记录快照,而非差异比较近乎所有操作都是本地执行保证完整性一般只添加数据 参考资料 Git 简史 众所周知,Linux 内核开源项目有着为数众多的参与者。这么多人在世界各地为 Linux 编写代码,那Linux 的代码是如何管理的呢?事实是在 2002 年以前,世界各地的开发者把源代码通过 diff 的方式发给 Linus,然后由 Linus