Techno-Plaza
Site Navigation [ News | Our Software | Calculators | Programming | Assembly | Downloads | Links | Cool Graphs | Feedback ]
 Main
   Site News

   Our Software

   Legal Information

   Credits

 Calculators
   Information

   C Programming

   Assembly

     Introduction

     Keyboard Input

     Basic Graphics

     C & Assembly

   Downloads

 Miscellaneous
   Links

   Cool Graphs

   Feedback Form

TIGCC Assembly Lessons

TIGCC Assembly Programming Lessons

Lesson 1: Introduction to TIGCC Assembly Programming

Introduction

If you are reading this, I assume you are interested in learning to program in 68000 assembly. More specifically, these lessons will describe programming for the TI-89 and TI-92+/V200 calculators.

Most assembly programming is very easy to do if you understand a few basic things. People think that is is complicated, but that is inaccurate. It is really just time consuming. Once you have the basics down, assembly programming is no more difficult than C. These lessons are designed to teach you these basic tenets, and show you where to go for reference.

So what is required for programming in assembly? Three things. First, you need a development environment. We will be using TIGCC, which, despite the name, also has an assembler we can use for our programs. The second is the assembler syntax. This is what people think of when they hear about assembly. These include the opcodes (processor instructions), the assembler syntax (GNU as uses 'AT&T' syntax), and the related assembler directives. These things will comprise your program. The final thing to know and understand is how the system interacts with assembly. The calculator has an operating system known as the AMS (Advanced Mathematics System) which has tons of built-in functions we can use to aide our programs. We will need to know how to setup an assembly program to be called from the AMS, and how we can call on the AMS functions for use in our own programs.

I hope these lessons serve you well.

Step 1 - Setup the Environment

The first thing I said we needed was a development environment. For our purposes, we will be using TIGCC. TIGCC is a collection of tools including a GUI IDE, an assembler which turns our code into machine code, a linker which turns the machine code into a calculator program, and a library of special functions in addition to what the AMS provides which may prove useful. One other very important thing is the TIGCC docs which provide a wealth of information to assist us. We will be using these quite often.

Because our programs will need to be tested, we will need somewhere to run them. You can run them on your own calculator, but this is not recommended. It's very easy to make a small mistake in an assembly program, and there are a seemingly endless number of innocent mistakes which will cause your program to crash your calculator. A better solution is to use an emulator, i.e. a program that works the same as the calculator, but runs on your computer. It's faster to test with, easier to reset, and doesn't waste battery power. The emulator we will be using is TiEmu. It's development is aided by members of the TIGCC team and is the standard choice for program testing.

Instructions for installing and configuring these two programs can be found in the first TIGCC C programming lesson in steps 1 and 2. The programs are available for download from our archives. You do not need to add the a68k assembler in the TIGCC setup. We will be using GNU as instead, which is included in the C compiler.

Step 2 - Creating a Program with TIGCC

Now that you have the environment setup, let's setup a program. TIGCC is useful for so many things, but one thing it will help us with is program setup. Most pure assembly programs need to perform some tasks to ready their program to be called from an operating system like the AMS. TIGCC adds this code for us in C programs, so we will make hybrid programs.

No, this doesn't mean we're not programming in assembly. In fact, all C programs are assembly programs. The C compiler turns all the C code into assembly anyway. This is why our assembler is included with the C compiler. And don't worry, you don't need to know C. We are just going to use the TIGCC C _main function as the start point of our program. This will save us the hassle of doing our own initialization.

Two things that TIGCC will take care of for us is saving and restoring the screen and determining which calculator to compile our programs for. If you look at the program options under the Project Menu, Options, Compilation Tab, Program Options, you will see several options. The calculator tab determines which calculators we will compile for. This is usually all of them. And if you scroll down to the Home Screen tab, you will see the Save/Restore LCD contents. If we don't do this, the home screen will get destroyed. These are just handy built-in things of TIGCC that we have no reason to duplicate.

Let's try writing a program now. Start TIGCC and choose New Project from the File, New menu. Then Create a new C Source File from the File, New menu. You can call this file main. Now create a new GNU Assembly Source file from the File, New menu. You can call this hello. Save the project somewhere as hello. Now let's edit these files.

