• Please review our updated Terms and Rules here

Installing CP/M 3 (Plus?) on a home-built Z80 computer

Z80ASM was my first thought - but (for a non-CP/M environment) ZMAC does look interesting.

Yep, MOSTEK were a ZILOG second-source for silicon - but the MOSTEK documentation describes the Z80 instructions the same way as the Zilog documentation does. They needn't be compatible (!) at the mnemonic level - but they were!

Dave
 
I was able to build zmac on my linux system (took some minor fiddling with YACC/LEX), and was able to compile LDRBIOS.ASM after making a few changes/fixes. I did nothing with the REL file, but my assumption is that it is correct. The listing file looked correct.

Here's an updated LDRBIOS.ASM:

View attachment ldrbios.asm.txt

UPDATE: I copied the REL file to my virtual H89 and used LINK on it. It complains about some "UNRECOGNIZED ITEM"s but seemed to succeed. I examined the code in SID and it looked viable.

MORE UPDATE: I found an old program to list out a REL file, and dumped the contents. The "UNRECOGNIZED ITEM"s seem to be related to code like this:

CP (serABuf+SER_BUFSIZE) & $FF

in other words, a "relocatable byte value". The value is getting forced to 00 so it will not work. It will take a little thinking to figure a new way to write that expression to avoid the relocatable attribute. Unfortunately, the ZMAC assembler does not complain - and probably generates valid REL bits - but DRI tools don't understand it. This is in console input code, so maybe it won't impact CP/M 3 Loader execution.
 
Last edited:
I was able to build zmac on my linux system (took some minor fiddling with YACC/LEX), and was able to compile LDRBIOS.ASM after making a few changes/fixes. I did nothing with the REL file, but my assumption is that it is correct. The listing file looked correct.

Here's an updated LDRBIOS.ASM:

View attachment 42519

UPDATE: I copied the REL file to my virtual H89 and used LINK on it. It complains about some "UNRECOGNIZED ITEM"s but seemed to succeed. I examined the code in SID and it looked viable.

MORE UPDATE: I found an old program to list out a REL file, and dumped the contents. The "UNRECOGNIZED ITEM"s seem to be related to code like this:

CP (serABuf+SER_BUFSIZE) & $FF

in other words, a "relocatable byte value". The value is getting forced to 00 so it will not work. It will take a little thinking to figure a new way to write that expression to avoid the relocatable attribute. Unfortunately, the ZMAC assembler does not complain - and probably generates valid REL bits - but DRI tools don't understand it. This is in console input code, so maybe it won't impact CP/M 3 Loader execution.

Not sure it would make any difference at all, but couldn't CP (serABuf+SER_BUFSIZE) & $FF be replaced with CP (serAInPtr-1) & $FF?

On a side note, I'm assuming the & $FF part clears the MSB so the CP is done against the LSB of the result of the calculation in parentheses? So it converts the 16-bit result of the calculation to an 8-bit value compatible with the CP command?

But yes, if it comes to it none of the interrupt routines are needed for LDRBIOS - but they will be required for CP/M 3 in the full BIOS and won't that cause the same problems when we try to build it?
 
A 'C' compiler or assembler warning is a run-time error waiting to occur!

You can change the expression all you like, as long as the mathematics work out right it doesn't generally matter. My rule is 'make the expression as readable (to the human) as possible'.

The expression effectively evaluates to a BYTE VALUE. The byte value (as you have correctly worked out) is the least significant byte of the value in parentheses.

I suspect your problems with the expression stems from the fact that some items are declared after the code that uses them - so the assembler doesn't know the value of some of the components of the expression during pass 1. There are generally 'rules' for dealing with expressions like this - don't! I would load the 16-bit address of the relocatable item into a register pair and work out the expression itself 'in code' rather than leaving it to the assembler and linker to work out (incorrectly in this case) what you are trying to do.

Dave
 
Last edited:
Yes, this will be needed - in some form - for the CP/M 3 BIOS. We could just eliminate the serial interrupts and these routines for the loader.

The problem is that the byte used in the CP instruction is *relocatable* - meaning that it's value will change depending on where the starting address of LDRBIOS lands at LINK time. If it were a constant, it would not cause a problem. I seem to recall this same problem from "the old days" - REL file format did not support 8-bit relocatable items. It would be interesting to see how support was added, but if any of the DRI tools are involved (i.e. LINK.COM) then we need to avoid this sort of expression.

