Namco 51xx

In Pole Position the Namco 51xx custom chip appears in the Option Switch Input and I/O Interface section. Here is the portion of the schematic redrawn.

Pole Position Namco 51xx

The ID bits are a byte-wide bus coordinated by a Namco 06xx chip, the subject of a future blog post. Here the main observation is that these signals, and the associated handshaking, are mapped to bus transactions of Pole Position's Z80 CPU.

Chip Operation

We know the Namco 51xx is a Fujitsu MB8843 MCU with a mask ROM. Thanks to Guru, we actually have the mask ROM contents, so we can analyze the function of the chip.

Mask ROM Analysis

The MCU's internal timer/counter is driven by VBLANK, and therefore advances at 60 Hz. The main processing loop polls this counter, and whenever it advances, it performs a polling sweep.

All other events are driven by the MCU's IRQ input, which is pulsed when a 4-bit read/write command is applied by the Z80 at port K. Any command with the MSB set causes an 8-bit value to be output at port O. Each 8-bit value is a pair of nibbles from an internal circular buffer of depth eight. The Z80 polls these values, also based on VBLANK.

There appears to be three 51xx operational modes, here called: IDLE (0), INIT (1), and ACTIVE (2). These modes determine the eight-nibble output buffer mapping. We note that the last two nibbles are never used and always contain all bits set.

Mode 0 1 2 3 4 5 6 7
INIT R[11:8] R[15:12] R[3:0] R[7:4] 0xf 0xf 0xf 0xf
IDLE CRED ones CRED tens x x x x 0xf 0xf
ACTIVE CRED ones CRED tens R[3:0]* SELECT R[7:4]* SHIFTER 0xf 0xf
(TEST) 0xb 0xb x x x x 0xf 0xf

Note that the TEST switch is only recognized when IDLE or ACTIVE, and the TEST mode only lasts for one VBLANK, afterwhich transitions to INIT mode.

The * in the preceding table indicates the nibble is conditionally subject to a lookup table mapping. The enable for this mapping is determined by a command from the Z80.

Credit Score

Credits are maintained as Binary Coded Decimal and saturate at 99. TEST mode is indicated by overloading this with the value 0xb (11) in the two digits.

Coin and credit counts are managed entirely within the 51xx MCU. When coins are detected, any partial credits are accrued until they reach a credit increment. Mulitple-credits per increment is also handled, if configured. The configuration values to apply to coin and credit counting are written to the MCU by the Z80.

The START1 signal is also handled internally. Evidently a START2 signal is also supported, but this is not connected in Pole Position. When a START is detected, the credits are decremented and the operational mode switches to ACTIVE. Presumably the Z80 detects this situation by output nibbles 4 and 5 adopting a value of something other than 0xf.

Electromechanical coin counters are pulsed in conjunction with VBLANK count mod 16. Any pulses for COIN1 apply on VBLANK count 4, and COIN2 on VBLANK count 12. Each pulse therefore asserts for one VBLANK time, and the two counters are presumably distributed to minimize instantaneous current.

Miscellaneous

  • The 51xx outputs nibbles 0x0 and 0xf to the MCU serial port at various times. This was not analyzed as the serial port is not connected in Pole Position.
  • When the Z80 reads a byte from the 51xx, ID[2:0] are effectively don't-care. The pattern of those bits, however, appear to change with each read, and could be futher analyzed.
  • It appears a good portion of the 51xx RAM is not used.

Pseudocode

# look-up table
NIBBLE_TABLE = [0xf, 0xe, 0xd, 0x5, 0xc, 0x9, 0x7, 0x6,
                0xb, 0x3, 0xa, 0x4, 0x1, 0x2, 0x0, 0x8]