If you want, you can download these files from our archives.

main.c


#include <default.h>

// C prototype for our assembly function
void asm_main(void);

void _main(void) {
    asm_main();
}

hello.s


|-------------------------------------------------------------------------------
| program constants

    .equ    AMS_jumptable,0xC8
    .equ    ClrScr,0x19e
    .equ    DrawStr,0x1a9
    .equ    ngetchx,0x51

|-------------------------------------------------------------------------------
| text section

    .text
    .xdef asm_main

asm_main:
    movem.l %a4-%a5,-(%sp)      | save a4/a5 registers

    movea.l AMS_jumptable,%a5   | load the AMS jumptable into a5

    movea.l 4*ClrScr(%a5),%a4   | load the ClrScr function
    jsr     (%a4)               | execute the ClrScr function

    move.w  #1,-(%sp)           | set default color (black text on white bg)
    pea     str(%pc)            | the string to print
    move.w  #0,-(%sp)           | set (x,y) coordinate to (0,0)
    move.w  #0,-(%sp)

    movea.l 4*DrawStr(%a5),%a4  | load the DrawStr function
    jsr     (%a4)               | execute the DrawStr function

    lea     10(%sp),%sp         | reset the stack pointer

    movea.l 4*ngetchx(%a5),%a4  | load the ngetchx function
    jsr     (%a4)               | execute the ngetchx function

    movem.l (%sp)+,%a4-%a5      | restore a4/a5 registers

    rts                         | return from subroutine

|-------------------------------------------------------------------------------
| data section

    .data

str:
    .string "Hello, World!"

Build the program by choosing make from the Project menu. Start TiEmu and send the program to it by choosing Run from the Debug menu. It will look like this screenshot.

Screenshot

 

Step 3 - Interpreting the Program

I'm sure this all seems a little confusing right now, but it won't soon. If you have used 68000 assembly in the past, it may or may not look familiar depending upon the assembler syntax you are used to. GNU as, like most un*x assemblers use AT&T syntax, while PC assemblers have traditionally used Intel syntax. The differences are subtle, and are not hard to get used to.

I'm going to completely ignore the main.c file, because it is not germane to what we are trying to learn here. It only exists so that TIGCC will save/restore the screen and so the program gets compiled for the right calculators. We could do this ourselves, but why duplicate the work TIGCC will do for us? We wouldn't copy their code into a C program, so why do that in in assembly? Keep the main.c file handy though, because it will be used in later lessons. If you know C and understand what it means, great. If not, don't worry about it.

Our work will begin in the hello.s file. I want to start by pointing out that the text following the | (pipe) are comments. They are used to make the code more readable. This is of great importance in assembly programs where readability is at its lowest.

Step 3a - Assembly Program Sections

Let's just start with an overview of what the program does. The program does three things. First, it clears (erases) the screen. Second, it prints the string "Hello, World!" at the top of the screen. Third, it waits for the user to press a key. This is done so that we can see the string before the screen is restored to its original contents.

Now that we know what the program is supposed to do, let's see how it does it. We'll just start at the top and work our way down.

|-------------------------------------------------------------------------------
| program constants

    .equ    AMS_jumptable,0xC8
    .equ    ClrScr,0x19e
    .equ    DrawStr,0x1a9
    .equ    ngetchx,0x51

The first lines declare some constants we will be using in the program. The mnemonics following the . (dot) are assembler directives. They have special meaning for the assembler. In this case, it means the two things that follow are equal. Whenever the assembler finds one of the first things, it will replace it with the second. So, every time in the code we see AMS_jumptable, it will be replaced with 0xC8. This is the hex number C8, or decimal 200. The use of these constants will become clear later in the program.

|-------------------------------------------------------------------------------
| text section

    .text
    .xdef asm_main

Here is an especially cryptic part. We see the . (dot) again, so these are more assembler directives. The first one .text says that everything that follows this line will be in the text section. The text section holds all your program code. Any code should be inside a text section. Since the text section is the default section, this line wasn't actually necessary, but I wanted to include it so we could talk about the text section.

