Starting precalibrated motor enters warp speed some of the time

Hello all, I have two ElectroCraft MP24 motors that I’m trying to use to drive a robot in a barn. However, I haven’t been able to start them reliably after a power cycle by using the index channel.

These motors have a lot of resistance on them (the wheels literally have metal rods dragging on them to clean off manure as the robot moves), but I’ve been able to get them manually calibrated easily (i.e., axis.requested_state = AXIS_STATE_FULL_CALIBRATION_SEQUENCE), after which they work swimmingly even under a lot of drag.

However, even with the wheels completely removed (i.e., hardly any resistance at all), I haven’t been able to start the motors reliably with axis.requested_state = AXIS_STATE_ENCODER_INDEX_SEARCH per the skip encoder calibration with index channel tutorial on the ODrive website. Sometimes, I go through the process (odrv0.reboot(), encoder search, axis.requested_state=AXIS_STATE_CLOSED_LOOP_CONTROL) and the motor does work, but more often (6/8 times in my quick test) the motor just takes off (in the backwards direction) at a fast, consistent but random speed (i.e., sometimes it’s consistently fast, sometimes its consistently holy-crap-hit-the-kill-switch fast). Depending on fast it decides to go, it will sometimes trip an over-current or over-velocity error.

I’ve been fighting this long enough to create the following script, which takes the motor from odrv0.erase_configuration() to what I’m trying to use:

import odrive
from odrive.enums import *
import time
from odrive.utils import dump_errors
import math
from fibre import ChannelBrokenException

def setup_sequence():
    odrv = odrive.find_any()
    axes = [odrv.axis0, odrv.axis1]
    print("Configuring odrive...")
    # Alternate theory for this step: It's possible to just save and load config from a file.
    for axis in axes:
        axis.controller.config.vel_limit = 50
        axis.motor.config.pole_pairs = 2
        axis.encoder.config.cpr = 4000
        axis.motor.config.torque_constant = 1.350645
        axis.motor.config.motor_type = MOTOR_TYPE_HIGH_CURRENT
        axis.motor.config.resistance_calib_max_voltage = 10
        axis.motor.config.calibration_current = 40
        #Temporarily removed and set back to default
        # axis.encoder.config.calib_scan_distance = (
        #     33 * 2 * math.pi
        # )  # 33 rotations, for one complete revolution of motor after gearbox plus one for good measure
        axis.controller.config.vel_gain = 9  # I believe this could be raised to as high as 17 if needed. Was lowered to help with a glitchy motor.
        axis.controller.config.vel_integrator_gain = 30
        axis.controller.config.control_mode = CONTROL_MODE_VELOCITY_CONTROL
        axis.motor.config.current_lim = 80
        axis.motor.config.requested_current_range = 100  # Requires reboot

    odrv.config.max_regen_current = 100
    odrv.config.dc_max_negative_current = -100
    odrv.config.brake_resistance = 0

    for num, axis in enumerate(axes):
        print("Running calibration on Axis #%d" % num)
        axis.encoder.config.use_index = True
        axis.requested_state = AXIS_STATE_MOTOR_CALIBRATION
        while axis.current_state != AXIS_STATE_IDLE:
            time.sleep(1)
        if axis.error != 0:
            print(
                "Axis #%d failed initial calibration. See upcoming error dump for information"
                % num
            )
            continue
        axis.requested_state = AXIS_STATE_ENCODER_INDEX_SEARCH
        while axis.current_state != AXIS_STATE_IDLE:
            time.sleep(1)
        axis.requested_state = AXIS_STATE_ENCODER_OFFSET_CALIBRATION
        while axis.current_state != AXIS_STATE_IDLE:
            time.sleep(1)
        axis.encoder.config.pre_calibrated = True
        if not axis.encoder.config.pre_calibrated:  # Verify it was accepted
            print("Encoder calibration failed for axis #%d" % num)
            continue
        axis.motor.config.pre_calibrated = True

    print("Dump of final odrive state:")
    dump_errors(odrv)

    odrv.save_configuration()
    try:
        odrv.reboot()
    except ChannelBrokenException:
        pass
    print("As long as you didn't recieve any errors, odrive should be good to go")


