Wednesday, January 12, 2011

Interrupts, Software Interrupts and Interrupt Priority in LPC1768

The cortex-m3 core which the LPC1768 uses has an extremely flexible interrupt controller called the NVIC (Nested Vectored Interrupt Controller). The NVIC has several distinguishing features.

1. Vector table is relocatable. It can even be kept in the RAM.

SCB->VTOR = NEW_VECTOR_TABLE_LOCATION;

2. The entries in the vector table are not instructions which lead to the ISR but the address of the ISR itself.

3. Interrupt entry and exit take 12 cycles irrespective of instruction being executed ( maybe there are some rare exceptions to this ). The point is that interrupts are very deterministic.

4. Some of the registers are pushed onto the stack automatically before ISR code execution begins and also popped off when returning ( this is done within the 12 cycles mentioned above). So, the ISR code can be pure C code with no assembly wrappers.

5. Tail - chaining. If the CPU is servicing an ISR and another interrupt ( lower priority, so it doesnt pre-empt the current ISR in execution ) occurs, the stack is not popped. The CPU vectors to the next interrupt in 6 cycles instead of 12 cycles.

6. Interrupt priorities can be changed during run-time and two kinds of priorities are defined. We'll get to that later.

7. An interrupt will never preempt itself. This reduces the chances of stack overflow. For example, if a timer is made to generate interrupts once in 100 clock cycles and the interrupt execution lasts for 200 cycles, the first time the interrupt arrives, the ISR is fully serviced and then only will it acknowledged that another interrupt is being called for and execute the ISR again. It doesn't preempt the running ISR to call the same ISR again.

8. Explicit support for software interrupts.

Now lets get to some code. We'll set up a timer to generate an interrupt once in a while and toggle the LED inside the interrupt. If you need help setting up the timer, here is an excellent tutorial.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include "LPC17xx.h"

int main (void) 
{
    LPC_SC->PCONP |= 1 << 1; //Power up Timer 0
    LPC_SC->PCLKSEL0 |= 1 << 2; // Clock for timer = CCLK
    LPC_TIM0->MR0 = 1 << 23; // Give a value suitable for the LED blinking frequency based on the clock frequency
    LPC_TIM0->MCR |= 1 << 0; // Interrupt on Match0 compare
    LPC_TIM0->MCR |= 1 << 1; // Reset timer on Match 0.
    LPC_TIM0->TCR |= 1 << 1; // Manually Reset Timer0 ( forced )
    LPC_TIM0->TCR &= ~(1 << 1); // stop resetting the timer.
    NVIC_EnableIRQ(TIMER0_IRQn); // Enable timer interrupt
    LPC_TIM0->TCR |= 1 << 0; // Start timer


    LPC_SC->PCONP |= ( 1 << 15 ); // power up GPIO
    LPC_GPIO1->FIODIR |= 1 << 29; // puts P1.29 into output mode. LED is connected to P1.29

    while(1)
    {
        //do nothing
    }
    return 0;
  
}
void TIMER0_IRQHandler (void)
{
    if((LPC_TIM0->IR & 0x01) == 0x01) // if MR0 interrupt
    {
        LPC_TIM0->IR |= 1 << 0; // Clear MR0 interrupt flag
        LPC_GPIO1->FIOPIN ^= 1 << 29; // Toggle the LED
    }

}

The code is fairly simple. We set up timer 0 to run off the CPU Clock (CCLK). Match 0 is set to 2^23 ( 2 power 23) and we've asked Timer 0 to be reset on Match 0 and also an interrupt to be generated when Match 0 occurs. The timer starts, counts from 0 to 2^23. At this point, match occurs. The timer is reset and the interrupt occurs. Inside the interrupt, we check for the source of the interrupt (Timer 0 can produce interrupts from many sources like Mat0 , Mat1 etc..) and then toggle the LED.

