From c07279d0e75a3305d5b8eb0936f2546cb0f03b41 Mon Sep 17 00:00:00 2001 From: Namlak Date: Sun, 29 Jul 2018 20:40:24 -0700 Subject: [PATCH 01/32] Update getting-started.md Fixed link to Interfaces, added .md --- docs/getting-started.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index ee221541..b0bbb489 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -230,7 +230,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. From 6b723f5da102b2a66d65a3f0f4209c29de19f41d Mon Sep 17 00:00:00 2001 From: metanoic <5554798+metanoic@users.noreply.github.com> Date: Sun, 5 Aug 2018 10:57:02 -0500 Subject: [PATCH 02/32] It's August. =P --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d801de25..9795d51d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ 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.2] - 2018-08-04 ### Added * Hall sensor feedback * Configurable RC PWM input From 9dcabb7b994ab96c1522a8c6a88688e8d6d66392 Mon Sep 17 00:00:00 2001 From: Oskar Weigl Date: Mon, 6 Aug 2018 13:37:22 -0700 Subject: [PATCH 03/32] brake duty saturation error reporting --- Firmware/MotorControl/low_level.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Firmware/MotorControl/low_level.cpp b/Firmware/MotorControl/low_level.cpp index 43e59bd0..ffeeeadd 100644 --- a/Firmware/MotorControl/low_level.cpp +++ b/Firmware/MotorControl/low_level.cpp @@ -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); } } From cd06834ade73fb552add8daeadb35a592f71337c Mon Sep 17 00:00:00 2001 From: Oskar Weigl Date: Mon, 6 Aug 2018 17:55:10 -0700 Subject: [PATCH 04/32] Update odrivetool.md --- docs/odrivetool.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/odrivetool.md b/docs/odrivetool.md index d3c4b67a..375df993 100644 --- a/docs/odrivetool.md +++ b/docs/odrivetool.md @@ -77,7 +77,7 @@ 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.
How to force DFU mode (ODrive v3.5)
- 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.
How to force DFU mode (ODrive v3.1, v3.2)
From 3089069f1def939d4c6b252124195641708852d6 Mon Sep 17 00:00:00 2001 From: Capo01 <503426+Capo01@users.noreply.github.com> Date: Sat, 11 Aug 2018 14:50:10 +1000 Subject: [PATCH 05/32] Updated: how to increase amplifier gains --- docs/getting-started.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index 563b172f..ae2bef2b 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -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` Enter
-* 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 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.
Ok so tell me how it actually works then...
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. From ada46be933f276e8ea6fe4a512cebb720512397b Mon Sep 17 00:00:00 2001 From: Oskar Weigl Date: Fri, 10 Aug 2018 23:16:40 -0700 Subject: [PATCH 06/32] Update getting-started.md --- docs/getting-started.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index ae2bef2b..844e99e8 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -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` Enter
-* 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 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. +* 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.
Ok so tell me how it actually works then...
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. From b9424461dd234b569a1fa18109aeb3246a3e451b Mon Sep 17 00:00:00 2001 From: Capo01 <503426+Capo01@users.noreply.github.com> Date: Tue, 14 Aug 2018 19:45:53 +1000 Subject: [PATCH 07/32] Added position, velocity and current definitions --- docs/commands.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/commands.md b/docs/commands.md index 0ec87a36..177e57cf 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -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 `.encoder.pos_estimate` [counts] +* View rotational velocity with `.encoder.ppl_vel` [counts/s] + +### Motor current and torque estimation +* View the commanded motor current with `.motor.current_control.Iq_setpoint` [A] +* View the measured motor current with `.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 From 81691eecbd7e8003de6ada652427a6367445cc6a Mon Sep 17 00:00:00 2001 From: Oskar Weigl Date: Tue, 14 Aug 2018 16:44:37 -0700 Subject: [PATCH 08/32] Update commands.md --- docs/commands.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/commands.md b/docs/commands.md index 177e57cf..16710ab0 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -75,7 +75,7 @@ An upcoming feature will enable automatic tuning. Until then, here is a rough tu ### Encoder position and velocity * View encoder position with `.encoder.pos_estimate` [counts] -* View rotational velocity with `.encoder.ppl_vel` [counts/s] +* View rotational velocity with `.encoder.pll_vel` [counts/s] ### Motor current and torque estimation * View the commanded motor current with `.motor.current_control.Iq_setpoint` [A] From bdc175317e8482a04c2fc2bcde47f4c068aea989 Mon Sep 17 00:00:00 2001 From: Capo01 <503426+Capo01@users.noreply.github.com> Date: Wed, 15 Aug 2018 21:30:13 +1000 Subject: [PATCH 09/32] Added a brief overview of liveplotter --- docs/odrivetool.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/docs/odrivetool.md b/docs/odrivetool.md index 375df993..f1737d72 100644 --- a/docs/odrivetool.md +++ b/docs/odrivetool.md @@ -131,3 +131,39 @@ 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 open `closed_loop_control` while they are being forced off position by hand. + +Image here + +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). + +Image here + +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) From 8ffc4003540ea5ce1112a68af81d2dee332abdac Mon Sep 17 00:00:00 2001 From: Capo01 <503426+Capo01@users.noreply.github.com> Date: Wed, 15 Aug 2018 21:31:48 +1000 Subject: [PATCH 10/32] images for liveplotter overview --- docs/figure_1-1.png | Bin 0 -> 21221 bytes docs/figure_1.png | Bin 0 -> 28413 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/figure_1-1.png create mode 100644 docs/figure_1.png diff --git a/docs/figure_1-1.png b/docs/figure_1-1.png new file mode 100644 index 0000000000000000000000000000000000000000..036ca2625a1c0eaa67659c4eb98116eb429470c9 GIT binary patch literal 21221 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∾Q^D2gSX35d=oJ-$tf~D~k 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 Hb;N%GUqUJF literal 0 HcmV?d00001 From 707a135bbfb84075405dfaf3a41e46ec7c1ceb07 Mon Sep 17 00:00:00 2001 From: Capo01 <503426+Capo01@users.noreply.github.com> Date: Wed, 15 Aug 2018 21:37:14 +1000 Subject: [PATCH 11/32] added figures --- docs/odrivetool.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/odrivetool.md b/docs/odrivetool.md index f1737d72..fd2b0878 100644 --- a/docs/odrivetool.md +++ b/docs/odrivetool.md @@ -136,7 +136,7 @@ If something doesn't work, make sure `openocd` is in your `PATH` variable, check 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 open `closed_loop_control` while they are being forced off position by hand. -Image here +![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: ``` @@ -158,12 +158,14 @@ For example, to plot the approximate motor torque [N.cm] and the velocity [RPM] ``` 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). -Image here +![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 +``` +data_rate = 100 plot_rate = 10 -num_samples = 1000` +num_samples = 1000 +``` For more examples on how to interact with the plotting functinality refer to the [Matplotlib examples.](https://matplotlib.org/examples) From 23ab2af36f9af30bf48cad64c60787821b73be68 Mon Sep 17 00:00:00 2001 From: Oskar Weigl Date: Thu, 16 Aug 2018 15:16:39 -0700 Subject: [PATCH 12/32] Update odrivetool.md --- docs/odrivetool.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/odrivetool.md b/docs/odrivetool.md index fd2b0878..90e71ee3 100644 --- a/docs/odrivetool.md +++ b/docs/odrivetool.md @@ -134,7 +134,7 @@ If something doesn't work, make sure `openocd` is in your `PATH` variable, check ## 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 open `closed_loop_control` while they are being forced off position by hand. +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) @@ -169,3 +169,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. + From dc717715dcfeb2e7b6510734f0841adf6b2e6d4c Mon Sep 17 00:00:00 2001 From: Capo01 <503426+Capo01@users.noreply.github.com> Date: Fri, 17 Aug 2018 10:50:56 +1000 Subject: [PATCH 13/32] Replaced motor0 with axis0 in rate_test function --- tools/odrive/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/odrive/utils.py b/tools/odrive/utils.py index a69c9ca1..45249262 100755 --- a/tools/odrive/utils.py +++ b/tools/odrive/utils.py @@ -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) From 19e3c779aefdf1cda17ad748a2c87a05812741b5 Mon Sep 17 00:00:00 2001 From: Oskar Weigl Date: Fri, 17 Aug 2018 23:39:43 -0700 Subject: [PATCH 14/32] force offset cal after idx search, index now homes axis --- CHANGELOG.md | 10 ++++++ Firmware/MotorControl/axis.cpp | 30 +++++++++++----- Firmware/MotorControl/axis.hpp | 11 ++++-- Firmware/MotorControl/encoder.cpp | 58 ++++++++++++++++++------------- Firmware/MotorControl/encoder.hpp | 8 ++--- 5 files changed, 77 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9795d51d..d885d6e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,16 @@ # Unreleased Features Please add a note of your changes below this heading if you make a Pull Request. +## Unreleased +### 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) + +### Fixed +* Once you got an axis error `ERROR_INVALID_STATE` you could never clear it + # Releases ## [0.4.2] - 2018-08-04 ### Added diff --git a/Firmware/MotorControl/axis.cpp b/Firmware/MotorControl/axis.cpp index 270ab15f..b5c7fbe5 100644 --- a/Firmware/MotorControl/axis.cpp +++ b/Firmware/MotorControl/axis.cpp @@ -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. @@ -178,7 +191,7 @@ bool Axis::run_sensorless_control_loop() { return true; }); set_step_dir_enabled(false); - return error_ == ERROR_NONE; + return check_for_errors(); } bool Axis::run_closed_loop_control_loop() { @@ -193,7 +206,7 @@ bool Axis::run_closed_loop_control_loop() { 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] diff --git a/Firmware/MotorControl/axis.hpp b/Firmware/MotorControl/axis.hpp index cfa1bd28..111f9e26 100644 --- a/Firmware/MotorControl/axis.hpp +++ b/Firmware/MotorControl/axis.hpp @@ -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 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 diff --git a/Firmware/MotorControl/encoder.cpp b/Firmware/MotorControl/encoder.cpp index 9a6210ce..ccd6ee2d 100644 --- a/Firmware/MotorControl/encoder.cpp +++ b/Firmware/MotorControl/encoder.cpp @@ -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,17 @@ 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_linear_count(0); // Avoid position control transient after search if (config_.pre_calibrated) { - offset_ = config_.offset; is_ready_ = true; + } else { + // Invalidate offset calibration that may have happened before idx search + is_ready_ = false; } index_found_ = true; } @@ -73,8 +76,13 @@ void Encoder::set_circular_count(int32_t count) { __disable_irq(); // Offset and state must be shifted by the same amount - offset_ += count - count_in_cpr_; - offset_ = mod(offset_, config_.cpr); + // Note that if the linear count is also cleared before running an update, + // the offset will drift by at least one count. Therefore we shouldn't rely + // on this during index search callback. + // Hence we invalidate calibration in enc_index_cb + 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 +132,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 +181,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 +190,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 +222,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; } @@ -296,7 +306,7 @@ bool Encoder::update() { } //// 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; diff --git a/Firmware/MotorControl/encoder.hpp b/Firmware/MotorControl/encoder.hpp index 86bc9b73..531bbf7b 100644 --- a/Firmware/MotorControl/encoder.hpp +++ b/Firmware/MotorControl/encoder.hpp @@ -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; @@ -65,7 +65,6 @@ 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] @@ -85,7 +84,6 @@ public: make_protocol_ro_property("index_found", const_cast(&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_), From e364fc1e94dd9f093087f9ae6371948b2eb83ecb Mon Sep 17 00:00:00 2001 From: Oskar Weigl Date: Sat, 18 Aug 2018 21:14:21 -0700 Subject: [PATCH 15/32] fix char to int conversion --- .../examples/ODriveArduinoTest/ODriveArduinoTest.ino | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Arduino/ODriveArduino/examples/ODriveArduinoTest/ODriveArduinoTest.ino b/Arduino/ODriveArduino/examples/ODriveArduinoTest/ODriveArduinoTest.ino index a06c9818..f948f7f6 100644 --- a/Arduino/ODriveArduino/examples/ODriveArduinoTest/ODriveArduinoTest.ino +++ b/Arduino/ODriveArduino/examples/ODriveArduinoTest/ODriveArduinoTest.ino @@ -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 From 4c7e0e478c5b28af254fd5a3956ebd1cd8647aeb Mon Sep 17 00:00:00 2001 From: Oskar Weigl Date: Sat, 18 Aug 2018 21:14:56 -0700 Subject: [PATCH 16/32] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d885d6e2..a12219d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Please add a note of your changes below this heading if you make a Pull Request. ### 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 # Releases ## [0.4.2] - 2018-08-04 From cf9cf854a47da2c75a326242c26280fe73e2522f Mon Sep 17 00:00:00 2001 From: Oskar Weigl Date: Sat, 18 Aug 2018 22:14:54 -0700 Subject: [PATCH 17/32] added a timeout retry to send path in python protocol --- Firmware/fibre/python/fibre/protocol.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Firmware/fibre/python/fibre/protocol.py b/Firmware/fibre/python/fibre/protocol.py index 50053a11..c8814fc1 100644 --- a/Firmware/fibre/python/fibre/protocol.py +++ b/Firmware/fibre/python/fibre/protocol.py @@ -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 From 90801c887f3b90e3efaa483e60882523c7a33a2b Mon Sep 17 00:00:00 2001 From: Capo01 <503426+Capo01@users.noreply.github.com> Date: Mon, 20 Aug 2018 22:00:20 +1000 Subject: [PATCH 18/32] Mention that reboot should be called after saving a config https://github.com/madcowswe/ODrive/issues/183 --- docs/getting-started.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index 844e99e8..4bcdb482 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -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()` Enter +You can save all `.config` parameters to persistent memory such that the ODrive remembers them between power cycles. +* `odrv0.save_configuration()` Enter. + +Due to a [known issue](https://github.com/madcowswe/ODrive/issues/183) it is strongly reccomneded that you reboot following every save of your configerations using `odrv0.reboot()`. + ## Position control of M0 From 0f743939ecd243657ddc008c93bce5bb5249e0c5 Mon Sep 17 00:00:00 2001 From: Capo01 <503426+Capo01@users.noreply.github.com> Date: Thu, 23 Aug 2018 22:14:23 +1000 Subject: [PATCH 19/32] added calibration_current for small motors reference Akex_ was unable to calibrate a small 15A peak current motor. Reducing `calibration_current` from 10A to 5A resolved the issue. I have included a reference to the use of small motors and the calibration current. --- docs/getting-started.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index 844e99e8..3e0ca944 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -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.
* 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 defualt 10A. ### 2. Set other hardware parameters: From d37cc446c851d406048e2877ea69ba80500ef243 Mon Sep 17 00:00:00 2001 From: Oskar Weigl Date: Thu, 23 Aug 2018 14:49:35 -0700 Subject: [PATCH 20/32] Update getting-started.md --- docs/getting-started.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index 4bcdb482..bf6f1f60 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -200,7 +200,7 @@ For instance, to set the current limit of M0 to 10A you would type: `odrv0.axis0 You can save all `.config` parameters to persistent memory such that the ODrive remembers them between power cycles. * `odrv0.save_configuration()` Enter. -Due to a [known issue](https://github.com/madcowswe/ODrive/issues/183) it is strongly reccomneded that you reboot following every save of your configerations using `odrv0.reboot()`. +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 From bd89f90615ace6deb5e9149c26f87aea2218d80f Mon Sep 17 00:00:00 2001 From: Oskar Weigl Date: Thu, 23 Aug 2018 14:51:04 -0700 Subject: [PATCH 21/32] Update getting-started.md --- docs/getting-started.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index 3e0ca944..8bc42c33 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -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. * 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. 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 defualt 10A. +* 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: From 14e917171d409cf6a596541af29007558ebae992 Mon Sep 17 00:00:00 2001 From: Unknown Date: Thu, 23 Aug 2018 22:26:26 -0400 Subject: [PATCH 22/32] Rename pll_vel to vel_estimate --- Firmware/MotorControl/axis.cpp | 4 ++-- Firmware/MotorControl/encoder.cpp | 14 +++++------ Firmware/MotorControl/encoder.hpp | 4 ++-- .../MotorControl/sensorless_estimator.cpp | 4 ++-- .../MotorControl/sensorless_estimator.hpp | 4 ++-- tools/odrive/tests.py | 24 +++++++++---------- 6 files changed, 27 insertions(+), 27 deletions(-) diff --git a/Firmware/MotorControl/axis.cpp b/Firmware/MotorControl/axis.cpp index b5c7fbe5..7d79b3d6 100644 --- a/Firmware/MotorControl/axis.cpp +++ b/Firmware/MotorControl/axis.cpp @@ -184,7 +184,7 @@ 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_, ¤t_setpoint)) + if (!controller_.update(sensorless_estimator_.pll_pos_, sensorless_estimator_.vel_estimate_, ¤t_setpoint)) return error_ |= ERROR_CONTROLLER_FAILED, false; if (!motor_.update(current_setpoint, sensorless_estimator_.phase_)) return false; // set_error should update axis.error_ @@ -199,7 +199,7 @@ 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_, ¤t_setpoint)) + if (!controller_.update(encoder_.pos_estimate_, encoder_.vel_estimate_, ¤t_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_ diff --git a/Firmware/MotorControl/encoder.cpp b/Firmware/MotorControl/encoder.cpp index ccd6ee2d..48d99eee 100644 --- a/Firmware/MotorControl/encoder.cpp +++ b/Firmware/MotorControl/encoder.cpp @@ -288,8 +288,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_)); @@ -298,10 +298,10 @@ 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; } @@ -316,8 +316,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; diff --git a/Firmware/MotorControl/encoder.hpp b/Firmware/MotorControl/encoder.hpp index 531bbf7b..1b3d2988 100644 --- a/Firmware/MotorControl/encoder.hpp +++ b/Firmware/MotorControl/encoder.hpp @@ -69,7 +69,7 @@ public: 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] @@ -89,7 +89,7 @@ public: 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", diff --git a/Firmware/MotorControl/sensorless_estimator.cpp b/Firmware/MotorControl/sensorless_estimator.cpp index 25210f98..43191ce3 100644 --- a/Firmware/MotorControl/sensorless_estimator.cpp +++ b/Firmware/MotorControl/sensorless_estimator.cpp @@ -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; }; diff --git a/Firmware/MotorControl/sensorless_estimator.hpp b/Firmware/MotorControl/sensorless_estimator.hpp index 77f50943..6d15820f 100644 --- a/Firmware/MotorControl/sensorless_estimator.hpp +++ b/Firmware/MotorControl/sensorless_estimator.hpp @@ -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", diff --git a/tools/odrive/tests.py b/tools/odrive/tests.py index 92dfc9b8..eeb5dcec 100644 --- a/tools/odrive/tests.py +++ b/tools/odrive/tests.py @@ -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) From 346a98b010a174ac0bf05e07d32d0070b70822f5 Mon Sep 17 00:00:00 2001 From: Oskar Weigl Date: Fri, 24 Aug 2018 15:43:23 -0700 Subject: [PATCH 23/32] update Changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a12219d7..e64319be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Please add a note of your changes below this heading if you make a Pull Request. ### 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`. ### Fixed * Once you got an axis error `ERROR_INVALID_STATE` you could never clear it From 584b73920d890f0ef6afa45f8d48594f4a73d6da Mon Sep 17 00:00:00 2001 From: Oskar Weigl Date: Fri, 24 Aug 2018 20:43:28 -0700 Subject: [PATCH 24/32] fix incorrect shifting of offset during index callback --- Firmware/MotorControl/encoder.cpp | 17 ++++++++--------- Firmware/MotorControl/encoder.hpp | 2 +- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/Firmware/MotorControl/encoder.cpp b/Firmware/MotorControl/encoder.cpp index 48d99eee..5b80ed8d 100644 --- a/Firmware/MotorControl/encoder.cpp +++ b/Firmware/MotorControl/encoder.cpp @@ -41,11 +41,13 @@ bool Encoder::do_checks(){ // 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) { 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; } @@ -70,18 +72,15 @@ 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 - // Note that if the linear count is also cleared before running an update, - // the offset will drift by at least one count. Therefore we shouldn't rely - // on this during index search callback. - // Hence we invalidate calibration in enc_index_cb - config_.offset += count - count_in_cpr_; - config_.offset = mod(config_.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); diff --git a/Firmware/MotorControl/encoder.hpp b/Firmware/MotorControl/encoder.hpp index 1b3d2988..2162d10b 100644 --- a/Firmware/MotorControl/encoder.hpp +++ b/Firmware/MotorControl/encoder.hpp @@ -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); From ed2c72d8396fbc72acb0353858a5aab31575145a Mon Sep 17 00:00:00 2001 From: Oskar Weigl Date: Sat, 18 Aug 2018 21:14:21 -0700 Subject: [PATCH 25/32] fix char to int conversion --- .../examples/ODriveArduinoTest/ODriveArduinoTest.ino | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Arduino/ODriveArduino/examples/ODriveArduinoTest/ODriveArduinoTest.ino b/Arduino/ODriveArduino/examples/ODriveArduinoTest/ODriveArduinoTest.ino index a06c9818..f948f7f6 100644 --- a/Arduino/ODriveArduino/examples/ODriveArduinoTest/ODriveArduinoTest.ino +++ b/Arduino/ODriveArduino/examples/ODriveArduinoTest/ODriveArduinoTest.ino @@ -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 From 2b86a2fa08f5e1f47119a7316b29fafe0ab130ec Mon Sep 17 00:00:00 2001 From: Oskar Weigl Date: Sun, 26 Aug 2018 13:42:08 -0700 Subject: [PATCH 26/32] Update hoverboard.md --- docs/hoverboard.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/hoverboard.md b/docs/hoverboard.md index 54cd70f5..5cf64332 100644 --- a/docs/hoverboard.md +++ b/docs/hoverboard.md @@ -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 ``` From 1d31d23d0e5297c0207b3e217d1300f4d96a0bca Mon Sep 17 00:00:00 2001 From: Oskar Weigl Date: Sun, 26 Aug 2018 13:42:32 -0700 Subject: [PATCH 27/32] Update hoverboard.md --- docs/hoverboard.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/hoverboard.md b/docs/hoverboard.md index 54cd70f5..5cf64332 100644 --- a/docs/hoverboard.md +++ b/docs/hoverboard.md @@ -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 ``` From d54d4479ac2ccce3681e20e0e364d4423760c42c Mon Sep 17 00:00:00 2001 From: Oskar Weigl Date: Mon, 27 Aug 2018 16:09:07 -0700 Subject: [PATCH 28/32] update max inductance check to 2500uH --- CHANGELOG.md | 1 + Firmware/MotorControl/motor.cpp | 2 +- Firmware/MotorControl/motor.hpp | 1 - 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e64319be..a7b7fc42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Please add a note of your changes below this heading if you make a Pull Request. ### 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 diff --git a/Firmware/MotorControl/motor.cpp b/Firmware/MotorControl/motor.cpp index d8e4764b..56817ee8 100644 --- a/Firmware/MotorControl/motor.cpp +++ b/Firmware/MotorControl/motor.cpp @@ -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; } diff --git a/Firmware/MotorControl/motor.hpp b/Firmware/MotorControl/motor.hpp index ae2cd870..07820726 100644 --- a/Firmware/MotorControl/motor.hpp +++ b/Firmware/MotorControl/motor.hpp @@ -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] From a2e0ea6374c4ad13637245f600df39de80b93123 Mon Sep 17 00:00:00 2001 From: Oskar Weigl Date: Mon, 27 Aug 2018 22:49:44 -0700 Subject: [PATCH 29/32] add DfuSeDemo tool instructions --- docs/odrivetool.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/docs/odrivetool.md b/docs/odrivetool.md index 90e71ee3..b2f8cf9e 100644 --- a/docs/odrivetool.md +++ b/docs/odrivetool.md @@ -84,6 +84,37 @@ To compile firmware from source, refer to the [developer guide](developer-guide) 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. +### 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. + +#### 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 From fb16534067f11ca7d5ab7b67af151c346fc49a96 Mon Sep 17 00:00:00 2001 From: Oskar Weigl Date: Mon, 27 Aug 2018 23:07:19 -0700 Subject: [PATCH 30/32] power cycle after dfu docs --- docs/odrivetool.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/odrivetool.md b/docs/odrivetool.md index b2f8cf9e..80d926b7 100644 --- a/docs/odrivetool.md +++ b/docs/odrivetool.md @@ -109,6 +109,7 @@ You can use the DfuSe app from ST. 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.** From b3bcdd5d5ea5fd9eb39d40885d5244812792136f Mon Sep 17 00:00:00 2001 From: Oskar Weigl Date: Wed, 29 Aug 2018 23:56:43 -0700 Subject: [PATCH 31/32] fill out sensorless guide a bit more --- docs/commands.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/commands.md b/docs/commands.md index 16710ab0..212a4899 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -100,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 / ( * ) From 71142a7c85633a6cf30e3c3e02de1530fd0ad2ab Mon Sep 17 00:00:00 2001 From: Oskar Weigl Date: Thu, 30 Aug 2018 21:49:20 -0700 Subject: [PATCH 32/32] release fw-v0.4.3 --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7b7fc42..f024cb65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ # Unreleased Features Please add a note of your changes below this heading if you make a Pull Request. -## Unreleased +# Releases +## [0.4.3] - 2018-08-30 ### Added * Encoder position count "homed" to zero when index is found. @@ -14,7 +15,6 @@ Please add a note of your changes below this heading if you make a Pull Request. * Once you got an axis error `ERROR_INVALID_STATE` you could never clear it * Char to int conversion to read motornum on arduino example -# Releases ## [0.4.2] - 2018-08-04 ### Added * Hall sensor feedback