3

I seem to be having a bit of trouble in waiting for the completion of serial data transmissions.

My interpretation of the relevant MSDN article is the EV_TXEMPTY event is the correct signal and which indicates that:

EV_TXEMPTY - The last character in the output buffer was sent.

However in my tests the event always fires immediately as soon as the data has been submitted to the buffer and long before the final has actually reached the wire. See the repro code below where the period is always zero.

Have I made an error in the implementation, am I misunderstanding the purpose of the flag, or is this feature simply not supported by modern drivers? In the latter case is there a viable workaround, say some form of synchronous line state request?

For the record the tests were conducted with FTDI USB-RS485 and TTL-232R devices in a Windows 10 system, a USB-SERIAL CH340 interface on a Windows 7 system, as well as the on-board serial interface of a 2005-vintage Windows XP machine. In the FTDI case sniffing the USB bus reveals only bulk out transactions and no obvious interrupt notification of the completion.

#include <stdio.h>
#include <windows.h>

static int fatal(void) {
    fprintf(stderr, "Error: I/O error\n");
    return 1;
}

int main(int argc, const char *argv[]) {
    static const char payload[] = "Hello, World!";
    // Use a suitably low bitrate to maximize the delay
    enum { BAUDRATE = 300 };
    // Ask for the port name on the command line
    if(argc != 2) {
        fprintf(stderr, "Syntax: %s {COMx}\n", argv[0]);
        return 1;
    }
    char path[MAX_PATH];
    snprintf(path, sizeof path, "\\\\.\\%s", argv[1]);
    // Open and configure the serial device
    HANDLE handle = CreateFileA(path, GENERIC_WRITE, 0, NULL,
        OPEN_EXISTING, 0, NULL);
    if(handle == INVALID_HANDLE_VALUE)
        return fatal();
    DCB dcb = {
        .DCBlength = sizeof dcb,
        .BaudRate = BAUDRATE,
        .fBinary = TRUE,
        .ByteSize = DATABITS_8,
        .Parity = NOPARITY,
        .StopBits = ONESTOPBIT
    };
    if(!SetCommState(handle, &dcb))
        return fatal();
    if(!SetCommMask(handle, EV_TXEMPTY))
        return fatal();
    // Fire off a write request
    DWORD written;
    unsigned int elapsed = GetTickCount();
    if(!WriteFile(handle, payload, sizeof payload, &written, NULL) ||
        written != sizeof payload)
        return fatal();
    // Wait for transmit completion and measure time elapsed
    DWORD event;
    if(!WaitCommEvent(handle, &event, NULL))
        return fatal();
    if(!(event & EV_TXEMPTY))
        return fatal();
    elapsed = GetTickCount() - elapsed;
    // Display the final result
    const unsigned int expected_time =
        (sizeof payload * 1000 /* ms */ * 10 /* bits/char */) / BAUDRATE;
    printf("Completed in %ums, expected %ums\n", elapsed, expected_time);
    return 0;
}

The background is that this is part of a Modbus RTU protocol test suite where I am attempting to inject >3.5 character idle delays between characters on the wire to validate device response. Admittedly, an embedded realtime system would have been more far suitable for the task but for various reasons I would prefer to stick to a Windows environment while controlling the timing as best as possible.

doynax
  • 4,285
  • 3
  • 23
  • 19
  • 1
    You could add a call to [`SetupComm()`](https://msdn.microsoft.com/en-us/library/windows/desktop/aa363439.aspx) to set a buffer size, not sure what it is by default. I'm not a Windows programmer, though. – unwind Dec 19 '17 at 10:06
  • Also, perhaps [`SetCommBreak()`](https://msdn.microsoft.com/en-us/library/windows/desktop/aa363433.aspx) can be used to implement a delay in the transmission? – unwind Dec 19 '17 at 10:16
  • @unwind: Thank you. I tried `SetupComm(handle, 2, 2)` (a single-character buffer results in `ERROR_INVALID_DATA` errors) but unfortunately it made no difference. At least not for the FTDI interfaces on the Windows 10 system, and the request yields no traffic on the USB bus that I can see. – doynax Dec 19 '17 at 10:31
  • 2
    The underlying device driver in general will only tell you if the transmit buffer is empty. It won't tell you about data in flight, such as what is stored in the FIFO buffer built into the UART chip. Or what the USB controller on the other end of the wire is doing. If that matters, do avoid making it matter, then you can't do anything but sleep for a while after detecting EV_TXEMPTY. – Hans Passant Dec 19 '17 at 10:42
  • @unwind: I see what you are getting at but unfortunately `SetCommBreak` [relies in application timing](https://support.microsoft.com/en-us/help/119506/transmitting-a-break-signal). I tried toggling the break state but it did not result synchronization. Plus, I don't particularly want to send a break character ;) – doynax Dec 19 '17 at 10:42
  • @doynax Right, it does mean a different character being sent, I was a bit uncertain about the exact semantics of break, haven't ever used it. – unwind Dec 19 '17 at 10:44
  • @Hans Passant: I see, thank you. My problem stems from the [Modbus RTU specification](http://www.modbus.org/docs/Modbus_over_serial_line_V1.pdf) according to which a space marks the end of a packet. Typically this requirement is relaxed by inspection of packet contents but this effort is part of validating the standard compliance of a device. I suppose a manual guestimated delay will have to suffice if I can't come up with any other workaround. – doynax Dec 19 '17 at 10:50
  • 1
    because you use synchronous handle and as result synchronous `WriteFile` - additional call to `WaitCommEvent` for wait on `SERIAL_EV_TXEMPTY` at all no sense - because synchronous write not return control until all bytes from your buffer will be send to lower level driver or hardware. *event always fires immediately as soon as the data has been submitted* this and must be – RbMm Dec 19 '17 at 10:54
  • @RbMm: Yes, I gather as much from Hans Passant's comment. I had incorrectly interpreted the mechanism as being end-to-end and not solely reflecting the Windows buffer (the documentation doesn't exactly spell out what _sent_ means in this context), mostly based on the a web search only turning up such uses for the event. – doynax Dec 19 '17 at 10:58

1 Answers1

2

According to the comments by @Hans Passant and @RbMm the output buffer being referred in the EV_TXEMPTY documentation is an intermediate buffer and the event indicates that data has been forwarded to the driver. No equivalent notification event is defined which encompasses the full chain down to the final device buffers.

No general workaround is presently clear to me short of a manual delay based upon the bitrate and adding a significant worst-case margin for any remaining buffer layers to be traversed, inter-character gaps, clock skew, etc.

I would therefore very much appreciate answers with better alternate solutions.


Nevertheless, for my specific application I have implemented a viable workaround.

The target hardware is a half-duplex bus with a FTDI RS485 interface. This particular device offers an optional local-echo mode in which data actively transmitted onto the bus is not actively filtered from the reception.

After each transmission I am therefore able to wait for the expected echo to appear as a round-trip confirmation. In addition, this serves to detect certain faults such as a short-circuited bus.

doynax
  • 4,285
  • 3
  • 23
  • 19