Stepper driver / AccelStepper library demo / Fur Elise demo (w/ LCD)

So I really went all-out tonight when I finally got my 10-years-moving-with-me stepper motor working... it's a little motor out of a 40mb(?) hard drive that I tore apart when I was 13 or 14. I tried various ways to step it at the time: I used an ohmmeter to find its coils, then attached it to 8 "key" buttons that would activate + or - for each of the 4 leads. I'd activate sequences of buttons to try to make it move, but it never would. I tried various times throughout my electronics life to make it work... especially after getting the Arduino (Oooh! Now I can make it work! Nope...). And after getting my Motor Shield v3. No dice. As it would turn out, what I found tonight, the steps were probably too small to notice...

I started with the driver circuit, based exactly on the http://arduino.cc/en/Tutorial/MotorKnob tutorial (but using adafruit's BoArduino on my modified Radio Shack electronics board, a.k.a. Arduino platform). It worked... perfectly... the first try. I was too excited. OK, let's see what else we can do... hmm, the driver chip is getting REALLY hot under that current. And the motor is stuck "on" too... understandable, since it's supposed to stay in place. This library doesn't give me any flexibility to "sleep" the servo...

So I searched around and found AccelStepper. Oof. That's some... well... not quite the worst I've seen (Motor Shield v3 is easily the worst-documented Arduino accessory I've ever used, I was happy to integrate it into my last project and retire it), but not very good either. For example, runSpeed() doesn't work like run() does, and neither does runSpeedToPosition(), so there's no way to simply "run to position at a fixed speed" in a while(runSpeed()); loop. I had to go over the library's cpp code several times to figure out each function I had to use to make it work... since there are no simple "basic usage" examples provided. :confused:

As a result, the board gained a mess of wires running to all 3 potentiometers, and the modded 16x2 LCD connected internally to the breadboard. Cool, I figured out the best speed parameters to make it run and sound pretty nice. And I added a resistor array to keep the current draw to a sane level. And since I have a resistor "shunt" array, I can use that as a measurement of voltage drop and power the board's 1mA current meter as well. Nice! I used a method in the loop to "sleep" the motor when it's done, selectable by the toggle switch (on = enable sleep, off = constant current). The current usage is very noticeably reflected in the meter. Amusingly, the stepper uses the least power when it's rotating at a high speed, and the most power when it's stopped. And the lower power it uses, the more inaccurate and prone to skips it is.

With adding the "constant drive" button (I actually ran out of digital I/O, so I used an analog pin as digital for this: digital pin 19), I found that the stepper makes really accurate tones based on the speed. Brilliant idea: I remember seeing a video of an HP ScanJet playing some classical tune. Turns out it's Fur Elise. That shouldn't be too hard. I Googled the notes to it, but it was damn near impossible to find a "flat list" of notes to the tune. And when I did, it was some piano student regurgitating the notes, saying "the first thing we learn". It wasn't the same sequence (but mostly the same notes in the most-memorable sequence). I guess this scanner used a bit of a modified rendition. I wanted that one, for geek's sake. So I dumped it into Audition 1.5, put it in spectral view, and used the frequency analyzer to pick out the notes. They mostly matched up, but I think the scanner has a higher-resolution, higher-frequency stepper motor than I have... there were a few notes I simply couldn't speed up to. I improvised :wink: But it still took about 3-4 hours to simply get through the song... Finally, I uploaded it.

It took a few tries, and I'm not entirely sure why it was failing so strangely... pulling up the wrong values, stalling on notes (doo-do-doo-do-dooooooo...), just general corruption. Out of memory? It was all getting loaded to RAM, but with 2kb, how can that be too little? I added a memory-check function and displayed it. 1,800+ bytes free. Huh. Well, I "progmem"'d it anyway, added "static", and everything settled down and worked after a few more bug-fixes.

Here's the end result:

And here's the code!

#include <AccelStepper.h>
#include <LiquidCrystal.h>
#include <avr/pgmspace.h>

const int stepsPerRevolution = 410;

// initialize the stepper library on pins 8 through 11:
AccelStepper myStepper(4, 8,9,10,11);            
LiquidCrystal lcd(7,6,5,4,3,2);

static int notes[] PROGMEM = {
440, 466, 494, 523, 554, 587, 622, 659, 698, 740, 784, 831, 880, 932, 988, 1046, 311, 330, 415, 1175, 1244, 1328, 1397, 392, 165
//A-1 A#-1 B-1  C    C#   D    D#   E    F    F#   G    G#   A    A#   B     C   D#-1 E-1 G#-1   D+1  D#+1  E+1   F+1   G-1  G-2
};

static uint8_t furElise[] PROGMEM = {
21,20,21,20,21,14,19,15,12,
17,0, 3,7,12,14, 17,18, 3,12,14,15, 17,0,7,
21,20,21,20,21,14,19,15,12,
17,0, 3,7,12,14, 17,18, 7,15,14,12,
17,0, 14,15,19,21, 10,15, 10,22,21,19,
23,2, 22,21,19,15, 17,0, 21,19,15,14,
23,10, 24,23,10,21, 24,23,10,21,
6,7,20,21, 6,7,20,21, 19,21,20,21,14,19,15,12,
17,0, 3,7,12,14, 17,18, 3,12,14,15, 17,0,7,
21,20,21,20,21,14,19,15,12,
17,0, 3,7,12,14, 17,18, 7,15,14,
12,12,12,12,12,12 };

int lastPos, lastSpeed;
unsigned int x; // temporary counter, 0-65535

void setup() {
  pinMode(13,OUTPUT); // Arduino LED indicates servo is moving to position
  pinMode(12,INPUT); // 12 = ground to signal "constant speed" rotation mode
  digitalWrite(12,HIGH); // 20k pull-up
  pinMode(19,INPUT); // 19 = analog pin 5 = ground to go constant-power, helps maintain position but wastes power
  digitalWrite(19,HIGH);
  lcd.begin(16,2);
}

void loop() {
  int posReading, accelReading, maxSpeedReading;
  if (digitalRead(12) == LOW) {
    // this runs only if pin 12 is grounded, i.e. button pressed for constant speed rotation
    if (analogRead(1) < 10) doFurElise(); // but do we want Fur Elise instead?
    myStepper.setMaxSpeed(2000); // remove the speed cap since we're not accelerating
    while (digitalRead(12) == LOW) {
      if (x%100 == 0) myStepper.setSpeed(maxSpeedReading = analogRead(2) * 2); // grab a dial sample every 100 iterations
      // here, x will go through 65,536 iterations before looping back to 0, triggering an LCD update. Requires almost zero overhead, allowing LCD to be updated while servo turns.
      if (!x) {
        lcd.setCursor(5,1);
        lcd.print("     ");
        lcd.setCursor(5,1);
        lcd.print(maxSpeedReading,DEC);
      }
      myStepper.runSpeed();
      x++;
    }
    myStepper.setCurrentPosition(map(posReading,0,1024,0,stepsPerRevolution * 2)); // reset position
    myStepper.setSpeed(0); // reset speed
  }
  if (digitalRead(19)) {
    myStepper.disableOutputs();
  } else {
    myStepper.setSpeed(0);
    myStepper.runSpeed();
  }
  posReading = analogRead(0);
  if (abs(posReading - lastPos) < 3) return; // don't make a bunch of movements if it's only noise
  delay(25);
  if (abs(analogRead(0) - posReading) > 3) return; // debounce the input, has it stopped turning? will only +/- by 1-2 values if stopped

  lastPos = posReading; // it's stopped, so we begin.
  lcd.clear();
  lcd.setCursor(0,0);
  // read/map the sensor values (various conditions)
  posReading = map(posReading,0,1024,0,stepsPerRevolution * 2); // will go 2 turns on one turn of dial
  accelReading = map(analogRead(1),0,1024,100,9999); // acceleration: steps per-sec-per-sec, good to be >maxSpeed.
  maxSpeedReading = map(analogRead(2),0,1024,100,2000);
  
  myStepper.setMaxSpeed(maxSpeedReading);
  myStepper.setAcceleration(accelReading);
  myStepper.moveTo(posReading);
  lcd.print("Pos  MxSpd Accel");
  lcd.setCursor(0,1);
  lcd.print(posReading,DEC);
  lcd.setCursor(5,1);
  lcd.print(maxSpeedReading,DEC);
  lcd.setCursor(11,1);
  lcd.print(accelReading,DEC);

  digitalWrite(13,HIGH); // LED on
  while (myStepper.run()) ; // do nothing but run myStepper.run() until it returns false (no step; completed).
  digitalWrite(13,LOW); // LED off
}

void doFurElise() {
  int note;
  long startNote;
  lcd.clear();
  lcd.setCursor(0,0);
  lcd.print("FElise Note Freq");
  myStepper.setMaxSpeed(3000);
  for (x=0;x<sizeof(furElise);x++) {
    note = pgm_read_byte(&furElise[x]);
    note = pgm_read_word(&notes[note]);
    lcd.setCursor(0,1);
    lcd.print("                ");
    lcd.setCursor(7,1);
    lcd.print(x,DEC);
    lcd.setCursor(12,1);
    lcd.print(note,DEC);
    startNote = millis();
    myStepper.setSpeed(note);
    while (millis() - startNote < 250) myStepper.runSpeed();
  }
  lastPos = 0;
}

... A few bits of goofs in there, like "lastSpeed" not being used in its stated context (I was just recycling it for temp, but then realized I deleted the code that used it originally), but hey... it's really here so people can copy Fur Elise for their own geeky uses :wink:

Love to hear what you think of it!

edit: commented it :slight_smile: