Rotary table/indexer controller.

Naively, if there is a negative step count, shouldn't one take the absolute value of the counts and reverse the direction to the stepper? I have not looked at the code.

Yes, that will keep the steps going in the correct direction, but it masks the problem that it should not go negative in the first place.
 
Yes, that will keep the steps going in the correct direction, but it masks the problem that it should not go negative in the first place.
Why can't they go negative? Again, perhaps a naive question.
 
I found it! I had a condition while moving CCW that I wasn't testing for "current == 360". I was resetting the counters for everything but that. In operation my display should never show 360 degrees, only "0.00".

More testing needed but everything appears to work now.
 
Why can't they go negative? Again, perhaps a naive question.
Because I don't want them to. Simple as that. If all the computations are positive It's easier for me to model on a spreadsheet.
 
Well I thought I had it fixed. I fails in different ways based on the number of divisions entered. Works perfect for some. For others it gets weird.
At 7 divisions the motor runs backward when you pass zero, at 13 the motor stops and skips that step. Maybe it's superstitious.

Hmmm...
 
I have it sorted out now. The borrowed code I started with was using the same procedure to move the motor and update the display, making the motor dependent to the display. I got rid of that and now the motor procedure acts like it should for whatever the input is. The display updates are now dependent on the motor position and are calculated separately. When I started I had made a comment in the code that the main procedure had too much stuff going on. Little did I know.
 
After a couple of weeks I have software I can use. It's accurate at least and, aside from the occasional scrambling of my special display characters, it hasn't failed in use.

It only has the two modes, Degree and Divisions. I can jog by entering a number of degrees and holding the movement key. For "continuous" I just set 1 division or 360+ degrees.

It's meant to do all the movement going clockwise in order to control backlash. It compensates for counter clockwise backlash by overshooting the reading and coming back to it from clockwise. My table has .2 degrees backlash, so the overshoot is .3 degrees. It works.

It will not make initial moves going counter clockwise. It will also not drive past the origin point going counter clockwise. I tried to figure out why I would want to index while counting backward and decided it was a bad idea.

It can toggle the motor on and off to make manual adjustments, and reminds the user of the motor status.

It can vary the stepping speed between moves, once the motor has stopped.

I think I've covered the situations where I plan to use it.

Eric

Code:
//Program for controlling a single stepper motor driving a rotary table.
//Uses I2C 4x4 matrix keypad for entry of degrees or number of divisions, and direction to move the table.
//Uses I2C 20 character by 4 line display.
//Uses bipolar stepper motor with TB6560 driver.



#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <I2CKeyPad.h>


//SETUP VARIABLES
#define dirPin 2
#define stepPin 3
#define enablePin 5

//CUSTOM CHARACTER BIT MAPS
//NOTE: These don't always display correctly without performing softwareReset()!
byte customChar0[8] = { //this is a CCW arrow symbol
  0b10110,   // * **
  0b11001,   // **  *
  0b11101,   // *** *
  0b00001,   //     *
  0b00001,   //     *
  0b10001,   // *   *
  0b01110,   //  ***
  0b00000
};

byte customChar1[8] = { //this is a CW arrow symbol
  0b01101,   //  ** *
  0b10011,   // *  **
  0b10111,   // * ***
  0b10000,   // *
  0b10000,   // *
  0b10001,   // *   *
  0b01110,   //  ***
  0b00000
};

const uint8_t KEYPAD_ADDRESS = 0x20; //keypad I2C address
const uint8_t LCD_ADDRESS = 0x3f;    //display I2C address
const int stepsPerRevolution = 400;  //motor steps per revolution
const int TableRatio = 72;           //ratio of rotary table shaft turns to 360 degrees
const long Multiplier = (stepsPerRevolution * TableRatio);
const int MaxSpeed = 500;            //minimum step delay for motor
const int MinSpeed = 1250;           //maximum step delay for motor

char keymap[19] = "147.2580369#ABCDNF"; //keypad map, N = NoKey, F = Fail, listed by columns

char key;                     //input from keypad
float Degrees = 0.;           //Degrees/move desired
long ToMove = 0;              //Steps to move
float Divisions;              //number of table postitions desired
float current = 0.;           //expected final table position after move in degrees
long cumSteps = 0;            //total steps sent to motor
int Mode = 0;                 //selected operating mode
int MotorSpeed = 500;         //initial motor step delay in micro seconds
bool newKey = false;          //was a key pressed status flag, set to true when a new key is read
bool MotStat = true;          //motor dirver enable status flag



