Go Down

Topic: [SOLVED!] MCP23x17 interrupts and rotary encoders (Read 4206 times) previous topic - next topic

dagoston93

Mar 24, 2017, 05:22 pm Last Edit: Apr 23, 2017, 10:53 pm by dagoston93
Hi,

I am thinking about connecting some rotary encoders to an MCP23017 or an MCP23S17 and use its interrupt functionality with an Arduino Mega 2560 to detect the movements of the encoder.
I studied the Adafruit library and the datasheet of the IC's and I am not completely sure if i would be able to correctly determine the direction of rotation and could capture each(or most the) rotations.
I want later add more MCP23x17's and more rotary encoders. In total about 12-14 encoders.
I also don't want to slow down the arduino too much.(some serial communications and reading of key matrix will be done simultaneously.)

Does anyone has any experience with such a project?
Can this setup work properly?
Or what other IO expander would you recommend for this purpose?

Thanks in advance for any help,
Agoston

PaulRB

Go for the "S" version. SPI is much faster than i2c, and that could be important for a large number of rotary switches.

dagoston93

Thank you, I am thinking about getting the SPI version.
For now I have only a few of the I2C versions and trying to get the interrupts working to test.
I am using the Adafruit MCP23017 library.
I have found very little information about getting the interrupts working, mainly the only usable code i found is the example included in the library.

After a few tries i could get that work fine.

Then I connected an encoder directly to on of the Arduino's interrupt and another one to the mcp and started testing.

In the code I am changing a counter and sending its value to the serialport when the encoder connected to the arduino is moved, and flashing an LED when the encoder connected to the IC is moved in any directions (this setup is obviously only for testing).

It works fine if I only use the encoder connected to the arduino. But if I move the other one two things happen depending on what I am doing in the interrupt function.
If I don't try to read the last interrupt pin and its value, then the LED blinks at the first movement but never again, and the other encoder keeps working.
But if I try to read it and as said in the example code, I try to clear the interrupt condition(with the same lines of code), my arduino freezes after the first movement of the encoder connected to mcp, the LED never blinks and the readings on the serial port stop coming.
Here is my code:

Code: [Select]

#include <Wire.h>
#include <Adafruit_MCP23017.h>

Adafruit_MCP23017 mcp;

byte pinA = 3;
byte pinB = 8;
byte intPin = 2;

byte redLedPin = 9;
byte greenLedPin = 4;

byte mcpPinA = 7;
byte mcpPinB = 6;

byte volatile counter = 0;

long greenOnTime=0;
bool isGreenOn = false;

void encInt()
{
  if(digitalRead(pinA) == digitalRead(pinB))
  {
    counter++;
  }else{
    counter--;
  }
  Serial.println(counter);
}

void mcpInt()
{
  //detachInterrupt(intPin);
  greenOnTime = millis();
 
  //byte pin=mcp.getLastInterruptPin();
  //byte val=mcp.getLastInterruptPinValue();
  //while((mcp.digitalRead(mcpPinB) && mcp.digitalRead(mcpPinA)) );
  //EIFR = 0x01;
  //mcp.readGPIOAB();
  //attachInterrupt(digitalPinToInterrupt(intPin), mcpInt, FALLING);
 
}

void setup() {
  // put your setup code here, to run once:
  pinMode(pinA, INPUT_PULLUP);
  pinMode(pinB, INPUT_PULLUP);
  pinMode(intPin, INPUT_PULLUP);
 
  attachInterrupt(digitalPinToInterrupt(pinA), encInt, FALLING);
  attachInterrupt(digitalPinToInterrupt(intPin), mcpInt, FALLING);
 
  Serial.begin(9600);
  mcp.begin();
 
  mcp.setupInterrupts(true,false,LOW);

  mcp.pinMode(mcpPinA, INPUT);
  mcp.pullUp(mcpPinA, HIGH);
  mcp.setupInterruptPin(mcpPinA, FALLING);

  mcp.readGPIOAB();
 
  greenOnTime = millis();
}

