The motor controller is a cascaded style position, velocity and current control loop, as per the diagram below. When the control mode is set to position control, the whole loop runs. When running in velocity control mode, the position control part is removed and the velocity command is fed directly in to the second stage input. In torque control mode, only the current controller is used.


Cascaded position and velocity I loops

Each stage of the control loop is a variation on a PID controller. A PID controller is a mathematical model that can be adapted to control a wide variety of systems. This flexibility is essential as it allows the ODrive to be used to control all kinds of mechanical systems.


The controller has been updated to use torque in Newton-meters instead of current at the “system” level. There is a torque_constant parameter which converts between torque and current, after which the rest of this explanation still holds.

Position Control loop

The position controller is a P loop with a single proportional gain,

pos_error = pos_setpoint - pos_feedback,
  vel_cmd = pos_error * pos_gain + vel_feedforward.

Velocity Control Loop

The velocity controller is a PI loop where

        vel_error = vel_cmd - vel_feedback,
current_integral += vel_error * vel_integrator gain,
      current_cmd = vel_error * vel_gain + current_integral + current_feedforward.

Current Control Loop

The current controller is also a PI loop,

    current_error = current_cmd - current_feedback,
voltage_integral += current_error * current_integrator gain,
      voltage_cmd = current_error * current_gain + voltage_integral + ...
              ... + voltage_feedforward (when we have motor model).


current_gain and current_integrator_gain are automatically set according to config.motor.current_control_bandwidth

Control Modes

Filtered Position Control

Asking the ODrive controller to go as hard as it can to raw setpoints may result in jerky movement. Even if you are using a planned trajectory generated from an external source, if that is sent at a modest frequency, the ODrive may chase each stair in the incoming staircase in a jerky way. In this case, a good starting point for tuning the filter bandwidth is to set it to one half of your setpoint command rate.

You can use the second order position filter in these cases.

odrv0.axis0.controller.config.input_filter_bandwidth = 2.0 # Set the filter bandwidth [1/s]
odrv0.axis0.controller.config.input_mode = InputMode.POS_FILTER # Activate the setpoint filter
odrv0.axis0.controller.input_pos = 1 # control the position [turns]

Step response of a 1000 to 0 position input with a filter bandwidth of 1.0 [/sec].

Trajectory Control

See the :usage section for details. This mode lets you smoothly accelerate, coast, and decelerate the axis from one position to another. With raw position control, the controller simply tries to go to the setpoint as quickly as possible. Using a trajectory lets you tune the feedback gains more aggressively to reject disturbance, while keeping smooth motion.


Position (blue) and velocity (orange) vs. time using trajectory control.


Trajectory control is not compatible with circular mode (<axis>.controller.config.circular_setpoints).


odrv0.axis0.trap_traj.config.vel_limit = <Float>
odrv0.axis0.trap_traj.config.accel_limit = <Float>
odrv0.axis0.trap_traj.config.decel_limit = <Float>
odrv0.axis0.controller.config.inertia = <Float>
  • vel_limit is the maximum planned trajectory speed. This sets your coasting speed.

  • accel_limit is the maximum acceleration in turns / sec^2

  • decel_limit is the maximum deceleration in turns / sec^2

  • controller.config.inertia is a value which correlates acceleration (in turns / sec^2) and motor torque. It is 0 by default. It is optional, but can improve response of your system if correctly tuned. Keep in mind this will need to change with the load / mass of your system.


All values should be strictly positive (>= 0).

Keep in mind that you must still set your safety limits as before. It is recommended you set these a little higher ( > 10%) than the planner values, to give the controller enough control authority.

odrv0.axis0.config.motor.current_soft_max = <Float>
odrv0.axis0.controller.config.vel_limit = <Float>


Make sure you are in position control mode. To activate the trajectory module, set the input mode to trajectory:

odrv0.axis0.controller.config.input_mode = InputMode.TRAP_TRAJ

Simply send a position command to execute the move:

odrv0.axis0.controller.input_pos = <Float>

Use the move_incremental function to move to a relative position.

odrv0.axis0.controller.move_incremental(pos_increment, from_goal_point)

To set the goal relative to the current actual position, use from_goal_point = False To set the goal relative to the previous destination, use from_goal_point = True

You can also execute a move with the appropriate ascii command.

Circular Position Control

To enable Circular position control, set

odrv0.axis0.controller.config.circular_setpoints = True