Now, since the start up code gets the chip running at 100Mhz by default, 
1 tick of the timer = 1 / 100Mhz = 10 ns (nano seconds)
So ( 2^23 + 1 ) ticks = 0.08388609 seconds.
We should see the LED toggling once in 0.083 secs. That's what i'm seeing in front of me.
The compiled code for the above example is here.

Caveat: 
1. Bit 1 of LPC_TIM0->TCR resets the timer and holds it there even if you start the timer. Make sure to clear it. Otherwise, the timer wont tick.
2. An extra 1 is added in the calculation above since the timer starts counting from 0 and not 1. ( The timer in the PWM block on the other hand counts from 1 ).


Now, we'll stop using the timer but use the timer interrupt ! This is basically a software interrupt.

#include "LPC17xx.h"

volatile uint32_t del;
void _delay(uint32_t delay);

int main (void)
{
    NVIC_EnableIRQ(TIMER0_IRQn);
    LPC_SC->PCONP |= ( 1 << 15 ); // power up GPIO
    LPC_GPIO1->FIODIR |= 1 << 29; // puts P1.29 into output mode. LED is connected to P1.29
    while(1)
    {
        _delay(1 << 24); // Wait for about 1 second
        NVIC_SetPendingIRQ(TIMER0_IRQn); // Software interrupt
    }
    return 0;
 
}
void TIMER0_IRQHandler (void)
{
    LPC_GPIO1->FIOPIN ^= 1 << 29; // Toggle the LED
}
void _delay(uint32_t delay)
{
    uint32_t i;
    for(i = 0;i < delay;i++ )
        del = i; // do this so that the compiler does not optimize away the loop.
}

We're accomplishing the same thing. Blinking the LED. But now, instead of using a timer, we are using a software delay. The delay is about 1 second ( cant be sure as it depends on the compiler and optimization level ). After the delay, the interrupt is "called". The ISR is also shorter as it doesn't have to check for the source of the interrupt within Timer 0.
The compiled eclipse project is here.

And finally, we have interrupt priorities. In the LPC17xx user manual as of date, NVIC is explained in chapter 6. In page 87 of the manual , we have the register description for interrupt priorities.We see that each interrupt has a 5 bit priority level associated with it. What does this 5 bit number mean ? Strangely enough, the answer is not in chapter 6 which covers NVIC.

This piece of info is presented in the appendix of the manual ! Chapter 34 . More specifically, 34.3.3.6 Interrupt priority grouping in page 749.
The 5 bit priority field for each interrupt has 2 parts separated by a decimal point. In fact, Cortex M3 provides 8 bit priority field for each interrupt. But LPC1768 has implemented only 5 bits. the 3 LSB bits are permanently mapped to 0.

Interrupts are assigned to different groups and interrupts in each group have different priority within the group. Lets see what the implications are:

1. An interrupt belonging to a lower group (number) may preempt an interrupt of a higher group(number). Suppose Interrupt A is in group 4 and is running. Interrupt B in group 2 arrives while Int A ISR is executing. Now, interrupt A ISR is preempted and ISR for Interrupt B is executed. After ISR B is done, execution of ISR A continues.
2. If interrupt C which is also in group 4 arrives while ISR A is executing, ISR A is NOT preempted. It has to wait. After execution of ISR A, ISR C is executed.
3. If interrupt A and C arrive simultaneously, the one with lower sub-priority value is executed first.

Here is another sweet thing. The decimal point ( to be accurate, binary point ) demarcating group priority number from sub priority number may be positioned as the user wishes. This means we can choose and have a compromise on the number of groups vs. subpriority.

Lets see how we go about doing this. Have a look at section 34.4.3.6.1 Binary Point in page  772 of the manual.

NVIC_SetPriorityGrouping(0x04);

Setting PRIGROUP to 4 puts the binary point after b4 as illustrated below. This will create 8 priority groups( 3 bits for priority group ) and 4 sub priority levels ( 2 bits for sub level ) in each group.

