[comp.sys.handhelds] A CHIP8 Assembler for the HP48SX long

gilles@disys.dis.incom.de (Gilles Kohl) (11/30/90)

Summary:
--------

This posting describes (and contains) an assembler for Andreas Gustafsons
CHIP-48. The assembler is for the HP48 itself - no computer required. A
successful assembly yields a string that may directly be fed to the CHIP
interpreter.


Introduction:
-------------

The method used has its advantages and has its flaws. Let me list the flaws
first:

- uncompatible with existing sources:  I have seen several forms of CHIP-8
sources posted here, including Z80-style (syzygy, puzzle15) implemented via
M80 macros, and one more BASIC-like (pong). The approach being described here
is RPN-style and certainly looks weird at first sight.

- requires getting used to:  If you've already written CHIP8 using another
assembler, AS48 will require some accustomization due to its unusual syntax
(and mnemonics - but these can be adapted to your gusto)

- very rudimentary error handling.


Lets have a look at the advantages:

- relatively fast assembling - considering that there is no machine 
  language involved.
- assembler is relatively small.
- short turn-around: no downloading required. 
  Nice to learn CHIP8 and to test out routines.
- macros, conditional assembly, modules (and more) are possible
- easily modifiable/expandable

This sounds like a lot for a tiny assembler, and of course there is a trick
to it. The basic idea is not to write a program that would take a text string
and assemble it, but rather have the source assemble _itself_ :)


Example
-------

A source for AS48 is in fact simply an HP48 user program. Every CHIP-8
mnemonic corresponds to a small subroutine which expects its arguments (the
operands for the CHIP-8 instruction, if any) on the stack, and generates the
corresponding opcode (The CHIP-8 string is built on the stack).  This is the
reason for the RPN-style syntax. Instead of long explanations, lets have a
look at a source in this format:

@ --- cut here ---
%%HP: T(3)A(D)F(.);
\<<                        @ User program delimiters
    begin                  @ Start AS48 source
'start' lbl                @ Define label 'start' (quotes mandatory)
    v0 # FFh movc          @ move const FFh into register v0
    v0 tset                @ set timer with v0
'loop' lbl                 @ define label 'loop'
    charbuf iset           @ i := charbuf (no quotes required here)
    v0 tget                @ get current timer value into v0
    v0 sbcd                @ store value in v0 as bcd bytes (where i points)
    v2 rreg                @ read registers (via i) up to v2
    v4 # 0h movc           @ zero v4

    1 2 START              @* repeat following twice (at assembly-time!)
      0 2 FOR I            @* loop from 0 to 2 (at assembly-time!)
        v3 I #5 * movc     @* v3 := 5 * I (i.e. 0, 5, 10)
        v0 I + ichr        @* point i to character in vI (tricky bit :-)
        v3 v4 # 5h drw     @* draw sprite at i on coords (v3,v4)
      NEXT                 @* inner loop
    NEXT                   @* outer loop 

    v0 #0 seqc             @ skip next instruction if v0 == #0
    loop  jmp              @ not timed out yet, continue
    start jmp              @ timed out, restart
'charbuf' lbl              @ character buffer
    # 0h db                @ define a byte
    # 0h db                @ and another one
    # 0h db                @ and the last one
    end                    @ end AS48 source
\>>
@ --- cut again ---

To assemble this source, you would:
- enter it resp. download it to your HP48
- name it (store in a variable)
- give AS48 the quoted variable name as input.
(You may also give AS48 a program as input, but it would be lost after
assembly - just as pressing EVAL with a program in level 1 does)

Most of the 'features' listed above directly result from the fact that the
source is a program itself. You can of course use the built-in editor to
enter your sources, take advantage of the function keys as typing aid, have
various sources stored under different names, enter binary, octal or hex
constants ... macros, conditional assembly, and the like result from
this, too. Lets have a closer look:

- You'll notice several pseudo-ops, one of them is "lbl" which defines a label.
The corresponding name must precede (of course) and be quoted.
References to labels (i.e. " loop jmp " for ex.) do not require the quotes,
but may have them.

- To avoid nameing conflicts, the AS48 mnemonics / pseudo-ops use lower
case. Otherwise, problems with OR, AND, XOR, END ... would have
resulted.

