_NOSTUB ASSEMBLY PROGRAMMING FOR THE TI-89/92+
Written for Ti-Fr by Kevin Kofler.
Version 1.02
This is the English version. The French original written for Ti-Fr is here and
at Ti-Fr.
I) Goals of this tutorial
This tutorial aims at teaching beginners at assembly (not C) programming, or assembly programmers who have experience only at writing programs which need a kernel, how to write programs which do not need any kernel (called "_nostub programs"). It does not aim at teaching the Motorola 68000 assembly language itself or the ROM_CALLs of the Advanced Mathematics Software (AMS) operating system. For that, please refer to the following links:
II) Differences between the kernel mode and the _nostub mode
The most relevant differences between the kernel and _nostub programming modes are the following:
To sum up, a _nostub program is easier to use than a kernel-dependent program, and it can do everything a kernel-dependent program is able to do, as long as one knows the appropriate programming techniques, which are sometimes different. Do not get discouraged if the aforementioned list of things to take care of looks complicated to you. It is much easier than what one might thing at the first sight. I hope that after reading this tutorial, you will also program in _nostub mode like me.
III) The bases of _nostub programming
III.1) Before you start
Before you start, you will have to download the utilities needed for programming, in other words the most recent version of the TIGCC package. (We will mainly use the TIGCC IDE development environment, the OS.h header file, the A68k assembler and, in section V, the TIGCCLIB static library.)
III.2) Getting started at _nostub programming
We will begin with writing an empty _nostub program. Thus, you will have to open TIGCC IDE and to create a new A68k assembly project. You will be able to use that same project for everything which follows. Here is the program:
include "OS.h" ;include file for _nostub programs, containing especially the ROM_CALLs xdef _nostub ;no kernel required xdef _ti89 ;This program runs on a TI-89. xdef _ti92plus ;This program runs on a TI-92+. rts
Note that there is no _main: nor xdef _main. In fact, that is totally optional in _nostub assembly since the execution always starts at the beginning of the program. It is possible to put a _main label directly after the xdef instructions, before the rts. It would be a nonsense to put it anywhere else. In order to start the execution somewhere else, it is enough to put a bra beginning directly after the xdef instructions.
Another particularity: there is no END instruction at the end. That has nothing to do with the _nostub mode, but the latest version of A68k does not require it anymore, so we can leave it out.
III.3) A few words about the register saving
In _nostub mode, the registers have to be saved manually. We will see how to do that with a very simple example: a program which only waits for a keypress.
The easiest solution is to save all the registers on the stack:
include "OS.h" xdef _nostub xdef _ti89 xdef _ti92plus movem.l d0-d7/a0-a6,-(a7) ;save ALL registers ROM_CALL ngetchx ;call the ngetchx ROM_CALL ;The next section (III.4) will show you how to optimize this call. movem.l (a7)+,d0-d7/a0-a6 ;restore ALL registers rts
However, a program is allowed to modify the registers a0-a1/d0-d2. (WARNING: The ROM_CALLs are also allowed to modify those registers!) The example can thus be reduced to:
include "OS.h" xdef _nostub xdef _ti89 xdef _ti92plus movem.l d3-d7/a2-a6,-(a7) ;save the registers ROM_CALL ngetchx ;call the ngetchx ROM_CALL ;The next section (III.4) will show you how to optimize this call. movem.l (a7)+,d3-d7/a2-a6 ;restore the registers rts
Moreover, you have to save only the registers you actually use. (Since the ROM_CALLs can destroy only the registers a0-a1/d0-d2 which you do not have to save, you do not need to take care of those. However, BEWARE, the ROM_CALL macro destroys the register a4!)
include "OS.h" xdef _nostub xdef _ti89 xdef _ti92plus move.l a4,-(a7) ;save a4 (here, move is enough because there is only 1 single register) ROM_CALL ngetchx ;call the ngetchx ROM_CALL ;The next section (III.4) will show you how to optimize this call. move.l (a7)+,a4 ;restore a4 rts
III.4) How to optimize the ROM_CALLs in _nostub mode
As we have seen in the previous example, the ROM_CALL macro allows you to easily do a ROM_CALL (like jsr doorsos::ngetchx in kernel mode). However, that macro destroys a4 in order to place the address of the function there. This might be useful when calling the same function more than once in a row, but most of the time, it is not the ideal solution. So, let's take a look at how the ROM_CALL macro is defined and why it is defined in that way. Here is a commented extract of OS.h:
ROM_CALL macro move.l $C8,a4 ;places the address of the ROM_CALL jump table into the register a4 move.l \1*4(a4),a4 ;computes the address of the function and places it into a4 jsr (a4) ;calls the ROM function endm
A first idea is to replace a4 with a0. This has 2 advantages:
The only disadvantage is that if you want to call the same function a second time in a row, you will have to redo all the calculations, whereas for the first method, jsr (a4) is enough. But let's step to the implementation:
include "OS.h" xdef _nostub xdef _ti89 xdef _ti92plus ;No registers have to be saved here. move.l $c8,a0 ;places the address of the ROM_CALL jump table into the register a0 move.l ngetchx*4(a0),a0 ;computes the address of ngetchx and places it into a0 jsr (a0) ;calls ngetchx rts
However, for a program calling many ROM_CALLs, there is an even better method: you can
place the address of the ROM_CALL jump table into a register which will not be destroyed
by the ROM_CALL and which we will not destroy afterwards. That is also the trick used in
C by the OPTIMIZE_ROM_CALLS directive of TIGCC.
include "OS.h"
xdef _nostub
xdef _ti89
xdef _ti92plus
move.l a5,-(a7) ;save a5
move.l $c8,a5 ;place the address of the ROM_CALL jump table into the register a5
;This has to be done only ONCE, AT THE BEGINNING of the program.
;We will then reuse a5 for each ROM_CALL!
move.l ngetchx*4(a5),a0 ;compute the address of ngetchx and place it into a0
;We do NOT use a5 here! a5 must stay constant.
jsr (a0) ;call ngetchx
move.l (a7)+,a5 ;restore a5
rts
Afterwards, for each ROM_CALL, it will be enough to use:
move.l ngetchx*4(a5),a0 jsr (a0)
This allows you to reduce the size required for each ROM_CALL to 6 bytes. The kernel mode jsr doorsos::ngetchx uses between 8 and 10 bytes (8 bytes per call + 2 extra bytes for each different ROM_CALL). Because of this and of the fact that there is no "stub", a well-programmed _nostub program can be smaller than an equivalent kernel mode program.
III.5) ROM_CALL2 or the AMS global variables
In the previous paragraph, we have seen some ROM_CALLs which represent functions. However, there are also some of them which correspond to variables, like for example FirstWindow. We will see how to use them by the means of a program which will only place that address into a2 for a moment. (We will not see here how to display it. That is possible using the sprintf and DrawStr ROM_CALLs, which you will learn to use in the next paragraph.) For those ROM_CALLs, the easiest way to use them is the ROM_CALL2 macro, which, like ROM_CALL, destroys a4. Here is the program:
include "OS.h" xdef _nostub xdef _ti89 xdef _ti92plus movem.l a2/a4,-(a7) ;save a2 and a4 ROM_CALL2 FirstWindow ;place the address of the FirstWindow variable into a4 move.l (a4),a2 ;place the content of the FirstWindow variable, a pointeur to a WINDOW structure ;describing the current window, into a2 ;Here, we would use a2. movem.l (a7)+,a2/a4 ;restore a2 and a4 rts
Now, let's take a look at how the ROM_CALL macro is defined and why it is defined in that way. Here is a commented extract of OS.h:
ROM_CALL2 macro move.l $C8,a4 ;places the address of the ROM_CALL jump table into the register a4 move.l \1*4(a4),a4 ;computes the address of the function and places it into a4 ;Contrary to ROM_CALL, there is no jsr (a4) to call the fonction here, but the address of the ;variable is placed into a4 and can be used. endm
We can thus use the same optimization techniques as in the previous paragraph, i.e. either simply replace a4 with another register like a0 which we do not have to save or a2 which we will destroy anyway since it is the destination register, or use a constant register for the jump table. The latter is the most interesting solution, so here is the adapted program:
include "OS.h" xdef _nostub xdef _ti89 xdef _ti92plus movem.l a2/a5,-(a7) ;save a2 and a5 move.l $c8,a5 ;place the address of the ROM_CALL jump table into the register a5 ;This has to be done only ONCE, AT THE BEGINNING of the program. ;We will then reuse a5 for each ROM_CALL! move.l FirstWindow*4(a5),a2 ;compute the address of FirstWindow and place it into a2 ;We do NOT use a5 here! a5 must stay constant. ;I have chosen a2 because it is the chosen destination. move.l (a2),a2 ;place the content of FirstWindow into a2 movem.l (a7)+,a2/a5 ;restore a2 and a5 rts
III.6) How to use the ROM_CALLs
In the previous examples, we have taken a look at 2 ROM_CALLs: ngetchx and FirstWindow. But those are only 2 of the hundreds of ROM_CALLs of AMS. Of course, it is out of discussion to document them all here. For that, please refer to the TIGCC documentation. That documentation also contains a section labeled Information for Assembly Programmers, which explains how to use them in assembly. However, I will repeat the most important pieces of information here:
We now have enough knowledge to write a Hello, World! in _nostub. Let's start with writing our message in the status line:
include "OS.h" xdef _nostub xdef _ti89 xdef _ti92plus ;No registers have to be saved here. pea.l hello_world(PC) ;pass the message to show as an argument move.l $c8,a0 move.l ST_helpMsg*4(a0),a0 jsr (a0) ;call ST_helpMsg addq.l #4,a7 ;clean up the stack rts hello_world: dc.b 'Hello, World!',0
Unfortunately, we are likely to have to display some text elsewhere than in the status line. Let's thus show our message in the upper-left corner using the much more flexible DrawStr ROM_CALL:
include "OS.h" xdef _nostub xdef _ti89 xdef _ti92plus ;No registers have to be saved here. move.w #4,-(a7) ;pass the A_REPLACE attribute as an argument pea.l hello_world(PC) ;pass the message to show as an argument clr.l -(a7) ;pass x=0 and y=0 as arguments (We clear 4 bytes, 2 of them for x and 2 more for y.) move.l $c8,a0 move.l DrawStr*4(a0),a0 jsr (a0) ;call DrawStr lea.l 10(a7),a7 ;clean up the stack rts hello_world: dc.b 'Hello, World!',0
Note that the text stays on the screen after execution and that this is not very pretty. On your calculator, press [F5] twice to get rid of it. The next section will show you the solution to this problem.
III.7) A few ready-made lines for saving the screen
In order to complete this section and to give an important application of the preceding paragraph, I will give you a commented source code which you might also use as is if you want, but which I recommend you to understand anyway because it represents an application of what precedes. Here is it:
include "OS.h" xdef _nostub xdef _ti89 xdef _ti92plus movem.l d4/a5,-(a7) ;save d4 and a5 move.l $c8,a5 ;place the ROM_CALL jump table into a5 pea.l 3840 ;size to allocate ;(same as move.l #3840,-(a7), but more optimized) move.l HeapAllocPtr*4(a5),a0 jsr (a0) ;allocate the 3840 bytes move.l a0,d4 ;save the address of the allocated handle to d4 tst.l d4 beq nomem ;if the pointer is NULL, quit the program move.l #3840,(a7) ;size to copy ;This instruction is not really necessary because HeapAllocPtr will not modify the value which is ;already on the stack, but it is better not to rely on it. pea.l $4c00 ;source: address of the screen move.l d4,-(a7) ;destination: adress of the allocated handle move.l memcpy*4(a5),a0 jsr (a0) ;save the screen lea.l 12(a7),a7 ;clean up the stack ;beginning of the main program move.w #4,-(a7) ;pass the A_REPLACE attribute as an argument pea.l hello_world(PC) ;pass the message to show as an argument clr.l -(a7) ;pass x=0 and y=0 as arguments (We clear 4 bytes, 2 of them for x and 2 more for y.) move.l DrawStr*4(a5),a0 jsr (a0) ;call DrawStr lea.l 10(a7),a7 ;clean up the stack ;end of the main program pea.l 3840 ;size to copy ;(same as move.l #3840,-(a7), but more optimized) move.l d4,-(a7) ;source: adress of the allocated handle pea.l $4c00 ;destination: address of the screen move.l memcpy*4(a5),a0 jsr (a0) ;restore the screen addq.l #8,a7 move.l d4,(a7) move.l HeapFreePtr*4(a5),a0 jsr (a0) ;free the allocated handle nomem: addq.l #4,a7 ;clean up the stack movem.l (a7)+,d4/a5 ;restore d4 and a5 rts hello_world: dc.b 'Hello, World!',0
You will notice that the message has vanished. Let's thus insert a wait for a keypress. Moreover, we can now afford to clear the screen without leaving too much damage. Here is the final version of our Hello, World!:
include "OS.h" xdef _nostub xdef _ti89 xdef _ti92plus movem.l d4/a5,-(a7) ;save d4 and a5 move.l $c8,a5 ;place the ROM_CALL jump table into a5 pea.l 3840 ;size to allocate ;(same as move.l #3840,-(a7), but more optimized) move.l HeapAllocPtr*4(a5),a0 jsr (a0) ;allocate the 3840 bytes move.l a0,d4 ;save the address of the allocated handle to d4 tst.l d4 beq nomem ;if the pointer is NULL, quit the program move.l #3840,(a7) ;size to copy ;This instruction is not really necessary because HeapAllocPtr will not modify the value which is ;already on the stack, but it is better not to rely on it. pea.l $4c00 ;source: address of the screen move.l d4,-(a7) ;destination: adress of the allocated handle move.l memcpy*4(a5),a0 jsr (a0) ;save the screen lea.l 12(a7),a7 ;clean up the stack ;beginning of the main program move.l ScreenClear*4(a5),a0 jsr (a0) ;call ScreenClear (ClrScr in the TIGCC documentation - it is the only function I know of ;which has a differing name in the TIGCC documentation compared to OS.h) move.w #4,-(a7) ;pass the A_REPLACE attribute as an argument pea.l hello_world(PC) ;pass the message to show as an argument clr.l -(a7) ;pass x=0 and y=0 as arguments (We clear 4 bytes, 2 of them for x and 2 more for y.) move.l DrawStr*4(a5),a0 jsr (a0) ;call DrawStr lea.l 10(a7),a7 ;clean up the stack move.l ngetchx*4(a5),a0 jsr (a0) ;call ngetchx ;end of the main program pea.l 3840 ;size to copy ;(same as move.l #3840,-(a7), but more optimized) move.l d4,-(a7) ;source: adress of the allocated handle pea.l $4c00 ;destination: address of the screen move.l memcpy*4(a5),a0 jsr (a0) ;restore the screen addq.l #8,a7 move.l d4,(a7) move.l HeapFreePtr*4(a5),a0 jsr (a0) ;free the allocated handle nomem: addq.l #4,a7 ;clean up the stack movem.l (a7)+,d4/a5 ;restore d4 and a5 rts hello_world: dc.b 'Hello, World!',0
That's it, you now know the bases of programming in _nostub mode. With this, you should be able to write some good _nostub programs. The 3 following sections will explain you how to replace the functionalities of the kernels which you are likely to miss. The last section will be dedicated to the ExePack compression.
IV) How to replace BSS sections and RAM_CALLs
IV.1) Dynamic memory allocation
One of the possibilities of the kernel mode which is often missed is the one to create "BSS sections", which contain non-initialized data which is not stored in the program, but automatically allocated and unallocated by the kernel. That is not possible in _nostub mode. Luckily, you are not obliged to put all your variables into the program itself. In fact, there are some ROM_CALLs which allow you to easily dynamically allocate and free memory blocks yourself. I recommend you to read the documentation of alloc.h of TIGCCLIB. For a code example, you might take a look at the screen content saving code above, which uses HeapAllocPtr and HeapFreePtr.
IV.2) RAM_CALLs and their equivalents in _nostub
An other function of the kernels which is often missed is given by the RAM_CALLs. Paxal (Cyril Pascal) gives at his web page a list of _nostub equivalents for the RAM_CALLs (WARNING: The comments and explanations are in French.). I also want to precise that some RAM_CALLs should be avoided because there are ROM_CALLs which do the same thing more cleanly:
One last thing I want to mention is that for the ROM version, there is a ROM_CALL in AMS 2 which gives the ROM version as a string (for example '2.05',0). It is the ROM_CALL number $440. So:
move.l $c8,a5 cmp.l #$440,-4(a5) ;check if the relevant ROM_CALL is there bcs AMSversion_AMS1 ;if not, use another routine move.l $440*4(a5),a0 ;obtain the address of the string
For AMS 1, you will have to use another technique. You might for example use a table of the ROM_CALL jump table addresses as Paxal recommends it. By the way, if you only need to know if one uses AMS 1 or AMS 2, it is enough to write:
move.l $c8,a5 cmp.l #1000,-4(a5) ;check if there are at least 1000 ROM_CALLs bcs AMS1 ;if not, it is AMS 1
V) Static libraries - code sharing made totally transparent for the user
At that point, you will probably ask the following question: Since the _nostub mode does not support dynamic libraries (it is possible to work around this like Thomas Nussbaumer's FAT Engine does, but it is very complicated), what should I do if I need a library function? First of all, most functions of the standard libraries of the kernels (userlib, graphlib, filelib) are already built-in into AMS: there are equivalent ROM_CALLs. For example, userlib::idle_loop can be replaced with ngetchx, graphlib::clr_scr with ScreenClear, ... If you have any doubts, the TIGCC documentation is a good reference. However, unfortunately, there are a few important functions, like for example the grayscales, which are not in AMS. What should one do in that case? Luckily, the dynamic libraries are not the only way to share code. There is another type of libraries, static libraries, which will be the subject of this section.
V.1) The advantages of static libraries over dynamic libraries
So, what are the particularities of that type of libraries? Static libraries are managed by the linker during the translation of the source code into an executable program. This induces many advantages when compared to dynamic libraries:
Thus, static libraries bring you all the advantages of using libraries, i.e. the ease of sharing code which frees you from having to rewrite everything all the time, without the disadvantages of dynamic libraries.
V.2) How to use a real static library (*.a) with A68k
Since version 0.92, TIGCC supports the use (and even the creation) of static libraries with A68k. And their use is very easy: it is enough to do a bsr or jsr to the function you want to use, and the linking system of TIGCC will take care of everything else. Let's give us an example: the assembly Gray Test from the TIGCC examples converted to _nostub mode:
include "OS.h" xdef _nostub xdef _ti89 xdef _ti92plus plane0 equ __D_plane ;It would not be good coding practice to access variables beginning with __ plane1 equ __L_plane ;directly. movem.l d4/a5,-(a7) ;save d4 and a5 move.l $c8,a5 ;place the ROM_CALL jump table into a5 pea.l 3840 ;size to allocate ;(same as move.l #3840,-(a7), but more optimized) move.l HeapAllocPtr*4(a5),a0 jsr (a0) ;allocate the 3840 bytes move.l a0,d4 ;save the address of the allocated handle to d4 tst.l d4 beq nomem ;if the pointer is NULL, quit the program move.l #3840,(a7) ;size to copy ;This instruction is not really necessary because HeapAllocPtr will not modify the value which is ;already on the stack, but it is better not to rely on it. pea.l $4c00 ;source: address of the screen move.l d4,-(a7) ;destination: adress of the allocated handle move.l memcpy*4(a5),a0 jsr (a0) ;save the screen lea.l 12(a7),a7 ;clean up the stack ;beginning of the main program move.l ScreenClear*4(a5),a0 jsr (a0) ;clear the main screen (first bitplane) bsr GrayOn ;activate grayscale mode move.l #$ef007f,-(a7) move.l plane1(PC),-(a7) move.l PortSet*4(a5),a0 jsr (a0) ;switch to the second bitplane addq.l #8,a7 move.l ScreenClear*4(a5),a0 jsr (a0) ;clear the second bitplane move.l #20*$1000000+20*$10000+40*$100+40,-(a7) move.w #1,-(a7) move.l ScrRect*4(a5),a0 pea.l (a0) pea.l 6(a7) move.l ScrRectFill*4(a5),a0 jsr (a0) ;draw a rectangle filled in light gray move.l #80*$1000000+20*$10000+100*$100+40,10(a7) move.l ScrRectFill*4(a5),a0 jsr (a0) ;draw the light plane of a rectangle filled in black lea.l 14(a7),a7 move.l #$ef007f,-(a7) move.l plane0(PC),-(a7) move.l PortSet*4(a5),a0 jsr (a0) ;activate the first bitplane addq.l #8,a7 move.l #50*$1000000+20*$10000+70*$100+40,-(a7) move.w #1,-(a7) move.l ScrRect*4(a5),a0 pea.l (a0) pea.l 6(a7) move.l ScrRectFill*4(a5),a0 jsr (a0) ;draw a rectangle filled in dark gray move.l #80*$1000000+20*$10000+100*$100+40,10(a7) move.l ScrRectFill*4(a5),a0 jsr (a0) ;draw the dark plane of a rectangle filled in black lea.l 14(a7),a7 move.l ngetchx*4(a5),a0 jsr (a0) ;wait for a keypress bsr GrayOff ;end of the main program pea.l 3840 ;size to copy ;(same as move.l #3840,-(a7), but more optimized) move.l d4,-(a7) ;source: adress of the allocated handle pea.l $4c00 ;destination: address of the screen move.l memcpy*4(a5),a0 jsr (a0) ;restore the screen addq.l #8,a7 move.l d4,(a7) move.l HeapFreePtr*4(a5),a0 jsr (a0) ;free the allocated handle nomem: addq.l #4,a7 ;clean up the stack movem.l (a7)+,d4/a5 ;restore d4 and a5 rts
That's it. During execution of this program, the calculator will show 3 rectangles filled with gray shades: a light gray one, a dark gray one and a black one. We have thus succeeded in writing a grayscale program in _nostub assembly, which is often the main blocking point for programmers who try to program in that way. Do not hesitate to reuse some code from this example, it is there to help you.
By the way, in this example, we have used the TIGCCLIB static library, which your project will be automatically linked to. If you want to use the functions from another static library (such as ExtGraph), you will have to add it to your project before: Go to Project / Add Files... and select extgraph.a, then OK. You can now use the functions from ExtGraph just like those from TIGCCLIB.
V.3) Another solution for static code sharing
The "real" static libraries (the *.a files) are not the only way to statically share code. There is another solution which works exactly like static libraries, except for a disadvantage: the function to use is recompiled or reassembled each time you compile a project which uses it. That solution is a very simple one: adding the source file containing the function to the project. But let's start with an example: the InputStr function described in the TIGCC FAQ. First of all, in addition to your A68k assembly file, add a second file, in C, to the project. What options you choose does not matter, as we will delete the template automatically created by TIGCC IDE anyway. WARNING: The name has to be different. In fact, the extensions differ, but if you give the same name with just a different extension to the 2 files, there will be a collision for the created object files. The content of the C file is the following:
#define NO_EXIT_SUPPORT #include <tigcclib.h> /* InputStr function taken from the TIGCC FAQ - thanks to Zeljko Juric */ void InputStr (char *buffer, short maxlen) { SCR_STATE ss; short key, i = 0; buffer[0] = 0; SaveScrState (&ss); do { MoveTo (ss.CurX, ss.CurY); printf ("%s_ ", buffer); /* Note that two spaces are required if F_4x6 font is used. */ key = ngetchx (); if (key >= ' ' && key <= '~' && i < maxlen) buffer[i++] = key; if (key == KEY_BACKSPACE && i) i--; buffer[i] = 0; } while (key != KEY_ENTER); }
Now, let's go on to our main program, in A68k assembly:
include "OS.h" xdef _ti89 xdef _ti92plus xdef _nostub ;You need not reexport _nostub here, since TIGCC will do it in the C part. movem.l d4/a5,-(a7) ;save d4 and a5 move.l $c8,a5 ;place the ROM_CALL jump table into a5 pea.l 3840 ;size to allocate ;(same as move.l #3840,-(a7), but more optimized) move.l HeapAllocPtr*4(a5),a0 jsr (a0) ;allocate the 3840 bytes move.l a0,d4 ;save the address of the allocated handle to d4 tst.l d4 beq nomem ;if the pointer is NULL, quit the program move.l #3840,(a7) ;size to copy ;This instruction is not really necessary because HeapAllocPtr will not modify the value which is ;already on the stack, but it is better not to rely on it. pea.l $4c00 ;source: address of the screen move.l d4,-(a7) ;destination: adress of the allocated handle move.l memcpy*4(a5),a0 jsr (a0) ;save the screen lea.l 12(a7),a7 ;clean up the stack ;beginning of the main program bsr clrscr ;clear the screen and set the coordinates for printf to 0 (calls a function from ;TIGCCLIB) move.w #100,-(a7) ;pass the maxlen argument pea.l buffer(PC) ;pass the buffer argument bsr InputStr ;call the C function addq.l #6,a7 ;clean up the stack ;end of the main program pea.l 3840 ;size to copy ;(same as move.l #3840,-(a7), but more optimized) move.l d4,-(a7) ;source: adress of the allocated handle pea.l $4c00 ;destination: address of the screen move.l memcpy*4(a5),a0 jsr (a0) ;restore the screen addq.l #8,a7 move.l d4,(a7) move.l HeapFreePtr*4(a5),a0 jsr (a0) ;free the allocated handle nomem: addq.l #4,a7 ;clean up the stack movem.l (a7)+,d4/a5 ;restore d4 and a5 rts buffer: ds.b 101
In general, it is recommendable to avoid this technique, and to prefer real static libraries, because of the time spent to recompile the function every time it is used, but it might be useful in some cases, like here where the function is not in a static library.
By the way, once more, I have not chosen the example randomly. In fact, as for the grayscales, an easy to use InputStr routine is one of the few useful functions which you won't find in AMS. With the help of the 2 previous examples, the code of which I invite you to reuse, we have passed the 2 biggest obstacles which a beginner in _nostub assembly has to face.
VI) External data files
You might sometimes find yourself forced to use an external file for your data for 2 reasons:
Luckily, there are some ROM_CALLs which make this relatively easy. Please refer to the
documentation of vat.h of TIGCC. Here, I will limit myself to give a simple example,
which displays the content of the string called datafile in the main folder on the
screen. Let's start with creating that string. Enter the following on your calculator:
"Hello, World!"->main\datafile
(-> represents of course the character obtained by pressing the [STO->] key.)
Now, let's go on to our example program:
include "OS.h" xdef _nostub xdef _ti89 xdef _ti92plus movem.l d4/a5,-(a7) ;save d4 and a5 move.l $c8,a5 ;place the ROM_CALL jump table into a5 pea.l 3840 ;size to allocate ;(same as move.l #3840,-(a7), but more optimized) move.l HeapAllocPtr*4(a5),a0 jsr (a0) ;allocate the 3840 bytes move.l a0,d4 ;save the address of the allocated handle to d4 tst.l d4 beq nomem ;if the pointer is NULL, quit the program move.l #3840,(a7) ;size to copy ;This instruction is not really necessary because HeapAllocPtr will not modify the value which is ;already on the stack, but it is better not to rely on it. pea.l $4c00 ;source: address of the screen move.l d4,-(a7) ;destination: adress of the allocated handle move.l memcpy*4(a5),a0 jsr (a0) ;save the screen lea.l 12(a7),a7 ;clean up the stack ;beginning of the main program move.l ScreenClear*4(a5),a0 jsr (a0) ;clear the screen move.w #4,-(a7) ;look in the main folder pea.l sym(PC) ;symbol to look for move.l SymFindPtr*4(a5),a0 jsr (a0) ;search for the variable addq.l #4,a7 move.w 12(a0),(a7) ;The handle of the symbol is located at offset 12 of the SYM_ENTRY structure. move.l HeapDeref*4(a5),a0 jsr (a0) ;dereference the handle move.w #4,(a7) ;pass the A_REPLACE attribute as an argument pea.l 3(a0) ;skip the size of the variable and the zero byte at the beginning of the string clr.l -(a7) ;pass x=0 and y=0 as arguments (We clear 4 bytes, 2 of them for x and 2 more for y.) move.l DrawStr*4(a5),a0 jsr (a0) ;display the content of the string lea.l 10(a7),a7 ;clean up the stack move.l ngetchx*4(a5),a0 jsr (a0) ;wait for a keypress ;end of the main program pea.l 3840 ;size to copy ;(same as move.l #3840,-(a7), but more optimized) move.l d4,-(a7) ;source: adress of the allocated handle pea.l $4c00 ;destination: address of the screen move.l memcpy*4(a5),a0 jsr (a0) ;restore the screen addq.l #8,a7 move.l d4,(a7) move.l HeapFreePtr*4(a5),a0 jsr (a0) ;free the allocated handle nomem: addq.l #4,a7 ;clean up the stack movem.l (a7)+,d4/a5 ;restore d4 and a5 rts dc.b 0,'datafile' sym: dc.b 0 ;The format of the symbols is special: You have to put a zero byte at the beginning and at the ;end, and the pointer must point to the final 0, not to the beginning.
A few remarks to conclude:
VII) The ExePack compression
At that point, you will probably have one last problem: What to do if your program exceeds 24 KB (8 KB on AMS 2.00-2.03)? Luckily, it is not very difficult to create a launcher which works even on HW2 AMS 2 without any kind of patches (like Julien Muchembled's HW2Patch) or memory resident anti-protection programs (like my HW2 AMS 2 TSR support (h220xTSR)). However, there is an even simpler and more practical solution: TIGCC can automatically compress your program for you. That not only has the advantage that the automatically generated decompressor will bypass the 24 (or 8) KB limit for you, but it also allows you to reduce the size of the program while you are at it. That compression is called ExePack.
Start with choosing an example file. You can use the empty program from section III.2:
include "OS.h" xdef _nostub xdef _ti89 xdef _ti92plus rts
Now compress it:
I recommend you to use ExePack for each program larger than 8 KB. For programs of 8 KB or less, it is not really worth it, so I do not recommend you to use ExePack in that case.
VIII) Conclusion
You should now be able to program in _nostub mode and to do everything you can do in kernel-dependent programs in that mode too, and maybe even more. As you have been able to see, the _nostub mode is not reserved for C programs! I hope that this tutorial has helped you and that it will inspire you so as to program in _nostub mode like I do it since more than a year.
Kevin Kofler
kevin.kofler@chello.at
http://kevinkofler.cjb.net