Advertisement

Winows API串口编程

阅读量:

通过Windows API实现串口通信可采用两种途径:即同步通信模式与异步通信模式(亦称重叠模式)。其中,在同步模式下,API函数会在操作完成后才会返回控制,并且在多线程环境下会导致主线程不会被阻塞但仍会阻塞监听线程。相比之下,在异步模式下,API函数会立即返回并让操作后台执行以避免线程阻塞。

操作串口一般有以下几步骤:

1)获取串口

2)打开串口

3)配置串口

4)读写串口

5)关闭串口

一、获取串口

1)下面是项目中使用到的方法,使用QueryDosDevice函数查找COM口

复制代码
 BOOL CActiveReaderDlg::GetLocalSerialPort(CUIntArray &ports)

    
 {
    
 	//Make sure we clear out any elements which may already be in the array
    
 	ports.RemoveAll();
    
 	//Determine what OS we are running on
    
 	OSVERSIONINFO osvi;
    
 	osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);
    
 	BOOL bGetVer = GetVersionEx(&osvi);
    
 	
    
 	//On NT use the QueryDosDevice API
    
 	if (bGetVer && (osvi.dwPlatformId == VER_PLATFORM_WIN32_NT))
    
 	{
    
 		//Use QueryDosDevice to look for all devices of the form COMx. This is a better
    
 		//solution as it means that no ports have to be opened at all.
    
 		TCHAR szDevices[65535];
    
 		DWORD dwChars = QueryDosDevice(NULL, szDevices, 65535);
    
 		if (dwChars)
    
 		{
    
 			int i=0;
    
 			
    
 			for (;;)
    
 			{
    
 				//Get the current device name
    
 				TCHAR* pszCurrentDevice = &szDevices[i];
    
 				
    
 				//If it looks like "COMX" then
    
 				//add it to the array which will be returned
    
 				int nLen = _tcslen(pszCurrentDevice);
    
 				if (nLen > 3 && _tcsncmp(pszCurrentDevice, _T("COM"), 3) == 0)
    
 				{
    
 					//Work out the port number
    
 					int nPort = _ttoi(&pszCurrentDevice[3]);
    
 					ports.Add(nPort);
    
 				}
    
 				
    
 				// Go to next NULL character
    
 				while(szDevices[i] != _T('\0'))
    
 					i++;
    
 				
    
 				// Bump pointer to the next string
    
 				i++;
    
 				
    
 				// The list is double-NULL terminated, so if the character is
    
 				// now NULL, we're at the end
    
 				if (szDevices[i] == _T('\0'))
    
 					break;
    
 			}
    
 		}
    
 		else
    
 			TRACE(_T("Failed in call to QueryDosDevice, GetLastError:%d\n"), GetLastError());
    
 	}
    
 	else
    
 	{
    
 		//On 95/98 open up each port to determine their existence
    
 		
    
 		//Up to 255 COM ports are supported so we iterate through all of them seeing
    
 		//if we can open them or if we fail to open them, get an access denied or general error error.
    
 		//Both of these cases indicate that there is a COM port at that number. 
    
 		for (UINT i=1; i<256; i++)
    
 		{
    
 			//Form the Raw device name
    
 			CString sPort;
    
 			sPort.Format(_T("\ \ .\ COM%d"), i);
    
 			
    
 			//Try to open the port
    
 			BOOL bSuccess = FALSE;
    
 			HANDLE hPort = ::CreateFile(sPort, GENERIC_READ | GENERIC_WRITE, 0, 0, OPEN_EXISTING, 0, 0);
    
 			if (hPort == INVALID_HANDLE_VALUE)
    
 			{
    
 				DWORD dwError = GetLastError();
    
 				
    
 				//Check to see if the error was because some other app had the port open or a general failure
    
 				if (dwError == ERROR_ACCESS_DENIED || dwError == ERROR_GEN_FAILURE)
    
 					bSuccess = TRUE;
    
 			}
    
 			else
    
 			{
    
 				//The port was opened successfully
    
 				bSuccess = TRUE;
    
 				
    
 				//Don't forget to close the port, since we are going to do nothing with it anyway
    
 				CloseHandle(hPort);
    
 			}
    
 			
    
 			//Add the port number to the array which will be returned
    
 			if (bSuccess)
    
 				ports.Add(i);
    
 		}
    
 	}
    
 	return true;
    
 } 
    
  
    
 BOOL CActiveReaderDlg::OnInitDialog()
    
 {
    
 	CDialog::OnInitDialog();
    
     GetLocalSerialPort(ports);
    
 	for (int i=0; i<ports.GetSize(); i++)
    
 	{
    
 		CString str="";
    
 		CString port="COM";
    
 		str.Format("%d",ports.ElementAt(i));
    
 		port = port+str;
    
 		((CComboBox *)GetDlgItem(IDC_SP_NUM_COMBO))->AddString(port);
    
 	}
    
 }

以下方法是网上摘录的:

2)打开注册表枚举串口的方法

