Monday, January 30, 2017

Arduino with FSX (Part 8)

Adding Rotary Encoders to Arduino with MCP23S17 Port Expanders

In "Arduino and FSX (Part 6)" and "Arduino and FSX (Part 7)", 32 pins were added to an Arduino using MCP23S17 Port Expanders (aka MCP). These 32 pins could be connected to buttons or switches, and their states (0,1) were transmitted in the "joyReport" data structure through USB to FSX/FSUIPC. As pointed out earlier, FSX has an inherent limit of 32 pins as part of any one joyStick. The Arduino, once programmed with a sketch for accepting switch values, can be converted to a HID-USB device, which can act as a USB 32-pin joyStick to a PC. Thirty-two buttons or switches can then send ON-OFF signals from the Arduino to FSX during simulations.

In Part 7, each port (8 pins) on a port expander had its port interrupt connected to a pin-change-interrupt on the Arduino. As an example, port expander #0 has its Port A INTA interrupt pin wired to the Arduino pin D2, and port expander #0 has its Port B INTB interrupt pin wired to the Arduino pin D3. Any pin that changed state (ON-OFF, OFF-ON) triggered either an INTA or INTB interrupt, which triggered its corresponding Arduino Port interrupt, and the new pin settings were transmitted to FSX.

In this Part 8, the sketch code in Part 7 was adapted to add 16 rotary encoders in place of switches and buttons. I use KY040  rotary encoders with 20-detents, but I suspect other encoders that use the A-On-Off, B-On-Off  pattern used by the KY040 should work. The article "Side Project: understanding cheap Rotary encoders" ( 2011-April-14) gives a good explanation how rotary encoders are constructed and function. My January 10, 2017 blog "Arduino and Rotary Encoder (Part 1)" documents the timing of encoder switches as they changed states. See the tables in the sketch code to get an idea of the variability of the switching. That sketch was helpful to me in understanding how the KY040 encoders behaved.

Rotary encoders have two pins (A and B) which toggle, either A-B-A-B or B-A-B-A, for each detent turn. One of the AB sequences is identified as clockwise and the other counter-clockwise, and is determined by the combination of hardware and software.

As an example, with a rotary encoder pin "reAPin" connected to port expander #0, Port A pin #0, a turn of the encoder will trigger the INTA interrupt which will trigger the Arduino pin D2 interrupt. As the rotary encoder turns, it will also toggle its "reBPin" connected to port expander #0, Port B pin #0, which will trigger the INTB interrupt which will trigger the Arduino pin D3 interrupt.

The order of the Arduino pin interrupts must be decoded to determine the direction of turn (clockwise, counter-clockwise) and the appropriate value must be copied into the joyReport data structure to be transmitted to FSX.

While rotary encoders provide a 0 or 1 digital response, as compared to an analog potentiometer, they can be very difficult to tame in software. Many problems arise due to  internal "contact bounce" as the A-B wipers move between detents. Encoders can be held in-between detents, in an intermediate state. Encoders can be turned fast or slow, and one or more encoders can be turned at the same (human) time, which may or may not be in any order at computer speed.

Various hardware and software techniques are used for decoding encoders. Capacitors are often suggested to be added across the encoder A-B pins to reduce contact bounce, while some software incorporates delays or timers to reduce faults. Some authors like and some do not like the use of hardware interrupts. The choice of which software to use can be difficult.

In the sketch provided below, code shared by Oleg Mazurov (Rotary encoder interrupt-service-routine for air micros, 2011 Mar 30) was adapted to add 16 rotary encoders (8 to each MCP23S17), 32 pins, to an Arduino Joystick for use with FSX.

The sketch uses hardware interrupts on both the MCP23S17 and the Arduino, with both devices using pin-change-interrupts on the individual pins and port interrupts to signal the next-in-line device that an interrupt has occurred. This configuration allows an interrupt to be tracked back to the pin on an encoder that initiated the interrupt sequence.




