OS  v7.3.3
Documentation
Co-Routines

Overview

A task coded as a Co-Routine, is just a task that allows multiple entry points for suspending and resuming execution at certain locations, this feature can bring benefits by improving the task cooperative scheme and providing a linear code execution for event-driven systems without complex state machines or full multithreading.

coroutines
Coroutines in QuarkTS

The QuarkTS implementation uses the Duff's device approach, and is heavily inspired by the Knuth method, Simon Tatham's Co-Routines in C and Adam Dunkels Protothreads . This means that a local-continuation variable is used to preserve the current state of execution at a particular place of the Co-Routine scope but without any call history or local variables. This brings benefits to lower RAM usage, but at the cost of some restrictions on how a Co-routine can be used.

Limitations and restrictions

  • The stack of a Co-Routine is not maintained when a yield is performed. This means variables allocated on the stack will loose their values. To overcome this, a variable that must maintain its value across a blocking call must be declared as static.
  • Calls to API functions that could cause the Co-Routine to block, can only be made from the Co-Routine function itself - not from within a function called by the Co-Routine.
  • The implementation does not permit yielding or blocking calls to be made from within a switch statement.

Coding a Co-Routine

The application writer just needs to create the body of the Co-Routine . This means starting a Co-Routine segment with qCR_Begin and end with qCR_End statement . From now on, yields and blocking calls from the Co-Routine scope are allowed.

void CoroutineTask_Callback( qEvent_t e ) {
if ( EventNotComing() ) {
}
DoTheEventProcessing();
qCR_Delay( WAIT_TIME_S );
PerformActions();
}
#define qCR_End
Ends a Coroutine segment. Only one segment is allowed inside a task. The qCR_End statement is used to...
Definition: qcoroutine.h:259
#define qCR_Yield
This statement is only allowed inside a Coroutine segment. qCR_Yield return the CPU control back to t...
Definition: qcoroutine.h:268
#define qCR_Delay(tValue)
Delay a coroutine for a given number of time.
Definition: qcoroutine.h:410
#define qCR_Begin
Defines a Coroutine segment. The qCR_Begin statement is used to declare the starting point of a Corou...
Definition: qcoroutine.h:222
The task argument with all the regarding information of the task execution.
Definition: qtasks.h:162

The qCR_Begin statement should be placed at the start of the task function in which the Co-routine runs. All C statements above the qCR_Begin will be executed as if they were in an endless-loop each time the task is scheduled.

A qCR_Yield statement return the CPU control back to the scheduler but saving the execution progress, thereby allowing other processing tasks to take place in the system. With the next task activation, the Co-Routine will resume the execution after the last qCR_Yield statement.

Note
Co-Routine statements can only be invoked from the scope of the Co-Routine.
Remarks
Do not use an endless-loop inside a Co-routine ,this behavior it's already hardcoded within the segment definition.

Blocking calls in a Co-routine

Blocking calls inside a Co-Routine should be made with the provided statements, all of them with a common feature: an implicit yield.

A widely used procedure is to wait for a fixed period of time. For this, the qCR_Delay should be used. This statement makes an apparent blocking over the application flow, but to be precise, a yield is performed until the requested time expires, this allows other tasks to be executed until the blocking call finish. This "yielding until condition meet" behavior its the common pattern among the other blocking statements.

Another common blocking call is qCR_WaitUntil. This statement takes a condition argument, a logical expression that will be performed when the Co-Routine resumes their execution. As mentioned before, this type of statement exposes the expected behavior, yielding until the condition is met.

An additional wait statement qCR_TimedWaitUntil is also provided. This one sets a timeout for the logical condition to be met, with a similar behavior of qCR_WaitUntil.

Optionally, the Do-Until structure gives to application writer the ability to perform a multi-line job before the yield, allowing more complex actions to being performed after the Co-Routine resumes:

/* Job : a set of instructions*/
} qCR_Until( Condition );
#define qCR_Do
This statement start a blocking Job segment.
Definition: qcoroutine.h:317
#define qCR_Until(bCondition)
This statement ends a blocking Job segment starting with the qCR_Do statement.
Definition: qcoroutine.h:334

Co-Routine usage example

