HOME - - - - - Delphi Tutorials TOC - - - - - - Other material for programmers
Delicious  Bookmark this on Delicious    Recommend to StumbleUpon

Delphi tutorial: serial comms, i.e. using the COM port
Bi-directional Communications

This has good information, and a search button at the bottom of the page

Please don't dismiss it because it isn't full of graphics, scripts, cookies, etc!

This tutorial will show you how to write a Delphi program to send data out of your PC over a serial link. The program will also be able to read data arriving on the serial link.

Click here if you want to know more about the source and format of these pages. It may be easier to read this if you re-size the window, so that it does not use the full width of your screen.

You can download the free sourcecode for the application developed in this tutorial. The zip file also includes a compiled copy of the application. The compiled version is hard coded to operate on COM1 at 9600, with no handshake.

At 02 Apr 2010, 11:49, British time, this TUTORIAL is a Work-In-Progress! But the sourcecode and compiled .exe, available from the link above, are finished.


Where we are going

In the following we will build an application with three buttons. One will look to see if any data has arrived on the serial port, and the other two will send fixed messages out over the serial port, "Hello" and "Bye".

Screenshot of DD80

"What!??", you say. Sounds pretty lame, doesn't it. The application isn't meant to be "useful" as it stands. It is meant to give you a stripped down skeleton, on which you can build whatever it is that you are wanting to do with a serial comm port.

When you click the "See If There Is Incoming Data" button, a message box pops up on the screen, displaying any data which may have been in the serial port's receive buffer. (More on that later).

This isn't the place to explain the general principles of serial ports to you. I have a separate page with more general serial port information. (At 2 Apr 10, that page has a number of topics which need further development, but it already has some useful things for you.... among them information on RS-232 to TTL level shifting, and how to use an inexpensive USB device to provide your PC with a virtual serial port which works fine with the software developed in this tutorial. If you look a little farther, I think you will find that there are "serial port proxies", which allow you to have "a serial port"... on a computer that you are only "connected" to across a LAN, or even across the internet! Serial ports are NOT "easy", but they have been earning their keep for many, many years. Master them, and you have access to all sorts of things.)

If you've never done anything with serial ports, it may pay you to go to another one I wrote. That covers setting up a serial connection from a PC to some external device, but a connection that merely sends data. Even more basic that the objectives of the tutorial you are currently reading... but all of the things that you learn to do there are incorporated in what we will do here. You can make your headache less painful by attacking the job in two halves. But even if you don't take my advice, the page you are reading should be all you need to get started with bi-directional serial comms.

WARNING....

I'm old fashioned. I disavow any responsibility for things you may elect to do. In particular, I disavow any responsibility for any consequences arising from connections you make between devices. You can't just plug "anything" into your computer or anything else!. In particular, make sure that you aren't assuming that an RS-232 interface can be connected directly to, say, an Arduino or BASICstamp, or PIC or other microprocessor. There are notes on some of those issues on my serial ports page, for those of you who need them.

Enough "lawyer feeding".... let's get down to the Fun Stuff....

Write the program....

Start creating the application the way I encourage you always to work: Choose a robust name for it, and create a folder, probably within something named something like "Delphi prj-tkb", if your initials are "tkb". To follow along with what is to come, you can put the application's folder anywhere you like, but call it DD80. (For Eightieth Delphi Demo).

Fire up your Delphi... I'm using Delphi 4 under Windows XP. Put four buttons on the form, don't bother to change even their names yet. Name the form they are on DD80f1. You can give the form a better caption that it had at the time I did the screenshot above; you should have something better than "DD80f1" in the window's title bar when you run the application! Save the unit as DD80u1.pas (in a DD80 folder), and save the project as DD80.dpr. (The system will create the essential DD80u1.dfm, and a bunch of other things that aren't essential to backing up your work.)

Name the buttons as follows, and give them the captions shown....

Near the top of the unit, just after the "Uses" clause, start a "const" clause with....

const ver='2 Apr 10';

That's "const" for "constants", and "ver" for the program's version ID. I suggest using dates for "ids" because I can (mostly) manage what day it is, and whether there are other copies of a given program with today's date on them. I don't have to remember or look up "where I've got to" with version IDs.

And put a label on the form to display the version. Call it laVersion. Double click on the form to start an "OnCreate" handler, and put the following in it....

laVersion.caption:='DD80, version: '+ver;
top:=100;
left:=100;

