Connecting a Creality K1 to a Raspberry Pi

Summary of this thread in Discord - huuuge credit to exor for figuring this all out. Just trying to keep a log here as a note-to-self for what to do once I get the appropriate hardware and time.

Prerequisites

  • Make sure your K1 mainboard has a populated microusb header. This is very risky to do unless you have a way to recover your K1 to stock. See here for Creality's recovery flashing instructions.
  • You should already be using a probe supported by Simple AF because stock Klipper does not support the K1's multiple load cells, and the bed/prtouch MCU is not used here. This guide assumes that you are using a Cartographer over USB, but most probes should work.
  • You will need an SBC that supports USB OTG mode. This guide assumes that you'll be using a Raspberry Pi 4, but it's technically possible with other devices.

Hardware

Important power considerations

The Raspberry Pi will potentially attempt to backfeed power to the K1 if there is continuity on the VCC+ line of the USB cable. This is very risky, and can cause all sorts of hard-to-debug issues.

At the same time, the Pi and K1 must still share ground. If you cut the VCC+ line and the GND line on the USB cable, data lines D+/D‑ will see undefined reference potentials. Ensure the USB shielding and ground wires remains intact if you're doing any cable surgery.

You will need to

  • Use the Pi's USB-C port to connect to the printer (it's the only port on the Pi that supports OTG mode)
  • Disconnect the VCC+ line on the USB cable connecting your Pi to the printer to prevent backfeeding
  • Still find a way to provide power the Pi while its USB-C port is being used for OTG mode

This is still an area of investigation.

Preventing backfeeding

Options include:

  • using Kaptom tape on the VCC+ pin of the USB cable plugged into the printer
  • cable surgery to snip the VCC+ wire on the USB cable connecting the Pi to the printer
  • using a device to block USB Power while allowing USB Data
  • print a jig to block the VCC+ pin on the USB-A cable
  • using JST connectors to wire to the K1 mainboard's USB header and removing/snipping the VCC+ cable there

Powering the Raspberry Pi

This can be a little tricky since we need to use its USB-C port for OTG. Options include:

  • using a power/data splitter so you can still power the Pi over USB Type C (Still needs VCC+ Cut/Disabled)
  • powering the Raspberry Pi with a PoE adapter or HAT
  • providing appropriately regulated voltage to its GPIO power pins if you know exactly what you're doing

There are likely special concerns for the Raspberry Pi 5 because of its somewhat unique power requirements and software-side checks for appropriate power. The USB Type C power splitter OTG cable above is capable of providing the rated power with an appropriate power supply without extra config aside from making sure the VCC+ pin is disabled.

Data cables

  • Connect Raspberry Pi's USB Type C port to the K1's USB header, making sure you've addressed the VCC+ continuity issue above!. Shielded cables are highly recommended.
    • Simple: USB Type A Male to USB Type C Male to plug into the front USB port of the K1
    • Neat: JST to USB plugged into the K1 mainboard's USB header
    • Combining some options above
  • Connect Cartographer to the Pi over USB, using the included Cartographer USB cable
  • Recommended: Unplug the Camera cable from the K1, adapt it from JST to USB, and plug into the Raspberry Pi
  • Optional: Redirect the front USB port on your printer to your Raspberry Pi by adapting its cable from JST to USB and plugging it into an open port on the Raspberry Pi

Note, you may need some male/male or female/female JST adapter cables as well!

Software

Raspberry Pi Side

Simple AF for RPi

  • Install Simple AF for RPi
    • after cloning the repo, but before running the installer, run this command to get the appropriate printer profiles loaded
      ~/pellcorp/installer.sh --branch jp_k1_pi_base_printers
      
      • note: if/when these profiles land in main, this step won’t be necessary, and this guide will be updated to reflect that
    • if you have a K1 or K1 Max and arent sure if it's a 2023 or 2024 variant, you can run the following on your K1. If it returns 1, then its 2024. Otherwise its 2023.
      /usr/bin/get_sn_mac.sh structure_version
      
    • continue installing Simple AF for RPi, using the appropriate profile for or printer

Exposing your Pi as a USB Gadget

