242010
 

最近做了一个串口通信的程序,PC到PC的通信,为之后的单片机和PC通信做点准备。很简单的小东西,就是实现了一下串口的通信连接、异步收发数据。虽然简单,还是遇到了些小问题,总结一下,供以后借鉴吧。

一、整体架构:

a) 全局变量

  1. 使用HANDLE m_hComm保存打开的端口句柄;
  2. BOOL m_bConnected来标示当前端口的状态,即是否已经连接成功;
  3. CWinThread* m_pThread来保存之后要开启的异步读取辅助线程;
  4. int nPort保存连接的端口号;
  5. 为了进行异步操作,还要给读取和写入分别定义一个OVERLAPPED结构变量,OVERLAPPED m_ovRead, m_ovWrite;
  6. 最后还要定义一个消息事件HANDLE m_hPostEvent,用于线程向窗体通知数据的到达;

b)函数概述:

  • BOOL ConfigPort();     //配置端口
  • BOOL ConnectToPort(int nPort);     //连接到端口
  • BOOL DisconnectToPort();     //从端口断开
  • DWORD WriteComm(char *buff, DWORD dwCount);     //向端口写入
  • DWORD ReadComm(char *buff, DWORD dwCount);     //从端口读出
  • UINT thCommProc(LPVOID pParam);      //全局函数,是一个线程的执行体

二、函数实现

a) 端口的连接ConnectToPort:
CreateFile 函数,用FILE_FLAG_OVERLAPPED方式即可打开端口。m_bConnected设置为TRUE。

b) 端口的配置和初始化:

  1. SetupComm设置进出队的值,一般设为MAXBLOCK = 4096即可,不重要。
  2. SetCommMask设置后面要用到的WaitCommEvent监视哪些通信事件,0为不监控,这里设置为EV_RXCHAR,即监控数据接收事件,这一步非常重要。
  3. 初始化COMMTIMEOUTS timeouts结构体,该结构指定了读出和写入的一系列相关的超时值。用SetCommTimeouts将结构体的内容加载到端口上。
  4. DCB是指Device Control Block,DCB dcb;声明这样一个变量,这个变量可以用GetCommStat来获得,之后用BuildCommDCB设置端口DCB的一系列属性,比如波特率、发送数据长度,停止位等等,形如"baud=9600 parity=N data=8 stop=1",这个设置字符串可以在MSDN查到。设置好DCB后用SetCommStat加载到端口上。这样就完成了端口的初始化。

c) 写入操作:
在正式说写入操作前,先说一下同步和异步操作的区别。同步IO操作的函数在IO执行没有完成之前会始终占用CPU配额,表现即为整个应用程序挂起,不响应操作直至IO过程完成。如果IO的数据量小,那么这样做影响不大;但如果IO数据量较大,会使得程序较长时间不响应,影响使用体验。异步操作则是在函数调用后立即返回,返回状态为ERROR_IO_PENDING,即IO操作被送到后台给操作系统处理了,程序不再经手,可以继续进行其他操作。那么怎样知道后台的操作进度如何,有没有结束呢?这是通过GetOverLappedResult来查询异步调用的函数中的OVERLAPPED结构知道的。这个函数还没有研究很透彻,这里就不多说啦。
使用WriteFile并且向其传递m_ovWrite变量来进行异步写入操作,如果得到的返回状态为ERROR_IO_PENDING,则用GetOverlappedResult查询并等待写入动作的完成。

