Introduction to Microwindows Programming
In the fast-changing world of embedded, handheld and wireless devices, there are many hardware and software design changes taking place. Many devices now feature 32-bit microprocessors from Intel, MIPS and Motorola, as well as larger LCD graphical displays. In order to leverage the significant results gained in the desktop arena the last ten years, many developers are turning to desktop-like operating systems with these new embedded designs. One of the most promising emerging developments is running Linux in these environments, for a couple of good reasons: Linux on embedded systems brings with it the entire power of desktop computing, along with many solutions already available. Linux, being open source, allows any aspect of the solution to be fully understood and then customized for a particular application.
The Microwindows Project is an open-source project aimed at bringing the features of modern graphical windowing environments to smaller devices and platforms running Linux. Designed as a replacement for the X Window System, Microwindows provides similar functionality using much less RAM and file storage space: from 100K to 600K. The architecture allows for ease in adding different display, mouse, touchscreen and keyboard devices. Starting with Linux version 2.2, the kernel contains code to allow user applications to access graphical display memory as a frame buffer, which ends up being a memory-mapped region in a user process space that, when written to, controls the display appearance. This allows graphics applications to be written without necessitating knowledge of the underlying graphics hardware or using the X Window System. This is the way that Microwindows typically runs on embedded systems.
Microwindows fully supports the new Linux kernel frame buffer architecture and currently has support for 1, 2, 4, 8, 16, 24 and 32 bits per pixel displays, with support for palettized and truecolor display color implementations, as well as grayscale. When programming applications, all colors are specified in the portable RGB format, and the system uses routines to convert to the nearest available color or shade of gray for monochromatic systems. Although Microwindows fully supports Linux, its internal, portable architecture is based on a relatively simple screen device interface and can run on many different RTOSes as well as bare hardware. This is beneficial: graphics programming by the customer can be shared between projects and even run on different targets with different RTOSes, without having to rewrite the graphics side of the application.
The Microwindows system supports host platform emulation of the target platform graphically. That is, Microwindows applications for Linux can be developed and prototyped on the desktop, then run and tested without having to cross-compile and run on the target platform. This is accomplished using Microwindows' X screen driver, rather than the frame buffer driver, where the target application is run on the desktop host and displayed within an X window. The driver can be told to emulate exactly the target platform's display in terms of bits per pixel and color depth. Thus, even though the desktop system is 24-bit color, it can display a 2bpp grayscale for previewing the target application. Since both the host and target are running Linux, almost all operating system services are available on the desktop host.
Microwindows was designed as an attempt to bring applications to market quickly with minimum effort. In order to accomplish this, I felt that designing yet another graphics applications programming interface (API) would steepen the learning curve, thus discouraging interest and increasing time-to-market. Microwindows implements two popular graphics programming interfaces: the Microsoft Windows Win32/WinCE graphics display interface (GDI), used by all Windows CE and Win32 applications, and an Xlib-like interface, known as Nano-X, used at the lowest level by all Linux X widget sets. This allows the extremely large pool of Windows programming talent to be used in developing the graphical side of the application, as well as being familiar to the core group of Linux graphics programmers used to working with X.
In this article we'll build a sample application using the Nano-X API and discuss the issues associated with lower-level Nano-X programming. The Nano-X API allows applications to be built using a client/server protocol over a network or local UNIX domain socket. This allows several applications, running on the embedded device or a remote host, to connect to the Microwindows server for display. In this way, Nano-X programs operate much like clients using the X Window System. The Nano-X API is similar to X's Xlib library, being quite low-level and concerned mostly with creating and destroying windows and basic graphics drawing functions. Because Microwindows was designed to be small, many options can be set using a configuration file supplied with the source package. I'll cover some of these options so that we can first build a working Nano-X server.
The Microwindows source package, available at http://microwindows.org, must be compiled first to build a Nano-X server for your particular host or embedded target platform. Most all options are contained in a configuration file. After unpacking the tar package, you must first change the directory to the microwin/src directory and edit the config file. Here's some of the most important options:
ARCH=LINUX-NATIVE ARCH=LINUX-ARM ARCH=LINUX-MIPS ARCH=LINUX-POWERPC
Setting the ARCH option to LINUX-NATIVE tells the system to create programs for the host Linux system you're currently running. If you want to cross-compile for a RISC processor target machine, set ARCH to one of the other options. Microwindows uses the Arch.rules file to hold the specific settings for each of these options. Image support is compiled into the Nano-X server by setting any of the following options:
HAVE_BMP_SUPPORT=Y HAVE_GIF_SUPPORT=Y HAVE_JPEG_SUPPORT=YIf you set the option for JPEG images, you must also indicate the location of an external JPEG decompression library using LIBJPEG=/usr/lib/libjpeg.a, for example. This library comes precompiled on most systems but is also available from the Microwindows web site.
The next major configuration item to concern yourself with is whether to compile in support for scalable font support. By default, Microwindows includes support for compiled-in fixed size bitmap fonts specified in the drivers/genfont.c file. In addition, if you're going to want to display larger fonts, for instance to run an embedded browser, Microwindows can compile in support for TrueType or Adobe Type 1 fonts. When either of these options is compiled in, you can specify a certain font file and pixel size for font rendering, and Microwindows will generate the appropriately sized font from that external font file. Recently, we've also added support for external Chinese fonts as well. All font characters can be specified using an 8-bit ASCII index, Unicode-16 or UTF-8, which is a byte-stream encoding for Unicode. The following options control compiling in font support:
HAVE_FREETYPE_SUPPORT=Y HAVE_T1LIB_SUPPORT=Y HAVE_HZK_SUPPORT=Y
The FreeType and T1lib external libraries are used to provide support for TrueType and Adobe Type 1 fonts, respectively. These libraries must be previously compiled and their location specified in the config file as well. Both libraries are also available from the Microwindows web site.
Since Microwindows is capable of running on frame buffer systems and within X, a few configuration settings are required to specify options available for each of these screen drivers. If you're already using a Linux desktop running X, it's best to build the system first using the X screen driver, then later create a frame buffer version for your embedded device. To configure the X screen driver, set the following options:
X11=Y SCREEN_WIDTH=640 SCREEN_HEIGHT=480 SCREEN_PIXTYPE=MWPF_TRUECOLOR0888
These options tell Microwindows to run within a virtual 640x480 window on the X desktop and to run programs using an output color model of eights bits each for red, green and blue. By changing these settings, you can control the emulation of a target embedded device on your desktop. For instance, to emulate a 16-bits per pixel display, use SCREEN_PIXTYPE=MWPF_TRUECOLOR565. These MWPF constants are explained in more detail in the src/include/mwtypes.h header file.
Setting up the frame buffer display is a bit more complicated since you have to be sure that your Linux system kernel is compiled for frame buffer support. Set the following options for most frame buffer systems:
X11=N FRAMEBUFFER=Y FBVGA=Y VTSWITCH=Y PORTRAIT_MODE=N
The FBVGA option compiles in support for a 16-color VGA planar mode screen driver. This option won't typically be used with embedded systems, however. The VTSWITCH option allows Microwindows to run on the console frame buffer but switch to another virtual console when an Alt-function key is pressed. For some embedded systems, this option must be turned off. Finally, the PORTRAIT_MODE option set to R or L builds a server that will run tilted either right or left, which is suitable for systems like the new Compaq iPAQ PDA.
If you get Can't open /dev/fb0 when you run the Nano-X server, it typically means that you don't have open permission or your system kernel doesn't have a frame buffer driver compiled in. The simplest way to tell is whether you see a graphical penguin logo when you boot your system. If not, make sure that at least the following options are set in the /usr/src/linux/.config file:
CONFIG_FB=y CONFIG_FB_VGA16=y CONFIG_FBCON_VGA=y CONFIG_FBCON_CFB4=y CONFIG_FBCON_CFB8=y
If you have a supported graphics card other than standard, old-fashioned VGA, you can select an option other than CONFIG_FB_VGA16. Before rebuilding the kernel, make sure that the original kernel is saved with a backup entry in the lilo.conf file. As you can see, building a frame buffer kernel is not for the faint of heart, which is why I recommend using the X screen driver initially. Most embedded systems come configured running the frame buffer as standard.
The last major configuration item is telling Microwindows what device is used for mouse or touch-screen input. Microwindows currently supports mice using the GPM utility or directly using a serial port. The easiest is probably setting GPM support using GPMMOUSE=Y and then running the gpm utility: `gpm -R -t ps2' for a PS/2 mouse, for instance. To set up a mouse using a serial port, use SERMOUSE=Y and set the MOUSE_TYPE and MOUSE_PORT environment variables as documented in src/drivers/mou_ser.c.
Luckily, we only have to set the options once in the config file when they can reside untouched. There's also a bunch of sample config files for various platforms in the src directory. To build your Nano-X server, as well as all the sample demo applications, make sure you're in the microwin/src directory and then type make. All programs are created in the microwin/src/bin directory, and client-linking libraries are put into the microwin/src/lib directory. To run demo applications, first run the Nano-X server (bin/nano-X) and then run the application:
bin/nano-X & sleep 1; bin/world
The sleep command is used to give the server a moment to initialize before running the demonstration world-plotting program on the display. The normal build also constructs some of the Win32 demonstration programs. These programs contain the application and the server linked together and don't require a separate Nano-X server. Some fun demos to run are the bin/mine landmine program or the bin/mdemo window demonstration.
Microwindows has a number of Nano-X demos, all in the microwin/src/demo/nanox directory. Currently, there's a terminal emulator nterm, sample window manager nanowm, as well as a font demonstration program. These samples are invaluable for learning about small Nano-X programs. In the next section, we'll build a Nano-X program from scratch.
We'll start out by building the simplest possible Nano-X application: one that draws a white box with a blue border (see Listing 1)
#define MWINCLUDECOLORS #include <stdio.h> #include "nano-X.h" int main(int ac,char **av) { GR_WINDOW_ID w; GR_GC_ID gc; GR_EVENT event; if (GrOpen() < 0) { printf("Can exit(1); } w = GrNewWindow(GR_ROOT_WINDOW_ID, 20, 20, 100, 60, 4, WHITE, BLUE); gc = GrNewGC(); GrSetGCForeground(gc, BLACK); GrSetGCUseBackground(gc, GR_FALSE); GrSelectEvents(w, GR_EVENT_MASK_EXPOSURE); GrMapWindow(w); for (;;) { GrGetNextEvent(&event); switch (event.type) { case GR_EVENT_TYPE_EXPOSURE: GrText(w, gc, 10, 30, "Hello World", -1, GR_TFASCII); break; } GrClose(); return 0; }
After configuring and testing the initial Microwindows installation, use make install to install the Nano-X server, client libraries and header files. Then compile link and run our sample program by typing:
gcc sample.c -o sample -lnano-X nano-X & sleep 1; sample
Press escape to exit; this special key will cause the server to exit.
The GrOpen() call tries to open a connection to the running Nano-X server. If the server isn't running, -1 is returned and the application exits with an error message. Then, a 100x60 pixel window is created at location 20,20 on the screen with the GrNewWindow call. The border size is specified as four pixels wide with a white interior and blue border color. Creating a window doesn't actually display it on the screen until the GrMapWindow call is made. The reason is that sometimes it's convenient to first create a set of windows but display and remove them from the screen according to user actions. After mapping the window to the screen, the program enters an event loop that waits for the next mouse or keyboard event, although nothing is done with it.
More complex Nano-X applications almost always follow this same logical structure: first windows are created, then mapped and then the program waits in an event loop for user action to take place. In our sample, we haven't actually written any explicit code to draw anything in the window. Before we try this, however, it's important to understand the concept of expose events. The Nano-X API provides a full suite of functions that draws lines, text, circles and images on the screen. When a the window is obscured, Microwindows handles clipping the drawing so that any windows above the drawn window aren't changed. However, when a previously obscured portion of a window becomes visible, that portion of the window must be redrawn. When this happens, the server sends an expose event to the application, requesting that it redraw its window contents. The tricky part is that for good program design, the code that originally draws the window contents and the code that redraws the contents should be the same code, in the same place. Specifically, the expose event routine can draw the original contents in the first place. This works because Microwindows sends an expose event immediately after mapping the window. Let's use this technique to draw some text in our sample program and, also, redraw the text when the window is moved (see Listing 1)
In order to test our expose event code, we'll run Nano-X with the NanoWM window manager so that we can move windows around. Here's the command line:
bin/nano-X & sleep 1; bin/nanowm & sleep 1; sample2
In the sample2 program, we've added the GrSelectEvents function to tell Microwindows to send GR_EVENT_TYPE_EXPOSURE events to our client program. In order to keep client/server traffic down, the server will only send selected events to each client window. The "Hello World" text display is handled in only one place, which is our expose event routine. An expose event is generated immediately after the GrMapWindow call, so that the text displays even though the window hasn't actually moved yet. In the GrText call, you may be wondering what the gc parameter is. We'll now introduce the notion of graphics contexts.
When drawing objects such as lines or text, there are many parameters that can be specified for each drawing function call that affects the operation. Besides the minimum information required for each call, such as the line starting points, other ``contextual'' information is kept by the system so that it doesn't have to be specified for every drawing function. Items such as foreground and background color, XOR vs. OR drawing modes and others parameters are all kept in a server structure known as the graphics context. In our sample above, a new graphics context is created with GrNewGC, which creates a graphics context with standard settings. Then, an additional call is made to set the foreground text color using GrSetGCForeground. During the expose event processing, the graphics context is passed along with the window ID to specify all the text drawing parameters.
Now that you've seen the very basics of creating programs, windows and graphics contexts, we can explain some of the other functions Microwindows provides in its graphics arsenal. Table 1 is a short detail of the different drawing functions.
GrClearWindow | Clear a window to its background color |
GrPoint | Draw a single point |
GrLine | Draw a lind |
GrRect | Draw a rectangle outline |
GrFillRect | Draw a filled rectangle |
GrEllipse | Draw an ellipse or circle outline |
GrFillEllipse | Draw a filled ellipse or circle |
GrArc | Draw an arc outline or pie wedge |
GrArcAngl | Like GrArc, but uses floating point and angles |
GrPoly | Draw a polygon outline |
GrFillPoly | Draw a filled polygon |
GrBitmap | Draw a bitmap image |
GrDrawImageFromFile | Draw a BMP, GIF or JPEG file from disk |
GrDrawImageToFit | Draw a cached image and stretch to fit |
GrArea | Draw from a memory array of pixels |
GrCopyArea | Copy a rectangular area from one window to another |
Microwindows also supports a type of window that is never displayed on the screen, known as a pixmap. Pixmap windows are sometimes called off-screen windows and are never directly displayed on the screen but, instead, copied from by using GrCopyArea. Pixmaps are useful because sometimes it's too CPU--or time-intensive to regenerate a window's contents during expose events, and normal windows never save their contents when obscured or unmapped. A pixmap is created using the GrNewPixmap function and can be used wherever a window is used.
Hopefully this introduction has helped you understand how a small system can be used to enable more sophisticated applications in the embedded Linux market. More documentation is available on the microwindows.org web site. In next month's article, we'll dig deeper into the Microwindows API and discuss Microwindows' architecture and how it works.
email: greg@censoft.com