Tuesday, January 10, 2017

Arduino and KY040 Rotary Encoder

Testing the KY040 Rotary Encoder

The KY040 Rotary Encoder is an inexpensive device that is widely used for applications like radio volume or frequency and speed controls. The encoder puts out HIGH-LOW and LOW-HIGH signals alternately from two pins that can convey direction of turn of the encoder and one or more pulses that can adjust the value of a device. A simple example is a volume control on an auto stereo. Unlike potentiometers that turn from minimum to maximum resistance, the encoder can turn an unlimited number of times in either direction. Each small click of one detent of the encoder sends out pulses to the associated electronics.

Using rotary encoders has been tricky for hobbyists--internet forums are full of questions about how to use encoders, how to decode the signals, how to deal with bouncing contacts that generate unwanted random signals, and a host of other issues. Another complication that may not be readily apparent is that different encoders toggle 1, 2, or 4 times for each detent.

This blog contains code written for an Arduino UNO that captures the signals from a KY040 rotary encoder as it triggers the Arduino interrupt pins. A sequential list of interrupts is recorded for examination. The sketch seems to do a pretty good job and helped me have a better understanding of what signals were being generated within the KY040.

The circuit is simple: Pin A (KY040 pin CLK) is connected to Arduino UNO pin D2 which is interrupt pin INT0. Pin B (KY040 pin DT) is connected to Arduino pin D3 which is interrupt pin INT1. The KY040 pin SW is a simple push button switch that connects to Arduino pin D4. A different push button or toggle switch can be used instead of the KY040 switch, with the other side of the switch connected to ground. A push of the switch then takes pin D4 LOW as it is grounded through the switch. The KY040 +V is connected to Arduino +5V and the KY040 Gnd is connected to Arduino ground (0V). The KY040 has built in 10K pull-up resistors built in for both the CLK and DT pins. The code in this blog should work with encoders that do not have the built-in resistors or switch, but has not been tested as such.

The sketch does the following:

1) the sketch initializes and displays some brief instructions on Serial Monitor. It then waits for the turn of the encoder and a button push. The interrupt pins (D2, D3) and switch pin (D4) are biased to HIGH state by "INPUT_PULLUP", as well as the resistors used as pull-ups on the KY040 rotary encoder pins CLK and DT.

2) A turn of the encoder, one or more detents, causes the interrupt pins to run the "interruptA()" or "interruptB()" functions. These functions capture the time of the interrupt in microseconds and the pin that caused the interrupt. Data for each interrupt are sequentially stored in arrays. The interrupts are set to occur on "CHANGE" so that either a HIGH-LOW or LOW-HIGH transition should be recorded.

3) When the push button is pushed, the arrays are processed and printed using the "printHeader()" function. The "printHeader" function will automatically print if 50 interrupts are recorded.

4) The array data can be examined as a report on the Serial Monitor by the user in three ways.

4.a) if the function "noDups2" is run before "printHeader", the report will display only those interrupts that are unique, effectively filtering out noise or contact bounce to a large degree. The index is retained from the original data, so the user can determine at which point in the sequence a particular interrupt occurred.

4.b) if the function "noDups" is run before "printHeader", the report will remove redundant information. However, if the time of the interrupt is the only data that has changed from a previous interrupt (noise, etc), that interrupt will be retained in the report.

4.c) if neither "noDups" or "noDups2" is run before "printHeader", the report will display each interrupt as recorded.

Once the report is displayed to Serial Monitor, the arrays are cleared, and the sketch is ready to capture a new sequence of interrupts.

The sketch minimizes Serial.print so that the slow printing process does not interfere with the fast interrupt changes. Hence the sequence of interrupts is not affected by delays due to code other than the loop() function that only handles the push button action.

The sketch code is heavily documented to help explain the purpose of the code. The sort filters used in "noDups()" and "noDups2()" are simple and could be optimized, but I wanted to get the job done using code that could be understood by a user.


/*
 * KY040_Interrupt_Counter_u_01
 * 2017-01-08
 * Lowell Bahner
 *
 * microsecond counter
 *
 * This code counts the transitions on a rotary encoder
 * for one detent rotation change.
 *
 * The code marks the time in micros() when an encoder
 * pin transition occurs.
 *
 * A push button signals when to print the results to
 * Serial Monitor.
 *
 * The report displays the sequence of transitions of HIGH-LOW
 * or LOW-HIGH of the interrupt pins INT0 and INT1 that occur
 * as the rotary encoder is turned one or more detents.
 *
 * A maximum of 50 transitions are recorded, then automatic report.
 * Oscillations of HIGH-LOW-HIGH on an INT pin indicate
 * contact bounces that may require a better encoder or
 * capacitor noise filtering for clean transitions.
 * ======================================================

  Serial Monitor Example:

  Starting Rotary Encoder Test

 > Wire Rotary Encoder Pin A (CLK) to Uno pin INT0 (D2).
 > Wire Rotary Encoder Pin B (DT) to Uno pin INT1 (D3).
 > Wire Rotary Encoder (SW) or Push Button SW to Uno pin D4.
 > Run Sketch with Serial Monitor window ON at 9600 baud.
 > Turn Rotary Encoder 1 detent CW (right) or CCW (left).
 > Push Rotary Encoder Switch (HIGH to LOW) to display results.

 >> Ready >> (1 detent CW, then SW pushed)

    Index  Int Pin     IntA     IntB     Time
  -------  -------  -------  -------  -------
        0        x        1        1  0
        1        B        1        0  4
        2        B        1        1  6316
        3        B        1        0  18000
        4        A        0        0  144640
        5        B        0        1  156724
        7        A        1        1  165708

Above: IntB (pin 3) goes LOW at t=4 micros.
IntB goes HIGH at t=6316.
IntB goes LOW at t=18000.
IntA goes LOW at t=144640.
IntB goes HIGH at t=156724.
IntA goes HIGH at t=165708.
Reading down the Int Pin column shows transition sequence B-A-B-A


  >> Ready >> (1 detent CCW, then SW pushed)

    Index  Int Pin     IntA     IntB     Time
  -------  -------  -------  -------  -------
        0        x        1        1  0
        1        A        0        1  4
        2        B        0        0  31968
        3        A        1        0  43992
        4        B        1        0  65068
        6        B        1        1  65108

  Above: IntA goes LOW at t=4 micros.
  IntB goes LOW at t=31968.
  IntA goes HIGH at t=43992.
  IntB goes HIGH at t=65108.
  Reading down the Int Pin column shows transition sequence A-B-A-B

 * ======================================================
 *
 */

