# Rotary Encoder

A rotary encoder, or more specifically a directional rotary encoder, may look similar to a potentiometer in some ways. Both have a knob that you turn to adjust a value. But unlike a potentiometer, a rotary encoder is far more flexible in the range and precision of values it can control. Our students love to use them in their projects to change the color or patterns in an LED strip.

Rotary encoders can be thought of as two concentric rings of switches that go on and off as you turn the knob. The switches are placed so that you can tell the direction of rotation by the order the two switches get turned on and off. They turn on and off quickly so we need a high-quality function to quickly detect their changes. And as we learned in the Button lab, switches can be noisy and have a complex state transition that must be debounced to get a good quality signal.

## Learning How to Monitor the Rotary Switch Transitions

We will be using a low-cost (\$1 USD) encoder that has five connectors, three for the direction and one for a momentary switch that is closed when you press the knob in. Here is the circuit that we will be using:

We hooked up the outer pins of the encoder to GPIO pins 16 and 17 in the lower right corner of the Pico.

Then we hooked the center pin to the 3.3 volt rail. The Pico likes to pull switches down from the 3.3 volt rail. This means that we will not be connecting any of the pins to GND.

We also hooked up the central press button to GPIO 22 and the 3.3 volt rail.

We then ran this code and turned the knob:

 ``` 1 2 3 4 5 6 7 8 9 10``` ``````import time from machine import Pin rotaryA = Pin(16, Pin.IN, Pin.PULL_DOWN) rotaryB = Pin(17, Pin.IN, Pin.PULL_DOWN) while True: print(rotaryA.value(), end='') print(rotaryB.value()) time.sleep(.1) ``````

the results look like the following lines:

 ``` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15``` ``````00 00 00 10 01 00 00 00 11 00 00 00 01 11 00 ``````

Note that the bit values the encoders switches (on or off as 0 and 1) are place next to each other on the same line. We did this by making the end of the first print statement be the null string not the default newline character.

This program prints out a LONG stream of numbers, mostly of the value `00`. The values are printed 10 times each second. Now let's take a closer look at only the values that change.

What we would like to do is now only print numbers if there is a change. To do this we will "pack" binary values into a two bit number by shifting the A pin value to the left:

 ``` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18``` ``````import time from machine import Pin rotaryA = Pin(16, Pin.IN, Pin.PULL_DOWN) rotaryB = Pin(17, Pin.IN, Pin.PULL_DOWN) # we set the old value to zero for both bits being off old_combined = 0 while True: A_val = rotaryA.value() B_val = rotaryB.value() # a sifts by one bit and then is ORed with the B calue new_combined = (A_val << 1) | B_val if new_combined != old_combined: print(A_val, end='') print(B_val) old_combined = new_combined time.sleep(.1) ``````

Now we get values that look like this:

 ``` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16``` ``````01 11 00 01 11 00 01 11 10 00 10 00 01 11 00 10 ``````
Turning the knob clockwise we see the `01` before the 11 frequently

Turning the know counterclockwise:

 ``` 1 2 3 4 5 6 7 8 9 10 11 12 13 14``` ``````00 10 11 00 10 11 00 11 00 10 00 10 01 00 ``````

Here we see the reverse `10` pattern occur more frequently. But there is noise in the switches as they open and close due to small variations in the contacts as they move.

 ``` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22``` ``````import time from machine import Pin rotaryA = Pin(16, Pin.IN, Pin.PULL_DOWN) rotaryB = Pin(17, Pin.IN, Pin.PULL_DOWN) # we set the old value to zero for both bits being off old_combined = 0 while True: A_val = rotaryA.value() B_val = rotaryB.value() # a sifts by one bit and then is ORed with the B calue new_combined = (A_val << 1) | B_val if new_combined != old_combined: #print(A_val, end='') #print(B_val) old_combined = new_combined if A_val == 0 and B_val == 1: print('clock') elif A_val == 1 and B_val == 0: print('counter clock') time.sleep(.1) ``````

## The Rotary Class

Here is one rotary class:

Mike Teachman Rotary Class. This is preferred since it does not call a slow scheduler within an interrupt.

## Using a Scheduler

