Prototyping moving kinetic display

You’ve seen the video(s), a vast sea of shiny balls suspended from a ceiling panel, moving up and down to make interesting shapes.
I’m looking to make something similar. Bought a couple of 28BYJ-48 geared motors with ULN2003 “driver” that buffers 4 Arduino outputs to sink current from 5V thru 4 motor phases to make it turn.

Used a Stepper.h example, got one to spin and move a holiday ornament up and down.
www.youtube.com/watch?v=hXEccqlsE6M
Added a 2nd, got one to move, than the other.
Now, the interesting part. How to make both move at the same time? Wrote this program to make them rotate at different speeds and for different distances, using arrays to hold values.
Arrays are sized for 17 motors on a Mega eventually, I have 2 wired up on a '1284P and am running the code to simulate having 6 connected. #2 seems to run pretty smooth, #1 seems a little jerky in comparison.
Is this just a play-with-settings-until-things-smooth-out kind of thing? Is 16 or 17 with a Mega going to be possible? Or is there a better way to achieve concurrent movement with different speeds and distance (# of steps)?

#include <Stepper.h> //include the function library, standard Arduino library
#define STEPS 64 // 64 steps per rev // hardware dependent

int speed[] = {
  300,300,300,300,300,300,0,0,0,0,0,0,0,0,0,0,0,}; // speed to move motor
int numSteps[] = {
  6000,5200,4000,3000,2000,1000,0,0,0,0,0,0,0,0,0,0,0,}; // steps to move in total

int countSteps[] = {
  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,}; // track how much moved

byte stepsDir[] =  {
  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,}; // up or down, 0 or 1

int stepSize[] = {
  9,3,4,5,2,6,0,0,0,0,0,0,0,0,0,0,0,}; // how big of a step

Stepper stepper0(STEPS, 5, 3, 4, 2); //create the stepper0  <<< Order here is important
Stepper stepper1(STEPS, 9, 7, 8, 6); //create the stepper1  <<< Order here is important
Stepper stepper2(STEPS, 13, 11, 12, 10); //create the stepper2  <<< Order here is important
Stepper stepper3(STEPS, 17, 15, 16, 14); //create the stepper3  <<< Order here is important
Stepper stepper4(STEPS, 21, 19, 20, 18); //create the stepper4  <<< Order here is important
Stepper stepper5(STEPS, 25, 23, 24, 22); //create the stepper5  <<< Order here is important
/*
 Stepper stepper6(STEPS, 29, 27, 28, 26); //create the stepper6  <<< Order here is important
 Stepper stepper7(STEPS, 33, 31, 32, 20); //create the stepper7  <<< Order here is important
 Stepper stepper8(STEPS, 37, 35, 36, 34); //create the stepper8  <<< Order here is important
 Stepper stepper9(STEPS, 41, 39, 40, 38); //create the stepper9  <<< Order here is important
 Stepper stepper10(STEPS, 45, 43, 44, 42); //create the stepper10  <<< Order here is important
 Stepper stepper11(STEPS, 49, 47, 48, 46); //create the stepper11  <<< Order here is important
 Stepper stepper12(STEPS, 53, 51, 52, 50); //create the stepper12  <<< Order here is important
 Stepper stepper13(STEPS, 57, 55, 56, 54); //create the stepper13  <<< Order here is important
 Stepper stepper14(STEPS, 61, 59, 60, 58); //create the stepper14  <<< Order here is important
 Stepper stepper15(STEPS, 65, 63, 64, 62); //create the stepper15  <<< Order here is important
 Stepper stepper16(STEPS, 69, 67, 68, 66); //create the stepper16  <<< Order here is important
 */
void setup()
{
  Serial.begin(9600); // initialize serial communication:
  stepper0.setSpeed(speed[0]); //set speed
  stepper1.setSpeed(speed[1]); //set speed
  stepper2.setSpeed(speed[2]); //set speed
  stepper3.setSpeed(speed[3]); //set speed
  stepper4.setSpeed(speed[4]); //set speed
  stepper5.setSpeed(speed[5]); //set speed
}
void loop()
{
  // Motor 0
  if (stepsDir[0] == 0){
    countSteps[0] = countSteps[0] + stepSize[0];
    if (countSteps[0] >= numSteps[0]){
      stepsDir[0] = 1;
    }
  }
  if (stepsDir[0] == 1){ 
    countSteps[0] = countSteps[0] - stepSize[0];
    if (countSteps[0] <= 0){
      stepsDir[0] = 0;
    }
  }
  if (stepsDir[0] == 0){
    stepper0.step(stepSize[0]); //move one direction
  }
  if (stepsDir[0] == 1){
    stepper0.step(-stepSize[0]); //move the other direction
  }
  // Motor 1
  // *****************************
  if (stepsDir[1] == 0){
    countSteps[1] = countSteps[1] + stepSize[1];
    if (countSteps[1] >= numSteps[1]){
      stepsDir[1] = 1;
    }
  }
  if (stepsDir[1] == 1){ 
    countSteps[1] = countSteps[1] - stepSize[1];
    if (countSteps[1] <= 0){
      stepsDir[1] = 0;
    }
  }
  if (stepsDir[1] == 0){
    stepper1.step(stepSize[1]); //move one direction
  }
  if (stepsDir[1] == 1){
    stepper1.step(-stepSize[1]); //move the other direction
  }
  // Motor 2
  // *****************************
  if (stepsDir[2] == 0){
    countSteps[2] = countSteps[2] + stepSize[2];
    if (countSteps[2] >= numSteps[2]){
      stepsDir[2] = 1;
    }
  }
  if (stepsDir[2] == 1){ 
    countSteps[2] = countSteps[2] - stepSize[2];
    if (countSteps[2] <= 0){
      stepsDir[2] = 0;
    }
  }
  if (stepsDir[2] == 0){
    stepper2.step(stepSize[2]); //move one direction
  }
  if (stepsDir[2] == 1){
    stepper2.step(-stepSize[2]); //move the other direction
  }
  // Motor 3
  // *****************************
  if (stepsDir[3] == 0){
    countSteps[3] = countSteps[3] + stepSize[3];
    if (countSteps[3] >= numSteps[3]){
      stepsDir[3] = 1;
    }
  }
  if (stepsDir[3] == 1){ 
    countSteps[3] = countSteps[3] - stepSize[3];
    if (countSteps[3] <= 0){
      stepsDir[3] = 0;
    }
  }
  if (stepsDir[3] == 0){
    stepper3.step(stepSize[3]); //move one direction
  }
  if (stepsDir[3] == 1){
    stepper3.step(-stepSize[3]); //move the other direction
  }
  // Motor 4
  // *****************************
  if (stepsDir[4] == 0){
    countSteps[4] = countSteps[4] + stepSize[4];
    if (countSteps[4] >= numSteps[4]){
      stepsDir[4] = 1;
    }
  }
  if (stepsDir[4] == 1){ 
    countSteps[4] = countSteps[4] - stepSize[4];
    if (countSteps[4] <= 0){
      stepsDir[4] = 0;
    }
  }
  if (stepsDir[4] == 0){
    stepper4.step(stepSize[4]); //move one direction
  }
  if (stepsDir[4] == 1){
    stepper4.step(-stepSize[4]); //move the other direction
  }
  // Motor 5
  // *****************************
  if (stepsDir[5] == 0){
    countSteps[5] = countSteps[5] + stepSize[5];
    if (countSteps[5] >= numSteps[5]){
      stepsDir[5] = 1;
    }
  }
  if (stepsDir[5] == 1){ 
    countSteps[5] = countSteps[5] - stepSize[5];
    if (countSteps[5] <= 0){
      stepsDir[5] = 0;
    }
  }
  if (stepsDir[5] == 0){
    stepper5.step(stepSize[5]); //move one direction
  }
  if (stepsDir[5] == 1){
    stepper5.step(-stepSize[5]); //move the other direction
  }
}

@CrossRoads, your link does not work.

Try again please, I think I fixed it. I can't seem to paste in links without the forum adding extra characters to the beginning (like "https://) and to the end (like M").

Have you considered tpic6B595 for driver? 1 chip can run 2 motors and can be daisy chained to (?) motors?
I never could get links to work right with the "WYSIWYG" reply screen, guess there's a trick I haven't stumbled upon yet.

Here are two motors running the code above.
https://www.youtube.com/watch?v=wdVCYzKgW3Q

The one on the right moves faster, and vibrates more. The one on the left moves slower and vibrates less. Both are on a red cotton thread.

We're also checking out printing our own pulleys, vs a ton of money on the presumably milled aluminum adapter/pulley from Actobotics @ Servocity.com.
Just winding a string here, so high precision AL is overkill.

Using the ULN2003 as that's what is common with these motors. TPIC6B595 is a shift register, so I'm pretty sure that require a different library.
https://www.amazon.com/s?k=28byj-48%2C+5+volt+stepper+motor+with+driver+board&crid=STT11NCHBG4E&sprefix=28byj%2Caps%2C184&ref=nb_sb_ss_i_6_5

These happen to be from Velleman, VMA401
https://www.vellemanusa.com/downloads/29/vma401_a4v01.pdf
as that is what our local electronics had on the shelf.

(links may need some trimming at beginning and end, extra https:// and " ", maybe other letters as well)

Just realized the VMA401 had a code example in it. I haven’t tried it yet.

// This Arduino example demonstrates bidirectional operation of a 
// 28BYJ-48, using a VMA401 - ULN2003 interface board to drive the stepper. 
// The 28BYJ-48 motor is a 4-phase, 8-beat motor, geared down by 
// a factor of 68. One bipolar winding is on motor pins 1 & 3 and 
// the other on motor pins 2 & 4. The step angle is 5.625/64 and the 
// operating Frequency is 100pps. Current draw is 92mA. 
//////////////////////////////////////////////// 
//declare variables for the motor pins 
int motorPin1 = 8; 
// Blue - 28BYJ48 pin 1 
int motorPin2 = 9; 
// Pink - 28BYJ48 pin 2 
int motorPin3 = 10; 
// Yellow - 28BYJ48 pin 3 
int motorPin4 = 11; 
// Orange - 28BYJ48 pin 4 
// Red - 28BYJ48 pin 5 (VCC) 
int motorSpeed = 1200; 
//variable to set stepper speed 
int count = 0; 
// count of steps made 
int countsperrev = 512; 
// number of steps per full revolution 
int lookup[8] = {
  B01000, B01100, B00100, B00110, B00010, B00011, B00001, B01001}; 
////////////////////////////////////////////////////////////////////////////// 
void setup() { 
  //declare the motor pins as outputs 
  pinMode(motorPin1, OUTPUT); 
  pinMode(motorPin2, OUTPUT); 
  pinMode(motorPin3, OUTPUT); 
  pinMode(motorPin4, OUTPUT); 
  Serial.begin(9600); 
} 
////////////////////////////////////////////////////////////////////////////// 
void loop(){ 
  if(count < countsperrev ) clockwise(); 
  else if (count == countsperrev * 2) count = 0; 
  else anticlockwise(); 
  count++; 
} 
////////////////////////////////////////////////////////////////////////////// 
//set pins to ULN2003 high in sequence from 1 to 4 
//delay "motorSpeed" between each pin setting (to determine speed) 
void anticlockwise() { 
  for(int i = 0; i < 8; i++) { 
    setOutput(i); 
    delayMicroseconds(motorSpeed); 
  } 
} 
void clockwise()
{
  for(int i = 7; i >= 0; i--)
  {
    setOutput(i);
    delayMicroseconds(motorSpeed);
  }
}
void setOutput(int out)
{
  digitalWrite(motorPin1, bitRead(lookup[out], 0));
  digitalWrite(motorPin2, bitRead(lookup[out], 1));
  digitalWrite(motorPin3, bitRead(lookup[out], 2));
  digitalWrite(motorPin4, bitRead(lookup[out], 3));
}

You might get smoother running with the 1/2 step pattern, 4096 steps per shaft rev.


512 SPR? You must have that modified version like Adafruit sells, SPR is oddball and closer to 513, nearly drove me nutz. >:(

Is this going to eventually expand to a large number of Megas controlling hundreds of balls? There's going to have to be communication between the Megas to synchronize them and those comms may cause glitches too.

Link in first post now good. 2 links in post #4 broken.

Did you try swapping the motors? Could one be slightly faulty?

For hundreds of motors, I would either look at controling the ULN drivers with 74hc595 shift registers or find/build tpic6b595 drivers, and face into either changing the library or writing your own (non library) code. 400 pins to control would need 50 shift registers, which a Nano could do, using SPI.

How will you calibrate the motor positions at startup?

Interesting problem.

Shift registers may make sense; or at least I'd suggest replace the ULN2003 with MOSFET versions (they do exist).

The main problem is 17 of them at different speeds.

For so many steppers it's quite likely easier to just take care of all that directly. The four digitalWrite() calls the library no doubt uses are likely to slow you down too much. I'd be looking at creating a single timing loop, running say every couple hundred µs. Then in that loop control each motor, when it's due to make the next step set the pins for that motor, and at the end of the loop shift all out. Or if you don't want shift registers but use the 1284's pins, use a bunch of PORT writes.

ULN2003 just needs direct port access, that seems faster to me than shifting out to TPIC6B595. (and I have shifted out to a string of 45 of them at 20KHz refresh rate, from a fixed pattern in 14625 bytes of the internal 1284P RAM, so that could be an option, but would need more hardware - 2 motors/shift register, so a string of 90 shift registers for 180 motors, at 10 KHz rate?) I could try a higher voltage too, the ULN2003 and the motor can both handle more. I have a board designed for 12 TPIC6B595s and easy daisychaining already I could use, so just buy motors and wire up connector boards (fresh design).

If I end up using these motors and controller cards, they come with a ULN2003, I'm not going to spend extra money getting MOSFET versions of the chips.

Links: I can't figure out how to get around the forum messing them up. If you hover the mouse over them, you can see the junk added at the start & end. Copy the link, clean the crap off.625

512 SPR? Oh, steps per revolution. I have the Velleman motor, I don't know how much it is. The shaft does not seem square with the motor, makes the pulley look like it bobbles compared to the motor body as it spins.

How do I change the step pattern? Something in the library?

I'll look into the Stepper.h library, see about getting rid of the digitalWrites if that's what it uses. For now, 465 seems to be the max speed that moves 6 motors, and 10 or 16 doesn't look too jerky for a step size. It is a mechanical system, so the thread used and the weight both play into it. How will it do with light fishing line, longer length? Who knows at this point.

We're using our own 3D printed pulley design, with a hole in the pulley area for the string to go thru, a post to tie it around, and shaft holes to accept 4/40 or 6/32 set screw. Also have the diameter hollowed out to save on plastic and print time. Takes an hour/pulley to print out 55mm outer diameter pulley.

Comm's between multiple 2560s controlling 20 motors each (with my full-breakout Mega boards, vs a Mega), I figure have a few patterns, have each one store it's portion, then use 1 as the master to tell the others which to run and send out a synchronizing start signal. If I get that far. These motors might drive me to use something that can be made to go faster smoother (i.e. not geared, like these are). We'll see as I slowly expand bigger, have 5 or 6 motors arriving Saturday to play with.

CrossRoads:

That's cheating! 3D printing makes it too easy to include neat features like that post that would be exceptionally expensive for conventional injection molding.

MorganS:
That's cheating! 3D printing makes it too easy to include neat features like that post that would be exceptionally expensive for conventional injection molding.

It'd have to be done in two halves, glued or welded together. The rim of the pulley itself is another problem for injection moulding.

Beats spending a ton at Servocity for 2-3 parts to make the equivalent assembly that would still need to be drilled to keep the string knot hole out of the way, and sides that are too shallow to hold much wound up string.

Anyway, lets keep it on subject.

So, if in Stepper.cpp I change all these digitalWrite to bitWrite or bitFast or something like that, it would help with performance?

void Stepper::stepMotor(int thisStep)
{
  if (this->pin_count == 2) {
    switch (thisStep) {
      case 0:  // 01
        digitalWrite(motor_pin_1, LOW);
        digitalWrite(motor_pin_2, HIGH);
      break;
      case 1:  // 11
        digitalWrite(motor_pin_1, HIGH);
        digitalWrite(motor_pin_2, HIGH);
      break;
      case 2:  // 10
        digitalWrite(motor_pin_1, HIGH);
        digitalWrite(motor_pin_2, LOW);
      break;
      case 3:  // 00
        digitalWrite(motor_pin_1, LOW);
        digitalWrite(motor_pin_2, LOW);
      break;
    }
  }
  if (this->pin_count == 4) {
    switch (thisStep) {
      case 0:  // 1010
        digitalWrite(motor_pin_1, HIGH);
        digitalWrite(motor_pin_2, LOW);
        digitalWrite(motor_pin_3, HIGH);
        digitalWrite(motor_pin_4, LOW);
      break;
      case 1:  // 0110
        digitalWrite(motor_pin_1, LOW);
        digitalWrite(motor_pin_2, HIGH);
        digitalWrite(motor_pin_3, HIGH);
        digitalWrite(motor_pin_4, LOW);
      break;
      case 2:  //0101
        digitalWrite(motor_pin_1, LOW);
        digitalWrite(motor_pin_2, HIGH);
        digitalWrite(motor_pin_3, LOW);
        digitalWrite(motor_pin_4, HIGH);
      break;
      case 3:  //1001
        digitalWrite(motor_pin_1, HIGH);
        digitalWrite(motor_pin_2, LOW);
        digitalWrite(motor_pin_3, LOW);
        digitalWrite(motor_pin_4, HIGH);
      break;
    }
  }


  if (this->pin_count == 5) {
    switch (thisStep) {
      case 0:  // 01101
        digitalWrite(motor_pin_1, LOW);
        digitalWrite(motor_pin_2, HIGH);
        digitalWrite(motor_pin_3, HIGH);
        digitalWrite(motor_pin_4, LOW);
        digitalWrite(motor_pin_5, HIGH);
        break;
      case 1:  // 01001
        digitalWrite(motor_pin_1, LOW);
        digitalWrite(motor_pin_2, HIGH);
        digitalWrite(motor_pin_3, LOW);
        digitalWrite(motor_pin_4, LOW);
        digitalWrite(motor_pin_5, HIGH);
        break;
      case 2:  // 01011
        digitalWrite(motor_pin_1, LOW);
        digitalWrite(motor_pin_2, HIGH);
        digitalWrite(motor_pin_3, LOW);
        digitalWrite(motor_pin_4, HIGH);
        digitalWrite(motor_pin_5, HIGH);
        break;
      case 3:  // 01010
        digitalWrite(motor_pin_1, LOW);
        digitalWrite(motor_pin_2, HIGH);
        digitalWrite(motor_pin_3, LOW);
        digitalWrite(motor_pin_4, HIGH);
        digitalWrite(motor_pin_5, LOW);
        break;
      case 4:  // 11010
        digitalWrite(motor_pin_1, HIGH);
        digitalWrite(motor_pin_2, HIGH);
        digitalWrite(motor_pin_3, LOW);
        digitalWrite(motor_pin_4, HIGH);
        digitalWrite(motor_pin_5, LOW);
        break;
      case 5:  // 10010
        digitalWrite(motor_pin_1, HIGH);
        digitalWrite(motor_pin_2, LOW);
        digitalWrite(motor_pin_3, LOW);
        digitalWrite(motor_pin_4, HIGH);
        digitalWrite(motor_pin_5, LOW);
        break;
      case 6:  // 10110
        digitalWrite(motor_pin_1, HIGH);
        digitalWrite(motor_pin_2, LOW);
        digitalWrite(motor_pin_3, HIGH);
        digitalWrite(motor_pin_4, HIGH);
        digitalWrite(motor_pin_5, LOW);
        break;
      case 7:  // 10100
        digitalWrite(motor_pin_1, HIGH);
        digitalWrite(motor_pin_2, LOW);
        digitalWrite(motor_pin_3, HIGH);
        digitalWrite(motor_pin_4, LOW);
        digitalWrite(motor_pin_5, LOW);
        break;
      case 8:  // 10101
        digitalWrite(motor_pin_1, HIGH);
        digitalWrite(motor_pin_2, LOW);
        digitalWrite(motor_pin_3, HIGH);
        digitalWrite(motor_pin_4, LOW);
        digitalWrite(motor_pin_5, HIGH);
        break;
      case 9:  // 00101
        digitalWrite(motor_pin_1, LOW);
        digitalWrite(motor_pin_2, LOW);
        digitalWrite(motor_pin_3, HIGH);
        digitalWrite(motor_pin_4, LOW);
        digitalWrite(motor_pin_5, HIGH);
        break;
    }
  }
}

And change a bunch of int to byte for pin assignments ...

Stepper::Stepper(int number_of_steps, int motor_pin_1, int motor_pin_2, int motor_pin_3, int motor_pin_4, int motor_pin_5)

No software improvements, but MrsCrossroads improved the wheel design. Now just 6 grams and with better shaft grip. Can go with a shorter screw now too. (string end is looking pretty raggedy, time for a new piece as I keep cutting it back as the end unravels for each test fit.)