Once you've finished setting up your KB2040 with CircuitPython, you can access the code and necessary libraries by downloading the Project Bundle.
To do this, click on the Download Project Bundle button in the window below. It will download to your computer as a zipped folder.
# SPDX-FileCopyrightText: 2025 Liz Clark for Adafruit Industries
#
# SPDX-License-Identifier: MIT
import asyncio
import board
import displayio
import i2cdisplaybus
import adafruit_imageload
from digitalio import DigitalInOut, Direction
from adafruit_seesaw import seesaw, rotaryio, digitalio
from adafruit_bitmap_font import bitmap_font
from adafruit_display_text import label
import adafruit_displayio_ssd1306
# rotary encoder
i2c = board.STEMMA_I2C()
seesaw = seesaw.Seesaw(i2c, addr=0x36)
encoder = rotaryio.IncrementalEncoder(seesaw)
pos = -encoder.position
last_pos = pos
seesaw.pin_mode(24, seesaw.INPUT_PULLUP)
button = digitalio.DigitalIO(seesaw, 24)
button_state = False
#display setup
displayio.release_displays()
# oled
oled_reset = board.D9
display_bus = i2cdisplaybus.I2CDisplayBus(i2c, device_address=0x3D, reset=oled_reset)
WIDTH = 128
HEIGHT = 64
display = adafruit_displayio_ssd1306.SSD1306(display_bus, width=WIDTH, height=HEIGHT)
# icon sprite sheet
bitmap, palette = adafruit_imageload.load("/icons.bmp",
bitmap=displayio.Bitmap,
palette=displayio.Palette)
icons_grid = displayio.TileGrid(bitmap, pixel_shader=palette,
tile_height=38, tile_width=38,
x=int((display.width / 2)-(38/2)), y=display.height-38)
# text at top of screen
font = bitmap_font.load_font('/Arial-14.bdf')
main_area = label.Label(
font, text=" ", color=0xFFFFFF)
main_area.anchor_point = (0.5, 0.0)
main_area.anchored_position = (display.width / 2, 0)
splash = displayio.Group()
splash.append(icons_grid)
splash.append(main_area)
display.root_group = splash
# direction and step pins
DIR = DigitalInOut(board.D5)
DIR.direction = Direction.OUTPUT
STEP = DigitalInOut(board.D6)
STEP.direction = Direction.OUTPUT
# enable pin, default off
EN = DigitalInOut(board.D4)
EN.direction = Direction.OUTPUT
EN.value = True
# stepper pins, default 32
MS2 = DigitalInOut(board.D3)
MS2.direction = Direction.OUTPUT
MS2.value = True
MS1 = DigitalInOut(board.D2)
MS1.direction = Direction.OUTPUT
MS1.value = False
# speed dictionaries
speed1 = {
"micro" : 16,
"ms1" : True,
"ms2" : True,
"icon" : 2,
"label" : "FAST"
}
speed2 = {
"micro" : 32,
"ms1" : True,
"ms2" : False,
"icon" : 1,
"label" : "MEDIUM"
}
speed3 = {
"micro" : 64,
"ms1" : False,
"ms2" : True,
"icon" : 0,
"label" : "SLOW"
}
speeds = [speed3, speed2, speed1]
def change_speed(speed):
MS1.value = speeds[speed]["ms1"]
MS2.value = speeds[speed]["ms2"]
icons_grid[0] = speeds[speed]["icon"]
main_area.text = speeds[speed]["label"]
# enable dictionaries
on = {
"en" : False,
"icon" : 6,
"label" : "ON"
}
off = {
"en" : True,
"icon" : 5,
"label" : "OFF"
}
onDict = [on, off]
def onOff(go):
EN.value = onDict[go]["en"]
icons_grid[0] = onDict[go]["icon"]
main_area.text = onDict[go]["label"]
# direction dictionaries
clock = {
"dir" : True,
"icon" : 3,
"label" : "CLOCK"
}
counter = {
"dir" : False,
"icon" : 4,
"label" : "COUNTER"
}
directions = [clock, counter]
def changeDirection(direct):
DIR.value = directions[direct]["dir"]
icons_grid[0] = directions[direct]["icon"]
main_area.text = directions[direct]["label"]
# menu states
states = ["SPEED", "ON/OFF", "DIRECTION", "scroll"]
state_icons = [2, 6, 3]
class GUI_Attributes:
def __init__(self):
self.menu = 0
self.index = 0
self.state = states[3]
self.current_speed = 1
async def step():
while True:
if EN.value is False:
STEP.value = not STEP.value
await asyncio.sleep(0)
async def read_encoder(p, last_p, choice):
while True:
p = encoder.position
if p != last_p:
if p > last_p:
if choice.state is states[0]:
choice.index = (choice.index + 1) % 3
change_speed(choice.index)
choice.current_speed = choice.index
if choice.state is states[1]:
choice.index = (choice.index + 1) % 2
onOff(choice.index)
if choice.state is states[2]:
choice.index = (choice.index + 1) % 2
changeDirection(choice.index)
if choice.state is states[3]:
choice.menu = (choice.menu + 1) % 3
main_area.text = states[choice.menu]
icons_grid[0] = state_icons[choice.menu]
else:
if choice.state is states[0]:
choice.index = (choice.index - 1) % 3
change_speed(choice.index)
choice.current_speed = choice.index
if choice.state is states[1]:
choice.index = (choice.index - 1) % 2
onOff(choice.index)
if choice.state is states[2]:
choice.index = (choice.index - 1) % 2
changeDirection(choice.index)
if choice.state is states[3]:
choice.menu = (choice.menu - 1) % 3
main_area.text = states[choice.menu]
icons_grid[0] = state_icons[choice.menu]
print(choice.menu)
last_p = p
await asyncio.sleep(0.1)
async def read_button(choice, b_state):
while True:
if not button.value and not b_state:
if choice.state == states[3]:
choice.state = states[choice.menu]
if choice.state == states[0]:
choice.index = choice.current_speed
change_speed(choice.index)
if choice.state == states[1]:
choice.index = EN.value
onOff(choice.index)
if choice.state == states[2]:
choice.index = DIR.value
changeDirection(choice.index)
else:
choice.state = states[3]
main_area.text = states[choice.menu]
b_state = True
if button.value and b_state:
b_state = False
await asyncio.sleep(0.1)
async def main():
choice = GUI_Attributes()
step_task = asyncio.create_task(step())
enc_task = asyncio.create_task(read_encoder(pos, last_pos, choice))
button_task = asyncio.create_task(read_button(choice, button_state))
main_area.text = states[choice.menu]
icons_grid[0] = state_icons[choice.menu]
await asyncio.gather(step_task, enc_task, button_task)
asyncio.run(main())
Upload the Code, Assets and Libraries to the KB2040
After downloading the Project Bundle, plug your KB2040 into the computer's USB port with a known good USB data+power cable. You should see a new flash drive appear in the computer's File Explorer or Finder (depending on your operating system) called CIRCUITPY. Unzip the folder and copy the following items to the KB2040's CIRCUITPY drive.
- lib folder
- code.py
- Arial-14.bdf
- icons.bmp
Your KB2040 CIRCUITPY drive should look like this after copying the lib folder, font file, bitmap file and the code.py file.
How the CircuitPython Code Works
The code begins by instantiating the rotary encoder and button over I2C with the adafruit_seesaw library.
# rotary encoder i2c = board.STEMMA_I2C() seesaw = seesaw.Seesaw(i2c, addr=0x36) encoder = rotaryio.IncrementalEncoder(seesaw) pos = -encoder.position last_pos = pos seesaw.pin_mode(24, seesaw.INPUT_PULLUP) button = digitalio.DigitalIO(seesaw, 24) button_state = False
Graphics
The OLED is instantiated over I2C. A bitmap sprite sheet is used for the icons and one bitmap label is used for the text at the top of the display.
#display setup
displayio.release_displays()
# oled
oled_reset = board.D9
display_bus = i2cdisplaybus.I2CDisplayBus(i2c, device_address=0x3D, reset=oled_reset)
WIDTH = 128
HEIGHT = 64
display = adafruit_displayio_ssd1306.SSD1306(display_bus, width=WIDTH, height=HEIGHT)
# icon sprite sheet
bitmap, palette = adafruit_imageload.load("/icons.bmp",
bitmap=displayio.Bitmap,
palette=displayio.Palette)
icons_grid = displayio.TileGrid(bitmap, pixel_shader=palette,
tile_height=38, tile_width=38,
x=int((display.width / 2)-(38/2)), y=display.height-38)
# text at top of screen
font = bitmap_font.load_font('/Arial-14.bdf')
main_area = label.Label(
font, text=" ", color=0xFFFFFF)
main_area.anchor_point = (0.5, 0.0)
main_area.anchored_position = (display.width / 2, 0)
splash = displayio.Group()
splash.append(icons_grid)
splash.append(main_area)
display.root_group = splash
TMC2209 Pins
The direction, step, enable, MS1 and MS2 pins are connected to output pins on the KB2040. All of them affect how the stepper motor is controlled:
- Direction - direction that the motor rotates
- Step - toggled to move the motor
- Enable - enables/disables the motor
- MS1 and MS2 - address pins that set the step division
# direction and step pins DIR = DigitalInOut(board.D5) DIR.direction = Direction.OUTPUT STEP = DigitalInOut(board.D6) STEP.direction = Direction.OUTPUT # enable pin, default off EN = DigitalInOut(board.D4) EN.direction = Direction.OUTPUT EN.value = True # stepper pins, default 32 MS2 = DigitalInOut(board.D3) MS2.direction = Direction.OUTPUT MS2.value = True MS1 = DigitalInOut(board.D2) MS1.direction = Direction.OUTPUT MS1.value = False
Dictionaries for Menus
Each setting option for the turntable has a few different parameters; usually graphics and pin values. To handle this, dictionaries are used to store all of the settings together. The first set of dictionaries control the speed of the turntable by changing the MS1 and MS2 pin values. A function called change_speed() sets the pin values and updates the graphics on the OLED.
# speed dictionaries
speed1 = {
"micro" : 16,
"ms1" : True,
"ms2" : True,
"icon" : 2,
"label" : "FAST"
}
speed2 = {
"micro" : 32,
"ms1" : True,
"ms2" : False,
"icon" : 1,
"label" : "MEDIUM"
}
speed3 = {
"micro" : 64,
"ms1" : False,
"ms2" : True,
"icon" : 0,
"label" : "SLOW"
}
speeds = [speed3, speed2, speed1]
def change_speed(speed):
MS1.value = speeds[speed]["ms1"]
MS2.value = speeds[speed]["ms2"]
icons_grid[0] = speeds[speed]["icon"]
main_area.text = speeds[speed]["label"]
The same dictionary and function pairing is used for turning the motor on and off and setting the direction of the motor.
# enable dictionaries
on = {
"en" : False,
"icon" : 6,
"label" : "ON"
}
off = {
"en" : True,
"icon" : 5,
"label" : "OFF"
}
onDict = [on, off]
def onOff(go):
EN.value = onDict[go]["en"]
icons_grid[0] = onDict[go]["icon"]
main_area.text = onDict[go]["label"]
# direction dictionaries
clock = {
"dir" : True,
"icon" : 3,
"label" : "CLOCK"
}
counter = {
"dir" : False,
"icon" : 4,
"label" : "COUNTER"
}
directions = [clock, counter]
def changeDirection(direct):
DIR.value = directions[direct]["dir"]
icons_grid[0] = directions[direct]["icon"]
main_area.text = directions[direct]["label"]
There is an array of menu states that tracks whether you are scrolling through the menu options ("scroll") or actively changing a setting.
# menu states states = ["SPEED", "ON/OFF", "DIRECTION", "scroll"] state_icons = [2, 6, 3]
asyncio Class
Since this code utilizes a lot of multitasking, with menu scrolling and toggling the step pin to move the motor, asyncio is used. To track states of things across the different tasks in an asyncio program, you need to create a class. The class for this program is called GUI_Attributes and has items for tracking the state of the menu and the current_speed of the turntable.
class GUI_Attributes:
def __init__(self):
self.menu = 0
self.index = 0
self.state = states[3]
self.current_speed = 1
asyncio Tasks
Three tasks are used in the code. The first is called step() and it toggles the step pin on the motor driver to move the turntable.
async def step():
while True:
if EN.value is False:
STEP.value = not STEP.value
await asyncio.sleep(0)
The next function is called read_encoder() and it handles the rotary encoder and scrolling through the various menus. When self.states is set to scroll, the encoder scrolls through the three menu options. When self.states is set to one of the three menus (SPEED, ON/OFF or DIRECTION), then the rotary encoder scrolls through the options associated with those menus and runs the function to change the associated pin value and icon on the OLED.
async def read_encoder(p, last_p, choice):
while True:
p = encoder.position
if p != last_p:
if p > last_p:
if choice.state is states[0]:
choice.index = (choice.index + 1) % 3
change_speed(choice.index)
choice.current_speed = choice.index
if choice.state is states[1]:
choice.index = (choice.index + 1) % 2
onOff(choice.index)
if choice.state is states[2]:
choice.index = (choice.index + 1) % 2
changeDirection(choice.index)
if choice.state is states[3]:
choice.menu = (choice.menu + 1) % 3
main_area.text = states[choice.menu]
icons_grid[0] = state_icons[choice.menu]
else:
if choice.state is states[0]:
choice.index = (choice.index - 1) % 3
change_speed(choice.index)
choice.current_speed = choice.index
if choice.state is states[1]:
choice.index = (choice.index - 1) % 2
onOff(choice.index)
if choice.state is states[2]:
choice.index = (choice.index - 1) % 2
changeDirection(choice.index)
if choice.state is states[3]:
choice.menu = (choice.menu - 1) % 3
main_area.text = states[choice.menu]
icons_grid[0] = state_icons[choice.menu]
print(choice.menu)
last_p = p
await asyncio.sleep(0.1)
The final task is read_button(). This task handles the button input on the rotary encoder and changes the value of self.state from the GUI_Attributes class. If the value of self.state is scroll and the button is pressed, then you will enter the menu for the selected option (SPEED, ON/OFF or DIRECTION). If self.state is one of these selected menus and the button is pressed, then self.state is set back to scroll and you are able to scroll through the menu options without changing any settings.
async def read_button(choice, b_state):
while True:
if not button.value and not b_state:
if choice.state == states[3]:
choice.state = states[choice.menu]
if choice.state == states[0]:
choice.index = choice.current_speed
change_speed(choice.index)
if choice.state == states[1]:
choice.index = EN.value
onOff(choice.index)
if choice.state == states[2]:
choice.index = DIR.value
changeDirection(choice.index)
else:
choice.state = states[3]
main_area.text = states[choice.menu]
b_state = True
if button.value and b_state:
b_state = False
await asyncio.sleep(0.1)
asyncio main()
The class and tasks are packed into an asyncio program called main(). Each task is instantiated with the create_task() function and then put into the program with the gather() function. main() runs as a loop with the run() function.
async def main():
choice = GUI_Attributes()
step_task = asyncio.create_task(step())
enc_task = asyncio.create_task(read_encoder(pos, last_pos, choice))
button_task = asyncio.create_task(read_button(choice, button_state))
main_area.text = states[choice.menu]
icons_grid[0] = state_icons[choice.menu]
await asyncio.gather(step_task, enc_task, button_task)
asyncio.run(main())
If you want to learn more about asyncio in CircuitPython check out the Learn Guide.
Page last edited February 14, 2025
Text editor powered by tinymce.