Interrupts Overview

Overview

By and large, a software application is a collection of machine instructions that occur in  a defined sequence.  The sequence of instructions is determined by the sourced code we generate.   Operation A is followed by operation B which is followed by operation C and so on.

In most embedded systems, there are events that occur that do not follow this predictable sequence of operations.  They are events that could happen at truly random intervals.  These events are considered to be asynchronous to the normal operation of the system.  We are going to call these events interrupts.  The purpose of an interrupt is to alert the microprocessor that some important event has occurred and the operating state of the application should change to accommodate this event.  The event interrupts the normal application flow in order to properly respond to the interrupt.

Interrupts allow a microprocessor to respond to events in a timely manner.  Let’s take the example of a user pressing a push button.  In our hypothetical example, the main application would perform board initialization and then enter an infinite loop that does some data operations, prints a message, and then checks to see if a push button has been pressed.  The figure below provides a visual representation of this system.  It represents two passes through the while loop.

InterruptIntro1

As long as the button is pressed while the application is currently examining the button, the button press is detected.  But what happens if the button is pressed and then released while the microprocessor is executing the instructions necessary to print the message?  The application would essentially ignore the fact the button was pushed.

Interrupts are going to allow us to change how we examine the button.  We want to be able to examine the button as soon as it is pressed.  In order to do that, we need to temporarily stop the execution of our main program and begin executing code that is dedicated to examining the button.  The figure below illustrates the desired behavior.

InterruptIntro2

Interrupt Service Routine

When an interrupt occurs, a dedicated section of code is executed in response to the interrupt.  This dedicated piece of code is called the Interrupt Service Routine or ISR.  The ISR runs at a higher priority than the main application.  This means that once the ISR begins to execute, it will run until completion and then return to the main application.

The purpose of the ISR is to acknowledge that the interrupt has occurred, queue any data the peripheral has received, and then possibly inform the main application that the event has occurred.  Lets break down the responsibilities of the ISR in more detail.

Queue Data

Often times an interrupt indicates that new data has arrived.  The ISR will read the data from the interrupt source and place the data into SRAM.  Since SRAM is accessible by both the ISR and the main application, information is passed between the two using global variables.

It is best practice to have the ISR add data into a data queue of some kind.  Examining the data and determining the appropriate response is not the responsibility of the ISR.  Examining the data is deferred to the main application.  The reason for only queuing the data is that we want our ISRs to be as short and fast as possible.  The reason for short, fast ISRs is that most systems have multiple interrupt sources.  If all of the interrupts are of the same priority, a slow ISR will block the other ISRs from executing for a significant period of time.  Deferring a bulk of the computationally intensive instructions to the main application increases the probability that all interrupts will be received by the system in a timely manner.

Alert the Main Application

When new data arrives and is placed in SRAM, we need a mechanism to inform the main application that new data has been been placed in SRAM.  The most basic approach is to have the ISR set a dedicated global variable to Boolean true.

The main application is written in a way that it only examines the data in SRAM if the dedicated global variable is set to true.  If the global variable is set to true, the main application sets the global variable to false and begins to process the data placed in the queue by the ISR.

Acknowledge the Interrupt

When an interrupt occurs it normally sets a bit in an interrupt status register.  The ISR needs to clear this bit in the status register so that processor resumes execution of the main application.  The ISR should clear the status bit prior to exiting the ISR.  If you fail to clear the status bit, the ISR will continually be called and the main application will never resume.

ISR Do’s

  • Interrupt service routines should be as minimal as possible
  • If the interrupt indicates that data has been received, add the data to a queue in SRAM
  • Indicate events to the application using global variables.
  • Clear the interrupt prior to returning to the main application

ISR Don’t’s

  • Do not poll on any status bits in an ISR
  • Do not use any computationally intensive loops
  • Do not modify SRAM other than the dedicated queues and global variables that are being used to communicate with the main application

Interrupt Service Routine Execution

When an interrupt occurs, how does the interrupt service routine get executed?  One common misconception is that the main application calls the interrupt service routine just like it would call any other function.  That, however, is not the case.  When an interrupt is detected, the microprocessor enters into a special interrupt mode called Handler mode.  The microprocessor will save all of the general purpose registers, any status registers, and the program counter to either a reserved portion of SRAM or a dedicated hardware resource.  This collection of registers fully describes the current computational state of an application.   Saving these registers is called a context save.  The context save allows the processor to halt the execution of an application and resume the application at a later time.

