ODrive pro max current control

Hello everyone,

For my master thesis I need to control this motor: Grin Front Max45 to power my bike. Its main parameters are as follows:

  • Max power = 4.1 kW (5 min)
  • Max torque = 120 Nm (1min)
  • Kv = 8.9 rpm/V (Kt = 0.929 Nm/A) => 120Nm gives iq = 129A

To power the system, we’re using a 48V (13S) Li-ion battery capable of delivering 70A peak and 40A continuous. We also want to limit the power output to 3 kW (which is still quite good, even for a heavy [150+ kg] bike). Everything is working great so far: GUI, ODrive tool, JSON config, and CAN communication with a Teensy.

To control the motor, we initially limited the motor current (motor.current_soft_max / motor.current_hard_max) to 70/100 A. However, with this limitation, we weren’t able to reach full torque at low speed. If we increase the limit, the battery gets overstressed at higher speeds, and we either get a DC_BUS_UNDER_VOLTAGE or DC_BUS_OVER_CURRENT error, depending on the battery’s SOC.

To make full torque available at low speed, we decided to use the active power limit feature of the ODrive:
config.I_bus_soft_max = 70 and config.P_bus_soft_max = 3000.
This works very well in our tests, and we’ve encountered fewer battery errors. (I saw that the team is interested in user feedback — if you’d like us to run specific tests, we’d be happy to do so.)

We know that the battery is undersized for this system, but our goal is to maximise the motor’s performance by fully leveraging all of ODrive’s features.

I set motor.current_soft_max to 129 A and motor.current_hard_max to 150 A, but in practice, it seems like I’m limited to 100 A. Could this be because inverter0.current_soft_max is set to 100 A? Is it possible to exceed the inverter soft max briefly (for short bursts), or not? The ODrive Pro documentation mentions 120 A peak, so I assumed it could go up to that, not just 100 A. A clarification about all theses values would help my I think. I aslo suppose that all motor current related parameters are expressed in the dq frame, am I right ?

I was also wondering if there’s already an active torque limit feature based on battery voltage — similar to the active power limit — to avoid low-voltage errors. For example, gradually decreasing the maximum torque from 120 Nm to 60 Nm as the battery SOC drops from 25% to 0%. We could implement this ourselves, but if such a feature already exists, it would be better handled directly by the ODrive than by us.

Here is our current configuration:

  "axis0.commutation_mapper.config.approx_init_pos": 0.0,
  "axis0.commutation_mapper.config.approx_init_pos_valid": false,
  "axis0.commutation_mapper.config.circular_output_range": 1.0,
  "axis0.commutation_mapper.config.circular": true,
  "axis0.commutation_mapper.config.index_gpio": 7,
  "axis0.commutation_mapper.config.index_offset": 0.0,
  "axis0.commutation_mapper.config.index_offset_valid": false,
  "axis0.commutation_mapper.config.offset": 0.0,
  "axis0.commutation_mapper.config.offset_valid": true,
  "axis0.commutation_mapper.config.passive_index_search": false,
  "axis0.commutation_mapper.config.scale": 23.0,
  "axis0.commutation_mapper.config.use_endstop": false,
  "axis0.commutation_mapper.config.use_index_gpio": false,
  "axis0.config.I_bus_hard_max": Infinity,
  "axis0.config.I_bus_hard_min": -Infinity,
  "axis0.config.I_bus_soft_max": 70.0,
  "axis0.config.I_bus_soft_min": -Infinity,
  "axis0.config.P_bus_soft_max": 3000.0,
  "axis0.config.P_bus_soft_min": -Infinity,
  "axis0.config.calib_range": 0.019999999552965164,
  "axis0.config.calib_scan_distance": 8.0,
  "axis0.config.calib_scan_vel": 2.0,
  "axis0.config.commutation_encoder_bandwidth": NaN,
  "axis0.config.commutation_encoder": 8,
  "axis0.config.dir_gpio_pin": 9,
  "axis0.config.enable_error_gpio": false,
  "axis0.config.enable_step_dir": false,
  "axis0.config.enable_watchdog": false,
  "axis0.config.encoder_bandwidth": 100.0,
  "axis0.config.error_gpio_pin": 10,
  "axis0.config.index_search_at_target_vel_only": false,
  "axis0.config.init_pos": NaN,
  "axis0.config.init_torque": 0.0,
  "axis0.config.init_vel": 0.0,
  "axis0.config.load_encoder": 8,
  "axis0.config.off_axis_k_commutation": 1.0,
  "axis0.config.off_axis_k": 1.0,
  "axis0.config.startup_closed_loop_control": false,
  "axis0.config.startup_encoder_index_search": false,
  "axis0.config.startup_encoder_offset_calibration": false,
  "axis0.config.startup_homing": false,
  "axis0.config.startup_max_wait_for_ready": 3.0,
  "axis0.config.startup_motor_calibration": false,
  "axis0.config.step_dir_always_on": false,
  "axis0.config.step_gpio_pin": 8,
  "axis0.config.torque_soft_max": 100.0,
  "axis0.config.torque_soft_min": -100.0,
  "axis0.config.watchdog_timeout": 0.0,
  "axis0.config.anticogging.calib_bidirectional": true,
  "axis0.config.anticogging.calib_coarse_integrator_gain": 10.0,
  "axis0.config.anticogging.calib_coarse_tuning_duration": 60.0,
  "axis0.config.anticogging.calib_end_vel": 0.15000000596046448,
  "axis0.config.anticogging.calib_fine_dist_scale": 1.0,
  "axis0.config.anticogging.calib_fine_tuning_duration": 120.0,
  "axis0.config.anticogging.calib_start_vel": 1.0,
  "axis0.config.anticogging.enabled": false,
  "axis0.config.anticogging.max_torque": 1.0,
  "axis0.config.calibration_lockin.accel": 3.183098793029785,
  "axis0.config.calibration_lockin.current": 10.0,
  "axis0.config.calibration_lockin.ramp_distance": 0.5,
  "axis0.config.calibration_lockin.ramp_time": 0.4000000059604645,
  "axis0.config.calibration_lockin.vel": 6.36619758605957,
  "axis0.config.can.bus_voltage_msg_rate_ms": 20,
  "axis0.config.can.encoder_msg_rate_ms": 1,
  "axis0.config.can.error_msg_rate_ms": 0,
  "axis0.config.can.heartbeat_msg_rate_ms": 100,
  "axis0.config.can.input_torque_scale": 1000,
  "axis0.config.can.input_vel_scale": 1000,
  "axis0.config.can.iq_msg_rate_ms": 4,
  "axis0.config.can.node_id": 0,
  "axis0.config.can.powers_msg_rate_ms": 0,
  "axis0.config.can.temperature_msg_rate_ms": 1000,
  "axis0.config.can.torques_msg_rate_ms": 4,
  "axis0.config.can.version_msg_rate_ms": 0,
  "axis0.config.general_lockin.accel": 20.0,
  "axis0.config.general_lockin.current": 10.0,
  "axis0.config.general_lockin.finish_distance": 100.0,
  "axis0.config.general_lockin.finish_on_distance": false,
  "axis0.config.general_lockin.finish_on_vel": false,
  "axis0.config.general_lockin.initial_pos": 0.0,
  "axis0.config.general_lockin.ramp_distance": 3.1415927410125732,
  "axis0.config.general_lockin.ramp_time": 0.4000000059604645,
  "axis0.config.general_lockin.vel": 40.0,
  "axis0.config.harmonic_compensation.calib_settling_delay": 2.0,
  "axis0.config.harmonic_compensation.calib_turns": 8,
  "axis0.config.harmonic_compensation.calib_vel": 8.0,
  "axis0.config.harmonic_compensation.cos2x_coef": 0.0,
  "axis0.config.harmonic_compensation.cosx_coef": 0.0,
  "axis0.config.harmonic_compensation.sin2x_coef": 0.0,
  "axis0.config.harmonic_compensation.sinx_coef": 0.0,
  "axis0.config.harmonic_compensation_commutation.calib_settling_delay": 2.0,
  "axis0.config.harmonic_compensation_commutation.calib_turns": 8,
  "axis0.config.harmonic_compensation_commutation.calib_vel": 8.0,
  "axis0.config.harmonic_compensation_commutation.cos2x_coef": 0.0,
  "axis0.config.harmonic_compensation_commutation.cosx_coef": 0.0,
  "axis0.config.harmonic_compensation_commutation.sin2x_coef": 0.0,
  "axis0.config.harmonic_compensation_commutation.sinx_coef": 0.0,
  "axis0.config.motor.acim_autoflux_attack_gain": 10.0,
  "axis0.config.motor.acim_autoflux_decay_gain": 1.0,
  "axis0.config.motor.acim_autoflux_enable": false,
  "axis0.config.motor.acim_autoflux_min_Id": 10.0,
  "axis0.config.motor.acim_gain_min_flux": 10.0,
  "axis0.config.motor.acim_nominal_slip_vel": 2.3399999141693115,
  "axis0.config.motor.bEMF_FF_enable": false,
  "axis0.config.motor.calibration_current": 10.0,
  "axis0.config.motor.current_control_bandwidth": 280.0,
  "axis0.config.motor.current_hard_max": 150.0,
  "axis0.config.motor.current_slew_rate_limit": 10000.0,
  "axis0.config.motor.current_soft_max": 129.0,
  "axis0.config.motor.dI_dt_FF_enable": false,
  "axis0.config.motor.direction": 1.0,
  "axis0.config.motor.ff_pm_flux_linkage": 0.0,
  "axis0.config.motor.ff_pm_flux_linkage_valid": false,
  "axis0.config.motor.fw_enable": false,
  "axis0.config.motor.fw_fb_bandwidth": 500.0,
  "axis0.config.motor.fw_mod_setpoint": 0.7786434292793274,
  "axis0.config.motor.motor_model_l_d": 0.0,
  "axis0.config.motor.motor_model_l_dq_valid": false,
  "axis0.config.motor.motor_model_l_q": 0.0,
  "axis0.config.motor.motor_type": 0,
  "axis0.config.motor.phase_inductance": 9.745523129822686e-05,
  "axis0.config.motor.phase_inductance_valid": true,
  "axis0.config.motor.phase_resistance": 0.06646940112113953,
  "axis0.config.motor.phase_resistance_valid": true,
  "axis0.config.motor.pole_pairs": 23,
  "axis0.config.motor.power_torque_report_filter_bandwidth": 8000.0,
  "axis0.config.motor.resistance_calib_max_voltage": 2.0,
  "axis0.config.motor.sensorless_observer_gain": 1000.0,
  "axis0.config.motor.sensorless_pll_bandwidth": 1000.0,
  "axis0.config.motor.sensorless_pm_flux_linkage": 0.0,
  "axis0.config.motor.sensorless_pm_flux_linkage_valid": false,
  "axis0.config.motor.torque_constant": 0.9292134642601013,
  "axis0.config.motor.wL_FF_enable": false,
  "axis0.config.sensorless_ramp.accel": 31.83098793029785,
  "axis0.config.sensorless_ramp.current": 10.0,
  "axis0.config.sensorless_ramp.finish_distance": 15.915493965148926,
  "axis0.config.sensorless_ramp.finish_on_distance": false,
  "axis0.config.sensorless_ramp.finish_on_vel": true,
  "axis0.config.sensorless_ramp.initial_pos": 0.0,
  "axis0.config.sensorless_ramp.ramp_distance": 0.5,
  "axis0.config.sensorless_ramp.ramp_time": 0.4000000059604645,
  "axis0.config.sensorless_ramp.vel": 63.6619758605957,
  "axis0.controller.config.absolute_setpoints": false,
  "axis0.controller.config.circular_setpoint_range": 1.0,
  "axis0.controller.config.circular_setpoints": false,
  "axis0.controller.config.commutation_vel_scale": 1.0,
  "axis0.controller.config.control_mode": 1,
  "axis0.controller.config.enable_gain_scheduling": false,
  "axis0.controller.config.enable_overspeed_error": true,
  "axis0.controller.config.enable_torque_mode_vel_limit": false,
  "axis0.controller.config.enable_vel_limit": true,
  "axis0.controller.config.gain_scheduling_width": 0.0010000000474974513,
  "axis0.controller.config.homing_speed": 0.25,
  "axis0.controller.config.inertia": 0.0,
  "axis0.controller.config.input_filter_bandwidth": 20.0,
  "axis0.controller.config.input_mode": 0,
  "axis0.controller.config.pos_gain": 20.0,
  "axis0.controller.config.spinout_electrical_power_bandwidth": 20.0,
  "axis0.controller.config.spinout_electrical_power_threshold": 100.0,
  "axis0.controller.config.spinout_mechanical_power_bandwidth": 20.0,
  "axis0.controller.config.spinout_mechanical_power_threshold": -100.0,
  "axis0.controller.config.steps_per_circular_range": 1024,
  "axis0.controller.config.torque_ramp_rate": 150.0,
  "axis0.controller.config.use_commutation_vel": false,
  "axis0.controller.config.use_load_encoder_for_commutation_vel": false,
  "axis0.controller.config.vel_gain": 10.0,
  "axis0.controller.config.vel_integrator_gain": 0.10000000149011612,
  "axis0.controller.config.vel_integrator_limit": Infinity,
  "axis0.controller.config.vel_limit": 10.0,
  "axis0.controller.config.vel_limit_tolerance": 1.25,
  "axis0.controller.config.vel_ramp_rate": 10.0,
  "axis0.enable_pin.config.debounce_ms": 50,
  "axis0.enable_pin.config.enabled": false,
  "axis0.enable_pin.config.gpio_num": 11,
  "axis0.enable_pin.config.is_active_high": false,
  "axis0.enable_pin.config.offset": 0.0,
  "axis0.interpolator.config.dynamic": true,
  "axis0.max_endstop.config.debounce_ms": 50,
  "axis0.max_endstop.config.enabled": false,
  "axis0.max_endstop.config.gpio_num": 0,
  "axis0.max_endstop.config.is_active_high": false,
  "axis0.max_endstop.config.offset": 0.0,
  "axis0.mechanical_brake.config.gpio_num": 0,
  "axis0.mechanical_brake.config.is_active_low": true,
  "axis0.min_endstop.config.debounce_ms": 50,
  "axis0.min_endstop.config.enabled": false,
  "axis0.min_endstop.config.gpio_num": 0,
  "axis0.min_endstop.config.is_active_high": false,
  "axis0.min_endstop.config.offset": 0.0,
  "axis0.motor.motor_thermistor.config.a": 3.9082999229431152,
  "axis0.motor.motor_thermistor.config.b": -0.0005774999735876918,
  "axis0.motor.motor_thermistor.config.beta": 3450.0,
  "axis0.motor.motor_thermistor.config.enabled": true,
  "axis0.motor.motor_thermistor.config.gpio_pin": 3,
  "axis0.motor.motor_thermistor.config.mode": 1,
  "axis0.motor.motor_thermistor.config.r_ref": 10000.0,
  "axis0.motor.motor_thermistor.config.t_ref": 25.0,
  "axis0.motor.motor_thermistor.config.temp_limit_lower": 60.0,
  "axis0.motor.motor_thermistor.config.temp_limit_upper": 80.0,
  "axis0.pos_vel_mapper.config.approx_init_pos": 0.0,
  "axis0.pos_vel_mapper.config.approx_init_pos_valid": false,
  "axis0.pos_vel_mapper.config.circular_output_range": 1.0,
  "axis0.pos_vel_mapper.config.circular": false,
  "axis0.pos_vel_mapper.config.index_gpio": 7,
  "axis0.pos_vel_mapper.config.index_offset": 0.0,
  "axis0.pos_vel_mapper.config.index_offset_valid": false,
  "axis0.pos_vel_mapper.config.offset": 0.0,
  "axis0.pos_vel_mapper.config.offset_valid": false,
  "axis0.pos_vel_mapper.config.passive_index_search": false,
  "axis0.pos_vel_mapper.config.scale": 1.0,
  "axis0.pos_vel_mapper.config.use_endstop": false,
  "axis0.pos_vel_mapper.config.use_index_gpio": false,
  "axis0.trap_traj.config.accel_limit": 60.0,
  "axis0.trap_traj.config.decel_limit": 0.5,
  "axis0.trap_traj.config.vel_limit": 2.0,
  "can.config.baud_rate": 250000,
  "can.config.data_baud_rate": 10000000,
  "can.config.protocol": 1,
  "can.config.tx_brs": 0,
  "config.dc_bus_overvoltage_trip_level": 55.0,
  "config.dc_bus_undervoltage_trip_level": 35.75,
  "config.dc_max_negative_current": -10.0,
  "config.dc_max_positive_current": 100.0,
  "config.enable_uart_a": false,
  "config.gpio0_mode": 17,
  "config.gpio10_mode": 17,
  "config.gpio11_mode": 17,
  "config.gpio12_mode": 17,
  "config.gpio13_mode": 17,
  "config.gpio14_mode": 17,
  "config.gpio15_mode": 17,
  "config.gpio16_mode": 17,
  "config.gpio17_mode": 17,
  "config.gpio18_mode": 17,
  "config.gpio1_mode": 17,
  "config.gpio2_mode": 17,
  "config.gpio3_mode": 17,
  "config.gpio4_mode": 17,
  "config.gpio5_mode": 17,
  "config.gpio6_mode": 17,
  "config.gpio7_mode": 17,
  "config.gpio8_mode": 17,
  "config.gpio9_mode": 17,
  "config.max_regen_current": 0.0,
  "config.uart0_protocol": 3,
  "config.uart_a_baudrate": 115200,
  "config.usb_cdc_protocol": 3,
  "config.user_config_0": 0,
  "config.user_config_1": 0,
  "config.user_config_2": 0,
  "config.user_config_3": 0,
  "config.user_config_4": 0,
  "config.user_config_5": 0,
  "config.user_config_6": 0,
  "config.user_config_7": 0,
  "config.gpio15_analog_mapping.endpoint": null,
  "config.gpio15_analog_mapping.max": 0.0,
  "config.gpio15_analog_mapping.min": 0.0,
  "config.gpio16_analog_mapping.endpoint": null,
  "config.gpio16_analog_mapping.max": 0.0,
  "config.gpio16_analog_mapping.min": 0.0,
  "config.gpio8_pwm_mapping.endpoint": null,
  "config.gpio8_pwm_mapping.max": 0.0,
  "config.gpio8_pwm_mapping.min": 0.0,
  "config.gpio9_pwm_mapping.endpoint": null,
  "config.gpio9_pwm_mapping.max": 0.0,
  "config.gpio9_pwm_mapping.min": 0.0,
  "config.inverter0.current_hard_max": 150.0,
  "config.inverter0.current_soft_max": 100.0,
  "config.inverter0.drv_config": 9029553772700800,
  "config.inverter0.mod_magn_max": 0.8651593923568726,
  "config.inverter0.shunt_conductance": 1999.9998779296875,
  "config.inverter0.temp_limit_lower": 83.95999908447266,
  "config.inverter0.temp_limit_upper": 103.11000061035156,
  "config.motor_fan.enabled": false,
  "config.motor_fan.lower": 70.0,
  "config.motor_fan.upper": 80.0,
  "config.odrv_fan.enabled": true,
  "config.odrv_fan.lower": 50.0,
  "config.odrv_fan.upper": 65.0,
  "hall_encoder0.config.edge0": 0.7169528603553772,
  "hall_encoder0.config.edge1": 0.8818782567977905,
  "hall_encoder0.config.edge2": 0.04617096483707428,
  "hall_encoder0.config.edge3": 0.22115886211395264,
  "hall_encoder0.config.edge4": 0.38211098313331604,
  "hall_encoder0.config.edge5": 0.5480634570121765,
  "hall_encoder0.config.edges_calibrated": true,
  "hall_encoder0.config.enabled": true,
  "hall_encoder0.config.hall_polarity_calibrated": true,
  "hall_encoder0.config.hall_polarity": 0,
  "hall_encoder0.config.ignore_illegal_hall_state": false,
  "hall_encoder1.config.edge0": 0.0,
  "hall_encoder1.config.edge1": 0.0,
  "hall_encoder1.config.edge2": 0.0,
  "hall_encoder1.config.edge3": 0.0,
  "hall_encoder1.config.edge4": 0.0,
  "hall_encoder1.config.edge5": 0.0,
  "hall_encoder1.config.edges_calibrated": false,
  "hall_encoder1.config.enabled": false,
  "hall_encoder1.config.hall_polarity_calibrated": false,
  "hall_encoder1.config.hall_polarity": 0,
  "hall_encoder1.config.ignore_illegal_hall_state": false,
  "inc_encoder0.config.cpr": 8192,
  "inc_encoder0.config.enabled": false,
  "inc_encoder0.config.filter": 0,
  "inc_encoder1.config.cpr": 8192,
  "inc_encoder1.config.enabled": false,
  "inc_encoder1.config.filter": 0,
  "rs485_encoder_group0.config.mode": 0,
  "rs485_encoder_group1.config.mode": 0,
  "spi_encoder0.config.baudrate": 1687500,
  "spi_encoder0.config.biss_c_bits": 18,
  "spi_encoder0.config.biss_c_multiturn_bits": 0,
  "spi_encoder0.config.delay": 0.0,
  "spi_encoder0.config.max_error_rate": 0.004999999888241291,
  "spi_encoder0.config.mode": 0,
  "spi_encoder0.config.ncs_gpio": 17,
  "spi_encoder1.config.baudrate": 1687500,
  "spi_encoder1.config.biss_c_bits": 18,
  "spi_encoder1.config.biss_c_multiturn_bits": 0,
  "spi_encoder1.config.delay": 0.0,
  "spi_encoder1.config.max_error_rate": 0.004999999888241291,
  "spi_encoder1.config.mode": 0,
  "spi_encoder1.config.ncs_gpio": 17

