[Since not closed by now, I'll try to give it a historic spin]
Hold Your Horses
In some way this is a case of second step before the first, or at least it seems like.
Thinking about how to swap segments, do caching or whatsoever is about implementation. There are many ways to do so, not at least shown by various home computers. But I belive this would fall shot of what can and should be done. Before starting to implement a design, it is more useful to think about how to extend the existing (Z80) ISA to handle a larger address space.
What measures can be used to extend an existing ISA to handle a larger address space?
Im Westen nichts Neues.
This question did come up many times over during CPU evolution. It was asked and solved many times by many teams. It was asked whenever an established architecture was to be extended, or a new one to be created with the intention preserving various levels of compatibility - or to realize effects of compactness by restricted addressing, as we will see.
Let's have a look at various examples:
[Picks are somewhat random to show various points, not neccessary first or most important]
IBM 360 ff
360 Model 20
In fact, already with the the very first family of compatible CPUs, IBM's /360 tackled this issue, albeit in a reverse direction. It was intended to replace all the various CPU architectures IBM had developed until the early 1960s. Ranging from electro mechanical punch card based tabulating machine, which still sold (!), to high end scientific computing. Amdahls design was eventually one of the greatest ever made. In turn it asked for a (at the time) complex basic CPU hardware.
So while the /360 was the great over all design, IBM needed as well a cheap(er) lower end machine, able to replace the immense number of unit recording (card tabulating) machinery as well as existing 1401. The original plan was to develop a somewhat extended, cost reduced 1401 follow up. Here Fred Brooks stepped in and developed an idea of a cut down /360, featuring only 8 instead of 16 registers and each of them only 16 bit instead of 32. This would cut register hardware by a fourth and further halving all units and wiring and droping floating point at all (*1). The Model 20 was born. And it became the highest selling version of the whole /360 lineup.
The instruction set was a binary compatible subset, leaving out all floating point and 32 bit integer instructions. If it wouldn't have been for subroutine jumps, which did get different opcodes, next to all programs could have been exchanged as binary. But since these instructions were functionally equivalent, a set of macros was used to solve this while even keeping address levels the same - and eventually adding emulation for other instructions as well.
Some manufacturers of 360 compatible machines who all had similar low end machines simply added the instructions under both opcodes (and added the missing LA), so 'full' 360 programs could run on their model 20 equivalents in binary.
/370: 26 Bit Addressing
While virtual address space was, to keep userland stable, still limited to 24 bit, the supervisor visible hardware started (ca. 1980) to support 26 bit addressing. To satisfy what today would be called 'Big Data' a dual address space feature was added, allowing programs to either have two 24 bit address spaces, or attaching to some global second address space. It was a bit weird and more of a hack than really useful. Though, the OS did use it.
/370-XA: 31 Bit Addressing
The 1983 Extended Architecture introduced 31 bit addressing not only to the OS but application processes as well. After all, an address word has always been 32 Bit ... except, in the era of low memory, and I mean real mow memory, like multi user operation with a few hundret KiB, programmers tend to use the top byte of an address as flag of some sort. Usually to indicate locks. So the solution was to introduce new subroutine branch instructions (BAS/BASR *2), which used the high bit of an address to switch between 24 and 31 bit addressing.
Enterprise Systems Architecture /370
In 1983 no machine was available to fill the 2 GiB (31 bit) address space with real RAM, but already in 1989 another extension was delivered with ESA/370: Address spaces. Each register became a new access register assigned that could hold an address space handle to be used whenever that register was used for addressing. A bit like 80286 segment handles.
That's 4 billion of address spaces, each 2 Gib in size. Effectively allowing 63 bit addressing with rather minor changes in programs. Or 55 bit for 24 bit applications :) Well, in reality the hardware 'only' supported 2048 of such address spaces, still, we had a hard time to find usage beside keeping whole databases in (virtual) RAM and let paging do the I/O.
While the 1990s were mostly wasting time with bloating the instruction set for more and more obscure use (*3,*4) and calling it /390, it wasn't until 2000 that the z architecture brought the next address increment: plain 64 bit addressing while keeping address spaces, essentially providing up to 32 zeta bytes (75 bit) addressing capabilities. On the backside the instruction set was almost doubled. It was no longer (easy= possible to use these expanded memory space without major changes. Programs had to be all around aware of the extensions. (*5)
The Z8000 is eventually the most interesting one. Like several other it's a clean 16 bit CPU with 16 bit registers, but the ability to do 32 and 64 bit operations on register pairs. The basic, non segmented Z8002, supplied a straight 16 bit address bus - plus a set of status signals classifying each access as being about
- User mode
- Supervisor mode
With minimal hardware this could be used to access more then 64 KiB.
The segmented Z8001 provided in addition a 7 bit segment number, allowing 128 segments. They could be either direct used to create a plain 23 bit address, combined with the status signals to multiply them, or feed into an MMU handling the assignment of any of the 128 (times 6) segments to a memory address of arbitrary size.
Within the CPU the program counter was straight 16 bit augmented by a special register holding the segment number of the current code segment, while for data access register pairs had to be used, one holding the 7 bit segment number, the other the 16 bit offset. Almost as if they implemented IBM's access registers 5 years prior :))
The '16 bit' (*6) enhancement of the 6500 series. It features, much like the Z8000 a Program Bank Register supplying an 8 bit prefix to the 16 bit program counter, generating a 24 bit address. Data access on the other hand was prefixed by an 8 bit Data Bank Register. Together they enabled existing 8 bit code to run in without modification either in a single 64 KiB segment (called Bank), or being split up in 64 KiB code and 64 KiB data. Any modification of these registers, as well as access to memory outside these banks had to be done with new instructions using a 24 bit 'long' address.
While providing an upgrade path, any access beyond the data bank way restricted to new instructions and rather slow.
I guess everyone was waiting for this, so here it comes. The 8086 was intended as a stop gap measure(*7) until the great saviour, the the architecture to replace all others, the iAPX 432 was ready (*8). Primary design goal for the 8086 was providing an upward path to existing 8080/85 customers with a need for more memory. 8080/85 were quite successful in embedded applications, many of them hitting the 64 KiB address barrier. Customers were asking for an enhanced CPU. Power wasn't so much of a concern as address space was. Of course the major argument to keep customers at bay would be 8080 compatibility. So the 8086 architecture was build around a plain 16 bit CPU with a 16 bit address space, much liek the 8080, but multiple address spaces this time.
Already by designing the ISA to handle two address spaces, one for code, one for data the over all addressing was increased to 128 KiB, delivering much needed relief. Doing so by assigning segment registers, holding a memory handle, to functions not only allowed to manage a second data address space (*9) but also taking stack outside the data address space. The later should prove quite useful with modern languages (*10).
The implementation of the segment resolver as an offseted addition is often marked as bad, but in reality it#s simply a fixed function MMU. That way all advantages of segmentation, like the Z8000 had, could be used without increasing the gate count much - or the need for an external chip.
On the long run the restriction to a strict 16 bit design did prove quite successful, not at least due its inherent compact code size. x86 code is in most cases more compact than most other CISC and RISC code. This translates direct to performance, as memory access is a bottle neck, used by code and data. Reducing one leaves more for the other (*12).
The 8086 as final example does show several important points when thinking about ISA extensions.
Separating memory access by function is eventually the most easy way to expand usable address space without changing an instruction set.
There are five 'natural' function spaces:
- System Management (x86 did only patly encapusle this)
- I/O (x86 used special I/O instructions as inherited from 8080)
A=B*C (*11) is an essential requirement.
And maybe most of all:
- Not the 'nicest', ivory tower solution wins, but one (barely) up for the task will.
Going back to the original question, let me give an example of a similar task I solved (Not strictly historical, but a long time in the making):
What I Did/Would Do
I don't think there is an friend of classic CPUs who hasn't mused about extensions, especially when it comes to addressing. I'm no exception. In fact, this idea already originated in in the early 1980s, when I got hands on a 65SC02 (which still works flawless in my Apple II+), but it didn't come to any implementation until rather recently.
The basic idea is kind of a combination of all the above in form of a CPU extension, modifying a genuine 65C02 in a way to expand memory (and add a few other niceties). The main goals were
- larger (unlimited) address space
- low code overhead/performance penalty
While the first depends on address translation hardware, the later requires a segmented design. Like described above it would be neat to have at least 5 segments in direct access:
- (Global) data
- Three additional data segments
Imagine a memory system much like the Z8000 with it's access function types for Code, Data, Stack and I/O. As shown this can already be used as most basic address extension. When augmented with a set of registers, each holding a segment number per function, resulting address size is only limited by width of these registers.
Implementation is rather straight forward. The 65C02 provides a signal (SYNC) activated whenever an instruction is read. This will be used to decode any instruction coming along to see which of the following accesses are about code and data. It helps that there are only 7 different access sequences of code and data within an instruction, and for all except one (JSR) it's a clean sequence of 1..3 bytes of code access followed by 1..3 bytes of data access with only the last being the 'real' data.
So when SYNC is signalled, the code segment register is to be outputted. The read opcode will be decoded and a counter started. For the regular 6 regular sequences it will simply count the following instruction bytes (0,1 or 2) while the code segment register is outputted, after that the data segment register is put onto the address lines.
So far easy and we're already extended the address range to 2 x 64 KiB. Of course without any chance to access code space as data, but stay with me.
The 65C02 has a nice feature, all unassigned opcodes are NOPs of various size and length. The whole $x3 column is, for example, filled with single byte single cycle NOPs. Exactly what we need to form prefixes to instruct our decoder to use different/additional segments for data access. Lets say we use $E3 as prefix to tell that the following instructions should not use the data segment register for it's data access, but the code segment register.
When a $E3 comes along at SYNC, the CPU will simply read and ignore it and fetch the next byte. The MMU in turn will set a flip flop that the next data access will be prefixed by the code segment instead. Voila, it's now not only possible to load constants from program code, but as well load the program itself by writing with a $E3 prefix.
The very same can now be done for the three additional data segments A, B and C - neatly encoded as $A3, $B3 and $C3.
[Of course there are a few more details for handling pointer access (to be in default data) or inter segment jumps/subroutine calls and returns and alike, but solutions are as well straight foreward, so we can leave it out for now]
Now we just need to find a way to tell the MMU which values to be used, aka loading the segment registers. We can not put it into any of the movable segments, as this would mean it could be unaccessible at times. But hey, why not borrowing the idea of an I/O space? Like using a $EF prefix to point to a single unmovable 64 KiB I/O segment. The MMU would signal any I/O access on a seperate line, and, for its registers react to an address within.
Let's say we put the segment registers at $010A..E. So a sequence of $EF followed by an STA $010D would set the default data segment to whatever value was written. In fact, since we're not restricted to I/O instructions, like a Z80, all memory instructions can be used, including Read-Modify-Write like INC and DEC, making these location essentially the upper byte of 24 bit pointers when using indirect addressing. The system can easy be expanded to a virtual memory system with several address spaces and so on.
Take that 65816, offering the same (or better) functionality at the same (or better) performance with only six additional opcodes spent.
Well, as said, it took me a while (~30+ years) until I got myself to do it, and so far it's only a proof of concept, not going all the way in all beauty, just showing the basic workings. Without the segment register everything fitted in a single PAL (ok, it's an ATF750, so rather luxurious), so quite doable with 1980s technology with a low price tag.
I didn't move any further from there, as additional ideas (like super visor modes, better trap handling, etc) would need more complex hardware to be implemented external to the CPU, or mean usign an FPGA to hold an expanded CPU. Bah. I do not like them.
What Does This Mean to Above Z80 System?
Handling all random access as complex 4 byte (64 bit) maniputaion may make a 20 MHz Z80 slower than a ZX81. So before thinking about implementation ataking step back and thinking about ISA and usage would be a good idea. And while the above 6502 based solution might be as well doable (using MM1 instead of SYNC), the Z80 opcode space is less open, so extensions may need additional thoughts.
In any case, a look at the eZ80 might as well be helpful (I may add it to thist later on).
*1 - Not to mention the vast saving by further dropping an independent operating channel system (For micro people, that's essentially a second CPU specialized on DMA)
*2 - Funny part, these were exactly the mnemonics (albeit not the opcodes) used for the model 20, so people still maintaining code for them in 1983 had to modify their macros to directly output the binary opcode instead of replacing the mnemonic. Been there, done that :))
*3 - Prior to 1990 the /370 instruction was rather straight forward, like envisioned by Amdahl, making the usual label of 'CISC' rather ambiguous. But the /390 (and following z/A) really changed that by adding extreme specific operations - like Unicode string handling or adding SHA-1, DEA or compression as machine instructions. Not funny anymore
*4 - Beside adding a lot of additional floating point functions relative branches and 16 bit immediate were added. The first extensions breaking the clean /360 structure. For many I assume the reason was to make it more convenient to 'modern' (read stupid) compilers.
*5 - I always like to think that AMD had a look at IBM when they did x86-64 a few years later.
*6 - I know that's debatable, but I will stay away from that honeypot.
*7 - In some kind the same way as the 8085 was a reaction to the Z80 with it's much simpler interface and single 5V power requirement.
*8 - Yeah, we all know history is a bitch :)
9 - Later augmented with two more to allow arbitrary A=BC (*11) operation.
*10 - Like the whole 8086 was designed with a keen eye on what compilers would like to see as instructions.
11 - A=BC means operating on three elements at the same time, like adding two numbers and storing at a third location, without (or only minimal) address space management inbetween.
*12 - With the Thumb instruction set ARM did go that way to keep the CPU simple while increasing performance - that was before they joined the mob in throwing hardware onto the issue, much like x86 does.