if __name__ == "__main__":
    setup_sequence()

Information from one of the axes:
Encoder:

error = 0x0000 (int)
is_ready = True (bool)
index_found = True (bool)
shadow_count = -1519 (int)
count_in_cpr = 2481 (int)
interpolation = 0.5 (float)
phase = 1.5009195804595947 (float)
pos_estimate = -0.3795000910758972 (float)
pos_estimate_counts = -1518.000244140625 (float)
pos_cpr = 0.620437502861023 (float)
pos_cpr_counts = 2481.75 (float)
pos_circular = 0.6212158799171448 (float)
hall_state = 2 (int)
vel_estimate = 0.0 (float)
vel_estimate_counts = 0.0 (float)
calib_scan_response = 0.0 (float)
pos_abs = 0 (int)
spi_error_rate = 0.0 (float)
config:
  mode = 0 (int)
  use_index = True (bool)
  find_idx_on_lockin_only = False (bool)
  abs_spi_cs_gpio_pin = 1 (int)
  zero_count_on_find_idx = True (bool)
  cpr = 4000 (int)
  offset = -9996 (int)
  pre_calibrated = True (bool)
  offset_float = -0.25721877813339233 (float)
  enable_phase_interpolation = True (bool)
  bandwidth = 1000.0 (float)
  calib_range = 0.019999999552965164 (float)
  calib_scan_distance = 50.26548385620117 (float)
  calib_scan_omega = 12.566370964050293 (float)
  idx_search_unidirectional = False (bool)
  ignore_illegal_hall_state = False (bool)
  sincos_gpio_pin_sin = 3 (int)
  sincos_gpio_pin_cos = 4 (int)
set_linear_count(count: int)

Motor:

error = 0x0000 (int)
armed_state = 0 (int)
is_calibrated = True (bool)
current_meas_phB = -0.0601348876953125 (float)
current_meas_phC = -0.13639980554580688 (float)
DC_calib_phB = -0.8263078927993774 (float)
DC_calib_phC = -0.5083249807357788 (float)
phase_current_rev_gain = 0.05000000074505806 (float)
effective_current_lim = 80.0 (float)
current_control:
  p_gain = 0.7359321117401123 (float)
  i_gain = 123.63776397705078 (float)
  v_current_control_integral_d = -0.18157467246055603 (float)
  v_current_control_integral_q = -1.3240957260131836 (float)
  Ibus = 0.7657429575920105 (float)
  final_v_alpha = 0.5790687203407288 (float)
  final_v_beta = 1.2977954149246216 (float)
  Id_setpoint = 0.0 (float)
  Iq_setpoint = -10.0 (float)
  Iq_measured = -10.034514427185059 (float)
  Id_measured = 0.5472937822341919 (float)
  I_measured_report_filter_k = 1.0 (float)
  max_allowed_current = 121.5 (float)
  overcurrent_trip_level = 135.0 (float)
  acim_rotor_flux = 0.0 (float)
  async_phase_vel = 0.0 (float)
  async_phase_offset = 0.0 (float)
gate_driver:
  drv_fault = 0 (int)
timing_log:
  general = 60988 (int)
  adc_cb_i = 4578 (int)
  adc_cb_dc = 14786 (int)
  meas_r = 10618 (int)
  meas_l = 10758 (int)
  enc_calib = 11170 (int)
  idx_search = 24110 (int)
  foc_voltage = 11114 (int)
  foc_current = 11998 (int)
  spi_start = 39252 (int)
  sample_now = 12912 (int)
  spi_end = 45269 (int)
