Encoder calibration fails for T-Motor U10 Plus

I want to use the T-Motor U10 Plus (170KV, 36N40P) with ODrive. Unfortunately, encoder calibration consistently fails. I am currently using the new ODrive GUI and configure the motor to 170kv and 20 pole pairs. Calibration of the motor suceeds, but encoder calibration keeps failing (ATM102-V). The encoder is known to work properly, if I attach it to a D6374 everything works perfectly.

The U10 Plus is mounted to a harmonic drive, so it experiences some load, but that is barely noticeable. Even if I allow the housing of the motor to spin freely without any mechanical resistance, the calibration does not look as smooth as it does for many other motors, e.g. the D6374.

Any tips what to try?

Hi Adrian,

Since your motor has 20 pole pairs, can you set odrvX.axisN.encoder.config.calib_scan_distance to something like 42 * Pi and try to calibrate again? By default, it is 16 * Pi, which is enough for just over a full revolution of movement with a 7 pole pair motor like the D6374.

Hi Patrick,

thanks a million, that was the hint I was looking for!

I also had to increase calib_range for the calbration to succeed. To get started, I just set it to 5 times the default to make it work. I did not look into details yet, but probably there is some systematic functional dependency that would help to circumvent experimentation. That btw. makes me wonder why the firmware currently does not derive calib_scan_distance from the number of pole pairs on its own?

No problem! Glad to hear that it works!

We could do that, but in general the various firmware classes (like Motor, Encoder) are designed to not depend heavily on each other. This allows for more flexibility. It’s true that all of them are interrelated by virtue of controlling a motor, but conceptually they exist independently. However, changing parameters like calib_scan_distance based on motor parameters is a good idea, and is a feature that will show up in the GUI.

The development idea now is that the ODrive parameters are an API that the user need not interact with directly. Instead, pesky real-world details like calib_scan_range depending on pole pairs, etc, will be handled by higher level software like the GUI to be easier to use. There’s a lot of work to do on that front, but I figured this point warranted explanation.

1 Like

I’m new to this forum. My implementation requires me to also change the parameters for calib_range and encoder.config.calib_scan_distance. Your response to Adrian implies that these parameters may be possible to change without modifying the encoder.hpp file and recompiling/flashing. If true, then how is that done? Is it only via the GUI which I havn’t been able to get to work, yet. I’ve tried odrv0.axis0.encoder.config.calib_scan_distance = 42 * 3.14 but that throws an error.

Is is definitely possible to change those parameters with odrivetool, the GUI is not necessary for it. What errors do you get?

Thanks for your reply. It was a typo error on my part (had an _ in place of a .)
I have the motor passing full calibration.

But, I could use some advice on configuration for my implementation.

I’m using an eBike brushless hub motor (~1 KW maybe 14KV) with a 4000 cpr encoder and a 10:1 multiplying timing belt gear for 40000 cpr. I would like to control a telescope with this which means that it needs very low RPM. I would like to track the earth’s rotation which means the motor will move very slowly at 0.004 deg/sec or 0.00007 rad/sec. Max slew speed would be around 20 deg/sec.

I’ve experimented with different control and trajectory modes and can’t seem to find anything that works. Typically, I get a very fast movement and overspeed errors. There is some load on the motor from friction components and it seems to not generate enough early torque to overcome this.

Can you recommend a general configuration that would shortcut some of my experimentation? (FYI, my background is Electrical Engineering for 35 yrs)

You will likely need the anticogging algorithm for this. Check out this video Activating Anti-Cogging on the ODrive Robotics Controller (smooth brushless motor control) - YouTube

Unfortunately, I am not able to get the motor working enough to do the anti-cogging function.

I’m able to pass motor and encoder calibrations, but when I go to set a position, the motor will not move (or sometimes moves after a delay). When it doesn’t move, my 22V DC supply shows the current steadily increasing to around 4 A before shutting off the motor with a DC_BUS_UNDER_VOLTAGE error.

After a complete confuration erase here are the settings I use:

odrv0.axis0.encoder.config.cpr = 40000
odrv0.axis0.encoder.config.calib_scan_distance = 150.72
odrv0.axis0.encoder.config.calib_range = 0.050
odrv0.axis0.motor.config.pole_pairs = 24
odrv0.axis0.motor.config.calibration_current = 20
odrv0.axis0.motor.config.resistance_calib_max_voltage = 5.0
odrv0.axis0.motor.config.current_lim = 30
odrv0.config.brake_resistance = 1
odrv0.axis0.motor.config.torque_constant = .69

