6 Button Controllers


On to the next chapter

I recently finished this little Sega Genesis demo game called Retail Clerk '89. It's not especially fun but I'm not sure that was ever my reason for making it. It lacks 6 button support simply because the game only needs 3 buttons. Really it only needed 2 if I knew what I was doing before I started on it. Still, the X/Y/Z buttons could just mimic their A/B/C counterparts. That would have been kind of neat.

After looking into it I found that supporting 6 buttons isn't as easy as it sounds. It's not rocket science but it's not just testing the controller input for a couple extra values. So that's all this article & demo will be about - reading all 6 (really 8) buttons on a Genesis controller. As an extra bonus we'll try to detect whether a 6 or 3 button controller is attached.

I have a secondary goal - determining whether any part of the Retail Clerk '89 code is reusable in other projects. In case I decide to do another full-blown Genesis game I'd like to have a library of code that solves common problems. Not an engine or a framework, just a random pile of minimally useful code. You could even say It's Definitely not a Genesis Adventure Framework. I guess that means the code for this little 6 button demo is a bit larger than it needs to be.

The first thing I learned from this demo is that I really didn't understand how Genesis controllers communicated with the base unit. I found some sample code that worked and called it a day. At the end of writing this demo that copy 'n paste code was largely replaced.

I never even stopped to think about what the 9 pins on the Genesis controller even mean. I've always been more of a software guy than a hardware guy. After reading several different sources, some with conflicting information, here's what I think the 9 pins on the Genesis controller port mean:

Sega Genesis controller pins

So there are 7 pins that transmit binary data. Since the base Genesis controller has 8 buttons that means Sega had to account for handling >7 buttons from the beginning. This is done by sending data out over cycles. The "TH" pin serves as the toggle here. Flipping it from low to high initiates another read cycle. There are 9 cycles supported but really only the first 7 are used. For a 3 button controller game only the first two cycles are needed. So you flip bit 7 to initiate another read cycle. After a short delay the controller flips back to cycle 0 so you better get what you need quickly.

There are many variations of this chart available, including from leaked Sega development manuals. I created one because I'm not very smart and needed a dumbed-down version to help me understand this:

Sega Genesis controller cycles

Yeah, I know, this is just a spreadsheet view of the previous image. You really just came here to see code I bet. Alright, let's get to it..

I'll skip all the stuff to create the layout, you can download the full source somewhere on this page.

Let's start with some constants we'll need:

CTRL_1_DATA=$00A10003

CTRL_1_CONTROL=$00A10009

MEM_CONTROL_1_6BUTTON=$FFFF000A

MEM_CONTROL_HELD=$FFFF000C

MEM_CONTROL_PRESSED=$FFFF000E

MEM_CONTROL_6_HELD=$FFFF0010

MEM_CONTROL_6_PRESSED=$FFFF0012

MEM_DEBUG_CYCLE1_INIT=$FFFF0020

MEM_DEBUG_CYCLE2_INIT=$FFFF0022

MEM_DEBUG_CYCLE3_INIT=$FFFF0024

MEM_DEBUG_CYCLE4_INIT=$FFFF0026

MEM_DEBUG_CYCLE5_INIT=$FFFF0028

MEM_DEBUG_CYCLE6_INIT=$FFFF002A

MEM_DEBUG_CYCLE7_INIT=$FFFF002C

MEM_DEBUG_CYCLE8_INIT=$FFFF002E

MEM_DEBUG_CYCLE9_INIT=$FFFF0030

MEM_DEBUG_CYCLE1_VBLANK=$FFFF0032

MEM_DEBUG_CYCLE2_VBLANK=$FFFF0034

MEM_DEBUG_CYCLE3_VBLANK=$FFFF0036

MEM_DEBUG_CYCLE4_VBLANK=$FFFF0038

MEM_DEBUG_CYCLE5_VBLANK=$FFFF003A

MEM_DEBUG_CYCLE6_VBLANK=$FFFF003C

MEM_DEBUG_CYCLE7_VBLANK=$FFFF003E

MEM_DEBUG_CYCLE8_VBLANK=$FFFF0040

MEM_DEBUG_CYCLE9_VBLANK=$FFFF0042

Right after the Genesis starts let's see what type of controller is connected. This is needed because if a 3 button controller is connected we should skip reading everything after cycle 2.

