5

I have a memory setup where the whole address space can be exchanged (default 2 slots with 2 banks each, but may change depending on "expansion cards" plugged in).

Banks are switched by sending a certain control byte to a certain control port (actual values depend on hardware setup / expansion cards / etc and should be left to the program. I might get away with hardcoding the port since changing that in the "default setup" requires solder jumpers, but any "expansion cards" adding memory would have different ports, so I'd rather not).

By convention, $FFFE..FFFF on each bank holds the SP for that bank, while this handler gets copied onto each bank on bootup.

To be able to call across bank boundaries I came up with the following RST $08 handler:

; call using:
;   RST $08
;   DB controlport
;   DB controlbyte
;   DW calladdress
RST08: ; $0008 contains a JP RST08
  POP IX ; returnpoint
  INC IX ; controlbyte
  INC IX ; controlport
  INC IX ; jump low
  INC IX ; jump high
  PUSH IX ; put new returnpoint back
  LD C, (IX+2) ; control port
  LD B, (IX+3) ; control byte
  LD ($FFFE),SP ; back up SP
  IN A, (C) ; so we can go back there
  OUT (C), B ; we're in remote bank now, better hope the bankcall handler was installed there
  LD SP, ($FFFE) ; load remote SP
  LD B, A ; load "return control byte"
  PUSH BC ; back up "return controls"
  LD H, (IX+0) ; jump high
  LD L, (IX+1) ; jump low
  CALL JPHL
  POP BC ; restore "return controls
  LD SP, ($FFFE) ; load backed up SP
  OUT (C), B ; go back to previous bank
  RET
JPHL: ; because there's no CALL (HL)
  JP (HL)

Something tells me there must be an easier way to do this, but I can't quite wrap my head around it. I'm also positive I've seen way easier code achieving similar effects in other Z80 "boot ROMs" I stumbled upon randomly during my research on the architecture, but somehow I'm unable to find them when actively searching for it

  • What is the question? Are is the code working and you're looking for an optimization, or is it at fault? Looks suspicious to me that a port is read before C is loaded (line 13/14), similar, if boards have different control ports, where does data for the actual port and setting comes from - not to mention that I have a hard time to imagine the hardware to do all of this. – Raffzahn Nov 10 '20 at 21:22
  • @Raffzahn i'm looking for an optimization here. the port is read to get the "current" bank setup, and yes, you're right, that was reordered by mistake, updating the question to reflect the right order. the hardware is simply a bunch of latches bound to that port (implemented in a PLA chip). the actual data comes from the program calling that RST handler (see the comment describing the "fake 5 byte instruction" beginning with RST 8; you can think of it like a 32bit call with the address made up of the port, controlbyte, and actual 16bit address). – nonchip Nov 10 '20 at 21:28
  • @Raffzahn to get a rough overview of the hardware: i have an internal ROM ($0000..$7FFF) and internal RAM ($8000..$FFFF) with a pulled-up "disable internal memory" line on my expansion bus, and the hardware modules providing additional banks just sink that line low while setting the CS lines on their memory chips if they're enabled (using those IO ports they're bound to) and a MEMRQ happens. you can see an example of such an implementation for a simple "2 additional ram banks" setup there: gitlab.com/nonchip/nz80mod/-/blob/master/ramboard/mapper.pld – nonchip Nov 10 '20 at 21:38
  • @Raffzahn the only "optimization" i could come up with would be to replace the "bank call" with a "bank 0 call" to allow for "bios calls" but leave everything else up to the program itself, but i cant help but thinking the above functionality should be possible in a way shorter piece of code... – nonchip Nov 10 '20 at 22:01
  • Well, first of all, stack exchange isn't exactly the best format to handle such 'open' questions. A forum type site might be more appropriate. Next, The whole logic seems to me a bit too variable. Why having different ports at all? One port can be good for 256 pages. Allowing, with 32 KiB pages, 8 MiB of memory. Quite a lot for a Z80 - even a very fast one - a 100+ MHz Z80 might be appropriate. We all know, there is no memory too big, but shouldn't it be useful as well? Just think how long it wouldtake to sweep thru all of that. – Raffzahn Nov 10 '20 at 22:19
3

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

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 now enter this card into the system and jumper the ID 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.

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 :))

  • due to simplicity, i'll stick to what you described under "Basic Workings with a Single Port" for future hardware iterations and drop compatibility to my first card design (which only decodes D0/D1 to directly write to 2 chip select flipflops). "only the actual selected card reacts" can then simply (if i'm not mistaken) be done by using a tristated I/O which always outputs its "configuration" (from e.g. dipswitches on the high data pins and its "ram chip select" flipflop(s) on the low one(s)) but is only output-enabled if there's a read access AND the "selected" flipflop is set. – nonchip Nov 15 '20 at 10:38
  • about your notes: *1) yeah i'll most likely stick to 8bit addresses in this setup anyway, and then pass "bigger" parameters along using e.g. a pointer in a register, and just not switching the bank that parameter was set up in. *2) since i aim to run at 6MHz, i doubt that overhead will be too much, yeah, and mostly used for stuff like "bios calls" (which can get an optimized RST), "i don't have enough space in this bank, so jump me to a routine that DMAs a big chunk from that other bank into my 'work ram area', then jumps me back here" or larger function calls i'm going to stay in longer. – nonchip Nov 15 '20 at 10:52
