User Tools

Site Tools

         @@@@@@            @@@@@@
    @@@@@  @@@@  @@@@      @@      @@@@@   @@@@  @@@@  @@@@  @@@@  @@@@   @@@@@
  @@@@@    @@    @@      @@@@    @@   @@   @@  @@@     @@    @@@@  @@   @@   @@
 @@@@@    @@@@@@@@     @@  @@   @@        @@@@@       @@    @@ @@ @@   @@
@@@@@    @@    @@    @@@@@@@@  @@   @@   @@  @@@     @@    @@  @@@@   @@   @@
@@@@@  @@@@  @@@@  @@@@  @@@@  @@@@@   @@@@  @@@@  @@@@  @@@@  @@@@   @@@@@@
@@@@@                                                                     @@
 @@@@@@            @@@@@@             Issue #5
   @@@@@@@@@@@@@@@@@@              March 7, 1993

Editor's Notes:
by Craig Taylor

  It seems that each issue of C= Hacking has always began with a "Sorry, It's
  late but here it is message." - Well, this one will start out again like 
  that - This issue was originally scheduled to be out the middle of January
  but due to several delays in obtaining articles and my delaying trying to
  debug the multi-tasking source code it's been held up until now. 
  My apologies to the authors who have had their articles into me on time -
  school is coming first for me and having to do a lot of coding for several
  classes was the major contributing factor to the delays. 

  Now, after the apologies are out of the way - Let's take a look at what has
  happened since last time I wrote.

  - RUN magazine is no longer with us.

  As one of the last hold-outs I was expecting RUN magazine to keep on printing
  until the Commodore 64/128's really did die out but apparently the publishers
  decided it wouldn't be so. This leaves the Twin Cities magazine as the only
  US magazine in publication for the Commodore (6502 based) computers that I am
  aware of. Speaking of Twin Cities (not sure if he's combining the 64/128 or
  just coming out with seperate Twin Cities magazines) does anybody know or
  have any information on when the next issue will be out? Or has my 
  lastest issue just not been sent out?

  As I was writing this I got the latest issue of Twin Cities which has
  expanded to C=64 coverage also. The new issue looks very nice, about 53
  pages of so of good decent material. I'd recommend get a subscription for
  those of you who are looking to still hear about new Commodore products.

  I'd like to get people's reactions on the demise of RUN and what will
  people will think will probably be the main source of information for C=
  owners.  A lot of people reading this magazine are on the comp.sys.cbm
  newsgroup but I'm wondering about individuals who do not have access to
  such a newsgroup and do not have access to the internet. Let me know what
  you think - hopefully through a friend w/ access to the internet. Sort of
  a catch-22 I guess.

  - A Mail-Server has been setup to automate sending issue requests.

  The full details of how to use the Mail-Server is in a documentation file
  contained within but this mail-server (whose source code is available for
  anyone who wishes to see it written in VAX DCL code) also allows file
  requests which will be uuencoded and sent to you. I am trying to have all of
  the programs in each issue available via request as for some people it is
  a minor pain trying to extract and compile the programs contained within.

  - I saw a note recently that the speed-up board work was still being done.

  Does anybody know anything further about this? I'm interested in this and
  how it would be carried out / done but aside from an occasional post here
  and there about it I actually hear very little. 

  - There is also work on an Ansi C compiler being done.

  Recently a group of people (about 9 currently) are working on a C compiler
  for the C=64 and C=128 which will eventually support the full ANSI C
  library. A large list of extensions have been proposed and the compiler
  will probably be released as either shareware or possibly, public domain.

  Ack! - This magazine keeps growing. The last issue was approx.
  somewhere around 3000 lines, this one is just a tad over 6000. I'm
  sure that we're not suffering the quality just because of the
  quantity. :-) Be sure to take a look at the previous back issues
  available via the Mail-Server and don't be afraid to suggest comments
  or suggestions. While usually the authors are too busy to take ideas
  for new programs we always welcome to hear how useful you find certain
  programs included herein etc.

  Also I am looking for articles on any type of software project, hardware
  project or general theory articles that you would like to submit. Just
  leave me a message via email at "". Note also
  that I've just signed up for a GENIE account and can be reached there via
  C.TAYLOR37 once my account is approved.


  Please note that this issue and prior ones are available via anonymous
  FTP from under pub/rknop/hacking.mag in addition to the
  mailserver which is documented in this issue.

  NOTICE: Permission is granted to re-distribute this "net-magazine", in 
  whole, freely for non-profit use. However, please contact individual
  authors for permission to publish or re-distribute articles seperately.
  A charge of no greater than 5 US. Dollars or equivlent may be charged for
  library service / diskette costs for this "net-magazine." 


In this Issue:

Mail-Server Documentation

This articles describes how to access the mail-server for Commodore Hacking
and includes a list of currently available files and back-issues.

Stretching Sprites

It's possible to expand sprites to more than twice their original size, but 
there is no need to expand all of them equally. This article examins how to
expand them 2,3, or more multiples of their original size.

Rob Hubbard's Music: Disassembled, Commented and Explained

This article written by Anthony McSweeney, presents the valuable source to
Rob Hubbard's first music routine. This routine was used in Rob's first 20
or 30 musics, including such classics as Thing on a Spring (Gremlin Graphics),
Commando (Elite), Thrust (Firebird), International Karate (System 3), and
Proteus (also known as Warhawk, by Firebird). 

ZPM3 and ZCCP Enhancements for CP/M Plus from Simeon Cran

Although all the articles to date in C= Hacking have focused on 6510/ 8502
programming, there have been some interesting developments on the Z80 front.
C128 CP/M users should be aware of the benefits of a new set of enhancements
to the operating system that offers inreased speed and flexibility as well
as new features. If that isn't enough, this package will also run ZCPR 3.3
utilities and applications that won't run under standard CP/M Plus.

Multi-Tasking on the C=128 - Part 1

This article examines the rudiments of Multi-Tasking and also details the
system calls in the Multi-Tasking package to be released in the next issue
of C= Hacking.

LITTLE RED WRITER: MS-DOS file reader/writer for the C128 and 1571/81.

This article is an extension on Little Red Reader which was presented in the
last issue and allows for reading and writing of MS-Dos diskettes from and to
1571/81 drives. 


Mail-Server Documentation

