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.