Figure 1. Rotary encoder toggles pins A and B which are connected to pin 0 on Port A and Port B, respectively. Pin 0 state changes trigger the INTA and INTB port interrupts, which trigger the Arduino pin-change-interrupts on D2 and D3. If D2 is triggered before D3, the encoder was turned in a clockwise direction. If D3 was triggered before D2, the encoder was turned in a counter-clockwise direction. The direction data bits are set in the joyReport data structure and are transmitted out USB to FSX. Sixteen encoders can be connected in this manner to the two MCP port expanders.

Following is a Serial.print example of what happens in the function "checkForPinChange ()" code when an encoder is turned one detent:
Bank[chip]: 0, flagMCPA: 1, PortGPIOVal: 00000001, mcpFlag 1
Bank[chip]: 0, flagMCPB: 1, PortGPIOVal: 00000001, mcpFlag 2
Bank[chip]: 0, flagMCPA: 1, PortGPIOVal: 00000000, mcpFlag 1
Bank[chip]: 0, flagMCPB: 1, PortGPIOVal: 00000001, mcpFlag 2
Bank[chip]: 0, flagMCPB: 1, PortGPIOVal: 00000000, mcpFlag 2

Which translates to:
 - MCP[chip=0] Port A, pin #0 has a value of 1
        so flagMCPA=1 to indicate a valid read of the pin.
 - Then, MCP[chip=0] Port B, pin #0 has a value of 1
        so flagMCPB=1 (valid read of the pin).
 - Then, MCP[chip=0] Port A, pin #0 has a value of 0
        so flagMCPA=1 (valid read of the pin).
 - Then, MCP[chip=0] Port B, pin #0 has a duplicate value of 1
        so flagMCPB=1 (valid read of the pin).
 - Then, MCP[chip=0] Port B, pin #0 has a value of 0
        so flagMCPB=1 (valid read of the pin).

The following is a Serial.print example of what happens in the function "processRE ()" code when an encoder is turned seven detents, 3 clockwise count up, 4 counter-clockwise count down:



    MCP[0], RE[0] dir: -1, CW, Count 1
    MCP[0], RE[0] dir: -1, CW, Count 2
    MCP[0], RE[0] dir: -1, CW, Count 3
    MCP[0], RE[0] dir: 1, CCW, Count 2
    MCP[0], RE[0] dir: 1, CCW, Count 1
    MCP[0], RE[0] dir: 1, CCW, Count 0
    MCP[0], RE[0] dir: 1, CCW, Count -1


The sketch code has been tested with dual MCP23S17 Port Expanders on an Arduino Uno programmed as an Arduino HID-USB Joystick with FSX. However, the configuration has not been used over a period of time to determine that it works satisfactorily under extensive flight simulations.



