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