#!/usr/bin/env python # This file is part of the OpenMV project. # Copyright (c) 2013/2014 Ibrahim Abdelkader # This work is licensed under the MIT license, see the file LICENSE for # details. """This module implements enough functionality to program the STM32F4xx over DFU, without requiring dfu-util. See app note AN3156 for a description of the DFU protocol. See document UM0391 for a description of the DFuse file. """ from __future__ import print_function import argparse import collections import inspect import re import struct import sys import usb.core import usb.util import zlib # USB request __TIMEOUT __TIMEOUT = 4000 # DFU commands __DFU_DETACH = 0 __DFU_DNLOAD = 1 __DFU_UPLOAD = 2 __DFU_GETSTATUS = 3 __DFU_CLRSTATUS = 4 __DFU_GETSTATE = 5 __DFU_ABORT = 6 # DFU status __DFU_STATE_APP_IDLE = 0x00 __DFU_STATE_APP_DETACH = 0x01 __DFU_STATE_DFU_IDLE = 0x02 __DFU_STATE_DFU_DOWNLOAD_SYNC = 0x03 __DFU_STATE_DFU_DOWNLOAD_BUSY = 0x04 __DFU_STATE_DFU_DOWNLOAD_IDLE = 0x05 __DFU_STATE_DFU_MANIFEST_SYNC = 0x06 __DFU_STATE_DFU_MANIFEST = 0x07 __DFU_STATE_DFU_MANIFEST_WAIT_RESET = 0x08 __DFU_STATE_DFU_UPLOAD_IDLE = 0x09 __DFU_STATE_DFU_ERROR = 0x0A _DFU_DESCRIPTOR_TYPE = 0x21 __DFU_STATUS_STR = { __DFU_STATE_APP_IDLE: "STATE_APP_IDLE", __DFU_STATE_APP_DETACH: "STATE_APP_DETACH", __DFU_STATE_DFU_IDLE: "STATE_DFU_IDLE", __DFU_STATE_DFU_DOWNLOAD_SYNC: "STATE_DFU_DOWNLOAD_SYNC", __DFU_STATE_DFU_DOWNLOAD_BUSY: "STATE_DFU_DOWNLOAD_BUSY", __DFU_STATE_DFU_DOWNLOAD_IDLE: "STATE_DFU_DOWNLOAD_IDLE", __DFU_STATE_DFU_MANIFEST_SYNC: "STATE_DFU_MANIFEST_SYNC", __DFU_STATE_DFU_MANIFEST: "STATE_DFU_MANIFEST", __DFU_STATE_DFU_MANIFEST_WAIT_RESET: "STATE_DFU_MANIFEST_WAIT_RESET", __DFU_STATE_DFU_UPLOAD_IDLE: "STATE_DFU_UPLOAD_IDLE", __DFU_STATE_DFU_ERROR: "STATE_DFU_ERROR", } # USB device handle __dev = None # Configuration descriptor of the device __cfg_descr = None __verbose = None # USB DFU interface __DFU_INTERFACE = 0 # Python 3 deprecated getargspec in favour of getfullargspec, but # Python 2 doesn't have the latter, so detect which one to use getargspec = getattr(inspect, "getfullargspec", getattr(inspect, "getargspec", None)) if "length" in getargspec(usb.util.get_string).args: # PyUSB 1.0.0.b1 has the length argument def get_string(dev, index): return usb.util.get_string(dev, 255, index) else: # PyUSB 1.0.0.b2 dropped the length argument def get_string(dev, index): return usb.util.get_string(dev, index) def find_dfu_cfg_descr(descr): if len(descr) == 9 and descr[0] == 9 and descr[1] == _DFU_DESCRIPTOR_TYPE: nt = collections.namedtuple( "CfgDescr", [ "bLength", "bDescriptorType", "bmAttributes", "wDetachTimeOut", "wTransferSize", "bcdDFUVersion", ], ) return nt(*struct.unpack(" 1: raise ValueError("Multiple DFU devices found") __dev = devices[0] __dev.set_configuration() # Claim DFU interface usb.util.claim_interface(__dev, __DFU_INTERFACE) # Find the DFU configuration descriptor, either in the device or interfaces __cfg_descr = None for cfg in __dev.configurations(): __cfg_descr = find_dfu_cfg_descr(cfg.extra_descriptors) if __cfg_descr: break for itf in cfg.interfaces(): __cfg_descr = find_dfu_cfg_descr(itf.extra_descriptors) if __cfg_descr: break # Get device into idle state for attempt in range(4): status = get_status() if status == __DFU_STATE_DFU_IDLE: break elif status == __DFU_STATE_DFU_DOWNLOAD_IDLE or status == __DFU_STATE_DFU_UPLOAD_IDLE: abort_request() else: clr_status() def abort_request(): """Sends an abort request.""" __dev.ctrl_transfer(0x21, __DFU_ABORT, 0, __DFU_INTERFACE, None, __TIMEOUT) def clr_status(): """Clears any error status (perhaps left over from a previous session).""" __dev.ctrl_transfer(0x21, __DFU_CLRSTATUS, 0, __DFU_INTERFACE, None, __TIMEOUT) def get_status(): """Get the status of the last operation.""" stat = __dev.ctrl_transfer(0xA1, __DFU_GETSTATUS, 0, __DFU_INTERFACE, 6, 20000) # firmware can provide an optional string for any error if stat[5]: message = get_string(__dev, stat[5]) if message: print(message) return stat[4] def check_status(stage, expected): status = get_status() if status != expected: raise SystemExit("DFU: %s failed (%s)" % (stage, __DFU_STATUS_STR.get(status, status))) def mass_erase(): """Performs a MASS erase (i.e. erases the entire device).""" # Send DNLOAD with first byte=0x41 __dev.ctrl_transfer(0x21, __DFU_DNLOAD, 0, __DFU_INTERFACE, "\x41", __TIMEOUT) # Execute last command check_status("erase", __DFU_STATE_DFU_DOWNLOAD_BUSY) # Check command state check_status("erase", __DFU_STATE_DFU_DOWNLOAD_IDLE) def page_erase(addr): """Erases a single page.""" if __verbose: print("Erasing page: 0x%x..." % (addr)) # Send DNLOAD with first byte=0x41 and page address buf = struct.pack(" 0: write_size = size if not mass_erase_used: for segment in mem_layout: if addr >= segment["addr"] and addr <= segment["last_addr"]: # We found the page containing the address we want to # write, erase it page_size = segment["page_size"] page_addr = addr & ~(page_size - 1) if addr + write_size > page_addr + page_size: write_size = page_addr + page_size - addr page_erase(page_addr) break write_memory(addr, data[:write_size], progress, elem_addr, elem_size) data = data[write_size:] addr += write_size size -= write_size if progress: progress(elem_addr, addr - elem_addr, elem_size) def cli_progress(addr, offset, size): """Prints a progress report suitable for use on the command line.""" width = 25 done = offset * width // size print( "\r0x{:08x} {:7d} [{}{}] {:3d}% ".format( addr, size, "=" * done, " " * (width - done), offset * 100 // size ), end="", ) try: sys.stdout.flush() except OSError: pass # Ignore Windows CLI "WinError 87" on Python 3.6 if offset == size: print("") def main(): """Test program for verifying this files functionality.""" global __verbose # Parse CMD args parser = argparse.ArgumentParser(description="DFU Python Util") parser.add_argument( "-l", "--list", help="list available DFU devices", action="store_true", default=False ) parser.add_argument("--vid", help="USB Vendor ID", type=lambda x: int(x, 0), default=None) parser.add_argument("--pid", help="USB Product ID", type=lambda x: int(x, 0), default=None) parser.add_argument( "-m", "--mass-erase", help="mass erase device", action="store_true", default=False ) parser.add_argument( "-u", "--upload", help="read file from DFU device", dest="path", default=False ) parser.add_argument("-x", "--exit", help="Exit DFU", action="store_true", default=False) parser.add_argument( "-v", "--verbose", help="increase output verbosity", action="store_true", default=False ) args = parser.parse_args() __verbose = args.verbose kwargs = {} if args.vid: kwargs["idVendor"] = args.vid if args.pid: kwargs["idProduct"] = args.pid if args.list: list_dfu_devices(**kwargs) return init(**kwargs) command_run = False if args.mass_erase: print("Mass erase...") mass_erase() command_run = True if args.path: elements = read_dfu_file(args.path) if not elements: print("No data in dfu file") return print("Writing memory...") write_elements(elements, args.mass_erase, progress=cli_progress) print("Exiting DFU...") exit_dfu() command_run = True if args.exit: print("Exiting DFU...") exit_dfu() command_run = True if command_run: print("Finished") else: print("No command specified") if __name__ == "__main__": main()