Slow data transfer rates with usb-rs232 serial adapters

For some years now I’ve been happily using the CP2102-based usb-serial adapters from Pololu,
but have not tried to use them at high data rates. Now, I have an application that requires continuous serial transfers from a Windows PC to a micro at 115200 baud.

It works fine if I use a standard RS232 serial port, but when I switch to the usb adapter, the byte transfer rate slows down by a factor of about 10. In fact no characters are lost, but I observe about the same byte transfer rate if I set the baud rate to 9600 or 115200! In either case 800-1000 characters/sec are being output to the micro.

Slow data transfer is seen using both Win XP and Win 7 Professional. (I had no problem using the SiLabs signed device drivers on Win7). At first I thought the usb poll rate might be the issue, but I have experimentally verified that the usb poll rate is 1000Hz on a Win 7 machine and with a 16 byte data buffer, the device should be capable of transferring about 16000 characters/second. As a last resort, I tried an FTDI 232R-based usb-serial adapter, which has completely different usb driver code, and got the same result.

Extensive googling has not shed light on why these adapters can’t provide more than about 1,000 characters/second, regardless of the port bit rate. Has anyone else noted this?

Incidentally, I’m using a tried-and-true standard C program on the PC end, with the Microsoft-recommended serial port open, close, read & write procedures; see msdn.microsoft.com/en-us/library/ms810467.aspx

Thanks for any insight you can offer, Jim

Hello,

It sounds you are having a problem that nobody else is having, and it is independent of your hardware and operating system. It should be obvious to you by now that the problem lies in your code.

So, you are basically asking us to debug your code, but you are not providing that code here for us to look at. How are we supposed to help you? You should first simplify it to the simplest thing that demonstrates the problem, then post the full code here for us to see.

-Paul

Paul:

Thanks for the response. I very much appreciate your willingness to look at the code, but all I actually asked is whether anyone had observed slow data transfer over USB, using the usb-serial adapter, as opposed to a standard serial port. The exact same code is used in both instances, with very different results.

Following is the code for the PC side, simplified about as much as possible. It does in principle do overlapped read/write IO and I haven’t yet turned that off, because it works so well on a standard serial port. Since it transmits only, nothing is required on the uP side. I use CODE::BLOCKS as the development platform and the executable runs on Win98, WinXP and Win7.

I certainly don’t expect anyone to debug this, but if something jumps out, fantastic!

Best regards, Jim

//
// serial com port transmit & receive
//

#include <conio.h>
#include <stdio.h>
#include <time.h>
#include <windows.h>
#include <string.h>
#include <math.h>

HANDLE SerialInit(char*, int);
char SerialGetc(HANDLE*);
void SerialPutc(HANDLE*, unsigned char);
void MySleep(int mSec);

// variables used with the com port

BOOL bPortReady;
DCB dcb;
COMMTIMEOUTS CommTimeouts;
BOOL bWriteRC;
BOOL bReadRC;
DWORD iBytesWritten;
DWORD dwRead;
HANDLE SerPrt = INVALID_HANDLE_VALUE;
OVERLAPPED osReader = {0};
DWORD dwEvtMask;
BOOL fWaitingOnRead = FALSE;

#define READ_BUF_SIZE 16
char InBuf[READ_BUF_SIZE];
typedef unsigned int uint;

//

int main(int argc, char *argv[])
{
	int	i, tmp, cnt = 0;

// COMx definition
	int port = 5;  //must be set to suit

	unsigned int n = 0;
	unsigned int timeCnt = 0;
	unsigned int x;

	char SerialPortStr[] = "\\\\.\\COM0";
	int baudrate = 38400; //115200; 9600; 38400; 57600;

	SerialPortStr[7] = port + '0';
	printf("Using COM%c: at %d baud\n", port + '0', baudrate);
	SerPrt = SerialInit(SerialPortStr, baudrate);
	if (SerPrt == INVALID_HANDLE_VALUE) {
		printf("Unable to Open COM%c:\n", port + '0');
		return FALSE;
	}

        printf("Test character transmit speed \n");

//send characters as quickly as possible

        n = 0;
        timeCnt = GetTickCount();

// infinite loop for testing

        while(1) {
        for (i='a'; i<'a'+20; i++) {
        SerialPutc(&SerPrt, i); n++;
//        MySleep(0);

// about every second, output character count

            if ( (tmp = (x = GetTickCount()) - timeCnt) >= 1000) {
			printf("char cnt: %d ticks: %d\n", n, tmp);
			timeCnt = x; n=0;
            }
        }
        SerialPutc(&SerPrt,0x0D); SerialPutc(&SerPrt,0x0A); n+=2;
//        MySleep(0);
        }

	return 0;

}

