Microchip Vectored Interrupts

Few months ago Microchip presented new 8 bit PIC microcontroller family – “K42”. Thanks to Microchip I have become owner of few new eval boards from that family – MPLAB Xpress PIC18F25K42.

Microcontrollers from that family are equipped with a lot of Microchips useful peripherals like:

  • ADC with Computation (ADCC)
  • Configurable Logic Cells (CLC)
  • Complementary Waveform Generators (CWG)
  • Numerically Controlled Oscillators (NCO)
  • Data Signal Modulator (DSM)
  • … and a lot more

I already tested some of those peripherals on other microcontrollers (NCOADCC). I even make one test on “K42” eval board (CLC).

This time I want to show you one of really new things in 8 bit PIC microcontrollers that were introduced in “K42” family: Vectored Interrupts. If you are familiar with previous PIC microcontrollers, then you know that have just one Interrupt Service Routine (ISR or interrupt handler) that handles all of interrupts. It was programmers responsibility to check what caused the execution of ISR and then react on that cause.

Using Vectored Interrupts you are writing different interrupt handlers for each of interrupt. The microcontrollers hardware is executing proper handler based on interrupt source and the interrupt vector table.

In this article I would like to compare both methods of interrupt handling. Good thing is that in PIC18F25K42 microcontroller we can choose which way the interrupts will be handled – the “old way” – single interrupt handler or by Vectored Interrupt Controller. To make it easier, I will make most of configurations in MCC (MPLAB Code Configurator).

Single interrupt handler.

First let’s try to write program that uses interrupts in single handler mode. Actually there can be two interrupt handlers – one for low and one for high priority. Our program will run one timer and put output of timer to one of microcontrollers pins. That timer will be also source of interrupt, which causes the other pin to go low. Connecting oscilloscope probe to both pins we’ll see how fast the interrupt handler is executed.

The configuration of our microcontroller (from MCC) is showed below:

Our microcontroller will work from internal high frequency oscillator (64 MHz).

We are using high frequency internal oscillator as clock source to timer 0. Interrupt from timer 0 is enabled, and period of timer 0 is set to about 20 ms.

We’ll use pin C.0 (pin 0 of port C) as output of timer 0. The timer will toggle that output with its period, so there will be rectangular waveform with period of about 40 ms (two toggles – from 0 to 1 and from 1 to 0). We also set the pin A.3 as output – we’ll set it to 1 in main loop, and we’ll reset it to 0 at interrupt from timer 0.

Now let’s take a look at generated code (or at least some fragments of generated code). I already filled in main function (the green color) and removed some comments. Global interrupts are enabled as suggested in Interrupt Module (in MCC window in above image).

main.c

(...)

void main(void)
{
    SYSTEM_Initialize();

    INTERRUPT_GlobalInterruptEnable();

    SLRCONA = 0x00;
    SLRCONC = 0x00;

    while (1)
    {
        PORTA = 0x04;
    }
}

(...)

Slew rate of port A and C (all pins) is set to high. It would be enough to set high slew rate of just pin A.3 and C.0, but other pins are not used so it doesn’t change anything.

interrupt_manager.c

(...)

void  INTERRUPT_Initialize (void)
{
    // Disable Interrupt Priority Vectors (16CXXX Compatibility Mode)
    INTCON0bits.IPEN = 0;
}

void interrupt INTERRUPT_InterruptManager (void)
{
    // interrupt handler
    if(PIE3bits.TMR0IE == 1 && PIR3bits.TMR0IF == 1)
    {
        TMR0_ISR();
    }
}

(...)

The first function is interrupt initializer. There is just disabling Interrupt Priority Vectors (we won’t talk about interrupt priorities in this article).

The second function is our interrupt handler. If timer 0 interrupts are enabled (PIE3bits.TMR0IE == 1) and also if the timer 0 caused the interrupt (PIR3bits.TMR0IF == 1) then the function TMR0_ISR() is executed. Definition of that function is in the file tmr0.c.

tmr0.c

(...)