(The "top" and "left" lines set properties of the form, and cause it to appear at the upper left of your screen when run. With these in place, then when the application isn't running, when you are working on it, you can drag the form to any convenient spot on your screen, and it will stay there for editing, but not be in some eccentric place when a user runs the application later.)

At the start of the code, at the start of the programming process, put some rems about the application. I find it amusing to see, later, notes about when the project started. It is useful to include things like web pages consulted, etc.

The start of the code is an excellent place to list "to do" items, and to maintain a log of the history of bug-fixes and the program's development.

Write your documentation as you go along. It isn't something you put in after the rest is finished.

A detail: Just after the...

{$R *.DFM}

... add...

{$R+}

(Usually a Good Idea. I'll spare you the details. The two "R+"s have nothing to do with one another, by the way. The presence of the "*.DFM" after the first one changes what the first one does.) End of "detail".

The sPortState flag

To access the serial data port, for sending data or for reading it, we need a "handle". And that handle will sometimes be set up for writing, sometimes for reading, and sometimes it will not be set up at all. It is a slight distortion, but I'd set off down this path before noticing the sloppy thinking. So you'll have to bear with me.... I'm going to be speaking of THE PORT being open for writing, open for reading, or closed, when in fact is isn't really any of these things. When I speak of "the port being open for read", what I mean is that we currently have a handle set up to use for reading data from the port. Every time we "do things" to the state of that handle, we are going to "make a note" of what we have done, and we will be storing that "note" in the variable "sPortState". (It should probably have been called "sHandleState"). The only values we will allow in that are....

We make it easy to ensure that only r, w or c can be in the variable if, just after the....

private
    { Private declarations }

...in the TDD80f class definition at the start of the unit, we add....

sPortState:set of (c,w,r);//For closed, open for write, open for read

How we use that variable will become clear as we go along. For a start, put the following in the OnFormCreate handler....

sPortState:=[c];//Changed to [r] if handle set up
//to read from the port, or [w] if
//handle set up to write to the port

Handling Handles

Have you dealt with "handles" before? They are complex entities holding a bunch of information. (To be precise, they are "records"... not in the database sense, in the programming sense). A handle makes complex things... like using serial ports... easier. It "packages" "stuff" that different routines will need. So, typically (and in this instance), you have to start by "setting up" a handle. This essentially means creating it, and filling in at least some of the information it can hold. You then use that handle as a parameter when calling subroutines. The subroutine will "know" to look in the handle for things it needs, and the subroutine will sometimes put some of the "answers" that it generates in known parts of the insides of the handle.

Don't let all of the above give you too much worry. In DD80 there just one handle, hCommPort... and you can infer how it works as we go along. But it is busy, and crucial to almost everything in this application.

The last shall be first

I often attach just "application.terminate" to the "Quit" button I give my programs. However in the case of DD80, we need to be a bit fancier. If the handle we were just talking about, hCommPort, has been set up for reading from or writing to the serial port, we should close it before the program ends. All it takes is....

closehandle(hCommFile);

However, there may be times when the handle is already closed. This is one of the jobs of the variable we created called sPortState.

Add an OnClose handler to your form....

procedure TDD80f1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
if sPortState<>[c] then closehandle(hCommFile);
//showmessage('x');//For testing that closehandle is done regardless
// of whether app closed with buQuit or red X. It is, as long as
// the buQuitClick handler is "close", not "application.terminate"
end;

(You can't just copy/paste that in its entirety. Use the Delphi IDE to open the shell of the OnClose handler, and then copy/paste the material between "begin" and "end".)

Once that's in place, create an onClick handler for the "Quit" button....

close;//close, not "application.terminate"... to get
// the "if sPortState<>[c]... processed.

Before the above will compile, you need to add....

hCommFile: THandle;

... to the variables declaration up at the top of the unit. Put the new line just after....

sPortState:set of (c,w,r);//For closed, open for....

Now your "Quit" button works!

Saving time....

I am now going to ask you to do something a little differently than you would do it if actually writing this application for yourself, but it will make things easier in the context of this tutorial.

Just after the....

hCommFile: THandle;

... which you just inserted, add....

bTmp:byte;
procedure WriteString(sToSend:string);
procedure SendChars(sToSend:string);
function SetUpSerPort:byte;
function OpenForRead:byte;
function OpenForWrite:byte;

And, so that what you have will compile, down at the bottom insert the following. Leave the rems. (The compiler would "throw out" empty subroutine definitions.)

procedure TDD80f1.WriteString(sToSend:string);
begin
//shell
end;

procedure TDD80f1.SendChars(sToSend:string);
begin
//shell
end;

function TDD80f1.SetUpSerPort:byte;
begin
//shell
end;

function TDD80f1.OpenForRead:byte;
begin
//shell
end;

function TDD80f1.OpenForWrite:byte;
begin
//shell
end;

Something simple...

Double click on the Send "Hello" and Send "Bye" buttons, and create their OnClick handlers as follows....

procedure TDD80f1.buSendHelloClick(Sender: TObject);
begin
SendChars('Hello');
end;

procedure TDD80f1.buSendByeClick(Sender: TObject);
begin
SendChars('Bye');
end;

Oh! If only that was all we needed to do! The program should "run" at this stage, but because our "SendChars" routine does nothing (yet), clicking the buttons does nothing. Sigh. But it's a start.

Fasten our seatbelts....

Take a break. Have some pizza. Take a walk around the block- dispense with today's "exercise" requirement. Because things are about to get difficult. (No, we haven't done anything difficult yet.)

We'll deal with the easier of two difficult matters first.

Little Difficulty: Protocols and their settings

"Serial data" comes in many shapes and sizes. And apart from some happy exceptions that you shouldn't rely on, if the "shape and size" your program is using doesn't match the shape and size of the thing at the other end of your serial cable, then the messages won't be exchanged successfully.

The simplest matter is that of the data rate. The Arduino works at 9600. (That is measured... sort of... in bits per second... or is it bytes per second? And does "baud" say the same thing, or will those readers who are more precise than me have a fit if I call it 9600 baud?) WHATEVER the right units are, I don't think I've ever seen more than one units used for the speed of transmission, so if the device you want to work with says that it works at 9600, you'll be okay with this program as written, and as distributed in compiled form.

I have spent hours over the years fighting with getting the settings right in pairs of pieces of equipment that I want to "talk" to one another over serial links. With professionally produced software it can be a struggle.

Part of the problem is that there are several agents with fingers in the pie. And, Murphy's law, some of the settings seem to change when you don't want them to, or not change when they "should" have.

The settings are sometimes set on a Windows or Linux machine by one of the Control Panel mini-apps. And they are sometimes set-table from within specific special purpose applications. You will have played with such things if you were around in the days of acoustic couplers for accessing dial up services.

The first program in this series, the one discussed in my "How to send serial data from a PC", didn't "do things" with the settings... it relied on what had been set by the Windows Control Panel mini-app. (You can also get at the settings via the Device Manager, if you know Microsoft's latest hiding place for this straightforward and useful tool.)

The program we are building in this exercise, DD80, may have a flaw in it. I'm pretty sure that it makes settings of the protocol when SetUpSerPort is called... but that is only called (and called repeatedly, shouldn't be necessary, if all it does is make the protocol settings).... only called while you are setting up the handle (hCommHandle) for reading something from the serial port.

