Lightweight Arduino Library for Rotary Encoder

Hi,

Introducing small Arduino library for Rotary Encoder with interrupts. Tested on Arduino AVR, Arduino ESP8266, Arduino STM32.

To make it work:

  • add 100nF/0.1μF capacitors between A pin & ground
  • add 100nF/0.1μF capacitors between B pin & ground
  • add 100nF/0.1μF capacitors between button pin & ground

Switch bounce theory - link
Hardware Debounce calaculator - link

KY-040 shield pinout

Download link - https://github.com/enjoyneering/RotaryEncoder

UDP1: I’ve used OOP and made a heavier class called “RotaryEncoderAdvanced”. Added the number of steps per click, minimum and maximum value. It turned out a complete “bicycle” for average Arduino user.

RotaryEncoderAdvanced**** encoder(PIN_A, PIN_B, BUTTON, 0.1, 0.0, 3.3); //0.1 step per click, minimum value 0, maximum value 3.3

UDP2: Changed “RotaryEncoderAdvanced” to the template. The class may take less memory - it all depends on the type of variables used. For example, your values are integer, then initialization string will look like this:

RotaryEncoderAdvanced**** encoder(PIN_A, PIN_B, BUTTON, 1, 0, 12); //1 step per click, minimum value 0, maximum value 12

Also added the ability to change: step per click, minimum value and maximum value on the fly. Control different values with a single encoder!!!

encoder.setValues(backupVoltageValue, V_STEPS_PER_CLICK, V_MIN_VALUE, V_MAX_VALUE)

More details here: AVRRotaryEncoderAdvancedMultiValuesLCD

enjoyneering:
Hi,

Introducing small Arduino library for Rotary Encoder with interrupts.

The only interrupt used is a timer interrupt and that is used via Timer1 library.

All that comment stuff about the hardware interrupt pins is misleading and superfluous.

Looks heavyweight to me. Why getPushButton() to encapsulate a single digitalRead()? Why setPushButton()?

I'll stick with Paul Stoffregen's official library. That works on the Arduinos I mostly use.

Whandall:
The only interrupt used is a timer interrupt and that is used via Timer1 library.

All that comment stuff about the hardware interrupt pins is misleading and superfluous.

Agree. I'll remove it for AVR.

MorganS:
Looks heavyweight to me. Why getPushButton() to encapsulate a single digitalRead()? Why setPushButton()?

I'll stick with Paul Stoffregen's official library. That works on the Arduinos I mostly use.

The digitalRead(_encoderButton) returns LOW(FALSE) if pressed. I think if it's confusing & not logical if getPushButton() will return FALSE after you press the button.

The setPushButton() is useful for many things. For example you already have old huge code with a function that turns on vent fan if you press the button. Now you want use encoder and turn-on the fan at specific temperature. Just add:

if (temperatue >= 25.00) setPushButton(true);

in the loop and it will do the job without major code modification

Paul Stoffregen's official library is great, but I want may own bicycle.

I don't think you take a good approach here.

For whatever reason you decided to use a timer interrupt that fires every 0.01 seconds, and reads the pin states. For a quadrature encoder that limits you to 25 complete pulses per second. Any faster and you start losing pulses. 25 pulses per second is SLOW: even with just one pulse per rotation that means you can do only 1500 rpm. That's a severe limitation.

The caps don't make ANY sense to me. The RC constant of a 10kΩ/100nF combo is 1 ms, your sample time is 10 times longer. There is not much chance that the caps delay a LOW signal enough to not miss it. A typical quadrature output is 50% duty cycle. Those caps may do more harm than good as they do result in current surges.

A more sensible (and probably lighter-weight) approach is to simply enable pin interrupts for the pins the encoder is connected to. I know only two "external interrupts" for the 328, but all pins do have pin change interrupts available as well. That's what you need. A bit of logic behind it to count the pulses and direction and done. That way you can comfortably count tens of thousands of pulses a second without losing a single one (but do remove those caps or the rising edge is too slow) and use timer1 for something more useful, like blinking an LED or whatever.

The push button can be polled of course, but doing that though a library adds an unnecessary layer of complexity. Better just treat that as any regular button in your code.

I think 95% of 'encoders' used on Arduino are knobs turned by humans. The 1500RPM speed limit is not a problem, so long as it's documented clearly that this library is not a general-purpose encoder library.

At those kinds of shaft speeds, you're likely no longer measuring the shaft position but the speed and this library has no way of measuring speed.

MorganS:
I think 95% of 'encoders' used on Arduino are knobs turned by humans. The 1500RPM speed limit is not a problem, so long as it's documented clearly that this library is not a general-purpose encoder library.

At those kinds of shaft speeds, you're likely no longer measuring the shaft position but the speed and this library has no way of measuring speed.

Yep, this library is for knobs turned by humans. Arduino framework for AVR is not good/fast for NEMA servo encoders. It can't read two digitalRead() attached to one external interrupts. Too slow. This is why I'm using Timer1 interrupt for AVR and external interrupt for STM32 and ESP2866. Paul Stoffregen used pure assembler for AVR in his library.

I've used OOP and made a heavier class called "RotaryEncoderAdvanced". Added the number of steps per click, minimum and maximum value. It turned out a complete "bicycle" for average Arduino user.

RotaryEncoderAdvanced encoder(PIN_A, PIN_B, BUTTON, 0.1, 0.0, 3.3); //0.1 step per click, minimum value 0, maximum value 3.3

To read and change values use:

lcd.print(encoder.getValue());
 
encoder.setValue(3.3);

Good news everyone.

Change class “RotaryEncoderAdvanced” to template. It may take less memory - it all depends on the type of variables used. For example, if int values are used, initialization will look like this:

RotaryEncoderAdvanced<int> encoder(PIN_A, PIN_B, BUTTON, 1, 0, 12); //1 step per click, minimum value 0, maximum value 12

Added the ability to change on the fly - step per click, minimum value and maximum value. Control different values with a single encoder!!!

encoder.setValues(backupVoltageValue, V_STEPS_PER_CLICK, V_MIN_VALUE, V_MAX_VALUE)

Added EPS8266 example for build-in Ticker library - a non interrupt method to read encoder

For the ESP8266, add the ICACHE_RAM_ATTR to your ISRs. Not doing so will cause random crashes, and it won't even run with core 2.5.2, which enforces this.

Using int as type for a counter doesn't make much sense as it is signed. Use unsigned int, or another unsigned type. Or you really want your count to go negative?

I’ve made the ticker example today & haven’t a chance to check it on the chip. Will do today. All the rest were tested many times.

It is a TEMPLATE class. You can use any type of variables. For float it will be like this:

RotaryEncoderAdvanced<float> encoder(PIN_A, PIN_B, BUTTON, 0.1, -12, 12); //0.1 step per click, minimum value -12, maximum value 12

For int16_t:

RotaryEncoderAdvanced<int16_t> encoder(PIN_A, PIN_B, BUTTON, 1000, -32767, 32767); //1000 step per click, minimum value -32767, maximum value 32767

wvmarle:
For the ESP8266, add the ICACHE_RAM_ATTR to your ISRs. Not doing so will cause random crashes, and it won’t even run with core 2.5.2, which enforces this.

Thank you. FIXED.

Also just TESTED “ESP8266RotaryEncoderTickerSerial.ino”
on latest core 2.5.2 and it WORKS.

ticker.jpg