Part Nine:
Absolute Addressing and the VAT

    Now the problem arises of how to reference banked memory: a two byte address pointer will not suffice.  TI has devised a scheme they call absolute addressing that uses a three byte pointer to cover all memory, banked and unbanked; the system of using an 8-bit pointer with swapped pages is called ASIC (Asynchronous Serial Communications Interface).  Many ROM calls take input or give output of the absolute kind (see ti86abs.inc), usually in the simulated 24-bit register groups of AHL or BDE.  For instance, <call _GETB_AHL> will retrieve the byte whose absolute address is contained in AHL.
    Here's how the system works: $000000 to $00ffff is the standard ASIC address -- whatever page is swapped will be at that address; $010000 to $02bfff are RAM pages $41 to $47, in order, respectively; $02c000 to $05ffff are ROM pages $01 to $0d respectively (pages $0e and $0f are blank).
    That's great, you may think, but what can I do with these?  Well, first, if you want to be able to manipulate external variables -- create, read, manipulate strings, programs and reals -- you have to know how to access them.  You could swap pages, but because you don't control where a variable begins or ends (user memory is different on every calculator), you don't know whether it lies among multiple pages, a problem that is magnified when you want to copy large amounts of data into a variable.  In the end, it turns out that our lives are greatly facilitated by absolute addressing, as you will see.

    Before I get into variable manipulation, there are few more instructions that need to be explained.  First are the block transfer and search instructions.  Block transfer instructions provide an easy method of copying large amounts of data -- much faster than could be done with a loop.  These instructions take three input factors: 1) a source, HL should hold a pointer the ASIC address from where you want to the information to be copied from; 2) a destination, DE (a quick mnemonic: DEstination) should hold a pointer to where you want the information to be copied to, 3) the number of bytes to copy, determined by what 16-bit value is in BC.

 ld hl,source                           ;put a 16-bit pointer to the source into HL
 ld de,new_storage                 ;a 16-bit pointer to the destination into DE
 ld bc,source_end - source     ;& a 16-bit length into BC
 ldir                                        ;copy

    The compiler can easily determine the length by subtracting the pointer to the source's end by the pointer to the source's beginning -- this is done during compilation, not during execution.  Anyhow, once we have the input set up, we can provide the instruction.  In the above example, we want to copy each byte by incrementing the pointers for each single copy; the instruction for this is: ldir; load, increment and repeat, which copies a byte, increments both pointers, decrements BC and repeats until BC becomes 0.  Another instruction, <lddr>, does the same thing, save that it decrements both pointers after each byte it copies.  As far as I can tell, you can use ldir and lddr interchangably (the input must be changed to pointers addressing the end of the destination for lddr) and neither has a practical benifit over the other.
    Two similiar instructions are ldi and ldd, which input a source and a destination in HL and DE, but only copy a single byte, though also incrementing / decrementing the two pointers afterwards.

    There are also block search instructions, useful in search and compare algorithms.  Again, there are two main instructions: cpir, cpdr, compare, inc / dec, repeat.  Cpir will execute <cp (hl) / inc hl> and then repeat until either A matches (HL) or until BC becomes zero.  To find which occured, you can test either the P / V flag or the Z flag.  The zero flag will be set iff the comparison results in a match and the parity overflow flag will be set iff BC has become zero.  There also exist two instructions, cpi and cpd, that do not repeat.
    Example of string comparison call:

compare_str:                 ;compare length byte string at HL to string at DE
 ld a,(de)                       ;get length byte of string at DE
 ld c,a                            ;BC will need length to continue for cpi
 sub (hl)                         ;compare it to length byte of HL string
 ret nz                           ;return with z flag reset if they are not the same
 inc hl                            ;point HL to first byte of string
 inc de                           ;point DE to first byte of string
 ld b,a                           ;put zero into B; the entire length fits in C, so this needs to be 0

