Buttons and ports

The 8080 does I/O via its IN and OUT instructions. It has 8 separate IN and OUT ports - the port is specified by the data byte of the instruction. For example, IN 3 will put the value of port 3 into register A, and OUT 2 will send A to port 2.

For information about what port does what, we'll rely on this info via Computer Archeology. If this information was not available, we'd have to deduce it by looking at the schematic and reading and stepping through the code.

   Ports:    
    Read 1    
    BIT 0   coin (0 when active)    
        1   P2 start button    
        2   P1 start button    
        3   ?    
        4   P1 shoot button    
        5   P1 joystick left    
        6   P1 joystick right    
        7   ?    

    Read 2    
    BIT 0,1 dipswitch number of lives (0:3,1:4,2:5,3:6)    
        2   tilt 'button'    
        3   dipswitch bonus life at 1:1000,0:1500    
        4   P2 shoot button    
        5   P2 joystick left    
        6   P2 joystick right    
        7   dipswitch coin info 1:off,0:on    

    Read 3      shift register result    

    Write 2     shift register result offset (bits 0,1,2)    
    Write 3     sound related    
    Write 4     fill shift register    
    Write 5     sound related    
    Write 6     strange 'debug' port? eg. it writes to this port when    
            it writes text to the screen (0=a,1=b,2=c, etc)    

    (write ports 3,5,6 can be left unemulated, read port 1=$01 and 2=$00    
    will make the game run, but but only in attract mode)    

There are 3 ways to implement I/O in our software stack (which consists of the 8080 emulator, machine code, and platform code).

  1. Put knowledge about the machine into your 8080 emulator

  2. Put knowledge about the 8080 emulator into your machine code

  3. Invent a formal interface between the 3 pieces of code to allow information to be exchanged via API

I ruled out option 1 - it is pretty clear that the emulator is the bottom level of this call chain and should stand alone. (Imagine wanting to reuse the emulator for a different game and you'll see what I mean.) In general pushing higher-level data structures down into lower levels of abstraction is bad software design.

I chose option 2. Let me show you the code first:

   while (!done)    
   {    
    uint8_t opcode = state->memory[state->pc];    

    if (*opcode == 0xdb) //machine specific handling for IN    
    {    
        uint8_t port = opcode[1];    
        state->a = MachineIN(state, port);    
        state->pc++;    
    }    
    else if (*opcode == 0xd3)  //OUT    
    {    
        uint8_t port = opcode[1];    
        MachineOUT(state, port);    
        state->pc++;    
    }    
    else    
        Emulate8080Op(state);    
   }    

This reimplements the opcode handling for IN & OUT into the same layer that calls the emulator for the rest of the instructions. In my opinion, this is cleaner. It is like an override or subclass for these 2 instructions that belong in the machine layer.

The drawback is that it moves the opcode emulation into 2 places. I wouldn't fault you for deciding to do option 3. Option 2 is certainly less code, but option 3 is more "pure" at the expense of some added complexity. It is a style choice.

Shift Register

There is a clever bit of hardware in the Space Invaders machine that implements a bit shift command. The 8080 has instructions to shift by 1 bit, but it would take tens of 8080 instructions to implement a multi-bit/multi-byte shift. The special hardware allows the game to do it it in just a few instructions. Every sprite on the game board is drawn using it, so it is used many times per frame.

I do not believe I could explain it any better than the excellent Computer Archeology breakdown:

   ;16 bit shift register:    
   ;    f              0    bit    
   ;    xxxxxxxxyyyyyyyy    
   ;    
   ;    Writing to port 4 shifts x into y, and the new value into x, eg.    
   ;    $0000,    
   ;    write $aa -> $aa00,    
   ;    write $ff -> $ffaa,    
   ;    write $12 -> $12ff, ..    
   ;    
   ;    Writing to port 2 (bits 0,1,2) sets the offset for the 8 bit result, eg.    
   ;    offset 0:    
   ;    rrrrrrrr        result=xxxxxxxx    
   ;    xxxxxxxxyyyyyyyy    
   ;    
   ;    offset 2:    
   ;      rrrrrrrr  result=xxxxxxyy    
   ;    xxxxxxxxyyyyyyyy    
   ;    
   ;    offset 7:    
   ;           rrrrrrrr result=xyyyyyyy    
   ;    xxxxxxxxyyyyyyyy    
   ;    
   ;    Reading from port 3 returns said result.    

For OUT, writing to port 2 sets the shift amount, and writing port 4 sets the data in the shift registers. Reading via IN 3 returns the data shifted by the shift amount. An implementation for my machine is something like this:


   -(uint8_t) MachineIN(uint8_t port)    
   {    
       uint8_t a;    
       switch(port)    
       {    
           case 3:    
           {    
               uint16_t v = (shift1<<8) | shift0;    
               a = ((v >> (8-shift_offset)) & 0xff);    
           }    
        break;    
       }    
       return a;    
   }    

   -(void) MachineOUT(uint8_t port, uint8_t value)    
   {    
       switch(port)    
       {    
           case 2:    
               shift_offset = value & 0x7;    
               break;    
           case 4:    
               shift0 = shift1;    
               shift1 = value;    
               break;    
       }    

   }    

Keyboard

We have to map the computer keyboard input to the machine to get it to respond. Most platforms have a way to get key down and key up events. The platform code for the buttons will be something like:

   if(PeekMessage(&msg,NULL,0,0,PM_REMOVE))    
   {    
    if (msg.message==WM_KEYDOWN )    
    {    
        if ( msg.wParam == VK_LEFT )    
            MachineKeyDown(LEFT);    
    }    
    else if (msg.message==WM_KEYUP )    
    {    
        if ( msg.wParam == VK_LEFT )    
            MachineKeyUp(LEFT);    
    }    
   }    

The machine code to glue the platform code to the emulator will be something like:


   MachineKeyDown(char key)    
   {    
    switch(key)    
    {    
    case LEFT:    
        port[1] |= 0x20;  //Set bit 5 of port 1    
        break;    
    case RIGHT:    
        port[1] |= 0x40;  //Set bit 6 of port 1    
        break;    
    /*....*/    
    }    
   }    

   PlatformKeyUp(char key)    
   {    
    switch(key)    
    {    
    case LEFT:    
        port[1] &= 0xDF //Clear bit 5 of port 1    
        break;    
    case RIGHT:    
        port[1] &= 0xBF //Clear bit 6 of port 1    
        break;    
    /*....*/    
    }    
   }    

You could combine the machine and platform code if you want - that is an implementation choice. I'll not make it because I'm going to port the machine to several different platforms.

← Prev: rest-of-the-machine   Next: interrupts →


Post questions or comments on Twitter @realemulator101, or if you find issues in the code, file them on the github repository.