前日有同学问起,特此分享:
https://blog.axqd.net/2004/10/19/serial-communication-with-borland-c-builder/
https://blog.axqd.net/2004/10/19/serial-communication/
标签: 沈弘
在C++ Builder中利用串行通信控件编程
摘要:串口是常用的计算机与外部串行设备之间的数据传输通道,由于串行通信方便易行,所以应用广泛。本文介绍了在C++ Builder中如何利用串行通信控件进行串行通信编程。
一、引言
目前,在用计算机进行数据传输时,常用的是串行通信方式。用C++ Builder来编写串行通信程序时,可以调用Windows API函数,也可以利用VB中的MSComm控件。 利用 API函数编写实际应用程序时,往往要考虑多线程的问题,这样编出来的程序不但十分庞大,而且结构比较复杂,继承性差,维护困难。但是使用串行通信控件就相对简单一些,而且功能强大,性能安全可靠。本文就简单的介绍一下在C++ Builder中利用MSComm控件进行编程。
二、MSComm控件的常用属性和事件
MSComm 控件通过串行端口传输和接收数据,为应用程序提供串行通讯功能。具体的来说,它提供了两种处理通信问题的方法:一是事件驱动(Event-driven)方法,一是查询法。
事件驱动方式
在使用事件驱动法设计程序时,每当有新字符到达,或端口状态改变,或发生错误时,MSComm控件将解发OnComm事件,而应用程序在捕获该事件后,通过检查MSComm控件的CommEvent属性可以获知所发生的事件或错误,从而采取相应的操作。这种方法的优点是程序响应及时,可靠性高。
查询方式
查询方式实质上还是事件驱动,但在有些情况下,这种方式显得更为便捷。在程序的每个关键功能之后,可以通过检查 CommEvent 属性的值来查询事件和错误。如果应用程序较小,并且是自保持的,这种方法可能是更可取的。
1.MSComm 控件的常用属性
CommPort属性:设置或返回通讯端口号,可以设置为1到16之间的任何值,本系统采用缺省值2;
Settings属性:以字符串形式设置或返回波特率、奇偶校验、数据位和停止位,本系统采用缺省值”9600,n,8,1″;
PortOpen属性:设置或返回通讯口的状态以及打开和关闭端口,可通过把该属性设置为true或者false来打开或者关闭端口;
InBufferSize和OutBufferSize属性:分别设置接收和发送缓冲区分配的内存数量,单位为字节,缺省值分别为1024byte和512byte;
InputLen属性:确定希望从接收缓冲区移出的字符数量,当InputLen=0时,一次把接收缓冲区的字符全部移出;
Input属性:从接收缓冲区中读出数据,然后将该数据从缓冲区移走。
OutPut属性:向发送缓冲区传递待发送的数据。
InBufferCount和OutBufferCount属性:分别确定当前驻留在接收缓冲区等待被取出和发送缓冲区准备发送的字符数量,这两个属性设置为0,接收和发送缓冲区的内容将被清除;
InputMode属性:设置接收传入数据的格式,设置为0采用文本形式,设置为1采用二进制格式,本系统设置为二进制格式进行发送和接收;
SThreshold属性:保存一个产生发送OnComm事件的界限值,本系统设置该属性为0,发送数据时不产生OnComm事件;
RThreshold属性:设定当接收几个字符时触发OnComm事件,本系统设置该属性为1,每接收一个字符就产生一个OnComm事件;
2.MSComm控件的事件
MSCOMM控件只使用一个事件OnComm,用属性CommEvent的十七个值来区分不同的触发时机。主要有以下几个:
(1)CommEvent=1时:传输缓冲区中的字符个数已少于Sthreshold(可设置的属性值)个。
(2)CommEvent=2时:接收缓冲区中收到Rthreshold(可设置的属性值)个字符,利用此事件可编写接收数据的过程。
(3)CommEvent=3时:CTS线发生变化。
(4)CommEvent=4时:DSR线发生变化。
(5)CommEvent=5时:CD线发生变化。
(6)CommEvent=6时:检测到振铃信号。
另外十种情况是通信错误时产生,即错误代码。
三、程序的实现
1.注册MSComm控件
众所周知,C++Builder本身并不提供串行通讯控件MSComm,但我们却可以通过注册后直接使用它。启动C++Builder5.0后,然后选择C++Builder主菜单中的Component菜单项,单击Import Active Control命令,弹出Import Active窗口,选择Microsoft Comm Control6.0,再选择Install按钮执行安装命令,系统将自动进行编译,编译完成后即完成MSComm控件在C++Builder中的注册, 系统默认安装在控件板的Active页,接下来我们就可以像使用C++Builder本身提供的控件那样使用新注册的MSComm控件了。(前提条件是你的机子上安装了Visual Basic,或者有它的库)
2.具体实现
新建一个工程Project1,把注册好的MSComm控件加入到窗体中,然后再加入5个ComboBox用来设置串口的属性,4个Button分别用来”打开串口” “关闭串口””发送数据””保存数据” ,2个Memo控件分别用来显示接收到的数据和发送的数据。再加入一个Shape控件用来标明串口是否打开。
ComboBox1用来设置串口号,通过它的Items属性设置1,2,3,4四个列表项分别表示COM1,COM2,COM3,COM4口。 ComboBox2用来设置波特率,ComboBox3用来设置奇偶校验位,ComboBox4用来设置数据位,ComboBox5用来设置停止位。他们的缺省值分别是9600,n,8,1。
Button1用来打开串口,Button2用来关闭串口,Button3用来发送数据,Button4用来保存数据。Memo1用来显示发送的数据,Memo2显示接收的数据。Shape1的Shape属性设置为stCircle。
下面给出部分源码:
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
if(MSComm1->PortOpen==true)
{
Button1->Enabled=false;
Button2->Enabled=true;
Button3->Enabled=true;
Button4->Enabled=true;
Shape1->Brush->Color=clGreen;
}
else
{
Button2->Enabled=true;
Button2->Enabled=false;
Button3->Enabled=false;
Button4->Enabled=false;
Shape1->Brush->Color=clRed;
}
}
void __fastcall TForm1::Button1Click(TObject *Sender) / /打开串口
{
if(MSComm1->PortOpen!=true)
{
MSComm1->CommPort=StrToInt(ComboBox1->Text);//选择串口号
MSComm1->Settings=
ComboBox2->Text+","+
ComboBox3->Text+","+
ComboBox4->Text+","+
ComboBox5->Text; file://设置串口的属性波特率、奇偶校验、数据位和、//停止位。
MSComm1->InputMode=0;//设置传入数据的格式,0表示文本形式
MSComm1->PortOpen=true;//打开串口
Button1->Enabled=false;
Button2->Enabled=true;
Button3->Enabled=true;
Button4->Enabled=true;
Shape1->Brush->Color=clGreen;
}
}
void __fastcall TForm1::Button2Click(TObject *Sender) / /关闭串口
{
if(MSComm1->PortOpen!=false)
{
MSComm1->PortOpen=false;
Button1->Enabled=true;
Button2->Enabled=false;
Button3->Enabled=false;
Button4->Enabled=false;
Shape1->Brush->Color=clRed;
}
else
{
Button1->Enabled=false;
Button2->Enabled=true;
Shape1->Brush->Color=clRed;
}
}
MSComm控件的Input和Output属性在Object Inspector中是看不到的,而且在C++Builder环境下这两个属性已不在是VB、VC中的原类型,而是OleVariant类型,也就是 Ole万能变量,这就需要我们在发送接收数据时要把数据转换成Ole类型。
void __fastcall TForm1::Button3Click(TObject *Sender) file://发送Memo2中的数据
{
MSComm1->Output=StringToOleStr(Memo2->Text); file://把AnsiString型转化成//Ole形式。
}
通过OnComm事件接收数据,必须把MSComm的RThreshold属性设置为大于0,只有这样在接收到字符时才会产生一个OnComm事件。
void __fastcall TForm1::MSComm1Comm(TObject *Sender)
{
AnsiString str; file://声明一个AnsiString类型的变量
OleVariant s; file://声明一个用于接收数据的OleVariant变量。
if(MSComm1->CommEvent==comEvReceive)
// 接收缓冲区中是否收到Rthreshold个字符。
{
if(MSComm1->InBufferCount)// 是否有字符驻留在接收缓冲区等待被取出
{
s=MSComm1->Input;//接收数据
str=s.AsType(varString); file://把接收到的OleVariant变量转换成AnsiString类型
Memo1->Text=Memo1->Text+str;//把接收到的数据显示在Memo1中。
}
}
}
要保存数据应该再加入一个SaveDialog模块
void __fastcall TForm1::Button4Click(TObject *Sender)
file://把Memo1中的数据保存在指定的文件中
{
AnsiString filename1;
SaveDialog1->Filter="Text files (*.txt)|*.txt|All files (*.*)|*.*";//文件类型过滤器
SaveDialog1->FilterIndex=2;
if(SaveDialog1->Execute())
{
filename1=SaveDialog1->FileName;
Memo1->Lines->SaveToFile(filename1);//把收到的数据保存在文件filename1中
}
}
四、结束语
上面给出了C++ Builder中利用MSComm控件进行串行通信编程的实现和部分源码,有了上面的参照读者可以根据实际需要编写出具有发送文件和接收文件功能的程序。
Serial Communication with Borland C++ Builder
David Poinsett
November 1999
[email protected]
Introduction…
I wish this site had been around when I was trying to figure out how to make serial communications work in Windows95. I, like many programmers, was hit with the double-whammy of having to learn Windows programming and Win95 serial comm programming at the same time. I found both tasks confusing at best. It was particularly frustrating because I had, over the years, written so much stuff (including lots of serial comm software) for the DOS environment and numerous embedded applications. Interrupt driven serial comm, DMA transfer serial comm, TSR serial comm, C, assembler, various processors…you name it, it had written it. Yet, everything I knew seemed upside-down in the message-driven-callback world of Windows.
After spending lots of money on books and seemingly endless effort, I have finally gotten enough of a handle on Win95 and serial comm programming to write something usable in this environment. Borland’s C++ Builder has done a lot to help make Win95 programming easier and, once you know the tricks, the serial communications stuff is pretty easy, too.
The purpose of this site is to spare you hardship of my early efforts and get you up and running with your Win9x/NT serial comm programming as quickly as possible. If you’re already familiar with using BCB to develop Windows programs, the example code should be plenty to get you going. You can also download the source code in BCBComm.zip. Good luck.
The Example…
In the example that follows we’re going to write a bare-bones program to do serial communication. It will consist of a Form with a Memo object (for text I/O) and a Thread object that handles incoming serial data. There are no menus or other features to distract us from focusing on the serial comm aspect of the program. Obviously, you’ll want to add these and other elements to a fully functioning program.
Fire up BCB and start a New Project. Place a Memo object on Form1. Using the Object Inspector, set Memo1 properties as follows:
Alignment = alClient
MaxLength = 0
ScrollBars = ssVertical
WantReturns = true
WantTabs = false
WordWrap = true
Next, under the File | New menu, add a Thread Object. Use TRead for the class name when asked.
You should now have two Unit files: Unit1.cpp for Form1 activity and Unit2.cpp for the thread.
Using the Object Inspector again, create event handlers for the following events. The easiest way to create events handlers is as follows:
Go to the event tab sheet in Object Inspector.
Find the event of interest.
Double-click the blank space next to the event name.
If you follow this scheme, Object Inspector will create and automatically name the event handlers to the same name used in our examples. OK, here are the objects and the events we need to handle:
Form1 OnCreate
Form1 OnClose
Memo1 OnKeyPress
The framework for Unit1.cpp is now in place. Using the following listing as a guide, fill in Unit1.cpp with the following code. Be sure to note the #includes and global variables. If the framework for event handlers is missing in your program, DO NOT put it there by typing in the framework code! Go back and figure out what you missed. BCB MUST CREATE THE FRAMEWORK FOR YOU.
The Main Form…
//---------------------------------------------------------------------------
#include <vclvcl.h>
#pragma hdrstop
#include "Unit1.h"
// YOU MUST INCLUDE THE HEADER FOR UNIT2 (THE THREAD UNIT)
#include "Unit2.h"
// GLOBAL VARIABLES
HANDLE hComm = NULL;
TRead *ReadThread;
COMMTIMEOUTS ctmoNew = {0}, ctmoOld;
//---------------------------------------------------------------------------
#pragma resource "*.dfm"
TForm1 *Form1;
//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
//---------------------------------------------------------------------------
void __fastcall TForm1::FormCreate(TObject *Sender)
{
DCB dcbCommPort;
// OPEN THE COMM PORT.
// REPLACE "COM2" WITH A STRING OR "COM1", "COM3", ETC. TO OPEN
// ANOTHER PORT.
hComm = CreateFile("COM2",
GENERIC_READ | GENERIC_WRITE,
0,
0,
OPEN_EXISTING,
0,
0);
// IF THE PORT CANNOT BE OPENED, BAIL OUT.
if(hComm == INVALID_HANDLE_VALUE) Application->Terminate();
// SET THE COMM TIMEOUTS IN OUR EXAMPLE.
GetCommTimeouts(hComm,&ctmoOld);
ctmoNew.ReadTotalTimeoutConstant = 100;
ctmoNew.ReadTotalTimeoutMultiplier = 0;
ctmoNew.WriteTotalTimeoutMultiplier = 0;
ctmoNew.WriteTotalTimeoutConstant = 0;
SetCommTimeouts(hComm, &ctmoNew);
// SET BAUD RATE, PARITY, WORD SIZE, AND STOP BITS.
// THERE ARE OTHER WAYS OF DOING SETTING THESE BUT THIS IS THE EASIEST.
// IF YOU WANT TO LATER ADD CODE FOR OTHER BAUD RATES, REMEMBER
// THAT THE ARGUMENT FOR BuildCommDCB MUST BE A POINTER TO A STRING.
// ALSO NOTE THAT BuildCommDCB() DEFAULTS TO NO HANDSHAKING.
dcbCommPort.DCBlength = sizeof(DCB);
GetCommState(hComm, &dcbCommPort);
BuildCommDCB("9600,N,8,1", &dcbCommPort);
SetCommState(hComm, &dcbCommPort);
// ACTIVATE THE THREAD. THE FALSE ARGUMENT SIMPLY MEANS IT HITS THE
// GROUND RUNNING RATHER THAN SUSPENDED.
ReadThread = new TRead(false);
}
//---------------------------------------------------------------------------
void __fastcall TForm1::FormClose(TObject *Sender, TCloseAction &Action)
{
// TERMINATE THE THREAD.
ReadThread->Terminate();
// WAIT FOR THREAD TO TERMINATE,
// PURGE THE INTERNAL COMM BUFFER,
// RESTORE THE PREVIOUS TIMEOUT SETTINGS,
// AND CLOSE THE COMM PORT.
Sleep(250);
PurgeComm(hComm, PURGE_RXABORT);
SetCommTimeouts(hComm, &ctmoOld);
CloseHandle(hComm);
}
//---------------------------------------------------------------------------
void __fastcall TForm1::Memo1KeyPress(TObject *Sender, char &Key)
{
// TRANSMITS ANYTHING TYPED INTO THE MEMO AREA.
TransmitCommChar(hComm, Key);
// THIS PREVENTS TYPED TEXT FROM DISPLAYING GARBAGE ON THE SCREEN.
// IF YOU ARE CONNECTED TO A DEVICE THAT ECHOES CHARACTERS, SET
// Key = 0 WITHOUT THE OTHER STUFF.
if(Key != 13 && (Key < ' ' || Key > 'z')) Key = 0;
}
//---------------------------------------------------------------------------
Now we turn our attention to the thread code in Unit2.cpp. The framework should already be in place. Use this listing as a guide and fill in Unit2.cpp with the following code.
The Thread…
//---------------------------------------------------------------------------
#include <vclvcl.h>
#pragma hdrstop
// YOU MUST INCLUDE THE HEADER FOR UNIT1
#include "Unit1.h"
#include "Unit2.h"
extern HANDLE hComm;
char InBuff[100];
//---------------------------------------------------------------------------
// Important: Methods and properties of objects in VCL can only be
// used in a method called using Synchronize, for example:
//
// Synchronize(UpdateCaption);
//
// where UpdateCaption could look like:
//
// void __fastcall TRead::UpdateCaption()
// {
// Form1->Caption = "Updated in a thread";
// }
//---------------------------------------------------------------------------
__fastcall TRead::TRead(bool CreateSuspended)
: TThread(CreateSuspended)
{
}
//---------------------------------------------------------------------------
void __fastcall TRead::DisplayIt()
{
// NOTE THAT IN THIS EXAMPLE, THERE IS NO EFFORT TO MONITOR
// HOW MUCH TEXT HAS GONE INTO Memo1. IT CAN ONLY HOLD ABOUT 32K.
// ALSO, NOTHING IS BEING DONE ABOUT NON-PRINTABLE CHARACTERS
// OR CR-LF'S EMBEDDED IN THE STRING.
// DISPLAY THE RECEIVED TEXT.
Form1->Memo1->SetSelTextBuf(InBuff);
}
//---------------------------------------------------------------------------
void __fastcall TRead::Execute()
{
//---- Place thread code here ----
DWORD dwBytesRead;
// MAKE THE THREAD OBJECT AUTOMATICALLY DESTROYED WHEN THE THREAD
// TERMINATES.
FreeOnTerminate = true;
while(1)
{
// TRY TO READ CHARACTERS FROM THE SERIAL PORT.
// IF THERE ARE NONE, IT WILL TIME OUT AND TRY AGAIN.
// IF THERE ARE, IT WILL DISPLAY THEM.
ReadFile(hComm, InBuff, 50, &dwBytesRead, NULL);
if(dwBytesRead)
{
InBuff[dwBytesRead] = 0; // NULL TERMINATE THE STRING
Synchronize(DisplayIt);
}
}
}
//---------------------------------------------------------------------------
One last thing…
To do a synchronized call to DisplayIt() from within the thread’s Execute() function, DisplayIt() it must be declared as a __fastcall type in the header file. Here’s how to do it.
Open the header file “unit2.h” and add the DisplayIt() line as shown below:
//---------------------------------------------------------------------------
class TRead : public TThread
{
private:
protected:
void __fastcall DisplayIt(void); // ADD THIS LINE
void __fastcall Execute();
public:
__fastcall TRead(bool CreateSuspended);
};
//---------------------------------------------------------------------------
Notes…
As mentioned earlier this example focuses strictly on the core elements that make the serial communication functions work. In its present form it’s unlikely to be particularly useful or acceptable in an actual application. In other words, you need to add what’s missing. If you’ve followed along this far, that should not be too difficult. To minimize any confusion on what’s missing, I’ll highlight some of the areas that should be addressed:
There is little or no provision for error handling
The 32K display limit of the Memo object is not handled
For proper text display in Memo, ignore linefeeds and replace carriage returns with a CR-LF pair
Menus
Storing incoming serial data to disk
Sending disk contents out serial port
Handshaking
Protocol (Xmodem, Zmodem, etc.)
There are several ways to test your work. One method is to perform a loop-back test by jumping pins 2 and 3 on your computer’s RS-232 connector. With the loop-back connection anything you type into the Memo area will be echoed back.
Here are some online references that you might find useful:
Serial Communications in Win32 . This is a comprehensive reference.
www.ontrak.net . Excellent example of simple serial port access.
www.temporaldoorway.com . Good example of threaded serial program with overlapped I/O.
www.codeguru.com . Yet another example (more for VC++).
Good luck.
==============================================================================