VC 读串口方法
------分隔线----------------------------

  VC 串口通信技术网曾经发表过一篇文章叫:《VC串口编程基础之如何用 VC 打开串口和关闭串口》,这里说说在用VC打开串口后怎么再用VC 读串口数据。

  《VC 串口编程基础之如何用 VC 打开串口和关闭串口》一文只是说的用API 方法,实际上用VC 读串口根据编程方法不一样,打开方法也不一样,串口编程方法有基于activex控件的,也有基于动态链接库的,还有使用串口类的,用这些方法读串口都非常简单,只需要调用其提供的相关接口就可以了(有时需要映射消息),不管用何种方法,其实质是串口控件最终都调用了底层API函数ReadFile来读串口,下面分别讲讲常用控件的读串口方法和直接利用串口API读串口的方法。

1、ActiveX控件Mscomm读串口

  先正确打开并配置好串口,具体内容不是这里的重点,相关内容在本站里也有,但有个属性是重点,那就是RThreshold属性,当接收字符后,若 Rthreshold 属性设置为 0(缺省值)则不产生 OnComm 事件。例如,设置 Rthreshold 为 1,接收缓冲区收到每一个字符都会使 MSComm 控件产生 OnComm 事件。

  添加串口事件消息处理函数OnComm(),打开ClassWizard->Message Maps,选择类CSCommTestDlg,选择IDC_MSCOMM1,双击消息OnComm,将弹出的对话框中将函数名改为OnComm。我们在OnComm()函数加入相应的处理代码就能实现自已想要的功能了,以下一个实例:

 

  1. void CDemoDlg::OnComm()      
  2. {     
  3.  
  4.     VARIANT variant_inp;     
  5.     COleSafeArray safearray_inp;     
  6.     LONG len,k;     
  7.     BYTE rxdata[2048]; //用于存放接收到的数据,BYTE格式(即CHAR格式)  
  8.     CString strtemp;   //可将转换成字符串格式后的串口数据存入此变量  
  9.     //读缓冲区数据并进行转换  
  10.     if(m_ctrlComm.GetCommEvent()==2) //事件值为2表示接收缓冲区内有字符     
  11.     {                 
  12.         variant_inp=m_ctrlComm.GetInput(); //读缓冲区     
  13.         safearray_inp=variant_inp; //将VARIANT型变量转换为ColeSafeArray型变量     
  14.         len="safearray"_inp.GetOneDimSize(); //得到有效数据长度     
  15.         for(k=0;k<len;k++)     
  16.             safearray_inp.GetElement(&k,rxdata+k);//转换为BYTE型数组     
  17.         for(k=0;k<len;k++) //将数组转换为Cstring型变量     
  18.         {     
  19.             BYTE bt=*(char*)(rxdata+k); //字符型     
  20.             strtemp.Format("%c",bt); //将字符送入临时变量strtemp存放     
  21.             m_strRXData+=strtemp; //加入接收编辑框对应字符串      
  22.         }     
  23.     }     
  24.       
  25.     //以下可根据需要处理接收到的数据  
  26.     ....  
  27. }  

由于用Mscomm控件的GetInput方法读到的串口缓冲区数据为VARIANT格式,需要用一个COleSafeArray类型来转换,具体步骤按上面的示例来就是了。

 

2、动态链接库dll读串口方法

  不同的串口dll有不同的方法,应该根据其手册来,如VC串口通信技术网的VC串口通信技术资料集里的一个例子用到的串口dll-ThreadSerial.dll,它是通过查询成员变量m_LastIndex的值是否大于1来判断是否接串口缓冲区中数据的,VC串口通信技术资料集的一个例子串口调试助手EasySerialAssistant就是用的这个dll,

 

