This is a sequel to a previous blog post in which we examined the basics of a multitasking executive for the Cortex-M. In this post, we look at the remaining issues, and how a multitasking executive we call nanoexec, which we have uploaded to Github, solves them.

NanoExec Functions

Nanoexec provides just two external functions: one to create a task, and one to start the multitasking executive. These functions (and a few define constants) are declared in the header file nanoexec.h

extern int nanoexec_NewTask(void (*p)(int), unsigned arg, unsigned char *stack_addr, unsigned stack_size);
extern void nanoexec_Start(void (*errfcn)(unsigned));

The first argument to nanoexec_NewTask, “p”, is the function code, while “arg” is the argument passed to the task function when it starts running. “stack_addr” is the address of the stack to be used by the task, and “stack_size” is the size of the stack. See usage example in the later part of this post.

(click post to read more…)

The function argument to nanoexec_Start, “errfcn”, is used for error checking in case the executive detects an out-of-range stack pointer access, as explained later.

Task Function

A task is a C function, usually running as an infinite loop. For example:

void foo(int i)
    {
    while (1)
            {
            putchar(i + '0');
            unsigned u = rand() % 3000;

            DelayMilliSecs(u);
            }
    }

The above example task function “foo” runs an infinite loop, prints out its argument (and assumes that its argument is an integer from 0 to 9), and then uses the JumpStart API function to delay a random number of milliseconds, from 0 to 2999.

The following fragment creates two new tasks:

unsigned char stack1[STKSIZE];
unsigned char stack2[STKSIZE];

nanoexec_NewTask(foo, 1, stack1, NANOEXEC_MIN_STACKSIZE);
nanoexec_NewTask(foo, 2, stack2, NANOEXEC_MIN_STACKSIZE);

The function “foo” provides the function code for both of these tasks. The two tasks are differentiated only by the argument being passed to the function. Note that while multiple tasks may share the same function, each must have its own stack.

In a real application, a task loop usually consists of the task waiting for an input, and then performing some action(s). For example, an SPI write task can take a string of bytes from another task and write it out to the SPI bus. As the SPI bus transaction may take a long time, this allows the sending task to continue running instead of having to wait for SPI transactions to finish. Some handshaking or synchronization mechanism should be used to prevent data corruption (e.g. if multiple tasks send write requests to the SPI write task, it must have a way to ensure that the data for one task is completely written out before the data for another task is). Nanoexec does not provide synchronization services, but global variables can be used for this purpose.

A task function should generally run an infinite loop and never return. If it does return, nanoexec “kills” the task, so that it will not be run again.

You may also create tasks inside of another task. The maximum number of tasks is limited by the #define constant MAX_TASKS in the nanoexec.h header file.

Creating New Tasks

When a multitasking executive runs a task function for the first time, it acts as if the function has been called, similar to a normal C function call. In nanoexec, one argument may be passed to the function. The number of arguments passed to a task function is dependent on the executive’s design.

Recall that when a timer interrupt occurs, the CPU automatically saves some of the CPU registers to the PSP (Process Stack Pointer). The interrupt handler needs to save the rest of the CPU registers to the PSP, so that task resumption just makes the PSP point to the task stack and then exit from the interrupt handler.

When creating a new task, the executive set up the task’s stack frame such that when the task is run for the first time, the same task resumption mechanism can be used. Making task resumption and running a new task using the same code simplifies the executive code and lessens the overhead.

Initial Stack Frame

//This defines the stack frame that is saved by the hardware
typedef struct {
  uint32_t r0;
  uint32_t r1;
  uint32_t r2;
  uint32_t r3;
  uint32_t r12;
  uint32_t lr;
  uint32_t pc;
  uint32_t psr;
} hw_stack_frame_t;

//This defines the stack frame that must be saved by the software
typedef struct {
  uint32_t r4;
  uint32_t r5;
  uint32_t r6;
  uint32_t r7;
  uint32_t r8;
  uint32_t r9;
  uint32_t r10;
  uint32_t r11;
  unsigned LR;
} sw_stack_frame_t;

The above structures describe the stack frames required. hw_stack_frame_t is the set of registers pushed by the CPU automatically at the entry to an interrupt handler, and sw_stack_frame_t is the set that must be saved by the interrupt handler. Note that LR is saved in the sw_stack_frame_t as well, as it contains the EXCEPTION RETURN CODE (as explained in the previous post.)

When creating a new task, the executive leaves space for these two frames and ensures that the EXCEPTION RETURN CODE has the correct value so that the PSP will be used when the interrupt handler returns.

((sw_stack_frame_t *)task_table[i].sp)->LR = THREAD_RETURN;

This line of code modifies the initial stack frame’s LR, so that when this task is started first time, the CPU will “resume” using the PSP.

Task Stack Size

A task stack must be at least large enough to hold the CPU registers saved by the SYSTICK timer interrupt handler: there are 16 32-bit registers (R0 to R15), plus the PSR (Processor Status Register), and the LR (Link Register) which is saved twice, i.e. 18 registers in all, or 18 * 4 = 64 bytes. If a task function makes a function call, each call may save up to 9 registers, or 36 bytes, per the C calling convention defined by ARM. The task function may also use local stack space for local variables.

When you create a task using nanoexec_NewTask, you must provide a stack that is large enough for the task. The header file nanoexec.h has a define constant NANOEXEC_MIN_STACKSIZE, which can be used as the size of a byte array to be used a task stack. NANOEXEC_MIN_STACKSIZE is defined as 200, which should be sufficient for a typical function that makes one or two nested function calls. If your task function or the functions it calls require more stack space, then a larger stack should be defined. Otherwise, if the stack is too small, random data memory may get corrupted and incorrect behaviors will result.

There is some provision in nanoexec to detect stacks that have been corrupted, but it is not foolproof, since if the system is corrupted by a bad stack, then nanoexec itself may fail to run. When you call nanoexec_Start, you may supply the name of an error function that will be called when a stack problem is detected.

Entering The Multitasking System

To transition control from the user program (e.g. main) to the multitasking executive, you call the function naoexec_Start which will start the multitasking executive. This function never returns to the caller.

nanoexec_Start creates a “null” task, or Task 0, so that there is always something that the executive can run if no other task is available. This is a typical design decision in a multitasking executive.

nanoexec_Start then enables the SYSTICK interrupt, and loops forever while  waiting for the SYSTICK timer to trigger. After the SYSTICK interrupt triggers, the executive’s scheduler takes over and runs the tasks in the system.

“First Interrupt” Problem

The first entry to the multitasking executive is different from all the subsequent times: the interrupted code is the infinite loop in nanoexec_Start, which should never be resumed. Nanoexec handles this by setting up the system such that it looks like the first interrupt is pausing the “null” task (instead of interrupting at its real location in nanoexec_Start), and ensuring that when the null task is selected to run later, it will start correctly. From then on, the null task pauses and resumes just like any other task.

Conclusion

With these two blog posts, you should be able to go through the nanoexec code repository on http://github.com/imagecraft and understands how it works. (Please leave a comment if you have any questions!)

Looking forward, as part of our effort in supporting our new Internet of Things io2go hardware modules, we will be re-implementing our eMOS message passing multitasking kernel in the next few months. We will announce more details later, but the source code will be released on Github again, and it will be free for non-commercial use and low cost commercial use options. More later.

Happy coding! :)