The next line is an xdef directive. xdef stands for eXternal DEFinition. asm_main is our assembly function. It is where we will write our program code. Since we have combined assembly and C, we called this function from our C _main function, which is where the program actually starts. This line is needed to tell the assembler that other parts of the program will need to know about asm_main. Specifically, the _main function in our C program needs to know about it, otherwise, it can't call it. Whenever you have a function that will be used outside your current module (usually another file), you need to use an .xdef directive. Try commenting it out and see if you can still build the program. The compiler will complain about not being able to find asm_main.

asm_main:
    movem.l %a4-%a5,-(%sp)      | save a4/a5 registers

    movea.l AMS_jumptable,%a5   | load the AMS jumptable into a5

We have finally reached some code. Each 68000 assembly opcode is broken into three pieces, the instruction, the source operand, and the destination operand. Sometimes one or both of these operands are not needed, but this is the basic format.

movem and movea are processor instructions. It tells the processor to do something. The .l is the size of the operation. The items before the comma are the source operand, and the items after the comma are the destination operands. Knowing this, let's examine our first opcode.

movem stands for MOVE Multiple registers. It is a commonly used instruction that allows us to save registers we are going to use. Although we can save them anywhere, the stack is the typical place to save registers.

I don't want to gloss over anything, so let's talk about the 68000 processor real quick. The 68000 processor is a 32-bit CPU which has 16 general purpose registers, a program counter, and a status register. A register is like a variable. It's a small piece of memory on the CPU that stores data. We use registers to perform arithmetic and help us address data sets. There are eight general purpose data registers, called d0, d1, d2, ..., d7. These are used to store data like real numbers and perform math operations. There are eight general purpose address registers called a0, a1, a2, ..., a7. The eight register, a7 doubles as the stack pointer, and so its use is reserved. Address registers are used for program control and tracking sets of data. Their use will become more clear later. The program counter register keeps track of the next opcode to execute. The status register informs us about the results of certain opcodes. For example, if two numbers were subtracted, the status register would know if a 0 or a negative value was the result. There are many other uses for the status register as well.

We said that the 68000 is a 32-bit CPU. This means two things. First, we can address memory up to 2^32-1 bytes (slightly more than 4 GB). Second, that all the registers are 32-bits long. This means each register can store a value up to 2^32-1 (around 4 billion). Note that we have no where near that much memory on the calculator, so the addressing fact is a bit useless. I just want to cover the basic architecture before we go on.

Okay, now that we know what a register is, let's cover the stack. The stack is simply a place in memory we reserve for temporary storage. The stack on the calculator is defined by the AMS and is addressed using the a7 register (aka the sp - stack pointer register). It is roughly 16 KB.

The next real quick thing I want to address is the temporary registers. The system (i.e. the AMS) assumes that d0-d2 and a0-a1 are temporary registers, and their values can change at any time without consequence. This means anytime you call a function, these registers may be destroyed. The rest of the registers are considered to keep their value. This means if we are going to change them, we better save the old values. If we don't, the calculator will crash.

That's a lot of background for a single instruction, but at least we understand why we're doing it now. So, back to movem. movem takes a group of registers and moves them somewhere. In our case, we will move them to the stack. We are going to use the a4 and a5 registers in our program, so we'll need to save their values.

You may have noticed the % (percent) sign in front of the registers. This is part of the AT&T assembler syntax to differentiate registers from other symbols. The way we specify which registers to move is also part of the assembler syntax. For our assembler (GNU as), we simply write the registers. We can use the - (dash) for a range, as in %a0-%a2 (a0, a1, and a2), and we can separate with the / (slash) as in, %d2/%d5. So, if we wanted to specify d2,d4,d5,d6,d7,a3,a5, we could do %d2/%d4-%d7/%a3/%a5. It may seem complex at first, but it's really not.

Okay, that covers the source, now let's tackle the destination. -(%sp) means to place the registers to the place in memory the stack pointer is pointing at. That's the (%sp) part. Parentheses mean indirection, as in, don't move me to the stack pointer, move me to the place the stack pointer is pointing. The - in front is pre-decrement. This means we move the stack pointer up before we do the move. This is so we don't overwrite the data already on the stack. We want to put it on top of the stack, so we need to move up before moving the registers.

