Share via


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:

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.