HANDLE SerialInit(char *ComPortName, int BaudRate)
{
	HANDLE hCom;

	hCom = CreateFileA(ComPortName,
			GENERIC_READ | GENERIC_WRITE,
			0, // exclusive access
			NULL, // no security
			OPEN_EXISTING,
			FILE_FLAG_OVERLAPPED, // overlapped I/O
			NULL); // null template

	if (hCom == INVALID_HANDLE_VALUE) {
		DWORD err = GetLastError();
		char lpMsg[512];
		FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER |
			  FORMAT_MESSAGE_FROM_SYSTEM |
			  FORMAT_MESSAGE_IGNORE_INSERTS,
			  NULL, GetLastError(),
			  MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
			  (LPTSTR)&lpMsg, 0, NULL);
		return hCom;
	}

	bPortReady = SetupComm(hCom, 16, 128); // set buffer sizes


	bPortReady = GetCommState(hCom, &dcb);
	dcb.BaudRate = BaudRate;
	dcb.ByteSize = 8;
	dcb.Parity = NOPARITY;		// EVENPARITY;
	dcb.StopBits = ONESTOPBIT;
	dcb.fAbortOnError = TRUE;

	// set XON/XOFF
	dcb.fOutX = FALSE; // XON/XOFF off for transmit
	dcb.fInX = FALSE; // XON/XOFF off for receive
	// set RTSCTS
	dcb.fOutxCtsFlow = FALSE; // turn on CTS flow control
	dcb.fRtsControl = RTS_CONTROL_DISABLE; //
	// set DSRDTR
	dcb.fOutxDsrFlow = FALSE; // turn on DSR flow control
	dcb.fDtrControl = DTR_CONTROL_DISABLE; // DTR_CONTROL_HANDSHAKE;

	bPortReady = SetCommState(hCom, &dcb);

	// Communication timeouts are optional

	bPortReady = GetCommTimeouts (hCom, &CommTimeouts);

	CommTimeouts.ReadIntervalTimeout = 2500;
	CommTimeouts.ReadTotalTimeoutConstant = 2500;
	CommTimeouts.ReadTotalTimeoutMultiplier = 1000;
	CommTimeouts.WriteTotalTimeoutConstant = 2500;
	CommTimeouts.WriteTotalTimeoutMultiplier = 1000;

	bPortReady = SetCommTimeouts (hCom, &CommTimeouts);

	// Create Read event

	osReader.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);

	if (NULL == osReader.hEvent) {
		printf("SerialInit: failure to create reader hEvent.\n");
	}

	return hCom;
}

void SerialStartRead(void)
{
	int i;
	unsigned long err;
	if (!fWaitingOnRead) {
		// Issue Read Operation
		if (!ReadFile(SerPrt, InBuf, READ_BUF_SIZE, &dwRead, &osReader)) {
			err = GetLastError();
			if ((ERROR_IO_PENDING != err) && (ERROR_IO_INCOMPLETE != err)) {
				printf("SerialStartRead: ReadFile failed error: 0x%lx ", err);
				ClearCommError(SerPrt, &err, NULL);
				printf("detail: 0x%lx\n", err);
			} else {
				fWaitingOnRead = TRUE;
			}
		} else {
			//ReadFile finished
			for (i = 0; i < (int)dwRead; i++) printf("%2x ", InBuf[i]);
            fWaitingOnRead = FALSE;
			}
			fflush(stdout);
		}
	}

