Considering what I learned in the comments under the question, I'd suggest a combined hard/software solution to this design issue. It's based around a single port, or a consecutive group of ports that are handled as one, by all 'cards'. Decoding is done as sum of address and data.
[Insert]
Assumptions for all following:
- Bit-numbering in Address/Data is LSB to MSB, so A0/D0 is lowest value
- The term 'Card' is used for any functional block providing an address block independent of being a card on its own, part of another card or some base system.
- The system provides single memory window of 32 KiB, starting at 8000h to map card content.
- The term 'selected' describes a card active mapping its content into the window.
- The term 'page' describes a memory region to be mapped into the window
- The term 'id' describes the static porion of a cards 'address' - usually hard coded via switches, jumpers or some ROM.
- The term 'page number' describes an identifier (address) of a page supplied by a card
[/Insert]
Basic Workings with a Single Port
The essential idea is to address all pages of all cards via a single port and decode the value written there as card id and page number.
- There is a single I/O port used for bank selection.
- Lets say at FFh.
- All cards will decode that address
- Each card has an 'id' 0..7 bits wide
- If it's a read access
- only the actual selected card reacts and returns the actual setting (all of D0..D7)
- If it's a write access
- All cards deselect
- All cards decode data written
- The data is decoded as card-id
- by selecting D7..Dn depending on the card structure and matching it with the card id.
- If data matches the card id it selects
- The page part of the data (Dn-1..D0) is used as page number and the according page mapped.
Sounds complicated? It isn't. Lets make a simple example.
Lets assume a card offering 64 KiB of RAM as two pages (0,1).
The card decode port F0h.
On a write access to port F0h it will compare D7..D1 to a set of DIP-Switches with it's address. If they are the same, the select flip-flop is set and D0 is saved to another flip-flop (page FF) to address either page.
From that time on all access to the window (8000h..FFFFh) will go to the selected page (A15=1 & select flip-flop set). If the id does not match, the 'selected' flip-flop gets reset, and the card becomes inactive.
On a read access on port F0h and if the select flip-flop is set, the card will put it's id on D7..D1 and the state of the page-flip-flop on D0. Otherwise it does nothing.
If we enter this card into the system and jumper the ID to 0000.010x, it will provide two pages as 04h and 05h.
Plugging a second card of the same type, jumpered to 0000.011x, it will provide two pages as 06h and 07h.
Now lets take a card with 256 KiB or 8 pages. Workings are as before, except the id match only goes this time for D7..D3 with D2..D0 being the page within. If jumpered as 0000.1xxx, it will provide pages 08h..0Fh.
With a 1 MiB card ... Well, I guess by now the picture is clear.
The fun part here is the cooperative protocol the cards adhere to. No need for reconfiguration when adding a new card - well, as long as there is room for additional pages that is :))
So far the hardware is no more complex than with the original solution. Code is also quite similar to your original solution - just sans some quirks. Let's look at the software:
; call using:
; RST $08
; DB callpagelow ; Pagenumber
; DB callpagehi ; Ignored for now (port-part)
; DW calladdress ; Address within page (Special value with A15=0)
;
; $0008 contains a JP RST08
;
; Destroys registers BC, DE and HL before entering the new bank.
; Only A, IX and IY can be used for parameter transfer
;
; Destroys BC and DE on return.
; Only A, Flags, HL, IX and IY can be used for return values.
;
RST08:
POP HL ; load parameter list address into HL without changing SP and save HL
LD B,(HL) ; Load pagenumber (callpagelow)
INC HL
INC HL ; Skip callpagehigh
LD E,(HL) ; Load target address low
INC HL
LD D,(HL) ; Load target address high
INC HL
PUSH HL ; Place new return address
EX DE,HL ; Bring target address into HL
LD E,B ; Free B for IN/OUT
LD (0FFFEh),SP ; Save current page SP
;Do the flip
LD BC,00F0h ; Portaddress
IN D,(C) ; Load actual Page
OUT (C),E ; Flip to new page
; here we continue in (hopefully) the same code on the new page
LD SP,(0FFFEh) ; Load page SP
PUSH DE ; Save previous/actual page
CALL JPHL ; Call the desired function
POP DE ; Reload page
LD BC,00F0h ; Portaddress
OUT (C),D ; Flip to previous page
; And back to code in the original page
LD SP, (0FFFEh) ; Load page SP
RET
JPHL: ; because there's no CALL (HL)
JP (HL)
I do not like the fact that it destroys a lot of registers, after all, how to transfer parameters between pages?(*1) So while there could be still a fey cycles shaved, I do think this is a viable compromise.
I wouldn't care so much about the switchers size. Any routine so small that above instructions would make a considerable overhead should be embedded in the caller code anyway (*2).
In general I'd suggest to put that code into ROM and have as at least a data access function as well, allowing to read bytes (or words) form a different pages - maybe in conjunction with an interbank block move to exchange parameter blocks.
In general this is not much different than your original idea, except now the cards act cooperative and programs only need to know a logical page number, not a physical port and page. With some careful address assignment programs can 'see' a continuous series of pages without the need to know about each cards structure and limits.
Using a logical page number while hiding real port numbers within the switcher also enables transparent hardware changes - i.e. a later machine having, for what ever reason, these ports at different locations and/or different organized, which brings us to part #2, usage of a port group to expand past 8 MiB (if ever needed):
##Using a Port Group
The scheme is much like before, except we now decode a wider page number than 8 bit. How? simply by using addresses as data. This allows in theory the use of up to 16 (I/O) address bits together with the 8 data bits as a 24 bit page number, enabling up to 16 Mi Pages of 32 KiB each, all together an address space of 39 Bit 512 Gigabyte. Cool, isn't it. Of course it would use up all I/O Ports available, so maybe we just go for a usage of a maximum of 8 of the address bits. This still allows up to 64 Ki pages with 32 KiB each - or 2 GiB of address space. Truly more than a Z80 can wish for. Workings are much like described before (*3):
- There is sequence of I/O ports used for bank selection.
- Address is again arbitrary but need to be alligned to the size of the port group. E.g. if there are 4 ports it needs to be aligned on a 4 port boundary (0, 4, 8, ...), with 16 it needs to be a 16 port boundary (like 10h, 20h, ...)
- All cards will decode that address range
- Each card has an 'id' 0..15 bits wide
- If it's a read access
- only the actual selected card reacts and returns the actual setting (all of D0..D7)
- If it's a write access
- All cards deselect
- All cards decode address and data written
- The The lower address bits and all data bis are decoded as card-id
- by selecting Am..A0 and D7..Dn depending on the card structure and matching it with the card id.
- If data matches the card id it selects
- The page part of the data (Am-1..D0) is used as page number and the according page mapped.
So lets assume there we spend 16 port addresses for the switcher, located at E0h. Now cards would decode A3..A0 and D7..D0 as page address. That'll be 4 Ki pages or 128 MiB of RAM.
Software wouldn't be much different. A few more instructions to handle the wider page number, but that's it. I'll leve that up as an exercice.
Best of all, from a software's view this would be fully upward compatible - only difference is that now more than 256 pages are available. Heck, even the hardware can be made to already adhere to that by optional decoding address bits as zero. So old cards could be used in a follow up system.
*1 - Using 8080 IN/OUT (aka restricting port address to 8 bit) would free (with some register reloading) enabling the preservation HL as input parameter (or another 16 bit register). Also BC could be saved, but not be used as In or Out.
*2 - Yes, there may be exceptions for some OS functions that urely exist to abstract access, but even then, if it's that time constrained the whole concept needs to be rethought.
*3 - There is a not so tiny problem hidden - lets see who spots it first :))