There is also a linker program mentioned in the ZMAC docs, I'll see if that could be used. You'll still have to do the final stage of the CP/M 3 build in CP/M, unless you are going to write your own GENCPM program.

A couple ways to avoid the 8-bit relocatable objects by changing the code:

Code:
bufAend DW serABuf+SER_BUFSIZE
...
        LD      A,(bufAend)
        CP      L
        JR      NZ,...

or...detect the end of the buffer without using the address (i.e. use an index value for the current position, which is a constant ranging 0 thru SER_BUFSIZE-1, instead of using a pointer). Using an index might not be any less efficient, and might actually be faster (you only have to manipulate 16-bit values at the end, when you access the character, so most instructions are working on bytes - and if you make SER_BUFSIZE a power of 2 then the math becomes even simpler).

or...align serABuf (and serBBuf) on a 256-byte boundary so that the (8-bit) end address is a constant. Although, that is tricky in a relocatable file since the starting address may not be aligned on 256-byte pages.
 
Yes, this will be needed - in some form - for the CP/M 3 BIOS. We could just eliminate the serial interrupts and these routines for the loader.

The problem is that the byte used in the CP instruction is *relocatable* - meaning that it's value will change depending on where the starting address of LDRBIOS lands at LINK time. If it were a constant, it would not cause a problem. I seem to recall this same problem from "the old days" - REL file format did not support 8-bit relocatable items. It would be interesting to see how support was added, but if any of the DRI tools are involved (i.e. LINK.COM) then we need to avoid this sort of expression.

There is also a linker program mentioned in the ZMAC docs, I'll see if that could be used. You'll still have to do the final stage of the CP/M 3 build in CP/M, unless you are going to write your own GENCPM program.

A couple ways to avoid the 8-bit relocatable objects by changing the code:

Code:
bufAend DW serABuf+SER_BUFSIZE
...
        LD      A,(bufAend)
        CP      L
        JR      NZ,...

or...detect the end of the buffer without using the address (i.e. use an index value for the current position, which is a constant ranging 0 thru SER_BUFSIZE-1, instead of using a pointer). Using an index might not be any less efficient, and might actually be faster (you only have to manipulate 16-bit values at the end, when you access the character, so most instructions are working on bytes - and if you make SER_BUFSIZE a power of 2 then the math becomes even simpler).

or...align serABuf (and serBBuf) on a 256-byte boundary so that the (8-bit) end address is a constant. Although, that is tricky in a relocatable file since the starting address may not be aligned on 256-byte pages.

Defining bufAend seems like the simplest solution to the problem, though it's still relying on serABuf which isn't defined until near the end of the 1st pass?
 
Be careful you don't end up with something that is based on something else which is itself relocatable... you end up with a relocatable item again...

The general way relocatable items work is that you havecac16-bit vale computed by the assembler which has a 16 bit value added to it by the linker based on a specific item that is relocatable. In this case, you don't seem to be able to do this - hence the problem.

If you break your code down into smaller bits, it should work. E.g.:

LD DE, relocatable item.
ADD DE,offset value ; although this specific instruction doesn't exist!
Compare the E register to what's in A (which, in turn, came from L).

Dave
 
Defining bufAend seems like the simplest solution to the problem, though it's still relying on serABuf which isn't defined until near the end of the 1st pass?

Yes, that is the least amount of change to code design. The assembler doesn't need to know the value on the first pass. It will still be relocatable, but is 16-bit instead of 8-bit, and LINK should take care of everything and the "LD A,(bufAend)" will get the fully-relocated (byte) value at runtime.

I looked at the "ld80" that was referenced on the ZMAC web page. It does not appear to support SPR or PRL output formats - in which case it can't be used to link BNKBIOS3.SPR. So, fixing the code so that it is compatible with DRI REL tools looks to be the right approach.

I looked at the code a bit, and it appears that ZMAC (or more modern REL formats) fixed this problem by effectively storing the entire expression in the REL file. So, DRI tools won't understand that extension since it was invented after they were created. I'm not sure if these extension came from MicroSoft in the MS-DOS era or if they were created by some outside group and might not even be "standard".
 
Would this work as an alternative?

Code:
LD		DE,serABuf					; Get serABuf value
LD		A,SER_BUFSIZE					; Get size of buffer into accumulator
ADD		A,E						; Add the LSB of serABuf to get size
LD		E,A						; Store it back in E

; DE is now serABuf+SER_BUFSIZE (with no carry - but it's the LSB we're interested in anyway)

LD		A,L						; Get LSB of buffer pointer
CP		E						; Check it's within buffer

