FTDI Chipsets

FTDI chipsets behave reasonably well. There are however some minor behavioural issues.

Reading the EOF Character

According to MSDN, the fBinary field must always be 1. If this is set, then FTDI documentation states that the EOF character is ignored. This is not quite the case.

My first test case was to send data from one UART to a FTDI UART consisting of 128KB of random data. As data is being sent at a rate of 115200, a SerialPortStream.ReadTimeout of 100ms should suffice to know when the 128KB of data has been sent.

What I observed was that a timeout of 100ms or greater was occuring during the read operation. This caused the test program to think all data was received. We didn't receive all data that was expected. The last byte to be received was the hexadecimal character 0x1A.

Interestingly, to keep compatibility of the MS implementation, we set the EventChar and EofChar to be 0x1A. By changing the EofChar to something else, the behaviour would change such that the timeout would occur on the new EofChar.

So the FTDI driver does something interesting when the EofChar arrives. It pauses the data in the input stream. A timeout of 250ms appears to overcome this issue, so the timeout is less than 250ms, but more than 200ms.

I would have expected there to be no interpretation of the EofChar as the fBinary field of the DCB is always set to 1.

PL2303 (Prolific) Chipsets

The Prolific chipsets (tested the SiteCom CN-116) shows restrictive behaviour, which the SerialPortStream has workarounds for. Some of the workarounds provide minor invonveniences, described in the following sections. Versions of the PL2303 driver tested are:
  • Prolific Driver Win 7 x86 & x64; Date 26.07.2012; Version 3.4.36.247
  • Prolific Driver Win 7 x86; Date 31.07.2007; Version 3.2.0.0
  • Prolific Driver Win 7 x86; Date 03.08.2005; Version 2.0.0.19 (unsigned)

Hardware Flow Control

The PL2303 doesn't properly set the RTS flag. The software was modified, so that the I/O thread loop is put to sleep for 1000ms after each event. This should simulate an extreme case allowing the driver buffer to fill up quickly. By using another program, such as ZOC or TeraTerm to send data from an FTDI chipset to the PL2303, we observe that 4092 bytes are sent. Data is paused.

It would be expected that once the timeout loop has completed, the next ReadFile() would occur (after a EV_RXCHAR event), clearing out the buffer, allowing the driver to deassert the RTS line. Then the sender program can continue sending the next chunk of data.

What is observed is the sending program (ZOC or TeraTerm) does not resume sending data.

Investigations

Reversing Roles

Reversing the roles, so that the PL2303 sends data and the FTDI receives the data, the test case works as expected. Approximately 4000 bytes of data is sent, there is a pause, on the expiry of the 1000ms sleep call, the sending terminal sends another 4000 byte block of data.

This continues until all the data is sent affirming that hardware flow control works.

Sniffing Programs

Using some trial and freeware sniffing programs which hook into the Win32 API, we see that the PL2303 driver buffer is empty and that no EV_RXCHAR event occurs. The driver isn't receiving data.

Sending Flow Control disabled

If we disable flow control from the sending application, then data is sent and the SerialPortStream is able to continue receiving data. overflows occur in the input stream, as both sides must support flow control, but this tells us that the CTS is being held by the PL2303 incorrectly, which the sender is actually honouring. We see the EV_RXCHAR which we didn't see otherwise. The error CE_OVERRUN is indicated by ClearCommError().

Using a 16550A UART

Just like the FTDI chipset, if we repeat the proper test case with the receiver being a 16550A UART (I still have one on my 6 year old laptop) then hardware flow control works as expected.

Investigating a Workaround

  • I tried to perform a ReadFile() periodically regardless of the EV_RXCHAR event, just in case that this wasn't being sent properly. No data was received. This confirms that the sending terminal thinks hardware flow control is active.
  • I tried to rewrite the DCB but this didn't change the behaviour.
  • I tried to write the RTS line explicitly. However, because hardware flow control is active, an error is returned (as expected from MSDN documentation)

Native.DCB m_Dcb = new Native.DCB();
Native.GetCommState(m_ComPortHandle, ref m_Dcb);
if ((m_Dcb.Flags & Native.DcbFlags.RtsControlMask) == Native.DcbFlags.RtsControlHandshake) {
  if (!Native.EscapeCommFunction(m_ComPortHandle, Native.ExtendedFunctions.SETRTS)) {
    Console.WriteLine("Couldn't set RTS: {0}", Marshal.GetLastWin32Error());
  }
}

Conclusion

When using a PL2303 based chipset for receiving data, one can't expect flow control to work. Make sure you don't enable flow control if you expect a PL2303 will be used to receive data. Try to increase the driver receive buffer to 8192 bytes (12288 might be the maximum). You have to ensure that the CPU is able to service the ReadFile() at least every 711ms (81920 / 115200). Your program doesn't need to access the buffer this often, it just needs to have enough CPU resources so that the I/O thread in SerialPortStream does.

Reading Data

The PL2303 chipset doesn't support all values available when using the SetCommTimeouts function. If the following settings are used:
  • ReadIntervalTimeout = -1
  • ReadTotalTimeoutConstant = 0
  • ReadTotalTimeoutMultipler = 0

