OS  v1.7.5
Documentation
Loading...
Searching...
No Matches
Events

Retrieving the event data

In the QuarkTS++ OS, tasks can be triggered from multiple event sources including time-elapsed, notifications, queues and event-flags. This can lead to several situations that must be handled by the application writer from the task context, for example:

  • What is the event source that triggers the task execution?
  • How to get the event-associated data?
  • What is the task execution status?

The OS provides a simple approach for this, a class with all the regarding information of the task execution. This class, that is already defined in the callback function as the qOS::event_t argument, is filled by the kernel dispatcher, so the application writer only needs to read the fields inside.

This class has the following attributes and methods:

class event_t {
trigger getTrigger( void );
void *TaskData;
void *EventData;
bool firstCall( void );
bool firstIteration( void );
bool LastIteration( void );
clock_t startDelay( void );
task& self( void );
};

Please review the qOS::event_t class reference for more details.

The Time-Elapsed event

Running tasks at pre-determined rates is desirable in many situations, like sensory data acquisition, low-level servoing, control loops, action planning and system monitoring. As previously explained in Adding tasks to the scheme, you can schedule tasks at any interval your design demands, at least, if the time specification is lower than the scheduler tick. When an application consists of several periodic tasks with individual timing constraints, a few points must be taken:

  • When the time interval of a certain task has elapsed, the scheduler triggers the byTimeElapsed event that put the task in a READY state (see figure below).
  • If a task has a finite number of iterations, the scheduler will disable the task when the number of iterations reaches the programmed value.
  • Tasks always have an inherent time lag that can be noticed even more, when the programmed time interval is too low (see figure below). In a real-time context, it is important to reduce this time lag or jitter, to an acceptable level for the application.
Remarks
QuarkTS++ uses the TTA approach to trigger time-elapsed events as explained here Timing Approach
Note
QuarkTS++ can generally meet a time deadline if you use lightweight code in the callbacks and there is a reduced pool of pending tasks, so it can be considered a soft real-time scheduler, however, it cannot meet a deadline deterministically like a hard real-time OS.
timeelapsed
Inherent time lag
  • The most significant delay times are produced inside the callbacks. As mentioned before, use short efficient callback methods written for cooperative scheduling.
  • If two tasks have the same time interval, the scheduler executes first, the task with the highest priority value (see figure below).
prioschedexample
Priority scheduling example with three (3) tasks attached, all triggered by time-elapsed events

Asynchronous events and inter-task communication

Applications existing in heavy environments require tasks and ISR interacting with each other, forcing the application to implement some event model. Here, we understand events, as any identifiable occurrence that has significance for the embedded system. As such, events include changes in hardware, user-generated actions or messages coming from components of the application itself.

heavycoop
Heavy cooperative environment

As shown in the figure above, two main scenarios are presented, ISR-to-task and task-to-task interaction.

When using interrupts to catch external events, it is expected to be handled with fast and lightweight code to reduce the variable ISR overhead introduced by the code itself. If too much overhead is used inside an ISR, the system will tend to lose future events. In some specific situations, in the interest of stack usage predictability and to facilitate system behavioral analysis, the best approach is to synchronize the ISR with a task to leave the heavy job in the base level instead of the interrupt level, so the interrupt handler only collect event data and clear the interrupt source and therefore exit promptly by deferring the processing of the event data to a task, this is also called Deferred Interrupt Handling.

The other scenario is when a task is performing a specific job and another task must be awakened to perform some activities when the other task finishes.

Both scenarios require some ways in which tasks can communicate with each other. For this, the OS does not impose any specific event processing strategy to the application designer but does provide features that allow the chosen strategy to be implemented in a simple and maintainable way. From the OS perspective, these features are just sources of asynchronous events with specific triggers and related data.

The OS provides the following features for inter-task communication:

Notifications

The notifications allow tasks to interact with other tasks and to synchronize with ISRs without the need of intermediate variables or separate communication objects. By using notifications, a task or ISR can launch another task sending an event and related data to the receiving task. This is depicted in the figure below.

notification
A notification used to send an event directly from one task to another

