StepperWinch Arduino Sketch¶
This sketch controls four stepper motors as capstan drives for moving control lines. It receives control messages from an external source over a USB serial port. The protocol is event-driven with local filtering and motion smoothing, much like a MIDI instrument driven by note events.
The full code for the sketch spans several files; all files may be downloaded in a single archive file as StepperWinch.zip, browsed in raw form in the source folder, or browsed below.
Compiling the sketch requries installing the optional TimerOne library in the Arduino IDE for supporting timer interrupt processing.
Main¶
The main entry points and event loop are in file StepperWinch.ino.
1/// \file StepperWinch.ino
2/// \brief Driver for four-channel stepper-motor capstan winch system.
3///
4/// \copyright Written over 2014-2018 by Garth Zeglin <garthz@cmu.edu>. To the
5/// extent possible under law, the author has dedicated all copyright and
6/// related and neighboring rights to this software to the public domain
7/// worldwide. This software is distributed without any warranty. You should
8/// have received a copy of the CC0 Public Domain Dedication along with this
9/// software. If not, see <http://creativecommons.org/publicdomain/zero/1.0/>.
10///
11/// \details This sketch is designed to support the expressive gestural motion
12/// of a kinetic fabric sculpture rather than perform precision trajectory
13/// tracking as would be typical for CNC applications. The communicates over
14/// the serial port to receive motion commands and report status. The protocol
15/// is event-driven and implements parameterized gestural motions; it is
16/// patterned after MIDI or OSC conventions. This sketch assumes the following:
17///
18/// 1. The TimerOne library is installed in the Arduino IDE.
19/// 2. Four A4988 stepper motor drivers are connected following the CNC Shield pin conventions.
20/// 3. If using a CNC Shield board, A-axis jumpers are installed to connect A-DIR to D13 (also SpinDir) and A-STEP to D12 (also SpinEnable)..
21/// 4. The serial communication baud rate is 115200 bps.
22///
23/// Typically, for 200 step/rev (1.8 deg) stepper motors the drivers are
24/// configured for 1/4 step microstepping (MS2 pulled high). However, the
25/// protocol commands use integer step units so the code does not depend on
26/// this.
27
28// ================================================================
29// Import the TimerOne library to support timer interrupt processing.
30#include "TimerOne.h"
31
32// Include the other modules from this sketch.
33#include "cnc_shield.h"
34#include "Stepper.h"
35#include "Path.h"
36
37// ================================================================
38// Communication protocol.
39
40// The message protocol is based on plain-text commands sent as keywords and
41// values in a line of text. One line is one message. All the input message
42// formats begin with a string naming a specific command or destination followed
43// by one or two arguments. The output formats are similar but include more
44// general debugging output with a variable number of tokens.
45
46// ----------------------------------------------------------------
47// The following global messages are not channel-specific.
48
49// Command Arguments Meaning
50// ping query whether the server is running
51// version query the identity of the sketch
52// srate <value> set the status reporting interval in milliseconds
53// enable <value> enable or disable all driver outputs, value is 0 or non-zero
54
55// ----------------------------------------------------------------
56// The following messages include a token representing the flag set specifying
57// the affected axes. The flag set should include one or more single-letter
58// channel specifiers (regex form: "[xyza]+"). Note that the flag set cannot be
59// empty.
60
61// --------------------------------
62
63// Absolute move. There should be an integer target value corresponding to each
64// included channel; each controller target is set to the specified position.
65// The motion is not coordinated; different channels may finish at different
66// times. Note that this command will enable all drivers.
67
68// a <flags> <offset>+
69//
70// Examples:
71// a xyza 100 120 -200 -50 move the axes to the specified locations
72// a x 50 move the X axis to +50
73
74// --------------------------------
75
76// Relative move. There should be an offset value corresponding to each included
77// channel; each controller target is incremented by the specified amount. The
78// motion is not coordinated; different channels may finish at different times.
79// Note that this command will enable all drivers.
80//
81// d <flags> <offset>+
82//
83// Examples:
84// d xyza 10 10 -10 -10 move all axes ten steps (two forward, two backward)
85// d x 50 move the X axis fifty steps forward
86
87// --------------------------------
88
89// Set velocity. There should be an integer velocity value corresponding to each
90// included channel; each controller target velocity is set to the amount
91// specified in units/sec. Note that this command will enable all drivers.
92//
93// v <flags> <value>+
94//
95// Examples:
96// v xyza 10 10 -10 -10 set all axes to drift 10 steps per second (two forward, two backward)
97// v x 500 set the X axis to constantly move forward at roughly half speed
98
99// --------------------------------
100// Set second-order gains. The same dynamic parameters are applied to all included channels.
101// g <flags> <frequency (Hz)> <damping-ratio>
102//
103// Examples:
104// g xyza 2.0 1.0 set all channels to 1 Hz natural frequency with critical damping
105// g xyza 0.1 0.5 set all channels to 0.1 Hz natural frequency and underdamping
106
107// --------------------------------
108// Set velocity and acceleration limits. The same dynamic parameters are applied to all included channels.
109// l <flags> <maximum velocity (steps/sec)> <maximum acceleration (steps/sec/sec)>
110//
111// Examples:
112// l xyza 4000 40000 set all channels to 4000 steps/sec and 40000 steps/sec/sec
113
114// ----------------------------------------------------------------
115// This program generates the following messages:
116
117// Command Arguments Meaning
118// awake initialization has completed or ping was received
119// txyza <usec> <x> <y> <z> <a> Arduino clock time in microseconds, followed by absolute step position
120// dbg <value-or-token>+ debugging message to print for user
121// id <tokens>+ tokens identifying the specific sketch
122
123// ================================================================
124// Global variables and constants.
125
126// The baud rate is the number of bits per second transmitted over the serial port.
127#define BAUD_RATE 115200
128
129// Interval in microseconds between status messages.
130static unsigned long status_poll_interval = 200000; // 5 Hz message rate to start
131
132/// Control objects for the stepper channels. The declaration statically
133/// initializes the global state objects for the channels. Note that this does
134/// not initialize the hardware; that is performed in setup().
135static Stepper x_axis(X_AXIS_STEP_PIN, X_AXIS_DIR_PIN);
136static Stepper y_axis(Y_AXIS_STEP_PIN, Y_AXIS_DIR_PIN);
137static Stepper z_axis(Z_AXIS_STEP_PIN, Z_AXIS_DIR_PIN);
138static Stepper a_axis(A_AXIS_STEP_PIN, A_AXIS_DIR_PIN);
139
140/// Path generator object for each channel.
141static Path x_path, y_path, z_path, a_path;
142
143/// The timestamp in microseconds for the last polling cycle, used to compute
144/// the exact interval between stepper motor updates.
145static unsigned long last_interrupt_clock = 0;
146
147/// Identification string.
148static const char version_string[] = "id StepperWinch " __DATE__;
149
150// ================================================================
151/// Enable or disable the stepper motor drivers. The output is active-low,
152/// so this inverts the sense.
153static inline void set_driver_enable(int value)
154{
155 digitalWrite(STEPPER_ENABLE_PIN, (value != 0) ? LOW : HIGH);
156}
157
158// ================================================================
159/// Interrupt handler to update all the stepper motor channels. Note that this
160/// is called from a timer interrupt context, so it should take as little time as
161/// feasible and cannot use serial I/O (i.e. no debugging messages).
162void stepper_output_interrupt(void)
163{
164 // read the clock
165 unsigned long now = micros();
166
167 // Compute the time elapsed since the last poll. This will correctly handle wrapround of
168 // the 32-bit long time value given the properties of twos-complement arithmetic.
169 unsigned long interval = now - last_interrupt_clock;
170 last_interrupt_clock = now;
171
172 // Update all the stepper channels. This may emit step signals or simply
173 // update the timing and state variables.
174 x_axis.pollForInterval(interval);
175 y_axis.pollForInterval(interval);
176 z_axis.pollForInterval(interval);
177 a_axis.pollForInterval(interval);
178}
179
180// ================================================================
181/// Polling function called from the main event loop to update the path model
182/// and update the step generators.
183void path_poll(unsigned long interval)
184{
185 x_path.pollForInterval(interval);
186 y_path.pollForInterval(interval);
187 z_path.pollForInterval(interval);
188 a_path.pollForInterval(interval);
189
190 // update the step generator for new targets
191 x_axis.setTarget(x_path.currentPosition());
192 x_axis.setSpeed(abs(x_path.currentVelocity()));
193
194 y_axis.setTarget(y_path.currentPosition());
195 y_axis.setSpeed(abs(y_path.currentVelocity()));
196
197 z_axis.setTarget(z_path.currentPosition());
198 z_axis.setSpeed(abs(z_path.currentVelocity()));
199
200 a_axis.setTarget(a_path.currentPosition());
201 a_axis.setSpeed(abs(a_path.currentVelocity()));
202}
203// ================================================================
204/// Return a Path object or NULL for each flag in the flag token. As a side effect, updates
205/// the source pointer, leaving it at the terminating null.
206static Path *path_flag_iterator(char **tokenptr)
207{
208 char flag = **tokenptr;
209 if (flag == 0) return NULL;
210 else {
211 (*tokenptr) += 1;
212 switch (flag) {
213 case 'x': return &x_path;
214 case 'y': return &y_path;
215 case 'z': return &z_path;
216 case 'a': return &a_path;
217 default: return NULL;
218 }
219 }
220}
221
222// ================================================================
223/// Process an input message. Unrecognized commands are silently ignored.
224/// argc - number of argument tokens
225/// argv - array of pointers to strings, one per token
226
227static void parse_input_message(int argc, char *argv[])
228{
229 if (argc == 0) return;
230
231 // Interpret the first token as a command symbol.
232 char *command = argv[0];
233
234 if (string_equal(command, "enable")) {
235 if (argc > 1) set_driver_enable(atoi(argv[1]));
236
237 } else if (string_equal(command, "a")) {
238 if (argc > 2) {
239 set_driver_enable(1);
240 char *flags = argv[1];
241 int channel = 0;
242 while (*flags) {
243 Path *p = path_flag_iterator(&flags);
244 if (p) {
245 if (argc > (channel+2)) {
246 p->setTarget(atol(argv[channel+2]));
247 channel++;
248 }
249 }
250 }
251 }
252 } else if (string_equal(command, "d")) {
253 if (argc > 2) {
254 set_driver_enable(1);
255 char *flags = argv[1];
256 int channel = 0;
257 while (*flags) {
258 Path *p = path_flag_iterator(&flags);
259 if (p) {
260 if (argc > (channel+2)) {
261 p->incrementTarget(atol(argv[channel+2]));
262 channel++;
263 }
264 }
265 }
266 }
267 } else if (string_equal(command, "v")) {
268 if (argc > 2) {
269 set_driver_enable(1);
270 char *flags = argv[1];
271 int channel = 0;
272 while (*flags) {
273 Path *p = path_flag_iterator(&flags);
274 if (p) {
275 if (argc > (channel+2)) {
276 p->setVelocity(atol(argv[channel+2]));
277 channel++;
278 }
279 }
280 }
281 }
282 } else if (string_equal(command, "g")) {
283 if (argc > 3) {
284 char *flags = argv[1];
285 float frequency = atof(argv[2]);
286 float damping_ratio = atof(argv[3]);
287 while (*flags) {
288 Path *p = path_flag_iterator(&flags);
289 if (p) p->setFreqDamping(frequency, damping_ratio);
290 }
291 }
292 } else if (string_equal(command, "l")) {
293 if (argc > 3) {
294 char *flags = argv[1];
295 float qdmax = atof(argv[2]);
296 float qddmax = atof(argv[3]);
297 while (*flags) {
298 Path *p = path_flag_iterator(&flags);
299 if (p) p->setLimits(qdmax, qddmax);
300 }
301 }
302 } else if (string_equal(command, "version")) {
303 send_message(version_string);
304
305 } else if (string_equal(command, "ping")) {
306 send_message("awake");
307
308 } else if (string_equal(command, "srate")) {
309 if (argc > 1) {
310 long value = atol(argv[1]);
311 // set the reporting interval (milliseconds -> microseconds)
312 if (value > 0) status_poll_interval = 1000*value;
313 else send_debug_message("invalid srate value");
314 }
315 }
316}
317
318/****************************************************************/
319/// Polling function to send status reports at periodic intervals.
320static void status_poll(unsigned long interval)
321{
322 static long timer = 0;
323 timer -= interval;
324
325 if (timer < 0) {
326 timer += status_poll_interval;
327
328 // send a time and position reading
329 long clock = micros();
330 long x = x_axis.currentPosition();
331 long y = y_axis.currentPosition();
332 long z = z_axis.currentPosition();
333 long a = a_axis.currentPosition();
334 send_message("txyza", clock, x, y, z, a);
335 }
336}
337
338/****************************************************************/
339/**** Standard entry points for Arduino system ******************/
340/****************************************************************/
341
342/// Standard Arduino initialization function to configure the system.
343void setup(void)
344{
345 // set up the CNC Shield I/O
346 digitalWrite(STEPPER_ENABLE_PIN, HIGH); // initialize drivers in disabled state
347 pinMode(STEPPER_ENABLE_PIN, OUTPUT);
348
349 pinMode(X_AXIS_STEP_PIN, OUTPUT);
350 pinMode(Y_AXIS_STEP_PIN, OUTPUT);
351 pinMode(Z_AXIS_STEP_PIN, OUTPUT);
352 pinMode(A_AXIS_STEP_PIN, OUTPUT);
353
354 pinMode(X_AXIS_DIR_PIN, OUTPUT);
355 pinMode(Y_AXIS_DIR_PIN, OUTPUT);
356 pinMode(Z_AXIS_DIR_PIN, OUTPUT);
357 pinMode(A_AXIS_DIR_PIN, OUTPUT);
358
359#ifdef LED_BUILTIN
360 pinMode(LED_BUILTIN, OUTPUT);
361#endif
362
363 // initialize the Serial port
364 Serial.begin(BAUD_RATE);
365
366 // set up the timer1 interrupt and attach it to the stepper motor controls
367 last_interrupt_clock = micros();
368 Timer1.initialize(100); // 100 microsecond intervals, e.g. 10kHz
369 Timer1.attachInterrupt(stepper_output_interrupt);
370
371 // send a wakeup message
372 send_message("awake");
373}
374
375/****************************************************************/
376/// Standard Arduino polling function to handle all I/O and periodic processing.
377/// This loop should never be allowed to stall or block so that all tasks can be
378/// constantly serviced.
379static unsigned long last_event_loop = 0;
380
381void loop(void)
382{
383 // read the clock
384 unsigned long now = micros();
385
386 // Compute the time elapsed since the last polling cycle. This will correctly handle wrapround of
387 // the 32-bit long time value given the properties of twos-complement arithmetic.
388 unsigned long interval = now - last_event_loop;
389 last_event_loop = now;
390
391 serial_input_poll();
392 status_poll(interval);
393 path_poll(interval);
394
395 // other polled tasks can go here
396}
397
398/****************************************************************/
399/****************************************************************/
Path Generator¶
The input filtering and path generation are implemented by Path objects. These are purely data operators, they do not directly interface to hardware.
1/// \file Path.h
2/// \brief Path generator for a single motor.
3///
4/// \copyright Written 2018 by Garth Zeglin <garthz@cmu.edu>. To the extent
5/// possible under law, the author has dedicated all copyright and related and
6/// neighboring rights to this software to the public domain worldwide. This
7/// software is distributed without any warranty. You should have received a
8/// copy of the CC0 Public Domain Dedication along with this software. If not,
9/// see <http://creativecommons.org/publicdomain/zero/1.0/>.
10///
11/// \details This class implements several smooth path generators intended for
12/// generating gestural motions on a single motor channel. It assumes a
13/// separate controller manages the step generator of closed-loop control of the
14/// physical hardware.
15
16#ifndef __PATH_H_INCLUDED__
17#define __PATH_H_INCLUDED__
18
19#include <math.h>
20
21// ================================================================
22class Path {
23
24private:
25 float q; ///< current model position, in dimensionless units (e.g. step or encoder counts)
26 float qd; ///< current model velocity in units/sec
27 float qdd; ///< current model acceleration, in units/sec/sec
28 float q_d; ///< current model target position in dimensionless units
29 float qd_d; ///< current model target velocity in dimensionless units/sec
30 float t; ///< elapsed model time, in seconds
31 float k; ///< proportional feedback gain, in (units/sec/sec)/(units), which is (1/sec^2)
32 float b; ///< derivative feedback gain, in (units/sec/sec)/(units/sec), which is (1/sec)
33 float qd_max; ///< maximum allowable speed in units/sec
34 float qdd_max; ///< maximum allowable acceleration in units/sec/sec
35
36public:
37
38 /// Main constructor.
39 Path();
40
41 /// Path-integration polling function to be called as often as possible,
42 /// typically from the main event loop. The interval argument is the duration
43 /// in microseconds since the last call.
44 void pollForInterval(unsigned long interval);
45
46 /// Add a signed offset to the target position. The units are dimensionless
47 /// 'steps'. If using a microstepping driver, these may be less than a
48 /// physical motor step.
49 void incrementTarget(long offset) { q_d += offset; }
50
51 /// Set the absolute target position in dimensionless units.
52 void setTarget(long position) { q_d = position; }
53
54 /// Set the target velocity in dimensionless units/second.
55 void setVelocity(long velocity) { qd_d = velocity; }
56
57 /// Return the current position in dimensionless units.
58 long currentPosition(void) { return (long) q; }
59
60 /// Return the current velocity in units/second.
61 long currentVelocity(void) { return (long) qd; }
62
63 /// Configure the second-order model gains.
64 void setPDgains(float k_new, float b_new) { k = k_new; b = b_new; }
65
66 /// Convenience function to set second order model gains in terms of natural frequency and damping ratio.
67 /// The frequency is in Hz, the damping ratio is 1.0 at critical damping.
68 void setFreqDamping(float freq, float damping) {
69 k = freq * freq * 4 * M_PI * M_PI;
70 b = 2 * sqrtf(k) * damping;
71 }
72
73 /// Configure the velocity and acceleration limits.
74 void setLimits(float qdmax, float qddmax) { qd_max = qdmax; qdd_max = qddmax; }
75};
76
77#endif //__PATH_H_INCLUDED__
1/// \file Path.cpp
2/// \brief Step and path generator for a single path motor
3///
4/// \copyright Written over 2014-2018 by Garth Zeglin <garthz@cmu.edu>. To the
5/// extent possible under law, the author has dedicated all copyright and
6/// related and neighboring rights to this software to the public domain
7/// worldwide. This software is distributed without any warranty. You should
8/// have received a copy of the CC0 Public Domain Dedication along with this
9/// software. If not, see <http://creativecommons.org/publicdomain/zero/1.0/>.
10
11#include <Arduino.h>
12#include <math.h>
13#include <stdint.h>
14
15#include "Path.h"
16
17//================================================================
18Path::Path()
19{
20 q = 0.0;
21 qd = 0.0;
22 qdd = 0.0;
23 q_d = 0.0;
24 qd_d = 0.0;
25 t = 0;
26 k = 4*M_PI*M_PI; // 1 Hz; freq = (1/2*pi) * sqrt(k/m); k = (freq*2*pi)^2
27 b = 1.0; // damping (would be 2*sqrt(k) for critical damping)
28 qd_max = 3500.0; // typical physical limit for 4x microstepping
29 qdd_max = 35000.0;
30}
31
32//================================================================
33// Path integration running from main event loop.
34void Path::pollForInterval(unsigned long interval)
35{
36 float dt = 1e-6 * interval;
37
38 // calcuate the derivatives
39 float qdd = k * (q_d - q) + b * (qd_d - qd);
40
41 // clamp the acceleration within range for safety
42 qdd = constrain(qdd, -qdd_max, qdd_max);
43
44 // integrate one time step
45 q += qd * dt;
46 qd += qdd * dt;
47 q_d += qd_d * dt; // integrate the target velocity into the target position
48 t += dt;
49
50 // clamp the model velocity within range for safety
51 qd = constrain(qd, -qd_max, qd_max);
52}
53//================================================================
Step Generator¶
The hardware drivers are operated by the step generator running on a fast timer interrupt. The step targets are updated from the path generator outputs in the main event loop.
Sources: Stepper.h, Stepper.cpp.
1/// \file Stepper.h
2/// \brief Step generator for a single stepper motor.
3///
4/// \copyright Written over 2014-2018 by Garth Zeglin <garthz@cmu.edu>. To the
5/// extent possible under law, the author has dedicated all copyright and
6/// related and neighboring rights to this software to the public domain
7/// worldwide. This software is distributed without any warranty. You should
8/// have received a copy of the CC0 Public Domain Dedication along with this
9/// software. If not, see <http://creativecommons.org/publicdomain/zero/1.0/>.
10///
11/// \details This class implements fast constant-velocity stepping. It is
12/// possible to use this directly, but the overall sketch pairs this with a
13/// interpolating path generator which frequently updates the position and
14/// velocity setpoints.
15
16#ifndef __STEPPER_H_INCLUDED__
17#define __STEPPER_H_INCLUDED__
18
19#include <stdint.h>
20
21/// An instance of this class manages generation of step and direction signals
22/// for one stepper motor.
23class Stepper {
24
25private:
26 /****************************************************************/
27 // The following instance variables may only be modified from a non-interrupt
28 // context, i.e., not within poll().
29
30 /// the I/O pins for this channel designated using the Arduino convention
31 uint8_t step_pin, dir_pin;
32
33 /// the target position in dimensionless step counts
34 long target;
35
36 /// the interval in microseconds between steps
37 unsigned long step_interval;
38
39 /****************************************************************/
40 // The following instance variables may be modified within poll() from an interrupt context.
41
42 /// the current position in dimensionless step counts
43 long position;
44
45 /// the time elapsed in microseconds since the last step occurred
46 unsigned long elapsed;
47
48 /****************************************************************/
49public:
50
51 /// Main constructor. The arguments are the pin numbers for the step and
52 /// direction outputs. Note: this does not initialize the underlying hardware.
53 Stepper(uint8_t step_pin, uint8_t dir_pin);
54
55 /// Step-generator polling function to be called as often as possible. This
56 /// is typically called from a timer interrupt. The interval argument is the
57 /// duration in microseconds since the last call.
58 void pollForInterval(unsigned long interval);
59
60 /// Add a signed offset to the target position. The units are dimensionless
61 /// 'steps'. If using a microstepping driver, these may be less than a
62 /// physical motor step.
63 void incrementTarget(long offset) { target += offset; }
64
65 /// Set the absolute target position.
66 void setTarget(long position) { target = position; }
67
68 /// Return the current position in dimensionless 'steps'.
69 long currentPosition(void) { return position; }
70
71 /// Set a constant speed in steps/second. Note that the value must be
72 /// non-zero and positive. The maximum rate available is a function of the
73 /// polling rate.
74 void setSpeed(int speed) {
75 // (1000000 microseconds/second) / (steps/second) = (microseconds/step)
76 if (speed > 0) {
77 step_interval = 1000000 / speed;
78 if (step_interval == 0) step_interval = 1;
79 }
80 }
81
82};
83
84#endif //__STEPPER_H_INCLUDED__
1/// \file Stepper.cpp
2/// \brief Step and path generator for a single stepper motor
3///
4/// \copyright Written over 2014-2018 by Garth Zeglin <garthz@cmu.edu>. To the
5/// extent possible under law, the author has dedicated all copyright and
6/// related and neighboring rights to this software to the public domain
7/// worldwide. This software is distributed without any warranty. You should
8/// have received a copy of the CC0 Public Domain Dedication along with this
9/// software. If not, see <http://creativecommons.org/publicdomain/zero/1.0/>.
10
11#include "Stepper.h"
12#include <Arduino.h>
13
14Stepper::Stepper(uint8_t _step_pin, uint8_t _dir_pin)
15{
16 step_pin = _step_pin;
17 dir_pin = _dir_pin;
18 position = 0;
19 target = 0;
20 elapsed = 0;
21
22 step_interval = 200; // 200 microseconds = 5000 steps/sec
23}
24//================================================================
25// Step generator running on fast timer interrupt.
26void Stepper::pollForInterval(unsigned long interval)
27{
28 // Accumulated the time elapsed since the step.
29 elapsed += interval;
30
31 if (elapsed >= step_interval) {
32 // reset the timer according to the target interval to produce a correct an
33 // average rate even if extra time has passed
34 elapsed -= step_interval;
35
36 // check whether to emit a step
37 if (position != target) {
38
39 // always set the direction to match the sign of motion
40 digitalWrite(dir_pin, (position < target) ? HIGH : LOW);
41
42 // emit a step
43 digitalWrite(step_pin, HIGH);
44
45 // update the position count
46 if (position < target) position++;
47 else position--;
48 digitalWrite(step_pin, LOW);
49 }
50 }
51}
52//================================================================