027 - MicroPython TechNotes: Rotary Encoder

Introduction

In this article, we will learn how to use ROTARY ENCODER module with ESP32 using MicroPython programming language.

Pinout

  1. GND – for the ground pin.
  2. VCC – for the supply voltage.
  3. SA – for the signal pin A.
  4. SB – for the signal pin B.
  5. SW – for the signal pin from the push button switch.

Bill Of Materials

  1. ESP32 development board that will serve as the brain for the experiment.
  2. Gorillacell ESP32 shield to extend ESP32 board to pin headers for easy circuit connection.
  3. 5-pin female-female dupont wires to attach the Rotary Encoder module to ESP32 shield.
  4. Rotary Encoder module from Gorillacell ESP32 development kit.

Hardware Instruction

  1. First, attach the ESP32 board on top of the Gorillacell ESP32 shield and make sure that both USB ports are on the same side.
  2. Next, attach dupont wires to the Rotary Encoder by following a color coding which is black for the ground, red for the VCC, yellow and the following colors for the signal pins.
  3. Next, attach the other end of the dupont wires to the ESP32 shield by matching the colors of the wires to the colors of the pin headers. I choose GPIO 32, GPIO 33, and GPIO 34 for the pin SA, pin SB, and pin SW respectively.
  4. Next, power the ESP32 shield with an external power supply with a type-C USB cable and make sure that the power switch is set to ON state.
  5. Lastly, connect the ESP32 to the computer through a micro USB cable.

Software Instruction

  1. Copy the rotary.py and rotary_irq_esp.py which I renamed as rotary_irq.py and save it to the ESP32 MicroPython Device root directory. The rotary library came from the Github of Mike Teachman: https://github.com/miketeachman/micropython-rotary
  2. Try the provided example source code, play with it, and adapt it according to your needs. Most of all, enjoy learning. Happy tinkering.

Video Demonstration

Call To Action

For any concern, write your message in the comment section.

You might also like to support my journey on Youtube by Subscribing. Click this to Subscribe to TechToTinker.

Thank you and have a good days ahead.

See you,

– George Bantique | tech.to.tinker@gmail.com

Source Code

1. Example # 1, basic example of using the Rotary Encoder:

 1# More details can be found in TechToTinker.blogspot.com 
 2# George Bantique | tech.to.tinker@gmail.com
 3
 4from machine import Pin
 5from time import sleep_ms
 6from rotary_irq import RotaryIRQ
 7
 8r = RotaryIRQ(pin_num_clk=32, 
 9              pin_num_dt=33, 
10              min_val=0, 
11              max_val=19, 
12              reverse=True, 
13              range_mode=RotaryIRQ.RANGE_WRAP)
14sw = Pin(34, Pin.IN)              
15val_old = r.value()
16isRotaryEncoder = False
17
18while True:
19    if sw.value()==1:
20        isRotaryEncoder = not isRotaryEncoder
21        if isRotaryEncoder==True:
22            print('Rotary Encoder is now enabled.')
23        else:
24            print('Rotary Encoder is now disabled.')
25            
26    if isRotaryEncoder==True:
27        val_new = r.value()
28        if val_old != val_new:
29            val_old = val_new
30            print('result = {}'.format(val_new))
31
32    sleep_ms(200)

How the code works:

from machine import Pin

Imports the Pin class from the machine module for accessing the ESP32 GPIO pins.

from time import sleep_ms

Imports the sleep_ms class from the time module to create delays in milli seconds resolution.

from rotary_irq import RotaryIRQ

Imports RotaryIRQ class from rotary_irq library to handle reading the Rotary encoder signals.

r = RotaryIRQ(pin_num_clk=32,

pin_num_dt=33,

min_val=0,

max_val=19,

reverse=True,

range_mode=RotaryIRQ.RANGE_WRAP)

Creates a RotaryIRQ object named “r” with pins connected to GPIO 32 and GPIO 33.
min_val and max_val sets the number of steps rotary encoder can have.

reverse=True sets an increasing value when the rotary encoder is rotated clockwise and a decreasing value when the rotary encoder is rotated in counter clockwise direction. You may change this to False if you want the other way around.

range_mode sets returned value of the library to wrap around between its maximum step value (max_val) and minimum step value (min_val) and vice-versa.

sw = Pin(34, Pin.IN)

Creates a Pin object named “sw” which is connected to GPIO 34 with a pin direction set as input.

val_old = r.value()

Reads the current value of the rotary encoder and store it to the val_old variable for comparison later on.

isRotaryEncoder = False

Create a variable isRotaryEncoder and sets its default value to False. This defaults to disabling the reading of rotary encoder.

