Learn Multi platform TMS9900 Assembly Programming... With Workspaces!



Don't like to read? you can learn while you watch and listen instead!

Every Lesson in this series has a matching YOUTUBE video... with commentary and practical examples

Visit the authors Youtube channel, or Click the icons to the right when you see them to watch the Lessons video!

The TMS9900 is a 16 bit CPU, a miniature version of the TI 990 minicomputer, it was designed for home computer use, but it was really only used in the Ti-99

Despite it's failure in the market the TMS9900 is a fascinating chip, it has virtually no registers, instead using 'Workspaces'... banks of 32 bytes that make the 16 regsiters (like a zeropage)... it also has no stack!



When a function is called, one register is used as a 'Link pointer' - pointing to the return address... if we need new registers for our subroutine, we allocate another 'workspace' giving us a new set of registers - when we're done we restore the original workspace.

If you want to learn TMS9900 get the Cheatsheet! it has all the TMS9900 commands, it also covers the extra commands used by the 65c02 and PC-Engine HuC6280


Systems covered in these tutorials
TI-99/4A

What is the TMS9900 and what are 8 'bits' You can skip this if you know about binary and Hex (This is a copy of the same section in the Z80 tutorial)
The TMS9900 is an 16-Bit processor with a 15 bit Address bus!
What's 8 bit... well, one 'Bit' can be 1 or 0
four bits make a Nibble (0-15)
two nibbles (8 bits) make a byte (0-255)
two bytes (16 bits) make a word (0-65535)

And what is 65535? well that's 64 kilobytes ... in computers 'Kilo' is 1024, because binary works in powers of 2, and 2^10 is 1024 
64 kilobytes is the amount of memory a basic 8-bit system can access

6502 is 8 bit so it's best at numbers less than 256... it can do numbers up to 65535 too more slowly... and really big numbers will be much harder to do! - we can design our game round small numbers so these limits aren't a problem.

You probably think 64 kilobytes doesn't sound much when a small game now takes 8 gigabytes, but that's 'cos modern games are sloppy, inefficient,  fat and lazy - like the basement dwelling losers who wrote them!!!
6502 code is small, fast, and super efficient - with ASM you can do things in 1k that will amaze you!

Numbers in Assembly can be represented in different ways.
A 'Nibble' (half a byte) can be represented as Binary (0000-1111) , Decimal (0-15) or  Hexadecimal (0-F)... unfortunately, you'll need to learn all three for programming!

Also a letter can be a number... Capital 'A'  is stored in the computer as number 65!

Think of Hexadecimal as being the number system invented by someone wit h 15 fingers, ABCDEF are just numbers above 9!
Decimal is just the same, it only has 1 and 0.


NASM /ASW
TMS9900 assembler 
classic
TI99 Assembler
Decimal
123 123
Hex
0FF00h >FF00
Binary
00110011b
Ascii
"A" 'A'

ASW and Byte definitions
Be careful with BYTE definitions - they are automatically Word aligned... EG
BYTE 1
BYTE 2
Will produce data 1,0,2,0 ... You probably want
BYTE 1,2
instead which will result in 1,2





We're going to use Macro AS in these tutorials, it uses the same syntax as Intel x80 chips... this is very different to the original TI99 Assemblers, but will make it easier for us to learn.
Decimal 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ... 255
Binary 0000 0001 0010 0011 0100 0101 0110 0111 1000 1001 1010 1011 1100 1101 1110 1111   11111111
Hexadecimal 0 1 2 3 4 5 6 7 8 9 A B C D E F   FF

Another way to think of binary is think what each digit is 'Worth' ... each digit in a number has it's own value... lets take a look at %11001100 in detail and add up it's total

Bit position 7 6 5 4 3 2 1 0
Digit Value (D) 128 64 32 16 8 4 2 1
Our number (N) 1 1 0 0 1 1 0 0
D x N 128 64 0 0 8 4 0 0
128+64+8+4= 204            So %11001100 = 204 !

