Smoothen stepper motor movement

Hi. I have a small X27.168 stepper motor controlled with the SwitecX25.h library. I use it to control an RPM gauge from Euro Truck Simulator, using this function:

void function_rpm()
{
  serial_byte = Serial.read();
  rpm_pwm = map(serial_byte, 0, 250, 0, MAX_RPM);
  rpm.setPosition(rpm_pwm);
  rpm.update();
}

The problem is that although it is capable of moving at a very fast pace (it can go from 0 to 240 degrees in less than a second), because of the arduino refresh speed, it moves very slowly and kind of tripping. Adding a delay to that function messes with everything else. This is how it looks like:

In case you need it, this is the full code:

#include <LiquidCrystal_I2C.h>
#include <SwitecX25.h>

const int LEFT_INDICATOR  = 32;
const int RIGHT_INDICATOR = 33;
const int PARKING_BREAK   = A11;
const int FUEL_WARNING    = A13;
const int LOW_BEAM        = A12;
const int HIGH_BEAM       = A10;

int serial_byte;
int fuel = 3;
int fuel_pwm;
int temp = 2;
int temp_pwm;
int speed_pwm;
int rpm_pwm;
int new_rpm;
int cruise_control;
int new_cruise_control;
String line1;
String line2;
String new_odometer;
bool enabled;
int MAX_SPEED = 720;
int MAX_RPM = 720;

#define STEPS (315*3)
SwitecX25 speedometer(STEPS,35,37,41,39);
SwitecX25 rpm(STEPS,34,36,40,38);

#define PACKET_SYNC 0xFF
#define PACKET_VER  2

LiquidCrystal_I2C lcd(0x27,20,4);

void setup()
{
  Serial.begin(115200);

  speedometer.zero();
  rpm.zero();
  
  lcd.init();
  lcd.backlight();
  lcd.print("Self Test");
  
  // Initialise LEDs
  pinMode(LEFT_INDICATOR, OUTPUT);
  pinMode(RIGHT_INDICATOR, OUTPUT);
  pinMode(PARKING_BREAK, OUTPUT);
  pinMode(FUEL_WARNING, OUTPUT);
  pinMode(LOW_BEAM, OUTPUT);
  pinMode(HIGH_BEAM, OUTPUT);
  pinMode(fuel, OUTPUT);
  pinMode(temp, OUTPUT);
  
  speedometer.setPosition(0);
  rpm.setPosition(0);
  digitalWrite(LEFT_INDICATOR, 0);
  digitalWrite(RIGHT_INDICATOR, 0);
  digitalWrite(PARKING_BREAK, 0);
  digitalWrite(FUEL_WARNING, 0);
  digitalWrite(LOW_BEAM, 0);
  digitalWrite(HIGH_BEAM, 0);
  analogWrite(fuel, 0);
  analogWrite(temp, 0);
  
  delay(500);
  
  digitalWrite(LEFT_INDICATOR, 1);
  digitalWrite(RIGHT_INDICATOR, 1);
  digitalWrite(PARKING_BREAK, 1);
  digitalWrite(FUEL_WARNING, 1);
  digitalWrite(LOW_BEAM, 1);
  digitalWrite(HIGH_BEAM, 1);
  analogWrite(fuel, 224);
  analogWrite(temp, 132);

  delay(1000);
  
  digitalWrite(LEFT_INDICATOR, 0);
  digitalWrite(RIGHT_INDICATOR, 0);
  digitalWrite(PARKING_BREAK, 0);
  digitalWrite(FUEL_WARNING, 0);
  digitalWrite(LOW_BEAM, 0);
  digitalWrite(HIGH_BEAM, 0);
  analogWrite(fuel, 0);
  analogWrite(temp, 0);
    
  lcd.clear();
  lcd.print("Wait");
  
  // Wait a second to ensure serial data isn't from re-programming 
  delay(1000);
  lcd.clear();
  lcd.print("Ready");
}

void function_speed()
{
  serial_byte = Serial.read();
  speed_pwm = map(serial_byte, 0, 120, 0, MAX_SPEED);
  speedometer.setPosition(speed_pwm);
  speedometer.update();
}

void function_rpm()
{
  serial_byte = Serial.read();
  rpm_pwm = map(serial_byte, 0, 250, 0, MAX_RPM);
  rpm.setPosition(rpm_pwm);
  rpm.update();
}

void skip_serial_byte()
{
  (void)Serial.read();
}

void writeFuelValue(){
  serial_byte = Serial.read();
  fuel_pwm = map(serial_byte, 0, 100, 0, 224);
  analogWrite(fuel, fuel_pwm);
}

void writeTempValue(){
  serial_byte = Serial.read();
  temp_pwm = map(serial_byte, 30, 120, 0, 132);
  if(serial_byte < 30) 
  analogWrite(temp, 0);
  else
  analogWrite(temp, temp_pwm);
}

void digitalWriteFromBit(int port, int value, int shift)
{
  digitalWrite(port, (value >> shift) & 0x01);
}

