Windows/Windows CE下的串行I/O重叠/非重叠。

6
抱歉,这不是一个问题,而是为了帮助遇到以下问题的人们。我正在解决需要使用串行输入/输出(Serial I/O)的问题,但主要是在Windows CE 6.0下运行。然而,最近有人问我是否可以将应用程序也制作成能够在Windows下运行,所以我开始解决这个问题。我花了很多时间寻找答案,但发现许多信息都是错误的或不准确的。因此,在解决了这个问题之后,我决定与大家分享我的研究结果,以便任何遇到这些困难的人都能得到答案。
在Windows CE下,OVERLAPPED I/O不受支持。这意味着通过串口进行双向通信可能会非常麻烦。主要问题是当您正在等待从串口接收数据时,您不能发送数据,因为这样做会导致您的主线程阻塞,直到读取操作完成或超时(取决于您是否设置了超时)。
像大多数进行串行I/O的人一样,我设置了一个读取器串行线程来读取串口,它使用带有EV_RXCHAR掩码的WaitCommEvent()等待串行数据。现在,这就是Windows和Windows CE的困难所在。
如果我有一个简单的读取器线程,例如:
UINT SimpleReaderThread(LPVOID thParam)
{
    DWORD eMask;
    WaitCommEvent(thParam, &eMask, NULL);
    MessageBox(NULL, TEXT("Thread Exited"), TEXT("Hello"), MB_OK);
}

显然,在上面的示例中,我没有从串口读取数据或其他任何操作,并且假设thParam包含已打开的comm端口的句柄等。现在,问题是在Windows下,当您的线程执行并命中WaitCommEvent()时,您的读取器线程将进入睡眠状态,等待串口数据。好吧,这很好,应该是这样的,但是...如何结束此线程并使MessageBox()出现?嗯,事实证明,这实际上并不容易,并且是Windows CE和Windows之间在其串行I/O方面处理方式上的根本差异。
在Windows CE下,您可以做一些事情来使WaitCommEvent()失效,例如SetCommMask(COMMPORT_HANDLE, 0)甚至CloseHandle(COMMPORT_HANDLE)。这将允许您正确终止线程,因此释放串行端口以便开始发送数据。但是,在Windows下,这两种方法都无法工作,并且都会导致您调用它们的线程进入睡眠状态,等待WaitCommEvent()完成。那么,在Windows下如何结束WaitCommEvent()呢?通常,您会使用OVERLAPPED I/O,并且线程阻塞不会成为问题,但由于解决方案必须与Windows CE兼容,因此OVERLAPPED I/O不是一种选择。在Windows下,有一件事可以做,就是调用CancelSynchronousIo()函数,这将结束您的WaitCommEvent(),但请注意,这可能与设备相关。 CancelSynchronousIo()的主要问题是Windows CE也不支持它,因此对于这个问题,您无法使用它!
那么你该怎么做呢?事实上,为了解决这个问题,您不能使用WaitCommEvent(),因为没有一种方式可以在Windows上终止此函数,而这种方式又得到Windows CE的支持。那么,只剩下ReadFile(),它将在读取非OVERLAPPED I/O时阻塞,并且这将与Comm Timeouts一起工作。
使用ReadFile()和COMMTIMEOUTS结构意味着您必须有一个紧密的循环等待串行数据,但如果您没有接收大量串行数据,则不应该成为问题。还可以使用小超时事件来结束循环,以确保资源传递回系统,并且您不会以100%的负载攻击处理器。以下是我提出的解决方案,如果您认为它可以改进,请给予反馈。
typedef struct
{
    UINT8 sync;
    UINT8 op
    UINT8 dev;
    UINT8 node;
    UINT8 data;
    UINT8 csum;
} COMMDAT;

COMSTAT cs = {0};
DWORD byte_count;
COMMDAT cd;

ZeroMemory(&cd, sizeof(COMMDAT));
bool recv = false;
do
{
    ClearCommError(comm_handle, 0, &cs);
    if (cs.cbInQue == sizeof(COMMDAT))
    {
        ReadFile(comm_handle, &cd, sizeof(COMMDAT), &byte_count, NULL);
        recv = true;
    }
} while ((WaitForSingleObject(event_handle, 2) != WAIT_OBJECT_0) && !recv);
ThreadExit(recv ? cd.data : 0xFF);

