Features


Advanced Serial Port Communication Under Win32

Thanos D. Konstantinidis and M.G. Strintzis

It's really easy to communicate with an external device through a serial port — once you get all the administrivia under control.


Introduction

Many of the devices that can be connected to a computer do so through the serial port. The advantages are simplicity in hardware, low cost, and a relatively straightforward API (Application Programming Interface) for the serial port, since most OS's (Operating Systems) treat ports as files. The disadvantages are slower communication and the need for advanced programming techniques like multithreading to fully exploit the OS's capabilities.

In the Win32 world, the term file covers a very broad range of logical devices, including the parallel and serial ports, disk files, and the screen and keyboard in console-mode applications. With the same basic set of API routines, a programmer can handle all of these devices.

When files are involved, the most time-consuming chores are reading and writing, because hardware is directly involved. With the serial port, the program must wait for the FIFO buffers to fill before accessing any of the data. Luckily there are a number of techniques to work around this limitation. Overlapped I/O is one technique that allows client code to be more efficient, because the client code is notified via event objects when the file operation is complete. Until then, other duties can be performed. (A very thorough analysis of the technique can be found in [1], but I will cover the basics here.) This article presents a class for encapsulating overlapped serial port I/O in both Windows 95 and Windows NT.

Overlapped File I/O

When using serial I/O under Win32, the Win32 function CreateFile is used to get a handle to the port. One of the parameters in this function, the DWORD dwFlagsAndAttributes, must be ORed with the flag FILE_FLAG_OVERLAPPED to ensure overlapped I/O port mode. Later, when the Win32 functions ReadFile or WriteFile are called, the parameter lpOverlapped passed to them must have the address of an OVERLAPPED structure, which is defined as:

typedef struct _OVERLAPPED {
    DWORD  Internal;
    DWORD  InternalHigh;
    DWORD  Offset;
    DWORD  OffsetHigh;
    HANDLE hEvent;
} OVERLAPPED;

The hEvent member must hold the handle of a manual event. The event must be manual or a deadlock might occur (for more details, see [1]). When the file operation is complete, the state of that event will change to "signaled." For the purpose of this article, the only other interesting member of the OVERLAPPED structure is Offset, which will store the offset where the reading or writing starts. The purpose of this member is to handle the situation in which many file operations are occurring on the same file, and each operation requires its own separate offset. OffsetHigh is used for files larger than 4 GB, because that size cannot be described by a single 32-bit DWORD. In this implementation, OffsetHigh must be zero. A detailed description of OVERLAPPED can be found in [1] or [2].

Note that the file handle can be used to determine when the file operation is finished. However, this technique can be used only when there is only one overlapped operation in progress. Once an operation is complete, the OS sets the file handle to the signaled state. Therefore, if there are a number of operations in progress when the first one is completed, the file handle will be in the signaled state, and there will be no way to determine when the others complete.

When used with overlapped I/O, ReadFile and WriteFile return immediately. If all the data has been transferred, the return value is TRUE (e.g., the file was already cached in memory, so the operation was completed immediately); otherwise it is FALSE, and the GetLastError function returns ERROR_IO_PENDING. In the latter case, the GetOverlappedResult function

BOOL GetOverlappedResult(
    HANDLE  hFile,      
    LPOVERLAPPED  lpOverlapped,
    LPDWORD  lpNumberOfBytesTransferred,
    BOOL  bWait   
   );

must be called to determine the number of bytes transferred (in the parameter lpNumberOfBytesTransferred). hFile and lpOverlapped must contain the same values passed to ReadFile or WriteFile. In our case, bWait must be FALSE to indicate that we do not want to wait for the overlapped operation to be completed. We just want to read whatever we can in one pass. The other bytes will be read in the next pass. Obviously, if the DWORD pointed by lpNumberOfBytesTransferred is zero, there are no more bytes to read.

Port Notification Events

Asynchronous I/O is only half the story. The performance of the client application can be further improved with port notification events. Win32 allows a thread to monitor the port so that when a certain event occurs that thread can trigger an action. The events of interest can be specified (ORed) with a call to SetCommMask and monitored with calls to WaitCommEvent. If the monitoring of the event should be executed asynchronously (i.e., the event involves data transfer), the address of an OVERLAPPED structure can be passed to WaitCommEvent. WaitCommEvent is called in an infinite loop in the function body of the monitoring thread. If WaitCommEvent finishes immediately, it returns TRUE; otherwise, it returns FALSE, and GetOverlappedResult(...) can be called to check the progress.

