ODrive Micro: "Not Calibrated" result when starting closed loop control after full calibration

Hello everyone,

I’m using an ODrive Micro x4 with a T-motor RI50, powering it off a 24V power supply. We’re trying to get homing working with a custom end-stop (hall sensor) and use the onboard encoder for position control, and having trouble getting it working.

We are currently using a Python script to drive the ODrive via USB, and we want to execute a test with flow of operations like this:

  1. Clear config
  2. Upload motor and homing config (see below)
  3. Run calibration
  4. Move between points using position control -20 +20
  5. Enter homing sequence to set position relative to an end stop (not fully implemented as I can’t get past step 4)

The issue is after the FULL_CALIBRATION_SEQUENCE completes successfully (no errors), requesting closed loop control fails with a procedure result of NOT_CALIBRATED. So we are unable to successfully complete step 4 above.

axis0
  active_errors: no error
  disarm_reason: no error
  procedure_result: ProcedureResult.NOT_CALIBRATED
  last_drv_fault: none
internal issues: none

Where I’m confused is that I dump errors after calibration and before requesting closed loop control and it says no errors and the calibration was successful.

axis0
  active_errors: no error
  disarm_reason: no error
  procedure_result: ProcedureResult.SUCCESS
  last_drv_fault: none
internal issues: none

Is there some aspect of calibration which were are not doing properly? Or some other reason you could get a NOT_CALIBRATED result immediately after successful calibration?

I’ve included code snippets and configuration details below.

Thanks,
Finn


Python code:

def calibrate_motor(odrv0):
    logger.info("calibrating")
    odrv0.axis0.requested_state = AxisState.FULL_CALIBRATION_SEQUENCE  # may be offset calibration and not full sequence
    sleep(0.1)
    while odrv0.axis0.current_state != AxisState.IDLE:
        sleep(0.2)
        logger.debug("current state: %s", repr(AxisState(odrv0.axis0.current_state)))
        # dump_errors(odrv0)
    dump_errors(odrv0)
    sleep(5.0)
    logger.info("successfully calibrated")

def test_movement(odrv0):
    logger.info("entering testing range")
    dump_errors(odrv0)
    odrv0.axis0.controller.config.control_mode = ControlMode.POSITION_CONTROL
    odrv0.axis0.requested_state = AxisState.CLOSED_LOOP_CONTROL
    sleep(0.1)
    logger.debug("current state: %s", repr(AxisState(odrv0.axis0.current_state)))
    if odrv0.axis0.current_state != AxisState.CLOSED_LOOP_CONTROL:
        dump_errors(odrv0) # <-- dies here
        exit(1)
    while True:
        odrv0.axis0.controller.input_pos = 20
        for _ in range(5):
            logger.info(f"State: {odrv0.get_gpio_states()}")
            sleep(1)
        odrv0.axis0.controller.input_pos = -20
        for _ in range(5):
            logger.info(f"State: {odrv0.get_gpio_states()}")
            sleep(1)
        logger.info(f"pos_estimate: {odrv0.axis0.pos_estimate}")
        dump_errors(odrv0)

def main():
    odrv0 = odrive.find_any(timeout=5)
    configure_odrive(odrv0)
    calibrate_motor(odrv0)
    test_movement(odrv0)  # fails due to not_calibrated

Motor configuration:

odrv0.config.dc_bus_overvoltage_trip_level = 28
odrv0.config.dc_max_positive_current = 5
odrv0.config.dc_max_negative_current = -1
odrv0.axis0.config.motor.motor_type = MotorType.HIGH_CURRENT
odrv0.axis0.config.motor.pole_pairs = 7
odrv0.axis0.config.motor.torque_constant = 8.27 / 96
odrv0.axis0.config.motor.calibration_current = 3
odrv0.axis0.config.motor.resistance_calib_max_voltage = 3
odrv0.axis0.config.calibration_lockin.current = 3
odrv0.axis0.config.motor.current_soft_max = 10
odrv0.axis0.config.motor.current_hard_max = 12
# odrv0.axis0.controller.config.vel_limit = 10
odrv0.axis0.config.load_encoder = EncoderId.ONBOARD_ENCODER0
odrv0.axis0.config.commutation_encoder = EncoderId.ONBOARD_ENCODER0


