This program is more complex than the one in lesson 1, but because we
understand the basics, it should actually be easier. Let's go over what
the program is supposed to do first.
This program is going to ask the user to press a key. If the user
presses the ESC key, the program will end. If the user presses any other
key, we will display the keycode value of that key. We will keep
checking for keys until the user presses ESC to quit.
Once again, I plan to ignore the main.c program. It's the same loader
program we used last time. We'll be using it again, too.
Let's start at the top and work our way down.
.include "os.h"
.equ AMS_jumptable,0xC8
.equ KEY_ESC,0x108
.text
.xdef asm_main
Most of this should be familiar from lesson 1. The only new thing we
have is the .include directive. The .include directive adds the contents
of one file to this file. In this case, we are including the file os.h.
os.h is a header included with TIGCC for assembly programming. It
contains definitions for all the AMS functions. This means we won't be
defining these functions anymore. Let's move on.
asm_main:
movem.l %a4-%a5,-(%sp) | save registers on stack
movea.l AMS_jumptable,%a5 | load the AMS jumptable
movea.l 4*ClrScr(%a5),%a4 | ClrScr();
jsr (%a4)
This should be familiar from lesson 1, so we'll go over it real quick.
We save the registers on the stack and load the jumptable into a5. Then
we use the jumptable to call the ClrScr function.
move.w #1,-(%sp) | DrawStr(0, 0, message, 1);
pea message(%pc)
clr.l -(%sp)
movea.l 4*DrawStr(%a5),%a4
jsr (%a4)
adda.l #10,%sp | restore stack pointer
Next we call the DrawStr function to draw the message string on the
screen. This is similar to things we have done before, but there are a
couple subtle differences I want to go over.
Note that instead of moving the 0,0 to the stack, we used the clr
instruction. clr, short for CLeaR sets a value to zero. The size is a
longword, which is the same as two words. This is just another way of
moving two 0 word values to the stack. It is slightly faster as well.
Finally, when we restore the stack pointer, we use adda instead of lea.
I mentioned in the last lesson we could have used movea as well. This is
just another way to restore the stack pointer. We put 10 bytes onto the
stack, so we need to move the pointer 10 bytes down to reset it.
At this point, I want to talk about the Motorola Programmer's Reference
Manual. It is a guide to all the instructions on the 68000 and its
related processors. This is a very helpful reference for the 68000
opcodes which you will probably want to look over. You can download it
from our archives. The
adda instruction is discussed on page 110. The instruction docs start
on page 106 with ABCD. Note that some instructions are not available on
the 68000. You will want to have a look at this, as it tells you
everything about an opcode. What it does, how to use it, and what
effective addressing modes you can use with it. For now, let's continue
with the analysis.
getKey:
movea.l 4*ngetchx(%a5),%a4 | %d0 = ngetchx();
jsr (%a4)
There are two things to note here. First, we are calling the ngetchx()
AMS function. Note in the comments, we say that %d0 will equal the
result of ngetchx(). If you look in the TIGCC docs, you will see that
ngetchx returns the keycode for the key the user pressed. Remember from
lesson 1 where we talked about calling conventions. Calling conventions
also deal with return values and how we return a value from a function.
Most of the time, we return values in either the d0 or the a0 register.
We use d0 for values, and a0 for addresses. Since we are returning a
short, which is a word (16-bits, 2 bytes), the return value will be in
%d0.w (the lower word of the %d0 register).
The other thing to notice here is we have another label. We haven't
talked much about labels yet, but labels are the assemblers way of
marking positions for us. This is the part of the program that is going
to get a key for us, and we may need to do that again if the user
doesn't press ESC. This label will allow us to come back to this place
in the code later, if we so choose. Note that the label will not be
part of the final program, but will be replaced by the assembler with
the address, just like our string literals.
cmpi.w #KEY_ESC,%d0 | Did the user press ESC?
bne notEsc | no? then goto notEsc
| otherwise,
Here we introduce some new instructions, cmpi (CoMPare Immediate) and
bne (Branch if Not Equal to zero). This is one of the ways we construct
if-then-else blocks in assembly. We test a condition, and then jump
somewhere depending upon the result of that test.
Remember that the ngetchx function returned our keycode in the d0
register. We want to test for the ESC key. We defined the value of the
ESC key in our .equ directives. I got this value from the CommonKeys
enumeration in the TIGCC docs. The ESC key has a value of 264 decimal,
or 0x108 hex.
You may be wondering about the # (pound) sign in front of KEY_ESC. We
use this to indicate a value, rather than an address. We are comparing
the value 0x108 to %d0, not the value stored at the address 0x108. Note
the difference when we move the jumptable address into a5 earlier. Here,
we don't want the value 0xC8, we want the value stored at the address
0xC8. So there is no # sign in front of AMS_jumptable.
The cmpi instruction sets certain properties of the status register. We
touched on this briefly in lesson 1, but here is a demonstration. We
want to know if ESC was pressed. In other words, we want to see if the
value returned by ngetchx (which is in d0, remember) is the same as the
known value for the ESC key (0x108). If it is, the status register's
Z (zero) flag will be set.
If we look in the Motorola Programmer's Manual on page 183, the cmpi
instruction is described. You may be wondering why a zero flag would get
set. Here is your answer. At the top of the page, Destination - Source
-> CC. That's a little cryptic, so let's translate. Take the destination
and subtract the source from it. Set the condition codes (the flags on
the status register) according to the result of this subtraction. So,
the zero flag will be set if we pressed ESC, because 0x108 - 0x108 = 0.
If we don't get a zero, then the user pressed something other than ESC.
We know a key was pressed, because ngetchx waits until a key press has
occurred.
Moving on, we now know that the status register's zero flag with either
be set or clear, depending upon the result of our compare. If it is
clear, then we use the bne instruction to jump to the notEsc label. The
various branch instructions enable us to act on the results of compare
tests. These two opcodes basically say, if the keycode was not equal to
the ESC keycode, then jump to the notEsc label.
move.w #1,-(%sp) | DrawStr(0, 30, success, 1);
pea success(%pc)
move.w #30,-(%sp)
clr.w -(%sp)
movea.l 4*DrawStr(%a5),%a4
jsr (%a4)
adda.l #10,%sp | restore stack pointer
movea.l 4*ngetchx(%a5),%a4 | ngetchx();
jsr (%a4)
movem.l (%sp)+,%a4-%a5 | restore registers
rts | exit
Before we check out the notEsc label code, let's see what happens if we
didn't branch. If we didn't branch, then we fall through to the next
opcode.
The remainder of the code is pretty straightforward, as you have seen it
all before. We'll go over it real quick. We call the DrawStr function to
print the success string. Then we call ngetchx again to wait for the
user to press another key before we exit. The stack pointer is restored
after the call to DrawStr, and our saved registers are popped off the
stack. rts signals the end of the program.
notEsc:
lea (%sp,-30),%sp | grab 30 bytes on the stack
move.w %d0,-(%sp) | sprintf(6(%sp), keySring, %d0);
pea keyString(%pc)
pea (%sp,6)
movea.l 4*sprintf(%a5),%a4
jsr (%a4)
If we didn't press ESC, then we branched to this label. There are a
couple things we should take a look at. The first lea instruction is
being used to reserve some stack space. We'll be using that for a
temporary string buffer. To get this space, we move the stack pointer
30 bytes up. I want to note real quick that (x,y) is the same as y(x)
here. When we moved the function addresses into a4 for example, we
used things like 4*ClrScr(%a5), but we could also have done
(%a5,4*ClrScr). These things are equivalent. We can also do
(4*ClrScr,%a5). These things are all the same in the GNU as assembler
syntax.
Next we call the sprintf function. If we look up the sprintf function in
the TIGCC docs, we see it takes an unknown number of arguments. Our
particular call takes three arguments, the string buffer, the format
string, and the single format argument. If you have never used sprintf
before, its usage is simple. It formats a string buffer according to a
format string. It is how we will turn our numeric keycode into part of
our string. I don't plan to go into much detail here, there are plenty
of references on sprintf and format specifiers. The only germane details
are the calling conventions.
Remember that parameters are pushed on the stack in reverse order. So we
need to push the keycode first. The keycode is a short (word sized).
Next, we push the format string. Just like all the string literals we
have used thus far, we use PC relative addressing. Finally, we push the
address of our string buffer. This is the memory we reserved on the
stack. Because we just pushed a word and a longword onto the stack, our
reserved memory is now 6 bytes (2 bytes for the word, 4 bytes for the
longword) down on the stack.
The call to sprintf will turn our stack buffer space into a copy of the
format string with the %-5hd replaced with our keycode value. Take a
look at the TIGCC docs for printf if you want more information on the
%-5hd format specifier.
move.w #4,-(%sp) | DrawStr(0, 10, (%sp,12), 4);
pea (%sp,12)
move.w #10,-(%sp)
clr.w -(%sp)
movea.l 4*DrawStr(%a5),%a4
jsr (%a4)
lea 50(%sp),%sp | restore the stack pointer
bra getKey | goto getKey
The rest of the code is mostly straightforward. We are calling the
DrawStr function to display the string we just created. Let's take a
moment to make sure we understand why the buffer is now 12 bytes down on
the stack rather than 6 like it was above. Well, starting from 6 bytes
down, remember that pushing that address was another 4 bytes. Then the
move word is another 2 bytes. This is another 6 bytes on the stack, so
our buffer is now 12 bytes into the stack, rather than just 6.
Now it's time to restore the stack pointer. How did we get 50? Let's do
the math real quick. Starting at the notEsc label, we moved 30 bytes up.
The call to sprintf pushed 10 bytes on the stack, so now we are at 40.
Finally, the call to DrawStr pushed another 10 bytes on the stack. This
puts us at 50. It is very easy to get the math wrong, and this is a
common source of assembly program crashes. If you start getting address
error crashes on your calc, try checking your stack math.
The final instruction bra (BRanch Always) is simply an unconditional
goto. After we have printed our string, we need to return to the getKey
label so we can start all over again.