- Macros, conditional assembly and the like simply take advantage of the fact
that RPL is available anyway. In the section above marked '*' in the
comments, the outer loop causes all contained code to be generated twice.
The inner loop expands three times, and generates slightly different code
each time. One must keep in mind that such constructs are evaluated and
executed at assembly-time, not at CHIP8 runtime. The outer loop above would
be more elegantly done using a subroutine. (But its still a nice example)

- Registers are named 'v0' to 'vf'. Each register in fact simply yields a
binary value corresponding to its number. One _may_ take advantage of this,
as done above in the line marked "tricky bit" ...


Mnemonics table
---------------

This is basically the opcode table by Andreas Gustafson, I've just added the
AS48 mnemonics and parameters in the left columns. 

Note that:  

- The 'call 1802 machine code at nnn' is not available as a mnemonic 

- there's more mnemonics than the other assemblers have. I have chosen to
keep AS48 as simple as possible, and not to have an "ld" mnemonic with 7
different flavors. If you don't like the mnemonics, just change them. If you
want the same mnemonic for several different opcodes, you'll have to
differentiate according to parameter types, and maybe introduce a bit of your
own syntax. I'm afraid this would (further) slow the beast down. (And spoil
the basically very simple idea behind all this a bit)

--- cut here for a quickref chart ---
Parameters Mnem.   Opcode  Description
---------- -----   ------  ---------------------------------------------
       c   N.A.    0NNN    Call 1802 machine code subroutine at NNN
     x y   mov     8XY0    VX := VY, VF may change
     x c   movc    6XKK    VX := KK
     adr   iset    ANNN    I := NNN
       x   iinc    FX1E    I := I + VX
       x   ichr    FX29    Point I to 5-byte font sprite for hex character VX
       x   tset    FX15    delay_timer := VX
       x   tget    FX07    VX := delay_timer
       x   sset    FX18    sound_timer := VX
       x   kget    FX0A    wait for keypress, store hex value of key in VX
       x   sreg    FX55    Store V0..VX in memory starting at M(I)
       x   rreg    FX65    Read V0..VX from memory starting at M(I)
       x   sbcd    FX33    Store BCD representation of VX in M(I)..M(I+2)
     x c   addc    7XKK    VX := VX + KK
     x y   add     8XY4    VX := VX + VY, VF := carry
     x y   sub     8XY5    VX := VX - VY, VF := not borrow
     x y   subn    8XY7    VX := VY - VX, VF := not borrow
       x   shl     8XYE    VX := VX shl 1, VF := carry
       x   shr     8XY6    VX := VX shr 1, VF := carry
     x y   and     8XY2    VX := VX and VY, VF may change
     x y   or      8XY1    VX := VX or VY, VF may change
     x y   xor     8XY3    VX := VX xor VY, VF may change 
     x c   rnd     CXKK    VX := pseudorandom_number and KK
     adr   jmp     1NNN    Jump to NNN
     adr   jsr     2NNN    Call subroutine at NNN
           rts     00EE    Return from subroutine
     x c   seqc    3XKK    Skip next instruction if VX == KK
     x c   snec    4XKK    Skip next instruction if VX != KK
     x y   seq     5XY0    Skip next instruction if VX == VY
     x y   sne     9XY0    Skip next instruction if VX != VY
       x   skp     EX9E    Skip next instruction if key VX pressed
       x   snkp    EXA1    Skip next instruction if key VX not pressed
     adr   jv0     BNNN    Jump to NNN+V0
           cls     00E0    Clear display
   x y c   drw     DXYN    Show N-byte sprite from M(I) at coords (VX,VY), VF := collision