VC 读串口
 

 这个示例专门为读串口开了一个线程,在线程里不断地读m_LastIndex的值,如果大于代表串口缓冲区中有数据,然后将数据显示到显示区:

 

  1. UINT CEasySerialAssistantDlg::ProDispThread(LPVOID pParam)  
  2. {     
  3.     CEasySerialAssistantDlg* pParlPro=(CEasySerialAssistantDlg*)pParam;  
  4.     pParlPro->m_bThread=TRUE;  
  5.     CString c="",d="";  
  6.     char b[4];  
  7.  
  8.     while(1)  
  9.     {  
  10.         Sleep(1);  
  11.           
  12.         if(pParlPro->m_LastIndex > 0)  
  13.         {  
  14.             c.Format("RX:%d t:%d n:%d",pParlPro->m_RecvCount,pParlPro->m_nCurLen,pParlPro->m_nRecvTimes);  
  15.             (pParlPro->GetDlgItem(IDC_STATIC_RX))->SetWindowText(c);  
  16.  
  17.             if(pParlPro->m_bDispRecv == FALSE)  
  18.             {  
  19.                 pParlPro->m_LastIndex=0;  
  20.             }  
  21.             else 
  22.             {  
  23.                 CEdit *pList =(CEdit *)(pParlPro->GetDlgItem(IDC_EDIT_RX_DISP));  
  24.                   
  25.                 if(pParlPro->m_bAutoClearn == FALSE) c=pParlPro->m_StrBuf;    
  26.                 else c="";  
  27.  
  28.                 if(pParlPro->m_bHexDispMode == TRUE)  
  29.                 {  
  30.                     for(int i=0;i<(int)pParlPro->m_LastIndex;i++)  
  31.                     {  
  32.                       ::sprintf(b,"%2.2X",pParlPro->m_RecvBuf[i]);  
  33.                        c+=" ";  
  34.                        c+=b;  
  35.                     }  
  36.                     pParlPro->m_LastIndex=0;  
  37.                 }  
  38.                 else 
  39.                 {  
  40.                     pParlPro->m_RecvBuf[pParlPro->m_LastIndex+1]='\0';  
  41.                     d.Format("%s",pParlPro->m_RecvBuf);  
  42.                     if(pParlPro->m_bAutoChangRow == TRUE)  
  43.                     {  
  44.                         d.Insert(c.GetLength(),"\r\n");  
  45.                     }  
  46.                     c+=d;  
  47.                     pParlPro->m_LastIndex=0;  
  48.                 }  
  49.  
  50.                 if(pParlPro->m_bAutoClearn == FALSE) pParlPro->m_StrBuf=c;  
  51.  
  52.                 pList->SetWindowText(c);  
  53.  
  54.                 unsigned int nVScrollPos=pList->GetLineCount();  
  55.                 pList->LineScroll(nVScrollPos);   
  56.             }     
  57.         }  
  58.     }  
  59.  
  60.     pParlPro->m_bThread=false//工作标志复位  
  61.     return 0;  

4、串口类读串口方法

  同样不同串口类有不同方法和接口,这里只说说最常用的串口类CSerialPort的读串口方法。

  启动串口通信监测线程函数 StartMonitoring(),串口初始化成功后,就可以调用BOOL StartMonitoring()启动串口监测线程。线程启动成功,返回TRUE。

  串口收到的字符会向主线程或父窗口发送 WM_COMM_RXCHAR 消息,我们需要在主线程或父窗口中手动映射这个消息,手动映射消息的方法属于VC基础性内容,就不在这里讲解了,本站VC串口通信技术资料集有很多CSerialPort类的应用实例,大家可以参考,以下本资料中一个实例 串口调试助手SComAssistant的串口接收源代码,OnCommunication函数是WM_COMM_RXCHAR 消息的映射函数:

 

  1. LONG CSCOMMDlg::OnCommunication(WPARAM ch, LPARAM port)  
  2. {  
  3.     if (port <= 0 || port > 4)  
  4.         return -1;  
  5.     rxdatacount++;   //接收的字节计数  
  6.     CString strTemp;  
  7.     strTemp.Format("%ld",rxdatacount);  
  8.     strTemp="RX:"+strTemp;  
  9.     m_ctrlRXCOUNT.SetWindowText(strTemp);  //显示接收计数  
  10.       
  11.     if(m_bStopDispRXData)   //如果选择了“停止显示”接收数据,则返回  
  12.         return -1;          //注意,这种情况下,计数仍在继续,只是不显示  
  13.     //若设置了“自动清空”,则达到50行后,自动清空接收编辑框中显示的数据  
  14.     if((m_ctrlAutoClear.GetCheck())&&(m_ctrlReceiveData.GetLineCount()>=50))  
  15.     {  
  16.         m_ReceiveData.Empty();  
  17.         UpdateData(FALSE);  
  18.     }  
  19.     //如果没有“自动清空”,数据行达到400后,也自动清空  
  20.     //因为数据过多,影响接收速度,显示是最费CPU时间的操作  
  21.     if(m_ctrlReceiveData.GetLineCount()>400)  
  22.     {  
  23.         m_ReceiveData.Empty();  
  24.         m_ReceiveData="***The Length of the Text is too long, Emptied Automaticly!!!***\r\n";          
  25.         UpdateData(FALSE);  
  26.     }  
  27.  
  28.     //如果选择了"十六进制显示",则显示十六进制值  
  29.     CString str;  
  30.     if(m_ctrlHexReceieve.GetCheck())  
  31.         str.Format("%02X ",ch);  
  32.     else   
  33.         str.Format("%c",ch);  
  34.     //以下是将接收的字符加在字符串的最后,这里费时很多  
  35.     //但考虑到数据需要保存成文件,所以没有用List Control  
  36.     int nLen=m_ctrlReceiveData.GetWindowTextLength();  
  37.     m_ctrlReceiveData.SetSel(nLen, nLen);  
  38.     m_ctrlReceiveData.ReplaceSel(str);  
  39.     nLen+=str.GetLength();  
  40.  
  41.     m_ReceiveData+=str;  
  42.     return 0;  

4、串口API如何读串口

  以上各串口编程方法的实质都是调用了API的,只是被封装了而已,读串口的API函数是ReadFile,由于不知道何时串口收到数据,所以一般需要开一个线程不停地用ReadFile来读串口。

  在用ReadFile读串口时,既可以同步执行,也可以重叠执行。在同步执行时,函数直到操作完成后才返回。这意味着同步执行时线程会被阻塞,从而导致效率下降。在重叠执行时,即使操作还未完成,这个函数也会立即返回,费时的I/O操作在后台进行。

  ReadFile函数是同步还是异步由CreateFile函数决定,如果在调用CreateFile创建句柄时指定了FILE_FLAG_OVERLAPPED标志,那么调用ReadFile对该句柄进行的操作就应该是重叠的;如果未指定重叠标志,则读写操作应该是同步的。ReadFile和WriteFile函数的同步或者异步应该和CreateFile函数相一致。
  ReadFile函数只要在串口输入缓冲区中读入指定数量的字符,就算完成操作。如果操作成功,这个函数返回TRUE。需要注意的是,当ReadFile返回FALSE时,不一定就是操作失败,线程应该调用GetLastError函数分析返回的结果。例如,在重叠操作时如果操作还未完成函数就返回,那么函数就返回FALSE,而且GetLastError函数返回ERROR_IO_PENDING。这说明重叠操作还未完成。

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

  1. //同步读串口  
  2. char str[100];  
  3. DWORD wCount;//读取的字节数  
  4. BOOL bReadStat;  
  5. bReadStat=ReadFile(hCom,str,100,&wCount,NULL);  
  6. if(!bReadStat)  
  7. {  
  8.     AfxMessageBox("读串口失败!");  
  9.     return FALSE;  
  10. }  
  11. return TRUE;  
  12.  
  13. //同步写串口  
  14.  
  15.     char lpOutBuffer[100];  
  16.     DWORD dwBytesWrite=100;  
  17.     COMSTAT ComStat;  
  18.     DWORD dwErrorFlags;  
  19.     BOOL bWriteStat;  
  20.     ClearCommError(hCom,&dwErrorFlags,&ComStat);  
  21.     bWriteStat=WriteFile(hCom,lpOutBuffer,dwBytesWrite,& dwBytesWrite,NULL);  
  22.     if(!bWriteStat)  
  23.     {  
  24.         AfxMessageBox("写串口失败!");  
  25.     }  
  26.     PurgeComm(hCom, PURGE_TXABORT|  
  27.         PURGE_RXABORT|PURGE_TXCLEAR|PURGE_RXCLEAR);  

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

  重叠I/O非常灵活,它也可以实现阻塞(例如我们可以设置一定要读取到一个数据才能进行到下一步操作)。有两种方法可以等待操作完成:一种方法是用象WaitForSingleObject这样的等待函数来等待OVERLAPPED结构的hEvent成员;另一种方法是调用GetOverlappedResult函数等待。
OVERLAPPED结构和GetOverlappedResult函数请查看MSDN,本站VC串口通信资料集中也有详细详述。

异步读串口的示例代码:

 

  1. char lpInBuffer[1024];  
  2. DWORD dwBytesRead=1024;  
  3. COMSTAT ComStat;  
  4. DWORD dwErrorFlags;  
  5. OVERLAPPED m_osRead;  
  6. memset(&m_osRead,0,sizeof(OVERLAPPED));  
  7. m_osRead.hEvent=CreateEvent(NULL,TRUE,FALSE,NULL);  
  8.  
  9. ClearCommError(hCom,&dwErrorFlags,&ComStat);  
  10. dwBytesRead=min(dwBytesRead,(DWORD)ComStat.cbInQue);  
  11. if(!dwBytesRead)  
  12. return FALSE;  
  13. BOOL bReadStatus;  
  14. bReadStatus=ReadFile(hCom,lpInBuffer,  
  15.                      dwBytesRead,&dwBytesRead,&m_osRead);  
  16.  
  17. if(!bReadStatus) //如果ReadFile函数返回FALSE  
  18. {  
  19.     if(GetLastError()==ERROR_IO_PENDING)  
  20.     //GetLastError()函数返回ERROR_IO_PENDING,表明串口正在进行读操作      
  21.     {  
  22.         WaitForSingleObject(m_osRead.hEvent,2000);  
  23.         //使用WaitForSingleObject函数等待,直到读操作完成或延时已达到2秒钟  
  24.         //当串口读操作进行完毕后,m_osRead的hEvent事件会变为有信号  
  25.         PurgeComm(hCom, PURGE_TXABORT|  
  26.             PURGE_RXABORT|PURGE_TXCLEAR|PURGE_RXCLEAR);  
  27.         return dwBytesRead;  
  28.     }  
  29.     return 0;  
  30. }  
  31. PurgeComm(hCom, PURGE_TXABORT|  
  32.           PURGE_RXABORT|PURGE_TXCLEAR|PURGE_RXCLEAR);  
  33. return dwBytesRead;  

  对以上代码再作简要说明:在使用ReadFile 函数进行读操作前,应先使用ClearCommError函数清除错误。ClearCommError函数的原型如下:

  1. BOOL ClearCommError(     
  2.     
  3.     HANDLE hFile,   // 串口句柄     
  4.     LPDWORD lpErrors,   // 指向接收错误码的变量     
  5.     LPCOMSTAT lpStat    // 指向通讯状态缓冲区     
  6.    );      

 该函数获得通信错误并报告串口的当前状态,同时,该函数清除串口的错误标志以便继续输入、输出操作。
参数lpStat指向一个COMSTAT结构,该结构返回串口状态信息。 COMSTAT结构 COMSTAT结构包含串口的信息,结构定义如下:

  1. typedef struct _COMSTAT { // cst    
  2.     DWORD fCtsHold : 1;   // Tx waiting for CTS signal   
  3.     DWORD fDsrHold : 1;   // Tx waiting for DSR signal   
  4.     DWORD fRlsdHold : 1;  // Tx waiting for RLSD signal   
  5.     DWORD fXoffHold : 1;  // Tx waiting, XOFF char rec''d   
  6.     DWORD fXoffSent : 1;  // Tx waiting, XOFF char sent   
  7.     DWORD fEof : 1;       // EOF character sent   
  8.     DWORD fTxim : 1;      // character waiting for Tx   
  9.     DWORD fReserved : 25; // reserved   
  10.     DWORD cbInQue;        // bytes in input buffer   
  11.     DWORD cbOutQue;       // bytes in output buffer   
  12. } COMSTAT, *LPCOMSTAT;   

这里只用到了cbInQue成员变量,该成员变量的值代表输入缓冲区的字节数。

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

  这段代码用WaitForSingleObject函数来等待OVERLAPPED结构的hEvent成员,下面我们再演示一段调用GetOverlappedResult函数等待的异步读串口示例代码:

 

  1. char lpInBuffer[1024];  
  2. DWORD dwBytesRead=1024;  
  3.     BOOL bReadStatus;  
  4.     DWORD dwErrorFlags;  
  5.     COMSTAT ComStat;  
  6. OVERLAPPED m_osRead;  
  7.  
  8.     ClearCommError(hCom,&dwErrorFlags,&ComStat);  
  9.     if(!ComStat.cbInQue)  
  10.         return 0;  
  11.     dwBytesRead=min(dwBytesRead,(DWORD)ComStat.cbInQue);  
  12.     bReadStatus=ReadFile(hCom, lpInBuffer,dwBytesRead,  
  13.         &dwBytesRead,&m_osRead);  
  14.     if(!bReadStatus) //如果ReadFile函数返回FALSE  
  15.     {  
  16.         if(GetLastError()==ERROR_IO_PENDING)  
  17.         {  
  18.             GetOverlappedResult(hCom,  
  19.                 &m_osRead,&dwBytesRead,TRUE);  
  20.            // GetOverlappedResult函数的最后一个参数设为TRUE,  
  21.            //函数会一直等待,直到读操作完成或由于错误而返回。  
  22.  
  23.             return dwBytesRead;  
  24.         }  
  25.         return 0;  
  26.     }  
  27.     return dwBytesRead;  

  建议大家可以参考VC 串口类 CSerialPort类的源代码,可以加深API读串口的理解。

  本文介绍的不同情况下VC 读串口的方法,根据串口编程方法不同(用控件还是API)有不同的规则,建议大家多多看实例,并实例操作一下才能真正体会。

------分隔线----------------------------