A tiny Z80 based computer

 

A tiny Z80 based computer

 

A rather small Z80 based computer with the following features:

  • Z80 single board computer
  • IDE controller
  • FAT file system (currently read only) in Z80 assembler


In September 2011 I somehow got the feeling that I just HAD to build a small Z80 based computer again. My last homebrew Z80 was built when I was still in school (more than 20 years ago) and somehow I felt a bit nostalgic and missed the (truly) good old times when computers were small, rather simple, easy to understand and program. Times when writing software meant writing assembler code and not installing several GB of a development package which creates files no smaller than several MB. So I searched for useable parts in my hardware repository and found everything necessary to build a small (tiny, in fact) Z80 based computer.

Since I love wire wrapping, I decided to build the computer on a single Euro card using wire wrap sockets where ever possible (if you happen to have wire wrap sockets or wire etc. and would like to give it to a good home I would love to take care of it - I am running low on wire wrap parts and it is next to impossible to find new sockets etc.). Designing and building the card took two evenings of about 5 hours each, and debugging took another, third evening (there were only four wiring errors but I made a software error in configuring the UART which was a bit hard to find).

The left row of the board shown above contains the TTL clock oscillators for the Z80 CPU (4 MHz) and the 16C550 UART (1.8432 MHz), a MAX232 for the serial line, a 74LS14 for the (simple) reset circuit and the reset push button. The next row contains the Z80 CPU, the 16C550 UART, a 62256 32kB RAM and a 27(C)512 32kB EPROM in a ZIF socket. The remainder of the board contains a simple voltage regulator with a 7805, several bus drivers (74LS245) and some address select logic.

The following pictures give an impression of the process of building this simple computer:

The schematic of the computer is quite simple and straight forward. Since I wanted to build a really simple computer, there is no provision for DMA transfers and the interrupt logic has not been tested by now.

Please note that there is a tiny error in the schematic drawing: The TTL oscillator driving the UART is a 1.8432 MHz type and not an 8 MHz oscillator as noted in the drawing! (Sorry - I forgot to correct this.)

Here is the parts list for the computer (excluding sockets - I recommend to use precision sockets):

  • TTL clock oscillator 4 MHz (for Z80 CPU - depending on your model, higher clock rates are possible)
  • TTL clock oscillator 1.8432 MHz (for UART)
  • MAX232 level converter for the serial line
  • 74LS14 (for reset-circuitry)
  • Push button for reset
  • Z80 CPU (at least "A" variant which is capable of running at 4 MHz)
  • 16C550 UART
  • 62256 static RAM, 32 kB
  • 27(C)256 EPROM (you might want to use a ZIF socket for the EPROM to facilitate insertion and removal of EPROMs during software development)
  • 74LS08 (quad AND gate)
  • 74LS139 (double 1-out-4 selector)
  • 74LS32 two times (quad OR gate)
  • 74LS245 four times (buffers for the bus - these are optional and only necessary if you plan to use external devices on the bus)
  • 74LS07 (LED driver)
  • 100 nF capacitors (16 times - one for each IC)
  • 1N4148 diode (reset circuit)
  • 10 uF capacitor (reset circuit)
  • 10 Ohm resistor (reset circuit)
  • 10 kOhm resistor (reset circuit)
  • 1 uF capacitor (5 times - needed by the MAX232)
  • 4.7 kOhm resistors (four times - needed to pull up some control lines - alternatively you can use a resistor array as I did)
  • 10 pin socket or something equivalent for the serial line
  • 100 uF capacitor (voltage regulator)
  • 100 nF capacitor (voltage regulator)
  • 7805 linear voltage regulator, 5 V
  • 3mm LEDs, 1 green, 2 yellow, 2 red)
  • 220 Ohm resistor, 5 times (LEDs)
  • Connector for power supply
  • Experimental Euro-card (160 mm times 100 mm)
  • VG connector, 64 or 96 pins

As stylish as a paper tape reader/puncher or at least a cassette drive would be, I decided to implement a simple IDE interface to use cheap and ruggedized Compact Flash cards for mass storage. After reading quite a bit about the IDE interface and having a look at other people's interfaces, I decided not to reinvent the wheel and implement the controlled developed by Phil from Retroleum since his design is clean and simple and features a really nice flipflop logic to generate the various control signals instead of RC delay circuits etc. as found in other designs.

The picture on the right shows the schematic of the IDE controller. Please note that there is an error that I forgot to correct on the schematic: The two eight bit latches on the lower right are of type 74LS374 and NOT (!) 74LS574 as written. (74LS574 would work, too, but have a completely different pinout!)

The circuit is described in detail on Phil's page at Retroleum. My implementation only differs with respect to an additional address decoder build around a 74LS85 4 bit comparator and a four place DIP switch. Currently my IDE controller is located at address $10 in the IO address space of the Z80 processor.

The picture on the left shows the completed IDE controller (which drove me nuts during debugging since I managed to handle the two select signals for the IDE devices incorrectly... *sigh*).

Since the tiny Z80 computer consists of two boards by now, an enclosure was needed. I decided to use a 10 inch standard enclosure (had I known before how much fiddling would be required to build the enclosure out of the many parts delivered by my supplier, I might have settled for something else :-) ). The picture on the right shows the first picture of the CPU card in its new housing. The iPad is used as terminal (a very useful feature :-) ).

The picture on the left shows both cards, the CPU board on the far left and the IDE controller right next to it, in operation. By the way - at first I thought about building a simple power supply with a linear regulator - then I wondered if I should use an old PC power supply and then I realized that new switching power supplies, capable of delivering 5V with 8A, cost only about 18 EUR... That is the reason for the small and modern power supply on the right hand side of the enclosure.

While the processor card worked quite right out of the box, the IDE controller was bit of a challenge since there were two independent bugs buried in its logic. The first problem was that the 74LS32 quad OR gate chip was defective (this is all the more puzzling since it was a new device). But more difficult to find was a mistake I did concerning the two select lines of the IDE device. I "thought" that I had understood the IDE logic and wired one of the /CS-lines to ground as I would have done with any TTL chip requiring two chip select lines. I did not realize that the two /CS lines act in fact as yet another address line and allow access to the upper registers of the IDE device. Fixing this bug was extremely simple, but finding it was hard since I was so sure that grounding the second /CS line was the right thing to do. :-)

In order to make any use at all of such a small homebrew computer one needs a small operating system, a so called "monitor" in fact. Writing the monitor took another couple of evenings up to now. Currently it occupies $166C bytes of memory (5740 bytes), so the 32 kB EPROM is not really well utilized at the moment. :-) The monitor is quite simple: All commands are collected into functional groups like CONTROL, DISK etc. The first letter typed at the command prompt selects the desired group while the second letter selects the command to be executed. The FAT file system implementation currently only supports reading files and displaying directory listings but this simplifies development a lot since one can now cross assemble/compile on another system and transfer the binaries by means of a CompactFlash card. :-)

Currently the monitor supports the following commands:

  • Control group:
    • Cold start: Restart the computer, clearing memory.
    • Info: Print monitor version.
    • Start: Start a machine program.
    • Warm start: Restart the computer without clearing memory.
  • Disk group:
    • Info: Print disk information.
    • Mount: Mount the disk.
    • Transfer: Initiate a transfer from or to disk:
      • Read: Read sectors from disk into memory.
      • Write: Write sectors from memory to disk.
    • Unmount: Unmount the disk.
  • File group:
    • Cat: Print a file's contents to STDOUT.
    • Directory: Print the disk directory.
    • Load: Load a file's contents into memory.
  • Help: Print help.
  • Memory group:
    • Dump: Dump a memory region - the user is asked to enter a start and end address.
    • Examine: Examine individual memory locations. After entering an address, the user can press the space bar to read the next memory cell or press any other key which will terminate the command.
    • Fill: Fill a memory area with a constant value.
    • Intel hex load: Load the contents of an Intel Hex file into memory. This is really useful for testing newly developed software.
    • Load: Load memory locations manually - after entering a start address, bytes in hexadecimal notation are read and stored in successive memory cells until a non-hexadecimal character will be entered.
    • Register dump: Dump the current contents of both register banks.

Working with this simple monitor looks like this:

Simple Z80-monitor - V 0.8 (B. Ulmann, Sep. 2011 - Jan. 2012)
Cold start, clearing memory.

Z> DISK/MOUNT

        FATNAME:        MSDOS5.0
        CLUSIZ: 40
        RESSEC: 0008
        FATSEC: 00F0
        ROOTLEN:        0200
        PSIZ:           003C0030
        PSTART: 00001F80
        FAT1START:      00001F88
        ROOTSTART:      00002168
        DATASTART:      00002188

Z> FILE/DIRECTORY
Directory contents:
-------------------------------------------
FILENAME.EXT  DIR?   SIZE (BYTES)  1ST SECT
-------------------------------------------
_~1     .TRA            00001000        000021C8
TRASHE~1.       DIR     00000000        00002188
SPOTLI~1.       DIR     00000000        00002208
FSEVEN~1.       DIR     00000000        00002408
TEST    .TXT            00000013        00002688
FAT_1   .ASM            0000552C        000026C8
MEMO    .TXT            00000098        00002748
TEST210 .TXT            00000210        00002788
TEST1F0 .TXT            000001F0        00002DC8
TEST200 .TXT            00000200        00002E08
GROSS   .TXT            0000CB5A        00003488

Z> FILE/CAT: FILENAME=TEST.TXT
123 ich bin so frei
Z> FILE/LOAD FILE: ADDR=C000 FILENAME=TEST.TXT

0013 bytes loaded.

Z> MEMORY/DUMP: START=C000 END=C01F
C000: 31 32 33 20 69 63 68 20 62 69 6E 20 73 6F 20 66   123 ich bin so f
C010: 72 65 69 00 00 00 00 00 00 00 00 00 00 00 00 00   rei.............

Z> 
       

The source of the monitor can be found here:

