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:
- /AS - address strobe. This says "the CPU has placed a valid on the address bus and I am waiting for data". It is active low (hence the slash)
- R/W - read/write. This selects between the two bus modes: read or write. If read, the data bus is configured to receive a signal from an outside source. If write, the CPU places data on the bus ready for an external device to receive. 'R' is active high, 'W' is active low.
- /DTACK - data transfer acknowledge. This is controlled by an external device to say the bus operation has completed. If it was a read from the CPU's point of view, the external device will assert /DTACK once data has been placed on the address bus. If it's a write, the external device will assert /DTACK once the data has been taken from the bus and the operation completed. /DTACK is active low.
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,
- R/W is asserted
- the address is written to the address bus
- /AS is asserted
- (no change)
- the CPU waits for /DTACK to be asserted. If it is not asserted, the CPU will insert whole clock cycles until it is asserted.
- (no change)
- 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!
- this read data is latched, and /AS is negated
A write works in a similar fashion:
- R/W is asserted
- the address is set
- R/W is negated
- data is written on the bus
- the CPU waits for /DTACK...inserting whole clock cycles if not received in this half-cycle
- (no change)
- (no change)
- /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
Become a Hackaday.io Member
Create an account to leave a comment. Already have an account? Log In.