├── .gitattributes ├── images ├── sg_chart2.PNG ├── connections_V2.jpg └── connections_V3.jpg ├── LICENSE ├── src ├── board_V3 │ ├── main.py │ ├── TMC_2209_driver.py │ ├── rgb_led.py │ ├── TMC_2209_uart.py │ ├── example.py │ └── stepper.py └── board_V2 │ ├── TMC_2209_driver.py │ ├── rgb_led.py │ ├── TMC_2209_uart.py │ ├── example.py │ └── stepper.py └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /images/sg_chart2.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndreaFavero71/stepper_sensorless_homing/HEAD/images/sg_chart2.PNG -------------------------------------------------------------------------------- /images/connections_V2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndreaFavero71/stepper_sensorless_homing/HEAD/images/connections_V2.jpg -------------------------------------------------------------------------------- /images/connections_V3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndreaFavero71/stepper_sensorless_homing/HEAD/images/connections_V3.jpg -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Andrea 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/board_V3/main.py: -------------------------------------------------------------------------------- 1 | """ 2 | Andrea Favero 25/06/2025 3 | 4 | Micropython code for Raspberry Pi Pico (RP2040 and RP2350) 5 | It demonstrates how to use TMC2209 StallGuard function for stepper sensorless homing. 6 | PIO is used by the RP2040 (or RP2350) to generate the stepper steps frequency. 7 | """ 8 | 9 | # INFO 10 | # when main.py exists, MicroPython execute it right after the booting. 11 | # This makes easy to get your own code automatically started, via an import command. 12 | 13 | # RISKS 14 | # In case the code has an endless loop, or deeper issues, it might prevent the board from 15 | # exposing the USB. If this happens, re-flashing the memory might be the only option. 16 | 17 | # SUGGESTIONs 18 | # The easy way, for sharp people: Comment out the import when you're working on the code. 19 | # The safer way, for lazy people (myself): Subject the import to a GPIO pin you can alter. 20 | 21 | from machine import Pin 22 | auto_start_pin = Pin(0, Pin.IN, Pin.PULL_UP) 23 | 24 | if auto_start_pin.value(): # GPIO high, via internal pullup resistor 25 | print("\nCode started via import in main.py\n") # feedback is printed to the terminal 26 | import example # file to execute by main.py, after booting 27 | 28 | else: # GPIO forced to GND to skip the autostart 29 | print("\nGPIO auto_start_pin forced LOW") # feedback is printed to the terminal 30 | print("Import at main.py gets skipped\n") # feedback is printed to the terminal 31 | -------------------------------------------------------------------------------- /src/board_V2/TMC_2209_driver.py: -------------------------------------------------------------------------------- 1 | """ 2 | This code is a small extract of a much larger TMC 2209 stepper driver. 3 | In this implementation are only kept the StallGuard related functions. 4 | 5 | Acknowledgements: 6 | Many thanks to Chr157i4n for making that exstensive TMC_2209 library. 7 | Many thanks to anonymousaga for his adaptation for Raspberry Pi Pico. 8 | 9 | 10 | Original file: TMC_2209_StepperDriver.py 11 | 12 | Source: https://github.com/troxel/TMC_UART 13 | Source: https://github.com/Chr157i4n/TMC2209_Raspberry_Pi 14 | Source: https://github.com/kjk25/TMC2209_ESP32 15 | Source: https://github.com/anonymousaga/TMC2209_RPI_PICO 16 | 17 | Copyright (c) 2020 troxel 18 | Copyright (c) 2014 The Python Packaging Authority (PyPA) 19 | Copyright (c) 2014 Mapbox 20 | 21 | Modified by: Andrea Favero (13/02/2025) 22 | 23 | Licensed under the GNU General Public License v3.0 24 | 25 | """ 26 | 27 | 28 | 29 | from TMC_2209_uart import TMC_UART 30 | from machine import Pin 31 | 32 | 33 | #----------------------------------------------------------------------- 34 | # TMC_2209 35 | # 36 | # this class has two different functions: 37 | # 1. change setting in the TMC-driver via UART 38 | # 2. move the motor via STEP/DIR pins 39 | #----------------------------------------------------------------------- 40 | class TMC_2209: 41 | 42 | #----------------------------------------------------------------------- 43 | # constructor 44 | #----------------------------------------------------------------------- 45 | def __init__(self, rx_pin, tx_pin, mtr_id=0, baudrate=115200, serialport=0): 46 | self.tmc_uart = TMC_UART(serialport, baudrate, rx_pin, tx_pin, mtr_id) 47 | 48 | self.TCOOLTHRS = 0x14 49 | self.SGTHRS = 0x40 50 | self.SG_RESULT = 0x41 51 | 52 | 53 | #----------------------------------------------------------------------- 54 | # destructor 55 | #----------------------------------------------------------------------- 56 | def __del__(self): 57 | print("Closing TMC driver serial comm") 58 | self.tmc.close() 59 | 60 | 61 | 62 | #----------------------------------------------------------------------- 63 | # return the current stallguard result 64 | # its will be calculated with every fullstep 65 | # higher values means a lower motor load 66 | #----------------------------------------------------------------------- 67 | def getStallguard_Result(self): 68 | sg_result = self.tmc_uart.read_int(self.SG_RESULT) 69 | return sg_result 70 | 71 | 72 | 73 | #----------------------------------------------------------------------- 74 | # sets the register bit "SGTHRS" to a given value 75 | # this is needed for the stallguard interrupt callback 76 | # SG_RESULT becomes compared to the double of this threshold. 77 | # SG_RESULT ≤ SGTHRS*2 78 | #----------------------------------------------------------------------- 79 | def setStallguard_Threshold(self, threshold): 80 | self.tmc_uart.write_reg_check(self.SGTHRS, threshold) 81 | 82 | 83 | 84 | #----------------------------------------------------------------------- 85 | # This is the lower threshold velocity for switching 86 | # on smart energy CoolStep and StallGuard to DIAG output. (unsigned) 87 | #----------------------------------------------------------------------- 88 | def setCoolStep_Threshold(self, threshold=1600): 89 | # CoolStep and StallGuards are active when threshold > TSPTEP (timeStep) 90 | # TSTEP is the step period in TMC clocks units (f=2MHz) calculated for 1/256 microstep 91 | # threshold = 1600 ensures STALLGUARD operation for f > 250Hz at 1/8 microstep 92 | self.tmc_uart.write_reg_check(self.TCOOLTHRS, threshold) 93 | 94 | 95 | 96 | #----------------------------------------------------------------------- 97 | # set a function to call back, when the driver detects a stall 98 | # via stallguard 99 | # high value on the diag pin can also mean a driver error 100 | #----------------------------------------------------------------------- 101 | def setStallguard_Callback(self, threshold = 50, handler=None): 102 | self.setStallguard_Threshold(threshold) 103 | self.setCoolStep_Threshold() 104 | gp11 = Pin(11, Pin.IN, Pin.PULL_DOWN) 105 | gp11.irq(trigger=Pin.IRQ_RISING, handler=handler) 106 | -------------------------------------------------------------------------------- /src/board_V2/rgb_led.py: -------------------------------------------------------------------------------- 1 | """ 2 | Andrea Favero 16/05/2025 3 | 4 | Micropython code for Raspberry Pi Pico (RP2040 and RP2350) 5 | It demonstrates how to use StallGuard function from TMC2209 stepper driver. 6 | This Class uses the RGB led of RP2040-Zero boards to feedback about the 7 | SENSORLESS HOMING process. 8 | 9 | 10 | 11 | 12 | MIT License 13 | 14 | Copyright (c) 2025 Andrea Favero 15 | 16 | Permission is hereby granted, free of charge, to any person obtaining a copy 17 | of this software and associated documentation files (the "Software"), to deal 18 | in the Software without restriction, including without limitation the rights 19 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 20 | copies of the Software, and to permit persons to whom the Software is 21 | furnished to do so, subject to the following conditions: 22 | 23 | The above copyright notice and this permission notice shall be included in all 24 | copies or substantial portions of the Software. 25 | 26 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 27 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 28 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 29 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 30 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 31 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 32 | SOFTWARE. 33 | """ 34 | 35 | 36 | from machine import Pin 37 | from neopixel import NeoPixel 38 | import _thread, time 39 | 40 | class RgbLed: 41 | 42 | _instance = None 43 | 44 | def __new__(cls, *args, **kwargs): 45 | if cls._instance is None: 46 | print("\nUploading rgb_led ...") 47 | cls._instance = super().__new__(cls) 48 | cls._instance._init() 49 | return cls._instance 50 | 51 | 52 | def _init(self): 53 | 54 | self.LED_ONBOARD_PIN = Pin(16, Pin.OUT) 55 | self.np = NeoPixel(Pin(self.LED_ONBOARD_PIN, Pin.OUT), 1) # 1=NeoPixel number 1 (the only one on the board) 56 | self._lock = _thread.allocate_lock() 57 | 58 | 59 | def _get_rgb_color(self, color, bright): # Adjust brightness for the specified color 60 | if color == 'red': 61 | return (int(bright * 255), 0, 0) 62 | elif color == 'green': 63 | return (0, int(bright * 255), 0) 64 | elif color == 'blue': 65 | return (0, 0, int(bright * 255)) 66 | else: 67 | raise ValueError("Invalid color. Choose 'red', 'green', or 'blue'.") 68 | 69 | 70 | 71 | def _validate_args(self, color, bright, times, time_s): # Validate parameters 72 | if not (0 <= bright <= 1): 73 | raise ValueError("Brightness 'bright' must be between 0 and 1.") 74 | if not (isinstance(times, int) and times > 0): 75 | raise ValueError("'times' must be a positive integer.") 76 | if not (isinstance(time_s, (int, float)) and time_s >= 0): 77 | raise ValueError("'time_s' must be a >= 0.") 78 | 79 | 80 | 81 | def flash_color(self, color, bright=1, times=1, time_s=0.01): 82 | self._validate_args(color, bright, times, time_s) 83 | rgb_color = self._get_rgb_color(color, bright) 84 | 85 | # Flash the specified color 86 | with self._lock: 87 | for _ in range(times): 88 | self.np[0] = rgb_color 89 | self.np.write() 90 | time.sleep(time_s) 91 | self.np[0] = (0, 0, 0) 92 | self.np.write() 93 | time.sleep(time_s) 94 | 95 | 96 | def fast_flash_red(self, ticks=1): 97 | self.np[0] = (255, 0, 0) 98 | self.np.write() 99 | for i in range(ticks): 100 | continue 101 | self.np[0] = (0, 0, 0) 102 | self.np.write() 103 | 104 | 105 | def fast_flash_green(self, ticks=1): 106 | self.np[0] = (0, 255, 0) 107 | self.np.write() 108 | for i in range(ticks): 109 | continue 110 | self.np[0] = (0, 0, 0) 111 | self.np.write() 112 | 113 | 114 | def fast_flash_blue(self, ticks=1): 115 | self.np[0] = (0, 0, 255) 116 | self.np.write() 117 | for i in range(ticks): 118 | continue 119 | self.np[0] = (0, 0, 0) 120 | self.np.write() 121 | 122 | 123 | def heart_beat(self, n=10,delay=0): 124 | self.flash_color('red', bright=0.06, times=n, time_s=0.05) 125 | time.sleep(delay/2) 126 | self.flash_color('green', bright=0.04, times=n, time_s=0.05) 127 | time.sleep(delay/2) 128 | self.flash_color('blue', bright=0.20, times=n, time_s=0.05) 129 | return True 130 | 131 | 132 | 133 | 134 | 135 | rgb_led = RgbLed() -------------------------------------------------------------------------------- /src/board_V3/TMC_2209_driver.py: -------------------------------------------------------------------------------- 1 | """ 2 | This code is a small extract of a much larger TMC 2209 stepper driver. 3 | In this implementation are only kept the StallGuard related functions. 4 | 5 | Acknowledgements: 6 | Many thanks to Chr157i4n for making that exstensive TMC_2209 library. 7 | Many thanks to anonymousaga for his adaptation for Raspberry Pi Pico. 8 | 9 | 10 | Original file: TMC_2209_StepperDriver.py 11 | 12 | Source: https://github.com/troxel/TMC_UART 13 | Source: https://github.com/Chr157i4n/TMC2209_Raspberry_Pi 14 | Source: https://github.com/kjk25/TMC2209_ESP32 15 | Source: https://github.com/anonymousaga/TMC2209_RPI_PICO 16 | 17 | Copyright (c) 2020 troxel 18 | Copyright (c) 2014 The Python Packaging Authority (PyPA) 19 | Copyright (c) 2014 Mapbox 20 | 21 | Modified by: Andrea Favero (13/02/2025, rev 25/06/2025) 22 | 23 | Licensed under the GNU General Public License v3.0 24 | 25 | """ 26 | 27 | 28 | 29 | from TMC_2209_uart import TMC_UART 30 | from machine import Pin 31 | 32 | 33 | #----------------------------------------------------------------------- 34 | # TMC_2209 35 | # 36 | # this class has two different functions: 37 | # 1. change setting in the TMC-driver via UART 38 | # 2. move the motor via STEP/DIR pins 39 | #----------------------------------------------------------------------- 40 | class TMC_2209: 41 | 42 | #----------------------------------------------------------------------- 43 | # constructor 44 | #----------------------------------------------------------------------- 45 | def __init__(self, rx_pin, tx_pin, mtr_id=0, baudrate=115200, serialport=0): 46 | self.tmc_uart = TMC_UART(serialport, baudrate, rx_pin, tx_pin, mtr_id) 47 | 48 | self.TCOOLTHRS = 0x14 49 | self.SGTHRS = 0x40 50 | self.SG_RESULT = 0x41 51 | 52 | 53 | #----------------------------------------------------------------------- 54 | # destructor 55 | #----------------------------------------------------------------------- 56 | def __del__(self): 57 | print("Closing TMC driver serial comm") 58 | self.tmc.close() 59 | 60 | 61 | 62 | #----------------------------------------------------------------------- 63 | # return the current stallguard result 64 | # its will be calculated with every fullstep 65 | # higher values means a lower motor load 66 | #----------------------------------------------------------------------- 67 | def getStallguard_Result(self): 68 | sg_result = self.tmc_uart.read_int(self.SG_RESULT) 69 | return sg_result 70 | 71 | 72 | 73 | #----------------------------------------------------------------------- 74 | # sets the register bit "SGTHRS" to a given value 75 | # this is needed for the stallguard interrupt callback 76 | # SG_RESULT becomes compared to the double of this threshold. 77 | # SG_RESULT ≤ SGTHRS*2 78 | #----------------------------------------------------------------------- 79 | def setStallguard_Threshold(self, threshold): 80 | self.tmc_uart.write_reg_check(self.SGTHRS, threshold) 81 | 82 | 83 | 84 | #----------------------------------------------------------------------- 85 | # This is the lower threshold velocity for switching 86 | # on smart energy CoolStep and StallGuard to DIAG output. (unsigned) 87 | #----------------------------------------------------------------------- 88 | def setCoolStep_Threshold(self, threshold=1600): #2800 89 | # CoolStep and StallGuards are active when threshold > TSTEP (timeStep) 90 | # TSTEP is the step period in TMC clocks units (f=2MHz) calculated for 1/256 microstep 91 | # threshold = 1600 ensures STALLGUARD operation for f > 250Hz at 1/8 microstep 92 | # 93 | # threshold = 3200 ensures STALLGUARD operation for 94 | # --> f > 250Hz at 1/8 microstep 95 | # --> f > 800Hz at 1/16 microstep 96 | # --> f > 1600Hz at 1/16 microstep 97 | # --> f > 3200Hz at 1/32 microstep 98 | self.tmc_uart.write_reg_check(self.TCOOLTHRS, threshold) 99 | 100 | 101 | 102 | #----------------------------------------------------------------------- 103 | # set a function to call back, when the driver detects a stall 104 | # via stallguard 105 | # high value on the diag pin can also mean a driver error 106 | #----------------------------------------------------------------------- 107 | def setStallguard_Callback(self, threshold = 50, handler=None): 108 | self.setStallguard_Threshold(threshold) 109 | self.setCoolStep_Threshold() 110 | gp11 = Pin(11, Pin.IN, Pin.PULL_DOWN) 111 | gp11.irq(trigger=Pin.IRQ_RISING, handler=handler) 112 | 113 | 114 | 115 | #----------------------------------------------------------------------- 116 | # test the UART functionality 117 | #----------------------------------------------------------------------- 118 | def test(self): 119 | return self.tmc_uart.test() 120 | 121 | 122 | -------------------------------------------------------------------------------- /src/board_V3/rgb_led.py: -------------------------------------------------------------------------------- 1 | """ 2 | Andrea Favero 16/05/2025 (rev 25/06/2025) 3 | 4 | Micropython code for Raspberry Pi Pico (RP2040 and RP2350) 5 | It demonstrates how to use StallGuard function from TMC2209 stepper driver. 6 | This Class uses the RGB led of RP2040-Zero boards to feedback about the 7 | SENSORLESS HOMING process. 8 | 9 | 10 | 11 | 12 | MIT License 13 | 14 | Copyright (c) 2025 Andrea Favero 15 | 16 | Permission is hereby granted, free of charge, to any person obtaining a copy 17 | of this software and associated documentation files (the "Software"), to deal 18 | in the Software without restriction, including without limitation the rights 19 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 20 | copies of the Software, and to permit persons to whom the Software is 21 | furnished to do so, subject to the following conditions: 22 | 23 | The above copyright notice and this permission notice shall be included in all 24 | copies or substantial portions of the Software. 25 | 26 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 27 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 28 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 29 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 30 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 31 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 32 | SOFTWARE. 33 | """ 34 | 35 | 36 | from machine import Pin 37 | from neopixel import NeoPixel 38 | import _thread, time 39 | 40 | class RgbLed: 41 | 42 | _instance = None 43 | 44 | def __new__(cls, *args, **kwargs): 45 | if cls._instance is None: 46 | print("\nUploading rgb_led ...") 47 | cls._instance = super().__new__(cls) 48 | cls._instance._init() 49 | return cls._instance 50 | 51 | 52 | def _init(self): 53 | 54 | self.LED_ONBOARD_PIN = Pin(16, Pin.OUT) 55 | self.np = NeoPixel(Pin(self.LED_ONBOARD_PIN, Pin.OUT), 1) # 1=NeoPixel number 1 (the only one on the board) 56 | self._lock = _thread.allocate_lock() 57 | 58 | 59 | def _get_rgb_color(self, color, bright): # Adjust brightness for the specified color 60 | if color == 'red': 61 | return (int(bright * 255), 0, 0) 62 | elif color == 'green': 63 | return (0, int(bright * 255), 0) 64 | elif color == 'blue': 65 | return (0, 0, int(bright * 255)) 66 | else: 67 | raise ValueError("Invalid color. Choose 'red', 'green', or 'blue'.") 68 | 69 | 70 | 71 | def _validate_args(self, color, bright, times, time_s): # Validate parameters 72 | if not (0 <= bright <= 1): 73 | raise ValueError("Brightness 'bright' must be between 0 and 1.") 74 | if not (isinstance(times, int) and times > 0): 75 | raise ValueError("'times' must be a positive integer.") 76 | if not (isinstance(time_s, (int, float)) and time_s >= 0): 77 | raise ValueError("'time_s' must be a >= 0.") 78 | 79 | 80 | 81 | def flash_color(self, color, bright=1, times=1, time_s=0.01): 82 | self._validate_args(color, bright, times, time_s) 83 | rgb_color = self._get_rgb_color(color, bright) 84 | 85 | # Flash the specified color 86 | with self._lock: 87 | for _ in range(times): 88 | self.np[0] = rgb_color 89 | self.np.write() 90 | time.sleep(time_s) 91 | self.np[0] = (0, 0, 0) 92 | self.np.write() 93 | time.sleep(time_s) 94 | 95 | 96 | def fast_flash_red(self, ticks=1): 97 | self.np[0] = (255, 0, 0) 98 | self.np.write() 99 | for i in range(ticks): 100 | continue 101 | self.np[0] = (0, 0, 0) 102 | self.np.write() 103 | 104 | 105 | def fast_flash_green(self, ticks=1): 106 | self.np[0] = (0, 255, 0) 107 | self.np.write() 108 | for i in range(ticks): 109 | continue 110 | self.np[0] = (0, 0, 0) 111 | self.np.write() 112 | 113 | 114 | def fast_flash_blue(self, ticks=1): 115 | self.np[0] = (0, 0, 255) 116 | self.np.write() 117 | for i in range(ticks): 118 | continue 119 | self.np[0] = (0, 0, 0) 120 | self.np.write() 121 | 122 | 123 | def heart_beat(self, n=10,delay=0): 124 | self.flash_color('red', bright=0.06, times=n, time_s=0.05) 125 | time.sleep(delay/2) 126 | self.flash_color('green', bright=0.04, times=n, time_s=0.05) 127 | time.sleep(delay/2) 128 | self.flash_color('blue', bright=0.20, times=n, time_s=0.05) 129 | return True 130 | 131 | 132 | 133 | 134 | 135 | rgb_led = RgbLed() -------------------------------------------------------------------------------- /src/board_V2/TMC_2209_uart.py: -------------------------------------------------------------------------------- 1 | """ 2 | This code is slightly modified from the original (TMC_2209_reg.py file is not 3 | imported and the few registers used in this project are initialized within the Classes). 4 | 5 | Acknowledgements: 6 | Many thanks to Chr157i4n for his estensive TMC_2209 library. 7 | Many thanks to anonymousaga for his adaptation for Raspberry Pi Pico. 8 | 9 | 10 | Original file: TMC_2209_uart.py 11 | 12 | Source: https://github.com/troxel/TMC_UART 13 | Source: https://github.com/Chr157i4n/TMC2209_Raspberry_Pi 14 | Source: https://github.com/kjk25/TMC2209_ESP32 15 | Source: https://github.com/anonymousaga/TMC2209_RPI_PICO 16 | 17 | Copyright (c) 2020 troxel 18 | Copyright (c) 2014 The Python Packaging Authority (PyPA) 19 | Copyright (c) 2014 Mapbox 20 | 21 | Modified by: Andrea Favero (13/02/2025) 22 | 23 | Licensed under the GNU General Public License v3.0 24 | 25 | """ 26 | 27 | 28 | import time 29 | import sys 30 | import binascii 31 | import struct 32 | from machine import UART 33 | 34 | #----------------------------------------------------------------------- 35 | # TMC_UART 36 | # 37 | # this class is used to communicate with the TMC via UART 38 | # it can be used to change the settings of the TMC. 39 | # like the current or the microsteppingmode 40 | #----------------------------------------------------------------------- 41 | class TMC_UART: 42 | 43 | mtr_id=0 44 | ser = None 45 | rFrame = [0x55, 0, 0, 0 ] 46 | wFrame = [0x55, 0, 0, 0 , 0, 0, 0, 0 ] 47 | communication_pause = 0 48 | 49 | #----------------------------------------------------------------------- 50 | # constructor 51 | #----------------------------------------------------------------------- 52 | def __init__(self, serialport, baudrate, rxpin, txpin, mtr_id_arg): 53 | self.ser = UART(serialport, baudrate=115200, bits=8, parity=None, stop=1, tx=txpin, rx=rxpin) 54 | self.mtr_id=mtr_id_arg 55 | #self.ser.timeout = 20000/baudrate # adjust per baud and hardware. Sequential reads without some delay fail. 56 | self.communication_pause = 500/baudrate # adjust per baud and hardware. Sequential reads without some delay fail. 57 | 58 | self.IFCNT = 0x02 59 | 60 | 61 | 62 | #----------------------------------------------------------------------- 63 | # destructor 64 | #----------------------------------------------------------------------- 65 | def __del__(self): 66 | print("Closing UART") 67 | self.ser.close() 68 | 69 | #----------------------------------------------------------------------- 70 | # this function calculates the crc8 parity bit 71 | #----------------------------------------------------------------------- 72 | def compute_crc8_atm(self, datagram, initial_value=0): 73 | crc = initial_value 74 | # Iterate bytes in data 75 | for byte in datagram: 76 | # Iterate bits in byte 77 | for _ in range(0, 8): 78 | if (crc >> 7) ^ (byte & 0x01): 79 | crc = ((crc << 1) ^ 0x07) & 0xFF 80 | else: 81 | crc = (crc << 1) & 0xFF 82 | # Shift to next bit 83 | byte = byte >> 1 84 | return crc 85 | 86 | #----------------------------------------------------------------------- 87 | # reads the registry on the TMC with a given address. 88 | # returns the binary value of that register 89 | #----------------------------------------------------------------------- 90 | def read_reg(self, reg): 91 | 92 | rtn = "" 93 | 94 | self.rFrame[1] = self.mtr_id 95 | self.rFrame[2] = reg 96 | self.rFrame[3] = self.compute_crc8_atm(self.rFrame[:-1]) 97 | 98 | rt = self.ser.write(bytes(self.rFrame)) 99 | if rt != len(self.rFrame): 100 | print("TMC2209: Err in write {}".format(__), file=sys.stderr) 101 | return False 102 | time.sleep(self.communication_pause) # adjust per baud and hardware. Sequential reads without some delay fail. 103 | if self.ser.any(): 104 | rtn = self.ser.read()#read what it self 105 | time.sleep(self.communication_pause) # adjust per baud and hardware. Sequential reads without some delay fail. 106 | if rtn == None: 107 | print("TMC2209: Err in read") 108 | return "" 109 | # print("received "+str(len(rtn))+" bytes; "+str(len(rtn)*8)+" bits") 110 | return(rtn[7:11]) 111 | 112 | 113 | #----------------------------------------------------------------------- 114 | # this function tries to read the registry of the TMC 10 times 115 | # if a valid answer is returned, this function returns it as an integer 116 | #----------------------------------------------------------------------- 117 | def read_int(self, reg): 118 | tries = 0 119 | while(True): 120 | rtn = self.read_reg(reg) 121 | tries += 1 122 | if(len(rtn)>=4): 123 | break 124 | else: 125 | if tries <= 1: 126 | print("\nTMC2209: did not get the expected 4 data bytes. Instead got "+str(len(rtn))+" Bytes ...") 127 | if(tries>=10): 128 | print("TMC2209: after 10 tries not valid answer. exiting") 129 | print("TMC2209: is Stepper Powersupply switched on ?\n\n") 130 | return() # AF 131 | # raise SystemExit # AF 132 | val = struct.unpack(">i",rtn)[0] 133 | return(val) 134 | 135 | 136 | #----------------------------------------------------------------------- 137 | # this function can write a value to the register of the tmc 138 | # 1. use read_int to get the current setting of the TMC 139 | # 2. then modify the settings as wished 140 | # 3. write them back to the driver with this function 141 | #----------------------------------------------------------------------- 142 | def write_reg(self, reg, val): 143 | 144 | self.wFrame[1] = self.mtr_id 145 | self.wFrame[2] = reg | 0x80; # set write bit 146 | self.wFrame[3] = 0xFF & (val>>24) 147 | self.wFrame[4] = 0xFF & (val>>16) 148 | self.wFrame[5] = 0xFF & (val>>8) 149 | self.wFrame[6] = 0xFF & val 150 | 151 | self.wFrame[7] = self.compute_crc8_atm(self.wFrame[:-1]) 152 | 153 | rtn = self.ser.write(bytes(self.wFrame)) 154 | if rtn != len(self.wFrame): 155 | print("TMC2209: Err in write {}".format(__), file=sys.stderr) 156 | return False 157 | time.sleep(self.communication_pause) 158 | 159 | return(True) 160 | 161 | 162 | #----------------------------------------------------------------------- 163 | # this function also writes a value to the register of the TMC 164 | # but it also checks if the writing process was successfully by checking 165 | # the InterfaceTransmissionCounter before and after writing 166 | #----------------------------------------------------------------------- 167 | def write_reg_check(self, reg, val): 168 | IFCNT1 = self.read_int(self.IFCNT) 169 | self.write_reg(reg, val) 170 | IFCNT2 = self.read_int(self.IFCNT) 171 | IFCNT2 = self.read_int(self.IFCNT) 172 | 173 | if(IFCNT1 >= IFCNT2): 174 | print("TMC2209: writing not successful!") 175 | print("reg:{} val:{}", reg, val) 176 | print("IFCNT:", IFCNT1, IFCNT2) 177 | return False 178 | else: 179 | return True 180 | 181 | 182 | 183 | #----------------------------------------------------------------------- 184 | # this sets a specific bit to 1 185 | #----------------------------------------------------------------------- 186 | def set_bit(self, value, bit): 187 | return value | (bit) 188 | 189 | 190 | #----------------------------------------------------------------------- 191 | # this sets a specific bit to 0 192 | #----------------------------------------------------------------------- 193 | def clear_bit(self, value, bit): 194 | return value & ~(bit) 195 | 196 | -------------------------------------------------------------------------------- /src/board_V3/TMC_2209_uart.py: -------------------------------------------------------------------------------- 1 | """ 2 | This code is slightly modified from the original (TMC_2209_reg.py file is not 3 | imported and the few registers used in this project are initialized within the Classes). 4 | 5 | Acknowledgements: 6 | Many thanks to Chr157i4n for his estensive TMC_2209 library. 7 | Many thanks to anonymousaga for his adaptation for Raspberry Pi Pico. 8 | 9 | 10 | Original file: TMC_2209_uart.py 11 | 12 | Source: https://github.com/troxel/TMC_UART 13 | Source: https://github.com/Chr157i4n/TMC2209_Raspberry_Pi 14 | Source: https://github.com/kjk25/TMC2209_ESP32 15 | Source: https://github.com/anonymousaga/TMC2209_RPI_PICO 16 | 17 | Copyright (c) 2020 troxel 18 | Copyright (c) 2014 The Python Packaging Authority (PyPA) 19 | Copyright (c) 2014 Mapbox 20 | 21 | Modified by: Andrea Favero (13/02/2025, rev 25/06/2025) 22 | 23 | Licensed under the GNU General Public License v3.0 24 | 25 | """ 26 | 27 | 28 | import time 29 | import sys 30 | import binascii 31 | import struct 32 | from machine import UART 33 | 34 | #----------------------------------------------------------------------- 35 | # TMC_UART 36 | # 37 | # this class is used to communicate with the TMC via UART 38 | # it can be used to change the settings of the TMC. 39 | # like the current or the microsteppingmode 40 | #----------------------------------------------------------------------- 41 | class TMC_UART: 42 | 43 | mtr_id=0 44 | ser = None 45 | rFrame = [0x55, 0, 0, 0 ] 46 | wFrame = [0x55, 0, 0, 0 , 0, 0, 0, 0 ] 47 | communication_pause = 0 48 | 49 | #----------------------------------------------------------------------- 50 | # constructor 51 | #----------------------------------------------------------------------- 52 | def __init__(self, serialport, baudrate, rxpin, txpin, mtr_id_arg): 53 | self.ser = UART(serialport, baudrate=baudrate, bits=8, parity=None, stop=1, tx=txpin, rx=rxpin) # baudrate 115200 54 | self.mtr_id=mtr_id_arg 55 | #self.ser.timeout = 20000/baudrate # adjust per baud and hardware. Sequential reads without some delay fail. 56 | self.communication_pause = 500/baudrate # adjust per baud and hardware. Sequential reads without some delay fail. 57 | 58 | self.IFCNT = 0x02 59 | 60 | 61 | 62 | #----------------------------------------------------------------------- 63 | # destructor 64 | #----------------------------------------------------------------------- 65 | def __del__(self): 66 | print("Closing UART") 67 | self.ser.close() 68 | 69 | #----------------------------------------------------------------------- 70 | # this function calculates the crc8 parity bit 71 | #----------------------------------------------------------------------- 72 | def compute_crc8_atm(self, datagram, initial_value=0): 73 | crc = initial_value 74 | # Iterate bytes in data 75 | for byte in datagram: 76 | # Iterate bits in byte 77 | for _ in range(0, 8): 78 | if (crc >> 7) ^ (byte & 0x01): 79 | crc = ((crc << 1) ^ 0x07) & 0xFF 80 | else: 81 | crc = (crc << 1) & 0xFF 82 | # Shift to next bit 83 | byte = byte >> 1 84 | return crc 85 | 86 | #----------------------------------------------------------------------- 87 | # reads the registry on the TMC with a given address. 88 | # returns the binary value of that register 89 | #----------------------------------------------------------------------- 90 | def read_reg(self, reg): 91 | 92 | rtn = "" 93 | 94 | self.rFrame[1] = self.mtr_id 95 | self.rFrame[2] = reg 96 | self.rFrame[3] = self.compute_crc8_atm(self.rFrame[:-1]) 97 | 98 | rt = self.ser.write(bytes(self.rFrame)) 99 | if rt != len(self.rFrame): 100 | print("TMC2209: Err in write {}".format(__), file=sys.stderr) 101 | return False 102 | time.sleep(self.communication_pause) # adjust per baud and hardware. Sequential reads without some delay fail. 103 | if self.ser.any(): 104 | rtn = self.ser.read()#read what it self 105 | time.sleep(self.communication_pause) # adjust per baud and hardware. Sequential reads without some delay fail. 106 | if rtn == None: 107 | print("TMC2209: Err in read") 108 | return "" 109 | # print("received "+str(len(rtn))+" bytes; "+str(len(rtn)*8)+" bits") 110 | return(rtn[7:11]) 111 | 112 | 113 | #----------------------------------------------------------------------- 114 | # this function tries to read the registry of the TMC 10 times 115 | # if a valid answer is returned, this function returns it as an integer 116 | #----------------------------------------------------------------------- 117 | def read_int(self, reg): 118 | tries = 0 119 | while(True): 120 | rtn = self.read_reg(reg) 121 | tries += 1 122 | if(len(rtn)>=4): 123 | break 124 | else: 125 | if tries <= 1: 126 | print("\nTMC2209: did not get the expected 4 data bytes. Instead got "+str(len(rtn))+" Bytes ...") 127 | if(tries>=10): 128 | print("TMC2209: after 10 tries not valid answer. exiting") 129 | print("TMC2209: is Stepper Powersupply switched on ?\n\n") 130 | return() # AF 131 | # raise SystemExit # AF 132 | val = struct.unpack(">i",rtn)[0] 133 | return(val) 134 | 135 | 136 | #----------------------------------------------------------------------- 137 | # this function can write a value to the register of the tmc 138 | # 1. use read_int to get the current setting of the TMC 139 | # 2. then modify the settings as wished 140 | # 3. write them back to the driver with this function 141 | #----------------------------------------------------------------------- 142 | def write_reg(self, reg, val): 143 | 144 | self.wFrame[1] = self.mtr_id 145 | self.wFrame[2] = reg | 0x80; # set write bit 146 | self.wFrame[3] = 0xFF & (val>>24) 147 | self.wFrame[4] = 0xFF & (val>>16) 148 | self.wFrame[5] = 0xFF & (val>>8) 149 | self.wFrame[6] = 0xFF & val 150 | 151 | self.wFrame[7] = self.compute_crc8_atm(self.wFrame[:-1]) 152 | 153 | rtn = self.ser.write(bytes(self.wFrame)) 154 | if rtn != len(self.wFrame): 155 | print("TMC2209: Err in write {}".format(__), file=sys.stderr) 156 | return False 157 | time.sleep(self.communication_pause) 158 | 159 | return(True) 160 | 161 | 162 | #----------------------------------------------------------------------- 163 | # this function also writes a value to the register of the TMC 164 | # but it also checks if the writing process was successfully by checking 165 | # the InterfaceTransmissionCounter before and after writing 166 | #----------------------------------------------------------------------- 167 | def write_reg_check(self, reg, val): 168 | IFCNT1 = self.read_int(self.IFCNT) 169 | self.write_reg(reg, val) 170 | IFCNT2 = self.read_int(self.IFCNT) 171 | IFCNT2 = self.read_int(self.IFCNT) 172 | 173 | if(IFCNT1 >= IFCNT2): 174 | print("TMC2209: writing not successful!") 175 | print("reg:{} val:{}", reg, val) 176 | print("IFCNT:", IFCNT1, IFCNT2) 177 | return False 178 | else: 179 | return True 180 | 181 | 182 | 183 | #----------------------------------------------------------------------- 184 | # this sets a specific bit to 1 185 | #----------------------------------------------------------------------- 186 | def set_bit(self, value, bit): 187 | return value | (bit) 188 | 189 | 190 | #----------------------------------------------------------------------- 191 | # this sets a specific bit to 0 192 | #----------------------------------------------------------------------- 193 | def clear_bit(self, value, bit): 194 | return value & ~(bit) 195 | 196 | 197 | #----------------------------------------------------------------------- 198 | # check the UART functionality by reading the IOIN register (0x06) 199 | #----------------------------------------------------------------------- 200 | def test(self): 201 | rtn = self.read_reg(0x06) # register 0x06 inquires the IOIN pin status 202 | if len(rtn) >= 4: 203 | return True 204 | else: 205 | return False -------------------------------------------------------------------------------- /src/board_V2/example.py: -------------------------------------------------------------------------------- 1 | """ 2 | Andrea Favero 16/05/2025 3 | 4 | Micropython code for Raspberry Pi Pico (RP2040 and RP2350) 5 | It demonstrates how to use StallGuard function from TMC2209 stepper driver. 6 | The RP2040 (or RP2350) use PIO to generate the stepper steps 7 | 8 | 9 | 10 | 11 | MIT License 12 | 13 | Copyright (c) 2025 Andrea Favero 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy 16 | of this software and associated documentation files (the "Software"), to deal 17 | in the Software without restriction, including without limitation the rights 18 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 19 | copies of the Software, and to permit persons to whom the Software is 20 | furnished to do so, subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included in all 23 | copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 31 | SOFTWARE. 32 | """ 33 | 34 | from machine import Pin 35 | import time, os 36 | 37 | 38 | def import_led(): 39 | """ 40 | The rgb_led is used to visually feedback once the code is started. 41 | The led flashes for quite some time, allowing to connect to the RP2040 via an IDE (i.e. Thonny). 42 | During this time is possible to interrupt (CTRL + C) the code from further imports. 43 | """ 44 | print("waiting time to eventually stop the code before further imports ...") 45 | from rgb_led import rgb_led 46 | ret = False 47 | ret = rgb_led.heart_beat(n=10, delay=1) 48 | while not ret: 49 | time. sleep(0.1) 50 | 51 | 52 | def import_stepper(): 53 | """ 54 | Stepper module is imported via a function for a delayed import. 55 | This gives time to eventually stop the code before further imports. 56 | """ 57 | from stepper import Stepper 58 | return Stepper 59 | 60 | 61 | def pin_handler(pin): 62 | """ 63 | Short and fast function to capture the interrupt based on GPIO (push button). 64 | This function is also used to debounce the push button. 65 | """ 66 | global centering, homing_requested 67 | 68 | time.sleep_ms(debounce_time_ms) # wait for debounce time 69 | if pin.value() == 0: # check if GPIO pin is still LOW after debounce period 70 | if not centering: # case a motor centering process is not in place 71 | homing_requested = True # set a flag instead of calling directly a function 72 | 73 | 74 | 75 | def _centering(pin, stepper_frequencies): 76 | """ 77 | Internal helper to call the centering method at the stepper.py Class. 78 | This function updates Global variables. 79 | """ 80 | global last_idx, centering 81 | 82 | centering = True 83 | print("\n\n") 84 | print("#"*78) 85 | print("#"*12, " Stepper centering via SENSORLESS homing function ", "#"*12) 86 | print("#"*78) 87 | 88 | idx = 0 if last_idx == 1 else 1 # flag 0 and 1 (alternates every time this function is called) 89 | last_idx = idx # flag tracking the last idx value 90 | 91 | # call to the stepper centering method. Note: The stepper frequency alternates each time between 92 | # the two vaues set in stepper_frequencies, therefore testing the extreme speed cases 93 | ret = stepper.centering(stepper_frequencies[idx]) 94 | 95 | if ret: 96 | print("\nStepper is centered\n\n") 97 | else: 98 | print("\nFailed to center the stepper\n\n") 99 | 100 | centering = False 101 | 102 | 103 | 104 | def stop_code(): 105 | if 'stepper' in locals(): # case stepper has been imported 106 | stepper.stop_stepper() # stepper gets stopped (PIO steps generation) 107 | stepper.deactivate_pio() # PIO's get deactivated 108 | if 'enable_pin' in locals(): # case enable_pin has been defined 109 | enable_pin.value(1) # pin is set high (disable TMC2209 current to stepper) 110 | if 'homing_pin' in locals(): # case homing_pin has been defined 111 | homing_pin.irq(handler=None) # disable IRQ 112 | print("\nClosing the program ...") # feedback is printed to the terminal 113 | 114 | 115 | 116 | ################################################################################################ 117 | ################################################################################################ 118 | ################################################################################################ 119 | 120 | # variables setting 121 | 122 | last_idx = 1 # flag used to alternate between the 2 stepper frequencies 123 | stepper_frequencies = (400, 1200) # stepper speeds (Hz) alternatively used for the centering demo 124 | # The stepper_frequencies values could be changed based on your need 125 | # Note: values outside the range 400 ~ 1200Hz will be clamped to these values by stepper.py 126 | 127 | debounce_time_ms = 10 # minimum time (ms) for push button debounce 128 | homing_requested = False # flag used by the IRQ and main function to start the centering 129 | centering = False # flag tracking if centering process (not in action / in action) 130 | 131 | debug = True # if True some informative prints will be made on the Shell 132 | 133 | 134 | 135 | 136 | try: 137 | 138 | rgb_led = import_led() # led module for visual feedback 139 | Stepper = import_stepper() # stepper module 140 | board_info = os.uname() # determining wich board_type is used 141 | 142 | # assigning max PIO frequency, depending on the board type 143 | if '2040' in board_info.machine: 144 | if 'W' in board_info.machine: 145 | board_type = 'RP2040 W' 146 | else: 147 | board_type = 'RP2040' 148 | max_pio_frequency = 125_000_000 149 | 150 | elif '2350' in board_info.machine.lower(): 151 | if 'W' in board_info.machine: 152 | board_type = 'RP2350 W' 153 | else: 154 | board_type = 'RP2350' 155 | max_pio_frequency = 150_000_000 156 | else: 157 | board_type = '???' 158 | max_pio_frequency = 125_000_000 159 | 160 | 161 | 162 | # GPIO pin to enable the motor 163 | # Note: Alternatively, the TMC2209 EN pin must be wired to GND 164 | enable_pin = Pin(2, Pin.IN, Pin.PULL_UP) 165 | enable_pin.value(0) # pin is set low (TMC2209 enabled, stepper always energized) 166 | 167 | # GPIO pin used to start each run of the sensorless homing demo 168 | homing_pin = Pin(9, Pin.IN, Pin.PULL_UP) 169 | 170 | # interrupt for the push button GPIO pin used to start the sensorless homing 171 | homing_pin.irq(trigger=Pin.IRQ_FALLING, handler=pin_handler) 172 | 173 | # stepper Class instantiatiation 174 | stepper = Stepper(max_frequency=max_pio_frequency, frequency=5_000_000, debug=debug) 175 | 176 | 177 | print("\nCode running in {} board".format(board_type)) 178 | print("Sensorless homing example") 179 | print("\nPress the GPIO homing_pin for SENSORLESS homing demo") 180 | 181 | 182 | # iterative part of the mainn function 183 | while True: # infinite loop 184 | if homing_requested: # case the homing_requested flag is True 185 | homing_requested = False # reset the homing_requested flag 186 | _centering(homing_pin, stepper_frequencies) # call the centering function 187 | time.sleep(0.1) 188 | 189 | 190 | 191 | except KeyboardInterrupt: # keyboard interrupts 192 | print("\nCtrl+C detected!") # feedback is printed to the terminal 193 | 194 | except Exception as e: # error 195 | print(f"\nAn error occured: {e}") # feedback is printed to the terminal 196 | 197 | finally: # closing the try loop 198 | stop_code() # stop_code function to stop PIOs 199 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stepper motor SENSORLESS homing & centering. 2 | This sensorless feature is based on the StallGuard function of Trinamic TMC2209's stepper driver.
3 | 4 | The TMC2209 integrates a UART interface, to which the RP2040 is connected.
5 | Via the UART, the StallGuard settings are applied, and its real time value can be accessed.
6 | When the torque on the motor increases, the StallGuard value decreases. This value can then be compared to an expected threshold.
7 | Additionally, the TMC2209's StallGuard controls the DIAG pin: the RP2040 uses an input interrupt to detect the rising edge of the DIAG level.
8 | The sensorless homing accuracy has been tested, and it's just great; details are provided below.
9 | 10 | This MicroPython code is designed for RP2040 or RP2350 microcontrollers, as it leverages the PIO features. Boards using these chips include the Raspberry Pi Pico, Pico 2, RP2040-Zero, RP2350-Zero, and many others.
11 | 12 | This is part of a larger project, involving stepper motors and RP2040 microcontrollers, yet this specific part could be useful for other makers.
13 | 14 |


