Using FreeRTOS with PMSIS API and drivers

Introduction

FreeRTOS is the Operating System used in GAP SDK. While it provides the standard FreeRTOS APIs to users, it also leverages PMSIS (Pulp Microcontroller Software Interface Standard), both for drivers and some standard system tasks. This guides explains the basics rules to use FreeRTOS and PMSIS together.

Startup

At startup, before going into the application main, we start the scheduler, as well as a small event kernel. Those two are hard requirements to use any PMSIS driver.

Warning

vTaskSchedulerStart from FreeRTOS must not be called manually.

Scheduler v2

The scheduler v2 aims to limit contexts switch. It is idle less and there is no global event kernel. Each thread has its own, meaning that each callback inherits the priority of the thread that created it.

There are a few things to pay attention to when developping on the scheduler v2:

  • Do not wait inside a callback. Delay the next part with another callback. This prevents the stack to increase too much.

  • Each callback is executed inside the stack of the thread that created it.

  • When executing a long blocking process, keep in mind to give hand back to higher priority processes if an event occurs:

    • Yield regularly: no context switch but less reactive to higher priority event;

    • Use another thread with higher priority: better reactivity but a context switch cost to pay.

  • The printf on cluster are delegated as IRQ to FC so they can interrupt it if it is in a busy loop. Prevent them.

Tasks

Multi-threaded environment must be carrefully used, to prevent well known issues on memory threads issues. Using asynchronous environment is more secured this way and should be preferred. It uses events to switch from a task to another. See the following section to know how to handle tasks synchronisation in an event-driven programming.

Inter tasks synchronisation

All the PMSIS API uses pi_evt structures for synchronisation. These can be used either for semaphore like construct (initialized with pi_evt_sig_init), or callbacks (initialized with pi_evt_callback_no_irq_init).

These can also be used between tasks, and between a task and callbacks themselves. xTaskNotify and xTaskSemaphore can also be used, howvever, pi_evt has the advantage of being directly usable inside PMSIS functions.

As an example, one could dispatch from a thread and block on another via blocking:

pi_evt_t evt_write;
pi_evt_t evt_read;
void task1(void)
{
    /*
     ....
    */
    while (something)
    {
        // wait on event, and reinit it
        pi_evt_wait_on(&evt_write);
        pi_evt_destroy(&evt_write);
        pi_evt_sig_init(&evt_write);
        pi_some_device_read(some_device, some_buffer, some_size, &evt_read);
    }
    /*
     ....
    */
}

void task2(void)
{
    /*
     ....
    */
    while (something)
    {
        pi_some_device_write(some_device, some_buffer, some_size, &evt_write)
        // wait on event, and reinit it
        pi_evt_wait_on(&evt_read);
        pi_evt_destroy(&evt_read);
        pi_evt_sig_init(&evt_read);
    }
    /*
     ....
    */
}

void main_task(void)
{
    /*
     ....
    */
    pi_evt_sig_init(&evt_write);
    pi_evt_sig_init(&evt_read);
    /*
     ....
    */
}

Note

pi_evt_t structures are by nature thread/event safe objects.

Timers

Precise timers

PMSIS API provides access to precise hw timers. This API’s resolution is in the order of 100 micro seconds. Its two main components are:

pi_time_wait_us(uint32_t timeout_us):

This API allows to block the current thread for timeout_us micro seconds.

pi_evt_push_delayed_us(pi_evt_t *evt, uint32_t timeout_us):

This API allows to execute a callback or unlock a semaphore timeout_us micro seconds in the future.

These APIs allow to completely replace FreeRTOS’s vTaskDelay and xTimerXXX APIs. Those have the advantage of being much more precise and lightweight.

The only non direct replacement would be a periodic timer. It is however extremely straightforward to do:

/*
 .....
*/

/* example callback that re-pushes itself, effectively creating a periodic
 * timer
 */
void my_pi_periodic_timer(void *arg)
{
    pi_evt_t *evt = (pi_evt_t *) arg;
    // re-push the same event with same delay
    pi_evt_push_delayed_us(evt, 1000);
}

void test_sw_timer(void)
{
    /*
     .....
    */
    pi_evt_t cb;
    // pass task itself as callback argument, for real case, using
    // a structure containing all desired information is recomended.
    pi_evt_callback_no_irq_init(&cb, my_pi_periodic_timer, (void*) &cb);
    // delayed push, 1ms
    pi_evt_push_delayed_us(&cb, 1000);
    /*
     .....
    */
}
/*
 .....
*/

Note

For full documentation on those APIs, please see API documentation.

Imprecise tick based timer

FreeRTOS provides imprecise timers, which are based on tick resolution.

Note

By default, FreeRTOS tick rate is 10ms on GAP9.

Warning

The use of these APIs in non legacy code is NOT recomended. These APIs are much less precise and incure larger performance cost than their PMSIS counterparts.

