Contents
Introduction
This is the fourth in a series of articles concerning Windows SDK development that I have written over the years. The first three are:
- Win32 SDK Data Grid View Made Easy [^]
- Win32 SDK C Tab Control Made Easy [^]
- Win32 SDK C Autocomplete Combobox Made Easy [^]
Several years ago, I wanted to have a simple to use, message based, serial port control that I could employ in my Win32 C projects. I started experimenting with the idea and playing around with the serial port in my spare time, but as so often happens, the project ended up on the shelf when I got busy again.
Recently, I came across that project and, with a better understanding of how serial port communications work on Windows, decided to finish it. Since I haven't encountered a serial port wrapper that takes this approach anywhere, I decided to publish it on The Code Project.
A message based RS232 component
The Windows SDK provides methods for configuring the serial port. Other methods are available for monitoring the port and raising events, as well as file stream methods for reading and writing data. Take it all together, and serial port communication can become a somewhat daunting exercise. This control packs most of that functionality into a single Windows class that can be included in a Win32 project and accessed using standard Windows messaging and notifications.
Using the serial port control
To begin, include the serial port control's header file in the project:
#include "serialport.h"
This serial port control is a message based, custom control, and as such must be initialized before use. One way to handle this is to call the initializer in the WinMain()
method of the application just after the call to InitCommonControlsEx()
.
int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpszCmdLine, int nCmdShow)
{
INITCOMMONCONTROLSEX icc;
WNDCLASSEX wcx;
ghInstance = hInstance;
icc.dwSize = sizeof(icc);
icc.dwICC = ICC_WIN95_CLASSES;
InitCommonControlsEx(&icc);
InitSerialPortControl(hInstance);
To make things simple though, I combined this step in the control's pseudo constructor. It is called only once, the first time a new serial port control is instantiated.
HWND New_SerialPortControl(HWND hParent, DWORD dwID)
{
static ATOM aSerialPortControl = 0;
static HWND hSerial;
HINSTANCE hinst = (HINSTANCE) GetWindowLongPtr(hParent,GWLP_HINSTANCE);
if (!aSerialPortControl)
aSerialPortControl = InitSerialPortControl(hinst);
hSerial = CreateWindowEx(0, g_szClassName, _T(""), WS_CHILD, 0, 0, 0, 0,
hParent, (HMENU)dwID, hinst, NULL);
return hSerial;
}
Here is a snippet showing a very simple write/read operation with default configurations and no serial port monitoring:
char buf[256];
memset(&buf,0,sizeof(buf));
HWND hCom = New_SerialPortControl(hDialog,IDC_SERIAL);
CONFIG cg;
cg.mask = SPCF_PORTNAME;
cg.pszPortName = "COM2";
if(SerialPort_Open(hCom, &cg))
{
SerialPort_WriteString(hCom, "ATI3\r");
Sleep(1000);
SerialPort_ReadString(hCom, &buf, 255);
}
SerialPort_Close(hCom);
Here is a snippet showing a more robust write/read technique, polling the serial port to determine whether all of the bytes have been received before reading them from the port.
LPTSTR Query(HWND hSerial, LPTSTR szCommand, INT iTimeout)
{
LPTSTR buf;
SerialPort_WriteString(hSerial, szCommand);
DWORD dwTimeStamp = timeGetTime();
BOOL fElapsedTime = FALSE;
BOOL found = FALSE;
do
{
if (SerialPort_BytesToRead(hSerial) > 0) found = TRUE;
fElapsedTime = timeGetTime() - dwTimeStamp > iTimeout;
Sleep(100);
} while (!fElapsedTime && !found);
DWORD dwCount = 0;
BOOL fReading = TRUE;
while(fReading) {
if (0 == SerialPort_BytesToRead(hSerial)) fReading = FALSE;
else if (dwCount == SerialPort_BytesToRead(hSerial)) fReading = FALSE;
else dwCount = SerialPort_BytesToRead(hSerial);
Sleep(100); }
if(1 > dwCount)
return "";
else
{
buf = TmpStr(dwCount);
SerialPort_ReadString(hSerial, buf, dwCount);
return buf;
}
}
And here is a snippet showing the use of the SPN_DATARECEIVED
notification to respond to the arrival of bytes on the serial port:
static LRESULT MainDlg_OnNotify(HWND hwnd, INT id, LPNMHDR pnm)
{
switch(pnm->code)
{
case SPN_DATARECEIVED:
{
if(EV_RXFLAG == ((LPNMSERIAL) pnm)->dwCode) return FALSE;
DWORD dwCount = 0;
BOOL fReading = TRUE;
while(fReading) {
if (0 == SerialPort_BytesToRead(pnm->hwndFrom)) fReading = FALSE;
else if (dwCount == SerialPort_BytesToRead(pnm->hwndFrom)) fReading = FALSE;
else dwCount = SerialPort_BytesToRead(pnm->hwndFrom);
Sleep(100); }
if (0 == dwCount) return FALSE;
LPTSTR lpBuf = TmpStr(dwCount);
SerialPort_ReadBytes(pnm->hwndFrom, lpBuf, dwCount)
}
}
return TRUE;
}
These examples show some of the ways that the control can be simply implemented in a Win32 project. To demonstrate the class in a useful context, I put together a demo that makes use of two modems connected to two land lines. I happened to have the lines and hardware available to play with, but even without them, the demo code should give a fairly complete idea of how to use the class.
What follows is a programming reference for the serial port control class.
Public data structures
CONFIG
The CONFIG
structure specifies or receives attributes for an instance of the serial port control.
typedef struct tagConfig {
UINT mask;
LPTSTR pszPortName;
INT cchTextMax;
DWORD dwBaudRate;
BYTE bParity;
BYTE bDataBits;
BYTE bStopBits;
BOOL fDiscardNull;
FLOWCONTROL flowControl;
} CONFIG, *LPCONFIG;
Members
mask
: A set of bit flags that specify the attributes of this data structure or of an operation that is using this structure. The following bit flags specify the members of the CONFIG
structure that contain valid data or need to be filled in. One or more of these bit flags may be set:
SPCF_PORTNAME
- The pszPortName
member is valid or needs to be filled in.SPCF_BAUDRATE
- The dwBaudRate
member is valid or needs to be filled in.SPCF_PARITY
- The bParity
member is valid or needs to be filled in.SPCF_DATABITS
- The bDataBits
member is valid or needs to be filled in.SPCF_STOPBITS
- The bStopBits
member is valid or needs to be filled in.SPCF_NULLDISCARD
- The fDiscardNull
member is valid or needs to be filled in.SPCF_FLOWCONT
- The flowControl
member is valid or needs to be filled in.SPCF_ALLSETTINGS
- All members are valid or need to be filled in.
pszPortName
- Pointer to a null-terminated string that contains the port name (e.g.: "COM1") if the structure specifies item attributes. If the structure is receiving item attributes, this member is the pointer to the buffer that receives the item text.cchTextMax
- Size of the buffer pointed to by the pszPortName
member if the structure is receiving item attributes. If the structure specifies item attributes, this member is ignored.dwBaudRate
- Specifies the baud rate at which the communications device operates. This member can be an actual baud rate value, or one of the following baud rate indexes:
CBR_110
- 110CBR_300
- 300CBR_600
- 600CBR_1200
- 1200CBR_4800
- 4800CBR_9600
- 9600CBR_14400
- 14400CBR_19200
- 19200CBR_38400
- 38400CBR_56000
- 56000CBR_57600
- 57600CBR_115200
- 115200CBR_128000
- 128000CBR_256000
- 256000
bParity
- Specifies the parity scheme to be used. This member can be one of the following values:
NOPARITY
- No parityODDPARITY
- OddEVENPARITY
- EvenMARKPARITY
- MarkSPACEPARITY
- Space
bDataBits
- Specifies the number of bits in the bytes transmitted and received.bStopBits
- Specifies the number of stop bits to be used. This member can be one of the following values:
ONESTOPBIT
- 1 stop bitONE5STOPBITS
- 1.5 stop bitsTWOSTOPBITS
- 2 stop bits
fDiscardNull
- Specifies whether null bytes are discarded. If this member is TRUE
, null bytes are discarded when received.flowControl
- Specifies the type of flow control desired/employed. This member can be one of the following enumerated values:
NoFlowControl
CtsRtsFlowControl
CtsDtrFlowControl
DsrRtsFlowControl
DsrDtrFlowContro
XonXoffFlowControl
Remarks
- The address of this structure is specified as the lParam
parameter of the SPM_SETCONFIG
, SPM_GETCONFIG
, and SPM_OPEN
messages.
FLOWCONTROL
The FLOWCONTROL
enumerated type is used in the CONFIG
structure to specify the type of flow control desired or employed by the serial port control.
typedef enum tagFlowControl {
NoFlowControl,
CtsRtsFlowControl,
CtsDtrFlowControl,
DsrRtsFlowControl,
DsrDtrFlowControl,
XonXoffFlowControl
}FLOWCONTROL;
Messages and Macros
Configure the control to do what you want using Windows messages. To make this easy and as a way of documenting the messages, I created macros for each message. If you prefer to call SendMessage()
or PostMessage()
explicitly, please refer to the macro defs in the header for usage.
SerialPort_GetPortNames
LPTSTR* SerialPort_GetPortNames(
HWND hwnd
LPDWORD lpCount
);
hwnd
Handle to the serial port control.
lpCount
Pointer to number of items returned.
Return Values
Returns a list of installed serial ports.*/
SerialPort_SetConfigurations
BOOL SerialPort_SetConfigurations(
HWND hwnd
LPCONFIG lpConfig
);
hwnd
Handle to the serial port control.
lpConfig
Pointer to a CONFIG structure that contains configuration attributes.
Return Values
Returns TRUE if successful or FALSE otherwise.*/
SerialPort_GetConfigurations
BOOL SerialPort_GetConfigurations(
HWND hwnd
LPCONFIG lpConfig
);
hwnd
Handle to the serial port control.
lpConfig
Pointer to a CONFIG structure that specifies the information to retrieve and receives
information about the serial port control.
Return Values
Returns TRUE if successful or FALSE otherwise.*/
SerialPort_SetReadTimeout
BOOL SerialPort_SetReadTimeout(
HWND hwnd
DWORD dwTimeout
);
hwnd
Handle to the serial port control.
dwTimeout
Read timeout in milliseconds.
Return Values
Returns TRUE if successful or FALSE otherwise.*/
SerialPort_GetReadTimeout
DWORD SerialPort_GetReadTimeout(
HWND hwnd
);
hwnd
Handle to the serial port control.
Return Values
Returns Read timeout in milliseconds.*/
SerialPort_SetWriteTimeout
BOOL SerialPort_SetWriteTimeout(
HWND hwnd
DWORD dwTimeout
);
hwnd
Handle to the serial port control.
dwTimeout
Write timeout in milliseconds.
Return Values
Returns TRUE if successful or FALSE otherwise.*/
SerialPort_GetWriteTimeout
DWORD SerialPort_GetWriteTimeout(
HWND hwnd
);
hwnd
Handle to the serial port control.
Return Values
Returns Write timeout in milliseconds.*/
SerialPort_GetCTS
BOOL SerialPort_GetCTS(
HWND hwnd
);
hwnd
Handle to the serial port control.
Return Values
Returns TRUE if the CTS (clear-to-send) signal is on.*/
SerialPort_GetDSRs
BOOL SerialPort_GetDSR(
HWND hwnd
);
hwnd
Handle to the serial port control.
Return Values
Returns TRUE if the DSR (data-set-ready) signal is on.*/
SerialPort_BytesToRead
DWORD SerialPort_BytesToRead(
HWND hwnd
);
hwnd
Handle to the serial port control.
Return Values
Returns The number of bytes available to read from the rx buffer.*/
SerialPort_BytesToWrite
DWORD SerialPort_BytesToWrite(
HWND hwnd
);
hwnd
Handle to the serial port control.
Return Values
Returns The number of bytes remaining in the tx buffer.*/
SerialPort_WriteBytes
BOOL SerialPort_WriteBytes(
HWND hwnd
LPBYTE lpData
DWORD dwSize
);
hwnd
Handle to the serial port control.
lpData
pointer to byte data to write to port.
dwSize
The number of bytes to write.
Return Values
Returns TRUE if successful or FALSE otherwise.*/
SerialPort_ReadBytes
DWORD SerialPort_ReadBytes(
HWND hwnd
LPBYTE lpBuf
DWORD dwSize
);
hwnd
Handle to the serial port control.
lpBuf
pointer to a buffer to receive data.
dwSize
The buffer size.
Return Values
Returns The number of bytes read.*/
SerialPort_WriteString
BOOL SerialPort_WriteString(
HWND hwnd
LPTSTR lpsztext
);
hwnd
Handle to the serial port control.
lpsztext
The string to write to port.
Return Values
Returns TRUE if successful or FALSE otherwise.*/
SerialPort_ReadString
DWORD SerialPort_ReadString(
HWND hwnd
LPTSTR lpszBuf
DWORD dwSize
);
hwnd
Handle to the serial port control.
lpszBuf
pointer to a buffer to receive string.
dwSize
The buffer size.
Return Values
Returns The number of characters read.*/
SerialPort_Close
BOOL SerialPort_Close(
HWND hwnd
);
hwnd
Handle to the serial port control.
Return Values
Returns TRUE if successful or FALSE otherwise.*/
SerialPort_Open
BOOL SerialPort_Open(
HWND hwnd
LPCONFIG lpConfig
);
hwnd
Handle to the serial port control.
lpConfig
Pointer to a CONFIG structure that contains configuration attributes.
Return Values
Returns TRUE if successful or FALSE otherwise.*/
SerialPort_FlushReadBuf
BOOL SerialPort_FlushReadBuf(
HWND hwnd
);
hwnd
Handle to the serial port control.
Return Values
Returns TRUE if successful or FALSE otherwise.*/
SerialPort_FlushWriteBuf
BOOL SerialPort_FlushWriteBuf(
HWND hwnd
);
hwnd
Handle to the serial port control.
Return Values
Returns TRUE if successful or FALSE otherwise.*/
SerialPort_SetReceiveByteThreshold
BOOL SerialPort_SetReceiveByteThreshold(
HWND hwnd
DWORD dwNumBytes
);
hwnd
Handle to the serial port control.
dwNumBytes
The number of bytes necessesary to trigger the SPN_DATARECEIVED
notification.
Return Values
Returns TRUE if successful or FALSE otherwise.*/
SerialPort_GetReceiveByteThreshold
DWORD SerialPort_GetReceiveByteThreshold(
HWND hwnd
);
hwnd
Handle to the serial port control.
Return Values
Returns The number of bytes necessesary to trigger
the SPN_DATARECEIVED notification.*/
Notifications
The serial port control provides several notifications via WM_NOTIFY
. The lParam
parameter of these notification messages points to an NMSERIAL
structure.
NMSERIAL
The NMSERIAL
structure contains information about a serial port control notification message.
typedef struct tagNMSERIAL {
NMHDR hdr;
DWORD dwCode;
} NMSERIAL, *LPNMSERIAL;
SPN_DATARECEIVED
The SPN_DATARECEIVED
notification message notifies a serial port control's parent window that data was received. This notification message is sent in the form of a WM_NOTIFY
message.
SPN_DATARECEIVED
pnm = (NMSERIAL *) lParam;
SPN_PINCHANGED
The SPN_PINCHANGED
notification message notifies a serial port control's parent window that a pin has changed. This notification message is sent in the form of a WM_NOTIFY
message.
SPN_PINCHANGED
pnm = (NMSERIAL *) lParam;
SPN_ERRORRECEIVED
The SPN_ERRORRECEIVED
notification message notifies a serial port control's parent window that an error was received. This notification message is sent in the form of a WM_NOTIFY
message.
SPN_ERRORRECEIVED
pnm = (NMSERIAL *) lParam;
Receiving concurrent notifications
In order to have a serial port control with notifications, I employed asynchronous overlapped IO along with WaitCommEvent()
in a working thread that listens to the serial port and posts any activity to the main thread. I created methods to launch, and if necessary, terminate this thread. Here is the code that runs in the working thread:
DWORD WINAPI Listner_Proc(LPVOID StartParam)
{
BOOL fStarting = TRUE;
DWORD dwEvtMask = 0;
OVERLAPPED ov;
ov.Offset = 0;
ov.OffsetHigh = 0;
ov.hEvent = g_lpInst->hListnerEvent;
ResetEvent(ov.hEvent);
if (INVALID_HANDLE_VALUE != g_lpInst->hComm)
{
while (!g_lpInst->fEndListner)
{
if (SetCommMask(g_lpInst->hComm, EV_BREAK | EV_CTS |
EV_DSR | EV_ERR | EV_RING | EV_RLSD | EV_RXCHAR | EV_RXFLAG))
{
if (fStarting)
{
SetEvent(g_lpInst->hStartEvent);
fStarting = FALSE;
}
if (!WaitCommEvent(g_lpInst->hComm, &dwEvtMask, &ov))
{
if (GetLastError() == ERROR_IO_PENDING)
{
DWORD numBytes;
BOOL flag = WaitForSingleObject(ov.hEvent, -1);
do
{
flag = GetOverlappedResult(g_lpInst->hComm,
&ov, &numBytes, FALSE);
}
while ((GetLastError() == ERROR_IO_PENDING) && !flag);
}
}
PostMessage((HWND)StartParam, SPM_DISPATCHNOTIFICATIONS,
(DWORD)dwEvtMask, 0L);
}
}
}
return 0xDEAD;
}
Posted event messages are queued, and will be processed by the main thread in the order that they are received by the SPM_DISPATCHNOTIFICATIONS
event handler. The upside of this is that you can respond to notifications and update the user interface without the threat of cross threading. The downside is that no new notifications will reach the parent's WM_NOTIFY
handler until you have finished responding to the current notification.
One scenario where this behavior is undesirable is when the application is handling the SPN_DATARECEIVED
notification but you want to monitor pin changes concurrently. The notifications will arrive after they are needed when the SPN_DATARECEIVED
handler returns. The solution is to launch a working thread in response to the SPN_DATARECEIVED
notification so that WM_NOTIFY
returns to process the next queued notification.
Here is an example of this technique:
static LRESULT MainDlg_OnNotify(HWND hwnd, INT id, LPNMHDR pnm)
{
switch(pnm->code)
{
case SPN_DATARECEIVED:
{
if(EV_RXFLAG == ((LPNMSERIAL) pnm)->dwCode) return FALSE;
if(NULL == hWorkerThread)
{
_hWorkerThread = CreateThread(NULL, 0, On_DataReceived,
hwnd, 0, &dwWorkerID);
SetThreadPriority(_hWorkerThread, THREAD_PRIORITY_NORMAL);
}
}
case SPN_PINCHANGED:
{
OnPinChanged(hwnd, pnm);
}
}
}
Once the SPN_DATARECEIVED
handler completes, be sure to set the stored handle to NULL
so that a new thread will be launched in response to the next SPN_DATARECEIVED
notification.
DWORD WINAPI On_DataReceived(LPVOID StartParam)
{
__try
{
}
__finally
{
_hWorkerThread = NULL;
}
return 0;
}
History
- October 12, 2009: Version 1.0.0.0.
- August 3, 2010: Version 1.1.0.0 - Fixed allocation bug in
GetPortNames()
. - May 18, 2011: Version 1.2.0.0 - Several bug fixes: I rewrote
TerminateListner()
method to exit listner thread gracefully instead of using the less desirable TerminateThread()
. - May 20, 2011: Version 1.3.0.0 - Fixed bug introduced in the
GetPortNames()
method during last update.
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.