Tasks and Priority Management
Target Platform: Arduino M0 Pro (or legacy Arduino Zero or Zero Pro, but not Arduino M0)
This lesson is designed to teach core OS concepts and strategies encountered when building applications using Mynewt. Specifically, this lesson will cover tasks, simple multitasking, and priority management running on an Arduino M0 Pro.
Prerequisites
Before starting, you should read about Mynewt in the Introduction section and complete the QuickStart guide and the Blinky tutorial. Furthermore, it may be helpful to take a peek at the task documentation for additional insights.
Equipment
You will need the following equipment:
- Arduino M0 Pro (or legacy Arduino Zero or Zero Pro, but not Arduino M0)
- Computer with Mynewt installed
- USB to Micro USB Cable
Build Your Application
To save time, we will simply modify the Blinky application. We'll add the Task Management code to the Blinky application. Follow the Arduino Zero Blinky tutorial to create a new project and build your bootloader and application. Finally, build and load the application to your Arduino to verify that everything is in order. Now let’s get started!
Default Main Task
During Mynewt system startup, Mynewt creates a default main task and executes the application main()
function in the context of this task. The main task priority defaults to 127 and can be configured with the OS_MAIN_TASK_PRIO
system configuration setting.
The blinky application only has the main
task. The main()
function executes an infinite loop that toggles the led and sleeps for one second.
Create a New Task
The purpose of this section is to give an introduction to the important aspects of tasks
and how to properly initialize them. First, let’s define a second task called work_task
in main.c (located in apps/blinky/src):
struct os_task work_task;
A task is represented by the os_task struct which will hold the task’s information (name, state, priority, etc.). A task is made up of two main elements, a task function (also known as a task handler) and a task stack.
Next, let’s take a look at what is required to initialize our new task.
Task Stack
The task stack is an array of type os_stack_t
which holds the program stack frames. Mynewt gives
us the ability to set the stack size for a task giving the application developer room to optimize
memory usage. Since we’re not short on memory, our work_stack
is plenty large
for the purpose of this lesson. Notice that the elements in our task stack are of type os_stack_t
which are generally 32 bits, making our entire stack 1024 Bytes.
#define WORK_STACK_SIZE OS_STACK_ALIGN(256)
Note: The OS_STACK_ALIGN
macro is used to align the stack based on the hardware architecture.
Task Function
A task function is essentially an infinite loop that waits for some “event” to wake it up. In general, the task function is where the majority of work is done by a task. Let’s write a task function for work_task
called work_task_handler()
:
void work_task_handler(void *arg) { struct os_task *t; g_led_pin = LED_BLINK_PIN; hal_gpio_init_out(g_led_pin, 1); while (1) { t = os_sched_get_current_task(); assert(t->t_func == work_task_handler); /* Do work... */ } }
The task function is called when the task is initially put into the running state by the scheduler. We use an infinite loop to ensure that the task function never returns. Our assertion that the current task's handler is the same as our task handler is for illustration purposes only and does not need to be in most task functions.
Task Priority
As a preemptive, multitasking RTOS, Mynewt decides which tasks to run based on which has a higher priority; the highest priority being 0 and the lowest 255. Thus, before initializing our task, we must choose a priority defined as a macro variable.
Let’s set the priority of work_task
to 0, because everyone knows that work is more important than blinking.
#define WORK_TASK_PRIO (0)
Initialization
To initialize a new task we use os_task_init() which takes a number of arguments including our new task function, stack, and priority.
Add the init_tasks()
function to initialize work_task
to keep our main function clean.
int init_tasks(void) { /* … */ os_stack_t *work_stack; work_stack = malloc(sizeof(os_stack_t)*WORK_STACK_SIZE); assert(work_stack); os_task_init(&work_task, "work", work_task_handler, NULL, WORK_TASK_PRIO, OS_WAIT_FOREVER, work_stack, WORK_STACK_SIZE); return 0; }
Add the call to init_tasks()
in main()
before the while
loop:
int main(int argc, char **argv) { ... /* Initialize the work task */ init_tasks(); while (1) { ... } }
And that’s it! Now run your application using the newt run command.
$ newt run arduino_blinky 0.0.0
When GDB appears press C then Enter to continue and … wait, why doesn't our LED blink anymore?
Review
Before we run our new app, let’s review what we need in order to create a task. This is a general case for a new task called mytask:
1) Define a new task, task stack, and priority:
/* My Task */ struct os_task mytask /* My Task Stack */ #define MYTASK_STACK_SIZE OS_STACK_ALIGN(256) os_stack_t mytask_stack[MYTASK_STACK_SIZE]; /* My Task Priority */ #define MYTASK_PRIO (0)
2) Define task function:
void mytask_handler(void *arg) { while (1) { /* ... */ } }
3) Initialize the task:
os_task_init(&mytask, "mytask", mytask_handler, NULL, MYTASK_PRIO, OS_WAIT_FOREVER, mytask_stack, MYTASK_STACK_SIZE);
Task Priority, Preempting, and Context Switching
A preemptive RTOS is one in which a higher priority task that is ready to run will preempt (i.e. take the place of) the lower priority task which is running. When a lower priority task is preempted by a higher priority task, the lower priority task’s context data (stack pointer, registers, etc.) is saved and the new task is switched in.
In our example, work_task
(priority 0) has a higher priority than the main
task (priority 127). Since work_task
is never put into a sleep state, it holds the processor focus on its context.
Let’s give work_task
a delay and some simulated work to keep it busy. The delay is measured in os ticks and the actual number of ticks per second is dependent on the board. We multiply OS_TICKS_PER_SEC
, which is defined in the MCU, by the number of seconds we wish to delay.
void work_task_handler(void *arg) { struct os_task *t; g_led_pin = LED_BLINK_PIN; hal_gpio_init_out(g_led_pin, 1); while (1) { t = os_sched_get_current_t:ask(); assert(t->t_func == work_task_handler); /* Do work... */ int i; for(i = 0; i < 1000000; ++i) { /* Simulate doing a noticeable amount of work */ hal_gpio_write(g_led_pin, 1); } os_time_delay(3 * OS_TICKS_PER_SEC); } }
In order to notice the LED changing, modify the time delay in main()
to blink at a higher frequency.
os_time_delay(OS_TICKS_PER_SEC/10);
Before we run the app, let’s predict the behavior. With the newest additions to work_task_handler()
,
our first action will be to sleep for three seconds. This allows the main
task, running main()
, to take over the CPU and blink to its heart’s content. After three seconds, work_task
will wake up and be made ready to run.
This causes it to preempt the main
task. The LED will then remain lit for a short period while work_task
loops, then blink again for another three seconds while work_task
sleeps.
You should see that our prediction was correct!
Priority Management Considerations
When projects grow in scope, from blinking LEDs into more sophisticated applications, the number of tasks needed increases alongside complexity. It remains important, then, that each of our tasks is capable of doing its work within a reasonable amount of time.
Some tasks, such as the Shell task, execute quickly and require almost instantaneous response. Therefore, the Shell task should be given a high priority. On the other hand, tasks which may be communicating over a network, or processing data, should be given a low priority in order to not hog the CPU.
The diagram below shows the different scheduling patterns we would expect when we set the work_task
priority higher and lower than the main
task priority.
In the second case where the main
task has a higher priority, work_task
runs and executes “work” when
the main
task sleeps, saving us idle time compared to the first case.
Note: Defining the same priority for two tasks fires an assert in os_task_init() and must be avoided. Priority 127 is reserved for main task, 255 for idle task.