ODrive 3.6 Sensorless Setup

Write-up on configuring an ODrive v3.6 brushless motor controller to drive two sensor-less motors from a battery. From unboxing to first runs, adding some notes and experiences to the startup guide

Table of ContentsInhoudsopgave

Note

From firmware / odrivetool 0.5.3 and up the device name in the command-line tool has changed from odrv0 to dev0. No mention of that in the guide.

Wiring

To keep things simple I decided to use a 6S (22V) LiPo pack. My battery pack came with an Amass XT-60 pack (not by coincicence) and given that there’s no reset button it would be good prep to solder a counter (male) plug to two short bits of wire that you’ll screw in the ODrive. This also allows you to skip mucking about with the load resistor for now, as a battery is fine with regen. (Most good supplies with enough bulk capacitance too, in contrast to what’s claimed on the site).

Make sure your motor(s) are resting in such a manner that they won’t drive off when accientally switched on.

Need to Know

You’ll need to know the following:

  • The capacity of your battery in (m)Ah
  • The discharge rating (xxC)
  • The charge rating (xC)
  • The motor pole pair count
  • The motor KV constant

Tools

Install the tools as directed in the ‘getting started’ guide.

Firmware Management

Here the fun started for me. If you came here looking how to de-brick your board pay attention now.

My board came with firmware 0.5.1 . A quick dive in the changelog for the current firmware, v0.5.3 showed some interesting fixes and improvements, so of cource I immediately ran odrivetool dfu, which then carefully proceeded to find the correct firmware, properly erase the device, write the new firmware to flash, load it in memory and then completely brick the board. mkay. Did not expect that. Should have though, as a quick search shows I’m one of the many ‘happy few’.

Luckily the MCU used by ODrive is an old friend and I have some resurection incarnations ready. First download release 0.5.1. Pick the .bin file for your board version and voltage.

Next you need a copy of dfu-util to do the updating.

With the software side done unplug the battery set the the tiny switch (remove the pick&place foil if needed) towards DFU. Run the DFU tool (it’ll wait for the board to appear):

dfu-util -v -w -R -a 0 -d 0483:df11 -s 0x08000000:leave -D {{ODriveFirmware_vX.Y-ZZV.bin}}

Plug the battery back in and wait for the tool to complete. Once complete you’ll have to unplug it again, set the switch away from DFU and back towards RUN. Now it should be ready to run again.

Initial Configuration

The guide goes straight into the configuration process. It might be better to explore a bit. Once the tool is started entering a variable path (the tool interfaces a tree structure) will return its current state:

$ odrivetool
In [0]: dev0.system_stats
Out[0]: 
i2c:
  addr: 0 (uint8)
  addr_match_cnt: 0 (uint32)
  error_cnt: 0 (uint32)
  rx_cnt: 0 (uint32)
min_heap_space: 41560 (uint32)
min_stack_space_axis0: 1660 (uint32)
min_stack_space_axis1: 1660 (uint32)
min_stack_space_can: 788 (uint32)
min_stack_space_comms: 3868 (uint32)
min_stack_space_startup: 1716 (uint32)
min_stack_space_uart: 3924 (uint32)
min_stack_space_usb: 3668 (uint32)
min_stack_space_usb_irq: 1796 (uint32)
stack_usage_axis0: 388 (uint32)
stack_usage_axis1: 388 (uint32)
stack_usage_can: 236 (uint32)
stack_usage_comms: 228 (uint32)
stack_usage_startup: 332 (uint32)
stack_usage_uart: 172 (uint32)
stack_usage_usb: 428 (uint32)
stack_usage_usb_irq: 252 (uint32)
uptime: 93469 (uint32)
usb:
  rx_cnt: 2656 (uint32)
  tx_cnt: 2656 (uint32)
  tx_overrun_cnt: 0 (uint32)

