65.9K
CodeProject 正在变化。 阅读更多。
Home

简单的串行端口监视器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (35投票s)

2012年1月6日

CPOL

9分钟阅读

viewsIcon

256720

downloadIcon

52808

非常简单的串口监视器。

引言

首先,请原谅我的英语,因为它不是我的母语,这也是我的第一篇文章。在这篇文章中,我想分享我所知道的关于如何监视串口的知识。请注意,这仅仅是我“所知道的”,并且我可能在驱动程序编程方面有所误解,尤其是在这篇文章中。如果您发现我错了,请告诉我,我们可以进一步讨论。

那么,什么是串口监视器呢?嗯,我相信您知道它是什么。这个串口监视器的基本思想是:创建一个系统驱动程序,然后为其添加过滤驱动程序的功能。好了,让我们开始详细介绍。

I. 系统驱动程序

正如您在源代码中看到的,这只是一个系统驱动程序(没有实际硬件),并且它实现了系统驱动程序的最小调度函数。如果您想了解系统驱动程序的要求,请查阅 MSDN。在这个驱动程序中,我简单地将发送到此驱动程序的 IRP 转发到低层驱动程序作为默认处理程序,并使用 WDK 建议的“标准 PnP 和电源调度处理”。此驱动程序还处理打开、清理、关闭、读取和控制请求,并根据 WDK(Windows 驱动程序工具包)的要求处理一些作为串口驱动程序的 IRP 请求。

II. 附加到目标设备并从目标设备分离

当客户端应用程序发送一个 I/O 控制请求以附加到目标设备时,使用一个串口名称的字符串参数执行 IOCTL_DKPORTMON_ATTACH_DEVICE,驱动程序会执行以下操作:

  1. 驱动程序使用 IOCTL_DKPORTMON_ATTACH_DEVICE 请求中的字符串参数,通过 IoGetDeviceObjectPointer() 获取目标设备对象的最顶层。如果成功,此例程将填充我们提供的设备对象变量的指针。
  2. 然后,驱动程序创建一个新的设备对象,该对象具有从 IoGetDeviceObjectPointer() 获取的设备对象特征,并且设备扩展的大小为 0。
  3. 之后,驱动程序从 IoGetDeviceObjectPointer() 创建的设备对象中复制标志,并添加任何“附加标志”。
  4. 使用 IoAttachDeviceToDeviceStack() 函数将新创建的设备对象附加到设备堆栈,然后设置初始化标志。

附加设备的详细代码(您可以在文件 DkIoExt.cDkCreateAndAttachDevice() 函数中看到)。

...
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;
...

当客户端应用程序发送 IOCTL_DKPORTMON_DETACH_DEVICE 请求时,驱动程序将从目标设备分离,并删除之前创建的设备对象,如下面的代码片段所示。

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

III. 处理 I/O 请求

附加到目标设备后,此驱动程序不仅会接收发往此驱动程序的 I/O 请求,还会接收发往我们附加到的目标设备对象的 I/O 请求。这意味着,如果某个应用程序发送一个针对我们附加到的设备的 I/O 请求,它将首先发送到我们的驱动程序,因为我们的驱动程序位于目标设备堆栈的顶部,在这种情况下,我们的目标设备就是串口驱动程序。我们如何知道请求是“发往”我们的设备对象还是不是呢?嗯,最简单的方法是声明一个我们的设备对象的全局变量,在 DkAddDevice() 例程中创建此对象,然后在收到 I/O 请求时,执行一个简单的“if”判断,如下所示:

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

III. A. 处理发往我们的设备对象的 I/O 请求

在进一步讨论处理请求之前,我想简单谈谈这个驱动程序中的队列。这个驱动程序使用两种类型的队列,一种用于处理 IRP(如 WDK 所建议的取消安全队列),另一种用于收集数据(简单的先进先出数据队列/FIFO 数据队列)。我们将在下一节讨论如何收集数据。

我们的驱动程序处理打开(IRP_MJ_CREATE)、关闭(IRP_MJ_CLOSE)、清理(IRP_MJ_CLEANUP)、读取(IRP_MJ_READ)和控制(IOCTL_DKPORTMON_ATTACH_DEVICEIOCTL_DKPORTMON_DEATCH_DEVICE)请求。正如您在源代码中看到的,打开、关闭和清理请求在同一个调度例程 DkCreateClose() 中处理。对于打开请求,我们只初始化我们的 FIFO 数据队列,以 STATUS_SUCCESS 完成请求,并返回 STATUS_SUCCESS。对于清理请求,我们分离设备(如果存在,如分离函数所述),清理数据队列和取消安全队列,然后完成请求。对于关闭请求,它只是“接受”它,完成请求,并返回 STATUS_SUCCESS

当从客户端应用程序接收到读取请求时,我们从 FIFO 数据队列中检索数据。如果有数据,我们将其复制到“代表”用户缓冲区的系统缓冲区中,然后将其移除/销毁/删除/释放,最后以 STATUS_SUCCESS 完成请求,并包含从 FIFO 数据队列中获取的数据大小。如果 FIFO 数据队列中没有数据,我们则将 IRP 入队到取消安全队列,然后返回一个挂起状态,并指示该 IRP 已入队,稍后将由驱动程序中的另一个函数(DkTgtCompletePendedIrp() 函数)完成。这是文件 DkIoReq.cDkReadWrite() 函数中的代码片段。

...
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;
}
...

对于控制请求,请参阅上面 **II. 附加到目标设备并从目标设备分离** 的子节。