d) 读取操作:

  • 由于无法得知数据何时会到达端口,所以需要在main线程之外开一个辅助线程m_pThread来等待通信事件的发生以及通知主线程这一事件,辅助线程的实现方式如下。
  • 首先,一些操作都放在一个“无限循环”的while里面;
  • 进入循环之后,函数用ClearCommError得到COMSTAT结构,从而在COMSTAT的cbInQue中查看是否已经把所有本次通信发过来的所有内容读取走,若没有读取完,会检查m_hPostEvent消息事件的状态;有信号则表示主线程已经处理完一次读取过程可以继续处理,辅助线程便ResetEvent然后PostMessage(pDlg->m_hWnd, WM_COMMNOTIFY, EV_RXCHAR, 0)给主线程,这就通知了主线程再次进行读取,完了再SetEvent来通知辅助线程检查端口状态;如果已经读取完了,则使用WaitCommEvent函数查询端口状态,等待通信事件的再次发生。同样的,这个查询也是异步进行,具体实现与写入操作一致。
  • 前面说的是辅助线程,而主线程的工作则很简单,通过Windows消息机制等候WM_COMMNOTIFY,即读取端口的时机,一旦消息到来,即可简单的利用ReadFile异步读取端口内容,然后ResetEvent通知辅助线程,不过再次之前先要ClearCommError查询一下是本次允许读取的缓存大还是剩余可读内容大,选取较小的数目来读取才不会出问题。

e) 收尾工作和关闭端口:
当操作员发命令结束的时候,进行这些操作。

  1. 把m_bConnected设置为FALSE;
  2. 主动将m_hPostEvent置为有信号,使辅助线程的WaitForSingleObject结束;
  3. SetCommMask设置掩码为0,使辅助线程的WaitCommEvent结束;这样辅助线程就自动结束了,在主线程中用WaitForSingleObject等待它的完成。
  4.   CloseHandle关闭所有的已经打开的句柄,包括事件和端口。这样,就完成了所有操作。

三、中途遇到的一些关于C++的问题

  • 我在处理接收到的数据的时候,发现打印出的字符除了应有的字符串之外,还有一大堆“烫烫烫烫……”,后来发现是我没有对使用的字符串缓冲区内存清零所导致。同样的,其他类型的变量在没有初始化的情况下使用,其结果也有可能是不可预料的。这告诫我无论是C还是C++里面,变量初始化是一定要做的。例如DWORD dwError = 0;再例如 memset(buffSend, 0, 1024 * sizeof(char));
  • 线程函数可以接受一个被定义为LPVOID pParam的参数,一般都是把开始这个线程的主线程的this指针传入,这样在辅助线程中可以很方便的使用主线程的资源,像这样CSerialExDlg *pDlg = (CSerialExDlg *)pParam; 其中CSerialExDlg 是我主线程的那个类。
  • sprintf函数可以很方便的把输入值格式化到一个字符串中,和printf的用法很类似。
  • CString类型的字符串和char *类型的互换,->就用CString str.Format("%s", buff),<-就用buff = str.GetBuffer。 

四、和Windows以及MFC有关的一点东西

  • Windows消息机制的问题。使用起来和方便,就像前文所述,需要细究的话可以参考前面几篇文章。 
  • 动态设置MFC的对话框标题,直接在相应的类下面调用SetWindowText就可以了。
  • 窗体上空间的焦点设置。这个有点意思,首先要修改MFC自动生成的OnInitDialog函数,返回值改成FALSE,因为微软说:return TRUE  unless you set the focus to a control。之后用GetDlgItem(IDC_EDIT_SEND)->SetFocus();这种方式设置焦点就行了。
  • 最后一点,有些控件变量的使用,这个可以去找现成代码了,比如EDIT控件,把它绑定到一个CEDIT类型的控件变量上,就可以使用这个变量的很多成员函数来操作它了。

好了,终于结束了。后面要做的东西还有好多,越来越觉得自己要学的好多,不懂的更多,唉,果真是时间仓促水平有限啊,做个IT民工真是不容易。

最近过得还算不错,虽然有很多事情要忙,不过比上学期的空虚乏味要好些,总之有点收获什么的。目前看起来刚好23周岁了,再不上完学都老了,很恐怖,可惜无能为力。睡觉去喽,天气还不错啦啦啦啦啦……

(以下省略痛并快乐着的矛盾心理叙述10000字)

 Leave a Reply

(必须填写)

(必须填写,邮件地址不会被泄露)

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>