Thanks for you support and interest,
MOCmaniac

Hi! Cool project, thanks for all the detail.

Some notes:

To control the motor, we initially limited the motor current (motor.current_soft_max / motor.current_hard_max) to 70/100 A. However, with this limitation, we weren’t able to reach full torque at low speed. If we increase the limit, the battery gets overstressed at higher speeds, and we either get a DC_BUS_UNDER_VOLTAGE or DC_BUS_OVER_CURRENT error, depending on the battery’s SOC.

Makes sense.

To make full torque available at low speed, we decided to use the active power limit feature of the ODrive:
config.I_bus_soft_max = 70 and config.P_bus_soft_max = 3000.

Perfect, glad to hear that worked!

I set motor.current_soft_max to 129 A and motor.current_hard_max to 150 A, but in practice, it seems like I’m limited to 100 A. Could this be because inverter0.current_soft_max is set to 100 A? Is it possible to exceed the inverter soft max briefly (for short bursts), or not? The ODrive Pro documentation mentions 120 A peak, so I assumed it could go up to that, not just 100 A. A clarification about all theses values would help my I think. I aslo suppose that all motor current related parameters are expressed in the dq frame, am I right ?

It can do 120-150A but it takes some consideration, so it’s not unlocked by default. Shoot me an email at solomon.greenberg@odriverobotics.com and I’ll send you the unlock instructions. Note that you need some good cooling to push >120A – the heatspreader+heatsink+fan at an absolute minimum, something custom ideally.

I aslo suppose that all motor current related parameters are expressed in the dq frame, am I right ?

Correct

I was also wondering if there’s already an active torque limit feature based on battery voltage — similar to the active power limit — to avoid low-voltage errors. For example, gradually decreasing the maximum torque from 120 Nm to 60 Nm as the battery SOC drops from 25% to 0%. We could implement this ourselves, but if such a feature already exists, it would be better handled directly by the ODrive than by us.

Hmm, not at the moment. I think doing this externally is likely the best bet. In the future we’ll have user scripting support, so you could add this directly on the controller.

Thank you for your reply,

Since the ODrive is mounted on the bike, we chose to place it inside the dedicated enclosure. The heatsink fins are aligned with the direction of travel to allow for natural airflow, and the 12V fan is also installed, connected to the ODrive’s fan port and properly configured in the parameters. I’ll send you more details via email.

We’ll attempt to implement the active torque limit based on the battery’s state of charge ourselves, and if it proves useful to the community, we’d be happy to contribute!

Makes sense!