Welcome to the Modern Embedded Systems Programming course. My name is Miro Samek and in this
lesson I'll delve deeper into interrupts. Today you will see how interrupts work in
the MSP430 processor, which will help you to understand how ARM Cortex-M differs from
other processors. Specifically, you will see exactly how an interrupt service routine is entered
and how it returns.
Today, I will start with the lesson17 project that I have prepared in advance instead of
copying the previous lesson16.
The reason is that I've used a different header file for the TivaC MCU, which is more compatible
with the latest Cortex Microcontroller Software Interface Standard (CMSIS).
The lesson-17 project is available for download from the usual URL: state-machine.com/quickstart.
After downloading the project, get inside the "lesson17" directory and open the provided
workspace in the IAR toolset. If you don't have the IAR toolset, go back to "lesson0".
Other than the replacement of the header file, the project is identical to what you had by
the end of the last lesson 16 or lesson 0x10, as I called it in the video.
To quickly summarize what happened so far: in the last lesson you have completely changed
the structure of your blinky program to use the SysTick interrupt instead of the brain-dead
busy-polling to wait between toggling the LED.
You probably didn't appreciate it at the time, but the SysTick interrupt handler turned out
to be a regular, completely ordinary C function, which you could call directly, not just through
the vector table in the magic process of preemption.
In fact, let's start today with modifying your code to call SysTick directly from main().
Let's also put some code into the empty while(1) loop, because for studying interrupts it's
more educational to have a piece of linear code for interrupts to preempt rather than
just a single branch-to-self instruction.
So, let's turn the green LED on and then immediately off in the while loop. This will add a few
instructions to the body of the loop and cause a visual effect of green LED glowing at about
half of its maximum intensity, because it will blink too fast to be noticeable by the
human eye.
The code compiles error free, so let's set a breakpoint at the function call and another
one at the beginning of the SysTick_Handler.
When you run this program, you can see that the call happens in a very standard way by
means of the BL (branch-with-link instruction).
The SysTick handler called as a regular function also works correctly and toggles the red LED.
And finally, the function returns to the caller flawlessly by means of the standard BX-lr
instruction.
When you continue, the program hits the breakpoint at the beginning of SysTick, but this time,
it is called through a completely different process of interrupt preemption.
And this is actually quite unusual. In fact, the ability to call interrupt handlers as
regular C functions is a unique feature of the ARM Cortex-M processor, because no other
processor allows interrupt handlers to be regular C functions.
In most other processors, the interrupt handlers require a special entry code and they return
through a special return-from-interrupt instruction, so they can't be regular C functions. Also,
interrupt handlers typically must save and restore more CPU registers than regular functions.
To understand why interrupt handlers must do this, you really need to see how they work
on other processors than ARM Cortex-M.
For that, I have here the MSP-EXP430G2 LaunchPad board right next to the TivaC LauchPad. I
had already used that board back in lesson 11. The MSP430 LauchPad is quite similar to
the Tiva LauchPad, except it has the MSP430 microcontroller instead of the ARM Cortex-M.
I have also prepared an interrupt-driven version of the blinky program for the MSP430 LauchPad,
which is analogous to the blinky for TivaC.
The programs are very similar and consist of initializing the pins for the LEDs, setting
up the periodic timer interrupt, enabling interrupts, and the while(1) loop, in which
the one of the LEDs is rapidly turned on and off.
Just to make it clear, MSP430 is a different processor than ARM, so I cannot use the IAR
toolset for ARM. Instead I am using IAR Embedded Workbench for MSP430. You can get it from
IAR.com, just like you got EW for ARM, whereas IAR offers a free KickStart version of the
MSP430 toolset as well.
Before you load and run blinky on MSP430, please make sure that the FET debugger on
the MSP430 LaunchPad is configured not to use software breakpoints. This means that
the debugger will use the hardware breakpoints built into the MCU. The MSP430 variant you
are using has two such hardware breakpoints, which is perfect for what you need today.
When you run the program you can see that the red LED blinks once per second and the green LED glows.
When you break into the code, you can see it spinning in the while (1) loop, whereas
single-stepping through the code causes the green LED to turn on and off.
Most importantly, though, when you set a breakpoint inside the Timer0_Handler, which is the interrupt
handler in blinky for MSP430, you can see that, first of all, the breakpoint is hit
at all, meaning that the interrupt handler runs.
And second, the LED toggles every time the handler runs, so the handler is doing its
job.
But wait a minute, the Timer0_Handler is apparently written in C, so what's the big deal here?
Well, the big deal here is the __interrupt extended keyword in front of the Timer0_Handler.
This keyword instructs the IAR compiler that this is no ordinary C function, but rather
an interrupt handler, also called interrupt service routine (ISR).
Also, there is the pragma-vector directive that automatically assigns the designated
interrupt handler to the specific location in the MSP430 vector table. This is a much
simpler mechanism than in ARM Cortex-M.
Let me make absolutely clear that both the __interrupt keyword and the pragma-vector
directive go beyond the standard C and are specific to both the IAR toolset and the MSP430
processor.
All this makes Timer0_Handler a NON-standard C function that you definitely cannot call
directly from your program.
But you don't need to take my word for it. In a minute you will examine closer the Timer0_Handler
code by stepping through it in disassembly.
But first, you need to find a way to trigger the interrupt at will from the debugger. This
is actually not quite trivial, because you need to fake the expiration of the Timer0
in your MSP430 MCU, for which you need to understand how this timer works.
So, here is the MSP430 Timer0 peripheral. It consists of three registers, with functions
quite similar to that of SysTick in ARM Cortex-M, but the registers are only 16-bit wide.
Timer0 can be configured to use different clock sources, but in the blinky program it
is set up to use the Subsystem Clock (SMCLK) divided by 8, meaning that only every 8th
Subsystem Clock cycle increments the TA0R register.
This large divisor is necessary to fit the half-second worth of clock ticks in just 16-bits.
It turns out that half-a-second is an awfully long time for any MCU, even an MSP430 running
at only at 1MHz.
I said that Timer0 increments, because unlike SysTick in ARM Cortex-M, Timer0 is an up-counter
that increments until it reaches the value in the TACCR0 register, at which point it
is reset to 0 and the Timer0 interrupt is generated.
This gives you a clue how to trigger the interrupt. You can write to the TA0R register a value
just below the limit in TACCR0. The next clock cycle will cause these values to match, which
will trigger the Timer0 interrupt.
So, let's just try this idea.
Load the program to the MSP430 LaunchPad and run it free.
Break into the program. As expected, you find the program inside the while(1) loop.
Next, open the Timer0 registers and find TA0R. Click on it and enter a value of 0xF422, which
is one less than the value in TA0CCR0.
Finally, set two breakpoints. One at the next instruction BIC (for bit-clear) and the other
at Timer0_Handler.
At this point, you have set up the following experiment:
You are stopped at the BIS instruction. By writing to the TA0R register, you arranged
for the Interrupt Line to go high on the next clock cycle.
You have also placed two breakpoints at the two possible paths through the code:
One is the very next BIC instruction. This breakpoint would be hit if the interrupt would
not preempt after the BIS instruction and the program would execute as usual.
Your other breakpoint is inside the Timer0 interrupt handler. This breakpoint would be
hit only when the interrupt would fire right after the BIS instruction thus preempting
the normal program flow in this exact point.
So, which one do you think is it gona be?
To experimentally settle this question, you cannot single step, because it disables checking
for interrupts after each instruction. Instead, you need to let the program run free.
So, the answer is... that the interrupt has fired.
But wait, there is more, because now, you can single step through the code.
The XOR instruction toggles the red LED.
And the next instruction called RET-I is very special, because it causes return from the
interrupt.
As you can see the program returns to the BIC instruction, where you still have your
first breakpoint.
So, congratulations. You have just created an interrupt preemption at will.
Maybe you don't quite appreciate it at this point, but a technique of triggering any interrupt
you want, exactly at the instruction of your choosing is invaluable, similarly to the fault-injection
technique that I showed you a couple of lessons ago.
Such techniques will help you track down the most elusive, intermittent, and difficult
problems in embedded systems programming, because instead of waiting forever for some
rare event, you can make it happen at will, any number of times.
For example, you can now answer the big question: How does the interrupt know where to return
to?
So, let's reset the target and setup the experiment again...
But this time, let's watch the CPU registers, and specifically the SP (stack pointer).
Let's also setup the memory view to the see the contents of the stack.
Because MSP430 is a 16-bit CPU, it is best to watch the stack in 2-byte chunks. Unfortunately,
I can't make the window narrow enough to show you only one column of stack entries, but
I highlight the current top of stack, that is the position of the SP.
Now, when I run the program and hit the breakpoint inside the interrupt, you can see that the
SP dropped from 0x3FE to 0x3FA, which is a difference of 4 bytes, that is 2 stack entries.
When you look into the datasheet of the MSP430 MCU, in the section about Interrupt Processing
you can see that interrupt entry pushes the PC and the SR (status register) to the stack,
so the SP drops by 4 bytes.
With this information, you can now identify that 0x000d is the saved value of the SR and
0xC038 is the saved PC, that is, the return address.
In fact, you can actually see that address in the disassembly window, which happens to
be the BIC instruction inside the while (1) loop.
Interestingly, you can also see that, after being saved, the SR register is cleared upon
the entry to the interrupt. Among others, this clears the Global Interrupt Enable bit
(GIE), which disables further interrupts to the CPU.
The RETI instruction, causes the exact opposite to the interrupt entry instruction by restoring
the SR and PC registers.
So that's how the code returns back to exactly the preemption point. Also the GIE bit is
restored, so the interrupts can be serviced again.
The return from an interrupt service routine (ISR) through the RETI instruction is interesting,
but an ISR can differ from a regular C function in one more important way.
And that is, an ISR must save more CPU registers than a regular function.
To see this, you need to modify your Timer0_Handler ISR to use more CPU registers, for instance
by calling a regular C function.
So, here I copy the current body of the Timer0_Handler and make it into a regular C function LED_toggle().
I then call this function from Timer0_Handler, instead of doing the toggling directly.
Next, I make another copy of the ISR and make it into a regular function Timer0_Function()
to have a control sample for comparison with the original ISR.
To prevent the smart IAR linker from eliminating this control function, I need to actually
call it somewhere, so I call it from main().
Finally, I need to provide the prototypes of all new functions, which I put in the bsp.h
header file.
When you load this new code
and set breakpoints in Timer0_Handler and Timer0_Function,
you can see that Timer0_Function() consists of just one instruction: branch to LED_toggle().
In contrast, the Timer0_Handler contains much more code. It starts with pushing registers
R13/12/15 and R14 on the stack, then it calls LED_toggle via the CALL instruction, and then
it pops the registers in the exact reverse order. Finally, the ISR returns via the RETI
instruction.
So, as you can see, the compiler generated very different code for a C function and ISR
that have otherwise identical bodies.
I hope you start sensing why the ISR more to do. A function call from within an ISR
can apparently clobber registers R12 through R15, so they have to be preserved. Otherwise
the interrupt preemption would have a side effect of clobbering registers.
Please remember that an interrupt can preempt asynchronously any two instructions, so the
compiler cannot tolerate clobbering registers.
In contrast, a regular function call is synchronous, because the compiler is doing it via the CALL
instruction or sometimes the BR instruction. In any case, the compiler is prepared that
certain CPU registers will be potentially clobbered at this particular point in the
code.
This concludes this closer look at interrupt handling in MSP430, which is much more typical
than in ARM Cortex-M. In the next lesson, I will go back to ARM and take a similar closer
look at how it handles interrupts.
If you like this channel, please subscribe to stay tuned. You can also visit state-machine.com/quickstart
for the class notes and project file downloads. There will be two projects for this lesson17,
one for the ARM and the other for MSP430.