uint8_t pinA = 2;  // Connected to CLK on KY-040
uint8_t pinB = 3;  // Connected to DT on KY-040
uint8_t pinSW = 4;  // Connected to SW on KY-040

volatile uint8_t intPin[51];
volatile uint8_t aVal[51];
volatile uint8_t bVal[51];
volatile unsigned long milAB[51];      // current pinA micros at time of interrupt
int8_t indexAB[51];
volatile uint8_t aValLast;
volatile uint8_t bValLast;
volatile uint8_t intPinLast;
volatile unsigned long milABLast;
uint8_t flagSW = 0;
volatile uint8_t index = 0;
uint8_t indexCopy = 0;
unsigned long start, finished, elapsed;

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

void setup() {
  pinMode (pinA, INPUT_PULLUP);
  pinMode (pinB, INPUT_PULLUP);
  pinMode (pinSW, INPUT_PULLUP);

  attachInterrupt(0, interruptA, CHANGE);
  attachInterrupt(1, interruptB, CHANGE);

  // Save pin states
  aValLast = digitalRead(pinA);
  bValLast = digitalRead(pinB);
  milABLast = 0;
  intPinLast = 0;

  Serial.begin (9600);
  Serial.println(F("\n Starting Rotary Encoder Test"));

  displayInstructions();
  displayReady();
  clearArrays();
}


// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// function displayInstructions()
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

void displayInstructions() {
  Serial.println (F("\n"));
  Serial.println (F(" > Wire Rotary Encoder Pin A (CLK) to Uno pin INT0 (D2). "));
  Serial.println (F(" > Wire Rotary Encoder Pin B (DT) to Uno pin INT1 (D3). "));
  Serial.println (F(" > Wire Rotary Encoder (SW) or Push Button SW to Uno pin D4. "));
  Serial.println (F(" > Run Sketch with Serial Monitor window ON at 9600 baud."));
  Serial.println (F(" > Turn Rotary Encoder 1 detent CW (right) or CCW (left). "));
  Serial.println (F(" > Push Switch (HIGH to LOW) to display results. "));
}

void displayReady() {

  Serial.println (F("\n >> Ready >> "));
}



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

void interruptA() {
  //Serial.print (F("\n > interruptA "));
  cli(); //stop interrupts happening before we read pin values
  if (index == 0) {
    start = micros();
    milAB[index] = 0;
  }
  index = index + 1;
  finished = micros();
  milAB[index] = finished - start;
  aVal[index] = digitalRead(pinA);
  bVal[index] = digitalRead(pinB);
  intPin[index] = 2; // which pin is connected to A interrupt
  indexCopy = index;
  flagSW = 1;
  sei(); //restart interrupts
}

// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// function interruptB()
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

void interruptB() {
  //Serial.print (F("\n > interruptB "));
  cli(); //stop interrupts happening before we read pin values
  if (index == 0) {
    start = micros();
    milAB[index] = 0;
  }
  index = index + 1;
  finished = micros();
  milAB[index] = finished - start;
  bVal[index] = digitalRead(pinB);
  aVal[index] = digitalRead(pinA);
  intPin[index] = 3; // which pin is connected to B interrupt
  indexCopy = index;
  flagSW = 1;
  sei(); //restart interrupts
}


// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// function noDups
// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// do not record A or B duplicates during interrupt processing
//
void noDups() {
  byte dup = 0;

  if (milAB[index] == milABLast) { // include time of interrupt as a selector
    if (aVal[index] == aValLast) {
      if (bVal[index] == bValLast) {
        if (intPin[index] == intPinLast) {
          dup = 1;
        }
      }
    }
  }

  if (dup > 0) {
    index = index - 1;
  }

  // save the last values
  milABLast = milAB[index];
  aValLast = aVal[index];
  bValLast = bVal[index];
  intPinLast = intPin[index];
  indexCopy = index;
}


// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// function noDups2
// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// remove A or B duplicates before printing arrays
// but retain sequential Index values
//
void noDups2() {
  uint8_t dup = 0;
  uint8_t prev = 0;
  unsigned long mil2[51];
  unsigned long mil2Last;
  uint8_t aVal2[51];
  uint8_t bVal2[51];
  uint8_t intPin2[51];
  uint8_t index2[51]; // use to retain index
  char buf [10]; // use to print int64 values

  // save the [0] values
  mil2[prev] = milAB[prev];
  aVal2[prev] = aVal[prev];
  bVal2[prev] = bVal[prev];
  intPin2[prev] = intPin[prev];
  index2[prev] = indexAB[prev]; // use to retain index

  for (uint8_t eID = 1; eID <= indexCopy; eID++) {
    // copy records to x2 arrays
    mil2[eID] = milAB[eID];
    aVal2[eID] = aVal[eID];
    bVal2[eID] = bVal[eID];
    intPin2[eID] = intPin[eID];
    index2[eID] = indexAB[eID]; // use to retain index
    /* debug trace
        Serial.print("\n\nindex "); Serial.print (eID);
        Serial.print(", prev "); Serial.print (prev);
        double temp = (double)mil2[prev];
        dtostrf(temp, 9, 0, buf);
        Serial.print(", mil2[prev] "); Serial.print (buf);
        temp = (double)milAB[eID];
            dtostrf(temp, 9, 0, buf);
        Serial.print(", milAB[eID] "); Serial.print (buf);
        Serial.print(", aVal2[prev] "); Serial.print (aVal2[prev]);
        Serial.print(", aVal[eID] "); Serial.print (aVal[eID]);
        Serial.print(", bVal2[prev] "); Serial.print (bVal2[prev]);
        Serial.print(", bVal[eID] "); Serial.print (bVal[eID]);
        Serial.print(", intPin2[prev] "); Serial.print (intPin2[prev]);
        Serial.print(", intPin[eID] "); Serial.print (intPin[eID]);
    */
    dup = 0;
    if (aVal[eID] == aVal2[prev]) {
      if (bVal[eID] == bVal2[prev]) {
        if (intPin[eID] == intPin2[prev]) {
          // add the following line of code to retain time as a selector
          // if (milAB[eID] == mil2[prev]) { // include time of interrupt as a selector
          dup = 1;
          //Serial.print("\n\n Duplicate [eID]: "); Serial.print (eID);
          // } // end if (milAB[eID] == mil2[prev]) {
        }
      }
    }
    // if not duplicate record, copy to next record [prev]
    if (dup < 1) {
      //Serial.print("\n\n Not Duplicate [eID]: "); Serial.print (eID);
      prev = prev + 1;
      mil2[prev] = milAB[eID];
      aVal2[prev] = aVal[eID];
      bVal2[prev] = bVal[eID];
      intPin2[prev] = intPin[eID];
      index2[prev] = indexAB[eID]; // use to retain index
    }
  }

  // copy [prev] records over [eID] records
  for (uint8_t eID = 0; eID <= prev; eID++) {
    milAB[eID] = mil2[eID];
    aVal[eID] = aVal2[eID];
    bVal[eID] = bVal2[eID];
    intPin[eID] = intPin2[eID];
    indexAB[eID] = index2[eID]; // this retains the original index rather than renumbering
  }
  index = prev;
  indexCopy = index;

  // save the last values
  milABLast = milAB[index];
  aValLast = aVal[index];
  bValLast = bVal[index];
  intPinLast = intPin[index];
}

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