If a binary number is small, it may be shown as %11 ... this is the same as %00000011
Also notice in the chart above, each bit has a number, the bit on the far right is no 0, and the far left is 7... don't worry about it now, but you will need it one day!

If you ever get confused, look at Windows Calculator, Switch to 'Programmer Mode' and  it has binary and Hexadecimal view, so you can change numbers from one form to another!
If you're an Excel fan, Look up the functions DEC2BIN and DEC2HEX... Excel has all the commands to you need to convert one thing to the other!

But wait! I said a Byte could go from 0-255 before, well what happens if you add 1 to 255? Well it overflows, and goes back to 0!...  The same happens if we add 2 to 254... if we add 2 to 255, we will end up with 1
this is actually usefull, as if we want to subtract a number, we can use this to work out what number to add to get the effect we want

Negative number -1 -2 -3 -5 -10 -20 -50 -254 -255
Equivalent Byte value 255 254 253 251 246 236 206 2 1
Equivalent Hex Byte Value FF FE FD FB F6 EC CE 2 1

All these number types can be confusing, but don't worry! Your Assembler will do the work for you!
You can type %11111111 ,  &FF , 255  or  -1  ... but the assembler knows these are all the same thing! Type whatever you prefer in your ode and the assembler will work out what that means and put the right data in the compiled code!

The TMS9900 is a BIG ENDIAN machine... what's stranger is that byte operations (like MOVB) work with the TOP BYTE of the register.

Words must be loaded from EVEN addresses, if an attempt is made to load from an ODD address, the bottom bit 0 will be ignored (causing a word read from 1001 to read in from 1000)

The TMS9900 Registers
The 3 real registers in the CPU are as follows
Register Purpose
PC Program Counter
WP Workspace Pointer
ST Status

The 16 Workspace registers (in ram) have the following Names and functions
Register Purpose
R0 Bits 12-15 Shift Count
R1 -
R2 -
R3 -
R4 -
R5 -
R6 -
R7 -
R8 -
R9 -
R10 -
R11 Return address (PC BL)
R12 CRU Addressing
R13 Context Switching (WP BLWP)
R14 Context Switching (PC BLWP)
R15 Context Switching (ST BLWP)

    Flags: I I I I - - - - / - X P O C = A> L>

Flag
Meaning
Bit Details
L> Logical Greater than
A> Arithmatic Greater than
= Equal
C Carry
O Overflow 1=overflow?
P Parity
X Xop
I Interrupt Mask









The TMS9900 Addressing Modes
The TMS9900 has 8 different addressing modes... many are similar to much later systems... in addition it has 4 'effective modes' which are defined by using Reg 7 as a parameter
'Deferred' addressing is known as indirect addressing on other systems

