C++串口同步和异步的读取与串口设备编程

402次阅读

本文主要讲述如何对串口进行高效率的读写,当串口中数据到达时立即读取进行处理,以及如何将该方法运用到串口设备编程中。为了使得程序更加清晰文中的代码去除了异常处理的情况。文中加粗的文字相应的比较重要,需要多注意。当然文中会有错误,欢迎评论指正。

 

文章中代码下载地址 http://pan.baidu.com/s/1pLsP9wB

 

1、COM口WindowsAPI函数
CreateFile(“COM1”, …); //打开串口设备
SetupComm //设置串口发送接收缓存
GetCommState //配置串口、设置波特率、停止位、校验位等待
PurgeComm //清空发送接收缓存
SetCommTimeouts //设置发送接收超时
ClearCommError //清除COM口错误、查询发送和 接收缓存中的字节数
SetCommMask //设置监听事件,设置后可以调用 WaitCommEvent 等待事件,若是以同步方式打开串口此函数会清除之前触发的事件
WaitCommEvent //等待监听事件: 当 SetCommMask 注册的事件到达则会立即返回,
//如果是以同步方式打开串口需要调用 SetCommMask 清除事件,
//否则再次调用 WaitCommEvent 会立即返回 。

ReadFile(hCom, ReadBuf, ReadLen, &ReadSize, NULL) //读取缓存, 当缓存中已有ReadLen个字节数据则立即返回,没有则会一直等到
// SetCommTimeouts 中设置的超时过去则反回。
WriteFile //写入数据

以上函数具体介绍请参考微软官方文档MSDN 地址 https://msdn.microsoft.com/en-us/library/windows/desktop/aa363194(v=vs.85).aspx

2、串口的读取
读取串口时希望串口中一有数据则立即读取到结果并返回。不管是同步还是异步都有两种方式实现,都是一种利用WaitCommEvent 等待EV_RXCHAR事件,令一种利用ReadFile函数的特性( 当缓存中已有ReadLen个字节数据则立即返回)。
WaitCommEvent 方法:则先注册EV_RXCHAR事件,如果读取缓存中有数据则会触发该事件,应用程序可以得到通知然后再调用ReadFile读取。
ReadFile的方法:则需要先读取一个字节 ReadFile(hCom, buf, 1, &ReadSize, NULL),返回TRUE后则通过ClearCommError 查询有多少数据需要读取,然后再次调用ReadFile将其余的数据读取出来。

由于同步方式打开串口时调用WaitCommEvent等待时,不能在其他线程调用WriteFile进行写入操作,且WaitCommEvent无超时参数所以该方法对于同步串口读取基本无实用价值。

3、COM口同步读写

由于通过ReadFile等待数据的读取方法

打开并配置串口
HANDLE InitCOM(LPCTSTR Port)
{
HANDLE hCom = INVALID_HANDLE_VALUE;
hCom = CreateFile(Port, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING,
0/*同步方式打开串口*/, NULL);
if (INVALID_HANDLE_VALUE == hCom)
{
return INVALID_HANDLE_VALUE;
}
SetupComm(hCom, 4096, 4096);//设置缓存

DCB dcb;

GetCommState(hCom, &dcb);//设置串口
dcb.DCBlength = sizeof(dcb);
dcb.BaudRate = CBR_9600;
dcb.StopBits = ONESTOPBIT;
SetCommState(hCom, &dcb);

PurgeComm(hCom, PURGE_RXABORT|PURGE_TXCLEAR|PURGE_RXCLEAR|PURGE_TXABORT);//清空缓存

COMMTIMEOUTS ct;
//设置读取超时时间,及ReadFlie最长等待时间
ct.ReadIntervalTimeout = 0;
ct.ReadTotalTimeoutConstant = 5000;
ct.ReadTotalTimeoutMultiplier = 500;

ct.WriteTotalTimeoutMultiplier = 500;
ct.WriteTotalTimeoutConstant = 5000;

SetCommTimeouts(hCom, &ct);//设置超时

return hCom;
}