/*

   Sketch - Arduino_ISR_KY040_MCP23S17_8X40Joystick_Oleg

   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.

   16 KY040 Rotary Encoders are connected to the 32 MCP23S17 pins. Each encoder CLK "A" pin
   is connected to a successive MCP[0,1] Port[0] A pin [0,1,...7] and the encoder DT "B" pin
   is connected to the MCB[0,1] Port[1] B pin [8,9,...15].

   The function "processRE()" code is adapted from Oleg Mazurov (2011 Mar 30)
   Ref: www_circuitsathome.com/mcu/rotary-encoder-
                interrupt-service-routine-for-avr-micros/

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

   A one detent turn of an encoder takes either the "A" or "B" pin LOW (ground) which
   takes the MCP23S17 input pin LOW which triggers the MCP Port interrupt
   (INTA or INTB, respectively). The MCB interrupt triggers the respective Arduino
   pin-change-interrupt which calls the Arduino ISR and MCP ISR code. The MCP ISR code
   reads the port pins and sets flags for further processing to determine the
   encoder direction of turn and the number of turns. Each port interrupt is processed
   independently of other interrupts.

   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

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

   Hardware Wiring Example: KY040 Rotary Encoder #0, pin CLK "A" wired
      to MCP #0, Port A, pin 0, and Rotary Encoder #0, pin DT "B" wired
      to MCP #0, Port B, pin 0 (device input pin 8).

   MCP digital pins are set as pin-change-interrupt INPUT_PULLUP
      which trigger the port interrupt when state changes HIGH-LOW or LOW-HIGH.
   MCP #0 pin INTA is port interrupt for MCP Port A.
   MCP #0 pin INTB is port interrupt for MCP Port B.

   MCP #0 pin INTA wired to Arduino D2 on pin-change-interrupt port PIND.
   MCP #0 pin INTB wired to Arduino D3 on pin-change-interrupt port PIND.
   MCP #1 pin INTA wired to Arduino D4 on pin-change-interrupt port PIND.
   MCP #1 pin INTB wired to Arduino D5 on pin-change-interrupt port PIND.

   A clockwise (CW) turn of a Rotary Encoder evaluates to -1.
   A counter-clockwise (CCW) turn of a Rotary Encoder evaluates to 1.

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

   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].pciPortIntA: 2, mcp [chip].pciPortIntB: 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].pciPortIntA: 2, mcp [chip].pciPortIntB: 2
... *ICRmaskPort: 00111100, PCICR: 00000100, PCMSK0: 00000000, PCMSK1: 00000000, PCMSK2: 00111100

(turn multiple RE rotary encoders on different MCP port expanders to obtain direction and count)

 MCP[0], RE[7], encport: 00000011, old_AB: 00111011, enc_states[11]:  dir: -1, CW, Count 1
 MCP[0], RE[7], encport: 00000011, old_AB: 10011011, enc_states[11]:  dir: -1, CW, Count 2
 MCP[0], RE[7], encport: 00000011, old_AB: 11011011, enc_states[11]:  dir: -1, CW, Count 3
...
 MCP[0], RE[7], encport: 00000011, old_AB: 11100111, enc_states[7]:  dir: 1, CCW, Count 0
 MCP[0], RE[7], encport: 00000011, old_AB: 11100111, enc_states[7]:  dir: 1, CCW, Count -1
 MCP[0], RE[7], encport: 00000011, old_AB: 11100111, enc_states[7]:  dir: 1, CCW, Count -2
 MCP[0], RE[7], encport: 00000011, old_AB: 10011011, enc_states[11]:  dir: -1, CW, Count -1
 MCP[0], RE[7], encport: 00000011, old_AB: 01101011, enc_states[11]:  dir: -1, CW, Count 0
...
 MCP[0], RE[0], encport: 00000011, old_AB: 11101011, enc_states[11]:  dir: -1, CW, Count 1
 MCP[0], RE[0], encport: 00000011, old_AB: 11011011, enc_states[11]:  dir: -1, CW, Count 2
 MCP[0], RE[0], encport: 00000011, old_AB: 11011011, enc_states[11]:  dir: -1, CW, Count 3
 MCP[0], RE[0], encport: 00000011, old_AB: 11011011, enc_states[11]:  dir: -1, CW, Count 4
...
 MCP[1], RE[4], encport: 00000011, old_AB: 11100111, enc_states[7]:  dir: 1, CCW, Count 0
 MCP[1], RE[4], encport: 00000011, old_AB: 11100111, enc_states[7]:  dir: 1, CCW, Count -1
 MCP[1], RE[4], encport: 00000011, old_AB: 10100111, enc_states[7]:  dir: 1, CCW, Count -2
 MCP[1], RE[4], encport: 00000011, old_AB: 01100111, enc_states[7]:  dir: 1, CCW, Count -3
 MCP[1], RE[4], encport: 00000011, old_AB: 01100111, enc_states[7]:  dir: 1, CCW, Count -4
 MCP[1], RE[4], encport: 00000011, old_AB: 10010111, enc_states[7]:  dir: 1, CCW, Count -5

*/

// 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 ENCODERS = 8;        // the number of rotary encoders per MCP
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 joyReport 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;     // MCP data changed
volatile uint8_t reFlag = 0;      // only send RE data when joyReport RE data change

// create the ENCODER struct
typedef struct
{
  int reAPin;     // which RE pin for the "A" side interrupt
  int reBPin;     // which RE pin for the "B" side interrupt

  volatile uint8_t reAB;       // retained value
  volatile int8_t reValue;       // current RE value -1, 1
  volatile int32_t reCounter;   // running count for this re[0,1]
  volatile uint8_t reAPinGPIO;   // current value reAPin
  volatile uint8_t reBPinGPIO;   // current value reBPin
  //volatile uint8_t reAPinPrev;   // previous value reAPin
  //volatile uint8_t reBPinPrev;   // previous value reBPin
  //volatile uint8_t reAFlag;      // reAFlag
  //volatile uint8_t reBFlag;      // reBFlag

} RE;