;******************************************************************************
;
;  Small monitor for the Z80 single board computer consisting of 32 kB ROM 
; ($0000 to $ffff), 32 kB RAM ($8000 to $ffff) and a 16c550 UART.
;
; B. Ulmann, 28-SEP-2011, 29-SEP-2011, 01-OCT-2011, 02-OCT-2011, 30-OCT-2011,
;            01-NOV-2011, 02-NOV-2011, 03-NOV-2011, 06/07/08-JAN-2012
; I. Kloeckl, 06/07/08-JAN-2012 (FAT implementation for reading files)
; B. Ulmann, 14-JAN-2011, 
;
; Version 0.8
;
;******************************************************************************
;
; TODO: 
;       Read and print IDE error status codes in case of error!
;
; Known issues:
;       Memory Dump has a problem when the end address is >= FF00
;
;******************************************************************************
;
;  RST $00 will enter the monitor (do not care about the return address pushed
; onto the stack - the stack pointer will be reinitialized during cold as well
; as during warm starts.
;
;  Monitor routines will generally called by placing the necessary parameters
; into some processor registers and then issuing RST $08. More about this later.
;
;  Memory layout is as follows:
;
;  +-------+
;  ! $FFFF !    General purpose 512 byte buffer
;  !  ---  !
;  ! $FE00 !
;  +-------+
;  ! $DFFF !    FAT control block
;  !  ---  !
;  ! $FDDC !
;  +-------+
;  ! $FDDB !    File control block
;  !  ---  !
;  ! $FBBE !
;  +-------+
;  ! $FBBD !    81 byte string buffer
;  !  ---  !
;  ! $FB6D !
;  +-------+
;  ! $FB6C !    12 byte string buffer
;  !  ---  !
;  ! $FB61 !
;  +-------+
;  ! $FB60 !    Buffers for various routines
;  !  ---  !
;  ! $FB4D !
;  +-------+
;  ! $FB4C !    Cold/warm start control (1 byte)
;  +-------+
;  ! $FBBD !    Stack
;  !  ...  !
;  ! $8000 !    Begin of RAM
;  +-------+
;  ! $7FFF !    ROM area
;  !  ---  !    RST $08 calls a system routine
;  ! $0000 !    RST $00 restarts the monitor
;  +-------+
;
;
monitor_start   equ     $0000           ; $0000 -> ROM, $8000 -> Test image
;
                org     monitor_start
;
rom_start       equ     $0
rom_end         equ     $7fff
ram_start       equ     $8000
ram_end         equ     $ffff
buffer          equ     ram_end - $1ff  ; 512 byte IDE general purpose buffer
;
; Define the FAT control block memory addresses:
;
datastart       equ     buffer - 4      ; Data area start vector
rootstart       equ     datastart - 4   ; Root directory start vector
fat1start       equ     rootstart - 4   ; Start vector to first FAT
psiz            equ     fat1start - 4   ; Size of partition (in sectors)
pstart          equ     psiz - 4        ; First sector of partition
rootlen         equ     pstart - 2      ; Maximum number of entries in directory
fatsec          equ     rootlen - 2     ; FAT size in sectors
ressec          equ     fatsec - 2      ; Number of reserved sectors
clusiz          equ     ressec - 1      ; Size of a cluster (in sectors)
fatname         equ     clusiz - 9      ; Name of the FAT (null terminated)
fatcb           equ     fatname         ; Start of the FATCB
;
; Define a file control block (FCB) memory addresses and displacements:
;
file_buffer     equ     fatcb - $200            ; 512 byte sector buffer
cluster_sector  equ     file_buffer - 1         ; Current sector in cluster
current_sector  equ     cluster_sector - 4      ; Current sector address
current_cluster equ     current_sector - 2      ; Current cluster number
file_pointer    equ     current_cluster - 4     ; Pointer for file position
file_type       equ     file_pointer - 1        ; 0 -> not found, else OK
first_cluster   equ     file_type - 2           ; First cluster of file
file_size       equ     first_cluster - 4       ; Size of file
file_name       equ     file_size - 12          ; Canonical name of file
fcb             equ     file_name               ; Start of the FCB
;
fcb_filename            equ     0
fcb_file_size           equ     $c
fcb_first_cluster       equ     $10
fcb_file_type           equ     $12
fcb_file_pointer        equ     $13
fcb_current_cluster     equ     $17
fcb_current_sector      equ     $19
fcb_cluster_sector      equ     $1d
fcb_file_buffer         equ     $1e
;
; We also need some general purpose string buffers:
;
string_81_bfr   equ     fcb - 81
string_12_bfr   equ     string_81_bfr - 12
;
;  A number of routines need a bit of scratch RAM, too. Since these are 
; sometimes interdependent, each routine gets its own memory cells (only
; possible since the routines are not recursive).
;
load_file_scrat equ     string_12_bfr - 2       ; Two bytes for load_file
str2filename_de equ     load_file_scrat - 2     ; Two bytes for str2filename
fopen_eob       equ     str2filename_de - 2     ; Eight bytes for fopen
fopen_rsc       equ     fopen_eob - 4
fopen_scr       equ     fopen_rsc - 2
dirlist_scratch equ     fopen_scr - 2           ; Eight bytes for fopen
dirlist_eob     equ     dirlist_scratch - 2
dirlist_rootsec equ     dirlist_eob - 4
;
start_type      equ     dirlist_rootsec  - $1   ; Distinguish cold/warm start
;
uart_base       equ     $0
ide_base        equ     $10
;
uart_register_0 equ     uart_base + 0
uart_register_1 equ     uart_base + 1
uart_register_2 equ     uart_base + 2
uart_register_3 equ     uart_base + 3
uart_register_4 equ     uart_base + 4
uart_register_5 equ     uart_base + 5
uart_register_6 equ     uart_base + 6
uart_register_7 equ     uart_base + 7
;
eos             equ     $00             ; End of string
cr              equ     $0d             ; Carriage return
lf              equ     $0a             ; Line feed
space           equ     $20             ; Space
tab             equ     $09             ; Tabulator
;
; Main entry point (RST 00H):
;
rst_00          di                      ; Disable interrupts 
                jr      initialize      ; Jump over the RST-area
;
;  RST-area - here is the main entry point into the monitor. The calling 
; standard looks like this:
;
; 1) Set register IX to the number of the system routine to be called.
; 2) Set the remaining registers according to the routine's documentation.
; 3) Execute RST $08 to actually call the system routine.
; 4) Evaluate the values returned in the registers as described by the 
;    Routine's documentation. 
;
;  (Currently there are no plans to use more RST entry points, so this routine
; just runs as long as necessary in memory. If more RSTs will be used, this
; routine should to be moved to the end of the used ROM area with only a 
; simple jump at the RST $08-location.)
;
;  This technique of calling system routines can be used as the following
; example program that just echos characters read from the serial line
; demonstrates:
;
;         org     $8000           ; Start in lower RAM
; loop    ld      ix, 5           ; Prepare call to getc
;         rst     08              ; Execute getc
;         cp      3               ; CTRL-C pressed?
;         jr      z, exit         ; Yes - exit
;         ld      ix, 6           ; Prepare call to putc
;         rst     08              ; Execute putx
;         jr      loop            ; Process next character
; exit    ld      ix, 4           ; Exit - print a CR/LF pair
;         rst     08              ; Call CRLF
;         ld      hl, msg         ; Pointer to exit message
;         ld      ix, 7           ; Prepare calling puts
;         rst     08              ; Call puts
;         rst     00              ; Restart monitor (warm start)
; msg     defb    "That's all folks.", $d, $a, 0
;
;  Currently the following functions are available (a more detailed description
; can be found in the dispatch table itself):
;
;       0:      cold_start
;       1:      is_hex
;       2:      is_print
;       3:      to_upper
;       4:      crlf
;       5:      getc
;       6:      putc
;       7:      puts
;       8:      strcmp
;       9:      gets
;       A:      fgetc
;       B:      dump_fcb
;       C:      fopen
;       D:      dirlist
;       E:      fatmount
;       F:      fatunmount
;
;                org     monitor_start + $08
                nop                     ; Beware: zasm is buggy concerning
                nop                     ; the org pseudo-statement. Therefore
                nop                     ; The displacement to the RST $08
                nop                     ; entry point is generated by this
                nop                     ; NOP-sequence.
rst_08          push    bc              ; Save bc and hl
                push    hl
                push    ix              ; Copy the contents of ix
                pop     hl              ; into hl
                add     hl, hl          ; Double to get displacement in table
                ld      bc, dispatch_table
                add     hl, bc          ; Calculate displacement in table
                ld      bc, (hl)        ; Load bc with the destination address
                push    bc
                pop     ix              ; Load ix with the destination address
                pop     hl              ; Restore hl
                pop     bc              ; and bc
                jp      (ix)            ; Jump to the destination routine
dispatch_table  defw    cold_start      ; $00 = clear etc.
                ; Parameters:    N/A
                ; Action:        Performs a cold start (memory is cleared!)
                ; Return values: N/A
                ;
                defw    is_hex
                ; Parameters:    A contains a character code
                ; Action:        Tests ('0' <= A <= '9) || ('A' <= A <= 'F')
                ; Return values: Carry bit is set if A contains a hex char.
                ;
                defw    is_print
                ; Parameters:    A contains a charater code
                ; Action:        Tests if the character is printable
                ; Return values: Carry bit is set if A contains a valid char.
                ;
                defw    to_upper
                ; Parameters:    A contains a character code
                ; Action:        Converts an ASCII character into upper case
                ; Return values: Converted character code in A
                ;
                defw    crlf
                ; Parameters:    N/A
                ; Action:        Sends a CR/LF to the serial line
                ; Return values: N/A
                ;
                defw    getc
                ; Parameters:    A contains a character code
                ; Action:        Reads a character code from the serial line
                ; Return values: N/A
                ;
                defw    putc
                ; Parameters:    A contains a character code
                ; Action:        Sends the character code to the serial line
                ; Return values: N/A
                ;
                defw    puts
                ; Parameters:    HL contains the address of a 0-terminated 
                ;                string
                ; Action:        Send the string to the serial line (excluding
                ;                the termination byte, of course)
                ; Return values: N/A
                ;
                defw    strcmp
                ; Parameters:    HL and DE contain the addresses of two strings
                ; Action:        Compare both strings.
                ; Return values: A contains return value, <0 / 0 / >0
                ;
                defw    gets
                ; Parameters:    HL contains a buffer address, B contains the
                ;                buffer length (including the terminating
                ;                null byte!)
                ; Action:        Reads a string from STDIN. Terminates when
                ;                either the buffer is full or the string is
                ;                terminated by CR/LF.
                ; Return values: N/A
                ;
                defw    fgetc
                ; Parameters:    IY (pointer to a valid FCB)
                ; Action:        Reads a character from a FAT file
                ; Return values: Character in A, if EOF has been encountered,
                ;                the carry flag will be set
                ;
                defw    dump_fcb
                ; Parameters:    IY (pointer to a valid FCB)
                ; Action:        Prints the contents of the FCB in human
                ;                readable format to STDOUT
                ; Return values: N/A
                ;
                defw    fopen
                ; Parameters:    HL (points to a buffer containing the file
                ;                file name), IY (points to an empty FCB)
                ; Action:        Opens a file for reading
                ; Return values: N/A (All information is contained in the FCB)
                ;
                defw    dirlist
                ; Parameters:    N/A (relies on a valid FAT control block)
                ; Action:        Writes a directory listing to STDOUT
                ; Return values: N/A
                ;
                defw    fatmount
                ; Parameters:    N/A (needs the global FAT control block)
                ; Action:        Mounts a disk (populates the FAT CB)
                ; Return values: N/A
                ;
                defw    fatunmount
                ; Parameters:    N/A (needs the global FAT control block)
                ; Action:        Invalidates the global FAT control block
                ; Return values; N/A
;
;  The stackpointer will be predecremented by a push instruction. Since we need
; a 512 byte buffer for data transfers to and from the IDE disk, the stack 
; pointer is initialized to start at the beginning of this buffer space.
;
initialize      ld      sp, start_type - $1
;
; Initialize UART to 9600,8N1:
;
                ld      a, $80
                out     (uart_register_3), a
                ld      a, $c           ; 1843200 / (16 * 9600)
                out     (uart_register_0), a
                xor     a
                out     (uart_register_1), a
                ld      a, $3           ; 8N1
                out     (uart_register_3), a
;
; Print welcome message:
;
                ld      hl, hello_msg
                call    puts
;
;  If this is a cold start (the location start_type does not contain $aa)
; all available RAM will be reset to $00 and a message will be printed.
;
                ld      a, (start_type)
                cp      $aa             ; Warm start?
                jr      z, main_loop    ; Yes - enter command loop
                ld      hl, cold_start_msg
                call    puts            ; Print cold start message
                ld      hl, ram_start   ; Start of block to be filled with $00
                ld      de, hl          ; End address of block
                inc     de              ; plus 1 (for ldir)
                ld      bc, ram_end - ram_start
                ld      (hl), $00       ; Load first memory location
                ldir                    ; And copy this value down
                ld      hl, start_type
                ld      (hl), $aa       ; Cold start done, remember this
;
; Read characters from the serial line and send them just back:
;
main_loop       ld      hl, monitor_prompt
                call    puts
; The monitor is rather simple: All commands are just one or two letters. 
; The first character selects a command group, the second the desired command
; out of that group. When a command is recognized, it will be spelled out 
; automatically and the user will be prompted for arguments if applicable. 
                call    monitor_key     ; Read a key
; Which group did we get?
                cp      'C'             ; Control group?
                jr      nz, disk_group  ; No - test next group
                ld      hl, cg_msg      ; Print group prompt
                call    puts
                call    monitor_key     ; Get command key
                cp      'C'             ; Cold start?
                jp      z, cold_start
                cp      'W'             ; Warm start?
                jp      z, warm_start
                cp      'S'             ; Start?
                jp      z, start
                cp      'I'             ; Info?
                call    z, info
                jr      z, main_loop
                jp      cmd_error       ; Unknown control-group-command
disk_group      cp      'D'             ; Disk group?
                jr      nz, file_group  ; No - file group?
                ld      hl, dg_msg      ; Print group prompt
                call    puts
                call    monitor_key     ; Get command
                cp      'I'             ; Info?
                call    z, disk_info
                jr      z, main_loop
                cp      'M'             ; Mount?
                call    z, mount
                jr      z, main_loop
                cp      'T'             ; Read from disk?
                call    z, disk_transfer
                jr      z, main_loop
                cp      'U'             ; Unmount?
                call    z, unmount
                jr      z, main_loop
                jr      cmd_error       ; Unknown disk-group-command
file_group      cp      'F'             ; File group?
                jr      nz, help_group  ; No - help group?
                ld      hl, fg_msg      ; Print group prompt
                call    puts
                call    monitor_key     ; Get command
                cp      'C'             ; Cat?
                call    z, cat_file
                jr      z, main_loop
                cp      'D'             ; Directory?
                call    z, directory
                jr      z, main_loop
                cp      'L'             ; Load?
                call    z, load_file
                jr      z, main_loop
                jr      cmd_error       ; Unknown file-group-command
help_group      cp      'H'             ; Help? (No further level expected.)
                call    z, help         ; Yes :-)
                jp      z, main_loop
