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

Thread: Encoders (once more)

  1. #1
    Senior Member+ Theremingenieur's Avatar
    Join Date
    Feb 2014
    Location
    Colmar, France
    Posts
    2,543

    Encoders (once more)

    This time, it's simply about these small and cheap rotary encoder knobs which one uses often as input controls, replacing analog potentiometers or whatever. Basically, these are quadrature encoders like others, but when using these as user control inputs, for example for a midi device, you don't need a huge numeric range to count millions of turns at 0.1 angle resolution.

    The problem with these mechanical switches is that these bounce. That means that instead of simply closing a contact once, it will bounce back and forth tenths of times during a few milliseconds before remaining stable which might lead to wrong results. That's why one needs to do either hardware debouncing with external components (resistor-capacitor filters, schmitt triggers), or software debouncing, filtering the wrong impulses out and/or waisting time with delay().

    Most encoder drivers or libraries I've seen attach interrupts to the encoder pins which means that even if the code will filter the wrong pulses out, a lot of unnecessary interrupts could (and would) still be fired by the bouncing contacts, eating up precious cpu cycles.

    That's why I decided to write my own simple and compact rotKnob class (templated, thanks to @tni who taught us that). The approach to filter bouncing is as follows: Knowing that the rotary knob will pull down and let up pins A and B alternating, there is no reason to handle for example multiple consecutive interrupts (due to contact bouncing) on pinA after the first one. That's why the first pinA interrupt will disable further pinA interrupts with a quick port config register write until an interrupt at pinB will occur, which will disable pinB interrupts and re-enable pinA interrupts and vice versa. That means that independent of contact bouncing, for turning the rotary knob by one detent, only exactly 4 interrupts A-B-A-B or B-A-B-A will be fired and handled, not more. Since the class is templated by pin pairs, one might use as many of these rotary knobs as one wants as long as there are enough free digital pins, since the memory and CPU impact is greatly reduced. Just declare these as rotKnob<pinA1,pinB1> enc1; ... rotKnob<pinAx, pinBx> encx; before setup() in your sketch as in the following example and have fun:
    Code:
    /* Compact and quick encoder class for these horribly bouncing
     * cheap rotary knobs with 4 transitions per click.
     * Default range is from -128 to +127 starting at 0.
     *
     * Might be initialized with begin(startVal); to start at
     * a predefined number.
     * Might be initialized with begin(startVal, lowBound, hiBound);
     * to reduce the range, see examples below.
     * Works with all TEENSY 3.x processors.
     *
     * Free to use for everybody - by (ThF) 2017 */
    
    template<uint8_t pinA, uint8_t pinB>
    class rotKnob {
    public:
    	void begin(int8_t startVal, int8_t lowBound, int8_t hiBound) {
    		d._sValue = startVal;
    		d._lBound = lowBound;
    		d._hBound = hiBound;
    		d._aConf = digital_pin_to_info_PGM[(pinA)].config;
    		d._bConf = digital_pin_to_info_PGM[(pinB)].config;
    		pinMode(pinA, INPUT_PULLUP);
    		pinMode(pinB, INPUT_PULLUP);
    		delay(1);
    		d._aVal = digitalReadFast(pinA);
    		d._bVal = digitalReadFast(pinB);
    		attachInterrupt(pinA, intPinA, CHANGE);
    		attachInterrupt(pinB, intPinB, CHANGE);
    	}
    	void begin(int8_t startVal) {
    		begin(startVal, -128, 127);
    	}
    	void begin() {
    		begin(0, -128, 127);
    	}
    	void end() {
    		detachInterrupt(pinA);
    		detachInterrupt(pinB);
    	}
    	int8_t read() {
    		d._avail = false;
    		return d._sValue;
    	}
    	bool available() {
    		return d._avail;
    	}
    private:
    	struct objData {
    		volatile uint32_t* _aConf, *_bConf;
    		volatile int8_t _lBound, _hBound, _sValue;
    		volatile bool _aVal, _bVal, _avail;
    	};
    	static objData d;
    	static void intPinA() {
    		*d._aConf &= ~0x000F0000; // disable pin A interrupts
                    // decoding logic
    		if (!d._aVal) {
    			if ((d._sValue < d._hBound) && !d._bVal) {
    				d._sValue++;
    				d._avail = true;
    			}
    			if ((d._sValue > d._lBound) && d._bVal) {
    				d._sValue--;
    				d._avail = true;
    			}
    		}
    		d._bVal = digitalReadFast(pinB); // read pinB which is stable after pinA transition
    		*d._bConf |= 0x000B0000; // (re-) enable pinB interrupts
    	}
    	static void intPinB() {
    		*d._bConf &= ~0x000F0000; // disable pinB interrupts
    		d._aVal = digitalReadFast(pinA); // read pinA which is stable after pinB transition
    		*d._aConf |= 0x000B0000; // (re-) enable pinA interrupts
    	}
    };
    template<uint8_t pinA, uint8_t pinB>
    typename rotKnob<pinA, pinB>::objData rotKnob<pinA, pinB>::d;
    
    /* Describe your hardware connections, pins and knobs */
    rotKnob<18, 19> enc1;
    rotKnob<7, 8> enc2;
    
    void setup() {
    	Serial.begin(57600);
    	while (!Serial)
    		;
    	delay(5);
    	pinMode(LED_BUILTIN, OUTPUT);
    	/* Configure your encoders */
    	enc1.begin();          // will start at 0 and go from -128 to 127
    	enc2.begin(20, 0, 40); // will start at 20 and go from 0 to 40
    }
    
    void loop() {
    	if (enc1.available()) {
    		Serial.print("Encoder 1: ");
    		Serial.println(enc1.read());
    	}
    	if (enc2.available()) {
    		Serial.print("Encoder 2: ");
    		Serial.println(enc2.read());
    	}
    }
    Last edited by Theremingenieur; 06-05-2017 at 10:27 AM.

  2. #2
    Senior Member+ KurtE's Avatar
    Join Date
    Jan 2014
    Posts
    5,699
    Quote Originally Posted by Theremingenieur View Post
    That's why I decided to write my own simple and compact rotKnob class (templated, thanks to @tni who taught us that).
    ...
    Since the class is templated by pin pairs, one might use as many of these rotary knobs as one wants as long as there are enough free digital pins, since the memory and CPU impact is greatly reduced. Just declare these as rotKnob<pinA1,pinB1> enc1; ... rotKnob<pinAx, pinBx> encx;
    Thanks!

    Still trying to get my head around all of the newer stuff of C+ (different thread) and working my way through 4th edition of the C++ book... As I mention still getting head around things like templates. So wondering pros/cons of using template here versus simply having a simple class whose constructor takes two pin numbers.

    My gut tells me your version with templates may be faster and smaller for one instance, as the compiler can probably directly compile in the pin numbers directly into the code without having to load from member variable... But what does it do when I have multiple instances? Replicate all of the code for each instance? So maybe bigger? Again this is my gut wondering this and it has been wrong before! So trying to learn new tricks!

    Thanks again

  3. #3
    Senior Member+ Theremingenieur's Avatar
    Join Date
    Feb 2014
    Location
    Colmar, France
    Posts
    2,543
    Yes, thanks to the template parameters, there will be one replicated instance for each pin pair.

    There is one major problem when using interrupt routines as object member functions, since these have to be static, so that the interrupt vector can correctly be set. When I had multiple (non templated) encoder instances with the pin pair in the constructor, all would share the same static ISR routines which is not wanted. I need to know in the code which pin has triggered the interrupt. Thus, I would have to waist much time reading and comparing all affected (and bouncing!) pins in that shared ISR to see what happens which is not very efficient.

    Now from your questions, @KurtE, I decided to do compile tests with the sketch instatiating (and using) varying number of encoders. With Teensy 3.6, 180MHz, Faster without LTO settings, I got the following:
    Code:
    1 Encoder  : 14808 bytes flash, 5588 bytes global variables
    2 Encoders : 15112 bytes flash, 5604 bytes global variables
    3 Encoders : 15424 bytes flash, 5620 bytes global variables
    4 Encoders : 15768 bytes flash, 5636 bytes global variables
    5 Encoders : 16080 bytes flash, 5652 bytes global variables
    That allows to deduce that every new instance eats just a little more than 300 bytes flash and 16 bytes global variables.
    Last edited by Theremingenieur; 06-05-2017 at 02:50 PM.

  4. #4
    Senior Member
    Join Date
    Jul 2014
    Posts
    2,398
    Quote Originally Posted by KurtE View Post
    But what does it do when I have multiple instances? Replicate all of the code for each instance? So maybe bigger? Again this is my gut wondering this and it has been wrong before! So trying to learn new tricks!
    reading also about templates, I realize (but I may be wrong) that templates are simply instructions to the compiler on how to create functions/classes etc.
    So my conclusion is use of templates increases code size.
    Also, it seams that templates make it harder to debug run-time errors (e.g. code runs fine with 'short' or 'int' but fails with 'float' or 'complex', because nobody implemented the specific functionality)

  5. #5
    Senior Member+ KurtE's Avatar
    Join Date
    Jan 2014
    Posts
    5,699
    Thanks again,

    Yes - I totally understand the idea of having the ISR be part of the actual object and not have to do kludges to make it work... Could use some of that with the SPI code.
    Nice stuff for that!

    Still need to learn more, like if hypothetically I have three SPI classes built from template, I probably then can't be able to pass for example SPI or SPI1 or SPI2 as reference to some class that hopefully can work with any of them...

    Wonder if I was doing this in the full template power, if it would make sense to pass in, a type for the values, currently you use int8_t... and potentially maybe the increment value... That way maybe one could use float values like range of 0.0 to 1.0 with increment of 0.1...

    But if one implements the additional functionality I mentioned, probably need to add additional code. Example: if the type is unsigned and the increment can cause it to go logically negative, it may never find it and wrap around!

    More random wondering... May have to experiement. Things like: would it make sense to move the parameters in begin to template parameters? Would it then generate smaller code? Or will the compiler do this anyway? Likewise suppose I add a direction setting on the template, On some I may rotate right to increment and left decrement, but others the opposite... If I then add code if (_reverse) x -= _incr; else x += _incr; Will the compiler remove all of the unnecessary code?

    But feel free to ignore all of this, as I am just thinking out laud as a learning exercise!
    Last edited by KurtE; 06-05-2017 at 04:41 PM.

  6. #6
    Senior Member
    Join Date
    Jan 2013
    Posts
    843
    Quote Originally Posted by KurtE View Post
    Still need to learn more, like if hypothetically I have three SPI classes built from template, I probably then can't be able to pass for example SPI or SPI1 or SPI2 as reference to some class that hopefully can work with any of them...
    They are distinct / incompatible classes. There is no inheritance relationship, unless you introduce a common parent.

    Wonder if I was doing this in the full template power, if it would make sense to pass in, a type for the values, currently you use int8_t... and potentially maybe the increment value... That way maybe one could use float values like range of 0.0 to 1.0 with increment of 0.1...
    My code does that:
    https://forum.pjrc.com/threads/43862...ncoders-as-pot
    https://github.com/tni/teensy-sample...pped_encoder.h

  7. #7
    Senior Member
    Join Date
    Apr 2014
    Location
    Germany
    Posts
    570
    Quote Originally Posted by tni View Post
    They are distinct / incompatible classes. There is no inheritance relationship, unless you introduce a common parent.
    Wouldn't that destroy the original attempt to optimize the code by passing pin numbers etc as template parameters? Is there something like a best practice for defining a class which uses say two passed in rotKnob<int,int> objects?

  8. #8
    Senior Member
    Join Date
    Feb 2015
    Posts
    160
    ooh im gonna have to try this one out. I am getting bouncing problems with an Alps encoder and the Encoder library.

  9. #9
    Senior Member+ Theremingenieur's Avatar
    Join Date
    Feb 2014
    Location
    Colmar, France
    Posts
    2,543
    @tenkai: Feel free to write feedback here. Although I found my code performing well and being compact, there is always room for some improvements, I think.

  10. #10
    Senior Member
    Join Date
    Dec 2015
    Location
    LA
    Posts
    133
    While this code looks good for the described purpose I'm not sure it works like the typical quadrature decoder should. It looks like it only changes values at one sensor position and not the other. Even worse after moving over a sensor it won't change when you move back until the next pass. Looks like a divide by two quad encoder. As long as not used for servo position feedback its ok.

    Am I missing something?

  11. #11
    Senior Member+ Theremingenieur's Avatar
    Join Date
    Feb 2014
    Location
    Colmar, France
    Posts
    2,543
    Quote Originally Posted by bicycleguy View Post
    While this code looks good for the described purpose I'm not sure it works like the typical quadrature decoder should. It looks like it only changes values at one sensor position and not the other. Even worse after moving over a sensor it won't change when you move back until the next pass. Looks like a divide by two quad encoder. As long as not used for servo position feedback its ok.

    Am I missing something?
    As I wrote in my first posting, the code is for these rotary knob switches which do 4 transitions per detent. It's not a full quadrature decoder.

  12. #12
    Senior Member
    Join Date
    Dec 2016
    Posts
    101
    Hi

    I tested the template yesterday and it give sometimes (well quite often) 2 times the same value. I think that the usage of avail inside the normal flow and inside the interrupts is maybe the reason. Inside the loop it's clearly used non atomic, aka the svalue can be changed after checking avail.
    Running on t3.2 on 24MHz with one rotary encoder and one pushbutton (bounce library), encoder library works flawless btw.
    --> adding a "lastreadvalue" in the template and using one member function to check if it's changed and return the svalue could be a solution. All changes of svalue are then inside the interrupt. I expect that assigning an int to another local to be atomic, although svalue is a int8_t and then I'm not so sure anymore.
    (I would make svalue an int and even move the bounds checking outside the interrupt, but this last change does change the behaviour a bit -in some cases-) The shorter the interrupt the better.

    A more basic question: Would the contacts bounce when a dented encoder is not touched? ---> It will be seldom that more than one (or two) knobs are turned.

  13. #13
    Senior Member+ Theremingenieur's Avatar
    Join Date
    Feb 2014
    Location
    Colmar, France
    Posts
    2,543
    I never got or get 2 times the same value with dented encoders giving 4 transitions per detention, but only when moving slightly forth and back on the same dent which the end user is normally not intended to do... In case you see a different behavior, I’d be interested to know which encoder hardware you use and how you wired it.
    It might also be a problem with the Teensy T3.2 running a such an unusual slow speed (default is 96MHz) - as I wrote, I tested on a T3.6 @180MHz. I will investigate as soon as I find some free time.

  14. #14
    Senior Member
    Join Date
    Dec 2016
    Posts
    101
    Quote Originally Posted by Theremingenieur View Post
    I never got or get 2 times the same value with dented encoders giving 4 transitions per detention, but only when moving slightly forth and back on the same dent which the end user is normally not intended to do... In case you see a different behavior, Id be interested to know which encoder hardware you use and how you wired it.
    It might also be a problem with the Teensy T3.2 running a such an unusual slow speed (default is 96MHz) - as I wrote, I tested on a T3.6 @180MHz. I will investigate as soon as I find some free time.
    It's the " but only when moving slightly forth and back on the same dent which the end user is normally not intended to do...". But I differ in the user intend, in practice I quite often do this while fine tuning a value (with a 30mm knob and yes it should have no effect on the value with dented encoders).

  15. #15
    I find this templated class very useful with these kind of encoders, it works really well! I have one question though, as my C++ knowledge is next to nothing. Is there a way to create an array of instances of this class or a pointer, which could be used in a similar manner?
    I want to read 8 rotary encoders and the code would be much more compact and efficient if all encoders were included in an array. Below is an example of how I would image to use it, something that works with the Encoder class (but that's not a template):
    Code:
    rotKnob *rotKnobs[8];
    uint8_t pinsA[8] = { 5, 7, 20, 15, 22, 25, 33, 24 };
    uint8_t pinsB[8] = { 6, 14, 17, 16, 21, 32, 27, 30 };
    
    void setup() {
      for (int i = 0; i < 8; i++) {
        rotKnobs[i] = new rotKnob(pinsA[i], pinsB[i]);
        rotKnobs[i]->begin(0, -999, 999);
      }
    }
    Or something like this so I can reference the encoders by way of indexing. I've read stuff about parent classes etc. but I don't really understand what needs to be done.
    Thanks for the code anyway!

  16. #16
    Senior Member PaulS's Avatar
    Join Date
    Apr 2015
    Location
    Netherlands
    Posts
    108

    Encoders encore

    @ Theremingenieur: thanks, this is by far the best way to read those cheap 75 cent encoders! Running a T3.2 @ 96MHz never gave a wrong readout.
    It took me a while to figure out what you did in that class, but eventually I understood what "digital_pin_to_info_PGM[]" does.
    I stripped down your code to better understand. Here it is just in case someone else initially gets "overwhelmed" by the class.
    Code:
    // https://forum.pjrc.com/threads/44592-Encoders-(once-more)
    
    #define pinA 2
    #define pinB 3
    
    volatile uint8_t valA = 1;          // default pinA value at detent
    volatile uint8_t valB = 1;          // default pinB value at detent
    volatile bool RotationDetected = false;
    volatile int8_t Count = 0;
    volatile uint32_t *confA = digital_pin_to_info_PGM[(pinA)].config;
    volatile uint32_t *confB = digital_pin_to_info_PGM[(pinB)].config;
    
    void setup() {
      Serial.begin(57600);
      
      pinMode(pinA, INPUT_PULLUP);
      pinMode(pinB, INPUT_PULLUP);
    
      attachInterrupt(digitalPinToInterrupt(pinA), ISRpinA, CHANGE);
      attachInterrupt(digitalPinToInterrupt(pinB), ISRpinB, CHANGE);
    }
    
    void loop() {
      if (RotationDetected) {
        Serial.println(Count);
        RotationDetected = false;A
      }
    }
    
    void ISRpinA() {
      *confA &= ~0x000F0000;            // disable pin A interrupts
      if (!valA && !valB) {             // decoding logic
        Count++;
        RotationDetected = true;
      }
      if (!valA && valB) {
        Count--;
        RotationDetected = true;
      }
      valB = digitalReadFast(pinB);     // read pinB which is stable after pinA transition
      *confB |= 0x000B0000;             // (re-)enable pinB interrupts
    }
    
    void ISRpinB() {
      *confB &= ~0x000F0000;            // disable pinB interrupts
      valA = digitalReadFast(pinA);     // read pinA which is stable after pinB transition
      *confA |= 0x000B0000;             // (re-)enable pinA interrupts
    }
    Out of curiousity, I also took such an encoder apart:

    Click image for larger version. 

Name:	Tinytronics EC11 rotary encoder 1.jpg 
Views:	3 
Size:	72.5 KB 
ID:	18306

    Click image for larger version. 

Name:	Tinytronics EC11 rotary encoder 2.jpg 
Views:	3 
Size:	93.3 KB 
ID:	18307

    Regards,
    Paul

Posting Permissions

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