SmoothingSensor: Experimental sensor angle extrapoltion

Branching off from discussion in this thread Programming the Lepton from start to finish - #100 by dekutree64

I’ve been working on a method to get better performance from low resolution or slow updating sensors, by using the low-pass filtered shaft velocity to predict the true angle from the time elapsed since the last update.

So far the results are somewhat promising, however it needs more testing to really verify its usefulness. I only have digital hall sensors to work with, and they are a fair bit different from other sensors.

Here is the present state of the code:

class SmoothingSensor : public Sensor
{
public:
  SmoothingSensor(Sensor *s, const FOCMotor *m):wrappedSensor(s),motor(m){}
  
  void init() { wrappedSensor->init(); }

  float getVelocity() { return wrappedSensor->getVelocity(); }
  int needsSearch() { return wrappedSensor->needsSearch(); }

  float getMechanicalAngle() {
    return enable_smoothing == false ? wrappedSensor->getMechanicalAngle() :
      _normalizeAngle(wrappedSensor->getMechanicalAngle() + projectedAngle());
  }

  float getAngle() {
    return enable_smoothing == false ? wrappedSensor->getAngle() :
      wrappedSensor->getAngle() + projectedAngle();
  }
  
  double getPreciseAngle() {
    return enable_smoothing == false ? wrappedSensor->getPreciseAngle() :
      wrappedSensor->getPreciseAngle() + projectedAngle();
  }

  int32_t getFullRotations() {
    float angle = getAngle();
    return lroundf((angle - _normalizeAngle(angle)) / _2PI);
  }
  
  void update() {
    if(sensor_cnt++ < sensor_downsample) return;
    sensor_cnt = 0;
    wrappedSensor->update();
  }

  bool enable_smoothing = true;
  unsigned int sensor_downsample = 0; // parameter defining the ratio of downsampling for sensor update
  unsigned int sensor_cnt = 0; // counting variable for downsampling

protected:
  virtual float projectedAngle() { return motor->shaft_velocity * (_micros() - wrappedSensor->angle_prev_ts) / 1000000; }
  float getSensorAngle() { return wrappedSensor->getSensorAngle(); }
  
  Sensor *wrappedSensor;
  const FOCMotor *motor;
};

And to deal with a couple of problems specific to hall sensors, I’ve also written this class:

class SmoothingHallSensor : public SmoothingSensor
{
public:
  SmoothingHallSensor(int encA, int encB, int encC, int pp, const FOCMotor *m):
    SmoothingSensor(&hallSensor, m), hallSensor(encA, encB, encC, pp) {}

  // Pass-through functions to the hallSensor. You can use these or call the hallSensor's functions directly
  void handleA() { hallSensor.handleA(); }
  void handleB() { hallSensor.handleB(); }
  void handleC() { hallSensor.handleC(); }
  void enableInterrupts(void (*doA)() = nullptr, void(*doB)() = nullptr, void(*doC)() = nullptr)
    { hallSensor.enableInterrupts(doA, doB, doC); }
  void attachSectorCallback(void (*onSectorChange)(int a) = nullptr)
    { hallSensor.attachSectorCallback(onSectorChange); }

  HallSensor hallSensor;
  // The predicted angle is always 0 to 60 electrical degrees ahead of where it would be without 
  // smoothing, so offset it backward by 30 degrees to get back approximately in phase with the rotor
  float phase_correction = -_PI_6;
  float velocity_threshold = 5; // Shaft speed below which phase_correction will not be applied
  
protected:
  float projectedAngle() {
    float offset = 0;
    if (motor->shaft_velocity < -velocity_threshold) offset = -phase_correction / motor->pole_pairs;
    else if (motor->shaft_velocity > velocity_threshold) offset = phase_correction / motor->pole_pairs;
    return offset + motor->shaft_velocity * (_micros() - hallSensor.pulse_timestamp) / 1000000;
  }
};

It uses hallSensor.pulse_timestamp to make the prediction, rather than angle_prev_ts which is not used by the HallSensor class. And it attempts to correct for the fact that extrapolating from hall state changes results in a field weakening effect (higher top speed but lower torque) because the predicted angle is always 0 to 60 electrical degrees ahead of where it would be without smoothing.