Now, we can set the priority level for an interrupt like this

NVIC_SetPriority(UART3_IRQn,5);

This will assign UART3 IRQ to priority group 1 ( see the table in the figure above ) and give it a sub priority level of 1 within the group.


PS:

If you are trying out making changes in the code, the latest Quick Startup code can be found here. Use CodeSourcery g++ Lite with Eclipse.
If you are not using NXP secondary USB bootloader, then you should correspondingly change the start flash address in the LPC17xx.ld linker file. If you are not using any bootloader, set the flash start to 0x00000000(I've done this in the startup code for programming via JTAG ). If you are using the bootloader presented here, set the flash start to 0x00002000 ( I've done this already ). If you are using some other bootloader, set the flash start accordingly. Finally,if u are using the USB bootloader drag and drop the .bin file to the USB drive ( "CRP Disable" ) that appears. Not the ELF or HEX file.

UPDATE:

If you dont want non-preemptive priority levels (sub priority levels), then you can leave the decimal point at its default value (0) and just use NVIC_SetPriority. For instance , suppose we needed TIMER0_IRQ to pre-empt UART3_IRQ , then , this would work fine. This way there are 32 pre-emptive priority levels with 0 being the highest and 31 being the lowest.


NVIC_SetPriority(TIMER0_IRQn,3);
NVIC_SetPriority(UART3_IRQn,4); //level here has to be > level above since UART3_IRQ can be pre-empted by TIMER0_IRQ.

