Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stepper motor support #70

Closed
techniq opened this issue Sep 29, 2016 · 31 comments
Closed

Stepper motor support #70

techniq opened this issue Sep 29, 2016 · 31 comments

Comments

@techniq
Copy link

techniq commented Sep 29, 2016

Any plans to support stepper motors, especially driver mode?

I have an Arduino johnny-five project I'm looking to port to a Raspberry PI.

@nebrius
Copy link
Owner

nebrius commented Sep 30, 2016

Not directly, no, because the Raspberry Pi can't directly control a stepper motor. Stepper motors require very specially timed pulses, something the Raspberry Pi can't do unfortunately. To get the timing right, you either have to be running on bare-metal without an OS (such as the Arduino), or you have to use hardware PWM. The Raspberry Pi doesn't have enough PWM ports on it to drive a stepper motor though, and since it runs an OS, we can't fake it with GPIO and have it be stable because the OS can interrupt timers.

That said, pretty much any time you want to use a stepper motor with any microcontroller, not just the Raspberry Pi, the preferred way is to use an extension board, such as this one here. These boards are connected to the Raspberry Pi using I2C, which is already supported by raspi-io.

Let me know if you have any other questions!

@techniq
Copy link
Author

techniq commented Sep 30, 2016

Thanks for the quick reply @nebrius .

I'm currently using an Arduino and a DRV8825 with johnny-five. Using the DRV8825, I'm able to step a motor "manually" by turning a pin on then off, like so:

board.on('ready', function() {
  board.pinMode(7, board.MODES.OUTPUT);
  let spinPulse = true;
  setInterval(() => {
    board.digitalWrite(7, spinPulse ? 1 : 0);
    spinPulse = !spinPulse;
  }, 0);

  board.pinMode(8, board.MODES.OUTPUT);
  let direction = true;
  setInterval(() => {
    board.digitalWrite(8, direction ? 1 : 0);
    direction = !direction;
  }, 1000);
});

What this does is step the stepper as fast as possible using pin 7, and also change the direction every second using pin 8.

I've recently started using johnny-five's Stepper component with the DRIVER type, which has a nicer API and includes speed (rpm), acceleration, deceleration, and more, but the principal is the same.

  var stepper = new five.Stepper({
    type: five.Stepper.TYPE.DRIVER,
    stepsPerRev: 200,
    pins: {
      step: 7,
      dir: 8 
    }
  });

  stepper.rpm(20).ccw().step(100, function {
    // ...
  });

My question is, could raspi-io support just the Stepper type Stepper.TYPE.DRIVER?.

Also, I've seen https://github.com/nkolban/node-stepper-wiringpi which looks to provide stepper motor control for Raspberry PI. Would this be helpful in adding support?

@dtex
Copy link

dtex commented Sep 30, 2016

@techniq With the DRV8825 this is all just digital writes. Why wouldn't it work with rasp-io?

@techniq
Copy link
Author

techniq commented Sep 30, 2016

@dtex I'll be honest, I haven't tried just yet.

I'm currently driving my Arduino with my Raspberry PI, but I saw this, this, and stepper support - no and assumed it wouldn't work (yet).

@dtex
Copy link

dtex commented Sep 30, 2016

Hmmm, I'll need to try it out myself as I haven't actually done it yet either.

@RonB
Copy link

RonB commented Dec 31, 2016

Hi,
I'm using an adafruit motorhat and would also like support for steppers. I would be willing to try and port the python library of adafruit to add to rasp-io and johnny5, allthough i am really new to this kind of programming.
Is this a good idea and if so, anyone care to help?
Ronald

@nebrius
Copy link
Owner

nebrius commented Jan 3, 2017

I'm using an adafruit motorhat and would also like support for steppers. I would be willing to try and port the python library of adafruit to add to rasp-io and johnny5, allthough i am really new to this kind of programming.
Is this a good idea and if so, anyone care to help?

Looking at the product, it should already work with the Raspberry Pi. The Adafruit Motor HAT uses I2C to communicate, which is already supported in raspi-io. Digging deeper, the HAT uses the PCA9685, which is already a supported expander in Johnny-Five.

So, there's nothing to port, no assembly needed! :)

EDIT: Here's the link to the example for this controller.

@rwaldron
Copy link
Collaborator

rwaldron commented Jan 3, 2017

@nebrius I think we still need a Stepper class controller (which would use the expander internally), but that's blocked by the Stepper class rewrite, which is necessary because it brings in controller definition support. However! Ignore all of that, one could definitely use an Expander object to implement a Stepper plugin that's specific to this shield.

@RonB
Copy link

RonB commented Jan 5, 2017

@nebrius and @rwaldron thank you for your response.

I realized that the pca9685 is already supported regarding the i2c communication, but how that ic has to write to the two motordriver ic's TB6612 needs to be addressed I think. In the Motor.js class of Johnny5 the ADAFRUIT shield is available, for the Stepper class it is not.

image

I've come this far:

`var raspi = require("raspi-io");
var five = require("johnny-five");
var io = new raspi();
var board = new five.Board({io: io});

board.on("ready", function () {

var expander = new five.Expander({
    controller: "PCA9685",
    address: 0x60
});

var adafruitmotorhat = new five.Board.Virtual(expander);
var stepper = new five.Stepper({
    type: five.Stepper.TYPE.DRIVER,
    stepsPerRev: 200,
    board: adafruitmotorhat,
    pins: {
        step: 11, //??
        dir: 12 //??
    }
});
// error stepper not supported because raspi-io io.MODES.STEPPER is not available

});`

but as the rasp-io board has stepper support turned off in general it will still report 'Stepper not supprted.'

Can either off you point me in the right direction on where and how to write a stepper plugin specifically for this board?

//==== the other path i've walked... ==//

