Multiplexing rotary encoders or using port expander. What will work with the library?

Hey
The need for multiple rotary encoders is strong in my projects. I'm talking 16. Using 32 inputs is possible with the 3.6, but for example with the 4.0 it's getting difficult under my circumstances and this is what I will head for.

Also it feels a bit "unrefined" to just use 32 inputs... is it?

So I'm looking at normal multiplexer two 1:16 for example or a port expander like MCP23008 which would give me the inputs over i2c.

Before I go and order chips and get out my breadboard. Is any of those ideas even feasible with the rotary encoder library for teensy?

What would be the "right" way to do this. (Except throwing a pic at every encoder, that's over my head)
 
I assume you are talking about the usual mechanical encoders for human input, not high speed encoders for motor applications? If so, I'd use a simple polled algorithm instead of the usual interrupt based algorithm and use two simple shift registers to multiplex the encoders. Here a (untested) schematic idea with an 8bit shift registers as an example

encoder.jpg

Connect the A/B line to two pins on the Teensy. With the load line you latch the current values of the encoder switches. Then clock in one after the other. If you define all encoders on the same two pins (A/B) you'll get what you want. You can find an example of the polled encoder algorithm here: https://github.com/luni64/PollEncoder

Here a simple example how to use the polled encoders (you find others in the GitHub repo)

Code:
#include "pollEncoder.h"
using namespace PollEncoder;

Encoder enc(0, 1, INPUT_PULLUP); // generate an encoder on pins 0/1

void setup()
{
  while(!Serial);
  Serial.println("Start");
}

int old_value;

void loop()
{
    enc.tick(); // call this as often as possible

    int value = enc.value;

    if (value != old_value)
    {
        old_value = value;
        Serial.printf("v:%d\n", value);
    }
}


And here some untested pseudo code how to read out the multiplexed encoders:

Code:
#include "Arduino.h"
#include "pollEncoder.h"

using namespace PollEncoder;

Encoder enc[]{{0, 1}, {0, 1}, {0, 1}, {0, 1}}; // Array of encoders, all on pins 0 / 1 
constexpr int nrOfEncoders = sizeof(enc) / sizeof(enc[0]);

void read() // untested pseudo code
{
    DO_LATCH   // code to latch the current values

    for (int i = 0; i < nrOfEncoders; i++)  // clock in all encoders and tick the corresponding encoder to update its counter. Do this as quickly as possible 
    {
        DO_CLOCK
        enc[i].tick();  // this will update the encoder
    }
}

IntervalTimer timer;

void setup()
{
    while (!Serial);
    Serial.println("Start");

    // read in the encoders in the background from a timer;
    // A tick frequency of 2kHz, should be more than enough for mechanical encoders
    timer.begin(read, 500);
}

void loop()  // just display the values
{
    for (int i = 0; i < nrOfEncoders; i++)
    {
        Serial.printf("encoder%d: %d\n", i, enc[i].value);
    }
    Serial.println("----------------------------------");

    delay(200);  // print current values every 200ms
}
 
Wow! Thank you so much. This looks very promising! I will certainly test this out. Yes, it's rotary encoders moved by human hands, usually max 2 at a time. Also it is for a midi sequencer which is timing sensitive, so the polling method could be beneficial.
Thank you again. I appreciate you taking the time and I will keep this updated on my progress. But it will be a while :)
 
Luni's encoder library

I assume you are talking about the usual mechanical encoders for human input, not high speed encoders for motor applications? If so, I'd use a simple polled algorithm instead of the usual interrupt based algorithm and use two simple shift registers to multiplex the encoders. Here a (untested) schematic idea with an 8bit shift registers as an example
...
You can find an example of the polled encoder algorithm here: https://github.com/luni64/PollEncoder

Here a simple example how to use the polled encoders (you find others in the GitHub repo)

