Assemblers for the ZX80

Any discussions related to the creation of new hardware or software for the ZX80 or ZX81
User avatar
1024MAK
Posts: 5102
Joined: Mon Sep 26, 2011 10:56 am
Location: Looking forward to summer in Somerset, UK...

Re: Assemblers for the ZX80

Post by 1024MAK »

Machine code in a numerical array on a ZX80… (see this post)
TMD2003 wrote: Thu Jul 08, 2021 12:30 am So I tried the version above, which meant finding out where it was supposed to be stored; I knew the program was 102 bytes, so after DIM O(51), the value I needed was 16575. That's fine, and I tried the routine to populate the right addresses with the alphabet. However, I'd noticed that if I changed that +2 in line 2 to +28, to account for the 26 bytes of the alphabet characters, the new value of A was 16602... when, surely, it should be 16601.

I tried my re-re-assembled program - putting the LD A,(HL) back into it seeing as there was no need to avoid it now - and entered the 127 bytes into the program above, then replaced it with my BASIC listing to test everything was OK...

...and whether I tried RANDOMISE USR (I+26) or (16601)... black screen.

This is doing my head in.
The PEEKs to addresses 16392 & 16393 are the system variable for VARS. This system variable is the pointer to where the start of the variables area storage is. Because the variables are stored AFTER the BASIC program, the start of the variables area will move as program lines are added, removed or edited (if the edit results in the length changing). Hence the need for the running program to get the value. This also means that only relative addressing can be used if the length of the BASIC program is likely to change. The same applies if the code needs to store data in RAM, then it is better off somewhere else. “Safe” numbers can be stored in a REM line. If the length of the BASIC program will not change, then these restrictions don’t apply.

If the first variable used (after a CLEAR or RUN) is a numerical array, then this will be at the start of the variables area, and the start won’t move while the program is running.

For a numerical array, the +2 in the program that I posted as an example, is to skip past the single letter array name (one byte) and the number total of elements (one byte). Each element is two bytes long. Hence why the total number of bytes is divided by 2.

See Appendix 2, section 2(a) (normally pages 106 to 108) in the ZX80 manual.

So, when using a numerical array, the machine code should always start at VARS +2.

Mark
ZX81 Variations
ZX81 Chip Pin-outs
ZX81 Video Transistor Buffer Amp

:!: Standby alert :!:
There are four lights!
Step up to red alert. Sir, are you absolutely sure? It does mean changing the bulb :!:
Looking forward to summer later in the year.
User avatar
TMD2003
Posts: 149
Joined: Sun Oct 11, 2020 5:39 pm

Re: Assemblers for the ZX80

Post by TMD2003 »

1024MAK wrote: Thu Jul 08, 2021 1:38 am The PEEKs to addresses 16392 & 16393 are the system variable for VARS. This system variable is the pointer to where the start of the variables area storage is. Because the variables are stored AFTER the BASIC program, the start of the variables area will move as program lines are added, removed or edited (if the edit results in the length changing). Hence the need for the running program to get the value. This also means that only relative addressing can be used if the length of the BASIC program is likely to change. The same applies if the code needs to store data in RAM, then it is better off somewhere else. “Safe” numbers can be stored in a REM line. If the length of the BASIC program will not change, then these restrictions don’t apply.

If the first variable used (after a CLEAR or RUN) is a numerical array, then this will be at the start of the variables area, and the start won’t move while the program is running.

For a numerical array, the +2 in the program that I posted as an example, is to skip past the single letter array name (one byte) and the number total of elements (one byte). Each element is two bytes long. Hence why the total number of bytes is divided by 2.
...
So, when using a numerical array, the machine code should always start at VARS +2.
It was getting late at night when I tried that last time so I was probably not thinking totally straight.

Trying to change the line with the PEEK could have been mistake even though it looked like what I was doing was right - if the first byte of the code is after 26 bytes of values that are going to be rearranged, and the first value of these is POKEd to VARS+2, then surely USR (VARS+28) will execute it. What I saw from the original PEEK (VARS+2) was that it reported an address 16575, but when I changed it to add 28 to VARS instead of 2, suddenly the value was 16602... when, if VARS+2=16575, VARS+28=16601.

I think my next experiment may be to work out the complete list of codes that can't be stored in a REM. It might take a while, but I'll get there...


EDIT: ...and I did get there. The entire block of unused character codes from 64 to 127 is either not recommended (64, for instance, turned a character of the REM statement into " 7196 " - spaces included) while others outright crashed the computer. All the other 192 codes are trouble-free, but 64-127 is a big block of all the LD instructions that transfer between one register and another, or (HL). Even before we consider all the CBs and EDs, I conclude it is totally impractical to store machine code in a REM statement!