Another attempt I've made was to rewrite the python library from Adafruit to a node module by using the pca9685 node driver and translate the python ADAFRUIT Stepper Class. Unfortunately I'm too new in this kind of thing to get it working and I also beleive that this effort shouldn't be necessary. Just for information purposes I've included my poor attempt here:

`
var i2cBus = require("i2c-bus");
var Pca9685Driver = require("pca9685").Pca9685Driver;

// initialize Stepper

function Stepper(options) {

options = options || {};
options.motornum = options.motornum || 1;
options.steps = options.steps || 200;
options.address = options.adress || 0x60;
options.frequency = options.frequency || 50;

var stepper = {};

var pwmOptions = {
    frequency: options.frequency,
    address: options.address,
    i2c: i2cBus.openSync(1),
    debug: true
};

stepper.pwm = new Pca9685Driver(pwmOptions, function (err) {
    if (err) {
        console.error("Error initializing PCA9685");
        process.exit(-1);
    }
});

// statics
stepper.MICROSTEPS = 8;
stepper.MICROSTEP_CURVE = [0, 50, 98, 142, 180, 212, 236, 250, 255];
stepper.FORWARD = 1;
stepper.BACKWARD = 2;
stepper.BRAKE = 3;
stepper.RELEASE = 4;
stepper.SINGLE = 1;
stepper.DOUBLE = 2;
stepper.INTERLEAVE = 3;
stepper.MICROSTEP = 4;
// options
stepper.revsteps = options.steps;
stepper.motornum = options.motornum;
stepper.sec_per_step = 0.1;
stepper.steppingcounter = 0;
stepper.currentstep = 0;

// intiialize stepper
if (options.motornum === 1) {
    stepper.PWMA = 8;
    stepper.AIN2 = 9;
    stepper.AIN1 = 10;
    stepper.PWMB = 13;
    stepper.BIN2 = 12;
    stepper.BIN1 = 11;
} else if (options.motornum === 2) {
    stepper.PWMA = 2;
    stepper.AIN2 = 3;
    stepper.AIN1 = 4;
    stepper.PWMB = 7;
    stepper.BIN2 = 6;
    stepper.BIN1 = 5;
} else {
    throw Error('MotorHAT Stepper must be between 1 and 2 inclusive');
}

stepper.setSpeed = function (rpm) {
    this.sec_per_step = 60.0 / (this.revsteps * rpm);
    this.steppingcounter = 0;
};

stepper.oneStep = function (dir, style) {

    var pwm_a = 255, pwm_b = 255;

    // first determine what sort of stepping procedure we're up to
    if (style === this.SINGLE) {

        if ((this.currentstep / (this.MICROSTEPS / 2)) % 2) {
            // we're at an odd step, weird
            if (dir === this.FORWARD) {
                this.currentstep += this.MICROSTEPS / 2;
            } else {
                this.currentstep -= this.MICROSTEPS / 2;
            }
        } else {
            // go to next even step
            if (dir === this.FORWARD) {
                this.currentstep += this.MICROSTEPS;
            } else {
                this.currentstep -= this.MICROSTEPS;
            }
        }
    }
    
    if (style === this.DOUBLE) {
        if (!(this.currentstep / (this.MICROSTEPS / 2) % 2)) {
            // we're at an even step, weird
            if (dir === this.FORWARD) {
                this.currentstep += this.MICROSTEPS / 2;
            } else {
                this.currentstep -= this.MICROSTEPS / 2;
            }
        } else {
            // go to next odd step
            if (dir === this.FORWARD) {
                this.currentstep += this.MICROSTEPS;
            } else {
                this.currentstep -= this.MICROSTEPS;
            }
        }
    }

    if (style === this.INTERLEAVE) {
        if (dir === this.FORWARD) {
            this.currentstep += this.MICROSTEPS / 2;
        } else {
            this.currentstep -= this.MICROSTEPS / 2;
        }
    }

    if (style === this.MICROSTEP) {
        if (dir === this.FORWARD) {
            this.currentstep += 1;
        } else {
            this.currentstep -= 1;

            // go to next 'step' and wrap around
            this.currentstep += this.MICROSTEPS * 4;
            this.currentstep %= this.MICROSTEPS * 4;
        }
        var pwm_a = 0, pwm_b = 0;
        if (this.currentstep >= 0 && this.currentstep < this.MICROSTEPS) {
            pwm_a = this.MICROSTEP_CURVE[this.MICROSTEPS - this.currentstep];
            pwm_b = this.MICROSTEP_CURVE[this.currentstep];
        } else if (this.currentstep >= this.MICROSTEPS && this.currentstep < this.MICROSTEPS * 2) {
            pwm_a = this.MICROSTEP_CURVE[this.currentstep - this.MICROSTEPS];
            pwm_b = this.MICROSTEP_CURVE[this.MICROSTEPS * 2 - this.currentstep];
        } else if (this.currentstep >= this.MICROSTEPS * 2 && this.currentstep < this.MICROSTEPS * 3) {
            pwm_a = this.MICROSTEP_CURVE[this.MICROSTEPS * 3 - this.currentstep];
            pwm_b = this.MICROSTEP_CURVE[this.currentstep - this.MICROSTEPS * 2];
        } else if (this.currentstep >= this.MICROSTEPS * 3 && this.currentstep < this.MICROSTEPS * 4) {
            pwm_a = this.MICROSTEP_CURVE[this.currentstep - this.MICROSTEPS * 3];
            pwm_b = this.MICROSTEP_CURVE[this.MICROSTEPS * 4 - this.currentstep];
        }
    }
    // go to next 'step' and wrap around
    this.currentstep += this.MICROSTEPS * 4;
    this.currentstep %= this.MICROSTEPS * 4;

    // only really used for microstepping, otherwise always on!
    this.pwm.setDutyCycle(this.PWMA, pwm_a * 16);
    this.pwm.setDutyCycle(this.PWMB, pwm_b * 16);

    // set up coil energizing!
    var coils = [0, 0, 0, 0];

    if (style === this.MICROSTEP) {
        if (this.currentstep >= 0 && this.currentstep < this.MICROSTEPS) {
            coils = [1, 1, 0, 0];
        } else if (this.currentstep >= this.MICROSTEPS && this.currentstep < this.MICROSTEPS * 2) {
            coils = [0, 1, 1, 0];
        } else if (this.currentstep >= this.MICROSTEPS * 2 && this.currentstep < this.MICROSTEPS * 3) {
            coils = [0, 0, 1, 1];
        } else if (this.currentstep >= this.MICROSTEPS * 3 && this.currentstep < this.MICROSTEPS * 4) {
            coils = [1, 0, 0, 1];
        }
    } else {
        var step2coils = [
            [1, 0, 0, 0],
            [1, 1, 0, 0],
            [0, 1, 0, 0],
            [0, 1, 1, 0],
            [0, 0, 1, 0],
            [0, 0, 1, 1],
            [0, 0, 0, 1],
            [1, 0, 0, 1]
        ];
        coils = step2coils[this.currentstep / (this.MICROSTEPS / 2)];
    }
    //print "coils state = " + str(coils)
    this.pwm[coils[0] ? 'channelOn' : 'channelOff'](this.AIN2);
    this.pwm[coils[1] ? 'channelOn' : 'channelOff'](this.BIN1);
    this.pwm[coils[2] ? 'channelOn' : 'channelOff'](this.AIN1);
    this.pwm[coils[3] ? 'channelOn' : 'channelOff'](this.BIN2);
    console.log('currentstep', this.currentstep, coils);
    return this.currentstep;
};

stepper.step = function (steps, direction, stepstyle, callback) {
    var s_per_s = this.sec_per_step;
    var lateststep = 0;

    if (stepstyle === this.INTERLEAVE) {
        s_per_s = s_per_s / 2.0;
    }
    if (stepstyle === this.MICROSTEP) {
        s_per_s /= this.MICROSTEPS;
        steps *= this.MICROSTEPS;
    }
    console.log(s_per_s, " sec per step");
    var stepsLeft = steps;
    for (s = 0; s < steps; s++) {
        
        // If we should already be in the middle of a movement, cancel it.
        if (this._timerId !== null) {
          clearInterval(this._timerId);
        }
        // Note: A question comes up on scheduling the first move immediately
        // as opposed to a stepDelay later.  We should always pause at least
        // one stepDelay even for the first step.  Consider what would happen
        // if we didn't.  Imagine we issued a step(1) and then a step(-1)
        // immediately on "completion" of the step(1).  We would imagine that
        // we should end up exactly where we started (which is correct).  However
        // if we don't wait at least one stepDelay then the call to step(-1) could
        // happen before the completion of a stepDelay period and we would now be
        // executing a -1 step even though the +1 step hadn't completed which
        // would not allow us to end up at the same position as that at which
        // we started.
        this._timerId = setInterval(function() {
            // If we have moved the correct number of steps then cancel the timer and return
            // after invoking a callback (if one has been supplied).
            if (stepsLeft <= 0) {
              clearInterval(this._timerId);
              this._timerId = null;
              if (callback) {
                callback();
              }
              return;
            } // End of stepsLeft <= 0
        });
        
        lateststep = this.oneStep(direction, stepstyle);
        stepsLeft--;
        
        if (stepstyle === this.MICROSTEP) {
            // this is an edge case, if we are in between full steps, lets just keep going
            // so we end on a full step
            while (lateststep !== 0 && lateststep !== this.MICROSTEPS) {
                lateststep = this.oneStep(direction, stepstyle);
                // delay...
            }
        }
    }
};
return stepper;

}

module.exports = Stepper;`

