14

I'm planning out a 6502 homebrew build but seem to be stuck in the issue of buying parts. My plan is to extend the addressing capabilities of the 6502 by giving myself 64kb of RAM, and another 64kb of ROM, by using an MMU. I've looked up those such as the MOS-8722 and 74LS610, but can't seem to find them anywhere that don't cost $30AUD or more, because they have to be shipped from overseas and are now incredibly rare.

Obviously using a memory mapper would serve me well, however is there any tricks you can do with an Address Decoder or alike to extend the memory without needing any MMU's?

I'm pretty sure I read somewhere that you can flip a byte in memory and then it somehow knows which chips to take from, but then is that byte being stored in a register, ROM/RAM, I don't know.

If there is a trick, what happens if I assumingly have two single 64kb ROM and 64kb RAM chips, want to read from ROM and put it in RAM. How does the decoder know to swap chips, grab the data and put into the other? What kind of soldering are we looking at here?

4
  • 3
    Some useful information for you: wilsonminesco.com/6502primer/addr_decoding.html Oct 22, 2017 at 11:11
  • Related: retrocomputing.stackexchange.com/q/587/276
    – Chenmunka
    Oct 25, 2017 at 7:33
  • $0.02 worth of advice....I recently designed my own 65C02 based computer (Potpourri6502) that contains 16KiB of RAM and four expansion slots. I found that 16KiB was more than enough for BASIC and other stuff. Plus, the glue logic is very simple. So I'd suggest sticking with a simple, small design. Unless, of course, you've already done the simple designs.
    – cbmeeks
    Jan 10, 2019 at 14:46
  • If you do extend the memory it is useful to do it in a way that allows access to more than one bank without having to change the mapping register. That way you can easily copy data from one region to another or even access the data almost transparently. Apple did with Language Card extension for the Apple II. The ROM, and four 16kB regions all occupied the same physical address space while retaining access to the main RAM in the bottom 48kB. Mar 5, 2021 at 1:22

5 Answers 5

12

The simplest way to do that is extending your address bus by at least one bit and have a latch (like, for example, a 74279 TTL latch) to store these bits. Then put in some address decoding to memory-(or i/o-) map this latch as a register allowing you to store arbitrary values in there.

Whatever you put into this latch then will determine which area of memory the CPU is addressing. You might want to disable this circuit on some address ranges to for example make sure ROM is paged in at a certain address all the time.

This is obviously a very crude way to memory-map a 64k address range to some larger space. More advanced approaches would split the memory into banks (typically 16kBytes) and have a bit more advanced logic allowing to overlay any of these banks into anywhere in the 64k.

9
  • yeap I saw also i8255 used for this purpose (for example in ZX clone Didaktik GAMA) which allows to use the unused ports of it as IO interface for joistics, printers , etc ... as you will need to add the addressing decoding logic anyway you can use it also for such purposes and do not add another parts for it ... but beware those chips where very fragile (electrically speaking) and broke a lot when not used properly.
    – Spektre
    Oct 22, 2017 at 9:47
  • Ahhhh this is interesting... Just so I know, how would I store the values in the latch, or furthermore, how do you even extend an address bus thats limited to A15?
    – user7013
    Oct 22, 2017 at 10:44
  • 1
    The 6522 VIA could be a natural pairing for a 6502 — two programmable input/output ports plus four control signals plus two interrupt-generating timers (plus a shift register, but the implementation is buggy). The only issue is that all lines default to input at reset.
    – Tommy
    Oct 22, 2017 at 12:32
  • @finnrayment you put the latch on the bus, with a decoder so that it latches the value on the bus when there's a write to a certain address. And you extend the bus by, well, having more wires. You need to get the output of your latch to the RAM address decoder :)
    – hobbs
    Oct 23, 2017 at 17:54
  • @Tommy Ahhh, will have to buy one. Atleast the Rockwell ones are still easily found. Only issue is, the AP ones are 2MHz yet I have a 4MHz 6502, or specifically the R65C02P4. Will they be compatible?
    – user7013
    Oct 24, 2017 at 6:33
8

There are many solutions to your problem.

  1. use a latch as suggested by tofro.
  2. extend this to a full register. This register provides the most significant bits of your extended addressbus.
  3. build a minimal MMU yourself by adding the content of a register to the 6502 address (using an adder).
  4. use a 6510 instead of a 6502. This processor is used in a Commodore 64 to do exactly what you want. This processor has an internal 'port' register connected to the processor pins which allows you to select different memories (or I/O areas).

