With today's low-cost electronics and micro-manufacturing techniques the average electronics hobbyist has access to a plethora of powerful little microcontrollers (MCUs), along with a huge assortment of possible peripherals, and nearly all of these require a RS-232 serial interface to communicate with a PC. The accessibility and ease-of-use of VB.Net has made it an attractive platform for developing small purpose-driven applications meant to configure or otherwise communicate with these microcontrollers and microcontroller-based devices. However, as obvious a choice as the SerialPort component is, there are some common pitfalls to its use; and even though these pitfalls are explained in the documentation, a proper implementation of the component can still be elusive for some developers. This article will attempt to explain the common usage of the SerialPort component and offer a sample "SerialDevice" class which can be used to encapsulate the SerialPort component instance along with the data and logic associated with its usage.
↑ Return to Top
There are a few very popular hobby/prototyping MCU platforms, among them are Arduino, BasicStamp, and more recently mBed. But there are also many, many others out there. These typically range from 8-bit to 32-bit processors containing some amount of flash-memory used for storage and operating memory, as well as an array of peripherals such as Digital/Analog I/O, UARTs, I2C/SPI buses, etc. Most are inexpensive (<$100 USD) and several offer rich online communities and/or public code libraries. These devices are popular with electronics designers and hobbyists alike, and once you get past the initial introduction and setup they tend to be quite easy to work with. Since these devices are popular with electronics designers and hobbyists and since Visual Basic .Net is free, easy to use, and well supported it is natural that people would attempt to write a VB app to configure or communicate with their MCU, having little or no programming experience at the outset. There are typically just a few requirements for this type of application; it needs to connect to the MCU on a serial port (often a USB virtual serial port) and then send and receive what are usually a small number of bytes representing a command with possible parameters and a command execution response. Sometimes the response is expected to contain a long sequence of byte data which will require further processing beyond a simple status result comparison, but most times the result will be a small number of bytes which decode to a status result or requested data value. Due to the nature of the SerialPort component and its multithreaded events the process of sending a command, reading a result, and then processing that result is not as intuitive as the methods of the SerialPort component might make it seem.
In the VB.Net forums, I have seen many developers fall into the trap of attempting to use the SerialPort.Write method to send a command and then follow it with loop of SerialPort.Read method calls in an attempt to gather the response. This seems like a natural thing to do and is based on a simple assumption. The data being transmitted is small, so you know it happens very quickly, and you might even flush the buffer to be sure it is sent before reading. Once sent, there is nothing to do but wait for the resulting byte stream and the SerialPort.Read method will block until the bytes start to come in. When that happens, the loop will proceed sequentially and some small buffer array will be populated with each read byte. Simple, right? But whenever someone tries this, it almost always ends badly. The buffer they are trying to build doesn't contain the correct bytes, or they are not in the correct sequence, and the results seem sporadic and uncontrollable. The root of this problem, and the flaw with the simple assumption, is that there are layers of abstraction between the SerialPort component in managed code and the physical hardware which actually performs the serial transmissions. And these layers can vary depending on the type of serial port and underlying hardware. The SerialPort component is designed to handle all of this complexity and notify you when data is accurately available to be consumed in managed code, but this means using the component as its creators intended.
To do this, an application using a SerialPort component must subscribe to the SerialPort.DataReceived event. When data is available to managed code, the SerialPort will raise the DataReceived event and the attached event handler in the application can then read the available data and store for later processing. Since the DataReceived event is raised on a secondary thread the code will have to take multithreading into account when reading and storing the available data. And as an event implies, the code will also have to perform the reading and storing of received data as quickly as possible so as not to block the thread which raises the event. It is because there is very little time for processing during the DataReceived event handler's execution that the bytes read are stored for processing at a later time. In this way the application defines an asynchronous read-write pattern with the serial port. This means that the user can potentially issue multiple commands while the MCU is processing the first command received, possibly resulting in DataReceived events which contain data for more than one command result. There is also now the issue of exactly when and how this "later processing" of the received data will occur. To address these issues, the application can introduce its own secondary thread which can provide the primary work execution for the application. By utilizing a System.Threading.Tasks.Task object, the application can run three threads to handle all of the functionality requirements. The main thread will host the user interface and will be responsible for responding to user input, communicating that input to the secondary worker thread, updating display controls based on notifications from the secondary worker thread, and writing to the SerialPort component. The SerialPort component will raise its DataReceived event on its own secondary thread, and that thread will be responsible for reading data and communicating that data to the secondary worker thread. The secondary worker thread will have the responsibility of waiting for command results, processing the results, and notifying the main thread of updated display content.
The following code sample defines a SerialDevice class which encapsulates the SerialPort component and worker Task, along with the functionality outlined above. This class is meant to provide a lot of flexibility in usage and can benefit from performance improvements by redesigning it to fit a specific purpose. But as a general sample the performance should be adequate for most purposes. First we'll look at the code and then we'll go over the members in greater detail.
Option
Strict
On
Explicit
Infer Off
Imports
System.IO.Ports
System.Collections.Concurrent
System.Threading
''' <summary>
''' Provides a wrapper for a SerialPort which automatically reads data from the port and processes it via assigned Func(Of Byte(), Integer, Integer) delegates.
''' Also provides "Send" methods for writing character, byte, hex, and string data to the port.
''' </summary>
''' <remarks></remarks>
Public
Class
SerialDevice
Private
WithEvents
_Port
As
SerialPort
_DataQueue
New
ConcurrentQueue(Of
Byte
)
_CancelSource
CancellationTokenSource
_Task
System.Threading.Tasks.Task
_Waiter
System.Threading.ManualResetEventSlim
''' Gets a value determining if the serial port is open.
''' <value></value>
''' <returns></returns>
ReadOnly
Property
Connected
Boolean
Get
Return
_Port.IsOpen
End
''' Gets or sets the COM Port name that the physical device is connected to, or virtual COM Port name created by the physical device.
ComPort
String
_Port.PortName
Set
(value
_Port.PortName = value
''' Gets or sets a value determining whether or not the serial port will use data-terminal-ready signals (default is False).
DtrEnable
_Port.DtrEnable
_Port.DtrEnable = value
''' Gets or sets the text encoding used to convert between character strings and byte data (default is ASCII).
Encoding
System.Text.Encoding
_Port.Encoding
System.Text.Encoding)
_Port.Encoding = value
''' Gets or sets the string used to represent a line terminator (default CrLf).
NewLine
_Port.NewLine
_Port.NewLine = value
''' Gets or sets a value specifying the baud rate to use.
Baud
Integer
_Port.BaudRate
_Port.BaudRate = value
''' Gets or sets a value specifying the data bits to use.
DataBits
_Port.DataBits
_Port.DataBits = value
''' Gets or sets a value specifying the stop bits to use.
StopBits
_Port.StopBits
StopBits)
_Port.StopBits = value
''' Gets or sets a value specifying the parity to use.
Parity
_Port.Parity
Parity)
_Port.Parity = value
''' Gets or sets a value specifying the hardware handshaking to use.
FlowControl
Handshake
_Port.Handshake
Handshake)
_Port.Handshake = value
''' Gets or sets a value determining whether or not the serial port uses ready-to-send signals (default False).
RtsEnable
_Port.RtsEnable
_Port.RtsEnable = value
''' Gets or sets the processing mode. In ByteMode, the CheckMessageComplete delegate will be invoked for every byte received.
''' In BlockMode, available data will be read at once and sent to CheckMessageComplete in chunks, as available.
MessageMode
MessageProcessingMode
''' Gets or sets a delegate function which will be called to determine if a complete message has been queued.
''' This function must return zero if the bytes received so far do not constitute a complete message, or a positive
''' integer value representing the number of bytes which make up the complete message.
''' <remarks>
''' </remarks>
''' <example>
''' The following example defines a simple check message delegate function and sets it
''' to the CheckMessageComplete property.
''' <code>
''' mySerialDevice.CheckMessageComplete = AddressOf(DoCheckMessage)
'''
''' Function DoCheckMessage(data As Byte(), count As Integer) As Integer
''' '1) Determine if data contains complete message
''' '2) Return number of bytes in message if complete, or return 0 if not complete
''' End Function
''' </code>
''' </example>
CheckMessageComplete
Func(Of
(),
,
''' An optional Action(Of PortState, SerialError) delegate to be invoked when a serial port error occurs.
''' Only as reliable as the SerialPort.ErrorReceived event. See MSDN documentation for details.
ProcessError
Action(Of PortState, SerialError)
''' A required Action(Of Byte(), Integer) delegate to be invoked when the CheckMessageComplete delegate call returns a positive value.
''' This delegate method performs the actual work of processing the message data received from the serial device.
ProcessMessage
Action(Of
''' An optional Action(Of PortState, SerialPinChange) delegate to be invokde when a serial pin change event occurs.
''' See the MSDN documentation for SerialPort.PinChanged event for more information.
ProcessPinChange
Action(Of PortState, SerialPinChange)
''' Configures the serial port with the default baud rate (9800), 8 data bits, no parity, one stop bit, and no flow control.
Sub
ConfigurePort()
ConfigurePort(CommonBaudRate.
Default
, 8, Parity.None, StopBits.One, Handshake.None)
ConfigurePort(baud
CommonBaudRate)
ConfigurePort(
CInt
(baud), 8, Parity.None, StopBits.One, Handshake.None)
ConfigurePort(baud, 8, Parity.None, StopBits.One, Handshake.None)
CommonBaudRate, dataBits
, parity
Parity, stopBits
StopBits, flowControl
(baud), dataBits, parity, stopBits, flowControl)
, dataBits
_Port.BaudRate = baud
_Port.DataBits = dataBits
_Port.Handshake = flowControl
_Port.Parity = parity
_Port.StopBits = stopBits
''' Creates a new instance of SerialDevice using the specified existing SerialPort instance.
''' <param name="serialPort"></param>
(serialPort
SerialPort)
_Port = serialPort
''' Creates a new instance of SerialDevice with its own internal SerialPort.
()
Me
.
(
SerialPort())
''' Opens the SerialPort and connects to the physical device if the PortName, CheckMessageComplete delegate, and ProcessMessage delegate have all been specified.
Connect()
If
Is
Nothing
Then
.IsNullOrEmpty(_Port.PortName)
Throw
InvalidOperationException(
"Cannot connect when COM Port is not specified."
"Cannot connect when no IsMessageComplete delegate has been specified."
"Cannot connect when no ProcessMessage delegate has been specified."
_Port.Open()
_CancelSource =
_Waiter =
ManualResetEventSlim
_Task = System.Threading.Tasks.Task.Factory.StartNew(
Object
)(
AddressOf
ProcessData), _MessageMode, _CancelSource.Token)
''' Ends processing of the SerialDevice and closes the underlying serial port.
Disconnect()
_Task IsNot
_CancelSource.Cancel()
_Waiter.
_Task.Wait()
_Port.Close()
_Task.Dispose()
_CancelSource.Dispose()
_Waiter.Dispose()
_Task =
''' Sends a single character to the serial port.
''' <param name="c"></param>
SendChar(c
Char
_Port.Write({c}, 0, 1)
''' Sends a byte or series of bytes to the serial port.
''' <param name="bytes"></param>
SendData(
ParamArray
bytes()
_Port.Write(bytes, 0, bytes.Length)
Else
MessageBox.Show(
"The port is not open."
"Not Connected"
, MessageBoxButtons.OK, MessageBoxIcon.
Error
''' Sends a byte or series of bytes in hexadecimal format to the serial port.
''' <param name="hexString"></param>
SendHex(hexString
hexString = hexString.Replace(
" "
""
).ToUpper
Not
hexString.Length
Mod
2 = 0
hexString =
"0"
& hexString
Dim
result(
(hexString.Length / 2) - 1)
delta
For
i
= 0
To
result.Length - 1
.TryParse(hexString.Substring(i * 2, 2), Globalization.NumberStyles.AllowHexSpecifier,
, delta)
result(i) = delta
ArgumentException(
"Invalid hex string."
Next
SendData(result)
''' Sends a string to the serial port using the current text encoding.
''' <param name="text"></param>
SendString(text
_Port.Write(text)
_Port_DataReceived(sender
, e
System.IO.Ports.SerialDataReceivedEventArgs)
Handles
_Port.DataReceived
portRef
SerialPort =
DirectCast
(sender, SerialPort)
data(portRef.BytesToRead - 1)
count
= portRef.Read(data, 0, data.Length)
count - 1
_DataQueue.Enqueue(data(i))
_Port_Disposed(sender
System.EventArgs)
_Port.Disposed
_Port_ErrorReceived(sender
System.IO.Ports.SerialErrorReceivedEventArgs)
_Port.ErrorReceived
ProcessError IsNot
_ProcessError(
PortState(_Port), e.EventType)
_Port_PinChanged(sender
System.IO.Ports.SerialPinChangedEventArgs)
_Port.PinChanged
ProcessPinChange IsNot
_ProcessPinChange(
Protected
ProcessData(modeObj
mode
MessageProcessingMode =
(modeObj, MessageProcessingMode)
currentMessage
List(Of
Do
While
_CancelSource.IsCancellationRequested
= currentMessage.Count
available
= _DataQueue.Count
available > 0
b
mode = MessageProcessingMode.ByteMode
_DataQueue.TryDequeue(b)
currentMessage.Add(b)
CheckAndProcess(currentMessage)
ElseIf
mode = MessageProcessingMode.BlockMode
available -= 1
delta = currentMessage.Count
_Waiter.Wait()
_Waiter.Reset()
Loop
CheckAndProcess(currentMessage
))
removeCount
= _CheckMessageComplete(currentMessage.ToArray, currentMessage.Count)
removeCount > 0
_ProcessMessage(currentMessage.Take(removeCount).ToArray, removeCount)
currentMessage.RemoveRange(0, removeCount)
''' Provides information about the state of a serial port.
Structure
PortState
BreakState
CDHolding
CTSHolding
DSRHolding
IsOpen
(port
BreakState = port.BreakState
CDHolding = port.CDHolding
CTSHolding = port.CtsHolding
DSRHolding = port.DsrHolding
IsOpen = port.IsOpen
''' Provides a list of common serial baud rates.
Enum
CommonBaudRate
[
] = 9600
bps2400 = 2400
bps4800 = 4800
bps9600 = 9600
bps14400 = 14400
bps19200 = 19200
bps28800 = 28800
bps38400 = 38400
bps57600 = 57600
bps115200 = 115200
''' Specifies the processing mode for messages.
''' The CheckMessageComplete delegate will be invoked for every byte received.
ByteMode
''' The CheckMessageComplete delegate will be invoked after all available data is read on each data received event.
BlockMode
The class begins by declaring the SerialPort component, then a thread-safe queue of bytes to use as the primary working data buffer, and finally the Task object along with the related CancellationTokenSource and ManualResetEvent needed to control the secondary thread's execution. Next come simple properties to expose the primary members of the internal SerialPort instance. These will be set by the user according to the requirements of their serial device. The following MessageMode property holds a value from the MessageProcessingMode enumeration defined at the end of the code sample. This value directs the SerialDevice class on how to process received data. Next come the delegate properties which define the methods that will perform the actual data processing work. With this design, when you declare an instance of SerialDevice in your application you will also write methods to do the work of processing messages from the serial port. You will assign a delegate pointing to the work methods to the related SerialDevice properties. The CheckMessageComplete delegate will be a method in your program that receives an array of bytes along with the number of bytes in the collection. This method returns an integer which indicates the position within the collection where a complete message ends. The secondary worker thread of the SerialDevice will call the CheckMessageComplete delegate and pass it all of the data received so far. The application code can then analyze the data and decide whether or not a complete reply to the last sent command has been received. If it has, the method returns the number of bytes in the collection which represent the complete message. If it has not yet completed, the method should return zero and wait to be called again. Once the CheckMessageComplete delegate returns a positive value, the indicated number of bytes will be removed from the working data queue and the ProcessMessage delegate will be executed and passed the complete message data. The ProcessError and ProcessPinChange delegates provide an optional means of executing custom application code should the serial port raise an error event or if the application needs to monitor pin changes for a specific reason (typically this is not required). Next come a few overrides of a method which can be used to configure the serial port for a connection. These are followed by the class constructors which either create a new SerialPort instance or use an existing one (in most cases an application will allow the SerialDevice to create the SerialPort instance). The class then exposes a Connect and Disconnect method which open the serial port and setup the secondary thread, or close the port and tear down the multithreading objects. These methods are followed by a number of helpers for writing data to the serial port. These helper methods cover the common requirements of sending characters, bytes, text, and bytes in hex format to the serial port. We then come to the simple event handlers for the SerialPort object. In the DataReceived event handler, we simply read all available data and add it to the working data queue. The ManualResetEvent instance is then set to notify the secondary worker thread that it should start working again if it had been idle. The other event handlers are rather self-explanatory and simply end the secondary thread should the SerialPort instance become disposed (should not happen) or call the appropriate delegate for the event if one has been defined. Finally we get to the ProcessData method which represents the secondary thread work, and the CheckAndProcess helper method which calls the defined CheckMessageComplete and ProcessMessage delegates as necessary. The ProcessData message declares a List(Of Byte) to build up the current MCU response. This intermediate list holds the data to be checked and processed based on the setting of the MessageMode property. If the MessageMode is set to ByteMode, then the CheckAndProcess helper is executed for every byte received. If the MessageMode is set to BlockMode, the CheckAndProcess helper is only executed each time new data is queued and all queued data is sent to be checked. The CheckAndProcess method then simply calls the CheckMessageComplete delegate and then updates the queue and calls the ProcessMessage delegate if the check returns a positive value.
A common scenario when working with a MCU is to send a command to configure or interrogate the device. Many MCU's firmware will use a simple text-based protocol with all data transmitted in ASCII text and control codes implemented using ASCII format codes. Commonly the user only needs to look for a single character such as Carriage Return (CR) or Line Feed (LF) or Zero (0) to indicate the end of a response. To that end, we will build a simple program that allows us to build up a message from characters received from an MCU. The following example will demonstrate the use of a SerialDevice instance in a Windows Forms Application. The example will use a RichTextBox as a console screen allowing the user to type in characters which are passed to the MCU connected on COM3 (the characters are not displayed in the RichTextBox as they are typed). The MCU then simply echos any received character back to the PC. The SerialDevice CheckMessageComplete delegate will look for a CR character (13) to determine when a message is complete and the ProcessMessage delegate will display the complete message in the RichTextBox. In this way, the program will buffer characters until the Enter key pressed and then the entire string will be written to the RichTextBox. Keep in mind though that each byte is passed through the MCU before being buffered by the SerialDevice for display in the RichTextBox.
Form1
IOTextBox
RichTextBox
With
{.BackColor = Color.Black, .ForeColor = Color.Lime, .Dock = DockStyle.Fill, .
=
True
}
McuDevice
Form1_FormClosing(sender
System.Windows.Forms.FormClosingEventArgs)
.FormClosing
McuDevice.Disconnect()
Form1_Load(sender
System.
MyBase
.Load
Controls.Add(IOTextBox)
McuDevice.CheckMessageComplete =
IsMessageComplete
McuDevice.ProcessMessage =
McuDevice.ConfigurePort()
McuDevice.ComPort =
"COM3"
McuDevice.MessageMode = MessageProcessingMode.BlockMode
McuDevice.Connect()
Function
IsMessageComplete(data
(), length
index
= Array.IndexOf(data,
CByte
(13))
index > -1
index + 1
0
ProcessMessage(data
message
= System.Text.Encoding.ASCII.GetString(data.ToArray)
Invoke(
AppendOutput), message)
AppendOutput(message
IOTextBox.TextLength > 0
IOTextBox.AppendText(ControlChars.NewLine)
IOTextBox.AppendText(message)
IOTextBox_KeyDown(sender
System.Windows.Forms.KeyEventArgs)
IOTextBox.KeyDown
e.Handled =
IOTextBox_KeyPress(sender
System.Windows.Forms.KeyPressEventArgs)
IOTextBox.KeyPress
McuDevice.SendChar(e.KeyChar)
IOTextBox_KeyUp(sender
IOTextBox.KeyUp
e.SuppressKeyPress =
With this design, the SerialDevice class offers a versatile framework for communicating with a MCU or other serial device regardless of the particular data protocol employed. A similar design could be used in a more purpose-specific way to more efficiently analyze and process the received data based on protocol-specifics unique to the application. That said, this class should still be suitable for many quick-and-simple MCU configuration and/or interrogation utilities. The example also shows how to implement a SerialPort component, and the basic event handling routines could be used directly on a form without the encapsulating "SerialDevice" class. The SerialDevice class can be stripped apart into its constituent components in order to apply each concept to a another design.