@rwaldron
Copy link
Collaborator

rwaldron commented Jan 5, 2017

In the Motor.js class of Johnny5 the ADAFRUIT shield is available, for the Stepper class it is not.

This is what I was referring to w/r to Stepper class undergoing changes for new "controller definition" support.

// error stepper not supported because raspi-io io.MODES.STEPPER is not available

That's correct, because the Stepper class is still a very old implementation that pre-dates the decoupling that "controller definitions" provides.

the other path i've walked...

A similar thing could be made using an Expander instance to implement a Stepper class. I haven't actually tested this, but I'm pretty sure it should work or is very close:

  1. Make a file called stepper-hat.js
var five = require("johnny-five");
var Board = five.Board;
var Fn = five.Fn;
var priv = new Map();
var pins = [
  {
    PWMA: 8,
    AIN2: 9,
    AIN1: 10,
    PWMB: 13,
    BIN2: 12,
    BIN1: 11,
  },
  {
    PWMA: 2,
    AIN2: 3,
    AIN1: 4,
    PWMB: 7,
    BIN2: 6,
    BIN1: 5,
  }
];

var coilstates = [
  [1, 0, 0, 0],
  [1, 1, 0, 0],
  [0, 1, 0, 0],
  [0, 1, 1, 0],
  [0, 0, 1, 0],
  [0, 0, 1, 1],
  [0, 0, 0, 1],
  [1, 0, 0, 1],
];

var motors = [
  1, 2
];