In [1]: dev0.axis0.motor.config
Out[1]: 
acim_autoflux_attack_gain: 10.0 (float)
acim_autoflux_decay_gain: 1.0 (float)
acim_autoflux_enable: False (bool)
acim_autoflux_min_Id: 10.0 (float)
acim_gain_min_flux: 10.0 (float)
acim_slip_velocity: 14.706000328063965 (float)
calibration_current: 10.0 (float)
current_control_bandwidth: 1000.0 (float)
current_lim: 10.0 (float)
current_lim_margin: 8.0 (float)
direction: 0 (int32)
inverter_temp_limit_lower: 100.0 (float)
inverter_temp_limit_upper: 120.0 (float)
motor_type: 0 (int32)
phase_inductance: 0.0 (float)
phase_resistance: 0.0 (float)
pole_pairs: 7 (int32)
pre_calibrated: False (bool)
requested_current_range: 60.0 (float)
resistance_calib_max_voltage: 2.0 (float)
torque_constant: 0.03999999910593033 (float)
torque_lim: inf (float)

From here on I’ll highlight several parameters I changed. Please do go over the guide too.

First disable the brake resistor:

dev0.config.enable_brake_resistor = False

We’re also going to set the regeneration current to a modest 1C (1x pack capacity, ~3A in my case):

dev0.config.dc_max_negative_current = -3

Note the double negation here!

My motors are only rated for 20-30A, so 10A calibration current is a little much. We’ll reduce that by assigning a new limit:

dev0.axis0.motor.config.calibration_current = 5
dev0.axis1.motor.config.calibration_current = 5

Next up is some counting. If you know the pole-pair count of your motor skip this, but for the rest of us find a way to count the number of magnets. In my case the supplier had a nice picture saving me from having to take a motor apart. This number is used to translate eRPM to RPM. The number entered below is half the magnet count (my motors have 42 magnets):

dev0.axis0.motor.config.pole_pairs = 21
dev0.axis1.motor.config.pole_pairs = 21
dev0.axis0.motor.config.torque_constant = 8.27 / 120
dev0.axis1.motor.config.torque_constant = 8.27 / 120

Time to jump to the sensorless settings. Besides copying the standard settings I started with half the default ramp current:

dev0.axis0.config.sensorless_ramp.current = 5
dev0.axis1.config.sensorless_ramp.current = 5

Don’t forget to set a speed limit, as the default is only 2Hz:

dev0.axis0.controller.config.vel_limit = 50
dev0.axis1.controller.config.vel_limit = 50

Once all settings are complete we’ll skip the normal calibration procedure (it’s not required for sensorless) and call the motor calibration ourselves:

dev0.axis0.requested_state = AXIS_STATE_MOTOR_CALIBRATION
dev0.axis1.requested_state = AXIS_STATE_MOTOR_CALIBRATION

This should produce a beep from the motor. Dump errors to make sure all went ok:

dump_errors(dev0)

If everything reports as ‘no error’ you’re good to go.

To be sure, set the requested velocities to 0:

dev0.axis0.controller.input_vel = 0
dev0.axis1.controller.input_vel = 0

Then switch the states to enable control:

dev0.axis0.requested_state = AXIS_STATE_CLOSED_LOOP_CONTROL
dev0.axis1.requested_state = AXIS_STATE_CLOSED_LOOP_CONTROL

Note that this immediately spins up the motors, so keep those fingers away if you’re spinning anything dangerous. Now you’ll want to use the up arrow quickly to re-write the input_vel for the axis you just enabled and stop the motors. Yes, the warning came after the spell.

Troubleshooting

My board turned out to be a clone board sold as the real thing. I had two main issues with these boards:

Soldering

The PCB I got was littered with small drops of solder. Two of these blobs had actually bridged IC legs, one set on a DRV, the other on the MCU. Lesson here: Inspect the boards and clean up the soldering before diagnosing futher. In my case these showed up as driver malfunction in odrivetool.

Corrupt ROM Data

The official ODrive boards have their hardware version stored in the MCU’s OTP (One-Time-Programmable) memory. Mine did not. Failed upgrades ensured, as the firmware update from 0.5.1 to any newer will introduce checks which will simply freeze the board (stuck in an infinite loop) if the OTP can’t be read correctly. If you run into this the solution is to build a custom firmware disabling the OTP and related functions.