void loop ()
{

  //Serial.println ("loop ...");
  if ((digitalRead(pinSW) == LOW) && (flagSW > 0)) {

    /*
      Serial.println (F("\n -- Print All Interrupts"));
      printHeader(); // print the original arrays with all interrupts
    */

    /*
      // Time of interrupts is included as a selector in determining duplicate records
      noDups(); // remove duplicate records and re-index
      Serial.println (F("\n -- Print No Duplicate Interrupts and re-index"));
      printHeader(); // print the trimmed arrays with changed interrutps
    */

    /* */
    // Time of interrupts is not included as a selector in determining duplicate records
    noDups2(); // remove duplicate records but retain the index
    //Serial.println (F("\n -- Print No Duplicate Interrupts"));
    printHeader(); // print the trimmed arrays with changed interrutps
    /* */

    clearArrays();
    displayReady();
  }

  // avoid array overflows
  if (index >= 50) {
    printHeader();
    clearArrays();
    displayReady();
  }

}

// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// function clearArrays
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

void clearArrays() {
  for (index = 0; index <= 50; index++ ) {
    // clear the arrays
    milAB[index] = 0;
    aVal[index] = 0;
    bVal[index] = 0;
    intPin[index] = 0;
    indexAB[index] = index;
  }
  flagSW = 0; // clear flag
  index = 0;
  indexCopy = index;
  start = micros();
  milAB[index] = 0;
  aVal[index] = digitalRead(pinA);
  bVal[index] = digitalRead(pinB);
}

// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// function printHeader
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

void printHeader() {

  Serial.println (F("\n    Index  Int Pin     IntA     IntB     Time"));
  Serial.println   (F("  -------  -------  -------  -------  -------"));
  for (byte eID = 0; eID <= index; eID++)
  {
    print1(eID);
  }
  Serial.println ();

} // end of printHeader

// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// function print1
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

void print1(byte eID) {

  // print the current interrupt data

  char buf [10];
  sprintf (buf, "%9d", (int)indexAB[eID]);
  Serial.print (buf);
  if (intPin[eID] == 2) {
    Serial.print ("        A");
  } else if (intPin[eID] == 3) {
    Serial.print ("        B");
  } else {
    Serial.print ("        x");
  }
  sprintf (buf, "%9d", (int)aVal[eID]);
  Serial.print (buf);
  sprintf (buf, "%9d", (int)bVal[eID]);
  Serial.print (buf);
  Serial.print ("  "); Serial.print (milAB[eID]);
  Serial.println ();

} // end of printHeader1



// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// print binary8 binary16 and hex functions
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

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

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

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

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


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

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

Thursday, December 29, 2016

Arduino and MCP23S17 Port Expander (Part 4)

MCP23S17 Port Expander Interrupts— 
The MCP23S17 Port Expander chip has two built-in hardware interrupt pins. Pin 20 is the Port A interrupt (INTA) and pin 19 is the Port B interrupt (INTB). INTA and INTB terminology distinguishes the MCP interrupts from the Arduino Uno interrupts, INT0 and INT1.

The INTA and INTB pins can be used to sense when a pin on that particular port (A or B) has changed state. Alternatively, INTA and INTB pins can be mirrored so that any one of the 16 digital pins can be sensed when it has changed state.
The INTA interrupt pin can be wired to the Arduino INT0 pin so that the Arduino processor can be interrupted instantly if a pin changes state on an MCP Port A pin (any 1 or more of 8 pins). Similarly, the INTB interrupt pin can be wired to the Arduino INT1 pin so that the Arduino processor can be interrupted instantly if a pin changes state on an MCP Port B pin (any 1 or more of 8 pins).
The Majenko MCP23S17 library (.h and .cpp files) provides functions to handle MCP interrupts which use similar functions as the Arduino interrupt functions:
  • void enableInterrupt(uint8_t pin, uint8_t type);
  • void disableInterrupt(uint8_t pin);
  • void setMirror(boolean m);
  • uint16_t getInterruptPins();
  • uint16_t getInterruptValue();
  • void setInterruptLevel(uint8_t level);
  • void setInterruptOD(boolean openDrain);
I added Port interrupt functions to Majenko's code. The Port functions allow reading the 8 pin states of the port that was interrupted, as opposed to the functions provided by Majenko that read all 16 pin states when an interrupt occurs. Otherwise, the functions behave the same. The Port interrupt function code is provided later in this blog.
Function enableInterrupt(pin, type) is used on any MCP input pin on which an interrupt is desired. The type is CHANGE, RISING, or FALLING which matches interrupt types for the Arduino. CHANGE means trigger an interrupt on both rising or falling changes of state on this particular input pin.
Function setInterruptLevel(level), where level = HIGH means the interrupt pin will go HIGH when an interrupt occurs, level = LOW means it will go LOW.
Missing from the MCP interrupt functions is the Arduino attachInterrupt() function. For example, if MCP pin 4 changing state causes the MCP INTA pin to go LOW, and the MCP INTA pin is wired to the Arduino INT0 pin, then the INT0 pin will also go LOW triggering the INT0 code being immediately processed to take some action. The INTA and INT0 pins require the wire connection to trigger the Arduino attachInterrupt() function, otherwise the MCP interrupt pin INTA would have to be polled by the sketch to determine when INTA changed state. In cases where speed is not of concern, polling for an interrupt might work well. But if speed is a possible requirement, hardware interrupts will provide the best option.
Writing code for handling interrupts is a bit of a challenge, because MCP interrupts can occur on any input pin, but each triggers either INTA or INTB. The pin that caused the INTA or INTB interrupt must be determined so the proper action can be taken. The interrupt must be cleared so that another interrupt can be registered. And, what happens when more than one pin changes state at the same time? Can that happen?
Another issue surfaces for Arduino Uno processors that only have INT0 and INT1 hardware interrupt pins. What does one do if they want to use interrupts for more than one MCP23S17 chip? Where do the MCP23S17 INTA and INTB pins connect on the Arduino? The simple answer is to use a processor, like the Mega 2560, with more interrupts. A bit more difficult approach is to program other Arduino digital pins to have "pin change interrupts" and have code respond to those interrupts. A third option (yet untested) is to tie the MCP interrupts together using the setInterruptOD(boolean openDrain) function and attach them to one set of Arduino interrupts.
The getInterruptPins(), getInterruptAPins(), and getInterruptBPins() functions return either 16 or 8 pin values at the time of an MCP interrupt. These values need to be retained in uint16_t or uint8_t variables, because the registers on the chip will be reset when the interrupt is cleared. When that interrupt is cleared, the next interrupt can be triggered and new values will be written into the MCP interrupt registers. Meanwhile, the values saved in the variables do not change and can be used as needed in the sketch. Their values can then be updated for the new interrupt and the process can be repeated.
  • Note: use .getInterruptPins() to identify the pin that interrupted. Its value is retained until the interrupt is cleared. Further interrupts are blocked until the current interrupt is cleared.
  • Use .getInterruptValue() or .readPort(port) to clear the interrupt
  • Note: .getInterruptValue retains its value. It changes value when it reads the pins after a new pin interrupt. It then takes on the new pin values of .getInterruptPins.
The getInterruptValue(), getInterruptAValue(), getInterruptBValue() functions behave differently than the getInterruptPins() functions. The getInterruptValue() functions retain the values read for the 16 or 8 pins, until the values change. If pins are read over and over using getInterruptValue(), the values may or may not change. Using getInterruptValue() will clear the interrupt, but the getInterruptValue() pin values will not change until getInterruptPins() is called after a new interrupt occurs.
Test sketches with debugging indicate the following results:
Example 1. Ground MCP pin 9, followed by getInterruptBPins() indicates pin 9 was interrupted
     > Port B Interrupt Pins: 00000010
Then,  getInterruptBValues() captures port values
     > Port B Interrupt Values: 11111101
Then a second getInterruptBPins() indicate interrupts are cleared
     > Port B Interrupt Pins: 00000000
Again, getInterruptBValues() captures port values that have not changed
     > Port B Interrupt Values: 11111101
Example 2. Interrupt Port A pin, then interrupt Port B pin
- MCP INTA is wired to Arduino INT0.
- MCP INTB is wired to Arduino INT1.
     > INT0 is triggered by INTA and INT0 state changes from HIGH to LOW.
     > INTA is not cleared.
     > Independent of INTA, INT1 is triggered by INTB and INT1 state changes from HIGH to LOW.
     > INTB is not cleared.
- Neither INTA or INTB allow further interrupts on INT0 or INT1 until INTA and INTB  are cleared.
Example 3. Two or more MCP simultaneous interrupts can occur on a Port.
- Simultaneously ground 2 MCP pins 10 & 13 on Port B.
     > Port B Interrupt Pins: 00100100
     > Port B Interrupt Values: 11011011
- Determine which MCP interrupts (bits [7...0] , right to left) were set (LOW)
     > whichBit: 2
     > whichBit: 5
     > whichBitCount: 2
Example 3 demonstrates that one Arduino interrupt can indicate that one or more MCP port interrupts have occurred. Each MCP Port interrupt would need to be processed individually. Successive interrupts are therefore different than simultaneous interrupts, and would require different code to interpret the interrupts.
The INT0 and INT1 ISR code --

void interruptA() {
  // ISR to respond to INT0 interrupt
  // Respond to the INTA interrupt
  // which was triggered by one or more MCP inputs
  interruptMCP(Bank0, 0); // MCP ISR (bank, port)
}

When INT0 is triggered by INTA (due to an MCP pin going LOW), the function interruptA is executed and calls the interruptMCP(bank, port) function. Similar code is used for INT1 and INTB.

void interruptMCP(MCP23S17 &bank, uint8_t port) {
  // read the Port Expander Interrupt pins
  readBankPortIntPins(bank, port);

  // read the Port Expander Interrupt values
  readBankPortIntVal(bank, port);

  // take action on new pin interrupt values
  if (port < 1) {
    flagMCPA = 1; // set flag that Port A interrupt occurred
  } else {
    flagMCPB = 1; // set flag that Port B interrupt occurred
  }
  // act on Reading in loop()
}

The interruptMCP() function reads the Port getInterruptXPins(), where X=A or B. It then reads the Port getInterruptXValues() which saves the values to the mcpXReading variable. The mcpXReading variable (8 bits holding the Port X interrupt pin values) is then processed to take some action. In this case, a flag is set and passed to loop(). In loop(), the 8 bits are read to determine which pins caused the interrupt, and then that information is printed to Serial Monitor to show what just happened, and further processed as a demonstration of the code. 
Port Interrupt functions --
Add the following code at the end of public: in MCP23S17.h
/* Following functions added by Lowell Bahner, 2016-12-29, for Port interrupts */
        uint8_t getInterruptAPins();
        uint8_t getInterruptAValue();
        uint8_t getInterruptBPins();
        uint8_t getInterruptBValue();

