Forums » Programming with the ECS » Oberon »
Coroutine for MainLoop and Task abstraction
Added by Runar Tenfjord 3 months ago
I made a small test module with an adaption of the obl.generators.mod
code for a MainLoop and Task abstraction.
MODULE Test;
IMPORT SYSTEM, Exceptions IN OBL;
TYPE
Coroutine = RECORD*
frame, stack: SYSTEM.PTR
END;
Loop = RECORD (Coroutine)
END;
Task = RECORD (Coroutine)
caller: POINTER TO VAR- Loop;
time : UNSIGNED32;
status: BOOLEAN
END;
VAR
tick : UNSIGNED32;
PROCEDURE* (VAR coroutine: Coroutine) Call;
PROCEDURE- (VAR coroutine: Coroutine) Transfer (VAR target: Coroutine);
CONST StackSize = ASH (16, SIZE (INTEGER) + SIZE (LENGTH));
VAR pointer: SYSTEM.PTR; exception: Exceptions.AllocationFailure;
BEGIN
SYSTEM.CODE ("mov ptr [$fp + pointer], ptr $fp");
coroutine.frame := pointer;
IF target.frame = NIL THEN
SYSTEM.NEW (target.stack, StackSize);
pointer := target.stack;
IF pointer = NIL THEN exception.Raise END;
SYSTEM.CODE ("add ptr $sp, ptr [$fp + pointer], ptr StackSize - stackdisp");
target.Call;
SYSTEM.CODE ("mov ptr $sp, ptr $fp + exception - stackdisp");
SYSTEM.DISPOSE (target.stack);
target.frame := NIL;
ELSE
pointer := target.frame;
SYSTEM.CODE ("mov ptr $fp, ptr [$fp + pointer]");
END;
END Transfer;
PROCEDURE (VAR task: Task) Sleep(time : UNSIGNED32);
VAR caller: Loop;
BEGIN
task.time := time;
caller := task.caller^;
task.Transfer(caller);
END Sleep;
PROCEDURE (VAR task: Task) Finish;
VAR caller: Loop;
BEGIN
task.time := 0;
task.status := FALSE;
caller := task.caller^;
task.Transfer(caller);
END Finish;
PROCEDURE (VAR task: Task) Call;
BEGIN
TRACE("Task.Call");
task.Sleep(50);
TRACE("Task.Call2");
task.Sleep(100);
TRACE("Task.Call3");
task.Finish;
TRACE("Not to be reached!");
HALT(1);
END Call;
PROCEDURE (VAR loop: Loop) Call;
VAR
task : Task;
i : INTEGER;
BEGIN
task.caller := PTR(loop);
task.status := TRUE;
task.time := 0;
TRACE("Loop.Call");
i := 0;
LOOP
IF ~task.status THEN EXIT END;
loop.Transfer(task);
TRACE(task.time);
TRACE(task.status);
TRACE(i);
INC(i);
END;
TRACE("Loop finished");
END Call;
PROCEDURE Test;
VAR loop : Loop;
BEGIN
loop.Call;
END Test;
BEGIN
Test;
END Test.
In order to get this to work properly I changed the definition of
the field caller to:
caller: POINTER TO VAR- Loop;
Otherwise it will not point to the correct instance of
Loop.
The code for Sleep and Finish also corrected for this and
these changes might not be optimal.
This could be an issue with obl.generators.mod also.
Replies (7)
RE: Coroutine for MainLoop and Task abstraction - Added by Florian Negele 3 months ago
I took the liberty and rewrote your module using the existing Generators module:
MODULE Test;
IMPORT Generators IN OBL;
TYPE Task = RECORD (Generators.Generator)
time: UNSIGNED32;
END;
PROCEDURE (VAR task: Task) Sleep (time: UNSIGNED32);
BEGIN
task.time := time;
task.Yield;
END Sleep;
PROCEDURE (VAR task: Task) Finish;
BEGIN
task.time := 0;
task.Yield;
END Finish;
PROCEDURE (VAR task: Task) Call;
BEGIN
TRACE ("Task.Call");
task.Sleep (50);
TRACE ("Task.Call2");
task.Sleep (100);
TRACE ("Task.Call3");
task.Finish;
END Call;
PROCEDURE Loop;
VAR task: Task; i: INTEGER;
BEGIN
task.time := 0;
TRACE ("Loop.Call");
i := 0;
WHILE task.Await () DO
TRACE (task.time);
TRACE (i);
INC (i);
END;
TRACE ("Loop finished");
END Loop;
BEGIN
Loop;
END Test.
I don't know if this is helpful. The call to Finish is strictly speaking not necessary as the implicit return from Call actually stops the while loop. The same applies to the status and the caller which I therefore removed. I also turned Loop into a procedure as it does not need to be couroutine either.
RE: Coroutine for MainLoop and Task abstraction - Added by Runar Tenfjord 3 months ago
An almost embarrassing simplification of my version.
Thanks.
RE: Coroutine for MainLoop and Task abstraction - Added by Runar Tenfjord 23 days ago
I got this to work now on the ECSMicro library on a ARM32 MCU with
a simple round-robin scheduler:
(** Simple coroutine demo with round-robin type scheduler *)
MODULE Test;
IMPORT BoardConfig;
IN Std IMPORT Coroutine(256, UNSIGNED32);
IN Std IMPORT UInt := ADTBasicType(UNSIGNED32);
IN Std IMPORT TaskQueue := ADTVector(Coroutine.TaskEntry);
IN Micro IMPORT ARMv7M;
IN Micro IMPORT SysTick := ARMv7MSTM32SysTick0;
CONST Pins = BoardConfig.Pins;
TYPE
Task1 = RECORD (Coroutine.Task) END;
Task2 = RECORD (Coroutine.Task) END;
VAR
queue : TaskQueue.Vector;
task1: Task1;
task2: Task2;
pin : Pins.Pin;
tasks : INTEGER;
PROCEDURE TaskCompare(left-, right- : Coroutine.TaskEntry): INTEGER;
BEGIN RETURN UInt.Compare(right.time, left.time);
END TaskCompare;
PROCEDURE (VAR task: Task1) Call;
BEGIN
WHILE TRUE DO
pin.On;
TRACE("Task1.On");
task.Sleep(200);
pin.Off;
TRACE("Task1.Off");
task.Sleep(800);
END;
END Call;
PROCEDURE (VAR task: Task2) Call;
BEGIN
WHILE TRUE DO
task.Sleep(250);
pin.On;
TRACE("Task2.On");
task.Sleep(50);
pin.Off;
TRACE("Task2.Off");
task.Sleep(200);
END;
END Call;
PROCEDURE AddTask(VAR task: Coroutine.Task);
VAR
entry : Coroutine.TaskEntry;
BEGIN
task.time := 0;
entry.task := PTR(task);
entry.time := SysTick.GetTicks() + tasks;
queue.HeapInsert(TaskCompare, entry);
INC(tasks);
END AddTask;
PROCEDURE Run;
VAR
entry : Coroutine.TaskEntry;
BEGIN
TRACE("Loop start");
(* run loop until task queue is empty *)
LOOP
IF queue.Size() = 0 THEN EXIT END;
queue.Get(0, entry);
LOOP
IF SysTick.GetTicks() >= entry.time THEN
(* remove task from queue *)
IGNORE(queue.HeapPop(TaskCompare, entry));
IF entry.task.Await() THEN
(* Reschedule task *)
entry.time := SysTick.GetTicks() + entry.task.time;
queue.HeapInsert(TaskCompare, entry);
END;
EXIT; (* Get next task *)
END;
ARMv7M.WFI; (* Idle *)
END;
END;
TRACE("Loop finished");
END Run;
BEGIN
TRACE("START");
BoardConfig.Init;
pin.Init(BoardConfig.USER_LED1_PORT, BoardConfig.USER_LED1_PIN, Pins.output,
Pins.pushPull, Pins.medium, Pins.noPull, Pins.AF0);
SysTick.Init(BoardConfig.HCLK, 1000);
tasks := 0;
queue.Init(2);
AddTask(task1);
AddTask(task2);
Run;
END Test.
The Coroutine module was modified with removed memory allocation
and the code simplified with just a loop as you suggested. Thanks.
This solution should be a good fit for these smaller devices and
much simpler than preemptive task switching.
One question regarding the code in obl.generators.mod.
What does this procedure call in IGNORE (caller) ?
PROCEDURE- (VAR caller: Caller) Call; BEGIN IGNORE (caller); END Call;
RE: Coroutine for MainLoop and Task abstraction - Added by Florian Negele 22 days ago
Interesting work. What does the replacement for the removed stack memory allocation in the Coroutine module look like?
The predeclared proper procedure IGNORE evaluates an expression of any type but ignores its value. It was introduced to explicitly ignore the result of function procedures. In this particular case it generates no code but also prevents the compiler from issuing a warning about the otherwise unused variable.
RE: Coroutine for MainLoop and Task abstraction - Added by Runar Tenfjord 22 days ago
Std.Coroutine.mod:
(**
Coroutine module based on Eigen Compiler Suite version
modified for static allocation of the coroutine stack
and add task oriented abstraction.
Copyright (C) Florian Negele
License is GNU General Public License version 3 with ECS Runtime Support Exception.
*)
MODULE Coroutine(StackSize, TimeType) IN Std;
IMPORT SYSTEM;
TYPE
Coroutine* = RECORD
frame, stack: SYSTEM.PTR;
data : ARRAY StackSize OF SYSTEM.BYTE;
END;
Caller = RECORD (Coroutine) END;
Task* = RECORD (Coroutine)
caller*: Caller;
time* : TimeType;
status*: BOOLEAN;
END;
TaskEntry* = RECORD
time* : TimeType;
task*: POINTER TO VAR Task;
END;
PROCEDURE (VAR caller: Caller) Call*;
BEGIN IGNORE (caller);
END Call;
PROCEDURE (VAR coroutine: Coroutine) Call*;
BEGIN END Call;
PROCEDURE- (VAR coroutine: Coroutine) Transfer* (VAR target: Coroutine);
CONST StkSize = StackSize;
VAR pointer: SYSTEM.PTR;
BEGIN
SYSTEM.CODE ("mov ptr [$fp + pointer], ptr $fp");
coroutine.frame := pointer;
IF target.frame = NIL THEN
target.stack := SYSTEM.VAL(SYSTEM.PTR, SYSTEM.ADR(target.data));
pointer := target.stack;
SYSTEM.CODE ("add ptr $sp, ptr [$fp + pointer], ptr StkSize - stackdisp");
target.Call;
SYSTEM.CODE ("mov ptr $sp, ptr $fp - stackdisp");
target.frame := NIL;
ELSE
pointer := target.frame;
SYSTEM.CODE ("mov ptr $fp, ptr [$fp + pointer]");
END;
END Transfer;
(** Waits for a call to the task to return and return status. *)
PROCEDURE (VAR task: Task) Await* (): BOOLEAN;
BEGIN
task.caller.Transfer (task);
RETURN task.status;
END Await;
(** Returns from task with request to reschedule execution. *)
PROCEDURE (VAR task: Task) Sleep*(time : TimeType);
BEGIN
task.time := time;
task.status := TRUE;
task.Transfer(task.caller);
END Sleep;
(** Return from task and request to stop the task. *)
PROCEDURE (VAR task: Task) Finish*;
BEGIN
task.time := 0;
task.status := FALSE;
task.Transfer(task.caller);
END Finish;
END Coroutine.
I also removed the exception, as this is not expected anymore.
This seems to work, but I might be overlooking things here.
Thanks for the explanation on IGNORE. This is very useful.
RE: Coroutine for MainLoop and Task abstraction - Added by Florian Negele 22 days ago
Thank you for the insight. Does this mean that tasks should better be global variables due to their increased size?
I think line 50 should read as follows in order to restore the stack pointer correctly. Your version probably still works because the stack pointer is no longer read after that line:
SYSTEM.CODE ("mov ptr $sp, ptr $fp + pointer - stackdisp");
If you like you could also remove SYSTEM.VAL in line 46 which always catches my eye:
target.stack := PTR(target.data);
Keep up the good work!
RE: Coroutine for MainLoop and Task abstraction - Added by Runar Tenfjord 21 days ago
The tasks should preferable be declared as globals as this will allow me to
just check the start of the heap to check the memory situation. Otherwise
stack allocation is more dynamic and difficult to have under complete control.
These small MCUs are more clever than what might be visible a first glance.
The UART and many similar sub-systems actually run in parallel while the
main CPU is in sleep mode. The loop will then wake up (from WFI instruction)
on the timer (each milli-second) or on an sub-system event like a full read buffer.
All to save valuable battery energy.
This is a very important capability to have on these MCU systems. These types
of periodic tasks is very common and with coroutines this can be done efficiently
and easy.
Thanks spending your time and looking into this!