Friday, January 27, 2017

Arduino and FSX (Part 7)

Arduino Joystick with 32 Pins Using Pin-Change-Interrupts

The 8-axis by 40-pin Joystick discussed in the previous "Arduino and FSX (Part 6)" is modified in this Part 7 to use "pin change interrupts". Pin-Change-Interrupts are a technique to implement an increased number of interrupts if required. In a future blog, pin-change-interrupts will be used for adding rotary encoders that are connected to the MCP23S17 Port Expanders. Each 16-pin Port Expander can connect 8 rotary encoders, and each Port Expander will require two interrupt pins on the Arduino.

In Part 6, MCP23S17 (MCP) 16-pin Port Expanders had the port interrupt pins "mirrored" together and those were then wired together and configured as "open-drain". By mirroring and bridging with open-drain, only one Arduino interrupt pin was used to sense when any of the 32 MCP pins changed state. When that interrupt was sensed, the joyReport data was transmitted through USB to FSX. If only buttons or switches are to be used, this approach is ideal.

In this Part 7, "pin-change-interrupts" are used in place of the mirrored and open-drain interrupt. Each of the Port A and Port B interrupts on each MCP will be wired to an Arduino digital pin that is configured as a pin-change-interrupt. The Arduino Uno has two pins assigned as hardware interrupts, INT0, and INT1, both of which are on the Arduino Port D. Port D is comprised of the digital pins D0-D7, Port B includes digital pins D8-13, and Port C includes analog pins A0-A5. Rather than being limited to two hardware interrupt pins (INT0 and INT1), each Arduino pin can be configured as an interrupt pin. Interrupt pins on each of these 3 ports work just like those on the MCP chips. Each port pin can serve as an interrupt pin, and each can trigger a port interrupt--PCIE0 (Port B), PCIE1 (Port C), or PCIE2 (Port D).

In the sketch code below, pin D2 is wired to MCP Bank0 Port INTA, and D3 is wired to MCP Bank0 Port INTB. Pin D4 is wired to MCP Bank1 Port INTA, and D5 is wired to MCP Bank1 Port INTB. Four MCP Port interrupts are connected to four Arduino pin-change-interrupts.

When an MCP pin is grounded through a switch, that pin transitions from HIGH to LOW. When that happens, the corresponding MCP Port interrupt transitions HIGH to LOW which takes the Arduino interrupt pin HIGH to LOW. That transition triggers an Interrupt Service Routine (ISR) for the Arduino port. The ISR calls function "checkForPinChange (port)" which reads the MCP pin data and sets flags to indicate to function "loop()" that the joyReport needs to be filled with data and transmitted through USB to FSX. When the switch is released, the same process happens, but the pin value returns to 0.

The "Arduino_ISR_MCP23S17_8X40Joystick" sketch below is a near replica of the sketch used in Part 6, with added changes necessary to implement pin-change-interrupts. The hardware does not change from those used in Part 6. Be sure to comment out DEBUG so that the joyReport is not corrupted with Serial.print output.


Software and Firmware --
The Arduino sketch is presented below. Once the Arduino is programmed with the sketch and tested with your hardware, the Arduino needs to be converted from a USB-Serial device to a USB-HID joystick device (Arduino-big-joystick.hex) with 8-axes and 40-buttons/switches. That device can then provide an additional 32 buttons/switches to be used by FSX. Those buttons/switches can be programmed in FSUIPC to activate the desired function in FSX. Also modify the sketch to incorporate the axes you require. See the comments in the sketch as a guide.
Sketch -

