├── .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 | [](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 | [](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 | [](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 | 
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 | 
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 |
--------------------------------------------------------------------------------