To use these requires 3 other changes:

  • the Sensor base class must declare SmoothingSensor a friend class so it can access the protected functions and angle_prev_ts
  • HallSensor must declare SmoothingHallSensor a friend
  • change HallSensor’s pulse_timestamp to protected rather than private
5 Likes

I know I said I was going to stop working on this thing, but insomnia is the mother of invention, so it’s refactoring time already.

While trying unsuccessfully to sleep, I made several realizations:

  • Having SmoothingHallSensor inherit from SmoothingSensor is dumb. The wrapper approach was Runger’s idea to make it generic, but if I’m making a specialized version anyway it would be better off as its own class. This also saves a few CPU cycles because projectedAngle() no longer needs to be virtual.

  • The enable_smoothing variable is a waste of CPU cycles because if you want to disable smoothing you can just call motor.linkSensor with the wrapped sensor. But because SmoothingHallSensor will no longer be a wrapper, it will still need it.

  • Instead of a simple voltage cutoff for the phase correction, I can use the kv and applied voltage to calculate the no load speed and apply it proportionally. Also it needs to be multiplied by sensor direction.

  • The velocity threshold is still useful because I can really improve low speed motion by switching to a pseudo-open-loop mode when it gets below the speed where it transitions from continuous motion to moving in cogging steps.

So that will be my task for today. Or perhaps tomorrow because there’s a pretty good chance I’ll end up spending all my time helping my dad sort out a perplexing car problem and then fall asleep.

Can also spiral out of hand. Be good to yourself. I was able to quit my sleeping-pharma when I re-discovered Chamomile tè. Especially the pure flower (organic), bag it yourself kind.

Well, I think it’s time to admit defeat for the time being. There are no improvements in functionality of this code, only elegance.

class SmoothingSensor : public Sensor
{
public:
  SmoothingSensor(Sensor *s, const FOCMotor *m):wrappedSensor(s),motor(m){}

  float getVelocity() { return wrappedSensor->getVelocity(); }
  int needsSearch() { return wrappedSensor->needsSearch(); }
  float getMechanicalAngle() { return _normalizeAngle(wrappedSensor->getMechanicalAngle() + projectedAngle()); }
  float getAngle() { wrappedSensor->getAngle() + projectedAngle(); }
  double getPreciseAngle() { wrappedSensor->getPreciseAngle() + projectedAngle(); }
  int32_t getFullRotations() { float angle = getAngle(); return lroundf((angle - _normalizeAngle(angle)) / _2PI); }
  
  void update() {
    if(sensor_cnt++ < sensor_downsample) return;
    sensor_cnt = 0;
    wrappedSensor->update();
  }

  unsigned int sensor_downsample = 0; // parameter defining the ratio of downsampling for sensor update
  unsigned int sensor_cnt = 0; // counting variable for downsampling

protected:
  float projectedAngle() { return motor->shaft_velocity * (_micros() - wrappedSensor->angle_prev_ts) * 1e-6f; }
  float getSensorAngle() { return wrappedSensor->getSensorAngle(); }
  void init() { wrappedSensor->init(); }
  
  Sensor *wrappedSensor;
  const FOCMotor *motor;
};

class SmoothingHallSensor : public HallSensor
{
public:
  SmoothingHallSensor(int encA, int encB, int encC, int pp, const FOCMotor *m):
    HallSensor(encA, encB, encC, pp), motor(m) {}

  float getMechanicalAngle() { return _normalizeAngle(HallSensor::getMechanicalAngle() + projectedAngle()); }
  float getAngle() { return  HallSensor::getAngle() + projectedAngle(); }
  double getPreciseAngle() { return HallSensor::getPreciseAngle() + projectedAngle(); }
  int32_t getFullRotations() { float angle = getAngle(); return lroundf((angle - _normalizeAngle(angle)) / _2PI); }

  bool enable_smoothing = true;
  
  // The predicted angle is always 0 to 60 electrical degrees ahead of where it would be without 
  // smoothing, so offset it backward by 30 degrees to get back approximately in phase with the rotor
  float phase_correction = -_PI_6;
  float velocity_threshold = 5; // Shaft speed below which phase_correction will not be applied
  
protected:
  float projectedAngle() {
    if (enable_smoothing == false) return 0;
    float offset = 0;
    if (motor->shaft_velocity < -velocity_threshold) offset = -phase_correction / motor->pole_pairs;
    else if (motor->shaft_velocity > velocity_threshold) offset = phase_correction / motor->pole_pairs;
    return offset + motor->shaft_velocity * (_micros() - pulse_timestamp) * 1e-6f;
  }
  const FOCMotor *motor;
};

