Close

Using a Z80 assembler

A project log for eForth/z80 modifications

A more hackable eForth/z80

ken-yapKen Yap 08/07/2019 at 14:018 Comments

One of my goals is to be able to write platform-specific routines in native Z80 assembler instead of hand assembly or separate assembly, then inserting the bytes as DB lines in the code.

Before I show how it's accomplished, let me present the Z80 source for a couple of Forth words:

;   PC!         ( uc p --  )
;               output uc to IO port p

;!                $CODE   3,'PC!',PCSTO
EXX           ; 4t
POP BC        ;10t
POP DE        ;10t
LD  A, E      ; 4t
OUT (C), A    ;12t
EXX           ; 4t
;!DB 0C3h
;!DW NextStep          ;       JP  NextStep  ;10t
                       ;                     ;54t==(10MHz)5.4usec

;   PC@         ( p -- uc )
;               input uc from IO port p

;!                $CODE   3,'PC@',PCAT
EXX           ; 4t
POP BC        ;10t
IN  A, (C)    ;12t
LD  E, A      ; 4t
LD  D, 0      ; 7t
PUSH DE       ;11t
EXX           ; 4t
;!DB 0C3h
;!DW NextStep          ;       JP  NextStep  ;10t
                       ;                     ;62t==(10MHz)6.2usec

 Now the transformed source:

;   PC!         ( uc p --  )
;               output uc to IO port p

                $CODE   3,'PC!',PCSTO
DB 0D9h         ;EXX           ; 4t
DB 0C1h         ;POP BC        ;10t
DB 0D1h         ;POP DE        ;10t
DB 7Bh          ;LD  A, E      ; 4t
DB 0EDh,79h     ;OUT (C), A    ;12t
DB 0D9h         ;EXX           ; 4t
DB 0C3h
DW NextStep          ;       JP  NextStep  ;10t
                       ;                     ;54t==(10MHz)5.4usec

;   PC@         ( p -- uc )
;               input uc from IO port p

                $CODE   3,'PC@',PCAT
DB 0D9h         ;EXX           ; 4t
DB 0C1h         ;POP BC        ;10t
DB 0EDh,78h     ;IN  A, (C)    ;12t
DB 5Fh          ;LD  E, A      ; 4t
DB 16h,00h      ;LD  D, 0      ; 7t
DB 0D5h         ;PUSH DE       ;11t
DB 0D9h         ;EXX           ; 4t
DB 0C3h
DW NextStep          ;       JP  NextStep  ;10t
                       ;                     ;62t==(10MHz)6.2usec

The transformation is done by a Python script, lsttoinc.py. It follow several rules:

However this scheme has its limitations, witness this piece of code:

;   ?RX         ( -- c T | F )
;               Return input character and true, or a false if no input.

;!                $CODE   3,'?RX',QRX
;!DB    21h, 14h,0FEh    ;       LD  HL, SiobRxQout  ;16t
LD  A, (HL)         ; 7t
;!DB    21h, 12h,0FEh    ;       LD  HL, SiobRxQin   ;16t
CP  (HL)            ; 7t
JR  NZ,.+9          ; 7t/12t (NZ jump=12t) ptr <>, get the char
                    ; can't use label because JP NextStep not accounted for
LD  HL, 0           ;10t   False flag
PUSH HL             ;11t
;!DB   0C3h
;!DW   NextStep          ;       JP  NextStep        ;10t ==> False Timing:84t==(10MHz)8.4usec
                       ;                           ;
LD  E, (HL)         ; 7t  get this ptr LOW
INC HL              ; 6t
LD  D, (HL)         ; 7t  get this ptr HI
EX  DE,HL           ; 4t  let HL point the char
LD  E, (HL)         ; 7t  get the char
LD  D, 0            ; 7t  high byte =0
PUSH DE             ;11t
INC L               ; 4t  ptr+1, a 256 byte Ring queue
;!DB    22h, 12h,0FEh    ;       LD  (SiobRxQin),HL  ;16t
LD  HL,0xFFFF       ;10t
PUSH HL             ;11t
;!DB   0C3h
;!DW   NextStep          ;       JP  NextStep        ;10t ==> TRUE case: 100t(10MHz)10.0usec

And the transformed code:

;   ?RX         ( -- c T | F )
;               Return input character and true, or a false if no input.

                $CODE   3,'?RX',QRX
DB    21h, 14h,0FEh    ;       LD  HL, SiobRxQout  ;16t
DB 7Eh          ;LD  A, (HL)         ; 7t
DB    21h, 12h,0FEh    ;       LD  HL, SiobRxQin   ;16t
DB 0BEh         ;CP  (HL)            ; 7t
DB 20h,07h      ;JR  NZ,.+9          ; 7t/12t (NZ jump=12t) ptr <>, get the char
                    ; can't use label because JP NextStep not accounted for
DB 21h,00h,00h  ;LD  HL, 0           ;10t   False flag
DB 0E5h         ;PUSH HL             ;11t
DB   0C3h
DW   NextStep          ;       JP  NextStep        ;10t ==> False Timing:84t==(10MHz)8.4usec
                       ;                           ;