复制代码
 void CPageSetCom::ShowComm()

    
 {
    
 long  lReg; 
    
 HKEY  hKey; 
    
 DWORD  MaxValueLength; 
    
 DWORD  dwValueNumber; 
    
  
    
 lReg=RegOpenKeyExA(HKEY_LOCAL_MACHINE, "HARDWARE\ DEVICEMAP\ SERIALCOMM", 0, KEY_QUERY_VALUE, &hKey); 
    
 if(lReg!=ERROR_SUCCESS) 
    
 { 
    
   AfxMessageBox(L"Open Registry Error!\n"); 
    
   return; 
    
 } 
    
  
    
 lReg=RegQueryInfoKeyA(hKey, 
    
   NULL, 
    
   NULL, 
    
   NULL, 
    
   NULL, 
    
   NULL, 
    
   NULL, 
    
   &dwValueNumber, //返回和hKey关联的值 
    
   &MaxValueLength, 
    
   NULL, 
    
   NULL, 
    
   NULL); 
    
 if(lReg!=ERROR_SUCCESS) //没有成功 
    
 { 
    
   AfxMessageBox(L"Getting  Info  Error!\n"); 
    
   return; 
    
 } 
    
  
    
 LPSTR  pValueName,pCOMNumber; 
    
 DWORD  cchValueName,dwValueSize=6; 
    
  
    
 for(DWORD  i=0;i < dwValueNumber;i++) 
    
 { 
    
   cchValueName=MaxValueLength+1; 
    
   dwValueSize=6; 
    
   pValueName=(LPSTR)VirtualAlloc(NULL,cchValueName,MEM_COMMIT,PAGE_READWRITE); 
    
   lReg=RegEnumValueA(hKey, 
    
    i, 
    
    pValueName, 
    
    &cchValueName, 
    
    NULL, 
    
    NULL, 
    
    NULL, 
    
    NULL); 
    
   
    
   if((lReg!=ERROR_SUCCESS)&&(lReg!=ERROR_NO_MORE_ITEMS)) 
    
   { 
    
    AfxMessageBox(L"Enum  Registry  Error or No More Items!\n"); 
    
    continue; 
    
   } 
    
   
    
   pCOMNumber=(LPSTR)VirtualAlloc(NULL,6,MEM_COMMIT,PAGE_READWRITE); 
    
   lReg=RegQueryValueExA(hKey, 
    
    pValueName, 
    
    NULL, 
    
    NULL, 
    
    (LPBYTE)pCOMNumber, 
    
    &dwValueSize); 
    
   
    
   if(lReg!=ERROR_SUCCESS) 
    
   { 
    
    AfxMessageBox(L"Can not get the name of the port"); 
    
    continue; 
    
   } 
    
   CString strCommList;
    
   //AfxMessageBox(pCOMNumber);
    
   CharToUnicode(pCOMNumber,&strCommList);
    
   //m_ctlPort.AddString(strCommList);
    
   BOOL m_bInsert=0;
    
   
    
   if(((CComboBox*)GetDlgItem(IDC_CMBREADERCOM))->GetCount()==0)
    
    ((CComboBox*)GetDlgItem(IDC_CMBREADERCOM))->AddString(strCommList);
    
   else
    
   {
    
    CString strTemp=strCommList;
    
    strCommList.TrimLeft(L"COM");
    
    for(int icurrent=0;icurrent<((CComboBox*)GetDlgItem(IDC_CMBREADERCOM))->GetCount();icurrent++)
    
    {
    
     CString strCurrent;
    
     ((CComboBox*)GetDlgItem(IDC_CMBREADERCOM))->GetLBText(icurrent,strCurrent);
    
     strCurrent.TrimLeft(L"COM");
    
     if(_ttol(strCurrent)>_ttol(strCommList))
    
     {
    
      ((CComboBox*)GetDlgItem(IDC_CMBREADERCOM))->InsertString(icurrent,strTemp);
    
      m_bInsert = 1;
    
      break;
    
     }
    
    }
    
    if(!m_bInsert)
    
     ((CComboBox*)GetDlgItem(IDC_CMBREADERCOM))->InsertString(icurrent,strTemp);
    
   }
    
   
    
   VirtualFree(pValueName,0,MEM_RELEASE); 
    
   VirtualFree(pCOMNumber,0,MEM_RELEASE); 
    
 } 
    
 }
    
 int CPageSetCom::CharToUnicode(char *pchIn, CString *pstrOut)
    
 {
    
 int nLen;
    
     WCHAR *ptch;
    
     if(pchIn == NULL)
    
     {
    
     return 0;
    
     }
    
     nLen = MultiByteToWideChar(CP_ACP, 0, pchIn, -1, NULL, 0);
    
     ptch = new WCHAR[nLen];
    
     MultiByteToWideChar(CP_ACP, 0, pchIn, -1, ptch, nLen);
    
     pstrOut->Format(_T("%s"), ptch);
    
     
    
     delete [] ptch;
    
     return nLen;
    
 }

3)使用EnumPort方法

