Nereid Regulus
Nereid Regulus

Reputation: 132

Serial WriteFile returns before completion

I've been working on a program which dialog with an external device via a serial RS422 bus. The goal is to send commands to the device, which send an answer back. So far, the code for sending a message look like this:

OVERLAPPED osWrite = {0};

void init()
{
    // Create this write operation's OVERLAPPED structure's hEvent.
    osWrite.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
    if (osWrite.hEvent == NULL)
        // error creating overlapped event handle
        std::cout << "Error osWrite.hEvent" << std::endl; // Error in communications; report it.

    *hPort = CreateFile("\\\\.\\COM18", (GENERIC_READ | GENERIC_WRITE), FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL);

    if (*hPort == INVALID_HANDLE_VALUE) {
        std::cout << "Invalid port com handle" << GetLastError() << std::endl;
        return;
    }
    
    COMMTIMEOUTS commTimeout;
    if (GetCommTimeouts(*hPort, &commTimeout)) {
        commTimeout.ReadIntervalTimeout = 10;
        commTimeout.ReadTotalTimeoutConstant = 10;
        commTimeout.ReadTotalTimeoutMultiplier = 10;
        commTimeout.WriteTotalTimeoutConstant = 10;
        commTimeout.WriteTotalTimeoutMultiplier = 10;
    } else
        return;

    if (!SetCommTimeouts(*hPort, &commTimeout)) {
        std::cout << "Error comm timeout" << std::endl;
    }

    DCB dcb;

    if (!GetCommState(*hPort, &dcb)) {
        std::cout << "Invalid port com settings" << std::endl;
        return;
    }

    dcb.BaudRate = CBR_115200;
    dcb.ByteSize = 8;
    dcb.Parity = NOPARITY;
    dcb.StopBits = ONESTOPBIT;

    SetCommMask(*hPort, EV_RXCHAR);
    SetCommState(*hPort, &dcb);
    
    return;
}

DWORD serial_send(HANDLE *hPort, char *msg, int length) {

    DWORD dwWritten;
    DWORD dwRes;
    BOOL fRes;

    PurgeComm(*hPort, PURGE_TXCLEAR);

    ResetEvent(osWrite.hEvent);

    // Issue write.
    if (!WriteFile(*hPort, msg, length, &dwWritten, &osWrite)) {
        if (GetLastError() != ERROR_IO_PENDING) {
            // WriteFile failed, but isn't delayed. Report error and abort.
            fRes = FALSE;
        } else {
            fRes = FALSE;
            while (!fRes) {
                // Write is pending.
                dwRes = WaitForSingleObject(osWrite.hEvent, INFINITE);
                switch (dwRes) {
                // OVERLAPPED structure's event has been signaled.
                case WAIT_OBJECT_0:
                    if (!GetOverlappedResult(*hPort, &osWrite, &dwWritten, FALSE))
                        fRes = FALSE;
                    else
                        // Write operation completed successfully.
                        fRes = TRUE;
                    break;

                default:
                    // An error has occurred in WaitForSingleObject.
                    // This usually indicates a problem with the
                    // OVERLAPPED structure's event handle.
                    fRes = FALSE;
                    break;
                }
            }
        }
    } else {
        // WriteFile completed immediately.
        fRes = TRUE;
    
    }
    return dwWritten;
}

The last function can't return until the write operation is successful. The init() function load without error. I've used a lot of code from here : https://learn.microsoft.com/en-us/previous-versions/ff802693(v=msdn.10)

Each message is 210 bytes long, and the serial port is running at 115200 bit/s, meaning that I should send a message every ~18.2ms. (210 bytes * 10 bits / 115200) However, when I measure the time elapsed between 2 messages, I sometimes get a duration much inferior than the expected 18ms (it can go down to 11ms).

Is this a normal behavior for an asynchronous WriteFile + WaitForSingleObject? What happens if I send another message just after 11ms, will it corrupt the previous message, or does it gets buffered?

I used std::chrono::high_resolution_clock::now() and std::chrono::duration<double, std::milli>(end - start).count() to get the duration of the frame, is it really accurate?

Upvotes: 0

Views: 622

Answers (1)

kunif
kunif

Reputation: 4350

Since Windows is not a real-time OS and is a multi-process & multi-thread OS, time accuracy should not be guaranteed.

If the system is lightly loaded, most of it will work as intended, but not always.

Conversely, the completion of WriteFile() may be notified earlier than it actually is, depending on the hardware and device driver stack configuration.

For example, it may be considered that the process is completed at the time when the data is completely stored in the buffer of the device driver or when the last data is written to the FIFO buffer of the interface chip.

It is better to think that WriteFile() may be completed even if not all the data actually reaches the other party.

It is considered the same as writing file data to the hard disk. Completion of writing to a file on disk is done in the system buffer, and should be written to the actual media at a different time.


If the next serial_send() function is called before all the WriteFile data of the previous time has reached the other party due to bad conditions, there is a possibility that some of the previous transmission data will be discarded.

Because PurgeComm(*hPort, PURGE_TXCLEAR); is called at the beginning of the serial_send() function.
It's not as critical as specifying PURGE_TXABORT, but there is still the possibility of data being discarded with PURGE_TXCLEAR.

PurgeComm function

PURGE_TXABORT 0x0001 Terminates all outstanding overlapped write operations and returns immediately, even if the write operations have not been completed.

PURGE_TXCLEAR 0x0004 Clears the output buffer (if the device driver has one).

If a thread uses PurgeComm to flush an output buffer, the deleted characters are not transmitted. To empty the output buffer while ensuring that the contents are transmitted, call the FlushFileBuffers function (a synchronous operation).

The workaround is to simply not call PurgeComm().

If it is a serial port API, it may be possible to wait for the completion of transmission by specifying/detecting EV_TXEMPTY with SetCommMask()/WaitCommEvent(), but this will only be complicated.

SetCommMask function / WaitCommEvent function

EV_TXEMPTY 0x0004 The last character in the output buffer was sent.


Then your usage of WriteFile() + WaitForSingleObject() + GetOverlappedResult() will eventually work similar to calling WriteFile() synchronously.

Asynchronous operation is not always necessary, but it is better to consider in detail based on what kind of behavior your system requires.

Upvotes: 1

Related Questions