// Work in progress
// This moves a set of steppers towards an random vector of locations.
// https://wokwi.com/projects/413104758423535617
// for https://forum.arduino.cc/t/controlling-32-or-more-stepper-motors/1280070/104
//
// built from
// https://wokwi.com/projects/390741120778917889
// for https://forum.arduino.cc/t/controlling-many-stepper-motors-beginner/1228155/16
//
// v1: V1_simultaneousSteps.ino.txt tab -- move all steppers at same rate
// v2: Move steppers at independent rates to reach new target at the same time.
// v3: Use Bresenham algorithm to sync axes, so you could accelerate and keep in step.
//
// Pasted together from bits of these:
// https://wokwi.com/projects/356666059490147329 -- non-blocking FastLED
// https://wokwi.com/projects/390478474763185153 -- 6axis RAMPS GCODE interpreter
// https://forum.arduino.cc/t/changing-button-states-from-momentary-to-latch/993706/14 -- handling vectors of parts with structs
//
// Serial plotter shows decreasing error as steppers approach new targets
#include <FastLED.h>
#define NUM_LEDS 32
const int FASTLED_PIN = 2;
const int SpeedPin = A15;
const int PatternPin = A14;
const int verbose = 4;
// FastLED variables
CRGB leds[NUM_LEDS];
uint8_t patternCounter = 0;
bool isRunning = false;
bool changePattern = false;
void checkTimer();
int reportInterval = 100; // ms
// FIREglobals:
const uint8_t cooling = 55;
const uint8_t sparking = 120;
bool gReverseDirection = false;
// timing coefficients for fast dummy axis
uint32_t RetargetInterval = 1000; //ms
const int msPerStep = 3; // if you want acceleration, vary this step rate and the others will follow
// Bresenham fast dummy axis delta
int32_t dx = RetargetInterval / msPerStep; // ghost axis dx for Bresenham Algorithm synchronization
struct Steppers {
int8_t StepPin;
int8_t EnablePin;
int8_t DirPin;
int8_t MinPin;
int8_t MaxPin;
long position;
long target;
// uint32_t lastStep; //
// uint32_t interval; // ms between steps
int32_t dy; // bresenham dy (ghost axis dx=constant is faster than everything)
int32_t D; // bresenham Decision sum
} steppers[] = { //
{22, 3, 4, 5}, //
{23, 3, 4, 5}, //
{24, 3, 4, 5}, //
{25, 3, 4, 5}, //
{26, 3, 4, 5}, //
{27, 3, 4, 5}, //
{28, 3, 4, 5}, //
{29, 3, 4, 5}, //
{30, 3, 4, 5}, //
{31, 3, 4, 5}, //
{32, 3, 4, 5}, //
{33, 3, 4, 5}, //
{34, 3, 4, 5}, //
{35, 3, 4, 5}, //
{36, 3, 4, 5}, //
{37, 3, 4, 5}, //
{38, 3, 4, 5}, //
{39, 3, 4, 5}, //
{40, 3, 4, 5}, //
{41, 3, 4, 5}, //
{42, 3, 4, 5}, //
{43, 3, 4, 5}, //
{44, 3, 4, 5}, //
{45, 3, 4, 5}, //
{46, 3, 4, 5}, //
{47, 3, 4, 5}, //
{48, 3, 4, 5}, //
{49, 3, 4, 5}, //
{50, 3, 4, 5}, //
{51, 3, 4, 5}, //
{52, 3, 4, 5}, //
{53, 3, 4, 5}, //
};
void updateRetargetInterval(uint32_t newInterval) {
if (newInterval != RetargetInterval) {
RetargetInterval = newInterval;
dx = RetargetInterval / msPerStep; // set speed to reach the target by the end of the interval
Serial.print("RetargetInterval=");
Serial.println(RetargetInterval);
}
}
uint32_t targetInterval = RetargetInterval;
uint32_t lastLedChange = -targetInterval;
void setup(void) {
Serial.begin(115200); // open coms
FastLED.addLeds<WS2812B, FASTLED_PIN, GRB>(leds, NUM_LEDS);
FastLED.setBrightness(100);
for (auto &stepper : steppers) {
pinMode(stepper.StepPin, OUTPUT);
pinMode(stepper.DirPin, OUTPUT);
pinMode(stepper.EnablePin, OUTPUT);
pinMode(stepper.MinPin, INPUT_PULLUP);
digitalWrite(stepper.EnablePin, LOW);
}
lastLedChange = millis() - targetInterval; // backdate change
updateRetargetInterval(1000);
}
void loop(void) {
uint32_t now = millis();
if (now - lastLedChange >= targetInterval) {
lastLedChange = now;
switch (map(analogRead(PatternPin), 0, 1024, 0, 2)) {
case 0:
rainbowLoop(1, 17);
break;
case 1:
fireLoop();
break;
}
FastLED.show();
updateRetargetInterval(map(analogRead(SpeedPin), 0, 1023, 2000, 500));
targetInterval = RetargetInterval;
}
stepperSyncWithLeds(now);
report();
}
void stepperSyncWithLeds(uint32_t now) {
static uint32_t last = 0;
if (now - last < msPerStep) return;
last = now;
int ii = 0;
for (auto &stepper : steppers) {
long target = leds[ii][0]; // red channel
if (stepper.target != target) { // new target, set slope
stepper.target = target;
long distance_to_go = stepper.target - stepper.position;
stepper.dy = abs(distance_to_go);
stepper.D = 2 * stepper.dy - dx;
Serial.print("NEWTARGET:");
Serial.print(stepper.dy);Serial.print("/"); Serial.print(dx);
Serial.print(" ");
Serial.print(stepper.StepPin); Serial.print(" ");
Serial.print(stepper.StepPin-ii);
Serial.print(" #");
Serial.print(ii); Serial.print(" t:");Serial.print(target); Serial.println();
}
if (stepper.position != stepper.target && stepper.D > 0 ) { // time to step
stepper.D -= 2L * dx; // decrement the Decision rule by the slope effect of one dy
// stepper.lastStep = now;
if (stepper.position < stepper.target) {
digitalWrite(stepper.DirPin, LOW);
++stepper.position;
} else {
digitalWrite(stepper.DirPin, HIGH);
--stepper.position;
}
delayMicroseconds(50);
digitalWrite(stepper.StepPin, HIGH);
digitalWrite(stepper.StepPin, LOW);
}
stepper.D += 2L * stepper.dy; // increment y Decision rule by the slope effect of one of dx
++ii; // next stepper
}
}
void fireLoop() {
static byte heat[NUM_LEDS];
// Step 1. Cool down every cell a little
for ( int i = 0; i < NUM_LEDS; i++) {
heat[i] = qsub8( heat[i], random8(0, ((cooling * 10) / NUM_LEDS) + 2));
}
// Step 2. Heat from each cell drifts 'up' and diffuses a little
for ( int k = NUM_LEDS - 1; k >= 2; k--) {
heat[k] = (heat[k - 1] + heat[k - 2] + heat[k - 2] ) / 3;
}
// Step 3. Randomly ignite new 'sparks' of heat near the bottom
if ( random8() < sparking ) {
int y = random8(3);
heat[y] = qadd8( heat[y], random8(160, 255) );
}
// Step 4. Map from heat cells to LED colors
for ( int j = 0; j < NUM_LEDS; j++) {
CRGB color = HeatColor( heat[j]);
int pixelnumber;
if ( gReverseDirection ) {
pixelnumber = (NUM_LEDS - 1) - j;
} else {
pixelnumber = j;
}
leds[pixelnumber] = color;
}
}
void rainbowLoop(uint8_t thisSpeed, uint8_t deltaHue) { // The fill_rainbow call doesn't support brightness levels.
uint8_t thisHue = beatsin8(thisSpeed, 0, 255); // A simple rainbow wave.
//uint8_t thisHue = beat8(thisSpeed,255); // A simple rainbow march.
fill_rainbow(leds, NUM_LEDS, thisHue, deltaHue); // Use FastLED's fill_rainbow routine.
}
void report(void) {
const uint32_t interval = 100;
static uint32_t last = -interval;
if (millis() - last < interval) return;
last += interval;
if(0){
for ( auto &stepper : steppers) {
Serial.print(stepper.target - stepper.position);
Serial.print(", ");
}
Serial.println();
}
}
void writeStepperJson() {
// write Wokwi json fragment for the diagram.json file
char buff[180];
Serial.println(",");
for (int ii = 1; ii < 33 ; ++ii) {
snprintf(buff, 180, R"vogon( [ "mega:%d", "drv%d:STEP", "green", [ ] ],)vogon", ii + 21, ii);
Serial.println(buff);
snprintf(buff, 180, R"vogon( [ "mega:%d", "drv%d:DIR", "green", [ ] ],)vogon", 4, ii);
Serial.println(buff);
snprintf(buff, 180, R"vogon( [ "mega:%d", "drv%d:ENABLE", "green", [ ] ],)vogon", 3, ii);
Serial.println(buff);
}
}
Speed
Pattern