此方法依赖于EnumPort()函数;此函数本就是用于列举电脑端口的工具;其枚举的内容不仅限于串口;因此需要对所得出的串口号进行筛选。

复制代码
 </pre><pre name="code" class="cpp">int m_nSerialPortNum(0);<span style="white-space:pre">        </span>// 串口计数

    
 CString strSerialList[256];<span style="white-space:pre">	</span>// 临时定义 256 个字符串组
    
 LPBYTE pByte  = NULL;
    
 DWORD pcbNeeded = 0;  <span style="white-space:pre">		</span>// bytes received or required
    
 DWORD pcReturned = 0;  <span style="white-space:pre">		</span>// number of ports received
    
 m_nSerialPortNum = 0;
    
  
    
 // 获取端口信息,能得到端口信息的大小 pcbNeeded
    
 EnumPorts(NULL, 2, pByte, 0, &pcbNeeded, &pcReturned);
    
 pByte = new BYTE[pcbNeeded];
    
  
    
 // 枚举端口,能得到端口的具体信息 pByte 以及端口的的个数 pcReturned
    
 EnumPorts(NULL, 2, pByte, pcbNeeded, &pcbNeeded, &pcReturned);
    
  
    
 PORT_INFO_2 *pPort;
    
 pPort = (PORT_INFO_2*)pByte;
    
  
    
 for ( i = 0; i < pcReturned; i++)
    
 {
    
 <span style="white-space:pre">	</span>CString str = pPort[i].pPortName;
    
  
    
 <span style="white-space:pre">	</span>// 串口信息的具体确定
    
 <span style="white-space:pre">	</span>if (str.Left(3) == "COM")
    
 <span style="white-space:pre">	</span>{                  
    
 <span style="white-space:pre">		</span>strSerialList[m_nSerialPortNum] = str.Left(strlen(str) - 1);
    
 <span style="white-space:pre">		</span>//CString temp = str.Right(strlen(str) - 3);// 下面两行注释获取串口序号用
    
 <span style="white-space:pre">		</span>//m_nSerialPortNo[m_nSerialPortNum] = atoi(temp.Left(strlen(temp) - 1));
    
 <span style="white-space:pre">		</span>m_nSerialPortNum++;                
    
 <span style="white-space:pre">	</span>}
    
  
    
 }

以上方法除了串口外,还可以遍历所有并口及打印机等接口,并能同时获取到虚拟串口(这些仅在未使用状态下无法通过注册表与硬件设备管理器访问)。然而该方法运行时间较长,在作者自行测试的情况下大约耗时几十毫秒。其中一个主要问题是部分USB串口号无法被探测到

4)依次打开串口的方法

该方法通过打开串口是否成功来判断串口是否存在。

复制代码
 int m_nSerialPortNum(0);<span style="white-space:pre">   </span>// 串口数

    
 CString strSerialList[256];  <span style="white-space:pre">	</span>// 临时定义 30 个字符串组
    
 int nCom = 0;
    
 int count = 0;
    
 HANDLE hCom;
    
  
    
 do {
    
 <span style="white-space:pre">	</span>nCom++;
    
 <span style="white-space:pre">	</span>strCom.Format("COM%d", nCom);
    
 <span style="white-space:pre">	</span>hCom = CreateFile(strCom, 0, 0, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
    
  
    
     if(INVALID_HANDLE_VALUE == hCom )
    
 <span style="white-space:pre">		</span>break;
    
  
    
     strSerialList[m_nSerialPortNum] = strCom;
    
  
    
     m_nSerialPortNum++;         
    
  
    
     CloseHandle(hCom);
    
  
    
 } while(1);

以下是对原文的同义改写

改写说明

5)使用第三方库SetupAPI

详细信息可参考以下链接:http://www.codeguru.com/Cpp/W-P/system/hardwareinformation/article.php/c5721/

复制代码
 int m_nSerialPortNum(0);<span style="white-space:pre">   </span>// 串口计数

    
 CString strSerialList[256];  <span style="white-space:pre">	</span>// 临时定义 256 个字符串组
    
 CArray<SSerInfo,SSerInfo&> asi;
    
  
    
 EnumSerialPorts(asi,TRUE);// 参数为 TRUE 时枚举当前可以打开的串口, 
    
 // 否则枚举所有串口
    
 m_nSerialPortNum = asi.GetSize();
    
  
    
 <pre name="code" class="cpp" style="color: rgb(34, 0, 0);">for (int i=0; i<asi.GetSize(); i++)
    
 {
    
 <span style="white-space:pre">	</span>CString str = asi[i].strFrien dlyName;
    
 }
复制代码

在工程中需要增添'EnumSerial.cpp'和'EnumSerial.h'两个文件,并同时包含Setupapi.lib。从时间角度来看, 该方法与第三种方法大致相同; 然而其获取的串口则是硬件设备管理器直接提供的串口.

二、打开串口