one would expect that there is no asynchronous behaviour when using ReadFile(), it copies data from the driver buffer into the application buffer and returns immediately.

Indeed this is the case, but data corruption was observed (but not when using an FTDI chipset). The dataset sent is bytes from 0x01 to 0x4D in sequence and repeated. The corruption observed appears like interleaving between two buffers (I suspect a DMA problem?). The column Tx indicates data sent to the PL2303 (as per another UART). The column Rx indicates data returned by ReadFile().

Tx  Rx   | Tx  Rx   | Tx  Rx   | Tx  Rx   | Tx  Rx
---------+----------+----------+----------+----------
20  20   | 34  34   | 48  48   | 0F  4D   | 23  14
21  21   | 35  26   | 49  3A   | 10  01   | 24  15
22  22   | 36  27   | 4A  3B   | 11  02   | 25  25
23  23   | 37  37   | 4B  4B   | 12  12   | 26  17
24  24   | 38  38   | 4C  3D   | 13  04   | 27  18
25  16   | 39  2A   | 4D  3E   | 14  05   | 28  28
26  17   | 3A  2B   | 01  01   | 15  15   | 29  1A
27  27   | 3B  3B   | 02  40   | 16  07   | 2A  1B
28  19   | 3C  2D   | 03  41   | 17  08   | 2B  1C
29  1A   | 3D  2E   | 04  04   | 18  18   | 2C  2C
2A  2A   | 3E  3E   | 05  43   | 19  0A   | 2D  1E
2B  1C   | 3F  30   | 06  44   | 1A  1A   | 2E  1F
2C  1D   | 40  31   | 07  45   | 1B  1B   | 2F  2F
2D  1E   | 41  41   | 08  08   | 1C  0D   | 30  21
2E  2E   | 42  33   | 09  47   | 1D  0E   | 31  22
2F  20   | 43  34   | 0A  48   | 1E  1E   | 32  32
30  21   | 44  44   | 0B  0B   | 1F  10   | 33  24
31  31   | 45  36   | 0C  4A   | 20  11   | 34  25
32  23   | 46  37   | 0D  4B   | 21  12   | 35  35
33  24   | 47  38   | 0E  0E   | 22  22   | 36  27

Solution to Reading Data

It was found, that if we change the timeout parameters, we can configure the PL2303 driver to correctly receive data.
  • ReadIntervalTimeout = -1
  • ReadTotalTimeoutConstant = 100
  • ReadTotalTimeoutMultiplier = 0

No corruption occurs! This has a minor impact for high performance applications. With this workaround, one cannot expect to receive data any faster than 100ms. So a ReadTimeout parameter of less than 100ms is likely to cause TimeoutExceptions.

The codebase has a define PL2303_WORKAROUNDS. If you modify the code to undefine this field, we use the original settings of 0ms, so no asynchronous read operations occur.

Perhaps this is a condition of the documentation for ReadFile which states:
lpNumberOfBytesRead [out, optional]: A pointer to the variable that receives the number of bytes read when using a synchronous hFile parameter. ReadFile sets this value to zero before doing any work or error checking. Use NULL for this parameter if this is an asynchronous operation to avoid potentially erroneous results.
Not all ReadFile() invocations will be asynchronous and if true is returned, we have no way of knowing how many bytes were actually read. The SerialPortStream actually provides a pointer, because it is observed that not every ReadFile() operation is asynchronous.

Writing and the EV_TXEMPTY event

In a simple test where 128KB of random data is sent from a Prolific adapter, the WriteFile() events occur generally before the WaitCommEvent() with the flag EV_TXEMPTY. I would have expected this to be generated only once, as observed by the FTDI driver. An example of the output:

