Writing a Serial Communication Library for Windows

Serial Data transmission can be diffucult for newcomers to the world of Serial Communication and especially for developers trying it on the Windows platform using C++/VC++. This article illustrates how to develop a simple class for implementing serial data communication.

Introduction

Serial Data transmission seems a bit difficult for those who are new to the world of Serial Communication and especially for the developers who try to achieve it on the Windows platform using C++/VC++. When I was an Electronics graduate doing my Final year project that was developing a Add-on Data Acquisition Card that works on Windows 98/2000/XP, I faced a lot of problems on Windows 2000/XP and I searched a lot for some help on accessing ports in Windows 2000/XP. I got some valuable information on Direct Port Access under Windows NT on DDJ.com in an article by Dale Roberts. Here in this article I combine that experience with Serial communication and show you how to develop a simple class for implementing serial data communication.

This simple Class will ease development of Data Communication or Embedded Interface applications. It implements the functionality using the Win32 API functions, so I’d expect basic familiarity with Win32 on the reader’s part. So we shall first understand the basics of Serial data communication before we get to the class.

Serial Data Transmission Basics

In serial data communication the data is transmitted in serial format with the LSB (Least Significant Bit) of the byte to be transmitted as the first among the data bits. The general format for serial transmission is as in the figure below.

Serial Data Communication Library

The Parity bit (as in the third box) is optional. It is to be included only if you want to put a check on the validity of the transmitted data on the receiving end i.e. to perform error checking in communication. Parity settings can be altered by software modifications and hence it is easy to enable or disable it as per the design of the application and hardware you’re designing. You might be aware that the parity can be odd or even so you might like to configure it appropriately.

The following steps are used for sending and receiving data through the serial port of a PC:

  • Open the Communication Port that you decide to use.

  • Configure the selected Communication Port by setting the Baud rate, parity, no. of data bits, etc.

  • Set time-out value for Communication.

  • Read/Write Data to the Port.

  • Close the Port.

Now we shall discuss these steps in detail so as to get a closer look at the process.

{mospagebreak title=Opening the Selected Serial Port}

The CreateFile () function opens a communications port. There are two ways to call CreateFile () to open the port:

• Overlapped: Overlapped operations enable a thread to execute a time-consuming I/O operation in the background, leaving the thread free to perform other tasks. Now while reading or writing to the opened device in overlapped mode of operation, the calling thread must specify a pointer to an OVERLAPPED structure. This mode is mostly used to monitor the activity on the port or to do I/O intensive operations and this library is written to handle Non-Overlapped operations so I’d suggest to interested readers to peek in the MSDN documentation for more details.

• Non-Overlapped: One may open a Communication Port for either Overlapped IO operation or Non-Overlapped IO operation. This class/library is written for Non-Overlapped IO Operation. So we’re not going to discuss about the overlapped mode.

Configuring the Selected Serial Port

When the CreateFile() function opens a handle to a serial communications resource, the system initializes and configures the resource according to the values set up the last time the resource was opened. Preserving the previous settings enables the user to retain the settings specified through a mode command when the device is reopened. The values inherited from the previous open operation include the configuration settings of the device control block (a DCB structure) and the time-out values used in I/O operations. If the device has never been opened, it is configured with the system defaults.

One may use the GetCommState and SetCommState APIs to determine the initial configuration of a serial communications resource or to set a new configuration.

The most critical part in serial communication programming is configuring the port settings with the DCB structure. The DCB structure defines the control setting for a serial communications device. Initializing the DCB structure incorrectly is a common problem among beginners. When a serial communications function does not produce the expected results, the DCB structure must be the first place to check. A call to the CreateFile() function opens a serial port with default port settings. Usually, you’d need to change the defaults as per your requirements. You must set the Baud rate for communication, Parity functions, number of Stop Bits, etc. in accordance with the requirements of the interfaced hardware device by calling appropriate Win32 API Functions.

Configuring Time-Out value

An application must always set communication time-outs by using the COMMTIMEOUTS structure each time it opens a communication port. If this structure is not configured, the port uses default time-outs supplied by the driver, or time-outs from a previous communication application as explained above. By assuming specific time-out settings when the settings are actually different, an application can have read/write operations that never complete or complete too often. You must configure the read & write time-outs by calling the appropriate Win32 API Functions.

Reading/Writing to the Serial Port

The WriteFile() function transfers data through the serial connection to another device. Before calling this function, an application must open and configure a serial port.

