2 This file is part of Smoothie (http://smoothieware.org/). The motion control part is heavily based on Grbl (https://github.com/simen/grbl) with additions from Sungeun K. Jeon (https://github.com/chamnit/grbl)
3 Smoothie is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
4 Smoothie is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
5 You should have received a copy of the GNU General Public License along with Smoothie. If not, see <http://www.gnu.org/licenses/>.
8 #include "libs/Module.h"
9 #include "libs/Kernel.h"
16 #include "libs/nuts_bolts.h"
18 #include "libs/StepperMotor.h"
19 #include "../communication/utils/Gcode.h"
20 #include "arm_solutions/BaseSolution.h"
21 #include "arm_solutions/CartesianSolution.h"
22 #include "arm_solutions/RotatableCartesianSolution.h"
23 #include "arm_solutions/RostockSolution.h"
27 static double m2stime
;
31 this->inch_mode
= false;
32 this->absolute_mode
= true;
33 this->motion_mode
= MOTION_MODE_SEEK
;
34 this->select_plane(X_AXIS
, Y_AXIS
, Z_AXIS
);
35 clear_vector(this->current_position
);
36 clear_vector(this->last_milestone
);
37 this->arm_solution
= NULL
;
38 seconds_per_minute
= 60.0;
44 //Called when the module has just been loaded
45 void Robot::on_module_loaded() {
46 register_for_event(ON_CONFIG_RELOAD
);
47 this->register_for_event(ON_GCODE_RECEIVED
);
50 this->on_config_reload(this);
52 // Make our 3 StepperMotors
53 this->alpha_stepper_motor
= this->kernel
->step_ticker
->add_stepper_motor( new StepperMotor(&alpha_step_pin
,&alpha_dir_pin
,&alpha_en_pin
) );
54 this->beta_stepper_motor
= this->kernel
->step_ticker
->add_stepper_motor( new StepperMotor(&beta_step_pin
, &beta_dir_pin
, &beta_en_pin
) );
55 this->gamma_stepper_motor
= this->kernel
->step_ticker
->add_stepper_motor( new StepperMotor(&gamma_step_pin
,&gamma_dir_pin
,&gamma_en_pin
) );
59 void Robot::on_config_reload(void* argument
){
60 if (this->arm_solution
) delete this->arm_solution
;
61 int solution_checksum
= get_checksum(this->kernel
->config
->value(arm_solution_checksum
)->by_default("cartesian")->as_string());
63 // Note checksums are not const expressions when in debug mode, so don't use switch
64 if(solution_checksum
== rostock_checksum
) {
65 this->arm_solution
= new RostockSolution(this->kernel
->config
);
67 }else if(solution_checksum
== delta_checksum
) {
68 // place holder for now
69 this->arm_solution
= new RostockSolution(this->kernel
->config
);
71 }else if(solution_checksum
== rotatable_cartesian_checksum
) {
72 this->arm_solution
= new RotatableCartesianSolution(this->kernel
->config
);
74 }else if(solution_checksum
== cartesian_checksum
) {
75 this->arm_solution
= new CartesianSolution(this->kernel
->config
);
78 this->arm_solution
= new CartesianSolution(this->kernel
->config
);
82 this->feed_rate
= this->kernel
->config
->value(default_feed_rate_checksum
)->by_default(100 )->as_number() / 60;
83 this->seek_rate
= this->kernel
->config
->value(default_seek_rate_checksum
)->by_default(100 )->as_number() / 60;
84 this->mm_per_line_segment
= this->kernel
->config
->value(mm_per_line_segment_checksum
)->by_default(5.0 )->as_number();
85 this->mm_per_arc_segment
= this->kernel
->config
->value(mm_per_arc_segment_checksum
)->by_default(0.5 )->as_number();
86 this->arc_correction
= this->kernel
->config
->value(arc_correction_checksum
)->by_default(5 )->as_number();
87 this->max_speeds
[X_AXIS
] = this->kernel
->config
->value(x_axis_max_speed_checksum
)->by_default(60000 )->as_number();
88 this->max_speeds
[Y_AXIS
] = this->kernel
->config
->value(y_axis_max_speed_checksum
)->by_default(60000 )->as_number();
89 this->max_speeds
[Z_AXIS
] = this->kernel
->config
->value(z_axis_max_speed_checksum
)->by_default(300 )->as_number();
90 this->alpha_step_pin
.from_string( this->kernel
->config
->value(alpha_step_pin_checksum
)->by_default("2.0" )->as_string())->as_output();
91 this->alpha_dir_pin
.from_string( this->kernel
->config
->value(alpha_dir_pin_checksum
)->by_default("0.5" )->as_string())->as_output();
92 this->alpha_en_pin
.from_string( this->kernel
->config
->value(alpha_en_pin_checksum
)->by_default("0.4" )->as_string())->as_output()->as_open_drain();
93 this->beta_step_pin
.from_string( this->kernel
->config
->value(beta_step_pin_checksum
)->by_default("2.1" )->as_string())->as_output();
94 this->gamma_step_pin
.from_string( this->kernel
->config
->value(gamma_step_pin_checksum
)->by_default("2.2" )->as_string())->as_output();
95 this->gamma_dir_pin
.from_string( this->kernel
->config
->value(gamma_dir_pin_checksum
)->by_default("0.20" )->as_string())->as_output();
96 this->gamma_en_pin
.from_string( this->kernel
->config
->value(gamma_en_pin_checksum
)->by_default("0.19" )->as_string())->as_output()->as_open_drain();
97 this->beta_dir_pin
.from_string( this->kernel
->config
->value(beta_dir_pin_checksum
)->by_default("0.11" )->as_string())->as_output();
98 this->beta_en_pin
.from_string( this->kernel
->config
->value(beta_en_pin_checksum
)->by_default("0.10" )->as_string())->as_output()->as_open_drain();
102 //A GCode has been received
103 void Robot::on_gcode_received(void * argument
){
104 Gcode
* gcode
= static_cast<Gcode
*>(argument
);
105 gcode
->call_on_gcode_execute_event_immediatly
= false;
106 gcode
->on_gcode_execute_event_called
= false;
107 //If the queue is empty, execute immediatly, otherwise attach to the last added block
108 if( this->kernel
->conveyor
->queue
.size() == 0 ){
109 gcode
->call_on_gcode_execute_event_immediatly
= true;
110 this->execute_gcode(gcode
);
111 if( gcode
->on_gcode_execute_event_called
== false ){
112 //printf("GCODE A: %s \r\n", gcode->command.c_str() );
113 this->kernel
->call_event(ON_GCODE_EXECUTE
, gcode
);
116 Block
* block
= this->kernel
->conveyor
->queue
.get_ref( this->kernel
->conveyor
->queue
.size() - 1 );
117 this->execute_gcode(gcode
);
118 block
->append_gcode(gcode
);
124 void Robot::reset_axis_position(double position
, int axis
) {
125 this->last_milestone
[axis
] = this->current_position
[axis
] = position
;
126 this->arm_solution
->millimeters_to_steps(this->current_position
, this->kernel
->planner
->position
);
130 //See if the current Gcode line has some orders for us
131 void Robot::execute_gcode(Gcode
* gcode
){
133 //Temp variables, constant properties are stored in the object
134 uint8_t next_action
= NEXT_ACTION_DEFAULT
;
135 this->motion_mode
= -1;
137 //G-letter Gcodes are mostly what the Robot module is interrested in, other modules also catch the gcode event and do stuff accordingly
138 if( gcode
->has_letter('G')){
139 switch( (int) gcode
->get_value('G') ){
140 case 0: this->motion_mode
= MOTION_MODE_SEEK
; break;
141 case 1: this->motion_mode
= MOTION_MODE_LINEAR
; break;
142 case 2: this->motion_mode
= MOTION_MODE_CW_ARC
; break;
143 case 3: this->motion_mode
= MOTION_MODE_CCW_ARC
; break;
144 case 17: this->select_plane(X_AXIS
, Y_AXIS
, Z_AXIS
); break;
145 case 18: this->select_plane(X_AXIS
, Z_AXIS
, Y_AXIS
); break;
146 case 19: this->select_plane(Y_AXIS
, Z_AXIS
, X_AXIS
); break;
147 case 20: this->inch_mode
= true; break;
148 case 21: this->inch_mode
= false; break;
149 case 90: this->absolute_mode
= true; break;
150 case 91: this->absolute_mode
= false; break;
152 if(gcode
->get_num_args() == 0){
153 clear_vector(this->last_milestone
);
155 for (char letter
= 'X'; letter
<= 'Z'; letter
++){
156 if ( gcode
->has_letter(letter
) )
157 this->last_milestone
[letter
-'X'] = this->to_millimeters(gcode
->get_value(letter
));
160 memcpy(this->current_position
, this->last_milestone
, sizeof(double)*3); // current_position[] = last_milestone[];
161 this->arm_solution
->millimeters_to_steps(this->current_position
, this->kernel
->planner
->position
);
162 return; // TODO: Wait until queue empty
165 }else if( gcode
->has_letter('M')){
166 switch( (int) gcode
->get_value('M') ){
167 case 92: // M92 - set steps per mm
169 this->arm_solution
->get_steps_per_millimeter(steps
);
170 if (gcode
->has_letter('X'))
171 steps
[0] = this->to_millimeters(gcode
->get_value('X'));
172 if (gcode
->has_letter('Y'))
173 steps
[1] = this->to_millimeters(gcode
->get_value('Y'));
174 if (gcode
->has_letter('Z'))
175 steps
[2] = this->to_millimeters(gcode
->get_value('Z'));
176 if (gcode
->has_letter('F'))
177 seconds_per_minute
= gcode
->get_value('F');
178 this->arm_solution
->set_steps_per_millimeter(steps
);
179 // update current position in steps
180 this->arm_solution
->millimeters_to_steps(this->current_position
, this->kernel
->planner
->position
);
181 gcode
->stream
->printf("X:%g Y:%g Z:%g F:%g ", steps
[0], steps
[1], steps
[2], seconds_per_minute
);
182 gcode
->add_nl
= true;
184 case 114: gcode
->stream
->printf("C: X:%1.3f Y:%1.3f Z:%1.3f %f",
185 this->current_position
[0],
186 this->current_position
[1],
187 this->current_position
[2], m2scnt
> 0 ? m2stime
/m2scnt
:0);
188 m2scnt
= 0; m2stime
= 0;
189 gcode
->add_nl
= true;
191 case 220: // M220 - speed override percentage
192 if (gcode
->has_letter('S'))
194 double factor
= gcode
->get_value('S');
195 // enforce minimum 1% speed
198 seconds_per_minute
= factor
* 0.6;
202 if( this->motion_mode
< 0)
206 double target
[3], offset
[3];
207 clear_vector(target
); clear_vector(offset
);
209 memcpy(target
, this->current_position
, sizeof(target
)); //default to last target
211 for(char letter
= 'I'; letter
<= 'K'; letter
++){ if( gcode
->has_letter(letter
) ){ offset
[letter
-'I'] = this->to_millimeters(gcode
->get_value(letter
)); } }
212 for(char letter
= 'X'; letter
<= 'Z'; letter
++){ if( gcode
->has_letter(letter
) ){ target
[letter
-'X'] = this->to_millimeters(gcode
->get_value(letter
)) + ( this->absolute_mode
? 0 : target
[letter
-'X']); } }
214 if( gcode
->has_letter('F') )
216 if( this->motion_mode
== MOTION_MODE_SEEK
)
217 this->seek_rate
= this->to_millimeters( gcode
->get_value('F') ) / 60.0;
219 this->feed_rate
= this->to_millimeters( gcode
->get_value('F') ) / 60.0;
222 //Perform any physical actions
223 switch( next_action
){
224 case NEXT_ACTION_DEFAULT
:
225 switch(this->motion_mode
){
226 case MOTION_MODE_CANCEL
: break;
227 case MOTION_MODE_SEEK
: this->append_line(gcode
, target
, this->seek_rate
); break;
228 case MOTION_MODE_LINEAR
: this->append_line(gcode
, target
, this->feed_rate
); break;
229 case MOTION_MODE_CW_ARC
: case MOTION_MODE_CCW_ARC
: this->compute_arc(gcode
, offset
, target
); break;
234 // As far as the parser is concerned, the position is now == target. In reality the
235 // motion control system might still be processing the action and the real tool position
236 // in any intermediate location.
237 memcpy(this->current_position
, target
, sizeof(double)*3); // this->position[] = target[];
241 // Convert target from millimeters to steps, and append this to the planner
242 void Robot::append_milestone( double target
[], double rate
){
243 int steps
[3]; //Holds the result of the conversion
246 this->arm_solution
->millimeters_to_steps( target
, steps
);
247 m2stime
+= m2st
.read_us();
251 for(int axis
=X_AXIS
;axis
<=Z_AXIS
;axis
++){deltas
[axis
]=target
[axis
]-this->last_milestone
[axis
];}
254 double millimeters_of_travel
= sqrt( pow( deltas
[X_AXIS
], 2 ) + pow( deltas
[Y_AXIS
], 2 ) + pow( deltas
[Z_AXIS
], 2 ) );
256 for(int axis
=X_AXIS
;axis
<=Z_AXIS
;axis
++){
257 if( this->max_speeds
[axis
] > 0 ){
258 double axis_speed
= ( fabs(deltas
[axis
]) / ( millimeters_of_travel
/ rate
)) * seconds_per_minute
;
259 if( axis_speed
> this->max_speeds
[axis
] ){
260 rate
= rate
* ( this->max_speeds
[axis
] / axis_speed
);
265 this->kernel
->planner
->append_block( steps
, rate
* seconds_per_minute
, millimeters_of_travel
, deltas
);
267 memcpy(this->last_milestone
, target
, sizeof(double)*3); // this->last_milestone[] = target[];
271 void Robot::append_line(Gcode
* gcode
, double target
[], double rate
){
274 // We cut the line into smaller segments. This is not usefull in a cartesian robot, but necessary for robots with rotational axes.
275 // In cartesian robot, a high "mm_per_line_segment" setting will prevent waste.
276 gcode
->millimeters_of_travel
= sqrt( pow( target
[X_AXIS
]-this->current_position
[X_AXIS
], 2 ) + pow( target
[Y_AXIS
]-this->current_position
[Y_AXIS
], 2 ) + pow( target
[Z_AXIS
]-this->current_position
[Z_AXIS
], 2 ) );
278 if( gcode
->call_on_gcode_execute_event_immediatly
== true ){
279 //printf("GCODE B: %s \r\n", gcode->command.c_str() );
280 this->kernel
->call_event(ON_GCODE_EXECUTE
, gcode
);
281 gcode
->on_gcode_execute_event_called
= true;
284 if (gcode
->millimeters_of_travel
== 0.0) {
285 this->append_milestone(this->current_position
, 0.0);
289 uint16_t segments
= ceil( gcode
->millimeters_of_travel
/ this->mm_per_line_segment
);
290 // A vector to keep track of the endpoint of each segment
291 double temp_target
[3];
293 memcpy( temp_target
, this->current_position
, sizeof(double)*3); // temp_target[] = this->current_position[];
296 for( int i
=0; i
<segments
-1; i
++ ){
297 for(int axis
=X_AXIS
; axis
<= Z_AXIS
; axis
++ ){ temp_target
[axis
] += ( target
[axis
]-this->current_position
[axis
] )/segments
; }
298 this->append_milestone(temp_target
, rate
);
300 this->append_milestone(target
, rate
);
304 void Robot::append_arc(Gcode
* gcode
, double target
[], double offset
[], double radius
, bool is_clockwise
){
306 double center_axis0
= this->current_position
[this->plane_axis_0
] + offset
[this->plane_axis_0
];
307 double center_axis1
= this->current_position
[this->plane_axis_1
] + offset
[this->plane_axis_1
];
308 double linear_travel
= target
[this->plane_axis_2
] - this->current_position
[this->plane_axis_2
];
309 double r_axis0
= -offset
[this->plane_axis_0
]; // Radius vector from center to current location
310 double r_axis1
= -offset
[this->plane_axis_1
];
311 double rt_axis0
= target
[this->plane_axis_0
] - center_axis0
;
312 double rt_axis1
= target
[this->plane_axis_1
] - center_axis1
;
314 // CCW angle between position and target from circle center. Only one atan2() trig computation required.
315 double angular_travel
= atan2(r_axis0
*rt_axis1
-r_axis1
*rt_axis0
, r_axis0
*rt_axis0
+r_axis1
*rt_axis1
);
316 if (angular_travel
< 0) { angular_travel
+= 2*M_PI
; }
317 if (is_clockwise
) { angular_travel
-= 2*M_PI
; }
319 gcode
->millimeters_of_travel
= hypot(angular_travel
*radius
, fabs(linear_travel
));
321 if( gcode
->call_on_gcode_execute_event_immediatly
== true ){
322 //printf("GCODE C: %s \r\n", gcode->command.c_str() );
323 this->kernel
->call_event(ON_GCODE_EXECUTE
, gcode
);
324 gcode
->on_gcode_execute_event_called
= true;
327 if (gcode
->millimeters_of_travel
== 0.0) {
328 this->append_milestone(this->current_position
, 0.0);
332 uint16_t segments
= floor(gcode
->millimeters_of_travel
/this->mm_per_arc_segment
);
334 double theta_per_segment
= angular_travel
/segments
;
335 double linear_per_segment
= linear_travel
/segments
;
337 /* Vector rotation by transformation matrix: r is the original vector, r_T is the rotated vector,
338 and phi is the angle of rotation. Based on the solution approach by Jens Geisler.
339 r_T = [cos(phi) -sin(phi);
340 sin(phi) cos(phi] * r ;
341 For arc generation, the center of the circle is the axis of rotation and the radius vector is
342 defined from the circle center to the initial position. Each line segment is formed by successive
343 vector rotations. This requires only two cos() and sin() computations to form the rotation
344 matrix for the duration of the entire arc. Error may accumulate from numerical round-off, since
345 all double numbers are single precision on the Arduino. (True double precision will not have
346 round off issues for CNC applications.) Single precision error can accumulate to be greater than
347 tool precision in some cases. Therefore, arc path correction is implemented.
349 Small angle approximation may be used to reduce computation overhead further. This approximation
350 holds for everything, but very small circles and large mm_per_arc_segment values. In other words,
351 theta_per_segment would need to be greater than 0.1 rad and N_ARC_CORRECTION would need to be large
352 to cause an appreciable drift error. N_ARC_CORRECTION~=25 is more than small enough to correct for
353 numerical drift error. N_ARC_CORRECTION may be on the order a hundred(s) before error becomes an
354 issue for CNC machines with the single precision Arduino calculations.
355 This approximation also allows mc_arc to immediately insert a line segment into the planner
356 without the initial overhead of computing cos() or sin(). By the time the arc needs to be applied
357 a correction, the planner should have caught up to the lag caused by the initial mc_arc overhead.
358 This is important when there are successive arc motions.
360 // Vector rotation matrix values
361 double cos_T
= 1-0.5*theta_per_segment
*theta_per_segment
; // Small angle approximation
362 double sin_T
= theta_per_segment
;
364 double arc_target
[3];
371 // Initialize the linear axis
372 arc_target
[this->plane_axis_2
] = this->current_position
[this->plane_axis_2
];
374 for (i
= 1; i
<segments
; i
++) { // Increment (segments-1)
376 if (count
< this->arc_correction
) {
377 // Apply vector rotation matrix
378 r_axisi
= r_axis0
*sin_T
+ r_axis1
*cos_T
;
379 r_axis0
= r_axis0
*cos_T
- r_axis1
*sin_T
;
383 // Arc correction to radius vector. Computed only every N_ARC_CORRECTION increments.
384 // Compute exact location by applying transformation matrix from initial radius vector(=-offset).
385 cos_Ti
= cos(i
*theta_per_segment
);
386 sin_Ti
= sin(i
*theta_per_segment
);
387 r_axis0
= -offset
[this->plane_axis_0
]*cos_Ti
+ offset
[this->plane_axis_1
]*sin_Ti
;
388 r_axis1
= -offset
[this->plane_axis_0
]*sin_Ti
- offset
[this->plane_axis_1
]*cos_Ti
;
392 // Update arc_target location
393 arc_target
[this->plane_axis_0
] = center_axis0
+ r_axis0
;
394 arc_target
[this->plane_axis_1
] = center_axis1
+ r_axis1
;
395 arc_target
[this->plane_axis_2
] += linear_per_segment
;
396 this->append_milestone(arc_target
, this->feed_rate
);
399 // Ensure last segment arrives at target location.
400 this->append_milestone(target
, this->feed_rate
);
404 void Robot::compute_arc(Gcode
* gcode
, double offset
[], double target
[]){
407 double radius
= hypot(offset
[this->plane_axis_0
], offset
[this->plane_axis_1
]);
409 // Set clockwise/counter-clockwise sign for mc_arc computations
410 bool is_clockwise
= false;
411 if( this->motion_mode
== MOTION_MODE_CW_ARC
){ is_clockwise
= true; }
414 this->append_arc(gcode
, target
, offset
, radius
, is_clockwise
);
419 // Convert from inches to millimeters ( our internal storage unit ) if needed
420 inline double Robot::to_millimeters( double value
){
421 return this->inch_mode
? value
/25.4 : value
;
424 double Robot::theta(double x
, double y
){
425 double t
= atan(x
/fabs(y
));
426 if (y
>0) {return(t
);} else {if (t
>0){return(M_PI
-t
);} else {return(-M_PI
-t
);}}
429 void Robot::select_plane(uint8_t axis_0
, uint8_t axis_1
, uint8_t axis_2
){
430 this->plane_axis_0
= axis_0
;
431 this->plane_axis_1
= axis_1
;
432 this->plane_axis_2
= axis_2
;