Forum Rule: Always post complete source code & details to reproduce any issue!
Results 1 to 14 of 14

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

  1. #1
    Junior Member
    Join Date
    Feb 2020
    Posts
    6

    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)

  2. #2
    Senior Member
    Join Date
    Apr 2014
    Location
    Germany
    Posts
    805
    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

    Click image for larger version. 

Name:	encoder.jpg 
Views:	25 
Size:	53.8 KB 
ID:	18987

    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
    }

  3. #3
    Junior Member
    Join Date
    Feb 2020
    Posts
    6
    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

  4. #4
    Member
    Join Date
    Jan 2020
    Location
    Port Elizabeth
    Posts
    46

    Luni's encoder library

    Quote Originally Posted by luni View Post
    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);
      }
    }

  5. #5
    Senior Member
    Join Date
    Apr 2014
    Location
    Germany
    Posts
    805
    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/58478...l=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;
        };
    }

  6. #6
    Senior Member
    Join Date
    Apr 2014
    Location
    Germany
    Posts
    805
    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)

    Click image for larger version. 

Name:	3d.jpg 
Views:	12 
Size:	216.1 KB 
ID:	19007
    Attached Files Attached Files

  7. #7
    Junior Member
    Join Date
    Feb 2020
    Posts
    6
    !! Awesome! Just ordered a couple of 74HC165 !!

  8. #8
    Junior Member
    Join Date
    Feb 2020
    Posts
    6
    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?

  9. #9
    Senior Member
    Join Date
    Apr 2014
    Location
    Germany
    Posts
    805
    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.

  10. #10
    Senior Member
    Join Date
    Apr 2014
    Location
    Germany
    Posts
    805
    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

    Click image for larger version. 

Name:	Anmerkung 2020-02-16 191925.jpg 
Views:	11 
Size:	138.4 KB 
ID:	19082

    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.

  11. #11
    Junior Member
    Join Date
    Feb 2020
    Posts
    6
    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.

  12. #12
    Member
    Join Date
    Jan 2020
    Location
    Port Elizabeth
    Posts
    46

    Luni's encoder library

    Quote Originally Posted by luni View Post
    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.

  13. #13
    Senior Member
    Join Date
    Apr 2014
    Location
    Germany
    Posts
    805
    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.

  14. #14
    Senior Member
    Join Date
    Apr 2014
    Location
    Germany
    Posts
    805
    Looks like China is back! Got some test boards yesterday which I ordered a couple of weeks ago.

    Click image for larger version. 

Name:	Testsetup.jpg 
Views:	4 
Size:	120.8 KB 
ID:	19433

    The multiplexed encoder library described above works nicely with it.

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •