TI-85 Assembler Programming Guide
Part I : Introduction
This page intends to give you some help in programming ZShell, Usgard, PhatOS, or Rigel games. Part II describes some topics essential for all programmers. If you don't understand what I am talking about in Part II, look in part III and hopefully it will explain it. Part III provides a tutorial of TI-85 programming and shows in detail how to make some simple programs. P This information is (to the best of my knowledge) reasonably accurate, but I am making no guarantees about it. Always make suure you have backed up all data on your calculator that you need to keep before you start experimenting with assembly programming. If you see any errors or omissions in this document, or want to ask me something about programming, E-Mail me!!
Part II : Essential TI-85 Programming Information
What you need to get started
Unlike with BASIC, you will need more than just your calculator to program games. You need:
- A TI-85 (never would have guessed)
- A computer with a graph-link
- Software to run the graph link
- ZShell, Usgard, PhatOS, or Rigel
- A Z80 assembler (for PCs, TASM)
- ZShell reference documents : TI-RAM.TXT, TI-PORTS.TXT, Z80ISS.TXT, ZSFNLIB.TXT and the included documentation of PhatOS, Usgard, or Rigel if your're using one of them
All of the software and reference documents can be downloaded at ticalc.org. You can also get the instructions for making a your own graph-link there. You should print out all of the reference documents, as you will need them!
Binary and Hexadecimal
To program the TI-85 in assembler, you will need some knowledge of binary and hexadecimal number bases. Math teachers usually want you to think that this is extremely difficult, but they are just trying to fool you! It's EASY!
In our usual decimal system, each digit of a number can have ten values (0-9). So, using the decimal system, we count like this : 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10... Wwwooaahhh! Something strange seems to have happened between those last two numbers. It sure did! Whenever the units digit must go past its maximum value, you set it back to zero and then increment the digit to the left of it (in the case here, that digit was originally a 0). The second digit reprents a number of tens, making it more significant than the first. This amazing phenomenon is called place value. Let's try counting some more : 96, 97, 98, 99, 100... as you see, the next time the units digit reaches nine, you must increment the next digit to the left. But, when it is already nine (the highest it can be in the decimal system), you set it to zero and increment the next digit to the left. This digit has a value of a hundred (that is, you multiply it by a hundred to find its value).
The binary system works in about the same way, but only uses the digits 0 and 1. Thus you count in binary like this : 0, 1, 10, 11, 100, 101, 110, 111, 1000, 1001, 1010... using the same method as in the decimal system. This time, however, the highest digit you can use is 1. Thus the second place (from the left) has a value of 2 (in decimal). The next place has a value of 4, then 8, 16, 32, 64, 128, 256... Binary numbers are usually denoted by preceding the value with a % or following it with a b, such as %1110 (14 decimal) or 10001b (17 decimal). Want to double a binary number? Add a zero to the right side. Multiply by four? Add two zeros. It works just like adding zeros to multiply by 10 and 100 in decimal (as 2 and 4 are %10 and %100).
The hexadecimal system gives you 16 digits, 0-9 and then A-F. Hex numbers are indicated by a preceding $ or a following h. The values of the hex digits are like this:
Hexadecimal | Binary | Decimal |
---|
$0 | %0000 | 0 |
$1 | %0001 | 1 |
$2 | %0010 | 2 |
$3 | %0011 | 3 |
$4 | %0100 | 4 |
$5 | %0101 | 5 |
$6 | %0110 | 6 |
$7 | %0111 | 7 |
$8 | %1000 | 8 |
$9 | %1001 | 9 |
$A | %1010 | 10 |
$B | %1011 | 11 |
$C | %1100 | 12 |
$D | %1101 | 13 |
$E | %1110 | 14 |
$F | %1111 | 15 |
The second place in a hex number has a place value of 16. The third place, 256, and the fourth, 4096. Converting between binary and hex is easy, as each hex digit matches a specific sequence of binary digits. If the length of a binary number you want to change to hex is not a multiple of 4, just add zeros on the left side...
You do not in any way need to learn to do number base conversions! Your TI-85 can do this for you. Just hit [2nd] and then [1] to get into the hex menu. [F1] takes you into a menu where you can get the special A-F digits for use in hex numbers. [F2] gives you the suffixes b and h which you can add to numbers to show that they are binary or hex. [F3] gives you a conversion operators which will change the result to a number in a specific base.
Bits, Nybbles, Bytes, and Words
A bit is a single binary digit. It can be a 0 or 1, and represents the smallest unit of data. A bit is "set" if it contains the value of 1, and "reset" or "cleared" if its value is 0.
A nybble is 4 binary digits, from %0000 to %1111. It can be represented as one hex digit.
A byte is 8 bits, or 2 nybbles. Most Z80 commands operate on at least one byte at a time. This is two digits long in binary and 8 digits long in hex. The most significant bit is the one with the highest place value, in this case 128. The least significant bit is the units digit. The bits are numbered with the least significant as bit 0 and the most significant as bit 7.
A word consists of 2 bytes. These can be represented by four hex digits. The Z80 processor uses words to represents memory addresses. The bits in a word are numbered with the least significant as bit 0 and the most significant (with a place value of 32,768) as bit 15.
Two's Complement
Negative numbers in binary are represented by what is called two's complement. To negate a number, just invert every bit and add one. For example, 5 is represented by the byte %00000101. To negate this, just invert it (%11111010) and then increment it (%11111011). This binary numer is -5 in decimal (it's also 251). When you add these two bytes, you get %100000000. However, since you were doing a byte operation, only the lower 8 bits are kept, giving 0. Two's complement basically stores a negative value as 256 more than the value (for bytes) or 65536 more than the value (for words). The result is always 256 (or 65536) greater than the actual result should be, but it comes to the right value because the high bit is ignored. Actually, it isn't ignored since it's stored in the carry flag (will be discussed later), but it is not part of the resulting number.
TI-85 Memory
The TI-85 has 32 kilobytes (32,768 bytes) of RAM (memory contains your programs, and can be modified) and 128 kilobytes (131,072 bytes) of ROM (which contains all the built-in TI-85 software that cannot be changed). Since the Z80 processor stores all memory addresses as words, the range of memory it can address is from $0000 to $FFFF. This is not enough for all the TI-85's RAM and ROM, so the ROM is addressed through bank switching. This means that only a piece of the ROM is accessed at a time, and you must switch a different part into position to read it. The TI-85 memory architecture is like this:
Start | End | Size in bytes | Contents |
---|
$0000 | $3FFF | 16,384 | ROM (always shows first part of ROM) |
$4000 | $7FFF | 16,384 | ROM (this part can be switched to show any section |
$8000 | $FFFF | 32,768 | RAM |
Since the positions of routines in ROM (with a few exceptions) are different on different calculators, you cannot call ROM routines directly. Instead, you must use the ZShell ROM_CALL function. Some addresses in RAM are constant, but your program can be moved anywhere in RAM at any time! For this reasons, you cannot reference addresses within your own program directly under ZShell. Specially written PhatOS programs are, however, always loaded at the same address (current $8e57). They must always directly reference addresses within the program. In order to tell the assembler where the program is, you must start the program with ".org $85e7". Instead, you must read the value of (PROGRAM_ADDR) and add that to the address when trying to get a data address, or the special JUMP_ and CALL_ functions to redirect program flow. Under USGARD, you can reference these addresses directly, which is must faster, but must put an & in front of the label name to tell USGARD to relocate it. Programs written under PhatOS are always loaded to the same address at all times. Because of this, you don't need any relocation at all, and don't even need the &. Under PhatOS or Rigel, you should never use anything referencing PROGRAM_ADDR. You must put ".org $8e57" at the start of a PhatOS program and ".org PROG_START" at the top of a Rigel program, and use only direct addressing. RAM addresses which can be used are:
Start Address | Size in bytes | Use |
---|
$80DF | 168 | Text memory. This is always set to all zeroes by ZShell before your program is started. |
$8010 | 100 | DELc buffer. This stores the text you last deleted with DELc in the program editor. This is not initialized to any specific value. You can use it for your own storage, but must set $800F to zero so the user won't try to undelete it and get garbage out. |
$8641 | 1024 | Graph memory. This stores the image on your graph screen. It is not initialized to any speicfic value when your program is started. If you use this for your data, the graph screen will be scrambled after you exit the program. Since many users may not like to see this, you should execute the statement "set 0,(iy+3)" at some point during your program to tell the OS to redraw the graph screen the next time it is displayed. |
$FC00 | 1024 | Video memory. This holds the image that is actually displayed. The TI-85 LCD has 128 columns and 64 rows, a total of 8192 pixles. Each byte in the range of $FC00 to $FFFF hold the value of a row of 8 pixels. $FC00 hold the first 8 pixles in the upper left corner, and $FFFF the 8 pixels in the lower right corner. It takes 16 bytes of the display memory to form one line of the display. The leftmost pixel is controlled by bit 7, and the rightmost pixel is bit 0. When the bit is set (has a value of 1), the pixel is black. When the bit is cleared (has a value of 0), the pixel is not shown. |
Z-80 Architecture
The Z-80 processor (which is in the TI-85) is a very simple processor. It has few registers and a very limited instruction set. It doesn't even have a multiply instruction at all!
Like most microprocessors, the Z-80 does most of its calculations in registers. Registers are small temporary storage areas within the processor itself. They are usually used to hold values that are currently being worked with. The Z-80's registers are:
- A (Accumulator) - This is the "main" register in the Z-80. It is usually used to store temporary results during calculations. Many instructions always use this register as their destination. This register is 8 bits in size.
- B - This is another 8 bit register. It is used as the counter for DJNZ loops (see below).
- C - This is another 8 bit register. It is sometimes used to store a port number for port-based I/O (will be explained later.)
- D - This is another 8 bit register.
- E - This is another 8 bit register.
- H - This 8 bit register is usually combined with L.
- L - This 8 bit register is usually combined with H.
- BC - This 16 bit register uses B for the "upper byte" (the 8 most significant bits) and C for the "lower byte" (the 8 least significant bits). It can be used as a pointer. A pointer stores a number which indicates a location in memory. It is said to point to that location because it contains the address of it. To reference the register itself, use BC. To reference the memory it points to, use (BC).
- DE - This 16 bit register uses D for the upper byte and E for the lower byte. It is used in a way similar to BC.
- HL - This 16 bit register uses H for the upper byte and L for the lower byte. It is used in a similar manner as BC and DE, but more operations are allowed with it.
- IX - This is a 16 bit index register. In addition to being able to read the memory it points to, you can also read the memory above and below it with an offset. For example, you can reference the memory address 10 bytes beyond what IX points to with (IX+10).
- IY - This 16 bit register is used in a way similar to that of IX. However, the TI-85 ROM routines expect this to always point to $8346. If you change this, be sure to set it back before calling any ROM routines!
- PC - The most important register! PC stands for Program Counter. It contains the address in memory where the program is currently being executed. What the processor does all of the time is this:
- Read in the instruction pointed to by the PC.
- Increment the PC to point to the instruction after the one just read.
- Execute the instruction just read.
- Go back to step 1
- SP - Stack Pointer. This register points to an address for the stack, a temporary storage of values. When a value is "pushed" onto the stack, the stack pointer is decremented and the value is stored where the stack pointer points to. When a value is "popped", the value pointed to by the stack pointer is read, and the stack pointer is incremented. You can store many values on the stack, as long as you be sure to pop them off when you are done! If you continuously push things without popping them back, the stack will eventually overflow. This corrupts your memory and usually locks up the calculator. When a subroutine is called, the current address is pushed on the stack. When you return from the subroutine, the value is popped back off the stack into the PC, allowing execution to continue right after where the subroutine was called.
- F - Flags. This very important register gives information about the result of the last calculation. It has a "zero flag" which is set when the result of the calculation is zero. It also has a "sign flag" which tells you whether or not the result was negative, and a "carry flag" which tells you when the result was too large to fit in the destination register. The carry basically is a copy of the highest bit of the result. Not all instructions set all of the flags, so be sure to check in Z80ISS.TXT before depending on one of these! Note that the carry flag has some special uses. For example, the bit shifting instructions copy the bits shifted out of the register into the carry flag. Due to the nature of two's complement arithmetic (see above), the carry flag is set after a subtraction if the value subtracted is less than or equal to the value in A (the Z-80 subtraction instruction only works on A). WARNING: I've had problems with this myself. Maybe some time I'll get around to experimenting with this enough to figure out how it really works. Until then, be careful because what I said about using the carry flag in this case just might be wrong! If you just want to compare the value with A without changing it, you can use the "CP" instruction which sets the flags in the same way but changes no registers.
Control Statements
In Z-80, the program flow is controlled by special instructions which conditionally alter the program flow. Many of these rely on the data in the flags register.
- Relative jumps (using the JR instruction) - these add or subtract a small value from the PC. A simple "jr label" always jumps to the label. You can also jump conditionally with instructions like "jr nz,label" which only jumps if the zero flag is not set. Other conditions are z (zero flag is set), c (carry flag is set), and nc (carry flag is not set).
- Long jumps - The Z-80 can normally jump to any absolute address in memory. However, since TI-85 programs can be moved anywhere in memory, you must use the special ZShell functions. These are much less efficient than releative jumps. Use them only when the distance is too great for a relative jump to work. The basic function is JUMP_(label). You can use the same condition codes as with jr, allowing commands like "JUMP_NZ(label)". This is not necessary under USGARD! If writing for USGARD, just do something like "jp &label" or "jp nz,&label" and USGARD automatically relocates the reference to point to the right place. Under PhatOS or Rigel, you must use only "jp label".
- Subroutine calls - These work in a similar way to long jumps. However, they push the PC onto the stack, allowing the routine called to return to exactly where it was called from. This is useful is you have a routine you want to use in many places in your program. Like before, you must use special ZShell functions, such as "CALL_NZ(label)". There is also a special type of call in ZShell which calls a ROM routine. It is accessed like "ROM_CALL(index)", where index is the name of a special index value that ZShell uses to determine which routine to call. Almost all ROM functions must be called this way, but some must use a direct call, like "call CP_HL_DE". Again, USGARD offers a better solution - just use "call &label" or "call nz,&label" in your program. USGARD and PhatOS also allow you to do direct calls to all ROM routines, so you can just do "call D_ZT_STR", etc. and never have to use ROM_CALL. Under PhatOS, you just do "call label" with nothing extra. PhatOS does, however, require ROM_CALL to call ROM routines.
- Returns - These pop the PC off the stack. This is useful for returning from a subroutine that is called from many places, as it always goes back exactly where it left off. The basic form is "ret", although you can also use conditional returns, which only return if the condition codes have certain values, like "ret nz". "Ret" can use all of the condition code "jr" can, and a few more (m for minus, pe for positive or equal, etc.). This instruction always takes the top value on the stack for the program counter. If you have pushed something else on the stack and not popped it off before returning, this will cause your calculator to jump to the address pointed to by whatever was on top of the stack. This usually locks up the calculator very quickly. Be careful!
Part III : Tutorial
More lessons will be coming!!!!
Part IV : Your Turn!
The information above will hopefully give you a start in TI-85 assembler programming. The only way you can truly learn it is to try it yourself. If there's anything this world needs, it's more TI-85 games! So code as many games as possible, and be sure to spread your work for the benefit of the rest of the world!