in case 1..3 you will have to address the latch or register in your memorymap. This is not necessary in case 4 since the port register appears on addresses 0 and 1.

Since you can't write to a rom, you can always acces RAM if the read/write line is set to write. This way you can read the ROM and write the RAM on the same address (like it is done on a c64).

2
  • Can you please elaborate on point 2, I don't think I understand you completely. As for 4, that would just be cheating! :)
    – user7013
    Oct 24, 2017 at 6:37
  • @finnrayment: you posed the question in a remark how to extend the addresbus. Point 2 is how this could be done. A0 to A15 is your original 6502 addresbus. A16 to A23 (if you use a 8bit register to extend to a 24 bit bus) come from an external register. Remark that a jump or a fetch outside your current 64K block will require special techniques.
    – EL Dendo
    Oct 25, 2017 at 10:17
6

One simple way to extend address space is to use a register file to support bank-switching. For example, suppose you have a 74LS670 4x4 register. This chip lets you use 2 bits of input to select the value from 1 of 4 4-bit registers. The ability to convert 2 bits to 4 bits means you can increase the address space from 16 bits to 18 bits, mapped in 16KiB chunks.

The top 2 address bits would then select 1 of the 4 registers, and the selected register would then supply 4 new address bits (which combined with the remaining 14 bits of the original address, gives you an 18-bit effective address). This scheme divides up the 64KiB address space into 4 16KiB regions, where each region can be chosen from among 16 possible 16KiB segments.

If you use 2 74LS670 chips, you can extend this to 8 bits per register for 22-bit addressing.

Note that because the 6502 uses memory-mapped I/O, you need to take some care to ensure that you never lose the ability to change the memory map by accidentally unmapping the address range where the register file is accessed. This is the nice thing about the 6510 -- it has a dedicated I/O port (addresses 0 and 1) which can never be unmapped.

1
  • 1
    Just a reminder: you don't have to switch 16 KiB blocks. You could, for example, dedicate the lower 32K to a single segment of physical RAM via A15 select and use the register file when A15 is high to set A13-14 to switch between 16 blocks of 8 KiB physical RAM in the upper 32 KiB of address space.
    – cjs
    Nov 4, 2018 at 9:14
4

First, some theory: (TL;DR: Everything is possible :))

There are like a zillion possible ways to access more storage 'outside' a limited address space. They generally fall into three groups:

  1. Have one or more mapping registers that to map memory into an address region

  2. Have an access port whose content is specified by one or more access registers.

  3. Trap and modify the addressing on one instruction to access additional memory in another address space

Method 1 is what is usually called banking. One region of defined size within the CPU address space is selected via traditional means. All address bits within this region are supplied to an external address space plus some additional to select a block within this address space. Examples here would be GeoRAM or NeoRAM for the C64 (256 Byte region) or Language Card and compatible (like Saturn) for the Apple II (4/12 KiB region). With several regions, and thus more than one source for additional address bits, more variable configurations could be achieved.

All data in these region(s) can be used for any access. Program, pointers or data.

Method 2, in contrast, can only be used with user data, as only a single byte is accessible at a time, and all access is performed via the same address within the main address space.

Method 3 is the most exotic but also most powerful, as it requires a deep knowledge of the CPU. An example here would be the way the Commodore 6509 computers (PET II series) operate. Here the LDA (zp),Y and STA(zp),y instructions are modified during the data access cycle to use a 4-bit latch to access one of 16 64KiB address spaces while the program still runs in the main address space.


Less theoretical:

For your project, the use of a 6522 might be the least complicated. While it might seem overpowered at first, it features several advantages. The timing is well-defined and the setting can be read again. The latter is quite important if you intend to have a multi-programming environment. That includes not only two concurrent user programs where each should have its value set when running, but also 'simple' things as interrupt routines.

Just assume something such as a background transfer buffer (like for a printer). Wouldn't it be great to have that spool data out of the way, within some extended address space? So whenever the printer signals the ability to accept a byte via interrupt, the access region needs to be switched to point to the print data. No problem; just write the latch. But later on, before returning, the value the foreground program uses needs to be restored. With a self-designed latch, this read-back will need several additional chips. Or additional software to keep the value in a memory location, and well-behaved software so it always gets written and done so in a secure way.


