PDA

View Full Version : Guidelines for writing software for CP/M?



nockieboy
August 24th, 2017, 11:21 AM
Hi everyone,

Apologies if that has been asked or answered elsewhere, but I've search the interweb, skim-read a couple of CP/M books and can't find a concise answer to my question.

I've recently built a single-board computer running CP/M 2.2 on a Z80, mostly as an education in digital electronics but also as an excuse to learn Z80 assembly. What I'd really like to do right now is write my own CP/M programs. I have SLR Z80ASM on the system (which can compile straight to COM file) and the usual software for CP/M 2.2, along with TASM that I'm using on the PC to assemble the monitor ROM program etc, so these are both at my disposal.

What I really need though is a brief overview of what is required to write a basic program that I can run as a file in CP/M. My understanding so far is that the code should start at 0100h, the start of the TPA. But that's about the limit of my knowledge. I know there are routines that can be called in BDOS and CPM, but I'm not sure what is available, where it is and what its requirements are.

So, really, an example of a CP/M 'Hello World!' program would be ideal - as would any links to reference material or suggestions where I could look myself for this sort of info.

Any help would be appreciated!

Thanks :D

daver2
August 27th, 2017, 11:14 PM
Hi,

As this is your first post, welcome to VCFED.

Most of what you need can be found at http://www.seasip.info/Cpm/index.html. Look for "BDOS". I would suggest starting here
http://www.seasip.info/Cpm/bdos.html To start with.

Basically:

1. Set up your own stack and stack pointer.

2. Set up register DE to point to your string. Don't forget to terminate your string with '$'.

3. Call BDOS function 9.

4. Exit your program. Jump to location 0, call BDOS function 0, or use RST 0.

Just watch which BDOS calls you are using and compatibility with CP/M 2.2.

Ask more questions if you need to.

Dave

nockieboy
August 28th, 2017, 01:15 AM
That's awesome, thanks Dave - exactly what I was after. :D

Chuck(G)
August 28th, 2017, 08:15 AM
Personally, I'd keep things simple and use the DRI-provided tools to start--ASM, LOAD and DDT. While using the Z80 instruction set may be convenient, there's little benefit to using it for simple tasks--and it's considerably more complex for a beginner. Many commercial programs were written in 8080, not Z80 code to appeal to the widest possible group of 8-bit CP/M users. Remember that the Intel 8085 existed at the same time as the Zilog Z80.

glitch
August 28th, 2017, 08:30 AM
I wrote the first part of a multi-part article for 300 Baud Magazine on ASM for CP/M. The magazine is no longer available, but they released the PDFs for free:

http://filedump.glitchwrks.com/zines/300_baud/300_Baud_03.pdf

I stuck to the DRI tools included with CP/M 2.2. I believe I was using VEDIT as my programmer's editor at the time.

Mike_Z
August 28th, 2017, 11:31 AM
Here is a link to the CPM BDOS calls (if you don't have them). http://www.gaby.de/cpm/manuals/archive/cpm22htm/ch5.htm The BDOS calls do much of the interface work for you. You do not have to know port numbers etal. I use WordStar for my editor. EDLIN will work but it takes a little extra thinking. I'm not familiar with VEDIT. But you need to have the editor save in the text mode not a document mode. Once you have your file say test.asm you need to run ASM test. If there are errors, fix them else run LOAD which will produce the COM file. It's not rocket science, but can be confusing until you've struggled thru a few times. Try it and ask questions. Good Luck, Mike

Chuck(G)
August 28th, 2017, 12:47 PM
Easiest thing to do is to start with a "Hello World" test using ASM. It's best if you have experience with assembly language in general.

First edit and create a program file called "HELLO.ASM":



ORG 100H

BDOS EQU 0005H ; LOCATION OF BDOS ENTRY POINT
BOOT EQU 0000H ; LOCATION OF BOOT REQUEST

START:
MVI C,9 ; BDOS REQUEST 9 - PRINT STRING
LXI D,MESSAGE ; OUR STRING TO PRING
CALL BDOS
JMP BOOT ; EXIT TO CP/M

MESSAGE:
DB 13,10,'HELLO WORLD',13,10,'$'

END START





To assemble and run the program (assuming that your system is on drive A:


A>ASM HELLO.AAZ
CP/M ASSEMBLER - VER 2.0
011B
000H USE FACTOR
END OF ASSEMBLY
A>LOAD HELLO

FIRST ADDRESS 0100
LAST ADDRESS 011A
BYTES READ 001B
RECORDS WRITTEN 01

A>HELLO

HELLO WORLD
A>


Just that simple!

nockieboy
August 28th, 2017, 01:59 PM
Firstly, thanks everyone for the replies - there's plenty for me to be getting on with. Thanks Chuck for the code example - that's precisely what I needed to get me up and running. I'm certainly no assembler expert, but I'm learning! :)


I wrote the first part of a multi-part article for 300 Baud Magazine on ASM for CP/M. The magazine is no longer available, but they released the PDFs for free:

http://filedump.glitchwrks.com/zines/300_baud/300_Baud_03.pdf

I stuck to the DRI tools included with CP/M 2.2. I believe I was using VEDIT as my programmer's editor at the time.

Thanks for the link to the 300 Baud magazines, glitch - they're really interesting! I'm assuming 300 Baud never got to issue 4? :(

nockieboy
August 29th, 2017, 12:48 AM
Hmm... I'm trying the example provided by Chuck(G) above and getting the following error:


A>ASM HELLO.AAZ
CP/M ASSEMBLER - VER 2.0
NO SOURCE FILE PRESENT

A>

This is despite there being an .AAZ file present and correctly named. I've tried writing it in ED and still get the same error (I'm using the AltairZ80 emulator, so I initially tried importing a Notepad file.) Pretty much the same error when I use ZASM80 too, although that indicates line 1 as being the cause of the error (line 1 is just ORG 100H)...? :huh:

daver2
August 29th, 2017, 10:42 PM
Have you called the file HELLO.ASM or HELLO.AAZ?

The file should be called HELLO.ASM. The AAZ are the parameters to assemble the source file against. You have told the assembler that the source file HELLO.ASM is on drive A. Is this actually correct?

Checking the TDL ZASM manual, I can't see an ORG directive (all of the pseudo operations begin with a '.'), so I suspect the error that has been returned is correct in this case! The equivalent to ORG is .LOC in this assembler...

Do not keep switching assemblers - they are not compatible.

The question is, do you want to learn 8080 or Z80 mnemonics? You originally stated you wanted to learn Z80 assembly, so my recommendation would be to learn the Zilog mnemonics and pick a compatible assembler (preferably a macro assembler). Z80ASM seems good. If you want to learn to program for CP/M, then 8080 mnemonics would be a good choice, so pick your assembler accordingly (ASM or MAC will be suitable). If I can find some time today I will write a Z80 HELLO WORLD for you.

Dave

daver2
August 30th, 2017, 12:47 AM
This would be my attempt for the SLR Z80ASM assembler.

Name the file "HELLO.Z80" and assemble it with the command:

A>Z80ASM HELLO

This should take source file HELLO.Z80 and produce HELLO.COM.

Run it with:

A>HELLO

I have not run this (we don't have Z80 systems at work...). Any problems or questions, let me know and I will try and sort them out for you.

Regards,

Dave


TITLE Hello World!

; Define some ASCII control characters.
;
CR EQU 0DH
LF EQU 0AH

; Define the CP/M terminating character for BDOS function 9 (WRITESTR).
;
TERM EQU '$'

; Define some useful CP/M BDOS function numbers.
;
TERMCPM EQU 0
WRITESTR EQU 9

; Define my local stack size.
;
STACK EQU 128

; Define a useful macro to 'wrap' the call to BDOS.
;
BDOS MACRO FUNC

LD C,FUNC ; The desired BDOS function.
CALL 5 ; Entry point into CP/M BDOS.

ENDM

; This is the main program...
;
START::

; Initialise the local stack to be
; at the end of the program as loaded
; into the TPA.

LD HL,PROGEND ; Adress of end of program.
ADD HL,STACK ; Advance to allow space for stack.
LD SP,HL ; Set up the Stack Pointer.

; Display our message.
;
LD DE,HELLO ; DE points to the message.
BDOS WRITESTR ; Display the message.

; Bye bye...
;
BDOS TERMCPM ; Exit back to CP/M.

; Should never get here!

HELLO: DB CR,LF,"Hello World!",CR,LF,TERM

PROGEND:

END START

nockieboy
August 30th, 2017, 01:24 AM
Have you called the file HELLO.ASM or HELLO.AAZ?

The file should be called HELLO.ASM. The AAZ are the parameters to assemble the source file against. You have told the assembler that the source file HELLO.ASM is on drive A. Is this actually correct?


Ah, thanks Dave - that's the problem! :rolleyes: I'd called the assembly source HELLO.AAZ... was slightly confused by the ASM syntax!! ;)

It has now compiled happily and I've got a working 'hello world!' COM file! :D



Checking the TDL ZASM manual, I can't see an ORG directive (all of the pseudo operations begin with a '.'), so I suspect the error that has been returned is correct in this case! The equivalent to ORG is .LOC in this assembler...

Do not keep switching assemblers - they are not compatible.


Yes, I'm trying to stick with what I know (or am learning), which in my case is the TASM assembler as that's what I'm using (via DOSBOX) to compile my monitor ROM code for the SBC.



The question is, do you want to learn 8080 or Z80 mnemonics? You originally stated you wanted to learn Z80 assembly, so my recommendation would be to learn the Zilog mnemonics and pick a compatible assembler (preferably a macro assembler). Z80ASM seems good. If you want to learn to program for CP/M, then 8080 mnemonics would be a good choice, so pick your assembler accordingly (ASM or MAC will be suitable). If I can find some time today I will write a Z80 HELLO WORLD for you.

Dave

Well, I (currently) have no interest whatsoever in the 8080 (why would I when I have a Z80?) ;) I grew up in the 80's with an Amstrad CPC464 and dabbled with assembly back then (though got nowhere near as far as I have now), and have now built a Z80-based computer, so Z80 assembly is the natural choice for me. NOTE: I'm not trying to start an 8080/Z80 war, I know the Z80 shares commands with the 8080 and I'm sure the 8080 was a capable processor in its own right, I'm just more familiar with the Z80. :)

