03 August 2010

Tutorial 10-b: Interrupt Examples

GPIO Interrupt Example
The first example we'll do uses the Port 1 interrupts; this code is easily changed for any port number used in your particular device.  This code will show how to do the DCO example we did in Tutorial  08-b, which demonstrates changing the DCO, using interrupts.

First, we need to configure the port to use interrupts.  The LaunchPad button is on P1.3, so all of our concern will be on that pin.  The pin needs to be configured as an input.  Since the default state of the pin is logic 1, a high-low transition (a drop from 1 to 0) should be used to signal the interrupt.  We can code this using:
P1IES |= BIT3;   // high -> low is selected with IES.x = 1.
P1IFG &= ~BIT3;  // To prevent an immediate interrupt, clear the flag for
                 // P1.3 before enabling the interrupt.
P1IE |= BIT3;    // Enable interrupts for P1.3

We've enabled the interrupt for P1 now, and a high-low transition caused by the button press will set the interrupt flag in P1IFG.  The processor, however, isn't set to recognize maskable interrupts like P1IFG.  We can turn on the interrupts with:
_BIS_SR(GIE);

or equivalently:
_enable_interrupt();  // Note the singular name here!  It's not interrupts.

The first method parallels the bis.w instruction in assembly used for this command, and can be used to configure the SR in one step with other options, such as low power modes.  The second method is a little more human readable, and works well enough if you don't need to enable any LPM's.

Next we need to write the ISR.  Something like this will work:
#pragma vector = PORT1_VECTOR
__interrupt void P1_ISR(void) {
    switch(P1IFG&BIT3) {
        case BIT3:
            P1IFG &= ~BIT3;    // clear the interrupt flag
            BCSCTL1 = bcs_vals[i];
            DCOCTL = dco_vals[i];
            if (++i == 3)
                i = 0;
            return;
        default:
            P1IFG = 0;    // probably unnecessary, but if another flag occurs
                          // in P1, this will clear it.  No error handling is
                          // provided this way, though.
            return;
    }
} // P1_ISR

The port interrupts can be sourced from 8 port pins, and so the ISR needs to decide what to do with each possible interrupt.  The switch statement in C is ideal for this task, and is used here to single out a BIT3 flag.  If, for some reason, the MSP430 is flagged with a different Port 1 interrupt, it simply clears the flag and moves on.  Since none of the other flags are enabled, this should never happen, but it's good coding practice to include something like this.  Error handling would be ideal, but it's not done here.

Note that since the P1 interrupt has 8 sources and individual flags, you need to clear the flag manually.  Don't forget to do this, or your code will continually keep interrupting!  This code makes use of three pre-stored values for BCSCTL1 and DCOCTL and increments through them each time the interrupt is called.  These values, as well as the counter i, need to be global for the ISR to see them, and so they are declared outside of the main() function.  See the whole code put together in interrupted-1_G2211.c.

Timer_A Example
As a second example, we'll do something similar to a project I'm working on and turn a light on and off using a relay switch.  I've coded this using a switch on P1.6 so that you can observe its behavior on a LaunchPad using the green LED instead of the actual relay.  If you have a relay, be sure to connect it correctly to prevent too much current draw from your MSP430.  Using the 1 MHz calibrated DCO divided by 8, Timer_A will count up to 62,500 in 0.5 s.  In my actual application, I'd like to turn the light on for a few hours, then turn it off for a few hours.  That's easier to do with a slower clock, and an ideal application for the LFXT1 clock source.  But, since we haven't covered that yet and many people likely do not have the crystal soldered to their LaunchPad's, we'll do more frequent on/off cycles to simulate the actual process.  We'll use the DCO to turn the light on and off in 1 minute intervals.  To do this, we'll make use of the Timer_A up mode and interrupt using the CCR0 registers.  (Yes, this is essentially a fancy version of blinky.  Turns out "Hello, world!" is actually useful in microcontrollers!)

We configure the timer with the following:
TACCR0 = 62500 - 1;  // a period of 62,500 cycles is 0 to 62,499.
TACCTL0 = CCIE;      // Enable interrupts for CCR0.
TACTL = TASSEL_2 + ID_3 + MC_1 + TACLR;