Simple Notifications

Each task node has a 32-bit notification value which is initialized to zero when a task is added to the scheme. The method qOS::core::notify() with mode = qOS::notifyMode::SIMPLE with is used to send an event directly updating the receiving task's notification value increasing it by one. As long as the scheduler sees a non-zero value, the task will be changed to a READY state and eventually, the dispatcher will launch the task according to the execution chain. After being served, the notification value is later decreased.

Note
Sending simple notifications using qOS::core::notify() is interrupt-safe, however, this only catches one event per task because the method overwrites the associated data.

Queued Notifications

If the application notifies multiple events to the same task, queued notifications are the right solution instead of using simple notifications.

Here, the qOS::core::notify() with mode = qOS::notifyMode::QUEUED take advantage of the scheduler FIFO priority-queue. This kind of queue, is somewhat similar to a standard queue, with an important distinction: when a notification is sent, the task is added to the queue with the corresponding priority level, and will be later removed from the queue with the highest priority task first. That is, the tasks are (conceptually) stored in the queue in priority order instead of the insertion order. If two tasks with the same priority are notified, they are served in the FIFO form according to their order inside the queue. The figure below illustrates this behavior.

prioqueuebehav
Priority-queue behavior

The scheduler always checks the queue state first, being this event the one with more precedence among the others. If the queue has elements, the scheduler algorithm will extract the data and the corresponding task will be launched with the trigger flag set in byNotificationQueued.

The next figure, shows a cooperative environment with five tasks. Initially, the scheduler activates Task-E, then, this task enqueues data to Task-A and Task-B respectively using the qOS::core::notify() using the qOS::notifyMode::QUEUED mode. In the next scheduler cycle, the scheduler realizes that the priority-queue is not empty, generating an activation over the task located at the beginning of the queue. In this case, Task-A will be launched and its respective data will be extracted from the queue. However, Task-A also enqueues data to Task-C and Task-D. Following the priority-queue behavior, the scheduler makes a new reordering, so the next queue extraction will be for Task-D, Task-C, and Task-B sequentially.

notifyqueue++
Priority-queue example
Note
Any queue extraction involves an activation of the receiving task. The extracted data will be available inside the qOS::event_t class.
Remarks
Among all the provided events, queued notifications have the highest precedence.

Sending notifications

The kernel handles all the notifications by itself (simple or queued), so intermediate objects are not needed. Just calling qOS::core::notify() is enough to send notifications. After the task callback is invoked, the notification is cleared by the dispatcher. Here the application writer must read the respective fields of the event-data class to check the received notification.

The next example shows an ISR to task communication. Two interrupts send notifications to a single task with specific event data. The receiver task taskA after further processing, send an event to taskB to handle the event generated by the transmitter taskA.