An application calls the ReadFile() function to receive data from a device at the other end of a serial connection.

Closing a Serial Port

You must close the communications port after serial transmission in order to make this port available for other applications which use this resource. As long as you are working with a port (i.e. the port is in an open state), other threads or applications will not be able to access to this port till you close the handle to that port in NON-OVERLAPPED IO Operation. Call the CloseHandle() function to close the serial port. CloseHandle() has one parameter, which is the handle returned by the CreateFile() call that opened the port.

{mospagebreak title=CSerialComm Class}

The CSerialComm class uses eight member functions to achieve the above mentioned functionality. Six are explained above and the rest two GetBytesRead and GetBytesWritten allow you to keep track of the amount of Transacted bytes. The class Diagram of the class is as below:

Serial Data Communications Library

Figure 1: The CSerialComm Class Diagram


The code for the methods is below and is self describing if you’ve understood the above theory.

BOOL CSerialComm::OpenPort(CString strPortName)
{
 strPortName= “//./” +strPortName;

 m_hComm = CreateFile (strPortName,
    GENERIC_READ | GENERIC_WRITE,
    0,
    0,
OPEN_EXISTING,
    0,
    0);

 if (m_hComm==INVALID_HANDLE_VALUE)
 {
::MessageBox(NULL,_T(“Cannot open Communication Port”),_T(“Com Port Error”),MB_OK | MB_ICONERROR);
  
  return FALSE;
 }
 else
 {
  return TRUE;
 }

}

The OpenPort() member function opens a communication port for data transmission. The parameter to be passed to this function is a string containing the port name. For example “com1” for COM1, “com2” for COM2 etc. If the function succeeds the return value is true, otherwise it is false.

BOOL CSerialComm::ConfigurePort(DWORD BaudRate,
BYTE ByteSize,
DWORD dwParity,
BYTE Parity,
BYTE StopBits)
{
if((m_bPortReady = GetCommState(m_hComm, &m_dcb))==0)
{
 ::MessageBox(NULL,_T(“GetCommState Error”),_T(“Error”),MB_OK | MB_ICONERROR);  CloseHandle(m_hComm);
 return FALSE;
}
 m_dcb.BaudRate  = BaudRate;
 m_dcb.ByteSize  = ByteSize;
 m_dcb.Parity  = Parity ;
 m_dcb.StopBits  = StopBits;
 m_dcb.fBinary  = TRUE;
 m_dcb.fDsrSensitivity = FALSE;
 m_dcb.fParity  = dwParity;
 m_dcb.fOutX  = FALSE;
 m_dcb.fInX   = FALSE;
 m_dcb.fNull   = FALSE;
 m_dcb.fAbortOnError = TRUE;
 m_dcb.fOutxCtsFlow = FALSE;
 m_dcb.fOutxDsrFlow = FALSE;
 m_dcb.fDtrControl = DTR_CONTROL_DISABLE;
 m_dcb.fDsrSensitivity= FALSE;
 m_dcb.fRtsControl = RTS_CONTROL_DISABLE;
 m_dcb.fOutxCtsFlow = FALSE;
 m_dcb.fOutxCtsFlow = FALSE;
 m_bPortReady = SetCommState(m_hComm, &m_dcb);
 if(m_bPortReady ==0)
 {
  ::MessageBox(NULL,_T(“SetCommState Error”),_T(“Error”),MB_OK | MB_ICONERROR);    CloseHandle(m_hComm);
  return FALSE;
 }
 return TRUE;
}

The ConfigurePort () member function configures a communication port for data transmission. The parameters to be passed to this function are given next.

{mospagebreak title=DWORD BaudRate, Parity}

DWORD BaudRate

It represents the Baud rate for communication supported by external device. For example, you can give this parameter as 9600 or CBR_9600 for a BaudRate of 9600. The available Standard Baud rates supported as defined in Winbase.h are:

CBR_110
CBR_300
CBR_600
CBR_1200
CBR_2400
CBR_4800
CBR_9600
CBR_14400
CBR_19200
CBR_38400
CBR_56000
CBR_57600
CBR_115200
CBR_128000
CBR_256000

BYTE ByteSize

This represents the number of bits in the bytes transmitted and received. Standard values are 8 or 4.

DWORD fParity

Specifies whether parity checking is enabled. If this parameter is TRUE<code>, parity checking is performed and errors are reported. If <code>FALSE no parity checking is performed.

