From 83b06c4053c7793d830f405b6878ac458122216d Mon Sep 17 00:00:00 2001 From: Linus NEP <86525017+LinusNEP@users.noreply.github.com> Date: Mon, 8 May 2023 09:54:00 +0200 Subject: [PATCH] Add files via upload --- Arduino Sketches/arduino_sketches.tar.gz | Bin 0 -> 9566 bytes ODrive Calibration/ODrive_calibration.py | 272 ++++++++++++++++++ ODrive Calibration/ODrive_calibration.tar.gz | Bin 0 -> 5529 bytes .../Arduino Sketches/arduino_sketches.tar.gz | Bin 0 -> 9566 bytes .../ODrive Calibration/ODrive_calibration.py | 272 ++++++++++++++++++ .../ODrive_calibration.tar.gz | Bin 0 -> 5529 bytes 6 files changed, 544 insertions(+) create mode 100644 Arduino Sketches/arduino_sketches.tar.gz create mode 100644 ODrive Calibration/ODrive_calibration.py create mode 100644 ODrive Calibration/ODrive_calibration.tar.gz create mode 100644 Software Files/Arduino Sketches/arduino_sketches.tar.gz create mode 100644 Software Files/ODrive Calibration/ODrive_calibration.py create mode 100644 Software Files/ODrive Calibration/ODrive_calibration.tar.gz diff --git a/Arduino Sketches/arduino_sketches.tar.gz b/Arduino Sketches/arduino_sketches.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..7fe6ec19b0dab74cc4a500d7f242b46e95ab6ad0 GIT binary patch literal 9566 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 I(g5TD05M7P&Hw-a literal 0 HcmV?d00001 diff --git a/ODrive Calibration/ODrive_calibration.py b/ODrive Calibration/ODrive_calibration.py new file mode 100644 index 0000000..7aca43f --- /dev/null +++ b/ODrive Calibration/ODrive_calibration.py @@ -0,0 +1,272 @@ + + +""" +These configurations is based on Austin Owens implementation: https://github.com/AustinOwens/robodog/blob/main/odrive/configs/odrive_hoverboard_config.py + +It was based on the hoverboard tutorial found on the Odrive Robotics website at: https://docs.odriverobotics.com/v/0.5.4/hoverboard.html + +""" + +import sys +import time +import math +import odrive +from odrive.enums import * +import fibre.libfibre +from fibre import Logger, Event +from odrive.utils import OperationAbortedException + + +class MotorConfig: + """ + Class for configuring an Odrive axis for a Hoverboard motor. + Only works with one Odrive at a time. + """ + + # Hoverboard Kv + HOVERBOARD_KV = 16.0 + + # Min/Max phase inductance of motor + MIN_PHASE_INDUCTANCE = 0 + MAX_PHASE_INDUCTANCE = 0.001 + + # Min/Max phase resistance of motor + MIN_PHASE_RESISTANCE = 0 + MAX_PHASE_RESISTANCE = 0.5 + + # Tolerance for encoder offset float + ENCODER_OFFSET_FLOAT_TOLERANCE = 0.05 + + def __init__(self, axis_num): + """ + Initalizes MotorConfig class by finding odrive, erase its + configuration, and grabbing specified axis object. + + :param axis_num: Which channel/motor on the odrive your referring to. + :type axis_num: int (0 or 1) + """ + + self.axis_num = axis_num + + # Connect to Odrive + print("Looking for ODrive...") + self._find_odrive() + print("Found ODrive.") + + def _find_odrive(self): + # connect to Odrive + self.odrv = odrive.find_any() + self.odrv_axis = getattr(self.odrv, "axis{}".format(self.axis_num)) + + def configure(self): + """ + Configures the odrive device for a hoverboard motor. + """ + + # Erase pre-exsisting configuration + print("Erasing pre-exsisting configuration...") + try: + self.odrv.erase_configuration() + except ChannelBrokenException: + pass + + self._find_odrive() + + # Standard 6.5 inch hoverboard hub motors have 30 permanent magnet + # poles, and thus 15 pole pairs + self.odrv_axis.motor.config.pole_pairs = 15 + + # Hoverboard hub motors are quite high resistance compared to the hobby + # aircraft motors, so we want to use a bit higher voltage for the motor + # calibration, and set up the current sense gain to be more sensitive. + # The motors are also fairly high inductance, so we need to reduce the + # bandwidth of the current controller from the default to keep it + # stable. + self.odrv_axis.motor.config.resistance_calib_max_voltage = 4 + self.odrv_axis.motor.config.requested_current_range = 25 + self.odrv_axis.motor.config.current_control_bandwidth = 100 + + # Estimated KV but should be measured using the "drill test", which can + # be found here: # https://discourse.odriverobotics.com/t/project-hoverarm/441 + self.odrv_axis.motor.config.torque_constant = 8.27 / self.HOVERBOARD_KV + + # Hoverboard motors contain hall effect sensors instead of incremental + # encorders + self.odrv_axis.encoder.config.mode = ENCODER_MODE_HALL + + # The hall feedback has 6 states for every pole pair in the motor. Since + # we have 15 pole pairs, we set the cpr to 15*6 = 90. + self.odrv_axis.encoder.config.cpr = 90 + + # Since hall sensors are low resolution feedback, we also bump up the + #offset calibration displacement to get better calibration accuracy. + self.odrv_axis.encoder.config.calib_scan_distance = 150 + + # Since the hall feedback only has 90 counts per revolution, we want to + # reduce the velocity tracking bandwidth to get smoother velocity + # estimates. We can also set these fairly modest gains that will be a + # bit sloppy but shouldn’t shake your rig apart if it’s built poorly. + # Make sure to tune the gains up when you have everything else working + # to a stiffness that is applicable to your application. + self.odrv_axis.encoder.config.bandwidth = 100 + self.odrv_axis.controller.config.pos_gain = 1 + self.odrv_axis.controller.config.vel_gain = 0.02 * self.odrv_axis.motor.config.torque_constant * self.odrv_axis.encoder.config.cpr + self.odrv_axis.controller.config.vel_integrator_gain = 0.1 * self.odrv_axis.motor.config.torque_constant * self.odrv_axis.encoder.config.cpr + self.odrv_axis.controller.config.vel_limit = 10 + + # Set in position control mode so we can control the position of the wheel + self.odrv_axis.controller.config.control_mode = CONTROL_MODE_POSITION_CONTROL + + # In the next step we are going to start powering the motor and so we + # want to make sure that some of the above settings that require a + # reboot are applied first. + print("Saving manual configuration and rebooting...") + self.odrv.save_configuration() + print("Manual configuration saved.") + try: + self.odrv.reboot() + except ChannelBrokenException: + pass + + self._find_odrive() + + input("Make sure the motor is free to move, then press enter...") + + print("Calibrating Odrive for hoverboard motor (you should hear a " "beep)...") + + self.odrv_axis.requested_state = AXIS_STATE_MOTOR_CALIBRATION + + # Wait for calibration to take place + time.sleep(10) + + if self.odrv_axis.motor.error != 0: + print("Error: Odrive reported an error of {} while in the state " + "AXIS_STATE_MOTOR_CALIBRATION. Printing out Odrive motor data for " + "debug:\n{}".format(self.odrv_axis.motor.error, + self.odrv_axis.motor)) + + sys.exit(1) + + if self.odrv_axis.motor.config.phase_inductance <= self.MIN_PHASE_INDUCTANCE or \ + self.odrv_axis.motor.config.phase_inductance >= self.MAX_PHASE_INDUCTANCE: + print("Error: After odrive motor calibration, the phase inductance " + "is at {}, which is outside of the expected range. Either widen the " + "boundaries of MIN_PHASE_INDUCTANCE and MAX_PHASE_INDUCTANCE (which " + "is between {} and {} respectively) or debug/fix your setup. Printing " + "out Odrive motor data for debug:\n{}".format(self.odrv_axis.motor.config.phase_inductance, + self.MIN_PHASE_INDUCTANCE, + self.MAX_PHASE_INDUCTANCE, + self.odrv_axis.motor)) + + sys.exit(1) + + if self.odrv_axis.motor.config.phase_resistance <= self.MIN_PHASE_RESISTANCE or \ + self.odrv_axis.motor.config.phase_resistance >= self.MAX_PHASE_RESISTANCE: + print("Error: After odrive motor calibration, the phase resistance " + "is at {}, which is outside of the expected range. Either raise the " + "MAX_PHASE_RESISTANCE (which is between {} and {} respectively) or " + "debug/fix your setup. Printing out Odrive motor data for " + "debug:\n{}".format(self.odrv_axis.motor.config.phase_resistance, + self.MIN_PHASE_RESISTANCE, + self.MAX_PHASE_RESISTANCE, + self.odrv_axis.motor)) + + sys.exit(1) + + # If all looks good, then lets tell ODrive that saving this calibration + # to persistent memory is OK + self.odrv_axis.motor.config.pre_calibrated = True + + # Check the alignment between the motor and the hall sensor. Because of + # this step you are allowed to plug the motor phases in random order and + # also the hall signals can be random. Just don’t change it after + # calibration. + print("Calibrating Odrive for encoder...") + self.odrv_axis.requested_state = AXIS_STATE_ENCODER_OFFSET_CALIBRATION + + # Wait for calibration to take place + time.sleep(30) + + if self.odrv_axis.encoder.error != 0: + print("Error: Odrive reported an error of {} while in the state " + "AXIS_STATE_ENCODER_OFFSET_CALIBRATION. Printing out Odrive encoder " + "data for debug:\n{}".format(self.odrv_axis.encoder.error, + self.odrv_axis.encoder)) + + sys.exit(1) + + # If offset_float isn't 0.5 within some tolerance, or its not 1.5 within + # some tolerance, raise an error + if not ((self.odrv_axis.encoder.config.offset_float > 0.5 - self.ENCODER_OFFSET_FLOAT_TOLERANCE and \ + self.odrv_axis.encoder.config.offset_float < 0.5 + self.ENCODER_OFFSET_FLOAT_TOLERANCE) or \ + (self.odrv_axis.encoder.config.offset_float > 1.5 - self.ENCODER_OFFSET_FLOAT_TOLERANCE and \ + self.odrv_axis.encoder.config.offset_float < 1.5 + self.ENCODER_OFFSET_FLOAT_TOLERANCE)): + print("Error: After odrive encoder calibration, the 'offset_float' " + "is at {}, which is outside of the expected range. 'offset_float' " + "should be close to 0.5 or 1.5 with a tolerance of {}. Either " + "increase the tolerance or debug/fix your setup. Printing out " + "Odrive encoder data for debug:\n{}".format(self.odrv_axis.encoder.config.offset_float, + self.ENCODER_OFFSET_FLOAT_TOLERANCE, + self.odrv_axis.encoder)) + + sys.exit(1) + + # If all looks good, then lets tell ODrive that saving this calibration + # to persistent memory is OK + self.odrv_axis.encoder.config.pre_calibrated = True + + print("Saving calibration configuration and rebooting...") + self.odrv.save_configuration() + print("Calibration configuration saved.") + try: + self.odrv.reboot() + except ChannelBrokenException: + pass + + self._find_odrive() + + print("Odrive configuration finished.") + + def mode_idle(self): + """ + Puts the motor in idle (i.e. can move freely). + """ + + self.odrv_axis.requested_state = AXIS_STATE_IDLE + + def mode_close_loop_control(self): + """ + Puts the motor in closed loop control. + """ + + self.odrv_axis.requested_state = AXIS_STATE_CLOSED_LOOP_CONTROL + + def move_input_pos(self, angle): + """ + Puts the motor at a certain angle. + + :param angle: Angle you want the motor to move. + :type angle: int or float + """ + + self.odrv_axis.controller.input_pos = angle/360.0 + +if __name__ == "__main__": + hb_motor_config = MotorConfig(axis_num = 0) + hb_motor_config.configure() + + print("CONDUCTING MOTOR TEST") + print("Placing motor in close loop control. If you move motor, motor will " + "resist you.") + hb_motor_config.mode_close_loop_control() + + # Go from 0 to 360 degrees in increments of 30 degrees + for angle in range(0, 390, 30): + print("Setting motor to {} degrees.".format(angle)) + hb_motor_config.move_input_pos(angle) + time.sleep(5) + + print("Placing motor in idle. If you move motor, motor will " + "move freely") + hb_motor_config.mode_idle() + diff --git a/ODrive Calibration/ODrive_calibration.tar.gz b/ODrive Calibration/ODrive_calibration.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..fc661dbdb15804d002e89726689a13f5031dc172 GIT binary patch literal 5529 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 literal 0 HcmV?d00001 diff --git a/Software Files/Arduino Sketches/arduino_sketches.tar.gz b/Software Files/Arduino Sketches/arduino_sketches.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..7fe6ec19b0dab74cc4a500d7f242b46e95ab6ad0 GIT binary patch literal 9566 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 I(g5TD05M7P&Hw-a literal 0 HcmV?d00001 diff --git a/Software Files/ODrive Calibration/ODrive_calibration.py b/Software Files/ODrive Calibration/ODrive_calibration.py new file mode 100644 index 0000000..7aca43f --- /dev/null +++ b/Software Files/ODrive Calibration/ODrive_calibration.py @@ -0,0 +1,272 @@ + + +""" +These configurations is based on Austin Owens implementation: https://github.com/AustinOwens/robodog/blob/main/odrive/configs/odrive_hoverboard_config.py + +It was based on the hoverboard tutorial found on the Odrive Robotics website at: https://docs.odriverobotics.com/v/0.5.4/hoverboard.html + +""" + +import sys +import time +import math +import odrive +from odrive.enums import * +import fibre.libfibre +from fibre import Logger, Event +from odrive.utils import OperationAbortedException + + +class MotorConfig: + """ + Class for configuring an Odrive axis for a Hoverboard motor. + Only works with one Odrive at a time. + """ + + # Hoverboard Kv + HOVERBOARD_KV = 16.0 + + # Min/Max phase inductance of motor + MIN_PHASE_INDUCTANCE = 0 + MAX_PHASE_INDUCTANCE = 0.001 + + # Min/Max phase resistance of motor + MIN_PHASE_RESISTANCE = 0 + MAX_PHASE_RESISTANCE = 0.5 + + # Tolerance for encoder offset float + ENCODER_OFFSET_FLOAT_TOLERANCE = 0.05 + + def __init__(self, axis_num): + """ + Initalizes MotorConfig class by finding odrive, erase its + configuration, and grabbing specified axis object. + + :param axis_num: Which channel/motor on the odrive your referring to. + :type axis_num: int (0 or 1) + """ + + self.axis_num = axis_num + + # Connect to Odrive + print("Looking for ODrive...") + self._find_odrive() + print("Found ODrive.") + + def _find_odrive(self): + # connect to Odrive + self.odrv = odrive.find_any() + self.odrv_axis = getattr(self.odrv, "axis{}".format(self.axis_num)) + + def configure(self): + """ + Configures the odrive device for a hoverboard motor. + """ + + # Erase pre-exsisting configuration + print("Erasing pre-exsisting configuration...") + try: + self.odrv.erase_configuration() + except ChannelBrokenException: + pass + + self._find_odrive() + + # Standard 6.5 inch hoverboard hub motors have 30 permanent magnet + # poles, and thus 15 pole pairs + self.odrv_axis.motor.config.pole_pairs = 15 + + # Hoverboard hub motors are quite high resistance compared to the hobby + # aircraft motors, so we want to use a bit higher voltage for the motor + # calibration, and set up the current sense gain to be more sensitive. + # The motors are also fairly high inductance, so we need to reduce the + # bandwidth of the current controller from the default to keep it + # stable. + self.odrv_axis.motor.config.resistance_calib_max_voltage = 4 + self.odrv_axis.motor.config.requested_current_range = 25 + self.odrv_axis.motor.config.current_control_bandwidth = 100 + + # Estimated KV but should be measured using the "drill test", which can + # be found here: # https://discourse.odriverobotics.com/t/project-hoverarm/441 + self.odrv_axis.motor.config.torque_constant = 8.27 / self.HOVERBOARD_KV + + # Hoverboard motors contain hall effect sensors instead of incremental + # encorders + self.odrv_axis.encoder.config.mode = ENCODER_MODE_HALL + + # The hall feedback has 6 states for every pole pair in the motor. Since + # we have 15 pole pairs, we set the cpr to 15*6 = 90. + self.odrv_axis.encoder.config.cpr = 90 + + # Since hall sensors are low resolution feedback, we also bump up the + #offset calibration displacement to get better calibration accuracy. + self.odrv_axis.encoder.config.calib_scan_distance = 150 + + # Since the hall feedback only has 90 counts per revolution, we want to + # reduce the velocity tracking bandwidth to get smoother velocity + # estimates. We can also set these fairly modest gains that will be a + # bit sloppy but shouldn’t shake your rig apart if it’s built poorly. + # Make sure to tune the gains up when you have everything else working + # to a stiffness that is applicable to your application. + self.odrv_axis.encoder.config.bandwidth = 100 + self.odrv_axis.controller.config.pos_gain = 1 + self.odrv_axis.controller.config.vel_gain = 0.02 * self.odrv_axis.motor.config.torque_constant * self.odrv_axis.encoder.config.cpr + self.odrv_axis.controller.config.vel_integrator_gain = 0.1 * self.odrv_axis.motor.config.torque_constant * self.odrv_axis.encoder.config.cpr + self.odrv_axis.controller.config.vel_limit = 10 + + # Set in position control mode so we can control the position of the wheel + self.odrv_axis.controller.config.control_mode = CONTROL_MODE_POSITION_CONTROL + + # In the next step we are going to start powering the motor and so we + # want to make sure that some of the above settings that require a + # reboot are applied first. + print("Saving manual configuration and rebooting...") + self.odrv.save_configuration() + print("Manual configuration saved.") + try: + self.odrv.reboot() + except ChannelBrokenException: + pass + + self._find_odrive() + + input("Make sure the motor is free to move, then press enter...") + + print("Calibrating Odrive for hoverboard motor (you should hear a " "beep)...") + + self.odrv_axis.requested_state = AXIS_STATE_MOTOR_CALIBRATION + + # Wait for calibration to take place + time.sleep(10) + + if self.odrv_axis.motor.error != 0: + print("Error: Odrive reported an error of {} while in the state " + "AXIS_STATE_MOTOR_CALIBRATION. Printing out Odrive motor data for " + "debug:\n{}".format(self.odrv_axis.motor.error, + self.odrv_axis.motor)) + + sys.exit(1) + + if self.odrv_axis.motor.config.phase_inductance <= self.MIN_PHASE_INDUCTANCE or \ + self.odrv_axis.motor.config.phase_inductance >= self.MAX_PHASE_INDUCTANCE: + print("Error: After odrive motor calibration, the phase inductance " + "is at {}, which is outside of the expected range. Either widen the " + "boundaries of MIN_PHASE_INDUCTANCE and MAX_PHASE_INDUCTANCE (which " + "is between {} and {} respectively) or debug/fix your setup. Printing " + "out Odrive motor data for debug:\n{}".format(self.odrv_axis.motor.config.phase_inductance, + self.MIN_PHASE_INDUCTANCE, + self.MAX_PHASE_INDUCTANCE, + self.odrv_axis.motor)) + + sys.exit(1) + + if self.odrv_axis.motor.config.phase_resistance <= self.MIN_PHASE_RESISTANCE or \ + self.odrv_axis.motor.config.phase_resistance >= self.MAX_PHASE_RESISTANCE: + print("Error: After odrive motor calibration, the phase resistance " + "is at {}, which is outside of the expected range. Either raise the " + "MAX_PHASE_RESISTANCE (which is between {} and {} respectively) or " + "debug/fix your setup. Printing out Odrive motor data for " + "debug:\n{}".format(self.odrv_axis.motor.config.phase_resistance, + self.MIN_PHASE_RESISTANCE, + self.MAX_PHASE_RESISTANCE, + self.odrv_axis.motor)) + + sys.exit(1) + + # If all looks good, then lets tell ODrive that saving this calibration + # to persistent memory is OK + self.odrv_axis.motor.config.pre_calibrated = True + + # Check the alignment between the motor and the hall sensor. Because of + # this step you are allowed to plug the motor phases in random order and + # also the hall signals can be random. Just don’t change it after + # calibration. + print("Calibrating Odrive for encoder...") + self.odrv_axis.requested_state = AXIS_STATE_ENCODER_OFFSET_CALIBRATION + + # Wait for calibration to take place + time.sleep(30) + + if self.odrv_axis.encoder.error != 0: + print("Error: Odrive reported an error of {} while in the state " + "AXIS_STATE_ENCODER_OFFSET_CALIBRATION. Printing out Odrive encoder " + "data for debug:\n{}".format(self.odrv_axis.encoder.error, + self.odrv_axis.encoder)) + + sys.exit(1) + + # If offset_float isn't 0.5 within some tolerance, or its not 1.5 within + # some tolerance, raise an error + if not ((self.odrv_axis.encoder.config.offset_float > 0.5 - self.ENCODER_OFFSET_FLOAT_TOLERANCE and \ + self.odrv_axis.encoder.config.offset_float < 0.5 + self.ENCODER_OFFSET_FLOAT_TOLERANCE) or \ + (self.odrv_axis.encoder.config.offset_float > 1.5 - self.ENCODER_OFFSET_FLOAT_TOLERANCE and \ + self.odrv_axis.encoder.config.offset_float < 1.5 + self.ENCODER_OFFSET_FLOAT_TOLERANCE)): + print("Error: After odrive encoder calibration, the 'offset_float' " + "is at {}, which is outside of the expected range. 'offset_float' " + "should be close to 0.5 or 1.5 with a tolerance of {}. Either " + "increase the tolerance or debug/fix your setup. Printing out " + "Odrive encoder data for debug:\n{}".format(self.odrv_axis.encoder.config.offset_float, + self.ENCODER_OFFSET_FLOAT_TOLERANCE, + self.odrv_axis.encoder)) + + sys.exit(1) + + # If all looks good, then lets tell ODrive that saving this calibration + # to persistent memory is OK + self.odrv_axis.encoder.config.pre_calibrated = True + + print("Saving calibration configuration and rebooting...") + self.odrv.save_configuration() + print("Calibration configuration saved.") + try: + self.odrv.reboot() + except ChannelBrokenException: + pass + + self._find_odrive() + + print("Odrive configuration finished.") + + def mode_idle(self): + """ + Puts the motor in idle (i.e. can move freely). + """ + + self.odrv_axis.requested_state = AXIS_STATE_IDLE + + def mode_close_loop_control(self): + """ + Puts the motor in closed loop control. + """ + + self.odrv_axis.requested_state = AXIS_STATE_CLOSED_LOOP_CONTROL + + def move_input_pos(self, angle): + """ + Puts the motor at a certain angle. + + :param angle: Angle you want the motor to move. + :type angle: int or float + """ + + self.odrv_axis.controller.input_pos = angle/360.0 + +if __name__ == "__main__": + hb_motor_config = MotorConfig(axis_num = 0) + hb_motor_config.configure() + + print("CONDUCTING MOTOR TEST") + print("Placing motor in close loop control. If you move motor, motor will " + "resist you.") + hb_motor_config.mode_close_loop_control() + + # Go from 0 to 360 degrees in increments of 30 degrees + for angle in range(0, 390, 30): + print("Setting motor to {} degrees.".format(angle)) + hb_motor_config.move_input_pos(angle) + time.sleep(5) + + print("Placing motor in idle. If you move motor, motor will " + "move freely") + hb_motor_config.mode_idle() + diff --git a/Software Files/ODrive Calibration/ODrive_calibration.tar.gz b/Software Files/ODrive Calibration/ODrive_calibration.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..fc661dbdb15804d002e89726689a13f5031dc172 GIT binary patch literal 5529 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 literal 0 HcmV?d00001