Although - what happens if (having been placed in the appropriate place in memory) the buffer straddles a page boundary? Checking just the LSB of the pointer and buffer end will return false results in that situation, so I will have to modify this code to do a full 16-bit check against serABuf+SER_BUFSIZE anyway.
 
Yes, that is the least amount of change to code design. The assembler doesn't need to know the value on the first pass. It will still be relocatable, but is 16-bit instead of 8-bit, and LINK should take care of everything and the "LD A,(bufAend)" will get the fully-relocated (byte) value at runtime.

Ah okay - sounds like the best solution then.

I looked at the "ld80" that was referenced on the ZMAC web page. It does not appear to support SPR or PRL output formats - in which case it can't be used to link BNKBIOS3.SPR. So, fixing the code so that it is compatible with DRI REL tools looks to be the right approach.

So I'll need to run through LDRBIOS.ASM and convert it to 8080 mnemonics?
 
Ah okay - sounds like the best solution then.



So I'll need to run through LDRBIOS.ASM and convert it to 8080 mnemonics?

No, you'll just have to use LINK.COM on CP/M in order to create the BNKBIOS3.SPR file. The next step will be GENCPM which has to be run on CP/M anyway, so not much difference.
 
Although - what happens if (having been placed in the appropriate place in memory) the buffer straddles a page boundary? Checking just the LSB of the pointer and buffer end will return false results in that situation, so I will have to modify this code to do a full 16-bit check against serABuf+SER_BUFSIZE anyway.

Your check for buffer end is based on equal/not-equal. So, as long as the buffer is < 256 bytes in length, you won't have an issue with page boundaries (the 8-bit buffer end value is unique).
 
Okay, had a day and a half break away from this (learning about ANSI terminal codes to apply some sort of formatting and colour to the console output) - now I'm back. :sleepy:

I just need to recap where I am with this and what I need to do next, before I get completely lost in CP/M source files... :shock:

This is what I understand to be the next steps (please correct me, it's bound to be wrong!):

  1. I have an LDRBIOS.ASM file, with modified serial interrupts to sidestep the recent relocatable-byte issue, primarily geared towards getting a working CPMLDR.COM file working. I need to get this assembled using ZMAC to produce a LDRBIOS.REL file.
  2. I need to modify a BIOSKRNL.ASM file with the code from LDRBIOS.ASM and compile it (using RMAC?) to produce a BIOS3.SPR file?
  3. I need to create CPM3.SYS using GENCPM on the BIOS3.SPR file?
 
For #1, yes that sounds correct.

For #2, assuming BIOSKRNL.ASM is a complete CP/M 3 Banked-memory BIOS (with CSEG/DSEG defining resident and banked portions of code and data), and assuming Zilog Mnemonics, you will need to produce a BIOSKRNL.REL using ZMAC, then on CP/M use LINK to produce BNKBIOS3.SPR from BIOSKRNL.REL. I'm assuming you will need to, at least, link in SCB.REL as we previously discussed. I think you need:
Code:
LINK BNKBIOS3=BIOSKRNL,SCB[B,OS,NR]

For #3, take a look at the section "System Generation" in the CP/M Plus "System Guide". The questions asked by GENCPM are fairly simple, as long as your BIOS doesn't defer too much allocation to GENCPM. This, of course, has to be run on CP/M. GENCPM will produce CPM3.SYS, which is a complete image of banked and resident portions of BDOS and BIOS. CPMLDR knows how to read that image and load it into memory, and run it. So, minimal content for a CP/M 3 disk (the way your are booting it) is CPMLDR.COM, CPM3.SYS, and CCP.COM. This is assuming you are going to start CP/M 3 from a running CP/M 2.2 instance.

Probably the trickiest part of #2 will be getting the right parts of the BIOS in CSEG (common/resident part) vs. DSEG (banked part). The easiest, but least efficient, is to just put it all in CSEG - provided that does not produce more than 16K of code when combined with RESBDOS3. But, to give you the largest possible TPA for programs, you'll want to move more code (and data) into DSEG. But that requires careful consideration of the context in which the code is run - and any requirements of the BDOS. The "System Guide" is pretty good about specifying whether code and data structures require common memory or a specific bank - and under what conditions. Each BIOS entry should be classified by how it is called (which bank is selected). It still requires keeping a clear head and thinking through the execution of the BIOS function and all the places that data is accessed (for example, the DPB is returned "by reference" and must be accessible from user programs as well as BNKBDOS, so it must reside in common memory).
 
For #1, yes that sounds correct.

Okay, I've run ZMAC on the most recent version of LDRBIOS.ASM and got no errors (in fact no response at all!) In the /zout folder, there's quite a few files that have been created, however. Here's the .LST output and the .REL file:

View attachment LDRBIOS.zip

Haven't done anything else with it yet, but the next step is to see if I can get CPMLDR.COM created. Then I'll need to port the relevant code from LDRBIOS.ASM into BIOSKRNL.ASM. I haven't looked at that file yet (it's described as a template) but I'm guessing it'll have extra functions for banking etc. I was intending originally to just get a non-banked version of CP/M 3 up and running first, but am happy to go straight ahead and start writing bank-switching routines etc.

