ArduinoJoystickWithFFBLibrary icon indicating copy to clipboard operation
ArduinoJoystickWithFFBLibrary copied to clipboard

How to connect output steppers - Its not an issue with library, more like need help in using it

Open gaurav2827 opened this issue 3 years ago • 8 comments

Thanks a lot for this wonderful library. Detailed work indeed!

I have gone through the "SingleJoystickFFB" in examples but unable to understand where or how to connect steppers in the arduino board. The sketch says pin 7, 6, 9 are outputs. Pin 9 gives PWM output (to motor driver) understood but unable to catch the idea of pin 6 & 7. I have used the previous Joystick library for my home cockpit building but I am new to controlling steppers. Please help!

Regards, Gaurav

gaurav2827 avatar Apr 16 '21 04:04 gaurav2827

Hi guys, anyone who can help me on this?

gaurav2827 avatar Apr 23 '21 18:04 gaurav2827

Hi, pin7,6 are used to determine the direction of motor rotation.

YukMingLaw avatar Jun 24 '21 01:06 YukMingLaw

Hi, pin7,6 are used to determine the direction of motor rotation.

YukMingLaw avatar Jun 24 '21 01:06 YukMingLaw

Stepper motors require a special type of driver, and they commonly use two control pins, "direction" and "step". You need to precisely control the step signal to make the motor move.

You can either use a pwm peripheral and vary the frequency, or you can use a timer interrupt to toggle it with code. You likely won't be able to do what you're hoping in the main loop as this joystick library without using pwm because the library takes several milliseconds per cycle to do its calculations and USB accesses.

sgtnoodle avatar Jun 30 '21 04:06 sgtnoodle

Hi, pin7,6 are used to determine the direction of motor rotation.

Thanks for this. I am trying to use the example code but unable to get any output from arduino. My motor simply wont run or respond to anything. Its detected on fsforce.exe but nothing after that. No input is registered in the application or the windows game controller window.

If you have a working code, could you plz share in the example folder.

Regards

gaurav2827 avatar Aug 05 '21 18:08 gaurav2827

I wouldn't expect any sort of example to run out of the box without some amount of hardware specific tweaking. Here's my main sketch for a wheel with a DC motor driven by an H-bridge, and a SPI quadrature decoder IC. It uses a modified joystick library that I forked so I could fix some bugs and add default spring centering. You likely won't be able to even compile it, but you can see it as a reference of what all I had to do to get my wheel working.

#include <Joystick.h>
#include <LS7366.h>
#include <SPI.h>

#define ICR 600

#define DIR_PIN 8
#define PWM_PIN 9
#define BRAKE_PIN 10
#define LED_PIN 13

//#define COUNT_HALF_RANGE 61440  // 1.5 turn
#define COUNT_HALF_RANGE 51200  // 1.25 turn for 900 deg lock to lock

#define AXIS_HALF_RANGE 32767

#define ENABLE_JOYSTICK 1

#define ENABLE_SWEEP 0


// These were measured at about 12.3V
//#define MAX_RATE 35987.5f  // 1 turn
//#define MAX_RATE 23991.666666667f  // 1.5 turns
//#define MAX_RATE 28790.0f  // 1.25 turns

// This is at 14V.
// This number just worked out really nicely...
#define MAX_RATE 32767.0f  // 1.25 turns

#define HARDSTOP_DC (ICR * 0.8f)
#define HARDSTOP_FADE 1500.0f

#define FEEDBACK_DC (ICR * 0.25f)

// When applying a braking torque to the motor, there's a linear electrical power gain and a quadratic resistive power loss.
// When there's more electrical power than resistive loss, power gets dumped back into the supply, aka regen braking.
// If we knew the motor's and brake resistor's parameters, we could just calculate that power and compute the right
// average voltage to apply across the braking resistor. That's a lot of work, though, so instead we can just fudge it
// and measure voltage spikes with a multimeter. Too low, and the supply voltage will spike. Too high, and the resistor will
// get unnecessarily hot.
#define BRAKE_MULTIPLIER 0.5f

LS7366 myLS7366(7 /* chip select */);



class LowPassFilter
{
  public:
    LowPassFilter(float tc_sec)
      : _tc_sec(tc_sec),
        _tc_sec_reciprocal(1.0f/tc_sec)
    {}

