If you recall from last time, we got Veronica to perform a free run by NOP-ing her way through a phantom memory space.
Well, the next logical step is to get some real memory for her to run code from. I’m calling this a ROM Emulator, because it’s faking what would be the role of Read-Only Memory in a normal CPU startup sequence. The code and data will be entered into this memory by an external tool, and the CPU will not be able to write to it. In all other ways, however, it looks like regular memory to our girl.
A RAM chip by itself can’t do a whole lot. When powered up, it will be full of random junk. We need a method of data entry. Early computers usually had a big bank of toggle switches and blinking lights to handle this. If you’ve seen an old Altair or an old PDP-8, you know what I’m talking about. Hollywood has made sure we will always think Important Computers look like that. Anyways, the point of those switches and lights is to modify RAM directly, in order to give the CPU code to run immediately upon startup. It’s another one of those chicken-and-egg problems that plagued early computers. The CPU needs code in memory to run, but the CPU is usually what loads code into memory. You need a way to bootstrap that cycle.
We need two types of data entry- 16-bit addresses and 8-bit data. Rather than make a massive bank of 24 toggle switches, I found some nifty hexadecimal rotary encoders at the surplus store. Each has four data pins, and one common pin. Each data pin corresponds to a bit in the 4-bit nibble that represents the hex digit you select. Each ‘0’ in the bit pattern is connected to the common pin. Each ‘1’ in the pattern is left unconnected. If we connect the common pin to ground, and put pull-up resistors on all pins, we’ll get a 4-bit nibble composed of +5V for ‘1’, and 0V for ‘0’. If you find that confusing, you’re not alone. I set one up on the breadboard to make sure I understood it. When building a complex system, you need to make sure each piece you build on is completely robust and well-tested on its own. Otherwise, when things go wrong later on, you don’t know which parts you can trust when isolating the fault.
I need a bank of six of these guys. Four of them are for a 16-bit address, and two are for an 8-bit data byte. That’s what the 6502 wants, so that’s how we’ll enter our data as well. As I mentioned above, these switches will not fit in a breadboard. I did find, however, that they can be coaxed into an IC socket with some pin bending. Since I need so many connections, along with 24 pull-up resistors, I opted to solder up a little panel with some scraps. This will be much more robust than trying to do this on a breadboard, and it only took a few minutes. Since the switches are in IC sockets, I can reuse them later (they’re kinda expensive!)
Okay, now that we can enter data and addresses, we need an actual RAM chip, and a way to talk to it. I picked up a 32k chip with 15-bit addresses and 8-bit data. Luckily, RAM chips this small can still be bought at the major suppliers. Helpful commenters on past articles have also pointed out that CPU caches on old PCs are a good source for these. The 6502 specifies the RAM has to have a maximum access time of 450ns. Honestly, you’d have trouble finding RAM that slow nowadays, so no problem there. I’m using Static RAM (SRAM), which is by far the easiest. You put data in, and it stays there until power is cut. It works just like you want RAM to. Other types, such as Dynamic RAM (DRAM) are more complex to work with, so I’ll avoid those for now.
The main trick to this ROM Emulator is that the CPU and our data entry switches both need access to the RAM pins, but they can’t do it at the same time. That would amount to “bus contention”, which is when more than one thing is trying to drive data or address lines at the same time. Since our data source is just pull-up resistors, we need a way to “disconnect” them from the bus when it isn’t their turn. I’m using 74HC541 tri-state buffers for this. These guys are really nifty. Each one is basically a big block of on/off switches that can be controlled all at once. When they’re “off”, they leave their pins in a high-impedence state, so they won’t pick up noise like a floating pin would. They’re kind of like big Lego blocks that you can use to connect digital pieces together.
Here’s the schematic for my ROM emulator. The Eagle file is also available.
A RAM chip has two control signals- a Read/Write line, and a Chip Enable (or Chip Select) line. When the R/W line is high, a byte is read from the address specified on the address lines and placed on the data lines. If the R/W line is low, a byte is pulled from the data lines, and stored in the address specified on the address lines. The Chip Enable line controls when the chip takes these actions. It’s like a “Go” order. You set up the address and/or data lines, then pulse the Chip Enable line once to make it happen.
There’s a little bit of control circuitry needed at the bottom of the schematic. I’m using a 3PDT switch to control who has access to the RAM. In Run mode, the CPU gets the RAM. In Program mode, my data entry system gets access. There’s a DPDT switch to control whether we’re reading from, or writing to memory while in Program mode.These two switches juggle the R/W and Chip Enable lines as needed.
In Program mode, R/W is controlled directly by a toggle switch, and Chip Enable is controlled by a push button which lets me pulse it manually to control the RAM’s actions.
In Run mode, the R/W line goes directly to the CPU (pin 34), or can pulled high with a resistor to prevent the CPU from accidently overwriting the RAM (a good idea while debugging the circuit). The Chip Enable line goes to the clock output (labelled ϕ2, pin 39) on the 6502. The CPU knows what it’s doing, and will pulse this line when it wants to read or write memory.
So, to enter data into the memory, I power it up, set the Load/Store switch to Store, and set the Run/Program switch to Program. Then, for each byte I want to enter, I set the desired address on the Address hex switches, set the desired byte on the Data hex switches, then press the pushbutton once. Repeat that for every byte of data to be stored into RAM. If you think that sounds incredibly tedious, you’re not wrong. But hey, we gotta start somewhere.
Here’s a test of this process, with my ROM Emulator built on a breadboard. HexOut is showing the lower byte of memory address $00be on the left, and the contents of that address on right. When I start, you can see the SRAM has a random value ($9D) there. The device is in Read mode, so each time I push the button, it’s reading the value from the given memory location. I push it twice to confirm that it is $9D. Next, I switch to Write mode. My desired value ($EF) is already set on the hex switches, so it shows on the display. I then hit the button once to write it to memory. Then I switch back to Read mode, and push the button to verify my value is now there, replacing the $9D. It works!
Okay, so the next step is to bring this together with Veronica and use it to write and run some real code! Here’s what that all looks like, building on the previous breadboard that Veronica was sitting on:
Next, I want to use the ROM Emulator to write some code that Veronica can run. I’ve already entered $1230 at addresses $FFFC and $FFFD. If you recall from last time, this is the Restart Vector where the 6502 looks for an address to start running code from. I’m now going to write some code at address $1230. The code is in assembly language, of course. We’re a looooong way from a C compiler or anything fancy like that. The program is quite simply:
It’s one line of 6502 assembly code that jumps to address $1230. If we put that code at address $1230 in memory, we’ll have a nice little infinite loop. To enter this code, we need to hand-assemble it into machine code, since we don’t even have the luxury of an assembler here. Hand assembling is tedious but straightforward. You find the opcode for the instruction you want with the addressing mode you’re using, then look up the format of the argument(s), and finally compose the pattern of bits that will have that meaning to the CPU. This information is all in the 6502 datasheet, and you can also get it from any good 6502 assembly programming reference. So, using the datasheet, we determine that the machine code for a JMP to an absolute address is $4C. To assemble this code, we need to enter $4C $30 $12 at memory address $1230. Note that the bytes of the memory address are backwards. That’s because the 6502 is “little endian”. The least-signficant byte always comes first.
Here’s some video of me entering this program into memory:
Once the code is in memory, I can flip the Run/Program switch to Run, turn on the CPU, and single-step it to see if it works. Here you see me reset it, then after seven clocks, it fetches the address $1230 from the Restart Vector ($FFFC), and jumps to there. Then you see it fetch the JMP from $1230, and the two-byte destination for the jump from $1231 and $1232. Then it executes the jump, which lands it back at $1230 to do it all over again. Finally, I flip to the full speed clock and let her rip.
Seeing that was a damn fine moment, I can tell you. It’s one thing to read up on how this stuff works, but pushing some code by hand into a RAM chip and watching that code execute is really magical. I seem to have accidentally built a computer.
This is cool and all, but writing code with hex switches is for chumps. I need an easier way to do this, and I think I know just the thing. Stay tuned to find out if my idea works!