Basic Serial Communication
The interface for a serial device is a combination of generic driver I/O calls and specific communication-related functions. The serial device is treated as a generic, installable stream device for opening, closing, reading from, and writing to the serial port. For configuring the port, the Win32 API supports a set of Comm functions. Windows CE supports most of the Comm functions supported by Windows XP.A word of warning: programming a serial port under Windows CE isn't like programming one under MS-DOS. You can't simply find the base address of the serial port and program the registers directly. While there are ways for a program to gain access to the physical memory space, every Windows CE device has a different physical memory map. Even if you solved the access problem by knowing exactly where the serial hardware resided in the memory map, there's no guarantee the serial hardware is going to be compatible with the 16550-compatible serial interface we've all come to know and love in the PC world. In fact, the implementation of the serial port on some Windows CE devices looks nothing like a 16550.But even if you know where to go in the memory map and the implementation of the serial hardware, you still don't need to "hack down to the hardware." The serial port drivers in Windows CE are interrupt-driven designs and are written to support its specific serial hardware. If you have any special needs not provided by the base serial driver, you can purchase the Microsoft Windows CE Platform Builder and write a serial driver yourself. Aside from that extreme case, there's just no reason not to use the published Win32 serial interface under Windows CE.
Opening and Closing a Serial Port
As with all stream device drivers, a serial port device is opened using CreateFile. The name used needs to follow a specific format: the three letters COM followed by the number of the COM port to open and then a colon. The colon is required under Windows CE and is a departure from the naming convention used for device driver names used in Windows XP. The following line opens COM port 1 for reading and writing:
hSer = CreateFile (TEXT ("COM1:"), GENERIC_READ | GENERIC_WRITE,
0, NULL, OPEN_EXISTING, 0, NULL);
You must pass a 0 in the sharing parameter as well as in the security attributes and the template file parameters of CreateFile. Windows CE doesn't support overlapped I/O for devices, so you can't pass the FILE_FLAG_OVERLAPPED flag in the dwFlagsAndAttributes parameter. The handle returned is either the handle to the opened serial port or INVALID_HANDLE_VALUE. Remember that unlike many of the Windows functions, CreateFile doesn't return a 0 for a failed open.You close a serial port by calling CloseHandle, as in the following:
CloseHandle (hSer);
You don't do anything differently when using CloseHandle to close a serial device than when you use it to close a file handle.
Reading from and Writing to a Serial Port
Just as you use the CreateFile function to open a serial port, you use the functions ReadFile and WriteFile to read and write to that serial port. Reading data from a serial port is as simple as making this call to ReadFile:
INT rc;
DWORD cBytes;
BYTE ch;
rc = ReadFile(hSer, &ch, 1, &cBytes, NULL);
This call assumes the serial port has been successfully opened with a call to CreateFile. If the call is successful, one byte is read into the variable ch, and cBytes is set to the number of bytes read.Writing to a serial port is just as simple. The call would look something like the following:
INT rc;
DWORD cBytes;
BYTE ch;
ch = TEXT ('a');
rc = WriteFile(hSer, &ch, 1, &cBytes, NULL);
This code writes the character a to the serial port previously opened. As you may remember from Chapter 8, both ReadFile and WriteFile return TRUE if successful.Because overlapped I/O isn't supported under Windows CE, you should be careful not to attempt to read or write a large amount of serial data from your primary thread or from any thread that has created a window. Because those threads are also responsible for handling the message queues for their windows, they can't be blocked waiting on a relatively slow serial read or write. Instead, you should use separate threads for reading from and writing to the serial port.You can also transmit a single character using this function:
BOOL TransmitCommChar (HANDLE hFile, char cChar);
The difference between the TransmitCommChar and WriteFile functions is that TransmitCommChar puts the character to be transmitted at the front of the transmit queue. When you call WriteFile, the characters are queued up after any characters that haven't yet been transmitted by the serial driver. TransmitCommChar allows you to insert control characters quickly in the stream without having to wait for the queue to empty.
Asynchronous Serial I/O
While Windows CE doesn't support overlapped I/O, there's no reason why you can't use multiple threads to implement the same type of overlapped operation. All that's required is that you launch separate threads to handle the synchronous I/O operations while your primary thread goes about its business. In addition to using separate threads for reading and writing, Windows CE supports the Win32 WaitCommEvent function that blocks a thread until one of a group of preselected serial events occurs. I'll demonstrate how to use separate threads for reading and writing a serial port in the CeChat example program later in this chapter.You can make a thread wait on serial driver events by means of the following three functions:
BOOL SetCommMask (HANDLE hFile, DWORD dwEvtMask);
BOOL GetCommMask (HANDLE hFile, LPDWORD lpEvtMask);
and
BOOL WaitCommEvent (HANDLE hFile, LPDWORD lpEvtMask,
LPOVERLAPPED lpOverlapped);
To wait on an event, you first set the event mask using SetCommMask. The parameters for this function are the handle to the serial device and a combination of the following event flags:
EV_BREAKA break was detected.
EV_CTSThe Clear to Send (CTS) signal changed state.
EV_DSRThe Data Set Ready (DSR) signal changed state.
EV_ERRAn error was detected by the serial driver.
EV_RLSDThe Receive Line Signal Detect (RLSD) line changed state.
EV_RXCHARA character was received.
EV_RXFLAGAn event character was received.
EV_TXEMPTYThe transmit buffer is empty.
You can set any or all of the flags in this list at the same time using SetCommMask. You can query the current event mask using GetCommMask.
To wait on the events specified by SetCommMask, you call WaitCommEvent. The parameters for this call are the handle to the device, a pointer to a DWORD that will receive the reason the call returned, and lpOverlapped, which under Windows CE must be set to NULL. The code fragment that follows waits on a character being received or an error. The code assumes that the serial port has already been opened and that the handle is contained in hComPort.
DWORD dwMask;
// Set mask and wait.
SetCommMask (hComPort, EV_RXCHAR | EV_ERR);
if (WaitCommEvent (hComPort, &dwMask, 0) {
// Use the flags returned in dwMask to determine the reason
// for returning.
Switch (dwMask) {
case EV_RXCHAR:
//Read character.
break;
case EV_ERR:
// Process error.
break;
}
}
Configuring the Serial Port
Reading from and writing to a serial port is fairly straightforward, but you also must configure the port for the proper baud rate, character size, and so forth. The masochist could configure the serial driver through device I/O control (IOCTL) calls, but the IoCtl codes necessary for this are exposed only in the Platform Builder, not the Software Development Kit. Besides, here's a simpler method.You can go a long way in configuring the serial port using two functions, GetCommState and SetCommState, prototyped here:
BOOL SetCommState (HANDLE hFile, LPDCB lpDCB);
BOOL GetCommState (HANDLE hFile, LPDCB lpDCB);
Both these functions take two parameters, the handle to the opened serial port and a pointer to a DCB structure. The extensive DCB structure is defined as follows:
typedef struct _DCB {
DWORD DCBlength;
DWORD BaudRate;
DWORD fBinary: 1;
DWORD fParity: 1;
DWORD fOutxCtsFlow:1;
DWORD fOutxDsrFlow:1;
DWORD fDtrControl:2;
DWORD fDsrSensitivity:1;
DWORD fTXContinueOnXoff:1;
DWORD fOutX: 1;
DWORD fInX: 1;
DWORD fErrorChar: 1;
DWORD fNull: 1;
DWORD fRtsControl:2;
DWORD fAbortOnError:1;
DWORD fDummy2:17;
WORD wReserved;
WORD XonLim;
WORD XoffLim;
BYTE ByteSize;
BYTE Parity;
BYTE StopBits;
char XonChar;
char XoffChar;
char ErrorChar;
char EofChar;
char EvtChar;
WORD wReserved1;
} DCB;
As you can see from structure, SetCommState can set a fair number of states. Instead of attempting to fill out the entire structure from scratch, you should use the best method of modifying a serial port, which is to call GetCommState to fill in a DCB structure, modify the fields necessary, and then call SetCommState to configure the serial port.The first field in the DCB structure, DCBlength, should be set to the size of the structure. This field should be initialized before the call to either GetCommState or SetCommState. The BaudRate field should be set to one of the baud rate constants defined in Winbase.h. The baud rate constants range from CBR_110 for 110 bits per second to CBR_256000 for 256 kilobits per second (Kbps). Just because constants are defined for speeds up to 256 Kbps doesn't mean that all serial ports support that speed. To determine what baud rates a serial port supports, you can call GetCommProperties, which I'll describe shortly. Windows CE devices generally support speeds up to 115 Kbps, although some support faster speeds. The fBinary field must be set to TRUE because no Win32 operating system currently supports a nonbinary serial transmit mode familiar to MS-DOS programmers. The fParity field can be set to TRUE to enable parity checking.
The fOutxCtsFlow field should be set to TRUE if the output of the serial port should be controlled by the port CTS line. The fOutxDsrFlow field should be set to TRUE if the output of the serial port should be controlled by the DSR line of the serial port. The fDtrControl field can be set to one of three values: DTR_CONTROL_DISABLE, which disables the DTR (Data Terminal Ready) line and leaves it disabled; DTR_CONTROL_ENABLE, which enables the DTR line; or DTR_CONTROL_HANDSHAKE, which tells the serial driver to toggle the DTR line in response to how much data is in the receive buffer.The fDsrSensitivity field is set to TRUE, and the serial port ignores any incoming bytes unless the port DSR line is enabled. Setting the fTXContinueOnXoff field to TRUE tells the driver to stop transmitting characters if its receive buffer has reached its limit and the driver has transmitted an XOFF character. Setting the fOutX field to TRUE specifies that the XON/XOFF control is used to control the serial output. Setting the fInX field to TRUE specifies that the XON/XOFF control is used for the input serial stream.The fErrorChar and ErrorChar fields are ignored by the default implementation of the Windows CE serial driver, although some drivers might support these fields. Likewise, the fAbortOnError field is also ignored. Setting the fNull field to TRUE tells the serial driver to discard null bytes received.The fRtsControl field specifies the operation of the RTS (Request to Send) line. The field can be set to one of the following: RTS_CONTROL_DISABLE, indicating that the RTS line is set to the disabled state while the port is open; RTS_CONTROL_ENABLE, indicating that the RTS line is set to the enabled state while the port is open; or RTS_CONTROL_HANDSHAKE, indicating that the RTS line is controlled by the driver. In this mode, the RTS line is enabled if the serial input buffer is less than half full; it's disabled otherwise. Finally, RTS_CONTROL_TOGGLE indicates that the driver enables the RTS line if there are bytes in the output buffer ready to be transmitted and disables the line otherwise.The XonLim field specifies the minimum number of bytes in the input buffer before an XON character is automatically sent. The XoffLim field specifies the maximum number of bytes in the input buffer before the XOFF character is sent. This limit value is computed by taking the size of the input buffer and subtracting the value in XoffLim. In the sample Windows CE implementation of the serial driver provided in the Platform Builder, the XonLim field is ignored and XON and XOFF characters are sent based on the value in XoffLim. However, this behavior might differ in some systems.The next three fields, ByteSize, Parity, and StopBits, define the format of the serial data word transmitted. The ByteSize field specifies the number of bits per byte, usually a value of 7 or 8, but in some older modes the number of bits per byte can be as small as 5. The Parity field can be set to the self-explanatory constant EVENPARITY, MARKPARITY, NOPARITY, ODDPARITY, or SPACEPARITY. The StopBits field should be set to ONESTOPBIT, ONE5STOPBITS, or TWOSTOPBITS, depending on whether you want one, one and a half, or two stop bits per byte.
The next two fields, XonChar and XoffChar, let you specify the XON and XOFF characters. Likewise, the EvtChar field lets you specify the character used to signal an event. If an event character is received, an EV_RXFLAG event is signaled by the driver. This "event" is what triggers the WaitCommEvent function to return if the EV_RXFLAG bit is set in the event mask.
Setting the Port Timeout Values
As you can see, SetCommState can fine-tune, to almost the smallest detail, the operation of the serial driver. However, one more step is necessary—setting the timeout values for the port. The timeout is the length of time Windows CE waits on a read or write operation before ReadFile or WriteFile automatically returns. The functions that control the serial timeouts are the following:
BOOL GetCommTimeouts (HANDLE hFile, LPCOMMTIMEOUTS lpCommTimeouts);
and
BOOL SetCommTimeouts (HANDLE hFile, LPCOMMTIMEOUTS lpCommTimeouts);
Both functions take the handle to the open serial device and a pointer to a COMMTIMEOUTS structure, defined as the following:
typedef struct _COMMTIMEOUTS {
DWORD ReadIntervalTimeout;
DWORD ReadTotalTimeoutMultiplier;
DWORD ReadTotalTimeoutConstant;
DWORD WriteTotalTimeoutMultiplier;
DWORD WriteTotalTimeoutConstant;
} COMMTIMEOUTS;
The COMMTIMEOUTS structure provides for a set of timeout parameters that time both the interval between characters and the total time to read and write a block of characters. Timeouts are computed in two ways. First ReadIntervalTimeout specifies the maximum interval between characters received. If this time is exceeded, the ReadFile call returns immediately. The other timeout is based on the number of characters you're waiting to receive. The value in ReadTotalTimeoutMultiplier is multiplied by the number of characters requested in the call to ReadFile and is added to ReadTotalTimeoutConstant to compute a total timeout for a call to ReadFile.The write timeout can be specified only for the total time spent during the WriteFile call. This timeout is computed the same way as the total read timeout, by specifying a multiplier value, the time in WriteTotalTimeoutMultiplier, and a constant value in WriteTotalTimeoutConstant. All of the times in this structure are specified in milliseconds.
In addition to the basic timeouts that I just described, you can set values in the COMMTIMEOUTS structure to control whether and exactly how timeouts are used in calls to ReadFile and WriteFile. You can configure the timeouts in the following ways:
Timeouts for reading and writing as well as an interval timeout. Set the fields in the COMMTIMEOUTS structure for the appropriate timeout values.
Timeouts for reading and writing with no interval timeout. Set ReadIntervalTimeout to 0. Set the other fields for the appropriate timeout values.
The ReadFile function returns immediately regardless of whether there is data to be read. Set ReadIntervalTimeout to MAXDWORD. Set ReadTotalTimeoutMultiplier and ReadTotalTimeoutConstant to 0.
ReadFile doesn't have a timeout. The function doesn't return until the proper number of bytes is returned or an error occurs. Set the ReadIntervalTimeout, ReadTotalTimeoutMultiplier, and ReadTotalTimeoutConstant fields to 0.
WriteFile doesn't have a timeout. Set WriteTotalTimeoutMultiplier and WriteTotalTimeoutConstant to 0.
The timeout values are important because the worst thing you can do is to spin in a loop waiting on characters from the serial port. While the calls to ReadFile and WriteFile are waiting on the serial port, the calling threads are efficiently blocked on an event object internal to the driver. This saves precious CPU and battery power during the serial transmit and receive operations. Of course, to block on ReadFile and WriteFile, you'll have to create secondary threads because you can't have your primary thread blocked waiting on the serial port.Another call isn't quite as useful—SetupComm, prototyped this way:
BOOL SetupComm (HANDLE hFile, DWORD dwInQueue, DWORD dwOutQueue);
This function lets you specify the size of the input and output buffers for the driver. However, the sizes passed in SetupComm are only recommendations, not requirements to the serial driver. For example, the example implementation of the serial driver in the Platform Builder ignores these recommended buffer sizes.
Querying the Capabilities of the Serial Driver
The configuration functions enable you to configure the serial driver, but with varied implementations of serial ports, you need to know just what features a serial port supports before you configure it. The function GetCommProperties provides just this service. The function is prototyped this way:
BOOL GetCommProperties (HANDLE hFile, LPCOMMPROP lpCommProp);
GetCommProperties takes two parameters: the handle to the opened serial driver and a pointer to a COMMPROP structure defined as
typedef struct _COMMPROP {
WORD wPacketLength;
WORD wPacketVersion;
DWORD dwServiceMask;
DWORD dwReserved1;
DWORD dwMaxTxQueue;
DWORD dwMaxRxQueue;
DWORD dwMaxBaud;
DWORD dwProvSubType;
DWORD dwProvCapabilities;
DWORD dwSettableParams;
DWORD dwSettableBaud;
WORD wSettableData;
WORD wSettableStopParity;
DWORD dwCurrentTxQueue;
DWORD dwCurrentRxQueue;
DWORD dwProvSpec1;
DWORD dwProvSpec2;
WCHAR wcProvChar[1];
} COMMPROP;
As you can see from the fields of the COMMPROP structure, GetCommProperties returns generally enough information to determine the capabilities of the device. Of immediate interest to speed demons is the dwMaxBaud field that indicates the maximum baud rate of the serial port. The dwSettableBaud field contains bit flags that indicate the allowable baud rates for the port. Both these fields use bit flags that are defined in WinBase.h. These constants are expressed as BAUD_xxxx, as in BAUD_19200, which indicates that the port is capable of a speed of 19.2 kbps. Note that these constants are not the constants used to set the speed of the serial port in the DCB structure. Those constants are numbers, not bit flags. To set the speed of a COM port in the DCB structure to 19.2 kbps, you would use the constant CBR_19200 in the BaudRate field of the DCB structure.
Starting back at the top of the structure are the wPacketLength and wPacketVersion fields. These fields allow you to request more information from the driver than is supported by the generic call. The dwServiceMask field indicates what services the port supports. The only service currently supported is SP_SERIALCOMM, indicating that the port is a serial communication port.The dwMaxTxQueue and dwMaxRxQueue fields indicate the maximum size of the output and input buffers internal to the driver. The value 0 in these fields indicates that you'll encounter no limit in the size of the internal queues. The dwCurrentTxQueue and dwCurrentRxQueue fields indicate the current size for the queues. These fields are 0 if the queue size can't be determined.The dwProvSubType field contains flags that indicate the type of serial port supported by the driver. Values here include PST_RS232, PST_RS422, and PST_RS423, indicating the physical layer protocol of the port. PST_MODEM indicates a modem device, and PST_FAX tells you the port is a fax device. Other PST_ flags are defined as well. This field reports what the driver thinks the port is, not what device is attached to the port. For example, if an external modem is attached to a standard, RS-232, serial port, the driver returns the PST_RS232 flag, not the PST_MODEM flag.The dwProvCapabilities field contains flags indicating the handshaking the port supports, such as XON/XOFF, RTS/CTS, and DTR/DSR. This field also shows you whether the port supports setting the characters used for XON/XOFF, parity checking, and so forth. The dwSettableParams, dwSettableData, and dwSettableStopParity fields give you information about how the serial data stream can be configured. Finally, the fields dwProvSpec1, dwProvSpec2, and wcProvChar are used by the driver to return driver-specific data.
Controlling the Serial Port
You can stop and start a serial stream using the following functions:
BOOL SetCommBreak (HANDLE hFile);
and
BOOL ClearCommBreak (HANDLE hFile);
The only parameter for both these functions is the handle to the opened COM port. When SetCommBreak is called, the COM port stops transmitting characters and places the port in a break state. Communication is resumed with the ClearCommBreak function.You can clear out any characters in either the transmit or the receive queue internal to the serial driver using this function:
BOOL PurgeComm (HANDLE hFile, DWORD dwFlags);
The dwFlags parameter can be a combination of the flags PURGE_TXCLEAR and PURGE_RXCLEAR. These flags terminate any pending writes and reads and reset the queues. In the case of PURGE_RXCLEAR, the driver also clears any receive holds due to any flow control states, transmitting an XON character if necessary, and setting RTS and DTR if those flow control methods are enabled. Because Windows CE doesn't support overlapped I/O, the flags PURGE_TXABORT and PURGE_RXABORT, used under Windows XP and Windows Me, are ignored.The EscapeCommFunction provides a more general method of controlling the serial driver. It allows you to set and clear the state of specific signals on the port. On Windows CE devices, it's also used to control serial hardware that's shared between the serial port and the IrDA port. The function is prototyped as
BOOL EscapeCommFunction (HANDLE hFile, DWORD dwFunc);
The function takes two parameters, the handle to the device and a set of flags in dwFunc. The flags can be one of the following values:
SETDTRSets the DTR signal
CLRDTRClears the DTR signal
SETRTSSets the RTS signal
CLRRTSClears the RTS signal
SETXOFFTells the driver to act as if an XOFF character has been received
SETXONTells the driver to act as if an XON character has been received
SETBREAKSuspends serial transmission and sets the port in a break state
CLRBREAKResumes serial transmission from a break state
SETIRTells the serial port to transmit and receive through the infrared transceiver
CLRIRTells the serial port to transmit and receive through the standard serial transceiver
The SETBREAK and CLRBREAK commands act identically to SetCommBreak and ClearCommBreak and can be used interchangeably. For example, you can use EscapeCommFunction to put the port in a break state and ClearCommBreak to restore communication.
Clearing Errors and Querying Status
The function
BOOL ClearCommError (HANDLE hFile, LPDWORD lpErrors, LPCOMSTAT lpStat);
performs two functions. As you might expect from the name, it clears any error states within the driver so that I/O can continue. The serial device driver is responsible for reporting the errors. The default serial driver returns the following flags in the variable pointed to by lpErrors: CE_OVERRUN, CE_RXPARITY, CE_FRAME, and CE_TXFULL. ClearCommError also returns the status of the port. The third parameter of ClearCommError is a pointer to a COMSTAT structure defined as
typedef struct _COMSTAT {
DWORD fCtsHold : 1;
DWORD fDsrHold : 1;
DWORD fRlsdHold : 1;
DWORD fXoffHold : 1;
DWORD fXoffSent : 1;
DWORD fEof : 1;
DWORD fTxim : 1;
DWORD fReserved : 25;
DWORD cbInQue;
DWORD cbOutQue;
} COMSTAT;
The first five fields indicate that serial transmission is waiting for one of the following reasons. It's waiting for a CTS signal, waiting for a DSR signal, waiting for a Receive Line Signal Detect (also known as a Carrier Detect), waiting because an XOFF character was received, or waiting because an XOFF character was sent by the driver. The fEor field indicates that an end-of-file character has been received. The fTxim field is TRUE if a character placed in the queue by the TransmitCommChar function instead of a call to WriteFile is queued for transmission. The final two fields, cbInQue and cbOutQue, return the number of characters in the input and output queues of the serial driver.The function
BOOL GetCommModemStatus (HANDLE hFile, LPDWORD lpModemStat);
returns the status of the modem control signals in the variable pointed to by lpModemStat. The flags returned can be any of the following:
MS_CTS_ONClear to Send (CTS) is active.
MS_DSR_ONData Set Ready (DSR) is active.
MS_RING_ONRing Indicate (RI) is active.
MS_RLSD_ONReceive Line Signal Detect (RLSD) is active.
Stayin' Alive
One of the Chapter 21.