    void reset(float output)
    {
      _output = output;
      _rate = 0.0f;
    }
    
    float output() const { return _output; }
    float rate() const { return _rate; }
    void update(float value, float dt_sec)
    {
      if (dt_sec >= _tc_sec)
      {
        _rate = (value - _output) / dt_sec;
      }
      else
      {
        _rate = (value - _output) * _tc_sec_reciprocal;
      }
      _output += _rate * dt_sec;
    }
    
  private:
    const float _tc_sec;
    const float _tc_sec_reciprocal;
    float _output = 0.0f;
    float _rate = 0.0f;
};


LowPassFilter pos_filter(0.01f);
// Low pass filter the position filter's rate to get acceleration.
LowPassFilter vel_filter(0.01f);

#if ENABLE_JOYSTICK
//X-axis & Y-axis REQUIRED
Joystick_ Joystick(JOYSTICK_DEFAULT_REPORT_ID, 
  JOYSTICK_TYPE_JOYSTICK, 0, 0,
  true, false, false, //X,Y,Z
  false, false, false,//Rx,Ry,Rz
  false, false, false, false, false);
#endif

Gains mygains[2];
EffectParams myeffectparams[2];
int32_t forces[2] = {0};

void setup(){

#if ENABLE_JOYSTICK
    Joystick.setXAxisRange(-AXIS_HALF_RANGE, AXIS_HALF_RANGE);    
    //set X Axis gains
    mygains[0].totalGain = 100;//0-100
    mygains[0].springGain = 100;//0-100
    mygains[0].damperGain = 100;
    mygains[0].inertiaGain = 100;
    mygains[0].frictionGain = 100;    
    //enable gains REQUIRED
    Joystick.setGains(mygains);
    Joystick.EnableAutoCenter(7500, 2000);

    myeffectparams[0].springMaxPosition = AXIS_HALF_RANGE;
    myeffectparams[0].damperMaxVelocity = MAX_RATE;
    myeffectparams[0].inertiaMaxAcceleration = 32767;
    myeffectparams[0].frictionMaxPositionChange = MAX_RATE;
    
    Joystick.begin(false /* Auto send state */);
#endif

    myLS7366.write_mode_register_0(FILTER_1 | DISABLE_INDX | FREE_RUN | QUADRX4);
    myLS7366.write_mode_register_1(NO_FLAGS | EN_CNTR | BYTE_4 );
    myLS7366.clear_counter();
    myLS7366.clear_status_register();
    myLS7366.write_data_register(4);    

    pos_filter.reset(0);

    setupPWM16(ICR);
    analogWrite16(PWM_PIN, 0);
    pinMode(DIR_PIN, OUTPUT);
    analogWrite16(BRAKE_PIN, 0);
    digitalWrite(LED_PIN, LOW);
    pinMode(LED_PIN, OUTPUT);
}


static float bound(float val, float minv, float maxv)
{
  return min(maxv, max(minv, val));
}

