ESE101: Interrupts Blink A Light

Three steps are required to use an interrupt:

  1. Configure and enable the interrupt to happen when you want it to
  2. Write the Interrupt Service Routine (ISR) to actually handle the interrupt when it happens
  3. Configure the ISR

Step 1: Configure and Enable the Interrupt

Last week, we did Step 1. We configured and enabled the GPIO input interrupt by:

  • Configuring a GPIO as a high-to-low interrupt pin using the P1IES register
  • Enabling interrupts on that GPIO input pin using the P1IFG and P1IE registers
  • Enabling system-wide interrupts using the SR register’s GIE bit

That gave us a working interrupt. Next we need to write the ISR to do something useful when the interrupt happens.

Step 2: Write the ISR

For the most part, an ISR is just like any other code you write. The main restrictions for an ISR are:

  • An ISR needs to be short and run fast so it doesn’t delay other interrupts that might happen
  • An ISR must end with the RETI instruction

We’ll write our simple ISR to toggle the LED each time it’s called:

PORT1_ISR:
           ; Clear the interrupt flag.
           BIC #2, &P1IFG

           ; Toggle GPIO P1.0
           XOR.B      #1, &P1OUT

           RETI

The ISR starts with a label, PORT1_ISR. This can be any name you want, but it’s a good idea to make it clear that this code is an ISR that handles the PORT1 interrupt, so I chose PORT1_ISR.

Our ISR has three instructions:

  1. BIC clears the P1IFG interrupt flag, which was automatically set by hardware when the P1 interrupt triggered. If you don’t clear P1IFG (set it to 0) then the interrupt will keep firing forever
  2. XOR toggles the P1.0 pin which turns the LED on or off each time the ISR runs
  3. RETI is the special “return from interrupt” instruction which must be the last instruction an ISR runs

Now we have a simple ISR that will turn the light on and off when it runs. The next step is to configure the system so PORT1_ISR gets called when the interrupt happens.

Step 3: Configure the ISR

Each interrupt has its own dedicated (hardcoded) memory address called an interrupt vector address, or just interrupt vector. The interrupt vector contains the start address of the ISR.

When an interrupt occurs, the microcontroller:

  1. Finishes the instruction it’s currently running
  2. Reads the value in memory at the interrupt vector for the interrupt that occurred
  3. Sets the PC register equal to the value in memory at the interrupt vector: the PC is now pointing to the first instruction in the ISR
  4. The next instruction that runs will be the first instruction in the ISR
  5. The ISR runs until the RETI instruction
    • The RETI instruction changes the PC to wherever the microcontroller was before the interrupt happened, and so the microcontroller picks up where it left off before the interrupt

You can usually find a list of interrupts and the interrupt vector addresses in a microcontroller’s programming reference or the datasheet. TI puts it in their datasheet section under “Interrupt Vector Addresses” for the MSP430F5529: here’s an online version, and here’s a pdf.

Here’s the list of the MSP430F5529 interrupts:

Each row in the table shows describes one interrupt; I’ve circled the row for the GPIO Port P1 interrupt. The last column shows the interrupt priority. TI will use this number, “47”, to help us place the ISR; we’ll see that later in this post. The second to last column shows the address of the interrupt vector address. For GPIO Port P1, the interrupt vector address is 0xFFDE.

This means that when an interrupt happens for GPIO Port P1, the microcontroller reads the value at address 0xFFDE, let’s say it’s 0x1234. This tells us that the GPIO Port P1 interrupt’s ISR starts at address 0x1234. Then the microcontroller sets PC = 0x1234, and the code starting at address 0x1234 starts running.

This might be a bit confusing, so let me draw it out:

Here we can see that the interrupt vector at 0xFFDE has the start address of the GPIO P1 ISR, which is 0x1234 in this simple example. You can think of the interrupt vector as a pointer to the actual ISR (in fact, that’s exactly what it is). I’ve put a special label next to the interrupt vector called “.int47”; we’ll see how this is used later.

Now that we understand the mechanics of how an ISR starts running, how do we get our PORT1_ISR to run when the GPIO P1 interrupt happens? We want the address of our PORT1_ISR code to be at the GPIO P1 interrupt vector - how do we make that happen?

TI’s Code Composer Studio lets us configure interrupt vectors using some special syntax. The last two lines here do what we want:

;-------------------------------------------------------------------------------
; Interrupt Vectors
;-------------------------------------------------------------------------------
           .sect   ".reset"                ; MSP430 RESET Vector
           .short  RESET
           .sect   ".int47"   ; added this line
           .short  PORT1_ISR  ; added this line

What is this “.sect” and “.short” business?

The ‘.sect “.int47”’ is TI’s way of saying “I’m about to tell you what to put in memory in a section named .int47,” and ‘.short PORT1_ISR’ says “put the address of the label PORT1_ISR at whatever memory address the current section is at.”

