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.