For #2, assuming BIOSKRNL.ASM is a complete CP/M 3 Banked-memory BIOS (with CSEG/DSEG defining resident and banked portions of code and data), and assuming Zilog Mnemonics, you will need to produce a BIOSKRNL.REL using ZMAC, then on CP/M use LINK to produce BNKBIOS3.SPR from BIOSKRNL.REL. I'm assuming you will need to, at least, link in SCB.REL as we previously discussed. I think you need:
Code:
LINK BNKBIOS3=BIOSKRNL,SCB[B,OS,NR]

Right. Hopefully I'll get some time to look at BIOSKRNL.ASM later in the week. Finally feel like I'm making a little progress though. :D I know next to nothing about LINK, however. Don't I need to specify a start address (i.e. 0100h) at some point - either in the ZMAC/RMAC assembly prior to producing the .REL files or whilst LINKing them in CP/M?

Probably the trickiest part of #2 will be getting the right parts of the BIOS in CSEG (common/resident part) vs. DSEG (banked part). The easiest, but least efficient, is to just put it all in CSEG - provided that does not produce more than 16K of code when combined with RESBDOS3. But, to give you the largest possible TPA for programs, you'll want to move more code (and data) into DSEG. But that requires careful consideration of the context in which the code is run - and any requirements of the BDOS. The "System Guide" is pretty good about specifying whether code and data structures require common memory or a specific bank - and under what conditions. Each BIOS entry should be classified by how it is called (which bank is selected). It still requires keeping a clear head and thinking through the execution of the BIOS function and all the places that data is accessed (for example, the DPB is returned "by reference" and must be accessible from user programs as well as BNKBDOS, so it must reside in common memory).

Righto. I can't really see it going past 16K, but I've been wrong before on this subject. So that's what CSEG and DSEG are for? Okay, well, assembling and linking a valid CPMLDR is my first task - will see if I get any errors trying to do that then I'll be back! :D
 
I still see one more place that needs to be fixed in GETFROMCHARB, that still produces the unsupported REL components. See line 580 (as numbered by ZMAC) of the listing file.

Generally, use of CSEG/DSEG and ORG are mutually exclusive. The point of REL files, most of the time, is to let the linker decide the addresses. In most case, you'll break things by adding ORG statements to a file that's used as relocatable. The "OC" and "OS" linker options tell LINK certain defaults, such as ORG 0100H in the case of "OC".

LINK.COM will assemble the listed components in the order they appear on the command, and assign addresses accordingly. In this case, that is the desired behavior. The SPR files are also relocatable, albeit by "page" (256 byte) only. "SPR" == "System Page Relocatable". GENCPM will assemble the SPR files (RESBDOS3.SPR, BNKBDOS3.SPR, and BNKBIOS3.SPR) into an image and relocate addresses based on where they will be loaded into memory - by CPMLDR. So, both LINK and GENCPM will do relocation of the code, but for different purposes.
 
I still see one more place that needs to be fixed in GETFROMCHARB, that still produces the unsupported REL components. See line 580 (as numbered by ZMAC) of the listing file.

Thanks durgadas311, I missed that! Okay, here's the output from LINK this time around:

Code:
A>LINK CPMLDR[L100]=CPMLDR,LDRBIOS
LINK 1.31

ABSOLUTE     0000
CODE SIZE    1B86 (0100-1C85)
DATA SIZE    0000
COMMON SIZE  0000
USE FACTOR     35

A>

CPMLDR.COM is 8K in size. Running it clears the screen and prints the message, "Preparing MMU..." with a CR & LF, then freezes the system. I presume this is normal as I don't have a CPM3.SYS or CPM3.COM file yet?
 