15 | 16 | 17 | ## Showcasing video: 18 | Showcase objective: Stepper motor stops at the midpoint between two constraints (hard stops, aka homes).
19 | 20 | In the video: 21 | - The stepper motor reverses direction 2 times, if it detects high torque within a predefined range (5 complete revolutions). 22 | - RP2040 counts the steps in between the two constraints (home positions). 23 | - Steps generation and steps counting are based on PIO. For more details, see [https://github.com/AndreaFavero71/pio_stepper_control](https://github.com/AndreaFavero71/pio_stepper_control). 24 | - After the second direction change, the stepper stops at the midpoint between the two constraints. 25 | - Homing speed can be adjusted. Note: StallGuard is not accurate below 400 Hz. 26 | 27 | 28 | https://youtu.be/Dh-xW871_UM 29 | [![Watch the Demo](https://i.ytimg.com/vi/Dh-xW871_UM/maxresdefault.jpg)](https://youtu.be/Dh-xW871_UM) 30 |


31 | 32 | ## Collaboration with Lewis: 33 | Lewis (DIY Machines) and I collaborated on this topic to make it easier to reproduce this functionality.
34 | Our shared goal is to help others get started and make this technique more well-known and usable in other projects.
35 | 36 | This demo uses Lewis’s V2 board (modified as V3) and 3D-printed fixture, along with the latest code release:

37 | https://youtu.be/fMuNHKNTSt8 38 | [![Watch the Demo](https://i.ytimg.com/vi/fMuNHKNTSt8/maxresdefault.jpg)](https://youtu.be/fMuNHKNTSt8) 39 | 40 |
41 | 42 | #### Showcase test setup: 43 | - 1 NEMA 17 stepper motor. 44 | - 1 RP2040-Zero board. 45 | - 1 Trinamic TMC 2209 driver. 46 | - The stepper is 200 pulses/rev, set to 1/8 microstepping = 1600 pulses/rev. 47 | - The stepper is controlled by the RP2040-Zero board, running MicroPython. 48 | - The range in between the hard-stops (homes) is varied along the video. 49 | - Each time the push button is pressed: 50 | - a new homing & centering cycle starts. 51 | - the stepper speed changes, for demo purpose, by alternating 400 Hz and 1200 Hz. 52 | - UART communication between RP2040 and TMC2209. 53 | - The RGB LED flashes red when SG (StallGuard) is triggered, and green when the stepper is centered (it flashes three times when stalling is detected via UART, once if via the DIAG pin). 54 | 55 |


56 | 57 | 58 | ## Repeatability test: 59 | Despite being a promising technology, already adopted in commercial 3D printers, there is little published data on its precision and repeatability.
60 | In the below setup, a 0.01mm dial gauge measures the stepper arm’s stopping position, which is controlled by a predefined step count after **sensorless homing**.
61 | Imprecise homing would immediately reflect on the dial gauge, as seen at 0:28 in the video ([link](https://youtu.be/ilci2rO6KwE?t=28)), where a spacer was added to alter the homing position.
62 | The key metric is repeatability (effectively precision for most stepper motor applications).
63 | Test result: **3σ repeatability of ±0.01 mm**, better than a single microstep, despite the slight mechanical flex of the 3D-printed setup. 64 | 65 | https://youtu.be/ilci2rO6KwE 66 | [![Watch the Demo](https://i.ytimg.com/vi/ilci2rO6KwE/maxresdefault.jpg)](https://youtu.be/ilci2rO6KwE) 67 | 68 |
69 | 70 | ### StallGuard Depends on Speed as Well as Torque: 71 | The StallGuard value varies with speed: The chart below shows StallGuard values experimentally collected in my setup, with the motor running unloaded.
72 | When the stepper speed varies within a limited frequency range, the SG variation is relatively (and usefully) linear.
73 | In the code, the expected minimum SG is calculated using: `min_expected_SG = 0.15 * speed # speed is stepper frequency in Hz`
74 | 75 | Sensorless homing stops the stepper when the first of these two conditions is true: 76 | - SG value, retrieved from UART, falling below 80% of the expected minimum (parameter `k` in `stepper.py` file).
77 | - DIAG pin raising, when SG falling below the 45% of the expected minimum (parameter `k2` in latest `stepper.py` file).
78 | 79 | This method works well from 400Hz to 1200Hz (up to 2000Hz with latest code).
80 | **Note:** StallGuard is ignored for the first 100ms of every motor's startup; This saves quite a bit of trouble :smile:
81 | 82 | ![chart image](/images/sg_chart2.PNG) 83 | 84 |

85 | 86 | 87 | ## Connections: 88 | Wiring diagram kindly shared by Lewis (DIY Machines), for the **V3 board**.
89 | One of the key differences from the V2 board is the GPIO 11 connection to the TMC2209 DIAG pin.
90 | Compare with [board_V2 wiring diagram](./images/connections_V2.jpg) if you plan to upgrade your V2 board.
91 | ![connections_image](/images/connections_V3.jpg) 92 |

93 | 94 | 95 | ## Installation: 96 | The easiest setup is to: 97 | - Watch Lewis's tutorial https://youtu.be/TEkM0uLlkHU 98 | - Use a board from DIY Machines and 3D-print the fixture designed by Lewis.
99 | 100 | Necessary steps are: 101 | 1. Set the TMC2209 Vref according to the driver's datasheet and your stepper motor. 102 | 2. Flash your board with Micropython v1.24.1 or later version. If using the RP2040-Zero, refer to V1.24.1 from this link https://micropython.org/download/PIMORONI_TINY2040/ 103 | 3. Copy all the files from [board_V3 folder](https://github.com/AndreaFavero71/stepper_sensorless_homing/tree/main/src/board_V3) into the root folder in your RP2040-Zero; In case you have a V2 board, you can either upgrade it to V3 (connect GPIO 11 to DIAG), or download the files from [board_V2 folder](https://github.com/AndreaFavero71/stepper_sensorless_homing/tree/main/src/board_V2). 104 | 4. The example code `example.py` gets automatically started by `main.py`. To prevent the auto-start, keep GPIO 0 shorted to GND while powering the board. 105 | 5. Press the button connected to GPIO 9 to start the homing process. 106 | Every time the button is pressed, the stepper motor speed is alternated between the two frequencies values set at `stepper_frequencies = (400, 1200)` in `example.py` file; If you prefer testing with a single speed, write the same value on both the tuple values. 107 | 6. Adjust the k parameter in stepper.py to increase/decrease StallGuard sensitivity (UART), as well as K2 parameters (acting on DIAG pin). 108 | 7. In my setup, I could vary the Vref between 1.0V and 1.4V and reliably getting the homing at 400Hz and 1200Hz, without changing the code. 109 |

110 | 111 | 112 | With the latest code release: 113 | - Sensorless homing uses both the SG values readings from the UART, as well as the DIAG pin signal level from the TMC2209 driver.
114 | - The sensorless homing detection is extended up to 2000Hz. 115 | - The sensorless functionality is extended to all the microstepping settings (from 1/8 to 1/64).
116 | You can configure microstepping in `stepper.py` using `(ms = self.micro_step(0) # ms --> 0=1/8, 1=1/16, 2=1/32, 3=1/64)`. 117 | - The stepper motor moves shortly backward at the beginning, to ensure enough 'room' while searching for the first hard-stop. 118 | 119 |


120 | 121 | 122 | ## Notes: 123 | The built-in RGB LED flashes in different colors to indicate various activities/results.
124 | This code uses the onboard RGB LED of the RP2040-Zero or RP2350-Zero, which is not available on the Pico, Pico W, or Pico 2.
125 | 126 | Please note that the TMC2209 files are subject to license restrictions.
127 | You’re free to use and modify the code, provided you follow the license terms. I welcome any feedback or suggestions for improvement. 128 | 129 | 130 | **Note:** Use this code at your own risk :smile: 131 | 132 |

133 | 134 | 135 | ## Acknowledgements: 136 | Many thanks to: 137 | - Lewis (DIY Machines), for the nice collaboration and his detailed [video tutorial](https://youtu.be/TEkM0uLlkHU) on this topic. 138 | - Daniel Frenkel and his ebook on Trinamic drivers, book available in Kindle format at [Amazon](https://www.amazon.com/stores/Daniel-Frenkel/author/B0BNZG6FPD?ref=ap_rdr&isDramIntegrated=true&shoppingPortalEnabled=true). 139 | - Chr157i4n for making the extensive TMC_2209 library. 140 | - anonymousaga for his adaptation of the driver for Raspberry Pi Pico. 141 | 142 | 143 | Original files I've modified for this demo: TMC_2209_StepperDriver.py and TMC_2209_uart.py
144 | Original source: https://github.com/troxel/TMC_UART
145 | Original source: https://github.com/Chr157i4n/TMC2209_Raspberry_Pi
146 | Original source: https://github.com/kjk25/TMC2209_ESP32
147 | Original source: https://github.com/anonymousaga/TMC2209_RPI_PICO
148 | -------------------------------------------------------------------------------- /src/board_V3/example.py: -------------------------------------------------------------------------------- 1 | """ 2 | Andrea Favero 16/05/2025 (rev 25/06/2025) 3 | 4 | Micropython code for Raspberry Pi Pico (RP2040 and RP2350) 5 | It demonstrates how to use StallGuard function from TMC2209 stepper driver. 6 | The RP2040 (or RP2350) use PIO to generate the stepper steps 7 | 8 | 9 | 10 | 11 | MIT License 12 | 13 | Copyright (c) 2025 Andrea Favero 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy 16 | of this software and associated documentation files (the "Software"), to deal 17 | in the Software without restriction, including without limitation the rights 18 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 19 | copies of the Software, and to permit persons to whom the Software is 20 | furnished to do so, subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included in all 23 | copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 31 | SOFTWARE. 32 | """ 33 | 34 | from machine import Pin 35 | import time, os 36 | 37 | 38 | def import_led(): 39 | """ 40 | The rgb_led is used to visually feedback once the code is started. 41 | The led flashes for quite some time, allowing to connect to the RP2040 via an IDE (i.e. Thonny). 42 | During this time is possible to interrupt (CTRL + C) the code from further imports. 43 | """ 44 | print("waiting time to eventually stop the code before further imports ...") 45 | from rgb_led import rgb_led 46 | ret = False 47 | ret = rgb_led.heart_beat(n=10, delay=1) 48 | while not ret: 49 | time. sleep(0.1) 50 | 51 | 52 | def import_stepper(): 53 | """ 54 | Stepper module is imported via a function for a delayed import. 55 | This gives time to eventually stop the code before further imports. 56 | """ 57 | from stepper import Stepper 58 | return Stepper 59 | 60 | 61 | def pin_handler(pin): 62 | """ 63 | Short and fast function to capture the interrupt based on GPIO (push button). 64 | This function is also used to debounce the push button. 65 | """ 66 | global centering, homing_requested 67 | 68 | time.sleep_ms(debounce_time_ms) # wait for debounce time 69 | if pin.value() == 0: # check if GPIO pin is still LOW after debounce period 70 | if not centering: # case a motor centering process is not in place 71 | homing_requested = True # set a flag instead of calling directly a function 72 | 73 | 74 | 75 | def _centering(pin, stepper_frequencies): 76 | """ 77 | Internal helper to call the centering method at the stepper.py Class. 78 | This function updates Global variables. 79 | """ 80 | global last_idx, centering 81 | 82 | centering = True 83 | print("\n\n") 84 | print("#"*78) 85 | print("#"*12, " Stepper centering via SENSORLESS homing function ", "#"*12) 86 | print("#"*78) 87 | 88 | idx = 0 if last_idx == 1 else 1 # flag 0 and 1 (alternates every time this function is called) 89 | last_idx = idx # flag tracking the last idx value 90 | 91 | # call to the stepper centering method. Note: The stepper frequency alternates each time between 92 | # the two vaues set in stepper_frequencies, therefore testing the extreme speed cases 93 | ret = stepper.centering(stepper_frequencies[idx]) 94 | 95 | if ret: 96 | print("\nStepper is centered\n\n") 97 | else: 98 | print("\nFailed to center the stepper\n\n") 99 | 100 | centering = False 101 | 102 | 103 | 104 | def stop_code(): 105 | if 'stepper' in locals(): # case stepper has been imported 106 | stepper.stop_stepper() # stepper gets stopped (PIO steps generation) 107 | stepper.deactivate_pio() # PIO's get deactivated 108 | if 'enable_pin' in locals(): # case enable_pin has been defined 109 | enable_pin.value(1) # pin is set high (disable TMC2209 current to stepper) 110 | if 'homing_pin' in locals(): # case homing_pin has been defined 111 | homing_pin.irq(handler=None) # disable IRQ 112 | print("\nClosing the program ...") # feedback is printed to the terminal 113 | 114 | 115 | 116 | ################################################################################################ 117 | ################################################################################################ 118 | ################################################################################################ 119 | 120 | # variables setting 121 | 122 | last_idx = 1 # flag used to alternate between the 2 stepper frequencies 123 | stepper_frequencies = (400, 2000) # stepper speeds (Hz) alternatively used for the centering demo 124 | # The stepper_frequencies values for homing could be changed based on your need 125 | # Note1: values within the range 400 ~ 1200Hz respond well to the SENSORLESS HOMING. 126 | # Note2: values outside the range 400 ~ 2000Hz will be clamped to these values by stepper.py 127 | 128 | debounce_time_ms = 10 # minimum time (ms) for push button debounce 129 | homing_requested = False # flag used by the IRQ and main function to start the centering 130 | centering = False # flag tracking the centering process (not in action / in action) 131 | 132 | debug = True # if True some informative prints will be made on the Shell 133 | 134 | 135 | 136 | 137 | try: 138 | 139 | rgb_led = import_led() # led module for visual feedback 140 | Stepper = import_stepper() # stepper module 141 | board_info = os.uname() # determining wich board_type is used 142 | 143 | # assigning max PIO frequency, depending on the board type 144 | if '2040' in board_info.machine: 145 | if 'W' in board_info.machine: 146 | board_type = 'RP2040 W' 147 | else: 148 | board_type = 'RP2040' 149 | max_pio_frequency = 125_000_000 150 | 151 | elif '2350' in board_info.machine.lower(): 152 | if 'W' in board_info.machine: 153 | board_type = 'RP2350 W' 154 | else: 155 | board_type = 'RP2350' 156 | max_pio_frequency = 150_000_000 157 | else: 158 | board_type = '???' 159 | max_pio_frequency = 125_000_000 160 | 161 | 162 | 163 | # GPIO pin to enable the motor 164 | # Note: Alternatively, the TMC2209 EN pin must be wired to GND 165 | enable_pin = Pin(2, Pin.IN, Pin.PULL_UP) 166 | enable_pin.value(0) # pin is set low (TMC2209 enabled, stepper always energized) 167 | 168 | # GPIO pin used to start each run of the sensorless homing demo 169 | homing_pin = Pin(9, Pin.IN, Pin.PULL_UP) 170 | 171 | # interrupt for the push button GPIO pin used to start the sensorless homing 172 | homing_pin.irq(trigger=Pin.IRQ_FALLING, handler=pin_handler) 173 | 174 | # stepper Class instantiatiation 175 | stepper = Stepper(max_frequency=max_pio_frequency, frequency=5_000_000, debug=debug) 176 | 177 | # case the TMC driver UART reacts properly (it is powered and properly wired) 178 | if stepper.tmc_test(): 179 | 180 | # board tupe and other info are printed to the terminal 181 | print("\nCode running in {} board".format(board_type)) 182 | print("Sensorless homing example") 183 | print("\nPress the push button for SENSORLESS homing demo") 184 | 185 | 186 | # iterative part of the mainn function 187 | while True: # infinite loop 188 | if homing_requested: # case the homing_requested flag is True 189 | homing_requested = False # reset the homing_requested flag 190 | _centering(homing_pin, stepper_frequencies) # call the centering function 191 | time.sleep(0.1) # small sleeping while waiting for the 'homing request' 192 | 193 | 194 | # ################################################################################################### # 195 | # Here you'd put the application code, to be executed after the SENSORLESS HOMING # 196 | # # 197 | # Recall to set the StallGuard to a proper value for the application. # 198 | # If the application do not require Stall control, set the StallGuard threshol to 0 (max torque). # 199 | # # 200 | # Example of no Stall control: # 201 | # stepper.set_stallguard(threshold = 0) # set SG threshold acting on the DIAG pin, to max torque # 202 | # # 203 | # For a proper Stall control, first set the driver current and the stepper speed for your usage case. # 204 | # Afterward, check the StallGuard value with and without load. # 205 | # Set the StallGuard threshold to <= 0.5 * measured SG_value in normal application. # 206 | # # 207 | # Example of no Stall control: # 208 | # sg_threshold = 0.45 * minimum_measure_SG_value_in_application # 209 | # stepper.set_stallguard(threshold = sg_threshold) # set SG threshold acting on the DIAG pin # 210 | # ################################################################################################### # 211 | 212 | 213 | # case the TMC driver UART does not react (not powered or not wired properly) 214 | else: 215 | 216 | # info is printed to the terminal 217 | print("\n"*2) 218 | print("#"*67) 219 | print("#"*67) 220 | print("#", " "*63, "#",) 221 | print("# The TMC driver UART does not react: IS THE DRIVER POWERED ? #") 222 | print("#", " "*63, "#",) 223 | print("#"*67) 224 | print("#"*67) 225 | print("\n"*2) 226 | 227 | 228 | except KeyboardInterrupt: # keyboard interrupts 229 | print("\nCtrl+C detected!") # feedback is printed to the terminal 230 | 231 | except Exception as e: # error 232 | print(f"\nAn error occured: {e}") # feedback is printed to the terminal 233 | 234 | finally: # closing the try loop 235 | stop_code() # stop_code function to stop PIOs 236 | -------------------------------------------------------------------------------- /src/board_V2/stepper.py: -------------------------------------------------------------------------------- 1 | """ 2 | Andrea Favero 16/05/2025 3 | 4 | Micropython code for Raspberry Pi Pico (RP2040 and RP2350) 5 | It demonstrates how to use TMC2209 StallGuard function for stepper sensorless homing. 6 | PIO is used by the RP2040 (or RP2350) to generate the stepper steps frequency. 7 | 8 | 9 | 10 | 11 | MIT License 12 | 13 | Copyright (c) 2025 Andrea Favero 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy 16 | of this software and associated documentation files (the "Software"), to deal 17 | in the Software without restriction, including without limitation the rights 18 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 19 | copies of the Software, and to permit persons to whom the Software is 20 | furnished to do so, subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included in all 23 | copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 31 | SOFTWARE. 32 | """ 33 | 34 | from TMC_2209_driver import * 35 | from rgb_led import rgb_led 36 | from rp2 import PIO, StateMachine, asm_pio, asm_pio_encode 37 | from machine import Pin 38 | import time 39 | 40 | 41 | 42 | 43 | class Stepper: 44 | 45 | def __init__(self, max_frequency=125000000, frequency=5000000, debug=False): 46 | print("\nUploading stepper_controller ...") 47 | 48 | # debug bool for printout 49 | self.debug = debug 50 | 51 | # frequencies for the PIO state machines 52 | self.max_frequency = max_frequency 53 | self.frequency = frequency 54 | 55 | # (RP2040) GPIO pins 56 | self.STEPPER_STEPS_PIN = Pin(5, Pin.OUT) 57 | self.STEPPER_DIR = Pin(6, Pin.OUT) 58 | self.STEPPER_MS1 = Pin(3, Pin.OUT) 59 | self.STEPPER_MS2 = Pin(4, Pin.OUT) 60 | self.UART_RX_PIN = Pin(13) 61 | self.UART_TX_PIN = Pin(12) 62 | 63 | # stepper characteristics 64 | self.STEPPER_STEPS = 200 # number of (full) steps per revolution 65 | 66 | # PIO intrustions for steps generation (sm0) 67 | self.PIO_VAR = 2 # number of PIO commands repeated, when PIO stepper state machine is called 68 | self.PIO_FIX = 37 # fix number of PIO commands, when PIO stepper state machine is called 69 | 70 | # pre-encode some of the PIO instructions (>100 times faster) 71 | self.PULL_ENCODED = asm_pio_encode("pull()", 0) 72 | self.MOV_X_OSR_ENCODED = asm_pio_encode("mov(x, osr)", 0) 73 | self.PUSH_ENCODED = asm_pio_encode("push()", 0) 74 | self.MOV_ISR_X_ENCODED = asm_pio_encode("mov(isr, x)", 0) 75 | 76 | # state machine for stepper steps generation 77 | self.sm0 = StateMachine(0, self.steps_mot_pio, freq=self.frequency, set_base=self.STEPPER_STEPS_PIN) 78 | self.sm0.put(65535) # initial OSR to max value (minimum stepper speed) 79 | self.sm0.active(0) # state machine is kept deactivated 80 | 81 | # state machine for stopping the stepper 82 | self.sm1 = StateMachine(1, self.stop_stepper_pio, freq=self.max_frequency, in_base=self.STEPPER_STEPS_PIN) 83 | self.sm1.irq(self._stop_stepper_handler) 84 | self.sm1.active(0) # state machine is kept deactivated 85 | 86 | # state machine for stepper steps tracking 87 | self.sm2 = StateMachine(2, self.steps_counter_pio, freq=self.max_frequency, in_base=self.STEPPER_STEPS_PIN) 88 | self.set_pls_counter(0) # sets the initial value for stepper steps counting 89 | self.sm2.active(1) # starts the state machine for stepper steps counting 90 | 91 | # microsteppingmap --> setting: (descriptor, reduction, ms1, ms2) 92 | self.microstep_map = {0: ("1/8", 0.125, 0, 0), 93 | 1: ("1/16", 0.0625, 1, 1), 94 | 2: ("1/32", 0.03125, 1, 0), 95 | 3: ("1/64", 0.015625, 0, 1)} 96 | 97 | # microstep resolution configuration (internal pull-down resistors) 98 | ms = self.micro_step(0) # 0=1/8, 1=1/16, 2=1/32, 3=1/64 99 | 100 | # get the steps for a full revolution 101 | if ms is not None: 102 | self.full_rev = self.get_full_rev(ms) 103 | 104 | # instantiation of tmc2209 driver 105 | # args: pin_step, pin_dir, pin_en, rx_pin, tx_pin, mtr_id=0, baudrate=115200, serialport=0 106 | self.tmc = TMC_2209( rx_pin=self.UART_RX_PIN, tx_pin=self.UART_TX_PIN, mtr_id=0, serialport=0 ) 107 | 108 | # set StallGuard to max torque for the GPIO interrupt callback 109 | self.set_stallguard(threshold = 0) 110 | 111 | # flag for stepper spinning status (True when the motor spins) 112 | self.stepper_spinning = False 113 | 114 | # instance variable for the max number of stepper revolution to find home 115 | self.max_homing_revs = 5 116 | 117 | # instance variable for the max number of steps to be done while homing 118 | self.max_steps = self.max_homing_revs * self.full_rev 119 | 120 | # instance variable for the steps to be done 121 | self.steps_to_do = self.max_steps 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | # sm0, the PIO program for generating steps 133 | @asm_pio(set_init=PIO.OUT_LOW) 134 | def steps_mot_pio(): # Note: without 'self' in PIO callback function ! 135 | """ 136 | A frequency is generated at PIO: The frequency remains fix (it can activated and stopped). 137 | A Pin is associated to the Pin 138 | The speed can be varied by changing the off time (delay) between ON pulses 139 | This method allows for stepper speed variation, not really for precise positioning. 140 | """ 141 | label("main") # entry point from jmp 142 | pull(noblock) # get delay data from osr, and maintained in memory 143 | mov(x,osr) # OSR (Output Shift Register) content is moved to x, to prepare for future noblock pulls 144 | mov(y, x) # delay assigned to y scretch register 145 | set(pins, 1) [15] # set the pin high 16 times 146 | set(pins, 1) [15] # set the pin high 16 times 147 | label("delay") # entry point when x_dec>0 148 | set(pins, 0) # set the pin low 1 time 149 | jmp(y_dec, "delay") # y_dec-=1 if y_dec>0 else jump to "delay" 150 | jmp("main") # jump to "main" 151 | 152 | 153 | # sm1, the PIO program for stopping the sm0 (steps generator) 154 | @asm_pio() 155 | def stop_stepper_pio(): # Note: without 'self' in PIO callback function ! 156 | """ 157 | This PIO fiunction: 158 | - Reads the lowering edges from the output GPIO that generates the steps. 159 | - De-counts the 32bits OSR. 160 | - The starting value, to decount from, is sent by inline command. 161 | - Once the decount is zero it stops the steps generation (sm0). 162 | """ 163 | label("wait_for_step") 164 | wait(1, pin, 0) # wait for step signal to go HIGH 165 | wait(0, pin, 0) # wait for step signal to go LOW (lowering edge) 166 | jmp(x_dec, "wait_for_step") 167 | irq(block, rel(0)) # trigger an IRQ 168 | 169 | 170 | # sm2, the PIO program for counting steps 171 | @asm_pio() 172 | def steps_counter_pio(): # Note: without 'self' in PIO callback function ! 173 | """ 174 | This PIO fiunction: 175 | -Reads the rising edges from the output GPIO that generates the steps. 176 | -De-counts the 32bit OSR. 177 | -The starting value, to decount from, is sent by inline command. 178 | -The reading is pushed to the rx_fifo on request, via an inline command. 179 | """ 180 | label("loop") 181 | wait(0, pin, 0) # wait for step signal to go LOW 182 | wait(1, pin, 0) # wait for step signal to go HIGH (rising edge) 183 | jmp(x_dec, "loop") # decrement x and continue looping 184 | 185 | 186 | def set_pls_to_do(self, val): 187 | """Sets the initial value for the stpes to do, from which to decount from. 188 | Pre-encoded instructions are >140 times faster.""" 189 | self.sm1.put(val) # val value is passed to sm1 (StateMachine 1) 190 | self.sm1.exec(self.PULL_ENCODED) # execute pre-encoded 'pull()' instruction 191 | self.sm1.exec(self.MOV_X_OSR_ENCODED) # execute pre-encoded 'mov(x, osr)' instruction 192 | self.sm1.active(1) # sm1 is activated 193 | 194 | 195 | def set_pls_counter(self, val): 196 | """Sets the initial value for the stpes counter, from which to decount from. 197 | Pre-encoded instructions are >140 times faster.""" 198 | self.sm2.put(val) 199 | self.sm2.exec(self.PULL_ENCODED) # execute pre-encoded 'pull()' instruction 200 | self.sm2.exec(self.MOV_X_OSR_ENCODED) # execute pre-encoded 'mov(x, osr)' instruction 201 | 202 | 203 | def get_pls_count(self): 204 | """Gets the steps counter value. 205 | Pre-encoded instructions are >140 times faster.""" 206 | self.sm2.exec(self.MOV_ISR_X_ENCODED) # execute pre-encoded 'mov(isr, x)' instruction 207 | self.sm2.exec(self.PUSH_ENCODED) # execute pre-encoded 'push()' instruction 208 | if self.sm2.rx_fifo: # case there is data in the rx buffer 209 | return -self.sm2.get() & 0xffffffff # return the current sm2 counter value (32-bit unsigned integer) 210 | else: # case the rx buffer has no data 211 | return -1 # returning -1 is a clear exception for a positive counter ...) 212 | 213 | 214 | def set_steps_value(self, val): 215 | """Set the ref position (after homing).""" 216 | self.position = val 217 | 218 | 219 | def _is_tmc_powered(self): 220 | """Check if the TMC driver is energized, via the UART.""" 221 | rtn = self.read_stallguard() 222 | if isinstance(rtn, int): 223 | print("TMC_2209 driver is powered") 224 | return 1 225 | elif rtn == (): 226 | print("TMC_2209 driver is not powered (or UART not connected)") 227 | return 0 228 | 229 | 230 | def read_stallguard(self): 231 | """Gets the instant StallGuard value from the TMC driver, via the UART.""" 232 | return self.tmc.getStallguard_Result() 233 | 234 | 235 | def set_stallguard(self, threshold): 236 | """Sets the StallGuard threshold at TMC driver, via the UART.""" 237 | # clamp the SG threshold between 0 and 255 238 | threshold = max(0, min(threshold, 255)) 239 | 240 | # set the StallGuard threshold and the call-back handler function 241 | self.tmc.setStallguard_Callback(threshold = threshold, handler = self._stallguard_callback) 242 | if self.debug: 243 | if threshold != 0: 244 | print("\nSetting StallGuard (irq to GPIO) to value {}.".format(threshold)) 245 | else: 246 | print("\nSetting StallGuard (irq to GPIO) to 0, meaning max possible torque.") 247 | 248 | 249 | def get_stepper_frequency(self, pio_val): 250 | """Convert the PIO value (delay) to stepper frequency.""" 251 | if pio_val > 0: 252 | return int(self.frequency / (pio_val * self.PIO_VAR + self.PIO_FIX)) 253 | else: 254 | return None 255 | 256 | 257 | def get_stepper_value(self, stepper_freq): 258 | """Convert the stepper frequency to PIO value (delay).""" 259 | if stepper_freq > 0: 260 | return int((self.frequency - stepper_freq * self.PIO_FIX) / (stepper_freq * self.PIO_VAR)) 261 | else: 262 | return None 263 | 264 | 265 | def micro_step(self, ms): 266 | """Sets the GPIO for th decided microstepping (ms) setting.""" 267 | settings = self.microstep_map.get(ms) # retrieve the corresponding settings 268 | 269 | if settings: # case the ms setting exists 270 | ms_txt, k, ms1, ms2 = settings # ms map elements are assigned 271 | self.STEPPER_MS1.value(ms1) # STEPPER_MS1 output set to ms1 value 272 | self.STEPPER_MS2.value(ms2) # STEPPER_MS2 output set to ms2 value 273 | print(f"Microstep to {ms_txt}") # feedback is printed to the terminal 274 | return ms 275 | else: # case the ms setting does not exist 276 | print("Wrong parameter for micro_step") # feedback is printed to the terminal 277 | return None 278 | 279 | 280 | def get_full_rev(self, ms): 281 | """Calculates the steps for one fulle revolution of the stepper.""" 282 | settings = self.microstep_map.get(ms) # retrieve the corresponding settings 283 | 284 | if settings: # case the ms setting exists 285 | ms_txt, k, ms1, ms2 = settings # ms map elements are assigned 286 | full_rev = int(self.STEPPER_STEPS/k) # steps for a stepper full revolution 287 | print(f"Full revolution takes {full_rev} steps") # feedback is printed to the terminal 288 | return full_rev 289 | else: # case the ms setting does not exist 290 | print("Wrong parameter for full revolution") # feedback is printed to the terminal 291 | return None 292 | 293 | 294 | def _stop_stepper_handler(self, sm0): 295 | """Call-back function by a PIO interrupt""" 296 | self.sm0.active(0) # state machine for stepper-steps generation is deactivated 297 | self.stepper_spinning = False # flag tracking the stepper spinning is set False 298 | if self.steps_to_do < self.max_steps / 2: # case stepper stops within half way of max_steps for homing 299 | rgb_led.flash_color('green', bright=0.2, times=1, time_s=0.01) # flashing red led 300 | 301 | 302 | def _stallguard_callback(self, channel): 303 | """Call-back function from the StallGuard.""" 304 | print("\nStallGuard detections\n") 305 | self.sm0.active(0) # state machine for stepper-steps generation is deactivated 306 | self.stepper_spinning = False # flag tracking the stepper spinning is set False 307 | 308 | 309 | def stop_stepper(self): 310 | self.sm0.active(0) # state machine for stepper-steps generation is deactivated 311 | self.stepper_spinning = False # flag tracking the stepper spinning is set False 312 | 313 | 314 | def start_stepper(self): 315 | self.stepper_spinning = True # flag tracking the stepper spinning is set True 316 | self.sm0.active(1) # state machine for stepper-steps generation is activated 317 | 318 | 319 | def deactivate_pio(self): 320 | """Function to deactivate PIO.""" 321 | self.sm0.active(0) # sm0 is deactivated 322 | self.sm1.active(0) # sm1 is deactivated 323 | self.sm2.active(0) # sm2 is deactivated 324 | PIO(0).remove_program() # reset SM0 block 325 | PIO(1).remove_program() # reset SM1 block 326 | print("State Machines deactivated") 327 | 328 | 329 | def _homing(self, h_speed, stepper_freq): 330 | """ 331 | Spins the stepper at stepper_freq until StillGuard value below threshold. 332 | Argument h_speed is the PIO value to get stepper_freq at the GPIO pin. 333 | """ 334 | self.stop_stepper() # stepper is stopped (in the case it wasn't) 335 | self.set_pls_counter(0) # sets the initial value for stepper steps counting 336 | self.set_pls_to_do(self.max_steps) # (max) number of steps to find home 337 | self.sm0.put(h_speed) # stepper speed 338 | self.start_stepper() # stepper is started 339 | 340 | min_sg_expected = int(0.15 * stepper_freq) # expected minimum StallGuard value on free spinning 341 | k = 0.8 # reduction coeficient (it should be 0.7 ~ 0.8, tune it on your need) 342 | sg_threshold = int(k * min_sg_expected) # StallGuard threshold 343 | max_homing_ms = int(self.max_homing_revs * 1000 * self.full_rev / stepper_freq) # timeout in ms 344 | 345 | if self.debug: # case self.debug is set True 346 | sg_list = [] # list for debug purpose 347 | print(f"Homing with stepper speed of {stepper_freq}Hz and StallGuard threshold of {sg_threshold}") 348 | 349 | t_ref = time.ticks_ms() # time reference 350 | i = 0 # iterator index 351 | while time.ticks_ms() - t_ref < max_homing_ms: # while loop until timeout (unless SG detection) 352 | sg = self.tmc.getStallguard_Result() # StallGuard value is retrieved 353 | if self.debug: # case self.debug is set True 354 | sg_list.append(sg) # StallGuard value is appended to the list 355 | i+=1 # iterator index is increased 356 | if i > 10: # case at least 10 SG readings (avoids startup effect to SG) 357 | if sg < sg_threshold: # case StallGuard value lower than threshold 358 | self.stop_stepper() # stepper is stopped (if not done by the _homing func) 359 | rgb_led.flash_color('red', bright=0.3, times=1, time_s=0.01) # flashing red led 360 | if self.debug: # case self.debug is True 361 | print(f"Homing reached. Last SG values: {sg_list}.", \ 362 | f"Total of {i} iterations in {time.ticks_ms()-t_ref} ms\n") 363 | return True # return True 364 | else: # case StallGuard value higher than threshold 365 | if self.debug: # case self.debug is set True 366 | del sg_list[0] # first element in list is deleted 367 | else: # case self.debug is set False 368 | pass # do nothing 369 | 370 | self.stop_stepper() # stepper is stopped (if not done by the _homing func) 371 | print("Failed homing") # feedback is printed to the Terminal 372 | return False # False is returned when homing fails 373 | 374 | 375 | 376 | def centering(self, stepper_freq): 377 | """ 378 | SENSORLESS homing, on both directions, to stop the stepper in the middle. 379 | Argument is the stepper speed, in Hz. 380 | Clockwise (CW) and counter-clockwise (CCW) also depends on motor wiring. 381 | """ 382 | 383 | stepper_freq = max(400, min(1200, stepper_freq)) # homing speed (frequency) clamped in range 400 ~ 1200 Hz 384 | stepper_val = self.get_stepper_value(stepper_freq) # stepper pio value is calculated 385 | 386 | self.STEPPER_DIR.value(1) # set stepper direction to 1 (CW) 387 | if self._homing(stepper_val, stepper_freq): # call the _homing function at CW 388 | self.STEPPER_DIR.value(0) # set stepper direction to 0 (CCW) 389 | if self._homing(stepper_val, stepper_freq): # call the _homing function at CW 390 | steps_range = self.get_pls_count() # retrieves steps done on previous activation run 391 | half_range = int(steps_range/2) # half range 392 | self.STEPPER_DIR.value(1) # set stepper direction to 1 (CW) 393 | self.steps_to_do = half_range # steps to be done assigned to instance variable 394 | self.set_pls_to_do(self.steps_to_do) # number of steps to do now is half range 395 | self.sm0.put(stepper_val) # stepper speed 396 | self.start_stepper() # stepper is started 397 | if self.debug: # case self.debug is set True 398 | print(f"Counted {steps_range} steps in between the 2 homes") 399 | print(f"Positioning the stepper at {half_range} from the last detected home") 400 | return True # True is returned when centering succeeds 401 | 402 | else: # case the homing fails 403 | self.steps_to_do = self.max_steps # max number of homing steps is assigned to the steps to be done next 404 | self.stop_stepper() # stepper is stopped (in the case it wasn't) 405 | return False # False is returned when centering fails 406 | 407 | 408 | 409 | 410 | 411 | -------------------------------------------------------------------------------- /src/board_V3/stepper.py: -------------------------------------------------------------------------------- 1 | """ 2 | Andrea Favero 16/05/2025 (rev 25/06/2025) 3 | 4 | Micropython code for Raspberry Pi Pico (RP2040 and RP2350) 5 | It demonstrates how to use TMC2209 StallGuard function for stepper sensorless homing. 6 | PIO is used by the RP2040 (or RP2350) to generate the stepper steps frequency. 7 | 8 | 9 | 10 | 11 | MIT License 12 | 13 | Copyright (c) 2025 Andrea Favero 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy 16 | of this software and associated documentation files (the "Software"), to deal 17 | in the Software without restriction, including without limitation the rights 18 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 19 | copies of the Software, and to permit persons to whom the Software is 20 | furnished to do so, subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included in all 23 | copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 31 | SOFTWARE. 32 | """ 33 | 34 | 35 | from TMC_2209_driver import * 36 | from rgb_led import rgb_led 37 | from rp2 import PIO, StateMachine, asm_pio, asm_pio_encode 38 | from machine import Pin 39 | import time 40 | 41 | 42 | class Stepper: 43 | 44 | def __init__(self, max_frequency=125000000, frequency=5000000, debug=False): 45 | print("\nUploading stepper_controller ...") 46 | 47 | # debug bool for printout 48 | self.debug = debug 49 | 50 | # frequencies for the PIO state machines 51 | self.max_frequency = max_frequency 52 | self.frequency = frequency 53 | 54 | # (RP2040) GPIO pins 55 | self.STEPPER_STEPS_PIN = Pin(5, Pin.OUT) 56 | self.STEPPER_DIR = Pin(6, Pin.OUT) 57 | self.STEPPER_MS1 = Pin(3, Pin.OUT) 58 | self.STEPPER_MS2 = Pin(4, Pin.OUT) 59 | self.UART_RX_PIN = Pin(13) 60 | self.UART_TX_PIN = Pin(12) 61 | 62 | # stepper characteristics 63 | self.STEPPER_STEPS = 200 # number of (full) steps per revolution 64 | 65 | # PIO intructions for steps generation (sm0) 66 | self.PIO_VAR = 2 # number of PIO commands repeated, when PIO stepper state machine is called 67 | self.PIO_FIX = 37 # fix number of PIO commands, when PIO stepper state machine is called 68 | 69 | # pre-encoded some of the PIO instructions (>100 times faster) 70 | self.PULL_ENCODED = asm_pio_encode("pull()", 0) 71 | self.MOV_X_OSR_ENCODED = asm_pio_encode("mov(x, osr)", 0) 72 | self.PUSH_ENCODED = asm_pio_encode("push()", 0) 73 | self.MOV_ISR_X_ENCODED = asm_pio_encode("mov(isr, x)", 0) 74 | 75 | # state machine for stepper steps generation 76 | self.sm0 = StateMachine(0, self.steps_mot_pio, freq=self.frequency, set_base=self.STEPPER_STEPS_PIN) 77 | self.sm0.put(65535) # initial OSR to max value (minimum stepper speed) 78 | self.sm0.active(0) # state machine is kept deactivated 79 | 80 | # state machine for stopping the stepper 81 | self.sm1 = StateMachine(1, self.stop_stepper_pio, freq=self.max_frequency, in_base=self.STEPPER_STEPS_PIN) 82 | self.sm1.irq(self._stop_stepper_handler) 83 | self.sm1.active(0) # state machine is kept deactivated 84 | 85 | # state machine for stepper steps tracking 86 | self.sm2 = StateMachine(2, self.steps_counter_pio, freq=self.max_frequency, in_base=self.STEPPER_STEPS_PIN) 87 | self.set_pls_counter(0) # sets the initial value for stepper steps counting 88 | self.sm2.active(1) # starts the state machine for stepper steps counting 89 | 90 | # microsteppingmap --> setting: (descriptor, reduction, ms1, ms2, SG_adjustment, serialport_nodeaddress) 91 | self.microstep_map = {0: ("1/8", 0.125, 0, 0, 1, 0), 92 | 1: ("1/16", 0.0625, 1, 1, 2, 3), 93 | 2: ("1/32", 0.03125, 1, 0, 4, 1), 94 | 3: ("1/64", 0.015625, 0, 1, 8, 2)} 95 | 96 | # microstep resolution configuration (internal pull-down resistors) 97 | ms = self.micro_step(0) # ms --> 0=1/8, 1=1/16, 2=1/32, 3=1/64 98 | 99 | # if the microstepping is set correctly 100 | # get the steps for a full revolution, the StallGuard SG_adjustment (based on microstepping), and the serialport_nodeaddress 101 | if ms is not None: 102 | self.full_rev, self.SG_adj, sp_na = self.get_full_rev(ms) 103 | 104 | # instantiation of tmc2209 driver 105 | # args: pin_step, pin_dir, pin_en, rx_pin, tx_pin, baudrate, serialport=0, mtr_id (Serial NodeAddress changes with the MS0 MS1 pins) 106 | self.tmc = TMC_2209(rx_pin=self.UART_RX_PIN, tx_pin=self.UART_TX_PIN, mtr_id=sp_na, serialport=0, baudrate=230400) 107 | 108 | # test if the tmc driver UART 109 | if self.tmc.test(): # case the tmc UART test returns True 110 | 111 | # set StallGuard to max torque for the GPIO interrupt callback 112 | self.set_stallguard(threshold = 0) 113 | 114 | # instance variable for the max number of stepper revolution to find home 115 | self.max_homing_revs = 5 116 | 117 | # instance variable for the max number of steps to be done while homing 118 | self.max_steps = self.max_homing_revs * self.full_rev 119 | 120 | # instance variable for the steps to be done 121 | self.steps_to_do = self.max_steps 122 | 123 | # flag for stepper spinning status (True when the motor spins) 124 | self.stepper_spinning = False 125 | 126 | # flag to monitor the StallGuard detection 127 | self.stallguarded = True 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | # sm0, the PIO program for generating steps 136 | @asm_pio(set_init=PIO.OUT_LOW) 137 | def steps_mot_pio(): # Note: without 'self' in PIO callback function ! 138 | """ 139 | A frequency is generated at PIO: The frequency remains fix (it can activated and stopped). 140 | A Pin is associated to the Pin 141 | The speed can be varied by changing the off time (delay) between ON pulses 142 | This method allows for stepper speed variation, not really for precise positioning. 143 | """ 144 | label("main") # entry point from jmp 145 | pull(noblock) # get delay data from osr, and maintained in memory 146 | mov(x,osr) # OSR (Output Shift Register) content is moved to x, to prepare for future noblock pulls 147 | mov(y, x) # delay assigned to y scretch register 148 | set(pins, 1) [15] # set the pin high 16 times 149 | set(pins, 1) [15] # set the pin high 16 times 150 | label("delay") # entry point when x_dec>0 151 | set(pins, 0) # set the pin low 1 time 152 | jmp(y_dec, "delay") # y_dec-=1 if y_dec>0 else jump to "delay" 153 | jmp("main") # jump to "main" 154 | 155 | 156 | # sm1, the PIO program for stopping the sm0 (steps generator) 157 | @asm_pio() 158 | def stop_stepper_pio(): # Note: without 'self' in PIO callback function ! 159 | """ 160 | This PIO fiunction: 161 | - Reads the lowering edges from the output GPIO that generates the steps. 162 | - De-counts the 32bits OSR. 163 | - The starting value, to decount from, is sent by inline command. 164 | - Once the decount is zero it stops the steps generation (sm0). 165 | """ 166 | label("wait_for_step") 167 | wait(1, pin, 0) # wait for step signal to go HIGH 168 | wait(0, pin, 0) # wait for step signal to go LOW (lowering edge) 169 | jmp(x_dec, "wait_for_step") 170 | irq(block, rel(0)) # trigger an IRQ 171 | 172 | 173 | # sm2, the PIO program for counting steps 174 | @asm_pio() 175 | def steps_counter_pio(): # Note: without 'self' in PIO callback function ! 176 | """ 177 | This PIO fiunction: 178 | -Reads the rising edges from the output GPIO that generates the steps. 179 | -De-counts the 32bit OSR. 180 | -The starting value, to decount from, is sent by inline command. 181 | -The reading is pushed to the rx_fifo on request, via an inline command. 182 | """ 183 | label("loop") 184 | wait(0, pin, 0) # wait for step signal to go LOW 185 | wait(1, pin, 0) # wait for step signal to go HIGH (rising edge) 186 | jmp(x_dec, "loop") # decrement x and continue looping 187 | 188 | 189 | def set_pls_to_do(self, val): 190 | """Sets the initial value for the stpes to do, from which to decount from. 191 | Pre-encoded instructions are >140 times faster.""" 192 | self.sm1.put(val) # val value is passed to sm1 (StateMachine 1) 193 | self.sm1.exec(self.PULL_ENCODED) # execute pre-encoded 'pull()' instruction 194 | self.sm1.exec(self.MOV_X_OSR_ENCODED) # execute pre-encoded 'mov(x, osr)' instruction 195 | self.sm1.active(1) # sm1 is activated 196 | 197 | 198 | def set_pls_counter(self, val): 199 | """Sets the initial value for the stpes counter, from which to decount from. 200 | Pre-encoded instructions are >140 times faster.""" 201 | self.sm2.put(val) 202 | self.sm2.exec(self.PULL_ENCODED) # execute pre-encoded 'pull()' instruction 203 | self.sm2.exec(self.MOV_X_OSR_ENCODED) # execute pre-encoded 'mov(x, osr)' instruction 204 | 205 | 206 | def get_pls_count(self): 207 | """Gets the steps counter value. 208 | Pre-encoded instructions are >140 times faster.""" 209 | self.sm2.exec(self.MOV_ISR_X_ENCODED) # execute pre-encoded 'mov(isr, x)' instruction 210 | self.sm2.exec(self.PUSH_ENCODED) # execute pre-encoded 'push()' instruction 211 | if self.sm2.rx_fifo: # case there is data in the rx buffer 212 | return -self.sm2.get() & 0xffffffff # return the current sm2 counter value (32-bit unsigned integer) 213 | else: # case the rx buffer has no data 214 | return -1 # returning -1 is a clear exception for a positive counter ...) 215 | 216 | 217 | def set_steps_value(self, val): 218 | """Set the ref position (after homing).""" 219 | self.position = val 220 | 221 | 222 | def read_stallguard(self): 223 | """Gets the instant StallGuard value from the TMC driver, via the UART.""" 224 | return self.tmc.getStallguard_Result() 225 | 226 | 227 | def set_stallguard(self, threshold): 228 | """Sets the StallGuard threshold at TMC driver, via the UART.""" 229 | # clamp the SG threshold between 0 and 255 230 | threshold = max(0, min(threshold, 255)) 231 | 232 | # set the StallGuard threshold and the call-back handler function 233 | self.tmc.setStallguard_Callback(threshold = threshold, handler = self._stallguard_callback) 234 | if self.debug: 235 | if threshold != 0: 236 | print("Setting StallGuard (irq to GPIO 11) to value {}.".format(threshold)) 237 | else: 238 | print("Setting StallGuard (irq to GPIO 11) to 0, meaning max possible torque.") 239 | 240 | 241 | def get_stepper_frequency(self, pio_val): 242 | """Convert the PIO value (delay) to stepper frequency.""" 243 | if pio_val > 0: 244 | return int(self.frequency / (pio_val * self.PIO_VAR + self.PIO_FIX)) 245 | else: 246 | return None 247 | 248 | 249 | def get_stepper_value(self, stepper_freq): 250 | """Convert the stepper frequency to PIO value (delay).""" 251 | if stepper_freq > 0: 252 | return int((self.frequency - stepper_freq * self.PIO_FIX) / (stepper_freq * self.PIO_VAR)) 253 | else: 254 | return None 255 | 256 | 257 | def micro_step(self, ms): 258 | """Sets the GPIO for th decided microstepping (ms) setting.""" 259 | settings = self.microstep_map.get(ms) # retrieve the corresponding settings 260 | 261 | if settings: # case the ms setting exists 262 | ms_txt, k, ms1, ms2, SG_red, sp_na = settings # ms map elements are assigned 263 | self.STEPPER_MS1.value(ms1) # STEPPER_MS1 output set to ms1 value 264 | self.STEPPER_MS2.value(ms2) # STEPPER_MS2 output set to ms2 value 265 | print(f"Microstepping set to {ms_txt}") # feedback is printed to the terminal 266 | return ms # return info on microstepping 267 | else: # case the ms setting does not exist 268 | print("Wrong parameter for micro_step") # feedback is printed to the terminal 269 | return None 270 | 271 | 272 | def get_full_rev(self, ms): 273 | """Calculates the steps for one fulle revolution of the stepper.""" 274 | settings = self.microstep_map.get(ms) # retrieve the corresponding settings 275 | 276 | if settings: # case the ms setting exists 277 | ms_txt, k, ms1, ms2, SG_red, sp_na = settings # ms map elements are assigned 278 | full_rev = int(self.STEPPER_STEPS/k) # steps for a stepper full revolution 279 | print(f"Full revolution takes {full_rev} steps") # feedback is printed to the terminal 280 | return full_rev, SG_red, sp_na # return steps for full revolution microstepping, StallGuard reduction, Serialport NodeAddress 281 | else: # case the ms setting does not exist 282 | print("Wrong parameter for full revolution") # feedback is printed to the terminal 283 | return None, None, None 284 | 285 | 286 | def _stop_stepper_handler(self, sm0): 287 | """Call-back function by a PIO interrupt""" 288 | self.sm0.active(0) # state machine for stepper-steps generation is deactivated 289 | self.stepper_spinning = False # flag tracking the stepper spinning is set False 290 | 291 | 292 | def _stallguard_callback(self, pin): 293 | """Call-back function from the StallGuard.""" 294 | self.sm0.active(0) # state machine for stepper-steps generation is deactivated 295 | self.stallguarded = True # flag tracking the StallGuard at GPIO is set True 296 | self.stepper_spinning = False # flag tracking the stepper spinning is set False 297 | 298 | 299 | def stop_stepper(self): 300 | self.sm0.active(0) # state machine for stepper-steps generation is deactivated 301 | self.stepper_spinning = False # flag tracking the stepper spinning is set False 302 | 303 | 304 | def start_stepper(self): 305 | self.stepper_spinning = True # flag tracking the stepper spinning is set True 306 | self.sm0.active(1) # state machine for stepper-steps generation is activated 307 | 308 | 309 | def deactivate_pio(self): 310 | """Function to deactivate PIO.""" 311 | self.sm0.active(0) # sm0 is deactivated 312 | self.sm1.active(0) # sm1 is deactivated 313 | self.sm2.active(0) # sm2 is deactivated 314 | PIO(0).remove_program() # reset SM0 block 315 | PIO(1).remove_program() # reset SM1 block 316 | print("State Machines deactivated") 317 | 318 | 319 | def tmc_test(self): 320 | return self.tmc.test() 321 | 322 | 323 | def _homing(self, h_speed, stepper_freq, startup_loops, retract_time, retract_steps): 324 | """ 325 | Spins the stepper at stepper_freq until StallGuard value below threshold. 326 | Argument h_speed is the PIO value to get stepper_freq at the GPIO pin. 327 | """ 328 | self.set_stallguard(threshold = 0) # set SG threshold acting on the DIAG pin, to max torque 329 | max_homing_ms = retract_time + int(self.max_homing_revs * 1000 * self.full_rev / stepper_freq) # timeout in ms 330 | do_once = True # set a boolean as True for a single time operation 331 | 332 | self.stallguarded = False # flag tracking the StallGuard at GPIO is set initially False 333 | self.stop_stepper() # stepper is stopped (in the case it wasn't) 334 | self.set_pls_counter(0) # sets the initial value for stepper steps counting 335 | self.set_pls_to_do(retract_steps + self.max_steps) # (max) number of steps to find home 336 | self.sm0.put(h_speed) # stepper speed 337 | self.start_stepper() # stepper is started 338 | 339 | # calculate the SG threshold reference to which SG_value readings from UART are compared 340 | min_sg_expected = int(0.15 * stepper_freq / self.SG_adj) # expected minimum StallGuard value on free spinning 341 | k = 0.8 # reduction coeficient (it should be 0.7 ~ 0.8, tune it on your need) 342 | sg_threshold = int(k * min_sg_expected) # StallGuard threshold, for the SG value readings from UART 343 | 344 | # calculate the SG threshold, to be set on the stepper driver and acting to the DIAG pin 345 | k2 = 0.45 # reduction coeficient (<= 0.5 according to TMC datasheet, tune it on your need) 346 | sg_threshold_diag = int(k2 * min_sg_expected) # StallGuard threshold acting on the DIAG pin 347 | 348 | if self.debug: # case self.debug is set True 349 | sg_list = [] # list for debug purpose 350 | print(f"Homing with stepper speed of {stepper_freq}Hz and UART StallGuard threshold of {sg_threshold}") 351 | 352 | t_ref = time.ticks_ms() # time reference 353 | i = 0 # iterator index 354 | while time.ticks_ms() - t_ref < max_homing_ms: # while loop until timeout (unless Stalling detection) 355 | sg = self.tmc.getStallguard_Result() # StallGuard value is retrieved 356 | 357 | if self.debug: # case self.debug is set True 358 | sg_list.append(sg) # StallGuard value is appended to the list 359 | 360 | i+=1 # iterator index is increased 361 | if i > startup_loops: # case of at least startup_loops SG_readings (avoids startup effect to SG) 362 | if do_once: # case do_once is True 363 | self.set_stallguard(threshold = sg_threshold_diag) # set SG threshold acting on the DIAG pin 364 | do_once = False # set do_once False (execute this block of code only once) 365 | 366 | if sg < sg_threshold or self.stallguarded: # case StallGuard UART < SG threshold or DIAG pin HIGH 367 | self.stop_stepper() # stepper is stopped 368 | 369 | if self.get_pls_count() < 0.95 * (retract_steps + self.max_steps): # Stalling within 95% max steps 370 | if self.stallguarded: # case StallGuard detection via DIAG pin 371 | times = 1 # LED flashes once 372 | time_s = 0.01 # LED flashing time very short 373 | bright = 0.8 # LED flashing brightness (0.8 = 80%) 374 | else: # case StallGuard detection via SG value from UART 375 | times = 3 # LED flashes three times 376 | time_s = 0.05 # LED flashing time, allowing multiple flashes visibility 377 | bright = 0.1 # LED flashing brightness, allowing multiple flashes visibility 378 | rgb_led.flash_color('red', bright=bright, times=times, time_s=time_s) # flashing red led 379 | 380 | if self.debug: # case self.debug is True 381 | if self.stallguarded: # case StallGuard detection via DIAG pin 382 | print("StallGuard detections via DIAG pin") 383 | sg = self.tmc.getStallguard_Result() # Another StallGuard value is retrieved 384 | sg_list.append(sg) # The last SG value is appended 385 | print(f"Homing reached. Last SG values via UART: {sg_list}.", \ 386 | f"Total of {i} iterations in {time.ticks_ms()-t_ref} ms\n") 387 | return True # return True (successfull homing) 388 | 389 | else: # case StallGuard value higher than threshold 390 | if self.debug: # case self.debug is set True 391 | del sg_list[0] # first element in list is deleted 392 | else: # case self.debug is set False 393 | pass # do nothing 394 | 395 | self.set_stallguard(threshold = 0) # set SG threshold acting on the DIAG pin, to max torque 396 | self.stop_stepper() # stepper is stopped (if not done by the _homing func) 397 | print("Failed homing") # feedback is printed to the Terminal 398 | return False # return False (homing failure) 399 | 400 | 401 | def _retract(self, speed, stepper_freq, startup_loops): 402 | """ 403 | Spins the stepper backward for startup_loops time. 404 | It makes 'room' for the upcoming home search, preventing steps skipping, in the case 405 | the motor is already at the first home location. 406 | """ 407 | self.set_stallguard(threshold = 0) # set SG threshold acting on the DIAG pin, to max torque 408 | self.stop_stepper() # stepper is stopped (in the case it wasn't) 409 | self.set_pls_counter(0) # sets the initial value for stepper steps counting 410 | self.set_pls_to_do(self.max_steps) # (max) number of steps to find home 411 | self.sm0.put(speed) # stepper speed 412 | self.start_stepper() # stepper is started 413 | if self.debug: # case self.debug is set True 414 | print(f"Retract the stepper prior the first home search") 415 | sg_list = [] # list holding the SG value during startup_loops 416 | t_ref = time.ticks_ms() # time reference 417 | for i in range(startup_loops): # while loop until timeout 418 | sg = self.tmc.getStallguard_Result() # StallGuard value is retrieved via UART 419 | sg_list.append(sg) # StallGuard value is appended to the list 420 | 421 | self.stop_stepper() # stepper is stopped 422 | retract_time = time.ticks_ms() - t_ref # time for the stepper retracting 423 | retract_steps = self.get_pls_count() # number of steps made by the stepper during retract 424 | return retract_time, retract_steps 425 | 426 | 427 | def centering(self, stepper_freq): 428 | """ 429 | SENSORLESS homing, on both directions, to stop the stepper in the middle. 430 | Argument is the stepper speed, in Hz. 431 | Clockwise (CW) and counter-clockwise (CCW) also depends on motor wiring. 432 | """ 433 | min_freq = 400 * self.SG_adj # min stepper frequency for sensorless homing 434 | max_freq = 1200 * self.SG_adj # max stepper frequency (it can be much higher, like 2000, when using the SG DIAG) 435 | stepper_freq = max(min_freq, min(max_freq, self.SG_adj * stepper_freq)) # homing speed (frequency) clamped in range min ~ max 436 | stepper_val = self.get_stepper_value(stepper_freq) # stepper pio value is calculated 437 | startup_loops = 10 # number of code loops for the stepper startup 438 | 439 | self.STEPPER_DIR.value(0) # set stepper direction to 1 (CCW) 440 | retract_time, retract_steps = self._retract(stepper_val, stepper_freq, startup_loops) # retract the stepper 441 | 442 | self.STEPPER_DIR.value(1) # set stepper direction to 1 (CW) 443 | if self._homing(stepper_val, stepper_freq, startup_loops, retract_time, retract_steps): # call the _homing function at CW 444 | 445 | self.STEPPER_DIR.value(0) # set stepper direction to 0 (CCW) 446 | if self._homing(stepper_val, stepper_freq, startup_loops, retract_time, retract_steps): # call the _homing function at CCW 447 | 448 | steps_range = self.get_pls_count() # retrieves steps done on previous activation run 449 | half_range = int(steps_range/2) # half range 450 | self.STEPPER_DIR.value(1) # set stepper direction to 1 (CW) 451 | self.steps_to_do = half_range # steps to be done assigned to instance variable 452 | self.set_pls_to_do(self.steps_to_do) # number of steps to do now is half range 453 | self.sm0.put(stepper_val) # stepper speed 454 | self.start_stepper() # stepper is started 455 | centering_time_ms = 100 + int(self.steps_to_do * 1000 / stepper_freq) # stepper centering time in ms 456 | if self.debug: # case self.debug is set True 457 | print(f"Counted {steps_range} steps in between the 2 homes") 458 | print(f"Positioning the stepper at {half_range} from the last detected home") 459 | time.sleep_ms(centering_time_ms) # sleeping while the motor reaches the center 460 | if not self.stepper_spinning: # case the stepper_spinning is False 461 | rgb_led.flash_color('green', bright=0.2, times=3, time_s=0.05) # flashing green led 462 | return True # True is returned when centering succeeds 463 | 464 | else: # case the homing fails 465 | self.steps_to_do = self.max_steps # max number of homing steps is assigned to the steps to be done next 466 | self.stop_stepper() # stepper is stopped (in the case it wasn't) 467 | rgb_led.flash_color('blue', bright=0.1, times=10, time_s=0.05) # flashing blue led 468 | return False # False is returned when centering fails 469 | 470 | 471 | --------------------------------------------------------------------------------