[SOLVED!] MCP23x17 interrupts and rotary encoders

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:

#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 :slight_smile:

Best regards,
Agoston

2 Likes