function Stepper(opts) {
  if (!(this instanceof Stepper)) {
    return new Stepper(opts);
  }

  Board.Component.call(
    this, opts = Board.Options(opts)
  );

  var address = opts.address || 0x60;
  var frequency = opts.frequency || 1600;
  var motor = opts.motor || 1;
  var pins = Object.assign({}, pins[motor - 1]);
  var rpm = opts.rpm || 30;
  var steps = opts.steps || 200;

  var state = {
    address: address,
    currentstep: 0,
    frequency: frequency,
    interval: null,
    motor: motor,
    pins: pins,
    secondsPerStep: 0,
    steps: steps,
  };

  state.expander = Expander.get({
    address: state.address,
    bus: 1,
    controller: "PCA9685",
    frequency: state.frequency,
  });

  priv.set(this, state);

  Object.defineProperties(this, {
    pins: {
      get: function() {
        return state.pins;
      }
    }
  });

  this.speed(rpm);
}

Stepper.FORWARD = 1;
Stepper.BACKWARD = 2;

Stepper.SINGLE = 1;
Stepper.DOUBLE = 2;
Stepper.INTERLEAVE = 3;
Stepper.MICROSTEP = 4;

Stepper.MICROSTEPS_4  = 4;
Stepper.MICROSTEPS_8  = 8;
Stepper.MICROSTEPS_16 = 16;
Stepper.MICROSTEPS_24 = 24;
Stepper.MICROSTEPS_32 = 32;

Stepper.MICROSTEP_CURVE = [
  0, 50, 98, 142, 180, 212, 236, 250, 255
];

Stepper.prototype.speed = function(rpm) {
  var state = priv.get(this);
  state.secondsPerStep = 60 / (state.steps * rpm);
  return this;
};

Stepper.prototype.step = function(direction, style) {
  var pwm = { a: 0xFF, b: 0xFF };

  if (style === Stepper.SINGLE) {

    if ((state.currentstep / Stepper.MICROSTEPS_4) % 2) {
      if (direction === Stepper.FORWARD) {
        state.currentstep += Stepper.MICROSTEPS_4;
      } else {
        state.currentstep -= Stepper.MICROSTEPS_4;
      }
    } else {
      // go to next even step
      if (direction === Stepper.FORWARD) {
        state.currentstep += Stepper.MICROSTEPS_8;
      } else {
        state.currentstep -= Stepper.MICROSTEPS_8;
      }
    }
  }

  if (style === Stepper.DOUBLE) {
    if (!(state.currentstep / Stepper.MICROSTEPS_4 % 2)) {
      // we're at an even step, weird
      if (direction === Stepper.FORWARD) {
        state.currentstep += Stepper.MICROSTEPS_4;
      } else {
        state.currentstep -= Stepper.MICROSTEPS_4;
      }
    } else {
      // go to next odd step
      if (direction === Stepper.FORWARD) {
        state.currentstep += Stepper.MICROSTEPS_8;
      } else {
        state.currentstep -= Stepper.MICROSTEPS_8;
      }
    }
  }

  if (style === Stepper.INTERLEAVE) {
    if (direction === Stepper.FORWARD) {
      state.currentstep += Stepper.MICROSTEPS_4;
    } else {
      state.currentstep -= Stepper.MICROSTEPS_4;
    }
  }

  if (style === Stepper.MICROSTEP) {
    if (direction === Stepper.FORWARD) {
      state.currentstep += 1;
    } else {
      state.currentstep -= 1;

      // go to next 'step' and wrap around
      state.currentstep += Stepper.MICROSTEPS_32;
      state.currentstep %= Stepper.MICROSTEPS_32;
    }

    pwm.a = 0;
    pwm.b = 0;

    if (state.currentstep >= 0 &&
        state.currentstep < Stepper.MICROSTEPS_8) {

      pwm.a = Stepper.MICROSTEP_CURVE[(Stepper.MICROSTEPS_8 - state.currentstep) | 0];
      pwm.b = Stepper.MICROSTEP_CURVE[state.currentstep];

    } else if (state.currentstep >= Stepper.MICROSTEPS_8 &&
                state.currentstep < Stepper.MICROSTEPS_16) {

      pwm.a = Stepper.MICROSTEP_CURVE[(state.currentstep - Stepper.MICROSTEPS_8) | 0];
      pwm.b = Stepper.MICROSTEP_CURVE[(Stepper.MICROSTEPS_16 - state.currentstep) | 0];

    } else if (state.currentstep >= Stepper.MICROSTEPS_16 &&
                state.currentstep < Stepper.MICROSTEPS_24) {

      pwm.a = Stepper.MICROSTEP_CURVE[(Stepper.MICROSTEPS_24 - state.currentstep) | 0];
      pwm.b = Stepper.MICROSTEP_CURVE[(state.currentstep - Stepper.MICROSTEPS_16) | 0];

    } else if (state.currentstep >= Stepper.MICROSTEPS_24 &&
                state.currentstep < Stepper.MICROSTEPS_32) {
      pwm.a = Stepper.MICROSTEP_CURVE[(state.currentstep - Stepper.MICROSTEPS_24) | 0];
      pwm.b = Stepper.MICROSTEP_CURVE[(Stepper.MICROSTEPS_32 - state.currentstep) | 0];

    }
  }

  state.currentstep += Stepper.MICROSTEPS_32;
  state.currentstep %= Stepper.MICROSTEPS_32;

  state.expander.pwmWrite(this.pins.PWMA, pwm.a * 16);
  state.expander.pwmWrite(this.pins.PWMB, pwm.b * 16);

  // set up coil energizing!
  var coils = [0, 0, 0, 0];

  if (style === Stepper.MICROSTEP) {
    if (state.currentstep >= 0 &&
        state.currentstep < Stepper.MICROSTEPS_8) {

      coils[0] = 1;
      coils[1] = 1;

    } else if (state.currentstep >= Stepper.MICROSTEPS_8 &&
                state.currentstep < Stepper.MICROSTEPS_16) {

      coils[1] = 1;
      coils[2] = 1;

    } else if (state.currentstep >= Stepper.MICROSTEPS_16 &&
                state.currentstep < Stepper.MICROSTEPS_24) {

      coils[2] = 1;
      coils[3] = 1;

    } else if (state.currentstep >= Stepper.MICROSTEPS_24 &&
                state.currentstep < Stepper.MICROSTEPS_32) {

      coils[0] = 1;
      coils[1] = 3;

    }
  } else {
    coils = coilstates[(state.currentstep / (Stepper.MICROSTEPS_4)) | 0];
  }

  state.expander.digitalWrite(this.pins.AIN2, coils[0]);
  state.expander.digitalWrite(this.pins.BIN1, coils[1]);
  state.expander.digitalWrite(this.pins.AIN1, coils[2]);
  state.expander.digitalWrite(this.pins.BIN2, coils[3]);

  return state.currentstep;
};

