c语言 串口上位机,VC串口上位机编程学习笔记

第二 让串口按我们的要求进行读写

串口工作环境地建立比较程式化,一步一步做完即可。那怎么按我们的要求让串口工作起来呢?下面说说怎么具体地实现利用多线程进行异步串口操作。我也只是有了初步概念,以下叙述若有错误,日后再更正。

先回忆一下上一节的内容,如何建立串口的工作环境:

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之类的东西而已。

转载请注明出处!

你可能感兴趣的:(c语言,串口上位机)