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
hlregister, 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;
nilif none of the streams should be attached.
- chan_name
-
a channel name (immediate value). if
nilor0no 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
ulasub-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
truewill useretinstruction.
not_found-
if
subroutineisfalseandnot_foundis defined, the routine will jump to this address when argument was not found, otherwise success is signalled with ZF=1.
not_found_blk-
if
subroutineisfalseandnot_foundisnil, the routine produced by the block will be run. Make sure the routine doesn't fall through though.
cf_on_direct-
if
trueand 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
ulasub-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 passtrueto thescreen:option if bit-3 of thebankshould select a screen to be displayed.
screen-
0 - Display screen from bank 5. 1 - Display screen from bank 7.
nilto preserve screen bit-3 from thebankregister.
disable_intr-
A boolean flag indicating that the routine should disable interrupts. Provide
falseonly if you have already disabled the interrupts.
enable_intr-
A boolean flag indicating that the routine should enable interrupts. Provide
falseif you need to perform more uninterrupted actions.
mmu_port_in_bc-
A boolean flag indicating if
bcregisters already contain themmu_portI/O address. If not thebcwill be loaded withsys128.mmu_portvalue.
sys128-
A label with
mmu_portsub-label addressing the 128k MMU I/O port (0x7FFD) andmmu_valueaddressing 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
falseonly if you have already disabled the interrupts.
enable_intr-
A boolean flag indicating that the routine should enable interrupts. Provide
falseif you need to perform more uninterrupted actions.
mmu_port_in_bc-
A boolean flag indicating if
bcregisters already contain themmu_portI/O address. If not thebcwill be loaded withsys128.mmu_portvalue.
sys128-
A label with
mmu_portsub-label addressing the 128k MMU I/O port (0x7FFD) andmmu_valueaddressing 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
truechecks 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..9or 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..9or 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
trueinvokeeiinstruction 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
falseif 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
iyregister to. Set tonilto not restoreiy.
restore_hl_alt-
A value to restore
hl'register to. Set tonilto 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
trueinvokeeiinstruction 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_pagex 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
iregister. 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