Jump to content


Check out our Community Blogs





- - - - -

TI ASM Battle, Part 2: Stacks On Deck

Posted by gregwarner, 23 May 2014 · 5863 views

assembly ti-99/4a old computers
For whatever reason, I cannot fathom, I have the inescapable desire to try and learn how to program in Assembly Language on my old 80’s era TI-99/4A computer.

Why would I subject myself to this? Perhaps I’m a **. I dunno. But I love this little ancient computer, and so I can’t help but try and bring a tiny bit of the modern world into its life.

But that means I’m going to need a call stack. A call stack is a useful data structure that lets you do fancy things like set up local variables, pass parameters, and even do recursion.

But last time, I discovered that this ancient architecture was designed and built entirely without a stack pointer register. How on earth do you return from subroutines, you might ask? Well, you have to keep track of that yourself, I'm afraid. Or never branch further than one subroutine deep!

That's the premise that prompted me to implement my own call stack on this archaic piece of hardware. Strap yourselves in, this is gonna be a bumpy ride.

The first step is to take a look at the hardware and see what we have to work with. The TI-99/4A contains a TMS9900 CPU, which uses 16 general purpose registers called R0 through R15. Since there's no stack pointer register, we need to pick one of the GP ones to use. R11 is already used as the link register when calling subroutines, and R12 is used by the TMS's device I/O, so we can't use either of those. R13 through R15 are used by the context switching mechanism, such as interrupt service routines, and since we won't be using this mechanism, we can repurpose R13 as our Stack Pointer. This just happens to be the same one that the ARM processor uses for it's SP.

Great. So we've decided which register will be our SP. Now we need to figure out where in memory we're gonna put our stack. The TI-99 was designed with its 32K memory space separated into 2 non-contiguous blocks: 8 KB located from >2000 to >3FFF, called Low Memory, and the remaining 24 KB from >A000 to >FFFF, called High Memory.

In case you're wondering, >2000 is the TI Assembler's notation for hexadecimal. It's equivalent to 0x2000.