The long string of assignments to TACTL are, in order, select SMCLK as the clock for Timer_A (runs off the DCO by default), select input divider of 8 (1 MHz SMCLK becomes 125 kHz), select up mode, and clear the count in TAR.  These values are defined in the device header file so that you don't have to constantly manipulate bits by hand.  The mnemonics are easier to use most of the time.  (For other configuration options, see the Timer_A section in your device's header file.)

The ISR may look something like this:
#pragma vector = TIMERA0_VECTOR
__interrupt void CCR0_ISR(void) {
    // no flag clearing necessary; CCR0 has only one source, so it's automatic.
    if (++i == 120) {
        P1OUT ^= RLY1;
        i = 0;
    }
} // CCR0_ISR

Again, we're using a global variable i as a counter, and presumably have defined RLY1 to be BIT6 in the program header.  Every time a flag occurs (twice a second) the counter is incremented.  If we've reached 120 (60 s at 2 Hz) the relay control is toggled and the counter resets.

The complete code for this example is in interrupted-2_G2211.c.  For you impatient folks, feel free to adjust the timer period to whatever value preserves your sanity.

These two examples should get you started with interrupts.  As always, feel free to ask questions or suggest other helpful tips!

32 comments:

Anonymous said...

Hey!

I really like TimerA0 ISR where you use global variable to expand delay, or to be specific - count interrupts and toggle when reaching some desired value.

Couple of days ago, I really scratch my head how to do so. I used default DCO, prescaler and continous mode reaching maximum delay of 0.5s.

10^6 / 65355 / 8 = 2 Hz or 0.5s

Now I see that software solution must be introduced.

Thanks for your tutorials and keep up!

David said...

The ideal way is to slow down the clock more. If you can live with an inaccurate time, a lower frequency on the DCO (or the VLO, even) would work.

If accurate timing is needed, a crystal is perfect. A 32kHz crystal divided by 8 for ACLK, then again by 8 for Timer_A runs at 512 Hz, giving a period for Timer_A interrupts at 128 s (counting to the full 16 bits)! Especially useful in an LPM, as it means you only turn on the processor once every couple of minutes or so.

Anonymous said...

Just wanted to say that I really appreciate your work on this!

I'm hoping you haven't moved on, as I'd love to see the MSP430 bent to the task of instrumentation.

tor said...

Thank you! Very useful blog and post. If you have time, tell us how to put it in LPM while waiting for the interrupt. I tried _BIS_SR(LPM4_bits + GIE); instead of _enable_interrupt(); but then the interrupt never came.

David said...

There's an excellent reason LPM4 didn't work; take a look at the new post on LPMs in Tutorial 11-a. I hint at what's going on in the exercise posted at the end. If it's not clear thinking about it that way, I will address the exact reason in the next post.

Jon said...

I am assuming the "x" in IES.x is referring to the bit affected in the P1IES register. Therefor the comment in the GPIO example is referring to BIT3 or IES.3?

Thanks,

Jon

Jon said...

Sorry for the double post back to back. I noticed in the code for the GPIO interrupt you set BCSCTL1 and DCOCTL using integer values like 7 and 3 from the char arrays. Is this the same as setting 7 for BCSCTL1 in the following way

BCSCTL1 |= BIT0 + BIT1 + BIT2;

Thanks,

Jon

David said...

Jon, you're correct in all your statements. Keep in mind that |= is a lot different from =: by doing a direct assignment BCSCTL1 = 7, you set it so that only bits 0, 1, and 2 are set. If you were to do BCSCTL1 |= 7, and it had something other than 0 in it before, you may get unexpected results since |= 7 would only ensure bits 0, 1, and 2 are set, leaving the other bits unchanged.

Jon said...

Thanks David!

Anonymous said...

I wondered if during debugging the timer counter incremented or not.
Turns out it doesnt.

What I wanted to know, is what exactly happens when i stop my debugging at a particular step. As in does the clock input to the timer stop as well? What is the state of the hardware when I have stopped the debugging at a particular command?

Anonymous said...