void SerialCheckRead(void)
{
	DWORD dwRes;
	int i;
	unsigned long err;

	if (fWaitingOnRead) {
		dwRes = WaitForSingleObject(osReader.hEvent, 0);	// don't wait
		switch (dwRes) {
		case WAIT_OBJECT_0:	// Read Completed
			if (!GetOverlappedResult(SerPrt, &osReader, &dwRead, FALSE)) {
				err = GetLastError();
				if ((ERROR_IO_PENDING != err) && (ERROR_IO_INCOMPLETE != err)) {
					printf("SerialCheckRead: GetOverlappedResult failed error: 0x%lx ", err);
					ClearCommError(SerPrt, &err, NULL);
					printf("detail: 0x%lx\n", err);
				} else {
					// Still waiting
				}
				break;
			}
			// success, dump characters to window for testing

			for (i = 0; i < (int)dwRead; i++) {
				printf("%2x ", InBuf[i]);
			}
			fflush(stdout);
			fWaitingOnRead = FALSE;		// ready for another operation
			break;
		case WAIT_TIMEOUT:
			// still waiting
			break;
		default:
			// error
			printf("SerialCheckRead: communication error: 0x%lx\n", dwRes);
		}
	}
}

void MySleep(int mSec)
{
	while (mSec > 10) {
		SerialStartRead();
		SerialCheckRead();
		Sleep(10);
		mSec -= 10;
	}
	SerialStartRead();
	SerialCheckRead();
	Sleep(mSec);
}


void SerialPutc(HANDLE *hCom, unsigned char txchar)
{
	BOOL bWriteRC;
	static DWORD iBytesWritten;
	OVERLAPPED osWrite = {0};
	DWORD dwRes;

	// Create an event object for write operation

    osWrite.hEvent = CreateEvent(
        NULL,   // default security attributes
        TRUE,   // manual-reset event
        FALSE,  // not signaled
        NULL    // no name
		);

	if (NULL == osWrite.hEvent) {
		printf("Error: unable to create Write Operation Event\n");
		return;
	}

	bWriteRC = WriteFile(*hCom, &txchar, 1, &iBytesWritten,&osWrite);
	if (!bWriteRC) {
		// Check if Write is pending.
		if (ERROR_IO_PENDING != GetLastError()) {
			printf("SerialPutc: unexpected return 0x%lx from WriteFile()\n", GetLastError());
			return;
		}
		dwRes = WaitForSingleObject(osWrite.hEvent, INFINITE);
		switch (dwRes) {
		case WAIT_OBJECT_0:
			if (0 == GetOverlappedResult(*hCom, &osWrite, &iBytesWritten, TRUE)) {
				printf("GetOverlappedResult failed with error 0x%lx.\n", GetLastError());
			} else {
				// success
			}
			break;
		default:
			printf("WaitForSingleObject failed with error 0x%lx.\n", dwRes);
			break;
		}
	} else {
		// WriteFile() returned TRUE
		DWORD err = GetLastError();	//returns 995, Operation Aborted
	}
	CloseHandle(osWrite.hEvent);

// transmit speed does not depend on the following calls:
	SerialStartRead();
	SerialCheckRead();
}

Hello,

Are you hoping for answers along the lines of “make sure not to use function X, because that will slow it down”? There are probably a thousand ways to make Windows serial code run too slowly, so it seems like a big waste of time to have such a non-specific discussion without just looking at your code to see whether you, in fact, did use function X. Thanks for posting the actual code.

Sure, nobody is going to want to actually run and debug your code, but the simpler you can make it, the more likely it is that something will jump out at us. For example, you have receiving, sleeping, and error handling code in there right now, none of which is at all relevant to the problem of slow transmission. Commented lines of code and empty comments could obviously be removed to make it easier to read.

