I have been getting to grips with rotary encoders on the Arduino, and to add a little drama I have gotten this working on the i2c bus. Here I will be showing how to set up the necessary hardware and demonstrate a program for the Arduino. I have used a similar setup to control an LED RGB light strip, with three rotary encoders to control the Red, Green and Blue and a fourth for special effects.
The i2c bus
The i2c bus allows connection of multiple devices to the Arduino on just two wires, these can be just about anything from temperature sensors to motor controllers with each device having its own address, up to eight of these can be used using just two wires from the Arduino.
For this project I’ll be using a single MCP23017 port expander, with which I can add sixteen digital I/O pins to the Arduino

The address for the expander is set on pins 15, 16 and 17 (A0, A1 and A2), for a single encoder set all of these to ground. Should you require more expanders the addresses can be set as in the table below, the MCP address is for the Arduino program.
chip address |
hardwired address | i2c address |
MCP address |
||
A2 | A1 | A0 | |||
000 | GND | GND | GND | 0x20 | 0 |
001 | GND | GND | 3v3 | 0x21 | 1 |
010 | GND | 3v3 | GND | 0x22 | 2 |
011 | GND | 3v3 | 3v3 | 0x23 | 3 |
100 | 3v3 | GND | GND | 0x24 | 4 |
101 | 3v3 | GND | 3v3 | 0x25 | 5 |
110 | 3v3 | 3v3 | GND | 0x26 | 6 |
111 | 3v3 | 3v3 | 3v3 | 0x27 | 7 |
Rotary Encoders
The encoders I’m using are the SparkFun 12-step rotary encoder with integrated push-button

Inside they have mechanical contacts that output two square waves when rotated, A and B these are 90o out of phase with each other so when rotated clockwise output A is ahead of B, and counter-clockwise output B takes the lead, this is a two bit Grey code.

