EECS 373 Lab 4: Interrupts

Copyright © 2010 - Thomas Schmid, Ye-Sheng Kuo, Matt Smith, and Prabal Dutta.

Schedule

See posted lab schedule for due dates, in-lab, and post-lab due dates.

Objectives

The purpose of this lab is to understand the concept of interrupts in embedded systems.

  1. Understand how interrupts get generated.
  2. Understand how interrupts get handled in the microcontroller subsystem (MSS).
  3. Understand latencies in processing interrupts in the MSS.
  4. Understand the Nested Vectored Interrupt Controller (NVIC) and how it can assign priorities.

Background

Interrupts are a fundamental mechanism in embedded systems to let the microcontroller know about events. On the Cortex-M3 core, the Nested Vectored Interrupt Controller (NVIC) allows the core to configure interrupt handling. Interrupts are also a key to low-power designs, where the core is in deep-sleep mode and only wakes up if it needs to treat an interrupt.

Additional Material

Overview

Pre-Lab Assignment

  1. Read through the lab 4 notes.
  2. Go through the material of lecture 7.

In-Lab Assignment

  1. Extend your GPIO FPGA Code with Interrupts
  2. Treat Interrupts on the MSS
  3. Measure Software Interrupt Latency
  4. Managing Multiple Interrupts
  5. NVIC and Interrupt Priorities

In-Lab Assignment

Extending your GPIO FPGA Code with Interrupts

In Lab 3 you learned how to write a peripheral in the FPGA fabric, and how to interface it over the APB3 bus to the microcontroller. So far, the only access you had to your peripheral was through memory mapped I/O, i.e., you either read, or wrote a register to get data to or from the peripheral respectively. For example, you had to specifically poll the switch status in order to see if it is pushed or released. This is not ideal as the core has to busy-loop and poll the I/O line continuously. Wouldn't it be better if the microcontroller gets interrupted when a person pushes a button?

First, download the Lab 4 FPGA Project. This is a similar configuration as you ended up in Lab 3. Have a look at its MSS configuration and the TOP wiring. Our FPGA peripheral has two inputs, SW[1:0] and 8 outputs, LED[7:0]. The peripheral has only one memory location and performs a 3 to 8 encoding, i.e., it will turn on the LED indicated in the register. At the same time, reading the register will show the status of the two SW inputs.

Connect the two SW[1:0] inputs to the two switches SW1 and SW2, and the LED[7:0] to the 8 LEDs. Then, program your evaluation kit with the code.

The Soft Console application consists of a counter that gets incremented or decremented by pushing one of the switches. Upon a change, the counter is written into the memory location of our 3to8 peripheral.

Create a new SoftConsole project and import the CMSIS directory from the Lab4 FPGA Project. Then, create a new main.c file with the following content:

#include <inttypes.h>

int main()
{
    uint32_t counter = 0;
    uint32_t switches = 0;
    uint32_t * P_WDG_ENABLE = (uint32_t *)(0x40006010);
    uint32_t * myGpio = (uint32_t *) 0x40050000;
    uint32_t * mySw   = (uint32_t *) 0x40050004;

    /* Disable Watchdog Timer*/
    *P_WDG_ENABLE = 0x4C6E55FA;


    *myGpio = 0x0; // set LED to known state

    switches = *mySw;

    while(1)
    {
        uint32_t tmpSw = *mySw;

        if(switches != tmpSw)
        {
            // something changed on the switches

            if(((switches & 0x1) == 1) && ((tmpSw & 0x01) == 0))
                counter -= 1;
            if(((switches & 0x2) == 0x2) && ((tmpSw & 0x02) == 0))
                counter += 1;
            switches = tmpSw;
            *myGpio = counter;
        }
    }

    return 0;
}

Program your board with your application in the SRAM. Start the debugger and resume your application. Try pushing the switches SW1 and SW2. The LED should go up or down respectively. However, notice that sometimes, the LED jumps several LEDs at once. This is a common artifact of switches that are not properly debounced. The problem is that the switch itself does not provide a clean high-low transition, but flips back and forth several times. Our application runs fast enough to catch this.