And, add the following code to the end of MCP23S17.cpp:


/* Following functions added by Lowell Bahner, 2016-12-29, for Port interrupts */

/*! This function returns an 8-bit bitmap of the Port-A pin or pins that have caused an interrupt to fire.
 *
 *  Example:
 *
 *      unsigned int pins = myExpander.getInterruptAPins();
 */
uint8_t MCP23S17::getInterruptAPins() {
    readRegister(INTFA);
    return  _reg[INTFA];
}

/*! This returns a snapshot of the Port-A IO pin states at the moment the last interrupt occured.  Reading
 *  this value clears the interrupt status (and hence the INT pins) for the port.
 *  Until this value is read (or the current live port value is read) no further interrupts can
 *  be indicated.
 *
 *  Example:
 *
 *      unsigned int pinValues = myExpander.getInterruptAValue();
 */
uint8_t MCP23S17::getInterruptAValue() {
    readRegister(INTCAPA);
    return _reg[INTCAPA];
} 

/*! This function returns an 8-bit bitmap of the Port-B pin or pins that have caused an interrupt to fire.
 *
 *  Example:
 *
 *      unsigned int pins = myExpander.getInterruptBPins();
 */
uint8_t MCP23S17::getInterruptBPins() {
    readRegister(INTFB);
    return _reg[INTFB];
}

/*! This returns a snapshot of the Port-B IO pin states at the moment the last interrupt occured.  Reading
 *  this value clears the interrupt status (and hence the INT pins) for the port.
 *  Until this value is read (or the current live port value is read) no further interrupts can
 *  be indicated.
 *
 *  Example:
 *
 *      unsigned int pinValues = myExpander.getInterruptBValue();
 */
uint8_t MCP23S17::getInterruptBValue() {
    readRegister(INTCAPB);
    return _reg[INTCAPB];
} 

Sketch Results and Code --

Following is an example of the Serial Monitor report from the sketch. Each time one or more MCP23S17 input pins are grounded (Switch or wire), the Port pins that caused the interrupt are identified and counted, then those pins are further processed, as an example of processing each pin that had changed state.

 Starting MCP23S17 Interrupt Test

 > Wire MCP23S17 Pin INTA to Arduino interrupt pin pinA (D2). 
 > Wire MCP23S17 Pin INTB to Arduino interrupt pin pinB (D3). 
 > Run Sketch with Serial Monitor window ON at 115200 baud.
 > Take MCP23S17 input pin(s) LOW (Switch or wire to Ground). 

 >> Ready >> 

 --- Interrupts Port A Values: 00100100
 Port A whichBit: 2
 Port A whichBit: 5
 byteACount: 2

 Process Port A pin: 2
 Process Port A pin: 5

 --- Interrupts Port B Values: 10000000
 Port B whichBit: 7
 byteBCount: 1

 Process Port B pin: 7


Sketch to demonstrate MCP23S17 Interrupts --

/*
 * MCP23S17_Interrupt_Counter_01
 * 2016-12-29
 * Lowell Bahner
 *
 * This code counts interrupts on an MCP23S17 Port Expander
 *
 * A push button or wire takes one or more MCP23S17 input pins LOW to trigger interrupts
 * which are processed and the action is printed to Serial Monitor.
 *
 * The MCP23S17 has an interrupt on each digital input pin which can set
 * a PORT interrupt pin. The Port A interrupt (INTA) is chip pin 20, and Port B
 * interrupt (INTB) is chip pin 19.
 *
 * The MCP23S17 INTA and INTB can be wired to interrupt pins on the Arduino
 * which can use hardware interrupts to interrupt the sketch code to take
 * immediate action.
 *
 * UNO hardware interrupt pins INT0 (D2), INT1 (D3)
 * Mega 2560 interrupt pins D2, D3, D18, D19, D20, D21
 *
 * ======================================================

  Sketch Interrupt Tests:
  Test 1. Demonstrate MCP port interrupt process.
   - Ground MCP pin 9 and getInterruptBPins() indicate pin 9 was interrupted
      >  Port B Interrupt Pins: 00000010
   - then getInterruptBValues() captures port values
      >  Port B Interrupt Values: 11111101
   - then a second getInterruptBPins() indicate interrupts are cleared
      >  Port B Interrupt Pins: 00000000
   - again, getInterruptBValues() captures port values that have not changed
      >  Port B Interrupt Values: 11111101

  Test 2. Interrupt A and B are separate events.
          INTA ==> INT0 state change is captured.
          INTA is not cleared and does not capture another interrupt.
          INTB ==> INT1 state changes.
          INTB is not cleared and does not capture another interrupt.
      Index  Int Pin     IntA     IntB     Time
  -------  -------  -------  -------  -------
        0        x        1        1  0
        1        A        0        1  4

    Index  Int Pin     IntA     IntB     Time
  -------  -------  -------  -------  -------
        0        x        0        1  0
        1        B        0        0  4

  Test 3. Ground 2 MCP pins 10 & 13 on Port B.
    - One Arduino interrupt can indicate one or more simultaneous MCP interrupts.
    >  Port B Interrupt Pins: 00100100
    >  Port B Interrupt Values: 11011011
    - Determine which MCP interrupts (bits [7...0]) were set (LOW)
    >  whichBit: 2
    >  whichBit: 5
    >  whichBitCount: 2


 * ======================================================
 *
 *
   Sketch Output to Serial Monitor

   Starting MCP23S17 Interrupt Test


 > Wire MCP23S17 Pin INTA to Arduino interrupt pin pinA (D2).
 > Wire MCP23S17 Pin INTB to Arduino interrupt pin pinB (D3).
 > Run Sketch with Serial Monitor window ON at 115200 baud.
 > Take MCP23S17 input pin(s) LOW (Switch or wire to Ground).

 >> Ready >>


 --- Interrupts Port A Values: 00100100
 Port A whichBit: 2
 Port A whichBit: 5
 byteACount: 2

 Process Port A pin: 2
 Process Port A pin: 5

 --- Interrupts Port B Values: 10000000
 Port B whichBit: 7
 byteBCount: 1

 Process Port B pin: 7

 *
 *
 */

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

