1. Do not share user accounts! Any account that is shared by another person will be blocked and closed. This means: we will close not only the account that is shared, but also the main account of the user who uses another person's account. We have the ability to detect account sharing, so please do not try to cheat the system. This action will take place on 04/18/2023. Read all forum rules.
    Dismiss Notice
  2. For downloading SimTools plugins you need a Download Package. Get it with virtual coins that you receive for forum activity or Buy Download Package - We have a zero Spam tolerance so read our forum rules first.

    Buy Now a Download Plan!
  3. Do not try to cheat our system and do not post an unnecessary amount of useless posts only to earn credits here. We have a zero spam tolerance policy and this will cause a ban of your user account. Otherwise we wish you a pleasant stay here! Read the forum rules
  4. We have a few rules which you need to read and accept before posting anything here! Following these rules will keep the forum clean and your stay pleasant. Do not follow these rules can lead to permanent exclusion from this website: Read the forum rules.
    Are you a company? Read our company rules

Showroom My DIY Budget 3DOF Build

Discussion in 'DIY Motion Simulator Projects' started by Tintin Boulsard, May 19, 2025.

  1. Tintin Boulsard

    Tintin Boulsard Member

    Joined:
    Dec 23, 2022
    Messages:
    42
    Location:
    France
    Balance:
    155Coins
    Ratings:
    +23 / 0 / -0
    My Motion Simulator:
    3DOF, Arduino
    Hello everyone :) !
    I wanted to share how I built my own DIY racing simulator.
    My goal was to have 4 actuators so it is for sure not the cheapest build but it is much cheaper than the purchasable actuators or well-known DIY ones such as SFX100. As you will see below, I made three different configurations but the minimal config is enough to enjoy your motion simulator. The two other ones are made for those who like to have a really clean build with a dedicated "control center", cables connectors, electrical panel,... In conclusion, you can build your own 3DOF motion platform for only 661€ !

    1. OVERALL PICTURES
    IMG_3787.JPG IMG_3766.JPG PXL_20250510_111332811.jpg PXL_20250510_111317029.jpg PXL_20250510_111258847.jpg

    2. EXPLANATIONS

    How did i manage to get it so cheap ? :p
    1. I used the classic components list (ex : SFX100, SRT100/80) but I modified it to use cheaper ones (8080 alu extrusions instead of 100,..).
    2. I spent time online finding the cheapest product or alternatives.
    3. I managed to found them on some websites at really low prices. The "HLTNC France Store" on Aliexpress is almost unfindable but offers prices twice as low as the HLTNC Official (China) Store. I bought a pack of steppers, PSUs and controllers at 360€ instead of 600€ on the others stores. Moreover, they freely ship from Europe so you can probably buy them even though you are not living in France.
    For all the aluminium extrusions, screws,... I used Motedis which serves in multiple countries all around the world. The prices are also really low.
    4. I used STEPPERS ! Lot of people hate them and prefer servos. However, I managed to get them working almost as good and smooth as servos. It is all about the Arduino CODE ! Go to the code section so you can understand what I'm talking about.
    5. 3D Printing : I 3D printed as much as I could, even the ballscrews nuts.
    6. AliExpress offers really big coupons, just wait a few days or weeks so you can get a 80-100€ discount when buying ~500€.

    List of Components :
    Capture d'écran 2025-05-18 222456.png
    I did not include the basic tools you have at home or some consumables such as Lithium Grease or PLA Plastic (~25€) as most already have them (especially if you have a 3D printer).

    Nice features I added :cool: :
    - Everything is centralized into my "control center" and one button initializes the whole rig. This makes the experience more fluid and less painful, while having a clean build (no cables going everywhere). Also, it features fans, an electrical panel and an emergency button that cuts everything off.
    IMG-20250206-WA0002.jpeg IMG-20250212-WA0000.jpeg PXL_20250510_111258847.jpg

    - Automatic calibration : at each startup, the actuators calibrate themselves using magnetic reed switches (magnets are placed on the slider, and the switch on the aluminium extrusion).
    IMG-20250302-WA0000.jpeg

    - Connectors : I placed cable connectors between the actuator and the rig and between the rig and the control center. If I need to make some adjustments or displace my simulator, it gets very easy.
    IMG-20250301-WA0005.jpeg

    - Motors Rubber Joints : helps reduce the steppers vibrations.
    - All-in-one custom nut and slider : I designed and 3D printed my own nut with also serves as the slider. I can now easily get my tube fixed onto it (and easily remove it in case smthg breaks) and place the calibration magnets.

    How did I mange to use these horrible steppers ?! :eek:
    As I said earlier, it is all about your Arduino code. Stepper motors have a big default : they are noisy, loud and vibrate a lot (more than a washing machine o_O). I had to find a way to reduce these vibrations, without losing speed (you can run our steppers at a higher microstep setting to reduce noise and vibrations but you will lose speed. I'm using Lebois SRT80 code as a base that I heavily modified in order to make it work with steppers. I introduced the following features :
    - User-friendly configuration (wiring, variables, instructions, clearly added at the beginning of the code) :D
    - Dynamic delay : delay between each pulse automatically calculated according to the distance to be covered instead of static, which helps reduce the steppers vibrations. ;)
    - Small Movements Reducer : we filter all of the small movements inferior to a specific value in order to reduce vibrations.
    - Automatic Calibration : steppers sometimes lose their steps or shift of centimeters with time. With the automatic calibration, you don't have this problem anymore. Also, I added an anti-electromagnetic interferences function : my reed switches cables being close to the high current motors ones, I had some interferences that was making the calibration fail every time. :rolleyes:

    Here is my code :
    Code:
    // Original by Lebois Racing, heavily modified by Tintin Boulsard (https://www.xsimulator.net/community/members/tintin-boulsard.46040/).
    // Simtools Output Settings : Bits/s : 9600, Data bits : 8 bits, Parity : None, Stop bits : 1
    //                            Bit Range : 16, Binary, P<Axis1a><Axis2a><Axis3a><Axis4a>, 50 ms
    // Steppers controller configuration : 3200 microsteps.
    
    // WIRING CANNOT BE CHANGED !
    #define StepPin1  8
    #define StepPin2  9
    #define StepPin3  10
    #define StepPin4  11
    
    #define DirPin1   4
    #define DirPin2   5
    #define DirPin3   6
    #define DirPin4   7
    
    
    // Dynamic delay : delay between each pulse automatically calculated (Can be considered as the actuator speed)
    #define minDelay 20 // Minimal delay
    #define maxDelay 130 // Maximal dealy
    
    #define averageDelay 30 // Delay for non-dynamic actions such as direction changes
    
    // As we are using steppers, we filter all of the small movements inferior to this value in order to reduce vibrations.
    #define vibrationReducer 3000
    
    // Limit switches ports for automatic calibration :
    #define LimitSwitch1Pin A1
    #define LimitSwitch2Pin A2
    #define LimitSwitch3Pin A3
    #define LimitSwitch4Pin A4
    
    #define automaticCalibration 1 // Atomatic Calibration ON/OFF --> 1/0
    #define calibrationSpeed 35 // Automatic calibration speed (delay between each pulse)
    #define limitTime 100 // As we sometimes have electromagnetic interference, we make sure the limit switch has been pressed longer than this value (ms)
    //---------------------------------------------------------------------------------------------------------------
    
    byte buffer           = 0 ;    // It takes the value of the serial data
    byte buffercount      = 0 ;    // To count where we are in the serial datas
    byte commandbuffer[8] = {0};   // To stock the serial datas in the good order.
    
    unsigned m1Target = 32768,   m2Target = 32768,   m3Target = 32768,   m4Target = 32768; // Target of each motor
    unsigned m1Position = 32768, m2Position = 32768, m3Position = 32768, m4Position = 32768; // Actual position of each motor
    byte pulseM1 = B00000000, pulseM2 = B00000000, pulseM3 = B00000000, pulseM4 = B00000000;
    int  dir1 = -1, dir2 = -1, dir3 = -1, dir4 = -1; // Direction of each motor
    byte dirChange = 0;
    bool noData = true;
    bool servoEnabled = true;
    unsigned adjustedDelayM1, adjustedDelayM2, adjustedDelayM3, adjustedDelayM4; // Calculated dynamic delay
    unsigned distanceM1, distanceM2, distanceM3, distanceM4; // Remaining distance in order to filter small movements and calculate delay
    
    
    void setup() {
      Serial.begin(9600); // To communicate with Simtools
    
      pinMode(StepPin1, OUTPUT);
      pinMode(DirPin1, OUTPUT);
      digitalWrite(DirPin1, HIGH);
    
      pinMode(StepPin2, OUTPUT);
      pinMode(DirPin2, OUTPUT);
      digitalWrite(DirPin2, HIGH);
    
      pinMode(StepPin3, OUTPUT);
      pinMode(DirPin3, OUTPUT);
      digitalWrite(DirPin3, HIGH);
    
      pinMode(StepPin4, OUTPUT);
      pinMode(DirPin4, OUTPUT);
      digitalWrite(DirPin4, HIGH);
    
      pinMode(LimitSwitch1Pin, INPUT_PULLUP);
      pinMode(LimitSwitch2Pin, INPUT_PULLUP);
      pinMode(LimitSwitch3Pin, INPUT_PULLUP);
      pinMode(LimitSwitch4Pin, INPUT_PULLUP);
    
      delayMicroseconds(2000);
    
      if (automaticCalibration) {
      calibrateMotors();
        }
    
    }
    
    void loop() {
      SerialReader(); // Get the datas from Simtools
      moveMotor(); // Move the motors
    }
    
    void SerialReader() {
      while (Serial.available())
      {
        if (buffercount == 0)
        {
          buffer = Serial.read();
    
                if (buffer != 'P') {
                      
            buffercount = 0; // "P" is the marquer. If we read P, the next data is Motor1
          } else {
            buffercount = 1;
          }
        }
        else   //  if(buffercount>=1)
        {
          buffer = Serial.read();
          commandbuffer[buffercount-1] = buffer; // The first value next to "P" is saved in commandbuffer in the place "buffercount"
          buffercount++;
          if (buffercount >= 9) {
              m1Target = commandbuffer[0] * 256 + commandbuffer[1];      // As the communication is 8 bits and the data are 16 bits, there is two bytes to combine
              m2Target = commandbuffer[2] * 256 + commandbuffer[3];
              m3Target = commandbuffer[4] * 256 + commandbuffer[5];
              m4Target = commandbuffer[6] * 256 + commandbuffer[7];
              buffercount = 0;
            break;
          }
        }
      }
    }
    
    
    
    void moveMotor() {
      directionManager(); // Set the directions of each motor
      calculateDelay(); // Calculate delay between pulses
      singleStep(); // Make the motor move one step
    
    }
    
    void directionManager() {
    
      if ((m1Target > m1Position) && (dir1 == -1)) {
        PORTD &= B11101111;
        dir1 = 1;
        dirChange = 1;
    
      }
    
      if ((m1Target < m1Position ) && (dir1 == 1)) {
        PORTD |= B00010000;
        dir1 = -1;
        dirChange = 1;
      }
    
      if ((m2Target > m2Position) && (dir2 == -1)) {
        PORTD &= B11011111;
        dir2 = 1;
        dirChange = 1;
      }
     
      if ((m2Target < m2Position ) && (dir2 == 1)) {
        PORTD |= B00100000;
        dir2 = -1;
        dirChange = 1;
      }
    
      if ((m3Target > m3Position) && (dir3 == -1)) {
        PORTD &= B10111111;
        dir3 = 1;
        dirChange = 1;
      }
     
      if ((m3Target < m3Position ) && (dir3 == 1)) {
        PORTD |= B01000000;
        dir3 = -1;
        dirChange = 1;
      }
    
      if ((m4Target > m4Position) && (dir4 == -1)) {
        PORTD &= B01111111;
        dir4 = 1;
        dirChange = 1;
      }
     
      if ((m4Target < m4Position ) && (dir4 == 1)) {
        PORTD |= B10000000;
        dir4 = -1;
        dirChange = 1;
      }
     
      if (dirChange == 1) {
        delayMicroseconds(averageDelay);
        dirChange = 0;
      }
    }
    
    void calculateDelay() {
    // Calculate a dynamic delay according to the remaining distance
    distanceM1 = abs(m1Target - m1Position);
    adjustedDelayM1 = map(distanceM1, 0, 65536, maxDelay, minDelay);
    adjustedDelayM1 = constrain(adjustedDelayM1, maxDelay, minDelay);
    
    distanceM2 = abs(m2Target - m2Position);
    adjustedDelayM2 = map(distanceM2, 0, 65536, maxDelay, minDelay);
    adjustedDelayM2 = constrain(adjustedDelayM2, maxDelay, minDelay);
    
    distanceM3 = abs(m3Target - m3Position);
    adjustedDelayM3 = map(distanceM3, 0, 65536, maxDelay, minDelay);
    adjustedDelayM3 = constrain(adjustedDelayM3, maxDelay, minDelay);
    
    distanceM4 = abs(m4Target - m4Position);
    adjustedDelayM4 = map(distanceM4, 0, 65536, maxDelay, minDelay);
    adjustedDelayM4 = constrain(adjustedDelayM4, maxDelay, minDelay);
    
    }
    
    void singleStep() {
      if ((m1Target != m1Position)  && (distanceM1 > vibrationReducer)){
        pulseM1 = B00000001;
        m1Position += dir1;
      }
      else{pulseM1 = B00000000;
      delayMicroseconds(adjustedDelayM1);
      }
     
      if ((m2Target != m2Position)  && (distanceM2 > vibrationReducer)) {
        pulseM2 = B00000010;
        m2Position += dir2;
      }
      else{pulseM2 = B00000000;
      delayMicroseconds(adjustedDelayM2);
      }
     
      if ((m3Target != m3Position)  && (distanceM3 > vibrationReducer)) {
        pulseM3 = B00000100;
        m3Position += dir3;
      }
      else{pulseM3 = B00000000;
      delayMicroseconds(adjustedDelayM3);
      }
    
      if ((m4Target != m4Position)  && (distanceM4 > vibrationReducer)) {
        pulseM4 = B00001000;
        m4Position += dir4;
      }
      else{pulseM4 = B00000000;
      delayMicroseconds(adjustedDelayM4);
      }
     
      if((pulseM1 == B00000000) && (pulseM2 == B00000000) && (pulseM3 == B00000000) && (pulseM4 == B00000000)){return;}
          PORTB |= pulseM1 | pulseM2 | pulseM3 | pulseM4;
          delayMicroseconds(averageDelay);
          PORTB &= B11110000;
    }
    
    
    void generateStepPulse(int stepPin, unsigned int pulseWidth) {
      digitalWrite(stepPin, HIGH);
      delayMicroseconds(pulseWidth);
      digitalWrite(stepPin, LOW);
      delayMicroseconds(pulseWidth);
    }
    
    void calibrateMotors() {
      Serial.println("Calibration started...");
    
      m1Position = 0;
      m2Position = 0;
      m3Position = 0;
      m4Position = 0;
      servoEnabled = false;
      bool m1Calibrated = false, m2Calibrated = false, m3Calibrated = false, m4Calibrated = false;
      unsigned long timePression[4] = {0, 0, 0, 0};  // Stores how long the limit switch has been pressed
    
      Serial.println("Checking limit switch states...");
      Serial.print("Limit 1: "); Serial.println(digitalRead(LimitSwitch1Pin));
      Serial.print("Limit 2: "); Serial.println(digitalRead(LimitSwitch2Pin));
      Serial.print("Limit 3: "); Serial.println(digitalRead(LimitSwitch3Pin));
      Serial.print("Limit 4: "); Serial.println(digitalRead(LimitSwitch4Pin));
    
      delay(1000);
    
      Serial.println("Moving actuators up to limit switches...");
    
      digitalWrite(DirPin1, HIGH);
      digitalWrite(DirPin2, HIGH);
      digitalWrite(DirPin3, HIGH);
      digitalWrite(DirPin4, HIGH);
    
      while (!m1Calibrated || !m2Calibrated || !m3Calibrated || !m4Calibrated) {
        unsigned long actualTime = millis();
    
        if (!m1Calibrated) {
          if (digitalRead(LimitSwitch1Pin) == LOW) {
            if (timePression[0] == 0) timePression[0] = actualTime;
            if (actualTime - timePression[0] >= limitTime) {
              m1Calibrated = true;
              Serial.println("Vérin 1 atteint la butée.");
            }
          } else {
            timePression[0] = 0;
            generateStepPulse(StepPin1, calibrationSpeed);
          }
        }
    
        if (!m2Calibrated) {
          if (digitalRead(LimitSwitch2Pin) == LOW) {
            if (timePression[1] == 0) timePression[1] = actualTime;
            if (actualTime - timePression[1] >= limitTime) {
              m2Calibrated = true;
              Serial.println("Vérin 2 atteint la butée.");
            }
          } else {
            timePression[1] = 0;
            generateStepPulse(StepPin2, calibrationSpeed);
          }
        }
    
        if (!m3Calibrated) {
          if (digitalRead(LimitSwitch3Pin) == LOW) {
            if (timePression[2] == 0) timePression[2] = actualTime;
            if (actualTime - timePression[2] >= limitTime) {
              m3Calibrated = true;
              Serial.println("Vérin 3 atteint la butée.");
            }
          } else {
            timePression[2] = 0;
            generateStepPulse(StepPin3, calibrationSpeed);
          }
        }
    
        if (!m4Calibrated) {
          if (digitalRead(LimitSwitch4Pin) == LOW) {
            if (timePression[3] == 0) timePression[3] = actualTime;
            if (actualTime - timePression[3] >= limitTime) {
              m4Calibrated = true;
              Serial.println("Vérin 4 atteint la butée.");
            }
          } else {
            timePression[3] = 0;
            generateStepPulse(StepPin4, calibrationSpeed);
          }
        }
      }
    
      Serial.println("All actuators reached limit switches. Stopping...");
      delay(500);
    
      unsigned centerPosition = 32768;
      bool m1Centered = false, m2Centered = false, m3Centered = false, m4Centered = false;
    
      Serial.println("Moving actuators down to center...");
    
      digitalWrite(DirPin1, LOW);
      digitalWrite(DirPin2, LOW);
      digitalWrite(DirPin3, LOW);
      digitalWrite(DirPin4, LOW);
    
      while (!m1Centered || !m2Centered || !m3Centered || !m4Centered) {
        if (!m1Centered && m1Position < centerPosition) {
          generateStepPulse(StepPin1, calibrationSpeed);
          m1Position++;
        } else {
          m1Centered = true;
        }
    
        if (!m2Centered && m2Position < centerPosition) {
          generateStepPulse(StepPin2, calibrationSpeed);
          m2Position++;
        } else {
          m2Centered = true;
        }
    
        if (!m3Centered && m3Position < centerPosition) {
          generateStepPulse(StepPin3, calibrationSpeed);
          m3Position++;
        } else {
          m3Centered = true;
        }
    
        if (!m4Centered && m4Position < centerPosition) {
          generateStepPulse(StepPin4, calibrationSpeed);
          m4Position++;
        } else {
          m4Centered = true;
        }
      }
    
      servoEnabled = true;
      Serial.println("Calibration and centering complete.");
    }
    
    3D printed parts :
    I used Lebois designs (SRT80) but modified some of them, such as the two joints between the extrusion and the motor that didn't fit. I used and kept intact Lebois' lower part of the actuator. I designed my own slider, reed switch mount, actuators to rig brackets. I will upload the parts I modified and the ones I designed myself.

    3. CONCLUSION :
    That's it! Thank you very much for stopping by and I hope you have found it interesting. :grin:thumbs

    • Like Like x 2
    • Winner Winner x 2
    Last edited: May 20, 2025
  2. Misanthrop

    Misanthrop Member

    Joined:
    Feb 5, 2024
    Messages:
    75
    Balance:
    448Coins
    Ratings:
    +39 / 1 / -0
    My Motion Simulator:
    6DOF
    Can you make an Video of your Rig? The Motors are capable of only 2000 rpm, compared to 3000 of the often used ST80 ones. But the Torque, even if the torque curve looks very similar, will fall to 2Nm at max rpm rate at 2k rpm. Servos have twice the torque at 2k rpm, which you will notice very fast when the Rig has fast movements with humpy tracks like Road Atlanta or similar ones, mostly American ones...even at 100V the Servos are faster and have more torque.

    Because of the load i´m also wondering how they act at slow movements like Slope differences and how hot the get after a longer session with many details.
  3. Tintin Boulsard

    Tintin Boulsard Member

    Joined:
    Dec 23, 2022
    Messages:
    42
    Location:
    France
    Balance:
    155Coins
    Ratings:
    +23 / 0 / -0
    My Motion Simulator:
    3DOF, Arduino


    You are right, steppers are for sure not as good as servos but servos have 2,4Nm so when the stepper torque drops it is not so far. On the video, they are not close from being at full speed and there is no difference when adding load. Also, I haven't tested for really long sessions, but after 1h30, they are not really hot, far from not being touchable.
    Lastly, the cracking sounds come from the aluminium chassis which is not closed at the front as it is not designed for a motion system.
    I will soon upload a video at their max capacity.
    • Like Like x 2
    Last edited: May 20, 2025