memory_group    cp      'M'             ; Memory group?
                jp      nz, group_error ; No - print an error message
                ld      hl, mg_msg      ; Print group prompt
                call    puts
                call    monitor_key     ; Get command key
                cp      'D'             ; Dump?
                call    z, dump
                jp      z, main_loop
                cp      'E'             ; Examine?
                call    z, examine
                jp      z, main_loop
                cp      'F'             ; Fill?
                call    z, fill
                jp      z, main_loop
                cp      'I'             ; INTEL-Hex load?
                call    z, ih_load
                jp      z, main_loop
                cp      'L'             ; Load?
                call    z, load
                jp      z, main_loop
                cp      'M'             ; Move?
                call    z, move
                jp      z, main_loop
                cp      'R'             ; Register dump?
                call    z, rdump
                jp      z, main_loop
                jr      cmd_error       ; Unknown memory-group-command
group_error     ld      hl, group_err_msg
                jr      print_error
cmd_error       ld      hl, command_err_msg
print_error     call    putc            ; Echo the illegal character
                call    puts            ; and print the error message
                jp      main_loop
;
; Some constants for the monitor:
;
hello_msg       defb    cr, lf, cr, lf, "Simple Z80-monitor - V 0.8 "
                defb    "(B. Ulmann, Sep. 2011 - Jan. 2012)", cr, lf, eos
monitor_prompt  defb    cr, lf, "Z> ", eos
cg_msg          defb    "CONTROL/", eos
dg_msg          defb    "DISK/", eos
fg_msg          defb    "FILE/", eos
mg_msg          defb    "MEMORY/", eos
command_err_msg defb    ": Syntax error - command not found!", cr, lf, eos
group_err_msg   defb    ": Syntax error - group not found!", cr, lf, eos
cold_start_msg  defb    "Cold start, clearing memory.", cr, lf, eos
;
; Read a key for command group and command:
;
monitor_key     call    getc
                cp      lf              ; Ignore LF
                jr      z, monitor_key  ; Just get the next character
                call    to_upper
                cp      cr              ; A CR will return to the prompt
                ret     nz              ; No - just return
                inc     sp              ; Correct SP to and avoid ret!
                jp      main_loop
;
;******************************************************************************
;***
;*** The following routines are used in the interactive part of the monitor
;***
;******************************************************************************
;
; Print a file's contents to STDOUT:
;
cat_file        push    bc
                push    de
                push    hl
                push    iy
                ld      hl, cat_file_prompt
                call    puts
                ld      hl, string_81_bfr
                ld      b, 81
                call    gets            ; Read the filename into buffer
                ld      iy, fcb         ; Prepare fopen (only one FCB currently)
                ld      de, string_12_bfr
                call    fopen
cat_file_loop   call    fgetc           ; Get a single character
                jr      c, cat_file_exit
                call    putc            ; Print character if not EOF
                jr      cat_file_loop   ; Next character
cat_file_exit   pop     iy
                pop     hl
                pop     de
                pop     bc
                ret
cat_file_prompt defb    "CAT: FILENAME=", eos
;
;  directory - a simple wrapper for dirlist (necessary for printing the command
; name)
;
directory       push    hl
                ld      hl, directory_msg
                call    puts
                call    dirlist
                pop     hl
                ret
directory_msg   defb    "DIRECTORY", cr, lf, eos
;
; Get and print disk info:
;
disk_info       push    af
                push    hl
                ld      hl, disk_info_msg
                call    puts
                call    ide_get_id      ; Read the disk info into the IDE buffer
                ld      hl, buffer + $13
                ld      (hl), tab
                call    puts            ; Print vendor information
                call    crlf
                ld      hl, buffer + $2d
                ld      (hl), tab
                call    puts
                call    crlf
                pop     hl
                pop     af
                ret
disk_info_msg   defb    "INFO:", cr, lf, eos
;
; Read data from disk to memory
;
disk_transfer   push    af
                push    bc
                push    de
                push    hl
                push    ix
                ld      hl, disk_trx_msg_0
                call    puts            ; Print Read/Write prompt
disk_trx_rwlp   call    getc
                call    to_upper
                cp      'R'             ; Read?
                jr      nz, disk_trx_nr ; No
                ld      ix, ide_rs      ; Yes, we will call ide_rs later
                ld      hl, disk_trx_msg_1r
                jr      disk_trx_main   ; Prompt the user for parameters
disk_trx_nr     cp      'W'             ; Write?
                jr      nz, disk_trx_rwlp
                ld      ix, ide_ws      ; Yes, we will call ide_ws later
                ld      hl, disk_trx_msg_1w
disk_trx_main   call    puts            ; Print start address prompt
                call    get_word        ; Get memory start address
                push    hl
                ld      hl, disk_trx_msg_2
                call    puts            ; Prompt for number of blocks
                call    get_byte        ; There are only 128 block of memory!
                cp      0               ; Did the user ask for 00 blocks?
                jr      nz, disk_trx_1  ; No, continue prompting
                ld      hl, disk_trx_msg_4
                call    puts
                jr      disk_trx_exit
disk_trx_1      ld      hl, disk_trx_msg_3
                call    puts            ; Prompt for disk start sector
                call    get_word        ; This is a four byte address!
                ld      bc, hl
                call    get_word
                ld      de, hl
                pop     hl              ; Restore memory start address
                ; Register contents:
                ;       A:  Number of blocks
                ;       BC: LBA3/2
                ;       DE: LBA1/0
                ;       HL: Memory start address
disk_trx_loop   push    af              ; Save number of sectors
                call    disk_trampoline ; Read/write one sector (F is changed!)
                push    hl              ; Save memory address
                push    bc              ; Save LBA3/2
                ld      hl, de          ; Increment DE (LBA1/0)
                ld      bc, $0001       ; by one and
                add     hl, bc          ; generate a carry if necessary
                ld      de, hl          ; Save new LBA1/0
                pop     hl              ; Restore LBA3/2 into HL (!)
                jr      nc, disk_trx_skip
                add     hl, bc          ; Increment BC if there was a carry
disk_trx_skip   ld      bc, hl          ; Write new LBA3/2 into BC
                pop     hl              ; Restore memory address
                push    bc              ; Save LBA3/2
                ld      bc, $200        ; 512 byte per block
                add     hl, bc          ; Set pointer to next memory block
                pop     bc              ; Restore LBA3/2
                pop     af
                dec     a               ; One block already done
                jr      nz, disk_trx_loop
disk_trx_exit   pop     ix
                pop     hl
                pop     de
                pop     bc
                pop     af
                ret
disk_trampoline jp      (ix)
disk_trx_msg_0  defb    "TRANSFER/", eos
disk_trx_msg_1r defb    "READ: ", cr, lf, "    MEMORY START=", eos
disk_trx_msg_1w defb    "WRITE: ", cr, lf, "    MEMORY START=", eos
disk_trx_msg_2  defb    " NUMBER OF BLOCKS (512 BYTE)=", eos
disk_trx_msg_3  defb    " START SECTOR=", eos
disk_trx_msg_4  defb    " Nothing to do for zero blocks.", cr, lf, eos
;
; Dump a memory area
;
dump            push    af
                push    bc
                push    de
                push    hl
                ld      hl, dump_msg_1
                call    puts            ; Print prompt
                call    get_word        ; Read start address
                push    hl              ; Save start address
                ld      hl, dump_msg_2  ; Prompt for end address
                call    puts
                call    get_word        ; Get end address
                call    crlf
                inc     hl              ; Increment stop address for comparison
                ld      de, hl          ; DE now contains the stop address
                pop     hl              ; HL is the start address again
                ; This loop will dump 16 memory locations at once - even
                ; if this turns out to be more than requested.
dump_line       ld      b, $10          ; This loop will process 16 bytes
                push    hl              ; Save HL again
                call    print_word      ; Print address
                ld      hl, dump_msg_3  ; and a colon
                call    puts
                pop hl                  ; Restore address
                push    hl              ; We will need HL for the ASCII dump
dump_loop       ld      a, (hl)         ; Get the memory content
                call    print_byte      ; and print it
                ld      a, ' '          ; Print a space
                call    putc
                inc     hl              ; Increment address counter
                djnz    dump_loop       ; Continue with this line
                ; This loop will dump the very same 16 memory locations - but
                ; this time printable ASCII characters will be written.
                ld      b, $10          ; 16 characters at a time
                ld      a, ' '          ; We need some spaces
                call    putc            ; to print
                call    putc
                pop     hl              ; Restore the start address
dump_ascii_loop ld      a, (hl)         ; Get byte
                call    is_print        ; Is it printable?
                jr      c, dump_al_1    ; Yes
                ld      a, '.'          ; No - print a dot
dump_al_1       call    putc            ; Print the character
                inc     hl              ; Increment address to read from
                djnz    dump_ascii_loop
                ; Now we are finished with printing one line of dump output.
                call    crlf            ; CR/LF for next line on terminal
                push    hl              ; Save the current address for later
                and     a               ; Clear carry
                sbc     hl, de          ; Have we reached the last address?
                pop     hl              ; restore the address
                jr      c, dump_line    ; Dump next line of 16 bytes
                pop     hl
                pop     de
                pop     bc
                pop     af
                ret
dump_msg_1      defb    "DUMP: START=", eos
dump_msg_2      defb    " END=", eos
dump_msg_3      defb    ": ", eos
;
; Examine a memory location:
;
examine         push    af
                push    hl
                ld      hl, examine_msg_1
                call    puts
                call    get_word        ; Wait for a four-nibble address
                push    hl              ; Save address for later
                ld      hl, examine_msg_2
                call    puts
examine_loop    pop     hl              ; Restore address
                ld      a, (hl)         ; Get content of address
                inc     hl              ; Prepare for next examination
                push    hl              ; Save hl again for later use
                call    print_byte      ; Print the byte
                call    getc            ; Get a character
                cp      ' '             ; A blank?
                jr      nz, examine_exit; No - exit
                ld      a, ' '          ; Print a blank character
                call    putc
                jr      examine_loop
examine_exit    pop     hl              ; Get rid of save hl value
                call    crlf            ; Print CR/LF
                pop     hl
                pop     af
                ret
examine_msg_1   defb    "EXAMINE (type ' '/RET): ADDR=", eos
examine_msg_2   defb    " DATA=", eos
;
; Fill a block of memory with a single byte - the user is prompted for the
; start address, the length of the block and the fill value.
;
fill            push    af              ; We will need nearly all registers
                push    bc
                push    de
                push    hl
                ld      hl, fill_msg_1  ; Prompt for start address
                call    puts
                call    get_word        ; Get the start address
                push    hl              ; Store the start address
                and     a               ; Clear carry
                ld      bc, ram_start
                sbc     hl, bc          ; Is the address in the RAM area?
                jr      nc, fill_get_length
                ld      hl, fill_msg_4  ; No!
                call    puts            ; Print error message
                pop     hl              ; Clean up the stack
                jr      fill_exit       ; Leave routine
fill_get_length ld      hl, fill_msg_2  ; Prompt for length information
                call    puts
                call    get_word        ; Get the length of the block
                ; Now make sure that start + length is still in RAM:
                ld      bc, hl          ; BC contains the length
                pop     hl              ; HL now contains the start address
                push    hl              ; Save the start address again
                push    bc              ; Save the length
                add     hl, bc          ; Start + length
                and     a               ; Clear carry
                ld      bc, ram_start
                sbc     hl, bc          ; Compare with ram_start
                jr      nc, fill_get_value
                ld      hl, fill_msg_5  ; Print error message
                call    puts
                pop     bc              ; Clean up the stack
                pop     hl
                jr      fill_exit       ; Leave the routine