Because of the way the program is written, you do set the handle up to read from the serial port very early in the program's execution, but I suspect that the placement of SetUpSerPort could be made more elegant.

That's the "easy difficulty" explored. Now we move on to....

Bigger Difficult: Taking turns

This program does not use "threads". They might be a solution to the problem I'm going to discuss. But I don't know how to do threads yet, for one thing, but I already know that they make for a much more complicated program even if you do know how they work. If what I am presenting here isn't hard enough or advanced enough for you, there is an excellent technical discussion of serial comms at Microsoft's site, thank you, Allen Denver, for that article.

But without threads, or some other solution, we have a problem: At any one moment, the computer can be configured to read from the serial port, or write to it. But you can't always know when incoming data is going to arrive. If you happen to be configured for sending something out on the serial port at a moment when new data "decides" to arrive, you will not "see" that data.

There are steps you can take. You can make the sending device re-send if it doesn't get a "receipt" for what it has tried to deliver. There are fancy things you can do to implement hardware handshaking. But they must remain ideas for the future. For now we are going to proceed as follows....

We will have the computer configured to read from the serial port most of the time. Note that in this simple, demonstration, program ... see "buffers" below, though.

When we have something to send, we will, briefly, take the computer out of the receive mode, send what we want to send, quickly, and then immediately return to the receive mode. That may be crude... but it "will do" for many needs.