odrv0.axis0.controller.config.pos_gain = 10
odrv0.axis0.controller.config.vel_gain = 0.04
odrv0.axis0.controller.config.vel_integrator_gain = 0.2
odrv0.axis0.controller.config.vel_limit = 30
odrv0.axis0.controller.config.vel_limit_tolerance = 1.2
odrv0.axis0.controller.config.vel_ramp_rate = 30
odrv0.axis0.controller.config.input_filter_bandwidth = 20
odrv0.axis0.controller.config.homing_speed = 0.75

Homing configuration - some duplication here, but I don’t think it would affect the calibration:

    odrv0.config.gpio5_mode = GpioMode.DIGITAL
    odrv0.axis0.min_endstop.config.enabled = False
    odrv0.axis0.min_endstop.config.gpio_num = 5
    odrv0.axis0.min_endstop.config.offset = -5
    odrv0.axis0.min_endstop.config.debounce_ms = 500
    odrv0.axis0.min_endstop.config.is_active_high = False  # end stop is active-low

    odrv0.axis0.config.motor.motor_type = MotorType.HIGH_CURRENT
    odrv0.axis0.config.motor.pole_pairs = 7
    odrv0.axis0.config.motor.calibration_current = 3
    odrv0.axis0.config.motor.current_hard_max = 10
    odrv0.axis0.config.startup_homing = False  # Set to true once working

Hi! Couple questions:

  • What firmware version are you running?
  • When are you running save_configuration()? This should ideally be run once after step (2) (to apply the new config) and after step (3) (to save the motor calibration).
  • Is there any other config whatsoever that you’re applying?

Hi!

  1. I am running ODrive 0.6.8.

  2. I am running save_configuration() after the config however, i am not currently running it after calibration. (Since revised and implemented)

  3. There is no other config that i am applying.

I have now moved everything into one python script and made adjustments like you said to, regarding the save_configuration() and no longer get the “Not_Calibrated” error.

The script currently successfully clears config, uploads config, calibrates, enters closed loop control but will make a sudden small movement and then will return this error when dump_errors() is called.

active_errors: no error
disarm_reason: Error(s):
ODriveError.CURRENT_LIMIT_VIOLATION
procedure_result: ProcedureResult.DISARMED
last_drv_fault: none
internal issues: none

I have included the whole script below. All config that is uploaded is within this script.

python code:

import logging
from time import sleep

import odrive
from odrive.enums import EncoderId, AxisState, MotorType, GpioMode, ControlMode
from odrive.utils import dump_errors
from sys import stderr
from rich.logging import RichHandler

logger = logging.getLogger(__name__)


def erase_odrive():
    print("finding an ODrive...")
    odrv0 = odrive.find_any(timeout=5)
    if odrv0 is None:
        print("Error: ODrive not found. Check connections.", file=stderr)
        exit(1)
    dump_errors(odrv0)
    sleep(5.0)
    # clear config

    try:
        odrv0.erase_configuration()
    except:
        pass  # fails because drive restarts
    print("Odrive cleared, waiting 2 secs for it to start")
    sleep(2.0)

    print("finding an ODrive...")
    odrv0 = odrive.find_any(timeout=5)
    dump_errors(odrv0)

    print("save erased ODrive")
    try:
        odrv0.save_configuration()
    except:
        pass  # fails because drive restarts
    print("Odrive saved, waiting 2 secs for it to start")
    sleep(2.0)

    print("finding an ODrive...")
    odrv0 = odrive.find_any(timeout=5)
    dump_errors(odrv0)
    print("config erased")