;note: we know that A is zero because because the subtraction made it zero (if it wasn't zero, we ;would have returned)

compare_loop:             ;the loop begins
 ld a,(de)                      ;get the current byte of string 2
 inc de                          ;point to the next byte
 cpi                              ;cp (hl), inc hl, dec bc
 ret nz                           ;ret if no match with the zero flag reset
 ret po                          ;ret if strings completed compared with z flag set
 jr compare_loop          ;if neither case is true, compare the next byte

    This call takes the input of two length byte strings pointed to by DE and HL to be compared.  As output, it will set or reset the zero flag: Z indicating that the strings do, in fact, match; NZ indicating that the strings do not match.  Cpi is used instead of cpir because each time DE needs to be incremented each time HL is; if cpir could be used, there would be no need for recursion.

    The VAT, or the Variable Allocation Table, is a table located on the end of RAM page 7 that's contains the names of and the absolute pointers to every user variable on the calculator.  The first byte of this table is at $bfff and the table moves down until all variables have been accounted for.  Each variable entry has the following ordered format: object (variable) type in bits 0 through 4 (see table below) and variable flags in bits 5 through 7 (to get the type, AND a bitmask of $1f); a three byte absolute pointer to the variable; an unused byte (some shells have taken advantage of the unused byte by using it to represent which folder a variable lies in); a length byte string of the variables name (maximum of 8 bytes, not including the length byte).
 
 
 

Object Type Value Object Value
$00 Real
$01 Complex
$02 Vector (Real)
$03 Vector (Complex)
$04 List (Real)
$05 List (Complex)
$06 Matrix (Real)
$07 Matrix (Complex)
$08 Constant (Real)
$09 Constant (Complex)
$0a Equation
$0b System Use
$0c String
$0d GDB (Function)
$0e GDB (Polar)
$0f GDB (Parametric)
$10 GDB (Differential)
$11 Pic
$12 Program
$13 Conversion
$14 System Use
$15 System Use
$16 System Use
$17 System Use
$18 System Use
$19 System Use
$1a System Use
$1b System Use
$1c System Use
$1d System Use
$1e System Use
$1f System Use

    Here's what the VAT would look like if a variable Foo of type string was the only variable on the calc:

bfff: $0c               ;object type for string
bffe: $00              ;least significant abs addr byte
bffd: $80              ;abs addr = $018000
bffc: $01              ;most significant abs addr byte
bffb: $00             ;unused
bffa: $03              ;length byte for variable name
bff9: $46             ;ASCII code for 'F'
bff8: $6f              ;ASCII code for 'o'
bff7: $6f              ;ASCII code for 'o'

    You will never actually have your own variable as the first VAT object, because things like xStat and Ans will take that honor.  If you want, you can take a look at your own calculator's VAT by using a memory editing program (I like Joshua Gramms' jmv ... available at all major distribution sites) to look at the end of RAM page 7.

    The manipulation of VAT objects should be done exclusively with ROM calls.  The only case where you might consider doing it directly would be if you wanted to change a name to another of the same number of characters.  The most important of the calls is _FINDSYM, which will search the VAT for a variable name, the carry flag will be set iff the variable is not found, and the absolute address of a found variable will be returned in BDE, a pointer to the VAT entry in HL, and object type in A.  The input of the variable name is taken from the RAM location _OP1+1.  Other calls are _delvar, which deletes the variable whose name is at _OP1+1, and a list of create variable calls (see ti86asm.inc) that will create a variable with a given length and name at _OP1+1.  (OP1 is an eleven byte section of RAM at $c089 that will hold object type, length byte, string.  I say _OP1+1 because the object type isn't necessay.)

 ld hl,progname-1              ;load hl with a pointer to the string and where a type would be
 rst $20                             ;load _OP1 with ten bytes at HL
 rst $10                             ;call _FINDSYM
 call nc,_delvar                  ;if variable found, delete it
 ret

progname:
.db $5,"whamo"                ;length byte string

    Rst $10 is a short (1 byte) way of doing <call _FINDSYM>; rst $20 is a short way of calling a routine called _MOV10TOOP1.  Rst is a like instruction to a call, but only one byte in length and only capable of calling these addresses: $00, $08, $10, $18, $20, $28, $30, $38.  At address $0010 in ROM there is this instruction: <jp _FINDSYM>.  <call $10> would do the same thing as <rst $10>, but would occupy an additional two bytes of code.
    When a variable name is loaded into op1, it uses the following syntax: object type - 1 byte; variable name length byte - 1 byte; variable name - between 1 and 8 bytes.  I don't know of any calls that require the type to be input for a variable so this byte can have any value.  <ld hl,progname-1> will make HL point to the byte before progname (which is actually <ret>) and rst $20 will copy the return opcode ($c9) into where the type would go.  I could put a dummy object type byte before $5 and then just point to progname, but that would occupy an extra byte unnecessarily.
 

.org _asm_exec_ram

 ld hl,string_name-1                    ;pointer to string name minus byte for type
 rst $20                                      ;move name to op1
 ld hl,string_end-string_start        ;load length into HL
 push hl                                      ;save length for later
 call _CREATESTRNG             ;create new string object of size HL whose name is in op1
 ld a,b                                        ;all calls to create variables leave an absolute pointer in BDE
 ld h,d                                        ;we want to move that pointer into AHL
 ld l,e
 call $4c3f                                  ;inc AHL twice; make AHL point past length bytes of object
 call _SET_ABS_DEST_ADDR       ;set absolute destination address (AHL) for absolute ldir call
 xor a                                         ;another quick & small method of <ld a,0>
 ld hl,string_start
 call _SET_ABS_SRC_ADDR  ;set absolute source address (AHL) for absolute ldir call
 pop hl                                       ;get length back, A still zero
 call _SET_MM_NUM_BYTES       ;set absolute number of bytes (AHL) to be copied in abs ldir
 call _mm_ldir                             ;absolute ldir call; move our data into new string object
 ret

string_name:
.db 7,"LikeIke"

string_start:
.db "I know that rhinos are stupid, "
.db "but is that any reason for them "
.db "to give up ASM?"
string_end:

.end
.end

    This is a program that will create a new string variable called LikeIke containing the text under string_start.  The routine goes: load name into OP1, get the length and call create string, leaving an absolute pointer to the new string object (not a pointer to its VAT entry); set up 24-bit absolute address pointers and 24-bit absolute length; call _mm_ldir to copy the data into the string.  The three calls to set up absolute input for _mm_ldir copy the input into the RAM locations with equates of _ABS_SRC_ADDR, _ABS_DEST_ADDR and _MM_NUM_BYTES (see ti86abs.inc) without destroying any registers.  Since _CREATESTRING returns a pointer to the variable it creates in BDE, we can use that as our absolute destination address; first, though, the pointer needs to be moved into AHL and incremented by two (the first two bytes of a string object hold the string's length).  Because our source is on page 0 (all asm programs are executed on page 0), we can just load HL with the ASIC address and then make sure that A is zero for an absolute address around $00d780.  We also already loaded the length of the code into HL when we created the new string and we pushed, so it just needs to be popped back into HL; A is still zero, just as we need it for AHL to be the absolute length (our string can't be more than 64k).  _mm_ldir now exectues; it's the same as ldir except that it will copy an absolute source to an absolute destination (if your data needs to be copied to the end of page 3 and to the beginning of page 4 because of the memory on a particaular calc, then _mm_ldir will handle that).
    You should also note that instead of <call _mm_ldir / ret>, we could write just <jp _mm_ldir>, saving a byte.  Any return instruction that has a call the line above it should be modified in this manner.

    The final routine I'm going to show you will search the VAT for strings -- these strings are special, though, because the first character is the invalid character '!' (the calc's own search routine won't pick these up, but a shell will).  Normally, you would want to create a table of string names (or do something with the found strings), but this example will just count up the number of strings it finds.

VAT_search:
 ld hl,$bfff                      ;point to end of VAT (or start of VAT, depending on your perspective)
 ld d,h                            ;copy HL into DE
 ld e,l
 ld bc,($d29b)               ;this RAM location holds a pointer to the end of the VAT
 and a                            ;reset carry
 sbc hl,bc                      ;subtract VAT stsrt from VAT end to get VAT length
 ld b,h                           ;result is in HL, move it to BC
 ld c,l                            ;BC = length of VAT
 ex de,hl                        ;HL = $bfff (this instruction swaps DE and HL)
cp_outer_loop:
 ld a,$0c                        ;looking for strings
cp_loop:
 cpdr                             ;cp (HL), dec HL, dec bc
 jp po,done_cp_loop     ;parity is odd when bc = 0
 ld d,h                           ;string found, check if it is one of our strings
 ld e,l                            ;save HL because the search continues later
 dec hl                           ;dec HL until it points to the string name
 dec hl
 dec hl
 dec hl
 dec hl
 ld a,(hl)
 cp '!'                             ;if the first char of the string is '!'
 ex de,hl                         ; then its our string
 jr nz,cp_loop                ; else continue loop, <ex de,hl> doesn't affect flags
 call store_string             ;we found a string, so store it
 jr cp_outer_loop           ;see if we can't find more

store_string:                   ;here's where you want to implement a table, but I'll just count the strings
 ld de,string_count         ;point to area of mem we can use as a counter
 ld a,(de)                       ;load current counter into A
 inc a                             ;increment
 ld (de),a                       ;put it back
 ret

done_cp_loop:
 ld de,string_count
 ld a,(de)                      ;get string count
 ld l,a                            ;we want it in AHL, it will fit in just L
 sub a                           ;so put zero into A and H
 ld h,a
 jp $4a33                      ;call to display AHL as a decimal number (also ret)

string_count:
.db 0                            ;start with string count as zero