Buffers: A moment ago, I said "you still have to click a button to DO the reading". Actually, that's not quite true. There are bits of the computer's electronics which will read bytes from the serial stream (in the right circumstances), and, up to a point, keep them on hand for your later use in "a buffer".

Thus, I might send "ABC" to my computer at exactly noon, and not click the "See If There Is Incoming Data" button until 10 seconds after noon.... aeons, in computer terms. , this will not be a problem. My program will receive the "ABC", even though the sending device finished sending it a "long" time ago.

So far so good. The one little fly in the ointment is that "as long as nothing has upset the contents of the buffer" bit. I'm afraid that I have to tell you that if you close hCommPort and then re-open it, for read, then the buffer will be emptied. If it even captured bytes sent to it before the reopening of hCommPort. Whether it did or not is academic. (By the way, notice I said "open hCommPort"? You should have found that a little confusing. But I am from here on going to allow myself the luxury of speaking a bit imprecisely. When I said "open hCommPort", that was close to saying "open the serial port". Of course, I haven't really "opened" "the port"... I have really established the handle (hCommPort) by means of which I am going to access various subroutines associated with reading and writing from and to the serial port. I hope you'll be able to go with the flow on this.)

If you have fully grasped all of that, then you are not only a mind reader, but a mind repairer... those concepts are not fully clear inside my brain, and I know that I haven't even perfectly conveyed my imperfect understanding... but I hope I have brought you far enough along that we can proceed. I hope I have made you sensitive to the issues which are implicit in the following working! software!

The main features

We've done a good job of building many elements on the fringes of what our application will eventually do. The time has come to start installing the "heart" and "lungs" of the beast....

As quickly as possible when the application starts, we want to get it into the state where it is listening to the serial port, and the buffer is accepting data as it arrives. We won't see that data immediately, but when we click the right button, our program should go off to the buffer, harvest what's there, and present it to us on the screen.

A detail: The buffer is of limited size. Furthermore our program will not fetch an unlimited number of characters from the buffer. We work around these constraints by ensuring that the user is likely to click the "See If There Is Incoming Data" button before either limit is reached. Yes, I realize that will not always be easy. But many applications will be able to live within those constraints. Also, we can extend DD80 to a form which doesn't rely on a human clicking that button.

Another detail: That button might more accurately be labeled "See If There Is Any Data Waiting To Be Harvested From The Buffer That Collects Things From The Serial Port And Display It Or A Message Saying 'No data at present' If Not".... if you didn't mind rather wide buttons.