Next, we will Set your Raspberry Pi to boot as a USB gadget so the K1 can see it and interact with it. This disables host‑mode on the Pi's USB‑C port. Devices that are normally powered from that port stop will stop working, but you should still have the VCC+ pin disabled on your USB connection to the K1 to prevent backfeeding power.

  • set the appropriate boot flags
    #!/usr/bin/env bash
    set -euo pipefail
    
    BOOT=/boot/firmware       # Use /boot for pre‑Bookworm images
    CFG="$BOOT/config.txt"
    CMD="$BOOT/cmdline.txt"
    
    # Tell the bootloader to load the dwc2 overlay (puts USB‑C in OTG mode)
    grep -qxF 'dtoverlay=dwc2' "$CFG" || echo 'dtoverlay=dwc2' | tee -a "$CFG"
    
    # Add modules‑load=dwc2 to the single‑line kernel cmdline
    #    (‑i.bak keeps a one‑time backup)
    sed -i.bak -E 's/$/ modules-load=dwc2/' "$CMD"
    
    # Autoload libcomposite each boot so configfs gadgets “just work”
    grep -qxF 'libcomposite' /etc/modules || echo 'libcomposite' | tee -a /etc/modules
    
    echo "Done. Reboot and the Pi will present itself as a USB gadget."
    
  • Create a script to expose the Raspberry Pi to the K1 as a USB device using it's USB-C OTG functionality by writing the following to /usr/bin/setup_usb_gadget.sh
    #!/bin/bash
    set -euo pipefail
    
    GADGETDIR="g1"
    
    cd /sys/kernel/config/usb_gadget/
    if [ -d /sys/kernel/config/usb_gadget/$GADGETDIR ]; then
        exit 0
    fi
    
    mkdir -p $GADGETDIR
    cd $GADGETDIR
    
    echo 0x1d6b > idVendor  # Linux Foundation
    echo 0x0104 > idProduct # Multifunction Composite Gadget
    echo 0x0100 > bcdDevice # v1.0.0
    echo 0x0200 > bcdUSB    # USB2
    
    mkdir -p strings/0x409
    cat /sys/firmware/devicetree/base/serial-number > strings/0x409/serialnumber
    echo "Raspberry Pi" > strings/0x409/manufacturer
    echo "Multi-Serial Gadget" > strings/0x409/product
    
    # TODO: set max power
    
    mkdir -p configs/c.1/strings/0x409
    echo "Config 1: Multi-Serial" > configs/c.1/strings/0x409/configuration
    mkdir -p functions/acm.usb0     #ACM0 on the K1 is visible as GS0 on the Pi
    mkdir -p functions/acm.usb1     #ACM1 on the K1 is visible as GS1 on the Pi
    mkdir -p functions/acm.usb2     #ACM2 on the K1 is visible as GS2 on the Pi
    ln -s functions/acm.usb0 configs/c.1/
    ln -s functions/acm.usb1 configs/c.1/
    ln -s functions/acm.usb2 configs/c.1/
    
    # Attach the gadget to the USB Device Controller with the correct UDC
    UDC=$(ls /sys/class/udc | head -n1)
    echo "$UDC" > UDC
    
  • Make the script executable
    sudo chmod +x /usr/bin/setup_usb_gadget.sh
    
  • Set up a launch service to automatically run the script by creating /etc/systemd/system/usb-gadget-init.service
    [Unit]
    Description=MCU bridge gadget init
    Before=multi-user.target
    DefaultDependencies=no
    After=basic.target
    Requires=sys-kernel-config.mount
    Before=klipper.service
    
    [Service]
    ExecStart=/usr/bin/setup_usb_gadget.sh
    # TODO: add cleanup script that properly tears things down?
    Type=oneshot
    RemainAfterExit=yes
    
    [Install]
    WantedBy=multi-user.target
    
  • Enable the service and run it now in one command using: sudo systemctl enable --now usb-gadget-init.service

K1 Side