15 comments:

  1. Nice job
    I had many problems about interrupt but your code solved all of them
    but On reset, Timer0/1 are enabled so first line it is extra and it is the same for gpio
    watch page 63 of user manual
    thanks again for your explanation

    ReplyDelete
  2. The extra line which powers up the peripheral is worth it. I had spent over an hour trying to figure out why a certain peripheral wasn't working only to find that in system_LPC17xx.c , the default value of LPC_SC->PCONP is modified upon reset before control is transferred to main(). Instead of opening up system_LPC17xx.c every time a peripheral is used, I found it easier to power up the peripheral any way. This also makes your code more portable since it makes fewer assumptions about pre-existing conditions.

    However, I have to agree with you when it comes to GPIO. I can hardly think of a situation where GPIO is powered off by default.

    ReplyDelete
  3. Your tutorials have been extremely useful for helping me get started! I managed to get my LED blinking by using the 'for loop accessing a volatile' method, but I can't seem to get any of the interrupts working!

    I tried using your timer interrupt code (doesn't work), modified it such that if it gets to the interrupt handler at all, the LED will light up (doesn't work), and also tried using an unconfigured SysTick_Handler to switch the LED on during the interrupt (doesn't work).

    Do you think you could give me some hints to fix this? Thanks!

    ReplyDelete
  4. Looks like interrupts aren't working at all. When I wrote this tutorial, I was using C only. Now, my codebase is configured to work with C++. One of the sad things is that this transit can break interrupts because of C++ name mangling.

    Try using

    extern "C" void TIMER0_IRQHandler()
    {
    // LED code
    }

    If you're still having trouble, try this example. I tested it on the Hardware and it works. So, it can serve as a reference point for you.

    http://cortex-m3-tutorials.googlecode.com/files/TIMER0_interrupt_blinky.rar

    ReplyDelete
  5. Interesting! It works now! Thanks!

    I was using C++ as well and I have never encountered such a problem. Does this mean it'll be easier to simply program completely in C?

    ReplyDelete
  6. By the way, have you tried using SysTick? I used the same extern "c" trick with SysTick_Handler() and it got a little weird. I made it turn the LED on if it ever got to the handler (works). I made it increment an "uint32_t a = 0;" everytime it got there, and in the main while loop, turn the LED on if a > 0 (doesn't work).

    I checked out the user manual for system tick timer, but I can't figure out how to configure the registers (can't find appropriate structs/items in LPC17xx.h or the Core files).

    ReplyDelete
  7. The object oriented programming features of C++ will come in handy at some point. My advice is to use C++.

    Adding extern "C" for interrupt handlers is probably the only difference you'll notice as far as C is concerned and this is hardly a big deal.

    You may face ticklish issues interfacing C and C++ but they aren't too hard to deal with.

    I've not used sysTickHandler. You may find this useful.

    http://embeddedfreak.wordpress.com/2009/08/10/cortex-m3-systick-peripheral/

    ReplyDelete
  8. Did the LED remain permanently on ?
    Perhaps you should declare "a" as int.

    ReplyDelete
  9. Thanks! I just discovered the search workspace option in Eclipse and found the SysTick registers hiding somewhere. I'll fool around with it in a bit and see if I can get it to work.

    Thanks again!

    ReplyDelete
  10. can you provide such code for fujitsu MB9BF506R cortex M3 Microcontroller

    ReplyDelete
  11. I searched for NVIC_ functions however couldn't find its meaning.
    Could you please tell where they are defined?
    Thanks in advance.

    ReplyDelete
  12. Look in the CMSIS documentation

    ReplyDelete
  13. Is there any possibility an ISR in a lower priority group cannot preempt a running ISR which is in a higher priority group?

    ReplyDelete
  14. I tried the example...couldn't get it to work 8~(
    The pin doesn't toggle whatsoever. I added the line above that says "Caveat:
    1. Bit 1 of LPC_TIM0->TCR resets the timer and holds it there even if you start the timer. Make sure to clear it..."
    Still no go.
    I'm using C++, so I wrapped the extern C declaration around the IRQ handler, still no-go.
    Not sure what's left to try...any ideas?

    I do have one question. Can I toggle ANY pin? The example uses P1.29. Can I use P0.1, for example?

    Thanks for any help, GR


    ReplyDelete
  15. problem with pwm interrupt

    I am using LPC 1768 board....I wrote a program for creating interrupt when counter matches with match 0 register.....but its not entering into interrupt program....led3 is not toggling...please help...attached c prog

    #include "mbed.h"

    void PWMInit();

    PwmOut chA(p23);
    DigitalOut chB(LED3);

    int main (void)
    {
    PWMInit();

    while(1) {


    LPC_PWM1->MR3 = 6000;
    LPC_PWM1->MR4 = 15000;
    LPC_PWM1->LER |= (1<<3) | (1<<4) ;




    }

    }


    void PWM1_IRQHandler (void)
    {
    if((LPC_PWM1->IR & 0x01) == 0x01) // Check whether it is an interrupt from match 0
    {

    chB != chB;
    wait(.2);

    LPC_PWM1->IR |= 1<<0; // Clear the interrupt flag
    }



    return;
    }

    void PWMInit()
    {
    LPC_SC->PCONP |= 1 << 6; // enable power for PWM1
    LPC_SC->PCLKSEL0 |= 1 << 12; // PCLK_PWM1 = CCLK

    LPC_PWM1->MR0 = 16000; // PWM freq = CCLK/16000

    LPC_PWM1->MCR |= 1 << 0; // interrupt on Match0

    LPC_PINCON->PINSEL3 |= (2 << 14 ); // P2.4 works as PWM1 output
    LPC_PINCON->PINMODE3 |= 2 << 14; // enable neither pull up nor pull down


    LPC_PWM1->PCR |= 1 << 4; // Configure channel 4 as double edge controlled PWM
    LPC_PWM1->PCR |= 1 << 12; // Enable PWM channel 4 output

    LPC_PWM1->MR0 = 16000; // PWM freq = CCLK/16000

    LPC_PWM1->TCR = (1 << 0) | (1 << 3); // enable timer counter and PWM



    NVIC_EnableIRQ(PWM1_IRQn);
    return;
    }

    ReplyDelete