How to Control BL Drill?

Hello,

I’m new to ODrive, and I was hoping to have it control the drive system of the RC UGV I’m building, which is powered by two 20V cordless drills.

I have the drill’s ESC which obviously runs the motor perfectly fine, but my issue is that I want to run the motor at a slower speed than what the ESC can run it (the slowest speed the ESC allows is ~150rpm before it cuts out to 0; I want at least 20-40rpm as the slowest speed). I was hoping to achieve a ‘creep’ speed function utilizing an ODrive V3.6-56v board.

I have no information on the motor, but I measured the resistance between each phase and it 2.4 ohms exactly between all phases.

I did run some test codes, had my motor successfully ‘beep’ at me, but it doesn’t move at all (Although I have had the motor move a tik (I think it moved the next pole. It moved very little) when running the code, but when I re-ran it, it beeped but didn’t move. I even tried manually spinning the motor with no luck. (I see the amperage go up on my power supply before the motor beeps; The highest being ~1.5A then dropping down to ~0.87A after the motor beeps then drops to 0A)

I’m completely lost and any help on getting my motor running with this board would be greatly appreciated.

Thanks in advance.

The drill I am using

Drill Motor:


Wiring Setup:


Output from the code I used (originally I was using 20V but I bumped it to 24v because I heard that’s the operating voltage the ODrive board likes):

=== ODrive Sensorless Motor Setup (FW 0.5.1) ===
Searching for ODrive...
Found ODrive!
Firmware version: 0.5.1
Erasing existing configuration...
Expected disconnection: Device 355434653034 disconnected
Waiting for ODrive to reboot...
Reconnecting to ODrive...
Reconnected successfully!
Configuring for FW 0.5.1...
Configuring motor...
Motor configuration successful
Configuring sensorless control...
Encoder set to NONE mode for sensorless
Controller configuration successful
Saving configuration...
Configuration saved successfully!
Reconnecting after save...
Reconnected after save!
Starting motor calibration...
Calibrating... State: 4
Calibrating... State: 4
Calibrating... State: 4
Calibrating... State: 4
Calibrating... State: 4
Calibrating... State: 4
Calibrating... State: 4
Calibrating... State: 4
Calibrating... State: 4
Calibrating... State: 4
Calibrating... State: 4
Calibrating... State: 4
Calibrating... State: 4
Calibrating... State: 4
Calibrating... State: 4
Calibrating... State: 4
Calibrating... State: 4
Calibrating... State: 4
Calibrating... State: 4
Calibrating... State: 4
Calibrating... State: 4
Calibrating... State: 4
Calibrating... State: 4
Calibrating... State: 4
Calibrating... State: 4
Calibrating... State: 4
Calibrating... State: 4
Calibrating... State: 4
Calibrating... State: 4
Calibrating... State: 4
Calibrating... State: 4
Calibrating... State: 4
Calibrating... State: 4
Calibrating... State: 4
Calibrating... State: 4
Calibrating... State: 4
Calibrating... State: 4
Calibrating... State: 4
Calibrating... State: 1
Motor calibration complete!
Phase resistance: 0.101556 Ohms
Phase inductance: 0.000045 H
Starting sensorless control...
Trying alternative control state...
Sensorless control active! State: 1

=== Starting Motor Demo ===
Current state: IDLE
Velocity: 0.00 RPS
Motor current: 0.00 A
Bus voltage: 24.19 V
AXIS ERROR: 1
Attempting to start motor at 3 RPS (180 RPM)...
Motor not in closed loop! Current state: 1
Starting sensorless control...
Trying alternative control state...
Sensorless control active! State: 1
Velocity set to 3.0 RPS (180.0 RPM)
Current state: 1, Actual vel: 0.00 RPS
Current state: IDLE
Velocity: 0.00 RPS
Motor current: 0.00 A
Bus voltage: 24.19 V
AXIS ERROR: 1
Motor not spinning. Trying higher velocity...
Motor not in closed loop! Current state: 1
Starting sensorless control...
Trying alternative control state...
Sensorless control active! State: 1
Velocity set to 8.0 RPS (480.0 RPM)
Current state: 1, Actual vel: 0.00 RPS
Current state: IDLE
Velocity: 0.00 RPS
Motor current: 0.00 A
Bus voltage: 24.25 V
AXIS ERROR: 1
Motor still not spinning. Trying 15 RPS...
Motor not in closed loop! Current state: 1
Starting sensorless control...
Trying alternative control state...
Motor stopped

Program interrupted by user

One of the codes I used (the output is posted above):

import odrive
from odrive.enums import *
import time
import sys

def setup_odrive_sensorless():
    print("Searching for ODrive...")
    
    # Connect to ODrive
    try:
        odrv0 = odrive.find_any(timeout=10)
        print("Found ODrive!")
        print(f"Firmware version: {odrv0.fw_version_major}.{odrv0.fw_version_minor}.{odrv0.fw_version_revision}")
    except:
        print("No ODrive found. Please check USB connection and try again.")
        return None

    # Erase configuration and handle disconnection
    print("Erasing existing configuration...")
    try:
        odrv0.erase_configuration()
        print("Configuration erased. ODrive will reboot...")
    except Exception as e:
        print(f"Expected disconnection: {e}")
    
    # Wait for reboot and reconnect
    print("Waiting for ODrive to reboot...")
    time.sleep(5)
    
    print("Reconnecting to ODrive...")
    for attempt in range(5):
        try:
            odrv0 = odrive.find_any(timeout=10)
            print("Reconnected successfully!")
            break
        except:
            print(f"Reconnection attempt {attempt + 1}/5 failed, retrying...")
            time.sleep(2)
    else:
        print("Failed to reconnect to ODrive. Please unplug and replug USB, then run script again.")
        return None

    # For Firmware 0.5.1 - use correct configuration paths
    print("Configuring for FW 0.5.1...")
    
    # Configure motor parameters
    print("Configuring motor...")
    try:
        # In FW 0.5.1, motor type might be configured differently
        odrv0.axis0.motor.config.pole_pairs = 7  # Common for drill motors
        
        # Current limits
        odrv0.axis0.motor.config.current_lim = 25.0  # Increased for drill motor
        odrv0.axis0.motor.config.calibration_current = 15.0
        odrv0.axis0.motor.config.resistance_calib_max_voltage = 8.0  # Increased for better calibration
        
        print("Motor configuration successful")
    except Exception as e:
        print(f"Motor configuration warning: {e}")

    # For sensorless in FW 0.5.1, we need to use encoderless control
    print("Configuring sensorless control...")
    try:
        # In older firmware, sensorless might be the default when no encoder is configured
        odrv0.axis0.encoder.config.mode = 0  # ENCODER_MODE_NONE
        print("Encoder set to NONE mode for sensorless")
    except Exception as e:
        print(f"Encoder configuration warning: {e}")

    # Controller configuration
    try:
        odrv0.axis0.controller.config.control_mode = 2  # VELOCITY_CONTROL = 2
        odrv0.axis0.controller.config.vel_limit = 20.0  # Increased limit
        odrv0.axis0.controller.config.vel_gain = 0.2
        odrv0.axis0.controller.config.vel_integrator_gain = 0.4
        print("Controller configuration successful")
    except Exception as e:
        print(f"Controller configuration warning: {e}")

    # Save configuration
    print("Saving configuration...")
    try:
        odrv0.save_configuration()
        print("Configuration saved successfully!")
    except Exception as e:
        print(f"Save configuration warning: {e}")

    # Reconnect after save
    print("Reconnecting after save...")
    time.sleep(2)
    
    try:
        odrv0 = odrive.find_any(timeout=10)
        print("Reconnected after save!")
    except:
        print("Reconnecting after save...")
        try:
            odrv0 = odrive.find_any(timeout=10)
            print("Reconnected!")
        except:
            print("Could not reconnect automatically. Please restart script.")
            return None

    return odrv0

def calibrate_motor(odrv0):
    """Calibrate the motor"""
    if odrv0 is None:
        return False
        
    print("Starting motor calibration...")
    try:
        odrv0.axis0.requested_state = 4  # AXIS_STATE_MOTOR_CALIBRATION = 4
        
        # Wait for calibration to complete
        start_time = time.time()
        while odrv0.axis0.current_state != 1:  # AXIS_STATE_IDLE = 1
            if time.time() - start_time > 15:  # Increased timeout
                print("Calibration timeout!")
                # Try to clear errors
                try:
                    odrv0.axis0.error = 0
                except:
                    pass
                return False
            time.sleep(0.1)
            print(f"Calibrating... State: {odrv0.axis0.current_state}")
            
        print("Motor calibration complete!")
        try:
            print(f"Phase resistance: {odrv0.axis0.motor.config.phase_resistance:.6f} Ohms")
            print(f"Phase inductance: {odrv0.axis0.motor.config.phase_inductance:.6f} H")
        except:
            print("Motor calibration values available")
        return True
        
    except Exception as e:
        print(f"Calibration failed: {e}")
        return False

def start_sensorless_control(odrv0):
    """Start sensorless control mode"""
    if odrv0 is None:
        return False
        
    print("Starting sensorless control...")
    try:
        # For FW 0.5.1, sensorless control might be state 5 or 8
        # Try both if needed
        odrv0.axis0.requested_state = 8  # AXIS_STATE_CLOSED_LOOP_CONTROL = 8
        
        # Check if it worked
        time.sleep(0.5)
        if odrv0.axis0.current_state != 8:
            print("Trying alternative control state...")
            odrv0.axis0.requested_state = 5  # AXIS_STATE_SENSORLESS_CONTROL = 5
            time.sleep(0.5)
        
        print(f"Sensorless control active! State: {odrv0.axis0.current_state}")
        return True
    except Exception as e:
        print(f"Failed to start sensorless control: {e}")
        return False

# Control functions
def set_velocity(odrv0, vel_rps):
    """Set motor velocity in revolutions per second (RPS)"""
    if odrv0 is None:
        print("No ODrive connected!")
        return
    try:
        # For sensorless, we might need to ensure we're in the right state
        if odrv0.axis0.current_state != 8 and odrv0.axis0.current_state != 5:
            print(f"Motor not in closed loop! Current state: {odrv0.axis0.current_state}")
            start_sensorless_control(odrv0)
            time.sleep(0.5)
        
        odrv0.axis0.controller.input_vel = vel_rps
        print(f"Velocity set to {vel_rps} RPS ({vel_rps * 60:.1f} RPM)")
        print(f"Current state: {odrv0.axis0.current_state}, Actual vel: {odrv0.axis0.encoder.vel_estimate:.2f} RPS")
    except Exception as e:
        print(f"Error setting velocity: {e}")

def stop_motor(odrv0):
    """Stop the motor"""
    if odrv0 is None:
        return
    try:
        odrv0.axis0.controller.input_vel = 0
        print("Motor stopped")
    except Exception as e:
        print(f"Error stopping motor: {e}")

def check_status(odrv0):
    """Check ODrive status"""
    if odrv0 is None:
        print("No ODrive connected!")
        return
        
    try:
        state_names = {
            0: "UNDEFINED", 1: "IDLE", 2: "STARTUP_SEQUENCE", 3: "FULL_CALIBRATION_SEQUENCE",
            4: "MOTOR_CALIBRATION", 5: "SENSORLESS_CONTROL", 6: "ENCODER_INDEX_SEARCH",
            7: "ENCODER_OFFSET_CALIBRATION", 8: "CLOSED_LOOP_CONTROL"
        }
        state = odrv0.axis0.current_state
        state_name = state_names.get(state, f"UNKNOWN({state})")
        
        print(f"Current state: {state_name}")
        print(f"Velocity: {odrv0.axis0.encoder.vel_estimate:.2f} RPS")
        print(f"Motor current: {odrv0.axis0.motor.current_control.Iq_measured:.2f} A")
        print(f"Bus voltage: {odrv0.vbus_voltage:.2f} V")
        
        # Check for errors
        if odrv0.axis0.error != 0:
            print(f"AXIS ERROR: {odrv0.axis0.error}")
        if odrv0.axis0.motor.error != 0:
            print(f"MOTOR ERROR: {odrv0.axis0.motor.error}")
    except Exception as e:
        print(f"Error reading status: {e}")

# Main execution
if __name__ == "__main__":
    print("=== ODrive Sensorless Motor Setup (FW 0.5.1) ===")
    
    # Setup ODrive
    odrv0 = setup_odrive_sensorless()
    
    if odrv0 is None:
        print("Setup failed. Exiting.")
        sys.exit(1)
    
    # Calibrate motor
    if not calibrate_motor(odrv0):
        print("Calibration failed. Exiting.")
        sys.exit(1)
    
    # Start sensorless control
    if not start_sensorless_control(odrv0):
        print("Failed to start sensorless control. Exiting.")
        sys.exit(1)
    
    # Demo sequence
    print("\n=== Starting Motor Demo ===")
    try:
        # Check status first
        check_status(odrv0)
        
        # Start slow - try different speeds
        print("Attempting to start motor at 3 RPS (180 RPM)...")
        set_velocity(odrv0, 3.0)
        time.sleep(2)
        
        # Check if it's actually moving
        check_status(odrv0)
        
        if abs(odrv0.axis0.encoder.vel_estimate) < 0.1:
            print("Motor not spinning. Trying higher velocity...")
            set_velocity(odrv0, 8.0)
            time.sleep(2)
            check_status(odrv0)
        
        # Try even higher if still not moving
        if abs(odrv0.axis0.encoder.vel_estimate) < 0.1:
            print("Motor still not spinning. Trying 15 RPS...")
            set_velocity(odrv0, 15.0)
            time.sleep(2)
            check_status(odrv0)
        
        # Stop motor
        stop_motor(odrv0)
        
        print("\n=== Demo Complete ===")
        print("Use these commands for manual control:")
        print("  set_velocity(odrv0, speed_rps)")
        print("  stop_motor(odrv0)")
        print("  check_status(odrv0)")
        
        # Interactive control
        while True:
            cmd = input("\nEnter command (v [rps], s, c, q): ").strip().lower()
            if cmd.startswith('v'):
                try:
                    speed = float(cmd[1:].strip())
                    set_velocity(odrv0, speed)
                except ValueError:
                    print("Invalid speed. Use 'v5.0' for 5 RPS")
            elif cmd == 's':
                stop_motor(odrv0)
            elif cmd == 'c':
                check_status(odrv0)
            elif cmd == 'q':
                stop_motor(odrv0)
                print("Exiting.")
                break
            else:
                print("Commands: v[speed], s=stop, c=check, q=quit")
                
    except KeyboardInterrupt:
        stop_motor(odrv0)
        print("\nProgram interrupted by user")

If you’re looking to run that motor in sensorless mode, the ODrive isn’t going to do much better than the drill controller. If you want control at low speeds, you’ll need to add an encoder.

Additionally, I’d note that LLMs do a really bad job with ODrive parameters – a lot of those API calls are just incorrect. I’d recommend reading the docs, and upgrading the firmware on your board (v0.5.1 is old, even for the v3.6).

Thanks for your response, I really appreciate it.

I do agree that LLMs do a terrible job, but I figured I’d give it a try just to see what happens.

I managed to upgrade the firmware to v0.5.6. & I’m reading through the docs now.

So how would an encoder help the ODrive start/run the motor? Especially since this motor has such a high resistance compared to other motors used in examples and youtube tutorials. Also, does it have to be a certain encoder or can it be any rotary encoder?

Thanks again.

No worries, glad to help!

So how would an encoder help the ODrive start/run the motor?

Some sort of position sensing is required for proper control of brushless motors. There are various “sensorless” methodologies to avoid this requirement, but they nearly all require some minimum speed of the motor – usually a sensorless controller (like a drone ESC, your drill’s internal ESC, or the ODrive in sensorless mode) will send a series of pulses to the motor to “drag” the rotor around (sort of like a stepper motor – very different methodology than actual proper BLDC control) until it’s at the minimum speed feasible for sensorless control, then it’ll kick in the actual control algorithm.

However, if you have an encoder, all this is null and void – the position sensing that the encoder gives allows for 100% torque output at zero speed. Plus, way more precise torque/speed/position control than any sort of sensorless scheme.

Also, does it have to be a certain encoder or can it be any rotary encoder?

You can see supported encoders here: ODrive encoder guide - Google Sheets

Note that quadrature (“incremental”) encoders require an encoder calibration at each startup, which can only be done with no (or very little) load on the motor. If the encoder has an index line, you only have to do the encoder calibration once (e.g. before you put the motor on a gearbox) and as long as you don’t shift the relative rotation of the encoder and motor (they need to stay coupled), future startups of the motor only require up to a single rotation to restore calibration. Absolute encoders only require one-time calibration, but usually communicate over a digital communications protocol like SPI, which the v3.6 has a lot of issues with, so I wouldn’t recommend that (ODrive Pro/S1/Micro are much more reliable and performant overall). More info here: Encoders — ODrive Documentation 0.5.6 documentation

Especially since this motor has such a high resistance

Shouldn’t super matter! Careful to set the current limit correctly so the motor doesn’t get too hot. But the ODrive auto-calibrates the motor’s resistance/inductance.

Thanks. I will try a compatible encoder and see how it goes.

Best of luck, let me know if I can help!