分享

串口编程入门

 QomoIT 2018-03-21

最近本人在做有关串口通信的编程,可谓边学边用,在网上看到一篇好文,将它翻译过来,大家一起学习,其中有翻译错误的地方,还请多多指教!

这是一篇在Windows (NT 系列) 系统上进行串口通信的入门资料。在这篇资料中提供了一个CSerialCommHelper的类,该类可以被直接放在你的应用程序中,用于串口通信。需要提出的是,该类使用了重叠IO,而本文并不需要你过多的知道串口通信和重叠IO,但是你需要知道一些关于象事件之类的同步对象和一些象WaitForSingleObject和WaitForMultipleObject的Windows API函数。另外,一些windows基础的线程知识,如线程创建和线程结束,也是很必要的。

要使你的电脑具有串口通信的功能,你的电脑必须有串口。大多数电脑上都至少有一个串口,它们通常被称作COM1口和COM2口。有了串口,就有设备驱动程序。只要你仔细思考一下,串口通信所做的就是发送和接受数据,换句话说,你所作的一切就是在对串口进行IO的输入/输出,同时也在对磁盘文件进行IO操作。所以,当我们看见那些用于读写文件的API函数也被用于串口编程的时候,也就没什么人惊讶的了。当你向串口发送数据时,数据以字节为单位发送;当它离开串口时,数据以位为单位发送。同样,当数据到达串口时,数据以位为单位发送,当从串口获取数据后,数据的格式又是字节。

打开串口

串口通信的第一步是打开指定的串口。你可以让你的设备连接上串口COM1,你可以通过以下API函数来打开串口:

HANDLE m_hCommPort = ::CreateFile(szPortName,
                                                          GENERIC_READ|GENERIC_WRITE,
                                                          0,
                                                          0,
                                                          OPEN_EXISTING,
                                                          FILE_FLAG_OVERLAPPED,
                                                          0);
在上面的例子代码中,第三个、第五个和第七个参数必须这么设定。我们要以重叠的方式打开文件(串口),因此,第六个参数必须为FILE_FLAG_OVERLAPPED。扫后我们将详细讨论重叠IO。正如你从名字所猜测的那样,CreateFile函数可被用于创建磁盘文件,也可以用于打开存在的文件。

对于Windows系统来说,串口和磁盘文件都是IO设备。因此,要打开一个已经存在的文件(串口),我们所需要做的就是知道这个设备的名称(COM1),然后传入创建标签OPEN_EXISTING。

如果成功打开了一个COM串口,象成功打开了一个文件一样,API函数将返回指向该串口的句柄。当串口打开失败时,API函数也会返回值INVALID_HANDLE_VALUE。你可以调用GetLastError来获取失败的原因。最常见的一个失败原因是该串口已经被其它应用程序打开,其错误代码为ERROR_ACCESS_DENIED(5)。同样,如果你错误地打开了一个不存在的串口,通过GetLastError,你将获取错误代码ERROR_FILE_NOT_FOUND。

 注意:不要在调用GetLastError之前调用其它函数(ASSERT也不行),否则它的返回值为0。

打开一个串口之后,你现在所要做的就是开始使用。

 串口的读/写

现在你已经打开了一个串口,你一定想向串口连接的设备发送数据。例如,你想向连接的设备(另外一台电脑)发送“Hello”信息。当你向串口发送信息时,你所做的操作就和向文件中写入数据一样。使用以下API函数:

BOOL iRet = WirteFile(m_hCommPort, data, dwSize, &dwBytesWritten, &ov);

为了能够回应“Hello”信息,接受方发送“Hi”。因此,你需要读取串口信息,使用以下API函数:

BOOL iRet = ReadFile(m_hCommPort, szTmp, sizeof(szTmp), &dwBytesRead, &ovRead);

其他你需要了解的东西我将在以后的内容中讨论。现在所了解的一切看起来都很简单,对吧!

 关于串口通信的若干问题

上面我们提到,为了回应你发送的“Hello”信息,设备会发送回“Hi”信息,然后你将读取这个信息。但是现在的问题是你不知道设备会在什么时候做出回应,或者设备更本就不会回应,你应该在何时开始对串口进行读操作。其中一个方案是一旦你调用了WriteFile,你立即调用ReadFile。如果没有数据可读取,那么你得接着进行读操作。这会导致所谓的循环检查--循环检查串口,看是否有数据传入。这种模式看上去的确不是一种好模式。如果系统能够在数据接入时以某种方式通知你,并且你只在那时调用ReadFile,岂不是更好。这种模式以事件驱动,正符合Windows编程的一般方法论。幸好,这种模式是可能实现的。