InitController:

 move.b #$40,(CTRL_1_CONTROL)

 lea CTRL_1_DATA,a0 ; load address to read controller 1 data

 ;---------------------------------

 ; set counter to 1 + TH high

 ;---------------------------------

 move.b #$40,(a0)

 nop ; bus synchronization

 nop ; bus synchronization

 move.b (a0),(MEM_DEBUG_CYCLE1_INIT)

 ;---------------------------------

 ; set counter to 2 + TH low

 ;---------------------------------

 move.b (a0),d0

 move.b #$00,(a0)

 nop ; bus synchronization

 nop ; bus synchronization

 move.b (a0),(MEM_DEBUG_CYCLE2_INIT)

 ;---------------------------------

 ; set counter to 3 + TH high

 ;---------------------------------

 move.b #$40,(a0)

 nop ; bus synchronization

 nop ; bus synchronization

 move.b (a0),(MEM_DEBUG_CYCLE3_INIT)

 ;---------------------------------

 ; set counter to 4 + TH low

 ;---------------------------------

 move.b #$00,(a0) ; set TH low

 nop ; bus synchronization

 nop ; bus synchronization

 move.b (a0),(MEM_DEBUG_CYCLE4_INIT)

 ;---------------------------------

 ; set counter to 5 + TH high

 ;--------------------------------- 

 move.b #$40,(a0) ; set TH high

 nop ; bus synchronization

 nop ; bus synchronization

 move.b (a0),(MEM_DEBUG_CYCLE5_INIT)

 ;---------------------------------

 ; set counter to 6 + TH low

 ; 6 button id is in counter 6

 ;--------------------------------- 

 move.b #$00,(a0) ; set TH low

 nop ; bus synchronization

 nop ; bus synchronization

 move.b (a0),d0 ; copy controller data to d0

 move.b d0,(MEM_DEBUG_CYCLE6_INIT)

 cmpi.b #%00110011,d0 ; 00110011 = 3 button controller

 beq.s .1

 move.b #%111111,(MEM_CONTROL_1_6BUTTON)

.1

 [and so on for 7-9]

And now here's the new code to read the joypad. This is executed during vblank. It now is a bit different than the original sample I found a few years ago.

ReadJoypad:

 ;---------------------------------

 ; set counter to 1 + TH high

 ;---------------------------------

 move.b #$40,(a0) ; set TH high

 nop ; bus synchronization

 nop ; bus synchronization

 move.b (a0),d0 ; get joypad data - C/B/Dpad

 move.b d0,(MEM_DEBUG_CYCLE1_VBLANK)

 andi.b #%00111111,d0 ; C/B/Dpad in low 6 bits

 ;---------------------------------

 ; set counter to 2 + TH low

 ;---------------------------------

 move.b #$00,(a0) ; set TH low

 nop ; bus synchronization

 nop ; bus synchronization

 move.b (a0),d1 ; get joypad data - Start/A

 move.b d1,(MEM_DEBUG_CYCLE2_VBLANK)

 lsl.b #2,d1 ; shift them so they are at the 2 highest bits

 andi.b #%11000000,d1 ; Start/A in high 2 bits - clear others

 or.b d1,d0 ; merge values from both registers

 not.b d0 ; flip bits so 0 means not pressed, and 1 means pressed

 move.b d0,d1 ; copy current buttons to d1

 move.b (MEM_CONTROL_HELD),d2 ; copy the last previously read buttons

 eor.b d2,d0 ; flip buttons being pressed now

 move.b d1,(MEM_CONTROL_HELD) ; store held buttons

 and.b d1,d0 ; AND with current buttons

 move.b d0,(MEM_CONTROL_PRESSED) ; store pressed buttons

 ; if this is a 3 button controller skip the remaining steps

 move.b (MEM_CONTROL_1_6BUTTON),d0 ; save the value

 tst.b d0 ; is this zero?

 beq.w ExitReadJoypad ; zero means 3 button controller

 ;---------------------------------

 ; set counter to 3 + TH high

 ;---------------------------------

 move.b #$40,(a0) ; set TH high

 nop ; bus synchronization

 nop ; bus synchronization

 move.b (a0),(MEM_DEBUG_CYCLE3_VBLANK)

 ;---------------------------------

 ; set counter to 4 + TH low

 ;--------------------------------- 

 move.b #$00,(a0) ; set TH low

 nop ; bus synchronization

 nop ; bus synchronization

 move.b (a0),(MEM_DEBUG_CYCLE4_VBLANK)

 ;---------------------------------

 ; set counter to 5 + TH high

 ;--------------------------------- 

 move.b #$40,(a0) ; set TH high

 nop ; bus synchronization

 nop ; bus synchronization

 move.b (a0),(MEM_DEBUG_CYCLE5_VBLANK)

 ;---------------------------------

 ; set counter to 6 + TH low

 ;--------------------------------- 

 move.b #$00,(a0) ; set TH low

 nop ; bus synchronization

 nop ; bus synchronization

 move.b (a0),(MEM_DEBUG_CYCLE6_VBLANK)

 ;---------------------------------

 ; set counter to 7 + TH high

 ;--------------------------------- 

 move.b #$40,(a0) ; set TH high

 nop ; bus synchronization

 nop ; bus synchronization

 move.b (a0),d0 ; get joypad data - x/y/z/mode

 move.b d0,(MEM_DEBUG_CYCLE7_VBLANK)

 not.b d0  ; flip bits so 0 means not pressed, and 1 means pressed

 and.b #%00001111,d0 ; x/y/z/mode are in lowest 4 bits

 move.b d0,d1 ; copy current buttons to d1

 move.b (MEM_CONTROL_6_HELD),d2 ; copy the last previously read buttons

 eor.b d2,d0 ; flip buttons being pressed now

 move.b d1,(MEM_CONTROL_6_HELD) ; store held buttons

 and.b d1,d0 ; AND with current buttons

 move.b d0,(MEM_CONTROL_6_PRESSED) ; store pressed buttons

 ;---------------------------------

 ; set counter to 8 + TH low

 ; just for demo purposes - not needed

 ;--------------------------------- 

 move.b #$00,(a0) ; set TH low

 nop ; bus synchronization

 nop ; bus synchronization

 move.b (a0),(MEM_DEBUG_CYCLE8_VBLANK)

 ;---------------------------------

 ; set counter to 9 + TH high

 ; just for demo purposes - not needed

 ;---------------------------------

 move.b #$40,(a0) ; set TH high

 nop ; bus synchronization

 nop ; bus synchronization

 move.b (a0),(MEM_DEBUG_CYCLE9_VBLANK)

 ;---------------------------------

 ; done reading controller

 ;---------------------------------

