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:
#pragma interrupt isr
void isr()
{
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:
[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
’.
[interrupt event] -> high_remap() -> interrupt_jump() -> isr()
pbldr
uses this code to handle the interrupt remapping:
#define REMAP_HIGH_INTERRUPT 0x808
#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:
#pragma interrupt isr_high
void isr_high()
{
char state = LATBbits.LATB0;
INTCONbits.TMR0IF = 0;
if (state == 0)
PORTBbits.RB0 = 1;
else
PORTBbits.RB0 = 0;
}
#pragma code high_vector=0x808
void high_int()
{
_asm goto isr_high _endasm
}
#pragma code main=0x820
void main()
{
TRISBbits.TRISB0 = 0; T0CONbits.T08BIT = 0; T0CONbits.T0CS = 0; T0CONbits.T0PS = 255; T0CONbits.TMR0ON =1; INTCONbits.GIE = 1; INTCONbits.TMR0IF = 0; INTCONbits.TMR0IE = 1;
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.