Also, thanks for the Z80ASM code sample you just posted while I was writing this! My reply probably won't get through moderation for a good few hours, but I'll definitely be using Z80ASM in future as it makes more sense - compiles straight to COM and looks like it has some interesting features (the macro you've declared is interesting - don't know if TASM supports those (haven't looked) but could make the code much prettier!)

nockieboy
August 30th, 2017, 02:00 AM
OK, I entered the above program via ED (whoa.. talk about having to learn keyboard shortcuts!) and tried compiling. The first error was that Z80ASM expects the file to end in .Z80 instead of .ASM - that was easily fixed. I also had to make a small addition to the assembly to get it to work as it seems ADD doesn't like adding a value to HL, so I loaded STACK into DE first then added DE to HL. (I picked DE at random - let me know if it's likely to break anything!)



LD HL,PROGEND ; ADDRESS OF THE END OF PROGRAM.
LD DE,STACK ; LOAD STACK SIZE INTO DE.
ADD HL,DE ; ADVANCE TO ALLOW SPACE FOR STACK.
LD SP,HL ; SET UP THE STACK POINTER.


The example works perfectly - thanks very much! :D

daver2
August 30th, 2017, 04:56 AM
You may find this reference quite useful:

http://www.cpm.z80.de/manuals/cpm22-m.pdf

(especially section 5.1 onwards) as it avoids the confusion of different versions of CP/M (e.g. MP/M and 8086 variants) and concentrates on CP/M 2.2 (albeit with 8080 instead of Z80 mnemonics).

It covers where and how the command line is stored (for passing to a transient program) and how the first two file references on the command line are handled by CP/M itself on your behalf. There are also useful examples that you should be able to follow (perhaps with a bit of help).

I notice you are this side of the pond in the UK. Just bear with the moderators for a short while, they will soon let you post without moderation shortly...

Dave

Chuck(G)
August 30th, 2017, 07:51 AM
Hmm... I'm trying the example provided by Chuck(G) above and getting the following error:


A>ASM HELLO.AAZ
CP/M ASSEMBLER - VER 2.0
NO SOURCE FILE PRESENT

A>

This is despite there being an .AAZ file present and correctly named. I've tried writing it in ED and still get the same error (I'm using the AltairZ80 emulator, so I initially tried importing a Notepad file.) Pretty much the same error when I use ZASM80 too, although that indicates line 1 as being the cause of the error (line 1 is just ORG 100H)...? :huh:

You'll note that I said this:


First edit and create a program file called "HELLO.ASM":

No--ASM assumes file extensions of .ASM .HEX and .LST and uses the stuff after the dot to indicate where to find or create files. So HELLO.AAZ says that you'll find HELLO.ASM on drive A, HELLO.HEX will be created on drive A and there will be no listing file created. It's in the manual. :) AFAIK, ASM is the only program that uses this convention.

daver2
August 31st, 2017, 05:42 AM
>>> Z80 assembly is the natural choice for me.

That's fine, I just wanted to confirm what it was that you were trying to achieve...

>>> Z80ASM expects the file to end in .Z80 instead of .ASM.

Yep, I did say that...

>>> ADD doesn't like adding a value to HL, so I loaded STACK into DE first then added DE to HL. (I picked DE at random - let me know if it's likely to break anything!)

My original program that I sketched out on paper had exactly that - but I decided to check with a document on the internet and changed my mind! What you did is exactly what I would have done... You can use any register you like within your own program. It is only necessary to be careful what registers get 'trashed' across a BDOS call (hence the reason I usually save/restore all my registers in use across a BDOS call and only optimise that if I need performance (see an example shortly)).

>>> The macro you've declared is interesting.