// Majenko MCP23S17 Library (revised 2017-Jan-23)
#include <MCP23S17.h>

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

const byte NUM_ENCODERS = 4;

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

uint8_t pinA = 2;   // INT0 Connected to MCP INTA
uint8_t pinB = 3;   // INT1 Connected to MCP INTB

volatile uint16_t mcpReading = 0; // 16-bit interrupt reading
volatile uint8_t mcpAReading = 0; // 8-bit Port-A interrupt reading
volatile uint8_t mcpBReading = 0; // 8-bit Port-B interrupt reading
uint8_t whichBit = 0;    // one bit [7...0] that is set in byte
uint8_t byteACount = 0; // number of bits set in byte
uint8_t byteBCount = 0; // number of bits set in byte
uint8_t byteABits[8] = {0}; // set bit array
uint8_t byteBBits[8] = {0}; // set bit array
volatile uint8_t flagMCPA = 0;
volatile uint8_t flagINTA = 0;
volatile uint8_t flagMCPB = 0;
volatile uint8_t flagINTB = 0;

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

MCP23S17 Bank0(&SPI, chipSelect, 0);
MCP23S17 Bank1(&SPI, chipSelect, 1);
MCP23S17 Bank2(&SPI, chipSelect, 2);
MCP23S17 Bank3(&SPI, chipSelect, 3);

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

void setup() {

  Bank0.begin();
  Bank1.begin();
  Bank2.begin();
  Bank3.begin();

  pinMode (pinA, INPUT_PULLUP);
  pinMode (pinB, INPUT_PULLUP);

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

  Serial.begin (115200);
  Serial.println(F("\n Starting MCP23S17 Interrupt Test"));

  displayInstructions();
  displayReady();

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

  // clear the interrupt values variables
  mcpAReading = 0;
  mcpBReading = 0;
  mcpReading = 0;

}  // end of setup



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

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

  setPin(Bank0);
  setPin(Bank1);
  setPin(Bank2);
  setPin(Bank3);
}

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

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

  // set Port Expander Interrupt configuratons
  bank.setMirror(false);
  bank.setInterruptOD(false);
  bank.setInterruptLevel(LOW);
  // setInterruptLevel(LOW) requires the revised library (2017-Jan-23)
  // clear all interrupts on this Port Expander
  mcpReading = bank.getInterruptValue();
}

// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// function readBankPortIntVal()
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

// Read all Port Expander port interrupt values
void readBankPortIntVal(MCP23S17 &bank, uint8_t port) {
  if (port < 1) { // Port A
    mcpAReading = bank.getInterruptAValue();
    //Serial.print ("\n Port A Interrupt Values: "); print8Bits(mcpAReading);
  } else {        // Port B
    mcpBReading = bank.getInterruptBValue();
    //Serial.print ("\n Port B Interrupt Values: "); print8Bits(mcpBReading);
  }
}

// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// function readBankPortIntPins()
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

// Read all Port Expander port interrupt pins
void readBankPortIntPins(MCP23S17 &bank, uint8_t port) {
  if (port < 1) { // Port A
    mcpAReading = bank.getInterruptAPins();
    //Serial.print ("\n Port A Interrupt Pins: "); print8Bits(mcpAReading);
  } else {        // Port B
    mcpBReading = bank.getInterruptBPins();
    //Serial.print ("\n Port B Interrupt Pins: "); print8Bits(mcpBReading);
  }
}


// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// function readByteBits()
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

// Read all Port Expander port interrupt pins
// save set bit to byteBits arrays
// byteBits array can be sequentially processed later
void readByteBits(MCP23S17 &bank, uint8_t port) {
  if (port < 1) { // Port A
    byteACount = 0;
    for (uint8_t ind = 0; ind < 8; ind++) {
      if (bitRead(~mcpAReading, ind)) { // ~ inverts bit values
        byteABits[byteACount] = ind; // save the bit number to sequential array
        byteACount = byteACount + 1;
        Serial.print ("\n Port A whichBit: "); Serial.print (ind);
      }
    }
    Serial.print ("\n byteACount: "); Serial.print (byteACount);
  } else { // Port B
    byteBCount = 0;
    for (uint8_t ind = 0; ind < 8; ind++) {
      if (bitRead(~mcpBReading, ind)) { // ~ inverts bit values
        byteBBits[byteBCount] = ind; // save the bit number to sequential array
        byteBCount = byteBCount + 1;
        Serial.print ("\n Port B whichBit: "); Serial.print (ind);
      }
    }
    Serial.print ("\n byteBCount: "); Serial.print (byteBCount);
  }
}

// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// function processPins()
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

// Process the port byteBits array of interrupted MCP pins
void processPins(MCP23S17 &bank, uint8_t port) {
  if (port < 1) { // Port A
    // byteACount = number of pins to process
    for (uint8_t ind = 0; ind < byteACount; ind++) {
      uint8_t pin = byteABits[ind]; // recover the bit number from sequential array
      Serial.print ("\n Process Port A pin: "); Serial.print (pin);
    }
  } else { // Port B
    // byteACount = number of pins to process
    for (uint8_t ind = 0; ind < byteBCount; ind++) {
      uint8_t pin = byteBBits[ind]; // recover the bit number from sequential array
      Serial.print ("\n Process Port B pin: "); Serial.print (pin);
    }
  }
}

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

void interruptA() {
  // ISR to respond to INT0 interrupt
  // Respond to the INTA interrupt
  // which was triggered by one or more MCP inputs
  interruptMCP(Bank0, 0); // MCP ISR (bank, port)
}

// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// function interruptB()
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

void interruptB() {
  // ISR to respond to INT1 interrupt
  // Respond to the INTB interrupt
  // which was triggered by one or more MCP inputs
  interruptMCP(Bank0, 1); // MCP ISR (bank, port)
}

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

void interruptMCP(MCP23S17 &bank, uint8_t port) {
  // read the Port Expander Interrupt pins
  readBankPortIntPins(bank, port);

  // read the Port Expander Interrupt values
  readBankPortIntVal(bank, port);

  // take action on new pin interrupt values
  if (port < 1) {
    flagMCPA = 1; // set flag that Port A interrupt occurred
  } else {
    flagMCPB = 1; // set flag that Port B interrupt occurred
  }
  // act on Reading in loop()
}


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