void Sender_Task( qEvent_t e ) {
static qSTimer_t timeout;
Send_Packet();
/* Wait until an acknowledgment has been received, or until
* the timer expires. If the timer expires, we should send
* the packet again.
*/
qSTimer_Set( &timeout, TIMEOUT_TIME );
qCR_WaitUntil( PacketACK_Received() || qSTimer_Expired(&timeout));
}
/*===================================================================*/
void Receiver_Task( qEvent_t e ) {
/* Wait until a packet has been received*/
qCR_WaitUntil( Packet_Received() );
Send_Acknowledgement();
}
#define qCR_WaitUntil(bCondition)
Yields until the logical condition being true.
Definition: qcoroutine.h:288
qBool_t qSTimer_Expired(const qSTimer_t *const t)
Non-Blocking STimer check.
Definition: qstimers.c:55
qBool_t qSTimer_Set(qSTimer_t *const t, const qTime_t tTime)
Set the expiration time for a STimer. On success, the STimer gets armed immediately.
Definition: qstimers.c:22
A STimer(Software Timer) object.
Definition: qstimers.h:32

Positional jumps

This feature provides positional local jumps, control flow that deviates from the usual Co-Routine call.

The complementary statements qCR_PositionGet and qCR_PositionRestore provide this functionality. The first one saves the Co-Routine state at some point of their execution into CRPos, a variable of type qCR_Position_t , that can be used at some later point of program execution by qCR_PositionRestore to restore the Co-Routine state to the one saved by qCR_PositionGet into CRPos. This process can be imagined to be a "jump" back to the point of program execution where qCR_PositionGet saved the Co-Routine environment.

Semaphores

This extension implements counting semaphores on top of Co-Routines. Semaphores are a synchronization primitive that provide two operations: wait and signal. The wait operation checks the semaphore counter and blocks the Co-Routine if the counter is zero. The signal operation increases the semaphore counter but does not block. If another Co-Routine has blocked waiting for the semaphore that is signaled, the blocked Co-Routines will become runnable again.

Semaphores are referenced by handles, a variable of type qCR_Semaphore_t and must be initialized with qCR_SemInit before any usage. Here, a value for the counter is required. Internally, semaphores use an uint32_t to represent the counter, therefore the value argument should be within range of this data-type.

To perform the |a wait operation, the qCR_SemWait statement should be used. The wait operation causes the Co-routine to block while the counter is zero. When the counter reaches a value larger than zero, the Co-Routine will continue.

Finally, qCR_SemSignal carries out the signal operation on the semaphore. This signaling increments the counter inside the semaphore, which eventually will cause waiting Coroutines to continue executing.

Co-Routine example with semaphores.

The following example shows how to implement the bounded buffer problem using Co-Routines and semaphores. The example uses two tasks: one that produces items and other that consumes items.

Note that there is no need for a mutex to guard the add_to_buffer() and get_from_buffer() functions because of the implicit locking semantics of Co-Routines, so it will never be preempted and will never block except in an explicit qCR_SemWait statement.

