Day 4: The memory bus and a running CPU program

So last time I got a Motorola 68K up and running, cycling through its entire address space and attempting to execute a dummy instruction. However, in order to get the CPU running an actual program, I need to hook up an actual bus to it.

I ended up having to do a hell of a lot of trial and error and poring over data sheets cross referencing signal descriptions to get this working. In the end, I settled upon a handful of signals on the 68000 as being the interface for my bus: RW, LDS, UDS, A23-A1, and D15-D0. These signals are as follows:

  • RW: Indicates whether the 68000 is currently in a read or a write cycle. 1 if reading, 0 if writing.
  • LDS: 0 if the 68000 wants to read/write D7-D0, 1 if those pins should be ignored.
  • UDS: 0 if the 68000 wants to read/write D15-D8, 1 if those pins should be ignored.
  • A23-A1: The address the 68000 wants to read from or write to.
  • D15-D0: Bidirectional data pins to connect to each device on the bus.

With that settled on, I also need a way of being able to put multiple devices on the same bus. The way I’ll do this is by allocating some number of bits of the address pins to a demultiplexer, this demultiplexer will feed individual enable lines to all devices on the bus and each of those devices will use the enable line to determine if it needs to suppress or enable operation.

In the end, I went with using A23-A17 as input to the demultiplexer (which will allow me to connect up to 128 different devices on the bus), and using the remaining A16-A1 as an address passed to each device on the bus (allowing each device to address up to 65535 16-bit words of data, giving 128KB per device).

With the bus interface out of the way, it’s time to hook up a ROM to this CPU and see if I can get it executing some real instructions.

My ROM is really simple actually:

module ROM #(
	parameter ROM_PATH = "",
	parameter ROM_ADDR_BITS = 8
) (
	input wire				I_CLK,
	input wire				I_ENABLE_N,
	
	input wire [15:0]		I_ADDR,
	input wire				I_RW,
	input wire				I_UDS,
	input wire				I_LDS,
	output wire [15:0]	O_DATA
);

localparam ROM_SIZE = 1 << ROM_ADDR_BITS;
localparam ADDR_MASK = ROM_SIZE - 1;

reg [15:0] mem [ROM_SIZE];
reg [15:0] out;

// if chip select is asserted and I_RW is read, set O_DATA to output data
// otherwise, set 0 to let it be or'd together with other bus devices
assign O_DATA = I_ENABLE_N ? 0 : ( I_RW ? out : 0 );

initial begin
	$readmemb( ROM_PATH, mem );
end

// note: we actually need this clock cycle setup because otherwise Quartus fails to infer block RAM
always @(posedge I_CLK) begin
	out <= mem[I_ADDR & ADDR_MASK];
end

endmodule

All this really does is gives me a handy module I can instantiate with a path to a memory initialization file and a configurable power-of-two size and adds a read-only. It’s also structured so that Quartus is able to deduce that it should instantiate it using on-chip block RAM rather than using a bunch of flip-flops.

Connecting this is very simple, I can just feed it my global 50MHz clock, one of the enable lines from my bus demux (I picked 0 because that puts this ROM at $000000, which will be the very first address the CPU tries to read), and the A, RW, LDS, and UDS lines from the CPU. I also set the CPU’s input data lines to this module’s O_DATA lines.

Next, I need some code to initialize this ROM’s memory with. I used Easy68K to put together a small program containing the 256-byte interrupt table the 68000 expects as well as a simple loop that’s just an infinite nop loop (a label, a nop, and a jump back to the label), plus a C# utility that could convert the final binary ROM file into the format expected by readmemb.

Watching this from a simulator showed exactly what I had hoped to see: the CPU looping over the same two instructions (the NOP and the JUMP).

My next goal was to add a test bus device that could take a value write and store it in a register, as well as output the current value of that register so I could hook it up to some LEDs on the actual board. I hooked this device up to bus enable line 127, putting it at address $FE0000, and then changed my ROM code to write $BEEF into that test device.

Once again, this worked pretty much as I expected: the CPU issues a write cycle and selects device 127 with the value $BEEF on its data pins, and my test bus device takes the value as expected. Good!

From there, it didn’t take very much time to hook up some actual work RAM to the CPU. My RAM module works almost identically to the ROM module above, except I added a write cycle and also added support for reading/writing individual low and high bytes. I instantiate a 64KB RAM module and hook it up to bus enable 1 (which puts work RAM at $020000). I tested it by writing a value to the beginning of work RAM and then reading it back, as well as pushing a value onto the stack and popping it off (I modified the interrupt table to put the initial stack pointer at the end of work RAM).

That left me with one more task: get code running on my CPU to interface with my gamepad serial interface.

This involved writing a bus interface that would take my existing serial controller and interface it with the memory bus to allow the CPU to interact using just memory reads/writes. The plan is this: a write to $100000 relative will cause the gamepad to enter the polling state. A read from $100000 will return whether or not the gamepad is currently busy polling. A read from $100002 will return the last known state of player 1’s gamepad, and a read from $10004 will return the last known state of player 2’s gamepad.

After a bit of wiring, I went ahead and instantiated the gamepad module in my testbench. I also changed my ROM code to read the gamepad as follows:

  • Write a value to $100000. Any arbitrary value will work.
  • Reads back from $100000 in a loop and waits for the result to become 1 (indicating that the gamepad module has acknowledged the read request and entered the polling state)
  • Reads back from $100000 in another loop waiting for the result to become 0 (indicating that the gamepad module has finished polling the gamepad state)
  • Reads a value from $100002 and writes the result to my test bus device so that later I’ll be able to replicate this test on hardware and see the result on the LEDs for debugging.

Now, I should note that my first attempt at this revealed some amusing errors. First of all, I generated a read signal in my gamepad bus interface by checking if the CPU was in a write cycle. Unfortunately I completely forgot to check if the gamepad bus interface was actually, y’know, selected by the CPU. Which meant that any write, anywhere at all, would cause the gamepad module to enter the polling state. Whoops.

My next mistake was testing the wrong bit of $100000 when checking for the busy flag. I accidentally tested the second bit and reversed my comparisons, which caused the CPU to hang forever waiting for the second bit to become 1 (which it never did).

My next mistake was forgetting to actually tie the gamepad bus interface’s data output into the CPU’s data input. This caused the CPU to hang while wait for the busy flag to become 1, which it never did because the input was all 0s. Whoops.

My last mistake was reading from $100001 instead of $100002 for getting player 1’s gamepad state. That triggered a bus address exception because I tried to read a whole word on a non-word-aligned address, although I could see from the logic analyzer that it had jumped to my defined interrupt handler in the interrupt table as a result, so at least I know that works!

But, finally, at long last, I saw my code polling for gamepad state in a loop and writing the result out to my test bus device. It was time to program this onto the real thing!

And it works!

The result is you can see the states of the B, Y, Select, Start, and D-Pad buttons on the lights on the board. Now, I know it looks precisely the same as my previous gamepad FPGA experiment, but this time it’s driven by a running CPU which proves to me that my CPU is up and running real code and interacting with bus devices. Excellent!

Now that I’ve got this done, I think my next goal will be to start working on the other components of the game console. Perhaps the video encoder next?

Published by KillaMaaki

I'm a programmer, a gamer, and author of Unity Multiplayer Games by Packt Publishing.

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.