TSerialPort: Basic Serial Communications In Delphi
by Jason 'Wedge' Perry
This article was originally published on Feb. 16, 1997.
I remember the days in DOS XBase when I could just call 'OPENFILE,' select the comm port, and start writing the data to the serial port.
Not any more.
Now we have this massive, confusing, bumbling giant called the Windows API to muddle through in order to send a single byte of information to the serial port. What do we do, where do we start? To begin with, it is important to understand the terminology that makes up serial communication without getting too 'technical.'
Serial Port Control is an inexpensive royalty-free component. It supports VB, VB.Net, C#, Borland Delphi, Borland Builder. Just put it on a form and you've got all the functionality you need to access an RS232 port. Delphi 7 XP prof. Hi - I'm using the free Tcomport component for Delphi. Although I do not usually have problems. Delphi usb serial port. Delphi 7 XP prof. Delphi Serial Port. They are 'little boxes' which, on one side, 'talk' to your PC via a serial port, while on the other side, they take care of reading values from 1-Wire chips on MicroLans. Delphi 7 XP prof. Hi - I'm using the free Tcomport component for Delphi. Although I do not usually have problems. Delphi usb serial port. Delphi 7 XP prof. Delphi Serial Port. They are 'little boxes' which, on one side, 'talk' to your PC via a serial port, while on the other side, they take care of reading values from 1-Wire chips on MicroLans.
A serial port only does two things: send and receive data. What could be simpler? Well, there is a lot that has to happen to send that data to the port. First, serial ports are far slower that the computer. So, when you send a file to a serial port, it is like strapping a rocket on a camel and trying to shove it through the eye of a needle. What do you end up with? A bunch of burned up camel pieces. The same thing would happen to your data without some clever buffering and flow control.
The best way to describe flow control is to watch your toilet. You push the handle, 'flush' the water which empties the tank, and then the tank fills back up again. You can only send so much water through because you have to wait for the tank to fill up again. If your toilet didn't have the fancy valve in the tank, what would happen? You would end up with toilet water all over the floor. It works the same way with your data. There are two main ways of flow control: RTS/CTS and XON/XOFF.
RTS (request to send) and CTS (clear to send) are built into the hardware of the serial port. Your RTS line is connected to the remote devices CTS line, and vise versa. Whenever the remote device is ready for data, it will activate its RTS line, thereby telling your CTS line, to start sending data. When the remote device has received enough data, it drops the RTS line which tells you to stop sending data. This cycle continues until all of the data has been sent.
The software way of handling flow control is checking for XON/XOFF characters. When you start to send data and the buffer gets full, the serial port will signal you with an XOFF character (ASCII 13h). By receiving this character, the serial port knows to stop the flow of data. When it can accept more data, it issues the XON character (ASCII 11h) to continue the flow. RTS/CTS and XON/XOFF both accomplish the same tasks, and each has it qualities and problems.
Once you get the data flowing, it is important that it gets sent and received correctly. There are numerous ways to check the data to confirm that it was correctly received. The most primitive way is parity error checking. There are two ways of parity checking, even and odd. When using parity to evaluate the data that is sent, it adds an additional bit to the end of the data byte that reflects the correct number of bits that are set. If using even parity, the bit is set if the total of the set bits is even, and if using odd parity, the bit is set if the total of the set bits if odd.
However, there is a serious flaw in the concept. If you sent a 01110101 with an odd parity bit, and a 01110000 is received, the incorrect data will be accepted as correct. There are two other types of parity, mark and space. Mark parity adds the additional bit, however it is always set. What good is that? Space parity if the same, except that it is always un-set. So, ultimately, these two type of parity are useless. Due to the low capability of parity error checking, programmers have come up with sophisticated methods of error checking such as CRC (cyclic redundancy check) checking. This checks the order and amount of data that is sent, and the checksum (the value of the byte) of the data that was sent. It is efficient and works pretty well, but is out of the scope of this discussion.
Now that we know how to control the flow of data, it won't do much good if the devices aren't listening to each other. Just like flow control, there is a hardware approach and a software approach. The hardware approach is to monitor the status of the DSR (data set ready) /DTR (data terminal ready) lines. When two modems connect, for instance, each one sets it DTR line to active (commonly 'hi,' or 'hot'). The first modem's DTR line is connected to the second's DSR, and vise-versa. When both modems get a response from the other, they have just performed hardware handshaking. Although all serial ports have DSR/DTR lines in them, this method is primarily only found in older systems of communication. Software handshaking is far more efficient.
Today, software handshaking completes many tasks for modems. When you hear all of the noise during a connection, the modems are negotiating each other's presence, what features are turned on such as flow control, compression level, baud rate, and etc. Software handshaking is not necessary for simple communications, but is an absolute must for complex tasks such as file transfers. Like RTS/CTS or XON/XOFF, DSR/DTR can also be used for flow control, however it is an ancient method of doing so and isn't used much anymore.
The Windows 95/NT API has a tremendous amount of features built into it for serial communications, however along with features comes confusion. The serial component that I created is centered around only a few of the API calls that are necessary to perform the most basic of serial communications. I will give you a brief introduction to the calls that I used and then will take you through the assemble of the component, step-by-step.
The first thing, and the most important, that has to be accomplished is to open the port. This is accomplished by a call to CreateFile.
Check out that parameter list! Although this appears to be overwhelming, it is far easier to use than you think. If CreateFile is successful, it returns a handle to the serial port, which is used in every other API call we make to refer to the port.
Since we are concerned about a serial port, lpFileName is the logical name of the serial port, such as 'COM1' or 'COM2.' The parameter dwDesiredAccess describes the way in which we will access the port. Since we want to read and write to it, the parameter value is 'GENERIC_READ OR GENERIC_WRITE.' dwShareMode tells Windows if the file, or in our case the serial port, can be shared by other applications.
The answer here is no, so a zero has to be passed here so that our application will have exclusive access to the port. lpSecurityAttributes points to a structure that specifies whether the handle is inheritable, and some other junk. For convenience, this is nil. dwCreationDisposition tells Windows how to open or create the file. Since the 'file' always exists, the parameter value is 'OPEN_EXISTING.' This opens the existing file, or port.
dwFlagsAndAttributes only applies to serial ports when set to FILE_FLAG_OVERLAPPED. This allows the serial port to perform asynchronous communication. In other words, the port can read and write at the same time. To make writing the component easier. I chose to set this to zero, using the serial port in a synchronous manor. We will talk about some possibilities in using this parameter later. And finally, hTemplateFile is passed a zero because it has absolutely nothing to do with serial communications.
The DEVICE CONTROL BLOCK (DCB)
The whole heart of establishing a handle to the serial port is the Device Control Block structure. This structure describes all of the settings that you want to apply to the serial port and is declared in windows.pas. Almost all of these settings represent properties in the TSerialPort component that I created. As I develop the component, you will become very familiar with the DCB.
GETCOMMSTATE and SETCOMMSTATE
GetCommState and SetCommState are the functions used to retrieve the current DCB parameters, and to modify them as well. For each function, just pass in the handle to the port that was returned from CreateFile, and the address to the DCB. GetCommState uses a variable parameter of type TDCB to return the current settings to you. SetCommState accepts a parameter of type TDCB to modify the DCB for the port.
function GetCommState(hFile: THandle; var lpDCB: TDCB): BOOL; stdcall; function SetCommState(hFile: THandle; const lpDCB: TDCB): BOOL; stdcall; GETCOMMTIMEOUTS and SETCOMMTIMEOUTS
Sometimes something happens to our data and it never reaches the us or the other device. This is common when surfing the net in the middle of a raging thunder storm. The serial port doesn't know that it will never be receiving more data or that the other device was just wiped out by a 10 megajoule bolt of energy. So it will wait forever for the next byte of data. GetCommTimeouts and SetCommTimeouts set the maximum amount of time that the ReadFile will wait on a piece of data. When that time is surpassed, it terminates the read and sets a timeout status on the port. Again, all you have to do is pass the function the handle to the port and fill the record lpCommTimeouts which is of type TComTimeouts.
I chose to use the defaults for the timeouts since I am sure that the people at Microsoft knew more about the optimum settings than I do. However, in the source that is included with the subscription, you will notice that I put the code in the correct place and then commented it out in case you need to implement it.
PurgeComm is a life saver. It will allow you to cancel and read or write operations, immediately. It will also clear the input and output buffers if you tell it to. It consists of a handle parameter (surprise), and a set of flags.
The flags available are PURGE_TXABORT, PURGE_TXCLEAR, PURGE_RXABORT, and PURGE_RXCLEAR. I think these are fairly self explanatory. The 'abort' ones terminate all operation immediately. The 'clear' ones tell it to clear the corresponding buffer.
CloseHandle will close the open file handle to the port, and returns true if successful and false if not. This is how we close the serial port.
ClearCommError retrieves the current status of the specified device and reports on any errors in that device. Also, when a communications error occurs, it gets called and it clears the devices error flag so that it can continue with read and write operations. The lpStat parameter points to a TComStat structure that contains the fields that represent the errors that have occurred, and the current device status. Razer copperhead driver windows 10.
Although you can do some nice error reporting and handling with this function, I am only concerned with one of the com status parameters: cbInQue. This field contains the number of bytes that are in the buffer that have not been read by the ReadFile method. I describe how I used this function later in the article.
WRITEFILE and READFILE
The two most import functions are WriteFile and ReadFile. These functions are used to send and receive data from the serial port. Each one has the same number and type of parameters, with minor exceptions.
For WriteFile, hFile is the handle to the port (aren't you glad we created that handle with CreateFile?). Buffer is a pointer to the data that you want to send. nNumberOfBytesToWrite is the number of characters that we are going to send to the port. lpNumberOfBytesWritten is the number of bytes that were actually send to the port after the function is complete.
And finally, lpOverLapped is a pointer to a TOverLapped structure that when defined allows the asynchronous use of the serial port. In lay terms, the port can read and write at the same time. I chose not to use the overlapped structure, mostly because it requires significant more work and I have so limited time. I will comment in it in the conclusion, however.
The differences in ReadFile are minimal. Buffer is a pointer to a memory block that holds the result. nNumberOfBytesToRead is the number of bytes that you want to read. lpNumberOfBytesRead reflects the number of bytes that were read on return from the function.
Now don't let all this 'byte' stuff and 'pointer' stuff worry you. It is actually far easier than you think. When you declare a variable named MyString, of type string, it is actually a pointer to a block of memory that contains the value that you assign it.
And the byte stuff, a single character is a single byte. So all you have to do is use the SizeOf() function or even the Length() function to count the total number of characters. One last comment on nNumberOfBytesToWrite/ToRead and lpNumberOfBytesWritten/Read. It would be a good idea to check that these values each match on return from the function. It is a simple check, and it will let you know that the correct number of characters were written and read each time (be the characters correct or not).
Now that you understand the basic terminology of the functions that make up the TSerialPort component, I can get down to business on how to assemble it.
First, I made a list of all of properties that I thought the component would need and the range of values for those properties. I got all of them from the DCB structure since it contains many of the serial port features in it.
Since of these properties affect the DCB directly, each must have a corresponding 'Set' method declared in the 'write' section of the property declaration. I also assigned a default constant to each one in order to establish some basic setup defaults.
implementationNext, for each property, I defined a new type that contains sets of possible values for each property that has specific values that it could be. For instance, I defined a TCommPort type that contains the set of possible comports that are available (Listing 2). In turn, I also defined a constant for each property, with the prefix 'dflt_,' as a simple matter of convenience while programming the component. Each of these properties and the corresponding field look almost identical. The 'set' methods are basically the same as well. Each 'set' method is implemented the exact same way:
For the next hour you can declare all of the fields, properties, types, and set methods with little or no understanding of how a serial port works. When you compile it up, you get a component with cool property drop downs that don't do anything yet.
Now what about a Receive and Transmit event? In my book, if a custom component doesn't have any custom events, it is a pretty whimpy component -- probably of type TWhimpy. So I declared 4 events: OnReceive, OnTransmit, AfterReceive and AfterTransmit. This required two new 'TNotify' events named TNotifyTXEvent and TNotifyRXEvent (Listing 2). Each is defined as a procedure with the parameters sender and data. So when the event is called, it passes the sender, and the data that was used, to the custom event in your application.
That was the easy part of the setup. Next I made a list of the private and public methods that I would need to make the TSerialPort component work. To start with, every component has a create and destroy that needs to be defined.
end;You will notice that the create is straight forward. I just called the 'inherited Create()' and then set all of the property defaults. At the top of the method, I initialized the hCommPort (comm port handle, remember?) to INVALID_HANDLE_VALUE. This lets me test for a comport that is not successfully opened, and also prevent some operations from performing on and unopened port. In addition, I created a public 'PortIsOpen' method to check for that value. If it is not equal to INVALID_HANDLE_VALUE, the port must be open!
Two important methods that I created next were the OpenPort and Close Port methods. The OpenPort method is the one that makes the call to CreateFile in order to get a handle to the port. First, I made sure the port was closed. Closing the port is real easy. You will notice in the ClosePort function that I called CloseHandle. This call deallocates the handle to the port. If it is successful, it returns true. Just before closing the handle, I made a call to FlushTX and FlushRX to purge the receive and transmit buffers of any remaining bytes. Each procedure calls PurgeComm with the handle of the comm port, and with parameters that describe the task to perform. Also notice that I reinitialized the hCommPort handle back to INVALID_HANDLE_VALUE for future use.
Back in the OpenPort method again. After making sure that the port was close, I made the call to CreateFile. I set the parameters exactly as discussed in the introduction. If OpenPort is successful, it creates a handle to the port, and returns true. Lastly, OpenPort calls Initialize_DCB to set up the DCB features of the port.
Every time a property is changed, it needs to update the DCB (you will notice that all the 'set' methods call Initialize_DCB). The Initialize_DCB method is fairly trivial. All I did was check the value of the components type in a 'case' statement and then set the correct field in the DCB. I want to mention a couple points, however. You will notice at the top of the procedure that I made a call to GETCOMMSTATE.
This function has a variable parameter that will return the DCB record filled with the current settings for the port. Then the changes are made to the temporary variable that stores the copy of the DCB. Finally, a call is made to SETCOMMSTATE to save the changes. You may be a little confused by the 'flags' field in the DCB. It is a 'bit flag' field. It has many possible flags, most of which deal with flow control and parity checking. I don't know why they chose to represent the flags in hex, but here is a listing of the values for your future use:
All I did was set the 'flags' field equal to the equivalent value, and voila - you now have flow control, parity checking, and null stripping capability.
Lastly, the two most important public methods, SendData and GetData. SendData was much easier than Getdata. First, I called the OnTransmit event with self and data. This passes the sender and the data to the OnTransmit event that I declared. By making the call at the top, I signal the event just before sending the data to the port. I do the same thing at the bottom of the procedure with the AfterTransmit event, in order to call the event after the data has been sent.
The best use I have found for these is filling up a memo with the sent data and controlling some fancy LEDs to make your GUI have sufficient 'whiz-bang' appeal.
The WriteFile function was easy to set up. I just passed in the handle to the port, a pointer to the data the I wanted to send, and the size of the data to send. The size the data is just the number of characters it has. The NumBytesWritten variable that I declared will be filled with the total number of bytes that the function passed when it is done. The last parameter is just 'nil.' For simplicity's sake, I chose not to make use of the 'overlapped' I/O capability of the port. This just means that the port can read and write at the same time. In order to do this, you have to do some fancy buffer handling, size reallocation, and etc. The ultimate serial communications component would be multi-threaded so that you could send data while you were reading it.
If any of you enhance this one to do it, I would love a copy!
The GetData function took more work to get behaving correctly. Just like SendData, I called the corresponding event procedures at the top and bottom. That was the easy part.
Initially, I just made a call to ReadFile, similar to the call to WriteFile, and expected the data to come back. Not a chance. I may have gotten some characters and garbage, but nothing that I expected. What had to be done was to determine the number of bytes that were waiting in the receive buffer, and then to read them. I did this with a call to ClearCommError. In order to make it work correctly, I had to declare a variable of type TComStat. TComStat is a record that contains information about errors and, you got it, buffer contents. So all that had to be done was to allocate the readbuffer to the same number of bytes in the receive buffer, plus one additional byte.
I then called ReadFile and filled the readbuffer with the contents of the buffer. Lastly, I set the length of the readbuffer to the exact length of itself. This made a nice and tidy string for me to pass into the AfterReceive event. I am not a serial communications expert, and I certainly didn't hit on every function and procedure in the API for handling comports. There is a lot more that can be done, such as CRC checking, asynchronous communications, multi-threaded reads and writes, etc.
If any of you make any cool enhancements, send me a copy, I would love to see what you did.
I threw together a small terminal application to demonstrate some of the function for you. Just run the setup and point the comport to your modem. A really good example is to enter 'ATI4' to return your modem's current NVRAM settings. Another cool one is 'ATDT and your own number.' This will return a 'BUSY' message to you. When it does it, you know for sure that you are communicating with the port. You might also check out the setup screen's code closely. I save off all of the settings into the system Registry (wooa, bad word). I just pass the Comm1 component into the setup form, manipulate it, and send it back with the new settings.
Pretty cool stuff. I hope you enjoy the component!