class ZXUtils::Multitasking

ZXUtils::Multitasking

Run machine code programs (a.k.a. “tasks”) in parallel with the ZX Spectrum's Basic.

This class contains Macros and kernel labels for tasks and the kernel code.

The “tasks” are machine code programs that are run in parallel as opposed to system programs which are run in sequence.

The Multitasking module provides a kernel that handles a task creation, execution switching and termination. Switching between tasks is handled on each maskable interrupt or on demand. Each task is being provided with its own stack space. System programs such as Basic or user machine code (USR) use the system stack as usual. An execution of a system program is intertwined with an execution of each task in turn. Tasks as well as system programs may “yield” its execution early with an API call.

Task guide

Machine code for tasks should respect the following restrictions:

Task code recommendations:

Task initialization:

Each task receives its own, dedicated stack space. Whenever the execution context is being switched, next task's SP register address is being checked against its own stack's boundary. If it points below the designated address space the whole multitasking is being terminated (panic) and the control is returned to the system.

Tasks may also use bottom of the stack space for task-local variables storing them via IY register which addresses the 128'th byte above the stack's bottom. So to be on the safe side tasks should start allocating variables from [IY-128] up.

Task registers:

Registers may be used freely with some limitations (SP, IY) mentioned above. When the task starts, the CPU registers hold:

Memory map:

                          TaskVars
+---------+-------------+------------+-----------+-----------+-------------+--------------+--------------+
| BASIC   | Display and | ZX Printer | System    | ZX Basic  | Tasks' Code | Multitasking | Multitasking |
|  ROM    |  Attributes |     Buffer | Variables | Workspace |    and Data |  Stack Space |       Kernel |
+---------+-------------+------------+-----------+-----------+-------------+--------------+--------------+
^         ^             ^            ^           ^           ^             ^              ^              ^
$0000     $4000         $5B00        $5C00       $5CB6       RAMTOP        |              |         P_RAMT
                        mtvars                                             stack_bot      stack_end

ZX Basic API

REM Setup:
REM Initializes or resets multitasking.
REM Returns the number of bytes available for new tasks' stacks.
PRINT USR api

REM Spawns a task:
1 DEF FN m(a,s) = USR api

LET tid = FN m(address,stacksize)

REM Terminates a task:
2 DEF FN t(t) = USR api

REM Returns the number of bytes available for new tasks' stacks after the task is terminated.
PRINT FN t(tid)

REM Returns the number of bytes available for new tasks' stacks.
3 DEF FN f() = USR api

PRINT FN f()
address

Must be a task's machine code entry point address.

stacksize

Must be an even number of bytes, at least 42.

tid

Must be a number returned from FN m(a,s).

Setup must be invoked at least once before any other function is used. It can also be called to terminate all tasks.

Spawn will result in “4 Out of memory” error if there is not enough room for a task info entry or stack space.

“A Invalid argument” error may also be reported when arguments are incorrect.

Task API

import        ZXUtils::Multitasking, code: false, macros: true, labels: ZXUtils::Multitasking.kernel_org

              # ...
              # instead of invoking halt, call task_yield
              call task_yield
              # ...
              # to terminate:
              jp   terminate
              # or when a stack is depleted just:
              ret

Constants

MT_STACK_BOT

Bottom of the multitasking stack space.

MT_VARS

An address of the task variables (TaskVars). Can be moved elsewhere if ZX Printer is needed.

TASK_QUEUE_MAX

A hard limit on the number of tasks. Should not be enlarged if MT_VARS are placed inside ZX Printer Buffer.

Public Class Methods

kernel_org() click to toggle source

The Multitasking kernel code start address.

# File lib/zxutils/multitasking.rb, line 160
def self.kernel_org
  0x10000 - code.bytesize
end
new_kernel(*args, **opts) click to toggle source

Instantiate Multitasking kernel with the proper code address.

# File lib/zxutils/multitasking.rb, line 155
def self.new_kernel(*args, **opts)
  new kernel_org, *args, **opts
end

Public Instance Methods

USR api click to toggle source
DEF FN m(a,s)=USR api
DEF FN t(t)=USR api
DEF FN f()=USR api

ZX Basic API

This endpoint should be invoked from the ZX Basic directly via USR or indirectly via FN.

LET stackFreeBytes=USR api: REM Initializes multitasking.

1 DEF FN m(a,s)=USR api: REM Spawns a task
LET tid=FN m(address,stacksize)

2 DEF FN t(t)=USR api: REM Terminates a task
LET stackFreeBytes=FN t(tid)