while True:

Creates an infinite loop

if sw.value()==1:

isRotaryEncoder = not isRotaryEncoder

if isRotaryEncoder==True:

print(‘Rotary Encoder is now enabled.’)

else:

print(‘Rotary Encoder is now disabled.’)

Checks if the push button switch of the rotary encoder.

if isRotaryEncoder==True:

val_new = r.value()

if val_old != val_new:

val_old = val_new

print(‘result = {}’.format(val_new))

And if the isRotaryEncoder is set to True, then display the rotary encoder values.

2. Example # 2, using the Rotary Encoder to control a menu displayed in 0.96 OLED display:

  1
  2# More details can be found in TechToTinker.blogspot.com   
  3 # George Bantique | tech.to.tinker@gmail.com  
  4 from time import sleep_ms  
  5 from time import ticks_ms  
  6 from rotary_irq import RotaryIRQ  
  7 from machine import Pin, SoftI2C  
  8 from machine import RTC  
  9 from ssd1306 import SSD1306_I2C   
 10 led = Pin(2, Pin.OUT)  
 11 rsw = Pin(34, Pin.IN)  
 12 i2c = SoftI2C(scl=Pin(22), sda=Pin(21), freq=400000)   
 13 oled = SSD1306_I2C(128, 64, i2c, addr=0x3C)   
 14 r = RotaryIRQ(pin_num_clk=32,   
 15        pin_num_dt=33,   
 16        min_val=0,   
 17        max_val=19,   
 18        reverse=True,   
 19        range_mode=RotaryIRQ.RANGE_WRAP)        
 20 val_old = r.value()  
 21 rtc = RTC()  
 22 rtc.datetime((2021, 4, 11, 0, 10, 12, 0, 0))  
 23 # rtc.datetime((YYYY, MM, DD, WD, HH, MM, SS, MS))  
 24 # WD 0 = Monday  
 25 # WD 6 = Sunday  
 26 menus = ['Blink LED',  
 27      'Activate LED',  
 28      'Invert OLED',  
 29      'Display Time',  
 30      'Display Date',  
 31      'Display Weekday',  
 32      'Menu 6',  
 33      'Menu 7',  
 34      'Menu 8',  
 35      'Menu 9',  
 36      'Menu 10',  
 37      'Menu 11',  
 38      'Menu 12']  
 39 working_idx = 0  
 40 sel_menu_idx = 0  
 41 menu_idx = 0  
 42 prev_time = ticks_ms()  
 43 isBlinkLED = False  
 44 isActiveLED = False  
 45 isInvertOLED = False  
 46 isDisplayTime = False  
 47 isDisplayDate = False  
 48 isDisplayWkday = False  
 49 def print_menu(rotary_dir=0):  
 50   NUM_MENU_LINE = 5  
 51   global menus  
 52   global working_idx  
 53   global sel_menu_idx  
 54   global menu_idx  
 55   print_cnt = 0  
 56   menu_x_pos = 10 # default x position  
 57   menu_y_pos = 10 # default y position, will be updated every menu printed by 10  
 58   # Clear the screen  
 59   oled.fill_rect(0, 9, 128, 55, 0)  
 60   # Create the working index  
 61   # It can only have a value of 0 to len(menus)-1  
 62   working_idx += rotary_dir  
 63   if working_idx < 0:  
 64     working_idx = 0  
 65   elif working_idx > len(menus) - 1:  
 66     working_idx = len(menus) - 1  
 67   # Create the selected menu  
 68   # It can only have a value of  
 69   # 0, 1, 2, 3, 4  
 70   # to create 5 lines of menus  
 71   if working_idx > 1 and working_idx < len(menus)-2:  
 72     sel_menu_idx = 2  
 73   elif working_idx == len(menus)-2:  
 74     sel_menu_idx = 3  
 75   elif working_idx == len(menus)-1:  
 76     sel_menu_idx = 4  
 77   else:  
 78     sel_menu_idx = working_idx  
 79   if working_idx < 2:  
 80     menu_idx = 0  
 81   elif working_idx > len(menus)-NUM_MENU_LINE + 1:  
 82     menu_idx = len(menus)-NUM_MENU_LINE  
 83   else:  
 84     menu_idx = working_idx - 2  
 85   for i in range(menu_idx, len(menus), 1):  
 86     if print_cnt < NUM_MENU_LINE:  
 87       if print_cnt == sel_menu_idx:  
 88         #oled.fill_rect(x, y, w, h, col)  
 89         oled.fill_rect(0, ( ( ( sel_menu_idx + 1 ) * 10 ) -1 ), 128, 9, 1)  
 90         oled.text(menus[i], menu_x_pos, menu_y_pos, 0)  
 91       else:  
 92         oled.text(menus[i], menu_x_pos, menu_y_pos, 1)  
 93       oled.show()  
 94       menu_y_pos+=10  
 95       print_cnt+=1  
 96 # Prints the header  
 97 oled.text('Rotary Encoder:', 0, 0)   
 98 # Prints the initial menus  
 99 print_menu()  