// MCP device=devID{0,1,...EXPANDERS-1}, where Bank0=0, Bank1=1...Bank7=7
// Each MCP devID:portID = 8 pins
// Bank0 connects to Ard pins D2,D3; Bank1 connects to Ard pins D4,D5;, etc
// Rotary Encoders "A" pin to MCP Port A pin, "B" pin to MCP Port B pin
// MCP Port A pins count [0,1,2...7], Port B pins count [8,9,...15]
// 8 ENCODERS per MCP chip
// volatile RE re [ENCODERS];
// end of encoders

// create the EXPANDERS struct
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 pciPortIntA;  // which pin-change interrupt port (0, 1, 2)
  byte pciPortIntB;  // which pin-change interrupt port (0, 1, 2)

  // Add 8 Encoders to each EXPANDER
  volatile RE re [ENCODERS];

} 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 pciPort);
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 sendJoyReport(struct joyReport_t *report);
void interruptMCP();
void processRE();
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 "pciPort" Arduino ISR.
// pciPort==0 is Ard Port B, pciPort==1 is Ard Port C, pciPort==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 pciPort)
{
  //Serial.print ("\n\n =============== pciPort: "); Serial.print (pciPort);
  //Serial.print ("\n checkForPinChange ");

  for (uint8_t chip = 0; chip < EXPANDERS; chip++) {
    if (mcp [chip].pciPortIntA == pciPort) {
      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
        /* */
        // print what pins are changing and in what order of interrupt
        Serial.print ("\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].pciPortIntB == pciPort) {
      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 ("\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

  /* Example output for one-detent turn of encoder
   *  with some switch bounce duplicate interrupts
  Bank[chip]: 0, flagMCPA: 1, PortGPIOVal: 00000001, mcpFlag 1 (A up)
  Bank[chip]: 0, flagMCPB: 1, PortGPIOVal: 00000001, mcpFlag 2 (B up)
  Bank[chip]: 0, flagMCPA: 1, PortGPIOVal: 00000000, mcpFlag 1 (A down)
  Bank[chip]: 0, flagMCPB: 1, PortGPIOVal: 00000001, mcpFlag 2 (B up again)
  Bank[chip]: 0, flagMCPB: 1, PortGPIOVal: 00000000, mcpFlag 2 (B down) done

  Bank[chip]: 1, flagMCPB: 1, PortGPIOVal: 10000000, mcpFlag 2 (B up)
  Bank[chip]: 1, flagMCPB: 1, PortGPIOVal: 10000000, mcpFlag 2 (B up again)
  Bank[chip]: 1, flagMCPA: 1, PortGPIOVal: 10000000, mcpFlag 1 (A up)
  Bank[chip]: 1, flagMCPA: 1, PortGPIOVal: 10000000, mcpFlag 1 (A up)
  Bank[chip]: 1, flagMCPB: 1, PortGPIOVal: 00000000, mcpFlag 2 (B down)
  Bank[chip]: 1, flagMCPA: 1, PortGPIOVal: 00000000, mcpFlag 1 (A down) done
  */

} // 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].pciPortIntA = digitalPinToPCICRbit (mcp [chip].aPin);
    mcp [chip].pciPortIntB = 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, pciPort 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
    // examine the Arduino ISR setup
    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].pciPortIntA: "); Serial.print (mcp [chip].pciPortIntA);
    Serial.print (", mcp [chip].pciPortIntB: "); Serial.print (mcp [chip].pciPortIntB);
    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

    // initalize the 8 encoders on each expander
    for (uint8_t reID = 0; reID < ENCODERS; reID++) {
      mcp[chip].re[reID].reAPin = reID;    // which RE pin for the "A" side interrupt
      mcp[chip].re[reID].reBPin = reID;    // which RE pin for the "B" side interrupt
      mcp[chip].re[reID].reAB = 3;         // old_AB
      mcp[chip].re[reID].reValue = 0;      // current RE value -1, 1
      mcp[chip].re[reID].reCounter = 0;    // running count for this re[0,1]
      mcp[chip].re[reID].reAPinGPIO = 0;   // current value reAPin
      mcp[chip].re[reID].reBPinGPIO = 0;   // current value reBPin
      //mcp[chip].re[reID].reAPinPrev = 0;   // previous value reAPin
      //mcp[chip].re[reID].reBPinPrev = 0;   // previous value reBPin
      //mcp[chip].re[reID].reAFlag = 0;      // reAFlag
      //mcp[chip].re[reID].reBFlag = 0;      // reBFlag
    } // end of setup encoders

  } // 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 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
  /* comment out printing joyReport to Serial Monitor for now
    // 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 interruptMCP() not used with encoders
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

void interruptMCP() {
  // Parse the previously read .mcpGPIO values for encoder pin changes
  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
  /*
    // examine the pins as they change
    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 processRE()
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

void processRE() {

  /*
   * Process the encoders on each expander
   * 8 ENCODERS per MCP chip
   *
      for (uint8_t reID = 0; reID < ENCODERS; reID++) {
        mcp[chip].re[reID].reAPin = reID;    // which RE pin for the "A" side interrupt
        mcp[chip].re[reID].reBPin = reID;    // which RE pin for the "B" side interrupt
        mcp[chip].re[reID].reValue = 0;      // current RE value -1, 1
        mcp[chip].re[reID].reCounter = 0;    // running count for this re[0,1]
        mcp[chip].re[reID].reAPinGPIO = 0;   // current value reAPin
        mcp[chip].re[reID].reBPinGPIO = 0;   // current value reBPin
        mcp[chip].re[reID].reAPinPrev = 0;   // previous value reAPin
        mcp[chip].re[reID].reBPinPrev = 0;   // previous value reBPin
        mcp[chip].re[reID].reAFlag = 0;      // reAFlag
        mcp[chip].re[reID].reBFlag = 0;      // reBFlag
      } // end of setup encoders

  */

  // ++++++++++++++++++++++++++++++++++++++++++++++++++++
  // this code is adapted from Oleg Mazurov (2011 Mar 30)
  // Ref: www_circuitsathome.com/mcu/rotary-encoder-
  //            interrupt-service-routine-for-avr-micros/
  // ++++++++++++++++++++++++++++++++++++++++++++++++++++

  // This code determines the direction of turn and cumulative count
  //     for up to 8 rotary encoders on 1 or more port expanders.
  // A turn of a rotary encoder on a port expander
  //     causes a series of pin change interrupts
  //     that triggers this code for each interrupt.
  // For the KY040 Rotary Encoder:
  //    pin CLK ==> RE Pin reAPin, pin DT ==> RE Pin reBPin
  //    MCP Pin INTA ==> Arduino aPin, MCP Pin INTB ==> Arduino bPin

  static const int8_t enc_states [] PROGMEM =
  {0, -1, 1, 0, 1, 0, 0, -1, -1, 0, 0, 1, 0, 1, -1, 0}; //encoder lookup table

  // cycle through all port expanders
  for (uint8_t chip = 0; chip < EXPANDERS; chip++) {

    // cycle through each of 8 rotary encoders
    for (uint8_t reID = 0; reID < ENCODERS; reID++) {

      // recall retained value of old_AB
      uint8_t old_AB = mcp[chip].re[reID].reAB;
      uint8_t encport = 0;
      int8_t dir;

      // get the expander:encoder:pinA, pinB values at time of interrupt
      // eg, Port_A [0] & Port_B [0], Port_A [5] & Port_B [5]
      mcp[chip].re[reID].reAPinGPIO = bitRead(mcp[chip].mcpAGPIO, reID);
      mcp[chip].re[reID].reBPinGPIO = bitRead(mcp[chip].mcpBGPIO, reID);

      // check if this RE sent a signal, if not, ignore it
      if ((mcp[chip].re[reID].reAPinGPIO > 0) || (mcp[chip].re[reID].reBPinGPIO > 0)) {
        old_AB <<= 2; //remember previous RE state and shift-left by 2 bits
        // copy encoder pin values to encport bits 1,0
        if (mcp[chip].re[reID].reAPinGPIO > 0) bitSet(encport, 0);
        if (mcp[chip].re[reID].reBPinGPIO > 0) bitSet(encport, 1);
        //copy bits 1,0 to old_AB
        old_AB |= encport & 0x03;
        // use index to obtain direction and state
        dir = pgm_read_byte(&(enc_states[( old_AB & 0x0f )]));
        //check if at detent and transition is valid
        // dir=1 CCW, dir=-1 CW
        if ( dir && ( encport == 3 )) {
          mcp[chip].re[reID].reValue = dir;
          mcp[chip].re[reID].reCounter = mcp[chip].re[reID].reCounter - dir;

          reFlag = 1; // joyReport data changed flag
          // place the 2-expander 8-encoder data into
          //       joyReport [ expander [port a] or [port b] ]
          switch (chip) {
            case 0: // Expander 0
              switch (dir) {
                case 1: // reBPin
                  bitSet(joyReport.btnArray[1], reID);
                  break;
                case -1: // reAPin
                  bitSet(joyReport.btnArray[0], reID);
                  break;
                default:
                  break;
              }
              break;
            case 1: // Expander 1
              switch (dir) {
                case 1: // reBPin
                  bitSet(joyReport.btnArray[3], reID);
                  break;
                case -1: // reAPin
                  bitSet(joyReport.btnArray[2], reID);
                  break;
                default:
                  break;
              }
              break;
            default:
              break;
          }

#ifdef DEBUG
          /* post encoder CW/CCW and Count */
          Serial.print ("\n MCP["); Serial.print (chip); Serial.print ("]");
          Serial.print (", RE["); Serial.print (reID); Serial.print ("]");
          //Serial.print (", encport: "); print8Bits (encport);
          //Serial.print (", old_AB: "); print8Bits (old_AB);
          //Serial.print (", enc_states["); Serial.print (old_AB & 0x0f); Serial.print ("]: ");
          Serial.print (" dir: "); Serial.print (dir);
          if ( dir == 1 ) {
            Serial.print (", CCW");
          }
          else {
            Serial.print (", CW");
          }
          Serial.print (", Count "); Serial.print ( mcp[chip].re[reID].reCounter);
#endif
        } // end if (dir...
      } // end if ((mcp[chip].re[reID].reAPinGPIO...

      // retain settings for this encoder for next indent comparison
      mcp[chip].re[reID].reAB = old_AB;

    } // end encoder  for (uint8_t reID...
  } // end expander  for (uint8_t chip...
} // end code adapted from Oleg Mazurov (2011 Mar 30)


// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// 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
  // skip function interruptMCP() when using encoders
  // if (mcpFlag > 0) {
  //   interruptMCP(); // write the interrupt pins to joyReport
  //   Serial.println("");
  // }

  // process Rotary Encoders
  if (mcpFlag > 0) {
    processRE();
  }


  /* 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 = 0; // 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 joyReport
    reFlag = 0;
    joyReport.btnArray[0] = 0b0;
    joyReport.btnArray[1] = 0b0;
    joyReport.btnArray[2] = 0b0;
    joyReport.btnArray[3] = 0b0;
    joyReport.btnArray[4] = 0b0;


    // 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;
          mcp[chip].flagAGPIO = 0;
          mcp[chip].flagBGPIO = 0;
          break;
        case 1:
          //mcpReading = Bank1.getInterruptValue(); // this is not necessary
          mcp[chip].flagMCPA = 0;
          mcp[chip].flagMCPB = 0;
          mcp[chip].flagAGPIO = 0;
          mcp[chip].flagBGPIO = 0;
          break;
        case 2:
          // mcpReading = Bank2.getInterruptValue(); // this is not necessary
          mcp[chip].flagMCPA = 0;
          mcp[chip].flagMCPB = 0;
          mcp[chip].flagAGPIO = 0;
          mcp[chip].flagBGPIO = 0;
          break;
        case 3:
          // mcpReading = Bank3.getInterruptValue(); // this is not necessary
          mcp[chip].flagMCPA = 0;
          mcp[chip].flagMCPB = 0;
          mcp[chip].flagAGPIO = 0;
          mcp[chip].flagBGPIO = 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




(2017-Jan-30)