/*

   Sketch - Arduino_ISR_MCP23S17_8X40Joystick

   Lowell Bahner
   January, 2017

   This code sends joyStick data when axis or button/switches change values.

   This code uses two MCP23S17 16-pin Port Expanders to add 32 pins to an Uno or Mega2560.

   This code uses 4 ISR's (one per MCP port) to sense port interrupts on the two MCP's.

   A push button or switch takes one or more MCP23S17 input pins LOW (to ground)
   to trigger interrupts which are processed to read the button/switch data.
   Multiple switches can change state at the same interrupt.

   The MCP23S17 has an interrupt on each digital input pin which can set
   a PORT interrupt pin. The Port A interrupt (INTA) is chip pin 20, and Port B
   interrupt (INTB) is chip pin 19. Each INTA and INTB then connects to an
   Arduino ISR interrupt pin.

   Bank   Port   MCP_INT   Ard_INT Pin
   0      A      A         D2
   0      B      B         D3
   1      A      A         D4
   1      B      B         D5

   Each Arduino interrupt triggers the appropriate
   "pin-change-interrupt ISR" (interrupt service routine) in the sketch.
   On an Uno, there are two external interrupts Int0, Int1) but 24 Pin-Change-Interrupts.
   Pin-Change-Interrupts share an ISR between all the pins on a port (port B, C, and D).

   Ref: thewanderingengineer.com/2014/08/11/arduino-pin-change-interrupts/
   Ref: www_gammon.com.au/forum/?id=11130

   The interrupts on the MCP Port Expanders are NOT bridged together using
   MIRROR or OPEN DRAIN. Both the MCP and Arduino pins must be set as INPUT_PULLUP.

   UNO/MEGA hardware interrupt pins (D2, D3, D4, D5...)

   For use as USB-HID joyStick:
   1) connect potentiometer wiper pins to analog pins A0, A1, A2
   2) In loop(), comment out "sendFlag = 0;" line to use axes
   3) connect rotary encoders to MCP23S17 digital pins
   4) test with DEBUG to check joyReport data are correct
   5) #undef DEBUG so that joyReport data are not corrupted with Serial.print
   6) program USB ATmega16U2 as USB-HID

   ======================================================

   Sketch Output to Serial Monitor Example


   ======================================================

  Starting MCP23S17 Pin-Change-Interrupt Joystick


  Bank[chip]: 0
  ...mcp [chip].aPin: 2, mcp [chip].bPin: 3, mcp [chip].aBitMask: 00000100, mcp [chip].bBitMask: 00001000
  ...mcp [chip].whichAInterrupt: 2, mcp [chip].whichBInterrupt: 2
  ... *ICRmaskPort: 00001100, PCICR: 00000100, PCMSK0: 00000000, PCMSK1: 00000000, PCMSK2: 00001100

  Bank[chip]: 1
  ...mcp [chip].aPin: 4, mcp [chip].bPin: 5, mcp [chip].aBitMask: 00010000, mcp [chip].bBitMask: 00100000
  ...mcp [chip].whichAInterrupt: 2, mcp [chip].whichBInterrupt: 2
  ... *ICRmaskPort: 00111100, PCICR: 00000100, PCMSK0: 00000000, PCMSK1: 00000000, PCMSK2: 00111100

  (Button down, Bank0 Pin 1 & Bank1 Pin 4)
  Bank[chip]: 0, flagMCPA: 1, PortGPIOVal: 00000010, mcpFlag 1

  Bank[chip]: 1, flagMCPA: 1, PortGPIOVal: 00010000, mcpFlag 1

  joyReport btnArray: 00000010, 00000000, 00010000, 00000000

  axis[0]= -21045 axis[1]= -20853 axis[2]= -21173 axis[3]= 0 axis[4]= 0 axis[5]= 0 axis[6]= 0 axis[7]= 0
  btnArray[0]= 00000010 btnArray[1]= 00000000 btnArray[2]= 00010000 btnArray[3]= 00000000 btnArray[4]= 00000000

  (Button up)
  Bank[chip]: 0, flagMCPA: 1, PortGPIOVal: 00000000, mcpFlag 1

  Bank[chip]: 1, flagMCPA: 1, PortGPIOVal: 00000000, mcpFlag 1

  joyReport btnArray: 00000000, 00000000, 00000000, 00000000

  axis[0]= -20341 axis[1]= -20276 axis[2]= -20533 axis[3]= 0 axis[4]= 0 axis[5]= 0 axis[6]= 0 axis[7]= 0
  btnArray[0]= 00000000 btnArray[1]= 00000000 btnArray[2]= 00000000 btnArray[3]= 00000000 btnArray[4]= 00000000

  (Button down, Port A: Bank0 Pin 1, Bank1 Pin 4)
  Bank[chip]: 0, flagMCPA: 1, PortGPIOVal: 00000010, mcpFlag 1

  Bank[chip]: 1, flagMCPA: 1, PortGPIOVal: 00010000, mcpFlag 1

  joyReport btnArray: 00000010, 00000000, 00010000, 00000000

  axis[0]= -20597 axis[1]= -20533 axis[2]= -20917 axis[3]= 0 axis[4]= 0 axis[5]= 0 axis[6]= 0 axis[7]= 0
  btnArray[0]= 00000010 btnArray[1]= 00000000 btnArray[2]= 00010000 btnArray[3]= 00000000 btnArray[4]= 00000000

  (and, Button down, Port B: Bank0 Pin 14, Bank1 Pin 12)
  Bank[chip]: 0, flagMCPB: 1, PortGPIOVal: 01000000, mcpFlag 2

  Bank[chip]: 1, flagMCPB: 1, PortGPIOVal: 00010000, mcpFlag 2

  joyReport btnArray: 00000010, 01000000, 00010000, 00010000

  axis[0]= -21494 axis[1]= -21430 axis[2]= -21750 axis[3]= 0 axis[4]= 0 axis[5]= 0 axis[6]= 0 axis[7]= 0
  btnArray[0]= 00000010 btnArray[1]= 01000000 btnArray[2]= 00010000 btnArray[3]= 00010000 btnArray[4]= 00000000

*/

// to turn on DEBUG, define DEBUG. Make sure to undef DEBUG for joystick use
//#undef DEBUG
#define DEBUG

// ======================================================
// Sketch Code

// Majenko MCP23S17 Library updated 2017-01-23 which fixed interrupts bug
#include <MCP23S17.h>

// Arduino Library SPI.h
#include <SPI.h>

const byte EXPANDERS = 2;       // the number of MCP23S17 chips to use
const byte NUM_BUTTONS = 40;    // do not change this value
const byte NUM_AXES = 8;        // 6 axes to UNO, and 8 to MEGA. do not change this value.

typedef struct joyReport_t {
  int16_t axis[NUM_AXES];
  uint8_t btnArray[(NUM_BUTTONS + 7) / 8]; // 8 buttons per byte
} joyReport_t;
joyReport_t joyReport;
joyReport_t prevjoyReport;
uint8_t sendFlag = 0; // only send data when data change

// SPI CS/SS chipselect pin can be changed by user as desired
const uint8_t chipSelect = 10;

volatile uint16_t mcpReading = 0; // 16-bit interrupt reading
volatile uint8_t mcpFlag = 0;     // only send data when data change

typedef struct
{
  int deviceID; // MCP chip 0,1,...7
  int aPin;     // which Arduino pin for the "A" side interrupt
  int bPin;     // which Arduino pin for the "B" side interrupt

  // values below are calculated at run-time
  volatile uint16_t mcpGPIO; // chip word value
  volatile uint8_t mcpAGPIO; // Port A byte value
  volatile uint8_t mcpBGPIO; // Port B byte value

  volatile uint8_t flagMCPA;
  volatile uint8_t flagMCPB;

  volatile uint8_t flagGPIO;
  volatile uint8_t flagAGPIO;
  volatile uint8_t flagBGPIO;

  byte aBitMask;   // which interrupt bit in the A port
  byte bBitMask;   // which interrupt bit in the B port
  byte whichAInterrupt;  // which pin-change interrupt port (0, 1, 2)
  byte whichBInterrupt;  // which pin-change interrupt port (0, 1, 2)

} MCP;

// MCP device=devID{0,1,...EXPANDERS-1}, where Bank0=0, Bank1=1...Bank7=7
// MCP port=portID{0,1}, where PORTA=0 PORTB=1
// Each MCP devID:portID = 8 pins
// Bank0 connects to Ard pins D2,D3; Bank1 connects to Ard pins D4,D5;, etc
volatile MCP mcp [EXPANDERS] = {
  {0, 2, 3},
  {1, 4, 5},
  // {2, 6, 7},
  // {3, 8, 9},
}; // end of expanders

// Create an object for each chip
// Bank0 is address 0. Pins A0,A1,A2 grounded.
// Bank1 is address 1. Pin A0=+5V, A1,A2 grounded.

MCP23S17 Bank0(&SPI, chipSelect, 0);
MCP23S17 Bank1(&SPI, chipSelect, 1);

// ++++++++++++++++++++++++++++++++++++++++++++++++++++


// ++++++++++++++++++++++++++++++++++++++++++++++++++++
//   function prototypes - not necessary but show the
//         functions used in this sketch
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

// pin change interrupts
void checkForPinChange (const byte which);
ISR (PCINT0_vect);
ISR (PCINT1_vect);
ISR (PCINT2_vect);
void setup();
void setChipPins();
void setPin(MCP23S17 &bank, uint8_t chip);
void readBankPortGPIOVal(MCP23S17 &bank, uint8_t chip, uint8_t port);
void interruptMCP();
void sendJoyReport(struct joyReport_t *report);
void loop ();
void print8Bits(uint8_t myByte);
void print16Bits(uint16_t myWord);
void crPrintHEX(unsigned long DATA, unsigned char numChars);

// ++++++++++++++++++++++++++++++++++++++++++++++++++++


// ++++++++++++++++++++++++++++++++++++++++++++++++++++
//   function checkForPinChange()
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

// MCP pin change triggers MCP INTA or INTB that triggers "which" Arduino ISR.
// which==0 is Ard Port B, which==1 is Ard Port C, which==2 is Ard Port D.
// A switch ON = pin change HIGH-LOW or, a switch OFF = pin change LOW-HIGH.
// if MCP chip INTA fired, set flagMCPA and read the Port A pins.
// if MCP chip INTB fired, set flagMCPB and read the Port B pins.
// Set mcpFlag>0 means one or more pins/interrupts changed state.
// ISR finishes, interrupts are cleared, then process the flags in loop().
// Using mcp.portRead() clears the mcp interrupt during the Ard interrupt.

void checkForPinChange (const byte which)
{
  //Serial.print ("\n\n =============== which: "); Serial.print (which);
  //Serial.print ("\n checkForPinChange ");

  for (uint8_t chip = 0; chip < EXPANDERS; chip++) {
    if (mcp [chip].whichAInterrupt == which) {
      if ((digitalRead(mcp [chip].aPin) == LOW)) {
        mcp[chip].flagMCPA = 1;
        mcpFlag = 1;
        switch (chip) {
          case 0:
            readBankPortGPIOVal(Bank0, chip, 0);
            break;
          case 1:
            readBankPortGPIOVal(Bank1, chip, 0);
            break;
          default:
            break;
        }
#ifdef DEBUG
        /*
          Serial.print ("\n\nBank[chip]: "); Serial.print (chip);
          Serial.print (", flagMCPA: "); Serial.print (mcp[chip].flagMCPA);
          Serial.print (", PortGPIOVal: "); print8Bits (mcp[chip].mcpAGPIO);
          Serial.print (", mcpFlag "); Serial.print (mcpFlag);
        */
#endif
      }
    }
    if (mcp [chip].whichBInterrupt == which) {
      if ((digitalRead(mcp [chip].bPin) == LOW)) {
        mcp[chip].flagMCPB = 1;
        mcpFlag = 2;
        switch (chip) {
          case 0:
            readBankPortGPIOVal(Bank0, chip, 1);
            break;
          case 1:
            readBankPortGPIOVal(Bank1, chip, 1);
            break;
          default:
            break;
        }
#ifdef DEBUG
        /*
          Serial.print ("\n\nBank[chip]: "); Serial.print (chip);
          Serial.print (", flagMCPB: "); Serial.print (mcp[chip].flagMCPB);
          Serial.print (", PortGPIOVal: "); print8Bits (mcp[chip].mcpBGPIO);
          Serial.print (", mcpFlag "); Serial.print (mcpFlag);
        */
#endif
      }
    }
  }     // end of for each expander
} // end of checkForPinChange

// ++++++++++++++++++++++++++++++++++++++++++++++++++++
//   ISR (PCINT0_vect)
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

// handle pin change interrupt for D8 to D13 here
ISR (PCINT0_vect)
{
  cli(); //stop interrupts happening before we read pin values
  checkForPinChange (PCIE0);
  sei(); //restart interrupts
}  // end of PCINT0_vect


// ++++++++++++++++++++++++++++++++++++++++++++++++++++
//   ISR (PCINT1_vect)
// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// handle pin change interrupt for A0 to A5 here
ISR (PCINT1_vect)
{
  cli(); //stop interrupts happening before we read pin values
  checkForPinChange (PCIE1);
  sei(); //restart interrupts
}  // end of PCINT1_vect

// ++++++++++++++++++++++++++++++++++++++++++++++++++++
//   ISR (PCINT2_vect)
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

// MCP23S17 INTA and INTB wired to Arduino pin D2-D7
//    triggers this ISR (PCINT2_vect).
// handle pin change interrupt for D0 to D7 here
ISR (PCINT2_vect)
{
  //Serial.println ("ISR (PCINT2_vect) ...");
  cli(); //stop interrupts happening before we read pin values
  checkForPinChange (PCIE2);
  sei(); //restart interrupts
}  // end of PCINT2_vect

// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// function setup
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

