User Tools

Site Tools


magazines:chacking2

C= Hacking #2

     CCCCCC       HH    HH    AAAA    CCCCC KK  KK IIIIII NN   NN  GGGG
    CC   ====     HH    HH  AA    AA CC     KK KK    II   NNN  NN GG
    CC   ===      HH    HH  AA    AA CC     KKKK     II   NN NNNN GG
    CC            HHHHHHHH  AA    AA CC     KKKK     II   NN NNNN GG GGG
    CC   ===      HH    HH  AAAAAAAA CC     KK KK    II   NN  NNN GG   GG
    CC   ====     HH    HH  AA    AA CC     KK KK    II   NN   NN GG   GG
     CCCCCC       HH    HH  AA    AA  CCCCC KK  KK IIIIII NN   NN  GGGGG

                    Volume 1 - Issue 2 - April 22, 1992
==============================================================================

Editor's Notes:
by Craig Taylor (duck@pembvax1.pembroke.edu)

   Eeegh! - When I first started this I never realized how much work it'd be.
I'm glad of the reception it's gotten from the Commodore community at large. I'd
like to thank each of the author's in this issue and last for their work they've
put into it as well as their time. 

  Please note that all files, documentation etcetera associated with C= Hacking
and whatnot contained within are also now available at tybalt.caltech.edu via
anonymous ftp under the directory /pub/rknop/hacking.mag.  Any updates to files
contained within or corrections will be posted there as well as mentioned
here.  Currently it has the correct 1st issue and (soon to be) 2nd issue.  Also
Robert Knop's file bmover.sfx is there for the Banking Geos article in this
issue.

  It seems as if we're averaging about 2 months for each issue and hopefully 
we'll keep that rate during the summer but due to an internship (I'll hopefully
get) I may not have net access during the summer.  In that case it'll be delayed
until after I get back to school in the fall.

  Also, if you've got any ideas for articles or have written a program that is
unique that you'd be interested in documenting and p'haps letting other people
see some of the tricks of the trade then please send any queries to 
duck@pembvax1.pembroke.edu.  

****************** WARNINGS, UPDATES, BUG REPORTS, ETC... *********************

  Please note that in the last issue when the undocumented opcodes were 
discussed that they are _VERY NON-STANDARD_.  And any future accelerator boards
for the C=128 or C=64, in all likelehood, _will not work_. Zip's board [when are
they ever gonna finish it?] for the C=128 will be based on a similair processor
to the 8502 and is practically guarenteed not to work with the undocumented 
op-codes.  If you plan to release any ML programs for general use PLEASE be 
aware that they may be in-compatible with future systems.

============================================================================

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

  *** AUTHORS LISTED BELOW RETAIN ALL RIGHTS TO THEIR ARTICLES ***

============================================================================

  In this edition we've got the following articles:

Learning ML - Part 2

  Yes, the infamous learning machine langauge tutors by Craig Taylor 
(duck@pembvax1.pembroke.edu).  In this session we examine indexed addressing
and it's usefulness in printing strings of characters.

8563 : An In-Depth Look

  This article documents and details the 8563 in-depth.  It covers all 
available registers, documents their functions and suggested methods of getting
certain effects.  Also covers how to read and write to 8563 registers as well
as read the 16k or 64k of memory that contains the VDC char-set, screen memory
etc.  Written by Craig Taylor (duck@pembvax1.pembroke.edu).

The Poor Man's Way to Getting Files from MS-Dos Diskettes

  Now there's a way to transfer files of any length from MS-Dos diskettes using
a public domain program that will only read files of 43k or less and a IBM
program to split the files up.  There are better ways, but if you don't want
to pay for Big-Blue Reader this is one method to go.  Written by Mark Lawrence
(9152427D@Levels.UniSa.Edu.Au).

Banking on Geos

  GEOS 128, being an extended and expanded version of GEOS 64, provides a 
contiguous block of application space in a single RAM bank.  The "standard"
programming documentation makes few references to the use of other banks in 
GEOS.  This article describes accessing other RAM banks (including RAM banks 2
and 3 for 256K expanded 128's) under GEOS128, using both the GEOS Kernal
routines and more direct methods.  By Robert Knop (rknop@tybalt.caltech.edu).

Dynamic Memory Allocation

  Written by Craig Bruce (csbruce@ccnga.uwaterloo.ca) this article examines
how to implement and use dynamically allocated memory that will allow your
programs to better utilize all of the available memory on your C=128, including
expansion memory.  These routines are extracted from the Zed-128 program which
is a text editor that can edit 600KByte files on a 512K expanded 128.

=============================================================================

Beginning ML #2

by Craig Taylor (duck@pembvax1.pembroke.edu)

  Last time we introduced the definition of what exactly Machine Language / 
Assembly Language is along with an example of clearing the screen in Machine
Language.

  Now, in this issue let's print my name (and later your name). Looking at the
code from last time the following assembly source jumps to mind:

------------ 
print_1.asm:

          lda #147         ; clr/screen code
          jsr $ffd2        ; print
          lda #'C'         ; code for ascii "C"
          jsr $ffd2        ; print
          lda #'r'    
          jsr $ffd2
          lda #'a'
          jsr $ffd2
          lda #'i'
          jsr $ffd2
          lda #'g'
          jsr $ffd2
          lda #32          ; code for space 
          jsr $ffd2
          lda #'T'         ; print my last name....
          jsr $ffd2
             .
               .            (ad naseum...)    
                 .        
          rts
----------

  Now, for short strings like "HI!" that might be fine but if your name is 
something like "Seymour Johnson the third" it can get a little bit ridiculous
in terms of the amount of memory and the amount of typing (eegh! - typing!) 
involved. There's an easier way.
  
  It's called indexed addressing. What is this you say? Let's first take a 
look at the above program using indexed addressing and then explain it.

------------ 
print_2.asm

          ldy #$00
   loop   lda string,y
          jsr $ffd2
          iny
          cpy #numchars
          bne loop
          rts

 string   .byte 147
          .ascii "Craig Taylor"

 numchars = *-string

------------

  Hmm, looks a little bit confusing 'eh?

  What we're doing is using the register y to act as a pointer to grab the 
y'th character in what is at memory location STRING. If y is 0 then we'll get
string+0 which happens to be a 147.

  The .byte and .ascii directives are not real instructions the computer
understands. What happens is that your assembler sees that you want data
put at those locations so it convert 147 and "Craig Taylor" to numbers
and puts them at the proper locations, relieving you the burden of doing it.

   The numchars = *-string looks confusing at first... obviously, numchars
stands for the number of characters we need to print out but how is it being
figured?  Most assemblers keep the current location in memory it's assembling
to in something called a program counter of PC.  Most assemblers also will let
you have the value at assembly time by referencing it using the "*" symbol.
"string" is already a symbol that has been set an address in memory and after
assembling the .byte and .ascii instruction "*" will be equal to the next 
address that the assembler would put any instructions at, had we had any.
Now, *-string basically is saying to the compiler, look, take the current 
program counter (where it's assembling to) and subtract it from where the 
symbol string starts at (which it just assembled a while back). This should
be then, our number of characters we have.

WALK-THROUGH:

  Register Y is initially set to zero in the first instruction, as we want to
begin with the first character. The first character is at string+0, not 
string+1. This may seem a little bit odd at first, but try thinking of it this
way:

     Take, for example, 3 diskettes. Put them in a row and call the one on the
     left "string" (or some other name). Then point at "string+1", "string+2"..
     Notice there's no "string+3" even tho' there's 3 diskettes?  This may 
     seem a little bit strange at first, but after thinking about it a while
     you'll begin to understand. In machine / assembly language, you typically
     count starting from zero, in the real world, typically from one.

  The lda string,y instruction is telling the computer to reference string as 
if it was an array, get the byte at location string + y, or for you basic
programmers thinking of string as an array of bytes (with no bounds checking)
string[y]. Thus, the accumulator is equal to the yth byte starting from
the location string is defined to be.

  We then call the routine Commodore so graciously provided for us that prints
out the contents of the accumulator. Now, some routines, as we'll see in other
editions, are not so nice and may change the value of the accumulator, the 
x and y registers. However, Commodore was extra nice and the routine at $ffd2 is
guaranteed not to change any of the registers.
 
  The routine then "iny".  What is this? It "INcrements the Y register". INX 
will "INcrement the X register".  The X and Y register can not have any math
performed on them other than increment and decrement operations (ie: adding
one and subtracting one).   The only register that allows addition or
subtraction is the accumulator. However, in this case we just want y to point
to the next character, the next byte so "INY" serves us fine.
  
  We then "ComPare Y" register to the number of characters in the string. Notice
the # sign. If we hadn't have had that there, it would've tried to look at 
whatever memory location numchars was defined for. Numchars was set up to hold
the number of characters in the string, not be a pointer for a memory location.

  Now that we've compared it, we "Branch if the last comparison was Not Equal"
back to where loop is at (in this case, where we load a with character again).

  If it was equal we fall through to the RTS where we return from our little
program.

  Basically, we've got something like the following flowchart:
                  _______
                 / START \
                 \_______/
                    |
                   \|/
                +-----------------+
                | Set Index (Y)   |
                | first character | 
                +-----------------+
                    |
                   \|/
                +-------------------+
                | Get the character | /
                | pointed to by     |<------------------+
                | the index(Y)      | \                 |
                +-------------------+                   |
                    |                                   |
                   \|/                                  |
                +-------------+                         |
                | Print it    |                         |
                +-------------+                         |
                    |                                   |
                   \|/                                  |
                +------------------------+              |
                | Increment the Index(Y) |              |
                +------------------------+              |
                    |                                   |
                   \|/                                  |
                    /\                                  |
                   /= \                                 |
                  /# of\                                |
                 /chars?\                               |
                /to print\___no,not =_____------------->+
                \???     /
                 \      /
                  \    /
                   \  /
                    \/
                     |
                    \|/
                    _____
                   / END \
                   \_____/

  Indexed addressing is used *very* often in assembly language.  Try playing 
with the second program and experiment with it until you understand fully what
is going on.  Next time we'll look at how to access some of the diskette 
routines and to display a file on disk.

===============================================================================

An In-Depth Look at the 8563 Video Chip on the C= 128

by Craig Taylor (duck@pembvax1.pembroke.edu)

  Due to the article in the last issue by Craig Bruce (csbruce@ccnga.uwaterloo.
ca) and some letters from people asking about how the 8563 Video Chip works and
more technical information this article will attempt to present as much detail
as possible on the 8563 chip & it's various capibilities.

  
                            ---------------------  
                            ! Hardware Aspects: !
                            ---------------------

  The following is a physical layout of the 8563 and the available pin outs:

              +------------------+
         42 o_|DD7  VDD    CS DA7|_o 33   DA0-DA7 - Address Bus for Ram
         41 o_|DD6            DA6|_o 32   DD0-DD7 - Data Bus for Ram
         40 o_|DD5            DA5|_o 31   D0 - D7 - Data Bus 8563 / Cpu
         39 o_|DD4            DA4|_o 30   CS /CS  - Chip Selection Pin
         38 o_|DD3            DA3|_o 29   /RS     - Register Select
         36 o_|DD2            DA2|_o 28   R/W     - Data Direction for Data Bus
         35 o_|DD1            DA1|_o 27   INIT    - Initialize internal latches
         34 o_|DD0            DA0|_o 26   DISPEN  - (Unused) Display Enable
              |                  |        RES     - (Unused) Reset all scan cnts
              |                  |        TST     - (Unused) Test purposes only
         10 o_|D7            /CAS|_o 48   DR/W    - Local Dram Read/Write
         11 o_|D6            /RAS|_o 47   /RAS    - Row Address Strobe
         13 o_|D5            DR/W|_o 21   /CAS    - Column Address Strobe
         14 o_|D4                |        DCLK    - Video Dot Clock
         15 o_|D3               R|_o 46   CCLK    - (Unused) Character Clock 
         16 o_|D2               G|_o 45   LP2     - Input for Light Pen
         17 o_|D1               B|_o 44   HSYNC   - Horizontal Sync
         18 o_|D0               I|_o 43   R,G,B,I - Pixel Data Outputs
              |                  |        
              |                  |
          8 o_|/RS               |
          7 o_|/CS               |
          9 o_|R/W           VSYN|_o 20
         23 o_|/RES          HSYN|_o 3
              |                  |
              |              CCLK|_o 1
         25 o_|/LP2        DISPEN|_o 19 
              |                  |
              |               TST|_o 24
          2 o_|/DCLK  VS5    INIT|_o 22
              +------------------+ 
                       !12
                       o


   Taken from Pg. 596-8 C=128 Programmer's Reference Manual Publ. Feb 1986 
   Bantem Books



                         +-----------------------------+
                         | How Commodore Hooked It Up! |
                         +-----------------------------+

  Now, the 8563 is hooked up to the computer via the following method:

     +---------------------+                          
     |                     |             +--------+   +-------+
     |Computer Memory      |             | 8563   |   |16k or |   
     |    (RAM)            |             |        | % | 64k   |   
     |                     |___$d600_____|da0-7   | % |VDC RAM|   
     |                     |             |        | % |       |
     |                     |___$d601_____|dr0-7   | % |(Screen|
     |                     |   ( /rs)    |    d0-7|___| Mem)  |
     +---------------------+             +--------+   +-------+  

  Confusing 'eh? (The %'s represent control signals that also are used).. What 
basically happens is that every time the computer wants to access the 8563 to 
program or change one of it's numerous registers it has to store the register
number to $d600, then loop until the 7th bit of $d600 changes to a 1.  Once
this is done, you can then read or write a value to/from $d601.

  Commodore also employed the MMU (Memory Management Unit) to manipulate pages 
of memory and thus, the 8563 only shows up in the I/O page (usually referenced
as Bank 15 or a value of $00 in the MMU Register at $ff00) or in pages that the 
I/O section of memory is enabled.

  The register at $d600 in the I/O space of the C=128 is laid out as follows:

  Bit Position:
      7       6        5       4       3       2        1      0
      Status  LightPen VBlank  -----Unused---- ------Version #--------

  When a value is placed in $d600 instead of putting the value in Status,
LightPen bits etc, the value reflects which register # is requested. Bit 7 of
this register (Status) will then return a binary 1 when $d601 reflects the 
actual value of the register just poked to $d600. (See the ML routines for
storing and reading values to/from registers at the end of this article). When
a value is first place in this register, $d600 bit 7 is equal to a zero.

  Bit 6, is used to indicate when new values have been latched into the 
lightpen registers (16-17). Bit 5, VBlank refers to when the 8563 is in the
period known as "Vertical Blanking Period".  Usually, however this bit is 
seldom referred to as updating the 8563 is usally too slow to make use of this
for any special effects.  

  Bits 0-2 return a version # of which %000 and %001 are the known versions out.
Early 128's will contain the value of $0 while later 128's will contain the 
value of $1.  Note that there are slight differences between the 8563's, in that
register 25 (horizontal smooth scoll register) requires different settings. 

  The register at $d601 returns the value of register # that has been written
into $d601 (when bit 7 of $d600 = %1). Note that storing a value here will also
do a write into the register # selected. (Refer to the ML routines for storing
and reading values to/from registers at the end of this article for an example).


                              ------------------------
                              | Register Definitions |
                              ------------------------