fill_get_value  ld      hl, fill_msg_3  ; Prompt for fill value
                call    puts
                call    get_byte        ; Get the fill value
                pop     bc              ; Get the length from the stack
                pop     hl              ; Get the start address again
                ld      de, hl          ; DE = HL + 1
                inc     de
                dec     bc
                ; HL = start address
                ; DE = destination address = HL + 1
                ;      Please note that this is necessary - LDIR does not
                ;      work with DE == HL. :-)
                ; A  = fill value
                ld      (hl), a         ; Store A into first memory location
                ldir                    ; Fill the memory
                call    crlf
fill_exit       pop     hl              ; Restore the register contents
                pop     de
                pop     bc
                pop     af
                ret
fill_msg_1      defb    "FILL: START=", eos
fill_msg_2      defb    " LENGTH=", eos
fill_msg_3      defb    " VALUE=", eos
fill_msg_4      defb    " Illegal address!", cr, lf, eos
fill_msg_5      defb    " Block exceeds RAM area!", cr, lf, eos
;
; Help
;
help            push    hl
                ld      hl, help_msg
                call    puts
                pop     hl
                ret
help_msg        defb    "HELP: Known command groups and commands:", cr, lf
                defb    "         C(ontrol group):", cr, lf
                defb    "             C(old start), I(nfo), S(tart), "
                defb    "W(arm start)", cr, lf
                defb    "         D(isk group):", cr, lf
                defb    "             I(nfo), M(ount), T(ransfer),"
                defb    "         U(nmount)", cr, lf
                defb    "                                  R(ead), W(rite)"
                defb    cr, lf
                defb    "         F(ile group):", cr, lf
                defb    "             C(at), D(irectory), L(oad)", cr, lf
                defb    "         H(elp)", cr, lf
                defb    "         M(emory group):", cr, lf
                defb    "             D(ump), E(xamine), F(ill), "
                defb    "I(ntel Hex Load), L(oad), R(egister dump)"
                defb    cr, lf, eos
;
; Load an INTEL-Hex file (a ROM image) into memory. This routine has been 
; more or less stolen from a boot program written by Andrew Lynch and adapted
; to this simple Z80 based machine.
;
; The INTEL-Hex format looks a bit awkward - a single line contains these 
; parts:
; ':', Record length (2 hex characters), load address field (4 hex characters),
; record type field (2 characters), data field (2 * n hex characters),
; checksum field. Valid record types are 0 (data) and 1 (end of file).
;
; Please note that this routine will not echo what it read from stdin but
; what it "understood". :-)
; 
ih_load         push    af
                push    de
                push    hl
                ld      hl, ih_load_msg_1
                call    puts
ih_load_loop    call    getc            ; Get a single character
                cp      cr              ; Don't care about CR
                jr      z, ih_load_loop
                cp      lf              ; ...or LF
                jr      z, ih_load_loop
                cp      space           ; ...or a space
                jr      z, ih_load_loop
                call    to_upper        ; Convert to upper case
                call    putc            ; Echo character
                cp      ':'             ; Is it a colon?                
                jr      nz, ih_load_error
                call    get_byte        ; Get record length into A
                ld      d, a            ; Length is now in D
                ld      e, $0           ; Clear checksum
                call    ih_load_chk     ; Compute checksum
                call    get_word        ; Get load address into HL
                ld      a, h            ; Update checksum by this address
                call    ih_load_chk
                ld      a, l
                call    ih_load_chk
                call    get_byte        ; Get the record type
                call    ih_load_chk     ; Update checksum
                cp      $1              ; Have we reached the EOF marker?
                jr      nz, ih_load_data; No - get some data
                call    get_byte        ; Yes - EOF, read checksum data
                call    ih_load_chk     ; Update our own checksum
                ld      a, e
                and     a               ; Is our checksum zero (as expected)?
                jr      z, ih_load_exit ; Yes - exit this routine
ih_load_chk_err ld      hl, ih_load_msg_3
                call    puts            ; No - print an error message
                jr      ih_load_exit    ; and exit
ih_load_data    ld      a, d            ; Record length is now in A
                and     a               ; Did we process all bytes?
                jr      z, ih_load_eol  ; Yes - process end of line
                call    get_byte        ; Read two hex digits into A
                call    ih_load_chk     ; Update checksum
                ld      (hl), a         ; Store byte into memory
                inc     hl              ; Increment pointer
                dec     d               ; Decrement remaining record length
                jr      ih_load_data    ; Get next byte
ih_load_eol     call    get_byte        ; Read the last byte in the line
                call    ih_load_chk     ; Update checksum
                ld      a, e
                and     a               ; Is the checksum zero (as expected)?
                jr      nz, ih_load_chk_err
                call    crlf
                jr      ih_load_loop    ; Yes - read next line
ih_load_error   ld      hl, ih_load_msg_2
                call    puts            ; Print error message
ih_load_exit    call    crlf
                pop     hl              ; Restore registers
                pop     de
                pop     af
                ret
;
ih_load_chk     ld      c, a            ; All in all compute E = E - A
                ld      a, e
                sub     c
                ld      e, a
                ld      a, c
                ret
ih_load_msg_1   defb    "INTEL HEX LOAD: ", eos
ih_load_msg_2   defb    " Syntax error!", eos
ih_load_msg_3   defb    " Checksum error!", eos
;
; Print version information etc.
;
info            push    hl
                ld      hl, info_msg
                call    puts
                ld      hl, hello_msg
                call    puts
                pop     hl
                ret
info_msg        defb    "INFO: ", eos
;
; Load data into memory. The user is prompted for a 16 bit start address. Then
; a sequence of bytes in hexadecimal notation may be entered until a character
; that is not 0-9 or a-f is encountered.
;
load            push    af
                push    bc
                push    de
                push    hl
                ld      hl, load_msg_1  ; Print command name
                call    puts
                call    get_word        ; Wait for the start address (2 bytes)
                push    hl              ; Remember address
                and     a               ; Clear carry
                ld      bc, ram_start   ; Check if the address is valid
                sbc     hl, bc          ; by subtracting the RAM start address
                pop     hl              ; Restore address
                ld      de, 0           ; Counter for bytes loaded
                jr      nc, load_loop   ; OK - start reading hex characters
                ld      hl, load_msg_3  ; Print error message
                call    puts
                jr      load_exit
                ; All in all we need two hex nibbles per byte. If two characters
                ; in a row are valid hexadecimal digits we will convert them 
                ; to a byte and store this in memory. If one character is 
                ; illegal, the load routine terminates and returns to the 
                ; monitor.
load_loop       ld      a, ' '
                call    putc            ; Write a space as byte delimiter
                call    getc            ; Read first character
                call    to_upper        ; Convert to upper case
                call    is_hex          ; Is it a hex digit?
                jr      nc, load_exit   ; No - exit the load routine
                call    nibble2val      ; Convert character to value
                call    print_nibble    ; Echo hex digit
                rlc     a
                rlc     a
                rlc     a
                rlc     a
                ld      b, a            ; Save the upper four bits for later
                call    getc            ; Read second character and proceed...
                call    to_upper        ; Convert to upper case
                call    is_hex
                jr      nc, load_exit
                call    nibble2val
                call    print_nibble
                or      b               ; Combine lower 4 bits with upper
                ld      (hl), a         ; Save value to memory
                inc     hl
                inc     de
                jr      load_loop       ; Get next byte (or at least try to)
load_exit       call    crlf            ; Finished...
                ld      hl, de          ; Print number of bytes loaded
                call    print_word
                ld      hl, load_msg_2
                call    puts
                pop     hl
                pop     de
                pop     bc
                pop     af
                ret
load_msg_1      defb    "LOAD (xx or else to end): ADDR=", eos
load_msg_2      defb    " bytes loaded.", cr, lf, eos
load_msg_3      defb    " Illegal address!", eos
;
; Load a file's contents into memory:
;
load_file       push    af
                push    bc
                push    de
                push    hl
                push    iy
                ld      hl, load_file_msg_1
                call    puts            ; Print first prompt (start address)
                call    get_word        ; Wait for the start address (2 bytes)
                ld      (load_file_scrat), hl
                and     a               ; Clear carry
                ld      bc, ram_start   ; Check if the address is valid
                sbc     hl, bc          ; by subtracting the RAM start address
                jr      nc, load_file_1
                ld      hl, load_file_msg_2
                call    puts
                jr      load_file_exit  ; Illegal address - exit routine
load_file_1     ld      hl, load_file_msg_4
                call    puts            ; Prompt for filename
                ld      hl, string_81_bfr
                ld      b, 81           ; Buffer length
                call    gets            ; Read file name into bfr
                ld      iy, fcb         ; Prepare open (only one FCB currently)
                ld      de, string_12_bfr
                call    fopen           ; Open the file (if possible)
                ld      hl, (load_file_scrat)
                ld      de, 0           ; Counter for bytes loaded
load_file_loop  call    fgetc           ; Get one byte from the file
                jr      c, load_file_exit
                ld      (hl), a         ; Store byte and
                inc     hl              ; increment pointer
                inc     de
                jr      load_file_loop  ; Process next byte
load_file_exit  call    crlf
                ld      hl, de          ; Print number of bytes loaded
                call    print_word
                ld      hl, load_file_msg_3
                call    puts
                pop     iy
                pop     hl
                pop     de
                pop     bc
                pop     af
                ret
