Merge remote-tracking branch 'madcowswe/devel' into TrajPlan

This commit is contained in:
Unknown
2018-08-31 23:45:53 -04:00
20 changed files with 229 additions and 88 deletions
@@ -46,19 +46,20 @@ void loop() {
// Run calibration sequence
if (c == '0' || c == '1') {
int motornum = c-'0';
int requested_state;
requested_state = ODriveArduino::AXIS_STATE_MOTOR_CALIBRATION;
Serial << "Axis" << c << ": Requesting state " << requested_state << '\n';
odrive.run_state(atoi(c), requested_state, true);
odrive.run_state(motornum, requested_state, true);
requested_state = ODriveArduino::AXIS_STATE_ENCODER_OFFSET_CALIBRATION;
Serial << "Axis" << c << ": Requesting state " << requested_state << '\n';
odrive.run_state(atoi(c), requested_state, true);
odrive.run_state(motornum, requested_state, true);
requested_state = ODriveArduino::AXIS_STATE_CLOSED_LOOP_CONTROL;
Serial << "Axis" << c << ": Requesting state " << requested_state << '\n';
odrive.run_state(atoi(c), requested_state, false); // don't wait
odrive.run_state(motornum, requested_state, false); // don't wait
}
// Sinusoidal test move
+14 -1
View File
@@ -2,7 +2,20 @@
Please add a note of your changes below this heading if you make a Pull Request.
# Releases
## [0.4.2] - 2018-07-04
## [0.4.3] - 2018-08-30
### Added
* Encoder position count "homed" to zero when index is found.
### Changed
* We now enforce encoder offset calibration must happen after index is found (if using index)
* Renaming of the velocity estimate `pll_vel` -> `vel_estimate`.
* Hardcoded maximum inductance now 2500 uH.
### Fixed
* Once you got an axis error `ERROR_INVALID_STATE` you could never clear it
* Char to int conversion to read motornum on arduino example
## [0.4.2] - 2018-08-04
### Added
* Hall sensor feedback
* Configurable RC PWM input
+24 -10
View File
@@ -93,6 +93,19 @@ void Axis::set_step_dir_enabled(bool enable) {
}
}
bool Axis::check_for_errors() {
// Maybe we should update this to only trigger on new errors?
// The danger with that is we could fail to bail on uncleared errors that still prevent
// correct opreation.
// For now: we treat ERROR_INVALID_STATE in idle loop special, or we could never stay
// in idle after this kind of error.
if (current_state_ == AXIS_STATE_IDLE)
return (error_ & ~ERROR_INVALID_STATE) == ERROR_NONE;
else
return error_ == ERROR_NONE;
}
// @brief Do axis level checks and call subcomponent do_checks
// Returns true if everything is ok.
bool Axis::do_checks() {
@@ -112,7 +125,7 @@ bool Axis::do_checks() {
// sensorless_estimator_.do_checks();
// controller_.do_checks();
return error_ == ERROR_NONE;
return check_for_errors();
}
// @brief Update all esitmators
@@ -120,7 +133,7 @@ bool Axis::do_updates() {
// Sub-components should use set_error which will propegate to this error_
encoder_.update();
sensorless_estimator_.update();
return error_ == ERROR_NONE;
return check_for_errors();
}
float Axis::get_temp() {
@@ -159,7 +172,7 @@ bool Axis::run_sensorless_spin_up() {
// is zeroed. So we make the setpoint the spinup target for smooth transition.
controller_.vel_setpoint_ = config_.spin_up_target_vel;
return error_ == ERROR_NONE;
return check_for_errors();
}
// Note run_sensorless_control_loop and run_closed_loop_control_loop are very similar and differ only in where we get the estimate from.
@@ -171,14 +184,14 @@ bool Axis::run_sensorless_control_loop() {
// Note that all estimators are updated in the loop prefix in run_control_loop
float current_setpoint;
if (!controller_.update(sensorless_estimator_.pll_pos_, sensorless_estimator_.pll_vel_, &current_setpoint))
if (!controller_.update(sensorless_estimator_.pll_pos_, sensorless_estimator_.vel_estimate_, &current_setpoint))
return error_ |= ERROR_CONTROLLER_FAILED, false;
if (!motor_.update(current_setpoint, sensorless_estimator_.phase_))
return false; // set_error should update axis.error_
return true;
});
set_step_dir_enabled(false);
return error_ == ERROR_NONE;
return check_for_errors();
}
bool Axis::run_closed_loop_control_loop() {
@@ -186,14 +199,14 @@ bool Axis::run_closed_loop_control_loop() {
run_control_loop([this](){
// Note that all estimators are updated in the loop prefix in run_control_loop
float current_setpoint;
if (!controller_.update(encoder_.pos_estimate_, encoder_.pll_vel_, &current_setpoint))
if (!controller_.update(encoder_.pos_estimate_, encoder_.vel_estimate_, &current_setpoint))
return error_ |= ERROR_CONTROLLER_FAILED, false; //TODO: Make controller.set_error
if (!motor_.update(current_setpoint, encoder_.phase_))
return false; // set_error should update axis.error_
return true;
});
set_step_dir_enabled(false);
return error_ == ERROR_NONE;
return check_for_errors();
}
bool Axis::run_idle_loop() {
@@ -203,7 +216,7 @@ bool Axis::run_idle_loop() {
run_control_loop([this](){
return true;
});
return error_ == ERROR_NONE;
return check_for_errors();
}
// Infinite loop that does calibration and enters main control loop as appropriate
@@ -249,9 +262,10 @@ void Axis::run_state_machine_loop() {
task_chain_[pos++] = requested_state_;
task_chain_[pos++] = AXIS_STATE_IDLE;
}
task_chain_[pos++] = AXIS_STATE_UNDEFINED;
// TODO: bounds checking
task_chain_[pos++] = AXIS_STATE_UNDEFINED; // TODO: bounds checking
requested_state_ = AXIS_STATE_UNDEFINED;
// Auto-clear any invalid state error
error_ &= ~ERROR_INVALID_STATE;
}
// Note that current_state is a reference to task_chain_[0]
+8 -3
View File
@@ -79,6 +79,7 @@ public:
bool check_PSU_brownout();
bool do_checks();
bool do_updates();
bool check_for_errors();
float get_temp();
// @brief Runs the specified update handler at the frequency of the current measurements.
@@ -104,9 +105,13 @@ public:
template<typename T>
void run_control_loop(const T& update_handler) {
while (requested_state_ == AXIS_STATE_UNDEFINED) {
if (!do_checks()) // look for errors at axis level and also all subcomponents
break;
if (!do_updates()) // Update all estimators
// look for errors at axis level and also all subcomponents
bool checks_ok = do_checks();
// Update all estimators
// Note: updates run even if checks fail
bool updates_ok = do_updates();
if (!checks_ok || !updates_ok)
break;
// Run main loop function, defer quitting for after wait
+43 -34
View File
@@ -8,7 +8,6 @@ Encoder::Encoder(const EncoderHardwareConfig_t& hw_config,
config_(config)
{
if (config.pre_calibrated && (config.mode == Encoder::MODE_HALL)) {
offset_ = config.offset;
is_ready_ = true;
}
}
@@ -38,13 +37,19 @@ bool Encoder::do_checks(){
// Triggered when an encoder passes over the "Index" pin
// TODO: only arm index edge interrupt when we know encoder has powered up
// (maybe by attaching the interrupt on start search, synergistic with following)
// TODO: disable interrupt once we found the index
void Encoder::enc_index_cb() {
if (config_.use_index && !index_found_) {
set_circular_count(0);
set_circular_count(0, false);
set_linear_count(0); // Avoid position control transient after search
if (config_.pre_calibrated) {
offset_ = config_.offset;
is_ready_ = true;
} else {
// We can't use the update_offset facility in set_circular_count because
// we also set the linear count before there is a chance to update. Therefore:
// Invalidate offset calibration that may have happened before idx search
is_ready_ = false;
}
index_found_ = true;
}
@@ -67,14 +72,16 @@ void Encoder::set_linear_count(int32_t count) {
// Function that sets the CPR circular tracking encoder count to a desired 32-bit value.
// Note that this will get mod'ed down to [0, cpr)
void Encoder::set_circular_count(int32_t count) {
void Encoder::set_circular_count(int32_t count, bool update_offset) {
// Disable interrupts to make a critical section to avoid race condition
uint32_t prim = __get_PRIMASK();
__disable_irq();
// Offset and state must be shifted by the same amount
offset_ += count - count_in_cpr_;
offset_ = mod(offset_, config_.cpr);
if (update_offset) {
config_.offset += count - count_in_cpr_;
config_.offset = mod(config_.offset, config_.cpr);
}
// Update states
count_in_cpr_ = mod(count, config_.cpr);
pos_cpr_ = (float)count_in_cpr_;
@@ -124,10 +131,11 @@ bool Encoder::run_offset_calibration() {
static const float scan_distance = 16.0f * M_PI;
static const int num_steps = (int)(scan_distance / scan_omega * (float)current_meas_hz);
// Temporarily disable index search so it doesn't mess
// with the offset calibration
bool old_use_index = config_.use_index;
config_.use_index = false;
// Require index found if enabled
if (config_.use_index && !index_found_) {
set_error(ERROR_INDEX_NOT_FOUND_YET);
return false;
}
// We use shadow_count_ to do the calibration, but the offset is used by count_in_cpr_
// Therefore we have to sync them for calibration
@@ -172,16 +180,7 @@ bool Encoder::run_offset_calibration() {
if (axis_->error_ != Axis::ERROR_NONE)
return false;
//TODO avoid recomputing elec_rad_per_enc every time
float elec_rad_per_enc = axis_->motor_.config_.pole_pairs * 2 * M_PI * (1.0f / (float)(config_.cpr));
float expected_encoder_delta = scan_distance / elec_rad_per_enc;
float actual_encoder_delta_abs = fabsf(shadow_count_-init_enc_val);
if(fabsf(actual_encoder_delta_abs - expected_encoder_delta)/expected_encoder_delta > config_.calib_range)
{
set_error(ERROR_CPR_OUT_OF_RANGE);
return false;
}
// check direction
// Check response and direction
if (shadow_count_ > init_enc_val + 8) {
// motor same dir as encoder
axis_->motor_.config_.direction = 1;
@@ -190,7 +189,18 @@ bool Encoder::run_offset_calibration() {
axis_->motor_.config_.direction = -1;
} else {
// Encoder response error
set_error(ERROR_RESPONSE);
set_error(ERROR_NO_RESPONSE);
return false;
}
//TODO avoid recomputing elec_rad_per_enc every time
// Check CPR
float elec_rad_per_enc = axis_->motor_.config_.pole_pairs * 2 * M_PI * (1.0f / (float)(config_.cpr));
float expected_encoder_delta = scan_distance / elec_rad_per_enc;
float actual_encoder_delta_abs = fabsf(shadow_count_-init_enc_val);
if(fabsf(actual_encoder_delta_abs - expected_encoder_delta)/expected_encoder_delta > config_.calib_range)
{
set_error(ERROR_CPR_OUT_OF_RANGE);
return false;
}
@@ -211,12 +221,11 @@ bool Encoder::run_offset_calibration() {
if (axis_->error_ != Axis::ERROR_NONE)
return false;
offset_ = encvaluesum / (num_steps * 2);
config_.offset = offset_;
int32_t residual = encvaluesum - ((int64_t)offset_ * (int64_t)(num_steps * 2));
config_.offset = encvaluesum / (num_steps * 2);
int32_t residual = encvaluesum - ((int64_t)config_.offset * (int64_t)(num_steps * 2));
config_.offset_float = (float)residual / (float)(num_steps * 2) + 0.5f; // add 0.5 to center-align state to phase
is_ready_ = true;
config_.use_index = old_use_index;
return true;
}
@@ -278,8 +287,8 @@ bool Encoder::update() {
//// run pll (for now pll is in units of encoder counts)
// Predict current pos
pos_estimate_ += current_meas_period * pll_vel_;
pos_cpr_ += current_meas_period * pll_vel_;
pos_estimate_ += current_meas_period * vel_estimate_;
pos_cpr_ += current_meas_period * vel_estimate_;
// discrete phase detector
float delta_pos = (float)(shadow_count_ - (int32_t)floorf(pos_estimate_));
float delta_pos_cpr = (float)(count_in_cpr_ - (int32_t)floorf(pos_cpr_));
@@ -288,15 +297,15 @@ bool Encoder::update() {
pos_estimate_ += current_meas_period * pll_kp * delta_pos;
pos_cpr_ += current_meas_period * pll_kp * delta_pos_cpr;
pos_cpr_ = fmodf_pos(pos_cpr_, (float)(config_.cpr));
pll_vel_ += current_meas_period * pll_ki * delta_pos_cpr;
vel_estimate_ += current_meas_period * pll_ki * delta_pos_cpr;
bool snap_to_zero_vel = false;
if (fabsf(pll_vel_) < 0.5f * current_meas_period * pll_ki) {
pll_vel_ = 0.0f; //align delta-sigma on zero to prevent jitter
if (fabsf(vel_estimate_) < 0.5f * current_meas_period * pll_ki) {
vel_estimate_ = 0.0f; //align delta-sigma on zero to prevent jitter
snap_to_zero_vel = true;
}
//// run encoder count interpolation
int32_t corrected_enc = count_in_cpr_ - offset_;
int32_t corrected_enc = count_in_cpr_ - config_.offset;
// if we are stopped, make sure we don't randomly drift
if (snap_to_zero_vel) {
interpolation_ = 0.5f;
@@ -306,8 +315,8 @@ bool Encoder::update() {
} else if (delta_enc < 0) {
interpolation_ = 1.0f;
} else {
// Interpolate (predict) between encoder counts using pll_vel,
interpolation_ += current_meas_period * pll_vel_;
// Interpolate (predict) between encoder counts using vel_estimate,
interpolation_ += current_meas_period * vel_estimate_;
// don't allow interpolation indicated position outside of [enc, enc+1)
if (interpolation_ > 1.0f) interpolation_ = 1.0f;
if (interpolation_ < 0.0f) interpolation_ = 0.0f;
+6 -8
View File
@@ -11,9 +11,10 @@ public:
ERROR_NONE = 0,
ERROR_UNSTABLE_GAIN = 0x01,
ERROR_CPR_OUT_OF_RANGE = 0x02,
ERROR_RESPONSE = 0x04,
ERROR_NO_RESPONSE = 0x04,
ERROR_UNSUPPORTED_ENCODER_MODE = 0x08,
ERROR_ILLEGAL_HALL_STATE = 0x10,
ERROR_INDEX_NOT_FOUND_YET = 0x20,
};
enum Mode_t {
@@ -31,8 +32,7 @@ public:
// state as soon as the index is found.
float idx_search_speed = 10.0f; // [rad/s electrical]
int32_t cpr = (2048 * 4); // Default resolution of CUI-AMT102 encoder,
int32_t offset = 0; // If pre_calibrated is true, this is copied into encoder.offset_ once
// index search succeeds
int32_t offset = 0; // Offset between encoder count and rotor electrical phase
float offset_float = 0.0f; // Sub-count phase alignment offset
float calib_range = 0.02f;
float bandwidth = 1000.0f;
@@ -48,7 +48,7 @@ public:
void enc_index_cb();
void set_linear_count(int32_t count);
void set_circular_count(int32_t count);
void set_circular_count(int32_t count, bool update_offset);
bool calib_enc_offset(float voltage_magnitude);
bool scan_for_enc_idx(float omega, float voltage_magnitude);
@@ -65,12 +65,11 @@ public:
bool is_ready_ = false;
int32_t shadow_count_ = 0;
int32_t count_in_cpr_ = 0;
int32_t offset_ = 0;
float interpolation_ = 0.0f;
float phase_ = 0.0f; // [rad]
float pos_estimate_ = 0.0f; // [rad]
float pos_cpr_ = 0.0f; // [rad]
float pll_vel_ = 0.0f; // [rad/s]
float vel_estimate_ = 0.0f; // [rad/s]
// float pll_kp_ = 0.0f; // [rad/s / rad]
// float pll_ki_ = 0.0f; // [(rad/s^2) / rad]
@@ -85,13 +84,12 @@ public:
make_protocol_ro_property("index_found", const_cast<bool*>(&index_found_)),
make_protocol_property("shadow_count", &shadow_count_),
make_protocol_property("count_in_cpr", &count_in_cpr_),
make_protocol_property("offset", &offset_),
make_protocol_property("interpolation", &interpolation_),
make_protocol_property("phase", &phase_),
make_protocol_property("pos_estimate", &pos_estimate_),
make_protocol_property("pos_cpr", &pos_cpr_),
make_protocol_property("hall_state", &hall_state_),
make_protocol_property("pll_vel", &pll_vel_),
make_protocol_property("vel_estimate", &vel_estimate_),
// make_protocol_property("pll_kp", &pll_kp_),
// make_protocol_property("pll_ki", &pll_ki_),
make_protocol_object("config",
+2 -1
View File
@@ -602,7 +602,8 @@ void update_brake_current() {
if (low_off < 0) low_off = 0;
safety_critical_apply_brake_resistor_timings(low_off, high_on);
} else {
safety_critical_disarm_brake_resistor();
//shuts off all motors AND brake resistor, sets error code on all motors.
low_level_fault(Motor::ERROR_BRAKE_CURRENT_OUT_OF_RANGE);
}
}
+1 -1
View File
@@ -241,7 +241,7 @@ bool Motor::measure_phase_inductance(float voltage_low, float voltage_high) {
config_.phase_inductance = L;
// TODO arbitrary values set for now
if (L < 1e-6f || L > 500e-6f)
if (L < 1e-6f || L > 2500e-6f)
return set_error(ERROR_PHASE_INDUCTANCE_OUT_OF_RANGE), false;
return true;
}
-1
View File
@@ -44,7 +44,6 @@ typedef struct {
float phase_resistance = 0.0f; // to be set by measure_phase_resistance
int32_t direction = 1; // 1 or -1
Motor_type_t motor_type = MOTOR_TYPE_HIGH_CURRENT;
// Read out max_allowed_current to see max supported value for current_lim.
// float current_lim = 70.0f; //[A]
float current_lim = 10.0f; //[A]
@@ -69,13 +69,13 @@ bool SensorlessEstimator::update() {
}
// predict PLL phase with velocity
pll_pos_ = wrap_pm_pi(pll_pos_ + current_meas_period * pll_vel_);
pll_pos_ = wrap_pm_pi(pll_pos_ + current_meas_period * vel_estimate_);
// update PLL phase with observer permanent magnet phase
phase_ = fast_atan2(eta[1], eta[0]);
float delta_phase = wrap_pm_pi(phase_ - pll_pos_);
pll_pos_ = wrap_pm_pi(pll_pos_ + current_meas_period * pll_kp * delta_phase);
// update PLL velocity
pll_vel_ += current_meas_period * pll_ki * delta_phase;
vel_estimate_ += current_meas_period * pll_ki * delta_phase;
return true;
};
@@ -25,7 +25,7 @@ public:
Error_t error_ = ERROR_NONE;
float phase_ = 0.0f; // [rad]
float pll_pos_ = 0.0f; // [rad]
float pll_vel_ = 0.0f; // [rad/s]
float vel_estimate_ = 0.0f; // [rad/s]
// float pll_kp_ = 0.0f; // [rad/s / rad]
// float pll_ki_ = 0.0f; // [(rad/s^2) / rad]
float flux_state_[2] = {0.0f, 0.0f}; // [Vs]
@@ -38,7 +38,7 @@ public:
make_protocol_property("error", &error_),
make_protocol_property("phase", &phase_),
make_protocol_property("pll_pos", &pll_pos_),
make_protocol_property("pll_vel", &pll_vel_),
make_protocol_property("vel_estimate", &vel_estimate_),
// make_protocol_property("pll_kp", &pll_kp_),
// make_protocol_property("pll_ki", &pll_ki_),
make_protocol_object("config",
+3
View File
@@ -297,6 +297,9 @@ class Channel(PacketSink):
except ChannelDamagedException:
attempt += 1
continue # resend
except TimeoutError:
attempt += 1
continue # resend
finally:
self._my_lock.release()
# Wait for ACK until the resend timeout is exceeded
+20 -3
View File
@@ -71,6 +71,18 @@ An upcoming feature will enable automatic tuning. Until then, here is a rough tu
* Back down `pos_gain` until you do not have overshoot anymore.
* The integrator is not easily tuned, nor is it strictly required. Tune at your own discretion.
## System monitoring commands
### Encoder position and velocity
* View encoder position with `<axis>.encoder.pos_estimate` [counts]
* View rotational velocity with `<axis>.encoder.pll_vel` [counts/s]
### Motor current and torque estimation
* View the commanded motor current with `<axis>.motor.current_control.Iq_setpoint` [A]
* View the measured motor current with `<axis>.motor.current_control.Iq_measured` [A]. If you find that this returns noisy data then use the command motor current instead. The two values should be close so long as you are not approching the maximim achieveable rotational velocity of your motor for a given supply votlage, in which case the commanded current may become larger than the measured current.
Using the motor current and the known KV of your motor you can estimate the motors torque using the following relationship: Torque [N.m] = 8.27 * Current [A] / KV.
## General system commands
### Saving the configuration
@@ -88,10 +100,15 @@ All variables that are part of a `[...].config` object can be saved to non-volat
## Setting up sensorless
The ODrive can run without encoder/hall feedback, but there is a minimum speed, usually around a few hunderd RPM.
However the
However the units of this mode is different from when using an encoder. Velocities are not measured in counts/s, instead it is electrical rad/s. This also applies to the gains. For example, `vel_gain` is in units of `A / (rad/s)` instead of `A / (count/s)`.
To give an example, suppose you have a motor with 7 pole pairs, and you want to spin it at 3000 RPM. Then you would set the `vel_setpoint` to `3000 * 2*pi/60 * 7 = 2199 rad/s electrical`.
Below are some suggested starting parameters that you can use. Note that you _must_ set the `pm_flux_linkage` correctly for sensorless mode to work.
```
odrv0.axis0.controller.config.vel_gain = 0.1
odrv0.axis0.controller.config.vel_integrator_gain = 0
odrv0.axis0.controller.config.vel_gain = 0.01
odrv0.axis0.controller.config.vel_integrator_gain = 0.05
odrv0.axis0.controller.config.control_mode = 2
odrv0.axis0.controller.vel_setpoint = 400
odrv0.axis0.sensorless_estimator.config.pm_flux_linkage = 5.51328895422 / (<pole pairs> * <motor kv>)
Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

+8 -5
View File
@@ -163,7 +163,7 @@ In the previous step we started `odrivetool`. In there, you can assign variables
For instance, to set the current limit of M0 to 10A you would type: `odrv0.axis0.motor.config.current_lim = 10` <kbd>Enter</kbd>
</div></details>
* The current limit: `odrv0.axis0.motor.config.current_lim` [A]. The default current limit, for safety reasons, is set to 10A. This is quite weak, and good for making sure the drive is stable. Once you have tuned the drive, you can increase this to 75A to get some performance. Note that above 75A, you must change the current amplifier gains.
* The current limit: `odrv0.axis0.motor.config.current_lim` [A]. The default current limit, for safety reasons, is set to 10A. This is quite weak, and good for making sure the drive is stable. Once you have tuned the drive, you can increase this to 75A to get some performance. Note that above 75A, you must change the current amplifier gains. You do this by requesting a different current range. i.e. for 90A on M0: 'odrv0.axis0.motor.config.requested_current_range = 90' [A], then save the configeration and reboot as the gains are written out to the DRV (MOSFET driver) only during startup.
* Note: The motor current and the current drawn from the power supply is not the same in general. You should not look at the power supply current to see what is going on with the motor current.
<details><summary markdown="span">Ok so tell me how it actually works then...</summary><div markdown="block">
The current in the motor is only connected to the current in the power supply _sometimes_ and other times it just cycles out of one phase and back in the other. This is what the modulation magnitude is (sometimes people call this duty cycle, but that's a bit confusing because we use SVM not straight PWM). When the modulation magnitude is 0, the average voltage seen across the motor phases is 0, and the motor current is never connected to the power supply. When the magnitude is 100%, it is always connected, and at 50% it's connected half the time, and cycled in just the motor half the time.
@@ -171,7 +171,7 @@ For instance, to set the current limit of M0 to 10A you would type: `odrv0.axis0
The largest effect on modulation magnitude is speed. There are other smaller factors, but in general: if the motor is still it's not unreasonable to have 50A in the motor from 5A on the power supply. When the motor is spinning close to top speed, the power supply current and the motor current will be somewhat close to each other.
</div></details>
* The velocity limit: `odrv0.axis0.controller.config.vel_limit` [counts/s]. The motor will be limited to this speed; again the default value is quite slow.
* You can change `odrv0.axis0.motor.config.calibration_current` [A] to the largest value you feel comfortable leaving running through the motor continously when the motor is stationary.
* You can change `odrv0.axis0.motor.config.calibration_current` [A] to the largest value you feel comfortable leaving running through the motor continously when the motor is stationary. If you are using a small motor (i.e. 15A current rated) you may need to reduce `calibration_current` to a value smaller than the default.
### 2. Set other hardware parameters:
@@ -197,8 +197,11 @@ For instance, to set the current limit of M0 to 10A you would type: `odrv0.axis0
### 3. Save configuration.
You can save all `.config` parameters to persistent memory such that the ODrive remembers them between power cycles.
* `odrv0.save_configuration()` <kbd>Enter</kbd>
You can save all `.config` parameters to persistent memory such that the ODrive remembers them between power cycles.
* `odrv0.save_configuration()` <kbd>Enter</kbd>.
Due to a [known issue](https://github.com/madcowswe/ODrive/issues/183) it is strongly recommended that you reboot following every save of your configuration using `odrv0.reboot()`.
## Position control of M0
@@ -233,7 +236,7 @@ The ODrive also supports velocity control and current (torque) control.
You can now:
* See what other [commands and parameters](commands.md) are available, including setting tuning parameters for better performance.
* Control the ODrive from your own program or hook it up to an existing system through one of it's [interfaces](interfaces).
* Control the ODrive from your own program or hook it up to an existing system through one of it's [interfaces](interfaces.md).
* See how you can improve the behavior during the startup procedure, like [bypassing encoder calibration](encoders.md#encoder-with-index-signal).
If you have any issues or any questions please get in touch. The [ODrive Community](https://discourse.odriverobotics.com/) warmly welcomes you.
+1
View File
@@ -33,6 +33,7 @@ Lets also start in velocity control mode since that is probably what you want fo
odrv0.axis0.encoder.config.bandwidth = 100
odrv0.axis0.controller.config.pos_gain = 1
odrv0.axis0.controller.config.vel_gain = 0.02
odrv0.axis0.controller.config.vel_integrator_gain = 0.1
odrv0.axis0.controller.config.vel_limit = 1000
odrv0.axis0.controller.config.control_mode = CTRL_MODE_VELOCITY_CONTROL
```
+78 -1
View File
@@ -77,13 +77,45 @@ To compile firmware from source, refer to the [developer guide](developer-guide)
* If the DFU script can't find the device, try forcing it into DFU mode.
<details><summary markdown="span">How to force DFU mode (ODrive v3.5)</summary><div markdown="block">
Flick the DIP switch that "DFU, RUN" to "DFU" and power cycle the board. If that alone doesn't work, also connect the After you're done, put the switch back into the "RUN" position and power cycle the board again.
Flick the DIP switch that "DFU, RUN" to "DFU" and power cycle the board. After you're done, put the switch back into the "RUN" position and power cycle the board again.
</div></details>
<details><summary markdown="span">How to force DFU mode (ODrive v3.1, v3.2)</summary><div markdown="block">
Connect the pin "BOOT0" to "3.3V" and power cycle the board. If that alone doesn't work, also connect the pin "GPIO1" to "GND". After you're done, remove the wires and power cycle the board again.
</div></details>
### Upgrading firmware with a different DFU tool
Some people have had issues using the python dfu tool, so below is a guide on how to manually use a different tool.
Before starting the below steps, you need to get firmware binary. You can download one of the officially released firmware files from [here](https://github.com/madcowswe/ODrive/releases). Make sure you select the file that matches your board version, and that you get the __.hex__ file (not the __.elf__ file).
To compile firmware from source, refer to the [developer guide](developer-guide).
#### Windows
You can use the DfuSe app from ST.
1. Download the tool [here](https://www.st.com/en/development-tools/stsw-stm32080.html). Unfortunately they make you create a login to download. Sorry about that.
1. After installing the tool, launch `DfuFileMgr.exe` which probably got added to the start menu as "Dfu file manager".
1. Select "I want to GENERATE a DFU file from S19, HEX or BIN files", press OK.
1. Click the button that says "S19 or Hex...", find the `ODriveFirmware.hex` file you built or downloaded.
1. Leave all the other settings as default and click the "Generate..." button.
1. Save the output file as `ODriveFirmware.dfu`. Note that the success message has a warning sign for some reason...
1. Launch `DfuSeDemo.exe` which probably got added to the start menu as "DfuSeDemo".
1. Force the ODrive into DFU mode, as per the instructions above "How to force DFU mode".
1. In the top left it should now be connected to "STM Device in DFU Mode".
1. If it doesn't appear, it may be because the driver is set to libusb by Zadig. We need to set it back to the original driver. Follow [these instructions](https://github.com/pbatard/libwdi/wiki/FAQ#Help_Zadig_replaced_the_driver_for_the_wrong_device_How_do_I_restore_it).
1. In the bottom right section called "Upgrade or Verify Action" click the button "Choose...".
1. Locate the `ODriveFirmware.dfu` we made before.
1. Click button "Upgrade".
1. If you get a warning that it's not possible to check that it's the correct device type: click yes to continue.
1. Congratulations your ODrive should now be flashed; you can now quit DfuSeDemo.
1. Turn off the power to the ODrive and set the DIP switch back to RUN mode.
#### MacOS or Linux
**This section needs more detail. Please consider adding detail if you got it to work.**
You may be able to use [dfu-util](http://dfu-util.sourceforge.net/) to upgrade the firmware. You will need to convert the .hex file to a .dfu file. You may be able to do it with the python script [dfu-convert](https://github.com/plietar/dfuse-tool/blob/master/dfu-convert) or the c program [hex2dfu](https://github.com/encedo/hex2dfu).
You probably need to force DFU mode, as per the instructions above.
## Flashing with an STLink
@@ -131,3 +163,48 @@ adapter speed: 2000 kHz
```
If something doesn't work, make sure `openocd` is in your `PATH` variable, check that the wires are connected properly and try with elevated privileges.
## Liveplotter
Liveplotter is used for the graphical plotting of odrive parameters (i.e. position) in real time. To start liveplotter, close any other instances of liveplotter and run `odrivetool liveplotter` from a new anaconda prompt window. By defult two parameters are plotted on startup; the encoder positon of axis 1 and axis 2. In the below example the motors are running in `closed_loop_control` while they are being forced off position by hand.
![Liveplotter position plot](figure_1.png)
To change what parameters are plotted open odrivetool (located in Anaconda3\Scripts or ODrive-master\tools) with a text editor and modify the liveplotter function:
```
# If you want to plot different values, change them here.
# You can plot any number of values concurrently.
cancellation_token = start_liveplotter(lambda: [
my_odrive.axis0.encoder.pos_estimate,
my_odrive.axis1.encoder.pos_estimate,
])
```
For example, to plot the approximate motor torque [N.cm] and the velocity [RPM] of axis1 with a 150KV motor and an 8192 count per rotation econder you would modify the function to read:
```
# If you want to plot different values, change them here.
# You can plot any number of values concurrently.
cancellation_token = start_liveplotter(lambda: [
(((my_odrive.axis0.encoder.pll_vel)/8192)*60), # 8192 CPR encoder
((8.27*my_odrive.axis0.motor.current_control.Iq_setpoint/150) * 100), # Torque [N.cm] = (8.27 * Current [A] / KV) * 100
])
```
In the example below the motor is forced off axis by hand and held there. In response the motor controller increases the torque (orange line) to counteract this disturbance up to a peak of 500 N.cm at which point the motor current limit is reached. When the motor is released it returns back to its commanded position very quickly as can be seen by the spike in the motor velocity (blue line).
![Liveplotter torque vel plot](figure_1-1.png)
To change the scale and sample rate of the plot modify the following parameters located at the beginning of utils.py (located in Anaconda3\Lib\site-packages\odrive):
```
data_rate = 100
plot_rate = 10
num_samples = 1000
```
For more examples on how to interact with the plotting functinality refer to the [Matplotlib examples.](https://matplotlib.org/examples)
### Liveplotter from interactive odrivetool instance
You can also run `start_liveplotter(...)` directly from the interactive odrivetool prompt. This is useful if you want to issue commands or otherwise keep interacting with the odrive while plotting.
For example you can type the following directly into the interactive prompt: `start_liveplotter(lambda: [odrv0.axis0.encoder.pos_estimate])`. Just like the examples above, you can list several parameters to plot separated by comma in the square brackets.
In general, you can plot any variable that you are able to read like normal in odrivetool.
+12 -12
View File
@@ -195,10 +195,10 @@ class AxisTest(ABC):
def check_preconditions(self, axis_ctx: AxisTestContext, logger):
test_assert_no_error(axis_ctx)
test_assert_eq(axis_ctx.handle.current_state, AXIS_STATE_IDLE)
if (abs(axis_ctx.handle.encoder.pll_vel) > 100):
if (abs(axis_ctx.handle.encoder.vel_estimate) > 100):
logger.warn("axis still in motion, delaying 2 sec...")
time.sleep(2)
test_assert_eq(axis_ctx.handle.encoder.pll_vel, 0, range=500)
test_assert_eq(axis_ctx.handle.encoder.vel_estimate, 0, range=500)
test_assert_eq(axis_ctx.odrv_ctx.handle.config.dc_bus_undervoltage_trip_level, axis_ctx.odrv_ctx.yaml['vbus-voltage'] * 0.85, accuracy=0.001)
test_assert_eq(axis_ctx.odrv_ctx.handle.config.dc_bus_overvoltage_trip_level, axis_ctx.odrv_ctx.yaml['vbus-voltage'] * 1.08, accuracy=0.001)
#test_assert_eq(axis_ctx.odrv_ctx.handle.config.dc_bus_undervoltage_trip_level, axis_ctx.odrv_ctx.yaml['vbus-voltage'] * 0.96, accuracy=0.001)
@@ -218,11 +218,11 @@ class DualAxisTest(ABC):
test_assert_no_error(axis1_ctx)
test_assert_eq(axis0_ctx.handle.current_state, AXIS_STATE_IDLE)
test_assert_eq(axis1_ctx.handle.current_state, AXIS_STATE_IDLE)
if (abs(axis0_ctx.handle.encoder.pll_vel) > 100) or (abs(axis1_ctx.handle.encoder.pll_vel) > 100):
if (abs(axis0_ctx.handle.encoder.vel_estimate) > 100) or (abs(axis1_ctx.handle.encoder.vel_estimate) > 100):
logger.warn("some axis still in motion, delaying 2 sec...")
time.sleep(2)
test_assert_eq(axis0_ctx.handle.encoder.pll_vel, 0, range=500)
test_assert_eq(axis1_ctx.handle.encoder.pll_vel, 0, range=500)
test_assert_eq(axis0_ctx.handle.encoder.vel_estimate, 0, range=500)
test_assert_eq(axis1_ctx.handle.encoder.vel_estimate, 0, range=500)
@abc.abstractmethod
def run_test(self, axis0_ctx: AxisTestContext, axis1_ctx: AxisTestContext, logger):
@@ -390,11 +390,11 @@ class TestClosedLoopControl(AxisTest):
axis_ctx.handle.controller.set_pos_setpoint(50000, 0, 0)
axis_ctx.handle.controller.config.vel_limit = 40000
time.sleep(0.3)
test_assert_eq(axis_ctx.handle.encoder.pll_vel, 40000, range=4000)
test_assert_eq(axis_ctx.handle.encoder.vel_estimate, 40000, range=4000)
expected_sensorless_estimation = 40000 * 2 * math.pi / axis_ctx.yaml['encoder-cpr'] * axis_ctx.yaml['motor-pole-pairs']
test_assert_eq(axis_ctx.handle.sensorless_estimator.pll_vel, expected_sensorless_estimation, range=50)
test_assert_eq(axis_ctx.handle.sensorless_estimator.vel_estimate, expected_sensorless_estimation, range=50)
time.sleep(3)
test_assert_eq(axis_ctx.handle.encoder.pll_vel, 0, range=1000)
test_assert_eq(axis_ctx.handle.encoder.vel_estimate, 0, range=1000)
time.sleep(0.5)
request_state(axis_ctx, AXIS_STATE_IDLE)
@@ -494,7 +494,7 @@ class TestHighVelocity(AxisTest):
# set and measure velocity
axis_ctx.handle.controller.set_vel_setpoint(vel_setpoint, 0)
measured_vel = axis_ctx.handle.encoder.pll_vel
measured_vel = axis_ctx.handle.encoder.vel_estimate
max_measured_vel = max(measured_vel, max_measured_vel)
test_assert_eq(measured_vel, expected_velocity, range=vel_range)
test_assert_no_error(axis_ctx)
@@ -512,10 +512,10 @@ class TestHighVelocity(AxisTest):
axis_ctx.handle.controller.set_vel_setpoint(0, 0)
time.sleep(0.5)
# If the velocity integrator at work, it may now work against slowing down.
test_assert_eq(axis_ctx.handle.encoder.pll_vel, 0, range=rated_limit*0.3)
test_assert_eq(axis_ctx.handle.encoder.vel_estimate, 0, range=rated_limit*0.3)
# TODO: this is not a good bound, but the encoder float resolution results in a bad velocity estimate after this many turns
time.sleep(0.5)
test_assert_eq(axis_ctx.handle.encoder.pll_vel, 0, range=2000)
test_assert_eq(axis_ctx.handle.encoder.vel_estimate, 0, range=2000)
request_state(axis_ctx, AXIS_STATE_IDLE)
test_assert_no_error(axis_ctx)
@@ -775,6 +775,6 @@ class TestSensorlessControl(AxisTest):
request_state(axis_ctx, AXIS_STATE_SENSORLESS_CONTROL)
# wait for spinup
time.sleep(2)
test_assert_eq(odrv0.axis0.encoder.pll_vel, target_vel, range=2000)
test_assert_eq(odrv0.axis0.encoder.vel_estimate, target_vel, range=2000)
request_state(axis_ctx, AXIS_STATE_IDLE)
+1 -1
View File
@@ -116,7 +116,7 @@ def rate_test(device):
numFrames = 10000
vals = []
for _ in range(numFrames):
vals.append(device.motor0.loop_counter)
vals.append(device.axis0.loop_counter)
plt.plot(vals)