_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:

  1. In the TIGCC IDE menu, select Project, then Options....
  2. Click on Compress File (*.89y/*.9xy) in the upper left, under Target, in order to activate that option.
  3. The field On-calc variable name should now be active. Enter the name which the compressed file will take on the calculator there. WARNING: This name has to be different than the name of the program. I recommend you to take as a name the 5 first characters of the name of your program followed by ppg. For example, if your program is called abcdefg, I recommend you to call your compressed file abcdeppg.
  4. Click on OK.
  5. Reassemble your program (using for example Project/Build). It will be compressed automatically.

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