Stepper.prototype.steps = function(numberOfSteps, direction, style, callback) {
  var state = priv.get(this);
  var sec = state.secondsPerStep;
  var lateststep = 0;

  if (state.interval) {
    throw new Error("Stepper is active!");
  }

  if (style === Stepper.INTERLEAVE) {
    sec /= 2;
  }

  if (style === Stepper.MICROSTEP) {
    sec /= Stepper.MICROSTEPS_8;
    numberOfSteps *= Stepper.MICROSTEPS_8;
  }

  var ms = sec * 1000;

  state.interval = setInterval(function() {
    if (numberOfSteps > 0) {
      lateststep = this.step(direction, style);
    }

    // User code may have _really_ slow steps and
    // we always want to call the callback AFTER the
    // stepper has completed a movement
    if (numberOfSteps === 0) {
      clearInterval(state.interval);

      if (style === Stepper.MICROSTEP) {
        // This is the edge case discussed in
        // Adafruit_MotorHAT.py...

        // If the last latest step was BETWEEN a
        // full step, then we'll take another step.
        //
        // But we're not done with this movement,
        // so we don't call the callback yet.
        if (lateststep !== 0 &&
            lateststep !== Stepper.MICROSTEPS_8) {
          this.steps(1, direction, style, callback);
        }
      } else {

        callback();
      }
    }

    numberOfSteps--;
  }.bind(this), ms);

  return this;
};

module.exports = Stepper;
  1. Your main program:
var five = require("johnny-five");
var raspi = require("raspi-io");
var Stepper = require("./stepper-hat.js");

var board = new five.Board({ io: new raspi() });

board.on("ready", function() {
  var stepper = new Stepper({ bus: 1, address: 0x60 });

  // Defaults to: 200 steps, 30 rpm
  stepper.steps(200, Stepper.FORWARD, Stepper.SINGLE, function() {
    console.log("Hopefully this fires when the complete move is done");
  });
});

