Assembly Programming for the TI-81 ---------------------------------- Writing assembly programs for the TI-81 is somewhat different from programming on other TI calculators. Although the TI-81's hardware is extremely similar to its descendants, the software differs in many ways from the later calculator OSes. This guide provides a brief overview of the TI-81's internals, and how to write TI-81 assembly programs using the Unity program loader. (This information may, of course, also be helpful for programmers interested in using another loader, or writing their own.) This is not intended as a guide for learning assembly language; I will assume that the reader already has some experience with Z80 assembly programming. The TI-81 is not a friendly environment for beginners! Programming Basics ------------------ The Unity package includes 'asm81', a specialized assembler for writing TI-81 programs. To assemble a program, simply run asm81 foo.asm Running this command will produce several files as output: foo.81: Plain text version of the assembled program, with instructions for typing it into the calculator. foo.prg: TI-81 binary version of the program, for use with a TI-81 emulator. If the program uses any ROM routines, a separate version of the file will be created for each ROM version (foo-1.6K.prg, foo-1.8K.prg, foo-2.0V.prg.) foo.lst: Assembly listing, showing the original source together with the assembled bytes and addresses. If the program uses ROM routines, a separate version of the file will be created for each ROM version. A simple assembly program looks like this: .nolist .include .list .org tempMatrix ld hl,hello_string ROM_CALL PutFlagStr ROM_CALL GetKey ret hello_string: .ascis "Hello!" Most programs should begin with the statement ".include ". The file ti81.inc contains definitions of the TI-81 ROM routines, RAM areas, flags, font characters, and more (including many more routines and RAM areas than are described in this guide.) See that file for more information. All programs must include a .org statement, specifying where in memory the program is to be loaded. The TI-81 does not have any memory areas set aside for assembly programs' use (for obvious reasons), so you must re-use an existing memory area. A few good choices: tempMatrix (288 bytes): This area is used for storing intermediate matrix results while evaluating an expression. Unless your program calls the parser, this area should be free for assembly programs to use. cmdShadow (128 bytes): This memory area stores a copy of the home screen while other apps are in use. This area is free to use while the home screen is actually being displayed. plotSScreen (768 bytes): This memory area stores the current graph, so if you put your program or data there, you'll be overwriting the graph contents. If you use this area, you should set the graphDraw flag, so that the calculator will redraw the graph if necessary. scratchMem + 5 (283 bytes): The scratchMem area is used for scratch space by various ROM routines (and you can use it as scratch space yourself, if you like.) Unfortunately, all of the text display routines (PutC, PutMap, etc.) use the first 5 bytes of this area to store a copy of the font bitmap being displayed. However, the remaining 283 bytes may be safe to use if you're not doing any complicated math. If you want to write a larger program, another option is to overwrite any of the BASIC variables you're not using. For instance, the three matrix variables, combined, make up 870 bytes; you could store your program there, then clean up afterwards by jumping to the ResetMatrices routine. Hardware Ports -------------- The original TI-81 (version 1.x) hardware ports are identical to those of the TI-85 and TI-86. See ti-ports.txt for more information about them. In brief: Port 0: Starting address for the display buffer. You might be able to display grayscale by changing this rapidly. Port 1: Keypad. Same as every other Z80 model. Port 2: Display contrast. Port 3: Interrupt status, ON key, and power control. Port 4: Timer and LCD control. Port 5: Mapping for memory bank A. Don't touch this. Port 6: Mapping for memory bank B. The ROM doesn't use this memory bank; you can map it either to RAM or to one of the two ROM pages if you feel like it. Port 7: GPIO. Doesn't do anything - there's no link port connected. The revised TI-81 (version 2.x) hardware ports are (as far as I can tell) identical to those of the original TI-82. See 82-ports.txt for more information about them. Port 0: Presumably GPIO. Port 1: Keypad. Port 2: Memory mapping. Probably works the same way as the TI-82 and TI-83, but there's really no reason to alter it. Port 3: Interrupt status, ON key, and power control. Port 4: Timer control and maybe other stuff. Port 10h: Control for the T6A04 LCD driver. Port 11h: Data for the T6A04 LCD driver. Memory Layout ------------- The TI-81's memory layout is very different from those of later calculators. Instead of a large "user memory" area where variables are dynamically allocated, most variables (even the matrices) are assigned to fixed addresses in RAM. (Most of these addresses can be found in ti81.inc.) The exceptions are programs, equations, and the {x} and {y} lists. The equations are dynamically allocated within the equMem area. They are stored in order, starting with Y1 and ending with Y3t. So, for instance, (y3Start) is both the starting address of Y3, and the ending address (plus 1) of Y2. In normal operation, (y1Start) always equals equMem, but you can set it to some other value if you like. Programs are dynamically allocated in the progMem area. Like equations, programs are stored in numerical order (prgm0 first, followed by prgm1 through prgm9, then prgmA through prgmTheta.) In normal operation, (prgm0Start) would always equal progMem, but if Unity is installed, a small amount of memory is reserved between progMem and (prgm0Start). The statistics data lists, {x} and {y}, are stored in reverse order, at the end of the program memory area. So {y}(1) will always be stored at progMemEnd - 7, {x}(1) at progMemEnd - 15, {y}(2) at progMemEnd - 23, and so forth. To obtain the address of the last item in the list (which is also the end of the free memory area), call the system routine GetStatCount followed by GetStatXPtr. As with other TI calculators, the TI-81 has a "system flags" area, which can be addressed using the IY register. (As with other calculators, the IY register is always expected to point to this area, and changing it may cause problems.) The following diagram summarizes the layout of variables in RAM: E000 ================================ Start of RAM Video memory E300 -------------------------------- System flags, variables, etc. (statically allocated.) F347 -------------------------------- progMem Unity program loader * -------------------------------- (prgm0Start) prgm0 * -------------------------------- (prgm1Start) prgm1 * -------------------------------- (prgm2Start) ... * -------------------------------- (prgmThetaStart) prgmTheta * -------------------------------- (prgmThetaEnd) Free memory * -------------------------------- progMemEnd - 16*N + 1 {x}(N) {y}(N) ... {x}(2) {y}(2) {x}(1) {y}(1) FCA7 -------------------------------- progMemEnd + 1 More static storage FCC7 -------------------------------- equMem Y1 * -------------------------------- (y2Start) Y2 * -------------------------------- (y3Start) ... * -------------------------------- (x3tStart) X3t * -------------------------------- (y3tStart) Y3t * -------------------------------- (y3tEnd) Free memory FE2F -------------------------------- equMemEnd + 1 Hardware stack 10000 ================================ End of RAM LCD Management -------------- The two hardware versions of the TI-81 manage the LCD in very different ways. The original TI-81 hardware, like the TI-85 and TI-86, refreshes the LCD directly from RAM, so whatever is stored in the videoMem area will show up immediately on the screen. The revised TI-81 hardware, like the TI-82 series, uses a T6A04 LCD driver, which is separate from the CPU. To update the screen contents, you need to call the LCD_Copy routine. This routine will be called as part of the GetKey routine, and will also be called periodically while the run indicator is enabled. Thus, if your program is interactive but doesn't use GetKey (e.g., you use GetCSC in a loop), you should either enable the run indicator (call RunIndicOn or set the indicRun flag) or call LCD_Copy periodically. ti81.inc defines the macro UPDATE_LCD, which either does nothing or calls LCD_Copy depending on the hardware version. Display Routines ---------------- The routines ClrScrn, PutC, PutMap, DispHL, NewLine, HomeUp, EraseEOL, and EraseEOW work as they do on the other Z80 calculators. The TI-81 has a peculiar, non-standard character set. The normal, "printable" characters range from 01h (space) to 7Fh (cross). The lowercase letters 'j', 'k', and 'z' are not included (since these letters are not used in any system messages or tokens.) In addition, the "small font" used for displaying values on the graph screen is in fact a subset of the large font (for instance, the small digit '2' is the same as the superscript '2' used for squaring.) The "small" set is very limited: only the characters =, -, ., 0 to 9, E, theta, R, T, X, and Y are available. For displaying normal, large-font strings, you can use PutFlagStr. This routine is similar to PutS, but instead of marking the end of the string with a zero byte, the end of the string is marked by setting bit 7 of the last character. In asm81, you can define such a string using the ASCIS directive. For example: ld hl,my_string ROM_CALL PutFlagStr ... my_string: .ascis "Blah blah" (The last line is equivalent to .db "Blah bla", 'h' | $80 but more readable. Note that asm81 is also translating ASCII characters to the TI-81 character set.) For displaying floating-point numbers, the FormReal and FormEReal routines are similar to their TI-83 counterparts, but produce a "flag"-marked string as output rather than a zero-terminated string. The small-font display routines are somewhat tricky to use; you must pass the address of the location in the screen buffer where you want the character to be displayed. The position of the character within that byte is determined by the textLeftSide flag. For instance, to display a character at row 5, column 16: set textLeftSide,(iy + textFlags) ld hl,videoMem + 12*5 + 2 ld a,ScapX ROM_CALL PutSmallChar The PutSmallFlagStr routine can be used to display numbers (e.g., strings produced by FormReal or FormEReal) in small characters: set textLeftSide,(iy + textFlags) ld hl,videoMem + 12*5 + 2 ld de,my_string ROM_CALL PutSmallFlagStr ... my_string: .ascis "3.14159" (PutSmallFlagStr can also be abused to display stuff other than numbers, but this is clearly not the intention.) Input Routines -------------- The routines GetCSC and GetKey should be familiar to TI-83-series programmers. GetCSC checks if any key has been pressed since the last call to GetCSC (the actual scanning is done by the system interrupt routine, which calls the KbdScan routine.) GetKey waits for a key to be pressed, and handles the 2nd and Alpha keys as modifiers. (If you use GetKey, the user can press 2nd+Off to turn the calculator off, which aborts the running program. Unlike many other calculators and shells, this is perfectly safe on the TI-81, provided that your program doesn't itself leave memory in an unstable state.) The ReadKeyGroup routine (known as KEY_READ on the TI-82, and similar to MirageOS's directin) can be used to scan the keypad manually. Floating-Point Math ------------------- Like later calculators, the TI-81 has both a "standard-precision" (64-bit) and an "extended-precision" (80-bit) floating-point format. Unlike later calculators, the difference is not trivial. Most math routines require extended-precision numbers as inputs, and produce extended-precision numbers as outputs. Most variables, however, are stored in standard-precision form. The "standard-precision" format is 8 bytes, containing 13 decimal digits: EE MM MM MM MM MM MM MT The "extended-precision" format is 10 bytes, containing 16 decimal digits: EE MM MM MM MM MM MM MM MM 0T In these formats, EE is the biased exponent (same as on the TI-82/83 series), MMMM... are the mantissa digits (BCD, big endian), and T is a set of flags (where 8 = negative, 4 = matrix value, 2 = value used in graphing, 1 = unknown.) The routines OP1StdToExt, OP2StdToExt, and OP1ExtToStd can be used to convert numbers between the two formats. The routines MovFPToOP1, MovFPToOP2, and MovFPFrOP1 will both convert and copy a number, so they can be helpful for copying values to and from standard-precision variables. The TI-81 does not use "variable names" in the sense that they are used by later calculators, but it is possible for an OP register to store a "matrix reference." This is the format used for inputs and outputs to the matrix math routines, as well as by certain other routines such as StoAns and RclAns. A matrix reference has the following form: -- -- CC RR LL HH -- 04 where RR is the number of rows, CC the number of columns, and HHLL is the address of the matrix data. The other byte values are ignored. Custom Interrupt Routines ------------------------- Unity allows programs to install custom interrupt handler routines, which you can use for a variety of purposes (key hooks, cursors or other status indicators, timers, grayscale emulation, whatever you can think of.) Custom interrupt routines are responsible for uninstalling themselves when necessary (whether because the program exited normally, an error occurred, or the user aborted the program using Y= + GRAPH + ON.) The easiest and safest way to do so is by using the macros defined in ti81.inc. As an example, the following code displays a continuously-increasing counter in the top right corner of the screen. Note that we save and restore (curRow), as well as OP1 and scratchMem (since the latter two are destroyed by the DispHL routine.) DEFINE_CUSTOM_INT Counter ld hl,(curRow) push hl ld de,counter_temp ROM_CALL Mov10FrOP1 ld hl,scratchMem ROM_CALL Mov5B ld hl,11 * 256 ld (curRow),hl ld hl,(counter_value) inc hl ld (counter_value),hl ROM_CALL DispHL UPDATE_LCD ld hl,counter_temp ROM_CALL Mov10ToOP1 ld de,scratchMem ROM_CALL Mov5B pop hl ld (curRow),hl ret To enable this routine, simply write: CUSTOM_INT_START Counter To disable it: CUSTOM_INT_STOP If you want a routine to only run once, you can use DEFINE_CUSTOM_INT_NO_REPEAT instead of DEFINE_CUSTOM_INT. A few important notes about custom interrupt routines: - Custom interrupt routines are called before the system interrupt routine runs (which has both advantages and drawbacks.) - Obviously, the "emergency abort" sequence (Y= + GRAPH + ON) cannot be used to abort an interrupt routine, so make your routines as simple as possible. - Custom interrupt routines MUST preserve the IX register. You may, however, destroy the AF, BC, DE, HL, AF', BC', DE', and HL' registers. - Custom interrupt routines must not enable interrupts (since that could lead to massive stack overflows) and therefore must not call any system routines (such as GetCSC) that enable interrupts. - If your interrupt routine takes a long time to complete, you will find that you are missing some timer events (this will, for instance, cause the cursor to blink more slowly.) This is probably harmless, apart from taking up a lot of CPU time and draining the batteries faster. - If for whatever reason you need to enable interrupts, or you want to allow for emergency aborts inside your routine, one option is for your routine to use DEFINE_CUSTOM_INT_NO_REPEAT, then manually re-enable itself when it's finished: DEFINE_CUSTOM_INT_NO_REPEAT BigRoutine ei di CUSTOM_INT_CONTINUE ret - If you need to do something *after* the system interrupt routine runs, you can do the following: DEFINE_CUSTOM_INT_NO_REPEAT PostRoutine rst 38h CUSTOM_INT_CONTINUE pop hl jp _PopHLDEBCAFRet Be very careful, however - this sort of thing can easily get you into an extremely nasty infinite loop. It's also possible to create a persistent interrupt routine that will stick around after your program exits, but in order to do that, you'll need to allocate a permanent memory area (most likely somewhere in program memory) to use for the purpose. This is rather complicated to do correctly, so I will leave it as an exercise for the interested reader.