数据读取
bool ComRead(HANDLE hCom, LPBYTE buf, int &len)
{
DWORD ReadSize = 0;
BOOL rtn = FALSE;

//设置读取1个字节数据,当缓存中有数据到达时则会立即返回,否则直到超时
rtn = ReadFile(hCom, buf, 1, &ReadSize, NULL);

//如果是超时rtn=true但是ReadSize=0,如果有数据到达,会读取一个字节ReadSize=1
if (rtn == TRUE && 1 == ReadSize)
{
DWORD Error;
COMSTAT cs = {0};
int ReadLen = 0;
//查询剩余多少字节未读取,存储于cs.cbInQue中
ClearCommError(hCom, &Error, &cs);
ReadLen = (cs.cbInQue > len) ? len : cs.cbInQue;
if (ReadLen > 0)
{
//由于之前等待时以读取一个字节,所欲buf+1
rtn = ReadFile(hCom, buf+1, ReadLen, &ReadSize, NULL);
len = 0;
if (rtn)
{
len = ReadLen + 1;
}
}
}
PurgeComm(hCom, PURGE_RXABORT|PURGE_TXCLEAR|PURGE_RXCLEAR|PURGE_TXABORT);
return rtn != FALSE;
}

数据写入
bool ComWrite(HANDLE hCom, LPBYTE buf, int &len)
{
PurgeComm(hCom, PURGE_RXABORT|PURGE_TXCLEAR|PURGE_RXCLEAR|PURGE_TXABORT);
BOOL rtn = FALSE;
DWORD WriteSize = 0;
rtn = WriteFile(hCom, buf, len, &WriteSize, NULL);

len = WriteSize;
return rtn != FALSE;
}

由于同步串口WaitCommEvent等待数据读取的方法基本无实用价值,所以不讨论。

4、COM口异步读写
因为异步读取是在后台进行,数据到达一般需要单独的线程等待,所以本文采用了一个类进行说明。

WaitCommEvent等待数据读取的方法

异步读取类声明
class ComAsy
{
public:
ComAsy();
~ComAsy();
bool InitCOM(LPCTSTR Port);//打开窗口
void UninitCOM(); //关闭串口并清理

//写入数据
bool ComWrite(LPBYTE buf, int &len);

//读取线程
static unsigned int __stdcall OnRecv(void*);

private:
HANDLE m_hCom;
OVERLAPPED m_ovWrite;//用于写入数据
OVERLAPPED m_ovRead;//用于读取数据
OVERLAPPED m_ovWait;//用于等待数据
volatile bool m_IsOpen;//串口是否打开
HANDLE m_Thread;//读取线程句柄
};

ComAsy::ComAsy():
m_hCom(INVALID_HANDLE_VALUE),
m_IsOpen(false),
m_Thread(NULL)
{
memset(&m_ovWait, 0, sizeof(m_ovWait));
memset(&m_ovWrite, 0, sizeof(m_ovWrite));
memset(&m_ovRead, 0, sizeof(m_ovRead));
}

ComAsy::~ComAsy()
{
UninitCOM();
}

初始化并配置串口
bool ComAsy::InitCOM(LPCTSTR Port)
{
m_hCom = CreateFile(Port, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING,
FILE_FLAG_OVERLAPPED|FILE_ATTRIBUTE_NORMAL,//设置异步标识
NULL);
if (INVALID_HANDLE_VALUE == m_hCom)
{
return false;
}
SetupComm(m_hCom, 4096, 4096);//设置发送接收缓存

DCB dcb;
GetCommState(m_hCom, &dcb);
dcb.DCBlength = sizeof(dcb);
dcb.BaudRate = CBR_9600;
dcb.StopBits = ONESTOPBIT;
SetCommState(m_hCom, &dcb);//配置串口

PurgeComm(m_hCom, PURGE_RXABORT|PURGE_TXCLEAR|PURGE_RXCLEAR|PURGE_TXABORT);

COMMTIMEOUTS ct;
ct.ReadIntervalTimeout = MAXDWORD;//读取无延时,因为有WaitCommEvent等待数据
ct.ReadTotalTimeoutConstant = 0; //
ct.ReadTotalTimeoutMultiplier = 0;//

ct.WriteTotalTimeoutMultiplier = 500;
ct.WriteTotalTimeoutConstant = 5000;

SetCommTimeouts(m_hCom, &ct);

//创建事件对象
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(m_hCom, EV_ERR | EV_RXCHAR);//设置接受事件

//创建读取线程
m_Thread = (HANDLE)_beginthreadex(NULL, 0, &ComAsy::OnRecv, this, 0, NULL);
m_IsOpen = true;
return true;
}

