Runnables

The previous section we focused on the initialization of the system at runtime up to the point where the user can use the async API to schedule runnables within the created tasks. We now want to look into some details on how this is implemented internally and which FreeRTOS functionalities we use. Also here, let’s take a look into a simple usage example as shown below.

1void startApp()
2{
3    ::async::execute(TASK_EXAMPLE, runnable);
4    ::async::schedule(TASK_EXAMPLE, runnable, timeout, 1, ::async::TimeUnit::SECONDS);
5    ::async::scheduleAtFixedRate(TASK_EXAMPLE, runnable, timeout, 1, ::async::TimeUnit::SECONDS);
6}

It shows the main three API functions provided by async which allow to schedule a runnable in a given task identified by its integer id (in our case again an enum value). To find out the task object by its integer id we utilize the previously described _taskContexts array and call the belonging method on it.

Before looking into the concrete implementation of the different API calls, let’s first have a quick look into the TaskContext::dispatch function, which is by default the task entry function of the created FreeRTOS task (ignoring the calls through TaskContext::staticTaskFunction, TaskContext::callTaskFunction and defaultTaskFunction).

void TaskContext<Binding>::dispatch()
{
    EventMaskType eventMask = 0U;
    while ((eventMask & STOP_EVENT_MASK) == 0U)
    {
        eventMask = waitEvents();
        handleEvents(eventMask);
    }
}

As you can see, it is simply an endless loop waiting for events and handling them. The term event here is mapped to the FreeRTOS concept of task notifications which can be triggered by xTaskNotify or waited for using xTaskNotifyWait. The latter one releases the task and gives control back to the scheduler to allow other tasks to be executed. We currently use two of these events as can be seen by the EventPolicy types aliases declared.

using ExecuteEventPolicyType = EventPolicy<TaskContext<Binding>, 0U>;
using TimerEventPolicyType   = EventPolicy<TaskContext<Binding>, 1U>;

For each one we have a specific handler defined which is called by the handleEvents call in the task entry function loop in case the corresponding event is present. To understand what these handlers are doing, let’s come back to our async API calls.

The most simple one is the async::execute call which utilizes the _runnableExecutor maintaining a queue of functions that are called within the RunnableExecutor::handleEvent event handler corresponding to the ExecuteEventPolicyType. When new runnable function is requested to run in a target task, it is inserted into the queue and the event bit is set as shown in the simplified code below.

void RunnableExecutor::enqueue(Runnable& runnable)
{
    if (!runnable.isEnqueued())
    {
        _queue.enqueue(runnable);
    }
    _eventPolicy.setEvent();
}

The target task gets then unblocked (assuming it was waiting in waitEvents) and once FreeRTOS schedules it again, the provided runnable function is executed from within the handleEvents call. Note, that the actual implementation is a little bit more convoluted due to the heavy use of templates.

The two other async API calls async::schedule and asyncScheduleAtFixedRate are implemented quite similarly and we will leave it to the reader to have a look at the concrete implementation. They simply use the second event type TimerEventPolicyType with some clever logic to calculate proper timeouts for the xTaskNotifyWait call, such that FreeRTOS wakes us up at the deadline of the next runnable to schedule.