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:
- Player 1
- Player 2
- Modem
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:
- Control port: sets the direction of each pin
- Data port: the values that end up on the pins
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):
Port | Control port | Data 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:
- Set bit 7 = 1 when writing to the port's control register
- Set bit 3 = 1 of VDP register
$8Bxx
(VDPREG_MODE3
) - Allow interrupts on the 68000
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 mimics the serial ports in old PCs (albeit at 5V), but it's also really slow which is why nothing else bothers with it.
Serial mode uses its own set of registers:
- Serial control port: turns on serial mode and configures how it works.
- RxData: where you read bytes you receive
- TxData: where you write bytes to send
These registers also have their own addresses (beware that they're ordered in a different way than the parallel mode registers!)
Port | Serial control | RxData | TxData |
---|---|---|---|
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:
- Bits 7-6: speed
00
: 4800 bps (480 bytes/sec)01
: 2400 bps (240 bytes/sec)10
: 1200 bps (120 bytes/sec)11
: 300 bps (30 bytes/sec)
- Bit 5: how TR (pin 9) works
0
: parallel mode (pin 5)1
: serial mode (RxData)
- Bit 4: how TL (pin 6) works
0
: parallel mode (pin 4)1
: serial mode (TxData)
- Bit 3: 1 to trigger external interrupt when receiving a byte
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_1200BPS|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.
You must read from the serial control register even if you're sure it's OK to send a new value, or otherwise the hardware may not send the correct data (this is not mentioned in the official documentation…)
; 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
control register 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.
You must read from the serial control register even if you're sure you received a value, or otherwise you may get whatever was received last time instead (again, this is not mentioned in the official documentation…)
; 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.