A section is a part of the compiled and linked program; some sections have special names with special meanings, like .int47. Remember the interrupt table from earlier in this post? It said that the GPIO Port P1 interrupt has interrupt priority 47, and TI uses this number to help us place the ISR address at the interrupt’s vector address. TI defines a special section called .int47 that it automatically puts at the GPIO Port P1 interrupt vector address, 0xFFDE.

So these two lines:

           .sect   ".int47"   ; added this line
           .short  PORT1_ISR  ; added this line

put the address of PORT1_ISR in the GPIO Port P1 interrupt vector. Now when that interrupt happens, the microcontroller will automatically start running the PORT1_ISR code. The memory layout looks like this:

Putting It All Together

Here’s the complete code for our example:

;-------------------------------------------------------------------------------
; Main loop here
;-------------------------------------------------------------------------------

           ; Set GPIO P1.0 to be an output (P1DIR bit 0 == 1)
           BIS.B      #1, &P1DIR
           ; Set GPIO P1.1 to be an input (P1DIR bit 1 == 0)
           BIC.B      #2, &P1DIR
           ; Set GPIO P1.1 to be pulled up or down (P1REN bit 1 == 1)
           BIS.B      #2, &P1REN
           ; Set GPIO P1.1 as a pull-up resistor (P1OUT bit 1 == 1)
           BIS.B      #2, &P1OUT

           ; Set interrupt on high-to-low transition of P1.1
           BIS.B      #2, &P1IES
           ; Clear any interrupts that happened when changing P1IES
           BIC.B      #2, &P1IFG
           ; Enable P1.1 interrupts
           BIS.B      #2, &P1IE

           ; Enable interrupts
           NOP ; the user’s guide recommends a NOP before setting #GIE
           BIS #GIE, SR
           NOP ; the user’s guide recommends a NOP after setting #GIE

MainLoop:  ; infinite loop that does nothing

           JMP MainLoop
           NOP

PORT1_ISR:
           ; Clear the interrupt flag.
           BIC #2, &P1IFG

           ; Toggle GPIO P1.0
           XOR.B      #1, &P1OUT

           RETI

;-------------------------------------------------------------------------------
; Stack Pointer definition
;-------------------------------------------------------------------------------
            .global __STACK_END  ; This code was already here from TI.
            .sect   .stack       ; It is not new code.
            
;-------------------------------------------------------------------------------
; Interrupt Vectors
;-------------------------------------------------------------------------------
           .sect   ".reset"                ; MSP430 RESET Vector
           .short  RESET
           .sect   ".int47"   ; We added these two lines
           .short  PORT1_ISR  ; for configuring our GPIO Port P1 ISR.

Note: We added a NOP instruction before and after the “BIS #GIE, SR” instruction because CCS warned me that the NOPs were needed. This is another example of what I said last time:

“Don’t just skim interrupt documentation, since it usually contains dangerous gotchas that are too easy to overlook.”

The TI docs mention that sometimes a NOP is needed before an after changing the GIE bit (in the programmer’s reference manual section 1.3.4.1). I still think I don’t technically need the NOPs with this usage, but better safe than sorry - the NOPs have no downside except a one-time tiny delay when they run.

Download that code to your MSP430 LaunchPad and run it. The code should stay in the MainLoop, doing nothing, until you press the P1.1 button. Each time you press the button the ISR should run, toggling the P1.0 LED once.

Does it work for you? Do you notice anything strange? Does the LED blink on/off more than once per button-push?

It will usually toggle the LED once, but - surprise! - sometime it will toggle multiple times! What’s going on here? The code is correct, I promise you - physics is to blame!

You’re seeing the button bouncing - each time you think you’re pressing and releasing the button exactly once, the button’s contacts may be connected/disconnected several times, extremely quickly. The GPIO P1 interrupt will fire each time the button connection is made, which might happen multiple times when you’ve only pushed the button once.

Embedded systems guru Jack Ganssle sums it up well:

“When the contacts of any mechanical switch bang together they rebound a bit before settling, causing bounce. Debouncing, of course, is the process of removing the bounces, of converting the brutish realities of the analog world into pristine ones and zeros. Both hardware and software solutions exist, though by far the most common are those done in a snippet of code.”

You can get rid of the extra ‘bounces’ by debouncing the button so we get exactly one LED toggle per button push. Read Jack’s post above for a very in-depth look at debouncing. (And while there read any of his other articles - they're always good!)

We’ll come back to debouncing in a later post, but next time I’ll introduce a new peripheral called a timer. I tried to introduce timers a while ago and failed because you needed to understand interrupts before timers made sense, and you needed to understand GPIO inputs as an easy way to learn about interrupts. Now that I’ve taught you about GPIO inputs and interrupts we’re (finally!) ready for timers! See you next time!

The original interrupting cow picture is by Jeroen BenninkCC BY 2.0. The photoshopping is by me.