Nested Vector Interrupt Controller (NVIC)

Objective

This tutorial demonstrates how to use interrupts on a processor. In general, you will understand the concept behind interrupts on any processor, but we will use the SJ2 board as an example.

What is an interrupt?

An interrupt is the hardware capability of a CPU to break the normal flow of software to attend an urgent request.

The science behind interrupts lies in the hardware that allows the CPU to be interrupted. Each peripheral in a micro-controller may be able to assert an interrupt to the CPU core, and then the CPU core would jump to the corresponding interrupt service routine (ISR) to service the interrupt.

void main_loop(void) {
  while (forever) {
    logic();
    another_function();
    // ...
  }
}

// Interrupt on button-press
void button_press_interrupt(void) {
}

ISR Procedure

The following steps demonstrate what happens when an interrupt occurs :

  • CPU manipulates the PC (program counter) to jump to the ISR
  • IMPORTANT: CPU will disable interrupts (or that priority level's interrupts until the end of ISR)
  • Registers are saved before running the ISR (pushed onto the stack)
  • ISR is run
  • Registers are restored (popped from stack)
  • Interrupts are re-enabled (or that priority level's interrupt is re-enabled)

On some processors, the savings and restoring of registers is a manual step and the compiler would help you do it. You can google the "GCC interrupt attribute" to study this topic further. On the SJ2 board, which uses LPC40xx (ARM Cortex M4), this step is automatically taken care of by the CPU hardware.

 

Figure 1. Nested Interrupt Processing

 

Nested Vector Interrupt Controller

Nested Vector Interrupt Controllers or NVIC for short, have two properties:

  • Can handle multiple interrupts.
    • The number of interrupts implemented is device-dependent.
  • A programmable priority level for each interrupt.
    • A higher level corresponds to a lower priority, so level 0 is the highest interrupt priority.
  • Level and pulse detection of interrupt signals.
  • Grouping of priority values into group priority and sub-priority fields.
    • This means that interrupts of the same priority are grouped together and do not preempt each other.
    • Each interrupt also has a sub-priority field which is used to figure out the run order of pending interrupts of the same priority.
  • Interrupt tail-chaining.
    • This enables back-to-back interrupt processing without the overhead of state saving and restoration between interrupts.
    • This saves us from the step of having to restore and then save the registers again.
  • An external Non-maskable interrupt (NMI)

NVIC Interrupt Example 

 

Figure 2. Multiple Interrupt Processing 

The SW to HW Connection

Now that we understand how the CPU hardware services interrupts, we need to define how we inform the CPU WHERE our ISR function is located at.

Interrupt Vector Table

This table is nothing but addresses of functions that correspond to the microcontroller interrupts. Specific interrupts use specific "slots" in this table, and we have to populate these spots with our software functions that service the interrupts.

Figure 3. HW Interrupt Vector Table

SJTwo (LPC40xx) Example

Using a linker script and compiler directives (commands for the compiler), the compiler is able to place the software interrupt vector table at a specific location that the CPU expects the interrupt vector table to be located at. This connects the dots about how the CPU is able to determine WHERE your interrupt service routines are located at. From there on, anytime a specific interrupt occurs, the CPU is able to fetch the address and make the JUMP.

static void halt(void);

typedef void (*void_func_ptr_t)(void);

__attribute__((section(".interrupt_vector_table"))) void_func_ptr_t interrupt_vector_table[] = {
    /**
     * Core interrupt vectors - Mandated by Cortex-M4 core
     */
    (void_func_ptr_t)&_estack, // 0 ARM: Initial stack pointer
    cpu_startup_entry_point,   // 1 ARM: Initial program counter
    halt,                      // 2 ARM: Non-maskable interrupt
    halt,                      // 3 ARM: Hard fault
    halt,                      // 4 ARM: Memory management fault
    halt,                      // 5 ARM: Bus fault
    halt,                      // 6 ARM: Usage fault
    halt,                      // 7 ARM: Reserved
    halt,                      // 8 ARM: Reserved
    halt,                      // 9 ARM: Reserved
    halt,                      // 10 ARM: Reserved
    vPortSVCHandler,           // 11 ARM: Supervisor call (SVCall)
    halt,                      // 12 ARM: Debug monitor
    halt,                      // 13 ARM: Reserved
    xPortPendSVHandler,        // 14 ARM: Pendable request for system service (PendableSrvReq)
    xPortSysTickHandler,       // 15 ARM: System Tick Timer (SysTick)

    /**
     * Device interrupt vectors - routed to a 'dispatcher' that allows users to register their ISR at this vector
     * You can 'hijack' this vector and directly install your interrupt service routine
     */
    lpc_peripheral__interrupt_dispatcher, // 16 WDT
    lpc_peripheral__interrupt_dispatcher, // 17 Timer 0
    lpc_peripheral__interrupt_dispatcher, // 18 Timer 1
    lpc_peripheral__interrupt_dispatcher, // 19 Timer 2
    lpc_peripheral__interrupt_dispatcher, // 20 Timer 3
    lpc_peripheral__interrupt_dispatcher, // 21 UART 0
    lpc_peripheral__interrupt_dispatcher, // 22 UART 1
    lpc_peripheral__interrupt_dispatcher, // 23 UART 2
    lpc_peripheral__interrupt_dispatcher, // 24 UART 3
    lpc_peripheral__interrupt_dispatcher, // 25 PWM 1
    lpc_peripheral__interrupt_dispatcher, // 26 I2C 0
    lpc_peripheral__interrupt_dispatcher, // 27 I2C 1
    lpc_peripheral__interrupt_dispatcher, // 28 I2C 2
    lpc_peripheral__interrupt_dispatcher, // 29 UNUSED
    lpc_peripheral__interrupt_dispatcher, // 30 SSP 0
    lpc_peripheral__interrupt_dispatcher, // 31 SSP 1
    lpc_peripheral__interrupt_dispatcher, // 32 PLL 0
    lpc_peripheral__interrupt_dispatcher, // 33 RTC and Event Monitor/Recorder
    lpc_peripheral__interrupt_dispatcher, // 34 External Interrupt 0 (EINT 0)
    lpc_peripheral__interrupt_dispatcher, // 35 External Interrupt 1 (EINT 1)
    lpc_peripheral__interrupt_dispatcher, // 36 External Interrupt 2 (EINT 2)
    lpc_peripheral__interrupt_dispatcher, // 37 External Interrupt 3 (EINT 3)
    lpc_peripheral__interrupt_dispatcher, // 38 ADC
    lpc_peripheral__interrupt_dispatcher, // 39 BOD
    lpc_peripheral__interrupt_dispatcher, // 40 USB
    lpc_peripheral__interrupt_dispatcher, // 41 CAN
    lpc_peripheral__interrupt_dispatcher, // 42 DMA Controller
    lpc_peripheral__interrupt_dispatcher, // 43 I2S
    lpc_peripheral__interrupt_dispatcher, // 44 Ethernet
    lpc_peripheral__interrupt_dispatcher, // 45 SD Card Interface
    lpc_peripheral__interrupt_dispatcher, // 46 Motor Control PWM
    lpc_peripheral__interrupt_dispatcher, // 47 PLL 1
    lpc_peripheral__interrupt_dispatcher, // 48 Quadrature Encoder
    lpc_peripheral__interrupt_dispatcher, // 49 USB Activity
    lpc_peripheral__interrupt_dispatcher, // 50 CAN Activity
    lpc_peripheral__interrupt_dispatcher, // 51 UART 4
    lpc_peripheral__interrupt_dispatcher, // 52 SSP 2
    lpc_peripheral__interrupt_dispatcher, // 53 LCD
    lpc_peripheral__interrupt_dispatcher, // 54 GPIO Interrupt
    lpc_peripheral__interrupt_dispatcher, // 55 PWM 0
    lpc_peripheral__interrupt_dispatcher, // 56 EEPROM
};

static void halt(void) {
  // This statement resolves compiler warning: variable define but not used
  (void)interrupt_vector_table;

  while (true) {
  }
}

Code Block 1. Software Interrupt Vector Table

NOTE: that a vector table is really just a lookup table that hardware utilizes.

Two Methods to set up an ISR on the SJ2

All of the methods require that you run this function to allow the NVIC to accept a particular interrupt request.

NVIC_EnableIRQ(EINT3_IRQn);

Where the input is the IRQ number. This can be found in the LCP40xx.h file. Search for enum IRQn.

 

Method 1. Modify IVT 

We discourage modifying the interrupt_vector_table.c (or startup.cpp for SJ2) vector tables directly.

IVT modify

__attribute__((section(".interrupt_vector_table"))) void_func_ptr_t interrupt_vector_table[] = {
    /**
     * Core interrupt vectors
     */
    (void_func_ptr_t)&_estack, // 0 ARM: Initial stack pointer
    cpu_startup_entry_point,   // 1 ARM: Initial program counter
    halt,                      // 2 ARM: Non-maskable interrupt
    halt,                      // 3 ARM: Hard fault
    halt,                      // 4 ARM: Memory management fault
    halt,                      // 5 ARM: Bus fault
    halt,                      // 6 ARM: Usage fault
    halt,                      // 7 ARM: Reserved
    halt,                      // 8 ARM: Reserved
    halt,                      // 9 ARM: Reserved
    halt,                      // 10 ARM: Reserved
    vPortSVCHandler,           // 11 ARM: Supervisor call (SVCall)
    halt,                      // 12 ARM: Debug monitor
    halt,                      // 13 ARM: Reserved
    xPortPendSVHandler,        // 14 ARM: Pendable request for system service (PendableSrvReq)
    xPortSysTickHandler,       // 15 ARM: System Tick Timer (SysTick)

    /**
     * Device interrupt vectors
     */
    lpc_peripheral__interrupt_dispatcher, // 16 WDT
    lpc_peripheral__interrupt_dispatcher, // 17 Timer 0
    lpc_peripheral__interrupt_dispatcher, // 18 Timer 1
    lpc_peripheral__interrupt_dispatcher, // 19 Timer 2
    lpc_peripheral__interrupt_dispatcher, // 20 Timer 3
    lpc_peripheral__interrupt_dispatcher, // 21 UART 0
    lpc_peripheral__interrupt_dispatcher, // 22 UART 1
    lpc_peripheral__interrupt_dispatcher, // 23 UART 2
    my_own_uart3_interrupt, // 24 UART 3  <-------------------- Install your function to the ISR vector directly
    // ...
};

Code Block 3. Weak Function Override Template

 

Method 2. ISR Register Function

There is a simple API defined at lpc_peripherals.h that you can use. Be sure to check the implementation of this code module to actually understand what it is doing.

This is the best option! Please use this option almost always!

// Just your run-of-the-mill function
void my_uart3_isr(void) {
    do_something();
    clear_uart3_interrupt();
}

#include "lpc_peripherals.h"
int main() {
    lpc_peripheral__enable_interrupt(LPC_PERIPHERAL__UART3, my_uart3_isr);
  
    // ... rest of the code
}

Code Block  5. Weak Function Override Template

PROS CONS
  • Can dynamically change ISR during runtime.
  • Does not disturb core library files in the process of adding/changing ISRs.
    • Always try to prevent changes to the core libraries.
  • Does not cause compiler errors.
  • Your ISR cpu utilization is tracked.
  • Must wait until main is called before ISR is registered
    • Interrupt events could happen before main begins.

What to do inside an ISR

Do very little inside an ISR. When you are inside an ISR, the whole system is blocked (other than higher priority interrupts). If you spend too much time inside the ISR, then you are destroying the real-time operating system principle and everything gets clogged.

With that said, here is the general guideline:

Short as possible

DO NOT POLL FOR ANYTHING! Try to keep loops as small as possible.  Note that printing data over UART can freeze the entire system, including the RTOS for that duration.  For instance, printing 4 chars may take 1ms at 38400bps.

FreeRTOS API calls

If you are using FreeRTOS API, you must use FromISR functions only! If a FromISR function does not exist, then don't use that API.

Clear Interrupt Sources

Clear the source of the interrupt. For example, if interrupt was for rising edge of a pin, clear the "rising edge" bit such that you will not re-enter into the same interrupt function. 

If you don't do this, your interrupt will get stuck in an infinite ISR call loop.  For the Port interrupts, this can be done by writing to the IntClr registers.

ISR processing inside a FreeRTOS Task

What you may argue with the example below is that we do not process the ISR immediately, and therefore delay the processing. But you can tackle this scenario by resuming a HIGHEST priority task. Immediately, after the ISR exits, due to the ISR "yield", FreeRTOS will resume the high priority task immediately rather than servicing another task

/* Create the semaphore in main() */
SemaphoreHandle_t button_press_semaphore = NULL;

void my_button_press_isr(void) {
    long yield = 0;
    xSemaphoreGiveFromISR(button_press_semaphore, &yield);
    portYIELD_FROM_ISR(yield);
}

void button_task(void *pvParameter)
{
    while(1) {
        if (xSemaphoreTake(button_press_semaphore, portMAX_DELAY)) {
            /* Process the interrupt */
        }
    }
}

void main(void)
{
    button_press_semaphore = xSemaphoreCreateBinary();
    /* TODO: Hook up my_button_press_isr() as an interrupt */
    /* TODO: Create button_task() and start FreeRTOS scheduler */
}

Code Block 6. Wait on Semaphore ISR design pattern example 

Resources

http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.ddi0489b/CACDDJHB.html

Back to top