Connect the oscilloscope to the switch, and try to look at the cases where the LED jumps several positions, even though you pressed only once. Keep a screenshot of one of the captures.

There are two major problems with this application:

  1. Switches bounce
  2. The CPU is constantly actively polling the registers
We will leave point 1 as a post-lab assignment, and worry about point 2 now. The usual way to avoid busy polling is to have an interrupt waking up, or informing, the CPU that something happened. In our case, this would be the push of one of the switches.

Adding the FABINT

There are several ways how the FPGA can interrupt the Cortex-M3. The most common way will be through the Fabric Interrupt (FABINT). This is a dedicated interrupt line just for the fabric. Another way we will explore later is through the I/O lines of the Cortex-M3. We will use this method later, when we want more than one interrupt. The main difference between FABINT and interrupting through I/O lines is that FABINT is a level triggered interrupt, while the I/O lines can be configured for positive, negative, or both edge triggering.

Open up the Smart Design MSS Configurator. First of all, add a checkmark to the UART_0 component. This doesn't have anything to do with interrupts. But we will use the UART_0 to plot some interesting information.

Once you added the checkmark to the UART_0 component, click on the Firmware tab. Then click the menu Firmware > Refresh or hit F5. The MSS_UART_Driver_0 should have appeared now. Make sure it has a checkmark in front of it. We will use this later in our C code to talk to the UART component.

Now, click on the Interrupt Management block on the lower left-hand corner. Make a checkmark next to the FABINT line. This will enable the fabric interrupt and allow you to connect to it in the smart designer.

Generate the configuration by clicking and return back to Libero. We will now add the FABINT capabilities to the existing verilog code.

First, we have to update our SmartDesign. In Libero, click on the lab4_top component. You will notice that Libero already loaded your newly generated MSS code. But it did not update its visual representation. Right click on the lab4_mss_0 IP component and select Update Instance(s) with Latest Component. You will notice that the UART connection was added automatically and promoted to the top level. In addition, we got a FABINT port.

Next, double-click on the enc3to8wrapper. This will open the verilog code. Add another output called FABINT to this component.

First, delete the always@* block. We have to replace this block with logic that generates an interrupt when either one of the push buttons gets pressed. To do so, we will have to

Synchronization of the asynchronous switch is necessary to avoid glitches. We can perform this with the following block:

   /* Synchonization of Asynchronous switch input */
   always@(posedge PCLK or negedge SW[0])
   if(~SW[0])
     begin
       switch0_intrpt_temp   <= 1'b0;
       switch0_intrpt_temp1  <= 1'b0;
       switch0_intrpt_temp2  <= 1'b0;
     end
   else
     begin
       switch0_intrpt_temp <= 1'b1;
       switch0_intrpt_temp1 <= switch0_intrpt_temp;
       switch0_intrpt_temp2 <= switch0_intrpt_temp1;
     end
Add the necessary registers to your module, and a second block similar to this one for SW[1].

Explain in a few sentences how this block synchronises the input switch.

Why are we triggering on the negative edge of the Switch, and not the positive?

Now, we have to generate the interrupt pulse. Unfortunately, we can not directly use the output of the switch synchronizer because the FABINT is a level-triggered interrupt. Therefore, we have to generate a pulse, instead of just a rising edge. Fortunately, our synchronizer generates already all the right signals to do so. The following code will generate a pulse every time either one of the switches gets pressed.

   /* Pulse detection and pulse generation logic */
   always@(posedge PCLK or negedge PRESERN)
   if(~PRESERN)
     begin
       FABINT   <= 1'b0;
     end
   else
     begin
       if(switch0_intrpt_temp1 == 1'b1 && switch0_intrpt_temp2 == 1'b0)
         begin
           FABINT   <= 1'b1;
         end
       else if(switch1_intrpt_temp1 == 1'b1 && switch1_intrpt_temp2 == 1'b0)
         begin
           FABINT <= 1'b1;
         end
       else
         begin
           FABINT   <= 1'b0;
         end
     end

