MCP23017 Cascade

As the board only have a few pins with external interrupt capability, my goal is to use these chips to control about 15 rotary encoders.
I got something working already, but all can I found on the internet is for a single chip.

I would like to ask your guys for a working example where I can cascade them, I think wiring them is not a problem, but the code is a different story.

Thanks in advance.

Jay98:
As the board only have a few pins with external interrupt capability, my goal is to use these chips to control about 15 rotary encoders.
I got something working already, but all can I found on the internet is for a single chip.

I would like to ask your guys for a working example where I can cascade them, I think wiring them is not a problem, but the code is a different story.

Thanks in advance.

Lets get some terminology correct:
You can't cascade them, connect A->B->C.
But you can connect up to 8 to the I2C bus in parallel.
I2C--->A address 0x20
+->B
+->C
+->D
+->E
+->F
+->G
+->H address 0x27

Now, each has 16pins that can create an interrupt so, you could have 8*16 interrupt sources.
How are you actually wiring them?

Depending on how fast you encoders change you might need to use the MCP23S17 SPI version, the I2C version has a much slower access rate.

Chuck.

chucktodd:
Lets get some terminology correct:
You can't cascade them, connect A->B->C.
But you can connect up to 8 to the I2C bus in parallel.
I2C--->A address 0x20
+->B
+->C
+->D
+->E
+->F
+->G
+->H address 0x27

Now, each has 16pins that can create an interrupt so, you could have 8*16 interrupt sources.
How are you actually wiring them?

Depending on how fast you encoders change you might need to use the MCP23S17 SPI version, the I2C version has a much slower access rate.

Chuck.

Thank you Chuck, this is how I'm wiring them, this is a diagram I found

So, do I just connect them in parallel to the I2C pins?

Yes in parallel to the I2C pins but only have one pair of pull up resistors for all the chips.

On the terminology front, you do not control a rotory encoder you read it. It controls you not the other way round.

Jay98:
Thank you Chuck, this is how I'm wiring them, this is a diagram I found

So, do I just connect them in parallel to the I2C pins?

Yes, but you will have to adjust the A0,A1,A2 to unique values for each IOExpander. in your schematic you have them all connected to GND, so that IOExpander as the I2C address of 0x20. For the next expander, change A0 to VCC that would give you an I2C address of 0x21.

Also, in your schematic, the drawing for the encoder is confusing me.

When you drew the Encoders, you place 'balls' on the intersection between the boundry of the encorders and the signal lines connecting to the encoders. Under standard interpretation,

Which of my examples is an accurate representation of your schematic?

chuck.

Thank you both Mike and chuck

chucktodd:
Yes, but you will have to adjust the A0,A1,A2 to unique values for each IOExpander. in your schematic you have them all connected to GND, so that IOExpander as the I2C address of 0x20. For the next expander, change A0 to VCC that would give you an I2C address of 0x21.

Well, this is when things start getting hard to understand to me.
In the datasheet these pins are refereed as "hardware address" but honestly, I don't know what they do or how they work. I will have to google a bit about this.

chucktodd:
Also, in your schematic, the drawing for the encoder is confusing me.

When you drew the Encoders, you place 'balls' on the intersection between the boundry of the encorders and the signal lines connecting to the encoders. Under standard interpretation,

Which of my examples is an accurate representation of your schematic?

chuck.

Is not my draw but I know what you mean, it's just the guy that made this used the dots to represent encoders pins, they are not all really connected to ground. :slight_smile:

In the datasheet these pins are refereed as "hardware address"

They change the I2C address of the device. Look at the picture attached from the data sheet. If all three address lines are connected to ground those bits in the address are all zero and the address is 0x20, if just A1 is set to high then the address becomes 0x22, if all bits are set high then the address is 0x27. So they define the address where you talk over the I2C bus to the chip. Each chip needs a different combination of high and low and thus you can have up to 8 of them on the bus at the same time because they all have different addresses.

it's just the guy that made this used the dots to represent encoders pins,

An encoder normally has three pins on it not five.
This is probably too advanced but it describes how to get more resolution from an encoder, but it starts off with an explanation of how encoders work.
http://www.thebox.myzen.co.uk/Workshop/Rotary_Max.html

