diff --git a/tools/odrive/dfu.py b/tools/odrive/dfu.py index b9ee697d..3eced60d 100755 --- a/tools/odrive/dfu.py +++ b/tools/odrive/dfu.py @@ -254,6 +254,38 @@ def find_device_in_dfu_mode(serial_number, cancellation_token): time.sleep(1) return None +def get_hw_version_in_dfu_mode(dfudev): + """ + Reads the hardware version from one-time-programmable memory. + This is written on all ODrives sold since Summer 2018. + """ + otp_sector = [s for s in dfudev.sectors if s['name'] == 'OTP Memory' and s['addr'] == 0x1fff7800][0] + otp_data = dfudev.read_sector(otp_sector) + if otp_data[0] == 0: + otp_data = otp_data[16:] + if otp_data[0] == 0xfe: + return (otp_data[3], otp_data[4], otp_data[5]) + else: + return None + +def unlock_device(serial_number, cancellation_token): + print("Looking for ODrive in DFU mode...") + print("If the program hangs at this point, try to set the DFU switch to \"DFU\" and power cycle the ODrive.") + + stm_device = find_device_in_dfu_mode(serial_number, cancellation_token) + dfudev = DfuDevice(stm_device) + + print("Unlocking device (this may take a few seconds)...") + dfudev.unprotect() + print("done") + print("") + print("Now do the following:") + print(" 1. Put the DFU switch on the ODrive to \"DFU\"") + print(" 2. Power-cycle the ODrive") + print(" 3. Run \"odrivetool dfu\" (or any third party DFU tool)") + print(" 4. Put the DFU switch on the ODrive to \"RUN\"") + + def update_device(device, firmware, logger, cancellation_token): """ Updates the specified device with the specified firmware. @@ -271,16 +303,8 @@ def update_device(device, firmware, logger, cancellation_token): if (logger._verbose): logger.debug("OTP:") dump_otp(dfudev) + hw_version = get_hw_version_in_dfu_mode(dfudev) or (0, 0, 0) - # Read hardware version from one-time-programmable memory - otp_sector = [s for s in dfudev.sectors if s['name'] == 'OTP Memory' and s['addr'] == 0x1fff7800][0] - otp_data = dfudev.read_sector(otp_sector) - if otp_data[0] == 0: - otp_data = otp_data[16:] - if otp_data[0] == 0xfe: - hw_version = (otp_data[3], otp_data[4], otp_data[5]) - else: - hw_version = (0, 0, 0) else: found_in_dfu = False serial_number = "{:08X}".format(device.serial_number) @@ -434,8 +458,12 @@ def update_device(device, firmware, logger, cancellation_token): temp_config_filename = odrive.configuration.get_temp_config_filename(device) odrive.configuration.restore_config(device, None, logger) os.remove(temp_config_filename) - - logger.success("Device firmware update successful.") + + logger.success("Device firmware update successful.") + else: + logger.success("Firmware upload successful.") + logger.info("To complete the firmware update, set the DFU switch to \"RUN\" and power cycle the board.") + def launch_dfu(args, logger, cancellation_token): """ diff --git a/tools/odrive/dfuse/DfuDevice.py b/tools/odrive/dfuse/DfuDevice.py index 49a17b29..6b0a59ac 100644 --- a/tools/odrive/dfuse/DfuDevice.py +++ b/tools/odrive/dfuse/DfuDevice.py @@ -24,6 +24,13 @@ MAX_TRANSFER_SIZE = 2048 def address_to_4bytes(a): return [ a % 256, (a >> 8)%256, (a >> 16)%256, (a >> 24)%256 ] +def make_exception(status): + if status[0] == 11: # errVENDOR + suffix = " - Try running \"odrivetool unlock\" and then try \"odrivetool dfu\" again." + else: + suffix = "" + return RuntimeError("An error occured. Device Status: {!r}{}".format(status, suffix)) + class DfuDevice: def __init__(self, device, timeout = None): self.dev = device @@ -87,6 +94,16 @@ class DfuDevice: def set_address(self, ap): return self.dnload(0x0, [0x21] + address_to_4bytes(ap)) + def unprotect(self): + alt = [a for a in self.alternates() if a[0].startswith("@Device Feature/")] + assert(len(alt) == 1) + self.set_alternate_safe(alt[0]) + + self.dnload(0x0, [0x92]) + status = self.get_status() + if status[1] != DfuState.DFU_DOWNLOAD_BUSY: + raise make_exception(status) + def write(self, block, data): return self.dnload(block + 2, data) @@ -171,12 +188,12 @@ class DfuDevice: self.set_address(addr) status = self.wait_while_state(DfuState.DFU_DOWNLOAD_BUSY) if status[1] != DfuState.DFU_DOWNLOAD_IDLE: - raise RuntimeError("An error occured. Device Status: {!r}".format(status)) + raise make_exception(status) # take device out of DFU_DOWNLOAD_SYNC and into DFU_IDLE self.abort() status = self.wait_while_state(DfuState.DFU_DOWNLOAD_SYNC) if status[1] != DfuState.DFU_IDLE: - raise RuntimeError("An error occured. Device Status: {!r}".format(status)) + raise make_exception(status) def erase_sector(self, sector): @@ -184,7 +201,7 @@ class DfuDevice: self.erase(sector['addr']) status = self.wait_while_state(DfuState.DFU_DOWNLOAD_BUSY, timeout=sector['len']/32) if status[1] != DfuState.DFU_DOWNLOAD_IDLE: - raise RuntimeError("An error occured. Device Status: {!r}".format(status)) + raise make_exception(status) def write_sector(self, sector, data): self.set_alternate_safe(sector['alt']) @@ -199,7 +216,7 @@ class DfuDevice: self.write(blocknum, block) status = self.wait_while_state(DfuState.DFU_DOWNLOAD_BUSY) if status[1] != DfuState.DFU_DOWNLOAD_IDLE: - raise RuntimeError("An error occured. Device Status: {!r}".format(status)) + raise make_exception(status) def read_sector(self, sector): """ @@ -226,9 +243,9 @@ class DfuDevice: #self.set_address(address) #status = self.wait_while_state(DfuState.DFU_DOWNLOAD_BUSY) #if status[1] != DfuState.DFU_DOWNLOAD_IDLE: - # raise RuntimeError("An error occured. Device Status: {}".format(status[1])) + # raise make_exception(status) self.leave() status = self.wait_while_state(DfuState.DFU_MANIFEST_SYNC) if status[1] != DfuState.DFU_MANIFEST: - raise RuntimeError("An error occured. Device Status: {}".format(status[1])) + raise make_exception(status) diff --git a/tools/odrivetool b/tools/odrivetool index 5d0e9776..0be69899 100755 --- a/tools/odrivetool +++ b/tools/odrivetool @@ -59,6 +59,8 @@ dfu_parser.add_argument('file', metavar='HEX', nargs='?', 'If no file is provided, the script automatically downloads ' 'the latest firmware.') +unlock_parser = subparsers.add_parser('unlock', help="Try to remove read-out protection." + "If no serial number is specified, the first ODrive that is found is unlocked") dfu_parser = subparsers.add_parser('backup-config', help="Saves the configuration of the ODrive to a JSON file") dfu_parser.add_argument('file', nargs='?', @@ -147,6 +149,11 @@ try: import odrive.dfu odrive.dfu.launch_dfu(args, logger, app_shutdown_token) + elif args.command == 'unlock': + print_version() + import odrive.dfu + odrive.dfu.unlock_device(args.serial_number, app_shutdown_token) + elif args.command == 'liveplotter': from odrive.utils import start_liveplotter print("Waiting for ODrive...")