Help Needed: Reading Serial Data and processing for Stepper Motor Driver

Hi All,

I am working on building linear actuators for a motion simulator using the outputs from SimTools.

I have merged and adapted several sketches and it works as expected when I send commands via the Serial Monitor. When I run SimTools and the commands being send are constant and stable, the stepper motor moves erratically.

The data from SimTools is in the format P[###]~ with the range from P0~ to P255~

Any feedback on getting this working as the next stage is to add the 2nd and 3rd axis to create a 3DOF motion platform.

Colin

//********************************************************************************************
// Simtools Stepper 1 Axis code
// Based on code By EAOROBBIE (Robert Lindsay)
// By Peter Brennan
// With stepper acceleration and motion code by iforce2d
// https://www.youtube.com/watch?v=fHAO7SW-SZI
//********************************************************************************************

#define STEP_PIN         8
#define DIR_PIN          11
#define STEP_HIGH        digitalWrite(STEP_PIN, HIGH);
#define STEP_LOW         digitalWrite(STEP_PIN, LOW);
#define TIMER1_INTERRUPTS_ON    TIMSK1 |=  (1 << OCIE1A);
#define TIMER1_INTERRUPTS_OFF   TIMSK1 &= ~(1 << OCIE1A);

unsigned int c0;
const char kEOL = '~';                              // End of Line - the delimiter for our acutator values
const int kMaxCharCount = 3;                        // some insurance...
int valueCharCount = 0;                             // how many value characters have we read (must be less than kMaxCharCount!!
int actuatorPosition = 0;                           // current Actuator position, initialised to 0
int newActuatorPosition = 0;                        // initialise variable to collect new motor position

int currentState = 1;

volatile int dir = 0;
volatile unsigned int maxSpeed = 30;
volatile unsigned long n = 0;
volatile float d;
volatile unsigned long stepCount = 0;
volatile unsigned long rampUpStepCount = 0;
volatile unsigned long totalSteps = 0;
volatile int stepPosition = 0;

volatile bool movementDone = false;

void setup()
{
  pinMode(STEP_PIN,   OUTPUT);
  pinMode(DIR_PIN,    OUTPUT);

  noInterrupts();
  TCCR1A = 0;
  TCCR1B = 0;
  TCNT1  = 0;
  OCR1A = 1000;
  TCCR1B |= (1 << WGM12);
  TCCR1B |= ((1 << CS11) | (1 << CS10));
  interrupts();

  c0 = 1600; // was 2000 * sqrt( 2 * angle / accel )

  Serial.begin(9600); // opens serial port at a baud rate of 9600
}

void loop()
{

}

ISR(TIMER1_COMPA_vect)
{
  if ( stepCount < totalSteps ) {
    STEP_HIGH
    STEP_LOW
    stepCount++;
    stepPosition += dir;
  }
  else {
    movementDone = true;
    TIMER1_INTERRUPTS_OFF
  }
  if ( rampUpStepCount == 0 ) { // ramp up phase
    n++;
    d = d - (2 * d) / (4 * n + 1);
    if ( d <= maxSpeed ) { // reached max speed
      d = maxSpeed;
      rampUpStepCount = stepCount;
    }
    if ( stepCount >= totalSteps / 2 ) { // reached halfway point
      rampUpStepCount = stepCount;
    }
  }
  else if ( stepCount >= totalSteps - rampUpStepCount ) { // ramp down phase
    n--;
    d = (d * (4 * n + 1)) / (4 * n + 1 - 2);
  }
  OCR1A = d;
}

void moveNSteps(long steps) {
  digitalWrite(DIR_PIN, steps < 0 ? HIGH : LOW);
  dir = steps > 0 ? 1 : -1;
  totalSteps = abs(steps);
  d = c0;
  OCR1A = d;
  stepCount = 0;
  n = 0;
  rampUpStepCount = 0;
  movementDone = false;
  TIMER1_INTERRUPTS_ON
}

void moveToPosition(long p, bool wait = true) {
  moveNSteps(p - stepPosition);
  while ( wait && ! movementDone );
}


// this code only runs when we have serial data available. ie (Serial.available() > 0).
void serialEvent() {
  char tmpChar;
  int tmpValue;
  while (Serial.available()) {
    // if we're waiting for a Actuator name, grab it here
    if (currentState == 0) {
      tmpChar = Serial.read();
      if (tmpChar == 'P') {
        currentState = 1;                 // start looking for the Actuator position
        valueCharCount = 0;               // initialise number of value chars read in
        break;
      }
    }

    // if we're ready to read in the current Actuator's position data
    if (currentState == 1) {
      while ((valueCharCount < kMaxCharCount) && Serial.available()) {
        tmpValue = Serial.read();
        if (tmpValue != kEOL) {
          tmpValue = tmpValue - 48;
          if ((tmpValue < 0) || (tmpValue > 9)) tmpValue = 0;
          // Determine the new position for the stepper
          newActuatorPosition = newActuatorPosition * 10 + tmpValue;
          valueCharCount++;
        }
        else break;
      }

      // if we've read the value delimiter, update the Actuator and start looking for the next Actuator name
      if (tmpValue == kEOL || valueCharCount == kMaxCharCount) {
        // Transfer the new actuator value to the current one, and reset the new position
        actuatorPosition = map (newActuatorPosition, 0, 255, 0, 2000);
        moveToPosition( actuatorPosition );
        tmpChar = 0;
        tmpValue = 0;
        valueCharCount = 0;
        currentState = 0;
        newActuatorPosition = 0;
      }
    }
  }
}

The code blocks in the moveToPosition() function. Remove that while loop and it should work much better.

MorganS:
The code blocks in the moveToPosition() function. Remove that while loop and it should work much better.

Hi MorganS,

That is definitely at improvement. I commented out all 3 lines that mention movementDone, which removes the while loop in moveToPosition(). The motor moves to the location being sent from SimTools and stops.

The issue now is that when I send the command from the Serial Monitor it runs fast and smooth (e.g. P127~ from the 0 position is 5 revolutions and takes less that 0.4 seconds) but when I send the serial data it is coggy and rough and takes 4.40 seconds.

Colin

I would not use serialEvent(). It just checks for serial data in every iteration of loop() even though you probably don't need that - especially as your messages are very short and won't overflow the Serial Input Buffer.

I have a simple program to control stepper motors and it gets the data from the buffer when it has finished (say) move 23. That way the movement is not upset by reading the buffer. When my program has got the data from the buffer and is ready to start move 24 it signals the PC to send data for move 25 so it will be waiting in the buffer when the move 24 is complete.

...R

Hi Robin2,

The reason this code uses serialEvent() is because with the motion simulator it doesn’t matter if the motion 100% completed. What matters most is that the direction of the motion matches the visual information.

Below is prototype code that has 3DOF motion and moves RC servos using the serialEvent() code.

//********************************************************************************************
// RC Model Servo
// Original code By EAOROBBIE (Robert Lindsay)
//********************************************************************************************
#include <Servo.h>
const int kActuatorCount = 3;                       // how many Actuators we are handling

// the letters ("names") sent from Sim Tools to identify each actuator
// NB: the order of the letters here determines the order of the remaining constants kPins and kActuatorScale
const char kActuatorName[kActuatorCount] = { 'P', 'R', 'Y'};
const int kPins[kActuatorCount] = {8, 9, 10};                      // pins to which the Actuators are attached
const int kActuatorScale[kActuatorCount][2] = { { 0, 179 } ,
                                                { 179, 0 } ,
                                                { 179, 0 }
                                              };
const char kEOL = '~';                                 // End of Line - the delimiter for our acutator values 
const int kMaxCharCount = 3;                           // some insurance...
Servo actuatorSet[kActuatorCount];                     // our array of Actuators
int actuatorPosition[kActuatorCount] = {90, 90,90};    // current Actuator positions, initialised to 90
int currentActuator;                                   // keep track of the current Actuator being read in from serial port
int valueCharCount = 0;                                // how many value characters have we read (must be less than kMaxCharCount!!

// set up some states for our state machine
// psReadActuator = next character from serial port tells us the Actuator
// psReadValue = next 3 characters from serial port tells us the value
enum TPortState { psReadActuator, psReadValue };
TPortState currentState = psReadActuator;

void setup()
{
    // attach the Actuators to the pins
    for (int i = 0; i < kActuatorCount; i++) 
        actuatorSet[i].attach(kPins[i]);

    // initialise actuator position
    for (int i = 0; i < kActuatorCount; i++) 
        updateActuator(i);

    Serial.begin(9600); // opens serial port at a baud rate of 9600
}

void loop()
{

}

// this code only runs when we have serial data available. ie (Serial.available() > 0).
void serialEvent() {
    char tmpChar;
    int tmpValue;

    while (Serial.available()) {
        // if we're waiting for a Actuator name, grab it here
        if (currentState == psReadActuator) {
            tmpChar = Serial.read();
            // look for our actuator in the array of actuator names we set up 
            for (int i = 0; i < kActuatorCount; i++) {
                if (tmpChar == kActuatorName[i]) {
                    currentActuator = i;                        // remember which actuator we found
                    currentState = psReadValue;                 // start looking for the Actuator position 
                    actuatorPosition[currentActuator] = 0;      // initialise the new position
                    valueCharCount = 0;                         // initialise number of value chars read in 
                    break;
                }
            }
        }

        // if we're ready to read in the current Actuator's position data
        if (currentState == psReadValue) {
            while ((valueCharCount < kMaxCharCount) && Serial.available()) {
                tmpValue = Serial.read();
                if (tmpValue != kEOL) {
                    tmpValue = tmpValue - 48;
                    if ((tmpValue < 0) || (tmpValue > 9)) tmpValue = 0;
                    actuatorPosition[currentActuator] = actuatorPosition[currentActuator] * 10 + tmpValue;
                    valueCharCount++;
                }
                else break;
            }
            
            // if we've read the value delimiter, update the Actuator and start looking for the next Actuator name
            if (tmpValue == kEOL || valueCharCount == kMaxCharCount) {
                // scale the new position so the value is between 0 and 179
                actuatorPosition[currentActuator] = map(actuatorPosition[currentActuator], 0, 255, kActuatorScale[currentActuator][0], kActuatorScale[currentActuator][1]);
                updateActuator(currentActuator);
                currentState = psReadActuator;
            }
        }
    }
}


// write the current Actuator position to the passed in Actuator 
void updateActuator(int thisActuator) {
    actuatorSet[thisActuator].write(actuatorPosition[thisActuator]);
}

This works as expected and has no issue with communication. Here is my video of the prototype working (please forgive the voice over… this vid was made to show my friend the project progress :slight_smile: )

I feel I must be missing something simple in adapting the RC servo code to pulse and direction signals for the stepper. I have reduced the complexity to only one axis in trying to get this working.

Colin

ManiacMotion:
The issue now is that when I send the command from the Serial Monitor it runs fast and smooth (e.g. P127~ from the 0 position is 5 revolutions and takes less that 0.4 seconds) but when I send the serial data it is coggy and rough and takes 4.40 seconds.

You're saying it works OK the first time it moves away from zero but then further moves from that point are coggy? Maybe there's something wrong with the "ramp up step count" that doesn't reset properly for the second move.

It only works as expected (smooth, fast, correct acceleration) when I send a command (or even a line of commands) from the Serial Monitor.

As soon as I send a stream of commands from the game interface, it is slow, and notchy.

So when the second command arrives "early" and directs it to a new position before it's even finished accelerating towards the previous commanded position, what does that do to the acceleration code?

Double-check the commands that were actually sent. Make sure there's nothing else in there which is clogging up the data.

My intention is that when the second command arrives it should immediately start moving towards the new location and ignore trying to "finish" the previous command. New commands are sent at 10ms intervals so it is 100% likely that the last command hasn't completed before the next arrives.

I have monitored the serial port and only clean P###~ commands are being sent.

What if you get a command to go to 170 and start moving, then 10ms later, get another command to go to 170? I think that will reset the acceleration to the slowest speed, won't it?

Reversing direction will also be a problem as it doesn't slow down to stop - it just goes instantly to slow-reverse from whatever speed it had.

So I think you need a little more 'smarts' between accepting serial commands and sending steps to the motor.

ManiacMotion:
My intention is that when the second command arrives it should immediately start moving towards the new location and ignore trying to "finish" the previous command. New commands are sent at 10ms intervals so it is 100% likely that the last command hasn't completed before the next arrives.

That seems a very strange way to design anything.

I strongly suspect that it would be sufficient to send new commands at 200ms intervals.

What size is the motion simulator machine that is being moved?

What is generating the movement commands? Is it a human?

...R

@MorganS
In the center position there is a continual stream at 10ms intervals of P127~ The would explain why when I send a discrete command it works, but on the continuous stream it doesn't.

@Robin2
With motion simulators, the actual amplitude of the movement (exact position) is not as important as the timing of the motion with the game. If you accelerate hard the nose of the car pitches up but quickly settles as speed increases. If the initial command to move wasn't complete it doesn't matter compared to re-positioning based on subsequent commands.

The movement commands are computer generated from the G-force information supplied from the driving game. The "slowest" I can send commands is with a 50ms delay. Most simulators use an 8-10ms output rate to achieve realistic results.

The motion rig is a single seat car with 3 screens. Here is a video of the rig running (poorly) on commercial SCN6 linear actuators: iRacing Simtools with 3 x SCN6 - YouTube

As I mentioned earlier I built a prototype rig using the Arduino Servo.h library and it handled 10ms streaming commands no problems at all: Simtools Arduino Test with RC Servos - YouTube

I am amazed (and somewhat sceptical) that a device containing a person can more usefully respond to commands at 10ms intervals compared to (say) 200ms intervals.

This seems to me a situation where the dynamics of the real device will be very different from the model to the extent that I see very little value in using the model - other than to explore very broad concepts, such as making sure the correct input is being used for pitch rather than roll.

Also, it seems to me that if the commands are to be issued at a high frequency none of them should be considered as a complete movement. In other words moveToPosition() is inappropriate. It should simply be move-at-speed(S)-and-direction(D) - the sort of "command" you would give to a DC motor.

If you are using high-torque stepper motors then you will need to apply acceleration and deceleration if they are not to miss steps. That means, for example, that a motor that is moving quickly clockwise cannot stop instantly or reverse direction instantly. If that is the sort of behaviour you need then it seems to me that a DC motor would be a lot more suitable.

And just to close the logical circle, I don't see how a high-torque stepper motor can respond quickly enough for commands sent at 10ms intervals to be meaningful.

...R

I don’t think we need to change anything on the PC side. The commands coming out are well-formed and aren’t subject to glitches.

Since it’s a simulation of a physical system it’s not going to suddenly command a movement to the other side of the park with a totally different move coming 10ms later. If the steps were 1:1, you could just simply make steps on the stepper based on the raw data from the PC. If it starts to move away from 0 then it’s only going to command a move to 1 and 10ms later command a move to 2. That fixes the maximum step rate pretty easily.

However you are mapping commanded positions to actual motor steps. It seems like you are mapping 0-255 to 0-179. So you have less than one step per command. Problem solved. Ditch all of the timer stuff. Remove the acceleration, remove the moveToPosition crap…

  if(newSerialCommandReceived) {
    if(newPosition > currentPosition) {
      oneStepForward();
    } else if(newPosition < currentPosition) {
      oneStepBackward();
    }
    newSerialCommandReceived = false;
  }

This means it can only move when there’s serial data. If someone trips over the cable, it stops where it is. If the PC sends a ‘long distance’ command then it must be maintained with that command being sent many many times before it actually arrives at that place. At 100 steps per second, it can take at most 1.79 seconds to travel over the full range. Not fast, but probably adequate for the purpose.

If the mapping went the other way, say 9.5 motor steps per command step, then you would have to deal with acceleration, deceleration and continuing a movement to a target position without serial data.

If I understand Reply #13 properly (and I may not) it is talking about sending each individual step instruction one by one from the PC.

That's going to mean a very slow maximum step rate and all of the step-timing uncertainty that is inherent in the PC operating system. One of the reasons people use Arduinos for CNC and 3D printing projects is to achieve high precision with step timing.

...R

MorganS

The 255 to 179 mapping is in the RC servo version only. The original code I posted maps 255 to 2000 steps (10 revolutions). In the final version I need 100mm of movement using a 1.5mm pitch lead screw. At 200 steps per revolution that means approximately 13300 steps for 100% travel.

I have the option of changing the output format from 8bit all the way up to 16bit, and the baud rate up to 921600 so the resolution and data speed can be refined as much as I need. It is inevitable that I need to manage accelerations on this project as the top motor speed will need to be over 5000rpm.

Robin2

The data rate in the interface program is selectable from 0ms to 50ms. The reason I built the RC servo model was to write and confirm the serialEvent() code was receiving and processing the serial data stream correctly.

5000rpm? That is going to take kilowatts of power just to accelerate the motor up to that speed in a reasonable amount of time. You need extremely powerful motors so that output power is greater than zero.

Steppers are very inefficient at that kind of rpm, so you need even bigger motors. How much weight are you moving?

Time to get started on the smart acceleration stuff.

MorganS:
5000rpm? That is going to take kilowatts of power just to ...

Time to get started on the smart acceleration stuff.

I seriously think the OP needs to go back to his drawing board with a clean sheet of paper and figure out the mechanical system before worrying about coding.

Maybe this sort of machine needs a hydraulic or pneumatic power system in which the microprocessor just turns valves on and off and there is a large motor somewhere in the background providing the energy.

...R

Hi Robin2,

The mechanical system is already fabricated and has been running on commercially available SCN6 linear actuators: iRacing Simtools with 3 x SCN6 - YouTube (This is our simulator)

I am going through the Arduino/stepper process to replace the commercial actuators with a more cost effective/customizable system.

The stated RPM is to match the specifications of the commercial actuator (see spec posted earlier).

Tonight I have reworked the code removing the acceleration code and replacing with MorganS's code snippet from reply #13. I found I needed to add a 300 microsecond delay between the high and low pulses to stop the motor stalling on startup.

Combined with changing the output to 10bit giving 1024 positions, I have been able to get the full 13300 steps for full movement.

Next step is to add some logic that allows some acceleration to give a higher top speed.

Please post a link to the datasheet for the SCN6 actuators - it may help to get a better understanding of what you are doing.

...R