Manager for calling subroutines from various memory banks

ZXNet echo conference «code.zx»

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.