Jump to content

P-Machinery: Hand-Controller Decoding


Recommended Posts

Since hand-controller decoding has been discussed much lately, I thought I shared the algorithm I designed for P-Machinery 2.0. This approach was originally inspired by the SCANHAND routine in the SDK-1600, by Joe Z., and informed by great insight provided by Joe, Arnauld Chevallier, and Carl Mueller Jr. It also grew from the work done for the Christmas Carol game.

Ultimately, the algorithm, I/O architecture, and implementation are of my own device, but I remain eternally grateful and indebted to those mentioned for their assistance and support.

I/O Architecture
P-Machinery 2.0 employs a rather sophisticated "plug-in" I/O infrastructure (well, mostly "I", since there's no "O" in the Intellivision hand-controller tsk! tsk! ;)). It exposes a modular, event-driven framework in order to support any number of user input mechanics.

The modular nature of the framework allows new types of input devices and mechanics to be supported by merely defining a new custom input decoder and registering it with the I/O sub-system.

At the heart of the I/O sub-system is a chain comprised of three steps:

  • Scan - Scans the I/O ports for a signal and conditions it to remove bounce and other artefacts.
  • Decode - Takes a clean and valid input signal (the output of the Scan step) and decodes it into a usable format that the rest of the program can use. This typically involves translating the raw input code into a record describing the type of input (e.g., disc, keypad, etc.) and the precise value (e.g., disc direction value, key button number, etc.).
  • Dispatch - Takes a decoded input record (the output of the Decode step) and dispatches the necessary "Pressed" and "Release" events with the decoded information.

The Scan and Dispatch steps are always available to any new decoder module, although they too can be bypassed completely if so desired. By switching the underlying routine used in the Decode step, dynamically during program execution, programmers can add support for additional input mechanics or devices.

This article will concentrate on the Decode mechanism of the P-Machinery 2.0 I/O sub-system. Future articles may delve deeper into the other aspects of the framework, but right now we'll only discuss the input decoding parts.

We will further limit the discussion to the built-in Intellivision hand-controllers. Other types of input devices are supported, but they require a custom Scan phase to poll the appropriate signal channels.

Framework Conventions
First, let's discuss some conventions employed by the I/O sub-system. There are three specific elements that it depends on:

  • I/O Port Identifier:
    The I/O Port Identifier is a numeric value used to identify a specific hand-controller unit. The ID is provided as part of the I/O event payload so that event handlers may know which controller generated the event. Below are the four valid hand-controller IO Port Identifiers and their descriptions:
      ID  IO Port     Description
      --  ----------  ----------------------------------------
      0   Player 1    Master Component left hand-controller
      1   Player 2    Master Component right hand-controller
      2   Player 3    ECS left hand-controller
      3   Player 4    ECS right hand-controller
  • I/O Port Status Record:
    The I/O Port Status Record is a block of four consecutive bytes used to store temporary information used by the I/O scanner and decoder routines. The layout of the record is as follows:
      iorec->[0] (VALID) - Raw input (inverted)           1 BYTE
      iorec->[1] (COUNT) - Debouncing filter counter      1 BYTE
      iorec->[2] (LAST)  - Last accepted raw input        1 BYTE
      iorec->[3] (MODE)  - Input mode vector              1 BYTE
  • Event Message Word:
    The Event Message Word is a 16-bit field containing hand-controller event status and decoded input information. It is defined as follows:
            Action    Keypad/Disc      Port     Pressed    Released
               |           |             |         |           |
             ,=|=,   ,=====|=====,     ,=|=,   ,===|===,   ,===|===,
             |   |   |           |     |   |   |       |   |       |
             v   v   v           v     v   v   v       v   v       v
   +---+---+---+---+---+---+---+---+ +---+---+---+---+---+---+---+---+
   | . : . | Y : Y | X : X : X : X | | n : n | K : A : D | K : A : D |
   +---+---+---+---+---+---+---+---+ +---+---+---+---+---+---+---+---+
     F   E   D   C   B   A   9   8     7   6   5   4   3   2   1   0
    \______________,______________/   \______________,______________/
                   |                                 |
                   v                                 v
              Event Data                        Event Status

   Event Status:         Event Data:            Ports:
   -----------------     -----------------      ----------------
   K - Keypad            X - Keypad/Disc        00 - 0: Player 1
   A - Action Button     Y - Action Button      01 - 1: Player 2
   D - Disc              . - Unused             10 - 2: Player 3 *
   n - Reserved                                 11 - 3: Player 4 *
                          (* Requires ECS)

It is the responsibility of the Scan routine to provide a valid I/O Port Input Record to the Decode routine, and for the latter to provide a valid Event Message Word to the Dispatch routine. The dispatcher makes no assumptions or qualifications on the actual decoded values.

Heuristics & Assumptions
The entire hand-controller decoding framework makes several assumptions and employes a few simple rules of thumb as a way to increase practical accuracy. These are based on standard usage patterns in Intellivision games and some ergonomic considerations of the hand-controllers themselves.

  • The keypad and action buttons are treated as "switches." That is, they are expected to be used exclusively and singularly, one at a time. This means that a change from one keypad or action button input code to another implies the release of the previous one.
  • The disc is treated as a "directional platform," with a stream of potential input changes. This means that the disc is presumed active until the it is fully released and the controller signal is idle.
  • There are two mutually exclusive modes of use:
    • "Run & Shoot" (disc and action buttons)
    • "Input Entry" (keypad buttons)
  • Consequently, there are three I/O decoding states:
    • Disc/Action
    • Keypad
    • Idle
  • Switching between both modes requires a transition to the "Idle" state first, which requires the user to let go of a keypad button before pressing the disc, and vice-versa.
  • The hand-controller signal employs a type of Gray-Code scheme in order to ensure distinct signals across all inputs.
  • For disc input, a single bit in the input signal defines the four cardinal directions of the disc (bits #0 through #3).
  • Each direction overlaps with its adjacent ones by two bits, which are disambiguated by a fourth bit. (See table below.)
  • Bit #4 of the input signal further qualifies the input as a perfect diagonal (i.e., NE, SE, SW, NW). Since it only applies to disc input, its presence can be considered evidence of a disc press.
  • The most-significant 3 bits of the input signal encode a pressed action button event, but overlap with keypad input as well. However, if disc input can be unambiguously detected (or if the current decoder state is "Disc/Action"), their presence can be considered evidence of a an action button press.
  • It is ergonomically awkward for a user to press a keypad button and an action button simultaneously on an Intellivision hand-controller, especially if the disc has been pressed recently. Therefore, we do not expect these signals to coincide, and in fact, we discourage it as a valid means of input.



Hand-Controller Disc Input Table:

Down:	00001 -> Direction Bit

	10011 -> Shared with "Right"
	11001 -> Shared with "Left"

Right:	00010 -> Direction Bit

	10011 -> Shared with "Down"
	10110 -> Shared with "Up"

Up:	00100 -> Direction Bit

	11100 -> Shared with "Left"
	10110 -> Shared with "Right"

Left:	01000 -> Direction Bit

	11100 -> Shared with "Up"
	11001 -> Shared with "Down"

Decoding Algorithm
The hand-controller decoding algorithm employed by P-Machinery 2.0 looks something like this:

  • Debounce and filter signal to obtain a stable, sustained input pattern.
  • Complement input signal so that "active = high."
  • If input = 0, then event = idle
    • If mode was not "Idle," trigger appropriate "Release" events depending on previous input.
    • Set mode to "Idle".
    • End decoding.
  • If mode is not "Disc/Action" and diagonal bit (#4) is not set:
    • Mask key bits.
    • Decode key.
    • If match found:
      • Set mode to "Key"
      • Trigger "Key Pressed" event
      • If mode was not "Idle" trigger "Release" event.
      • End decoding.
  • Mask action buttons bits, if not zero:
    • Decode action button.
    • If match found:
      • Set mode to "Disc/Action"
      • Trigger "Button Pressed" event
  • Mask disc bits, it not zero:
    • Decode disc.
    • If match found:
      • Set mode to "Disc/Action"
      • Trigger "Disc Pressed" event
    • End decoding.
  • If we made it to here without a match, then the signal is a glitch and it's invalid, so we discard it and remain in current state.


A more technical treatment of the algorithm, written in a "C-ish" pseudo-code follows. In fact, the decoding routine was first implemented this way to validate that it meets the specification, and then manually translated into CP-1610 Assembly Language.

EventStatus hndctrl_dec(valid, last, *iorec, portid) {
    mode  = iorec->[MODE];

    // Align Port ID to its event message field:
    event = (NO_EVENT ^ (portid << 6));     // ........|PP......

    // Check that we have a valid input
    if (valid == NO_VALID) {
        return event;

    // Check for idle controller
    if (valid == FULL_IDLE) {
        __do_idle(mode, valid, last, &iorec, &event);
        return event;

    // Simple heuristics rules are:
    //  #1: We already know it's valid input
    //  #2: We already asserted it's not idle.
    //  #3: If Combo Mode is active (disc + action), we don't expect keypad input.
    //  #4: If bit 4 (disc diagonal) is set, then it's definitely not keypad input.
    //  #5: If bit 4 is set with high 3 bits, then it's "disc + action" input.
    //  #6: Otherwise, it's an isolated disc input.

    // If not in disc-mode, decode keypad
    if (!(mode & (DISC_MODE | ACT_MODE)) && !(valid & DISC_DIAG)) {
        if ( __dec_key(mode, valid, last, &iorec, &event) ) {
            return event;

    // Decode action buttons
    if (valid & ACT_MASK) {
        __dec_act(mode, valid, last, &iorec, &event);

    // Decode disc
    __dec_disc(mode, valid, last, &iorec, &event);

    // we should have a valid event word by now.
    return event;

The above pseudo-code represents the individual input look-up functions as subroutines, but they were actually implemented in-line as part of the default I/O Decoder routine. The algorithms for each of them are described below, again in pseudo-code:

// Decode keypad
Boolean __dec_key(mode, valid, last, *iorec, *event) {
    // The keypad works by itself, so a full key-up
    // implies an idle controller, which would have
    // been caught upstream.  By the time we get here
    // we can be sure that the input has changed,
    // and its non-zero.  We don't know if it's a
    // key yet, but any previously pressed key must
    // have been released.
    // (Mode values are binary equivalent to Up events)
    event ^= (mode & KEY_MODE);

    match = search(TBL_HNDCTRL.Keypad, valid);
    if (match != NO_MATCH) {
        mode  |= KEY_MODE;
        event ^= KEY_DOWN;
        event ^= (match << ;              // ....KKKK|..K.....

    iorec->[MODE] = mode;
    return (match != NO_MATCH);

// Decode action buttons
void __dec_act(mode, valid, last, *iorec, *event) {
    newin = (valid & ACT_MASK);
    oldin = (last  & ACT_MASK);

    // Only react if the value has changed
    if (newin != oldin) {
        // We set the "up" event on input change
        // (Mode values are binary equivalent to Up events)
        event ^= (mode & ACT_MODE);  // Set "up" event if mode is active

        // Check mode
        if (newin == NO_ACT) {
            mode &= !(ACT_MODE);            // Clear mode
        } else {
            match = search(TBL_HNDCTRL.Action, valid);
            if (match) {
                mode  |= ACT_MODE;
                event ^= ACT_DOWN;
                event ^= (match << 12);     // ..AA....|...A....

    iorec->[MODE] = mode;

// Decode disc
void __dec_disc(mode, valid, last, *iorec, *event) {
    newin = (valid & DISC_MASK);
    oldin = (last  & DISC_MASK);
    match = NO_DISC;

    // Only react if the value has changed
    if (newin != oldin) {
        // Check mode
        if (newin == NO_DISC) {
            // We only set the "up" event on release
            // (Mode values are binary equivalent to Up events)
            mode  &= !(DISC_MODE);          // Clear mode
            event ^= DISC_UP;               // Set "up" event
        } else {
            match = search(TBL_HNDCTRL.Action, valid);
            if (match) {
                mode  |= DISC_MODE;
                event ^= DISC_DOWN;
                event ^= (match << ;      // ....DDDD|....D...

        iorec->[LAST] = match;

    iorec->[MODE] = mode;

Defining I/O Decoders
The I/O Decoder routine described above is but one of the initial decoders available in P-Machinery 2.0. It is intended to be a "general-purpose" hand-controller decoder and should be practical for many use cases. There are other decoders available, such as one that treats the 16-way directional disc as a 4-way input surface, and yet another one that is optimized for action games where keypad entry is neither applicable nor desired (Christmas Carol, for instance).

Any decoder (including custom ones) can be registered for use assigned to a particular state of the P-Machinery Game State Machine, which is the core of the framework.

Defining new decoders is straight forward: You just need to write the code for the appropriate module (Scan, Decode, Dispatch), or use any of built-in ones if you'd like, and then define a call dispatch table with the routine chain.

For instance, making available decoders for single player and double player separately, is as simple as defining two separate dispatch tables re-using the same routines. In fact, that's what the framework does already:

;; ======================================================================== ;;
                ; --------------------------------------
                ; Single-Player mode: Player 1
                ; --------------------------------------
@@Player1:      DECLE   HAND.SCAN.P1
                DECLE   HAND.DECODE.Port
                DECLE   HAND.DISP
                DECLE   IO.DispatchReturn

                ; --------------------------------------
                ; Two-Player mode: Both
                ; --------------------------------------
@@Double:       DECLE   HAND.SCAN.P1
                DECLE   HAND.DECODE.Port
                DECLE   HAND.DISP
                ;       Fall through to Player 2

                ; --------------------------------------
                ; Single-Player mode: Player 2
                ; --------------------------------------
@@Player2:      DECLE   HAND.SCAN.P2
                DECLE   HAND.DECODE.Port
                DECLE   HAND.DISP
                DECLE   IO.DispatchReturn

                ; --------------------------------------
                ; Single-Player mode: Any
                ; --------------------------------------
@@Single:       DECLE   HAND.SCAN.Any
                DECLE   HAND.DECODE.Port
                DECLE   HAND.DISP
                DECLE   IO.DispatchReturn

After that, we just need to register them with the framework to make them accessible to the game program:

                ; Define I/O Decoders (name, address)
                .IO.DefineDecoder(GeneralP1,        IO.LIB.HANDCTRL.Player1)
                .IO.DefineDecoder(GeneralP2,        IO.LIB.HANDCTRL.Player2)
                .IO.DefineDecoder(GeneralSingle,    IO.LIB.HANDCTRL.Single )
                .IO.DefineDecoder(GeneralDouble,    IO.LIB.HANDCTRL.Double )

This allows for an extensible "plug-in" architecture that is versatile and practical. I hope this information not only showcases the features of P-Machinery 2.0, but that it also proves instructive to others working on hand-controller decoding routines for Intellivision games.



  • Like 4
Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

  • Recently Browsing   0 members

    • No registered users viewing this page.
  • Create New...