The DIM version it will have to be, then...
Spectribution: Dr. Jim's Sinclair computing pages.
Features my own programs, modified type-ins, RZXs, character sets & UDGs, and QL type-ins... so far!
User avatar
TMD2003
Posts: 149
Joined: Sun Oct 11, 2020 5:39 pm

Re: Assemblers for the ZX80

Post by TMD2003 »

Last night, I had the sudden urge to recreate a decimal to hex/bin converter that I'd made during my initial forays into Spectrum machine code, and it took no time at all to make a one-byte and two-byte version. The one-byte version is simple enough: you input your integer (up to 255), which is poked into an address and (as we know) stored in binary. The machine code will check each bit of the binary and store either a 0 or 1 in a sequence of 8 addresses which can then be PEEKed in BASIC. Then, to generate the hex code, both four-bit blocks of the initial binary value are analysed - AND 240 followed by four right shifts for one character produces the first value, AND 15 produced the second - then these are both adjusted to turn them into the character codes for 0-9 and A-F.

Seeing that it only took 114 bytes, I then turned to a ZX81 version - which would be shorter as it didn't need a subroutine to add different values in the hex conversion depending on whether or not we're dealing with a number or a letter. The total was 104 bytes - this will work on a 1K ZX81, won't it? Sure enough, it will.

Code: Select all

; One-byte decimal to hex/bin converter

         org 16514

; POKE 16514,d
decval:  defb 0

; 16515-16516 contains character codes of hex string
hexvals: defb 0,0

; 16517-16524 contains zeroes and ones
bitvals: defb 0,0,0,0,0,0,0,0

; code starts at 16525
         ld a,(decval)        ; this gets the right starting value into A
         ld c,a               ; keep a copy of the initial value in C

; poke individual bits into 8 addresses starting at bitvals (60005)
; but first ensure they are all 0
         ld hl,bitvals
         ld b,8
loop1:   ld (hl),0
         inc hl
         djnz loop1

; need to test 8 bits
b1:      ld hl,bitvals
         bit 7,a              ; test highest bit, sets Z flag if it is 0
         jr z,b2              ; hence jump the next instruction if the bit is 0
         ld (hl),1
b2:      inc hl               ; increase HL and continue
         bit 6,a
         jr z,b3
         ld (hl),1
b3:      inc hl
         bit 5,a
         jr z,b4
         ld (hl),1
b4:      inc hl
         bit 4,a
         jr z,b5
         ld (hl),1
b5:      inc hl
         bit 3,a
         jr z,b6
         ld (hl),1
b6:      inc hl
         bit 2,a
         jr z,b7
         ld (hl),1
b7:      inc hl
         bit 1,a
         jr z,b8
         ld (hl),1
b8:      inc hl
         bit 0,a
         jr z,hexconv
         ld (hl),1
; all addresses should be filled with 0s and 1s now.

hexconv: ld hl,hexvals        ; HL is now pointing at the correct address for the hex characters

; first character
         ld a,c               ; C is the copy of the original value
         and 240              ; top four bits only
         rrca
         rrca
         rrca
         rrca                 ; shifted to bottom four bits - value between 0 and 15
         add a,28             ; convert value to a character "0" to "F" (letters follow immediately after numbers on ZX81)
         ld (hl),a
         inc hl
; second character
         ld a,c
         and 15               ; bottom four bits only, no need to shift
         add a,28
         ld (hl),a

         ret                  ; FINISHED!
The trial program I've attached is small enough that I didn't need to use any VAL or NOT/SGN/INT PI trickery to force it to fit, and the only problem with the listing - if it is a problem - is that the REM statement tries to display a character that assimilates 10 INPUT D into its line, biting off the line number. On the other hand, doing this on the ZX81 means that after the program has been run, the result can be seen on screen: try it with 255, and the REM statement will contain COPY FF followed by eight graphic characters that are CHR$ 1.