串口的打开采用CreateFile函数。CreateFile函数不仅用于打开文件还可以创建其他类型的I/O设备。最常用的I/O设备包括文件文件流目录物理磁盘卷控制台缓冲磁带驱动器通信资源邮槽和管道。

CreateFile函数的第一个参数涉及Win32设备名称空间的相关问题。详细参考MSDN上的https://msdn.microsoft.com/zh-cn/office/aa365247(v=vs.80)#maxpath,其中提到了Win32设备名称空间的相关内容。如果你想要打开串口通信端口1,则可以通过使用'COM1'来实现这一目标。这是因为COM1至COM9是NT命名空间中的保留名称的一部分,所以这种方式能够正常工作。值得注意的是,在'COM1'前面添加'\ .''前缀即使不加前缀'\ .'也能正常工作。然而,在部署了一个包含100个串口端口的扩展板后,并试图打开COM56时,则无法直接使用'COM56'来实现这一目标。因为对于(COM56)并没有预定义的NT命名空间支持。此时需要通过指定路径的方式访问该设备名称空间:即通过指定路径如'\ ._COM56'才能成功打开该端口进行通信

CreateFile函数的原型如下:

复制代码
 HANDLE WINAPI CreateFile(

    
   _In_     LPCTSTR               lpFileName,
    
   _In_     DWORD                 dwDesiredAccess,
    
   _In_     DWORD                 dwShareMode,
    
   _In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes,
    
   _In_     DWORD                 dwCreationDisposition,
    
   _In_     DWORD                 dwFlagsAndAttributes,
    
   _In_opt_ HANDLE                hTemplateFile
    
 );

参数说明:

lpFileName: 将要打开的串口名,如“COM1”。

dwDesiredAccess:串口访问的类型,可以是只可读、只可写、可读且可写。

dwShareMode:共享模式,由于串口不能共享,该参数必须设为0。

lpSecurityAttributes:引用安全属性结构,缺省值为NULL。

dwCreationDisposition方法用于初始化配置,并要求在串口操作中将该字段指定为OPEN_EXISTING状态

dwFlagsAndAttributes:属性字段用于指示该串口是否执行异步操作;当其值等于FILE_FLAG_OVERLAPPED时,则采用异步I/O;当其值为零时,则采用同步I/O。

hTemplateFile:对串口而言,该参数必须设为NULL。

同步I/O方式打开串口的示例代码:

复制代码
 HANDLE hCom;     // 全局变量,串口句柄

    
 hCom = CreateFile("COM1",	<span style="white-space:pre">		</span>// COM1口
    
 		GENERIC_READ | GENERIC_WRITE,	// 允许读和写
    
 		0,	<span style="white-space:pre">			</span>// 独占方式
    
 		NULL,
    
 		OPEN_EXISTING,	<span style="white-space:pre">		</span>// 打开而不是创建
    
 		0,	<span style="white-space:pre">			</span>// 同步方式
    
 		NULL
    
 );
    
 if(hCom == -1)
    
 {
    
 	AfxMessageBox("打开COM失败!");
    
 	return FALSE;
    
 }
    
 return TRUE;

异步I/O打开串口的示例代码:

复制代码
 HANDLE hCom;     // 全局变量,串口句柄

    
 hCom = CreateFile("COM1",	// COM1口
    
 		GENERIC_READ | GENERIC_WRITE,	// 允许读和写
    
 		0,	// 独占方式
    
 		NULL,
    
 		OPEN_EXISTING,	// 打开而不是创建
    
 		FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,	// 重叠方式
    
 		NULL
    
 );
    
 if(hCom == INVALID_HANDLE_VALUE)
    
 {
    
 	AfxMessageBox("打开COM失败!");
    
 	return FALSE;
    
 }
    
 return TRUE;

三、配置串口

打开通讯设备句柄时通常会涉及一些基本操作。这些操作往往包括对串口的一些基本设置和参数调整。为了实现这一目标,在设计系统架构时采用了专门的DCB(串行通信控制块)结构来进行组织与管理。其中包含了许多关键参数:如波特率、数据传输位数、奇偶校验类型以及停止位的数量。当访问或修改串口的各种属性时,系统会利用DCB作为临时存储区域来缓存这些参数设置。

通常使用CreateFile打开串口后,在之后可以通过GetCommState函数来获取串口的初始配置信息。如果需要调整串口的配置参数,在调整前需先修改DCB结构参数设置,并随后调用SetCommState函数来完成配置参数的设置工作。

DCB结构包含有串口的各种配置参数设置内容,下面将重点阐述一些该结构中常用的主要变量