Reg#     7    6    5    4    3    2    1    0     Description              Notes
------- ---- ---- ---- ---- ---- ---- ---- ----   ------------------------ -----
  0     HzT7 HzT6 HzT5 HzT4 HzT3 HzT2 HzT1 HzT0   Horizontal Total          ^1  
  1     HzD7 HzD6 HzD5 HzD4 HzD3 HzD2 HzD1 HzD0   Horizontal Displayed      ^1
  2     HzS7 HzS6 HzS5 HzS4 HzS3 HzS2 HzS2 HzS0   Horizontal Sync Position  ^1
  3     VSW3 VSW2 VSW1 VSW0 HSW3 HSW2 HSW1 HSW0   Vert/Horiz. Sync Width    ^2
  4     VeT7 VeT6 VeT5 VeT4 VeT3 VeT2 VeT1 VeT0   Vertical Total            ^3
  5     .... .... .... VeA4 VeA3 VeA2 VeA1 VeA0   Vertical Total Fine Adju  ^3
  6     VeD7 VeD6 VeD5 VeD4 VeD3 VeD2 VeD1 VeD0   Vertical Displayed        ^3
  7     VeS7 VeS6 VeS5 VeS4 VeS3 VeS2 VeS1 VeS0   Vertical Sync Position    ^2
  8     .... .... .... .... .... .... Ilc1 Ilc0   Interlace Mode            ^4
  9     .... .... .... CTV4 CTV3 CTV2 CTV1 CTV0   Character Total Vertical  ^5
 10     .... CrM1 CrM0 Css4 Css3 Css2 Css1 Css0   Cursor Mode/ Start Scan   ^6
 11     .... .... .... Ces4 Ces3 Ces2 Ces1 Ces0   Cursor End Scan           ^6
 12     Ds15 Ds14 Ds13 Ds12 Ds11 Ds10 Ds09 Ds08   Display Start Adrs (Hi)   ^7
 13     Ds07 Ds06 Ds05 Ds04 Ds03 Ds02 Ds01 Ds00   Display Start Adrs (Lo)   ^7
 14     Cp15 Cp14 Cp13 Cp12 Cp11 Cp10 Cp09 Cp08   Cursor Position (Hi)      ^7
 15     Cp07 Cp06 Cp05 Cp04 Cp03 Cp02 Cp01 Cp00   Cursor Position (Lo)      ^7
 16     LpV7 LpV6 LpV5 LpV4 LpV3 LpV2 LpV1 LpV0   Light Pen Veritcal        ^8
 17     LpH7 LpH6 LpH5 LpH4 LpH3 LpH2 LpH1 LpH0   Light Pen Horizontal      ^8
 18     Ua15 Ua14 Ua13 Ua12 Ua11 Ua10 Ua09 Ua08   Update Address (Hi)       ^9
 19     Ua07 Ua06 Ua05 Ua04 Ua03 Ua02 Ua01 Ua00   Update Address (Lo)       ^9
 20     At15 At14 At13 At12 At11 At10 At09 At08   Attribute Start Adrs (Hi) ^7
 21     At07 At06 At05 At04 At03 At02 At01 At00   Attribute Start Adrs (Lo) ^7
 22     HcP3 HcP2 HcP1 HcP0 IcS3 IcS2 IcS1 IcS0   Hz Chr Pxl Ttl/IChar Spc  ^A
 23     .... .... .... VcP4 VcP3 VcP2 VcP1 VcP0   Vert. Character Pxl Spc   ^5
 24     BlkM RvsS Vss5 Vss4 Vss3 Vss2 Vss1 Vss0   Block/Rvs Scr/V. Scroll ^9^B^C
 25     Text Atri Semi Dble Hss3 Hss2 Hss1 Hss0   Diff. Mode Sw/H. Scroll  ^D,^E
 26     Fgd3 Fgd2 Fgd1 Fgd0 Bgd3 Bgd2 Bgd1 Bgd0   ForeGround/BackGround Col ^F
 27     Rin7 Rin6 Rin5 Rin4 Rin3 Rin2 Rin1 Rin0   Row/Adrs. Increment       ^G
 28     CSa2 CSa1 CSa0 RamT .... .... .... ....   Character Set Addrs/Ram  ^H,^I
 29     .... .... .... UdL4 UdL3 UdL2 UdL1 UdL0   Underline Scan Line       ^6
 30     WdC7 WdC6 WdC5 WdC4 WdC3 WdC2 WdC1 WdC0   Word Count (-1)           ^9
 31     Dta7 Dta6 Dta5 Dta4 Dta3 Dta2 Dta1 Dta0   Data                      ^9  
 32     BlkF BlkE BlkD BlkC BlkB BlkA Blk9 Blk8   Block Copy Source (hi)    ^9
 33     Blk7 Blk6 Blk5 Blk4 Blk3 Blk2 Blk1 Blk0   Block Copy Source (lo)    ^9
 34     DeB7 DeB6 DeB5 DeB4 DeB3 DeB2 DeB1 DeB0   Display Enable Begin      ^J
 35     DeE7 DeE6 DeE5 DeE4 DeE3 DeE2 DeE1 DeE0   Display Enable End        ^J
 36     .... .... .... .... Drm3 Drm2 Drm1 Drm0   DRAM Refresh Rate         ^K

                             +-----------------+
                             | Register Usage: |
                             +-----------------+

^1 : Register #0:     Horizontal Total
---  Register #1:     Horizontal Displayed
     Register #2:     Horizontal Sync Pulse
  
  These two register function to define the display width of the screen. 
Register 0 will contain the number of characters minus 1 between sucessive
horizontal sync pulses, the horizontal border and the interval between
horizontal sync pulses. The normal value for this is usually set to 126.
Register 1 specifies how many of the positions as specified in register 0 can
actually be used to display characters.  The default value for this is 80. 
The VDC can take values less than 80 and thus, will only display that many
characters. A useful effect can be a sweep from the right by incrementing
the value here from 1 to 80. Register #2 specifies the starting character 
position at which the vertical sync pulse begins. Thus, it also determines
where on the active screen characters appear. A default value of 102, 
increasing the value moves the screen to the left, decreasing it moves it to
the right.
 
^2 : Register #3:     Vertical / Horizontal Sync Position.
---- Register #7:     Vertical Sync Position 

  In Register 3, Bits 0-3 of this register specifies the horizontal sync width
and should be equal to 1 + the number of pixels per character. Thus, the value
here is normally 1+8 or 9. Bits 4-7 of register 3 specify the vertical sync
width and normally contains a value of 4. For interlace sync and video mode,
use a value that is twice the number of scan lines desired. Register #7 allows
for adjustment of where the vertical sync will be generated allowing shifting
of the actual display up and down. Normally, a value of 4, decreasing the value
will move the screen down, increasing it will appear to move it upwards.

^3 : Register #4:     Vertical Total
---- Register #5:     Vertical Total Fine Adjust
     Register #6:     Vertical Displayed

  Register #4 of this register determines the total number of screen rows, 
including the rows for the active display, and the top and bottom borders in
addition to that of the vertical sync width. The value held here is normally
a value of 32 for NTSC systems (US Standard) or 39 for PAL(European) systems.
Register #5 holds in bits 0-4 a "fine-adjust" where any extra scan lines that
are necessary to make up the display can be specified here. The value here is 
normally a 0 in both the NTSC and PAL initializations by the kernal and bits
5-7 are unused, always returning a binary 1111. Register #6 specifies the total
number of the vertical character positions (as set in Register 4) that can be
used for actual display of characters. Thus, this register usually holds a 
value of 25 for a standard 25-row display.
 
^4 : Register #8:     Interlace Mode Control
----
  
  Register 8 allows control of various display modes the 8563 can generate. 
Bits 0 and 1 are the only bits used in this register, the rest always reading
a binary 1. Bits 0 and 1 are configured as follows:

  Binary %00, %10 - NonInterlaced Mode
              %01 - Interlaced Sync
              %11 - Interlaced Sync and Video

  Note that the default value is $00 which is standard, non-interlaced.
Interlaced sync draws each horizontal scan line twice but appears to suffer from
an annoying jitter due to how it is drawn. Interlaced Sync and Video draws twice
as many lines, thus doubling the resolution. However, it also suffers from
jitter and that is why most monitors suffer horribly when using programs that 
support more than 30 rows. Note that for interlaced sync and video, the
following registers will need to be changed: #'s: 0,4,6,7,8.

^5 : Register #9:     Total Scan Lines Per Character
---- 
  
  Bits 0-4 of this register are the only relevant ones, the rest returning a 
binary 1. Bits 0-4 determine the character height in scan-lines of displayed
characters and allow up to scan-line heights of 32 scan lines. The VDC normally
sets aside 16 bytes for each character (normally, each byte is equivlent to
1 scan line) so the value here could be increased to 16-1 and a double-height
character set could be loaded in. Note, however that values less than 16 will
tell the VDC to use a 8,192 byte character set (normal) while specifying values
greater than 16 will make it use 32 bytes per character even if some of the 
bytes are not used.
 
^6 : Register #10:    Cursor Mode / Start Scan Line     
---- Register #11:    Cursor End Scan Line.
     Register #29:    UnderLine Scan Line Control.
  
  These registers allow the user to specify the cursor blink mode, as well as
