A7 - Input & Interrupts Pt II
Last chapter, we set up a basic framework to detect when when a button on the Gamepad has been pressed, and react to that event. In this chapter, we'll be continuing that by learning how to find out specifically which key was pressed, and then react accordingly.
We've learned how to react when a device requires attention. However, at the moment, our program doesn't really talk to the device at all. Going back to our party metaphor - our friend has rung the doorbell, and we've stopped working and acknowledged it, but we haven't opened the door or greeted them. We don't even know which friend is at the door yet.
In this chapter, we will learn to read the input of a device, and modify the flow of our program based on its status.
For this chapter, we're going to need the program we created last time. Here it is:
IRQ7 rfe IRQ6 rfe IRQ5 rfe IRQ4 rfe IRQ3 rfe IRQ2 j input ;handle gamepad input IRQ1 rfe IRQ0 rfe listenTo .wordu 2#000001000000000000000001 ;our psw settings .start main main lw r1,listenTo ;load our psw settings movi2s psw,r1 ;move into psw register loop addi r2,r2,1 ;add 1 to register 2 j loop ;loop endlessly input addi r1,r1,1 ;add 1 to register 1 rfe ;go back to the main program
We're also going to need our multiplying program from the subroutine chapter:
mult_r3 .equ 0 multiply sw mult_r3(r30),r3 clr r3 mult_loop beqz r2,mult_end add r3,r3,r1 subi r2,r2,1 j mult_loop mult_end add r1,r0,r3 lw r3,mult_r3(r30) jr r31
Which also means that we'll need to load the RAM offset into r30. We'll put
RAM_OFFSET .equ 16#8000 addui r30,r0,RAM_OFFSET
Somewhere in the main program.
And of course, we're going to need our tools and runtimes just like always!
We've mentioned the Gamepad a few times without really going into detail about it. Physically speaking, it is a gameboy-style array of buttons. However, all our CPU can see is a few bytes of memory, like any other, at a certain address.
Specifically, the gamepad is mapped to 0xFFFFFF00, and occupies 4 bytes. These bytes always contain the current status of the buttons on the gamepad.
The first byte (0xFFFFFF00) is the left/right axis on the directional pad. It reads zero if neither left or right is being pressed, -1 (0xFF) if left is being pressed, and 1 (0x01) if right is being pressed. The second byte (0xFFFFFF01) works the same for the up/down axis, with -1 meaning "up" and 1 meaning "down". These numbers are chosen because they make it very easy to move characters on the screen with the directional pad - just adding the value of these bytes to the x and y positions of an instance will move it appropriately on the screen. However, we'll come back to instances in a later tutorial.
The third byte (0xFFFFFF02) represents the A and B buttons. It will read 1 if A is pressed, 2 if B is pressed, and 3 if both are pressed.
The fourth byte (0xFFFFFF03) works the same for the Start and Select buttons, respectively.
With any program running in the debugger, try bringing up the Memory Status page by pressing 3, then switching to the Hardware registers by pressing the U key. Play with the Arrow keys and Z (A) and X (B) buttons, as well as the Spacebar (Start) and S key (Select). Can you see the memory values at 0xFFFFFF00 moving?
Making our program
For this tutorial, we're going to allow the user to control the input for the "multiply" subroutine - instead of programming in our own "a" and "b" arguments to multiply, the user will select them with the directional pad and press the "A" button to multiply their chosen numbers together.
First, we're going to need to load up the memory address offset of the gamepad into a register so that we can load its data. We'll make a word:
GP_OFFSET .wordu 16#FFFFFF00
And then, in the "main" part of the program, we'll load that word into a register:
I put it in r10 because it's easy to remember and out of the way.
Next, we need to look at our "input" subroutine. This routine is called whenever the gamepad requests the CPU's attention, meaning whenever it's called, there should be a button press in one of the Gamepad's memory bytes for us to read.
We're going to store the user's "a" and "b" arguments in registers 6 and 7, respectively.
input lb r5,(r10) ;load the first byte of the gamepad register (up/down) add r6,r6,r5 ;add it to 'a' lb r5,1(r10) ;load the second byte (left/right) add r7,r7,r5 ;add it to 'b'
Pay attention to the third line - the 1(r10) means "contents of r10 + 1". Since the contents of r10 point to the first byte of the Gamepad's memory, adding 1 makes it point to the second byte.
Because our directional pad buttons give us a range from -1 to 1, we can just add them to our current values to increment and decrement them - convenient, right?
Next, we need to check if our user is pressing the "A" button. To do that, we need to load the third byte:
lb r5,2(r10) ;load the third byte (a/b)
If you remember from above, the third byte contains the status of both the A and B buttons. So, we need to isolate the "A" button's byte. We do this by using an "AND" operation. "AND" let's you compare two sets of bits, and only keep the 1s if both bits have the same bit set to 1.
So, for example, if we had
and we AND them together, we get
Do you see why? The only digit where both were set to "1" was the lowest digit, so that's the only one we kept. This is often referred to as "bit masking" when it's used to discard parts of a byte we don't need.
In this case, we only need the very first bit in our byte, so we can mask it with 000000001, like so:
andi r5,r5,2#00000001 ;strip it down to just the A button
And then, we can use beqz to test if it's zero just like we use in our multiplication program - if it's zero, we'll skip to the end of the routine. If it's not, we'll multiply the numbers we have together!
beqz r5,input_end ;if A isn't pressed, end
How do we multiply the numbers together? You'll remember that our multiply subroutine multiplies the values of registers 1 and 2 together, so we'll need to copy our "a" and "b" arguments from r6 and r7 into r1 and r2. Then, we just have to call the "multiply" subroutine.
add r1,r0,r6 add r2,r0,r7 jal multiply
And finally, we put an "rfe" instruction at the end to get back to our main program:
input_end rfe ;go back to the main program
Putting it all together
Here's all the code for the tutorial put together:
IRQ7 rfe IRQ6 rfe IRQ5 rfe IRQ4 rfe IRQ3 rfe IRQ2 j input ;handle gamepad input IRQ1 rfe IRQ0 rfe ;The values we need RAM_OFFSET .equ 16#8000 GP_OFFSET .wordu 16#FFFFFF00 listenTo .wordu 2#000001000000000000000001 ;our psw settings .start main ;Our main program main addui r30,r0,RAM_OFFSET lw r10,GP_OFFSET lw r1,listenTo ;load our psw settings movi2s psw,r1 ;move into psw register loop wait ;wait for something to happen j loop ;go back to waiting ;The gamepad input handler input lb r5,(r10) ;load the first byte of the gamepad register (up/down) add r6,r6,r5 lb r5,1(r10) ;load the second byte (left/right) add r7,r7,r5 lb r5,2(r10) ;load the third byte (a/b) andi r5,r5,2#00000001 ;strip it down to just the A button beqz r5,input_end add r1,r0,r6 add r2,r0,r7 jal multiply input_end rfe ;go back to the main program ;Multiplies r1 by r2; puts the result in r1 mult_r3 .equ 0 multiply sw mult_r3(r30),r3 clr r3 mult_loop beqz r2,mult_end add r3,r3,r1 subi r2,r2,1 j mult_loop mult_end add r1,r0,r3 lw r3,mult_r3(r30) jr r31
You might notice that I changed the contents of "loop" to have a "wait" instruction instead of incrementing r2. We were incrementing r2 to show that the CPU can indeed do work while it's waiting to be interrupted. However, for this tutorial, we don't have any work for it to do. The "wait" instruction simply sleeps the CPU until an interrupt occurs that it is listening for. On handheld devices, this saves a lot of battery power! It could extend the battery life of the original Gameboy by hours.
I saved mine as "input2.dls", and assembled it with
java -jar dasm.jar input2.dls -a
In the debugger, the Directional Pad is mapped to the arrow keys on your keyboard. The A button is mapped to Z, B is mapped to X, Start is mapped to the spacebar, and Select is mapped to S.
What do we expect this program to do? It should:
- Do nothing while waiting for using input
- Increment and decrement r6 with the left/right buttons
- Increment and decrement r7 with the up/down buttons
- Multiply r6 and r7 together when the A button is pressed, displaying the result in r1
Next time, we'll learn how to put things on the screen in A8 - Introduction to Computer Graphics.
There are a few shortcomings in this program! Can you manage to improve it?
- Because of the way the up/down register works, pressing "up" decreases the register and pressing "down" increases it. This is unintuitive! Can you fix it by changing just one instruction?
- The program still crashes when the "b" variable is negative. Can you fix it?
- It would be convenient to have a "clear" button - can you make the program clear both the inputs and the results when the "B" button is pressed?