Luni,
thanks for that reference. I was looking for a simpler, easier to understand library than that of PJ and this does the job nicely.
BUT, I am experiencing a strange phenomenon, with your code and with PJ's library. I am not sure what to make of it and I hope you can give me some useful pointers.

I am using low cost rotary encoders from Aliexpress. With your code and with that of PJ, the encoder advances by two counts for every detent position. This happens reliably for two encoders and I think it is not contact bounce. I have dealt with it in the following manner:
Code:
      if(valueL % 2 == 0)
        Serial.printf("L:%d\n", valueL/2);
This reliably gives me counts smoothly increasing or decreasing by one.
Below is my code, taken from your example and slightly modified
Code:
#include "Arduino.h"
#include "pollEncoder.h"

using namespace PollEncoder;

Encoder encL(5,4, INPUT_PULLUP); // generate an encoder on pins 5/4
Encoder encR(9,6, INPUT_PULLUP); // generate an encoder on pins 9/6
IntervalTimer timer;

void setup() {
  
  timer.begin(tickAll, 1000);
  while(!Serial);
  Serial.println("Start");
}

void tickAll(){
  encL.tick();
  encR.tick();
}

int old_valueR;
int old_valueL;

void loop() {

  int valueL = encL.value;

  if (valueL != old_valueL){
      old_valueL = valueL;
      if(valueL % 2 == 0)
        Serial.printf("L:%d\n", valueL/2);
  }

  int valueR = encR.value;

  if (valueR != old_valueR){
      old_valueR = valueR;
      if(valueR % 2 == 0)
        Serial.printf("R:%d\n", valueR/2);
  }
}
 
I was looking for a simpler, easier to understand library
Glad you like it. It is a common misunderstanding that you need to use interrupts to catch all of the bouncing signals. In fact the underlying gray code (aka quadrature signal) is totally insensitive to bouncing. As long as you sample with at least twice the maximum pulse frequency (not the bounce frequency) the result will always end up correctly no matter if you miss bounces or not.

With your code and with that of PJ, the encoder advances by two counts for every detent position. This happens reliably for two encoders and I think it is not contact bounce.

This is quite common. See e.g. here https://forum.pjrc.com/threads/5847...ncoder-Library?p=225691&viewfull=1#post225691.

BTW: You can simplify your code by directly dividing the result when you read it.
Code:
void loop()
{
    int valueL = encL.value / 2;

    if (valueL != old_valueL)
    {
        old_valueL = valueL;
        Serial.printf("L:%d\n", valueL);
    }

    int valueR = encR.value / 2;

    if (valueR != old_valueR)
    {
        old_valueR = valueR;
        Serial.printf("R:%d\n", valueR);
    }
}

You can also think of adding a getValue() function to the encoder class:

File: PollEncoder.h
Code:
#pragma once

// https://www.mikrocontroller.net/articles/Drehgeber

#include "core_pins.h"
#include <atomic>

namespace PollEncoder
{
    class Encoder
    {
    public:
        Encoder(int _pinA, int _pinB, int mode = INPUT) : value(0), pinA(_pinA), pinB(_pinB)
        {
            pinMode(_pinA, mode);
            pinMode(_pinB, mode);
        }

        volatile std::atomic<int> value;   // use atomic since tick might be called by a timer ISR -> prevent issues

        int getValue(){return value/2;} //<==== Add this

        void tick();

    protected:
        const int pinA, pinB;
        unsigned last;
    };
}
 