It's not as simple as people often think - and one could write a whole book about it. :))

28
  • Haha, thankyou for the detailed and interesting answer. You suggested a 6522 VIA as did Tommy in a comment on another answer. My question to him (and you) is that I'm going to use Rockwell R65C02P4's, which are 4MHz yet the 6522 comes in 1 and 2 MHz only. Will they still work together?
    – user7013
    Oct 24, 2017 at 7:13
  • @finnrayment I think you could post that as a separate question. The best of luck, I'm interested in following this project! Oct 24, 2017 at 8:22
  • 1
    @Tommy: Code running from a "special" region of zero page would, for better or for worse, see the selected data in the banked region instead of seeing whatever the default would be. As for the 2600, I've done some rather creative banking on that platform myself, including a scheme were one can do "high-resolution pixel plotting" [Stella Sketch style] using e.g. lda $7F00,x / ora $7E00,y / sta $7E00,y to set a pixel at (x,y) or lda $7F80,x / and $7E00,y / sta $7E00,y to clear one.
    – supercat
    Oct 24, 2017 at 22:27
  • 1
    @Tommy: That's weird. I wonder why they would limit the decode to 10x10001 rather than decide not to accept all eight opcodes of the form xxx10001 (allowing any opcode of the form (ZP),y). The Atari 2600 doesn't make /sync available on the bus, but if one has separate addressing regions where code execution is or isn't allowed it's possible to infer its presence. Further, if code execution is limited to bus addresses 0x0880-0x08FF and 0x1800-0x1FFF, then a cart can infer the MSB of an address in the 0x1000-0x17FF by looking at what was read from the last address where bit 11 was clear.
    – supercat
    Oct 25, 2017 at 14:26
  • 1
    @Tommy Oh, being pedantic is a great idea, as it isn't a PLA either: there is no AND plane :)) I still see a clear microprogramm logic when looking at the 6502. There are (micro) instructions selected purely by the instruction register and the micro program counter (aka cycle counter). The fact that more than one instruction can fire at a given time, or that one ROM entry is used in multiple CPU instructions doesn't change this very basic behaviour. Ok, I admit, I'm maybe biased here, as I'm familar with similar encodings used in 1970s mainframes - which clearly where microprogrammed :))
    – Raffzahn
    Nov 12, 2017 at 2:35
2

You've already read the Address Decoding page of the 6502 Primer, so you have the basics of what you need to understand how to handle reading/writing multiple devices. Let's do a quick summary of what's going on with address decoding.

Roughly, when the CPU intends to read an address, it places it on the its A0-15 pins, sets R/WB high, waits around for a bit, and then reads the data bus for the value. It neither knows nor cares what what put the data on the bus or how it was selected. In particular, it doesn't know or care what addresses anything else saw; it just knows what it put out on its own address pins.

A Simple Configuration

In a really simple configuration of 32 KB RAM and 32 KB ROM, one could set up the decoding as follows:

  • CPU A0-14 connected directly to RAM A0-14 and ROM A0-14.
  • CPU A15 connected to:
    • RAM /CS (chip select) pin, and
    • an inverter, which in turn is connected to:
      • ROM /CS pin, which sees low when A15 is high and high when A15 is low.

When the CPU reads address $7FFF, both the RAM and ROM will see $7FFF on their address pins, A0-14. (Neither has an A15 pin because they're only 32K devices.) But /CS will be low for the RAM because A15 is low, causing it to read address $7FFF and put its value on the data bus, and /CS will be high for the ROM (because it gets inverted A15) which means the ROM will do nothing.

When CPU reads $FFFF, again both the RAM and ROM will see $7FFF on their address pins (because neither has an A15 pin; they see only the bottom 15 bits of the address). But in this case /CS will be high for the RAM (because A15 is high) and low for the ROM (because inverted A15 is low) and this time the RAM will ignore the access and the ROM, being selected, will place the contents of its location $7FFF on the data bus.

The key takeaway here is that each device has its own address and select pins, and what they see there and how they react to it depends on how the address decoding logic is set up. The devices don't necessarily see the actual address on the CPU's address bus: in the ROM read example above it saw address $7FFF even though the CPU's address bus had $FFFF (an address that the ROM doesn't have) on it.

Bank Switching