the starting and ending scan lines for the cursor (allowing a full solid, 
an underline, Bits 0-4 of regiseter #10 determines the scan line within each
position for the top of the cursor. Normally, this value holds a value
of 0 for the block cursor, or a value of 7 for the underline cursor. Bits 5-6 of
Register 10 specify the blink rate for the cursor. A value of %00 specifies no
blink, ie: a solid cursor. A value of %01 specifies no cursor, a value of %10
specifies a flash rate of 1/16 the screen refresh rate, while a value of %11
specifies a flash rate of 1/32 the screen refresh rate. Note that bit 7 of
Register 10 is unused and normally returns a binary 1. Register 11 specifies
the bottom scan lines in bits 0-4, the other unused bits returning a binary 1.
The value held in these bits usually is 7 for the block and underline cursor
modes in the normal 128 editor. Register #29 is used to indicate where the scan
line is "set" in the character. The "underline" is only 1 pixel tall and thus,
this location just indicates the start and end location in pixels, similair to 
registers #10 and #11 being the same value. Note that bits 5-7 of this register
is unused and normally return a binary 1.

^7 : Register #12:    Display Start Address (Hi)
---- Register #13:    Display Start Address (Lo)
     Register #14:    Cursor Position       (Hi)
     Register #15:    Cursor Position       (Lo)
     Register #20:    Attribute Start Addrs (Hi)
     Register #21:    Attribute Start Addrs (Lo)
 
  Note first, that all of these registers are grouped in Hi byte, Lo byte order
which is usually different from the 6502 convention of low byte, hi byte (ie: 
in normal 6502 ml, $c000 is stored as $00 $c0, however in the 8563 it would be
stored as $c0 $00).  Registers 12 and 13 determine, where in VDC memory the
8563 is the start of the screen. Incrementing this value by 80 (the number of
characters per line) and with a little additional work can provide a very 
effecient way of having a screen that "seems" to be larger than just 80x25.
The cursor position in registers 14 and 15 reflect the actual character in
memory that the cursor currently lies over. If it's not on the display screen,
then it is not displayed. Registers 20 and 21 reflect where in the 8563 memory
attribute memory is held. Attribute memory refers to the character attributes
such as flash, inverse video, color etc that can be set for each character.
 
^8 : Register #16:    Light Pen Vertical
---- Register #17:    Light Pen Horizontal
 
  These registers return the light pen position and refer to the actual 
character positions on screen (ie: values ranging from 1..25 for vertical).
The horizontal reading will not corrospond exactly to character positions, but
will range from values of 27-29 to 120 depending on the edge of the screen.
It's recommended that the horizontal character position is given more tolerance
than the vertical light pen position for this reason.
 
^9 : Register #18:    Update Address (Hi)
---- Register #19:    Update Address (Lo)
     Register #24:7   Copy / Fill Bit
     Register #30:    Word Count(-1) 
     Register #31:    Data
     Register #32:    Block Copy Src (Hi)
     Register #33:    Block Copy Src (Lo)
 
  These registers allow control and manipulation of the 16k or 64k block within
the 8563 memory.  Registers 18 and 19 point to where in VDC memory the next
read or write will take place from. Register 30 specifies the number of bytes
- 1 to copy or fill depending on bit # 7 of register #24. Normally, the 8563 
will automatically perform the designated operation (of what bit 7 of register
#24 says) when register #31 (the data byte) is written to. Registers 18 and 19
automatically update upon read or write, so that is why register #30 specifies
a value 1 less than what is actually needed. Register #31, as already mentioned
is the byte to write for register #30 times (or copy from Register#32 / #33).
If register #24, bit 7 is specified as a binary 1 then the memory is copied from
the address in VDC memory pointed to by registers #32 and #33.

^A : Register #22:    Character Horizontal Size Control
----

  Bits 0-3 of this register determines how many horizontal pixels are used
for each displayed character. Values greater than 8 here result in apparent
gaps in the display. Inter-character spacing can be achieved by setting this
value greater than that of bits 4-7. Bits 4-7 determine the width of each
character position in pixels. Thus, while bits 0-3 allocate n-pixels, bits
4-7 specify how many of those pixels are used for character display.

^B : Register #24:5   Reverse Screen Bit
---- Register #24:6   Blink Rate for Characters.

  Bit #6 specifies for the VDC for all pixels normally unset on the VDC screen
to be set, and all set pixels to be unset.  Bit #5 specifies the blink rate
for all characters with the blink attribute. Setting this to a binary 1
specifies a blink rate of 1/32 the refresh rate, while a binary 0 is equivlant
to a blink rate 1/16th of the refresh rate.

^C : Register #24:0-4 Vertical Smooth Scroll
----
   
  The 8563 provides for a smooth scroll, allowing bits 0-4 to function as an 
indicator of the number of bits to scroll the screen vertically upward.

^D : Register #25:7   Text or Graphics Mode Indicator Bit
---- Register #25:6   Monochrome Mode Bit
     Register #25:5   Semi-Graphics Mode
     Register #25:4   Double-Pixel Mode

  The 8563 allows the implementation of a graphics mode, in where all of the 16k
of the screen may be bit-mapped sequentially resulting in a resolution of
640x200 (see Craig Bruce's 8563 Line-Plotting routine in the first issue for a
more detailed explanation of this feature). Setting this bit to 1 specifies
graphics mode, binary 0 indicates text mode.  Bit 6 indicates to the 8563 where
to obtain its color information etc, about the characters. Bit 6 when it is a
binary 0 results in the 8563 taking it's color information from bits 4-7 of 
register 26. When this bit is a binary 1, the attribute memory is used to 
obtain color, flash, reverse information. Also note than when this bit is a
binary 1 that only the first of the two character sets is available. Bit #5
indicates a semi-graphics mode that allows the rightmost pixel of any characters
to be repeated through-out the intercharacter spacing gap. Activating it on the
normal display will result in what appears to be a "digital" character font. The
8563 with bit #4 allows a pixel-double feature which results in all displayed
horizontal pixels having twice their usual size. Thus, a 40 column screen is
easily obtainable although the values in registers #00-#02 must be halved.

^E : Register #25:    Horizontal Smooth Control
---- 
 
  This register is analogous to register #24 Vertical Smooth Control and 
functions similairly. Increasing this bits moves the screen one pixel to the
right, while decreasing them moves the screen one pixel to the left.
 
^F : Register #26:    ForeGround / BackGround Color Register
----
 
  This register, in bits 0-3 specifies the background color of the display while
bits 4-7 specify the foreground character colors when attributes are disabled 
(via bit 6 of register #25).  Note, these are not the usual C= colors but are
instead organized as follows:

   Bit Value  Decimal Value   Color
   ----------------------------------       +-----------------------------+
    %0000       0 / $00       Black         |  Note: Bit 0 = Intensity    |
    %0001       1 / $01       Dark Gray     |        Bit 1 = Blue         |
    %0010       2 / $02       Dark Blue     | RGBI   Bit 2 = Green        |
    %0011       3 / $03       Light Blue    |        Bit 3 = Red          | 
    %0100       4 / $04       Dark Green    |                             |
    %0101       5 / $05       Light Green   +-----------------------------+
    %0110       6 / $06       Dark Cyan
    %0111       7 / $07       Light Cyan
    %1000       8 / $08       Dark Red
    %1001       9 / $09       Light Red
    %1010      10 / $0A       Dark Purple
    %1011      11 / $0B       Light Purple
    %1100      12 / $0C       Dark Yellow
    %1101      13 / $0D       Light Yellow
    %1110      14 / $0E       Light Gray (Dark White)
    %1111      15 / $0F       White

^G : Register #27:    Row Address Display Increment
----

  This register specifies the number of bytes to skip, when displaying
characters on the 8563 screen. Normally, this byte holds a value of $00 
indicating no bytes to skip; however typically programs that "scroll" the 
screen do so by setting this to 80 or 160 allowing the program to then alter
the Screen Start (Registers #12 and #13) and appear to "scroll". Note the 
normal C= 128 Kernal Screen Editor does not support this function.

^H : Register #28:7-5 Character Set Address
----
 
  These bits indicate the address of screen memory * 8k. Thus the values in 
these bits may be multiplied by 8192 to obtain the starting character set
position (normall these bits hold a value of $01 indicating the character
set begins at 8192).  Note that the character set is not in ROM, but is usually
copied to 8192 when the computer is first turned on and the 8563 is initialized.
(Examine the INIT80 routine at $CE0C in bank 15).

^I : Register #28:4   Ram Chip Type
----
  
  This bit specifies whether 16k or 64k of RAM has been installed. Note, however
that this value may not reflect future upgrades from 16k to 64k.  It is best,
if a program is dependant on 64k to write to an address > 16k and see if it
is mirrored at any other location in another section of memory. This bit has a
binary value of 0 if 16k or 1 if 64k RAM.

^J : Register #34:    Display Enable Begin
---- Register #35:    Display Enable End

  The 8563 can extend it's horizontal blanking interval to blank a portion of
the displayed screen.  The value in register #34 determines the rightmost 
blanked column, and register #35 determines the leftmost blanked column.  Note
that a value of 6 usually corresponds to the leftmost column of the screen, 
while a value of 85 corresponds to the rightmost column.  This feature is useful
for "inside-out" wraps in which both the right and left margin can close-in on
text, the text can be cleared, these values reset etc... 

^K : Register #36:    Refresh Cycles per Scan Line
----
 
  This register in bits 0-3 allows the user (if he had any reason) to specify
the number of refresh cycles for memory for the ram.  Setting this value too
low may cause the RAM to not remember all the information.  Changing this value
gives some advantage, in terms of display speed increases but is not advised. 
The value normally held here is $05, for five refresh cycles per scan line.

                           +--------------------------+
                           | 8563 Memory Organization |
                           +--------------------------+

  Normally, the extra memory of the C=128's equipped with 64k goes unused (48k
worth) unless programs like Basic-8 etc, take advantage of it. There are various
mod files describing the upgrade from 16k to 64k and it is _strongly_ advised
(although the author has not yet done so) and be aware that ***OPENING YOUR
COMPUTER JUST TO LOOK, YOU MAY MESS IT UP*** and it is _strongly_ advised that 
you contact a person experienced with electronics to perform the upgrade for
you.  Note also that some mail order companies are offering an "up-grade board"
which plugs into the 8563 slot and does not involve desoldering the RAM chips.

  Now, the 8563 uses the 16k of memory (it ignores the extra 48k of memory when
it's got 64k, thus the following applies also to the 8563's equipped with 64k
of memory) and normally, has the following memory map:
 
  $0000 - $07ff - Screen Memory
  $0800 - $0fff - Attribute Memory
  $1000 - $1fff - Unused
  $2000 - $2fff - UpperCase / Graphic Character Set   (Char Set #1)
  $3000 - $3fff - LowerCase / UpperCase Character Set (Char Set #2)
  
                          +---------------------------+ 
                          | Writing to 8563 Registers |
                          +---------------------------+ 

  Now how do we write to these registers we've learned so much about? There's
several ways depending on how lazy you are.  The pure-ml version:
 
WRITING TO A REGISTER:
 
   writereg = * ; this routine writes .a to register # .x, Asssumes I/O block in
         stx $d600
       - ldx $d600
         bpl -
         sta $d601
         rts
   
  also, in bank 15 there is a similair routine at $cdcc. Calling it at $cdca
loads .x with a value of 31 indicating the data register which is often useful.
 
  From basic, just use a SYS 52684, value, register#

 READING FROM A REGISTER:
   
   readreg = * ; this routine returns the contents of register # .x in .a
               ; Assumes I/O block switched in
        stx $d600
      - ldx $d600
        bpl -
        lda $d601
 
   or use the routine in bank 15 at $cdda.  From basic, a SYS 52698,,register#
and then a RREG A returns the value in variable A. 

                          +--------------------+
                          | Further 8563 Notes |
                          +--------------------+ 

  Many C=128 owners are still using their monitors they had when they had their
C=64's and are able to use the 80 column screen through a "converter-cable"
(basically taking pin 7 of the RGBI port and feeding it as raw video).  There
is also a text file out explaining how to take the R,G,B,I pins on that port
to display shades of gray on a monochrome monitor (basically tying resistors
with diodes across each color pin and then joining them).  There is relief!! :-)

  The 8563 is a chip full of cabibilities waiting to be found and developed. I'd
be interested in seeing any code / techniques that readers of this net-mag have
found.  Given that enough are submitted, a possible listing of some of the 
better tricks and techniques might be possible in the future.

===========================================================================

FILE SPLITTER - Mark Lawrence

9152427d@levels.unisa.edu.au

This program stemmed from the inability of XLINK to transfer CS-DOS from my pc
to my 128.  XLINK transfers about 43K (I think), whereas CS-DOS was about 48K.

Rather than do the whole thing at once, why not cut the job up into more
sizeable pieces, transfer the program piece by piece, and then reassemble the
pieces at the other end?

And so eventuated the birth of SPLIT :-)

SPLIT, written entirely in Turbo Pascal, allows you to split DOS files into
smaller pieces - you can either tell it a size to split the files into, or
tell it a number of files to create.  You then give SPLIT the base filename
for the new files WITH NO EXTENSION - SPLIT will give the new files their own
extensions, and SPLIT will then create these files to your liking.

Just transfer the following program to Turbo, compile it, and away you go!!!

Hopefully, the program is commented enough to give you a fair idea of what's
going on - although it isn't at all complicated to understand.

At some points I have comments that seem the least important - END { CASE } -
they are to help me when I program...  I find it easy to lose track of which
END is for what, stuff up my indentation, lost bits and pieces, delete the
wrong parts, etc, etc.

I found it helped me, so it may help others.

If you need any further explanation, just let me know :-)

Another interesting thing I discovered about XLINK.  It doesn't transfer the
files to the correct size.  I think (haven't had time to sit down and check
it out yet) it transfers to the nearest 256, 512 or 1024 byte boundary.  If
your file doesn't reach the boundary, it will pad the rest out with zeroes
I think.  So, when you go to reassemble the file, it's got all this garbage
in places where it shouldn't be, and the thing won't work.

So, when SPLITting a file, specify the size to a multiple of one of these
boundaries.

Then, using a m/c monitor, load all the parts in together.

I'll try to set aside a little time in the not too distant future to write a
m/c program to join the parts for you, since it can get confusing reassembling
the parts by hand, and the built in dos copy that commodore so kindly graced us
with is so darned fast <cough> <cough> :-)

[Ed. Note: While the dos copy command is slow.... for those of you who are
 impatient try using somethine like the following to join files togather making
 sure that there's enough space on the disk:
     open15,8,15,"c0:name=name1,name2...": close15]


So, good luck and enjoy!

---------------------------------------------------------------------------
Program Split (input,output);

Uses Dos;
  { uses specific file handling routines }
Var
   InFile,Outfile                : File of Byte;
   Count,Number,Size,NewSize,
   Last,Counter                  : Longint;
   InFileName,Newfile,OutFileName: String;
   S,P                           : PathStr;
   D                             : DirStr;
   N                             : NameStr;
   E                             : ExtStr;
   SplitType,Check               : Char;
   Data                          : Byte;
   Extension                     : String[3];

Begin
   For count := 1 to 25 do
      Writeln;
      { Dumb way to clear the screen :-)  }

   Writeln ('*********************************************************');
   Writeln ('*  FILE SPLITTING UTILITY V0.01 (C) 1992 MARK LAWRENCE  *');
   Writeln ('*               (-: MADE IN AUSTRALIA :-)               *');
   Writeln ('*********************************************************');
   Writeln;
   Write   ('Enter Filename (including drive and path)    ');
   ReadLn  (InFileName);
   Writeln;

   For count := 1 to length (InFileName) do
      InFileName[count] := UpCase ( InFileName[count] );
      { change filename to all uppercase characters }

   FSplit(InFileName,d,n,e);
      { split filename into it's respective parts:
              d - Directory
              n - Name
              e - Extension }

   S := FSearch(InFileName,GetEnv(D));
      { search for file FILENAME in directory D }

   if S = '' then
      writeln ('*ERROR*     File "',InFileName,'" not found.')
      { S equals '' (nothing) if FILENAME doesn't exist }

   Else
   Begin
      Assign  (Infile,InFileName);
      Reset   (Infile);
      { Open the Input File }

      Size := FileSize (InFile);
      { Get file size }

      Writeln ('FileName:     ',InFileName);
      Writeln ('FileSize:     ',Size,' Bytes.');
      Writeln;
      { Show file info }

      Writeln ('In which way would you like the file split?');
      Writeln ('     (a)  Number of Bytes.');
      Writeln ('     (b)  Number of Files.');
      Repeat
         Write ('Enter your selection    ');
         Readln (SplitType);
         SplitType := UpCase(SplitType);
      Until (SplitType >= 'A') and (SplitType <= 'B');
      { let user choose which way to split file }

      Writeln;
      Case SplitType of
         'A': Begin
            { split by number of bytes }
            Write ('Enter byte size of new files    ');
            Readln (NewSize);
            Writeln;
            If (NewSize > Size) then
               Writeln ('Hey - Even I can''t do that!!!')
            Else
            begin
               Number  := Size div NewSize;
               Last    := Size - Number * NewSize;
               Number  := Number + 1;
               Write ('Enter Base Filename (including drive and path)    ');
               Readln (NewFile);
               Writeln;
               Writeln ('Creating ',Number,' new files...');
            End;
         End; { A }

         'B': Begin
            { Split by file size }
            Write ('Enter number of new files: ');
            Readln(Number);
            Writeln;
            NewSize := Size div Number + 1;
            Last    := Size - (Number - 1) * Newsize;
            Number  := Number;
            Write ('Enter Base Filename (including drive and path)    ');
            Readln (NewFile);
            Writeln;
            Writeln ('Creating ',Number,' new files...');
         End; { B }
      End;  { Case }

         Writeln;

         For Count := 1 to Number do
         { NUMBER new files will be created }

         Begin
            If Count = Number then
               NewSize := Last;
            { More often than not, the files won't divide evenly from the
              original.  So, the last file will be smaller than the rest.
              Because of this, I previously calculated the size of the final
              file, and here check if we're up to the last part yet - and if
              we are, I set the size of the last file accordingly }

            Str(Count,Extension);
            { Make EXTENSION a string representation of COUNT, to be added to
              the OutFileName to make things a tad easier }

            OutFileName := Concat(NewFile,'.',Copy('00',1,3-Length(Extension)),E
            { Create filename based on which part we're up to }

            Assign  (OutFile,OutFileName);
            Rewrite (Outfile);
            { Open each Output File }

            Write   ('Creating ',OutFileName,'... ');

            For Counter := 1 to NewSize do
            { Write to each Output File }

            Begin
               Read  (Infile,Data);
               Write (OutFile,Data);
               { Transfer data from input file to output file }
            End;

            Close   (Outfile);
            { Close each Output File }
            Writeln ('Done!');

         End;

      Writeln;
      Writeln ('Job Completed :-)');

   end;

   For Counter := 1 to 3 do
      Writeln;
   { Make a bit of space when finished :-) }
end.

================================================================================

BANKING ON GEOS

by Robert A. Knop Jr.

I. Introduction

GEOS was originally written for the Commodore 64.  When Berkeley Softworks
came out with GEOS128 (and, for a time, it wasn't clear that they would; then,
it looked like they would release a GEOS128 that wouldn't support the 80
column screen; finally, the release of GEOS128 did turn out to be a full 128
program), it was largely compatible with GEOS64.  Applications could share
documents (a geoPaint file is a geoPaint file), and even many GEOS64
appliations run on the 128 in 40 columns.  This heritage is also evident to
the GEOS programmer.

As we all know, the C-128 has two 64K RAM banks; the C-64 only has one 64K RAM
"bank."  Thus, of course, all of GEOS64 goes into that one 64K RAM space.
This includes the Kernal as well as the space available to applications.  Once
the Kernal, graphics screens, and so forth, have claimed their RAM, the GEOS
programmer is left with 23.75K of memory from $0400 to $5fff.

To a cursory "glance," the GEOS128 programming environment looks very much
like the GEOS64 programming enviroment.  You still have $0400 to $5fff
available for applications; graphics screens and variables are in the same
place; the Kernal jump table is the same (with some 128 specific additions).
What happened to the other 64K that the 128 has available?

As it turns out, the core of GEOS128- including the application program space,
the 40 column foreground and background screen, and the Kernal jump table- are
all in the 128's RAM block 1, what GEOS calls FrontRAM.  To us 128 programmers
used to RAM block 0 being the "main" RAM block, this may sound odd.  However,
it actually makes sense.  First of all, since GEOS is an operating system in
and of itself, and applications almost never need to call the C128's Kernal
routines, the application no longer needs access to Bank 15.  Second, it
allows GEOS128 to keep much of its memory map the same as GEOS64; it can use
the memory range from $200-$3ff in RAM 1 without worrying about disturbing key
system routins like STAFAR which are in the same memory range in RAM 0.


II. Yeah, Yeah, But What Happened to RAM 0 Anyway?

It's still there.  Some of RAM 0 is used by GEOS128 to improve the system
performance and to take advantage of the 128's unique features.  (For
instance, the code for the "software sprites" seen on the 128's 80 column
screen is found beneath $2000 in RAM 0.)  Fortunately, some space does remain
available for an application to use.  In RAM 0, the 32K memory space between
$2000 and $9fff is not normally used by GEOS, and is ALMOST available for
application use [1].

Why do I say "almost"?  The problem is desk accessories.  When GEOS 64 loads a
desk accessory (DA), it must load it into the same application space as the
application loading the DA.  The memory that the DA will used is first saved
to disk in a swap file.  Under GEOS128, the routine LdDeskAcc, instead of
saving a swap file to disk, copies the memory to be overwritten by the DA to
RAM0 between $2000 and $9fff.  So, if your application uses DA's (and it is
highly recommended that major applications support DA's), you have to be
careful using the space between $2000 and $9fff.  You can use it as temporary
swap space within routines- but you cannot assume that it will remain intact
whenever your routine returns to the GEOS MainLoop with your application in a
state that will allow the loading of DA's.

Nowadays, RAM 0 is not the be-all and end-all.  GEOS128 was written for the
C=128, not the C=256.  Consequently, if you have expanded your 128 to 256K or
512K as described in the articles by Richard Curcio in Twin Cities 128 Issues
#30 and #31 [2,3], you have free use of RAM 2-3 (256K) or RAM 2-7 (512K).
(Note that you should not touch RAM 4-7 on a 512K 128 if you want to be
compatible with task switching as described in TC128 #31.  Also, although GEOS
right now does not run in the 2nd 256K, applications should not assume they
are in the 1st 256K, and thus should be careful with the 512K mode bits (4-5)
in the MMU Ram Configuration Register (RCR), $d506.)  While the number of
people with 256K and 512K 128's is now small, you can be sure that it will
increase when the promised ZIP accelerator board for the 128 comes out; the
current specs for the ZIP board include provisions for memory expansion on the
board.

RAM 2-3 provide almost another complete 128K available for your application to
use.  So how do you go about accessing this?


III. Storing Data In Other RAM Blocks

The most obvious use for RAM blocks other than FrontRAM (which is the only
block where GEOS Kernal routines are available) is as data storage.  For
instance, one could visualize a geoPaint previewing utility which loads and
decompacts an entire geoPaint document at once to RAM 2.  (The full
decompacted geoPaint document would reqire 56.25K.)  One could then quickly
scroll through the document by just copying the relevant portions of the
bitmap from RAM 2 to the foreground screen.  Or, if one were really bold, one
could just redirect the VIC screen memory to the relevant range in RAM2 using
the proper MMU and VIC registers.  (This would actully require use of both RAM
2 and 3, since VIC screen locations are quantized to 8K; you lose the use of
the highest 8K, since you don't want to overwrite the MMU registers at
$ff00-$ff05; additional practical considerations make use of the lowest 8K
difficult.)

GEOS128 provides a few routines for easily moving data between FrontRAM and
what it calls BackRAM (but we know it just means RAM 0).  Happily, these
routines work quite admirably with RAM 2 and 3.  (To access RAM 4-7, fiddle
bits 4 and 5 of the MMU RCR to make the desired RAM blocks appear to the
system as virtual RAM 2 and RAM 3, then call these routines.)  The core
routine is DoBOp, which is summarized below [4]:

***********************************************************************
DoBOp=$c2ec:  Copy/verify memory between RAM blocks on the C-128.

Pass:
   r0   : ADDR1 - address of first ("source") memory range
   r1   : ADDR2 - address of second ("destination") memory range
   r2   : COUNT - number of bytes to operate on
   r3L  : A1BANK - bank of ADDR1 (e.g. 1=FrontRAM, 0=BackRAM)
   r3H  : A2BANK - bank of ADDR2
   y    : MODE - operation to perform


Returns: r0-r3 unchanged

  when verifying: x=$00 if two ranges match, x=$ff if they don't match


Destroys: a,x,y


The operation mode is passed in y as follows:

        bit0  bit1   Description
        ----  ----   -----------
         0     0      Move from memory at ADDR1 to memory at ADDR2
         0     1      Move from memory at ADDR2 to memory at ADDR1
         1     0      Swap memory at ADDR1 and ADDR2
         1     1      Verify (compare) memory at ADDR1 and ADDR2
***********************************************************************

(r0, r1, etc. are all the standard BSW symbols defined in the Official GEOS
Programmer's Reference Guide [5], and that come in the file geosSym with
geoProgrammer.)

There are a number of additional routines which are also provided for
programmer convenience which automatically set the MODE in the y register for
you.  In all of these routines, r0-r3 have the same meaning as they do in
DoBOp.

Routine     Address  MODE   Description
-------     -------  ----   -----------
MoveBData    $c2e3    00    Copy data at ADDR1 to ADDR2
SwapBData    $c2e6    10    Swap data between ADDR1 and ADDR2
VerifyBData  $c2e9    11    Compare data at ADDR1 and ADDR2

I have written a short demonstration program which shows the use of MoveBData
and VerifyBData.  The full source to this program, BMover, is available
through anonymous ftp at tybalt.caltech.edu (in the /pub/rknop/hacking.mag
directory) as well as elsewhere.  If you can't find it, contact me (addresses
are below).  The source is geoProgrammer code, in geoWrite 2.1 format.  All of
the files you need (except geosSym and geosMac, which come with geoProgrammer)
are in the bmover.sfx archive.

The first function of BMover repeatedly copies a single block on RAM 1 to
successive parts in memory in any other specified bank.  The destination bank,
destination addresses, size of the block to move, and number of times to copy
it are all set in constants found at the beginning of the source file BMovAsm.
Once the moves (which use MoveBData) have all been performed, BMover uses
VerifyBData to make sure that all of the blocks were copied succesfully.

For informational purposes, BMover reports the amount of time (in tenths of
seconds) it took to perform all of the moves.  (For this, I use the CIA #1 TOD
clock, saving its value at the beginning and end of the move, and subtracting
to get the difference.)  I ran a trial where I copied an 8K block of memory to
RAM 2 7 times (thus filling 56K of RAM 2).  These moves together took 1 second
at 2 MHz, and 2.2 seconds at 1 Mhz.  56K/second may be no DMA, but it's faster
than a burst load!


IV. Executing Routines In Other Banks

So, you've written an object oriented drawing program that stores its list of
objects (32 byte records) in RAM 2.  Or, you have a database that has records
in RAM 0.  You want to delete one record at the beginning of the list, which
means moving all of the subsequent records down over the memory freed up by
the deletion.  There are a few things you can do.  One, you can use Craig
Bruce's dynamic memory allocation routines (highly recommended).  Two, you can
repeated do MoveBData to move memory from RAM 2 (or 0) to a buffer in FrontRAM
and back.  Or, you can write a short mover routine in the RAM bank where all
the moving is going to happen.

This is just an example.  One can visualize other reasons for calling routines
in other RAM banks (what I call "extrabankal routines").  There exist no GEOS
Kernal routines for calling extrabankal routines.  Additionally, since your
main application memory is in RAM 1, you are inable to use the 128 Kernal's
JSRFAR (which returns you to Bank 15).  So, we are left with implementing our
own JSRFAR.

GEOS128 normally operates with NO common memory enabled.  Thanks to one of the
less well-known features of the MMU, there is no need to enable common memory.
The MMU zero page registers ($d507 and $d508) allow you to locate the zero
page that the processor sees anywhere in RAM 0 or RAM 1.  What this means is,
no matter what your memory configuration is, the processor sees zero page in
the RAM block specified in $d508.  (Unless you have common memory enable, in
which case it is not a good idea to put ZP in RAM blocks other than RAM 0
[6,7].)  So, zero page is effectively common memory!