void setup() {

  Bank0.begin();
  Bank1.begin();

  Serial.begin (115200);

#ifdef DEBUG
  Serial.println(F("\n Starting MCP23S17 Pin-Change-Interrupt Joystick"));
#endif

  //----------------------------------------------------------
  // Port Expander pin and interrupts configuration
  //----------------------------------------------------------
  //
  // Input port code
  // pins 0-15 are on device:port
  // device Bank0, Bank1
  // port 0=Port A, 1=Port B
  //
  // Set MCP23S17 pin modes and Interrupt configurations
  // example set one pin: chip.pinMode(0, INPUT_PULLUP);
  //
  setChipPins(); // use loop to set individual pins

  // clear the MCP interrupt values variables
  mcpReading = 0;

  //----------------------------------------------------------
  // Arduino pin-change-interrupts configuration
  //----------------------------------------------------------
  //
  // clear any outstanding Arduino pin-change-interrupts
  PCIFR  |= bit (PCIF0) | bit (PCIF1) | bit (PCIF2);

  for (uint8_t chip = 0; chip < EXPANDERS; chip++)
  {
    // set interrupt pins to INPUT_PULLUP
    pinMode (mcp [chip].aPin, INPUT_PULLUP);
    pinMode (mcp [chip].bPin, INPUT_PULLUP);

    // Create a Mask with the pin bit = 1
    mcp [chip].aBitMask = digitalPinToBitMask (mcp [chip].aPin);
    mcp [chip].bBitMask = digitalPinToBitMask (mcp [chip].bPin);

    // Which ISR for each interrupt pin (0=PCIE0, 1=PCIE1, 2=PCIE2)
    mcp [chip].whichAInterrupt = digitalPinToPCICRbit (mcp [chip].aPin);
    mcp [chip].whichBInterrupt = digitalPinToPCICRbit (mcp [chip].bPin);

    // Activate this pin-change interrupt bit (eg. PCMSK0, PCMSK1, PCMSK2)
    /*   Ref: thewanderingengineer.com/2014/08/11/arduino-pin-change-interrupts/
      // PCMSK definitions:
      PCMSK0 |= 0b00000011;    // turn on pins PB0 & PB1, PCINT0 & PCINT1, pins D8, D9
      PCMSK1 |= 0b00010000;    // turn on pin PC4, which is PCINT12, pin A4
      PCMSK2 |= 0b00001100;    // turn on pins PD2 & PD3, PCINT18 & PCINT19, pins D2, D3
    */
    volatile byte * ICRmaskPort = digitalPinToPCMSK (mcp [chip].aPin);
    *ICRmaskPort  |= bit (digitalPinToPCMSKbit (mcp [chip].aPin));
    //Serial.print ("\n*ICRmaskPort A: "); print8Bits (*ICRmaskPort);
    *ICRmaskPort  |= bit (digitalPinToPCMSKbit (mcp [chip].bPin));
    //Serial.print (", *ICRmaskPort B: "); print8Bits (*ICRmaskPort);

    // Enable this pin-change interrupt
    /*   Ref: thewanderingengineer.com/2014/08/11/arduino-pin-change-interrupts/
      // PCICR definitions:
      PCICR |= 0b00000001;    // turn on port b
      PCICR |= 0b00000010;    // turn on port c
      PCICR |= 0b00000100;    // turn on port d
      PCICR |= 0b00000111;    // turn on all ports
    */
    PCICR |= bit (digitalPinToPCICRbit (mcp [chip].aPin));
    //Serial.print ("\nPCICR A: "); print8Bits (PCICR);
    PCICR |= bit (digitalPinToPCICRbit (mcp [chip].bPin));
    //Serial.print (", PCICR B: "); print8Bits (PCICR);

#ifdef DEBUG
    Serial.print ("\n\nBank[chip]: "); Serial.print (chip);
    Serial.print ("\n...mcp [chip].aPin: "); Serial.print (mcp [chip].aPin);
    Serial.print (", mcp [chip].bPin: "); Serial.print (mcp [chip].bPin);
    Serial.print (", mcp [chip].aBitMask: "); print8Bits(mcp [chip].aBitMask);
    Serial.print (", mcp [chip].bBitMask: "); print8Bits(mcp [chip].bBitMask);
    Serial.print ("\n...mcp [chip].whichAInterrupt: "); Serial.print (mcp [chip].whichAInterrupt);
    Serial.print (", mcp [chip].whichBInterrupt: "); Serial.print (mcp [chip].whichBInterrupt);
    Serial.print ("\n... *ICRmaskPort: "); print8Bits (*ICRmaskPort);
    Serial.print (", PCICR: "); print8Bits (PCICR);
    Serial.print (", PCMSK0: "); print8Bits (PCMSK0);
    Serial.print (", PCMSK1: "); print8Bits (PCMSK1);
    Serial.print (", PCMSK2: "); print8Bits (PCMSK2);
#endif

  } // end of Arduino interrupts for each expander

}  // end of setup


// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// function setChipPins()
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

// Set all MCP Chip pins to desired mode
void setChipPins() {
  // Example: pass the Bank object to the setPin function

  for (uint8_t chip = 0; chip < EXPANDERS; chip++) {
    switch (chip) {
      case 0:
        setPin(Bank0, chip);
        break;
      case 1:
        setPin(Bank1, chip);
        break;
      case 2:
        //setPin(Bank2, chip);
        break;
      case 3:
        //setPin(Bank3, chip);
        break;
      default:
        break;
    }
  }
}

// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// function setPin()
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

// Set all Port Expander pins to desired mode INPUT_PULLUP
// Set all Port Expander input pins as interrupts CHANGE
// CHANGE allows buttons to return to 0 (open)
void setPin(MCP23S17 &bank, uint8_t chip) {
  for (uint8_t ind = 0; ind <= 15; ind++) {
    bank.pinMode(ind, INPUT_PULLUP);
    bank.enableInterrupt(ind, CHANGE);
  }

  // set Port Expander Interrupt configuratons
  bank.setMirror(false);
  bank.setInterruptOD(false);
  bank.setInterruptLevel(LOW); // not used with OD

  // clear all interrupts on this Port Expander
  mcpReading = bank.getInterruptValue();
  //Serial.print ("\nBank"); Serial.print (chip);
  //Serial.print (".getInterruptValue() = "); print8Bits (mcpReading);

}

// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// function readBankPortGPIOVal()
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

