Sega Megadrive – 7: Gamepad Input and the Game Loop

Since I plan on the next few articles to be about plane scrolling and sprite animation, I’d like to experiment with a few of the prerequisites first – basic pad input, timing, and the main game loop. I already have a sprite on screen, plus some subroutines for setting its X and Y coords, so I’ll aim to move it around the screen using the D-pad at various speeds. Hopefully this won’t take long.

Timing seems pretty awkward; in modern games programming I’m used to recording a frame’s delta time and using that to determine how far a character should move in one frame. This would require some extra maths which could bog the 68k down, and since the VDP has a fixed refresh rate the common technique seems to be to use hard-coded speed values, but wait for vsync at the end of the game loop. It sounds very hacky to me, since I was taught to use time deltas to achieve FPS independence all through my programming career, but let’s see how it goes.

Polling gamepad input

Gamepads are interacted with through the port control and port data addresses. These are generic 9-pin serial ports, used to connect pads, joysticks, light guns, even modems, but for simplicity I’ll assume we only have gamepads connected for now. I’ll also assume we’re not interested in port C, which is the EXT port on the back of the American Genesis model 1.

To read a pad’s state, we need to read from its data port – there’s one per port, 0x00A10003 and 0x00A10005. Only a byte at a time can be read from these addresses, and to tell the port whether we want the upper or lower byte returned we have to write bit 7 to it first. A typical read for all of the buttons goes something like this:

  • Read one byte from data port to a register (contains 00SA0000)
  • Shift the data to the upper byte
  • Write bit 7 ON to the port
  • Read one byte again to the register (contains 00CBRLDU)
  • Write bit 7 OFF to the port to put it back to normal
This should be pretty simple in code:
   ; d0 (w) - Return result
   move.b pad_data_a, d0    ; Read upper byte from data port
   rol.w  #0x8, d0          ; Move to upper byte of d0
   move.b #0x40, pad_data_a ; Write bit 7 to data port
   move.b pad_data_a, d0    ; Read lower byte from data port
   move.b #0x00, pad_data_a ; Put data port back to normal

After calling the subroutine, d0 should contain a word with bits representing Up, Down, Left, Right, A, B, C and Start, in this format: 00SA0000 00CBRLDU. I’ve added some defines to make it easy to BTST the word to check if a particular button is being held down:
pad_button_up    equ 0x0
pad_button_down  equ 0x1
pad_button_left  equ 0x2
pad_button_right equ 0x3
pad_button_a     equ 0xC
pad_button_b     equ 0x4
pad_button_c     equ 0x5
pad_button_start equ 0xD

Getting the state of a 6 button pad is a little more complex – it requires bit 7 to be set ON, and then OFF, and then ON again to retrieve the 3rd byte of data. For the moment I’ll leave it out, I don’t own any 6 button controllers for testing anyway.

Waiting for vertical blanking

In order to update a sprite’s position without causing any tearing or flickering, it’s best to modify them during vertical blanking. This is the period during which the electron beam has reached the bottom-right hand side of the screen and is in the process of moving back up to the top-left. To test for this state, we need to poll the VDP’s status register. This is as simple as reading a word from the VDP control port. The word’s bits represent the following:

  • 0: Region mode: OFF=NTSC, ON=PAL
  • 1: ON during a DMA operation
  • 2: ON during horizontal blanking
  • 3: ON during vertical blanking
  • 4: ON during odd frame in interlaced mode
  • 5: ON whilst two sprites have non-transparent pixels colliding
  • 6: ON whilst too many sprites are on a single scanline
  • 7: ON during a vertical interrupt
  • 8: ON if FIFO is full
  • 9: ON if FIFO is empty
  • 10-15: Unused

We’re interested in bit 4, which will get turned ON whilst the screen is being blanked to perform a vertical retrace, and OFF whilst the screen is active and drawing:

   move.w vdp_control, d0 ; Move VDP status word to d0
   andi.w #0x0008, d0     ; AND with bit 4 (vblank), result in status register
   bne    WaitVBlankStart ; Branch if not equal (to zero)

   move.w vdp_control, d0 ; Move VDP status word to d0
   andi.w #0x0008, d0     ; AND with bit 4 (vblank), result in status register
   beq    WaitVBlankEnd   ; Branch if equal (to zero)

Waiting for the vertical blanking has a second advantage – it happens once every 50th (PAL) or 60th (NTSC) of a second, so it forces our game loop to run at a maximum of 50 or 60 FPS.

Putting it all together

I’m assuming the whole idea is to ensure that the game code is fast enough to execute inside one whole frame, before the VBlank occurs, to keep it running at 24 frames per second. If it oversteps the mark, it’ll have to wait until the next VBlank which will reduce the framerate to 12. This all sounds awkward to me, but let’s see how it pans out when I make a start on the actual game.