void loop() {
  // put your main code here, to run repeatedly:
  if(millis() < greenOnTime + 250)
  {
    if(!isGreenOn)
    {
      digitalWrite(greenLedPin, HIGH);
      isGreenOn = true;   
    }
  }
  else
  {
    if(isGreenOn)
    {
      digitalWrite(greenLedPin, LOW);
      isGreenOn = false;
    }
  }
}


In the mcpInt() function you can see the lines of code i tried to get it work.
I tried them in all logical combinations, they are from the example and the readGPIOAB(); idea is from a website i found when googling...
But none of these solve the problem, in all cases one of the above mentioned happens.
And its also strange that same thing happens if I add pullup resistor to the SDA and SCL line or not.
I tried with more IC's to test if one is broken but all of them gives the same result.

Here is my wireing diagram:



Does anyone have an idea what might I be missing or doing wrong?

Thanks in advance for any help,
Agoston

Grumpy_Mike

The mega already has PULLUP resistors on it there is no need to add more.

Read the data sheet of that chip, use the interrupt lines to trigger an ISR but look at the larptch register to see the state of the pins when the interrupt occurred

dagoston93

Thanks for the answer :)
So you recommend not using the Adafruit library and trying to do it myself?
I will try so :)

dagoston93

Hi,
After reading the datasheet of the IC and finding this website, I learned how the chip works.
I used the example of the link above to create a test code, where the main parts are the same as in the example, and hooked up the built in push button of the encoder to test the code before I try to read the encoder. In my code I use two leds one is continously blinking, one should only blink when i push the button.
But I get the same results as I wrote above.
If I try to read the INTCAP or the GPIO registers after an interrupt, the only thing that happens is that arduino freezes, the blinking led stays on or off, regarding the state it was in the moment I push the button. (I tried also reading the INTF registers.)
If I comment out those lines, then the blinking led keeps blinking and the other also blinks once, but never again. Of course I understand that in this case the interrupt condition is not cleared, so the MCP23017 keeps waiting for me to read the interrupt.
But I don't understand, why the other case occurs...

Here is my code:

Code: [Select]

#include <Wire.h>

#define IODIRA 0x00 //IO direction 0 = output, 1 = input (def)
#define IODIRB 0x01
#define IOPOLA 0x02 //IO polarity 0 = normal, 1 = reverse
#define IOPOLB 0x03
#define GPINTENA 0x04 //Interrupt on change 0 = disable, 1 = enable
#define GPINTENB 0x05
#define DEFVALA 0x06 //Default value (interrupts on opposite)
#define DEFVALB 0x07
#define INTCONA 0x08 //Interrupt control 0 = compare to previous value, 1 = compare to DEFVAL
#define INTCONB 0x09
#define IOCON 0x0A  // IO Configuration: Bank/Mirror/SEQOP/DISSLW/HAEN/ODR/INTPOL/NotImplemented
//#define IOCON 0x0B //Same as 0x0A
#define GPPUA 0x0C //Pull up resistors 0 = off, 1 = on
#define GPPUB 0x0D
#define INTFA 0x0E //Interrupt flag 0 = no interrupt, 1 = pin caused interrupt
#define INTFB 0x0F
#define INTCAPA 0x10 //Interrupt capture: value of GPIO at time of interrupt
#define INTCAPB 0x11
#define GPIOA 0x12  //Port value
#define GPIOB 0x13
#define OLATA 0x14  //Output latch
#define OLATB 0x15

#define address 0x20 //MCP23017 adress
#define redLedPin 9
#define greenLedPin 4
volatile byte flashes;

