Serial Communication Under Win32 to program on a PC
Serial ports used to be easy to program on a PC. Then they got more complex, then unreachable. Now they can be made to look simple again.
Anyone porting 16-bit serial communication code to 32-bit Windows NT or Windows 95 faces a common problem: the familiar methods of implementing communication are at the very least different and at the worst, no longer present. Some of the Win32 API function for setting up the communications port have not changed with respect to their Win16 counterparts. However, the functions used to open, close, read, and write to the port do not exist, nor do the messages generated by the driver when an I/O event occurs. If, like me, you move from 16-bit DOS right into 32-bit Windows, the change is even more pronounced, as you can no longer use an interrupt routine to perform serial communications and you must learn new methods of performing the required tasks of serial I/O.
Having said that, the Win32 API does offer improved support for communication devices. Win32 eliminates the need to deal with communication devices in a nonstandard way; it also eliminates the need to deal with the hardware directly. Instead, you perform serial communication with the standard Win32 file I/O functions. For those moving from 16-bit Windows, Table 1 lists the Win32 API equivalents for the 16-bit API functions.
16-bit API Win-32 API OpenComm CreateFile CloseComm CloseHandle FlushComm PurgeComm GetComError ClearCommError ReadComm ReadFile WriteComm WriteFile SetCommEventMask SetCommMask GetCommEventMask GetCommMask EnableCommEventNotification WaitComEvent* UngetCommChar -None- *WaitCommEvent will not post WM_COMMNOTIFY messages
Table 1: 16-bit Communication Functions and their Win32 Equivalents
While the Win32 API does make it simple to open a port and start sending and receiving data, I soon found that there is more to serial I/O than that. For example, as always, you must configure the port with the right set of options and timeout values for these operations to work as expected. This article presents a class that encapsulates the Win32 API functions used for serial communication and simplifies their use. This class also provides some member functions that make it easy to start and stop a separate thread for sending and receiving data. Some sample programs are included on the CUJ ftp site to demonstrate how the class can be used. (See p. 3 for instructions on downloading source code from CUJ.) I developed and tested the code using Borland C++ 5.01 and Visual C++ 4.2.
Listing 1 (SerialPort.h) shows the class declaration for CSerialPort and its supporting definitions. The class consists of some protected data members that track the state of the object, a set of configuration functions, a set of I/O functions, and wrappers for the Win32 API functions relating to serial communication. The class also provides built-in support for overlapped I/O and for starting and stopping a separate thread to send and/or receive data via the port. More often than not, the basic class can be used as is unless there is a need for specialized read/write operations. In those cases, its fairly easy to derive a class from CSerialPort and override one or more of its virtual functions. This will enable you to set up customized, multithreaded, serial communications. To date, I have not needed the classs overlapped read/write features beyond the polled I/O support provided by the WaitCommEvent and CheckForCommEvent functions. Most of my applications have made good use of the StartCommThread function, though.
To create a communication object, simply pass its constructor the name of the port you want to open. By default CSerialPort initializes the port for 19,200 bps, no parity, eight data bits, one stop bit, hardware flow control, no read timeouts, one-second write timeouts, and enables monitoring of EV_RXCHAR events. Once the object is created, you can alter these settings with the configuration member functions shown in Table 2. The Win32 wrapper functions such as SetCommState, SetupComm, and SetCommTimeouts can be used if necessary, but the functions in Table 2 take care of many of the low-level details associated with initializing the required structures. Each of the functions in Table 2 also combines several steps into a single function call. Once the port is opened and configured, use the ReadCommBlock and WriteCommBlock member functions to send and receive data.
Function Purpose SetBaudRate Sets the baud rate (bps) SetParityDataStop Change parity, data bits, stop bit settings SetBufferSizes Change input/output buffer sizes used by Windows SetReadTimeouts Change the read timeouts SetWriteTimeouts Change the write timeouts SetCommMask Specify which set of comm events to monitor
Table 2: CSerialPort Configuration Functions
Listing 2 (Terminal.cpp) shows the ubiquitous dumb terminal program, reworked to take advantage of the 32-bit environment and utilize the basic CSerialPort class. The DumbTerminal function calls the StartCommThread member function to start a separate thread to handle incoming serial data (signaled by the EV_RXCHAR communication events) while the main thread waits for keyboard input and writes it out to the port. Note that by allowing a separate thread to handle incoming serial port data you can eliminate the need to continuously poll for both forms of input in the main thread. Thus the application consumes less CPU time without the programmer expending any special effort.
TermPoll.cpp, included on the CUJ ftp site, is a less efficient implementation of Terminal.cpp that illustrates this point. In the TermPoll version, the Sleep function must be called to introduce a slight delay in the main loop. This prevents the CPU from reaching 100% continuous utilization. Using separate threads instead to send and recieve data especially makes sense in a GUI application; the main thread remains responsive to user-interface events.
A CSerialPort object can receive notification of certain communication events. To select which notifications your object will receive, use the SetCommMask member function. The events are specified by ORing together constants such as EV_RXCHAR, EV_ERR, etc. defined in WINBASE.H. When constructed, the class enables EV_RXCHAR automatically, so if thats the only notification you need, you dont need to call SetCommMask in your application.
Selecting which notifications the object is to receive is different than enabling the object to actually receive notification. After selecting the events of interest with SetCommMask, you must enable the object to receive notification by calling WaitCommEvent. (This situation is analogous to setting an interrupt mask and then later enabling interrupts by executing a special instruction.) I did not implement WaitCommEvent quite like its API equivalent in Win32. My version splits the APIcall into two separate functions. Member function WaitCommEvent should be used to enable notification; use CheckForCommEvent to see if any have occurred.
I implemented these functions this way to enable a program to either block while waiting for an event to occur (by calling CheckForCommEvent(TRUE)) or poll for events as needed (CheckForCommEvent(FALSE)). The return value is a bit mask of the events that have occurred; it is zero if none are available or an error occurred. As with the Win32 API, WaitCommEvent must be called again to re-enable event notification after CheckForCommEvent returns a value other than zero. Refer to the CommReader thread function in Listing 2 for an example of their use.
The GUITerm Example
The GUITerm example demonstrates the use of CSerialPort in an MFC application. It provides a dumb terminal much like the console mode example and it can also perform a basic XMODEM file transfer. This example also demonstrates stopping and restarting a thread function for the port object and a way to use timeouts on read operations. The application uses the document/view model. In this case, the document manages the serial port object and the view simply displays received data and passes key presses on to the document for transmission. When the Connect option is chosen, the document object opens the serial port and starts a thread to handle incoming data. This approach is similar to that of the Terminal.cpp example presented above. The difference here is that when data arrives, the receiver thread sends a WM_COMMDATA message (defined by the application as WM_USER + 500) to the view object, which causes it to insert the received data into the edit control used for display purposes. This application behaves somewhat like 16-bit Windows, in which the communications driver generates a WM_COMMNOTIFY event when data arrives.
The XMODEM protocol used for the file transfer requires specific timeout values for its read and write operations. The application temporarily alters the ports timeout settings for the duration of the transfer and resets them afterwards. Under Windows, all communications resources have an associated set of timeout parameters that affect the behavior of read and write operations. Timeouts can cause a read or write operation to finish even though the specified number of characters have not been read or written. When this occurs, it is not treated as an error. The read or write functions return value indicates success but the count of bytes actually read or written will be less than what was requested.
There are two types of timeouts: interval timeouts and total timeouts. Read operations can utilize either or both forms of timeout. Write operations only use total timeouts. An interval timeout occurs when the time between the receipt of any two characters exceeds a specified number of milliseconds. Timing starts when the first character is received and is restarted when each new character arrives. A total timeout occurs when the total amount of time consumed by a read or write operation exceeds a calculated number of milliseconds. Timing starts immediately when the I/O operation begins. The number of milliseconds is calculated as follows:
Total_Timeout = (Multiplier * Number_Of_Bytes) + Constant
The use of a multiplier value allows for longer timeouts based on the number of bytes being read or written. If you do not need both a multiplier and a constant, you can set the unwanted parameter to zero. If both parameters are zero, total timeouts are disabled for the given operation and the read or write will not return until all bytes have been read or written.
Table 3 summarizes the various values and combinations of valid read timeouts. Because read operations can utilize either or both forms, you must take extra care to ensure that they are set correctly for your application. Setting the read timeouts too low can result in a read operation stopping early and possibly giving the impression that data loss occurred. Setting timeouts too high usually is not a problem, especially when a separate thread is handling the receive operation. However, it may become a problem if the receiver thread is also responsible for other operations besides checking the port for incoming data. With a little experimentation, you can determine whether or not the classs default behavior of disabling read timeouts and setting the write timeout to one second is sufficient for your needs.
Interval Total Behavior
MAXDWORD 0 No read timeouts. Return immediately
with any available data.
MAXDWORD * Special case. If the interval and
multiplier values are both set to
MAXDWORD, and the constant is set to
any non-zero value less than MAXDWORD,
one of the following occurs:
If there are any characters in the
input buffer, return immediately with
If there are no characters in the
input buffer, wait until a character
arrives and then return immediately.
If no character arrives within the
time specified by the constant value,
a timeout occurs.
0 0 Return only when the buffer is
completely filled. Timeouts are not
0 T Returns when the buffer is completely
filled or when T milliseconds have
elapsed since the beginning of the
I 0 Returns when the buffer is completely
filled or when I milliseconds have
elapsed between the receipt of any two
characters. Timing does not begin
until the first character is received.
I T Returns when the buffer is completely
filled or when either type of timeout
Table 3: Behavior of Read Timeout Value Combinations
This article and the example code cover the most common uses for the CSerialPort class. Instead of covering the remaining member functions in detail, I refer you to the appropriate Win32 online documentation provided with the compilers. The wrapper functions are identical in name and form except for the omitted handle parameter that the class manages internally. One final point worth mentioning is that the wrapper functions will keep track of any error code resulting from the call. The inline member function CSerialPort::GetLastError will return the proper error value even if your application has called other Win32 functions that alter what the API-level ::GetLastError returns.
To date, I have used CSerialPort to communicate with other PCs and modems as well as with hand-held data collection devices and cash registers. It is a versatile class in its own right and provides a solid foundation from which to build specialized serial communication classes. By letting CSerialPort handle the underlying details it also makes the transition from the 16-bit to the 32-bit platform a much easier task.
Entry filed under: Communications.