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.

Sources: Path.h, Path.cpp.

 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//================================================================