OS
v7.3.3
Documentation
|
In this pattern each task runs until it finishes or explicitly yields control back to the scheduler. Events are serviced by the kernel in the scheduler loop. If an event is available, it is posted to the task based on the scheduling rules, and then the dispatcher triggers the task. The task executes the actions associated with that event and then returns to the scheduler.
This pattern means that in the absence of exceptions or asynchronous destruction of the task execution, a pending event occurrence is dispatched only after the processing of the previous occurrence is completed, and a stable task-scheme configuration has been reached. That is, an event occurrence will never be dispatched while the task execution is busy processing the previous one. This behavioral paradigm was chosen to avoid complications arising from concurrency conflicts that may occur when a task tries to respond to multiple concurrent or overlapping events. This also provides better power efficiency. In the absence of events, the developer can use CPU low-power modes and make the core active only during the execution of a RTC step.
The provided features enables you to implement task procedures that does not have to execute all the way down to the last line of code. Instead execution can be broken down into different entities by using Finite State Machines (FSM) or by using Co-Routines where the procedure execution can be suspended and resumed at defined locations.
The kernel implements a Time-Triggered Architecture (TTA) , in which the tasks are triggered by comparing the corresponding task-time with a reference clock. The reference clock must be real-time and follow a monotonic behavior. Usually, all embedded systems can provide this kind of reference with a constant tick generated by a periodic background hardware-timer, typically, at 1Khz
or 1mS
tick.
For this, the kernel allows you to select the reference clock source among these two scenarios:
The initialization and configuration of QuarkTS through qOS_Setup() will allow you to set the reference clock source in addition to specifying Idle task activities.
Usage example:
Like many operating systems, the basic unit of work is the task. Tasks can perform certain functions, which could require periodic or one-time execution, update specific variables or wait for specific events. Tasks also could be controlling specific hardware or be triggered by hardware interrupts. In the QuarkTS OS, a task is seen as a node concept that links together:
The OS uses a Task Control Block (TCB) to represent each task, storing essential information about task management and execution. Part of this information also includes link-pointers that allow it to be part of one of the lists available in the Kernel Control Block (KCB).
Each task performs its activities via a callback function and each of them is responsible for supporting cooperative multitasking by being “good neighbors”, i.e., running their callback methods quickly in a non-blocking way and releasing control back to the scheduler as soon as possible (returning).
Every task node, must be defined using the qTask_t data type and the callback is defined as a function that returns void and takes a qEvent_t data structure as its only parameter (This input argument can be used later to get the event information, see Retrieving the event data).
It's a special task loaded by the OS scheduler when there is nothing else to do (no task in the whole scheme has reached the ready state). The idle task is already hard-coded into the kernel, ensuring that at least, one task is able to run. Additionally, the OS setup this task with the lowest possible priority to ensure that does not use any CPU time if there are higher priority application tasks able to run. The idle task doesn't perform any active functions, but the user can decide if it should perform some activities defining a callback function for it. This could be done at the beginning of the kernel setup. Of course, the callback must follow the same function prototype for tasks.
NULL
should be passed as argument on qOS_Setup() or qOS_Set_IdleTask().After setting up the kernel with qOS_Setup(), the user can proceed to deploy the multitasking application by adding tasks. If the task node and their respective callback is already defined, the task can be added to the scheme using qOS_Add_Task(). This API can schedule a task to run every t
seconds, n
executions times and invoking the callbackFcn
method on every pass.
Caveats:
t
defined as qTimeImmediate
, will always get the qReady
state in every scheduling cycle, as consequence, the idle task will never get dispatched.n
executions argument. After the iterations are done, the internal iteration counter decreases until reaches zero. If another set of iterations is needed, the user should set the number of iterations again and resume the task explicitly.qDisabled
. Asynchronous triggers do not affect the iteration counter.arg
parameter can be used as a storage pointer, so, for multiple data, create a structure with the required members and pass a pointer to that structure.Invoking qOS_Add_Task() is the most generic way to add tasks to the scheme, supporting a mixture of time-triggered and event-triggered tasks, however, additional simplified API functions are also provided to add specific purpose tasks:
An event-triggered task reacts asynchronously to the occurrence of events in the system, such as external interrupts or changes in the available resources.
The API qOS_Add_EventTask() is intended to add this kind of tasks, keeping it in a qSuspended
state. Only asynchronous events followed by their priority value dictate when a task can change to the qRunning
state.
The qOS_Remove_Task() function removes the task from the scheduling scheme. This means the task node will be disconnected from the kernel chain, preventing additional overhead provided by the scheduler when it does checks over it and course, preventing it from running.
Caveats:
Task nodes are variables like any other. They allow your application code to reference a task, but there is no link back the other way and the kernel doesn't know anything about the variables, where the variable is allocated (stack, global, static, etc.) or how many copies of the variable you have made, or even if the variable still exists. So the qOS_Remove_Task() API cannot automatically free the resources allocated by the variable. If the task node has been dynamically allocated, the application writer it's responsible to free the memory block after a removal call.
After preparing the multitasking environment for your application, a call to qOS_Run() is required to execute the scheduling scheme. This function is responsible to run the following OS main components:
The states involved in the interaction between the scheduler and dispatcher are described here.
This functionality must be enabled from the Q_ALLOW_SCHEDULER_RELEASE
macro. This API stops the kernel scheduling. In consequence, the main thread will continue after the qOS_Run() call.
Although producing this action is not a typical desired behavior in any application, it can be used to handle a critical exception.
When used, the release will take place after the current scheduling cycle finishes. The kernel can optionally include a release callback function that can be configured to get called if the scheduler is released. Defining the release callback, will help to take actions over the exception that caused the release action. To perform a release action, the qOS_Set_SchedulerReleaseCallback() API should be used
A task can be in one of the four global states: qRunning
, qReady
, qSuspended
or qWaiting
. Each of these states is tracked implicitly by putting the task in one of the associated kernel lists.
These global states are described below:
qWaiting
: The task cannot run because the conditions for running are not in place.qReady
: The task has completed preparations for running, but cannot run because a task with higher precedence is running.qRunning
: The task is currently being executed.qSuspended
: The task doesn't take part in what is going on. Normally this state is taken after the qRunning
state or when the task does not reach the qReady
state.The presence of a task in a particular list indicates the task's state. There are many ready lists as defined in the Q_PRIORITY_LEVELS
macro. To select the target ready list, the OS uses the user-assigned priority between 0 (the lowest priority) and Q_PRIORITY_LEVELS-1
(the highest priority). For instance, if Q_PRIORITY_LEVELS
is set to 5, then QuarkTS will use 5 priority levels or ready lists: 0 (lowest priority), 1, 2, 3, and 4 (highest priority).
Except for the idle task, a task exists in one of these states. As the real-time embedded system runs, each task moves from one state to another (moving it from one list to another), according to the logic of a simple finite state machine (FSM). The figure above illustrates the typical flowchart used by QuarkTS to handle the task's states, with brief descriptions of the state transitions, additionally you may also notice the interaction between the scheduler and the dispatcher.
The OS assumes that none of the tasks does a block anywhere during the qRunning
state. Based on the round-robin fashion, each ready task runs in turn from every ready list. The developer should take care to monitor their system execution times to make sure during the worst case, when all tasks have to execute, all of the deadlines are still met.
Task precedence is used as the task scheduling rule and precedence among tasks is determined based on the priority of each task. If there are multiple tasks able to run, the one with the highest precedence goes to qRunning
state first.
In determining precedence among tasks of those tasks having different priority levels, that with the highest priority has the highest precedence. Among tasks having the same priority, the one that entered the scheduling scheme first has the highest precedence if the Q_PRESERVE_TASK_ENTRY_ORDER
configuration is enabled, otherwise, the OS will reserves for himself the order according to the dynamics of the kernel lists.
The scheduler also has an order of precedence for incoming events, in this way, if events of different natures converge to a single task, these will be served according to the following flowchart:
Each task has independent operating states from those globally controlled by the scheduler. These states can be handled by the application writer to modify the event flow to the task and consequently, affect the transition to the qReady
global state. These states are described as follows:
qAwake
: In this state, the task is conceptually in an alert mode, handling most of the available events. This operational state is available when the SHUTDOWN
bit is set, allowing the next operational states to be available:qEnabled
: The task can catch all the events. This operational state is available when the ENABLE
bit is set.qDisabled
: In this state, the time events will be discarded. This operational state is available when the ENABLE
bit is cleared.qAsleep
: Task operability is put into a deep doze mode, so the task can not be triggered by the lower precedence events. This operational state is available when the SHUTDOWN
bit is cleared. The task can exit from this operational state when it receives a high precedence event (a queued notification) or using the qTask_Set_State() API.The figure below shows a better representation of how the event flow can be affected by these operational states.
qAsleep
operational state overrides the qEnabled
and qDisabled
states.Since the kernel is non-preemptive, the only critical section that must be handled are the shared resources accessed from the ISR context. Perhaps, the most obvious way of achieving mutual exclusion is to allow the kernel to disable interrupts before it enters their critical section and then, enable interrupts after it leaves its critical section.
By disabling interrupts, the CPU will be unable to change the current context. This guarantees that the currently running job can use a shared resource without another context accessing it. But, disabling interrupts, is a major undertaking. At best, the system will not be able to service interrupts for the time the current job is doing in its critical section, however, in QuarkTS, these critical sections are handled as quickly as possible.
Considering that the kernel is hardware-independent, the application writer should provide the necessary piece of code to enable and disable interrupts.
For this, the qCritical_SetInterruptsED() API should be used. In this way, communication between ISR and tasks using queued notifications or data queues is performed safely.
In some systems, disabling the global IRQ flags is not enough, as they don't save/restore state of interrupt, so here, the qUINT32_t
argument and return value in both functions (Disabler
and Restorer
) becomes relevant, because they can be used by the application writer to save and restore the current interrupt configuration. So, when a critical section is performed, the Disabler
, in addition to disabling the interrupts, returns the current configuration to be retained by the kernel, later when the critical section finish, this retained value is passed to Restorer
to bring back the saved configuration.
Some OS features can be customized using a set of macros located in the header file qconfig.h
. Here is the default configuration, followed by an explanation of each macro:
Q_PRIORITY_LEVELS
: Default: 3
. The number of priorities available for application tasks.Q_SETUP_TIME_CANONICAL
: Default: 0
(disabled). If enabled, the kernel assumes the timing base to 1mS(1KHz). So all time specifications for tasks and STimers must be set in milliseconds(mS). Also can be used to remove the floating-point operations when dealing with time. In some systems, can reduce memory usage.Q_SETUP_TICK_IN_HERTZ
: Default: 0
(disabled). If enabled, the timing base will be taken as frequency(Hz) instead of period(S) by qOS_Setup() (In some systems, can reduce memory usage ).Q_PRIO_QUEUE_SIZE
: Default: 10
. Size of the priority queue for notifications. This argument should be an integer number greater than zero. A zero value can be used to disable this functionality.Q_PRESERVE_TASK_ENTRY_ORDER
: Default: 0
(disabled). If enabled, the kernel will preserve the tasks entry order every OS scheduling cycle.Q_MEMORY_MANAGER
: Default: 1
(enabled). Used to enable or disable the Memory Management extension.Q_BYTE_ALIGNMENT
: Default: 8
. Used by the Memory Management extension to perform the byte alignment.Q_DEFAULT_HEAP_SIZE
: Default: 2048
. The total amount of heap size for the default memory pool.Q_NOTIFICATION_SPREADER
: Default: 0
(disabled). Used to enable or disable the spread notification functionality.Q_FSM
: Default: 1
(enabled). Used to enable or disable the Finite State Machine (FSM) extension.Q_FSM_MAX_NEST_DEPTH
: Default: 5
. The max depth of nesting in Finite State Machines (FSM).Q_FSM_MAX_TIMEOUTS
: Default: 3
. Max number of timeouts inside a timeout specification for the Finite State Machines (FSM) extension.Q_FSM_PS_SIGNALS_MAX
: Default: 8
. Max number of signals to subscribe for a Finite State Machine (FSM).Q_FSM_PS_SUB_PER_SIGNAL_MAX
: Default: 4
. Max number of FSM subscribers per signal.Q_QUEUES
: Default: 1
(enabled). Used to enable or disable the queues APIs for inter-task communication.Q_TRACE_VARIABLES
: Default: 1
(enabled). Used to enable or disable debug and trace macros.Q_DEBUGTRACE_BUFSIZE
: Default: 36
. The buffer size for debug and trace macrosQ_DEBUGTRACE_FULL
: Default: 1
(enabled). Used to enable or disable the extended output for trace macros.Q_ATCLI
: Default: 1
(enabled). Used to enable or disable the AT Command Line Interface (CLI) extension.Q_TASK_COUNT_CYCLES
: Default: 0
(disabled). Used to enable or disable the counting of cycles of a task.Q_TASK_EVENT_FLAGS
: Default: 1
(enabled). Used to enable or disable the task event flags.Q_MAX_FTOA_PRECISION
: Default: 10
. The default precision used to perform float to ASCII conversions.Q_ATOF_FULL
: Default: 0
(disabled). Used to enable or disable the scientific notation in ASCII to float conversions.Q_ALLOW_SCHEDULER_RELEASE
: Default: 0
(disabled). Used to enable or disable the scheduler release functionality.Q_RESPONSE_HANDLER
: Default: 1
(enabled). Used to enable or disable the response handler functionality.Q_EDGE_CHECK_IOGROUPS
: Default: 1
(enabled). Used to enable or disable the edge check functionality for I/O groups.Q_BYTE_SIZED_BUFFERS
: Default: 1
(enabled). Used to enable or disable the usage of Byte-sized buffers.Q_USE_STDINT_H
: Default: 1
(enabled). Use the stdint.h
header to define kernel data types.