(I only used the address 0x60 in that code because you did, otherwise I would've used the default PCA9685 address of 0x40. Change if necessary.)

@RonB
Copy link

RonB commented Jan 6, 2017

@rwaldron
Wow, this is brilliant. I've had to make some adjustments but it works... Very nice, thanks a lot.

Any plans to include this in the standard Johnny5 lib? I'm using node-red so I want to include it in node-red-contrib-gpio :-)

` var five = require("johnny-five");
var Board = five.Board;
var Fn = five.Fn;
var priv = new Map();
var pinassignment = [
{
PWMA: 8,
AIN2: 9,
AIN1: 10,
PWMB: 13,
BIN2: 12,
BIN1: 11
},
{
PWMA: 2,
AIN2: 3,
AIN1: 4,
PWMB: 7,
BIN2: 6,
BIN1: 5
}
];

var coilstates = [
    [1, 0, 0, 0],
    [1, 1, 0, 0],
    [0, 1, 0, 0],
    [0, 1, 1, 0],
    [0, 0, 1, 0],
    [0, 0, 1, 1],
    [0, 0, 0, 1],
    [1, 0, 0, 1]
];

var motors = [
    1, 2
];

function Stepper(opts) {
    if (!(this instanceof Stepper)) {
        return new Stepper(opts);
    }

    Board.Component.call(
            this, opts = Board.Options(opts)
            );

    var address = opts.address || 0x60;
    var frequency = opts.frequency || 1600;
    var motor = opts.motor || 1;
    var pins = Object.assign({}, pinassignment[motor - 1]);
    var rpm = opts.rpm || 30;
    var steps = opts.steps || 200;

    var state = {
        address: address,
        currentstep: 0,
        frequency: frequency,
        interval: null,
        motor: motor,
        pins: pins,
        secondsPerStep: 0,
        steps: steps
    };

    state.expander = new five.Expander.get({
        address: state.address,
        bus: 1,
        controller: "PCA9685",
        frequency: state.frequency
    });

    priv.set(this, state);

    Object.defineProperties(this, {
        pins: {
            get: function () {
                return state.pins;
            }
        }
    });

    this.speed(rpm);
}

Stepper.FORWARD = 1;
Stepper.BACKWARD = 2;

Stepper.SINGLE = 1;
Stepper.DOUBLE = 2;
Stepper.INTERLEAVE = 3;
Stepper.MICROSTEP = 4;

Stepper.MICROSTEPS_4 = 4;
Stepper.MICROSTEPS_8 = 8;
Stepper.MICROSTEPS_16 = 16;
Stepper.MICROSTEPS_24 = 24;
Stepper.MICROSTEPS_32 = 32;

Stepper.MICROSTEP_CURVE = [
    0, 50, 98, 142, 180, 212, 236, 250, 255
];

Stepper.prototype.speed = function (rpm) {

    var state = priv.get(this);
    state.secondsPerStep = 60 / (state.steps * rpm);
    return this;
};

Stepper.prototype.step = function (direction, style) {

    var state = priv.get(this);
    var pwm = {a: 0xFF, b: 0xFF};

    if (style === Stepper.SINGLE) {

        if ((state.currentstep / Stepper.MICROSTEPS_4) % 2) {
            if (direction === Stepper.FORWARD) {
                state.currentstep += Stepper.MICROSTEPS_4;
            } else {
                state.currentstep -= Stepper.MICROSTEPS_4;
            }
        } else {
            // go to next even step
            if (direction === Stepper.FORWARD) {
                state.currentstep += Stepper.MICROSTEPS_8;
            } else {
                state.currentstep -= Stepper.MICROSTEPS_8;
            }
        }
    }

    if (style === Stepper.DOUBLE) {
        if (!(state.currentstep / Stepper.MICROSTEPS_4 % 2)) {
            // we're at an even step, weird
            if (direction === Stepper.FORWARD) {
                state.currentstep += Stepper.MICROSTEPS_4;
            } else {
                state.currentstep -= Stepper.MICROSTEPS_4;
            }
        } else {
            // go to next odd step
            if (direction === Stepper.FORWARD) {
                state.currentstep += Stepper.MICROSTEPS_8;
            } else {
                state.currentstep -= Stepper.MICROSTEPS_8;
            }
        }
    }

    if (style === Stepper.INTERLEAVE) {
        if (direction === Stepper.FORWARD) {
            state.currentstep += Stepper.MICROSTEPS_4;
        } else {
            state.currentstep -= Stepper.MICROSTEPS_4;
        }
    }

    if (style === Stepper.MICROSTEP) {
        if (direction === Stepper.FORWARD) {
            state.currentstep += 1;
        } else {
            state.currentstep -= 1;

            // go to next 'step' and wrap around
            state.currentstep += Stepper.MICROSTEPS_32;
            state.currentstep %= Stepper.MICROSTEPS_32;
        }

        pwm.a = 0;
        pwm.b = 0;

        if (state.currentstep >= 0 &&
                state.currentstep < Stepper.MICROSTEPS_8) {

            pwm.a = Stepper.MICROSTEP_CURVE[(Stepper.MICROSTEPS_8 - state.currentstep) | 0];
            pwm.b = Stepper.MICROSTEP_CURVE[state.currentstep];

        } else if (state.currentstep >= Stepper.MICROSTEPS_8 &&
                state.currentstep < Stepper.MICROSTEPS_16) {

            pwm.a = Stepper.MICROSTEP_CURVE[(state.currentstep - Stepper.MICROSTEPS_8) | 0];
            pwm.b = Stepper.MICROSTEP_CURVE[(Stepper.MICROSTEPS_16 - state.currentstep) | 0];

        } else if (state.currentstep >= Stepper.MICROSTEPS_16 &&
                state.currentstep < Stepper.MICROSTEPS_24) {

            pwm.a = Stepper.MICROSTEP_CURVE[(Stepper.MICROSTEPS_24 - state.currentstep) | 0];
            pwm.b = Stepper.MICROSTEP_CURVE[(state.currentstep - Stepper.MICROSTEPS_16) | 0];

        } else if (state.currentstep >= Stepper.MICROSTEPS_24 &&
                state.currentstep < Stepper.MICROSTEPS_32) {
            pwm.a = Stepper.MICROSTEP_CURVE[(state.currentstep - Stepper.MICROSTEPS_24) | 0];
            pwm.b = Stepper.MICROSTEP_CURVE[(Stepper.MICROSTEPS_32 - state.currentstep) | 0];

        }
    }

    state.currentstep += Stepper.MICROSTEPS_32;
    state.currentstep %= Stepper.MICROSTEPS_32;

    state.expander.pwmWrite(this.pins.PWMA, pwm.a * 16);
    state.expander.pwmWrite(this.pins.PWMB, pwm.b * 16);

    // set up coil energizing!
    var coils = [0, 0, 0, 0];

    if (style === Stepper.MICROSTEP) {
        if (state.currentstep >= 0 &&
                state.currentstep < Stepper.MICROSTEPS_8) {

            coils[0] = 1;
            coils[1] = 1;

        } else if (state.currentstep >= Stepper.MICROSTEPS_8 &&
                state.currentstep < Stepper.MICROSTEPS_16) {

            coils[1] = 1;
            coils[2] = 1;

        } else if (state.currentstep >= Stepper.MICROSTEPS_16 &&
                state.currentstep < Stepper.MICROSTEPS_24) {

            coils[2] = 1;
            coils[3] = 1;

        } else if (state.currentstep >= Stepper.MICROSTEPS_24 &&
                state.currentstep < Stepper.MICROSTEPS_32) {

            coils[0] = 1;
            coils[1] = 3;

        }
    } else {
        coils = coilstates[(state.currentstep / (Stepper.MICROSTEPS_4)) | 0];
    }

    state.expander.digitalWrite(this.pins.AIN2, coils[0]);
    state.expander.digitalWrite(this.pins.BIN1, coils[1]);
    state.expander.digitalWrite(this.pins.AIN1, coils[2]);
    state.expander.digitalWrite(this.pins.BIN2, coils[3]);

    return state.currentstep;
};

Stepper.prototype.steps = function (numberOfSteps, direction, style, callback) {

    var state = priv.get(this);
    var sec = state.secondsPerStep;
    var lateststep = 0;

    if (state.interval) {
        throw new Error("Stepper is active!");
    }

    if (style === Stepper.INTERLEAVE) {
        sec /= 2;
    }

    if (style === Stepper.MICROSTEP) {
        sec /= Stepper.MICROSTEPS_8;
        numberOfSteps *= Stepper.MICROSTEPS_8;
    }

    var ms = sec * 1000;

    state.interval = setInterval(function () {

        if (numberOfSteps > 0) {
            lateststep = this.step(direction, style);
        }

        // User code may have _really_ slow steps and
        // we always want to call the callback AFTER the
        // stepper has completed a movement
        if (numberOfSteps === 0) {
            clearInterval(state.interval);
            state.interval = undefined;

            if (style === Stepper.MICROSTEP) {
                // This is the edge case discussed in
                // Adafruit_MotorHAT.py...

                // If the last latest step was BETWEEN a
                // full step, then we'll take another step.
                //
                // But we're not done with this movement,
                // so we don't call the callback yet.
                if (lateststep !== 0 &&
                        lateststep !== Stepper.MICROSTEPS_8) {
                    this.steps(1, direction, style, callback);
                }
            } else {
                callback();
            }
        }

        numberOfSteps--;
    }.bind(this), ms);

    return this;
};

module.exports = Stepper;`

@nebrius
Copy link
Owner

nebrius commented Jan 6, 2017

Any plans to include this in the standard Johnny5 lib? I'm using node-red so I want to include it in node-red-contrib-gpio :-)

Once the stepper rewrite lands in Johnny-Five, my understanding is that this shouldn't be necessary. Until that lands, there's not really a place for this in raspi-io or J5, based on how the division between IO plugins and J5 works.

That said, it may be possible to get full stepper support into raspi-io itself, now that we have support for software PWM. I'd need to do some testing to see if software PWM is stable enough for stepper motors, but I have the feeling it may be from what I've seen of it so far.

So let's game this out. If we decide to get this into Raspi IO, the first thing we need to do is add base stepper support into Raspi IO. This is a unique case where we don't need to create a new Raspi.js plugin, because stepper motors aren't actually a unique peripheral (they're just PWM, not an all-together new peripheral). If you haven't seen the architecture discussion in CONTRIBUTING, I recommend taking a peak.

