Welcome to the Modern Embedded Systems Programming course.
My name is Miro Samek and in this lesson I'll finally explain how ARM Cortex-M handles interrupts
and why interrupt handlers can be regular C functions on this CPU.
Specifically, you will see how the designers of the chip have solved the problem with saving
all the right CPU registers and returning from interrupt functions.
As usual, let's get started with making a copy of the previous "lesson17" project and
renaming it to "lesson18".
If you are just joining the course, you can download the previous projects from state-machine.com/quickstart.
Get inside the new "lesson18" directory and double-click on the workspace file to open
the IAR toolset.
If you don't have the IAR toolset, go back to "lesson0".
To quickly summarize what happened so far: in the last lesson you saw how interrupts
work on MSP430, which is simpler than ARM Cortex-M and handles interrupts in a way that
is much more typical for embedded CPUs.
Based on MSP430, you learned that interrupts differ from regular functions in the two important
ways:
First, interrupts return with a different machine instruction than regular functions.
Specifically, regular functions like LED_toggle return with the RET instruction on MSP430,
while interrupts, like Timer0_Handler return with RETI instruction.
The instructions must be different, because they pop different registers from the stack.
Second, interrupts must save more registers than regular functions.
For example, Timer0_Handler saves registers R12-R15, while regular function Timer0_Function
with the exact same body does not need to save any registers.
So, the question now is, how ARM Cortex-M addresses these two requirements, so that
it can indeed use regular functions as interrupt handlers.
As in the previous lesson, you will answer these questions experimentally.
To do this, you need to trigger the SysTick interrupt on ARM Cortex-M from the debugger,
so that you can precisely control when the interrupt occurs.
The method you used on MSP430, was to set the counter register of the timer in the debugger
to the value one-less than the limit.
The timer then reached the limit on the next clock cycle, which triggered the interrupt.
Unfortunately, you cannot use this method with the SysTick timer in ARM Cortex-M, because
the STCURRENT register is write-clear, meaning that writing to it with any value clears the
register without triggering an interrupt.
But not all hope is lost.
As described in the TivaC datasheet, ARM Cortex-M offers another, even more direct way, which
is to manually set the interrupt pending bit in the Interrupt Control and State register.
Specifically, bit 26 in this register sets the SysTick interrupt to the pending state.
OK, so let's use this information to generate the SysTick interrupt at will.
Load the code to the TivaC LaunchPad board and set a breakpoint at the top of the while(1)
loop.
Run the code.
When the breakpoint is hit, move the breakpoint to the very next LDR.N instruction and also,
set another breakpoint in your SysTick_Handler.
Finally, in the Register panel, select the System Control Block and expand the ICSR section.
Go to the PENDSTSET bit and set it to 1.
By the way, Cortex-M provides an interrupt pending bit for every interrupt source, so
you can trigger every interrupt in the system by this method.
Most of these pending bits reside inside the Nested Vectored Interrupt Controller (NVIC).
But going back to your code, you've just set up the following experiment:
You are stopped at the MOVS instruction.
By writing to the PENDSTSET 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 your code:
One is at the very next LDR.N instruction.
This breakpoint would be hit if the interrupt would not preempt after the MOVS instruction
and the program would execute as usual.
Your other breakpoint is inside the SysTick_Handler.
This breakpoint would be hit only when the interrupt would fire right after the MOVS
instruction thus preempting the normal program flow at this exact point.
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.
And the answer is... that the SysTick interrupt has fired.
At this point you have a verified method to trigger your interrupt at a machine instruction
of your choosing.
You will use it in a minute to take a closer look at the interrupt stack frame and the
interrupt return process, just like you did in the previous lesson for MSP430.
But before this, please go back to the project options and set the Floating Point Unit (FPU)
to None.
The FPU is a complex peripheral for speeding up floating-point computations, but unfortunately,
it adds complications to the interrupt processing, which you don't want to deal with at this
time.
All right, so after re-building your project without the FPU, you can finally take a closer
look at the details of ARM Cortex-M interrupt handling.
Let's set the breakpoints
and trigger the SysTick interrupt exactly as before.
But this time, let's also setup the memory view to watch the stack content.
Remember that the stack grows down on ARM, so I scroll the memory view to place current
SP at the bottom.
Now, when you run the program and hit the breakpoint inside the SysTick interrupt, you
can see that the SP dropped to by 8 stack entries.
When you look into the datasheet of your TivaC MCU, in the section about "Exception Entry
and Return" you can see the interrupt stack frame without FPU.
Remembering that datasheets show memory from high to low, you need to turn the picture
upside down to align it with the memory view in your debugger.
When you do this, you can now identify which registers are saved on the stack.
For example, you can identify the saved PC, that is, the address to return to after the
interrupt.
In fact, when you scroll up the disassembly window, you can see that the return address
is the LDR.N instruction inside the while (1) loop.
But, more importantly, let me mark for you all the registers saved in the interrupt stack
frame, with the hope that you might recognize this particular group.
Well, back in lesson 9, you learned about the ARM Application Procedure Call Standard
AAPCS.
AAPCS was a convention that specified, among others, which registers must be preserved
by a function call.
I mark these registers for you again, so that you can see that this group precisely complements
the registers saved in the interrupt stack frame.
So, here you have the answer to today's first issue: Cortex-M interrupt entry complements
the ARM Procedure Call Standard, so that's why a regular C function can be used as an
interrupt handler.
Please note that this elevates the AAPCS from merely a calling convention that in principle
could be different for each compiler, to a hard rule that must be implemented the same
way by all compilers.
OK, so now let's step through the interrupt function and take a look at how it returns.
Well, the function returns in a completely standard way via the BX LR instruction, because
after all, this is just a regular C function.
But wait a minute, the value in the LR register is 0xFFFFFFF9, which is negative 7 in two's
complement.
This is NOT a valid address in the code space.
So what the hell is it?
Well, this is an ARM-special.
When this special value is loaded into the PC, the Cortex-M hardware treats this as a
return from interrupt.
That's why when you execute the BX LR instruction, you can see that the SP goes back to 0x3F8
and that the contents of registers is restored to the pre-interrupt state.
The PC jumps back to your while(1) loop, just after your original breakpoint, which is exactly
the point of preemption.
So, here you have the answer to today's second question.
A standard return from a function works also as a special return from an interrupt, because
the LR is loaded with a special value upon the interrupt entry.
Anther way of looking at this is, is that what other processors, such as MSP430, achieve
by a special instruction (interrupt-return "iret"), ARM Cortex-M achieves by using special
data--content of the LR register in this case.
The ARM solution based on data is actually more flexible and extensible than a special
instruction.
In fact, ARM provides several variants of interrupt returns, which are all summarized
in the Datasheet.
I'm not going to discuss in detail all the options, which you can simply read about.
But let me only quickly explain the terminology used in this table.
Handler mode is the distinct processor state when it handles an exception, such as an interrupt
or a fault.
Thread mode is when it executes regular code, such as your while(1) loop inside your main
function.
Floating-point state means here that FPU is activated and that interrupts use the FPU
stack frame as opposed to the regular stack frame.
I'm going to show you the FPU stack frame in a couple of minutes.
MSP stands for the Main Stack Pointer, while PSP stands for Process Stack Pointer.
The datasheet makes this distinction, because your ARM CPU has actually two stack pointers,
SP_main and SP_process, but only one of them is visible as SP, depending on the internal
state of the CPU.
This concept is called register banking and is another of the "ARM-specials".
At this point I only mention banking of the SP register to explain the terminology, but
the concept will be important when I'll talk about real-time operating system (RTOS), which
I plan to do in one of the future lessons.
But to finish with the basic interrupt stack frame, let me only explain the optional "aligner"
word.
The purpose of this optional stack entry is to align the interrupt stack frame at an address
that is a multiple of 8 bytes.
The reason the hardware needs SP to be aligned is to perform highly optimized, block transfers
of registers to and from the stack.
In fact, interrupt entry and exit take only 12 clock cycles each, which is very fast,
when you consider that 8 registers are pushed or popped from the stack.
To show you when stack alignment will be necessary, let's run the program again, but intentionally
misalign the SP in the debugger.
As before, when the breakpoint in your while(1) loop is hit, you trigger the SysTick interrupt.
You also setup the memory view to watch the stack in the same way as before.
But this time, to better see what has changed on the stack, you additionally pre-fill the
unused stack above the current value of SP with some easy to identify garbage, such as
0xDEADBEEF.
And finally, you misalign the stack by subtracting 4 bytes from the SP.
I you wish, you can check that the value 0x3F4 is not divisible by 8.
After you hit the breakpoint in the SysTick interrupt, you can see that the 8-register
interrupt stack frame has been pushed on the stack,
but, interestingly, one stack entry at address 0x3F0 has been skipped.
This is the "aligner" word.
Upon the interrupt return, the whole 9-word stack frame is removed, because the SP goes
back to the original, mis-aligned value 0x3F4.
Please note that in practice stack misalignment should never happen.
This is because the eight byte stack alignment is a requirement of the AAPCS and compilers
make sure that the stack is always aligned.
Still, the stack alignment concept will be important when I'll talk about the real-time
operating system (RTOS) in one of the future lessons.
As a final subject of this lesson, let me show you the impact of the FPU on the interrupt
stack frame.
To do this, please open the project options dialog box and re-enable the FPU.
Re-build the project and setup the interrupt entry experiment exactly as before.
Note that right before the interrupt, the SP is 0x3F8.
When the SysTick interrupt in entered this time, please note that the SP drops all the
way to 0x390, which is 26 4-byte words.
This is because now the CPU uses the stack frame with floating-point storage, which is
more than 4 times bigger than the regular exception.
Also, please note that the LR is now set to 0xFFFFFFE9 instead of 0xFFFFFFF9, which is
the special FPU-type interrupt return utilizing the much bigger FPU stack frame.
The moral from this is that you need to size the stack significantly bigger if you use
the FPU.
There is also an additional price to pay in longer interrupt entry and exit time.
This concludes this lesson about interrupt entry and exit on ARM Cortex-M.
In the next lesson I will talk about race conditions, which is a concept that you absolutely
need to understand to effectively work with 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.