def configure_and_restart_odrive():
    odrv0 = odrive.find_any(timeout=5)

    # ODrive config
    odrv0.config.gpio5_mode = GpioMode.DIGITAL
    odrv0.config.dc_bus_overvoltage_trip_level = 28
    odrv0.config.dc_max_positive_current = 5
    odrv0.config.dc_max_negative_current = -1

    # axis0 End-stop config
    odrv0.axis0.min_endstop.config.enabled = False
    odrv0.axis0.min_endstop.config.gpio_num = 5
    odrv0.axis0.min_endstop.config.offset = -5
    odrv0.axis0.min_endstop.config.debounce_ms = 500
    odrv0.axis0.min_endstop.config.is_active_high = False  # this may need to be true

    # axis0 config
    odrv0.axis0.config.load_encoder = EncoderId.ONBOARD_ENCODER0
    odrv0.axis0.config.commutation_encoder = EncoderId.ONBOARD_ENCODER0
    odrv0.axis0.config.motor.motor_type = MotorType.HIGH_CURRENT
    odrv0.axis0.config.motor.pole_pairs = 7
    odrv0.axis0.config.motor.calibration_current = 3
    odrv0.axis0.config.motor.torque_constant = 8.27 / 96
    odrv0.axis0.config.motor.resistance_calib_max_voltage = 3
    odrv0.axis0.config.motor.current_soft_max = 10
    odrv0.axis0.config.motor.current_hard_max = 12
    odrv0.axis0.config.calibration_lockin.current = 3
    odrv0.axis0.config.startup_homing = False  # MAke true when needed
    # odrv0.axis0.controller.config.control_mode = AxisState.CLOSED_LOOP_CONTROL  # doc said issues will arise if
    # calling this before calib

    # axis0 controller config
    # Enable homing reference frame Read in the docs that this is needed
    # odrv0.axis0.controller.config.absolute_setpoints = True
    odrv0.axis0.controller.config.pos_gain = 10
    odrv0.axis0.controller.config.vel_gain = 0.04
    odrv0.axis0.controller.config.vel_integrator_gain = 0.2
    odrv0.axis0.controller.config.vel_limit = 30  # from 30 to 10
    odrv0.axis0.controller.config.vel_limit_tolerance = 1.2
    odrv0.axis0.controller.config.vel_ramp_rate = 30  # from 30 to 10
    odrv0.axis0.controller.config.input_filter_bandwidth = 20
    odrv0.axis0.controller.config.homing_speed = 0.75
    # odrv0.axis0.controller.config.vel_limit = 10

    # Enable homing on GPIO5
    # odrv0.axis0.pos_vel_mapper.config.index_gpio = 5
    # odrv0.axis0.pos_vel_mapper.config.use_index_gpio = True
    # odrv0.axis0.pos_vel_mapper.config.index_offset = 0
    # odrv0.axis0.pos_vel_mapper.config.index_offset_valid = True
    print("config setup")
    try:
        odrv0.save_configuration()
    except:
        pass  # fails because drive restarts
    print("config saved, waiting 2 secs for it to start")
    sleep(5.0)

    print("finding an odrive...")
    odrv0 = odrive.find_any(timeout=5)
    dump_errors(odrv0)
    logger.info("Configuration complete.")


def calibrate_motor(odrv0):
    logger.info("calibrating")
    odrv0.axis0.requested_state = AxisState.FULL_CALIBRATION_SEQUENCE  # may be offset calibration and not full sequence
    sleep(0.1)
    while odrv0.axis0.current_state != AxisState.IDLE:
        sleep(0.2)
        logger.debug("current state: %s", repr(AxisState(odrv0.axis0.current_state)))
        # dump_errors(odrv0)
    dump_errors(odrv0)
    sleep(5.0)
    print("Saving calibration")
    try:
        odrv0.save_configuration()
    except:
        pass  # fails because drive restarts
    print("Calibration saved, waiting 2 secs for it to start")
    sleep(2.0)

    print("finding an odrive...")
    odrv0 = odrive.find_any(timeout=5)
    dump_errors(odrv0)
    logger.info("Calibration complete.")

    logger.info("successfully calibrated")