III. B. 处理发往我们的目标设备对象的 I/O 请求

当 I/O 请求的目标是目标设备时,基本上就像普通的过滤驱动程序一样,将其转发到下一个较低的设备对象。但是为了监控目的,我们需要收集到设备或从设备收集数据以进行进一步分析。问题是,何时可以收集到数据?是在转发请求之前还是之后?首先,我们需要知道“数据的方向”。换句话说,我们需要知道这种请求是“GET 请求”还是“PUT 请求”。在“GET 请求”(例如,IRP_MJ_READIOCTL_SERIAL_GET_BAUD_RATE 等)中,数据来自目标驱动程序,这意味着我们可以在目标驱动程序完成请求后收集数据。所以,如果我们收到这类请求,我们会为该请求设置一个完成例程。此完成例程将在较低的下一个驱动程序完成其工作后执行,这样我们就可以收集数据。正如您在源代码(文件 DkIoReq.c)中看到的,当此驱动程序收到发往目标设备的 IRP_MJ_READ 请求时,该请求由 DkTgtReadWrite() 处理,它会设置一个完成例程,然后转发到下一个对象,就像这个代码片段一样。

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

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

而在完成例程中,我们可以通过 DkTgtCompletePendedIrp() 函数收集数据,如下所示。

...
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);
}
...

对于其他请求,例如发往目标驱动程序的 I/O 控制请求,使用与上面相同的方法,关键是:注意“数据的方向”。

IV. 客户端程序

客户端程序打开设备驱动程序然后开始监视。它通过在另一个线程循环中读取来捕获数据。当数据到来时,它会“触发一个事件”,该事件会调用“与之连接”的函数。基本上,它只是一个函数指针,我们为其分配一个函数。您可以查看文件 DkPortClient.hDkPortClient.cpp 中的 C++ 类 CDkPortClient。要开始监视,它向驱动程序发送 IOCTL_DKPORTMON_ATTACH_DEVICE;要停止监视,它发送 IOCTL_DKPORTMON_DETACH_DEVICE

V. 缺点。

以下是我能想到的缺点:

  1. 当您开始监视时,目标驱动程序或串口不能被另一个应用程序使用(打开)。因此,这个串口监视器必须在任何应用程序(如超级终端)开始访问目标端口之前启动。这是因为串口驱动程序一次只能打开一次。
  2. 它不能保证 I/O 请求的顺序。这是什么意思?嗯,考虑这种情况:某些应用程序以重叠标志打开端口,然后异步读取端口,然后写入端口。之后,应用程序等待来自先前读取请求的传入数据,然后数据到来。在这种情况下,您将在监视器上看到的请求顺序可能如下所示:
  3. ...
    IRP_MJ_CREATE
    ...
    IRP_MJ_WRITE
    ...
    IRP_MJ_READ
    ...

    如您所见,IRP_MJ_WRITE 出现在 IRP_MJ_CREATE 之后。这是因为此端口监视器不监视“IRP 状态”。它在驱动程序转发请求之前/之后收集数据,正如我们在前一节中讨论的关于收集数据的内容。

  4. 此驱动程序不检查请求的状态,无论它们成功还是失败。

VI. 安装和使用

此驱动程序仅支持 Windows XP x86。如果您在 Windows XP 以外的其他平台上使用它,则需要根据目标平台重新编译。因此,以下过程仅适用于 Windows XP。

IV. A. 安装

驱动程序安装步骤

  1. 在“控制面板”中,双击“添加硬件”,然后单击“下一步”,这将显示“正在搜索硬件”对话框。
  2. 在“正在搜索硬件”对话框完成后,单击“是,我已经连接了硬件”单选按钮,然后单击“下一步”。
  3. 在“已安装的硬件”列表框中,向下滚动并选择/单击“添加新硬件设备”,然后单击“下一步”。
  4. 选择/单击“手动从列表中选择要安装的硬件(高级)”单选按钮,然后单击“下一步”。
  5. 在“常见硬件类型”列表框中,选择/单击“系统设备”,然后单击“下一步”。
  6. 单击“从磁盘开始”按钮,然后找到文件 DkPortMon2.inf,然后单击“下一步”。
  7. 再次单击“下一步”以完成驱动程序安装。

如果驱动程序安装成功,设备管理器应该如下所示:

SERIAL_PORT/PortMonInst.jpg

VI. B. 使用。

只需运行 Bin 目录中的名为“DkPortMonGui.exe”的程序。首先,您需要选择要监视的端口,单击工具 - 选择端口,选择它,然后单击工具 - 开始以开始监视端口。如果您想查看驱动程序的调试消息,请使用 SysInternals 的 DbgView。

SERIAL_PORT/PortMonGui.jpg

VII. 编译和链接源代码

源代码分为两部分:驱动程序源代码本身和客户端程序。安装 WDK,然后单击开始 - WDK xxx-xxxxxx-x - Build Environments - Windows XP - x86 Checked Build Environment。这将为您提供一个用于构建此源代码的命令提示符环境(x86 机器的检查构建环境/调试构建)。进入每个目录,然后键入build -cegZ 来编译和链接源代码。此方法适用于具有“调试构建”环境的 Windows XP。

已修复错误  

  1. 2012 年 4 月 26 日,在 DkGui.cpp 文件中的 DkPortOnDataReceived() 函数中,将 *pTmp++ = HexWChar[g_DkPortClient.m_Dat.Data[ul] && 0xF ]; 更改为 *pTmp++ = HexWChar[g_DkPortClient.m_Dat.Data[ul] & 0xF ];

© . All rights reserved.