复制代码
 typedef struct _DCB{

    
   <span style="white-space:pre">	</span>………
    
   <span style="white-space:pre">	</span>//波特率,指定通信设备的传输速率。这个成员可以是实际波特率值或者下面的常量值之一:
    
   <span style="white-space:pre">	</span>DWORD BaudRate;
    
 <span style="white-space:pre">	</span>CBR_110,CBR_300,CBR_600,CBR_1200,CBR_2400,CBR_4800,CBR_9600,CBR_19200, CBR_38400,
    
 <span style="white-space:pre">	</span>CBR_56000, CBR_57600, CBR_115200, CBR_128000, CBR_256000, CBR_14400
    
  
    
 <span style="white-space:pre">	</span>DWORD fParity; //指定奇偶校验使能。若此成员为1,允许奇偶校验检查
    
   <span style="white-space:pre">	</span>…
    
 <span style="white-space:pre">	</span>BYTE ByteSize; //通信字节位数,4—8
    
 <span style="white-space:pre">	</span>BYTE Parity; //指定奇偶校验方法。此成员可以有下列值:
    
 <span style="white-space:pre">			</span>EVENPARITY偶校验     NOPARITY 无校验
    
 <span style="white-space:pre">			</span>MARKPARITY标记校验   ODDPARITY 奇校验
    
 <span style="white-space:pre">	</span>BYTE StopBits; //指定停止位的位数。此成员可以有下列值:
    
 <span style="white-space:pre">			</span>ONESTOPBIT 1位停止位  TWOSTOPBITS 2位停止位
    
 <span style="white-space:pre">			</span>ONE5STOPBITS  1.5位停止位
    
 <span style="white-space:pre">	</span>  ………
    
  } DCB;
复制代码
 winbase.h文件中定义了以上用到的常量。如下:

    
 #define NOPARITY           0
    
 #define ODDPARITY          1
    
 #define EVENPARITY         2
    
 #define ONESTOPBIT         0
    
 #define ONE5STOPBITS       1
    
 #define TWOSTOPBITS        2
    
 #define CBR_110            110
    
 #define CBR_300            300
    
 #define CBR_600            600
    
 #define CBR_1200           1200
    
 #define CBR_2400           2400
    
 #define CBR_4800           4800
    
 #define CBR_9600           9600
    
 #define CBR_14400          14400
    
 #define CBR_19200          19200
    
 #define CBR_38400          38400
    
 #define CBR_56000          56000
    
 #define CBR_57600          57600
    
 #define CBR_115200         115200
    
 #define CBR_128000         128000
    
 #define CBR_256000         256000
复制代码
 GetCommState函数可以获得COM口的设备控制块,从而获得相关参数:

    
 BOOL GetCommState(
    
   HANDLE hFile, //标识通讯端口的句柄
    
   LPDCB lpDCB //指向一个设备控制块(DCB结构)的指针
    
  );
复制代码
 SetCommState函数设置COM口的设备控制块:

    
 BOOL SetCommState(
    
   HANDLE hFile,
    
   LPDCB lpDCB                                                                                                                                                          <span style="font-family: SimSun;">);</span>

除了在BCD中的配置外,程序通常还需设定I/O缓冲区的大小以及超时参数。Windows通常会使用I/O缓冲区来暂存串口输入与输出的数据。当通信速率较高时,则建议选择较大的缓冲区。通过调用SetupComm函数能够实现对串行口输入与输出缓冲区尺寸的配置。

复制代码
 BOOL SetupComm(

    
    HANDLE hFile,   // 通信设备的句柄
    
    DWORD dwInQueue, // 输入缓冲区的大小(字节数)
    
    DWORD dwOutQueue // 输出缓冲区的大小(字节数)
    
 );

当使用ReadFile和WriteFile进行串行口读写操作时,请注意可能出现超时情况。具体而言,在规定时间内未能正确读取或传输所需字符数量的情况下,ReadFile或WriteFile的操作将无法完成任务目标。

要查询当前的超时设置应通过调用GetCommTimeouts函数,并将返回的结果存储在一个COMMTIMEOUTS结构变量中。通过调用SetCommTimeouts方法可以将一个COMMTIMEOUTS结构的内容作为参数来设定超时值。

串口中读写操作的超时分为两种类型:一种是接收方两个字符之间的时间差(间隔超时),另一种是整个读写过程的最大时间限制(总 super time)。其中间隔 super 时时指在接收端两个连续字符之间出现的最大延迟时间;而 total super time 则代表了整个数据传输过程所需的最大持续时间。需要注意的是,在配置参数中,默认情况下仅对数据传输的速度进行限制;但当执行数据发送任务(即"write" 操作)时不考虑间隔 super limit 的问题;并且对于"read" 操作而言,则同时支持两种类型的限时约束;最后,在实际应用中可以通过COMMTIMEOUTS结构来具体设定各类型的操作限时参数

COMMTIMEOUTS结构的定义为:

复制代码
 typedef struct _COMMTIMEOUTS {  
    
    DWORD ReadIntervalTimeout; //读间隔超时
    
    DWORD ReadTotalTimeoutMultiplier; //读时间系数
    
    DWORD ReadTotalTimeoutConstant; //读时间常量
    
    DWORD WriteTotalTimeoutMultiplier; // 写时间系数
    
    DWORD WriteTotalTimeoutConstant; //写时间常量
    
 } COMMTIMEOUTS,*LPCOMMTIMEOUTS;

COMMTIMEOUTS结构的成员都以毫秒为单位。总超时的计算公式是:

