SwivelSpeakers
This is an XY controlled sphere/ concentric/ “globus” loudspeaker positioning system driven by two stepper motors (vertical axis turning and horizontal axis turning) devised by Stefan Doepner. The cutting of elements was one of the first jobs for CNC2 machine. The swivel system provides movements of one to a couple of turns in each direction – therefore relatively precise positioning of loudspeaker.
MIDI control:
– midi channel defines the selection of particular system
– note with velocity above zero dictates the moving to a position defined by velocity. Velocity number is multiplied by some factor to define the maximum position
– note 1 is used for X motor position
– note 2 is used for X motor speed
– note 3 is used for X motor direction
– notes 4, pilule 5, 6 are used for the same on Y motor
– a reset button is used to define the zero position
Both arduino codes below use:
– arduino MIDI library http://arduinomidilib.fortyseveneffects.com/a00025.html
– arduino AccelStepper library http://www.schmalzhaus.com/EasyDriver/Examples/EasyDriverExamples.html
This is arduino code by Borut Savski used with a couple of old ISEL Stepper Drivers ( L297/ L298 combination; SwivelSpeaker.txt)
/* midiInServoControl use the serial receive (pin 0) to receive MIDI note data check for the speed note - translate the velocity to pulse train speed check for the dir note - translate the velocity (0 or >0)to direction pin */ /* http://www.schmalzhaus.com/EasyDriver/Examples/EasyDriverExamples.html */ // use the AccelStepper library with L297/L298 combination #include // since we use ISEL Stepper Driver, 2 driver pins are required // Define X stepper and the pins it will use - pin7 pulse train, pin6 dir AccelStepper stepper1(1, 5, 4); // Define Y stepper and the pins it will use - pin9 pulse train, pin8 dir AccelStepper stepper2(1, 6, 7); // first: 1 means DRIVER -> means that we use a driver like L297 (with Step and Direction pins) // If an enable line is also needed, call setEnablePin() after construction. // You may also invert the pins using setPinsInverted(). // second: Arduino digital pin number for motor pin 1. Defaults to pin 2. // For a AccelStepper::DRIVER (interface==1), this is the Step input to the driver. // Low to high transition means to step) // third: Arduino digital pin number for motor pin 2. Defaults to pin 3. // For a AccelStepper::DRIVER (interface==1), this is the Direction input the driver. // High means forward. /** 1. we receive a note for position: velocity 1-127 describes the final position - multiply by steps by motor turns to get to the final position 2. second note is speed 1-127 - multiply to get the sensible speed - if speed 3. third note tells about the direction: 0 or >0 - 4. do this for X and Y 5. we may need a zero positioning button to tell the machine the zero position - from this zero position we must always keep in memory the current position and the both limits - left and write **/ /* http://arduinomidilib.fortyseveneffects.com/a00025.html#ga414b3426cd08e148d612f94d3e462344 */ // receive input midi data and translate to stepper control #include // we can use MIDI lib or not const boolean usemidilib = true; // use Rx Tx arduino pins 0, 1 const boolean usemidi = false; const boolean printout = false; #define MIDICH 1 // define the midi channel to listen to #define LED 13 // LED pin on Arduino board #define ZEROPOS 2 // zero position button - pulled up #define TURNS1 1 // how many turns #define TURNS2 1 // how many turns // microstepping is the feature of stepper drivers: 1, 2, 4 for L297/298 in ISEL #define STEPS1 1 // define the microsteps if used - 1=1, 1/2=2, 1/4=4 etc #define STEPS2 1 // define the microsteps if used // microsteps enlarge the resolutiuon -> multiply by steps per revolution // calculate steps per revolution based on above two const long steps1 = STEPS1 * TURNS1; const long steps2 = STEPS2 * TURNS2; // from here on we use steps1 and steps2 to to check the positions // limits for both const long limit1 = -steps1 * TURNS1; // 200 steps per rotation * 3 rotations = 600 long zeroposition1 = 0; // this is check against a minus and plus limits // always remember the current position! long currentposition1 = 0; const long limit2 = steps2 * TURNS2; long zeroposition2 = 0; // this is check against a minus and plus limits // always remember the current position! long currentposition2 = 0; // when limits reached we need to stop nad wait for the next command // or turn the direction #define SPEEDFACTOR1 2 // multiply speed by this factor #define SPEEDFACTOR2 2 // multiply speed by this factor long speedlimit1 = 64 * SPEEDFACTOR1; long speedlimit2 = 64 * SPEEDFACTOR2; // variables // midi read byte midichannel = MIDICH; byte midinote = 0; byte midivelocity = 0; // globals to check with the RxTx serial MIDI IN OUT // may not be needed if MIDI lib works byte commandByte; byte noteByte; byte velocityByte; char ledstate = LOW; long lastledtime = millis(); // steppers definitions // adapted midi values long midipos1 = 0; long midispeed1 = 64 * SPEEDFACTOR1; long mididir1 = 0; long midipos2 = 0; long midispeed2 = 64 * SPEEDFACTOR2; long mididir2 = 0; // speed and position -> these two need to be long! // direction long pos1 = 200; long speed1 = 64 * SPEEDFACTOR1; byte dir1 = 0; long pos2 = 200; long speed2 = 64 * SPEEDFACTOR2; byte dir2 = 0; // temporary globals - before conversion byte mpos1; byte mpos2; byte mspeed1; byte mspeed2; byte mdir1; byte mdir2; // remember previous values long prevpos1 = 0; long prevspeed1 = 50; byte prevdir1 = 0; long prevpos2 = 0; long prevspeed2 = 50; byte prevdir2 = 0; void setup() { pinMode(LED, OUTPUT); // blink on receive digitalWrite(LED, LOW); // blink on receive pinMode(ZEROPOS, INPUT); digitalWrite(ZEROPOS, HIGH); // turn on pull up resistor // set the hardware interrupt for zeroposition pin //attachInterrupt(0, setZeroPosition, LOW); // int.0 on pin2 int.1 on pin 3 // LOW FALLING RISING CHANGE HIGH // output pins for stepper drivers get the output mode with the library // AccelStepper lib stepper1.setMaxSpeed(speedlimit1); // steps per s; 50, 127 works stepper1.setAcceleration(100); //Calling setAcceleration() is expensive, since it requires a square root to be calculated. stepper1.setMinPulseWidth(1000); //The minimum pulse width in microseconds - 10ms is ok stepper1.setCurrentPosition(0); //Resets the current position of the motor, so that wherever the motor happens to be right now is considered to be the new 0 position. stepper1.setSpeed(speedlimit1); //this allows to take in the new value // first move - one round stepper1.moveTo(200); // long absolute //stepper1.move(100); // long relative move stepper2.setMaxSpeed(speedlimit2); // steps per s; 50 works stepper2.setAcceleration(100); stepper2.setMinPulseWidth(1000); // 10ms stepper2.setCurrentPosition(0); //Resets the current position of the motor, so that wherever the motor happens to be right now is considered to be the new 0 position. stepper2.setSpeed(speedlimit2); //this allows to take in the new value // first move - one round stepper2.moveTo(200); //stepper2.moveTo(1000000); if(usemidilib){ // Initiate MIDI communications //MIDI.begin(MIDI_CHANNEL_OMNI); // listen to all channels //MIDI.begin(); // Launch MIDI with default options - input channel set to 1 MIDI.begin(MIDICH); // input channel is set to predefined value } // if we use the Tx Rx pins with MIDI IN OUT we need to use MIDI baud rate // Rx pin0, Tx pin1 if(usemidi) Serial.begin(31250); // Set MIDI baud rate else Serial.begin(9600); } void loop() { // timecheck to turn off led BlinkLedOff(); // check if zeropisition switch pushed if(digitalRead(ZEROPOS)==LOW){ setZeroPosition(); } if(usemidilib){ // we can use the interrupt function above or this below // Read messages from the serial port using the main input channel. if (MIDI.read() && MIDI.check()) { // Is there a MIDI message incoming? //if(MIDI.check()) // check if a valid message is stored in the structure // types: NoteOff NoteOn ControlChange AfterTouchPoly AfterTouchChannel PitchBend ProgramChange switch(MIDI.getType()) { // Get the type of the message we caught case NoteOn: // If it is a NoteOn //case ProgramChange: // If it is a Program Change //Get the channel of the message stored in the structure. //Channel range is 1 to 16. For non-channel messages, this will return 0 // if correct channel for this machine - read further - or ignore midichannel = MIDI.getChannel(); //MIDI.getInputChannel(); if(midichannel==MIDICH){ // Get the first data byte of the last received message. midinote = MIDI.getData1(); // get the note value = pitch // Get the second data byte of the last received message. midivelocity = MIDI.getData2(); // get the note velocity BlinkLed(); // Blink the LED now that motor data is on their way // do some action based on note received checkMIDI(midinote, midivelocity); } break; } } } else { // use above or below //if (Serial.available() > 0) { if (Serial.available()){ // filter by note - these are hex values! //commandByte = Serial.read();//read first byte //midichannel== // if command byte is channel //if(midichannel==MIDICH){ // if command byte is noteon //if(midichannel==MIDICH){ // BlinkLed(); // Blink the LED // noteByte = Serial.read();//read next byte // midinote = 0; // translate note hex byte to dec //velocityByte = Serial.read();//read final byte //checkMIDI(); //} //} } } //testSteppers(); steppersPosSpeedDir(); } void setZeroPosition() { // redefine the global zeroposition variable for both stepper1.setCurrentPosition(0); //Resets the current position of the motor, so that wherever the motor happens to be right now is considered to be the new 0 position. stepper2.setCurrentPosition(0); //Resets the current position of the motor, so that wherever the motor happens to be right now is considered to be the new 0 position. } // check what to do with received midi void checkMIDI(byte note, byte velocity){ // motor 1 // goto position if(note==1){ // this would be: with stepper1 go to position 0-127 - we need some multiplying if (velocity == 0) { // stop mpos1 = 0; } else { mpos1 = velocity; } // with speed } else if(note==2) { // this could be: speed for stepper1 0-127 - we need some multiplying here if (velocity == 0) { // stop mspeed1 = 0; } else { mspeed1 = velocity; } // use direction } else if(note==3) { // this could be: direction for stepper1 0 or >0 if (velocity == 0) { // left mdir1 = 0; } else { // right mdir1 = 1; } // motor 2 } else if(note==4) { // this could be: go with stepper2 to position 0-127 - we need some multiplying if (velocity == 0) { // stop mpos2 = 0; } else { mpos2 = velocity; } } else if(note==5) { // this could be: speed for stepper2 0-127 - we need some multiplying here if (velocity == 0) { // stop midispeed2 = 0; } else { midispeed2 = velocity; } } else if(note==6) { // this would be: direction for stepper2 0 or >0 if (velocity == 0) { // left mdir2 = 0; } else { // right mdir2 = 1; } } // with these globals adapt the stepper values adaptValues(); } void adaptValues(){ long pfactor1; long pfactor2; long sfactor1; long sfactor2; pfactor1 = limit1 / 127; pfactor2 = limit2 / 127; // adapt position number from max 127 to max limit1 midipos1 = mpos1 * pfactor1; midipos2 = mpos2 * pfactor2; if(midipos1>limit1) midipos1=limit1; if(midipos2>limit2) midipos2=limit2; // speed * SPEEDFACTOR2 sfactor1 = speedlimit1 / 127; sfactor2 = speedlimit2 / 127; midispeed1 = mspeed1 * sfactor1; midispeed2 = mspeed2 * sfactor2; if(midispeed1>speedlimit1) midispeed1=speedlimit1; if(midispeed2>speedlimit2) midispeed2=speedlimit2; // dir is ok } // AccelStepper lib // check details void steppersPosSpeedDir() { boolean change1=false; boolean change2=false; // here we use midi globals to adapt stepper globals // stepper receives moveTo position command // and runs // setting setSpeed(0) allows to take in the new value for moveTo()! // if some action occurs - like new note //stepper1.setSpeed(0); //stepper2.setSpeed(0); // we have globals: midipos1, midispeed1, mididir1 // ADAPT HERE THE VALUES // check for previous values - allow to change on the fly if(midipos1!=prevpos1){ prevpos1 = midipos1; // remember anew change1 = true; } if(midispeed1!=prevspeed1){ prevspeed1 = midispeed1; // remember anew change1 = true; } if(mididir1!=prevdir1){ prevdir1 = mididir1; // remember anew change1 = true; } if(change1 == true) { stepper1.setSpeed(0); // this allows to change the moveTo value stepper1.moveTo(midipos1); stepper1.setSpeed(midispeed1); } if(midipos2!=prevpos2){ prevpos2 = midipos2; // remember anew change2 = true; } if(midispeed2!=prevspeed2){ prevspeed2 = midispeed2; // remember anew change2 = true; } if(mididir2!=prevdir2){ prevdir2 = mididir2; // remember anew change2 = true; } if(change2 == true) { stepper2.setSpeed(0); // this allows to change the moveTo value stepper2.moveTo(midipos2); stepper2.setSpeed(midispeed2); } //stepper1.currentPosition() // Change direction at the limits if (stepper1.distanceToGo() == 0) stepper1.moveTo(-stepper1.currentPosition()); if (stepper2.distanceToGo() == 0) stepper2.moveTo(-stepper2.currentPosition()); // stepper1.distanceToGo() is long // this would mean: if distance is reached - turn direction if (stepper1.distanceToGo() == 0) { pos1 = -pos1; stepper1.moveTo(pos1); } if (stepper2.distanceToGo() == 0) { pos2 = -pos2; stepper2.moveTo(pos2); } // moveTo argument should be limited to what we can handle // currentPosition() -> the current step // distanceToGo() -> how far still to go // setMaxSpeed(300); // steps per ? // setAcceleration(100); //moveTo(600) // this must hppen as often as possible - in the main loop // - one step per loop // run steppers stepper1.run(); stepper2.run(); if(printout) { Serial.print(" current position: "); Serial.print(stepper1.currentPosition()); Serial.print(", speed: "); Serial.print(stepper1.speed()); Serial.print(", speed limit: "); Serial.println(midispeed1); // some delay delay(10); } } void testSteppers() { // this must be in the loop // stepper receives motoTo position command // and runs // setting this allows to take in the new value for moveTo()! // if some action occurs - like new note //stepper1.setSpeed(0); //stepper2.setSpeed(0); // moveTo argument should be limited to what we can handle // currentPosition() -> the current step // distanceToGo() -> how far still to go // setMaxSpeed(300); // steps per ? // setAcceleration(100); //moveTo(600) //stepper1.moveTo(600); // one round = 200 //stepper2.moveTo(600); // one round = 200 // Change direction at the limits and go back if (stepper1.distanceToGo() == 0){ // change speed? //stepper1.setMaxSpeed(200); // steps per sec? //stepper1.setAcceleration(50); stepper1.moveTo(-stepper1.currentPosition()); } if (stepper2.distanceToGo() == 0){ stepper2.moveTo(-stepper2.currentPosition()); } // must be in the loop each loop cycle! stepper1.run(); stepper2.run(); /** Serial.print(" values: "); Serial.print(stepper1.currentPosition()); Serial.print(", "); Serial.println(stepper2.currentPosition()); // some delay delay(10); **/ } void checkMIDI2(){ do{ if (Serial.available()){ commandByte = Serial.read();//read first byte noteByte = Serial.read();//read next byte velocityByte = Serial.read();//read final byte BlinkLed(); // Blink the LED a number of times } } while (Serial.available() > 2); //when at least three bytes available } // Basic blink function void BlinkLed() { digitalWrite(LED, HIGH); lastledtime = millis(); ledstate = HIGH; } void BlinkLedOff() { if(ledstate == HIGH) { if(millis()-lastledtime > 10){ //ms digitalWrite(LED, LOW); lastledtime = millis(); ledstate = LOW; } } } |
Boštjan ?adež made a much more streamlined program for the second SwivelSpeaker with another stepper driver – GECKO with abundant microstepping (MidiXYStepper.txt)
#include #include #include #include #include #include struct MySettings : public midi::DefaultSettings { // static const bool UseRunningStatus = false; // Messes with my old equipment! }; MIDI_CREATE_CUSTOM_INSTANCE(HardwareSerial, Serial1, MIDI, MySettings); #define USBMIDI 1 //for Teensy with usbMIDI; comment out if not #define MICROSTEP 1 #ifdef MICROSTEP #define MS1 14 #define MS2 15 #define MS3 16 #endif #define MOT1_STEP 19 #define MOT1_DIR 22 #define MOT2_STEP 20 #define MOT2_DIR 23 //#define MOT_DISABLE 23 #define STEPS_PER_REVOLUTION 3200 #define MOVEMENT_RANGE 1000 //movement range - negative and positive axis together (1.5 round in each direction) #define MAX_SPEED 183050 #define ACCELERATION 57850 #define MIDI_CHANNEL 2 #define LED 13 long lastCommandX=0; //last received coordinates long lastCommandY=0; boolean haveNewCommandX=false; boolean haveNewCommandY=false; // Define a stepper and the pins it will use AccelStepper stepper1(1,MOT1_STEP,MOT1_DIR); AccelStepper stepper2(1,MOT2_STEP,MOT2_DIR); #ifdef USBMIDI IntervalTimer ledTimer; #endif void setup() { #ifdef MICROSTEP pinMode(MS1,OUTPUT); pinMode(MS2,OUTPUT); pinMode(MS3,OUTPUT); digitalWrite(MS1,HIGH); digitalWrite(MS2,HIGH); digitalWrite(MS3,HIGH); #endif #ifdef USBMIDI usbMIDI.setHandleNoteOn(OnNoteOn) ; usbMIDI.setHandleControlChange(OnNoteOn) ; #endif MIDI.setHandleNoteOn(OnNoteOn); MIDI.setHandleControlChange(OnNoteOn); MIDI.begin(MIDI_CHANNEL); stepper1.setMaxSpeed(MAX_SPEED); stepper1.setAcceleration(ACCELERATION); stepper2.setMaxSpeed(MAX_SPEED); stepper2.setAcceleration(ACCELERATION); // pinMode(MOT_DISABLE,OUTPUT); // digitalWrite(MOT_DISABLE,LOW); pinMode(LED,OUTPUT); } void loop() { #ifdef USBMIDI usbMIDI.read(MIDI_CHANNEL); #endif MIDI.read(MIDI_CHANNEL); stepper1.run(); stepper2.run(); } void OnNoteOn(byte channel, byte note, byte velocity) { #ifdef USBMIDI digitalWrite(LED,HIGH); ledTimer.begin(turnOffLed,100000); #endif switch(note) { case 0:// x os { //haveNewCommandX=true; lastCommandX=((float)velocity/127.f-.5f)*MOVEMENT_RANGE; stepper1.moveTo(lastCommandX); break; } case 1:// y axis { //haveNewCommandY=true; lastCommandY=((float)velocity/127.f-.5f)*MOVEMENT_RANGE; stepper2.moveTo(lastCommandY); break; } case 2:// speed { stepper1.setMaxSpeed(((float)velocity/127.f)*MAX_SPEED); stepper2.setMaxSpeed(((float)velocity/127.f)*MAX_SPEED); break; } } //usbMIDI.sendNoteOn(note,velocity,channel); } void turnOffLed() { digitalWrite(LED,LOW); ledTimer.end(); } |