写入数据,由于写入数据一般不会有太高的性能要求,所以异步写入时如果数据在后台写入,则会等待写入完成后再退出,此时相当于同步的写入。
bool ComAsy::ComWrite(LPBYTE buf, int &len)
{
BOOL rtn = FALSE;
DWORD WriteSize = 0;

PurgeComm(m_hCom, PURGE_TXCLEAR|PURGE_TXABORT);
m_ovWait.Offset = 0;
rtn = WriteFile(m_hCom, buf, len, &WriteSize, &m_ovWrite);

len = 0;
if (FALSE == rtn && GetLastError() == ERROR_IO_PENDING)//后台读取
{
//等待数据写入完成
if (FALSE == ::GetOverlappedResult(m_hCom, &m_ovWrite, &WriteSize, TRUE))
{
return false;
}
}

len = WriteSize;
return rtn != FALSE;
}

读取数据
unsigned int __stdcall ComAsy::OnRecv( void* LPParam)
{
ComAsy *obj = static_cast<ComAsy*>(LPParam);

DWORD WaitEvent = 0, Bytes = 0;
BOOL Status = FALSE;
BYTE ReadBuf[4096];
DWORD Error;
COMSTAT cs = {0};

while(obj->m_IsOpen)
{
WaitEvent = 0;
obj->m_ovWait.Offset = 0;
Status = WaitCommEvent(obj->m_hCom,&WaitEvent, &obj->m_ovWait );
//WaitCommEvent也是一个异步命令,所以需要等待
if (FALSE == Status && GetLastError() == ERROR_IO_PENDING)//
{
//如果缓存中无数据线程会停在此,如果hCom关闭会立即返回False
Status = GetOverlappedResult(obj->m_hCom, &obj->m_ovWait, &Bytes, TRUE);
}
ClearCommError(obj->m_hCom, &Error, &cs);
if (TRUE == Status //等待事件成功
&& WaitEvent&EV_RXCHAR//缓存中有数据到达
&& cs.cbInQue > 0)//有数据
{
Bytes = 0;
obj->m_ovRead.Offset = 0;
memset(ReadBuf, 0, sizeof(ReadBuf));
//数据已经到达缓存区,ReadFile不会当成异步命令,而是立即读取并返回True
Status = ReadFile(obj->m_hCom, ReadBuf, sizeof(ReadBuf), &Bytes, &obj->m_ovRead);
if (Status != FALSE)
{
cout<<“Read:”<<(LPCSTR)ReadBuf<<” Len:”<< Bytes<<endl;
}
PurgeComm(obj->m_hCom, PURGE_RXCLEAR|PURGE_RXABORT);
}

}
return 0;
}

关闭串口
void ComAsy::UninitCOM()
{
m_IsOpen = false;
if (INVALID_HANDLE_VALUE != m_hCom)
{
CloseHandle(m_hCom);
m_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;
}
}

对于异步读取一般都采用WaitCommEvent的方式等待数据,采用ReadFile的方式等待数据也可以,只需要设置一个很大的超时时间,然后通过读取1个字节等待。

5、串口设备开发
一般的串口设备都是上位机发送一个命令设备返回命令执行的结果,同步窗口读取非常适合这种模式。一般先WriteFile发送一个命令,然后ReadFile读取结果。将超时参数设为设备动作返回需要的最长时间,这样就在发送命令后知道命令的执行结果。
采用同步方式读取可以封装一个设备类,类的结构大致如下。
class Device
{
public:
Device();
~Device();
bool Init(LPCTSTR Port, ...);
void UnInit();

bool Option1(LPCTSTR Param1,...)
{
m_cs.Lock();//每一个操作前先锁定设备
WriteFile(m_hCom, ...);//发送命令

ReadFile(m_hCom, ...);//获取命令结果
m_cs.Unlock();
return true;
}
bool Option2(LPCTSTR Param1,...);
//...


//查询状态线程,每隔一段时间查询一次状态,以便知道设备是否在线还是离线
static unsigned int __stdcall QueryStatus(void*);


private:
HANDLE m_hCom;
bool m_IsOpen;
int m_DeviceStatus;

CCriticalSection m_cs;//

HANDLE m_ThreadStatus;
};

 