This provides for the possiblity of copying to zero page a short "switchboard"
routine, basically a reimplementation of JSRFAR, which configures the system
for the destination bank, jsr's to a routine, reconfigures the system for the
calling bank, and rts's.

I also demonstrate this technique in BMover.  The second function of BMover
first uses MoveBData to copy a routine to $2000 in DESTBANK (which is set
right now in the source code to RAM 0).  It then copies the routine ZPJSR to
$02, which stores DESTCFG in $ff00 and jsr's to $2000.  The routine at $2000
moves some data around in DESTBANK.  Once ZPJSR has returned the program flow
to FrontRAM, BMover calls VerifyBData to make sure everything worked.

While messing around in different banks, to be safe I dissable IRQ interrupts.
On a related note, geoDebugger 2.0 seems to have problems with programs
messing around with different banks.  It is not surprising that the BackRAM
debugger (which locates itself in RAM 0) would have trouble with programs that
tried to use RAM 0, but it also has trouble with programs that try to use RAM
2 and 3.  This is true even when one uses the system routine MoveBData.  (I
found that I was sometimes able to make it past a call to MoveBData while in
the debugger, but that more often the system would hang.  This is all probably
an interrupt-related issue.)

If one is to be really classy, one doesn't actually have to copy the ZPJSR
routine to zero page.  One could assemble the application such that ZPJSR fell
to a known offset from a page boundry; then, use the MMU to point zero page to
the page containg ZPJSR.  Unfortunately, this technique did not work on my
512K expanded 128.  The one incompatility I have found is that with the 512K
modification enabled (I do have a switch to disable it, don't worry), the MMU
fails to correctly see zero page in RAM 1 when requested to.  Richard Curcio
experimented with it, and it seems that when you try to relocate zero page to
a page in RAM 1, it is actually seen in RAM 3.  It is not yet clear whether
this is a problem with the 256K/512K modification, or if the MMU in a stock
128 just relocates ZP to RAM 3 figuring that RAM 3 = RAM 1 (which is true on a
stock 128, but not on a 256K expanded 128!)



Anyone who wants to get ahold of the BMover source, or who has other
questions/comments/flames can contact me, Robert Knop, at the following
addresses:

InterNet: rknop@tybalt.caltech.edu
GEnie:    R.KNOP1
U.S. Mail:  Robert Knop
            123 S. Chester #3
            Pasadena, CA  91106


V. References

[1] William Coleman, 1989: "Inside GEOS 128"  _The_Transactor_ 9(4), p. 29.

[2] Richard Curcio, 1991: "Expanding the 128 Part One: 256K" _Twin_Cities_128_
#30, p. 7.

[3] Richard Curcio, 1992: "Expanding the 128 Part Two: 4 Mode 512K"
_Twin_Cities_128_ #31, p. 5.

[4] Berkeley Softworks, 1988: _The_Hitchhiker's_Guide_To_Geos_.

[5] Michael Farr, 1987: _The_Offical_GEOS_Programmer's_Reference_Guide_.
Bantam Books, New York/Toronto.

[6] Larry Greenly et. al, 1986: _Commodore_128_Programmer's_Reference_Guide_.
Bantam Books, New York/Toronto.

[7] Ottis R. Cowper, 1986: _Mapping_the_Commodore_128_.  Compute! Publications,
Greensboro, NC.

==============================================================================

DYNAMIC MEMORY ALLOCATION FOR THE 128: Breaking the 64K Barrier

by Craig Bruce (csbruce@ccnga.uwaterloo.ca)

Although this article would be best described as extremely technical, I think
that it has something for everyone.  It could also be described as being
extremely long.

Below I have written a program that will read in the lines of a file, sort
them, and write then back out to another file.  Because of the nature of the
problem, the each line of the entire file must reside in the memory of the
computer.  I implement and use dynamic memory allocation such that the file to
be sorted can be larger than 64K, and I use a dynamic data structure such that
the memory is used very efficiently.  The memory routines were extracted from
a text editor called "Zed-128" which also breaks the 64K barrier and can edit
some humongous files (and very efficiently too).  Although implemented for the
C-128, the dynamic memory scheme could also be fairly easily (ie. in a single
lifetime) ported to the C-64.

------------------------------------------------------------------------------

1. INTRODUCTION

How many of us are sick and tired of the "64K limit" that a lot of programs for
the 128 and 64 seem to have?  Many terminal programs, text editors, and even
file copiers seem to be afflicted with this problem.  Another problem is that
programs often reserve large sections of memory for specific purposes (such as
the kill buffer of a text editor) and cannot reconfigure themselves (very
easily) for different demands.  Still another problem is that many programs do
not make use of a Ram Expansion Unit (if you are fortunate enough to have one)
to store your volumnous user data.

The way to overcome the limitations of the 64K architecture of the C128 and
C64 is to use dynamically allocated memory.  What this means is that
initially, all of the memory of the computer is free and when a user program
requires some memory to store user data, it calls a special subroutine that
allocates a given number of bytes of memory to the program to store the user
data.  And when the program is finished using that chunk of memory, it calls a
special subroutine to free the memory chunk and make it available for future
allocation requests.

One complication of this memory usage scheme is that a program has to keep
track of which chunks of memory it uses for what.  This is where dynamic data
structures come in.  The most important concept here is a pointer.  A pointer
is simply a variable that stores the address of some data structure (ie. some
chunk of memory).  If we know the address of a data structure, then we can
read it and modify it.

To overcome the problem of not knowing how many records will need to be
stored, records are often stored in lists, where every record contains a
pointer to the next record in the list, except for the last one, which
contains a special value that could not be mistaken for an ordinary pointer
(it is called the Null (or Nil for you Pascalers) pointer).  Thus, if we know
the address of the first record (by using a "head pointer"), then we have
sequential access to all of the records in the list.  If we want to add or
delete records from the list, then we must modify the other pointers such that
the consistency of the list is maintained.  Organizations other than simple
lists are also possible.

The implementation here is able to allocate RAM0 memory for storing user data
records, as well as RAM1 memory and even REU memory.  As long as the
application program keeps track of the pointers to its records, large volumes
of user data can be stored since it will be distributed among all of the
memory that is available from both the internal memory banks and the external
memory banks, thus breaking the 64K barrier.

------------------------------------------------------------------------------

2. FOR THE NOVICE HACKER

You get a sorting utility program.  This program implements the insertion sort
algorithm, so don't expect to break any speed records.  Also, the way that
dynamic memory is implemented here is more suited for large data structures
that will only be accessed slowly and infrequently (such as the current
document in a text editor); however, I wanted to come up with a useful utility
and I have never heard of a general file sorter for the 128 or 64.  The
insertion sort does, however, lend itself well to being used with dynamic data
structures in general, since you don't actually have to move anything; you
just change a couple of pointers in order to insert a line (record) between
two other lines.  Also, it turns out the the insertion sort is quite efficient
if your input file is already mostly or partially sorted.

The sort utility itself is completely machine language but assumes that the
input and output files are already opened, so a BASIC driver program is
required to set things up to and allow the user to easily change the sorting
parameters.  Such a program is listed here:

 1 i$="inputfile.txt" : id=8 : sf=1
 2 o$="outputfile.txt" : od=8
 3 :
 100 print"loading sort.bin..."
 110 bank 15
 120 bload"sort.bin",u(id)
 130 print"scratching old file..."
 140 scratch(o$),u(od)
 150 print"sorting..."
 160 open1,id,2,"0:"+i$
 170 open2,od,3,"0:"+o$+",s,w"
 180 sys dec("1300"),sf
 190 close2
 200 close1
 210 print"finished!"

Lines 1 and 2 set up the sorting parameters: the input and output filenames,
the input and output file device numbers, and the sorting field position.
Change the "sf" value to the position of the first character of the key
field.  The first position on the line is 1 (not 0).  (This corresponds to
what Zed uses for columns).  Starting from that position, the rest of the line
is used for the comparison that determines the order of the lines.  If a line
is encountered that is shorter than the position of the sorting field, the key
value is taken to be the Null String (which comes before any other string).

The program continues to load in the machine language (which fits into the
$1300 slot) and scratch the output file if it already exists.  Then the files
are opened, machine language is called, and the files are closed and the
program exits.  While reading the file, the program will split any lines that
are longer than 242 characters and treat them as multiple lines.

For testing the sort utility, I used a file that contains 1058 lines of the
following form:

ROXETTE                     MUST HAVE BEEN LOVE                       A01-1-01
ADAMS, BRYAN                SUMMER OF '69                             A05-1-10
JOEL, BILLY                 PRESSURE                                  M11-1-07
EAGLES                      NEW KID IN TOWN                           R06-2-04
ELECTRIC LIGHT ORCHESTRA    CALLING AMERICA                           R11-1-05
COCKBURN, BRUCE             WONDERING WHERE THE LIONS ARE             R14-1-03

As you may guess, it is a tape library.  The file is 83K in length.  I sorted
it on both my 1581 (with JiffyDOS) and my RamLink and then I sorted again the
file that I sorted in the first place.  The resulting execution times are as
follows:

    WITH EXPANSION MEMORY                WITHOUT EXPANSION MEMORY

 Ramlink regular = 110 seconds         Ramlink regular = 376 seconds
 Ramlink sorted  =  20 seconds         Ramlink sorted  =  24 seconds
 1581 regular    = 120 seconds         1581 regular    = 397 seconds
 1581 sorted     =  33 seconds         1581 sorted     =  55 seconds

You'll note that having expansion memory makes sort operate faster.  This is
because the REU Controller can transfer data around faster than the CPU can.
The effect is even more pronounced when using records longer than 78-character
lines of text.  This is why it is sensible to use expansion memory for general
data storage and accessing.  The reason why the execution times are so long is
that approximately 1058*529/2 = 280,000 "far memory" line-length fetches have
to take place, along with that number of "zpload"s and string comparisons, and
1058 "malloc"s and "free"s.  Also, we all know that the Commodore file reading
and writing mechanisms are not severely swift.

------------------------------------------------------------------------------

3. FOR THE INTERMEDIATE HACKER

You get a dynamic memory allocation and usage package that can be incorporated
into your own programs.

3.1. MEMORY PACKAGE CALLS

The package includes eight system calls:

 startup ()
 shutdown()
 zpload  ( [zp1]=FarPointer, .X=ZpAddr, .Y=Length )
 zpstore ( [zp1]=FarPointer, .X=ZpAddr, .Y=Length )
 fetch   ( [zp1]=FarPointer, (zw1)=Ram0pointer, .AY=Length )
 stash   ( [zp1]=FarPointer, (zw1)=Ram0pointer, .AY=Length )
 malloc  ( .AY=Length ) : [zp1]=FarPointer, .CS=error
 free    ( [zp1]=FarPointer, .AY=Length ) : .CS=error

The "(...)" means input parameters and ":" preceeds output parameters.  ".X"
and ".Y" refer to the processor registers and ".AY" means the 16-bit value
with the .A register holding the low byte and the .Y register holding the high
byte.  With "(zw1)" I am refering to the indirect pointer in the zero page
locations "zw1" (low byte) and "zw1+1" (high byte) ("zw" means zero page word
and it is assigned to locations $FE to $FF) and with "[zp1]" I am refering to
the three byte pointer value in locations "zp1" (address low byte), "zp1+1"
(address high byte), and "zp1+2" (bank number byte) ("zp" means zero page
pointer and it is assigned to addresses $FA to $FC).  This three-byte pointer
is refered to as a "Far Pointer".  The ".CS=error" means that if the routine
returns with the carry flag set, an error has occured.  The only possible
error return in this package is if malloc cannot find enough contiguous free
memory to satisfy your request.

You do not actually have to know what the bank numbers mean since they are
generated and used by the package as an opaque data type (parlez-vous
Modula-2?), but here is what they mean anyway.  A value of $3F means internal
bank RAM0 and a value of $7F means internal bank RAM1.  This works out
conveniently in the implementation, since these are the MMU configuration
register values for those two banks.  A value from $80 to $FE refers to an
expansion (REU) memory bank.  $80 means expansion bank0, $81 bank1, etc.  This
means that the package can support up to 8 Megs (minus 64K) of expansion
memory (and it does).  These values are convenient to use since after loading
the bank number into a register, the Negative flag of the processor will be
set (useful if handling expansion memory is a special case), and this value
can be put directly into the REU Controller's bank register.  I don't think
you have to worry about having the high bit be a "1" since it is done
consistently and I have never heard of an REU larger than 2 Megs.  A bank
value of $FF is used to represent the Null pointer.

The "startup" routine installs the common code, determines the size of your
REU, and initializes the dynamic memory allocation mechanism.  In order for
the package to access internal memory bank RAM1, it has to call a routine that
is in memory below address $0400.  Since the package starts at $1300, it has
to copy a few "common code" subroutines into low memory such that it can call
them later.  The common code is installed at address $0200, the BASIC input
buffer.  Don't overwrite this area while the package is in use.  The "sniff"
routine is called to determine the number of banks that your REU has.  Zero
banks means that you have no REU.  While sniffing, the package overwrites the
first four bytes of every existing expansion bank (unless you limit the number
of expansion banks that the package is allowed to use).  To initialize the
dynamic memory allocation, the "free" routine is called for RAM0, RAM1, and
each expansion bank.  RAM0 from $4000 to the top of BASIC memory ($FEFF) is
freed, RAM1 from $0400 to $FEFF is freed, and all expansion banks are freed
between addresses $0000 to $FFF7.  Thus, if you have no expansion memory, you
get about 110K free and if you have a 512K expander, you get about 620K free.

The "shutdown" routine doesn't actually have very much to do.  Basically, it
just zeros out the common code.  I did this so if you called the sort routine
from BASIC direct input mode, you would not get a "syntax error" from BASIC
trying to interpret the garbage left behind.  Now, when BASIC encounters a
zero, it stops interpreting.

The "zpload" routine will load the given number of bytes into zero page
starting at the given zero page address, from any far pointer address.  It
doesn't matter whether the far address is in internal or expansion memory; the
operation is the same.  This is the level of software that makes accessing the
different types of memory transparent to the user.  To load from RAM0, true
RAM0 is switched into context (did I mention that the package is meant to
execute with MMU configuration $0E in context - this configuration gives RAM0
from $0000 to $BFFF, the kernel ROM from $C000 to $FFFF and the I/O space on
top of the kernel ROM from $D000 to $DFFF - I call this the SYS or SYS0 bank)
and the transfer is done with a loop.  For a zpload from RAM1, a common code
routine is called that switches RAM1 into context, copies in a loop, and then
switches back to SYS0.  For an expansion memory pointer, the REU Controller
registers are set up and the transfer is performed.  The package will work
with whatever Zero Page is in context (with MMU register $D507), since it is
convenient to use your own zero page in your programs.  For transfers of less
than about 16 bytes, internal memory is faster, and for longer transfers,
expansion memory turns out to be faster.  For really long transfers (say, 80
bytes), using the expansion memory is MUCH faster (a marginal cost of one
microsecond per byte as opposed to nine).  The "[zp1]" parameter is unaltered
by this call, but the register values are quite changed.  The "(zw1)"
parameter area is also left untouched.

The "zpstore" routine works the same as "zpload" except it stores to the far
memory from zero page.

The "fetch" routine fetches the given number of bytes from a far address into
the RAM0 bank (not SYS0) at the given address.  Unlike the zero page load
routine, you can transfer up to 64K of memory with this routine.  Again, the
type of memory to be fetched is transparent to the user.  For an internal
memory fetch, the transfers are performed in 256 byte chunks.  This makes the
implementation easier.  For each byte transferred from RAM1, RAM1 is switched
in and then RAM0 is switched in, so the transfer is not extremely efficient.
For the expansion memory, the REU Controller is set up and then the entire
transfer (up to 64K) is performed at a rate of 1 Meg/second.  This is
considerably faster than internal memory fetching.  This routine handles a
transfer length of 0 bytes properly.  The "zp1" and "zw1" parameters are
returned unaltered, but again, the registers are smashed.

The "stash" routine operates the same as fetch, except that the data is
transferred from the near ("zw1") address to the far ("zp1") address.

The "malloc" routine attempts to find a chunk of contiguous memory of the
given length to allocate to you.  If it can find one, it returns the far
pointer to it in the "[zp1]" parameter.  If it cannot find one, it returns
with the carry flag set.  This routine clobbers the registers.

The "free" routine returns to the pool of free memory the chunk of memory
specified by the far pointer and length parameters.  This routine clobbers the
"[zp1]" parameter and the registers.  The carry flag is always cleared upon
return, since the routine does not (currently) check for any errors.

3.2. MEMORY ALLOCATE AND FREE

The malloc and free routines maintain a linked list of free memory chunks.  A
free memory chunk is described by a five byte structure that is at the
beginning of the chunk.  The first three bytes are a far pointer to the next
free memory chunk and the following two bytes give the total length of the
chunk.  The structure is thus:

  +----------+----------+----------+----------+----------+---...
  | Next     | Next     | Next     | Chunk    | Chunk    |
  | chunk    | chunk    | chunk    | length   | length   |  garbage
  | low addr | high addr| bank num | low      | high     |
  +----------+----------+----------+----------+----------+---...
    chunk+0    chunk+1    chunk+2    chunk+3    chunk+4

All of the free (and allocated) memory chunks are always aligned on an eight
byte boundary.  This guarantees that no matter what happens, there will always
be at least eight bytes available in each free memory chunk to hold the free
chunk descriptor information.  Thus, if you were to make a request for three
bytes, the system would give you eight, and when you request to free those
three bytes, the system would automatically free eight.  This can lead to some
some wasted space when using small structures.

The memory chunks are kept in order of "increasing" address.  I say
"increasing" because while the chunks within a bank are in increasing address
order, the system considers bank number $87 (expansion bank 7) to be lower
than bank number $3F (RAM0).  This anomaly makes the system allocate its
external memory before allocating internal memory.  This is good since
external memory generally works faster than internal memory.

This memory is allocated first since the malloc routine uses a first-find
algorithm for searching for a sufficient free memory chunk.  It stops
searching when it finds a free memory chunk large enough to satisfy the user's
request.  If the free chunk is exactly the same size as the request, the free
chunk is unlinked from the free chunk list and the pointer is returned.  If
the free chunk is larger than the requested size, it is split up.  A pointer
to the top N bytes of the chunk is retured to the user and the size of the
free chunk is reduced by N.  The memory is allocated from the top of the chunk
to make it so no linking and unlinking has to take place in this case.

The free routine is more complicated than the allocate routine since free has
to deal with more cases.  Free has to search through the linked list of free
memory chunks to find the two chunks that straddle the chunk to be freed.
Free attempts to coalesce (merge) the new chunk with the previous chunk and
with the next chunk in order to end up with the largest free chunk that it
can under the circumstances.  Large free chunks are good since they can be
used for larger requests.  Two chunks can be coalesced if they are
side-by-side in memory (zero bytes apart) and on the same bank.  Note that
chunks on different banks cannot be coalesced together, so the largest
possible free chunk is 64K in length.  To coalesce them, the size of the first
one is increased by the size of the second one and the pointer to the second
one is forgotten.

Note that this scheme works differently from the dynamic allocation scheme
that BASIC uses for its strings.  BASIC does not attempt to coalesce together
(or even re-use) freed chunks; it relies upon garbage collecting to get rid of
the free chunks.  The scheme implemented here is more static (interesting word
to choose) in that once you are allocated a chunk, that chunk is pinned to
that address and will never move.  This static organization can lead to the
problem of memory fragmentation, where lots of memory can be free but is in
un-coalescable chunks that are too small to be useful.  Oh well.  I don't
think that it is really a problem for storing lines of text as individual
records, and it is no problem at all for a program that always uses fixed size
records.

3.3. THE SORT UTILITY

The sort utility makes full use of the capabilites of the package.  First it
reads in the input file one line at a time and stores the lines in a linked
list as individual records of the form:

  +--------+--------+--------+--------+-------...-----+--------+
  | Next   | Next   | Next   | Total  |               |        |
  | line   | line   | line   | record |  characters   | .byte  |
  | ptr    | ptr    | ptr    | length |  of the line  |    $00 |
  | low    | high   | bank   |        |               |        |
  +--------+--------+--------+--------+-------...-----+--------+
    line+0   line+1   line+2   line+3     line+4        line+?

Note that these are variable length records; each record is only as long as it
has to be.  The total record length is stored at the front of the record.  In
order to read a line into a processing buffer, a "zpload" is done that reads
the first four bytes of the record in order to get the length of the record.
Then the entire record can be fetched since its length is known at that time.
Each record ends with a $00 byte to simplify the string comparison
subroutine.

The line list is maintained in alphabetical order (actually, reverse
alphabetical order; below).  When a new line is read in from the input file,
the line list is searched for the two other lines whose values straddle the
value of the new line.  The line is then linked in at that position in the
list.  No other lines have to be moved around since pointers are used to
maintain the order of the list.  In order for a line already in the list to be
compared with the new line, the old line has to be fetched from far memory
(using the zpload + fetch scheme above) into a work buffer in the SYS0 bank.
On average, half of the existing list will have to be searched in this way in
order to find the correct spot to insert the new line.

After the position for the new line is found, space for the line is allocated
by calling "malloc" and then the data is stored from the work buffer it was
read into to far memory.  The zpload and zpstore routines are used to modify
the pointers to link in the new line.  A number of pointer manipulations are
also required on the zero page varialbles.

If the line list was generated in forward alphabetic order, then the utility
would achieve its WORST performance when the input file was already mostly or
partially sorted.  This is because when each line is read, if it comes after
most or all of the other lines, the most or all of the line list would have to
be searched to find the final resting position for the new line.  This would
be unacceptable and extremely wasteful.  A better scheme is to generate the
line list in reverse alphabetic order.  Then, when a "higher valued" line is
read in, its correct position would be at or near the top of the list, so
it would only have to be compared against a few of the lines already on the
list.  In the case of an input file that is already in pretty much random
order, it makes no difference whether the list is in forward or reverse
order.

Since the list is generated in reverse order, it must be reveresed again
before writing it to the output file, since the user would want it to be in
forward order (and since this is the order that can be most easily sorted
again later).  A clever little subroutine is called that reverses the order of
the list.  It only has to make use of zpload and zpstore to read/change the
first few bytes of each record, since it is not concerned with the data
contents of each record.

Although this is not strictly necessary, all of the records in the line list
are freed before the sort utilitiy exits.  This is a good practice, and would
be necessary if the program were to continue to do useful work after writing
the sorted file to output.  A pointer is stepped through the list (starting
from the head pointer) and the space for each line is deallocated by calling
free, after determining the size of the record by reading the first few bytes
of it.  Since the list will be in (pretty much) random order (of addresses),
the deallocation mechanism does not achieve its best performance.

A convenient jump table is set up at the start of the code to make it easier
for you to link your own programs to the package.  Make sure that MMU
configuration value $0E is in effect before calling any of the routines.  You
may have to muck with the code a little bit to get it to work for you.

------------------------------------------------------------------------------

4. FOR THE EXPERT HACKER

You get to see the code that actually implements the memory package and the
sort utility.  I have it here in a special form; each code line is preceeded
by a few special characters and the line number.  The line number is there to
allow me to refer to specific lines, and the special characters are there to
allow you to easily extract the assembler code from the rest of this magazine
(and all of my ugly comments).  On a Unix system, all you have to do is
execute the following command line (substitute filenames as appropriate):

grep '^\.%....\!' Hack2 | sed 's/^.%....\!..//' | sed 's/.%....\!//' >sort.asm

Dontcha just love those Unix commands!  Here is the assembler code:

.%0001!  ;Sort utility using dynamic memory allocation with expanded memory
.%0002!  ;written 92/04/22 by Craig Bruce for C= Hacking Net Magazine
.%0003!  ;--------------------------------------------------------------------

This program is written for the Buddy assembler.  Like most assemblers, it
needs a few directives to start off, so here they are.  Note that my comments
come BEFORE the section of code that I am commenting on.

.%0004!  .mem
.%0005!  .bank 15
.%0006!  .org $1300
.%0007!
.%0008!  ;*** global declarations
.%0009!

Here are the zero page locations that the package uses for its own purposes.
I stuck the sysWork variable over the BASIC graphics command parameters since
it seems like a good place.  It requires 16 bytes and is used by most of the
routines for temporary storage.  "temp1" is used for "very" temporary
storage.

.%0010!  zp1 = $fa
.%0011!  temp1 = $fd
.%0012!  zw1 = $fe
.%0013!  sysWork = $80    ;16-byte block
.%0014!

These are the non-zero page storage locations.  The common code buffer pretty
much has to be at $200 since that is (about) the only free section of memory
below address $0400 (in the common memory range).

.%0015!  comCodeBuffer = $200
.%0016!  workBuffer = $b00
.%0017!

These are the MMU configuration register values and some important I/O
addresses.

.%0018!  bkSys = $0e
.%0019!  bkKernel = $00
.%0020!  bkSelect = $ff00
.%0021!  bkSelectRam0 = $ff01
.%0022!  bkSelectRam1 = $ff02
.%0023!  bkRam0 = $3f
.%0024!  bkRam1 = $7f
.%0025!  bkExp0 = $80
.%0026!  bkNull = $ff
.%0027!  zpSelect = $d507
.%0028!  reu = $df00
.%0029!  vic = $d000
.%0030!
.%0031!  errInsufficientMemory = 1
.%0032!
.%0033!  ;*** jump to main routine
.%0034!
.%0035!     jmp main
.%0036!
.%0037!  ;*** jump table
.%0038!

Here's that jump table.

.%0039!  startup   jmp internStartup
.%0040!  shutdown  jmp internShutdown
.%0041!  zpload    jmp internZpLoad
.%0042!  zpstore   jmp internZpStore
.%0043!  fetch     jmp internRam0Fetch
.%0044!  stash     jmp internRam0Stash
.%0045!  malloc    jmp internAlloc
.%0046!  free      jmp internFree
.%0047!
.%0048!  ;*** storage
.%0049!

Here are some useful storage locations.  "errno" contains the code for the
error encountered in a routine if the routine exits with the carry flag set
(and it is supposed to be cleared for OK).  "nExpBanks" gives the number of
expansion memory banks, and "freeMemory" gives the number of bytes currently
free in the system.  Both of these are useful status values and can be read
directly.

.%0050!  errno      .buf 1
.%0051!  nExpBanks  .buf 1
.%0052!  mallocHead .buf 3
.%0053!  freeMemory .buf 3
.%0054!
.%0055!  ;***startup
.%0056!

This routine gets the ball rolling.  It clears the status register in case you
start up the system with the decimal mode flag set or interrupts disabled.

.%0057!  internStartup = *
.%0058!     lda #0
.%0059!     pha
.%0060!     plp
.%0061!     lda #bkSys
.%0062!     sta bkSelect
.%0063!     jsr installCommonCode
.%0064!     jsr sniffREU
.%0065!     jsr initDynamicMemory
.%0066!     rts
.%0067!

And this routine stops the ball from rolling.  I fill the BASIC command line
buffer with zeros to stop that syntax error thing.

.%0068!  internShutdown = *
.%0069!     ldx #0
.%0070!     lda #0
.%0071!  -  sta $200,x
.%0072!     inx
.%0073!     cpx #comCodeEnd-comCodeStart
.%0074!     bne -
.%0075!     lda #bkKernel
.%0076!     sta bkSelect
.%0077!     rts
.%0078!
.%0079!  ;***install common code
.%0080!

This routine copies the common code subroutines into the common code buffer
(at $0200).

.%0081!  installCommonCode = *
.%0082!     ldx #0
.%0083!  -  lda comCodeStart,x
.%0084!     sta comCodeBuffer,x
.%0085!     inx
.%0086!     cpx #comCodeEnd-comCodeStart
.%0087!     bcc -
.%0088!     rts
.%0089!
.%0090!  ;--------------------------------------------------------------------
.%0091!  ;***common code
.%0092!

And this is the common code.  It contains four subroutines for accessing RAM1
(and the zero page routines are used for RAM0 as well).

.%0093!  comCodeStart = *
.%0094!

Selects the MMU configuration according to the bank number and copies the
number of bytes required for a zpload.  It exits by restoring the SYS bank.
This is used only for internal memory zploads.

.%0095!  comZpLoad = *
.%0096!     lda zp1+2
.%0097!     sta bkSelect
.%0098!     sty temp1
.%0099!     ldy #0
.%0100!  -  lda (zp1),y
.%0101!     sta 0,x
.%0102!     inx
.%0103!     iny
.%0104!     cpy temp1
.%0105!     bcc -
.%0106!     lda #bkSys
.%0107!     sta bkSelect
.%0108!     rts
.%0109!

Pretty much the same as zpload.

.%0110!  comZpStore = *
.%0111!     lda zp1+2
.%0112!     sta bkSelect
.%0113!     sty temp1
.%0114!     ldy #0
.%0115!  -  lda 0,x
.%0116!     sta (zp1),y
.%0117!     inx
.%0118!     iny
.%0119!     cpy temp1
.%0120!     bcc -
.%0121!     lda #bkSys
.%0122!     sta bkSelect
.%0123!     rts
.%0124!

As the name suggests, this copies from RAM1 to RAM0.  Only .Y number of bytes
are copied, and if .Y=0, 256 bytes are copied.  You'll notice that the MMU
configurations are switched between for every byte copied.  This is not the
most efficient scheme, but it suffices.  The MMU preconfiguration registers
are used and the value that BASIC put in them are assumed to still be there.

.%0125!  comCopyRam1ToRam0 = *
.%0126!     dey
.%0127!     beq +
.%0128!  -  sta bkSelectRam1
.%0129!     lda (zp1),y
.%0130!     sta bkSelectRam0
.%0131!     sta (zw1),y
.%0132!     dey
.%0133!     bne -
.%0134!  +  sta bkSelectRam1
.%0135!     lda (zp1),y
.%0136!     sta bkSelectRam0
.%0137!     sta (zw1),y
.%0138!     lda #bkSys
.%0139!     sta bkSelect
.%0140!     rts
.%0141!

The opposite direction.

.%0142!  comCopyRam0ToRam1 = *
.%0143!     dey
.%0144!     beq +
.%0145!  -  sta bkSelectRam0
.%0146!     lda (zw1),y
.%0147!     sta bkSelectRam1
.%0148!     sta (zp1),y
.%0149!     dey
.%0150!     bne -
.%0151!  +  sta bkSelectRam0
.%0152!     lda (zw1),y
.%0153!     sta bkSelectRam1
.%0154!     sta (zp1),y
.%0155!     lda #bkSys
.%0156!     sta bkSelect
.%0157!     rts
.%0158!

The end of the common code.  The length of the common code is determined by
subtracting the end address from the start address.

.%0159!  comCodeEnd = *
.%0160!
.%0161!  ;--------------------------------------------------------------------
.%0162!  ;*** zpload( [zp1]=Source, .X=ZpDest, .Y=Length )
.%0163!

The actual zpload routine.  It dispatches to the common code routine if
internal memory is specified by the far pointer, or falls through to REU code
if expansion memory is specified.

.%0164!  internZpLoad = *
.%0165!     lda zp1+2
.%0166!     bmi +
.%0167!     jmp comZpLoad-comCodeStart+comCodeBuffer
.%0168!  +  sty reu+7
.%0169!     ldy #$91
.%0170!

Sets up the REU Controller registers for the parameters of the transfer.  Note
that the value of the zero page address is not assumed to be absolute $0000
but is taken from the zero page selection register of the MMU.  The REU
Controller does not use the MMU for decoding zero page and stack page
addresses; it accesses the absolute memory directly.

.%0171!  zeroPageReuOp = *
.%0172!     sta reu+6
.%0173!     stx reu+2
.%0174!     lda zpSelect
.%0175!     sta reu+3
.%0176!     lda zp1
.%0177!     sta reu+4
.%0178!     lda zp1+1
.%0179!     sta reu+5
.%0180!     lda #0
.%0181!     sta reu+8

Here the system clock speed is put into Slow mode while the transfer occurs
and is then restored.  This is necessary.

.%0182!     lda vic+$30
.%0183!     ldx #$00
.%0184!     stx vic+$30
.%0185!     sty reu+1
.%0186!     sta vic+$30
.%0187!     rts
.%0188!
.%0189!  ;*** zpstore( .X=ZpSource, [zp1]=Dest, .Y=Length )
.%0190!

Pretty much the same as the zpload routine, except that a command code for the
REU Controller is different (specifying an internal to expansion memory
transfer).  The REU code in the zpload routine is called.

.%0191!  internZpStore = *
.%0192!     lda zp1+2
.%0193!     bmi +
.%0194!     jmp comZpStore-comCodeStart+comCodeBuffer
.%0195!  +  sty reu+7
.%0196!     ldy #$90
.%0197!     jmp zeroPageReuOp
.%0198!
.%0199!  ;--------------------------------------------------------------------
.%0200!  ;*** fetch( [zp1]=FarSource, (zw1)=Ram0Dest, .AY=Length )
.%0201!

Some working storage locations are necessary for this routine, since it is
designed to copy data a page at a time.  The source (zp1+1) and destination
(zw1+1) page addresses are saved and later restored because this routine
alters them while copying.  If the far address is in expansion memory, this
routine dispatches to the REU fetch/stash code.

.%0202!  fetchLength = sysWork
.%0203!  fetchSaveSource = sysWork+2
.%0204!  fetchSaveDest = sysWork+3
.%0205!
.%0206!  internRam0Fetch = *
.%0207!     ldx zp1+2
.%0208!     bpl +
.%0209!     ldx #$91
.%0210!     jmp doReu

If the transfer is less than one page long, it can be done by calling the
fetchPage code directly.  Otherwise, the long fetch code has to be called.

.%0211!  +  cpy #0
.%0212!     bne fetchLong
.%0213!     tay
.%0214!     bne fetchPage
.%0215!     rts
.%0216!

If the (internal) page to be fetched is on RAM1, the common code routine is
called; otherwise, the copy is done here by switching RAM0 into context.  We
can copy between RAM0 locations without switching contexts for every byte.

.%0217!     fetchPage = *
.%0218!     cpx #bkRam0
.%0219!     beq +
.%0220!     jmp comCopyRam1ToRam0-comCodeStart+comCodeBuffer
.%0221!  +  stx bkSelect
.%0222!     dey
.%0223!     beq +
.%0224!  -  lda (zp1),y
.%0225!     sta (zw1),y
.%0226!     dey
.%0227!     bne -
.%0228!  +  lda (zp1),y
.%0229!     sta (zw1),y
.%0230!     lda #bkSys
.%0231!     sta bkSelect
.%0232!     rts
.%0233!

This is called for long (>=256 byte) (internal) fetches.  It calls the
fetchPage code repeatedly, after incrementing the source and destination page
numbers.  The transfer length is decremented until it is less than 256 bytes.

.%0234!     fetchLong = *
.%0235!     sta fetchLength
.%0236!     sty fetchLength+1
.%0237!     lda zp1+1
.%0238!     sta fetchSaveSource
.%0239!     lda zw1+1
.%0240!     sta fetchSaveDest
.%0241!     lda fetchLength+1
.%0242!     beq fetchLongExit
.%0243!  -  ldx zp1+2
.%0244!     ldy #0
.%0245!     jsr fetchPage
.%0246!     inc zp1+1
.%0247!     inc zw1+1
.%0248!     dec fetchLength+1
.%0249!     bne -
.%0250!
.%0251!     fetchLongExit = *

This fetches the last chunk of less than 256 bytes and then restores the zp1
and zw1 parameters to what they were before this routine was called.

.%0252!     ldy fetchLength
.%0253!     beq +
.%0254!     ldx zp1+2
.%0255!     jsr fetchPage
.%0256!  +  lda fetchSaveSource
.%0257!     sta zp1+1
.%0258!     lda fetchSaveDest
.%0259!     sta zw1+1
.%0260!     rts
.%0261!
.%0262!  ;*** stash( (zw1)=Ram0Source, [zp1]=FarDest, .AY=length )
.%0263!

Stash has exactly the same structure as fetch.

.%0264!  stashLength = sysWork
.%0265!  stashSaveSource = sysWork+2
.%0266!  stashSaveDest = sysWork+3
.%0267!
.%0268!  internRam0Stash = *
.%0269!     ldx zp1+2
.%0270!     bpl +
.%0271!     ldx #$90
.%0272!     jmp doReu
.%0273!  +  cpy #0
.%0274!     bne stashLong
.%0275!     tay
.%0276!     bne stashPage
.%0277!     rts
.%0278!
.%0279!     stashPage = *
.%0280!     cpx #bkRam0
.%0281!     beq +
.%0282!     jmp comCopyRam0ToRam1-comCodeStart+comCodeBuffer
.%0283!  +  stx bkSelect
.%0284!     dey
.%0285!     beq +
.%0286!  -  lda (zw1),y
.%0287!     sta (zp1),y
.%0288!     dey
.%0289!     bne -
.%0290!  +  lda (zw1),y
.%0291!     sta (zp1),y
.%0292!     lda #bkSys
.%0293!     sta bkSelect
.%0294!     rts
.%0295!
.%0296!     stashLong = *
.%0297!     sta stashLength
.%0298!     sty stashLength+1
.%0299!     lda zw1+1
.%0300!     sta stashSaveSource
.%0301!     lda zp1+1
.%0302!     sta stashSaveDest
.%0303!     lda stashLength+1
.%0304!     beq stashLongExit
.%0305!  -  ldx zp1+2
.%0306!     ldy #0
.%0307!     jsr stashPage
.%0308!     inc zp1+1
.%0309!     inc zw1+1
.%0310!     dec stashLength+1
.%0311!     bne -
.%0312!
.%0313!     stashLongExit = *
.%0314!     ldy stashLength
.%0315!     beq +
.%0316!     ldx zp1+2
.%0317!     jsr stashPage
.%0318!  +  lda stashSaveSource
.%0319!     sta zw1+1
.%0320!     lda stashSaveDest
.%0321!     sta zp1+1
.%0322!     rts
.%0323!
.%0324!  ;*** ram0 load/store(.X) expn memory [zp1] <- -> (zw1) for .AY bytes
.%0325!

This is the code that does the fetching and stashing from/to expansion
memory.  The only difference between a fetch and a stash is the REU Controller
command code, so that is an input parameter.  The REU Controller registers are
set up, the clock is slowed, the transfer happens, and then the clock speed is
restored.  The bulk transfer is done entirely by the REU Controller.
Interestingly, it would have been faster to transfer the internal memory to
expansion memory and then fetch it back again in order to achieve an internal
memory transfer (if you have an REU), but I didn't bother with that.

.%0326!  doReu = *
.%0327!     sta reu+7
.%0328!     sty reu+8
.%0329!     lda zw1
.%0330!     ldy zw1+1
.%0331!     sta reu+2
.%0332!     sty reu+3
.%0333!     lda zp1
.%0334!     ldy zp1+1
.%0335!     sta reu+4
.%0336!     sty reu+5
.%0337!     lda zp1+2
.%0338!     sta reu+6
.%0339!     ldy vic+$30
.%0340!     lda #0
.%0341!     sta vic+$30
.%0342!     stx reu+1
.%0343!     sty vic+$30
.%0344!     rts
.%0345!
.%0346!  ;*** sniffREU - determine number of banks of expansion memory
.%0347!

The work locations are used to store a string to the first four addresses of
each expansion memory bank and then fetch them back again in order to
determine whether the bank exists or not.  Expansion bank #0 is also checked
after each bank to see if a bank number wrap-around occured.  The
"reuSizeLimit" will force this routine to stop searching after that number of
banks have been sniffed.  The maximum value is 127, since only bank numbers
$80 to $FE are available.  By changing this value, you can stop this package
from using expansion memory reserved by another program.  Note that this
program uses expansion banks 0 up to but not including "reuSizeLimit".

.%0348!  sniffWork1 = sysWork
.%0349!  sniffWork2 = sysWork+4
.%0350!  reuSizeLimit .byte 127
.%0351!
.%0352!  sniffREU = *

Here I save the data in the memory "beneath" the REU Controller registers.  If
there isn't a REU installed, this memory would otherwise be corrupted by I/O
addresses bleeding through to the underlying RAM.

.%0353!     lda #bkRam0
.%0354!     sta bkSelect
.%0355!     ldx #$a
.%0356!  -  lda reu,x
.%0357!     sta workBuffer,x
.%0358!     dex
.%0359!     bpl -
.%0360!     lda #bkSys
.%0361!     sta bkSelect

Here I initialize the configuration REU Controller registers.  They are set
only once by this package.

.%0362!     lda #$00
.%0363!     sta reu+$9
.%0364!     sta reu+$a
.%0365!     lda reu+$0

The three-byte identifier string is copied into the source tag.  The fourth
byte will be filled in by the bank number.

.%0366!     ldx #2
.%0367!  -  lda expRamId,x
.%0368!     sta sniffWork1,x
.%0369!     dex
.%0370!     bpl -

Initialization continues.

.%0371!     lda #0
.%0372!     sta nExpBanks
.%0373!     lda #$00
.%0374!     ldx #bkExp0
.%0375!     sta zp1
.%0376!     sta zp1+1
.%0377!     stx zp1+2
.%0378!

This is the main loop.  It tests the current expansion bank and then goes on
to the next one if ok.  Otherwise, it stops at the number of okay banks.

.%0379!  -  jsr testExpBank
.%0380!     bcs +
.%0381!     inc nExpBanks
.%0382!     inc zp1+2
.%0383!     bne -
.%0384!  +  lda nExpBanks
.%0385!     bne +

Restore the underlying RAM contents and exit.

.%0386!     lda #bkRam0
.%0387!     sta bkSelect
.%0388!     ldx #$a
.%0389!  -  lda workBuffer,x
.%0390!     sta reu,x
.%0391!     dex
.%0392!     bpl -
.%0393!     lda #bkSys
.%0394!     sta bkSelect
.%0395!  +  rts
.%0396!
.%0397!  ;*** test expansion bank( [zp1]=BankPtr ) : .CC=ok
.%0398!

First checks that the maximum number of allowed expansion banks has not been
exceeded.  Stores the test string through the bank pointer and then tests to
see that the string has been stored correctly and that the string on expansion
bank 0 is still ok (it wouldn't be ok if a wrap-around occured).

.%0399!  testExpBank = *
.%0400!     lda nExpBanks
.%0401!     cmp reuSizeLimit
.%0402!     bcc +
.%0403!     rts
.%0404!  +  lda zp1+2
.%0405!     sta sniffWork1+3
.%0406!     ldx #sniffWork1
.%0407!     ldy #4
.%0408!     jsr zpstore
.%0409!     jsr testExpBankInternal  ;test current bank
.%0410!     bcs +
.%0411!     lda zp1+2
.%0412!     pha
.%0413!     lda #bkExp0
.%0414!     sta zp1+2
.%0415!     sta sniffWork1+3
.%0416!     jsr testExpBankInternal  ;test expansion bank 0
.%0417!     pla
.%0418!     sta zp1+2
.%0419!  +  rts
.%0420!

This routine reads the bytes at address [zp1] and makes sure they are the same
as the previous routine put there.  On return, the carry flag is set if the
string found is not the same as what was previously put out.

.%0421!  testExpBankInternal = *
.%0422!     lda #$00
.%0423!     sta sniffWork2
.%0424!     sta sniffWork2+3
.%0425!     ldx #sniffWork2
.%0426!     ldy #4
.%0427!     jsr zpload
.%0428!     ldx #3
.%0429!  -  lda sniffWork2,x
.%0430!     cmp sniffWork1,x
.%0431!     bne +
.%0432!     dex
.%0433!     bpl -
.%0434!     clc
.%0435!     rts
.%0436!  +  sec
.%0437!     rts
.%0438!

This is the three-byte string put into the expansion banks.  The value means
"RAM identifier".

.%0439!  expRamId   .byte "r"
.%0440!             .byte "I"
.%0441!             .byte "d"
.%0442!
.%0443!  ;--------------------------------------------------------------------
.%0444!  ;*** initialize dynamically allocated memory() : nExpBanks
.%0445!

This routine calls "free" to initialize the free memory on each existing
bank.  RAM0 is set to be free from $4000 to the top of BASIC memory, so you'll
have to change the "ram0FreeStartPage" parameter if you want to have a program
that occupies memory higher than this address.  RAM1 is declared to be free
from $0400 to $FEFF

.%0446!  ram0FreeStartPage .byte $40
.%0447!  ram1FreeStartPage .byte $04
.%0448!  ram1FreeLength    .byte 256-1-$04
.%0449!
.%0450!  currentExpBank = sysWork+$f
.%0451!
.%0452!  initDynamicMemory = *

Set the memory allocation first free chunk pointer to Null and set the number
of bytes of free memory to 0.

.%0453!     ldx #2
.%0454!  -  lda #$00
.%0455!     sta freeMemory,x
.%0456!     lda #$ff
.%0457!     sta mallocHead,x
.%0458!     dex
.%0459!     bpl -

Determine the length of free memory on RAM0 and free the memory.

.%0460!     sec
.%0461!     lda $1212   ;top of BASIC program Low
.%0462!     beq +
.%0463!     clc
.%0464!  +  lda $1213   ;top of BASIC program High
.%0465!     sbc ram0FreeStartPage
.%0466!     tay
.%0467!     lda ram0FreeStartPage
.%0468!     ldx #bkRam0
.%0469!     jsr initInternalBankMalloc

Free the memory of RAM1

.%0470!     lda ram1FreeStartPage
.%0471!     ldy ram1FreeLength
.%0472!     ldx #bkRam1
.%0473!     jsr initInternalBankMalloc
.%0474!

For each existing expansion bank, free it from addresses $0000 to $FFF7.  You
cannot free all 65536 bytes since this would cause the length of the free
chunk to be set to $0000 which would cause problems later on.  $FFF8 bytes are
set to free since then length has to be a multiple of eight bytes.

.%0475!     lda #0
.%0476!     sta currentExpBank
.%0477!  -  lda currentExpBank
.%0478!     cmp nExpBanks
.%0479!     bcs +
.%0480!     ora #bkExp0
.%0481!     sta zp1+2
.%0482!     lda #$00
.%0483!     sta zp1
.%0484!     sta zp1+1
.%0485!     lda #$f8
.%0486!     ldy #$ff
.%0487!     jsr free
.%0488!     inc currentExpBank
.%0489!     bne -
.%0490!  +  rts
.%0491!

This routine is called for freeing banks RAM0 and RAM1.  It does nothing other
than set parameters and is put in for convenience.

.%0492!  initInternalBankMalloc = *
.%0493!     sta zp1+1
.%0494!     stx zp1+2
.%0495!     lda #0
.%0496!     sta zp1
.%0497!     jmp free
.%0498!
.%0499!  ;--------------------------------------------------------------------
.%0500!  ;*** malloc( .AY=Bytes ) : [zp1]=FarPointer
.%0501!

One of the biggies.  The "MemNextPtr" and "MemLength" variables are used to
store the information at the start of the current free memory chunk.  "Length"
is used to hold the length input parameter and "Q" is the pointer to the
previous free memory chunk whereas "zp1" is used to point to the current free
chunk.  I prefix these variables with "malloc" to avoid naming collisions with
other routines.  The concept of local variables might be a nice thing for
future assemblers to have.

.%0502!  mallocMemNextPtr = sysWork
.%0503!  mallocMemLength  = sysWork+3
.%0504!  mallocLength     = sysWork+5
.%0505!  mallocQ          = sysWork+7
.%0506!
.%0507!  internAlloc = *

Align the number of bytes requested to an even multiple of eight.

.%0508!     clc
.%0509!     adc #7
.%0510!     bcc +
.%0511!     iny
.%0512!  +  and #$f8
.%0513!     sta mallocLength
.%0514!     sty mallocLength+1

Set the current free chunk pointer to the first free chunk and set Q to Null.

.%0515!     ldx #2
.%0516!  -  lda mallocHead,x
.%0517!     sta zp1,x
.%0518!     lda #$ff
.%0519!     sta mallocQ,x
.%0520!     dex
.%0521!     bpl -
.%0522!

Search for a free chunk that is long enough to satisfy the request.

.%0523!     mallocLook = *

If the current free chunk pointer is Null, then we are S.O.L. (Out of Luck)
since that means we have exhausted the list of free chunks and have to report
that insufficient free memory could be found.

.%0524!     lda zp1+2
.%0525!     cmp #$ff
.%0526!     bne +
.%0527!
.%0528!     mallocErrorExit = *
.%0529!     lda #$ff   ;return a Null pointer
.%0530!     sta zp1
.%0531!     sta zp1+1
.%0532!     sta zp1+2
.%0533!     lda #errInsufficientMemory
.%0534!     sta errno
.%0535!     sec
.%0536!     rts
.%0537!

Fetch the header information of the current free chunk and check the length.
If the current free chunk is not large enough, then we set the Q pointer to
the current pointer, and take the new value for the current pointer from the
header of the current free chunk (mallocMemNextPtr) and then continue
searching.

.%0538!  +  ldx #mallocMemNextPtr
.%0539!     ldy #5
.%0540!     jsr zpload
.%0541!     lda mallocMemLength
.%0542!     cmp mallocLength
.%0543!     lda mallocMemLength+1
.%0544!     sbc mallocLength+1
.%0545!     bcs mallocGotBlock
.%0546!     ldx #2
.%0547!  -  lda zp1,x
.%0548!     sta mallocQ,x
.%0549!     lda mallocMemNextPtr,x
.%0550!     sta zp1,x
.%0551!     dex
.%0552!     bpl -
.%0553!     jmp mallocLook
.%0554!

Now, we've found a block that is large enough.

.%0555!     mallocGotBlock = *
.%0556!     sec

Subtract the number of bytes requested from the total number of bytes free.

.%0557!     lda freeMemory
.%0558!     sbc mallocLength
.%0559!     sta freeMemory
.%0560!     lda freeMemory+1
.%0561!     sbc mallocLength+1
.%0562!     sta freeMemory+1
.%0563!     bcs +
.%0564!     dec freeMemory+2

If the size of the current free chunk is exactly the same as the number of
bytes requested, then branch ahead.

.%0565!  +  lda mallocMemLength
.%0566!     cmp mallocLength
.%0567!     bne +
.%0568!     lda mallocMemLength+1
.%0569!     sbc mallocLength+1
.%0570!     beq mallocTakeWholeBlock

Subtract the number of bytes requested from the length of the current free
chunk and then write the updated header back to the current free chunk.

.%0571!  +  sec
.%0572!     lda mallocMemLength
.%0573!     sbc mallocLength
.%0574!     sta mallocMemLength
.%0575!     lda mallocMemLength+1
.%0576!     sbc mallocLength+1
.%0577!     sta mallocMemLength+1
.%0578!     ldx #mallocMemNextPtr
.%0579!     ldy #5
.%0580!     jsr zpstore

Add the length of the free chunk to the pointer to the start of the free chunk
to determine the address of the memory that has just been allocated.  Then
exit, returning this address.

.%0581!     clc
.%0582!     lda zp1
.%0583!     adc mallocMemLength
.%0584!     sta zp1
.%0585!     lda zp1+1
.%0586!     adc mallocMemLength+1
.%0587!     sta zp1+1
.%0588!     clc
.%0589!     rts
.%0590!

Here, the size of the free chunk is exactly the same size as the request, so
the entire block has to be allocated and thus removed from the free chunk
list.  This is why the Q pointer has been maintained.

.%0591!     mallocTakeWholeBlock = *

If there is no previous block (Q == Null) then set the free chunk list head
pointer to the next free chunk after the current one.  Then exit with the
current chunk as the return pointer.

.%0592!     lda mallocQ+2
.%0593!     cmp #bkNull
.%0594!     bne +
.%0595!     ldx #2
.%0596!  -  lda mallocMemNextPtr,x
.%0597!     sta mallocHead,x
.%0598!     dex
.%0599!     bpl -
.%0600!     clc
.%0601!     rts

If there is an actual previous chunk, then we have to set it to point to the
next chunk from the current chunk.  This will unlink the current free chunk
from the free chunk list, thereby allocating it.

First, we swap the Q and current pointers, since we can only access memory
through the "zp1" pointer.

.%0602!  +  ldx #2
.%0603!  -  lda zp1,x
.%0604!     ldy mallocQ,x
.%0605!     sta mallocQ,x
.%0606!     sty zp1,x
.%0607!     dex
.%0608!     bpl -

Then we set the the NextPointer of the previous free chunk to point to the
next free chunk after the current chunk.

.%0609!     ldx #mallocMemNextPtr
.%0610!     ldy #3
.%0611!     jsr zpstore

And then we restore the current chunk pointer and return it to the user.

.%0612!     ldx #2
.%0613!  -  lda mallocQ,x
.%0614!     sta zp1,x
.%0615!     dex
.%0616!     bpl -
.%0617!     clc
.%0618!     rts
.%0619!
.%0620!  ;*** free( [zp1]=FarPointer, .AY=Length )  {alters [zp1]}
.%0621!

And here is the real biggie, since Free is more complicated than Malloc.  The
variables are the same as for free, except that "NewPtr" is required to
remember the input parameter to new chunk to be freed.

.%0622!  freeMemNextPtr = sysWork
.%0623!  freeMemLength  = sysWork+3
.%0624!  freeLength     = sysWork+5
.%0625!  freeNewPtr     = sysWork+7
.%0626!  freeQ          = sysWork+10
.%0627!
.%0628!  internFree = *

Again, align the length of the chunk.  The pointer to the start of the new
chunk is assumed to be aligned (since malloc only returns aligned chunks).  If
the chunk pointer is not aligned, all hell can break loose.

.%0629!     clc
.%0630!     adc #7
.%0631!     bcc +
.%0632!     iny
.%0633!  +  and #$f8
.%0634!     sta freeLength
.%0635!     sty freeLength+1

Save the new chunk input parameter and set "zp1" for searching the free chunk
list.  Also set Q to Null since Q will be used to remember the previous block
to "zp1".

.%0636!     ldx #2
.%0637!  -  lda zp1,x
.%0638!     sta freeNewPtr,x
.%0639!     lda mallocHead,x
.%0640!     sta zp1,x
.%0641!     lda #$ff
.%0642!     sta freeQ,x
.%0643!     dex
.%0644!     bpl -
.%0645!

Search for the two free chunks whose addresses straddle the new free chunk.

.%0646!     freeSearchLoop = *

If the current free chunk pointer is Null or if the current free chunk's bank
number is less than the new chunk's bank number, then we can stop searching;
we have found a free chunk that is "higher" than the new chunk, so Q and zp1
must straddle the address of the new chunk.  Note that by using a "bcc" on
line 652, external memory free chunks will be allocated first.  If I had used
a "bcs" there, the internal memory starting from RAM0 would be allocated
first.

.%0647!     lda zp1+2
.%0648!     cmp #$ff
.%0649!     beq freeCoalesceQandNew
.%0650!     lda zp1+2
.%0651!     cmp freeNewPtr+2
.%0652!     bcc freeCoalesceQandNew  ;** determines bank order

Here we know that the bank number is not "higher", so if the bank numbers are
not equal, then we continue searching.  If the bank numbers are equal, we must
check the addresses within the bank to see if zp1 is higher than the new
chunk.  If so, we stop searching.

.%0653!     bne +
.%0654!     lda zp1
.%0655!     cmp freeNewPtr
.%0656!     lda zp1+1
.%0657!     sbc freeNewPtr+1
.%0658!     bcs freeCoalesceQandNew

Here we continue searching.  We stick the current free chunk pointer into Q
and get the next free chunk pointer from the current chunk in memory.  Then we
go back to the top of the search.

.%0659!  +  ldx #freeMemNextPtr
.%0660!     ldy #3
.%0661!     jsr zpload
.%0662!     ldx #2
.%0663!  -  lda zp1,x
.%0664!     sta freeQ,x
.%0665!     lda freeMemNextPtr,x
.%0666!     sta zp1,x
.%0667!     dex
.%0668!     bpl -
.%0669!     bmi freeSearchLoop
.%0670!

Here we know that Q and zp1 straddle the new chunk, and we try to coalesce the
new chunk to the Q chunk.

.%0671!     freeCoalesceQandNew = *
.%0672!     ldx #2
.%0673!  -  lda freeQ,x
.%0674!     sta zp1,x
.%0675!     dex
.%0676!     bpl -

If the Q pointer is Null, then there is no Q chunk to coalesce with, so the
free chunk head pointer is set to point to the new chunk and the new chunk
header is set to the size of the new chunk.  Then next pointer for the new
chunk is set to what was previously the head pointer.

.%0677!     lda zp1+2
.%0678!     cmp #$ff
.%0679!     bne +
.%0680!     ldx #2
.%0681!  -  lda mallocHead,x
.%0682!     sta freeMemNextPtr,x
.%0683!     lda freeNewPtr,x
.%0684!     sta mallocHead,x
.%0685!     dex
.%0686!     bpl -
.%0687!     lda freeLength
.%0688!     ldy freeLength+1
.%0689!     sta freeMemLength
.%0690!     sty freeMemLength+1
.%0691!     jmp freeCoalesceNewAndP
.%0692!

Here there actually is a previous (Q) chunk, so its header is fetched.  If it
is not on the same bank as the new chunk, then the new chunk cannot be
coalesced with it.  Also, if the address of the new chunk does not exactly
follow the Q chunk, then they cannot be coalesced.

.%0693!  +  ldx #freeMemNextPtr
.%0694!     ldy #5
.%0695!     jsr zpload
.%0696!     lda zp1+2
.%0697!     cmp freeNewPtr+2
.%0698!     bne +
.%0699!     clc
.%0700!     lda zp1
.%0701!     adc freeMemLength
.%0702!     tax
.%0703!     lda zp1+1
.%0704!     adc freeMemLength+1
.%0705!     cmp freeNewPtr+1
.%0706!     bne +
.%0707!     cpx freeNewPtr
.%0708!     bne +

Here, we know that the previous chunk and the new chunk can be coalesced.  We
add the length of the new chunk to the length of the previous chunk and change
the new chunk pointer to point to the previous chunk.

.%0709!     clc
.%0710!     lda freeMemLength
.%0711!     adc freeLength
.%0712!     sta freeMemLength
.%0713!     lda freeMemLength+1
.%0714!     adc freeLength+1
.%0715!     sta freeMemLength+1
.%0716!     ldx #2
.%0717!  -  lda freeQ,x
.%0718!     sta freeNewPtr,x
.%0719!     dex
.%0720!     bpl -
.%0721!     bmi freeCoalesceNewAndP
.%0722!

Here, we know that the previous and new chunks cannot be coalesced.  We change
the actual header of the pervious chunk to point to the new chunk and change
the new chunk header length to the free request length.  The pointer to the
next chunk is already in the new chunk header from before.  Note that now we
are using "memNextPtr" and "memLength" to construct the new free chunk
header.  Line 729 caused Mr. Bruce some problems because he forgot to stick
the "+1" there after extracting the code from Zed.

.%0723!  +  ldx #freeNewPtr
.%0724!     ldy #3
.%0725!     jsr zpstore
.%0726!     lda freeLength
.%0727!     ldy freeLength+1
.%0728!     sta freeMemLength
.%0729!     sty freeMemLength+1
.%0730!

At this point, we are finished trying to coalesce the new chunk with the
previous chunk, so we will attempt to coalesce the new chunk with the next
higher address free chunk.  The "memNextPtr" and "memLength" variables hold
the header information for the new chunk (the "memNextPtr" also points to the
next free chunk), and "NewPtr" points to the new chunk.  We check to see if
the new chunk immediately preceeds the next chunk in the same way as before.
Note that the case of a Null next chunk pointer is handled here implicitly,
since the bank numbers won't match.

.%0731!     freeCoalesceNewAndP = *
.%0732!     lda freeNewPtr+2
.%0733!     cmp freeMemNextPtr+2
.%0734!     bne +
.%0735!     clc
.%0736!     lda freeNewPtr
.%0737!     adc freeMemLength
.%0738!     tax
.%0739!     lda freeNewPtr+1
.%0740!     adc freeMemLength+1
.%0741!     cmp freeMemNextPtr+1
.%0742!     bne +
.%0743!     cpx freeMemNextPtr
.%0744!     bne +
.%0745!

Here, we know that the new chunk can be coalesced with the next chunk.  We
have to fetch the header of the next chunk to know the length and the pointer
to the free chunk after the next chunk.  We then add the length of the next
chunk to the length of the new chunk and keep the pointer to the chunk after
the next chunk for the new chunk header.  Effectively, the next free chunk is
unlinked (since nothing is left to point to it) and the new chunk grows to
swallow it up.

.%0746!     ldx #2
.%0747!  -  lda freeMemNextPtr,x
.%0748!     sta zp1,x
.%0749!     dex
.%0750!     bpl -
.%0751!     lda freeMemLength+1
.%0752!     pha
.%0753!     lda freeMemLength
.%0754!     pha
.%0755!     ldx #freeMemNextPtr
.%0756!     ldy #5
.%0757!     jsr zpload
.%0758!     clc
.%0759!     pla
.%0760!     adc freeMemLength
.%0761!     sta freeMemLength
.%0762!     pla
.%0763!     adc freeMemLength+1
.%0764!     sta freeMemLength+1
.%0765!

Here, we wrap things up.  We have the header for the new free chunk all
prepared and we have tried to coalesce the two neighboring chunks to the new
chunk.  All we do now is write the new chunk header out to main memory and
increase the number of bytes free variable by the length of the (original)
free request.

.%0766!  +  ldx #2
.%0767!  -  lda freeNewPtr,x
.%0768!     sta zp1,x
.%0769!     dex
.%0770!     bpl -
.%0771!     ldx #freeMemNextPtr
.%0772!     ldy #5
.%0773!     jsr zpstore
.%0774!     clc
.%0775!     lda freeMemory
.%0776!     adc freeLength
.%0777!     sta freeMemory
.%0778!     lda freeMemory+1
.%0779!     adc freeLength+1
.%0780!     sta freeMemory+1
.%0781!     bcc +
.%0782!     inc freeMemory+2

We always return with carry cleared, since we don't check for any errors.

.%0783!  +  clc
.%0784!     rts
.%0785!
.%0786!  ;--------------------------------------------------------------------
.%0787!  ;*** sort - the application: reads from file #1, writes to file #2
.%0788!

This is where the actual application code starts.  If you want to write your
own program that uses the dynamic memory allocation package, then you can
follow the structure of this application.

We start off by declaring the storage areas for the current line being
processed and for the line being compared to the current line.  The addresses
reflect the structure of the record for the input line that was discussed
earlier.  The sorting field starting column number parameter is can be put at
$8FF since the input line can only be 242 characters long.

.%0789!  sortbuf    = $b00
.%0790!  sortbuflen = $b03
.%0791!  sortline   = $b04
.%0792!  cmpbuf     = $800
.%0793!  cmpbuflen  = $803
.%0794!  cmpline    = $804
.%0795!  sortColumn = $8ff
.%0796!

These are the zero page locations that sort uses.

.%0797!  eofstat  = $02  ;deferred ST variable ($90)
.%0798!  sorthead = $03  ;pointer to first line in line list
.%0799!  sortP    = $06  ;current line for list searching
.%0800!  sortQ    = $09  ;previous line for list searching
.%0801!  header   = $0c  ;4 bytes - holds the current line record's header
.%0802!

And these are the kernel routines that are called.

.%0803!  kernelChkin  = $ffc6
.%0804!  kernelChkout = $ffc9
.%0805!  kernelClrchn = $ffcc
.%0806!  kernelChrin  = $ffcf
.%0807!  kernelChrout = $ffd2

"echoStatus" can be changed to point to an RTS if you do not want sort to
print status information out while it is working.

.%0808!  echoStatus   = kernelChrout
.%0809!
.%0810!  ;*** getline( sortline ) : .CS=eof
.%0811!

This routine reads a new line in from the current input channel and puts it
into the processing buffer.  It returns with carry set if there are no more
lines to read or if a read error occurs.

.%0812!  getline = *
.%0813!     ldy #0

The "eofstat" is checked first to see if the previous character read before
the new call was the last of the file.  This overcomes the kernel's awkward
way of setting EOI for the last character rather than when for when you go
beyond the last character.

.%0814!  -  bit eofstat
.%0815!     bvs getlineEof
.%0816!     jsr kernelChrin
.%0817!     bcs getlineEof
.%0818!     sta sortline,y
.%0819!     iny
.%0820!     ldx $90
.%0821!     stx eofstat

It exits when the maximum line length is exceeded or when a carriage return is
encountered.

.%0822!     cpy #242
.%0823!     bcs getlineExit
.%0824!     cmp #13
.%0825!     bne -
.%0826!     dey
.%0827!

A trailing '\0' is appended to the string for easier processing later, and the
length of the input line record is recorded.  The length of the entire record
rather than the length of just the text is more convenient to know when
working with the memory package.

.%0828!     getlineExit = *
.%0829!     lda #0
.%0830!     sta sortline,y
.%0831!     clc
.%0832!     tya
.%0833!     adc #5
.%0834!     sta sortbuflen
.%0835!     clc
.%0836!     rts
.%0837!

On end of file, we exit with carry set.  If, however, we have read characters
before the EOF was encountered, they are returned as belonging to the last
line of the file.  True EOF will be returned on the next call.

.%0838!     getlineEof = *
.%0839!     lda #$40
.%0840!     sta eofstat
.%0841!     cpy #0
.%0842!     bne getlineExit
.%0843!     sec
.%0844!     rts
.%0845!
.%0846!  ;*** putline( appline )
.%0847!

This routine simply writes out the current line ('\0' terminated) and writes
an additional carriage return, since the getline routine strips off the CR.

.%0848!  putline = *
.%0849!     ldy #0
.%0850!  -  lda sortline,y
.%0851!     beq +
.%0852!     jsr kernelChrout
.%0853!     iny
.%0854!     bne -
.%0855!  +  lda #13
.%0856!     jmp kernelChrout
.%0857!
.%0858!  ;*** fetchline( sortP=LinePtr, .AY=Ram0buf )
.%0859!

This routine fetches the line at the pointer sortP into RAM0 at the given
address.  It has to zpload the line header first to determine the record size
to fetch.

.%0860!  fetchline = *
.%0861!     sta zw1
.%0862!     sty zw1+1
.%0863!     ldx #2
.%0864!  -  lda sortP,x
.%0865!     sta zp1,x
.%0866!     dex
.%0867!     bpl -
.%0868!     ldx #header
.%0869!     ldy #4
.%0870!     jsr zpload
.%0871!     lda header+3
.%0872!     ldy #0
.%0873!     jmp fetch
.%0874!
.%0875!  ;*** sortGTcmp( sortline, cmpline ) : .CS={sortline >= cmpline}
.%0876!

This routine compares the lines stored in the "sortline" and "cmpline" buffers
and returns with carry set if the "sortline" is larger (alphabetically).  It
also takes into account the starting comparison positions and handles the case
of either or both lines not being as long as the start position of the string
comparison.

.%0877!  sortGTcmp = *

This section of code makes bit0 of .X a "1" if sortline is not long enough to
be compared, and makes bit1 a "1" if cmpline is too short.

.%0878!     ldx #0
.%0879!     clc
.%0880!     lda sortColumn
.%0881!     adc #5
.%0882!     cmp sortbuflen
.%0883!     bcc +
.%0884!     inx
.%0885!  +  cmp cmpbuflen
.%0886!     bcc +
.%0887!     inx
.%0888!     inx

And here is where it takes action depending whether the lines are large enough
or not.  The cases are:

. .X=%00000000 - strings are long enough to be compared, so continue
. .X=%00000001 - sortline is too short, cmpline ok, so return with carry clear
. .X=%00000010 - cmpline is too short, sortline ok, so return with carry set
. .X=%00000011 - both sortline and cmpline are too short; carry set

.%0889!  +  txa
.%0890!     beq doCompare
.%0891!     cmp #2
.%0892!     rts
.%0893!
.%0894!     doCompare = *

This section does the compare if both lines are long enough.

.%0895!     ldy sortColumn
.%0896!  -  lda sortline,y
.%0897!     cmp cmpline,y
.%0898!     bne +
.%0899!     cmp #0
.%0900!     beq +
.%0901!     iny
.%0902!     bne -
.%0903!  +  rts
.%0904!
.%0905!  ;*** positionLine( sortline ) : sortQ=prev, sortP=next
.%0906!

This routine searches for the correct position in the line list to insert the
new line, and returns sortQ and sortP to straddle the new line position.  Note
that this routine causes the list to be in reverse order as discussed
earlier.

.%0907!  positionLine = *

Set P to head and Q to Null.

.%0908!     ldx #2
.%0909!  -  lda #bkNull
.%0910!     sta sortQ,x
.%0911!     lda sorthead,x
.%0912!     sta sortP,x
.%0913!     dex
.%0914!     bpl -
.%0915!
.%0916!     positionSearch = *

This routine breaks out if the current line pointer is Null.  Otherwise, it
fetches the current line pointer (sortP) into the cmpline buffer and calls the
string compare routine.  If the new line read in from the file is greater than
or equal to the current line already in the list, the search kicks out.  The
"bcs" on line 924 controls the order of the sort.  Otherwise, the P and Q
pointers are updated in the usual way and the search continues.

.%0917!     lda sortP+2
.%0918!     cmp #bkNull
.%0919!     beq positionExit
.%0920!     lda #<cmpbuf
.%0921!     ldy #>cmpbuf
.%0922!     jsr fetchline
.%0923!     jsr sortGTcmp
.%0924!     bcs positionExit    ;** controls sort order
.%0925!     ldx #2
.%0926!  -  lda sortP,x
.%0927!     sta sortQ,x
.%0928!     lda cmpbuf,x
.%0929!     sta sortP,x
.%0930!     dex
.%0931!     bpl -
.%0932!     bmi positionSearch
.%0933!
.%0934!     positionExit = *

At this point, sortP and sortQ straddle the position to put the new line, so
we return.

.%0935!     rts
.%0936!
.%0937!  ;*** storeline( sortline )    {between sortQ and sortP}
.%0938!

This routine actually stores the new line read in between the sortQ and sortP
lines.

.%0939!  storeline = *

First, space for the new line is allocated.

.%0940!     lda sortbuflen
.%0941!     ldy #0
.%0942!     jsr malloc
.%0943!     bcc +
.%0944!     rts

And the new line's next pointer is set to point to sortP.

.%0945!  +  ldx #2
.%0946!  -  lda sortP,x
.%0947!     sta sortbuf,x
.%0948!     dex
.%0949!     bpl -

And the new line is stashed out to main memory.

.%0950!     lda #<sortbuf
.%0951!     ldy #>sortbuf
.%0952!     sta zw1
.%0953!     sty zw1+1
.%0954!     lda sortbuflen
.%0955!     ldy #0
.%0956!     jsr stash

Now all that is left to is make the previous line record (sortQ) point to the
new line record.

.%0957!     lda sortQ+2
.%0958!     cmp #bkNull
.%0959!     beq storelineFirst

If there is an actual previous line, the new line pointer is written out over
the next line pointer in its header.

.%0960!     ldx #2
.%0961!  -  lda zp1,x
.%0962!     ldy sortQ,x
.%0963!     sta sortQ,x
.%0964!     sty zp1,x
.%0965!     dex
.%0966!     bpl -
.%0967!     ldx #sortQ
.%0968!     ldy #3
.%0969!     jsr zpstore
.%0970!     clc
.%0971!     rts
.%0972!

If there is no actual previous line, then the line list head pointer is set to
point to the new line (which is now the first line on the list).

.%0973!     storelineFirst = *
.%0974!     ldx #2
.%0975!  -  lda zp1,x
.%0976!     sta sorthead,x
.%0977!     dex
.%0978!     bpl -
.%0979!     clc
.%0980!     rts
.%0981!
.%0982!  ;*** readfile()
.%0983!

This routine reads in the file and puts the lines into their correct sorted
positions as it is reading.

.%0984!  readfile = *

Clear the line list by setting the head pointer to Null.

.%0985!     ldx #2
.%0986!     lda #bkNull
.%0987!  -  sta sorthead,x
.%0988!     dex
.%0989!     bpl -

Set the EOF flag to 0 and set the current input channel to logical file #1
which is assumed to be opened before the sort utility is invoked.

.%0990!     lda #0
.%0991!     sta eofstat
.%0992!     ldx #1
.%0993!     jsr kernelChkin
.%0994!     bcs readExit

Until EOF, read the new line, find the position in the line list, store it,
print out a "." to indicate to the user that another line has been processed,
and repeat.  Exit on EOF.

.%0995!  -  jsr getline
.%0996!     bcs readExit
.%0997!     jsr positionLine
.%0998!     jsr storeline
.%0999!     bcs readExit
.%1000!     lda #"."
.%1001!     jsr echoStatus
.%1002!     jmp -
.%1003!
.%1004!     readExit = *
.%1005!     rts
.%1006!
.%1007!  ;*** writefile()
.%1008!

This routine writes the line list out to logical file number 2 which is
assumed to be opened before the sort utility is invoked.  This routine follows
the standard structure for processing a linked list.

.%1009!  writefile = *
.%1010!     ldx #2
.%1011!  -  lda sorthead,x
.%1012!     sta sortP,x
.%1013!     dex
.%1014!     bpl -
.%1015!     ldx #2
.%1016!     jsr kernelChkout
.%1017!
.%1018!     writeLine = *
.%1019!     lda sortP+2
.%1020!     cmp #bkNull
.%1021!     beq writeExit
.%1022!     lda #<sortbuf
.%1023!     ldy #>sortbuf
.%1024!     jsr fetchline
.%1025!     jsr putline
.%1026!     ldx #2
.%1027!  -  lda sortbuf,x
.%1028!     sta sortP,x
.%1029!     dex
.%1030!     bpl -
.%1031!     jmp writeLine
.%1032!
.%1033!     writeExit = *
.%1034!     jsr kernelClrchn
.%1035!     rts
.%1036!
.%1037!  ;*** reverseList()
.%1038!

This routine will reverse the order of the line list.  Starting from the head
line, each line is extracted and is made to point to the previous line
extracted.  No data actually has to be moved around; only the headers of the
line records have to be changed.

.%1039!  reverseFile = *
.%1040!     ldx #2
.%1041!  -  lda sorthead,x
.%1042!     sta zp1,x
.%1043!     lda #bkNull
.%1044!     sta sorthead,x
.%1045!     dex
.%1046!     bpl -
.%1047!
.%1048!     reverseLine = *
.%1049!     lda zp1+2
.%1050!     cmp #bkNull
.%1051!     beq reverseExit

Fetch the pointer from the current line into sortP and then replace it with
the value at sorthead (the previous line altered).

.%1052!     ldx #sortP
.%1053!     ldy #3
.%1054!     jsr zpload
.%1055!     ldx #sorthead
.%1056!     ldy #3
.%1057!     jsr zpstore

Make sorthead point to the current line, and then go to the next line whose
pointer was extracted from the current line (before the current line was
changed).

.%1058!     ldx #2
.%1059!  -  lda zp1,x
.%1060!     sta sorthead,x
.%1061!     lda sortP,x
.%1062!     sta zp1,x
.%1063!     dex
.%1064!     bpl -
.%1065!     bmi reverseLine
.%1066!
.%1067!     reverseExit = *
.%1068!     rts
.%1069!
.%1070!  ;*** freefile()
.%1071!

This routine scans through the lines in the line list and deallocates each
line record.

.%1072!  freefile = *
.%1073!     ldx #2
.%1074!  -  lda sorthead,x
.%1075!     sta zp1,x
.%1076!     dex
.%1077!     bpl -
.%1078!
.%1079!     freeLine = *
.%1080!     lda zp1+2
.%1081!     cmp #bkNull
.%1082!     bne +
.%1083!     rts
.%1084!  +  ldx #header
.%1085!     ldy #4
.%1086!     jsr zpload
.%1087!     lda header+3
.%1088!     ldy #0
.%1089!     jsr free
.%1090!     ldx #2
.%1091!  -  lda header,x
.%1092!     sta zp1,x
.%1093!     dex
.%1094!     bpl -
.%1095!     jmp freeLine
.%1096!
.%1097!  ;*** main()
.%1098!

Finally!  The main routine sets the sort key column and calls each of the
subroutines for the different phases of the sort and prints out a letter
indicating what the program is currently doing.

.%1099!  main = *
.%1100!     cmp #1
.%1101!     bcc +
.%1102!     sbc #1
.%1103!  +  sta sortColumn
.%1104!     lda #"s"
.%1105!     jsr echoStatus
.%1106!     jsr startup
.%1107!     lda #"r"
.%1108!     jsr echoStatus
.%1109!     jsr readfile
.%1110!     lda #"v"
.%1111!     jsr echoStatus
.%1112!     jsr reverseFile
.%1113!     lda #"w"
.%1114!     jsr echoStatus
.%1115!     jsr writefile
.%1116!     lda #"f"
.%1117!     jsr echoStatus
.%1118!     jsr freefile
.%1119!     lda #"x"
.%1120!     jsr echoStatus
.%1121!     jsr shutdown
.%1122!     lda #13
.%1123!     jsr echoStatus

It returns with .A set to zero in case the user calls sort again and forgets
to specify a value for the sorting column using the BASIC SYS statement.

.%1124!     lda #0
.%1125!     rts

------------------------------------------------------------------------------

5. FUTURE ENHANCEMENTS

This dynamic memory allocation package does not support expanded internal
memory (as specified in Twin Cities-128 Magazine) or RamLink memory.  I am
planning to modify the memory allocation in the Zed-128 program to support
both of these kinds of memory.  The extra internal memory banks would be
accessed in a similar manner as RAM1 is, except that I will need to have some
special bank numbers for them, since they cannot be handled in exactly the
same way as RAM0 and RAM1.  I will also have to modify that other MMU register
in order to select which real banks show up in the RAM2 and RAM3 positons.

The memory inside a RamLink can be accessed in a similar way to how memory is
accessed in an REU.  One big difference is that the layout of the storage in a
RamLink is actually organized.  A RamLink (and a RamDrive I assume) can have
up to 31 partitons of various types.  I am thinking that to sniff a RamLink,
the package will check to see if you have a RamLink and will then check to see
if you have partiton number 31 set up as a "foreign" mode partition with the
name "swap".  If so, the package will ask the RL-DOS for the start address and
length of the partition and will then use the RamLink memory instead of an
REU.  This makes sense since an REU can be made to be part of the RamLink
and since you can get a lot more memory in a RamLink than I have ever heard of
in an expanded REU.  I personally have an 8 Meg RamLink and I have set aside a
1 Meg partition for the swap space.  Now I just have to write the software to
use it.

These additional types of memory can be seemlessly implemented into this
package and the usage will be compeletely transparent to the user and to the
higher level routines.

Also, although I have not attempted to do this, the code presented here could
be ported to the Commodore 64.  The common code routines would be removed
since the 64 has only one internal bank, and instead of using the MMU to
select RAM0, you would store into the processor I/O port to select the bare
internal RAM.  (You would also have to worry about interrupts happening while
you are accessing this memory).  All of the higher level code above the
zpload, zpstore, fetch and stash routines would (probably) stay (pretty much)
the same, since they call the lower level routines to do the actual
machine-specific grunt work.

If you have any questions or comments about this article, feel free to drop me
a line.

------------------------------------------------------------------------------

6. UUENCODED BINARIES

Here are the BASIC program and the machine language subroutines for the
sorting utility.  They are in uuencoded form and you will probably have to
extract them into separate files before you uudecode them.  Enjoy!

begin 640 sort
M`1PF'`$`222R(DE.4%541DE,12Y46%0B(#H@242R."`Z(%-&LC$`11P"`$\D
MLB)/5510551&24Q%+E185"(@.B!/1+(X`$L<`P`Z`&8<9`"9(DQ/041)3D<@
M4T]25"Y"24XN+BXB`'`<;@#^`B`Q-0"'''@`_A$B4T]25"Y"24XB+%4H240I
M`*4<@@"9(E-#4D%40TA)3D<@3TQ$($9)3$4N+BXB`+4<C`#R*$\D*2Q5*$]$
M*0#'')8`F2)33U)424Y'+BXN(@#;'*``GS$L240L,BPB,#HBJDDD`/8<J@"?
M,BQ/1"PS+"(P.B*J3R2J(BQ3+%<B``D=M`">(-$H(C$S,#`B*2Q31@`0';X`
=H#(`%QW(`*`Q`"@=T@"9(D9)3DE32$5$(2(````H
`
end

begin 640 sort.bin
M`!-,I!E,(Q-,-A-,R!-,_A-,#11,;11,`Q9,R18``````````*D`2"BI#HT`
M_R!($R#\%""G%6"B`*D`G0`"Z.!RT/BI`(T`_V"B`+U6$YT``NC@<I#U8*7\
MC0#_A/V@`+'ZE0#HR,3]D/:I#HT`_V"E_(T`_X3]H`"U`)'ZZ,C$_9#VJ0Z-
M`/]@B/`-C0+_L?J-`?^1_HC0\XT"_['ZC0'_D?ZI#HT`_V"(\`V-`?^Q_HT"
M_Y'ZB-#SC0'_L?Z-`O^1^JD.C0#_8*7\,`-,``*,!]^@D8T&WXX"WZT'U8T#
MWZ7ZC03?I?N-!=^I`(T(WZTPT*(`CC#0C`'?C3#08*7\,`-,&0*,!]^@D$S4
M$Z;\$`6BD4S-%,``T"*HT`%@X#_P`TPR`HX`_XCP!['ZD?Z(T/FQ^I'^J0Z-
M`/]@A8"$@:7[A8*E_X6#I8'P#Z;\H``@'A3F^^;_QH'0\:2`\`6F_"`>%*6"
MA?NE@X7_8*;\$`6BD$S-%,``T"*HT`%@X#_P`TQ2`HX`_XCP!['^D?J(T/FQ
M_I'ZJ0Z-`/]@A8"$@:7_A8*E^X6#I8'P#Z;\H``@?A3F^^;_QH'0\:2`\`6F
M_"!^%*6"A?^E@X7[8(T'WXP(WZ7^I/^-`M^,`]^E^J3[C03?C`7?I?R-!M^L
M,-"I`(TPT(X!WXPPT&!_J3^-`/^B"KT`WYT`"\H0]ZD.C0#_J0"-"=^-"M^M
M`-^B`KVA%96`RA#XJ0"-'!.I`**`A?J%^X;\(%P5L`?N'!/F_-#TK1P3T!6I
M/XT`_Z(*O0`+G0#?RA#WJ0Z-`/]@K1P3S?L4D`%@I?R%@Z*`H`0@#!,@A16P
M#Z7\2*F`A?R%@R"%%6B%_&"I`(6$A8>BA*`$(`D3H@.UA-6`T`7*$/<88#A@
M4LE$0`3[H@*I`)T@$ZG_G1T3RA#S.*T2$O`!&*T3$NVD%:BMI!6B/R#X%:VE
M%:RF%:)_(/@5J0"%CZ6/S1P3L!4)@(7\J0"%^H7[J?B@_R`8$^:/T.1@A?N&
M_*D`A?I,&!,8:0>0`<@I^(6%A(:B`KT=$Y7ZJ?^5A\H0]*7\R?_0#ZG_A?J%
M^X7\J0&-&Q,X8**`H`4@"1.E@\6%I83EAK`0H@*U^I6'M8"5^LH0]4P=%CBM
M(!/EA8T@$ZTA$^6&C2$3L`/.(A.E@\6%T`:EA.6&\",XI8/EA86#I83EAH6$
MHH"@!2`,$QBE^F6#A?JE^V6$A?L88*6)R?_0#*("M8"='1/*$/@88*("M?JT
MAY6'E/K*$/6B@*`#(`P3H@*UAY7ZRA#Y&&`8:0>0`<@I^(6%A(:B`K7ZE8>]
M'1.5^JG_E8K*$/"E_,G_\"BE_,6)D"+0"J7ZQ8>E^^6(L!:B@*`#(`D3H@*U
M^I6*M8"5^LH0]3#2H@*UBI7ZRA#YI?S)_]`:H@*]'1.5@+6'G1T3RA#SI86D
MAH6#A(1,A!>B@*`%(`D3I?S%B=`J&*7Z98.JI?MEA,6(T!SDA]`8&*6#986%
M@Z6$98:%A*("M8J5A\H0^3`/HH>@`R`,$Z6%I(:%@X2$I8G%@M`S&*6'98.J
MI8AEA,6!T"7D@-`AH@*U@)7ZRA#YI81(I8-(HH"@!2`)$QAH98.%@VAEA(6$
MH@*UAY7ZRA#YHH"@!2`,$QBM(!-EA8T@$ZTA$V6&C2$3D`/N(A,88*``)`)P
M)"#/_[`?F00+R*:0A@+`\+`%R0W0YXBI`)D$"QB8:06-`PL88*E`A0+``-#J
M.&"@`+D$"_`&(-+_R-#UJ0U,TO^%_H3_H@*U!I7ZRA#YH@R@!"`)$Z4/H`!,
M#Q.B`!BM_PAI!<T#"Y`!Z,T#")`"Z.B*\`/)`F"L_PBY!`O9!`C0!\D`\`/(
MT/%@H@*I_Y4)M0.5!LH0]:4(R?_P'*D`H`@@*Q@@1ABP$*("M0:5";T`")4&
MRA#T,-Y@K0,+H``@%1.0`6"B`K4&G0`+RA#XJ0"@"X7^A/^M`PN@`"`2$Z4+
MR?_P%J("M?JT"94)E/K*$/6B":`#(`P3&&"B`K7ZE0/*$/D88*("J?^5`\H0
M^ZD`A0*B`2#&_[`5(.47L!`@=!@@I!BP"*DN(-+_3`098*("M0.5!LH0^:("
M(,G_I0C)__`7J0"@"R`K&"`9&*("O0`+E0;*$/A,*!D@S/]@H@*U`Y7ZJ?^5
M`\H0]:7\R?_P':(&H`,@"1.B`Z`#(`P3H@*U^I4#M0:5^LH0]3#=8*("M0.5
M^LH0^:7\R?_0`6"B#*`$(`D3I0^@`"`8$Z("M0R5^LH0^4R#&<D!D`+I`8W_
M"*E3(-+_(`,3J5(@TO\@\!BI5B#2_R!)&:E7(-+_(!H9J48@TO\@>AFI6"#2
,_R`&$ZD-(-+_J0!@
`
end

============================================================================

Next Issue: (Hopefully!!! :-] )

The 1351 Mouse Demystified

  An indepth look at how the 1351 mouse operates and how to access it within 
your ML programs, in addition to a BASIC driver for the 80 column screen.

ML Tutor - Part 3

  In this edition we take a look at reading and writing commands to the disk
drive, including reading the disk directory.  This article will also parallel
the discussion of the C=128 and C=64 KERNAL jump tables of available routines.

KERNAL 64/128 

  The C=128 and C=64 jump table that points to many valuable system routines is
listed and discussed with example applications and how to use them.

Bursting your 128

  This article will examine the routines and mysteries about how to use Burst
commands on the 1571 and 1581, including the Fastload utility and the block
Read and Write calls.
============================================================================
END of C= Hacking Issue 2.
============================================================================
magazines/chacking2.txt · Last modified: 2015-04-17 04:34 by 127.0.0.1