There are several "layers" involved in "installing the heart and lungs." It will probably pay you to "pre-read" quite a lot of what follows without, at first, doing it, and then come back to here (I'll give you a hyperlink) and actually do the programming in a second pass.

We'll put some ingredients in place first, and then "turn them on". Don't try to run the program for a while.

There is the "SetUpSerPort" routine, of which I've made mention. I believe it over-rides the serial port's settings, i.e. it makes changes to what may have been set previously, either by other programs, or via the operating system's Control Panel. I suspect that these settings are "permanent", in other words they will persist after DD80 is shut down. They can, happily, be re-set again later. But they may not be permanent. They may not even be sufficient! (You may need to make these settings here... to tell the program what parameters to use, and make comparable settings in the PC, to tell the hardware how to operate. And I don't know if SetUpSerPort is ONLY dealing with such things, or whether it has other purposes. But, like this, called where I will eventually call it, things do work!

Use the shell we already created and some careful copy/paste to flesh out your program's SetUpSerialPort. The forward declaration at the start of the code is already in place.

A little aside: The material I drew upon when creating the following was written with errors to be handled by user supplied exception handling. I left the relevant rems in, but dealt with errors with the bErrCode flag mechanism. Sorry for the mish-mash; I hope you see why I did things that way.

function TDD80f1.SetUpSerPort:byte;
//Call AFTER establishing hCommFile. Returns 0 if setup goes okay.

var
   DCB: TDCB;
   Config : string;
   CommTimeouts : TCommTimeouts;
   bErrCode:byte;

begin
bErrCode:=0;//Will eventually be returned. If zero, no error seen
if not SetupComm(hCommFile, RxBufferSize, TxBufferSize) then
   bErrCode:=1;
   { Raise an exception --- these comments
       all scraps of source material... will try to use exceptions in
       due course!}

if bErrCode=0 then //no "begin" here...  (this is part of kludge avoiding exceptions)
if not GetCommState(hCommFile, DCB) then
   bErrCode:=2;
   { Raise an exception }

Config := 'baud=9600 parity=n data=8 stop=1' + chr(0);

if bErrCode=0 then //no "begin" here...  (this is part of avoiding exceptions)
if not BuildCommDCB(@Config[1], DCB) then
   bErrCode:=3;
   { Raise an exception }

if bErrCode=0 then //no "begin" here...  (this is part of avoiding exceptions)
if not SetCommState(hCommFile, DCB) then
   bErrCode:=4;
   { Raise an exception }

if bErrCode=0 then //no "begin" here...  (this is part of avoiding exceptions)
with CommTimeouts do
begin
   ReadIntervalTimeout := 0;
   ReadTotalTimeoutMultiplier := 0;
   ReadTotalTimeoutConstant := 200;//This determines(?) how long
      //you stay in an attempt to read from serial port. milliseconds
      //I hope these routines are reading from a buffer managed by
      //the OS independently of these routines.
   WriteTotalTimeoutMultiplier := 0;
   WriteTotalTimeoutConstant := 1000;
end;

if bErrCode=0 then //no "begin" here...  (this is part of avoiding exceptions)
if not SetCommTimeouts(hCommFile, CommTimeouts) then
   bErrCode:=5;
   { Raise an exception }

result:=bErrCode;
end;//function SetUpSerPort:byte;

Note the little "//function SetUpSerPort:byte;" I tacked on after the routines "end;" This could slightly complicate your "copy/paste" work, but if it does, the trouble is worth it!

Now add the following two lines to the "const" section at the top of the program....

   RxBufferSize = 256;
   TxBufferSize = 256;
     //These constants are concerned with setting space
     //aside for buffering data to and from the serial
     //port. Make them too large, and space is wasted;
     //too small, and the buffers won't be adequate to
     //tide you over times when the computer's attention
     //is elsewhere. It may be that there are places in
     //the program where absolute values, or numbers from
     //other sources were used which should be replaced
     //with these constants.

Try running the program, but DON'T click anything except "Quit" when it does run. It should compile, and start up, but it won't "work" yet. You are only "running" it now to catch typos.

The next thing to add is the OpenForRead function. Again, you have a shell already in place, just replace that. The forward declaration at the start of the code is already in place.

function TDD80f1.OpenForRead:byte;
//Set up a handle for reading from serial stream.
//Returns zero if successful.
//Only enter with port closed.
//Uses global variable hCommFile to return handle created
//Fills global variable sPortState with appropriate value.
var
  CommPort : string;
  bTmp:byte;//important to keep THIS "extra" bTmp.
begin
bTmp:=0;//Will be returned. If still zero, open was successful
sPortState:=[r];//presumption. Change to c if open fails.
//Only enter with port not open for read or write.
  CommPort := 'COM1';
  hCommFile := CreateFile(PChar(CommPort),
                          GENERIC_READ,
                          0,
                          nil,
                          OPEN_EXISTING,
                          FILE_ATTRIBUTE_NORMAL,
                          0);
  if hCommFile=INVALID_HANDLE_VALUE then begin
      ShowMessage('Unable to open for write '+ CommPort);
      sPortState:=[c];
      bTmp:=1;//Error flag of "1" can arise here, or at SetUpUserPort
      end// no ; here
      else begin //else 1
      bTmp:=SetUpSerPort;
      if bTmp<>0 then begin
         showmessage('Failed to setup serial port, error:'+inttostr(bTmp));
         sPortState:=[c];
         end//no ; here
      end; //else 1
result:=bTmp;
end;//buOpenForRead

Remember, here and elsewhere, that I've used "open" poorly. We aren't so much "opening the port" as preparing the handle (hCommFile) with which we will access things received from (in this case) or sent to (in others) the port. (Beware the "to" and "from" issues, too. Things received from the port by DD80 were sent to the computer with the port DD80 is receiving things from, when you look at it all from the point of view of the sending device.)

We now have several important "bricks" in the "wall". Add the following at the end of what we have so far in the OnFormCreate handler.

bTmp:=OpenForRead;
if bTmp<>0 then showmessage('Could not open port during form create');
  //Yes.. it can display this showmessage if necessary.

The program should compile, but still don't click anything except "quit".

With what we've written so far, when DD80 starts, the handle (hCommFile) is usually very quickly set up for reading from the serial port. sPortState should be holding "r", to reveal this. (sPortState is set within OpenForRead. It should be "c" if the attempt to set up the handle failed for some reason.) If OpenForRead failed for some reason, not only will sPortState be holding "c", but the function will have returned something other than zero.

The note about "Yes.. it can display..." is there to put my mind at rest. There are some things that you can't do during the OnFormCreate handler, but if the call fails, the message will display as I intend.

We've made serious progress!!

Now that we are ready to read from the serial port, we can put the code for that behind the "See If There Is Incoming Data" button.

Double-click on the button, to create the shell for the OnClick handler, and then copy the following into your program to do the job.

procedure TDD80f1.buSeeIfTheresDataClick(Sender: TObject);
var
   d: array[1..RxBufferSize] of Char;//Should this match the number in
   //"SetUpSerPort"?
   sTmpL: string;
   i: Integer;
   BytesRead,nNumBytesToRead:dword;
   //You will find various versions of the ReadFile call
   //on the internet. Getting the types right appears to
   //be a problem... perhaps what is "right" varies from
   //one version of Windows or Delphi to another??

   //Note also that there are TWO, DIFFERENT, routines for
   //setting up hCommFile, the handle to the data stream,
   //one for creating a stream to be read from, one for
   //a stream to write to. And they can't, in a simple
   //world, both be open at once. And opening the handle
   //for reading a stream seems to flush any incoming data
   //which might be in the buffer... if it was even collected
   //to the buffer while the handle wasn't valid.

begin
if sPortState<>[r] then showmessage('Could not read')//no ; here
else begin //1
 nNumBytesToRead:=sizeof(d);
 if (ReadFile(hCommFile, d, nNumBytesToRead, BytesRead, nil))=false then
   {ORIGINAL FROM WHICH THIS IS ADAPTED HAD "Raise an exception" here,
   no begin..else structure }
   begin showmessage('Problem with ReadFile call');
   end//no ; here
  else begin //else2
    sTmpL:='';
    for i:=1 to BytesRead do sTmpL:= sTmpL + d[i];
    if sTmpL='' then sTmpL:='(Nothing was found coming in on the serial port '+
       'or in the buffer.)';
    showmessage(sTmpL);
  end;//else2
  //leave open, in case another read is next. Write starts with a close. closehandle(hCommFile);
 end;//1
end; //buSeeIfTheresDataClick

The "Read stuff being sent to DD80 by some external device, across the serial port" button should now work. Of course, you have to arrange for some serial data to be sent to the port!! I'll try to offer some help on that front eventually, but for now you are on your own. You wouldn't have read this far(?) unless you had something to send data with, would you? Maybe you, like me, wanted to hook a microprocessor up to your "big" computer, for instance an Arduino?

One set-up you could use would be to have two computers, one running DD80, and the other running PuTTY, a free HyperTerminal replacement. Be sure when you wire them together that you connect the "out" of one to the "in" of the other. Cables for such hook ups are called "null modem cables."

If a while ago, on your first pass through the text above, you accepted my suggestion that you "pre-read" what you would eventually do, then... a) Thank you for trusting me. b) This is the time to jump back to where you started pre-reading.