IO.Ports.SerialPortStream Verbose: 3224 : SerialThread: ProcessWaitCommEvent: EV_CTS, EV_DSR, EV_RLSD
IO.Ports.SerialPortStream Verbose: 3224 : SerialThread: DoWriteEvent: WriteFile(3224, 572391552, 131072, ...) == False
IO.Ports.SerialPortStream Verbose: 3224 : SerialThread: ProcessWriteEvent: 12032 bytes
IO.Ports.SerialPortStream Verbose: 3224 : SerialThread: DoWriteEvent: WriteFile(3224, 572403584, 119040, ...) == False
IO.Ports.SerialPortStream Verbose: 3224 : SerialThread: ProcessWriteEvent: 11776 bytes
IO.Ports.SerialPortStream Verbose: 3224 : SerialThread: DoWriteEvent: WriteFile(3224, 572415360, 107264, ...) == False
IO.Ports.SerialPortStream Verbose: 3224 : SerialThread: ProcessWriteEvent: 11776 bytes
IO.Ports.SerialPortStream Verbose: 3224 : SerialThread: DoWriteEvent: WriteFile(3224, 572427136, 95488, ...) == False
IO.Ports.SerialPortStream Verbose: 3224 : SerialThread: ProcessWriteEvent: 11776 bytes
IO.Ports.SerialPortStream Verbose: 3224 : SerialThread: DoWriteEvent: WriteFile(3224, 572438912, 83712, ...) == False
IO.Ports.SerialPortStream Verbose: 3224 : SerialThread: ProcessWriteEvent: 11776 bytes
IO.Ports.SerialPortStream Verbose: 3224 : SerialThread: DoWriteEvent: WriteFile(3224, 572450688, 71936, ...) == False
IO.Ports.SerialPortStream Verbose: 3224 : SerialThread: ProcessWriteEvent: 11776 bytes
IO.Ports.SerialPortStream Verbose: 3224 : SerialThread: DoWriteEvent: WriteFile(3224, 572462464, 60160, ...) == False
IO.Ports.SerialPortStream Verbose: 3224 : SerialThread: ProcessWriteEvent: 11776 bytes
IO.Ports.SerialPortStream Verbose: 3224 : SerialThread: DoWriteEvent: WriteFile(3224, 572474240, 48384, ...) == False
IO.Ports.SerialPortStream Verbose: 3224 : SerialThread: ProcessWriteEvent: 11776 bytes
IO.Ports.SerialPortStream Verbose: 3224 : SerialThread: DoWriteEvent: WriteFile(3224, 572486016, 36608, ...) == False
IO.Ports.SerialPortStream Verbose: 3224 : SerialThread: ProcessWriteEvent: 11776 bytes
IO.Ports.SerialPortStream Verbose: 3224 : SerialThread: DoWriteEvent: WriteFile(3224, 572497792, 24832, ...) == False
IO.Ports.SerialPortStream Verbose: 3224 : SerialThread: ProcessWriteEvent: 11776 bytes
IO.Ports.SerialPortStream Verbose: 3224 : SerialThread: DoWriteEvent: WriteFile(3224, 572509568, 13056, ...) == False
IO.Ports.SerialPortStream Verbose: 3224 : SerialThread: ProcessWriteEvent: 11776 bytes
IO.Ports.SerialPortStream Verbose: 3224 : SerialThread: DoWriteEvent: WriteFile(3224, 572521344, 1280, ...) == False
IO.Ports.SerialPortStream Verbose: 3224 : SerialThread: ProcessWriteEvent: 1280 bytes
IO.Ports.SerialPortStream Verbose: 3224 : SerialThread: ProcessWaitCommEvent: EV_TXEMPTY
IO.Ports.SerialPortStream Verbose: 3224 : SerialThread: ProcessWaitCommEvent: EV_TXEMPTY
IO.Ports.SerialPortStream Verbose: 3224 : SerialThread: ProcessWaitCommEvent: EV_TXEMPTY
IO.Ports.SerialPortStream Verbose: 3224 : SerialThread: ProcessWaitCommEvent: EV_TXEMPTY
IO.Ports.SerialPortStream Verbose: 3224 : SerialThread: ProcessWaitCommEvent: EV_TXEMPTY
IO.Ports.SerialPortStream Verbose: 3224 : SerialThread: ProcessWaitCommEvent: EV_TXEMPTY
IO.Ports.SerialPortStream Verbose: 3224 : SerialThread: ProcessWaitCommEvent: EV_TXEMPTY
IO.Ports.SerialPortStream Verbose: 3224 : SerialThread: ProcessWaitCommEvent: EV_TXEMPTY
IO.Ports.SerialPortStream Verbose: 3224 : SerialThread: ProcessWaitCommEvent: EV_TXEMPTY
IO.Ports.SerialPortStream Verbose: 3224 : SerialThread: ProcessWaitCommEvent: EV_TXEMPTY
IO.Ports.SerialPortStream Verbose: 3224 : SerialThread: ProcessWaitCommEvent: EV_TXEMPTY
IO.Ports.SerialPortStream Verbose: 3224 : SerialThread: ProcessWaitCommEvent: EV_TXEMPTY
IO.Ports.SerialPortStream Verbose: 3224 : OverlappedIO: Stopping Thread
IO.Ports.SerialPortStream Verbose: 3224 : OverlappedIO: Waiting for Thread
IO.Ports.SerialPortStream Verbose: 3224 : SerialThread: Thread closing
IO.Ports.SerialPortStream Verbose: 3224 : OverlappedIO: Thread Stopped


The SerialPortStream relies on the EV_TXEMPTY event to know when data has finished writing as part of the Flush() method. The EV_TXEMPTY event is only used in case the .NET write buffer is empty which should minimise any errors.

The line DoWriteEvent: WriteFile(3224, ptr, len, ...) == False indicates that the operation is asynchronous. So when the asynchronous operation is finished, the method ProcessWriteEvent() is called.

If we start a new WriteFile() operation, either a ProcessWaitCommEvent: EV_TXEMPTY would occur before the next WriteFile() (the event for WaitCommEvent() is before all other events, so WaitForMultipleObjects() will return this instead of other events if it's triggered), or not at all, occurring only at the end.

Note the behaviour is independent of the WriteTotalTimeoutConstant (tested 0ms and 500ms), with a WriteTotalTimeoutMultiplier of 0ms.

Last edited Sep 23, 2012 at 12:55 PM by jmcurl, version 1

Comments

No comments yet.