class ZXUtils::MultitaskingIO
ZXUtils::MultitaskingIO
¶ ↑
Asynchronous communication channels between tasks running in parallel with ZX Spectrum's Basic programs.
See Multitasking
for more information on tasks.
This class contains Macros
and kernel labels for tasks and the kernel code.
+----------- find_io_handles ------------+ CHANS v | +----------+ +----------+ output +------------+ input handle | | PRINT #n | ----> | User | -------> | I/O Buffer | <------> +-------+ | ZX Basic | | Channel | +------------+ | Tasks | | INKEY$#n | <---- | Record | <------- | I/O Buffer | <------> +-------+ +----------+ +----------+ input +------------+ output handle
The ZX Spectrum's I/O channels are indentified by single upper-case letters. There are four system channels: “K”, “S”, “R” and “P”. Channels in order to be used must be “opened” by associating a channel with streams # 0-15. By default streams 0-3 are occupied by the system channels. In ZX Spectrum's Basic there are dedicated OPEN #
and CLOSE #
statements to associate channels with streams. Unfortunately their usage is limited only to the system channels.
MultitaskingIO
provides API to create its own channels and associate them with user streams. Each MultitaskingIO
channel represents a pair of I/O Buffers, one for writing and one for reading from the system (full-duplex). The tasks can read or write data to any of the buffers. The most common use-case however is that the system's output provides the input for tasks and vice-versa.
To speed up things tasks are not using streams but rather acquire a direct handle to an I/O Buffer and operate on its data directly by the routines provided by MultitaskingIO::Macros
.
Memory map:¶ ↑
TaskVarsIO | I/O user channel records +---------+-------------+-------------+-----------+----------+---+-----------+-------------+-- | BASIC | Display and | ZX Printer | System | Channel |$80| ZX Basic | Tasks' Code | | ROM | Attributes | Buffer | Variables | Info | | Workspace | and Data | +---------+-------------+-------------+-----------+----------+---+-----------+-------------+-- ^ ^ ^ ^ ^ ^ ^ ^ $0000 $4000 $5B00 $5C00 $5CB6 = CHANS PROG RAMTOP mtio_buffers_bot mtiovars | <-| --+--------------+--------------+---------------+--------+--------------+ | Reserved for | I/O Buffers | Multitasking | I/O | Multitasking | | I/O Buffers | Data Space | Stack Space | Kernel | Kernel | --+--------------+--------------+---------------+--------+--------------+ ^ ^ ^ ^ ^ mtio_buffers_bot | | | P_RAMT buffers_top tv.stack_bot tv.stack_end
Multitasking
Api¶ ↑
All of the labels and macros exported by the Multitasking
kernel are being re-exported by the MultitaskingIO
module.
Programs compiled for the Multitasking
kernel without the MultitaskingIO
will work fine, as the addresses of the Multitasking
functions don't change.
ZX Basic API¶ ↑
REM Setup: REM Initializes multitasking and I/O task variables, makes space for channels. REM Returns the number of bytes available for new tasks' stacks. PRINT USR open_io REM Reset: 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() REM Opens or creates a MultitaskingIO channel and assigns a stream # to it: 4 DEF FN o(s,c$)=USR open_io REM Initializes channel Q and allocates buffers if called for the first time with that letter. REM Opens channel Q and assigns stream #7 to that channel. Returns previously assigned channel identifier. PRINT CHR$ FN o(7,"Q") REM Closes stream #7. Returns previously assigned channel identifier. PRINT CHR$ FN o(7,"") REM Wait for I/O data availability. 5 DEF FN w(s,n)=USR wait_io REM Blocks an execution of a program until at least 3 characters are available to be read from an I/O buffer at stream #7. LET ReadCharsNo=FN w(7,3): REM ReadCharsNo >= 3 REM Reads 3 characters from I/O channel #7. LET a$=INKEY$#7+INKEY$#7+INKEY$#7 REM Blocks an execution of a program until at least 5 characters can be written to an I/O buffer at stream #7. LET WriteCharsNo=FN w(7,-5): REM WriteCharsNo >= 5 REM Sends 5 characters into I/O channel #7. PRINT #7;"hello";
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 a call to
FN m(a,s)
.
Setup
must be invoked once before any other function is used.
Reset
can be called to terminate all tasks.
Spawn
will report a “4 Out of memory” error if there is not enough room for a task info entry or stack space.
Open
will report a “4 Out of memory” error if there is not enough room for I/O buffer data.
“A Invalid argument”, “B Integer out of range”, “F Invalid file name”, “O Invalid stream”, “Q Parameter error” errors may also be reported when arguments are incorrect.
Printing to the stream with the I/O buffer full will report “8 End of file” error.
When reading a stream without any data available an INKEY$# function will return an empty string.
Task API¶ ↑
To communicate with the MultitaskingIO
api tasks should obtain a direct handle to the I/O buffer. This is done via one of the kernel functions:
Example:
# import kernel macros and labels from MultitaskingIO into the "mtio" namespace import ZXUtils::MultitaskingIO, :mtio, code: false, macros: true, labels: ZXUtils::MultitaskingIO.kernel_org # ... # instead of invoking halt, call mtio.task_yield call mtio.task_yield # to terminate: jp mtio.terminate # or when a stack is depleted just: ret # wait for the channel Q being available wait_loop ld a, "Q".ord call mtio.find_io_handles jr Z, got_handles # HL: input handle, DE: output handle call mtio.task_yield jr wait_loop # read a character, assume HL contains an address of the input handle mtio_getc(a, not_ready: :eoc, subroutine: false, mtyield: mtio.task_yield) jr C, got_the_character # write a @ character, assume HL contains an address of the output handle ld a, "@".ord mtio_putc(a, not_ready: :eoc, subroutine: false, mtyield: mtio.task_yield) jr C, character_was_written # read a character, block a task if not available, assume HL contains an address of the input handle mtio_getc(a, not_ready: :wait, subroutine: false, mtyield: mtio.task_yield) # do something with a character in accumulator # wait for data, assume HL contains an address of the input handle ld c, 5 # at least 5 characters mtio_wait(:read, c, mtyield: mtio.task_yield) # write a small string up to 255 characters, assume HL contains an address of the output handle ld de, hello_world ld a, +hello_world mtio_puts(a, subroutine:false, mtyield: mtio.task_yield) cp +hello_world jr Z, written_all_characters ... hello_world data "Hello world!"
Constants
- MTIO_BUFFER_CHANNELS
Defines how many I/O buffer channels may be allocated. May be changed to a higher number. More buffers equals less memory but more independent channels. Each channel costs 527 bytes (2 *
BufferIO
+ 5 bytes for a channel record).- MTIO_CLEAR_TV_FLAG_ON_INPUT
When using INPUT # with an I/O channel other than “K” and then the error report is being printed in channel “K”, a cursor is shown after a reported message expecting further user input, e.g.:
INPUT #2;a
will report the: “J Invalid I/O device” error, but the flashing [K] will be visible at the end of the message.
The expected behaviour is that the cursor is not visible until the next key is being pressed. Pressing the key then should clear the error message first, before printing anything else.
When
MTIO_CLEAR_TV_FLAG_ON_INPUT
istrue
on each character input the TV_FLAG system variable is being cleared to prevent bogus cursor behaviour.This is the default. Set to
false
to disable this countermeasure.This happens because INPUT uses WAIT-KEY ($15D4) procedure which in turn sets the bit 3 of TV_FLAG signalling to reprint the edit area. When using channel “K” after each accepted key a procedure ED-COPY ($111D) at $2162 is being called which reprints the edit area and clears the bit 3. However when another channel is in use this part is being skipped at $2168. When the program execution ends, the editor starts reading user input from channel “K” (at KEY-INPUT: $10A8). But the bit 3 of TV_FLAG is still set. This in turn makes the ED-COPY being called immediately and the cursor appears. ED-COPY prevents the error message to be cleared, so the further user input will be echoed after the message.
- MTIO_DETECT_BREAK_KEY_ON_BLOCKING_IO
If the I/O operation blocks program while waiting for data or free buffer space, pressing BREAK will stop the program execution with “8 End of file” error report. Define
MTIO_DETECT_BREAK_KEY_ON_BLOCKING_IO
=false
to disable this.
Public Class Methods
The MultitaskingIO
kernel code start address.
# File lib/zxutils/multitasking_io.rb, line 259 def self.kernel_org 0x10000 - code.bytesize end
Instantiate MultitaskingIO
kernel with the proper code address.
# File lib/zxutils/multitasking_io.rb, line 249 def self.new_kernel(*args, **opts) new(kernel_org, *args, **opts).tap do |kernel| mtio_buffers_bot = kernel[:initial_stack_bot] - kernel[:mtio_buffer_chans]*2*MultitaskingIO::BufferIO.to_i if kernel[:mtio_buffers_bot] > mtio_buffers_bot raise CompileError, "mtio_buffers_bot may be at most: #{mtio_buffers_bot}" end end end
Public Instance Methods
Looks for a channel name.
Input:
a
-
A channel name as an upper-case letter code.
On success returns channel's record address + 4 in hl
(pointing to the channel name) and the ZF flag is being set (Z). If ZF flag is clear (NZ) indicates that the channel could not be found. bc
will always contain 0xFFFB (0x10000 - 5) so it's easy to move hl
back to the beginning of the channel record.
e.g.:
ld a, "Q".ord call find_channel jr NZ, not_found add hl, bc # -5 inc hl # hl points to the beginning of the channel's record
Each channel record has the following format:
-
two-byte address of the output routine,
-
two-byte address of the input routine,
-
one-byte channel code letter (a channel name).
Modifies: af
, af'
, bc
, de
, hl
.
# File lib/zxutils/multitasking_io.rb, line 1135 ns :find_channel do ld hl, [vars.prog] 2.times { dec hl } ld bc, -5 cp [hl] ret Z ld de, [vars.chans] add hl, bc ex af, af search_loop ex af, af cp [hl] ret Z add hl, bc ex af, af cp16r d,e, h,l, jr_msb_c: search_loop, jr_msb_nz: not_found jr C, search_loop not_found ex af, af ret end
Looks for a channel name from a FN string argument.
- NOTE
-
This routine must never be called from a task!
Input:
hl
-
An address of the DEF FN argument's FP-value (as a string).
Reads a string and validates channel's name by checking string's length and converting the 1st letter to the upper-case. Also checks if the channel name is valid and it's not the one of system's channel names: “K”, “S”, “R” nor “P”. If the validation fails reports error: “F Invalid file name”.
If the string is empty returns with the ZF flag set but with 0 in the accumulator. On success the ZF flag is being set and the accumulator holds the channel code letter, hl
holds the channel's record address + 4 and bc
0xFFFB (twos complement: -5).
See find_channel
for more information.
Modifies: af
, af'
, bc
, de
, hl
.
# File lib/zxutils/multitasking_io.rb, line 1088 ns :find_channel_arg do read_arg_string(d, e, b, c) ld a, b ora b jr NZ, error_f.err # b<>0 ora c ret Z # empty string dec c # is 1-character string? jr NZ, error_f.err # LEN c$ > 1 ld a, [de] # get channel name anda 0xDF # make uppercase cp ?A.ord jr C, error_f.err cp ?Z.ord + 1 jr NC, error_f.err ld hl, system_chan_names ld c, +system_chan_names # b: 0 cpir jr Z, error_f.err end
Looks for an input handle for tasks.
Provide a channel name as an upper-case letter code in accumulator.
On success returns I/O buffer handle address in hl
and sets the ZF flag (Z). ZF flag is clear (NZ) when a channel with the requested name could not be found.
Modifies: af
, af'
, bc
, de
, hl
.
# File lib/zxutils/multitasking_io.rb, line 1196 ns :find_input_handle do call find_channel ret NZ add hl, bc # hl+= -5 inc hl ld a, [hl] # system output routine (is input for tasks) inc hl ld h, [hl] ld l, a dec hl # input handle for tasks ret end
Looks for I/O handles.
Provide a channel name as an upper-case letter code in accumulator.
On success returns I/O buffer handle addresses in hl
(task input) and de
(task output) and sets the ZF flag (Z). ZF flag is clear (NZ) when a channel with the requested name could not be found.
Modifies: af
, af'
, bc
, de
, hl
.
# File lib/zxutils/multitasking_io.rb, line 1167 ns :find_io_handles do call find_channel ret NZ dec hl ld d, [hl] # system input routine (is output for tasks) dec hl ld e, [hl] dec de # output handle for tasks dec hl ld a, [hl] # system output routine (is input for tasks) dec hl ld l, [hl] ld h, a dec hl # input handle for tasks ret end
Looks for an output handle for tasks.
Provide a channel name as an upper-case letter code in accumulator.
On success returns I/O buffer handle address in hl
and sets the ZF flag (Z). ZF flag is clear (NZ) when a channel with the requested name could not be found.
Modifies: af
, af'
, bc
, de
, hl
.
# File lib/zxutils/multitasking_io.rb, line 1221 ns :find_output_handle do call find_channel ret NZ dec hl ld a, [hl] # system input routine (is output for tasks) dec hl ld l, [hl] ld h, a dec hl # output handle for tasks ret end
Attempts to read an integer in the range -255..255 from a FN argument.
- NOTE
-
This routine must never be called from a task!
Input:
hl
-
an address of a FP-value.
On success register e
holds a positive number: 0..255 and register c
a sign (0 or -1). hl
will be incremented past the last FP-value's byte. On failure reports either “A Invalid argument” or “B Integer out of range” error.
Modifies: af
, de
, hl
, c
.
# File lib/zxutils/multitasking_io.rb, line 1249 ns :get_int8_norm_arg do xor a cp [hl] jr NZ, error_a.err call rom.int_fetch # de: sign normalized integer, c: sign ld a, d ora d ret Z end
Attempts to read a stream number 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 accumulator holds a stream number: 0..15 and hl
will be incremented past the last FP-value's byte. On failure reports “O Invalid stream” error.
Modifies: af
, de
, hl
.
# File lib/zxutils/multitasking_io.rb, line 948 ns :get_stream_arg do call get_uint_arg ld a, d ora a jr NZ, error_o.err ld a, e cp 16 ret C end
Initializes I/O and multitasking.
- NOTE
-
This routine must never be called from a task!
Modifies: af
, bc
, de
, hl
, i
, IFF
.
# File lib/zxutils/multitasking_io.rb, line 842 ns :initialize_io do ld hl, [mtiovars.buffers_top] ld a, l ora h ret NZ # initialize only once ld hl, [vars.prog] # make room for new channels below PROG dec hl ld bc, mtio_buffer_chans*5 call rom.make_room # make space HL->p nnnn DE->n ooooo ld bc, mtio_buffer_chans*5 - 1 ld16 hl, de ld [hl], 0 dec de lddr # clear new chan space call init_multitasking ld hl, [mtiovars.tv.stack_bot] ld [mtiovars.buffers_top], hl ret end
ZX Basic API
This endpoint should be invoked from the ZX Basic directly via USR or indirectly via FN.
LET stackFreeBytes=USR open_io: REM Initializes multitasking and I/O task variables, makes space for channels. 1 DEF FN o(s,c$)=USR open_io: REM Opens or creates a MultitaskingIO channel and assigns a stream # to it. REM Initializes channel Q and allocates buffers if called for the first time with that letter. REM Opens channel Q and assigns stream #7 to that channel. Returns previously assigned channel identifier. LET prevChan=CHR$ FN o(7,"Q") REM Closes stream #7. Returns previously assigned channel identifier. LET prevChan=CHR$ FN o(7,"")
# File lib/zxutils/multitasking_io.rb, line 808 ns :open_io do call find_def_fn_arg jr C, initialize_io jr NZ, error_q.err call get_stream_arg push af # save stream # call find_def_fn_arg.seek_next jr NZ, error_q.err call find_channel_arg jp NZ, create_channel anda a # was empty string? jr Z, close_stream add hl, bc inc hl jp open_stream_stacked end
ZX Basic API
This endpoint should be invoked from the ZX Basic indirectly via FN.
2 DEF FN w(s,n)=USR wait_io: REM Wait for I/O data availability. REM Blocks an execution of a program until at least 3 characters are available to be read from an I/O buffer at stream #7. LET ReadCharsNo=FN w(7,3): REM ReadCharsNo >= 3 REM Blocks an execution of a program until at least 5 characters can be written to an I/O buffer at stream #7. LET WriteCharsNo=FN w(7,-5): REM WriteCharsNo >= 5
# File lib/zxutils/multitasking_io.rb, line 877 ns :wait_io do call find_def_fn_arg jr NZ, error_q.err call get_stream_arg ex af, af # save stream # call find_def_fn_arg.seek_next jr NZ, error_q.err call get_int8_norm_arg # e: int, c: sign inc c ex af, af # a: stream #, f': ZF: sign call stream_channel_data jr Z, error_o.err ex af, af # f: ZF: sign jr Z, wait_write call mtiobuf_inp_handle if MTIO_DETECT_BREAK_KEY_ON_BLOCKING_IO check_read mtio_ready?(:read, nchars:e) jr C, read_not_ready quit_bc ld c, a ld b, 0 ret read_not_ready call cheeki_breeki jr check_read else mtio_wait(:read, e) quit_bc ld c, a ld b, 0 ret end wait_write call mtiobuf_out_handle if MTIO_DETECT_BREAK_KEY_ON_BLOCKING_IO check_write mtio_ready?(:write, nchars:e) jr C, write_not_ready sub e cpl jr quit_bc write_not_ready call cheeki_breeki jr check_write else mtio_wait(:write, e) sub e cpl jr quit_bc end end