串口通信的另外一个问题是,既然它们是两个设备进行通信,那么这两个设备就必须在如何通信上达成协议。每一边都必须遵守一定的协议来工作。既然实现通信的正是串口,我们要配置的就是串口。以下的API函数就是要达到这一目的:

BOOL SetCommState( HANDLE hFile, LPDCB lpDCB);

第一个参数是COM口的句柄,第二个参数是就是所谓的设备控制块。DCB是一个结构,这个结构在winbase.h中被定义,它包含有28个成员变量。例如,我们需要COM口工作的波特率,我们就需要设置它的成员变量BaudRate。波特率一般在9600bps。但是两个设备必须在同一波特率下工作。同样,如果你想进行奇偶校验,你需要设置它的成员变量Parity。并且,两个设备必须使用同一个奇偶校验。它的部分成员变量是保留的且必须被设置为0。以下的代码是获取当前的dcb,并对部分字段进行设置:

DCB dcb = {0};
dcb.DCBlength = sizeof(dcb);
if (!GetCommState(m_hCommPort, &dcb))
{
    TRACE("Failed to Get Comm State Reason: %d", GetLastError());
     return;
}

dcb.BaudRate = dwBaudRate;
dcb.ByteSize  = byByteSize;
dcb.Parity = byParity;

if(!SetCommState(m_hCommPort, &dcb))
{
    TRACE("Failed to Set Comm State Reason:%d", GetLastError());
    ASSERT(0);
}


在大多数情况下,你不需要设置DCB结构中的其它字段。但是如果你要对其进行设置,那你得留心,因为修改这些字段值会影响串口的工作。所以当你修改其值时,你得确信你所做的修改。

事件驱动

回到我们开始时遇到的读取数据的问题。如果我们不想通过循环检查COM口来获取数据,那么我们就得拥有某种事件机制。值得庆幸的是,有一个途径允许你要求系统在特定的事件发生时通知你。调用的API函数:

BOOL SetCommMask(HANDLE hHandle, DWORD dwEvtMask)

第一个参数是COM口的句柄,第二个参数是用于设置你所关心的事件掩体。需要在列表中设置的事件取决于应用程序的需要。简而言之,比如,我们关心一个字符什么时候到达串口,那么我们就将掩体设置为EV_RXCHAR作为事件掩体。同样,如果我们想知道什么时候所有的数据发送完毕,我们要将事件掩体设置为EV_TXEMPTY。那么,得出调用方式为:

SetCommMask(m_hCommPort, EV_RXCHAR | EV_TXEMPTY);

在这里,有趣的事情是,虽然我们告诉系统我们关心的事件,但是我们并没有告诉系统当这些事件发生后,系统该做什么。就像系统是如何让我们知道特定的事件发生了一样,很明显,我们需要的是回调机制。但是这个机制没有现成的。此时,事情变得有点狡猾。为了让系统通知我们通信事件什么时候发生,我们得调用WaitCommEvent。这个函数将等待SetCommMask中设置的事件。但是如果你再仔细思考一下,现在看起来我们又从事件通知机制回到开始的循环检查机制。WaitCommEvent将阻塞线程知道有事件产生。那么我们为什么要使用他呢?答案就在于重叠IO。以下是WaitCommEvent的函数原型:

BOOL WaitCommEvent(HANDLE hCommPort, LPDWORD dwEvtMask, LPOVERLAPPED lpOverlapped);

第三个参数就是关键。

你可以把重叠IO看成是异步IO。不论你什么时候执行函数调用,设置重叠IO的结构,那都是在对当前的IO进行操作,但是如果你不能立即完成这个操作,那么告诉我你什么时候能够完成它。让系统告诉你完成操作完成的方法是设置Kenerl的事件对象,而这个对象是lpOverlapped结构的一部分。那么你说要做的就是创建一个线程,通过WaitForSingleObject API函数,让这个线程等待事件对象。下面是overlapped结构:

typedef struct _OVERLAPPED{
    DWORD Internal;
    DWORD InternalHigh;
    DWORD Offset;
    DWORD OffsetHigh;
    HANDLE hEvent;
} OVERLAPPED, *LPOVERLAPPED;

其中,最后一个参数就是需要你设置的事件处理句柄。

你可以将重叠IO设想成异步IO。无论什么时候调用函数设置重叠结构,那都意味着执行当前的调用操作,但是如果你不能立即结束调用,那请告诉我你什么时候完成对IO的调用操作。让系统告诉你IO操作完成的方发是设置Kenerl的事件对象,事件对象是lpOverlapped结构的一部分。所以你所要做的就是创建一个线程,通过API函数WaitForSingleObject,让这个线程等待事件对象的触发。以下是重叠结构:

typedef struct _OVERLAPPED{
    DWORD Internal;
    DWORD InternalHigh;
    DWORD Offset;
    DWORD OffsetHigh;
    HANDLE hEvent;
}  OVERLAPPED, *LPOVERLAPPED;

最后一个参数就是你需要设置的事件句柄。这个事件通常是手动设置的。当你调用WaitCommEvent时,传入一个重叠结构作为最后一个参数。而此时系统并不会完成调用,这就意味着当前没有数据到达串口,因此WaitCommEvent会立即返回,只是返回值为FALSE。如果你调用GetLastError,你会获得错误代码ERROR_IO_PENDING,它的意思是调用被接受,但是还没有数据到达串口。同时,这也意味着无论何时有数据到达串口,系统都会设置传入重叠结构中的hEvent。

在当前的案例中,既然我们关心的是不止一个事件,我们需要检查我们通过调用GetCommMask获得了什么事件,并且还要检查每个事件的DWORD类型参数。我们通过以下伪代码来进行进一步分析(你可以从COM口读取数据,重置事件,再次调用WaitCommEvent等等)

unsigned __stdcall CSerialCommHelper::ThreadFn(void *pvParam)
{
    OVERLAPPED ov;
    menset(&ov, sizeof(ov));
    ov.hEvent = CreateEvent(0, true, 0, 0);
    HANDLE arHandles[2];
    arHandles[0] = apThis ->m_hThreadTerm;//获取当前的线程句柄

    DWORD dwWait;
    SetEvent(apThis ->m_hThreadStarted);

     while (abContinue)
    {
        BOOL abRet = WaitCommEvent(apThis -> m_hCommPort, &dwEventMask, &ov);
        if (!abRet) ASSERT(GetLastError() == ERROR_IO_PENDING);
        
        arHandles[1] = ov.hEvent;

        dwWait = WaitForMultipleObjects(2,arHandles, FALSE, INFINITE);

        switch (dwWait)
        {
        case WAIT_OBJECT_0;
            {
                _endthreadex(1);
            }
            break;
        case WAIT_OBJECT_0 + 1;
            {
                DWORD dwMask;
                if (GetCommMask(apThis -> m_hCommPort, &dwMask))
                {
                        if  (dwMask & EV_TXEMPTY) TRACE("DATA Sent");
                        ResetEvent(ov.hEvent);
                        continue;
                }
                else
                {
                    //读取数据,并重置事件
                }
            }
        }//switch
    }//while

return 0;   
}

上面的伪代码是用重叠IO工作的简单例子。

一旦我们接受到了有数据抵达的消息后我们就得读取数据。在这里值得注意的是,当数据抵达串口时,它被拷贝到系统缓存。只有当你用系统API函数,如ReadFile读取它之后,该系统缓存才会被清空。象其它缓存一样,系统缓存的空间也是有限的。所以,如果你不尽快从系统缓存中将数据读出,只要有其它数据达到,系统缓存很快就会被塞满。 那么对于接下来到达的数据会发生什么了,这取决于SetCommState的参数配置。通常情况下,应用程序在应用层要做一些握手操作,但是你也可以通过配置,让串口在缓存塞满事件发生后,不再接受任何数据。但是这已经超出了本文的讨论范畴。在可能的情况下,让应用程序在应用层进行握手检查,即在收到第一数据块的Ok回应之前,不再发送下一数据块。这种握手检查的执行,一般通过ACK/NAK 和ENQ协议完成。