def test_hall_sensor(odrv0):
    logger.info("entering testing range")
    dump_errors(odrv0)
    odrv0.axis0.controller.config.control_mode = ControlMode.POSITION_CONTROL
    odrv0.axis0.requested_state = AxisState.CLOSED_LOOP_CONTROL
    sleep(0.1)
    logger.debug("current state: %s", repr(AxisState(odrv0.axis0.current_state)))
    if odrv0.axis0.current_state != AxisState.CLOSED_LOOP_CONTROL:
        dump_errors(odrv0)
        exit(1)
    while True:
        odrv0.axis0.controller.input_pos = 20
        for _ in range(5):
            logger.info(f"State: {odrv0.get_gpio_states()}")
            sleep(1)
        odrv0.axis0.controller.input_pos = -20
        for _ in range(5):
            logger.info(f"State: {odrv0.get_gpio_states()}")
            sleep(1)
        logger.info(f"pos_estimate: {odrv0.axis0.pos_estimate}")
        dump_errors(odrv0)


def home_motor(odrv0):
    logger.info("homing motor to endstop")
    odrv0.axis0.controller.config.homing_speed = 2
    odrv0.axis0.requested_state = AxisState.HOMING
    sleep(5)
    while not odrv0.axis0.is_homed:
        logger.info(f"State: {odrv0.get_gpio_states()}")
        sleep(1)

    logger.info("successfully homed")


def main():
    logging.basicConfig(level=logging.DEBUG, format="%(levelname)s\t%(name)s\t%(message)s", datefmt="[%X]")
    logger.info("finding an odrv")

    erase_odrive()

    configure_and_restart_odrive()

    odrv0 = odrive.find_any(timeout=5)

    calibrate_motor(odrv0)

    odrv0 = odrive.find_any(timeout=5)

    test_hall_sensor(odrv0)  # won't come back

    # home_motor(odrv0)

    # odrv0.save_configuration()

    dump_errors(odrv0)


if __name__ == "__main__":
    main()

The output for this script currently is this:

INFO	__main__	finding an odrv
DEBUG	asyncio	Using proactor: IocpProactor
finding an ODrive...
axis0
  active_errors: no error
  disarm_reason: Error(s):
    ODriveError.CURRENT_LIMIT_VIOLATION
  procedure_result: ProcedureResult.DISARMED
  last_drv_fault: none
internal issues: none
Odrive cleared, waiting 2 secs for it to start
finding an ODrive...
axis0
  active_errors: no error
  disarm_reason: no error
  procedure_result: ProcedureResult.SUCCESS
  last_drv_fault: none
internal issues: none
save erased ODrive
Odrive saved, waiting 2 secs for it to start
finding an ODrive...
axis0
  active_errors: no error
  disarm_reason: no error
  procedure_result: ProcedureResult.SUCCESS
  last_drv_fault: none
internal issues: none
config erased
config setup
config saved, waiting 2 secs for it to start
finding an odrive...
INFO	__main__	Configuration complete.
INFO	__main__	calibrating
axis0
  active_errors: no error
  disarm_reason: no error
  procedure_result: ProcedureResult.SUCCESS
  last_drv_fault: none