Getting there...

At this point in my first pass through creating this, we are at line 574. You might think that we shouldn't have far to go, and, relatively speaking, I suppose we don't. But there are still "a few little things" to catch us out. Sigh.

But enough moaning... pressing on....

Let me remind you of something?

Most of the time, DD80 sits there quietly in a state which means that arriving data will collect in a buffer, and if the user clicks the "See If There Is Incoming Data" button, the data which has arrived will be displayed.

When the user clicks either of the "Send" buttons, the handle (hCommFile) is very quickly closed, then re-opened set up for using to send data, the data is sent. The handle is closed again. And re-opened set up for reading data. One or another of those steps flushes the buffer which collects data from the serial port. The program then settles down again in its usual "waiting for data" state.

So! It follows that we have an "OpenForWrite" routine to get the handle set up for writing. And we do. We even already have a shell for that in our program. Copy the following into the shell.

function TDD80f1.OpenForWrite:byte;
//Set up a handle for writing to serial stream.
//Returns zero if successful.
//Uses global variable hCommFile to hold handle created
var
  CommPort : string;
  bTmp:byte;//important to keep THIS "extra" bTmp.
begin
  CommPort := 'COM1';
  hCommFile := CreateFile(PChar(CommPort),
                          GENERIC_WRITE,
                          0,
                          nil,
                          OPEN_EXISTING,
                          FILE_ATTRIBUTE_NORMAL,
                          0);
  if hCommFile=INVALID_HANDLE_VALUE then begin
      ShowMessage('Unable to open for write '+ CommPort);
      bTmp:=1;
      end// no ; here
      else begin //else 1
      bTmp:=SetUpSerPort;
      if bTmp<>0 then showmessage('Failed to setup serial port, error:'+inttostr(bTmp))//no ; here
      end; //else 1
