 | Level: Intermediate Peter Seebach (developerworks@seebs.plethora.net), Freelance author, Plethora.net
30 May 2006 The Z-machine interpreter is a benchmark for OS functionality and development environments. With the groundwork of porting curses accomplished in Part 3, this article shows you how to complete the Z-machine interpreter, setting it up to use flash memory to save state, without the newfangled luxury of a file system.
The Z-machine is a virtual machine with the unusual design requirement
that it be able to be emulated on a Commodore 64, an Apple II, and an
early 8088-based DOS machine. The initial goal was to allow games to be
written once and run on multiple machines. The games don't need (and
can't have) a great deal of volatile storage, but do have to be able to
access a fairly large amount of raw text. Later versions added some
minimal graphics support, but the fundamental goal is to run text
adventures.
While you might think this would make the program utterly trivial, it is
less trivial than it appears. The Z-machine has cursor addressing
abilities, can (in theory) use both fixed-width and proportional fonts,
and can even perform timed actions in response to user input.
My favorite Z-machine interpreter, and the one I generally use when
testing a development platform, is Frotz. Frotz is reasonably
straightforward, running with essentially no effort on most UNIX®-like
systems.
Having ported the curses library to eCos (see the previous
article), it's time to start on Frotz.
Getting the source
The Frotz distribution (see Resources) contains a few subdirectories for
source; the only ones important for our purposes are the common/ and
curses/ subdirectories, containing the generic Frotz code and the
interface code for UNIX systems with curses, respectively. Getting the
code to compile is fairly trivial; a couple #defines are missing, but that's nothing to break a sweat over.
However, this leaves you with a program which will do nothing but print a
usage message. After all, there are no command line arguments to tell it
which file to load, and indeed, no file system to load the file from. This
is a common issue encountered when porting application software from a
desktop environment into an embedded one. Furthermore, the program
performs sanity checks (it requires the HOME
environment variable to exist, and refuses to run as root), which do not
fit the embedded environment, and must be removed.
Here you face a design decision: should you use a memory device to load the
file, or embed it in the executable? For now, embedding the story file in
the executable is simpler. This defeats the elegant design of a standard
interpreter program for story files, but fits well with the single
executable image design favored by eCos.
The code to manipulate the story file is found in fastmem.c, mostly in
the init_memory function. So, the cheap thing to do is write another file
containing
unsigned char game[] = {
...
};
|
and fill it in with the contents of an actual game file, and then include
this file in fastmem.c. Because the file is internal, you can omit the logic for
calculating the size of a story file; you already know how
large it is. Replacing a couple of calls to fread() with calls to memcpy() gets you a working Frotz executable which,
when loaded on the 3011, can play Zork. This isn't the only remaining
reference to the file system. One reference, easily fixed, is the re-read
of the story file on a restart. That's no problem; just replace it with
memcpy. Loading and saving, though...
Loading and saving
Frotz writes to files in a few cases.
You can save transcripts of
sessions and can save and later play back lists of commands. These features, while excellent, are hard to apply to an embedded
appliance. What would be fairly useful, though, is support for loading
and saving games.
The save format Frotz uses merits mention in passing, because it's an
instance of the IFF file format, which I still think is the world's best
file format; more on this in a bit. What matters, for now, is finding some way to store some data. First off, you need to create a
partition in the flash structure, just so you know where to store your data.
In RedBoot, you do this with the "fis" command. The exact layout of
existing files in flash can vary. I went with:
fis create -b 0x0 -f 0xc3800000 -l 0x20000 save
|
The options specify, in order, the memory location of the data to store
in flash, the address in the flash memory block, and its length. The
flash memory on the 3011 has the address range 0xc0000000 through
0xc4000000. Normally, this would be used to store an image that's already
been loaded into memory, but in this case, it's just a dummy region that the program will use.
At this point, it's time to start storing data. You might think that,
because there's a memory address associated with this, it could just be
read and written from memory; in fact, you have to use the flash driver.
The flash driver
The eCos flash driver exists primarily to serve the needs of RedBoot.
It's not reentrant, and it won't handle interruptions well. You can't
specify a particular flash device; when you initialize the driver, it
finds the flash device on the system and uses it. Conveniently, since
there's usually only one, that's a very suitable interface. The flash
driver provides a somewhat abstracted interface to flash memory; for
instance, it hides the ECC management required by the NAND flash on the
TAMS 3011.
The obvious interface consists of flash_init(), used to initialize the
flash driver, flash_read(), used to read chunks of data from flash memory,
and flash_program(), used to write data to flash memory. In fact, this
isn't quite everything; before you write to flash memory, you must erase
it, using flash_erase(). (I omitted this step during development and
ended up with a program which would get ECC errors trying to read save
files.)
The flash_init function takes a single argument, a pointer to a
printf-like function used for diagnostic messages. One obvious choice
would be the diag_printf function used for diagnostics. Using this is a
good choice during development, because it can be used to get crucial
error messages.
After the flash area is initialized, you can read or write blocks of it.
In practice, a save file for a Z-machine game needs at most 64k for
modifiable data, plus stack storage. In short, it will definitely fit
within 128kB of storage, which is what was allocated above. Even with the
overhead introduced by the file format used for saves, 128kB is plenty.
Reading data is trivial; the flash_read() function is not much more
complicated than memcpy() to use, although it takes an extra parameter for
reporting the addresses of any errors. The flash_program() function is
similar; the only quirk is that you have to erase a block before
programming it.
Save file formats and stdio
However, it would be well beyond foolhardy to simply flash each byte of
memory. What you want is a buffer to read and write from. The code used
for save and restore in Frotz already had #defines for a pair of functions
named put_c and get_c; I extended this to include an f_tell and f_seek
function and replaced the trivial macros with functions manipulating a
simplistic file structure:
Listing 1. File manipulation
typedef struct {
unsigned char *space;
size_t pos;
} MFILE;
int get_c(MFILE *m);
int put_c(int, MFILE *m);
int f_seek(MFILE *m, int, int);
size_t f_tell(MFILE *m);
|
This interface, with tiny little functions (only f_seek is more than two
lines of code), is enough of a subset of stdio to allow for completely
trivial conversion of the save file code in Frotz to read and write to
allocated chunks of memory. Before the restore routine is started, the
chunk of flash defined from RedBoot is read in:
flash_read((void *) SAVE_ADDR, gfp->space, SAVE_SIZE, &v);
|
On the saving end, after the save routine has filled in a block of
memory, the memory can be written out to flash:
flash_erase(SAVE_ADDR, SAVE_SIZE, &v);
flash_program(SAVE_ADDR, gfp->space, SAVE_SIZE, &v);
|
The save format Frotz uses, called Quetzal, is a standardized save
format shared by a number of Z-machine interpreters. The Quetzal format
is actually an instance of the venerable IFF file format, and is
exceedingly flexible. It also, delightfully enough, records the length of
the file in a header, so that the restore routine doesn't rely on getting
an EOF; this means you can just hand the routine the full 128k buffer and
let it read what it wants. The IFF file format is rarely seen these days,
but it remains one of the best combinations of flexibility and feature
set, and it made the porting task easier here.
In the process of adding this, I either commented out or simply removed
references to file names, because this particular implementation doesn't
really provide for files; it just provides for a single save slot.
New features, new bugs
In fact, the restore feature didn't work right away. Z-machine games
contain up to 64k of modifiable memory; the starting values for this
memory are simply part of the save file. The compressed save format (the
default, because it's more efficient) works by saving only bytes which are
different from those in the story file. My initial design, having copied
the game into memory, made a MFILE structure which pointed at the
in-memory copy. This meant that, when it came time to save, the current
in-memory copy was compared to the current in-memory copy, and only
differences were stored. Saving the game produced a save file indicating
that nothing had changed yet, so restoring the save file had no effect.
Making a spare copy of the story file resolved this.
A more subtle bug is that the curses library is easily confused if the
cursor is moved by any other function, for instance, the diag_printf()
calls made by the flash library. You can't just pass a null function
pointer, but you could pass a dummy function that doesn't do anything:
int
say_no_evil(const char *fmt, ...)
{
return 0;
}
|
This resolves the problem, and save and restore both work now. Since the
game doesn't need to ask for a file name, the operations are essentially
instant; the only immediate feedback is that the status bar updates, and
the game prints "Ok."
One feature of the Z-machine that's not used in Zork, but is in some
other games, is the ability to have character input time out. This
feature isn't very common, because text adventures generally eschew timed
events; these are thinking games, not shooters. However, it's certainly
possible to make use of this. I picked a game from the 2002 interactive
fiction competition which uses the feature, to test it out. The game is
called Janitor (see Resources), and it uses the
routine for a few "cutscenes" in which the game inputs characters
automatically. The test was utterly trivial; as expected, it worked on
the first try. (As a coauthor of Janitor, I was greatly relieved.)
Other surprises and quirks
This article hasn't always covered the detailed process by which I
learned how to do things. There are gaps in the eCos documentation, and
you have to be willing to poke around a bit to find things sometimes.
The version of eCos I used for this was actually rather old: September
2005. I spent a good two hours debugging the lack of gettimeofday(), only
to discover that it's been moved into the place I looked first since the
release I was working with.
You cannot always easily figure out which module contains a given feature you're looking for.
In practice, you mostly don't need to. You can simply
use one of the more complete templates (such as the "net" template) and
trust the linker to strip out unneeded features. This will work for most
users. The majority of systems have plenty of free space, and unused code
is dropped at link time.
The command-line ecosconfig tool doesn't give full control over
fine-grained tweaks; to do that, you need to either use the graphical
configuration tool (configtool, which is a Tcl application) or edit files
by hand. Editing files by hand involves learning to read the CDL file
format, which is fully documented in the eCos reference manual. If you're
coming to eCos for the first time, allow a bit of time to get familiar
with the tools.
Coming up next: Having looked at porting applications to eCos, I'll
move on to the RedBoot boot loader, which is itself an eCos application.
RedBoot has been used as a host environment for other OSes, and it offers
many of the sorts of features (loading files over tftp, manipulating
flash) that people often wish a bootloader had. A more detailed look at
its capabilities and design is perhaps in order; join us then.
Resources Learn
Get products and technologies
Discuss
About the author  | 
|  | Peter Seebach thinks 64k is enough for anyone. |
Rate this page
|  |