OS
v7.3.3
Documentation
|
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.
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
static
.switch
statement.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.
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.
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:
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.
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.
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.
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.
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.
NULL
initialization its mandatory on qCR_Handle_t variables. Undefined behavior may occur if this step is ignored.