Hoverboard motors can't hold consistent low speeds under load

I’m pretty new to working with ODrive and recently I’ve run into a issue that I’ve been struggling to solve. I have been trying to drive a cart with two BLDC hoverboard style wheels (120 cpr) at 0.2 rev/s. I’ve been able to get the wheels to spin very consistently at 0.2 rev/s when there is no load on them in velocity control mode. However, when trying to actually drive the cart and with load on the wheels, I’ve found that both wheels start by spinning faster than 0.2 rev/s (around 0.25 rev/s), but after a few seconds will drop down to about 0.1 rev/s. This lasts for a few seconds before they speed back up to 0.25 rev/s. The motors continually oscillate between overshooting and undershooting the desired speed. Additionally, when I run dump_errors(), it shows no errors in either axis.

My first thought was that this was probably a tuning issue. I tried both changing vel_gain and vel_integrator_gain to a variety of values. I found that this did affect the behavior, but did not fix it. Changing these values either made the wheels start rapidly vibrating during the period it was slowing down or make the cart stop entirely instead of just slowing down. I’ve tried changing some other values as well, such as the bandwidth of the encoder or the current limit, but did not see improvement. I’m guessing this is a problem with either how I am configuring the encoder or the controller since the problem is so consistent, but I’m unsure what exactly is wrong.

While trying to find the origin of this problem I was printing out the vel_estimate and vel_estimate_counts from the encoder, while additionally doing my own calculations for the velocity using the difference in shadow_count each second. I did notice that the vel_estimate was always a good bit larger than what I determined in my calculations. When testing the wheels at 0.2 rev/s without load, my calculation would consistently read out 0.2, where as vel_estimate fluctuated between 0.25 and 0.3 rev/s. This could possibly be completely unrelated to the aforementioned problem, but I figured that is was worth mentioning as it seemed peculiar.

I am leaving my configuration settings for the motor, the encoder, and the controller below to try to make sure all relevant information is available. My configuration for axis0 and axis1 are identical. Any help would be greatly appreciated. Thanks!

Motor Settings

DC_calib_phA: 0.4081537127494812 (float)
DC_calib_phB: -0.17754331231117249 (float)
DC_calib_phC: -0.23056994378566742 (float)
I_bus: 0.0 (float)
config:
  I_bus_hard_max: inf (float)
  I_bus_hard_min: -inf (float)
  I_leak_max: 0.10000000149011612 (float)
  R_wL_FF_enable: False (bool)
  acim_autoflux_attack_gain: 10.0 (float)
  acim_autoflux_decay_gain: 1.0 (float)
  acim_autoflux_enable: False (bool)
  acim_autoflux_min_Id: 10.0 (float)
  acim_gain_min_flux: 10.0 (float)
  bEMF_FF_enable: False (bool)
  calibration_current: 10.0 (float)
  current_control_bandwidth: 100.0 (float)
  current_lim: 40.0 (float)
  current_lim_margin: 8.0 (float)
  dc_calib_tau: 0.20000000298023224 (float)
  inverter_temp_limit_lower: 100.0 (float)
  inverter_temp_limit_upper: 120.0 (float)
  motor_type: 0 (uint8)
  phase_inductance: 0.0002825653937179595 (float)
  phase_resistance: 0.20705918967723846 (float)
  pole_pairs: 20 (int32)
  pre_calibrated: True (bool)
  requested_current_range: 60.0 (float)
  resistance_calib_max_voltage: 4.0 (float)
  torque_constant: 0.2977199852466583 (float)
  torque_lim: inf (float)
current_control:
  I_measured_report_filter_k: 1.0 (float)
  Ialpha_measured: -6.474801063537598 (float)
  Ibeta_measured: -7.8197174072265625 (float)
  Id_measured: 10.063177108764648 (float)
  Id_setpoint: 10.0 (float)
  Iq_measured: 1.342167615890503 (float)
  Iq_setpoint: 0.0 (float)
  Vd_setpoint: 0.0 (float)
  Vq_setpoint: 0.0 (float)
  final_v_alpha: -1.6940659284591675 (float)
  final_v_beta: -1.6403868198394775 (float)
  i_gain: 20.70591926574707 (float)
  p_gain: 0.02825653925538063 (float)
  phase: 1.3816916942596436 (float)
  phase_vel: 40.0 (float)
  power: 35.665802001953125 (float)
  v_current_control_integral_d: 2.3594958782196045 (float)
  v_current_control_integral_q: 0.07132841646671295 (float)
current_meas_phA: -0.40825068950653076 (float)
current_meas_phB: 0.1785786896944046 (float)
current_meas_phC: 0.23011401295661926 (float)
effective_current_lim: 40.0 (float)
error: 0 (uint64)
fet_thermistor:
  config: ...
  temperature: 31.258031845092773 (float)
