Permalink
Cannot retrieve contributors at this time
executable file
537 lines (428 sloc)
16.1 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python2 | |
| # -*- coding: utf-8 -*- | |
| # | |
| # Copyright 2010-2012 Michael Ossmann, Travis Goodspeed | |
| # | |
| # This file is forked from Project Ubertooth in order to support the | |
| # STM32F2xx. | |
| # The STM32 series of ARM chips implement a weird variant of DFU, so | |
| # I've written this client in order to understand it. Sometime in the | |
| # future, I expect to build a general-purpose DFU tool that works for | |
| # all chips. For now, though, this will certainly not be expected to | |
| # run on anything but the STM32 series. | |
| # http://pyusb.sourceforge.net/docs/1.0/tutorial.html | |
| from __future__ import print_function | |
| import struct | |
| import sys | |
| import time | |
| import usb.core | |
| stm32_vendor = 0x0483 | |
| stm32_product = 0xdf11 | |
| ram_offset = 0x20002000 # RAM | |
| ram_size = 0x1E000 | |
| application_offset = 0x08000000 # Flash | |
| application_size = 0x100000 # 1MB | |
| rom_offset = 0x1fff0000 # ROM | |
| rom_size = 0x8000 # 32K | |
| otp_offset = 0x1fff7800 # OTP ROM | |
| otp_size = 512 | |
| # Smaller block sizes cause problems. Not sure why. | |
| block_size = 2048 | |
| class Enumeration(object): | |
| def __init__(self, id, name): | |
| self._id = id | |
| self._name = name | |
| setattr(self.__class__, name, self) | |
| self.map[id] = self | |
| def __int__(self): | |
| return self.id | |
| def __repr__(self): | |
| return self.name | |
| @property | |
| def id(self): | |
| return self._id | |
| @property | |
| def name(self): | |
| return self._name | |
| @classmethod | |
| def create_from_map(cls): | |
| for id, name in cls.map.items(): | |
| cls(id, name) | |
| class Request(Enumeration): | |
| map = { | |
| 0: 'DETACH', | |
| 1: 'DNLOAD', | |
| 2: 'UPLOAD', | |
| 3: 'GETSTATUS', | |
| 4: 'CLRSTATUS', | |
| 5: 'GETSTATE', | |
| 6: 'ABORT', | |
| } | |
| Request.create_from_map() | |
| class State(Enumeration): | |
| map = { | |
| 0: 'appIDLE', | |
| 1: 'appDETACH', | |
| 2: 'dfuIDLE', | |
| 3: 'dfuDNLOAD_SYNC', | |
| 4: 'dfuDNBUSY', | |
| 5: 'dfuDNLOAD_IDLE', | |
| 6: 'dfuMANIFEST_SYNC', | |
| 7: 'dfuMANIFEST', | |
| 8: 'dfuMANIFEST_WAIT_RESET', | |
| 9: 'dfuUPLOAD_IDLE', | |
| 10: 'dfuERROR', | |
| } | |
| State.create_from_map() | |
| class Status(Enumeration): | |
| map = { | |
| 0x00: 'OK', | |
| 0x01: 'errTARGET', | |
| 0x02: 'errFILE', | |
| 0x03: 'errWRITE', | |
| 0x04: 'errERASE', | |
| 0x05: 'errCHECK_ERASED', | |
| 0x06: 'errPROG', | |
| 0x07: 'errVERIFY', | |
| 0x08: 'errADDRESS', | |
| 0x09: 'errNOTDONE', | |
| 0x0A: 'errFIRMWARE', | |
| 0x0B: 'errVENDOR', | |
| 0x0C: 'errUSBR', | |
| 0x0D: 'errPOR', | |
| 0x0E: 'errUNKNOWN', | |
| 0x0F: 'errSTALLEDPKT', | |
| } | |
| Status.create_from_map() | |
| class DFU(object): | |
| def __init__(self, device): | |
| self._device = device | |
| def detach(self): | |
| self._device.ctrl_transfer(0x21, Request.DETACH, 0, 0, None) | |
| def download(self, block_number, data): | |
| """Download a block to RAM or Flash.""" | |
| self._device.ctrl_transfer(0x21, Request.DNLOAD, block_number, 0, data) | |
| def masserase(self): | |
| """Mass erase all of Flash memory. OTP left intact.""" | |
| self._device.ctrl_transfer(bmRequestType=0x21, | |
| bRequest=Request.DNLOAD, | |
| wValue=0, | |
| wIndex=0, | |
| data_or_wLength=[0x41]) | |
| # Erasure doesn't occur until we check the status. | |
| status = self.get_status() | |
| print("The device is now erasing itself.") | |
| print("This will result in a USB disconnect.") | |
| return True | |
| def go(self, address=0x08000000): | |
| """Branch to the address at addrptr+4.""" | |
| # Branches to the value stored at the word *AFTER* this address. | |
| self.setaddresspointer(address) | |
| # Use DNLOAD, not DETACH. | |
| self._device.ctrl_transfer(bmRequestType=0x21, | |
| bRequest=Request.DNLOAD, | |
| # bRequest=Request.DETACH, | |
| wValue=0, | |
| wIndex=0, | |
| data_or_wLength=[]) # Zero length. | |
| # Execution. | |
| print(self.get_status()) | |
| print("The device is now executing its application.") | |
| print("This will result in a USB disconnect.") | |
| return True | |
| def readprotect(self): | |
| """Over the control word to protect the chip.""" | |
| # Option byte address. | |
| adr = 0x1fffc000 | |
| self.setaddresspointer(adr) | |
| # #Grab the old block. | |
| # data=self.upload(2,2); | |
| # #Second read verifies the state. | |
| # status, timeout, state, discarded = dfu.get_status() | |
| # print state; | |
| # assert state==State.dfuUPLOAD_IDLE | |
| # print "Grabbing old option bytes."; | |
| # for foo in data: | |
| # print "%02x" % foo; | |
| # Write the new block. | |
| # self._device.ctrl_transfer(0x21, Request.DNLOAD, 2, 0, [0xFF, 0xAA, 0x00, 0x55]) | |
| self.download(2, [0xFF, 0xFF]) | |
| # status check causes the write | |
| status, timeout, state, discarded = dfu.get_status() | |
| assert state == State.dfuDNBUSY; | |
| def readunprotect(self): | |
| """Mass erase all of Flash memory. OTP left intact.""" | |
| self._device.ctrl_transfer(bmRequestType=0x21, | |
| bRequest=Request.DNLOAD, | |
| wValue=0, | |
| wIndex=0, | |
| data_or_wLength=[0x92]) | |
| print("Unprotecting the device.") | |
| print("This will cause a USB disconnection.") | |
| status = self.get_status() | |
| return True | |
| def upload(self, block_number, length): | |
| # 2 to 2048 byte size for Flash, RAM, and System memory. | |
| # option bytes should be equal to option byte block size. | |
| # Other locations defined in Important Considerations in AN2606 | |
| # Address_Pointer is expected to have been already set by | |
| # Address = ((wBlockNum - 2) * wTransferSize) + Address_Pointer, | |
| # print "Requesting block 0x%08x size %06x" % (block_number,length); | |
| if block_number > 0xFFFF: | |
| print("WARNING: Block 0x%04x will be returned instead. (16-bit addr.)" % (block_number & 0xFFFF)) | |
| # data = self._device.ctrl_transfer(0xA1, Request.UPLOAD, block_number, 0, length) | |
| data = self._device.ctrl_transfer(bmRequestType=0xA1, | |
| bRequest=Request.UPLOAD, | |
| wValue=block_number, | |
| wIndex=0, | |
| data_or_wLength=length) | |
| return data | |
| def setaddresspointer(self, address=application_offset): | |
| """Sets the address pointer in the STM32.""" | |
| byte0 = address & 0xFF | |
| byte1 = (address >> 8) & 0xFF | |
| byte2 = (address >> 16) & 0xFF | |
| byte3 = (address >> 24) & 0xFF | |
| data = [0x21, # Set pointer op-code | |
| byte0, byte1, byte2, byte3 # Address, little-endian. | |
| ] | |
| # print self.get_status(); | |
| toret = self._device.ctrl_transfer(bmRequestType=0x21, | |
| bRequest=Request.DNLOAD, | |
| wValue=0, | |
| wIndex=0, | |
| data_or_wLength=data) | |
| # Address isn't set until first GETSTATUS query. | |
| status = self.get_status() | |
| # Second query is needed to check if correctly set. | |
| # Failures result in dfuERROR or errTARGET. | |
| status = self.get_status() | |
| if status[2] == State.dfuDNLOAD_IDLE: | |
| print("Setting address pointer to 0x%08x." % address) | |
| # This will get us back to the entry point. | |
| self.enter_dfu_mode() | |
| else: | |
| print("Failed to set address pointer.") | |
| return False | |
| return True | |
| def get_status(self): | |
| status_packed = self._device.ctrl_transfer(0xA1, Request.GETSTATUS, 0, 0, 6) | |
| status = struct.unpack('<BBBBBB', status_packed) | |
| return (Status.map[status[0]], (((status[1] << 8) | status[2]) << 8) | status[3], | |
| State.map[status[4]], status[5]) | |
| def clear_status(self): | |
| self._device.ctrl_transfer(0x21, Request.CLRSTATUS, 0, 0, None) | |
| def get_state(self): | |
| state_packed = self._device.ctrl_transfer(0xA1, Request.GETSTATE, 0, 0, 1) | |
| return State.map[struct.unpack('<B', state_packed)[0]] | |
| def abort(self): | |
| self._device.ctrl_transfer(0x21, Request.ABORT, 0, 0, None) | |
| def enter_dfu_mode(self): | |
| action_map = { | |
| State.dfuDNLOAD_SYNC: self.abort, | |
| State.dfuDNLOAD_IDLE: self.abort, | |
| State.dfuMANIFEST_SYNC: self.abort, | |
| State.dfuUPLOAD_IDLE: self.abort, | |
| State.dfuERROR: self.clear_status, | |
| State.appIDLE: self.detach, | |
| State.appDETACH: self._wait, | |
| State.dfuDNBUSY: self._wait, | |
| State.dfuMANIFEST: self.abort, | |
| State.dfuMANIFEST_WAIT_RESET: self._wait, | |
| State.dfuIDLE: self._wait | |
| } | |
| while True: | |
| state = self.get_state() | |
| if state == State.dfuIDLE: | |
| break | |
| action = action_map[state] | |
| action() | |
| def _wait(self): | |
| time.sleep(0.1) | |
| def download(dfu, data, flash_address): | |
| # block_size = 1 << 8 | |
| sector_size = 1 << 12 | |
| print("Flashing to 0x%08x" % flash_address) | |
| base_address = flash_address | |
| # Rebase the address pointer. | |
| if not dfu.setaddresspointer(base_address): | |
| print("Failed to set address.") | |
| sys.exit(1) | |
| flash_address = flash_address + block_size * 2 - base_address # Correct offset. | |
| if flash_address & (sector_size - 1) != 0: | |
| raise Exception('Download must start at flash sector boundary') | |
| block_number = flash_address / block_size | |
| assert block_number * block_size == flash_address | |
| print("Based from 0x%08x" % base_address) | |
| try: | |
| while len(data) > 0: | |
| packet, data = data[:block_size], data[block_size:] | |
| if len(packet) < block_size: | |
| print("Padding a short packet.") | |
| packet += '\xFF' * (block_size - len(packet)) | |
| # print "Downloading block %i." % block_number; | |
| dfu.download(block_number, packet) | |
| # status check causes the write | |
| status, timeout, state, discarded = dfu.get_status() | |
| assert state == State.dfuDNBUSY; | |
| # Second read verifies the state. | |
| status, timeout, state, discarded = dfu.get_status() | |
| assert state == State.dfuDNLOAD_IDLE | |
| sys.stdout.write('.') | |
| sys.stdout.flush() | |
| block_number += 1 | |
| finally: | |
| print() | |
| dfu.enter_dfu_mode() | |
| def upload(dfu, flash_address, length, path): | |
| """Uploads a region from the chip to a file on the workstation.""" | |
| # Set the base address, then make it zero. | |
| base_address = flash_address | |
| # flash_address=base_address; | |
| # Rebase the address pointer. | |
| if not dfu.setaddresspointer(base_address): | |
| print("Failed to set address.") | |
| sys.exit(1) | |
| flash_address = flash_address + block_size * 2 - base_address # Correct offset. | |
| if flash_address & (block_size - 1) != 0: | |
| raise Exception('Upload must start at block boundary') | |
| block_number = flash_address / block_size | |
| # assert block_number * block_size == flash_address #Ubertooth, not STM32 | |
| # address_pointer=0; | |
| # assert flash_address==((block_number-2)*block_size)+address_pointer; | |
| print("flash_address = %08x" % flash_address) | |
| print("block_number = %08x" % block_number) | |
| print("block_size = %08x" % block_size) | |
| f = open(path, 'wb') | |
| try: | |
| while length > 0: | |
| data = dfu.upload(block_number, block_size) | |
| status, timeout, state, discarded = dfu.get_status() | |
| sys.stdout.write('.') | |
| sys.stdout.flush() | |
| if len(data) == block_size: | |
| f.write(data) | |
| block_number += 1 | |
| length -= len(data) | |
| else: | |
| # raise Exception('Upload failed to read full block') | |
| print("Failed to return full block number 0x%x" % block_number) | |
| print("Got 0x%i bytes." % len(data)) | |
| finally: | |
| f.close() | |
| print() | |
| def detach(dfu): | |
| if dfu.get_state() == State.dfuIDLE: | |
| dfu.detach() | |
| print('Detached') | |
| else: | |
| print('In unexpected state: %s' % dfu.get_state()) | |
| def init_dfu(idVendor=stm32_vendor, idProduct=stm32_product): | |
| dev = usb.core.find(idVendor=idVendor, idProduct=idProduct) | |
| if dev is None: | |
| raise RuntimeError('Device not found') | |
| dfu = DFU(dev) | |
| dev.default_timeout = 3000 | |
| try: | |
| dfu.enter_dfu_mode() | |
| except usb.core.USBError as e: | |
| if len(e.args) > 0 and e.args[0] == 'Pipe error': | |
| raise RuntimeError('Failed to enter DFU mode. Is bootloader running?') | |
| else: | |
| raise e | |
| return dfu | |
| def usage(): | |
| print(""" | |
| Usage: stm32-dfu <command> <arguments> | |
| Write a file to application flash region: | |
| stm32-dfu writeflash $file | |
| Write a file to RAM at 0x20002000, after DFU region. | |
| stm32-dfu writeram $file | |
| Write a file to an arbitrary address. | |
| stm32-dfu write $file $adr | |
| Read data from application flash region and write to a file: | |
| stm32-dfu read <filename> | |
| Read data from SRAM region and write to a file: | |
| stm32-dfu readram <filename> | |
| Read data from ROM region and write to a file: | |
| stm32-dfu readrom <filename> | |
| Read data from OTP region and write to a file: | |
| stm32-dfu readotp <filename> | |
| Mass erase STM32 in preparation for reflashing. | |
| stm32-dfu erase | |
| Unprotect the STM32's RDP. | |
| stm32-dfu unprotect | |
| Protect the STM32's RDP. | |
| stm32-dfu protect | |
| Detach the bootloader and execute the flash application. | |
| stm32-dfu go [0x08000000] | |
| Detach the bootloader and execute from RAM at 0x20002000. | |
| stm32-dfu goram | |
| """) | |
| def main(): | |
| if len(sys.argv) > 1: | |
| if sys.argv[1] == 'read': | |
| dfu = init_dfu() | |
| upload(dfu, application_offset, application_size, sys.argv[2]) | |
| print('Read complete') | |
| elif sys.argv[1] == 'readram': | |
| dfu = init_dfu() | |
| upload(dfu, ram_offset, ram_size, sys.argv[2]) | |
| print('Read complete') | |
| elif sys.argv[1] == 'readrom': | |
| dfu = init_dfu() | |
| upload(dfu, rom_offset, rom_size, sys.argv[2]) | |
| print('Read complete') | |
| elif sys.argv[1] == 'readotp': | |
| dfu = init_dfu() | |
| upload(dfu, otp_offset, otp_size, sys.argv[2]) | |
| print('Read complete') | |
| elif sys.argv[1] == 'write': | |
| f = open(sys.argv[2], 'rb') | |
| data = f.read() | |
| f.close() | |
| dfu = init_dfu() | |
| firmware = data | |
| application_offset = int(sys.argv[3], 16) | |
| download(dfu, firmware, application_offset) | |
| print('Write complete') | |
| elif sys.argv[1] == 'writeflash': | |
| f = open(sys.argv[2], 'rb') | |
| data = f.read() | |
| f.close() | |
| dfu = init_dfu() | |
| firmware = data | |
| download(dfu, firmware, application_offset) | |
| print('Write complete') | |
| elif sys.argv[1] == 'writeram': | |
| f = open(sys.argv[2], 'rb') | |
| data = f.read() | |
| f.close() | |
| dfu = init_dfu() | |
| firmware = data | |
| download(dfu, firmware, 0x20002000) | |
| print('Write complete') | |
| elif sys.argv[1] == 'detach': | |
| dfu = init_dfu() | |
| detach(dfu) | |
| elif sys.argv[1] == 'erase': | |
| dfu = init_dfu() | |
| dfu.masserase() | |
| elif sys.argv[1] == 'go': | |
| dfu = init_dfu() | |
| if len(sys.argv) == 2: | |
| dfu.go() | |
| else: | |
| dfu.go(int(sys.argv[2], 16)) | |
| elif sys.argv[1] == 'unprotect': | |
| dfu = init_dfu() | |
| dfu.readunprotect() | |
| elif sys.argv[1] == 'protect': | |
| dfu = init_dfu() | |
| dfu.readprotect() | |
| else: | |
| usage() | |
| else: | |
| usage() | |
| if __name__ == '__main__': | |
| main() |