Text Editor

Adafruit recommends using the Mu editor for editing your CircuitPython code. You can get more info in this guide.

Alternatively, you can use any text editor that saves simple text files.

Download the Project Bundle

Your project will use a specific set of CircuitPython libraries and the code.py file. To get everything you need, click on the Download Project Bundle link below, and uncompress the .zip file.

Drag the contents of the uncompressed bundle directory onto your Feather board's CIRCUITPY drive, replacing any existing files or directories with the same names, and adding any new ones that are necessary.

# SPDX-FileCopyrightText: 2022 John Park for Adafruit Industries
# SPDX-License-Identifier: MIT
# Motorized fader demo
import time
import board
import pwmio
import analogio
import touchio
import neopixel
from digitalio import DigitalInOut, Pull
from adafruit_debouncer import Debouncer
from adafruit_motor import motor

MIDI_DEMO = False  # set to True to send MIDI CC

# optional MIDI setup
if MIDI_DEMO:
    import usb_midi
    import adafruit_midi
    from adafruit_midi.control_change import ControlChange
    midi = adafruit_midi.MIDI(midi_out=usb_midi.ports[1], out_channel=0)
    fader_cc_number = 16

# Button setup to store four saved values
button_pins = (board.D10, board.D9, board.D6, board.D5)
buttons = []
for button_pin in button_pins:
    tmp_pin = DigitalInOut(button_pin)
    tmp_pin.pull = Pull.UP
    buttons.append(Debouncer(tmp_pin))

saved_positions = (230, 180, 120, 60)  # pre-saved positions for the buttons to call

# Slide pot setup
fader = analogio.AnalogIn(board.A0)
fader_position = fader.value  # ranges from 0-65535
fader_pos = fader.value // 256  # make 0-255 range
last_fader_pos = fader_pos

# Motor setup
PWM_PIN_A = board.D12  # pick any pwm pins on their own channels
PWM_PIN_B = board.D11

# DC motor driver setup -- these pins go to h-bridge driver such as L9110
pwm_a = pwmio.PWMOut(PWM_PIN_A, frequency=50)
pwm_b = pwmio.PWMOut(PWM_PIN_B, frequency=50)
motor1 = motor.DCMotor(pwm_a, pwm_b)

# Touch setup  pin goes from touch pin on slide pot to touch capable pin and then 1MΩ to gnd
touch = touchio.TouchIn(board.A3)
touch.threshold = touch.raw_value + 30  # tune for fader knob cap

# NeoPixel setup
led = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.2, auto_write=True)
led.fill(0xff0000)

def clamp(num, min_value, max_value):  # function for clamping motor throttle -1.0 to 1.0
    return max(min(num, max_value), min_value)

