Saturday, February 4, 2017

FSX Learning Center Flight Training Download

FSX Learning Center

MS Flight Simulator 2004, 2009, and FSX contained valuable flight ground school training from Microsoft FSX Learning Center. Those file links are still on a 2009 Microsoft web site, but the links do not work.

The Learning Center Ground School files are contained in two directories of HTM and BMP files, and they display well on Google Chrome or Safari. The html files use frames, so they do not display well if the files are copied to an iPad (I could not make that work for off-line study). These files have links to the "Fly This Lesson Now" which do not work without the FSX software. The start file is /Uires/lc01.htm which can be aliased and renamed if desired.

I have put the files on Google Drive for download. I hope I am not breaking any copyright laws by doing so. I saw that people were not able to get the training in the new Steam FSX, so hopefully this might help. You might get a warning that the file is too big (80 Mb) for Google Drive to check for viruses. The file I published on Google Drive was virus free. You may want to scan it just to make sure it has not been infected. Good flying.

Lowell


Thursday, February 2, 2017

Arduino with 8 KY040 Rotary Encoders and 16 Switches

Adding 8 Rotary Encoders and 16 Switches to an Arduino Mega 2560

In the last blog, "Arduino with 8 KY040 Rotary Encoders", I made passing reference that switches could be added into the sketch. The Microsoft flight simulator software, FSX, can accept data from an Arduino when it is programmed as a HID-USB joystick with 8-axes and 32 buttons or switches. The joyReport data structure within the sketch holds and conveys the axes, switches, and encoders data through USB to the simulator software on a PC. Eight (8) rotary encoders add 16 switches to the joyReport, so there are 16 additional bits of data that can be added from other pins on the Mega 2560.

The 8 rotary encoders were added to the sketch by setting 2 8-bit ports (Port B and Port K) as pin-change-interrupts, effectively adding 16 additional hardware interrupts to the Mega 2560. The analog pins A0-A7 are used in the sketch for axis sensors (potentiometers). The 6 external hardware interrupts on the Mega 2560 are pins 21, 20, 19, 18 (INT0, INT1, INT2, INT3) and, pins 2 and 3 (INT4, INT5), which can be used for additional switches that require immediate attention of the software. Thirty-eight (38) remaining digital pins can be used for switches, but without a hardware interrupt, polling is required to cycle through the pins to determine if a pin has changed state. In FSX, buttons and switches are generally used to turn on and off lights or other airplane functions that are slow human-speed activities, so polling of switch states will not degrade simulator performance to any significant degree. Interrupts, rather than polling, were used for encoders since the timing of the A-B switching of the encoder was critical to reading the encoder accurately. Since changed switch states happens so rarely (relative to CPU speed), all the switch states can be accumulated into the joyReport data structure, and that data structure can then be transmitted when any one or more switches has changed HIGH-LOW or LOW-HIGH. Any switch that stays ON or OFF will have its value transmitted in the joyReport each time the joyReport is sent out through USB. The FSX/FSUIPC software on the PC has to deal with the repetition of switch setting values.

Sketch Design --

In the sketch, the existing Axes (pins A0-A7) will be retained. Also the 8 rotary encoders which write their 16 values into joyReport bytes [0] and [1] will be retained.
Sixteen digital pins will be added to populate joyReport bytes [2] and [3]. And for demonstration, 2 of 6 hardware interrupts will be used to capture switch values, and those values will be written to joyReport byte [4] bits [1,0]. FSX does not use the data from byte 4, but the programming will have been provided for anyone wanting an example of how to use those interrupts.

Sketch --


/*

   Sketch - Arduino_ISR_KY040_Mega2560_16_8X40Joystick_Oleg

   Lowell Bahner
   February, 2017

   This code sends joyStick data when rotary encoders change values.

   This code adds 16 digital pins for switches plus 2 hardware interrupt pins
   for switches.

   This code uses 16 pins on 2 ports on a Mega2560 for 8 KY040 rotary encoders.
   Other encoders may also work but have not been tested with this code.
   Each encoder pin is on a separate port (Port B and Port K)
   Ref: www_atmel.com/Images/Atmel-2549-8-bit-AVR-Microcontroller-ATmega640-
               1280-1281-2560-2561_datasheet.pdf

   8 Rotary Encoders are connected to the 16 Arduino pins. Each encoder CLK "A" pin
   is connected to a successive Mega Port B pin [0,1,...7] and the encoder DT "B" pin
   is connected to the Mega Port K pin [A8,A9,...A15].

   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 2 ISR's (one per port) to sense port interrupts on the two Mega ports.

   A one detent turn of an encoder takes either the "A" or "B" pin LOW (ground)
   which triggers the respective Arduino port interrupt. That interrupt triggers
   the Arduino ISR code which 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.

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

   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 Mega 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 Ard pin 53 (on port B), and Rotary Encoder #0, pin DT "B" wired
      to Ard pin A8 (on port K).

   Arduino digital pins are set as pin-change-interrupt INPUT_PULLUP
      which trigger the port interrupt when state changes HIGH-LOW or LOW-HIGH.

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

   Switches are wired to pins 34-49 to ground and will display as joystick
   pins 16-31. Switches can also be wired to hardware interrupt pins 21 and 20,
   however those switches will not be sensed if used with FSX.

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

*/

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

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

