Rotary Encoder via I2C
This Rotary Encoder peripheral connects via I2C, to the MCU of your choice (2.7V to 5.5V operating voltage). It is possible to have up to 8 IO expander chips on one I2C bus and so one address will be occupied by this peripheral. There are also 3 tactile switches, each with a LED next to it, along with one further LED. The use of these LEDs is programmable. A MicroPython class driver has been written for this peripheral below and so it would make a good UI addition to a student project. Note, that it is fairly straight forward to divide this board into two boards connected by a ribbon cable; UI (rotary encoder & switches) and I2C chip boards; see remote control as example.



"""
rotary3swMain.py
Micropython Rotary Encoder with three more switches and four LEDs - Main
MIT License
Copyright (c) 2025 Microtron NZ - Stephen Eichler
"""
from machine import Pin, I2C
import mcp23017
import rotary3sw
import time
#i2c = I2C(scl=Pin(22), sda=Pin(21))
i2c = I2C(id=0, scl=Pin(1), sda=Pin(0))
mcp = mcp23017.MCP23017(i2c)
# interrupt pin
p21 = Pin(21, mode=Pin.IN)
# encoder pins
sw_pin = 5
clk_pin = 4
dt_pin = 3
sw1_pin = 0
sw2_pin = 1
sw3_pin = 2
led1_pin = 0
led2_pin = 1
led3_pin = 2
led0_pin = 3
def i2c_scan(i2c):
try:
devices = i2c.scan()
if len(devices) == 0:
print("No I2C devices found.")
else:
print("Found the following I2C devices:")
for device in devices:
print(f"Device address: 0x{device:02X}")
except OSError as e:
print(f"An error occurred during the I2C scan: {e}")
# callback with unicode art
def cb(val, sw, sw1, sw2, sw3):
#print(sw1, " ", sw2, " ", sw3)
volume = '\u2590' * val + '\xb7' * (10 - val)
if sw:
btn = '\u2581\u2583\u2581'
else:
btn = '\u2581\u2587\u2581'
if sw1:
btn1 = '\u2581\u2583\u2581'
else:
btn1 = '\u2581\u2587\u2581'
if sw2:
btn2 = '\u2581\u2583\u2581'
else:
btn2 = '\u2581\u2587\u2581'
if sw3:
btn3 = '\u2581\u2583\u2581'
else:
btn3 = '\u2581\u2587\u2581'
print(volume + ' ' + btn + ' ' + btn1 + ' ' + btn2 + ' ' + btn3)
# simpler callback with just values
#def cb(val, sw):
# print('value: {}, switch: {}'.format(val, sw))
i2c_scan(i2c)
# init
r = rotary3sw.Rotary3sw(mcp.porta, mcp.portb, p21, clk_pin, dt_pin, sw_pin, cb, sw1_pin, sw2_pin, sw3_pin, led1_pin, led2_pin, led3_pin, led0_pin)
# add irq
r.start()
# remove irq
# r.stop()
count = 0
while True:
r.led(1, count%4 == 1)
r.led(2, count%4 == 2)
r.led(3, count%4 == 3)
r.led(0, count%4 == 0)
count += 1
time.sleep_ms(1000)
==================================================================================
"""
Micropython Rotary Encoder with three more switches and four LEDs
MIT License
Copyright (c) 2025 Microtron NZ - Stephen Eichler
Adapted from:
MicroPython MCP23017 16-bit I/O Expander
https://github.com/mcauser/micropython-mcp23017
MIT License
Copyright (c) 2019 Mike Causer
"""
class Rotary3sw():
def __init__(self, port, portb, int_pin, clk, dt, sw=None, cb=None, sw1=None, sw2=None, sw3=None,
led1=None, led2=None, led3=None, led0=None, start_val=0, min_val=0, max_val=10):
self.port = port
self.clk = clk
self.dt = dt
self.sw = sw
self.cb = cb
self.sw1 = sw1
self.sw2 = sw2
self.sw3 = sw3
self.led1 = led1
self.led2 = led2
self.led3 = led3
self.led0 = led0
# initial value
self.value = start_val
self.min_val = min_val
self.max_val = max_val
pins = (1 << clk | 1 << dt)
if self.sw is not None:
pins |= 1 << sw
if self.sw1 is not None:
pins |= 1 << sw1
if self.sw2 is not None:
pins |= 1 << sw2
if self.sw3 is not None:
pins |= 1 << sw3
# input
self.port.mode |= pins
# enable pull ups
self.port.pullup |= pins
# input inverted
self.port.input_polarity |= pins
# enable interrupt
self.port.interrupt_enable |= pins
# interrupt pin, set as input
self.int_pin = int_pin
self.int_pin.init(mode=int_pin.IN)
# last 4 states (2-bits each)
self.state = 0
self.sw_state = 0
self.sw1_state = 0
self.sw2_state = 0
self.sw3_state = 0
self.portb = portb
pinsb = 0
if self.led1 is not None:
pinsb |= 1 << led1
if self.led2 is not None:
pinsb |= 1 << led2
if self.led3 is not None:
pinsb |= 1 << led3
if self.led0 is not None:
pinsb |= 1 << led0
self.portb.mode &= pinsb ^ 0xff
self.portb.gpio = 0x00
def _step(self, val):
self.value = min(self.max_val, max(self.min_val, self.value + val))
self._callback()
def _switched(self, val):
self.sw_state = val
self._callback()
def _switched1(self, val):
self.sw1_state = val
#print("sw1: ", val)
self._callback()
def _switched2(self, val):
self.sw2_state = val
self._callback()
def _switched3(self, val):
self.sw3_state = val
self._callback()
def _rotated(self, clk, dt):
# shuffle left and add current 2-bit state
self.state = (self.state & 0x3f) << 2 | (clk << 1) | dt
if self.state == 180:
self._step(-1)
elif self.state == 120:
self._step(1)
def _callback(self):
if callable(self.cb):
self.cb(self.value, self.sw_state, self.sw1_state, self.sw2_state, self.sw3_state)
def _irq(self, p):
flagged = self.port.interrupt_flag
captured = self.port.interrupt_captured
if self.sw is not None and flagged == (1 << self.sw):
self._switched((captured >> self.sw) & 1)
elif self.sw1 is not None and flagged == (1 << self.sw1):
self._switched1((captured >> self.sw1) & 1)
elif self.sw2 is not None and flagged == (1 << self.sw2):
self._switched2((captured >> self.sw2) & 1)
elif self.sw3 is not None and flagged == (1 << self.sw3):
self._switched3((captured >> self.sw3) & 1)
else:
clk = (captured >> self.clk) & 1
dt = (captured >> self.dt) & 1
if (flagged & (1 << self.clk | 1 << self.dt)) > 0:
self._rotated(clk, dt)
def start(self):
self.int_pin.irq(trigger=self.int_pin.IRQ_FALLING, handler=self._irq)
# clear previous interrupt, if any
self.port.interrupt_captured
def stop(self):
self.int_pin.irq(None)
# clear previous interrupt, if any
self.port.interrupt_captured
def led(self, led, value):
if led > 3 or led < 0:
return
pindict = {1:self.led1, 2:self.led2, 3:self.led3, 0:self.led0}
if value:
self.portb.gpio |= 1 << pindict[led]
else:
self.portb.gpio &= (1 << pindict[led]) ^ 0xff
====================================================================================
>>> %Run -c $EDITOR_CONTENT
MPY: soft reboot
Found the following I2C devices:
Device address: 0x20
Device address: 0x50
▐········· ▁▇▁ ▁▇▁ ▁▇▁ ▁▇▁
▐▐········ ▁▇▁ ▁▇▁ ▁▇▁ ▁▇▁
▐▐▐······· ▁▇▁ ▁▇▁ ▁▇▁ ▁▇▁
▐▐▐▐······ ▁▇▁ ▁▇▁ ▁▇▁ ▁▇▁
▐▐▐▐▐····· ▁▇▁ ▁▇▁ ▁▇▁ ▁▇▁
▐▐▐▐▐▐···· ▁▇▁ ▁▇▁ ▁▇▁ ▁▇▁
▐▐▐▐▐▐▐··· ▁▇▁ ▁▇▁ ▁▇▁ ▁▇▁
▐▐▐▐▐▐▐▐·· ▁▇▁ ▁▇▁ ▁▇▁ ▁▇▁
▐▐▐▐▐▐▐▐▐· ▁▇▁ ▁▇▁ ▁▇▁ ▁▇▁
▐▐▐▐▐▐▐▐·· ▁▇▁ ▁▇▁ ▁▇▁ ▁▇▁
▐▐▐▐▐▐▐··· ▁▇▁ ▁▇▁ ▁▇▁ ▁▇▁
▐▐▐▐▐▐···· ▁▇▁ ▁▇▁ ▁▇▁ ▁▇▁
▐▐▐▐▐····· ▁▇▁ ▁▇▁ ▁▇▁ ▁▇▁
▐▐▐▐······ ▁▇▁ ▁▇▁ ▁▇▁ ▁▇▁
▐▐▐······· ▁▇▁ ▁▇▁ ▁▇▁ ▁▇▁
▐▐········ ▁▇▁ ▁▇▁ ▁▇▁ ▁▇▁
▐▐········ ▁▃▁ ▁▇▁ ▁▇▁ ▁▇▁
▐▐········ ▁▇▁ ▁▇▁ ▁▇▁ ▁▇▁
▐▐········ ▁▇▁ ▁▃▁ ▁▇▁ ▁▇▁
▐▐········ ▁▇▁ ▁▇▁ ▁▇▁ ▁▇▁
▐▐········ ▁▇▁ ▁▇▁ ▁▃▁ ▁▇▁
▐▐········ ▁▇▁ ▁▇▁ ▁▇▁ ▁▇▁
▐▐········ ▁▇▁ ▁▇▁ ▁▇▁ ▁▃▁
▐▐········ ▁▇▁ ▁▇▁ ▁▇▁ ▁▇▁