Once the context save is finished, the hardware will automatically modify the program counter (PC) and load it with the pre-defined address of an interrupt service routine.  The ISR executes and carries out the instructions in the ISR.  When the ISR exits, the microprocessor does a context restore by restoring the general purpose registers, PC, and status registers with the values each register held immediately before the interrupt.  The final step the microprocessor carries out is to exit Handler Mode.  At this point, the PC contains the address of the next instruction in the application and the application continues on as normal.  Its important to realize that the ISR does not return to the main routine using the value in the link register.  The microprocessor ‘returns’ to the main application by restoring the value of the PC when the microprocessor exits Handler Mode.

Polled Vs Vectored Interrupts

When an interrupt is detected and hardware has saved the context of  the application, how does it know which interrupt service vector to execute?  There are two common approaches to determining how to execute the correct interrupt service routine.

The first approach is called Polled Interrupts.  If a microprocessor uses polled interrupts, the microprocessor will branch to a predefined location that contains a master ISR.  Once inside the master ISR, the source of the interrupt is determined by examining interrupt status registers.  The interrupt status registers will contain a bit mask that indicates which interrupts are currently active. Once the active interrupt has been determined, the master ISR will then make a function call to a function that acts as the dedicated ISR for the specified device.

The second approach to executing an ISR is called a vectored interrupt handler.  A processor that implements a vectored interrupt handler has dedicated memory addresses for each peripheral device.  Each of those memory address locations contains the address of the ISR associated with the specified peripheral.  When an interrupt occurs, the microprocessor hardware detects which device caused the interrupt and will load the PC with the address specified in the vector table.  Vectored interrupts normally result in a more complex interrupt mechanism, but it provides superior performance since software is not responsible for determining the active interrupt source.

ARM Cortex-M Interrupt Priority

Most embedded systems will have multiple interrupts active.  So what happens when the ISR of one peripheral device is being executed and another interrupt occurs?   The answer to that depends on the priority of each ISR.  The ARM architecture allows us to set a priority level for different interrupts.  The lower the interrupt number, the higher priority.  If an ISR is already running and a higher priority interrupt occurs, the lower priority ISR is interrupted and the higher priority ISR begins to execute.  When the higher priority ISR finishes, the remainder of the lower ISR will then complete.

IRQ-HigherPriority

 

If the second interrupt is of the same priority or lower priority, the active ISR will complete and then the second interrupt’s ISR is executed.  Notice that the second ISR is not executed immediately, but it does in fact get executed.

IRQ-LowerSame

If multiple lower/same priority interrupts occur while a higher priority ISR is active, only one of the interrupts from the lower/same priority source will be acknowledged.   The ISR status registers do not keep track of how many times a specific interrupt has occurred.

IRQ-LowerSame-multiple

MSP432 IRQ Examples

Configuring the Vector Table

The ARM Cortex-M microprocessors define the start of the interrupt vector table at address 0x00000004.  The first 15 vectors are assigned to exceptions.  Exceptions are the highest priority interrupts in the Cortex-M architecuture.  These exceptions are common to every Cortex-M processor.  If we look at startup_msp432p401r_ccs.c.s, we can see how the addresses in the vector table are initialized for the first 15 vectors.

; Vector Table Mapped to Address 0 at Reset

               #pragma DATA_SECTION(interruptVectors, ".intvecs")
