shiftStepper Arduino Library Intro & Tutorial

I've been working on an Arduino library to drive unipolar steppers with the sixteen channel high current shield. You could do that before, of course, but this will make it much easier.

The major motivation for this, other than simply making my boards more useful, is the possibility of radically cheaper RepRap electronics. One sixteen channel high current shield + one Arduino + one Proto Shield to land the end stops, the thermistor circuit, and a MOSFET to drive the heater should bring the electronics cost down from around $250 to around $100. 

Since I plan other shift register boards besides the six channel and sixteen channel high current boards in the future, this will have to expand to cover various different types of boards doing various types of things on a single daisy chain. I've been holding off on the stepper driver while I figure out how to architect that system in a reasonable way, and I think I've got a workable architecture. Here's a quick video of a sixteen channel high current shield driving four Lin Engineering NEMA 17 unipolar stepper motors. 

The library is still very alpha, but the core features work. There is the start of some documentation in the download; I'm working on making it available online as well. The example code provided and this (over-long) blog post should give you enough of a framework to get started.

Downloading the Library

You can download the library from our Libraries file download area, here. A GitHub repository is coming, but is not ready yet.

Chains, Boards, Devices, and Channels

This library is intended to be flexible enough to support all of the shift register interface boards that I plan to be bringing out in the future. Therefore, it has pieces that are not strictly necessary for controlling a few unipolar stepper motors.

The highest level object in this library is the chain. A chain is a set of one or more shift register boards. The farthest from the host (along the daisy chain cable) is board 0 and the one closest to the host. This little endian structure mirrors the operation of the shift register chain, i.e. the first bit shifted ends up the farthest from the host at the end of the shifting operation. Each board contains one or more devices. A device is a group of one or more channels that operate togeter -- for example, the four switches that drive a unipolar stepper motor.

The only device type currently implemented is the shiftStepper; a shiftSwitch is high on the TODO list. A channel is the smallest logical element on the board. On all of the shift register boards so far, a channel is a single switch controlled by a single bit. Future boards may have channels that involve many more bits; hence the term should be taken as generic.

New data is shifted out to the chain by calling shiftChain::doTick(). This can be called from inside the loop() function, but for the best stepper performance, it should be called by a timer interrupt service routine. The shiftChain object has member functions for setting up the timer correctly, but the user must supply the interrupt service routine. Only Timer 2 is implemented so far; adding more is also on the TODO list. Here's an examples, using Timer 2:

ISR(TIMER2_OVF_vect)
{
     myChain->doTick();
}

shiftChain::doTick() calls the shiftBoard::doBoardTick() for each board in the chain. In turn, each example of shiftBoard::doBoardTick() calls shiftDevice::doDeviceTick() for each device in that board. These function are defined as virtual functions in the shiftBoard and shiftDevice classes; individual boards and devices are implemented subclasses of these two classes, respectively, and must implement shiftBoard::doBoardTick() and shiftDevice::doDeviceTick().

The aim of all of this is to allow each board and each applicaiton of that board to be separately defined in a way that still plays nice together in a (potentially) long chain of such device and allows the addition of new boards and applications without disrupting source code based on this version of the library. 

 Using the Library

The first step is declaring the devices that make up each board, each board, and the chain, in that order. There are clever ways to do this, but the simplest and most fool-proof way to do it is to declare all the objects at global scope. Here's how I do it in the included examples:

static const uint8_t stepSequence[4] = {0x2, 0x4, 0x1, 0x8};

// Set up channel combinations for the motors...
static const uint8_t motChans0[__channelsPerMotor__] = {0,2,3,1};
static const uint8_t motChans1[__channelsPerMotor__] = {4,5,6,7};
static const uint8_t motChans2[__channelsPerMotor__] = {11,9,10,8};
static const uint8_t motChans3[__channelsPerMotor__] = {15,14,13,12};

// Declare the global objects  
shiftChain *myChain = 0;
shiftStepMotor motor0(__channelsPerMotor__, stepSequence, motChans0);
shiftStepMotor motor1(__channelsPerMotor__, stepSequence, motChans1);
shiftStepMotor motor2(__channelsPerMotor__, stepSequence, motChans2);
shiftStepMotor motor3(__channelsPerMotor__, stepSequence, motChans3);
shiftDevice *motors[4] = {&motor0, &motor1, &motor2, &motor3};
shiftSixteen board0(4, motors); shiftBoard *boards[1] = {&board0};
shiftChain storeChain(1, boards, DATPIN, SCLPIN, LATPIN, MRPIN, INDPIN);

All of these declarations could alternately be made static in the setup() function, with the exception of the declaration of *myChain, which has to be globally visible in order to be useful. There are no blank constructors defined for any of these objects, so they must be fully initialized at declaration; that drives the order of the declaration in the above snippet. The various arrays are declared as global variables here due to the lack of array literals in C++.

Once you've declared all the members of the chain, you define the interrupt routine as shown above, assign the myChain pointer to point at storeChain, and start the timer:

myChain = &storeChain;
myChain->startTimer(__preScaler32__, 0, 2);

After that, the interrupt will execute every time the selected timer interrupt trips. What it does depends on what each device has been commanded to do. There are three command functions:

  • incrStep(int8_t dir) -- Do a single step. A positive non-zero value for dir will result in a forward step; zero or negative value will results in a reverse step. If the motor is in the middle of a commanded move, it will add/subtract one step as appropriate for the current speed and commanded direction.
  • doSteps(uint16_t steps, int16_t speed) -- Do steps at speed. The sign of the speed parameter determines the direction. A negative value for the steps parameter commands continuous rotation
  • setSpeed(int16_t speed) -- Set the speed. The sign of speed determines the direction of motor rotation. 

Examples

Two example programs are provided with package -- IncrementExample.pde and MotorExample.pde. Both use the same wiring, as shown:

IncrementExample.pde allows the user to single-step the motors in either the positive or negative direction by sending a '+' or a '-' from the serial console. If, like me, you just wire the motors up willy-nilly without paying much attention to which phase you attach to which channel, the motors are likely to jump back and forth when you first try this out. You can fix this jumpiness by adjusting either the order of the members of the stepSequence array or the order of the channels for the motor. 

MotorExample.pde sets all four motors spinning. Motors 0 and 2 start spinning forward at minimum speed while motors 1 and 3 start spinning in reverse at minimum speed. Sending a '+' increases the absolute speed of both while sending a '-' decreases the absolute speed. Decreasing the speed below 1 reverses all motors, so 1 and 3 spin forward while 0 and 2 spin in reverse.