Each task has a corresponding task structure that contains the following information:

Creating a task

You allocate task structures by declaring them as C variables.

CTL_TASK_t mainTask;

You create the first task by using ctl_task_init to turn the main program into a task. This function takes a pointer to the task structure that represents the main task, its priority, and a name as parameters.

ctl_task_init(&mainTask, 255, "main");

This function must be called before any other CrossWorks tasking library calls. The priority (second parameter) must be between 0 (the lowest priority) and 255 (the highest priority). It is advisable to create the first task with the highest priority, which enables it to create other tasks without being descheduled. The name should point to a zero-terminated ASCII string, which is shown in the Threads window.

You can create other tasks with the function ctl_task_run, which initializes a task structure and may cause a context switch. You supply the same arguments as for ctl_task_init, together with the function the task will run and the memory the task will use for its stack.

The function a task will run should take a void * parameter and not return any value.

void task1Fn(void *parameter)
{
  // task code goes in here
}

The parameter value is supplied to the function by the ctl_task_run call. Note that, when a task function returns, the ctl_task_die function is called, terminating the task.

You must allocate the stack for the task as a C array of unsigned elements.

unsigned task1Stack[64];

The stack size you need depends on the CPU (i.e., the number of registers that must be saved), the function calls the task will make, and (again depending on the CPU) the stack used for interrupt service routines. Running out of stack space is a common problem for multitasking systems, and the resulting error behavior is often misleading. Recommended practice is to initialize the stack to known values—that will make it easier to check the stack's contents with the CrossWorks debugger if problems should occur.

memset(task1Stack, 0xba, sizeof(task1Stack));

Your ctl_task_run function call should look something like this:

ctl_task_run(&task1Task,
             12,
             task1Fn,
             0,
             "task1",
             sizeof(task1Stack) / sizeof(unsigned),
             task1Stack,
             0);

The first parameter is a pointer to the task structure. The second parameter is the priority (in this case 12) at which the task will start executing. The third parameter is a pointer to the function to execute (in this case task1Fn). The fourth parameter is the value supplied to the task function (zero, in this case). The fifth parameter is a null-terminated string that names the task for debug purposes. The sixth parameter is the size of the stack, in words. The seventh parameter is the pointer to the stack. The last parameter is for systems that have a separate call stack, and its value is the number of words to reserve for that stack.

Changing a task's priority

You can change the priority of a task using ctl_task_set_priority. It takes a pointer to a task structure and the new priority as parameters, and returns the old priority.

old_priority = ctl_task_set_priority(&mainTask, 255); // lock scheduler
//
// ... your critical code here
//
ctl_task_set_priority(old_priority);

To enable time slicing, you need to set the ctl_timeslice_period variable before any task scheduling occurs.

ctl_timeslice_period = 100; // time slice period of 100 ms

If you want finer control over the scheduling of tasks, you can call ctl_task_reschedule. The following example turns main into a task and creates a second task. The main task ultimately will be the lowest-priority task that switches the CPU into a power-saving mode when it is scheduled—this satisfies the requirement of always having a task to execute and enables a simple, power-saving system to be implemented.

#include <ctl.h>

void task1(void *p)
{
  // task code; on return, the task will be terminated.
}

static CTL_TASK_t mainTask, task1Task;
static unsigned task1Stack[64];
int main(void)
{
  // Turn myself into a task running at the highest priority.
  ctl_task_init(&mainTask, 255, "main");

  // Initialize the stack of task1.
  memset(task1Stack, 0xba, sizeof(task1Stack));
  
  // Prepare another task to run.
  ctl_task_run(&task1Task, 1, task1, 0, "task1",
               sizeof(task1Stack) / sizeof(unsigned),
               task1Stack, 0);

  // Now that all the tasks have been created, go to the lowest priority task.
  ctl_task_set_priority(&mainTask, 0);

  // Main task, if activated because task1 is suspended, just
  // enters low-power mode and waits for task1 to run again
  // (for example, because an interrupt wakes it).
  for (;;)
    {
      // Go into low-power mode.
      sleep();
    }
}

Note that, initially, the main task is assigned the highest priority while it creates the other tasks; then it changes its priority to the lowest value. This technique can be used, when multiple tasks are to be created, to ensure all the tasks are created before they start to execute.

Note the use of sizeof when passing the stack size to ctl_task_run.