I happen to know that the Linker/Loader begins loading user object code by default at >A000, so we'll wanna stay out of this space. I want to try and make use of the low memory at >2000. The space at the top of this block (>3FFF growing downward) happens to be used by the Linker for building its symbol tree, so I know this area will be free after my program is linked. So we'll place the stack here. (The Linker/Loader also places some utility routines at >2000, but they're only a couple hundred bytes long, so the chances of our stack overwriting them are minimal. Plus, I never call these routines anyway, so I couldn’t give a rip if I overwrite them.)

So we'll initialize our stack pointer to >4000 (just above our stack space), because we will need to decrement our stack pointer before pushing a word onto the stack, and so >4000 indicates an empty stack. Here's the code:

*Initialize our stack pointer with the value >4000.
       LI   R13,>4000

*PUSH the Link Register (R11) onto the stack.
       DECT R13
       MOV  R11,*R13

*POP the Link Register from the stack and return.
       MOV  *R13+,R11
       RT
Several things are going on in there. First, we use the Load Immediate (LI) opcode to initialize R13 (our designated SP) with the value representing the top of the (empty) stack. Next, since there is no hardware SP, we must implement our own PUSH and POP instructions. For PUSH, we first DECrement R13 by Two (DECT) (since addresses are 2 bytes in this architecture), and then we move (MOV) the value in the Link Register (R11) to the area of memory now pointed to by R13. (That's the * addressing mode in TMS9900 assembler.) Performing a POP from the stack is easier, thanks to the TMS9900's Auto Increment addressing mode (the * with a + trailing the register name), which automatically increments the value in the register by two after retrieving the value that it previously pointed to.

Congratulations! We now have a fully functioning call stack! But we're not out of the woods yet. Oh, no, I want to make this truly painful for myself. To have a complete working stack, we're going to have to design a calling convention which defines stack frames, function preambles and epilogues, register saving, local variable allocation, and parameter passing!

But that’s going to have to wait till next time. For now, let’s just do a quick program that calls a subroutine, which in turn calls another subroutine, and then returns from everything. Here’s the source listing:

DEF  MAIN         *DEFINE THE ENTRY POINT TO OUR PROGRAM
       REF  VDPMBW       *REFERENCES A VIDEO WRITING ROUTINE

M1LEN  EQU  7            *LENGTH OF MESSAGE 1
M2LEN  EQU  15           *LENGTH OF MESSAGE 2
M3LEN  EQU  20           *LENGTH OF MESSAGE 3
M4LEN  EQU  4            *LENGTH OF MESSAGE 4

MSG1   TEXT ‘IN MAIN’
MSG2   TEXT ‘IN SUBROUTINE 1’
MSG3   TEXT ‘HELLO WORLD! (SUB 2)’
MSG4   TEXT ‘DONE’

       EVEN

MAIN   LWPI >8300        *PAY NO ATTENTION TO THIS, WILL EXPLAIN LATER
       LI   R13,>4000    *INITIALIZE THE STACK POINTER

*WRITE THE FIRST MESSAGE TO THE SCREEN
       CLR  R0           *LINE 1
       LI   R1,MSG1      *MESSAGE 1 ADDRESS
       LI   R2,M1LEN     *MESSAGE 1 LENGTH
       BL   @VDPMBW      *CALL THE VIDEO DISPLAY PROCESSOR MULTI BYTE WRITE ROUTINE

*CALL THE FIRST SUBROUTINE
       BL   @SUB1

*WRITE THE FINAL MESSAGE TO THE SCREEN
       LI   R0,96        *LINE 4
       LI   R1,MSG4      *MESSAGE 4 ADDRESS
       LI   R2,M1LEN     *MESSAGE 4 LENGTH
       BL   @VDPMBW      *CALL THE DISPLAY ROUTINE

*END THE PROGRAM
       LIMI 2            *ENABLE INTERRUPTS
HCF    JMP  HCF          *HALT AND CATCH FIRE

*------------------------------------------------
*SUBROUTINE 1
SUB1   DECT R13          *PUSH R11 (LINK REGISTER)
       MOV  R11,*R13     *2ND HALF OF PUSH INSTRUCTION

*WRITE THE SECOND MESSAGE TO THE SCREEN
       LI   R0,32        *LINE 2
       LI   R1,MSG2
       LI   R2,M2LEN
       BL   @VDPMBW

*CALL THE SECOND SUBROUTINE
       BL   @SUB2

*POP R11 AND RETURN
       MOV  *R13+,R11
       RT


*------------------------------------------------
*SUBROUTINE 2
SUB2   DECT R13
       MOV  R11,*R13

*WRITE THE THIRD MESSAGE TO THE SCREEN
       LI   R0,64        *LINE 3
       LI   R1,MSG3
       LI   R2,M3LEN
       BL   @VDPMBW

*POP R11 AND RETURN
       MOV  *R13+,R11
       RT


       END  MAIN         *INSTRUCT THE LINKER TO AUTOMATICALLY BRANCH
                         *TO MAIN AFTER LINKING IS COMPLETED
The code first defines our start point, and then references another routine I wrote called Video Display Processor Multi Byte Write. It pretty much does what it's name implies, and it's used for writing to the screen. It's defined elsewhere, so we wont' worry about it here.

Next I declare some static strings we'll use later. The VDPMBW routine needs to know how many bytes to write to the video memory, so the string lengths are noted. The EVEN assembler directive simply ensures the next instruction falls on a word boundary, just in case I had an odd number of bytes preceeding it.

The code is pretty much easy to follow from there. It writes to the screen, then calls a subroutine using the Branch and Link (BL) instruction. If you follow the code all the way through, you'll see it goes 2 subroutines deep. (3 if you count the VDPMBW routine, although this routine is a leaf procedure, so it doesn't make use of the call stack.)

At the end, I make sure and turn on hardware interrupts, so that I can reset the computer using the keyboard reset command, and I simply halt execution with an infinite loop known as Halt and Catch Fire, so we can have a chance to look at the output. Here's the screen's output after running this program:

Attached Image

It works!

(The extra stuff below "DONE" is simply left over from the Loader's screen, since I didn't bother to clear the screen at the beginning of my program.)

Next time, we will create our calling convention and start working with local variables and parameter passing.

  • 2



Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download