Instead of using Klipper on the K1's embedded Linux system, we want the K1's Linux system to forward every bit of data from the printer's MCU hardware to the Raspberry Pi over the USB connection.

  • If it's not already installed, install Simple AF firmware on your K1
  • Disable Klipper and Moonraker services by stopping them from being picked up at init:
    mv /etc/init.d/S55klipper_service /etc/init.d/_S55klipper_service
    mv /etc/init.d/S56moonraker_service /etc/init.d/_S56moonraker_service
    
  • Install required utilities to expose MCU devices to the USB OTG device and manage the USB lifecycle
    opkg install socat usbutils
    
  • Set up a service that uses socat by creating this file: /etc/init.d/S99rpi-bridge-init
    #!/bin/bash
    
    # OTG gadget info
    VENDOR_ID="1d6b"
    PRODUCT_ID="0104"
    DEV_PREFIX="/dev/ttyACM"
    
    # mcu paths
    MCU="/dev/ttyS7"
    NOZZLE_MCU="/dev/ttyS1"
    
    SOCAT_OPTS="raw,echo=0,b230400"
    
    PIDFILE_MCU="/var/run/socat_mcu.pid"
    PIDFILE_NOZZLE_MCU="/var/run/socat_nozzle_mcu.pid"
    LOG_FILE="/tmp/rpi_bridge.log"
    RETRY_DELAY=5
    MAX_RETRIES=12
    
    MCU_RESET_BIN="/usr/bin/mcu_reset.sh"
    USBRESET_BIN="/opt/bin/usbreset"
    SOCAT_BIN="/opt/bin/socat"
    
    # Function to log to /tmp/rpi_bridge.log
    log_message() {
        if [ -f "$LOG_FILE" ] || touch "$LOG_FILE" 2>/dev/null; then
            echo "$(date '+%Y-%m-%d %H:%M:%S') rpi-bridge-init: $1" >> "$LOG_FILE"
        else
            echo "$(date '+%Y-%m-%d %H:%M:%S') rpi-bridge-init: $1" > /dev/null
        fi
    }
    
    # Function to find ttyACM devices
    find_tty_devices() {
        ls ${DEV_PREFIX}* 2>/dev/null | sort -V
    }
    
    # Function to wait for USB device
    wait_for_usb_device() {
        while ! lsusb -d "$VENDOR_ID:$PRODUCT_ID" >/dev/null 2>&1; do
            log_message "Waiting for USB device $VENDOR_ID:$PRODUCT_ID to appear..."
            sleep 1
        done
        log_message "USB device $VENDOR_ID:$PRODUCT_ID detected"
    }
    
    # Function to perform usbreset
    usbreset_device() {
        local usb_dev
        usb_dev=$(lsusb -d "$VENDOR_ID:$PRODUCT_ID" | awk '{print $2 "/" $4}' | sed 's/:$//')
        if [ -n "$usb_dev" ]; then
            log_message "Resetting USB device: $usb_dev"
            /opt/bin/usbreset $usb_dev 2>/dev/null
            return $?
        else
            log_message "USB device $VENDOR_ID:$PRODUCT_ID not found for reset"
            return 1
        fi
    }
    
    # Function to check devices and select the first two valid ACM devices
    check_and_handle_devices() {
        local devices
        local valid_devices=""
        local count=0
        devices=$(find_tty_devices)
    
        if [ -z "$devices" ]; then
            log_message "No ttyACM devices found"
            return 1
        fi
    
        for dev in $devices; do
            # Check if valid character device
            if [ -c "$dev" ]; then
                valid_devices="$valid_devices $dev"
                count=$((count + 1))
                [ $count -eq 2 ] && break
            else
                log_message "Invalid device node $dev, removing"
                rm -f "$dev"
            fi
        done
    
        if [ $count -lt 2 ]; then
            log_message "Found only $count valid /dev/ttyACM devices, need 2"
            return 1
        fi
    
        TTY_0=$(echo "$valid_devices" | awk '{print $1}')
        TTY_1=$(echo "$valid_devices" | awk '{print $2}')
        return 0
    }
    
    setup_bridge() {
        if ! check_and_handle_devices; then
            return 1
        fi
    
        if ! usbreset_device; then
            log_message "USB reset failed, retrying process"
            return 1
        fi
    
        log_message "Waiting $RETRY_DELAY seconds to recheck ACM devices after USB reset"
        sleep $RETRY_DELAY
    
        if ! check_and_handle_devices; then
            log_message "Failed to reconfirm ACM devices after USB reset"
            return 1
        fi
    
        log_message "Starting socat bridge: $MCU <-> $TTY_0"
    
        start-stop-daemon --start --background --pidfile "$PIDFILE_MCU" --make-pidfile            --exec /opt/bin/socat -- "$MCU,$SOCAT_OPTS" "$TTY_0,raw,echo=0"
        if [ $? -eq 0 ]; then
            log_message "Socat bridge for $MCU started"
        else
            log_message "Failed to start socat bridge for $MCU"
            return 1
        fi
    
        log_message "Starting socat bridge: $NOZZLE_MCU <-> $TTY_1"
        start-stop-daemon --start --background --pidfile "$PIDFILE_NOZZLE_MCU" --make-pidfile            --exec /opt/bin/socat -- "$NOZZLE_MCU,$SOCAT_OPTS" "$TTY_1,$SOCAT_OPTS"
        if [ $? -eq 0 ]; then
            log_message "Socat bridge for $NOZZLE_MCU started"
        else
            log_message "Failed to start socat bridge for $NOZZLE_MCU"
            return 1
        fi
    
        log_message "Executing MCU reset script"
        /usr/bin/mcu_reset.sh
        if [ $? -eq 0 ]; then
            log_message "MCU reset completed successfully"
            return 0
        else
            log_message "Failed to execute MCU reset script"
            return 1
        fi
    }
    
    stop_socat() {
        if [ -f "$PIDFILE_MCU" ]; then
            log_message "Stopping socat bridge for $MCU"
            start-stop-daemon --stop --pidfile "$PIDFILE_MCU" --retry 5
            if [ $? -eq 0 ]; then
                rm -f "$PIDFILE_MCU"
                log_message "Socat bridge for $MCU stopped"
            else
                log_message "Failed to stop socat bridge for $MCU"
            fi
        fi
    
        if [ -f "$PIDFILE_NOZZLE_MCU" ]; then
            log_message "Stopping socat bridge for $NOZZLE_MCU"
            start-stop-daemon --stop --pidfile "$PIDFILE_NOZZLE_MCU" --retry 5
            if [ $? -eq 0 ]; then
                rm -f "$PIDFILE_NOZZLE_MCU"
                log_message "Socat bridge for $NOZZLE_MCU stopped"
            else
                log_message "Failed to stop socat bridge for $NOZZLE_MCU"
            fi
        fi
    }
    
    case "$1" in
        start)
            for x in "$USBRESET_BIN" "$SOCAT_BIN" "$MCU_RESET_BIN"; do
                if ! [ -x "$x" ]; then
                    echo "Error: '$x' not found or not executable" >&2
                    log_message "Error: '$x' not found or not executable"
                    exit 1
                fi
            done
    
            log_message "Starting rpi-bridge-init (max retries: $MAX_RETRIES)"
    
            attempt=0
            while [ $attempt -lt $MAX_RETRIES ]; do
                wait_for_usb_device
    
                if setup_bridge; then
                    log_message "rpi-bridge-init started successfully"
                    exit 0
                fi
    
                attempt=$((attempt + 1))
                log_message "Attempt $attempt failed, retrying after $RETRY_DELAY seconds..."
                sleep $RETRY_DELAY
    
            done
    
            log_message "Failed to start rpi-bridge"
            exit 1
            ;;
    
        stop)
            log_message "Stopping rpi-bridge-init"
            stop_socat
            exit 0
            ;;
    
        restart|reload)
            stop_socat
            sleep 1
            $0 start
            exit $?
            ;;
    
        *)
            echo "Usage: $0 {start|stop|restart|reload}" >&2
            exit 1
            ;;
    esac
    
    exit 0
    
  • Make it executable with chmod +x /etc/init.d/S99rpi-bridge-init
  • Restart your K1

