最近在做一个PC机上和ARM机串口通讯的程序。
实际上,我并没有在VC上编写过串口程序。记得大一下学期的课程实践上倒是在DOS环境下做个简单的串口通讯,可是就是因为太简单了,而且是DOS那种独占式的进程,所以现在要搬到VC和MFC界面应用程序环境中,难度还是有的,我一时没有头绪。
我首先当然想到用ActiveX控件了。曾听说过Microsoft曾做过一个ActiveX控件,用来简化在MFC中进行的串口编程。找了点资料,又去图书馆找了本书,试了两天,结果以失败告终。网上说,用那个MSComm控件进行串口编程是最简单的,可是我仍然没有成功,可见我有多愚笨!也不知道哪儿出现问题了,总之,由于时间紧迫,我不得不选择其他方案。MSComm的使用,我想等这个任务完成之后,我会回过头来再看看的,到时候再写篇文章来向大家说明。
不用MSComm控件,那看起来只能是使用Windows API了,因为MFC貌似没有什么类封装了串口API函数的。
用Windows API 编写串口程序本身是有巨大优点的,因为控制能力会更强,效率也会更高,而且对于那些纯绿色软件追求者来说,没有ActiveX控件比什么都重要——呵呵,我也是这么认为。
API编写串口,过程一般是这样的:
1、 创建串口句柄,用CreateFile;
2、 对串口的参数进行设置,其中比较重要的是波特率(BaudRate),数据宽度(BytesBits),奇偶校验(Parity),停止位(StopBits),当然,重要的还有端口号(Port);
3、 然后对串口进行相应的读写操作,这时候用到ReadFile和WriteFile函数;
4、 读写结束后,要关闭串口句柄,用CloseFile;
下面依次大致讲讲个步骤的过程:
第一步,从字面上去理解,大家也可以发现CreateFile实际上表明Windows是把串口当作一个文件来处理的,所以它也有文件那样的缓冲区、句柄、读写错误等,不同的是,这个文件名字只有固定的几个(一般为四个),而且始终存在(EXSITING),而且在调用CreateFile的时候请注意它的参数。CreateFile函数原型如下:
HANDLE CreateFile(LPCTSTR lpFileName,
DWORD dwDesiredAccess,
DWORD dwShareMode,
LPSECURITY_ATTRIBUTES lpSecurityAttributes,
DWORD dwCreationDisposition,
DWORD dwFlagsAndAttributes,
HANDLE hTemplateFile );
lpFileName是你需要创建的端口号,默认情况下是COM1;dwDesiredAccess是表明你想让你创建的串口以何种方式存在于你的应用程序中,因为串口通常是可读可写的,所以这里必须设置为GENERIC_READ|GENERIC_WRITE;dwShareMode是用来设置串口共享属性的,因为串口属于临界资源,当然不能共享,所以这里也必须设置为0;lpSecurityAttributes是设置安全模式,一般采用默认的安全模式就可以了,选择NULL;dwCreationDisposition是设置是否打开新的“文件”(上面说过了,Windows是把串口等端口当作文件来处理的),因为串口属于硬件端口,当然不能随便重复创建,所以这里必须告诉Windows,每次创建的时候必须使用已经存在的串口,所以这里设置OPEN_EXSITING;dwFlagsAndAttributes,这个参数可以设置的值比较多,大家若需要深入了解可以查找MSDN,这里因为我们接下去要做的是异步通讯,所以需要设置FILE_FLAG_OVERLAPPED;最后一个参数hTemplateFile是指定模板文件,串口没有模板,选择NULL;
所以最后我们设置的CreateFile函数如下:
m_hCom=CreateFile(m_sPort,
GENERIC_READ|GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
FILE_FLAG_OVERLAPPED,
NULL);
在创建完串口后,最后进行句柄测试:
if(m_hCom==INVALID_HANDLE_VALUE)
{
AfxMessageBox("打开串口失败!");
return;
}
上面说到了异步,那什么是异步呢?异步是相对同步这个概念而言的。异步,就是说,在进行串口读写操作时,不用等到I/O操作完成后函数才返回,也就是说,异步可以更快得响应用户操作;同步,相反,响应的I/O操作必须完成后函数才返回,否则阻塞线程。对于一些很简单的通讯程序来说,可以选择同步,这样可以省去很多错误检查,但是对于复杂一点的应用程序,异步是最佳选择;
第二步,设置串口,并创建串口线程。串口有很多的属性,上面也已经介绍了一些最重要的参数。这里不得不介绍一个重量级的数据结构DCB:
typedef struct _DCB { // dcb
DWORD DCBlength; //DCB结构体大小
DWORD BaudRate; //波特率
DWORD fBinary: 1; //是否是二进制,一般设置为TRUE
DWORD fParity: 1;//是否进行奇偶校验,我做的是ARM嵌入式,所以FALSE
DWORD fOutxCtsFlow:1; //CTS线上的硬件握手
DWORD fOutxDsrFlow:1; //DSR线上的硬件握手
DWORD fDtrControl:2; //DTR控制
DWORD fDsrSensitivity:1;
DWORD fTXContinueOnXoff:1;
DWORD fOutX: 1; //是否使用XON/XOFF协议
DWORD fInX: 1; //是否使用XON/XOFF协议
DWORD fErrorChar: 1; //发送错误协议
DWORD fNull: 1;
DWORD fRtsControl:2;
DWORD fAbortOnError:1;
DWORD fDummy2:17;
WORD wReserved;
WORD XonLim; //设置在XON字符发送之前inbuf中允许的最少字节数
WORD XoffLim; //在发送XOFF字符之前outbuf中允许的最多字节数
BYTE ByteSize; //数据宽度,一般为8,有时候为7
BYTE Parity; //奇偶校验
BYTE StopBits; //停止位数
char XonChar; //设置表示XON字符的字符,一般是采用0x11这个数值
char XoffChar; //设置表示XOFF字符的字符,一般是采用0x13这个数值
char ErrorChar;
char EofChar;
char EvtChar;
WORD wReserved1;
} DCB;
大家不要被这个结构体“强大”的身躯所吓倒,我这里只是为了向大家展示一下DCB的所有内部数据成员,其实我们真正在串口编程中用到的数据成员没有几个。
用DCB进行串口设置时,应先调用API’函数GetCommState,来获得串口的设置信息:
GetCommState(m_hCom, &dcb);
然后在需要设置的地方对dcb进行设置,然后再末尾调用
SetCommState(m_hCom,&dcb)
就可以了,还是比较方便的。然后调用SetCommMask,用来指定程序接收特定的串口事件,调用SetupComm函数,设置串口缓冲区大小:
SetCommMask(m_hCom, EV_RXCHAR);
//EV_RXCHAR表示当有字符在inbuf中时产生这个事件
SetupComm(m_hCom, MAXBLOCK, MAXBLOCK);
还有,串口因为是I/O操作,可能会产生错误,这时候需要设置超时限制,以避免阻塞现象。设置超时设置需要一个结构体COMMTIMEOUTS:
typedef struct _COMMTIMEOUTS {
DWORD ReadIntervalTimeout; //两个字符之间的超时设置
DWORD ReadTotalTimeoutMultiplier; //读操作时总的超时系数
DWORD ReadTotalTimeoutConstant; //读操作时总的超时常数
DWORD WriteTotalTimeoutMultiplier; //写操作时总的超时系数
DWORD WriteTotalTimeoutConstant; //写操作时总的超时常数
} COMMTIMEOUTS,*LPCOMMTIMEOUTS;
我的设置如下:
COMMTIMEOUTS timeouts;
timeouts.ReadIntervalTimeout=MAXDWORD;
timeouts.ReadTotalTimeoutConstant=0;
timeouts.ReadTotalTimeoutMultiplier=0;
timeouts.WriteTotalTimeoutConstant=50;
timeouts.WriteTotalTimeoutMultiplier=2000;
SetCommTimeouts(m_hCom, &timeouts);
这里将ReadIntervalTimeout设置为最大字节数,.ReadTotalTimeoutConstant和ReadTotalTimeoutMultiplier都设置为0,表示不设置读操作超时,也就是说读操作瞬间完成,不进行等待。
接下去是一步比较关键的操作,建立工作者线程,用来监听串口消息,如果发现inbuf中有接收到的字符,及时通知相应处理函数进行处理。
调用MFC全局函数AfxBeginThread建立线程。好的线程应该短小精悍,所以,我在这个线程里面其实什么事也不做,只是起到通知别的函数的作用。
m_pThread=AfxBeginThread(CommProc,
this,
THREAD_PRIORITY_NORMAL,
0,
CREATE_SUSPENDED,// 挂起线程
NULL);
m_pThread就是指向我新创建的线程的指针。
线程函数如下,有点长,但是已经是最简单的线程了:
//串口线程
UINT CommProc(LPVOID lParam)
{
COMSTAT commstat;//这个结构体主要是用来获取端口信息的
DWORD dwError;
DWORD dwMask;
DWORD dwLength;
OVERLAPPED overlapped;
//OVERLAPPED结构体用来设置I/O异步,具体可以参见MSDN
memset(&overlapped, 0, sizeof(OVERLAPPED));
//初始化OVERLAPPED对象
overlapped.hEvent=CreateEvent(NULL, TRUE, FALSE, NULL);
//创建CEvent对象
CUartDlg* dlg=(CUartDlg*)lParam;
if(dlg->m_hCom==NULL)
{
AfxMessageBox("串口句柄为空!");
return -1;
}
while(dlg->m_bConnected)
{
ClearCommError(dlg->m_hCom, &dwError, &commstat);
if(commstat.cbInQue)
//如果串口inbuf中有接收到的字符就执行下面的操作
{
WaitForSingleObject(dlg->m_hPostMsgEvent, INFINITE);
//无线等待。。。
ResetEvent(dlg->m_hPostMsgEvent);
//设置CEvent对象为无信号状态
::PostMessage(dlg->m_hWnd, WM_COMMSG, EV_RXCHAR, 0);
//发送特定信息,用来通知特定函数进行处理
continue;
}
if(!WaitCommEvent(dlg->m_hCom, &dwMask, &overlapped))
{
if(GetLastError()==ERROR_IO_PENDING)
//如果操作被挂起,也就说正在读取或这在写,则进行下面的操作
GetOverlappedResult(dlg->m_hCom,
&overlapped, &dwLength, TRUE);
//无限等待这个I/O操作的完成
else
{
CloseHandle(overlapped.hEvent);
return (UINT)-1;
}
}
}
CloseHandle(overlapped.hEvent);
return 0;
}
因为是多线程,所以,要注意的是对临界资源的访问问题,也就是说互斥问题,避免死锁现象的发生。所以,在这个线程中多次使用了事件对象CEvent,通过它来标志串口有没有被占据,和标志是否正在进行读取串口(串口无法同时进行读写操作)。具体的大家可以看我的代码注释 : )
这是我对这个线程发出的消息进行处理的函数:
void CUartDlg::OnComMsg(WPARAM wParam, LPARAM lParam)
{
char buf[MAXBLOCK/4];
CString str;
int nLength;
int nStartChar, nEndChar;
if(!m_bConnected || (wParam & EV_RXCHAR)!=EV_RXCHAR)
// 是否是EV_RXCHAR事件?
{
SetEvent(m_hPostMsgEvent);
// 允许发送下一个线程读取消息
return;
}
nLength=ReadComm(buf,100);
buf[nLength]='/0';
if(nLength)
{
//IDC_EDIT_EDIT是我在一个对话框上一个CEdit控件的ID号,大家可设置成
//自己的控件ID号
GetDlgItem(IDC_EDIT_EDIT)->SetFocus();
CString str(buf);
m_strMessage+=str;
UpdateData(FALSE);
CEdit* pEdit=(CEdit*)GetDlgItem(IDC_EDIT_EDIT);
pEdit->GetSel(nStartChar, nEndChar);
pEdit->SetSel(nStartChar-2, nEndChar-2);
}
SetEvent(m_hPostMsgEvent); // 允许发送下一个线程读取消息
}
第三步,已经建立好了工作者线程,那么接下去我们就可以进行串口的读写操作了。
DWORD CUartDlg::ReadComm(char *buf, DWORD dwLength)
{
COMSTAT comstat;
DWORD dwError;
DWORD length;
DWORD dwByteReaded;
ClearCommError(m_hCom, &dwError, &comstat);
length=min(comstat.cbInQue, dwLength);
if(!ReadFile(m_hCom, buf, length, &dwByteReaded, &m_osRead))
return 0;
return dwByteReaded;
}
这是读串口函数;
DWORD CUartDlg::WriteComm(char *buf, DWORD dwLength)
{
BOOL fState=FALSE;
DWORD length=0;
COMSTAT ComStat;
DWORD dwErrorFlags;
//ClearCommError是用来清除Comm中的错误,从而可以在下面的代码通过
//GetLastError抓取错误
ClearCommError(m_hCom,&dwErrorFlags,&ComStat);
fState=WriteFile(m_hCom,buf,dwLength,&length,&m_osWrite);
if(!fState)
{
if(GetLastError()==ERROR_IO_PENDING)
{
SetEvent(m_osWrite.hEvent);
while(!GetOverlappedResult(m_hCom,&m_osWrite,&length,TRUE))// 等待
{
if(GetLastError()==ERROR_IO_INCOMPLETE)
continue;
}
}
else
length=0;
}
return length;
}
这是写串口函数。这两个函数其实本质是一样的,操作过程也近似,大家可以参考着写。
第四步,好了,现在一个串口程序大致上已经完工了,呵呵,是不是挺繁琐?确实,用windows API函数进行硬件层次的编程都是比较繁琐的。还有一点,就是在结束程序的时候,千万不要忘了关闭串口的句柄,否则容易造成内存泄露的问题!
void CUartDlg::OnClose()
{
// TODO: Add your message handler code here and/or call default
if(m_bConnected)
{
m_bConnected=FALSE;
SetEvent(m_hPostMsgEvent);
SetCommMask(m_hCom, 0);
WaitForSingleObject(m_pThread->m_hThread, INFINITE);
m_pThread=NULL;
CloseHandle(m_hCom);
}
CDialog::OnClose();
}
总结:串口通讯应用非常广泛,特别是在硬件设计领域,更是没有串口不行。但是VC爱好者中懂串口编程的不多。我想,还是大家比较喜欢上层的东西吧。
在编写这个程序的时候,大体上已经写的差不多了,可以读取ARM机串口发送过来的字符,并显示出来,但是就是不能发送通过串口发送命令给ARM机。我在网上看了好多信息,也查阅了很多相关书籍,仍然没有找到答案。最后只能自己埋头一句代码一句代码得找,找了两天,还是没有找到,人差不多已经到了崩溃的边缘了,最后突然灵感发现,将错误锁定在dcb参数设定上。原来,我在dcb硬件握手参数上设置错了,那个fOutxCtsFlow参数应该设置成FALSE,否则串口读操作将一直处于阻塞状态。当时我差不多想亲吻每一个人,呵呵,程序员常常是带有一点病态的 : ) 希望大家以后不要犯我这样的错误。
还有,就是因为基于对话框(Dialog-based)的应用程序是不能接收WM_CHAR这个消息的,所以我在进行写串口操作时,重载了PreTranslateMessage这个函数,然后在这个函数内部对消息进行分检处理,起到了很好的效果。大家可以试一试。
因为一大早的,寝室里居然因为线路改造停电,所以只能依靠笔记本里可怜的剩余电池来写这篇文章,时间仓促,肯定有很多不足的地方,大家可以向我提出来,我一定认真回答改正,谢谢!