void loop()
{
//  DynamicHID().pidReportHandler.PrintEffect(0);
//  DynamicHID().pidReportHandler.PrintEffect(1);
  
  static unsigned long last_loop_run = micros();

#if ENABLE_SWEEP
  static unsigned long next_speed_change = micros();
  static int16_t current_dc = 0;
  static int16_t current_dc_step = 10;
#endif

  const unsigned long now = micros();
  const float dt_sec = static_cast<long>(now - last_loop_run) * 1e-6f;
  if (dt_sec >= 0.001f)
  {
    last_loop_run = now;
    const long count = myLS7366.read_counter();
    const float pos = count * (-static_cast<float>(AXIS_HALF_RANGE) / COUNT_HALF_RANGE);
    pos_filter.update(pos, dt_sec);
    vel_filter.update(pos_filter.rate(), dt_sec);

//    Serial.println(dt_sec*1e3);

    int16_t axis_pos;
    float hardstop_dc = 0.0f;
    if (pos_filter.output() > AXIS_HALF_RANGE)
    {
      axis_pos = AXIS_HALF_RANGE;
      hardstop_dc = max(-HARDSTOP_DC, (AXIS_HALF_RANGE - pos_filter.output()) * (HARDSTOP_DC / HARDSTOP_FADE));
    }
    else if (pos_filter.output() < -AXIS_HALF_RANGE)
    {
      axis_pos = -AXIS_HALF_RANGE;
      hardstop_dc = min(HARDSTOP_DC, (-AXIS_HALF_RANGE - pos_filter.output()) * (HARDSTOP_DC / HARDSTOP_FADE));
    }
    else
    {
      axis_pos = round(pos_filter.output());
    }

    float feedback_dc = 0.0f;
#if ENABLE_JOYSTICK    
    myeffectparams[0].springPosition = axis_pos;
    myeffectparams[0].damperVelocity = bound(pos_filter.rate(), -MAX_RATE, MAX_RATE);
    myeffectparams[0].inertiaAcceleration = bound(vel_filter.rate() * 0.1f, -32767, 32767);
    myeffectparams[0].frictionPositionChange = myeffectparams[0].damperVelocity;
    Joystick.setEffectParams(myeffectparams);
    Joystick.getForce(forces);
    Joystick.setXAxis(axis_pos);
    Joystick.sendState();
    digitalWrite(LED_PIN, forces[0] >= 255 || forces[0] <= -255);
    feedback_dc = forces[0] * (FEEDBACK_DC / 255);
#endif

#if ENABLE_SWEEP
    const long micros_until_sweep = next_speed_change - now;
    if (micros_until_sweep <= 0)
    {
      Serial.print(current_dc);
      Serial.print(", ");
      Serial.println(pos_filter.rate());
      current_dc += current_dc_step;
      if (current_dc >= ICR || current_dc <= -ICR)
      {
        current_dc_step = -current_dc_step;
      }
      digitalWrite(DIR_PIN, current_dc < 0);
      analogWrite16(PWM_PIN, current_dc >= 0 ? current_dc : -current_dc);
      next_speed_change += 1000000;
    }
#else
    const float feed_forward_duty_cycle = pos_filter.rate() * (ICR / MAX_RATE);
    const float duty_cycle = bound(feed_forward_duty_cycle + hardstop_dc + feedback_dc, -ICR, ICR);
    float brake_duty_cycle = 0.0f;
    if ((duty_cycle > 0 && duty_cycle < feed_forward_duty_cycle) ||
        (duty_cycle < 0 && duty_cycle > feed_forward_duty_cycle))
    {
       // We're regen braking. We need to turn on the brake resistor.
       brake_duty_cycle = bound(BRAKE_MULTIPLIER * sqrtf(feed_forward_duty_cycle * duty_cycle - duty_cycle * duty_cycle), 0, ICR);
    }    
    digitalWrite(DIR_PIN, duty_cycle < 0);
    analogWrite16(PWM_PIN, duty_cycle >= 0 ? duty_cycle : -duty_cycle);
    analogWrite16(BRAKE_PIN, brake_duty_cycle);
#endif

    static float max_accel = 0.0f;
    if (fabsf(vel_filter.rate()) > max_accel)
    {
      max_accel = fabsf(vel_filter.rate());
    }
//    Serial.println(max_accel);

#if 0
    Serial.print("now: ");
    Serial.println(now);
    Serial.print("dt_ms: ");
    Serial.println(dt_sec * 1e3f);
    Serial.print("count: ");
    Serial.println(count);
    Serial.print("filtered pos: ");
    Serial.println(pos_filter.output());
    Serial.print("filtered rate: ");
    Serial.println(pos_filter.rate());
    Serial.print("axis_pos: ");
    Serial.println(axis_pos);
    Serial.print("force: ");
    Serial.println(forces[0]);
#endif
  }
}

sgtnoodle avatar Aug 05 '21 19:08 sgtnoodle

Wow this is intense! Guys i just want a simple sketch to make my flight sim yoke auto centre. Nothing more.

I have x and y axis for roll and pitch of flight stick, both connected to two dc motors with full h bridge. I can give pwm signal to motors and it moves within those commands if programmed without this library, i am just unable to use this library to auto centre my yoke using the signals from pots connected to the range of yoke. My yoke has physical end switches so no need to add a program for hard end stops...can someone plz help what i am missing from the example code. I used the same code to pass the DIRECTION and PWM to h bridge, but the motor simply doesn't move