const byte ENCODERS = 8;        // the number of rotary encoders, 4 per pciPort
const byte NUM_BUTTONS = 40;    // do not change this value.
const byte NUM_AXES = 8;        // do not change this value.

// ======================================================
// add 2 hardware interrupts
const byte HWINTS = 2;
uint8_t hwArray[HWINTS] = {21, 20};
uint8_t hwFlag[HWINTS];

// ======================================================
// add 16 switches
const byte SWITCHES = 16;
uint8_t swArray[SWITCHES] = {34, 35, 36, 37, 38, 39, 40, 41,
                             42, 43, 44, 45, 46, 47, 48, 49
                            };
uint8_t swFlag = 0; // switch data placed into joyReport flag

// ======================================================
// define joyReport
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; // axis data placed into joyReport flag

// ======================================================
// add 8 encoders
volatile uint8_t reFlag = 0;          // encoder data placed into joyReport flag
volatile uint8_t pciPortReadFlag [3]; // flag if pciPort was read
volatile uint8_t pciPortRead [3];     // save the port interrupt pin value byte

// 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

  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)

  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;

// Use these arrays for encoder A pins on Port B and B pins on Port K interrupts
volatile RE re[ENCODERS] = {
  {53, A8},
  {52, A9},
  {51, A10},
  {50, A11},
  {10, A12},
  {11, A13},
  {12, A14},
  {13, A15},
}; // end of encoders


// Mega 2560 External Interrupt pins, 2, 3, 18, 19, 20, & 21

// Mega 2560 Pin-Change-Interupt Ports and pins
// PB7...PB4 DPins 13...10 and PB3...PB0 DPins 50...53 (PCINT7...0)
// PK7...PK0 APins A15...A8 (PCINT23...16)
// PJ1, PJ0  DPins D14, D15 not used

// 8 Rotary Encoders are possible with 16 PCI pins across two 8-pin ports PB and PK
// Mega 2560 PCI pins: PB7...PB0 D13...D10 & D50...D53 (no SPI possible)
// Mega 2560 PCI pins: PK7...PK0 A15...A8

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


// ++++++++++++++++++++++++++++++++++++++++++++++++++++
//   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 hw0Interrupt();
void hw1Interrupt();
void setup();
void sendJoyReport(struct joyReport_t *report);
void processRE(uint8_t pciPort);
void loop ();
void print8Bits(uint8_t myByte);
void print16Bits(uint16_t myWord);
void crPrintHEX(unsigned long DATA, unsigned char numChars);

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


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

// Rotary encoder pin change triggers "pciPort" Arduino ISR.
// pciPort==0 is Ard Port B, pciPort==1 is not used, pciPort==2 is Ard Port K.
// Once the ISR finishes, interrupts are cleared, then loop() processes the pins.
// The pin data are written to the joyReport data structure for sending out USB.

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

  if (pciPort == 0) {
    pciPortRead [pciPort] = ~PINB;
    pciPortReadFlag [pciPort] = 1;
  } else if (pciPort == 1) {
    pciPortRead [pciPort] = 0b0;
    pciPortReadFlag [pciPort] = 1;
  } else if (pciPort == 2) {
    pciPortRead [pciPort] = ~PINK;
    pciPortReadFlag [pciPort] = 1;
  }

  for (uint8_t reID = 0; reID < ENCODERS; reID++) {
    if (re[reID].pciPortIntA == pciPort) {
      if ((digitalRead(re[reID].reAPin) == LOW)) {
        re[reID].reAFlag = 1;
#ifdef DEBUG
        /*
        // print what pins are changing and in what order of interrupt
        Serial.print ("\n, reAFlag: "); Serial.print (re[reID].reAFlag);
        Serial.print (", pciPortRead [pciPort]: "); print8Bits (pciPortRead [pciPort]);
        */
#endif
      }
    }
    if (re[reID].pciPortIntB == pciPort) {
      if ((digitalRead(re[reID].reBPin) == LOW)) {
        re[reID].reBFlag = 1;
#ifdef DEBUG
        /*
        // print what pins are changing and in what order of interrupt
        Serial.print ("\n, reBFlag: "); Serial.print (re[reID].reBFlag);
        Serial.print (", pciPortRead [pciPort]: "); print8Bits (pciPortRead [pciPort]);
        */
#endif
      }
    }
  }     // end of for each encoder
} // end of checkForPinChange

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