This means that we would need to modify these lines over in raspi-io-core that first checks if software PWM is enabled (we'll probably need to store a flag on the Raspi IO Core instance, as it's currently just a parameter in the constructor), and then if software PWM is enabled, it does the right PWM things, otherwise it throws an error saying software PWM needs to be enabled.

Once that's done, my understanding is that the expander should just work, right @rwaldron?

@rwaldron
Copy link
Collaborator

rwaldron commented Jan 6, 2017

Probably safest to just make the thing we hashed out here into a plugin that can be npm installed—would that work?

@nebrius
Copy link
Owner

nebrius commented Jan 6, 2017

Probably safest to just make the thing we hashed out here into a plugin that can be npm installed—would that work?

I like that idea. We can release it quickly, and there's no risk to the existing plugin or worries about handling some controllers but not others or the confusion of not support stepper motors directly since it's a separate module. I can then add a blurb to Raspi IO's README about it.

@RonB if you can get a repo setup on your GitHub account with this code and drop the link to it here, we can help you flesh out the non-code bits necessary to get this wrapped into an npm module, if you'd like.

@RonB
Copy link

RonB commented Jan 8, 2017

Thanks both of you for the constructive support.
@nebrius I Will do that. First I will do some extensive testing and make sure the motor runs fine with different parameters and scenario's. Do you have a convention for naming the repo/node module ie node-stepper-adafruit-motorhat?
@rwaldron
I think I run into a problem if I want to combine the module with the standaard dc motor class as both will try to lock the PCA9685 address? Would it be a beter idea to create a module for the entire board and include stepper and dc motor support?

@rwaldron
Copy link
Collaborator

rwaldron commented Jan 9, 2017

I think I run into a problem if I want to combine the module with the standaard dc motor class as both will try to lock the PCA9685 address? Would it be a beter idea to create a module for the entire board and include stepper and dc motor support?

There shouldn't be any such locking with address, since Expander.get(...) will either create a new Expander for a given I2C address or reuse an already existing instance associated with that address.

@nebrius
Copy link
Owner

nebrius commented Jan 9, 2017

I Will do that. First I will do some extensive testing and make sure the motor runs fine with different parameters and scenario's. Do you have a convention for naming the repo/node module ie node-stepper-adafruit-motorhat?

I don't have a naming convention except for the core libraries for raspi-io. Pick whatever you prefer! I would include something in the name indicating it's specifically for the raspberry pi, maybe pi-adafruit-motorhat-stepper?

@Triadager
Copy link

Hello,

I'm sorry to bother, but I wanted to control stepper drivers (A4988, DRV8825 and TB6600) with johnny-five. Why is the stepper class disabled for the raspberry pi? My understanding is that it should work without any (major) issues, since all the RasPi has to do is sending a few pulses to the pins. Could somebody explain whats going on there? Am I missing something?

I highly appreciate your help!

Kind regards,

Triadager

@dtex
Copy link

dtex commented Oct 10, 2017

Update on stepper.

There is a PR open for the new stepper API in firmata.js. I think it's nearly ready to land, we're just seeing some failures on Travis that need to be ironed out. I don't think the API itself will change at all.

I'm working on the new Stepper class for Johnny-Five. It will include support for both the accelStepper library that can be bundled with configurableFirmata and a slower J5 only solution that will just uses digital pin writes.

For the former to work in rasp-io we would need to port the accelStepper library so that we can replicate that functionality. Hopefully someone will step up and want to take that on. I hope to have the J5 class done in the next couple of months.

For the latter to work in rasp-io there will need to be one small change made to reflect a change I'm planning to submit for firmata.js. I need to test a hypothesis first.

@nebrius
Copy link
Owner

nebrius commented Oct 10, 2017

So I think I may be missing something, but I cannot for the life of me figure out how to download accelStepper so I can peak at the files.

Anyways.