def go_to_position(new_position):
    global fader_pos  # pylint: disable=global-statement
    fader_pos = int(fader.value//256)
    while abs(fader_pos - new_position) > 2 :
        if fader_pos > new_position :
            speed = 2.25 * abs(fader_pos - new_position) / 256 + 0.12
            speed = clamp(speed, -1.0, 1.0)
            motor1.throttle = speed
            led[0] = (fader_pos, 0, 0)

            if MIDI_DEMO:
                global fader_cc  # pylint: disable=global-statement
                fader_cc = int(fader_pos / 2)  # cc is 0-127
                midi.send(ControlChange(fader_cc_number, fader_cc))

        if fader_pos < new_position:
            speed = -2.25 * abs(fader_pos - new_position) / 256 - 0.12
            speed = clamp(speed, -1.0, 1.0)
            motor1.throttle = speed
            led[0] = (fader_pos, 0, 0)

            if MIDI_DEMO:
                fader_cc = int(fader_pos / 2)  # cc is 0-127
                midi.send(ControlChange(fader_cc_number, fader_cc))

        fader_pos = int(fader.value//256)
    motor1.throttle = None
print("--__ Flying Fader Demo __--")
print("\n"*4)

go_to_position(saved_positions[3])  # boot up demo
go_to_position(saved_positions[0])
time.sleep(.6)

current_saved_position = 0  # state to store which is current position from the list

while True:
    for i in range(len(buttons)):
        buttons[i].update()
        if buttons[i].fell:  # if a button is pressed, update the position from list
            current_saved_position = i

    if touch.value:
        motor1.throttle = None  # idle
    else:
        go_to_position(saved_positions[current_saved_position])

    filter_amt = 0.1  # higher number will be a slower filter between 1.0 and 0.1 is good
    fader_pos = int((filter_amt * last_fader_pos) + ((1.0-filter_amt) * fader.value//256))
    led[0] = (fader_pos, 0, 0)
    if abs(fader_pos - last_fader_pos) > 1 :  # do things in here, e.g. send MIDI CC
        fader_width = 90  # for text visualization in serial output
        print("-" * (fader_width - int(fader_pos/3)), fader_pos, "-" * int(fader_pos/3), end='\r')
        last_fader_pos = fader_pos
        if MIDI_DEMO:
            fader_cc = int(fader_pos / 2)  # cc is 0-127
            midi.send(ControlChange(fader_cc_number, fader_cc))

Use the Flying Fader

Once the code is up and running, allow it to run through the boot up sequence that will move the fader to a couple of position presets, and it will also calibrate the touch sensor. Be sure not to touch the potentiometer during this start up.

Then, you can press any of the four buttons to go to a preset location, or move the fader -- you'll feel the motor is disengaged until you let go, then it will move back to the last preset position!

The default may be fine, but you can fine tune the touch sensitivity by adjusting the value of this line: touch.threshold = touch.raw_value + 30

How It Works

Libraries

First, the code imports the necessary libraries. In particular:

  • pwmio along with adafruit_motor are used to control the motor speed and direction via the H-bridge driver
  • analogio is used to read the slide potentiometer as an analog input
  • touchio is used to detect when the potentiometer lever is being touched
import time
import board
import pwmio
import analogio
import touchio
import neopixel
from digitalio import DigitalInOut, Pull
from adafruit_debouncer import Debouncer
from adafruit_motor import motor

Button Setup

Button setup is optional (you can use the motorized slide pot without buttons) but it's nice to have some buttons to press to go to preset positions. You could also repurpose these to record new presets if you like.

button_pins = (board.D10, board.D9, board.D6, board.D5)
buttons = []
for button_pin in button_pins:
    tmp_pin = DigitalInOut(button_pin)
    tmp_pin.pull = Pull.UP
    buttons.append(Debouncer(tmp_pin))

The saved_positions list is a set of integer numbers that can be recalled by pressing buttons. Change these to whatever you like.

saved_positions = (230, 180, 120, 60)

Slide Potentiometer Setup

The slide potentiometer is set up as an analog input using analogio on pin A0 (an ADC pin is required) named fader.

By reading the pin value (fader.value) a 16-bit value is returned -- we then convert this to a 0-255 range for use in the code (the ADC is usually 12- or 10-bit, so the 16-bit value tends to be too noisy in practice. The 8-bit works very well.)

A state variable called last_fader_pos is created so we can detect when the pot position is changed.

fader = analogio.AnalogIn(board.A0)
fader_position = fader.value  # ranges from 0-65535
fader_pos = fader.value // 256  # make 0-255 range
last_fader_pos = fader_pos

Motor Setup

The motor is a DC motor that can be controlled to go in either direction and at varying speed by using a full H-bridge driver, in this case the L9910.

To do this, we'll use two PWM (pulse width modulation) pins from the microcontroller which act as a sort of pseudo analog output.

The two PWM objects are created with a frequency of 50Hz (more on tuning PWM for DC motors in this guide), and then the motor1 object is created using motor.DCMotor().

PWM_PIN_A = board.D12  
PWM_PIN_B = board.D11

pwm_a = pwmio.PWMOut(PWM_PIN_A, frequency=50)
pwm_b = pwmio.PWMOut(PWM_PIN_B, frequency=50)
motor1 = motor.DCMotor(pwm_a, pwm_b)

Touch Setup

Next, the capacitive touch sense for the pot lever is set up. This requires a touch-capable pin.

You can fine-tune the touch threshold if you want, or use the setting that is self-calibrated during setup.

touch = touchio.TouchIn(board.A3)
touch.threshold = touch.raw_value + 30

Clamp Function

This function is created to simply clamp the motor throttle values between -1.0 and 1.0.

def clamp(num, min_value, max_value):
    return max(min(num, max_value), min_value)

Go_to_position Function

This function is called whenever the fader needs to fly to a new position!

The new_position argument can be a value from 0-255.

The way this works is a sort of PD (position-derivative) feedback loop. Essentially, the motor moves a tiny bit, then the slide potentiometer value is read. The difference between the current position and the new position will continue to decrease, approaching zero, as the motor moves the slide to the goal.

To avoid overshoot, the throttle value is decreased as the slider gets closer to the goal, sort of like slowing a car by letting off the gas before hitting the brakes at a stop sign.

def go_to_position(new_position):
    global fader_pos
    fader_pos = int(fader.value//256)
    while abs(fader_pos - new_position) > 2 :
        if fader_pos > new_position :
            speed = 2.25 * abs(fader_pos - new_position) / 256 + 0.12
            speed = clamp(speed, -1.0, 1.0)
            motor1.throttle = speed
            led[0] = (fader_pos, 0, 0)

        if fader_pos < new_position:
            speed = -2.25 * abs(fader_pos - new_position) / 256 - 0.12
            speed = clamp(speed, -1.0, 1.0)
            motor1.throttle = speed
            led[0] = (fader_pos, 0, 0)

        fader_pos = int(fader.value//256)
    motor1.throttle = None

Main Loop

The main loop of the program does three essential things:

  • Check for button presses, which change the current_saved_position value
  • Check for touch sense on the potentiometer lever, which disables the motor throttle to allow free movement. When the touch is removed, the go_to_positon() function is called to return the fader to the last position
  • Watch for changes to the potentiometer value -- this can be both when manually moving the fader or when the motor is driving it. When the fader is slid, the analog values are filtered to remove noise/jitter and then anything you want to happen, happens! This can be LED fading, MIDI output, etc.
for i in range(len(buttons)):
        buttons[i].update()
        if buttons[i].fell:  # if a button is pressed, update the position from list
            current_saved_position = i

    if touch.value:
        motor1.throttle = None  # idle
    else:
        go_to_position(saved_positions[current_saved_position])

    filter_amt = 0.1  # higher number will be a slower filter between 1.0 and 0.1 is good
    fader_pos = int((filter_amt * last_fader_pos) + ((1.0-filter_amt) * fader.value//256))
    led[0] = (fader_pos, 0, 0)
    if abs(fader_pos - last_fader_pos) > 1 :  # do things in here, e.g. send MIDI CC
        fader_width = 90  # for text visualization in serial output
        print("-" * (fader_width - int(fader_pos/3)), fader_pos, "-" * int(fader_pos/3), end='\r')
        last_fader_pos = fader_pos

Code Customization

The demo code includes an option to send MIDI CC values. To enable this, simply change the line:

MIDI_DEMO = False

to 

MIDI_DEMO = True

This will allow the code to load the necessary MIDI libraries and then send 0-127 values over MIDI channel 1 CC number 16. (You can change those settings as well if you like.)

Try firing up a software synthesizer and set a synth parameter to be controlled by your flying fader!

This guide was first published on Aug 02, 2022. It was last updated on Jun 16, 2024.

This page (Code the Flying Fader in CircuitPython) was last updated on Jun 15, 2024.

Text editor powered by tinymce.