by Craig Taylor (

What is a mail-server?

   A mailserver is an automated job that will scan my mail file for messages
   with a subject line of "MAILSERV" and will then automatically carry out
   certain operations within the body of the mail message. This makes it easier
   on me and you. Easier for me so that I don't have to deal with 50+ messages
   each month asking for files to be sent out and also insures that your files
   that you requested will be sent within 24 hours. In addition it allows 
   files to be more easily sent and accessed in case you are not able to 
   extract the source files from C= Hacking. 

   If you have FTP access please see the Editor's Notes at the start for
   information on R. Knop's FTP site and how to access it as you may find
   using that somewhat quicker to use.

How to use the mail-server / What it is.

   This mail-server is intended to help me keep track / more easily update my
   mailing list of individuals who wish to sub-scribe or get back-issues of
   C= Hacking mailed to them. 

   To use it simply send a message to "" (me) with a
   subject line of "MAILSERV" and then with one of the following commands in the
   body of the mail message:

Currently the following commands are supported:

  help              - sends current documentation f file list
  send iss<number>. - sends issue # (1-4 currently). Remember the period!!
  subscribe         - subscribe to the mailing list automatically
 *subscribe catalog - subscribes to a list that will be sent out 
                      everytime the catalog changes.
  catalog           - show list of available source /uuencoded binaries
  psend name        - send uuencoded binary.

Commands no longer supported:

  status            - returns the current commands (this list)
                      (use the help file)

Please note that the mailserver is only run at 2:00 AM EST.

Catalog List - Last update February 27, 1993.

  iss1.                 - C= Hacking, Issue #1
  iss2.                 - C= Hacking, Issue #2
  iss3.                 - C= Hacking, Issue #3
  iss4.                 - C= Hacking, Issue #4
  iss5.                 - C= Hacking, Issue #5
  contents.lis          - Content Listing of Issues #1-4
  mailserv.012493       - VAX/DCL Mailserver .share file

 *invasion1.sfx         - Space Invasion Source (Starting with Issue 4)
 *bmover.sfx            - Geos 128 Banking with Banks 2f3 (Issue #2)
 *vdc-bg.sfx            - Use of 64K VDC RAM in Geos (Issue #3)
 *               - C64 Emulator for IBM
 *lrr.sfx               - Little Red Reader (from C= Hacking #4)

  -- Temporary Files -> Or files that will be deleted as needed for space

 *zedv075.sfx           - Zed-128 Text Editor 
 *ramdosii.sfx          - REU Dos for the C=128 Allows > 512k REU.
  NOTE: Files marked with "*" should be requested via PSEND - they will be
        sent to you in uuencoded form. They may _not_ be requested via SEND.


The Demo Corner: Stretching Sprites

by Pasi 'Albert' Ojala (
        Written: 16-May-91  Translation/Revision 01-Jun-92, Dec-92

(All timings are in PAL, principles will apply to NTSC too)

You might have heard that it is possible to expand sprites to more than
twice their original size. Imagine a sprite scroller with 6-times expanded
sprites. However, there is no need to expand all of them equally. Using
this technique, it is possible to make easy sinus effects and constantly
expanding and shrinking letters.

The VIC (video interface controller) may be fooled in many things. One of
them is the vertical expansion of sprites. If you clear the expand flag and
then set it back straight away, VIC will think it has only displayed the
first one of the expanded lines. If we do the trick again, VIC will continue
to display the same data again and again. But why does VIC behave like this ?

_Logic gates will tell the truth_

It is not really a bug, but a feature. The hardware design to implement the
vertical enlargement was just as simple as possible. Those, who do not care
about hardware should skip this part... The whole y-enlargement is handled
with five simple logical ports. Each sprite has an associated Set-Reset
flip-flop to tell whether to jump to the next sprite line (add three bytes
to the data counter) or not.

Let's call the state of the flip-flop Q and the inputs R (reset) and S (set).
The function of a SR flip-flop is quite simple: if R is one, Q goes to zero,
if S is one, Q goes to one. Otherwise the state of the flip-flop does not
change. In this case the flip-flop is Set, if either the Y-enlargement bit
is zero or the state of the flip-flop is zero at the end of a scan line. The
flip-flop is reset, if both the state and the Y-enlargement are ones at the
end of the line.

When you clear the bit in the vertical expansion register, the flip-flop will
be set regardless of the electron beam position on the scan line. If you
set the bit again before the end of the line, the flip-flop will be cleared
and VIC will be displaying the same sprite line again. In other words, VIC
will think that it is starting to display the second line of the expanded
sprite row. This way any of the lines in any of the sprites may be stretched
as wanted.

 .---- Current flipflop state (if one, enables add to sprite pointer)
 |  .---- Y-expansion bit.
 |  |  .--- End of line pulse (briefly one at end of line)
 |  |  |  .--- Next state (What state will become under these conditions)
 |  |  |  |
 0  0  0  1
 0  0  1  1
 0  1  0  no change
 0  1  1  1
 1  0  0  1             Clear $D017 -> flip-flop is set
 1  0  1  1
 1  1  0  no change     Set $D017   -> flip-flop resets at the end of line
 1  1  1  0

So, simply, at any time, if vertical expand is zero, the add enable is set
to one. At the end of the line - before adding - the state is cleared if
vertical expand is one.

_Even odder ?_

Something very weird happens when we clear the expansion bit right when VIC
is adding three to the sprite image counters. The values in the counters will
be increased only by two, and the data is then read from the wrong place.

Normally the display of a sprite ends when VIC has shown all of the 21
lines of the sprite (the counter will end up to $3f). If there has been a
counter mixup, $3f is not reached after 21 lines and VIC will go on counting
and will display the sprite again, now normally. If we fool the counter only
once, the counter value $3f is reached when the sprite is displayed twice.


I don't think the distorted counter effect can be used for anything, but
there is many things where the variable stretching could be used. When you
open the borders, you can be sure that there is a constant amount of time,
if you stretch the sprites to the whole lenght of the area. You may stretch
only the first and last lines, stretch the other lines by a constant or
using a table, or using a variable table or any of the combinations possible.

_A raster routine is a must_

Because you have to access the VIC registers on each line during the stretch,
you need some kind of routine which can do other kinds of tricks besides the
stretch. You can open the side borders and change the background color and
maybe you have to shift the screen (and the bad lines with it) downwards.
[See previous C=Hacking Issues for talk about raster interrupts.]

Look at the demo program. In the beginning of the raster routine there is
first some timing, then a loop that lasts exactly 46 clock cycles. It takes
exactly one scan line to execute. Inside the loop we first do the necassary
modifications to the vertical scroll register, then we change the background
color and then we open the side borders. And finally we handle the stretching
using the stretch data, where a zero-bit means that the corresponding sprite
will be stretched. A one-bit means that VIC is allowed to go to the next line
of the sprite data.

_Stretching takes time_

Besides showing the stretched sprites we need time to generate the stretching
data, unless of course, the stretch is constant. We have to have 20
one-bits for each sprite in our table. It is not feasible to determine the
state of each byte in the table, instead you clear the table and plot the
needed bits.

The routine is quite straightforward, but many optimizations may be applied
to make it faster. First we load Y with the stretch of the first line (the
y-coordinate of the data). Then we use it as an index to the table and plot
the right bit and increase Y with the expansion value. Then we do it again
until we have all of the 20 bits scattered to the table. The last sprite line
will then stretch until we stop the stretching, because the last line is
not allowed to be drawn.

_Speed is everything_

The calculation itself is easy, but optimizing the routine is not. If all
of the sprites are stretched equally (by integer amounts) and from the same
position, the routine is the fastest possible.  You can also have variable
and smooth stretch.  Smooth stretch uses other than integer expansion values
and thus also needs more processor time.  If each sprite has to be stretched
individually, you need much more time to do it.

The fastest routine I have ever written uses some serious selfmodification
tricks. There are also some other tricks to speed up the stretch, but they
are all secret ones.. :-)  Well, what the h*ck, I will include it anyway.
By the time you read this I have already made a faster routine..

You can speed up that routine (by 17%) by unrolling the inner loop, but you
have to use a different addressing mode for ORA (zero-page). You also need
to place some restrictions to the tables used.. If you unroll both loops,
you can get ~25% faster routine than the Fore!-version.

_Demo program_

I tried to collect all of the main principles of stretching and raster
routines to the demo program. I use the term "raster routine" when the
execution is tightly synchronized to the electron beam and to the screen
display. The program may be unclear in places, but I wanted to keep it as
short as possible. The routine opens the side borders, scrolls the screen
vertically, changes the background color and stretches the sprites.

The stretcher routine allows different y-position and amount of expansion
for each sprite. This routine uses 1/8 fractions to do the counting, and so
it is much too slow to use in a real demo.  VIC registers are initialized
from a table, instead of setting them separately. Interrupt position is one
line above the sprites. The program does not open the top or bottom borders.
(I usually use a NMI to open the vertical borders, so that I only need to
 use one raster-IRQ position.)

I tried to make a NTSC version, but I couldn't get it to synchronize.
There are also less cycles available so you can't stretch all of the sprites
individually in NTSC (with this routine that is..).

Fast-stretch from Megademo92 (part: Fore!)

SINPOS          Stretch sinus index
SINSPEED        Stretch sinus index speed
YSINPOS         Y-sinus index
YSINSPEED       Y-sinus index speed
MASK            Bit mask for passess (usually $01,$02,$04,$08,$10..)

YSINUS          Y-sinus table
STRETCH         Sprite line sizes   (LSB of the address must be 0)
SIZET           Sprite size/2 table (LSB of the address must be 0)
DATA            Stretch data table (cleared before this routine)

[xx] marks selfmodification. For example loop counter, bit mask and
index to the stretch and size data tables are stored straight in the

0b90    lda #$06        ; Number of sprites-1 (here I used only 7 sprites)
0b92    sta $0b96
0b95    ldx #$[ff]      ; Load counter
0b97    clc             ; Clear carry for adc
0b98    lda SINPOS,x    ; Stretch sinus position
0b9b    sta $0bd1       ; Set low bytes of indices
0b9e    sta $0bb8
0ba1    adc SINSPEED,x  ; Add stretch sinus speed (carry is not set)
0ba4    and #$7f        ; Table is 128 bytes (twice)
0ba6    sta SINPOS,x    ; Save new sinus position
0ba9    lda YSINPOS,x   ; Get the Y sinus position
0bac    adc YSINSPEED,x ; Add Y sinus speed
0baf    sta YSINPOS,x   ; Save new Y sinus position
0bb2    tay             ; Position to index register
0bb3    lda YSINUS,y    ; Get Y-position from table (can be 256 bytes long)
0bb6    sec             ; adc either sets or clears carry, we have to set it
0bb7    sbc SIZET[1e]   ; Subtract size of the sprite/2 to get the sprite
0bba    clc             ;  to stretch from the middle.
0bbb    tay             ; MaxSize/2 < Y-sinus < AreaHeight-MaxSize/2
0bbc    lda MASK,x      ; Get the ora-mask for this pass
0bbf    sta $0bcb       ; Store mask
0bc2    sta $0bdb
0bc5    ldx #$13        ; 19 lines here + 1 after
0bc7    lda DATA,y      ; Load & ora-mask & store
0bca    ora #[$01]
0bcc    sta DATA,y
0bcf    tya
0bd0    adc STRETCH[1e],x ; Add the stretch from the table (carry is not set)
0bd3    tay
0bd4    dex             ; decrease counter
0bd5    bne $0bc7       ; Do the 19 lines
0bd7    lda DATA,y      ; Load & ora-mask & store the 20th line
0bda    ora #[$01]
0bdc    sta DATA,y
0bdf    dec $0b96       ; Next sprite(s)
0be2    bpl $0b95
0be4    rts

clear 128 bytes: 514  + 12 cycles       8.16 lines
7 passes       : 3820 + 12 cycles       60.6 lines = 8.66 lines/pass

The unrolled clear routine consists of one load (lda #$00) and 128
store instructions (sta $nnnn). 12 cycles are counted for JSR/RTS.

Stretching of 8 sprites would take slightly less than 80 lines, which is one
fourth of the total raster time. Displaying a 128-line high stretcher takes
about 130 lines (counting sprite setup and synchronization), scroller couple
of lines more. Total 212 lines leaves 100 lines (6300 cycles) free for other
activities in a PAL system. In a NTSC system you would have only 50 lines

A simple basic routine to create the stretch data:
a=0:for f=0 to 127:a=a+Height*(2+sin(f*PI/64)):poke Table+f,a:
poke Table+f+128,a:a=a-int(a):next f

This will also handle the 'rounding'. Because of this we don't have to
handle fractions in the stretcher routine. The use of a table also gives the
opportunity to have a separate size for each sprite line. The table does
not need to be a sinus, it could have triangle or any other 'waveform' as
long as the minimum value in the table (sprite line size) is 1.

A basic routine to do the size/2 table:
a=0:for f=0 to 19:a=a+peek(Table+f):next f: rem get the size in position 0
for f=0 to 127:poke STable+f,a/2:a=a-peek(Table+f)+peek(Table+f+20):next f

_Stretcher program_

YSCROLL= $CF00 ; Vertical scroll table (moves bad lines)
STRETCH= $CF80 ; Stretch table
COLORS=  $CE80 ; Table for background colors
YCOORD=  $0380 ; Sprite y-positions (eight bytes)
HEIGHT=  $0388 ; Sprite stretches   (eight bytes)
YPOS=    52    ; Sprite y-coordinate
SPRCOL=  2     ; Sprite colors

*= $C000

        SEI             ; Disable interrupts
        LDA #$7F
        STA $DC0D       ; Disable timer interrupts
        LDA #<IRQ       ; Our own interrupt handler
        STA $0314
        LDA #>IRQ
        STA $0315
        LDX #$3E        ; We create a sprite to cassette buffer
        STA $0340,X
        BPL LOOP
        LDX #7
LOOP2   LDA #$D         ; Set the sprite image pointers
        STA $07F8,X
        LDA #SPRCOL     ; Set sprite colors
        STA $D027,X
        BPL LOOP2
        LDX #$26
LOOP3   LDA VIDEO,X     ; Init VIC
        STA $D000,X
        BPL LOOP3
        LDX #$7F        ; Create the y-scroll table
LOOP4   TXA             ;  and clear the color table
        AND #$07
        ORA #$10        ; Non-blank screen
        LDA #$00
        STA COLORS,X
        BPL LOOP4
        STA $3FFF
        LDX #23         ; Create a color table
        STA COLORS+8,X
        STA COLORS+32,X
        STA COLORS+56,X
        STA COLORS+80,X
        STA COLORS+96,X
        BPL LOOP5
        JSR CHANGE      ; Init sprite sizes and y-positions
        CLI             ; Enable interrupts

IRQ     LDX #$01
        LDY #$08        ; 'normal' $D016
        NOP             ; Timing
        BIT $EA         ; (Add NOP's etc. for NTSC)
LOOP6   LDA YSCROLL-1,X ; Move the screen (bad lines)      5
        STA $D011                                          4
        LDA COLORS,X    ; Load the background color        4
        DEC $D016       ; Open the border                  6
        STA $D021       ; Set the background color         4
        STY $D016       ; Screen to normal                 4
        LDA STRETCH,X   ; Stretch the sprites              4
        STA $D017                                          4
        EOR #$FF                                           2
        STA $D017                                          4
                        ; (Add NOP for NTSC     +2)
        INX             ; Increase counter                 2
        BPL LOOP6       ; Loop 127 times                 + 3
        LDA #1          ; Ack the raster interrupt       =46
        STA $D019                                        +17(sprites)
        JSR DOSTRETCH   ; New stretch                    =63(whole)

        JMP $EA31

SPRITE  BYT 0,0,0,3,$FB,0,7,$7E          ; An Example sprite
        BYT 0,$35,$DF,0,$1D,$77,0,$B7
        BYT $5D,0,$BD,$83,$7E,$EF,1,$DE
        BYT $BB,1,$78,$AE,3,$70,$EB,0
        BYT 0,$BA,3,$60,$EE,3,$D8,$FB
        BYT 2,$F6,$FE,$83,$BD,$9F,$BA,0
        BYT $37,$EE,0,$3D,$FB,0,7,$7E
        BYT 0,3,$DF,0,0,0,0

        BYT $E0,YPOS,$10.YPOS,$40,YPOS,$C1,$18,YPOS-1,0,0
        BYT $FF,8,$FF,$15,1,1,$FF,$FF,$FF,0,0,0,0,0,0,0,1,10
        ; Init values for VIC - sprites, interrupts, colors

BACK    BYT 0,$B,$C,$F,1,$F,$C,$B   ; Example color bars
        BYT 0,6,$E,$D,1,$D,$E,6
        BYT 0,9,2,$A,1,$A,2,9

        LDX #31            ; Clear the table
        LDA #0             ; (Unrolling will help the speed,
LOOP7   STA STRETCH,X      ;  because STA nnnn,X is 5 cycles
        STA STRETCH+32,X   ;  and STA nnnx is only 4 cycles.)
        STA STRETCH+64,X
        STA STRETCH+96,X
        BPL LOOP7
        STA REMAIND+1      ; Clear the remainder
        LDA #7
        STA COUNTER+1      ; Init counter for 8 loops
        LDA #$80
        STA MASK+1         ; First sprite 7, mask is $80
COUNTER LDX #$00           ; The argument is the counter
        LDY YCOORD,X       ; y-position
        LDA HEIGHT,X       ; Height of one line (5 bit integer part)
        STA ADD+1
        LDX #20            ; Handle 20 lines
MASK    ORA #$00
        STA STRETCH+2,Y    ; Set a one-bit
        STY YADD+1
        AND #7             ; Previous remainder
ADD     ADC #0             ;  add to the height
        STA REMAIND+1      ; Save the new value
        CLC                ; Take the integer part
YADD    ADC #0
        TAY                ; New value to y-register
        BNE LOOP8
        LSR MASK+1         ; Use new mask
        DEC COUNTER+1      ; Next sprite

        ASL                ; Sprite height changes with 2x speed
        AND #$3F
        TAY                ; 64 bytes long table
        INC CHANGE+1       ; Increase the counter
        LDX #7             ; Do eight sprites
        CLC                ; Use the same sinus as y-data
        ADC #8
        STA HEIGHT,X       ; Sprite height will be from 1 to 3 lines
        ADC #10            ; Next sprite enlargement will be 10 entries
        AND #$3F           ;  from this
        BPL LOOP9
        LDX #7
        LDA CHANGE+1
        AND #$3F
LOOP10  LDA SINUS,Y        ; Y-position
        STA YCOORD,X
        ADC #10            ; Next sprite position is 10 entries from this one
        AND #$3F
        BPL LOOP10

SINUS   BYT $20,$23,$26,$29,$2C,$2F,$31,$34 ; A part of a sinus table
        BYT $36,$38,$3A,$3C,$3D,$3E,$3F,$3F
        BYT $3F,$3F,$3F,$3E,$3D,$3C,$3A,$38
        BYT $36,$34,$31,$2F,$2C,$29,$26,$23
        BYT $20,$1C,$19,$16,$13,$10,$E,$B
        BYT 9,7,5,3,2,1,0,0,0,0,0,1,2,3,5,7
        BYT 9,$B,$E,$10,$13,$16,$19,$1C

Stretching sprites demo program basic loader (PAL)

1 S=49152
2 DEFFNH(C)=C-48+7*(C>64)
4 FORF=0TO31:Q=FNH(ASC(MID$(A$,F*2+1)))*16+FNH(ASC(MID$(A$,F*2+2)))
100 DATA 78A9648D1403A9C08D1503A23EBD96C09D4003CA10F7A207A90D9DF807A9029D, 3614
101 DATA 27D0CA10F3A226BDD5C09D00D0CA10F7A27F8E0DDC8A290709109D00CFA9009D, 3897
103 DATA 67C15860A201A008EAEAEA24EABDFFCE8D11D0BD80CECE16D08D21D08C16D0BD, 4699
104 DATA 80CF8D17D049FF8D17D0E810E0EE19D02014C14C31EA00000003FB00077E0035, 3394
105 DATA DF001D7700B75D00BD837EEF01DEBB0178AE0370EB0000BA0360EE03D8FB02F6, 3628
106 DATA FE83BD9FBA0037EE003DFB00077E0003DF00000000E834203450348034B034E0, 3015
107 DATA 3410344034C118330000FF08FF150101FFFFFF00000000000000010A000B0C0F, 1859
108 DATA 010F0C0B00060E0D010D0E060009020A010A0209A21FA9009D80CF9DA0CF9DC0, 1876
109 DATA CF9DE0CFCA10F18D4DC1A9078D35C1A9808D45C1A200BC8003BD88038D51C1A2, 4314
110 DATA 14B982CF09009982CF8C5AC1A900290769008D4DC14A4A4A186900A8CAD0E24E, 3430
111 DATA 45C1CE35C110CDA9000A293FA8EE68C1A207B99EC14A4A1869089D880398690A, 3474
112 DATA 293FA8CA10ECA207AD68C1293FA8B99EC19D800398690A293FA8CA10F1602023, 3622
113 DATA 26292C2F313436383A3C3D3E3F3F3F3F3F3E3D3C3A383634312F2C292623201C, 1654
114 DATA 191613100E0B09070503020100000000000102030507090B0E101316191C0000, 296
200 DATA END,0

Uuencoded C64 exutable for stretching sprites (PAL)

begin 644 stretch.64
size 1362


Rob Hubbard's Music: Disassembled, Commented and Explained

by Anthony McSweeney (

[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.]


  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
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

- 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 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

- 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'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

  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
  sta tempstore
  adc tempstore    ;now music num*6

- lda songs,x      ;copy ptrs to this
  sta currtrkhi,y  ;music's tracks to
  inx              ;current tracks
  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


;music off

musicoff =*

  lda #$c0         ;flag music off
  sta mstatus

;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
  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

;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

;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 =*

  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)

  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 =*

  lda ($04),y      ;get pitch of note
  sta notenum,x
  asl              ;pitch*2
  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

  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

;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              ;instr num*8
  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
  adc instr+0,y    ;add the pulsespeed
  pha              ;to the pulse width
  lda instr+1,y
  adc #$00
  and #$0f
  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
  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
  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
  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 =*


;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


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

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

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


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


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



ZPM3 and ZCCP Enhancements for CP/M Plus from Simeon Cran

by Randy Winchester (

Operating System Components

The CP/M Plus operating system consists of three modules.  The CCP (Console
Command Processor), is the part of CP/M that you see when you first boot the
system.  The CCP prints the A> disk prompt, accepts user input, and loads
commands from disk.

The BDOS (Basic Disk Operating System) handles the CP/M functions of disk,
console, and printer input/output, and the tasks of file management.

The BIOS (Basic Input Output System) does the real input/output work for the
BDOS. The BIOS contains the code customized for the CP/M hardware that you're
using. On the C128, the BIOS contains the routines for driving the 40 and 80
column screens, using the REU as a RAM drive, and reading/writing several
different disk formats on 1571 and 1581 drives.  The BIOS can be thought of as
a collection of device drivers that are specific to your computer.

What's New - BIOS-R6

BIOS-R6 (C128 BIOS modified by Randy Winchester and others) is the latest of
the modified versions of the C128 CP/M BIOS.  Most of the changes to the BIOS
result in faster processing speed. For example, all the code for driving a 40
column screen has been removed.  Almost everyone using CP/M is going to be
using it in 80 columns anyway.  Cutting this code takes a big load off the
system and increases overall speed by about 15%.  Similarly, the interrupt
driven RS232 has been set from 300 to 75 baud.  The higher the baud rate, the
more processor time is required to service RS232.  Since the RS232 code is
always running, decreasing the baud rate frees up cycles that the processor
needs to service RS232.  This doesn't affect the operation of terminal programs
which explicitly set the baud rate when they start up.

Other features of BIOS-R6 include a screen dump function, commented source to
assist the programmer in producing customized systems, and support for
additional disk formats.  Some of the new disk formats include Commodore's
standard 1581 CP/M format, MAXI 71 (398K on 5.25" disks), and GP 1581 (796K on
3.5" disks).

C128 CP/M programmers who want to add or change operating system features
should try to make changes to the BIOS.  For one thing, BIOS source code is
available, but not available for the BDOS or CCP.  (Source code is not
available for the BDOS and CCP replacements mentioned in this article either). 
Another reason is that the BDOS and CCP are intended to be "invariable"
operating system components - that is, they are identical for different
computers that run CP/M Plus.  A study of the BIOS source code will reveal
segments of code that can be removed if they aren't needed, and will provide
hints as to new features that can be added.

The distribution package, BIOS-R6.LBR includes documentation, source code,
utilities, and support files.  BIOS-R6.LBR also contains the latest version of
ZPM3. [Ed. Note: The files mentioned in this article can be found via
anonymous FTP or via the mailserver through the "psend" command.]

ZPM3 Features

ZPM3 is a replacement BDOS by Simeon Cran.  Since the BDOS is supposed to be
"invariable," why would anyone want to replace it? The answers to that are
pretty typical - bug fixes, speed enhancements, and new features!  ZPM3
interacts with the BIOS and CCP in most of the same ways as the standard
Digital Research BDOS, and for the most part appears to be a clone of the
standard BDOS.  The standard BDOS was coded in 8080 assembly to make it
compatible with machines that use the older slower 8080 processor.  Very few
(if any) CP/M Plus machines used the 8080. ZPM3 is coded in faster, compact Z80
assembly language, for the Z80 processor that is at the heart of most CP/M Plus
computers (including the C128).

The ZPM3 documentation details fixes to several bugs that have plagued CP/M
Plus since day one.  Although the bugs sound somewhat obscure, there's no
telling when one might cause problems.

ZPM3 is much faster than standard CP/M Plus.  The increased speed should be
obvious after using it for a short time.

The new features offered by ZPM3 are remarkable.  Three closely related
features are enhanced command line editing, a history buffer that stores and
recalls multiple commands, and Automatic Command Prompting.  These features
work in concert to provide a flexible and convenient command line interface. 
Command line editing now has 20 control key functions for moving or deleting by
characters or whole words.  The most recent command lines (up to 250
characters) are stored in the history buffer, and can be recalled and reused,
or reedited if necessary.  Automatic Command Prompting is best appreciated if
seen in action.  It's similar to command line completion in Unix, except that
it's automatic, with matching responses coming directly from the history
buffer.  If you've recently entered a long command line with lots of options,
and need to reuse it (or edit it slightly first), typing the first few unique
characters will bring back the entire command from the history buffer if it's
still intact.  Automatic Command Prompting is so radical that it might take
some getting used to. If you don't think you can get used to it, it can be shut

The latest version of ZPM3, ZPM3N08.ARK, is included inside BIOS-R6.LBR, and
can also be found as a separate file.

ZCCP Documentation, Version 1.0

The remainder of this article will describe ZCCP and how to configure a system
disk to get a fully functional ZPM3/ZCCP system up and running.  BIOS-R6 and
ZPM3 both come with enough documentation to keep you busy for hours, but ZCCP
has never been distributed by itself, because up until this article, there has
not been any documentation for it.  Most of the documentation that follows was
figured out through experimentation and later verified by Simeon Cran.

ZCCP Features

This documentation is provided to assist the user in getting a ZCCP system up
and running.  It is not an exhaustive course on Z- System or ZCPR.  The
following list details which ZCPR features are provided with ZCCP, and which
ones aren't.

    * ZCPR 3.3 compatibility.  ZCCP can run a wide range of utilities an
    applications created for ZCPR 3.3 and ZCPR 3.4.

    * TCAP.  A Z3T termcap file describing terminal characteristics can be
    loaded into the system.  Z-System programs make use of the TCAP for output
    to the screen - a big improvement over the old method of patching
    individual programs with terminal control codes.  TCAP files are loaded by
    the ZCCP LOADSEG command.

    * Named directories.  User areas can be assigned names.  Up to 12 user
    areas can be assigned names.  Named Directory Registers (*.NDR files) are
    loaded by the ZCCP LOADSEG command.

    * Command Search Path.  ZCCP will search for commands along a user defined
    search path.  Up to six path elements (directories) can be defined.

    * Environment block.  Contains TCAP, Named Directory, and Path information. 
    Also includes a map of active disk drives and other system information. 
    The environment block can be viewed with the Z-System SHOW utility.

    * Flow control.  Conditional processing for batch files.  Relies on
    Z-System IF.COM for setting the flow state.  Other flow control commands
    (FI, ELSE, XIF, OR, AND) are resident.

    * Multiple commands can be entered on the command line.  The command line
    buffer will hold up to 225 characters.  Commands should be separated by

    * Extended Command Processor.  If a command is not a built-in flow command,
    resident command, or located on disk along the search path, the command
    line is passed to an extended command processor.  A typical extended
    command processor is ARUNZ, a sophisticated batch file executor with alias
    features.  To use a program as an extended command processor, rename it to
    CMDRUN.COM and place it in the ROOT directory of your boot disk.

    * Error handler.  In the event that the extended command processor can't
    handle a command, control is passed to an error handler.  Error handlers
    give information about the error (instead of the useless CP/M "?" message)
    and allow the command line to be edited and reused.

    * Resident commands.  The following commands are built in: 
      CLS  - clears the screen 
      NOTE - text following the NOTE command is treated as a comment.
      FI   - Flow control:  terminate the current 
      IF level ELSE - Flow control:  toggle the flow state
      XIF  - Flow control:  exit all pending IF levels 
      OR   - Flow control:  OR IF tests to set flow state 
      AND  - Flow control:  AND IF tests to set flow state

    * Shell stack.  Up to four shell levels can be defined.  Z-System provides
    a choice of several different shells.  Applications such as terminal
    programs and word processors can also be assigned shell status.

    * ZCCP uses the LOADSEG command for direct loading of RSX files that have
    not been GENCOMed.  Example: LOADSEG SAVE.RSX loads SAVE.RSX.

    There are some things that Z3Plus will do that ZCCP won't do.

    - ZCCP does not support a Flow Command Package (FCP).  It relies on the
    transient IF command.  Other flow commands (FI, ELSE, XIF, OR, AND) are
    resident in ZCCP.

    - A Resident Command Package (RCP) is not implemented.  CLS and NOTE are
    resident in ZCCP.  All other commands must be loaded from disk.  This isn't
    as much of a handicap as it might sound if you have a fast RAM drive, such
    as a CBM 17xx REU, Quick Brown Box, or RAMLink.

    - ZCCP can not load type 4 programs (used with ZCPR 3.4).  It loads
    standard COM files at 100H, and type 3 programs that load higher in memory. 
    Most type 4 programs have type 3 or COM equivalents.

    - ZCCP can not reexecute loaded programs.  This trick is usually performed
    on Z-Systems with a GO command that jumps to 100H. Since ZCCP also loads at
    100H, a GO command would only restart ZCCP.

The Files

Three files are included in ZCCP.ARK:

 File name      Size  Description
 ============   ====  ==========================================
 CCP     .COM   3k    ZCCP replacement for CCP.COM
 LOADSEG .COM   3k    Loader for named directories and termcaps
 ZINSTAL .ZPM   1k    Segment containing environment information

Getting Started - Preparing a Boot Disk

Format a Commodore CP/M format 5.25 or 3.5 inch disk.  ZCCP must be booted from
device 8 (CP/M drive A).

Copy the files from ZCCP.ARK to user area 0 of the newly formatted disk.

Copy CPM+.SYS to user 0 of the boot disk.  The CPM+.SYS must have been
generated using the BDOS segments from ZPM3.

Locate a copy of a Z-System alias utility.  A good one is SALIAS16, although
others should work also.  Copy it to user 0 of the boot disk.

At this point, hit the reset switch and boot the system with the new disk. 
After the system boots, you won't be able to do much with it.  The only
resident commands are CLS and NOTE, and ZCCP can only locate commands if they
are prefixed with the drive and user number.

The next step is to create a startup alias.  When ZCCP boots, it looks for a
file named STARTZPM.COM and executes commands from it.  STARTZPM.COM is created
with a ZCPR alias utility.  Here is a listing of a STARTZPM.COM created with



     15:                ; Logs the ROOT directory (A15) on the
                        ; current drive.

     QD F/F             ; Installs Quick Brown Box ramdisk driver.

                        ; LOADSEG loads the Named Directory Register
                        ; and TCAP.
                        ; Directories can now be referred to by
                        ; name, as in the next command:

     SETPTH10 /C COMMANDS REU 1581 $$$$ $$0 ROOT
                        ; SETPTH sets the command search path.
                        ; The /c option first clears any existing path.
                        ; Directories are then listed in the
                        ; order searched.  In this case, COMMANDS
                        ; is a 64K QBB ramdisk (drive/user F0) where
                        ; frequently used commands are stored.  REU is
                        ; a 1750 REU (drive/user M0).  1581 is a 1581
                        ; drive, (drive/user C15) where some 700K
                        ; of utilities and applications are
                        ; located.  $$$$ refers to the currently
                        ; logged drive and user area.  $$0 refers
                        ; to user area 0 of the current drive.
                        ; The ROOT directory is on drive A, user
                        ; 15, where startup utilities and system
                        ; files can be found.

     1571 [AB           ; This speeds up 1571 disk drives A and B
                        ; by shutting off the redundant write verify.

     AUTOTOG ON         ; Turns on keyboard control of ZPM3 Auto
                        ; Command Prompting.  Auto Command
                        ; Prompting is toggled by entering CTRL-Q.

     COMMANDS:          ; Logs the commands directory.

     IF ~EXIST CP.*     ; Test to see if commands are loaded.
                        ; This line reads:  "If the CP command
                        ; does not exist . . ." and sets the flow
                        ; state to true if the file doesn't exist.
        QD I/F          ; ". . . then initialize the QBB . . ."
        C1:CP C1:*.* F0:
                        ; ". . . copy all of the commands in
                        ; drive/user C1 to the commands (F0)
                        ; directory . . ."
     FI                 ; ". . . end if."

     ROOT:              ; Log the root directory (A15).

     CP C:ZF*.* M0:     ; Copy ZFILER.COM and ZFILER.CMD to the
                        ; REU directory (M0).

     VERROR             ; Install VERROR error handler.

     DATE S             ; Set the system time and date.

     ZF                 ; Invoke ZFILER as a shell.


Of course, your STARTZPM alias will vary depending on the hardware you need to
support, your software preferences, and your work habits.  This alias is close
to the upward size limit that ZCCP can handle based on the capacity of the
multiple command buffer.  At the very least, I recommend an alias that will set
up a search path and load a TCAP.

Actually, I put the cart before the horse in this example.  If you try to
reboot your system with the LOADSEG command as listed, you'll notice that you
don't have a NAMES.NDR file.  There isn't one distributed with ZCCP either. 
Z-System utilities won't let you edit the NDR either, since the buffer for it
hasn't been created yet.  This turned out to be a nasty chicken/egg situation,
hopefully solved by the inclusion of a sample NAMES.NDR file containing simply

At this point, you should have a mostly functioning ZCCP system disk.  Press
reset and boot it up.  You might want to correct any problems with it or tweak
it to perfection before moving on.

List of Z-System Utilities for ZCCP

Some of the following utilities are essential, others are nice to have.  The
version numbers listed are the latest known versions at the time that this
documentation was written.  Utilities can be found on ZNode BBSs, and some of
them are available on Simtel20 or its mirror sites.  Some of the more important
utilities will be uploaded to

         SALIAS16  - already mentioned in the example above.  SALIAS (or one of
         the other ZCPR alias utilities) are essential.

         ARCOPY    - not a ZCPR utility, but one of the best CP/M file copiers

         SD138B    - excellent DIRectory utility.  SD offers many different
         types of sorts, list formats, etc., displays date stamps, and supports
         output to a file.

         MKDIR32   - utility for manipulating directory names and Named
         Directory Register (*.NDR) files.

         ERASE57   - erases files.

         ZFILER10  - a file management shell that can launch applications. It
         is programmable in that it can execute user defined macros from a
         file.  Multiple files can be "tagged" and operated on by other
         programs.  ZFILER is an excellent program, sort of a GUI desktop
         without the slow graphics.

         C128-XGR  - a library of eXtended GRaphics termcaps for the C128. This
         file is essential if you want to use any ZCPR programs that need a
         TCAP.  These termcaps are the first for the C128 that implement
         character graphics, standout mode, and control of blinking reverse,
         and underline modes.

         SETPTH10  - used to set the command search path.  Essential!

         VERROR17  - error handler that displays the command line for
         reediting.  VERROR17 is the only error handler that I found that works
         with ZCCP.

         ZEX50     - Z-System EXecutive is a powerful batch file processor that
         replaces the CP/M SUBMIT command.

         LBRHLP22  - Z-System Help utility displays help files.  Help files can
         be crunched (*.HZP), and/or loaded from a HELP.LBR library.

         ARUNZ09   - runs an alias script from a text file.  ARUNZ is
         frequently used as an extended command processor.  To use ARUNZ (or
         any other executable utility) as an extended command processor, rename
         it to CMDRUN.COM.

         VLU102    - Video Library Utility views or extracts files from
         libraries.  Versions of VLU above 1.02 do not work reliably with

         Z33IF16   - is the IF.COM discussed in the section on flow control.

         SHOW14    - displays an immense amount of information about your
         Z-System.  SHOW also includes a memory patching function.

         ZCNFG24   - configures Z-System program options.  Most Z-System
         programs are distributed with a configuration (*.CFG) file that
         produces a menu of configuration options when run with ZCNFG.

         ZP17      - Z-System Patch utility edits files, disk sectors, or
         memory, and includes a built-in RPN calculator and number base

         ZMAN-NEW  - This is a manual describing Z-System features in depth. 
         It is based on earlier versions of Z-System, and is a little dated,
         but otherwise contains information that you won't find anywhere else. 
         Not everything in the manual applies to operation of ZPM3/ZCCP, but
         with the documentation presented here, you should be able to get a
         good idea of what works and what doesn't.

ZCCP Technical Notes

ZCCP is a replacement CCP that implements ZCPR 3.3.  It loads at 100H and is
stored in the bank 0 CCP buffer for fast reloading as does the standard CCP. 
By contrast, Z3Plus loads into high memory and can be overwritten by transient
commands, requiring reloading Z3Plus from disk.  Because ZCCP replaces the CCP,
a ZCCP system has more TPA (transient program area) than a Z3Plus system.  A
ZCCP system on the C128 has more than 57K of TPA, almost the same amount as a
standard C128 CP/M system.
This should be enough information to get started with ZPM3/ZCCP. Set up a boot
disk, experiment with some Z-System utilities, read ZMAN-NEW, and get some
applications running.  You'll agree that ZPM3/ZCCP breaths new life into CP/M.


Multi-Tasking on the C=128 - Part 1

by Craig Taylor (

I.    Introduction / Package Over-view..

 This article will detail the multi-tasking kernal which I have written butt
 is still in the debugging stage . The documentation is being released now as
 C= Hacking has been delayed for a month while this article and a few others
 were in the process of being shaped. The source code listings, binaries, and
 a few sample programs will be in the next issue of C= Hacking as well as
 available on the mailserver and on R. Knop's FTP site when they are

 The Commodore 128 does not support TRUE multi-tasking in that the processor
 handles swapping from task to task. Rather the package will make use of the
 interrupts occuring sixty times a second to determine when to switch tasks..
 The Commodore 128 greatly simplifies things as in addation to the interrupts
 it also has the provision to relocate zero page and the stack page. So the
 package basically works by intercepting the IRQ vector, taking a look at the
 current job, saving the stack pointer, finding the next active job, loading
 the stack page and registers and resuming the normal IRQ as if nothing had
 ever happened.

 Unfortunatly Commodore never thought of having multiple programs in memory
 executing at any given time. Hence, problems will occur with file accesses,
 with memory contention, and with an over-all slowdown in speed. The package
 will detail how to handle device contentions, but it's recommended that
 programmers make use of the C= 128 kernal call LKUPLA $ff59 containing the
 logical file number they wish to use in .A; if the carry flag is set upon
 return then it is safe to use, else find another one as another program is
 using it. However, note that if you have multiple programs doing this then
 you may have problems with one grabbing a logical file number after the
 other process has checked for it. Multi-tasking is fun 'eh?  Problems like
 this will be examined when we get into semaphores later in this article..

 Craig Bruce's Dynamic Memory Allocation article in the second issue of C=
 Hacking should provide a very strong basis for a full-blown memoryy manager.
 With minor modifications (basically just changing the initial allocations so
 that the package is not killed) it should be able to work.  Also it will need
 changes to make sure that processes don't try to allocate at the same time.
 So a memory manager is not too much of a problem. Details of what changes
 will be necessary shall be in the next issue.

 What is a process? What is a program? I've been using the terms almost
 inter-changebly throughout this article at this point. Basically I'm calling
 them the same. A process, or program is defined as a program with it's own
 executable section, it's own data sections, and it's own stack and zero page.
 (Note, however, that the multi-tasking package does not support relocation of
 the zero page although this is likely to change).  The "kernal" of the
 multi-tasker is basically that part of the package which governs which
 process is executed or switched to next. Semaphores will be examined in
 detail later; they function as flags for processes too know when it is safe
 to execute something, and serve as signals betweenn them.

 Future versions of the package, (even though I know it does not exist out
 side of my house yet), will support pipes and a more strongly typed kernal
 so that processes may be prioritized.

II.   A Look At Multi-Tasking

 The introduction introduced some basic elements of multi-tasking but I'll
 repeat them here, defining them so that this article can be clear as some of
 the concepts can get a bit confusing.

    Background - A process is said to be in the "backgr ound" if it is not
    the foreground task and may or may not have input devi ces associated
    with it.

    Foreground - A process is said to be "foreground" if it is the main
    active process and is holding the keyboard and screen display captive
    (ie: the user is actually working within it).
    Kernal  - A small section of code that performs low-leval work that is 
    needed by any programs in memory..

    Multi-Tasking - Execution of more than one process at any given

    Priority - A value associated with each process that determines how
    often, and possibly when a process is executed.

    Process - The space in memory taken up by executable program code, any
    associated data, the stack and the registers associated and currently in
    use by it, including the current PC (program counter)..

    Semaphores - Values that are globally accessed by processes to share and
    communicate information between each other and the kernal.

 Some CPU's have available a multi- tasking mode (the 386 and 486 are the
 most famaliar ones that come to mind), y et the 8502 chip contained inside
 the Commodore 128 was first designed before 1985 and lacks multi-tasking. It
 would be nice if such a multi-tasking CPU in the 6502 family did exist but
 it would also create problems with the 6502 style architecture and wouldd
 produce severe compatibility problems.

 So how is the C=128 supposed to do multi-tasking? Well, we'll "simulate"

 Basically if we had two programs in machine language:

            Program 1:                            Program 2:
          - lda #65    ; the "A" character     - lda #64    ; the "@" character
            jsr $ffd2  ; print it                jsr $ffd2  ; print it
            jmp -                                jmp --

 And we wanted them to multi-task we'd expect something like the following:


 It's unlikely that you'll get that in many multi-tasking environments,
 even non-simulated ones. Since we're only going to be switching tasks every
 1/60 of a second then we're more likely to see an output similair to this:


 So that it seems a process will run for about 1/60 of a second beforee
 switching to the next one.
 We run into problems however. The KERNAL in the C128 that contains most
 off the file handling, screen manipulations, and keyboard input routines. It
 was never designed with the idea of multi-tasking in mind. So we're gonna
 have code running in the KERNAL in two spots for the two differant processes
 and it's more than likely we'll end up with something like:

@@@@@@@@<and then a BRK or some strange error or never-never-land>>

  There's got to be some way to fix it - There is - It's called a semaphore..

 A semaphore is a value that is checked before access is granted to another
 group of memory locations. The semaphore is basically requested via the

        request_semaphore       sei
                                ldx semaphore
                                beq +
                              - ldy #$ff
                                bne -
                              + inc semaphore

 Now the request_semaphore has to disable interrupts to prevent another task
 from changing the semaphore value before this routine has had a chance. The
 actual code for the request_semaphore will be very similair to the above.

 Using a similair routine that performs the opposite - setting the semaphore
 to a zero value when finished we can dictate what program has control over
 what device or what memory areas.

 The semaphores will be used to govern access to the KERNAL routines which
 manipulate the locations in zero page etc, they'll also be used to manage the
 memory manager when it is implemented as it'd be awkward for it to allocate
 the same block of memory to two or more processes.

III.  Multi-Tasking Function Calls (Package Calls)

  OffSet | Name          | Notes
   $00   | SetUp         | .C=0 Init Package, .C=1 Uninstall Package
         |               |      (including Kernal re-direction).
   $03   | SpawnProcess  | On return: .C=0 parent, .C = 1 child
   $06   | ChangePriority| .A = new foreground priority, .X = new background
   $09   | KillThisProc  | Kills Calling Process (no return)
   $0c   | KillOtherProc | Kills Process # .A
   $0f   | RequestSemaph | Requests Semaphore #.X
   $12   | ReleaseSemaph | Releases Semaphore #.X
   $14   | GetProcInfo   | Returns Process Information, Input=#A
         |               |   of 0: Process Id
         |               |   of 1: Process Foreground Priority
         |               |   of 2: Process Background Priority
         |               |   of 3+ Other Information To Be Decided Later
   $17   | PipeInit      | .AY - Address of Pipe, .X = Size/8
         |               |    Return: .X - Pipe #
   $1a   | WritePipe     | .AY - Address of Null Term. Data, .X = Pipe #
   $1d   | ReadPipe      | .AY - Address to Put Data .X=Pipe #
   $20   | ....          |  \
   $2e   | ....          |   \ More Routines for the future.

IV.   Availibility of the Packagee

The package should be available at the time of the next issue. A further
examination of how the routines work shall be examined along with the source

Errors popped up in developing it and rather than delay C= Hacking any
further I decided to go ahead and release the above information so that
individuals can start developing appropriate routines. In addition,
please note that PIPEs _may_ or may not be supported in the next issue.
I have not fully made up my mind yet on them.

V.    Referencess

  Born to Code in C, Herbert Schildt, Osborne-McGraw Hill, p.203-252.

  Notes from Operating Systems Course, Pembroke State Univ, Fall '92.


LITTLE RED READER: MS-DOS file reader/WRITER for the C128 and 1571/81.

by Craig Bruce  (


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

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.


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.


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

   ---  -  ---  ---  --------  ---  ------
     1  *  ASC  SEQ  HACK4     TXT  120732
     2     BIN  PRG  RAMDOS    SFX   34923


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.


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

   ---  -  ---  ---------------- -  ------
     1  *  BIN  LRR-128          P    9876
     2     ASC  COM-HACKING-005  S  175412


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.


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:

----------------       ---------------
"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

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.


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:

   ------     ---     -----------
     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


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.


The subroutine call interface to the file copying package is summarized as

   -------     -----------
   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:

   -------     -----------
   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


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


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.


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.


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".


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


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.


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.


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.


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.


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


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

% 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

% 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

% ;====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

%    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

% 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

%    ;** 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

% 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

% 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

% +  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 )


%    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 = *
% trackbuf   = bss
% clusterBuf = trackbuf+4608
% fatbuf     = clusterBuf+1024
% dirbuf     = fatbuf+1536
% end        = dirbuf+4096


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


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
begin 640 lrr.bin


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.


In the Next Issue:


This article will examine how a two-key rollover mechanism would work for the
keyboards of the C-128 and 64 and will present Kernal-wedge implementations
for both machines.  Webster's doesn't seem to know, so I'll tell you that this
means that the machine will act sensibly if you are holding down one key and
then press another without releasing the first.  This will be useful to fast
touch typers.

The Second Rob Hubbard Music Routine

  In this article, the second Rob Hubbard music routine will be presented in
the same way as the first. Future issues will hopefully examine various other
music routines including various Martin Galway, Benn Daglish, Jeoren Tel,
and Manaics of Noise routines. Note: Unfortunately the author completes
university (and thus loses internet access) in August 1993.

DYCP - Horizontal Scrolling

DYCP - is a name for a horizontal scroller, where characters go smoothly
up and down during their voyage from right to left. One possibility is a
scroll with only 8 characters - one character per sprite, but a real demo
coder won't be satisfied with that.

Multi-Tasking on the C=128 - Part 2

This article will examine the actual code that makes up the multi-tasking
kernal in detail and include some example programs explaining it use.

The 1351 Mouse Demystified

This article will explain how the 1351 mouse works as well as provide an easy
to use interface in machine language for both basic and machine language
magazines/chacking5.txt · Last modified: 2015-04-17 04:34 by