def onReset( ):
  # all zero-inits here implied by clearing entire RAM space to zero
  CREDIT_COIN_INFO = [0]*4 # [0] = credits per coin2
                           # [1] = coins per credit2
                           # [2] = credits per coin1
                           # [3] = coins per credit1

  OUT_NIBBLES   = [0, 0, 0, 0, 0, 0, 0, 0]
  CREDIT_DIGITS = [0, 0] # low, high in BCD

  PARTIAL_CREDIT_COIN1 = 0
  PARTIAL_CREDIT_COIN2 = 0

  UNLOGGED_COIN1 = 0
  UNLOGGED_COIN2 = 0

  R3_DEBOUNCE   = [0, 0, 0]
  R3_STATE      = 0
  R2_DEBOUNCE   = [0, 0, 0]
  R2_STATE      = 0

  PART_CREDIT_COIN1 = 0
  PART_CREDIT_COIN2 = 0

  OUT_INDEX = 0
  LAST_TLA  = 0

  DIPSW_CLEAR = False
  ACTIVE_CREDITS = 0

  # non-zero inits after RAM cleared
  COIN_LOGGER_LINES = 0xc # active-low

  R2_DEBOUNCE[2] = 0xf
  R2_STATE = 0xf

  OPER_MODE = 1 # "init"

def accrue_coin1( ):
  PARTIAL_CREDIT_COIN1 += 1
  if PARTIAL_CREDIT_COIN1 == CREDIT_COIN_INFO[3]:
    PARTIAL_CREDIT_COIN1 = 0
    CREDIT_DIGITS[0] += CREDIT_COIN_INFO[2]
    if CREDIT_DIGITS[0] > 9:
      CREDIT_DIGITS[0] -= 9
      CREDIT_DIGITS[1] += 1
  UNLOGGED_COIN1 += 1

def accrue_coin2( ):
  PARTIAL_CREDIT_COIN2 += 1
  if PARTIAL_CREDIT_COIN2 == CREDIT_COIN_INFO[1]:
    PARTIAL_CREDIT_COIN2 = 0
    CREDIT_DIGITS[0] += CREDIT_COIN_INFO[0]
    if CREDIT_DIGITS[0] > 9:
      CREDIT_DIGITS[0] -= 9
      CREDIT_DIGITS[1] += 1
  UNLOGGED_COIN2 += 1

def on_service( ):
  # increments credits without affecting coin tracking variables or loggers
  CREDIT_DIGITS[0] += CREDIT_COIN_INFO[0]
  if CREDIT_DIGITS[0] > 9:
    CREDIT_DIGITS[0] -= 9
    CREDIT_DIGITS[1] += 1

def cache_credits( ):
  OUT_NIBBLES[0] = CREDIT_DIGITS[0]
  OUT_NIBBLES[1] = CREDIT_DIGITS[1]