因此,要结束线程,只需在event_handle中发送信号即可,这将使您能够正确退出线程并清理资源,并且在Windows和Windows CE上工作正常。

希望这能帮助所有遇到这个问题的人。


为什么不尝试在运行时动态加载CancelSynchronousIo(),如果您在CE下只需调用一个空存根?或者如果它在桌面上,则使库使用重叠,但在Windows CE下则不重叠?似乎有更顺畅的方法解决这个问题。 - ctacke
你不能动态加载CancelSynchronousIo(),它是Windows API函数,不是Windows CE的一部分。这样做的目的是拥有一个在Windows CE和Windows上都能工作而不需要修改的解决方案。没有必要为Windows创建OVERLAPPED版本,为Windows CE创建单独的非OVERLAPPED版本,因为你可以使用#ifdef/#endif条件编译你的代码来适配目标平台。 - The Welder
“SetCommMask”一直是我用来使“WaitCommEvent”提前完成的方法。然而,在我的代码中,它总是触发正在进行的重叠“WaitCommEvent”的完成。你是说重叠的“WaitCommEvent”确实会被“SetCommMask”完成,但同步的不会吗? - Ben Voigt
你为 commTimeouts 设置了什么?是 MAXDWORD, 0,0,0,0 吗? - AaA
“WaitForSingleObject”主要是一个“Sleep”吗? - Sam Ginrich
1个回答

6

我认为在我的上面的评论中有一些误解,下面详细介绍两种可能的解决方案,它们不使用紧密循环。请注意,这些方案使用运行时确定,因此在两个操作系统下都可以正常工作(尽管您必须为每个目标单独编译),并且由于它们都没有使用#ifdef,所以不太可能在不立即注意到的情况下在一侧或另一侧破坏编译器。

首先,您可以动态加载CancelSynchonousIo,并在操作系统中使用它。甚至可以选择在CE上执行某些操作而不是取消(例如关闭句柄?);

typedef BOOL (WINAPI *CancelIo)(HANDLE hThread);

HANDLE hPort;

BOOL CancelStub(HANDLE h)
{
    // stub for WinCE
    CloseHandle(hPort);
}

void IoWithCancel()
{
    CancelIo cancelFcn;

    cancelFcn = (CancelIo)GetProcAddress(
        GetModuleHandle(_T("kernel32.dll")), 
        _T("CancelSynchronousIo"));

    // if for some reason you want something to happen in CE
    if(cancelFcn == NULL)
    {
        cancelFcn = (CancelIo)CancelStub;
    }

    hPort = CreateFile( /* blah, blah */);

    // do my I/O

    if(cancelFcn != NULL)
    {
        cancelFcn(hPort);
    }
}

另一种选择需要更多的工作,因为您可能会遇到不同的线程模型(但如果您使用C ++,这将是基于平台的单独类的绝佳案例),那就是确定平台并在桌面上使用overlapped:

HANDLE hPort;

void IoWithOverlapped()
{
    DWORD overlapped = 0;
    OSVERSIONINFO version;

    GetVersionEx(&version);
    version.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);
    if((version.dwPlatformId == VER_PLATFORM_WIN32_WINDOWS)
        || (version.dwPlatformId == VER_PLATFORM_WIN32_NT))
    {
        overlapped = FILE_FLAG_OVERLAPPED;
    }
    else
    {
        // create a receive thread
    }

    hPort = CreateFile(
        _T("COM1:"), 
        GENERIC_READ | GENERIC_WRITE, 
        FILE_SHARE_READ | FILE_SHARE_WRITE, 
        NULL, 
        OPEN_EXISTING, 
        overlapped,
        NULL);
}

好的,我承认你关于确定CancelSynchronousIo()的存在并在可用时使用它的观点是正确的!干得好,这从来没有真正跨越过我的思维...我只是采用了对两个平台都相同的代码。该死,我的老派字节保存方式! - The Welder
cancelFcn(hPort); 是错误的;该函数需要取消同步 I/O 的线程句柄。要么提供该句柄(例如,由 CreateThread 返回),要么使用接受端口句柄的 CancelIoEx 函数。 - M.M

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接