void (* const interruptVectors[])(void) =
{
    (void (*)(void))((uint32_t)&__STACK_END),
                                           /* The initial stack pointer */
    Reset_Handler,                         /* The reset handler         */
    NMI_Handler,                           /* The NMI handler           */
    HardFault_Handler,                     /* The hard fault handler    */
    MemManage_Handler,                     /* The MPU fault handler     */
    BusFault_Handler,                      /* The bus fault handler     */
    UsageFault_Handler,                    /* The usage fault handler   */
    0,                                     /* Reserved                  */
    0,                                     /* Reserved                  */
    0,                                     /* Reserved                  */
    0,                                     /* Reserved                  */
    SVC_Handler,                           /* SVCall handler            */
    DebugMon_Handler,                      /* Debug monitor handler     */
    0,                                     /* Reserved                  */
    PendSV_Handler,                        /* The PendSV handler        */
    SysTick_Handler,                       /* The SysTick handler       */
    PSS_IRQHandler,                        /* PSS Interrupt             */
    CS_IRQHandler,                         /* CS Interrupt              */
    PCM_IRQHandler,                        /* PCM Interrupt             */
    WDT_A_IRQHandler,                      /* WDT_A Interrupt           */
    FPU_IRQHandler,                        /* FPU Interrupt             */
    FLCTL_IRQHandler,                      /* Flash Controller Interrupt*/
    COMP_E0_IRQHandler,                    /* COMP_E0 Interrupt         */
    COMP_E1_IRQHandler,                    /* COMP_E1 Interrupt         */
    TA0_0_IRQHandler,                      /* TA0_0 Interrupt           */
    TA0_N_IRQHandler,                      /* TA0_N Interrupt           */
    TA1_0_IRQHandler,                      /* TA1_0 Interrupt           */
    TA1_N_IRQHandler,                      /* TA1_N Interrupt           */
    TA2_0_IRQHandler,                      /* TA2_0 Interrupt           */
    TA2_N_IRQHandler,                      /* TA2_N Interrupt           */
    TA3_0_IRQHandler,                      /* TA3_0 Interrupt           */
    TA3_N_IRQHandler,                      /* TA3_N Interrupt           */
    EUSCIA0_IRQHandler,                    /* EUSCIA0 Interrupt         */
    EUSCIA1_IRQHandler,                    /* EUSCIA1 Interrupt         */
    EUSCIA2_IRQHandler,                    /* EUSCIA2 Interrupt         */
    EUSCIA3_IRQHandler,                    /* EUSCIA3 Interrupt         */
    EUSCIB0_IRQHandler,                    /* EUSCIB0 Interrupt         */
    EUSCIB1_IRQHandler,                    /* EUSCIB1 Interrupt         */
    EUSCIB2_IRQHandler,                    /* EUSCIB2 Interrupt         */
    EUSCIB3_IRQHandler,                    /* EUSCIB3 Interrupt         */
    ADC14_IRQHandler,                      /* ADC14 Interrupt           */
    T32_INT1_IRQHandler,                   /* T32_INT1 Interrupt        */
    T32_INT2_IRQHandler,                   /* T32_INT2 Interrupt        */
    T32_INTC_IRQHandler,                   /* T32_INTC Interrupt        */
    AES256_IRQHandler,                     /* AES256 Interrupt          */
    RTC_C_IRQHandler,                      /* RTC_C Interrupt           */
    DMA_ERR_IRQHandler,                    /* DMA_ERR Interrupt         */
    DMA_INT3_IRQHandler,                   /* DMA_INT3 Interrupt        */
    DMA_INT2_IRQHandler,                   /* DMA_INT2 Interrupt        */
    DMA_INT1_IRQHandler,                   /* DMA_INT1 Interrupt        */
    DMA_INT0_IRQHandler,                   /* DMA_INT0 Interrupt        */
    PORT1_IRQHandler,                      /* Port1 Interrupt           */
    PORT2_IRQHandler,                      /* Port2 Interrupt           */
    PORT3_IRQHandler,                      /* Port3 Interrupt           */
    PORT4_IRQHandler,                      /* Port4 Interrupt           */
    PORT5_IRQHandler,                      /* Port5 Interrupt           */
    PORT6_IRQHandler                       /* Port6 Interrupt           */
};

If we look at the SysTick timer, we can assign a specific handler to the SysTick timer by implementing the function name provided in the vector table.  In this case, SysTick_Handler.

volatile bool AlertSysTick;

// SysTick Handler
void SysTick_Handler(void)
{
   uint32_t val;

   // Alert the main application the SysTick Timer has expired
   AlertSysTick = true;

   // Clears the SysTick Interrupt
   val = SysTick->VAL;
}

The compiler will determines the address of the SysTick_Handler  and writes this address into the 16th entry of the vector table.