总超时=时间系数×要求读/写的字符数+时间常量

例如,要读入10个字符,那么读操作的总超时的计算公式为:

读总超时等于ReadTotalTimeoutMultiplier值乘以10再加上ReadTotalTimeoutConstant常数。可以看出间隔超时与总超时的设置相互独立,这种设计使得通信程序能够灵活地配置各种超时参数。若所有写相关参数设为0,则将忽略写入操作的时间限制机制。若读取间隔时间设为MAXDWORD且读时间系数及读时间常量均为0,则在读取输入缓冲区后程序将立即返回,无需等待读入所需字符。

在用重叠方式读写串口时, 尽管ReadFile和WriteFile函数在其处理完毕之前就可能返回结果, 但超时机制仍然会发挥作用. 这种情况下, 超时的规定是基于操作的结束时间而言, 而不是直接关联到ReadFile和WriteFile的具体返回结果.

配置串口的示例代码:

复制代码
 SetupComm(hCom,1024,1024); //输入缓冲区和输出缓冲区的大小都是1024

    
  
    
 COMMTIMEOUTS TimeOuts;
    
 //设定读超时
    
 TimeOuts.ReadIntervalTimeout=1000;
    
 TimeOuts.ReadTotalTimeoutMultiplier=500;
    
 TimeOuts.ReadTotalTimeoutConstant=5000;
    
 //设定写超时
    
 TimeOuts.WriteTotalTimeoutMultiplier=500;
    
 TimeOuts.WriteTotalTimeoutConstant=2000;
    
 SetCommTimeouts(hCom,&TimeOuts); //设置超时
    
  
    
 DCB dcb;
    
 GetCommState(hCom,&dcb);
    
 dcb.BaudRate=9600; //波特率为9600
    
 dcb.ByteSize=8; //每个字节有8位
    
 dcb.Parity=NOPARITY; //无奇偶校验位
    
 dcb.StopBits=TWOSTOPBITS; //两个停止位
    
 SetCommState(hCom,&dcb);
    
  
    
 PurgeComm(hCom,PURGE_TXCLEAR|PURGE_RXCLEAR);

在读写串口之前,还要用PurgeComm()函数清空缓冲区,该函数原型:

复制代码
 BOOL PurgeComm(

    
    HANDLE hFile,   //串口句柄
    
    DWORD dwFlags   // 需要完成的操作
    
  );

参数dwFlags指定要完成的操作,可以是下列值的组合:

复制代码
 PURGE_TXABORT  中断所有写操作并立即返回,即使写操作还没有完成。

    
 PURGE_RXABORT  中断所有读操作并立即返回,即使读操作还没有完成。
    
 PURGE_TXCLEAR  清除输出缓冲区
    
 PURGE_RXCLEAR  清除输入缓冲区

四、读写串口

我们使用ReadFile和WriteFile读写串口,下面是两个函数的声明:

复制代码
 BOOL ReadFile(

    
    HANDLE hFile,   //串口的句柄
    
    
    
    // 读入的数据存储的地址,即读入的数据将存储在以该指针的值为首地址的一片内存区
    
    LPVOID lpBuffer,
    
    DWORD nNumberOfBytesToRead,   // 要读入的数据的字节数
    
    
    
    // 指向一个DWORD数值,该数值返回读操作实际读入的字节数
    
    LPDWORD lpNumberOfBytesRead, 
    
    
    
    // 重叠操作时,该参数指向一个OVERLAPPED结构,同步操作时,该参数为NULL。
    
    LPOVERLAPPED lpOverlapped    
    
 ); 
    
 BOOL WriteFile( 
    
    HANDLE hFile,   //串口的句柄
    
    
    
    // 写入的数据存储的地址,
    
    // 即以该指针的值为首地址的nNumberOfBytesToWrite
    
    // 个字节的数据将要写入串口的发送数据缓冲区。
    
    LPCVOID lpBuffer,     
    
    
    
    DWORD nNumberOfBytesToWrite,  //要写入的数据的字节数
    
    
    
    // 指向指向一个DWORD数值,该数值返回实际写入的字节数
    
    LPDWORD lpNumberOfBytesWritten,     
    
    
    
    // 重叠操作时,该参数指向一个OVERLAPPED结构,
    
    // 同步操作时,该参数为NULL。
    
    LPOVERLAPPED lpOverlapped    
    
 );

当使用ReadFile和WriteFile进行串口读写操作时

Readfile与writefile的操作同步性或异步性取决于createfile函数的选择。当使用FILE_FLAG_OVERLAPPED标志时,该操作应为重叠;若未指定此标志,则读写操作应维持同步状态。其同步性或异步性应与createfile的一致性保持一致。

ReadFile函数仅当串口输入缓冲区接收了指定数量的字符即视为操作已完成而WriteFile函数不仅需要将这些字符复制到输出缓冲区还需等到所有数据从串行口传输完毕才算完成操作

