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's code shouldn't change interrupt handler (no tampering with register
I
and noIM
instructions). -
Task's stacks may be moved up when other tasks terminate - tasks shouldn't store pointers to its own stack entries.
-
When using
SP
for other purposes always disable interrupts and restoreSP
to the previous value before enabling interrupts. -
At the task initialization,
IY
register is set to tasks'stack_bot + 128
address and may be used as a stack pointer frame for tasks' variables as the value ofIY
register is being moved along with the stack.
Task code recommendations:¶ ↑
-
Avoid modifying ZX Basic variables or using ROM routines that modifies them.
-
To yield execution instead of using
halt
usecall task_yield
. -
To terminate itself the task should either jump to
terminate
or justret
. -
Tasks may store local variables using indexing register
IY
- see below.
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:
SP
-
The task's end of stack - 2.
SP
points to the address ofterminate
routine, so invokingret
will terminate the task.
IY
-
The task's bottom of stack + 128.
IX
-
The task's stack size.
HL
-
The task's initial
PC
.
BC
-
The task's id.
DE
-
The
terminate
routine address.
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
Public Class Methods
The Multitasking
kernel code start address.
# File lib/zxutils/multitasking.rb, line 160 def self.kernel_org 0x10000 - code.bytesize end
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
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
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
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
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
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
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
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