When I relaunched the Minstrel 4th earlier this year, I added some more ROMs to the standard ROM image. These included the ZX80 and ZX81 BASIC I have talked much about, and the Enhanced Forth ROM which I have not.
This is the work of Alexander Sharikhin, and features additional code by George Beckett (and I think the clock check and BEEP patch might have been by me? or maybe just "Based on an original idea by Tony Warren Dave Curran")
This takes advantage of the extra 5K of ROM space I gave the Minstrel 4th, and adds various new functionality to the Forth ROM, including:
- Serial communications using the 68B50 RC2014 module
- LOAD and SAVE via serial using server program at PC end
- LOAD and SAVE blocks of memory using XMODEM
- Serial => Keyboard input and Screen => Serial
- Programming tools to support hex characters and code blocks
- CASE statements
- BEEP patched to adjust frequency when run at 6.5MHz
When I came to add that, even though the latest source was 5 years old, I appeared to have an older version, so I though it best to get the latest version and tried to build it.
That didn't go well. It needed the sjasmplus compiler with LUA support, and I couldn't get that working. Not sure what I was doing wrong. I guess something in current version of LUA or sjasmplus has changed to make it incompatible with the other.
At the time, I took the easy option of asking George to build it for me, and that was the version that went into the ROM.
I have been planning to revisit that for a while, I wanted to achieve a few things:
- Be able to built it (quite important)
- Add new functionality
- Integrate the ACE ROM source
- Rename some words
The current version is a patch. It takes the compiled Jupiter ACE ROM binary and patches a few things in there to graft in the new commands, which are compiled and added onto the end.
It also patches the names of LOAD, SAVE, BLOAD and BSAVE to change those to LOAT, SAVT, BLOAT and BSAVT to allow the new routines to use the standard names (the change of letter so not to change the code size, hence why it wasn't changed to TLOAD for example)
I can see why that has been done, but I think I would prefer to leave the standard tape load as LOAD and make the serial load SLOAD (or ULOAD to match things like UINIT which initialises the UART).
That way, I think I would push this as the main ROM, with the option of going to the original unmodified version if you want. A normal user could just run LOAD as normal for a tape load without being aware of the new functionality.
It would be ideal if I could have all the source together in a single place and do one build command to build the whole ROM from source. That is non-trivial, since the ACE source I have is based on Geoff Wearmouth's commented disassembly, and that is written to use the TASM assembler.
That's fine by me as I have been using TASM to write Z80 code since I got a copy on 5¼" floppy from a teacher at school. All of the ZX80 and ZX81 BASIC etc. is written in TASM so it seemed the best bet to stick with that.
All I had to do was manually apply the patches to the Ace source and merge the source together.
Easy.
End of post, take the rest of the day off.
Ah, no. Not quite.
It turns out there are quite a few differences between TASM Z80 and sjasmplus. Many of them were just search and replace, but some required a bit more thought.
Here are some examples I found.
Numbers
The numbers were in several formats, 0x1234 and also #1234 (which I found rather annoyingly Visual Studio Code displayed with a box to the left showing what #1234 would look like if it were an HTML colour).
I am sure there is a way to turn that off, but I need to change them anyway since TASM used $1234 for hexadecimal numbers (and also 1234H, but I rarely use that notation).
RECV_RETRY: equ 0x1000 🠆 RECV_RETRY .EQU $1000
TMP_DICT = #2709 🠆 TMP_DICT .EQU $2709
That was mostly search and replace, but as with all of these changes, a good opportunity to read through the code and find out what it all did (and for me to unnecessarily align all the comments because that's the way I roll).
Local Labels
Local labels save you having to think of a new name for the point you jump back to in a loop every time.
You can't call them all "loop", so you can use a local label that's just called "loop" within this scope of your function. sjasmplus uses ".loop" for this, but TASM uses "_loop" (you can change that, but I'd rather stick with the standard). That's not too bad, just search and replace again.
But the scope is also different. In sjasmplus this seems to be relative to the last proper label only.
main:
ld b, 42
.loop
djnz .loop
That .loop is treated as main.loop, which makes sense and is a neat way to do it.
I use ACME for 6502 code, and that also has + and -, so you can do
main:
lda, 42
- dec
bne -
Where - just refers to the previous - label, and + to the next + label (you can do -- and ++ as well for the one before / after that, but it starts to get a bit complicated and is usually better to give things names unless it's obvious)
TASM doesn't have either of those, it needed .MODULE definitions adding where necessary to control the scope.
main:
.MODULE MAIN
ld b, 42
_loop
djnz _loop
So that is now MAIN._loop.
Instruction differences
There were a few instructions that had to be changed.
ex hl, de
That was used in a couple of places. This isn't an "official" Z80 opcode, but the assembler just accepted it and converted it to the more correct
ex de, hl
TASM didn't, so I had to do that manually.
I wanted to check that to be sure, so I disassembled the Enhanced part of the ROM.
z80dasm -a -g 0x2800 -l -t 2800.bin
That gave me a listing of the code so I could check what had actually been built.
ex de,hl
Yes, the compiler had just replaced the instruction.
The next one confused me, as far as I know, there isn't an ld (hl), de instruction.
.save:
ld (hl), de
ret
Checking the disassembly, it turns out the code actually generated was:
ld (hl),e
inc hl
ld (hl),d
dec hl
ret
Interesting, not sure why you would use an instruction that doesn't exist, particularly if for example, you didn't need the value in hl, so the last dec hl would be unnecessary.
_save:
ld (hl),e ;ld (hl), de
inc hl ;
ld (hl),d ;
dec hl ;
ret
I left the original instruction in the comment for reference.
This one also confused me.
dup 4
call loadVar
jr c, _error
edup
I was thinking this was some kind of forth thing, since dup is a forth word to duplicate the item at the top of the stack
But no, it must be a compiler thing. Looking at the disassembly it appears to mean "duplicate this 4 times......end duplicate"
call sub_2ac6h
jr c,l2ce0h
call sub_2ac6h
jr c,l2ce0h
call sub_2ac6h
jr c,l2ce0h
call sub_2ac6h
jr c,l2ce0h
I just replace that with the unrolled code, again with a comment
RST calls
There were two variations of RST calls, using 0xnn and #nn (both seen in this function)
.cont:
rst 0x08 ; Print character
pop bc
djnz .loop
ld a, CR
rst #08
ld a, e ; Retrieve block type
and a ; Indicate success
ret
Unfortunately it turns out TASM doesn't accept either of those. Furthermore, it doesn't accept what I would have expected
rst $08
You have to do
rst 08H
More search and replace and we are getting closer.
Just a side note, got to love Z80 syntax
cpl ; Compute 255-A, ones ComPLiment
cp l ; Compare A to the L register
And
rra ; Rotate accumulator right through carry (4 cycles)
rr a ; Rotate accumulator right through carry (8 cycles)
Oh, for a RISC processor ......
Forth Words
The elephant in the room.
The is probably described better somewhere else, by someone who understands it better that I do, but here goes.
The forth dictionary is a singly linked list. There are two bytes at the end of the original ROM at $1FFD. This points to the first word in the dictionary (after FORTH).
On the original ROM that is UFLOAT, but if you wanted to add a new word into the ROM, you would change the value at $1FFD to point to your new word, and you would set your word to point back to UFLOAT, inserting it into the chain.
(what, you didn't expect me to include a vlist print out? at least I might have surprised you by not using the inverse colour scheme I normally do)
There are lots of forth words added in the enhanced ROM, all of which are linked between FORTH and UFLOAT.
(paused part way through as they normally scroll off the top of the screen - do we need a new vlist word with a ZX Spectrum style SCROLL? function????)
(a benefit of the work I previously did getting the extra 5K supported in EightyOne means I can now run and debug the enhanced ROMs. Not perfect as the save detection has to be disabled to avoid false triggering with the ZX ROMs)
In the original ROM, forth words are defined as follows:
; ---------------
; THE 'BASE' WORD
; ---------------
; ( -- 15423)
; A one-byte variable containing the system number base.
; $3C3F BASE
L0483: DEFM "BAS" ; 'name field'
DEFB 'E' + $80
DEFW L047F ; 'link field'
L0489: DEFB $04 ; 'name length field'
L048A: DEFW L044D ; 'code field'
The actual code follows elsewhere, pointed to by the code field.
The name is "BASE", the last letter of which has $80 added to it to indicate the end of the string.
There is also a length field, which in some cases has $40 added to indicate an immediate word (for things like DO WHILE, IF THEN ELSE etc.)
The links are all hard coded, from word to word, in the order they appear in the file (not sure if that is true for all of them, but it seems to be the general case).
This was one of the nice features that the enhanced ROM needed sjasmplus assembler with LUA for.
w_uinit:
FORTH_WORD "UINIT"
call uart_init
jp (iy)
.word_end:
FORTH_WORD is a macro which expands out to the type of header above, creating the name, link and length fields automatically.
I couldn't use that, so I had to do it manually, in the same way as the original ROM (or at least the disassembly - maybe they had some equivalent function in whatever assembler was used for the Ace ROM).
w_uinit:
;FORTH_WORD "UINIT"
.MODULE UINIT
.BYTE "UINI" ; 'name field'
.BYTE 'T'+$80 ;
.WORD w_uread_link ; 'link field'
w_uinit_link:
.BYTE $05 ; 'name length field'
.WORD w_uinit_code ; 'code field'
w_uinit_code:
call uart_init
jp (iy)
I initially hard coded all the links, setting the link address to the label for the previous word (in this case UREAD), and then setting a new link label for the next word to use.
Right, almost there.
CASE
The last thing to tackle was the code for the case statement construct. This was in a different format to the standard assembly code words. This one used FORTH words (or bits of them), in the same way as a few original words, such as ROT. This rotates the top three bytes of the stack:
; --------------
; THE 'ROT' WORD
; --------------
; (n1, n2, n3 -- n2, n3, n1)
L08F9: DEFM "RO" ; 'name field'
DEFB 'T' + $80
DEFW L08ED ; 'link field'
L08FE: DEFB $03 ; 'name length field'
L08FF: DEFW L0EC3 ; 'code field' - docolon
; ---
L0901: DEFW L08D2 ; >R
L0903: DEFW L0885 ; swap
L0905: DEFW L08DF ; R>
L0907: DEFW L0885 ; swap
L0909: DEFW L04B6 ; exit
It does that using five forth words, rather than any assembly language.
It appears CASE and related words are the same, but the version in the source is just a list of bytes.
;; CASE
ABYTEC 0 "CASE"
dw LINK
SET_VAR LINK, $
db 0x44
db 0x08, 0x11
dw $ + 0x0f ; 0x93, 0x3C
db 0x11, 0x10, 0x00, 0x00, 0x11, 0x10, 0x09, 0x00
db 0x40, 0x11, 0x00, 0xE7, 0xFF
db 0x42, 0x11, 0x79, 0x08, 0xB6, 0x04
To get building, I just pasted in the bytes from the disassembly (code and header).
;; CASE
.BYTE $4F, $C6, $69, $29
.BYTE $42, $08, $11, $A1, $29, $11, $10, $09, $00
.BYTE $46, $29, $60, $04, $11, $10, $00, $00, $4E
.BYTE $0F, $11, $10, $07, $00, $40, $11, $02, $DF
.BYTE $FF, $42, $11, $D2, $08, $12, $09, $4A, $0C
.BYTE $DF, $08, $85, $08, $83, $12, $09, $00, $79
.BYTE $08, $79, $08, $71, $12, $0B, $00, $B3, $08
.BYTE $DF, $08, $D2, $0D, $D2, $08, $A4, $12, $B6
.BYTE $04
OK I think it's there, so time to build it.
wine TASM.EXE -80 -b -L minstrel4th.asm minstrel4th.rom
I am on linux, but still using the old DOS command line TASM, via wine.
I had a few typos to fix here and there, but finally got it built.
I use HxD hex editor (again because I have been using that since the year dot), and that brought up a few differences, usually cut and paste errors with links in the word definitions.
A few more tweaks and I finally got it to build binary identical.
Great.
End of post, take the rest of the day off.
Ah, no. Not quite.
The challenge now was to fix the links, and do something with the case code.
Automatic linked list
The FORTH_WORD macro used a variable called LINK, which was initialised to the address of UFLOAT (first word after FORTH in the original ROM). This was then updated each time to the current link.
Once I worked out the syntax for TASM, I was able to use the same sort of thing, so I went through and replaced all the hard coded links with this new automated link code. E.g.
w_hex:
.TEXT "HE" ; 'name field'
.BYTE 'X'+$80 ;
.WORD LINK ; 'link field'
LINK .SET $ ;
.BYTE $03 ; 'name length field'
.WORD w_hex_code ; 'code field'
w_hex_code:
ld (IX + $3F), $10
jp (iy)
That was neater and will also allow me to change things more easily.
I rebuilt it and it was still binary identical. Great.
Making the case
I now had various different sources for case the version in the enhanced ROM with a header and just bytes. The version from the disassembly that was just bytes.
I now had found a third, from Geroge's github
https://github.com/markgbeckett/jupiter_ace/tree/master/case
This was in the form of the forth words.
0 COMPILER CASE
0 ( MARKER ON STACK, USED BY ENDCASE TO CHECK DONE )
9 ( SYNTAX GUIDE, MUST MATCH WITH 'OF' )
RUNS>
DROP ( NOTHING TO DO EXCEPT DROP ADDR OF PARAMETER FIELD )
;
With a bit of searching through the various versions, I was able to create something a bit more readable than the list of bytes (but still binary identical)
.TEXT "CAS" ; 'name field'
.BYTE 'E' + $80
.WORD LINK ; 'link field'
LINK .SET $ ;
.BYTE $04 + $40 ; 'name length field' (immediate word)
.WORD L1108 ; 'code field' - compile
.WORD w_case ;
.WORD L1011 ; stack next word
.WORD $0000 ; 0
.WORD L1011 ; stack next word
.WORD $0009 ; 9
.WORD L1140 ; (part of) RUNS>
.BYTE $00, $E7, $FF ; ???
w_case:
.WORD L1142 ; (part of) RUNS>
.WORD L0879 ; DROP
.WORD L04B6 ; exit
I think I understand most of that, although I am not sure about the code that makes up the "RUNS>" bit.
The important thing is that it still builds binary identical, and I do not think there are any absolute links, so the code could move around and still build and work.
What next?
I have a few things to add in terms of functionality and also hardware support. If you think of anything else that would be useful, there is still about 2.5KB remaining.
I am also thinking about spiting up the source further into separate files for each word, grouped in folders, rather than all the related words in a single file etc. Not sure if that is necessary or beneficial, but it might make reordering things a bit easier. I will try it for the new words, and if it works out, I will probably go back and reformat the existing code (I will go back to this version and make it build identical before I add back the new code).
I also hope none of that comes off as critical or negative, that wasn't my intention, this was just my choice to give myself a lot of work translating someone else's code to an archaic assembler that I happen to like, to theoretically make my life easier in future.
Adverts
The Minstrel 4th is available form my Tindie store:
I can still ship worldwide.
Currently it looks like Royal Mail to the US is working although it seems I now have to pay the 10% tariff, so hopefully that will be plain sailing and no problems with customs. We're all pawns in a petulant child's political games, but we've got to just keep going and try to make this stuff work.
I have also built some more Mini PET 40/80 Internal boards, since the Mini PET II is still in development but people keep asking, so here they.
Patreon
You can support me via Patreon, and get access to advance previews of development logs on new projects like the Mini PET II and Mini VIC and other behind the scenes updates. This also includes access to my Patreon only Discord server for even more regular updates.