// handle pin change interrupt for Mega 2560 pins 53-50 & 10-13
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 not used
//ISR (PCINT1_vect)
//{
//  cli(); //stop interrupts happening before we read pin values
//  checkForPinChange (PCIE1);
//  sei(); //restart interrupts
//}  // end of PCINT1_vect

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

// handle pin change interrupt for Mega 2560 pins A8-A15
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

// ++++++++++++++++++++++++++++++++++++++++++++++++++++
//   hw0Interrupt()
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

// hwArray[0] pin triggers this hardware interrupt
void hw0Interrupt()
{
  //Serial.println ("hw0Interrupt() ...");
  cli(); //stop interrupts happening before we read pin values
  hwFlag[0] = 0; // clear the flag
  if (digitalRead(hwArray[0] < 1)) { // pin is LOW
    hwFlag[0] = 1; // set the flag
  }
  sei(); //restart interrupts
}  // end of hw0Interrupt()

// ++++++++++++++++++++++++++++++++++++++++++++++++++++
//   hw1Interrupt()
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

// hwArray[1] pin triggers this hardware interrupt
void hw1Interrupt()
{
  //Serial.println ("hw1Interrupt() ...");
  cli(); //stop interrupts happening before we read pin values
  hwFlag[1] = 0; // clear the flag
  if (digitalRead(hwArray[1] < 1)) { // pin is LOW
    hwFlag[1] = 1; // set the flag
  }
  sei(); //restart interrupts
}  // end of hw1Interrupt()

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

