An LCD panel is a simple enough device, but one well worth encapsulating inside a C++ class or two.
Introduction
Okay, so your embedded system can handle weapons control while producing stock picks in the background. Maybe you'd better think about something a little more sophisticated than the traditional 40x2 LCD text display. Full graphics displays are now commonly available, some with little or no glue logic required. They perch on the data bus and a few chip-select lines, and then it's all software from there.
I have written a small, but useful, C++ class library to handle graphic LCD displays in embedded applications. The library handles multi-process output on a graphics LCD display, as well as simple user input.
The library meets the following design goals:
- Ability to virtualize the display to allow different processes to have a separate screen. The user can cycle between the screens to see what each process is doing.
- Point-, line-, and text-drawing capability.
- Dynamic output fields within each virtual display. These fields can be updated regularly when connected to a multitasking system.
- Support for a simple button-and-dial interface.
The end result is a set of classes that allow me to create up to four virtual screens, each containing dynamic text or graphics fields, with user input marshaled to the appropriate processes.
Overview of the Class Hierarchy
I'll describe the class library first in an overview and later in more detail. As you might guess, there is a class LCDDisplay (see Figure 1). It handles initialization of the hardware, virtualization of the screen, and drawing to the LCD. The class interface is hardware-independent in case I move to a different display in the future.
I have purposely limited the scope of LCDDisplay's functionality. User I/O is a higher-level concept than plotting pixels. The decisions about what to draw and when, for example, aren't the responsibility of the LCD. Neither is the LCD responsible for watching the status of an input switch. So, LCDDisplay objects are just simple output devices. Extending LCDDisplay to handle different types of displays would be appropriate and easy, but adding drop-down menu functions goes beyond their design scope.
The base class IOProcess is what enables user I/O. All IOProcess objects encapsulate an LCDDisplay data member, which they use for output. To create a screen full of gauges and annunciators, the user might derive a class InstrumentClusterDisplay from IOProcess. Classes derived from IOProcess automatically receive messages like Paint and ButtonPress. Then, they decide how to change their internal state and what actions should be taken as a result.
Some IOProcess classes may need full "raw" access to the screen to show unusual types of output. This is easily accomplished by sending messages directly to the embedded LCDDisplay member. However, most of my output screens use common elements, like text fields. It makes sense for each IOProcess class to be able to throw up any of several predefined I/O fields on the contained display member. These predefined fields, objects based on the class IOField, know how to draw and update themselves once they've been told which LCDDisplay to use. They receive Paint and other I/O messages directly from the IOProcess class that created them.
IOProcess also handles basic user input. I needed two input devices for my embedded system: a push-button to tab between input fields on the display and a rotary dial to select a value. There are overrideable IOProcess member functions that handle the hardware interface to these devices. Also, there is a function NextLCDDisplay, a friend to IOProcess, that monitors a toggle switch, which the user flips to select one of the available virtual screens.
As a summary of this overview, let's say I want to create an LCD screen that shows the time. In essence, the classes and objects I need to create look like this:
class TimeField : IOField { virtual void Paint(); ... }; class TimeDisplay : IOProcess { TimeField TimeOutput; ... } MyDisplay;It's a little more complicated than this. The object TimeOutput must be told which LCDDisplay to use, and it must be registered with the object MyDisplay before use. But this is easily and trivially handled in the constructor for TimeDisplay. I discuss this in more detail later.
Graphics LCD Hardware
The Samsung UG24B03, Optrex DMF5005, and Sharp LM24014 are inexpensive 240x64 monochrome graphics LCD displays. All are based on the Toshiba T6963C controller chip. This chip provides a quick and easy eight-bit bus interface with little or no support hardware. The displays are available from surplus for as little as $30, or new from a distributor for $70-$100, depending on quantity and options.
Low-level control of a T6963C-based LCD is a matter of sending it a series of command and data byte values. I accomplished this by mapping the eight-bit LCD port to a specific address in the processor address space. This was done easily by way of programmable chip selects on the target microcontroller. I could then read and write command bytes directly to the LCD controller chip. Details of the hardware, timing, and lowest-level control code are discussed on my website (www.dbit.com/~lansie/lcd.htm).
The LCDDisplay class
Naturally, when working at the level of an output process, I want to think about drawing lines on a screen, not shoveling bytes back and forth. Class LCDDisplay represents the LCD and contains member functions for drawing points, lines, text, etc. This class is also responsible for virtualizing the LCD, so that independent processes can run under the assumption that each has its own separate output screen.
My systems typically have many processes running at once, a small number of which need to send output to the LCD. The user toggles a switch to cycle through the few available output screens created by these processes. Some screens have static text or graphics, others take a long time to fully repaint the screen after a display switch. For these reasons, it's desirable to maintain a copy of screen contents or allow processes to draw even when the user is viewing the output from another process. Allowing each output process to have its own independent virtual screen is a clean and simple model that addresses these issues.
Figure 1 shows the skeleton of the LCDDisplay class and a sample member function for drawing on the screen. A process that wants to create LCD output creates an instance of this class, setting up a 240x64 virtual screen. This virtual screen is immediately usable for drawing via the member functions Plot, Line, and so forth.
One of the most convenient features of many T6963C-based LCDs is that they come with 8Kb of built-in display RAM, enough for four complete 240x64 graphics screens. Drawing pixels is a simple matter of setting the desired bits within this 8Kb range. Then a special command to the display sets the position of the "graphics home address" pointer, telling the physical LCD panel where within the 8Kb it should start displaying.
Since I didn't need more than four virtual screens, I was able to simplify things considerably by giving each virtual screen its own block of memory in LCD RAM. Each new instance of LCDDisplay remembers the start point ("graphics home address") of its block within the 8Kb range. All drawing functions set bits within that block. That allows the processes to draw at will without clobbering one another's output. They don't have to repaint static items either.
The friend functions NextLCDDisplay and FirstLCDDisplay are activated by a user via a toggle switch. They reposition the graphics home address pointer by a special command sent to the LCD, causing one of the extant virtual screens to appear.
The drawing functions were chosen with an eye toward utility and speed. You can draw horizontal lines much faster than vertical lines on a T6963C. This is because horizontal scan lines on the display are mapped to consecutive bytes, and you can set a whole byte (eight horizontal pixels) with one low-level command. A column of eight pixels would take eight separate commands. Wherever practical, the more complex drawing functions rely on LCDDisplay::HLine instead of VLine or Plot.
Full source code for this class (and others discussed in this article) is available on my website and the CUJ ftp site (see p. 3 for instructions on accessing the CUJ ftp site).
IO Processes and Support Classes
My embedded systems incorporate a non-preemptive, round-robin task scheduler for low-priority tasks via a Process class. At first, I created LCD output by letting Process objects create their own instances of LCDDisplay, and then figuring out when and what to draw. Later, I defined the class IOProcess to extend the functionality of the basic Process class. All instances of IOProcess contain an LCDDisplay member object. They receive Paint messages at regular intervals while the virtual screen is visible. They also receive messages corresponding to changes in the user-input push-button and dial. The essentials of the class declaration are shown in Figure 2.
Usage of this class is simple. For each desired output screen, I derive a class from IOProcess, and override the Paint function. Then the system can instantiate the derived class to put up the working output screen.
I've kept things simple for user input. In addition to the toggle switch for selecting different output screens, I have provided one push-button and one rotary dial (a potentiometer). The push-button can be used as an event trigger or (as described later) to tab between input fields. The dial can be used as a general-purpose analog input. The ButtonPress and DialMovedTo functions can be overridden to take appropriate action in the derived classes.
Of course, button presses and dial movement should be sent to an IOProcess object only when that object's Display is visible. (You wouldn't want to take the user's input out of context.) The private base class functionIOProcess::Go (Figure 2) checks to make sure the Display is currently visible before parceling out ButtonPress, DialMovedTo, and Paint messages.
I've designed a further extension to standardize common user I/O. Each output screen naturally breaks down into a collection of rectangular fields, some for output and some for input. I want the user to be able to press the push-button to tab through the different input fields. Then when an input field has focus, the dial can be turned to set a value or select an option.
It seems wasteful to extend IOProcess to draw all desired fields. Instead, I've decided that each field should be an object unto itself, of class IOField. Each IOField encapsulates its own knowledge about how to draw itself. By the default action of its member functions, IOProcess acts as manager for a collection of IOField objects, telling them when to draw themselves and sending them user input. The basics of IOField and a simple text output field are shown in Figure 3.
All types of fields deriving from IOField will typically override the Paint member function. Only input fields will normally override GotFocus, LostFocus, and DialMovedTo.
Once an IOField is instantiated, it must be registered with the IOProcess object that is to manage it. This is the purpose of function IOProcess::RegisterField. Registration tells the IOProcess object the order in which to paint the IOField objects. It also identifies which fields are user-input fields and which ones are to receive GotFocus, LostFocus, and DialMovedTo messages.
The IOProcess object keeps a list of all IOFields registered to it. The details of maintaining this list are pedestrian and omitted for space.
The base member function IOProcess::Paint runs through the list of registered IOFields. It dispatches Paint messages to the individual IOFields in the order that they are to be painted (as defined during IOField registration).
IOProcess::ButtonPress is defined in the base class to capture button presses and move the input focus between the input IOFields. The IOFields expecting input are registered with the desired tab-stop order. On a button press, the IOProcess::ButtonPress function sends the IOField with focus a LostFocus message, finds the next input field in order, and sends it a GotFocus message. A typical response by the IOField is to highlight itself upon receipt of GotFocus and unhighlight itself on LostFocus.
When an object derived from IOProcess uses IOField objects for all I/O, it is not necessary to override IOProcess::Paint. However, if the derived object needs to do something fancier than the base class Paint method, it should remember to call IOProcess::Paint explicitly to make sure that all registered IOField objects get updated properly.
My library contains two more classes derived from IOField: StripChartField and ParmSelectField. (The latter lets the user dial up various system operating parameters for view.) These classes are described in the following example.
An Example
Figure 4 shows the code used to create a two-channel, strip-chart display. Some items have been omitted for clarity, such as the definition of the class PRM, which represents system parameters. (You may wish to refer to my website.) Figure 5 is a picture of the LCD display running the strip-chart process.
The screen is set to update four times per second. There is not much point in going any faster, as the pixels for this type of display take a long time to change state. However, it is important to minimize the number of screen elements that are redrawn during each update in order to keep the update time less than the time it takes to change a pixel. This is particularly true when elements are made up mostly of dots instead of horizontal lines. An eight-bit bus interface just isn't as fast as memory-mapped video RAM. But the example shown exhibits little or no flicker and does not hog the CPU.
Conclusion
Setting up this little class library has made LCD graphics programming a breeze for my systems. All I have to do is instantiate a few classes, and I've got concurrent graphics screens with useful user I/O. Helping to make this possible is an easy-to-use LCD graphics display with virtually no hardware interface requirements. Welcome your embedded systems to the graphics age! o
Edward J. Lansinger has been writing embedded and application code for over fourteen years as an engineer, consultant, and entrepreneur. Recently he has been working in the auto industry and has joined Visteon Automotive Systems to work on advanced chassis and driveline controls. He has a B.S. in Computer and Systems Engineering and an M.S. in Management. He may be reached at lansie@rpi.edu or elansing@ford.com.