3 DEF FN f() = USR api: REM Returns the number of bytes available for new tasks' stacks.
# File lib/zxutils/multitasking.rb, line 414
ns :api do
                    call find_def_fn_arg            # is this an FN call with arguments?
                    jr   Z, new_or_terminate        # yes: skip to new_or_terminate
                    jr   C, init_multitasking       # called via USR
end
call init_multitasking click to toggle source

Initializes multitasking.

NOTE

This routine must never be called from a task!

Clears all tasks, sets global variables and enables the custom interrupt handler.

Modifies: af, bc, de, hl, i, IFF.

# File lib/zxutils/multitasking.rb, line 478
ns :init_multitasking, use: :mtvars do
                    di                              # prevent switching
                    clrmem mtvars, +mtvars          # remove all tasks
  init_stack_end    ld   hl, initial_stack_end
                    ld   [mtvars.stack_end], hl     # initialize stack_end
  init_stack_bottom ld   hl, initial_stack_bot
                    ld   [mtvars.stack_bot], hl     # initialize stack_bot
                    call stack_space_free
                    ld   a, 0x3B
                    ld   i, a                       # load the accumulator with unused (filled with 255) page in rom.
                    im2
                    ei
                    ret
end
call find_def_fn_arg click to toggle source

Looks for a first DEF FN argument value address.

NOTE

This routine must never be called from a task!

If an argument is found the hl registers will address an argument's FP-value and the ZF flag will be set (Z). If the code wasn't invoked via the FN function call, the CF flag will be set instead (C).

Modifies: af and hl.

# File lib/zxutils/multitasking.rb, line 719
ns :rdoc_mark_find_def_fn_arg, merge: true do
  find_def_fn_arg     find_def_fn_args 1, cf_on_direct: true
end
call stack_space_free click to toggle source

Returns (in bc) how many bytes are available in multitasking stack space for new tasks. Reports an OOM error if the task variables are corrupted or uninitialized.

Modifies: af, bc, de, hl.

# File lib/zxutils/multitasking.rb, line 427
ns :stack_space_free, use: :mtvars do
                    ld   hl, mtvars.stack_end - (+TaskInfo - 3)
                    ld   bc, +TaskInfo - 3       # begin with mtvars.stack_end as the 1st task's stack_end
  search_last       add  hl, bc                  # skip task.stack_save
                    ld   e, [hl]                 # task.stack_bot
                    inc  hl
                    ld   d, [hl]                 # task.stack_bot
                    inc  hl
                    ld   a, [hl]                 # task.tid
                    inc  hl
                    ora  [hl]                    # task.tid
                    jr   NZ, search_last         # some tid?
                    ld   hl, [mtvars.stack_bot]  # DE: tasks[last].stack_bot
                    ex   de, hl
                    sbc  hl, de                  # task.stack_bot - mtvars.stack_bot
                    jr   C, ei_report_oom
                    ld16 bc, hl
                    ret
end
call task_yield click to toggle source

Yields task execution.

Tasks or system programs should call this endpoint instead of invoking halt. This will switch the execution context to the next task in the queue immediately. The execution of the calling task will resume when its turn will come.

Modifies: nothing. Requires at least 22 bytes on a machine stack to be available.

# File lib/zxutils/multitasking.rb, line 734
ns :task_yield      do
                    di
                    push bc
                    push de
                    push hl
                    push af
                    push ix
                    ex   af, af
                    push af
                    exx
                    push bc
                    push de
                    push hl
                    push iy
                    jr   handle_interrupts.task_yield
end
jp terminate click to toggle source

Terminates the current task.

Tasks may jump to this endpoint directly to terminate themselves. If called or jumped to from a system program and not from a task reports “0 OK” error.

ret instruction from a task will actually jump back here.