//CREATE EXTERNAL DEVICES

I2CKeyPad keyPad(KEYPAD_ADDRESS); //create keyPad

LiquidCrystal_I2C lcd(LCD_ADDRESS,20,4);  // create 20x4 lcd



//START OF PROGRAM//

void setup()
{
  lcd.createChar(0, customChar0); //create CCW arrow character from bit map
  lcd.createChar(1, customChar1); //create CW arrow character from bit map
 
  pinMode(stepPin, OUTPUT);       //motor control pins as outputs
  pinMode(dirPin, OUTPUT);
  pinMode(enablePin, OUTPUT);

  digitalWrite(enablePin, LOW); //enable motors now or nothing works
 
  if (keyPad.begin() == false) //make sure keypad is working
    {
        lcd.setCursor(0,0);
        lcd.print("Keypad Fail"); //tell if it isn't
        delay(2500);
        lcd.clear();
      while (1);
    }

  keyPad.loadKeyMap(keymap); //create the keymap for the keypad
 
  lcd.init();                  //initialize the lcd
  lcd.backlight();             //turn on the backlight
  lcd.setCursor(0,0);
  lcd.print("       Eric's");  //display welcome message
  lcd.setCursor(0,1);
  lcd.print("   Rotary Indexer");
  lcd.setCursor(0,2);
  lcd.print("    Version 3.0");
  lcd.setCursor(7,3);
  lcd.write((byte)0);         //display custom characters
  lcd.print("   ");
  lcd.write((byte)1);
  delay(2500);
}



//This procedure restarts program from beginning but does not reset the peripherals and registers
//This uses an absolute call to the staring point in the processor memory
//Bang the Hardware!
void software_Reset()
{
  asm volatile ("  jmp 0"); //jump to start of processor memory
}



//This procedure pulses the step pin on the driver.
//Stopping the motor during this procedure will require a hard reset or panic switch!
//Inputs: movement amount in steps
//Uses: MotorSpeed
void rotation(long tm)
{
  for (int i = 0; i < tm; i++) {
    // These four lines result in 1 step:
    digitalWrite(stepPin, HIGH);
    delayMicroseconds(MotorSpeed);
    digitalWrite(stepPin, LOW);
    delayMicroseconds(MotorSpeed);
  }
}



//This procedure checks for a new keypress by polling the keypad and updates the value of key
//Modifies: char key, bool newKey
void GetKey()
{
  if (keyPad.isPressed())   //check if a key has been pressed
  {
    key = keyPad.getChar(); //this gets the mapped char of the last key pressed
    delay(250);             //allow time to release key
    newKey = true;          //tell rest of program "key" has new value
  }
}



//This procedure gets a floating point number with decimal from the keypad and returns it
//to the calling procedure.
//Uses: GetKey()
//Modifies: bool newKey
//Returns: float decnum
float GetNumber()
{
   float num = 0.00;
   float decimal = 0.00;
   float decnum = 0.00;
   int counter = 0;
   GetKey();

   lcd.setCursor(0,0);lcd.print("Enter degrees then");lcd.setCursor(0,1);lcd.print("    press [#].");
   lcd.setCursor(0,3);lcd.print("  Restart [D]");
   lcd.setCursor(8,2);
   bool decOffset = false;

   while(key != '#')
   {
    if (newKey == true)
    {
      switch (key)
      {
         case 'N':
            break;
        
         case '.':
           if(!decOffset)
           {
             decOffset = true;
           }
            lcd.print(key);
            newKey = false;
            break;
      
         case '0': case '1': case '2': case '3': case '4':
         case '5': case '6': case '7': case '8': case '9':
           if(!decOffset)
           {
            num = num * 10 + (key - '0');
            lcd.print(key);
           }
           else if((decOffset) && (counter <= 1))
           {
            num = num * 10 + (key - '0');
            lcd.print(key);
            counter++;
           }
           newKey = false;
           break;

         case 'D':
           software_Reset();
         break;
      }

      decnum = num / pow(10, counter);
 
   }
   newKey = false;
   GetKey();
   }
  return decnum;
}



