Lecture 17.1
Revised Feb 2024
Andreas Moshovos
Introduction to
Interrupts/Exceptions: Using Interrupts with the Buttons
In the previous lecture we saw a way to interface (programmatically) with the serial device. For this purpose we used “polling”, that is busy-waiting; any time we wanted to communicate with the serial device we had to first probe the status register to see if it is available (if we wanted to send a character) or whether a character has been received. We had to keep probing the status register until the desired condition was met. This is completely counter-productive as we consumed all our processing power just waiting. Moreover, while we have not seen a concrete example of this, it turns out that if we have to communicate with multiple devices, writing code that uses polling for all devices simultaneously is at the very least complicated and in some cases it will be virtually impossible. We use this discussion as a motivation for another way for communicating with devices called interrupts. Interrupts are a very powerful mechanism that can be used to support multitasking, handling of exceptional conditions (i.e., unexpected results during calculation such as a division with zero or overflow), emulation of unimplemented (in hardware) instructions, single-step execution for debugging, and operating system calls/protection among others. So, while for the time we will focus on a very specific application of interrupts please keep in mind that this is just a stepping stone towards explaining the diverse applications of interrupts.
(A note on terminology: the terms interrupt and exception are commonly used to refer to the same concept, often interrupts is used for external device induced requests whereas exception is used for interrupts that are caused by instruction execution. There is no common usage of these terms. Here, we will use the two terms interchangeably to refer to the same concept and we will not make an attempt to differentiate between external of program induced interrupts.)
As we have seen, polling is almost the equivalent of a real-life scenario where you hand-off to work to someone and then you keep bugging them all the time asking whether they are done. Interrupts on the other hand are quite different. When we hand-off some work to someone else we also ask them to let us know when they are done. This way we do not need to keep asking them all the time whether they are done. When the work is finished they will come back and notify us, and since we are talking about computers, we can rely on them: they will come back and tell us.
Here’s a high-level view of the interrupt mechanism:
1. Say the processor is executing instructions.
2. At some point in time, a device requests an interrupt.
3. Later on the processor decides to grant that request. This amounts to completing the execution of the current instruction and then inducing a call to a specialized routine that is called an interrupt handler. The call to the interrupt handler is also a special call in that it must save sufficient information so when eventually the interrupt handler finishes, the processor can resume executing instructions immediately after the instruction that finished prior to inducing the call to the interrupt handler.
There are several requirements that must be met for interrupts to work.
1. We need a mechanism for devices to request interrupts.
2. We need a mechanism for inducing calls to the interrupt handler.
3. We need a mechanism to let the device know that its interrupt request was handled.
4. We need a way of determining where is the interrupt handler (that is where in memory its instructions reside).
5. We need a mechanism for saving and restoring all appropriate machine state (registers plus memory) that may be affected across an interrupt handler call.
The solutions to all these requirements entail hardware units, software and several conventions that must be followed for everything to work as expected. In what follows we explain the NIOS II interrupt model. NIOS II’s interrupt model is simple and representative of that of many other load/store architectures that were designed after the mid-80’s (e.g., SPARC, MIPS, PowerPC). Other processor families have more elaborate models. Given time we may touch upon some of the more elaborate mechanisms.
Let’s us first look at the requirements.
REQUIREMENTS #1
and #3: ASKING FOR AN INTERRUPT AND GRANTING THE REQUEST
The device asks for an interrupt, the processor grants that request at a convenient time and informs the device. One straightforward mechanism uses two wires at the physical level. One wire is directed from the device to the CPU (we can call it the Interrupt-Request - IRQ) whereas the second is directed from the CPU to the device (we can call it Interrupt-Granted or Interrupt-Acknowledge IACK). When the device wants to request an interrupt it asserts the interrupt-request signal. When the CPU grants the interrupt, it responds by asserting the Interrupt-Granted signal. This process of “asking” and “granting” is often called a hand-shake.
REQUIREMENTS #2
and #4: INDUCING A CALL TO THE INTERRUPT HANDLER – WHERE IS THE INTERRUPT
HANDLER?
With these in place, we next consider covering requirement 3, that is how to induce a call to an interrupt handler. How do we know where in memory is the interrupt handler)? One straightforward solution to this would be to use a hardwired (i.e., decided at designed time and built into the CPU design) address. For example, we could have the interrupt handler to be at address 0x90000 for all interrupt requests. In fact, some CPUs, including NIOS II, do exactly that. So, all external devices share a common interrupt handler. The interrupt handler then probes all appropriate devices to figure out which device actually requested the interrupt. This probing is done in software. Inducing a call to the interrupt handler is straightforward: change the PC to the interrupt handler’s start address (0x90000 in our example) after we have saved somewhere (e.g., on the stack) the current value of the PC so that we can return back to it once the interrupt handler has finished.
REQUIREMENT #5:
SAVING/RESTORING STATE
As an interrupt appears to be just like a call we can always use the stack to save and restore state as needed. This can be done automatically by the hardware or we can simply rely on software. There may be odd bits of state that have to be stored depending on the implementation. These may require special hardware support. We’ll return to this discussion with the specific case of NIOS II.
HOW NIOS II MEETS
THE 5 REQUIREMENTS
With this high-level understanding of a straightforward solution for the aforementioned requirements we can now start discussing the way NIOS II handles these requirements.
1. How Devices Request Interrupts: Instead of just one interrupt request wire, NIOS II provides 32, IRQ0 through IRQ31. These request lines are not prioritized in hardware and thus they all have equal priority.
2. How the CPU informs a device that its interrupt request has been granted: NIOS II does not rely on hardware for this. Instead, it is left up to software to notify the device. That is, there is some register in the device that has to be accessed to notify the device that the interrupt has been granted. The exact process is device specific.
3. How the CPU determines where is the interrupt handler: NIOS II uses a pre-configured address for this. All interrupt requests result into executing code starting at location 0x0000020. In assembly you can use the directive ‘.section exceptions, “ax”‘ to assign code to that part of memory. The interrupt handler must then interrogate all devices to figure out which one was the one that caused the interrupt. If there is more than one device requesting an interrupt, it is up to the interrupt handler to determine in what order these requests will be handled.
Before we can review how NIOS II handles interrupts we need to take a closer look at some of the control registers.
CONTROL REGISTERS
Ctl0: Control register 0 is the master interrupt enable switch. Bit 0 of this register controls whether NIOS II will respond to interrupt requests. If bit 0 is 1 then interrupts will be serviced. Otherwise, NIOS II silently ignores all requests.
Ctl1: Control register 1 is used by NIOS II to preserve the current state of ctl0 upon accepting an interrupt. More on this soon.
Ctl3: Control register 3 is the equivalent of a switch board for each IRQ line. Each bit controls whether NIOS II will respond interrupt requests from the specific IRQ line. This register allows us to selectively enable interrupt handling per device. Interrupts are accepted for IRQi if bit I of ctl3 is 1 and bit 0 of ctl0 is 1.
The only way to access control registers is through two specialized instructions:
2. Writing to a control register: wrctl control_register, general_purpose register. Example: “wrctl ctl0, r8” writes the value of r8 into control register ctl0.
In addition to the registers, register r29 also participates in the interrupt sequence. It’s used to store the PC of the instruction following the one that was interrupted. R29 has an alias which is ea for exception address.
Now we can review the
complete interrupt request/acknowledge/interrupt handler calling/return
sequence:
1. The device requests an interrupt by asserting one of the IRQi lines. Say, this is line IRQ1.
2. The processor completes execution of the current instruction.
3. The processor disables interrupts, but before doing so, it saves the current interrupt enable bit. Specifically, the processor saves the current value of control register 0 (ctl0) in control register 1 (ctl1) and then updates ctl0 disabling further responses to interrupts. In more detail:
1. ctl1 = ctl0
2. bit 0 of ctl0 = 0
4. r29 = PC + 4. This is the PC immediately following the instruction that would have executed.
5. PC = 0x20, so that execution now continues in the interrupt handler.
6. The handler terminates by executing the special instruction eret. This instruction restores ctl0 from ctl1 and PC from register r29 (ea). The net effect is that execution resumes by executing the instruction that was interrupted.
USING INTERRUPTS
WITH THE BUTTONS:
Let’s us now discuss how we can use interrupts with the buttons. For our example, the “regular” program will be an infinite loop that toggles LED 0. The loop calls two “helper” functions. Ledtoggle() toggles bit 0 of the LED DR register (which is at address 0xff200000). The timerdelay() uses polling to wait for 100M clock ticks. We have discussed the functionality of the LED PIT and the timer in previous notes. The functions follow the NIOS II calling conventions.
Here’s the code for this main program:
.text
.global _start
_start:
movia sp, 0x200000
fever:
call ledtoggle
call timerdelay
br fever
ledtoggle:
movia r5, 0xff200000
ldwio r2, 0(r5)
xori r2, r2, 1
stwio r2, 0(r5)
ret
timerdelay:
movia r4, 0xff202000
movia r5, 100000000
movi r2, 0x8
stwio r0, 0(r4) # clear TO
stwio r2, 4(r4) # stop timer
stwio r5, 8(r4) # periodlo
srli r5, r5, 16
stwio r5, 12(r4) # periodhi
movi r2, 0x4
stwio r2, 4(r4)
bwait:
ldwio r2, 0(r4)
andi r2, r2, 0x1 # check TO
beq r2, r0, bwait
ret
To illustrate interrupts we will enable interrupts for button 0 and have the interrupt handler (aka interrupt service routine) toggle LED 4. The expected behavior is as follows:
1. LED 0 will turn on and off with a delay of roughly 100M ticks. This is done by the main program.
2. LED 3 will toggle any time we pressed button 0. This is done by the interrupt handler.
We can now write the interrupt handler (at the end we will write the initialization code which will cause this interrupt handler to be called when button 0 is pressed and released.
.section .exceptions, “ax”
.align 2
ihandler:
#LED
TOGGLE
movia r10, 0xff200000 #base of LED PIT
ldwio r11, 0(r10) # r11 = DR of LED PIT
xori r11, r11, 0x8 # invert bit 3 (4th)
stwio r11, 0(r10) # write DR of LED PIT
# LED 3 changes stage (on to off or vice versa)
# RESET INT
CAUSE
movia r10, 0xff200050 # button PIT base
movi r11, 1 # reset EDGE bit 0
stwio r11, 12(r10) # reset edge for button 0
# RETURN
FROM INT
subi ea, ea, 4 # make sure we execute the instruction that was interrupted. Ea/r29 points to the instruction after it
eret # return from interrupt
# this restores ctl0 to it’s previous state that was saved in ctl1
# and does pc = ea
To make sure that the handler gets placed at address 0x0000020 use “.section .exceptions, “ax””.
The first part of the handler (LED TOGGLE) toggles bit 3 of the DR of the LED PIT effectively making the 4th LED blink.
The second part of the handler (RESET INT CAUSE) resets bit 0 of the EDGE register of the Button PIT. This is necessary so that the interrupt handler confirms that the interrupt request was granted and handled. The button PIT will as a result deassert IRQ1. If we do not clear the EDGE bit that caused the interrupt, the moment the interrupt handler returns using eret, the CPU will find the IRQ1 is still on and will, as part of normal operation, grant the request and the interrupt handler will run again, and again, and again…
The third part (RETURN FROM INT) decrements ea by 4 and uses eret to return to the interrupted program which will resume execuction at the instruction that was interrupted and never go to execute. Later on we will explain that having the EA point to PC+4 instead of the actual PC of the instruction probably simplifies the hardware implementation. This simplification is exposed to the programmer who has to explicitly decrement the PC to point to the right instruction.
Initialization
Code: Setting up NIOS II to accept interrupts and the Button 0 to request them
Now we can write the initialization code. There are several things that we need to do and writing this code requires that we discuss the MASK register of the Buttons PIT.
The MASK register has 32b out of which only the 4 LSb are in use. Setting bit 0 of the MASK register to 1 enables button 0 to request an interrupt from NIOS II anytime the EDGE register bit 0 is set to 1. As a result, any time button 0 is pressed and released, an interrupt request will be sent to NIOS II. The buttons
are connected to IRQ1. This is not programmable on our system. It was etched on stone at design time.
When NIOS II accepts an interrupt it can read the EDGE register to determine whether it was the button 0 that caused the interrupt. In our case, button 0 will be the only possible cause of an interrupt. Accordingly, our interrupt handler will not have to check why it started executing.
So, now we can write the initialization code. Here’s the complete code. The initialization code is shown in red.
button0EnableInt:
# DEVICE
SIDE
movia r2, 0xff200050 #buttons
movi r4, 0xF # first make sure the EDGE register is all clear
stwio r4, 12(r2) # reset EDGE bits
movi r4, 0x1 # set MASK bit 0 to 1, enable int requests for button 0
stwio r4, 8(r2)
# CPU SIDE
#CTL3
movi r5, 0x2 # button are connected to IRQ1 (2nd bit of ctl3)
wrctl ctl3, r5 # enable ints for IRQ1/buttons
#CTL0
movi r4, 0x1
wrctl ctl0, r4 # enable ints globally (bit 0)
ret
The “DEVICE SIDE” code enables interrupts requests on the Buttons PIT. First it clears the EDGE register (this is a precaution to start from a known state).
Then it sets bit 0 of the EDGE register to 1. From this moment on, pressing and releasing button 0 will not only set the EDGE register bit 0 to 1, but will also raise the IRQ1 effectively requesting an interrupt from the CPU.
The “CPU SIDE” code programs the CPU to accept interrupts (ctl0) and specifically from the buttons (ctl3). The control register ctl0 is the global interrupt enable. Its bit 0 controls whether the CPU will accept any IRQ request (when it is 1) or none (when it is 0). The control register ctl3 controls which IRQs are allowed to request interrupts. Bit 1 (the 2nd bit) corresponds to IRQ1. Setting it to 1 allows IRQ1 to go through.
Our main program can now be modified to include a call to button0EnableInt() prior to the infinite loop:
.text
.global _start
_start:
movia sp, 0x200000
call
button0EnableInt
loop:
call ledtoggle
call waitasec
br loop
Completing the
interrupt handler
As it stands there are two issues with the interrupt handler:
1. It uses registers that may be used by the program that is interrupted.
2. It assumes that the reason the interrupt occurred because button 0 was pressed and released.
Both of these are not problems for the example code because:
1. The example code does not use the registers used by the interrupt handler. The example code uses r2-r5, while the interrupt handler uses r10 and r11.
2. The example only enables interrupts for when button 0 is pressed and released.
Preserving
registers in the interrupt handler
Let’s first see how we avoid overwriting registers in the interrupt handler. An interrupt may happen at any point of time. It’s an induced call that can happen in between any instructions of our program. This is different that a call our program makes. Those calls we place at specific points and we can insert code before and after to preserve registers as needed.
The interrupt handler must assume that any register any register other ea could be holding a value that might be of use by the interrupted program. So, the interrupt handler must treat all registers (except ea) as “callee-saved” and itself as the “callee”.
The interrupt handler must make sure that the interrupt handler does not change any register other than ea. Again, the solution is to make sure that any registers that are changed during the execution of the interrupt handler have their values saved and restored by the handler. We are going to use the stack for this purpose.
Returning to a discussion we had when we first commented about how subroutines are supposed to use the stack: This is why we can never assume that values that are outside the stack will remain unchanged: an interrupt handler may run, and use the stack temporarily to preserve register values.
Here’s the interrupt handler. For illustration purposes we changed the handler to use r2 and r4. The additions are shown in blue.
.section .exceptions, "ax"
addi sp, sp,
-8
stw r2, 0(sp)
stw r4, 4(sp)
movia r2, 0xff200000
ldwio r4, 0(r2)
xori r4, r4, 0x8
stwio r4, 0(r2)
movia r2, 0xff200050
movi r4, 1
stwio r4, 12(r2) #clear edge for button 0
ldw r4, 4(sp)
ldw r2, 0(sp)
addi sp, sp, 8
addi ea, ea, -4
eret
COMPLETE CODE:
.section .exceptions, "ax"
addi sp, sp, -8
stw r2, 0(sp)
stw r4, 4(sp)
movia r2, 0xff200000
ldwio r4, 0(r2)
xori r4, r4, 0x8
stwio r4, 0(r2)
movia r2, 0xff200050
movi r4, 1
stwio r4, 12(r2) #clear edge for button 0
ldw r4, 4(sp)
ldw r2, 0(sp)
addi sp, sp,
8
addi ea, ea,
-4
eret
.text
.global _start
_start:
movia sp, 0x200000
call
button0EnableInt
loop:
call
ledtoggle
call
waitasec
br loop
ledtoggle:
movia r5, 0xff200000
ldwio r2, 0(r5)
xori r2, r2, 1
stwio r2, 0(r5)
ret
waitasec:
movia r4, 0xff202000
movia r5, 100000000
movi r2, 0x8
stwio r0, 0(r4) # clear TO
stwio r2, 4(r4) # stop timer
stwio r5, 8(r4) # periodlo
srli r5, r5, 16
stwio r5, 12(r4) # periodhi
movi r2, 0x4
stwio r2, 4(r4)
bwait:
ldwio r2, 0(r4)
andi r2, r2, 0x1 # check TO
beq r2, r0, bwait
ret
button0EnableInt:
movia r2, 0xff200050 #buttons
movi r4, 0x1
stwio r4, 12(r2) # reset EDGE bit
stwio r4, 8(r2)
movi r5, 0x2
wrctl ctl3, r5 # enable ints for
IRQ1/buttons
wrctl ctl0, r4 # enable ints
globally
ret
Determining the
interrupt cause
Let’s now augment our handler to check whether the interrupt was caused by the button 0 of the Button PIT. To do so, we need to check whether bit 0 of the button PIT EDGE is 1. Here’s the code – additions shown in red:
.section .exceptions, "ax"
addi sp, sp,
-8
stw r2, 0(sp)
stw r4, 4(sp)
#
CHECK EDGE bit 0 (buttons)
movia r2, 0xff200050
ldwio r2, 12(r2)
andi r2, r2, 1
beq r2, r0, NOTBUTTON0
movia r2, 0xff200000
ldwio r4, 0(r2)
xori r4, r4, 0x8
stwio r4, 0(r2)
movia r2, 0xff200050
movi r4, 1
stwio r4, 12(r2) #clear edge for button 0
br iepi
NOTBUTTON0:
CODE
TO HANDLER OTHER POSSIBLE INTERRUPTS (not used here)
iepi:
ldw r4, 4(sp)
ldw r2, 0(sp)
addi sp, sp, 8
addi ea, ea, -4
eret
Nested Interrupts
What happens when an interrupt request is received when a previous interrupt request is still handled by the interrupt handler?
In the example code we discussed thus far, interrupts remain disabled while the interrupt handler is running and are only re-enabled by the eret instruction when the interrupt handler exits. So there is no possibility of interrupting the handler to service another interrupt request. In some cases however, it is desirable to enable interrupts as soon as possible. This would be the case for example, when we are servicing an interrupt handler that takes a long time to complete. For example, copying data from a hard drive to memory or from a network interface to memory. In this case, we would like to enable interrupt even while we are still running the interrupt handler. To do this safely we must take the following actions:
1. Save the value of ea on the stack
2. Save the value of ctl2 on the stack.
3. Make sure to tell the device that is currently being serviced to stop requesting interrupts.
4. Re-enable interrupts in ctl0.
Actions 1 and 2 are necessary because once we enable interrupts, one might occur immediately and these two registers will be automatically overwritten. If things are to work correctly, we must make sure that the current interrupt handler sees the right values for those two registers. Ea contains the address of the instruction this interrupt handler must resume execution at, and ctl1 contains the original state of ctl0 which must be restored.
Action 3 is necessary so that we make sure that the interrupt handler will not be called for ever before it had a chance of servicing the current request. If we do not disable the request, the moment interrupts are enabled, the very same request will cause another invocation of the interrupt handler.
Here’s the modified interrupt handler that enables interrupts inside the interrupt handler. However, we cannot enable them immediately upon entry to the handler. Instead, we have to first check whether it was the button 0 that caused this interrupt and clear the edge register. If we don’t, the moment we enable interrupts the CPU will be faced with the following scenario:
1. I am about to execute an instruction.
2. IRQ1 is 1
3. Interrupts are enabled
4. What should do? Service the interrupt.
Hence, the CPU will call the interrupt handler again, and we will essentially have an infinite loop where we start the interrupt handler every time we enter it and get to the instruction that re-enables interrupts.
So, unless we acknowledge one IRQ, unfortunately, the interrupt handler enters an infinite loop allocating more and more space on the stack until the stack overwrites the code. What goes wrong? Well, our interrupt handler saves a few values on the stack and then immediately re-enables interrupts using these two instructions. The reason we are executing the interrupt handler in the first place is that a device requested the interrupt. However, unless the processor *explicitly* goes and notifies the device that it is handling the interrupt, the device will keep asking. In this case, as soon as the processor enables interrupts, the hardware finds the same interrupt request on the IRQ lines, and then triggers another interrupt call. To avoid having the same interrupt request continuously triggering interrupts, the processor must notify the device that its interrupt is being handled.
Additions are shown in green.
handler:
#
PROLOGUE
subi sp, sp, 16 # we will be
saving four registers on the stack
stw r2, 0(sp) # save r2
stw r4, 4(sp) # save r5
# save ea -- we
will enable ints while this handler has not yet
returns
stw
ea, 8(sp)
# preserve ctl1
# used when ctl0 and ctl1 have more info
in them besides the int enable
# not applicable on our system
rdctl r2, ctl1
stw r2, 12(sp)
# CHECK EDGE bit 0
(buttons) and clear
movia
r2, 0xff200050
ldwio
r4, 12(r2)
andi
r4, r4, 1
beq
r4, r0, NORESETEDGE
stwio r4, 12(r2) # clear EDGE
NORESETEDGE:
addi r2,
r2, 0x1 # enable interrupts
wrctl ctl0, r2
#interrupts may happen from this point on
# r4 will be 1 if it was the button that
caused the INT
beq r4, r0, NOTBUTTON0
movia r2, 0xff200000
ldwio r4, 0(r2)
xori r4, r4, 0x8
stwio r4, 0(r2)
movia r2, 0xff200050
movi r4, 1
stwio r4, 12(r2) #clear edge for button 0
br iepi
NOTBUTTON0:
CODE HERE FOR CHECKING OTHER INTERRUPT SOURCES
iepi:
#
EPILOGUE
ldw
r2, 12(sp) # restore ctl1
wrctl ctl1, r2
ldw
ea, 8(sp)
ldw r4, 4(sp) # restore r5
ldw r2, 0(sp) # restore r2
addi sp, sp, 16 # we will be
saving two registers on the stack
subi ea, ea, 4 # make sure we execute the instruction that was interrupted. Ea/r29 points to the instruction after it
eret # return from interrupt
# this restores ctl0 to it’s previous state that was saved in ctl1
# and does pc = ea
Using C to access the PIT devices
We do not have to write code in assembly to
program devices. We can use a high-level programming language. Here we explain
how this can be done using C. Here’s a first example that uses a C pointer to
access the LED DR:
void
LEDtoggle0()
{
volatile unsigned int *const ledp = 0xFF200000;
unsigned int v;
int delay;
while (1) {
v = *ledp;
v = v^0x1; *ledp = v; // toggle LED 0
for (delay = 1000000; delay > 0;
delay--);
}
}
The declaration of pointer “ledp” is for a pointer to a word in memory that is at
address 0xff200000. The “const” qualifier states that this pointer value (the
address) will never change and allows the compiler to use it directly any time
the code deferences “ledp”.
Please do read the timer notes for more details on this. The “volatile”
qualifier also instructs the compiler to always perform all accesses to the
pointer address using ldwio and stwio.
The pointer will not be register allocated.
The above code works correctly, however, if
we wanted to program for example the buttons PIT or the GPIO PIT, we would need
to declare three or four separate pointers for the DR, DIR (not used for
buttons), MASK and EDGE registers. Subjective as it might be, to improve code
readability and maintainability, we can use C structures to expose the devices
structure to the program as follows:
struct
PIT_t {
volatile unsigned int DR;
volatile unsigned int DIR;
volatile unsigned int MASK;
volatile unsigned int EDGE;
};
//
The LED pit is at this base address
struct PIT_t *const ledp = ((struct PIT_t *) 0xFF200000);
//
The BUTTONS pit is at this base address
struct
PIT_t
*const buttonp = ((struct PIT_t
*) 0xFF200050);
void
LEDButton0Toggle(void)
{
ledp->DR = 0;
// turn off all leds
while (1) {
// check if the first button was pressed
and released
if (buttonp->EDGE
& 0x1) {
ledp->DR
= ledp->DR ^ 0x1; // toggle bit 0 of the leds
buttonp->EDGE
= 0x1; // reset bit 0 of the BUTTON EDGE
}
}
}
The code declares first a structure containing
four words one per register in the PIT devices. Then the ledp
and button declarations define two constants that can be used to directly
access the corresponding PITs (LEDs and buttons).
The function first turns off all LEDS and
then polls the EDGE register of the buttons waiting for button 0 to be pressed
and released. Then, toggles LED 0, clears bit 0 of the EDGE register for the
buttons, and repeats.