#include "HAL.h"
#include "QuarkTS.h"
#include "AppLibrary.h"
#define NUM_ITEMS ( 32 )
#define BUFSIZE ( 8 )
qTask_t ProducerTask, ConsumerTask;
qCR_Semaphore_t mutex, full, empty;
/*===================================================================*/
void ProducerTask_Callback( qEvent_t e ) {
static int produced;
for ( produced = 0 ; produced < NUM_ITEMS ; ++produced ) {
qCR_SemWait( &full );
qCR_SemWait( &mutex );
add_to_buffer( produce_item() );
qCR_SemSignal( &mutex );
qCR_SemSignal( &empty );
}
}
/*===================================================================*/
void ConsumerTask_Callback( qEvent_t e ) {
static int consumed;
for ( consumed = 0 ; consumed < NUM_ITEMS ; ++consumed ) {
qCR_SemWait( &empty );
qCR_SemWait( &mutex );
consume_item( get_from_buffer() );
qCR_SemSignal( &mutex );
qCR_SemSignal( &full );
}
}
/*===================================================================*/
void IdleTask_Callback( qEvent_t e ) {
/*nothing to do*/
}
/*===================================================================*/
int main(void) {
HAL_Init();
qOS_Setup( HAL_GetTick, 0.001f, IdleTask_Callback );
qCR_SemInit( &empty, 0 );
qCR_SemInit( &full, BUFSIZE );
qCR_SemInit( &mutex, 1 );
qOS_Add_Task( &ProducerTask, ProducerTask_Callback,
qOS_Add_Task( &ConsumerTask, ConsumerTask_Callback,
return 0;
}
#define qCR_SemSignal(pSem)
Carries out the "signal" operation on the semaphore. The signal operation increments the counter insi...
Definition: qcoroutine.h:371
#define qCR_SemWait(pSem)
Carries out the "wait" operation on the semaphore. The wait operation causes the Co-routine to block ...
Definition: qcoroutine.h:358
#define qCR_SemInit(pSem, sValue)
Initializes a semaphore with a value for the counter. Internally, the semaphores use an "unsigned int...
Definition: qcoroutine.h:346
#define qMedium_Priority
A macro directive to indicate the medium priority level.
Definition: qkernel.h:74
qBool_t qOS_Add_Task(qTask_t *const Task, qTaskFcn_t callbackFcn, const qPriority_t p, const qTime_t t, const qIteration_t n, const qState_t init, void *arg)
Add a task to the scheduling scheme. The task is scheduled to run every t seconds,...
Definition: qkernel.c:411
qBool_t qOS_Run(void)
Executes the scheduling scheme. It must be called once after the task pool has been defined.
Definition: qkernel.c:676
qBool_t qOS_Setup(const qGetTickFcn_t tFcn, const qTimingBase_t t, qTaskFcn_t idleCallback)
Task Scheduler Setup. This function is required and must be called once in the application main threa...
Definition: qkernel.c:126
#define qPeriodic
A directive indicating that the task will run every time its timeout has expired.
Definition: qkernel.h:80
#define qEnabled
A state value that enables a task.
Definition: qtypes.h:167
A typedef to instantiate a Co-Routine Semaphore.
Definition: qcoroutine.h:45
A task node object.
Definition: qtasks.h:268

External control

In certain situations, it may be necessary to control the flow of execution outside of the segment that defines the Co-routine itself. This is typically done to defer or resume the Co-routine in response to specific occurrences that arise in other contexts, such as tasks or interrupts.

To address these specific scenarios, a handler for the Co-routine must be defined, which is a variable of type qCR_Handle_t. Additionally, to initiate the scope of the target Co-routine, the statement qCR_BeginWithHandle should be used instead of qCR_Begin.

qCR_Handle_t xHandleCR = NULL; /*NULL initialization are strictly necessary*/
/*===================================================================*/
void AnotherTask_Callback( qEvent_t e) {
int UserInput = 0;
if ( e->FirstIteration ) {
qCR_ExternControl( xHandleCR, qCR_RESUME, 0 );
}
if ( e->LastIteration ) {
qCR_ExternControl( xHandleCR, qCR_SUSPEND, 0 );
}
UserInput = GetTerminalInput( );
if ( UserInput == USR_RESTART ) {
qCR_ExternControl( xHandleCR, qCR_RESTART, 0 );
}
Perform_AnotherTask_Activities();
}
/*===================================================================*/
void CoroutineTask_Callback( qEvent_t e ) {
qCR_BeginWithHandle( xHandleCR ) { /*externally controlled*/
if ( EventNotComing() ) {
}
RunFirstJob();
qCR_Delay( WAIT_TIME );
SecondJobStatus = RunSecondJob();
qCR_TimedWaitUntil( JobFlag == JOB_SUCCESS, JOB_TIMEOUT );
CleanUpStatus = CleanupJob();
qCR_WaitUntil( SomeVar > SomeValue );
}
#define qCR_TimedWaitUntil(bCondition, tValue)
Yields until the logical condition being true or the specified timeout expires.
Definition: qcoroutine.h:303
qBool_t qCR_ExternControl(qCR_Handle_t h, const qCR_ExternAction_t action, const qCR_ExtPosition_t pos)
Perform an external action over the requested Co-routine.
Definition: qcoroutine.c:10
#define qCR_BeginWithHandle(handle)
Defines a Coroutine segment with a supplied external handle. The qCR_BeginWithHandle statement is use...
Definition: qcoroutine.h:241
_qCR_Instance_t * qCR_Handle_t
A typedef to instantiate a Co-Routine handle.
Definition: qcoroutine.h:42
@ qCR_RESUME
Definition: qcoroutine.h:67
@ qCR_SUSPEND
Definition: qcoroutine.h:66
@ qCR_RESTART
Definition: qcoroutine.h:64
qBool_t FirstIteration
Indicates whether current pass is the first iteration of the task. This flag will be only set when ti...
Definition: qtasks.h:193
qBool_t LastIteration
Indicates whether current pass is the last iteration of the task. This flag will be only set when tim...
Definition: qtasks.h:201

As shown in the code snippet above, the Co-routine handle its globally declared to enable other contexts to access it. The example demonstrates how another task can control the Coroutine using the qCR_ExternControl() API. It's important to note that the actions performed by this API can only be effective after the handle instantiation, which is a one-time operation that occurs during the first call of the Co-routine.

Note
A NULL initialization its mandatory on qCR_Handle_t variables. Undefined behavior may occur if this step is ignored.