Serial Port I/O

Home
Back To Tips Page

I see a lot of incredibly convoluted and complex code for dealing with serial ports.  It ranks as some of the most complex code I've ever seen, and it is surprising, because there is rarely any need to have such complex code.

The first thing to deal with is that you need to do asynchronous I/O to serial ports.  Otherwise, you can't do concurrent reading and writing.  In particular, if you use synchronous I/O, if you have a pending ReadFile, you can't do a WriteFile

I see a lot of people trying to use the serial port ActiveX control, with the consequent set of questions about how complex it is.  I have never used this control because there is little reason I see for it.  Serial port I/O isn't that complex.  Once you've written it, you can clone this code infinitely, and it gives much more capability than relying on the ActiveX control.

There are many common errors I see being committed.  The use of SetCommEvent and WaitCommEvent rank high in bad methodologies for serial port work; these exist largely to support 16-bit Windows which did not allow for multiple threads or asynchronous I/O.  Forget they exist.  Another is the use of Event objects to signal the thread that there is something to write, or there is a pending read request.  As far as writing, the best and safest approach is to use a queue to handle this.  There are several approaches.  I'm going to explain one of them here but the other approaches are equally viable

Another problem I see is people trying to do too much at once.  Using a single thread to do both input and output results in code that is far too convoluted.  Such code is difficult to create, debug, or even reason about successfully and should be avoided.  Using separate threads for input and output results in cleaner code, with a nice separation of concerns.

Opening the serial port

The first thing you have to do with your serial port is to open it.  This code will open the serial port for full-duplex I/O

hCom = ::CreateFile(_T("\\\\.\\COMn"),             // filename
                    GENERIC_READ | GENERIC_WRITE,  // desired access
                    0,                             // exclusive
                    NULL,                          // security irrelevant
                    OPEN_EXISTING,                 // it has to be there
                    FILE_FLAG_OVERLAPPED,          // open asynchronous
                    NULL);                         // template file

Note the use of the string for the serial port name.  The names "COM1".."COM9" work because there is a special hack in CreateFile that recognizes "C" "O" "M" followed by a single digit as being a special case.  If you want to open COM10, however, you have to specify it as \\.\COM10, which in a quoted string requires doubling the "\" character.

The FILE_FLAG_OVERLAPPED is critical here.  This opens an asynchronous handle.

Defining messages

We will need to send this thread the messages we want to write.  The assumption here is that these are 8-bit character strings not containing embedded NUL characters.  Note that this is Unicode-aware code, and the correct declaration for a string of 8-bit characters is CStringA.  For VS6, this type does not exist, and you will have to do additional work if you want to support Unicode apps that send 8-bit strings.  In the case of Unicode apps, you must be careful because you could receive an odd number of bytes, so you might get half-a-Unicode character, and the programming is a bit more complex to deal with this; you have to save the half-character and prepend it to the characters coming in.  At this point, it might be best to abandon the concept of CString entirely and work with a raw LPBYTE buffer which is selectively converted to Unicode.

To send messages, I first define a message code.  I prefer Registered Window Messages (see my essay on Message Management) so I would do

/*******************************************************************************
*                                UWM_SEND_DATA
* Inputs:
*       WPARAM: (WPARAM)(CString *) string to send
*       LPARAM: unused
* Result: void
*       This is only called for PostThreadMessage calls and returns no value
* Effect:
*       Queues up a message for transmission
*******************************************************************************/

