本文在撰写时参考了zwhxz的博客,博主的思路很清晰,可惜没有具体实现代码,所以本文在该博客的基础上进行了扩充并提供了实现代码。zwhxz的博客网址:http://www.cnblogs.com/zahxz/archive/2012/12/24/2830535.html
在windows系统,windows处理串口和其他通信设备都是作为文件来处理的。串口的处理包括四个阶段:打开阶段、串口的初始化、从串口读取和写入数据以及串口的关闭。本文分析四个阶段串口完成的工作,并提供具体代码,将对串口的操作封装成一个单例类,声明如下:
#ifndef SERIALSINGLETON_H_
#defineSERIALSINGLETON_H_
#include<Windows.h>
classSerialSingleton
{
public:
~SerialSingleton();
staticSerialSingleton& getInstance(); //获取串口对象,单例模式下,每个串口只有一个类对象
bool openPort(); //打开串口
bool initPort(); //串口初始化
int readFromPort(void* buff, DWORD size,unsignedint timeout); //从串口读数据
bool writeToPort(constvoid* buff,DWORD size,unsignedint timeout);//写数据到串口
bool closePort(); //关闭串口
private:
SerialSingleton(); //将构造函数声明为私有,单例模式时常用方式
SerialSingleton(constSerialSingleton& ref); //只声明不定义,防止调用隐式复制构造函数
SerialSingleton& operator = (constSerialSingleton& ref);//只声明不定义,防止调用隐式赋值函数
HANDLE m_ucom; //串口句柄
};
#endif
(1)打开串口
在使用串口前,需要打开串口,可以使用CreateFile函数打开串口,CreateFile有两种形式CreateFileA和CreateFileW,使用ASCII码时用CreateFileA,使用Unicode码时使用CreateFileW,系统是没有CreateFile函数的。以CreateFileA为例,CreateFileA返回一个HANDLE句柄,该句柄在随后的操作中会被使用到。CreateFileA函数原型如下:
CreateFileA(
_In_LPCSTR lpFileName,
_In_DWORD dwDesiredAccess,
_In_DWORD dwShareMode,
_In_opt_LPSECURITY_ATTRIBUTES lpSecurityAttributes,
_In_DWORD dwCreationDisposition,
_In_DWORD dwFlagsAndAttributes,
_In_opt_HANDLE hTemplateFile
);
其中,lpFileName对应串口名,如“COM24”;dwDesiredAccess指定对串口的访问权限,GENERIC_READ表示读取权限,GENERIC_WRITE表示写入权限,GENERIC_READ | GENERIC_WRITE表示读写权限;dwShareMode表示共享模式,设为0表示不共享;lpSecurityAttributes指向一个SECURITY_ATTRIBUTES结构指针,定义了文件安全属性;dwCreationDisposition指定文件存在和不存在时如何操作,有五个值:CREATE_NEW、CREATE_ALWAYS、OPEN_EXISTING、OPEN_ALWAYS、TRUNCATE_EXISTING,打开串口设备时,选择OPEN_EXISTING,表示打开时该串口必须存在,不然函数调用失败;dwFlagsAndAttributes指定文件属性和标志位;hTemplateFile为一个文件或设备句柄,表示按这个参数给出的句柄为模板创建文件。函数调用成功返回true,否则,返回false。
打开串口设备的程序实现如下:
bool SerialSingleton::openPort()
{
// open serial port
if (m_ucom !=INVALID_HANDLE_VALUE)
{
returntrue;
}
std::string com_port ="\\\\.\\COM24";//打开窗口24
m_ucom =CreateFileA(com_port.c_str(),GENERIC_READ |GENERIC_WRITE, 0,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);
if (m_ucom ==INVALID_HANDLE_VALUE)
{
return false;
}
return true;
}
(2)串行口的初始化
打开串口后需要对串口的一些参数进行初始化,包括设置串口设备控制块DCB的参数、设置串口缓冲区大小、清除缓存区中数据。
A.获取串口当前参数
一般先获取串口当前参数,然后在修改。获取串口当前设备控制块参数通过GetCommState函数,函数原型如下:
GetCommState(
_In_ HANDLE hFile,
_Out_LPDCB lpDCB
);
其中,hFile为通过CreateFileA函数打开的设备句柄m_ucom,第二个参数指向设备控制块DCB,DCB为一个结构体,用来设置串口的众多参数,比如串口波特率等。函数调用成功返回true,否则,返回false。获取串口参数后,可以通过DCB变量来修改串口设备控制块的参数,比如波特率等了。
B.设置串口参数
修改后的串口设备控制块参数,可以通过SetCommState函数设到串口设备控制块,其函数原型如下:
SetCommState(
_In_HANDLE hFile,
_In_LPDCB lpDCB
);
因此,可以通过GetCommState函数获得当前串口设备控制块DCB,然后修改DCB,再通过SetCommState来配置串口。函数调用成功返回true,否则,返回false。
C.设置串口接收和发送缓冲区大小
当一个串口被打开时,可以为该串口分配一个发送缓冲区和一个接收缓冲区。串口发送缓冲区和接收缓冲区的配置可以由函数SetupComm实现。如果不调用SetupComm,系统会为该串口分配默认大小的发送缓冲区和接收缓冲区。如果对缓冲区的大小没有特别的需求,则不需要调用该函数进行设置。SetupComm函数原型如下:
SetupComm(
_In_HANDLE hFile,
_In_DWORD dwInQueue,
_In_DWORD dwOutQueue
);
其中hFile是由CreateFile函数返回指向已打开串口的句柄,本文为m_ucom。参数dwInQueue和dwOutQueue分别指定应用程序推荐使用的接收缓冲区和发送缓冲区的大小。如果不设置则采用默认值,没有特别需求的情况下,不需要设置。函数调用成功返回true,否则,返回false。
D.清空发送和接收缓冲区
在进行串口发送和接收数据操作之前,最好使用PurgeComm函数将串行口发送缓冲区和接收缓冲区中的数据清楚干净。PurgeComm函数原型如下:
PurgeComm(
_In_HANDLE hFile,
_In_DWORD dwFlags
);
参数hFile是由CreateFile函数返回指向已打开串行口的句柄,本文为m_ucom。参数dwFlags指明执行的动作。如果dwFlags为PURGE_TXCLEAR,则通知系统清空发送缓冲区;如果dwFlags为PURGE_RXCLEAR,则通知系统清空接收缓冲区;如果需要将发送缓冲区和接收缓冲区全部清空,可以把dwFlags设置为PURGE_TXCLEAR|PURGE_RXCLEAR。如果PurgeComm函数调用成功返回true,否则,返回false。初始化串口程序如下:
bool SerialSingleton::initPort()
{
// initilize opend serial port
if (!openPort())
{
printf("open serialfailed!/n");
return false;
}
DCB dcb;
memset(&dcb, 0,sizeof(dcb));
GetCommState(m_ucom, &dcb);
dcb.BaudRate = 460800;
dcb.ByteSize = 8;
dcb.StopBits =ONESTOPBIT;
dcb.fParity =FALSE;
dcb.fNull =FALSE;
dcb.Parity =NOPARITY;
// set serial port device parameter
if (!SetCommState(m_ucom, &dcb))
{
printf("Set serialport error:%d!!/n", GetLastError());
return false;
}
// set serial port receive buffer andsend buffer size
/*if (!SetupComm(m_ucom, 1048576,1048576))
{
printf("Set serialreceive buffer and send buffer failed!, error %d/n", GetLastError());
return 0;
}*/
// clear serial port receive bufferand send buffer
if (!PurgeComm(m_ucom,PURGE_TXCLEAR |PURGE_RXCLEAR))
{
printf("clear receivebuffer and send buffer failed!, error %d/n", GetLastError());
return 0;
}
return true;
}
(3)从串口读取和写入数据
A.接收串口发送过来的数据
接收串口发送来的数据主要通过ReadFile函数,其原型如下:
ReadFile(
_In_HANDLE hFile,
_Out_writes_bytes_to_opt_(nNumberOfBytesToRead,*lpNumberOfBytesRead)__out_data_source(FILE)LPVOID lpBuffer,
_In_DWORD nNumberOfBytesToRead,
_Out_opt_LPDWORD lpNumberOfBytesRead,
_Inout_opt_LPOVERLAPPED lpOverlapped
);
参数hFile是由CreateFileA函数返回指向已打开串口的句柄,本文为m_ucom。_Out_writes_bytes_to_opt_(nNumberOfBytesToRead,*lpNumberOfBytesRead)为可选参数。nNumberOfBytesToRead指定从串口中读取的字节数,lpNumberOfBytesRead表示实际读取到的字节数,lpOverlapped为OVERLAPPED结构体指针,如果CreateFileA时没有指定文件的标志位为FILE_FLAG_OVERLAPPED来创建hFile时,一般置为NULL。函数调用成功返回true,否则,返回false。在用ReadFile读取串口数据时,如果一直没读到数据,程序会一直停留在这,因此需要设置一个超时,超过这个时间,程序跳出等待。COMMTIMEOUTS是用ReadFile和WriteFile来读写串口时使用的参数,其原型如下:
typedefstruct_COMMTIMEOUTS {
DWORD ReadIntervalTimeout; /* Maximum time between read chars.*/
DWORD ReadTotalTimeoutMultiplier; /* Multiplier of characters. */
DWORD ReadTotalTimeoutConstant; /* Constant in milliseconds. */
DWORD WriteTotalTimeoutMultiplier; /* Multiplier of characters. */
DWORD WriteTotalTimeoutConstant; /* Constant in milliseconds. */
}COMMTIMEOUTS,*LPCOMMTIMEOUTS;
COMMTIMEOUTS结构的成员都以毫秒为单位。读串口数据时,一般设置一个总的读时间常量ReadTotalTimeoutConstant就好,通过GetCommTimeOuts函数来获取当前COMMTIMEOUTS结构,修改结构成员的值后,通过SetCommTimeOuts,用修改后的参数设置超时。
读串口数据的函数实现如下:
int SerialSingleton::readFromPort(void*buff,DWORDsize,unsignedinttimeout)
{
// read data from serial
COMMTIMEOUTS readCommTimeOuts;
memset(&readCommTimeOuts, 0,sizeof(readCommTimeOuts));
readCommTimeOuts.ReadTotalTimeoutConstant=timeout;
SetCommTimeouts(m_ucom,&readCommTimeOuts);
DWORD dwReadBytes = 0;
if (ReadFile(m_ucom,buff,size, &dwReadBytes,NULL))
{
return dwReadBytes;
}
else
{
printf("read data fromserial port failed, error %d/n", GetLastError());
return -1;
}
}
B.向串口写入数据
向串口写入数据主要通过WriteFile函数,其原型如下:
WriteFile(
_In_HANDLE hFile,
_In_reads_bytes_opt_(nNumberOfBytesToWrite)LPCVOID lpBuffer,
_In_DWORD nNumberOfBytesToWrite,
_Out_opt_LPDWORD lpNumberOfBytesWritten,
_Inout_opt_LPOVERLAPPED lpOverlapped
);
各参数与ReadFile函数的参数类似,不同的是nNumberOfBytesToWrite指定向串口中写入的字节数,lpNumberOfBytesWritten为实际向串口中写入的字节数。函数调用成功返回true,否则,返回false。与读串口数据的实现类似,向串口写入数据时也设置一个超时,向串口写入数据的实现如下:
bool SerialSingleton::writeToPort(constvoid*buff,DWORDsize,unsignedinttimeout)
{
// write data to port serial
COMMTIMEOUTS writeCommTimeOuts;
memset(&writeCommTimeOuts, 0,sizeof(writeCommTimeOuts));
writeCommTimeOuts.WriteTotalTimeoutConstant=timeout;
SetCommTimeouts(m_ucom,&writeCommTimeOuts);
DWORD dwWriteBytes = 0;
if (WriteFile(m_ucom,buff,size, &dwWriteBytes,NULL))
{
return true;
}
else
{
printf("write data toserial port failed, eror %d/n", GetLastError());
return false;
}
}
(4)串行口的关闭
在使用完串口后,需要关闭该串口,不然串口会一直处于打开状态,导致其他程序不能使用。关闭串口使用closePort()函数,closePort()实现如下:
bool SerialSingleton::closePort()
{
if (m_ucom != INVALID_HANDLE_VALUE)
{
return CloseHandle(m_ucom);
}
return true;
}
CloseHandle原型如下:
CloseHandle(
_In_HANDLE hObject
);
hObject为打开串口的句柄,本文为m_ucom,函数调用成功返回true,否则,返回false。
最后给出构造函数、析构函数和SerialSingleton& SerialSingleton::getInstance()
函数的实现,如果需要使用SerialSingleton类中各函数,只需调用getInstance()来获得类对象,再调用各个函数,例如调用openPort函数:SerialSingleton::SerialSingleton().openPort()。
SerialSingleton::SerialSingleton()
:m_ucom(INVALID_HANDLE_VALUE)
{
}
SerialSingleton::~SerialSingleton()
{
}
SerialSingleton&SerialSingleton::getInstance()
{
staticSerialSingleton serial;
return serial;
}