void loop ()
{

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

  if (flagMCPA > 0) {
    //Serial.println ("\n loop flagMCPA");
    printInterruptHeader(Bank0, 0);
    processPins(Bank0, 0); // do some processing of each interrupted pin
    flagMCPA = 0;
  }

  if (flagMCPB > 0) {
    //Serial.println ("\n loop flagMCPB");
    printInterruptHeader(Bank0, 1);
    processPins(Bank0, 1); // do some processing of each interrupted pin
    flagMCPB = 0;
  }

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

}

// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// function printInterruptHeader
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

void printInterruptHeader(MCP23S17 bank, uint8_t port) {
  // inReading is either mcpAReading or mcbBReading 8-pin byte values
  Serial.print ("\n\n --- Interrupts Port ");
  if (port < 1) {
    Serial.print (F("A "));
    Serial.print ("Values: "); print8Bits(~mcpAReading);
  } else {
    Serial.print (F("B "));
    Serial.print ("Values: "); print8Bits(~mcpBReading);

  }
  // get the bit count and write the bits to byteBit[] array
  readByteBits(bank, port);
  Serial.println ();
} // end of printInterruptHeader


// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// function displayInstructions()
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

void displayInstructions() {
  Serial.println (F("\n"));
  Serial.println (F(" > Wire MCP23S17 Pin INTA to Arduino interrupt pin pinA (D2). "));
  Serial.println (F(" > Wire MCP23S17 Pin INTB to Arduino interrupt pin pinB (D3). "));
  Serial.println (F(" > Run Sketch with Serial Monitor window ON at 115200 baud."));
  Serial.println (F(" > Take MCP23S17 input pin(s) LOW (Switch or wire to Ground). "));
}

void displayReady() {
  Serial.println (F("\n >> Ready >> "));
}


// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// print binary8 binary16 and hex functions
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

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

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

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

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


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

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

Useful references -
  1. SPI - Serial Peripheral Interface - for Arduino (gammon, 2011)
  2. MCP23017 Interrupts (gammon, 2013)
(Dec 29, 2016)

Wednesday, December 14, 2016

Arduino and MCP23S17 Port Expander (Part 3)

Testing the MCP23S17 Port Expander — 
In the past two Parts of this series, the Microchip MCP23S17 Port Expander was installed either in a single chip breadboard prototype or as a quad-chip Arduino backpack. In the sketches, there was a lot of redundant code due to having multiple MCP Bank objects. In this Part 3, a sketch is provided that shows how to pass the instantiated Bankx objects to a function. As an example, the quad backpack has four (4) MCP23S17 chips, instantiated as Bank0, Bank1, Bank2, and Bank3. To process each of these objects, separate functions with identical internal code could be written for each. Alternatively, the object can be passed to a function, so that only one copy of the code has to be written. The SetChipPins() function used in Part 1 can be modified to pass the Bank object to another function setPin(&object). Each of the four Banks are processed using that same code.

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

    setPin(Bank0);
    setPin(Bank1);
    setPin(Bank2);
    setPin(Bank3);
}

void setPin(MCP23S17 &bank) {
    for (uint8_t i = 0; i <= 15; i++)
    bank.pinMode(i, INPUT_PULLUP);
}

The following sketch will print one line on Serial Monitor when one or more MCP23S17 input pins are pulled LOW (i.e., shorted with switch or wire to ground). The binary string that is printed will show which Bank digital pin [15,14,...,1,0] is LOW. For example, "Bank2: BIN: 1101111111111111 HEX: 0xDFFF DEC: 57343" shows that the Bank2 input pin 13 (INPUT_PULLUP, normally HIGH) was grounded (LOW). The binary display of the Port A and/or Port B pins provides a means of visually checking on the state of 8 or 16 pins. The functions "print8Bits(uint8_t)" and "print16Bits(uint16_t)" handle the binary print to Serial Monitor in place of the standard Serial.print function.