I am confused about the RETURNs in
P1_ISR. I thought you need a RETI which gets issued automatically or you won't get the proper reinstatement of critical registers. For the switch statement wouldn't you use breaks instead and then let the interrupt routine finish on its own?

David said...

A couple of good questions; I'm not sure I have answers for them. I think in terms of the return statements, it would have the same effect. The key is to be certain you clear interrupt flags. A break statement should also work, however, and since no values are being returned may be more appropriate here.

Any other readers have some thoughts on this or the hardware state during debugging?

Anonymous said...

Hey David,

Thanks a lot for the very fine detailed easy to understand tutorials. I have a project to do I am working on and I was wondering if you could help me finish it personally via e-mail.
For being anonymous: I haven't created an account yet.

Thanks again

David said...

The best place to go for project help is the online forums, such as at http://www.43oh.com/forum/ (I hang out there a bit myself). I'm certainly willing to help out where I can, but I also want to encourage use of the community that's being established. Right here, I'll address specific questions about the tutorials, but more general questions may be better suited for (and get better responses from) the forums. Thanks for reading; I'm so glad to hear that these are helpful!

Anonymous said...

Hey man,

This is the same anonymous that asked for your help yesterday. Thanks for the link and I'm on it. I understand your code and explanation easily but what I am having trouble with is finding what these acronyms stand for. I've figured out some of them but if I can't figure out all of them it'll be impossible for me to remember on the job what acronym was for what. I wish there was a database for that kind of a dictionary. wiki didn't help.

Thanks for the lectures again.

Anonymous: Still hasn't registered yet. Will do it sometime.

David said...

The two best sources I've found for all the names are the Family User's Guide and the device header files. The User's Guide doesn't have any code examples in C, and the software examples that TI provides are great, but don't really explain all the features of each peripheral. If you ever see something I use in my code that you're unsure about, look in the device header file. That coupled with the register explanations in the Family User's Guide should be a big help!

krf said...

I ran into an interesting problem with this code. I'm running CCS v5.1.1 and couldnt' figure out why the LED came on and stayed on. Stepping through the code, I saw that it was stepping over the call to delay(). When I opened the disassembly window, there was no subroutine call and no delay() subroutine. It appearently had been optomized out.

I went to Properties/Advanced Options/Advanced Optimiztions and checked "Disable all high level optimization", and things went to working OK.

Apparently, the compiler will do some optimization even if it is not set on the
Properties/Optimiztion tab.

Anonymous said...

your use of the following is wrong:>>

_enable_interrupt();

The correct version is :>>

__enable_interrupt();

You forgot that it starts witha double underscore.

David said...

krf: Thanks; I wonder if the newer version is set to a default optimization that is different than the older versions I've been using. I'll have to check, and adjust the code accordingly.

David said...

Anon: single underscore works for me; all the code I post on this blog is exactly what I used when testing on my own device.

Anonymous said...

Hello David! This tutorial is very helpful by the way. I have a quick question. How did you know that the value in TACCR0=62500-1 and the value in
if (++i == 120) {
P1OUT ^= RLY1;
i = 0;

is gonna be 1 minute?

David said...

Excellent question. The clock is set to 1 MHz for this code, and the timer runs on that clock divided by 8: 125 kHz. Counting to 62500 at that rate takes 62500/125000 = 0.5 seconds, so interrupts are triggered every half second. If we want a full 1 minute delay, we need 120 interrupts.

In general, the number we need to count to is given by t*f, where t is the delay you want and f the frequency the timer is running on. If that number is larger than 65536, then you'll need to use the counter inside the ISR as is done here.

Hope that helps!

Anonymous said...

Oh!! I see. Thanks man. :)

Filip said...

Hi David!
Your statement in tutorial:
"_enable_interrupt(); // Note the singular name here! It's not interrupts."
is not correct.
Based on this TI document:
http://www.ti.com/general/docs/lit/getliterature.tsp?literatureNumber=slau132g&fileType=pdf
at the end of a page 112 is written that both forms are correct.

Vishwanath said...

Hi,
This tutorial had given me a lot of clarity on interrupts. However, I am confused with the naming conventions, at #pragma and the next line. Please throw some light on the same.