# File lib/zxutils/multitasking.rb, line 617
ns :terminate, use: :mtvars do
                    di                            # prevent switching
                    check_current_task            # HL: -> task.stack_bot, CF: 0
                    jr   Z, ei_report_ok          # called not from a task
                    ld   sp, [mtvars.system_sp]
                    ld   bc, switch_to_sys        # store switch_to_sys as a
                    push bc                       # ret address
                    3.times { dec hl }
                    ld   b, [hl]
                    dec  hl
                    ld   c, [hl]                  # BC: task's tid
  # Find task info entry by tid.
  search_terminate  ld   [restore_sp + 1], sp     # BC: tid, save SP to be restored at the end
                    ld   sp, mtvars.stack_end     # search for a task.tid == BC
  search_loop       pop  de                       # DE: stack_end
                    pop  hl                       # HL: task.tid
                    ld   a, l
                    ora  h                        # CF: 0
                    jr   Z, task_not_found        # task.tid == 0
                    sbc  hl, bc                   # task.tid == BC
                    pop  hl                       # HL: task.stack_save (ignored)
                    jr   NZ, search_loop          # SP: -> task.stack_bot, CF: 0
  # Found task info entry to terminate. Now we need to:
  # - Update the following tasks' stack_save and stack_bot pointers.
  # - Move the following tasks' info entries up.
  # - Move the following tasks' stacks down.
  terminate_found   pop  bc                       # BC: stack_bot, DE: stack_end, SP: -> task[tid + 1], CF: 0
                    ld16 ix, bc                   # IX: stack_bot
                    ld16 hl, de                   # HL: stack_end
                    sbc  hl, bc                   # HL: stack size = stack_end - stack_bot
                    ld16 bc, hl                   # BC: stack size
                    exx                           # B'C': stack size, D'E': stack_end
                    ld   hl, 0
                    ld16 de, hl
                    add  hl, sp                   # HL: task[tid + 1], DE: 0
                    ex   de, hl                   # HL: 0, DE: task[tid + 1]
                    exx                           # BC: stack size, DE: stack_end, D'E': task[tid + 1], H'L': 0
                    jr   update_pointers.start
  # Increment all the following task's pointers by the terminated task's stack size and get the last task's stack_bot.
  ns :update_pointers do
                    pop  hl                       # task.stack_save
                    ld   [restore_sp + 1], sp     # save SP
                    ld   sp, hl                   # SP: task's SP
                    pop  iy                       # IY: task's IY
                    add  iy, bc                   # add reclaimed delta to IY
                    push iy
    restore_sp      ld   sp, 0                    # SP: restore
                    add  hl, bc                   # add reclaimed delta to stack_save
                    push hl
                    pop  hl                       # task.stack_save
                    pop  hl                       # task.stack_bot
                    add  hl, bc                   # add reclaimed delta to stack_bot
                    push hl
                    pop  de                       # updated task.stack_bot
    start           pop  hl                       # task.tid
                    ld   a, l
                    ora  h                        # CF=0
                    jr   NZ, update_pointers
  end                                             # SP: -> task[ntasks].stack_save, BC: terminated task stack size, DE: task[ntasks].stack_bot + stack size
  # Move the following tasks' info entries + terminator up.
                    exx                           # DE: task[tid + 1], HL: 0
                    add  hl, sp                   # HL: SP, CF: 0
                    sbc  hl, de                   # HL: SP - task[tid + 1]
                    ld16 bc, hl                   # BC: SP - task[tid + 1]
                    ld16 hl, de                   # HL: task[tid + 1]
                    ld   sp, -(+TaskInfo)
                    add  hl, sp                   # HL: task[tid]
                    ex   de, hl                   # DE: task[tid], HL: task[tid + 1]
                    ldir
                    exx                           # BC: stack size, DE: task[ntasks-1].stack_bot + tid stack size
  # Move the following tasks' stacks down.
                    ld   sp, ix                   # SP: task[tid].stack_bot
                    ld16 hl, bc                   # tid stack size
                    add  hl, sp                   # HL: task[tid].stack_end
                    ld16 bc, hl                   # BC: task[tid].stack_end
                    sbc  hl, de                   # task[tid].stack_end - (HL: task[ntasks-1].stack_bot + tid stack size)
                    jr   Z, skip_reclaim_stk
                    ld16 de, bc                   # DE: task[tid].stack_end
                    ld16 bc, hl                   # BC: task[ntasks-1].stack_bot + tid stack size - task[tid].stack_end
                    ld   hl, -1
                    add  hl, sp                   # HL: task[tid].stack_bot - 1
                    dec  de                       # DE: task[tid].stack_end - 1
                    lddr
  # That's it, now restore SP and just return.
  skip_reclaim_stk  label
  task_not_found    label
  restore_sp        ld   sp, 0
                    ret
end
call get_uint_arg click to toggle source

Attempts to read a positive 16-bit integer from a FP-value addressed by hl.

NOTE

This routine must never be called from a task!

Input:

  • hl

    an address of a FP-value.

On success de holds a value and hl will be incremented past the last FP-value's byte. On failure reports “A Invalid argument” error.

Modifies: af, de, hl.

# File lib/zxutils/multitasking.rb, line 462
get_uint_arg      read_positive_int_value d, e