void setup() {

  Serial.begin (115200);

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

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

  for (uint8_t reID = 0; reID < ENCODERS; reID++)
  {

    // set interrupt pins to INPUT_PULLUP
    pinMode (re[reID].reAPin, INPUT_PULLUP);
    pinMode (re[reID].reBPin, INPUT_PULLUP);

    // Create a Mask with the pin bit = 1
    re[reID].aBitMask = digitalPinToBitMask (re[reID].reAPin);
    re[reID].bBitMask = digitalPinToBitMask (re[reID].reBPin);

    // Which ISR for each interrupt pin (0=PCIE0, 1=PCIE1, 2=PCIE2)
    re[reID].pciPortIntA = digitalPinToPCICRbit (re[reID].reAPin);
    re[reID].pciPortIntB = digitalPinToPCICRbit (re[reID].reBPin);

    // Activate this pin-change interrupt bit (eg. PCMSK0, PCMSK1, PCMSK2)
    /*   Ref: thewanderingengineer.com/2014/08/11/arduino-pin-change-interrupts/
      // Example 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
    */

    PCMSK0 |= 0b11111111;    // turn on port b pins PCINT0 to PCINT7
    PCMSK1 |= 0b00000000;    // not used
    PCMSK2 |= 0b11111111;    // turn on port k pins PCINT16 to PCINT23

    // Enable this pin-change interrupt
    /*   Ref: thewanderingengineer.com/2014/08/11/arduino-pin-change-interrupts/
      // Example 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 (re[reID].reAPin));
    //Serial.print ("\nPCICR A: "); print8Bits (PCICR);
    PCICR |= bit (digitalPinToPCICRbit (re[reID].reBPin));
    //Serial.print (", PCICR B: "); print8Bits (PCICR);

#ifdef DEBUG
    // examine the Arduino ISR setup
    Serial.print ("\n\nMega 2560: ");
    Serial.print ("\n...re[reID].reAPin: "); Serial.print (re[reID].reAPin);
    Serial.print (", re[reID].reBPin: "); Serial.print (re[reID].reBPin);
    Serial.print (", re[reID].aBitMask: "); print8Bits(re[reID].aBitMask);
    Serial.print (", re[reID].bBitMask: "); print8Bits(re[reID].bBitMask);
    Serial.print ("\n...re[reID].pciPortIntA: "); Serial.print (re[reID].pciPortIntA);
    Serial.print (", re[reID].pciPortIntB: "); Serial.print (re[reID].pciPortIntB);
    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 port
    for (uint8_t reID = 0; reID < ENCODERS; reID++) {
      //re[reID].reAPin = RE pin for the "A" side interrupt
      //re[reID].reBPin = RE pin for the "B" side interrupt
      re[reID].reAB = 3;         // old_AB
      re[reID].reValue = 0;      // current RE value -1, 1
      re[reID].reCounter = 0;    // running count for this re[0,1]
      re[reID].reAPinGPIO = 0;   // current value reAPin
      re[reID].reBPinGPIO = 0;   // current value reBPin
      //re[reID].reAPinPrev = 0;   // previous value reAPin
      //re[reID].reBPinPrev = 0;   // previous value reBPin
      re[reID].reAFlag = 0;      // reAFlag
      re[reID].reBFlag = 0;      // reBFlag
    } // end of setup encoders

  } // end of Arduino interrupts for each expander

  // -------------------------------------------------
  // set up two hardware interrupt pins (Mega 2560 = 21, 20, 19, 18, 2, 3)
  for (uint8_t hw = 0; hw < HWINTS; hw ++) {
    pinMode (hwArray[hw], INPUT_PULLUP);
    hwFlag[hw] = 0; // clear flag
  }
  attachInterrupt(digitalPinToInterrupt(hwArray[0]), hw0Interrupt, CHANGE);
  attachInterrupt(digitalPinToInterrupt(hwArray[1]), hw1Interrupt, CHANGE);

  // -------------------------------------------------
  // set up 16 switch pins
  for (uint8_t sw = 0; sw < SWITCHES; sw++) {
    pinMode (swArray[sw], INPUT_PULLUP);
  }

}  // end of setup


// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// 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
  if (memcmp( report, &prevjoyReport, sizeof( joyReport_t ) ) != 0)
  {
    //Serial.write((uint8_t *)report, sizeof(joyReport_t));
    memcpy ( &prevjoyReport, report, sizeof( joyReport_t ) );
    //}
    // 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(" ");
    }
  } else {
    //Serial.print("\n ...No Change in joyReport...");
  }

#endif
}

// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// function processRE()
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

void processRE(uint8_t pciPort) {

  /*
   * Process the encoders on each expander
   * 4 ENCODERS per Arduino Port Interrupt
   *
      for (uint8_t reID = 0; reID < ENCODERS; reID++) {
        re[reID].reAPin = read array;    // which RE pin for the "A" side interrupt
        re[reID].reBPin = read array;    // which RE pin for the "B" side interrupt
        re[reID].reValue = 0;      // current RE value -1, 1
        re[reID].reCounter = 0;    // running count for this re[0,1]
        re[reID].reAPinGPIO = 0;   // current value reAPin
        re[reID].reBPinGPIO = 0;   // current value reBPin
        re[reID].reAPinPrev = 0;   // previous value reAPin
        re[reID].reBPinPrev = 0;   // previous value reBPin
        re[reID].reAFlag = 0;      // reAFlag
        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 Arduino Mega 2560 using 2 8-pin ports.
  // A turn of a rotary encoder on a port
  //     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
  // The reAPin goes to port B [0...7] pins (53, 52, 51, 50, 10, 11, 12, 13)
  // The reBPin goes to port K [0...7] pins (A8, A9, A10, A11, A12, A13, A14, A15)
  // This arrangement gives a separate port interrupt for each pin of an encoder

  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 each of 8 rotary encoders
  // with pin A on Port B and pin B on Port K
  for (uint8_t reID = 0; reID < ENCODERS; reID++) {

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

    // get the expander:encoder:pinA, pinB values at time of interrupt
    // eg, Port_B [0] & Port_K [0], Port_B [5] & Port_K [5]

    // for encoders with pin A on Port B and pin B on Port K
    re[reID].reAPinGPIO = bitRead(pciPortRead [re[reID].pciPortIntA], reID);
    re[reID].reBPinGPIO = bitRead(pciPortRead [re[reID].pciPortIntB], reID);

    // check if this RE sent a signal, if not, ignore it
    if ((re[reID].reAPinGPIO > 0) || (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 (re[reID].reAPinGPIO > 0) bitSet(encport, 0);
      if (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 )) {
        re[reID].reValue = dir;
        re[reID].reCounter = re[reID].reCounter - dir;

        reFlag = 1; // joyReport data changed flag
        // place the 8-encoder data into
        //       joyReport [ 0 and 1 ]

        if (dir < 0) bitSet(joyReport.btnArray[1], reID); // CW data
        if (dir > 0) bitSet(joyReport.btnArray[0], reID); // CCW data

        // btnArray[2] and btnArray[3] are available for use with 16 switches
        // connected to other Mega 2560 pins (code has to be added)

#ifdef DEBUG

        Serial.print ("\n 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 ( re[reID].reCounter);
#endif
      } // end if (dir...
    } // end if ((re[reID].reAPinGPIO...

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

  } // end encoder  for (uint8_t reID...

} // end code adapted from Oleg Mazurov (2011 Mar 30)


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

void loop ()
{

  // This code runs Arduino Mega 2560 pin-change-interrupt ISR's
  //    and then takes action (checkForPinChange()) for the Arduino pins that
  //    went LOW or HIGH. Port B and Port K pins are set as pin-change-interrupts.

  // checkForPinChange() flags that a pin interrupt (re[reID].reAFlag>0) has occurred
  //    and each pciPortReadFlag[pciPort] is set if a port pin
  //    changed state.

  // checkForPinChange() reads the inverse value of the port pins
  //   (pciPortRead[pciPort] = ~PINB;) and  (pciPortRead[pciPort] = ~PINK;)
  // processRE(pciPort) writes the encoder values into joyReport[0], [1].

  // process Rotary Encoders
  for (uint8_t pciPort = 0; pciPort < 3; pciPort++) {
    if (pciPortReadFlag [pciPort] > 0) {
      processRE(pciPort);
    }
  }

  // process the switches for ON and OFF
  swFlag = 1;

  // process switches attached to hardware interrupts
  for (uint8_t hw = 0; hw < HWINTS; hw++) {
    if (hwFlag[hw] > 0) { // test if interrupt occurred with pin LOW
      if (digitalRead(hwArray[hw]) < 1) {  // check if pin is still LOW
        bitSet(joyReport.btnArray[4], hw); // switch is ON
      }
    }
  }

  // process 16 switches and place their values into joyReport[2], [3].
  // poll each switch to determine its state
  // sendJoyReport() will determine if there is a change in one or more switches
  uint8_t bt = 0; // bit number
  uint8_t pn = 2; // port number
  for (uint8_t sw = 0; sw < SWITCHES; sw++) {
    if (digitalRead(swArray[sw]) < 1) {
      if (sw < 8) {
        bt = sw; // 0...7
        pn = 2;
      } else {
        bt = sw - 8; // 8...15 ==> 0...7
        pn = 3;
      }
      bitSet(joyReport.btnArray[pn], bt);
    }
  }

  /* 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 ((swFlag > 0) || (reFlag > 0) || (sendFlag > 0)) {
    //Send Data to HID
    sendJoyReport(&joyReport);

    // clear the joyReport
    sendFlag = 0;
    reFlag = 0;
    swFlag = 0;
    joyReport.btnArray[0] = 0b0;
    joyReport.btnArray[1] = 0b0;
    joyReport.btnArray[2] = 0b0;
    joyReport.btnArray[3] = 0b0;
    joyReport.btnArray[4] = 0b0;
  }
  // clear pci port read flags
  for (uint8_t pciPort = 0; pciPort < 3; pciPort++) {
    pciPortReadFlag [pciPort] = 0;
  }

  //}
  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


(February 2, 2017)


Wednesday, February 1, 2017

Arduino with 8 KY040 Rotary Encoders

Adding 8 Rotary Encoders to an Arduino Mega 2560

A rotary encoder requires two pins to be connected to two digital input pins on an Arduino so that the software can determine the direction of turn of the encoder and increment or decrement a running count. A Mega 2560 has two 8-pin ports (B and K) that can be programmed as pin-change-interrupts for use with rotary encoders. The sketch provided below will accommodate 8 rotary encoders on an Arduino Mega 2560.

An earlier blog, "Arduino and KY040 Rotary Encoder (January 10, 2017)" provided a sketch which provided a report of microseconds between A-B switch changes as an encoder was rotated. That sketch provided a good understanding of what was happening inside the KY040, as far as which pin (A or B) changed state first (which determines the direction of turn) and how many extraneous toggles of the A or B switches were being made during a one-detent rotation.

A subsequent blog, "Arduino and FSX (Part 6) (January 23, 2017)" provided a sketch that added 32 pins for buttons or switches to an Arduino using MCP23S17 Port Expanders. That sketch was modified in "Arduino and FSX (Part 7)" to use interrupts that were mirrored and open-drain to provide 32 pins for buttons or switches. The next blog in that series "Arduino and FSX (Part 8)" provided a sketch for adding 16 rotary encoders to an Arduino with two 16-pin MCP23S17 Port Expanders. The sketch incorporated pin-change-interrupts on both the MCP23S17 Port Expanders and the Arduino.

The sketch provided below eliminates the use of the Port Expanders by using the Mega 2560 which has two 8 pin-change-interrupt ports. While each port expander provides 16 pins for 8 rotary encoders, the Mega 2560 maxes out with 8 encoders.

If you need more than 8 encoders, build a board with multiple MCP23S17 Port Expanders. Eight Port Expanders can be used together using just 4 SPI address pins to provide 64 rotary encoders.

Hardware --

The hardware used with this sketch is straightforward: Arduino Mega 2560, 8 (KY040) rotary encoders, wiring from ground and +5V, and encoder pins A (CLK) and B (DT) to Arduino pins {53, A8}, {52, A9}, {51, A10}, {50, A11}, {10, A12}, {11, A13}, {12, A14}, and {13, A15}.

Sketch Software --

The sketch senses a HIGH-LOW pin change on either or both pin A and B of each rotary encoder. Each input pin is programmed as pin-change-interrupt, so when the pin state changes, a port interrupt signal triggers an ISR (interrupt service routine). While the interrupt is held, port pin values are read (saved in memory) and flags are set to indicate what caused the interrupt. Then the interrupts are cleared. The CPU can then go back to doing loop() processing or handle another interrupt.

One detent turn clockwise of the encoder causes the A pin to change from HIGH-LOW, then the B pin changes from HIGH-LOW, then the A pin changes LOW-HIGH, and finally the B pin changes LOW-HIGH, to complete the turn. A counter-clockwise turn causes the B pin to change first, then the A pin, and so on.

Within loop(), between interrupts, the previously set flags are used to determine how to process the pin state data that was captured during the port reads. The data from successive interrupts are accumulated to determine the direction of turn of the encoder and to increment or decrement a running sum for each encoder.

Sketch --



/*

   Sketch - Arduino_ISR_KY040_Mega2560_02_8X40Joystick_Oleg

   Lowell Bahner
   February, 2017

   This code sends joyStick data when rotary encoders change values.

   This code uses 16 pins on 2 ports on a Mega2560 for 8 KY040 rotary encoders.
   Other encoders may also work but have not been tested with this code.
   Each encoder pin is on a separate port (Port B and Port K)
   Ref: www_atmel.com/Images/Atmel-2549-8-bit-AVR-Microcontroller-ATmega640-
               1280-1281-2560-2561_datasheet.pdf

   8 Rotary Encoders are connected to the 16 Arduino pins. Each encoder CLK "A" pin
   is connected to a successive Mega Port B pin [0,1,...7] and the encoder DT "B" pin
   is connected to the Mega Port K pin [A8,A9,...A15].

   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 2 ISR's (one per port) to sense port interrupts on the two Mega ports.

   A one detent turn of an encoder takes either the "A" or "B" pin LOW (ground)
   which triggers the respective Arduino port interrupt. That interrupt triggers
   the Arduino ISR code which 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.

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

   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 Mega 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 Ard pin 53 (on port B), and Rotary Encoder #0, pin DT "B" wired
      to Ard pin A8 (on port K).

   Arduino digital pins are set as pin-change-interrupt INPUT_PULLUP
      which trigger the port interrupt when state changes HIGH-LOW or LOW-HIGH.

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

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

*/

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

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

const byte ENCODERS = 8;        // 8 rotary encoders on 2 pciPorts
const byte NUM_BUTTONS = 40;    // do not change this value.
const byte NUM_AXES = 8;        // 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

volatile uint8_t reFlag = 0;          // only send RE data when joyReport RE data change
volatile uint8_t pciPortReadFlag [3]; // flag if pciPort was read
volatile uint8_t pciPortRead [3];     // save the port interrupt pin value byte

// 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

  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)

  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;

// Use these arrays for encoder A pins on Port B and B pins on Port K interrupts
volatile RE re[ENCODERS] = {
  {53, A8},
  {52, A9},
  {51, A10},
  {50, A11},
  {10, A12},
  {11, A13},
  {12, A14},
  {13, A15},
}; // end of encoders


// Mega 2560 External Interrupt pins, 2, 3, 18, 19, 20, & 21 (not used in this code)

// Mega 2560 Pin-Change-Interupt Ports and pins
// PB7...PB4 DPins 13...10 and PB3...PB0 DPins 50...53 (PCINT7...0)
// PK7...PK0 APins A15...A8 (PCINT23...16)

// 8 Rotary Encoders are possible with 16 PCI pins across two 8-pin ports PB and PK
// Mega 2560 PCI pins: PB7...PB0 D13...D10 & D50...D53 (no SPI possible)
// Mega 2560 PCI pins: PK7...PK0 A15...A8

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


// ++++++++++++++++++++++++++++++++++++++++++++++++++++
//   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 sendJoyReport(struct joyReport_t *report);
void processRE(uint8_t pciPort);
void loop ();
void print8Bits(uint8_t myByte);
void print16Bits(uint16_t myWord);
void crPrintHEX(unsigned long DATA, unsigned char numChars);

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


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

// Rotary encoder pin change triggers "pciPort" Arduino ISR.
// pciPort==0 is Ard Port B, pciPort==1 is not used, pciPort==2 is Ard Port K.
// Once the ISR finishes, interrupts are cleared, then loop() processes the pins.
// The pin data are written to the joyReport data structure for sending out USB.

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

  if (pciPort == 0) {
    pciPortRead [pciPort] = ~PINB;
    pciPortReadFlag [pciPort] = 1;
  } else if (pciPort == 1) {
    pciPortRead [pciPort] = 0b0;
    pciPortReadFlag [pciPort] = 1;
  } else if (pciPort == 2) {
    pciPortRead [pciPort] = ~PINK;
    pciPortReadFlag [pciPort] = 1;
  }

  for (uint8_t reID = 0; reID < ENCODERS; reID++) {
    if (re[reID].pciPortIntA == pciPort) {
      if ((digitalRead(re[reID].reAPin) == LOW)) {
        re[reID].reAFlag = 1;
#ifdef DEBUG
        /*
        // print what pins are changing and in what order of interrupt
        Serial.print ("\n, reAFlag: "); Serial.print (re[reID].reAFlag);
        Serial.print (", pciPortRead [pciPort]: "); print8Bits (pciPortRead [pciPort]);
        */
#endif
      }
    }
    if (re[reID].pciPortIntB == pciPort) {
      if ((digitalRead(re[reID].reBPin) == LOW)) {
        re[reID].reBFlag = 1;
#ifdef DEBUG
        /*
        // print what pins are changing and in what order of interrupt
        Serial.print ("\n, reBFlag: "); Serial.print (re[reID].reBFlag);
        Serial.print (", pciPortRead [pciPort]: "); print8Bits (pciPortRead [pciPort]);
        */
#endif
      }
    }
  }     // end of for each encoder
} // end of checkForPinChange

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

