A6 - Input & Interrupts
Up until now, every program that we have written has simply run from start to end without any input from the user. This is particularly boring considering we're writing programs for a game console, which arguably exists entirely for the purpose of user input!
This time, we're going to explore how to respond to input from the user through the console's gamepad. In order to process input, we're going to be using a mainstay of old-school console development - the hardware interrupt.
VO-EM has a single gamepad, which consists of a directional pad, an A and B button, and a Start and Select button. You can read in-depth about it's specifications here. If that's too technical, sit tight and we'll go over it later.
But, before we can do anything meaningful with the gamepad, we need a way to tell if it's being pressed in the first place! There are several ways to do this, but for this article, we'll be using Interrupts. If you're familiar with programming in a higher-level language, the concept of interrupts may remind you of Event listeners. Otherwise, hold tight for the explanation in the next section!
Imagine you are planning for a party. You have pizza rolls in the microwave, you've sent your friend to go and buy a bag of ice, a different friend to pick up some guests from the bus stop, and you're copying some music onto a USB to play through the stereo. While that's all going on, you need to clean the lounge room.
Once your friends come back, you have other errands to send them on. There's some chicken nuggets that need to go in the microwave once the pizza rolls are done, and the USB needs to be taken to the sound system once it's done copying.
Now, how do you go about this? Do you clean a tiny bit of the lounge, then call both friends on their cell phones to ask if they're waiting outside, walk over and inspect the microwave timer, walk over and check the progress of the USB transfer, then go back to clean a tiny bit more lounge, repeat ad absurdum?
If you do, the odds of that loungeroom getting clean in time are very slim. Of course, it would be better to wait for the friends to ring the doorbell, the microwave to ding, and then USB to make it's "I'm done!" sound.
Ie, you wait for them to 'interrupt' you from your task before dealing with them.
If that's too complicated, think of it this way: When you're expecting a phone call, do you put the phone to your ear every thirty seconds and say "Hello?", or do you go about your day while you wait for it to ring?
The CPU works on the same principal. Rather than checking every single cycle if the devices in our computer need our attention, it's much more efficient for them to have a way of requesting attention from the CPU when they need it.
The Interrupt Queue
Whenever a device wants attention from the CPU, it fires an interrupt request. This interrupt request becomes visible in the Interrupt Request Queue, which you can see at the bottom of the CPU Status page of the debugger.
The bits from 16 to 23 represent different pieces of hardware inside the computer. If a device's bit is set to '1', that means it is requesting attention.
Above the Queue is the PSW register. The PSW register lets us instruct the CPU as to which requests for attention it should respond to, and which it should ignore.
For example, while our friends, the microwave and the USB are all things we're interested in, we have no mind to pay attention to the downstairs neighbor who keeps prodding at his ceiling with a broom and yelling at us to keep the noise down. He is requesting attention, but we ignore him.
In this tutorial, we only want to respond to interrupts from the gamepad. The gamepad, by default, requests attention on interrupt level 2. So, we'll set the CPU to listen to interrupts on that channel only by filling out the PSW appropriately:
listenTo .wordu 2#000001000000000000000001
First, we lay out our PSW. The 18th bit is enabled, because this is the bit that the gamepad requests on. Bit number 0 is also enabled, because if this bit is disabled, we just ignore everything.
Now, we need to put it into PSW. First, we load it into a register:
And then we use a special opcode to shift it into PSW
This command lets us move the contents of regular registers into special registers, such as PSW.
Now, we're listening for interrupts, but what actually happens when one occurs? How does the CPU handle it?
Well, first the CPU checks to see if bit zero is set on PSW. If if isn't, it doesn't bother checking for any interrupts. If it is set, the CPU checks for interrupts before it loads the instruction for the cycle. It starts checking from interrupt priority 7, and works its way down to interrupt priority 0. If it finds an interrupt request on a channel that it is listening for interrupts on (Ie, there is a bit set to 1 in IRQ that is also set to 1 in PSW), then it will do the following:
- Turns off bit 0 on PSW (Otherwise we could get stuck listening to interrupts until the cows come home)
- Stores the current value of PC in the XAR register (so we can get back where we were before the interrupt fired)
- Sets PC to XBR+(7-Interrupt_level)*4
This probably sounds complicated but in reality, all it's doing is calling a subroutine automatically, just like we did last tutorial.
Probably the most intimidating part of that list of actions was the
Sets PC to XBR+(7-Interrupt_level)*4
part, but it's actually very intuitive. It means we need to have a list of instructions like this:
IRQ7 rfe IRQ6 rfe IRQ5 rfe IRQ4 rfe IRQ3 rfe IRQ2 rfe IRQ1 rfe IRQ0 rfe
And to have XBR set to point at the first one. By default, XBR is set to 0, so we can just put this at the very top of our program.
So, for example, if we get an interrupt on level 4 with XBR set to zero, our program will jump to
XBR+(7-Interrupt_level)*4 0 +(7-4)*4
Which equals 0xC, or decimal 12. If the label IRQ7 in the above example is at 0, then this will jump down 3 labels (because each instruction is 4 bytes) to the label "IRQ4", and perform the instruction at that label.
Currently, all the instructions are set to "rfe", or "return from exception". This instruction is the interrupt equivalent of "jr 31" - it turns bit 0 back on in PSW, and sets PC to the value stored in XAR.
So, all we have to do to handle our interrupt is to make the label in the interrupt table point to a subroutine!
Let's make an ultra simple routine - all it's going to do is increment register 1
input addi r1,r1,1 rfe
The gamepad defaults to interrupt level 2, so we point interrupt level 2 to our subroutine:
IRQ7 rfe IRQ6 rfe IRQ5 rfe IRQ4 rfe IRQ3 rfe IRQ2 j input ;handle gamepad input IRQ1 rfe IRQ0 rfe
Why do we use 'j' and not 'jal'? Because our return address has already been stored in XAR for us; we don't need to put it in r31.
Let's put this all into a program and see how it works - here's everything together:
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
Let's look at what this program does, so we know how it's supposed to work: In the "main" program
- Set up PSW to listen only to gamepad input interrupts
- Endlessly increment r2
Then, in the "input" handler
- Increment r1
So, if the program is working properly, we will see r2 constantly counting upwards. Whenever we press a button, r1 will count up by 1, but r2 will resume counting with no perceivable gap.
I saved mine as input.dls, so you can compile it with
java -jar dasm.jar input.dls -a
Interrupts are invaluable when it comes to programming in assembly - especially when the system you're programming for has limited resources. They allow us to make sure the CPU keeps doing work until its attention is needed elsewhere. In the next chapter, which is part two of this tutorial, we'll be using the basic framework we developed in this chapter to actually do something meaningful with the gamepad input!