Porting Guide - Saved Registers

The following details how you can decide which registers need to saved and restored during context switches. Context switches in Atomthreads can occur through the following mechanisms:

  • A thread voluntarily switches itself out due to going into suspension (cooperative switch)
  • From an interrupt handler because a new thread has become ready to run (preemptive switch)

In the former case, only those registers which are not expected to be modified by a C subroutine need to be saved. Because the call came through the C function atomThreadSwitch(), the compiler will already have saved those registers it does expect to be modified. This means that (depending on your architecture) your archContextSwitch() routine may only need to save a subset of the full register set.

In the latter case, when a context switch occurs in the interrupt handler, the interrupt handler also calls the C function atomThreadSwitch(), in which case the compiler should again have saved the registers that are expected to be modified by a C subroutine (or on some architectures you will explicitly save those registers on entry to any interrupt handler). This means that, again, your archContextSwitch() routine need only save the same subset of the full register set.

This system means that you can share the same context switch assembler routine for both types of context switch, reducing any overhead for unnecessary register saves during a voluntary thread suspension, and simplifies development and debugging through concentrating resources on a single routine.

For example, consider an architecture with 32 general purpose registers R0 to R31. The compiler expects C subroutines to only modify registers R16 to R31. If subroutines are to modify R0 to R15, they are expected to save them explicitly. 

Consider the following voluntary thread switch, where the thread puts itself to sleep:

* atomTimerDelay(10);
* |_atomSched();
* |_atomThreadSwitch();
* |_archContextSwitch(); // saves R0 to R15

archContextSwitch() is the assembler routine which your port should provide for switching threads. During the call to archContextSwitch(), execution will switch to another thread. This means that this function won't actually return until the original thread is scheduled back in. Because the compiler expects R16 to R31 to be modified by subroutines, you need only save registers R0 to R15 in your context switch. These registers are expected to be intact on return from archContextSwitch(), whereas R0 to R15 can be modified at will. It doesn't matter that the context switch changed execution to a different thread which may have clobbered the R0 to R15 registers, because the compiler will have already saved those in upper routines such as atomSched() if it needed them to be intact.

* void archContextSwitch (ATOM_TCB *old_tcb, ATOM_TCB *new_tcb)
* {
*   // Save current thread's R0 to R15 in old_tcb's context save area
*   PUSH R0;
*   ..
*   PUSH R15;
*   // PUSH other system registers, SP, return address etc.
*
*   // Restore new thread's R15 to R0 from new_tcb's context save area
*   // First POP the other system registers
*   POP R15;
*   ..
*   POP R0;
* }

Now consider the other mechanism for a thread switch, where an interrupt handler has made a new thread preempt the current thread. In this case atomSched() is called from the interrupt handler:

* interruptHandler()
* |_atomTimerTick()
* |_atomSched()<br>
* |_atomThreadSwitch()
* |_archContextSwitch()

In this example a timer tick has occurred which resulted in a new thread preempting the current one. atomSched() decides to switch contexts and calls through to the above, same archContextSwitch() routine. We still only need to save and restore R0 to R15, but because the interrupt handler has called C subroutines, it should already have saved R16 to R31. archContextSwitch() starts execution of another thread, so when we actually return from archContextSwitch(), we will be back in the interrupt handler which restores the previously saved registers.

* void interruptHandler (void)
* {
* PUSH R16;
* ..
* PUSH R31;
*
* CALL atomTimerTick
* CALL atomSched
*
* POP R31;
* ..
* POP R16;
* }

Note that, while it may seem peculiar, in both cases the call to archContextSwitch() does not actually return until the thread is scheduled back in. This can result in execution paths looking like the following:

* atomTimerDelay(10); // Thread 1 context
* atomSched();
* atomThreadSwitch();
* archContextSwitch();
* atomThreadSwitch(); // Thread 2 context (thread 2 was preempted)
* atomSched();
* interruptHandler();
* // Interrupt handler returns to where thread 2 was previously preempted

In this case there is a voluntary call to archContextSwitch() (with return path back to atomTimerDelay()) but when the processor does the return instruction from archContextSwitch(), it is in another thread's context, in which the call to archContextSwitch() came from an interrupt handler. Subsequent returns will therefore bring it back out in the interrupt handler routine. When Thread 1 is later scheduled back in, its stack frame will lead the return instruction from archContextSwitch() to return back through the original call path to atomTimerDelay().