VC串口上位机编程学习笔记
下面就结合自己的理解,分析MTTY的SAMPLE CODE,就当读书笔记吧。希望对打算用API进行串口编程的朋友有所帮助,能尽快读懂MTTY的源码,建立起串口编程的思想框架,高手请飘过。阅读这篇文章需要你有阅读MSDN的英文能力,对串口概念有基本的认识。
开讲之前再啰嗦一把,按我找的资料来看,有几个普遍观点:1 WINNT串口不能用同步模式工作 2 简单地应用用同步模式、开单线程,实时性高地用异步单线程或多线程,因为异步+多线程比同步\异步+单线程要复杂得多。先说第二点,首先异步多线程确实要复杂得多,但也不至于有登天那么难,以我现在的水平就能基本搞明白,我从6月1日儿童节头脑发热决心研究编程,要知道那时候我还以为VC是一种语言,而且最开始的几天都花在纠结该学哪种语言上,半路出家,完全没有基础。其次出于学习考虑,找最难的整,学到的东西才多,至少学完串口能对多线程的使用有一定地直观了解。再次,从性能上说,异步多线程是最好的,而且使用非常灵活,能应付一切需求。所以初学串口,就咬紧牙搞异步多线程吧,CreatFile时用上OVERLAPPED,哈哈,那就一发不可收拾了。差点忘了说第一点,我从来不打算用同步模式,我也不知道行不行。
以下 xxx()表示一个函数,没有特别说明的话指SAMPLE CODE里的自定义函数。
第一:串口工作环境地建立
点击FILE里的CONNECT后,调用 INIT.C里的SetupCommPort()进行串口初始化:
1打开串口
用API函数CreateFile
2设置串口
调用UpdateConnection(),先建立一个dcb结构体(记得一定要初始化后再使用),然后用API函数GetCommState把现在的串口配置存入新建的dcb中,因为一般对串口参数的设置就是波特率之类,其他用默认即可,获取现在串口配置保存到我们建立的 dcb中,再根据需要进行修改会方便得多。根据需要对dcb内的成员赋值后,再用API函数SetCommState使设置好的 dcb生效,下一步就是用API函数SetCommTimeouts进行超时设置。
最后用用API函数SetupComm设置输入输出缓存。
至此,串口已经准备好,可以供我们使用了。
无论什么时候需要开始使用串口,按上面的步骤进行初始化操作都是必须而且有效的。
这里有另外两个API函数顺便说一下,1CommConfigDialog,2PurgeComm。
第一个是调用WINDOWS自带的串口设置对话框设置串口参数,这需要新建立一个COMMCONFIG 结构体来接收设置,COMMCONFIG里也有一个dcb结构,通话框里的设置自然就是保存到这里边来了,这个结构体在使用前记得要初始化。在调用CommConfigDialog前把现在的dcb 结构存到COMMCONFIG里是个好办法,不然对话框里的参数都是系统默认的,还得一个一个改。另外COMMCONFIG 在使用前要对成员wVersion和dwSize 赋值,否则执行会有异常,具体设置在MSDN里有详细说明。有人说其中的dwSize 不能填 sizeof COMMCONFIG,但MSDN里用的就是SIZEOF,我也用得很好,没有任何问题。
第二个函数在MSDN的说明是:Discards all characters from the output or input buffer of a specified communications resource. It can also terminate pending read or write operations on the resource.简单说就是立即中断串口的一切操作,当用CommConfigDialog设置好串口参数后,当然是希望串口按新的状态工作,所以应该接着调用PurgeComm。如果你想等所有的读写(包括未决的)操作都完成再生效,就需要使用事件“event”,在后面会说到它。
最后用StartThreads(void)建立读、写线程,MTTY里的监视和读是同一个线程。
至此这里串口的工作环境建立完成。
第二 让串口按我们的要求进行读写
串口工作环境地建立比较程式化,一步一步做完即可。那怎么按我们的要求让串口工作起来呢?下面说说怎么具体地实现利用多线程进行异步串口操作。我也只是有了初步概念,以下叙述若有错误,日后再更正。
第二 让串口按我们的要求进行读写
串口工作环境地建立比较程式化,一步一步做完即可。那怎么按我们的要求让串口工作起来呢?下面说说怎么具体地实现利用多线程进行异步串口操作。我也只是有了初步概念,以下叙述若有错误,日后再更正。
先回忆一下上一节的内容,如何建立串口的工作环境:
1.打开串口,用CreateFile
2.DCB xxx ,xxx就是我们要用的串口配置结构体。可以选择用GetCommState获取当前串口配置,存入xxx。
3.按需求填写串口配置块xxx。这时可以用用CommConfigDialog。
4.SetCommState让串口配置块xxx生效
5.设置超时,用SetCommTimeouts
6.设置缓存,用SetupComm
7.创建读、写、监视线程
还是以写串口为例子,我的程序里需要定时更新仪表面板数据,那段程序执行时间最多能达到150ms,它是在主进程里执行的,更新面板数据后还要把数据写到串口,又是一段不短的时间,这样接口程序运行起来让人感觉老是慢半拍,更严重的是如果这个时候下位机发了数据上来,是完全不会得到响应的。所以非常有必要把读、写、监视串口用单独的线程来执行。
我的接口程序最开始写串口时对CPU的点用是近100%,后来把写单独做一个线程,CPU占用率降到25%,再用简单的处理办法:在更新面板数据时让写线程挂起,更新完成后再唤醒写线程,CPU占用率降到可以乎略不计。
平时常用的串口调试助手,同时打开两个,其中一个向另一个循环发字符,CPU(单核闪龙3400+)占用率立即会飙到100%,但做同样的事情,MS的MTTY工作得就非常好,CPU占用率在5%以内,足见多线程的威力。
创建了线程,当然就有对应线程的函数,MTTY里对应写线程的是
DWORD WINAPI WriterProc(LPVOID lpV),LPVOID lpV是传递给线程函数的参数,在MTTY里这个值是NULL。
写串口是主动的,我们需要在需要写串口的时候让写串口线程函数里的程序工作起来,平时碰到类似的任务一般是用设flag实现,但是我们写串口的时候需要知道上一次的写操作是否已经结束,这又需要一个flag,其他还有很多需要设flag的地方,flag这么多,怎么安排得过来。还好,WINDOWS有办法解决这个问题,那就是用event (事件)。
跟事件相关的API函数有
CreatEvent, 创建一个事件,需要用到事件时就创建它
SetEvent, 置事件为有信号的
ResetEvent, 重置事件为无信号
WaitForSingleObjects, 等待一个事件
WaitForMultipleObjects,等待多个事件
来打个比方说说这些函数的作用:
一个个的事件就像是一个个的信号灯,我们想知道谁的状态,那就用CreatEvent做几盏灯给他戴到头上,一个状态对应一盏灯。状态发生了就用SetEvent让灯亮(让事情有信号)代表对应的状态发生了,状态结束了就用ResetEvent(重置事件没无信号)让灯灭掉代表嘛事都没有了。然后谁想偷_窥别人了,就用WaitForMultipleObjects或者WaitForSingleObjects这两个马仔来监视,当然得先告诉马仔你想监视谁的哪个状态,马仔就会不间断地把人家的头顶都瞅一遍,要是你想要的状态代表的灯亮了,它立马会告诉你,碰到其他情况它也会给你相应的信息,你再根据不同的信息做不同的处理。
还有一种情况,就是你想让别人控制你,那你就自己建信号灯然后自己盯着头顶,别人调用SetEvent和ResetEvent来控制你的灯,你自己再根据灯的亮灭做相应的操作。MTTY里的写线程函数就是这么干的。
下面开始分析MTTY里神奇的SENDREPEATEDLY:
从这开始case ID_TRANSFER_SENDREPEATEDLY:,这个case里最后的操作是调用TransferRepeatCreate,我们来看看这个函数干了什么,看这个函数的代码,它干了不少事情,怎么没有在里边没看到操作串口?前边我们已经说了,为写串口单独建立了一个线程,所以操作操口的勾当绝对是在线程函数里干的。
我们只关心重点,既然是repeatedly,定时的话,那肯定是用定时器了,果然有
mmTimer = timeSetEvent((UINT) dwFrequency, 10, TransferRepeatDo, dwRead, TIME_PERIODIC);
TransferRepeatDo就是定时器的回调函数,我们再看看它干了什么,只是调用了WriterAddNewNodeTimeout,看意思是新增加一个节点,这个写串口还跟节点扯什么关系?再看这个函数干了什么:Adds a new write request packet, timesout if can't allocate packet.里边的第一句就是 PWRITEREQUEST pWrite;
这个PWRITEREQUESTtypedef struct WRITEREQUEST
{
DWORD dwWriteType; // char, file start, file abort, file packet
DWORD dwSize; // size of buffer
char ch; // ch to send
char * lpBuf; // address of buffer to send
HANDLE hHeap; // heap containing buffer
HWND hWndProgress; // status bar window handle
struct WRITEREQUEST *pNext; // next node in the list
struct WRITEREQUEST *pPrev; // prev node in the list
} WRITEREQUEST, *PWRITEREQUEST;
明白了前面为啥是addnewnode了,这里用到了链表lined list ,因为要写的数据可能很多,串口只能一个一个写,要写的数据先放到链表里,然后串口再依次地写。把建立要写的数据和写数据地操作分开来,那两者就互不影响了。
建完新的数据节点,下一步当然就是把节点放到链表里,倒数第二句AddToLinkedList(pWrite);
这个函数里就是典型的链表增加节点地操作,重点在后面SetEvent(ghWriterEvent),增加完节点,让ghWriterEvent这个信号灯亮起来,直觉告诉我们,这就是亮给写线程看的,看一下都有谁使用它,真相揭晓,传说中的写线程函数终于闪亮登场:
DWORD WINAPI WriterProc(LPVOID lpV)
这里边的细枝末节比较多,照例只关心重点,里面建立了两个事件
ghWriterEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
if (ghWriterEvent == NULL)
ErrorInComm("CreateEvent(writ request event)");
ghTransferCompleteEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
if (ghTransferCompleteEvent == NULL)
ErrorInComm("CreateEvent(transfer complete event)");
ghWriterEvent 就是给AddToLinkedList(pWrite);用的,它更新完链表就通知写线程去写串口,那写线程是怎么等这个消息的呢?就是在这里:
hArray[0] = ghWriterEvent;
hArray[1] = ghThreadExitEvent;
while ( !fDone )
{
dwRes = WaitForMultipleObjects(2, hArray, FALSE, WRITE_CHECK_TIMEOUT);
switch(dwRes)
{
case WAIT_TIMEOUT:
break;
case WAIT_FAILED:
ErrorReporter("WaitForMultipleObjects( writer proc )");
break;
//
// write request event
//
case WAIT_OBJECT_0:
HandleWriteRequests();
break;
//
// thread exit event
//
case WAIT_OBJECT_0 + 1:
fDone = TRUE;
break;
}
}
CloseHandle(ghTransferCompleteEvent);
CloseHandle(ghWriterEvent);
//
// Destroy WRITE_REQUEST heap
//
HeapDestroy(ghWriterHeap);
return 1;
}
ghWriterEvent是事件组hArray的第一个,那如果这个事件有信号后, WaitForMultipleObjects返回 WAIT_OBJECT_0:
case WAIT_OBJECT_0:
HandleWriteRequests();
break;
HandleWriteRequests处理写串口请求,绕了这么多弯,终于要写串口了,激动,冲过去一看,靠,就是一堆case,原来这里是要根据不同的数据包选择不同的写串口函数,这就是所谓的全能型软件了。我们写的是file,那就是这个case
case WRITE_FILE: WriterFile(pWrite);
//
// free data block
//
EnterCriticalSection(&gcsDataHeap);
fRes = HeapFree(pWrite->hHeap, 0, pWrite->lpBuf);
LeaveCriticalSection(&gcsDataHeap);
if (!fRes)
ErrorReporter("HeapFree(file transfer buffer)");
break;
WriteFile里是WriterGeneric,看到了吧,这个WriterGeneric就是典型的写串口函数了,跟Serial Communications in WIN32里一模一样,这个在里边有详细的说明,这里就不讲了。
再来考虑另一个问题,线程函数内的代码一旦执行完成,线程函数就返回了,然后线程就结束了,那我们肯定是希望线程在我们要求它结束时他才结束,其他时候他就老老实实地听命令定串口,那这是怎么实现的呢?就是在这里while ( !fDone )很简单吧,就是一个while,while里面就是马仔
dwRes = WaitForMultipleObjects(2, hArray, FALSE, WRITE_CHECK_TIMEOUT);
switch(dwRes)
{
case WAIT_TIMEOUT:
break;
case WAIT_FAILED:
ErrorReporter("WaitForMultipleObjects( writer proc )");
break;
//
// write request event
//
case WAIT_OBJECT_0:
HandleWriteRequests();
break;
//
// thread exit event
//
case WAIT_OBJECT_0 + 1:
fDone = TRUE;
break;
}
}
我们再看看这个fDone是谁控制的,最后一个case
case WAIT_OBJECT_0 + 1:
fDone = TRUE;
break;
当fDone为真,那while就退出了,然后线程函数内的代码执行完,线程也就结束了。那WAIT_OBJECT_0 + 1:又是表示哪个事件有信号呢?看前面:hArray[1] = ghThreadExitEvent;, ghThreadExitEvent,这是一个全局变量,字面意思就是进程退出事件,我们先来想想,什么时候才需要退出写线程,是不是应该在关闭串口的时候,看看ghThreadExitEvent被谁引用了
就是这个DWORD WaitForThreads(DWORD dwTimeout),里边有一句SetEvent(ghThreadExitEvent);这句代码一执行ghThreadExitEvent就置为有信号。
我们可以看到,可以在函数内用局部的flag ,但跟别的线程通信一定要用event
DWORD WaitForThreads是在BOOL BreakDownCommPort()调用的,跟设想的一样,在关闭串口时结束写线程。再看看是谁调用了它,case ID_FILE_DISCONNECT:,点击菜单内的DISCONNECT关闭串口。
SENDREPEATEDLY的代码描绘了多线程异步串口的写操作,虽然层层包装但功能清淅,读懂了这部分,其他功能再研究起来就很简单了,读串口也就是多了SetCommMask之类的东西而已。
楼主最近还看过