How does it work?
The 6502 (or W65C02S in our case) is really designed to have some ROM (or modern flash, etc.) to enable it to boot. At power on or reset, it reads an address out of a fixed location normally stored in ROM and jumps to it. That code then does what’s needed to make the system get going.
And here we have a small issue: The Ruby has no ROM…. So why not, and how to make it work?
One reason was to simplify the hardware. ROMs are typically, relatively slow, even by the very old 6502 standard. To make matters worse, the 6502 has a somewhat interesting way of accessing memory that means the memory has to be at least capable of responding in half the time you might expect. So for a 1Mhz 6502, you might expect a 1-microsecond access time, however it really needs just under 500nS. Take that to the maximum speed that a modern W65C02S is rated for which is 14Mhz and you need better than 70nS. In reality, you actually need better due to the latency/propagation delay of signals going from the 6502 through e.g. address decoder before it gets to the ROM chips. The same timings apply for RAMs too.
Also the 6502 only reads or writes the memory for half a clock cycle and this was taken advantage of by the various video generation circuitry of the early microcomputers such as the Apple II – 6502 runs flat-out, but does it’s RAM access in half a cycle, the video jumps in on the other half cycle and pulls data out to send to the display.
So we need (relatively) fast ROMs, and there weren’t any suitable. I was aiming for classic through hole technology too. There’s also the issue of having to reprogram them. I did this regularly over 30 years ago and it quickly becomes a chore, so I decided to go for an all RAM solution.
RAM chips are still available in through-hole technology, fast enough and with the capacity I needed, although I ended up with 2 32K x 8 bit chips as there were no suitable 64Kx8 ones.
The issue now – how to get data into the 6502 in the first place.
Enter the ATmega.
I had been experimenting with using an ATmega to generate composite video and had a system using an ATmega1284p which has 16KB of internal RAM and was generating a nice 320×240 pixel monochrome display, so I went about working out how to interface that to the memory system with the aim of using that as it’s video output. (I’ve since put that idea on hold – more on that later)
And this is where a modern version of the 6502 works well. The WDC 6502s which are still being made today have a couple of features that work well here. One is the BE or Bus Enable signal. Pull this low and the 6502 immediately tri-states all its address and data lines. The other is the Rdy signal. Pull this low and the 6502 stops at the end of the current clock cycle. So, it’s a simple matter of halting the 6502, disabling its bus, connecting the bus to the ATmega and having the ATmega fill the RAM, then reverse the process and let the 6502 boot. What could possibly go wrong …
The 6502 has a single Read/Write output pin. the RAM needs separate Read and Write inputs. There are also 2 RAM chips and I want to decode a block for some IO devices…
Enter the GAL…
A GAL is the pre-cursor to the FPGA. It’s a Generic Array of Logic. Initially one-time programmable (PALs), the GAL can be re-programmed 100 or so times. They were popular in the early 1980’s to the mid 90’s when the ULA was being used in home computers such as the BBC Micro and ZX Spectrum. In the Ruby it’s doing the job of sorting out the read and write signals, working out which enable signal to send to the RAM chips and enable some IO decoding to be performed. As a bonus I can hook up an LED or 2 to help with debugging if required.
There is a minor issue with GALs these days – Lattice stopped making them a few years ago an dsupplies from the mainstream outlets have all dried up. Atmel (now Microchip) are still making new GALs, however I lack the programmer to correctly program them and programmers that can program the Lattice ones simply won’t work, so right now I’m sourcing them from what appears to be reliable component recycles in China and so-far they’re all working perfectly.
The memory map
The 6502 divides it’s 64KB of memory up into 256 byte pages and it needs RAM in page 0 ($0000 -> $00FF) and page 1 ($0100 -> $01FF). This is for fast data storage and the stack. The reset vector (and interrupt vectors) are at the top of memory ($FFFA -> $FFFF) which is normally ROM, so I arranged the system so that the top page from $FF00 through $FFFF was the page that was shared with the ATmega. This needs 8 pins from the ATmega for the data bus and 8 for the address. Also a few more to control the 6502 Reset input, Rdy and BE signals, as well as read and write. The resulting memory map looks like:
$0000 -> $FDFF: RAM $FE00 -> $FEFF: 256 bytes of IO space $FF00 -> $FFFF: 256 bytes of RAM shared between the 6502 and ATmega
It’s quite simple and easy to decode inside the GAL and leaves the maximum amount of RAM for program and data usage.
On the breadboard version there was no real IO – just a pin I could connect to a ‘scope. On the stripboard version there was an 8-bit latch I could use and on the PCB version there is a WDC65C22S VIA with it’s dual parallel ports, timers and other features.
So at power on, the ATmega controls the system. It holds the 6502 in reset and pulls the BE pin low to cause the 6502 to tri-state it’s buses. It copies the minimal bootloader into the shared memory page which includes the reset vector then releases the memory (tri-states the pins used for address and data lines), lets the 6502 access the memory and takes it out of reset.
The 6502 wakes up, reads the reset vector and jumps to it. (This is $FF00 initially) That code relocates itself to a lower location in RAM then communicates with the ATmega to get more code to finish the bootstrap process.
The communication is fairly straightforward and relies on another trick of the W65C02S – the Rdy pin is bi-directional and there is a new instruction; WAI which causes the 6502 to halt, pull Rdy low then wait for an interrupt. The ATmega meanwhile has been reading the Rdy pin and when it sees it going low, it switches the bus RAM control over, reads a command and data out of the shared memory page and does whatever it’s commanded to do. For example, it might be asked to load block 5 of the bootloader, so it copies that block from it’s own internal flash to the shared memory then switches the bus control back to the 6502 and sends an interrupt to the 6502 which causes it to wake from the WAI sleep and carry on.
And that’s the key to the 6502s communications. Everything happens via the small page of shared RAM – a virtual serial port, simple ROM filing system and a full-featured filing system that can use the spare RAM on the ATmega or it’s NVRAM or an external SD card as file storage.
It’s not a million miles away from the BBC Micro, however in this case, once the 6502 has booted, it’s really the master of the system. The ATmega is a multi-function peripheral device that just helped to boot the 6502 in the first instance.
From now on, it’s 6502 code time and I’ll talk about that next.