There is another class Gurgle Apps Rotary Encoder that uses a scheduler within an interrupt which is not a best practice. However, we can show how this does work work with the one she created. The numbers incremented, but they didn't decrement. I had to change the pins to use the PULL_DOWN settings in the init method.

 ``` 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``` ``````import machine import utime as time from machine import Pin import micropython class Rotary: ROT_CW = 1 ROT_CCW = 2 SW_PRESS = 4 SW_RELEASE = 8 def __init__(self,dt,clk,sw): self.dt_pin = Pin(dt, Pin.IN, Pin.PULL_DOWN) self.clk_pin = Pin(clk, Pin.IN, Pin.PULL_DOWN) self.sw_pin = Pin(sw, Pin.IN, Pin.PULL_DOWN) self.last_status = (self.dt_pin.value() << 1) | self.clk_pin.value() self.dt_pin.irq(handler=self.rotary_change, trigger=Pin.IRQ_FALLING | Pin.IRQ_RISING ) self.clk_pin.irq(handler=self.rotary_change, trigger=Pin.IRQ_FALLING | Pin.IRQ_RISING ) self.sw_pin.irq(handler=self.switch_detect, trigger=Pin.IRQ_FALLING | Pin.IRQ_RISING ) self.handlers = [] self.last_button_status = self.sw_pin.value() def rotary_change(self, pin): new_status = (self.dt_pin.value() << 1) | self.clk_pin.value() if new_status == self.last_status: return transition = (self.last_status << 2) | new_status if transition == 0b1110: micropython.schedule(self.call_handlers, Rotary.ROT_CW) elif transition == 0b1101: micropython.schedule(self.call_handlers, Rotary.ROT_CCW) self.last_status = new_status def switch_detect(self,pin): if self.last_button_status == self.sw_pin.value(): return self.last_button_status = self.sw_pin.value() if self.sw_pin.value(): micropython.schedule(self.call_handlers, Rotary.SW_RELEASE) else: micropython.schedule(self.call_handlers, Rotary.SW_PRESS) def add_handler(self, handler): self.handlers.append(handler) def call_handlers(self, type): for handler in self.handlers: handler(type) ``````

The following were the lines that I changed:

 ```1 2 3``` `````` self.dt_pin = Pin(dt, Pin.IN, Pin.PULL_DOWN) self.clk_pin = Pin(clk, Pin.IN, Pin.PULL_DOWN) self.sw_pin = Pin(sw, Pin.IN, Pin.PULL_DOWN) ``````

## Testing Script

 ``` 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``` ``````from rotary import Rotary import utime as time from machine import Pin # GPIO Pins 16 and 17 are for the encoder pins. 22 is the button press switch. rotary = Rotary(16, 17, 22) val = 0 def rotary_changed(change): global val if change == Rotary.ROT_CW: val = val + 1 print(val) elif change == Rotary.ROT_CCW: val = val - 1 print(val) elif change == Rotary.SW_PRESS: print('PRESS') elif change == Rotary.SW_RELEASE: print('RELEASE') rotary.add_handler(rotary_changed) while True: time.sleep(0.1) ``````

Now I can move the knob back and forth and get consistent values that go up and down. You can turn on the plot function of Thonny to see the values consistently go up and down.

## Mysterious Runtime Error on Scheduling Queue

I did notice that the Shell output did register the following errors:

 ```1 2 3``` ``````Traceback (most recent call last): File "rotary.py", line 30, in rotary_change RuntimeError: schedule queue full ``````

The error also occurred on line 32.

The following lines generated this error:

 ```1 2``` ``````micropython.schedule(self.call_handlers, Rotary.ROT_CW) micropython.schedule(self.call_handlers, Rotary.ROT_CCW) ``````

This error did not seem to impact the execution of the code. My suspicion is that this is a bug in the Micropython firmware.

As a fix, you can use the try/except block an catch any runtime error. The pass function is a no-op (no operation)

 ``` 1 2 3 4 5 6 7 8 9 10 11 12 13``` `````` def rotary_change(self, pin): new_status = (self.dt_pin.value() << 1) | self.clk_pin.value() if new_status == self.last_status: return transition = (self.last_status << 2) | new_status try: if transition == 0b1110: micropython.schedule(self.call_handlers, Rotary.ROT_CW) elif transition == 0b1101: micropython.schedule(self.call_handlers, Rotary.ROT_CCW) except RuntimeError: pass self.last_status = new_status ``````

## References

1. Counter get stuck on "schedule queue full" - suggest using a try/catch