Syntax Effect
R Workspace Regsiter Addressing
*R Workspace Regsister Indirect Addressing
*R+ Workspace Regsister Indirect Auto Increment Addressing
@Label Symbolic (Direct) Addressing
@Table(R) 
Indexed Addressing (doesn't work with R0)
n Immediate Addressing
?
Program Counter Relative Addressing
?
CRU Relative Addressing (R12)


Lesson 1 - Getting started with TMS-9900
Lets learn the basics of using TMS9900...  In this lesson we'll set some registers, and do a few simple maths operations.
We'll be testing on a Ti99/4A emulator


These tutorials use Macro-AS as an assembler... it compiles TMS9900 perfectly, however it uses more traditional syntax rather than the 'odd' syntax you may see in TMS9900 documentation...

For example we'll use 0FFFFh to denote as a hexadecimal number, but the 'normal' syntax is >FFFF !!!

Structure of an ASM (.MAC) source file

Lets look at a simple file (Minimal.asm)

we have a header - We're including a header to do our setup, and calling the screen initialization routine. (BL = Branch and Link)

In our body we're running a simple monitor program - this is where you would put your code

In our footer we're including some useful files (with include statements)

This example will show a hello world message, the status of the registers, and dump some bytes of memory.

The Include files are doing most of the work for us here - this will give you an easy starting point to begin learning, but you'll want to modify it and make something of your own once you're more experienced

Loading Values
Lets load a value into a register...

We're going to Load an Immediate value into a register with LI

The Destination register is on the Left (R0)

The source value is on the Right (1234 in decimal)

Immediate values must be fixed numbers... LI R0,R1 will not work!
By default a number will be treated as Decimal.

We can specify Hex by putting a h at the end... we'll need to make sure it starts with a number, so if the first digit is A-F add a 0

We can specify Binary by putting a b at the end

We can specify Octal by putting an o at the end
Here are the results
We can use Ascii if we put our characters in apostraphe '--'... each register can hold 2 characters
Here is the result

Moving values between registers
If we want to transfer data between registers we use MOV ... the source is on the left, the destination is on the right
Lets try a variety of move commands

We'll copy R0 into R1,R2 and R3
Here is the result

One word commands to set values
There are two special commands that don't need a parameter (making them faster)

We can set a register to zero with CLR

We can set a register to all 1 bits (FFFFh) with SETO (SET Ones)
Here is the result

Addition and Subtraction
When we want to Add or Subtract a fixed Immediate value we use AI (Add Immediate)...

The destination register is on the left of the comma, the amount to add is on the right,

There is no subtract version, but the amount to add can be negative!
Here is the result
If we want to add a register to another we use A (add)... the source is on the left, the destination is on the right.

If we want to subtract a register from another we use S (add)... the source is on the left, the destination is on the right.
Here is the result

Adding or Subtracting by 1 or 2 quickly
There will be many times when we want to increase or decrease by 1 or 2 (for loops or addresses)... we have single commands to do this!

INC will INCrement a register by 1
DEC will DECrement a register by 1
INCT will INCrement a register by 2
DECT will DECrement a register by 2

We'll do this four times.
Here is the result

Don't forget, you can only load Words from even addresses!

If you try to load from an odd address like 1001h it will actually read from address 1000h... so don't do it!
Code will mess up too... so make sure if data segments exist in your code they use even byte numbers... You can use Align 2 to align data to an even boundary.

Lesson 2 - Addressing Modes on the TMS-9900
Lets take a look at the Addressing modes of the TMS-9900, In this lesson we'll be looking at each addressing mode, and trying out each with an example.



Immediate Addressing
Immediate addressing is where a fixed numeric value is included in the line of code.

We can use a Symbol to define a text label as EQUal to a number

Here is the result.

Workspace Register Addressing
Workspace Registers are normal registers (R0-R15)

Workspace Register Addressing is the use of a register as the source or destination of an operation... in the case of MOVe, the source is on the Left, and the destination is on the right.
Here is the result

Workspace Register Indirect Addressing
We'll often want to load data from an address... and this address may be in a register.

Workspace Register Indirect Addressing is where the source or destination of an operation isn't a register... but the address in the register.

This is denoted by an asterisk * before the register - the address in the register will be used as the source or destination

The registers will be loaded from the address in the register.
For example R3 has loaded 1122h from the address 8300h in R0

Workspace Register Indirect Auto Increment Addressing
We've learned we can read from an address in a register by prefixing with * , but many times we'll want that address to change
For example, if we're reading sprite data, or filling an area of ram with data.

We can automatically use the address in a register and increment it by keeping the * prefix and adding a +
Here are the results

In this example we've loaded two bytes, so the register went up by 2
If we use MOVB *R0+,R1 - then it will go up by 1.


Symbolic (Direct) Addressing
This is where we are loading from a fixed address... we just put an @ at the start of a number. (without the @ it would be an immediate value)

We can also use a symbol if we wish

Remember... Words must be loaded on even boundaries... odd boundary reads will malfunction
Here are the results

Indexed Addressing
Indexed addressing uses the value in a register plus an offset (index)... First we put an @ symbol, next the offset number, and a register to use in brackets...

for example, look at the sample to the right ->

in this case the offset is 4 and the register is r3... if R3=10 then the final address is 14
@4(R3)
We can specify an index (offset) as a Positive number or a Negative number

We can use Symbols to make things easier to read.

Symbols will often be used to read offsets in data arrays...
For example a game object may have 8 bytes of data... an Xpos at offset 0, Ypos at offset 2, Sprite Address at offset 4, and Speed at offset 6
We would point our register to an object, and run the routines for that object, then move the register to the next object and repeat

*** This command cannot use register R0 as the source ***
Here are the results

In most cases these addressing modes can be used as the source or destination of a command

If you try and addressing mode that isn't possible, the assembler will soon tell you.. so give it a go!

Lesson 3 - Comparisons and Byte commands
We've looked at basic commands for registers, and various addressing modes, but we've just got started.

Although the registers are 16 bit, the TMS9900 has 8 bit 'byte commands'... we also need to learn about compare commands... lets learn about it!


Byte Commands
Byte commands on the TMS9900 are a bit weird... most processors use the bottom byte of a register, but the TMS9900 uses the TOP byte of the 16 bit registers!

To load an 8 bit byte, we use MovB (B for byte)... this will load in from an address, or another register.
Here is the result... note the difference between Word and Byte commands...

The top byte is loaded in... the bottom byte is unchanged
We can Add bytes with the AB command

We can Subtract bytes with the SB command
Only the top bit is affected.
We can swap the high and low byte of a register with SWPB
Here is the result... top and bottom bytes of each register were swapped.

Branch (B)... Branch and Link (BL) and Jump!
Branch would be called JMP (jump) on most processors... it allows to move to a different address in the code without return

We can load an address into a register and branch to it with *

Alternatively we can specify a label (or address) with the @ symbol, and the code execution will continue from that point.

if we want to call a subroutine and return we want to use BL (Branch and Link)... we specify BL with @ and a label

Because the TMS9900 doesn't have a stack, we need to handle the return address... BL puts the return address in R11, and we can return with B *R11

If we want to use R11 for something else (or a nested BL) we'll have to do something else with it... for example move it to R10
Here is the result.

On systems like the 68000 BRAnch can only jump short distances, but JuMP can jump anywhere.
On the TMS9900 it's the opposite!...JMP is for close ranges...Branch can go anywhere!
Because we don't have a stack, nested subroutines will have problems with re-using variables... and keeping the return address is a pain.
However, there's a solution!

'Workspaces' give us a full set of new registers for our sub (using 32 bytes of ram)... we'll learn about them soon!

Compare... Equals / Not Equals (Zero / NZ)
Like most CPU's, we can use the status flags to do conditional jumps (branches)

When we want to compare values, we have 3 options...

CI will compare a register to an immediate

C will compare 16 bit values in registers

CB will compare bytes (the top byte) of the registers
These commands will set the flags... when we want to actually act on the comparison we need to use special Jump commands

JEQ will Jump if EQuals (if the zero flags is set - if the difference is zero)
JNE will Jump if Not Equals (if the zero flag is not set - if the difference is not zero)

The results of these test will depend on the commands you run!

It's impossible to show enough screenshots to make it clear how these commands work.
You'll need to download the examples, rem out and unrem some of the lines, and see how the results change!... Quit being lazy! go download the devtools!

Unsigned... Higher / Lower / Higher or Equals / Lower or Equals
If we're working with Unsigned numbers, after a compare we have four branch options.

JH will Jump if the first parameter is Higher than the second

JL will Jump if the first parameter is Lower than the second

JHE will Jump if the first parameter is Higher or Equal to the second

JLE will Jump if the first parameter is Lower or Equal to the second

Signed... Greater Than / Less Than
If we're working with Signed numbers, we have two options

JGT will Jump if the first parameter is Greater Than the second

JLT will Jump if the first parameter is Lower Than the second

Note, that 7000h is a positive number... but 8000h is a negative one (in signed numbers) so we must use the correct conditional jump or we may get the wrong result!

Carry Flag
The carry flag is set whenever an operation pushes a bit out of a register - it's effectively the '17th bit' of a register

In this example, we'll repeatedly INC R0 until it overflows (Setting the carry)...

We have two jump options

JNC will jump if there is No Carry

JOC will jump On Carry
When R0 rolls over... the Carry Flag will be set.

Overflow Flag
The Overflow flag is used for Signed numbers...

The limit of a Signed number is -32768 to +32767 ... this causes a problem!

If we have a register with 7FFFh in it (+32767)  and we add one it will change to 8000h... -32768!... the sign has flipped and the number is invalid!

To detect this we have the overflow flag... we have only one Jump option

JNO will Jump if there is No Overflow.

Of course, if execution continues without a jump that must mean there was an overflow.
When the register overflows the oVerflow flag will be set

Parity Flag
Parity is a bit special!... it's designed for error checking - for example, when reading data from tape or punched cards

It only works on byte commands.

The parity flag is set according to whether the count of '1' bits in the byte is odd or even

To detect this we have the Parity flag... we have only one Jump option

JOP will Jump on Odd Parity

Of course, if execution continues without a jump that must mean parity was even.
When the '1 bit count' is odd... the Parity flag is set

Lesson 4 - Wonderous workspaces and Mega Maths!
It's time to learn how the TMS9900 copes without the stack (Spoiler: Workspaces!)

We'll also learn about the Multiplication and Division commands.


What's these workspaces?
All through these tutorials we've used Registers R0-R15 for various purposes... 

However these are not 'Registers' like BC and DE are on the Z80, or X and Y are on the 6502... These are actually held in an area of memory called the 'Workspace'...
Effectively they are like the Zero page on the 6502

In fact the TMS9900 only has 3 'true' registers in the CPU... WP,PC and ST - they're all 16 bit.

WP is the Workspace Pointer - this points to the start of a 32 byte (0020h) range of memory used by workspace registers R0-R15 (the ones we've used so far!)
PC is the program counter - this is the pointer to the running byte of our program
ST is the STatus flag register
Before we can use the WorkSpace Registers we need to define the Workspace Pointer

We do this with LWPI ... Load Workspace Pointer from Immediate

The Header (V1_Header.asm) has been doing this for us in these tutorials... it's been setting the workspace to Ram address 83C0h (Ram is from 8300-83FFh)

BLWP - Branching and Linking with a new Workspace Pointer
Before we used Branch and Link - but we had to be careful with R11 (the return address) making nesting difficult or impossible - we also had to ensure we didn't change any other registers which could cause problems.

The solution is BLWP - this will keep the old registers intact, and use a different 32 bytes of memory for a new set of Workspace registers - when we return, the old workspace will be restored, keeping the old registers intact!

We do this with BLWP - this points to a Transfer VECTOR - Two Words which will define the the new vector.
BLWPTEST is the Vector table - it contains two words

The first is the address in RAM for the new workspace for registers R0-R15 (32 bytes)... this goes into register WP

The Second is the address of the subroutine where execution will resume... this goes into register PC
When BLWP occurs some of the previous register values are transferred.

Old Register (Before BLWP) 
New Register (after BLWP)
WP (Workspace Pointer) R13
PC (Program Counter) R14
ST (STatus Flags) R15
Transfer Vector Word 1 WP (Workspace Pointer)
Transfer Vector Word 2 ST (STatus Flags)

R13,R14 and R15 are used by the return command RTWP (ReTurn Workspace Pointer) - so must not be changed by the subroutine.
RTWP will restore the original Workspace, and carry on with the command after BLWP

Our Test Sub just makes a few registers changes as an example then returns
The Subroutine used 83A0h as it's workspace...

The Register changes of this subroutine can be seen in ram after it runs.
We can nest subroutines using BLWP again within our subroutine just fine... PROVIDING the subroutine does not use the same Workspace Ram as one of it's parents
Otherwise the return address would be corrupted ... Also Two different Subs can have the same WorkSpace providing they won't be nested within each other
You can always have different Transfer Vector Tables - with different choices of workspace depending on the nesting!
In theory, you could never use BL, and just use BLWP... but remember the TI99/4A only has 256 bytes of RAM... Enough for just 8 workspaces... so you'll need to try to limit yourself to as few workspaces as possible.

Working with WP (Workspace Pointer) and ST (STatus flags)
Apart from BLWP and RTWP there are 3 special commands for working with WP and ST registers

LWPI is Load Workspace Pointer from Immediate - this is how we set our default workspace pointer

STWP is STore Workspace Pointer - this transfers the current Workspace Pointer to a register (R0-R15)

STST is STore STatus register - this transfers the current STatus flags to a register (R0-R15)
WP was transferred to R0

ST was transferred to R2

Multiplication and Division... in 32 bit!
Regular commands are all 16 bit... but MPY and DIV are different
When it comes to Multiplication and Division a pair of registers are combined into a High.Low pair.... we specify the first, and the following register will be used as well so if we specify R0 then R0+R1 would be used as a pair.

mpy r0,r2 will Multiply R0 by R2... the result will be stored in R2.R3 (R2 = High Word / R3 = Low Word)
div r0,r2 will Divide R2.R3 by R0 ... the 'whole number' result will be stored in R2... any remainder will be stored in R3
In this example we first Multiplied R0 by R2... The result was stored in R2.R3

Next We Divided R2.R3 by R0... The result was stored in R2 - the remainder was in R3

Where as most commands can use any addressing mode as a source or destination, the 32 bit register pairs will not work with memory addresses.

Lesson 5 - Bit Operations.
We're nearly at the end of our tutorials... but we've not yet looked at the Bit shift and Logical Operations'

We'll also have a look at a few other 'conversion commands' for switching values around


Bit Shifts
The TMS9900 has a variety of bit shifting commands... what's better is they all allow a shift amount of 0-15!

The commands available are:
SRC - Shift Right Circular (ROR on the 68000)
SRL - Shift Right Logical
SRA - Shift Right Arithmetic
SLA - Shift Left Arithmetic (Also works as SLL)

All these work with shift sizes of 0-15...SRC with a shift of 15 would effectively act like Shift Left Circular (SLC doesn't actually exist)
SRC will shift the bits to the right, bits that drop off the right side will reappear on the left
SRL will shift the bits to the right, New bits that appear on the left will be zero

This will halve an unsigned number, but may break a signed one
SRA will shift bits to the right - New bits that appear on the left will be the same as the last top bit

This will halve an signed number, but may break an unsigned one
SLA will shift bits to the left - New bits that appear on the right will be zero

This will halve an signed or unsigned number.

Bit based Mathematical transformations
There are a few mathematical operations that alter register values relating to bits (?)

NEG r1 converts a positive to negative and vice versa - effectively flipping all the bits and adding 1
INV r1 will flip all the bits in a register.
XOR r1,r2 will flip the bits of r2 where a bit in R1 is 1... effectively if r1=FFFFh then this is the same as NEG

ABS will remove the negative sign from a negative number... it will have no effect on a positive number
Here are the results of the commands.

Logical Ops, Set and Clear
We have 6 logical operation commands... though two are just 'byte versions' of others.

ANDI will AND an immediate value - effectively zeroing bits in R0 which are not 1 in the parameter
ORI will OR an immediate value - effectively setting to 1 bits in R0 which are 1 in the parameter

SOC Sets to 1 any bits in the destination that are 1 in the source... it's effectively the same as OR
SZC sets to 0 any bits in the destination that are 1 in the source... it's the equivalent of AND where all the bits of the parameter are flipped.


SOCB is the same as SOC, but works on the top byte of the register (BYTE)
SZCB is the same as SZC, but works on the top byte of the register (BYTE)
Here is the result

Compare ones, compare Zeros
We have some special commands for comparisons

COC will 'Compare ones corresponding'... this will check all the 1's in the first parameter, are 1 in the second... setting the zero flag accordingly
The zero flag changes according to the values in the two parameters.
CZC will 'Compare Zeros corresponding'....this will check all the 1's in the first parameter, are 0 in the second... setting the zero flag
The zero flag changes according to the values in the two parameters.

COC and CZC are the closest we have to BIT or BTST commands on the TMS9900,

They're a bit weird compared to the Z80, but the PDP-11 was also similar in it's commands.