#include "HAL.h" /*hardware dependent code*/
#include "QuarkTS.h"
using namespace qOS;
task_t taskA, taskB;
void taskA_Callback( event_t e );
void taskB_Callback( event_t e );
const char *app_events[] = {
"Timer1seg",
"ButtonRisingEdge",
"ButtonFallingEdge",
"3Count_ButtonPush"
};
/*==================================================================*/
void interrupt Timer1Second_ISR( void) {
os.notify( notifyMode::SIMPLE, taskA );
HAL_ClearInterruptFlags( HAL_TMR_ISR ); /*hardware dependent code*/
}
/*==================================================================*/
void interrupt ExternalInput_ISR( void ) {
if ( RISING_EDGE == HAL_GetInputEdge() ) { /*hardware dependent code*/
os.notify( notifyMode::QUEUED, taskA, app_events[1] );
}
else {
os.notify( notifyMode::QUEUED, taskA, app_events[2] );
}
HAL_ClearInterruptFlags( HAL_EXT_ISR ); /*hardware dependent code*/
}
/*==================================================================*/
void taskA_Callback( event_t e ) {
static int press_counter = 0;
switch ( e.getTrigger() ) { /*check the source of the event*/
case trigger::byNotificationSimple:
/*
* Do something here to process the timer event
*/
break;
case trigger::byNotificationQueued:
/*here, we only care about the Falling Edge events*/
if ( 0 == strcmp( e.EventData, "ButtonFallingEdge" ) ) {
press_counter++; /*count the button press*/
if ( 3 == press_counter ) { /*after 3 presses*/
/*send the notification of 3 presses to taskB*/
os.notify( notifyMode::SIMPLE, taskB, app_events[3] );
press_counter = 0;
}
}
break;
default:
break;
}
}
/*==================================================================*/
void taskB_Callback( event_t e ) {
if ( trigger::byNotificationSimple == e.getTrigger() ) {
/*
* we can do more here, but this is just an example,
* so, this task will only print out the received
* notification event.
*/
trace::log << static_cast<char>( e->EventData ) << trace::endl;
}
}
/*==================================================================*/
int main( void ) {
HAL_Setup_MCU(); /*hardware dependent code*/
trace::setOutputFcn( HAL_OutPutChar );
/* setup the scheduler to handle up to 10 queued notifications*/
os.init( HAL_GetTick, nullptr );
os.add( taskA, taskA_Callback, core::LOWEST_PRIORITY );
os.add( taskB, taskB_Callback, core::LOWEST_PRIORITY );
os.run();
return 0;
}
bool run(void) noexcept
Executes the scheduling scheme. It must be called once after the task pool has been defined.
bool add(task &Task, taskFcn_t callback, const priority_t p, const duration_t t, const iteration_t n, const taskState s=taskState::ENABLED_STATE, void *arg=nullptr) noexcept
Add a task to the scheduling scheme. The task is scheduled to run every t time units,...
bool init(const getTickFcn_t tFcn=nullptr, taskFcn_t callbackIdle=nullptr) noexcept
Task Scheduler initialization. This core method is required and must be called once in the applicatio...
The task argument with all the regarding information of the task execution.
Definition task.hpp:105
bool notify(notifyMode mode, task &Task, void *eventData=nullptr) noexcept
Sends a notification generating an asynchronous event. If mode = notifyMode::SIMPLE,...
core & os
The predefined instance of the OS kernel interface.
OS/Kernel interfaces.
Definition bytebuffer.hpp:7

Spread a notification

In some systems, we need the ability to broadcast an event to all tasks. This is often referred to as a barrier. This means that a group of tasks should stop activities at some point and cannot proceed until another task or ISR raise a specific event. For this kind of implementation, we can also use the qOS::core::notify() method but in this case, without specifying a target task.

Note
In this mode, qOS::core::notify() spreads a notification event among all the tasks in the scheduling scheme, so, for tasks that are not part of the barrier, just discard the notification. This operation will be performed in the next scheduling cycle.

Queues

A queue is a linear data structure with simple operations based on the FIFO (First In First Out) principle. It is capable to hold a finite number of fixed-size data items. The maximum number of items that a queue can hold is called its length. Both the length and the size of each data item are set when the queue is created.

queues
Queues conceptual representation

As shown above, the last position is connected back to the first position to make a circle. It is also called ring-buffer or circular-queue.

In general, this kind of data structure is used to serialize data between tasks, allowing some elasticity in time. In many cases, the queue is used as a data buffer in interrupt service routines. This buffer will collect the data so, at some later time, another task can fetch the data for further processing. This use case is the single "task to task" buffering case. There are also other applications for queues as serializing many data streams into one receiving stream (multiple tasks to a single task) or vice-versa (single task to multiple tasks).

Note
The OS uses the queue by copy method. Queuing by copy is considered to be simultaneously more powerful and simpler to use than queuing by reference.

Queuing by copy does not prevent the queue from also being used to queue by reference. For example, when the size of the data being queued makes it impractical to copy the data into the queue, then a pointer to the data can be copied into the queue instead.

Setting up a queue

A queue must be explicitly initialized before it can be used. These objects are referenced by the class qOS::queue. Both, the constructor or the qOS::queue::setup() method can be used to configure the queue and initialize the instance.

The required RAM for the queue data should be provided by the application writer and could be statically allocated at compile time or in run-time using the Memory Management extension.

Attach a queue to a task

