Maker.io main logo

Motorized Camera Slider 2-Axis

26

2025-08-05 | By Adafruit Industries

License: Attribution-Share Alike 3D Printing Motors Raspberry Pi MCU

Courtesy of Adafruit

Guide by Ruiz Brothers and Liz Clark

Overview

slider_1

 

 

Slide & Pan

This project uses two stepper motors to create smooth cinematic motion for capturing video or timelapse photography. You can use it to automate both trucking and panning for beautiful compound camera motion. It features a 1.3in TFT screen for displaying a user interface and a rotary encoder for controlling the stepper motors.

It's powered by an Adafruit KB2040 running CircuitPython. The TMC2209 stepper motor drivers offer smooth and silent micro stepping.

The software features a mode for capturing timelapses and a one-shot mode for filming video.

pan_2

DIY Motorized Slider

3D printed parts support two pieces of aluminum extrusion. A timing belt and pulley wheels are used to slide the carriage along the profile offering about 16 inches (40cm) of travel. A second motor is dedicated for panning offering a bit over 180 degrees.

A custom PCB reduces the amount of wiring connecting the microcontroller, TFT display, buck converter and two stepper motor drivers. It runs off a 12V AA battery pack making the build portable for filming outdoors.

parts_3

Crank It!

Use the rotary encoder to set the start and ending positions of the panning motor. A 3D printed rotary handle lets you crank the motor making an intuitive interface for setting the panning stepper motor.

3d_printing_motor2-loop

This learn guide assumes prior experience with electronics, 3D printing, mechanical engineering and videography. It is not recommended for beginner makers but is achievable with patience and the willingness to learn new skills.

Parts from Adafruit

Parts from Amazon

  • 2 x Stepper Motor

  • 1 x 6008-2RS Bearing

  • 4 x Pulley Wheels with Bearing Idler

  • 1 x GT2 Timing Pulley

  • 1 x GT2 Timing Belt

  • 1 x 1/4"-20 x 1/2" Tripod Screw

  • 1 x 1/4"-20 Rubber Washer

  • 1 x 8x AA Rechargeable Batteries

Circuit Diagram

This is a big build so there's a PCB to help keep the wiring nice and tidy. The PCB uses headers to easily plug in the breakouts and KB2040. The rotary encoder, terminal blocks, DC jack and JST connectors get soldered directly to the board. There are labels on the silkscreen to help identify the correct placement for everything. The Eagle files and Gerber files are available on GitHub if you want to order your own.

PCB Eagle Files on GitHub

Gerber Files on GitHub

Schematic and Fab Print

schematic_4

print_5

dimensions are in inches

Fritzing

Of course, if you're feeling adventurous, you can forego the PCB and wire the breakouts and components directly.

friz_6

Power

  • DC Jack 12V Power to MPM3601 Vin (red wire)

  • DC Jack GND to MPM3601 GND (black wire)

  • DC Jack 12V Power to TMC2209 1 terminal block + (red wire)

  • DC Jack GND to TMC2209 1 terminal block - (black wire)

  • DC Jack 12V Power to TMC2209 2 terminal block + (red wire)

  • DC Jack GND to TMC2209 2 terminal block - (black wire)

  • MPM3601 5V to KB2040 RAW (red wire)

  • MPM3601 GND to KB2040 G (black wire)

Rotary Encoder

  • Encoder button to KB2040 D2 (purple wire)

  • Encoder GND to KB2040 G (black wire)

  • Encoder pin A to KB2040 D7 (orange wire)

  • Encoder pin B to KB2040 D6 (cyan wire)

TFT

  • TFT Vin to KB2040 3V (red wire)

  • TFT Gnd to KB2040 G (black wire)

  • TFT SCK to KB2040 SCK (green wire)

  • TFT MOSI to KB2040 MOSI (blue wire)

  • TFT TCS to KB2040 D10 (white wire)

  • TFT RST to KB2040 D9 (grey wire)

  • TFT DC to KB2040 D8 (pink wire)

Limit Switches

  • Limit switch GND to KB2040 G (black wire)

  • Limit switch 1 output to KB2040 A2 (ochre wire)

  • Limit switch 2 output to KB2040 A3 (brown wire)

TMC2209 Breakout 1

  • KB2040 3V to TMC2209 1 VDD (red wire)

  • KB2040 G to TMC2209 1 GND (black wire)

  • KB2040 TX to 2.2K resistor (white wire)

  • KB2040 RX to 2.2K resistor (green wire)

  • TMC2209 1 UART to RX side of resistor (purple wire)

  • TMC2209 1 1A to stepper motor coil 1 positive (green wire)

  • TMC2209 1 1B to stepper motor coil 1 negative (yellow wire)

  • TMC2209 1 2A to stepper motor coil 2 positive (red wire)

  • TMC2209 1 2B to stepper motor coil 2 negative (black wire)

TMC2209 Breakout 2

  • KB2040 3V to TMC2209 2 VDD (red wire)

  • KB2040 G to TMC2209 2 GND (black wire)

  • KB2040 D4 to 2.2K resistor (yellow wire)

  • KB2040 D5 to 2.2K resistor (blue wire)

  • TMC2209 2 UART to RX side of resistor (purple wire)

  • TMC2209 2 1A to stepper motor coil 1 positive (green wire)

  • TMC2209 2 1B to stepper motor coil 1 negative (yellow wire)

  • TMC2209 2 2A to stepper motor coil 2 positive (red wire)

  • TMC2209 2 2B to stepper motor coil 2 negative (black wire)

Ordering PCB

OSH Park

The PCB is available to purchase from OSH park using the green button link below or from the projects permalink.

camera_7

Order PCB from OSH Park

JLCPCB / PCBWay

Alternative fabrication services like JLCPCB or PCBWay are great options. You can get your boards made quickly and just use the default options. The boards are made in 1-2 days, and DHL shipping to the US from China takes less than a week. Just upload the Gerber zip file and follow the ordering process. You can also check out PCB Builder | DigiKey.

services_8

Note: At the time this guide was written, the tariff situation between the US and China was changing every several days. If there are tariffs for goods from China, JLCPCB and PCBWay orders may come with unexpected tariff costs. OSH Park is in the US and there are no tariff costs within the US. We suggest you do some research as to costs prior to placing an order with a PCB board vendor.

Download PCB Gerber ZIP file from GitHub

CAD Files

CAD Assembly

The main assembly is available in Fusion 360 and STEP file formats. This includes all of the 3D printed parts and electronic components used in the project. Use the main assembly to create any edits, updates, or modifications.

assembly_9

3D Printed Parts

Individual 3MF files for 3D printing are oriented and ready to print on FDM machines using PLA filament. Original design source may be downloaded using the links below.

printed_10

Download 3MFs.zip

Download CAD Source

Support Material

Enable support material for the left and right Lever Cap parts. In the BambuStudio slicing app, enable the Support critical regions only option under the Support section. This will only apply supports to the large surface areas that need support. In other slicing applications, support blockers might be need.

support_11

Build Volume

The parts require a 3D printer with a minimum build volume of:

  • 140mm (X) x 170mm (Y) x 30mm (Z)

build_12

Install CircuitPython

CircuitPython is a derivative of MicroPython designed to simplify experimentation and education on low-cost microcontrollers. It makes it easier than ever to get prototyping by requiring no upfront desktop software downloads. Simply copy and edit files on the CIRCUITPY drive to iterate.

CircuitPython QuickStart