The only thing left to note is the size operator, the .l. This means longword, which is 32-bits, the size of the registers. We need to make sure we are saving the entire register, so we specify .l. There are other specifiers, .w and .b which stand for word (16-bits) and byte (8-bits) respectively. Certain opcodes do not require size suffixes, but we do need one here.

If you're still reading, I promise it gets easier. It just seems complex because there's a lot of things to explain. It will become second nature soon enough. Let's continue with the movea instruction next.

The movea instruction will be much simpler. movea is short for MOVE to Address register. We use movea when we want to put a value into an address register. In this case, we are moving the value of AMS_jumptable into the a5 register. We will see why very soon. See how easy that one was?

    movea.l 4*ClrScr(%a5),%a4   | load the ClrScr function
    jsr     (%a4)               | execute the ClrScr function

Now we start doing something. We are going to use the movea instruction again to move a value into an address register. We have a new kind of source value though. ClrScr is one of our constants from the top, and the assembler will automatically do the math for us, so we end up with 1656(%a5). Remember that parentheses are indirection. In other words, we want the value that a5 is pointing at. Our last movea instruction moved the AMS_jumptable address into a5. But we don't want that value. We want the value that is 1656 bytes after it. This is what the number means in front of it.

So, what does this really mean? Well, as you will recall, the AMS has many built-in functions that we can use to help us in our programs. ClrScr is one of these functions. Because the AMS software changes from time to time, TI includes a jump table of all its functions addresses so that programs will still work on newer AMS versions.

If you are not familiar with a jump table, here are the basics. First, we have a list of addresses. This is the jump table. Starting from the first address, we use an offset to find the address we want. This offset is always the same. For us, this offset is at 4*ClrScr into the jump table. The multiplier 4 is used because we are working in bytes, and addresses on a 32-bit machines are 4 bytes.

So, we have moved the address of the jump table offset into a4. This means a4 is pointing at the location in the jump table where the address of the ClrScr function is stored. So, one more indirection from a4 will give us the actual address of the ClrScr function, as stored in the jump table. Finally, we call the jsr (Jump to SubRoutine) with that indirection to call the ClrScr function. As you might have guessed, ClrScr clears the screen.

    move.w  #1,-(%sp)           | set default color (black text on white bg)
    pea     str(%pc)            | the string to print
    move.w  #0,-(%sp)           | set (x,y) coordinate to (0,0)
    move.w  #0,-(%sp)

    movea.l 4*DrawStr(%a5),%a4  | load the DrawStr function
    jsr     (%a4)               | execute the DrawStr function

Now that we have cleared the screen, it's time to draw our "Hello, World!" string. To do that, we will call the DrawStr function.

DrawStr is a little more complicated than ClrScr. We need to supply it some information. This information is called the function arguments. One of the nice things about TIGCC is that it has all the AMS functions documented. If you go to the TIGCC docs to the index and search for DrawStr, you will find a description of the function and its arguments.

From the TIGCC docs, we need to pass 4 arguments to DrawStr. Passing arguments to functions is very platform-specific. Almost all of the built-in AMS functions pass their arguments on the stack. This is called the 'calling conventions'. We might learn about other calling conventions later. In the TIGCC docs, you can tell if a function is part of the AMS because at the top right they say Function (ROM Call [some hex number]). This is how we know to pass the arguments on the stack, because nearly all the AMS functions have this calling convention.

The other part of the calling convention for AMS functions is that the arguments are pushed in reverse order. This means we push the last one first and the first one last. The four arguments are the Attr (how the string is drawn), the string pointer, and the y and x coordinates to draw at. It is helpful to know a little C so you can read the TIGCC docs to learn about the AMS functions, but we will go over things to help you out.

A short (or a short int) is word-sized (16-bits .w). A const char * is a pointer to an address, and addresses are longword-sized (32-bits .l).