At least with hall sensors, the only thing it’s really good for is continuous running velocity mode. If I slow the rotor down with my fingers it becomes twitchy, whereas with un-smoothed hall sensors it continues running smoothly even down to near-zero speed. And in angle mode it twitches regularly due to its own inertia. I haven’t narrowed down exactly what is the sequence of feedbacks that causes it, so it’s possible it could be eliminated with more effort.

Using open loop for precise angle control is promising, but the twitching issue applies to it too, as it twitches when transitioning back to closed loop control. The current implementation is rather hacky, having the sensor monkey with the motor’s variables. And as-is it only works in velocity or angle mode with no hardware current sense. This code also includes my idea of applying phase correction proportional to shaft speed relative to no-load speed, but it didn’t make any noticeable difference compared to the simple velocity threshold.

class SmoothingHallSensor : public HallSensor
{
public:
  SmoothingHallSensor(int encA, int encB, int encC, int pp, FOCMotor *m):
    HallSensor(encA, encB, encC, pp), motor(m) {}

  float getMechanicalAngle() {
    return open_loop_active ? _normalizeAngle(motor->shaft_angle) :
      _normalizeAngle(HallSensor::getMechanicalAngle() + projectedAngle());
  }
  float getAngle() {
    return open_loop_active ? motor->shaft_angle :
      HallSensor::getAngle() + projectedAngle();
  }
  double getPreciseAngle() {
    return open_loop_active ? motor->shaft_angle :
      HallSensor::getPreciseAngle() + projectedAngle();
  }
  int32_t getFullRotations() {
    float angle = getAngle();
    return lroundf((angle - _normalizeAngle(angle)) / _2PI);
  }

  void update() {
    // At low speed if motor is not lagging significantly behind sensor angle, use pseudo-open-loop mode
    if (open_loop_active)
    {
      if (!enable_smoothing ||
          fabs(motor->shaft_velocity) > velocity_threshold * 1.1 ||
          fabs(motor->shaft_velocity_sp) > velocity_threshold * 1.1 ||
          fabs(motor->shaft_angle - HallSensor::getAngle()) > _PI / motor->pole_pairs)
        open_loop_active = false;
    }
    else if (enable_smoothing && fabs(motor->shaft_velocity) < velocity_threshold && fabs(motor->shaft_velocity_sp) < velocity_threshold)
      open_loop_active = true;
    
    if (open_loop_active)
    {
      // get current timestamp
      unsigned long now_us = _micros();
      // calculate the sample time from last call
      float Ts = (now_us - open_loop_timestamp) * 1e-6f;
      // quick fix for strange cases (micros overflow + timestamp not defined)
      if(Ts <= 0 || Ts > 0.5f) Ts = 1e-3f;
      motor->shaft_angle += motor->shaft_velocity_sp * Ts;
      open_loop_timestamp = now_us;
      // Drive motor at half of voltage/current limit, to reduce heating
      motor->voltage.q = !_isset(motor->phase_resistance) ? motor->voltage_limit / 2 :
        _constrain(motor->current_limit*motor->phase_resistance/2,-motor->voltage_limit, motor->voltage_limit);
    }
  }

  bool enable_smoothing = true;
  
  // The predicted angle is always 0 to 60 electrical degrees ahead of where it would be without 
  // smoothing, so offset it backward by 30 degrees to get back approximately in phase with the rotor.
  // If the motor has its KV_rating set, it will be applied proportionally to the shaft velocity relative
  // to no-load speed for the applied voltage, improving efficiency during acceleration.
  float phase_correction = -_PI_6;