void (*TMR0_InterruptHandler)(void);

void TMR0_Initialize(void)
{
    (...)
    TMR0_SetInterruptHandler(TMR0_DefaultInterruptHandler);
    (...)
}

(...)

void TMR0_ISR(void)
{
    PIR3bits.TMR0IF = 0;
    if(TMR0_InterruptHandler)
    {
        TMR0_InterruptHandler();
    }
}

void TMR0_SetInterruptHandler(void (* InterruptHandler)(void)){
    TMR0_InterruptHandler = InterruptHandler;
}

void TMR0_DefaultInterruptHandler(void){
    PORTA = 0x00;
}

First there is defined pointer to function which will be our timer interrupt function.

void (*TMR0_InterruptHandler)(void);

In timer initialization function, that pointer is set to TMR0_DefaultInterruptHandler function.

TMR0_SetInterruptHandler(TMR0_DefaultInterruptHandler);

Below there is our TMR0_ISR function (that we mentioned before in interrupt_menager.c file). It will be executed when timer 0 causes interrupt.

void TMR0_ISR(void)
{
    PIR3bits.TMR0IF = 0;
    if(TMR0_InterruptHandler)
    {
        TMR0_InterruptHandler();
    }
}

That function first clears the interrupt flag. It’s necesarry, because otherwise the interrupt will be executed continuously without even getting out to main loop. Then there is check if the function pointer TMR0_InterruptHandler pointing to some function, and if it’s not null then that function is executed. In our case it’s TMR0_DefaultInterruptHandler. We could use TMR0_SetInterruptHandler to set that pointer to other function, but it will be easier to write our code just in default function.

In TMR0_DefualtInterruptHandler, we are just resetting all pins of A port to 0.

void TMR0_DefaultInterruptHandler(void){
    PORTA = 0x00;
}

And that’s it. Now let’s check how fast the pin A.3 is reset to 0 after TIM0 changes the state of pin C.0. We’ve connected yellow oscilloscope probe to pin C.0 and blue to A.3.

The delay is about 2.2 μs. But if you analyse our code, there’s a lot of places that can be optimized. We could make all optimization at once, but I decided to make it step by step, just to check how specified changes influences our delay. First of all…

void TMR0_ISR(void)
{
    PIR3bits.TMR0IF = 0;
    if(TMR0_InterruptHandler)
    {
        TMR0_InterruptHandler();
    }
}

Here we are checking if the TMR0_InterruptHandler is pointing to some address (not pointing to 0). Is it really necessary? During initialization we are assigning TMR0_InterruptHandler to our TMR0_DefaultInterruptHandler so it will never be unassigned again. Well… I really like that kind of “idiot-proof” checks. It saves my life few times. But for now, let’s assume that we’re not idiots and we won’t change our TMR0_InterruptHandler, so we don’t need to check it.

void TMR0_ISR(void)
{
    PIR3bits.TMR0IF = 0;
    TMR0_InterruptHandler();
}

Just by removing that check, we have reduced delay by about 200 ns. Can we reduce it more? Of course! Right now we have execution of function pointer. Let’s check if executing our TIM0_DefaultInterruptHandler directly will make that delay shorter.

void TMR0_ISR(void)
{
    PIR3bits.TMR0IF = 0;
    TMR0_DefaultInterruptHandler();
}

We have reduced our delay again by about 700 ns. Ok, but now, we are still having execution of another function inside TMR0_ISR. To reduce time we can just put our TMR0_DefaultInterruptHandler code inside TMR0_ISR function.

void TMR0_ISR(void)
{
    PIR3bits.TMR0IF = 0;
    PORTA = 0x00;
}

Now we have about 120 ns delay reduction. Another thing we can do if the time delay of interrupt is most critical property. If the interrupt occurs, let’s first execute our time critical line, and then clear the interrupt flag.

void TMR0_ISR(void)
{
    PORTA = 0x00;
    PIR3bits.TMR0IF = 0;
}

