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

       Part I

       Part II

       Part III

     Basic Graphics

     C & Assembly

   Downloads

 Miscellaneous
   Links

   Cool Graphs

   Feedback Form

TIGCC Assembly Lessons

TIGCC Assembly Programming Lessons

Lesson 2: The Basics of Keyboard Input

Introduction

This lesson will cover the basics of keyboard input using the AMS key reading functions.

Step 1 - Keyboard Input via ngetchx

Start TIGCC and create a new project. Create a C Source File and a GNU assembly source file. Name the C Source File main and the assembly file kbdinput. Save the project as kbdinput. We will use the same code for the C source as last time. Modify the assembly file so that it looks as follows.

You can download the source and program files for this lesson from our archives.

kbdinput.s


    .include "os.h"

    .equ AMS_jumptable,0xC8
    .equ KEY_ESC,0x108

    .text
    .xdef asm_main

|-------------------------------------------------------------------------------
| asm_main - our program starts here
|

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)

    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

getKey:
    movea.l 4*ngetchx(%a5),%a4  | %d0 = ngetchx();
    jsr     (%a4)

    cmpi.w  #KEY_ESC,%d0        | Did the user press ESC?
    bne     notEsc              | no? then goto notEsc
                                | otherwise,

    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

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)

    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

|-------------------------------------------------------------------------------
| Data Section
|
    .data

message:
    .string "Press any key. ESC to exit."

keyString:
    .string "Key has value %-5hd"

success:
    .string "You pressed the ESC key."

 


You can copy the main.c code from lesson 1. Nothing in this file has changed.

Save the files, and make sure there is a blank line at the bottom of both files. The example may not compile without it. Build the project and send it to TiEmu. It will look something like this, depending upon which keys you press:

Screenshot

Step 2 - Analyzing the Program

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.

Step 2a - Program Conclusions

This is a good intro for keyboard input, but it only works when we are actively waiting for a key. Nothing can happen with ngetchx until after the user presses a key. This is sometimes desirable, sometimes not. You can't write a game with lots of things going on at once if you are always waiting for the user to press keys.

Let's try a different approach to keyboard input where we get the keys at our convenience, when they are available, when we need them. You will see this approach has a lot of good things, and some downsides.

 

Continue with Part II

 

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

Get Firefox!    Valid HTML 4.01!    Made with jEdit