Image Map Image Map
Results 1 to 10 of 10

Thread: Small Z80 monitor

  1. #1

    Default Small Z80 monitor

    I'm new to Z80. I'm bring up a new board based on Z280. I want to use the UART bootstrap capability of the Z280 to serial load a small boot monitor (256 bytes max) which, in turn, loads a full feature monitor. I do want to learn Z80 programming, but right now I have many unknowns and it is not the time to open another can of worms. I like to know whether such small boot monitor already existed?

  2. #2
    Join Date
    Mar 2013
    Location
    Chaffee, MO
    Posts
    1,203

    Default

    Plasmo,
    I'm sure there are others but one I downloaded was MINIMON.ZIP. Here is the README! File:
    MINIMON version 9 Readme - Richard Thwaites 22/10/2010

    ZIP PACKAGE CONTAINS:

    README.1st - THIS FILE
    MINIMON9.Z80 - MINIMON SOURCE
    MONLIFT.Z80 - LOAD HIGH MODULE SOURCE
    SERILOAD.BAT - EXAMPLE DOS BATCH TO SEND USING PRINT
    SERILOD1.BAT - EXAMPLE DOS BATCH TO SEND USING COPY
    GOMON.CPM - EXAMPLE CP/M COMMAND TO JUMP TO MINIMON
    (rename to GOMON.COM ONCE TRANSFERRED TO CP/M


    USAGE

    Minimon is a tool to assist restoration, operation and maintenance of 8080/Z80
    computers independent of an operating system or a working boot device other than
    a bootable 2732 EPROM slot or equivalent).

    Burned into a boot EPROM, Minimon cold-boots with a self-test sequence
    that checks system elements and sends a confirmation character to
    console after each test.

    The order of tests is Console (C), RAM (R),Stack (S), Disk (#D).
    At the keyboard test, Minimon pauses for input. Input character is echoed to console,
    followed by (K).

    If the initial keypress is "M", Minimon displays its menu and waits for a command.
    If any other key is pressed, Minimon jumps to the module to load CP/M from diskette.
    So it only interrupts the boot process when you ask it to.

    Minimon can start to be useful as an automatic test of functionality even in a system
    with only EPROM and CPU functioning. Console I/O will be tested first. If console i/o
    is working but RAM and disk drives are not, Minimon can be assembled to function permanently
    from EPROM while locating faults in RAM and in disk drive controller or configuration.

    In current configuration, EPROM cold boots at 000H, then writes itself into RAM at
    location C600H. DRAM is intialised, then EPROM is disabled (via phantom signal) and
    execution continues in RAM.

    Hex code can be entered directly to memory from console (S).

    Binary data can be loaded to any memory address from a file on another machine, or from
    data typed on another machine, via serial port (L).

    Code in memory can be run, from a nominated start address to a single breakpoint, where
    state of registers and flags is displayed (G).

    Minimon allows for loading, testing and overlaying any amount of code at any address, even
    when there is no disk drive working in the system and no operating system loaded.
    It provides an alternate means to load and test a whole system, or system modifications,
    that are developed on another machine (or emulator), without swapping disks or burning
    EPROMS for each iteration.

    This supports doing system development (using emulators) in a modern, multi-tasking,
    high-speed environment, then moving code into the z80 system only when ready to test.
    The SIMH/AltairZ80 emulator provides easy movement of files between CP/M and Windows
    environments.



    SOURCES:

    This core elements of this code are based upon upon several 8080/Z80 monitor/debug programs,
    especially:

    - John Monahan's MASTERv420 monitor program
    - the original TDL Zapple monitor (8080 mnemonics)
    - Digital Research ddt.asm (8080 mnemonics)
    - N8VEM project's dbgmon.asm by Andew Lynch, Dan Werner and others
    - IMS International CP/M 2.23 bios.asm

    Current Hardware environment

    Z80A processor, 4MHz
    64K DRAM
    2732 EPROM at address 0000H
    Serial ASCII Console and Serial Comms port
    I/O board 2 x 6402 UART, 1 x 8255 PIO
    NEC upd765 FDD controller with DMA
    2 x Tandon TM 100-2 5.25" FDD

    Version History...

    22/10/2010 all modules tested, "G<addr>,0" gives unconditional jump,
    invoke Minimon from CP/M via "monitor.com"
    documentation and zip package.
    19/10/2010 version to load to C600, tighten up memory locs.
    3/10/2010 'load from serial' module added (from scratch)
    23/9/2010 successful cold boot code reverse-engineered
    source code cleanup, subroutines culled
    19/9/2010 revert to maximum warmboot code - larger EPROM
    10/9/2010 completely re-written compact GOTO module
    5/9/2010 non-indexed menu system for easier customization
    25/8/2010 IMS CP/M load added - still buggy
    28/7/2010 RT begins mods for IMS5000SX
    30/6/2010 Sources collected and begin conversion to Z80.



    For the Big Board II there was PFM. Here is that README:


    PFM80 USERS MANUAL


    --INTRODUCTION--

    The PFM—80 monitor is the basic control program for the BIGBOARD
    single-board computer. It begins execution when the computer is first
    turned on, or whenever the reset button is pressed, and resides in the top
    4K of memory.

    The monitor provides two essential functions for the system. It
    is the "software front panel" of the computer and it contains the routines
    that initialize and control all the basic system input/output resources.
    The "front panel" functions of the monitor include commands to display and
    alter the contents of memory and I/O ports, to begin execution at a given
    address, and to bootstrap programs from disk. The basic I/O functions of
    PFM—80 provide driving routines for the built-in CRT display and keyboard
    input, the external RS-232 serial terminal, the floppy disk controller,
    and the real—time clock. In this capacity the monitor is always active,
    even when application programs like the CP/M * disk operating system have
    control of the CPU.

    The following sections of this manual will explain how to use the
    console monitor commands, what facilities are provided by the resident I/O
    handlers, and how to interface applications programs to the monitor.

    CONSOLE MONITOR COMMAND SUMMARY
    -------------------------------

    The PPM-80 monitor enters the command mode after it has
    initialized the system following a power—on or pushbutton reset. The
    following sign-on message is displayed on the console output device as an
    indication that the monitor is ready to accept commands.

    .. system reset ..
    * _

    Commands consist of a single character command name and zero to
    three hexadecimal numeric parameters separated by commas or spaces. The
    command line may by entered using upper case or lower case letters. A
    carriage return is used as the terminator. Errors within a line can be
    corrected by typing control-H to delete the last character or control-X to
    delete the entire line. If a line is entered with an unknown command
    name, an invalid number or parameters or an out-of-range parameter, an
    error message will be displayed and the command will not be executed.

    The user may wish to halt long running commands like the memory
    dump before they are finished. This can be done by typing carriage return
    while the command is doing output. Output can also be frozen temporarialy
    and then re-started by typing repeatedly on the space bar.

    The following table summarizes the monitor's command set. The
    items enclosed in angle brackets represent the numeric parameters expected


    c 1980 Russel Smith








    There is also 2K-ROM-Monitor Here is that README File:

    http://cpuville.com/cpm_on_new_computer.html

    Setting up CP/M 2.2 on a New Z80 Computer

    This is to document how I set up CP/M 2.2 to run on a newly created Z80-based microcomputer, intended as a kit for hobbyists or educators. The computer was designed to be extremely simple to build and understand, and as such does not implement interrupts or direct memory access, even though the Z80 can support these functions. My goal was to set up CP/M in the simplest way possible.
    The System Architecture
    Memory Space

    The computer system memory space takes on one of two configurations, software selected through OUT instructions to port 0 or port 1. In configuration 0, memory locations 0x0000 to 0x07FF are ROM, and locations 0x0800 to 0xFFFF are RAM. In configuration 1, the memory is all RAM from 0x0000 to 0xFFFF. This dual configuration is needed to solve the issue presented by the Z80 CPU's requirement that some code be present at location 0x0000 when it is started, and CP/M's requirement that location 0x0000 be available for RAM. There are other ways to solve this problem of course, but with 2K of ROM present in configuration 0, subroutines can be stored there that will aid in bringing up the CP/M system.
    Input/Output Ports

    As mentioned above, ports 0 and 1 are used to select the memory configuration. An OUT (0),A instruction selects configuration 0 (2K ROM + 62K RAM), and an OUT (1),A instruction selects configuration 1 (all-RAM). There is no data transferred by these instructions, and there are no input ports implemented at addresses 0 or 1.

    There is a single serial interface implemented that uses port addresses 2 and 3. Port 2 is the data input and output port, and port 3 is the UART status/control port. The port is initialized at system start (or system reset) by code in the ROM. It does not have to be initialized by CP/M.

    The single IDE disk interface uses input/output ports 8 through 15. Only the IDE command block registers are implemented; the control block registers, which are selected if CS3FX- is asserted, are not used (CS3FX- is tied to VCC). They are not needed for proper function of the disk interface. The disk is configured so that input and output data bits 0 though 7 are sent to the system data bus; bits 8 through 15 are ignored. This means that half the storage on the drive is not used, but it simplifies the hardware and software drive interface.
    The Software
    The ROM System Monitor

    The 2K ROM contains a system monitor program with several functions accessible from a simple command line interface, or as subroutines called by user programs. The ROM monitor assembly language file is here, and the list file is here. There are commands to examine memory (dump), change memory (load), and run programs (run). There are commands to dump binary data from the memory to the serial port (bdump) and to load binary data from the serial port (bload). There are commands to read a disk sector and place it in memory (diskrd), and to write a sector to disk from memory (diskwr). Finally, there is a command to start CP/M (cpm), assuming it has been installed.
    The RAM System Monitor

    I made a copy of the ROM system monitor, assembled to address 0xDC00, so that it sits just below CP/M in the all-RAM memory configuration. This RAM monitor (the assembly language file is here and the listing is here) is for use when ROM-type functions are needed with the computer memory in configuration 1 (all-RAM). It is needed for getting the first few programs into the CP/M system through the serial port. I also wrote a CP/M version of the RAM monitor, MONITOR.COM, that was assembled to location 0x0100. The MONITOR assembly language file is here, and the listing is here. This program, when run, copies itself from location 0x0100 to location 0xDC00, to be clear of the 0x0100 address area where CP/M programs are loaded and run. It is useful for further loading of programs into CP/M by the serial port, without having to reset the computer, once CP/M is up and running.
    CP/M 2.2
    Source Code and Manuals

    CP/M is a very flexible operating system. It uses a standard core of system software that is almost entirely machine independent, other than the requirement for an 8080 or Z80 CPU. The standard core consists of the Console Command Processor (CCP) and the Basic Disk Operating System (BDOS). In addition to the standard, machine-independent core, there is a machine-dependent Basic Input/Output System (BIOS). The BIOS contains the drivers for the various hardware elements used by CP/M to operate the disk system and communicate through the input and output ports. It also contains the disk parameter tables that are used by CP/M to create and use the file system. The main task in creating a CP/M system for a new computer is to write a customized BIOS tailored to that new computer's architecture.

    The CP/M source code I used is found at The Unofficial CP/M Web Site. The archive CP/M 2.2 ASM SOURCE on the Digital Research Sources page on that site was used to build this system. The source file CPM22.Z80 was the one I assembled. This source contains the CCP and BDOS components of CP/M, along with a skeleton of the BIOS jump table (needed to assemble correctly). This source file is the work of Clark A. Calkins, and was obtained by disassembling a working system. The other CP/M sources on the web site were obtained from the original Digital Research documents by scanning and optical character recognition (OCR). As such, they have a few OCR-type errors, and are also in 8080 rather than Z80 assembly language. One can convert the 8080 to Z80 by using the awk script toZ80 (found at Github here). The script is not perfect, but most of the problems with translation are easy to recognize and fix. Nevertheless, just to get CP/M up and running, I used the CPM22.Z80 source.

    The CP/M 2.0 System Manual has been put on this web page: www.gaby.de/cpm/manuals/archive/cpm22htm/. This manual describes CP/M for the user, including the basic operating system commands. An OCR-derived CP/M 2.0 Alteration Guide document is present here also as chapter 6 of the System Manual. A scan of the original Alteration Guide is available here. The Alteration Guide explains how to create a new CP/M 2 system. However, it is targeted to a system with floppy drives, and assumes that one already has a CP/M 2 system up and running, and can use CP/M commands such as MOVECPM and DDT (the debugger program). I did not have this available. (Yes, I could have used one of the fine CP/M emulators out there, but I wanted to do everything from scratch.) However, the Alteration Guide was useful in a number of ways, as I will explain below. Finally, there is the CP/M 2.0 Interface Guide, which describes how to use CP/M features in user programs. A scanned copy of the Interface Guide can be found here.
    CCP and BDOS Assembly

    Only a few small changes were needed to assemble the CPM22.Z80 source for a 64K system. I set the code origin for the CCP to 0xE400. I adjusted a few zeros after the CCP to put the the BDOS entry at 0xEC06, and similarly at the end of the BDOS to make the BIOS entry at 0xFA00. These are the standard entry points for the CCP, BDOS, and BIOS according to the CP/M 2.0 Alteration Guide. I named the binary output file of this assembly cpm22.sys.
    Creating the BIOS

    I used the skeletal customizable BIOS code found as an appendix to the CP/M Alteration Guide as the basis for a custom BIOS for this computer. This source was only available in 8080 code, so I had to translate it to Z80 code using the AWK script toZ80 as described above. There was one OCR error in the source, an 8080 DAD 0 instruction toward the end of the SELDSK subroutine that should have been DAD D, otherwise the code was correct. I had to fix a few translation errors as well. Using this source as the basis, I wrote the minimum amount of code needed to get CP/M working, as described below. The finished customized BIOS assembly language is here, and the listing is here.
    The BIOS Coding Tasks

    There are several coding tasks needed to create a functional BIOS tailored for a certain computer system. One needs to write cold and warm boot routines, drivers for the input/output ports, drivers for the disk drive(s), and disk parameter tables.
    Boot Routines and Disk Parameter Tables

    The cold and warm start boot routines in the skeletal BIOS are written for the classical standard system with four 8-inch IBM floppy drives. I wanted to make as few changes to the skeletal BIOS source code as possible, so I left these subroutines intact. I also left the disk parameter tables essentially unchanged. See below for details on how CP/M, which thinks it is using four floppy disks, ends up using a 1 Gb hard disk.
    Coding for the Console Input and Output

    The CP/M Alteration Guide has fairly detailed explanations for what each BIOS subroutine requires, that is, which registers are used to convey information to the subroutine, and how data and error codes should be returned. The subroutines for console input and output are very simple, just single character output (CONOUT), single character input (CONIN), and input status query (CONST). Here is the code I wrote for these three subroutines:
    conin: ;returns console character in register a
    in a,(3) ;get status
    and 002h ;check RxRDY bit
    jp z,conin ;loop until char ready
    in a,(2) ;get char
    AND 7fh ;strip parity bit
    ret
    ;
    conout: ;console character output from register c
    in a,(3)
    and 001h ;check TxRDY bit
    jp z,conout ;loop until port ready
    ld a,c ;get the char
    out (2),a ;out to port
    ret
    ;
    const: ;console status, return 0ffh if character ready, 00h if not
    in a,(3) ;get status
    and 002h ;check RxRDY bit
    jp z,no_char
    ld a,0ffh ;char ready
    ret
    no_char: ld a,00h ;no char
    ret

    There are subroutines in the BIOS for character output to a list device (a printer), and for output to a punch device, and input from a reader device. The punch and reader subroutines were intended as drivers for a paper tape puncher and reader. These other character input and output drivers can be used for a variety of physical devices, but since my system has only one serial port, which is dedicated to the console, I simply left these drivers as they were in the skeletal BIOS -- simple returns without performing any actions.
    Coding for the Disk System

    The remainder of the subroutines in the BIOS have to do with disk input and output. As mentioned above, CP/M comes out of the era when essentially all systems used floppy disks. Floppy disk controllers are fairly complicated compared to modern IDE drives. Most were run using interrupt-driven, direct-memory access hardware, and often used subroutines present in a system ROM. These drives were typically much slower than the computer system, and there were timing issues to be dealt with in the driver software. Disk reading and writing errors were very common, and the software had to deal with these. The disk in a particular drive could be changed, and the software had to deal with this possibility also. So there is a lot of code in old BIOSes that is dedicated to running these floppy disks. An example of this complexity can be seen in a standard CP/M 2.2 BIOS here: www.gaby.de/cpm/manuals/archive/cpm22htm/axa.htm

    However, with the new Z80 system described here, an IDE disk interface to a hard disk is used instead. The disk interface is very simple. The disk is much faster than the system CPU, so there are no timing issues to worry about. Modern hard disks are essentially error-free. The disk cannot be changed. This makes the software to operate the disk very simple to write.
    Subroutines Needed for Reading and Writing the Disk
    SETDMA, SELDSK, SETTRK, and SETSEC Subroutines

    The CP/M BDOS accesses the disk by setting the memory address for a 128-byte sector buffer (SETDMA subroutine), selecting the disk to be accessed (SELDSK), and setting the track (SETTRK), and sector (SETSEC) to be accessed. In old BIOSes, especially those written for disk hardware using interrupts and direct memory access, these subroutines interacted directly with the disk hardware. Once these settings were made, a disk read or write command was given, and the software waited for the task to be finished. However, with the IDE disk, these subroutines need merely set variables in RAM to be used by the disk read and write subroutines. The SELDSK subroutine has additional code that returns the address to the proper disk parameter table header. More about the disk parameter tables later.
    ;
    setdma: ;set dma address given by registers b and c
    LD l, c ;low order address
    LD h, b ;high order address
    LD (dmaad),HL ;save the address
    ret
    ;
    seldsk: ;select disk given by register c
    LD HL, 0000h ;error return code
    LD a, c
    LD (diskno),A
    CP disks ;must be between 0 and 3
    RET NC ;no carry if 4, 5,...
    ; disk number is in the proper range
    ; defs 10 ;space for disk select
    ; compute proper disk Parameter header address
    LD A,(diskno)
    LD l, a ;l=disk number 0, 1, 2, 3
    LD h, 0 ;high order zero
    ADD HL,HL ;*2
    ADD HL,HL ;*4
    ADD HL,HL ;*8
    ADD HL,HL ;*16 (size of each header)
    LD DE, dpbase
    ADD HL,DE ;hl=,dpbase (diskno*16). Note typo "DAD 0" here in original 8080 source.
    ret
    ;
    settrk: ;set track given by register c
    LD a, c
    LD (track),A
    ret
    ;
    setsec: ;set sector given by register c
    LD a, c
    LD (sector),A
    ret
    ;
    Disk READ and WRITE Subroutines

    An important issue with CP/M 2.2 is the requirement for disk data to be handled in 128-byte pieces. This comes out of the floppy disk era when disk sectors were this size. Modern disks have larger sectors, 512 bytes in the IDE standard (although the hardware of this computer only uses 256 bytes of each sector). CP/M 2.2 has some available BIOS routines for sector deblocking, so that sector sizes larger than 128 bytes can be used, the software breaking up the larger sector into the 128-byte pieces that CP/M wants to see. We could use the CP/M sector deblocking code to make use of the full 256 bytes available in each sector, but with a 1 Gb hard drive, we do not need to preserve disk space. Therefore, in the interest of simplicity and understandability, I decided to use only 128 bytes of each sector. Yes, that means I am only using one-quarter of the available disk space, but this is still a vast disk space for an 8-bit system. The disk READ and WRITE subroutines below use a 256-byte host buffer for the physical access, and additional code sends to or receives from the 128-byte buffer set by the SETDMA subroutine the top 128 bytes of the 256 byte physical sector.

    Modern hard disks can use either cylinder-head-sector (CHS) addressing or logical block addressing (LBA). In LBA mode, the disk is presented as one long array of sectors, numbered from 0x000000 to 0x1FFFFF for a 1 Gb disk. I decided to use LBA to access the disk in a very simple way. The BIOS disk read and write subroutines set LBA bits 0 to 7 to the CP/M sector, bits 8 to 15 to the CP/M track, and bits 16 and 17 to the CP/M disk. For this scheme to work one needs at least a 340 Mb drive, but drives of this capacity are easily found. To use a disk of smaller capacity one would need to write code to translate the CP/M addressing to LBA addressing more efficiently, in a way that did not waste so much space. But, just to get it working, this is what I did.

    Here are the disk READ and WRITE subroutines for my customized BIOS:
    read:
    ;Read one CP/M sector from disk.
    ;Return a 00h in register a if the operation completes properly, and 01h if an error occurs during the read.
    ;Disk number in 'diskno'
    ;Track number in 'track'
    ;Sector number in 'sector'
    ;Dma address in 'dmaad' (0-65535)
    ;
    ld hl,hstbuf ;buffer to place disk sector (256 bytes)
    rd_status_loop_1: in a,(0fh) ;check status
    and 80h ;check BSY bit
    jp nz,rd_status_loop_1 ;loop until not busy
    rd_status_loop_2: in a,(0fh) ;check status
    and 40h ;check DRDY bit
    jp z,rd_status_loop_2 ;loop until ready
    ld a,01h ;number of sectors = 1
    out (0ah),a ;sector count register
    ld a,(sector) ;sector
    out (0bh),a ;lba bits 0 - 7
    ld a,(track) ;track
    out (0ch),a ;lba bits 8 - 15
    ld a,(diskno) ;disk (only bits 16 and 17 used)
    out (0dh),a ;lba bits 16 - 23
    ld a,11100000b ;LBA mode, select host drive 0
    out (0eh),a ;drive/head register
    ld a,20h ;Read sector command
    out (0fh),a
    rd_wait_for_DRQ_set: in a,(0fh) ;read status
    and 08h ;DRQ bit
    jp z,rd_wait_for_DRQ_set ;loop until bit set
    rd_wait_for_BSY_clear: in a,(0fh)
    and 80h
    jp nz,rd_wait_for_BSY_clear
    in a,(0fh) ;clear INTRQ
    read_loop: in a,(08h) ;get data
    ld (hl),a
    inc hl
    in a,(0fh) ;check status
    and 08h ;DRQ bit
    jp nz,read_loop ;loop until clear
    ld hl,(dmaad) ;memory location to place data read from disk
    ld de,hstbuf ;host buffer
    ld b,128 ;size of CP/M sector
    rd_sector_loop: ld a,(de) ;get byte from host buffer
    ld (hl),a ;put in memory
    inc hl
    inc de
    djnz rd_sector_loop ;put 128 bytes into memory
    in a,(0fh) ;get status
    and 01h ;error bit
    ret
    write:
    ;Write one CP/M sector to disk.
    ;Return a 00h in register a if the operation completes properly, and 0lh if an error occurs during the read or write
    ;Disk number in 'diskno'
    ;Track number in 'track'
    ;Sector number in 'sector'
    ;Dma address in 'dmaad' (0-65535)
    ld hl,(dmaad) ;memory location of data to write
    ld de,hstbuf ;host buffer
    ld b,128 ;size of CP/M sector
    wr_sector_loop: ld a,(hl) ;get byte from memory
    ld (de),a ;put in host buffer
    inc hl
    inc de
    djnz wr_sector_loop ;put 128 bytes in host buffer
    ld hl,hstbuf ;location of data to write to disk
    wr_status_loop_1: in a,(0fh) ;check status
    and 80h ;check BSY bit
    jp nz,wr_status_loop_1 ;loop until not busy
    wr_status_loop_2: in a,(0fh) ;check status
    and 40h ;check DRDY bit
    jp z,wr_status_loop_2 ;loop until ready
    ld a,01h ;number of sectors = 1
    out (0ah),a ;sector count register
    ld a,(sector)
    out (0bh),a ;lba bits 0 - 7 = "sector"
    ld a,(track)
    out (0ch),a ;lba bits 8 - 15 = "track"
    ld a,(diskno)
    out (0dh),a ;lba bits 16 - 23, use 16 to 20 for "disk"
    ld a,11100000b ;LBA mode, select drive 0
    out (0eh),a ;drive/head register
    ld a,30h ;Write sector command
    out (0fh),a
    wr_wait_for_DRQ_set: in a,(0fh) ;read status
    and 08h ;DRQ bit
    jp z,wr_wait_for_DRQ_set ;loop until bit set
    write_loop: ld a,(hl)
    out (08h),a ;write data
    inc hl
    in a,(0fh) ;read status
    and 08h ;check DRQ bit
    jp nz,write_loop ;write until bit cleared
    wr_wait_for_BSY_clear: in a,(0fh)
    and 80h
    jp nz,wr_wait_for_BSY_clear
    in a,(0fh) ;clear INTRQ
    and 01h ;check for error
    ret
    ;

    The label hstbuf points to a 256-byte area in RAM that holds the data after transfer from or before transfer to the disk. One might note the use of polling status bits to decide when all 256 bytes of data have been transferred. One can code ones own 256-byte counter, or use the 256-byte counter that is internal to the drive. I used the latter method here.
    Disk Parameter Tables

    CP/M needs a logical description of the disk system in order to operate properly. This description is needed by the system software that creates directories and files, and watches for available disk space. This description is held in a series of disk parameter tables in the BIOS, described in more detail below when I discuss the SECTRAN subroutine. The disk parameter tables do not need to be a true description of the actual physical disk drive(s), as long as one creates in the disk read and write subroutines a way to translate a BDOS request to read or write a particular logical disk sector into a read or write of a unique physical disk sector. The standard CP/M system, which is represented in the unmodified skeletal BIOS, has four 8" IBM floppy disks, each with 77 26-sector tracks (numbered 1 to 26), and a total capacity of about 250 Kb. This disk setup is also coded into the BIOS cold and warm start routines, which load the operating system from the disk into memory. Again, I decided to stick with this original, standard logical disk description, so that I was making the fewest changes to the skeletal BIOS as possible. Here are the disk parameter tables in my finished BIOS:
    ;
    ; fixed data tables for four-drive standard
    ; ibm-compatible 8" disks
    ; no translations
    ;
    ; disk Parameter header for disk 00
    dpbase: defw 0000h, 0000h
    defw 0000h, 0000h
    defw dirbf, dpblk
    defw chk00, all00
    ; disk parameter header for disk 01
    defw 0000h, 0000h
    defw 0000h, 0000h
    defw dirbf, dpblk
    defw chk01, all01
    ; disk parameter header for disk 02
    defw 0000h, 0000h
    defw 0000h, 0000h
    defw dirbf, dpblk
    defw chk02, all02
    ; disk parameter header for disk 03
    defw 0000h, 0000h
    defw 0000h, 0000h
    defw dirbf, dpblk
    defw chk03, all03
    ;
    ; sector translate vector
    trans: defm 1, 7, 13, 19 ;sectors 1, 2, 3, 4
    defm 25, 5, 11, 17 ;sectors 5, 6, 7, 6
    defm 23, 3, 9, 15 ;sectors 9, 10, 11, 12
    defm 21, 2, 8, 14 ;sectors 13, 14, 15, 16
    defm 20, 26, 6, 12 ;sectors 17, 18, 19, 20
    defm 18, 24, 4, 10 ;sectors 21, 22, 23, 24
    defm 16, 22 ;sectors 25, 26
    ;
    dpblk: ;disk parameter block for all disks.
    defw 26 ;sectors per track
    defm 3 ;block shift factor
    defm 7 ;block mask
    defm 0 ;null mask
    defw 242 ;disk size-1
    defw 63 ;directory max
    defm 192 ;alloc 0
    defm 0 ;alloc 1
    defw 0 ;check size
    defw 2 ;track offset
    ;
    ; end of fixed tables
    ;

    The key table, labeled dpblk, is the description of the floppy disk. The block shift factor, block mask, and null mask are linked to the block size, here 1024 bytes. See the Alteration Guide for details. The disk size tells CP/M that the disk has 243 blocks available for data. The directory max value tells CP/M to use at most 64 directory entries for each disk. The allocation values are a bitmap that shows CP/M which disk areas have the directories. Check size is a value used with changeable disks, set to zero here because our system has a hard disk. This is the only value in the dpblk table that I changed. (Note: I could have left it unchanged though). The track offset tells CP/M to reserve the first two tracks of each disk for system files.
    SECTRAN and the Big Bug

    I suppose with any good-sized software project you end up with a bug that makes you want to pull your hair out and/or drives you to prayer. Here is the one that got me.

    You will notice the sector translate table in the code above. This is used by the BIOS SECTRAN subroutine to translate a logical sector request into a physical sector selection that is different from the logical sector. This was used by systems with floppy disks to increase efficiency, because the disk was much slower than the system, and waiting for a full disk rotation to read the next sector in physical order took a long time. So, the logical sectors were translated to physical sectors that were 6 sectors apart, all around the track. A simple system, easy to understand.

    The translation table is connected to the rest of the CP/M by the SECTRAN subroutine in the BIOS. A SELDSK call returns the address of the disk parameter header for a particular disk, and the first item in that header is a pointer to the translation table. (The other items in the disk parameter header point to buffers and scratch areas for use by the CP/M BDOS). In the Alteration Guide, we are told that if we put 0x0000 in the place where the translation table pointer belongs, no translation will be done. Of course, in my system, no translation is needed, so I put 0x0000 there.

    After this, I assembled my BIOS and tested it by placing it into memory using the ROM monitor bload command, and running short programs that used calls to the various BIOS routines for console input and output, and disk reads and writes. All worked perfectly. I prepared (formatted) the disk. I then created the CP/M system (essentially placed it into the first two sectors of the disk, as described in detail below). It worked -- sort of. CP/M would load and seem to start normally. But, directory listings were strange, sometimes one file was listed twice. Occasional disk read errors popped up. No amount of fiddling with the disk parameter table helped. I looked at the BDOS. Maybe some 8080 to Z80 translation error there. I looked at the CCP. Tried a few things. Hair pulling and prayer was done.

    At the Lord's prompting I took a closer look at the SECTRAN subroutine. I had not changed it, because the Alteration Guide said if I put zero in the disk parameter header, no translation would be done. I was lied to.

    Here is the original SECTRAN subroutine, fixed up after translation from the 8080 code by the toZ80 awk script. By fixed up I mean that the LD l,(hl) instruction originally came out as "LD l,m" -- the m should have been changed to (hl):
    sectran:
    ;translate the sector given by bc using the
    ;translate table given by de
    EX DE,HL ;hl=.trans
    ADD HL,BC ;hl=.trans (sector)
    LD l, (hl) ;l=trans (sector)
    LD h, 0 ;hl=trans (sector)
    ret ;with value in hl

    Note that there is no test to see if the pointer to the translation table (.trans) is zero. As such, if this subroutine is called, it will return a sector value that will be some random byte from the zero page of memory. I simply added a return after the exchange and addition of the logical sector and translation pointer (which will be zero), so it just returns the sector number it was called with:
    sectran:
    ;translate the sector given by bc using the
    ;translate table given by de
    EX DE,HL ;hl=.trans
    ADD HL,BC ;hl=.trans (sector)
    ret ;debug no translation
    LD l, (hl) ;l=trans (sector)
    LD h, 0 ;hl=trans (sector)
    ret ;with value in hl

    After this CP/M ran perfectly. Apparently, some routines in the BDOS were calling SECTRAN without checking to see if translation was on or off.

    Note that if I had not changed the first word in the disk parameter table header to zero all would have worked perfectly. That is, translation would have been done, but this would not be noticeable to the user. So, after everything, I learned that I did not need to make any changes at all in the disk parameter headers, or block tables, or the translate table to get CP/M to work properly on my system.
    Preparing the Disk

    This version of CP/M is meant to be installed into the first two tracks of the disk -- specifically track 0, sectors 2 though 26, and track 1, sectors 1 to 25. This is the area of the disk that the BIOS boot subroutines load into memory when CP/M is started or rebooted. I am referring to tracks on the the logical floppy disk described in the disk parameter tables -- remember we are using LBA to physically access the hard drive, translating the CP/M logical disk-track-sector numbers to a hard disk physical sector in the BIOS disk read and write subroutines. But, since the BIOS read and write subroutines are working perfectly when called after the appropriate SETDSK, SETDMA, SELTRK, and SELSEC calls, we will use these routines to access the disk whenever we can.

    But, before installing CP/M, we need to prepare the disk for the CP/M file system. This is an easy task, since the only thing CP/M needs to see are empty directories in the disk areas where the directories will be found, as described by the disk parameter tables. The CP/M directory entry is a simple structure. It is 32 bytes long. The very first byte is the status of the directory. An empty directory has a status byte of 0E5h. If CP/M sees this byte, it considers the directory entry available, and will create an entry there. The remainder of the directory entry structure does not have to be created ahead of time, we only need to put OE5h in the first byte of the 64 directory entries on each disk. In fact, it is easier than that -- we can just fill the whole disk with the byte 0E5h.

    To do this, I wrote a simple "format" program that calls the BIOS subroutines to fill each CP/M disk with 0E5h. I used the RAM monitor bload command to load the assembled BIOS into memory at 0xFA00, then loaded the format program at 0x0300, and ran it. Here is the format program:
    ;Formats four classical CP/M disks
    ;Writes E5h to 26 sectors on tracks 2 to 77 of each disk.
    ;Uses calls to BIOS, in memory at FA00h
    seldsk: equ 0fa1bh ;pass disk no. in c
    setdma: equ 0fa24h ;pass address in bc
    settrk: equ 0fa1eh ;pass track in reg C
    setsec: equ 0fa21h ;pass sector in reg c
    write: equ 0fa2ah ;write one CP/M sector to disk
    monitor_start: equ 0dc00h
    org 0300h
    ld sp,format_stack
    ld a,00h ;starting disk
    ld (disk),a
    disk_loop: ld c,a ;CP/M disk a
    call seldsk
    ld a,2 ;starting track (offset = 2)
    ld (track),a
    track_loop: ld a,1 ;starting sector
    ld (sector),a
    ld hl,directory_sector ;address of data to write
    ld (address),hl
    ld a,(track)
    ld c,a ;CP/M track
    call settrk
    sector_loop: ld a,(sector)
    ld c,a ;CP/M sector
    call setsec
    ld bc,(address) ;memory location
    call setdma
    call write
    ld a,(sector)
    cp 26
    jp z,next_track
    inc a
    ld (sector),a
    jp sector_loop
    next_track: ld a,(track)
    cp 77
    jp z,next_disk
    inc a
    ld (track),a
    jp track_loop
    next_disk: ld a,(disk)
    inc a
    cp 4
    jp z,done
    ld (disk),a
    jp disk_loop
    done: jp monitor_start
    disk: db 00h
    sector: db 00h
    track: db 00h
    address: dw 0000h
    directory_sector:
    ds 128,0e5h ;sector filled with 0E5h
    ds 32 ;stack space
    format_stack:
    end
    Putting CP/M on the Disk

    Once I had assembled the CCP, BDOS, and customized BIOS programs, the next thing was to put them onto the disk. For this I wrote a putsys program. It uses calls to BIOS subroutines to copy a memory image of CP/M onto tracks 0 and 1 of the CP/M disk. Note track 0 sector 1 is reserved for boot code, not needed here.

    This version of the putsys program was loaded and run from the ROM monitor.
    ;Copies the memory image of CP/M loaded at E400h onto tracks 0 and 1 of the first CP/M disk
    ;Load and run from ROM monitor
    ;Uses calls to BIOS, in memory at FA00h
    ;Writes track 0, sectors 2 to 26, then track 1, sectors 1 to 25
    seldsk: equ 0fa1bh ;pass disk no. in c
    setdma: equ 0fa24h ;pass address in bc
    settrk: equ 0fa1eh ;pass track in reg C
    setsec: equ 0fa21h ;pass sector in reg c
    write: equ 0fa2ah ;write one CP/M sector to disk
    monitor_warm_start: equ 046Fh ;Return to ROM monitor
    org 0800h ;First byte in RAM when memory in configuration 0
    ld c,00h ;CP/M disk a
    call seldsk
    ;Write track 0, sectors 2 to 26
    ld a,2 ;starting sector
    ld (sector),a
    ld hl,0E400h ;start of CCP
    ld (address),hl
    ld c,0 ;CP/M track
    call settrk
    wr_trk_0_loop: ld a,(sector)
    ld c,a ;CP/M sector
    call setsec
    ld bc,(address) ;memory location
    call setdma
    call write
    ld a,(sector)
    cp 26 ;done:
    jp z,wr_trk_1 ;yes, start writing track 1
    inc a ;no, next sector
    ld (sector),a
    ld hl,(address)
    ld de,128
    add hl,de
    ld (address),hl
    jp wr_trk_0_loop
    ;Write track 1, sectors 1 to 25
    wr_trk_1: ld c,1
    call settrk
    ld hl,(address)
    ld de,128
    add hl,de
    ld (address),hl
    ld a,1
    ld (sector),a
    wr_trk_1_loop: ld a,(sector)
    ld c,a ;CP/M sector
    call setsec
    ld bc,(address) ;memory location
    call setdma
    call write
    ld a,(sector)
    cp 25
    jp z,done
    inc a
    ld (sector),a
    ld hl,(address)
    ld de,128
    add hl,de
    ld (address),hl
    jp wr_trk_1_loop
    done: jp monitor_warm_start
    sector: db 00h
    address: dw 0000h
    end

    The file cpm.sys which contains the assembled code for the CCP and BDOS was first loaded into memory at location 0xE400. Then, the BIOS was loaded at location 0xFA00. Then the putsys program was loaded at location 0x0800 and run. The drive activity light went on, and then off, and CP/M was now on the disk.
    The CP/M Loader

    To get CP/M from the disk into the system memory at computer startup, I created a cpm_loader program. Note that CP/M uses disk sectors 1 to 26 of each track. That means that the hard disk LBA sector 0 was available for use. Once I assembled the cpm_loader, I put the machine code into LBA sector zero using the ROM monitor diskwr command.

    The ROM monitor, which starts when the computer is turned on, or taken out of reset, has a command cpm that loads the full 256 bytes of hard disk sector 0 into RAM at location 0x0800 and jumps to it. Here is the ROM monitor code:
    cpm_jump: ld hl,0800h
    ld bc,0000h
    ld e,00h
    call disk_read
    jp 0800h

    It uses the ROM monitor disk_read subroutine that takes BC and E as the LBA, and HL as the memory area to write to. Here is the CP/M loader code:
    ;Retrieves CP/M from disk and loads it in memory starting at E400h
    ;Uses calls to ROM subroutine for disk read.
    ;Reads track 0, sectors 2 to 26, then track 1, sectors 1 to 25
    ;This program is loaded into LBA sector 0 of disk, read to loc. 0800h by ROM disk_read subroutine, and executed.
    ;
    hstbuf: equ 0900h ;will put 256-byte raw sector here
    disk_read: equ 0294h ;subroutine in 2K ROM
    cpm: equ 0FA00h ;CP/M cold start entry in BIOS
    org 0800h ;Start of RAM, configuration 0
    ;Read track 0, sectors 2 to 26
    ld a,2 ;starting sector -- sector 1 reserved
    ld (sector),a
    ld hl,0E400h ;memory address -- start of CCP
    ld (dmaad),hl
    ld a,0 ;CP/M track
    ld (track),a
    rd_trk_0_loop: call read
    ld a,(sector)
    cp 26
    jp z,rd_trk_1
    inc a
    ld (sector),a
    ld hl,(dmaad)
    ld de,128
    add hl,de
    ld (dmaad),hl
    jp rd_trk_0_loop
    ;Read track 1, sectors 1 to 25
    rd_trk_1: ld a,1
    ld (track),a
    ld hl,(dmaad)
    ld de,128
    add hl,de
    ld (dmaad),hl
    ld a,1 ;starting sector
    ld (sector),a
    rd_trk_1_loop: call read
    ld a,(sector)
    cp 25
    jp z,done
    inc a
    ld (sector),a
    ld hl,(dmaad)
    ld de,128
    add hl,de
    ld (dmaad),hl
    jp rd_trk_1_loop
    done: out (1),a ;switch memory config to all-RAM
    jp cpm ;to BIOS cold start entry
    read:
    ;Read one CP/M sector from disk 0
    ;Track number in 'track'
    ;Sector number in 'sector'
    ;Dma address (location in memory to place the CP/M sector) in 'dmaad' (0-65535)
    ;
    ld hl,hstbuf ;buffer to place raw disk sector (256 bytes)
    ld a,(sector)
    ld c,a ;LBA bits 0 to 7
    ld a,(track)
    ld b,a ;LBA bits 8 to 15
    ld e,00h ;LBA bits 16 to 23
    call disk_read ;subroutine in ROM
    ;Transfer top 128-bytes out of buffer to memory
    ld hl,(dmaad) ;memory location to place data read from disk
    ld de,hstbuf ;host buffer
    ld b,128 ;size of CP/M sector
    rd_sector_loop: ld a,(de) ;get byte from host buffer
    ld (hl),a ;put in memory
    inc hl
    inc de
    djnz rd_sector_loop ;put 128 bytes into memory
    in a,(0fh) ;get status
    and 01h ;error bit
    ret
    sector: db 00h
    track: db 00h
    dmaad: dw 0000h
    end

    The assembled code for this program takes up only 136 bytes so it fits easily into the single 256-byte sector alloted for it. I could also have put it into CP/M track 0 sector 1.
    Loading Programs into CP/M through the Serial Interface

    With CP/M loaded onto disk, to start it, one simply issues the cpm command at the ROM monitor prompt. The CP/M loader puts CP/M into its proper place in memory, changes the memory configuration to all-RAM, and jumps to the BIOS cold start entry point to start CP/M. The CP/M prompt A> appears, and we can enter commands.

    CP/M has a small number of internal, or built-in commands -- DIR to list the directory of a disk to the screen, ERA to erase a file, and a few others. See the System Manual for details. But, there is no built-in command available to check disk space, copy files, edit text, assemble code, or do most other things that we would expect an operating system to do. There is no command to load a file into the disk through the serial port. Once CP/M is working properly, we need a way to get programs into the CP/M disk file system.

    Fortunately, there is a built-in command SAVE that will write code from memory into a CP/M file. The SAVE command will take a number of 256-byte pages from memory, starting at location 0x0100, and create a file. So, our task is to use the RAM monitor program bload command to load a CP/M program file through the serial port into memory at 0x0100, then use the CP/M SAVE command to create a program file. Once that file is created, to run the program, one types the program name at the CP/M prompt. CP/M loads the file into memory at location 0x0100, and jumps to that location to execute it.

    Since we only have one serial port, and CP/M dedicates that port to console input and output, we have to work around CP/M to do the binary loads through that port. To get the process started, we need to have the both the RAM monitor program and CP/M present in memory at the same time. To do this, we start by loading the RAM monitor at location 0xDC00, using the ROM monitor bload command. Once the RAM monitor is in the memory, we load CP/M into memory using the ROM monitor cpm command. Now, both programs are in memory at the same time, and we are running CP/M. But, we need to be in the RAM monitor to bload programs into memory. There is no CP/M command to jump to a particular memory location. How to get from CP/M to the RAM monitor?

    Simple. We reset the computer. A computer reset does nothing to the RAM system memory -- it leaves the memory contents unchanged. If we reset the computer, then take it out of reset, we will be in the ROM monitor, with the memory in configuration 0. Then we can use the run command to jump to the RAM monitor which is in memory at 0xDC00. The first instruction in the RAM monitor changes the memory configuration to 1, so now we have access to the lower RAM locations and can bload a program into memory at 0x0100 using the RAM monitor command line. Then, we go to CP/M by using the monitor run command with the address 0xFA00 (BIOS cold start entry). (Note that even though the RAM monitor, like the ROM monitor, has a cpm command, we must not use this to start CP/M, because it will load the cpm loader program into memory at 0x0800, which might overwrite the program we previously loaded, the one we want to save.) Jumping to location 0xFA00 in the BIOS re-loads CP/M into memory, but will not touch the program file previously loaded at 0x0100. Now, at the CP/M prompt we can issue the SAVE command, and the memory contents will be saved as a file on the CP/M disk and placed in the directory, ready to be used.

    Which program should we start with? The first should be the MONITOR.COM program. This is a version of the RAM monitor designed to be called and used by CP/M. MONITOR.COM is 2008 bytes long. 2008 divided by 256 is 7.84, so we need to save at least 8 pages of memory. After bloading it to location 0x0100 with the RAM monitor, we issue this command:

    A>SAVE 8 MONITOR.COM

    A DIR command will show the new file in the disk directory. Now, if we want to load a file through the port, we can simply issue the CP/M command MONITOR and we will get the monitor prompt. The MONITOR program, when executed, moves itself from location 0x0100 where CP/M places it, to location 0xDC00. It uses buffer space, variables, and stack space in memory page 0xDB00 also. This allows us to use the memory space from 0x0100 to 0xDAFF, or 55,807 bytes, to load programs for saving in the CP/M file system. And, once a file is loaded into memory using the MONITOR program, we can return to CP/M by issuing the cpm command at the MONITOR program prompt -- this command does not affect the low program memory, just jumps to the CP/M BIOS cold entry start point, without using the cpm loader program, so the memory is left undisturbed. Then we can issue a CP/M SAVE command.

    Once the MONITOR command is working, we can load the rest of the important CP/M command files. There is STAT, which shows statistics for file sizes and disk room available. There is the ED command, which is a text editor. I got a smile out of using it, it is like editing a ghost, because you cannot see the text you are editing -- you are moving an invisible cursor through an invisible text buffer in memory to make changes, which you can see afterward using the T (type) command. There is the PIP command, for file copying. There are the ASM and LOAD commands for assembling 8080 files. There are also Z80 assemblers. Finally, there are hundreds of CP/M programs available on-line that you can download and try. The Humongous CP/M Archive is a good starting point. I found the chess program Sargon, and played a game against my computer. It won -- maybe I let it win, but I don't think so.
    Future Tasks

    Now that I have a working CP/M 2.2 system, I may try to create a better BIOS. Perhaps I will have it print out a greeting message at cold boot. I would probably remove unnecessary code, like the unused translation table. I might work on the disk read and write routines to make more efficient use of the disk. In the current configuration, a disk has 77 tracks (7 bits), 26 tracks (5 bits), and 4 disks (2 bits). That means 14 bits is enough to give each sector a unique LBA by a simple OR operation. This would be important if one wanted to use a small disk, like a 16 MB compact flash drive. I might try different disk parameters in the tables. I need to create some easy way to get files out of CP/M, probably by writing a CP/M program that loads a file into memory without executing it, and then using the MONITOR to bdump the memory containing the file to the serial port. I might try to get CP/M 3 up and running. And I will certainly play chess with my computer again.

    --Donn Stewart, July 2014

    © Donn Stewart 2014


    One of these should fit your situation.

    Larry

  3. #3

    Default

    Thank you. Minimon has the right idea, a monitor for debugging of minimal system is what I want. CP/M is the goal but it is far far away right now. The problem is to keep the monitor small, no more than 256 bytes which is what UART bootstrap will load into RAM starting from 0x0 at power on.

    I can have several versions of the 256-byte monitor that check for memory, display/modify memory content, load & go (John Monahan's MASTER is the full feature monitor I like to load into memory).

  4. #4
    Join Date
    Jan 2010
    Location
    Central VA
    Posts
    4,645

    Default

    I've got a small monitor up and working:

    https://github.com/chapmajs/glitchworks_monitor

    It targets the Intel 8080 but will of course work fine on a Z80.

  5. #5

    Default

    Thank you,

    It looks small and compact, something I may be able to figure out except I don't know Intel mnemonics and I just started with Z80 mnemonics, working on it is like translating Swahili to Arabic. Is there a translation tool between Intel mnemonics and Z80? On comp.os.cpm PC Pete had a 8080 <-> Z80 translator, but the link is broken. Is the software still available somewhere?

    I guess people took it in stride, but I find it strange having to master two machine languages to program Z80.

    Edit:
    I find this translation table by Erkki Ruohtula useful in converting Intel mnemonics to Z80
    Attached Files Attached Files
    Last edited by Plasmo; January 21st, 2018 at 06:14 PM. Reason: Add additional comments

  6. #6
    Join Date
    Mar 2013
    Location
    Chaffee, MO
    Posts
    1,203

    Default

    Plasmo,
    Here is an interesting read on Zapple.

    http://www.retrotechnology.com/restore/TDLmonitor.html

    Larry

  7. #7

    Default

    Quote Originally Posted by glitch View Post
    I've got a small monitor up and working:

    https://github.com/chapmajs/glitchworks_monitor

    It targets the Intel 8080 but will of course work fine on a Z80.
    Had to remove the 'I' and 'O' commands, pare down the Display command, and remove help and sign-on messages to make it fit in 256 bytes. The monitor is 255 bytes. It can Display memory, Edit memory, and Go. I use the UART bootstrap feature of Z280 to upload the monitor into the first 256 bytes of memory and execute. This is the screen shot of it running in Z280.

    It is not much, but enough for me to debug the hardware and as a template for a small bootloader.
    Attached Images Attached Images
    Attached Files Attached Files

  8. #8

    Default

    Wrote a small file loader that reads Intel Hex file into memory and execute. The first program to load is the Glitchworks_monitor. This is screen shot of the original glitchworks_monitor running in Z280.

    I fixed a bug in DMP16 so the address pointer increment correctly.
    Attached Images Attached Images
    Attached Files Attached Files

  9. #9
    Join Date
    Jan 2010
    Location
    Central VA
    Posts
    4,645

    Default

    Hah, awesome! It's really neat to see one of my projects up and going on someone else's hardware If you wouldn't mind, send a GitHub PR for the fix to DMP16 or just PM me the details. I'll give you credit in the repo README.

  10. #10

    Default

    Oh never mind. I examined your original code again and realize it is me who made a mistake translating Intel mnemonics to Z80. The instruction in question in DMP16 is "INX H". I translated to "INC H" which is incorrect. It should be "INC HL". The memory pointer was going everywhere and took me a while to figure out why.

Tags for this Thread

Bookmarks

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •