4.8. Programming ExamplesIn this section, we will look at examples of programming in the Open Firmware environment. While doing so, we will also come across several device support extensions implemented by Apple's version of Open Firmware. These device-specific extensions allow programs to access the keyboard, mouse, real-time clock (RTC), NVRAM, and so on. 4.8.1. Dumping NVRAM ContentsIn the example shown in Figure 44, we dump the entire contentsin a raw formatof the NVRAM device. The following relevant methods are provided by the NVRAM device: size ( -- <size in bytes> ) seek ( position.low position.high -- <boolean status> ) read ( <buffer address> <bytes to read> -- <bytes read> ) Figure 44. Dumping NVRAM contents
We first open the NVRAM device and query its size, which is reported to be 8KB (0x2000 bytes). We allocate an 8KB buffer to pass to the read method. Before we read from the device, we seek to its beginning. We use the dump word to display the contents of the buffer in a meaningful format. Among the NVRAM's contents, you can see the computer's serial number and the various Open Firmware variables. 4.8.2. Determining Screen DimensionsIn this example, we call the dimensions method of the screen device to retrieve the horizontal and vertical pixel counts of the display associated with the device. Alternatively, the screen-width and screen-height words can be used to query this information. 0 > showstack ok -> <- Empty 0 value myscreen ok -> <- Empty " screen" open-dev to myscreen ok -> <- Empty " dimensions" myscreen $call-method ok -> 1280 854 <- Top 2drop ok -> <- Empty 4.8.3. Working with ColorsIn Open Firmware's default (8-bit) graphics model, each pixel is represented by an 8-bit value that defines the pixel's color. This valuethe color numbermaps to one of 256 colors according to entries in a color lookup table (CLUT). Each entry is a triplet of red, green, and blue (RGB) values. For example, the default CLUT defines color number 0 to be blackcorresponding to the (0, 0, 0) RGB tripletand defines color number 255 to be whitecorresponding to the (255, 255, 255) RGB triplet. The color! and color@ methods of the display device allow individual CLUT entries to be set and retrieved, respectively. color@ ( color# -- red blue green ) color! ( red blue green color# -- ) get-colors ( clut-dest-address starting# count -- ) set-colors ( clut-src-address starting# count -- ) get-colors and set-colors, respectively, can be used to retrieve or set a range of consecutive colors, including an entire CLUT. 0 > showstack ok -> <- Empty 0 value myscreen ok -> <- Empty " screen" open-dev to myscreen ok -> <- Empty 0 " color@" myscreen $call-method ok -> 0 0 0 <- Top 3drop ok -> <- Empty 255 " color@" myscreen $call-method ok -> 255 255 255 <- Top 3drop ok -> <- Empty foreground-color ok -> 0 <- Top drop ok -> <- Empty background-color ok -> 15 <- Top " color@" myscreen $call-method ok -> 255 255 255 <- Top 3drop ok -> <- Empty 256 3 * buffer: myclut ok -> <- Empty myclut 0 256 " get-colors" myscreen $call-method ok -> <- Empty myclut 256 3 * dump ffbbc000: 00 00 00 00 aa 00 aa 00 00 ... ... ffbbd2e0: d5 fd 68 ... ff ff ff -> <- Empty The foreground-color and background-color words, respectively, fetch the color numbers of the foreground colorsdefined to be 0 (black)and background colorsdefined to be 15 (white). Note that color number 15 also maps to the white color in the default CLUT. This is in accordance with Open Firmware's 16-color text extension, which states that the display driver shall initialize the first 16 colors per a predefined list. 4.8.4. Drawing a Color-Filled RectangleOpen Firmware's graphics extension standard provides a method to draw a color-filled rectangle (fill-rectangle), a method to draw a rectangle using a specified pixel map (draw-rectangle), and a method to read a rectangular pixel map from the display buffer (read-rectangle). Using these methods as primitives, more sophisticated drawing routines can be constructed. draw-rectangle ( src-pixmap x y width height -- ) fill-rectangle ( color# x y width height -- ) read-rectangle ( dest-pixmap x y width height -- ) The following program draws a black rectangle that is 100 pixels wide and 100 pixels tall, with its top-left corner at the center of the screen. \ fill-rectangle-demo \ fill-rectangle usage example 0 value myscreen " screen" open-dev to myscreen 0 value mycolor \ color x y width height mycolor screen-width 2 / screen-height 2 / 100 100 " fill-rectangle" myscreen $call-method Running the fill-rectangle-demo program, say, by "booting" it using the TFTP method, should draw the desired black rectangle. Note that the screen's origin, that is, position (0, 0), is located at the top left of the physical display. 4.8.5. Creating an Animated Solution to the "Towers of Hanoi" ProblemGiven the ability to draw a rectangle at a specified location on the screen, let us look at a more complex example: an animated solution to the Towers of Hanoi problem.[13] We will use the ms word, which sleeps for a specified number of milliseconds, to control the rate of animation. Figure 45 shows the layout and relative dimensions of the objects we will draw on the screen.
Figure 45. The Towers of Hanoi: layout and relative dimensions of on-screen objectsThe code for the program can be conveniently divided into two parts: the code for animating and the code for generating moves for the Towers of Hanoi problem. We will use the stack-based algorithm shown as pseudocode in Figure 46 for solving an N-disk Towers of Hanoi problem. Figure 46. The Towers of Hanoi: simulating recursion using a stack
The movedisk function in Figure 46 is required to graphically move a disk from one tower to another. It could be broken down into distinct steps from an animation standpoint, corresponding to the horizontal and vertical motion of the disk. For example, moving a disk from the left tower to the right tower requires us to first move the disk up on the source tower, move it to the right so that it reaches the destination tower, and finally move it down until it comes to rest in its appropriate position on the destination tower. The code shown in Figure 47 is the first part of the program that provides the following key functionality:
Figure 47. The Towers of Hanoi: Forth code for animation
The functions are subdivided into smaller functions. The hanoi-disk-move function is a harness function that is equivalent to movedisk in Figure 46. Now that we have an implementation of movedisk, we can implement the algorithm in Figure 46, which will give us a complete implementation. Figure 48 shows the remaining part of the overall program. Note that we will provide the end user with a simple Forth word called hanoi, which requires one argumentthe number of diskson the stack. Figure 49 shows a screenshot of the running program. Figure 48. The Towers of Hanoi: Forth code for the program's core logic
Figure 49. Actual photo of the Towers of Hanoi program in Open Firmware
4.8.6. Fabricating and Using a Mouse PointerIn this example, we will write a program to move a "pointer"one that we will fabricateon the screen using the mouse. Moreover, clicking a mouse button will print the coordinates of the click on the screen. We will use the fill-rectangle method to draw, erase, and redraw the pointer, which will be a small square. Opening the mouse device gives us access to its get-event method. get-event ( ms -- pos.x pos.y buttons true|false ) get-event is called with one argument: the time in milliseconds to wait for an event before returning failure. It returns four values: the coordinates of the mouse event, a bit mask containing information about any buttons pressed, and a Boolean value indicating whether an event happened in that interval. An interval of zero milliseconds causes get-event to wait until an event occurs. The event coordinates returned by get-event may be absolute (for a device such as a tablet), or they may be relative to the last event, as in the case of a mouse. This implies that the pos.x and pos.y values should be treated as signed or unsigned depending on the type of device. This may be programmatically determined by checking for the presence of the absolute-position property. The mouse demonstration program is shown in Figure 410. It starts by drawing a pointer at position (0, 0) and then goes into an infinite loop, waiting for get-event to return. Note that this programand Open Firmware programs in generalcan be interrupted by typing control-z. Figure 410. Fabricating and using a mouse pointer in Open Firmware
Because we are using a mouse, get-event will give us the new position relative to the old one. Therefore, we need to remember the old coordinates. Once we get the new position, we will erase the old pointer and draw one in the new position. For the sake of simplicity, we will not handle the case when the pointer is moved "outside" one of the edges of the screen. Moreover, our mouse pointer is essentially an eraser in the drawing sense too: Since we will not save the region under the pointer, anything that the pointer moves over will be erased as we will simply redraw the newly uncovered region using the background color. We can also create an arbitrarily shaped mouse pointer, including a familiar arrow-shaped one that is partially transparent, by using a well-known masking technique. Suppose we wish to create a 5x5 pointer in the shape of an X. If C is the pointer's color and S is the screen background color, then the 5x5 square containing the pointer would look like the following when displayed on the screen: C S S S C S C S C S S S C S S S C S C S C S S S C We can achieve this effect by having two masks: an AND mask and an XOR mask, as shown in Figure 411. Figure 411. AND and XOR masks for an X-shaped pointer
While displaying the cursor on the screen, we use the following sequence of operations, which yields the desired 5x5 square: Snew = (Scurrent AND A) XOR X We now need to maintain in-memory bitmaps for the pointer and the region underneath it. Before drawing the contents of the pointer's bitmap on the screen (using draw-rectangle instead of fill-rectangle), we need to perform the masking operation, which will give us the desired partially transparent mouse pointer. 4.8.7. Stealing a FontApple's Open Firmware includes the terminal-emulator support package, which presents a display framebuffer device as a cursor-addressable text terminal. Another support package, fb8, provides generic framebuffer routines that can be used by display device drivers to perform low-level operations. Thus, there are several ways to display characters on the screen in Open Firmware. We shall devise yet anotherrather contrivedway in this example. We will create a function called font-print that takes an input ASCII string and draws it on the screen, starting at a specified pixel location. To achieve this, we will use the display device's draw-rectangle method, which requires a memory address containing data for the rectangle to be drawn. We can consider each character in a font to be contained in an imaginary rectangle. Our program will perform the following operations.
Although these steps appear straightforward, the first step of creating a font is rather arduousat least in our context. We will bypass this step in our example by stealing Open Firmware's default font. As our program is booted by Open Firmware, we will output on the screen a template string containing all ASCII characters of interest. Open Firmware provides Forth words to determine the height and width of a character: char-height and char-width, respectively. Since we have a priori knowledge that our string will appear on the first line of the screen, we know the position and dimensions of the screen region containing the template string that we print. We will simply copy this region using read-rectangle, which will give us a ready-made font buffer. Figure 412 shows the implementation of the font-print word. Figure 412. Pixel-addressable printing in Open Firmware made possible by stealing a font
4.8.8. Implementing a ClockGiven the functionality of font-print from Figure 412, we can make a clock appear, say, in a corner of the screen. We will use two additional functions for this: one to retrieve the current time and another that will allow us to update the clock every second. Open Firmware provides the get-time function, which retrieves the current time. Calling the function results in six items being pushed on the stack: 0 > decimal get-time .s -> 32 16 12 20 3 2004 <- Top ok The items are (from the bottom to the top on the stack): seconds, minutes, hours, day of the month, month, and year. For our clock, we will discard the date-related items. The alarm function allows us to periodically invoke another function. Thus, we can arrange for a clock-drawing function to be invoked every second. alarm takes two arguments: an execution token of the function to be periodically invoked and the period in milliseconds.
The method to be periodically invoked through alarm must neither consume any stack items nor leave any items on the stack once it has finished. In other words, this function's stack notation must be ( -- ). A function's execution token is its identification. ['] returns the execution token of the function name that follows it, as shown by the following example: 0 > : myhello ( -- ) ." Hello!" ; ok 0 > myhello Hello ok 0 > ['] myhello . ff9d0a30 ok 0 > ff9d0a30 execute Hello ok Given an execution token of a function, the execute word can be used to execute the corresponding function. Note that retrieving a function's execution token is context-specific: ['] is not a valid way to get a method's execution token inside a word definition, for example. The code shown in Figure 413 creates a clock that updates every second. It is displayed at the top-right corner of the screen. Note that (u.) converts an unsigned number into a text string, which is what font-print requires as one of the arguments. Figure 413. A clock implemented in the Open Firmware environment
4.8.9. Drawing ImagesIn this example, let us examine how to draw images in Open Firmware. In fact, we have already encountered all the functionality required to do so: The draw-rectangle function can draw a memory buffer's contents to screen. The buffer requires the image data to be in an appropriate format. We can make the task easier by choosing to draw the Apple logo drawn during booting, since we can find the corresponding data in the correct format in the bootloader's source code. Drawing the Apple logoor any image in generalwill require the logo data and the custom CLUT (if one is needed) to be in memory. The Apple logo data can be found in a C header file called appleboot.h in BootX's source (bootx.tproj/sl.subproj/appleboot.h). The custom CLUT can be found in another header fileclut.hin the same directory as appleboot.h. Both files contain byte arrays that can be readily used with Open Firmware. For example, the CLUT data can be simply passed to set-colors. Thus, we can draw the Apple logo using the following steps.
If you wish to draw an arbitrary image, you could do so by converting the image to a format that makes it easy to view the RGB values for each pixel. The ASCII-based portable pixmap (PPM) is such a format. Given a PPM file, we could write a script that reads the file and generates Forth versions of both the CLUT and the image data. Consider the example of a 4x4-pixel image, whose PPM file looks like the one in Figure 414. Figure 414. PPM image data for a 4x4-pixel image
The first line shown in Figure 414 is a magic number.[14] The second line contains the image's width and height. The value 15 on the third line specifies the maximum decimal value that a color component has. The last four lines contain RGB values for each of the 16 pixels of the image. Since this image has only three distinct RGB triplets, our custom CLUT needs only three entries:
decimal 0 0 0 0 color! \ CLUT entry with index 0 15 0 15 1 color! \ CLUT entry with index 1 0 15 7 2 color! \ CLUT entry with index 2
Since Open Firmware's 8-bits-per-pixel model means a CLUT can have at most 256 entries, you may need to reduce the number of colors in an image before you draw it using the approach described in this example. 4.8.10. Creating WindowsGiven the various examples discussed so far, we are now in a position to create a window that can be dragged around using the mouse. Doing so may be a worth-while exercise for those interested in learning how to create graphical environments from scratch. We could combine multiple techniques in the following way.
Figure 415 shows a rudimentary implementation in Open Firmware of a window that can be dragged.[15]
Figure 415. A window created using Open Firmware primitives
Open Firmware provides various other types of functionality that is beyond the scope of this book. For example, you can "talk" to IDE, SATA, and SCSI drives directly in Open Firmware, thus allowing you to fabricate your own command packets to such devices and perform I/O. |