Anonymous said...

Hey David
Awesome tutorials.
Small suggestion.
For G2553 uC #pragma vector = TIMERA0_VECTOR doesnt seem to compile in code composer ...
#pragma vector=TIMER0_A0_VECTOR works perfectly .
Thanks.
Keep up the good work .

Vaibhav

David Olson said...

As far as I understand, the #pragma declaration in C causes the compiler to do some specific, special task that is unique to the compiler. In this case, it tells the MSP430 compiler where interrupt code is located. The vector=... specifies which interrupt the following code associates with.

As for the name TIMERA0_VECTOR, I've had no problem using it. You can find these names in the header file for your specific device. Vaibhav, I don't know if the G2553 header is different, but for the G2211 and G2231 devices TIMERA0_VECTOR works fine. Check your device's header file to be ceratin.

The naming in the next line (in this code I use CCRO_ISR) is arbitrary; it should be named something meaningful to you. In this case I chose the name to reflect that this is an Interrupt Service Routine (ISR) using Capture Compare Register 0 (CCR0). If you don't like it, feel free to name the routine whatever you'd like. =)

Note, however, that the vector name is not arbitrary. If TIMERA0_VECTOR is not working, again, check your device's header file for the correct naming convention.

Anonymous said...

Hello, great tutorial! It helped me a lot.

By using the timer interrupt, I'm trying to catch when a signal goes low and count for 88 microseconds to enter my interrupt. However, at any time the signal goes high before the 88 microseconds end, I wanna stop the timer and wait till the signal goes back low.

Do I need another interrupt after the timer? I would really appreciate your help.

David Olson said...

There are two ways you could do it; the dirty way would be to trigger the first interrupt, and then poll the input until either it goes high or 88 us has passed, whichever comes first. The compromise with this method is that the mcu can't do anything else during this time, and the polling may make your 88 us timing less accurate.

The cleaner way would be to use the interrupt again; here's how I'd approach it: on the falling edge interrupt, start the timer, which counts up to 88 us and set the interrupt to catch a rising edge. If the rising edge is seen, stop and reset the timer. If not, the timer interrupt at 88 us will continue your code.

Anonymous said...


I'm trying to do the second method.

I got this code for the timer interrupts:

P1IN = BIT1;
if(!(P1IN & BIT1))
{

P1OUT ^= BIT0;
// light up a Led for checking
P1IFG = 0;
TACCR0 = 1408 - 1;
TACCTL0 = CCIE;
// Enable interrupts for CCR0.
TACTL = TASSEL_2 + ID_3 + MC_1 + TACLR;
// SMCLK, div 8, up mode,
// clear timer
}

But I'm not sure how to do the second part "If the rising edge is seen, stop and reset the timer."

David Olson said...

Now switch the P1IES to the opposite; you'll need an if or switch statement in the P1 ISR to start the timer on a falling edge, and stop the timer on a rising edge.

If you don't want a rising edge to interrupt after the 88 us, make sure your timer interrupt turns off the P1 interrupts.

Also, you can configure the timer before; just leave it in mode 0. When your falling edge occurs, change to up mode, and the counting starts. Less overhead that way, so more accurate timing. Timer interrupts and everything else can be initialized at the beginning of your main loop.

Good luck with it!

Mattias said...

Regarding the questions above about multiple return statements in the interrupt routine - that's actually quite an interesting question, but it seems that the person asking the question assumes that a C RETURN statement is always compiled into an assembler RET instruction.

As an interrupt routine can occur at any time, when entering an interrupt routine we need to push the old value of any registers that are used within the routine to the stack. This means that at the end of an interrupt routine, we not only need to use the RETI instruction, but we also potentially need to pop one or more registers from the stack. This also means that any RETURN statement within an interrupt routine must go through the same epilog to function correctly. The compiler knows this and converts any RETURN statements to jumps to the epilog.

This is the same way it would work with normal function calls, only that since we have prefixed the routine with an __interrupt directive, the compiler knows to end the routine with a RETI instruction instead of a RET instruction as for a normal function.