ExitReadJoypad:

 rts

Here's what the final product looks like:

6 button controller test

It's handy for verifying what exactly is sent during each cycle - see what I mean:

Pins - up

Pins - down

Pins - left

Pins - right

Pins - start

Pins - mode

Pins - A

Pins - B

Pins - C

Pins - X

Pins - Y

Pins - Z

Please note - the red boxes were added for illustrative purposes and don't appear in the demo ROM.

After testing this with various things, here's a chart I'm calling "why Sega Genesis programming will turn you into a crazy person":

Controller debug

The part that's really messing with my mind is how reading the controller during initialization (shortly after passing the Genesis "security" step) produces different results than reading it during vblank. Trust me, I tried to rule out that I was doing something wrong. I tried copying & pasting identical code into both places and got the same result.

Cycles 4,6,8 just flat-out produce different results for 6 button controllers at very-near-startup vs during normal game play. Since 3 button controllers don't behave this way it must be something different in the 6 button hardware. I know that the mode button must be pressed during startup to flip the controller into 3 button mode. It doesn't flip it every time it's pressed, I'm sure this confused people badly. I have to assume then that during this brief period the 6 button controller is in a different state.

There are two other important things this illustrates:

1) Gens is a fun emulator and all that stuff. It's fast and will run any crazy thing you throw at it. Anecdotally I've never built a test ROM it won't run. The tradeoff to this is it's not an especially accurate emulator.

2) The mode button can be used like a regular button but you risk having issues on clone systems. I get it. Some clone systems have hardwired 6 button controllers and need to be compatible with the small assortment of games that break with them. Rather than include a mode button, which might be confusing to non-hardcore-Genesis-people, they implemented a novel solution. They made the mode button constantly pressed. It's an interesting workaround. So games explicitly looking for the mode button would find it I guess? That's not really how the mode button is intended to work, it's meant to be detected by the controller hardware and not game software. How could a game that predates 6 button controllers be expected to look for it? There are no commercial games that use mode like a regular button so nothing breaks. I'm just not sure this makes anything work either.

What's Next?

I think next I'll try to decipher Team Player support. I own two of them so maybe I'll really lose my mind and try to figure out how 8-player support might work.

Download

Demo ROM - this turned out to be pretty handy for testing controller configuration on emulators

Source code - at least until I move it to Github