//Write the same data on both ports to a register
void mcpWriteBoth(byte reg, byte data)
{
  Wire.beginTransmission(address);
  Wire.write(reg);
  Wire.write(data); //Port A
  Wire.write(data); //Port B
  Wire.endTransmission();
}
//Read a byte from register
unsigned int mcpRead(byte reg)
{
  Wire.beginTransmission(address);
  Wire.write(reg);
  Wire.endTransmission();
  Wire.requestFrom(address, 1);
  return Wire.read();
}
//Handle interrupt
void handleInterrupt()
{
  flashes++;
  mcpRead(INTCAPA);
  mcpRead(INTCAPB);
  //mcpRead(GPIOA);
  //mcpRead(GPIOB);
}
void setup() {

 
  pinMode(redLedPin, OUTPUT);
  pinMode(greenLedPin, OUTPUT);
 

  Wire.begin();
  //Config MCP
  mcpWriteBoth(IOCON, 0b01100000); //Mirror interrupts, disable sequential mode
  mcpWriteBoth(GPPUA, 0xFF); //Turn on pullup resistors
  mcpWriteBoth(IOPOLA, 0xFF); //Invert polarity
  mcpWriteBoth(GPINTENA, 0xFF); //Enable interrupts for all pins
  //Read ports to clear them
  mcpRead(GPIOA);
  mcpRead(GPIOB);
  //Setup interrupt
  attachInterrupt(digitalPinToInterrupt(2), handleInterrupt, FALLING);
}

void loop() {
  //Blink
  digitalWrite(greenLedPin, HIGH);
  delay(250);
  digitalWrite(greenLedPin, LOW);
  delay(250);
 
  //Blink if needed
  if(flashes)
  {
    digitalWrite(redLedPin, HIGH);
    delay(250);
    digitalWrite(redLedPin, LOW);
    flashes--;
  }


}


The only thing i changed in the circuit is that I removed the connections between mcp and the rotary encoder, and i connected one end of the push button to gnd and the other to pin 21 (GPA0) on the mcp.
And I also removed the pullup resistors from the i2c lines, since you told me it's built in the mega.
Is the mistake in my code? Or might it be in the schematics?

Thanks in advance for any help,
Agoston

Grumpy_Mike

You can not print in an ISR, likewise you can not access the I2C bus because these rely on interrupts and it is disabled during an ISR.

dagoston93

Ohh, I thank you!
Now I changed the code to use a bool in the interrupt to indicate to the loop function that an interrupt needs to be handled.
It works finally, so I can begin testing with encoders :)

dagoston93

#8
Apr 13, 2017, 03:16 pm Last Edit: Apr 13, 2017, 03:31 pm by dagoston93
Hi,
Finally I received the SPI versions of the chip.
So I replaced the 23017 with the 23S17 in my circuit, I made the correct connections to the SPI bus,
and did some test with buttons and LEDs successfully.
Now started to experiment with rotary encoder.
Two LEDs and a rotary encoder is connected to the chip and I set interrupt for one of the encoder pins, and when an interrupt occurs I check whether both pins of the encoder is at the same level or not to determine the direction of the rotation. If rotating clockwise flashing a red LED if anti-clockwise, flashing a green one.
But the problem is, that if I rotate one step clockwise, both LED flash once and if to the other direction the green one flashes twice.
For me it seemed that it is because the interrupt occurs on every change of the square wave generated by the encoder.
I thought that it might be because of the INTCON register but on the interrupt pin it is set to compare to DEFVAL, so it shouldn't happen so. The strange thing is that if I change the INTCON to 0 the same thing happens. So the register looks to be ineffective...
The two LEDs are connected to A0 and A1 pins and the encoder is connected to A6 and A7.

I am using this debounce circuit for the encoder:


And my code is based on the posts here:
https://www.gammon.com.au/forum/bbshowpost.php?id=10945&page=3

And here is my code:

Code: [Select]

#include <SPI.h>

// MCP23017 registers (everything except direction defaults to 0)

