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 is true 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

kernel_org() click to toggle source

The MultitaskingIO kernel code start address.

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

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

call find_channel click to toggle source

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
call find_channel_arg click to toggle source

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
call find_input_handle click to toggle source

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
call find_io_handles click to toggle source

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
call find_output_handle click to toggle source

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
call get_int8_norm_arg click to toggle source

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
call get_stream_arg click to toggle source

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
call initialize_io click to toggle source

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
USR open_io click to toggle source
DEF FN o(s,c$)=USR open_io

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
DEF FN w(s,n)=USR wait_io click to toggle source

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