In our case, the event to be monitored should be EV_RXCHAR (receipt of characters). The monitoring thread should not be given any other responsibilities. The final part of the technique involves notification of the main program when the event in question occurs. The most straightforward way to achieve this is to use standard Windows message passing.

Implementation

Now I will translate the above logic into a C++ class, CPort. First, note that there are enough variables involved to justify wrapping them in a separate class, CPortData. Class CErr is a general class for error reporting and serves as the base class for CPortData and CPort. CErr and CPortData are defined as follows:

class CErr {   
protected :
    // name of the derived class
    char  szCName[30]; 

    CErr(char*);
    CErr();
    virtual ~CErr(){};

    //report error
    void ShowErr(char*, char* = NULL,
                 int = 0, int = 0 );    
    ...
};

class CPort ;

class CPortData : public CErr {

    volatile HANDLE _hPort;
    volatile CWnd*  _pWnd;
    volatile HANDLE _hEvNot;
    volatile BOOL   _bGoOn;

    OVERLAPPED      _olWrite, _olRead;
    UINT            _uMsg;

    CPortData(CWnd* pWnd, UINT uMsg);
    ~CPortData();    

    friend class CPort;
};

#define Err(s2) ShowErr(s2,
    __FILE__,__LINE__)

(CErr has been abbreviated here. Full source code is available on the CUJ ftp site. See p. 3 for downloading instructions.)

In class CPortData, _hPort is the handle of the port, _pWnd is the pointer to the window object (an MFC library class, CWnd) that will receive the notification from the monitoring thread, and _hEvNot is used for signaling the end of received characters. To use a fixed-size buffer, _hEvNot is needed to enable reading in chunks (e.g., 128 characters at a time). Each pass will be executed in overlapped mode (asynchronously, through _olRead). But if more than 128 characters (the buffer size) are received at the port, another mechanism is required to know when to end the series of passes. This series of passes will end with the help of _hEvNot. The thread resets the state of _hEvNot when characters are received, and the window (indirectly, through the call of CPort::Read) sets it when all the characters are read. In other words, the window calls CPort::Read repeatedly until there are no more bytes to be read. At that point, the state of _hEvNot is set to signaled in CPort::Read.

In this implementation, writing to the port is not so complicated because the size of the written data is small and can be transferred almost instantly. The other members of CPortData are _olWrite, which is used for overlapped writing; _bGoOn, which is used to terminate the thread; and _uMsg, which is the message sent to _pWnd. CPortData has no public members, as it is intended merely as a place to store the variables. Note also the use of the volatile keyword. Since multitasking is involved, we can never be too careful. The code must warn the compiler that the value of these variables should not be cached in optimizations. With the help of CPortData, I can now define the CPort class:

class CPort : public CErr {
    CPortData*  _pData;
    CWinThread* _pThr;

    BOOL SetDCB();
    BOOL InitPort(const char*);
    void DoneReading();

    //thread function, must be static
    static UINT PortFunc(PVOID);
    
public:
    CPort(const char* szPort,
          CWnd* pWnd, UINT uMsg);
    ~CPort();

    int   Read(char*,int);
    BOOL  Write(const char*,int);
    void  Flush();

    BOOL  IsConn(){
        return (BOOL)_pThr;};
    void  StartOp();
    void  StopOp();
};

Using the serial port involves the following steps:

1. Open the port through the CreateFile API.

2. Specify the events of which we want to be notified through the SetCommMask API.

3. Set the size of the port buffers through the SetupComm API.

4. Set the port timeouts through the SetCommTimeouts API.

5. Set the DCB (Device Control Block) structure in CPort::SetDCB.

The most complicated of these steps is the fifth. Usually a program obtains the port's DCB, changes some of its members, and then reconfigures the port. In this case, the configuration for the port is kept in a file (.ini), but the program could pass a DCB structure in the constructor of CPort or pass the most important configuration data (baud rate, parity, etc.) through individual variables. Of course, the constructor will be rather complicated, so the configuration file is an elegant solution. The above steps are performed in CPort::InitPort (Figure 1), which shows the default arguments to be passed to the API functions.

The functions CPort::Write (Figure 2) and CPort::Read (Figure 3), show how the overlapped port operations are performed. Note that CPort::Read first checks (via the ClearCommError API) if there are any unread bytes and then tries to read them. Also when there are no more bytes to be read, Read sets the state of _hEvNot to signaled through CPort::DoneReading.