There's something telling me I could compress it further, though - say, if there was some way that the binary bits could be tested and shifted within a LD B,8 / DJNZ loop. Something tells me it involves some ANDs to mask off the individual bits rather than using the BIT instructions, which all take two bytes. Still, this works, so I'll experiment with the Spectrum version, and if I can get it to work there (Spin's in-built assembler is very useful), I'll have a crack at translating it to the ZX81.

And then... as short a listing as I can get to work, that is what I'll translate further for the ZX80. I am going to make a ZX80 machine code listing do something useful... eventually!
Attachments
HBC uncompressed - 468 bytes.p
(465 Bytes) Downloaded 141 times
Spectribution: Dr. Jim's Sinclair computing pages.
Features my own programs, modified type-ins, RZXs, character sets & UDGs, and QL type-ins... so far!
User avatar
TMD2003
Posts: 149
Joined: Sun Oct 11, 2020 5:39 pm

Re: Assemblers for the ZX80

Post by TMD2003 »

As it turns out, waiting for a package to arrive from Amazon (at the second attempt!) can be filled with lots of wonderful machine code programming. It wasn't too hard to come up with an alternative way to isolate the bits that can be done within a B loop. Or, in this case, two B loops.

Code: Select all

; One-byte decimal to Hex converter

         org 16514

; POKE 16514,d
decval:  defb 0

; 16515-16516 contains character codes of hex string
hexvals: defb 0,0

; 16517-16524 contains zeroes and ones
bitvals: defb 0,0,0,0,0,0,0,0

; code starts at 16525
    ld a,(decval)	; this gets the right starting value into A
	ld c,a		; keep a copy of the initial value in C

; split the value at (decval) into its binary bits
	ld hl,bitvals
	ld b,8
	ld d,128	; D contains the "mask" for each bit
loop2:	push bc		; store the value of B on the stack
	dec b		; B contains how many times we need to move the bits each time
	ld a,c		; A contains the initial value to be worked on
	and d		; select only the bit we want - now need to shift it to become bit 0
loop3:	rrca
	djnz loop3	
	pop bc		; get our initial B value back from the stack
	ld (hl),a	; put the bit in A into the correct address
	inc hl		; move HL along by one
	rr d		; divide D by two
	djnz loop2
; all addresses are now filled with 0s and 1s.

hexconv: ld hl,hexvals        ; HL is now pointing at the correct address for the hex characters

; first character
         ld a,c               ; C is the copy of the original value
         and 240              ; top four bits only
         rrca
         rrca
         rrca
         rrca                 ; shifted to bottom four bits - value between 0 and 15
         add a,28             ; convert value to a character "0" to "F" (letters follow immediately after numbers on ZX81)
         ld (hl),a
         inc hl
; second character
         ld a,c
         and 15               ; bottom four bits only, no need to shift
         add a,28
         ld (hl),a

         ret                  ; FINISHED!
Including the 13 initial bytes used to store the initial decimal, hex and binary characters, there is a grand total of 57 bytes used in the REM statement. Also, I discovered I'd left one character off the end of the REM statement in the MkI test program, and that's why it was eating the next line. And there I was thinking I'd counted properly.

Something that is very strange: I'm running this on a 1K ZX81 in SLOW mode, so I can see everything happen as it happens. There is a pause before the variable D is printed on screen, but there is no pause before the two hex characters are printed. I am assuming that this is because the ZX81 takes longer to sift through the list of variables than it does to respond to a PEEK. However, when printing the binary characters, there is a pause before a 1 but not before a 0. How is that explained?

Now, as for the ZX80... 57 bytes isn't all that much, but even so, I see major headaches...
1024MAK wrote: Thu Jul 08, 2021 1:38 am The PEEKs to addresses 16392 & 16393 are the system variable for VARS. This system variable is the pointer to where the start of the variables area storage is. Because the variables are stored AFTER the BASIC program, the start of the variables area will move as program lines are added, removed or edited (if the edit results in the length changing). Hence the need for the running program to get the value. This also means that only relative addressing can be used if the length of the BASIC program is likely to change. The same applies if the code needs to store data in RAM, then it is better off somewhere else. “Safe” numbers can be stored in a REM line. If the length of the BASIC program will not change, then these restrictions don’t apply.
It might need a bit of tweaking, but if I've got this right, then I should be able to store the output bytes in a REM statement, as these will only ever be either characters 0-F, or CHR$ 0 or 1. CHR$ 1 might be a problem as it's defined as a "null string", but if I remember rightly, it'll be printed as a quotation mark.

I suppose I can start the ZX80 machine code with ld c,0 and POKE the value directly to where VARS says it is... and if I ever need to call hexvals or bitvals, these are fixed addresses given by the REM statement.

The experiments will continue...
Attachments
HBC MkII uncompressed - 423 bytes.p
(419 Bytes) Downloaded 143 times
Spectribution: Dr. Jim's Sinclair computing pages.
Features my own programs, modified type-ins, RZXs, character sets & UDGs, and QL type-ins... so far!
User avatar
TMD2003
Posts: 149
Joined: Sun Oct 11, 2020 5:39 pm

Re: Assemblers for the ZX80

Post by TMD2003 »

Even more good news! The ZX80 conversion has worked! I've managed to do something actually useful on a ZX80, in machine code, and I can't see any way it would have been possible to do it in ZX80 BASIC, with its catastrophic lack of string slicing (TL$ may be unique to the ZX80 among Sinclair computers, but it really doesn't cut the mustard here.)

Simple this may be, but I am so proud of it, that I've officially added it to my own website and the BASIC Dumping Ground on Spectrum Computing, where I've elaborated on the process at great length.

Image

Image Image

Rather than pinning it solely to this forum, get it here.
Spectribution: Dr. Jim's Sinclair computing pages.
Features my own programs, modified type-ins, RZXs, character sets & UDGs, and QL type-ins... so far!
Post Reply