Tuesday, January 20, 2015

ColecoVision Driving Module

Introduction

    ColecoVision became one of the major game consoles when released in 1982 and sold over two million units, mainly in North America. However, sales and popularity of the console dropped quickly in the big video game crash of 1983 and was withdrawn from the video game market in 1985. ColecoVision carried titles such as Donkey Kong, Smurf, and Zaxxon.

    The console was powered by an 8 bit Z80 processor running at 3.58MHz Z80 and had a whopping 1 kB RAM. The video processor had 16kB video ram providing 256 x 192 pixel resolution in 16 predefined colors. The sound was powered by a three channel square wave sound generator with an additional noise channel. The games were released on cartridges with up to 32 kB ROM.

    One of the more interesting aspects of the console was the hardware expandability through the Expansion Module interface. The first expansion module made the ColecoVision compatible with Atari 2600, enabling the biggest game library at the time. The second expansion module was a driving controller with a steering wheel and gas pedal, and other expansion modules followed.

    This article focuses on the driving controller, and the challenges using it in modern homebrew development projects. The steering wheel was first introduced with the game Turbo, and it was later used in a handful of games. Typically for the games in the 80’s is that these where one person games, and we’ll go into details on why and what actually can be done to allow two steering wheels to be used at the same time in a game.


Motivation

    The main reason for me learning about and solving the problems with the driving module came when me and Vincent van Dam decided to develop a new racing game for the ColecoVision with graphics help from Luc Miron. Our goal was to do something never done before on ColecoVision; a variation of the microcars game, including multi directional scrollers, real physics engines to model the cars, advanced music players, and of course support for the driving module allowing two players to compete. Many of the features we wanted had never been done on ColecoVision, so it was a quite ambitious goal. The hardware constraints of the console made the development hard and the game took over 2,000 hours to develop. The finished game was a game published by CollectorVision in 2011:



Driving Module Overview

    The steering wheel has a 2 bit rotary encoder with two concentric rings with contact surfaces and openings offset by 90 degrees as the picture below illustrates.



    The rings move as a person turns the steering wheel, metal plates at fixed position generates an ‘on’ signal when they touch the metal surface of the rings, and an ‘off’ signal otherwise. These signals can be read by software on I/O port $FC for the first steering wheel and I/O port $FF for the second steering wheel, allowing the software to read the angle of the rotary wheel as a 2 bit value:

Contact 1 Contact 2
    Angle
Off Off
    0° to 90°
Off On
    90° to 180°
On On
    180° to 270°
On Off
    270° to 360°


On the ColecoVision, contact 1 is mapped to bit 4 and contact 2 to bit 5.

Driving games typically want to know how much the user moved the steering wheel. A convenient measurement is a single integer value (called steeringCunter in the algorithms below) where a negative value means steering to the left, and a positive value steering to the right. A small value means steering a little bit and a large value steering a lot. We’ll discuss two approaches, one pure polling based, and one that is using interrupts that calculates the steeringCunter value.