My plate is really full with Raspi IO work for the rest of the year, so I won't be able to take on this work, unfortunately. Is this something you would be interested in working on @Triadager? I can help walk you through how to integrate it into Raspi IO

@Triadager
Copy link

@nebrius
Regarding accelStepper files: I think you might be looking for this: http://www.airspayce.com/mikem/arduino/AccelStepper/AccelStepper-1.57.zip
It's the package with examples. Maybe you didn't see the link on the page?

I would love to, but since I only got into touch with both the Raspberry Pi and Node.js a couple of weeks ago and don't really have any other programming experience, I might not be the right guy for that. I still need to learn loads of things. However, if you still think I could be of any help let me know, I would love to contribute!

At this point I just want to thank all of you big time for the effort you put into this. It's awesome.

@nebrius
Copy link
Owner

nebrius commented Oct 11, 2017

Ah, thanks for the link @Triadager! Sorry we couldn't be of more help to you right now. And I sadly do think you're right, this particular project will probably require more experience than most changes require :-/

@nebrius
Copy link
Owner

nebrius commented Nov 22, 2017

I'm going to go ahead and close this issue due to inactivity. Feel free to reopen if needed!

@nebrius nebrius closed this as completed Nov 22, 2017
@a1xon
Copy link

a1xon commented Mar 10, 2018

I still don't get why it's more complicated to talk to a stepper driver with an RPi than with an Arduino? Can somebody explain?

@nebrius
Copy link
Owner

nebrius commented Mar 11, 2018

I still don't get why it's more complicated to talk to a stepper driver with an RPi than with an Arduino? Can somebody explain?

Super short tl;dr:

  1. the Arduino and the Raspberry Pi support different kinds of hardware because they are different kinds of devices. The Arduino is more geared towards embedded applications than the Raspberry Pi is...Broadcom and Atmel designed their respective chips for radically different things. There's no such thing as a perfect, one-size-fits-all design in the hardware world. Each company made their choices for their respective chips, and we can't change that (you can't patch hardware).

  2. The Raspberry Pi runs an operating system and the Arduino does not. This has major implications on what software is available and guarantees on timing within the OS. Having a thread scheduler vs. not having a thread schedular is really significant when it comes to timing of things, and timing is really important when it comes to certain hardware operations. It makes it so that many hardware protocols cannot be emulated in software when there is a thread scheduler, but can be implemented when there is not a thread scheduler. When there's a thread scheduler, the timing is such that you can guarantee that a setTimeout will take no shorter than x ms, but there's no guarantee that it won't take longer than x ms (and it often does take longer, FYI).

Comparing the Raspberry Pi and the Arduino is a lot like comparing ANSI C (not even C++) to JavaScript. They both have their strengths and weaknesses, but they are radically different languages.

@dtex
Copy link

dtex commented Mar 11, 2018

I have successfully controlled a stepper connected to an Arduino from Johnny-Five running on my Macbook Pro without using stepper specific c code on the Arduino (i.e. firmataStepper or firmataAccelSteper).

Here's a video https://twitter.com/dtex/status/922597004500504576

I'm using just standardFirmata and JS to toggle the pins. This code will run on a Pi w/o any changes to rasp-io but I have no clue how well it will perform.

I've seen decent performance on my Macbook (about 500 steps per second) but I know and accept that my step timing is not gauranteed to be smooth. For instance, when the V8 garbage collector does its thing the event loop grinds to a halt. How long that takes depends on so many things we may as well say it's nondeterministic. It might be a couple milliseconds, it might be less. When it does kick in there'll be a hitch in the stepper's giddyup (sorry, the rodeo is in town this week). So far those events are undetectable by my eye, but my program is doing nothing but spinning a single stepper.

Generally performance will depend on how quickly the Pi can run through the event loop (really needs to be more than once every millisecond on average to achieve 500 steps per second smoothly). It will also depend on the interface between the CPU and GPIO because we are toggling lots of pins really quickly.

All this code lives in a complete refactor of the Johnny-Five stepper class. It's not even ready for first commit in my mind but if you are adventurous, have time to kill, and want to try it out let me know and I will share the code in a gist. So far you can only spin the motor, but it's a start.

BTW, I'm working on getting my first Pi up and running with Johnny-Five so I'll start testing it myself soon enough. I have another J5 refactor that's turned into a real hairball and has led me to back burner this stepper stuff for a while.

@a1xon
Copy link

a1xon commented Jun 9, 2018

@dtex any updates? I ran steppers with the RPi via a simple python script without any problems. But it feels stupid calling a python script that cares for the steppers while working with johnny five.

@dtex
Copy link

dtex commented Jun 9, 2018

That other project is done. Now I’m working on some changes to rwaldron/temporal to allow for a wider variety of step speeds, but I do have the basic step code working. Haven’t tried with Pi yet but will soon.

Little confused by your comment though... Stepper and servo are different things.

@a1xon
Copy link

a1xon commented Jun 11, 2018

I'm sorry. Of course I meant steppers and not servos. I followed this tutorial for the python script. But for my project I've decided I will use a continuous servo so I can keep the code in plain JavaScript and saving tons of GPIOs. Thank you so much for your work on jonny-five. If you need testers for your code I'm happy to fry a pi for you.

@oligriffiths
Copy link

Hey @RonB @rwaldron @nebrius what's the state of play with this?

I recently received the Adafruit Motor Controller HAT https://www.adafruit.com/product/2348 and was hoping to use the stepper motor functionality with J5. Is this natively supported now or does it still require the custom Stepper class as outlined above?

Thanks

@nebrius
Copy link
Owner

nebrius commented May 18, 2020

Hi @oligriffiths. There hasn't been any progress on this issue since the last posting date. I'm afraid that raspi-io isn't as active as it used to be, and isn't likely to be implemented.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants