My apologies about this issue being posted later than was mentioned in a preview post on comp.sys.cbm newsgroup. Due to some problems with coding, school and Murphy's law the issue had to be delayed until now.
I have asked the system admin's at my site concerning a mail-server but they said they did not have enough man-power (go figure) to get somebody to run it. I will be implementing a mail-server system in my account in the near future for retrieval of programs and back-issues. I'll post descriptions of how to use it it the next issue of C= Hacking as well as on the newsgroup comp.sys.cbm when I finish writing it.
In this issue of C= Hacking we also start on an ambitious task: Developing a game for both the C128 and C64 modes that includes all of the features found in commercial games. Take a look in the Learning ML Column for more information.
Also, The article concerning the 1351 mouse has _again_ been delayed due to time constraints. Rest assured that it will be in the next issue of C= Hacking.
If you are interested in helping write for C= Hacking please feel free to mail duck@pembvax1.pembroke.edu (or duck@handy.pembroke.edu). We're always looking for new authors on almost any subject, software or hardware.
*** AUTHORS LISTED BELOW RETAIN ALL RIGHTS TO THEIR ARTICLES ***
Note | I am looking for someone to help aid music composition that will -----+ be introduced in a later issue. Programming of the 6502 is helpful but not a requirement. Please email me at duck@pembvax1.pembroke.edu if you are interested. Many thanks to Pasi Ojala for his work with the graphics in this program. Also please note: This entire program has been assembled sucessfully with the Buddy-128 assembler for both the C=128 and C=64 version. Due to the length of the source files (over 1,500 lines) I'm not sure if Buddy-64 will handle it. Thus if you get errors during assembly, all I can say is: sorry. If this is the case then the next issue will handle dividing the program and data up into segments which can then each be loaded seperatly.
In addation there will be several source files and some miscellaneous include
files for graphics and sound. For those of you who are or will be converting
the assembly source over to a different assembler the conditional
assembly directives .if (condition) will only be true if the condition is
non-zero. Ie: if the symbol computer is defined as 128 then the following
example illustrates it:
In addition the program will show you how to use IRQ interrupts to simplify
programming. We will be using them to play music in the background on three
voices (sound effects will temporarily pre-empt the third voice from playing).
Also animation of characters will be done via the IRQ. A little background
on interrupts for those of you who are a bit hazy on what they are or have
never seen them before (Also try taking a look at Rasters: What they are and
how to use them in C= Hacking #3 - While this does not necessarily cover
what we are going to be using interrupts for it does describe them quite
well.) Basically the computer generates an interrupt every 1/60th a second
from a timer on the computer (usually from the CIA chip or the screen for
those of you who are curious). The computer will save all the registers, jump
to a subroutine - perform the instructions there (usually updating time,
scanning the keyboard etc...) and then recall all the registers and return
to the user program. This is an interrupt. An IRQ interrupt describes an
interrupt that we can allow to be "turned on" and "turned off" - ie: we
can temporarily disable it if we have to. A NMI interrupt describes an
interrupt which we can _not_ temporarily disable -- we will not be using
NMI interrupts in this program.
Let's take this approach to Space Invasion:
Problem Statement: Build a Space Invader program called Space Invasion.
Usually, given a problem you have to re-work the problem statement to
encompass all of what you want. Let's try again:
Problem Statement: Develop a Space Invader program called Space Invasion
utilizing the 64 or 128 screen with interrupt driven
music / sound, and allowing input from the keyboard,
joystick or mouse.
Hmmm... The problem statement listed above is better but it has no real order;
we have no clear idea of where to start and what we need to do. It does
however tell us that we have the following sections:
Shrew! Long list 'eh? - Now you may have thought of some not listed above,
and we may have possibly overlooked some crucial routines -- that's fine --
the above is just intended as a building block - a place to start coding from.
If we think of these as subroutines we can build a skeleton outline of the
program - yet we need some order in how we call them. Obviously we aren't
going to move the player until we scan the input and that requires prior
device selection etc...
Hmm... Taking order into account we can re-state the problem as:
Problem Statement: Develop a game similair to Space Invaders called Space
Invasion by initializing memory, the display device,
setting up Custom Characters, setting up the Music
Registers and displaying the title screen. From there,
select the input device and after that setup the current
level. Next, while playing music in the background and
scanning the input device, move the aliens, missles and
player checking for collisions and taking appropriate
action as required (player dies, score increases etc or
what-not). After each level display if the player is dead,
or set-up for the next level and repeat. When the game has
ended update the high score if necessary.
Try saying that five times real fast! :-) But that problem statement is a
whole lot better than the one we had at the beginning which simply said to
develop a game.
Originally, I was planning on updating and listing the revised code in each
issue. However, due to space limitations and the enormity of the program
currently (1,500+ lines!!) it will be placed for anonymous ftp at
ccosun.caltech.edu under the directory: pub/rknop/HACKING.MAG.
Our main loop for this program will be:
In the file: invasion.src most of the statements above are commented out.
Once we write the routines we'll un-comment them. For now, this serves to
still remind us of the routines we need to write.
Also there are a couple of programming tricks that I used in the main loop
that probably need some clarifying.
When handling the collisions the .X register is loaded with the result of the
collision checking - $00 = no collisions, $01 = player died, $02 = alien died,
$03 = end of level. Anytime a load to a register is done the flags are
automatically set as if you had compared it to 0 - hence we can ldx the
collision flag and immediately branch if equal to zero for no collisions. In
addition to the load anytime the .X or .Y registers are incremented or
decremented an implicit comparison to zero is performed. So if the .X register
is 1 previously, we decrement it then it will be zero and our BEQ instruction
will branch. If it's two then it will be one and we can continue like this.
[NOTE: Technically it's not a real comparison to zero but calling it a
comparison to zero servers our purpose here. The only significant difference
would be in the effect of the carry flag which is insignificant in our
code segment here.]
Also in several locations are the two instructions:
There are some problems with copying the 'system' characters, however. The
Commodore 64 usually masks out the character set and typically it is only
available to the VIC chip so that more space can be present for user programs
and such. It also takes up the section of memory that the I/O block in
$d000-$dfff does so that switching it in while interrupts are enabled is sure
to result in a crash.
We're also going to be doing a few things that you may not expect -- instead
of copying all 256 characters - we're gonna _just_ copy the first 128. This
will give us all of the normal characters as the last 128 are the reverse-
video counterparts to the first 128 characters. We're doing this to conserve
space and because we really don't need that many characters defined.
Also location $01 contains what $d000-$dfff holds and we will have to modify
bit 2 to switch the character ROM in. Hence, the following program code is
used to copy the character set:
The 8563 has a character set in much the same way the VIC chip does, save
one exception - each character set can have up to 16 lines. Normally, the last
eight lines are filled with $00 and are not shown. (Provisions can be made to
have 8x16 characters but it is not needed for this game and thus, will not be
shown - For more information See C= Hacking Issue #2: 8563: An In-Depth Look.)
Thus the algorithim is similair to the C=64 but 8 zero-bytes will need to be
written at the end of every eight bytes read.
However, the 8563 does make things easier for us! - When the computer is first
turned on a copy of the Character Set from ROM is copied into the 8563. The
8563 has no ROM Character Set associated with it and thus we are able to just
simply modify the character set that is in the 8563 memory instead of copying
it over. Because of this no routine will be presented to copy a character
set into the 8563 memory, rather the discussion of copying individually
defined characters will take place in the next section. The C=128 also makes
life even easier for us at the end when we will exit the program,
modifying the character set back to the "standard" Commodore character set
by a routine in the KERNAL that will copy the characters back. We'll take a
look at it closer when we write the exit routine.
Also note that since the 8563 chip supports the 80 column screen we will
be defining two characters that can be placed side by side for each alien
so that the playing field will be similair to the C64 version. However, for
the title screen we will be switching the 8563 into a "40 column" mode
to make programming easier, in addition to expanding the character bit-mapped
logo.
Now you may be asking why would you want to store to the computer memory
in 128 mode? Why not just have two seperate versions? - Yes - that could
be possible but I'm implementing it this way because in the future I may
see a need to define custom characters in 128 mode for the 40 column screen.
This way I can just extract the routine, pop it into my program and I've got
that section of the code complete.
This is what I was thinking of for the data table:
Mapping the Commodore 128
128 Internals
and I'm holding Mapping the Commodore 64 in my hands. If I push (or toss)
the book onto the stack (and hopefully hit the stack instead of the floor)
I'll have the following stack:
Mapping the Commodore 64
Mapping the Commodore 128
128 Internals
and it should be easy to see that if I "pull" the next book off the stack
that I'll get the Mapping the Commodore 64 book. The next book to be "pull"ed
after that would be the Mapping the Commodore 128 book. This idea can be
applied to the 6502 stack -- It will keep storing values (up to 256) when you
"push" them on (via the PHA instruction) and will retrieve the last value
stored when you "pull" them off (via the PLA instruction). Another PLA
instruction would return the next value that had been stored.
The game logo is made up of 120 custom defined characters that will be
printed in the following manner (on the 128 screen they will be centered).
(in reverse video)...
So that it will look like a "mini-bitmap". We could have used bitmap mode
and made a very nice looking title screen but that would have involved
switching and allocating memory for the bitmap, etc . . . On both the
8563 and the VIC that involves a bit more work and so custom characters
will be used for the title screen. The regular letter and numeric characters
will be available so that we can display credits and game instructions
below the logo.
Now - in the program listing we could list them as binary #'s and that would
make editing them very easy but we're gonna use their decimal representation
in the program listing.
The characters are defined similair to the logo except they are treated as
single characters. In the 128 version due to the 80 column screen we are
going to use two characters side by side to simulate one alien so that the
playing field will be similair to the C64 version. In addation, during the
main loop we will modify the character sets to support animation of the
aliens. In the data listing there is a reference to "frames" - for each of
the aliens there are 8 differant frames.
Oh! - There will be more characters defined in the future. Right now I'm
mainly interested in getting some base characters down so you can see how
custom characters are implemented. When we start setting up different levels
and such we'll add more characters then. Currently the custom characters
are not used - only the characters for the logo. For those of you who are
curious try installing the characters via install'char and taking a look
at the aliens.
Now this is my idea of the screen layout (rough drawing as we're not using
the actual screen dimensions):
What we'll do is to just specify the address on screen, the # of characters
and then list the characters. It will be similair to our custom character
table driver above but will be different enough that a new routine is
warrented. We will however use the two subroutines writebyte and setadrs
that were developed in the previous routine. The data will look like the
following:
So basically we come up with the following:
For example, at one point I had a section of code similair to the following:
The C=64 only has 64k of memory of which typically the range $0800-$a000
is available and $c000-$cfff is also. If we had blindly picked numbers
all over the place to store our code then we would have a disorganized
program that would most likely accidentally use one subroutines storage
as temporary data for another. It's like shooting randomly in Laser Tag
not checking to see if there is a target there or not first... The end
result: Chaos.
Currently we're not following the rule for "temporary variables" but as
we gradually fade out of the normal C-64/128 default mode and write our
own routines / interrupt handlers we'll switch things over. Also, on the
C=128 instead of using Bank 0 with the I/O block enabled we're currently
using the BANK 15 configuration as the program doesn't extend past $4000
yet ($0000-$4000 is common memory in the normal C=128 configuartion).
Temporary Storage is going to be defined as follows. Each routine that needs
temporary storage will be assigned a "level" number. The lower levels will
be assigned level 1 on up to level 3. The range $0800-$3000 will be
broken down into the following sub-ranges.
Free memory on the C128 typically consists of the range $0400-$09ff
(where we'll be overwriting the 40 column screen (which we're gonna blank
anyway) and the Basic run-time stack.) Also the area from $0b00-$0fff
is free (overwriting the tape area, the rs-232 buffers,l and the sprite
definition area). Also $1300-$cfff will be free.
Now, the C=128 has different memory maps it can configure itself to -
Bank 15 is the standard mode under most basic programs and allows the
programmer to directly "sys" to calls. The MMU (memory management unit -
the chip that does everything) sees memory in a slightly differant way
than from basic. We'll cover it in more detail when we examine the mem_init
routine. For now, we're just gonna set up in the program and not in the
coding segments. The explanation of what we're doing will be "revealed"
in a future issue.
We will use Bank 0 of memory and from $1300+ will be the program. The ranges
of $0400-$09ff and $0b00-$0fff will be used in a similair mannar as the C64
ranges were for Temporary Storage. We will also have the I/O section from
$d000-$dfff swapped in. This is not a standard "basic BANK #" but when we
cover the init'memory routine we'll see how we can do this. Music data
will be from $a000-$d000.
Take a look at the different sections of code and analyze them to see how
they do what they do. Take a look at how the code was organized in terms
of simplification. Trace through each subroutine so that you're able to
know what the return values will be. In other words: Study, Study, Study!!
I'm in school and so I know I just used the dreaded 'S' word but that's
what you're going to have to do if you're interested in learning 65xx/85xx
machine language. The only way to learn it (easily) is to study other
people's code and try to understand why they did what they did.
Next time we will take a look at the input routines for the mouse, joystick
and keyboard scanning. In addition we will also allow the player to move
the ship around on the screen to test the input drivers.
In addation, I am still looking for an individual to help with music and
sound composition for this program. A knowledge of the SID chip and
programming is helpful but not required. If you're willing to help then
please email me at duck@pembvax1.pembroke.edu
For those of you on the mailing list who would like to recieve it, a Mail-
Server will be set up soon to handle requests and information will be
sent to you concerning information about using it as soon as it's completed.
In the
invasion1.sfx file there are the following files:
Written on 16-May-91 Translation 01-Jun-92
(All timings are in PAL, altho the principles will apply to NTSC too)
All of us have heard complaints about the color constraints on C64. One 8x8
pixel character position may only carry four different colors. FLI picture
can have all of the 16 colors in one char position. What then is this FLI
and how it is done ?
In the normal multicolor mode can one character position (4x8 pixels) have
only four different colors and one of them is the common background color.
Color codes are stored in half bytes (nybbles) to the video matrix memory
(anywhere video matrix pointer points at, normally $0400) and to the color
memory ($D800-$DBFF). In multicolor mode the color of each pixel is
determined by two bits in the graphics memory. Bit pair 11 will refer to
color memory, background color is the color for bit pair 00, and video
matrix will define the colors for bit pairs 01 and 10.
How does VIC know where to fetch the graphical information ? Some of you know
the mystical formulas needed to mess with the pixels in the hires screen.
How are these functions obtained ? Are they just magic ? No, there are some
internal counters in VIC. They always point to the right place in grafix
memory and the address is determined like this:
VC9-VC0 (Video Counter) forms the address bits 12-3. The counter rolls
through all 1000 character positions, 0-39 on the first eight lines, 40-79
on the second eight lines and so on. The lowest three bits come from the row
counter, RC2-RC0. This is another VIC counter and it counts the scan lines
from zero to seven.
If we change the vertical scroll register, we can fool VIC to think that
every line is a bad line, so it will fetch the color information on every
line too. Because VIC will fetch the colors continuosly, we can get
independent colors on each scan line. We just have to change colors and VIC
will handle the rest. Unfortunately the result is the loss of 40 processor
cycles per line (see the Missing Cycles article for more information about
VIC stealing cycles).
Because we have to keep the bitmap in the same video bank, we only have half
of the bank free for video matrices. Fortunately, that's all we need to get
individual multicolor colors for each line and character position.
VIC will fetch the color data from the eight video matrices and then it will
roll on to the next 40 bytes. After eight lines and matrices we will select
the first video matrix again. (See picture 1)
Usually it is not necassary to use the whole screen for a FLI picture,
especially if you want to have a scroller or some other effects. You just
have to make sure that VIC is foolable in the usual way. The timing is also
very important, even one cycle variations in the routine entry are not
allowed. There is many ways to do the synchronization. One way is to use a
sprite, as in the previous article. (See C= Hacking, Vol. 1, Iss. 3, The
Demo Corner: Missing Cycles).
With FLI we get two selectable colors for each character position and line,
each scan line can have it's own background color and each character position
still has its own character color from color memory. In theory each character
position could have 25 different colors, unfortunately VIC only has 16.
However, this doesn't mean that you can't use those three columns. FLI
editors may not support the fixed colors though, so it may be hard to use
them.
Picture 1: From which matrix VIC fetches the multicolor values
Available from:
cwaves.stfx.ca and
nic.funet.fi:/pub/amiga/graphics/applications/convert
C64GFX.doc
This article presents a way to interface from the C= rs232 hardware
behind the user port to a standard 25pin female rs232 connector using only
one IC and a few capacitors. It is not a UART or a SWIFTLINK type interface
which take place of the internal C= rs232 circuitry, but a simple level shifting
interface that uses the internal rs232 routines and translates the user port
levels to rs232 levels. Therefore you can only get upto 2400baud/9600baud
(C=64/C=128) with this design.
The "old" way to do this was to use MC1488 and MC1489 parts (a line driver
and line receiver), however these required a negative supply to interface
properly. The user port only supplies +5volts, hence this presents a problem.
There has been success using these parts or discrete transistors and resistors
since many modems are somewhat friendly and seem to work even though the levels
were marginal. Also, some signals were not used, allowing for potential
problems. Another way to solve this problem was to buy a $25-30 interface. If
you can find the IC below, you have another choice that is relatively
inexpensive.
The LT1133 is basically the MC1488 and 1489 put together into one part
with an internal charge pump scheme that allows the internal drivers to output
+5 and -5 volts to the rs232 connector. It also has enough drivers and
receivers to handle all of the signals that the C= uses for rs232.
So with this IC, 5 capacitors and the two connectors (user port and
rs232) you can build your own interface to the standard 25 pin modem cable.
Here are the plans for an User port TO RS232 connector using just ONE IC and
4 capacitors. It uses a Linear Technology LT1133 buffer that has 3 RS232
drivers and 5 receivers. It has worked for me with no problems and takes
a minimum amout of wiring to get to work. My board is only the width of
the user port and about 1.5 inches deep in size.
Here is a description of parts that might be substituted:
(Note, with all of them you should use a bypass cap on the +5v supply which
I have NOT included in the counts below.)
The Video Interface Controller used in C64 have several different operating
modes and different graphical primitives. Basically there is a) character
mode, b) bitmap mode and c) movable objects that can be mixed with the
other graphics. These primitives can also be put into a more colorful mode,
but you lose half of the resolution in that process.
Each character code can have an unique image, which consists of 8 bytes in
character memory. The position of the character memory can be moved with the
character base pointer and thus it is possible to have several character sets
simultaneosly in memory. One character memory is 2048 bytes.
In addition to the character code, each position in video matrix has an
associated color nybble (4 bits) in color memory ($D800-$DFFF). For each
zero-bit in the charset the background color from register $21 is displayed,
the color-nybble is used for the one-bit.
The highest bit in the color memory defines whether the character is to be
displayed in multicolor ("1") or in standard mode ("0"). Because of this,
only colors in the range from 0 to 7 are possible for bit pair "11". And since
two bits are required to specify one dot color, the character is now displayed
as a 4x8 matrix instead of the 8x8 matrix and the size of the dots are doubled
horizontally.
Extended color mode and multicolor mode should not be selected simultaneosly,
because this will result a black screen. However, this is a very easy way to
hide something if needed.
Each MOB can be selectively enabled (MnE="1") or disabled (MnE="0"). A MOB
is positioned via its X and Y position registers. Nine bits are needed to
define the vertical position and the most significant bits of all MOBs are
stored in the register $10. As X locations 23 to 347 and Y locations 50 to
249 are entirely visible on the screen, MOBs can be smoothly moved to an
off-screen position.
Each MOB has its own color register and a MOB can be displayed either in
standard or multicolor mode (MnMC="1"). As usually, multicolor mode gives
more colors, but halves the horizontal resolution. In multicolor mode bit
pair "00" is transparent, the MOB color register defines the color for pair
"10", and MOB multicolor registers give the colors for pairs "01" and "11".
MOBs can be selectively expanded in both directions. When MOB is expanded,
the pixel size also expands and it is still displayed as 24 x 21 matrix
(12 x 21 in multicolor mode).
MOB priorities define whether a MOB appears behind or on top of the character
or bit map graphics. A "1" in MnDP means MOB is displayed behind. MOB
collision registers may be used to detect if a non-transparent data of two
MOBs or a MOB and character or bitmap foreground data is colliding.
[Ed's Note: MOB's are Sprites. Commodore initially referred to them as MOB's
and still does in some areas.]
The normal display consists of 25 rows of 40 characters each. The display
window can be reduced to 24 rows and 38 characters. This has no effect on how
the data is interpreted, only the characters next to the border are covered
by the border. RSEL controls the number of rows ("1" for 25 rows) and CSEL
controls the number of columns ("1" for 40 columns).
The display data may be scrolled up to one character space in both vertical
and horizontal direction. Position of the screen is set with the 3 lowest
order (least significant) bits in registers 22 ($16) and 17 ($11).
Light pen latch is used to catch the position of the light pen when a pulse
is received in the LP pin. The value is latched only once in a frame.
The raster register is a dual-function register. A read from the raster
register returns the current raster position and a write to it will set the
raster compare value. When the written value and the current raster line
matches, a raster interrupt is generated if enabled. Raster register has its
most significant (9th) bit in register 17 ($11).
The interrupt register shows the status of the four sources of interrupt.
A corresponding bit will be set to "1" when an interrupt source has generated
an interrupt request. To enable an interrupt request to set the /IRQ output
to zero, the corresponding enable bit in register 26 ($1a) must be set to
"1". The interrupt latch may only be cleared by writing a "1" to the
desired latch in the interrupt register.
VIC register map (Base address $d000)
The Burst Command Instruction Set of the 1571/81 is used to read the MS-DOS
disk blocks and the standard kernel routines are used for outputting the
data. (I am an operating systems specialist, so I call it a kernEl!) Thus,
the MS-DOS files must be read from a 1571 or 1581 disk drive, but the output
device may be any disk drive type, the screen or a printer, or a virtual drive
type such as RAMLink, RAMDrive, or RAMDOS (for the REU). It is interesting to
note that the data can be read in from an MS-DOS disk faster than it can be
written out to a 1571, 1581, or even a RAMDOS file. A RAMLink can swallow the
data only slightly faster than it can be read.
Little Red Reader (LRR) supports double density 3.5" disks formatted with 80
tracks, 9 sectors per track, and 2 sides with a 1581 and 5.25" double density
disks formatted with 40 tracks, 9 sectors per track, and 2 sides with a 1571.
A limit of 128 directory entries and 3 File Allocation Table (FAT) sectors is
imposed. There must be 2 copies of the FAT and the cluster size may be 1 or 2
sectors. The sector size must be 512 bytes.
Oh, about the name. It is a play on the name of another MS-DOS file copier
available for the C-128. "Little" means that it is smaller in scope than the
other program, and "Red" is a different primary color to avoid any legal
complications. It is also the non-white color of the flag of the country of
origin of this program (no, I am not Japanese). Also, this program is Public
Domain Software, as is all software I develop for 8-bit Commodore Computers.
Feel free to E-mail me if you have questions or comments about this article.
Information about all MS-DOS files in the root directory of the MS-DOS disk is
displayed in columns below the drive information. "NUM" gives the number of
the MS-DOS file in the directory listing, and "S" indicates whether the file
is "selected" or not. If the file is selected, an asterisk (*) is displayed;
otherwise, a blank is displayed. When you later enter Copy Mode, only the
files that have been "selected" are copied.
The "TRN" field indicates the character translation scheme to be used when the
file is copied. A value of "BIN" (binary) means no translation and a value of
"ASC" (ascii) means the file characters are to be translated from MS-DOS ASCII
(or "ASCII-CrLf") to PETSCII. The "TYP" field indicates the type of
Commodore-DOS file to create for writing the MS-DOS file contents into. The
possible values are "SEQ" (sequential) and "PRG" (program). The values of the
TRN and TYP fileds are set independently, so you can copy binary data to SEQ
files and ascii data to PRG files if you wish.
The "FILENAME" and "EXT" fields give the filename and extension type of the
MS-DOS files and "LENGTH" gives the exact length of the files in bytes. Note
that if you perform "ASC" translation on a file, its PETSCII version will have
a shorter length.
Pressing F will prompt you for the CBM-DOS device number. You may enter a
number from 0 to 30, except that it must not be the MS-DOS drive number.
Enter a "1" for Cassette Drive (God forbid!), a "3" for the screen, a "4" for
the printer (with an automatic secondary address of 7 (lowercase)), any number
above 7 for a Commodore disk drive or special virtual drive, or a value of "0"
for the special "null" drive. A CBM-DEV value of 0 will case the program to
read MS-DOS files and do nothing with the output. You can use this feature to
check out the raw reading speed of the program.
After setting up the drives, press D to read in the root directory off the
MS-DOS disk. The data will come blazing in from the disk but BASIC will take
its good ole time sifting through it. Filenames are displayed on the screen
as they are scanned in. The program will (eventually) return to the main
screen and display the formatted file information. One note: the process of
logging in a 1581 MS-DOS disk takes about 12 seconds (on my 1581, anyway), so
be patient. An MS-DOS disk will have to be "logged in" every time you change
MS-DOS disks. (Disks are logged in automatically).
A couple of notes about accessing MS-DOS disks: don't try to access a device
that is not present because the machine language routines cannot handle this
error for some reason and will lock up, requiring a STOP+RESTORE. Also, make
sure that an actual MS-DOS disk is loaded into the drive. If you accidentally
place Commodore-DOS disk into the MS-DOS drive, the 1581 will report an
invalid boot parameters error (#60), but a 1571 will lock up (since I don't
check the sector size and my burst routines are expecting 512 bytes to come
out of a sector whereas Commodore disks have only 256 bytes per sector).
Now you are ready to pick what files you want copied and how you want them
copied. You will notice that a "cursor" appears in the "S" column of the
first file. You may move the cursor around with the cursor keys: UP, DOWN,
LEFT, RIGHT, HOME, and CLR. CLR (SHIFT-HOME) will move the cursor back to the
first file on the first screen. You can move the cursor among the select,
translation, and file-type columns of all the files. Pressing a SPACE or a
RETURN will toggle the value of the field that the cursor is on. To toggle
all of the values of the "cursor" column (including files on all other
screens), press T. You will notice that moving the cursor around and toggling
fields is a bit sluggish, especially if you are in Slow mode on the 40-column
screen. Did I mention that this program will run on either the 40 or
80-column screen? Toggling an entire column can take a couple of seconds.
If there are more than 18 MS-DOS files, you can press the "+" and "-" keys to
move among all of the screens of files. The cursor movement keys will wrap
around on the current screen. "+" is page forward, and "-" is page backward.
The screens wrap around too.
After you have selected all of the files you want to copy and their translation
and file-type fields have been set, press the C key to go into Copy Mode (next
section). After copying, you are returned to the main screen with all of the
field settings still intact. To exit from the program, press Q.
To generate a CBM-DOS filename from an MS-DOS filename, the eight filename
characters are taken (including spaces) and a dot (.) and the three characters
of the extension are appended. Then, all spaces are removed, and if the name
ends with a dot (.) character, then that dot character is removed as well. I
think this is fairly reasonable.
If there already is a file with the same filename on the CBM-DOS disk, then
you will be prompted if you want to overwrite the file or not. Entering an
"n" will abort the copying of that file and go on to the next file, and
entering a "y" (or anything else) will cause the CBM-DOS file to be
"scratched" and then re-written.
The physical copying of the file is done completely in machine language and
nothing is displayed on the screen while this is happening, but you can follow
things by looking at das blinkin lichtes and listening for clicks and grinds.
You will probably be surprised by the MS-DOS file reading speed (I mean in a
good way). The disk data is read in whole tracks and cached in memory and the
directory information and the FAT are retained in memory as well. The result
is that minimal time is spent reading disk data, and no costly seeks are
required for opening a new MS-DOS file. A result is that small files are
copied one after another very quickly. You will have to wait, however, on the
relatively slow standard kernel/Commodore-DOS file writing.
A few changes had to be made to the program to accomodate the RAMDOS program.
RAMDOS uses memory from $2300 to $3FFF of RAM0, which is not really a good
place for a device driver, and it uses some of the zero-page locations that I
wanted to use. But, difficulties were overcome. The importance of RAMDOS
compatibility is that if you only have one disk drive but you have an REU, you
can use RAMDOS to store the MS-DOS files temporarily. If you only have one
disk drive and no REU, you are SOL (Out of Luck) unless you can get a
RamDisk-type program for an unexpanded 128.
The RAMDOS program is available from FTP site ftp.funet.fi in file
/pub/cbm/c128/utilities/ramdosii.sfx.
One note I found out about RAMDOS: you cannot use a
DOPEN#1,(CF$),U(CD),W
with it like you are supposed to be able to; you have to use a
DOPEN#1,(CF$+",W"),U(CD)
Here is a table of copying speeds for copying from 1571s and 1581s with ASC
and BIN translation modes. All figures are in bytes/second. These results
were obtained from copying a 127,280 byte text file (the text of C= Hacking
Issue #3).
The "null" figures are quite impressive, but the raw sector reading speed
without the overhead of mucking around with file organization is 6700
bytes/sec for a 1581 and 4600 B/s for a 71. The reason that the 1571 operates
so quickly is that I use a sector interleave of 4 (which is optimal) for
reading the tracks. I think that other MS-DOS file copier program uses an
interleave of 1 (which is not optimal). I lose some of the raw performance
because I copy the file data internally once before outputting it (to simplify
some of the code).
In a couple of places you will notice that ASC translation gives slightly
better or slightly worse performance than BIN. This is because although
slightly more work is required to translate the characters, slightly fewer
characters will have to be written to the CBM-DOS file, since PETSCII uses
only CR where MS-DOS ASCII uses CR and LF to represent end-of-line.
Translation is done by using a table (that you can change if you wish). Many
entries in this table contain a value of zero, which means that no character
will be output on translation. Most of the control characters and all of the
characters of value 128 (0x80) or greater are thrown away on being
translated. The table is set up so that CR characters are thrown away and the
LF character is translated to a CBM-DOS CR character. Thus, both MS-DOS ASCII
files and UNIX ASCII files can be translated correctly.
The Sector Interleave command is used to set a soft interleave for the Read
command. I use an interleave of 1 for the 1581 and an interleave of 4 for the
1571. This means that the MS-DOS sectors will come from 1571 to the computer
in the following order: 1, 5, 9, 4, 8, 3, 7, 2, 6 (there are 9 sectors per
track on an MS-DOS disk (both 3.5" and 5.25"), numbered from 1 to 9). LRR
handles the data coming in in this order, and in straight order from the
1581. The Sector Interleave command has the following format, where the W and
N bits are 0 for us:
The read command returns the following data using burst mode handshaking:
The FAT is an array of 12-bit numbers, with an entry corresponding to each
cluster that can be allocated to files. FAT entries 0 and 1 are reserved. If
a FAT entry contains a value of $000, then the corresponding cluster is free
and can be allocated to a file; otherwise, the cluster is allocated and the
FAT entry contains the number of the NEXT FAT entry that belongs to the file.
Thus, MS-DOS files are stored in a singly-linked list of clusters like
Commodore-DOS files are, except that the links are not in the data sectors but
rather are in the FAT. The pointer to the first FAT entry for a file is given
in the file's directory entry.
A special NULL/NIL pointer value of $FFF is used to indicate the end of the
chain of clusters allocated to a file. This value is stored in the FAT entry
of the last cluster allocated to a file (of course). Consider the following
example FAT:
The MS-DOS designers were a little sneaky in storing the 12-bit FAT entries -
they used only 12 real bits per entry. Ie., they store two FAT entries in
three bytes, where the two entries share the two nybbles of the middle byte.
The following diagram shows how the nybbles 1 (high), 2 (mid), and 3 (low) are
stored into FAT entries A and B:
The attributes bits tell whether the directory entry is for a regular file, a
subdirectory, a disk volume name (in which case there is no file data), and a
couple of other things I can't remember. I'm not sure about the exact
position or format of the date, but LRR doesn't use it anyway. The starting
FAT entry number and the file length are stored in lower-byte-first order.
Since the Read burst command of the 1571/81 wants the side, track, and sector
number of a sector rather than its logical number, we also need formulae for
these conversions:
Oh yeah, the way that you know how many file data bytes are in the last
cluster of a file chain (the cluster with the NULL FAT entry) is to take the
file length from the directory entry and "mod" (the C language % operator) it
with the cluster size. One special case is if this calculation results in a
zero, then the last cluster is completely full (rather than completely empty
as the calculation would suggest). This calculation is easily done in
machine language with an AND operation since the cluster size is always a
power of two.
The "InitPackage" subroutine should be called when the package is first
installed, whenever the MS-DOS device number is changed, and whenever a new
disk is mounted to invalidate the internal track cache. It requires no
parameters.
The "LoadDirectory" subroutine will load the directory, FAT, and the Boot
Sector parameters into the internal memory of the package from the current
MS-DOS device number. No (other) input parameters are needed and the
subroutine returns a pointer to the directory space in the .AY registers and
the number of directory entries in the .X register. If an error occurs, then
the subroutine returns with the Carry flag set and the error code is available
in the "errno" interface variable. The directory entry data is in the
directory space as it was read in raw from the directory sectors on the MS-DOS
disk.
The "CopyFile" subroutine will copy a single file from the MS-DOS disk to a
specified CBM-Kernal logical file number (the CBM file must already be
opened). If the CBM logical file number is zero, then the file data is simply
discarded after it is read from the MS-DOS file. The starting cluster number
of the file to copy and the low and mid bytes of the file length are passed in
the PK+18 and PK+20 interface words. The translation mode to use is passed in
the .A register ($00=binary, $FF=ascii) and the CBM logical file number to
output to is passed in the .X register. If an error occurs, the routine
returns with the Carry flag set and the error code in the "errno" interface
variable. There are no other output parameters.
Note that since the starting cluster number and low-file length of the file to
be copied are required rather than the filename, it is the responsibility of
the front-end application program to dig through the raw directory sector data
to get this information. The application must also open the Commodore-DOS
file of whatever filetype on whatever device is required; the package does not
need to know the Commodore-DOS device number.
The MS-DOS device number and device type interface variables allow you to set
the MS-DOS drive and the package identification number allows the application
program to check if the package is already loaded into memory so that it only
has to load the package the first time the application is run and not on
re-runs. The identification sequence is a value of $CB followed by a value
of 131.
grep '^\.%...\!' Hack4 | sed 's/^.%...\!..//' | sed 's/.%...\!//' > lrr.s
You'll notice that the initial comment lines here were an afterthought.
III. The Process
Part of what this series of articles is focused at is the development of being
able to analyze programming tasks and break them down into smaller workable
problems. Once these problems or subroutines are completed your original
problem is solved.
Let's think a bit more about each of these sections and what each will
involve:
128 / 64 Screen Handling:
Music / Sound:
Input Handling:
Game Driver:
IV. Not All At One Time - What We're Doing This Time
Now this program is too complex, (as seen by the problem statement above) to
have in one article so this issue we'll concentrate on the basic main loop
and the initialization of the Custom Characters and the title screen.
V. The Main Loop
What is a main loop? Basically it's where everything gets done. It calls other
subroutines and keeps repeating until certain criteria are met - usually when
the player requests to exit the game. However, inside you'll find inner loops
for level play etc.
VI. Custom Characters
Since we're writing for each of the seperate modes (64 mode, 128 mode) we have
to take a look at the differences between the VIC chip (64 mode) and the 8563
chip in the 128.
The Vic-Chip
The character sets in the VIC chip are defined as in the example below of
the character code $00 "@" (all references are to screen "poke" codes - not
print codes).
The 8563 Chip
The 8563 80-Column chip usually has 16k or 64k Ram attatched to the chip
which the CPU does not have direct control over. It has to direct the 8563
to store and retrieve values to that memory. What makes control over that
memory all the more difficult is the fact that the 8563 only has two lines
or addresses that the CPU can control.
Changing the Characters
A lot of the times you'll find yourself re-using subroutines and code that
you have previously created, gradually, over a period of time building up
a library of routines. When thinking through the purpose and intent of this
routine I thought about possibly building it so it would read a table and
change the character set based on that table. The 64/128 character sets
would be the same - this routine would automatically generate the eight
additional bytes needed by the 8563 if need-be and it would call the
appropriate storage routine - store to either the 8563 or the computer
memory.
The Character Bitmaps
Pasi Ojala is to be credited with all the graphics and many thanks go out
to him.
VII. Title Screen
The title screen is usually a lead-in to the actual game and it's aim is
to tell the player how to play the game, any available options and p'haps
present a nice graphic or two to "wow" the user into playing. In addation,
the main musical theme can be introduced here to unify the game-playing.
The discussion below does not take into account color but rest-assured we
will be using varying colors in the title screen. The format for the color
data will be almost identical to the title screen format except it will
be structured via the following:
Title Screen BackGround
The title screen I envisoned as a bordered screen (using the normal C=
character set - ie: C= A,S,Z,X on the keyboard) with our bitmap in the middle
and under-neath it a short description of the game and game-play instructions.
Title Screen Formatting
We come into a problem here -- the screen is some 1000 characters on the C64,
and 2000 characters for the C=128. It would be extremely wasteful to store
that many characters in memory just to reproduce a title screen - and most of
them consisting of spaces at that!!
VIII. Debugging
Now, not all programs are perfect, and during the development of this
portion of the game there were several errors found. Tracing an error in
Machine/Assembly-Language is like trying to find a grammatical error in a
language you don't know. ;-) But seriously, there are several ways to track
down errors in your code.
I can't stress numbers 3 and 5 enough. During the writing of the install'char
routine there were numerous bugs that were eventually tracked down by
setting a BRK instruction further along in the code and seeing exactly what
the register / memory locations were. Also the use of temporary load and
store instructions into "safe" regions of memory helped me monitor what some
of the values were.
IX. Memory Map Considerations
Before you start a program it's a good idea to consider where in memory you
will have everything. Now we've already started some of the program above
and just blindly picked numbers at random it seemed like $3000 for the
character set for the C=64 etc... We didn't - I'm introducing the Memory
Map Considerations here to show the example of what if we didn't think
about how memory was going to be organized.
64 Considerations
The 64 will have free memory in the following areas: $0800-$a000, and
$c000-$cfff. However, if we disable the Basic Rom we can have the whole
area from $0800-$cfff free for our program. Because we don't need the
Basic Rom we will do just that (in the listing now we currently won't but
it will be done in a future issue). Therefore having the character set
at $3000-$5000, the music data at $5000-$8000, the program will have the
area free from $8000-$cfff. $0800-$3000 will be available if needed for
routines who need temporary storage.
128 Considerations
The 128 has two "banks" of 64k each. Normally for large programs we would
think about using both banks - (from the idea: Hey! - We got it, why not
flaunt it?) but we won't be using both banks.
X. Looking Forward / Back
Hopefully through the listing and the discussion of the routines you have
started to understand the basic concept of programming: breaking down problems
into smaller solvable steps. Try looking back over the code asking yourself
why that instruction is there. What would happen if you switched the order?
Is there an easier, better way to do the same thing? Why? Better yet, how?
Examine the code, mess with it, muck it up so it doesn't work and then figure
out exactly why. The only way to learn is by experimentation. (BTW, muck up
a _copy_ of it - not the original ... *grins*)
XI. Listings
Because of the enormity of the program listing (some 1,500+ lines) it will
not be listed in this article but will instead be available via anonymous
FTP from ftp.funet.fi.
The Demo Corner:
FLI - more color to the screen
by Pasi 'Albert' Ojala (po87553@cs.tut.fi or albert@cc.tut.fi)
What happens in the VIC ?
VIC (Video Interface Controller) fetches color information from memory on
each bad line. This will steal time from processor, because VIC needs to use
processor's bus cycles. Bad line is a curse in the C64 world. Fortunately
VIC's data bus is 12 bits wide and so the color data fetch for each character
position will take only one bus cycle. Color memory is physically wired to
the VIC databus lines D8-D11.
A software graphics mode - FLI
VIC will systematically go through every byte in the bitmap memory, but how
does it know where and when to get the color information ? This is where
the main principle of FLI (Flexible Line Interpretation) lies. Color data
is fetched (and this means it is a bad line), when the line counter matches
with the vertical scroll register. VC9-VC0 defines where the color data is
inside the video matrix and color memory.
Doing it in practice
In practice there is no time to change color memory, but in multicolor
mode VIC uses video matrix for color information too. We have just enough
time to change the video matrix pointer, $D018. Now VIC will see a
different video matrix on each scan line, different block of memory. With
the four upper bits in the register we select one of the 16 video memories
in the video bank. Just remember that the register also selects the position
of the graphics memory (bitmap) inside the video bank.
Not much time
Because a bad line will steal 40 cycles, there is only 23 cycles left on
each scan line. It is enough for changing the video matrix and backgroud
color. There is not a moment to lose, because you must change the vertical
scroll register, video matrix pointer and the background color. This is why
you can't have sprites in front of a FLI picture.
A little feature
VIC does not like it when we change the vertical scroll register ($D011),
and is a bit annoyed. It 'sees' code 255 (light gray) in video matrix
and 9 (brown) in the color memory instead of the correct values stored there.
Actually the color value seems to be the lower nybble of the data byte
currently on the data bus (accessed by the processor (LDA#=$A9)).
Unfortunately there is no chance to do the register change in the border
and thus the three leftmost character columns are a bit useless, because
the colors are fixed.
What to do with FLI ?
Because FLI will eat up all the available processor time (no Copper :-),
it is not suitable for any action-games. Each FLI picture takes about 17 kB
of memory: not so many pictures fit on one floppy. So, the only place for FLI
is demos, intros, board-type games and maybe a GIF viewer..
Additional reading
If you have an Amiga you might want to get your hands into my conversion
programs in C64GFX1.lha. The packet also includes FLI viewer for PAL C64's
and some documentation about the FLI file format. It also has the same
utilities for Koala format pictures.
RS-232 Converter
by Warren Tustin (warren@col.hp.com)
Parts list:
Connections:
User port connector (Looking into the C64 or C128)
Parts Substitution:
There are other IC's available which will work in this application.
The rs232 bus levels for the LT parts are spec'ed at ~ +/- 7v typical, while
the MAXIM parts are +/-9v typical. (Both are min at +/- 5v which should work
in all applications). I think that an interface can be done with only 3
lines, Din(Rx), Dout(Tx), and either DSR or CTS, so if you can't get the
LT1133 one of these others might work, although the pinouts would be different.
The max you would need is what the LT1133 supplies, 3 drivers & 5 receivers.
Introduction to the VIC-II
by Pasi 'Albert' Ojala ( ... )
I. Standard Character Display Mode
In the character display mode, VIC fetches character pointers from video
matrix, which consists of 1000 8-bit bytes formatted as 25 rows of 40
characters each. The 8-bit character code implies 256 different characters
simultaneously onscreen.
II. Character Multicolor Mode
In character multicolor mode, color selection is increased. Each byte is
fetched from the character memory just like in the standard character mode,
but they are interpreted differently. In this mode bytes are divided into
bit-pairs. For bit pair "00" the background color from register $21 is
displayed, background color #1 is used for bit pair "01" and background
color #2 for bit pair "10". The color nybble will define the color for bit
pair "11".
III. Extended Color Mode
The extended color mode allows the selection of one background color from four
possibilities for each character position in the normal 8x8 resolution. The
character image data is processed like in standard character mode, but the
two most significant bits in the character code (video matrix) are used to
select the right background color register. Only character images from 0 to 63
are accessible, because two of the most significant bits are used for the
background color selection.
IV. Standard Bit Map Mode
In bit map mode, a one-to-one correspondence exists between each displayed
dot and a memory bit. The bit map provides a resolution of 320H x 200V
individually controlled pixels. The video matrix is still accessed as in
character mode, but the data is interpreted as color data. When a bit is "0"
in the bit map data, the color from the lower nybble is selected. The
higher nybble from the video matrix is used for the bit "1".
V. Multicolor Bit Map Mode
In multicolor bit map mode two bits in the bit map memory determine the color
of one pixel. If the bit pair is "11", the color found from the color memory
is used. The background color is used for bit pair "00" and the video matrix
defines the colors for bit pairs "01" and "10". As it takes two bits to define
one pixel color, the horizontal resolution is halved to 160H x 200V.
VI. Movable Object Blocks (MOBs)
The movable object block is a special type of graphical object which can be
displayed independently from the other graphics. Each one of the MOBs can
be moved independently anywhere in the screen. Eight unique MOBs can be
displayed simulataniously, each defined by 64 bytes in memory which are
displayed as a 24 x 21 pixel array.
VII. Other features
The display screen may be blanked by setting the DEN bit to a "0". The entire
screen will be filled with the border color as set in register 32 ($20).
When the screen is blanked, VIC will need only transparent memory cycles and
the processor is not slowed down. However, MOB data is still fetched, if
the MOBs are not also disabled.
LITTLE RED READER: MS-DOS file reader for the 128 and 1571/81 drives.
by Craig Bruce 1. INTRODUCTION
This article presents a program that reads MS-DOS files and the root directory
of MS-DOS disks. The program copies only from drive to drive without
buffering file data internally. This is simpler and imposes no limits on the
size of the files transferred, although it requires the use of two disk drives
(or a logical drive). The user-interface code is written in BASIC and
presents a full-screen file selection menu. The grunt-work code is written in
assembly language and operates at maximum velocity.
2. USER GUIDE
LOAD and RUN the "lrr.128" BASIC program file. When the program is first run,
it will display an "initializing" message and will load in the binary machine
language package from the "current" Commodore DOS drive (the current drive is
obtained from PEEK(186) - the last device accessed). The binary package is
loaded only on the first run and is not reloaded on subsequent runs if the
package ID field is in place.
2.1. MAIN SCREEN
The main screen of the program is then displayed. The main screen of the
program will look something like this:
2.2. USER COMMANDS
The bottom of the screen gives the command summary. After starting the
program, you will want to setup the MS-DOS and CBM-DOS drives with the "M" and
"F" commands. Simply press the (letter) key corresponding to the command
name to activate the command. Pressing M will prompt you for the MS-DOS Drive
Number and the MS-DOS Drive Type. In both cases, type the number and press
RETURN. (Sorry for insulting all non-novices out there, but I want to be
complete). The MS-DOS drive number cannot be the same as the CBM-DOS drive
number (since the program copies from drive-to-drive without internal
buffering). For the drive type, enter an "8", "81", or "1581" for a 1581
drive or anything else for a 1571 drive.
2.3. COPY MODE
When you enter copy mode, the screen will clear and the name of each selected
file is displayed as it is being copied. If an error is encountered on either
the MS-DOS or CBM-DOS drive during copying, an error message will be displayed
and copying will continue (after you press a key for MS-DOS errors).
2. BURST COMMANDS
Three burst commands from the 1571/81 disk drive Burst Command Instruction Set
are required to allow this program to read the MS-DOS disks: Query Disk
Format, Sector Interleave, and Read. The grungy details about issuing burst
commands and burst mode handshaking are covered in C= Hacking Issue #3. The
Query Disk Format command is used to "log in" the MS-DOS disk. The Inquire
Disk burst command cannot be used with an MS-DOS disk on the 1581 for some
unknown reason. I found this out the hard way. The Query Disk Format command
has the following format:
3. MS-DOS DISK FORMAT
An MS-DOS disk is separated into 4 different parts: the Boot Sector, the
FAT(s), the Root Directory, and the File Data Sectors. The logical sectors
(blocks) of a disk are numbered from 0 to some maximum number (1439 for a
3.5", 719 for a 5.25" DD disk). The physical layout and the logical sector
numbers typically used by a 3.5" disk are shown here:
3.1. THE BOOT SECTOR
The Boot Sector is always at logical sector number 0. It contains some
important information about the format of the disk and it also contains code
to boot an MS-DOS machine from. We aren't concerned with the bootstrapping
code, but the important values we need to obtain from the boot sector are:
3.2. CHEWING THE FAT
MS-DOS disks use a data structure called a File Allocation Table (FAT) to
record which clusters belong to which file in what order and which blocks are
free. A cluster is a set of contiguous sectors which are allocated to files
as a group. LRR handles cluster sizes of 1 and 2 sectors, giving a logical
file block size of 512 or 1024 bytes. Typically, a cluster size of 2 sectors
is used.
3.3. THE ROOT DIRECTORY
The root directory has a fixed size, although I don't think that
subdirectories do. LRR cannot access subdirectories. Each 512-byte sector of
the root directory contains sixteen 32-byte directory entries. One directory
entry is required for each file stored on the disk. A directory entry has the
following structure:
3.4. THE FILE DATA SPACE
The ramainder of the disk space is used for storing file data in clusters of 1
or 2 sectors each. Given a cluster number (which is also the FAT entry
number), the following formula is used to calculate the starting logical
sector number of the cluster:
4. FILE COPYING PACKAGE
This section discusses the interface to and implementation of the MS-DOS file
copying package. It is written in assembly language and is loaded into memory
at address $8000 on bank 0 and requires about 13K of memory. The package is
loaded at this high address to be out of the way of the main BASIC program,
even if RAMDOS is installed.
4.1. INTERFACE
The subroutine call and parameter passing interface to the file copying
package is summarized as follows:
4.2. IMPLEMENTATION
This section presents the code that implements the MS-DOS file reading
package. It is here in a special form; each code line is preceded by a few
special characters and the line number. The special characters are there to
allow you to easily extract the assembler code from the rest of this magazine
(and all of my ugly comments). On a Unix system, all you have to do is
execute the following command line (substitute filenames as appropriate):
5. USER-INTERFACE PROGRAM
This section presents the listing of the user-interface BASIC program. You
should be aware that you can easily change some of the defaults to your own
preferences if you wish. This program is not listed in the ".%nnn!" format
that the assembler listing is since you can recover this listing from the
binary program file. This program should be a little easier to
follow than the assembler listing since BASIC is a self-commenting language. :-)