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
- GND – for the ground pin.
- VCC – for the supply voltage.
- SA – for the signal pin A.
- SB – for the signal pin B.
- SW – for the signal pin from the push button switch.
Bill Of Materials
- ESP32 development board that will serve as the brain for the experiment.
- Gorillacell ESP32 shield to extend ESP32 board to pin headers for easy circuit connection.
- 5-pin female-female dupont wires to attach the Rotary Encoder module to ESP32 shield.
- Rotary Encoder module from Gorillacell ESP32 development kit.
Hardware Instruction
- First, attach the ESP32 board on top of the Gorillacell ESP32 shield and make sure that both USB ports are on the same side.
- 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.
- 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.
- 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.
- Lastly, connect the ESP32 to the computer through a micro USB cable.
Software Instruction
- 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
- 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
-
Purchase your Gorillacell ESP32 development kit at: https://gorillacell.kr
-
Rotary Encoder library: https://github.com/miketeachman/micropython-rotary
Posts in this series
- 049 - MicroPython TechNotes: MP3 Player
- 048 - MicroPython TechNotes: Analog Touch Sensor
- 047 - MicroPython TechNotes: E108 GPS
- 046 - MicroPython TechNotes: RF433 Transceivers
- 045 - MicroPython TechNotes: Infrared Transmitter
- 044 - MicroPython TechNotes: Infrared Receiver
- 043 - MicroPython TechNotes: ESP12E WiFi | External WiFi module
- 042 - MicroPython TechNotes: JDY-32 | Bluetooth Low Energy BLE
- 041 - MicroPython TechNotes: Bluetooth HC-06
- 040 - MicroPython TechNotes: Relay
- 039 - MicroPython TechNotes: Electromagnet
- 038 - MicroPython TechNotes: Buzzer
- 037 - MicroPython TechNotes: Servo Motor
- 036 - MicroPython TechNotes: Stepper Motor
- 035 - MicroPython TechNotes: Dual Motor Driver
- 034 - MicroPython TechNotes: DC Motors | Gear Motor and Fan Motor
- 033 - MicroPython TechNotes: TCS34725 RGB Color Sensor
- 032 - MicroPython TechNotes: BMP280 Sensor
- 031 - MicroPython TechNotes: TOF Distance Sensor
- 030 - MicroPython TechNotes: DS3231 RTC
- 029 - MicroPython TechNotes: HC-SR04 Ultrasonic Sensor
- 028 - MicroPython TechNotes: DHT11 Temperature and Humidity Sensor
- 026 - MicroPython TechNotes: Light Dependent Resistor (LDR)
- 025 - MicroPython TechNotes: Joystick
- 024 - MicroPython TechNotes: Slider Switch
- 023 - MicroPython TechNotes: Continuous Rotation Potentiometer
- 022 - MicroPython TechNotes: Potentiometer | Reading an Analog Input
- 021 - MicroPython TechNotes: Color Touch Sensor
- 020 - MicroPython TechNotes: Touch Sensor
- 019 - MicroPython TechNotes: Switch Module
- 018 - MicroPython TechNotes: Button | Reading an Input
- 017 - MicroPython TechNotes: LASER Module
- 016 - MicroPython TechNotes: RGB LED Matrix
- 015 - MicroPython TechNotes: Neopixel 16
- 014 - MicroPython TechNotes: 8x8 Dot Matrix Display (I2C)
- 013 - MicroPython TechNotes: 8x16 Dot Matrix Display (SPI)
- 012 - MicroPython TechNotes: 8x8 Dot Matrix Display (SPI)
- 011 - MicroPython TechNotes: 1.3 OLED Display
- 010 - MicroPython TechNotes: 0.96 OLED Display
- 009 - MicroPython TechNotes: 7 Segment Display
- 008 - MicroPython TechNotes: 16x2 LCD
- 007 - MicroPython TechNotes: RGB LED
- 006 - MicroPython TechNotes: Traffic Light LED Module
- 005 - MicroPython TechNotes: Gorilla Cell LED | MicroPython Hello World
- 004 - MicroPython TechNotes: Gorilla Cell I/O Devices
- 003 - MicroPython TechNotes: Gorillacell ESP32 Shield
- 002 - MicroPython TechNotes: Introduction for Gorillacell ESP32 Dev Kit
- 001 - MicroPython TechNotes: Get Started with MicroPython
- 000 - MicroPython TechNotes: Unboxing Gorillacell ESP32 Development Kit
No comments yet!