如何正确使用 COMMTIMEOUTS 和从串行端口读取的 OVERLAPPED IO 模式

Posted

技术标签:

【中文标题】如何正确使用 COMMTIMEOUTS 和从串行端口读取的 OVERLAPPED IO 模式【英文标题】:How do I correctly use COMMTIMEOUTS with OVERLAPPED IO mode reading from a Serial port 【发布时间】:2013-08-13 18:53:46 【问题描述】:

我正在尝试使用 Windows 7/8 X64 上的重叠 IO 模式来模拟 Linux 开放标志支持的非阻塞模式 (IO_NONBLOCK) 行为。这里的代码是跨平台串行 API 的 windows 部分的一部分。

我可以使用 SerialCommWnt 对象的构造函数参数以阻塞或非阻塞 (OVERLAPPED) 模式打开 COMM 端口。就这个问题而言,我的所有问题都与 COMM 端口何时以 OVERLAPPED 模式打开(由流控制构造函数参数指定)有关。对于 Read 方法,我指定了一个 timeout 参数,当成功从串行端口检索至少 1 个字节的数据时,它应该指示 rTimeout 参数在数据位于串行通信的输入缓冲区时的剩余时间(我相信串行驱动程序在收到任何数据时通知重叠结构中的手动重置事件。

我阅读了许多关于如何处理这些 API 的 *** 线程,其中许多都引用了 Microsoft Win32 API。到目前为止我能找到的最好的信息是

http://msdn.microsoft.com/en-us/library/ff802693.aspx 这个 API 对 Overlapped IO 来说是令人困惑的(特别是当它传递一个指向在重叠模式下调用 ReadFile 时接收到的字节数的指针时),但据我所知,它们都没有解决如何正确使用重叠 IO模式与 COMMTIMEOUTS 结合使用。在过去的几天里,我尝试了 COMMTIMEOUTS 和与 ::WaitForSingleObject 一起使用的超时参数的设置组合。最后显示了似乎最有效的组合。关于与重叠 IO 结构和 COMMTIMEOUTS 关联的手动重置事件对象相关的超时,我有一些可靠性问题。我不完全确定,但似乎为了在从串行端口读取时超时正常工作,必须在 COMMTIMEOUTS 中指定超时。我尝试了一种组合,在 SetCommTimeouts 中禁用超时,而是在 ::WaitForSingleObject 的超时参数中使用显式超时,但这不起作用,而是我做了相反,通过在 COMMTIMEOUTS 中指定超时并使用 ::WaitForSingleObject 方法调用指定 INFINITE。但是,我不确定是否存在这种情况会永远挂起,如果是这样,我该如何处理。我将不胜感激有关如何正确处理可能挂在这里的任何信息。

这是我用来打开 COMM 端口的方法 - 在这种情况下我有超时问题,我指定 FILE_FLAG_OVERLAPPED。

    /**
     * Open the serial port using parameters set in theconstructor.<p>
     * The Port Number, Speed, Overlapped IO mode, #data bits &
     * async mode etc. are specified as constructor arguments.
     *
     * @return OS_FAILED, OS_SUCCESS
     */
    OsStatus
    SerialCommWnt::open()
    
        // Critical Section
        std::lock_guard<std::recursive_mutex> lock (mMutexGuard);
        OsStatus result = OS_FAILED;
        std::ostringstream os;
        os << "\\\\.\\COM" << mCommPort;
        std::string deviceName = os.str();
        DWORD dwFlagsAndAttrs = (mFlowControl ==
            SerialCommBase::FCTL_OVERLAPPED)?
            FILE_FLAG_OVERLAPPED : 0;
        // open the underlying device for read and write
        mOsFileHandle = CreateFile (
            deviceName.c_str(),
            GENERIC_READ | GENERIC_WRITE,
            0,                      //(share) 0:cannot share the COM port
            NULL,                   // no security attributes
            OPEN_EXISTING,          // COMM devices must use OPEN_EXISTING
            dwFlagsAndAttrs,        // optional FILE_FLAG_OVERLAPPED
            NULL);                  // hTemplate must be NULL for comm devices
        if ( mOsFileHandle != INVALID_HANDLE_VALUE ) 
            // reserve an 8k communications channel buffer (both directions)
            BOOL isOK = SetupComm(mOsFileHandle, 8200, 8200);
            // Omit the call to SetupComm to use the default queue sizes.
            // Get the current configuration.
            DCB dcb;
            SecureZeroMemory(&dcb, sizeof(DCB));
            isOK = GetCommState (mOsFileHandle, &dcb);
            if (isOK) 
                // Fill in the DCB: baud=125000, 8 data bits, even parity, 1 stop bit.
                // This is the standard baud rate. The card we have has a custom crystal
                // changing this baud rate to 125K.
                dcb.BaudRate = static_cast<DWORD>(mBaudRate);
                dcb.ByteSize = static_cast<BYTE>(mByteSize);
                // enum values are ame as dcb.Parity defines
                dcb.Parity   = static_cast<BYTE>(mParity);
                dcb.fParity  = (mParity == SerialCommBase::PRTY_NONE)? FALSE : TRUE;
                dcb.StopBits = ONESTOPBIT;
                // ----------------------------------------------------
                // When running in win32 loopback with the simulator
                // in loopback mode, we must enable the RTS/CTS handshake
                // mode as there seems to be a 4K limit in the input
                // buffer when the DBU Simulator performs reads.
                // ----------------------------------------------------
                if (mFlowControl == SerialCommBase::FCTL_RTS_CTS) 
                    dcb.fOutxCtsFlow = 1;
                    dcb.fRtsControl = RTS_CONTROL_HANDSHAKE;
                
                // Not absolutely necessary as the DTR_CONTROL_DISABLE is default
                dcb.fDtrControl = DTR_CONTROL_DISABLE;
                isOK = SetCommState (mOsFileHandle, &dcb);
                if (isOK) 
                    COMMTIMEOUTS commTimeouts;
                    SecureZeroMemory(&commTimeouts, sizeof(COMMTIMEOUTS));
                    // These settings will cause ReadFile to return
                    // immediately if there is no data available at the port
                    // A value of MAXDWORD, combined with zero values for both the
                    // ReadTotalTimeoutConstant and ReadTotalTimeoutMultiplier members,
                    // specifies that the read operation is to return immediately with
                    // the bytes that have already been received, even if no bytes
                    // have been received.
                    //isOK = GetCommTimeouts (mOsFileHandle, &CommTimeouts);
                    commTimeouts.ReadIntervalTimeout = MAXDWORD;
                    // ReadTotalTimeoutConstant - when set with a ms timeout value
                    // in conjunction with will ReadIntervalTimeout == MAXDWORD &&
                    // ReadTotalTimeoutMultiplier set to 0 be used to control the
                    // timeout for the read operation.   Each time the read with a
                    // timeout is called, we compare the existing timeouts in CommTimeouts
                    // before changing it.
                    commTimeouts.ReadTotalTimeoutConstant = 0;
                    commTimeouts.ReadTotalTimeoutMultiplier = 0;
                    // timeouts not used for write operations
                    commTimeouts.WriteTotalTimeoutConstant = 0;
                    commTimeouts.WriteTotalTimeoutMultiplier = 0;
                    isOK = SetCommTimeouts (mOsFileHandle, &commTimeouts);
                    if (isOK) 
                        // test for asynchronous mode
                        if (mFlowControl == SerialCommBase::FCTL_OVERLAPPED) 
                            // allocate & initialize overlapped
                            // structure support for rx & tx
                            mpOverlappedTx.reset(new(OVERLAPPED));
                            mpOverlappedRx.reset(new(OVERLAPPED));
                            if (mpOverlappedTx && mpOverlappedRx) 
                                SecureZeroMemory(mpOverlappedTx.get(), sizeof(OVERLAPPED));
                                SecureZeroMemory(mpOverlappedRx.get(), sizeof(OVERLAPPED));
                                // create an unsignaled manual reset (2nd Param TRUE)
                                // event used for GetOverlappedResult. This event will
                                // be signaled by the ReadFile to indicate when
                                // IO operations are complete or encounter errors
                                mpOverlappedTx->hEvent = CreateEvent(
                                    NULL, TRUE, FALSE, NULL);
                                if (mpOverlappedTx->hEvent != NULL) 
                                    // now do the same for the RX side
                                    mpOverlappedRx->hEvent = CreateEvent(
                                        NULL, TRUE, FALSE, NULL);
                                    if (mpOverlappedRx->hEvent != NULL) 
                                        setState(COMM_OPENED);
                                        result = OS_SUCCESS;
                                     else 
                                        result = handleError(deviceName);
                                    
                                 else 
                                    result = handleError(deviceName);
                                
                                // close the handle and set error
                                if (result != OS_SUCCESS) 
                                    close();
                                    setState(COMM_OPEN_FAILED);
                                
                             else 
                                // close the handle and overlapped event
                                close();
                                setState(COMM_OPEN_FAILED);
                                result = OS_NO_MEMORY;
                            
                         else  // blocking mode
                            setState(COMM_OPENED);
                            result = OS_SUCCESS;
                        
                     else 
                        result = handleError(deviceName);
                        close();
                    
                 else  // unable to set the baud rate or something
                    result = handleError(deviceName);
                    close();
                
            
         else 
            result = handleError(deviceName);
            close();
        
        return result;
    

这是执行定时读取的代码

    /**
     * Read a block of data into the specified raw buffer.
     * See http://msdn.microsoft.com/en-us/library/ms810467(v=MSDN.10).aspx
     * for details for Overlapped IO usage, in particular note that setting
     * the timeout each time is tricky.
     *
     * @param pData     [in/out] data buffer
     * @param rNumBytes [in] buffer size
     * @param rTimeout  [in/out] timeout specified in milliseconds.
     *                  This parameter is updated to reflect the
     *                  remaining time.
     * @param rNumBytesRead
     *                  [out] number of bytes read
     *
     * @return OS_SUCCESS, OS_WAIT_TIMEOUT, OS_INVALID_ARGUMENT or
     *         OS_FAILED
     */
    OsStatus
    SerialCommWnt::read(
        void* pData,
        const size_t& rNumBytes,
        milliseconds& rTimeout,
        size_t& rNumBytesRead)
    
        OsStatus result = OS_WAIT_TIMEOUT;
        rNumBytesRead = 0;
        DWORD numBytesRead = 0;
        DWORD commError;
        COMSTAT commStatus;
        auto startTime = system_clock::now();
        if (mpOverlappedRx) 
            // update the timeout used for ReadFile - note that the
            // magic combination that works for an absolute timeout is
            // MAXDWORD, timeoutMS, 0.
            COMMTIMEOUTS commTimeouts;
            GetCommTimeouts(mOsFileHandle, &commTimeouts);
            if (commTimeouts.ReadTotalTimeoutConstant != rTimeout.count()) 
                commTimeouts.ReadIntervalTimeout = MAXDWORD;
                commTimeouts.ReadTotalTimeoutConstant =
                    static_cast<DWORD>(rTimeout.count());
                SetCommTimeouts (mOsFileHandle, &commTimeouts);
            

            // asynchronous overlapped IO mode.
            // reset the manual event to the non-signaled.
            // No Need for this as ReadFile resets it by itself
            // ResetEvent(mpOverlappedRx->hEvent);
            BOOL isOK = ReadFile(
                mOsFileHandle, pData, (DWORD)rNumBytes,
                reinterpret_cast<DWORD*>(&rNumBytesRead),
                mpOverlappedRx.get());
            // get the result to date - only valid to call this
            // if ReadFile returns !isOK (FALSE) &&
            // last error set to ERROR_IO_PENDING
            //milliseconds elapsedTime;
            if (!isOK) 
                DWORD dwLastError = GetLastError();
                if (dwLastError == ERROR_IO_PENDING) 
                    // pending IO, wait to complete using the COMMTIMEOUTS timer.
                    // when the COMMTIMEOUTS timer expires it will signal the 
                    // manual mpOverlappedRx->hEvent
                    DWORD ovlStatus = ::WaitForSingleObject(
                        mpOverlappedRx->hEvent, static_cast<DWORD>(
                            /*rTimeout.count()*/INFINITE));
                    switch (ovlStatus) 
                    case WAIT_TIMEOUT:
                        // timeout - update the remaining time to 0
                        rTimeout = milliseconds::zero();
                        result = OS_WAIT_TIMEOUT;
                        //elapsedTime = duration_cast<milliseconds>(
                        //    system_clock::now() - startTime);
                        break;
                    case WAIT_OBJECT_0:
                        // now that we have some data avaialable
                        // read it from overlapped IO
                        isOK = ::GetOverlappedResult(
                            mOsFileHandle, mpOverlappedRx.get(),
                            reinterpret_cast<DWORD*>(&rNumBytesRead),
                            FALSE);
                        result = (isOK && rNumBytesRead>0)?
                            OS_SUCCESS : OS_FAILED;
                        //elapsedTime = duration_cast<milliseconds>(
                        //    system_clock::now() - startTime);
                        // update the remaing time (cannot be < 0)
                        rTimeout = std::max<milliseconds>(
                            rTimeout - duration_cast<milliseconds>(
                                system_clock::now() - startTime),
                            milliseconds::zero());
                        break;
                    default:
                        rTimeout = milliseconds::zero();
                        break;
                    
                 else if (dwLastError == ERROR_HANDLE_EOF) 
                    ClearCommError(mOsFileHandle, &commError, &commStatus);
                    result = OS_FILE_EOF;
                 else 
                    ClearCommError(mOsFileHandle, &commError, &commStatus);
                    result = OS_FAILED;
                
             else  // Success
                //elapsedTime = duration_cast<milliseconds>(
                //    system_clock::now() - startTime);
                rTimeout = std::max<milliseconds>(
                    rTimeout - duration_cast<milliseconds>(
                        system_clock::now() - startTime),
                    milliseconds::zero());
                result = OS_SUCCESS;
            
         else  // sync mode
            BOOL isOK = ReadFile ( mOsFileHandle, pData, (DWORD)rNumBytes,
                reinterpret_cast<LPDWORD>(&numBytesRead), NULL);
            if ( isOK && (numBytesRead > 0) ) 
                rNumBytesRead = (size_t) numBytesRead;
                result = OS_SUCCESS;
             else 
                ClearCommError(mOsFileHandle, &commError, &commStatus);
                // @JC Changed from simple test if lpErrors == 9)
                // which is equivalent to (CE_BREAK | CE_RXOVER)
                //if ((lpErrors & (CE_BREAK | CE_FRAME | CE_OVERRUN |
                //     CE_RXOVER | CE_RXPARITY)) != 0x00) 
                if (commError == 9) 
                    result = OS_FAILED;
    //              printf ("ClearCommError - lpErrors[%02x]", lpErrors);
                
            
            // update the remaing time (cannot be < 0)
            rTimeout = std::max<milliseconds>(
                rTimeout - duration_cast<milliseconds>(
                    system_clock::now() - startTime),
                milliseconds::zero());
        
        return result;
    

【问题讨论】:

【参考方案1】:
    if (dwLastError == ERROR_IO_PENDING) 
        DWORD ovlStatus = ::WaitForSingleObject(mpOverlappedRx->hEvent, ...);
        //...
    

程序员使用重叠 I/O 时,这是一个非常常见的错误。核心思想是您使用它来允许设备驱动程序通过第一个 ReadFile() 调用开始工作。这需要一段时间,I/O 总是如此,尤其是串行端口,因为它们是非常慢的设备。

所以你问司机“开始吧”,它就开始工作了。驱动程序最终将通过调用 OVERLAPPED.hEvent 上的 SetEvent() 方法发出信号已完成。这完成了您的 WaitForSingleObject() 调用。

应该在驱动程序工作时您应该做的是其他事情。您的线程应该做的另一项工作,在驱动程序处理 I/O 请求时很有用。例如,您可以用它点亮 MsgWaitForMultipleObjects()。这会泵出一个消息循环,因此您的 UI 仍然可以响应。并且还会告诉您串行端口何时有新数据可用。

代码中的缺陷是你不知道还能做什么。它立即调用 WaitForSingleObject() 以等待重叠 I/O 完成。在驱动程序处理读取请求时阻塞线程并且不做任何有用的工作。这是一个很常见的问题。

换句话说,您还没有找到使用重叠 I/O 的充分理由。通过使用同步 ReadFile() 调用,您将获得完全相同相同的结果。就像您当前的代码一样,它将阻塞,直到串行端口有可用数据。

所以不要打扰它。也解决了超时问题。

【讨论】:

严格来说并非如此。虽然这是重叠 I/O 的一种用例,但另一种是全双工串行端口操作。在这种情况下,如果有一个线程专用于从端口读取,那么在 WaitForSingleObject 调用上等待时间是完全可以的。 我意识到它与使用非重叠 IO 几乎相同,但不同之处在于(正如我的问题所提到的),我试图使这个 API 在本质上与其使用的 Linux 对应物相似一个 select()/poll() 系统调用来等待数据到达并被发送到文件句柄 - 之后指定的超时被更新以反映剩余的超时。使用非重叠方法来做同样的事情需要重复低效的 ReadFile/sleep 直到超时过期或数据到达。其实在这之前我用的是不重叠的方法 嗯,是的,这就是重叠 I/O 的目的。但是,您必须以非常不同的方式编写程序。关键是您不要等待 I/O 调用完成和轮询。使用 WaitForSingleObject() 超时为 0。或者使用 WaitForMultipleObjects() 更有效。您发布的代码与此完全不同,使用阻塞的 ReadFile 调用可以更简单。 谢谢汉斯,你看到'if (mpOverlappedRx)' 的else 块在哪里cets // 同步模式?这是更简单的方法,基本上立即返回 ReadFile 结果。【参考方案2】:

以下是直接来自串行驱动程序的注释,可能会对您有所帮助:

if (timeoutsForIrp.ReadIntervalTimeout == MAXULONG) 

//
// We need to do special return quickly stuff here.
//
// 1) If both constant and multiplier are
//    0 then we return immediately with whatever
//    we've got, even if it was zero.
//
// 2) If constant and multiplier are not MAXULONG
//    then return immediately if any characters
//    are present, but if nothing is there, then
//    use the timeouts as specified.
//
// 3) If multiplier is MAXULONG then do as in
//    "2" but return when the first character
//    arrives.
//

首先,我们来看看你的同步读取:

假设您在初始化中设置的 COMMTIMEOUT 值没有任何问题,其中间隔设置为 MAXDWORD 并且其他所有值都为 0,那么您的同步 ReadFile 始终会立即返回任何可用字节数,包括 0 (情况1)。如果您指定了读取常量超时,那么如果没有数据可用,则将使用它,这意味着如果没有数据到达,您的 ReadFile 调用可能会超时(案例 #2)。最后,如果您将读取乘数和读取间隔都设置为MAXDWORD,那么它本质上是#2 的一种特殊情况,ReadFile 调用会在第一个字节存在时返回(所以除非您使用 USB 串行桥或一些可以传递数据块的管道,写入的字节值很可能是 1)。

现在,让我们看看异步读取:

对于您的异步ReadFile 调用,您需要知道无论您设置的COMMTIMEOUT 值的组合如何,该函数都会立即返回。您检查ERROR_IO_PENDING 的方式是正确的。如果ReadFile 调用返回待处理,那么您应该等待重叠对象并获取调用的重叠结果。这与同步 ReadFile 之间的区别在于,在同步 ReadFile 只会阻塞的情况下,重叠读取现在有一个额外的返回值 ERROR_IO_PENDING 返回。

您似乎对异步方法中的超时进行了太多不必要的修改。我只是将读取常量超时设置为一个合理的值,其余的设置为 0 仅在初始化时,不要管它。在同步情况下,它将阻塞,直到数据到达或超时。在异步情况下,它将返回,您可以发布一个等待,该等待可以通过WAIT_TIMEOUT 自行超时,或者发出信号以显示原始请求的完成,成功、超时或其他一些失败。

另外评论 Hans 所说的内容,这里的行为将是非异步的 ReadFile 将阻塞串行驱动程序,而异步的 ReadFile 将使您有机会调用 WaitForSingleObject ,但是如果你调用它,你现在在你的应用程序中被阻塞了。您需要决定哪个更适合您的解决方案。

【讨论】:

以上是关于如何正确使用 COMMTIMEOUTS 和从串行端口读取的 OVERLAPPED IO 模式的主要内容,如果未能解决你的问题,请参考以下文章

在BIOS中如何设置出串行总线控制器???紧急求助!!!!!!!!!!

串行端口重定向或拆分[关闭]

BIOS串口3F8跟2E8是啥? 如何设置正确?

如何使用android蓝牙接收串行数据

客户端是如何上传数据到FTP服务器和从FTP服务器下载文件的?

如何从串行数据中提取特定序列