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.