Polling

    A straightforward way of maintaining the steeringCounter value is to initialize it to 0, then read the values of contact 1 and contact 2 at a periodic interval. The contact bits are Gray coded, which means that it is a binary counter where two adjacent codes differ by only one bit position. The algorithm for a polling function increases or decreases the steeringCounter value by one depending on the previous values of the contact bits and which contact changed since last read. If both contact bits has changed we don’t do anything as we can’t say if the wheel turned left or right.

    The code snippet below illustrates how the polling method could be implemented in a fairly efficient way utilizing the Gray coded binary counter.

  #define CONTACT_1 0x10
  #define CONTACT_2 0x20
  #define CONTACT_MASK (CONTACT_1 | CONTACT_2)

  void pollSteeringWheel() {
  int wheelCounter = 0;
  unsigned char lastState = 0;

  void pollSteeringWheel() {
    /* Read the current state of the steering wheel contacts */
    /* and compute the bits that changed since last call. */

    unsigned char newState = readIoPort(0xFC) & CONTACT_MASK;
    unsigned char bitsChanged = lastState ^ newState;
    /* Mirror bitsChanged if the high bit is set in lastState
    /* to allow a single test for increment and decrement. */

    if (lastState & CONTACT_2) {
      bitsChanged ^= CONTACT_MASK;
    }

    /* Increase or decrease wheelCounter if only one */
    /* bit has changed. */

    if (bitsChanged == CONTACT_1) {
      wheelCounter++;
    }
    if (bitsChanged == CONTACT_2) {
      wheelCounter--;
    }
    lastState = newState;
  }


    The code can easily be extended to support two steering wheel controllers. Just duplicate the code for the second steering wheel, reading from port $FF instead of port $FC.

    The drawback of using a polling method like the one above, is that it needs to be called at a frequency that is higher than the rotary wheel moves 90 degrees as the user moves the steering wheel. Although the code is not complicated it uses a fair bit of CPU on a ColecoVision system, and interleaving calls to the polling method is sometimes not that easy.


Interrupt Driven Approach

    To allow software to register steering wheel movements in real time, the first contact is connected to a non maskable interrupt, which means that when the first contact changes from ‘off’ to ‘on’ the cpu is interrupted and the software can read the bits at that time. We can use the interrupt and the state of the rotary encoder to implement a counter that measures how much the wheel has been rotated to the left or to the right.

    The first contact can turn from ‘off’ to ‘on’ in two places, either if the wheel is turned clockwise from ‘off’ to ‘on’ or counter clockwise from ‘off’ to ‘on’. By looking at the state of the second contact we can see if the wheel was turned clockwise or counter clockwise. The second contact is ‘on’ when the transition from ‘off’ to ‘on’ occurs on the first contact when we rotate clockwise. Similarly, the second contact is ‘off’ if we rotate counter clockwise.

    There are however two quite big problems with using interrupts on the steering wheel controllers.
  1. Both steering wheels share the same nonmaskable interrupt, so when supporting two steering wheels, it is not possible to tell which of them that caused the interrupt to be fired.
  2. The mechanical contacts can cause jitter if the steering wheel is positioned just at the edge of the connecting metal plates, causing multiple interrupts to be fired even though the wheel is not spinning.

Algorithm using Interrupts

    To address both these issues, we’ll implement a clock pulse for each steering wheel that is set in the interrupt service routine, and reset in a background polling task. When the interrupt is triggered, the program checks if the logical clock pulse. If the clock pulse is high, no action is taken. If the clock pulse is low, we know that the steering wheel has moved, and we update the counter based on the state of the second contact. The pseudo code below implements the algorithm for one of the steering wheel controller. The real code linked at the end of the article duplicates the code for the second controller both in the polling method and the interrupt service routine.

  #define CONTACT_1 0x10
  #define CONTACT_2 0x20

  unsigned char clockPulse = CONTACT_1;
  int wheelCounter = 0;

  void pollSteeringWheel() {
    /* Clear clock pulse if contact 1 is off. */
    clockPulse &= readIoPort(0xFC);
  }

  void interruptServiceRoutine() {
    /* Update counter for steering wheel if clock pulse */
    /* was reset in the polling routine. */
    if (clockPulse == 0) {
      /* Read contact 2 to check direction. */
      if (readIoPort(0xFC) & CONTACT_2) {
        wheelCounter++;
      } else {
        wheelCounter--;
      }
      /* Set clock pulse. */
      clockPulse = CONTACT_1;
    }
  }


    Using one clock pulse for each steering wheel (setting in the interrupt service routine and clearing it in the background) serves two purposes:
  1. It allows for two steering wheels to share a single interrupt
  2. It provides a pseudo low-pass filter that significantly reduces any jitter caused by the mechanical contacts.

    Although this approach also require polling, it has some advantages. First the polling interval can be twice as long as the pure polling based version. Second, the polling method is rather short compared to the one in the polling only version. Together this gives a solution that require much less CPU time.


Tuning

    To get a responsive input from the controllers, the background task needs to run around once every one millisecond. Running it too often causes more jitter, and running it less often reduces the responsiveness of the steering wheel. The algorithm is not that sensitive for some variance in when the background task is run, and occasional large delays between runs does not cause any large impact to the user. An easy way during development to see that the background task is called at the desired interval is to add a code snippet that changes the border color every time it is been called.

    Also, a game may want to downshift the wheelCounter values to get the desired sensitivity of the steering wheel.


Sample Code

    A Hitech C implementation (assembly code using c calling conventions) of the interrupt based algorithm can be downloaded here.


No comments:

Post a Comment