result:=bTmp;
end;//buOpenForWrite

As usual, the program should compile, but not "work" following that step forward. We will be putting the code to call OpenForWrite into the code soon.

A confession: There is a sloppiness (or several) in DD80. We took care of sPortState within OpenForRead. But it isn't taken care of within OpenForWrite. This inconsistency is a BadIdea. It would be best to move the manipulation of sPortState either into both, or out of both, whichever can be done cleanly. Changing a global variable inside a subroutine is a bad idea, so I'd use the OpenForWrite approach with OpenForRead, if I could... but I am too tired of all this just now to go back for a philosophical nicety.

Our "Send" buttons are already "finished". For instance, to save you looking back, the OnClick handler for the buSendHello is....

procedure TDD80f1.buSendHelloClick(Sender: TObject);
begin
SendChars('Hello');
end;

Before we finish our program, a little diversion:

Why did I write the hard bits inside the SendChars subroutine? Wouldn't it have been better to base things on a SendByteToSerial routine? If you had such a routine, you might well ALSO have a SendChars routine, but it would be simple...

//FICTIONAL.... NOT part of DD80....

procedure SendChars(sToSend:string);
var i:integer;
begin
for i:=1 to length(sToSend) do begin
   SendByteToSerial(ord(sToSend[i]));
   end;//for
end;

The reason DD80 is not built upward in that nice sensible modular manner is that the Windows API function which this part of DD80 is built on requires a string to be passed to it. I suppose it is easy enough to convert a byte to a character, and send that as a string of length 1 when you want to send a byte. It just seems more elegant to me to give people atomic tools, and let them build up fancier things as needed. Oh Well. Diversion ends. Onward.....

Nearly there!!

Only two things left to do: WriteString and SendChars. I see as I write that that I chose bad names!

WriteString is the lower level procedure. It takes a string, and, if everything was set up properly before WriteString was called, it sends that string to the serial port. WriteString is called by SendChars, which does rather more, but nothing you shouldn't already be aware of.

As it is the lower level procedure, WriteString is the right one to "install" next. Just replace our already existing shell with....

procedure TDD80f1.WriteString(sToSend:string);
//While in THIS program, we only write stings,
//  this procedure is perfectly capable of sending a
//  "string" consisting of a single character.
var NumberWritten : dWord;
  //The type of NumberWritten is problematic... some
  //posts online say use dWord, others say use longint.
  //Perhaps it is a matter of what version of Windows
  //   and/or Delphi you have? For XP + Delphi 4, dWord is right.
begin
if WriteFile(hCommFile,
               PChar(sToSend)^,
               Length(sToSend),
               NumberWritten,
               nil)=false then
      showmessage('Unable to send');
end;//WriteString

Test for typos by compiling. Click "Quit". Install SendChars by replacing our already existing shell with....

procedure TDD80f1.SendChars(sToSend:string);
//A low level routine to send bytes might
// seem a better thing to have, but the Windows
// API function we are calling expects to be
// given a string to send, so our procedure
// takes a string as an argument. If you want
// to send numbers, convert them to strings first.
begin
//On entry, port should already be open for reading
//... but we want it open for writing, so...
if sPortState=[r] then begin
   closehandle(hCommFile);
   sPortState:=[c];
   end;

bTmp:=0;//in case port already open for writing
if sPortState=[c] then bTmp:=OpenForWrite;

if bTmp<>0 then showmessage('Could not open port '+
     'during attempt to write')//no ; here
  else begin //1- big block...

sPortState:=[w];

WriteString(sToSend);
closehandle(hCommFile);//Get ready to go back to being ready to read
sPortState:=[c];

bTmp:=OpenForRead;
if bTmp=0 then sPortState:=[r]//no ; here
  else begin //2
    showmessage('Was not able to re-open for read after a write');
    sPortState:=[c];//untangle inconsistencies re: where sPortState set
    end;//2