Building on the code from the last article, I can now write a game loop which will check the gamepad data, and set the sprite’s X and Y coordinates during vertical blanking, and whilst maintaining 24 frames per second. I’ve also added a check for the A button, which will increase the speed of the sprite’s movement:

 move.l #0x80, d4 ; Store X pos in d4
 move.l #0x80, d5 ; Store Y pos in d5

 ; ************************************
 ; Main game loop
 ; ************************************

 ; ************************************
 ; Read gamepad input
 ; ************************************
 jsr ReadPad1 ; Read pad 1 state, result in d0

 move.l #0x1, d6              ; Default sprite move speed in d6

 btst   #pad_button_a, d0     ; Check A button
 bne    @NoA                  ; Branch if button off
 move.l #0x2, d6              ; Double sprite move speed

 btst   #pad_button_right, d0 ; Check right button
 bne    @NoRight              ; Branch if button off
 add.w  d6, d4                ; Increment sprite X pos by move speed

 btst   #pad_button_left, d0  ; Check left button
 bne    @NoLeft               ; Branch if button off
 sub.w  d6, d4                ; Decrement sprite X pos by move speed

 btst   #pad_button_down, d0  ; Check down button
 bne    @NoDown               ; Branch if button off
 add.w  d6, d5                ; Increment sprite Y pos by move speed

 btst   #pad_button_up, d0    ; Check up button
 bne    @NoUp                 ; Branch if button off
 sub.w  d6, d5                ; Decrement sprite Y pos by move speed

 ; ************************************
 ; Update sprites during vblank
 ; ************************************

 jsr    WaitVBlankStart ; Wait for start of vblank

 move.w #0x0, d0        ; Sprite ID
 move.w d4, d1          ; X coord
 jsr    SetSpritePosX   ; Set X coord
 move.w d5, d1          ; Y coord
 jsr    SetSpritePosY   ; Set Y coord

 jsr    WaitVBlankEnd   ; Wait for end of vblank

 jmp    GameLoop        ; Back to the top

More timing

I’ve been experimenting with some code which delays for a set number of frames, it’s currently of no use to me but along the way it’s forced me to take a more detailed look at the h/v-sync interrupts and the 68000 status register, so I’ll share my findings.

First, a correction. My original code for VDP registers 1 and 2 make the following claims:

dc.b 0x20 ; 0: Horiz. interrupt on, plus bit 2 (unknown, but docs say it needs to be on)
dc.b 0x74 ; 1: Vert. interrupt on, display on, DMA on, V28 mode (28 cells vertically), + bit 2

The values aren’t very useful, and the comments aren’t strictly true. I’ve had a good read of a document (in references) laying out each bit of each register and the following makes better sense:

dc.b 0x14 ; 0: Horiz. interrupt on, display on
dc.b 0x74 ; 1: Vert. interrupt on, screen blank off, DMA on, V28 mode (40 cells vertically), Genesis mode on

The mysterious “bit 2” of the first two registers are actually compatibility modes for the SEGA Master System. Bit 2 of register 1 OFF sets 8 colours per palette, and bit 2 of register 2 OFF puts the VDP in SMS display mode. The first register needed fixing up to turn on horizontal sync interrupts.

The horizontal and vertical sync interrupts are jumped to each time the proton beam reaches the right-hand side of the screen, and when it reaches the bottom-right hand corner of the screen. As far as I can find, the horizontal interrupt is the most frequently occuring event we can monitor. By reserving an integer’s worth of RAM, we can increment a counter every time it fires, and use that as a system tick count. Surprisingly, this is the first time I’ve even used main memory – everything I’ve done so far transfers data straight from cartridge ROM into the VDP’s arena. I’ll need a memory map:

hblank_counter        equ 0x00FF0000  ; Start of main RAM
vblank_counter        equ 0x00FF0004

I’ll start off simple, but perhaps later I could come up with some sort of macro to allow me to specify how much to allocate, and the address could be incremented automatically – like a simplified version of the art asset size/address defines.

The interrupts have already been defined in the init code, so we just need to put them to good use:

   addi.l #0x1, hblank_counter    ; Increment hinterrupt counter

   addi.l #0x1, vblank_counter    ; Increment vinterrupt counter