  // Shaft speed below which the sensor will switch to operating in a pseudo-open-loop mode, enabling 
  // precise angle control despite being unable to actually sense it. This threshold will likely need to be tuned.
  float velocity_threshold = 10;
  bool open_loop_active = false;
  long open_loop_timestamp;
  
protected:
  float projectedAngle() {
    if (enable_smoothing == false) return 0;

    float offset = phase_correction / motor->pole_pairs;
    if (_isset(motor->KV_rating)) {
      float no_load_speed = motor->KV_rating * fabs(motor->voltage.q) * _RPM_TO_RADS;
      if (fabs(motor->shaft_velocity) < no_load_speed)
        return (offset / no_load_speed + 1) * motor->shaft_velocity * (_micros() - pulse_timestamp) * 1e-6f;
    }
    if (motor->shaft_velocity < 0) offset = -offset;
    return offset + motor->shaft_velocity * (_micros() - pulse_timestamp) * 1e-6f;
  }
  FOCMotor *motor;
};
1 Like

Can you give us some example code that uses this? There’s a couple outstanding questions that would be answered by this. I’m trying to make a fan, so this should be good for me, otherwise I can hack up the library to do this projected sensor thing maybe or something. It’s important for a pwm based sensor, which I am ending up using because you don’t get that many updates per second with one.

Create the wrapped sensor like normal, pass it to the SmoothingSensor, and link the SmoothingSensor to the motor. Also set sensor_downsample to something other than 0 or it will sample the sensor every update like usual.

MagneticSensorPWM pwmSensor(your parameters here);
SmoothingSensor(&pwmSensor, &motor);

