Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Improving the Performance of Serial Ports Using C#: Part 2

0.00/5 (No votes)
13 Oct 2011 1  
Simple test programs designed to demonstrate performance issues with the .NET serial port interface and what might be done to improve things.

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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here