So by comparing the two outputs we can determine the direction of rotation.
A Test Circuit
My test setup comprises of two rotary encoders, one Arduino Uno, one MCP23017 port expander, and a couple of resistors. External pull-up resistors are not required on the GPx input ports as the MCP23017 has these internally. Encoder A uses GPA0, GPA1 and GPA2 for the push button. Encoder B is on GPA4, GPA5, and GPA6.
Programming
My program uses the Adafruit MCP23017 and standard wire libraries. The Adafriut library addresses the GPx ports from 0-15, so GPA2 is 2, and GPB2 is 9. I have written this to output the state of rotation to the serial port at 9600 baud.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 |
#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; } } |
links:
- Rotary encoder http://www.hobbytronics.co.uk/arduino-tutorial6-rotary-encoder
- MCP23017 tutorial http://tronixstuff.com/2011/08/26/tutorial-maximising-your-arduinos-io-ports/
- MCP23017 datasheet: http://ww1.microchip.com/downloads/en/DeviceDoc/21952b.pdf
- Rotary encoder datasheet: http://www.hobbytronics.co.uk/datasheets/TW-700198.pdf
Hi,
Thank you for the very useful post. Helped me a lot. Much appreciated!
Hi Karl this is a great explanation! How would you go about using more than one MCP23017? I have 2 of them connected but would love my code to group all encoders in the same array, to easily map to a single encoder data array. Thanks for letting me know
Helle Karl,
i came across your tutorial about rotary encoder on the I2C bus and first of all I want to say thank you for sharing your expirence.
What I like to know before I start to implement this into my project, would it be easy to connect a second and a third MCP23017. I want to connect 16 encoder and 16 buttons via the mcp23017 but i’m not shure if this will work out with your code.
Thx
Sebastian
Sebastian
This should be OK in theory, I’ve not tried it. Don’t forget to set the address on each MCP, then in the software initialise additional MCP’s , eg: Adafruit_MCP23017 mcp1; and in the setup add mcp1.begin(1); and in other places add in extras along the lines what exists already for mcp0. Don’t forget though that i2c isn’t particularly fast and there are limits to how many encoders you can sensibly use.
Hey Karl,
thank you for your reply and we still could manage to get 8 encoder working on an mcp23017. but our problem ist to address the second one. unfortunately we couldn’t get it to work. do you might be have any input for us to solve this problem? i think it can’t be really so hard to address the second mcp but we are might be missing something.
by the way, its working now 😉
cheers sebastian
Works flawless, thank you.
Hey Karl, do you know about some example with a few of these chips in cascade?
Hi, thanks for your tutorial!
I have a little issue, I tried your library and it works as expected but if I turn the encoders too fast it doesn’t work properly, every now and then it reads a CCW for a CW and viceversa.
Have you experienced such problem? Might be the way I soldered the board?
Thanks for your help 🙂
Hello Francesco,
Yes I had that problem too, I got round it by not turning the knobs too fast :-). I seem to remember reading that these types of encoder are a bit poor at high speeds, something to do with the way they are constructed. I suppose you could add some de-bounce logic to the program, and try adding de-bounce hardware (google will help you with this).
Hi, thanks for your reply!
Before using MCP23017 I used a single rotary encoder and I had no issues with speed, and now I realise that this library (https://github.com/brianlow/Rotary) has all these features:
– Debounce handling with support for high rotation speeds
– Correctly handles direction changes mid-step
– Checks for valid state changes for more robust counting / noise immunity
– Interrupt based or polling in loop()
– Counts full-steps (default) or half-steps
So, now I’ll explore these subjects and learn about them 🙂
Thanks!
My MCP will stop detecting rotations and buttons pressed after a while and then starts detecting again. Could you please help?
Anyway, I was able to figure it out on other posts on the net. Basically, you cannot connect the rest pin directly to +5V. You must connect it to +5V trough a 1K Ohm resistor. Solved my issue. Now it works continuously. Hopefully this will help someone.
Hi there,
Question, you are assigning INPUT’s and Pullups to pin[0] and[1] both used for encoder A?
mcpX.pinMode(pin[0], INPUT); // A
mcpX.pullUp(pin[0], HIGH);
mcpX.pinMode(pin[1], INPUT); // B
mcpX.pullUp(pin[1], HIGH);
it seems to me you are only adressing the first Rotary encoder
Should’t it be like this? :
mcpX.pinMode(pin[0], INPUT); // ENC_A-A
mcpX.pullUp(pin[0], HIGH);
mcpX.pinMode(pin[1], INPUT); // ENC_A-B
mcpX.pullUp(pin[1], HIGH);
mcpX.pinMode(pin[3], INPUT); // ENC_B-A
mcpX.pullUp(pin[3], HIGH)
mcpX.pinMode(pin[4], INPUT); // ENC_B-B
mcpX.pullUp(pin[4], HIGH)
Funny thing though; if you comment those line’s out /* text */
The sketch still works.
i am working on a keypad on GPA0-7, and 4 encoders on GPB0-7
The 4 rotary’s work, but pretty often i get whilst turning CW, CCW value’s.
it doesnt matter with using external Pullups (10k) or with a debounce circuit)
Kind regards, and thanks for your work.
Robert
Robert,
The pinMode and pullUP is being set in a loop, look in the setup() you will see:
// 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 } where encPins0 is a 2D array, each element containing the rotaty encoder pins A and B.
I know this is an old blog but it is exactly what I’ve been looking for.
The problem is that the”MCP23017 rotary encoder test circuit” picture is missing. Broken link?
Anybody?
Looks to be OK now, I recently re-drew the diagram and wordpress has been a bit slow updating.
Hi, i try your code and works fine for one of my encoder (the one on GPA0/GPA1) but i have 4 encoders on GPB0/GPB1 GPB2/GPB3 GPB4/GPB5 GPB6/GPB7 and I can not figure out how to configure the pins.
here i have to set the number of the encoders
const int encCount0 = 5; // number of rotary encoders
const int encPins0[encCount0][2] = {
{0,1}, // enc:0 AA GPA0,GPA1 – pins 21/22 on MCP23017
{?,?} // enc:1 BB GPB1,GPA2
…
};
Thanks