Additional features are provided by the kernel when the queues are attached to tasks; this allows the scheduler to pass specific queue events to it, usually, states of the object itself that needs to be handled, in this case by a task. For this, use the qOS::task::attachQueue() method.

The following attaching modes are provided:

Note
For the qOS::queueLinkMode::QUEUE_RECEIVER mode, data from the front of the queue will be received automatically in every trigger, this involves a data removal after the task is served. During the respective task execution, the qOS::event_t::EventData field of the qOS::event_t class will be pointing to the extracted data. For the other modes, the qOS::event_t::EventData field will point to the queue that triggered the event.

A queue example

This example shows the usage of QuarkTS++ queues. The application is the classic producer/consumer example. The producer task puts data into the queue. When the queue reaches a specific item count, the consumer task is triggered to start fetching data from the queue. Here, both tasks are attached to the queue.

#include "QuarkTS.h"
using namespace qOS;
/*-----------------------------------------------------------------------*/
void interrupt Timer0_ISR( void ) {
clock::sysTick();
}
/*-----------------------------------------------------------------------*/
task tProducer, tConsumer; /*task nodes*/
queue userQueue; /*Queue Handle*/
/*-----------------------------------------------------------------------*/
/* The producer task puts data into the buffer if there is enough free
* space in it, otherwise the task blocks itself and wait until the queue
* is empty to resume. */
void tProducer_Callback( event_t e ) {
static uint16_t unData = 0;
unData++;
/*Queue is empty, enable the producer if it was disabled*/
if ( byQueueEmpty == e.getTrigger() ) {
e.thisTask().resume();
}
/*send data to the queue*/
if ( false == UserQueue.send( &unData ) ) {
/*
* if the data insertion fails, the queue is full
* and the task disables itself
*/
e.thisTask().suspend();
}
}
/*-----------------------------------------------------------------------*/
/* The consumer task gets one element from the queue.*/
void tConsumer_Callback( event_t e ) {
uint16_t unData;
queue *ptrQueue; /*a pointer to the queue that triggers the event*/
if ( byQueueCount == e.getTrigger() ) {
ptrQueue = (queue_t *)e.EventData;
ptrQueue->receive( &unData );
return;
}
}
/*-----------------------------------------------------------------------*/
void IdleTask_Callback( event_t e ) {
/*nothing to do...*/
}
/*-----------------------------------------------------------------------*/
int main( void ) {
uint8_t BufferMem[ 16*sizeof(uint16_t) ] = { 0u };
HardwareSetup(); //hardware-specific code
/* next line is used to set up hardware with specific code to fire
* interrupts at 1ms - timer tick*/
Configure_Periodic_Timer0_Interrupt_1ms();
os.init( nullptr, IdleTask_Callback );
/*Setup the queue*/
userQueue.setup<uint16_t>( BufferMem, 16 );
/* Append the producer task with 100mS rate. */
os.add( tProducer, tProducer_Callback, core::MEDIUM_PRIORITY,
100_ms, task::PERIODIC, taskState::ENABLED_STATE, "producer" );
/* Append the consumer as an event task. The consumer will
* wait until an event trigger their execution
*/
os.add( tConsumer, tConsumer_Callback, core::MEDIUM_PRIORITY, "consumer" );
/* the queue will be attached to the consumer task
* in Count mode. This mode sends an event to the consumer
* task when the queue fills to a level of 4 elements
*/
tConsumer.attachQueue( userQueue, queueLinkMode::QUEUE_COUNT, 4 );
/* the queue will be attached to the producer task in
* queueLinkMode = QUEUE_EMPTY mode. This mode sends an event to the producer
* task when the queue is empty
*/
tProducer.attachQueue( userQueue, queueLinkMode::QUEUE_EMPTY );
os.run();
return 0;
}
A Queue object.
Definition queue.hpp:44
bool receive(void *dst) noexcept
Receive an item from a queue (and removes it). The item is received by copy so a buffer of adequate s...
bool setup(void *pData, const size_t size, const size_t count) noexcept
Configures a Queue. Here, the RAM used to hold the queue data pData is statically allocated at compil...
A task node object.
Definition task.hpp:348
bool attachQueue(queue &q, const queueLinkMode mode, const size_t arg=1U) noexcept
Attach a queue to the Task.