end;//1
end;//SendChars

And.......

..... that's it!!... We've done it!





If you've read this far...

You are welcome to use the material here free of charge. But if you want to show your appreciation, you easily can make a gift to me or contribute to a charity I would like to help... I've listed several to choose from. (The link will open in a new tab or window.)





Concluding remarks...

So... there you have it: A way to interact with external devices via a serial link, using programs written by you to run in a Windows computer. Although the tutorial was written using Delphi, nothing particularly difficult was done with Delphi... all the work is done by the Windows API calls. If you know how to do API calls in the language of your choice, then you should be able to adapt this program to your needs quite easily.

The program is not "clever"... by design. I wanted to expose the vital underlying API calls, not give you a full Hyperterminal clone. The worst "flaw" in the program above is that you have to click the "See If There's Data" button to discover if new data has arrived since you last clicked it. And if you do a "send" before you've done that, you lose anything that arrived before the send takes place.

To recap...

This tutorial shows you how to read and write data via a Windows computer serial port. It may not show you the best way, but until something better comes along, maybe it will do?

While it will seem to the user that the program is doing both, reading and writing, it is only doing one or the other at any given instant. The program switches back and forth between the two activities as necessary. Note that designing that switching to truly meet the needs of users may be a non-trivial challenge... especially in the simple, "no handshake" environment this program operates within.

The whole program is built around the "handle", hCommFile. A handle is like a variable in many ways, and it is used in this application to interact with subroutines of the operating system, via API (Application Program Interface) calls.

Regarding sending data: I presume you want to send more than the two rather dull messages provided for, "Hello" and "Bye". However, depending on what external device you are controlling, a system of buttons with preset texts might be sufficient. I have a program that opens and closes the sun shades in my greenhouse, and turns lights on an off. They are usually operated according to the time of day, but I can also call for "lights on", etc, by clicking buttons.

These shortcomings can be overcome... but only by "wrapping" the concepts we have explored above in a further layer of programming, and Windows events chicanery. I will try to write up an example in due course. It won't really do anything that the program above doesn't do, but it will do it in a more user friendly manner. If you want to do it before I get around to it....

There would be a memo. A timer would call what's behind "See If There Is Incoming Data" from time to time, and put any received data in the memo. There would be an edit box. When users typed things into it, what was typed would be sent out. It would probably be best to offer a choice between two options: Send-at-once, and send-line-when-finished. A way for users to select a com port and the data protocol (speed, etc) is needed.

Apart from that, some rough edges in DD80 should be cleaned up: Where sPortState is set should become consistent. When and where SetupSerPort gets called needs sorting out. In a perfect world, I'd want the program to be capable of just using whatever serial port settings were in place from earlier Control Panel, or other, settings OR over-riding those settings.

And, of course, although it is a distant objective for me at this point, if threads or other mechanisms can get around the need of DD80 to be switched back and forth from reading from the port to writing to it, so much the better!

Lastly, if you want to work through an earlier, imperfect tutorial that was a "stage along the way" to what you saw above, my "Serial Port Program To Read From External Hardware" is still online. Looking at that might help you with something that wasn't clear above, or merely be useful to consolidate what you learned above.




            powered by FreeFind
  Site search Web search
Site Map    What's New    Search This search merely looks for the words you enter. It won't answer "Where can I download InpOut32?"

Click here if you're feeling kind! (Promotes my site via "Top100Borland")


If you visit 1&1's site from here, it helps me. They host my website, and I wouldn't put this link up for them if I wasn't happy with their service. They offer things for the beginner and the corporation.www.1and1.com icon

Ad from page's editor: Yes.. I do enjoy compiling these things for you. I hope they are helpful. However... this doesn't pay my bills!!! Sheepdog Software (tm) is supposed to help do that, so if you found this stuff useful, (and you run a Windows or MS-DOS PC) please visit my freeware and shareware page, download something, and circulate it for me? Links on your page to this page would also be appreciated!
Click here to visit editor's freeware, shareware page.

Link to Tutorials main page
How to email or write this page's editor, Tom Boyd


Valid HTML 4.01 Transitional Page tested for compliance with INDUSTRY (not MS-only) standards, using the free, publicly accessible validator at validator.w3.org


If this page causes a script to run, why? Because of things like Google panels, and the code for the search button. Why do I mention scripts? Be sure you know all you need to about spyware.

....... P a g e . . . E n d s .....