MSP430 Workshop Series 5 of 12 - Interrupts

Welcome to the interrupts chapter of the MSP430 workshop. What is an embedded system without interrupts? If you just needed to solve a math problem, you'd most likely just sit down and use a desktop computer nowadays. Embedded systems, on the other hand, take inputs from real world events and act upon them. These real world events usually translate into interrupts.
[INAUDIBLE] interrupts? Asynchronous signals that are provided to our micro controllers. For example, the MSP430. They could come from timers, serial ports, push buttons, and so on. This chapter discusses how interrupts work, how they are implemented on the MSP430, and what code we need to write in order to harness their functionality.
The lab exercises provided are relatively simple. But the skills we learn here will apply to all the rest of the remaining chapters in the workshop. We begin this chapter with a high level introduction to interrupts. Next, we'll examine how interrupts work, what does it take to trigger an interrupt, and what does the MSP430 do about it.
Since we have more than one interrupt on the MSP430, how do we figure out which one occurred? And how do we handle priority amongst all of them. Next, we'll examine what code is needed to enable and handle interrupts. Finally, before we begin our lab exercises, we'll take a brief look at TI-RTOS scheduling and how that works with the MSP430s interrupts.
While many of you are already familiar with interrupts, they are so fundamental to embedded systems that we wanted to briefly describe what they're all about. From Wikipedia, a hardware interrupt is an electronic alerting signal sent to a processor from an external device. Either a part of the device itself, such as an internal peripheral, or an external peripheral.
In other words, the interrupt is a signal which notifies the CPU that something's occurred. If the interrupt is configured and enabled, CPU will respond to it immediately. And that's what we're going to talk about in this chapter is how to make all that happen.
There are two ways that a processor can recognize events. Polling or interrupts. Looking at polling first, let's start with a non-engineering analogy. If you've ever been on a long vacation before, you've probably dealt with the are we there yet question. In fact, our kids ask it over and over again.
Are we there yet? Are we there yet? It does get a little old. Eventually, the answer is, yes, we're there. But there seems to be a lot of wasted time in the process.
The alternative to are we there yet is when my wife says, wake me up when we get there. Both methods signal that we've arrived at our destination. In most cases though, the use of interrupts tends to be more efficient. For example, in the case of the MSP430, we often want to sleep the processor while we're waiting for an event.
It lets us use a lot lower power. And then, when the event actually occurs, it will wake us up, and we can handle the event, and then go right back to sleep. Rather than examining my family vacation, let's look at something that would be more found in our real world embedded system development. And that might be pushing a button.
Let's say that causes an event, and we want to react to that event. In our polling case, you can see that here what we've got is a while loop. And we just keep checking that pin value. In this case, we're using driver library to do that. And we're just looking to see when that value changes.
And because we keep doing it over and over and over again, it takes up 100% of the CPU load. The alternative is using interrupts. In this case, since the interrupt code only kicks off once the events occurred, it doesn't tie up your processor. And so you can see that we have a much, much smaller amount of CPO load dedicated to this.
To make this happen along with our driver library, we do need a couple of lines. That pound pragma line and the underscore underscore interrupt. That's part of the syntax that we need for interrupts. But we'll talk about that as we go through the chapter. The big point here is polling while it works is not very efficient. It doesn't let our processor sleep. So we waste a lot of power and a lot of cycles doing effectively nothing.
We conclude our introduction to the interrupts chapter by defining a few common terms that are often used to describe interrupt driven systems. The words foreground, background, and thread. If you look at the code below, you might see there are three individual-- you know, by that we mean independent code segments.
Main, ISR1, and ISR2. We use the word independent because if you were to examine the code in such a system, there aren't any calls between the three of the routines. Each one begins and ends execution without calling the others. It's common to call these separate segments of code threads.
Now if you've written this type of code before for an embedded system, you'd probably recognize it. Basically, after initialization and set-top code in main, the code enters an endless while loop. We considered this code to be background or lower priority code because once an interrupt occurs, we're gonna leave main, and go execute that interrupt service routine, that ISR. ISR1 or ISR2. In this case, whichever one occurred.
We call that interrupt service routine the foreground, the higher priority processing. And when it's done, we return back to the background. In this case, me. Here's another look at the foreground and background processing. We could call them foreground or background states.
You can see that we begin in that background lower priority main function. So in, kind of, the background state. And then, when an interrupt occurs, we leave main, and go process our higher priority interrupt service routine. In this case, interrupt service routine 1.
When that's done, we return back to main. And we stay there until either ISR1, or it looks like, in this case, ISR2 gets triggered. We respond to it. We go back. We get another ISR2. And you can see how we can just move back and forth between the foreground and the background states.
Now that we've looked at interrupts from a high level, let's look at how they actually work. Now that we have a rough understanding of what interrupts are used for in our system, let's discuss the mechanics needed to make them work. At this point, if you want a little challenge, pause the video recording, and see if you can think of the four things that are required in order to get interrupts working.
Number one, this is almost a trick comment, but the interrupt has to occur. And we say this just to point out that an interrupt has to occur. And there are lots of different possible sources for interrupts. Later in the chapter, we're going to delve into the MSP430 data sheet, which lists all these interrupt sources.
But you can see some of them are listed here. Things like that you UART, GPIO, Timers, the A-to-D converter, and things like that. Two, an interrupt is flagged and also must be enabled. When an interrupt signal is received, an interrupt flag bit is latched. We abbreviate interrupt flag with IFG in most places in the processor.
You can think of this as the processors copy of the interrupt signal. As some interrupt sources are only for a short duration, it's important that the CPU registers latch that interrupt signal internally. MSP430 devices are designed with a distributed interrupt management.
That is most of the interrupt flag bits are found inside each peripherals control registers, which is a little different than many other processes which have a common, dedicated set of interrupt registers. The distributed nature of the interrupts provides a number of good benefits in terms of device flexibility and future expansion. Further, it fits really nicely with the low power nature of the MSP430.
The only negative, if you can call it that, of distributed interrupts might be that it's just different. Many of us are used to seeing them all grouped together. Bottom line, though, is that when working with interruptss-- whether enabling interrupts, clearing them, responding to them-- it's all done in, pretty much, the same way whether the hardware is laid out centrally or in a distributed fashion.
In the last slide, we said that interrupts had to be flagged, meaning that they occurred. But they also have to be enabled in order for the CPU to respond to them. So how does the interrupt signal reach the CPU? We just talked about the interrupt flag bit. So let's start over there on the left hand side of this diagram.
When the interrupt source signal is received, its associated interrupt flag bit is set. In fact, driver lib contains some functions that allow us to read most interrupt flag bits. That can be handy in those rare cases where you might need to pull for an interrupt.
Now when the interrupt flag bit is set, the MSP430 device sees that the signals occurred. But the signal hasn't made its way all the way over to CPU yet. For that to happen, the internet must be enabled. Interrupt enable bits-- IE bits, we call them-- exist in order to filter out interrupts going to the CPU and, thus, to your program. With so many peripherals and interrupt sources, it's likely that your program will only care about a few of them.
The enable bits let your program ignore all those interrupt sources you don't really need or care about. By default, all interrupt bits are disabled, except the watchdog timer. In other words, you have to enable those interrupt sources that you want to use in your program.
To that end, once again, driver lib makes it easy with functions that let you set the necessary IE bits. On this slide, we show a couple of examples for GPIO and the Timer module. Finally, there's a master switch that turns all the interrupts off. This lets you turn off all interrupts without having to modify every individual interrupt enable bit.
The MSP430 calls this the global interrupt enable or GIE. And you find the GIE bit in the MSP430 status register, which is usually nicknamed SR. Why would you need the GIE bit? Well sometimes your program may need to complete some code atomically. That is your program may need to complete a section of code without fear than an interrupt could preempt it.
For example, if your program shares a global variable between two threads-- say, between main and an interrupt service team-- it may be important to prevent interrupts from occurring while main actually reads and modifies that variable. So that takes us from the interrupt source on the left. We get an interrupt flag bit.
For that signal to reach the CPO, it's individual interrupt enable bit must be set. And the global interrupt enable bit must also be set. On a quick side note, the MSP430 supports a non-maskable interrupt, also known as NMI. This interrupt is not disabled by the GIE bit.
It's mainly used for critical external system events and things like that. We don't specifically cover the non-maskable interrupt in this chapter. It is mentioned in the clock and system initialisation chapter. If you want to find out more information about NMI, we suggest that you look in the MSP430 users guide for your specific processor.
Three, we see the CPU's hardware response to the interrupt. At this point, let's assume that you have an interrupt that's, one, occurred, two, been flagged, and since it was enabled, ita signals reached CPU. What would we expect the CPU to do in response to the interrupt?
As we stated earlier in the chapter, an interrupt preempts the current thread and starts running the interrupt service routine or ISR. Well this is true. There are actually a number of different things that we performed, as we show here. First, we hope that the first three items on this list are self-explanatory.
The current instruction is completed while the program counter and the status register are written to the system stack. By the way, the system stack is set up by the compilers initialization team. Please refer to the compiler user's guide for more information about the stack.
After saving the context of the status register, the interrupt hardware in the CPU clears most of the ASR bits. Most significantly, it clears GIE. That means that, by default, whenever your ISR functions starts running, all maskable interrupts have been turned off. That means no interrupt nesting by default. We'll touch on the idea of interrupt nesting in the next section.
Finally, the last three items tells us that the processor figures out which interrupt occurred, and calls the associated interrupt service routine. It also clears the interrupt flag bit if it's a dedicated interrupt. By the way, processor knows which ISR to run because each interrupt is associated with an ISR function via a look-up table. We call this look-up table the interrupt vector table. And we'll talk more about that in a few minutes.
Four, your interrupt service or ISR. Interrupt service routines are also called interrupt handlers. And this is the code you write, which runs whenever a hardware interrupt occurs. Your ISR code must perform whatever task you want to execute in response to the interrupt event. But this code has to work without adversely affecting the other threads. That is the other code already running in your system.
The crux of an ISR is doing what needs to be done in response to the interrupt. The fourth bullet listed in read reads run your interrupts code. Basically, this means what we've just been saying. This is what you really want to have happen when an interrupt occurs.
For example, if you're talking about a UART interrupt, the code might read the incoming bite of data, and write it to memory. OK, let's look at some of the other bullets here in part four. The second bullet, we called it optional, and we crossed it out.
While you can nest interrupts robes on the MSP430, it's not recommended. So we've crossed that out just to drive home that point. The third bullet tells us that if it's a group interrupt, you're going to have to write the code to figure out which interrupt in the group needs to be handled.
This is usually done by reading the groups interrupt vector register, which we'll talk about in little bit. The other bullets that are part of the ISR are related to saving and restoring the context of the system. This is required so that we can meet the condition we mentioned earlier.
That is, quote, "without adversely affecting the other code threads already running in the system," unquote. In this slide, we show the interrupt flow in a slightly different way. As you can see, when the enabled interrupt occurs, the processor will look up the interrupt service routines branch to address from a specific address in memory. This is called an interrupt vector.
For the MSP430, this address is defined when using the vector pragma, as shown here by the blue arrow. Looking at the bullets on the left hand side, when you're using the interrupt keyword as we've shown here by the green arrow, the compiler handles all of the context save and restore for you, and returns you back to your previous code, even restoring the original value for the status register.
Since the interrupt occurs asynchronously to the background thread, you can't pass arguments and receive values to or from the ISR. You have to communicate between threads using global variables or other appropriate data objects. As a bit of a side note, you can see here that we mentioned when you call a function within an interrupt service routine, the compiler has to do a full context save and restore because it just doesn't know what registers are gonna be used by functions down the road, so to speak.
Nesting must be set up manually on the MSP430. As we said earlier, the MSP430 apps team doesn't recommend that you nest interrupt service routines. Finally, TIs real time operating system, or TI-RTOS, provides a rich set of scheduling functions, which include handling hardware interrupts. In fact, it's actually easier to let TI-RTOS manage the interrupts for you.
It just does everything for you. All you have to do is write a standard C function. But the details of TI-RTOS are outside the scope of this workshop. While at the end of the PDF document for this chapter we provide a brief discussion of TI-RTOS, we recommend that you go look at the introduction to TI-RTOS kernel workshop for all the details.
Let's turn our attention to the various interrupt sources on the MSP430. Specifically, how the interrupt vector table helps us to determine which interrupt occurred. By the way, looking at the vector table also helps to explain how priorities are handled for concurrent interrupts.
The data sheet for each MSP430 device defines the pending priority for each of its hardware interrupts. In the case of the MSP430 F5529, there are 23 interrupts shown on the data sheet in decreasing priority. What do we mean by pending priority? As we just saw because GIE, the global interrupt enable bit, is disabled when the CPU jumps to an ISR, interrupts don't nest within each other.
That means if an interrupt occurs while an ISR is already being executed, it will have to wait until the current ISR is finished before it can be handled, even if the new interrupt is of a higher priority. On the other hand, if two interrupts occur at the same time, that is if there were two interrupts currently pending, then the highest priority interrupt is acknowledged and handled first.
Most of the 23 interrupts on the F5529 represent groups of interrupts. There are actually 145 different interrupt sources, each with their own interrupt flag. But these map into these 23 interrupts. In our slide, the sources listed in black represent interrupt groups. Although, shown in red represent discrete interrupts.
For example, the Timer B CCIFG0 interrupt shown in red represents a single interrupt signal. When the CPU acknowledges it, it will clear its interrupt flag. Conversely, the next interrupt in line shown here as just Timer B represents all of the remaining interrupts that can be initiated by Timer 0 B.
When any of these interrupts occurs, your ISR will need to figure out which one happened, and clear its flag, along with executing whatever code you need to associate with it. To help with using group interrupts, the MSP430 provides an interrupt vector register for each one of the group interrupts.
When you read this register, it actually returns the highest pending interrupt in that group. It also clears the pending interrupt flag for whichever IFG it returns. We'll see an example of using this later in the chapter when we look at the interrupt coding section.
Here, we expand the previous interrupt and source priority listing to include a few more items. First of all, we added a column that provides the interrupt vector-- that is IV register-- associated with each interrupt. Note the two names shown in red represent IFG bits instead of interrupt vector registers because they're for dedicated interrupts as opposed to grouped interrupts.
Another item of note on this slide is that the first three rows, which are highlighted in pink background fill, indicate that these interrupts groups are non-maskable. Therefore, as you might remember from earlier in the chapter, they bypass the GI bit. The final column in this diagram hints at the location of each interrupts address factor in the memory map.
For example, when using the watchdog as an interval timer, you would put the address of your ISR into a location 57. As we saw previously, this can easily be done using the vector pragma. And you don't even have to know that its number 57.
The MSP430 devices reserve the range FFFF down to FF80 hex for interrupt vectors. This means that for the F5529, the address for the system reset ISR sits right at FFFF. The remaining interrupt vector step down in memory from this point.
Remember, a 16-bit address requires two 8-bit memory locations. That's why there's two memory locations for each address. Finally, other MSP430 devices, such as the FR5969, are quite similar to what's shown here. The proceeding interrupt tables were redrawn to make them easier to view when projected during a workshop.
Here's what the actual table looks like in the F5529 data sheet. Here's a quick look at the table showing the MSP430 FR5969 interrupt vectors and priorities. Notice how similar it is to the F5529. The main differences stem from the fact that the two devices have a slightly different mix of peripherals.
Now we turn to writing code for our interrupt service routines. We'll start by looking at the simpler case for dedicated interrupts. Then, we'll look at how to handle grouped interrupts. As our last coding topic, we'll look at what it takes to enable interrupts. Starting with the case of the dedicated interrupt service routine, let's look at the watchdogs interrupt flag vector.
Being a dedicated interrupt, our [INAUDIBLE] code only needs to respond to a single interrupt condition. Additionally because it's dedicated, the CPU hardware automatically clears the WDTIFG bit when responding to the interrupt and branch into your interrupt service routine.
When writing an ISR for dedicated interrupts, we must do three things. One, put the ISRs address into the vector table using the vector pragma that we saw earlier, saving and restoring the CPU context using the interrupt keyword we looked at earlier and, finally, you have to write your interrupt handler code. In other words, do what needs doing.
Again, step one, we plug the vector table using the pound pragma vector. In our example, the line of code pound pragma vector equals WDT underscore vector tells the compiler to associate the following function with the watchdog vector. If you dug into the F5529 specific header files, you would find that WDT vector is associated with number 57, which matches the data sheet documentation we looked at earlier.
This slide shows both steps 2 and 3. Step 2, we save and restore the CPUs context using the interrupt keyword. The interrupt keyword tells the compiler that the function is an interrupt service routine and, thus, it needs to save the context of the processor that is the CPU registers before executing the ISRs code. Then, restore the context afterwards.
Don't forget. Functions using the interrupt keyword cannot accept arguments or return values. Notice that the interrupt keyword has two leading underscores here in our diagram. We've seen in our coding that we can actually get it to work with and without the underscores. But the preference is that we use the underscores because that's what's documented in the compiler user's guide.
Step three is the code that you really want to have happen when the interrupt occurs. In this example, we toggle a GPIO pin every time the watchdog timers interrupt event occurs. Not all ISRs will be this short. But we hope this gives you, kind of, a good starting point to work from.
Next, we examine coding grouped ISRs. Let's first summarize our two types of interrupts using a little logic. This diagram shows us one dedicated interrupt and two grouped interrupts. As mentioned earlier, the timer peripheral is provided with both types of interrupts.
For example, timer 0 has a dedicated interrupt for its capture and compare register zero that's nicknamed TA0CCR0. Notice how this is routed directly to the GIE input matrix. The remaining five timer 0 interrupts are logically anded together. This combination provides a second interrupt signal for timer 0 into the GIE input matrix.
Finally, this diagram also shows us that all of the GIO port inputs share a single group interrupt. This means that your GPIO ISR must always verify which pin actually caused an interrupt whenever the ISR is executed. The outcome of all of these is that the interrupt logic within the CPU recognizes each of the interrupt sources.
And therefore, well, for the first one, TA0CCR0, if that occurs, it will cause our code to vector to address 53. That is the timer A0 vector, and start executing. Similarly the remaining timer 0 interrupts are associated with vector 52. And finally, GPIO port P1 was assigned by the chip designer to vector 47.
Again, thankfully, we don't have to worry about all the actual vector numbers. The header files make it convenient because they've assigned symbols to all of them for us. The code for a group interrupt begins in the same way as for a dedicated ISR. We us the pound pragma vector, and the interrupt keyword.
For grouped interrupts, though, we need to determine which specific source caused the CPU to be interrupted. As we've described, reading the associated interrupt vector register gives us the highest priority pending event. In our GPO port one example shown here, we just need to read the P1 IV register. It's common to see IV registers read as part of a switch statement.
Shown here, if P1IV returns 6, it then means that pin 2 was our highest priority enabled interrupt. And therefore, it's case statement is executed. Note, the return values are detailed in the device user's guide, as well as the driver libraries user's guide. So when pin 2 on port 1 causes an interrupt, it looks like that, in this case, we'll just toggle a different GPIO pin.
By the way, there are two items in this example which help the compiler to produce better optimized code. While these intrinsic functions are not specific to interrupt processing, they are useful here. The even in range intrinsic function provides a compiler a bounded range to evaluate.
In other words, this function tells the compiler it only needs to worry about even results that are less than or equal to 10. Also, we can tell the compiler that the default case will never occur by using the never executed intrinsic. Finally, we need to enable any interrupts that we want to use.
Earlier, we learned that for the CPU to recognize an interrupt, two enable bits must be set. One, an individual enable. That is an IE bit for each interrupt source we want to use. And two, the global interrupt enable. GIE is the common master enable bit for all of the interrupts.
Well, except for those that are defined as non-maskable. In this example, we set up a GPO pin as an interrupt. We decided to create the function initGPIO to hold all of our GPIO setup code. As you can see here, we called it from main. The key driver lip function, which enables the external interrupt is GPIO underscore enable interrupt.
In fact, you'll find that most of the MSP430 driver lib interrupt enable functions take a similar form where you have the module name underscore with enable interrupt. Within our GPO function, before we actually enabled interrupt, we have three other related functions that we've called. Our first function, GPO set as input pin with pull up resistor configures the pin as an output.
On the launchpad, the hardware requires that we use a pull resistor to complete the circuit properly. Effectively, this function configures our interrupt source. The second function, GPO interrupt edge select, should be used to configure which edge transition you're interested in. A high to low transition or a low to high transition.
We included this function because the IEE register, which this sets up, is undefined at reset. Finally, the third function, GPIO clear interrupt flag, clears the IFG bit, the interrupt flag bit, that's associated with our pin. This isn't required. But it's commonly used right before you enable an interrupt.
You should clear the interrupt flag bit before setting the interrupt enable if you want ignore any prior interrupt event. In other words, clear the flag first if you only care about interrupts that will occur now or in the future. That takes care of setting up and enabling individual interrupts. But we still need to enable interrupts globally by setting the GIE bit in the status register.
This can be done a variety of ways. We've chosen to use the bit set status register intrinsic function. You can see this function in our main code just before the while loop. To describe this function, let's just go through each piece.
BIS stands for bit set. It's actually a analogous to an assembly instruction on the MSP430. SR stands for the status register. And then, in the parentheses, the bit we want to set is the GIE bit, the global interrupt enable. With that done, we've set up our interrupt source. We've enabled it, and we've globally enabled interrupts.
In the video presentation of this chapter, we're going to briefly cover two miscellaneous topics. While you're not required to provide interrupt vectors for every CPU interrupt, it's considered good programming practice to do so. To this end, the MSP430 compiler even issues warnings whenever there are unhandled interrupts.
If you adopt this code shown here for your own program, all you need to do is comment out the pound pragma whenever you end up implementing it in your own code. Here's the diagram we used to summarize GPIO control registers in our previous GPIO chapter.
It's a good way to visualize the GPIO interrupt capabilities. From the diagram, we can see that most of the MSP430 processors allow ports P1 and P2 to be used as external interrupt sources. This is because the ports actually have the required port interrupt registers. There are some devices in the MSP430 family that support interrupts on more than two ports.
For example, the FR5969 [INAUDIBLE] devices support interrupt inputs on ports P1 through P4. To help keep the video shorter, I'm going to be skipping the section on interrupts and TI-RTOS scheduling. Please refer to the workshop guide to review the TI-RTOS details.
Of course, if you want to know all the details about TI-RTOS, you can always take its workshop. And now we've come to our next lab exercise. In lab 5, we'll generate an interrupt with just a touch of a button. That is, we'll set up the MSP430 to trigger an interrupt when its launch pad button is pushed.
The optional part of this exercise has us experimenting with the watchdog timers interval mode. We'll use it as an ordinary timer, and run our eye ISR whenever it overflows.