const byte  IODIRA   = 0x00;   // IO direction  (0 = output, 1 = input (Default))
const byte  IODIRB   = 0x01;
const byte  IOPOLA   = 0x02;   // IO polarity   (0 = normal, 1 = inverse)
const byte  IOPOLB   = 0x03;
const byte  GPINTENA = 0x04;   // Interrupt on change (0 = disable, 1 = enable)
const byte  GPINTENB = 0x05;
const byte  DEFVALA  = 0x06;   // Default comparison for interrupt on change (interrupts on opposite)
const byte  DEFVALB  = 0x07;
const byte  INTCONA  = 0x08;   // Interrupt control (0 = interrupt on change from previous, 1 = interrupt on change from DEFVAL)
const byte  INTCONB  = 0x09;
const byte  IOCON    = 0x0A;   // IO Configuration: bank/mirror/seqop/disslw/haen/odr/intpol/notimp
const byte  GPPUA    = 0x0C;   // Pull-up resistor (0 = disabled, 1 = enabled)
const byte  GPPUB    = 0x0D;
const byte  INFTFA   = 0x0E;   // Interrupt flag (read only) : (0 = no interrupt, 1 = pin caused interrupt)
const byte  INFTFB   = 0x0F;
const byte  INTCAPA  = 0x10;   // Interrupt capture (read only) : value of GPIO at time of last interrupt
const byte  INTCAPB  = 0x11;
const byte  GPIOA    = 0x12;   // Port value. Write to change, read to obtain value
const byte  GPIOB    = 0x13;
const byte  OLLATA   = 0x14;   // Output latch. Write to latch output.
const byte  OLLATB   = 0x15;


const byte  DEVICE_ADDRESS = 0x20;  // MCP23017 is on I2C port 0x20

const byte ssPin = 10;   // slave select pin, if non-zero use SPI
const byte expanderPort = 0x20;

bool interrupted = false;
const byte intPin = 3;

byte counter = 0;

//Handle interrupts
void handleInterrupt()
{
  interrupted = true;
}

//Write to expander
void expanderWrite (const byte reg, const byte data )
  {
     digitalWrite (ssPin, LOW);
     SPI.transfer (expanderPort << 1);  // note this is write mode
     SPI.transfer (reg);
     SPI.transfer (data);
     digitalWrite (ssPin, HIGH);
}

//Read from expander
byte expanderRead(const byte reg)
{
     byte data = 0;
     digitalWrite (ssPin, LOW);
     SPI.transfer ((expanderPort << 1) | 1);  // note this is read mode
     SPI.transfer (reg);
     data = SPI.transfer (0);
     digitalWrite (ssPin, HIGH);

     return data;
}

void setup ()
{
    Serial.begin(9600);
   
    digitalWrite (ssPin, HIGH);
    SPI.begin ();
    pinMode (ssPin, OUTPUT);
    pinMode(intPin, INPUT_PULLUP);
    attachInterrupt(digitalPinToInterrupt(intPin), handleInterrupt, FALLING);

  //Config MCP
  expanderWrite (IOCON, 0b01100000); //Mirror interrupts, disable sequential mode
 
  expanderWrite (IODIRA, 0b11000000);
  expanderWrite (GPPUA, 0b11000000);
  expanderWrite (IOPOLA, 0b11000000);
  expanderWrite (INTCONA, 0b10000000);
  expanderWrite (DEFVALA, 0b00000000);
  expanderWrite (GPINTENA, 0b10000000);
 
 
 
}

void loop ()
{
 
  if(interrupted)
  {
    interrupted = false;
    byte data = expanderRead(GPIOA);
    expanderRead(GPIOB);
    if(data == 0b11000000)
    {
      counter++;
      expanderWrite(GPIOA, 1);
    }else{
      counter--;
      expanderWrite(GPIOA, 2);
    }
    Serial.println(counter);
    delay(100);
    expanderWrite(GPIOA, 0);
    delay(100);
   
  }

}


What might the problem be?

Thanks in advance for any help :)

dagoston93

#9
Apr 23, 2017, 10:37 pm Last Edit: Apr 23, 2017, 11:06 pm by dagoston93
Hi all,
After much thinking and testing i finally managed to work out how to read rotary encoders with the MCP23S17.
I haven't found any libraries for this purpose, neither a working solution(without adding extra hardware) so I post here the code I have come up with:

Code: [Select]

#include <SPI.h>

// MCP23S17 registers