//This procedure gets a floating point number with no decimal from the keypad and returns it
//to the calling procedure.
//Uses: GetKey()
//Modifies: bool newKey
//Returns: float num
float GetDivisions()
{
   float num = 0.00;
   GetKey();

   lcd.clear();
   lcd.setCursor(0,0);lcd.print("Enter Divisions then");lcd.setCursor(0,1);lcd.print("     press [#].");
   lcd.setCursor(0,30);lcd.print("  Restart [D]");
   lcd.setCursor(8,2);

   while(key != '#')
   {
     if(newKey == true)
     {
       switch (key)
       {
         case 'N':
            break;
        
         case '0': case '1': case '2': case '3': case '4':
         case '5': case '6': case '7': case '8': case '9':
            num = num * 10 + (key - '0');
            lcd.print(key);
            newKey = false;
            break;
    
        case 'D':
          software_Reset();
          break;
       }
     }
     newKey = false;
     GetKey();
   }
  return num;
}



//This procedure gets the selected mode from keypad and returns it tothe main loop.
//Allows user to reset program.
//Uses: GetKey(), software_Reset()
//Modifies: bool newkey
//Returns: int mode
int GetMode()
{
  int mode = 0;
  lcd.clear();
  lcd.setCursor(0,0);lcd.print("    Select Mode");
  lcd.setCursor(0,1);lcd.print("   [A] Divisions");
  lcd.setCursor(0,2);lcd.print("   [B] Degrees");
  while(mode == 0)
    {
     GetKey();
     if(key == 'A')
       {
         mode = 1;
       }
     if(key == 'B')
       {
         mode = 2;
       }
     if(key == 'D')
       {
         software_Reset(); //hidden reset in case **** don't work
       }
    }
  newKey = false;
  lcd.clear();
  return mode;
}



//There is too much stuff happening in the main loop!
//Uses: GetMode(), GetDivisions(), GetNumber(), rotation(), MotorOn(), MotorOff(), ChgSpeed()
//      updateDisplay(), GetKey()
//Modifies: Divisions, Degrees, current, cumSteps, newKey, ToMove, dirPin
void loop()
{
  Mode = GetMode();
  if(Mode == 1)
    {
      Divisions = GetDivisions(); //get desired divisions from keypad
      Degrees = (360/Divisions);  //convert to degrees
    }
  if(Mode == 2)
    {
      Degrees = GetNumber();      //get desired degrees from keypad
    }
    current = 0;   //reset current position with mode change
    cumSteps = 0;  //reset step counter with mode change
 
    lcd.clear();   //setup the display for table movements
    lcd.setCursor(0,0);lcd.print("Degrees/Move: ");lcd.print(Degrees);
    lcd.setCursor(0,1);lcd.print("POS: ");lcd.print(current);
    lcd.setCursor(1,3);lcd.write((byte)0);lcd.print(" [1] ");
    lcd.write((byte)1);lcd.print(" [3] Canx[C]");
 
    GetKey();      //get input from user

    while(key != 'C')              //mode not cancelled
      {
        if(newKey == true)         //a key has been pressed
          {
            switch(key)            //see if it's one we care about
              {
               case 'N':           //No key press, not sure if this case can happen
                 break;            //but if it does, this will save a few clock cycles
      
               case '3':                            // Rotate CW
                 newKey = false;                    // Clear key flag
                 if(MotStat == true)                // Do this if motor is on
                   {
                     current = current + Degrees;   // Add new degrees to current total
                     ToMove = (current/360) * Multiplier + 0.5 - cumSteps; // this eliminates partial steps
                     cumSteps = cumSteps + ToMove;     // Add move to step counter

                     lcd.setCursor(0,3);lcd.print("                    ");
                     lcd.setCursor(0,2);
                     lcd.print("     Moving ");
                     lcd.write((byte)1);             //display CW character
                     digitalWrite(dirPin, HIGH);
                     rotation(abs(ToMove));         //don't know if abs() is still needed
                     updateDisplay();
                   }
                 else                               //if motor is off
                   {
                     lcd.setCursor(0,2);
                     lcd.print("    Motor OFF");   //just a reminder
                     delay(1000);
                     lcd.setCursor(0,2);
                     lcd.print("                 ");
                   }
                 break;
      
               case '1':                           // Rotate CCW
                 newKey = false;                   // Clear key flag
                 if(MotStat == true)               // Do this if motor is on
                   {
                     current = current - Degrees;
                     if(cumSteps <= 0)             //prevents first move in reverse
                       {
                         current = 0;
                         break;
                       }
                     else
                       {
                         ToMove = cumSteps + 0.5 - (current * Multiplier)/360; // this eliminates partial steps
                         cumSteps = cumSteps - ToMove;
                       }
                  
                     lcd.setCursor(0,3);lcd.print("                    ");
                     lcd.setCursor(0,2);
                     lcd.print("     Moving ");
                     lcd.write((byte)0); //display CCW character
                     digitalWrite(dirPin, LOW);
                     rotation(abs(ToMove));        //don't know if abs() is still needed
                     rotation(24);                 //add .3 degrees overshoot for backlash
                     //delay(2000);                  //just for testing
                     digitalWrite(dirPin, HIGH);
                     rotation(24);                 //bring it back .3 degrees to target setting
                     updateDisplay();
                   }
                 else                              //if motor is off
                   {
                     lcd.setCursor(0,2);
                     lcd.print("    Motor OFF");   //just a reminder
                     delay(1000);
                     lcd.setCursor(0,2);
                     lcd.print("                 ");
                   }
                 break;

               case '4':
                 newKey = false;
                 MotorOff();
                 break;

               case '6':
                 newKey = false;
                 MotorOn();
                 break;

               case '9': case '7':
                 newKey = false;
                 ChgSpeed();
                 break;

             newKey = false;
            }
        }
          GetKey();
      }
      lcd.clear();
}