Again we have saved about 120 ns. Now we can resign from executing our TMR0_ISR function and again put the code inside our main interrupt handler – INTERRUPT_InterruptManager (which is in interrupt_manager.c file).

void interrupt INTERRUPT_InterruptManager (void)
{
    if(PIE3bits.TMR0IE == 1 && PIR3bits.TMR0IF == 1)
    {
        PORTA = 0x00;
        PIR3bits.TMR0IF = 0;
    }
}

Another 120 ns saved. Now we can make small assumption. If in our system we are using just one interrupt, then we don’t need to check interrupt source. Let’s make that test.

void interrupt INTERRUPT_InterruptManager (void)
{
    PORTA = 0x00;
    PIR3bits.TMR0IF = 0;
}

Now the reaction time is about 550 ns. That’s probably the shortest reaction delay that we can achieve. But in real systems there will be probably few interrupts. Let’s leave previous INTERRUPT_InterruptManager function, but with removed check if TMR0 interrupt is enabled… let’s assume that it’s enabled all the time.

void interrupt INTERRUPT_InterruptManager (void)
{
    if(PIR3bits.TMR0IF == 1)
    {
        PORTA = 0x00;
        PIR3bits.TMR0IF = 0;
    }
}

Now let’s add some other timers and interrupts to our system. We have added three timers: TMR1, TMR3, TMR5. All have quite similar configuration. I’ve configured different timer periods, but in that test it’s not important. Important thing is that all timers have enabled interrupts.

Now our INTERRUPT_InterruptManager function looks like that (again I removed checks if specified interrupt is enabled):

void interrupt INTERRUPT_InterruptManager (void)
{
    if(PIR3bits.TMR0IF == 1)
    {
        PORTA = 0x00;
        PIR3bits.TMR0IF = 0;
    }
    if(PIR8bits.TMR5IF == 1)
    {
        TMR5_ISR();
    }
    if(PIR6bits.TMR3IF == 1)
    {
        TMR3_ISR();
    }
    if(PIR4bits.TMR1IF == 1)
    {
        TMR1_ISR();
    }
}

Now you can see that reaction delay is the same, but the microcontroller exits from interrupt handler later (the low to high transition is made in main loop). That’s quite obvious, because inside interrupt handler there are another checks of interrupt source.

Now let’s change order of our checks in interrupt handler.

void interrupt INTERRUPT_InterruptManager (void)
{
    if(PIR8bits.TMR5IF == 1)
    {
        TMR5_ISR();
    }
    if(PIR3bits.TMR0IF == 1)
    {
        PORTA = 0x00;
        PIR3bits.TMR0IF = 0;
    }
    if(PIR6bits.TMR3IF == 1)
    {
        TMR3_ISR();
    }
    if(PIR4bits.TMR1IF == 1)
    {
        TMR1_ISR();
    }
}

Of course there will be some other delay, because before microcontroller executes the line PORTA = 0x00 it will make some other checks.

The delay increased by about 250 ns. Now let’s change order even more.

void interrupt INTERRUPT_InterruptManager (void)
{
    if(PIR8bits.TMR5IF == 1)
    {
        TMR5_ISR();
    }
    if(PIR6bits.TMR3IF == 1)
    {
        TMR3_ISR();
    }
    if(PIR3bits.TMR0IF == 1)
    {
        PORTA = 0x00;
        PIR3bits.TMR0IF = 0;
    }
    if(PIR4bits.TMR1IF == 1)
    {
        TMR1_ISR();
    }
}

As you can see, the the delay of interrupt reaction depends on order of interrupt checks inside interrupt handler. And it changes significantly (about 250 ns). All used interrupts will have different reaction delays.

Now we can check how it will work, if we use Vectored Interrupts.

Vectored Interrupts

After regenerating code from MCC, we have to change a bit code of main.c file:

void main(void)
{
    SYSTEM_Initialize();

    INTERRUPT_GlobalInterruptLowEnable();
    INTERRUPT_GlobalInterruptHighEnable();
    INTERRUPT_GlobalInterruptEnable();

    SLRCONA = 0x00;
    SLRCONC = 0x00;

    while (1)
    {
        PORTA = 0x08;
    }
}

