Ok, we connect an 8-bit microprocessor to Arduino Mega, but how does it really work? Let’s answer that for 6502:
- Arduino Mega to 6502 connections.
- Cycle-by-cycle operation of 6502
- Memory maps
- IO device(s)
Arduino to 6502 connections
Arduino Mega is connected to the 6502 as follows. Arduino will drive the clock signal high <-> low, and during each cycle will look at the uP control signals (R/W) and the address bus to figure out what needs to be done, and then will either drive data bus to uP or capture data from databus. Arduino can also drive the IRQ and NMI signals to cause interrupt.
Arduino ports are mapped to the 6502 signals as follows:
/* Digital Pin Assignments */
#define DATA_OUT PORTL
#define DATA_IN PINL
#define ADDR_H PINC
#define ADDR_L PINA
#define ADDR ((unsigned int) (ADDR_H << 8 | ADDR_L))
#define uP_RESET_N 38
#define uP_RW_N 40
#define uP_RDY 39
#define uP_SO_N 41
#define uP_IRQ_N 50
#define uP_NMI_N 51
#define uP_E 52
#define uP_GPIO 53
Cycle-by-cycle operation of 6502
Lets break each cycle in small events:
- CLK goes down (falling edge)
- CLK stays low
- CLK goes high (rising edge)
- CLK stays high
- finaly CLK goes down (falling edge).
Things happen on clock edges.
6502 takes in CLK0 (IN) and buffers it and outputs two clock outputs, CLK1 and CLK2. Internally it seems everything happens with respect to CLK2 edges however, the internal delay between CLK0 and outputs is small and for our purposes we will assume all clocks edge are aligned and refer to CLK0 (output from Arduino into 6502).
- CLK goes down.
- Processor drives new address and R/W to indicate the type of bu activity (Read or Write). Note the processor is not ready to do data transaction yet.
- CLK goes high to indicate data transaction has begun. If this is a READ transaction, the processor makes its databus INPUT and the corresponding IO device should start driving the databus. If this cycle is a WRITE operation, the processor will drive the databus and the corresponding IO device should make its databus an INPUT and get ready to latch the data.
- CLK goes down to indicate data transaction is complete. Since transaction is complete, databus does not have to be driven anymore by either 6502 or the IO device. Clock going down also starts the new cycle. Rinse and repeat.
As you can see each cycle can be a memory transaction.
If you plan to modify the Arduino code to add your own device support, it’s good to know the following two concepts:
- SETUP time.
- HOLD time.
Since all activity happens on clock edges, setup and hold times are the minimum time required to get the data ready before the clk edge (setup time) and to keep it constant after the clock edge so there is no corruption while latching is completed (hold time). if data was not ready in time or it changed during latch, you will be scratching your head on why your code does not work. Having said that, at ~100kHz operating clock frequencies, we usually don’t have to worry about this but make sure data is ready before toggling clock edges and we change data afterwards.
CLK driver code
We need a function that toggles the CLK and monitors R/W, address, and data bus signals and acts accordingly at each cycle. To achieve that the Arduino code uses timer1 interrupt.
Timer1 Interrupt fires at regular intervals, 95kHz to be specific and toggles CLK output, while monitoring CPU signals. Since timer fires at fixed times, it is important the timer handler completes before the next interrupt.
This is the excerpt from the Ardino code. Since we don’t care too much about when CLK is low, code starts by driving the CLK high.
////////////////////////////////////////////////////////////////////
// Processor Control Loop
////////////////////////////////////////////////////////////////////
// This is where the action is.
// it reads processor control signals and acts accordingly.
//
ISR(TIMER1_COMPA_vect)
{
// Drive CLK high
CLK_E_HIGH;
// Let's capture the ADDR bus
uP_ADDR = ADDR;
if (STATE_RW_N)
//////////////////////////////////////////////////////////////////
// HIGH = READ transaction
{
// uP wants to read so Arduino to drive databus to uP:
DATA_DIR = DIR_OUT;
// Check what device uP_ADDR corresponds to:
// ROM?
if ( (ROM_START <= uP_ADDR) && (uP_ADDR <= ROM_END) )
DATA_OUT = pgm_read_byte_near(rom_bin + (uP_ADDR - ROM_START));
else
if ( (BASIC_START <= uP_ADDR) && (uP_ADDR <= BASIC_END) )
DATA_OUT = pgm_read_byte_near(basic_bin + (uP_ADDR - BASIC_START));
else
// RAM?
if ( (uP_ADDR <= RAM_END) && (RAM_START <= uP_ADDR) )
DATA_OUT = RAM[uP_ADDR - RAM_START];
else
// 6821?
if ( KBD <=uP_ADDR && uP_ADDR <= DSPCR )
{
// KBD?
if (uP_ADDR == KBD)
{
... // handle KBD register
}
else
// KBDCR?
if (uP_ADDR == KBDCR)
{
... // handle KBDCR register
}
else
// DSP?
if (uP_ADDR == DSP)
{
... // handle DSP register
}
else
// DSPCR?
if (uP_ADDR == DSPCR)
{
... // handle DSPCR register
}
}
}
else
//////////////////////////////////////////////////////////////////
// R/W = LOW = WRITE
{
// RAM?
if ( (uP_ADDR <= RAM_END) && (RAM_START <= uP_ADDR) )
RAM[uP_ADDR - RAM_START] = DATA_IN;
else
// 6821?
if ( KBD <=uP_ADDR && uP_ADDR <= DSPCR )
{
// KBD?
if (uP_ADDR == KBD)
{
... // handle KBD register
}
else
// KBDCR?
if (uP_ADDR == KBDCR)
{
... // handle KBDCR register
}
else
// DSP?
if (uP_ADDR == DSP)
{
... // handle DSP register
}
else
// DSPCR?
if (uP_ADDR == DSPCR)
{
... // handle DSPCR register
}
}
}
////////////////////////////////////////////////////////////////
// We are done with this cycle.
// one full cycle complete
clock_cycle_count ++;
// start next cycle
CLK_E_LOW;
// If Arduino was driving the bus, no need anymore.
// natural delay for DATA Hold time after CLK goes low (t_HR)
DATA_DIR = DIR_IN;
}
Memory Map
The code currently emulates an Apple I which has 256 bytes of ROM, 8K of BASIC ROM, 4K of RAM and a 6821 used for keyboard and display.
// MEMORY LAYOUT
// 4K MEMORY
#define RAM_START 0x0000
#define RAM_END 0x0FFF
byte RAM[RAM_END-RAM_START+1];
// ROMs (Monitor + Basic)
#define ROM_START 0xFF00
#define ROM_END 0xFFFF
#define BASIC_START 0xE000
#define BASIC_END 0xEFFF
////////////////////////////////////////////////////////////////////
// Woz Monitor Code
////////////////////////////////////////////////////////////////////
//
PROGMEM const unsigned char rom_bin[] = {
0xd8, 0x58, 0xa0, 0x7f, 0x8c, 0x12, 0xd0, 0xa9, 0xa7, 0x8d, 0x11, 0xd0,
...
0x00, 0xff, 0x00, 0x00
};
// BASIC ROM starts at E000
PROGMEM const unsigned char basic_bin[] = {
0x4C, 0xB0, 0xE2, 0xAD, 0x11, 0xD0, 0x10, 0xFB,
...
0xE0, 0x80, 0xD0, 0x01, 0x88, 0x4C, 0x0C, 0xE0
};
RAM is created by byte RAM[RAM_END-RAM_START+1]
. The two PROGMEM… make sure the ROM code is saved in the Arduino’s Flash Memory.
IO device(s)
Apple I has a 6821 PIA to handle keyboard and display. After reviewing the monitor code, I cheated and emulated 6821 just enough to get keyboard and display working. Woz’s monitor code and BASIC seems to work, so that was enough for me.
To emulate any IO device, you need to read thru the datasheet and and figure out the registers and what they do/how. The beauty of software is you can anything you want.
6502 IO devices are memory mapped, meaning all IO devices are memory mapped and accessed thru memory read/writes.
Last, since you can use existing arduino shields like display, sdcard, audio, etc. you can memory map them to make them available to 6502.