So far, we interrupted the Cortex-M3, but we have no way to find out which switch got pressed. Extend the code such that the sw_reg indicates which switch got pressed, i.e., if bit 0 is set, then switch 0 got pressed, and if bit 1 is set, then switch 1 got pressed. The sw_reg register should get reset to 0 upon reading it from the Cortex-M3. This is a common behavior for peripheral interrupt status registers.

You will have to update your enc3to8wrapper component and regenerate the smart design before you can synthesize your project. For this, go back to the smart design lab4_top, right click on enc3to8wrapper and update the component. Make the necessary connections by selecting both ends of the wire while holding shift and then right-click a port. Select the Connect option. Your block diagram should now look like this.

Generate your Smart Design, and then synthesize, place and route, and program your evaluation board with the code. It is now time to update your C sources.

Adding Interrupt Service Routines for the FABINT

As mentioned earlier, we will use the UART_0 for some additional debugging information. For that purpose, add the UART drivers from Actel to your project. You can accomplish this by right clicking your project, then select Import. On the next window select File System and click Next. Locate your Libero FPGA Project folder and select the firmware subfolder. Then click OK. Expand the firmware folder and add a check mark to the CMSIS and drivers folders. Then click Finish. You can safely overwrite already existing files.

First, let's enable the UART. For this we will have to import the UART driver header file. Add the following command to the top of your main.c file.

#include "drivers/mss_uart/mss_uart.h"
Next, we have to define a C Symbol in the project properties. Right click on your project and select Properties. Then expand the C/C++ Build entry and select Settings. Under GNU C Compiler select Symbols. On the right hand side, click the paper with the plus sign and add the variable ACTEL_STDIO_THRU_UART.

And that was already it. You can now use the command printf() to output characters through the UART port. You will learn more about serial interfacing in Lab 5.

To receive the characters on your computer, start HyperTerminal or PuTTY and set the session to 57600 baud, 8 bits data, 1 stop bit, no parity, and no flow control. You can find out the COM port of the Smart fusion from the Device Manager. In my particular case, it was on COM3

Go ahead and launch your HyperTerminal or PuTTY session now. Once we start our application, you will see some text appear on it.

First, add a printf("Your Name\n\r") right after we disable the watchdog. This will indicate that your application has started.

Next, we will have to declare our interrupt handling service routine. Fortunately, the Actel SoftConsole already provisioned the interrupt vector with some weak links. Open the file CMSIS/startup_gcc/startup_a2fxxxm3.s. The first global definition you see, g_pfnVectors is the so called interrupt vector. It defines the link between interrupt number, and function to jump to if this interrupt happened. In our case, all the function names are already defined. However, the functions themselves only exist as weak links, i.e., you can overwrite them with your own functions.

Scroll down until you find the name of the FABINT interrupt. Then scroll even further down until you find the weak link for this function. Next, go into your main.c folder and declare the function as

__attribute__ ((interrupt)) void XYZ(void)
{
}
where you replace XYZ with the correct function name. Inside the interrupt service routine, add a printf statement that outputs the number of times this function was called so far. This will help you visualize the UART output and understand how often the interrupt fired.

There are two more things we have to do before the NVIC will treat our interrupts correctly.

  1. We have to enable the FABINT interrupt in our main function
  2. We have to clear the pending interrupt inside our IRQ Handler.
If we don't perform point 1., then our interrupt service routine will never get handled. If we don't perform point 2., then the interrupt will fire again as soon as you left your interrupt service routine.

You can perform both tasks with existing functions. To enable the fabric interrupt, use the following code in your main function

NVIC_EnableIRQ(Fabric_IRQn);
and to clear the pending interrupt, add the following function to your interrupt service routine:
NVIC_ClearPendingIRQ(Fabric_IRQn);

Explain what exactly these two functions do, and where they are defined. Give the memory address of potential registers that get written or reset, including bit indexes of the bits that get changed.