Note the keyword volatile next to AlertSysTick.  This forces the compiler to re-read the memory location for AlertSysTick every time the variable is referenced.  A good question is why do we need to re-read the global variable each time it is referenced in our code?

The reason we always re-read global variables that are shared between an ISR and the main application is that the main application has no forewarning that the interrupt service routine is going to modify a global variable.  Imagine a scenario where the main application has loaded a Boolean global variable into one of the general purpose registers and makes decisions based on the value of that global variable. If an interrupt occurs and the corresponding interrupt service handler modifies the same Boolean global variable in SRAM,  there are now have two different values associated with the same global variable:  the stale value the main routine has loaded in one of the general purpose registers and the most up-to-date value that the ISR wrote to SRAM.

Since both the main routine and the ISR access the same variable, we want both the ISR and main application to always re-read the variable from SRAM each time the variable is referenced so that a stale value of the variable that resides in a general purpose register is not used when deciding which action to take next.  The volatile keyword tells the compiler to generate code that re-reads SRAM each time the variable is referenced in the code.

Enabling SysTick Interrupts

In addition to defining the function in the vector table, we need to enable the desired peripheral and configure it to generate interrupts.  In the case of the SysTick timer, we can call a function provided by the Keil IDE to do this.

uint32_t SysTick_Config(uint32_t ticks)
{
  /* Reload value impossible */
  if ((ticks - 1) > SysTick_LOAD_RELOAD_Msk)  return (1);      

  /* set reload register */
  SysTick->LOAD  = ticks - 1;           

  /* set Priority for Systick Interrupt */
  NVIC_SetPriority (SysTick_IRQn, (1<<__NVIC_PRIO_BITS) - 1);  

  /* Load the SysTick Counter Value */
  SysTick->VAL   = 0;                                          

  /* Enable SysTick IRQ and SysTick Timer */
  SysTick->CTRL  = SysTick_CTRL_CLKSOURCE_Msk |
                   SysTick_CTRL_TICKINT_Msk   |
                   SysTick_CTRL_ENABLE_Msk;                   

  /* Function successful */
  return (0);                                                  
}

Communicating with Application Code

Once enabled, the SysTick timer will generate an interrupt every time the timer reaches zero.   The handler will set the AlertSysTick variable to true and then clear the interrupt.    The main routine will then poll on AlertSysTick.  Once AlertSysTick is true, the main routine will set AlertSysTick to false and carry out an action on behalf of the SysTick timer.

//*****************************************************************************
//*****************************************************************************
int 
main(void)
{
  // Interrupt once every 100ms assuming 50MHz clock
  SysTick_Config(5000000);

  // Infinite loop
  while(1)
  {
      if(AlertSysTick)
      {
	// Do something once every 100ms

        AlertSysTick = false;
      }
  }
}

 

Enabling Non-SysTick Interrupts

The TI specific interrupts are defined in the remainder of the vector table.  Again, our ISR names need to match the value in the vector table. Each of these peripherals needs to be enabled and configured to generate interrupts.  This is normally accomplished by writing to the appropriate bit locations of the peripheral devices interrupt control registers.  The datasheet will provide more information on the types of interrupts each peripheral device generates.

For non-SysTick interrupts, in addition to enabling interrupts in the peripherals IMR,  we need to write to the the NVIC (Nested Vector Interrupt Controller) and indicate that we want to enable an interrupt from a given peripheral.   This can be done using the following function call .

  NVIC_EnableIRQ(PORT1_IRQn);  // Enable PORT1 in the NVIC

The specific interrupt numbers the MSP432 are are defined in msp432p401r.h

