module ZXLib::Sys::Macros
ZXLib::Sys
Macros
¶ ↑
Some of the macros require:
require 'zxlib/math' # ... macro_import MathInt macro_import ::ZXLib::Math
Public Instance Methods
Looks for a ZX Spectrum CHAN entry determined by output
, input
and a chan_name
.
- output
-
output routine address or a 16bit register holding that address except
hl
- input
-
input routine address or a 16bit register holding that address
- strm_no
-
stream number (4 - 15) or 8bit register name
- chan_name
-
a channel name immediate value or a 8bit register
- buffer
-
address of buffer (5 bytes) to use, by default it's a printer buffer at 23296
Optionally give namespace label a name
.
ZF=1 if found, hl
points to the record address that matches ZF=0 if not found, hl
points to the memory address immediately after the last record
Modifies: af
, hl
, bc
, de
# File lib/zxlib/sys.rb, line 759 def chan_exists(name = nil, output: de, input: nil, chan_name: 'U', buffer: 23296) chan_name = chan_name.ord if String === chan_name raise ArgumentError, "output or input must not be hl register pair" if output == hl or input == hl isolate name, use: [:rom, :vars] do |eoc| input = rom.error_j if input.nil? output = rom.error_j if output.nil? ld a, chan_name unless chan_name == a ld hl, buffer push hl oh, ol = if register?(output) output.split else ld de, output [d, e] end ld [hl], ol inc hl ld [hl], oh inc hl ih, il = if register?(input) input.split else ld de, input [d, e] end ld [hl], il inc hl ld [hl], ih inc hl ld [hl], a pop de # de record address exx push hl ld hl, [vars.prog] dec hl exx # hl' stop search address ld bc, 5 # record length ld hl, [vars.chan] # CHAN find_record h, l jr NZ, cleanup sbc hl, bc # beginning of the record found cleanup exx pop hl exx end end
Calculates the address of the first byte of a character. The calculated address will be available in the hl
register.
chars
-
the address of a code=0 character as a
hl
register, address, label or a label pointer e.g.: [vars.chars].
code
-
an 8-bit register or a code number (addresses and pointers works to).
tt
-
a 16-bit register except
hl
.
T-States: 46, +(0|4|7|13) depending on code, +(0|10|16) depending on chars.
Modifies: af
, tt
, hl
# File lib/zxlib/sys.rb, line 652 def char_ptr_from_code(chars, code=a, tt:de) raise ArgumentError unless ((register?(chars) and chars == hl) or address?(chars)) and register?(tt) and !tt.bit8? and tt != hl th, tl = tt.split isolate do ld a, code unless code == a ld hl, chars unless chars == hl 3.times { rlca } ld tl, a anda 0b00000111 ld th, a xor tl ld tl, a add hl, th|tl end end
Creates a ZX Spectrum CHAN entry and opens it as a stream #N.
- output
-
a routine address or a 16bit register holding that address, except
hl
.
- input
-
a routine address or a 16bit register holding that address, except
hl
.
- strm_no
-
a stream number (4 - 15) or an 8bit register name;
nil
if none of the streams should be attached.
- chan_name
-
a channel name (immediate value). if
nil
or0
no channel name is being written.
Optionally give the returned namespace label a name
.
Modifies: af
, hl
, bc
, de
.
# File lib/zxlib/sys.rb, line 679 def create_chan_and_open(name = nil, output:, input: nil, strm_no: 4, chan_name: 'U') chan_name = String === chan_name ? chan_name.ord : chan_name.to_i raise ArgumentError, "output or input must not be the hl register" if output == hl or input == hl isolate name, use: [:rom, :vars] do input = rom.error_j if input.nil? output = rom.error_j if output.nil? if register?(strm_no) or pointer?(strm_no) unless strm_no == a ld a, strm_no # stream to open end add a, a # calculate the strm vector offset in hl # ld hl, vars.strms.user # hl points to STRMS #0 # adda_to h, l add vars.strms.user ld l, a ld h, vars.strms>>8 push hl end push input if register?(input) push output if register?(output) ld hl, [vars.prog] # a new channel starts below prog dec hl # ld bc, 0x0005 # make space call rom.make_room inc hl # hl points to 1st byte of new channel data if register?(output) pop de else ld de, output end ld [hl], e inc hl ld [hl], d if register?(input) pop de else ld de, input end # save address of 2nd byte of new channel data push hl unless strm_no.nil? inc hl ld [hl], e inc hl ld [hl], d inc hl unless chan_name.zero? # channel name ld [hl], chan_name end unless strm_no.nil? # open #stream_no to channel pop hl # get address of 2nd byte of output routine ld de, [vars.chans] # calculate the offset to the channel data anda a # and store it in de sbc hl, de ex de, hl # de = our channel address - chans + 1 if register?(strm_no) or pointer?(strm_no) pop hl else ld hl, vars.strms.user[strm_no] end ld [hl], e # lsb of 2nd byte of new channel data inc hl ld [hl], d # msb of 2nd byte of new channel data end end end
Test for cursor keys being pressed.
Options:
t
-
A temporary 8-bit register.
io
-
A label containing
ula
sub-label addressing ULA I/O bus.
Modifies: af
, t
.
Output:
ZF
=0-
if any of the cursor keys is being pressed.
a
-
bits b0..b3=1 if a cursor key is being pressed.
b3 b2 b1 b0 [←] [↓] [↑] [→]
# File lib/zxlib/sys.rb, line 630 def cursor_key_pressed?(t:b, io:self.io) isolate do key_pressed? 0xf7, 0x10, io:io # key [5] ld t, a key_pressed? 0xef, 0x1c, io:io # keys [6] [7] [8] . . rrca ora t # keys [5] [6] [7] [8] . rrca # keys [←] [↓] [↑] [→] end end
Gets a DEF FN argument value address.
Requires: macro_import MathInt
.
argnum
-
1-based argument index (0 is 256), may be a register or a number.
subroutine
-
if
true
will useret
instruction.
not_found
-
if
subroutine
isfalse
andnot_found
is defined, the routine will jump to this address when argument was not found, otherwise success is signalled with ZF=1.
not_found_blk
-
if
subroutine
isfalse
andnot_found
isnil
, the routine produced by the block will be run. Make sure the routine doesn't fall through though.
cf_on_direct
-
if
true
and DEFADD is not defined CF will be set.
When subroutine
is true
or not_found
is nil
, the success is signalled with ZF
:
-
ZF=1 if found,
-
ZF=0 if not found.
hl
points to the argument value when found.
Modifies: af
, hl
and optionally b
unless argnum
== 1.
# File lib/zxlib/sys.rb, line 1031 def find_def_fn_args(argnum=b, subroutine:true, not_found:nil, cf_on_direct:false, ¬_found_blk) isolate use: :vars do |eoc| ld b, argnum unless argnum == b or argnum == 1 ld hl, [vars.defadd] ld a, h ora l scf if cf_on_direct jr exit_on_zf if argnum != 1 loop0 ld a, 5 # skip argument value adda_to h, l end seek_next ld a, [hl] inc hl cp 0x0E # variable argument marker if argnum == 1 if subroutine ret Z else jr Z, eoc end else jr Z, found_arg end cp ?).ord # arguments terminator exit_on_zf jr NZ, seek_next if subroutine inc a # ZF=0, not found ret elsif not_found jp not_found elsif block_given? ns(¬_found_blk) else inc a # ZF=0, not found jr eoc if argnum != 1 end if argnum != 1 found_arg djnz loop0 ret if subroutine end end end
Search for a record that matches a large block of memory.
- +th|tl'+
-
address of the last byte to search + 1 (preserved)
hl
-
target
de
-
source (preserved)
bc
-
record length (preserved)
ZF=1 if found, hl
points immediately after the record that matches, CF=0 ZF=0 if not found, hl
points to the memory address immediately after the last record
Modifies: af
, hl
and stack
# File lib/zxlib/sys.rb, line 817 def find_record(th=h, tl=l) isolate do |eoc| seekloop1 push bc push de seekloop0 ld a, [de] inc de cpi jr NZ, adjust ld a, b ora c jr NZ, seekloop0 pop de pop bc # de - preserved, bc - preserved, hl to next record after found jr eoc # ZF=1 found adjust add hl, bc # next record pop de pop bc ld a, h exx cp th exx jr C, seekloop1 # h < h' jr NZ, eoc # h > h' ld a, l exx cp tl exx jr C, seekloop1 # l < l' ora 1 # ZF = 0 end end
Test for a key or keys being pressed.
line_mask
-
Keyboard half-line mask, may be an 8 bit register. The default is 0 which means all available lines.
key_mask
-
Key mask (b0..b4), may be an 8 bit register. The default is 0b11111 which means all keys in a half-line.
Options:
io
-
A label containing
ula
sub-label addressing ULA I/O bus.
line key bits line key bits b4, b3, b2, b1, b0 b4, b3, b2, b1, b0 0xf7 [5], [4], [3], [2], [1] 0xef [6], [7], [8], [9], [0] 0xfb [T], [R], [E], [W], [Q] 0xdf [Y], [U], [I], [O], [P] 0xfd [G], [F], [D], [S], [A] 0xbf [H], [J], [K], [L], [ENTER] 0xfe [V], [C], [X], [Z], [SHIFT] 0x7f [B], [N], [M], [SS], [SPACE]
Modifies: af
.
Output:
ZF
=0-
(NZ) if any of the specified keys is being pressed.
a
-
bits b0..b4=1 if a key is being pressed at any of the specified half-line.
# File lib/zxlib/sys.rb, line 603 def key_pressed?(line_mask=0, key_mask=0x1f, io:self.io) isolate do if line_mask == 0 xor a else ld a, line_mask unless line_mask == a end inp a, (io.ula) cpl anda key_mask end end
Selects an upper memory bank (0-7) and/or a screen memory page (0-1) to be displayed.
Options:
bank
-
Selects a memory bank available at 0xC000-0xFFFF as an integer or any of the 8-bit registers except
a
, or indirect memory address via a 16-bit register. In this instance you should passtrue
to thescreen:
option if bit-3 of thebank
should select a screen to be displayed.
screen
-
0 - Display screen from bank 5. 1 - Display screen from bank 7.
nil
to preserve screen bit-3 from thebank
register.
disable_intr
-
A boolean flag indicating that the routine should disable interrupts. Provide
false
only if you have already disabled the interrupts.
enable_intr
-
A boolean flag indicating that the routine should enable interrupts. Provide
false
if you need to perform more uninterrupted actions.
mmu_port_in_bc
-
A boolean flag indicating if
bc
registers already contain themmu_port
I/O address. If not thebc
will be loaded withsys128.mmu_port
value.
sys128
-
A label with
mmu_port
sub-label addressing the 128k MMU I/O port (0x7FFD) andmmu_value
addressing memory where the 128k ROM is storing the last value output to MMU I/O port (0x5B5C).
0xFFFF screen: 0 screen: 1 +--------+--------+--------+--------+--------+--------+--------+--------+ | Bank 0 | Bank 1 | Bank 2 | Bank 3 | Bank 4 | Bank 5 | Bank 6 | Bank 7 | | | |(also at| | |(also at| | | | | | 0x8000)| | | 0x4000)| | shadow | | | | | | | screen | | screen | +--------+--------+--------+--------+--------+--------+--------+--------+ 0xC000
Memory banks 1,3,5 and 7 are contended, which reduces the speed of memory access in these banks.
Modifies: af
, bc
, memory at sys128.mmu_value
(0x5B5C).
# File lib/zxlib/sys.rb, line 1136 def mmu128_select_bank(bank:nil, screen:nil, disable_intr:true, enable_intr:true, mmu_port_in_bc:false, sys128:self.sys128) mask = 0b11111111 merg = 0b00000000 unless bank.nil? mask = mask & 0b11111000 if register?(bank) merg = bank else merg = merg | (bank.to_i & 0b00000111) end end unless screen.nil? mask = mask & 0b11110111 unless register?(bank) merg = merg | ((screen.to_i.zero? ? 0 : -1) & 0b00001000) end end isolate do ld bc, sys128.mmu_port unless mmu_port_in_bc di if disable_intr ld a, [sys128.mmu_value] # previous value of port anda mask ora merg unless merg == 0 ld [sys128.mmu_value],a ei if enable_intr # directly after an EI, interrupts aren't accepted. out (c), a end end
Swap displayed screens.
Options:
swap_bank
-
A boolean flag indicating that the routine should additionally swap screen memory banks at 0xC000. For this to have a desired effect bank 5 or 7 should have been previously selected e.g. with
Macros.mmu128_select_bank
.
disable_intr
-
A boolean flag indicating that the routine should disable interrupts. Provide
false
only if you have already disabled the interrupts.
enable_intr
-
A boolean flag indicating that the routine should enable interrupts. Provide
false
if you need to perform more uninterrupted actions.
mmu_port_in_bc
-
A boolean flag indicating if
bc
registers already contain themmu_port
I/O address. If not thebc
will be loaded withsys128.mmu_port
value.
sys128
-
A label with
mmu_port
sub-label addressing the 128k MMU I/O port (0x7FFD) andmmu_value
addressing memory where the 128k ROM is storing the last value output to MMU I/O port (0x5B5C).
Modifies: af
, bc
, memory at sys128.mmu_value
(0x5B5C).
# File lib/zxlib/sys.rb, line 1181 def mmu128_swap_screens(swap_bank:false, disable_intr:true, enable_intr:true, mmu_port_in_bc:false, sys128:self.sys128) swap_bits = if swap_bank then 0b00001010 else 0b00001000 end isolate do ld bc, sys128.mmu_port unless mmu_port_in_bc di if disable_intr ld a, [sys128.mmu_value] # previous value of port xor swap_bits ld [sys128.mmu_value],a ei if enable_intr # directly after an EI, interrupts aren't accepted. out (c), a end end
Moves Basic
program and variables above the screen 1 (to 0x7B00).
check_ensure
-
when
true
checks if a call to MAKE-ROOM is needed.
Modifies: af
, bc
, de
, hl
.
# File lib/zxlib/sys.rb, line 1200 def move_basic_above_scld_screen_memory(check_ensure:false) isolate use: [:memT2k, :vars, :rom] do |eoc| ld hl, memT2k.rambot ld de, [vars.prog] dec de cp a sbc hl, de jr C, eoc if check_ensure jr Z, eoc if check_ensure ld16 bc, hl ex de, hl call rom.make_room end end
Reads a string address and its length from a ZX Basic's stringish FP-value.
hl
-
must point to the 1st byte of the FP-value.
adh
-
most significant byte address output register.
adl
-
least significant byte address output register.
lenh
-
most significant byte length output register.
lenl
-
least significant byte length output register.
hl
will point to the last byte of the FP-value.
T-States: 52.
Modifies: hl
, adh
, adl
, lenl
, lenh
# File lib/zxlib/sys.rb, line 995 def read_arg_string(adh=d, adl=e, lenh=b, lenl=c) raise ArgumentError if [adh, adl, lenl].any? {|r| [h, l].include?(r) } isolate do inc hl ld adl, [hl] inc hl ld adh, [hl] inc hl ld lenl, [hl] inc hl ld lenh, [hl] end end
Reads a 32-bit integer from a ZX Basic's FP-value.
Requires: macro_import ::ZXLib::Math
.
hl
-
must point to the 1st byte of the FP-value.
t3
-
most significant 8-bit output register.
t2
-
8-bit output register.
t1
-
8-bit output register.
t0
-
least significant 8-bit output register.
The result is being loaded into t3
|t2
|t1
|t0
. CF=1 signals that the FP-value is too big to fit into a 32-bit integer. ZF=1 signals that the FP-value is positive. In this instance accumulator a
= 0. ZF=0 signals that the FP-value is negative. In this instance accumulator a
= 0xFF. If the value is negative the integer WON'T BE a twos complement number.
hl
will always point to the last byte of the FP-value.
Modifies: af
, af'
, hl
, bc
, de
.
# File lib/zxlib/sys.rb, line 960 def read_integer32_value(t3=d, t2=e, t1=b, t0=c) # legacy arguments handling (th, tl) if [t3, t2].all? {|t| [bc, de].include?(t)} t1, t0 = t2.split t3, t2 = t3.split end raise ArgumentError unless [t3,t2,t1,t0].uniq.size == 4 and [t3,t2,t1,t0].all? {|t| [b,c,d,e].include?(t)} isolate do ld a, [hl] inc hl ld t3, [hl] inc hl ld t2, [hl] inc hl ld t1, [hl] inc hl ld t0, [hl] fp_to_integer32 t3, t2, t1, t0, exp:a end end
Reads a signed integer from a ZX Basic's FP-value.
hl
-
must point to the 1st byte of the FP-value.
th
-
most significant byte output register.
tl
-
least significant byte output register.
sgn
-
sign byte register.
normal_negative
-
if the twos complement number should be normalized.
t
-
a temporary register, used only with
normal_negative
=true
.
If t
is the accumulator register the a'
register is being used.
The result is being loaded into th
|tl
and sgn
. ZF=0 (NZ) signals the FP-value is not an integer. hl
will always point to the last byte of the FP-value.
T-States: 59, normal_negative=true: (t=a') 91, (t=r) 87.
Modifies: af
, hl
, th
, tl
and sgn
, optionally: t
or af'
.
# File lib/zxlib/sys.rb, line 867 def read_integer_value(th=d, tl=e, sgn=c, normal_negative:false, t:a) raise ArgumentError if [h,l,ixh,ixl,iyh,iyl,a].include?(tl) or !register?(tl) or [h,l,ixh,ixl,iyh,iyl,a].include?(th) or !register?(th) or [h,l,ixh,ixl,iyh,iyl,a].include?(sgn) or !register?(sgn) or (register?(t) and [h,l,th,tl,sgn,ixh,ixl,iyh,iyl].include?(t)) or !register?(t) isolate do |eoc| if normal_negative if t == a ld a, [hl] ex af, af else ld t, [hl] end inc hl ld sgn, [hl] inc hl ld a, [hl] inc hl xor sgn sub sgn ld tl, a ld a, [hl] inc hl adc a, sgn xor sgn ld th, a if t == a ex af, af else ld a, t end ora [hl] else ld a, [hl] inc hl ld sgn, [hl] inc hl ld tl, [hl] inc hl ld th, [hl] inc hl ora [hl] end end end
Reads a positive integer from a ZX Basic's FP-value.
hl
-
must point to the 1st byte of the FP-value.
th
-
most significant byte output register.
tl
-
least significant byte output register.
The result is being loaded into th
|tl
. ZF=0 (NZ) signals the FP-value is not a positive integer. hl
will always point to the last byte of the FP-value.
T-States: 59.
Modifies: af
, hl
, th
and tl
.
# File lib/zxlib/sys.rb, line 926 def read_positive_int_value(th=d, tl=e) raise ArgumentError if [h,l,ixh,ixl,iyh,iyl,a].include?(tl) or [h,l,ixh,ixl,iyh,iyl,a].include?(th) isolate do ld a, [hl] inc hl ora [hl] inc hl ld tl, [hl] inc hl ld th, [hl] inc hl ora [hl] end end
Returns to ZX Basic
with the error report.
- error
-
Error report signature as a number
0..9
or a letterA..R
# File lib/zxlib/sys.rb, line 507 def report_error(error) errno = [*'0'..'9', *'A'..'R'].index(error.to_s.upcase[0]) raise ArgumentError unless errno isolate do err rst 0x08 db errno - 1 end end
Returns to ZX Basic
with the error report if condition is NOT met.
- condition
-
NZ, Z, NC, C, PO, PE, P, M
- error
-
Error report signature as a number
0..9
or a letterA..R
# File lib/zxlib/sys.rb, line 492 def report_error_unless(condition, error) raise ArgumentError unless Condition === condition isolate do |eoc| if condition.jr_ok? jr condition, eoc else jp condition, eoc end err report_error error end end
Restore interrupt handler ZX Spectrum ROM's standard IM1 mode.
- enable_intr
-
If
true
invokeei
instruction at the end.
T-states: 28/24
Modifies: a
, i
.
# File lib/zxlib/sys.rb, line 572 def restore_rom_interrupt_handler(enable_intr:true) isolate do im1 ld a, 0x3F ld i, a ei if enable_intr end end
Creates a routine that returns to the calling ZX-Basic's USR function an FP value.
When returning from a user code an integer from the bc
register is being used as a USR
function return value via STACK-BC routine.
To return a floating point value use this routine instead of invoking ret
.
The FP value to be returned should be held in a
, e
, d
, c
, b
registers.
Options:
pop_ret_address
-
Set to
false
if the STACK-BC return address was already fetched from the machine stack.
rom
-
A namespace label containing the ROM routine addresses as sub-labels.
restore_iy
-
A value to restore
iy
register to. Set tonil
to not restoreiy
.
restore_hl_alt
-
A value to restore
hl'
register to. Set tonil
to not restorehl'
.
# File lib/zxlib/sys.rb, line 1090 def return_with_fp(pop_ret_address:true, rom:self.rom, restore_iy:self.vars_iy, restore_hl_alt:rom.end_calc) isolate do pop hl if pop_ret_address ld iy, restore_iy if restore_iy if restore_hl_alt exx ld hl, restore_hl_alt exx end call rom.stk_store rst rom.fp_calc db 0x38 # end-calc make HL = STKEND-5 ret end end
Creates a routine that sets up custom interrupt handler using ZX Spectrum ROM's unused space as a IM2 mode jump table.
- handler
-
One of the 16bit register pair or an address or a pointer to the address of the handler routine.
- enable_intr
-
If
true
invokeei
instruction at the end.
This routine uses a mode 2 interrupt. In this mode the address of the interrupt routine is formed in the following way: an 8-bit value found on the BUS (which in most ZX Spectrum models is 255) is being added to register i
x 256, which form a vector table address. A word (two bytes) is being read from that address, providing the address where the call to the interrupt routine is made to.
This routine makes these assumptions about the machine:
-
The byte at the memory address 0 contains 243.
-
The 2 consecutive bytes at the memory address
vector_page
x 256 + 8-bit BUS value and the following one, both contain 255. -
The RAM memory between 65524 and 65535 will not be used for other purposes.
- NOTE
-
The assumptions are true for most ZX Spectrum models and clones including Pentagon machines and Timex TC2048. However they are not true for Timex TC2068 or TS2068.
Options:
vector_page
-
A most significant byte of the interrupt vector table address loaded into
i
register. The default is 0x3B so the address of the routine is found at 0x3BFF in most ZX Spectrum models.
Modifies: a
, i
, hl
if handler
is not a register pair.
# File lib/zxlib/sys.rb, line 545 def setup_custom_interrupt_handler(handler, enable_intr:true, vector_page:0x3B) isolate do ld a, 0x18 # 18H is jr ld [0xFFFF], a ld a, 0xC3 # C3H is jp ld [0xFFF4], a set_handler label # set handler part (may be used to only change the routine address) if [bc, de, hl, sp, ix, iy].include?(handler) ld [0xFFF5], handler else ld hl, handler ld [0xFFF5], hl end ld a, vector_page # Supported by ZX Spectrum 128, +2, +2A, +3 and probably most clones. ld i, a # load the accumulator with FF filled page in rom. im2 ei if enable_intr end end