Coding in the Blue Glow
Duncan Mackenzie
Microsoft Developer Network
December 11, 2003
Summary: Duncan Mackenzie describes how to control a serial-port–connected LCD panel using Microsoft Visual Basic .NET and Microsoft Visual C# .NET. (15 printed pages)
Applies to:
Microsoft® Visual Basic® .NET
Microsoft® Visual C#®
Download the source code for this article.
I Just Love Blue Backlights
I recently bought a new cell phone with a blue backlight, and I just love it; I press the keys just to get it to light up on my desk. Now I've found a way to bring that most excellent of features into my computer system—a backlit LCD Panel (see Figure 1).
Figure 1. The bare panel, displaying its factory default startup text
If you've read my past columns, you may already know about my home music system, but let me refresh your memory. I have this computer sitting in my living room, a silver cube that hooks up to the TV, displays my CD collection, and lets me play music through my stereo. Cool enough, but it only supports one form of display, the TV, which means that the TV has to be on to see what is playing or to control the system. (Well, actually you could control it without the TV on, as the remote works regardless, but the menus might be a bit tricky to navigate without any form of display.) Now, however, I have found a way to avoid turning the TV on for some of the simpler UI functions—by adding a cool blue backlit LCD panel (with arrow keys!) into my system. The panel, as shown uninstalled in Figure 2, hooks up to the serial port and can be communicated with using a full set of control codes.
Figure 2. The panel mounted in a drive bay kit and hooked up, but not installed into my system
Note I'm using a panel from Crystalfontz, model 633, but the same concepts should apply to other Crystalfontz panels (which share a similar command set in many cases) or to panels from other manufacturers.
The first issue is figuring out how to talk to the device from within my Microsoft .NET Framework applications, how to send and receive information through the serial port. Serial communication is one of those unavoidable gaps in the .NET Framework. They couldn't cover everything, and it is just my luck that this is one of the areas that was left out. Luckily for me, there are components available to make this relatively painless.
In this article I am going to show you three different approaches to communicating through the Serial Port: using the MSCOMM OCX that shipped with Microsoft® Visual Basic® 6.0, using the DBComm library that is available as a sample on GotDotNet, and using the library created by Sax, available as part of the Visual Basic .NET Resource Kit. The actual codes sent back and forth will be the same, as the communication method does not affect how the panel works, but I will abstract the panel control from the serial communication to allow me to switch between my three implementations as necessary. Before I get into the three "easy" implementations though, I'm going to discuss the underlying Win32 API calls for a few moments.
Using the Win32 API
Historically, using the Win32 API, it has been possible to work with a serial port or a parallel port as if it were a file. Once you opened a connection to these special files, you would use standard read and write functions to communicate to any connected hardware. Although the .NET Framework provides its own functions for working with files, they explicitly block you from opening a file stream using one of the special names such as "COM1." To get around this limitation, as well-intentioned as it might be, you could choose to use the Win32 API for all of your communication with the serial port—but there is another option that ends up being a lot easier for you.
One of the constructor overloads for creating a file stream object in the .NET Framework takes a file handle as a parameter. This overload allows you to take the handle of a file opened through a Win32 API call (CreateFile in this case) and then create a .NET stream object against that file. When combined with some additional API calls (to set up the COM port for parity, stop bits, and so on) it is possible to do pretty much all of your serial port work using relatively "pure" .NET Framework code. There are several good samples of this technique available, including one from MSDN Magazine and quite a few on GotDotNet including the DBComm workspace.
If you decide to handle all of the communication yourself, using the API, then you will certainly have a lot of control, although I would certainly recommend using an existing library whenever possible.
Structuring the Application
I am going to create a class (LCDPanel) that exposes the features of the LCD panel to the rest of my application(s), and that class will need to call into another class to handle the serial communication. I want to ensure that switching from one form of communication code (from the MSComm control to the Sax control, for example) is as easy as possible, so I decided to structure the application such that all communication between the panel-specific code and the serial-port code will have to go through the same Interface (ISerial). Each of my three serial communication classes will implement ISerial, and my panel code won't need to change at all when I change communication methods. A simple class, SerialFactory (shown below), will create the appropriate class instance (MSCOMMWrapper, SaxWrapper or DBCOMMWrapper) based on a string parameter.
Public Class SerialFactory
Public Shared Function _
GetSerialCommunication( _
ByVal libraryName As String) _
As ISerial
Dim result As ISerial
Select Case libraryName.ToLower
Case "mscomm"
result = New _
MSCOMMWrapper.MSCOMMConnection
Case "sax"
result = New _
SaxWrapper.SaxCOMM
Case "dbcomm"
result = New _
DBCOMMWrapper.DBCommConnection
Case Else
Throw New ArgumentException( _
"Library Not Found")
End Select
Return result
End Function
End Class
ISerial, which exposes a simple set of methods and can even raise an event, is the only connection between the panel-specific code in LCDPanel and the serial communication classes:
Public Interface ISerial
Sub Open(ByVal portName As String, _
ByVal baudRate As Integer, _
ByVal dataBits As Byte, _
ByVal parity As parityOptions, _
ByVal stopBits As stopBitOptions)
Sub Open(ByVal port As Integer, _
ByVal baudRate As Integer, _
ByVal dataBits As Byte, _
ByVal parity As parityOptions, _
ByVal stopBits As stopBitOptions)
Sub SendMessage( _
ByVal message As Byte())
Event DataAvailable( _
ByVal sender As Object, _
ByVal e As EventArgs)
Sub Close()
Function ReadMessage( _
ByVal byteCount As Integer) As Byte()
End Interface
Note parityOptions and stopBitOptions are enums defined alongside ISerial to provide an easy way to specify these serial-port communication settings.
Building the Communication Wrappers
I then created a class for each of the three different communication methods, starting with the library from Sax. I will dig briefly into each of the three classes before showing you the LCDPanel class that handles the actual panel features.
Using the Serial Communications Control from SAX
Using this .NET library was the simplest and easiest of the three options. It was easy to configure and opening the port was intuitive.
'Assume "Imports Sax.Communications"
Public Overloads Sub Open(ByVal portName As String, _
ByVal baudRate As Integer, _
ByVal dataBits As Byte, _
ByVal parity As parityOptions, _
ByVal stopBits As stopBitOptions) _
Implements ISerial.Open
m_Sax = New SerialConnection
Dim saxParity As Parity
Select Case parity
Case parityOptions.EvenParity
saxParity = Parity.Even
Case parityOptions.MarkParity
saxParity = Parity.Mark
Case parityOptions.NoParity
saxParity = Parity.None
Case parityOptions.OddParity
saxParity = Parity.Odd
Case parityOptions.SpaceParity
saxParity = Parity.Space
End Select
Dim saxStopBits As CommStopBits
Select Case stopBits
Case stopBitOptions.oneStopBit
saxStopBits = CommStopBits.One
Case stopBitOptions.oneptfiveStopBit
saxStopBits = CommStopBits.OnePointFive
Case stopBitOptions.twoStopBit
saxStopBits = CommStopBits.Two
End Select
Dim options As New SerialOptions( _
portName, baudRate, _
saxParity, dataBits, _
saxStopBits, False, _
False, False, _
False, False, False)
Me.m_Sax.Options = options
Me.m_Sax.Open()
End Sub
The majority of that code is converting between a set of enums that I defined for the arguments in ISerial and whatever data types are required by the Sax control. I have to repeat this same type of code for the other two communication methods, to convert my generic values into the specific data types required. I defined the communication library (m_Sax in the code above) using the WithEvents keyword, which allows me to trap and handle the DataAvailable event.
Private WithEvents m_Sax As _
Sax.Communications.SerialConnection
Private Sub m_Sax_DataAvailable( _
ByVal sender As Object, _
ByVal e As System.EventArgs) _
Handles m_Sax.DataAvailable
RaiseEvent DataAvailable(Me, New EventArgs)
End Sub
In this case, I don't do any processing; I just raise another event (which is defined in ISerial) and let the calling class deal with pulling back the actual data by calling ISerial.ReadMessage.
Public Function ReadMessage( _
ByVal byteCount As Integer) _
As Byte() Implements ISerial.ReadMessage
Dim finalBuffer(byteCount) As Byte
Dim loopCount As Integer
Dim currentIndex As Integer = 0
Do While m_Sax.Available > 0 _
And currentIndex < byteCount
If m_Sax.Available > 0 Then
currentIndex += m_Sax.Read( _
finalBuffer, currentIndex, _
byteCount - currentIndex)
End If
Threading.Thread.CurrentThread.Sleep(10)
Loop
ReDim Preserve finalBuffer(currentIndex - 1)
Return finalBuffer
End Function
Connecting with the MSCOMM.OCX
The first step in creating this library was adding a reference to the "Microsoft Communications Control 6.0" COM library. I have Visual Basic 6.0 installed on my development machines, so I had this control already available, but you might not be able to find it in the list of available COM references. If you own a copy of Visual Basic 6.0, you can install it and it will register this component on your system. If you don't have Visual Basic 6.0, you can simply remove the MSCOMMWrapper project from your solution and comment out any lines of code referencing it.
As an "old-school" library, the MSCOMM library has a few quirks that made it hard to work with, such as a property (Input) that could return either an array of bytes or a string, depending on other settings. In the end, though, it works fine once you figure out all the properties and methods. I won't go through all of the code for this class (or for the DBCOMM wrapper, since the two of them are quite similar), but here is the MSCOMMWrapper's implementation of the Open and SendMessage methods.
Public Overloads Sub Open( _
ByVal port As Integer, _
ByVal baudRate As Integer, _
ByVal dataBits As Byte, _
ByVal parity As parityOptions, _
ByVal stopBits As stopBitOptions) _
Implements ISerial.Open
m_MSCOMM.CommPort = port
Dim settings As String
Dim parityChar As Char
Select Case parity
Case parityOptions.EvenParity
parityChar = "E"c
Case parityOptions.MarkParity
parityChar = "M"c
Case parityOptions.NoParity
parityChar = "N"c
Case parityOptions.OddParity
parityChar = "O"c
Case parityOptions.SpaceParity
parityChar = "S"c
End Select
Dim stopBitsNumber As String
Select Case stopBits
Case stopBitOptions.oneptfiveStopBit
stopBitsNumber = "1.5"
Case stopBitOptions.oneStopBit
stopBitsNumber = "1"
Case stopBitOptions.twoStopBit
stopBitsNumber = "2"
End Select
'the MSCOMM Library expects its settings as
'a string such as "9600,N,8,1"
settings = String.Format("{0},{1},{2},{3}", _
baudRate, parityChar, _
dataBits, stopBitsNumber)
m_MSCOMM.Settings = settings
m_MSCOMM.InputLen = 5
m_MSCOMM.InBufferSize = 5
m_MSCOMM.InputMode = _
MSCommLib.InputModeConstants.comInputModeBinary
m_MSCOMM.RThreshold = 5
m_MSCOMM.PortOpen = True
End Sub
Public Sub SendMessage( _
ByVal message() As Byte) _
Implements ISerial.SendMessage
m_MSCOMM.Output = message
End Sub
The MSCOMM library takes its settings in the form of a string (such as "9600,N,8,1"), which is detailed in the MSDN documentation for the Settings property.
Using the DBCOMM Library
I opened up an empty Class file and started working on the ISerial implementation that used the DBCOMM library, when I realized how much faster it would be to just copy the MSCOMM wrapper I had already created and make a few changes. This worked out to be easier because the DBCOMM library was carefully designed to match the properties, methods, and behavior of the original MSCOMM control that shipped with Visual Basic 6.0. There are a few differences, but they are really just minor naming changes, so it doesn't take long to modify existing MSCOMM-based code to work against the DBCOMM library. Since the code for this wrapper is so similar to the MSCOMMWrapper, I won't go into it here in the article.
Building the LCD Panel Code
Once the underlying serial classes were created, along with the ISerial interface, I could move onto programming against the actual LCD panel itself. Communicating with most serial devices consists of reading and writing messages that conform to a very specific format. In the case of the CrystalFontz panel, that format is described in detail in a PDF available from the manufacturer's Web site.
Sending Messages
All messages sent to this panel have to follow the same format:
- The first byte contains a combination of two values, the type of message and the command number.
- The second byte contains the length of additional data (the number of additional bytes) that is attached to this command.
- The data for the command, if any is needed, is next.
- The last two bytes are a 16-bit checksum for the rest of the message.
There isn't anything difficult about assembling this message, except for the checksum. The documentation for the panel specifies that the checksum is produced using the standard 16-bit CRC algorithm, often called crc16. Fortunately, I was able to find the details of this algorithm on the Web very quickly, but unfortunately, I found many different variations, all of which produced slightly different results. Without any real idea which one of the many code snippets corresponded to the formula expected by the panel, and suspecting that some of the code samples contained mistakes, I decided to check out CrystalFontz's own technical forums for an answer. Within a few moments, I located a perfect working sample written in Java. I copied that code down into a new C# class, made a few changes and compiled the result as a nice little CRC16 assembly (included with source in the download for this article).
With the CRC problem solved, I created a function in my LCDPanel class that would take the command type, number, and any associated data as arguments and produce a properly formatted message to send to the panel.
Private Function FormatCommand( _
ByVal type As CommandType, _
ByVal commandCode As Command, _
ByVal data As Byte()) As Byte()
Dim results As Byte()
Dim dataLength As Byte
If data Is Nothing Then
dataLength = 0
Else
dataLength = data.Length
End If
Dim totalLength As Integer = 4 + dataLength
ReDim results(totalLength - 1)
Dim arrayToChecksum(totalLength - 3) As Byte
arrayToChecksum(0) = (type << 6) Or commandCode
arrayToChecksum(1) = dataLength
If dataLength > 0 Then
Array.Copy(data, 0, _
arrayToChecksum, 2, dataLength)
End If
Dim checksum As Long
checksum = Convert.ToInt64( _
CRCUtilities.CRC16.compute(arrayToChecksum))
Dim checkBytes As Byte() = _
BitConverter.GetBytes(checksum)
Array.Copy(arrayToChecksum, _
results, dataLength + 2)
results(totalLength - 2) = checkBytes(0)
results(totalLength - 1) = checkBytes(1)
Return results
End Function
Writing a routine for each panel command was pretty easy now that all of the hard work was encapsulated into the message formatting and serial communication code. I only implemented a few of the 30 different commands supported by the panel, but I will probably add additional commands as the need arises.
Public Sub SetLine1(ByVal text As String)
If text.Length > 16 Then
text = text.Substring(0, 16)
Else
text = text.PadRight(16)
End If
Dim command As Byte()
Dim data As Byte()
data = System.Text.ASCIIEncoding.ASCII.GetBytes(text)
command = _
FormatCommand( _
CommandType.normal, _
LCDPanel.Command.SetLCDLine1, _
data)
Me.serial.SendMessage(command)
End Sub
Public Sub SetLine2(ByVal text As String)
If text.Length > 16 Then
text = text.Substring(0, 16)
Else
text = text.PadRight(16)
End If
Dim command As Byte()
Dim data As Byte()
data = System.Text.ASCIIEncoding.ASCII.GetBytes(text)
command = _
FormatCommand( _
CommandType.normal, _
LCDPanel.Command.SetLCDLine2, _
data)
Me.serial.SendMessage(command)
End Sub
Public Sub SetBacklight(ByVal value As Byte)
If value >= 0 And value <= 100 Then
Dim command As Byte()
Dim data(0) As Byte
data(0) = value
command = FormatCommand( _
CommandType.normal, _
LCDPanel.Command.SetLCDandKeypadBacklight, _
data)
Me.serial.SendMessage(command)
Else
Throw New ArgumentException( _
"Value must be within 0-100 range")
End If
End Sub
Public Sub SetContrast(ByVal value As Byte)
If value >= 0 And value <= 50 Then
Dim command As Byte()
Dim data(0) As Byte
data(0) = value
command = FormatCommand( _
CommandType.normal, _
LCDPanel.Command.SetLCDContrast, _
data)
Me.serial.SendMessage(command)
Else
Throw New ArgumentException( _
"Value must be within 0-50 range")
End If
End Sub
Public Sub StoreCurrentStateAsBootState()
Dim command As Byte()
command = FormatCommand( _
CommandType.normal, _
LCDPanel.Command.StoreCurrentAsBoot, _
Nothing)
Me.serial.SendMessage(command)
End Sub
Public Sub ClearLCDScreen()
Dim command As Byte()
command = FormatCommand( _
CommandType.normal, _
LCDPanel.Command.ClearScreen, _
Nothing)
Me.serial.SendMessage(command)
End Sub
Receiving Messages
For the keypad buttons, I watched the data coming back from the serial port for a specific code (&H80) that indicated a key press or release. The data portion of the message (messages back from the panel follow the same format as the messages that you use to control it) indicated which particular key event had occurred.
Private Sub serial_DataAvailable( _
ByVal sender As Object, _
ByVal e As System.EventArgs) _
Handles serial.DataAvailable
Dim buffer() As Byte = Me.serial.ReadMessage(5)
If Not (buffer Is Nothing OrElse buffer.Length < 3) Then
If buffer(0) = &H80; Then 'keypress
Dim loopCount As Integer
Dim keyPressed As Byte = buffer(2)
Dim bpArgs As New ButtonPushedEventArgs
bpArgs.RawKeyCode = CType(keyPressed, KeypadCodes)
Dim keyCode As Keys
Dim keyAction As TypeOfKeypress
Select Case bpArgs.RawKeyCode
Case KeypadCodes.KEY_DOWN_PRESS, _
KeypadCodes.KEY_DOWN_RELEASE
keyCode = Keys.DownKey
If bpArgs.RawKeyCode = _
KeypadCodes.KEY_DOWN_PRESS Then
keyAction = TypeOfKeypress.Press
Else
keyAction = TypeOfKeypress.Release
End If
'Repeat case block for each key
'(up, down, left, right, enter and exit)
End Select
bpArgs.Key = keyCode
bpArgs.TypeOfPress = keyAction
RaiseEvent ButtonPushed(Me, bpArgs)
End If
End If
End Sub
In the end, an application using the LCDPanel class, such as the test application (see Figure 3) included in the source code download, should not care what serial communication method is being used behind the scenes.
Figure 3. The test application allows you to try out a selection of commands against the panel.
As long as you implement the same interface and can produce the same results for each of the interface's methods, then the specific methods used should not affect the functionality of the application in any way. That has certainly been my experience, but I'm working against a single device and only using a small set of that device's features, so your results may vary.
Resources
The core of this article amounted to using three different serial communication libraries. The Microsoft COMM Control ships with Visual Basic 6.0, but more information on the other libraries is available on the web:
- Check out Sax on the Web at http://www.sax.net/, they have more than just serial communication components, so you might want to browse around.
- The DBComm workspace (created by Cory Smith) is available on GotDotNet at http://workspaces.gotdotnet.com/dbcomm, and you can also track Cory down at his personal site http://www.addressof.com.
Other than these two libraries, I would suggest you read the following article:
Coding Challenge
At the end of some of my Coding4Fun columns, I will have a little coding challenge—something for you to work on if you are interested. For this article, the challenge is to create a .NET application that uses the serial or parallel port. Anything goes, whether you are trying to do your own modem work (although war dialing is frowned upon!), talk to a serial device like I am using, or even handle your own printing. Just post whatever you produce to GotDotNet and send me an e-mail message (at duncanma@microsoft.com) with an explanation of what you have done and why you feel it is interesting. You can send me your ideas whenever you like, but please just send me links to code samples, not the samples themselves (my inbox thanks you in advance).
Have your own ideas for hobbyist content? Let me know at duncanma@microsoft.com, and happy coding!
Coding4Fun
Duncan Mackenzie is the Microsoft Visual Basic .NET Content Strategist for MSDN during the day and a dedicated coder late at night. It has been suggested that he wouldn't be able to do any work at all without his Earl Grey tea, but let's hope we never have to find out. For more on Duncan, see his site.