const byte  IODIRA   = 0x00;   // IO direction  (0 = output, 1 = input (Default))
const byte  IODIRB   = 0x01;
const byte  IOPOLA   = 0x02;   // IO polarity   (0 = normal, 1 = inverse)
const byte  IOPOLB   = 0x03;
const byte  GPINTENA = 0x04;   // Interrupt on change (0 = disable, 1 = enable)
const byte  GPINTENB = 0x05;
const byte  DEFVALA  = 0x06;   // Default comparison for interrupt on change (interrupts on opposite)
const byte  DEFVALB  = 0x07;
const byte  INTCONA  = 0x08;   // Interrupt control (0 = interrupt on change from previous, 1 = interrupt on change from DEFVAL)
const byte  INTCONB  = 0x09;
const byte  IOCON    = 0x0A;   // IO Configuration: bank/mirror/seqop/disslw/haen/odr/intpol/notimp
const byte  GPPUA    = 0x0C;   // Pull-up resistor (0 = disabled, 1 = enabled)
const byte  GPPUB    = 0x0D;
const byte  INFTFA   = 0x0E;   // Interrupt flag (read only) : (0 = no interrupt, 1 = pin caused interrupt)
const byte  INFTFB   = 0x0F;
const byte  INTCAPA  = 0x10;   // Interrupt capture (read only) : value of GPIO at time of last interrupt
const byte  INTCAPB  = 0x11;
const byte  GPIOA    = 0x12;   // Port value. Write to change, read to obtain value
const byte  GPIOB    = 0x13;
const byte  OLLATA   = 0x14;   // Output latch. Write to latch output.
const byte  OLLATB   = 0x15;

const byte ssPin = 38;   
const byte numOfMCPs = 2;
const byte ports[numOfMCPs] =  {0x20, 0x21};
const byte intPins[numOfMCPs] = {18,19};

bool interrupted[numOfMCPs] = {false,false};
static byte counters[numOfMCPs][8] = {{0,0,0,0,0,0,0,0 }, {0,0,0,0,0,0,0,0 }};
byte portALast[numOfMCPs] = {0b00000000, 0b00000000};
static byte counters[numOfMCPs][8] = {{0,0,0,0,0,0,0,0 }, {0,0,0,0,0,0,0,0 }};

//ISR functions
void handleInterrupt() { interrupted[0] = true; }
void handleInterrupt2(){ interrupted[1] = true; }

//Write to expander
void expanderWrite (const byte reg, const byte data, const byte port)
  {
     digitalWrite (ssPin, LOW);
     SPI.transfer (port << 1);  // note this is write mode
     SPI.transfer (reg);
     SPI.transfer (data);
     digitalWrite (ssPin, HIGH);
}

//Read from expander
byte expanderRead(const byte reg, const byte port)
{
     byte data = 0;
     digitalWrite (ssPin, LOW);
     SPI.transfer ((port << 1) | 1);  // note this is read mode
     SPI.transfer (reg);
     data = SPI.transfer (0);
     digitalWrite (ssPin, HIGH);

     return data;
}

void setup()
{

  SPI.begin();

  pinMode (ssPin, OUTPUT);
  digitalWrite (ssPin, HIGH);

  pinMode(intPins[0], INPUT_PULLUP);   
  attachInterrupt(digitalPinToInterrupt(intPins[0]), handleInterrupt, FALLING);
 
  pinMode(intPins[1], INPUT_PULLUP);   
  attachInterrupt(digitalPinToInterrupt(intPins[1]), handleInterrupt2, FALLING);

  for(int i = 0; i<numOfMCPs; i++)
  {
      //Config MCP
      expanderWrite (IOCON, 0b01101000, ports[i]); //Mirror interrupts, disable sequential mode, enable hardware adressing
   
      //Set PORT A registers
      expanderWrite (IODIRA, 0b11111111, ports[i]); //1 is input
      expanderWrite (GPPUA, 0b00000000, ports[i]); 
      expanderWrite (IOPOLA, 0b11111111, ports[i]);
      expanderWrite (INTCONA, 0b00000000, ports[i]);
      expanderWrite (GPINTENA, 0b11111111, ports[i]);
   
      //Set PORT B registers
      expanderWrite (IODIRB, 0b11111111, ports[i]); //1 is input
      expanderWrite (GPPUB, 0b00000000, ports[i]); 
      expanderWrite (IOPOLB, 0b11111111, ports[i]);
      expanderWrite (GPINTENB, 0b00000000, ports[i]);
  } 

}