static UINT UWM_SEND_DATA = ::RegisterWindowMessage(_T("UWM_SEND_DATA-{CC871902-DB13-4330-9E43-C03C6B8CBE45}");

Similarly I define messages the threads will send back to the main thread

/*******************************************************************************
*                           UWM_WRITER_SHUTTING_DOWN
* Inputs:
*       WPARAM: Error code (ERROR_SUCCESS if not an error)
*       LPARAM: unused
* Result: LRESULT
*       Logically void, 0, always
* Effect:
*       Notifies the main thread that the writer is quitting
*******************************************************************************/
static UINT UWM_WRITER_SHUTTING_DOWN = ::RegisterWindowMessage(_T("UWM_WRITER_SHUTTING_DOWN-{CC871902-DB13-4330-9E43-C03C6B8CBE45}");

/*******************************************************************************
*                           UWM_READER_SHUTTING_DOWN
* Inputs:
*       WPARAM: Error code (ERROR_SUCCESS if not an error)
*       LPARAM: unused
* Result: LRESULT
*       Logically void, 0, always
* Effect:
*       Notifies the main thread that the writer is quitting
*******************************************************************************/
static UINT UWM_READER_SHUTTING_DOWN = ::RegisterWindowMessage(_T("UWM_READER_SHUTTING_DOWN-{CC871902-DB13-4330-9E43-C03C6B8CBE45}");

/*******************************************************************************
*                           UWM_DATA_READ
* Inputs:
*       WPARAM: (WPARAM)(CStringA *) The data that was read
*       LPARAM: unused
* Result: LRESULT
*       Logically void, 0, always
* Effect:
*       Notifies the main thread that the writer is quitting
*******************************************************************************/
static UINT UWM_DATA_READ = ::RegisterWindowMessage(_T("UWM_DATA_READ-{CC871902-DB13-4330-9E43-C03C6B8CBE45}");

/*******************************************************************************
*                           UWM_DATA_WRITTEN
* Inputs:
*       WPARAM: unused
*       LPARAM: unused
* Result: LRESULT
*       Logically void, 0, always
* Effect:
*       Notifies the main thread that the writer is quitting
*******************************************************************************/
static UINT UWM_DATA_WRITTEN = ::RegisterWindowMessage(_T("UWM_DATA_WRITTEN-{CC871902-DB13-4330-9E43-C03C6B8CBE45}");

Note that I can use the same GUID as a suffix on all the messages

Passing parameters to the thread

Now, if the open was successful, you have a handle.  So let's create a class by which we can pass information to a worker thread.  The use of a shutdown event is documented in my essay on Worker Threads.

class SerialParameters {
    public:
        SerialParameters() { hCom = NULL; notifyee = NULL; shutdown = NULL; }
        SerialParameters(HANDLE h, CWnd * w, HANDLE sd) { 
                      hCom = h; notifyee = w; shutdown = sd; }
        HANDLE hCom;
        CWnd * notifyee;
        HANDLE shutdown;
};  

The Writer thread

Now we can create some threads.  What I'm going to do here is use a UI thread for the sender thread and a general worker thread for the receiver thread.

An alternate implementation is to use an ordinary worker thread, and instead use an I/O Completion Port as a queuing mechanism.  See my essay on I/O Completion Ports.

class SerialWriter : public CWinThread {
     ... usual ClassWizard stuff...
    public:
       SerialParameters * parms;
    protected:
       void OnSendData(WPARAM, LPARAM); // see below
       HANDLE WriteEvent;               // see below
};

So you create the SerialParameters structure with the appropriate arguments.

To create the thread, do

/* SerialWriter * */ writer = (SerialWriter *)AfxBeginThread(RUNTIME_CLASS(SerialWriter), 
                                                  THREAD_PRIORITY_NORMAL, // priority 
                                                  0,                      // default stack size
                                                  CREATE_SUSPENDED);      // don't run right away

if(writer == NULL)
   { /* deal with error */
    ...
   } /* deal with error */

writer->parms = new SerialParameters(...);
writer->ResumeThread();   // now let it run  

When I want to write an 8-bit CString, I might do something like

CStringA * msg = new CStringA(whatever_I_want_to_send);
writer->PostThreadMessage(UWM_SEND_DATA, (WPARAM)msg);
...create reader thread here (see below)

Next, in the SerialWriter thread class definition, I declare a handler as shown above,

void SerialWriter::OnSendData(WPARAM wParam, LPARAM)
   {
    CStringA * s = (CStringA *)wParam;

    OVERLAPPED ovl = {0};
    ovl.hEvent = WriteEvent;

    DWORD bytesWritten;
    BOOL ok = ::WriteFile(parms->hCom,         // handle
                          (LPCSTR)*s,          // 8-bit data
                          s->GetLength(),      // length
                          &bytesWritten,       // amount written
                          &ovl);               // overlapped structure
    if(!ok)
      { /* failed */
       DWORD err = ::GetLastError();
       if(err != ERROR_IO_PENDING)
         { /* serious error */
          parms->notifyee->PostMessage(UWM_WRITER_SHUTTING_DOWN, (WPARAM)::GetLastError());
          ... handle failure notification here
          PostQuitMessage(0);  // this responds by shutting down the thread
                               // Your Mileage May Vary
          delete s;
          return;
         } /* serious error */

       // If we get here, the reason is ERROR_IO_PENDING
       // so wait for the I/O operation to complete

       // By using WFMO and having the shutdown event be first, this allows us
       // to break out of the wait to shut the thread down cleanly
       HANDLE waiters[2];
       waiters[0] = parms->shutdown;
       waiters[1] = WriteEvent;
       DWORD reason = ::WaitForMultipleObjects(2, waiters, FALSE, INFINITE);
       switch(reason)
          { /* waitfor */
           case WAIT_OBJECT_0:  // it was the shutdown event
              // shutting down
              ::CancelIo(parms->hCom);
              parms->notifyee->PostMessage(UWM_WRITER_SHUTTING_DOWN, ERROR_SUCCESS);
              PostQuitMessage(0);
              delete s;
              return;
           case WAIT_OBJECT_0 + 1: // data complete
              { /* write complete */
               BOOL ok = ::GetOverlappedResult(parms->hCom, &ovl, &bytesWritten, TRUE);
               if(!ok)
                 { /* failed */
                  parms->notifyee->PostMessage(UWM_WRITER_SHUTTING_DOWN, (WPARAM)::GetLastError());
                  PostQuitMessage(0);
                  delete s;
                  return;
                 } /* failed */  
               delete s;
              } /* write complete */
              break;
           default:
              { /* trouble */
               DWORD err = ::GetLastError();
               ASSERT(FALSE);
               parms->notifyee->PostMessage(UWM_WRITER_SHUTTING_DOWN, (WPARAM)err);
               PostQuitMessage(0);
               delete s;
               return;
              } /* trouble */
           } /* waitfor */
      } /* failed */
    else
      { /* successful write */
       delete s;
      } /* successful write */
    // if we get here, either the WriteFile succeeded or we waited for it to complete
    parms->notifyee->PostMessage(UWM_DATA_WRITTEN);
    delete s;
   }

Note that in VS6 this has to be declared as an LRESULT type and return 0, but in VS.NET it must be a void (which actually makes more sense).

Where did that WriteEvent come from?  We create it in the InitInstance handler and free it in the ExitInstance handler:

BOOL SerialWriter::InitInstance()
   {
    WriteEvent = ::CreateEvent(NULL, TRUE, FALSE, NULL);
    if(WriteEvent == NULL)
       return FALSE;
    ... rest of initialization, if any
    return TRUE; 
   }

int SerialWriter::ExitInstance()
   {
    ::CloseHandle(WriteEvent);
    return CWinThread::ExitInstance();
   }

That takes care of the write side.  If you don't care when the write completes, the PostMessage(UWM_DATA_WRITTEN) could be omitted.

Creating the Reader thread

The reader side is symmetric.  It's just a worker thread

In the class that is going to receive the message, declare

static UINT ReaderThread(LPVOID p);
...create the writer thread (see above)
AfxBeginThread(ReaderThread, writer->parms);

Then write the code for the thread

/* static */ LRESULT CMyClass::ReaderThread(LPVOID p)
   {
    SerialParameters * parms = (SerialParameters *)p;
    OVERLAPPED ovl = {0};
    ovl.hEvent = ::CreateEvent(NULL, TRUE, FALSE, NULL);
    if(ovl.hEvent == NULL)
      { /* failed */
       DWORD err = ::GetLastError();
       parms->notifyee->PostMessage(UWM_READER_SHUTTING_DOWN, (WPARAM)err);
       return 0;
      } /* failed */

    DWORD shutdown;  // reason for shutdown
    HANDLE waiters[2];
    waiters[0] = parms->shutdown;
    waiters[1] = ovl.hEvent;

#define MAX_BUFFER_SIZE 100
    BOOL running = TRUE;
    DWORD bytesRead;
    while(running)
       { /* read loop */
        BYTE buffer[MAX_BUFFER_SIZE];
        BOOL ok = ::ReadFile(parms->hCom, buffer, MAX_BUFFER_SIZE - 1, &bytesRead, &ovl);
        if(!ok)
           { /* error */
            DWORD err = ::GetLastError();
            if(err != ERROR_IO_PENDING)
               { /* read error */
                shutdown = err;
                running = FALSE;
                continue;
               } /* read error */
            // otherwise, it is ERROR_IO_PENDING
            DWORD result = ::WaitForMultipleObjects(2, waiters, FALSE, INFINITE);
            switch(result)
               { /* wait */
                case WAIT_OBJECT_0:  // shutdown
                    ::CancelIo(parms->hCom);
                    shutdown = ERROR_SUCCESS;  // clean shutdown
                    running = FALSE;
                    continue;
                case WAIT_OBJECT_0 + 1: // I/O complete
                    ok = ::GetOverlappedResult(parms->hCom, &ovl, &bytesRead, TRUE);  
                    if(!ok)
                       { /* GetOverlappedResult failed */
                        DWORD err = ::GetLastError();
                        running = FALSE;
                        continue;
                       } /* GetOverlappedResult failed */
                    break;
                default:
                    { /* trouble */
                     shutdown = ::GetLastError();
                     ASSERT(FALSE); // failure
                     running = FALSE;
                     continue;
                    } /* trouble */
               } /* wait */  
           } /* error */

        // if we get here, either the ReadFile worked immediately, or we waited for completion
        if(bytesRead == 0)
           continue; // nothing was read
        buffer[bytesRead] = '\0'; // assumes 8-bit characters without embedded NUL
        CStringA * s = new CStringA((LPCSTR)buffer);
        parms->notifyee->PostMessage(UWM_DATA_READ, (WPARAM)s);
       } /* read loop */

    parms->notifyee->PostMessage(UWM_READER_SHUTTING_DOWN, (WPARAM)shutdown);
    ::CloseHandle(ovl.hEvent);
    return 0;  // we're gone. You may choose to do something different
   } // CMyClass::ReaderThread

The appropriate message handlers should be declared in the main GUI thread in the class of the recipient, as described in my essay on Worker Threads.

Summary

Key here is to separate concerns.  Readers read data.  Writers write data.  Many implementations I've seen try to impose sequentiality, or try to do both reading and writing in a single thread.  Neither of these approaches are viable, because the result is code which is gratuitously complex (note that most of the code above is required anyway, independent of the choice of implementation; by separating out reading from writing, the code is much easier to understand).

I have used code like this for over a decade.  In some cases, I'm reading data packets and I don't PostMessage the data until I have a properly-formatted data packet with correct checksum.  In some cases, the failure to get a packet is meaningful, in others, I can discard the invalid packet.  In other cases, I'm receiving 8-bit data which is 8-bit characters although the rest of my application is Unicode; I will do a A2T macro to convert it, or MultieByteToWideChar, depending on the needs of the application.  Similarly, I will convert Unicode to 8-bit characters.  Note that if the target page is CP_ACP, this can result in data loss. When data loss is not acceptable but Unicode is required, I will most commonly use UTF-8 encoding, target page CP_UTF8.  This can become tricky because it is necessary to make sure that an entire character sequence has been received at the end of the packet.  Addressing issues like this are left as Exercises For The Reader.

There is no download because copy-and-paste from this page should suffice.  I may add a download when I'm not trying to prepare for a 2-week trip for which I am about to depart in less than 19 hours, as of the time I am typing this sentence.  Much of the code here resembles the code in the MTTTY example in the MSDN.

Change Log

16-Aug-06 Added 'break' in case WAIT_OBJECT_0+1. Thanks to 'aaee' for discovering this bug and reporting it.

17-Aug-06 Fixed nesting of braces and added success case.

26-Nov-07 Changed "defome" to "define". 

25-Jan-08 Added links to explain the shutdown event.

[Dividing Line Image]

The views expressed in these essays are those of the author, and in no way represent, nor are they endorsed by, Microsoft.

Send mail to newcomer@flounder.com with questions or comments about this web site.
Copyright © 2006, The Joseph M. Newcomer Co. All Rights Reserved
Last modified: May 14, 2011