C++串口通信编程(异步通信)

时间:2024-02-19 20:28:44

串口通信一般分为同步和异步两种方式,本博客主要讲述异步通信程序的编写,其编程步骤主要分为四步骤:

一、打开串口 
Win32系统把文件的概念进行了扩展。无论是文件、通信设备、命名管道、邮件槽、磁盘、还是控制台,都是用API函数CreateFile来打开或创建的。本程序串口类中打开串口的函数定义如下:

bool My_Com::Open_Com(LPCTSTR  Port)
{
    hCom = CreateFile(
        Port, //将要打开的串口逻辑名
        GENERIC_READ | GENERIC_WRITE, //允许读和写
        0, //指定共享属性,由于串口不能共享,该参数必须置为0,独占方式
        NULL,//引用安全性属性结构,缺省值为NULL
        OPEN_EXISTING, /创建标志,对串口操作该参数必须置为OPEN_EXISTING
        FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, //属性描述,此处指定该串口异步
        NULL //对串口而言该参数必须置为NULL);
    if (hCom == INVALID_HANDLE_VALUE)
    {
        printf ("打开串口失败!\n");
        return FALSE;
    }
    else
    {
        printf("打开串口成功!\n");
    }
    return TRUE;
}
注意:FILE_ATTRIBUTE_NORMAL 和 FILE_FLAG_OVERLAPPED 均代表异步通信。也可以借用CreateFile函数加上for循环实现对外设驱动的扫描以自动找出插入的串口号,其代码实现如下:

GetCom()
{
    MyComm.hCom = INVALID_HANDLE_VALUE;
    BOOL ret = false;
    int j = 0;
    //逐一进行COM   端口检测
    printf("正在搜寻当前可用端口...\n");
    for (int i = 1; i <= 10; i++)
    {
        string comname = "COM" + to_string(i);
        MyComm.hCom = CreateFile(stringToLPCWSTR(comname), // 打开串口
            GENERIC_READ | GENERIC_WRITE,  //读写方式
            0, //不能共享
            NULL, //安全属性,一般不用设为NULL
            OPEN_EXISTING,   //打开已存在的设备
            FILE_ATTRIBUTE_NORMAL,  //普通文件属性
            NULL);       //无模板
        if (MyComm.hCom != INVALID_HANDLE_VALUE)
        {
            j++;
            printf("端口 COM %d 可用\n", i);
            ret = CloseHandle(MyComm.hCom);
            if (!ret) printf("关闭串口失败!!");
        }
    }
    if (!j)
    {
        printf("无可用端口!!\n");
        return FALSE;
    }
    else return TRUE;
}


二、配置串口
本部分主要是一些初始化配置:

1、DCB结构相关参数的配置(波特率、数据位数、奇偶校验和停止位数等信息),调用GetCommState函数获取串口的初始配置,然后先修改DCB结构,再调用SetCommState函数设置串口。

2、COMMTIMEOUTS结构串口读写超时参数设置(单位:毫秒;若设置为0,表示该参数不起作用)
ReadIntervalTimeout:两字符之间最大的延时,当读取串口数据时,一旦两个字符传输的时间差超过该时间,读取函数将返回现有的数据。在ReadFile操作期间,时间周期从第一个字符接收到算起。如果收到的两个字符之间的间隔超过该值,ReadFile操作完毕并返回所有缓冲数据。如果值为MAXDWORD,并且ReadTotalTimeoutConstant和ReadTotalTimeoutMultiplier两个值都为0,   则指定读操作携带已经收到的字符立即返回,即使没有收到任何字符。如果ReadIntervalTimeout为0,则该值不起作用。
ReadTotalTimeoutMultiplier:读取每字符间的超时。 指定累积值,用于计算读操作时的超时总数。对于每次读操作,该值与所要读的字节数相乘。
ReadTotalTimeoutConstant:一次读取串口数据的固定超时。在一次读取串口的操作中,其超时为 ReadTotalTimeoutMultiplier乘以读取的字节数再加上 ReadTotalTimeoutConstant。将ReadIntervalTimeout设置为MAXDWORD,并将 ReadTotalTimeoutMultiplier 和ReadTotalTimeoutConstant设置为0,表示读取操作将立即返回存放在输入缓冲区的字符。
WriteTotalTimeoutMultiplier:写入每字符间的超时。
WriteTotalTimeoutConstant:一次写入串口数据的固定超时。在一次写入串口的操作中,其超时为WriteTotalTimeoutMultiplier乘以写入的字节数再加上 WriteTotalTimeoutConstant。
间隔超时=ReadIntervalTimeout
总超时   = ReadTotalTimeoutMultiplier  *  字节数   +   ReadTotalTimeoutConstant 
           这里以串口读取事件为例进行详细的分析,其过程分两个阶段:
           第一个阶段是:串口执行到ReadFile函数时,串口还没有开始传输数据,所以串口缓冲区的第一个字节是没有装数据的,这时候总超时起作用,如果在总超时时间内没有进行串口数据的传输,ReadFile函数就返回,当然 没有读取到任何数据。而且,间隔超时并没有起作用。
          第二阶段:假设总超时为20秒,程序运行到ReadFile,总超时开始从0 计时,如果在计时到达10秒时,串口开始了数据的传输,那么从接收的第一个字节开始,间隔超时就开始计时,假如间隔超时为1ms,那么在读取完第一个字节后,串口开始等待1ms,如果1ms之内接收到了第二个字节,就读取第二个字节,间隔超时重置为0并计时,等待第三个字节的到来,如果第三个字节到来的时间超过了1ms,那么ReadFile函数立即返回,这时候总超时计时是没到20秒的。如果在20秒总计时时间结束之前,所有的数据都遵守数据间隔为1ms的约定并陆陆续续的到达串口缓冲区,那么就成功进行了一次串口传输和读取;如果20秒总计时时间到,串口还陆陆续续的有数据到达,即使遵守字节间隔为1ms的约定,ReadFile函数也会立即返回,这时候总超时就起作用了。
          总结起来,总超时在两种情况下起作用:一是串口没进行数据传输,等待总超时时间那么长ReadFile才返回(即非正常数据传输);二是数据太长,总超时设置太短,数据还没读取完就返回了(读取的数据是不全的)。间隔超时触发的条件:在总超时时间内且串口进行了数据的传输。

3、利用SetUpComm、PurgeComm两个函数分别设置输出输出缓冲区的大小并进行清空处理
SetupComm参数解释:dwInQueue指定输入缓冲区的大小(字节);dwOutQueue指定输出缓冲区的大小(字节)。
PurgeComm参数解释:PURGE_TXABORT 终止所有正在进行的字符输出操作,PURGE_RXABORT 终止所有正在进行的字符。输入操作PURGE_TXCLEAR 设备驱动程序清除输出缓冲,PURGE_RXCLEAR 设备驱动程序清除输入缓冲区。

4、利用CreateEvent函数创建读写及等待的操作事件,用于读写函数中做判断使用。

5、利用SetCommMask函数设置要监控的事件,而WaitCommEvent函数是等待串口通信事件的发生放在读函数中。

6、创建读取线程。

bool My_Com::Config_Com()
{
    SetupComm(hCom, 1024, 1024); //输入缓冲区和输出缓冲区的大小都是1024
    
        DCB dcb;
    GetCommState(hCom, &dcb);
    dcb.BaudRate = 9600; //波特率为9600
    dcb.ByteSize = 8; //每个字节有8位
    dcb.Parity = NOPARITY; //无奇偶校验位
    dcb.StopBits = TWOSTOPBITS; //两个停止位
    SetCommState(hCom, &dcb);
    
        COMMTIMEOUTS TimeOuts; //设定读超时
    TimeOuts.ReadIntervalTimeout = MAXDWORD;
    TimeOuts.ReadTotalTimeoutMultiplier = 0;
    TimeOuts.ReadTotalTimeoutConstant = 0; //设定写超时
    TimeOuts.WriteTotalTimeoutMultiplier = 500;
    TimeOuts.WriteTotalTimeoutConstant = 2000;
    SetCommTimeouts(hCom, &TimeOuts); //设置超时
 
    PurgeComm(hCom, PURGE_TXCLEAR | PURGE_RXCLEAR);
 
    m_ovRead.hEvent = CreateEvent(NULL, false, false, NULL);
    m_ovWrite.hEvent = CreateEvent(NULL, false, false, NULL);
    m_ovWait.hEvent = CreateEvent(NULL, false, false, NULL);
 
    //SetCommMask设置要监控的事件 
    //EV_RXCHAR:输入缓冲区中已收到数据,即接收到一个字节并放入输入缓冲区。
    //EV_ERR:线路状态错误,包括了CE_FRAME / CE_OVERRUN / CE_RXPARITY 3种错误。
    SetCommMask(hCom, EV_ERR | EV_RXCHAR);
 
    //_beginThreadex创建读取线程  
    m_Thread = (HANDLE)_beginthreadex(NULL, 0, &My_Com::ComRecv, this, 0, NULL);
    m_IsOpen = true;
 
    return TRUE;
}


三、串口读写
写函数ComWrite(发送数据)中主要调用了WriteFile函数参数:HANDLE hFile文件句柄,LPCVOID lpBuffer数据缓存区指针DWORD  nNumberOfBytesToWrite你要写的字节数,LPDWORD lpNumberOfBytesWritten用于保存实际写入字节数的存储区域的指针LPOVERLAPPED lpOverlappedOVERLAPPED结构体指针

bool My_Com::ComWrite(LPBYTE buf, int &len)
{
    BOOL rtn = FALSE;
    DWORD WriteSize = 0;   //DWORD 代表 unsigned long
    PurgeComm(hCom, PURGE_TXCLEAR | PURGE_TXABORT);
    m_ovWait.Offset = 0;
    rtn = WriteFile(hCom, buf, len, &WriteSize, &m_ovWrite);
    if (FALSE == rtn && GetLastError() == ERROR_IO_PENDING)//后台读取
    {
        //等待数据写入完成
        printf("已发送 :");
        for (int i = 0; i < len; i++)
            printf("%d ", buf[i]);
        printf("\n");
    }
    return rtn != FALSE;
}


读函数ComRecv(接收数据)的解释已在代码区详细备注,其函数定义如下:

unsigned int __stdcall My_Com::ComRecv(void* LPParam)
{
    My_Com *obj = static_cast<My_Com*>(LPParam);
    DWORD WaitEvent = 0, Bytes = 0;
    BOOL Status = FALSE;
    BYTE ReadBuf[4096];
    DWORD Error;
    COMSTAT cs = { 0 };
    int i;
    while (obj->m_IsOpen)
    {
        WaitEvent = 0;
        obj->m_ovWait.Offset = 0;
        Status = WaitCommEvent(obj->hCom, &WaitEvent, &obj->m_ovWait);
        /*
        WaitCommEvent等待串口通信事件的发生
        用途:用来判断用SetCommMask()函数设置的串口通信事件是否已发生。
        原型:BOOL WaitCommEvent(HANDLE hFile,LPDWORD lpEvtMask,LPOVERLAPPED lpOverlapped);
        参数说明:
        -hFile:串口句柄
        -lpEvtMask:函数执行完后如果检测到串口通信事件的话就将其写入该参数中。
        -lpOverlapped:异步结构,用来保存异步操作结果。
        */
 
        //GetLastError()函数返回ERROR_IO_PENDING,表明串口正在进行读操作
        if (FALSE == Status && GetLastError() == ERROR_IO_PENDING)
        {
            // GetOverlappedResult函数的最后一个参数设为TRUE,函数会一直等待,直到读操作完成或由于错误而返回。
            Status = GetOverlappedResult(obj->hCom, &obj->m_ovWait, &Bytes, TRUE);
        }
        //在使用ReadFile 函数进行读操作前,应先使用ClearCommError函数清除错误。
        ClearCommError(obj->hCom, &Error, &cs);
        if (TRUE == Status //等待事件成功
            && WaitEvent&EV_RXCHAR//缓存中有数据到达
            && cs.cbInQue > 0)//有数据        
        {
            Bytes = 0;
            obj->m_ovRead.Offset = 0;
            memset(ReadBuf, 0, sizeof(ReadBuf));
            /*
            BOOL ReadFile( 
            HANDLE hFile, //串口的句柄
            LPVOID lpBuffer,// 读入的数据存储的地址,即读入的数据将存储在以该指针的值为首地址的一片内存区
            DWORD nNumberOfBytesToRead,// 要读入的数据的字节数
            LPDWORD lpNumberOfBytesRead,// 指向一个DWORD数值,该数值返回读操作实际读入的字节数
            LPOVERLAPPED lpOverlapped // 重叠操作时,该参数指向一个OVERLAPPED结构,同步操作时,该参数为NULL
            );
            */
            Status = ReadFile(obj->hCom, &ReadBuf, 4096, &Bytes, &obj->m_ovRead);
            if (Status != FALSE)
            {
                printf("收到 :");
                for (i = 0; i < Bytes; i++)
                {
                    printf("%d ", ReadBuf[i]);
                }
                printf("\n");
            }
            //PurgeComm函数清空串口的输入输出缓冲区
            PurgeComm(obj->hCom, PURGE_RXCLEAR | PURGE_RXABORT);
        }
    }
    return 0;
}


四、关闭串口
本部分主要编写了关闭串口的函数体,其被串口类的析构函数调用,实现各开启功能的关闭。若只是关闭开启的串口号,可直接调用底层代码提供的CloseHandle函数。

void My_Com::Close_Com()
{
    m_IsOpen = false;
    if (INVALID_HANDLE_VALUE != hCom)
    {
        CloseHandle(hCom);
        hCom = INVALID_HANDLE_VALUE;
    }
    if (NULL != m_ovRead.hEvent)
    {
        CloseHandle(m_ovRead.hEvent);
        m_ovRead.hEvent = NULL;
    }
    if (NULL != m_ovWrite.hEvent)
    {
        CloseHandle(m_ovWrite.hEvent);
        m_ovWrite.hEvent = NULL;
    }
    if (NULL != m_ovWait.hEvent)
    {
        CloseHandle(m_ovWait.hEvent);
        m_ovWait.hEvent = NULL;
    }
    if (NULL != m_Thread)
    {
        WaitForSingleObject(m_Thread, 5000);//等待线程结束  
        CloseHandle(m_Thread);
        m_Thread = NULL;
    }
}