Recently I started a project where I needed to read 2-bit rotary encoder switch. I got rotary encoders from SunFounder with pull-ups & push button and started searching for simple code that would give me direction and number of steps turned. Simplest example would be volume control.
And here is where complications started. Each and every code and example I found relied on interpreting bit pairs (0:0, 1:0, 0:1, 1:1) encoder produced while being turned. While all of that works in theory in praxis all of them suffered various problems due to real world: bouncing contacts, skipped or wrong readings, .... So most of the code tried to somehow come around those problems by guessing missed steps, filtering obviously wrong inputs and so on. In other words all of them (at least the ones I found) where imperfect or complicated.
Solution to all of above is really simple, one had to look at the problem from another angle:
Working principle of 2-bit rotary encoder switch is that states of lines (A and B) MUST change at different point of time as seen in picture. Otherwise it would be impossible to read the direction of turning!
Each single step of encoder produces 4 state pairs but ultimately it ends with (1:1), and in this lies the solution:
Instead of trying to read and match states and calculate direction, solution is dead simple: IGNORE all changes before final state (1:1) and, using interrupts, determine which edge came first before reaching (1:1) - A or B. And this will give you direction of turning. Added bonus is that you don't need any hardware debouncing as it is included in code itself.
No steps are missed. No steps are misinterpreted!
In attached example I simulated volume knob. Depending on speed with which the knob is turned volume is increased/decreased as square function of speed. Rotary encoder is connected to GPIO pins 4 & 14 and interrupts are used.
Main loop checks every 100 msec if volume has been turned, and if so it adjusts Volume variable. Added complication in this example is that if you leave the code running for VERY LONG time and turn the knob always in one direction variable in which number of changes is held will wrap around to zero when ti reaches MAX or MIN integer value! To avoid it you have to reset it to 0 and in doing so watch out for simultaneous access from interrupt thread. I used simple locking for that.
Here is the code in Python, rewriting it in any other language should be simple:
Code: Select all
import RPi.GPIO as GPIO import threading from time import sleep # GPIO Ports Enc_A = 4 # Encoder input A: input GPIO 4 Enc_B = 14 # Encoder input B: input GPIO 14 Rotary_counter = 0 # Start counting from 0 Current_A = 1 # Assume that rotary switch is not Current_B = 1 # moving while we init software LockRotary = threading.Lock() # create lock for rotary switch # initialize interrupt handlers def init(): GPIO.setwarnings(True) GPIO.setmode(GPIO.BCM) # Use BCM mode # define the Encoder switch inputs GPIO.setup(Enc_A, GPIO.IN) GPIO.setup(Enc_B, GPIO.IN) # setup callback thread for the A and B encoder # use interrupts for all inputs GPIO.add_event_detect(Enc_A, GPIO.RISING, callback=rotary_interrupt) # NO bouncetime GPIO.add_event_detect(Enc_B, GPIO.RISING, callback=rotary_interrupt) # NO bouncetime return # Rotarty encoder interrupt: # this one is called for both inputs from rotary switch (A and B) def rotary_interrupt(A_or_B): global Rotary_counter, Current_A, Current_B, LockRotary # read both of the switches Switch_A = GPIO.input(Enc_A) Switch_B = GPIO.input(Enc_B) # now check if state of A or B has changed # if not that means that bouncing caused it if Current_A == Switch_A and Current_B == Switch_B: # Same interrupt as before (Bouncing)? return # ignore interrupt! Current_A = Switch_A # remember new state Current_B = Switch_B # for next bouncing check if (Switch_A and Switch_B): # Both one active? Yes -> end of sequence LockRotary.acquire() # get lock if A_or_B == Enc_B: # Turning direction depends on Rotary_counter += 1 # which input gave last interrupt else: # so depending on direction either Rotary_counter -= 1 # increase or decrease counter LockRotary.release() # and release lock return # THAT'S IT # Main loop. Demonstrate reading, direction and speed of turning left/rignt def main(): global Rotary_counter, LockRotary Volume = 0 # Current Volume NewCounter = 0 # for faster reading with locks init() # Init interrupts, GPIO, ... while True : # start test sleep(0.1) # sleep 100 msec # because of threading make sure no thread # changes value until we get them # and reset them LockRotary.acquire() # get lock for rotary switch NewCounter = Rotary_counter # get counter value Rotary_counter = 0 # RESET IT TO 0 LockRotary.release() # and release lock if (NewCounter !=0): # Counter has CHANGED Volume = Volume + NewCounter*abs(NewCounter) # Decrease or increase volume if Volume < 0: # limit volume to 0...100 Volume = 0 if Volume > 100: # limit volume to 0...100 Volume = 100 print NewCounter, Volume # some test print # start main demo function main()