Post install testing

At this point turning on the k1 and pi at the same time should result in the MCUs being available to the pi klipper setup as if they are local, after about 30s or so. A good indicator of successful setup is the printer's LEDs turning off and then on again as the last setup of the k1 init script runs. You may need to check the klippy.log for any configuration errors if it just says Unknown or any other non-specific message.

-K1 setup script logs to /tmp/rpi_bridge.log, and a normal boot should look something like:

2020-03-01 04:00:09 rpi-bridge-init: Starting rpi-bridge-init (max retries: 12)
2020-03-01 04:00:13 rpi-bridge-init: Waiting for USB device 1d6b:0104 to appear...
2020-03-01 04:00:14 rpi-bridge-init: USB device 1d6b:0104 detected
2020-03-01 04:00:14 rpi-bridge-init: Resetting USB device: 001/004
2025-08-12 23:45:07 rpi-bridge-init: Waiting 5 seconds to recheck ACM devices after USB reset
2025-08-12 23:45:12 rpi-bridge-init: Starting socat bridge: /dev/ttyS7 <-> /dev/ttyACM0
2025-08-12 23:45:12 rpi-bridge-init: Socat bridge for /dev/ttyS7 started
2025-08-12 23:45:12 rpi-bridge-init: Starting socat bridge: /dev/ttyS1 <-> /dev/ttyACM1
2025-08-12 23:45:12 rpi-bridge-init: Socat bridge for /dev/ttyS1 started
2025-08-12 23:45:12 rpi-bridge-init: Executing MCU reset script
2025-08-12 23:45:13 rpi-bridge-init: MCU reset completed successfully
2025-08-12 23:45:13 rpi-bridge-init: rpi-bridge-init started successfully