若操作成功,则这两个函数将同时返回TRUE;值得注意的是,在ReadFile和WriteFile均失败的情况下(即这两个函数读取和写入均失败时),并不一定意味着操作失败;此时会返回FALSE值,并触发GetLastError函数来获取错误码。例如,在重叠操作中若操作尚未完成就返回,则该函数将返回FALSE值(并触发GetLastError函数来获取错误码)。

同步方式读写串口比较简单,下面先例举同步方式读写串口的代码:

复制代码
 <span style="color:#33cc00;">//同步读串口</span><span style="color:#666666;">

    
 char str[100];
    
 DWORD wCount;//读取的字节数
    
 BOOL bReadStat;
    
 bReadStat=ReadFile(hCom,str,100,&wCount,NULL);
    
 if(!bReadStat)
    
 {
    
       AfxMessageBox("读串口失败!");
    
       return FALSE;
    
 }
    
 return TRUE;
    
  
    
 </span><span style="color:#33cc00;">//同步写串口</span><span style="color:#666666;">
    
 char lpOutBuffer[100];
    
 DWORD dwBytesWrite=100;
    
 COMSTAT ComStat;
    
 DWORD dwErrorFlags;
    
 BOOL bWriteStat;
    
 ClearCommError(hCom,&dwErrorFlags,&ComStat);
    
 bWriteStat=WriteFile(hCom,lpOutBuffer,dwBytesWrite,& dwBytesWrite,NULL);
    
 if(!bWriteStat)
    
 {
    
 <span>	</span>AfxMessageBox("写串口失败!");
    
 }
    
 PurgeComm(hCom, PURGE_TXABORT | PURGE_RXABORT | PURGE_TXCLEAR | PURGE_RXCLEAR);</span>

在重叠操作时,操作还未完成函数就返回。

重叠的输入输出具有很高的灵活性,并且还可以实现阻塞操作(例如我们必须要读取一个数据才能继续下一步操作)。主要有两种方式可以用来等待操作完成:第一种方式是使用如 WaitForSingleObject 这样的 wait function for overlapped events;第二种方式则是调用 GetOverlappedResult 函数来进行 wait 操作,并在后续演示中进行详细说明。

OVERLAPPED结构包含了重叠I/O的一些信息,定义如下:

复制代码
 typedef struct _OVERLAPPED {

    
    DWORD  Internal;
    
    DWORD  InternalHigh;
    
    DWORD  Offset;
    
    DWORD  OffsetHigh;
    
    HANDLE hEvent;
    
 } OVERLAPPED;

在进行ReadFile和WriteFile的重叠操作时

GetOverlappedResult函数

复制代码
 BOOL GetOverlappedResult(

    
    HANDLE hFile,   // 串口的句柄 
    
    
    
    // 指向重叠操作开始时指定的OVERLAPPED结构
    
    LPOVERLAPPED lpOverlapped,   
    
    
    
    // 指向一个32位变量,该变量的值返回实际读写操作传输的字节数。
    
    LPDWORD lpNumberOfBytesTransferred, 
    
    
    
    // 该参数用于指定函数是否一直等到重叠操作结束。
    
    // 如果该参数为TRUE,函数直到操作结束才返回。
    
    // 如果该参数为FALSE,函数直接返回,这时如果操作没有完成,
    
    // 通过调用GetLastError()函数会返回ERROR_IO_INCOMPLETE。
    
    BOOL bWait     
    
 );

该函数会返回重叠操作的结果,并用于检测异步操作的状态。这个机制通过检查OVERLAPPED结构中的hEvent标志位是否被设置来确定异步操作的状态。

异步读串口的示例代码:

复制代码
 char lpInBuffer[1024];

    
 DWORD dwBytesRead=1024;
    
 COMSTAT ComStat;
    
 DWORD dwErrorFlags;
    
 OVERLAPPED m_osRead;
    
 memset(&m_osRead,0,sizeof(OVERLAPPED));
    
 m_osRead.hEvent=CreateEvent(NULL, TRUE, FALSE, NULL);
    
  
    
 ClearCommError(hCom, &dwErrorFlags, &ComStat);                                                                             <span style="white-space:pre">						</span>
    
 dwBytesRead=min(dwBytesRead, (DWORD)ComStat.cbInQue);
    
 if(!dwBytesRead)
    
 <span style="white-space:pre">	</span>return FALSE;<span style="white-space:pre">																				</span>
    
 BOOL bReadStatus;
    
 bReadStatus=ReadFile(hCom, lpInBuffer, dwBytesRead, &dwBytesRead, &m_osRead);
    
  
    
 if(!bReadStatus) //如果ReadFile函数返回FALSE
    
 {
    
       if(GetLastError()==ERROR_IO_PENDING)
    
       //GetLastError()函数返回ERROR_IO_PENDING,表明串口正在进行读操作
    
       {
    
          WaitForSingleObject(m_osRead.hEvent,2000);
    
          //使用WaitForSingleObject函数等待,直到读操作完成或延时已达到2秒钟
    
          //当串口读操作进行完毕后,m_osRead的hEvent事件会变为有信号
    
          PurgeComm(hCom, PURGE_TXABORT|
    
                PURGE_RXABORT|PURGE_TXCLEAR|PURGE_RXCLEAR);
    
          return dwBytesRead;
    
       }
    
       return 0;
    
 }
    
 PurgeComm(hCom, PURGE_TXABORT|
    
            PURGE_RXABORT|PURGE_TXCLEAR|PURGE_RXCLEAR);
    
 return dwBytesRead;

