Close

Making a basic system from a CPU and an Arduino

A project log for Big Daddy 68k

Making a games console from the ground-up

simonhSimonH 02/23/2017 at 22:220 Comments

So you've done your free-runner, and you know your £2 CPU from ebay is not a brick. If you need a flashing LED in your home you now what to do. But now let's get busy programming this thing!

A basic system needs compiled code to execute, some sort of I/O and a place to store the temporary data - RAM. If you remember from before, the CPU presents a very generic address/data bus to the outside world. This is its only real means of communicating; there are no I/O ports. All I/O needs to be memory mapped. The bus that comes out does not have any integrated logic for driving DRAM or more complex memories. It just says the address to read...now please give me data. If you want more complex things attached to the bus (like DRAM) you've got to manage that with external component.

A bus cycle

The bus is really simple and requires minimal effort to do anything with it. There are discreet pins for address and data; pin functions are not multiplexed. There are really only three control signals:

The bus take at least four CPU clock cycles to complete one bus transfer - read or write. Each clock cycle is broken into two half-cycles. This means there are eight stage to a bus cycle.

For a read,

  1. R/W is asserted
  2. the address is written to the address bus
  3. /AS is asserted
  4. (no change)
  5. the CPU waits for /DTACK to be asserted. If it is not asserted, the CPU will insert whole clock cycles until it is asserted.
  6. (no change)
  7. data is read from the data bus into the CPU. Remember - the external device will have asserted /DTACK after it has placed data on the bus!
  8. this read data is latched, and /AS is negated

A write works in a similar fashion:

  1. R/W is asserted
  2. the address is set
  3. R/W is negated
  4. data is written on the bus
  5. the CPU waits for /DTACK...inserting whole clock cycles if not received in this half-cycle
  6. (no change)
  7. (no change)
  8. /AS is negated, R/W is asserted

A memory-mapped Arduino

What we will ultimately do is construct a system with RAM, ROM and I/O - where all I/O is provided by an Arduino. However as mentioned, all I/O is done via memory-mapped I/O. We may as well temporarily get the Arduino to also become ROM and RAM!

We can connect the address and data busses to the pins of our Arduino, in addition to the control signals. The Arduino can listen for /AS, then decode the address, read the data from the bus/write data to the bus, and then assert /DTACK. We can have a small byte array declared in the Arduino and this can represent the 'RAM' address space. The address decoded from the bus can just index into this array. Code and data can be stored in this array.

Here's some pseudocode from what we'll do on the Arduino:

void setup(void)
{
    //wait for the /AS
    attachInterrupt(AS_PIN, &address_strobe);
    write(DTACK, HIGH);
}
//////////////
//our megabyte address space
unsigned char memory_array[1048576];
//////////////
void address_strobe(void)
{
    //read the R/W signal
    bool rw = read(RW_PIN);

    //read the address bus
    unsigned int addr = 0;
    for (int count = 0; count < 20; count++)
         addr |= (read(ADDR_PIN + count) << count);

    if (rw)
    {
         unsigned char data = memory_array[addr & 1048575];
         for (int count = 0; count < 8; count++)
                write(DATA_PIN + count, data & (1 << count);
    }
    else
    {
         unsigned char data = 0;
         for (int count = 0; count < 8; count++)
                data |= (read(DATA_PIN + count) << count);
         memory_array[addr & 1048575] = data;
    }

    //tell the 68k the transfer is ready
    write(DTACK, LOW);
    //wait a few clock cycles
    write(DTACK, HIGH);
}

The amount of time the Arduino will take to do one of these transactions is high - there are a lot of instructions here, and even though it runs at a higher clock speed than the 68k, /DTACK will be high for some time. The 68k will just insert wait states until DTACK is low.

Not enough pins

In the above pseudocode we need at least eight pins for the data bus (used as both input or output), one input pin for /AS, one input pin for R/W, one output pin for DTACK and twenty input pins for the address bus. I'm using an Arduino nano and it does not nearly have enough pins for this!

So we can simplify what we need to get some pins back. We must have the eight data bus pins as they are bidirectional. We must have the /AS, as we use it to trigger an interrupt. We need control over the assertion time of /DTACK. The address bus and R/W are only ever inputs. We can replace these pins with a 74'165 shift register (or three). These integrated circuits allow us to change these parallel signals into a serial one. '165 chips can be chained together so we only need enough circuitry to drive one of them. The problem with doing this is speed - we're changing our parallel bus into a serial one. The more bits we wish to read, the longer it will take.

Driving a '165 chain requires a clock signal, a shift/load signal and a data output signal. To load the register with your parallel signal you simply negate the shift/load signal and then re-assert it. The data is now latched internally. To read the data out one bit at a time you assert and negate the clock signal once for every bit in the register. Each bit will then come out of the data output signal.

This allows us to turn our 21 pins of address and R/W into three pins: clock, shift/load, data. We update our pseudocode accordingly:

    write(SH_LD, LOW);
    //delay
    write(SH_LD, HIGH);
    //data is now latched in the register

    bool rw = read(DATA_PIN);

    unsigned int addr = 0;
    for (int count = 0; count < 20; count++)
    {
         //select the next bit
         write(CLK, LOW);
         //delay
         write(CLK, HIGH);
         addr |= (read(DATA_PIN << count);
    }
(this assumes the R/W signal is connected to the parallel input as the first signal, with each address bit connected sequentially)

Adding hypercalls

Our Arduino now acts as RAM - the CPU can read and write data to it. The Arduino simply acts as a slow, dumb memory. However the Arduino is connected to the host PC - it also has other spare pins we can do interesting things with. We can add memory-mapped I/O to the system easily by simply re-defining certain addresses within the memory space to do special things.

eg,

    if (rw)
    {
         unsigned char data = 0;
         //an arbitrary address
         if (addr == 0x1000)
                data = read_host_keyboard();     //and arbitrary function
         else
                data = memory_array[addr & 1048575];
         for (int count = 0; count < 8; count++)
                write(DATA_PIN + count, data & (1 << count);
    }
    else
    {
         unsigned char data = 0;
         for (int count = 0; count < 8; count++)
                data |= (read(DATA_PIN + count) << count);
         if (addr == 0x1000)
                write_host_console(data);
         else
                memory_array[addr & 1048575] = data;
    }

The 68k can now communicate with the outside world. By constructing a simple MMIO command protocol we can command the Arduino to do any function we like.

Next time we'll add real RAM...

Discussions