void readBankPortGPIOVal(MCP23S17 &bank, uint8_t chip, uint8_t port) {
  if (port < 1) {
    mcp[chip].mcpAGPIO = ~bank.readPort(port);
    mcp[chip].flagAGPIO = 1;
  } else {
    mcp[chip].mcpBGPIO = ~bank.readPort(port);
    mcp[chip].flagBGPIO = 1;
  }
}


// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// function interruptMCP()
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

void interruptMCP() {
  // Write the previously read .mcpGPIO values into joyReport btnArray
  //Serial.println ("\n");
  for (uint8_t chip = 0; chip < EXPANDERS; chip++) {
    delay (10);
    switch (chip) {
      case 0:
        joyReport.btnArray[0] = mcp[chip].mcpAGPIO;
        joyReport.btnArray[1] = mcp[chip].mcpBGPIO;
        break;
      case 1:
        joyReport.btnArray[2] = mcp[chip].mcpAGPIO;
        joyReport.btnArray[3] = mcp[chip].mcpBGPIO;
        break;
      default:
        break;
    }
  }

#ifdef DEBUG
  /*
    Serial.print ("\n\njoyReport btnArray: "); print8Bits (joyReport.btnArray[0]);
    Serial.print (", "); print8Bits (joyReport.btnArray[1]);
    Serial.print (", "); print8Bits (joyReport.btnArray[2]);
    Serial.print (", "); print8Bits (joyReport.btnArray[3]);
  */
#endif

}

// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// function sendJoyReport()
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

// Send an HID report to the USB interface
void sendJoyReport(struct joyReport_t *report)
{
#ifndef DEBUG
  //Serial.write((uint8_t *)report, sizeof(joyReport_t));
  // do not send duplicate values
  //
  if (memcmp( report, &prevjoyReport, sizeof( joyReport_t ) ) != 0)
  {
    Serial.write((uint8_t *)report, sizeof(joyReport_t));
    memcpy ( &prevjoyReport, report, sizeof( joyReport_t ) );
  }
  //
  // end do not send duplicate values
#else
  // dump human readable output for debugging
  Serial.println("\n");
  for (uint8_t ind = 0; ind < NUM_AXES; ind++) {
    Serial.print("axis[");
    Serial.print(ind);
    Serial.print("]= ");
    Serial.print(report->axis[ind]);
    Serial.print(" ");
  }
  Serial.println();
  for (uint8_t ind = 0; ind < NUM_BUTTONS / 8; ind++) {
    Serial.print("btnArray[");
    Serial.print(ind);
    Serial.print("]= ");
    print8Bits(report->btnArray[ind]);
    //Serial.print(report->btnArray[ind], HEX);
    Serial.print(" ");
  }

#endif
}


// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// function loop
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

void loop ()
{

  // This code runs the MCP Interrupt processing from the Arduino ISR
  //    and then takes action (checkForPinChange()) for the MCP pins that
  //    went LOW or HIGH.

  // checkForPinChange() flags that an interrupt (mcpFlag>0) has occurred
  //    and each mcp[chip].flagMCPA or mcp[chip].flagMCPB is set if a port pin
  //    changed state.

  // checkForPinChange() reads the MCP pins (readPort(0 or 1)).
  // interruptMCP() writes the MCP pin values into joyReport.

  // A pin setting in subsequent joyReports will remain ON as long as
  //    the switch is ON, and each joyReport that is sent (due to a different
  //    pin or axis change) will have that pin ON until the switch is OFF.
  //    When a switch turns off, that pin is cleared and a joyReport is sent.

  // process the digital input pins on MCP23S17's
  if (mcpFlag > 0) {
    interruptMCP(); // write the interrupt pins to joyReport
  }

  /* Axes connect to Analog pins A0, A1, A2...A7 */
  /* Arduino UNO has 6 analog pins of 8 possible. Set pin to 0 if not used */
  /* Ground any analog ports that are not connected to potentiometers to reduce noise */
  uint8_t axisCount = 3; // set the number of axes you want to use, 3=[0,1,2]
  for (uint8_t axis = 0; axis < axisCount; axis++) {
    int tmp = joyReport.axis[axis]; // copy previous axis value
    // Average 5 readings of port to get better values from noisy potentiometers
    // Use >5 to average more readings per pot
    long sumAxis = 0;
    int avg = 0;
    int count = 5;
    for (int i = 0; i < count; i++) {
      sumAxis = sumAxis + analogRead(axis);
    }
    avg = sumAxis / count;
    joyReport.axis[axis] = map(avg, 0, 1023, -32768, 32767 );

    // flag change in axis if avg reading changes by > 100
    if (abs(joyReport.axis[axis] - tmp) > 100) sendFlag = 1;
  }

  //Set un-used analog pins to 0 to reduce spurious values in joyReport.
  for (uint8_t i = axisCount; i < 8; i++) {
    joyReport.axis[i] = 0;
  }

  // for now, turn off axis data for testing digital inputs
  // comment out "sendFlag = 0;" line to use axes
  sendFlag = 0;

  if ((mcpFlag > 0) || (sendFlag > 0)) {
    //Send Data to HID
    sendJoyReport(&joyReport);
    mcpFlag = 0;
    sendFlag = 0;

    // Clear the MCP23S17 interrupt flags to allow new interrupts
    for (uint8_t chip = 0; chip < EXPANDERS; chip++) {
      switch (chip) {
        case 0:
          //mcpReading = Bank0.getInterruptValue(); // this is not necessary
          mcp[chip].flagMCPA = 0;
          mcp[chip].flagMCPB = 0;
          break;
        case 1:
          //mcpReading = Bank1.getInterruptValue(); // this is not necessary
          mcp[chip].flagMCPA = 0;
          mcp[chip].flagMCPB = 0;
          break;
        case 2:
          // mcpReading = Bank2.getInterruptValue(); // this is not necessary
          mcp[chip].flagMCPA = 0;
          mcp[chip].flagMCPB = 0;
          break;
        case 3:
          // mcpReading = Bank3.getInterruptValue(); // this is not necessary
          mcp[chip].flagMCPA = 0;
          mcp[chip].flagMCPB = 0;
          break;
        default:
          break;
      }
    }
  }

  delay (10); // give loop something to do while idle

}