Grumpy_Mike:
They change the I2C address of the device. Look at the picture attached from the data sheet. If all three address lines are connected to ground those bits in the address are all zero and the address is 0x20, if just A1 is set to high then the address becomes 0x22, if all bits are set high then the address is 0x27. So they define the address where you talk over the I2C bus to the chip. Each chip needs a different combination of high and low and thus you can have up to 8 of them on the bus at the same time because they all have different addresses.

Thank you!

Grumpy_Mike:
An encoder normally has three pins on it not five.

These come with a push button, that's why they have 5 pins.

In that case the wiring looks totally wrong. There should be two connections to ground and the other three to the expander , not four.

Start all over again!

  1. What type of rotary encoder? Is it a rotary switch or a quadrature encoder. Provide a link to the type of part you wish to use.

  2. Assuming that you are using quadrature encoders then which type 1 channel giving only speed two channel giving direction and speed? Or something else? Lots of options all with different needs.

  3. ALL ARDUINO PINS HAVE EXTERNAL INTERRUPTS. Take a look at pin change interrupts.

  4. Using a slave device which needs to be Continuously read if its to be useful, defeats the purpose of interrupts.

Mark

holmes4:
4. Using a slave device which needs to be Continuously read if its to be useful, defeats the purpose of interrupts.

No mark, the MCP23017 does not have to be read continuous. There is a pin change function in the chip that can produce an output on one of it's pins. This then can either be polled or used to generate an interrupt on the Arduino, so you are monitoring 16 inputs and you only have to read the chip once you know there is something to read.

The OP says:-

my goal is to use these chips to control about 15 rotary encoders.

So with two phase signals and a push button, that admittedly was not mentioned until the thread got going, he will need 15 * 3 = 45 inputs. Using three MCP23017s or better three MCP23S17s will do that nicely.

Grumpy_Mike:
There should be two connections to ground and the other three to the expander , not four.

That's how's wired in that schematic, it can be confused due the fact Chuck pointed before (pins and wires contacts use the same symbol).
Anyway, I'm not using that setup, I control encoders buttons via a 74HC165 chip.

No mark, the MCP23017 does not have to be read continuous.

Point.

So with two phase signals and a push button, that admittedly was not mentioned until the thread got going, he will need 15 * 3 = 45 inputs. Using three MCP23017s or better three MCP23S17s will do that nicely.

Or one mega which would be much simpler.

Mark

Three $1.50 chips vs a tens of dollars Mega board? I'd go with the chips if one doesn't mind connecting them up. Don't forget a 0.1uF cap across power & gnd for a few pennies more for each chip.
http://www.digikey.com/product-search/en?FV=fff40027%2Cfff80312&k=MCP23S17&mnonly=0&newproducts=0&ColumnSort=1000011&page=1&stock=0&pbfree=0&rohs=0&quantity=&ptm=0&fid=0&pageSize=25

Guys, is there a library I can use for this purpose?

is there a library I can use for this purpose?

Why? It is simple enough to talk to the chip and a library obfuscates what you are doing.

Grumpy_Mike:
Why? It is simple enough to talk to the chip and a library obfuscates what you are doing.

I guess because you always get cleaner code.
Also I don't know if the code I have, which is related to the posted schematic, will work with multiple chips

#include <Wire.h>
#include "Adafruit_MCP23017.h"  // https://github.com/adafruit/Adafruit-MCP23017-Arduino-Library

// setup the port expander
Adafruit_MCP23017 mcp0;

boolean change=false;        // goes true when a change in the encoder state is detected
int butPress = 101;          // stores which button has been pressed
int encSelect[2] = {101, 0}; // stores the last encoder used and direction {encNo, 1=CW or 2=CCW}
unsigned long currentTime;
unsigned long loopTime;

const int encCount0 = 2;  // number of rotary encoders
// encoder pin connections to MCP23017
//    EncNo { Encoder pinA  GPAx, Encoder pinB  GPAy },
const int encPins0[encCount0][2] = {
  {0,1},   // enc:0 AA GPA0,GPA1 - pins 21/22 on MCP23017
  {3,4}    // enc:1 BB GPA3,GPA4 - pins 24/25 on MCP23017
};  

const int butCount0 = 2;  // number of buttons
//             button on encoder: A  B
const int butPins0[butCount0] = { 2, 5 };
// arrays to store the previous value of the encoders and buttons
unsigned char encoders0[encCount0];
unsigned char buttons0[butCount0];

