Lecture 17

Andreas Moshovos

 

Introduction to Interrupts/Exceptions: Using Interrupts with the JTAG UART

 

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 instruction 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) whereas the second is directed from the CPU to the device (we can call it Interrupt-Granted). 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 cover requirement 3, that is how to induce a call to an interrupt handler. How do we know where in memory is the interrupt handler (which is just a piece of software)? 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 has to be stored depending on the implementation. These may require special hardware support. For the time being we have no context to really discuss this issue.

 

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. 

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 0x1000020. 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:

 

1.    Reading from a control register: rdctl control_register, general_purpose_register. Example: “rdctl ctl0, r8” reads the value of ctl0 and stores it in r8.

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 = 0x01000010, 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 UART:

 

Let’s us now discuss how we can use interrupts with the UART.

We will write a program that continuously sends a character through the UART using polling. The character is initially the space (32) but through interrupts this character should change to be whatever is received from the UART. The change of the character will happen asynchronously with respect to our polling sending program.

 

Here’s the core of the character-send program that uses polling:

 

.equ JTAG_UART_BASE, 0xFF10F0

.equ JTAG_UART_RR, 0

.equ JTAG_UART_TR, 0

.equ JTAG_UART_CSR, 4

 

      .data

thechar:    .byte ‘ ‘               # the character that will be sent. It is read by our program and overwritten by the interrupt handler.

 

      .text

      .align 2

flood:

      movia r8, JTAG_UART_BASE

 

      # WE’LL FILL THIS IN LATER

      INITIALIZATION CODE WILL GO IN HERE

     

wait:

      ldwio r2, JTAG_UART_CSR(r8)   # read CSR in r2

      srli  r2, r2, 16              # keep only the upper 16 bits

      beq   r2, r0, wait            # as long as the upper 16 bits were zero keep trying

     

      movia r5, thechar             # read the character from memory

      ldbuio r4, 0(r5)       

 

      stwio r4, JTAG_UART_TR(r8)    # place it in the output FIFO

      br    wait                    # OK, once more

 

 

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 the UART receives a character – so for the time being assume that this has been done):

 

      .section .exceptions, “ax”

      .align 2

 

handler:   

      movia r10, JTAG_UART_BASE

     

      ldwio r10, JTAG_UART_RR(r10)  # read RR in r2

                                    # this also clears the IRQ1 request line

 

      andi  r10, r10, 0xff          # a character was received, copy the lower 8 bits to r9

      movia r9, thechar             # write it to memory

      stbio r10, 0(r9)

 

      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 0x1000020 use “.section .exceptions, “ax””.

 

Initialization Code: Setting up NIOS II to accept interrupts and the JTAG UART 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 explain a bit more the UART control register.

In particular, the CSR register contents are as follows:

31

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

15

14

13

12

11

10

9

8

7

6

5

4

3

2

1

0

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

WIP

RIP

 

 

 

 

 

 

EWI

ERI

 

Bits 31 through 16 as we explained in the previous lecture report how many positions are free in the transmit buffer.

 

ERI: Enable Read Interrupts. When this is 1 the UART will request interrupts when characters are available for the CPU to read (received characters). This is meant to be written by the CPU.

EWI: Enable Write Interrupts. When this is 1 the UART will request an interrupt when there is space in the output FIFO for characters to be transmitted. This is meant to be written by the CPU.

 

RIP: Read Interrupt Pending: When this is 1 then the UART is currently requesting a Read Interrupt. The interrupt handler can read this bit to determine what caused an interrupt.

WIP: Write Interrupt Pending: When this is 1 then the UART is currently requesting a Write Interrupt.

 

The JTAG UART is connected to IRQ1.

 

So, now we can write the initialization code. Here’s the complete code. The initialization code is shown in red.

 

.equ JTAG_UART_BASE, 0xFF10F0

.equ JTAG_UART_RR, 0

.equ JTAG_UART_TR, 0

.equ JTAG_UART_CSR, 4

 

      .data

thechar:    .byte ‘ ‘               # the character that will be sent. It is read by our program and overwritten by the interrupt handler.

 

      .section .exceptions, “ax”

      .align 2

 

handler:   

      movia r10, JTAG_UART_BASE

     

      ldwio r10, JTAG_UART_RR(r10)  # read RR in r2

                                    # this also clears the IRQ1 request line

 

      andi  r10, r10, 0xff          # a character was received, copy the lower 8 bits to r9

      movia r9, thechar             # write it to memory

      stbio r10, 0(r9)

 

      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

flood:

      movia r8, JTAG_UART_BASE

 

      # Tell the UART to request interrupts when characters are received

      # set bit 0 (REI) of the CSR register to 1

      addi  r9, r0, 0x1

      stwio r9, JTAG_UART_CSR(r8)

 

      # Tell the CPU to accept interrupt requests from IRQ1 when interrupts are enabled
      # set bit 1 of ctl3 to 1

      addi  r9, r0, 0x2

      wrctl ctl3, r9

 

      # Tell the CPU to accept interrupts

      # set bit 0 of ctl0 to 1

      addi r9, r0, 0x1

      wrctl ctl0, r9

 

     

