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)

No comments:

Post a Comment