/******************************************************************************
* CMSIS-compatible Interrupt Number Definition                                *
******************************************************************************/
typedef enum IRQn
{
  /* Cortex-M4 Processor Exceptions Numbers */
  NonMaskableInt_IRQn         = -14,    /*  2 Non Maskable Interrupt */
  HardFault_IRQn              = -13,    /*  3 Hard Fault Interrupt */
  MemoryManagement_IRQn       = -12,    /*  4 Memory Management Interrupt */
  BusFault_IRQn               = -11,    /*  5 Bus Fault Interrupt */
  UsageFault_IRQn             = -10,    /*  6 Usage Fault Interrupt */
  SVCall_IRQn                 = -5,     /* 11 SV Call Interrupt */
  DebugMonitor_IRQn           = -4,     /* 12 Debug Monitor Interrupt */
  PendSV_IRQn                 = -2,     /* 14 Pend SV Interrupt */
  SysTick_IRQn                = -1,     /* 15 System Tick Interrupt */
  /*  Peripheral Exceptions Numbers */
  PSS_IRQn                    = 0,     /* 16 PSS Interrupt             */
  CS_IRQn                     = 1,     /* 17 CS Interrupt              */
  PCM_IRQn                    = 2,     /* 18 PCM Interrupt             */
  WDT_A_IRQn                  = 3,     /* 19 WDT_A Interrupt           */
  FPU_IRQn                    = 4,     /* 20 FPU Interrupt             */
  FLCTL_IRQn                  = 5,     /* 21 Flash Controller Interrupt*/
  COMP_E0_IRQn                = 6,     /* 22 COMP_E0 Interrupt         */
  COMP_E1_IRQn                = 7,     /* 23 COMP_E1 Interrupt         */
  TA0_0_IRQn                  = 8,     /* 24 TA0_0 Interrupt           */
  TA0_N_IRQn                  = 9,     /* 25 TA0_N Interrupt           */
  TA1_0_IRQn                  = 10,     /* 26 TA1_0 Interrupt           */
  TA1_N_IRQn                  = 11,     /* 27 TA1_N Interrupt           */
  TA2_0_IRQn                  = 12,     /* 28 TA2_0 Interrupt           */
  TA2_N_IRQn                  = 13,     /* 29 TA2_N Interrupt           */
  TA3_0_IRQn                  = 14,     /* 30 TA3_0 Interrupt           */
  TA3_N_IRQn                  = 15,     /* 31 TA3_N Interrupt           */
  EUSCIA0_IRQn                = 16,     /* 32 EUSCIA0 Interrupt         */
  EUSCIA1_IRQn                = 17,     /* 33 EUSCIA1 Interrupt         */
  EUSCIA2_IRQn                = 18,     /* 34 EUSCIA2 Interrupt         */
  EUSCIA3_IRQn                = 19,     /* 35 EUSCIA3 Interrupt         */
  EUSCIB0_IRQn                = 20,     /* 36 EUSCIB0 Interrupt         */
  EUSCIB1_IRQn                = 21,     /* 37 EUSCIB1 Interrupt         */
  EUSCIB2_IRQn                = 22,     /* 38 EUSCIB2 Interrupt         */
  EUSCIB3_IRQn                = 23,     /* 39 EUSCIB3 Interrupt         */
  ADC14_IRQn                  = 24,     /* 40 ADC14 Interrupt           */
  T32_INT1_IRQn               = 25,     /* 41 T32_INT1 Interrupt        */
  T32_INT2_IRQn               = 26,     /* 42 T32_INT2 Interrupt        */
  T32_INTC_IRQn               = 27,     /* 43 T32_INTC Interrupt        */
  AES256_IRQn                 = 28,     /* 44 AES256 Interrupt          */
  RTC_C_IRQn                  = 29,     /* 45 RTC_C Interrupt           */
  DMA_ERR_IRQn                = 30,     /* 46 DMA_ERR Interrupt         */
  DMA_INT3_IRQn               = 31,     /* 47 DMA_INT3 Interrupt        */
  DMA_INT2_IRQn               = 32,     /* 48 DMA_INT2 Interrupt        */
  DMA_INT1_IRQn               = 33,     /* 49 DMA_INT1 Interrupt        */
  DMA_INT0_IRQn               = 34,     /* 50 DMA_INT0 Interrupt        */
  PORT1_IRQn                  = 35,     /* 51 Port1 Interrupt           */
  PORT2_IRQn                  = 36,     /* 52 Port2 Interrupt           */
  PORT3_IRQn                  = 37,     /* 53 Port3 Interrupt           */
  PORT4_IRQn                  = 38,     /* 54 Port4 Interrupt           */
  PORT5_IRQn                  = 39,     /* 55 Port5 Interrupt           */
  PORT6_IRQn                  = 40      /* 56 Port6 Interrupt           */
} IRQn_Type;

 

Leave a Reply