This mode is useful for continuous incremental position movement. For example a robot rolling indefinitely, or an extruder motor or conveyor belt moving with controlled increments indefinitely. In the regular position mode, the input_pos would grow to a very large value and would lose precision due to floating point rounding.

In this mode, the controller will try to track the position within only one turn of the motor. Specifically, input_pos is expected in the range [0, 1). If the input_pos is incremented to outside this range (say via step/dir input), it is automatically wrapped around into the correct value. Note that in this mode encoder.pos_circular is used for feedback instead of encoder.pos_estimate.

If you try to increment the axis with a large step in one go that exceeds 1 turn, the motor will go to the same angle around the wrong way. This is also the case if there is a large disturbance. If you have an application where you would like to handle larger steps, you can use a larger circular range. Set

odrv0.axis0.controller.config.circular_setpoints_range = <N>

Choose N to give you an appropriate circular space for your application.

Absolute Position Control

The ODrive, by default, interprets position commands relative to its startup position. If you prefer to issue position commands with respect to an absolute reference frame for your machine, you can activate this by setting <axis>.controller.config.absolute_setpoints to True.

Note that this usually necessitates a homing procedure each time the ODrive starts up before you can switch to position control. This is regardless of whether you’re using a relative or absolute encoder.

However, you can avoid homing under two circumstances:

  1. If you can ensure that the physical position of the axis is always within +/- 0.5 turns of a predefined center position. More on this below.

  2. If you are only concerned with the absolute angle but not the total turn count. Again, see below for more details. Alternatively, this can be achieved using Circular Position Control with an absolute encoder (or an incremental encoder with index signal).

Using a Well-known Startup Position

If you’re using an absolute encoder, even though the ODrive knows the encoder angle immediately at startup, it can in general not know the absolute position on a multi-turn axis. For example, if your axis has a motion range of five turns, there are five possible axis positions that any known encoder angle can correspond to.

However if the user is able to guarantee the approximate startup position within +- half of an encoder turn, the ODrive can combine this information with the measured encoder angle to determine the exact absolute axis position at startup without homing.

The following example shows how to configure this feature. The example assumes that your ODrive is already configured for (relative) position control.

odrv0.axis0.pos_vel_mapper.config.offset = 0.0 # change this if you want the meaning of "position setpoint zero" to differ from the encoder's zero
odrv0.axis0.pos_vel_mapper.config.offset_valid = True
odrv0.axis0.pos_vel_mapper.config.approx_init_pos = 0.0 # change this if the guaranteed startup range is different from [-0.5, +0.5].
odrv0.axis0.pos_vel_mapper.config.approx_init_pos_valid = True
odrv0.axis0.controller.config.absolute_setpoints = True

This example sets both offset and approx_init_pos to 0.0. While this might not represent your final configuration, it’s generally easier to begin with these settings. After executing odrv0.save_configuration(), the position estimate odrv0.axis0.pos_vel_mapper.pos_abs will be immediately available without needing a homing procedure.

At this point, you can examine the pos_abs output to better understand if you need to adjust the offset (should you wish to shift the setpoint zero position) or alter the approx_init_pos (if the center of the startup range needs refining).

Velocity Control

Set the control mode

odrv0.axis0.controller.config.control_mode = ControlMode.VELOCITY_CONTROL

You can now control the velocity [turn/s] with

odrv0.axis0.controller.input_vel = 1

Ramped Velocity Control

Set the control mode

odrv0.axis0.controller.config.control_mode = ControlMode.VELOCITY_CONTROL

Set the velocity ramp rate (acceleration in turn/s^2):

odrv0.axis0.controller.config.vel_ramp_rate = 0.5

Activate the ramped velocity mode:

odrv0.axis0.controller.config.input_mode = InputMode.VEL_RAMP

You can now control the velocity (turn/s) with

odrv0.axis0.controller.input_vel = 1

Torque Control

Set the control mode

odrv0.axis0.controller.config.control_mode = ControlMode.TORQUE_CONTROL

Set the torque constant, e.g.:

# Approximately 8.23 / Kv where Kv is in the units [rpm / V]
odrv0.axis0.config.motor.torque_constant = 8.23 / 150

You can now control the torque (Nm) with e.g.

odrv0.axis0.controller.input_torque = 0.1


For safety reasons, the torque mode velocity limiter is enabled by default. This works by reducing the torque of the motor according to vel_limit and vel_gain, as shown below. Please note that with the default settings, torque will be limited even at 0 rpm.


The torque mode velocity limiter can be completely disabled by setting:

odrv0.axis0.controller.enable_torque_mode_vel_limit = False