对上述代码进行进一步详细说明:应在调用ReadFile函数执行读取操作之前,在系统中调用ClearCommError函数以清除相关错误信息。其基本框架如下所示:

复制代码
 BOOL ClearCommError(

    
    HANDLE hFile,   // 串口句柄
    
    LPDWORD lpErrors,      // 指向接收错误码的变量
    
    LPCOMSTAT lpStat // 指向通讯状态缓冲区
    
 );

该函数检测到通信错误并反馈串口的当前状态;同时,该函数去除串口的错误标志从而让输入和输出操作继续进行。

变量lpStat指向前端COMSTAT接口,并通过该接口返回串口的状态数据。 COMSTAT 结构 具有详细的数据架构描述,在其具体架构中包含了串口相关的详细信息。其具体架构如下:

复制代码
 typedef struct _COMSTAT { // cst

    
    DWORD fCtsHold : 1;   // Tx waiting for CTS signal
    
    DWORD fDsrHold : 1;   // Tx waiting for DSR signal
    
    DWORD fRlsdHold : 1;  // Tx waiting for RLSD signal
    
    DWORD fXoffHold : 1;  // Tx waiting, XOFF char rec''d
    
    DWORD fXoffSent : 1;  // Tx waiting, XOFF char sent
    
    DWORD fEof : 1;       // EOF character sent
    
    DWORD fTxim : 1;      // character waiting for Tx
    
    DWORD fReserved : 25; // reserved
    
    DWORD cbInQue;        // bytes in input buffer
    
    DWORD cbOutQue;       // bytes in output buffer
    
 } COMSTAT, *LPCOMSTAT;

在本文中,仅涉及了cbInQue成员变量。其中,cbInQue成员变量的数值表示输入缓冲区的字节数。

最后用PurgeComm函数清空串口的输入输出缓冲区。

这段代码主要通过使用WaitForSingleObject函数来等待OVERLAPPED结构中的hEvent成员。下面我们将展示一段利用GetOverlappedResult函数执行异步读串操作的示例代码。

复制代码
 char lpInBuffer[1024];

    
 DWORD dwBytesRead=1024;
    
 BOOL bReadStatus;
    
 DWORD dwErrorFlags;
    
 COMSTAT ComStat;
    
 OVERLAPPED m_osRead;
    
  
    
 ClearCommError(hCom,&dwErrorFlags,&ComStat);
    
 if(!ComStat.cbInQue)
    
 <span style="white-space:pre">	</span>return 0;
    
 dwBytesRead = min(dwBytesRead, (DWORD)ComStat.cbInQue);
    
 bReadStatus = ReadFile(hCom, lpInBuffer, dwBytesRead, &dwBytesRead, &m_osRead);
    
 if(!bReadStatus) //如果ReadFile函数返回FALSE
    
 {
    
 <span style="white-space:pre">	</span>if(GetLastError() == ERROR_IO_PENDING)
    
     {
    
     <span style="white-space:pre">	</span>GetOverlappedResult(hCom, &m_osRead, &dwBytesRead, TRUE);
    
       <span style="white-space:pre">	</span>// GetOverlappedResult函数的最后一个参数设为TRUE,
    
       <span style="white-space:pre">	</span>//函数会一直等待,直到读操作完成或由于错误而返回。
    
  
    
             return dwBytesRead;
    
      }
    
      return 0;
    
 }
    
 return dwBytesRead;

异步写串口的示例代码:

复制代码
 char buffer[1024];

    
 DWORD dwBytesWritten=1024;
    
 DWORD dwErrorFlags;
    
 COMSTAT ComStat;
    
 OVERLAPPED m_osWrite;
    
 BOOL bWriteStat;
    
  
    
 bWriteStat=WriteFile(hCom, buffer, dwBytesWritten, &dwBytesWritten, &m_OsWrite);
    
 if(!bWriteStat)
    
 {
    
 <span style="white-space:pre">	</span>if(GetLastError()==ERROR_IO_PENDING)
    
     {
    
      <span style="white-space:pre">	</span>WaitForSingleObject(m_osWrite.hEvent,1000);
    
             return dwBytesWritten;
    
      }
    
      return 0;
    
 }
    
 return dwBytesWritten;

五、关闭串口

通过API函数实现串口关闭非常简便:仅需获取CreateFile函数返回的句柄并将其传递给CloseHandle函数即可完成关闭操作。

复制代码
 BOOL CloseHandle(

    
    HANDLE hObject; //handle to object to close
    
 );

全部评论 (0)

还没有任何评论哟~