ADDI is one of the rare opcodes that can operate directly on a memory address, without having to load the value into a register first. This is a good thing, since the work done inside the interrupts must be absolutely minimal, we have very few clock cycles available before the proton beam has finished resetting. Next, interrupts must be enabled via the status register. In my original init code, I initialised this register to 0x2700 as per some sample code, with little thought as to what it was up to. I’ve found some information about its bits:

  • 0 – Trace exception
  • 1 – Unused
  • 2 – Supervisor mode (always enable)
  • 3 – Unused
  • 4 – Unused
  • 5 – Interrupt level (zero for all interrupts enabled)
  • 6 – Interrupt level
  • 7 – Interrupt level
  • 8 – Unused
  • 9 – Unused
  • 10 – Unused
  • 11 – CCR Extend
  • 12 – CCR Negative
  • 13 – CCR Zero
  • 14 – CCR Overflow
  • 15 – CCR Carry

I’ve encountered Supervisor Mode before, on the Atari ST. It allows non-user-mode operations to be called (OS traps and such), but I’m not sure what functionality is prohibited if it were turned of on the Megadrive. Bottom line, it needs to be ON. I also need to ensure that the three interrupt level bits are OFF – these determine the lowest interrupt level that is allowed to fire, and at the moment I don’t know which interrupts qualify for what level so I’ve enabled them all. With this in mind, I’ve corrected the init code to read:

; Init status register (no trace, supervisor mode, all interrupt levels enabled, clear condition code bits)
move #0x2000, sr

Now the h/v-sync interrupts should fire periodically, and we can fashion some sort of time delay out of them:

   ; d0 - Number of frames to wait

   move.l  vblank_counter, d1 ; Get start vblank count

   move.l  vblank_counter, d2 ; Get end vblank count
   subx.l  d1, d2             ; Calc delta, result in d2
   cmp.l   d0, d2             ; Compare with num frames
   bge     @End               ; Branch to end if greater or equal to num frames
   jmp     @Wait              ; Try again


I’ll probably use it to add delays between the startup game states (startup logo to main menu to first level) since it’s pretty useless for anything in the main game loop. Even if I don’t use it, I learned something along the way.


Source code

Assemble with:

asm68k.exe /p spritetest.asm,spritetest.bin


11 thoughts on “Sega Megadrive – 7: Gamepad Input and the Game Loop

  1. Very nice too see an update. I was worried we might not get anymore after such a long time, but I’ve very happy to see this.

    I’ve just started with the MD after think about it for ages and some of this info is really helpful.

  2. *FINALLY* got input to respond. It seems that waiting for two cycles (i.e. two NOP-s) give the hardware the enough time to respond. As things stand:

    move.b #$40, $00a10003;–Set bit 7 (hex $40) in controller 1 data port. This returns (x1CBRLDU), which you don’t really need to read, since it repeats
    nop;——————————Wait for two cycles
    move.b #$00,$00a10003;—-Move $00 to controller port 1 to read (x0SA00DU)
    move.b $00a10003,d0;——-Copy the results to d0
    rol.w #$08,d0;——————ROtate Left by 08 bits
    move.b #$40,$00a10003;—-Set bit 7 again to read (x1CBRLDU)
    move.b $00a10003,d0;——–Store the results to d0
    move.b #$00,$00a10003;—–Move $00 to controller port for (x0SA0000), which you can ignore, again
    move.b #$40,$00a10003;—–Set bit 7 (hex $40) for (x1CBMXYZ)
    move.b $00a10003,d1;———Store the results to d1
    move.b #$00,$00a10003;——Set bit 7 to $00 in controller 1 data port to reset things (x0SA1111)….?

    With a buttload of thanks to you and Charles MacDonald

    • Excellent! I had no idea this was a problem, though. Are you running on an emulator or real hardware?

      I’m still trying to get hold of the last piece for my devkit, so I’m afraid none of my samples have been tested on a real Megadrive yet 😦

      Do you mind if I use your findings to update my article? I’ll see if I can dig into why it’s necessary first, perhaps we’re both missing something important.

      • Thank you.

        Feel free to use the code I posted, though it’s not exactly *MY* findings. I just looked through forums & tutorials online. 😀

        My best guess is that in order to be backwards compatible with the Master System (which had a Z80 running @ 3.58 MHz), the processor(s) in the control pad needed to be slower than the 68000 (@7.67 MHz).

        Unfortunately, I don’t have hardware to test my code on. 😦

      • …in fact… the 68000 (7.67MHz) is *a little more than* twice as fast as the Z80 (3.58MHz X 2 = 7.16MHz). This might explain the required waiting period of two NOP-s.

  3. Hello! Awesome work.

    Hope you don’t mind if I use part of your vSync code in my project, credited of course. If there’s any problems, just tell me and I’ll remove it.


Leave a Reply to bigevilcorp Cancel reply

Please log in using one of these methods to post your comment: Logo

You are commenting using your account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s