Connect the terminal to the serial port and program your board with your application. Then, start the debugger and launch your application. Observe the output of the terminal. You should first see your name. Then, press the switches. You should see a new line for every time you press the switches.

Pause your debugging session and add a breakpoint to one of the interrupt service routines. Resume the operation and hit one of the switches. Your debugger should stop right at your breakpoint. Inspect the disassembled code. Which registers are used by the interrupt routine? And what does the __attribute__ ((interrupt)) do?

Wait for Interrupt

It is now time to change the application behavior. Until now, the LED's are still controlled by polling the sw_reg register. This introduces a significant latency. Change the application such that the reading of sw_reg is done inside the interrupt service routine, and replace the code inside the while loop with the following code:

while(1)
{
    printf("Wait For Interrupt\n\r");
    asm("wfi;");
}

Program your new application on your board and start it. Your output on the terminal should look similar to this:

Explain in two sentences why we have a text "Wait For Interrupt" in between every interrupt service routine.

Measuring Interrupt Latency

As you can imagine, it takes time from the interrupt generation in the fabric, until the Cortex-M3 actually processes the interrupt in software. This is usually called "Interrupt Latency". We will next investigate this latency to better understand its characteristics.

Extend your code such that the interrupt gets also routed to one of the physical User I/O lines on your development board used in Lab 1. Additionally, configure the MSS to use a gpio line, and route it to another one of the User I/O lines. Then, modify your application such that it toggles that I/O line every time it processes an interrupt.

Use the oscilloscope and connect a probe to each of the User I/O lines that you just exported. Configure the trigger to be sourced from the fab interrupt, and use the "Normal" or "One Shot" mode. Then, press a switch and measure the time between fab interrupt and toggling of the I/O line from the MSS.

Perform 10 latency measurements. What is the mean time and standard deviation of your measurements?

Your measurements should be fairly consistent, without too much jitter. This is because the Cortex-M3 is not very busy. Especially, in our current application, we do not have any blocks that disable interrupts.

Sometimes, an embedded system has to execute a code sequence that should not be interrupted. For example, if we modify a global variable that could also be modified in an interrupt routine, then we should disable interrupts before we start modifying, and re-enable them once done. This avoids concurrency issues. To turn off all interrupts, except hard faults, on the Cortex-M3, you can use the following assembler command:

asm("cpsid i");
To re-enable them, use the following command
asm("cpsie i");
This function modifies the PRIMASK.

Let's measure the interrupt latency again, but this time we add a non interruptable for loop into the main while loop. Modify the infinity while loop to

   while(1)
    {
    	uint32_t i;
    	asm("cpsid i");
    	for(i=1e2; i--; i>0);
    	asm("cpsie i");
    }
and measure the interrupt latency.

Perform 10 latency measurements of your new code. What is the mean time and standard deviation of your measurements? Explain the behavior in two sentences.

Concurrent Interrupts

Replace your while loop in your main function with an empty busy loop while(1){}

Until now, we only had to deal with one interrupt source. However, what happens if we have several potential interrupt sources? Especially in the case where they fire concurrently, which one will be treated first? Fortunately, the NVIC has many possibilities to configure its behavior. But first, we have to reconfigure our hardware.

The FABINT is only one way to interrupt the Cortex-M3. Another interrupt source are the I/O lines. They can be configured to fire an interrupt on a specific edge (falling or rising), or on both edges. And since we can connect the I/O lines to the fabric, we can use them to provide multiple interrupt lines from the FPGA to the Cortex-M3.

Open up the MSS Configurator and add one I/O line as input, and one additional line as output. Thus, we will now have two I/O lines configured as output, and one as input.

Next, click on the Firmware tab and make sure that there is a check mark next to the MSS_GPIO_Driver_0. Then generate your design by clicking .

Edit the enc3to8wrapper component and add an additional output. Then, make this output follow whatever your FABINT output does. This will assure that both interrupts get generated at the same time.

Go back to the canvas of your Smart Designer block diagram and update both components. Connect the second FABINT interrupt to the GPIO input of the MSS, and promote the second GPIO output to the top. Your connections should look similar to the following:

Next, go to the I/O Attribute Editor and assign M2F_GPO_1 to one of the user I/O lines, e.g. J21. Then, generate your Smart Design, synthesize, place and route, and program your FPGA with it.

Handling Concurrent Interrupts on the Cortex-M3

Once you programmed the FPGA, open up SoftConsole to edit your application. First, we add the GPIO drivers created by the MSS Configurator. Perform the same steps as you did for the UART drivers. However, this time select the mss_gpio directory instead of the mss_uart directory. Then, include the gpio driver header file

#include "drivers/mss_gpio/mss_gpio.h"
In our main function, we have to initialize the GPIO driver. Add the following command
MSS_GPIO_init();
Next, we have to configure the GPIO pins. Note that this is similar to what you performed in your gpio library written in Lab 2. First, we configure the two output GPIO's:
MSS_GPIO_config( MSS_GPIO_0, MSS_GPIO_OUTPUT_MODE);
MSS_GPIO_config( MSS_GPIO_1, MSS_GPIO_OUTPUT_MODE);
Then, we configure the input as an interrupt. The interrupt should fire on a rising edge.
MSS_GPIO_config( MSS_GPIO_2, MSS_GPIO_INPUT_MODE | MSS_GPIO_IRQ_EDGE_POSITIVE );
MSS_GPIO_enable_irq( MSS_GPIO_2 );
NVIC_EnableIRQ(GPIO2_IRQn);
Now all we have left is to add the interrupt service routine for GPIO2:
__attribute__ ((interrupt)) void GPIO2_IRQHandler( void )
{
    printf("Interrupt occurred - GPIO 2 \n\r");
    MSS_GPIO_set_output( 1, (iostate>1)&0x01);
    iostate ^= 0x2;
    MSS_GPIO_clear_irq( MSS_GPIO_2 );
    NVIC_ClearPendingIRQ( GPIO2_IRQn );
}

Your main.c file should now look something like this. Note, I removed some of the older code from this lab to make it smaller. Especially the code addressing the enc3to8 module got cut out.

#include <inttypes.h>
#include <stdio.h>
#include "drivers/mss_uart/mss_uart.h"
#include "drivers/mss_gpio/mss_gpio.h"

uint8_t iostate = 0;
uint32_t * P_WDG_ENABLE = (uint32_t *)(0x40006010);

__attribute__ ((interrupt)) void GPIO2_IRQHandler( void )
{
    printf("Interrupt occurred - GPIO 2 \n\r");
    MSS_GPIO_set_output( 1, (iostate>1)&0x01);
    iostate ^= 0x2;
    MSS_GPIO_clear_irq( MSS_GPIO_2 );
    NVIC_ClearPendingIRQ( GPIO2_IRQn );
}

__attribute__ ((interrupt)) void Fabric_IRQHandler( void )
{
    printf("Interrupt occurred - FABINT \n\r");
    iostate ^= 1;
    MSS_GPIO_set_output( 0, iostate);
    NVIC_ClearPendingIRQ( Fabric_IRQn );
}

int main()
{
    /* Disable Watchdog Timer*/
    *P_WDG_ENABLE = 0x4C6E55FA;

    printf("\n\nThomas Schmid\n\r");
    
    NVIC_EnableIRQ(Fabric_IRQn);
    MSS_GPIO_init();
    MSS_GPIO_config( MSS_GPIO_0, MSS_GPIO_OUTPUT_MODE );
    MSS_GPIO_config( MSS_GPIO_1, MSS_GPIO_OUTPUT_MODE );

    MSS_GPIO_config( MSS_GPIO_2, MSS_GPIO_INPUT_MODE | MSS_GPIO_IRQ_EDGE_POSITIVE );
	MSS_GPIO_enable_irq( MSS_GPIO_2 );
    NVIC_EnableIRQ(GPIO2_IRQn);

    while(1)
    {
    }

    return 0;
}