Yes, I try to make my programs readable and maintainable. I find using MACROS helps (providing you don't go too mad with them).

What do you want to know next? How to handle the command line? See code excerpt below (again, with a government health warning that I can't try it out).



Add the following definitions:

; This is where CP/M stores the command line for you.
CMDLINE EQU 80H

; This is the BDOS function to display the character in the 'E' register.
C_WRITE EQU 2

After the code to display Hello World - but before we terminate (!) enter the following code:

LD DE,COM1 ; Display the first half of our message.
BDOS WRITESTR

; Display the command line (if present).

LD HL,CMDLINE ; Start of buffer.
LD A,(HL) ; Length of command line.
INC HL ; Move to first real character of command line.
OR A ; Set flags.
JR Z,COMDONE ; No command line to process.

; A command line was entered.

LD B,A ; Get to correct Z80 register for a DJNZ.

COMMORE:

LD E,(HL) ; Pick up the first/next character from the command line.
INC HL ; Bump command line pointer.
PUSH BC ; Save my registers.
PUSH HL ; -- ditto --
BDOS C_WRITE ; Output the character in the 'E' register.
POP HL ; Recover my registers.
POP BC ; -- ditto --
DJNZ COMMORE ; Repeat as necessary.

COMDONE:

LD DE,COM2 ; Display the second half of our message.
BDOS WRITESTR

Finally, place the following strings at the end.

COM1: DB "You entered """,TERM

COM2: DB """ on the command line.",CR,LF,TERM



Next - file handling? Or something else?

Enjoy,

Dave

PS: If you get a few more posts under your belt, the mods will let you run free...

nockieboy
October 19th, 2017, 05:25 AM
That's awesome, thanks Dave!

Apologies for the huge delay replying - I've been busy with sorting out hardware and other improvements to the SBC, but I'm back to CP/M again now and working on trying to write a couple of programs using the framework you've kindly provided.

My SBC runs at either 4 or 8 MHz (software selectable), so it would be nice to have a little COM program I can run to show me what speed the SBC is running at. I store a simple ASCII '4' or '8' at address CFFF to denote whether the system is currently running at 4 or 8 MHz, so it should be simple enough for me to write a program that grabs that character and prints it to the terminal. Or at least so you'd think. :confused:

Here's what I've got so far:



TITLE SHOWSPD

; An attempt at creating a functioning and useful CP/M program.
; This program will show you what speed the SBC is running at by
; retrieving the value at #CFFF.

; DEFINE SOME ASCII CONTROL CHARACTERS.
;
CR EQU 0DH
LF EQU 0AH

; DEFINE THE CP/M TERMINATING CHARACTER FOR BDOS FUNCTION 9 (WRITESTR).
;
TERM EQU '$'

; DEFINE SOME USEFUL CP/M BDOS FUNCTION NUMBERS.
;
TERMCPM EQU 0
C_WRITE EQU 2 ; This is the BDOS function to display the character in the 'E' register.
WRITESTR EQU 9 ; Write the string pointed to by DE until TERM char is reached

; DEFINE MY LOCAL STACK SIZE.
;
STACK EQU 128

; DEFINE A USEFUL MACRO TO 'WRAP' THE CALL TO BDOS.
;
BDOS MACRO FUNC

LD C,FUNC
CALL 5

ENDM

; THIS IS THE MAIN PROGRAM...
;
PROGENT::
; INITIALISE THE LOCAL STACK TO BE
; AT THE END OF THE PROGRAM AS LOADED
; INTO THE TPA.

LD HL,PROGEND ; ADDRESS OF THE END OF PROGRAM.
LD DE,STACK ; LOAD STACK SIZE INTO DE.
ADD HL,DE ; ADVANCE TO ALLOW SPACE FOR STACK.
LD SP,HL ; SET UP THE STACK POINTER.

; ACTUAL PROGRAM CODE GOES HERE
LD DE,RESP1 ; DE POINTS TO THE MESSAGE.
BDOS WRITESTR ; DISPLAY THE MESSAGE.
LD A,(0CFFFH) ; LOAD CLOCK SPEED CHAR INTO A
LD E,A ; MOVE IT INTO E
BDOS C_WRITE ; WRITE THE CHAR
LD DE,RESP2 ; DISPLAY REST OF MESSAGE
BDOS WRITESTR

; BYE BYE...
;
BDOS TERMCPM ; EXIT BACK TO CP/M.

; DATA
RESP1:
DB "System is running at ",TERM

RESP2:
DB " MHz",TERM

PROGEND:

END


Problem is, when I run the program in CP/M all I get is this:

`System is running at MHz`

Any ideas what I'm doing wrong? (The value at #CFFF is either #34 or #38.)

GeoffB17
October 23rd, 2017, 12:59 PM
Hello,

There's a specific book that I've found quite useful, and I see that Amazon UK list second hand copies for about 13.50:

Mastering CP/M by Alan R Miller (Sybex 1983).

Fairly concerned with the CP/M system generally, and organised towards building up a substantial macro library. From what you say, the main problem would be that it's primarily 8080 rather than Z80, but it makes lots of reference to both sets of mnemonics.

Apart from that, the best way I've found to get into assembly is to find the source code for existing progs. There are many such on the web. Find something that does some/part of what you're interested in. Study it, get the hang of what it does, and how it works. Then start making changes/additions to it. Gradually you'll take over the whole prog, and you'll get very familiar with it, as you do the edit/compile/run cycle over and over again. If the prog WAS working, and you change something, and now it's NOT working, then YOU've done something, and it's just the bit you changed, so don't make too many changes at once!!

Geoff

durgadas311
October 23rd, 2017, 01:50 PM
One thought I had, looking at your program, is that 0CFFFH seems sort of low in memory. Is that part of the CCP? I'm assuming this is CP/M 2.2... Maybe run DDT and make sure 0CFFFH contains what you think it does. But if 0CFFFH is part of the CCP, then that will get overwritten by DDT.

While it appears as though no character is being output between RESP1 and RESP2 messages, it could be that some "garbage" (non-printable) character is being output, if 0CFFFH does not contain what you think. That is difficult to prove unless your terminal has a mode where it prints all characters including control/invalid characters. One thing to try is maybe grab (0CFFFH) before making any BDOS calls and save it on your stack, in case this location is getting overwritten by BDOS.

Basically, go back and prove that 0CFFFH does actually contain what you think at the time a program gets executed.

GeoffB17
October 23rd, 2017, 02:34 PM
Further to the above, if you've got the correct data, if it IS a printable character then it may show as such, but that may NOT be a lot of help.

You need an extra process, which will take the HEX char, and convert it into a three digit decimal number, and display that. That number may be more useful anyway? I seem to remember that the Miller book mentioned above shows a macro for doing this.

Geoff

JonB
October 23rd, 2017, 10:39 PM
This routine converts a numeric value in register A to ASCII, and writes it into the buffer pointed to by HL.


;-----------------------------------------------------------------------------
;Number in a to decimal ASCII
;adapted from 16 bit found in z80 Bits to 8 bit by Galandros
; HL = ascii buffer address
; A = value to print into buffer
;
; Call atoasc3 for a three charchter wide field, atoasc2 for a 2 character field, etc
atoasc3:
ld c,-100
call Na1
atoasc2:ld c,-10
call Na1
atoasc1:ld c,-1
Na1: ld b,'0'-1
Na2: inc b
add a,c
jr c,Na2
sub c ;works as add 100/10/1
ld (hl),b
inc hl
ret

So, to call it:



[Work out the speed in Mhz]

ld a,[the speed in mhz] ; get the value we want to print
ld hl,speed ; set the buffer where we want to put it
call atoasc2 ; convert to ASCII and write to buffer
ld de,msg1 ; print the message
BDOS WRITESTR ;

[exit the program]


; Buffers for writing processor speed message
msg1 : DB "System is running at " ; the main body of the message
speed: DB " Mhz$" ; the numeric field plus the tail of the message
; the three spaces at the start are overwritten by the
; atoasc routine

durgadas311
October 24th, 2017, 06:45 AM
@nockieboy, I see nothing wrong with your code, provided that (0CFFFH) actually contains a printable ASCII character at the time the program loads it.

It might be good to know what (BIOS? Boot ROM? ???) is placing the '4' or '8' in 0CFFFH, and where your CP/M image is "org'ed". Is this a 64K system? What is the memory map? In a 64K system, I would expect that 0CFFFH is likely to be below the CCP, and thus a part of CP/M memory that is highly likely to be overwritten.

nockieboy
October 24th, 2017, 09:05 AM
Thanks for the help everyone - unfortunately I'm still posting from behind a 'new user' moderation wall, so my messages are taking a while to get through to the forum. :dontgeti:

So I've added in some code to show the hexadecimal value of the memory location CFFF and low and behold, on first running 'SHOWSPD.COM', it''s showing 30(H) and later on 00(H), so clearly the RAM isn't secure at that location and is getting overwritten, despite it working fine during initial boot of CP/M to be used by the BIOS to work out what baud rate to set the SIO to. Obviously something after that - i.e. during the loading of CP/M itself, is wiping that location.

Using DDT, I've viewed the RAM in that area and it's all zeros. It seems CP/M likes to clear the TPA and set everything to zero? So, for my little CP/M program to work I need to find a safe plot of land in the memory - just one byte is all I need - to store the system clock speed when jumping from the monitor ROM into CP/M. Anyone have any suggestions or can point me to a (detailed) memory map for CP/M that I could use to find a place myself?

nockieboy
October 24th, 2017, 09:25 AM
@nockieboy, I see nothing wrong with your code, provided that (0CFFFH) actually contains a printable ASCII character at the time the program loads it.

It might be good to know what (BIOS? Boot ROM? ???) is placing the '4' or '8' in 0CFFFH, and where your CP/M image is "org'ed". Is this a 64K system? What is the memory map? In a 64K system, I would expect that 0CFFFH is likely to be below the CCP, and thus a part of CP/M memory that is highly likely to be overwritten.

Yes, after a little testing it appears the code is working fine - the memory location is being overwritten.

It's CBIOS that places the value at CFFFH - either a 34H or 38H (so it's an ASCII char 4 or 8 anyway) and I know it works initially as it is correctly setting the SIO serial port's baud rate based on that value. It seems, though, that CP/M overwrites the address a little later when it loads up.

It's a 64K system currently, until I work out a memory management system that will allow me to page in the extra RAM (my SBC has a 128K SRAM.) The CCP starts at D000H, so I chose CFFFH as I was unaware that CP/M seems to 'clear' all the TPA RAM with zeros.

Can I insert a little space between the CCP and BDOS that I could use to safely store values? Or is there somewhere else (perhaps below 0100H?) I could sneak a single-byte value in?

durgadas311
October 24th, 2017, 10:31 AM
I don't believe CP/M actually clears the TPA, but one thought I had was that the CCP very likely places it's stack there. If you have control over the BIOS code, you could have it save this value in the cold start routine, and perhaps tuck it away in the first page of the BIOS. Many implementations of CP/M have placed things after the standard BIOS JMP table, and will use the address at location 0000H+1 to get at them from programs.

Note that CP/M allows for the CCP to be overwritten, and (if the BIOS supports it) allows for the BDOS to also be overwritten (this requires that the BIOS re-load the BDOS on warm boot). Most programs do not overwrite the BDOS since they use it, but overwriting the CCP is very common. Basically, any memory below the BDOS is "fair game" to a program. I'd suggest squirreling away the value from 0CFFFH during cold start of the BIOS. Or else have the CBIOS save it in low memory, somewhere in page 0 (otherwise unused, at least during boot). I have done a similar thing in a CP/M 3 loader/boot program, where I examine the ROM data, extract key values, save them at 0040H, then load and start CP/M. The BIOS then grabs the data from 0040H during cold start.

durgadas311
October 24th, 2017, 10:41 AM
I think the CP/M system guide gives a fairly complete description of what CP/M reserves in page 0. Basically, 005CH to 00FFH is reserved as is most of 0000H-0007H. Other areas may be used by your BIOS/CBIOS for interrupt vectors (the RST vectors are generally considered to be 0000H-003FH). Also note that the Z80 NMI vector at 0066H actually conflicts with CP/M usage, so your BIOS must handle that if it needs NMI.

If you're considering using banked memory, while it is possible to modify CP/M 2.2 for that you may find it easier to go with CP/M 3. Trying to get CP/M 2.2 to operate in a banked memory environment is likely to be very challenging.

Chuck(G)
October 24th, 2017, 11:07 AM
Note that, by default, DDT uses 38h-3ah for its breakpoint vector (RST 7). Some machine-specific versions of DDT may have been modified to use other vectors because 7 is reserved for a different purpose.

nockieboy
October 25th, 2017, 05:51 AM
Yeah, sorry for the confusion - where I'd said that the TPA was all set to zero, it seems I was a little confused and instead of checking the memory on my SBC, I was in fact using DDT in my CP/M emulator (the windows look very similar on my desktop, okay?!:rolleyes:), which I use to compile my *.Z80 assembly into .COM files for transferring to the SBC's CP/M filesystem...!!!! :stupid:

Anyhow - I've inserted an extra byte into the CBIOS assembly which equates to a fixed address that doesn't get overwritten, so now my SHOWSPD.COM program is correctly showing the current system clock speed! :onfire:

So, on to the next problem:

I was originally writing a CP/M program to 'quit' CP/M and return me to the monitor/boot ROM I'd written (a little like BASIC in that it has a direct-mode interpreter but geared towards helping me learn assembly.) Essentially it has to do two things - enable the ROM which takes up the first 16K of RAM and then jump to 0000H to cold-start it. I couldn't get the program to work (it just returned me to the CP/M prompt) so I thought I'd do something a little simpler - the SHOWSPD program above.. :rolleyes:

So, here's my code:


TITLE EXIT

; Exits CP/M and returns to Direct Mode Interpreter

; Define local stack size
STACK EQU 128

; Main Program
PROGENT::
; INITIALISE THE LOCAL STACK TO BE
; AT AN ADDRESS HIGHER THAN ROM (4000H)
; OTHERWISE IT'LL BE OVERWRITTEN WHEN
; THE ROM IS SWITCHED BACK IN
LD HL,06100H ; ADDRESS OF THE STACK
LD DE,STACK ; LOAD STACK SIZE INTO DE.
ADD HL,DE ; ADVANCE TO ALLOW SPACE FOR STACK.
LD SP,HL ; SET UP THE STACK POINTER.

; Copy the reset program to 6000H so it's not overwritten by the ROM
LD HL,PROGSTART ; Start of transition code
LD DE,06000H ; Target address for transition code
LD BC,000FFH ; Size of memory block to copy
LDIR ; Copy it
JP 06000H ; Execute the transition code

PROGSTART:
; Transition to DMI
LD A,000H
OUT (038H),A ; Turn on ROM
JP 00000H ; Cold start
PROGEND:

END

Am I doing anything wrong there? It should enable the ROM and reset the SBC, so it boots back into the Direct Mode Interpreter (ROM). What it actually does is just return to the CP/M prompt, apparently having done nothing...

nockieboy
October 30th, 2017, 01:18 PM
Please ignore my last post - I've sorted the issue since posting the message and it getting through moderation. :D

nockieboy
October 31st, 2017, 08:06 AM
Okay, after some liberal hair-pulling, I've pulled the trigger and decided to post for some more help...

I'm trying to convert a 16-bit hex address, which is in the form of ASCII characters in a string, into a hexadecimal value that equates to that memory address. That's probably clear as mud, so here's an example:

* I read in a stream of ASCII characters from the serial buffer, which e.g. take the form: "0161".
* I want to convert those ASCII characters to the 16-bit hex value 0161h and store it in a memory location.

Here's what I'm doing so far:



; Get MSB
CALL GETFROMCHARB ; Get ASCII MSB MSN into A
LD D,A ; Move it into D
CALL GETFROMCHARB ; Get ASCII MSB LSN
LD E,A ; Otherwise, move it into E
CALL HexToNum ; Convert DE to hex value in A
LD (MEM_TGT+1),A ; Load the MSB into MEM_TGT MSB

; Get LSB
CALL GETFROMCHARB ; Get ASCII LSB MSN
LD D,A ; Move it into D
CALL GETFROMCHARB ; Get ASCII LSB LSN
LD E,A ; Otherwise, move it into E
CALL HexToNum ; Convert DE to hex value in A
LD (MEM_TGT),A ; Load the LSB into MEM_TGT LSB

HexToNum:
LD A,D
CALL Hex1 ; Convert value in D
AND 0Fh ; Clear the upper nibble
RLCA ; Move the value into the upper nibble
RLCA
RLCA
RLCA
LD D,A ; Store the converted value back in D
LD A,E ; Now convert value in E
CALL Hex1
AND 0Fh ; Clear the upper nibble
OR D ; Merge the MSN in D and the LSN in A
RET ; Return with converted value in A

Hex1: SUB 30h ; Drop down to 0-0Fh base
CP 0Ah ; Is it A-F? It'll be over 09h if it is
RET C ; It's 0-9, so return
SUB 07h ; Drop the remainder into A-F range
RET


So, I'm taking two of the ASCII chars and dropping them into DE. Then I'm calling HexToNum to convert DE into a hex value in A. Problem is, HexToNum isn't returning the proper results. i.e. in DE I can have the ASCII chars 01 (30h and 31h), HexToNum spits out 00h, or 61 (36h and 31h) and the result is 66h.

So I've made a mistake writing HexToNum somewhere, but I can't find it - it seems it should work? Can anyone spot the error?

P.S. The values are correct going into HexToNum, but incorrect coming out, so I'm (fairly) sure the problem is in that bit of code..

durgadas311
October 31st, 2017, 09:43 AM
I don't see anything wrong, immediately. Your example is for 2 digits, but the code is setup for 4 digits. But are you certain that GETFROMCHARB is doing what you expect? I'm assuming you are not using original DRI assemblers, but just in case... those assemblers have limitations on the number of characters in a label that are unique. So, for example, if you have any other labels that started with the same N characters, they are all considered the same. Might be good to show your PRN file instead of ASM. Check the PRN for proper addresses, or even opcodes.

Another thought, are you certain that GETFROMCHARB doesn't destroy register D?

I'm also assuming you are not showing the code between the end of "Get LSB" and HexToNum, otherwise you would fall-through and end up under-running the stack.

daver2
October 31st, 2017, 10:57 AM
I agree with durgadas311.

Dave

nockieboy
October 31st, 2017, 12:49 PM
I don't see anything wrong, immediately. Your example is for 2 digits, but the code is setup for 4 digits.

Yes, I'm focusing on the first 2 digits to simplify the example as much as possible to the relevant parts.


But are you certain that GETFROMCHARB is doing what you expect?

Err... Yes, I think so. I've been printing the results of GETFROMCHARB and it's behaving as expected. Here's the code anyway:



GETFROMCHARB:
LD HL,(serBRdPtr) ; Get a character from the
INC HL ; Port B character buffer
LD A,L ; Load read pointer LSB into A
CP (serBBuf+SER_BUFSIZE) & $FF ; Check for end of buffer
JP NZ, noWrapB ; and reset pointer to
LD HL,serBBuf ; start if at end

noWrapB: LD (serBRdPtr),HL ; Update buffer pointer
LD A,(serBBufUsed) ; Load buffer used size
DEC A ; Reduce buffer size as
LD (serBBufUsed),A ; a char has been removed
CP SER_EMPTYSIZE ; Check for buffer underrun
JP NC,retB
LD A,$05
OUT (SIOB_C),A ; Hold RTS LOW as we're
LD A,RTS_LOW ; ready to send more data
OUT (SIOB_C),A ; from the buffer

retB: LD A,(HL)
RET ; Char ready in A



I'm assuming you are not using original DRI assemblers, but just in case... those assemblers have limitations on the number of characters in a label that are unique. So, for example, if you have any other labels that started with the same N characters, they are all considered the same.

No, I'm assembling the code in TASM?


Might be good to show your PRN file instead of ASM. Check the PRN for proper addresses, or even opcodes.




I'm also assuming you are not showing the code between the end of "Get LSB" and HexToNum, otherwise you would fall-through and end up under-running the stack.

Yes, as I mentioned earlier I've cut the example down to the parts that are relevant to the problem. Looks like I've got to do some more debug value printing then. I was convinced the issue was with HexToNum, as the right value is going in to the function but the wrong one is coming out. It looks like the least significant nibble is a copy of the most significant one - i.e. 01 goes in, 00 comes out. 61 goes in, 66 comes out..

durgadas311
October 31st, 2017, 02:07 PM
I'm not familiar with TASM, and didn't use Zilog mnemonics, so I could easily miss something. But I have occasionally seen an assembler forge ahead and make an instruction even though the code was wrong... things like (Intel mnemonics) "MOV A,0" where you meant "MVI A,0"... your eyes convince you the instruction is correct, and the assembler doesn't complain, but if you look closely at the PRN file or the machine code, you'll see it's not what you wanted. I'm pretty sure that exact example is not possible with Zilog mnemonics, but maybe your machine code is not what you intended.

Also, and sorry for not keeping full context in mind, did you say this was a z80 emulator - or is it a true Z80-CPU? Just wondering if it's possible there is a bug in the emulation.

If the input data to the routine is correct, and the output is wrong, then the routine must not be doing what you expect. If the code looks correct, then we need to start looking at more-outrageous possibilities...

durgadas311
October 31st, 2017, 02:20 PM
I ran a quick test, using SID and Intel mnemonics, but it worked for me:

A>sid
CP/M 3 SID - Version 3.0
# ...(enter code)...

#l100
0100 LXI D,3031
0103 CALL 0200
0106 MOV H,A
0107 LXI D,3631
010A CALL 0200
010D MOV L,A
010E SHLD 0180
0111 RST 07
...
#l200
0200 MOV A,D
0201 CALL 0280
0204 ANI 0F
0206 RLC
0207 RLC
0208 RLC
0209 RLC
020A MOV D,A
020B MOV A,E
020C CALL 0280
020F ANI 0F
0211 ORA D
0212 RET
...
#l280
0280 SUI 30
0282 CPI 0A
0284 RC
0285 SUI 07
0287 RET
...
#d180
0180: 43 50 ...
#g100

*0111
#d180
0180: 61 01 ...
#

nockieboy
November 1st, 2017, 02:22 AM
It's running on a real, live Z80 - no emulation going on here unfortunately.

Okay, so I'm looking specifically into the HexToNum function now and it definitely seems the issue is somewhere in there. Here's the PRN for the function (well, it's actually a LST in TASM but I presume this is what you're asking for?):



0591 E9CA ;------------------------------------------------------------------------------------------------
0592 E9CA ; Convert ASCII hex character in DE to hex in A
0593 E9CA ;------------------------------------------------------------------------------------------------
0594 E9CA HexToNum:
0595 E9CA 3E 58 LD A,'X' ; DEBUG
0596 E9CC 4F LD C,A ; DEBUG
0597 E9CD CD F0 EA CALL conout ; DEBUG
0598 E9D0
0599 E9D0 4A LD C,D ; DEBUG
0600 E9D1 CD F0 EA CALL conout ; DEBUG
0601 E9D4
0602 E9D4 7A LD A,D
0603 E9D5 CD FD E9 CALL Hex1 ; Convert value in D
0604 E9D8
0605 E9D8 E6 0F AND 00Fh ; Clear the upper nibble
0606 E9DA 07 RLCA ; Move the value into the upper nibble
0607 E9DB 07 RLCA
0608 E9DC 07 RLCA
0609 E9DD 07 RLCA
0610 E9DE 57 LD D,A ; Store the converted value back in D
0611 E9DF 7B LD A,E ; Now convert value in E
0612 E9E0
0613 E9E0 4B LD C,E ; DEBUG
0614 E9E1 CD F0 EA CALL conout ; DEBUG
0615 E9E4
0616 E9E4 CD FD E9 CALL Hex1
0617 E9E7 E6 0F AND 00Fh ; Clear the upper nibble
0618 E9E9 B2 OR D ; Merge the MSN in D and the LSN in A
0619 E9EA
0620 E9EA 57 LD D,A ; DEBUG
0621 E9EB 3E 3D LD A,'=' ; DEBUG
0622 E9ED 4F LD C,A
0623 E9EE CD F0 EA CALL conout ; DEBUG
0624 E9F1 7A LD A,D ; DEBUG
0625 E9F2 CD AC E9 CALL PHEX ; DEBUG
0626 E9F5 3E 2F LD A,'/' ; DEBUG
0627 E9F7 4F LD C,A
0628 E9F8 CD F0 EA CALL conout ; DEBUG
0629 E9FB 7A LD A,D ; DEBUG
0630 E9FC
0631 E9FC C9 RET ; Return with converted value in A
0632 E9FD
0633 E9FD D6 30 Hex1: SUB 030h ; Drop down to 0-0Fh base
0634 E9FF FE 0A CP 00Ah ; Is it A-F? It'll be over 09h if it is
0635 EA01 D8 RET C ; It's 0-9, so return
0636 EA02 D6 07 SUB 007h ; Drop the remainder into A-F range
0637 EA04 C9 RET


I've left the debug printing lines in so you can see where the following output is coming from - this is what I'm getting back:


X01=00/X61=66/

The first two digits after the X are the two ASCII characters in DE going into the function. The second pair of digits immediately after the = sign is the 8-bit hex output from the function (the value in A) - they should match. :confused:

For completion really, included below is the code for `conout` and `PHEX` and all associated functions called in the process of executing `HexToNum`:



PHEX: AND 0F0h
RRCA
RRCA
RRCA
RRCA
CALL phex1
phex1: AND 00Fh
CP 00Ah
JP C,phex2
SUB 009h
OR 040h
JP phex3
phex2: OR 030h
phex3: LD C,A ; DEBUG
CALL conout ; DEBUG
RET


;------------------------------------------------------------------------------------------------
conout:
PUSH AF ; Store character
LD A,(iobyte)
AND $03
CP $02
JR Z,list2 ; "BAT:" redirect
CP $01
JR NZ,conoutB1

conoutA1: CALL CKSIOA ; See if SIO channel B is finished transmitting
JR Z,conoutA1 ; Loop until SIO flag signals ready
LD A,C
OUT (SIOA_D),A ; OUTput the character
POP AF ; RETrieve character
RET

conoutB1: CALL CKSIOB ; See if SIO channel B is finished transmitting
JR Z,conoutB1 ; Loop until SIO flag signals ready
LD A,C
OUT (SIOB_D),A ; OUTput the character
POP AF ; RETrieve character
RET

;------------------------------------------------------------------------------------------------
CKSIOA
SUB A
OUT (SIOA_C),A
IN A,(SIOA_C) ; Status byte D2=TX Buff Empty, D0=RX char ready
RRCA ; Rotates RX status into Carry Flag,
BIT 1,A ; Set Zero flag if still transmitting character
RET

CKSIOB
SUB A
OUT (SIOB_C),A
IN A,(SIOB_C) ; Status byte D2=TX Buff Empty, D0=RX char ready
RRCA ; Rotates RX status into Carry Flag,
BIT 1,A ; Set Zero flag if still transmitting character
RET

daver2
November 3rd, 2017, 01:47 AM
Your PHEX subroutine is corrupting the value in A.

On entry, A=61 (second instance of test).

AND F0 gives an A value of 60
4*RRCA gives an A value of 06

phex1: AND 0F gives an A value of 06

Output an ASCII value of '6' should be printed.

RET (with a value of 36 in A) back to phex1:

AND 0F gives an a value of 06

Go through code again will print another '6'.

My conclusion is that your conversion code works OK - but the means of displaying it doesn't.

Dave

nockieboy
November 3rd, 2017, 01:56 PM
Your PHEX subroutine is corrupting the value in A.

On entry, A=61 (second instance of test).

AND F0 gives an A value of 60
4*RRCA gives an A value of 06

phex1: AND 0F gives an A value of 06

Output an ASCII value of '6' should be printed.

RET (with a value of 36 in A) back to phex1:

AND 0F gives an a value of 06

Go through code again will print another '6'.

My conclusion is that your conversion code works OK - but the means of displaying it doesn't.

Dave

Ah yes, don't know how I missed that (well, probably because there's another bug causing problems as well.) Fixed the error in PHEX now, thanks Dave. :thumbsup: Just got to find the other issue now... :(

daver2
November 3rd, 2017, 03:23 PM
Can you re-post the code and what the current problem is again (with a sample) and I will have a look on Saturday.

Dave

nockieboy
November 3rd, 2017, 04:26 PM
The other problem is one that arose during testing for the PHEX problem. I suspect the problem is actually to do with the code running in the Atmega328 support module that connects to my Z80 via serial port B. I'm finding getting the 328 software to behave consistently to be a bit of an issue (seems to be a quirk of the Arduino IDE or its implementation of C in general.) When requested by the Z80, it should send the current date and time back along with a memory address over the serial interface (hence the previous task of decoding the ASCII memory address.) Problem is, it doesn't work when the Z80 requests the data - it seems to be receiving an almost empty string, but if I manually send a data string from the 328, the Z80 receives and decodes it just fine.

So it's not really a CP/M- or Z80-related issue at the moment (unless I find out otherwise later) but I think it's going to be a tricky one to pin down. Thanks for offering to help though. :)

nockieboy
November 4th, 2017, 02:32 AM
Just a thought - I've got a CP/M program that sends the request to the '328 to get the date and time. When it's finished, it should wait in a loop that checks the memory address for that data to be written. When it is, it will continue and print it to the console, or do whatever else I can think up.

EDIT: Long post reduced to a couple of paragraphs after a quick test!

Okay, it seems that the program calling TERMCPM is disrupting the serial interrupt service and causing it to misread the data stream. I've added a delay to the '328 software so that it waits a second before returning the data string to the Z80, which gives CP/M enough time to reset. This shouldn't be an issue once I've got the CP/M program waiting for the data to be returned before it does something with it and I'll be able to remove the delay. :D

nockieboy
November 4th, 2017, 03:32 AM
Okay - any reason this isn't writing data into memory?



WRITEDATA:
; Get the address from the next 4 chars (which will all be ASCII codes and need converting)
; Get MSB
CALL GETFROMCHARB ; Get ASCII MSB MSN into A
CP EOL ; Is it an End Of Line char?
JP Z,wd_exit ; Quit if it is the end of line
LD (CHR_BUF_D),A ; Move it into D
CALL GETFROMCHARB ; Get ASCII MSB LSN
CP EOL ; Is it an End Of Line char?
JP Z,wd_exit ; Quit if it is the end of line
LD (CHR_BUF_E),A ; Move it into E
CALL HexToNum ; Convert DE to hex value in A
LD (MEM_TGT+1),A ; Load the MSB into MEM_TGT MSB
; Get LSB
CALL GETFROMCHARB ; Get ASCII LSB MSN
CP EOL ; Is it an End Of Line char?
JP Z,wd_exit ; Quit if it is the end of line
LD (CHR_BUF_D),A ; Move it into D
CALL GETFROMCHARB ; Get ASCII LSB LSN
CP EOL ; Is it an End Of Line char?
JP Z,wd_exit ; Quit if it is the end of line
LD (CHR_BUF_E),A ; Move it into E
CALL HexToNum ; Convert DE to hex value in A
LD (MEM_TGT),A ; Load the LSB into MEM_TGT LSB
; We now have a target memory address decoded and stored in MEM_TGT
LD HL,(MEM_TGT) ; Load the target memory address
wd_write:
CALL GETFROMCHARB ; Get next character
; Load the char into the targeted memory location
LD (HL),A ; Load the char into the target memory location
CP EOL ; Is it End Of Line char?
JP Z,wd_exit ; Yes - exit
INC HL ; Move to next target memory location
; Check we're not out of data & loop if not
LD A,(serBBufUsed) ; Check to make sure
CP 0 ; the buffer isn't empty
JP NZ,wd_write ; Loop again if no match
wd_exit: CALL wd_chk ; DEBUG
RET ; Done

wd_chk: LD HL,MEM_TGT ; Set HL to location of data
wd_clp: LD A,(HL) ; Load char into A
CP EOL ; End Of Line char?
RET Z ; Yes - return
LD C,A
CALL conout ; No? Print it
INC HL
JP wd_clp


wd_write SHOULD be writing an ASCII string into memory at the location pointed to by MEM_TGT. There is definitely an ASCII string being received from the serial port. Have I messed up with the little endian/big endian format of the address in MEM_TGT, or is something else wrong?

I added wd_chk to print out the contents of memory at the location pointed to by MEM_TGT. It should be an ASCII string of characters as received from the serial port, but I'm just getting gobbledygook. As stated previously, the stream from GETFROMCHARB is valid.

daver2
November 4th, 2017, 04:50 AM
I was thinking about the contents of the reduced long post whilst I did another job. I have a question though...

It seems as though the buffer that is expecting the serial characters is located at 0158 (or some such address from memory). This is located within the area of the program that is calling TERMCPM (if memory serves me correctly). Am I assuming correctly that this buffer will receive characters under interrupt from the serial port?

If so, you have a problem. Once the program calls TERMCPM, then it gives up all rights to memory that it had. The memory becomes the 'property' of CP/M again.

In fact, CP/M will use the memory from 0100 upwards itself, thus corrupting the buffer anyhow - or vice versa.

Is my understanding of what you are trying to do is correct?

Dave

nockieboy
November 4th, 2017, 05:12 AM
I was thinking about the contents of the reduced long post whilst I did another job. I have a question though...

It seems as though the buffer that is expecting the serial characters is located at 0158 (or some such address from memory). This is located within the area of the program that is calling TERMCPM (if memory serves me correctly). Am I assuming correctly that this buffer will receive characters under interrupt from the serial port?

If so, you have a problem. Once the program calls TERMCPM, then it gives up all rights to memory that it had. The memory becomes the 'property' of CP/M again.

In fact, CP/M will use the memory from 0100 upwards itself, thus corrupting the buffer anyhow - or vice versa.

Is my understanding of what you are trying to do is correct?

Dave

What should be happening is when DATETIME is run, it identifies the address in memory of one of its variables (initialised in the assembly as "00/00/00 00:00:00 xxx" as ASCII chars) and sends that address, along with the data request, to the '328. It should then loop, checking the second byte of that variable (essentially what is the second digit of the day field), until that memory location is changed to a value other than ASCII '0'. This should be done by the serial interrupt routine when it receives the data back from the '328. When it detects a change in that memory location, it can go on to do whatever it wants with it - or quit back to the console.

At the moment, it does nothing but quit back to the console, but the code above is in the CBIOS serial interrupt service - so as soon as it has written the data to the memory location, wd_chk then reads it back.

daver2
November 4th, 2017, 06:24 AM
Shouldn't the "LD HL,MEM_TGT" be "LD HL,(MEM_TGT)"?

The former will load HL with the address of MEM_TGT, the latter will load the address pointed to by MEM_TGT into HL.

Dave

nockieboy
November 4th, 2017, 07:25 AM
Shouldn't the "LD HL,MEM_TGT" be "LD HL,(MEM_TGT)"?

The former will load HL with the address of MEM_TGT, the latter will load the address pointed to by MEM_TGT into HL.

Dave

MEM_TGT is a 2-byte location that holds the address that wd_write uses to write the data to. Its built up from the ASCII chars in the serial stream earlier in WRITEDATA and is set aside as a safe memory location in CBIOS. So wd_write needs to read its contents, rather than address it directly, whilst it writes the data stream. Have I done it wrong then?

EDIT: Ah yes, I see what you mean. Typo! When I'm back I'll change that and see what happens. :)

nockieboy
November 4th, 2017, 08:58 AM
Okay, that's fixed the wrong memory address being shown by wd_chk, but the output I'm getting now is this:


00-00-0000-00:00:00-xxx

That's the unchanged value of the memory in DATETIME when it's running... it should, however, be something like this:


04/11/2017 16:57:28 Sat

So it appears wd_write isn't writing anything - or is writing to the wrong memory location?

daver2
November 4th, 2017, 10:16 AM
Just trying to identify some potential culprits...

If the stream of characters coming in gets exhausted after the address has been processed - but before any characters of the date/time comes in - then the line "LD A,(SerBBufUsed)" will return 0 and the reading loop will terminate.

The writing loop will then output the default string...

Dave

nockieboy
November 5th, 2017, 03:37 AM
Okay, I feel like I'm in the Twilight Zone with this bug... It SHOULD be writing the date time to memory, but it isn't... Here's the relevant code:



LD HL,(MEM_TGT) ; Load the target memory address
wd_write:
CALL GETFROMCHARB ; Get next character
; Load the char into the targeted memory location

LD C,A ; DEBUG PRINT BUFFER CHAR TO BE WRITTEN
CALL conout

LD (HL),A ; Load the char into the target memory location
CP EOL ; Is it End Of Line char?
JP Z,wd_exit ; Yes - exit
INC HL ; Move to next target memory location
; Check we're not out of data & loop if not
LD A,(serBBufUsed) ; Check to make sure
CP 0 ; the buffer isn't empty
JP NZ,wd_write ; Loop again if no match

LD A,CR ; DEBUG
LD C,A
CALL conout
LD A,LF
LD C,A
CALL conout

wd_exit: CALL wd_chk ; DEBUG
RET ; Done

wd_chk: LD HL,(MEM_TGT) ; Set HL to location of data
wd_clp: LD A,(HL) ; Load char into A
CP EOL ; End Of Line char?
RET Z ; Yes - return
LD C,A
CALL conout ; No? Print it
INC HL
JP wd_clp


Okay, wd_write takes the data stream from the serial buffer and writes it to the address pointed to by MEM_TGT. I know that the correct data is coming from the serial port due to the debug printing I'm doing after GETFROMCHARB. The data is written with a LD (HL),A, then checks are made for end-of-line chars and buffer under-runs before looping.

Once the string is complete, wd_chk is called to print the contents of the memory pointed to by MEM_TGT (which should be the newly-written data), but all I'm getting is the default (unchanged) data there. Here's the console output:


05/11/2017 13:14:01 Sun#x0-00-0000-00:00:00-xxx

The first half (left side of the #) is the data being read from the serial buffer. All good. The right side of the # shows the output from wd_chk - the default value when DATETIME first loads.

Just to clarify what's going on here, DATETIME is being run as a CP/M COM file. It sends a request via the serial port to get the date and time. The date and time is returned via the serial interface to the serial buffer, where the interrupt service routine (in CBIOS) SHOULD write the data to memory indicated by DATETIME (DT) when it makes the initial request ('DT' is a DB value in DATETIME program space, initialised in the assembly as "x0-00-0000-00:00:00-xxx" but only read by DATETIME, never written to.)

DATETIME doesn't call TERMCPM now - it waits in a loop, checking the 1st value in DT and looping all the time it reads an 'x'. When the service routine successfully writes data to that location, it will always be a 0-3 so the 'x' will be overwritten and DATETIME can progress on to the next steps. It is currently stuck in that loop as the default value for 'DT' is never overwritten, as borne out by wd_chk when it prints the memory at that location after trying to write the data to it.

So:

* wd_write is receiving the correct data
* wd_chk is reading the correct memory location (MEM_TGT, verified as it's displaying the default value set up by DATETIME when it runs)
* wd_write is using the same memory location to write to (MEM_TGT)

So what on earth is going on?

daver2
November 5th, 2017, 06:15 AM
The difficult bugs are always the best! Think of it all as a learning opportunity...

Point 1: The output in the second code block was not produced by the source code in the first code block. I can't see any display of a '#' character - and a CR/LF should have been output between reading and storing the stream of characters and re-displaying them again.

Point 2: Are you sure that the address pointed to by MEM_TGT is in RAM and not ROM? A simple check (after storing the character with a "LD (HL),A") would be to perform a compare (HL) and see if the two compare OK. We know for sure then that the data was actually stored correctly.

Dave

nockieboy
November 5th, 2017, 08:10 AM
The difficult bugs are always the best! Think of it all as a learning opportunity...

Point 1: The output in the second code block was not produced by the source code in the first code block. I can't see any display of a '#' character - and a CR/LF should have been output between reading and storing the stream of characters and re-displaying them again.

Point 2: Are you sure that the address pointed to by MEM_TGT is in RAM and not ROM? A simple check (after storing the character with a "LD (HL),A") would be to perform a compare (HL) and see if the two compare OK. We know for sure then that the data was actually stored correctly.

Dave

Hi Dave!

1: The hash symbol is the EOL char at the end of the serial stream, so there's no explicit code printing it to separate the two outputs. You're right regarding the CRLF section though... That's a head scratcher. I can assure though that is the code that produces that output.

2: When CPM is running, there is no ROM available to the Z80. The address being written to is low in the TPA, but it is where the DATETIME program is being loaded...

From what you've suggested, the lack of CRLF in the output is a bit of a problem. I'll try comparing HL and memory immediately after the LD instruction and see how that comes out later!

Thanks! :)

daver2
November 5th, 2017, 08:49 AM
Ah...

So, if the '#' is your EOL then I can see why the CR/LF is not output - because your code checks for EOL and branches directly to label wd_exit when it is seen (thus bypassing the code that checks for end of buffer and displays the CR/LF).

Dave

daver2
November 5th, 2017, 08:57 AM
Looking back through a previous post (number 33) - GETFROMCHARB 'zaps' HL - so you have just loaded HL from (MEM_TGT) called GETFROMCHARB (which has promptly destroyed it)...

Dave

nockieboy
November 5th, 2017, 10:07 AM
Ah...

So, if the '#' is your EOL then I can see why the CR/LF is not output - because your code checks for EOL and branches directly to label wd_exit when it is seen (thus bypassing the code that checks for end of buffer and displays the CR/LF).

Dave

Your logic is impeccable, sir. That's what comes from replying on my phone!


Looking back through a previous post (number 33) - GETFROMCHARB 'zaps' HL - so you have just loaded HL from (MEM_TGT) called GETFROMCHARB (which has promptly destroyed it)...

Dave

Ahhhh that's it!! Well spotted! :bigups:

One of the things I'm learning about assembly (not as quickly as I'd hope!) is that you really have to dot your i's and cross your t's. Keeping track of registers is something I've never had to do in any of the high-level languages I'm used to - even 'simple' things like loops require forethought and planning if you're trying to juggle a number of values and call functions that alter registers.. :wow:

Thanks Dave. :)

daver2
November 5th, 2017, 10:27 AM
Yep, assembly language programming sorts the men out from the boys!

Although, from what I've seen, you have picked up Z80 assembly language pretty quickly!

There are a few ways to write 'bullet proof' assembly code:

1. In a subroutine save every register apart from the ones that are designed to return a value back to the caller.

2. The subroutine caller preserved the registers in use prior to calling any subroutine.

I (personally) prefer option 2 - as it tends to be less wasteful of CPU power (for example, if your code is making use of HL and C at the point at which you call a subroutine, then PUSH HL; PUSH BC before the subroutine and pop them on return). This gives the caller chance to manipulate the subroutine's returned parameters before popping its own register from the stack. Either way, CPU cycles are involved.

If the code you are writing needs to be fast (optimised) then loads of PUSH/POP instructions are not required. In this case, careful design of the use of CPU registers are called for.

You can also create a 'stack frame' (using IX or IY) to create some local storage on the stack and use that.

Both solutions permit you to write recursive code (providing you don't use global variables of course).

Dave

nockieboy
November 5th, 2017, 12:57 PM
Awesome - thanks for that Dave. What are stack frames? Though I'm a little reticent about using the stack too much - don't want to run out of space! ;)

Here's the output from my DATETIME program now:



A>datetime
05/11/2017 22:51:11 Sun
A>datetime /d
05/11/2017
A>datetime /w
Sun
A>datetime /t
22:51:27
A>


(Thanks to your earlier comment on page 2 regarding accessing command line parameters!) Though the RTC is still on British Summer Time - next thing on the to-do list is make it so I can change the date/time on the RTC from CP/M.

File handling is the next big thing I suppose?

daver2
November 7th, 2017, 12:27 PM
I am a little busy this week - but hopefully I will find some time at the weekend.

By 'run out of stack space' - if I remember correctly, CP/M only allocates an 8-level stack on entry to a transient program (e.g. your datetime.com program) and it uses one of those stack elements for a return address back to CP/M. So, if you use more than 7 words of stack - you run out...

I will give you a little write up on stack frames and a small 'starter for 10' on how to sequentially read a single filename passed to a transient program. You will probably be able to pick up the rest from there yourself.

Nice going with datetime though.

Don't forget - once your .COM program terminates back to CP/M, all memory used by it has to be relinquished (especially if it is being used by interrupt handlers in your CBIOS). This is because CP/M will re-use the memory again for it's own purposes or for the next transient program.

Dave

durgadas311
November 7th, 2017, 03:12 PM
The CP/M 2.2 CCP, which is what loads and "calls" your program, is running on an 8-level stack when it calls 0100H (your program). Programs have the choice of returning directly to CCP or executing "JMP 0" to "warm boot" CP/M. Both cases result in going back to the "A>" prompt... most of the time. If you avoid using much stack (either by restraint or by saving the CCP stack and restoring it), *AND* you do not use the memory occupied by the CCP (it is directly below the BDOS), then you can return directly to the CCP. Otherwise, you must do "JMP 0". Note, you can choose to keep the CCP stack pointer and simply overrun the 8 levels - which overwrites parts of the CCP code - as long as you JMP 0 when the program is finished. Of course, you must balance the stack usage (growing downwards) with memory usage (typically growing upwards). These old CPUs provide no protection for stack overruns or other such "mistakes".

Chuck(G)
November 7th, 2017, 03:20 PM
Generally, you're safe if you take the address in locations 6 and 7 and subtract 803h from it for the top of your stack. The length of CCP is pretty much hard-coded as 800h in 8-bit CP/M.

Be aware that DDT will give the debugged program a stack address of 0100h by default (00feh-00ffh) will be preset to 0000. So things that may run well as standalone programs may fail under DDT.

nockieboy
November 8th, 2017, 08:39 AM
Marvellous, thanks guys - I must say, you've all been very helpful to a novice CP/M user. :) I'm taking a software break for a few days whilst I design and build a memory management unit for my computer - I want to make use of the extra 64KB SRAM I've got sitting around in the 128KB chip. ;)

Speaking of - I'm looking to break the RAM up into 16KB pages and use the third block (32-48K area - 8000h-BFFFh) as the 'swappable' area of RAM. I'm hoping this won't cause any problems with CP/M as everything (CBIOS etc) loads above D000, or thereabouts? I could make the second block swappable too (16-32K), but when CP/M isn't running, the computer runs from the ROM which uses 4000-4200 as a scratchpad for command line buffers and all sorts.

TL;DR: I'm hoping it won't cause any problems with CP/M 2.2 (or CP/M 3 which I'll upgrade to once this is done) if I create a swappable RAM bank at 8000-BFFFh?

daver2
November 8th, 2017, 08:48 AM
CP/M 3 supports bank switched RAM. The bank switching is used from 0000 upwards, with the higher locations as non-switched 'common' memory.

This sounds the opposite of what you are planning to do. Can I suggest you have a quick read of the CP/M 3 manual before committing your design to real hardware?

I may be looking at doing a port of CP/M 3 myself shortly. More details to follow when I know more myself!

Dave

nockieboy
November 8th, 2017, 08:54 AM
CP/M 3 supports bank switched RAM. The bank switching is used from 0000 upwards, with the higher locations as non-switched 'common' memory.

This sounds the opposite of what you are planning to do. Can I suggest you have a quick read of the CP/M 3 manual before committing your design to real hardware?

I may be looking at doing a port of CP/M 3 myself shortly. More details to follow when I know more myself!

Dave

Ah okay, no problem. I'm happy to make the whole memory space switchable (I'll just have to be careful what banks I allow to be swapped when using the ROM), so it's no issue and I'd rather it was fully compatible with CP/M 3 than not. :)

Chuck(G)
November 8th, 2017, 09:18 AM
Basically, if you're designing your own hardware, you can implement anything you'd like, provided that you have some sort of way to restore the native CP/M environment. In other words, CP/M isn't doing anything between calls. You can swap the entire 64K if you want, as long as you restore the CP/M code and system areas before you make a CP/M request (that includes 'warm boot" calls).

The exception to this is where your hardware implements asynchronous interrupts that could hit at *any* time.

durgadas311
November 8th, 2017, 11:45 AM
If you're planning to support CP/M 3 and you have more than 2 banks (>128K), you may want to support a direct bank-to-bank copy. The ideal would be to have a DMA controller to do it, but just being able to direct-copy from common memory with an LDIR instruction will allow more of CP/M 3 buffers to be in other banks, which allows more buffers. It seems difficult to get REBDOS and RESBIOS much smaller than 8K, so the optimal common memory size is probably 8K. This gives 56K banks for the system and buffers. Note that TPA size is based on RESBDOS+RESBIOS size and is not directly affected by common memory size.

I haven't seen your hardware design yet, but if you get to that point you should be able to get plenty of help on this forum. Usually, the ROM enable/disable is separate from (orthogonal to) banked memory select, but it can also be integrated (the Magnolia Microsystems 128K add-on memory for H89 did that, "bank A" had ROM mapped-in, all other bank select patterns (B-H) had RAM instead of the ROM).

nockieboy
November 9th, 2017, 01:24 PM
If you're planning to support CP/M 3 and you have more than 2 banks (>128K), you may want to support a direct bank-to-bank copy. The ideal would be to have a DMA controller to do it, but just being able to direct-copy from common memory with an LDIR instruction will allow more of CP/M 3 buffers to be in other banks, which allows more buffers. It seems difficult to get REBDOS and RESBIOS much smaller than 8K, so the optimal common memory size is probably 8K. This gives 56K banks for the system and buffers. Note that TPA size is based on RESBDOS+RESBIOS size and is not directly affected by common memory size.

I haven't seen your hardware design yet, but if you get to that point you should be able to get plenty of help on this forum. Usually, the ROM enable/disable is separate from (orthogonal to) banked memory select, but it can also be integrated (the Magnolia Microsystems 128K add-on memory for H89 did that, "bank A" had ROM mapped-in, all other bank select patterns (B-H) had RAM instead of the ROM).

Okay, well, this is what I'm designing for with regard to the memory map - remember at the moment I just have a 128KB SRAM, I doubt I'm going to want (or need) to increase the size of it (I know there are systems out there that can address up to 1MB of memory, but whilst that would be educational, it would be total overkill for my little hobby system.)

https://preview.ibb.co/dBOfYG/Untitled.png

Based on what Dave said in a previous post and what I've subsequently read from the CP/M 3 System Guide, I'm looking to make the entire lower 48KB of the Z80's memory space switchable. Ideally, I want to make it so that I can switch ANY area of addressable memory space with ANY 16KB bank in the upper 64KB, but I'm no electronics expert :confused: and it's starting to look as though that might be prohibitively complicated. If I could find a schematic online where it's already been done I'd be fine, but I'm struggling to design it with limited help from the electronics community who (understandably) don't want to just put the answer out there or design it for me. The 'minimum viable product' version would be to straight-swap the lower 48KB of the lower 64KB with the upper 64KB (preserving area 3 as that's where CP/M hangs out) and I might have to settle for that as I can do that myself, but we'll have to wait and see, I'm chipping away at the problem.

For fear of turning this CP/M-related forum into an electronics one, here's a schematic of the design I arrived at before I realised CP/M requires the entire lower 48K to be swappable. (The schematic makes area 2 - 32-48K - swappable with any of 4 banks in the upper 64KB.)

https://preview.ibb.co/c0HF2b/Untitled.png (https://ibb.co/eStYoG)

Chuck(G)
November 9th, 2017, 02:12 PM
One of the designs that I did back in the day was to use a small RAM (bipolar at the time; there just weren't any fast NMOS SRAMS) to decode the upper bits of the address. (This was not a unique scheme). Since the RAM was being addressed with the upper 6 bits (64 bytes) of the CPU address bus, it extended the address space by 3 bits, which allowed mapping any 1K page from the 512KB memory space into the CPU address range.

The bankswitching demanded by CP/M 3 (and MP/M) is pretty simple. Their example in the system customization manual uses 16KB pages.

durgadas311
November 9th, 2017, 04:08 PM
As Chuck brings up, for MP/M you do want a more flexible memory mapping, because MP/M is capable of running a different program in each page. Of course, if a program is running in a page (or set of pages), that page must always be mapped at the same processor address (until the program ends). But for CP/M 3 you only need to (must always) map a complete bank - starting at 0000H. For the basic CP/M 3 case, it's all about the common memory size, and page size doesn't really matter (except that common memory size is typically required to be a multiple of the page size). Common memory size dictates bank size, which drives how much buffer space you have.

Looking at your schematic, I think about what to do with 128K of ROM. One thought was to put static parts of CP/M in ROM and then make things much faster. BDOS and CCP, and possibly some COM files (i.e. a ROMDISK), could be put there. But that's a whole 'nother level of complexity. If it's an EEPROM or flash, you could put the BIOS there also - as long as the procedure to replace it with a new version is convenient. It all depends on how much you want to do. If you only want to run CP/M 2.2, then you can have a 64K RAMDISK and (up to?) 120K ROMDISK. For that, you'd want a different style of memory mapping. The sky (and 128K) is the limit.

nockieboy
November 26th, 2017, 12:23 PM
Okay, back after a bit of an electronics break. My SBC now has a full memory management unit, able to map any of 16 x 16KB banks from the 256K memory space (128K RAM and 128K ROM) to any of the four 16KB pages in the Z80's memory space.

I'm hoping this is flexible enough to think about implementing CP/M+ (or CP/M 3 or whatever it's called)?

A couple of questions have arisen whilst I was putting the MMU together. How does CP/M report the total available memory? My version of CP/M 2.2 doesn't show any information about free memory on boot up, but it would be nice to have some way of reporting the full memory size (including the extra banks outside of the base 64K of addressable memory). I found a program called SURVEY.COM as part of the SIMH Altair emulator, which conveniently reported the memory makeup of my 64K system with a nice (almost graphical) display of TPA/CBIOS/CPM memory status. Just wondered if there were any hints or tips on anything that handles banked memory systems as well?

I guess I need to set to reading the CP/M 3 System Guide next to start working out what alterations I need to make to get it working on my particular SBC and memory setup.

EDIT: Oh! Nearly forgot! How do I convert hexadecimal to decimal? I found a convoluted way of multiplying each digit by 16 to the power of its position in the number and adding the results together, just wondered if anyone already had some code that does this in Z80 assembly?

gertk
November 27th, 2017, 02:46 AM
Have a look here:

http://map.grauw.nl/sources/external/z80bits.html

chapter 5.1

nockieboy
November 27th, 2017, 02:53 AM
Thanks gertk. :)

MikeBooth
November 28th, 2017, 06:31 AM
I'll take a look in my closet. I may have some old program code there. I wrote a lot of CP/M code years ago, on a TRS-80, to make chips for a embedded Z-80 application. I've also got the manuals I used.

I'll get back to you in a day or two.

Mike Booth
Clifton, VA

nockieboy
November 29th, 2017, 08:33 AM
Thanks MikeBooth (though I've sorted the hexadecimal->decimal problem now, would be happy to see some more routines I can learn from!) :) I've now moved on to looking at CP/M 3 in more detail to get it running on my SBC.