Updating PID control for XY stage (with code examples)

The xy-pid branch of the Octopi Research repository fixes issues we have seen with closed loop PID control of XY stages. Previously, the controller would report that the stage has reached its target position before reaching it. Additionally, the controller was unable to detect motor stall conditions which could lead to motor damage. When a stall is detected, motion along the stalled axis stops until the controller is reset.
To use the updated software, clone the repository and check out the xy-pid branch, or download a .zip file with the code. The firmware is firmware/octopi_firmware_v2/main_controller_teensy41/main_controller_teensy41.ino and the software is is a PyQT script software/main.py. Make sure you have the correct configuration file in the software directory.

The new XY stage and stall protection features have parameters that must be set given your specific application - the parameters present in the Arduino sketch worked for my application but might not work for your applications.

First, configure the stall detection on line 722 of main_controller_teensy41.ino.
The first argument of tmc4361A_config_init_stallGuard() sets which axis you are setting the stall detection parameters for.
The next argument is the stall sensitivity, an integer ranging from -64 to +63 where negative numbers are more sensitive than positive numbers. Start with this parameter set to 0 and move the stage around. If the stall prevention has a false positive trigger, decrease the sensitivity by increasing this argument until false positives no longer happen.
The third argument is a boolean setting whether to low-pass filter the values. This decreases the chance of having false positives at the cost of having a slightly increased delay between the stall happening and when it is detected. In most cases it is recommended to use the filter.
The final argument is the vstall parameter, the min speed necessary to halt operation. If there is a stall and the motor’s speed in microsteps per second is less than this value, the halt operation will not occur.

Next, set the PID parameters. The arguments are target reach tolerance, position error tolerance, Kp, Ki, Kd, max speed, integral winding limit, and the derivative update rate. The ideal PID parameters may vary between stages depending on the requirements.
See the utils docstring for more details.

You can use the following script to test the stage’s y axis performance:

import control.core as core
import control.microcontroller as microcontroller
from control._def import *
import time

N_REPETITIONS = 40

def print_positon_stats(t0):
        xp, yp, zp, _ = microcontroller.get_pos()
        xe, ye, ze    = microcontroller.get_enc()
        print(f"{time.time()-t0}, {xp}, {yp}, {zp}, {xe}, {ye}, {ze}")
        
microcontroller = microcontroller.Microcontroller(version=CONTROLLER_VERSION)
navigationController = core.NavigationController(microcontroller)

# configure PID
microcontroller.configure_stage_pid(0, transitions_per_revolution=16282, flip_direction=True)
microcontroller.configure_stage_pid(1, transitions_per_revolution=16282, flip_direction=False)

microcontroller.turn_on_stage_pid(0)
microcontroller.turn_on_stage_pid(1)
displacement = 10

t0 = time.time()
while (time.time() - t0) < 0:
        print_positon_stats(t0)
        time.sleep(0.1)

t0 = time.time()
navigationController.move_y(displacement)
while microcontroller.is_busy():
        print_positon_stats(t0)
        time.sleep(0.01)
        
time.sleep(0.50)

t0 = time.time()
navigationController.move_y(-displacement)
while microcontroller.is_busy():
        print_positon_stats(t0)
        time.sleep(0.01)
        
# repetition testing

t0 = time.time()
print("Start position: ")
print_positon_stats(t0)
for _ in range(N_REPETITIONS):
        navigationController.move_y(displacement)
        while microcontroller.is_busy():
                time.sleep(0.01)
        time.sleep(0.005)
        print_positon_stats(t0)
        #time.sleep(0.80)

        navigationController.move_y(-displacement)
        while microcontroller.is_busy():
                time.sleep(0.01)
        time.sleep(0.005)
        print_positon_stats(t0)

Make sure the stage PID is configured properly with the encoder’s parameters. To test the x axis, replace all instances of move_y with move_x.

Note that this updated version still has bugs - we have observed that the XY stage behaves predictably when controlled solely by serial commands or solely by the joystick but it may behave unpredictably if a user resets the controller, moves the stage with the joystick, then tries moving the stage using serial commands.