Explain in a few sentences what the MSS_GPIO_* functions exactly do. Especially, which registers do they modify? What other functions are provided by the MSS_GPIO drivers? List all of them with one sentence per function explaining its purpose.

Before you run the application, note on a paper your guess of which interrupt, GPIO2 or FABINT fires first. Don't show it to your lab partner yet!

Run the application you just wrote on your board and connect a serial terminal to the serial port. Push the switch several times. Which of the two interrupts fires first? Compare it to your answer that you just wrote down. Explain in two sentences why this interrupt gets handled before the other one.

Interrupt Priorities

In class you learned about the Nested Vectored Interrupt Controller (NVIC) and how it can handle priorities and groups for different interrupt sources. We will now use the NVIC to change the interrupt priority such that the interrupt that came first, now becomes second. We will use the integrated function call shown below:

NVIC_SetPriority(IRQn, priority);
IRQn is the interrupt number, and priority is the new priority for the interrupt. In class, you also learned about groups in the NVIC. You can use the following function to assemble a priority
uint32_t NVIC_EncodePriority (PriorityGroup, PreemptPriority, SubPriority)
You can find these two functions in CMSIS/core_cm3.h.

Modify your code such that the interrupt that was first, now becomes second. Execute your application and verify that it works using the serial console.

Explain in two sentences which specific registers you modified by using NVIC_SetPriority and NVIC_EncodePriority.

Interrupt Preemption

Changing the priority of an interrupt changes the order at which interrupts get handled. But what happens if an interrupt fires while another interrupt is still getting processed? Interrupt service routines are not protected from other interrupts if they have a higher priority (lower priority number). To illustrate this, let's add a delay between the two interrupts that so far fired at the same time, such that one of the interrupts comes later than the other.

The following code is a delay loop using a counter. Add it to your verilog code and adapt it such that the second interrupt (named FABINT_OUT2 in my code) fires later than the FABINT interrupt.

reg  [31:0]   counter; 
reg counter_en;

always@(posedge PCLK or posedge FABINT or negedge PRESERN)
if(~PRESERN)
  begin
    counter <= 32'h00000000;
    FABINT_OUT2   <= 1'b0;
    counter_en <= 1'b0; 
  end
else if(FABINT)
  begin
    counter <= 32'h00000000;
    counter_en <= 1'b1;
    FABINT_OUT2 <= 1'b0;
  end
else if(counter_en)
  begin
    if(counter == 32'h00004FFF)
      begin
        counter <= counter + 1;
        counter_en <= 1'b0;
        FABINT_OUT2 <= 1'b1;
      end
    else if(counter == 32'h00005000)
      begin
        counter <= 32'h00000000;
        counter_en <= 1'b0;
        FABINT_OUT2 <= 1'b0;
      end
    else
      begin
        counter <= counter + 1;
        counter_en <= 1'b1;
        FABINT_OUT2 <= 1'b0;
      end
  end
else
  begin
    counter <= 32'h00000000;
    counter_en <= 1'b0;
    FABINT_OUT2 <= 1'b0;
  end
endmodule
Program your SmartFusion with this addition, and look at the output of the terminal. Make sure that the later interrupt has a higher priority (lower priority number) in your SoftConsole code. Your output should look similar to the following picture. If it doesn't then most likely some of your clock subsystem is differently configured. If this is the case, ask a lab assistant for help.

Explain the output. What happened? And why is the string the same, even though that we have two different printf functions?

Explain at least two methods on how to avoid this problem.

Post-Lab Assignment

Write a debouncer for the switches, either in your C code or in verilog.

Write a verilog component that generates an interrupt every X clock ticks. Make this component into a peripheral such that the Cortex-M3 can start, stop, reset, and change X. Write a small C application using your peripheral and toggle an LED whenever your peripheral generates an interrupt.

Deliverables

You may work with a partner for this assignment. Submit a zip file of your SoftConsole workbench, and a second zip file of your Libero project to eecs373@gmail.com with the subject f10-lab4:uniquename1,uniquename2. Also, hand in your answers from the questions asked during the lab. You can use the following Answer Sheet.