-Pi doesn't need any active script running, but a good indicator that it's setup properly is the following output in dmesg:

1
2
3
4
[    7.614279] dwc2 fe980000.usb: bound driver configfs-gadget.g1
[    7.846025] dwc2 fe980000.usb: new device is high-speed
[    7.925026] dwc2 fe980000.usb: new device is high-speed
[    7.983275] dwc2 fe980000.usb: new address 4

Usage note:

This script's intended flow is that the pi and k1 turn on at the same time, and never reboot independently of eachother. You will have to manually trigger things if you do this in order to restart the link.

Optional steps:

  • Screen
    • If you want to keep the stock screen on the K1, you can connect grumpyscreen on the K1 to the Raspberry Pi's IP:
      1. Open the configuration file /usr/data/guppyscreen/guppyscreen.json
      2. look for "moonraker_host": "127.0.0.01", near the bottom
      3. Change this to your pi's IP to make the screen point to that klipper instance
    • Your Raspberry Pi will be running Klipperscreen by default. You can either uninstall Klipperscreen, or use it by connecting an HDMI touchscreen to your Pi.
  • You can print a nice mount for both the mainboard and a Raspberry Pi once you get things working, if you want a nice all-in-one package!

Switching between Raspberry Pi and SAF k1 or stock k1:

  1. Disable S99rpi-bridge-init on the k1 by renaming it to prefix it with a _
  2. Re-enable stock klipper and moonraker by removing the _
  3. If the guppyscreen IP was changed then revert it back to 127.0.0.1
  4. Power off both devices and switch the cables back to standard configuration
    • You can use both the Pi and the k1 stock/saf at the same time, such as for camera or other services, without changes.
    • You can disable klipper and moonraker on the pi.

Future work

Generic Serial Port

Instead of (or in addition to) exposing just the MCUs to the Pi over serial, we can also expose a serial port to the K1 SBC itself.

  • K1 side could be simplified to just connect a serial console to ACM0 whenever it sees it come up
  • Pi side can repeatedly attempt to connect over GS0
  • Both sides can have retry logic to make sure they auto-reconnect after power cycles or transient disconnections
  • When the devices see each other, the Pi can then just do all of the detection and setup over the serial link

Initial USB Negotiation

Initial USB negotiation can be flaky. Need to investigate USB race conditions between the Pi and K1. It's likely that we can update both ends of the script to be able to properly negotiate the connection

  • mostly fixed with newer init script, but reconnection logic is not present on the k1 side.

TCP moonraker tunnel over serial

A TCP tunnel can be implemented to run over the 3rd exposed serial port as an alternative to using as a console, as described above, and "export" the moonraker server back over to the k1 so no changes are required to use the screen and it doesn't require a network connection. This is possible and has been implemented but it adds to the fragility of the setup, and a network connection is always required to the k1 for configuration purposes anyways. Example script for anyone interested in exploring that route: serial_tunnel.py

Edit

Pub: 16 Jul 2025 21:55 UTC

Edit: 13 Aug 2025 07:48 UTC

Views: 463