// read the rotary encoder on pins X and Y, output saved in encSelect[encNo, direct]
unsigned char readEnc(Adafruit_MCP23017 mcpX, const int *pin, unsigned char prev, int encNo) {

  unsigned char encA = mcpX.digitalRead(pin[0]);    // Read encoder pins
  unsigned char encB = mcpX.digitalRead(pin[1]);

  if((!encA) && (prev)) { 
    encSelect[0] = encNo;
    if(encB) {
      encSelect[1] = 1;  // clockwise
    }
    else {
      encSelect[1] = 2;  // counter-clockwise
    }
    change=true;
  }
  return encA;
}

// read the button on pin N. Change saved in butPress
unsigned char readBut(Adafruit_MCP23017 mcpX, const int pin, unsigned char prev, int encNo) {

  unsigned char butA = mcpX.digitalRead(pin);    // Read encoder pins
  if (butA != prev) {
    if (butA == HIGH) {
      butPress = encNo;
    }
  }
  return butA;  
}

// setup the encoders as inputs. 
unsigned char encPinsSetup(Adafruit_MCP23017 mcpX, const int *pin) {

  mcpX.pinMode(pin[0], INPUT);  // A
  mcpX.pullUp(pin[0], HIGH);    // turn on a 100K pullup internally
  mcpX.pinMode(pin[1], INPUT);  // B
  mcpX.pullUp(pin[1], HIGH); 
}

// setup the push buttons
void butPinsSetup(Adafruit_MCP23017 mcpX, const int pin) {
  mcpX.pinMode(pin, INPUT);
  mcpX.pullUp(pin, HIGH); 
}

void setup() {  

  mcp0.begin(0);    // 0 = i2c address 0x20

  // setup the pins using loops, saves coding when you have a lot of encoders and buttons
  for (int n = 0; n < encCount0; n++) {
    encPinsSetup(mcp0, encPins0[n]);
    encoders0[n] = 1;  // default state
  }

  // buttons and encoders are in separate arrays to allow for additional buttons
  for (int n = 0; n < butCount0; n++) {
    butPinsSetup(mcp0, butPins0[n]);
    buttons0[n]  = 0;
  }

  Serial.begin(9600); 
  Serial.println("---------------------------------------");  

  currentTime = millis();
  loopTime = currentTime;
}

void loop() {

  // check the encoders and buttons every 5 millis
  currentTime = millis();
  if(currentTime >= (loopTime + 5)){

    for (int n = 0; n < encCount0; n++) {
      encoders0[n] = readEnc(mcp0, encPins0[n], encoders0[n],n);
    }

    for (int n = 0; n < butCount0 ; n++) {
      buttons0[n] = readBut(mcp0, butPins0[n], buttons0[n], n);
    }

    loopTime = currentTime;  // Updates loopTime
  } 
  
  // when an encoder has been rotated
  if (change == true) {

    if (encSelect[0] < 100) {
      Serial.print("Enc: ");
      Serial.print(encSelect[0]);  
      Serial.print(" " );  

      switch (encSelect[1]) {
        case (1): // clockwise
          Serial.println("CW  ");
          break;
        case (2): // counter-clockwise
          Serial.println("CCW  ");
          break;
      }

      // do something when a particular encoder has been rotated.
      if (encSelect[0] == 1) {
         Serial.println("Encoder One has been used");
      }

      // set the selection to 101 now we have finished doing things. Not 0 as there is an encoder 0.
      encSelect[0] = 101;
    }
    // ready for the next change
    change = false; 
  }

  // do things when a when a button has been pressed
  if (butPress < 100) {
    Serial.print("But: ");
    Serial.println(butPress); 

    butPress = 101; 
  }
}

No the code is not cleaner, there may be less of it but that I itself in is not a virtue.

That code does not use interrupts and so is useless to use as a basis to expand it for what you want.

That is my point about libraries you don't know how to drive them do you. So what starts out looking like a quick and easy short cut turns out to be much more complicated. Just put in a little effort learning to talk directly to the chip and you can make it do what you want, not what some one else guessed you might want.

Grumpy_Mike:
That code does not use interrupts and so is useless to use as a basis to expand it for what you want.

I made a few tests and it gives accurate readings, which is more I had when I was using arduino pins without external interrupts, but you need to be polling the chip all the time.
I found nothing better so far, do you have an example code I can use?

it gives accurate readings, which is more I had when I was using arduino pins without external interrupts,

There is no way that can be true without severe errors in your Arduino code.

do you have an example code I can use?

Sorry no.

What is wrong with you writing the code, it is your project after all.