Follow this step-by-step to quickly get CircuitPython running on your board.

Download the latest version of CircuitPython for this board via circuitpython.org

Click the link above to download the latest CircuitPython UF2 file.

Save it wherever is convenient for you.

click_13

board_14

To enter the bootloader, hold down the BOOT/BOOTSEL button (highlighted in red above), and while continuing to hold it (don't let go!), press and release the reset button (highlighted in red or blue above). Continue to hold the BOOT/BOOTSEL button until the RPI-RP2 drive appears!

If the drive does not appear, release all the buttons, and then repeat the process above.

You can also start with your board unplugged from USB, press and hold the BOOTSEL button (highlighted in red above), continue to hold it while plugging it into USB, and wait for the drive to appear before releasing the button.

A lot of people end up using charge-only USB cables and it is very frustrating! Make sure you have a USB cable you know is good for data sync.

You will see a new disk drive appear called RPI-RP2.

Drag the adafruit_circuitpython_etc.uf2 file to RPI-RP2.

drive_15

drive_16

The RPI-RP2 drive will disappear, and a new disk drive called CIRCUITPY will appear.

That's it, you're done! :)

circuitpy_17

Safe Mode

You want to edit your code.py or modify the files on your CIRCUITPY drive but find that you can't. Perhaps your board has gotten into a state where CIRCUITPY is read-only. You may have turned off the CIRCUITPY drive altogether. Whatever the reason, safe mode can help.

Safe mode in CircuitPython does not run any user code on startup and disables auto-reload. This means a few things. First, safe mode bypasses any code in boot.py (where you can set CIRCUITPY read-only or turn it off completely). Second, it does not run the code in code.py. And finally, it does not automatically soft-reload when data is written to the CIRCUITPY drive.

Therefore, whatever you may have done to put your board in a non-interactive state, safe mode gives you the opportunity to correct it without losing all of the data on the CIRCUITPY drive.

Entering Safe Mode

To enter safe mode when using CircuitPython, plug in your board or hit reset (highlighted in red above). Immediately after the board starts up or resets, it waits 1000ms. On some boards, the onboard status LED (highlighted in green above) will blink yellow during that time. If you press reset during that 1000ms, the board will start up in safe mode. It can be difficult to react to the yellow LED, so you may want to think of it simply as a slow double click of the reset button. (Remember, a fast double click of reset enters the bootloader.)

In Safe Mode

If you successfully enter safe mode on CircuitPython, the LED will intermittently blink yellow three times.

If you connect to the serial console, you'll find the following message.

Copy Code
Auto-reload is off.

Running in safe mode! Not running saved code.

CircuitPython is in safe mode because you pressed the reset button during boot. Press again to exit safe mode.

Press any key to enter the REPL. Use CTRL-D to reload.


You can now edit the contents of the CIRCUITPY drive. Remember, your code will not run until you press the reset button, or unplug and plug in your board, to get out of safe mode.

Flash Resetting UF2

If your board ever gets into a really weird state and CIRCUITPY doesn't show up as a disk drive after installing CircuitPython, try loading this 'nuke' UF2 to RPI-RP2. which will do a 'deep clean' on your Flash Memory. You will lose all the files on the board, but at least you'll be able to revive it! After loading this UF2, follow the steps above to re-install CircuitPython.

Download flash erasing “nuke” UF2

Code the Slider

Once you've finished setting up your KB2040 with CircuitPython, you can access the code, images, font 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.

Download Project Bundle

Copy Code
# SPDX-FileCopyrightText: 2025 Liz Clark for Adafruit Industries

#

# SPDX-License-Identifier: MIT

import time

import supervisor

import rotaryio

import keypad

import board

import busio

import displayio

from adafruit_display_text import label

from fourwire import FourWire

from adafruit_st7789 import ST7789

from adafruit_bitmap_font import bitmap_font

from adafruit_tmc2209 import TMC2209

displayio.release_displays()

RAILS = 520 # length of rails in mm

microsteps = 128

gear_ratio = 41 / 16

shot_velocities = [

20,

15,

10

]

keys = keypad.Keys((board.D2, board.A2, board.A3), value_when_pressed=False, pull=True)

encoder = rotaryio.IncrementalEncoder(board.D7, board.D6)

last_position = None

spi = board.SPI()

tft_cs = board.D10

tft_dc = board.D8

display_bus = FourWire(spi, command=tft_dc, chip_select=tft_cs, reset=board.D9)

display = ST7789(display_bus, width=240, height=240, rowstart=80, auto_refresh=False)

splash = displayio.Group()

display.root_group = splash

bitmap = displayio.OnDiskBitmap("/icons.bmp")

