Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / Win32

Simple Serial Port Monitor

4.94/5 (35 votes)
1 May 2012CPOL9 min read 253.9K   52.8K  
Very simple serial port monitor.

Introduction

First of all excuse my English since it is not my native language and this is my first article. In this article, I'd like to share "what I know" about how to monitor serial ports. Please note that this is about "what I know" and I could be wrong about what I understand about driver programming, specially in this article. If you find out that I am wrong, please let me know and we can discuss it further.

So, what is a serial port monitor? Well, I believe you know what it is. The basic idea of this serial port monitor is: create a system driver and then add the filter driver functionality to it. Okay, let's get on to the detail.

I. System driver

As you can see in the source, this is just a system driver (without real hardware) and it implements minimal dispatch functions for the system driver. If you want to see the requirements of a system driver, please take a look at MSDN. In this driver, I simply forward an IRP sent to this driver to the lower level driver as the default handler and use "standard PnP and Power dispatch handling" as the WDK suggest. This driver also handles open, clean up, close, read, and control request, plus handles some requests as a serial port driver IRP handler requirement in WDK (Window Driver Kits).

II. Attach to and detach from target device

When a client application sends an IO control request to attach to a target device, IOCTL_DKPORTMON_ATTACH_DEVICE with a string parameter of the serial port name, the driver does this:  

  1. Driver gets the top of the target device object identified by the string parameter in the IOCTL_DKPORTMON_ATTACH_DEVICE request with IoGetDeviceObjectPointer(). This routine will fill a pointer to the device object variable we provide if successful.
  2. The driver then creates a new device object characteristic of the device object we get from IoGetDeviceObjectPointer() and the 0 size of the device extension.
  3. After that, the driver copies the flags from the device object created by IoGetDeviceObjectPointer() and puts some "additional flags" if any.
  4. Attaches to the device object we just created with the IoAttachDeviceToDeviceStack() function and then sets up initialization flags.

And the code for attaching device (you can see the details in the function DkCreateAndAttachDevice() in the file DkIoExt.c).

C++
...
RtlInitUnicodeString(&usTgtDevName, 
                         (PCWSTR) pIrp->AssociatedIrp.SystemBuffer);

ntStat = IoGetDeviceObjectPointer(&usTgtDevName, 
                                      GENERIC_ALL, 
                                      &pFlObj, 
                                      &pTgtDevObj);
if (!NT_SUCCESS(ntStat)){
    DkDbgVal("Error get device object pointer!", ntStat);
    return ntStat;
}

ObDereferenceObject(pFlObj);

ntStat = IoCreateDevice(pDevEx->pDrvObj, 
                            0, 
                            NULL, 
                    pTgtDevObj->Characteristics, 
                            FILE_DEVICE_SECURE_OPEN, 
                            FALSE, 
                            &pDevEx->pTgtDevObj);
if (!NT_SUCCESS(ntStat)){
    DkDbgVal("Error create target device object!", ntStat);
    goto EndFunc;
}

pDevEx->pTgtDevObj->Flags |= (pTgtDevObj->Flags & 
              (DO_BUFFERED_IO | DO_POWER_PAGABLE | DO_DIRECT_IO));

pDevEx->pTgtNextDevObj = NULL;
pDevEx->pTgtNextDevObj = IoAttachDeviceToDeviceStack(pDevEx->pTgtDevObj, 
                                                         pTgtDevObj);
if (pDevEx->pTgtNextDevObj == NULL){
    DkDbgVal("Error attach device to device stack!", ntStat);
    ntStat = STATUS_UNSUCCESSFUL;
    goto EndFunc;
}

pDevEx->pTgtDevObj->Flags &= ~DO_DEVICE_INITIALIZING;
...

When the client application sends a IOCTL_DKPORTMON_DETACH_DEVICE request, the driver detaches from the target device and deletes the device object we created before, as in the code fragment below:

C++
...
if (pDevEx->pTgtNextDevObj){
    IoDetachDevice(pDevEx->pTgtNextDevObj);
    pDevEx->pTgtNextDevObj = NULL;
}
if (pDevEx->pTgtDevObj){
    IoDeleteDevice(pDevEx->pTgtDevObj);
    pDevEx->pTgtDevObj = NULL;
}
...

III. Handling an IO request

After attaching to the target device, this driver will receive an IO request not only for this driver, but it will also receive an IO request to the target device object we attached to. Which means, if some application sends an IO request for the device we attached to, it will send this request to our driver first, because our driver is in the top of the stack of the target device, in this case the serial port driver as our target device. How can we tell that the request is "addressed" to our device object or not? Well, the simplest way is to state a global variable of our device object, create this object in the DkAddDevice() routine, and then when we receive an IO request, we do a simple "if" like this:

C++
...
extern PDEVICE_OBJECT        g_pThisDevObj;
...
NTSTATUS DkCreateClose(PDEVICE_OBJECT pDevObj, PIRP pIrp)
{
...
    if (pDevObj != g_pThisDevObj)
        return DkTgtCreateClose(pDevExt, pIrp);
...

III. A. Handling an IO request that is coming to our device object

Before we discuss further about handling requests, I'd like to say a little bit about the queue in this driver. This driver uses two kinds of queues, one for handling an IRP (Cancel-Safe queue as WDK suggested) and another for collecting data (simple First In First Out data queue / FIFO data queue). We discuss how we collect data later in the next section.

Our driver handles open (IRP_MJ_CREATE), close (IRP_MJ_CLOSE), clean up (IRP_MJ_CLEANUP), read (IRP_MJ_READ), and control (IOCTL_DKPORTMON_ATTACH_DEVICE and IOCTL_DKPORTMON_DEATCH_DEVICE) requests. As you can see in the source, open, close, and clean up requests are handled in the same dispatch routine, that is DkCreateClose(). For open request, we just initialize our FIFO data queue, complete the request with STATUS_SUCCESS, and returns STATUS_SUCCESS. For clean up request, we detach the device (if any, as the detach function state), clean the data queue and Cancel-Safe queue, and then complete the request. For close request, it just "accepts" it, completes the request, and returns STATUS_SUCCESS.

When we receive a read request from the client application program, we retrieve data from FIFO data queue. If there is data, we copy it to the system buffer which "represents" the user buffer, and then remove/destroy/delete/free it, and then complete the request with STATUS_SUCCESS and with the size of data we get from the FIFO data queue. If there is no data present in the FIFO data queue, we queue the IRP to Cancel-Safe queue then return a pending status, and indicate that the IRP is queued and will be completed later by another function in this driver (DkTgtCompletePendedIrp() function). This is the code fragment in file DkIoReq.c, in function DkReadWrite():

C++
...
pQueDat = DkQueGet();
if (pQueDat == NULL){

    IoCsqInsertIrp(&pDevExt->ioCsq, pIrp, NULL);
    IoReleaseRemoveLock(&pDevExt->ioRemLock, (PVOID) pIrp);
    return STATUS_PENDING;

} else {
    pDat = (PDKPORT_DAT) pIrp->AssociatedIrp.SystemBuffer;            
    RtlCopyMemory(pDat, &pQueDat->Dat, sizeof(DKPORT_DAT));        
            
    DkQueDel(pQueDat);

    IoReleaseRemoveLock(&pDevExt->ioRemLock, (PVOID) pIrp);
                
    DkCompleteRequest(pIrp, ntStat, (ULONG_PTR) sizeof(DKPORT_DAT));
                
    return ntStat;
}
...

For control request, please read the sub section II. Attach to and detach from target device above.

III. B. Handling an IO request that is coming to our target device object

When an IO request is addressed for the target device, basically just forward it to the next lower device object, as a regular filter driver does. But for monitoring purposes, we need to collect data to/from device to be analyzed further. The question is when is the data available to collect? Before or after we forward the request? First, we need to know "the direction of data". In other words, we need to know if this kind of request is a "GET request" or a "PUT request". In a "GET request" (for example, IRP_MJ_READ, IOCTL_SERIAL_GET_BAUD_RATE, and so on) data comes from a target driver, which means we can collect the data after the target driver completes the request. So if we receive this kind of request, we set a completion routine for this request. This completion routine will be executed after the lower next driver has done whatever it is, so we can collect data. As you can see in the source (file DkIoReq.c), when this driver receives an IRP_MJ_READ to a target device, which is handled by DkTgtReadWrite(), it sets a completion routine and then forwards to the next object, like in this code fragment:

C++
...
IoCopyCurrentIrpStackLocationToNext(pIrp);
IoSetCompletionRoutine(pIrp, 
              (PIO_COMPLETION_ROUTINE) DkTgtReadCompletion, 
                           NULL, 
                           TRUE, 
                           TRUE, 
                           TRUE);

return IoCallDriver(pDevExt->pTgtNextDevObj, pIrp);
...

And in the completion routine, we can collect the data via the DkTgtCompletePendedIrp() function, like this:

C++
...
pDevExt = (PDEVICE_EXTENSION) g_pThisDevObj->DeviceExtension;

pIrp = IoCsqRemoveNextIrp(&pDevExt->ioCsq, NULL);
if (pIrp == NULL){

    DkQueAdd(szFuncName, ulFuncNameByteLen, pDat, ulDatByteLen);

} else {

    pNewDat = (PDKPORT_DAT) pIrp->AssociatedIrp.SystemBuffer;
    RtlFillMemory(pNewDat, sizeof(DKPORT_DAT), '\0');
    pNewDat->FuncNameLen = ulFuncNameByteLen;
    pNewDat->DataLen = ulDatByteLen;
    if (szFuncName != NULL){
        RtlCopyMemory(pNewDat->StrFuncName, szFuncName, ulFuncNameByteLen);
    }
    if (pDat != NULL){
        RtlCopyMemory(pNewDat->Data, pDat, ulDatByteLen);
    }

    pIrp->IoStatus.Status = STATUS_SUCCESS;
    pIrp->IoStatus.Information = sizeof(DKPORT_DAT);
    IoCompleteRequest(pIrp, IO_NO_INCREMENT);
}
...

For other requests like IO control request for a target driver, use the same method as described above, the point is: pay attention to "the direction of data".

IV. Client program

The client program opens the device driver and then starts monitoring. It captures the data by reading in another thread loop. When data comes, it "triggers an event" and this event calls the function that is "connected to it". Basically it's just a pointer to a function and we assign a function to it. You can look at the C++ class CDkPortClient in the files DkPortClient.h and DkPortClient.cpp. To start monitoring, it sends IOCTL_DKPORTMON_ATTACH_DEVICE to the driver, and to stop monitoring, it sends IOCTL_DKPORTMON_DETACH_DEVICE.

V. The weaknesses.

These are the weaknesses that I could think of:

  1. The target driver or serial port must not be used (open) by another application when you start to monitor, so this serial port monitor must start first before any application, like hyperterminal, starts to access the target port. This is because the serial driver can only be opened once a time.
  2. It does not guarantee an I/O request sequence. What does this mean? Well, consider this situation: some applications open the port with an overlapped flag and then reads the port asynchronously, and then writes to the port. After that, the application waits for incoming data from the previous read request and then data comes. The request sequence you'll see on the monitor in this situation may look like this:
  3. ...
    IRP_MJ_CREATE
    ...
    IRP_MJ_WRITE
    ...
    IRP_MJ_READ
    ...

    As you can see, IRP_MJ_WRITE comes just after IRP_MJ_CREATE. This is because this port monitor does not monitor "IRP state". It collects data after/before the driver forwards the request, as we discussed in the previous subsection about collecting data above.

  4. This driver doesn't check the status of the request whether they succeed or fail.

VI. Installation and usage

This driver only supports Windows XP x86. If you use this on other platform than Windows XP, you need to recompile it under the target you want. So this procedure below is only valid for Windows XP.

IV. A. Installation

Step by step driver installation:

  1. In Control Panel, double click Add Hardware and then click Next, this will show the "Searching hardware" dialog box.
  2. After the "Searching hardware" dialog box finishes, click the radio button "Yes, I have already connected the hardware", and then click Next.
  3. In the Installed Hardware list box, scroll down and choose/click "Add a new hardware device", and then click Next.
  4. Choose/click the "Install the hardware that I manually select from a list (Advanced)" radio button and then click Next.
  5. In the "Common Hardware Type" list box, choose/click "System Devices" and then click Next.
  6. Click the "Have Disk" button and then locate the file DkPortMon2.inf and then click Next.
  7. Click Next again to finish driver installation.

If the driver has installed successfully then the Device Manager should look like this:

SERIAL_PORT/PortMonInst.jpg

VI. B. Usage.

Just run the program called "DkPortMonGui.exe" in the Bin directory. First, you need to select the port to monitor, by clicking Tool - Select Port, select it and then click Tool - Start to start monitoring the port. If you want to see some debugging messages from the driver, use DbgView from SysInternals.

SERIAL_PORT/PortMonGui.jpg

VII. Compilation and linking the source

The source consist of two parts, the driver source itself and the client program. Install WDK and then click Start - WDK xxx-xxxxxx-x - Build Environments - Windows XP - x86 Checked Build Environment. This will give you a command prompt environment (checked build environment / debug build for x86 machine) to build this source. Enter each directory and then type build -cegZ to compile and link the source. This method is valid for Windows XP with "Debug build" environment.  

Bug fixed  

  1. April, 26th 2012,  *pTmp++ = HexWChar[g_DkPortClient.m_Dat.Data[ul] && 0xF ];  change to  *pTmp++ = HexWChar[g_DkPortClient.m_Dat.Data[ul] & 0xF ];  in DkGui.cpp file, function DkPortOnDataReceived().  

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)