Cos.mpas, a Cooperative Operating System.
-- with thanks to Jo Vandale for his "JoCos" that I could use as an example of a COS --
Introduction
An operating system is capable of executing tasks that have been submitted to it. It can play the role of a scheduler and takes the
tedious "execute this after xxx seconds" out of the developers hands.
A cooperative operating system is the simplest one imaginable. The tasks running under this type of COS are not interrupted
by COS to give execution time to another task (as with the preemptive Real Time OS), no, tasks always run to completion
before COS gives execution to another task.
So, simple enough. This means however that each single task must finish execution in an acceptable time. If a
task is too time consuming then it has to be split up in several subtasks, each with an acceptable execution time.
Hence the term cooperative: the tasks have to cooperate to make things work well.
Details
This unit contains a COS with the following characteristics:
- Cooperative: tasks complete before the next one is executed. Tasks never interrupt each other, also not those submitted
from within an ISR.
- There are 3 types of tasks:
- Not timed tasks: will be executed asap by COS
- submitted from within a non ISR (*)
- submitted from within an ISR (*)
- Timed tasks: to be executed after some amount of time has passed by (max 65535 millisecs).
Timed tasks can be made to re-submit themselves, using each time the same timeout before the next run.
- Submitted tasks are stored in the COS' task queues, of which there are 3: one for each type of task described above.
- Each COS iteration (**) the queued tasks are executed by COS in this order:
- All (not timed) tasks submitted from within an ISR (*) -- highest priority --
- All timed tasks of which the time has expired -- middle priority --
- One (the oldest) task in the queue with non timed tasks submitted from a non ISR -- lowest priority --
- All tasks are executed outside any ISR, even those submitted from within an ISR (the latter have priority though, see above).
- If a queue is full, and another task is submitted for it, then that task is ignored. In this case the "Cos_Submit..." function will return "false". If the task was put in the queue (the queue was not full) the "Cos_Submit..." function will return "true".
- The time delay of Timed tasks has a resolution of 1 millisecond.
Theoretically one can set a timed task to the millisecond accurate, but the actual moment a timed task gets executed
depends on the politeness of its predecessor task to finish in time. (A task is never interrupted, also not if a timed
task is due.)
- Each task that is sumbitted to COS carries also 1 word type parameter. The latter will be passed onto the task when
it is finaly executed.
- COS can be started and stopped, in the latter state no tasks are executed and the "time" of the timed tasks is frozen.
(*) ISR: Interrupt Service Routine
(**) The COS itself checks, in each iteration (the "Cos_Step"), the content of the 3 queues and executes them if appropriate.
The Interface
The COS unit's interface looks like this:
unit Cos;
uses __Lib_String;
// interface
type Cos_TaskType = procedure(Par: Word);
Cos_TaskPointer = ^Cos_TaskType;
procedure Cos_Init;
// Initialises Cos
procedure Cos_Start;
// make Cos scheduler running (the routine "Cos_Step" is to be called regurarly by the using code)
procedure Cos_Step;
// make the Cos sheduler make one step (if Started)
procedure Cos_Run;
// make the Cos scheduler running in an endless loop (the routine "Cos_Step" is NOT to be called regurarly by the using code)
procedure Cos_Stop;
// make Cos scheduler stop
function Cos_SubmitTask(Task_: Cos_TaskPointer; Parameter: word): boolean;
// Task_ will be executed asap
// Can NOT be called from within an Interrupt Service Routine (ISR)
// Returns TRUE if the task is added to the queue, else false (queue full)
function Cos_SubmitTask_From_ISR(Task_: Cos_TaskPointer; Parameter: word): boolean;
// Task_ will be executed asap
// Can ONLY be called from within an Interrupt Service Routine (ISR)
// All ISR's calling this routine must have the same interrupt priority
// Returns TRUE if the task is added to the queue, else false (queue full)
function Cos_SubmitTask_Timed(Task_: Cos_TaskPointer; Parameter, Tme: word; Repetitive: boolean): boolean;
// Task_ will be executed after "Tme" time (millisecs)
// If "Repetitive" is true the task will resubmit itself after it has run
// Can NOT be called from within an Interrupt Service Routine (ISR)
// Returns TRUE if the task is added to the queue, else false (queue full)
procedure Cos_TimerInterrupt;
// the handling if the Cos timer interrupt, to be called from a 1ms timer ISR
implementation
Usage
The 1 millisecs timer
// Define the COS interrupt bit
var CosTimerInterruptbit : sbit at TMR2IE_bit; // define the actual COS timer interrupt bit (e.g. here Timer 2 is used)
// The timer interrupt service routine that calls the one in the COS (again timer2 is used here):
procedure Interrupt;
begin
if TestBit(PIR1,TMR2IF) = 1 then // 1 ms TMR2
begin
PIR1.TMR2IF := 0;
Cos_TimerInterrupt; // <--- has to be called every millisecond
end;
end;
Initialisation
// Start up the 1 ms timer (again timer2 is used here):
T2CON := %01011001; // prescaler 4, postcaler 12
PR2 := 207; // preload value, interrupt every millisec <-- MPU clock speed dependant!!!
TMR2 := 0; // timer 2 register reset
T2CON.TMR2ON := 1; // start TMR2
INTCON := %11000000; // enable interrupts (PEIE and GIE)
TMR2IE_bit := 1; // Enable the timer2 interrupts
The init code of the timer has to be executed before Cos can be "started" or "runned".
// Initialise Cos
Cos_Init; // Empties the queues. To be called before cos is "started" of "runned".
Starting Cos
2 methods can be used:
-
// Method 1 (Cos steps in its own endless loop)
Cos_Run;
The Cos_Run routine contains an endless loop, so statements after this one are useless unless COS is stopped by one of its tasks.
Normally at least one task should be submitted to COS before it is runned, otherwise COS
will not execute any task (unless some tasks are submitted from within ISR's of course).
-
// Method 2 (Cos steps in program loop)
Cos_Start; // Start Cos
While true do
begin
Cos_Step; // make Cos do one step
// do other stuff here
end;
This method requires that "Cos_Step" is called regurarly by the using code. It offers however the advantage that other code (outside Cos) can still be executed.
Submission of non timed tasks
Cos_SubmitTask(@Task1, 54321); // Task1 will be executed asap (according priorities), with parameter value = 54321
Submission of non timed tasks from within an ISR
Cos_SubmitTask_From_ISR(@Task1, 100 + Cnt); // Task1 will be executes asap (according priorities), with parameter value = 100 + Cnt (some variable)
The user can submit any task from an ISR using this submission routine, also the ones that are submitted from a non ISR or another ISR.
Important to know: All tasks are executed outside the ISR's, only the submission of them happens inside the ISR.
Submission of timed tasks
Cos_SubmitTask_Timed(@Task1, 12345, 1000, false); // Task1 will be executed after 1000 ms, with parameter value = 12345
// and the execution is not repeating
Cos_SubmitTask_Timed(@Task2, 23456, 250, true); // Task2 will be executed after 250 ms, with parameter value = 23456
// and the execution is repeated every 250 ms.
Note: Tasks Addresses
As you can see, submitting a task requires the address of that task. The task should have been declared before the submission takes place (in reality or "forward"), otherwise the task address is not known.
If a tasks wants to re-submit itself for execution, the "forward" technique doe not work. In this case a DWord variable, filled with the tasks address, is to be used.
The tasks
The tasks are the routines where the actual work is done in the project. A task should have always the same "signature" as follows:
procedure Task1(Par: word);
where "Task1" was the one submitted to the COS in previous examples (you can choose any name you want, it does
not have to start with "Task").
The "Par" parameter the task receives is the value that was used when the task was submitted (see above examples).
Tasks may submit other tasks to the COS (if fact, they have to do so -- if no tasks are submitted from ISR's -- because the
the COS would run out of tasks to execute if they don't):
procedure Task1(Par: word);
begin
IntToStr(Par, Str);
Str := 'Task1 ' + Str + #13 +#10;
Uart1_Write_Text(Str);
Cos_SubmitTask(@Task2, 20); // <---- here, task 2 will be executed asap
Cos_SubmitTask_Timed(@Task3, 30, 1000); // <---- here, task 3 will be executed after 1 second
end;
Eventually, somewhere in all tasks there will be one that will submit Task1 (otherwise the COS would run out of work),
and the project keeps running...
Indirect calls: warn the linker
Since the tasks to be executed by COS are called indirectly from it (via a pointer to them), the linker should be warned of that.
This is accomplished by calling the mP pseudo routine "SetFuncCall", in the initialisation part of the unit where-in the tasks are defined, e.g.:
// for the linker (tasks are called indirectly, the linker must be warned):
SetFuncCall(Task1);
SetFuncCall(Task2);
...
-------------------------------------------