Reading the code from a mask rom i8052

We have a Rational R1000/400 computer in Datamuseum.dk and each PCB in it has an i8052 mask rom “diagnostic processor”.

We are currently trying to write a software emulation of this computer, and therefore we need the code in the mask ROM.

Later members of the MCS51 family of processors have all sorts of code protection features, but the basic i8052 does not, so it is a pretty simple operation.

The MCS51 family can execute code from internal (EP)ROM or external code memory:

../../_images/i8052_fig1.png

By default all code memory access above the internal (EP)ROM goes to external memory, but if the EA pin is pulled low, all access go to the external memory, so the trick is to pull EA low, run a small program out of external memory, jump to an address above the internal (EP)ROM, and then pull EA high to make the internal (EP)ROM visible again.

External code memory uses a partially multiplexed address/data bus:

../../_images/i8052_fig2.png

So with a 74x373 latch, some kind of ROM-ish chip, eprom, flash, whatever, and a bit of MCS51 code, one can hexdump the internal ROM over the chips serial port.

This is all old hat.

I didn’t particular want to bother with all that wiring, so instead I used a modern microcontroller, they are plenty fast to act as ROM for the rather pedestrian MCS51 family, in particular because the MCS51 can be seriously downclocked if need be.

I once bought a handful of Olimex LPC-P1343 so that is what I used, but whatever you have at hand is fine.

../../_images/i8052_fig3.jpg

If you use an Arduino or other chip in the low MHz clock range, you will have to downclock the MCS51 accordingly.

Connect an 8-bit I/O port to the i8052’s P0 pins, and three other pins to the ALE, PSEN and WR signals.

Next we need a tiny MCS51 program:

static uint8_t i8052_code[256] = {
    /* xx00 */ 0x02, 0x20, 0x03,    // LJMP #2003h
    /* xx03 */ 0x90, 0x12, 0x34,    // MOV  DPTR,#1234h
    /* xx06 */ 0x74, 0x00,          // MOV  A,#0
    /* xx08 */ 0x93,                // MOVC A,@A+DPTR
    /* xx09 */ 0xF0,                // MOVX @DPTR,A
    /* xx0A */ 0x02, 0x20, 0x03     // LJMP #2003h
};

The first LJMP gets us out of the address range of the internal ROM which in this case is 8KB. After that the MCS51 chip just sits in a tight loop reading the code memory location specified in xx04 and xx05, writing it to external data memory.

The array is 256 bytes long because we only implement the low 8 bits of the MCS51 chips address bus.

The microcontroller which acts as external “ROM” can update locations xx04 and xx05 as it see fits, and the second time the MCS51 chip pulls WR down, the locations content is readable on P0. We need to wait for the second WR pulse so we know the MCS51 has seen the updated address.

The main loop of the microcontroller code looks like this:

static void
spin()
{
        unsigned last_ale=1, last_wr=1, last_psen = 1;
        unsigned ale, wr, psen, adr = 0, data;
        port2_input();

        __disable_irq();
        while (1) {
                /* When ALE goes low, latch the address */
                ale = gpioGetValue(3, 0);
                if (last_ale == 1 && ale == 0)
                        adr = GPIO_GPIO2DATA & 0xff;
                last_ale = ale;

                /* When WR goes low, latch the read data */
                wr = gpioGetValue(3, 1);
                if (last_wr == 1 && wr == 0) {
                        data = GPIO_GPIO2DATA & 0xff;
                        got_read(data);
                }
                last_wr = wr;

                /* When PSEN is low, output the instruction */
                psen = gpioGetValue(3, 2);
                if (last_psen == 1 && psen == 0) {
                        port2_output();
                        data = GPIO_GPIO2DATA & ~0xff;
                        data |= i8052_code[adr];
                        GPIO_GPIO2DATA = data;
                } else if (psen == 1) {
                        port2_input();
                }
                last_psen = psen;
        }
}

Interrupts are disabled, we dont need them, and we dont want the microcontroller going of somewhere to fiddle with USB - that would ruin the access time of the simulated ROM.

The port2_{input|outut}() functions reconfigure the 8 pins we use for P0 in the obvious way.

The got_read() function is responsible for sending the read data upstream, and in my case I did it via the serial port using a small state-machine:

static uint8_t state = 0;
static const char hex[] = "0123456789abcdef";

static void
got_read(uint8_t val)
{
        if (serial_port_is_busy())
                return;

        switch (state) {
        case 0:
                sendchar(hex[(i8052_code[4] >> 4)]);
                break;
        case 1:
                sendchar(hex[(i8052_code[4] & 0x0f)]);
                break;
        case 2:
                sendchar(hex[(i8052_code[5] >> 4)]);
                break;
        case 3:
                sendchar(hex[(i8052_code[5] & 0x0f)]);
                break;
        case 4:
                sendchar(' ');
                break;
        case 5:
                sendchar(hex[val >> 4]);
                break;
        case 6:
                sendchar(hex[(val & 0x0f)]);
                break;
        case 7:
                if (!++i8052_code[5]) {
                    if (++i8052_code[4] == 0x21)
                        i8052_code[4] = 0;
                }
                if (i8052_code[5] & 0x0f) {
                        state = 4;
                        return;
                }
                break;
        case 8:
                sendchar('\r');
                break;
        case 9:
                sendchar('\n');
                state = 0;
                return;
        default:
                return;
        }
        state++;
}

If the serial port is still busy transmitting the previous character, we just ignore the reading, it will come back again in a moment. This takes care of the “second WR pulse” detail.

The rest of the code is just to emit a nice hexdump:

0000 c2 bb c2 88 75 d0 10 75 81 06 75 89 11 75 90 ff
0010 75 a0 ff 75 04 05 d1 26 21 78 00 00 00 00 00 00
0020 00 00 00 c1 5e 92 e0 f5 05 88 06 a1 1c 86 05 08
0030 88 06 a1 1c d5 05 02 a1 1c a8 06 a1 1c 20 b9 11
0040 a1 1c 20 b8 0c a1 1c 30 b9 07 a1 1c 30 b8 02 a1
0050 1c c0 10 92 e0 54 03 24 18 f9 87 10 a1 1c 20 b9
0060 15 08 a1 1c 20 b8 0f 08 a1 1c 30 b9 09 08 a1 1c
0070 30 b8 03 08 a1 1c 86 10 a1 1c 20 b9 11 a1 1c 20

The if (++i8052_code[4] == 0x21) means that addresses 0x0000-0x20ff will be dumped continously.

I ran the serial port at 57600 and used a signal generator to feed the i8052 chip a 100kHz clock.

When everything is ready, release RESET, wait for first lines of serial data, then pull EA high and let it run a handful of complete dumps, so you can check them against each other.

Obviously if one wants to the microcontroller can also control RESET, EA and for that matter generate the clock for the MCS51 chip.

../../_images/i8052_fig4.png

/phk