BYTE Parity

Specifies the parity scheme to be used. This member can be one of the following values:

EVENPARITY
MARKPARITY
NOPARITY
ODDPARITY
SPACEPARITY
BYTE StopBits

Specifies the number of stop bits to be used. This member can be one of the following values:

ONESTOPBIT
ONE5STOPBITS
TWOSTOPBITS
NOTE

The ConfigurePort() function is written on the assumption that the communication flow control is completely controlled on the basis of the protocol supported by the external device. It transmits and receives data without checking CTS/RTS and Xon/Xoff hardware flow control. You can modify this to your requirements by changing the values of the members of DCB which are responsible for it, in the implementation of ConfigurePort() in SerialComm.cpp.

ConfigurePort(CBR_9600, 8, true, EVENPARITY , ONESTOPBIT )

BOOL CSerialComm::SetCommunicationTimeouts(DWORD ReadIntervalTimeout,
DWORD ReadTotalTimeoutMultiplier,
DWORD ReadTotalTimeoutConstant,
DWORD WriteTotalTimeoutMultiplier,
DWORD WriteTotalTimeoutConstant)
{
if((m_bPortReady = GetCommTimeouts (m_hComm, &m_CommTimeouts))==0)
{
return FALSE;
}

m_CommTimeouts.ReadIntervalTimeout =ReadIntervalTimeout;
m_CommTimeouts.ReadTotalTimeoutConstant =ReadTotalTimeoutConstant;
m_CommTimeouts.ReadTotalTimeoutMultiplier =ReadTotalTimeoutMultiplier;
m_CommTimeouts.WriteTotalTimeoutConstant = WriteTotalTimeoutConstant;
m_CommTimeouts.WriteTotalTimeoutMultiplier =WriteTotalTimeoutMultiplier;
m_bPortReady = SetCommTimeouts (m_hComm, &m_CommTimeouts);
if(m_bPortReady ==0)
{
::MessageBox(NULL,_T(“SetCommTimeouts function failed”),_T(“Com Port Error”),MB_OK | MB_ICONERROR);
CloseHandle(m_hComm);
return FALSE;
}
return TRUE;
}

The SetCommunicationTimeouts() member function sets the write and read timeouts for data transmission. The parameters to be passed to this function are given next.

{mospagebreak title=Write and Read Timeouts}

DWORD ReadIntervalTimeout

Specifies the maximum time, in milliseconds, allowed to elapse between the arrival of two characters on the communications line. During a ReadFile() operation, the time period begins when the first character is received. If the interval between the arrival of any two characters exceeds this amount, the ReadFile operation is completed and any buffered data is returned. A value of zero indicates that interval time-outs are not used. A value of MAXDWORD, combined with zero values for both the ReadTotalTimeout constant and ReadTotalTimeoutMultiplier members, specifies that the read operation is to return immediately with the characters that have already been received, even if no characters have been received.

ReadTotalTimeoutConstant

Specifies the constant, in milliseconds, used to calculate the total time-out period for read operations. For each read operation, this value is added to the product of the ReadTotalTimeoutMultiplier member and the requested number of bytes. A value of zero for both the ReadTotalTimeoutMultiplier and ReadTotalTimeoutConstant members indicates that total time-outs are not used for read operations.

ReadTotalTimeoutMultiplier

Specifies the multiplier, in milliseconds, used to calculate the total time-out period for read operations. For each read operation, this value is multiplied by the requested number of bytes to be read.

WriteTotalTimeoutConstant

Specifies the constant, in milliseconds, used to calculate the total time-out period for write operations. For each write operation, this value is added to the product of the WriteTotalTimeoutMultiplier member and the number of bytes to be written.

WriteTotalTimeoutMultiplier

Specifies the multiplier, in milliseconds, used to calculate the total time-out period for write operations. For each write operation, this value is multiplied by the number of bytes to be written.

A value of zero for both the WriteTotalTimeoutMultiplier and WriteTotalTimeoutConstant members indicates that total time-outs are not used for write operations.

For example, if your device transmits a block of characters with a max. timeout value of 500 ms between each characters, you can set the time-out function as SetCommunicationTimeouts(0,500,0,0,0);. If the function succeeds the return value is true otherwise false.

BOOL CSerialComm::WriteByte(BYTE by)
{
 m_nBytesWritten=0;
 if(WriteFile(m_hComm,&by,1,&m_nBytesWritten,NULL)==0)
 {
  return FALSE;
 }
 else
 {
  return TRUE;
 }
}