Now we are using just low priority interrupts, but we’ll probably use high priority later, so I have enabled it.

Let’s take a look at file tmr0.c:

(...)

void (*TMR0_InterruptHandler)(void);

void TMR0_Initialize(void)
{   
    (...)
    TMR0_SetInterruptHandler(TMR0_DefaultInterruptHandler);
    (...)
}

(...)

void __interrupt(irq(IRQ_TMR0),base(IVT1_BASE_ADDRESS),low_priority) TMR0_ISR()
{
    PIR3bits.TMR0IF = 0;
    if(TMR0_InterruptHandler)
    {
        TMR0_InterruptHandler();
    }
}

(...)

void TMR0_SetInterruptHandler(void (* InterruptHandler)(void)){
    TMR0_InterruptHandler = InterruptHandler;
}

void TMR0_DefaultInterruptHandler(void){
    PORTA = 0x00;
}

Code architecture is quite similar to that “single interrupt handler” mode. We have pointer TMR0_InterruptHandler which points to TMR0_DefaultInterruptHandler (after initialization).

This time we don’t have interrupt handler in interrupt_manager.c file. There is few interrupt handlers in peripheral files.

In tmr0.c file:

void __interrupt(irq(IRQ_TMR0),base(IVT1_BASE_ADDRESS),low_priority) TMR0_ISR()

In tmr1.c file:

void __interrupt(irq(IRQ_TMR1),base(IVT1_BASE_ADDRESS),low_priority) TMR1_ISR()

etc.

The interrupt of our TMR0 handler, checks (if not null) and executes the TMR0_InterruptHandler (which points to TMR0_DefaultInterruptHandler). TMR0_DefaultInterruptHandler is clearing port A. As we checked before, that’s not the fastest way, but anyway let’s check how fast it is now.

It’s quite faster than first example with single interrupt handler (first example in this article), but not too much.

Now we’ll make the same optimisations as we made before. We’ll put our code right in interrupt handler, and see how it works. Now we don’t have to check the source of interrupt, because our handler will be executed just by interrupt of TMR0.

void __interrupt(irq(IRQ_TMR0),base(IVT1_BASE_ADDRESS),low_priority) TMR0_ISR()
{
    PORTA = 0x00;
    PIR3bits.TMR0IF = 0;
}

Now there’s first interesting thing. In single interrupt handler mode, in case without check of interrupt source the result was actually faster (it was about 550 ns). In Vectored Interrupts we have slower reaction (about 130 ns) but inside interrupt handler we know that it was TMR0 interrupt. So if we want to compare times with the same functionality, we should compare that time to case with interrupt source check (it was 744 ns), so with checking interrupt source Vectored Interrupts are faster (there is hardware check of interrupt source).

Now we should check if there is difference when we change the order of interrupts… but there is no order in Vectored Interrupts. We can change priorities of specified interrupts. I have changed all priorities of other timers to 1 (timer 0 interrupt have lower priority). In that case the reaction time was the same (of course when there’s no two interrupts in the same time). So in Vectored Interrupts mode interrupt reaction time is the same, no matter how much interrupts we are using.

Conclusion

The Vectored Interrupts featured in “K42” family of PIC microcontrollers changes the way of handling interrupts. There are specified interrupt handlers for each peripheral.

The advantage of new way of interrupt handling is that reaction time is the same for all interrupts, no matter how much interrupts we are using. In older way, the reaction time differs when we change order of our interrupts in interrupt handler. In Vectored Interrupts there’s no need to check interrupt source inside handler (there’s specified handler for each interrupt source).

The old way of interrupt handling will be actually faster in one case – if we have just one interrupt or we have few interrupts that executing the same code – in case where we don’t have to check the source of interrupt.

The code generated by MCC is quite clear but not the fastest. If we want to make our interrupts works as fast as they can, we have to make some optimizations inside generated code.