I/O ports

The Mega Drive has three "I/O ports", which refer to the ports where peripherals are plugged in (such as the controllers). This page goes over how to access the ports, while the details on how to use each peripheral will be in their respective page.

Note: for most uses the first two sections are enough, feel free to skip the rest until you run into a peripheral that actually needs that stuff.

What ports are there?

The Mega Drive has three I/O ports:

The first two ports are where the controllers go.

The modem port (also called EXT port) is only present in early consoles and is at the back. This port is "gender-flipped" (i.e. its shape goes the other way compared to the controllers), to ensure a wrong peripheral can't be connected there. Otherwise, it behaves pretty much the same way as the other two ports.

Basic usage

Normally, the 68000 manipulates the port pins directly. There are seven pins available which can be set to either input or output at will. Every port has two registers to handle this, each of which assigns one bit to every pin:

Writing to the control port sets the direction of every pin (with each of bit 6-0 belogning to a pin). Writing a 0 sets the pin to input (from peripheral), 1 sets the pin to output (to peripheral). What you write here depends on the peripheral (e.g. controllers need $40). Make sure that bit 7 = 0 unless you need to use external interrupts.

Writing to the data port changes the values of the pins that have been set as outputs (the rest are ignored), again in their respective bits. Reading back from the data port returns the current values of all pins (both input and output). Writing and reading this port is the main way to communicate with whatever is plugged into the console.

They're at these addresses, and in all case they're byte-sized (also note how each port address is two bytes apart):

Control and data port addresses
PortControl portData port
Player 1$A10009$A10003
Player 2$A1000B$A10005
Modem$A1000D$A10007

Convenient constants for use in 68000 assembly:

IoCtrl1:      equ $A10009  ; I/O control port 1P
IoCtrl2:      equ $A1000B  ; I/O control port 2P
IoCtrlExt:    equ $A1000D  ; I/O control port modem
IoData1:      equ $A10003  ; I/O data port 1P
IoData2:      equ $A10005  ; I/O data port 2P
IoDataExt:    equ $A10007  ; I/O data port modem

Example of how to set up them (as if configured for a controller):

    move.b  #$40, (IoCtrl1)
    move.b  #$40, (IoData1)
    move.b  #$40, (IoCtrl2)
    move.b  #$40, (IoData2)

External interrupt

The Mega Drive provides a so-called "external interrupt". This interrupt is used by lightguns to tell the console when it has detected the TV beam (so it can check its position). The interrupt happens when pin 6 is an input and it goes from high to low.

To enable the interrupt you must do all the following:

External interrupt is IRQ 2.

Serial mode

The I/O ports can operate in two modes. The one we normally use (controlling the pins directly) is "parallel mode". The other mode is "serial mode", and it's only really used by the modem. It gimmicks the serial ports in old PCs, but it's also really slow which is why nothing else bothers with it.

Serial mode uses its own set of registers:

These registers also have their own addresses (beware that they're ordered in a different way than the parallel mode registers!)

Serial mode port addresses
PortSerial controlRxDataTxData
Player 1$A10013$A10011$A1000F
Player 2$A10019$A10017$A10015
Modem$A1001F$A1001D$A1001B
IoSCtrl1:     equ $A10013  ; I/O serial control 1P
IoSCtrl2:     equ $A10019  ; I/O serial control 2P
IoSCtrlExt:   equ $A1001F  ; I/O serial control modem
IoRxData1:    equ $A10011  ; I/O RxData 1P
IoRxData2:    equ $A10017  ; I/O RxData 2P
IoRxDataExt:  equ $A1001D  ; I/O RxData modem
IoTxData1:    equ $A1000F  ; I/O TxData 1P
IoTxData2:    equ $A10015  ; I/O TxData 2P
IoTxDataExt:  equ $A1001B  ; I/O TxData modem

Setting up serial mode

In order to use serial mode, first you need to write to the serial control register in order to enable it and configure how it should work. The byte you have to write must be a combination of:

Turning on serial mode is a matter of setting 5-4 to 11, while returning to parallel mode is done by making them 00 (setting them to different values is possible, but not that useful).

The following constants can help with setting up serial mode:

SERIAL_4800BPS:   equ %00<<6    ; 4800bps speed
SERIAL_2400BPS:   equ %01<<6    ; 2400bps speed
SERIAL_1200BPS:   equ %10<<6    ; 1200bps speed
SERIAL_300BPS:    equ %11<<6    ; 300bps speed

SERIAL_DISABLE:   equ %00<<4    ; Use parallel mode
SERIAL_ENABLE:    equ %11<<4    ; Use serial mode

SERIAL_NOINT:     equ %0<<3     ; No external interrupt
SERIAL_INTOK:     equ %1<<3     ; Use external interrupt

Then pick one of each group, OR them and write them to the serial control register. For example, the following would set up the modem port for use with the modem (with interrupts when receiving bytes):

    move.b  #SERIAL_2400BPS|SERIAL_ENABLE|SERIAL_INTOK, (IoSCtrlExt)

And if for whatever reason you need to go back to parallel mode:

    move.b  #SERIAL_DISABLE, (IoSCtrlExt)

Sending bytes in serial mode

To send a byte over a serial mode port, first you need to poll that the port is ready to send more bytes. You do this by reading the serial control register and waiting until bit 0 becomes 0. Then write the byte to the TxData register.

; d0.b = byte to send
; a0.l = pointer to IoSCtrl*

SendByte:
@Wait:
    btst    #0, (a0)
    bne.s   @Wait
    move.b  d0, -4(a0)
    rts

Receiving bytes in serial mode

To check if you received a byte, you need to read back from the serial port and check bits 2 and 1. If bit 2 = 1, then there was a transmission error. If bit 1 = 0, then there isn't a byte ready yet. In other words, if these two bits are 01, you received a byte. Read the byte from the RxData port.

; a0.l = pointer to IoSCtrl*
; returns d0.w = $00xx if got byte
;                $FFFF if no byte

ReceiveByte:
    ; Check if a byte is available
    btst    #2, (a0)
    bne.s   @NoByte   ; Error?
    btst    #1, (a0)
    beq.s   @NoByte   ; Ready?
    
    ; Got a byte!
@HasByte:
    moveq   #0, d0
    move.b  -2(a0), d0
    rts
    
    ; No byte received
@NoByte:
    moveq   #-1, d0
    rts

If you set bit 3 = 1 when configuring serial mode earlier, you will receive an external interrupt whenever a byte is ready (make sure to enable the interrupt on the VDP too!). This way you don't have to be constantly polling to see if there's a new byte, albeit checking bits 2-1 once you got the interrupt is still a good idea.