5Copyright (c) 2017-2018 Sungeun K. Jeon for Gnea Research LLC
7Permission is hereby granted, free of charge, to any person obtaining a copy
8of this software and associated documentation files (the "Software"), to deal
9in the Software without restriction, including without limitation the rights
10to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11copies of the Software, and to permit persons to whom the Software is
12furnished to do so, subject to the following conditions:
14The above copyright notice and this permission notice shall be included in
15all copies or substantial portions of the Software.
17THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
29This Python script produces a continuous piece-wise line fit of actual spindle speed over
30programmed speed/PWM, which must be measured and provided by the user. A plot of the data
31and line fit will be auto-generated and saved in the working directory as 'line_fit.png'.
34 - Python 2.7 or 3.x
with SciPy, NumPy,
and Matplotlib Python Libraries
36 - For the most people, the easiest way to run this script
is on the free cloud service
37 https://repl.it/site/languages/python3. No account necessary. Unlimited runs.
39 - Last checked on 4/5/2018. This site has been regularly evolving
and becoming more
40 powerful. Things may
not work exactly
as shown. Please report any issues.
42 - To use, go to the website
and start the Python REPL. Copy
and paste this script into
43 the main.py file
in the browser editor. Click
'Run' and a solution should appear
in
44 the text output
in the REPL console window. You can edit the script directly
in the
45 browser
and re-run the script
as many times
as you need. A free account
is only
46 necessary
if you want to save files on their servers.
48 - This script will also automatically generate a png image
with the plot of the data
49 with the piece-wise linear fit over it, but this will
not show up by default on this
50 website. To enable this, just click the
'Add File' icon
and create a dummy file.
51 Name it anything, like dummy.py. Leave this file blank. Doing this places the REPL
52 in multiple file mode
and will enable viewing the plot. Click the
'Run' icon. The
53 solution will be presented again
in the console,
and the data plot will appear
in
54 the file list called
'line_fit.png'. Click the file to view the plot.
56 - For offline Python installs, most Mac
and Linux computers have Python pre-installed
57 with the required libraries. If
not, a quick google search will show you how to
58 install them. For Windows, Python installations are bit more difficult. Anaconda
and
59 Pyzo seem to work well.
62 - First, make sure you are using the stock build of Grbl
for the 328p processor. Most
63 importantly, the SPINDLE_PWM_MAX_VALUE
and SPINDLE_PWM_MIN_VALUE should be unaltered
64 from defaults, otherwise change them back to 255.0
and 1.0 respectively
for this test.
66 - Next, program the max
and min rpm Grbl settings to
'$30=255' and '$31=1'. This sets
67 the internal PWM values equal to
'S' spindle speed
for the standard Grbl build.
69 - Check
if your spindle does
not turn on at very low voltages by setting
'S' spindle
70 speed to
'S1'. If it does
not turn on
or turns at a non-useful rpm, increase
'S' by
71 one until it does. Write down this
'S' value
for later. You
'll start the rpm data
72 collection from this point onward
and will need to update the SPINDLE_PWM_MIN_VALUE
73 in cpu_map.h afterwards.
75 - Collect actual spindle speed
with a tachometer
or similar means over a range of
'S'
76 and PWM values. Start by setting the spindle
'S' speed to the minimum useful
'S' from
77 the last step
and measure
and record actual spindle rpm. Next, increase
'S' spindle
78 speed over equally sized intervals
and repeat the measurement. Increments of 20 rpm
79 should be more than enough, but decrease increment size,
if highly nonlinear. Complete
80 the data collection the
'S' spindle speed equal to
'$30' max rpm,
or at the max useful
81 rpm,
and record the actual rpm output. Make sure to collect rpm data all the way
82 throughout useful output rpm. The actual operating range within this model may be set
83 later within Grbl
with the
'$30' and '$31' settings.
85 - In some cases, spindle PWM output can have discontinuities
or not have a useful rpm
86 in certain ranges. For example, a known controller board has the spindle rpm drop
87 completely at voltages above ~4.5V. If you have discontinuities like this at the low
88 or high range of rpm, simply trim them
from the data set. Don
't include them. For
89 Grbl to compensate, you'll need to alter the SPINDLE_PWM_MIN_VALUE and/or
90 SPINDLE_PWM_MAX_VALUE in cpu_map.h to where your data set ends. This script will
91 indicate
if you need to do that
in the solution output.
93 - Keep
in mind that spindles without control electronics can slow down drastically when
94 cutting
and under load. How much it slows down
is dependent on a lot of factors, such
95 as feed rate, chip load, cutter diameter, flutes, cutter type, lubricant/coolant,
96 material being cut, etc. Even spindles
with controllers can still slow down
if the
97 load
is higher than the max current the controller can provide. It
's recommended to
98 frequently re-check and measure actual spindle speed during a job. You can always use
99 spindle speed overrides to tweak it temporarily to the desired speed.
101 - Edit this script
and enter the measured rpm values
and their corresponding
'S' spindle
102 speed values
in the data arrays below. Set the number of piecewise lines you would
103 like to use,
from one to four lines. For most cases, four lines
is perfectly fine.
104 In certain scenarios (laser engraving), this may significantly degrade performance
and
105 should be reduced
if possible.
107 - Run the Python script. Visually assess the line fit
from the plot. It will
not likely
108 to what you want on the first go. Dial things
in by altering the line fit junction
109 points
'PWM_pointX' in this script to move where the piecewise line junctions are
110 located along the plot x-axis. It may be desired to tweak the junction points so the
111 model solution
is more accurate
in the region that the spindle typically running.
112 Re-run the script
and tweak the junction points until you are satified
with the model.
114 - Record the solution
and enter the RPM_POINT
and RPM_LINE values into config.h. Set the
115 number of piecewise lines used
in this model
in config.h. Also set the
'$30' and '$31'
116 max
and min rpm values to the solution values
or in a range between them
in Grbl
'$'
117 settings. And
finally, alter the SPINDLE_PWM_MIN_VALUE
in cpu_map.h,
if your spindle
118 needs to be above a certain voltage to produce a useful low rpm.
120 - Once the solution
is entered. Recompile
and flash Grbl. This solution model
is only
121 valid
for this particular set of data. If the machine
is altered, you will need to
122 perform this experiment again
and regenerate a new model here.
125 The solver produces a set of values that define the piecewise fit
and can be used by
126 Grbl to quickly
and efficiently compute spindle PWM output voltage
for a desired RPM.
128 The first two are the RPM_MAX ($30)
and RPM_MIN ($31) Grbl settings. These must be
129 programmed into Grbl manually
or setup
in defaults.h
for new systems. Altering these
130 values within Grbl after a piece-wise linear model
is installed will
not change alter
131 model. It will only alter the range of spindle speed rpm values Grbl output.
133 For example,
if the solver produces an RPM_MAX of 9000
and Grbl
is programmed
with
134 $30=8000, S9000 may be programmed, but Grbl will only produce the output voltage to run
135 at 8000 rpm. In other words, Grbl will only output voltages the range between
136 max(RPM_MIN,$31)
and min(RPM_MAX,$30).
138 The remaining values define the slopes
and offsets of the line segments
and the junction
139 points between line segments, like so
for n_pieces=3:
141 PWM_output = RPM_LINE_A1 * rpm - RPM_LINE_B1 [ RPM_MIN < rpm < RPM_POINT12 ]
142 PWM_output = RPM_LINE_A2 * rpm - RPM_LINE_B2 [ RPM_POINT12 < rpm < RPM_POINT23 ]
143 PWM_output = RPM_LINE_A3 * rpm - RPM_LINE_B3 [ RPM_POINT23 < rpm < RPM_MAX ]
145 NOTE: The script solves
in terms of PWM but the final equations
and values are expressed
146 in terms of rpm
in the form
'PWM = a*rpm - b'.
150from scipy import optimize
153# ----------------------------------------------------------------------------------------
154# Configure spindle PWM line fit solver
156n_pieces = 4 # Number of line segments used for data fit. Only 1 to 4 line segments supported.
158# Programmed 'S' spindle speed values. Must start with minimum useful PWM or 'S' programmed
159# value and end with the maximum useful PWM or 'S' programmed value. Order of the array must
160# be synced with the RPM_measured array below.
161# NOTE: ** DO NOT USE DATA FROM AN EXISTING PIECEWISE LINE FIT. USE DEFAULT GRBL MODEL ONLY. **
162PWM_set = np.array([2,18,36,55,73,91,109,127,146,164,182,200,218,237,254], dtype=float)
164# Actual RPM measured at the spindle. Must be in the ascending value and equal in length
165# as the PWM_set array. Must include the min and max measured rpm output in the first and
166# last array entries, respectively.
167RPM_measured = np.array([213.,5420,7145,8282,9165,9765,10100,10500,10700,10900,11100,11250,11400,11550,11650], dtype=float)
169# Configure line fit points by 'S' programmed rpm or PWM value. Values must be between
170# PWM_max and PWM_min. Typically, alter these values to space the points evenly between
171# max and min PWM range. However, they may be tweaked to maximize accuracy in the places
172# you normally operate for highly nonlinear curves. Plot to visually assess how well the
173# solution fits the data.
174PWM_point1 = 20.0 # (S) Point between segments 0 and 1. Used when n_pieces >= 2.
193 return np.piecewise(x, [(x>=PWM_min)&(x<=PWM_max)], [
lambda x:k1*(x-PWM_min)+b])
197 b+k1*(PWM_point1-PWM_min)]
198 funcs = [
lambda x:k1*(x-PWM_min)+c[0],
199 lambda x:k2*(x-PWM_point1)+c[1]]
200 conds = [(x<PWM_point1)&(x>=PWM_min),
201 (x<=PWM_max)&(x>=PWM_point1)]
202 return np.piecewise(x, conds, funcs)
206 b+k1*(PWM_point1-PWM_min),
207 b+k1*(PWM_point1-PWM_min)+k2*(PWM_point2-PWM_point1)]
208 funcs = [
lambda x:k1*(x-PWM_min)+c[0],
209 lambda x:k2*(x-PWM_point1)+c[1],
210 lambda x:k3*(x-PWM_point2)+c[2]]
211 conds = [(x<PWM_point1)&(x>=PWM_min),
212 (x<PWM_point2)&(x>=PWM_point1),
213 (x<=PWM_max)&(x>=PWM_point2)]
214 return np.piecewise(x, conds, funcs)
218 b+k1*(PWM_point1-PWM_min),
219 b+k1*(PWM_point1-PWM_min)+k2*(PWM_point2-PWM_point1),
220 b+k1*(PWM_point1-PWM_min)+k2*(PWM_point2-PWM_point1)+k3*(PWM_point3-PWM_point2)]
221 funcs = [
lambda x:k1*(x-PWM_min)+c[0],
222 lambda x:k2*(x-PWM_point1)+c[1],
223 lambda x:k3*(x-PWM_point2)+c[2],
224 lambda x:k4*(x-PWM_point3)+c[3]]
225 conds = [(x<PWM_point1)&(x>=PWM_min),
226 (x<PWM_point2)&(x>=PWM_point1),
227 (x<PWM_point3)&(x>=PWM_point2),
228 (x<=PWM_max)&(x>=PWM_point3)]
229 return np.piecewise(x, conds, funcs)
234print(
" N_pieces: %i" % n_pieces)
235print(
" PWM_min: %.1f" % PWM_min)
236print(
" PWM_max: %.1f" % PWM_max)
238 print(
" PWM_point1: %.1f" % PWM_point1)
240 print(
" PWM_point2: %.1f" % PWM_point2)
242 print(
" PWM_point3: %.1f" % PWM_point3)
243print(
" N_data: %i" % len(RPM_measured))
244print(
" PWM_set: ", PWM_set)
245print(
" RPM_measured: ", RPM_measured)
248 piece_func = piecewise_linear_1
249 p_initial = [RPM_measured[0],slope_i]
251 p , e = optimize.curve_fit(piece_func, PWM_set, RPM_measured, p0=p_initial)
253 b = [ p[0]-p[1]*PWM_min]
255 p[0]+p[1]*(PWM_point1-PWM_min)]
258 piece_func = piecewise_linear_2
259 p_initial = [RPM_measured[0],slope_i,slope_i]
261 p , e = optimize.curve_fit(piece_func, PWM_set, RPM_measured, p0=p_initial)
263 b = [ p[0]-p[1]*PWM_min,
264 p[0]+p[1]*(PWM_point1-PWM_min)-p[2]*PWM_point1]
266 p[0]+p[1]*(PWM_point1-PWM_min),
267 p[0]+p[1]*(PWM_point1-PWM_min)+p[2]*(PWM_max-PWM_point1)]
270 piece_func = piecewise_linear_3
271 p_initial = [RPM_measured[0],slope_i,slope_i,slope_i]
273 p , e = optimize.curve_fit(piece_func, PWM_set, RPM_measured, p0=p_initial)
275 b = [ p[0]-p[1]*PWM_min,
276 p[0]+p[1]*(PWM_point1-PWM_min)-p[2]*PWM_point1,
277 p[0]+p[1]*(PWM_point1-PWM_min)+p[2]*(PWM_point2-PWM_point1)-p[3]*PWM_point2]
279 p[0]+p[1]*(PWM_point1-PWM_min),
280 p[0]+p[1]*(PWM_point1-PWM_min)+p[2]*(PWM_point2-PWM_point1),
281 p[0]+p[1]*(PWM_point1-PWM_min)+p[2]*(PWM_point2-PWM_point1)+p[3]*(PWM_max-PWM_point2) ]
284 piece_func = piecewise_linear_4
285 p_initial = [RPM_measured[0],slope_i,slope_i,slope_i,slope_i]
287 p , e = optimize.curve_fit(piece_func, PWM_set, RPM_measured, p0=p_initial)
288 a = [p[1],p[2],p[3],p[4]]
289 b = [ p[0]-p[1]*PWM_min,
290 p[0]+p[1]*(PWM_point1-PWM_min)-p[2]*PWM_point1,
291 p[0]+p[1]*(PWM_point1-PWM_min)+p[2]*(PWM_point2-PWM_point1)-p[3]*PWM_point2,
292 p[0]+p[1]*(PWM_point1-PWM_min)+p[2]*(PWM_point2-PWM_point1)+p[3]*(PWM_point3-PWM_point2)-p[4]*PWM_point3 ]
294 p[0]+p[1]*(PWM_point1-PWM_min),
295 p[0]+p[1]*(PWM_point1-PWM_min)+p[2]*(PWM_point2-PWM_point1),
296 p[0]+p[1]*(PWM_point1-PWM_min)+p[2]*(PWM_point2-PWM_point1)+p[3]*(PWM_point3-PWM_point2),
297 p[0]+p[1]*(PWM_point1-PWM_min)+p[2]*(PWM_point2-PWM_point1)+p[3]*(PWM_point3-PWM_point2)+p[4]*(PWM_max-PWM_point3) ]
300 print(
"ERROR: Unsupported number of pieces. Check and alter n_pieces")
303print(
"\nSOLUTION:\n\n[Update these #define values and uncomment]\n[ENABLE_PIECEWISE_LINEAR_SPINDLE in config.h.]")
304print(
"#define N_PIECES %.0f" % n_pieces)
305print(
"#define RPM_MAX %.1f" % rpm[-1])
306print(
"#define RPM_MIN %.1f" % rpm[0])
309 print(
"#define RPM_POINT12 %.1f" % rpm[1])
311 print(
"#define RPM_POINT23 %.1f" %rpm[2])
313 print(
"#define RPM_POINT34 %.1f" %rpm[3])
315print(
"#define RPM_LINE_A1 %.6e" % (1./a[0]))
316print(
"#define RPM_LINE_B1 %.6e" % (b[0]/a[0]))
318 print(
"#define RPM_LINE_A2 %.6e" % (1./a[1]))
319 print(
"#define RPM_LINE_B2 %.6e" % (b[1]/a[1]))
321 print(
"#define RPM_LINE_A3 %.6e" % (1./a[2]))
322 print(
"#define RPM_LINE_B3 %.6e" % (b[2]/a[2]))
324 print(
"#define RPM_LINE_A4 %.6e" % (1./a[3]))
325 print(
"#define RPM_LINE_B4 %.6e" % (b[3]/a[3]))
327print(
"\n[To operate over full model range, manually write these]")
328print(
"['$' settings or alter values in defaults.h. Grbl will]")
329print(
"[operate between min($30,RPM_MAX) and max($31,RPM_MIN)]")
330print(
"$30=%.1f (rpm max)" % rpm[-1])
331print(
"$31=%.1f (rpm min)" % rpm[0])
333if (PWM_min > 1)|(PWM_max<255):
334 print(
"\n[Update the following #define values in cpu_map.h]")
336 print(
"#define SPINDLE_PWM_MIN_VALUE %.0f" % PWM_min)
338 print(
"#define SPINDLE_PWM_MAX_VALUE %.0f" % PWM_max)
340 print(
"\n[No cpu_map.h changes required.]")
343test_val = (1./a[0])*rpm[0] - (b[0]/a[0])
345 print(
"ERROR: Solution is negative at RPM_MIN. Adjust junction points or increase n_pieces.\n")
349 matplotlib.use(
"Agg")
350 import matplotlib.pyplot
as plt
353 ax = fig.add_subplot(111)
354 xd = np.linspace(PWM_min, PWM_max, 10000)
355 ax.plot(PWM_set, RPM_measured,
"o")
357 plt.xlabel(
"Programmed PWM")
358 plt.ylabel(
"Measured RPM")
373 fig.savefig(
"line_fit.png")
def piecewise_linear_1(x, b, k1)
def piecewise_linear_3(x, b, k1, k2, k3)
def piecewise_linear_4(x, b, k1, k2, k3, k4)
def piecewise_linear_2(x, b, k1, k2)