These are used via FreeRTOS APIs, and can be one-shot or periodic. Once a timer is done, it calls the callback given as argument.

An example of one shot timer is as follows:

/*
 .....
*/
// a timer array
TimerHandle_t xTimers[TIMERS];

// example timer callback - just print and release creating task
void vTimerCallbackOneShot(TimerHandle_t xTimer)
{
    char *taskname = pcTaskGetName(NULL);
    printf("%s : Callback at TICK = %d.\n", taskname, xTaskGetTickCount());
    pi_evt_t *evt_block = (pi_evt_t *) pvTimerGetTimerID(xTimer);
    pi_evt_release(evt_block);
}

void test_sw_timer(void)
{
    /*
     .....
    */
    // create a freertos timer, with a blocking event as argument
    xTimers[0] = xTimerCreate("Timer0", pdMS_TO_TICKS(10), pdFALSE,
                        (void *) &evt_block, vTimerCallback0);
    if (xTimers[0] == NULL)
    {
        exit(0);
    }
    // start the timer
    xTimerStart(xTimers[0], portMAX_DELAY);
    printf("%s created and started one shot timer. Waiting.\n", taskname);
    /*
     .....
    */
}
/*
 .....
*/

The callback will be called once, and only once. After which, the timer will stop on its own and need to be rearmed for further usage.

An example of a periodidc timer is as follows:

/*
 .....
*/
// a timer array
TimerHandle_t xTimers[TIMERS];

static uint32_t call_nb = 0;
// example timer callback - just print and release after 10 cycles
void vTimerCallbackPeriodic(TimerHandle_t xTimer)
{
    call_nb++;
    char *taskname = pcTaskGetName(NULL);
    printf("%s : Callback at TICK = %d.\n", taskname, xTaskGetTickCount());
    pi_evt_t *evt_block_periodic = (pi_evt_t *) pvTimerGetTimerID(xTimer);
    if (call_nb >= 10)
    {
        pi_evt_release(evt_block_periodic);
        // stop the timer from trigering again
        xTimerStop(xTimers[1], portMAX_DELAY);
    }
}

void test_sw_timer(void)
{
    /*
     .....
    */
    pi_evt_wait_on(&evt_block);
    xTimers[1] = xTimerCreate("Timer1", pdMS_TO_TICKS(10), pdTRUE,
                        (void *) &evt_block_periodic, vTimerCallback0);
    if (xTimers[1] == NULL)
    {
        exit(0);
    }
    // start the timer
    xTimerStart(xTimers[1], portMAX_DELAY);
    /*
     .....
    */
}
/*
 .....
*/

The periodic callback will be called 10 times, until call_nb reaches 10. At that point, the timer will be stoped.

Saving dynamic power

Any time nothing happens on the core, it will be clock gated. Thus saving power. To reach that state, the condition is to have no thread to schedule. As a consequence it is extremely important not to make threads artificially busy. This forbids most while(true){wait_ms()} constructs. Instead, code needs to wait on a semaphore or a pi_evt initialized via pi_evt_sig_init().

Example:

/* PMSIS includes */
#include "pmsis.h"

#define TIMERS ( 1 )
TimerHandle_t xTimers[TIMERS];

void vTimerCallback0(TimerHandle_t xTimer)
{
    char *taskname = pcTaskGetName(NULL);
    printf("%s : Callback at TICK = %d.\n", taskname, xTaskGetTickCount());
    pi_evt_t *evt_block = (pi_evt_t *) pvTimerGetTimerID(xTimer);
    pi_task_release(evt_block);
}

int main(void)
{
    printf("\n\n\t *** SW Timer Test ***\n\n");

    char *taskname = pcTaskGetName(NULL);
    uint32_t ulValue = 0, wait_val = 0;

    pi_evt_t evt_block;
    // Initialize a blocking task, equivalent to a semaphore
    pi_evt_sig_init(&evt_block);
    printf("%s creating Timers.\n", taskname);
    // create a freertos timer, with said blocking task as argument
    xTimers[0] = xTimerCreate( "Timer0", pdMS_TO_TICKS(10), pdFALSE,
                        (void *) &evt_block, vTimerCallback0);
    if (xTimers[0] == NULL)
    {
        exit(0);
    }

    // start the timer
    xTimerStart(xTimers[0], portMAX_DELAY);

    printf("%s created and started timers. Waiting.\n", taskname);

    pi_evt_wait_on(&evt_block);

    printf("%s deleting timers and suspending.\n", taskname);
    xTimerStop(xTimers[0], portMAX_DELAY);
    xTimerDelete(xTimers[0], portMAX_DELAY);

    return 0;
}

In this example, one task wait on an event, here coming from a one shot timer. During all the wait the task is blocked, and therefore never scheduled. Since no other tasks are present, FreeRTOS will execute its idle task, which will clock gate the core until the next IRQ which could change the system state. As such, 10ms of dynamic power are potentially saved.