Since I wanted to try multiplexed encoders for quite some time and I wanted to test the new Eagle version anyway, I did a quick 74HC165 based test board. (Eagle files and Gerbers on github (https://github.com/luni64/PollEncoder) if somebody wants to try). It can multiplex up to 8 encoders + 8 switches. The board can be daisy chained to extend the number of encoders. Don't know yet if it works I'll post results when I got the boards.

On the Teensy side it requires 5 pins (A/B SW CLK and LOAD)

3d.jpg
 

Attachments

  • ENC_MPX_V0.1.pdf
    47.7 KB · Views: 264
So I downloaded eagle to have a look at this. Wow, it's different than from 10 years ago. Also it's autodesk now...

Anyway.
Two questions:
1. Why the pull downs on the encoders?
2. How about voltage level? I don't know much about encoders, but they are labeled "5V" mostly. Does this work with non 5v tolerant teensys?
 
So I downloaded eagle to have a look at this. Wow, it's different than from 10 years ago. Also it's autodesk now...

Yes, looks a bit different :) I'm using Eagle for about 35 years now, good thing is that despite the GUI, the underling workflow, the key shortcuts etc didn't really change and is still very effective. They improved the manual routing capabilities the last years, the autorouter is as bad as it always was, but for this board I simply was to lazy and let the autorouter do whatever it wanted to do.

1. Why the pull downs on the encoders?

The usual mechanical encoders are simply switches with a common gnd or vcc. You need to either pull them up or down. If you connect them directly to the teensy you can use the internal pullups but this time they go to the shift register which wouldn't be happy with floating pins. I chose a pull down since the common gnd was easier to route than a common vcc.

2. How about voltage level? I don't know much about encoders, but they are labeled "5V" mostly. Does this work with non 5v tolerant teensys?
The 74HC series is happy with vcc 2V-7V so you'd use the 3.3V from teensy for VCC. The mechanical encoders don't care about vcc. If you plan to use encoders with TTL output you can use a 74LV165 instead. They can be operated by 3.3V but the inputs are 5V tolerant. But it might be difficult to get them in PDIP version.
 
I meanwhile found time to do some experiments with the multiplexed encoders. The setup is a bit messy (need to wait for the boards from china) but works nicely. Schematic, BOM, Gerbers etc here https://github.com/luni64/EncPlex/tree/master/extras

Anmerkung 2020-02-16 191925.jpg

I added a class for the multiplexed encoders to the original library and moved everything to a new repo https://github.com/luni64/EncPlex. I also added callbacks which fire when one of the encoders changes its value this is quite useful for things like controlling menus.

Here a quick example showing how to use the callbacks

Code:
#include "EncPlex.h"

constexpr unsigned encoderCount = 4; //number of attached encoders (daisy chain shift regesters for more than 8)
constexpr unsigned pinLD = 14;       //load pin for all shift registers)
constexpr unsigned pinCLK = 15;      //clock pin for all shift registers
constexpr unsigned QH_A = 0;         //output pin QH of shift register A
constexpr unsigned QH_B = 1;         //output pin QH of shift register B
                                     //74165 datasheet: http://www.ti.com/product/SN74HC165

EncPlex74165 encoders(encoderCount, pinLD, pinCLK, QH_A, QH_B);

void encoderCallback(uint32_t encNr, int32_t value)
{
    Serial.printf("Encoder %d: %3d\n", encNr, value);
}

void setup()
{
    (new IntervalTimer)->begin([] { encoders.tick();}, 500);  // tick with 2kHz, should be more than enough for mechanical encoders

    encoders.setCallback(encoderCallback);
}

void loop()
{
}

Output after playing a bit with the encoders:

Code:
Encoder 0:   1
Encoder 0:   2
Encoder 0:   3
Encoder 0:   4
Encoder 1:  -1
Encoder 1:  -2
Encoder 1:  -3
Encoder 1:  -4
Encoder 1:  -5
Encoder 2:   1
Encoder 2:   2

Edit: BTW: using a T3.2 and 4 encoders one tick takes about 5µs only. -> At 2kHz tick frequency the library would generate about 1% background load only.
 
Thank you so much Luni for your work on this and for sharing it with us here.
I have been researching the problem of using more than 4 or 5 encoders for a while and there really are few or none convincing feasible solutions around. I was in fact amazed how many people developed their own solutions. To the point of the "i2c encoder" kickstarter project which is about 5$ per encoder board and throws a dedicated pic at each encoder.

A good solution for the low-end skills DIY-guy like me, who wants 8 or even 16 or more encoders without spending a fortune for such I2c encoder boards is certainly not only helping me along the way.

I'm yet to receive both encoders and multiplexers but I'm looking forward to trying this.
 
Luni's encoder library

Glad you like it. It is a common misunderstanding that you need to use interrupts to catch all of the bouncing signals. In fact the underlying gray code (aka quadrature signal) is totally insensitive to bouncing. As long as you sample with at least twice the maximum pulse frequency (not the bounce frequency) the result will always end up correctly no matter if you miss bounces or not.

You can also think of adding a getValue() function to the encoder class:

File: PollEncoder.h
Code:
#pragma once

// https://www.mikrocontroller.net/articles/Drehgeber

#include "core_pins.h"
#include <atomic>

namespace PollEncoder
{
    class Encoder
    {
    public:
        Encoder(int _pinA, int _pinB, int mode = INPUT) : value(0), pinA(_pinA), pinB(_pinB)
        {
            pinMode(_pinA, mode);
            pinMode(_pinB, mode);
        }

        volatile std::atomic<int> value;   // use atomic since tick might be called by a timer ISR -> prevent issues

        int getValue(){return value/2;} //<==== Add this

        void tick();

    protected:
        const int pinA, pinB;
        unsigned last;
    };
}

Luni,
I have been using your encoder library for a while now and I think it is really nice. I like its lean elegance. I added a getValue() function as you suggested and also added a write() function so that I could preset the encoder to any value. This is very useful.

I have a minor problem in that on the first rotation it must rotate through two detents and not one to deliver the first reading. Thereafter it works as expected. I haven't looked into it to see why this is happening but I will soon take a break from my project to look into it. It is bound to be something very simple.
 
I have been using your encoder library for a while now and I think it is really nice. I like its lean elegance. I added a getValue() function as you suggested and also added a write() function so that I could preset the encoder to any value. This is very useful.

Great, did you try the follow up lib in https://github.com/luni64/EncPlex ? This has a few more features like callbacks. The problem you observe with the start values is most probably due to a wrong initialization. The lib assumes that both switches are open at startup. This is often true but of course not always. I'll fix that in the next release.
 
Looks like China is back! Got some test boards yesterday which I ordered a couple of weeks ago.

Testsetup.jpg

The multiplexed encoder library described above works nicely with it.
 
Hi luni.

On two different computers, Win7 and Win10, IDE 1.8.13 - TD1.53, installed the ExcPlex library downloaded from Github two days ago and discovered issues in Examples.

Only two examples appear on the menu being 01_HelloEncoder and 04_Multiplexer_4067.

01_HelloEncoder is empty.
04_Multiplexer_4067 appears intact.

I opened the downloaded Zip and was able to retrieve the contents of Multiplexed_74165 from there.

Now to solder some 74165..
 
Sorry for the late answer, didn't see your post.

installed the ExcPlex library downloaded from Github
Actually this lib is not maintained anymore. I did a much better one a couple of month ago, but since I was distracted byCOVID I never finished the documentation and left everything in the development branch. https://github.com/luni64/EncoderTool/tree/develop. There is some rudimentary documentation in the accompanying WIKI https://github.com/luni64/EncoderTool/wiki. Examples also need a brush up.

If you want to try it here some info:

Key features
  • It supports single and multiplexed encoders.
  • Currently 74165 and 4067 based multiplexers are supported but the class structure allows for easy extension to any other multiplexer.
  • It supports all known (to me) flavours of encoders. I.e. 4 steps per detent, 2steps per detent, no detents at all. (other than the standard encoder library, the 4 and 2 step versions are debounce free)
  • A Callback mechanism allows for easy event based menu systems.
  • The callbacks use std::function per default, i.e., they can call non static member functions as well.

Here a simple example using 8 encoders multiplexed by 74165 shift registers:

Code:
#include "Arduino.h"
#include "EncoderTool.h"


using namespace EncoderTool;

constexpr unsigned encoderCount = 8; // number of attached  (daisy chain shift registers for more than 8)

constexpr unsigned QH_A   = 0; //output pin QH of shift register B
constexpr unsigned QH_B   = 1; //output pin QH of shift register A
constexpr unsigned pinLD  = 3; //load pin for all shift registers)
constexpr unsigned pinCLK = 4; //clock pin for all shift registers
                               //74165 datasheet: http://www.ti.com/product/SN74HC165

EncPlex74165 encoders(encoderCount, pinLD, pinCLK, QH_A, QH_B, CountMode::quarterInv);


void setup()
{
    encoders.begin();
}

elapsedMillis stopwatch = 0;


void loop()
{
    encoders.tick();

    if (stopwatch > 100)  // display encoder values every 100 ms
    {
        for (unsigned i = 0; i < encoderCount; i++)
        {
            Serial.printf("E%u:%3d ", i, encoders[i].getValue());
        }
        Serial.println();
        stopwatch = 0;
    }
}

And here an example showing the use of callbacks:
Code:
#include "Arduino.h"
#include "EncoderTool.h"

using namespace EncoderTool;

constexpr unsigned encoderCount = 8; // number of attached  (daisy chain shift regesters for more than 8)

constexpr unsigned QH_A = 0;   //output pin QH of shift register B
constexpr unsigned QH_B = 1;   //output pin QH of shift register A
constexpr unsigned pinLD = 3;  //load pin for all shift registers)
constexpr unsigned pinCLK = 4; //clock pin for all shift registers
                               //74165 datasheet: http://www.ti.com/product/SN74HC165

EncPlex74165 encoders(encoderCount, pinLD, pinCLK, QH_A, QH_B, CountMode::quarterInv);

void myCallback(int value, int delta)
{
    Serial.printf("Current value: %d, delta = %d\n", value, delta);
}

void setup()
{
    pinMode(13, OUTPUT);
    encoders.begin();

    encoders[0].attachCallback(myCallback); // standard callback
    encoders[1].attachCallback([](int v, int d) { digitalToggleFast(13); }); // a simple lambda expression to toggle the LED on every change
    encoders[2].attachCallback([](int v, int d) { Serial.printf("enc 2: %s\n", d > 0 ? "UP" : "DOWN"); }); 
}

void loop()
{
    encoders.tick();
}

Output:
Code:
Current value: 0, delta = -1
Current value: -1, delta = -1
Current value: -2, delta = -1
Current value: -1, delta = 1
Current value: 0, delta = 1
Current value: 1, delta = 1
enc 2: UP
enc 2: UP
enc 2: DOWN
enc 2: DOWN
enc 2: DOWN
enc 2: DOWN
enc 2: UP
Current value: 0, delta = -1
Current value: 1, delta = 1
 
Thank you @luni. No hurry. Yeah, darn Covid distraction. I've turned it into a Lockdown R and D fest.

Still working out what gets soldered where. Using a T3.2 and 74HC165s and have several spare 74HCT245 buffers so can power the HC165s from +5v or +3.3v. Ok, the HC165's can work a little faster on +5v so am curious, is there any benefit in running them at +5v for this application?
 
Still working out what gets soldered where.

You did see the schematic and boards I used here: https://github.com/luni64/EncoderTool/tree/master/extras ?

Using a T3.2 and 74HC165s and have several spare 74HCT245 buffers so can power the HC165s from +5v or +3.3v. Ok, the HC165's can work a little faster on +5v so am curious, is there any benefit in running them at +5v for this application?

Since the T3.2 is 5V tolerant you can directly connect the '165s to the Teensy and supply them with 5V. More speed is always good if you need a lot of multiplexed encoders but my experiments with 8 pcs didn't show any problem with a T3.2 so far.

Status:
I used the rainy day to brush up the documentation (ongoing), adopted the examples to run with this version and published everything on GitHub: https://github.com/luni64/EncoderTool/releases

Hope this little fun project is of use for anybody.
 
Got it. Man, that was way quicker than the mechanical details. Will opt for speed. Spring here = garden distraction. Will let you know when I see signs of life.

Cheers and thank you again.
 
Headscratching, got compiler error after installing Encoder Tool library so taking it back to basics, test case = blink example and compiler throws Invalid line format error:-

Code:
Arduino: 1.8.13 (Windows 7), TD: 1.53, Board: "Teensy 3.2 / 3.1, MIDI, 96 MHz (overclock), Faster, US English"

          C:\Program Files (x86)\Arduino\arduino-builder -dump-prefs -logger=machine -hardware C:\Program Files (x86)\Arduino\hardware -hardware C:\Users\Pats\AppData\Local\Arduino15\packages -tools C:\Program Files (x86)\Arduino\tools-builder -tools C:\Program Files (x86)\Arduino\hardware\tools\avr -tools C:\Users\Pats\AppData\Local\Arduino15\packages -built-in-libraries C:\Program Files (x86)\Arduino\libraries -libraries C:\Users\Pats\Documents\Arduino\libraries -fqbn=teensy:avr:teensy31:usb=midi,speed=96,opt=o2std,keys=en-us -ide-version=10813 -build-path C:\Users\Pats\AppData\Local\Temp\arduino_build_388652 -warnings=all -build-cache C:\Users\Pats\AppData\Local\Temp\arduino_cache_776287 -verbose C:\Program Files (x86)\Arduino\examples\01.Basics\Blink\Blink.ino loading libs from C:\Users\Pats\Documents\Arduino\libraries: loading library from C:\Users\Pats\Documents\Arduino\libraries\EncoderTool: loading library.properties: Error reading file (C:\Users\Pats\Documents\Arduino\libraries\EncoderTool\library.properties:6): Invalid line format, should be 'key=value'  Error compiling for board Teensy 3.2 / 3.1.
so sniffing out line 6 of library.properties and wondered if we're tripping over too much information, did this to it:-

Code:
paragraph= TeensyEncoderTool is a library to manage and read out rotary encoders connected either directly or via multiplexers.
which stopped the compiler complaining. Um, I don't understand, just followed MatrixRat's nose.
 
OMG, never thought that library.properties is actually parsed during build... why? Anyway, looks like some line breaks sneaked into it which broke it. I'll fix that tomorrow.
 
Many thanks luni. Got it working.

Set up for 16 encoders although only wired two so far for a test. Only getting response from one encoder. Believe it or not it looks like I have a dodgy brand new encoder. Am using pullups so tested all A and B inputs by prodding with a jumper wire to ground and response is same for all 32 inputs. There are no missing wires or dry solder joints so the conclusion is that the suspect encoder contacts do not seem to close. Bourns 96 ppr no detent. Expect the unexpected.

If your arm can be twisted, my hand is up for rotation speed sensitive value multiplier but for now am more than happy to press on with mechanical details.

Cheers for now.
 
Good, don't forget to set the count mode to 'CountMode::full ' if you have encoders with no detents. Default is 'quarter'. Will be interesting to see it working with 16 encoders and the 74165. Actually I never tried more than 8. But, I know that the CD4067 works fine with 16 encoders.

Let me know if you have issues I can help with.
 
Thank you. Had already sussed out the (CountMode::quarterInv) and other library features.

Multiplexed_74165 works as expected so for the learning experimented with 04_Callbacks.

Here, (CountMode::quarterInv) works as expected but (CountMode::full) gives jumpy output. Just flagging at this point. Need to mount a full batch of encoders and think about setting up some tests and snapshot/post the output and a some pics of the hardware.

Will be a few days.
 
Back
Top