Pikku Dial - Multimode Dial Controller Powered by RPi Pico

Designed a small open-source multimode USB dial controller powered by Raspberry Pi Pico. Code is highly customizable. STL files available.

Pikku Dial - Multimode Dial Controller Powered by RPi Pico
Pikku Dial controller posing with its sibling, the Pikku Macropad. Both are powered by Raspberry Pi Pico microcontrollers.

Designed a small USB dial controller to fit together with its predecessor, the Pikku Macropad. The Dial also uses Raspberry Pi Pico and is even simpler under the hood as the only component in addition to Pico is KY-040 rotary encoder. The rotary encoder includes a switch action in addition to rotary function so pushing it will switch between different modes that you can easily customize in Python code.

The modes I have there currently are:

  1. Volume
  2. Scroll (mouse-wheel up/down)
  3. Arrow keys up/down (like moving a cursor up/down or scroll in smaller steps)
  4. Fidget mode (does nothing, a stress toy 😀)

TL;DR: Check the GitHub repository for code: github.com/tlaukkanen/pikku-macropad. 3D printable .stl models are also available via Printables.com

Bill of Material

Components needed to build your own dial controller:

Hardware - 3D Printed Case

I designed the case to be simple and easy to print. It consists of four layers where the base houses the Raspberry Pico (same as in macropad), the second layer holds the rotary encoder in place on top of Pico. The third layer is the top panel enclosure and the last one is the actual dial knob that is connected to rotary encoder.

You can print parts with assorted colors to have different looks depending how you want it to look and to differentiate or make it use similar colors as your macropad.

I used some super glue to put the second layer in place with the rotary encoder. It shouldn't need it necessarily as there are only applied forces downwards when rotary is pressed.

All components printed and assembled. I did some sand & spray rounds after this to achieve the look that you can see in the first picture on this page.

Have Fun with Dial Knobs

You can quite easily design your own rotary knobs and try to find the one that suits you the best. I found the dial knobs with small "slot" for a finger to be nice.

Models of some of the dial knob designs that I tested

Improvement Ideas for the Case

There are couple of ideas that could improve the usability a bit: weight and smaller rotary ratio, meaning that it could have gearing so that rotary encoder would rotate faster with smaller movement. Now it's over 10° angle for each step. Something like 2:1 - 4:1 gear ration in between the dial and encoder could feel better.

The case is quite light so rotating the dial can easily rotate the case too. Some weight could be added by adding some metal pieces to the empty slots in the case.

Schematics and Soldering

The wiring is simple as you only have the Pico and rotary encoder. Wire the power from Pico's VSYS to + in KY-040. GND ground to GND ground of KY-040.

Depends on your code wire the GPIO pins in the following way (or choose your own):

  • GPIO PIN 20 to SW (Switch)
  • GPIO PIN 19 to DT
  • GPIO PIN 18 to CLK
Circuit diagram on how to connect KY-040 rotary encoder to Pico
Soldered wires from Pico to KY-040

Code

I flashed the Raspberry Pi Pico with the latest CircuitPython firmware by following this "Getting started with CircuitPython" tutorial from Adafruit. For code I used CircuitPython libraries:

Here's the full code for the macropad. Code can also be found from GitHub, here.

import board
import digitalio
import rotaryio
import time
import usb_hid
import adafruit_hid.keyboard as keyboard
import adafruit_hid.mouse as mouse
from adafruit_hid.keyboard_layout_us import KeyboardLayoutUS
from adafruit_hid.keycode import Keycode
from adafruit_hid.consumer_control import ConsumerControl
from adafruit_hid.consumer_control_code import ConsumerControlCode

# Define pins
DT_PIN = board.GP19
CLK_PIN = board.GP18
SW_PIN = board.GP20

BTN_DOWN = 1
BTN_UP = 0

button = digitalio.DigitalInOut(SW_PIN)
button.direction = digitalio.Direction.INPUT
button.pull = digitalio.Pull.DOWN
last_button_state = BTN_UP
last_position = None

# Define initial mode
mode = "VOL"

# Define keyboard layout
pikku_keyboard = keyboard.Keyboard(usb_hid.devices)
layout = KeyboardLayoutUS(pikku_keyboard)
cc = ConsumerControl(usb_hid.devices)
pikku_mouse = mouse.Mouse(usb_hid.devices)

# Define rotary encoder callback function
def change_mode():
    global mode
    if mode == "VOL":
        mode = "SCROLL"
    elif mode == "SCROLL":
        mode = "CURSOR"
    elif mode == "CURSOR":
        mode = "FIDGET"
    else:
        mode = "VOL"
    time.sleep(0.1)

# Define rotary encoder reading function
def read_encoder(encoder):
    global last_position
    position = encoder.position
    if last_position == None or position == last_position:
        last_position = position
        return 0
    delta = last_position - position
    last_position = position
    return delta

# Create rotary encoder object
encoder = rotaryio.IncrementalEncoder(DT_PIN, CLK_PIN)

# Define main loop
while True:
    # Read rotary encoder
    encoder_delta = read_encoder(encoder)
    if encoder_delta != 0:
        print("delta: " + str(encoder_delta))

    # Adjust volume, scroll or cursor based on mode
    if mode == "VOL":
        if encoder_delta > 0:
            cc.send(ConsumerControlCode.VOLUME_INCREMENT)
        elif encoder_delta < 0:
            cc.send(ConsumerControlCode.VOLUME_DECREMENT)
    elif mode == "SCROLL":
        if encoder_delta > 0:
            pikku_mouse.move(wheel=-1)
        elif encoder_delta < 0:
            pikku_mouse.move(wheel=1)
    elif mode == "CURSOR":
        if encoder_delta > 0:
            pikku_keyboard.send(Keycode.DOWN_ARROW)
        elif encoder_delta < 0:
            pikku_keyboard.send(Keycode.UP_ARROW)
    else:
        # Fidget mode - do nothing
        if encoder_delta > 0:
            print("right")
        elif encoder_delta < 0:
            print("left")

    # Check if rotary encoder switch is pressed
    if button.value is False and last_button_state == BTN_UP:
        change_mode()
        print("new mode: " + mode) 
        last_button_state = BTN_DOWN
        time.sleep(0.1)
    else:
        last_button_state = BTN_UP

    # Delay to avoid debounce
    time.sleep(0.02)

Have fun on building your own! Remember to post your make on Printables :)

Join the discussion in Mastodon:

Tommi Laukkanen (@tlaukkanen@fosstodon.org)
Attached: 2 images Released code and model files for this small multimode dial controller, “Pikku Dial”. It goes well together with its sibling, Pikku Macropad 🥰#RaspberryPi #3dPrinting #CircuitPython https://www.codeof.me/pikku-dial-multi-mode-dial-controller/
Mastodon