is_armed: False (bool)
is_calibrated: True (bool)
last_error_time: 0.0 (float)
max_allowed_current: 60.75 (float)
max_dc_calib: 6.075000286102295 (float)
motor_thermistor:
  config: ...
  temperature: 0.0 (float)
n_evt_current_measurement: 28053287 (uint32)
n_evt_pwm_update: 28053297 (uint32)
phase_current_rev_gain: 0.02500000037252903 (float)

Encoder Settings

calib_scan_response: 0.0 (float)
config:
  abs_spi_cs_gpio_pin: 1 (uint16)
  bandwidth: 100.0 (float)
  calib_range: 0.019999999552965164 (float)
  calib_scan_distance: 150.0 (float)
  calib_scan_omega: 12.566370964050293 (float)
  cpr: 120 (int32)
  direction: 1 (int32)
  enable_phase_interpolation: True (bool)
  find_idx_on_lockin_only: False (bool)
  hall_polarity: 0 (uint8)
  hall_polarity_calibrated: True (bool)
  ignore_illegal_hall_state: False (bool)
  index_offset: 0.0 (float)
  mode: 1 (uint16)
  phase_offset: 78 (int32)
  phase_offset_float: 1.447070598602295 (float)
  pre_calibrated: True (bool)
  sincos_gpio_pin_cos: 4 (uint16)
  sincos_gpio_pin_sin: 3 (uint16)
  use_index: True (bool)
  use_index_offset: True (bool)
count_in_cpr: 0 (int32)
delta_pos_cpr_counts: -5.605193857299268e-45 (float)
error: 0 (uint16)
hall_state: 1 (uint8)
index_found: True (bool)
interpolation: 0.5 (float)
is_ready: True (bool)
phase: -0.9917759895324707 (float)
pos_abs: 0 (int32)
pos_circular: 0.01625862345099449 (float)
pos_cpr_counts: 0.9750081300735474 (float)
pos_estimate: 0.008328811265528202 (float)
pos_estimate_counts: 0.9994573593139648 (float)
set_linear_count(obj: object_ref, count: int32)
shadow_count: 0 (int32)
spi_error_rate: 0.0 (float)
vel_estimate: 0.0 (float)
vel_estimate_counts: 0.0 (float)

Controller Settings

anticogging_valid: False (bool)
autotuning:
  frequency: 0.0 (float)
  pos_amplitude: 0.0 (float)
  pos_phase: 0.0 (float)
  torque_amplitude: 0.0 (float)
  torque_phase: 0.0 (float)
  vel_amplitude: 0.0 (float)
  vel_phase: 0.0 (float)
config:
  anticogging: ...
  axis_to_mirror: 255 (uint8)
  circular_setpoint_range: 1.0 (float)
  circular_setpoints: False (bool)
  control_mode: 2 (uint8)
  electrical_power_bandwidth: 20.0 (float)
  enable_current_mode_vel_limit: True (bool)
  enable_gain_scheduling: False (bool)
  enable_overspeed_error: True (bool)
  enable_vel_limit: True (bool)
  gain_scheduling_width: 10.0 (float)
  homing_speed: 0.25 (float)
  inertia: 0.0 (float)
  input_filter_bandwidth: 2.0 (float)
  input_mode: 1 (uint8)
  load_encoder_axis: 0 (uint8)
  mechanical_power_bandwidth: 20.0 (float)
  mirror_ratio: 1.0 (float)
  pos_gain: 1.0 (float)
  spinout_electrical_power_threshold: 10.0 (float)
  spinout_mechanical_power_threshold: -10.0 (float)
  steps_per_circular_range: 1024 (int32)
  torque_mirror_ratio: 0.0 (float)
  torque_ramp_rate: 0.009999999776482582 (float)
  vel_gain: 2.0 (float)
  vel_integrator_gain: 5.0 (float)
  vel_limit: 10.0 (float)
  vel_limit_tolerance: 1.2000000476837158 (float)
  vel_ramp_rate: 1.0 (float)
electrical_power: 3.6536953449249268 (float)
error: 0 (uint8)
input_pos: 0.0 (float)
input_torque: 0.0 (float)
input_vel: 0.0 (float)
last_error_time: 0.0 (float)
mechanical_power: 0.0 (float)
move_incremental(obj: object_ref, displacement: float, from_input_pos: bool)
pos_setpoint: 0.0 (float)
start_anticogging_calibration(obj: object_ref)
torque_setpoint: 0.0 (float)
trajectory_done: True (bool)
vel_integrator_torque: 0.0 (float)
vel_setpoint: 0.0 (float)

Did you try setting vel_integrator_gain to 0?

Hmm. The vel_estimate variance is interesting. You can try playing with the encoder.config.bandwidth. You only have 24 pulses / sec so 100rad/sec is actually pretty close… but maybe tweak it up or down to see what you get