config:
  pre_calibrated = True (bool)
  pole_pairs = 2 (int)
  calibration_current = 40.0 (float)
  resistance_calib_max_voltage = 10.0 (float)
  phase_inductance = 0.0007359320879913867 (float)
  phase_resistance = 0.12363775819540024 (float)
  torque_constant = 1.3506449460983276 (float)
  direction = -1 (int)
  motor_type = 0 (int)
  current_lim = 80.0 (float)
  current_lim_margin = 8.0 (float)
  torque_lim = inf (float)
  inverter_temp_limit_lower = 100.0 (float)
  inverter_temp_limit_upper = 120.0 (float)
  requested_current_range = 100.0 (float)
  current_control_bandwidth = 1000.0 (float)
  acim_slip_velocity = 14.706000328063965 (float)
  acim_gain_min_flux = 10.0 (float)
  acim_autoflux_min_Id = 10.0 (float)
  acim_autoflux_enable = False (bool)
  acim_autoflux_attack_gain = 10.0 (float)
  acim_autoflux_decay_gain = 1.0 (float)

Controller:

error = 0x0000 (int)
input_pos = -0.36994147300720215 (float)
input_vel = 3.0 (float)
input_torque = 0.0 (float)
pos_setpoint = -0.36994147300720215 (float)
vel_setpoint = 3.0 (float)
torque_setpoint = 0.0 (float)
trajectory_done = True (bool)
vel_integrator_torque = 3.645057565401714e-41 (float)
anticogging_valid = False (bool)
config:
  gain_scheduling_width = 10.0 (float)
  enable_vel_limit = True (bool)
  enable_current_mode_vel_limit = True (bool)
  enable_gain_scheduling = False (bool)
  enable_overspeed_error = True (bool)
  control_mode = 2 (int)
  input_mode = 1 (int)
  pos_gain = 20.0 (float)
  vel_gain = 9.0 (float)
  vel_integrator_gain = 30.0 (float)
  vel_limit = 50.0 (float)
  vel_limit_tolerance = 1.2000000476837158 (float)
  vel_ramp_rate = 1.0 (float)
  torque_ramp_rate = 0.009999999776482582 (float)
  circular_setpoints = False (bool)
  circular_setpoint_range = 1.0 (float)
  homing_speed = 0.25 (float)
  inertia = 0.0 (float)
  axis_to_mirror = 255 (int)
  mirror_ratio = 1.0 (float)
  load_encoder_axis = 1 (int)
  input_filter_bandwidth = 2.0 (float)
  anticogging: ...
move_incremental(displacement: float, from_input_pos: bool)
start_anticogging_calibration()

Thank you for any guidance on this.

If it helps anyone else who run across this problem… it does not make sense to me, but I was able to solve this problem by running axis.requested_state = AXIS_STATE_MOTOR_CALIBRATION before running the index search to prevent this runaway startup.

This sounds a lot like what we call “spinout”, potentially caused by noise on the index signal wire. For ODrive, torque production depends on having an accurate reference between motor phase current direction and the rotor orientation, hence the encoder offset calibration. Index calibration allows an offset to be saved to memory so that the relation between encoder mechanical 0 and electrical 0 is known.

Because ODrive torque depends on this relation, if the index is found at the wrong spot on accident, actual torque output can range from correct (maximum), to 0, and all the way to maximum incorrect (max torque in the opposite direction). Maximum incorrect produces the worst spinout, because a negative torque command produces torque, leading the system to get pegged at max torque and speed until an overcurrent or overspeed error is triggered. You can simulate this by doing
encoder.config.phase_offset += encoder.config.cpr / (2.0 * motor.config.pole_pairs) if you are curious.

Usually this can be fixed by using a shielded encoder cable or adding a 22nf capacitor between the z pin and ground on the ODrive. I am not sure why running motor calibration first would help. It might align the rotor so that there is less rotation to travel to the index, giving less time for interference from the motor phases to cause a false pulse.

1 Like

In addition to the index pin noise, @Lucas_Niewohner make sure you’re following the script completely:

axis.encoder.config.use_index = True
axis.requested_state = AXIS_STATE_FULL_CALIBRATION_SEQUENCE
axis.encoder.config.pre_calibrated = True
axis.motor.config.pre_calibrated = True
odrv0.save_configuration()

Then when you boot, it shouldn’t require any calibration.