2 Visual C++ 6.0中串口控件的新特征
在Visual C++ 6.0中,串行通信的控件不再叫做OCX控件,而是改名为ActiveX控件,通用于Visual Basic、Delphi 以及诸多Internet应用程序中。与Visual C++ 4.x中的串口控件相比,其最显著的变化一是将GetInput()函数的返回类型改为VARIANT(以前为CString);二是增加了一个新的属性:InputMode(输入方式),可以设置为InputMode(0)或InputMode(1),分别表示以文本方式(字符串)或以二进制方式读入接收到的字节串。这两大改进为我们解决下面将要介绍的某些问题提供了强有力的武器。
3 使用串口控件时一些常见问题的产生原因及解决办法
概括起来,在使用串口控件时,经常会遇到以下一些问题:
3.1 如何发送00H
在《用…》文给出的示例程序中,需要发送的是一个简单的字符串,所以使用了一个CString类型的变量来存储它,然后使用COleVariant类的构造函数将之转换为SetOutput()函数所需要的VARIANT类型的参数(VARIANT和COleVariant可以通用)。由于CString是以00H作为字符串结束标志的,所以如果要发送的串中含有00H,则00H及其后的字节都将被CString舍弃,导致只有部分字节被发送。要解决这个问题,必须使用其它类型的变量来存储待发送的字节串。
查看一下COleVariant类的构造函数可知,COleVariant()可以接受多种类型的参数,除了CString外,比较常用的还有BYTE、short、long、float、double及CByteArray等。由于通过串口发送的数据常常是一串二进制字节,因此CByteArray将是最合适的一个数据类型。CByteArray用于存储一个动态的BYTE数组(可以动态改变数组大小)。当待发送的字节串准备好后,我们可以定义一个CByteArray的临时变量,然后使用SetSize()函数设置其大小(包含的字节数),并将相应的字节值存入该数组,最后利用COleVariant()转换为COleVariant类型,即可利用SetOutput()函数发送出去(详见示例中Transmitt()函数)。
3.2 中文Windows下通信有时会出错
有些编制好的通信程序在英文Windows下运行一直很正常,但在中文Windows运行却常常出错,特别是一些大于80H的字节,接收和发送时会出现误码。产生这个问题的主要原因是由于Windows使用不同类型的字符串而导致的。
Windows使用两种类型的字符串,即ANSI字符串和Unicode字符串。所有16位的应用程序都使用ANSI字符串;而32位应用程序既可以使用ANSI字符串,也可以使用Unicode字符串。ANSI以unsigned char类型存储字符串,每个字节表示一个不同的字符(与DOS相同);而Unicode以unsigned short类型存储字符串,每两个字节表示一个不同的字符。使用Unicode字符串的一个好处就是便于应用程序的本地化(对我们来说就是汉化),因为汉字就是使用的两个字节的内码(大于80H的ASCII码多被汉字内码所使用)。但在Windows 95中,Windows系统调用使用的是ANSI字符串,因此在中文Windows下的应用程序中,我们必须进行Unicode 字符串和ANSI字符串之间的相互转换。
在Visual C++ 4.x中,读入串口接收数据的函数GetInput()返回的是CString类型,如没有进行上述转换,就有可能导致误码出现。前面提到,在Visual C++ 6.0中,GetInput()返回的是VARIANT类型,由于VARIANT结构中包含一个多种数据类型的联合(union),使得我们可以避开使用字符串。同时Visual C++ 6.0串口控件的另一个新属性InputMode
可以将输入方式设置为Binary(二进制),从而为我们避开使用字符串进一步铺平了道路。
3.3 不知如何使用VARIANT数据类型
有不少读者对VARIANT这个新的数据类型大感头疼。SetOutput()函数中需要的VARIANT参数还可以使用COleVariant类的构造函数简单生成,现在GetInput()函数的返回值也成了VARIANT类型,很多读者往往不知该如何从返回的值中提取有用的内容。VARIANT及由之而派生出的COleVariant类主要用于在OLE自动化中传递数据。实际上VARIANT也只不过是一个新定义的结构罢了,它的主要成员包括一个联合体及一个变量。该联合体由各种类型的数据成员构成,而该变量则用来指明联合体中目前起作用的数据类型。我们所关心的接收到的数据就存储在该联合体的某个数据成员中。该联合体中包含的数据类型很多,从一些简单的变量到非常复杂的数组和指针。由于通过串口接收到的内容常常是一个字节串,我们将使用其中的某个数组或指针来访问接收到的数据。这里推荐给大家的是指向一个SAFEARRAY类型变量的指针parray。新的数据类型SAFEARRAY正如其名字一样,是一个“安全数组”,它能根据系统环境自动调整其16位或32 位的定义,并且不会被OLE改变(某些类型如BSTR在16位或32位应用程序间传递时会被OLE翻译从而破坏其中的二进制数据)。大家无须了解SAFEARRAY的具体定义,只要知道它是另外一个结构,其中包含一个(void *)类型的指针pvData,其指向的内存就是存放有用数据的地方。
简而言之,从GetInput()函数返回的VARIANT类型变量中,找出parray指针,再从该指针指向的SAFEARRAY变量中找出pvData指针,就可以向访问数组一样取得所接收到的数据了。(详见示例程序中OnCommMscomm()及SaveData()函数)
4 一个实用的通信示例程序
下面给出一个完整的通信示例程序,该程序能在两台计算机之间根据指定要求进行数据通信。每台计算机既能发送也能接收各种二进制数据,并且采取了事件驱动的接收方式(Event-drive,类似于DOS中的中断)。其中的许多程序代码具有通用性,大家可以直接或稍加改动后用于自己的应用程序中。
4.1 程序功能简介
从任一台机上可以向另一台机发送一命令帧,请求对方发回一数据帧。该数据帧中包含的有效数据长度(字节数)、起始值及相应两数据之间的增量都可以由用户指定,并通过命令帧传递过来。收到命令帧的计算机发送指定要求的数据帧到对方。接收和发送的内容都显示在对话框中。
4.2 通信规约
命令帧格式如下:
帧长度 特征码00H 起始值 字节数 增量 校验和
其中校验和为前面所有字节的无进位累加和,用于检验通信是否出错。
对命令帧,帧长度=6。
数据帧格式如下:
帧长度 特征码0FFH 指定起始值、字节数及增量的字节串校验和
其中校验和同命令帧。对数据帧来说,帧长度=命令帧中指定的字节数+3。
4.3 具体实现过程
第一步,启动Visual C++ 6.0,新建一个基于对话框的应用程序TxRx。
第二步,插入串口控件。在Visual C++ 6.0版中插入控件的方法与4.x中不同,具体步骤是:选择Project菜单下Add To Project子菜单中的Components and Controls…选项,在随后的对话框中双击Registered ActiveX Controls项,则所有注册过的ActiveX控件出现在列表框中。选择Microsoft Communications Control, version 6.0,这就是新的串口控件,单击Insert按钮将它插入到我们的Project中来。
接下来的工作就是要改造应用程序的主对话框IDD_TXRX_DIALOG,删掉其中的静态文本及“确定”按钮,将“取消”按钮改为“退出”,然后增加新的静态文本、编辑框和按钮控件,并为它们添加相应的变量(表1),
表1 主对话框中新增控件及相应变量
控件 控件ID 变量名 变量类型
按钮 IDC_TRANSMITT 发送按钮
Edit IDC_TXDATA m_TxData CString
Edit IDC_RXDATA m_RxData CString
Edit IDC_INIT m_Init BYTE
Edit IDC_LENGTH m_Length BYTE
Edit IDC_STEP m_Step BYTE
串口 IDC_MSCOMM m_ComPort CMSComm
第四步,需要修改TxRxDlg.cpp文件,添加有关程序代码。具体步骤如下:
首先,在文件头部第一条注释行前加入如下常数定义及全局变量说明。
#define comEvReceive 2
unsigned char RcvData[300]; //接收数据存储区
static int SavePointer=0; //数据存储指针
unsigned char TxData[300]; //发送数据存储区
其次,需要初始化串口参数。在OnInitDialog()函数中TODO语句后加入
以下初始化代码:
// TODO: Add extra initialization here
m_ComPort.SetCommPort(1); //选择COM1
if (!m_ComPort.GetPortOpen()) //打开串口
m_ComPort.SetPortOpen(TRUE);
m_ComPort.SetInputMode(1); //设置输入方式为二进制方式
//设置波特率等参数
m_ComPort.SetSettings("9600,n,8,1");
m_ComPort.SetRThreshold(1);
/* 将Rthreshold参数设为1表示每当串口接收缓冲区中有多于或等于1个字符时将引发一个关于comEvReceive(接收数据)的OnComm事件 */
m_ComPort.SetInputLen(0); //先预读缓冲区以清除残留数据
m_ComPort.GetInput();
接着,为发送按钮IDC_ TRANSMITT添加BN_CLICKED(鼠标单击)消息处理函数OnTransmitt() ,内容如下:
void CTxRxDlg::OnTransmitt()
{
UpdateData(TRUE); //获取用户输入的数据
TxData[0]=0x06; //命令帧长度为6
TxData[1]=0x00; //命令帧特征码为00H
TxData[2]=m_Init; //指定数据初值
TxData[3]=m_Length; //指定数据长度值
TxData[4]=m_Step; //指定数据增量值
Transmitt(); //发送命令帧
}
接着,为类CTxRxDlg添加成员函数void Transmitt(void),其功能是将存储在TxData数组中的内容通过串口发送出去。
void CTxRxDlg::Transmitt()
{ int i;
unsigned char Sum=0,Count;
CByteArray array;
char sOutput[10];
Count=TxData[0]; //帧长度
for(i=0; i<Count-1; i++)
Sum+=TxData; //计算校验和
TxData[Count-1]=Sum; //存储校验和
array.RemoveAll(); //清空数组
array.SetSize(Count); //设置数组大小为帧长度
for(i=0; i<Count; i++) //把待发送数据存入数组
array.SetAt(i,TxData);
// 打开串口并发送数据
if (!m_ComPort.GetPortOpen())
m_ComPort.SetPortOpen(TRUE);
m_ComPort.SetOutput(COleVariant(array));
// 在对话框中显示发送出去的数据
m_TxData="";
for(i=0; i<Count; i++)
{ sprintf(sOutput,"%02X, ",TxData);
m_TxData+=sOutput;
}
UpdateData(FALSE); //更新对话框
}
至此,关于发送数据的代码已添加完毕,读者可以从中看到使用CByteArray来发送二进制字符串是何等的容易!
下面,我们将添加有关接收和处理数据的代码。为使用事件驱动的接收方式,我们需要为串口控件IDC_MSCOMM添加处理OnComm事件的函数(通过ClassWizard)OnCommMscomm(),内容如下:
void CTxRxDlg::OnCommMscomm()
{
VARIANT vResponse;
int k;
if (m_ComPort.GetCommEvent()==comEvReceive)
{ //判断是否comEvReceive事件
k=m_ComPort.GetInBufferCount();
If (k>0)
{
m_ComPort.SetInputLen(k);
//读取接收到的数据
vResponse=m_ComPort.GetInput();
SaveData(k,(unsigned char *) vResponse.parray->pvData);
//保存接收到的数据
}
}
}
在本段程序中,我们先定义了一个VARIANT类型的变量vResponse,利用它读取串口数据后,再通过它获取parray指向的SAFEARRAY对象中的pvData指针,则可以访问所需数据。下面我们看一看在SaveData()函数中如何使用该指针获取数据。
为类CTxRxDlg添加成员函数void SaveData(int Count, unsigned char *pbVal),内容如下:
void CTxRxDlg::SaveData(int Count, unsigned char *pbVal)
{
int i;
for(i=0; i<Count; i++)
RcvData[SavePointer+i]=pbVal; //存储数据
SavePointer+=Count;
//如接收计数指针超过一帧长度,则处理该帧数据
if (SavePointer>=RcvData[0])
ProcessData();
}
最后剩下的工作就是处理收到的数据。为简化起见,本程序将首先检测收到的一帧数据是否正确(通过校验和),然后判断是否是命令帧(通过特征码),是则按要求发送相应的数据帧。最后将接收到的内容显示在对话框中。
为类CTxRxDlg添加成员函数void ProcessData(void),其内容如下:
void CTxRxDlg::ProcessData()
{
int i, Count;
unsigned char Sum=0;
char sInput[10];
Count=RcvData[0] //帧长度
for(i=0; i<Count-1; i++)
Sum+=RcvData; //计算校验和
if (Sum!=RcvData[Count-1]) //校验和不正确
{
MessageBox("收到数据有误:校验和不正确!");
m_ComPort.SetOutBufferCount(0);
SavePointer=0; //清除缓冲区中全部数据
}
else
{
int Length;
unsigned char AL;
MessageBeep(0xFFFFFFFF);
if (RcvData[1]==0) //是否命令帧
{
TxData[0]=RcvData[3]+3; //数据帧长度
TxData[1]='\xFF'; //数据帧特征码
TxData[2]=RcvData[2]; //数据起始值
Length=RcvData[3]; //数据长度
AL=RcvData[4]; //数据增量
//根据起始值和增量计算其它数据
for (i=1; i<Length; i++)
TxData[i+2]=TxData[i+1]+AL;
Transmitt(); //发送数据帧
}
m_RxData=""; //显示接收到的数据
for(i=0; i<Count; i++)
{
sprintf(sInput,"%02X, ",RcvData);
m_RxData+=sInput;
}
UpdateData(FALSE);
//从接收存储区中清除已处理过的一帧数据
for(i=0; i<SavePointer-Count; i++)
RcvData=RcvData[Count+i];
SavePointer-=Count;
}
}
现在,我们已完成了全部编程工作。可以看到,并没有花很大的工作量,而我们已编写出了一个功能较强的串口通信程序。
4.4 程序运行演示
编译、连接以上程序,将生成的可执行文件TxRx.exe拷贝到两台计算机中(如该计算机没有安装Visual C++,则还需要把相关的动态链接库拷贝到其Windows目录下的System子目录中,例如mscomm32.ocx、msvcrt.dll、mfc42.dll 等),用一根电缆将两台计算机的串口连接起来(只需三根信号线:Gnd直接相连、Txd和Rxd对接即可),运行TxRx应用程序,在其中一台计算机上输入起始值、字节数和增量等数值,然后按下“发送”按钮,可以看到命令和数据在两台机之间传递的过程。
转自:http://playjian.blog.163.com/blog/static/174052262201132221733920/