void loop()
{
  if (Serial.available() < 16)
    return;
  
  serial_byte = Serial.read();
  if (serial_byte != PACKET_SYNC)
    return;
    
  serial_byte = Serial.read();
  if (serial_byte != PACKET_VER)
  {
    lcd.clear();
    lcd.print("PROTOCOL VERSION ERROR");
    return;
  }

  function_speed();
  function_rpm();

  skip_serial_byte(); // Brake air pressure
  skip_serial_byte(); // Brake temperature
  //skip_serial_byte(); // Fuel ratio
  writeFuelValue();
  skip_serial_byte(); // Oil pressure
  skip_serial_byte(); // Oil temperature
  //skip_serial_byte(); // Water temperature
  writeTempValue();
  skip_serial_byte(); // Battery voltage
  new_cruise_control = Serial.read();

  
  // Truck lights byte
  serial_byte = Serial.read();
  digitalWriteFromBit(LEFT_INDICATOR,  serial_byte, 5);  
  digitalWriteFromBit(RIGHT_INDICATOR, serial_byte, 4);
  if(enabled)
  {
    if(serial_byte >> 3 & 0x01) digitalWrite(LOW_BEAM, 1);
    else digitalWrite(LOW_BEAM, 0);
    if((serial_byte >> 3 & 0x01) && (serial_byte >> 2 & 0x01)){
      digitalWrite(HIGH_BEAM, 1);
    }
    else digitalWrite(HIGH_BEAM, 0);
  }
  else
  {
  digitalWrite(LOW_BEAM, 0);
  digitalWrite(HIGH_BEAM, 0);
  }

  // Warning lights bytes

  serial_byte = Serial.read();  
  if(enabled)
  {
    if(serial_byte >> 7 & 0x01) digitalWrite(PARKING_BREAK, 1);
    else digitalWrite(PARKING_BREAK, 0);
    if(serial_byte >> 3 & 0x01) digitalWrite(FUEL_WARNING, 1);
    else digitalWrite(FUEL_WARNING, 0);
  }
  else
  {
    digitalWrite(PARKING_BREAK, 0);
    digitalWrite(FUEL_WARNING, 0);
  }
 
  // Enabled flags
  serial_byte = Serial.read();
  enabled = serial_byte >> 1 & 0x01;
  
  // Text length
  int text_len = Serial.read();
  
  line2 = "ODO: ";
  // Followed by text
  if (0 < text_len && text_len < 127)
  {
    for (int i = 0; i < text_len; ++i)
    {
      while (Serial.available() == 0) // Wait for data if slow
      {
        delay(2);
      }
      serial_byte = Serial.read();
      if (serial_byte < 0 && serial_byte > 127)
        return;
      line2.concat(char(serial_byte));
    }

  }
  line2.concat(" KMS");

  if((new_odometer != line2) || (new_cruise_control != cruise_control)){
    new_odometer = line2;
    cruise_control = new_cruise_control;
    line1 = "CC: ";
    line1 += cruise_control;
    line1 += String(" KPH");
    lcd.clear();
    lcd.setCursor(0,0);
    lcd.print(line1);
    lcd.setCursor(0,1);
    lcd.print(line2);
  }
}

Check that You use that stepper library in the proper way. For example AccelStepper needs to be called at a high rate in order to function.
It looks like Your code only make a call when there's serial data availble.

Yes, and I believe that's actually the problem. The serial data is constantly flowing from the game, so it's constantly providing rpm data. Since the motor requires the function rpm.update() to actually move, and I believe this function is being called every iteration of the loop, I believe that's what's causing that flicker. I need to find a way to maybe call it less often, but without missing data, I guess.

I will also check that AccelStepper that you mention.

Look at the beginning of loop. If there are less than 16 byte serial execution does not advance. That return sends execution back the top of loop and no stepper update takes place at all.
Make a test and do a Serial.print every time update is done. Serial monitor activated...

I tried that (with the lcd, as I can't use the Serial monitor as the serial bus is busy with the game's data), and it don't think the problem is that the update function is not being called. As I said, I think the problem is quite the opposite, it's being called too often.

Is this the clearwater Switec library? THEIR documentation says:

This means for example if you are waiting for serial I/O, you should be calling update() while you are waiting.

which I found here: https://github.com/clearwater/SwitecX25

This leads me to believe that

this should be

while (Serial.available() < 16){
speedometer.update();
rpm.update();
}

At least this is what I would try.
Good luck and super jealous of your build once you get it up-and-running! (I love American Truck Simulator, haven't tried Euro yet)

I think using return; in a void function like loop() is a bad habit. Only slightly better than using goto <label>;. It leads to bad code structure. Code that appears later in the function does not get a chance to execute if some earlier code decides to use return; in a "selfish" way. That makes doing "two or more things at once" impossible. Better to re-structure the code carefully, removing the need to use return;.

Sounds like a strange decision. I don't see anything in your code that seeks and senses the zero position on the gauge. How do you ensure the gauge reads zero to begin?

A servo motor might have been a better choice, and would not have resulted in the problem you are now having.

this should be

while (Serial.available() < 16){
speedometer.update();
rpm.update();
}

Wow, that works much better now. It is still a little bit flickery yet (I'll try to record and upload a video later), but at least it is now giving live values.

@PaulRB, I'm using these stepper motors because they are basically what the actual vehicle gauges use in real life. They do have a function (speedometer.zero()) that take them to the 0 position.
I used servos before this, and it didn't work half as well, and were way more noisy.

1 Like

This is how it looks now:
Gauges working - YouTube
(Note that the other lights don't work because I don't have them plugged, I was just testing the gauges)

Ah, I get it now. These stepper motors have an internal mechanical stop. The .zero() function moves the motor to that position simply by moving the maximum number of steps, in case the needle is at the other extreme position. It's designed so that if this is more steps than physically needed, the mechanism is not damaged. So there is no need for a sensor to detect when the zero position has been reached.

Oh yeah, bud! I like it!

For a stepper motor this is only a matter of the robustness of the stop as the stepper itself cannot be damaged by being blocked.

Commonly used in printers, the principal reason for using a "home" sensor is to speed up the process, not for accuracy or avoiding damage.

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.