Now let's say you have two 32K RAM chips and a 32K ROM chip. Since this is 96K of memory, it can't all be accessed directly via setting A0-15, since that gives you only 64K addresses. But what you could do is have another device that stores a bit determining whether the top half of memory will access the RAM or ROM. (For simplicity we'll leave out here the details of exactly how you talk to that device.)

Calling that the 'bank bit', you could then set up your decoding as follows:

  • A0-14 connected directly to A0-14 of RAM0, ROM and RAM1.
  • A15 connected:
    • directly to /CS (chip select) of RAM0.
    • to an inverter whose output is connected to:
      • bank bit device /CS. That in turn has separate connections to:
        • ROM /CS, held high when the bank bit device is not selected, and
        • RAM1 /CS, also held high when the bank bit device is not selected.

When the bank bit device is selected:

  • If the bank bit is 0, it lowers ROM /CS, selecting it, and leaves RAM1 /CS high.
  • If the bank bit is 1, it leaves ROM /CS high and lowers RAM1 /CS, selecting it.

Now if the system starts on reset with the bank bit set to 0, reads will work just as in the simple configuration above. But if your code sets the bank bit to 1 (though magical undescribed means in this example, but via more decoding and an I/O address in the real world), the CPU will now be reading RAM1 in the higher addresses. In this way you can switch back and forth between ROM and RAM1 under CPU control whenever you like.

More Complexity

Obviously this general idea can be expanded to any degree you like: given any arbitrary number of 32K memories you can use A0-14 as the address for each device, and A15 and other information external to the address bus to decide which device gets accessed. And of course you are not restricted to 32K banks: you can do this remapping in arbitrarily small chunks, to the point where a sufficiently complex device (a full memory management unit) might translate all of A0-A15 from the CPU to any arbitrary addresses for other devices that vary depending on its current settings. (E.g., at one point in time a CPU read of $FFFF might read $00FF of RAM7, and at another point read $1234 of RAM19.)

So that's address decoding: mapping arbitrary addreses on the CPU's A0-A15 pins to any other arbitrary addresses on any other devices.

Timing

Address decoding logic doesn't work instantly; it takes time for the devices doing the address mapping (no matter how simple) detect the signals from the 6502, translate them, and set up the desired output signals. For example, the inverter we used above might be a 74LS04 which takes up to 5 ns to produce the proper inverted output after its input changes.

All of the remapping has to be done quickly enough that the device being accessed can still have the data on the data bus by the time the 6502 is going to read it. The Visual Guide to 65xx CPU Timing describes this in a lot more detail; I find the section "How the CPU Interacts With the Outside World" and its first diagram very helpful.

At lower speeds and with simple address decoding there are usually no problems, but at 4 MHz (which you mentioned you were aiming for) the time it takes to do the decoding could well become a problem, especially if you're using the rising edge of φ2 to enable things. You'll notice in the diagram I mentioned above that at 5 MHz you have only 90 ns between φ2 going high and when your devices must have their data stable on the data bus: if you're using a 74LS610 it may need 40 ns to do its thing, leaving not a lot of time for your device to read the address and set the data values. (That exact number may be wrong because I'm probably not reading the data sheet properly, but but you get the general idea. All the access numbers for that part are in that range.)

So think about how much decoding you really need! There's a lot of good discussion of both decoding and timing in this forum thread.

3
  • On the 6502, having movable 256-byte regions of address space which are relocatable on 256-byte boundaries is often nicer than having larger chunks which are only relocatable on correspondingly larger boundaries. If e.g. the region from $7F00-$7FFF is mappable to any 256-byte chunk in a 64K chunk of RAM that's separate from anything else, then code can use two-byte addresses to identify locations within that space. Many baking approaches try to bank large areas, but in many cases more fine-grained banking can be more useful, but simple if one uses an 8-bit upper-address latch.
    – supercat
    Nov 5, 2018 at 16:59
  • Yup. And if one were doing a multitasking system, being able to instantly swap out the zero page and stack page when switching processes would be very handy. Hmmm!
    – cjs
    Nov 7, 2018 at 2:56
  • Yup. A groups of three 74HC670 could be used to map four 256-byte chunks of address space to any of 4096 256-byte chunks (each chip handles four bits of all four addresses).
    – supercat
    Nov 7, 2018 at 4:04

You must log in to answer this question.