*
This commit is contained in:
@@ -0,0 +1,477 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Tool for flashing .hex files to the ODrive via the STM built-in USB DFU mode.
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
import argparse
|
||||
import sys
|
||||
import time
|
||||
import threading
|
||||
import platform
|
||||
import struct
|
||||
import requests
|
||||
import re
|
||||
import io
|
||||
import os
|
||||
import usb.core
|
||||
import fibre
|
||||
import odrive
|
||||
from odrive.utils import Event, OperationAbortedException
|
||||
from odrive.dfuse import *
|
||||
|
||||
try:
|
||||
from intelhex import IntelHex
|
||||
except:
|
||||
sudo_prefix = "" if platform.system() == "Windows" else "sudo "
|
||||
print("You need intelhex for this ({}pip install IntelHex)".format(sudo_prefix), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def get_fw_version_string(fw_version):
|
||||
if (fw_version[0], fw_version[1], fw_version[2]) == (0, 0, 0):
|
||||
return "[unknown version]"
|
||||
else:
|
||||
return "v{}.{}.{}{}".format(fw_version[0], fw_version[1], fw_version[2], "-dev" if fw_version[3] else "")
|
||||
|
||||
def get_hw_version_string(hw_version):
|
||||
if hw_version == (0, 0, 0):
|
||||
return "[unknown version]"
|
||||
else:
|
||||
return "v{}.{}{}".format(hw_version[0], hw_version[1], ("-" + str(hw_version[2]) + "V") if hw_version[2] > 0 else "")
|
||||
|
||||
def populate_sectors(sectors, hexfile):
|
||||
"""
|
||||
Checks for which on-device sectors there is data in the hex file and
|
||||
returns a (sector, data) tuple for each touched sector where data
|
||||
is a byte array of the same size as the sector.
|
||||
"""
|
||||
for sector in sectors:
|
||||
addr = sector['addr']
|
||||
size = sector['len']
|
||||
# check if any segment from the hexfile overlaps with this sector
|
||||
touched = False
|
||||
for (start, end) in hexfile.segments():
|
||||
if start < addr and end > addr:
|
||||
touched = True
|
||||
break
|
||||
elif start >= addr and start < addr + size:
|
||||
touched = True
|
||||
break
|
||||
|
||||
if touched:
|
||||
# TODO: verify if the section is writable
|
||||
yield (sector, hexfile.tobinarray(addr, addr + size - 1))
|
||||
|
||||
|
||||
def get_first_mismatch_index(array1, array2):
|
||||
"""
|
||||
Compares two arrays and returns the index of the
|
||||
first unequal item or None if both arrays are equal
|
||||
"""
|
||||
if len(array1) != len(array2):
|
||||
raise Exception("arrays must be same size")
|
||||
for pos in range(len(array1)):
|
||||
if (array1[pos] != array2[pos]):
|
||||
return pos
|
||||
return None
|
||||
|
||||
def dump_otp(dfudev):
|
||||
"""
|
||||
Dumps the contents of the one-time-programmable
|
||||
memory for debugging purposes.
|
||||
The OTP is used to determine the board version.
|
||||
"""
|
||||
# 512 Byte OTP
|
||||
otp_sector = [s for s in dfudev.sectors if s['name'] == 'OTP Memory' and s['addr'] == 0x1fff7800][0]
|
||||
data = dfudev.read_sector(otp_sector)
|
||||
print(' '.join('{:02X}'.format(x) for x in data))
|
||||
|
||||
# 16 lock bytes
|
||||
otp_lock_sector = [s for s in dfudev.sectors if s['name'] == 'OTP Memory' and s['addr'] == 0x1fff7A00][0]
|
||||
data = dfudev.read_sector(otp_lock_sector)
|
||||
print(' '.join('{:02X}'.format(x) for x in data))
|
||||
|
||||
class Firmware():
|
||||
def __init__(self):
|
||||
self.fw_version = (0, 0, 0, True)
|
||||
self.hw_version = (0, 0, 0)
|
||||
|
||||
@staticmethod
|
||||
def is_newer(a, b):
|
||||
a_num = (a[0], a[1], a[2])
|
||||
b_num = (b[0], b[1], b[2])
|
||||
if a_num == (0, 0, 0) or b_num == (0, 0, 0):
|
||||
return False # Cannot compare unknown versions
|
||||
return a_num > b_num or (a_num == b_num and not a[3] and b[3])
|
||||
|
||||
def __gt__(self, other):
|
||||
"""
|
||||
Compares two firmware versions. If both versions are equal, the
|
||||
prerelease version is considered older than the release version.
|
||||
"""
|
||||
if not isinstance(other, tuple):
|
||||
other = other.fw_version
|
||||
return Firmware.is_newer(self.fw_version, other)
|
||||
|
||||
def __lt__(self, other):
|
||||
"""
|
||||
Compares two firmware versions. If both versions are equal, the
|
||||
prerelease version is considered older than the release version.
|
||||
"""
|
||||
if not isinstance(other, tuple):
|
||||
other = other.fw_version
|
||||
return Firmware.is_newer(other, self.fw_version)
|
||||
|
||||
def is_compatible(self, hw_version):
|
||||
"""
|
||||
Determines if this firmware is compatible
|
||||
with the specified hardware version
|
||||
"""
|
||||
return self.hw_version == hw_version
|
||||
|
||||
class FirmwareFromGithub(Firmware):
|
||||
"""
|
||||
Represents a firmware asset
|
||||
"""
|
||||
def __init__(self, release_json, asset_json):
|
||||
Firmware.__init__(self)
|
||||
if release_json['draft'] or release_json['prerelease']:
|
||||
release_json['tag_name'] += "*"
|
||||
self.fw_version = odrive.version.version_str_to_tuple(release_json['tag_name'])
|
||||
|
||||
hw_version_regex = r'.*v([0-9]+).([0-9]+)(-(?P<voltage>[0-9]+)V)?.hex'
|
||||
hw_version_match = re.search(hw_version_regex, asset_json['name'])
|
||||
self.hw_version = (int(hw_version_match[1]),
|
||||
int(hw_version_match[2]),
|
||||
int(hw_version_match.groupdict().get('voltage') or 0))
|
||||
self.github_asset_id = asset_json['id']
|
||||
self.hex = None
|
||||
# no technical reason to fetch this - just interesting
|
||||
self.download_count = asset_json['download_count']
|
||||
|
||||
def get_as_hex(self):
|
||||
"""
|
||||
Returns the content of the firmware in as a binary array in Intel Hex format
|
||||
"""
|
||||
if self.hex is None:
|
||||
print("Downloading firmware {}...".format(get_fw_version_string(self.fw_version)))
|
||||
response = requests.get('https://api.github.com/repos/madcowswe/ODrive/releases/assets/' + str(self.github_asset_id),
|
||||
headers={'Accept': 'application/octet-stream'})
|
||||
if response.status_code != 200:
|
||||
raise Exception("failed to download firmware")
|
||||
self.hex = response.content
|
||||
return io.StringIO(self.hex.decode('utf-8'))
|
||||
|
||||
class FirmwareFromFile(Firmware):
|
||||
def __init__(self, file):
|
||||
Firmware.__init__(self)
|
||||
self._file = file
|
||||
def get_as_hex(self):
|
||||
return self._file
|
||||
|
||||
def get_all_github_firmwares():
|
||||
response = requests.get('https://api.github.com/repos/madcowswe/ODrive/releases')
|
||||
if response.status_code != 200:
|
||||
raise Exception("could not fetch releases")
|
||||
response_json = response.json()
|
||||
|
||||
for release_json in response_json:
|
||||
for asset_json in release_json['assets']:
|
||||
try:
|
||||
if asset_json['name'].lower().endswith('.hex'):
|
||||
fw = FirmwareFromGithub(release_json, asset_json)
|
||||
yield fw
|
||||
except Exception as ex:
|
||||
print(ex)
|
||||
|
||||
def get_newest_firmware(hw_version):
|
||||
"""
|
||||
Returns the newest available firmware for the specified hardware version
|
||||
"""
|
||||
firmwares = get_all_github_firmwares()
|
||||
firmwares = filter(lambda fw: not fw.fw_version[3], firmwares) # ignore prereleases
|
||||
firmwares = filter(lambda fw: fw.hw_version == hw_version, firmwares)
|
||||
firmwares = list(firmwares)
|
||||
firmwares.sort()
|
||||
return firmwares[-1] if len(firmwares) else None
|
||||
|
||||
def show_deferred_message(message, cancellation_token):
|
||||
"""
|
||||
Shows a message after 10s, unless cancellation_token gets set.
|
||||
"""
|
||||
def show_message_thread(message, cancellation_token):
|
||||
for _ in range(1,10):
|
||||
if cancellation_token.is_set():
|
||||
return
|
||||
time.sleep(1)
|
||||
if not cancellation_token.is_set():
|
||||
print(message)
|
||||
t = threading.Thread(target=show_message_thread, args=(message, cancellation_token))
|
||||
t.daemon = True
|
||||
t.start()
|
||||
|
||||
def put_into_dfu_mode(device, cancellation_token):
|
||||
"""
|
||||
Puts the specified device into DFU mode
|
||||
"""
|
||||
if not hasattr(device, "enter_dfu_mode"):
|
||||
print("The firmware on device {} cannot soft enter DFU mode.\n"
|
||||
"Please remove power, put the DFU switch into DFU mode,\n"
|
||||
"then apply power again. Then try again.\n"
|
||||
"If it still doesn't work, you can try to use the DeFuse app or \n"
|
||||
"dfu-util, see the odrive documentation.\n"
|
||||
"You can also flash the firmware using STLink (`make flash`)"
|
||||
.format(device.__channel__.usb_device.serial_number))
|
||||
return
|
||||
|
||||
print("Putting device {} into DFU mode...".format(device.__channel__.usb_device.serial_number))
|
||||
try:
|
||||
device.enter_dfu_mode()
|
||||
except fibre.ChannelBrokenException:
|
||||
pass # this is expected because the device reboots
|
||||
if platform.system() == "Windows":
|
||||
show_deferred_message("Still waiting for the device to reappear.\n"
|
||||
"Use the Zadig utility to set the driver of 'STM32 BOOTLOADER' to libusb-win32.",
|
||||
cancellation_token)
|
||||
|
||||
def find_device_in_dfu_mode(serial_number, cancellation_token):
|
||||
"""
|
||||
Polls libusb until a device in DFU mode is found
|
||||
"""
|
||||
while not cancellation_token.is_set():
|
||||
params = {} if serial_number == None else {'serial_number': serial_number}
|
||||
stm_device = usb.core.find(idVendor=0x0483, idProduct=0xdf11, **params)
|
||||
if stm_device != None:
|
||||
return stm_device
|
||||
time.sleep(1)
|
||||
return None
|
||||
|
||||
def update_device(device, firmware, logger, cancellation_token):
|
||||
"""
|
||||
Updates the specified device with the specified firmware.
|
||||
The device passed to this function can either be in
|
||||
normal mode or in DFU mode.
|
||||
The firmware should be an instance of Firmware or None.
|
||||
If firmware is None, the newest firmware for the device is
|
||||
downloaded from GitHub releases.
|
||||
"""
|
||||
|
||||
if isinstance(device, usb.core.Device):
|
||||
serial_number = device.serial_number
|
||||
dfudev = DfuDevice(device)
|
||||
if (logger._verbose):
|
||||
logger.debug("OTP:")
|
||||
dump_otp(dfudev)
|
||||
|
||||
# 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:
|
||||
serial_number = device.__channel__.usb_device.serial_number
|
||||
dfudev = None
|
||||
|
||||
# Read hardware version as reported from firmware
|
||||
hw_version_major = device.hw_version_major if hasattr(device, 'hw_version_major') else 0
|
||||
hw_version_minor = device.hw_version_minor if hasattr(device, 'hw_version_minor') else 0
|
||||
hw_version_variant = device.hw_version_variant if hasattr(device, 'hw_version_variant') else 0
|
||||
hw_version = (hw_version_major, hw_version_minor, hw_version_variant)
|
||||
|
||||
if hw_version < (3, 5, 0):
|
||||
print(" DFU mode is not supported on board version 3.4 or earlier.")
|
||||
print(" This is because entering DFU mode on such a device would")
|
||||
print(" break the brake resistor FETs under some circumstances.")
|
||||
print("Warning: DFU mode is not supported on ODrives earlier than v3.5 unless you perform a hardware mod.")
|
||||
if not odrive.utils.yes_no_prompt("Do you still want to continue?", False):
|
||||
raise OperationAbortedException()
|
||||
|
||||
fw_version_major = device.fw_version_major if hasattr(device, 'fw_version_major') else 0
|
||||
fw_version_minor = device.fw_version_minor if hasattr(device, 'fw_version_minor') else 0
|
||||
fw_version_revision = device.fw_version_revision if hasattr(device, 'fw_version_revision') else 0
|
||||
fw_version_prerelease = device.fw_version_prerelease if hasattr(device, 'fw_version_prerelease') else True
|
||||
fw_version = (fw_version_major, fw_version_minor, fw_version_revision, fw_version_prerelease)
|
||||
|
||||
print("Found ODrive {} ({}) with firmware {}{}".format(
|
||||
serial_number,
|
||||
get_hw_version_string(hw_version),
|
||||
get_fw_version_string(fw_version),
|
||||
" in DFU mode" if dfudev is not None else ""))
|
||||
|
||||
if firmware is None:
|
||||
if hw_version == (0, 0, 0):
|
||||
if dfudev is None:
|
||||
suggestion = 'You have to manually flash an up-to-date firmware to make automatic checks work. Run `odrivetool dfu --help` for more info.'
|
||||
else:
|
||||
suggestion = 'Run "make write_otp" to program the board version.'
|
||||
raise Exception('Cannot check online for new firmware because the board version is unknown. ' + suggestion)
|
||||
print("Checking online for newest firmware...", end='')
|
||||
firmware = get_newest_firmware(hw_version)
|
||||
if firmware is None:
|
||||
raise Exception("could not find any firmware release for this board version")
|
||||
print(" found {}".format(get_fw_version_string(firmware.fw_version)))
|
||||
|
||||
if firmware.fw_version <= fw_version:
|
||||
print()
|
||||
if firmware.fw_version < fw_version:
|
||||
print("Warning: you are about to flash firmware {} which is older than the firmware on the device ({}).".format(
|
||||
get_fw_version_string(firmware.fw_version),
|
||||
get_fw_version_string(fw_version)))
|
||||
else:
|
||||
print("You are about to flash firmware {} which is the same version as the firmware on the device ({}).".format(
|
||||
get_fw_version_string(firmware.fw_version),
|
||||
get_fw_version_string(fw_version)))
|
||||
if not odrive.utils.yes_no_prompt("Do you want to flash this firmware anyway?", False):
|
||||
raise OperationAbortedException()
|
||||
|
||||
# load hex file
|
||||
# TODO: Either use the elf format or pack a custom format with a manifest.
|
||||
# This way we can for instance verify the target board version and only
|
||||
# have to publish one file for every board (instead of elf AND hex files).
|
||||
hexfile = IntelHex(firmware.get_as_hex())
|
||||
|
||||
logger.debug("Contiguous segments in hex file:")
|
||||
for start, end in hexfile.segments():
|
||||
logger.debug(" {:08X} to {:08X}".format(start, end - 1))
|
||||
|
||||
# Back up configuration
|
||||
if dfudev is None:
|
||||
do_backup_config = device.user_config_loaded if hasattr(device, 'user_config_loaded') else False
|
||||
if do_backup_config:
|
||||
odrive.configuration.backup_config(device, None, logger)
|
||||
elif not odrive.utils.yes_no_prompt("The configuration cannot be backed up because the device is already in DFU mode. The configuration may be lost after updating. Do you want to continue anyway?", True):
|
||||
raise OperationAbortedException()
|
||||
|
||||
# Put the device into DFU mode if it's not already in DFU mode
|
||||
if dfudev is None:
|
||||
find_odrive_cancellation_token = Event(cancellation_token)
|
||||
put_into_dfu_mode(device, find_odrive_cancellation_token)
|
||||
stm_device = find_device_in_dfu_mode(serial_number, cancellation_token)
|
||||
find_odrive_cancellation_token.set()
|
||||
dfudev = DfuDevice(stm_device)
|
||||
|
||||
logger.debug("Sectors on device: ")
|
||||
for sector in dfudev.sectors:
|
||||
logger.debug(" {:08X} to {:08X} ({})".format(
|
||||
sector['addr'],
|
||||
sector['addr'] + sector['len'] - 1,
|
||||
sector['name']))
|
||||
|
||||
# fill sectors with data
|
||||
touched_sectors = list(populate_sectors(dfudev.sectors, hexfile))
|
||||
|
||||
logger.debug("The following sectors will be flashed: ")
|
||||
for sector,_ in touched_sectors:
|
||||
logger.debug(" {:08X} to {:08X}".format(sector['addr'], sector['addr'] + sector['len'] - 1))
|
||||
|
||||
# Erase
|
||||
try:
|
||||
for i, (sector, data) in enumerate(touched_sectors):
|
||||
print("Erasing... (sector {}/{}) \r".format(i, len(touched_sectors)), end='', flush=True)
|
||||
dfudev.erase_sector(sector)
|
||||
print('Erasing... done \r', end='', flush=True)
|
||||
finally:
|
||||
print('', flush=True)
|
||||
|
||||
# Flash
|
||||
try:
|
||||
for i, (sector, data) in enumerate(touched_sectors):
|
||||
print("Flashing... (sector {}/{}) \r".format(i, len(touched_sectors)), end='', flush=True)
|
||||
dfudev.write_sector(sector, data)
|
||||
print('Flashing... done \r', end='', flush=True)
|
||||
finally:
|
||||
print('', flush=True)
|
||||
|
||||
# Verify
|
||||
try:
|
||||
for i, (sector, expected_data) in enumerate(touched_sectors):
|
||||
print("Verifying... (sector {}/{}) \r".format(i, len(touched_sectors)), end='', flush=True)
|
||||
observed_data = dfudev.read_sector(sector)
|
||||
mismatch_pos = get_first_mismatch_index(observed_data, expected_data)
|
||||
if not mismatch_pos is None:
|
||||
mismatch_pos -= mismatch_pos % 16
|
||||
observed_snippet = ' '.join('{:02X}'.format(x) for x in observed_data[mismatch_pos:mismatch_pos+16])
|
||||
expected_snippet = ' '.join('{:02X}'.format(x) for x in expected_data[mismatch_pos:mismatch_pos+16])
|
||||
raise RuntimeError("Verification failed around address 0x{:08X}:\n".format(sector['addr'] + mismatch_pos) +
|
||||
" expected: " + expected_snippet + "\n"
|
||||
" observed: " + observed_snippet)
|
||||
print('Verifying... done \r', end='', flush=True)
|
||||
finally:
|
||||
print('', flush=True)
|
||||
|
||||
|
||||
# If the flash operation failed for some reason, your device is bricked now.
|
||||
# You can unbrick it as long as the device remains powered on.
|
||||
# (or always with an STLink)
|
||||
# So for debugging you should comment this last part out.
|
||||
|
||||
# Jump to application
|
||||
dfudev.jump_to_application(0x08000000)
|
||||
|
||||
logger.info("Waiting for the device to reappear...")
|
||||
device = odrive.find_any("usb", serial_number,
|
||||
cancellation_token, cancellation_token, timeout=30)
|
||||
|
||||
if do_backup_config:
|
||||
odrive.configuration.restore_config(device, None, logger)
|
||||
os.remove(odrive.configuration.get_temp_config_filename(device))
|
||||
|
||||
logger.success("Device firmware update successful.")
|
||||
|
||||
def launch_dfu(args, logger, cancellation_token):
|
||||
"""
|
||||
Waits for a device that matches args.path and args.serial_number
|
||||
and then upgrades the device's firmware.
|
||||
"""
|
||||
|
||||
serial_number = args.serial_number
|
||||
find_odrive_cancellation_token = Event(cancellation_token)
|
||||
|
||||
logger.info("Waiting for ODrive...")
|
||||
|
||||
devices = [None, None]
|
||||
|
||||
# Start background thread to scan for ODrives in DFU mode
|
||||
def find_device_in_dfu_mode_thread():
|
||||
devices[0] = find_device_in_dfu_mode(serial_number, find_odrive_cancellation_token)
|
||||
find_odrive_cancellation_token.set()
|
||||
t = threading.Thread(target=find_device_in_dfu_mode_thread)
|
||||
t.daemon = True
|
||||
t.start()
|
||||
|
||||
|
||||
# Scan for ODrives not in DFU mode
|
||||
# We only scan on USB because DFU is only implemented over USB
|
||||
devices[1] = odrive.find_any("usb", serial_number,
|
||||
find_odrive_cancellation_token, cancellation_token)
|
||||
find_odrive_cancellation_token.set()
|
||||
|
||||
device = devices[0] or devices[1]
|
||||
firmware = FirmwareFromFile(args.file) if args.file else None
|
||||
|
||||
update_device(device, firmware, logger, cancellation_token)
|
||||
|
||||
|
||||
|
||||
# Note: the flashed image can be verified using: (0x12000 is the number of bytes to read)
|
||||
# $ openocd -f interface/stlink-v2.cfg -f target/stm32f4x.cfg -c init -c flash\ read_bank\ 0\ image.bin\ 0\ 0x12000 -c exit
|
||||
# $ hexdump -C image.bin > image.bin.txt
|
||||
#
|
||||
# If you compare this with a reference image that was flashed with the STLink, you will see
|
||||
# minor differences. This is because this script fills undefined sections with 0xff.
|
||||
# $ diff image_ref.bin.txt image.bin.txt
|
||||
# 21c21
|
||||
# < *
|
||||
# ---
|
||||
# > 00000180 d9 47 00 08 d9 47 00 08 ff ff ff ff ff ff ff ff |.G...G..........|
|
||||
# 2553c2553
|
||||
# < 00009fc0 9e 46 70 47 00 00 00 00 52 20 96 3c 46 76 50 76 |.FpG....R .<FvPv|
|
||||
# ---
|
||||
# > 00009fc0 9e 46 70 47 ff ff ff ff 52 20 96 3c 46 76 50 76 |.FpG....R .<FvPv|
|
||||
|
||||
|
||||
Reference in New Issue
Block a user