4

I'm in the middle of making an 8Bit computer, I've selected Z80, the 20Mhz version, which is cheap and readily available. Since Z80 has a 16bit address line it can only address 64KB of memory location. 64KB is not a lot, so I decided to design a memory management unit using an FPGA (I'm not sure what to call this logic unit, for now I can only think of MMU) to expand the memory space.

I have connected a 64KB fast SRAM directly to the 16bit Address BUS of Z80, and placed it next to the CPU, It is serving as a cache for Z80 very nicely.

When the computer boots up MMU copies content of a 64KB ROM, which has some basic OS, and some goody software I have written in assembly, Z80 happily reads the cache and executes the instructions, which is good, (things are working correctly).

I have reserved the last 64 Bytes of the cache as the following diagram:

enter image description here

Which is divided equally intto four parts, in the lower part I put the 64bit address of the memory I want to access, and on top of that there is the 64bit address of the current 64KB copy of the memory in cache, on top of that there are some flags for the MMU which is used to see if Z80 is done with the current contents of 64KB cache, wants to access some other 64KB code+data from memory, wants to read to memory, or write to it, etc. The MMU checks these flags and makes the appropriate decision, accesses the requested memory address and fetches the data and puts it into cache.

The reason I have chosen a 64Bit addressing is to keep my option open, and keep the possibility for future expansion, also I wanted to be able to address memories larger than 4GB. Like RAM, SSD, Flash Drives, SD cards, HDD, etc.

I have programmed the MMU to always listen to 16bit address BUS, so It can make quick decisions, for example, Whenever Z80 wants to access a 64bit address it writes the address into the bottom 64bit of cache, when it accesses $FFFF it means the Z80 is writing the last byte of the 64bit address so MMU knows that it is time to fetch some data from that address in the given memory address, fetches it and copies it into cache. So far everything is working correctly.

But I am not entirely sure I have gone the right path, to be I honest I'm not sure where to go from here. The main question that is bugging my mind, are potential bugs that I might introduce, and also performance.

There are some problems I haven't come up with a straight forward solution yet.

for example what if Z80 wanted to only address some small part of the memory while executing code in the cache space, maybe an array or a variable, What should the MMU do? should it simply copy that data and put it in cache? what if there is some important code or data on the replicated address? should I reserve some memory in the cache for this? Or should MMU simply keep the data in some registers or some perhaps external temporary cache and send it over the data BUS to Z80?

What if there is a large data structure or an array in memory that Z80 wants to access? What about loops, functions, specially loops that frequently access a different part of the memory?

8
  • 7
    Having said that, my very first question would be why on earth any hardware control needs to be located within memory address space. It's an x80, it got a dedicated I/O address space of equal size, RAM should be left to code and data. Not to mention that this is as well the point were any kind of process encapsulation can easy be added. Managing code and data with a single 64 KiB window is cumbersome. It needs constant reloading, making the design extreme slow. A design sweet spot is to maintain code, (global) data and data for arbitrary A=B*C operations with minimal management.
    – Raffzahn
    Apr 7, 2021 at 18:46
  • 8
    That is a question of making new hardware, not about retro hardware, I presume it is closed soon. Your term "cache" for the SRAM makes also no sense, the MCU does not execute stuff by just wrapping around a 16-bit memory space time after time. SRAM could be left out and let the FPGA do the paging. 64-bit address space makes little sense as you generally don't access things like files or SD cards or HDDs as linearly mapped memory devices, they traditionally work in 512 byte sectors.
    – Justme
    Apr 7, 2021 at 18:49
  • 3
    @ilgaar This is a Q&A site. I don't think it is on topic to ask opinions how you should continue with your hobby project. While it does look cool, I think the Z80 will be a bottleneck handling all that hardware. I mean, it kind of resembles a modern laptop or Raspberry Pi, but with the CPU swapped to 40 year old 8-bit model
    – Justme
    Apr 7, 2021 at 19:26
  • 5
    Might be worth looking at how historic machines such as Amstrad CPC6128 or ZX Spectrum 128 did it. Generally a fixed windows was used in addressable ram, into which different parts of the extra memory could be 'swapped' via writing to an IO address. See cpctech.cpc-live.com/docs/rampage.html for details of the Amstrad approach. Apr 7, 2021 at 19:33
  • 10
    Or use an eZ80, which combines classic Z80 code with 24 bit address space - more than enough for a CPU that size - as here Zilog already solved that issue in a quite compatible way.
    – Raffzahn
    Apr 7, 2021 at 19:35

1 Answer 1

6

[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.

z/Architecture

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)

Zilog Z8000

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

  • Program
  • Data
  • Stack
  • I/O

and further

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

WDC 65816

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.

Intel 8086

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

Conclusion

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:

    • Code
    • Data
    • Stack
    • 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.

Background

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:

  • Code
  • (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 Theory

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.

Practical Implementation

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.

9
  • 1
    I would add the Apollo Guidance Computer, introduced 1966. It had 15-bit words, and instructions used 12 of those bits as the address of an operand. Thus the address space was 4k words. However, the software ended up filling 36k words. This was solved by banking the read-only core rope memory that held the software.
    – DrSheldon
    Apr 8, 2021 at 5:21
  • 1
    @DrSheldon Well, was banking a feature of the ISA, or was it application, like added as part of the I/O design?
    – Raffzahn
    Apr 8, 2021 at 5:32
  • 1
    The CPU itself did not do the memory banking; it was done by the address decoding logic. Three special memory locations specified which banks to use. So it is consistent with what the question asks to do.
    – DrSheldon
    Apr 8, 2021 at 5:55
  • I've seen a very similar 65C02 extension scheme as what you describe on the net, that was used commercially in the 80s. I thought it was on 6502.org but couldn't find it anymore. It did exactly what you are describing here. Apr 8, 2021 at 10:11
  • @PatrickSchlüter Now, that's quite interesting. Would be extreme cool to find that. Never claimed to be the only one to see that potential.
    – Raffzahn
    Apr 8, 2021 at 10:54

Not the answer you're looking for? Browse other questions tagged .