Contents
Introduction
As the title implies, this article is a continuation of the article Improving the Performance of Serial Ports Using C#.
These articles describe a number of simple test programs designed to demonstrate performance issues with the .NET serial port interface and what might be done to improve things.
In most practical cases, the serial port is used to facilitate control of a device by a PC. However, to avoid unnecessary complexities, the test setups
devised in these articles use a PC at both ends of the serial link.
In the previous article, I eventually resorted to using the C programming language to improve performance at the device end. Not surprisingly then, most of the text of this article
deals with C and CLR C++. However, the end game is a Windows device interface written predominantly if not entirely in C#.
Recap
In the previous article, the best results were achieved using the RemoteTimeReaderC program on the remote computer with the LocalTimeReaderGT program on the local computer.
The RemoteTimerReaderC program was written in C. The LocalTimeReaderGT program was written in C# with separate process threads for sending and receiving. Each of these
threads passed data to the rest of the program using a thread-safe queue.
A result similar to that shown below was obtained.
In the context of this article, the most relevant observation is the round trip interval between Local Time A
and Local Time B. The first round trip took 828 ms. The last round trip took about 8 seconds.
Further Observations
Before running the test with the RemoteTimeReaderC program and the LocalTimeReaderGT program again, roughly synchronize the clocks on the two connected computers. Once again, set the number
of threads to 10, the number of samples to 50, the sample interval to 50, and press Run. A result similar to that shown below should be obtained.
It can be seen that most of the 8 seconds identified previously occurs between Local Time A and Remote Time.
It can also be observed that the data collection visually appears to occur in bursts.
To account for this behaviour and cut a very long story short, refer to the section of code from the RemoteTimeReaderC program shown below.
while(true)
{
char buffer[1000];
DWORD readSize;
if (ReadFile(hSerialPort, buffer, 1000, &readSize, NULL) != 0)
{
for (int iii=0; iii<1000 && iii<(int)readSize; iii++)
{
.
.
.
.
if (messageIndex == 10)
{
messageIndex = 0;
if (!SendInfoMessage(hSerialPort, receivedMessage))
return 0;
}
}
}
}
Once inside the infinite loop, execution will wait at the ReadFile
command until the one second timeout occurs or until the buffer is full. Given the volume of data being sent,
the buffer probably fills fairly quickly, with control falling through to the for
loop to loop 1000 times. On every tenth loop, a 10 character message is completed and the
SendInfoMessage
is called to send back an 18 character message in response. Now the SendInfoMessage
method contains a WriteFile
command and execution
will actually wait on that WriteFile
command until the 18 character message is actually sent.
Thus once the ReadFile
command has read 1000 characters, control will move to the for
loop until all of the 100 10 character messages have been processed.
This means that the ReadFile
command will not be called again until 100 18 character messages have actually been sent.
Remote Time Reader Mark 3
To address these issues, the RemoteTimeReaderC program was restructured in line with the strategy used at the local end with
the LocalTimeReaderGT program where the sending and receiving processes occur in separate threads. Additionally, the buffer was reduced to one character.
The infinite loop previously described now looks like the following.
while(true)
{
char buffer;
DWORD readSize;
if (ReadFile(hSerialPort, &buffer, 1, &readSize, &osRead) == 0)
{
.
.
.
}
if (readSize != 0)
{
.
.
.
.
if (messageIndex == 10)
{
messageIndex = 0;
if (!SendInfoMessage(hSerialPort, receivedMessage))
return 0;
}
}
}
However, the SendInfoMessage
method no longer contains a WriteFile
command, instead it calls the StartSendMessage
method that looks like the following:
void StartSendMessage(Message message)
{
EnterCriticalSection(&criticalSection);
sendQueue.push(message);
LeaveCriticalSection(&criticalSection);
SetEvent(hQueueEvent);
}
The variable sendQueue
is a Standard Template Library (STL) queue that queues variables of type Message
. The type Message
is defined as follows:
typedef struct
{
int len;
char data[18];
}Message;
Because STL queues are not thread-safe, queue operations must be performed within critical sections. An event is set when a message is placed in the queue.
There is, of course, a corresponding method called SendMessageThread
that takes messages off the queue and calls the WriteFile
command to send them.
This method is initiated using a _beginthread
command within _tmain
before the infinite loop. The SendMessageThread
method is shown in part below.
while(true)
{
while(!sendQueue.empty())
{
Message message;
EnterCriticalSection(&criticalSection);
memcpy(&message, &((Message&)sendQueue.front()), sizeof(Message));
sendQueue.pop();
LeaveCriticalSection(&criticalSection);
DWORD bytesSent;
if (WriteFile(hSerialPort, message.data,
message.len, &bytesSent, &osWrite) == 0)
{
.
.
.
}
.
.
.
}
WaitForSingleObject(hQueueEvent, INFINITE);
ResetEvent(hQueueEvent);
}
Here too the queue operations are performed within a critical section. However, it is important that the WriteFile
command occurs outside the critical section so that
other process threads are not held up while this command sends its data. When the queue is empty, execution waits on the WaitForSingleObject
command.
Alas, this is not the end of the story because so far the C/C++ code used in these articles has used non-overlapped I/O. The best article I have been able
to find on the subject of overlapped and non-overlapped I/O is Serial Communications
in Win32 by Allen Denver. In this article, regarding non-overlapped I/O, Denver states "..if one thread were waiting for a ReadFile function to return, any other thread that
issued a WriteFile function would be blocked." So even with a multithreaded structure, the reading and writing of data to the serial port cannot happen simultaneously.
Fortunately, giving my code an overlapped makeover did not prove all that difficult.
Firstly, the CreateFile
command near the beginning of _tmain
was changed as shown below:
hSerialPort = CreateFile(_T("COM1"),
GENERIC_READ | GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
FILE_FLAG_OVERLAPPED,
NULL);
Then the following two lines were added just before the infinite loop:
OVERLAPPED osRead = {0};
osRead.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
Finally, the code around the ReadFile
and WriteFile
commands was changed. The code around the ReadFile
command now looks like the following:
if (ReadFile(hSerialPort, &buffer, 1, &readSize, &osRead) == 0)
{
if (GetLastError()!= ERROR_IO_PENDING)
{
cprintf("Read Error\n");
break;
}
else
GetOverlappedResult(hSerialPort, &osRead, &readSize, TRUE);
}
Note here that the fifth parameter of the ReadFile
command is now a pointer to the OVERLAPPED
structure labeled osRead
. Note also that the
GetOverlappedResult
command used here has its fourth parameter (bWait
) set to TRUE
. With this code, execution no longer waits at the
ReadFile
command. Instead it typically moves straight through to the GetOverlappedResult
command and execution waits there until data is received.
The WriteFile
command was changed in a similar manner. The following two lines were added near the beginning of the SendMessageThread
method:
OVERLAPPED osWrite = {0};
osWrite.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
The code around the WriteFile
command was changed as shown below:
if (WriteFile(hSerialPort, message.data, message.len, &bytesSent, &osWrite) == 0)
{
if (GetLastError() != ERROR_IO_PENDING)
{
cprintf("Failed to send message\n");
return;
}
else
GetOverlappedResult(hSerialPort, &osWrite, &bytesSent, TRUE);
}
The fifth parameter of the the WriteFile
command is also a pointer to an OVERLAPPED
structure. Once again, the fourth parameter
of the GetOverlappedResult
command is set to TRUE
.
Fourth Test Results
To test the new code, run the RemoteTimeReaderGTSC program on the remote computer. Once the RemoteTimeReaderGTSC program
is running, start the LocalTimeReaderGT program on the local computer and press Connect.
Once again, when everything looks good, crack things up a bit. Set the number of threads to 10, the number of samples
to 50, the sample interval to 50, and press Run. A result similar to that shown below should be obtained.
The results here look very good indeed with the first round trip taking only 62 ms! The last round trip now takes just over 6 seconds, but it should be noted that because
the sample interval is set to 50 ms, new messages may be sent before responses to previous messages have been received.
If the sample interval is increased so that there is no overlap between the sending of a new message and the receiving of the previous message, then round trip times remain very low.
Local Time Reader Mark 3
Buoyed by success, one might be tempted, as I was, to go to the next logical step and develop a FastSerialPort class library in CLR C++ for the local computer running the C#
coded Windows interface. However, I can report that for this exercise at least, there are no significant performance gains to be had from this approach.
In some cases, use of a CLR C++ class library for serial port access might be beneficial. One might, for example, be receiving large amounts of data for display with correspondingly
little data being sent. In this case, a large input buffer might facilitate a more readable visual display. The FastSerialPort class library and the LocalTimeReaderGTS
program that uses it have been included in this article for those cases.
The FastSerialPort code is, of course, quite similar to the RemoteTimeReaderGTSC program code, with some changes that allow it to be inherited by C# classes.
The first change being the splitting up of _tmain
into the OpenPort
method that contains serial port initialization and the ReceiveMessages
method that
contains the infinite message receiving loop.
Perhaps the only noteworthy difference with the OpenPort
method is the use of C# friendly variables as shown below:
bool FastSerialPort::FastSerialPort::OpenPort(array<TCHAR>^ port, int length)
{
TCHAR tport[15];
for (int iii=0; iii<length; iii++)
tport[iii] = port[iii];
tport[length] = 0;
m_hSerialPort = CreateFile(tport,
GENERIC_READ | GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
FILE_FLAG_OVERLAPPED,
NULL);
The FastSerialPort
class is inherited by the ConnectedSerialPort
class within the LocalTimeReaderGTS C# program.
The OpenPort
method shown above is actually called by the OpenPort
method shown below which is within the ConnectedSerialPort
class.
public bool OpenPort(string port)
{
if (portOpen)
return true;
portOpen = OpenPort(port.ToCharArray(), port.Length);
if (portOpen)
StartReceiving();
return portOpen;
}
Note also that the process thread for the ReceiveMessages
method in the FastSerialPort
class is actually initiated here in the C# code with the call to the
StartReceiving()
method. The StartReceiving()
method is shown below:
private void StartReceiving()
{
receivingThread = new Thread(this.ReceiveMessages);
receivingThread.Start();
}
Once again, the only significant difference between the ReceiveMessages
method and the code used in the RemoteTimeReaderGTSC program is the use of C# friendly variables.
Specifically, the receivedMessage
variable that was a char array now looks like this:
array<BYTE>^ receivedMessage = gcnew array<BYTE>(18);
As shown in the section of code from the ReceiveMessages
method below, receivedMessage
is still assembled as it was in the RemoteTimeReaderGTSC program.
receivedMessage[receivedIndex] = buffer;
receivedIndex++;
if (receivedIndex == 18)
{
receivedIndex=0;
ReceivedMessage(receivedMessage, 18);
}
The temptation to perform a virtual method call for each received byte was avoided in an attempt to achieve some performance gains. Indeed the fine tuning of parameters like
received buffer size might prove beneficial for some applications. The ReceivedMessage
method called in the code above is, of course, a virtual method which is actually
executed in the LocalTimeReaderPort
class within the LocalTimeReaderGTS C# program, as shown below:
protected override void ReceivedMessage(byte[] data, int length)
{
receiveMessageQueue.Enqueue(new Message(data, length));
receivedEvent.Set();
}
The message is queued. The process of reading messages off the queue and processing them is the same as it was for the LocalTimeReaderGT program. However, the Message
class
has been modified significantly because most of the message assembly is now done within the FastSerialPort
class.
The constructor for the Message
class is now as shown below:
public Message(byte[] data, int length)
{
if (length < 1)
status = MessageStatus.IgnoreError;
code = data[0];
if (code != 0x01 && code != 0x03 && code != 0x05)
{
status = MessageStatus.ResendError;
return;
}
status = MessageStatus.Complete;
if (code == 0x05)
{
index = data[1];
localTime = (long)BitConverter.ToInt64(data, 2);
remoteTime = (long)BitConverter.ToInt64(data, 10);
}
}
The queuing of messages to be sent occurs in the LocalTimeReaderGTS program in the same way as it did with the LocalTimeReaderGT program.
However, taking messages off the queue and sending them is a bit different. The SendMessageThread
method is now as shown below.
private void SendMessageThread()
{
while (true)
{
while (sendMessageQueue.Count > 0)
{
byte[] data = (byte[])sendMessageQueue.Dequeue();
SendMessage(data, data.Length);
}
sendEvent.WaitOne();
sendEvent.Reset();
}
}
The SendMessage
method is within the FastSerialPort
class and it is shown in part below.
bool FastSerialPort::FastSerialPort::SendMessage(array^ data, int length)
{
OVERLAPPED osWrite = {0};
DWORD bytesSent;
BYTE tdata[50];
for (int iii=0; iii < length; iii++)
tdata[iii] = data[iii];
osWrite.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
if (WriteFile(m_hSerialPort, tdata, length, &bytesSent, &osWrite) == 0)
{
As can be seen, the code is similar to the code used in the RemoteTimeReaderGTSC program apart from the use of C# friendly variables.
Conclusion
The C#.NET SerialPort
class provides satisfactory performance for most applications requiring serial port functionality, provided the sending and receiving processes are given
separate process threads as illustrated by the LocalTimeReaderGT program in this article.
A CLR C++ class library for serial port access has been described in this article. It can be fine tuned to suit specific applications.