The three move opcodes should be pretty obvious. We move those values to the stack. The less obvious one is the pea instruction. pea stands for Push Effective Address to the stack. An effective address, or EA for short, is an assembly term. It is a way of specifying a value or address according to specific rules. Here, we want to put the address of our "Hello, World!" string onto the stack. To do that, we use a PC relative effective address. PC (for program counter) relative means we will look for another address using the current instruction as a pointer. This is often how data is addresses, because data is usually close to the code you're working with.

Later, we will see where string is defined. For now, just know that the assembler will figure out the address of str and replace it for us. The move and jump opcodes we have seen before from ClrScr, so I won't go over it again.

Remember above when I said sometimes one of the operands is not needed. The pea instruction is one of those. It has only a source operand. The destination is always the stack.

    lea     10(%sp),%sp         | reset the stack pointer

    movea.l 4*ngetchx(%a5),%a4  | load the ngetchx function
    jsr     (%a4)               | execute the ngetchx function

Once we get back from our DrawStr call, we need to reset the stack pointer. If we don't, then we'll just keep adding stuff until it overflows, and then the calculator will crash. To reset the stack pointer, all we need to do is move the pointer the number of bytes we put on it.

We pushed 3 words and 1 address (longword) onto the stack. Words are 16-bits (2 bytes), and longwords are 32-bits (4 bytes), so 3 * 2 + 4 = 10 bytes. The lea instruction, short for Load Effective Address will load an effective address into an address register. Note that we could also have used movea here. I'm not sure right off which is more efficient, but it shows you there is often more than 1 way to do something. If you wanted to use movea, it would be movea.l 10(%sp),%sp.

The ngetchx function call is similar to the ClrScr and DrawStr function calls. If you look up ngetchx in the TIGCC docs, you will see it takes no arguments, so there is nothing to push on the stack. We don't care about the return value either, so we'll just ignore that. The ngetchx function waits for a key to be pressed. Remember above we said we needed to do this so we could see the result before the program exited.

    movem.l (%sp)+,%a4-%a5      | restore a4/a5 registers

    rts                         | return from subroutine

Before we end our program, we need to restore the registers we saved at the beginning. This is another reason we had to restore the stack pointer after our call to DrawStr, otherwise we would have moved the wrong values into our registers. This call is basically the opposite of what we saw in the original movem. The only difference is that the + post-increment doesn't change the stack pointer until after we have moved the registers. The compliment to pre-decrement is post-increment.

Finally, we have the rts instruction. rts has neither a source or a destination operand. Its job is to return from a subroutine, or what we are calling a function. This rts basically exits our program, although technically there is code from TIGCC that restores the screen that will be done before we really exit. We don't need to worry about that though. This is our programs exit point. TIGCC is just basically cleaning up after us.

|-------------------------------------------------------------------------------
| data section

    .data

str:
    .string "Hello, World!"

I mentioned the data section earlier. This is where all your data variables and constants will be stored. For this program, we have just one item, our string literal. Notice that the .data directive is used like the .text directive was to say that everything after this line is part of the .data section.

Now we have the str string literal. When declaring variables or constants, we need a label. str is this label. The .string directive is used to define a string literal.

Back when we used pea to push this on the stack, I said the assembler would replace the value for us. But it doesn't push the entire string, only the address of the string relative to the program counter. I think it's 50-something, but it's not important. The assembler will take care of this for us.

Step 4 - Lesson Conclusion

I know this has probably been a very difficult first lesson. Assembly programming is not the easiest thing to grasp. It is terse, has little documentation, and does very few things for you. But it does get easier with time. If you are serious about learning assembly, I have no doubt you will get the hang of it.

It took me about 6 months to get familiar with assembly, and I didn't have any programming lessons to read, so you're already a step ahead of me. You don't have to figure out what the code means by reading and re-reading it like I did.

I hope the lesson made sense. Please use the feedback form to contact me if it didn't.

 


Lesson 1: Introduction to TIGCC Assembly Programming
Questions or Comments? Feel free to contact us.


 

Copyright © 1998-2007 Techno-Plaza
All Rights Reserved Unless Otherwise Noted

Get Firefox!    Valid HTML 4.01!    Made with jEdit