Here are the status’s:

odrv0.axis0.controller.input_pos = 0

In [41]: odrv0.axis0.requested_state = AXIS_STATE_CLOSED_LOOP_CONTROL

In [42]: odrv0.axis0.controller.input_pos = 1

In [43]: odrv0.axis0.controller.input_pos = 0

In [44]: dump_errors(odrv0,1)
axis0
axis: Error(s):
AXIS_ERROR_DC_BUS_UNDER_VOLTAGE
motor: no error
fet_thermistor: no error
motor_thermistor: no error
encoder: no error
controller: no error
axis1
axis: Error(s):
AXIS_ERROR_DC_BUS_UNDER_VOLTAGE
motor: no error
fet_thermistor: no error
motor_thermistor: no error
encoder: no error
controller: no error

In [45]: odrv0.axis0.controller
Out[45]:
error = 0x0000 (int)
input_pos = 0.0 (float)
input_vel = 0.0 (float)
input_torque = 0.0 (float)
pos_setpoint = 0.0 (float)
vel_setpoint = 0.0 (float)
torque_setpoint = 0.0 (float)
trajectory_done = True (bool)
vel_integrator_torque = -16.312145233154297 (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 = 3 (int)
input_mode = 1 (int)
pos_gain = 20.0 (float)
vel_gain = 0.1666666716337204 (float)
vel_integrator_gain = 0.3333333432674408 (float)
vel_limit = 2.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 = 0 (int)
input_filter_bandwidth = 2.0 (float)
anticogging: …
move_incremental(displacement: float, from_input_pos: bool)
start_anticogging_calibration()

odrv0.axis0.motor
Out[47]:
error = 0x0000 (int)
armed_state = 0 (int)
is_calibrated = True (bool)
current_meas_phB = 0.21049678325653076 (float)
current_meas_phC = -0.26801440119743347 (float)
DC_calib_phB = -0.653647243976593 (float)
DC_calib_phC = -0.41695636510849 (float)
phase_current_rev_gain = 0.02500000037252903 (float)
effective_current_lim = 30.0 (float)
current_control:
p_gain = 0.1646914929151535 (float)
i_gain = 123.95084381103516 (float)
v_current_control_integral_d = 0.02746220864355564 (float)
v_current_control_integral_q = -3.0534331798553467 (float)
Ibus = 13.604094505310059 (float)
final_v_alpha = 2.9599316120147705 (float)
final_v_beta = -1.0756747722625732 (float)
Id_setpoint = 0.0 (float)
Iq_setpoint = -24.123762130737305 (float)
Iq_measured = -23.482318878173828 (float)
Id_measured = -0.08284255117177963 (float)
I_measured_report_filter_k = 1.0 (float)
max_allowed_current = 60.75 (float)
overcurrent_trip_level = 67.5 (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 = 62788 (int)
adc_cb_i = 2718 (int)
adc_cb_dc = 13050 (int)
meas_r = 7614 (int)
meas_l = 7698 (int)
enc_calib = 8070 (int)
idx_search = 42640 (int)
foc_voltage = 8014 (int)
foc_current = 9178 (int)
spi_start = 29031 (int)
sample_now = 22132 (int)
spi_end = 53207 (int)
config:
pre_calibrated = False (bool)
pole_pairs = 24 (int)
calibration_current = 20.0 (float)
resistance_calib_max_voltage = 5.0 (float)
phase_inductance = 0.00016469148977193981 (float)
phase_resistance = 0.12395083904266357 (float)
torque_constant = 0.6899999976158142 (float)
direction = 1 (int)
motor_type = 0 (int)
current_lim = 30.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 = 60.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)

In [48]: odrv0.axis0.encoder
Out[48]:
error = 0x0000 (int)
is_ready = True (bool)
index_found = False (bool)
shadow_count = 66299 (int)
count_in_cpr = 26299 (int)
interpolation = 0.5 (float)
phase = 1.2095787525177002 (float)
pos_estimate = 1.6574937105178833 (float)
pos_estimate_counts = 66299.75 (float)
pos_cpr = 0.6574937105178833 (float)
pos_cpr_counts = 26299.75 (float)
pos_circular = 0.6575433611869812 (float)
hall_state = 6 (int)
vel_estimate = 0.0 (float)
vel_estimate_counts = 0.0 (float)
calib_scan_response = 40742.0 (float)
pos_abs = 0 (int)
spi_error_rate = 0.0 (float)
config:
mode = 0 (int)
use_index = False (bool)
find_idx_on_lockin_only = False (bool)
abs_spi_cs_gpio_pin = 1 (int)
zero_count_on_find_idx = True (bool)
cpr = 40000 (int)
offset = 85978 (int)
pre_calibrated = False (bool)
offset_float = 0.6505299806594849 (float)
enable_phase_interpolation = True (bool)
bandwidth = 1000.0 (float)
calib_range = 0.05000000074505806 (float)
calib_scan_distance = 150.72000122070312 (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)

I’ve tried many different settings and modes. The motor acts as if there is not enough torque to overcome the small load. When it does move, I get OVERSPEED errors.

Can you help with some ideas??

What encoder are you using? 40000 is surprising. Over speed means increase your vel limit tolerance, or disable the vel over speed error, just make sure that it’s not spinning out like crazy. Low bus voltage makes no sense though

I am curious as to why you have picked such a powerful motor with high torque ripple, direct-drive, to position a telescope… :stuck_out_tongue: This really is like using a steamroller to crack a nut.

eBike motors are really quite awful for positioning, because they are so coggy. (they use iron slots in the stator, which attract the permanent magnets on the rotor to a varying degree, causing a variable torque offset as a function of position)
I would probably have gone for a small motor with a large gear ratio via a timing belt, and two encoders, one for the load and one for the motor. And ideally, a slotless/ironless motor.

The encoder is an optical incremental 4000 CPR encoder of unknown brand. It was salvaged from an older telescope for monitoring Azimuth and Altitude settings. An oscilloscope shows it is working fine. I’m using twisted pair cable with one side of pair connected to ground. I’ve added 22 pf caps to ground on both A and B inputs. The encoder is connected to the motor through a 10:1 gearing using a GT2 timing belt. Encoder gear is 32 tooth, motor gear is 320. I’m trying to get very high resolution cheaply similar to the Renishaw encoders. The calibration of the encoder shows that it returns to the starting position within <10 counts. Sometimes it’s actually within 1 count out of 40000.

The velocity limit is being tripped during the spinout. The spinout shouldn’t be happening, therefore changing the vel limit tolerance would mask the fundamental problem. I’ve got the vel_limit = 2.0 and the vel_ramp_rate = 1.0 (but have tried ramps as low as 0.1).

The encoder calibration sequence moves the motor in a nice steady slow motion which exactly mimics the speeds I want. I see from the encoder.cpp code, it is operating in current_mode and open_loop. Therefore, the problem is in the PID control loop. Still trying to understand the interactions among the many parameters, gains, etc.

I know this motor is really not intended to be used by this controller, but I had to give it a shot.

Yes, you are right, this motor is probably not suited but I got the idea from the following video:

The motor in the video is identical to the one I have. The hub motor I got had a great price. Free.

Also, depending on the Size/mass of the telescope, and if there is any wind load, a motor with at least 5 Nm of torque is required. My telescope, a 13" reflector, is fairly large, but probably doesn’t need this kind of torque.

I’m also building my own “pancake” 3 phase motors with 3D printing in parallel with trying to use the hub motor. Currently wiring the stator up and will try it with the odrive in the next week.

Direct drive telescopes using 3 phase motors are the way to get around the old problems with gear lash and periodic errors in gear trains. They are more expensive than worm gear/stepper motor drives but the costs are coming down, especially if something like the odrive can simplify the electronics.

1 Like

The “spinout” is almost always caused by the encoder losing calibration in some way. Especially if you put the motor in torque mode. Are you positive that the encoder is exactly 10:1? Is it definitely reporting good data while the motor is in CLOSED_LOOP mode? We often find that when the motor switches from IDLE to closed loop, the EMI becomes a big problem. Make sure your encoder wires are paired, twisted, and shielded (unless shielding causes capacitive coupling problems). Make sure your motor wires are twisted and using ferrite rings Ferrite Ring ESD-R-28C-1 — ODrive - ODrive v3 has some issues with common mode noise on the encoders and SPI lines etc.

I recommend putting it in torque mode (controller.config.control_mode = CONTROL_MODE_TORQUE_CONTROL) and then setting it to CLOSED LOOP with a torque command of 0. This will start the mosfets switching but you won’t have any real closed loop dynamics to deal with, so you can put a scope on the encoder traces.

Also, with 35 years experience feel free to tell us what we’re doing wrong! :smiley: I think you have more experience than the whole team combined haha

1 Like