x and y are registers (Use either vn or #n), c is a binary constant,
and adr an (unquoted) label. 

      
Pseudo Ops:
-----------

The following pseudo-ops are available:

Parameter      Pseudo-op    Description
---------      ---------    -----------
               begin        Start an AS48 source
               end          End an AS48 source
quoted-name    lbl          Define a label.
binary int     dw           deposit a word
binary int     db           deposit a byte
label          aevl         evaluate an address.
--- cut again ---

The last one (aevl) requires a bit of explanation. It is required when you're
wanting to do address calculations. Suppose, for example, that you've got:

'table' lbl  @ define label 'table'
   #48h db  
   #50h db  
   #34h db  
   #38h db   @ define some bytes 

and want to set register i to the third byte (table+2) 
and want to do this calculation at assembly-time. You'd use:

    table aevl #2d + iset

The 'aevl' is required here for the forward referencing mechanism to work
correctly. All mnemonics having an address as parameter (jmp, jsr, iset ...)
do this automatically, so it is not required there (that is, if only a label
precedes them). Another example:

Compute the difference between labels 'table' and 'endtable', and store into
variable v0:

   v0 endtable aevl table aevl - movc

Note that two aevl's are required here.


Listing
-------

@ --- cut here ---
%%HP: T(3)A(D)F(.);
DIR
  MNEM DIR
@ register transfer
    movc  \<< # 6000h xc  \>> 
    mov   \<< # 8000h xy  \>> 
    iset  \<< # A000h adr \>> 
    iinc  \<< # F01Eh x   \>> 
    ichr  \<< # F029h x   \>> 
    tset  \<< # F015h x   \>> 
    tget  \<< # F007h x   \>> 
    sset  \<< # F018h x   \>> 
    kget  \<< # F00Ah x   \>> 
    sreg  \<< # F055h x   \>> 
    rreg  \<< # F065h x   \>>
    sbcd  \<< # F033h x   \>>
@ arithmetic
    addc  \<< # 7000h xc  \>>
    add   \<< # 8004h xy  \>>
    sub   \<< # 8005h xy  \>>
    subn  \<< # 8007h xy  \>>
    shl   \<< # 800Eh x   \>>
    shr   \<< # 8006h x   \>>
    and   \<< # 8002h xy  \>>
    or    \<< # 8001h xy  \>>
    xor   \<< # 8003h xy  \>>
    rnd   \<< # C000h xc  \>>
@ flow control
    jmp   \<< # 1000h adr \>>
    jsr   \<< # 2000h adr \>>
    rts   \<< # 00EEh dw  \>>
    seqc  \<< # 3000h xc  \>>
    snec  \<< # 4000h xc  \>>
    seq   \<< # 5000h xy  \>>
    sne   \<< # 9000h xy  \>>
    skp   \<< # E09Eh x   \>>
    sknp  \<< # E0A1h x   \>>
    jv0   \<< # B000h adr \>>
@ miscellaneous
    cls   \<< # 00E0h dw  \>>
    drw   \<< # D000h xyc \>>
@ pseudo ops
    lbl   \<< PC SWAP STO \>>
    db    \<< IF 2 FS? THEN B\->R CHR + ELSE DROP END 
              1 'PC' STO+ PC 3 DISP \>>
    dw    \<< IF 2 FS? THEN B\->R DUP 256 / IP CHR SWAP 256 MOD CHR + + 
              ELSE DROP END 2 'PC' STO+ PC 3 DISP \>>
    aevl  \<< IF 2 FS? THEN EVAL DUP TYPE 6 IF SAME THEN 'UNDEFS' STO+ 
              1 SF # 0h END ELSE DROP #0h END \>>
    SYM DIR @ Directory where assembly actually takes place
    END     @ the 'symbol table' will be created here.
  END
  NEW  \<< \<< begin end \>> \>>
  ASM  \<< RCWS RCLF \-> ws flg \<< DUP 
           "AS48 (G. Kohl)\010Initializing ..." 1 DISP 
           MNEM SYM CLVAR 16 STWS HEX
           #0h v0 STO #1h v1 STO #2h v2 STO #3h v3 STO 
           #4h v4 STO #5h v5 STO #6h v6 STO #7h v7 STO 
           #8h v8 STO #9h v9 STO #Ah va STO #Bh vb STO 
           #Ch vc STO #Dh vd STO #Eh ve STO #Fh vf STO 
           2 CF "PASS 1" 2 DISP EVAL
           2 SF {} 'UNDEFS' STO "PASS 2" 2 DISP EVAL
           IF 1 FS? THEN UNDEFS "Undefd" \->TAG END
           UPDIR UPDIR ws STWS flg STOF \>> \>>
  begin \<< 1 CF #200h 'PC' STO IF 2 FS? THEN "" END \>>
  end   \<< IF 1 FS? THEN DROP END \>>
  xyc   \<< SWAP OR SWAP #10h * OR SWAP SLB OR dw \>>
  x     \<< SWAP SLB OR dw \>>
  xy    \<< SWAP #10h * OR SWAP SLB OR dw \>>
  xc    \<< OR SWAP SLB OR dw \>>
  adr   \<< SWAP aevl OR dw \>>
END
@ --- cut again ---


Usage 
----- 

Download the above directory as AS48 to your HP48. (Use ascii
transfer, translation code 3). Enter AS48. Hitting NEW will yield an empty
AS48 source (Just \<< begin end \>>). You may now enter the MNEM subdirectory
- the AS48 mnemonics are available as typing aids at this level. Use
[gold][EDIT] to modify the source. When done editing, leave the MNEM
subdirectory ([gold][UP]) and store your source under a name of your choice.
To assemble, enter this name (quoted) and hit ASM. (As an AS48 source is
really an RPL program, you may assemble sources located above ASM in the
directory tree. ASM relies upon being called just above MNEM, though)

Successfull assembly will yield a CHIP8 string in level 1. If undefined
symbols exist, a list containing them will instead be returned. There is very
few error checking (to keep this thing reasonably fast), so be careful.
Assembling your source may suddenly stop with an RPL error message if
something's wrong (bad parameter types, for example). If the problem cause
isn't obvious, remember that an AS48 source is really an RPL program: it can
be debugged ...


Tips & Tricks, Expansions
-------------------------

Symbol table: After an assembly, changing to MNEM SYM (and evtl. hitting VAR)
will yield the 'symbol table'. During assembly, every label creates a
variable of that name, the contents of the variable being of course the
CHIP8-adress where it was created. Predefined variables are v0 .. vf (the
CHIP8 registers) and PC. After a successful assembly, PC points to the last
byte (+1) of the generated code, in CHIP8 address space.

Macros: Macros may be implemented by creating an RPL program at the start of
your source, naming it, and calling it (with appropriate parameters) in the
rest of the source. Hint: flag 2 is internally used by ASM to keep track if
its on pass 1 or 2. You'll need to create your macro on pass 1 only, so you
might include it into an IF 2 FC? THEN ... END sequence. The macro itself may
also take advantage of flag 2.

Modules: a module is but an AS48 source with the 'begin' 'end' brackets
removed. You can then call it from another source. Such modules may reside
higher in the directory tree than ASM.

Computed tables: tables of various sorts can be computed using RPL (and some
dw's or db's). An interesting extension might be an RPL program to turn a
GROB into a sprite, or even to directly turn a character (string) into
CHIP8-sprites. 

Conversion to a more standard form: The following device could be used to
automatically convert an AS48 source into a format suitable for other
assemblers: in a separate directory, create a small program for every AS48
mnemonic (and pseudo-op) and make it output its own name (in converted form)
as well as its parameters, in prefix form. (Maybe output directly to the
serial port) Then, converting a source (and uploading to a computer) is just a
matter of changing to that directory and evaluating the source.
 ----

Hope AS48 can be useful for somebody. This is my first attempt at both
posting in this forum and at writing anything non-trivial in RPL. Please
be indulgent, excuse my mistakes, but tell me about them :-)

cloos@acsu.buffalo.edu (James H. Cloos) (12/04/90)

Now all someone needs to do is to make a (arg-checking) library out of
this and CHIP itself.  You would also, in that case, make CHIP use a
Library Data rather than a string, and include a DOEXT0\->$ and
$\->DOEXT0 routine pair to allow conversion between the current data
format and the DOEXT0 data format.  (DOEXT0 is the data type name
you'll see if you run a a Library Data thru usrlib & look a the -d or
-l ourput files; $ is the symbol you'll see if you run a string thru
usrlib.)  Perhaps it would even be possible to (in rpl) turn off the
clock, run, and turn the clock back on when you're done, as in
MatrixWriter, et al.  All we need is the appropriate rpl routine's
address. 

-JimC
--
James H. Cloos, Jr.		Phone:  +1 716 673-1250
cloos@ACSU.Buffalo.EDU		Snail:  PersonalZipCode:  14048-0772, USA
cloos@ub.UUCP			Quote:  <>