为了让我们能够读取数据,我们要使用API函数ReadFile,设置读取数据的长度。比如,我们正在监视数据的到达,并且有长度为10的字符串到达了串口。只要第一个字符到达了串口,系统就会设置重叠结构的事件对象(ov.hEvent),并跳出WaitForSingleObject,并且返回。那么我们应该读取多少数据,1字节还是10字节?其工作原理如下:

 当一个或多个字符到达串口时,重叠结构的事件对象只被设置一次。例如,现在你进行了一次读操作,并且读取了一个字符。当你读取完成后,你最后要重置重叠结构的事件对象。现在,你可能会想起WaitCommEvent,但它返回的是false,因为没有新的字符到达。因此,你不可能再读取更多的字符串。现在又有其它字符到达,系统会设置重叠结构的事件对象,然后你读取一个或多个字符串,但是现在所读取的数据实际上是上次达到的数据。很明显,这里还存在问题。

那么如何解决这个问题。最容易的解决方法是当你获得了一个字符到达的消息,你就要读取串口的所有字符串。接下来还有问题,读取多少呢?答案是通过循环读取所有的字符,以下是通过ReadFile读取字符的伪代码:

WaitCommEvent(m_hCommPort, &dwEventMask, &ov);
if (WaitForSingleObject(ov.hEvent, INFINITE) == WAIT_OBJECT_0)
{
    char szBuf[100];
    memset(szBuf, sizeof(szBuf));
    do
    {
        ReadFile(hPort, szBuf, sizeof(szBuf), &dwBytesRead, &ov);        
    } while (dwBytesRead >0);
}

下面介绍一下ReadFile:

BOOL ReadFile( HANDLE hFile, //文件句柄
                                LPVOID lpBuffer, //数据缓存
                                DWORD nNumberOfBytesToRead,//读取字节的长度
                                LPDWORD lpNumberOfBytesRead, //读取的字节数
                                LPOVERLAPPED lpOverlapped)//重叠结构缓存

第一个参数是串口句柄,最后一个参数是重叠结构。当然,我们要创建一个手动重置的事件,并把重叠结构作为参数传给ReadFile函数。

正如你看的的那样,ReadFile的参数nNumberOfBytesRead返回读取的字节数。如果没有数据留下,那么nNumberOfBytesRead返回0。比如说,到达了11字节,而你在第一次循环中读取了10字节。在第一次循环中,nNumberOfBytesRead返回10,在第二次循环中,nNumberOfBytesRead返回1,在第三次循环中,返回0,然后你会让程序挑出while循环。 你可以通过这种方式读取所有数据。在这种方法中,你会发现我们并没有用到重叠结构的优势,但是我们仍然将它作为参数传给ReadFile,那是因为我们的COM口是通过重叠方式打开的。

最后,如果你要向其它设备发送信息,你只需要调用WriteFile。WriteFile就不必在这里讨论了。

在我们继续讨论的时候,还有一件事情值得让我们注意,那就是通信超时。让各个部分运转起来,设置超时很重要。设置超时的API:

SetCommTimeouts( HANDLE hCommPort, LPCOMMTIMEOUTS lpCommTimeOuts);

typedef struct _COMMTIMEOUTS{
    DWORD ReadIntervalTimeout;
    DWORD ReadTotalTimeoutMultiplier;
    DWORD ReadTotalTimeoutConstant;
    DWORD WriteTotalTimeoutMultiplier;
    DWORD WriteTotalTimeoutConstant;
} COMMTIMEOUTS, * LPCOMMTIMEOUTS;

COMMTIMEOUTS是一个结构,在MSDN中可以查询到它的更多信息。“...ReadTotalTimeoutConstant和ReadTotalTimeoutMultiplier的值为0,意味着读操作立即结束,返回读取的数据,尽管没有接受到数据...”

这个结构的设置正是我们所需要的。因为我们不希望在调用ReadFile时,在没有数据的情况下,程序被WaitCommEvent卡住。

然后,是下面的代码:

COMMTIMEOUTS timeouts;
timeouts.ReadIntervalTimeout = MAXDWORD;
timeouts.ReadTotalTimeoutMultiplier = 0;
timeouts.ReadTotalTimeoutConstant = 0;
timeouts.WriteTotalTimeoutMultiplier = 0;
timeouts.WriteTotalTimeoutConstant = 0;

if(!SetCommTimeouts(m_hCommPort, &timeouts))
{
    TRACE("Error Setting time-outs %d", GetLastError());
    ASSERT(0);
    return;

 

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多