Initialization and Task Creation
Let’s first recap how the user initializes the system and declares task as described in the user documentation. Take a look at the very simple example shown below. Based on this, we will walk through the initialization sequence and how it is actually implemented.
1enum TASK
2{
3 TASK_EXAMPLE = 1,
4 ASYNC_CONFIG_TASK_COUNT,
5};
6
7::async::AsyncBinding::AdapterType::IdleTask<1024 * 2> IDLE_TASK {"idle"};
8::async::AsyncBinding::AdapterType::TimerTask<1024 * 2> TIME_TASK {"timer"};
9::async::AsyncBinding::AdapterType::Task<TASK_EXAMPLE, 1024 * 2> EXAMPLE_TASK {"example"};
10
11void startApp()
12{
13 ::async::scheduleAtFixedRate(TASK_EXAMPLE, ...);
14}
15
16int main()
17{
18 ::async::AsyncBinding::AdapterType::run(
19 ::async::AsyncBinding::AdapterType::StartAppFunctionType::create<&startApp>());
20}
In line 9 we specify the task example along with the mandatory idle and timer tasks. It is
declared as a global variable and is uniquely identified by an integer id, in our case the enum
value TASK_EXAMPLE
. For each declared task we create during initialization of the system a
native FreeRTOS task, into which we can schedule runnables using the known async API. In this
example we schedule a cyclically executed runnable in line 13.
From usage perspective this is straight forward, but how do we get from the globally declared
variables to the FreeRTOS tasks created during initialization? The key idea is implemented within
the TaskInitializer
class, which inherits from the StaticRunnable
class. Don’t get
confused by the name Runnable here, it has nothing to do with the runnables we later schedule
through the async API. As you can see below, we call the TaskInitializer::create()
method
from within our Task
constructor which is executed during static initialization of our
global Task
objects.
TaskImpl<Adapter, Context, StackSize>::TaskImpl(...)
{
TaskInitializer<Adapter>::create(Context, name, _task, _stack, taskFunction, taskConfig);
}
Within the TaskInitializer::create
method we allocate a task initializer object storing all
the necessary data which is needed to create a FreeRTOS task, for example a pointer to the stack
memory, the name of the task, … During initialization at runtime, we then iterate over these task
initializer objects and create a task for each one of it. But how do we iterate over these objects?
The secret for this lies within the StaticRunnable
implementation which constructs a linked
list and allows to iterate over it using the StaticRunnable::run
.
StaticRunnable<T>::StaticRunnable() : _next(_first)
{
_first = static_cast<T*>(this);
}
In summary, we create a linked list of task initializer objects during the static initialization of our globally declared task variables. Great, this already answers a lot of questions on how the declared tasks are created at runtime. There is just one more interesting implementation detail to look at, namely where we allocate our task initializer objects. Since we only need them a single time during initialization but never again during runtime, it would be inefficient to permanently allocate memory for them, e.g. within the task objects. Therefore, we utilize the anyway allocated stack memory of the task to place our task initializer objects into as shown below.
void TaskInitializer<Adapter>::create(...)
{
new (stackSlice.data()) TaskInitializer(...);
}
And that’s all that happens during the initialization. The user declares global variables with the
wanted tasks which are put into a linked list during static initialization. At runtime we iterate
over this list, create FreeRTOS tasks using xTaskCreateStatic
and maintain a lookup within
the static _taskContexts
array which can be indexed by the integer id. Finally, the system
is started using vTaskStartScheduler
and the FREERTOS_TASKS_C_ADDITIONS_INIT
hook is
called, in which we execute the user provided startApp
function.