Maybe someone else who knows about Windows programming will chime in, and once you simplify the code it will be easier to see, but it seems like you are using the serial port in an extremely inefficient way: you create a new event object for every byte, and you wait for that event to finish before proceeding. That seems almost guaranteed to be slow because it requires lots of context switching and prevents the OS from doing any kind of buffering. I think the correct way to use the functions you are using would be to put as many bytes as possible into an event, and to try to have multiple events constantly queued up so that the OS can constantly be doing work for you.

By the way, why are you trying to do things in this complicated way instead of just opening the serial port as a file and using functions like fprintf() and fscanf()?

-Paul

Hello.

Paul’s suggestion of using fprintf would probably work and be more efficient. Another thing you should look at is our example serial Windows C code for the Maestro, which is in the “Serial Example Code > Windows C” section of the Maestro User’s Guide.

That code shows how to write multiple bytes at a time using WriteFile. I think there is no reason to call WriteFile in a non-blocking way like you are doing unless your application is going to do something useful during the time that the bytes are being transmitted, so I would recommend just calling WriteFile in a non-blocking way with the last argument NULL, as we show in the example code.

If that’s not good enough for you, it is possible that using fopen/fprintf/fwrite or queuing up multiple WriteFile requests at a time would be faster.

–David

Paul and David:

Thanks for your helpful responses.

By way of explanation, when I started this project I just adopted some standard code as recommended by Microsoft. Since that code does transfer data at over 10,000 characters per second out the standard serial port, I didn’t worry about the unnecessary complexity or inefficient coding.

I’ll try the completely different methods that you suggested. Case closed!

Yeah, it is strange that your code was able to send bytes so quickly over normal RS-232. It looks like it should be really inefficient. Did Microsoft actually present an example where they send one byte at a time? The link you initially provided seemed to mostly be basic function documentation.

-Paul

Also, I am sorry for replying so harshly to your first post, Jim! I re-read it and do not know why I was originally annoyed by your question: it looks totally reasonable to me now. I did not realize that you were asking for some confirmation that the various USB-serial devices can transmit under Windows at more than ~10kbps - could you try sending data using a standard terminal program to verify that it is achievable on your system?

-Paul

Thanks, gentlemen. Your comments led to the solution. The single-character output was the problem. Buffering 16 characters before output leads to a character data rate of roughly 12000/second at 115200 baud and is the same on both usb-serial and standard comm ports.

Previously, despite setting the serial port driver internal buffer size to the maximum of 16 characters (on windows machines), the usb driver was apparently delivering only one character per poll cycle (1000 Hz).

If anyone is interested, the revised test code (which has the advantage of overlapped, bidirectional IO) follows. I didn’t provide a way to flush the final output buffer upon program termination, because that doesn’t matter for my application.

//
// serial com port transmit & receive
//

#include <conio.h>
#include <stdio.h>
#include <time.h>
#include <windows.h>
#include <string.h>
#include <math.h>

HANDLE SerialInit(char*, int);
char SerialGetc(HANDLE*);
void SerialPutc(HANDLE*, unsigned char);
void MySleep(int mSec);

// variables used with the com port

BOOL bPortReady;
DCB dcb;
COMMTIMEOUTS CommTimeouts;
BOOL bWriteRC;
BOOL bReadRC;
DWORD iBytesWritten;
DWORD dwRead;
HANDLE SerPrt = INVALID_HANDLE_VALUE;
OVERLAPPED osReader = {0};
DWORD dwEvtMask;
BOOL fWaitingOnRead = FALSE;

#define READ_BUF_SIZE 16
#define WRITE_BUF_SIZE 16
char InBuf[READ_BUF_SIZE];
char WriteBuf[WRITE_BUF_SIZE];
typedef unsigned int uint;

//