load_file_msg_1 defb    "LOAD FILE: ADDR=", eos
load_file_msg_2 defb    " Illegal address!", eos
load_file_msg_3 defb    " bytes loaded.", cr, lf, eos
load_file_msg_4 defb    " FILENAME=", eos
;
; mount - a wrapper for fatmount (necessary for printing the command's name)
;
mount           push    hl
                ld      hl, mount_msg
                call    puts
                call    fatmount
                pop     hl
                ret
mount_msg       defb    "MOUNT", cr, lf, cr, lf, eos
;
; Move a memory block - the user is prompted for all necessary data:
;
move            push    af              ; We won't even destroy the flags!
                push    bc
                push    de
                push    hl
                ld      hl, move_msg_1
                call    puts
                call    get_word        ; Get address of block to be moved
                push    hl              ; Push this address
                ld      hl, move_msg_2
                call    puts
                call    get_word        ; Get destination start address
                ld      de, hl          ; LDIR requires this in DE
                ; Is the destination address in RAM area?
                and     a               ; Clear carry
                ld      bc, ram_start
                sbc     hl, bc          ; Is the destination in RAM?
                jr      nc, move_get_length
                ld      hl, move_msg_4  ; No - print error message
                call    puts
                pop     hl              ; Clean up stack
                jr      move_exit
move_get_length ld      hl, move_msg_3
                call    puts
                call    get_word        ; Get length of block
                ld      bc, hl          ; LDIR requires the length in BC
                pop     hl              ; Get address of block to be moved
                ; I was lazy - there is no test to make sure that the block
                ; to be moved will fit into the RAM area.
                ldir                    ; Move block
move_exit       call    crlf            ; Finished
                pop     hl              ; Restore registers
                pop     de
                pop     bc
                pop     af
                ret
move_msg_1      defb    "MOVE: FROM=", eos
move_msg_2      defb    " TO=", eos
move_msg_3      defb    " LENGTH=", eos
move_msg_4      defb    " Illegal destination address!", eos
;
; Dump the contents of both register banks:
;
rdump           push    af
                push    hl
                ld      hl, rdump_msg_1 ; Print first two lines
                call    puts
                pop     hl
                call    rdump_one_set
                exx
                ex      af, af'
                push    hl
                ld      hl, rdump_msg_2
                call    puts
                pop     hl
                call    rdump_one_set
                ex      af, af'
                exx
                push    hl
                ld      hl, rdump_msg_3
                call    puts
                push    ix
                pop     hl
                call    print_word
                ld      hl, rdump_msg_4
                call    puts
                push    iy
                pop     hl
                call    print_word
                ld      hl, rdump_msg_5
                call    puts
                ld      hl, 0
                add     hl, sp
                call    print_word
                call    crlf
                pop     hl
                pop     af
                ret
rdump_msg_1     defb    "REGISTER DUMP", cr, lf, cr, lf, tab, "1st:", eos
rdump_msg_2     defb    tab, "2nd:", eos
rdump_msg_3     defb    tab, "PTR: IX=", eos
rdump_msg_4     defb    " IY=", eos
rdump_msg_5     defb    " SP=", eos
;
rdump_one_set   push    hl              ; Print one register set
                ld      hl, rdump_os_msg_1
                call    puts
                push    af              ; Move AF into HL
                pop     hl
                call    print_word      ; Print contents of AF
                ld      hl, rdump_os_msg_2
                call    puts
                ld      hl, bc
                call    print_word      ; Print contents of BC
                ld      hl, rdump_os_msg_3
                call    puts
                ld      hl, de
                call    print_word      ; Print contents of DE
                ld      hl, rdump_os_msg_4
                call    puts
                pop     hl              ; Restore original HL
                call    print_word      ; Print contents of HL
                call    crlf
                ret
rdump_os_msg_1  defb    " AF=", eos
rdump_os_msg_2  defb    " BC=", eos
rdump_os_msg_3  defb    " DE=", eos
rdump_os_msg_4  defb    " HL=", eos
;
; Start a program - this will prompt for a four digital hexadecimal start
; address. A program should end with "jp $0" to enter the monitor again.
;
start           ld      hl, start_msg
                call    puts
                call    get_word        ; Wait for a four-nibble address
                call    crlf
                jp      (hl)            ; Start program (and hope for the best)
start_msg       defb    "START: ADDR=", eos
;
;  unmount - simple wrapper for fatunmount (necessary for printing the command
; name)
;
unmount         push    hl
                ld      hl, unmount_msg
                call    puts
                call    fatunmount
                pop     hl
                ret
unmount_msg     defb    "UNMOUNT", cr, lf, eos
;
;******************************************************************************
;***
;*** String routines
;***
;******************************************************************************
;
; is_hex checks a character stored in A for being a valid hexadecimal digit.
; A valid hexadecimal digit is denoted by a set C flag.
;
is_hex          cp      'F' + 1         ; Greater than 'F'?
                ret     nc              ; Yes
                cp      '0'             ; Less than '0'?
                jr      nc, is_hex_1    ; No, continue
                ccf                     ; Complement carry (i.e. clear it)
                ret
is_hex_1        cp      '9' + 1         ; Less or equal '9*?
                ret     c               ; Yes
                cp      'A'             ; Less than 'A'?
                jr      nc, is_hex_2    ; No, continue
                ccf                     ; Yes - clear carry and return
                ret
is_hex_2        scf                     ; Set carry
                ret
;
; is_print checks if a character is a printable ASCII character. A valid
; character is denoted by a set C flag.
;
is_print        cp      space
                jr      nc, is_print_1
                ccf
                ret
is_print_1      cp      $7f
                ret
;
; nibble2val expects a hexadecimal digit (upper case!) in A and returns the
; corresponding value in A.
;
nibble2val      cp      '9' + 1         ; Is it a digit (less or equal '9')?
                jr      c, nibble2val_1 ; Yes
                sub     7               ; Adjust for A-F
nibble2val_1    sub     '0'             ; Fold back to 0..15
                and     $f              ; Only return lower 4 bits
                ret
;
; Convert a single character contained in A to upper case:
;
to_upper        cp      'a'             ; Nothing to do if not lower case
                ret     c
                cp      'z' + 1         ; > 'z'?
                ret     nc              ; Nothing to do, either
                and     $5f             ; Convert to upper case
                ret
;
;  Compare two null terminated strings, return >0 / 0 / <0 in A, works like
; strcmp. The routine expects two pointer in HL and DE which will be
; preserved.
;
strcmp          push    de
                push    hl
strcmp_loop     ld      a, (de)
                cp      0                       ; End of first string reached?
                jr      z, strcmp_exit
                cp      (hl)                    ; Compare two characters
                jr      nz, strcmp_exit         ; Different -> exit
                inc     hl
                inc     de
                jr      strcmp_loop
strcmp_exit     sub     (hl)
                pop     hl
                pop     de
                ret
;
;******************************************************************************
;***
;*** IO routines
;***
;******************************************************************************
;
; Send a CR/LF pair:
;
crlf            push    af
                ld      a, cr
                call    putc
                ld      a, lf
                call    putc
                pop     af
                ret
;
; Read a single character from the serial line, result is in A:
;
getc            call    rx_ready
                in      a, (uart_register_0)
                ret
;
; Get a byte in hexadecimal notation. The result is returned in A. Since
; the routine get_nibble is used only valid characters are accepted - the 
; input routine only accepts characters 0-9a-f.
;
get_byte        push    bc              ; Save contents of B (and C)
                call    get_nibble      ; Get upper nibble
                rlc     a
                rlc     a
                rlc     a
                rlc     a
                ld      b, a            ; Save upper four bits
                call    get_nibble      ; Get lower nibble
                or      b               ; Combine both nibbles
                pop     bc              ; Restore B (and C)
                ret
;
; Get a hexadecimal digit from the serial line. This routine blocks until
; a valid character (0-9a-f) has been entered. A valid digit will be echoed
; to the serial line interface. The lower 4 bits of A contain the value of 
; that particular digit.
;
get_nibble      call    getc            ; Read a character
                call    to_upper        ; Convert to upper case
                call    is_hex          ; Was it a hex digit?
                jr      nc, get_nibble  ; No, get another character
                call    nibble2val      ; Convert nibble to value
                call    print_nibble
                ret
;
; Get a word (16 bit) in hexadecimal notation. The result is returned in HL.
; Since the routines get_byte and therefore get_nibble are called, only valid
; characters (0-9a-f) are accepted.
;
get_word        push    af
                call    get_byte        ; Get the upper byte
                ld      h, a
                call    get_byte        ; Get the lower byte
                ld      l, a
                pop     af
                ret
;
;  Read a string from STDIN - HL contains the buffer start address,
; B contains the buffer length.
;
gets            push    af
                push    bc
                push    hl
gets_loop       call    getc                    ; Get a single character
                cp      cr                      ; Skip CR characters
                jr      z, gets_loop            ; only LF will terminate input
                call    to_upper
                call    putc                    ; Echo character
                cp      lf                      ; Terminate string at
                jr      z, gets_exit            ; LF or
                ld      (hl), a                 ; Copy character to buffer
                inc     hl
                djnz    gets_loop
gets_exit       ld      (hl), 0                 ; Insert termination byte
                pop     hl
                pop     bc
                pop     af
                ret
;
; print_byte prints a single byte in hexadecimal notation to the serial line.
; The byte to be printed is expected to be in A.
;
print_byte      push    af              ; Save the contents of the registers
                push    bc
                ld      b, a
                rrca
                rrca
                rrca
                rrca
                call    print_nibble    ; Print high nibble
                ld      a, b
                call    print_nibble    ; Print low nibble
                pop     bc              ; Restore original register contents
                pop     af
                ret
;
; print_nibble prints a single hex nibble which is contained in the lower 
; four bits of A:
;
print_nibble    push    af              ; We won't destroy the contents of A
                and     $f              ; Just in case...
                add     '0'             ; If we have a digit we are done here.
                cp      '9' + 1         ; Is the result > 9?
                jr      c, print_nibble_1
                add     'A' - '0' - $a  ; Take care of A-F
print_nibble_1  call    putc            ; Print the nibble and
                pop     af              ; restore the original value of A
                ret
;
; print_word prints the four hex digits of a word to the serial line. The 
; word is expected to be in HL.
;
print_word      push    hl
                push    af
                ld      a, h
                call    print_byte
                ld      a, l
                call    print_byte
                pop     af
                pop     hl
                ret
;
; Send a single character to the serial line (a contains the character):
;
putc            call    tx_ready
                out     (uart_register_0), a
                ret
;
; Send a string to the serial line, HL contains the pointer to the string:
;
puts            push    af
                push    hl
puts_loop       ld      a, (hl)
                cp      eos             ; End of string reached?
                jr      z, puts_end     ; Yes
                call    putc
                inc     hl              ; Increment character pointer
                jr      puts_loop       ; Transmit next character
puts_end        pop     hl
                pop     af
                ret
;
; Wait for an incoming character on the serial line:
;
rx_ready        push    af
rx_ready_loop   in      a, (uart_register_5)
                bit     0, a
                jr      z, rx_ready_loop
                pop     af
                ret
;
; Wait for UART to become ready to transmit a byte:
;
tx_ready        push    af
tx_ready_loop   in      a, (uart_register_5)
                bit     5, a
                jr      z, tx_ready_loop
                pop af
                ret
;
;******************************************************************************
;***
;*** IDE routines
;***
;******************************************************************************
;
ide_data_low    equ     ide_base + $0
ide_data_high   equ     ide_base + $8
ide_error_code  equ     ide_base + $1
;
;       Bit mapping of ide_error_code register:
;
;               0: 1 = DAM not found
;               1: 1 = Track 0 not found
;               2: 1 = Command aborted
;               3: Reserved
;               4: 1 = ID not found
;               5: Reserved
;               6: 1 = Uncorrectable ECC error
;               7: 1 = Bad block detected
;
ide_secnum      equ     ide_base + $2
;
;       Typically set to 1 sector to be transf.
;
ide_lba0        equ     ide_base + $3
ide_lba1        equ     ide_base + $4
ide_lba2        equ     ide_base + $5
ide_lba3        equ     ide_base + $6
;
;       Bit mapping of ide_lba3 register:
;
;               0 - 3: LBA bits 24 - 27
;               4    : Master (0) or slave (1) selection
;               5    : Always 1
;               6    : Set to 1 for LBA access
;               7    : Always 1
;
ide_status_cmd  equ     ide_base + $7
;
;       Useful commands (when written):
;
;               $20: Read sectors with retry
;               $30: Write sectors with retry
;               $EC: Identify drive
;
;       Status bits (when read):
;
;               0 = ERR:  1 = Previous command resulted in an error
;               1 = IDX:  Unused
;               2 = CORR: Unused
;               3 = DRQ:  1 = Data Request Ready (sector buffer ready)
;               4 = DSC:  Unused
;               5 = DF:   1 = Write fault
;               6 = RDY:  1 = Ready to accept command
;               7 = BUSY: 1 = Controller is busy executing a command
;
ide_retries     equ     $ff                     ; Number of retries for polls
;
;
;  Get ID information from drive. HL is expected to point to a 512 byte byte 
; sector buffer. If carry is set, the function did not complete correctly and 
; was aborted. 
;
ide_get_id      push    af
                push    bc
                push    hl
                call    ide_ready               ; Is the drive ready?
                jr      c, ide_get_id_err       ; No - timeout!
                ld      a, $a0                  ; Master, no LBA addressing
                out     (ide_status_cmd), a
                call    ide_ready               ; Did the command complete?
                jr      c, ide_get_id_err       ; Timeout!
                ld      a, $ec                  ; Command to read ID
                out     (ide_status_cmd), a     ; Write command to drive
                call    ide_ready               ; Can we proceed?
                jr      c, ide_get_id_err       ; No - timeout, propagate carry
                call    ide_error_check         ; Any errors?
                jr      c, ide_get_id_err       ; Yes - something went wrong
                call    ide_bfr_ready           ; Is the buffer ready to read?
                jr      c, ide_get_id_err       ; No
                ld      hl, buffer              ; Load the buffer's address
                ld      b, $0                   ; We will read 256 words
ide_get_id_lp   in      a, (ide_data_low)       ; Read high (!) byte
                ld      c, a
                in      a, (ide_data_high)      ; Read low (!) byte
                ld      (hl), a
                inc     hl
                ld      (hl), c
                inc     hl
                djnz    ide_get_id_lp           ; Read next word
                jr      ide_get_id_exit         ; Everything OK, just exit
ide_get_id_err  ld      hl, ide_get_id_msg      ; Print error message
                call    puts
ide_get_id_exit pop     hl
                pop     bc
                pop     af
                ret
ide_get_id_msg  defb    "FATAL(IDE): Aborted!", cr, lf
;
;  Test if the buffer of the IDE disk drive is ready for transfer. If not, 
; carry will be set, otherwise carry is reset. The contents of register A will 
; be destroyed!
;
ide_bfr_ready   push    bc
                and     a                       ; Clear carry assuming no error
                ld      b, ide_retries          ; How many retries?
ide_bfr_loop    in      a, (ide_status_cmd)     ; Read IDE status register
                bit     3, a                    ; Check DRQ bit
                jr      nz, ide_bfr_exit        ; Buffer is ready
                push    bc
                ld      b, $0                   ; Wait a moment
ide_bfr_wait    nop
                djnz    ide_bfr_wait
                pop     bc
                djnz    ide_bfr_loop            ; Retry
                scf                             ; Set carry to indicate timeout
                ld      hl, ide_bfr_rdy_err
                call    puts
ide_bfr_exit    pop     bc
                ret
ide_bfr_rdy_err defb "FATAL(IDE): ide_bfr_ready timeout!", cr, lf, eos
;
;  Test if there is any error flagged by the drive. If carry is cleared, no 
; error occured, otherwise carry will be set. The contents of register A will 
; be destroyed.
;
ide_error_check and     a                       ; Clear carry (no err expected)
                in      a, (ide_status_cmd)     ; Read status register
                bit     0, a                    ; Test error bit
                jr      z, ide_ec_exit          ; Everything is OK
                scf                             ; Set carry due to error
ide_ec_exit     ret
;
;  Read a sector from the drive. If carry is set after return, the function did 
; not complete correctly due to a timeout. HL is expected to contain the start 
; address of the sector buffer while BC and DE contain the sector address 
; (LBA3, 2, 1 and 0). Register A's contents will be destroyed!
;
ide_rs          push    bc
                push    hl
                call    ide_ready               ; Is the drive ready?
                jr      c, ide_rs_err           ; No - timeout!
                call    ide_set_lba             ; Setup the drive's registers
                call    ide_ready               ; Everything OK?
                jr      c, ide_rs_err           ; No - timeout!
                ld      a, $20
                out     (ide_status_cmd), a     ; Issue read command
                call    ide_ready               ; Can we proceed?
                jr      c, ide_rs_err           ; No - timeout, set carry 
                call    ide_error_check         ; Any errors?
                jr      c, ide_rs_err           ; Yes - something went wrong
                call    ide_bfr_ready           ; Is the buffer ready to read?
                jr      c, ide_rs_err           ; No
                ld      b, $0                   ; We will read 256 words
ide_rs_loop     in      a, (ide_data_low)       ; Read low byte
                ld      (hl), a                 ; Store this byte
                inc     hl
                in      a, (ide_data_high)      ; Read high byte
                ld      (hl), a
                inc     hl
                djnz    ide_rs_loop             ; Read next word until done
                jr      ide_rs_exit
ide_rs_err      ld      hl, ide_rs_err_msg      ; Print error message
                call    puts
ide_rs_exit     pop     hl
                pop     bc
                ret
ide_rs_err_msg  defb    "FATAL(IDE): ide_rs timeout!", cr, lf, eos
;
;  Write a sector from the drive. If carry is set after return, the function did
; not complete correctly due to a timeout. HL is expected to contain the start 
; address of the sector buffer while BC and DE contain the sector address 
; (LBA3, 2, 1 and 0). Register A's contents will be destroyed!
;
ide_ws          push    bc
                push    hl
                call    ide_ready               ; Is the drive ready?
                jr      c, ide_ws_err           ; No - timeout!
                call    ide_set_lba             ; Setup the drive's registers
                call    ide_ready               ; Everything OK?
                jr      c, ide_ws_err           ; No - timeout!
                ld      a, $30
                out     (ide_status_cmd), a     ; Issue read command
                call    ide_ready               ; Can we proceed?
                jr      c, ide_ws_err           ; No - timeout, set carry 
                call    ide_error_check         ; Any errors?
                jr      c, ide_ws_err           ; Yes - something went wrong
                call    ide_bfr_ready           ; Is the buffer ready to read?
                jr      c, ide_ws_err           ; No
                ld      b, $0                   ; We will write 256 word
ide_ws_loop     ld      a, (hl)                 ; Get first byte from memory
                ld      c, a
                inc     hl
                ld      a, (hl)                 ; Get next byte
                out     (ide_data_high), a      ; Write high byte to controller
                ld      a, c                    ; Recall low byte again
                out     (ide_data_low), a       ; Write low byte -> strobe
                djnz    ide_ws_loop
                jr      ide_ws_exit
ide_ws_err      ld      hl, ide_ws_err_msg      ; Print error message
                call    puts
ide_ws_exit     pop     hl
                pop     bc
                ret
ide_ws_err_msg  defb    "FATAL(IDE): ide_ws timeout!", cr, lf, eos
;
;  Set sector count and LBA registers of the drive. Registers BC and DE contain 
; the sector address (LBA 3, 2, 1 and 0).
;
ide_set_lba     push    af
                ld      a, $1                   ; We will transfer 
                out     (ide_secnum), a         ; one sector at a time
                ld      a, e
                out     (ide_lba0), a           ; Set LBA0, 1 and 2 directly
                ld      a, d
                out     (ide_lba1), a
                ld      a, c
                out     (ide_lba2), a
                ld      a, b                    ; Special treatment for LBA3
                and     $0f                     ; Only bits 0 - 3 are LBA3
                or      $e0                     ; Select LBA and master drive
                out     (ide_lba3), a
                pop     af
                ret
;
;  Test if the IDE drive is not busy and ready to accept a command. If it is 
; ready the carry flag will be reset and the function returns. If a time out 
; occurs, C will be set prior to returning to the caller. Register A will 
; be destroyed!
;
ide_ready       push    bc
                and     a                       ; Clear carry assuming no error
                ld      b, ide_retries          ; Number of retries to timeout
ide_ready_loop  in      a, (ide_status_cmd)     ; Read drive status
                and     a, $c0                  ; Only bits 7 and 6 are needed
                xor     $40                     ; Invert the ready flag
                jr      z, ide_ready_exit       ; Exit if ready and not busy 
                push    bc
                ld      b, $0                   ; Wait a moment
ide_ready_wait  nop
                djnz    ide_ready_wait
                pop     bc
                djnz    ide_ready_loop          ; Retry
                scf                             ; Set carry due to timeout
                ld      hl, ide_rdy_error
                call    puts
                in      a, (ide_error_code)
                call    print_byte
ide_ready_exit  pop     bc
                ret
ide_rdy_error   defb    "FATAL(IDE): ide_ready timeout!", cr, lf, eos
;
;******************************************************************************
;***
;*** Miscellaneous functions
;***
;******************************************************************************
;
; Clear the computer (not to be called - jump into this routine):
;
cold_start      ld      hl, start_type
                ld      (hl), $00
warm_start      ld      hl, clear_msg
                call    puts
                ld      a, $00
                ld      (ram_end), a
                rst     $00
clear_msg       defb    "CLEAR", cr, lf, eos
;
;******************************************************************************
;***
;*** Mathematical routines
;***
;******************************************************************************
;
; 32 bit add routine from
;       http://www.andreadrian.de/oldcpu/Z80_number_cruncher.html
;
; ADD ROUTINE 32+32BIT=32BIT
; H'L'HL = H'L'HL + D'E'DE
; CHANGES FLAGS
;
ADD32:  ADD     HL,DE   ; 16-BIT ADD OF HL AND DE
        EXX
        ADC     HL,DE   ; 16-BIT ADD OF HL AND DE WITH CARRY
        EXX
        RET
;
; 32 bit multiplication routine from
;       http://www.andreadrian.de/oldcpu/Z80_number_cruncher.html
;
; MULTIPLY ROUTINE 32*32BIT=32BIT
; H'L'HL = B'C'BC * D'E'DE; NEEDS REGISTER A, CHANGES FLAGS
;
MUL32:  AND     A               ; RESET CARRY FLAG
        SBC     HL,HL           ; LOWER RESULT = 0
        EXX
        SBC     HL,HL           ; HIGHER RESULT = 0
        LD      A,B             ; MPR IS AC'BC
        LD      B,32            ; INITIALIZE LOOP COUNTER
MUL32LOOP:
        SRA     A               ; RIGHT SHIFT MPR
        RR      C
        EXX
        RR      B
        RR      C               ; LOWEST BIT INTO CARRY
        JR      NC,MUL32NOADD
        ADD     HL,DE           ; RESULT += MPD
        EXX
        ADC     HL,DE
        EXX
MUL32NOADD:
        SLA     E               ; LEFT SHIFT MPD
        RL      D
        EXX
        RL      E
        RL      D
        DJNZ    MUL32LOOP
        EXX
        RET
;
;******************************************************************************
;***
;*** FAT file system routines
;***
;******************************************************************************
;
;  Read a single byte from a file. IY points to the FCB. The byte read is
; returned in A, on EOF the carry flag will be set.
;
fgetc           push    bc
                push    de
                push    hl
;  Check if fcb_file_pointer == fcb_file_size. In this case we have reached
; EOF and will return with a set carry bit. (As a side effect, the attempt to
; read from a file which has not been successfully opened before will be
; handled like encountering an EOF at the first fgetc call.)
                ld      a, (iy + fcb_file_size)
                cp      (iy + fcb_file_pointer)
                jr      nz, fgetc_start
                ld      a, (iy + fcb_file_size + 1)
                cp      (iy + fcb_file_pointer + 1)
                jr      nz, fgetc_start
                ld      a, (iy + fcb_file_size + 2)
                cp      (iy + fcb_file_pointer + 2)
                jr      nz, fgetc_start
                ld      a, (iy + fcb_file_size + 3)
                cp      (iy + fcb_file_pointer + 3)
                jr      nz, fgetc_start
; We have reached EOF, so set carry and leave this routine:
                scf
                jp      fgetc_exit
;  Check if the lower 9 bits of the file pointer are zero. In this case
; we need to read another sector (maybe from another cluster):
fgetc_start     ld      a, (iy + fcb_file_pointer)
                cp      0
                jp      nz, fgetc_getc          ; Bits 0-7 are not zero
                ld      a, (iy + fcb_file_pointer + 1)
                and     1
                jp      nz, fgetc_getc          ; Bit 8 is not zero
; The file_pointer modulo 512 is zero, so we have to load the next sector:
; We have to check if fcb_current_cluster == 0 which will be the case in the
; initial run. Then we will copy fcb_first_cluster into fcb_current_cluster.
                ld      a, (iy + fcb_current_cluster)
                cp      0
                jr      nz, fgetc_continue      ; Not the initial case
                ld      a, (iy + fcb_current_cluster + 1)
                cp      0
                jr      nz, fgetc_continue      ; Not the initial case
; Initial case: We have to fill fcb_current_cluster with fcb_first_cluste:
                ld      a, (iy + fcb_first_cluster)
                ld      (iy + fcb_current_cluster), a
                ld      a, (iy + fcb_first_cluster + 1)
                ld      (iy + fcb_current_cluster + 1), a
                jr      fgetc_clu2sec
; Here is the normal case - we will check if fcb_cluster_sector is zero -
; in this case we have to determine the next sector to be loaded by looking
; up the FAT. Otherwise (fcb_cluster_sector != 0) we will just get the next
; sector in the current cluster.
fgetc_continue  ld      a, (iy + fcb_cluster_sector)
                jr      nz, fgetc_same          ; The current cluster is valid
;  Here we know that we need the first sector of the next cluster of the file.
; The upper eight bits of the fcb_current_cluster point to the sector of the
; FAT where the entry we are looking for is located (this is true since a
; sector contains 512 bytes which corresponds to 256 FAT entries). So we must
; load the sector with the number fatstart + fcb_current_cluster[15-8] into
; the IDE buffer and locate the entry with the address
; fcb_current_cluster[7-0] * 2. This entry contains the sector number we are
; looking for.
                ld      hl, (fat1start)
                ld      c, (iy + fcb_current_cluster + 1)
                ld      b, 0
                add     hl, bc
                ld      de, hl                  ; Needed for ide_rs
                ld      bc, 0
                ld      hl, (fat1start + 2)
                adc     hl, bc
                ld      bc, hl                  ; Needed for ide_rs
                ld      hl, buffer
                call    ide_rs
;  Now the sector containing the FAT entry we are looking for is available in
; the IDE buffer. Now we need fcb_current_cluster[7-0] * 2
                ld      b, 0
                ld      c, (iy + fcb_current_cluster)
                sla     c
                rl      b
; Now get the entry:
                ld      hl, buffer
                add     hl, bc
                ld      bc, (hl)
                ld      (iy + fcb_current_cluster), c
                ld      (iy + fcb_current_cluster), b
; Now we determine the first sector of the cluster to be read:
fgetc_clu2sec   ld      a, (clusiz)             ; Initialize fcb_cluster_sector
                ld      (iy + fcb_cluster_sector), a
                ld      l, (iy + fcb_current_cluster)
                ld      h, (iy + fcb_current_cluster + 1)
                call    clu2sec                 ; Convert cluster to sector
                jr      fgetc_rs
fgetc_same      and     a                       ; Clear carry
                ld      bc, 1                   ; Increment fcb_current_sector
                ld      l, (iy + fcb_current_sector)
                ld      h, (iy + fcb_current_sector + 1)
                add     hl, bc
                ld      (iy + fcb_current_sector), l
                ld      e, l                    ; Needed for ide_rs
                ld      (iy + fcb_current_sector + 1), h
                ld      d, h                    ; Needed for ide_rs
                ld      l, (iy + fcb_current_sector + 2)
                ld      h, (iy + fcb_current_sector + 3)
                ld      bc, 0
                adc     hl, bc
                ld      (iy + fcb_current_sector + 2), l
                ld      c, l                    ; Needed for ide_rs
                ld      (iy + fcb_current_sector + 3), h
                ld      b, h                    ; Neede for ide_rs
fgetc_rs        ld      (iy + fcb_current_sector), e    ; Now read the sector
                ld      (iy + fcb_current_sector + 1), d
                ld      (iy + fcb_current_sector + 2), c
                ld      (iy + fcb_current_sector + 3), b
; Let HL point to the sector buffer in the FCB:
                push    iy                      ; Start of FCB
                pop     hl
                push    bc
                ld      bc, fcb_file_buffer     ; Displacement of sector buffer
                add     hl, bc
                pop     bc
                call    ide_rs                  ; Read a single sector from disk
; Since we have read a sector we have to decrement fcb_cluster_sector
                dec     (iy + fcb_cluster_sector)
; Here we read and return a single character from the sector buffer:
fgetc_getc      push    iy
                pop     hl                      ; Copy IY to HL
                ld      bc, fcb_file_buffer
                add     hl, bc                  ; HL points to the sector bfr.
; Get the lower 9 bits of the file pointer as displacement for the buffer:
                ld      c, (iy + fcb_file_pointer)
                ld      a, (iy + fcb_file_pointer + 1)
                and     1                       ; Get rid of bits 9-15
                ld      b, a
                add     hl, bc                  ; Add byte offset
                ld      a, (hl)                 ; get one byte from buffer
; Increment the file pointer:
                ld      l, (iy + fcb_file_pointer)
                ld      h, (iy + fcb_file_pointer + 1)
                ld      bc, 1
                add     hl, bc
                ld      (iy + fcb_file_pointer), l
                ld      (iy + fcb_file_pointer + 1), h
                ld      bc, 0
                ld      l, (iy + fcb_file_pointer + 2)
                ld      h, (iy + fcb_file_pointer + 3)
                adc     hl, bc
                ld      (iy + fcb_file_pointer + 2), l
                ld      (iy + fcb_file_pointer + 3), h
;
                and     a                       ; Clear carry
fgetc_exit      pop     hl
                pop     de
                pop     bc
                ret
;
;  Clear the FCB to which IY points -- this should be called every time one
; creates a new FCB. (Please note that fopen does its own call to clear_fcb.)
;
clear_fcb       push    af                      ; We have to save so many
                push    bc                      ; Registers since the FCB is
                push    de                      ; cleared using LDIR.
                push    hl
                ld      a, 0
                push    iy
                pop     hl
                ld      (hl), a                 ; Clear first byte of FCB
                ld      de, hl
                inc     de
                ld      bc, fcb_file_buffer
                ldir                            ; And transfer this zero byte
                pop     hl                      ; down to the relevant rest
                pop     de                      ; of the buffer.
                pop     bc
                pop     af
                ret
;
; Dump a file control block (FCB) - the start address is expected in IY.
;
dump_fcb        push    af
                push    hl
                ld      hl, dump_fcb_1
                call    puts
                push    iy                      ; Load HL with
                pop     hl                      ; the contents of IY
                call    print_word
; Print the filename:
                ld      hl, dump_fcb_2
                call    puts
                push    iy
                pop     hl
                call    puts
; Print file size:
                ld      hl, dump_fcb_3
                call    puts
                ld      h, (iy + fcb_file_size + 3)
                ld      l, (iy + fcb_file_size + 2)
                call    print_word
                ld      h, (iy + fcb_file_size + 1)
                ld      l, (iy + fcb_file_size)
                call    print_word
; Print cluster number:
                ld      hl, dump_fcb_4
                call    puts
                ld      h, (iy + fcb_first_cluster + 1)
                ld      l, (iy + fcb_first_cluster)
                call    print_word
; Print file type:
                ld      hl, dump_fcb_5
                call    puts
                ld      a, (iy + fcb_file_type)
                call    print_byte
; Print file pointer:
                ld      hl, dump_fcb_6
                call    puts
                ld      h, (iy + fcb_file_pointer + 3)
                ld      l, (iy + fcb_file_pointer + 2)
                call    print_word
                ld      h, (iy + fcb_file_pointer + 1)
                ld      l, (iy + fcb_file_pointer)
                call    print_word
; Print current cluster number:
                ld      hl, dump_fcb_7
                call    puts
                ld      h, (iy + fcb_current_cluster + 1)
                ld      l, (iy + fcb_current_cluster)
                call    print_word
; Print current sector:
                ld      hl, dump_fcb_8
                call    puts
                ld      h, (iy + fcb_current_sector + 3)
                ld      l, (iy + fcb_current_sector + 2)
                call    print_word
                ld      h, (iy + fcb_current_sector + 1)
                ld      l, (iy + fcb_current_sector)
                call    print_word
                call    crlf
                pop     hl
                pop     af
                ret
dump_fcb_1      defb    "Dump of FCB at address: ", eos
dump_fcb_2      defb    cr, lf, tab, "File name      : ", eos
dump_fcb_3      defb    cr, lf, tab, "File size      : ", eos
dump_fcb_4      defb    cr, lf, tab, "1st cluster    : ", eos
dump_fcb_5      defb    cr, lf, tab, "File type      : ", eos
dump_fcb_6      defb    cr, lf, tab, "File pointer   : ", eos
dump_fcb_7      defb    cr, lf, tab, "Current cluster: ", eos
dump_fcb_8      defb    cr, lf, tab, "Current sector : ", eos
;
;  Convert a user specified filename to an 8.3-filename without dot and
; with terminating null byte. HL points to the input string, DE points to
; a 12 character buffer for the filename. This function is used by
; fopen which expects a human readable string that will be transformed into
; an 8.3-filename without the dot for the following directory lookup.
;
str2filename    push    af
                push    bc
                push    de
                push    hl
                ld      (str2filename_de), de
                ld      a, ' '                  ; Initialize output buffer
                ld      b, $b                   ; Fill 11 bytes with spaces
str2filiniloop  ld      (de), a
                inc     de
                djnz    str2filiniloop
                ld      a, 0                    ; Add terminating null byte
                ld      (de), a
                ld      de, (str2filename_de)   ; Restore DE pointer
; Start string conversion
                ld      b, 8
str2filini_nam  ld      a, (hl)
                cp      0                       ; End of string reached?
                jr      z, str2filini_x
                cp      '.'                     ; Dot found?
                jr      z, str2filini_ext
                ld      (de), a
                inc     de
                inc     hl
                dec     b
                jr      nz, str2filini_nam
str2filini_skip ld      a, (hl)
                cp      0                       ; End of string without dot?
                jr      z, str2filini_x         ; Nothing more to do
                cp      '.'
                jr      z, str2filini_ext       ; Take care of extension
                inc     hl                      ; Prepare for next character
                jr      str2filini_skip         ; Skip more characters
str2filini_ext  inc     hl                      ; Skip the dot
                push    hl                      ; Make sure DE points
                ld      hl, (str2filename_de)   ; into the filename buffer
                ld      bc, 8                   ; at the start position
                add     hl, bc                  ; of the filename extension
                ld      de, hl
                pop     hl
                ld      b, 3
str2filini_elp  ld      a, (hl)
                cp      0                       ; End of string reached?
                jr      z, str2filini_x         ; Nothing more to do
                ld      (de), a
                inc     de
                inc     hl
                dec     b
                jr      nz, str2filini_elp      ; Next extension character
str2filini_x    pop     hl
                pop     de
                pop     bc
                pop     af
                ret
;
;  Open a file with given filename (format: 'FFFFFFFFXXX') in the root directory
; and return the 1st cluster number for that file. If the file can not
; be found, $0000 will be returned.
;  At entry, HL must point to the string buffer while IY points to a valid
; file control block that will hold all necessary data for future file accesses.
;
fopen   push    af
                push    bc
                push    de
                push    ix
                ld      (fopen_scr), hl
                ld      hl, fatname             ; Check if a disk has been
                ld      a, (hl)                 ; mounted.
                cp      0
                jp      z, fopen_e1             ; No disk - error exit
                call    clear_fcb
                push    iy                      ; Copy IY to DE
                pop     de
                ld      hl, (fopen_scr) ; Create the filename
                call    str2filename            ; Convert string to a filename
                ld      hl, buffer              ; Compute buffer overflow
                ld      bc, $0200               ; address - this is the bfr siz.
                add     hl, bc                  ; and will be used in the loop
                ld      (fopen_eob), hl         ; This is the buffer end addr.
;
                ld      hl, (rootstart)         ; Remember the initial root
                ld      (fopen_rsc), hl         ; sector number
                ld      hl, (rootstart + 2)
                ld      (fopen_rsc + 2), hl
; Read one root directory sector
fopen_nbf       ld      bc, (fopen_rsc + 2)
                ld      de, (fopen_rsc)
                ld      hl, buffer
                call    ide_rs                  ; Read one sector
                jp      c, fopen_e2             ; Exit on read error
fopen_lp        ld      (fopen_scr), hl
                xor     a                       ; Last entry?
                cp      (hl)                    ; The last entry has first
                jp      z, fopen_x              ; byte = $0
                ld      a, $e5                  ; Deleted entry?
                cp      (hl)
                jr      z, fopen_nxt            ; Get next entry
;                ld      (fopen_scr), hl
                ld      ix, (fopen_scr)
                ld      a, (ix + $b)            ; Get attribute byte
                cp      $0f
                jr      z, fopen_nxt            ; Skip long name
                bit     4, a                    ; Skip directories
                jr      nz, fopen_nxt
; Compare the filename with the one we are looking for:
                ld      (ix + $b), 0            ; Clear attribute byte
                ld      de, (fopen_scr)
                push    iy                      ; Prepare string comparison
                pop     hl
                call    strcmp                  ; Compare filename with string
                cp      0                       ; Are strings equal?
                jr      nz, fopen_nxt           ; No - check next entry
                ld      a, (ix + $1a + 1)       ; Read cluster number and
; Save cluster_number into fcb_first_cluster:
                ld      (iy + fcb_first_cluster + 1), a
                ld      a, (ix + $1a)
                ld      (iy + fcb_first_cluster), a
                ld      a, (ix + $1c)           ; Save file size to FCB
                ld      (iy + fcb_file_size), a
                ld      a, (ix + $1d)           ; Save file size to FCB
                ld      (iy + fcb_file_size + 1), a
                ld      a, (ix + $1e)           ; Save file size to FCB
                ld      (iy + fcb_file_size + 2), a
                ld      a, (ix + $1f)           ; Save file size to FCB
                ld      (iy + fcb_file_size + 3), a
                ld      (iy + fcb_file_type), 1 ; Set file type to found
                jr      fopen_x                 ; Terminate lookup loop
fopen_nxt       ld      bc, $20
                ld      hl, (fopen_scr)
                add     hl, bc
                ld      (fopen_scr), hl
                ld      bc, (fopen_eob)         ; Check for end of buffer
                and     a                       ; Clear carry
                sbc     hl, bc                  ; ...no 16 bit cp :-(
                jp      nz, fopen_lp            ; Buffer is still valid
                ld      hl, fopen_rsc           ; Increment sector number
                inc     (hl)                    ; 16 bits are enough :-)
                jp      fopen_nbf               ; Read next directory sector
fopen_e1        ld      hl, fopen_nmn           ; No disk mounted
                jr      fopen_err               ; Print error message
fopen_e2        ld      hl, fopen_rer           ; Directoy sector read error
fopen_err       call    puts
fopen_x         pop     ix
                pop     de
                pop     bc
                pop     af
                ret
fopen_nmn       defb    "FATAL(FOPEN): No disk mounted!", cr, lf, eos
fopen_rer       defb    "FATAL(FOPEN): Could not read directory sector!"
                defb    cr, lf, eos
;
;  Convert a cluster number into a sector number. The cluster number is
; expected in HL, the corresponding sector number will be returned in
; BC and DE, thus ide_rs or ide_ws can be called afterwards.
;
; SECNUM = (CLUNUM - 2) * CLUSIZ + DATASTART
;
clu2sec         push    af                      ; Since the 32 bit
                push    hl                      ; multiplication routine
                exx                             ; needs shadow registers
                push    bc                      ; we have to push many,
                push    de                      ; many registers here
                push    hl
                ld      bc, 0                   ; Clear BC' and DE' for
                ld      de, bc                  ; 32 bit multiplication
                exx
                ld      bc, 2                   ; Subtract 2
                sbc     hl, bc                  ; HL = CLUNUM - 2
                ld      bc, hl                  ; BC = HL; BC' = 0
                ld      a, (clusiz)
                ld      d, 0                    ; CLUSIZ bits 8 to 15
                ld      e, a                    ; DE = CLUSIZ
                call    MUL32                   ; HL = (CLUNUM - 2) * CLUSIZ
                ld      de, (datastart)
                exx
                ld      de, (datastart + 2)
                exx
                call    ADD32                   ; HL = HL + DATASTART
                exx
                push    hl
                exx
                pop     bc
                ld      de, hl
                exx
                pop     hl
                pop     de
                pop     bc
                exx
                pop     hl
                pop     af
                ret
;
; Print a directory listing
;
dirlist         push    af
                push    bc
                push    de
                push    hl
                push    ix
                ld      hl, fatname
                ld      a, (hl)
                cp      0
                jp      z, dirlist_nodisk
                ld      ix, string_81_bfr
                ld      (ix + 8), '.'           ; Dot between name and extens.
                ld      (ix + 12), 0            ; String terminator
                ld      hl, dirlist_0           ; Print title line
                call    puts
                ld      hl, buffer              ; Compute buffer overflow
                ld      bc, $0200               ; address - this is the bfr siz.
                add     hl, bc
                ld      (dirlist_eob), hl       ; This is the buffer end addr.
;
                ld      hl, (rootstart)         ; Remember the initial root
                ld      (dirlist_rootsec), hl   ; sector number
                ld      hl, (rootstart + 2)
                ld      (dirlist_rootsec + 2), hl
; Read one root directory sector
dirlist_nbfr    ld      bc, (dirlist_rootsec + 2)
                ld      de, (dirlist_rootsec)
                ld      hl, buffer
                call    ide_rs
                jp      c, dirlist_e1
dirlist_loop    xor     a                       ; Last entry?
                cp      (hl)                    ; The last entry has first
                jp      z, dirlist_exit         ; byte = $0
                ld      a, $e5                  ; Deleted entry?
                cp      (hl)
                jr      z, dirlist_next
                ld      (dirlist_scratch), hl
                ld      ix, (dirlist_scratch)
                ld      a, (ix + $b)            ; Get attribute byte
                cp      $0f
                jr      z, dirlist_next         ; Skip long name
                ld      de, string_81_bfr       ; Prepare for output
                ld      bc, 8                   ; Copy first eight characters
                ldir
                inc     de
                ld      bc, 3                   ; Copy extension
                ldir
;                ld      hl, de
;                ld      (hl), 0                 ; String terminator
                ld      hl, string_81_bfr
                call    puts       
                ld      hl, dirlist_NODIR       ; Flag directories with "DIR"
                bit     4, a
                jr      z, dirlist_prtdir
                ld      hl, dirlist_DIR
dirlist_prtdir  call    puts
                ld      h, (ix + $1c + 3)       ; Get and print file size
                ld      l, (ix + $1c + 2)
                call    print_word
                ld      h, (ix + $1c + 1)
                ld      l, (ix + $1c)
                call    print_word
; Get and print start sector
                ld      a, tab
                call    putc
                ld      h, (ix + $1a + 1)       ; Get cluster number
                ld      l, (ix + $1a)
                ld      bc, 0                   ; Is file empty?
                and     a                       ; Clear carry
                sbc     hl, bc                  ; Empty file -> Z set
                jr      z, dirlist_nosize
                call    clu2sec
                ld      hl, bc
                call    print_word
                ld      hl, de
                call    print_word
dirlist_nosize  call    crlf
                ld      hl, (dirlist_scratch)
dirlist_next    ld      bc, $20
                add     hl, bc
                ld      bc, (dirlist_eob)       ; Check for end of buffer
                and     a
                sbc     hl, bc
                jp      nz, dirlist_loop        ; Buffer is still valid
                ld      hl, dirlist_rootsec
                inc     (hl)
                jp      dirlist_nbfr
dirlist_e1      ld      hl, dirlist_1
                jr      dirlist_x
dirlist_nodisk  ld      hl, dirlist_nomnt
dirlist_x       call    puts
dirlist_exit    pop     ix
                pop     hl
                pop     de
                pop     bc
                pop     af
                ret
dirlist_nomnt   defb    "FATAL(DIRLIST): No disk mounted!", cr, lf, eos
dirlist_0       defb    "Directory contents:", cr, lf
                defb    "-------------------------------------------", cr, lf
                defb    "FILENAME.EXT  DIR?   SIZE (BYTES)"
                defb    "  1ST SECT", cr, lf
                defb    "-------------------------------------------", cr, lf
                defb    eos
dirlist_1       defb    "FATAL(DIRLIST): Could not read directory sector"
                defb    cr, lf, eos
dirlist_DIR     defb    tab, "DIR", tab, eos
dirlist_NODIR   defb    tab, tab, eos
;
; Perform a disk mount
;
fatmount        push    af
                push    bc
                push    de
                push    hl
                push    ix
                ld      hl, buffer              ; Read MBR into buffer
                ld      bc, 0
                ld      de, 0
                call    ide_rs
                jp      c, fatmount_e1          ; Error reading MBR?
                ld      ix, buffer + $1fe       ; Check for $55AA as MBR trailer
                ld      a, $55
                cp      (ix)
                jp      nz, fatmount_e2
                ld      a, $aa
                cp      (ix + 1)
                jp      nz, fatmount_e2
                ld      bc, 8                   ; Get partition start and size
                ld      hl, buffer + $1c6
                ld      de, pstart
                ldir
                ld      hl, buffer              ; Read partition boot block
                ld      de, (pstart)
                ld      bc, (pstart + 2)
                call    ide_rs
                jp      c, fatmount_e3          ; Error reading boot block?
                ld      bc, 8                   ; Copy FAT name
                ld      hl, buffer + 3
                ld      de, fatname
                ldir
                ld      ix, buffer
                ld      a, 2                    ; Check for two FATs
                cp      (ix + $10)
                jp      nz, fatmount_e4         ; Wrong number of FATs
                xor     a                       ; Check for 512 bytes / sector
                cp      (ix + $b)
                jp      nz, fatmount_e5
                ld      a, 2
                cp      (ix + $c)
                jp      nz, fatmount_e5
                ld      a, (buffer + $d)        ; Get cluster size
                ld      (clusiz), a
                ld      bc, (buffer + $e)       ; Get reserved sector number
                ld      (ressec), bc
                ld      bc, (buffer + $16)      ; Get FAT size in sectors
                ld      (fatsec), bc
                ld      bc, (buffer + $11)      ; Get length of root directory
                ld      (rootlen), bc
                ld      hl, (pstart)            ; Compute
                ld      bc, (ressec)            ; FAT1START = PSTART + RESSEC
                add     hl, bc
                ld      (fat1start), hl
                ld      hl, (pstart + 2)
                ld      bc, 0
                adc     hl, bc
                ld      (fat1start + 2), hl
                ld      hl, (fatsec)            ; Compute ROOTSTART for two FATs
                add     hl, hl                  ; ROOTSTART = FAT1START +
                ld      bc, hl                  ;             2 * FATSIZ
                ld      hl, (fat1start)
                add     hl, bc
                ld      (rootstart), hl
                ld      hl, (fat1start + 2)
                ld      bc, 0
                adc     hl, bc
                ld      (rootstart + 2), hl
                ld      bc, (rootlen)           ; Compute rootlen / 16
                sra     b                       ; By shifting it four places
                rr      c                       ; to the right
                sra     b                       ; This value will be used
                rr      c                       ; for the calculation of
                sra     b                       ; DATASTART
                rr      c
                sra     b
                rr      c
                ld      hl, (rootstart)         ; Computer DATASTART
                add     hl, bc
                ld      (datastart), hl
                ld      hl, (rootstart + 2)
                ld      bc, 0
                adc     hl, bc
                ld      (datastart + 2), hl
                ld      hl, fatmount_s1         ; Print mount summary
                call    puts
                ld      hl, fatname
                call    puts
                ld      hl, fatmount_s2
                call    puts
                ld      a, (clusiz)
                call    print_byte
                ld      hl, fatmount_s3
                call    puts
                ld      hl, (ressec)
                call    print_word
                ld      hl, fatmount_s4
                call    puts
                ld      hl, (fatsec)
                call    print_word
                ld      hl, fatmount_s5
                call    puts
                ld      hl, (rootlen)
                call    print_word
                ld      hl, fatmount_s6
                call    puts
                ld      hl, (psiz + 2)
                call    print_word
                ld      hl, (psiz)
                call    print_word
                ld      hl, fatmount_s7
                call    puts
                ld      hl, (pstart + 2)
                call    print_word
                ld      hl, (pstart)
                call    print_word
                ld      hl, fatmount_s8
                call    puts
                ld      hl, (fat1start + 2)
                call    print_word
                ld      hl, (fat1start)
                call    print_word
                ld      hl, fatmount_s9
                call    puts
                ld      hl, (rootstart + 2)
                call    print_word
                ld      hl, (rootstart)
                call    print_word
                ld      hl, fatmount_sa
                call    puts
                ld      hl, (datastart + 2)
                call    print_word
                ld      hl, (datastart)
                call    print_word
                call    crlf
                jr      fatmount_exit
fatmount_e1     ld      hl, fatmount_1
                jr      fatmount_x
fatmount_e2     ld      hl, fatmount_2
                jr      fatmount_x
fatmount_e3     ld      hl, fatmount_3
                jr      fatmount_x
fatmount_e4     ld      hl, fatmount_4
                jr      fatmount_x
fatmount_e5     ld      hl, fatmount_5
fatmount_x      call    puts
fatmount_exit   pop     ix
                pop     hl
                pop     de
                pop     bc
                pop     af
                ret
fatmount_1      defb    "FATAL(FATMOUNT): Could not read MBR!", cr, lf, eos
fatmount_2      defb    "FATAL(FATMOUNT): Illegal MBR!", cr, lf, eos
fatmount_3      defb    "FATAL(FATMOUNT): Could not read partition boot block"
                defb    cr, lf, eos
fatmount_4      defb    "FATAL(FATMOUNT): FAT number not equal two!"
                defb    cr, lf, eos
fatmount_5      defb    "FATAL(FATMOUNT): Sector size not equal 512 bytes!"
                defb    cr, lf, eos
fatmount_s1     defb    tab, "FATNAME:", tab, eos
fatmount_s2     defb    cr, lf, tab, "CLUSIZ:", tab, eos
fatmount_s3     defb    cr, lf, tab, "RESSEC:", tab, eos
fatmount_s4     defb    cr, lf, tab, "FATSEC:", tab, eos
fatmount_s5     defb    cr, lf, tab, "ROOTLEN:", tab, eos
fatmount_s6     defb    cr, lf, tab, "PSIZ:", tab, tab, eos
fatmount_s7     defb    cr, lf, tab, "PSTART:", tab, eos
fatmount_s8     defb    cr, lf, tab, "FAT1START:", tab, eos
fatmount_s9     defb    cr, lf, tab, "ROOTSTART:", tab, eos
fatmount_sa     defb    cr, lf, tab, "DATASTART:", tab, eos
;
;  Dismount a FAT volume (invalidate the FAT control block by setting the
; first byte (of fatname) to zero.
;
fatunmount      push    af
                push    hl
                xor     a
                ld      hl, fatname
                ld      (hl), a
                pop     hl
                pop     af
                ret
;
                defb    "THE MONITOR ENDS HERE...", eos
       

 

 

ulmann@vaxman.de

webmaster@vaxman.de

02-OCT-2011, 08-JAN-2012, 20-FEB-2012