Cooperative Multi Tasking on a Z80

I decided that the Zalt system is to be a game console operating system. Now I can focus on what bios/os code to write to accomplish this.

After getting the initial virtual device working I thought it would be a good idea to look into how to structure the rest of the code. One of the main problems I faced is how to let separate components work together on one CPU. I thought that a full preemptive thread scheduler would be pushing it at this point and also introduce overhead I could do without right now.

So I tried to use a cooperative multi tasking trick I had used before and see if it would work with the z88dk (sdcc) compiler. The idea is that you create a function for each task that has to run. The main loop (I guess I should now call it a game-loop) just calls each task-function in the appropriate order.

Each task-function does a little work and yields back to the main loop when its done, not taking too much time in each ‘slice’. On the next call into the task-function it remembers where it left off last time and continues there – a simple state machine.

The actual code trick to make this all look nice I came across here first. I have basically copied that implementation consisting of a set of macros. I normally try to avoid macros as much as possible, but here I could see no alternatives.

An example of a dummy task could look something like this:

Task_Begin(Task1_Execute)
{
    while(true) {
        Task_WaitUntil(CountDown());
        System_DebugConsole_Out('1');
    }
}
Task_End

The Task_Xxxx are the cooperative Task macros. CountDown simply counts a variable down on each call and returns true when it reaches zero. System_DebugConsole_Out is one of my functions to output a character to the debug-console.

What may surprise you is that the while-loop will not cause the method to hang the application. Task_WaitUntil will exit the Task1_Execute function unit CountDown returns true. Next time the Task1_Execute function is called it  will enter at Task_WaitUntil and call CountDown again. Only when CountDown returns true will the character be output to the debug-console.

So the main loop can be something as simple as:

while(true) {
    Task1_Execute();
    Task2_Execute();
    Task3_Execute();
 }

Each TaskX_Execute function will get a call and can do a small amount of work before returning.

Internally the Task-macros  use a integer variable to keep track of where to start executing in the task-function when it is called again. This is some nasty C code that you would normally not even think of writing involving jumping in and out of switch statements.

You can see that the current code __LINE__ is being used as a state value for the _task variable. bool_t is my boolean type.

#define Task_Begin(name) \
bool_t name() \
{ \
    bool_t _yield_ = false; \
    switch (_task) { \
    case 0:

#define Task_WaitUntil(expression) \
    _task = __LINE__; case __LINE__: \
    if (!(expression)) { \
        return false; \
    }

#define Task_End \
    } \
    _task = 0; return true; \
}

The _task variable is used in the switch to jump to the correct place when the task-function is called again.

If you would expand the macros in our example you’d get this:

bool_t Task1_Execute()
{
    bool_t _yield_ = false;
    switch (_task) {
        case 0:
        {
            while(true) {
                _task = __LINE__; case __LINE__:
                if (!(CountDown())) {
                    return false;
                };
                System_DebugConsole_Out('1');
            }
        }
    } 
    _task = 0; return true;
}

Note the odd placement of the case statements that cross scopes.

I must say the I had some strange errors in the z88dk (sdcc) compiler when testing this. I was able to make the code compile with some fiddling around though. If this turns out to be too brittle for this compiler I will try some other means, until then it’s good enough.

Advertisements
Published in: on February 18, 2017 at 11:49 am  Leave a Comment