The monitoring thread is created in CPort::StartOp (Figure 4) and destroyed in CPort::StopOp (Figure 5). Since the thread does not do any time-critical processing, it is created as a low-priority thread. The function pointer, CPort::PortFunc, passed to AfxBeginThread must be the address of a plain C-style function (see Figure 6). Since this function is also a member of CPort, it must be declared as static so that it does not include an implicit this pointer argument. The argument passed to this function is the address of CPort (this) to give the function access to the CPort members. As noted before, all this function does is wait for port events to happen, and when they do happen, it notifies the associated window. In our case, we are interested only in EV_RXCHAR events. In general, the program anticipates any event, so it must wait for events in overlapped mode. In every loop, the program must wait for all bytes to be read (wait for _hEvNot) before the next iteration.

In the message sent to the window, the address of CPort is passed in wParam and the event is passed in lParam. So when the window receives the message, we have all the information we need to proceed. Furthermore, we can have many ports sending different event notifications to the same window through the same message, and we will know which port sent which notification without the use of extra variables.

Using Class CPort

Using the CPort class is pretty straightforward. First, initialize the class by passing the proper arguments to its constructor. These arguments are a string describing the port (e.g. "COM2"), a pointer to the window being notified, and a UINT representing the message that will be sent to the window when an EV_RXCHAR event occurs. The port initialization could take place in the MFC functions CDialog::OnInitDialog, if the application is dialog-based, or CWnd::OnCreate.

The following code illustrates the creation of a CPort:

CString strPort =
    (LPCTSTR)AfxGetApp()->
        GetProfileString(
            "Communication",
            "Port", "COM2");

_pPort = new CPort(strPort, this,
                   WM_SERIAL_PORT);

The next step is to define a handler in CPort's parent window to respond to this message. Finally, before you start using the port, call CPort::StartOp. I chose a two-step initialization process to save processor time and have the monitoring thread running only when it is needed. Also, to temporarily stop the port operation, call CPort::StopOp. A typical handler would look like this:

LRESULT
CDragerDlg::OnPortMsg(WPARAM wParam,
                      LPARAM lParam){

    static char buf[PORTBUFLEN];
    MSG msg;
    int nLen ;
    CPort* pPort = (CPort*) wParam;
    do {
        while(PeekMessage(&msg, NULL,
                  0, 0, PM_NOREMOVE))
          AfxGetApp()->PumpMessage();

        if(nLen = pPort->Read(buf,
                    (int)PORTBUFLEN))
            ProcessPortData(
                (unsigned char*)buf,
                nLen);
  } while(nLen);
return 0;
}

This handler uses the PeekMessage mechanism to keep the window responsive, thus giving port messages low priority. To write to the port, call CPort::Write.

Sample Application

I've provided a sample MFC application on the ftp site (see p. 3 for downloading instructions) that you can use to read from and write to the serial port. Instructions for use are included with the source code.

Further Enhancements

The above class was tested extensively in a project involving real-time communication with medical devices, and it performs as specified. However, it is possible to enhance its functionality simply by making it a template with a functor as a template argument. Instead of calling CWnd::PostMessage, you would call that functor and pass the same arguments. Thus, the handling of port events could occur in another class, and CPort could be used in console applications or without the MFC framework. However, only function CPort::PortFunc should be a template (parameterized by the functor type being called), so this requires use of a new element of C++, member function templates. Which have yet to be implemented in all compilers. We could also pass a DWORD to the constructor with a bitwise OR of the events to be monitored.

References

[1] Jeffrey Richter. Advanced Windows, 3rd edition (Microsoft Press, 1997).

[2] Ralph Davis. Win32 Network Programming: Windows 95 and Windows NT Network Programming Using MFC (Addison Wesley, 1996).

Thanos D. Konstantinidis received his diploma from Aristotle's University of Thessaloniki, Department of Electric and Computer Engineering. He also holds a degree in programming. He has seven years experience programming with C++. His main fields of interest are biomedical signal and image processing, and 2-D and 3-D graphics.

Michael Gerassimos Strintzis received a Ph.D. in Electrical Engineering from Princeton University, Princeton, N.J., in 1970. Since 1980 he has been a professor of Electrical and Computer Engineering at the University of Thessaloniki, Thessaloniki, Greece. His current research interests include 2-D and 3-D Image Coding, Image Processing, and Biomedical Signal and Image Processing. Dr. Strintzis is a member of the NYAS, SPIE, and a senior member of IEEE. In 1984, Dr. Strintzis was awarded one of the Centennial Medals of the IEEE.