📌 Successor project — Mellow-LLL-Plus-Canonical. Same autonomous core, plus a complete Klipper host-control layer: per-lane roles, 21 G-code commands, multi-lane (one USB per board), telemetry events, a Mainsail dashboard, a prebuilt binary and a bilingual manual. This repo stays valid for a minimal, autonomous-only build.
Custom Klipper MCU firmware for the Mellow FLY LLL Buffer Plus that brings together the best of both worlds:
- 🟢 the autonomous smart-buffer auto-feed of Mellow's standalone firmware (hall sensors drive the feed motor in firmware, real-time), and
- 🟢 the board running as a standard Klipper USB MCU, so the filament-entrance sensor can trigger a print pause through Klipper.
Both at the same time, over a single USB cable (+ 24 V motor power) — something neither the stock standalone firmware nor the community host-macro configs can do on their own.
The official Mellow firmware is standalone: the buffer auto-feeds on its own but the board does not talk Klipper (no USB MCU; runout has to go through a separate signal wire).
The community Klipper integrations (river29, ss1gohan13) flash stock Klipper and re-implement the buffer with host-side g-code macros. That works, but feed bursts go through the g-code queue (latency, can lag during long G1 E moves) and they switch the active extruder, which can misdirect extrusion during a print.
This project runs the buffer state machine inside the MCU firmware as a Klipper task — zero g-code-queue latency, no active-extruder juggling, can't stall a print. The host only reads the entrance sensor on PB7.
| Standalone (Mellow) | Stock Klipper + host macros | This firmware | |
|---|---|---|---|
| Auto-feed | ✅ in-firmware | ✅ in-firmware | |
| Klipper MCU / USB sensor | ❌ | ✅ | ✅ |
| Risk of disrupting a print | n/a | ACTIVATE_EXTRUDER |
✅ none |
| Runout → print pause | signal wire | USB (PB7) | USB (PB7) |
A small Klipper MCU task (src/buffer.c, DECL_INIT + DECL_TASK) ports the state machine of Mellow's open firmware (Fly3DTeam/Buffer) and drives the on-board TMC2208 over its single-wire UART (VACTUAL velocity mode):
HALL3 (PB4) blocked → motor FORWARD (push filament)
HALL2 (PB3) blocked → motor STOP
HALL1 (PB2) blocked → motor BACK (retract)
ENDSTOP_3 (PB7) = no filament → STOP (+ host pauses the print)
Photo-sensor convention is taken verbatim from the source: blocked = 1, clear = 0. Manual feed/retract buttons (KEY1/KEY2) and the mainboard control signals (PB5/PB6, active-low) are supported too. A 60 s continuous-feed timeout acts as a jam safety.
The only change versus the standalone is that the button "hold" is handled event-driven (non-blocking) instead of the original blocking
while()loop, because blocking for seconds would break Klipper host comms. Behaviour is identical.
| Function | Pin | Function | Pin |
|---|---|---|---|
| HALL1 | PB2 | KEY1 (retract) | PB13 |
| HALL2 | PB3 | KEY2 (feed) | PB12 |
| HALL3 | PB4 | FRONT signal | PB5 |
| Entrance switch | PB7 | BACK signal | PB6 |
| TMC EN | PA6 | TMC UART | PB1 |
Done from your Klipper host (e.g. a Raspberry Pi). Commands assume
~/klipperand~/katapult(Arksine/katapult) are cloned, the Klipper build toolchain is installed, anddfu-utilis available (sudo apt install dfu-util).
Connect the buffer's USB to the host and give it 24 V on VIN (the motor needs it; the firmware configures the TMC at first boot, so 24 V must be present then).
List USB serial devices to identify the board and note your unique chip ID (you'll reuse it in printer.cfg):
ls /dev/serial/by-id/
# e.g. usb-Klipper_stm32f072xb_3C003A...3720-if00 (or a Mellow / katapult id)
# └──────── your XXXXXX ────────┘On the buffer board: hold BOOT, tap RESET, release RESET, then release BOOT. Confirm (needs sudo):
sudo dfu-util -l
# → Found DFU: [0483:df11] ... name="@Internal Flash /0x08000000/064*0002Kg"sudo dfu-util -a 0 -d 0483:df11 -U ~/lll_backup_flash.bin -s 0x08000000:0x20000
sudo dfu-util -a 1 -d 0483:df11 -U ~/lll_backup_optionbytes.bin # option bytesKatapult lets you flash all future updates over USB without opening the case.
cd ~/katapult
make menuconfig- Micro-controller Architecture: STMicroelectronics STM32
- Processor model: STM32F072
- Clock Reference: 8 MHz crystal
- Communication interface: USB (on PA11/PA12)
- Application start offset: 8 KiB
- Support bootloader entry on rapid double click of reset: OFF
⚠️ (the LLL Plus has no usable reset button; leaving this ON makes the board fail to enumerate)
make clean && make
# board still in DFU (Step 1); flash with mass-erase so no stale app remains:
sudo dfu-util -a 0 -d 0483:df11 -D out/katapult.bin -s 0x08000000:force:mass-erase:leaveRe-plug USB and confirm Katapult enumerates:
ls /dev/serial/by-id/
# → usb-katapult_stm32f072xb_XXXXXX-if00cp src/buffer.c ~/klipper/src/buffer.cAdd one line to ~/klipper/src/Makefile (only compiled for the F072 buffer, never for your main board):
src-$(CONFIG_MACH_STM32F072) += buffer.c
⚠️ This line is reverted bygit pullon the Klipper tree — re-add it after updating Klipper.buffer.citself is untracked and survives.
⚠️ make menuconfigoverwrites~/klipper/.config. If you build other MCUs (your main board, toolhead…) from this same tree, back up its config first and restore it afterwards:cp ~/klipper/.config ~/klipper/.config.mainboard # backup # ...build & flash the buffer... cp ~/klipper/.config.mainboard ~/klipper/.config # restore later
cd ~/klipper
make menuconfig- Micro-controller Architecture: STMicroelectronics STM32
- Processor model: STM32F072
- Bootloader offset: 8 KiB bootloader (must match Katapult)
- Clock Reference: 8 MHz crystal
- Communication interface: USB (on PA11/PA12)
make clean && make~/klippy-env/bin/python3 ~/katapult/scripts/flashtool.py \
-f ~/klipper/out/klipper.bin \
-d /dev/serial/by-id/usb-katapult_stm32f072xb_XXXXXX-if00Use the klippy-env Python — it has
pyserial(the system Python usually doesn't). After flashing, the board re-enumerates asusb-Klipper_stm32f072xb_XXXXXX-if00.
[mcu LLL_PLUS]
serial: /dev/serial/by-id/usb-Klipper_stm32f072xb_XXXXXX-if00
restart_method: command
[filament_switch_sensor lll_entrance]
switch_pin: ^!LLL_PLUS:PB7
pause_on_runout: TrueChoosing the runout behaviour (pick one):
-
Simplest — most setups:
pause_on_runout: Trueand nothing else. On runout Klipper runs your normalPAUSE, including the park position your[gcode_macro PAUSE]/[pause_resume]already defines (the same one your slicer pause uses). Your park position lives in yourPAUSEmacro —pause_on_runoutcalls it, so you don't repeat it here. -
Add a notification / lights / etc.: keep
pause_on_runout: Trueand add arunout_gcode:— it runs after the pause, so use it for extra actions (not another pause):runout_gcode: M118 LLL Plus: filament runout — paused insert_gcode: M118 LLL Plus: filament reinserted -
Fully custom routine (your own park macro,
M600, unload sequence…): setpause_on_runout: Falseand put your macro inrunout_gcode::pause_on_runout: False runout_gcode: MY_RUNOUT_MACRO
⚠️ Don't put a barePAUSEinrunout_gcodewhilepause_on_runout: True— it's a double pause. Andpause_on_runout/PAUSEboth rely on[pause_resume]being in your config (standard on virtually all printers).
No motor/stepper/TMC section — the firmware owns the motor; the host only reads PB7. Then FIRMWARE_RESTART.
cd ~/klipper && make clean && make # (re-add the Makefile line if a git pull removed it)
~/klippy-env/bin/python3 ~/katapult/scripts/flashtool.py \
-f ~/klipper/out/klipper.bin \
-d /dev/serial/by-id/usb-Klipper_stm32f072xb_XXXXXX-if00
# then FIRMWARE_RESTARTflashtool reboots the running app into Katapult over USB automatically — you can target the Klipper serial directly, no buttons.
Derived directly from Mellow's firmware parameters (via the TMCStepper library it uses), not guessed:
| Register | Value | Notes |
|---|---|---|
| GCONF | 0x000001C4 |
spreadCycle, pdn_disable, mstep_reg_select (shaft bit set for FORWARD) |
| CHOPCONF | 0x12020055 |
toff=5, vsense, 64 µsteps, intpol |
| IHOLD_IRUN | 0x00010F07 |
IRUN=15, IHOLD=7 (≈ rms_current(500) @ Rsense 0.11) |
| PWMCONF | 0xC10D0024 |
pwm_autoscale |
| VACTUAL | 77575 |
260 rpm, 64 µsteps |
Speed/current can be changed in src/buffer.c (SPEED_RPM, the register values) and re-flashed.
Once this firmware is running, Mainsail's MCU Load bar for the LLL Plus may read ~100 %. This is a false positive — the MCU is not overloaded.
The autonomous auto-feed task bit-bangs the TMC2208 VACTUAL value over a software UART. Each update is a short, tightly-timed burst (~8 ms), which inflates the per-task timing standard deviation that Mainsail's load indicator is built on — even though the MCU is idle the vast majority of the time.
Measured over a full print (klippy.log / MCU stats):
| Metric | Value | Meaning |
|---|---|---|
mcu_awake |
~0.03–0.06 | awake only ~3–6 % of the time |
mcu_task_avg |
~0.4 ms | tasks are short |
mcu_task_stddev |
~2.5 ms | spikes from the bit-bang VACTUAL bursts (~8 ms) — this is what drives the "100 %" bar |
send_seq vs receive_seq |
equal | no command backlog |
| retransmits | a few bytes over a whole print | link healthy |
| "Timer too close" / shutdowns | 0 | no real overload |
Conclusion: the ~100 % reading is a display artifact of the bit-bang timing jitter, not an overloaded MCU. Auto-feed, buttons and runout all keep working normally — no action needed.
Validated on one setup (FLY LLL Buffer Plus, STM32F072CB).
Bench-tested & working:
- Auto-feed — hall sensors driving the motor (core state machine +
VACTUAL). - Manual hold of the feed / retract buttons (KEY1 / KEY2).
- Runout on
PB7→ print pause.
Ported faithfully from Mellow's firmware but not individually exercised here (no hardware/wiring to trigger them on this setup): mainboard signal control (PB5/PB6), the buttons' single-/double-click side-effects (EXT pin output, pause toggle), and the 60 s jam timeout.
Pins and register values are specific to this board.
- Fly3DTeam / Mellow — original FLY Buffer firmware & hardware (state machine ported from their open source).
- Klipper — MCU firmware framework.
- Katapult — USB bootloader.
- Community Klipper integrations: river29, ss1gohan13.
GPL-3.0 — derived from GPL-3.0 sources (Fly3DTeam/Buffer, Klipper).