DB 5Eh          ;LD  E, (HL)         ; 7t  get this ptr LOW
DB 23h          ;INC HL              ; 6t
DB 56h          ;LD  D, (HL)         ; 7t  get this ptr HI
DB 0EBh         ;EX  DE,HL           ; 4t  let HL point the char
DB 5Eh          ;LD  E, (HL)         ; 7t  get the char
DB 16h,00h      ;LD  D, 0            ; 7t  high byte =0
DB 0D5h         ;PUSH DE             ;11t
DB 2Ch          ;INC L               ; 4t  ptr+1, a 256 byte Ring queue
DB    22h, 12h,0FEh    ;       LD  (SiobRxQin),HL  ;16t
DB 21h,0FFh,0FFh        ;LD  HL,0xFFFF       ;10t
DB 0E5h         ;PUSH HL             ;11t
DB   0C3h
DW   NextStep          ;       JP  NextStep        ;10t ==> TRUE case: 100t(10MHz)10.0usec

The instructions involving locations like SiobRxQout are inserted as DBs. To insert them as Z80 instructions, the symbols must be known to the Z80 assembler. We could declare them in an include file. If they are also used in the JWASM code then there is duplication and you have to make sure to keep the two defines in sync. That's probably not too bad as the locations are usually decided on once.

The more serious one is where the JP NextStep is inserted as DBs. Since the Z80 assembler doesn't see these instructions, it cannot calculate jump offsets correctly, hence the JP NZ,.+9 instead of JP NZ,label. Again to fix this we should define NextLabel to the Z80 assembler. This requires building it once, looking at where NextStep is and then editing the Z80 assembler define for NextStep.

The real problem is the use of ;! lines to pass lines through verbatim. These should be kept to a minimum. They are unavoidable in the case of the Forth word headers, e.g. the 

$CODE   3,'?RX',QRX

Those are ok since one should not reference code outside of the current Forth word, i.e. jump into the code for another word.

I'll probably leave it as it is for a while to think it over before making these changes.

In the long run what really needs to be done is to rewrite the code so that it assembles with asz80 only which does have a macro facility. The limitations are due to using two assemblers. But this is a larger undertaking and probably should get a new directory.

Discussions

Ken Yap wrote 08/08/2019 at 21:07 point

Thanks I'll apply that to locate the offending assembly line.

  Are you sure? yes | no

Thomas wrote 08/08/2019 at 05:12 point

Maybe I don't yet fully understand the approach - do you plan to keep Python in the tool chain or do you plan to do a one-time transformation?

Did you check if the macro features of the ASxxxx variant bundled with SDCC (or that of the stand-alone ASxxxx) meet your needs?

  Are you sure? yes | no

Ken Yap wrote 08/08/2019 at 05:39 point

As I mentioned in the last paragraph above, asz80 does have a macro facility so in the long term that should be the assembler to use, but I would conclude work on this one to start a new directory for the next version. It was strange that MASM was used but that's probably because in those days that was all the author had to hand.

I tried at the beginning to port the code to asz80 but it was tough going, and I even managed to make asz80 segfault. So I decided to first restructure the code (using includes, adding Z80 coding, creating equates) to make it more comprehensible to me without breaking anything. It had the benefit of showing me which parts I should isolate as system dependent and which are pure Forth. There are some magic numbers in the comments which are peripheral ports and should be turned into equates. I will make the changes I proposed above, push the code to Github, close off this experiment and clone to a new directory for a single assembler version. But that may take a while.

  Are you sure? yes | no

Thomas wrote 08/08/2019 at 19:00 point

asxxx macros, and segfaults: I had some troubles with that, too: https://sourceforge.net/p/sdcc/bugs/2626/

  Are you sure? yes | no

Ken Yap wrote 08/08/2019 at 11:50 point

I'm gradually converting all the DBs in the EFZ80.ASM file to real Z80 lines and discovered a handful of discrepancies betwen the hand inserted bytes and the purported assembler code. It looks like they got out of sync at some point. I don't know if anybody has managed to use the binary unless they had the exact same hardware as the author or managed to find the right places to patch. So this little diversion has been useful in cleaning up the code.

  Are you sure? yes | no

Thomas wrote 08/08/2019 at 19:04 point

Making a 8Bit eForth stable isn't easy. Hard coded MASM defines instead of assembly code surely doesn't help. Even the eForth 2.x (i.e. STC Forth) had some really nice bugs.

  Are you sure? yes | no

Ken Yap wrote 08/08/2019 at 20:31 point

Thanks, that's exactly the bug I hit. I'll have to see which macro definition or invocation is triggering it. Also if a newer asxxxx version has fixed it. The SDCC bug tracker wouldn't have got the attention of the asxxxx author and the SDCC devs don't care since they don't use assembler macros. If I can't work around it I'll have to detour into finding the asxxxx bug first.

  Are you sure? yes | no

Thomas wrote 08/08/2019 at 20:57 point

Check out the patch that I posted here: https://sourceforge.net/p/sdcc/bugs/2626/#fd60

This will, at least., return the line of the macro that triggers the segfault. I tried to solve the problem but the macro code is quite complex. Avoiding certain separator characters in macros ("," and ";" I think) avoids the problem.

What's also to be avoided is macros nearby includes.

  Are you sure? yes | no