对于会主动向上抛数据的设备,采用异步的方式更为合适,因为异步方式会一直等待读取数据。

 

 

————————————————————————————————————————————————————————————————————————
串口异步读取的另一种方法

串口初始化, 与之前的异步方法一致,只是在设置超时是将ReadIntervalTimeout设置为2ms, 这样这样ReadFile异步读取时,会返回FALSE,并且GetLastError() == ERROR_IO_PENDING,然后调用GetOverlappedResult等待数据到达,如果有数据到达且时间超过2ms则会返回,否则会一直等待。
串口未关闭的情况下,GetOverlappedResult返回需要两个条件,一是超时或者读取的字节以到达ReadFile中指定的字节数,二是有数据到达,只有同时两个条件满足才会返回。

代码如下:
初始化代码:
bool ComAsy::InitCOM(LPCTSTR Port)
{
m_hCom = CreateFile(Port, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING,
FILE_FLAG_OVERLAPPED|FILE_ATTRIBUTE_NORMAL,//设置异步标识
NULL);
if (INVALID_HANDLE_VALUE == m_hCom)
{
return false;
}
SetupComm(m_hCom, 4096, 4096);//设置发送接收缓存

DCB dcb;
GetCommState(m_hCom, &dcb);
dcb.DCBlength = sizeof(dcb);
dcb.BaudRate = CBR_9600;
dcb.StopBits = ONESTOPBIT;
SetCommState(m_hCom, &dcb);//配置串口

PurgeComm(m_hCom, PURGE_RXABORT|PURGE_TXCLEAR|PURGE_RXCLEAR|PURGE_TXABORT);

COMMTIMEOUTS ct;
//读取延时设为2ms,这样ReadFile异步读取时,会返回FALSE,并且GetLastError() == ERROR_IO_PENDING。
//如果设为MAXDWORD,ReadFile异步读取时,会返回TRUE
//然后调用GetOverlappedResult等待数据到达,如果有数据到达且时间超过2ms则会返回,否则会一直等待。
ct.ReadIntervalTimeout = 2;
ct.ReadTotalTimeoutConstant = 0; //
ct.ReadTotalTimeoutMultiplier = 0;//

ct.WriteTotalTimeoutMultiplier = 500;
ct.WriteTotalTimeoutConstant = 5000;

SetCommTimeouts(m_hCom, &ct);

//创建事件对象
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(m_hCom, EV_ERR | EV_RXCHAR);//设置接受事件

//创建读取线程
m_Thread = (HANDLE)_beginthreadex(NULL, 0, &ComAsy::OnRecv, this, 0, NULL);
m_IsOpen = true;
return true;
}

 

读取线程代码:
unsigned int __stdcall ComAsy::OnRecv( void* LPParam)
{
ComAsy *obj = static_cast<ComAsy*>(LPParam);

DWORD Bytes = 0;
BOOL Status = FALSE;
BYTE ReadBuf[4096];
DWORD Error;
COMSTAT cs = {0};

while(obj->m_IsOpen)
{

ClearCommError(obj->m_hCom, &Error, &cs);

Bytes = 0;
obj->m_ovRead.Offset = 0;
memset(ReadBuf, 0, sizeof(ReadBuf));
Status = ReadFile(obj->m_hCom, ReadBuf, sizeof(ReadBuf), &Bytes, &obj->m_ovRead);
//数据已经到达缓存区,读取会立即返回,并返回True, 否则返回False
if (Status == FALSE && GetLastError() == ERROR_IO_PENDING)
{
//如果有数据到达 且 时间超过ReadIntervalTimeout则会返回,否则会一直等待
Status = GetOverlappedResult(obj->m_hCom, &obj->m_ovRead, &Bytes, TRUE);
}
if (FALSE != Status && Bytes > 0)
{
cout<<"Read:"<<(LPCSTR)ReadBuf<<" Len:"<< Bytes<<endl;
}
PurgeComm(obj->m_hCom, PURGE_RXCLEAR|PURGE_RXABORT);

}

return 0;
}
1
yiywain
版权声明:本文于2021-07-22转载自C++串口同步和异步的读取与串口设备编程,共计9131字。
转载提示:此文章非本站原创文章,若需转载请联系原作者获得转载授权。