Monthly Archives: June 2025

3D Printing: Multi-Axis with Klipper

Updates:

  • 2025/06/17: new & advanced features with inline multi-tool support with 0.2.0
  • 2025/06/05: publishing article
  • 2025/05/30: updating maf.cfg and more documentation
  • 2025/05/23: first write-up with MAF maf.cfg

Introduction

As of May 2025 Klipper supports multi-axis support on G-code level. It was possible to define “manual stepper motors, but those needed to be handled via “Extended G-code” of Klipper. Now those “manual steppers” can be tied to a G-code axis, and thereby multi-axis system can be composed and becomes available in traditional G-code context.

MANUAL_STEPPER STEPPER=stepper_x1 GCODE_AXIS=A

and after that

G0 A100

can be used.

As of 2025/05 only G0/G1 is supported, but not G92, M82 (absolute extrusion), M83 (relative extrusion) in case “manual stepper” is used as additional extruder motors – so I coded a Multi-Axis Framework for Klipper, just a bunch of Klipper macros which provide a better multi-axis & multi-extruder support:

  • G0, G1 supports relative extrusion if enabled with M83 (I prefer)
  • G28 homing the new axes as well
  • G92 supports the new axes too
  • M82, M83 switches absolute/relative extrusion
  • T0, T1, … switch extruders
  • M104, M109 support of multiple extruders using T<n> notion

Klipper natively supports:

  • G90, G91 for new axes also

Setup

Create a dedicated file, or add following into printer.cfg, for example:

[include maf.cfg]

[gcode_macro MY_MAF]
variable_maf = {
      "X": { "motor": "stepper_x", "dir": -1, "end_pos": 0 },
      "Y": { "motor": "stepper_y", "dir": -1, "end_pos": 0 },
      "Z": { "motor": "stepper_z", "dir": -1, "end_pos": 0, "bounce": 2 },
      "E": { "motor": "extruder" },
      "U": { "motor": "stepper_x1", "dir": -1, "end_pos": 0 },
      "V": { "motor": "stepper_x2" "dir": 1, "end_pos": 380 },
      "W": { "motor": "stepper_x3" "dir": -1, "end_pos": 0 },
      "A": { "motor": "extruder1" },
      "B": { "motor": "extruder2" },
      "C": { "motor": "extruder3" }
   }
gcode:

This defines all axes, the additional 3 X carriages, and additional 3 extruders. If you declare X, Y, Z as well, it will home the same way as the new axes. By default after sensor is hit for homing, it bounces back 10mm (recommended for sensorless homing), you can override it with “bounce” value.

The maf.cfg once included, the MAF will be automatically executed at boot/restart.

M82 / M83: Absolute / Relative Extrusion

This applies to all extruders, M82 (absolute extrusion) is the default. I prefer relative extrusion (M83) as it makes it easier to insert or remove G-code without affecting existing G-code with extrusion.

G92: Set Position

Any new axis can be set now, including the extruders.

G92 W0 A0 B90 C110

M104 / M109: Set Temperatures

One can set temperature per extruder/tool:

T0
M104 S200
T1
M104 S200

or more compact

M104 T0 S200
M104 T1 S200

M104 set the temperature but doesn’t wait, and M109 waits until temperature is reached.

G28: Homing

The new axes can be homed as well:

G28 X Y U V W
G28 Z

Note: if you declared the axis in MY_MAF it will use the new G28 procedure, otherwise uses the native Klipper one. See also “Advanced Features” below to define tool axes.

In case you want to specify the default homing of G28 without any arguments, then add this in MY_MAF as well:

...
variable_misc = {
   ...
   "home_all": "XUYAIBZ"
   ...
}

this way you define the order exactly, otherwise the tool order (and the related axes) is used, and if no tools are defined, the order is according the map of axes to motor definition – it nicely falls back to what’s known to perform the homing. See also “Advanced Features” where you can define the order how axes are homed when you to define tool to axis mapping as well.

A multi-tool / multi-axis with multiple gantries can be quite complex to home, so this is why there is sufficient flexibility to define it and not hard-code in G-code or create another macro.

G0 / G1: Move & Extrude

The G0 / G1 is overridden in order to support relative extrusion.

M83                      ; relative extrusion
G0 X100 U300 Y100        ; move two X carriages on common Y gantry
G1 X150 U350 Y150 E5 W5  ; extrude with both extruders on the X carriages