The WriteByte() member function writes the data byte to the communication port. The parameter to be passed to this function is the byte to be transmitted. You can call this function repeatedly in a loop with your data to be written placed in an array. Each time you send characters, increment the index of the array and call WriteByte() till all data bytes are transmitted.

If the function succeeds the return value is true otherwise false.

BOOL CSerialComm::ReadByte(BYTE& by)
{
 BYTE byResByte;
 by=0;

 DWORD dwBytesTxD=0;

 if (ReadFile (m_hComm, &byResByte, 1, &dwBytesTxD, 0))
 {
   if (dwBytesTxD == 1)
   {
     by=byResByte;
     return TRUE;
  }
 }
 return FALSE;
}

The ReadByte() member function reads data bytes from the communication port. The parameter to be passed to this function is the address of the variable in which the received data byte is to be stored. You can call this function repeatedly in a loop with your received data moved to an array. Each time you receive characters, increment the index of the array and call ReadByte() till all data bytes are received. If you know exactly the no. of response bytes from the external device you can call the ReadByte() function in a loop till all characters are received or a time out occurs. Sometimes you may not be able to predict the no. of response bytes from the external device. In that case call ReadByte file repeatedly till you get a time out and if the character received previously is a character representing end of transmission in your protocol format, the communication process is successfully completed. For example, for a device following 3964 the end of transmission is ‘ETX’ character. So use the ReadByte() function properly in accordance with the protocol supported by your external device.

If ReadByte()  succeeds the return value is true and the received Byte will be stored in the location pointed by the address of ReadByte() ‘s parameter. If a timeout occurs the return value will be false.

void CSerialComm::ClosePort()
{
CloseHandle (m_hComm);
return;
}

The ClosePort() member function closes a communication Port which is already in an Open state.

{mospagebreak title=How To Use ‘CSerialComm’ Class}

Take the following steps to use the CSerialComm class

  • Copy the SerialCom.h & SerialCom.cpp files and paste into your project directory.

  • In your VC++ IDE, add the files to your project

  • Add the line #include “SerialComm.h” in your dialog’s header file

  • Create an instance of the CSerialComm class in your dialog’s header file.

You can now call the member functions of CSerialComm when you want to communicate with external device as shown below.

In Your dialog’s .cpp File:

    // Open Communication Port. Please check functions return value to ensure whether
    // Port opened successfully.
    port.OpenPort( );
   
    // Configure Port for Communication. Please check functions return value to
    // ensure whether Port is configured  successfully.
    port.ConfigurePort( ); 

    // Set communication time outs. Please check functions return
    // value to ensure whether communication time outs configured 
    // successfully.
    port.SetCommunicationTimeouts( );
     
    // call this function in a loop till all bytes are written. Please check
    // functions return value to ensure whether Write operation completed 
    // successfully.
    port.WriteByte();
     
    // call this function in a loop till all bytes are received. Please check
    // functions return value to ensure whether Read operation completed 
    // successfully or a time out occurred.
    port.ReadByte( );

    // Call this function to close the handle to the port.
    // Process the received Data
    port.ClosePort();

If you decide to use the extension DLL directly just include the library SCommLib.lib file in the Project | Settings | Link tab as shown in the figure below.

Serial Data Communications Library

Figure 2: Setting the import Library

Next include the SerialComm.h file in your project (because it contains the class definition for the library).Now you’re free to make any instance of the class and the compiler would compile the code happily for you.

Source Code

You may download the source code here. It also contains a Compiled DLL that exports the CSerialComm class that will ease development of serial communication applications using MFC.

You’re free to use the code in any way. I hope you found the article informative. You may contact me at digvijay.chauhan@rediffmail.com for any comments and suggestions.

One thought on “Writing a Serial Communication Library for Windows

  1. This article plus its associated download/source code are one of the few a pages on the web that shed some light on 32 bit c++ serial comms under Windows. Particularly for embedded, data or robotics applications (despite the huge number of articles out there on the serial port). I’m still looking for that elusive sensor to PC to 32 bit C++ application tutorial which contains hardware/microcontroller/serial port circuit diagrams to PC, plus software for both micrcontroller and PC. I’m sure its out there somewhere! mike@magellan-project.demon.co.uk

[gp-comments width="770" linklove="off" ]