int main(int argc, char *argv[])
{
	int	i, tmp, cnt = 0;

// COMx definition
	int port = 5;  //must be set to suit

	unsigned int n = 0;
	unsigned int timeCnt = 0;
	unsigned int x;

	char SerialPortStr[] = "\\\\.\\COM0";
	int baudrate = 115200; //115200; 9600; 38400; 57600;

	SerialPortStr[7] = port + '0';
	printf("Using COM%c: at %d baud\n", port + '0', baudrate);
	SerPrt = SerialInit(SerialPortStr, baudrate);
	if (SerPrt == INVALID_HANDLE_VALUE) {
		printf("Unable to Open COM%c:\n", port + '0');
		return FALSE;
	}

        printf("Test character transmit speed \n");

//send characters as quickly as possible

        n = 0;
        timeCnt = GetTickCount();

// infinite loop for testing

        while(1) {
        for (i='a'; i<'a'+20; i++) {
        SerialPutc(&SerPrt, i); n++;

// about every second, output character count

            if ( (tmp = (x = GetTickCount()) - timeCnt) >= 1000) {
			printf("char cnt: %d ticks: %d\n", n, tmp);
			timeCnt = x; n=0;
            }
        }
        SerialPutc(&SerPrt,0x0D); SerialPutc(&SerPrt,0x0A); n+=2;
        }

	return 0;

}

HANDLE SerialInit(char *ComPortName, int BaudRate)
{
	HANDLE hCom;

	hCom = CreateFileA(ComPortName,
			GENERIC_READ | GENERIC_WRITE,
			0, // exclusive access
			NULL, // no security
			OPEN_EXISTING,
			FILE_FLAG_OVERLAPPED, // overlapped I/O
			NULL); // null template

	if (hCom == INVALID_HANDLE_VALUE) {
		DWORD err = GetLastError();
		char lpMsg[512];
		FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER |
			  FORMAT_MESSAGE_FROM_SYSTEM |
			  FORMAT_MESSAGE_IGNORE_INSERTS,
			  NULL, GetLastError(),
			  MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
			  (LPTSTR)&lpMsg, 0, NULL);
		return hCom;
	}

	bPortReady = SetupComm(hCom, 16, 128); // set buffer sizes


	bPortReady = GetCommState(hCom, &dcb);
	dcb.BaudRate = BaudRate;
	dcb.ByteSize = 8;
	dcb.Parity = NOPARITY;		// EVENPARITY;
	dcb.StopBits = ONESTOPBIT;
	dcb.fAbortOnError = TRUE;

	// set XON/XOFF
	dcb.fOutX = FALSE; // XON/XOFF off for transmit
	dcb.fInX = FALSE; // XON/XOFF off for receive
	// set RTSCTS
	dcb.fOutxCtsFlow = FALSE; // turn on CTS flow control
	dcb.fRtsControl = RTS_CONTROL_DISABLE; //
	// set DSRDTR
	dcb.fOutxDsrFlow = FALSE; // turn on DSR flow control
	dcb.fDtrControl = DTR_CONTROL_DISABLE; // DTR_CONTROL_HANDSHAKE;

	bPortReady = SetCommState(hCom, &dcb);

	// Communication timeouts are optional

	bPortReady = GetCommTimeouts (hCom, &CommTimeouts);

	CommTimeouts.ReadIntervalTimeout = 2500;
	CommTimeouts.ReadTotalTimeoutConstant = 2500;
	CommTimeouts.ReadTotalTimeoutMultiplier = 1000;
	CommTimeouts.WriteTotalTimeoutConstant = 2500;
	CommTimeouts.WriteTotalTimeoutMultiplier = 1000;

	bPortReady = SetCommTimeouts (hCom, &CommTimeouts);

	// Create Read event

	osReader.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);

	if (NULL == osReader.hEvent) {
		printf("SerialInit: failure to create reader hEvent.\n");
	}

	return hCom;
}

void SerialStartRead(void)
{
	int i;
	unsigned long err;
	if (!fWaitingOnRead) {
		// Issue Read Operation
		if (!ReadFile(SerPrt, InBuf, READ_BUF_SIZE, &dwRead, &osReader)) {
			err = GetLastError();
			if ((ERROR_IO_PENDING != err) && (ERROR_IO_INCOMPLETE != err)) {
				printf("SerialStartRead: ReadFile failed error: 0x%lx ", err);
				ClearCommError(SerPrt, &err, NULL);
				printf("detail: 0x%lx\n", err);
			} else {
				fWaitingOnRead = TRUE;
			}
		} else {
			//ReadFile finished
			for (i = 0; i < (int)dwRead; i++) printf("%2x ", InBuf[i]);
            fWaitingOnRead = FALSE;
			}
			fflush(stdout);
		}
	}

