串口通信一般分为同步和异步两种方式,本博客主要讲述异步通信程序的编写,其编程步骤主要分为四步骤:
一、打开串口
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;
}
}