If CPMLDR got to the point of looking for CPM3.SYS or CCP.COM, and did not find them, it should have spit a specific error message. Your BIOS prints several messages showing the progress of the initialization, so if only this one is seen it would suggest to me that the system crashed after enabling the MMU (last output to the MMU port clears the disable bit). Another possibility is that the LDRBIOS MMU initialization is not the same as what was in use when CP/M 2.2 loaded and ran LDRBIOS.COM - in which case enabling the MMU with a different mapping will "pull the rug out from under itself". Maybe make sure that the LDRBIOS is initializing the MMU correctly, given that it is running as a program inside CP/M 2.2. Also, keep in mind that the BIOS3 code will need to also understand the initial mapping, since that must be known as "bank 0" for the rest of it's lifetime.

Maybe the LDRBIOS should not initialize from scratch, but just use the values in 0055H-0059H. That will only work if the values stored in 0055H... are strictly kept in sync. You will still need to establish "bank 0" and "bank 1" for use in CP/M 3, and CPMLDR will need to load into "bank 0".
 
If CPMLDR got to the point of looking for CPM3.SYS or CCP.COM, and did not find them, it should have spit a specific error message. Your BIOS prints several messages showing the progress of the initialization, so if only this one is seen it would suggest to me that the system crashed after enabling the MMU (last output to the MMU port clears the disable bit). Another possibility is that the LDRBIOS MMU initialization is not the same as what was in use when CP/M 2.2 loaded and ran LDRBIOS.COM - in which case enabling the MMU with a different mapping will "pull the rug out from under itself". Maybe make sure that the LDRBIOS is initializing the MMU correctly, given that it is running as a program inside CP/M 2.2. Also, keep in mind that the BIOS3 code will need to also understand the initial mapping, since that must be known as "bank 0" for the rest of it's lifetime.

Maybe the LDRBIOS should not initialize from scratch, but just use the values in 0055H-0059H. That will only work if the values stored in 0055H... are strictly kept in sync. You will still need to establish "bank 0" and "bank 1" for use in CP/M 3, and CPMLDR will need to load into "bank 0".

My bad. I didn't have time to think about the output last night, so should have held back on posting. Looked again this morning and yes, it was the MMU-mapping section of code causing the problem. The MMU was being disabled, then the areas re-mapped (to identical banks as they already were under CP/M 2.2) and the MMU enabled again. The issue was, as soon as the MMU was disabled, the rug was pulled (to use your excellent comparison) from under the code and it crashed.

I've updated the MMU code section to not disable the MMU and just set the new bank mappings 'on the fly'. I could (in theory) remove that section of code entirely, but as I have a custom-made 'BANK' command in CP/M 2.2, it's entirely possible the user may have messed around with the memory map before trying to load CP/M 3, so a reset-to-default is an important safety measure.

Here's the output I'm getting now when I run CPMLDR:

Code:
A>cpmldr
Preparing MMU...
Setting MMU lock byte...
Initialising SIO...
Z80 MINICOM II CP/M BIOS 1.2.74 by J.Nock 2017
Based on original CP/M BIOS 1.0 by G.Searle 2007-13

CP/M 2.2 Copyright 1979 (c) by Digital Research

(lots of empty lines removed for readability)

CP/M V3.0 Loader
Copyright (C) 1982, Digital Resea[
▒"▒ ▒y▒ڥ2▒ ▒2▒     :▒      2▒      {2▒     !"      "▒      9"@1▒!I ▒y▒2▒▒K!▒▒▒▒dڛ_^#V*▒   ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒         ▒▒▒▒6   ▒▒▒▒<   B       ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒}▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒[[D▒▒▒I :▒▒�▒▒

▒y!▒     ▒4▒ ▒5~▒▒yµ5▒▒
                       ▒6▒y▒   � ͒:▒ ▒▒▒▒!▒
                                            ▒▒▒Oͻ▒▒▒▒MD▒▒2 ▒>▒▒
BDOS ERR:
CPMLDR error:  failed to read CPM3.SYS

EDIT:

Maybe the LDRBIOS should not initialize from scratch, but just use the values in 0055H-0059H. That will only work if the values stored in 0055H... are strictly kept in sync. You will still need to establish "bank 0" and "bank 1" for use in CP/M 3, and CPMLDR will need to load into "bank 0".

Yeah, I don't trust the values in 0055H-0059H at all. The BANK command is a little glitchy at the moment, albeit only in the way it reports the existing memory map - at CP/M 2.2 startup, the values are being overwritten by something (Area 0's value at 0055H should be 00h, but it's always something else - so something is overwriting it. I need to find a safer spot in memory to save these values.)
 
Back
Top