void SerialCheckRead(void)
{
	DWORD dwRes;
	int i;
	unsigned long err;

	if (fWaitingOnRead) {
		dwRes = WaitForSingleObject(osReader.hEvent, 0);	// don't wait
		switch (dwRes) {
		case WAIT_OBJECT_0:	// Read Completed
			if (!GetOverlappedResult(SerPrt, &osReader, &dwRead, FALSE)) {
				err = GetLastError();
				if ((ERROR_IO_PENDING != err) && (ERROR_IO_INCOMPLETE != err)) {
					printf("SerialCheckRead: GetOverlappedResult failed error: 0x%lx ", err);
					ClearCommError(SerPrt, &err, NULL);
					printf("detail: 0x%lx\n", err);
				} else {
					// Still waiting
				}
				break;
			}
			// success, dump characters to window for testing

			for (i = 0; i < (int)dwRead; i++) {
				printf("%2x ", InBuf[i]);
			}
			fflush(stdout);
			fWaitingOnRead = FALSE;		// ready for another operation
			break;
		case WAIT_TIMEOUT:
			// still waiting
			break;
		default:
			// error
			printf("SerialCheckRead: communication error: 0x%lx\n", dwRes);
		}
	}
}

void MySleep(int mSec)
{
	while (mSec > 10) {
		SerialStartRead();
		SerialCheckRead();
		Sleep(10);
		mSec -= 10;
	}
	SerialStartRead();
	SerialCheckRead();
	Sleep(mSec);
}


void SerialPutc(HANDLE *hCom, unsigned char txchar)
{
	BOOL bWriteRC;
	static DWORD iBytesWritten;
	OVERLAPPED osWrite = {0};
	DWORD dwRes;
	static int n=0;

	if (n<WRITE_BUF_SIZE) {
	WriteBuf[n]=txchar;
	n++;
	}
    else {

	// Create an event object for write operation

    osWrite.hEvent = CreateEvent(
        NULL,   // default security attributes
        TRUE,   // manual-reset event
        FALSE,  // not signaled
        NULL    // no name
		);

	if (NULL == osWrite.hEvent) {
		printf("Error: unable to create Write Operation Event\n");
		return;
	}

	bWriteRC = WriteFile(*hCom, &WriteBuf, n, &iBytesWritten,&osWrite);

	n=0;

	if (!bWriteRC) {
		// Check if Write is pending.
		if (ERROR_IO_PENDING != GetLastError()) {
			printf("SerialPutc: unexpected return 0x%lx from WriteFile()\n", GetLastError());
			return;
		}
		dwRes = WaitForSingleObject(osWrite.hEvent, INFINITE);
		switch (dwRes) {
		case WAIT_OBJECT_0:
			if (0 == GetOverlappedResult(*hCom, &osWrite, &iBytesWritten, TRUE)) {
				printf("GetOverlappedResult failed with error 0x%lx.\n", GetLastError());
			} else {
				// success
			}
			break;
		default:
			printf("WaitForSingleObject failed with error 0x%lx.\n", dwRes);
			break;
		}
	} else {
		// WriteFile() returned TRUE
		DWORD err = GetLastError();	//returns 995, Operation Aborted
	}
	CloseHandle(osWrite.hEvent);

// transmit speed does not depend on the following calls:
	SerialStartRead();
	SerialCheckRead();
	}
}

I came back to this project after discovering that the code I posted aboce drops characters from time to time. I now do not recommend the CreateObject approach, and have adopted the approach recommended by the Pololu engineers.

That is, I based my (now completely successful!) code on the Maestro programming example for Windows, which uses CreateFile and WriteFile. It is still slower to transfer characters via the USB-serial adapter than with the standard serial port, but the rate is acceptable.

Thanks for the suggestions!