#ifdef DEBUG
// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// print binary8 binary16 and hex functions
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

//---------------------------------------------------------------------------------
// print 8-bit byte as 8 bit binary string
//---------------------------------------------------------------------------------

void print8Bits(uint8_t myByte) {
  for (uint8_t mask = 0x80; mask; mask >>= 1) {
    if (mask  & myByte)
      Serial.print('1');
    else
      Serial.print('0');
  }
}

//---------------------------------------------------------------------------------
// print 16-bit word as 2 8-bit bit binary strings with space
//---------------------------------------------------------------------------------

void print16Bits(uint16_t myWord) {
  print8Bits(highByte(myWord));
  Serial.print (" ");
  print8Bits(lowByte(myWord));
  /*
    for (uint16_t mask = 0x8000; mask; mask >>= 1) {
    if (mask  & myWord)
      Serial.print('1');
    else
      Serial.print('0');
    }
  */
}

//---------------------------------------------------------------------------------
// crPrintHEX print value as hex with specified number of digits
//---------------------------------------------------------------------------------

void crPrintHEX(unsigned long DATA, unsigned char numChars) {
  unsigned long mask  = 0x0000000F;
  mask = mask << 4 * (numChars - 1);
  Serial.print("0x");
  for (unsigned int eID = numChars; eID > 0;  --eID) {
    Serial.print(((DATA & mask) >> (eID - 1) * 4), HEX);
    mask = mask >> 4;
  }
  Serial.print("  ");
}

#endif


Useful references -
(Jan 27, 2017)


Monday, January 23, 2017

Arduino and FSX (Part 6)

Add 32 Switches to FSX with MCP23S17 Port Expander  — 
In previous blogs (Arduino and MCP23S17 Port Expander, Parts 1-4), I explained how these Port Expanders could be used with an Arduino to add an additional 16 to 128 pins. In this "Arduino and FSX (Part 6)", I provide sketch code to create an Arduino 32-pin joystick for FSX using an Uno and two MCP23S17 (aka MCP) chips. This code can be modified to create larger joysticks if the user's non-FSX software can utilize the larger number of pins.
Hardware --
The hardware for this example is: 1) Uno or Mega 2560 with USB chip ATmega 16U2; 2) dual MCP23S17 (SPI) Port Expanders. The wiring for connecting an Arduino to MCP was discussed in the earlier blogs. I will use the quad MCP backpack with the Arduino, since I have that already constructed, but will only use 2 of the 4 MCP chips.
Manually controlled buttons and switches do not change that often when used in FSX. More than one switch might change at the same time but that is about the only issue that needs to be considered. Otherwise, switch settings may not change for seconds, minutes, or hours, so timing is not critical. If rotary encoders get added into the mix, then timing becomes an issue. For now, only simple things like on-off switches are being considered.
Design --
The sketch software can be fairly simple: 1) one or more switches changes states; 2) the change of state on a Port Expander pin throws an interrupt; 3) the MCP interrupt pin throws the Arduino interrupt; and, 4) the Arduino reads then writes the MCP pin values to the joyReport which is transmitted through USB to FSX, where action is taken.
To simplify even further, the sketch software does not have to decide what to do for any particular pin on any particular port or any particular chip. The only decision is "if a pin has changed, send the new joyReport".
On the hardware/firmware level, the Majenko MCP23S17 library will be used. The MCP input pins will be set to INPUT_PULLUP, so they will be HIGH until pulled low by a switch changing states to ground (bank.enableInterrupt(MCP pin, CHANGE)). When the switch is opened (Off) then the input will return to HIGH by the pullup. All 16 pins on a chip will use one common interrupt (bank.setMirror(true)) and all chip interrupts will be wired together (bank.setInterruptOD(true)) using open-drain, so that any one MCP pin change will trigger the one Arduino interrupt (INT0) on pin D2.
I reported a bug I found in the interrupt code to Majenko which was promptly fixed on GitHub (2017-Jan-23). Be sure to use the revised library.
Software and Firmware --
The Arduino sketch is presented below. Once the Arduino is programmed with the sketch and tested with your hardware, the Arduino needs to be converted from a USB-Serial device to a USB-HID joystick device (Arduino-big-joystick.hex) with 8-axes and 40-buttons/switches. That device can then provide an additional 32 buttons/switches to be used by FSX. Those buttons/switches can be programmed in FSUIPC to activate the desired function in FSX. Also modify the sketch to incorporate the axes you require. See the comments in the sketch as a guide.
Sketch --

