Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C++

Writing a Bootloader – Part 2: Interrupts

5.00/5 (5 votes)
2 May 2012CPOL4 min read 19.7K  
Interrupts and how to implement them in a bootloader

In part 1, we went through a very simple bootloader and explained how it worked. I also mentioned some of the limitations. One of these was the lack of support for interrupts. Let's talk about interrupts and how to implement them in a bootloader. You might also want to see this article which gives a background of how code executes on a microcontroller.

What are Interrupts?

When a processor starts, it begins executing code at some specific location. It then executes code in order, following loops, branches, and function calls. Interrupts allow you to interrupt the normal flow of execution. Interrupts are configured to be triggered by external events, such as a button press, receiving a serial message, or finishing an analogue to digital conversion.

When one of these events happens, the program will automatically jump to a specified location called an interrupt vector. You put code at this location that services the interrupt. This is called an interrupt service routine (ISR). Now when the external event happens, your ISR runs and handles it. This is especially useful for building real-time systems that need to respond quickly to external events.

Vectors

An interrupt vector is a specific memory address where the processor jumps when an interrupt occurs. There is usually a very small amount of space for code at the vector, so the vector is used to jump to the actual ISR.

Processors can have one vector that handles all of the interrupts (single vector) or a vector for each type of interrupt (multi-vector). For a single vector system, you specify one ISR for all interrupts that must check what event caused the interrupt. In a multi-vector system, you can specify a different ISR for each interrupt event.

An Example

Let's look at the PIC18F microcontroller. According to the datasheet, it has two interrupt vectors: one for high priority interrupts at 0×08 and one for low priority interrupts at 0×18. For now, we’ll ignore the low priority interrupt (it’s disabled by default).

So at the memory address 0×08, we need to put a jump to our ISR. For the C18 compiler, this is done in C by using the #pragma code directive. Here’s an example:

C++
#pragma interrupt isr
void isr()
{
        /* check which interrupt happened*/
        /* clear the interrupt flag */
        /* handle that interrupt */
        return;
}
#pragma code high_vector=0x08
void interrupt_jump()
{
	_asm goto isr _endasm
}

This code first defines the ISR function called ‘isr’. It uses a #pragma interrupt directive to tell the compiler that the ISR function is an interrupt. The ISR would check various flags to figure out what the actual cause of the interrupt was, clear its flag, do something to respond to the interrupt, and then return. Clearing the interrupt flag is important, if you do not clear it, the interrupt will just keep running until it is cleared.

The #pragma code high_vector=0×08 directive tells the linker to put ‘interrupt_jump’ at memory location 0×08, which is the memory location of the interrupt vector. When an interrupt happens, the program counter will be loaded with 0×08, and ‘interrupt_jump’ function will run. This function just calls the isr function.

Bootloader Interrupt Remapping

So how do we use interrupts with a bootloader? Allowing the user to write to the interrupt vector at 0×08 would be a bad idea, since this is where the bootloader lives. If the user overwrites the bootloader, they will brick the device. So we remap the vectors to some other location.

In the last example (without a bootloader), the execution looked like this:

C++
[interrupt event] -> interrupt_jump() -> isr()

Now we’ll add code in the bootloader at the interrupt vector that jumps to another known memory location. We’ll put ‘interrupt_jump’ at that location, and have it call ‘isr’.

C++
[interrupt event] -> high_remap() -> interrupt_jump() -> isr()

pbldr uses this code to handle the interrupt remapping:

C++
#define REMAP_HIGH_INTERRUPT 0x808
 
// interrupt remapping
#pragma code high_vector=0x08
void high_remap()
{
	_asm goto REMAP_HIGH_INTERRUPT _endasm
}

Now the program can use 0×808 instead of 0×08 as its interrupt vector. Here’s a complete example that uses the timer 0 interrupt to blink an LED:

C++
// interrupt service routine
#pragma interrupt isr_high
void isr_high()
{
	char state = LATBbits.LATB0;
	INTCONbits.TMR0IF = 0;  // clear the timer flag
 
        // toggle led
	if (state == 0)
		PORTBbits.RB0 = 1;
	else
		PORTBbits.RB0 = 0;	
}
 
// interrupt vector
#pragma code high_vector=0x808
void high_int()
{
	_asm goto isr_high _endasm
}
 
// run this code after bootloader
#pragma code main=0x820            
void main()
{
    TRISBbits.TRISB0 = 0;    // make led pin an output      
    T0CONbits.T08BIT = 0;    // use 16 bit timer
    T0CONbits.T0CS = 0;      // use instruction cycle clock
    T0CONbits.T0PS = 255;    // timer 0 prescaler
    T0CONbits.TMR0ON =1;     // timer 0 enable
    INTCONbits.GIE = 1;      // global interrupt enable
    INTCONbits.TMR0IF = 0;   // clear timer 0 interrupt flag
    INTCONbits.TMR0IE = 1;   // enable the timer 0 interrupt
 
    // loop forever
    for (;;);
}

This code loops forever, but when the timer interrupt occurs, it will run ‘high_isr’ which toggles the LED output pin. When loaded using pbldr, the LED blinks! The memory location 0×808 is used for the interrupt vector, and the location 0×820 is where execution will start after the bootloader is complete.

Next up, making the flashing process a bit more robust and support for loading code over Controller Area Network.

Writing a Bootloader – Part 2: Interrupts" -> Original post.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)