gaurav2827 avatar Aug 11 '21 04:08 gaurav2827

I don't think you need this force feedback library to implement motor based centering. This library is specifically for implementing haptic feedback from games. If literally all you want is hardcoded centering forces, you can just use the standard Arduino joystick library. You still need to do the motor control, and that will require writing some non-trivial code.

For one thing, you're going to need to model the "spring". The equation for spring force is F= k*d, where k is the spring constant and d is the distance the spring is stretched or compressed. You can measure d via your potentiometer, and then just try different k values until it feels good.

That's going to give you an undamped spring. If you turn the yoke and let go, it's going to oscillate back and forth. You can damp it. F = k*v, where v is the velocity. You can compute that by taking the numerical derivative of position, then once again tweak the constant until it feels good.

Note that you've just made what's called a PD controller, or a PID controller without the I.

Doing all that will give you the appropriate force to apply on the motor. Force, or torque in the case of a motor, is proportional to motor current. PWMing an H-bridge doesn't control current, though, it controls applied voltage. A spinning motor generates a voltage proportional to its velocity, called back-EMF. The difference between the applied voltage and the back-EMF causes current to flow through the motor, based on the motor's resistance. In order to control force, you therefore need to know the velocity of the motor (in order to know its back-EMF) and then feed that back into the commanded H-bridge PWM.

If you don't do that, the motor will fight any sort of motion. Just try powering up the H-bridge and setting the PWM to 0. The yoke should be very hard to move. That's because the motion generates a voltage, and that voltage subsequently generates a torque in the opposite direction, because the H-bridge is applying 0 volts (equivalent to shorting the motor wires to themselves). When the H-bridge is unpowered, the motor moves freely because the FETs are all high impedance and so the voltage just floats around to match the back EMF.

If you get all that working, then the yoke will probably feel pretty good. If you're powering the motors with a power supply, though, you'll notice that the centering force fades out whenever you move the yoke rapidly away from centered. That's because when applying a motor torque that opposes motion, kinetic energy is converted back to electric energy. That's called regenerative braking. Power supplies are only designed to source current, not sink current. When you dump current back into a power supply, its output capacitors charge up and the supply voltage shoots up. When the supply voltage shoots up, the applied voltage of the H-bridge also shoots up. Regardless of what your microcontroller tries to do (within reason), any attempt to "brake" the motor while it's moving will result in the supply voltage shooting up rather than the motor slowing down. The typical solution for this is to add what's called a brake resistor. The idea is that the microcontroller connects a high power resistor to the supply whenever the motor is regen braking. Rather than the current charging up the output capacitors, it goes into the resistor instead and turns into heat. Another solution would be to use a rechargeable battery rather than a power supply, or use a fancy power supply designed to sink current. For my wheel project, I used a 100W 2 ohm resistor and a PWMed FET.

If all you really want is centering spring forces, could you ditch the motors and use actual springs instead? That would probably be a lot simpler.

On Tue, Aug 10, 2021, 9:49 PM gaurav2827 @.***> wrote:

Wow this is intense! Guys i just want a simple sketch to make my flight sim yoke auto centre. Nothing more.

I have x and y axis for roll and pitch of flight stick, both connected to two dc motors with full h bridge. I can give pwm signal to motors and it moves within those commands if programmed without this library, i am just unable to use this library to auto centre my yoke using the signals from pots connected to the range of yoke. My yoke has physical end switches so no need to add a program for hard end stops...can someone plz help what i am missing from the example code. I used the same code to pass the DIRECTION and PWM to h bridge, but the motor simply doesn't move

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/YukMingLaw/ArduinoJoystickWithFFBLibrary/issues/29#issuecomment-896497538, or unsubscribe https://github.com/notifications/unsubscribe-auth/ACNKBVJR64NX5KMB3EDL3YDT4H6NRANCNFSM43AYZ6BQ . Triage notifications on the go with GitHub Mobile for iOS https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675 or Android https://play.google.com/store/apps/details?id=com.github.android&utm_campaign=notification-email .

sgtnoodle avatar Aug 11 '21 06:08 sgtnoodle