by Anthony McSweeney (u882859@postoffice.utas.edu.au)
[Ed's Note: I questioned this article concerning copyright problems and he
has assured me that it is legal to present it in entirity like this as it is
past a certain # of years. Accordingly I'm presenting it and any concerns
should be taken up with him and not myself.]
Introduction:
************
How do you introduce someone like Rob Hubbard?? He came, he saw and he
conquered the '64 world. In my estimation, this one man was resposible for
selling more '64 software than any other single person. Hell! I think that Rob
Hubbard was responsible for selling more COMMODORE 64's than any other person!
I certainly bought my '64 after being blown away by the Monty on the Run music
in December 1985. In the next few years, Rob would totally dominate the '64
music scene, releasing one hit after another. I will even say that some really
terrible games sold well only on the strength of their brilliant Rob Hubbard
music (eg. KnuckleBusters and W.A.R.).
So how did Rob achieve this success? Firstly (of course) he is a superb
composer and musician, able to make the tunes that bring joy to our hearts
everytime we hear them! (also consider the amazing diversity of styles of
music that Rob composed). Secondly, he was able to make music which was suited
to the strengths and limitations of the SID chip. Just recall the soundfx used
at the beginning of Thrust, or in the Delta in-game music. Perhaps the biggest
limitation of SID must be the meagre 3 channels that can be used, but most
Hubbard songs appear to have four, five or even more instruments going (just
listen to the beginning of Phantoms of the Asteriods for example... that's
only one channel used!!). I could really go on for (p)ages identifying the
outstanding things that Rob Hubbard did, so I will finally mention that Rob's
coding skills and his music routines were a major factor in his success.
The First Rob Hubbard Routine:
*****************************
Rob Hubbard created a superb music routine from the very first tune which
was released (Confuzion). Furthermore, Rob used this routine to make music
for a very long time, only changing it _slightly_ over time. The sourcecode
that I present here was used (with slight modifications) in: Confuzion, Thing
on a Spring, Monty on the Run, Action Biker, Crazy Comets, Commando, Hunter
Patrol, Chrimera, The Last V8, Battle of Britain, Human Race, Zoids, Rasputin,
Master of Magic, One Man & His Droid, Game Killer, Gerry the Germ, Geoff Capes
Strongman Challenge, Phantoms of the Asteroids, Kentilla, Thrust,
International Karate, Spellbound, Bump Set and Spike, Formula 1 Simulator,
Video Poker, Warhawk or Proteus and many, many more! All you would need to do
to play a different music is to change the music data at the bottom, and a few
lines of the code.
This particular routine has been ripped off by many famous groups and
people over the years, but I don't think that they were ever generous enough
to share it around. Can you remember The Judges and Red Software?? They made
the famous Red-Hubbard demo, and used it in Rhaa-Lovely and many of their
other productions. I'm sure that the (Atari) ST freaks reading this will love
Mad Max (aka Jochen Hippel), and remember the BIG demo which featured approx
100 Rob Hubbard tunes converted to the ST. Although I hate to admit it, I
decided to start sharing around my own sourcecode after receiving the amazing
Protracker sourcecode (340K!) on the Amiga (thanks Lars Hamre). That made me
shameful to be selfish, especially after I learned alot of from it. Why don't
YOU share around your old sourcecodes too!
The particular routine that is included below was ripped from Monty on the
Run, and it appeared in memory from $8000 to about $9554. The complete
routine had code for soundfx in it, which I have taken out for the sake of
clarity. Although the routine is really tiny - a mere 900 or 1000 bytes of
code, there are some amazingly complex concepts in it which require alot of
explanation if you don't know much about computer music or SID. Fortunately
for you, I have put in excellent label names for you, and also alot of really
helpful and amazing comments. In fact, I think this sourcecode must have a
much better structure and comments than Rob Hubbard's original!!! I think that
the best way to understand the sourcecode is to study it, and figure out what
is going on using the comments.
In addition to the comments in the source, there are *3* descriptions of
the routine in this article. The first tells you how to use the music routine
when it's viewed as an already assembled 'module'. The second goes through an
overview of the music and instrument data format, and is great for getting an
overall feel for what the code is doing. The third description looks at the
various sections of the code, and how they come together.
How to use the sourcecode:
*************************
jsr music+0 to initialize the music number in the accumulator
jsr music+3 to play the music
jsr music+6 to stop the music and quieten SID
The music is supposed to run at 50Hz, or 50 times per second. Therefore
PAL users can run the music routine off the IRQ like this:
lda #$00 ; init music number 0
jsr music+0
sei ; install the irq and a raster compare
lda #<irq
ldx #>irq
sta $314
stx $315
lda #$1b
sta $d011
lda #$01
sta $d01a
lda #$7f
sta $dc0d
cli
loop =*
jmp loop ; endless loop (music is now playing off interrupt :-)
irq =*
lda #$01
sta $d019
lda #$3c
sta $d012
inc $d020 ; play music, and show a raster for the time it takes
jsr music+3
dec $d020
lda #$14
sta $d018
jmp $ea31
If this method is used on NTSC machines, then the music will be running at
60Hz and will sound much to fast - possibly it might sound terrible. I'm
afraid you'll have to put up with this unless YOU are good enough to make a
CIA interrupt at 50Hz. As I havn't had to worry about NTSC users before,
perhaps someone will send me the best way to do this...
[Ed. Note: You could also keep a counter for the IRQ and don't execute it
every 6 interrupt. This will make it the right speed although the best
solution is for modifying the CIA to 50Hz like he mentions above.]
How the music data is arranged:
******************************
1. The music 'module' contains one or more songs.
Each RH music is made up of a 'bunch' of songs in a single module. Thus
the 'module' can have the title music, in-game music, and the game-over music
all using the same playroutine (and even the same instruments :). The source
that appears below only has the one song in it, and the music number is
automatically set to 0 as a result (line 20). The label 'songs' is where you
want to look for the pointers to the songs if you want to change this.
2. Each song is made up of three tracks.
We all know that there are only 3 channels on the SID chip, so there are
also 3 tracks - one for each channel. When I said 'pointers to the songs'
above, I was therefore referring to 'pointers to the three tracks that make up
the song'...hence we are looking at the label 'songs' again. Each track needs
a high and low pointer, so there are 6 bytes needed to point to a song.
3. Each track is made up of a list of pattern numbers
Each track consists of a list of the pattern numbers in the order in which
they are to be played. Here we are looking at the labels 'montymaintr1' and
'montymaintr2'and 'montymaintr3'. Therefore I can tell you that the initial
patterns played in this song are $11, $12 and $13 on channels 1,2 and 3
respectively. The track is either ended with a $ff or $fe byte. A $ff means
that the song needs to be looped when the end of the track is reached (like
the monty main tune), while a $fe means that the song is only to be played
once. The current offset into the track is often called the current POSITION
for that track.
4. A pattern consists of a sequence of notes.
A pattern contains the data that says when the notes should be played, how
long they should be played for, at what pitch, with what instrument, should
there be ADSR, should there be bending (portamento) of the notes etc. Each
pattern is ended with a $ff byte, and when this is encountered, the next
pattern in the track will be played. Each note has up to a 4 byte
specification.
- The first byte is always the length of the note from 0-31 or 0-$1f in hex.
You will notice that the top three bits are not used for the length of the
note, so they are used for other things.
- Bit#5 signals no release needed. - Bit#6 signals that this note is
appended to the last one (no attack/etc). - Bit#7 signals that a new
instrument or portamento is coming up.
- The second byte is an optional byte, and it holds the instument number to
use or the portamento value (ie a bended note). This byte will be needed
according to whether bit#7 of the first byte is set or not...ie if the 1st
byte was negative, then this byte is needed.
- If the second byte is positive, then this is the new instrument number.
- If the second byte is negative, then this is a bended note (portamento).
and the value is the speed of the portamento (except for bits #7 and #0)
Bit #0 of the portamento byte determines the direction of the bend.
- Bit#0 = 0 then portamento is up.
- Bit#0 = 1 then portamento is down.
- The third byte of the specification is the pitch of the note. A pitch of
0 is the lowest C possible. A pitch of 12 or $C(hex) is the next highest
C above that. These pitches are denoted fairly universally as eg. 'C-1' and
for sharps eg. 'D#3'. Notice that this routine uses pitches of higher than
72 ($48) which is c-6 :-)
- The fourth byte if it exists will denote the end of the pattern.
ie. If the next byte is a $ff, then this is the end of the pattern.
NOTE: I have labelled the various bytes with numbers for convenience. Bear
in mind that some of these are optional, so if the second byte is not needed,
then I will say that the pitch of the note coming up is the 'third byte',
even though it isn't really.
Okay, here are some examples:
eg. $84,$04,$24 means that the length of the note is 4 (from the lower 5 bits
of the first byte), that the instrument to use is instrument number 4
(the second byte, as indicated by bit #7 of the first byte), and that the
pitch of the note is $24 or c-3.
eg. $D6,$98,$25,$FF means that the length of the note is 22 ($16), that this
note should be appended to the last, that the second byte is a portamento
(as both 1st and 2nd bytes -ve!), that the portamento is going up (as
bit#0 = 0) with a speed of 24 ($18), that the pitch of the note is $25
or c#3, and that this is the end of the pattern.
It doesn't get any harder than that!! Did you realise that this is exactly
the way that Rob Hubbard made the music!! He worked out some musical ideas
on his cheap (musical) keyboard, and typed the notes into his assembler in
hex, just like this.
5. The instruments are an 8 byte data structure.
You are looking at the label 'instr' at the bottom of the sourcecode. The
8 bytes which come along first are instrument numnber 0, the next 8 define
instrument number 1, etc. Here are the meanings of the bytes, but I suggest
that you check out your programming manuals if you are unfamiliar with these:
- Byte 0 is the pulse width low byte, and
- Byte 1 is the pulse width high byte. (also see byte 7).
- Byte 2 is the control register byte.
This specifies which type of sound should be used; sine, sawtooth etc.
- Byte 3 is the attack and decay values, and
- Byte 4 is the sustain and release values.
The note's volume is altered according to these values. When the attack and
decay are over, the volume of the note is held at the sustain level. When
length of a note is over, a release is done through the 'gate' bit in SID.
- Byte 5 is the vibrato depth for the instrument.
- Byte 6 is the pulse speed.
Timbre is created by changing the shape of the waveform each 50th of a
second, and this is the most common way of achieving it. The shape of
the pulse waveform changes from square to a very rectangular at a speed
according to this byte.
N.B. if you are interested in how the pulse value number works, then
e-mail me sometime, as I found this out (exhaustively) a few days ago!
- Byte 7 is the instrument fx byte, and is the major thing which changes
between different music routines. Each bit in this byte determines whether
this instrument will have a certain effect in it.
- Bit#0 signals that this is a drum. Drums are made from a noise channel
and also a fast frequency down, with fast decay. Bass drums use a square
wave, and only the first 50th of a second is a noise channel. This is
the tell-tale instrument that gives away a Rob Hubbard routine! Hihats
and other drums use noise all the time.
- Bit#1 signals a 'skydive'. This is a slower frequency down, that I think
sounds like somebody yelling as they fall out of a plane .. AHHHHhhhhgh..
..hence I call it a skydive!!
- Bit#2 signals an octave arpeggio. It's a very limited arpeggio routine in
this song. Listen for the arpeggio and the skydive when combined, which
is used alot in Hubbard songs.
- All the other bits have no meaning in this music, but were used alot in
later music for the fx.
A big reason that I presented this early routine, was because there was not
too much in the way of special fx to confuse you. As a result, you can
concentrate on the guts of the code instead of the special fx :-)
How the sourcecode works:
************************
The routines at the top of the sourcecode are concerned with turning the
music on and off, and you will see that this is done through a variable called
'mstatus' (or music status). If mstatus is set to $C0, then the music is
turned off and SID is quietened, thereafter mstatus is set to $80 which means
that the music is still off, but SID doesn't need to be quietened again. When
the music is initialized, then mstatus is given a value of $40 which kicks in
the further initialization stuff. If mstatus is any other value, then the
music is being played. For any of the initialization stuff to have any meaning
to you, you ofcourse have to understand the rest of the playroutine :-)
After we have got past the on/off/init stuff, we are at the label called
'contplay' at around line 100. The first thing you should notice is that this
is the start of a huge loop that is done *3* times - once for each channel.
The loop really *is* huge, as it ends right on the last few lines of the code
:-)
Now that we are talking about routines within the loop, we are talking about
these routines being applied to the channels independantly. There are 2 main
routines within the loop, one is called NoteWork, and the other is called
SoundWork. NoteWork checks to see whether a new note is needed on this
channel, and if it is, then the notedata is fetched and stuff is initialized.
If no note is needed, then SoundWork is called which processes the instruments
and does the portamento.
NoteWork first checks the speed at which the notes are fetched. If the delay
is still occurring, then new notes are not needed and soundwork is called.
N.B. that the speed for Monty on the Run is 1, which means that a note of
length $1f will last for 64 calls to the routine (ie just over a second). If
the speed of the song is reset, then NoteWork decrements the length of the
current note. When the length of the current note hits $ff (-1) then a new
note is needed, otherwise SoundWork is jumped to.
The data for a new note is collected at the label 'getnewnote'. In the
simplest case, this involves getting the next bytes of data from the current
pattern on this channel, but if the end of the pattern is reached, then the
next pattern number is fetched by reference to the current position within
this channel's track. In an even more complex situation, the end of a track is
reached, and the current position needs to be reset to 0 before the next
pattern number can be found.
You can see quite clearly in this part of the routine where the length of
the note is collected, and it is determined whether a 2nd byte is needed,
where the pitch is collected, and the end of the song checked. You can also
see where some of the data is collected from the current instrument and jammed
into the SID registers.
SoundWork is called if no new notes are needed, and it processes the
instruments and does the portamento etc. This part of the routine is neatly
expressed in sections which are really well commented and quite easy to
understand.
- The first thing that occurs in SoundWork is that the 'gate bit' of SID is
set when the length of the note is over - this causes a release of the note.
- The vibrato routine is quite inefficient, but it's pretty good for 1985!
Ofcourse vibrato is implemented by raising and lowering the pitch of the
note ever-so-slightly causing the note to 'float'. The amount of the
vibrato is determined in the current instrument.
- The pulsework routine changes the pulsewidth between square wave and very
rectangular wave according to the pulsespeed in the current instrument.
(ie. it changes the sound of the instrument and thus alters the 'timbre')
The routine goes backwards and forwards between the two; and switches
when one extremity is reached. It's interesting to note that the current
values of the pulse width are actually stored in the instrument :-)
- Portamento is achieved by adding/subtracting an amount of frequency to
the current frequency each time this part of the routine is called.
- The instrument fx routines are also really easy to figure out, as they are
well commented. Both the drums and the skydive do a very fast frequency
down, so it is the most significant byte of the frequency which is reduced
.. and not 16-bit maths (math?!) The arpeggio is only an octave arpeggio,
so for the first 50th of a second, the current note is played, and for
the next 50th of a second, current note+12 is played, followed by the
current note again etc.
( If you don't know what an arpeggio is..it's generally when the notes of )
( a chord are played individually in a rapid succession. It produces a )
( 'full' sound depending on the speed of the arpeggio. In most cases the )
( note is changed 50 times per second, which gives a very nice sound. If )
( you have listened to some computer music, then you will have definately )
( listened to an arpeggios all the time, even if you don't realize it! )
Final Thoughts:
**************
*Bounce* I'm finally near the end of this article! It has been alot of work
to try to explain this routine, but I'm glad that I've done it *grin* If you
have any questions then please feel free to e-mail me, or even e-mail Craig if
it's after August 1993 and I'll make sure that I leave a forwarding address
with him. Also, please feel free to e-mail me and tell me what you think of
this article. I will only be bothered writing more of the same if I know that
someone is finding them useful/interesting. Also e-mail me if you are
interested in Amiga or ST music too, as I've done alot on both of those
machines.
I'm not sure whether Craig will be putting the actual sourcecode below this
text, or in some kind of Appendix. In either case, I SHALL take all legal
responsibilty for publishing Rob Hubbard's routine. Craig was quite reluctant
to publish the routine in his net-mag because of copyright reasons. As a
post-graduate law student that will be working as a commercial lawyer
(attourney for Americans :) specializing in copyright/patents for computer
software/hardware starting August this year, I don't believe that there are
any practical legal consequences for me.
I would have given an arm or a leg for a commented Rob Hubbard sourcecode in
the past, so I hope you enjoy this valuable offering.
-----------------------------------------------------------------------------
;rob hubbard
;monty on the run music driver
;this player was used (with small mods)
;for his first approx 30 musix
.org $8000
.obj motr
jmp initmusic
jmp playmusic
jmp musicoff
;====================================
;init music
initmusic =*
lda #$00 ;music num
ldy #$00
asl
sta tempstore
asl
clc
adc tempstore ;now music num*6
tax
- lda songs,x ;copy ptrs to this
sta currtrkhi,y ;music's tracks to
inx ;current tracks
iny
cpy #$06
bne -
lda #$00 ;clear control regs
sta $d404
sta $d40b
sta $d412
sta $d417
lda #$0f ;full volume
sta $d418
lda #$40 ;flag init music
sta mstatus
rts
;====================================
;music off
musicoff =*
lda #$c0 ;flag music off
sta mstatus
rts
;====================================
;play music
playmusic =*
inc counter
bit mstatus ;test music status
bmi moff ;$80 and $c0 is off
bvc contplay ;$40 init, else play
;==========
;init the song (mstatus $40)
lda #$00 ;init counter
sta counter
ldx #3-1
- sta posoffset,x ;init pos offsets
sta patoffset,x ;init pat offsets
sta lengthleft,x ;get note right away
sta notenum,x
dex
bpl -
sta mstatus ;signal music play
jmp contplay
;==========
;music is off (mstatus $80 or $c0)
moff =*
bvc + ;if mstatus $c0 then
lda #$00
sta $d404 ;kill voice 1,2,3
sta $d40b ;control registers
sta $d412
lda #$0f ;full volume still
sta $d418
lda #$80 ;flag no need to kill
sta mstatus ;sound next time
+ jmp musicend ;end
;==========
;music is playing (mstatus otherwise)
contplay =*
ldx #3-1 ;number of chanels
dec speed ;check the speed
bpl mainloop
lda resetspd ;reset speed if needed
sta speed
mainloop =*
lda regoffsets,x ;save offset to regs
sta tmpregofst ;for this channel
tay
;check whether a new note is needed
lda speed ;if speed not reset
cmp resetspd ;then skip notework
beq checknewnote
jmp vibrato
checknewnote =*
lda currtrkhi,x ;put base addr.w of
sta $02 ;this track in $2
lda currtrklo,x
sta $03
dec lengthleft,x ;check whether a new
bmi getnewnote ;note is needed
jmp soundwork ;no new note needed
;==========
;notework
;a new note is needed. get the pattern
;number/cc from this position
getnewnote =*
ldy posoffset,x ;get the data from
lda ($02),y ;the current position
cmp #$ff ;pos $ff restarts
beq restart
cmp #$fe ;pos $fe stops music
bne getnotedata ;on all channels
jmp musicend
;cc of $ff restarts this track from the
;first position
restart =*
lda #$00 ;get note immediately
sta lengthleft,x ;and reset pat,pos
sta posoffset,x
sta patoffset,x
jmp getnewnote
;get the note data from this pattern
getnotedata =*
tay
lda patptl,y ;put base addr.w of
sta $04 ;the pattern in $4
lda patpth,y
sta $05
lda #$00 ;default no portamento
sta portaval,x
ldy patoffset,x ;get offset into ptn
lda #$ff ;default no append
sta appendfl
;1st byte is the length of the note 0-31
;bit5 signals no release (see sndwork)
;bit6 signals appended note
;bit7 signals a new instrument
; or portamento coming up
lda ($04),y ;get length of note
sta savelnthcc,x
sta templnthcc
and #$1f
sta lengthleft,x
bit templnthcc ;test for append
bvs appendnote
inc patoffset,x ;pt to next data
lda templnthcc ;2nd byte needed?
bpl getpitch
;2nd byte needed as 1st byte negative
;2nd byte is the instrument number(+ve)
;or portamento speed(-ve)
iny
lda ($04),y ;get instr/portamento
bpl +
sta portaval,x ;save portamento val
jmp ++
+ sta instrnr,x ;save instr nr
+ inc patoffset,x
;3rd byte is the pitch of the note
;get the 'base frequency' here
getpitch =*
iny
lda ($04),y ;get pitch of note
sta notenum,x
asl ;pitch*2
tay
lda frequenzlo,y ;save the appropriate
sta tempfreq ;base frequency
lda frequenzhi,y
ldy tmpregofst
sta $d401,y
sta savefreqhi,x
lda tempfreq
sta $d400,y
sta savefreqlo,x
jmp +
appendnote =*
dec appendfl ;clever eh?
;fetch all the initial values from the
;instrument data structure
+ ldy tmpregofst
lda instrnr,x ;instr num
stx tempstore
asl ;instr num*8
asl
asl
tax
lda instr+2,x ;get control reg val
sta tempctrl
lda instr+2,x
and appendfl ;implement append
sta $d404,y
lda instr+0,x ;get pulse width lo
sta $d402,y
lda instr+1,x ;get pulse width hi
sta $d403,y
lda instr+3,x ;get attack/decay
sta $d405,y
lda instr+4,x ;get sustain/release
sta $d406,y
ldx tempstore ;save control reg val
lda tempctrl
sta voicectrl,x
;4th byte checks for the end of pattern
;if eop found, inc the position and
;reset patoffset for new pattern
inc patoffset,x ;preview 4th byte
ldy patoffset,x
lda ($04),y
cmp #$ff ;check for eop
bne +
lda #$00 ;end of pat reached
sta patoffset,x ;inc position for
inc posoffset,x ;the next time
+ jmp loopcont
;==========
;soundwork
;the instrument and effects processing
;routine when no new note was needed
soundwork =*
;release routine
;set off a release when the length of
;the note is exceeded
;bit4 of the 1st note-byte can specify
;for no release
ldy tmpregofst
lda savelnthcc,x ;check for no release
and #$20 ;specified
bne vibrato
lda lengthleft,x ;check for length of
bne vibrato ;exceeded
lda voicectrl,x ;length exceeded so
and #$fe ;start the release
sta $d404,y ;and kill adsr
lda #$00
sta $d405,y
sta $d406,y
;vibrato routine
;(does alot of work)
vibrato =*
lda instrnr,x ;instr num
asl
asl
asl ;instr num*8
tay
sty instnumby8 ;save instr num*8
lda instr+7,y ;get instr fx byte
sta instrfx
lda instr+6,y ;get pulse speed
sta pulsevalue
lda instr+5,y ;get vibrato depth
sta vibrdepth
beq pulsework ;check for no vibrato
lda counter ;this is clever!!
and #7 ;the counter's turned
cmp #4 ;into an oscillating
bcc + ;value (01233210)
eor #7
+ sta oscilatval
lda notenum,x ;get base note
asl ;note*2
tay ;get diff btw note
sec ;and note+1 frequency
lda frequenzlo+2,y
sbc frequenzlo,y
sta tmpvdiflo
lda frequenzhi+2,y
sbc frequenzhi,y
- lsr ;divide difference by
ror tmpvdiflo ;2 for each vibrdepth
dec vibrdepth
bpl -
sta tmpvdifhi
lda frequenzlo,y ;save note frequency
sta tmpvfrqlo
lda frequenzhi,y
sta tmpvfrqhi
lda savelnthcc,x ;no vibrato if note
and #$1f ;length less than 8
cmp #8
bcc +
ldy oscilatval
- dey ;depending on the osc
bmi + ;value, add the vibr
clc ;freq that many times
lda tmpvfrqlo ;to the base freq
adc tmpvdiflo
sta tmpvfrqlo
lda tmpvfrqhi
adc tmpvdifhi
sta tmpvfrqhi
jmp -
+ ldy tmpregofst ;save the final
lda tmpvfrqlo ;frequencies
sta $d400,y
lda tmpvfrqhi
sta $d401,y
;pulse-width timbre routine
;depending on the control/speed byte in
;the instrument datastructure, the pulse
;width is of course inc/decremented to
;produce timbre
;strangely the delay value is also the
;size of the inc/decrements
pulsework =*
lda pulsevalue ;check for pulsework
beq portamento ;needed this instr
ldy instnumby8
and #$1f
dec pulsedelay,x ;pulsedelay-1
bpl portamento
sta pulsedelay,x ;reset pulsedelay
lda pulsevalue ;restrict pulse speed
and #$e0 ;from $00-$1f
sta pulsespeed
lda pulsedir,x ;pulsedir 0 is up and
bne pulsedown ;1 is down
lda pulsespeed ;pulse width up
clc
adc instr+0,y ;add the pulsespeed
pha ;to the pulse width
lda instr+1,y
adc #$00
and #$0f
pha
cmp #$0e ;go pulsedown when
bne dumpulse ;the pulse value
inc pulsedir,x ;reaches max ($0exx)
jmp dumpulse
pulsedown =*
sec ;pulse width down
lda instr+0,y
sbc pulsespeed ;sub the pulsespeed
pha ;from the pulse width
lda instr+1,y
sbc #$00
and #$0f
pha
cmp #$08 ;go pulseup when
bne dumpulse ;the pulse value
dec pulsedir,x ;reaches min ($08xx)
dumpulse =*
stx tempstore ;dump pulse width to
ldx tmpregofst ;chip and back into
pla ;the instr data str
sta instr+1,y
sta $d403,x
pla
sta instr+0,y
sta $d402,x
ldx tempstore
;portamento routine
;portamento comes from the second byte
;if it's a negative value
portamento =*
ldy tmpregofst
lda portaval,x ;check for portamento
beq drums ;none
and #$7e ;toad unwanted bits
sta tempstore
lda portaval,x ;bit0 signals up/down
and #$01
beq portup
sec ;portamento down
lda savefreqlo,x ;sub portaval from
sbc tempstore ;current frequency
sta savefreqlo,x
sta $d400,y
lda savefreqhi,x
sbc #$00 ;(word arithmetic)
sta savefreqhi,x
sta $d401,y
jmp drums
portup =*
clc ;portamento up
lda savefreqlo,x ;add portval to
adc tempstore ;current frequency
sta savefreqlo,x
sta $d400,y
lda savefreqhi,x
adc #$00
sta savefreqhi,x
sta $d401,y
;bit0 instrfx are the drum routines
;the actual drum timbre depends on the
;crtl register value for the instrument:
;ctrlreg 0 is always noise
;ctrlreg x is noise for 1st vbl and x
;from then on
;see that the drum is made by rapid hi
;to low frequency slide with fast attack
;and decay
drums =*
lda instrfx ;check if drums
and #$01 ;needed this instr
beq skydive
lda savefreqhi,x ;don't bother if freq
beq skydive ;can't go any lower
lda lengthleft,x ;or if the note has
beq skydive ;finished
lda savelnthcc,x ;check if this is the
and #$1f ;first vbl for this
sec ;instrument-note
sbc #$01
cmp lengthleft,x
ldy tmpregofst
bcc firstime
lda savefreqhi,x ;not the first time
dec savefreqhi,x ;so dec freqhi for
sta $d401,y ;drum sound
lda voicectrl,x ;if ctrlreg is 0 then
and #$fe ;noise is used always
bne dumpctrl
firstime =*
lda savefreqhi,x ;noise is used for
sta $d401,y ;the first vbl also
lda #$80 ;(set noise)
dumpctrl =*
sta $d404,y
;bit1 instrfx is the skydive
;a long portamento-down from the note
;to zerofreq
skydive =*
lda instrfx ;check if skydive
and #$02 ;needed this instr
beq octarp
lda counter ;every 2nd vbl
and #$01
beq octarp
lda savefreqhi,x ;check if skydive
beq octarp ;already complete
dec savefreqhi,x ;decr and save the
ldy tmpregofst ;high byte freq
sta $d401,y
;bit2 instrfx is an octave arpeggio
;pretty tame huh?
octarp =*
lda instrfx ;check if arpt needed
and #$04
beq loopcont
lda counter ;only 2 arpt values
and #$01
beq +
lda notenum,x ;odd, note+12
clc
adc #$0c
jmp ++
+ lda notenum,x ;even, note
+ asl ;dump the corresponding
tay ;frequencies
lda frequenzlo,y
sta tempfreq
lda frequenzhi,y
ldy tmpregofst
sta $d401,y
lda tempfreq
sta $d400,y
;==========
;end of dbf loop
loopcont =*
dex ;dbf mainloop
bmi musicend
jmp mainloop
musicend =*
rts
;====================================
;frequenz data
;====================================
frequenzlo .byt $16
frequenzhi .byt $01
.byt $27,$01,$38,$01,$4b,$01
.byt $5f,$01,$73,$01,$8a,$01,$a1,$01
.byt $ba,$01,$d4,$01,$f0,$01,$0e,$02
.byt $2d,$02,$4e,$02,$71,$02,$96,$02
.byt $bd,$02,$e7,$02,$13,$03,$42,$03
.byt $74,$03,$a9,$03,$e0,$03,$1b,$04
.byt $5a,$04,$9b,$04,$e2,$04,$2c,$05
.byt $7b,$05,$ce,$05,$27,$06,$85,$06
.byt $e8,$06,$51,$07,$c1,$07,$37,$08
.byt $b4,$08,$37,$09,$c4,$09,$57,$0a
.byt $f5,$0a,$9c,$0b,$4e,$0c,$09,$0d
.byt $d0,$0d,$a3,$0e,$82,$0f,$6e,$10
.byt $68,$11,$6e,$12,$88,$13,$af,$14
.byt $eb,$15,$39,$17,$9c,$18,$13,$1a
.byt $a1,$1b,$46,$1d,$04,$1f,$dc,$20
.byt $d0,$22,$dc,$24,$10,$27,$5e,$29
.byt $d6,$2b,$72,$2e,$38,$31,$26,$34
.byt $42,$37,$8c,$3a,$08,$3e,$b8,$41
.byt $a0,$45,$b8,$49,$20,$4e,$bc,$52
.byt $ac,$57,$e4,$5c,$70,$62,$4c,$68
.byt $84,$6e,$18,$75,$10,$7c,$70,$83
.byt $40,$8b,$70,$93,$40,$9c,$78,$a5
.byt $58,$af,$c8,$b9,$e0,$c4,$98,$d0
.byt $08,$dd,$30,$ea,$20,$f8,$2e,$fd
regoffsets .byt $00,$07,$0e
tmpregofst .byt $00
posoffset .byt $00,$00,$00
patoffset .byt $00,$00,$00
lengthleft .byt $00,$00,$00
savelnthcc .byt $00,$00,$00
voicectrl .byt $00,$00,$00
notenum .byt $00,$00,$00
instrnr .byt $00,$00,$00
appendfl .byt $00
templnthcc .byt $00
tempfreq .byt $00
tempstore .byt $00
tempctrl .byt $00
vibrdepth .byt $00
pulsevalue .byt $00
tmpvdiflo .byt $00
tmpvdifhi .byt $00
tmpvfrqlo .byt $00
tmpvfrqhi .byt $00
oscilatval .byt $00
pulsedelay .byt $00,$00,$00
pulsedir .byt $00,$00,$00
speed .byt $00
resetspd .byt $01
instnumby8 .byt $00
mstatus .byt $c0
savefreqhi .byt $00,$00,$00
savefreqlo .byt $00,$00,$00
portaval .byt $00,$00,$00
instrfx .byt $00
pulsespeed .byt $00
counter .byt $00
currtrkhi .byt $00,$00,$00
currtrklo .byt $00,$00,$00
;====================================
;monty on the run main theme
;====================================
songs =*
.byt <montymaintr1
.byt <montymaintr2
.byt <montymaintr3
.byt >montymaintr1
.byt >montymaintr2
.byt >montymaintr3
;====================================
;pointers to the patterns
;low pointers
patptl =*
.byt <ptn00
.byt <ptn01
.byt <ptn02
.byt <ptn03
.byt <ptn04
.byt <ptn05
.byt <ptn06
.byt <ptn07
.byt <ptn08
.byt <ptn09
.byt <ptn0a
.byt <ptn0b
.byt <ptn0c
.byt <ptn0d
.byt <ptn0e
.byt <ptn0f
.byt <ptn10
.byt <ptn11
.byt <ptn12
.byt <ptn13
.byt <ptn14
.byt <ptn15
.byt <ptn16
.byt <ptn17
.byt <ptn18
.byt <ptn19
.byt <ptn1a
.byt <ptn1b
.byt <ptn1c
.byt <ptn1d
.byt <ptn1e
.byt <ptn1f
.byt <ptn20
.byt <ptn21
.byt <ptn22
.byt <ptn23
.byt <ptn24
.byt <ptn25
.byt <ptn26
.byt <ptn27
.byt <ptn28
.byt <ptn29
.byt <ptn2a
.byt <ptn2b
.byt <ptn2c
.byt <ptn2d
.byt 0
.byt <ptn2f
.byt <ptn30
.byt <ptn31
.byt <ptn32
.byt <ptn33
.byt <ptn34
.byt <ptn35
.byt <ptn36
.byt <ptn37
.byt <ptn38
.byt <ptn39
.byt <ptn3a
.byt <ptn3b
;high pointers
patpth =*
.byt >ptn00
.byt >ptn01
.byt >ptn02
.byt >ptn03
.byt >ptn04
.byt >ptn05
.byt >ptn06
.byt >ptn07
.byt >ptn08
.byt >ptn09
.byt >ptn0a
.byt >ptn0b
.byt >ptn0c
.byt >ptn0d
.byt >ptn0e
.byt >ptn0f
.byt >ptn10
.byt >ptn11
.byt >ptn12
.byt >ptn13
.byt >ptn14
.byt >ptn15
.byt >ptn16
.byt >ptn17
.byt >ptn18
.byt >ptn19
.byt >ptn1a
.byt >ptn1b
.byt >ptn1c
.byt >ptn1d
.byt >ptn1e
.byt >ptn1f
.byt >ptn20
.byt >ptn21
.byt >ptn22
.byt >ptn23
.byt >ptn24
.byt >ptn25
.byt >ptn26
.byt >ptn27
.byt >ptn28
.byt >ptn29
.byt >ptn2a
.byt >ptn2b
.byt >ptn2c
.byt >ptn2d
.byt 0
.byt >ptn2f
.byt >ptn30
.byt >ptn31
.byt >ptn32
.byt >ptn33
.byt >ptn34
.byt >ptn35
.byt >ptn36
.byt >ptn37
.byt >ptn38
.byt >ptn39
.byt >ptn3a
.byt >ptn3b
;====================================
;tracks
;====================================
;track1
montymaintr1 =*
.byt $11,$14,$17,$1a,$00,$27,$00,$28
.byt $03,$05,$00,$27,$00,$28,$03,$05
.byt $07,$3a,$14,$17,$00,$27,$00,$28
.byt $2f,$30,$31,$31,$32,$33,$33,$34
.byt $34,$34,$34,$34,$34,$34,$34,$35
.byt $35,$35,$35,$35,$35,$36,$12,$37
.byt $38,$09,$2a,$09,$2b,$09,$0a,$09
.byt $2a,$09,$2b,$09,$0a,$0d,$0d,$0f
.byt $ff
;track2
montymaintr2 =*
.byt $12,$15,$18,$1b,$2d,$39,$39
.byt $39,$39,$39,$39,$2c,$39,$39,$39
.byt $39,$39,$39,$2c,$39,$39,$39,$01
.byt $01,$29,$29,$2c,$15,$18,$39,$39
.byt $39,$39,$39,$39,$39,$39,$39,$39
.byt $39,$39,$39,$39,$39,$39,$39,$39
.byt $39,$39,$39,$39,$39,$39,$39,$39
.byt $39,$39,$39,$39,$39,$01,$01,$01
.byt $29,$39,$39,$39,$01,$01,$01,$29
.byt $39,$39,$39,$39,$ff
;track3
montymaintr3 =*
.byt $13,$16,$19
.byt $1c,$02,$02,$1d,$1e,$02,$02,$1d
.byt $1f,$04,$04,$20,$20,$06,$02,$02
.byt $1d,$1e,$02,$02,$1d,$1f,$04,$04
.byt $20,$20,$06,$08,$08,$08,$08,$21
.byt $21,$21,$21,$22,$22,$22,$23,$22
.byt $24,$25,$3b,$26,$26,$26,$26,$26
.byt $26,$26,$26,$26,$26,$26,$26,$26
.byt $26,$26,$26,$02,$02,$1d,$1e,$02
.byt $02,$1d,$1f,$2f,$2f,$2f,$2f,$2f
.byt $2f,$2f,$2f,$2f,$2f,$2f,$2f,$2f
.byt $0b,$0b,$1d,$1d,$0b,$0b,$1d,$0b
.byt $0b,$0b,$0c,$0c,$1d,$1d,$1d,$10
.byt $0b,$0b,$1d,$1d,$0b,$0b,$1d,$0b
.byt $0b,$0b,$0c,$0c,$1d,$1d,$1d,$10
.byt $0b,$1d,$0b,$1d,$0b,$1d,$0b,$1d
.byt $0b,$0c,$1d,$0b,$0c,$23,$0b,$0b
.byt $ff
;====================================
;patterns
;====================================
ptn00 =*
.byt $83,$00,$37,$01,$3e,$01,$3e,$03
.byt $3d,$03,$3e,$03,$43,$03,$3e,$03
.byt $3d,$03,$3e,$03,$37,$01,$3e,$01
.byt $3e,$03,$3d,$03,$3e,$03,$43,$03
.byt $42,$03,$43,$03,$45,$03,$46,$01
.byt $48,$01,$46,$03,$45,$03,$43,$03
.byt $4b,$01,$4d,$01,$4b,$03,$4a,$03
.byt $48,$ff
ptn27 =*
.byt $1f,$4a,$ff
ptn28 =*
.byt $03,$46,$01,$48,$01,$46,$03,$45
.byt $03,$4a,$0f,$43,$ff
ptn03 =*
.byt $bf,$06
.byt $48,$07,$48,$01,$4b,$01,$4a,$01
.byt $4b,$01,$4a,$03,$4b,$03,$4d,$03
.byt $4b,$03,$4a,$3f,$48,$07,$48,$01
.byt $4b,$01,$4a,$01,$4b,$01,$4a,$03
.byt $4b,$03,$4d,$03,$4b,$03,$48,$3f
.byt $4c,$07,$4c,$01,$4f,$01,$4e,$01
.byt $4f,$01,$4e,$03,$4f,$03,$51,$03
.byt $4f,$03,$4e,$3f,$4c,$07,$4c,$01
.byt $4f,$01,$4e,$01,$4f,$01,$4e,$03
.byt $4f,$03,$51,$03,$4f,$03,$4c,$ff
ptn05 =*
.byt $83,$04,$26,$03,$29,$03,$28,$03
.byt $29,$03,$26,$03,$35,$03,$34,$03
.byt $32,$03,$2d,$03,$30,$03,$2f,$03
.byt $30,$03,$2d,$03,$3c,$03,$3b,$03
.byt $39,$03,$30,$03,$33,$03,$32,$03
.byt $33,$03,$30,$03,$3f,$03,$3e,$03
.byt $3c,$03,$46,$03,$45,$03,$43,$03
.byt $3a,$03,$39,$03,$37,$03,$2e,$03
.byt $2d,$03,$26,$03,$29,$03,$28,$03
.byt $29,$03,$26,$03,$35,$03,$34,$03
.byt $32,$03,$2d,$03,$30,$03,$2f,$03
.byt $30,$03,$2d,$03,$3c,$03,$3b,$03
.byt $39,$03,$30,$03,$33,$03,$32,$03
.byt $33,$03,$30,$03,$3f,$03,$3e,$03
.byt $3c,$03,$34,$03,$37,$03,$36,$03
.byt $37,$03,$34,$03,$37,$03,$3a,$03
.byt $3d
ptn3a =*
.byt $03,$3e,$07,$3e,$07,$3f,$07
.byt $3e,$03,$3c,$07,$3e,$57,$ff
ptn07 =*
.byt $8b
.byt $00,$3a,$01,$3a,$01,$3c,$03,$3d
.byt $03,$3f,$03,$3d,$03,$3c,$0b,$3a
.byt $03,$39,$07,$3a,$81,$06,$4b,$01
.byt $4d,$01,$4e,$01,$4d,$01,$4e,$01
.byt $4d,$05,$4b,$81,$00,$3a,$01,$3c
.byt $01,$3d,$03,$3f,$03,$3d,$03,$3c
.byt $03,$3a,$03,$39,$1b,$3a,$0b,$3b
.byt $01,$3b,$01,$3d,$03,$3e,$03,$40
.byt $03,$3e,$03,$3d,$0b,$3b,$03,$3a
.byt $07,$3b,$81,$06,$4c,$01,$4e,$01
.byt $4f,$01,$4e,$01,$4f,$01,$4e,$05
.byt $4c,$81,$00,$3b,$01,$3d,$01,$3e
.byt $03,$40,$03,$3e,$03,$3d,$03,$3b
.byt $03,$3a,$1b,$3b,$8b,$05,$35,$03
.byt $33,$07,$32,$03,$30,$03,$2f,$0b
.byt $30,$03,$32,$0f,$30,$0b,$35,$03
.byt $33,$07,$32,$03,$30,$03,$2f,$1f
.byt $30,$8b,$00,$3c,$01,$3c,$01,$3e
.byt $03,$3f,$03,$41,$03,$3f,$03,$3e
.byt $0b,$3d,$01,$3d,$01,$3f,$03,$40
.byt $03,$42,$03,$40,$03,$3f,$03,$3e
.byt $01,$3e,$01,$40,$03,$41,$03,$40
.byt $03,$3e,$03,$3d,$03,$3e,$03,$3c
.byt $03,$3a,$01,$3a,$01,$3c,$03,$3d
.byt $03,$3c,$03,$3a,$03,$39,$03,$3a
.byt $03,$3c,$ff
ptn09 =*
.byt $83,$00,$32,$01,$35,$01,$34,$03
.byt $32,$03,$35,$03,$34,$03,$32,$03
.byt $35,$01,$34,$01,$32,$03,$32,$03
.byt $3a,$03,$39,$03,$3a,$03,$32,$03
.byt $3a,$03,$39,$03,$3a,$ff
ptn2a =*
.byt $03,$34,$01,$37,$01,$35,$03,$34
.byt $03,$37,$03,$35,$03,$34,$03,$37
.byt $01,$35,$01,$34,$03,$34,$03,$3a
.byt $03,$39,$03,$3a,$03,$34,$03,$3a
.byt $03,$39,$03,$3a,$ff
ptn2b =*
.byt $03,$39,$03,$38,$03,$39,$03,$3a
.byt $03,$39,$03,$37,$03,$35,$03,$34
.byt $03,$35,$03,$34,$03,$35,$03,$37
.byt $03,$35,$03,$34,$03,$32,$03,$31
.byt $ff
ptn0a =*
.byt $03
.byt $37,$01,$3a,$01,$39,$03,$37,$03
.byt $3a,$03,$39,$03,$37,$03,$3a,$01
.byt $39,$01,$37,$03,$37,$03,$3e,$03
.byt $3d,$03,$3e,$03,$37,$03,$3e,$03
.byt $3d,$03,$3e,$03,$3d,$01,$40,$01
.byt $3e,$03,$3d,$03,$40,$01,$3e,$01
.byt $3d,$03,$40,$03,$3e,$03,$40,$03
.byt $40,$01,$43,$01,$41,$03,$40,$03
.byt $43,$01,$41,$01,$40,$03,$43,$03
.byt $41,$03,$43,$03,$43,$01,$46,$01
.byt $45,$03,$43,$03,$46,$01,$45,$01
.byt $43,$03,$46,$03,$45,$03,$43,$01
.byt $48,$01,$49,$01,$48,$01,$46,$01
.byt $45,$01,$46,$01,$45,$01,$43,$01
.byt $41,$01,$43,$01,$41,$01,$40,$01
.byt $3d,$01,$39,$01,$3b,$01,$3d,$ff
ptn0d =*
.byt $01,$3e,$01,$39,$01,$35,$01,$39
.byt $01,$3e,$01,$39,$01,$35,$01,$39
.byt $03,$3e,$01,$41,$01,$40,$03,$40
.byt $01,$3d,$01,$3e,$01,$40,$01,$3d
.byt $01,$39,$01,$3d,$01,$40,$01,$3d
.byt $01,$39,$01,$3d,$03,$40,$01,$43
.byt $01,$41,$03,$41,$01,$3e,$01,$40
.byt $01,$41,$01,$3e,$01,$39,$01,$3e
.byt $01,$41,$01,$3e,$01,$39,$01,$3e
.byt $03,$41,$01,$45,$01,$43,$03,$43
.byt $01,$40,$01,$41,$01,$43,$01,$40
.byt $01,$3d,$01,$40,$01,$43,$01,$40
.byt $01,$3d,$01,$40,$01,$46,$01,$43
.byt $01,$45,$01,$46,$01,$44,$01,$43
.byt $01,$40,$01,$3d,$ff
ptn0f =*
.byt $01,$3e,$01
.byt $39,$01,$35,$01,$39,$01,$3e,$01
.byt $39,$01,$35,$01,$39,$01,$3e,$01
.byt $39,$01,$35,$01,$39,$01,$3e,$01
.byt $39,$01,$35,$01,$39,$01,$3e,$01
.byt $3a,$01,$37,$01,$3a,$01,$3e,$01
.byt $3a,$01,$37,$01,$3a,$01,$3e,$01
.byt $3a,$01,$37,$01,$3a,$01,$3e,$01
.byt $3a,$01,$37,$01,$3a,$01,$40,$01
.byt $3d,$01,$39,$01,$3d,$01,$40,$01
.byt $3d,$01,$39,$01,$3d,$01,$40,$01
.byt $3d,$01,$39,$01,$3d,$01,$40,$01
.byt $3d,$01,$39,$01,$3d,$01,$41,$01
.byt $3e,$01,$39,$01,$3e,$01,$41,$01
.byt $3e,$01,$39,$01,$3e,$01,$41,$01
.byt $3e,$01,$39,$01,$3e,$01,$41,$01
.byt $3e,$01,$39,$01,$3e,$01,$43,$01
.byt $3e,$01,$3a,$01,$3e,$01,$43,$01
.byt $3e,$01,$3a,$01,$3e,$01,$43,$01
.byt $3e,$01,$3a,$01,$3e,$01,$43,$01
.byt $3e,$01,$3a,$01,$3e,$01,$43,$01
.byt $3f,$01,$3c,$01,$3f,$01,$43,$01
.byt $3f,$01,$3c,$01,$3f,$01,$43,$01
.byt $3f,$01,$3c,$01,$3f,$01,$43,$01
.byt $3f,$01,$3c,$01,$3f,$01,$45,$01
.byt $42,$01,$3c,$01,$42,$01,$45,$01
.byt $42,$01,$3c,$01,$42,$01,$48,$01
.byt $45,$01,$42,$01,$45,$01,$4b,$01
.byt $48,$01,$45,$01,$48,$01,$4b,$01
.byt $4a,$01,$48,$01,$4a,$01,$4b,$01
.byt $4a,$01,$48,$01,$4a,$01,$4b,$01
.byt $4a,$01,$48,$01,$4a,$01,$4c,$01
.byt $4e,$03,$4f,$ff
ptn11 =*
.byt $bf,$06,$56,$1f,$57,$1f,$56,$1f
.byt $5b,$1f,$56,$1f,$57,$1f,$56,$1f
.byt $4f,$ff
ptn12 =*
.byt $bf,$0c,$68,$7f,$7f,$7f,$7f,$7f
.byt $7f,$7f,$ff
ptn13 =*
.byt $bf,$08,$13,$3f,$13,$3f,$13,$3f
.byt $13,$3f,$13,$3f,$13,$3f,$13,$1f
.byt $13,$ff
ptn14 =*
.byt $97,$09,$2e,$03,$2e,$1b,$32,$03
.byt $32,$1b,$31,$03,$31,$1f,$34,$43
.byt $17,$32,$03,$32,$1b,$35,$03,$35
.byt $1b,$34,$03,$34,$0f,$37,$8f,$0a
.byt $37,$43,$ff
ptn15 =*
.byt $97,$09,$2b,$03,$2b,$1b,$2e,$03
.byt $2e,$1b,$2d,$03,$2d,$1f,$30,$43
.byt $17,$2e,$03,$2e,$1b,$32,$03,$32
.byt $1b,$31,$03,$31,$0f,$34,$8f,$0a
.byt $34,$43,$ff
ptn16 =*
.byt $0f,$1f,$0f,$1f,$0f,$1f,$0f,$1f
.byt $0f,$1f,$0f,$1f,$0f,$1f,$0f,$1f
.byt $0f,$1f,$0f,$1f,$0f,$1f,$0f,$1f
.byt $0f,$1f,$0f,$1f,$0f,$1f,$0f,$1f
.byt $ff
ptn17 =*
.byt $97,$09,$33,$03,$33,$1b,$37,$03
.byt $37,$1b,$36,$03,$36,$1f,$39,$43
.byt $17,$37,$03,$37,$1b,$3a,$03,$3a
.byt $1b,$39,$03,$39,$2f,$3c,$21,$3c
.byt $21,$3d,$21,$3e,$21,$3f,$21,$40
.byt $21,$41,$21,$42,$21,$43,$21,$44
.byt $01,$45,$ff
ptn18 =*
.byt $97,$09,$30,$03,$30,$1b,$33,$03
.byt $33,$1b,$32,$03,$32,$1f,$36,$43
.byt $17,$33,$03,$33,$1b,$37,$03,$37
.byt $1b,$36,$03,$36,$2f,$39,$21,$39
.byt $21,$3a,$21,$3b,$21,$3c,$21,$3d
.byt $21,$3e,$21,$3f,$21,$40,$21,$41
.byt $01,$42,$ff
ptn19 =*
.byt $0f,$1a,$0f,$1a,$0f,$1a,$0f,$1a
.byt $0f,$1a,$0f,$1a,$0f,$1a,$0f,$1a
.byt $0f,$1a,$0f,$1a,$0f,$1a,$0f,$1a
.byt $0f,$1a,$0f,$1a,$0f,$1a,$0f,$1a
.byt $ff
ptn1a =*
.byt $1f,$46,$bf,$0a,$46,$7f,$7f,$ff
ptn1b =*
.byt $1f,$43,$bf,$0a,$43,$7f,$ff
ptn1c =*
.byt $83,$02,$13,$03,$13,$03,$1e,$03
.byt $1f,$03,$13,$03,$13,$03,$1e,$03
.byt $1f,$03,$13,$03,$13,$03,$1e,$03
.byt $1f,$03,$13,$03,$13,$03,$1e,$03
.byt $1f,$03,$13,$03,$13,$03,$1e,$03
.byt $1f,$03,$13,$03,$13,$03,$1e,$03
.byt $1f,$03,$13,$03,$13,$03,$1e,$03
.byt $1f,$03,$13,$03,$13,$03,$1e,$03
.byt $1f,$ff
ptn29 =*
.byt $8f,$0b,$38,$4f,$ff
ptn2c =*
.byt $83,$0e,$32,$07,$32,$07,$2f,$07
.byt $2f,$03,$2b,$87,$0b,$46,$83,$0e
.byt $2c,$03,$2c,$8f,$0b,$32,$ff
ptn2d =*
.byt $43,$83,$0e,$32,$03,$32,$03,$2f
.byt $03,$2f,$03,$2c,$87,$0b,$38,$ff
ptn39 =*
.byt $83,$01
.byt $43,$01,$4f,$01,$5b,$87,$03,$2f
.byt $83,$01,$43,$01,$4f,$01,$5b,$87
.byt $03,$2f,$83,$01,$43,$01,$4f,$01
.byt $5b,$87,$03,$2f,$83,$01,$43,$01
.byt $4f,$01,$5b,$87,$03,$2f,$83,$01
.byt $43,$01,$4f,$01,$5b,$87,$03,$2f
.byt $83,$01,$43,$01,$4f,$01,$5b,$87
.byt $03,$2f
ptn01 =*
.byt $83,$01,$43,$01,$4f,$01,$5b,$87
.byt $03,$2f,$83,$01,$43,$01,$4f,$01
.byt $5b,$87,$03,$2f,$ff
ptn02 =*
.byt $83,$02,$13,$03,$13,$03,$1f,$03
.byt $1f,$03,$13,$03,$13,$03,$1f,$03
.byt $1f,$ff
ptn1d =*
.byt $03,$15,$03,$15,$03,$1f,$03,$21
.byt $03,$15,$03,$15,$03,$1f,$03,$21
.byt $ff
ptn1e =*
.byt $03,$1a,$03,$1a,$03,$1c,$03,$1c
.byt $03,$1d,$03,$1d,$03,$1e,$03,$1e
.byt $ff
ptn1f =*
.byt $03,$1a,$03,$1a,$03,$24,$03,$26
.byt $03,$13,$03,$13,$07,$1f,$ff
ptn04 =*
.byt $03,$18,$03,$18,$03,$24,$03,$24
.byt $03,$18,$03,$18,$03,$24,$03,$24
.byt $03,$20,$03,$20,$03,$2c,$03,$2c
.byt $03,$20,$03,$20,$03,$2c,$03,$2c
.byt $ff
ptn20 =*
.byt $03,$19,$03,$19,$03
.byt $25,$03,$25,$03,$19,$03,$19,$03
.byt $25,$03,$25,$03,$21,$03,$21,$03
.byt $2d,$03,$2d,$03,$21,$03,$21,$03
.byt $2d,$03,$2d,$ff
ptn06 =*
.byt $03,$1a,$03,$1a
.byt $03,$26,$03,$26,$03,$1a,$03,$1a
.byt $03,$26,$03,$26,$03,$15,$03,$15
.byt $03,$21,$03,$21,$03,$15,$03,$15
.byt $03,$21,$03,$21,$03,$18,$03,$18
.byt $03,$24,$03,$24,$03,$18,$03,$18
.byt $03,$24,$03,$24,$03,$1f,$03,$1f
.byt $03,$2b,$03,$2b,$03,$1f,$03,$1f
.byt $03,$2b,$03,$2b,$03,$1a,$03,$1a
.byt $03,$26,$03,$26,$03,$1a,$03,$1a
.byt $03,$26,$03,$26,$03,$15,$03,$15
.byt $03,$21,$03,$21,$03,$15,$03,$15
.byt $03,$21,$03,$21,$03,$18,$03,$18
.byt $03,$24,$03,$24,$03,$18,$03,$18
.byt $03,$24,$03,$24,$03,$1c,$03,$1c
.byt $03,$28,$03,$28,$03,$1c,$03,$1c
.byt $03,$28,$03,$28
ptn3b =*
.byt $83,$04,$36,$07
.byt $36,$07,$37,$07,$36,$03,$33,$07
.byt $32,$57,$ff
ptn08 =*
.byt $83,$02,$1b,$03,$1b,$03,$27,$03
.byt $27,$03,$1b,$03,$1b,$03,$27,$03
.byt $27,$ff
ptn21 =*
.byt $03,$1c,$03,$1c,$03,$28,$03,$28
.byt $03,$1c,$03,$1c,$03,$28,$03,$28
.byt $ff
ptn22 =*
.byt $03,$1d,$03,$1d,$03,$29,$03,$29
.byt $03,$1d,$03,$1d,$03,$29,$03,$29
.byt $ff
ptn23 =*
.byt $03,$18,$03,$18,$03,$24,$03,$24
.byt $03,$18,$03,$18,$03,$24,$03,$24
.byt $ff
ptn24 =*
.byt $03,$1e,$03,$1e,$03,$2a,$03,$2a
.byt $03,$1e,$03,$1e,$03,$2a,$03,$2a
.byt $ff
ptn25 =*
.byt $83,$05,$26,$01,$4a,$01,$34,$03
.byt $29,$03,$4c,$03,$4a,$03,$31,$03
.byt $4a,$03,$24,$03,$22,$01,$46,$01
.byt $30,$03,$25,$03,$48,$03,$46,$03
.byt $2d,$03,$46,$03,$24,$ff
ptn0b =*
.byt $83,$02,$1a,$03,$1a,$03,$26,$03
.byt $26,$03,$1a,$03,$1a,$03,$26,$03
.byt $26,$ff
ptn0c =*
.byt $03,$13,$03,$13,$03,$1d,$03,$1f
.byt $03,$13,$03,$13,$03,$1d,$03,$1f
.byt $ff
ptn26 =*
.byt $87,$02,$1a,$87,$03,$2f,$83,$02
.byt $26,$03,$26,$87,$03,$2f,$ff
ptn10 =*
.byt $07,$1a,$4f,$47,$ff
ptn0e =*
.byt $03,$1f,$03,$1f,$03,$24,$03,$26
.byt $07,$13,$47,$ff
ptn30 =*
.byt $bf,$0f,$32,$0f,$32,$8f,$90,$30
.byt $3f,$32,$13,$32,$03,$32,$03,$35
.byt $03,$37,$3f,$37,$0f,$37,$8f,$90
.byt $30,$3f,$32,$13,$32,$03,$2d,$03
.byt $30,$03,$32,$ff
ptn31 =*
.byt $0f,$32
.byt $af,$90,$35,$0f,$37,$a7,$99,$37
.byt $07,$35,$3f,$32,$13,$32,$03,$32
.byt $a3,$e8,$35,$03,$37,$0f,$35,$af
.byt $90,$37,$0f,$37,$a7,$99,$37,$07
.byt $35,$3f,$32,$13,$32,$03,$2d,$a3
.byt $e8,$30,$03,$32,$ff
ptn32 =*
.byt $07,$32,$03
.byt $39,$13,$3c,$a7,$9a,$37,$a7,$9b
.byt $38,$07,$37,$03,$35,$03,$32,$03
.byt $39,$1b,$3c,$a7,$9a,$37,$a7,$9b
.byt $38,$07,$37,$03,$35,$03,$32,$03
.byt $39,$03,$3c,$03,$3e,$03,$3c,$07
.byt $3e,$03,$3c,$03,$39,$a7,$9a,$37
.byt $a7,$9b,$38,$07,$37,$03,$35,$03
.byt $32,$af,$90,$3c,$1f,$3e,$43,$03
.byt $3e,$03,$3c,$03,$3e,$ff
ptn33 =*
.byt $03,$3e
.byt $03,$3e,$a3,$e8,$3c,$03,$3e,$03
.byt $3e,$03,$3e,$a3,$e8,$3c,$03,$3e
.byt $03,$3e,$03,$3e,$a3,$e8,$3c,$03
.byt $3e,$03,$3e,$03,$3e,$a3,$e8,$3c
.byt $03,$3e,$af,$91,$43,$1f,$41,$43
.byt $03,$3e,$03,$41,$03,$43,$03,$43
.byt $03,$43,$a3,$e8,$41,$03,$43,$03
.byt $43,$03,$43,$a3,$e8,$41,$03,$43
.byt $03,$45,$03,$48,$a3,$fd,$45,$03
.byt $44,$01,$43,$01,$41,$03,$3e,$03
.byt $3c,$03,$3e,$2f,$3e,$bf,$98,$3e
.byt $43,$03,$3e,$03,$3c,$03,$3e,$ff
ptn34 =*
.byt $03,$4a,$03,$4a,$a3,$f8,$48,$03
.byt $4a,$03,$4a,$03,$4a,$a3,$f8,$48
.byt $03,$4a,$ff
ptn35 =*
.byt $01,$51,$01,$54,$01
.byt $51,$01,$54,$01,$51,$01,$54,$01
.byt $51,$01,$54,$01,$51,$01,$54,$01
.byt $51,$01,$54,$01,$51,$01,$54,$01
.byt $51,$01,$54,$ff
ptn36 =*
.byt $01,$50,$01,$4f
.byt $01,$4d,$01,$4a,$01,$4f,$01,$4d
.byt $01,$4a,$01,$48,$01,$4a,$01,$48
.byt $01,$45,$01,$43,$01,$44,$01,$43
.byt $01,$41,$01,$3e,$01,$43,$01,$41
.byt $01,$3e,$01,$3c,$01,$3e,$01,$3c
.byt $01,$39,$01,$37,$01,$38,$01,$37
.byt $01,$35,$01,$32,$01,$37,$01,$35
.byt $01,$32,$01,$30,$ff
ptn37 =*
.byt $5f,$5f,$5f
.byt $47,$83,$0e,$32,$07,$32,$07,$2f
.byt $03,$2f,$07,$2f,$97,$0b,$3a,$5f
.byt $5f,$47,$8b,$0e,$32,$03,$32,$03
.byt $2f,$03,$2f,$47,$97,$0b,$3a,$5f
.byt $5f,$47,$83,$0e,$2f,$0b,$2f,$03
.byt $2f,$03,$2f,$87,$0b,$30,$17,$3a
.byt $5f,$8b,$0e,$32,$0b,$32,$0b,$2f
.byt $0b,$2f,$07,$2c,$07,$2c,$ff
ptn38 =*
.byt $87
.byt $0b,$34,$17,$3a,$5f,$5f,$84,$0e
.byt $32,$04,$32,$05,$32,$04,$2f,$04
.byt $2f,$05,$2f,$47,$97,$0b,$3a,$5f
.byt $5f,$84,$0e,$32,$04,$32,$05,$32
.byt $04,$2f,$04,$2f,$05,$2f,$ff
ptn2f =*
.byt $03,$1a,$03,$1a,$03
.byt $24,$03,$26,$03,$1a,$03,$1a,$03
.byt $18,$03,$19,$03,$1a,$03,$1a,$03
.byt $24,$03,$26,$03,$1a,$03,$1a,$03
.byt $18,$03,$19,$03,$18,$03,$18,$03
.byt $22,$03,$24,$03,$18,$03,$18,$03
.byt $16,$03,$17,$03,$18,$03,$18,$03
.byt $22,$03,$24,$03,$18,$03,$18,$03
.byt $16,$03,$17,$03,$13,$03,$13,$03
.byt $1d,$03,$1f,$03,$13,$03,$13,$03
.byt $1d,$03,$1e,$03,$13,$03,$13,$03
.byt $1d,$03,$1f,$03,$13,$03,$13,$03
.byt $1d,$03,$1e,$03,$1a,$03,$1a,$03
.byt $24,$03,$26,$03,$1a,$03,$1a,$03
.byt $18,$03,$19,$03,$1a,$03,$1a,$03
.byt $24,$03,$26,$03,$1a,$03,$1a,$03
.byt $18,$03,$19,$ff
;====================================
;instruments
;====================================
instr =*
.byt $80,$09,$41,$48,$60,$03,$81,$00
.byt $00,$08,$81,$02,$08,$00,$00,$01
.byt $a0,$02,$41,$09,$80,$00,$00,$00
.byt $00,$02,$81,$09,$09,$00,$00,$05
.byt $00,$08,$41,$08,$50,$02,$00,$04
.byt $00,$01,$41,$3f,$c0,$02,$00,$00
.byt $00,$08,$41,$04,$40,$02,$00,$00
.byt $00,$08,$41,$09,$00,$02,$00,$00
.byt $00,$09,$41,$09,$70,$02,$5f,$04
.byt $00,$09,$41,$4a,$69,$02,$81,$00
.byt $00,$09,$41,$40,$6f,$00,$81,$02
.byt $80,$07,$81,$0a,$0a,$00,$00,$01
.byt $00,$09,$41,$3f,$ff,$01,$e7,$02
.byt $00,$08,$41,$90,$f0,$01,$e8,$02
.byt $00,$08,$41,$06,$0a,$00,$00,$01
.byt $00,$09,$41,$19,$70,$02,$a8,$00
.byt $00,$02,$41,$09,$90,$02,$00,$00
.byt $00,$00,$11,$0a,$fa,$00,$00,$05
.byt $00,$08,$41,$37,$40,$02,$00,$00
.byt $00,$08,$11,$07,$70,$02,$00,$00
.end
=============================================================================
by Craig Bruce (csbruce@neumann.uwaterloo.ca)
1. INTRODUCTION
This article is a continuation of the Little Red Reader article from last
issue. The program has been extended to write MS-DOS files, in addition to
reading them. The program still works drive-to-drive so you'll still need two
disk drives (either physical or logical) to use it. The program has also been
extended to allow MS-DOS files to be deleted and to allow the copying of
Commodore-DOS files between CBM-DOS disks (this makes it more convenient to
use the program with a temporary logical drive like RAMDOS). Also, since I
have recently acquired a CMD FD-4000 floppy disk drive, I know that this
program works with MS-DOS disks with this drive (but only for the 720K
format).
The program still has the same organization as last time: a menu-oriented
user-interface program written in BASIC that makes use of a package of MS-DOS
disk accessing routines written in machine language. Oh, this program is
Public Domain Software, so feel free to distribute and/or mangle it as you
wish. Just note any manglings on the "initializing" screen so people don't
blame me.
The program runs on either the 40 or 80-column screens, but you will get
much better performance from the BASIC portion of the program by being
in 80-column mode and FAST mode. A modification that someone might want
to make would be to spread-out the display for the 80-column screen and add
color to the rather bland display.
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.
The system is designed to have two file selection menus: one for the MS-DOS
disk drive, and one for the Commodore-DOS disk drive (which may be a logical
disk drive). The idea for copying is that you select the files in one of
these menus, and then program knows to copy them to the disk for the other
menu. This idea of having two selection menus is also very consistent with
the original program.
2.1. MS-DOS MENU
When the program starts, the MS-DOS menu of the program is displayed. It
looks like:
MS-DOS MS=10:1581 CBM=8 FREE=715000
NUM S TRN TYP FILENAME EXT LENGTH
--- - --- --- -------- --- ------
1 * ASC SEQ HACK4 TXT 120732
2 BIN PRG RAMDOS SFX 34923
D=DIR M=MSDEV F=CBMDEV C=COPY Q=QUIT
T=TOGGLE R=REMOVE X=CBMCPY /=MENU +-=PG
except that immediately after starting up, "<directory not loaded>" will be
displayed rather than filenames. The menu looks and operates pretty much as
it did in the last issue of C= Hacking. The only differences are that the
number of bytes free on the drive are displayed (which is useful to know when
writing files) and there are some more commands.
The directory ("D"), change ms-dos device ("M"), change commodore file device
("F"), toggle column contents ("T"), copy ms-dos files to cbm-dos disk ("C"),
quit ("Q"), paging ("+" and "-"), column change (SPACE or RETURN), and the
cursor movement commands all work the same as before. They are all sticks to
use to flog the beast into submission. The new commands are: "R" (remove ==
delete), "/" (change menu), and "X" (copy CBM files == "Xerox").
The remove command is used to delete selected files from the MS-DOS disk.
After selecting this option, you will get an annoying "are you sure" question
and the the selected files will quickly disappear and the changes will finally
be written to disk. Deleting a batch of MS-DOS files is much quicker than
deleting Commodore-DOS files since MS-DOS disks use a File Allocation Table
rather than the linked list of blocks organization that CBM uses. In order to
make the BASIC program execute quicker, after deleting, the original order of
the filenames in the directory listing will be changed. Be forewarned that
the delete operation is non-recoverable.
The change menu command is used to move back and forth between the Commodore-
DOS and MS-DOS menus.
2.2. COMMODORE-DOS MENU
The Commodore-DOS menu, which displays the names of the Commodore files
selected for various operations, looks and works pretty much the same as
the MS-DOS menu:
CBMDOS MS=10:1581 CBM=8 FREE=3211476
NUM S TRN FILENAME T LENGTH
--- - --- ---------------- - ------
1 * BIN LRR-128 P 9876
2 ASC COM-HACKING-005 S 175412
D=DIR M=MSDEV F=CBMDEV C=COPY Q=QUIT
T=TOGGLE R=REMOVE X=CBMCPY /=MENU +-=PG
You'll notice, however, that the filetype field ("T" here) is moved and is
unchangable. Also, the file lengths are not exact; they are reported as the
block count of the file multiplied by 254. This menu is not maintained for
files being copied to the CBM-DOS disk from an MS-DOS disk. You'll
have to re-execute the Directory instruction to get an updated listing.
The "D" (directory) command has local effect when in this menu. The
Commodore-DOS directory will be loaded from the current CBM device number.
Note that in order for this to work, the CBM device must be number eight
or greater (a disk drive). Originally, the subroutine for this command was
written using only GET#'s from the disk and was very slow. It was modified,
however, to call a machine language subroutine to read the information for
a directory entry from the directory listing, and hence the subroutine now
operates at a tolerable speed.
The "C" (copy) command also has a different meaning when in this menu. It
means to copy the selected CBM files to the MS-DOS disk. See details below.
The copy CBM files ("X") command is used to copy the files in the CBM-DOS menu
to another CBM-DOS disk unit. Select the files you want to copy and then
press X. You will then be asked what device number you want to copy the files
to. The device can be another disk drive or any other device (except the
keyboard). Using device number 0 does not mean the "null" device as it does
with copying MS-DOS to CBM. If you are copying to a disk device and the file
already exists, then you will be asked if you wish to overwrite the file. You
cannot copy to the same disk unit. Also, all files are copied in binary mode
(regardless of what translation you have selected for a file).
The copy CBM files command was included since all of the low-level gear
needed to implement it (specifically "commieIn" and "commieOut" below) was
also required by other functions. This command can be very convenient when
working with RAMDOS. For example, if you only had a 1571 as device 8 but you
have a RAM expander and have installed RAMDOS as device 9, then you would
copy MS-DOS files to RAMDOS using the MS-DOS menu, and then you would go to
the Commodore-DOS menu ("/"), read the directory, select all files, insert an
Commodore-DOS diskette into your 1571, and then use "X" to copy from the
RAMDOS device to the 1571.
The remove command ("R") does not work for this directory. You can SCRATCH
your CBM-DOS files your damn self.
2.3. COPY CBM-DOS TO MS-DOS
Before you can copy selected CBM-DOS files to an MS-DOS disk, the MS-DOS disk
directory must be already loaded (from the MS-DOS menu). This is required
since the directory and FAT information are kept in memory at all times during
the execution of this program.
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). Please
note that not a whole lot of effort was put into error recovery.
To generate an MS-DOS filename from an CBM-DOS filename, the following
algorithm is used. The filename is searched from right to left for the last
"." character. If there is no "." character, then the entire filename, up to
11 characters, is used as the MS-DOS filename. Characters 9 to 11 will be
used as the extension. If there is a "." character, the all characters before
it, up to eight, will be used as the MS-DOS filename and all characters after
the final ".", up to three, will be used as the MS-DOS extension.
Then, the newly generated MS-DOS filename is scanned for any extra "."
characters or embedded spaces. If any are found, they are replaced by the
underscore character ("_", which is the backarrow character on a Commodore
display). Finally, all trailing underscores are removed from the end of both
the filename and extension portions of the MS-DOS filename. Also, all
characters are converted to lowercase PETSCII (which is uppercase ASCII) when
they are copied into the MS-DOS filename. Note that if the Commodore filename
is not in the 8/3 format of MS-DOS, then something in the name may be lost.
Some examples of filename conversion follow:
CBM-DOS FILENAME MS-DOS FILENAME
---------------- ---------------
"lrr.bin" "lrr.bin"
"lrr.128.bin" "lrr_128.bin"
"hello there.text" "hello_th.tex"
"long_filename" "long_fil.ena"
"file 1..3.s__5" "file_1.s"
It would have been time-consuming to have the program scan the MS-DOS
directory for a filename already existing on the disk, so LRR will put
multiple files on a disk with the same filename without complaining. This
also gets rid of the problem of asking you if you want to overwrite the old
file or generate a new name. However, in order to retrieve the file from
disk on an MS-DOS machine, you will probably have to use the RENAME command to
rename the first versions of the file on the disk to something else so MS-DOS
will scan further in the directory for the last version of the file with the
same filename. There is no rename command in LRR because I never thought of
it in time. It would have been fairly easy to put in.
The date generated for a new MS-DOS file will be all zeros. Some systems
interpret this as 12:00 am, 01-Jan-80 and others don't display a date at all
for this value.
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 the blinking lights and listening for clicks and grinds.
Since the FAT and directory are maintained in RAM during the entire copying
process and are only flushed to disk after the entire batch of files are
copied, copying is made more efficient, since there will be no costly seek
back to track 0 after writing each file (like MS-DOS does). If you have a
number of small files to copy, then they will be knocked off in quick
succession, faster than many MS-DOS machines will copy them.
To simplify the implementation, the current track of disk blocks for writing
is not maintained like it is for reading. Also, a writing interleave of 1:1
is used for a 1571, which is not optimal. However, since writing is such a
slow operation anyway, and since the 1571 is particularly bad by insisting on
verifying blocks, not much more overhead is introduced than is already
present.
An interesting note about writing MS-DOS disks is that you can terminate LRR
in the middle of a copy (with STOP+RESTORE) or in the middle of copying a
batch of files, and the MS-DOS disk will remain in a perfectly consistent
state afterwards. The state will be as if none of the files were copied. The
reason is that the control information (the FAT and directory) is maintained
internally and is flushed only after copying is all completed. But don't
terminate LRR while it is flushing the control information.
Here is a table of copying speeds for copying to 1571, 1581, and CMD FD-4000
disk units with ASC and BIN translation modes. All figures are in bytes/
second, which includes both reading the byte from a C= disk and writing it to
the MS-DOS disk. The average speed for either the read or write operation
individually will be twice the speed given below. These results were obtained
from copying a 156,273 byte text file (the text of C= Hacking Issue #4).
FROM \ TO: FD-bin FD-asc 81-bin 81-asc 71-bin 71-asc
--------+ ------ ------ ------ ------ ------ ------
RAMLink | 2,332 2,200 2,332 2,200 1,594 1,559
RAMDOS | 1,070 1,053 1,604 1,600 1,561 1,510
FD4000 | - - 1,645 1,597 1,499 1,464
JD1581 | 1,662 1,619 - - 1,474 1,440
JD1571 | 1,050 1,024 953 933 - -
These figures are for transfer speed only, not counting the couple of seconds
of opening files and flushing the directory. Note that all my physical drives
are JiffyDOS-ified, so your performance may be slower. I am at a loss to
explain why an FD-4000 is so much slower than a 1581 for copying from a
RAMDOS file, but the same speed or better for copying from anything else.
Since I don't have access to an actual MS-DOS machine, I have not tested the
files written onto an MS-DOS disk by LRR, except by reading them back with LRR
and BBR. I do know, however, that earlier encarnations of this program did
work fine with MS-DOS machines.
3. MS-DOS ROOT DIRECTORY
It was brought to my attention that I made a mistake in the pervious article.
I was wrong about the offset of the attributes field in a directory entry.
The layout should have been as follows:
OFFSET LEN DESCRIPTION
------ --- -----------
0..7 8 Filename
8..10 3 Extension
11 1 Attributes: $10=Directory, $08=VolumeId
12..21 10 <unused>
22..25 4 Date
26..27 2 Starting FAT entry number
28..31 4 File length in bytes
4. FILE COPYING PACKAGE
As was mentioned above, Little Red Reader is split into two pieces: a BASIC
front-end user interface program and a package of machine language subroutines
for disk accessing. The BASIC program handles the menu, user interaction, and
most of the MS-DOS directory searching/modifying. The machine language
package handles the hardware input/output, File Allocation Table and file
structure manipulations.
The file copying package 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.
This section of the article is presented in its entirety, including all of the
information given last time.
4.1. INTERFACE
The subroutine call interface to the file copying package is summarized as
follows:
ADDRESS DESCRIPTION
------- -----------
PK initPackage subroutine
PK+3 msDir (load MS-DOS directory/FAT) subroutine
PK+6 msRead (copy MS-DOS to CBM-DOS) subroutine
PK+9 msWrite (copy CBM-DOS to MS-DOS) subroutine
PK+12 msFlush subroutine
PK+15 msDelete subroutine
PK+18 msFormat subroutine [not implemented]
PK+21 msBytesFree subroutine
PK+24 cbmCopy (copy CBM-DOS to CBM-DOS) subroutine
PK+27 cbmDirent (read CBM-DOS directory entry) subroutine
where "PK" is the load address of the package ($8000).
The parameter passing interface is summarized as follows:
ADDRESS DESCRIPTION
------- -----------
PV two-byte package identification number ($CB, 132)
PV+2 errno : error code returned
PV+3 MS-DOS device number (8 to 30)
PV+4 MS-DOS device type ($00=1571, $FF=1581)
PV+5 two-byte starting cluster number for file copying
PV+7 low and mid bytes of file length for copying
PV+9 pointer to MS-DOS directory entry for writing
PV+11 CBM-DOS file block count
PV+13 CBM-DOS file type ("S"=seq, "P"=prg, etc.)
PV+14 CBM-DOS filename length
PV+15 CBM-DOS filename characters (max 16 chars)
Where "PV" is equal to PK+30. Additional subroutine parameters are passed in
the processor registers.
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
132.
4.1.1. INIT_PACKAGE SUBROUTINE
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.
4.1.2. MS_DIR SUBROUTINE
The "msDir" 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.
4.1.3. MS_READ SUBROUTINE
The "msRead" 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 PV+5 and PV+7 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.
4.1.4. MS_WRITE SUBROUTINE
The "msWrite" subroutine copies a single file from a specified CBM-Kernal
logical file number to a MS-DOS file. The MS-DOS device number and type are
set above. A pointer to the MS-DOS directory entry in the buffer returned by
the "msDir" call must be given in interface word PV+9 and the translation mode
and CBM lfn are passed in the .A and .X registers as in the "msRead" routine.
An error return is given in the usual way (.CS, errno). Otherwise, there are
no return values.
It is the responsibility of the calling program to initialize the MS-DOS
directory entry to all zeros and then set the filename and set the starting
cluster pointer to $0FFF. This routine will update the starting cluster and
file length fields of the directory entry when it finishes. The internal
"dirty flags" are modified so that the directory and FAT will be flushed on
the next call to "msFlush".
4.1.5. MS_FLUSH SUBROUTINE
The "msFlush" subroutine takes no input parameters other than the implicit
msDevice and msType. If "dirty" (modified), the FAT will be written to the
MS-DOS disk, to both physical replicas of the disk FAT. Then, each directory
sector that is dirty will be written to disk. After flushing, the internal
dirty flags will be cleared. An error return is given in the usual way.
There are no other output parameters. If you call this routine and there are
no dirty flags set, then it will return immediately, without any writing to
disk.
4.1.6. MS_DELETE SUBROUTINE
The "msDelete" subroutine will deallocate all File Allocation Table entries
(and hence, data clusters) allocated to a file and mark the directory entry as
being deleted (by putting an $E5 into the first character of the filename).
The file is specified by giving the pointer to the directory entry in
interface word at PV+9. After deallocating the file data, the internal
"dirty" flag will be set for the FAT and the sector that the directory entry
is on, but nothing will be written to disk. There is no error return from
this routine. It is the responsibility of the calling routine to eventually
call the "msFlush" routine.
4.1.7. MS_FORMAT SUBROUTINE
The "msFormat" subroutine is not implemented. It's intended function was to
format the MS-DOS disk and generate and write the boot sector, initial FAT,
and initial directory entry data.
4.1.8. MS_BYTES_FREE SUBROUTINE
The "msBytesFree" subroutine will scan the currently loaded MS-DOS File
Allocation Table, count the number of clusters free, and return the number of
bytes free for file storage on the disk. There are no input parameters and
the bytes free are returned in the .AYX registers (.A=low, .Y=mid, .X=high
byte). The subroutine has no error returns and does not check if an MS-DOS
directory is actually loaded.
4.1.9. CBM_COPY SUBROUTINE
The "cbmCopy" subroutine will copy from an input CBM-Kernal logical file
number given in the .A register to an output CBM-Kernal lfn given in the .X
register, in up to 1024 byte chunks. File contents are copied exactly (no
translation). This routine does not care if the lfn's are on the same device
or not, but the input device must be a disk unit (either logical or physical).
An error return is given in the usual way.
4.1.10. CBM_DIRENT SUBROUTINE
The "cbmDirent" subroutine reads the next directory entry from the CBM-Kernal
lfn given in .A and puts the data into interface variables. Of course, the
lfn is assumed to be open for reading a directory ("$"). The block count is
returned in the word at PV+11, the first character of the filetype is returned
at PV+13, the number of characters in the filename is returned in PV+14, and
the filename characters are returned in bytes PV+15 to PV+30. An error return
is given in the usual way.
This routine assumes that the first two bytes of the directory file have
already been read. The first call to this routine will return the name of the
disk. The end of a directory is signalled by a filename length of zero. In
this case, the block count returned will be the number of blocks free on the
disk.
4.2. IMPLEMENTATION
This section presents the code that implements the MS-DOS file reading and
writing package. It is here in a special form; each code line is preceded by
the % symbol. The % sign is 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):
grep '^%' Hack5 | sed 's/^% //' | sed 's/^%//' >lrr.s
% ; Little Red Reader/Writer utility package by Craig Bruce, 31-Jan-92
% ; Written for C= Hacking Net-Magazine; for C-128, 1571, 1581
%
The code is written for the Buddy assembler and here are a couple setup
directives. Note that my comments come before the section of code.
% .org $8000
% .obj "lrr.bin"
%
% ;====jump table and parameters interface ====
%
% jmp initPackage ;()
% jmp msDir ;( msDevice, msType ) : .AY=dirAddr, .X=direntCount
% jmp msRead ;( msDevice, msType, startCluster, lenML,.A=trans,.X=cbmLfn )
% jmp msWrite ;( msDevice, msType, writeDirent, .A=trans, .X=cbmLfn )
% jmp msFlush ;( msDevice, msType )
% jmp msDelete ;( writeDirent )
% jmp msFormat ;( msDevice, msType )
% jmp msBytesFree ;( ) : .AYX=bytesFree
% jmp cbmCopy ;( .A=inLfn, .X=outLfn )
% jmp cbmDirent ;( .A=lfn )
%
% .byte $cb,132 ;identification (location pk+30)
These interface variables are included in the package program space to
minimize unwanted interaction with other programs loaded at the same time,
such as the RAMDOS device driver.
% errno .buf 1
% msDevice .buf 1
% msType .buf 1 ;$00=1571, $ff=1581
% startCluster .buf 2
% lenML .buf 2 ;length medium and low bytes
% writeDirent .buf 2 ;pointer to dirent
% cdirBlocks .buf 2 ;cbm dirent blocks
% cdirType .buf 1 ;cbm dirent filetype
% cdirFlen .buf 1 ;cbm dirent filename length
% cdirName .buf 16 ;cbm dirent filename
%
This command is not currently implemented. Its stub appears here.
% msFormat = *
% brk
%
% ;====global declaraions====
%
% kernelListen = $ffb1
% kernelSecond = $ff93
% kernelUnlsn = $ffae
% kernelAcptr = $ffa2
% kernelCiout = $ffa8
% kernelSpinp = $ff47
% kernelChkin = $ffc6
% kernelChkout = $ffc9
% kernelClrchn = $ffcc
% kernelChrin = $ffcf
% kernelChrout = $ffd2
%
% st = $90
% ciaClock = $dd00
% ciaFlags = $dc0d
% ciaData = $dc0c
%
These are the parameters and derived parameters from the boot sector. They
are kept in the program space to avoid interactions.
% clusterBlockCount .buf 1 ;1 or 2
% fatBlocks .buf 1 ;up to 3
% rootDirBlocks .buf 1 ;up to 8
% rootDirEntries .buf 1 ;up to 128
% totalSectors .buf 2 ;up to 1440
% firstFileBlock .buf 1
% firstRootDirBlock .buf 1
% fileClusterCount .buf 2
% lastFatEntry .buf 2
%
The cylinder (track) and side that is currently stored in the track cache
for reading.
% bufCylinder .buf 1
% bufSide .buf 1
These "dirty" flags record what has to be written out for a flush operation.
% fatDirty .buf 1
% dirDirty .buf 8 ;flag for each directory block
% formatParms .buf 6
%
This package is split into a number of levels. This level interfaces with the
Kernal serial bus routines and the burst command protocol of the disk drives.
% ;====hardware level====
%
Connect to the MS-DOS device and send the "U0" burst command prefix and the
burst command byte.
% sendU0 = * ;( .A=burstCommandCode ) : .CS=err
% pha
% lda #0
% sta st
% lda msDevice
% jsr kernelListen
% lda #$6f
% jsr kernelSecond
% lda #"u"
% jsr kernelCiout
% bit st
% bmi sendU0Error
% lda #"0"
% jsr kernelCiout
% pla
% jsr kernelCiout
% bit st
% bmi sendU0Error
% clc
% rts
%
% sendU0Error = *
% lda #5
% sta errno
% sec
% rts
%
Toggle the "Data Accepted / Ready For More" clock signal for the burst
transfer protocol.
% toggleClock = *
% lda ciaClock
% eor #$10
% sta ciaClock
% rts
%
Wait for a burst byte to arrive in the serial data register of CIA#1 from the
fast serial bus.
% serialWait = *
% lda #$08
% - bit ciaFlags
% beq -
% rts
%
Wait for and get a burst byte from the fast serial bus, and send the "Data
Accepted" signal.
% getBurstByte = *
% jsr serialWait
% ldx ciaData
% jsr toggleClock
% txa
% rts
%
Send the burst commands to "log in" the MS-DOS disk and set the Read sector
interleave factor.
% mountDisk = * ;() : .CS=err
% lda #%00011010
% jsr sendU0
% bcc +
% rts
% + jsr kernelUnlsn
% bit st
% bmi sendU0Error
% clc
% jsr kernelSpinp
% bit ciaFlags
% jsr toggleClock
% jsr getBurstByte
% sta errno
% and #$0f
% cmp #2
% bcs mountExit
Grab the throw-away parameters from the mount operation.
% ldy #0
% - jsr getBurstByte
% sta formatParms,y
% iny
% cpy #6
% bcc -
% clc
Set the Read sector interleave to 1 for a 1581 or 4 for a 1571.
% ;** set interleave
% lda #%00001000
% jsr sendU0
% bcc +
% rts
% + lda #1 ;interleave of 1 for 1581
% bit msType
% bmi +
% lda #4 ;interleave of 4 for 1571
% + jsr kernelCiout
% jsr kernelUnlsn
% mountExit = *
% rts
%
Read all of the sectors of a given track into the track cache.
% bufptr = 2
% secnum = 4
%
% readTrack = * ;( .A=cylinder, .X=side ) : trackbuf, .CS=err
% pha
% txa
Get the side and put it into the command byte. Remember that we have to flip
the side bit for a 1581.
% and #$01
% asl
% asl
% asl
% asl
% bit msType
% bpl +
% eor #$10
% + jsr sendU0
% pla
% bcc +
% rts
% + jsr kernelCiout ;cylinder number
% lda #1 ;start sector number
% jsr kernelCiout
% lda #9 ;sector count
% jsr kernelCiout
% jsr kernelUnlsn
Prepare to receive the track data.
% sei
% clc
% jsr kernelSpinp
% bit ciaFlags
% jsr toggleClock
% lda #<trackbuf
% ldy #>trackbuf
% sta bufptr
% sty bufptr+1
Get the sector data for each of the 9 sectors of the track.
% lda #0
% sta secnum
% - bit msType
% bmi +
If we are dealing with a 1571, we have to set the buffer pointer for the next
sector, taking into account the soft interleave of 4.
% jsr get1571BufPtr
% + jsr readSector
% bcs trackExit
% inc secnum
% lda secnum
% cmp #9
% bcc -
% clc
% trackExit = *
% cli
% rts
%
Get the buffer pointer for the next 1571 sector.
% get1571BufPtr = *
% lda #<trackbuf
% sta bufptr
% ldx secnum
% clc
% lda #>trackbuf
% adc bufptr1571,x
% sta bufptr+1
% rts
%
% bufptr1571 = *
% .byte 0,8,16,6,14,4,12,2,10
%
Read an individual sector into memory at the specified address.
% readSector = * ;( bufptr ) : .CS=err
Get and check the burst status byte for errors.
% jsr getBurstByte
% sta errno
% and #$0f
% cmp #2
% bcc +
% rts
% + ldx #2
% ldy #0
%
Receive the 512 sector data bytes into memory.
% readByte = *
% lda #$08
% - bit ciaFlags
% beq -
% lda ciaClock
% eor #$10
% sta ciaClock
% lda ciaData
% sta (bufptr),y
% iny
% bne readByte
% inc bufptr+1
% dex
% bne readByte
% rts
%
% oldClock = 5
%
Write an individual sector to disk, from a specified memory address.
% writeSector = * ;( bufptr, .A=track, .X=side, .Y=sector ) : .CS=err
% pha
% sty secnum
Get the side into the burst command byte
% txa
% and #$01
% asl
% asl
% asl
% asl
% ora #$02
% bit msType
% bpl +
% eor #$10
% + jsr sendU0
% pla
% bcc +
% rts
Send rest of parameters for burst command.
% + jsr kernelCiout ;track number
% lda secnum ;sector number
% jsr kernelCiout
% lda #1 ;sector count
% jsr kernelCiout
% jsr kernelUnlsn
% sei
% lda #$40
% sta oldClock
% sec
% jsr kernelSpinp ;set for burst output
% sei
% bit ciaFlags
% ldx #2
% ldy #0
%
Write the 512 bytes for the sector.
% writeByte = *
% lda ciaClock
% cmp ciaClock
% bne writeByte
% eor oldClock
% and #$40
% beq writeByte
% lda (bufptr),y
% sta ciaData
% lda oldClock
% eor #$40
% sta oldClock
% lda #8
% - bit ciaFlags
% beq -
% iny
% bne writeByte
% inc bufptr+1
% dex
% bne writeByte
%
Read back the burst status byte to see if anything went wrong with the write.
% clc
% jsr kernelSpinp
% bit ciaFlags
% jsr toggleClock
% jsr serialWait
% ldx ciaData
% jsr toggleClock
% txa
% sta errno
% and #$0f
% cmp #2
% cli
% rts
%
This next level of routines deals with logical sectors and the track cache
rather than with hardware.
% ;====logical sector level====
%
Invalidate the track cache if the MS-DOS drive number is changed or if a new
disk is inserted. This routine has to establish a RAM configuration of $0E
since it will be called from RAM0. Configuration $0E gives RAM0 from $0000 to
$BFFF, Kernal ROM from $C000 to $FFFF, and the I/O space over the Kernal from
$D000 to $DFFF. This configuration is set by all application interface
subroutines.
% initPackage = *
% lda #$0e
% sta $ff00
% lda #$ff
% sta bufCylinder
% sta bufSide
% ldx #7
% - sta dirDirty,x
% dex
% bpl -
% sta fatDirty
% clc
% rts
%
Locate a sector (block) in the track cache, or read the corresponding physical
track into the track cache if necessary. This routine accepts the cylinder,
side, and sector numbers of the block.
% sectorSave = 5
%
% readBlock = * ;( .A=cylinder,.X=side,.Y=sector ) : .AY=blkPtr,.CS=err
Check if the correct track is in the track cache.
% cmp bufCylinder
% bne readBlockPhysical
% cpx bufSide
% bne readBlockPhysical
If so, then locate the sector's address and return that.
% dey
% tya
% asl
% clc
% adc #>trackbuf
% tay
% lda #<trackbuf
% clc
% rts
%
Here, we have to read the physical track into the track cache. We save the
input parameters and call the hardware-level track-reading routine.
% readBlockPhysical = *
% sta bufCylinder
% stx bufSide
% sty sectorSave
% jsr readTrack
Check for errors.
% bcc readBlockPhysicalOk
% lda errno
% and #$0f
% cmp #11 ;disk change
% beq +
% sec
% rts
If the error that happened is a "Disk Change" error, then mount the disk and
try to read the physical track again.
% + jsr mountDisk
% lda bufCylinder
% ldx bufSide
% ldy sectorSave
% bcc readBlockPhysical
% rts
%
Here, the physical track has been read into the track cache ok, so we recover
the original input parameters and try the top of the routine again.
% readBlockPhysicalOk = *
% lda bufCylinder
% ldx bufSide
% ldy sectorSave
% jmp readBlock
%
Divide the given number by 18. This is needed for the calculations to convert
a logical sector number to the corresponding physical cylinder, side, and
sector numbers that the lower-level routines require. The method of repeated
subtraction is used. This routine would probably work faster if we tried to
repeatedly subtract 360 (18*20) at the top, but I didn't bother.
% divideBy18 = * ;( .AY=number ) : .A=quotient, .Y=remainder
% ;** could repeatedly subtract 360 here
% ldx #$ff
% - inx
% sec
% sbc #18
% bcs -
% dey
% bpl -
% clc
% adc #18
% iny
% tay
% txa
% rts
%
Convert the given logical block number to the corresponding physical cylinder,
side, and sector numbers. This routine follows the formulae given in the
previous article with a few simplifying tricks.
% convertLogicalBlockNum = * ;( .AY=blockNum ) : .A=cyl, .X=side,.Y=sec
% jsr divideBy18
% ldx #0
% cpy #9
% bcc +
% pha
% tya
% sbc #9
% tay
% pla
% ldx #1
% + iny
% rts
%
Copy a sequential group of logical sectors into memory. This routine is used
by the directory loading routine to load the FAT and Root Directory, and is
used by the cluster reading routine to retrieve all of the blocks of a
cluster. After the given starting logical sector number is converted into its
physical cylinder, side, and sector equivalent, the physical values are
incremented to get the address of successive sectors of the group. This
avoids the overhead of the logical to physical conversion. Quite a number of
temporaries are needed.
% destPtr = 6
% curCylinder = 8
% curSide = 9
% curSector = 10
% blockCountdown = 11
% sourcePtr = 12
%
% copyBlocks = * ;( .AY=startBlock, .X=blockCount, ($6)=dest ) : .CS=err
% stx blockCountdown
% jsr convertLogicalBlockNum
% sta curCylinder
% stx curSide
% sty curSector
%
% copyBlockLoop = *
% lda curCylinder
% ldx curSide
% ldy curSector
% jsr readBlock
% bcc +
% rts
% + sta sourcePtr
% sty sourcePtr+1
% ldx #2
% ldy #0
Here I unroll the copying loop a little bit to cut the overhead of the branch
instruction in half. (A cycle saved... you know).
% - lda (sourcePtr),y
% sta (destPtr),y
% iny
% lda (sourcePtr),y
% sta (destPtr),y
% iny
% bne -
% inc sourcePtr+1
% inc destPtr+1
% dex
% bne -
Increment the cylinder, side, sector values.
% inc curSector
% lda curSector
% cmp #10
% bcc +
% lda #1
% sta curSector
% inc curSide
% lda curSide
% cmp #2
% bcc +
% lda #0
% sta curSide
% inc curCylinder
% + dec blockCountdown
% bne copyBlockLoop
% clc
% rts
%
Convert a given cluster number into the first corresponding logical block
number.
% convertClusterNum = * ;( .AY=clusterNum ) : .AY=logicalBlockNum
% sec
% sbc #2
% bcs +
% dey
% + ldx clusterBlockCount
% cpx #1
% beq +
% asl
% sty 7
% rol 7
% ldy 7
% + clc
% adc firstFileBlock
% bcc +
% iny
% + rts
%
Read a cluster into the Cluster Buffer, given the cluster number. The cluster
number is converted to a logical sector number and then the sector copying
routine is called. The formula given in the previous article is used.
% readCluster = * ;( .AY=clusterNumber ) : clusterBuf, .CS=err
% jsr convertClusterNum
%
% ;** read logical blocks comprising cluster
% ldx #<clusterBuf
% stx 6
% ldx #>clusterBuf
% stx 7
% ldx clusterBlockCount
% jmp copyBlocks
%
Write a logical block out to disk. The real purpose of this routine is to
invalidate the read-track cache if the block to be written is contained in
the cache.
% writeLogicalBlock = * ;( .AY=logicalBlockNumber, bufptr ) : .CS=err
% jsr convertLogicalBlockNum
% cmp bufCylinder
% bne +
% cpx bufSide
% bne +
% pha
% lda #$ff
% sta bufCylinder
% sta bufSide
% pla
% + jsr writeSector
% rts
%
% writeClusterSave .buf 2
%
Write a cluster-ful of data out to disk from the cluster buffer. This routine
simply calls the write logical block routine once or twice, depending on the
cluster size of the disk involved.
% writeCluster = * ;( .AY=clusterNumber, clusterBuf ) : .CS=err
% jsr convertClusterNum
% ldx #<clusterBuf
% stx bufptr
% ldx #>clusterBuf
% stx bufptr+1
% sta writeClusterSave
% sty writeClusterSave+1
% jsr writeLogicalBlock
% bcc +
% rts
% + lda clusterBlockCount
% cmp #2
% bcs +
% rts
% + lda writeClusterSave
% ldy writeClusterSave+1
% clc
% adc #1
% bcc +
% iny
% + jsr writeLogicalBlock
% rts
%
This next level of routines deal with the data structures of the MS-DOS disk
format.
% ;====MS-DOS format level====
%
% bootBlock = 2
%
Read the disk format parameters, directory, and FAT into memory.
% msDir = * ;( ) : .AY=dirbuf, .X=dirEntries, .CS=err
% lda #$0e
% sta $ff00
%
Read the boot sector and extract the parameters.
% ;** get parameters from boot sector
% lda #0
% ldy #0
% jsr convertLogicalBlockNum
% jsr readBlock
% bcc +
% rts
% + sta bootBlock
% sty bootBlock+1
% ldy #13 ;get cluster size
% lda (bootBlock),y
% sta clusterBlockCount
% cmp #3
% bcc +
%
If a disk parameter is found to exceed the limits of LRR, error code #60 is
returned.
% invalidParms = *
% lda #60
% sta errno
% sec
% rts
%
% + ldy #16 ;check FAT replication count, must be 2
% lda (bootBlock),y
% cmp #2
% bne invalidParms
% ldy #22 ;get FAT size in sectors
% lda (bootBlock),y
% sta fatBlocks
% cmp #4
% bcs invalidParms
% ldy #17 ;get directory size
% lda (bootBlock),y
% sta rootDirEntries
% cmp #129
% bcs invalidParms
% lsr
% lsr
% lsr
% lsr
% sta rootDirBlocks
% ldy #19 ;get total sector count
% lda (bootBlock),y
% sta totalSectors
% iny
% lda (bootBlock),y
% sta totalSectors+1
% ldy #24 ;check sectors per track, must be 9
% lda (bootBlock),y
% cmp #9
% bne invalidParms
% ldy #26
% lda (bootBlock),y
% cmp #2 ;check number of sides, must be 2
% bne invalidParms
% ldy #14 ;check number of boot sectors, must be 1
% lda (bootBlock),y
% cmp #1
% bne invalidParms
%
Calculate the derived parameters.
% ;** get derived parameters
% lda fatBlocks ;first root directory sector
% asl
% clc
% adc #1
% sta firstRootDirBlock
% clc ;first file sector
% adc rootDirBlocks
% sta firstFileBlock
% lda totalSectors ;number of file clusters
% ldy totalSectors+1
% sec
% sbc firstFileBlock
% bcs +
% dey
% + sta fileClusterCount
% sty fileClusterCount+1
% lda clusterBlockCount
% cmp #2
% bne +
% lsr fileClusterCount+1
% ror fileClusterCount
% + clc
% lda fileClusterCount
% adc #2
% sta lastFatEntry
% lda fileClusterCount+1
% adc #0
% sta lastFatEntry+1
%
% ;** load FAT
% lda #<fatbuf
% ldy #>fatbuf
% sta 6
% sty 7
% lda #1
% ldy #0
% ldx fatBlocks
% jsr copyBlocks
% bcc +
% rts
%
% ;** load actual directory
% + lda #<dirbuf
% ldy #>dirbuf
% sta 6
% sty 7
% lda firstRootDirBlock
% ldy #0
% ldx rootDirBlocks
% jsr copyBlocks
% bcc +
% rts
% + lda #<dirbuf
% ldy #>dirbuf
% ldx rootDirEntries
% clc
% rts
%
This routine locates the given FAT table entry number and returns the value
stored in it. Some work is needed to deal with the 12-bit compressed data
structure.
% entryAddr = 2
% entryWork = 4
% entryBits = 5
% entryData0 = 6
% entryData1 = 7
% entryData2 = 8
%
% locateFatEntry = * ;( .AY=fatEntryNumber ) : entryAddr, entryBits%1
Divide the FAT entry number by two and multiply by three because two FAT
entries are stored in three bytes. Then add the FAT base address and we have
the address of the three bytes that contain the FAT entry we are interested
in. I retrieve the three bytes into zero-page memory for easy manipulation.
% sta entryBits
% ;** divide by two
% sty entryAddr+1
% lsr entryAddr+1
% ror
%
% ;** times three
% sta entryWork
% ldx entryAddr+1
% asl
% rol entryAddr+1
% clc
% adc entryWork
% sta entryAddr
% txa
% adc entryAddr+1
% sta entryAddr+1
%
% ;** add base, get data
% clc
% lda entryAddr
% adc #<fatbuf
% sta entryAddr
% lda entryAddr+1
% adc #>fatbuf
% sta entryAddr+1
% ldy #2
% - lda (entryAddr),y
% sta entryData0,y
% dey
% bpl -
% rts
%
% getFatEntry = * ;( .AY=fatEntryNumber ) : .AY=fatEntryValue
% jsr locateFatEntry
% lda entryBits
% and #1
% bne +
%
If the original given FAT entry number is even, then we want the first 12-bit
compressed field. The nybbles are extracted according to the diagram shown
earlier.
% ;** case 1: first 12-bit cluster
% lda entryData1
% and #$0f
% tay
% lda entryData0
% rts
%
Otherwise, we want the second 12-bit field.
% ;** case 2: second 12-bit cluster
% + lda entryData1
% ldx #4
% - lsr entryData2
% ror
% dex
% bne -
% ldy entryData2
% rts
%
% fatValue = 9
%
Change the value in a FAT entry. This routine is quite similar to the get
routine.
% setFatEntry = * ;( .AY=fatEntryNumber, (fatValue) )
% jsr locateFatEntry
% lda fatValue+1
% and #$0f
% sta fatValue+1
% lda entryBits
% and #1
% bne +
%
% ;** case 1: first 12-bit cluster
% lda fatValue
% sta entryData0
% lda entryData1
% and #$f0
% ora fatValue+1
% sta entryData1
% jmp setFatExit
%
% ;** case 2: second 12-bit cluster
% + ldx #4
% - asl fatValue
% rol fatValue+1
% dex
% bne -
% lda fatValue+1
% sta entryData2
% lda entryData1
% and #$0f
% ora fatValue
% sta entryData1
%
% setFatExit = *
% ldy #2
% - lda entryData0,y
% sta (entryAddr),y
% dey
% bpl -
% sty fatDirty
% rts
%
Mark the directory sector corresponding to the given directory entry as being
dirty so it will be written out to disk the next time the msFlush routine is
called.
% dirtyDirent = * ;( writeDirent )
% sec
% lda writeDirent
% sbc #<dirbuf
% lda writeDirent+1
% sbc #>dirbuf
% lsr
% and #$07
% tax
% lda #$ff
% sta dirDirty,x
% rts
%
% delCluster = 14
%
Delete the MS-DOS file whose directory entry is given. Put the $E5 into
its filename, get its starting cluster and follow the chain of clusters
allocated to the file in the FAT, marking them as unallocated (value $000)
as we go. Exit by marking the directory entry as "dirty".
% msDelete = * ;( writeDirent )
% ldy #$0e
% sty $ff00
% lda writeDirent
% ldy writeDirent+1
% sta 2
% sty 3
% lda #$e5
% ldy #0
% sta (2),y
% ldy #26
% lda (2),y
% sta delCluster
% iny
% lda (2),y
% sta delCluster+1
% - lda delCluster+1
% cmp #5
% bcc +
% jmp dirtyDirent
% + tay
% lda delCluster
% jsr getFatEntry
% pha
% tya
% pha
% lda #0
% sta fatValue
% sta fatValue+1
% lda delCluster
% ldy delCluster+1
% jsr setFatEntry
% pla
% sta delCluster+1
% pla
% sta delCluster
% jmp -
%
% flushBlock = 14
% flushCountdown = $60
% flushRepeats = $61
% flushDirIndex = $61
%
Write the FAT and directory sectors from memory to disk, if they are dirty.
% msFlush = * ;( msDevice, msType ) : .CS=error
% lda #$0e
% sta $ff00
% lda fatDirty
% beq flushDirectory
% lda #0
% sta fatDirty
%
% ;** flush fat
Flush both copies of the FAT, if there are two; otherwise, only flush the one.
% lda #2
% sta flushRepeats
% lda #1
% sta flushBlock
%
% masterFlush = *
% lda fatBlocks
% sta flushCountdown
% lda #<fatbuf
% ldy #>fatbuf
% sta bufptr
% sty bufptr+1
% - lda flushBlock
% ldy #0
% jsr writeLogicalBlock
% bcc +
% rts
% + inc flushBlock
% dec flushCountdown
% bne -
% dec flushRepeats
% bne masterFlush
%
% ;** flush directory
% flushDirectory = *
% lda firstRootDirBlock
% sta flushBlock
% lda rootDirBlocks
% sta flushCountdown
% lda #0
% sta flushDirIndex
% lda #<dirbuf
% ldy #>dirbuf
% sta bufptr
% sty bufptr+1
% - ldx flushDirIndex
% lda dirDirty,x
% beq +
% lda #0
% sta dirDirty,x
% lda flushBlock
% ldy #0
% jsr writeLogicalBlock
% dec bufptr+1
% dec bufptr+1
% + inc flushBlock
% inc flushDirIndex
% inc bufptr+1
% inc bufptr+1
% dec flushCountdown
% bne -
% clc
% rts
%
% bfFatEntry = 14
% bfBlocks = $60
%
Count the number of free FAT entries (value $000) from entry 2 up to the
highest FAT entry available for cluster allocation. Then multiply this
by the number of bytes per cluster (either 512 or 1024).
% msBytesFree = * ;( ) : .AYX=fileBytesFree
% ldy #$0e
% sty $ff00
% lda #2
% ldy #0
% sta bfFatEntry
% sty bfFatEntry+1
% sty bfBlocks
% sty bfBlocks+1
% - lda bfFatEntry
% ldy bfFatEntry+1
% jsr getFatEntry
% sty 2
% ora 2
% bne +
% inc bfBlocks
% bne +
% inc bfBlocks+1
% + inc bfFatEntry
% bne +
% inc bfFatEntry+1
% + lda bfFatEntry
% cmp lastFatEntry
% lda bfFatEntry+1
% sbc lastFatEntry+1
% bcc -
% ldx clusterBlockCount
% - asl bfBlocks
% rol bfBlocks+1
% dex
% bne -
% lda #0
% ldy bfBlocks
% ldx bfBlocks+1
% rts
%
This is the file copying level. It deals with reading/writing the clusters of
MS-DOS files and copying the data they contain to/from the already-open CBM
Kernal file, possibly with ASCII/PETSCII translation.
% ;====file copy level====
%
% transMode = 14
% lfn = 15
% cbmDataPtr = $60
% cbmDataLen = $62
% cluster = $64
%
Copy the given cluster to the CBM output file. This routine fetches the next
cluster of the file for the next time this routine is called, and if it hits
the NULL pointer of the last cluster of a file, it adjusts the number of valid
file data bytes the current cluster contains to FileLength % ClusterLength
(see note below).
% copyFileCluster = * ;( cluster, lfn, transMode ) : .CS=err
Read the cluster and setup to copy the whole cluster to the CBM file.
% lda cluster
% ldy cluster+1
% jsr readCluster
% bcc +
% rts
% + lda #<clusterBuf
% ldy #>clusterBuf
% sta cbmDataPtr
% sty cbmDataPtr+1
% lda #0
% sta cbmDataLen
% lda clusterBlockCount
% asl
% sta cbmDataLen+1
%
Fetch the next cluster number of the file, and adjust the cluster data length
for the last cluster of the file.
% ;**get next cluster
% lda cluster
% ldy cluster+1
% jsr getFatEntry
% sta cluster
% sty cluster+1
% cpy #$05
% bcc copyFileClusterData
% lda lenML
% sta cbmDataLen
% lda #$01
% ldx clusterBlockCount
% cpx #1
% beq +
% lda #$03
% + and lenML+1
The following three lines were added in a last minute panic after realizing
that if FileLength % ClusterSize == 0, then the last cluster of the file
contains ClusterSize bytes, not zero bytes.
% bne +
% ldx lenML
% beq copyFileClusterData
% + sta cbmDataLen+1
%
% copyFileClusterData = *
% jsr commieOut
% rts
%
Copy the file data in the MS-DOS cluster buffer to the CBM output file.
% cbmDataLimit = $66
%
% commieOut = * ;( cbmDataPtr, cbmDataLen ) : .CS=err
If the the logical file number to copy to is 0 ("null device"), then don't
bother copying anything.
% ldx lfn
% bne +
% clc
% rts
Otherwise, prepare the logical file number for output.
% + jsr kernelChkout
% bcc commieOutMore
% sta errno
% rts
%
Process the cluster data in chunks of up to 255 bytes or the number of data
bytes remaining in the cluster.
% commieOutMore = *
% lda #255
% ldx cbmDataLen+1
% bne +
% lda cbmDataLen
% + sta cbmDataLimit
% ldy #0
% - lda (cbmDataPtr),y
% bit transMode
% bpl +
If we have to translate the current ASCII character, look up the PETSCII value
in the translation table and output that value. If the translation table
entry value is $00, then don't output a character (filter out invalid
character codes).
% tax
% lda transBuf,x
% beq commieNext
% + jsr kernelChrout
% commieNext = *
% iny
% cpy cbmDataLimit
% bne -
%
Increment the cluster buffer pointer and decrement the cluster buffer character
count according to the number of bytes just processed, and repeat the above if
more file data remains in the current cluster.
% clc
% lda cbmDataPtr
% adc cbmDataLimit
% sta cbmDataPtr
% bcc +
% inc cbmDataPtr+1
% + sec
% lda cbmDataLen
% sbc cbmDataLimit
% sta cbmDataLen
% bcs +
% dec cbmDataLen+1
% + lda cbmDataLen
% ora cbmDataLen+1
% bne commieOutMore
If we are finished with the cluster, then clear the CBM Kernal output channel.
% jsr kernelClrchn
% clc
% rts
%
The file copying main routine. Set up for the starting cluster, and call
the cluster copying routine until end-of-file is reached. Checks for a
NULL cluster pointer in the directory entry to handle zero-length files.
% msRead = * ;( cluster, lenML, .A=transMode, .X=lfn ) : .CS=err
% ldy #$0e
% sty $ff00
% sta transMode
% stx lfn
% lda startCluster
% ldy startCluster+1
% sta cluster
% sty cluster+1
% jmp +
% - jsr copyFileCluster
% bcc +
% rts
% + lda cluster+1
% cmp #$05
% bcc -
% clc
% rts
%
% inLfn = $50
% generateLf = $51
% cbmDataMax = $52
% reachedEof = $54
% prevSt = $55
%
Set the translation and input logical file number and set up for reading
from a CBM-Kernal input file.
% commieInInit = * ;( .A=transMode, .X=inLfn )
% sta transMode
% stx inLfn
% lda #0
% sta generateLf
% sta reachedEof
% sta prevSt
% rts
%
Read up to "cbmDataMax" bytes into the specified buffer from the established
CBM logical file number. The number of bytes read is returned in
"cbmDataLen". If end of file occurs, "cbmDataLen" will be zero and the .Z
flag will be set. Regular error return.
% commieIn = * ;( cbmDataPtr++, cbmDataMax ) : cbmDataLen, .CS=err, .Z=eof
Establish input file, or return immediately if already past eof.
% lda #0
% sta cbmDataLen
% sta cbmDataLen+1
% ldx reachedEof
% beq +
% lda #0
% clc
% rts
% + ldx inLfn
% jsr kernelChkin
% bcc commieInMore
% sta errno
% rts
%
Read next chunk of up to 255 bytes into input buffer.
% commieInMore = *
% lda #255
% ldx cbmDataMax+1
% bne +
% lda cbmDataMax
% + sta cbmDataLimit
% ldy #0
% - jsr commieInByte
% bcc +
% rts
% + beq +
% sta (cbmDataPtr),y
% iny
% cpy cbmDataLimit
% bne -
%
Prepare to read another chunk, or exit.
% + sty cbmDataLimit
% clc
% lda cbmDataPtr
% adc cbmDataLimit
% sta cbmDataPtr
% bcc +
% inc cbmDataPtr+1
% + clc
% lda cbmDataLen
% adc cbmDataLimit
% sta cbmDataLen
% bcc +
% inc cbmDataLen+1
% + sec
% lda cbmDataMax
% sbc cbmDataLimit
% sta cbmDataMax
% bcs +
% dec cbmDataMax+1
% + lda reachedEof
% bne +
% lda cbmDataMax
% ora cbmDataMax+1
% bne commieInMore
Shut down reading and exit.
% + jsr kernelClrchn
% lda cbmDataLen
% ora cbmDataLen+1
% clc
% rts
%
Read a single byte from the CBM-Kernal input logical file number. Translate
character into ASCII and expand CR into CR+LF if necessary. Return EOF if
previous character returned was last from disk input channel.
% commieInByte = * ;( ) : .A=char, .CS=err, .Z=eof, reachedEof
% ;** check for already past eof
% lda reachedEof
% beq +
% brk
% ;** check for generated linefeed
% + lda generateLf
% beq +
% lda #0
% sta generateLf
% lda #$0a
% clc
% rts
% ;** check for eof
% + lda prevSt
% and #$40
% beq +
% lda #$ff
% sta reachedEof
% lda #0
% clc
% rts
% ;** read actual character
% + jsr kernelChrin
% ldx st
% stx prevSt
% bcc +
% sta errno
% jsr kernelClrchn
% rts
% ;** translate if necessary
% + bit transMode
% bpl +
% tax
% lda transBufToAscii,x
% beq commieInByte
Note here that the translated character is checked to see if it is a carriage
return, rather than checking the non-translated character, to see if a
linefeed must be generated next. Thus, you could define that a Commodore
carriage return be translated into a linefeed (for Unix) and no additional
unwanted linefeed would be generated.
% cmp #$0d
% bne +
% sta generateLf
% ;** exit
% + ldx #$ff
% clc
% rts
%
% firstFreeFatEntry = $5a
%
Search FAT for a free cluster, and return the cluster (FAT entry) number. A
global variable "firstFreeFarEntry" is maintained which points to the first
FAT entry that could possibly be free, to avoid wasting time searching from
the very beginning of the FAT every time. Clusters are allocated in
first-free order.
% allocateFatEntry = * ;( ) : .AY=fatEntry, .CS=err
% - lda firstFreeFatEntry
% cmp lastFatEntry
% lda firstFreeFatEntry+1
% sbc lastFatEntry+1
% bcc +
% rts
% + lda firstFreeFatEntry
% ldy firstFreeFatEntry+1
% jsr getFatEntry
% sty 2
% ora 2
% bne +
% lda firstFreeFatEntry
% ldy firstFreeFatEntry+1
% clc
% rts
% + inc firstFreeFatEntry
% bne -
% inc firstFreeFatEntry+1
% jmp -
%
% msFileLength = $5c ;(3 bytes)
%
Allocate a new cluster to a file, link it into the file cluster chain, and
write the cluster buffer to disk in that cluster, adding "cbmDataLen" bytes
to the file.
% msWriteCluster = * ; (*) : .CS=err
% ;** get a new cluster
% jsr allocateFatEntry
% bcc +
% rts
% ;** make previous fat entry point to new cluster
% + sta fatValue
% sty fatValue+1
% lda cluster
% ora cluster+1
% beq +
% lda cluster
% ldy cluster+1
% ldx fatValue
% stx cluster
% ldx fatValue+1
% stx cluster+1
% jsr setFatEntry
% jmp msClusterNew
Handle case of no previous cluster - make directory entry point to new
cluster.
% + lda writeDirent
% ldy writeDirent+1
% sta 2
% sty 3
% ldy #26
% lda fatValue
% sta (2),y
% sta cluster
% iny
% lda fatValue+1
% sta (2),y
% sta cluster+1
%
% ;** make new fat entry point to null
% msClusterNew = *
% lda #$ff
% ldy #$0f
% sta fatValue
% sty fatValue+1
% lda cluster
% ldy cluster+1
% jsr setFatEntry
% ;** write new cluster data
% + lda cluster
% ldy cluster+1
% jsr writeCluster
% bcc +
% rts
% ;** add cluster length to file length
% + clc
% lda msFileLength
% adc cbmDataLen
% sta msFileLength
% lda msFileLength+1
% adc cbmDataLen+1
% sta msFileLength+1
% bcc +
% inc msFileLength+2
% + clc
% rts
%
Copy a CBM-Kernal file to an MS-DOS file, possibly with translation.
% msWrite = * ;( msDevice, msType, writeDirent, .A=trans, .X=cbmLfn ) :.CS=err
% ldy #$0e
% sty $ff00
% ;** initialize
Set input file translation and logical file number, init cluster, file length,
FAT allocation first free pointer (to cluster #2, the first data cluster).
% jsr commieInInit
% lda #0
% sta cluster
% sta cluster+1
% sta firstFreeFatEntry+1
% sta msFileLength
% sta msFileLength+1
% sta msFileLength+2
% lda #2
% sta firstFreeFatEntry
%
% ;** copy cluster from cbm file
% - lda #<clusterBuf
% ldy #>clusterBuf
% sta cbmDataPtr
% sty cbmDataPtr+1
% lda clusterBlockCount
% asl
% tay
% lda #0
% sta cbmDataMax
% sty cbmDataMax+1
% jsr commieIn
% bcc +
% rts
% + beq +
% jsr msWriteCluster
% bcc -
% rts
%
% ;** wrap up after writing - set file length, dirty flag, exit.
% + lda writeDirent
% ldy writeDirent+1
% sta 2
% sty 3
% ldx #0
% ldy #28
% - lda msFileLength,x
% sta (2),y
% iny
% inx
% cpx #3
% bcc -
% jsr dirtyDirent
% clc
% rts
%
This level deals exclusively with Commodore files.
% ;===== commodore file level =====
%
Copy from an input disk logical file number to an output lfn, in up to 1024
byte chunks. This routine makes use of the existing "commieIn" and
"commieOut" routines. No file translation is available; binary translation is
used for both commieIn and commieOut.
% cbmCopy = * ;( .A=inLfn, .X=outLfn )
% ldy #$0e
% sty $ff00
% stx lfn
% tax
% lda #0
% jsr commieInInit
% - lda #<clusterBuf
% ldy #>clusterBuf
% sta cbmDataPtr
% sty cbmDataPtr+1
% lda #<1024
% ldy #>1024
% sta cbmDataMax
% sty cbmDataMax+1
% jsr commieIn
% bcs +
% beq +
% lda #<clusterBuf
% ldy #>clusterBuf
% sta cbmDataPtr
% sty cbmDataPtr+1
% jsr commieOut
% bcs +
% jmp -
% + rts
%
Read a single directory entry from the given logical file number, which is
assumed to be open for reading a directory ("$"). The data of the directory
entry are returned in the interface variables.
% cbmDirent = * ;( .A=lfn )
Initialize.
% ldy #$0e
% sty $ff00
% tax
% jsr kernelChkin
% bcc +
% cdirErr = *
% lda #0
% sta cdirFlen
% sta cdirBlocks
% sta cdirBlocks+1
% rts
% ;** get block count
% + jsr cdirGetch
% jsr cdirGetch
% jsr cdirGetch
% sta cdirBlocks
% jsr cdirGetch
% sta cdirBlocks+1
% ;** look for filename
% lda #0
% sta cdirFlen
% - jsr cdirGetch
% cmp #34
% beq +
% cmp #"b"
% bne -
% jsr kernelClrchn
% rts
% ;** get filename
% + ldy #0
% - jsr cdirGetch
% cmp #34
% beq +
% sta cdirName,y
% iny
% bne -
% + sty cdirFlen
Look for and get file type.
% - jsr cdirGetch
% cmp #" "
% beq -
% sta cdirType
Scan for end of directory entry, return.
% - jsr cdirGetch
% cmp #0
% bne -
% jsr kernelClrchn
% rts
%
Get a single character of the directory entry, watching for end of file (which
would indicate error here).
% cdirGetch = *
% jsr kernelChrin
% bcs +
% bit st
% bvs +
% rts
% + pla
% pla
% jsr kernelClrchn
% jmp cdirErr
%
% ;===== data =====
%
This is the translation table used to convert from ASCII to PETSCII. You can
modify it to suit your needs if you wish. If you cannot reassemble this file,
then you can sift through the binary file and locate the table and change it
there. An entry of $00 means the corresponding ASCII character will not be
translated. You'll notice that I have set up translations for the following
ASCII control characters into PETSCII: Backspace, Tab, Linefeed (CR), and
Formfeed. I also translate the non-PETSCII characters such as {, |, ~, and _
according to what they probably would have been if Commodore wasn't so
concerned with the graphics characters.
% transBuf = *
% ;0 1 2 3 4 5 6 7 8 9 a b c d e f
% .byte $00,$00,$00,$00,$00,$00,$00,$00,$14,$09,$0d,$00,$93,$00,$00,$00 ;0
% .byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00 ;1
% .byte $20,$21,$22,$23,$24,$25,$26,$27,$28,$29,$2a,$2b,$2c,$2d,$2e,$2f ;2
% .byte $30,$31,$32,$33,$34,$35,$36,$37,$38,$39,$3a,$3b,$3c,$3d,$3e,$3f ;3
% .byte $40,$c1,$c2,$c3,$c4,$c5,$c6,$c7,$c8,$c9,$ca,$cb,$cc,$cd,$ce,$cf ;4
% .byte $d0,$d1,$d2,$d3,$d4,$d5,$d6,$d7,$d8,$d9,$da,$5b,$5c,$5d,$5e,$5f ;5
% .byte $c0,$41,$42,$43,$44,$45,$46,$47,$48,$49,$4a,$4b,$4c,$4d,$4e,$4f ;6
% .byte $50,$51,$52,$53,$54,$55,$56,$57,$58,$59,$5a,$db,$dc,$dd,$de,$df ;7
% .byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00 ;8
% .byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00 ;9
% .byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00 ;a
% .byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00 ;b
% .byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00 ;c
% .byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00 ;d
% .byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00 ;e
% .byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00 ;f
%
This is the translation table used to convert from PETSCII to ASCII. You can
modify it to suit your needs, similar to the ASCII to PETSCII table. An entry
of $00 means the corresponding PETSCII character will not be translated.
You'll notice that I have set up translations for the following PETSCII
control characters into ASCII: Delete (into Backspace), Tab, Carriage Return
(into CR+LF), and ClearScreen (into Fordfeed). Appropriate translations into
the ASCII characters {, }, ^, _, ~, \, and | are also set up.
% transBufToAscii = *
% ;0 1 2 3 4 5 6 7 8 9 a b c d e f
% .byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$09,$00,$00,$00,$0d,$00,$00 ;0
% .byte $00,$00,$00,$00,$08,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00 ;1
% .byte $20,$21,$22,$23,$24,$25,$26,$27,$28,$29,$2a,$2b,$2c,$2d,$2e,$2f ;2
% .byte $30,$31,$32,$33,$34,$35,$36,$37,$38,$39,$3a,$3b,$3c,$3d,$3e,$3f ;3
% .byte $40,$61,$62,$63,$64,$65,$66,$67,$68,$69,$6a,$6b,$6c,$6d,$6e,$6f ;4
% .byte $70,$71,$72,$73,$74,$75,$76,$77,$78,$79,$7a,$5b,$5c,$5d,$5e,$5f ;5
% .byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00 ;6
% .byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00 ;7
% .byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00 ;8
% .byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00 ;9
% .byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00 ;a
% .byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00 ;b
% .byte $60,$41,$42,$43,$44,$45,$46,$47,$48,$49,$4a,$4b,$4c,$4d,$4e,$4f ;c
% .byte $50,$51,$52,$53,$54,$55,$56,$57,$58,$59,$5a,$7b,$7c,$7d,$7e,$7f ;d
% .byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00 ;e
% .byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$7e ;f
%
% ;====bss storage (size=11,264 bytes)====
%
This is where the track cache, etc. are stored. This section requires 11K of
storage space but does not increase the length of the binary program file
since these storage areas are DEFINED rather than allocated with ".buf"
directives. The Unix terminology for this type of uninitialized data is
"bss".
% bss = *
% trackbuf = bss
% clusterBuf = trackbuf+4608
% fatbuf = clusterBuf+1024
% dirbuf = fatbuf+1536
% end = dirbuf+4096
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. In particular, you may wish to change the "dv" and
"dt" variables in lines 25 and 26. This program is not listed in the "%"
format that the assembler listing is since you can recover this listing from
the uuencoded binary program file. The listing is here in its entirety.
10 print chr$(147);"little red reader 128 version 1.00"
11 print : print"by craig bruce 09-feb-93 for c=hacking" : print
12 :
20 cd=peek(186):if cd<8 then cd=8 : rem ** default cbm-dos drive **
25 dv=9:dt=0 : rem ** ms-dos drive, type (0=1571,255=1581)
26 if dv=cd then dv=8:dt=0 : rem ** alternate ms-dos drive
27 :
30 print "initializing..." : print
40 bank0 : pk=dec("8000") : pv=pk+30
50 if peek(pv+0)=dec("cb") and peek(pv+1)=132 then 60
55 print"loading machine language routines..." : bload"lrr.bin",u(cd)
60 poke pv+3,dv : poke pv+4,dt : sys pk
70 dim t,r,b,i,a$,c,dt$,fl$,il$,x,x$
71 cm$="dmftc+-q "+chr$(13)+chr$(145)+chr$(17)+chr$(157)+chr$(29)+chr$(19)
72 cm$=cm$+chr$(147)+"/rnx"+chr$(92)
75 dl=-1 : cf=-1 : me=0 : ca=0 : ma=0
80 dim di$(1,300),cl(128),sz(128),dp(128),cn$(300)
90 if dt=255 then dt$="1581" :else dt$="1571"
100 fl$=chr$(19)+chr$(17)+chr$(17)+chr$(17)+chr$(17)
110 il$=fl$:fori=1to19:il$=il$+chr$(17):next
120 goto 500
130 :
131 rem ** load ms-dos directory **
140 print"loading ms-dos directory..." : print
150 sys pk : sys pk+3
160 dl=0
170 rreg bl,dc,bh,s : e=peek(pv+2)
180 if (s and 1) then gosub 380 : dl=-1 : return
190 print"scanning ms-dos directory..." : print
200 db=bl+256*bh
205 sys pk+21 : rreg bl,x,bh : ma=bl+bh*256+x*65536
210 if dc=0 then 360
220 for dp=db to db+32*(dc-1) step 32
230 if peek(dp)=0 or peek(dp)=229 then 350
240 if peek(dp+11) and 24 then 350
250 dl=dl+1
Line 260 sets the default selection, translation, and filetypes for MS-DOS
files. Change to your liking.
260 d$=right$(" "+str$(dl),3)+" asc seq " : rem ** default sel/tr/ft **
270 a$="" : fori=0to10 : a$=a$+chr$(peek(dp+i)) : next
280 a$=left$(a$,8)+" "+right$(a$,3)
290 print dl; a$
300 d$=d$+a$+" "
310 cl(dl)=peek(dp+26)+256*peek(dp+27)
320 sz=peek(dp+28)+256*peek(dp+29)+65536*peek(dp+30)
330 di$(0,dl)=d$+right$(" "+str$(sz),6)
335 dp(dl)=dp
340 sz(dl)=sz
350 next dp
360 return
370 :
371 rem ** report ms-dos disk error **
380 print chr$(18);"ms-dos disk error #";mid$(str$(e),2);
390 print " ($";mid$(hex$(e),3);"), press key.";chr$(146)
400 getkey a$ : return
410 :
411 rem ** screen heading **
420 print chr$(147);chr$(18);
421 if me=0 then print"ms-dos";:x=ma:else print"cbmdos";:x=ca
422 print chr$(146);" ms=";mid$(str$(dv),2);":";dt$;
430 print" cbm=";mid$(str$(cd),2);" free=";mid$(str$(x),2)
440 print : return
450 :
451 rem ** screen footing **
460 print il$;"d=dir m=msdev f=cbmdev c=copy q=quit "
470 print "t=toggle r=remove x=cbmcpy /=menu +-=pg";
480 return
490 :
491 rem ** main routine **
500 t=1 : c=0
501 r=0
510 if me=0 then mf=dl:mc=2 : else mf=cf:mc=1
520 gosub 420
521 if me<>0 then 542
530 print "num s trn typ filename ext length"
540 print "--- - --- --- -------- --- ------"
541 goto 550
542 print "num s trn filename t length"
543 print "--- - --- ---------------- - ------"
550 gosub 460
560 b=t+17 : if b>mf then b=mf
570 print fl$;: if t>mf then 590
580 for i=t to b : print di$(me,i) : next
590 if mf<0 then print chr$(18);"<directory not loaded>";chr$(146)
591 if mf=0 then print chr$(18);"<no files>";chr$(146)
600 if mf<=0 then 660
610 print left$(il$,r+5);chr$(18);
620 on c+1 goto 630,640,650
630 print spc(4);mid$(di$(me,t+r),5,3) : goto 660
640 print spc(7);mid$(di$(me,t+r),8,5) : goto 660
650 print spc(12);mid$(di$(me,t+r),13,5) : goto 660
660 getkey a$
670 i=instr(cm$,a$)
680 if mf>0 then print left$(il$,r+5);di$(me,t+r)
690 if i=0 then 600
700 on i goto 760,1050,1110,950,1150,1000,1020,730,860,860,770,790,810,830,850
705 on i-15 goto 500,713,1400,713,1500,713
710 stop
711 :
712 rem ** various menu options **
713 me=-(me=0)
714 goto500
730 print chr$(147);"have an awesome day." : bank15
740 end
760 if me=1 then gosub 420 : gosub 2500 : goto 500
765 gosub 420 : gosub 140 : goto 500
770 r=r-1 : if r<0 then r=b-t
780 goto 600
790 r=r+1 : if t+r>b then r=0
800 goto 600
810 c=c-1 : if c<0 then c=mc
820 goto 600
830 c=c+1 : if c>mc then c=0
840 goto 600
850 r=0 : c=0 : goto 600
860 if mf<=0 then 600
870 x=t+r : on c+1 gosub 890,910,930
880 print left$(il$,r+5);di$(me,x) : goto 600
890 if mid$(di$(me,x),6,1)=" " then x$="*" :else x$=" "
900 mid$(di$(me,x),6,1)=x$ : return
910 if mid$(di$(me,x),9,1)="a" then x$="bin" :else x$="asc"
920 mid$(di$(me,x),9,3)=x$ : return
930 if mid$(di$(me,x),14,1)="s" then x$="prg" :else x$="seq"
940 mid$(di$(me,x),14,3)=x$ : return
950 if mf<=0 then 600
960 for x=1 to mf
970 on c+1 gosub 890,910,930
980 next x
990 goto 520
1000 r=0:if b=mf then t=1 : goto 510
1010 t=t+18 : goto 510
1020 if mf<=0 then 660
1025 r=0:if t=1 then t=mf-(mf-int(mf/18)*18)+1 : if t<=mf then 510
1030 t=t-18 : if t<1 then t=1
1040 goto 510
1050 print il$;chr$(27);"@";
1060 input"ms-dos device number (8-30)";dv
1061 if cd=dv thenprint"ms-dos and cbm-dos devices must be different!":goto1060
1070 x=71 : input"ms-dos device type (71/81)";x
1080 if x=8 or x=81 or x=1581 then dt=255:dt$="1581" :else dt=0:dt$="1571"
1090 poke pv+3,dv : poke pv+4,dt : sys pk : dl=-1 : ma=0
1100 goto 500
1110 print il$;chr$(27);"@";
1120 input "cbm-dos device number (0-30)";cd
1130 if cd=dv thenprint"ms-dos and cbm-dos devices must be different!":goto1120
1140 cf=-1 : ca=0 : goto 500
1141 :
1142 rem ** copy files **
1150 if me=1 then 2000
1151 print chr$(147);"copy ms-dos -> cbm-dos":print:print
1160 if dl<=0 then fc=0 : goto 1190
1170 fc=0 : for f=1 to dl : if mid$(di$(0,f),6,1)="*" then gosub 1200
1180 next f
1190 print : print"files copied =";fc;" - press key"
1191 getkey a$ : goto 520
1200 fc=fc+1
1210 x$=mid$(di$(0,f),19,8)+"."+mid$(di$(0,f),29,3)
1220 cf$="":fori=1tolen(x$):if mid$(x$,i,1)<>" " then cf$=cf$+mid$(x$,i,1)
1230 next
1231 if right$(cf$,1)="." then cf$=left$(cf$,len(cf$)-1)
1232 cf$=cf$+","+mid$(di$(0,f),14,1)
1240 print str$(fc);". ";chr$(34);cf$;chr$(34);tab(20);sz(f)"bytes";
1245 print tab(35);mid$(di$(0,f),9,3)
1250 cl=cl(f) : lb=sz(f) - int(sz(f)/65536)*65536
1260 if cd>=8 then dopen#1,(cf$+",w"),u(cd) :else if cd<>0 then open 1,cd,7
1265 if cd<8 then 1288
1270 if ds<>63 then 1288
1275 x$="y" : print "cbm file exists; overwrite (y/n)";
1280 close 1 : input x$ : if x$="n" then fc=fc-1 : return
1285 scratch(cf$),u(cd)
1286 dopen#1,(cf$+",w"),u(cd)
1288 if cd<8 then 1320
1300 if ds<20 then 1320
1310 print chr$(18)+"cbm disk error: "+ds$ : fc=fc-1 : close1 : return
1320 poke pv+6,cl/256 : poke pv+5,cl-peek(pv+6)*256
1330 poke pv+8,lb/256 : poke pv+7,lb-peek(pv+8)*256
1340 tr=0 : if mid$(di$(0,f),9,1)="a" then tr=255
1346 x=1 : if cd=0 then x=0
1350 sys pk+6,tr,x
1355 rreg x,x,x,s : e=peek(pv+2)
1356 if (s and 1) then gosub 380 : fc=fc-1
1360 if cd<>0 and cd<8 then close1
1370 if cd>=8 then dclose#1 : if ds>=20 then 1310
1380 return
1398 :
1399 rem ** remove ms-dos file **
1400 print chr$(147);"remove (delete) selected ms-dos files:":print
1401 if me<>0 then print"ms-dos menu must be selected!" : goto2030
1402 a$="y":input"are you like sure about this (y/n)";a$
1403 print:if a$="n" then goto 520
1410 if dl<=0 then fc=0 : goto 1440
1420 fc=0 : f=1
1425 if mid$(di$(0,f),6,1)="*" then gosub 1470 : fc=fc+1 : f=f-1
1430 f=f+1 : if f<=dl then 1425
1434 print"flushing..."
1435 sys pk+12
1440 print : print"files removed =";fc;" - press key"
1445 sys pk+21 : rreg a,x,y : ma=a+y*256+x*65536
1450 getkey a$ : goto 500
1470 print"removing ";chr$(34);mid$(di$(0,f),19,13);chr$(34)
1490 poke pv+10,dp(f)/256 : poke pv+9,dp(f)-peek(pv+10)*256
1492 sys pk+15
1494 di$(0,f)=di$(0,dl):sz(f)=sz(dl):dp(f)=dp(dl):cl(f)=cl(dl)
1495 dl=dl-1
1496 return
1498 :
1499 rem ** copy cbm files **
1500 print chr$(147);"copy cbm-dos to cbm-dos:":print
1501 if cf<=0 then print"commodore directory not loaded" : goto 2030
1502 x=0 : input"device number to copy to";x : print
1503 if x<=0 or x>=64 then print"bad device number!" : goto 2030
1504 if x=cd then print"cannot copy to same device" : goto 2030
1505 for f=1 to cf : if mid$(di$(1,f),6,1)<>"*" then 1570
1506 print di$(1,f) : open1,cd,2,cn$(f)+",r"
1507 if x<8 then open 2,x,7 : goto1550
1508 cf$=cn$(f)+","+mid$(di$(1,f),31,1)+",w"
1509 open2,x,3,cf$
1510 if ds<>63 then 1530
1511 close2
1512 x$="y":input"file exists: overwrite (y/n)";x$ : if x$="n" then 1560
1520 scratch(cn$(f)),u(x)
1525 open2,x,3,cf$
1530 if ds>20 then print chr$(18);"cbm dos error: ";ds$ : goto1560
1550 sys pk+24,1,2
1560 close1 : close2
1570 next f
1580 print : print"finished - press a key" : getkey a$ : goto510
1998 :
1999 rem ** copy cbm-dos to ms-dos **
2000 print chr$(147);"copy cbm-dos to ms-dos:" : print : print
2010 if dl>=0 then 2035
2020 print"ms-dos directory must be loaded first"
2030 print : print"press any key" : getkey a$ : goto 510
2035 fc=0
2036 for f=1 to cf : if mid$(di$(1,f),6,1)<>"*" then 2045
2040 fc=fc+1 : c$=cn$(f)
2041 printmid$(str$(fc),2);" ";mid$(di$(1,f),14,16);mid$(di$(1,f),34);":";
2042 gosub2050 : print left$(m$,8);".";right$(m$,3)
2043 tr=0 : if mid$(di$(1,f),9,1)="a" then tr=255
2044 gosub2100
2045 next
2046 print"flushing..." : sys pk+12
2047 sys pk+21 : rreg a,x,y : ma=a+y*256+x*65536
2048 print: print"files copied =";fc : goto2030
2049 :
2050 x=instr(c$,".") : if x=0 then m$=c$+" " : goto2090
2055 x=len(c$)+1 : do : x=x-1 : loop until mid$(c$,x,1)="."
2060 m$=left$(left$(c$,x-1)+" ",8)
2070 x$=mid$(c$,x+1)+" "
2080 m$=m$+x$
2090 m$=left$(m$,11)
2091 fori=1to11:x$=chr$(asc(mid$(m$,i,1))and127):if x$="."orx$=" " then x$="_"
2092 mid$(m$,i,1)=x$ : next i
2093 i=8 : do while i>1 and mid$(m$,i,1)="_" : mid$(m$,i,1)=" " : i=i-1 : loop
2094 i=11 : do while i>8 and mid$(m$,i,1)="_" : mid$(m$,i,1)=" " : i=i-1 : loop
2098 return
2099 :
2100 fori=0to0
2105 for dp=db to db+32*(dc-1) step 32
2110 if peek(dp)=0 or peek(dp)=229 then 2140
2120 next dp
2130 print"no free ms-dos directory entires" : return
2140 next i
2160 fori=1tolen(m$):pokedp+i-1,asc(mid$(m$,i,1)) and 127:next
2170 fori=11to31:poke dp+i,0:next
2180 pokedp+26,255:pokedp+27,15
2190 poke pv+10,dp/256:poke pv+9,dp-peek(pv+10)*256
2200 open1,cd,2,c$
2300 sys pk+9,tr,1 : rreg x,x,x,s
2301 close1
2305 if s and 1 then e=peek(pv+2) : gosub380 : return
Line 2310 sets the default MS-DOS selection, translation, and filetype after
copying to MS-DOS disk, based on the CBM-DOS filetype. Change to your liking.
2310 x$=" asc seq ":if tr=0 then x$=" bin prg "
2320 dl=dl+1 : d$=right$(" "+str$(dl),3)+x$
2330 d$=d$+left$(m$,8)+" "+right$(m$,3)
2340 cl(dl)=peek(dp+26)+256*peek(dp+27)
2350 sz=peek(dp+28)+256*peek(dp+29)+65536*peek(dp+30)
2360 di$(0,dl)=d$+right$(" "+str$(sz),8)
2370 dp(dl)=dp
2380 sz(dl)=sz
2395 return
2498 :
2499 rem ** load commodore dos directory **
2500 print"loading commodore dos directory..." : print
2501 if cd<8 then print"cbmdos device must be >= 8!" : goto2030
2505 open1,cd,0,"$0":get#1,a$,a$ : cf=-1 : q$=chr$(34)
2506 do
2507 sys pk+27,1 : b=peek(pv+11)+256*peek(pv+12) : t$=chr$(peek(pv+13))
2510 x=peek(pv+14)
2520 if x=0 then exit
2530 x$="" : for i=pv+15 to pv+15+x-1 : x$=x$+chr$(peek(i)) : next
2575 cf=cf+1
2590 if cf=0 then print"disk="q$x$q$ : print : goto2650
2600 cn$(cf)=x$
2610 a$=left$(x$+" ",17)+t$+right$(" "+str$(b*254),8)
Lines 2620 and 2625 set the default CBM-DOS selection and translation modes
based on the filetype. Change to your liking.
2620 di$(1,cf)=right$(" "+str$(cf),3)+" asc "+a$
2625 if t$<>"s" then mid$(di$(1,cf),9,3)="bin"
2630 print di$(1,cf)
2650 loop
2670 ca=b*256 : close1 : return
6. UUENCODED FILES
Here are the binary executables in uuencoded form. The CRC32s of the two
files are as follows:
crc32 = 3896271974 for "lrr-128"
crc32 = 2918283051 for "lrr.bin"
The "lrr.128" file is the main BASIC program and the "lrr.bin" file contains
the machine lanugage disk-accessing routines.
begin 640 lrr-128
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
B0T8I``0_6@KL`!H_;@I#0;)"K#(U-B`Z(*`Q(#H@C@``````
`
end
begin 640 lrr.bin
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
M9H52L`+&4Z54T`:E4@53T+@@S/^E8@5C&&"E5/`!`*51\`BI`(51J0H88*55
M*4#P"*G_A52I`!A@(,__II"&59`'C2"`(,S_8"0.$`RJO?:)\,?)#=`"A5&B
M_QA@I5K-2("E6^U)@)`!8*5:I%L@=(2$`@4"T`:E6J1;&&#F6M#<YEM,8X<@
M8X>0`6"%"80*I60%9?`2I62D9:8)AF2F"H9E()*$3,>'K2>`K"B`A0*$`Z`:
MI0F1`H5DR*4*D0*%9:G_H`^%"80*I62D92"2A*5DI&4@+8.0`6`8I5QE8H5<
MI5UE8X5=D`+F7AA@H`Z,`/\@J8:I`(5DA66%6X5<A5V%7JD"A5JI]J"<A6"$
M8:T^@`JHJ0"%4H13(+:&D`%@\`8@C(>0X&"M)X"L*("%`H0#H@"@'+5<D0+(
MZ.`#D/8@TX088*`.C`#_A@^JJ0`@J8:I]J"<A6"$8:D`H`2%4H13(+:&L!+P
M$*GVH)R%8(1A(#6&L`-,6(A@H`Z,`/^J(,;_D`RI`(TL@(TI@(TJ@&`@Y(@@
MY(@@Y(B-*8`@Y(B-*H"I`(TL@"#DB,DB\`C)0M#U(,S_8*``(.2(R2+P!IDM
M@,C0\XPL@"#DB,D@\/F-*X`@Y(C)`-#Y(,S_8"#/_[`%))!P`6!H:"#,_TR+
MB```````````%`D-`),`````````````````````````("$B(R0E)B<H*2HK
M+"TN+S`Q,C,T-38W.#DZ.SP]/C]`P<+#Q,7&Q\C)RLO,S<[/T-'2T]35UM?8
MV=I;7%U>7\!!0D-$149'2$E*2TQ-3D]045)35%565UA96MO<W=[?````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M```````````````````````````````````````````````````````)````
M#0````````@``````````````"`A(B,D)28G*"DJ*RPM+B\P,3(S-#4V-S@Y
M.CL\/3X_0&%B8V1E9F=H:6IK;&UN;W!Q<G-T=79W>'EZ6UQ=7E\`````````
M````````````````````````````````````````````````````````````
M``````````````````````````````````````````````````````````!@
M04)#1$5&1TA)2DM,34Y/4%%24U155E=865I[?'U^?P``````````````````
2``````````````````````!^````
`
end
7. THE FUTURE
Future improvements to this program would include implementation of MS-DOS
formatting, more file manipluation commands (such as Rename), re-writing the
user-interface BASIC program in machine language, and making a file buffering
facility for those people with only one disk drive. However, I don't intend
to do much more to this program. My intentions are to put this functionality
into a device driver for a new operating system (or at least, operating
environment) for the C-128. Anyone else is free to improve this program.
=============================================================================