//This proceedure updates display after table moves to show current degrees always < 360.
//Uses: current
void updateDisplay()
{
  float dd;
  dd = current;
  while(dd >= 360){
    dd = dd - 360;
  }
  lcd.setCursor(0,2);lcd.print("                    ");
  lcd.setCursor(4,1);lcd.print("        ");
  lcd.setCursor(5,1);lcd.print(dd);
  lcd.setCursor(1,3);lcd.write((byte)0);lcd.print(" [1] ");
  lcd.write((byte)1);lcd.print(" [3] Canx[C]");
}



//This procedure changes the step delay to increase or decrease motor speed.
//Uses: key, MinSpeed, MaxSpeed
//Modifies: MotorSpeed
void ChgSpeed()
{
      if (key == '9')
        {
          if (MotorSpeed > MaxSpeed)         //has it reached maximum speed?
            {
              MotorSpeed = MotorSpeed - 250; //decrease step delay by 250 micro seconds
            }
        }
      if (key == '7')
        {
          if (MotorSpeed <= MinSpeed)        //has it reached minimum speed?
            {
              MotorSpeed = MotorSpeed + 250; //increase step delay by 250 micro seconds
            }
        }
        lcd.setCursor(0,2);
        lcd.print("  Motor Speed: ");
        lcd.print(MotorSpeed);
        delay(500);
        lcd.setCursor(0,2);
        lcd.print("                    ");
}


//This procedure disables the motor driver alowing motor shaft to be turned by hand
//Uses: MotStat
//Modifies: enablePin, MotStat
void MotorOff()
{
  if (MotStat == true)
    {
      digitalWrite(enablePin, HIGH);
      lcd.setCursor(0,2);
      lcd.print("    Motor OFF");
      delay(1000);
      lcd.setCursor(0,2);
      lcd.print("                 ");
      MotStat = false;
    }
}

//This procedure enables the motor driver and locks the motor shaft
//Uses: MotStat
//Modifies: enablePin, MotStat
void MotorOn()
{
  if (MotStat == false)
    {
      digitalWrite(enablePin, LOW);
      lcd.setCursor(0,2);
      lcd.print("    Motor ON");
      delay(1000);
      lcd.setCursor(0,2);
      lcd.print("                 ");
      MotStat = true;
    }
}


//END OF PROGRAM//
 
Last edited:
That kind of stuff worries me. Compiler rules and behavior can change over time... Not saying it is true for you, but I have experienced that. Is it that the Arduino can't do the math reliably and convert to integer steps? I guess that is what you are doing?

Failing steps past zero sounds like a unsigned integer problem. All the integers need to be signed, not uint16_t, or uint32_t. Need to be int16_t or int32_t. I manually do the math for going negative to see if it is correct. What you find may surprise you. It happens to me, occasionally, which is why I check my math...
I found the correct method to cast data types from floating point to integer is "y = int(x);" where y is the integer variable and x is the floating point variable. I guess the arduino compiler doesn't always need the explicit command.
 
Here is a picture of the working controller with Version 4 software almost ready to go in the box. All the code I've published is now obsolete. I stripped out all the fancy stuff that never really worked, reworked some of the procedures, added a hardware interrupt for the keypad, and added a very nice (IMHO) Jog mode.

The jog mode works in two ways. 1) The table will turn while the CCW or CW (1 or 3) key is held and stop when the key is released, which is what I wanted. Or 2) the table run continuously if the 1 or 3 key is tapped and released quickly, and stop when the key or any other key in the same column is pressed. This is a quirk of the interrupt timing. I didn't expect it but I like the results.

controller.jpg
 
Back
Top