void loop()
{
  for(int j = 0; j < numOfMCPs; j++)
  {
      //Check if the encoder has caused an ineterrupt
      if(interrupted[j])
      {
       
        interrupted[j] = false;
        byte portA = expanderRead(GPIOA, ports[j]);
        byte portB = expanderRead(GPIOB, ports[j]);
   
        //Check which pin of the encoder caused interrupt and and check the value of pin A
       
        byte mask = 0;
        byte i = 0;
        byte aLast;
        byte aCurrent;
               
        for(i = 0; i < 8; i++)
        {
   
            mask = 1 << i;
            aLast = portALast[j] & mask;
            aCurrent = portA & mask;
   
            if(aLast ^ aCurrent)
            {
              break;
            }
         
        }

        //Then determine the direction of rotation if its the raising edge of the square wave
        if(aCurrent)
        {
          if(!aLast)
          {
            portALast[j] = portA;
            byte bState = mask & portB;
           
            if(bState) //Clockwise
            {
             
             counters[j][i]++;
            }else{ //Counter-Clockwise
              counters[j][i]--;;
            } 

            Serial.print("Chip [");
            Serial.print(j);
            Serial.print("] Var (");
            Serial.print(i);
            Serial.print(") : ");
            Serial.println(counters[0][i]);           
           
          }
        }else{
          if(aLast)
          {
            portALast[j] = portA;
          }
        }
      }
  } 
}



Pin A of the first encoder connects to A0 on the MCP and pin B to port B0 and so on.
This way you can connect 8 encoders to an MCP and you can connect 8 MCP's the same SS pin, if you have that much interrupt pins. (I am using the MEGA which has only six interrupts.)
The code should also work with the I2C version with modified expanderWrite() and expanderRead() functions.
I use the debounce circuit with each encoders I mention in my previous post.
For me this code works quite well with two chips connected to SPI.

I hope this sketch will once help someone.
If there is something I should do different or any question about the code, please write me :)

Best regards,
Agoston
 

Govner

The only thing i changed in the circuit is that I removed the connections between mcp and the rotary encoder, and i connected one end of the push button to gnd and the other to pin 21 (GPA0) on the mcp.
And I also removed the pullup resistors from the i2c lines, since you told me it's built in the mega.
Is the mistake in my code? Or might it be in the schematics?

Thanks in advance for any help,
Agoston
dagoston93,

I just wanted to thank you for providing your most helpful code. Until yours, I had found many, many examples that addressed this subject. Not until your post was I able to understand this. THANK YOU so very much for writing your code so that it could be understood by your target audience. All too often, the coding is available but impossible for a newbie like myself to gain any understanding, much less LEARN from it. I can't thank you enough after spending literally DAYS working on this. Now, it's simple to me but thanks to you.  Best regards and good luck sir.

Gov


Govner


dagoston93,

Hey.. back with a question after working with it for a while.

Any idea why the red led blinks twice for each button push?  I thought it might be contact bounce (which was quite pronounced on my encoder switch). I added a Schmitt trigger which cleaned it up completely on the scope. I tried FALLING & CHANGE (RISIING locks up cuz of the Active LOW int logic).

Any, pressing the button 4 times, for example, gets 8 on/off cycles instead of 4. Maybe that's by design but I don't think so.

Any ideas?  Thanks in advance anyone.

Gov


Govner



Sorry.. disregard the previous question about 'double blinks'.  I just realized that you are driving your LEDs wired directly to the Arduino pins. Conversely, I am using a Trinket SAMD21, M0 without spare pins (thus the expander chip). So, when the interrupt occurs, I was issueing 'mcp.Writes' and generating the additional interrupt.. i.e. double-blink of the RED.  Makes sense? At least that's what I believe is happening.

Fun stuff. Thanks again.


Go Up