Note: given we work in cartesian kinematics, X Y Z dictates the total distance as dist = sqrt (X^2+Y^2+Z^2) and the feedrate F (mm/min), any additional axis is given time t = dist (mm) / feedrate (mm/min) to reach its destination, and therefore has its own feedrate or speed – so, when operating new axes in this context, it’s up to you to be aware of the other axes speeds and keep them below operation limits. This is taken care of since V0.2.0 which recalculates the internal speeds.

MAF: Current State

One can send MAF command, and an output like this is received:

echo: == MAF: Multi-Axis Framework 0.2.0 ==
echo: debug: 0
echo: duplication: 
echo: speed: 40 mm/s
echo: tool T0: X:X Y:Y Z:Z E:E home:XY 
echo: tool T1: X:U E:W 
echo: tool T2: X:A Y:B E:C 
echo: tool T3: X:I E:K 
echo: motion mode: absolute
echo: extrude mode: absolute
echo: axis X: stepper_x, pos: 0.0000
echo: axis Y: stepper_y, pos: 0.0000
echo: axis Z: stepper_z, pos: 0.0000
echo: axis E: extruder / T0, pos: 0.0000
echo:    temperature: 31.1C / 0.0C
echo: axis U: stepper_x1, pos: 0.0000
echo: axis A: stepper_x2, pos: 0.0000
echo: axis I: stepper_x3, pos: 0.0000
echo: axis B: stepper_y1, pos: 0.0000
echo: axis W: extruder1 / T1, pos: 0.0000
echo:    temperature: 31.49C / 0.0C
echo: axis C: extruder2 / T2, pos: 0.0000
echo:    temperature: 31.03C / 0.0C
echo: axis K: extruder3 / T3, pos: 0.0000
echo:    temperature: 31.26C / 0.0C
ok

Download

This is a very experimental set of macros – the code has grown more complex since V0.2.0 – you can still use V0.1.0 when checking earlier version:

I plan to keep the code updated and backward compatible.

Advanced Features

Tool Axis Mapping

[ Since V0.2.0 ]

Additionally you can define tools and which axes they use, like:

[gcode_macro MY_MAF]
...
variable_tools = { 
   "T0": { "X":"X", "Y":"Y", "Z":"Z", "E":"E" },
   "T1": { "X":"U", "E":"W" },
   "T2": { "X":"A", "Y":"B", "E":"C" },
   "T3": { "X":"I", "E":"K" }
   }
...

once you added it in your MY_MAF section, you can use it like this, I call this format “inline multi-tool” G-code:

G1  T0 X100 Y10 E1  T1 X200 E1  T2 X100 Y200 E1  T3 X200 E1

performing all 4 tools at once via internal tool axis mapping.

You can also do “traditional” aka sequential tool switching and keep using XY and E for each tool and it will be mapped to the internal axes – but all operations are sequentially performed, tool by tool:

T0
G1 X100 Y10 E1
T1
G1 X200 E1
T2
G1 X100 Y200 E1
T3
G1 X200 E1

perhaps only useful for debugging.

Switch to T0 again to use multi-axis support, e.g. naming the axes with A, B, C or whatever additional axes you have.

Homing Tools

Further, also homing G28 can take T<n> notion such as:

G28 T0 T1 T2 T3

and it will home all respective axes associated with the tool, in the order as defined in the mapping. If you want to define your order or skip a certain axis in a tool for homing, you define:

...
   "T0": { "X":"X", "Y":"Y", "Z":"Z", "E":"E", "home": "XY" },
   ...
   "T4": { "X":"I", "Y":"J", "E": "K", "home": "YX" }
...

so even T0 has XYZ axes defined, it only homes X and Y, or in case of T4 it homes in different order like Y and then X.

Multi-Tool Duplication (Experimental)

[ Since V0.2.0 ]

It allows to define duplication on the Klipper level – in order to support this you define an additional variable:

[gcode_macro MY_MAF]
...
variable_duplication = { 
   "T0": { "offset": [0,0], "size": [200,200], "row": 0 },
   "T1": { "offset": [200,0], "size": [200,200], "row": 0 },
   "T2": { "offset": [0,200], "size": [200,200], "row": 1 },
   "T3": { "offset": [200,200], "size": [200,200], "row": 1 }
   }