wait:

      ldwio r2, JTAG_UART_CSR(r8)   # read CSR in r2

      srli  r2, r2, 16              # keep only the upper 16 bits

      beq   r2, r0, wait            # as long as the upper 16 bits were zero keep trying

     

      movia r5, thechar             # read the character from memory

      ldbuio r4, 0(r5)       

 

      stwio r4, JTAG_UART_TR(r8)    # place it in the output FIFO

      br    wait                    # OK, once more

 

 

Note that by reading a character from the UART, we are also indirectly notifying it that the interrupt was serviced. The UART in response clear the IRQ1 request.

 

 

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 is because a character was received by the UART.

 

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, and r8, while the interrupt handler uses r9 and r10.

2.    The example only enables interrupts for when UART characters are received.

 

Preserving registers in the interrupt handler

 

Let’s first see how we avoid overwriting registers in the interrupt handler. Note that an interrupt may happen at any point of time. Hence, any register other ea can hold a value that might be of use by the interrupted program. So we 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. 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 do use registers r2 and r5 as opposed to r9 and r10. Registers r2 and r5 are used by our program that is interrupted by the handler. The additions are shown in blue.

 

      .section .exceptions, “ax”

      .align 2

 

handler:   

      # PROLOGUE

      subi  sp, sp, 8               # we will be saving two registers on the stack

      stwio r2, 0(sp)               # save r2

      stwio r5, 4(sp)               # save r5

 

      movia r5, JTAG_UART_BASE

     

      ldwio r5, JTAG_UART_RR(r5)    # read RR in r2

                                    # this clears the IRQ1 request

     

      andi  r5, r5, 0xff            # a character was received, copy the lower 8 bits to r9

      movia r2, thechar             # write it to memory

      stbio r5, 0(r2)

 

 

      # EPILOGUE

      ldwio r5, 4(sp)               # restore r5

      ldwio r2, 0(sp)               # restore r2

      addi  sp, sp, 8               # 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

 

Determining the interrupt cause

 

Let’s now augment our handler to check whether the interrupt was caused by a character received by the UART. To do so, we need to check whether bit RIP of the UART’s CSR register is 1 when the interrupt handler runs. Here’s the code – additions shown in red:

 

      .section .exceptions, “ax”

      .align 2

 

handler:   

      # PROLOGUE

      subi  sp, sp, 8               # we will be saving two registers on the stack

      stwio r2, 0(sp)               # save r2

      stwio r5, 4(sp)               # save r5

 

      movia r5, JTAG_UART_BASE

 

      ldwio r2, JTAG_UART_CSR(r5)   # read the status register

      andi  r2, r2, 0x100           # is bit RIP 1?

      beq   r2, r0, notRIP                # if not run code to check and handle what the interrupt is (not shown – depends on the device)

 

RIP:

     

      ldwio r5, JTAG_UART_RR(r5)    # read RR in r2

                                    # this clears the IRQ1 request

     

      andi  r5, r5, 0xff            # a character was received, copy the lower 8 bits to r9

      movia r2, thechar             # write it to memory

      stbio r5, 0(r2)

 

      br epilogue:                  # done, go to the epilogue

 

notRIP:

      CODE HERE FOR CHECKING OTHER INTERRUPT SOURCES

 

epilogue:

 

      # EPILOGUE

      ldwio r5, 4(sp)               # restore r5

      ldwio r2, 0(sp)               # restore r2

      addi  sp, sp, 8               # 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

 

 

Nester 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 immediately after reading a character from the UART. We have to enable interrupt after a character is read because this is how we tell the UART to stop requesting interrupts for this character. Additions are shown in green.

 

handler:   

      # PROLOGUE

      subi  sp, sp, 16              # we will be saving four registers on the stack

      stwio r2, 0(sp)               # save r2

      stwio r5, 4(sp)               # save r5

      stwio ea, 8(sp)               # save ea

      rdctl r2, ctl1                # save ctl1

      stwio r2, 12(sp)

 

      movia r5, JTAG_UART_BASE

 

      ldwio r2, JTAG_UART_CSR(r5)   # read the status register

      andi  r2, r2, 0x100           # is bit RIP 1?

      beq   r2, r0, notRIP                # if not run code to check and handle what the interrupt is (not shown – depends on the device)

 

RIP:

     

      ldwio r5, JTAG_UART_RR(r5)    # read RR in r2

                                    # this clears the IRQ1 request

 

      addi r2, r2, 0x1             # enable interrupts

      wrctl ctl0, r2

 

      andi  r5, r5, 0xff            # a character was received, copy the lower 8 bits to r9

      movia r2, thechar             # write it to memory

      stbio r5, 0(r2)

 

      br epilogue:                  # done, go to the epilogue

 

notRIP:

      CODE HERE FOR CHECKING OTHER INTERRUPT SOURCES

 

epilogue:

 

      # EPILOGUE

      ldwio r2, 12(sp)              # restore ctl1

      wrctl ctl1, r2

      ldwio ea, 8(sp)

 

      ldwio r5, 4(sp)               # restore r5

      ldwio 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