Event Flags

Every task node has a set of built-in event bits called Event-Flags, which can be used to indicate if an event has occurred or not. They are somewhat similar to signals, but with greater flexibility, providing a low-cost, but flexible means of passing simple messages between tasks. One task can set or clear any combination of event flags. Another task may read the event flag group at any time or may wait for a specific pattern of flags.

eventflags
Task event flags

Up to twenty(20) bit-flags are available per task and whenever the scheduler sees that one event-flag is set, the kernel will trigger the task execution.

Note
The scheduler will put the task into a READY state when any of the available event-flags is set. The flags should be cleared by the application writer explicitly

Using the task Event-flags

This example demonstrates the usage of Event-flags. The idle task will transmit data generated from another task, only when the required conditions are met, including two events from an ISR (A timer expiration and the change of a digital input) and when a new set of data is generated. The task that generates the data should wait until the idle task transmission is done to generate a new data set.

#include "QuarkTS.h"
using namespace qOS;
/*event flags application definitions */
#define SWITCH_CHANGED EVENT_FLAG(1)
#define TIMER_EXPIRED EVENT_FLAG(2)
#define DATA_READY EVENT_FLAG(3)
#define DATA_TXMIT EVENT_FLAG(4)
task TaskDataProducer;
uint8_t dataToTransmit[ 10 ] = { 0 };
/*-----------------------------------------------------------------------*/
void interrupt Timer0_ISR( void ) {
clock::sysTick();
}
/*-----------------------------------------------------------------------*/
void interrupt Timer1_ISR( void ) {
os.eventFlagsModify( TaskDataProducer, TIMER_EXPIRED, true );
}
/*-----------------------------------------------------------------------*/
void interrupt EXTI_ISR( void ) {
if ( EXTI_IsRisingEdge() ) {
os.eventFlagsModify( TaskDataProducer, SWITCH_CHANGED, true );
}
}
/*-----------------------------------------------------------------------*/
void TaskDataProducer_Callback( event_t e ) {
bool condition;
condition = os.eventFlagsCheck( TaskDataProducer, DATA_TXMIT, true, true );
if ( true == condition) {
GenerateData( dataToTransmit );
os.eventFlagsModify( TaskDataProducer, DATA_READY, true );
}
os.eventFlagsCheck( TaskDataProducer, DATA_READY | SWITCH_CHANGED | TIMER_EXPIRED, true, true );
}
/*-----------------------------------------------------------------------*/
void IdleTask_Callback( event_t e ) {
TransmitData( dataToTransmit );
os.eventFlagsModify( &TaskDataProducer, DATA_TXMIT, true );
}
/*-----------------------------------------------------------------------*/
int main( void ) {
HardwareSetup(); //hardware-specific code
/* next line is used to set up hardware with specific code to fire
* interrupts at 1ms - timer tick*/
Configure_Periodic_Timer0_Interrupt_1ms();
Configure_Periodic_Timer1_Interrupt_2s();
Configure_External_Interrupt();
/* Idle task will be responsible to transmit the generate the data after
* all conditions are met
*/
os.init( nullptr, IdleTask_Callback );
/* The task will wait until data is transmitted to generate another set of
* data */
os.add( TaskDataProducer, TaskDataProducer_Callback, core::HIGHEST_PRIORITY, "DATAPRODUCER" );
/* Set the flag DATA_TXMIT as the initial condition to allow the data
* generation at startup
*/
os.eventFlagsModify( TaskDataProducer, DATA_TXMIT, true );
os.run();
for ( ;; ) {}
return 0;
}
bool eventFlagsCheck(task &Task, taskFlag_t flagsToCheck, const bool clearOnExit=true, const bool checkForAll=false) noexcept
Check for flags set to true inside the task Event-Flags.
bool eventFlagsModify(task &Task, const taskFlag_t tFlags, const bool action) noexcept
Modify the EventFlags for the provided task.