internal issues: none
DEBUG	__main__	current state: <AxisState.MOTOR_CALIBRATION: 4>
DEBUG	__main__	current state: <AxisState.MOTOR_CALIBRATION: 4>
DEBUG	__main__	current state: <AxisState.MOTOR_CALIBRATION: 4>
DEBUG	__main__	current state: <AxisState.MOTOR_CALIBRATION: 4>
DEBUG	__main__	current state: <AxisState.MOTOR_CALIBRATION: 4>
DEBUG	__main__	current state: <AxisState.MOTOR_CALIBRATION: 4>
DEBUG	__main__	current state: <AxisState.MOTOR_CALIBRATION: 4>
DEBUG	__main__	current state: <AxisState.MOTOR_CALIBRATION: 4>
DEBUG	__main__	current state: <AxisState.MOTOR_CALIBRATION: 4>
DEBUG	__main__	current state: <AxisState.MOTOR_CALIBRATION: 4>
DEBUG	__main__	current state: <AxisState.MOTOR_CALIBRATION: 4>
DEBUG	__main__	current state: <AxisState.MOTOR_CALIBRATION: 4>
DEBUG	__main__	current state: <AxisState.MOTOR_CALIBRATION: 4>
DEBUG	__main__	current state: <AxisState.MOTOR_CALIBRATION: 4>
DEBUG	__main__	current state: <AxisState.MOTOR_CALIBRATION: 4>
DEBUG	__main__	current state: <AxisState.MOTOR_CALIBRATION: 4>
DEBUG	__main__	current state: <AxisState.MOTOR_CALIBRATION: 4>
DEBUG	__main__	current state: <AxisState.MOTOR_CALIBRATION: 4>
DEBUG	__main__	current state: <AxisState.MOTOR_CALIBRATION: 4>
DEBUG	__main__	current state: <AxisState.ENCODER_OFFSET_CALIBRATION: 7>
DEBUG	__main__	current state: <AxisState.ENCODER_OFFSET_CALIBRATION: 7>
DEBUG	__main__	current state: <AxisState.ENCODER_OFFSET_CALIBRATION: 7>
DEBUG	__main__	current state: <AxisState.ENCODER_OFFSET_CALIBRATION: 7>
DEBUG	__main__	current state: <AxisState.ENCODER_OFFSET_CALIBRATION: 7>
DEBUG	__main__	current state: <AxisState.ENCODER_OFFSET_CALIBRATION: 7>
DEBUG	__main__	current state: <AxisState.ENCODER_OFFSET_CALIBRATION: 7>
DEBUG	__main__	current state: <AxisState.ENCODER_OFFSET_CALIBRATION: 7>
DEBUG	__main__	current state: <AxisState.ENCODER_OFFSET_CALIBRATION: 7>
DEBUG	__main__	current state: <AxisState.ENCODER_OFFSET_CALIBRATION: 7>
DEBUG	__main__	current state: <AxisState.ENCODER_OFFSET_CALIBRATION: 7>
DEBUG	__main__	current state: <AxisState.ENCODER_OFFSET_CALIBRATION: 7>
DEBUG	__main__	current state: <AxisState.ENCODER_OFFSET_CALIBRATION: 7>
DEBUG	__main__	current state: <AxisState.ENCODER_OFFSET_CALIBRATION: 7>
DEBUG	__main__	current state: <AxisState.ENCODER_OFFSET_CALIBRATION: 7>
DEBUG	__main__	current state: <AxisState.ENCODER_OFFSET_CALIBRATION: 7>
DEBUG	__main__	current state: <AxisState.ENCODER_OFFSET_CALIBRATION: 7>
DEBUG	__main__	current state: <AxisState.ENCODER_OFFSET_CALIBRATION: 7>
DEBUG	__main__	current state: <AxisState.ENCODER_OFFSET_CALIBRATION: 7>
DEBUG	__main__	current state: <AxisState.ENCODER_OFFSET_CALIBRATION: 7>
DEBUG	__main__	current state: <AxisState.ENCODER_OFFSET_CALIBRATION: 7>
DEBUG	__main__	current state: <AxisState.ENCODER_OFFSET_CALIBRATION: 7>
DEBUG	__main__	current state: <AxisState.ENCODER_OFFSET_CALIBRATION: 7>
DEBUG	__main__	current state: <AxisState.ENCODER_OFFSET_CALIBRATION: 7>
DEBUG	__main__	current state: <AxisState.ENCODER_OFFSET_CALIBRATION: 7>
DEBUG	__main__	current state: <AxisState.ENCODER_OFFSET_CALIBRATION: 7>
DEBUG	__main__	current state: <AxisState.ENCODER_OFFSET_CALIBRATION: 7>
DEBUG	__main__	current state: <AxisState.ENCODER_OFFSET_CALIBRATION: 7>
DEBUG	__main__	current state: <AxisState.ENCODER_OFFSET_CALIBRATION: 7>
DEBUG	__main__	current state: <AxisState.ENCODER_OFFSET_CALIBRATION: 7>
DEBUG	__main__	current state: <AxisState.ENCODER_OFFSET_CALIBRATION: 7>
DEBUG	__main__	current state: <AxisState.ENCODER_OFFSET_CALIBRATION: 7>
DEBUG	__main__	current state: <AxisState.ENCODER_OFFSET_CALIBRATION: 7>
DEBUG	__main__	current state: <AxisState.ENCODER_OFFSET_CALIBRATION: 7>
DEBUG	__main__	current state: <AxisState.ENCODER_OFFSET_CALIBRATION: 7>
DEBUG	__main__	current state: <AxisState.ENCODER_OFFSET_CALIBRATION: 7>
DEBUG	__main__	current state: <AxisState.ENCODER_OFFSET_CALIBRATION: 7>
DEBUG	__main__	current state: <AxisState.ENCODER_OFFSET_CALIBRATION: 7>
DEBUG	__main__	current state: <AxisState.ENCODER_OFFSET_CALIBRATION: 7>
DEBUG	__main__	current state: <AxisState.ENCODER_OFFSET_CALIBRATION: 7>
DEBUG	__main__	current state: <AxisState.ENCODER_OFFSET_CALIBRATION: 7>
DEBUG	__main__	current state: <AxisState.ENCODER_OFFSET_CALIBRATION: 7>
DEBUG	__main__	current state: <AxisState.ENCODER_OFFSET_CALIBRATION: 7>
DEBUG	__main__	current state: <AxisState.IDLE: 1>
axis0
  active_errors: no error
  disarm_reason: no error
  procedure_result: ProcedureResult.SUCCESS
  last_drv_fault: none
internal issues: none
Saving calibration
Calibration saved, waiting 2 secs for it to start
finding an odrive...
axis0
  active_errors: no error
  disarm_reason: no error
  procedure_result: ProcedureResult.SUCCESS
  last_drv_fault: none
internal issues: none
INFO	__main__	Calibration complete.
INFO	__main__	successfully calibrated
INFO	__main__	entering testing range
axis0
  active_errors: no error
  disarm_reason: no error
  procedure_result: ProcedureResult.SUCCESS
  last_drv_fault: none
internal issues: none
DEBUG	__main__	current state: <AxisState.CLOSED_LOOP_CONTROL: 8>
INFO	__main__	State: 33
INFO	__main__	State: 33
INFO	__main__	State: 33
INFO	__main__	State: 33
INFO	__main__	State: 33
INFO	__main__	State: 33
INFO	__main__	State: 33
INFO	__main__	State: 33
INFO	__main__	State: 33
INFO	__main__	State: 33
INFO	__main__	pos_estimate: 0.2422010898590088
axis0
  active_errors: no error
  disarm_reason: Error(s):
    ODriveError.CURRENT_LIMIT_VIOLATION
  procedure_result: ProcedureResult.DISARMED
  last_drv_fault: none
internal issues: none
INFO	__main__	State: 33
INFO	__main__	State: 33
INFO	__main__	State: 33
INFO	__main__	State: 33
INFO	__main__	State: 33
INFO	__main__	State: 33
INFO	__main__	State: 33
INFO	__main__	State: 33
INFO	__main__	State: 33
INFO	__main__	State: 33
INFO	__main__	pos_estimate: 0.24220973253250122```