...

this defines for each tool at XY offset position and size in XY where the tool operates, the row gives a hint how to mirror like with gantries. In the example above a 400×400 bed with 4 extruders/tools each 200×200 printable, two rows/gantries.

You enable duplication with:

MAF DUPLICATION=copy   ; or 'mirror'
; or
MAF duplication=copy   ; or 'mirror'

once activated, you send G-code for a single tool and it will be expanded using inline multi-tool, hence, in order to have duplication working, you need to define the tool axes also.

> MAF DUPLICATION=mirror DEBUG=1
> G0 X10 Y10
echo: [duplication] G0 T0 X10.0000 Y10.0000 T1 X390.0000 Y10.0000 T2 X10.0000 Y210.0000 T3 X390.0000 Y210.0000
echo: [inline multi-tool] {'G': '0', 'X': '10.0000', 'Y': '10.0000', 'U': '390.0000', 'A': '10.0000', 'B': '210.0000', 'I': '390.0000'}
echo: [final] G0 X10.0000 Y10.0000 U390.0000 A10.0000 B210.0000 I390.0000 

With DEBUG=1 or 2 you can see the steps taken to compose the final multi-axis G-code.

Using following “basic” G-code:

G21
G90
G28

MAF DUPLICATION=copy
G0 X10 Y10 F4800
G0 X100 Y100
G0 X50 Y50

MAF DUPLICATION=mirror
G4 P2000
G0 X10 Y10
G0 X100 Y100
G0 X50 Y50

which looks like this:

and when moving to X10 Y10 after homing with G28, some tools (and their axes) have more distance to cover like T2 and T3, and their travel distance is used to comply with F4800 – right now (2025/06) Klipper does not enforce speed limits to manual steppers, MAF does it for you.

Use Case

I used “MAF” for early prototyping a Multi Gantry with Multi Extruder (IDEX) setup – Ashtar Q (MG2 IDEX) – printing in duplication mode (horizontal & vertical mirrored) on G-code level:

Operating in two different modes:

  • 1x Klipper instance (with 4x MCUs): 2x Y gantries, 4x X carriages, 4x extruders = 11 axis (XYZE+XE+XYE+XE) – all motors fully synced
  • 2x Klipper instances (with 2x MCUs each): Dual of 1x Y gantry, 2x X carriages, 2x extruders = 6 axis (XYZE+XE) + 5 axis (XYE+XE) – two gantries loosely synced

Here some brief video of Ashtar Q (MG2 IDEX) actually printing duplicating:

The example above is “multi-axis” G-code, where all axes are individually delivered – whereas the duplication feature of MAF is available since V0.2.0 (2025/06) and uses “inline multi-tool” G-code internally. The “inline” is important here, otherwise it might be confused with traditional multi-line spread T0 G0 T1 G0 commands.

References

3D Printing: Klipper

Updates:

  • 2025/06/05: published with a few more references
  • 2025/05/31: adding MAF reference to support multi-axis functionality better
  • 2025/05/14: starting write-up

Introduction

Klipper is a host software running usually on a dedicated Single Board Computer (SBC) like Raspberry Pi or alike, and sends compact binary to MCUs for low-level to control stepper motors, and various sensors.

The kinematics and the resulting motion (acceleration, speed, deceleration) is planned on the host, therefore it’s very good to prototype experimental setups such as my Ashtar Q with multiple gantries and multiple extruders on each gantry.

In the past I was skeptic about introducing another part like an SBC for operating a 3D printer, as one introduces another point-of-failure, yet, the Klipper developers in particular Kevin O’Connor, managed to find the right balance to separate the planning from the realtime sensitive aspects from the “host” vs the “microcontroller” (aka MCU).

I still like RepRapFirmware running on a more powerful MCU without the need of a separate SBC – but I wanted to experiment with Klipper and so far I really liked its capabilities.

Hardware Declaration

What I really like is that Klipper allows to describe the MCUs at the low-level:

  • pins are named which control the stepper motor
  • simply add “!” in front, and the logic is inverted (e.g. change direction of the motor)
  • multiple MCUs can control different aspect of the printer, each MCU is named, and the pins are referenced <mcu_name>:<pin_name> and so (re)use older MCUs to build quite capable kinematic systems (not just 3D printers)