def onVBLANK( ):
  LAST_TLA = read_tla( )
  if OPER_MODE == 1: # "init"
    OUT_NIBBLES[0] = read_portR(2) # button/switch inputs: START1, SHIFTER, SELECT
    OUT_NIBBLES[1] = read_portR(3) # button/switch inputs: TEST, SERVICE, COIN2, COIN1
    OUT_NIBBLES[2] = read_portR(0) # DIP switches at 9L: LSN
    OUT_NIBBLES[3] = read_portR(1) # DIP switches at 9L: MSN
    OUT_NIBBLES[4] = 0xf
    OUT_NIBBLES[5] = 0xf
    OUT_NIBBLES[6] = 0xf
    OUT_NIBBLES[7] = 0xf
  else:
    r3val = read_portR(3)
    if r3val & 8: # TEST bit
      CREDIT_DIGITS[0] = 0
      CREDIT_DIGITS[1] = 0
      PART_CREDIT_COIN1 = 0
      PART_CREDIT_COIN2 = 0
      OUT_NIBBLES[0] = 0xb
      OUT_NIBBLES[1] = 0xb
      OPER_MODE = 1 # "init"
      write_portP(0xf)
    else:
      if CREDIT_DIGITS[1] < 10:
        R3_DEBOUNCE[2] = R3_DEBOUNCE[1]
        R3_DEBOUNCE[1] = R3_DEBOUNCE[0]
        R3_DEBOUNCE[0] = r3val
        R3_STATE = ~R3_DEBOUNCE[2] | R3_DEBOUNCE[1] | R3_DEBOUNCE[0]
        if R3_STATE & 1: # COIN1
          accrue_coin1( )
        if R3_STATE & 2: # COIN2
          accrue_coin2( )
        if R3_STATE & 4: # SERVICE
          on_service( )
      cache_credits( )
      r2val = get_portR(2)
      R2_DEBOUNCE[0] = R2_DEBOUNCE[1]
      R2_DEBOUNCE[1] = R2_DEBOUNCE[2]
      R2_DEBOUNCE[2] = r2val
      R2_STATE = ~R2_DEBOUNCE[0] | R2_DEBOUNCE[1] | R2_DEBOUNCE[2]

      if OPER_MODE == 0: "idle"
        if (ACTIVE_CREDITS == 0) and ((CREDIT_DIGITS[1] != 0) or (CREDIT_DIGITS[0] != 0)):
          tmp = 0
          if (R2_STATE & 4): # START1
            tmp = 1
          elif (R2_STATE & 8): # START2 (not implemented)
            tmp = 2
          if tmp:
              CREDIT_DIGITS[0] -= tmp
            if CREDIT_DIGITS[0] < 0:
              CREDIT_DIGITS[0] += 10
              CREDIT_DIGITS[1] -= 1
            cache_credits( )
            OPER_MODE = 2 # "active"
            PART_CREDIT_COIN1 = PART_CREDIT_COIN2 = 0
            if RAMx13 == 0:
              CREDIT_DIGITS[0] = 9
              CREDIT_DIGITS[1] = 10

      if OPER_MODE != 0 # implies OPER_MODE==2 aka "active"
        OUT_NIBBLES[3] = (R2_STATE & 1) | ((R2_DEBOUNCE[2] & 1) << 1) # SELECT
        OUT_NIBBLES[5] = ((R2_STATE & 2) >> 1) | (R2_DEBOUNCE[2] & 2) # SHIFTER
        if DIPSW_CLEAR:
          OUT_NIBBLES[2] = read_portR(0)
          OUT_NIBBLES[4] = read_portR(1)
        else:
          OUT_NIBBLES[2] = NIBBLE_TABLE[read_portR(0)]
          OUT_NIBBLES[4] = NIBBLE_TABLE[read_portR(1)]

      # 0x120
      if UNLOGGED_COIN1:
        if (read_tla( ) == 4) and not (COIN_LOGGER_LINES & 8):
          COIN_LOGGER_LINES |= 8 # complete the logger advance COIN1
          UNLOGGED_COIN1 -= 1
        else:
          COIN_LOGGER_LINES &= ~8
      if UNLOGGED_COIN2:
        if (read_tla( ) == 12) and not (COIN_LOGGER_LINES & 4):
          COIN_LOGGER_LINES |= 4 # complete the logger advance COIN2
          UNLOGGED_COIN2 -= 1
        else:
          COIN_LOGGER_LINES &= ~4

      write_portP(COIN_LOGGER_LINES)

def onIRQ( ):
  k = read_portK( )
  if k == 1:
    done = False
    i = 3 
    while not done:
      # wait for IRQ deassert and reassert
      a = read_portK( )
      if a == 0:
        RAM[0x1a] = 0
      CREDIT_COIN_INFO[i--] = a
      done = (i < 0)
  elif k == 2:
    OPER_MODE = 0 # "idle"
  elif k == 3:
    DIPSW_CLEAR = True
  elif k == 4:
    DIPSW_CLEAR = False
  elif k == 5:
    OPER_MODE = 1 # "init"
  elif k <= 7:
    pass # do nothing
  else: # k >= 8
    write_portO(0, OUT_NIBBLES[OUT_INDEX++])
    write_portO(1, OUT_NIBBLES[OUT_INDEX++])
    if OUT_INDEX >= 8:
      OUT_INDEX = 0

def main( ):
  onReset( )
  onVBLANK( )

  while True:
    tla = read_tla( )
    if LAST_TLA != tla:
      onVBLANK( )

social