// handle pin change interrupt for Mega 2560 pins 53-50 & 10-13
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 not used
//ISR (PCINT1_vect)
//{
//  cli(); //stop interrupts happening before we read pin values
//  checkForPinChange (PCIE1);
//  sei(); //restart interrupts
//}  // end of PCINT1_vect

// ++++++++++++++++++++++++++++++++++++++++++++++++++++
//   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() {

  Serial.begin (115200);

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

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

  for (uint8_t reID = 0; reID < ENCODERS; reID++)
  {

    // set interrupt pins to INPUT_PULLUP
    pinMode (re[reID].reAPin, INPUT_PULLUP);
    pinMode (re[reID].reBPin, INPUT_PULLUP);

    // Create a Mask with the pin bit = 1
    re[reID].aBitMask = digitalPinToBitMask (re[reID].reAPin);
    re[reID].bBitMask = digitalPinToBitMask (re[reID].reBPin);

    // Which ISR for each interrupt pin (0=PCIE0, 1=PCIE1, 2=PCIE2)
    re[reID].pciPortIntA = digitalPinToPCICRbit (re[reID].reAPin);
    re[reID].pciPortIntB = digitalPinToPCICRbit (re[reID].reBPin);

    // Activate this pin-change interrupt bit (eg. PCMSK0, PCMSK1, PCMSK2)
    /*   Ref: thewanderingengineer.com/2014/08/11/arduino-pin-change-interrupts/
      // Example 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
    */

    PCMSK0 |= 0b11111111;    // turn on port b pins PCINT0 to PCINT7
    PCMSK1 |= 0b00000000;    // not used
    PCMSK2 |= 0b11111111;    // turn on port k pins PCINT16 to PCINT23

    // Enable this pin-change interrupt
    /*   Ref: thewanderingengineer.com/2014/08/11/arduino-pin-change-interrupts/
      // Example 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 (re[reID].reAPin));
    //Serial.print ("\nPCICR A: "); print8Bits (PCICR);
    PCICR |= bit (digitalPinToPCICRbit (re[reID].reBPin));
    //Serial.print (", PCICR B: "); print8Bits (PCICR);

#ifdef DEBUG
    // examine the Arduino ISR setup
    Serial.print ("\n\nMega 2560: ");
    Serial.print ("\n...re[reID].reAPin: "); Serial.print (re[reID].reAPin);
    Serial.print (", re[reID].reBPin: "); Serial.print (re[reID].reBPin);
    Serial.print (", re[reID].aBitMask: "); print8Bits(re[reID].aBitMask);
    Serial.print (", re[reID].bBitMask: "); print8Bits(re[reID].bBitMask);
    Serial.print ("\n...re[reID].pciPortIntA: "); Serial.print (re[reID].pciPortIntA);
    Serial.print (", re[reID].pciPortIntB: "); Serial.print (re[reID].pciPortIntB);
    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 port
    for (uint8_t reID = 0; reID < ENCODERS; reID++) {
      //re[reID].reAPin = RE pin for the "A" side interrupt
      //re[reID].reBPin = RE pin for the "B" side interrupt
      re[reID].reAB = 3;         // old_AB
      re[reID].reValue = 0;      // current RE value -1, 1
      re[reID].reCounter = 0;    // running count for this re[0,1]
      re[reID].reAPinGPIO = 0;   // current value reAPin
      re[reID].reBPinGPIO = 0;   // current value reBPin
      //re[reID].reAPinPrev = 0;   // previous value reAPin
      //re[reID].reBPinPrev = 0;   // previous value reBPin
      re[reID].reAFlag = 0;      // reAFlag
      re[reID].reBFlag = 0;      // reBFlag
    } // end of setup encoders

  } // end of Arduino interrupts for each expander

}  // end of setup


// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// 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 processRE()
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

void processRE(uint8_t pciPort) {

  /*
   * Process the encoders on each expander
   * 4 ENCODERS per Arduino Port Interrupt
   *
      for (uint8_t reID = 0; reID < ENCODERS; reID++) {
        re[reID].reAPin = read array;    // which RE pin for the "A" side interrupt
        re[reID].reBPin = read array;    // which RE pin for the "B" side interrupt
        re[reID].reValue = 0;      // current RE value -1, 1
        re[reID].reCounter = 0;    // running count for this re[0,1]
        re[reID].reAPinGPIO = 0;   // current value reAPin
        re[reID].reBPinGPIO = 0;   // current value reBPin
        re[reID].reAPinPrev = 0;   // previous value reAPin
        re[reID].reBPinPrev = 0;   // previous value reBPin
        re[reID].reAFlag = 0;      // reAFlag
        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 Arduino Mega 2560 using 2 8-pin ports.
  // A turn of a rotary encoder on a port
  //     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
  // The reAPin goes to port B [0...7] pins (53, 52, 51, 50, 10, 11, 12, 13)
  // The reBPin goes to port K [0...7] pins (A8, A9, A10, A11, A12, A13, A14, A15)
  // This arrangement gives a separate port interrupt for each pin of an encoder

  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 each of 8 rotary encoders
  // with pin A on Port B and pin B on Port K
  for (uint8_t reID = 0; reID < ENCODERS; reID++) {

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

    // get the expander:encoder:pinA, pinB values at time of interrupt
    // eg, Port_B [0] & Port_K [0], Port_B [5] & Port_K [5]

    // for encoders with pin A on Port B and pin B on Port K
    re[reID].reAPinGPIO = bitRead(pciPortRead [re[reID].pciPortIntA], reID);
    re[reID].reBPinGPIO = bitRead(pciPortRead [re[reID].pciPortIntB], reID);

    // check if this RE sent a signal, if not, ignore it
    if ((re[reID].reAPinGPIO > 0) || (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 (re[reID].reAPinGPIO > 0) bitSet(encport, 0);
      if (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 )) {
        re[reID].reValue = dir;
        re[reID].reCounter = re[reID].reCounter - dir;

        reFlag = 1; // joyReport data changed flag
        // place the 8-encoder data into
        //       joyReport [ 0 and 1 ]

        if (dir < 0) bitSet(joyReport.btnArray[1], reID); // CW data
        if (dir > 0) bitSet(joyReport.btnArray[0], reID); // CCW data

        // btnArray[2] and btnArray[3] are available for use with 16 switches
        // connected to other Mega 2560 pins (code has to be added)

#ifdef DEBUG

        Serial.print ("\n 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 ( re[reID].reCounter);
#endif
      } // end if (dir...
    } // end if ((re[reID].reAPinGPIO...

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

  } // end encoder  for (uint8_t reID...

} // end code adapted from Oleg Mazurov (2011 Mar 30)


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

void loop ()
{

  // This code runs Arduino Mega 2560 pin-change-interrupt ISR's
  //    and then takes action (checkForPinChange()) for the Arduino pins that
  //    went LOW or HIGH. Port B and Port K pins are set as pin-change-interrupts.

  // checkForPinChange() flags that a pin interrupt (re[reID].reAFlag>0) has occurred
  //    and each pciPortReadFlag[pciPort] is set if a port pin
  //    changed state.

  // checkForPinChange() reads the inverse value of the port pins
  //   (pciPortRead[pciPort] = ~PINB;) and  (pciPortRead[pciPort] = ~PINK;)
  // processRE(pciPort) writes the encoder values into joyReport[0], [1].

  // 16 switches could be added to this code and their values into joyReport[2], [3].

  // process Rotary Encoders
  for (uint8_t pciPort = 0; pciPort < 3; pciPort++) {
    if (pciPortReadFlag [pciPort] > 0) {
      processRE(pciPort);
    }
  }

  /* 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 ((reFlag > 0) || (sendFlag > 0)) {
    //Send Data to HID
    sendJoyReport(&joyReport);

    // clear the joyReport
    sendFlag = 0;
    reFlag = 0;
    joyReport.btnArray[0] = 0b0;
    joyReport.btnArray[1] = 0b0;
    joyReport.btnArray[2] = 0b0;
    joyReport.btnArray[3] = 0b0;
    joyReport.btnArray[4] = 0b0;
  }
  // clear pci port read flags
  for (uint8_t pciPort = 0; pciPort < 3; pciPort++) {
    pciPortReadFlag [pciPort] = 0;
  }

  //}
  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







(February 1, 2017)