/*
 *
   Sketch - Arduino_MCP23S17_8X40Joystick

   Lowell Bahner
   January, 2017

   This code sends joyStick data when axis or button/switches change values.

   This code uses two MCP23S17 16-pin Port Expanders to add 32 pins to an Uno or Mega2560.

   A push button or switch takes one or more MCP23S17 input pins LOW (to ground)
   to trigger interrupts which are processed to read the button/switch data.
   Multiple switches can change state at the same interrupt.

   The MCP23S17 has an interrupt on each digital input pin which can set
   a PORT interrupt pin. The Port A interrupt (INTA) is chip pin 20, and Port B
   interrupt (INTB) is chip pin 19.

   The interrupts on the MCP23S17 Port Expanders are bridged together using
   MIRROR and OPEN DRAIN. MIRROR connects the INTA and INTB interrupt pins internally
   in the MCP23S17 chip. Either the INTA or INTB pins are wired together to the
   Arduino INT0 interrupt pin, which senses when the bridged (Open drain) MCP23S17
   interrupts occur. INT0 must be set as INPUT_PULLUP and CHANGE.

   UNO/MEGA hardware interrupt pins INT0 (D2)

   For use as USB-HID joyStick:
   1) connect potentiometer wiper pins to analog pins A0, A1, A2
   2) In loop(), comment out "sendFlag = 0;" line to use axes
   3) connect buttons/switches to MCP23S17 digital pins
   4) test with DEBUG to check joyReport data are correct
   5) #undef DEBUG so that joyReport data are not corrupted with Serial.print
   6) program USB ATmega16U2 as USB-HID

   ======================================================

   Sketch Output to Serial Monitor Example

 Starting MCP23S17 Interrupt Joystick

(no buttons pushed)
joyReport btnArray: 00000000, 00000000, 00000000, 00000000

axis[0]= -21237 axis[1]= -21301 axis[2]= -21558 axis[3]= 0 axis[4]= 0 axis[5]= 0 axis[6]= 0 axis[7]= 0
btnArray[0]= 00000000 btnArray[1]= 00000000 btnArray[2]= 00000000 btnArray[3]= 00000000 btnArray[4]= 00000000

(buttons on Port A pushed and held down)
joyReport btnArray: 00000011, 00000000, 00010000, 00000000

axis[0]= -21622 axis[1]= -21686 axis[2]= -21942 axis[3]= 0 axis[4]= 0 axis[5]= 0 axis[6]= 0 axis[7]= 0
btnArray[0]= 00000011 btnArray[1]= 00000000 btnArray[2]= 00010000 btnArray[3]= 00000000 btnArray[4]= 00000000

(buttons on Port B pushed and held)
joyReport btnArray: 00000011, 01000000, 00010000, 00100000

axis[0]= -21301 axis[1]= -21173 axis[2]= -21301 axis[3]= 0 axis[4]= 0 axis[5]= 0 axis[6]= 0 axis[7]= 0
btnArray[0]= 00000011 btnArray[1]= 01000000 btnArray[2]= 00010000 btnArray[3]= 00100000 btnArray[4]= 00000000

(buttons on Port A released)
joyReport btnArray: 00000000, 01000000, 00000000, 00100000

axis[0]= -21750 axis[1]= -21878 axis[2]= -22006 axis[3]= 0 axis[4]= 0 axis[5]= 0 axis[6]= 0 axis[7]= 0
btnArray[0]= 00000000 btnArray[1]= 01000000 btnArray[2]= 00000000 btnArray[3]= 00100000 btnArray[4]= 00000000

(buttons on Port B released)
joyReport btnArray: 00000000, 00000000, 00000000, 00000000

axis[0]= -21686 axis[1]= -21686 axis[2]= -21686 axis[3]= 0 axis[4]= 0 axis[5]= 0 axis[6]= 0 axis[7]= 0
btnArray[0]= 00000000 btnArray[1]= 00000000 btnArray[2]= 00000000 btnArray[3]= 00000000 btnArray[4]= 00000000

   ======================================================

  *
  *
  */

// to turn on DEBUG, define DEBUG. Make sure to undef DEBUG for joystick use
//#undef DEBUG
#define DEBUG

// ======================================================
// Sketch Code

// Majenko MCP23S17 Library updated 2017-01-23 which fixed interrupts bug
#include <MCP23S17.h>

// Arduino Library SPI.h
#include <SPI.h>

const byte EXPANDERS = 2;       // the number of MCP23S17 chips to use
const byte NUM_BUTTONS = 40;    // do not change this value
const byte NUM_AXES = 8;        // 6 axes to UNO, and 8 to MEGA. do not change this value.

typedef struct joyReport_t {
  int16_t axis[NUM_AXES];
  uint8_t btnArray[(NUM_BUTTONS + 7) / 8]; // 8 buttons per byte
} joyReport_t;
joyReport_t joyReport;
joyReport_t prevjoyReport;
uint8_t sendFlag; // only send data when data change

// SPI CS/SS chipselect pin can be changed by user as desired
const uint8_t chipSelect = 10;

uint8_t pinA = 2;   // INT0 Connects to MCP INTA or INTB which are mirrored together
// then bridged with Open-Drain to Arduino INT0.

volatile uint16_t mcpReading = 0; // 16-bit interrupt reading
volatile uint8_t flagMCP = 0;

// Create an object for each chip
// Bank0 is address 0. Pins A0,A1,A2 grounded.
// Bank1 is address 1. Pin A0=+5V, A1,A2 grounded.

MCP23S17 Bank0(&SPI, chipSelect, 0);
MCP23S17 Bank1(&SPI, chipSelect, 1);

// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// function setup
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

void setup() {

  Bank0.begin();
  Bank1.begin();

  pinMode (pinA, INPUT_PULLUP); // Arduino INT0

  attachInterrupt(digitalPinToInterrupt(pinA), interruptA, CHANGE);

  Serial.begin (115200);

#ifdef DEBUG
  Serial.println(F("\n Starting MCP23S17 Interrupt Joystick"));
#endif

  //----------------------------------------------------------
  // Port Expander pin and interrupts configuration
  //----------------------------------------------------------
  //
  // Input port code
  // pins 0-15 are on device:port
  // device Bank0, Bank1
  // port 0=Port A, 1=Port B
  //
  // Set MCP23S17 pin modes and Interrupt configurations
  // example set one pin: chip.pinMode(0, INPUT_PULLUP);
  //
  setChipPins(); // use loop to set individual pins

  // clear the interrupt values variables
  mcpReading = 0;

}  // end of setup



// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// function setChipPins()
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

// Set all Chip pins to desired mode
void setChipPins() {
  // Example: pass the Bank object to the setPin function

  setPin(Bank0);
  setPin(Bank1);
}

// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// function setPin()
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

// Set all Port Expander pins to desired mode INPUT_PULLUP
// Set all Port Expander input pins as interrupts CHANGE
// CHANGE allows buttons to return to 0 (open)
void setPin(MCP23S17 &bank) {
  for (uint8_t ind = 0; ind <= 15; ind++) {
    bank.pinMode(ind, INPUT_PULLUP);
    bank.enableInterrupt(ind, CHANGE);
  }

  // set Port Expander Interrupt configuratons
  bank.setMirror(true);
  bank.setInterruptOD(true);
  //bank.setInterruptLevel(LOW); // not used with OD

  // clear all interrupts on this Port Expander
  mcpReading = bank.getInterruptValue();
}


// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// function interruptA()
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

