OregonGhost
OregonGhost

Reputation: 23759

SerialPorts and WaitForMultipleObjects

I'm having some problems with serial ports in a cross-platform application (with Linux embedded and actual embedded targets), which also works on Windows to make development easier. This is about the Windows implementation.

The implementation of the serial protocol is, therefore, targetted at a mixture of OS- and non-OS systems and I won't touch the implementation itself. I'd like to make it compatible with the existing implementation. If that fails within reasonable time, I'll just make a separate thread for serial reading.

OK, basically the implementation opens the serial port, registers the file descriptor in our IO system (which uses epoll on Linux and WaitForMultipleObjects on Windows) and then, basically, just waits for all handles and does whatever required. So we want to read from the serial port when the handle is signaled for reading. Unfortunately on Windows, you can't specify if you're waiting for read or write, so I thought I'd use the following solution:

That's the basic setup. I register the event handle instead of the file handle to wait on. When the handle is signalled, I do the following:

It seems, however, that if you specify FILE_FLAG_OVERLAPPED, you must use overlapped IO also for reading and writing. So I thought that whenever ReadFile or WriteFile return ERROR_IO_PENDING, I'll just wait for the IO with WaitForSingleObject and GetOverlappedResult. It seems that I don't get into that though. It seems to work basically, but sometimes it crashes on one of the ResetEvent calls, as if the overlapped was still active (though I guess it still shouldn't crash).

So, the actual question. Can this be done as I want it? Is there a problem with the approach in general, or should it work? Or is using yet another thread the only good solution? The communication is already in a separate thread, so it would be at least three threads then.


I'll try to post as much code as needed, though it is reduced from the actual code which contains a lot of things not directly related to serial reading.

SerialPort::SerialPort(const std::string &filename)
{
    fd = INVALID_HANDLE_VALUE;
    m_ov = new OVERLAPPED(); // Pointer because header shouldn't include Windows.h.
    memset(m_ov, 0, sizeof(OVERLAPPED));
    m_waitHandle = m_ov->hEvent = CreateEvent(0, true, 0, 0);
}

SerialPort::~SerialPort(void)
{
    Close();
    CloseHandle(m_ov->hEvent);
    delete m_ov;
}

The constructor is called in a separate thread, which later calls Open:

bool SerialPort::Open(void)
{
    if (fd != INVALID_HANDLE_VALUE)
        return true;
    fd = CreateFile(filename.c_str(), GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL);
    if (fd != INVALID_HANDLE_VALUE) {
        DCB dcb;
        ZeroMemory(&dcb, sizeof(DCB));

        COMMTIMEOUTS timeouts = {0};
        timeouts.ReadIntervalTimeout = TimeOut();
        timeouts.ReadTotalTimeoutConstant = TimeOut();
        timeouts.ReadTotalTimeoutMultiplier = TimeOut() / 5;
        if (timeouts.ReadTotalTimeoutMultiplier == 0) {
            timeouts.ReadTotalTimeoutMultiplier = 1;
        }

        if (!SetCommTimeouts(fd, &timeouts)) {
            DebugBreak();
        }
        SetCommMask(fd, EV_RXCHAR);
        InitWait();

        return true;
    }
    return false;
}

void SerialPort::InitWait()
{
    if (WaitForSingleObject(m_ov->hEvent, 0) == WAIT_OBJECT_0) {
        return; // Still signaled
    }         
    DWORD dwEventMask;
    if (!WaitCommEvent(fd, &dwEventMask, m_ov)) {
        // For testing, I have some prints here for the different cases.
    }
}

Via a rather long chain, the thread then calls WaitForMultipleObjects on m_waitHandle, which is the same as the hEvent member of the OVERLAPPED structure. This is done in a loop, and there are several other handles in the list, that's why this is different from the typical solution where you have a thread exclusively reading from the serial port. I have, basically, no control about the loop, that's why I try to do the WaitCommEvent (within InitWait) at just the right time.

When the handle is signaled, the ReadData method is called by the thread:

int SerialPort::ReadData(void *buffer, int size)
{
    if (fd != INVALID_HANDLE_VALUE) {
        // Timeouts are reset here to MAXDWORD/0/0, not sure if necessary.
        DWORD dwBytesRead;
        OVERLAPPED ovRead = {0};
        ovRead.hEvent = CreateEvent(0, true, 0, 0);
        if (ReadFile(fd, buffer, size, &dwBytesRead, &ovRead)) {
            if (WaitForSingleObject(m_ov->hEvent, 0) == WAIT_OBJECT_0) {
                // Only reset if signaled, because we might get here because of a timer.
                ResetEvent(m_waitHandle);
                InitWait();
            }
            CloseHandle(ovRead.hEvent);
            return dwBytesRead;
        } else {
            if (GetLastError() == ERROR_IO_PENDING) {
                WaitForSingleObject(ovRead.hEvent, INFINITE);
                GetOverlappedResult(fd, &ovRead, &dwBytesRead, true);
                InitWait();
                CloseHandle(ovRead.hEvent);
                return dwBytesRead;
            }
        }
        InitWait();
        CloseHandle(ovRead.hEvent);
        return -1;
    } else {
        return 0;
    }
}

The write is done as follows, without syncing:

int SerialPort::WriteData(const void *buffer, int size)
{
    if (fd != INVALID_HANDLE_VALUE) {
        DWORD dwBytesWritten;
        OVERLAPPED ovWrite = {0};
        ovWrite.hEvent = CreateEvent(0, true, 0, 0);
        if (!WriteFile(fd, buffer, size, &dwBytesWritten, &ovWrite)) {
            if (GetLastError() == ERROR_IO_PENDING) {
                WaitForSingleObject(ovWrite.hEvent, INFINITE);
                GetOverlappedResult(fd, &ovWrite, &dwBytesWritten, true);
                CloseHandle(ovWrite.hEvent);
                return dwBytesWritten;
            } else {
                CloseHandle(ovWrite.hEvent);
                return -1;
            }
        }
        CloseHandle(ovWrite.hEvent);
    }
    return 0;
}

It seems that it does work now. There are no crashes anymore, at least I can't reproduce them. So as it works now, I'm just asking if what I do is sane, or if I should do things differently.

Upvotes: 1

Views: 4135

Answers (1)

Remy Lebeau
Remy Lebeau

Reputation: 597166

Offhand, I don't see any errors in the code you have shown, but I would like to suggest alternative code to clean up your error handling in ReadData() and WriteData() in general:

int SerialPort::ReadData(void *buffer, int size)
{
    if (fd == INVALID_HANDLE_VALUE)
        return 0;

    OVERLAPPED ovRead = {0};
    ovRead.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
    if (!ovRead.hEvent)
        return -1;

    DWORD dwBytesRead;
    if (!ReadFile(fd, buffer, size, &dwBytesRead, &ovRead))
    {
        if (GetLastError() != ERROR_IO_PENDING)
        {
            CloseHandle(ovRead.hEvent);
            return -1;
        }

        if (!GetOverlappedResult(fd, &ovRead, &dwBytesRead, TRUE))
        {
            CloseHandle(ovRead.hEvent);
            return -1;
        }
    }

    if (WaitForSingleObject(m_waitHandle, 0) == WAIT_OBJECT_0)
    {
        ResetEvent(m_waitHandle);
        InitWait();
    }

    CloseHandle(ovRead.hEvent);
    return dwBytesRead;
}

int SerialPort::WriteData(const void *buffer, int size)
{
    if (fd == INVALID_HANDLE_VALUE)
        return 0;

    OVERLAPPED ovWrite = {0};
    ovWrite.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
    if (!ovWrite.hEvent)
        return -1;

    DWORD dwBytesWritten;
    if (!WriteFile(fd, buffer, size, &dwBytesWritten, &ovWrite))
    {
        if (GetLastError() != ERROR_IO_PENDING)
        {
            CloseHandle(ovWrite.hEvent);
            return -1;
        }

        if (!GetOverlappedResult(fd, &ovWrite, &dwBytesWritten, TRUE))
        {
            CloseHandle(ovWrite.hEvent);
            return -1;
        }
    }

    CloseHandle(ovWrite.hEvent);
    return dwBytesWritten;
}

Upvotes: 1

Related Questions