/*
 * MCP23S17_MultiButtonTestInputs_01.ino
 * Library Ref: github.com/MajenkoLibraries/MCP23S17
 *
 * Lowell Bahner
 * 2016-12-11
 *
 * Hardware: Arduino UNO/Mega2560/Mega2560&CH340-Serial-USB
 * IDE: Arduino 1.6.5
 *
 * Useful SPI Ref: gammon.com.au/spi
 *
 * Wire the SPI Interface common lines (Mega2560 SPI pins shown in [ ]):
 * Arduino SPI_MOSI pin 11 [51] <->   SI  MCP23S17 pin 13
 * Arduino SPI_MISO pin 12 [50] <->   SO  MCP23S17 pin 14
 * Arduino SPI_CLOCK pin 13 [52]<->   SCK MCP23S17 pin 12
 * Arduino SPI_CS pin 10 [53]   <->   SS  MCP23S17 pin 11
 * MCP23S17 VDD pin 9 to +5V
 * MCP23S17 VSS pin 10 to Gnd
 * MCP23S17 Reset pin 18 to +5V
 * MCP23S17 Interrupt pins 19-20
 * MCP23S17 A0,A1,A2 pins connect to Gnd or +5V (000 = Address 0, 100 = Address 1, 010 = Address 2, etc)
 * MCP23S17 Port A pins are chip pins 21-28
 * MCP23S17 Port B pins are chip pins 1-8
 *
 * This sketch tests button inputs.
 * Temporarily short each MCP23S17 pin to ground to test for continuity.
 * If a pin is grounded the pin value will print to Serial Monitor.
 *
 * This sketch instantiates 4 MCP23S17 chips.
 * 
 * This code passes the Bank objects as parameters to a function
 *
 * This code was tested on UNO and Mega2560
 * using the ICSP pins for SPI addressing.
 *
 * On each chip, Port A and Port B are set to INPUT_PULLUP.
 * Buttons are connected to one or more Port A and B inputs.
 *
 * Port A & B INPUT_PULLUP pins are set HIGH.
 * When a switch sets the chip's Port A or B input pin to LOW
 * both Port A and Port B 0-7 pin values are read as uint8_t
 * (byte) values and printed on Serial Output. oneTime variable
 * limits printing of multiple values to Serial Monitor.
 *
 * Majenko MCP23S17 library functions (see .h,.cpp files for details):
 *
 *  bank.pinMode(uint8_t pin, uint8_t mode)
 *    where modes are:
 *      OUTPUT - sets pin HIGH
 *      INPUT  - sets pin LOW for sensing change to HIGH +V
 *      INPUT_PULLUP - sets pin HIGH for sensing change to LOW 0V
 *
 *  bank.digitalWrite(uint8_t pin, uint8_t value)
 *    if mode=OUTPUT, pin value = LOW (0) or HIGH (1)
 *    if mode=INPUT, value LOW disables pullup on pin, value HIGH enables pullup on pin
 *
 *  uint8_t value = bank.digitalRead(uint8_t pin)
 *    read the state of the pin
 *
 *  uint8_t value = bank.readPort(uint8_t port)
 *    read entire 8-bit port of all INPUT pins on Port A (port=0) or Port B (port>=1)
 *
 *  uint16_t longValue = bank.readPort()
 *    read both ports as 16-bit combined
 *
 *  bank.writePort(uint8_t port, uint8_t val)
 *    write val (0 or 1, hex example 0x55) to all OUTPUT pins on Port A (port=0) or Port B (port>=1)
 *
 *  bank.writePort(uint16_t val)
 *    write val (hex example 0x55AA) to all OUTPUT pins on Port A and Port B
 *
 *  Also, interrupt functions are defined:
 *      enableInterrupt(uint8_t pin, uint8_t type);
 *      void disableInterrupt(uint8_t pin);
 *      void setMirror(boolean m);
 *      uint16_t getInterruptPins();
 *      uint16_t getInterruptValue();
 *      void setInterruptLevel(uint8_t level);
 *      void setInterruptOD(boolean openDrain);
 *
 *
 *
 *
Run Sketch: Example Serial Monitor Output

-- MCP23S17_MultiButtonTestInputs_01 --

Bank0: BIN: 1111111111111110  HEX: 0xFFFE    DEC: 65534
Bank0: BIN: 1111011111111111  HEX: 0xF7FF    DEC: 63487
Bank1: BIN: 1111111111101111  HEX: 0xFFEF    DEC: 65519
Bank2: BIN: 1101111111111111  HEX: 0xDFFF    DEC: 57343
Bank3: BIN: 1111011111111111  HEX: 0xF7FF    DEC: 63487

 *
 *
 *
 */

// Majenko MCP23S17 Library (revised 2017-Jan-23)
#include <MCP23S17.h>

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

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

// Limit when printing occurs
uint8_t limitPrint = 1; // if 1, then limit printing to button push events

// Limit pin print value to one time
uint16_t oneTime0 = 0;

// example Arduino board pin
uint8_t pin7 = 7;

// 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.
// Bank2 is address 2. Pin A1=+5V, A0,A2 grounded.
// Bank3 is address 3. Pin A0,A1=+5V, A2 grounded.

MCP23S17 Bank0(&SPI, chipSelect, 0);
MCP23S17 Bank1(&SPI, chipSelect, 1);
MCP23S17 Bank2(&SPI, chipSelect, 2);
MCP23S17 Bank3(&SPI, chipSelect, 3);

void setup() {

  Serial.begin(9600);

  Serial.print("\n -- MCP23S17_MultiButtonTestInputs_01 --"); Serial.println("");

  Bank0.begin();
  Bank1.begin();
  Bank2.begin();
  Bank3.begin();

  // LED connected to Arduino pin 7 to test inter-device communication
  pinMode(pin7, OUTPUT);

  //
  // Set MCP23S17 pin modes:
  // example set one pin: chip.pinMode(0, OUTPUT); // sets chip pin 0 (Port A 0) to OUTPUT mode
  //
  setChipPins(); // use loop to set individual pins

}

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

  setPin(Bank0);
  setPin(Bank1);
  setPin(Bank2);
  setPin(Bank3);
}

void setPin(MCP23S17 &bank) {
  for (uint8_t i = 0; i <= 15; i++)
    bank.pinMode(i, INPUT_PULLUP);
}

//
// Read a button push on input pin
// 1) Switch an INPUT_PULLUP pin to ground
// 2) Read the pin value
// 3) Write the value to Serial Monitor
// Use functions digitalRead(pin) and digitalWrite(pin,value)
//

//
// Read Bank values
//
void RWinputs(MCP23S17 &bank, uint8_t id) {

  // read the Chip pin values
  // id is just passed so the bank is identified in Serial.print
  // id could also be used in functions that take different actions for
  // different objects
  //
  uint16_t value = bank.readPort();

  if (oneTime0 != value) {
    if ((limitPrint < 1) || (value < 65535)) {
      Serial.print("\nBank"); Serial.print(id);
      Serial.print(": BIN: "); print16Bits(value);
      Serial.print("  HEX: "); crPrintHEX(value, 4);
      Serial.print("  DEC: "); Serial.println(value, DEC);
      oneTime0 = value;
    }
  }
}


void loop() {

  // Read Switches on Port A & B and Write Switch settings
  // to Serial Monitor
  RWinputs(Bank0, 0);
  RWinputs(Bank1, 1);
  RWinputs(Bank2, 2);
  RWinputs(Bank3, 3);

  delay(100);

}

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


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

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

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


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

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

  Serial.print("  ");
}

  Checking on SPI -- Initially the quad MCP23S17 backpack did not work as expected due to some missing wires on the board. A Syscomp CGR-101 oscilloscope was used to take a look at the SPI pins to see if SPI was working. If you experience issues with your hardware, a small oScope can be useful. The oScope was almost essential trying to figure out how rotary encoders worked. Encoders will be discussed in future blogs.

The following image shows the CS pin (Channel B, blue) displayed against SPI pin SCK (Channel A, red). If a similar pattern is not displayed on an oScope when the sketch is running, you can be pretty sure that SPI is not functioning.



Useful references -


  1. SPI - Serial Peripheral Interface - for Arduino (gammon, 2011)


(Dec 14, 2016)