void interruptA() {
  // ISR to respond to INT0 interrupt in loop().
  // Respond to the INTA/INTB interrupt
  // which was triggered by one or more MCP inputs
  cli(); //stop interrupts happening before we read pin values
  flagMCP = 1; // INT0 interrupt occurred
  sei(); //restart interrupts
}


// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// function interruptMCP()
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

void interruptMCP() {
  // read the Port Expander Interrupt pins inverse
  // and write them into joyReport btnArray
  Serial.println ("\n");
  for (uint8_t chip = 0; chip < EXPANDERS; chip++) {
    delay (10);
    switch (chip) {
      case 0:
        mcpReading = ~Bank0.readPort();
        joyReport.btnArray[0] = lowByte(mcpReading);
        joyReport.btnArray[1] = highByte(mcpReading);
        break;
      case 1:
        mcpReading = ~Bank1.readPort();
        joyReport.btnArray[2] = lowByte(mcpReading);
        joyReport.btnArray[3] = highByte(mcpReading);
        break;
      default:
        break;
    }
  }

#ifdef DEBUG
  Serial.print ("\njoyReport btnArray: "); print8Bits (joyReport.btnArray[0]);
  Serial.print (", "); print8Bits (joyReport.btnArray[1]);
  Serial.print (", "); print8Bits (joyReport.btnArray[2]);
  Serial.print (", "); print8Bits (joyReport.btnArray[3]);
#endif

}


// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// function sendJoyReport()
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

// Send an HID report to the USB interface
void sendJoyReport(struct joyReport_t *report)
{
#ifndef DEBUG
  //Serial.write((uint8_t *)report, sizeof(joyReport_t));
  // do not send duplicate values
  //
  if (memcmp( report, &prevjoyReport, sizeof( joyReport_t ) ) != 0)
  {
    Serial.write((uint8_t *)report, sizeof(joyReport_t));
    memcpy ( &prevjoyReport, report, sizeof( joyReport_t ) );
  }
  //
  // end do not send duplicate values
#else
  // dump human readable output for debugging
  Serial.println("\n");
  for (uint8_t ind = 0; ind < NUM_AXES; ind++) {
    Serial.print("axis[");
    Serial.print(ind);
    Serial.print("]= ");
    Serial.print(report->axis[ind]);
    Serial.print(" ");
  }
  Serial.println();
  for (uint8_t ind = 0; ind < NUM_BUTTONS / 8; ind++) {
    Serial.print("btnArray[");
    Serial.print(ind);
    Serial.print("]= ");
    print8Bits(report->btnArray[ind]);
    //Serial.print(report->btnArray[ind], HEX);
    Serial.print(" ");
  }

#endif
}


// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// function loop
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

void loop ()
{

  // This code runs the MCP Interrupt processing from the Arduino ISR
  // and then takes action (prints) on the MCP pins that went LOW

  // process the digital input pins on MCP23S17's
  if (flagMCP > 0) {
    interruptMCP(); // write the interrupt pins to joyReport
  }

  /* Axes connect to Analog pins A0, A1, A2...A7 */
  /* Arduino UNO has 6 analog pins of 8 possible. Set pin to 0 if not used */
  /* Ground any analog ports that are not connected to potentiometers to reduce noise */
  uint8_t axisCount = 3; // set the number of axes you want to use, 3=[0,1,2]
  for (uint8_t axis = 0; axis < axisCount; axis++) {
    int tmp = joyReport.axis[axis]; // copy previous axis value
    // Average 5 readings of port to get better values from noisy potentiometers
    // Use >5 to average more readings per pot
    long sumAxis = 0;
    int avg = 0;
    int count = 5;
    for (int i = 0; i < count; i++) {
      sumAxis = sumAxis + analogRead(axis);
    }
    avg = sumAxis / count;
    joyReport.axis[axis] = map(avg, 0, 1023, -32768, 32767 );

    // flag change in axis if avg reading changes by > 100
    if (abs(joyReport.axis[axis] - tmp) > 100) sendFlag = 1;
  }

  //Set un-used analog pins to 0 to reduce spurious values in joyReport.
  for (uint8_t i = axisCount; i < 8; i++) {
    joyReport.axis[i] = 0;
  }

  // for now, turn off axis data for testing digital inputs
  // comment out "sendFlag = 0;" line to use axes
  sendFlag = 0;

  if ((flagMCP > 0) || (sendFlag > 0)) {
    //Send Data to HID
    sendJoyReport(&joyReport);
    flagMCP = 0;
    sendFlag = 0;
  }

  // Clear the MCP23S17 interrupts to allow new interrupts
  if (flagMCP < 1) {
    mcpReading = Bank0.getInterruptValue();
    mcpReading = Bank1.getInterruptValue();
  }

  delay (10); // give loop something to do while idle

}

#ifdef DEBUG
// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// print binary8 binary16 and hex functions
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

//---------------------------------------------------------------------------------
// print 8-bit byte as 8 bit binary string
//---------------------------------------------------------------------------------

void print8Bits(uint8_t myByte) {
  for (uint8_t mask = 0x80; mask; mask >>= 1) {
    if (mask  & myByte)
      Serial.print('1');
    else
      Serial.print('0');
  }
}

//---------------------------------------------------------------------------------
// print 16-bit word as 16 bit binary string
//---------------------------------------------------------------------------------

void print16Bits(uint16_t myWord) {
  for (uint16_t mask = 0x8000; mask; mask >>= 1) {
    if (mask  & myWord)
      Serial.print('1');
    else
      Serial.print('0');
  }
}


//---------------------------------------------------------------------------------
// crPrintHEX print value as hex with specified number of digits
//---------------------------------------------------------------------------------

void crPrintHEX(unsigned long DATA, unsigned char numChars) {
  unsigned long mask  = 0x0000000F;
  mask = mask << 4 * (numChars - 1);
  Serial.print("0x");
  for (unsigned int eID = numChars; eID > 0;  --eID) {
    Serial.print(((DATA & mask) >> (eID - 1) * 4), HEX);
    mask = mask >> 4;
  }
  Serial.print("  ");
}
#endif


Useful references -
(Jan 23, 2017)