3

The normal way to handle cross-bank calls is to use a springboard which is either in an unbanked area of memory or appears identically in both old and new areas. If one doesn't mind using a different entry point for each origin bank, things can be pretty simple, especially if bank switching I/O addresses use the bottom bits of the address, rather than the data, to select the bank. For example if addresses 0xF0 to 0xFF select bank 0 to 15, then:

call_func1_from_bank0:  ; Assume func1 is in bank 2
  out 0F2h
  call func1
  out 0F0h
  ret
call_func1_from_bank1:  ; Assume func1 is in bank 2
  out 0F2h
  call func1
  out 0F1h
  ret

Not quite as fast as calling code in the same bank, but nowhere near as slow as the more general-purpose recipe.

  • Err, if you're reading above code, you might find that this is already what he's doing - just with variable bank numbers for origin and target and without the need to have fixed stubs in each bank for each other bank. – Raffzahn Nov 10 '20 at 22:24
  • @Raffzahn: He said "there has to be an easier way to do this". Having per-function springboards in a common bank allows the code to be much simpler and faster. – supercat Nov 10 '20 at 22:25
  • Only in a comparable small (8 MiB doesn't sound small) and static linked system. – Raffzahn Nov 10 '20 at 22:33
  • 1
    @Raffzahn: RAM-based springboard tables can be useful in systems that aren't statically linked. If the total number of different functions that need to be reached by cross-module calls isn't too big, the described table format will take eight bytes per function, or 128 functions per Kbyte of storage allocated to the purpose. – supercat Nov 10 '20 at 22:46
  • 1
    ok a) "he" is a she, b) yeah afraid while this makes for rather short springboards per call, this method is not really viable in my case since I'm writing a monitor/bios here, so i can't know all the calls beforehand. c) i also just realized my code only works for calls between one memory module and the internal one anyway (since i'm only accounting for one IO address), i might as well embrace that limitation by installing a pointer table in internal rom/ram and only provide a single call handler to either "bios functions" or that table. – nonchip Nov 11 '20 at 8:34
3

Hard to do a lot better while keeping full generality but the code can be considerably smaller and faster by avoiding IX. Code hasn't been tested.

; call using:
;   RST $08
;   DB controlport
;   DB controlbyte
;   DW calladdress
RST08:                  ; $0008 contains a JP RST08
     POP   HL           ; return point
     LD    C,(HL)       ; control port
     INC   HL
     LD    B,(HL)       ; control byte
     INC   HL
     LD    E,(HL)       ; jump low
     INC   HL
     LD    D,(HL)       ; jump high
     INC   HL
     PUSH  HL           ; put new return point back
     LD    ($FFFE),SP   ; back up SP
     IN    A,(C)        ; so we can go back there
     OUT   (C),B        ; we're in remote bank now, better hope the bankcall handler was installed there
     LD    SP,($FFFE)   ; load remote SP
     LD    B,A          ; load "return control byte"
     PUSH  BC           ; back up "return controls"
     CALL  JPDE
     POP   BC           ; restore "return controls
     LD    SP,($FFFE)   ; load backed up SP
     OUT   (C),B        ; go back to previous bank
     RET
JPDE: ; because there's no CALL (DE)
     EX    DE,HL
     JP    (HL)

As others have mentioned, removing a bit of generality can go a long way to simplifying things. If you're really looking to simplify you could have the hardware switch banks based on the Z-80 executing an instruction at a particular address. Say if the Z-80 runs an instruction at $01x0 then the hardware switches to bank x. You could then have 16 shorter context switch routines carefully aligned to have the bank switch without the Z-80 having is issue specific instructions.

  • I like it. Almost clean 8080 code, except for the IN/OUT (C) – Raffzahn Nov 11 '20 at 18:53
  • LD SP,(nn) and LD (nn),SP are also Z-80 specific. With 19 T-States for LD r,(IX+d) vs. 7 for LD r,(HL) there's quite a bit of room to do manual indexing. Especially mixing in EXX. Still, the index registers make programming easier and that's important too. – George Phillips Nov 11 '20 at 19:45
  • True, on 8080 one would have to go thru ADD HL,SP which always was nasty. BTW, you might want to change the final SP loading with the OUT,as the old pointer is in the oldpage :)) – Raffzahn Nov 11 '20 at 20:23
0

I have programmed on a device with paged flash memory (Hypercom Z80 based credit card terminal). It used a very simple scheme where the top 32K was paged (I think the terminal had 2mb flash). The bottom 8K was ROM (startup code, page switching and shared run time code). The stack was set to the top of the lower 32K RAM (visable to program at any time was 32k flash, 24k ram and 8k rom). A simple 8 bit latch was used for page selection (created extended address bus with A16 to A23). A cross page call made using rst instruction encodes as [rstn][8bit-page][16bit-address]. The compiler we were using supported the 'long' call instruction. Note that if you want a multi card system you just need to bring out the A16-A23 lines to your bus and decode on individual cards as required (basic memory selection logic, no special hardware needed). I can't remember the code exactly but it was basically, push current page, get address from stack, get page number from stack and output to latch, call address, pop and restore page and return.

When I wanter a faster call i would create a small stub in ram:

stub_func:
  ld a, far_page
  out control_port,a
  call far_func
  ld a, local_page
  out control_port, a
  ret

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.