void setup() {
...
  motor.linkSensor(&smoothingSensor);
  smoothingSensor.sensor_downsample = something;

Now that I think about it, I wonder if it would be better to sample the sensor on a set time interval rather than a set fraction of loop cycles. Something like

class SmoothingSensor : public Sensor
{
public:
  SmoothingSensor(Sensor *s, const FOCMotor *m):wrappedSensor(s),motor(m){}

  float getVelocity() { return wrappedSensor->getVelocity(); }
  int needsSearch() { return wrappedSensor->needsSearch(); }
  float getMechanicalAngle() { return _normalizeAngle(wrappedSensor->getMechanicalAngle() + projectedAngle()); }
  float getAngle() { wrappedSensor->getAngle() + projectedAngle(); }
  double getPreciseAngle() { wrappedSensor->getPreciseAngle() + projectedAngle(); }
  int32_t getFullRotations() { float angle = getAngle(); return lroundf((angle - _normalizeAngle(angle)) / _2PI); }
  
  void update() {
    unsigned long now_us = _micros();
    if (now_us - sample_timestamp >= sample_interval || 
      now_us < sample_timestamp) { // _micros() overflow
    sample_timestamp = now_us;
    wrappedSensor->update();
  }

  unsigned long sample_interval = 0; // how often to sample the sensor, in microseconds

protected:
  float projectedAngle() { return motor->shaft_velocity * (_micros() - wrappedSensor->angle_prev_ts) * 1e-6f; }
  float getSensorAngle() { return wrappedSensor->getSensorAngle(); }
  void init() { wrappedSensor->init(); }
  
  Sensor *wrappedSensor;
  const FOCMotor *motor;
  unsigned long sample_timestamp; // Value of _micros() when sensor was last read
};
1 Like

Why this? It seems to me this would introduce some additional latency, and make the wrapped updates less “regular”, but I guess since the expected frequency of calls to the wrapping sensor is much larger than the update frequency the difference would not be big. If there are a lot of comms going on (making the loop cycles irregular) then it could improve things, of course. Hard to call, somehow.

If you like I can try to integrate this into the drivers repository? Or would you prefer to send a pull request on GitHub?

1 Like

I was imagining with a fast CPU and slow sensor, you might complete several loop iterations in less time that it takes to read the sensor, so even with downsampling you’re still spending more time on sensor reading than you are running the motor. But with interval setting, you could measure the sensor time and then decide what percentage of your total time you want to spend on it.

But it’s probably 6 of one, half dozen of the other… if the sensor is that slow, the motor won’t run smoothly nomatter what you do, due to the stator field being stalled during the sensor wait.

Let me do a pull request… that will give me a kick in the rear to finally learn how to use github :slight_smile: And there are a few tweaks to other sensor classes to get the best quality timestamps for prediction.

Awesome, if you have any questions let me know…

Please make your changes against a fresh pull of the dev branch of our drivers repository - in that way it should be easy to just merge it in.

In terms of how it works, you need access to the angle_prev_ts field, so I think we should probably just make the field public in Sensor. Using the friend class would mean we’d have to put it in the main library, which I would avoid for the moment.

In terms of the HallSensor, it might be better to make a separate subclass SmoothingHallSensor to deal with the differences in timestamp handling?

For the encoder, I don’t see much benefit to the smoothing - except possibly at low speeds?

1 Like

I could go either way on the friend class. It’s done by forward declaration, so the actual SmoothingSensor class doesn’t have to be included in the main branch. And the init function would also need to be made public without it.

Where should I put SmoothingSensor in the drivers repository? Utilities?

I’ve done a little more studying of its behavior, and understand it better now. The major twitchiness when slowing the motor down by force is due to the predicted angle getting far enough ahead of reality that the electrical angle wraps around and starts pulling the rotor in the opposite direction (similar to missing steps in open loop). I think that can only happen during deceleration, so second-order prediction may fix it. But a simpler method is to constrain the projection to ±_PI_6/pole_pairs, i.e. don’t project more than one hall sensor step ahead from the last reading. I think that’s a reasonable restriction for any kind of sensor in any situation.

Another problem is due to interrupts. See if you can spot it :slight_smile:

  float getAngle() { return wrappedSensor->getAngle() + projectedAngle(); }

If a sensor update interrupt happens in the middle of those two calls, the projection is done relative to the new angle (near-zero delta time), but added to the old angle (which should have had large delta), so you end up pulling the motor backward until the next update. I can change it to

  float getAngle() {
    noInterupts();
    float angle = wrappedSensor->getAngle() + projectedAngle();
    interrupts();
    return angle;
  }

But then it will be important that it’s never called from inside an interrupt, else you’ll have interrupts enabled inside an interrupt, and nested interrupts are serious business. I’ve done it before, but it requires a very carefully written interrupt handler, and I doubt the ones for all the CPUs supported by Arduino are made for it. An interrupt disable function is supposed to return the previous state, so you can restore to that after your critical section rather than unconditionally enabling interrupts. But apparently whoever designed the Arduino library didn’t know what they were doing because noInterrupts() doesn’t return anything, and there doesn’t appear to be a function to check the current state either…

And in fact this is an issue for HallSensor itself,

float HallSensor::getAngle() {
  return ((float)(electric_rotations * 6 + electric_sector) / (float)cpr) * _2PI ;
}

That may get electric_rotations and electric_sector from different updates. But if I add interrupt disable/enable around that, then interrupts will be enabled in the middle of the SmoothingSensor::getAngle’s critical section. This is terrible. I think HallSensor is going to need some major rework even without SmoothingSensor.

MagneticSensorPWM updates via interrupts, but I think it’s ok because it only sets the pulse duration in the interrupt, and the actual angle is set from that in the base Sensor::update which won’t be happening in the middle of anything else.

I don’t think there are any other sensors that update via interrupts, so if I fix HallSensor then I think SmoothingSensor will be ok without disabling/enabling interrupts.

SmoothingSensor works quite well now after fixing the pervasive interrupt problems (see Refactoring interrupt-based sensor code · Issue #270 · simplefoc/Arduino-FOC · GitHub) and a bad condition in HallSensor::getVelocity, if(_micros() - pulse_timestamp > last_pulse_diff) which would return zero velocity any time the motor decelerates at all. I changed it to pulse_diff*2 so it usually won’t trigger unless the motor has actually been stopped for a short while.

In line with the new approach of copying the volatile interrupt variables into the base Sensor state variables during update(), I’ve changed SmoothingSensor to perform its prediction once during update() and use that for the duration of the frame.

Hall sensors do still need the phase correction (-pi/6). With it, I get more or less the same top speed as non-smoothed. Without it, top speed is 15% higher due to field weakening. I’m going to put it in the one class instead of having a separate SmoothingHallSensor. It seems to work fine with the full offset applied even at near-zero speed, so I removed the velocity threshold.

Low speed behavior is similar with or without smoothing. Below about 15rad/s (150rpm) is stop-and-go rather than continuous turning. And there’s no more twitching when slowing the rotor down with my fingers or running in angle mode (at least, no more than there is without smoothing). In all cases, there’s less vibration and noise than HallSensor alone.

I’d still like to do the pseudo-open-loop for precise positioning between hall steps, but I think that will need some elaborate logic to gradually transition into and out of open loop mode to prevent twitching. I’ll call it done for now, and write up some documentation and an example project and see if we can get it integrated into the dev branch.

class SmoothingSensor : public Sensor
{
public:
  SmoothingSensor(Sensor *s, const FOCMotor *m):wrappedSensor(s),motor(m){}

  float getVelocity() { return wrappedSensor->getVelocity(); }
  int needsSearch() { return wrappedSensor->needsSearch(); }

  void update() {
    if(sensor_cnt++ >= sensor_downsample) {
      sensor_cnt = 0;
      wrappedSensor->update();
    }
    
    // Copy state variables from the sensor
    angle_prev = wrappedSensor->angle_prev;
    angle_prev_ts = wrappedSensor->angle_prev_ts;
    full_rotations = wrappedSensor->full_rotations;

    // Perform angle prediction using low-pass filtered velocity, but don't advance more than 
    // pi/3 (equivalent to one step of block commutation) from the last angle reading.
    float dt = (_micros() - angle_prev_ts) * 1e-6f;
    angle_prev += _constrain(motor->shaft_velocity * dt, -_PI_3 / motor->pole_pairs, _PI_3 / motor->pole_pairs);

    // Apply phase correction if needed
    if (phase_correction != 0) {
      if (motor->shaft_velocity < -0) angle_prev -= phase_correction / motor->pole_pairs;
      else if (motor->shaft_velocity > 0) angle_prev += phase_correction / motor->pole_pairs;
    }

    // Handle wraparound of the projected angle
    if (angle_prev < 0) full_rotations -= 1, angle_prev += _2PI;
    else if (angle_prev >= _2PI) full_rotations += 1, angle_prev -= _2PI;
  }

  unsigned int sensor_downsample = 0; // parameter defining the ratio of downsampling for sensor update
  unsigned int sensor_cnt = 0; // counting variable for downsampling

  // For hall sensors, the predicted angle is always 0 to 60 electrical degrees ahead of where it would be without
  // smoothing, so offset it backward by 30 degrees (set to -PI_6) to get the average in phase with the rotor
  float phase_correction = 0;//-_PI_6;

protected:
  float getSensorAngle() { return wrappedSensor->getSensorAngle(); }
  void init() { wrappedSensor->init(); }
  
  Sensor *wrappedSensor;
  const FOCMotor *motor;
};
3 Likes

From what I have been reading on sensorless control systems, they often use a module that extrapolates the position of the rotor based on the inertia an acceleration being exhibited by the motor, too. Smoothing is a really good start, though, that gets most of it.

I am looking forward this as I could possibly use simpleFOC on hoverboard controllers.
Emanuel Feru developed this with Simulink in Matlab.
So he is using the extrapolation/advance control only if the speed is high enough(simple commutation is used close to standstill), and if there are no high transients (high changes of speed/direction)

Is it? The note “Always authorize advance control if the motor angle is measured (e.g. using an encoder).” sounds like it’s only disabled at low speed in sensorless mode. Unless that’s referring to a high resolution encoder, and extrapolation is disabled at low speed with hall sensors.

I finally went ahead and proposed my interrupt changes Fix for issue #270 by dekutree64 · Pull Request #276 · simplefoc/Arduino-FOC · GitHub

And SmoothingSensor is online Arduino-FOC-drivers/src/encoders/smoothing at dev · dekutree64/Arduino-FOC-drivers · GitHub
But I can’t do a pull request on it until the friend declaration in Sensor is accepted. I changed the code style some to match CalibratedSensor, which is the most similar thing that already exists (I also copypasta’d its readme).

What’s had me hung up these past couple weeks is the bit where the wrapped sensor variables are copied,

    angle_prev = wrappedSensor->angle_prev;
    angle_prev_ts = wrappedSensor->angle_prev_ts;
    full_rotations = wrappedSensor->full_rotations;

It occurred to me that I could use wrappedSensor->getMechanicalAngle() and wrappedSensor->getFullRotations() instead of copying the state variables directly, and if there was a virtual function to get the timestamp too, the sensor subclasses wouldn’t have to be changed to keep the base class state variables up to date, and would still have the potential to return real-time updated values via the virtual get functions, and SmoothingSensor wouldn’t have to be declared a friend of Sensor.

But in the end I decided to stick with my original solution of forcing subclasses to use the Sensor state variables. A virtual getTimestamp() would be ambiguous whether the returned value goes with getAngle() or getSensorAngle(). And preserving the real-time behavior would require more changes, and probably isn’t a good thing anyway.

There is no sensorless.
This was meant to feed the foc model with an encoder angle, but nobody used it. In this case you always use FOC.
But with the hall sensors you use commutation at low speed.

Hi,

I might be able to test this in a few days with some hoverboard controllers.
I just saw a bug in VESC interpolation was just fixed 2 days ago and this got me thinking… How do you test the interpolation actually ?
It could run smooth but generate phase advance, so a speed higher than the KV rating would be a problem ?
Have you ever compared the smoothing sensor output with an encoder output ?

I just hold the motor in my hand to feel the vibration and twitches in the motion, but that may be too hazardous with a big hoverboard motor… maybe just placing your hand on the mount will be enough. You can also hear the difference. Toggle smoothing on and off to compare (call motor.linkSensor with the SmoothingSensor or the HallSensor itself).

Smoothing does generate phase advance with hall sensors (increased top speed), which is counteracted using the phase_correction variable. With that, top speed is about the same with or without smoothing.

I haven’t compared with a high resolution encoder, but it would be great to do.

I did a first test.
That’s probably not the best use case for the extrapolation as we accelerate and decelerate in the loop, but this is the test code I have been using so far.

At the beginning it’s running with the normal sensor, then I flash it with the smoothing sensor.

It acts weird, it is more noisy, maybe I am doing something wrong.

Interesting. I haven’t heard noises like that, but my motor is much smaller (2208 size) and accelerates almost instantly, so maybe this is what second-order prediction would be good for.

To test if phase_correction is set right, set the voltage limit to a low value and command a speed higher than it can actually reach. Switch smoothing on and off while running (call motor.linkSensor) so you can hear the difference immediately and see if the speed changes. If you have serial output, print motor.shaft_velocity once per second so you can see exactly how much it changes.

Another diagnostic technique I used is to record a bunch of information each update, and set up a command to print it all out. The printing takes a while so the motor stops for a moment, but it gives a snapshot of recent undisturbed motion. Might be helpful to find out what’s going on during acceleration/deceleration.

#define HISTORY_SIZE 128
float angleHistory[HISTORY_SIZE];
float predictedHistory[HISTORY_SIZE];
float velocityHistory[HISTORY_SIZE];
float current_spHistory[HISTORY_SIZE];
unsigned long last_tsHistory[HISTORY_SIZE];
unsigned long cur_tsHistory[HISTORY_SIZE];
int historyCursor = 0;

void UpdateHistory()
{
  noInterrupts();
  angleHistory[historyCursor] = sensor.getAngle();
  predictedHistory[historyCursor] = smooth.getAngle();
  velocityHistory[historyCursor] = motor.shaft_velocity;
  current_spHistory[historyCursor] = motor.current_sp;
  last_tsHistory[historyCursor] = sensor.angle_prev_ts;
  cur_tsHistory[historyCursor] = _micros();
  if(++historyCursor >= HISTORY_SIZE)
    historyCursor = 0;
  interrupts();
}

void PrintHistory()
{
  for (int i = 0; i < HISTORY_SIZE; i++)
  {
    const int index = (historyCursor + i) & (HISTORY_SIZE - 1);
    Serial.print(i);
    Serial.print(": A");
    Serial.print(angleHistory[index]);
    Serial.print(", P");
    Serial.print(predictedHistory[index]);
    Serial.print(", V");
    Serial.print(velocityHistory[index]);
    Serial.print(", sp");
    Serial.print(current_spHistory[index]);
    Serial.print(", last_ts");
    Serial.print(last_tsHistory[index]);
    Serial.print(", delta");
    Serial.println(cur_tsHistory[index] - last_tsHistory[index]);
  }
}

Thanks for sharing this, that kind of logging is definitely the best to log very granular data.

I just tried visualizing shaft velocity with this new tool that I came across few days ago.

I will check what happens with constant speed.