100 while True:  
101   if ticks_ms() - prev_time >= 200:  
102     val_new = r.value()  
103     if val_old != val_new:  
104       if val_old == 0 and val_new == 19:  
105         val_dif = -1  
106       elif val_old == 19 and val_new == 0:  
107         val_dif = 1  
108       else:  
109         val_dif = val_new - val_old  
110       print_menu(val_dif)  
111       val_old = val_new  
112     if rsw.value()==1:  
113       if working_idx==0: # Blink LED  
114         isBlinkLED = not isBlinkLED  
115         isActiveLED = False  
116         if isBlinkLED:  
117           oled.fill_rect(0, 9, 128, 55, 0)  
118           msg = 'Blinking LED'  
119           print('Status: {}'.format(msg))  
120           oled.text(msg, 63-len(msg)*8//2, 40)  
121           oled.show()  
122         else:  
123           print_menu()  
124       elif working_idx==1: # Activate LED  
125         isActiveLED = not isActiveLED  
126         isBlinkLED = False  
127         if isActiveLED:  
128           oled.fill_rect(0, 9, 128, 55, 0)  
129           msg = 'LED activated'  
130           print('Status: {}'.format(msg))  
131           oled.text(msg, 63-len(msg)*8//2, 40)  
132           oled.show()  
133         else:  
134           print_menu()  
135       elif working_idx==2: # Invert OLED  
136         isInvertOLED = not isInvertOLED  
137         oled.invert(isInvertOLED)  
138         oled.show()  
139       elif working_idx==3: # Display Time  
140         isDisplayTime = not isDisplayTime  
141         isDisplayDate = False  
142         isDisplayWkday = False  
143         if isDisplayTime:  
144           oled.fill_rect(0, 9, 128, 55, 0)  
145           t = rtc.datetime()  
146           time = '{:02d}:{:02d}'.format(t[4],t[5])  
147           print('Time: {}'.format(time))  
148           oled.text(time, 63-len(time)*8//2, 40)  
149           oled.show()  
150         else:  
151           print_menu()  
152       elif working_idx==4: # Display Date  
153         isDisplayTime = False  
154         isDisplayDate = not isDisplayDate  
155         isDisplayWkday = False  
156         if isDisplayDate:  
157           oled.fill_rect(0, 9, 128, 55, 0)  
158           t = rtc.datetime()  
159           date = '{:04d}-{:02d}-{:02d}'.format(t[0],t[1],t[2])  
160           print('Date: {}'.format(date))  
161           oled.text(date, 63-len(date)*8//2, 40)  
162           oled.show()  
163         else:  
164           print_menu()  
165       elif working_idx==5: # Display Weekday  
166         isDisplayTime = False  
167         isDisplayDate = False  
168         isDisplayWkday = not isDisplayWkday  
169         if isDisplayWkday:  
170           oled.fill_rect(0, 9, 128, 55, 0)  
171           t = rtc.datetime()  
172           if t[3]==0:  
173             wkday = 'Monday'  
174           elif t[3]==1:  
175             wkday = 'Tuesday'  
176           elif t[3]==2:  
177             wkday = 'Wednesday'  
178           elif t[3]==3:  
179             wkday = 'Thursday'  
180           elif t[3]==4:  
181             wkday = 'Friday'  
182           elif t[3]==5:  
183             wkday = 'Saturday'  
184           elif t[3]==6:  
185             wkday = 'Sunday'  
186           print('Weekday: {}'.format(wkday))  
187           oled.text(wkday, 63-len(wkday)*8//2, 40)  
188           oled.show()  
189         else:  
190           print_menu()  
191     if isBlinkLED:  
192       led.value(not led.value())  
193     elif isActiveLED:  
194       led.value(1)  
195     else:  
196       led.value(0)  
197     prev_time = ticks_ms()  

3. rotary.py:

  1
  2 # The MIT License (MIT)  
  3 # Copyright (c) 2020 Mike Teachman  
  4 # https://opensource.org/licenses/MIT  
  5 # Platform-independent MicroPython code for the rotary encoder module  
  6 # Documentation:  
  7 #  https://github.com/MikeTeachman/micropython-rotary  
  8 import micropython  
  9 _DIR_CW = const(0x10) # Clockwise step  
 10 _DIR_CCW = const(0x20) # Counter-clockwise step  
 11 # Rotary Encoder States  
 12 _R_START = const(0x0)  
 13 _R_CW_1 = const(0x1)  
 14 _R_CW_2 = const(0x2)  
 15 _R_CW_3 = const(0x3)  
 16 _R_CCW_1 = const(0x4)  
 17 _R_CCW_2 = const(0x5)  
 18 _R_CCW_3 = const(0x6)  
 19 _R_ILLEGAL = const(0x7)  
 20 _transition_table = [  
 21   # |------------- NEXT STATE -------------|      |CURRENT STATE|  
 22   # CLK/DT  CLK/DT   CLK/DT  CLK/DT  
 23   #  00    01     10    11  
 24   [_R_START, _R_CCW_1, _R_CW_1, _R_START],       # _R_START  
 25   [_R_CW_2, _R_START, _R_CW_1, _R_START],       # _R_CW_1  
 26   [_R_CW_2, _R_CW_3, _R_CW_1, _R_START],       # _R_CW_2  
 27   [_R_CW_2, _R_CW_3, _R_START, _R_START | _DIR_CW],  # _R_CW_3  
 28   [_R_CCW_2, _R_CCW_1, _R_START, _R_START],       # _R_CCW_1  
 29   [_R_CCW_2, _R_CCW_1, _R_CCW_3, _R_START],       # _R_CCW_2  
 30   [_R_CCW_2, _R_START, _R_CCW_3, _R_START | _DIR_CCW], # _R_CCW_3  
 31   [_R_START, _R_START, _R_START, _R_START]]       # _R_ILLEGAL  
 32 _transition_table_half_step = [  
 33   [_R_CW_3,      _R_CW_2, _R_CW_1, _R_START],  
 34   [_R_CW_3 | _DIR_CCW, _R_START, _R_CW_1, _R_START],  
 35   [_R_CW_3 | _DIR_CW, _R_CW_2, _R_START, _R_START],  
 36   [_R_CW_3,      _R_CCW_2, _R_CCW_1, _R_START],  
 37   [_R_CW_3,      _R_CW_2, _R_CCW_1, _R_START | _DIR_CW],  
 38   [_R_CW_3,      _R_CCW_2, _R_CW_3, _R_START | _DIR_CCW]]  
 39 _STATE_MASK = const(0x07)  
 40 _DIR_MASK = const(0x30)  
 41 def _wrap(value, incr, lower_bound, upper_bound):  
 42   range = upper_bound - lower_bound + 1  
 43   value = value + incr  
 44   if value < lower_bound:  
 45     value += range * ((lower_bound - value) // range + 1)  
 46   return lower_bound + (value - lower_bound) % range  
 47 def _bound(value, incr, lower_bound, upper_bound):  
 48   return min(upper_bound, max(lower_bound, value + incr))  
 49 def _trigger(rotary_instance):  
 50   for listener in rotary_instance._listener:  
 51     listener()  
 52 class Rotary(object):  
 53   RANGE_UNBOUNDED = const(1)  
 54   RANGE_WRAP = const(2)  
 55   RANGE_BOUNDED = const(3)  
 56   def __init__(self, min_val, max_val, reverse, range_mode, half_step):  
 57     self._min_val = min_val  
 58     self._max_val = max_val  
 59     self._reverse = -1 if reverse else 1  
 60     self._range_mode = range_mode  
 61     self._value = min_val  
 62     self._state = _R_START  
 63     self._half_step = half_step  
 64     self._listener = []  
 65   def set(self, value=None, min_val=None,  
 66       max_val=None, reverse=None, range_mode=None):  
 67     # disable DT and CLK pin interrupts  
 68     self._hal_disable_irq()  
 69     if value is not None:  
 70       self._value = value  
 71     if min_val is not None:  
 72       self._min_val = min_val  
 73     if max_val is not None:  
 74       self._max_val = max_val  
 75     if reverse is not None:  
 76       self._reverse = -1 if reverse else 1  
 77     if range_mode is not None:  
 78       self._range_mode = range_mode  
 79     self._state = _R_START  
 80     # enable DT and CLK pin interrupts  
 81     self._hal_enable_irq()  
 82   def value(self):  
 83     return self._value  
 84   def reset(self):  
 85     self._value = 0  
 86   def close(self):  
 87     self._hal_close()  
 88   def add_listener(self, l):  
 89     self._listener.append(l)  
 90   def remove_listener(self, l):  
 91     if l not in self._listener:  
 92       raise ValueError('{} is not an installed listener'.format(l))  
 93     self._listener.remove(l)  
 94   def _process_rotary_pins(self, pin):  
 95     old_value = self._value  
 96     clk_dt_pins = (self._hal_get_clk_value() <<  
 97             1) | self._hal_get_dt_value()  
 98     # Determine next state  
 99     if self._half_step:  
100       self._state = _transition_table_half_step[self._state &  
101                            _STATE_MASK][clk_dt_pins]  
102     else:  
103       self._state = _transition_table[self._state &  
104                       _STATE_MASK][clk_dt_pins]  
105     direction = self._state & _DIR_MASK  
106     incr = 0  
107     if direction == _DIR_CW:  
108       incr = 1  
109     elif direction == _DIR_CCW:  
110       incr = -1  
111     incr *= self._reverse  
112     if self._range_mode == self.RANGE_WRAP:  
113       self._value = _wrap(  
114         self._value,  
115         incr,  
116         self._min_val,  
117         self._max_val)  
118     elif self._range_mode == self.RANGE_BOUNDED:  
119       self._value = _bound(  
120         self._value,  
121         incr,  
122         self._min_val,  
123         self._max_val)  
124     else:  
125       self._value = self._value + incr  
126     try:  
127       if old_value != self._value and len(self._listener) != 0:  
128         micropython.schedule(_trigger, self)  
129     except:  
130       pass  

4. rotary_irq.py (rotary_irq_esp.py):

 1
 2 # The MIT License (MIT)  
 3 # Copyright (c) 2020 Mike Teachman  
 4 # https://opensource.org/licenses/MIT  
 5 # Platform-specific MicroPython code for the rotary encoder module  
 6 # ESP8266/ESP32 implementation  
 7 # Documentation:  
 8 #  https://github.com/MikeTeachman/micropython-rotary  
 9 from machine import Pin  
10 from rotary import Rotary  
11 from sys import platform  
12 _esp8266_deny_pins = [16]  
13 class RotaryIRQ(Rotary):  
14   def __init__(self, pin_num_clk, pin_num_dt, min_val=0, max_val=10,  
15          reverse=False, range_mode=Rotary.RANGE_UNBOUNDED, pull_up=False, half_step=False):  
16     if platform == 'esp8266':  
17       if pin_num_clk in _esp8266_deny_pins:  
18         raise ValueError(  
19           '%s: Pin %d not allowed. Not Available for Interrupt: %s' %  
20           (platform, pin_num_clk, _esp8266_deny_pins))  
21       if pin_num_dt in _esp8266_deny_pins:  
22         raise ValueError(  
23           '%s: Pin %d not allowed. Not Available for Interrupt: %s' %  
24           (platform, pin_num_dt, _esp8266_deny_pins))  
25     super().__init__(min_val, max_val, reverse, range_mode, half_step)  
26     if pull_up == True:  
27       self._pin_clk = Pin(pin_num_clk, Pin.IN, Pin.PULL_UP)  
28       self._pin_dt = Pin(pin_num_dt, Pin.IN, Pin.PULL_UP)  
29     else:  
30       self._pin_clk = Pin(pin_num_clk, Pin.IN)  
31       self._pin_dt = Pin(pin_num_dt, Pin.IN)  
32     self._enable_clk_irq(self._process_rotary_pins)  
33     self._enable_dt_irq(self._process_rotary_pins)  
34   def _enable_clk_irq(self, callback=None):  
35     self._pin_clk.irq(  
36       trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING,  
37       handler=callback)  
38   def _enable_dt_irq(self, callback=None):  
39     self._pin_dt.irq(  
40       trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING,  
41       handler=callback)  
42   def _disable_clk_irq(self):  
43     self._pin_clk.irq(handler=None)  
44   def _disable_dt_irq(self):  
45     self._pin_dt.irq(handler=None)  
46   def _hal_get_clk_value(self):  
47     return self._pin_clk.value()  
48   def _hal_get_dt_value(self):  
49     return self._pin_dt.value()  
50   def _hal_enable_irq(self):  
51     self._enable_clk_irq(self._process_rotary_pins)  
52     self._enable_dt_irq(self._process_rotary_pins)  
53   def _hal_disable_irq(self):  
54     self._disable_clk_irq()  
55     self._disable_dt_irq()  
56   def _hal_close(self):  
57     self._hal_disable_irq()  

References And Credits

  1. Purchase your Gorillacell ESP32 development kit at: https://gorillacell.kr

  2. Rotary Encoder library: https://github.com/miketeachman/micropython-rotary



Posts in this series



No comments yet!

GitHub-flavored Markdown & a sane subset of HTML is supported.