So a multitude of MCUs are available, from cheap ~10 EUR board up to Duet3D boards at 200+ EUR price – all capable running Klipper MCU firmware.

Extended G-code

Klipper provides vast features, and therefore requires more powerful way to distinct settings from the limited way G-code does it, so it introduces a powerful macro system based on Jinja2, and an additional set of new G-code commands, such as:

  • RESTART starts host software
  • FIRMWARE_RESTART restarts the MCU software/firmware

and SOMETHING A=100 BC=100,200 can be processed like this:

[gcode_macro SOMETHING]
gcode:
   {% set A = params['A'] %}
   {% set B,C = params['BC'].split(',') %}
   MANUAL_STEPPER STEPPER=my_stepper MOVE={A}
   MANUAL_STEPPER STEPPER=my_stepper_2 MOVE={B}
   {% set C2 = (A|int + B|int)/2 %}
   MANUAL_STEPPER STEPPER=my_stepper_3 MOVE={C}

and a bare/subset of Python is available within the macro coding – one has to refrain (like me) to code complex code there as one will reach its limits quickly.

Software & Hardware Modularity

By having multiple microcontrollers (MCUs) attached via USB and addressable within Klipper printer configuration, and declaring so called multiple “manual steppers outside of the common kinematics system, together with its macro-system one can do more than just move an extruder in XYZ, but create a robotic system with a lot of functionality, but it misses multiple motions queues limiting some applications.

Multi-Axis Capability

As of May 2025, Klipper is able to handle multi-axis G-code:

  1. declare MANUAL_STEPPER with connection with a driver, such as TMC2209 (move driver declaration before MANUAL_STEPPER in case you use virtual_endstop)
  2. home that axis with Extended G-code of Klipper (or use the “MAF” macro collection I wrote using G28)
  3. register MANUAL_STEPPER into “G-gcode space” by assigning an axis, e.g. A, B, C, D, U, V, W, or I, J, K, P, Q, R (be aware I and J is used for G2/G3 arc motions); MAF does handle this as well

Example of 2nd X carriage, in my case I have a dedicated MCU for the 2nd extruder named mcu2, this is for a BTT SKR MINI E3 V2.0 board:

[tmc2209 manual_stepper stepper_x1]    # -- must come before [manual_stepper] https://github.com/Klipper3d/klipper/issues/2563
uart_pin: mcu2:PC11
diag_pin: ^mcu2:PC0
uart_address: 0
run_current: 0.580
stealthchop_threshold: 999999
driver_SGTHRS: 100  # 255 is most sensitive value, 0 is least sensitive

[manual_stepper stepper_x1]
step_pin: mcu2:PB13
dir_pin: mcu2:PB12
enable_pin: !mcu2:PB14
microsteps: 16
rotation_distance: 40
#endstop_pin: ^PC0
endstop_pin: tmc2209_stepper_x1:virtual_endstop  

at point writing this, homing a “manual stepper” with G28 wasn’t yet supported – so I wrote a “Multi-Axis For Klipper” aka “MAF” macros which implements some of the missing functionalities:

G28 X U
G0 X200 Y200 U300 F3000

moving two X-carriages independently but together or atomic, whereas using something like:

T0
G0 X200 Y200 F3000
T1
G0 X300 F3000

would not guarantee the exact atomicity, given both carriages mounted on the same Y gantry, but introduce slight drift as introduced from the motion planner and stepper motor control – having it done as a new “axis”, we have it guaranteed at the same time.

Note: T<number> (tool selection) notion requires macros to define the behavior, like changing extruder and such – regardless, G-code line-wise only guarantees atomicity at line-level, not beyond.

So, treating the 2nd (or 3rd) X carriage as separate axis allows to implement True IDEX, independent motion of multiple extruders, but fully coordinated. Usually firmware handles mirror or copy of IDEX systems at low-level, now one controls the carriages at G-code level fully giving great freedom for multiple extruders printing at the same time.

Limitations

No Multiple Motion Queues (Yet)

As of writing (2025/06) Klipper does not yet support multiple motion queues like RepRapFirmware (RRF) provides. In essence it means, one cannot issue two concurrent running G0 or G1 commands but they run in sequence always, even if those G0/G1 commands might address different axes or gantries altogether.

References