From
Ivan Roshin
→
To
All
8 February 2002
Hello, All!
Call .1
(c) Ivan Roshchin, Moscow
Fido: 2:5020/689.53
ZXNet: 500:95/462.53
E-mail: asder_ffc@softhome.net
WWW: http://www.ivr.da.ru
Manager for calling subroutines from various memory banks
═══════════════════════════ ═══════════════════════════
("Radiomir. Your computer" 12/2001)
(Added version)
A little theory
──────────────
The address space of the Z80 processor is small - only 64
kilobyte. To access more memory in your computer
ZX Spectrum 128 uses page addressing. Operational
memory (namely, it will be of interest to us) is divided into banks according to
16 kilobytes (in total there are 8 banks with numbers from 0 to 7). On
addresses #4000-#7FFF and #8000-#BFFF banks 5 and
2 respectively, and can be connected to addresses #C000-#FFFF
any of the banks (see Fig. 1).
╔═════════════════╗
║ ║
#C000-#FFFF ║ Any RAM bank ║
║ ║
╚═════════════════╝
│ │
#8000-#BFFF │ RAM 2 │
│ │
├─────────────────┤
│ │
#4000-#7FFF │ RAM 5 ││ │
├─────────────────┤
│ │
#0000-#3FFF │ ROM │
│ │
└─────────────────┘
Fig. 1
I will henceforth refer to banks 5 and 2 as lower memory. They
are always in the processor address space. All
the remaining banks (I will call them upper memory) do not have
this property. Only one of them can be connected.
Number of the memory bank connected to addresses #C000-#FFFF
specified by bits 0, 1 and 2 of the number output to port #7FFD. Reading
from this port is not possible. Therefore, in order to find out
which bank is connected, you need to remember each time you output to the port
output value in a special variable.
When outputting to a port, we cannot address only three
its junior ranks, without touching the rest. Therefore it will be necessary
tell us about the purpose of other bits of port #7FFD. In the third
bit indicates the number of the video page: 0 - standard, 1 -
located in memory bank 7. The fourth bit is the number
connected ROM bank: 0 - BASIC 128, 1 - BASIC 48. And,
finally, outputting a one to the fifth bit will result in a shutdown
additional memory before a hardware reset of the computer.
The remaining (6 and 7) bits are not used in the ZX Spectrum 128.Subroutines in upper memory and features of their calling
────────────────────────── ───────────────────────────
When writing a program it may be necessary
placement of its individual subroutines in various banks of the upper
memory. The reasons for this can be very different. Maybe
the program turns out to be so large that there is simply no other way
fits in memory. Perhaps you need to highlight as much as possible
more lower memory for data that should always be there
accessible from any memory bank, and because of this it is necessary
reduce the space occupied in lower memory by program code,
transferring most of it to the upper memory. Maybe
the program is written taking into account the unequal speed of access to
various memory banks (this feature has both
branded ZX Spectrum 128, and some compatible models),
and executable code is required to be placed only in “fast” banks.
Or maybe a continuous section is needed for data processing
memory as long as possible.
So, when accessing those located in the upper memory
subroutines, if the bank in which the called one is located
subroutine is not connected, difficulties arise. Like for example
call from lower memory a subroutine located in non-
currently connected upper memory bank (see Fig. 2,
arrow 1)? Or how to call from the connected top bankmemory subroutine located in another bank of upper memory
(see Fig. 2, arrow 2)?
connected bank unconnected upper memory banks
upper memory ----------------^----------------
╔══════════════│═══════╗ / \n
║ #FFFF┌───────│──────┐║ ┌──────────────┐ ┌──────────────┐
║ │subroutine a│║ 2 │..............│ │.....│
║ │subroutine b ──────>subroutine i│...│..............│
║ │.............│║ │..............│ │subroutine x│
║ #C000└──────────────┘║ └──────────────┘ └──────^───────┘
║ #BFFF┌──────────────┐║ │
║ │ │║ │
║ │ lower │║ │
║ │ memory │║ │
║ │ │║ 1 │
║ │ ──────────────────── ───────────────────┘
║ │ │║
║ │ │║
║ │ │║
║ #4000└──────────────┘║
║ #3FFF┌──────────────┐║
║ │ │║
║ │ ROM │║
║ │ │║
║ #0000└──────────────┘║
╚══════════════════════╝
Fig. 2
════════════════════════ ════════════════════════
Best regards, Ivan Roshchin.
From
Ivan Roshin
→
To
All
8 February 2002
Hello, All!
Call .2
Obviously, you can't do without switching banks. Therefore
Let's start by looking at the procedure used to do this.
SETPORT procedure - setting up a memory bank.
Input: A - value output to port #7FFD.
Output: The value is output to the port and written to the BANK variable.
Register values: not changed.
SETPORT PUSH BC
LD BC,#7FFD
LD (BANK),A
OUT(C),A
POP B.C.
RET
BANK DS 1 ;current state of port #7FFD is stored
When this procedure is called, the accumulator must contain
a value already prepared for output to the port, i.e. moreover,
that bits 0-2 should contain the number of the required memory bank,
the remaining bits must also be set appropriately
way (see previous section). If, for example, when working
The program has a BASIC-48 ROM connected to display the image
a standard video page is used, and you need to connect a 3rd one
bank of RAM, then for this you need to output number #13 to port #7FFD.
In the future, when I mention "bank number", I will
keep in mind the value already prepared for output to the port
(unless otherwise expressly implied).
And also, please note: in the procedure, first
writing the output value to the BANK variable, and only then -output to port #7FFD. Why is this sequence needed?
actions? Suppose that between the write and output commands in
port an interrupt occurred, and the interrupt handling procedure
it works like this: first it remembers which memory bank was
connected, according to the contents of the BANK variable, after that
sets the bank she needs, performs some actions, and
then restores the "old" memory bank. So, if
the SETPORT procedure first output the value to the port, and then
written to the BANK variable, then in this case after the end
interrupt processing would turn out to be a memory bank installed,
corresponding to the old value of the BANK variable. And this is for us
completely useless.
It seems they have sorted out the connection of banks. Let's go back now
to our routines. What to do with their challenge? Let's consider
first call from lower memory a subroutine located in
currently connected upper memory bank. Very often
In this case, it is also required that after the call the same
memory bank that existed before the call.
The sequence of actions required for this is as follows:
- remember the number of the current memory bank;
- set the bank in which the called one is located
subroutine;
- call her;
- set the memory bank that was before the call (i.e.
with a previously memorized number).
Then the subroutine can be called as follows:LD A,(BANK) ;Remember the number
PUSH AF ;current bank.
LD A,N ;Set the bank in which
CALL SETPORT ;locates the subroutine to be called.
CALL subrout ;Call it.
POP AF ;Set the bank,
CALL SETPORT ;which was before the call.
As you can see, compared to a regular CALL, an extra 13 are spent
byte. And that's not all. If to transfer data to the called
subroutine uses an accumulator, you may need
saving its value (for example, in some free
register) before installing the desired memory bank and restoring
before calling the subroutine, and this will also cost extra
bytes. Likewise, if the accumulator (or flag register!)
used to return the result of a subroutine,
you might need to store its value somewhere (I'm talking
"possibly" because in some cases it is possible to immediately
process the result returned by the subroutine and only then,
when it is no longer needed, execute the commands POP AF: CALL
SETPORT). As a result, as we see, an additional 13 bytes per call
subroutines are a minimum, but in reality there may be
more.
If the same subroutine needs to be called in
several places in the program, then you can, of course, call it itself
format it as a subroutine.Let's now consider another case - a call from a connected
bank of the upper memory of a subroutine located in another bank
top memory. Obviously, the program fragment that switches
jars will have to be moved to the lower memory. As a result, the call
will look something like this:
;In the memory bank where the call originates:
CALL l_subrout
;In lower memory:
l_subrout LD A,(BANK) ;Remember the number of the upper memory bank,
PUSH AF ;where the call came from.
LD A,N ;Set the bank in which
CALL SETPORT ;locates the subroutine to be called.
CALL subrout ;Call it.
POP AF ;Set the bank where the call came from,
JP SETPORT ;and return control there (JP here
;used instead of CALL: RET to
;save a byte).
As you can see, for each subroutine called in this way
Not counting the CALL, at least 16 bytes are spent.
So, when calling subroutines located in the upper memory
it is necessary to use bulky structures, and
located in the lower memory, but there is not enough of it anyway. And if there are such
there are many subroutines (and if the program is large, then there are many of them!), then
The consumption of lower memory will also be significant.
════════════════════════ ════════════════════════
Best regards, Ivan Roshchin.
From
Ivan Roshin
→
To
All
8 February 2002
Hello, All!
═══════════════════ call.3
Call manager: what is it and how does it work
──────────────────────── ────────────────────────
How to reduce memory consumption for subroutine calls? When
while writing my BestView program, I decided to do this: let
a special procedure will be located in the lower memory, so
called a call manager. If necessary, call any
subroutines, just contact the manager by passing him
number of this subroutine, and it will already carry out all the necessary
actions to call it: remember which memory bank is connected,
will set the bank in which the called subroutine is located,
will call it up, after which it will connect the memory bank that was before
call, and returns control.
Address of the called subroutine and memory bank number, in
in which it is located, the manager extracts from the corresponding
tables.
Naturally, the values of all registers must be preserved: with
what values the manager was called with - with the same ones he should
call the required subroutine; what values were in
registers after calling the subroutine - the same should remain and
after the manager finishes his work.
And one more requirement is reentrantness. That is, it is necessary
take into account that at any moment of the manager’s work, from contacting
before leaving it, an interruption may occur, and inthe procedure for processing it may also involve contacting the manager
(and, of course, when the manager runs the subroutine, it, in
in turn, may also contain appeals to the manager).
After the requirements for the manager have been formulated,
All that was left was to write it. And then questions arose.
Question 1. How to transfer the number of the person being called to the manager
subroutines?
Pass the number in some register? But, first of all, then
additional memory will be required for the command to load it into
each call point, and secondly, perhaps this particular
register is used to pass parameters to the callee
subroutine
Specify the number in the byte immediately following
manager call command? Then at each call point there will be
an extra byte is wasted (and if there are more than 256 subroutines, then even
two bytes). It would seem that one byte is nothing, but if you count
total number of calls... In addition, debugging will be difficult
program due to the fact that the debugger will read the byte number
byte of the beginning of the next command.
Fortunately, there is an original solution, devoid of those listed
above the shortcomings! Let's create our own for calling each subroutine
entry point so that control is ultimately transferred to one
and the same address:
;Entry points:
subr_1 NOP ;to call subroutine SUBR_1
subr_2 NOP ;to call subroutine SUBR_2
subr_n NOP ;to call the subroutine SUBR_N;Processing has started...
It can then be determined which entry point was used.
Let n be the address of the command to contact the manager (see Fig. 3). When
When executed, the address of the first byte of the next one will be pushed onto the stack.
teams - n+3. Taking this address and decreasing it by two, we get
address n+1 from which the entry point address is located. Taking the address
entry point and subtracting from it the address of the first entry point (subr_1),
we will get the number of the used entry point, i.e. number
called subroutine. Easy and simple, isn't it? The number is not
requires explicit reference neither in registers nor in memory, and this
No extra bytes are wasted!
n n+1 n+2 n+3
───┬────────────┬──────────────┬ ──────────────┬─────────────┬───
. │ code │ low byte │ high byte │ first byte │ .
. │ commands │ addresses │ addresses │ next │ .
. │ CALL │ entry points │ entry points │ commands │ .
───┴────────────┴──────────────┴ ──────────────┴─────────────┴───
Fig. 3
However, this method has its own characteristics, which
must be taken into account.
Firstly, when calling a subroutine there will be some time
spent on executing a chain of NOPs (4 clock cycles for each). Than
the smaller the serial number of the entry point, the greater will be the length of this
chains and, accordingly, delay. So if for somedelay when calling subroutines is undesirable, it is better to place
their entry points are last.
Secondly, the following optimization technique is often used:
the sequence CALL subrout: RET is replaced simply by JP (or
JR) subrout, thereby saving memory and increasing performance
(gain is 1 byte/17 clock cycles for JP and 2 bytes/15 clock cycles
for JR). But contacting the manager should happen _only_ with
using the CALL! Otherwise the address will not be pushed onto the stack
the next command after CALL and, accordingly, it will not be possible
determine the number of the called subroutine. So this method
optimization in the case when the subroutine is called using
manager cannot be used.
If the names of the subroutines themselves are written in capital letters,
and the names of entry points are lowercase, then in the program text immediately
it will be clear which subroutine is called how: using
manager or not. We see in the text, for example, CALL PRINT is
normal call. We see CALL cls - this is a call using
manager So it immediately becomes clear where you can perform
optimization and where not.
════════════════════════ ════════════════════════
Best regards, Ivan Roshchin.
From
Ivan Roshin
→
To
All
8 February 2002
Hello, All!
Call .4
Question 2. How to determine the number of the bank in which it is located
called subroutine?
The most natural answer is according to the table. Possible in every
byte of the table to store the bank number of the corresponding subroutine,
already prepared for delivery to the port. You can use that one
the fact that only three bits are required to store the bank number, and
store data more economically: one byte contains two numbers, or
(which is more difficult) there are eight numbers in three bytes. There will be memory
spend less, but will have to spend more time on
extracting the bank number from the table and preparing for output to the port.
But please note: on the one hand, the table inevitably
will take up additional space in memory, and on the other hand, we have
as many memory cells occupied by NOP commands as there are
subroutines Is it possible to combine them?
It turns out that it is possible! NOP commands in these cells are only needed
in order to, without performing any actions, go to
start of processing. Now remember that in the Z80 command system
There are other commands that also do not perform any action:
┌─────────┬────────────── ─┬───────────────────────┐
│ command │ code │ three least significant bits of code │
├─────────┼────────────── ─┼───────────────────────┤
│ LD A,A │ #7F=%01111111 │ 7 ││ LD B,B │ #40=%01000000 │ 0 │
│ LD C,C │ #49=%01001001 │ 1 │
│ LD D,D │ #52=%01010010 │ 2 │
│ LD E,E │ #5B=%01011011 │ 3 │
│ LD H,H │ #64=%01100100 │ 4 │
│ LD L,L │ #6D=%01101101 │ 5 │
└─────────┴────────────── ─┴───────────────────────┘
Table 1
In total, together with NOP, there are eight teams, and banks
memory is also eight! This means that between them we can establish
one-to-one correspondence. And if for each subroutine we
let's put one of eight at the entry point address to call it
commands corresponding to the memory bank in which this
subroutine, then don’t waste extra memory on the bank table
you have to!
Here it is, the beauty of the code!!! Teams that seemed
completely useless, suddenly they turned out to be the only ones needed!
At the same time, look how well it turns out: all seven
NOP-like instructions have different three least significant bits of the code (see Table 1).
So it is convenient to associate each of these commands with a memory bank,
whose number is the three least significant bits of the code of this command. Then for
Determining the bank number by command code will be enough
reset the five most significant bits with the AND %111 command.
As you can see, there is no team in the table that has three
the least significant bits of the code are 6. Therefore, we will put the sixth bank inmatching the NOP command. The code for this command is 0. It would be
Of course, it would be much more convenient if the three least significant bits of the NOP code
would be equal to 6 (or if among the seven NOP-like instructions there were no
turned out to be commands with the three least significant bits of the code equal to 0):
then when determining the bank number by command code, do not
additional checks would be required. But what is not there
no...
════════════════════════ ════════════════════════
Best regards, Ivan Roshchin.
From
Ivan Roshin
→
To
All
8 February 2002
Hello, All!
Call .5
Question 3. As mentioned above, to determine the number
the called subroutine needs to pop the return address from the stack and
perform certain actions. In this case, obviously, there will be
some registers are used. Therefore the initial values
these registers must be saved, and before calling the subroutine -
restore.
But if something is required in a reentrant section of code
save and then restore, then you can use it for this
only stack! In fact, look what happens if you save
values in fixed memory locations: if between saving
and restoration will occur an interruption, and the processing procedure
interrupt will refer to the same section of code, then saving
will again be executed in the same memory cells, and their original
the meaning will be lost!
But if we first save the values used on the stack
registers, we will no longer be able to access the return address,
After all, now he will not be at the top of the stack! And this is not the only one
this kind of collision. See: before running the subroutine we
you need to remember the number of the current memory bank on the stack so that later,
after launch, restore it. But if first we remember in
register values on the stack, and then the bank number, then how will we
restore register values before calling a subroutine?They won't be at the top of the stack anymore! And one more thing: how will we
pop this bank number from the stack after finishing work
running subroutine? Before this you will have to save it on the stack
the values of the registers used, which means the bank number is no longer
will be on top of the stack. And how can this be???
Question 4. How, exactly, do you run a subroutine? Let
We know her address, so what? Write this address in the field
operand of the CALL instruction and execute it? Yeah, they ran away! One more time
I repeat: if something is required in a reentrant section of code
save (in this case the launch address) so that later
use (in this case when executing a CALL command), then
fixed memory cells (in this case the operand field
CALL commands) cannot be used for this!
The answers to these two questions lie in unconventional
using the stack and commands for working with it. I think the best way
explain this - consider in detail all the manipulations with the stack in
manager.
In the pictures, to the right of each element of the stack, its
length in bytes. The top of the stack is shown at the top, but note that
that in memory the stack is stored upside down: the top corresponds to
the lowest address, or as they say, the stack grows down. Address
The top of the stack is contained in the SP register.
Initial state when calling the manager:
┌─────────────────┐
│ return address │ 2
├─────────────────┤│.............│
We reserve 5 bytes on the stack. To do this, it is enough to reduce
SP by 5 (don't forget: the stack grows down!). Reduction in progress
like this: DEC SP: PUSH HL: PUSH HL. What will be pushed onto the stack?
PUSH commands, in this case it doesn’t matter at all: we
we use them only to reduce SP (because PUSH per byte
shorter than two DEC SP commands).
┌─────────────────┐
│ reserve │ 5
├─────────────────┤
│ return address │ 2
├─────────────────┤
│.............│
We save HL, DE, AF on the stack.
┌─────────────────┐ ┐
│ AF │ 2 │
├─────────────────┤ │
│ DE │ 2 │
├─────────────────┤ ├ 11
│ HL │ 2 │
├─────────────────┤ │
│ reserve │ 5 │
├─────────────────┤ ┘
│ return address │ 2
├─────────────────┤
│.............│
Knowing the address of the top of the stack and the offset of the element on the stack, you can
access it even if it's not the top element! A
we know the offset of the return address: as can be seen from the figure, it
equals 11. Using the return address, we determine the number of the called
subroutine and the number of the memory bank in which it is located.
┌─────────────────┐ ┐
│ AF │ 2 │
├─────────────────┤ │
│ DE │ 2 │
├─────────────────┤ ├ 10
│ HL │ 2 │├─────────────────┤ │
│ reserve │ 4 │
├─────────────────┤ ┘
│ reserve │ 1
├─────────────────┤
│ return address │ 2
├─────────────────┤
│.............│
════════════════════════ ════════════════════════
Best regards, Ivan Roshchin.
From
Ivan Roshin
→
To
All
8 February 2002
Hello, All!
═══════════════════ call.6
We determine the address of the called subroutine by its number.
We save the number of the current (i.e. set when calling
manager) memory bank in one of the previously reserved
stack bytes, at address SP+10.
┌─────────────────┐
│ AF │ 2
├─────────────────┤
│ DE │ 2
├─────────────────┤
│ HL │ 2
├─────────────────┤
│ reserve │ 4
├─────────────────┤
│ bank number, │
│connected at│ 1
│calling the manager │
├─────────────────┤
│ return address │ 2
├─────────────────┤
│.............│
We write in the remaining 4 bytes of the reserve the address at which
control will be transferred when executing the RET command at the end
called subroutine (this is the SEL_EXIT address related to
manager), and the address of the called subroutine.
┌─────────────────┐
│ AF │ 2
├─────────────────┤
│ DE │ 2
├─────────────────┤
│ HL │ 2
├─────────────────┤
│ address │ 2
├─────────────────┤
│ address SEL_EXIT │ 2
├─────────────────┤
│ bank number, │
│connected at│ 1
│calling the manager │
├─────────────────┤
│ return address │ 2├─────────────────┤
│.............│
We connect the subroutine bank. We remove previously stored in
stack values HL, DE, AF. Now the values of all registers are like this
the same as they were when calling the manager.
┌─────────────────┐
│ address │ 2
├─────────────────┤
│ address SEL_EXIT │ 2
├─────────────────┤
│ bank number, │
│connected at│ 1
│calling the manager │
├─────────────────┤
│ return address │ 2
├─────────────────┤
│.............│
We execute the RET command, and the processor will remove from the stack
address of the subroutine and transfer control over it. As you can see,
RET is used here to call a subroutine, although the usual
The purpose of this command is, on the contrary, to exit the subroutine. Here
such a non-standard approach. :-)
┌─────────────────┐
│ address SEL_EXIT │ 2
├─────────────────┤
│ bank number, │
│connected at│ 1
│calling the manager │
├─────────────────┤
│ return address │ 2
├─────────────────┤
│.............│
After the subroutine is executed, when exiting it
on the RET command, control will be transferred to the next command
manager (with address SEL_EXIT).
┌─────────────────┐
│ bank number, │
│connected at│ 1
│calling the manager │
├─────────────────┤│ return address │ 2
├─────────────────┤
│.............│
We remember HL and AF on the stack.
┌─────────────────┐ ┐
│ AF │ 2 │
├─────────────────┤ ├ 4
│ HL │ 2 │
├─────────────────┤ ┘
│ bank number, │
│connected at│ 1
│calling the manager │
├─────────────────┤
│ return address │ 2
├─────────────────┤
│.............│
════════════════════════ ════════════════════════
Best regards, Ivan Roshchin.
From
Ivan Roshin
→
To
All
8 February 2002
Hello, All!
Call .7
At address SP+4 is the number of the memory bank that was
connected when calling the manager. Let's install this bank. Filming
from the AF and HL stack. Now the contents of all registers are the same,
what happened when exiting the subroutine.
┌─────────────────┐
│ bank number, │
│connected at│ 1
│calling the manager │
├─────────────────┤
│ return address │ 2
├─────────────────┤
│.............│
Using the INC SP command we remove the now unnecessary number from the stack
bank.
┌─────────────────┐
│ return address │ 2
├─────────────────┤
│.............│
The manager's job is complete! Using the RET command we return
management.
And also, pay attention: the scope of the manager
turned out to be wider than intended when it was created, i.e. him
can be used not only to call subroutines,
located in the upper memory. Let, for example, we need
call a subroutine located in lower memory, but
processing data located in a specific bank
top memory. Nothing could be simpler! We place information about it (address
and bank number with data) to the manager, and you can call it,
and from any memory bank. And if the subroutine needs to be called
first to process data in one memory bank, then inanother bank, then you can simply describe it several times in
manager, indicating the same address each time, but different
memory bank numbers.
Well, all that remains is to provide the manager's listing. After
such detailed explanations, I think, there are no incomprehensible places in it
there will be!
Call Manager Listing
────────────────────────
;NOP-like command codes corresponding to each
;from 8 memory banks:
bank_0 EQU #40 ;LD B,B
bank_1 EQU #49 ;LD C,C
bank_2 EQU #52 ;LD D,D
bank_3 EQU #5B ;LD E,E
bank_4 EQU #64 ;LD H,H
bank_5 EQU #6D ;LD L,L
bank_6 EQU #00 ;NOP
bank_7 EQU #7F ;LD A,A
;Table of addresses of subroutines:
SEL_TAB DW SUBR_1
DW SUBR_2
..........
DW SUBR_N
;In principle, the table of addresses can be placed at the top
;memory, then in the lower memory the cost will be only 1 byte
;for each subroutine (without taking into account the length of the executable code
;manager).
;Entry points:
SEL_BEG
subr_1 DB bank_3
subr_2 DB bank_6
subr_n DB bank_1
;Processing has started:
DEC SP ;reserved in the stack
PUSH HL ;5 bytes
PUSH HL
PUSH HL ;save
PUSH DE ;used
PUSH AF ;registers
LD HL,11
ADD HL,SP
LD E,(HL)
INC HL
LD D,(HL)
;DE - return address after the call;;this address is preceded by a command
;CALL XXXX; This is the address XXXX and we take it:
EX DE,HL
DEC HL
LD D,(HL)
DEC HL
LD E,(HL)
EX DE,HL
════════════════════════ ════════════════════════
Best regards, Ivan Roshchin.
From
Ivan Roshin
→
To
All
8 February 2002
Hello, All!
Call .8
;In HL address XXXX; look at what command is located on it, and
;by its code we determine the number of the memory bank in which it is located
;subroutine called:
LD A,(HL)
AND A
JR Z,BANK_M_1 ;if NOP
AND 7 ;otherwise - bank number in the lower three
OR #10 ;bits of command code; OR #10 - for
JR BANK_M_2 ;sets the remaining bits of the port.
BANK_M_1 LD A,#16 ;NOP corresponds to bank 6.
;Attention! If the program does not use the main screen and/or
;not BASIC-48 ROM, then the values in the commands OR #10 and LD A,#16
;must be adjusted!
;Now there is a number in the battery that needs to be output to port #7FFD
;to connect the memory bank where the subroutine is located.
;Using the entry point address (specified in HL) we determine the address in
;table where the address of the called subroutine is located.
;The address you are looking for in the table is (HL-SEL_BEG)*2+SEL_TAB, or what
;same thing (but easier to calculate), 2*HL-2*SEL_BEG+SEL_TAB.
BANK_M_2 ADD HL,HL ;*2
;Brackets in the expression loaded into DE are needed because multiplication
;negative numbers when evaluating expressions in assembly language
;ZX ASM, which I use, does not execute correctly.
LD DE,-(2*SEL_BEG)+SEL_TAB
ADD HL,DE;Read in DE the address of the called subroutine:
LD E,(HL)
INC HL
LD D,(HL)
;Place the bank number on the stack:
LD HL,10
ADD HL,SP
LD (HL),N ;N is the initial value of the BANK variable.
BANK EQU $-1
;Return address:
DEC HL
LD (HL),SEL_EXIT/256 ;high byte
DEC HL
LD (HL),SEL_EXIT256 ;low byte
;Address of the called subroutine:
DEC HL
LD(HL),D
DEC HL
LD(HL),E
;Set the bank of the called subroutine:
CALL SETPORT ;N bank
POP AF
POP DE
POP HL
;On the stack now is the address of the called subroutine, followed by the address
;SEL_EXIT commands.
RET ;start subroutine
;Restore the bank whose number was saved on the stack:
SEL_EXIT EX (SP),HL
PUSH AF
LD A,L
CALL SETPORT
POP AF
EX (SP),HL
INC SP
RET
Optimization options
───────────────────────
A fragment in which, according to the code of a NOP-like command,
located at the entry point, the number that is needed is determined
output to port #7FFD to install a memory bank from the called
subroutine:
LD A,(HL)
AND A
JR Z,BANK_M_1 ;if NOPAND 7 ;otherwise - bank number in the lower three
OR #10 ;bits of command code; OR #10 - for
JR BANK_M_2 ;sets the remaining bits of the port.
BANK_M_1 LD A,#16 ;NOP corresponds to bank 6.
BANK_M_2 ..........
═══════════════════════ ════════════════════════
Best regards, Ivan Roshchin.
From
Ivan Roshin
→
To
All
8 February 2002
Hello, All!
Call .9
can be optimized like this:
LD A,(HL)
AND A
JR NZ,BANK_M
LD A,6
BANK_M AND 7
OR#10
It turns out two bytes shorter. Moreover, if the called
the subroutine is not located in the sixth memory bank, then the gain is
time will be 7 clock cycles, and if in the sixth bank - vice versa,
the operating time will be 9 cycles longer. Such optimization is beneficial
or not, depends on the specific case and the selected criteria
profitability. For example, if the main thing is to reduce the size, or if
called subroutines are located mainly not in the sixth bank
(i.e., on average there will be a gain in time), then the method is suitable.
And if the goal is to speed up the startup time as much as possible
subroutines in the sixth memory bank, then this method will not work
(you can, however, try to place such subroutines in
any other bank).
If there are no subroutines in the sixth bank at all, you can write
even like this:
LD A,(HL)
AND 7
OR#10
which will be 7 bytes shorter and 23 clock cycles faster than the original
option.
As we can see, if subroutines are called using the manager
in the sixth memory bank, then the manager turns out to be longer and
works slower. And all because among NOP-like teamsthere is no one whose least significant three bits of code would be equal to six.
But there is an optimization method to get around this
inconvenience! True, when using it it is required that although
if one of the eight memory banks were not called up using
subroutine manager (in real programs this is a condition
seems to be done almost always).
See: if you determine the number output to port #7FFD by
code of a NOP-like command using AND 7: OR #10 commands, then
You can call subroutines from all memory banks except
sixth, i.e. from banks 0,1,2,3,4,5,7. And we need, let's say,
call subroutines from all banks except the third. Then
Let's write it like this: AND 7: XOR 5: OR #10. What happens? XOR 5 command
turns the numbers 0,1,2,3,4,5,7 into 5,4,7,6,1,0,2 respectively.
And this is all the numbers from 0 to 7, except 3.
Two XOR 5: OR #10 instructions can be replaced by one XOR #15,
since after AND 7 the most significant digits of the battery are reset to zero. B
As a result, the determination of the number output to the port using the command code will be
look like this: AND 7: XOR #15. Running this snippet
takes exactly the same amount of time as AND 7: OR #10, and the length
he has the same one.
But now we can choose the unused bank ourselves.
If the number of this bank is n, then the operand in the XOR instruction is evaluated
according to the formula 6 XOR n. In this case it is equal to 6 XOR 3 (%110 XOR
%011), i.e. 5 (%101). Combining with the instruction operand OR #10, intoAs a result we get #15.
And, of course, you still need to redefine the values of the constants
bank_0 - bank_7. Let's see which numbers turn into which when
executing the XOR command, and renaming the constants. In this
in case bank_0 is renamed to bank_5, bank_1 to bank_4 and so on
further, we end up with:
bank_5 EQU #40 ;LD B,B
bank_4 EQU #49 ;LD C,C
bank_7 EQU #52 ;LD D,D
bank_6 EQU #5B ;LD E,E
bank_1 EQU #64 ;LD H,H
bank_0 EQU #6D ;LD L,L
bank_2 EQU #7F ;LD A,A
════════════════════════ ════════════════════════
Best regards, Ivan Roshchin.
From
Ivan Roshin
→
To
All
8 February 2002
Hello, All!
Call .1
(c) Ivan Roshchin, Moscow
Fido: 2:5020/689.53
ZXNet: 500:95/462.53
E-mail: asder_ffc@softhome.net
WWW: http://www.ivr.da.ru
Manager for calling subroutines from various memory banks
═══════════════════════════ ═══════════════════════════
("Radiomir. Your computer" 12/2001)
(Added version)
A little theory
──────────────
The address space of the Z80 processor is small - only 64
kilobyte. To access more memory in your computer
ZX Spectrum 128 uses page addressing. Operational
memory (namely, it will be of interest to us) is divided into banks according to
16 kilobytes (in total there are 8 banks with numbers from 0 to 7). On
addresses #4000-#7FFF and #8000-#BFFF banks 5 and
2 respectively, and can be connected to addresses #C000-#FFFF
any of the banks (see Fig. 1).
╔═════════════════╗
║ ║
#C000-#FFFF ║ Any RAM bank ║
║ ║
╚═════════════════╝
│ │
#8000-#BFFF │ RAM 2 │
│ │
├─────────────────┤
│ │
#4000-#7FFF │ RAM 5 ││ │
├─────────────────┤
│ │
#0000-#3FFF │ ROM │
│ │
└─────────────────┘
Fig. 1
I will henceforth refer to banks 5 and 2 as lower memory. They
are always in the processor address space. All
the remaining banks (I will call them upper memory) do not have
this property. Only one of them can be connected.
Number of the memory bank connected to addresses #C000-#FFFF
specified by bits 0, 1 and 2 of the number output to port #7FFD. Reading
from this port is not possible. Therefore, in order to find out
which bank is connected, you need to remember each time you output to the port
output value in a special variable.
When outputting to a port, we cannot address only three
its junior ranks, without touching the rest. Therefore it will be necessary
tell us about the purpose of other bits of port #7FFD. In the third
bit indicates the number of the video page: 0 - standard, 1 -
located in memory bank 7. The fourth bit is the number
connected ROM bank: 0 - BASIC 128, 1 - BASIC 48. And,
finally, outputting a one to the fifth bit will result in a shutdown
additional memory before a hardware reset of the computer.
The remaining (6 and 7) bits are not used in the ZX Spectrum 128.Subroutines in upper memory and features of their calling
────────────────────────── ───────────────────────────
When writing a program it may be necessary
placement of its individual subroutines in various banks of the upper
memory. The reasons for this can be very different. Maybe
the program turns out to be so large that there is simply no other way
fits in memory. Perhaps you need to highlight as much as possible
more lower memory for data that should always be there
accessible from any memory bank, and because of this it is necessary
reduce the space occupied in lower memory by program code,
transferring most of it to the upper memory. Maybe
the program is written taking into account the unequal speed of access to
various memory banks (this feature has both
branded ZX Spectrum 128, and some compatible models),
and executable code is required to be placed only in “fast” banks.
Or maybe a continuous section is needed for data processing
memory as long as possible.
So, when accessing those located in the upper memory
subroutines, if the bank in which the called one is located
subroutine is not connected, difficulties arise. Like for example
call from lower memory a subroutine located in non-
currently connected upper memory bank (see Fig. 2,
arrow 1)? Or how to call from the connected top bankmemory subroutine located in another bank of upper memory
(see Fig. 2, arrow 2)?
connected bank unconnected upper memory banks
upper memory ----------------^----------------
╔══════════════│═══════╗ / \n
║ #FFFF┌───────│──────┐║ ┌──────────────┐ ┌──────────────┐
║ │subroutine a│║ 2 │..............│ │.....│
║ │subroutine b ──────>subroutine i│...│..............│
║ │.............│║ │..............│ │subroutine x│
║ #C000└──────────────┘║ └──────────────┘ └──────^───────┘
║ #BFFF┌──────────────┐║ │
║ │ │║ │
║ │ lower │║ │
║ │ memory │║ │
║ │ │║ 1 │
║ │ ──────────────────── ───────────────────┘
║ │ │║
║ │ │║
║ │ │║
║ #4000└──────────────┘║
║ #3FFF┌──────────────┐║
║ │ │║
║ │ ROM │║
║ │ │║
║ #0000└──────────────┘║
╚══════════════════════╝
Fig. 2
════════════════════════ ════════════════════════
Best regards, Ivan Roshchin.
From
Ivan Roshin
→
To
All
8 February 2002
Hello, All!
═══════════════════ call.3
Call manager: what is it and how does it work
──────────────────────── ────────────────────────
How to reduce memory consumption for subroutine calls? When
while writing my BestView program, I decided to do this: let
a special procedure will be located in the lower memory, so
called a call manager. If necessary, call any
subroutines, just contact the manager by passing him
number of this subroutine, and it will already carry out all the necessary
actions to call it: remember which memory bank is connected,
will set the bank in which the called subroutine is located,
will call it up, after which it will connect the memory bank that was before
call, and returns control.
Address of the called subroutine and memory bank number, in
in which it is located, the manager extracts from the corresponding
tables.
Naturally, the values of all registers must be preserved: with
what values the manager was called with - with the same ones he should
call the required subroutine; what values were in
registers after calling the subroutine - the same should remain and
after the manager finishes his work.
And one more requirement is reentrantness. That is, it is necessary
take into account that at any moment of the manager’s work, from contacting
before leaving it, an interruption may occur, and inthe procedure for processing it may also involve contacting the manager
(and, of course, when the manager runs the subroutine, it, in
in turn, may also contain appeals to the manager).
After the requirements for the manager have been formulated,
All that was left was to write it. And then questions arose.
Question 1. How to transfer the number of the person being called to the manager
subroutines?
Pass the number in some register? But first of all then
additional memory will be required for the command to load it into
each call point, and secondly, perhaps this particular
register is used to pass parameters to the callee
subroutine
Specify the number in the byte immediately following
manager call command? Then at each call point there will be
an extra byte is wasted (and if there are more than 256 subroutines, then even
two bytes). It would seem that one byte is nothing, but if you count
total number of calls... In addition, debugging will be difficult
program due to the fact that the debugger will read the byte number
byte of the beginning of the next command.
Fortunately, there is an original solution, devoid of those listed
above the shortcomings! Let's create our own for calling each subroutine
entry point so that control is ultimately transferred to one
and the same address:
;Entry points:
subr_1 NOP ;to call subroutine SUBR_1
subr_2 NOP ;to call subroutine SUBR_2
subr_n NOP ;to call the subroutine SUBR_N;Processing has started...
It can then be determined which entry point was used.
Let n be the address of the command to contact the manager (see Fig. 3). When
When executed, the address of the first byte of the next one will be pushed onto the stack.
teams - n+3. Taking this address and decreasing it by two, we get
address n+1 from which the entry point address is located. Taking the address
entry point and subtracting from it the address of the first entry point (subr_1),
we will get the number of the used entry point, i.e. number
called subroutine. Easy and simple, isn't it? The number is not
requires explicit reference neither in registers nor in memory, and this
No extra bytes are wasted!
n n+1 n+2 n+3
───┬────────────┬──────────────┬ ──────────────┬─────────────┬───
. │ code │ low byte │ high byte │ first byte │ .
. │ commands │ addresses │ addresses │ next │ .
. │ CALL │ entry points │ entry points │ commands │ .
───┴────────────┴──────────────┴ ──────────────┴─────────────┴───
Fig. 3
However, this method has its own characteristics, which
must be taken into account.
Firstly, when calling a subroutine there will be some time
spent on executing a chain of NOPs (4 clock cycles for each). Than
the smaller the serial number of the entry point, the greater will be the length of this
chains and, accordingly, delay. So if for somedelay when calling subroutines is undesirable, it is better to place
their entry points are last.
Secondly, the following optimization technique is often used:
the sequence CALL subrout: RET is replaced simply by JP (or
JR) subrout, thereby saving memory and increasing performance
(gain is 1 byte/17 clock cycles for JP and 2 bytes/15 clock cycles
for JR). But contacting a manager should happen _only_ with
using the CALL! Otherwise the address will not be pushed onto the stack
the next command after CALL and, accordingly, it will not be possible
determine the number of the called subroutine. So this method
optimization in the case when the subroutine is called using
manager cannot be used.
If the names of the subroutines themselves are written in capital letters,
and the names of entry points are lowercase, then in the program text immediately
it will be clear which subroutine is called how: using
manager or not. We see in the text, for example, CALL PRINT is
normal call. We see CALL cls - this is a call using
manager So it immediately becomes clear where you can perform
optimization and where not.
════════════════════════ ════════════════════════
Best regards, Ivan Roshchin.
From
Ivan Roshin
→
To
All
8 February 2002
Hello, All!
Call .4
Question 2. How to determine the number of the bank in which it is located
called subroutine?
The most natural answer is from the table. Possible in every
byte of the table to store the bank number of the corresponding subroutine,
already prepared for delivery to the port. You can use that one
the fact that only three bits are required to store the bank number, and
store data more economically: one byte contains two numbers, or
(which is more difficult) there are eight numbers in three bytes. There will be memory
spend less, but will have to spend more time on
extracting the bank number from the table and preparing for output to the port.
But please note: on the one hand, the table inevitably
will take up additional space in memory, and on the other hand, we have
as many memory cells occupied by NOP commands as there are
subroutines Is it possible to combine them?
It turns out that it is possible! NOP commands in these cells are only needed
in order to, without performing any actions, go to
start of processing. Now remember that in the Z80 command system
There are other commands that also do not perform any action:
┌─────────┬────────────── ─┬───────────────────────┐
│ command │ code │ three least significant bits of code │
├─────────┼────────────── ─┼───────────────────────┤
│ LD A,A │ #7F=%01111111 │ 7 ││ LD B,B │ #40=%01000000 │ 0 │
│ LD C,C │ #49=%01001001 │ 1 │
│ LD D,D │ #52=%01010010 │ 2 │
│ LD E,E │ #5B=%01011011 │ 3 │
│ LD H,H │ #64=%01100100 │ 4 │
│ LD L,L │ #6D=%01101101 │ 5 │
└─────────┴────────────── ─┴───────────────────────┘
Table 1
In total, together with NOP, there are eight teams, and banks
memory is also eight! This means that between them we can establish
one-to-one correspondence. And if for each subroutine we
let's put one of eight at the entry point address to call it
commands corresponding to the memory bank in which this
subroutine, then don’t waste extra memory on the bank table
you have to!
Here it is, the beauty of the code!!! Teams that seemed
completely useless, suddenly they turned out to be the only ones needed!
At the same time, look how well it turns out: all seven
NOP-like instructions have different three least significant bits of the code (see Table 1).
So it is convenient to associate each of these commands with a memory bank,
whose number is the three least significant bits of the code of this command. Then for
Determining the bank number by command code will be enough
reset the five most significant bits with the AND %111 command.
As you can see, there is no team in the table that has three
the least significant bits of the code are 6. Therefore, we will put the sixth bank inmatching the NOP command. The code for this command is 0. It would be
Of course, it would be much more convenient if the three least significant bits of the NOP code
would be equal to 6 (or if among the seven NOP-like instructions there were no
turned out to be commands with the three least significant bits of the code equal to 0):
then when determining the bank number by command code, do not
additional checks would be required. But what is not there
no...
════════════════════════ ════════════════════════
Best regards, Ivan Roshchin.
From
Ivan Roshin
→
To
All
8 February 2002
Hello, All!
Call .5
Question 3. As mentioned above, to determine the number
the called subroutine needs to pop the return address from the stack and
perform certain actions. In this case, obviously, there will be
some registers are used. Therefore the initial values
these registers must be saved, and before calling the subroutine -
restore.
But if something is required in a reentrant section of code
save and then restore, then you can use it for this
only stack! In fact, look what happens if you save
values in fixed memory locations: if between saving
and restoration will occur an interruption, and the processing procedure
interrupt will refer to the same section of code, then saving
will again be executed in the same memory cells, and their original
the meaning will be lost!
But if we first store the values used on the stack
registers, we will no longer be able to access the return address,
After all, now he will not be at the top of the stack! And this is not the only one
this kind of collision. See: before running the subroutine we
you need to remember the number of the current memory bank on the stack so that later,
after launch, restore it. But if we first remember in
register values on the stack, and then the bank number, then how will we
restore register values before calling a subroutine?They won't be at the top of the stack anymore! And one more thing: how will we
pop this bank number from the stack after finishing work
running subroutine? Before this you will have to save it on the stack
the values of the registers used, which means the bank number is no longer
will be on top of the stack. And how can this be???
Question 4. How, exactly, do you run a subroutine? Let
We know her address, so what? Write this address in the field
operand of the CALL instruction and execute it? Yeah, they ran away! One more time
I repeat: if something is required in a reentrant section of code
save (in this case the launch address) so that later
use (in this case when executing a CALL command), then
fixed memory cells (in this case the operand field
CALL commands) cannot be used for this!
The answers to these two questions lie in unconventional
using the stack and commands for working with it. I think the best way
explain this - consider in detail all the manipulations with the stack in
manager.
In the pictures, to the right of each stack element, its
length in bytes. The top of the stack is shown at the top, but note that
that in memory the stack is stored upside down: the top corresponds to
the lowest address, or as they say, the stack grows down. Address
The top of the stack is contained in the SP register.
Initial state when calling the manager:
┌─────────────────┐
│ return address │ 2
├─────────────────┤│.............│
We reserve 5 bytes on the stack. To do this, it is enough to reduce
SP by 5 (don't forget: the stack grows down!). Reduction in progress
like this: DEC SP: PUSH HL: PUSH HL. What will be pushed onto the stack?
PUSH commands, in this case it doesn’t matter at all: we
we use them only to reduce SP (because PUSH per byte
shorter than two DEC SP commands).
┌─────────────────┐
│ reserve │ 5
├─────────────────┤
│ return address │ 2
├─────────────────┤
│.............│
We save HL, DE, AF on the stack.
┌─────────────────┐ ┐
│ AF │ 2 │
├─────────────────┤ │
│ DE │ 2 │
├─────────────────┤ ├ 11
│ HL │ 2 │
├─────────────────┤ │
│ reserve │ 5 │
├─────────────────┤ ┘
│ return address │ 2
├─────────────────┤
│.............│
Knowing the address of the top of the stack and the offset of the element on the stack, you can
access it even if it's not the top element! A
we know the offset of the return address: as can be seen from the figure, it
equals 11. Using the return address, we determine the number of the called
subroutine and the number of the memory bank in which it is located.
┌─────────────────┐ ┐
│ AF │ 2 │
├─────────────────┤ │
│ DE │ 2 │
├─────────────────┤ ├ 10
│ HL │ 2 │├─────────────────┤ │
│ reserve │ 4 │
├─────────────────┤ ┘
│ reserve │ 1
├─────────────────┤
│ return address │ 2
├─────────────────┤
│.............│
════════════════════════ ════════════════════════
Best regards, Ivan Roshchin.
From
Ivan Roshin
→
To
All
8 February 2002
Hello, All!
Call .7
At address SP+4 is the number of the memory bank that was
connected when calling the manager. Let's install this bank. Filming
from the AF and HL stack. Now the contents of all registers are the same,
what happened when exiting the subroutine.
┌─────────────────┐
│ bank number, │
│connected at│ 1
│calling the manager │
├─────────────────┤
│ return address │ 2
├─────────────────┤
│.............│
Using the INC SP command we remove the now unnecessary number from the stack
bank.
┌─────────────────┐
│ return address │ 2
├─────────────────┤
│.............│
The manager's job is complete! Using the RET command we return
management.
And also, pay attention: the scope of the manager
turned out to be wider than intended when it was created, i.e. him
can be used not only to call subroutines,
located in the upper memory. Let, for example, we need
call a subroutine located in lower memory, but
processing data located in a specific bank
top memory. Nothing could be easier! We place information about it (address
and bank number with data) to the manager, and you can call it,
and from any memory bank. And if the subroutine needs to be called
first to process data in one memory bank, then inanother bank, then you can simply describe it several times in
manager, indicating the same address each time, but different
memory bank numbers.
Well, all that remains is to provide the manager's listing. After
such detailed explanations, I think, there are no incomprehensible places in it
there will be!
Call Manager Listing
────────────────────────
;NOP-like command codes corresponding to each
;from 8 memory banks:
bank_0 EQU #40 ;LD B,B
bank_1 EQU #49 ;LD C,C
bank_2 EQU #52 ;LD D,D
bank_3 EQU #5B ;LD E,E
bank_4 EQU #64 ;LD H,H
bank_5 EQU #6D ;LD L,L
bank_6 EQU #00 ;NOP
bank_7 EQU #7F ;LD A,A
;Table of addresses of subroutines:
SEL_TAB DW SUBR_1
DW SUBR_2
..........
DW SUBR_N
;In principle, the table of addresses can be placed at the top
;memory, then in the lower memory the cost will be only 1 byte
;for each subroutine (without taking into account the length of the executable code
;manager).
;Entry points:
SEL_BEG
subr_1 DB bank_3
subr_2 DB bank_6
subr_n DB bank_1
;Processing has started:
DEC SP ;reserved in the stack
PUSH HL ;5 bytes
PUSH HL
PUSH HL ;save
PUSH DE ;used
PUSH AF ;registers
LD HL,11
ADD HL,SP
LD E,(HL)
INC HL
LD D,(HL)
;DE - return address after the call;;this address is preceded by a command
;CALL XXXX; This is the address XXXX and we take it:
EX DE,HL
DEC HL
LD D,(HL)
DEC HL
LD E,(HL)
EX DE,HL
════════════════════════ ════════════════════════
Best regards, Ivan Roshchin.
From
Ivan Roshin
→
To
All
8 February 2002
Hello, All!
Call .9
can be optimized like this:
LD A,(HL)
AND A
JR NZ,BANK_M
LD A,6
BANK_M AND 7
OR#10
It turns out two bytes shorter. Moreover, if the called
the subroutine is not located in the sixth memory bank, then the gain is
time will be 7 clock cycles, and if in the sixth bank - vice versa,
the operating time will be 9 cycles longer. Such optimization is beneficial
or not, depends on the specific case and the selected criteria
profitability. For example, if the main thing is to reduce the size, or if
called subroutines are located mainly not in the sixth bank
(i.e., on average there will be a gain in time), then the method is suitable.
And if the goal is to speed up the startup time as much as possible
subroutines in the sixth memory bank, then this method will not work
(you can, however, try to place such subroutines in
any other bank).
If there are no subroutines in the sixth bank at all, you can write
even like this:
LD A,(HL)
AND 7
OR#10
which will be 7 bytes shorter and 23 clock cycles faster than the original
option.
As we can see, if subroutines are called using the manager
in the sixth memory bank, then the manager turns out to be longer and
works slower. And all because among NOP-like teamsthere is no one whose least significant three bits of code would be equal to six.
But there is an optimization way to get around this
inconvenience! True, when using it it is required that although
if one of the eight memory banks were not called up using
subroutine manager (in real programs this is a condition
seems to be done almost always).
See: if you determine the number output to port #7FFD by
code of a NOP-like command using AND 7: OR #10 commands, then
You can call subroutines from all memory banks except
sixth, i.e. from banks 0,1,2,3,4,5,7. And we need, let's say,
call subroutines from all banks except the third. Then
Let's write it like this: AND 7: XOR 5: OR #10. What happens? XOR 5 command
turns the numbers 0,1,2,3,4,5,7 into 5,4,7,6,1,0,2 respectively.
And this is all the numbers from 0 to 7, except 3.
Two XOR 5: OR #10 instructions can be replaced by one XOR #15,
since after AND 7 the most significant digits of the battery are reset to zero. B
As a result, the determination of the number output to the port using the command code will be
look like this: AND 7: XOR #15. Running this snippet
takes exactly the same amount of time as AND 7: OR #10, and the length
he has the same one.
But now we can choose the unused bank ourselves.
If the number of this bank is n, then the operand in the XOR instruction is evaluated
according to the formula 6 XOR n. In this case it is equal to 6 XOR 3 (%110 XOR
%011), i.e. 5 (%101). Combining with the instruction operand OR #10, intoAs a result we get #15.
And, of course, you still need to redefine the values of the constants
bank_0 - bank_7. Let's see which numbers turn into which when
executing the XOR command, and renaming the constants. In this
in case bank_0 is renamed to bank_5, bank_1 to bank_4 and so on
further, we end up with:
bank_5 EQU #40 ;LD B,B
bank_4 EQU #49 ;LD C,C
bank_7 EQU #52 ;LD D,D
bank_6 EQU #5B ;LD E,E
bank_1 EQU #64 ;LD H,H
bank_0 EQU #6D ;LD L,L
bank_2 EQU #7F ;LD A,A
════════════════════════ ════════════════════════
Best regards, Ivan Roshchin.