Introduction
This article will show you how to achieve Serial RS232 communication using WPF. Its overall layout and design is simple as the code is what I wished to focus on. In particular, there are key points such as the Dispatcher.Invoke
method and the ASCII to HEX conversion that are key stumbling blocks in serial communication to PICs that I have addressed.
Assumed Knowledge
This article expects a developer to have a basic understanding of C# and WPF.
Background
I've spent the last 7 years of my life happily programming in C#. About 5 months ago, I decided to make the move to WPF and it's been an interesting journey. I'm an electrical engineer and spend a majority of my time making my computer talk to the outside world. I was working on a DMX project and decided to utilise a XAML interface in WPF rather than the boring limited C# forms.
So the idea behind it all is to have a WPF project talk to a PIC Micro controller. In C#, this is achieved by utilising System.IO.Ports
; library and in WPF it's no different. However, there are some unique problems that any user from a C# background will encounter. This is the lack of an Invoke
method, this is only slightly true as is hidden in there, however has been inherited by the Dispatcher
Class.
Using the Code
The code is laid out very simply with little attention paid to naming conventions, etc. This will be addressed at a later date, however I wanted to get the basics out here so people can start using the code.
First, we will start of with the Interface. This is very basic and is simply a ComboBox
's box for COM Port and Baud Rate a Textbox
for Data to send and a Richtextbox
to display the received data from the COMS Port with a few buttons for actions. I've not show the code for this as it can be download in the source and the layout is not important.
Basic Style
Advanced Style
XAML Data Provider
The first important factor is where the COM port Names and Baud rates come from. Now there are ways of getting the relevant data from the computer you are using but I wanted a little more control so I placed all the titles in an XML file called CommsData.xml. This data is provided by a "Data Provider" and each one is placed within the Windows.Resource
element. The XPath
determines which element I'm looking at and the x:Key
is what I referenced in my code.
<Window.Resources>
<XmlDataProvider x:Key="ComPorts" Source="CommsData.xml" XPath="/Comms/Ports" />
<XmlDataProvider x:Key="ComSpeed" Source="CommsData.xml" XPath="/Comms/Baud" />
</Window.Resources>
To understand a little further, we need to examine the contents of CommsData.xml.
<Comms>
<Ports>COM1</Ports>
<Baud>921600</Baud>
</Comms>
The <Comms>
is my root element you can only have one of these in a data file. The <Ports>
and <Baud>
child elements separate the text so my data providers can only see what I want them to see. Each child element can have its own children; all you would have to do is set up the Data Providers correctly to show the data you want. If you want to format the way this data is presented, please search for the use of Data Templates.
The Data Providers are utilised by binding the ItemsSource
properties of each ComboBox
. Where below ComPorts
is the x:Key
I selected for my Data Provider:
ItemsSource="{Binding Source={StaticResource ComPorts}}"
Setting up the Serial Port
There are two approaches to this in the C# environment - you can drag an element onto the form and edit its attributes or you could hard code it. In WPF, unfortunately you must hard code the Com Port, however this is very easy to do.
First, we must include a reference to the IO Port library:
using System.IO.Ports;
Now, we can create a serial port. For ease, I have named this one serial however if you are utilising more ports, the naming convention should be changed.
SerialPort serial = new SerialPort();
There are several properties of the serial port you can edit. This occurs within the source code when the Connect button is pressed, however could be set anywhere as long as it is done before the port is opened.
serial.PortName = Comm_Port_Names.Text; serial.BaudRate = Convert.ToInt32(Baud_Rates.Text); serial.Handshake = System.IO.Ports.Handshake.None;
serial.Parity = Parity.None;
serial.DataBits = 8;
serial.StopBits = StopBits.One;
serial.ReadTimeout = 200;
serial.WriteTimeout = 50;
For information about these attributes, please see RS-232 communication protocol. These settings should satisfy a majority of applications, however there are two important attributes. ReadTimeout
is the time allowed to read data received on the serial port. If this is set to low, then errors may occur and message can be cut short. WriteTimeout
is the time allowed to write data out of the serial port - this can cause errors if you are trying to write long string
s of data.
I have found no perfect formula for these values and a trial and error approach is necessary, bearing in mind that the greater the time allowed, the more delay your program will inherit.
Receiving Data
Now that we have a serial port, it important to set up a function call for every time the serial port has data to be read out. This is far more efficient that producing a thread, polling for data and waiting for a time out exception. To do this, we simply introduce the following code:
serial.DataReceived += new System.IO.Ports.SerialDataReceivedEventHandler(Recieve);
This will call the Recieve
function every time data is received. Within this function, we read the data out to a String
called recieved_data
and then we Invoke a function to write this data to our form. To enable Invoke
, we have to include:
using System.Windows.Threading;
private void Recieve(object sender, System.IO.Ports.SerialDataReceivedEventArgs e)
{
recieved_data = serial.ReadExisting();
Dispatcher.Invoke(DispatcherPriority.Send,
new UpdateUiTextDelegate(WriteData), recieved_data);
}
Now in WPF, we have to use a Dispatcher.Invoke
method. The purpose of this is quite simple, the main form is controlled by its own thread. When the serial port announces it has received data, a new thread is created to read this data out. The Invoke
method allows the string
of data to be passed from the serial data received thread to the form thread when it is possibly preventing any errors.
The data can be written to any control capable of displayed text. In this example, we display it via a RichTextbox
. In WPF, you can only write a Flow Document to a RichTextbox
so you must crate a Paragraph to write the data in and then add this Paragraph to the Flow Document before displaying it. More complicated than C#, however not impossible to work with.
FlowDocument mcFlowDoc = new FlowDocument();
Paragraph para = new Paragraph();
private void WriteData(string text)
{
para.Inlines.Add(text);
mcFlowDoc.Blocks.Add(para);
Commdata.Document = mcFlowDoc;
}
Sending Data
This is where things may change if you are communicating between computers, the protocol will change and you can send data using the standard serial.Write(string)
method as in C#. However, if you are communicating to a P PIC micro-controller, you must convert the text to HEX bytes.
The text held with SerialData TextBox
is sent to a SerialCmdSend
function. This takes several steps in its encoding of the data string. The first is to check if the serial port is open serial.IsOpen
. It's imperative to do so otherwise an error will be flagged. Within the try{} catch{}
method is the encoding routine.
using System.Threading;
if (serial.IsOpen)
{
try
{
byte[] hexstring = Encoding.ASCII.GetBytes(data);
foreach (byte hexval in hexstring)
{
byte[] _hexval = new byte[] { hexval }; serial.Write(_hexval, 0, 1);
Thread.Sleep(1);
}
}
catch (Exception ex)
{
para.Inlines.Add("Failed to SEND" + data + "\n" + ex + "\n");
mcFlowDoc.Blocks.Add(para);
Commdata.Document = mcFlowDoc;
}
}
Now it is important to understand what is occurring here and why. The first step is to convert the string
hex values in a byte array. For example, "123
" will become:
[1]-49
[2]-50
[3]-51
Now in computer to computer communication, this can be sent directly using:
serial.Write(hexstring, 0, hexstring.length);
In PIC communication, however, there was a problem with this method. This may be apparent only when using higher Baud Rates but is caused by the computer timing issues as the computer will try and send the data continuously and the PIC will be overloaded. The program on the PIC will crash and need to be reset, in real world applications we can't have this.
The secret is to only send 1 byte at a time ensuring there is a delay in between bytes. The delay only has to be 1 millisecond however if this is not used the PIC will crash. This is done in the loop shown again below. The extra step within this loop to convert each byte to a byte array (byte[]
) is annoyingly necessary as the serial.Write
method utilised will only send a byte array.
foreach (byte hexval in hexstring)
{
byte[] _hexval = new byte[] { hexval }; serial.Write(_hexval, 0, 1);
Thread.Sleep(1);
}
Finally, there you have it - a working WPF serial communication platform for RS-232 check back for a fully working release and a re-design using Microsoft Expression Blend.
History
- Release V1.0 Basic style and source
- Release V1.1 Advanced style and source