grid_bg = displayio.TileGrid(bitmap, pixel_shader=bitmap.pixel_shader,

tile_height=100, tile_width=100,

x=(display.width - 100) // 2,

y=(display.height - 100) // 2)

splash.append(grid_bg)

text_group = displayio.Group()

font = bitmap_font.load_font("/Arial-14.bdf")

title_text = "Camera Slider"

title_area = label.Label(font, text=title_text, color=0xFFFFFF)

title_area.anchor_point = (0.5, 0.0)

title_area.anchored_position = (display.width / 2, 25)

text_group.append(title_area)

splash.append(text_group)

font = bitmap_font.load_font("/Arial-14.bdf")

text_area = label.Label(font, text="", color=0xFFFFFF)

text_area.anchor_point = (0.5, 1.0)

text_area.anchored_position = (display.width / 2, display.height - 25)

text_group.append(text_area)

uart = busio.UART(tx=board.TX, rx=board.RX, baudrate=115200, timeout=0.1)

driver1 = TMC2209(uart=uart, addr=0)

driver2 = TMC2209(tx_pin=board.D4, rx_pin=board.D5, addr=0)

version1 = driver1.version

version2 = driver2.version

print(f"TMC2209 #1 Version: 0x{version1:02X}")

print(f"TMC2209 #2 Version: 0x{version2:02X}")

driver1.microsteps = microsteps

print(driver1.microsteps)

driver2.microsteps = microsteps

print(driver2.microsteps)

STEPS_PER_MM = 200 * microsteps / 8

driver1.direction = False

driver2.direction = True

last_pos = 0

select = 0

menu = 0

time_mode = 0

shot_mode = 0

timelapse = True

movement_time = 0

titles = ["Camera Slider", "Motor 1", "Motor 2", "Mode",

"Timelapse", "One-Shot", "Start?", "Running"]

home_text = ["Press to Begin", "0"]

motor1_text = ["Slide to Start Point", "0"]

motor2_text = ["Move to Start", "Move to End", "0"]

mode_text = ["Timelapse", "One-Shot"]

time_text = ["1", "5", "10", "15", "30"]

shot_speeds = [10, 5, 2]

speeds = []

shot_text = ["Slow", "Medium", "Fast"]

start_text = ["Go!", 0]

running_text = ["STOP!", "Pause/Resume"]

running_icons = [6, 7]

mode_icons = [3, 4]

sub_titles = [home_text, motor1_text, motor2_text, mode_text,

time_text, shot_text, start_text, running_text]

motor2_coordinates = [0.0, 0.0]

text_area.text = home_text[0]

display.refresh()

def adv_menu(m):

m = (m + 1) % 8

title_area.text = titles[m]

sub = sub_titles[m]

if m == 4:

grid_bg[0] = 3

elif m == 5:

grid_bg[0] = 4

elif m > 5:

grid_bg[0] = m - 1

else:

grid_bg[0] = m

text_area.text = sub[0]

display.refresh()

return m

motor1_movement = {

"is_active": False,

"current_step": 0,

"total_steps": 0,

"start_pos": 0,

"end_pos": 0,

"step_direction": 1,

"last_step_time": 0,

"step_interval": 0,

"is_paused": False,

"toggle_pause": False,

"stop_requested": False

}

motor2_movement = {

"is_active": False,

"current_step": 0,

"total_steps": 0,

"start_pos": 0,

"end_pos": 0,

"step_direction": 1,

"last_step_time": 0,

"step_interval": 0,

"is_paused": False,

"toggle_pause": False,

"stop_requested": False

}

# pylint: disable=too-many-branches, too-many-statements, inconsistent-return-statements

def calculate_linear_velocity(steps_per_second, clock_frequency=12000000,

micro=128, scaling_factor=6):

frequency = steps_per_second * micro

vactual = int((frequency (1 << 23)) / (clock_frequency scaling_factor))

vactual = max(-(1 << 23), min((1 << 23) - 1, vactual))

return vactual

def move_steps_over_time(camera_driver, start_position, end_position,

time_seconds, micro=128, ratio=None):

steps = abs(end_position - start_position)

if camera_driver:

direction = -1 if end_position < start_position else 1

time_seconds = time_seconds * 2

else:

direction = 1 if driver1.direction else -1

if ratio is not None:

steps = steps / ratio

total_microsteps = steps * micro

microsteps_per_second = total_microsteps / time_seconds

fCLK = 12000000

if camera_driver:

vactual = int(microsteps_per_second / (fCLK / (1 << 24)))

else:

vactual = int(microsteps_per_second / (fCLK / (1 << 27)))

velocity = max(-(1 << 23), min((1 << 23) - 1, vactual))

velocity *= direction

return velocity

def calculate_timelapse_velocity(start_position, end_position, duration_seconds, micro=128,

clock_frequency=12000000, scaling_factor=6, min_velocity=100):

total_steps = abs(end_position - start_position)

steps_per_second = total_steps / duration_seconds

full_steps_per_second = steps_per_second / micro

vactual = calculate_linear_velocity(full_steps_per_second, clock_frequency,

micro, scaling_factor)

direction = -1 if end_position < start_position else 1

if abs(vactual) < min_velocity and vactual != 0:

vactual = min_velocity * direction

return vactual

def calculate_rail_velocity(total_steps, duration_sec, direction,

is_timelapse=True, micro=128, clock_frequency=12000000):

steps_per_second = total_steps / duration_sec

full_steps_per_second = steps_per_second / micro

if not is_timelapse:

base_scaling = 1.0

min_velocity = 400

vactual = int((full_steps_per_second micro (1 << 23))

/ (clock_frequency * base_scaling))

vactual *= direction

if abs(vactual) < min_velocity:

vactual = min_velocity * direction

else:

base_scaling = 6.0

min_velocity = 50

vactual = int((full_steps_per_second micro (1 << 23)) /

(clock_frequency * base_scaling))

vactual *= direction

if abs(vactual) < min_velocity:

vactual = min_velocity * direction

vactual = max(-(1 << 23), min((1 << 23) - 1, vactual))

return vactual

def move_motor_with_rotate(driver, movement_state, start_position=None,

end_position=None, duration_sec=0, micro=128):

if start_position is not None and end_position is not None and not movement_state["is_active"]:

if timelapse:

driver.enable_motor(run_current=20)

scaling_factor = 6

min_velocity = 50

velocity = calculate_timelapse_velocity(

start_position,

end_position,

duration_sec,

micro,

scaling_factor=scaling_factor,

min_velocity=min_velocity

)

else:

driver.enable_motor(run_current=30)

velocity = calculate_rail_velocity(

int(RAILS*STEPS_PER_MM),

duration_sec,

movement_state["step_direction"],

is_timelapse=timelapse,

micro=micro

)

initial_velocity = int(velocity * 0.2)

if abs(initial_velocity) < 200:

initial_velocity = 200 * (1 if velocity > 0 else -1)

driver.rotate(initial_velocity)

movement_state["initial_velocity"] = initial_velocity

movement_state["final_velocity"] = velocity

movement_state["ramp_up_done"] = False

movement_state["ramp_up_time"] = 500

movement_state["total_steps"] = int(RAILS*STEPS_PER_MM)

movement_state["step_direction"] = 1 if end_position > start_position else -1

movement_state["start_pos"] = 0

movement_state["end_pos"] = int(RAILS*STEPS_PER_MM)

movement_state["movement_start_time"] = supervisor.ticks_ms()

movement_state["movement_duration_ms"] = duration_sec * 1000

movement_state["is_active"] = True

movement_state["is_paused"] = False

return

movement_state["total_steps"] = int(RAILS*STEPS_PER_MM)

movement_state["step_direction"] = driver.direction

movement_state["start_pos"] = 0

movement_state["end_pos"] = int(RAILS*STEPS_PER_MM)

if duration_sec > 0 and movement_state["total_steps"] > 0:

movement_state["velocity"] = velocity

driver.rotate(velocity)

movement_state["movement_start_time"] = supervisor.ticks_ms()

movement_state["movement_duration_ms"] = duration_sec * 1000

else:

default_velocity = 2000 * movement_state["step_direction"]

driver.rotate(default_velocity)

movement_state["movement_duration_ms"] = movement_state["total_steps"] * 10

movement_state["movement_start_time"] = supervisor.ticks_ms()

movement_state["is_active"] = True

movement_state["is_paused"] = False

if movement_state["is_active"] and movement_state["toggle_pause"]:

movement_state["is_paused"] = not movement_state["is_paused"]

movement_state["toggle_pause"] = False

if movement_state["is_paused"]:

driver.rotate(0)

movement_state["pause_time"] = supervisor.ticks_ms()

else:

elapsed_ms = movement_state["pause_time"] - movement_state["movement_start_time"]

remaining_ms = movement_state["movement_duration_ms"] - elapsed_ms

if remaining_ms > 0:

driver.rotate(movement_state["velocity"])

movement_state["movement_start_time"] = supervisor.ticks_ms() - elapsed_ms

else:

driver.rotate(0)

driver.disable_motor()

movement_state["is_active"] = False

if movement_state["is_active"] and movement_state["stop_requested"]:

driver.rotate(0)

driver.disable_motor()

movement_state["is_active"] = False

movement_state["stop_requested"] = False

return {

"active": False,

"complete": False,

"progress_percent": (supervisor.ticks_ms() - movement_state["movement_start_time"])

/ movement_state["movement_duration_ms"] * 100,

"stopped_by_user": True

}

if movement_state["is_active"] and not movement_state["is_paused"]:

current_t = supervisor.ticks_ms()

e = current_t - movement_state["movement_start_time"]

if e >= movement_state["movement_duration_ms"]:

print("Movement time complete!")

driver.rotate(0)

driver.disable_motor()

movement_state["is_active"] = False

return {

"active": False,

"complete": True,

"progress_percent": 100,

"stopped_by_user": False

}

return {

"active": movement_state["is_active"],

"paused": movement_state["is_paused"],

"progress_percent": (supervisor.ticks_ms() - movement_state["movement_start_time"]) /

movement_state["movement_duration_ms"] * 100

if movement_state["is_active"] else 0,

"stopped_by_user": False

}

def pause_resume_motor1():

motor1_movement["toggle_pause"] = True

def stop_motor1():

driver1.disable_motor()

driver1.reset_position()

motor1_movement["stop_requested"] = True

def pause_resume_motor2():

motor2_movement["toggle_pause"] = True

def stop_motor2():

driver2.disable_motor()

driver2.reset_position()

motor2_movement["stop_requested"] = True

def stop_all_motors():

driver1.rotate(0)

driver2.rotate(0)

driver1.disable_motor()

driver2.disable_motor()

motor1_movement["is_active"] = False

motor2_movement["is_active"] = False

motor1_movement["stop_requested"] = False

motor2_movement["stop_requested"] = False

time.sleep(0.1)

driver1.disable_motor()

driver1.reset_position()

driver2.reset_position()

while True:

if motor1_movement["is_active"]:

current_time = supervisor.ticks_ms()

elapsed = current_time - motor1_movement["movement_start_time"]

if elapsed >= motor1_movement["movement_duration_ms"]:

driver1.rotate(0)

driver1.disable_motor()

motor1_movement["is_active"] = False

if motor2_movement["is_active"]:

current_time = supervisor.ticks_ms()

elapsed = current_time - motor2_movement["movement_start_time"]

if elapsed >= motor2_movement["movement_duration_ms"]:

driver2.rotate(0)

driver2.disable_motor()

motor2_movement["is_active"] = False

if menu == 7:

active_motors = 0

progress1 = 0

progress2 = 0

if motor1_movement["is_active"]:

active_motors += 1

current_time = supervisor.ticks_ms()

elapsed = current_time - motor1_movement["movement_start_time"]

progress1 = (elapsed / motor1_movement["movement_duration_ms"]) * 100

if motor2_movement["is_active"]:

active_motors += 1

current_time = supervisor.ticks_ms()

elapsed = current_time - motor2_movement["movement_start_time"]

progress2 = (elapsed / motor2_movement["movement_duration_ms"]) * 100

if active_motors > 0:

avg_progress = (progress1 + progress2) / active_motors

text_area.text = f"{running_text[select]} {avg_progress:.1f}%"

display.refresh()

elif active_motors == 0 and (motor1_movement["movement_duration_ms"] > 0

or motor2_movement["movement_duration_ms"] > 0):

text_area.text = "Movement Complete!"

display.refresh()

event = keys.events.get()

if event:

if event.pressed:

print(f"{event.key_number} pressed")

if event.key_number == 0:

if menu == 0:

menu = adv_menu(menu)

elif menu == 2:

if select == 0:

motor2_coordinates[select] = driver2.position

if select == 1:

motor2_coordinates[select] = driver2.position

select += 1

text_area.text = motor2_text[select]

if select > 1:

select = 0

menu = adv_menu(menu)

if motor2_coordinates[0] > motor2_coordinates[1]:

move = motor2_coordinates[0] - motor2_coordinates[1]

else:

move = motor2_coordinates[1] - motor2_coordinates[0]

move = -move

driver2.step(move)

elif menu == 3:

if select == 1:

timelapse = False

menu += 1

select = 0

else:

timelapse = True

menu = adv_menu(menu)

elif menu == 4:

menu += 1

time_mode = select

menu = adv_menu(menu)

select = 0

print(f"{time_text[time_mode]}, timelapse: {timelapse}")

elif menu == 5:

shot_mode = select

menu = adv_menu(menu)

select = 0

print(f"{shot_text[shot_mode]}, timelapse: {timelapse}")

elif menu == 6:

menu = adv_menu(menu)

if timelapse:

movement_time = int(time_text[time_mode]) * 60

print(f"starting a timelapse for {time_text[time_mode]} minutes")

status1 = move_motor_with_rotate(

driver1,

motor1_movement,

start_position=0,

end_position=int(RAILS * STEPS_PER_MM),

duration_sec=movement_time,

microsteps=microsteps

)

if abs(motor2_coordinates[1] - motor2_coordinates[0]) > 0:

velocity2 = move_steps_over_time(camera_driver=True,

start_position=motor2_coordinates[0],

end_position=motor2_coordinates[1],

time_seconds=movement_time,

microsteps=microsteps,

ratio=gear_ratio)

print(f"driver2 velocity is: {velocity2}")

driver2.enable_motor(run_current=25)

driver2.rotate(velocity2)

motor2_movement["is_active"] = True

motor2_movement["start_pos"] = motor2_coordinates[0]

motor2_movement["end_pos"] = motor2_coordinates[1]

motor2_movement["movement_start_time"] = supervisor.ticks_ms()

motor2_movement["movement_duration_ms"] = movement_time * 1000

motor2_movement["velocity"] = velocity2

motor2_movement["total_steps"] = (abs(motor2_coordinates[1] -

motor2_coordinates[0]))

else:

print(f"starting a {shot_text[shot_mode]} one-shot")

movement_time = shot_velocities[shot_mode]

status1 = move_motor_with_rotate(

driver1,

motor1_movement,

start_position=0,

end_position=int(RAILS * STEPS_PER_MM),

duration_sec=movement_time,

microsteps=microsteps

)

if abs(motor2_coordinates[1] - motor2_coordinates[0]) > 0:

velocity2 = move_steps_over_time(camera_driver=True,

start_position=motor2_coordinates[0],

end_position=motor2_coordinates[1],

time_seconds=movement_time,

microsteps=microsteps,

ratio=gear_ratio)

driver2.enable_motor(run_current=25)

driver2.rotate(velocity2)

motor2_movement["is_active"] = True

motor2_movement["start_pos"] = motor2_coordinates[0]

motor2_movement["end_pos"] = motor2_coordinates[1]

motor2_movement["movement_start_time"] = supervisor.ticks_ms()

motor2_movement["movement_duration_ms"] = movement_time * 1000

motor2_movement["velocity"] = velocity2

motor2_movement["total_steps"] = (abs(motor2_coordinates[1] -

motor2_coordinates[0]))

elif menu == 7:

if select == 0:

stop_all_motors()

text_area.text = "Stopping..."

menu = adv_menu(menu)

elif select == 1:

pause_resume_motor1()

pause_resume_motor2()

paused_state = motor1_movement["is_paused"] or motor2_movement["is_paused"]

text_area.text = "Paused" if paused_state else "Running"

display.refresh()

if event.key_number == 1:

if menu == 1:

driver1.direction = False

driver1.reset_position()

menu = adv_menu(menu)

elif menu == 7:

stop_all_motors()

menu = adv_menu(menu)

if event.key_number == 2:

if menu == 1:

driver1.direction = True

driver1.reset_position()

menu = adv_menu(menu)

elif menu == 7:

stop_all_motors()

menu = adv_menu(menu)

display.refresh()

pos = encoder.position

if pos != last_pos:

if pos > last_pos:

if menu == 2:

driver2.step(-10)

if menu == 3:

select = (select + 1) % 2

text_area.text = mode_text[select]

grid_bg[0] = mode_icons[select]

if menu == 4:

select = (select + 1) % len(time_text)

text_area.text = time_text[select]

if menu == 5:

select = (select + 1) % len(shot_text)

text_area.text = shot_text[select]

if menu == 7:

select = (select + 1) % len(running_text)

text_area.text = running_text[select]

grid_bg[0] = running_icons[select]

else:

if menu == 2:

driver2.step(10)

if menu == 3:

select = (select - 1) % 2

text_area.text = mode_text[select]

grid_bg[0] = mode_icons[select]

if menu == 4:

select = (select - 1) % len(time_text)

text_area.text = time_text[select]

if menu == 5:

select = (select - 1) % len(shot_text)

text_area.text = shot_text[select]

if menu == 7:

select = (select - 1) % len(running_text)

text_area.text = running_text[select]

grid_bg[0] = running_icons[select]

last_pos = pos

display.refresh()

View on GitHub

Upload the Code 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

  • adafruit_tmc2209.py

  • generic_uart_device.py

  • icons.bmp

  • Arial-14.bdf

Your KB2040 CIRCUITPY drive should look like this after copying the lib folder, image file, font file and the Python files.

drive_18

How the Code Works

There are three code files: adafruit_tmc2209.py, generic_uart_device.py and the main code.py. adafruit_tmc2209.py and generic_uart_device.py are driver files for the TMC2209 UART control. The code.py file depends on these files to control the TMC2209 drivers.

At the top of code.py are some user parameters. RAILS is the length of the rails in mm. microsteps are the number of microsteps used by the stepper motors. gear_ratio is the gear reduction for the camera pan mechanism. Finally, shot_velocities contains the one-shot speeds in seconds.

Download File

Copy Code
RAILS = 520 # length of rails in mm

microsteps = 128

gear_ratio = 41 / 16

shot_velocities = [

20,

15,

10

]

Keypad, TFT and Encoder

The two end stop switches, and the rotary encoder button are instantiated as a Keypad object. The TFT is controlled via SPI. The rotary encoder is wired directly to GPIO for rotaryio.

Download File

Copy Code
keys = keypad.Keys((board.D2, board.A2, board.A3), value_when_pressed=False, pull=True)

encoder = rotaryio.IncrementalEncoder(board.D7, board.D6)

last_position = None

spi = board.SPI()

tft_cs = board.D10

tft_dc = board.D8

display_bus = FourWire(spi, command=tft_dc, chip_select=tft_cs, reset=board.D9)

display = ST7789(display_bus, width=240, height=240, rowstart=80, auto_refresh=False)

Graphics

The icons for the different menu options are on a single bitmap file (icons.bmp) and are used as a sprite sheet with TileGrid. There are two text objects: title_text and text_area that update depending on the menu.

Download File

Copy Code
splash = displayio.Group()

display.root_group = splash

bitmap = displayio.OnDiskBitmap(open("/icons.bmp", "rb"))

grid_bg = displayio.TileGrid(bitmap, pixel_shader=bitmap.pixel_shader,

tile_height=100, tile_width=100,

x=(display.width - 100) // 2,

y=(display.height - 100) // 2)

splash.append(grid_bg)

text_group = displayio.Group()

font = bitmap_font.load_font("/Arial-14.bdf")

title_text = "Camera Slider"

title_area = label.Label(font, text=title_text, color=0xFFFFFF)

title_area.anchor_point = (0.5, 0.0)

title_area.anchored_position = (display.width / 2, 25)

text_group.append(title_area)

splash.append(text_group)

text_area = label.Label(font, text="", color=0xFFFFFF)

text_area.anchor_point = (0.5, 1.0)

text_area.anchored_position = (display.width / 2, display.height - 25)

text_group.append(text_area)

TMC2209 UART

The two TMC2209 drivers are controlled over UART. Each driver is on its on UART line. Driver 1 uses the default UART (RX and TX) and driver 2 uses the secondary UART (D4 and D5). The microsteps are set for each of the drivers, along with a starting direction.

Download File

Copy Code
uart = busio.UART(tx=board.TX, rx=board.RX, baudrate=115200, timeout=0.1)

driver1 = TMC2209(uart=uart, addr=0)

driver2 = TMC2209(tx_pin=board.D4, rx_pin=board.D5, addr=0)

version1 = driver1.version

version2 = driver2.version

print(f"TMC2209 #1 Version: 0x{version1:02X}")

print(f"TMC2209 #2 Version: 0x{version2:02X}")

driver1.microsteps = microsteps

print(driver1.microsteps)

driver2.microsteps = microsteps

print(driver2.microsteps)

STEPS_PER_MM = 200 * microsteps / 8

driver1.direction = False

driver2.direction = True

Menu

The menu system is the backbone of the project. It is used to navigate through setting up a shot sequence. The TFT displays icons and text that correspond with each menu scene. The adv_menu() function takes care of advancing the graphics.

Download File

Copy Code
titles = ["Camera Slider", "Motor 1", "Motor 2", "Mode",

"Timelapse", "One-Shot", "Start?", "Running"]

home_text = ["Press to Begin", "0"]

motor1_text = ["Slide to Start Point", "0"]

motor2_text = ["Move to Start", "Move to End", "0"]

mode_text = ["Timelapse", "One-Shot"]

time_text = ["1", "5", "10", "15", "30"]

shot_text = ["Slow", "Medium", "Fast"]

start_text = ["Go!", 0]

running_text = ["STOP!", "Pause/Resume"]

running_icons = [6, 7]

mode_icons = [3, 4]

sub_titles = [home_text, motor1_text, motor2_text, mode_text,

time_text, shot_text, start_text, running_text]

text_area.text = home_text[0]

def adv_menu(m):

m = (m + 1) % 8

title_area.text = titles[m]

sub = sub_titles[m]

if m == 4:

grid_bg[0] = 3

elif m == 5:

grid_bg[0] = 4

elif m > 5:

grid_bg[0] = m - 1

else:

grid_bg[0] = m

text_area.text = sub[0]

display.refresh()

return m

Moving the Motors

Each driver has a dictionary to track its movement and start/pause/stop state throughout the different movement functions and the main loop.

Download File

Copy Code
motor1_movement = {

"is_active": False,

"current_step": 0,

"total_steps": 0,

"start_pos": 0,

"end_pos": 0,

"step_direction": 1,

"last_step_time": 0,

"step_interval": 0,

"is_paused": False,

"toggle_pause": False,

"stop_requested": False

}

motor2_movement = {

"is_active": False,

"current_step": 0,

"total_steps": 0,

"start_pos": 0,

"end_pos": 0,

"step_direction": 1,

"last_step_time": 0,

"step_interval": 0,

"is_paused": False,

"toggle_pause": False,

"stop_requested": False

}

A few functions are used to control the motor movement. Driver 1, which moves the camera along the rails, has its own set of functions since it is moving a much longer distance than driver 2, which pans the camera. Both drivers utilize the velocity calculation from the datasheet:

Download File

Copy Code
vactual = int(microsteps_per_second / (fCLK / (1 << 24)))

vactual = max(-(1 << 23), min((1 << 23) - 1, vactual))

Velocity is used instead of individual steps because it's a lot smoother and silent. The functions let you pass the time and number of steps and calculate the velocity from that. Additionally, with the combination of the dictionaries, both motors can be paused/resumed/stopped without blocking in the loop.

The Loop

At the top of the loop, if either of the motors' ["is_active"] parameter is True, it means that they are rotating. ticks_ms() is used to keep track of the time.

Download File

Copy Code
while True:

if motor1_movement["is_active"]:

current_time = supervisor.ticks_ms()

elapsed = current_time - motor1_movement["movement_start_time"]

if elapsed >= motor1_movement["movement_duration_ms"]:

driver1.rotate(0)

driver1.disable_motor()

motor1_movement["is_active"] = False

if motor2_movement["is_active"]:

current_time = supervisor.ticks_ms()

elapsed = current_time - motor2_movement["movement_start_time"]

if elapsed >= motor2_movement["movement_duration_ms"]:

driver2.rotate(0)

driver2.disable_motor()

motor2_movement["is_active"] = False

Keypad events and the rotary encoder are used to navigate the menu system. When the rotary encoder button is pressed (key_number 0), the menu advances. It is also used to pause or stop the motors while they are moving through a shot. The end stop switches and rotary encoder have different functionality depending on the menu index. The display is updated with different text if the rotary encoder is used to scroll through options.

Download File

Copy Code
event = keys.events.get()

if event:

if event.pressed:

print(f"{event.key_number} pressed")

if event.key_number == 0:

if menu == 0:

menu = adv_menu(menu)

elif menu == 2:

if select == 0:

motor2_coordinates[select] = driver2.position

if select == 1:

motor2_coordinates[select] = driver2.position

select += 1

text_area.text = motor2_text[select]

if select > 1:

select = 0

menu = adv_menu(menu)

if motor2_coordinates[0] > motor2_coordinates[1]:

move = motor2_coordinates[0] - motor2_coordinates[1]

else:

move = motor2_coordinates[1] - motor2_coordinates[0]

move = -move

driver2.step(move)

elif menu == 3:

if select == 1:

timelapse = False

menu += 1

select = 0

else:

timelapse = True

menu = adv_menu(menu)

elif menu == 4:

menu += 1

time_mode = select

menu = adv_menu(menu)

select = 0

print(f"{time_text[time_mode]}, timelapse: {timelapse}")

elif menu == 5:

shot_mode = select

menu = adv_menu(menu)

select = 0

print(f"{shot_text[shot_mode]}, timelapse: {timelapse}")

elif menu == 6:

menu = adv_menu(menu)

if timelapse:

movement_time = int(time_text[time_mode]) * 60

print(f"starting a timelapse for {time_text[time_mode]} minutes")

status1 = move_motor_with_rotate(

driver1,

motor1_movement,

start_position=0,

end_position=int(RAILS * STEPS_PER_MM),

duration_sec=movement_time,

microsteps=microsteps

)

if abs(motor2_coordinates[1] - motor2_coordinates[0]) > 0:

velocity2 = move_steps_over_time(camera_driver=True,

start_position=motor2_coordinates[0],

end_position=motor2_coordinates[1],

time_seconds=movement_time,

microsteps=microsteps,

ratio=gear_ratio)

print(f"driver2 velocity is: {velocity2}")

driver2.enable_motor(run_current=25)

driver2.rotate(velocity2)

motor2_movement["is_active"] = True

motor2_movement["start_pos"] = motor2_coordinates[0]

motor2_movement["end_pos"] = motor2_coordinates[1]

motor2_movement["movement_start_time"] = supervisor.ticks_ms()

motor2_movement["movement_duration_ms"] = movement_time * 1000

motor2_movement["velocity"] = velocity2

motor2_movement["total_steps"] = (abs(motor2_coordinates[1] -

motor2_coordinates[0]))

else:

print(f"starting a {shot_text[shot_mode]} one-shot")

movement_time = shot_velocities[shot_mode]

status1 = move_motor_with_rotate(

driver1,

motor1_movement,

start_position=0,

end_position=int(RAILS * STEPS_PER_MM),

duration_sec=movement_time,

microsteps=microsteps

)

if abs(motor2_coordinates[1] - motor2_coordinates[0]) > 0:

velocity2 = move_steps_over_time(camera_driver=True,

start_position=motor2_coordinates[0],

end_position=motor2_coordinates[1],

time_seconds=movement_time,

microsteps=microsteps,

ratio=gear_ratio)

driver2.enable_motor(run_current=25)

driver2.rotate(velocity2)

motor2_movement["is_active"] = True

motor2_movement["start_pos"] = motor2_coordinates[0]

motor2_movement["end_pos"] = motor2_coordinates[1]

motor2_movement["movement_start_time"] = supervisor.ticks_ms()

motor2_movement["movement_duration_ms"] = movement_time * 1000

motor2_movement["velocity"] = velocity2

motor2_movement["total_steps"] = (abs(motor2_coordinates[1] -

motor2_coordinates[0]))

elif menu == 7:

if select == 0:

stop_all_motors()

text_area.text = "Stopping..."

menu = adv_menu(menu)

elif select == 1:

pause_resume_motor1()

pause_resume_motor2()

paused_state = motor1_movement["is_paused"] or motor2_movement["is_paused"]

text_area.text = "Paused" if paused_state else "Running"

display.refresh()

if event.key_number == 1:

if menu == 1:

driver1.direction = False

driver1.reset_position()

menu = adv_menu(menu)

elif menu == 7:

stop_all_motors()

menu = adv_menu(menu)

if event.key_number == 2:

if menu == 1:

driver1.direction = True

driver1.reset_position()

menu = adv_menu(menu)

elif menu == 7:

stop_all_motors()

menu = adv_menu(menu)

display.refresh()

pos = encoder.position

if pos != last_pos:

if pos > last_pos:

if menu == 2:

driver2.step(-10)

if menu == 3:

select = (select + 1) % 2

text_area.text = mode_text[select]

grid_bg[0] = mode_icons[select]

if menu == 4:

select = (select + 1) % len(time_text)

text_area.text = time_text[select]

if menu == 5:

select = (select + 1) % len(shot_text)

text_area.text = shot_text[select]

if menu == 7:

select = (select + 1) % len(running_text)

text_area.text = running_text[select]

grid_bg[0] = running_icons[select]

else:

if menu == 2:

driver2.step(10)

if menu == 3:

select = (select - 1) % 2

text_area.text = mode_text[select]

grid_bg[0] = mode_icons[select]

if menu == 4:

select = (select - 1) % len(time_text)

text_area.text = time_text[select]

if menu == 5:

select = (select - 1) % len(shot_text)

text_area.text = shot_text[select]

if menu == 7:

select = (select - 1) % len(running_text)

text_area.text = running_text[select]

grid_bg[0] = running_icons[select]

last_pos = pos

display.refresh()

PCB Assembly

PCB Parts

Gather up the components that will be soldered to the PCB. The various socket headers are cut to length using a 36-pin long strip. Follow the How to Solder Headers for a thorough tutorial.

  • 2x 1x13 Socket Headers

  • 2x 1x10 Socket Headers

  • 1x11 Socket Header

  • 1x4 Socket Header

  • 1x Rotary Encoder

  • 1x DC Jack

  • 2x 2-pin 3.5mm Terminal Block

  • 2x 2.5mm pitch JST XH connectors

  • 2x 2.2k resistors

pcb_19

Wear eye protection when cutting header parts. Often pieces fly through the air and could injure someone if not careful.

Install Headers

Place the socket headers into their designated spots. Use mounting tack to keep them in place.

Flip the PCB and solder all of the pins in place.

Ensure all of the socket headers have been properly soldered.

headers_20

headers_21

headers_22

Install Header for Display

Place the 1x11 socket header into pins for the TFT display on the correct side of the PCB.

Flip the PCB over and solder the pins of the socket header in place.

Ensure all of the pins have been properly soldered.

install_23

install_24

install_25

Install DC Jack

Place the DC jack into the designated spot on the correct side of the PCB.

Flip the PCB over and solder the pins in place.

jack_26

jack_27

Install Terminal Blocks

Fit the terminal blocks into the designated spots on the correct side of the PCB.

Ensure the terminal blocks are in the correct orientation with the wire inserts facing inwards.

Flip the PCB over and solder the pins in place.

encoder_28

encoder_29

Install Rotary Encoder

Insert the rotary encoder into the designated spot on the correct side of the PCB.

Ensure the rotary encoder is fully seated before soldering.

Flip the PCB over and solder the pins in place.

rotary_30rotary_31

Install JST Connectors

Insert the two JST XH connectors into the designated spots on the correct side of the PCB.

Ensure the connectors are oriented correctly.

Flip the PCB over and solder the pins in place.

jst_32

jst_33

Install Resistors

Insert the 2.2k resistors into the designated spots on the correct side of the PCB.

Flip the PCB over and solder the pins in place.

Trim the leads of the resistors using flush snips.

resistors_33

resistors_34

Soldered Components

Take a moment to ensure all of the components have been properly soldered to the PCB.

soldered_35

soldered_36

Power Wires

Use the red and black 26AWG silicone wires to create two sets of wires. These will be used for the 2-pin terminal blocks.

Create two wires that are 4 inches (10cm) in length.

Create another set of wires that are 2 inches in length (5cm) in length.

power_37

Power Wires

Insert and secure the 4in (10cm) wires into the designated block terminal.

Insert and secure the shorter wires into the remaining block terminal.

wires_38

Install Boards

Install and solder the male headers to the TMC2209 stepper motor drivers, MPM3601 buck converter, KB2040 microcontroller and 1.3in TFT display.

Fit the various boards into their designated socket headers on the carrier PCB.

Insert the shorter power wires from the terminal block into the TMC2209 on the right. Red wire to + and black wire to -.

Insert the longer power wires from the terminal block into the TMC2209 on the left. Red wire to + and black wire to -.

boards_39

Install TFT Display

Fit the TFT display onto the designated socket headers on the correct side of the PCB.

Optionally install M2.5 (10mm tall) standoffs to the top tabs of the display.

display_40

display_41

Pan Assembly

Shorten the wires from the stepper motors so they're 8.5in (21.6cm) in length.

Gather up the parts for the pan assembly. Parts include:

  • Camera Plate

  • Pan Bearing Mount

  • Stepper Gear 16T

  • Stepper Motor

  • 6008ZZ 2RS ball bearing

  • 1/4"-20 Tripod Screw / Rubber Washers

  • 4x M3 x 6mm screws

  • 4x M3 x 14mm screws

  • 4x M3 hex nuts

pan_42

Install Ball Bearing

Press fit the 6008-2RS into the center cavity of the pan bearing mount.

ball_43

ball_44

Install Stepper Motor

Place the stepper motor onto the pan mount with mounting holes lined up.

Insert and fasten the M3 x 6mm long screws to secure the stepper motor to the pan mount.

motor_45

motor_46

Stepper Motor Wires

Insert the wires from the stepper motor through the center of the ball bearing.

stepper_47

Install Tripod Screw

Insert the tripod screw through the center hole in the camera plate. Use the rubber washer to keep the screw in place.

screw_48

Secure Camera Plate

Place the camera plate over the pan mount with the mounting holes lined up.

Insert the M3 x 14mm long screws through the camera plate standoffs and pan mount.

Use the M3 hex nuts to secure the parts together.

secure_49

secure_50

Install Pan Gear

Line up the 16T stepper gear with the shaft of the stepper motor.

Press fit the gear onto the shaft of the stepper motor.

gear_51

gear_52

Carriage Assembly

Switch Plate

Gather up the two switches, switch mounting plates, and M3 hardware.

plate_53

Secure Switches

Place the switch over the switch plate. Reference the photo for the correct orientation.

Insert two M3 x 16mm long screws through the mounting plate and switch. Use the M3 hex nuts to secure the parts together.

Repeat the installation process for the second switch.

switches_54

switches_55

Battery Holder

Get the battery holder, carriage motor plate, switch assembles, and M3 x 8mm hardware.

holder_56

Secure Battery Holder

Place the battery holder down with the bottom side facing up.

Place the carriage motor plate over the battery holder with the mounting holes lined up.

Place the two switch plates over the carriage motor plate with the mounting holes lined up.

Insert and fasten the M3 x 8mm screws through the parts then use the M3 hex nuts to secure the parts together.

holder_57

holder_58

holder_59

Slider Stepper Motor

Shorten the wires from the stepper motor so they're 8in (20.3cm) in length.

Gather up the 36T timing pulley and four M3 x 6mm long machine screws.

motor_60

Secure Stepper Motor

Place the second stepper motor onto the carriage motor plate with the mounting holes lined up.

Secure the stepper motor using M3 x 6mm long screws.

motor_61

motor_62

Install Pulley

Slide the 36T timing pulley over the shaft of the stepper motor.

Tighten the two M4 set screws to secure the timing pulley in place.

pulley_63

pulley_64

Install AA Batteries

Insert and install eight AA batteries into the battery holder.

batteries_65

Secure Battery

Orient the battery holder with the battery mount

Clip the battery into the tabs of the battery mount. Slightly bend the tabs to fit the battery underneath the clips.

Ensure the battery is fitted into the mount in the correct orientation.

battery_66

battery_67

battery_68

Carriage Gear

Gather up the Carriage top plate, 41T Gear and 4x M3x8mm long screws.

carriage_69

Install Carriage Gear

Place the 41T gear over the carriage top plate with the mounting holes lined up.

Insert and fasten the 4x M3x8mm long screws to secure the parts together.

gear_70

gear_71

gear_72

Pulley Wheels

Gather up the four pulley wheels, M5 x 25mm long screws and hex nuts.

pulley_73

Install Pulley Wheels

Place the four pulley wheels over the corner standoffs on the carriage motor plate.

Then, carefully place the carriage top plate over the motor plate with the corner standoffs lined up.

install_74

install_75

Sandwich Pulley Wheels

Insert the M5 x 25mm long screws through the carriage top plate, pulley wheels and carriage motor plate.

While holding the two carriage plates together, flip the whole carriage assembly and insert the M5 hex nuts into the cavities.

sandwich_76

sandwich_77

sandwich_78

Secure Carriage Plates

Fasten the four M5 screws while holding the hex nuts in place to secure the two carriage plates.

plates_79

plates_80

Secure PCB Plate

Place the PCB plate over the carriage top plate with the two mounting holes lined up.

Insert and fasten two M3 x 8mm long machine screws through the mounting holes.

Use M3 hex nuts to secure the parts together.

secure_81

secure_82

secure_83

Pan Motor and Carriage

Get the pan motor assembly ready to install onto the carriage assembly.

pan_84

Install Pan Motor

Insert the wires from the pan stepper motor through the center of the carriage gear assembly.

Press fit the pan motor assembly into the center of the carriage gear.

Ensure the 16T stepper gear and 41T carriage gear mesh together.

mesh_85

mesh_86

mesh_87

Rail Assembly

Extrusion Feet Bumpers

Get the left and right feet mounts and stick on rubber bumpers onto the corners. Reference the photo for correct placement.

bumpers_88

Install Extrusion

Insert the two-aluminum extrusion through the square holes in the left foot mount.

Insert an M4 slim t-nut into the profile of the extrusion with the top mounting hole lined up.

Fasten an M4 x 8mm into the slim t-nut. Do not fully tighten yet, just enough for the screw to grab onto the t-nut.

extrusion_89

extrusion_90

Install Carriage Assembly

Carefully slide the carriage assembly onto the two aluminum extrusions.

Ensure none of the wires are being kinked or obstructed by the carriage assembly.

The pulley wheels should fit into the profiles of the aluminum extrusion.

carriage_91

Install Right Foot

Fit the two extrusions through the square holes on the right foot mount. Reference the photo for the correct orientation.

Insert an M4 slim t-nut into the profile of the extrusion with the top mounting hole lined up.

Fasten an M4x8mm screw into the slim t-nut. Do not fully tighten yet, just enough for the screw to grab onto the t-nut.

foot_92

foot_93

Timing Belt

Gather up the timing belt, end caps and M4 hardware.

Cut the timing belt to length using scissors or cutters. The belt should measure out to be approximately 116.4cm (45in or 3.75ft) long.

belt_94

Install Endcaps

Locate the left foot and get the left end cap.

Place an M4 slim t-nut onto the profile of the aluminum extrusion. Reference the photo for the correct location.

Slip the left end cap onto the extrusion with the mounting hole lined up.

Fasten to secure he end cap to the extrusion.

Repeat this installation process for the right foot using the right end cap.

endcaps_95

endcaps_96

Tighten Feet

With the left and right feet not fully tightened yet, begin sliding the feet over to be flush with the end cap.

Then, tighten the screws to secure the feet to the extrusion.

feet_97

feet_98

Install Timing Belt

Insert the end of the timing belt through the side of the extrusion where the timing pulley is located.

Continue to thread the timing belt through the profile and under the pulley wheels.

Pull the timing belt over the 36T pulley that is secured to the stepper motor.

Keep the timing belt taught while threading to the opposite end of the extrusion.

timing_99

timing_100

Lock and Tension Belt

Insert the timing belt under the other foot and through the slit on the end cap.

While holding the end of the timing belt, pull the level to lock the timing belt in place. The lever should bite into the belt.

On the other end, pull the timing belt so its taught and proceed to pull the lever on the end cap to lock the belt into place.

Cut the timing belt to length if it's too long. Tuck any excess under the end cap so it's going inside the profile of the extrusion.

lock_101

lock_102

lock_103

Connect Switches

Orient the slider assembly so the PCB plate is facing you.

Then, locate the quick connect cable that is attached to the left switch. Plug that cable into the JST XH connector labeled A2 on the PCB.

Locate the cable for the right switch and plug that into the A3 labeled connector on the PCB.

connect_104

Connect Motors

Locate the wires from the stepper motor with the timing pulley. Connect those wires into the block terminal of the stepper driver with the label MOTOR 1.

  • B- (blue wire) from stepper motor to 2B pin on TMC2209

  • B+ (red wire) from stepper motor to 2A pin on TMC2209

  • A+ (black wire) from stepper motor to 1A pin on TMC2209

  • A- (green wire) from stepper motor to 1B pin on TMC2209

Use a screwdriver to secure the wires to the terminal blocks on the stepper driver.

Repeat this process for the panning stepper motor with the wires connecting to MOTOR 2.

connect_105

PCB Standoffs

Use M3 sized standoffs and hardware to secure the PCB to the PCB mounting plate.

Combine the 10mm long and 6mm long M3 standoffs to create a 16mm long standoff.

Create four sets of extended M3 standoffs.

standoffs_106

standoffs_107

Secure PCB

Install the 16mm long standoffs to the corner mounting holes on the PCB mounting plate. Finger tightens the hardware.

Place the PCB over the standoffs with the corner mounting holes lined up with the standoffs.

Insert and fasten M3 screws to secure the PCB to the PCB mounting plate.

secure_108

secure_109

secure_110

Power Plug

Grab the DC barrel from the battery holder and plug it into the DC jack on the PCB.

Use the on/off switch built-into the battery holder to power the circuit on.

The TFT display will power on.

plug_111

plug_112

Install Rotary Crank

Orient the 3D printed crank with the shaft of the rotary encoder.

Firmly press the crank over the shaft of the rotary encoder until it's fully seated.

Rotate the crank to turn the shaft of the rotary encoder.

Press the hinge of the crank to actuate the select button that's built into the rotary encoder.

crank_113

crank_114

Final Build

Congratulations on your build!

Proceed to the Usage page for documentation on how to set up shoots and use the UI to get going with the motorized camera slider.

3d_printing_thumb-loop

Usage

Start Slide

Turn on the power, then press the rotary encoder to begin.

Move the carriage to either the far left or right side of the railing until the switch is actuated.

3d_printing_start

Start Pan Motor Angle

Turn the rotary encoder left or right to the desired starting angle. Press the rotary encoder to set this as the starting pan angle.

3d_printing_motor2-start

End Pan Motor Angle

Turn the rotary encoder left or right to the desired ending angle. Press the rotary encoder to set this as the ending pan angle. Wait for the motor to move back to the start angle.

3d_printing_motor2-end

Set Mode

Turn the rotary encoder to select either Timelapse or One-Shot mode. Press the rotary encoder to choose the selected mode.

Timelapse Mode: Use for long captures. Options are 1, 5, 10, 15 and 30 minutes.

One Shot Mode: Use for short video clips. Options are slow (20 seconds), medium (15 seconds) and fast (10 seconds).

3d_printing_modes-loop

Go

Before starting the slide, take a moment to start video recording or trigger the timelapse on your camera.

Press the rotary encoder to start the stepper motors.

The stepper motors will go through their assigned motions and then stop when the carriage reaches the end of the rail, the panning motor reach the end position, and the end stop switch has been triggered.

The display will return to the start screen. Repeat the setup progress to start a new session.

3d_printing_go

Mfr Part # 5302
KB2040 RP2040 KEE BOAR DRIVER
Adafruit Industries LLC
Mfr Part # 4739
MPM3610 5V BUCK CONVERTER BREAKO
Adafruit Industries LLC
Mfr Part # 4313
GRAPHIC DISPLAY TFT 1.3"
Adafruit Industries LLC
Mfr Part # 377
ENCODER MECH QUAD VERT PC PIN
Adafruit Industries LLC
Mfr Part # 875
BATTERY HOLDER AA 8 CELL LEADS
Adafruit Industries LLC
Mfr Part # 6121
ADAFRUIT TMC2209 STEPPER MOTOR D
Adafruit Industries LLC
Mfr Part # 4423
JST-XH 2.54MM PITCH CONN KIT
Adafruit Industries LLC
Mfr Part # 3835
ARCADE BUTTON & SWITCH QUICK-CON
Adafruit Industries LLC
TERMINAL BLOCK - 2-PIN 3.5MM - P
Mfr Part # 724
TERMINAL BLOCK - 2-PIN 3.5MM - P
Adafruit Industries LLC
Mfr Part # 598
36-PIN 0.1 FEMALE HEADER- 5PK
Adafruit Industries LLC
LITTLE RUBBER BUMPER FEET - PACK
Mfr Part # 550
LITTLE RUBBER BUMPER FEET - PACK
Adafruit Industries LLC
Mfr Part # 2513
HOOK-UP 26AWG 600V RED 25'
Adafruit Industries LLC
Mfr Part # 2517
HOOK-UP 26AWG 600V BLACK 25'
Adafruit Industries LLC
THROUGH-HOLE RESISTORS - 2.2K OH
Mfr Part # 2782
THROUGH-HOLE RESISTORS - 2.2K OH
Adafruit Industries LLC
Add all DigiKey Parts to Cart
Have questions or comments? Continue the conversation on TechForum, DigiKey's online community and technical resource.