第二 让串口按我们的要求进行读写
串口工作环境地建立比较程式化,一步一步做完即可。那怎么按我们的要求让串口工作起来呢?下面说说怎么具体地实现利用多线程进行异步串口操作。我也只是有了初步概念,以下叙述若有错误,日后再更正。
先回忆一下上一节的内容,如何建立串口的工作环境:
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之类的东西而已。
转载请注明出处!