From 76d74d210838e33028a6f078e615745e0a33a1bc Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Sun, 4 Mar 2018 15:46:09 +0100 Subject: [PATCH 001/369] Fix Python 3 syntax errors: add parentheses to print statements --- stm32loader.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/stm32loader.py b/stm32loader.py index 95adc05..6b1c0ba 100755 --- a/stm32loader.py +++ b/stm32loader.py @@ -22,6 +22,8 @@ # along with stm32loader; see the file COPYING3. If not see # . +from __future__ import print_function + import sys, getopt import serial import time @@ -50,7 +52,7 @@ def mdebug(level, message): if(QUIET >= level): - print >> sys.stderr , message + print(message, file=sys.stderr) class CmdException(Exception): @@ -238,7 +240,7 @@ def cmdExtendedEraseMemory(self): self.sp.write(chr(0x00)) tmp = self.sp.timeout self.sp.timeout = 30 - print "Extended erase (0x44), this can take ten seconds or more" + print("Extended erase (0x44), this can take ten seconds or more") self._wait_for_ask("0x44 erasing failed") self.sp.timeout = tmp mdebug(10, " Extended Erase memory done") @@ -337,12 +339,12 @@ def writeMemory(self, addr, data): - def __init__(self) : + def __init__(self) : pass def usage(): - print """Usage: %s [-hqVewvr] [-l length] [-p port] [-b baud] [-a addr] [-g addr] [file.bin] + print("""Usage: %s [-hqVewvr] [-l length] [-p port] [-b baud] [-a addr] [-g addr] [file.bin] -h This help -q Quiet -V Verbose @@ -358,7 +360,7 @@ def usage(): ./stm32loader.py -e -w -v example/main.bin - """ % sys.argv[0] + """ % sys.argv[0]) if __name__ == "__main__": @@ -367,7 +369,7 @@ def usage(): try: import psyco psyco.full() - print "Using Psyco..." + print("Using Psyco...") except ImportError: pass @@ -386,9 +388,9 @@ def usage(): try: opts, args = getopt.getopt(sys.argv[1:], "hqVewvrp:b:a:l:g:") - except getopt.GetoptError, err: + except getopt.GetoptError as err: # print help information and exit: - print str(err) # will print something like "option -a not recognized" + print(str(err)) # will print something like "option -a not recognized" usage() sys.exit(2) @@ -430,7 +432,7 @@ def usage(): try: cmd.initChip() except: - print "Can't init. Ensure that BOOT0 is enabled and reset device" + print("Can't init. Ensure that BOOT0 is enabled and reset device") bootversion = cmd.cmdGet() @@ -455,13 +457,13 @@ def usage(): if conf['verify']: verify = cmd.readMemory(conf['address'], len(data)) if(data == verify): - print "Verification OK" + print("Verification OK") else: - print "Verification FAILED" - print str(len(data)) + ' vs ' + str(len(verify)) + print("Verification FAILED") + print(str(len(data)) + ' vs ' + str(len(verify))) for i in xrange(0, len(data)): if data[i] != verify[i]: - print hex(i) + ': ' + hex(data[i]) + ' vs ' + hex(verify[i]) + print(hex(i) + ': ' + hex(data[i]) + ' vs ' + hex(verify[i])) if not conf['write'] and conf['read']: rdata = cmd.readMemory(conf['address'], conf['len']) From a6ecd09e85f5bd432a243b5e3649dded74f17ed6 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Sun, 4 Mar 2018 21:29:59 +0100 Subject: [PATCH 002/369] Fix many linting (PEP8) issues --- stm32loader.py | 378 +++++++++++++++++++++++-------------------------- 1 file changed, 175 insertions(+), 203 deletions(-) diff --git a/stm32loader.py b/stm32loader.py index 6b1c0ba..8491ccd 100755 --- a/stm32loader.py +++ b/stm32loader.py @@ -22,18 +22,12 @@ # along with stm32loader; see the file COPYING3. If not see # . -from __future__ import print_function - -import sys, getopt +from functools import reduce +import sys +import getopt import serial import time -try: - from progressbar import * - usepbar = 1 -except: - usepbar = 0 - # Verbose level QUIET = 20 @@ -50,22 +44,28 @@ 0x413: "STM32F4xx", } -def mdebug(level, message): - if(QUIET >= level): + +def debug(level, message): + if QUIET >= level: print(message, file=sys.stderr) class CmdException(Exception): pass + class CommandInterface: + extended_erase = 0 - def open(self, aport='/dev/tty.usbserial-ftCYPMYJ', abaudrate=115200) : + def __init__(self): + self.sp = None + + def open(self, a_port='/dev/tty.usbserial-ftCYPMYJ', a_baud_rate=115200): self.sp = serial.Serial( - port=aport, - baudrate=abaudrate, # baudrate - bytesize=8, # number of databits + port=a_port, + baudrate=a_baud_rate, + bytesize=8, # number of write_data bits parity=serial.PARITY_EVEN, stopbits=1, xonxoff=0, # don't enable software flow control @@ -73,12 +73,11 @@ def open(self, aport='/dev/tty.usbserial-ftCYPMYJ', abaudrate=115200) : timeout=5 # set a timeout value, None for waiting forever ) - - def _wait_for_ask(self, info = ""): + def _wait_for_ask(self, info=""): # wait for ask try: ask = ord(self.sp.read()) - except: + except TypeError: raise CmdException("Can't read port or timeout") else: if ask == 0x79: @@ -89,130 +88,127 @@ def _wait_for_ask(self, info = ""): # NACK raise CmdException("NACK "+info) else: - # Unknown responce + # Unknown response raise CmdException("Unknown response. "+info+": "+hex(ask)) - def reset(self): self.sp.setDTR(0) time.sleep(0.1) self.sp.setDTR(1) time.sleep(0.5) - def initChip(self): + def init_chip(self): # Set boot self.sp.setRTS(0) self.reset() - self.sp.write("\x7F") # Syncro + # Syncro + self.sp.write("\x7F") return self._wait_for_ask("Syncro") - def releaseChip(self): + def release_chip(self): self.sp.setRTS(1) self.reset() - def cmdGeneric(self, cmd): - self.sp.write(chr(cmd)) - self.sp.write(chr(cmd ^ 0xFF)) # Control byte - return self._wait_for_ask(hex(cmd)) + def generic(self, command): + self.sp.write(chr(command)) + # control byte + self.sp.write(chr(command ^ 0xFF)) + return self._wait_for_ask(hex(command)) - def cmdGet(self): - if self.cmdGeneric(0x00): - mdebug(10, "*** Get command"); - len = ord(self.sp.read()) + def get(self): + if self.generic(0x00): + debug(10, "*** Get interface") + length = ord(self.sp.read()) version = ord(self.sp.read()) - mdebug(10, " Bootloader version: "+hex(version)) - dat = map(lambda c: hex(ord(c)), self.sp.read(len)) - if '0x44' in dat: + debug(10, " Bootloader version: " + hex(version)) + data = [hex(ord(c)) for c in self.sp.read(length)] + if '0x44' in data: self.extended_erase = 1 - mdebug(10, " Available commands: "+", ".join(dat)) + debug(10, " Available commands: " + ", ".join(data)) self._wait_for_ask("0x00 end") return version else: raise CmdException("Get (0x00) failed") - def cmdGetVersion(self): - if self.cmdGeneric(0x01): - mdebug(10, "*** GetVersion command") + def get_version(self): + if self.generic(0x01): + debug(10, "*** GetVersion interface") version = ord(self.sp.read()) self.sp.read(2) self._wait_for_ask("0x01 end") - mdebug(10, " Bootloader version: "+hex(version)) + debug(10, " Bootloader version: " + hex(version)) return version else: raise CmdException("GetVersion (0x01) failed") - def cmdGetID(self): - if self.cmdGeneric(0x02): - mdebug(10, "*** GetID command") - len = ord(self.sp.read()) - id = self.sp.read(len+1) + def get_id(self): + if self.generic(0x02): + debug(10, "*** GetID interface") + length = ord(self.sp.read()) + id_data = self.sp.read(length+1) self._wait_for_ask("0x02 end") - return reduce(lambda x, y: x*0x100+y, map(ord, id)) + _device_id = reduce(lambda x, y: x*0x100+y, map(ord, id_data)) + return _device_id else: raise CmdException("GetID (0x02) failed") - - def _encode_addr(self, addr): - byte3 = (addr >> 0) & 0xFF - byte2 = (addr >> 8) & 0xFF - byte1 = (addr >> 16) & 0xFF - byte0 = (addr >> 24) & 0xFF + @staticmethod + def _encode_address(address): + byte3 = (address >> 0) & 0xFF + byte2 = (address >> 8) & 0xFF + byte1 = (address >> 16) & 0xFF + byte0 = (address >> 24) & 0xFF crc = byte0 ^ byte1 ^ byte2 ^ byte3 - return (chr(byte0) + chr(byte1) + chr(byte2) + chr(byte3) + chr(crc)) + return chr(byte0) + chr(byte1) + chr(byte2) + chr(byte3) + chr(crc) - - def cmdReadMemory(self, addr, lng): - assert(lng <= 256) - if self.cmdGeneric(0x11): - mdebug(10, "*** ReadMemory command") - self.sp.write(self._encode_addr(addr)) + def read_memory(self, address, length): + assert(length <= 256) + if self.generic(0x11): + debug(10, "*** ReadMemory interface") + self.sp.write(self._encode_address(address)) self._wait_for_ask("0x11 address failed") - N = (lng - 1) & 0xFF - crc = N ^ 0xFF - self.sp.write(chr(N) + chr(crc)) + n = (length - 1) & 0xFF + crc = n ^ 0xFF + self.sp.write(chr(n) + chr(crc)) self._wait_for_ask("0x11 length failed") - return map(lambda c: ord(c), self.sp.read(lng)) + return [ord(c) for c in self.sp.read(length)] else: raise CmdException("ReadMemory (0x11) failed") - - def cmdGo(self, addr): - if self.cmdGeneric(0x21): - mdebug(10, "*** Go command") - self.sp.write(self._encode_addr(addr)) + def go(self, address): + if self.generic(0x21): + debug(10, "*** Go interface") + self.sp.write(self._encode_address(address)) self._wait_for_ask("0x21 go failed") else: raise CmdException("Go (0x21) failed") - - def cmdWriteMemory(self, addr, data): + def write_memory(self, address, data): assert(len(data) <= 256) - if self.cmdGeneric(0x31): - mdebug(10, "*** Write memory command") - self.sp.write(self._encode_addr(addr)) + if self.generic(0x31): + debug(10, "*** Write memory interface") + self.sp.write(self._encode_address(address)) self._wait_for_ask("0x31 address failed") - #map(lambda c: hex(ord(c)), data) - lng = (len(data)-1) & 0xFF - mdebug(10, " %s bytes to write" % [lng+1]); - self.sp.write(chr(lng)) # len really + length = (len(data)-1) & 0xFF + debug(10, " %s bytes to write" % [length + 1]) + self.sp.write(chr(length)) # len really crc = 0xFF for c in data: crc = crc ^ c self.sp.write(chr(c)) self.sp.write(chr(crc)) self._wait_for_ask("0x31 programming failed") - mdebug(10, " Write memory done") + debug(10, " Write memory done") else: raise CmdException("Write memory (0x31) failed") - - def cmdEraseMemory(self, sectors = None): + def erase_memory(self, sectors=None): if self.extended_erase: - return cmd.cmdExtendedEraseMemory() + return interface.extended_erase_memory() - if self.cmdGeneric(0x43): - mdebug(10, "*** Erase memory command") + if self.generic(0x43): + debug(10, "*** Erase memory interface") if sectors is None: # Global erase self.sp.write(chr(0xFF)) @@ -226,13 +222,13 @@ def cmdEraseMemory(self, sectors = None): self.sp.write(chr(c)) self.sp.write(chr(crc)) self._wait_for_ask("0x43 erasing failed") - mdebug(10, " Erase memory done") + debug(10, " Erase memory done") else: raise CmdException("Erase memory (0x43) failed") - def cmdExtendedEraseMemory(self): - if self.cmdGeneric(0x44): - mdebug(10, "*** Extended Erase memory command") + def extended_erase_memory(self): + if self.generic(0x44): + debug(10, "*** Extended Erase memory interface") # Global mass erase self.sp.write(chr(0xFF)) self.sp.write(chr(0xFF)) @@ -243,13 +239,13 @@ def cmdExtendedEraseMemory(self): print("Extended erase (0x44), this can take ten seconds or more") self._wait_for_ask("0x44 erasing failed") self.sp.timeout = tmp - mdebug(10, " Extended Erase memory done") + debug(10, " Extended Erase memory done") else: raise CmdException("Extended Erase memory (0x44) failed") - def cmdWriteProtect(self, sectors): - if self.cmdGeneric(0x63): - mdebug(10, "*** Write protect command") + def write_protect(self, sectors): + if self.generic(0x63): + debug(10, "*** Write protect interface") self.sp.write(chr((len(sectors)-1) & 0xFF)) crc = 0xFF for c in sectors: @@ -257,94 +253,68 @@ def cmdWriteProtect(self, sectors): self.sp.write(chr(c)) self.sp.write(chr(crc)) self._wait_for_ask("0x63 write protect failed") - mdebug(10, " Write protect done") + debug(10, " Write protect done") else: raise CmdException("Write Protect memory (0x63) failed") - def cmdWriteUnprotect(self): - if self.cmdGeneric(0x73): - mdebug(10, "*** Write Unprotect command") + def write_unprotect(self): + if self.generic(0x73): + debug(10, "*** Write Unprotect interface") self._wait_for_ask("0x73 write unprotect failed") self._wait_for_ask("0x73 write unprotect 2 failed") - mdebug(10, " Write Unprotect done") + debug(10, " Write Unprotect done") else: raise CmdException("Write Unprotect (0x73) failed") - def cmdReadoutProtect(self): - if self.cmdGeneric(0x82): - mdebug(10, "*** Readout protect command") + def readout_protect(self): + if self.generic(0x82): + debug(10, "*** Readout protect interface") self._wait_for_ask("0x82 readout protect failed") self._wait_for_ask("0x82 readout protect 2 failed") - mdebug(10, " Read protect done") + debug(10, " Read protect done") else: raise CmdException("Readout protect (0x82) failed") - def cmdReadoutUnprotect(self): - if self.cmdGeneric(0x92): - mdebug(10, "*** Readout Unprotect command") + def readout_unprotect(self): + if self.generic(0x92): + debug(10, "*** Readout Unprotect interface") self._wait_for_ask("0x92 readout unprotect failed") self._wait_for_ask("0x92 readout unprotect 2 failed") - mdebug(10, " Read Unprotect done") + debug(10, " Read Unprotect done") else: raise CmdException("Readout unprotect (0x92) failed") # Complex commands section - def readMemory(self, addr, lng): + def read_memory_data(self, address, length): data = [] - if usepbar: - widgets = ['Reading: ', Percentage(),', ', ETA(), ' ', Bar()] - pbar = ProgressBar(widgets=widgets,maxval=lng, term_width=79).start() - - while lng > 256: - if usepbar: - pbar.update(pbar.maxval-lng) - else: - mdebug(5, "Read %(len)d bytes at 0x%(addr)X" % {'addr': addr, 'len': 256}) - data = data + self.cmdReadMemory(addr, 256) - addr = addr + 256 - lng = lng - 256 - if usepbar: - pbar.update(pbar.maxval-lng) - pbar.finish() + while length > 256: + debug(5, "Read %(len)d bytes at 0x%(address)X" % {'address': address, 'len': 256}) + data = data + self.read_memory(address, 256) + address = address + 256 + length = length - 256 else: - mdebug(5, "Read %(len)d bytes at 0x%(addr)X" % {'addr': addr, 'len': 256}) - data = data + self.cmdReadMemory(addr, lng) + debug(5, "Read %(len)d bytes at 0x%(address)X" % {'address': address, 'len': 256}) + data = data + self.read_memory(address, length) return data - def writeMemory(self, addr, data): - lng = len(data) - if usepbar: - widgets = ['Writing: ', Percentage(),' ', ETA(), ' ', Bar()] - pbar = ProgressBar(widgets=widgets, maxval=lng, term_width=79).start() - + def write_memory_data(self, address, data): + length = len(data) offs = 0 - while lng > 256: - if usepbar: - pbar.update(pbar.maxval-lng) - else: - mdebug(5, "Write %(len)d bytes at 0x%(addr)X" % {'addr': addr, 'len': 256}) - self.cmdWriteMemory(addr, data[offs:offs+256]) + while length > 256: + debug(5, "Write %(len)d bytes at 0x%(address)X" % {'address': address, 'len': 256}) + self.write_memory(address, data[offs:offs + 256]) offs = offs + 256 - addr = addr + 256 - lng = lng - 256 - if usepbar: - pbar.update(pbar.maxval-lng) - pbar.finish() + address = address + 256 + length = length - 256 else: - mdebug(5, "Write %(len)d bytes at 0x%(addr)X" % {'addr': addr, 'len': 256}) - self.cmdWriteMemory(addr, data[offs:offs+lng] + ([0xFF] * (256-lng)) ) - - - - - def __init__(self) : - pass + debug(5, "Write %(len)d bytes at 0x%(address)X" % {'address': address, 'len': 256}) + self.write_memory(address, data[offs:offs + length] + ([0xFF] * (256 - length))) def usage(): - print("""Usage: %s [-hqVewvr] [-l length] [-p port] [-b baud] [-a addr] [-g addr] [file.bin] + print("""Usage: %s [-hqVewvr] [-l length] [-p port] [-b baud] [-a address] [-g address] [file.bin] -h This help -q Quiet -V Verbose @@ -355,8 +325,8 @@ def usage(): -l length Length of read -p port Serial port (default: /dev/tty.usbserial-ftCYPMYJ) -b baud Baud speed (default: 115200) - -a addr Target address - -g addr Address to start running at (0x08000000, usually) + -a address Target address + -g address Address to start running at (0x08000000, usually) ./stm32loader.py -e -w -v example/main.bin @@ -371,18 +341,19 @@ def usage(): psyco.full() print("Using Psyco...") except ImportError: + psyco = None pass - conf = { - 'port': '/dev/tty.usbserial-ftCYPMYJ', - 'baud': 115200, - 'address': 0x08000000, - 'erase': 0, - 'write': 0, - 'verify': 0, - 'read': 0, - 'go_addr':-1, - } + configuration = { + 'port': '/dev/tty.usbserial-ftCYPMYJ', + 'baud': 115200, + 'address': 0x08000000, + 'erase': 0, + 'write': 0, + 'verify': 0, + 'read': 0, + 'go_address': -1, + } # http://www.python.org/doc/2.5.2/lib/module-getopt.html @@ -390,7 +361,8 @@ def usage(): opts, args = getopt.getopt(sys.argv[1:], "hqVewvrp:b:a:l:g:") except getopt.GetoptError as err: # print help information and exit: - print(str(err)) # will print something like "option -a not recognized" + # this print something like "option -a not recognized" + print(str(err)) usage() sys.exit(2) @@ -405,73 +377,73 @@ def usage(): usage() sys.exit(0) elif o == '-e': - conf['erase'] = 1 + configuration['erase'] = 1 elif o == '-w': - conf['write'] = 1 + configuration['write'] = 1 elif o == '-v': - conf['verify'] = 1 + configuration['verify'] = 1 elif o == '-r': - conf['read'] = 1 + configuration['read'] = 1 elif o == '-p': - conf['port'] = a + configuration['port'] = a elif o == '-b': - conf['baud'] = eval(a) + configuration['baud'] = eval(a) elif o == '-a': - conf['address'] = eval(a) + configuration['address'] = eval(a) elif o == '-g': - conf['go_addr'] = eval(a) + configuration['go_address'] = eval(a) elif o == '-l': - conf['len'] = eval(a) + configuration['len'] = eval(a) else: assert False, "unhandled option" - cmd = CommandInterface() - cmd.open(conf['port'], conf['baud']) - mdebug(10, "Open port %(port)s, baud %(baud)d" % {'port':conf['port'], 'baud':conf['baud']}) + interface = CommandInterface() + interface.open(configuration['port'], configuration['baud']) + debug(10, "Open port %(port)s, baud %(baud)d" % {'port': configuration['port'], 'baud': configuration['baud']}) try: try: - cmd.initChip() - except: + interface.init_chip() + except Exception: print("Can't init. Ensure that BOOT0 is enabled and reset device") + boot_version = interface.get() + debug(0, "Bootloader version %X" % boot_version) + device_id = interface.get_id() + debug(0, "Chip id: 0x%x (%s)" % (device_id, chip_ids.get(device_id, "Unknown"))) +# interface.get_version() +# interface.get_id() +# interface.readout_unprotect() +# interface.write_unprotect() +# interface.write_protect([0, 1]) - bootversion = cmd.cmdGet() - mdebug(0, "Bootloader version %X" % bootversion) - id = cmd.cmdGetID() - mdebug(0, "Chip id: 0x%x (%s)" % (id, chip_ids.get(id, "Unknown"))) -# cmd.cmdGetVersion() -# cmd.cmdGetID() -# cmd.cmdReadoutUnprotect() -# cmd.cmdWriteUnprotect() -# cmd.cmdWriteProtect([0, 1]) + write_data = None - if (conf['write'] or conf['verify']): - data = map(lambda c: ord(c), file(args[0], 'rb').read()) + if configuration['write'] or configuration['verify']: + write_data = open(args[0], 'rb').read() - if conf['erase']: - cmd.cmdEraseMemory() + if configuration['erase']: + interface.erase_memory() - if conf['write']: - cmd.writeMemory(conf['address'], data) + if configuration['write']: + interface.write_memory_data(configuration['address'], write_data) - if conf['verify']: - verify = cmd.readMemory(conf['address'], len(data)) - if(data == verify): + if configuration['verify']: + verify = interface.read_memory_data(configuration['address'], len(write_data)) + if write_data == verify: print("Verification OK") else: print("Verification FAILED") - print(str(len(data)) + ' vs ' + str(len(verify))) - for i in xrange(0, len(data)): - if data[i] != verify[i]: - print(hex(i) + ': ' + hex(data[i]) + ' vs ' + hex(verify[i])) + print(str(len(write_data)) + ' vs ' + str(len(verify))) + for i in range(0, len(write_data)): + if write_data[i] != verify[i]: + print(hex(i) + ': ' + hex(write_data[i]) + ' vs ' + hex(verify[i])) - if not conf['write'] and conf['read']: - rdata = cmd.readMemory(conf['address'], conf['len']) - file(args[0], 'wb').write(''.join(map(chr,rdata))) + if not configuration['write'] and configuration['read']: + read_data = interface.read_memory_data(configuration['address'], configuration['len']) + open(args[0], 'wb').write(read_data) - if conf['go_addr'] != -1: - cmd.cmdGo(conf['go_addr']) + if configuration['go_address'] != -1: + interface.go(configuration['go_address']) finally: - cmd.releaseChip() - + interface.release_chip() From ca6c72c2cc00d245a9601c6822a47c364f8833a6 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Sun, 4 Mar 2018 21:34:47 +0100 Subject: [PATCH 003/369] Simplify code by doing check-first (early return) --- stm32loader.py | 225 ++++++++++++++++++++++++------------------------- 1 file changed, 112 insertions(+), 113 deletions(-) diff --git a/stm32loader.py b/stm32loader.py index 8491ccd..8920e7c 100755 --- a/stm32loader.py +++ b/stm32loader.py @@ -117,42 +117,41 @@ def generic(self, command): return self._wait_for_ask(hex(command)) def get(self): - if self.generic(0x00): - debug(10, "*** Get interface") - length = ord(self.sp.read()) - version = ord(self.sp.read()) - debug(10, " Bootloader version: " + hex(version)) - data = [hex(ord(c)) for c in self.sp.read(length)] - if '0x44' in data: - self.extended_erase = 1 - debug(10, " Available commands: " + ", ".join(data)) - self._wait_for_ask("0x00 end") - return version - else: + if not self.generic(0x00): raise CmdException("Get (0x00) failed") + debug(10, "*** Get interface") + length = ord(self.sp.read()) + version = ord(self.sp.read()) + debug(10, " Bootloader version: " + hex(version)) + data = [hex(ord(c)) for c in self.sp.read(length)] + if '0x44' in data: + self.extended_erase = 1 + debug(10, " Available commands: " + ", ".join(data)) + self._wait_for_ask("0x00 end") + return version def get_version(self): - if self.generic(0x01): - debug(10, "*** GetVersion interface") - version = ord(self.sp.read()) - self.sp.read(2) - self._wait_for_ask("0x01 end") - debug(10, " Bootloader version: " + hex(version)) - return version - else: + if not self.generic(0x01): raise CmdException("GetVersion (0x01) failed") + debug(10, "*** GetVersion interface") + version = ord(self.sp.read()) + self.sp.read(2) + self._wait_for_ask("0x01 end") + debug(10, " Bootloader version: " + hex(version)) + return version + def get_id(self): - if self.generic(0x02): - debug(10, "*** GetID interface") - length = ord(self.sp.read()) - id_data = self.sp.read(length+1) - self._wait_for_ask("0x02 end") - _device_id = reduce(lambda x, y: x*0x100+y, map(ord, id_data)) - return _device_id - else: + if not self.generic(0x02): raise CmdException("GetID (0x02) failed") + debug(10, "*** GetID interface") + length = ord(self.sp.read()) + id_data = self.sp.read(length+1) + self._wait_for_ask("0x02 end") + _device_id = reduce(lambda x, y: x*0x100+y, map(ord, id_data)) + return _device_id + @staticmethod def _encode_address(address): byte3 = (address >> 0) & 0xFF @@ -164,126 +163,126 @@ def _encode_address(address): def read_memory(self, address, length): assert(length <= 256) - if self.generic(0x11): - debug(10, "*** ReadMemory interface") - self.sp.write(self._encode_address(address)) - self._wait_for_ask("0x11 address failed") - n = (length - 1) & 0xFF - crc = n ^ 0xFF - self.sp.write(chr(n) + chr(crc)) - self._wait_for_ask("0x11 length failed") - return [ord(c) for c in self.sp.read(length)] - else: + if not self.generic(0x11): raise CmdException("ReadMemory (0x11) failed") + debug(10, "*** ReadMemory interface") + self.sp.write(self._encode_address(address)) + self._wait_for_ask("0x11 address failed") + n = (length - 1) & 0xFF + crc = n ^ 0xFF + self.sp.write(chr(n) + chr(crc)) + self._wait_for_ask("0x11 length failed") + return [ord(c) for c in self.sp.read(length)] + def go(self, address): - if self.generic(0x21): - debug(10, "*** Go interface") - self.sp.write(self._encode_address(address)) - self._wait_for_ask("0x21 go failed") - else: + if not self.generic(0x21): raise CmdException("Go (0x21) failed") + debug(10, "*** Go interface") + self.sp.write(self._encode_address(address)) + self._wait_for_ask("0x21 go failed") + def write_memory(self, address, data): assert(len(data) <= 256) - if self.generic(0x31): - debug(10, "*** Write memory interface") - self.sp.write(self._encode_address(address)) - self._wait_for_ask("0x31 address failed") - length = (len(data)-1) & 0xFF - debug(10, " %s bytes to write" % [length + 1]) - self.sp.write(chr(length)) # len really - crc = 0xFF - for c in data: - crc = crc ^ c - self.sp.write(chr(c)) - self.sp.write(chr(crc)) - self._wait_for_ask("0x31 programming failed") - debug(10, " Write memory done") - else: + if not self.generic(0x31): raise CmdException("Write memory (0x31) failed") + debug(10, "*** Write memory interface") + self.sp.write(self._encode_address(address)) + self._wait_for_ask("0x31 address failed") + length = (len(data)-1) & 0xFF + debug(10, " %s bytes to write" % [length + 1]) + self.sp.write(chr(length)) # len really + crc = 0xFF + for c in data: + crc = crc ^ c + self.sp.write(chr(c)) + self.sp.write(chr(crc)) + self._wait_for_ask("0x31 programming failed") + debug(10, " Write memory done") + def erase_memory(self, sectors=None): if self.extended_erase: return interface.extended_erase_memory() - if self.generic(0x43): - debug(10, "*** Erase memory interface") - if sectors is None: - # Global erase - self.sp.write(chr(0xFF)) - self.sp.write(chr(0x00)) - else: - # Sectors erase - self.sp.write(chr((len(sectors)-1) & 0xFF)) - crc = 0xFF - for c in sectors: - crc = crc ^ c - self.sp.write(chr(c)) - self.sp.write(chr(crc)) - self._wait_for_ask("0x43 erasing failed") - debug(10, " Erase memory done") - else: + if not self.generic(0x43): raise CmdException("Erase memory (0x43) failed") - def extended_erase_memory(self): - if self.generic(0x44): - debug(10, "*** Extended Erase memory interface") - # Global mass erase + debug(10, "*** Erase memory interface") + if sectors is None: + # Global erase self.sp.write(chr(0xFF)) - self.sp.write(chr(0xFF)) - # Checksum self.sp.write(chr(0x00)) - tmp = self.sp.timeout - self.sp.timeout = 30 - print("Extended erase (0x44), this can take ten seconds or more") - self._wait_for_ask("0x44 erasing failed") - self.sp.timeout = tmp - debug(10, " Extended Erase memory done") else: - raise CmdException("Extended Erase memory (0x44) failed") - - def write_protect(self, sectors): - if self.generic(0x63): - debug(10, "*** Write protect interface") + # Sectors erase self.sp.write(chr((len(sectors)-1) & 0xFF)) crc = 0xFF for c in sectors: crc = crc ^ c self.sp.write(chr(c)) self.sp.write(chr(crc)) - self._wait_for_ask("0x63 write protect failed") - debug(10, " Write protect done") - else: + self._wait_for_ask("0x43 erasing failed") + debug(10, " Erase memory done") + + def extended_erase_memory(self): + if not self.generic(0x44): + raise CmdException("Extended Erase memory (0x44) failed") + + debug(10, "*** Extended Erase memory interface") + # Global mass erase + self.sp.write(chr(0xFF)) + self.sp.write(chr(0xFF)) + # Checksum + self.sp.write(chr(0x00)) + tmp = self.sp.timeout + self.sp.timeout = 30 + print("Extended erase (0x44), this can take ten seconds or more") + self._wait_for_ask("0x44 erasing failed") + self.sp.timeout = tmp + debug(10, " Extended Erase memory done") + + def write_protect(self, sectors): + if not self.generic(0x63): raise CmdException("Write Protect memory (0x63) failed") + debug(10, "*** Write protect interface") + self.sp.write(chr((len(sectors)-1) & 0xFF)) + crc = 0xFF + for c in sectors: + crc = crc ^ c + self.sp.write(chr(c)) + self.sp.write(chr(crc)) + self._wait_for_ask("0x63 write protect failed") + debug(10, " Write protect done") + def write_unprotect(self): - if self.generic(0x73): - debug(10, "*** Write Unprotect interface") - self._wait_for_ask("0x73 write unprotect failed") - self._wait_for_ask("0x73 write unprotect 2 failed") - debug(10, " Write Unprotect done") - else: + if not self.generic(0x73): raise CmdException("Write Unprotect (0x73) failed") + debug(10, "*** Write Unprotect interface") + self._wait_for_ask("0x73 write unprotect failed") + self._wait_for_ask("0x73 write unprotect 2 failed") + debug(10, " Write Unprotect done") + def readout_protect(self): - if self.generic(0x82): - debug(10, "*** Readout protect interface") - self._wait_for_ask("0x82 readout protect failed") - self._wait_for_ask("0x82 readout protect 2 failed") - debug(10, " Read protect done") - else: + if not self.generic(0x82): raise CmdException("Readout protect (0x82) failed") + debug(10, "*** Readout protect interface") + self._wait_for_ask("0x82 readout protect failed") + self._wait_for_ask("0x82 readout protect 2 failed") + debug(10, " Read protect done") + def readout_unprotect(self): - if self.generic(0x92): - debug(10, "*** Readout Unprotect interface") - self._wait_for_ask("0x92 readout unprotect failed") - self._wait_for_ask("0x92 readout unprotect 2 failed") - debug(10, " Read Unprotect done") - else: + if not self.generic(0x92): raise CmdException("Readout unprotect (0x92) failed") + debug(10, "*** Readout Unprotect interface") + self._wait_for_ask("0x92 readout unprotect failed") + self._wait_for_ask("0x92 readout unprotect 2 failed") + debug(10, " Read Unprotect done") + # Complex commands section From 48a346995d2ad146bd283a67550a0239c89e8ffd Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Sun, 4 Mar 2018 21:49:59 +0100 Subject: [PATCH 004/369] Replace ord() and chr() with Python 3 constructs --- stm32loader.py | 94 ++++++++++++++++++++++++++------------------------ 1 file changed, 48 insertions(+), 46 deletions(-) diff --git a/stm32loader.py b/stm32loader.py index 8920e7c..0b9a31e 100755 --- a/stm32loader.py +++ b/stm32loader.py @@ -59,10 +59,10 @@ class CommandInterface: extended_erase = 0 def __init__(self): - self.sp = None + self.serial = None def open(self, a_port='/dev/tty.usbserial-ftCYPMYJ', a_baud_rate=115200): - self.sp = serial.Serial( + self.serial = serial.Serial( port=a_port, baudrate=a_baud_rate, bytesize=8, # number of write_data bits @@ -76,7 +76,7 @@ def open(self, a_port='/dev/tty.usbserial-ftCYPMYJ', a_baud_rate=115200): def _wait_for_ask(self, info=""): # wait for ask try: - ask = ord(self.sp.read()) + ask = self.serial.read()[0] except TypeError: raise CmdException("Can't read port or timeout") else: @@ -92,38 +92,41 @@ def _wait_for_ask(self, info=""): raise CmdException("Unknown response. "+info+": "+hex(ask)) def reset(self): - self.sp.setDTR(0) + self.serial.setDTR(0) time.sleep(0.1) - self.sp.setDTR(1) + self.serial.setDTR(1) time.sleep(0.5) def init_chip(self): # Set boot - self.sp.setRTS(0) + self.serial.setRTS(0) self.reset() # Syncro - self.sp.write("\x7F") + self.serial.write(b'\x7F') return self._wait_for_ask("Syncro") def release_chip(self): - self.sp.setRTS(1) + self.serial.setRTS(1) self.reset() def generic(self, command): - self.sp.write(chr(command)) - # control byte - self.sp.write(chr(command ^ 0xFF)) + command_byte = bytes([command]) + control_byte = bytes([command ^ 0xFF]) + + self.serial.write(command_byte) + self.serial.write(control_byte) + return self._wait_for_ask(hex(command)) def get(self): if not self.generic(0x00): raise CmdException("Get (0x00) failed") debug(10, "*** Get interface") - length = ord(self.sp.read()) - version = ord(self.sp.read()) + length = self.serial.read()[0] + version = self.serial.read()[0] debug(10, " Bootloader version: " + hex(version)) - data = [hex(ord(c)) for c in self.sp.read(length)] + data = [hex(c) for c in self.serial.read(length)] if '0x44' in data: self.extended_erase = 1 debug(10, " Available commands: " + ", ".join(data)) @@ -135,8 +138,8 @@ def get_version(self): raise CmdException("GetVersion (0x01) failed") debug(10, "*** GetVersion interface") - version = ord(self.sp.read()) - self.sp.read(2) + version = self.serial.read()[0] + self.serial.read(2) self._wait_for_ask("0x01 end") debug(10, " Bootloader version: " + hex(version)) return version @@ -146,10 +149,10 @@ def get_id(self): raise CmdException("GetID (0x02) failed") debug(10, "*** GetID interface") - length = ord(self.sp.read()) - id_data = self.sp.read(length+1) + length = self.serial.read()[0] + id_data = self.serial.read(length + 1) self._wait_for_ask("0x02 end") - _device_id = reduce(lambda x, y: x*0x100+y, map(ord, id_data)) + _device_id = reduce(lambda x, y: x*0x100+y, id_data) return _device_id @staticmethod @@ -159,7 +162,7 @@ def _encode_address(address): byte1 = (address >> 16) & 0xFF byte0 = (address >> 24) & 0xFF crc = byte0 ^ byte1 ^ byte2 ^ byte3 - return chr(byte0) + chr(byte1) + chr(byte2) + chr(byte3) + chr(crc) + return bytes([byte0, byte1, byte2, byte3, crc]) def read_memory(self, address, length): assert(length <= 256) @@ -167,20 +170,20 @@ def read_memory(self, address, length): raise CmdException("ReadMemory (0x11) failed") debug(10, "*** ReadMemory interface") - self.sp.write(self._encode_address(address)) + self.serial.write(self._encode_address(address)) self._wait_for_ask("0x11 address failed") n = (length - 1) & 0xFF crc = n ^ 0xFF - self.sp.write(chr(n) + chr(crc)) + self.serial.write(bytes([n, crc])) self._wait_for_ask("0x11 length failed") - return [ord(c) for c in self.sp.read(length)] + return self.serial.read(length) def go(self, address): if not self.generic(0x21): raise CmdException("Go (0x21) failed") debug(10, "*** Go interface") - self.sp.write(self._encode_address(address)) + self.serial.write(self._encode_address(address)) self._wait_for_ask("0x21 go failed") def write_memory(self, address, data): @@ -189,16 +192,16 @@ def write_memory(self, address, data): raise CmdException("Write memory (0x31) failed") debug(10, "*** Write memory interface") - self.sp.write(self._encode_address(address)) + self.serial.write(self._encode_address(address)) self._wait_for_ask("0x31 address failed") length = (len(data)-1) & 0xFF debug(10, " %s bytes to write" % [length + 1]) - self.sp.write(chr(length)) # len really + self.serial.write(bytes([length])) crc = 0xFF for c in data: crc = crc ^ c - self.sp.write(chr(c)) - self.sp.write(chr(crc)) + self.serial.write(bytes([c])) + self.serial.write(bytes([crc])) self._wait_for_ask("0x31 programming failed") debug(10, " Write memory done") @@ -212,16 +215,16 @@ def erase_memory(self, sectors=None): debug(10, "*** Erase memory interface") if sectors is None: # Global erase - self.sp.write(chr(0xFF)) - self.sp.write(chr(0x00)) + self.serial.write(b'\xff') + self.serial.write(b'\x00') else: # Sectors erase - self.sp.write(chr((len(sectors)-1) & 0xFF)) + self.serial.write(bytes([(len(sectors) - 1) & 0xFF])) crc = 0xFF for c in sectors: crc = crc ^ c - self.sp.write(chr(c)) - self.sp.write(chr(crc)) + self.serial.write(bytes([c])) + self.serial.write(bytes([crc])) self._wait_for_ask("0x43 erasing failed") debug(10, " Erase memory done") @@ -230,16 +233,15 @@ def extended_erase_memory(self): raise CmdException("Extended Erase memory (0x44) failed") debug(10, "*** Extended Erase memory interface") - # Global mass erase - self.sp.write(chr(0xFF)) - self.sp.write(chr(0xFF)) - # Checksum - self.sp.write(chr(0x00)) - tmp = self.sp.timeout - self.sp.timeout = 30 + # Global mass erase and checksum byte + self.serial.write(b'\xFF') + self.serial.write(b'\xFF') + self.serial.write(b'\x00') + tmp = self.serial.timeout + self.serial.timeout = 30 print("Extended erase (0x44), this can take ten seconds or more") self._wait_for_ask("0x44 erasing failed") - self.sp.timeout = tmp + self.serial.timeout = tmp debug(10, " Extended Erase memory done") def write_protect(self, sectors): @@ -247,12 +249,12 @@ def write_protect(self, sectors): raise CmdException("Write Protect memory (0x63) failed") debug(10, "*** Write protect interface") - self.sp.write(chr((len(sectors)-1) & 0xFF)) + self.serial.write(bytes([((len(sectors) - 1) & 0xFF)])) crc = 0xFF for c in sectors: crc = crc ^ c - self.sp.write(chr(c)) - self.sp.write(chr(crc)) + self.serial.write(bytes([c])) + self.serial.write(bytes([crc])) self._wait_for_ask("0x63 write protect failed") debug(10, " Write protect done") @@ -287,7 +289,7 @@ def readout_unprotect(self): # Complex commands section def read_memory_data(self, address, length): - data = [] + data = bytearray() while length > 256: debug(5, "Read %(len)d bytes at 0x%(address)X" % {'address': address, 'len': 256}) data = data + self.read_memory(address, 256) @@ -309,7 +311,7 @@ def write_memory_data(self, address, data): length = length - 256 else: debug(5, "Write %(len)d bytes at 0x%(address)X" % {'address': address, 'len': 256}) - self.write_memory(address, data[offs:offs + length] + ([0xFF] * (256 - length))) + self.write_memory(address, data[offs:offs + length] + (b'\xff' * (256 - length))) def usage(): From b746cf6f275038fb95f978c3eaf52b23a3854b0d Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 6 Mar 2018 19:22:26 +0100 Subject: [PATCH 005/369] Rename some variables --- stm32loader.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/stm32loader.py b/stm32loader.py index 0b9a31e..4c11097 100755 --- a/stm32loader.py +++ b/stm32loader.py @@ -214,7 +214,7 @@ def erase_memory(self, sectors=None): debug(10, "*** Erase memory interface") if sectors is None: - # Global erase + # Global erase and checksum byte self.serial.write(b'\xff') self.serial.write(b'\x00') else: @@ -394,7 +394,7 @@ def usage(): elif o == '-g': configuration['go_address'] = eval(a) elif o == '-l': - configuration['len'] = eval(a) + configuration['length'] = eval(a) else: assert False, "unhandled option" @@ -417,31 +417,32 @@ def usage(): # interface.write_unprotect() # interface.write_protect([0, 1]) - write_data = None + binary_data = None + data_file = args[0] if args else None if configuration['write'] or configuration['verify']: - write_data = open(args[0], 'rb').read() + binary_data = open(data_file, 'rb').read() if configuration['erase']: interface.erase_memory() if configuration['write']: - interface.write_memory_data(configuration['address'], write_data) + interface.write_memory_data(configuration['address'], binary_data) if configuration['verify']: - verify = interface.read_memory_data(configuration['address'], len(write_data)) - if write_data == verify: + read_data = interface.read_memory_data(configuration['address'], len(binary_data)) + if binary_data == read_data: print("Verification OK") else: print("Verification FAILED") - print(str(len(write_data)) + ' vs ' + str(len(verify))) - for i in range(0, len(write_data)): - if write_data[i] != verify[i]: - print(hex(i) + ': ' + hex(write_data[i]) + ' vs ' + hex(verify[i])) + print(str(len(binary_data)) + ' vs ' + str(len(read_data))) + for i in range(0, len(binary_data)): + if binary_data[i] != read_data[i]: + print(hex(i) + ': ' + hex(binary_data[i]) + ' vs ' + hex(read_data[i])) if not configuration['write'] and configuration['read']: - read_data = interface.read_memory_data(configuration['address'], configuration['len']) - open(args[0], 'wb').write(read_data) + read_data = interface.read_memory_data(configuration['address'], configuration['length']) + open(data_file, 'wb').write(read_data) if configuration['go_address'] != -1: interface.go(configuration['go_address']) From 182bc8e492094106bc1f529633877ed4efa26c6e Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Sun, 4 Mar 2018 15:23:06 +0100 Subject: [PATCH 006/369] Add markdown file extension to README (README.md) This improves the rendering in GitHub. --- README => README.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename README => README.md (100%) diff --git a/README b/README.md similarity index 100% rename from README rename to README.md From fd09bae57c990ed129f7b0997f572ca48c28c933 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Sun, 4 Mar 2018 15:29:00 +0100 Subject: [PATCH 007/369] Add section headers and code blocks --- README.md | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index ac096dc..09a544c 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,14 @@ STM32Loader Python script which will talk to the STM32 bootloader to upload and download firmware. -Original Version by: Ivan A-R +Original Version by: Ivan A-R . -Usage: ./stm32loader.py [-hqVewvr] [-l length] [-p port] [-b baud] [-a addr] [file.bin] +Usage +----- + +``` +./stm32loader.py [-hqVewvr] [-l length] [-p port] [-b baud] [-a addr] [file.bin] -h This help -q Quiet -V Verbose @@ -17,13 +21,15 @@ Usage: ./stm32loader.py [-hqVewvr] [-l length] [-p port] [-b baud] [-a addr] [fi -l length Length of read -p port Serial port (default: /dev/tty.usbserial-ftCYPMYJ) -b baud Baud speed (default: 115200) - -a addr Target address + -a addr Target address +``` - ./stm32loader.py -e -w -v example/main.bin +Example +------- -Example: +``` stm32loader.py -e -w -v somefile.bin +``` -This will pre-erase flash, write somefile.bin to the flash on the device, and then perform a verification after writing is finished. - +This will pre-erase flash, write `somefile.bin` to the flash on the device, and then perform a verification after writing is finished. From 8b8fc7e9274bde6a66bcb913c604d9f7664ae75a Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Sun, 4 Mar 2018 15:33:19 +0100 Subject: [PATCH 008/369] Highlight that erase is required, and verify is recommended --- README.md | 4 ++-- stm32loader.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 09a544c..687bcc8 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,9 @@ Usage -h This help -q Quiet -V Verbose - -e Erase + -e Erase (note: this is required on previously written memory) -w Write - -v Verify + -v Verify (recommended) -r Read -l length Length of read -p port Serial port (default: /dev/tty.usbserial-ftCYPMYJ) diff --git a/stm32loader.py b/stm32loader.py index 4c11097..207585f 100755 --- a/stm32loader.py +++ b/stm32loader.py @@ -319,9 +319,9 @@ def usage(): -h This help -q Quiet -V Verbose - -e Erase + -e Erase (note: this is required on previously written memory) -w Write - -v Verify + -v Verify (recommended) -r Read -l length Length of read -p port Serial port (default: /dev/tty.usbserial-ftCYPMYJ) From 5236f2cfc3b39c0316d426d5a38201703f351437 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 7 Mar 2018 09:04:07 +0100 Subject: [PATCH 009/369] Centralize values of commands and replies --- stm32loader.py | 62 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 41 insertions(+), 21 deletions(-) diff --git a/stm32loader.py b/stm32loader.py index 207585f..3ccb5ca 100755 --- a/stm32loader.py +++ b/stm32loader.py @@ -28,11 +28,12 @@ import serial import time + # Verbose level QUIET = 20 -# these come from AN2606 -chip_ids = { +CHIP_IDS = { + # see ST AN2606 0x412: "STM32 Low-density", 0x410: "STM32 Medium-density", 0x414: "STM32 High-density", @@ -56,6 +57,27 @@ class CmdException(Exception): class CommandInterface: + class Command: + # See ST AN3155 + GET = 0x00 + GET_VERSION = 0x01 + GET_ID = 0x02 + READ_MEMORY = 0x11 + GO = 0x21 + WRITE_MEMORY = 0x31 + ERASE = 0x43 + EXTENDED_ERASE = 0x44 + WRITE_PROTECT = 0x63 + WRITE_UNPROTECT = 0x73 + READOUT_PROTECT = 0x82 + READOUT_UNPROTECT = 0x92 + + class Reply: + # See ST AN3155 + ACK = 0x79 + NACK = 0x1F + EXTENDED_ERASE = 0x44 + extended_erase = 0 def __init__(self): @@ -80,12 +102,10 @@ def _wait_for_ask(self, info=""): except TypeError: raise CmdException("Can't read port or timeout") else: - if ask == 0x79: - # ACK + if ask == self.Reply.ACK: return 1 else: - if ask == 0x1F: - # NACK + if ask == self.Reply.NACK: raise CmdException("NACK "+info) else: # Unknown response @@ -120,21 +140,21 @@ def generic(self, command): return self._wait_for_ask(hex(command)) def get(self): - if not self.generic(0x00): + if not self.generic(self.Command.GET): raise CmdException("Get (0x00) failed") debug(10, "*** Get interface") length = self.serial.read()[0] version = self.serial.read()[0] debug(10, " Bootloader version: " + hex(version)) - data = [hex(c) for c in self.serial.read(length)] - if '0x44' in data: + data = self.serial.read(length) + if self.Reply.EXTENDED_ERASE in data: self.extended_erase = 1 debug(10, " Available commands: " + ", ".join(data)) self._wait_for_ask("0x00 end") return version def get_version(self): - if not self.generic(0x01): + if not self.generic(self.Command.GET_VERSION): raise CmdException("GetVersion (0x01) failed") debug(10, "*** GetVersion interface") @@ -145,7 +165,7 @@ def get_version(self): return version def get_id(self): - if not self.generic(0x02): + if not self.generic(self.Command.GET_ID): raise CmdException("GetID (0x02) failed") debug(10, "*** GetID interface") @@ -166,7 +186,7 @@ def _encode_address(address): def read_memory(self, address, length): assert(length <= 256) - if not self.generic(0x11): + if not self.generic(self.Command.READ_MEMORY): raise CmdException("ReadMemory (0x11) failed") debug(10, "*** ReadMemory interface") @@ -179,7 +199,7 @@ def read_memory(self, address, length): return self.serial.read(length) def go(self, address): - if not self.generic(0x21): + if not self.generic(self.Command.GO): raise CmdException("Go (0x21) failed") debug(10, "*** Go interface") @@ -188,7 +208,7 @@ def go(self, address): def write_memory(self, address, data): assert(len(data) <= 256) - if not self.generic(0x31): + if not self.generic(self.Command.WRITE_MEMORY): raise CmdException("Write memory (0x31) failed") debug(10, "*** Write memory interface") @@ -209,7 +229,7 @@ def erase_memory(self, sectors=None): if self.extended_erase: return interface.extended_erase_memory() - if not self.generic(0x43): + if not self.generic(self.Command.ERASE): raise CmdException("Erase memory (0x43) failed") debug(10, "*** Erase memory interface") @@ -229,7 +249,7 @@ def erase_memory(self, sectors=None): debug(10, " Erase memory done") def extended_erase_memory(self): - if not self.generic(0x44): + if not self.generic(self.Command.EXTENDED_ERASE): raise CmdException("Extended Erase memory (0x44) failed") debug(10, "*** Extended Erase memory interface") @@ -245,7 +265,7 @@ def extended_erase_memory(self): debug(10, " Extended Erase memory done") def write_protect(self, sectors): - if not self.generic(0x63): + if not self.generic(self.Command.WRITE_PROTECT): raise CmdException("Write Protect memory (0x63) failed") debug(10, "*** Write protect interface") @@ -259,7 +279,7 @@ def write_protect(self, sectors): debug(10, " Write protect done") def write_unprotect(self): - if not self.generic(0x73): + if not self.generic(self.Command.WRITE_UNPROTECT): raise CmdException("Write Unprotect (0x73) failed") debug(10, "*** Write Unprotect interface") @@ -268,7 +288,7 @@ def write_unprotect(self): debug(10, " Write Unprotect done") def readout_protect(self): - if not self.generic(0x82): + if not self.generic(self.Command.READOUT_PROTECT): raise CmdException("Readout protect (0x82) failed") debug(10, "*** Readout protect interface") @@ -277,7 +297,7 @@ def readout_protect(self): debug(10, " Read protect done") def readout_unprotect(self): - if not self.generic(0x92): + if not self.generic(self.Command.READOUT_UNPROTECT): raise CmdException("Readout unprotect (0x92) failed") debug(10, "*** Readout Unprotect interface") @@ -410,7 +430,7 @@ def usage(): boot_version = interface.get() debug(0, "Bootloader version %X" % boot_version) device_id = interface.get_id() - debug(0, "Chip id: 0x%x (%s)" % (device_id, chip_ids.get(device_id, "Unknown"))) + debug(0, "Chip id: 0x%x (%s)" % (device_id, CHIP_IDS.get(device_id, "Unknown"))) # interface.get_version() # interface.get_id() # interface.readout_unprotect() From 4ec9555228d0eeea7fbdb1e1bd2489122bb529da Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 7 Mar 2018 09:07:57 +0100 Subject: [PATCH 010/369] Rename ask to ack --- stm32loader.py | 60 ++++++++++++++++++++++++-------------------------- 1 file changed, 29 insertions(+), 31 deletions(-) diff --git a/stm32loader.py b/stm32loader.py index 3ccb5ca..ae8d466 100755 --- a/stm32loader.py +++ b/stm32loader.py @@ -95,21 +95,19 @@ def open(self, a_port='/dev/tty.usbserial-ftCYPMYJ', a_baud_rate=115200): timeout=5 # set a timeout value, None for waiting forever ) - def _wait_for_ask(self, info=""): - # wait for ask + def _wait_for_ack(self, info=""): try: - ask = self.serial.read()[0] + ack = self.serial.read()[0] except TypeError: raise CmdException("Can't read port or timeout") - else: - if ask == self.Reply.ACK: - return 1 - else: - if ask == self.Reply.NACK: - raise CmdException("NACK "+info) - else: - # Unknown response - raise CmdException("Unknown response. "+info+": "+hex(ask)) + + if ack == self.Reply.NACK: + raise CmdException("NACK " + info) + + if ack != self.Replay.ACK: + raise CmdException("Unknown response. " + info + ": " + hex(ack)) + + return 1 def reset(self): self.serial.setDTR(0) @@ -124,7 +122,7 @@ def init_chip(self): # Syncro self.serial.write(b'\x7F') - return self._wait_for_ask("Syncro") + return self._wait_for_ack("Syncro") def release_chip(self): self.serial.setRTS(1) @@ -137,7 +135,7 @@ def generic(self, command): self.serial.write(command_byte) self.serial.write(control_byte) - return self._wait_for_ask(hex(command)) + return self._wait_for_ack(hex(command)) def get(self): if not self.generic(self.Command.GET): @@ -150,7 +148,7 @@ def get(self): if self.Reply.EXTENDED_ERASE in data: self.extended_erase = 1 debug(10, " Available commands: " + ", ".join(data)) - self._wait_for_ask("0x00 end") + self._wait_for_ack("0x00 end") return version def get_version(self): @@ -160,7 +158,7 @@ def get_version(self): debug(10, "*** GetVersion interface") version = self.serial.read()[0] self.serial.read(2) - self._wait_for_ask("0x01 end") + self._wait_for_ack("0x01 end") debug(10, " Bootloader version: " + hex(version)) return version @@ -171,7 +169,7 @@ def get_id(self): debug(10, "*** GetID interface") length = self.serial.read()[0] id_data = self.serial.read(length + 1) - self._wait_for_ask("0x02 end") + self._wait_for_ack("0x02 end") _device_id = reduce(lambda x, y: x*0x100+y, id_data) return _device_id @@ -191,11 +189,11 @@ def read_memory(self, address, length): debug(10, "*** ReadMemory interface") self.serial.write(self._encode_address(address)) - self._wait_for_ask("0x11 address failed") + self._wait_for_ack("0x11 address failed") n = (length - 1) & 0xFF crc = n ^ 0xFF self.serial.write(bytes([n, crc])) - self._wait_for_ask("0x11 length failed") + self._wait_for_ack("0x11 length failed") return self.serial.read(length) def go(self, address): @@ -204,7 +202,7 @@ def go(self, address): debug(10, "*** Go interface") self.serial.write(self._encode_address(address)) - self._wait_for_ask("0x21 go failed") + self._wait_for_ack("0x21 go failed") def write_memory(self, address, data): assert(len(data) <= 256) @@ -213,7 +211,7 @@ def write_memory(self, address, data): debug(10, "*** Write memory interface") self.serial.write(self._encode_address(address)) - self._wait_for_ask("0x31 address failed") + self._wait_for_ack("0x31 address failed") length = (len(data)-1) & 0xFF debug(10, " %s bytes to write" % [length + 1]) self.serial.write(bytes([length])) @@ -222,7 +220,7 @@ def write_memory(self, address, data): crc = crc ^ c self.serial.write(bytes([c])) self.serial.write(bytes([crc])) - self._wait_for_ask("0x31 programming failed") + self._wait_for_ack("0x31 programming failed") debug(10, " Write memory done") def erase_memory(self, sectors=None): @@ -245,7 +243,7 @@ def erase_memory(self, sectors=None): crc = crc ^ c self.serial.write(bytes([c])) self.serial.write(bytes([crc])) - self._wait_for_ask("0x43 erasing failed") + self._wait_for_ack("0x43 erasing failed") debug(10, " Erase memory done") def extended_erase_memory(self): @@ -260,7 +258,7 @@ def extended_erase_memory(self): tmp = self.serial.timeout self.serial.timeout = 30 print("Extended erase (0x44), this can take ten seconds or more") - self._wait_for_ask("0x44 erasing failed") + self._wait_for_ack("0x44 erasing failed") self.serial.timeout = tmp debug(10, " Extended Erase memory done") @@ -275,7 +273,7 @@ def write_protect(self, sectors): crc = crc ^ c self.serial.write(bytes([c])) self.serial.write(bytes([crc])) - self._wait_for_ask("0x63 write protect failed") + self._wait_for_ack("0x63 write protect failed") debug(10, " Write protect done") def write_unprotect(self): @@ -283,8 +281,8 @@ def write_unprotect(self): raise CmdException("Write Unprotect (0x73) failed") debug(10, "*** Write Unprotect interface") - self._wait_for_ask("0x73 write unprotect failed") - self._wait_for_ask("0x73 write unprotect 2 failed") + self._wait_for_ack("0x73 write unprotect failed") + self._wait_for_ack("0x73 write unprotect 2 failed") debug(10, " Write Unprotect done") def readout_protect(self): @@ -292,8 +290,8 @@ def readout_protect(self): raise CmdException("Readout protect (0x82) failed") debug(10, "*** Readout protect interface") - self._wait_for_ask("0x82 readout protect failed") - self._wait_for_ask("0x82 readout protect 2 failed") + self._wait_for_ack("0x82 readout protect failed") + self._wait_for_ack("0x82 readout protect 2 failed") debug(10, " Read protect done") def readout_unprotect(self): @@ -301,8 +299,8 @@ def readout_unprotect(self): raise CmdException("Readout unprotect (0x92) failed") debug(10, "*** Readout Unprotect interface") - self._wait_for_ask("0x92 readout unprotect failed") - self._wait_for_ask("0x92 readout unprotect 2 failed") + self._wait_for_ack("0x92 readout unprotect failed") + self._wait_for_ack("0x92 readout unprotect 2 failed") debug(10, " Read Unprotect done") From a9e7acae86eaf656e8e078e06dbeb2c0e4d2a511 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 7 Mar 2018 10:14:45 +0100 Subject: [PATCH 011/369] Rename method 'generic' to 'command' --- stm32loader.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/stm32loader.py b/stm32loader.py index ae8d466..ed9cd8d 100755 --- a/stm32loader.py +++ b/stm32loader.py @@ -128,7 +128,7 @@ def release_chip(self): self.serial.setRTS(1) self.reset() - def generic(self, command): + def command(self, command): command_byte = bytes([command]) control_byte = bytes([command ^ 0xFF]) @@ -138,7 +138,7 @@ def generic(self, command): return self._wait_for_ack(hex(command)) def get(self): - if not self.generic(self.Command.GET): + if not self.command(self.Command.GET): raise CmdException("Get (0x00) failed") debug(10, "*** Get interface") length = self.serial.read()[0] @@ -152,7 +152,7 @@ def get(self): return version def get_version(self): - if not self.generic(self.Command.GET_VERSION): + if not self.command(self.Command.GET_VERSION): raise CmdException("GetVersion (0x01) failed") debug(10, "*** GetVersion interface") @@ -163,7 +163,7 @@ def get_version(self): return version def get_id(self): - if not self.generic(self.Command.GET_ID): + if not self.command(self.Command.GET_ID): raise CmdException("GetID (0x02) failed") debug(10, "*** GetID interface") @@ -184,7 +184,7 @@ def _encode_address(address): def read_memory(self, address, length): assert(length <= 256) - if not self.generic(self.Command.READ_MEMORY): + if not self.command(self.Command.READ_MEMORY): raise CmdException("ReadMemory (0x11) failed") debug(10, "*** ReadMemory interface") @@ -197,7 +197,7 @@ def read_memory(self, address, length): return self.serial.read(length) def go(self, address): - if not self.generic(self.Command.GO): + if not self.command(self.Command.GO): raise CmdException("Go (0x21) failed") debug(10, "*** Go interface") @@ -206,7 +206,7 @@ def go(self, address): def write_memory(self, address, data): assert(len(data) <= 256) - if not self.generic(self.Command.WRITE_MEMORY): + if not self.command(self.Command.WRITE_MEMORY): raise CmdException("Write memory (0x31) failed") debug(10, "*** Write memory interface") @@ -227,7 +227,7 @@ def erase_memory(self, sectors=None): if self.extended_erase: return interface.extended_erase_memory() - if not self.generic(self.Command.ERASE): + if not self.command(self.Command.ERASE): raise CmdException("Erase memory (0x43) failed") debug(10, "*** Erase memory interface") @@ -247,7 +247,7 @@ def erase_memory(self, sectors=None): debug(10, " Erase memory done") def extended_erase_memory(self): - if not self.generic(self.Command.EXTENDED_ERASE): + if not self.command(self.Command.EXTENDED_ERASE): raise CmdException("Extended Erase memory (0x44) failed") debug(10, "*** Extended Erase memory interface") @@ -263,7 +263,7 @@ def extended_erase_memory(self): debug(10, " Extended Erase memory done") def write_protect(self, sectors): - if not self.generic(self.Command.WRITE_PROTECT): + if not self.command(self.Command.WRITE_PROTECT): raise CmdException("Write Protect memory (0x63) failed") debug(10, "*** Write protect interface") @@ -277,7 +277,7 @@ def write_protect(self, sectors): debug(10, " Write protect done") def write_unprotect(self): - if not self.generic(self.Command.WRITE_UNPROTECT): + if not self.command(self.Command.WRITE_UNPROTECT): raise CmdException("Write Unprotect (0x73) failed") debug(10, "*** Write Unprotect interface") @@ -286,7 +286,7 @@ def write_unprotect(self): debug(10, " Write Unprotect done") def readout_protect(self): - if not self.generic(self.Command.READOUT_PROTECT): + if not self.command(self.Command.READOUT_PROTECT): raise CmdException("Readout protect (0x82) failed") debug(10, "*** Readout protect interface") @@ -295,7 +295,7 @@ def readout_protect(self): debug(10, " Read protect done") def readout_unprotect(self): - if not self.generic(self.Command.READOUT_UNPROTECT): + if not self.command(self.Command.READOUT_UNPROTECT): raise CmdException("Readout unprotect (0x92) failed") debug(10, "*** Readout Unprotect interface") From 310ae3909dd6020e9de3d304a2b0f772f2a00f03 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 7 Mar 2018 10:28:24 +0100 Subject: [PATCH 012/369] Split out global erase and paged erase into helper methods --- stm32loader.py | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/stm32loader.py b/stm32loader.py index ed9cd8d..1eb88f7 100755 --- a/stm32loader.py +++ b/stm32loader.py @@ -231,21 +231,29 @@ def erase_memory(self, sectors=None): raise CmdException("Erase memory (0x43) failed") debug(10, "*** Erase memory interface") - if sectors is None: - # Global erase and checksum byte - self.serial.write(b'\xff') - self.serial.write(b'\x00') + + if sectors: + self._page_erase(sectors) else: - # Sectors erase - self.serial.write(bytes([(len(sectors) - 1) & 0xFF])) - crc = 0xFF - for c in sectors: - crc = crc ^ c - self.serial.write(bytes([c])) - self.serial.write(bytes([crc])) - self._wait_for_ack("0x43 erasing failed") + self._global_erase() + self._wait_for_ack("0x43 erase failed") debug(10, " Erase memory done") + def _global_erase(self): + # global erase: n=255, see ST AN3155 + self.serial.write(b'\xff') + self.serial.write(b'\x00') + + def _page_erase(self, pages): + # page erase, see ST AN3155 + nr_of_pages = (len(pages) - 1) & 0xFF + self.serial.write(bytes([nr_of_pages])) + crc = 0xFF + for page_number in pages: + self.serial.write(bytes([page_number])) + crc = crc ^ page_number + self.serial.write(bytes([crc])) + def extended_erase_memory(self): if not self.command(self.Command.EXTENDED_ERASE): raise CmdException("Extended Erase memory (0x44) failed") From 152b5775f557af5643e84d5e9f728bc814d80e5d Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 7 Mar 2018 10:36:45 +0100 Subject: [PATCH 013/369] Move private methods to bottom of the class --- stm32loader.py | 79 ++++++++++++++++++++++++-------------------------- 1 file changed, 38 insertions(+), 41 deletions(-) diff --git a/stm32loader.py b/stm32loader.py index 1eb88f7..b62a464 100755 --- a/stm32loader.py +++ b/stm32loader.py @@ -95,20 +95,6 @@ def open(self, a_port='/dev/tty.usbserial-ftCYPMYJ', a_baud_rate=115200): timeout=5 # set a timeout value, None for waiting forever ) - def _wait_for_ack(self, info=""): - try: - ack = self.serial.read()[0] - except TypeError: - raise CmdException("Can't read port or timeout") - - if ack == self.Reply.NACK: - raise CmdException("NACK " + info) - - if ack != self.Replay.ACK: - raise CmdException("Unknown response. " + info + ": " + hex(ack)) - - return 1 - def reset(self): self.serial.setDTR(0) time.sleep(0.1) @@ -173,15 +159,6 @@ def get_id(self): _device_id = reduce(lambda x, y: x*0x100+y, id_data) return _device_id - @staticmethod - def _encode_address(address): - byte3 = (address >> 0) & 0xFF - byte2 = (address >> 8) & 0xFF - byte1 = (address >> 16) & 0xFF - byte0 = (address >> 24) & 0xFF - crc = byte0 ^ byte1 ^ byte2 ^ byte3 - return bytes([byte0, byte1, byte2, byte3, crc]) - def read_memory(self, address, length): assert(length <= 256) if not self.command(self.Command.READ_MEMORY): @@ -239,21 +216,6 @@ def erase_memory(self, sectors=None): self._wait_for_ack("0x43 erase failed") debug(10, " Erase memory done") - def _global_erase(self): - # global erase: n=255, see ST AN3155 - self.serial.write(b'\xff') - self.serial.write(b'\x00') - - def _page_erase(self, pages): - # page erase, see ST AN3155 - nr_of_pages = (len(pages) - 1) & 0xFF - self.serial.write(bytes([nr_of_pages])) - crc = 0xFF - for page_number in pages: - self.serial.write(bytes([page_number])) - crc = crc ^ page_number - self.serial.write(bytes([crc])) - def extended_erase_memory(self): if not self.command(self.Command.EXTENDED_ERASE): raise CmdException("Extended Erase memory (0x44) failed") @@ -311,9 +273,6 @@ def readout_unprotect(self): self._wait_for_ack("0x92 readout unprotect 2 failed") debug(10, " Read Unprotect done") - -# Complex commands section - def read_memory_data(self, address, length): data = bytearray() while length > 256: @@ -339,6 +298,44 @@ def write_memory_data(self, address, data): debug(5, "Write %(len)d bytes at 0x%(address)X" % {'address': address, 'len': 256}) self.write_memory(address, data[offs:offs + length] + (b'\xff' * (256 - length))) + def _global_erase(self): + # global erase: n=255, see ST AN3155 + self.serial.write(b'\xff') + self.serial.write(b'\x00') + + def _page_erase(self, pages): + # page erase, see ST AN3155 + nr_of_pages = (len(pages) - 1) & 0xFF + self.serial.write(bytes([nr_of_pages])) + crc = 0xFF + for page_number in pages: + self.serial.write(bytes([page_number])) + checksum = checksum ^ page_number + self.serial.write(bytes([checksum])) + + def _wait_for_ack(self, info=""): + try: + ack = self.serial.read()[0] + except TypeError: + raise CmdException("Can't read port or timeout") + + if ack == self.Reply.NACK: + raise CmdException("NACK " + info) + + if ack != self.Reply.ACK: + raise CmdException("Unknown response. " + info + ": " + hex(ack)) + + return 1 + + @staticmethod + def _encode_address(address): + byte3 = (address >> 0) & 0xFF + byte2 = (address >> 8) & 0xFF + byte1 = (address >> 16) & 0xFF + byte0 = (address >> 24) & 0xFF + crc = byte0 ^ byte1 ^ byte2 ^ byte3 + return bytes([byte0, byte1, byte2, byte3, crc]) + def usage(): print("""Usage: %s [-hqVewvr] [-l length] [-p port] [-b baud] [-a address] [-g address] [file.bin] From 7bf6f61131b928774ddde1d0521e4adbd88adffc Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 7 Mar 2018 10:39:20 +0100 Subject: [PATCH 014/369] Rename crc to checksum As this is not really a CRC (polynomial division). --- stm32loader.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/stm32loader.py b/stm32loader.py index b62a464..4d800f0 100755 --- a/stm32loader.py +++ b/stm32loader.py @@ -168,8 +168,8 @@ def read_memory(self, address, length): self.serial.write(self._encode_address(address)) self._wait_for_ack("0x11 address failed") n = (length - 1) & 0xFF - crc = n ^ 0xFF - self.serial.write(bytes([n, crc])) + checksum = n ^ 0xFF + self.serial.write(bytes([n, checksum])) self._wait_for_ack("0x11 length failed") return self.serial.read(length) @@ -192,11 +192,11 @@ def write_memory(self, address, data): length = (len(data)-1) & 0xFF debug(10, " %s bytes to write" % [length + 1]) self.serial.write(bytes([length])) - crc = 0xFF + checksum = 0xFF for c in data: - crc = crc ^ c + checksum = checksum ^ c self.serial.write(bytes([c])) - self.serial.write(bytes([crc])) + self.serial.write(bytes([checksum])) self._wait_for_ack("0x31 programming failed") debug(10, " Write memory done") @@ -238,11 +238,11 @@ def write_protect(self, sectors): debug(10, "*** Write protect interface") self.serial.write(bytes([((len(sectors) - 1) & 0xFF)])) - crc = 0xFF + checksum = 0xFF for c in sectors: - crc = crc ^ c + checksum = checksum ^ c self.serial.write(bytes([c])) - self.serial.write(bytes([crc])) + self.serial.write(bytes([checksum])) self._wait_for_ack("0x63 write protect failed") debug(10, " Write protect done") @@ -307,7 +307,7 @@ def _page_erase(self, pages): # page erase, see ST AN3155 nr_of_pages = (len(pages) - 1) & 0xFF self.serial.write(bytes([nr_of_pages])) - crc = 0xFF + checksum = 0xFF for page_number in pages: self.serial.write(bytes([page_number])) checksum = checksum ^ page_number @@ -333,8 +333,8 @@ def _encode_address(address): byte2 = (address >> 8) & 0xFF byte1 = (address >> 16) & 0xFF byte0 = (address >> 24) & 0xFF - crc = byte0 ^ byte1 ^ byte2 ^ byte3 - return bytes([byte0, byte1, byte2, byte3, crc]) + checksum = byte0 ^ byte1 ^ byte2 ^ byte3 + return bytes([byte0, byte1, byte2, byte3, checksum]) def usage(): From 267245c0d5205521f15f42d1732d07e738213bea Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 7 Mar 2018 10:45:00 +0100 Subject: [PATCH 015/369] Clean up help text printing --- stm32loader.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/stm32loader.py b/stm32loader.py index 4d800f0..6bd595d 100755 --- a/stm32loader.py +++ b/stm32loader.py @@ -338,7 +338,7 @@ def _encode_address(address): def usage(): - print("""Usage: %s [-hqVewvr] [-l length] [-p port] [-b baud] [-a address] [-g address] [file.bin] + help_text = """Usage: %s [-hqVewvr] [-l length] [-p port] [-b baud] [-a address] [-g address] [file.bin] -h This help -q Quiet -V Verbose @@ -353,8 +353,9 @@ def usage(): -g address Address to start running at (0x08000000, usually) ./stm32loader.py -e -w -v example/main.bin - - """ % sys.argv[0]) + """ + help_text = help_text % sys.argv[0] + print(help_text) if __name__ == "__main__": From 1ab69f1f8c93dd49698449c023b7a44b103a103a Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 7 Mar 2018 10:49:50 +0100 Subject: [PATCH 016/369] Remove unused comments --- stm32loader.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/stm32loader.py b/stm32loader.py index 6bd595d..2aeac72 100755 --- a/stm32loader.py +++ b/stm32loader.py @@ -380,9 +380,8 @@ def usage(): 'go_address': -1, } -# http://www.python.org/doc/2.5.2/lib/module-getopt.html - try: + # parse command-line arguments using getopt opts, args = getopt.getopt(sys.argv[1:], "hqVewvrp:b:a:l:g:") except getopt.GetoptError as err: # print help information and exit: @@ -435,11 +434,6 @@ def usage(): debug(0, "Bootloader version %X" % boot_version) device_id = interface.get_id() debug(0, "Chip id: 0x%x (%s)" % (device_id, CHIP_IDS.get(device_id, "Unknown"))) -# interface.get_version() -# interface.get_id() -# interface.readout_unprotect() -# interface.write_unprotect() -# interface.write_protect([0, 1]) binary_data = None data_file = args[0] if args else None From dba0436059a8cab445bbf2fb2ef68310b6e5b492 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 7 Mar 2018 10:53:10 +0100 Subject: [PATCH 017/369] Replace link to project page (gone) with link to GitHub repo --- stm32loader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stm32loader.py b/stm32loader.py index 2aeac72..d402c2b 100755 --- a/stm32loader.py +++ b/stm32loader.py @@ -4,7 +4,7 @@ # vim: sw=4:ts=4:si:et:enc=utf-8 # Author: Ivan A-R -# Project page: http://tuxotronic.org/wiki/projects/stm32loader +# GitHub repository: https://github.com/jsnyder/stm32loader # # This file is part of stm32loader. # From 1136838196f4b9912959eb9302db85ee332174c4 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 7 Mar 2018 11:25:52 +0100 Subject: [PATCH 018/369] Fix incorrect checksum calculation for paged erase Based on https://github.com/jsnyder/stm32loader/pull/4 . --- stm32loader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stm32loader.py b/stm32loader.py index d402c2b..82f5a07 100755 --- a/stm32loader.py +++ b/stm32loader.py @@ -307,7 +307,7 @@ def _page_erase(self, pages): # page erase, see ST AN3155 nr_of_pages = (len(pages) - 1) & 0xFF self.serial.write(bytes([nr_of_pages])) - checksum = 0xFF + checksum = nr_of_pages for page_number in pages: self.serial.write(bytes([page_number])) checksum = checksum ^ page_number From 903b2da950c268e676da3704273177e848201407 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 7 Mar 2018 14:30:31 +0100 Subject: [PATCH 019/369] Remove Psyco, which is 'unmaintained and dead' There's other low-haning peformance-gain-fruit to be found here. --- stm32loader.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/stm32loader.py b/stm32loader.py index 82f5a07..4fd84af 100755 --- a/stm32loader.py +++ b/stm32loader.py @@ -360,15 +360,6 @@ def usage(): if __name__ == "__main__": - # Import Psyco if available - try: - import psyco - psyco.full() - print("Using Psyco...") - except ImportError: - psyco = None - pass - configuration = { 'port': '/dev/tty.usbserial-ftCYPMYJ', 'baud': 115200, From 75c25e84b799b73b97bed7c642a6387d34021be2 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 7 Mar 2018 14:49:21 +0100 Subject: [PATCH 020/369] Document the -g [go] option in the README --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 687bcc8..c3a4d4f 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,8 @@ Usage -l length Length of read -p port Serial port (default: /dev/tty.usbserial-ftCYPMYJ) -b baud Baud speed (default: 115200) - -a addr Target address + -a address Target address + -g address Address to start running at (0x08000000, usually) ``` From 3c5950a97482a94e90351b2beba3f85cf5e84a63 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 7 Mar 2018 14:53:34 +0100 Subject: [PATCH 021/369] Mention that Python 3 is required Not planning to add backwards compatibility with v2.x. --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index c3a4d4f..13a48ed 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ STM32Loader Python script which will talk to the STM32 bootloader to upload and download firmware. +Requires Python 3. + Original Version by: Ivan A-R . From daf4ec7c6c3474d3eeb64946497c21a10993e9d0 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 7 Mar 2018 21:42:40 +0100 Subject: [PATCH 022/369] Add Python2 backwards-compatibility bytearray() everywhere! --- stm32loader.py | 80 +++++++++++++++++++++++++++----------------------- 1 file changed, 43 insertions(+), 37 deletions(-) diff --git a/stm32loader.py b/stm32loader.py index 4fd84af..a669226 100755 --- a/stm32loader.py +++ b/stm32loader.py @@ -22,6 +22,9 @@ # along with stm32loader; see the file COPYING3. If not see # . + +from __future__ import print_function + from functools import reduce import sys import getopt @@ -115,8 +118,8 @@ def release_chip(self): self.reset() def command(self, command): - command_byte = bytes([command]) - control_byte = bytes([command ^ 0xFF]) + command_byte = bytearray([command]) + control_byte = bytearray([command ^ 0xFF]) self.serial.write(command_byte) self.serial.write(control_byte) @@ -127,13 +130,13 @@ def get(self): if not self.command(self.Command.GET): raise CmdException("Get (0x00) failed") debug(10, "*** Get interface") - length = self.serial.read()[0] - version = self.serial.read()[0] + length = bytearray(self.serial.read())[0] + version = bytearray(self.serial.read())[0] debug(10, " Bootloader version: " + hex(version)) - data = self.serial.read(length) + data = bytearray(self.serial.read(length)) if self.Reply.EXTENDED_ERASE in data: self.extended_erase = 1 - debug(10, " Available commands: " + ", ".join(data)) + debug(10, " Available commands: " + ", ".join(hex(b) for b in data)) self._wait_for_ack("0x00 end") return version @@ -142,7 +145,7 @@ def get_version(self): raise CmdException("GetVersion (0x01) failed") debug(10, "*** GetVersion interface") - version = self.serial.read()[0] + version = bytearray(self.serial.read())[0] self.serial.read(2) self._wait_for_ack("0x01 end") debug(10, " Bootloader version: " + hex(version)) @@ -153,10 +156,10 @@ def get_id(self): raise CmdException("GetID (0x02) failed") debug(10, "*** GetID interface") - length = self.serial.read()[0] - id_data = self.serial.read(length + 1) + length = bytearray(self.serial.read())[0] + id_data = bytearray(self.serial.read(length + 1)) self._wait_for_ack("0x02 end") - _device_id = reduce(lambda x, y: x*0x100+y, id_data) + _device_id = reduce(lambda x, y: x * 0x100 + y, id_data) return _device_id def read_memory(self, address, length): @@ -167,11 +170,11 @@ def read_memory(self, address, length): debug(10, "*** ReadMemory interface") self.serial.write(self._encode_address(address)) self._wait_for_ack("0x11 address failed") - n = (length - 1) & 0xFF - checksum = n ^ 0xFF - self.serial.write(bytes([n, checksum])) + nr_of_bytes = (length - 1) & 0xFF + checksum = nr_of_bytes ^ 0xFF + self.serial.write(bytearray([nr_of_bytes, checksum])) self._wait_for_ack("0x11 length failed") - return self.serial.read(length) + return bytearray(self.serial.read(length)) def go(self, address): if not self.command(self.Command.GO): @@ -189,14 +192,14 @@ def write_memory(self, address, data): debug(10, "*** Write memory interface") self.serial.write(self._encode_address(address)) self._wait_for_ack("0x31 address failed") - length = (len(data)-1) & 0xFF - debug(10, " %s bytes to write" % [length + 1]) - self.serial.write(bytes([length])) + nr_of_bytes = (len(data) - 1) & 0xFF + debug(10, " %s bytes to write" % [nr_of_bytes + 1]) + self.serial.write(bytearray([nr_of_bytes])) checksum = 0xFF for c in data: checksum = checksum ^ c - self.serial.write(bytes([c])) - self.serial.write(bytes([checksum])) + self.serial.write(bytearray([c])) + self.serial.write(bytearray([checksum])) self._wait_for_ack("0x31 programming failed") debug(10, " Write memory done") @@ -232,17 +235,18 @@ def extended_erase_memory(self): self.serial.timeout = tmp debug(10, " Extended Erase memory done") - def write_protect(self, sectors): + def write_protect(self, pages): if not self.command(self.Command.WRITE_PROTECT): raise CmdException("Write Protect memory (0x63) failed") debug(10, "*** Write protect interface") - self.serial.write(bytes([((len(sectors) - 1) & 0xFF)])) + nr_of_pages = (len(pages) - 1) & 0xFF + self.serial.write(bytearray([nr_of_pages])) checksum = 0xFF - for c in sectors: + for c in pages: checksum = checksum ^ c - self.serial.write(bytes([c])) - self.serial.write(bytes([checksum])) + self.serial.write(bytearray([c])) + self.serial.write(bytearray([checksum])) self._wait_for_ack("0x63 write protect failed") debug(10, " Write protect done") @@ -287,16 +291,16 @@ def read_memory_data(self, address, length): def write_memory_data(self, address, data): length = len(data) - offs = 0 + offset = 0 while length > 256: debug(5, "Write %(len)d bytes at 0x%(address)X" % {'address': address, 'len': 256}) - self.write_memory(address, data[offs:offs + 256]) - offs = offs + 256 - address = address + 256 - length = length - 256 + self.write_memory(address, data[offset:offset + 256]) + offset += 256 + address += 256 + length -= 256 else: debug(5, "Write %(len)d bytes at 0x%(address)X" % {'address': address, 'len': 256}) - self.write_memory(address, data[offs:offs + length] + (b'\xff' * (256 - length))) + self.write_memory(address, data[offset:offset + length] + (b'\xff' * (256 - length))) def _global_erase(self): # global erase: n=255, see ST AN3155 @@ -306,16 +310,16 @@ def _global_erase(self): def _page_erase(self, pages): # page erase, see ST AN3155 nr_of_pages = (len(pages) - 1) & 0xFF - self.serial.write(bytes([nr_of_pages])) + self.serial.write(bytearray([nr_of_pages])) checksum = nr_of_pages for page_number in pages: - self.serial.write(bytes([page_number])) + self.serial.write(bytearray([page_number])) checksum = checksum ^ page_number - self.serial.write(bytes([checksum])) + self.serial.write(bytearray([checksum])) def _wait_for_ack(self, info=""): try: - ack = self.serial.read()[0] + ack = bytearray(self.serial.read())[0] except TypeError: raise CmdException("Can't read port or timeout") @@ -334,7 +338,7 @@ def _encode_address(address): byte1 = (address >> 16) & 0xFF byte0 = (address >> 24) & 0xFF checksum = byte0 ^ byte1 ^ byte2 ^ byte3 - return bytes([byte0, byte1, byte2, byte3, checksum]) + return bytearray([byte0, byte1, byte2, byte3, checksum]) def usage(): @@ -430,7 +434,8 @@ def usage(): data_file = args[0] if args else None if configuration['write'] or configuration['verify']: - binary_data = open(data_file, 'rb').read() + with open(data_file, 'rb') as read_file: + binary_data = bytearray(read_file.read()) if configuration['erase']: interface.erase_memory() @@ -451,7 +456,8 @@ def usage(): if not configuration['write'] and configuration['read']: read_data = interface.read_memory_data(configuration['address'], configuration['length']) - open(data_file, 'wb').write(read_data) + with open(data_file, 'wb') as out_file: + out_file.write(read_data) if configuration['go_address'] != -1: interface.go(configuration['go_address']) From ddbe8f9993e75d533e0b40afa7282dc689f53d45 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 7 Mar 2018 21:59:26 +0100 Subject: [PATCH 023/369] Describe compatible Python versions --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 13a48ed..9d9f5ca 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,10 @@ STM32Loader Python script which will talk to the STM32 bootloader to upload and download firmware. -Requires Python 3. - Original Version by: Ivan A-R . +Compatible with Python version 2.6 to 2.7, 3.2 to 3.6. + Usage ----- From 9ea437b15346d39298fddf3e269bd37d799ddefe Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 7 Mar 2018 22:12:26 +0100 Subject: [PATCH 024/369] Split out reset and boot0 setting into separate methods --- stm32loader.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/stm32loader.py b/stm32loader.py index a669226..dc1d398 100755 --- a/stm32loader.py +++ b/stm32loader.py @@ -99,14 +99,13 @@ def open(self, a_port='/dev/tty.usbserial-ftCYPMYJ', a_baud_rate=115200): ) def reset(self): - self.serial.setDTR(0) + self._enable_reset(True) time.sleep(0.1) - self.serial.setDTR(1) + self._enable_reset(False) time.sleep(0.5) def init_chip(self): - # Set boot - self.serial.setRTS(0) + self._enable_boot0(True) self.reset() # Syncro @@ -114,7 +113,7 @@ def init_chip(self): return self._wait_for_ack("Syncro") def release_chip(self): - self.serial.setRTS(1) + self._enable_boot0(False) self.reset() def command(self, command): @@ -317,6 +316,16 @@ def _page_erase(self, pages): checksum = checksum ^ page_number self.serial.write(bytearray([checksum])) + def _enable_reset(self, enable=True): + # active low + level = 0 if enable else 1 + self.serial.setDTR(level) + + def _enable_boot0(self, enable=True): + # active low + level = 0 if enable else 1 + self.serial.setRTS(level) + def _wait_for_ack(self, info=""): try: ack = bytearray(self.serial.read())[0] From bf80b3a97fd96089974d07958ee6c7a869d005a7 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 7 Mar 2018 22:17:23 +0100 Subject: [PATCH 025/369] Give the 0x7F 'command' a name --- stm32loader.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/stm32loader.py b/stm32loader.py index dc1d398..1a9675b 100755 --- a/stm32loader.py +++ b/stm32loader.py @@ -74,11 +74,15 @@ class Command: WRITE_UNPROTECT = 0x73 READOUT_PROTECT = 0x82 READOUT_UNPROTECT = 0x92 + # not really listed under commands, but still... + # 'wake the bootloader' == 'activate USART' == 'synchronize' + SYNCHRONIZE = 0x7F class Reply: # See ST AN3155 ACK = 0x79 NACK = 0x1F + # not really an ack/nack reply, but still... EXTENDED_ERASE = 0x44 extended_erase = 0 @@ -107,9 +111,7 @@ def reset(self): def init_chip(self): self._enable_boot0(True) self.reset() - - # Syncro - self.serial.write(b'\x7F') + self.serial.write(bytearray([self.Command.SYNCHRONIZE])) return self._wait_for_ack("Syncro") def release_chip(self): From 561a2eb896adeec57871b72a2c72ee09fa754d95 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 7 Mar 2018 22:25:18 +0100 Subject: [PATCH 026/369] Make boot0 and reset active-high configurable in code --- stm32loader.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/stm32loader.py b/stm32loader.py index 1a9675b..fbb4c3b 100755 --- a/stm32loader.py +++ b/stm32loader.py @@ -89,6 +89,8 @@ class Reply: def __init__(self): self.serial = None + self._reset_active_high = False + self._boot0_active_high = False def open(self, a_port='/dev/tty.usbserial-ftCYPMYJ', a_baud_rate=115200): self.serial = serial.Serial( @@ -319,13 +321,17 @@ def _page_erase(self, pages): self.serial.write(bytearray([checksum])) def _enable_reset(self, enable=True): - # active low + # active low unless otherwise specified level = 0 if enable else 1 + if self._reset_active_high: + level = 1 - level self.serial.setDTR(level) def _enable_boot0(self, enable=True): - # active low + # active low unless otherwise specified level = 0 if enable else 1 + if self._boot0_active_high: + level = 1 - level self.serial.setRTS(level) def _wait_for_ack(self, info=""): From 267549876d9275170505a0a7cf21292e78a127d5 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 7 Mar 2018 22:30:05 +0100 Subject: [PATCH 027/369] Make RTS/DTR swap configurable in code --- stm32loader.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/stm32loader.py b/stm32loader.py index fbb4c3b..a28d3ac 100755 --- a/stm32loader.py +++ b/stm32loader.py @@ -89,6 +89,7 @@ class Reply: def __init__(self): self.serial = None + self._swap_RTS_DTR = False self._reset_active_high = False self._boot0_active_high = False @@ -325,14 +326,22 @@ def _enable_reset(self, enable=True): level = 0 if enable else 1 if self._reset_active_high: level = 1 - level - self.serial.setDTR(level) + + if self._swap_RTS_DTR: + self.serial.setRTS(level) + else: + self.serial.setDTR(level) def _enable_boot0(self, enable=True): # active low unless otherwise specified level = 0 if enable else 1 if self._boot0_active_high: level = 1 - level - self.serial.setRTS(level) + + if self._swap_RTS_DTR: + self.serial.setDTR(level) + else: + self.serial.setRTS(level) def _wait_for_ack(self, info=""): try: From 0a0f713e35600e38a53340b02a8549c91c408114 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Thu, 8 Mar 2018 00:15:46 +0100 Subject: [PATCH 028/369] Add command-line options for DTR/RTS swap and polarity --- README.md | 3 +++ stm32loader.py | 30 +++++++++++++++++++++++------- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 9d9f5ca..3847fe6 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,9 @@ Usage -w Write -v Verify (recommended) -r Read + -s Swap RTS and DTR: use RTS for reset and DTR for boot0. + -R Make reset active high. + -B Make boot0 active high. -l length Length of read -p port Serial port (default: /dev/tty.usbserial-ftCYPMYJ) -b baud Baud speed (default: 115200) diff --git a/stm32loader.py b/stm32loader.py index a28d3ac..a0ac3a4 100755 --- a/stm32loader.py +++ b/stm32loader.py @@ -87,11 +87,11 @@ class Reply: extended_erase = 0 - def __init__(self): + def __init__(self, swap_rts_dtr=False, reset_active_high=False, boot0_active_high=False): self.serial = None - self._swap_RTS_DTR = False - self._reset_active_high = False - self._boot0_active_high = False + self._swap_RTS_DTR = swap_rts_dtr + self._reset_active_high = reset_active_high + self._boot0_active_high = boot0_active_high def open(self, a_port='/dev/tty.usbserial-ftCYPMYJ', a_baud_rate=115200): self.serial = serial.Serial( @@ -368,7 +368,7 @@ def _encode_address(address): def usage(): - help_text = """Usage: %s [-hqVewvr] [-l length] [-p port] [-b baud] [-a address] [-g address] [file.bin] + help_text = """Usage: %s [-hqVewvrsRB] [-l length] [-p port] [-b baud] [-a address] [-g address] [file.bin] -h This help -q Quiet -V Verbose @@ -376,6 +376,9 @@ def usage(): -w Write -v Verify (recommended) -r Read + -s Swap RTS and DTR: use RTS for reset and DTR for boot0. + -R Make reset active high. + -B Make boot0 active high. -l length Length of read -p port Serial port (default: /dev/tty.usbserial-ftCYPMYJ) -b baud Baud speed (default: 115200) @@ -399,11 +402,14 @@ def usage(): 'verify': 0, 'read': 0, 'go_address': -1, + 'swap_rts_dtr': False, + 'reset_active_high': False, + 'boot0_active_high': False, } try: # parse command-line arguments using getopt - opts, args = getopt.getopt(sys.argv[1:], "hqVewvrp:b:a:l:g:") + opts, args = getopt.getopt(sys.argv[1:], "hqVewvrsRBp:b:a:l:g:") except getopt.GetoptError as err: # print help information and exit: # this print something like "option -a not recognized" @@ -431,6 +437,12 @@ def usage(): configuration['read'] = 1 elif o == '-p': configuration['port'] = a + elif o == '-s': + configuration['swap_rts_dtr'] = True + elif o == '-R': + configuration['reset_active_high'] = True + elif o == '-B': + configuration['boot0_active_high'] = True elif o == '-b': configuration['baud'] = eval(a) elif o == '-a': @@ -442,7 +454,11 @@ def usage(): else: assert False, "unhandled option" - interface = CommandInterface() + interface = CommandInterface( + swap_rts_dtr=configuration['swap_rts_dtr'], + reset_active_high=configuration['reset_active_high'], + boot0_active_high=configuration['boot0_active_high'], + ) interface.open(configuration['port'], configuration['baud']) debug(10, "Open port %(port)s, baud %(baud)d" % {'port': configuration['port'], 'baud': configuration['baud']}) try: From 4fb842ff9564fb920b239f64b8d1096543c0fc73 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Thu, 15 Mar 2018 00:44:48 +0100 Subject: [PATCH 029/369] Make reset a private method --- stm32loader.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/stm32loader.py b/stm32loader.py index a0ac3a4..9265fa0 100755 --- a/stm32loader.py +++ b/stm32loader.py @@ -105,21 +105,15 @@ def open(self, a_port='/dev/tty.usbserial-ftCYPMYJ', a_baud_rate=115200): timeout=5 # set a timeout value, None for waiting forever ) - def reset(self): - self._enable_reset(True) - time.sleep(0.1) - self._enable_reset(False) - time.sleep(0.5) - def init_chip(self): self._enable_boot0(True) - self.reset() + self._reset() self.serial.write(bytearray([self.Command.SYNCHRONIZE])) return self._wait_for_ack("Syncro") def release_chip(self): self._enable_boot0(False) - self.reset() + self._reset() def command(self, command): command_byte = bytearray([command]) @@ -321,6 +315,12 @@ def _page_erase(self, pages): checksum = checksum ^ page_number self.serial.write(bytearray([checksum])) + def _reset(self): + self._enable_reset(True) + time.sleep(0.1) + self._enable_reset(False) + time.sleep(0.5) + def _enable_reset(self, enable=True): # active low unless otherwise specified level = 0 if enable else 1 From ee68e702b5b801670043ca376eb9c9ec83dfe3ec Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Thu, 15 Mar 2018 00:46:01 +0100 Subject: [PATCH 030/369] Rename init/release methods They're both resets, but with a different boot configuration. --- stm32loader.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/stm32loader.py b/stm32loader.py index 9265fa0..55ba5f5 100755 --- a/stm32loader.py +++ b/stm32loader.py @@ -105,13 +105,13 @@ def open(self, a_port='/dev/tty.usbserial-ftCYPMYJ', a_baud_rate=115200): timeout=5 # set a timeout value, None for waiting forever ) - def init_chip(self): + def reset_from_system_memory(self): self._enable_boot0(True) self._reset() self.serial.write(bytearray([self.Command.SYNCHRONIZE])) return self._wait_for_ack("Syncro") - def release_chip(self): + def reset_from_flash(self): self._enable_boot0(False) self._reset() @@ -463,7 +463,7 @@ def usage(): debug(10, "Open port %(port)s, baud %(baud)d" % {'port': configuration['port'], 'baud': configuration['baud']}) try: try: - interface.init_chip() + interface.reset_from_system_memory() except Exception: print("Can't init. Ensure that BOOT0 is enabled and reset device") @@ -505,4 +505,4 @@ def usage(): interface.go(configuration['go_address']) finally: - interface.release_chip() + interface.reset_from_flash() From 126e27c25310e47bf7f6e54f7b591b71b03cd22b Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Thu, 15 Mar 2018 00:55:27 +0100 Subject: [PATCH 031/369] Describe where the inspiration came from --- README.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3847fe6..5c77ddb 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,6 @@ STM32Loader Python script which will talk to the STM32 bootloader to upload and download firmware. -Original Version by: Ivan A-R . - Compatible with Python version 2.6 to 2.7, 3.2 to 3.6. @@ -39,3 +37,13 @@ stm32loader.py -e -w -v somefile.bin ``` This will pre-erase flash, write `somefile.bin` to the flash on the device, and then perform a verification after writing is finished. + + +Acknowledgement +--------------- + +Original Version by: Ivan A-R . + +Inspiration for the configurable RTS/DTR and polarity feature: +hhttps://github.com/pazzarpj/stm32loader . + From 03d3d92872153780af39163084d141222d9d2f5e Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Thu, 8 Mar 2018 01:24:58 +0100 Subject: [PATCH 032/369] Print offending option in case of parsing error --- stm32loader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stm32loader.py b/stm32loader.py index 55ba5f5..c29add6 100755 --- a/stm32loader.py +++ b/stm32loader.py @@ -452,7 +452,7 @@ def usage(): elif o == '-l': configuration['length'] = eval(a) else: - assert False, "unhandled option" + assert False, "unhandled option %s" % o interface = CommandInterface( swap_rts_dtr=configuration['swap_rts_dtr'], From 8b4e1e23c75d485e23ba2391985a6bf388b175f3 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Thu, 15 Mar 2018 00:21:11 +0100 Subject: [PATCH 033/369] Remove double definition of extended erase value 0x44 No need to have a separate definition for it. --- stm32loader.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/stm32loader.py b/stm32loader.py index c29add6..67343f5 100755 --- a/stm32loader.py +++ b/stm32loader.py @@ -82,8 +82,6 @@ class Reply: # See ST AN3155 ACK = 0x79 NACK = 0x1F - # not really an ack/nack reply, but still... - EXTENDED_ERASE = 0x44 extended_erase = 0 @@ -132,7 +130,7 @@ def get(self): version = bytearray(self.serial.read())[0] debug(10, " Bootloader version: " + hex(version)) data = bytearray(self.serial.read(length)) - if self.Reply.EXTENDED_ERASE in data: + if self.Command.EXTENDED_ERASE in data: self.extended_erase = 1 debug(10, " Available commands: " + ", ".join(hex(b) for b in data)) self._wait_for_ack("0x00 end") From 539642a383f87c146a1356e6b03a81029d6eb067 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Thu, 15 Mar 2018 01:02:20 +0100 Subject: [PATCH 034/369] Replace integer 'booleans' with proper bools --- stm32loader.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/stm32loader.py b/stm32loader.py index 67343f5..94384fe 100755 --- a/stm32loader.py +++ b/stm32loader.py @@ -83,7 +83,7 @@ class Reply: ACK = 0x79 NACK = 0x1F - extended_erase = 0 + extended_erase = False def __init__(self, swap_rts_dtr=False, reset_active_high=False, boot0_active_high=False): self.serial = None @@ -131,7 +131,7 @@ def get(self): debug(10, " Bootloader version: " + hex(version)) data = bytearray(self.serial.read(length)) if self.Command.EXTENDED_ERASE in data: - self.extended_erase = 1 + self.extended_erase = True debug(10, " Available commands: " + ", ".join(hex(b) for b in data)) self._wait_for_ack("0x00 end") return version @@ -395,10 +395,10 @@ def usage(): 'port': '/dev/tty.usbserial-ftCYPMYJ', 'baud': 115200, 'address': 0x08000000, - 'erase': 0, - 'write': 0, - 'verify': 0, - 'read': 0, + 'erase': False, + 'write': False, + 'verify': False, + 'read': False, 'go_address': -1, 'swap_rts_dtr': False, 'reset_active_high': False, @@ -426,13 +426,13 @@ def usage(): usage() sys.exit(0) elif o == '-e': - configuration['erase'] = 1 + configuration['erase'] = True elif o == '-w': - configuration['write'] = 1 + configuration['write'] = True elif o == '-v': - configuration['verify'] = 1 + configuration['verify'] = True elif o == '-r': - configuration['read'] = 1 + configuration['read'] = True elif o == '-p': configuration['port'] = a elif o == '-s': From bc98563ff49e47999ba363085b0ddba1b17f2e71 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Thu, 15 Mar 2018 01:06:14 +0100 Subject: [PATCH 035/369] Rename some variables --- stm32loader.py | 65 +++++++++++++++++++++++++------------------------- 1 file changed, 32 insertions(+), 33 deletions(-) diff --git a/stm32loader.py b/stm32loader.py index 94384fe..99e35b0 100755 --- a/stm32loader.py +++ b/stm32loader.py @@ -32,8 +32,7 @@ import time -# Verbose level -QUIET = 20 +VERBOSITY = 20 CHIP_IDS = { # see ST AN2606 @@ -50,7 +49,7 @@ def debug(level, message): - if QUIET >= level: + if VERBOSITY >= level: print(message, file=sys.stderr) @@ -91,10 +90,10 @@ def __init__(self, swap_rts_dtr=False, reset_active_high=False, boot0_active_hig self._reset_active_high = reset_active_high self._boot0_active_high = boot0_active_high - def open(self, a_port='/dev/tty.usbserial-ftCYPMYJ', a_baud_rate=115200): + def open(self, serial_port='/dev/tty.usbserial-ftCYPMYJ', baud_rate=115200): self.serial = serial.Serial( - port=a_port, - baudrate=a_baud_rate, + port=serial_port, + baudrate=baud_rate, bytesize=8, # number of write_data bits parity=serial.PARITY_EVEN, stopbits=1, @@ -224,11 +223,11 @@ def extended_erase_memory(self): self.serial.write(b'\xFF') self.serial.write(b'\xFF') self.serial.write(b'\x00') - tmp = self.serial.timeout + previous_timeout_value = self.serial.timeout self.serial.timeout = 30 print("Extended erase (0x44), this can take ten seconds or more") self._wait_for_ack("0x44 erasing failed") - self.serial.timeout = tmp + self.serial.timeout = previous_timeout_value debug(10, " Extended Erase memory done") def write_protect(self, pages): @@ -415,42 +414,42 @@ def usage(): usage() sys.exit(2) - QUIET = 5 + VERBOSITY = 5 - for o, a in opts: - if o == '-V': - QUIET = 10 - elif o == '-q': - QUIET = 0 - elif o == '-h': + for option, value in opts: + if option == '-V': + VERBOSITY = 10 + elif option == '-q': + VERBOSITY = 0 + elif option == '-h': usage() sys.exit(0) - elif o == '-e': + elif option == '-e': configuration['erase'] = True - elif o == '-w': + elif option == '-w': configuration['write'] = True - elif o == '-v': + elif option == '-v': configuration['verify'] = True - elif o == '-r': + elif option == '-r': configuration['read'] = True - elif o == '-p': - configuration['port'] = a - elif o == '-s': + elif option == '-p': + configuration['port'] = value + elif option == '-s': configuration['swap_rts_dtr'] = True - elif o == '-R': + elif option == '-R': configuration['reset_active_high'] = True - elif o == '-B': + elif option == '-B': configuration['boot0_active_high'] = True - elif o == '-b': - configuration['baud'] = eval(a) - elif o == '-a': - configuration['address'] = eval(a) - elif o == '-g': - configuration['go_address'] = eval(a) - elif o == '-l': - configuration['length'] = eval(a) + elif option == '-b': + configuration['baud'] = eval(value) + elif option == '-a': + configuration['address'] = eval(value) + elif option == '-g': + configuration['go_address'] = eval(value) + elif option == '-l': + configuration['length'] = eval(value) else: - assert False, "unhandled option %s" % o + assert False, "unhandled option %s" % option interface = CommandInterface( swap_rts_dtr=configuration['swap_rts_dtr'], From cf3ef2d9a5a343a6cef9275d0da8912939cff41c Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Thu, 15 Mar 2018 01:09:27 +0100 Subject: [PATCH 036/369] Rename classes --- stm32loader.py | 58 +++++++++++++++++++++++++------------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/stm32loader.py b/stm32loader.py index 99e35b0..a0f609d 100755 --- a/stm32loader.py +++ b/stm32loader.py @@ -53,11 +53,11 @@ def debug(level, message): print(message, file=sys.stderr) -class CmdException(Exception): +class CommandException(Exception): pass -class CommandInterface: +class Stm32Bootloader: class Command: # See ST AN3155 @@ -123,7 +123,7 @@ def command(self, command): def get(self): if not self.command(self.Command.GET): - raise CmdException("Get (0x00) failed") + raise CommandException("Get (0x00) failed") debug(10, "*** Get interface") length = bytearray(self.serial.read())[0] version = bytearray(self.serial.read())[0] @@ -137,7 +137,7 @@ def get(self): def get_version(self): if not self.command(self.Command.GET_VERSION): - raise CmdException("GetVersion (0x01) failed") + raise CommandException("GetVersion (0x01) failed") debug(10, "*** GetVersion interface") version = bytearray(self.serial.read())[0] @@ -148,7 +148,7 @@ def get_version(self): def get_id(self): if not self.command(self.Command.GET_ID): - raise CmdException("GetID (0x02) failed") + raise CommandException("GetID (0x02) failed") debug(10, "*** GetID interface") length = bytearray(self.serial.read())[0] @@ -160,7 +160,7 @@ def get_id(self): def read_memory(self, address, length): assert(length <= 256) if not self.command(self.Command.READ_MEMORY): - raise CmdException("ReadMemory (0x11) failed") + raise CommandException("ReadMemory (0x11) failed") debug(10, "*** ReadMemory interface") self.serial.write(self._encode_address(address)) @@ -173,7 +173,7 @@ def read_memory(self, address, length): def go(self, address): if not self.command(self.Command.GO): - raise CmdException("Go (0x21) failed") + raise CommandException("Go (0x21) failed") debug(10, "*** Go interface") self.serial.write(self._encode_address(address)) @@ -182,7 +182,7 @@ def go(self, address): def write_memory(self, address, data): assert(len(data) <= 256) if not self.command(self.Command.WRITE_MEMORY): - raise CmdException("Write memory (0x31) failed") + raise CommandException("Write memory (0x31) failed") debug(10, "*** Write memory interface") self.serial.write(self._encode_address(address)) @@ -200,10 +200,10 @@ def write_memory(self, address, data): def erase_memory(self, sectors=None): if self.extended_erase: - return interface.extended_erase_memory() + return bootloader.extended_erase_memory() if not self.command(self.Command.ERASE): - raise CmdException("Erase memory (0x43) failed") + raise CommandException("Erase memory (0x43) failed") debug(10, "*** Erase memory interface") @@ -216,7 +216,7 @@ def erase_memory(self, sectors=None): def extended_erase_memory(self): if not self.command(self.Command.EXTENDED_ERASE): - raise CmdException("Extended Erase memory (0x44) failed") + raise CommandException("Extended Erase memory (0x44) failed") debug(10, "*** Extended Erase memory interface") # Global mass erase and checksum byte @@ -232,7 +232,7 @@ def extended_erase_memory(self): def write_protect(self, pages): if not self.command(self.Command.WRITE_PROTECT): - raise CmdException("Write Protect memory (0x63) failed") + raise CommandException("Write Protect memory (0x63) failed") debug(10, "*** Write protect interface") nr_of_pages = (len(pages) - 1) & 0xFF @@ -247,7 +247,7 @@ def write_protect(self, pages): def write_unprotect(self): if not self.command(self.Command.WRITE_UNPROTECT): - raise CmdException("Write Unprotect (0x73) failed") + raise CommandException("Write Unprotect (0x73) failed") debug(10, "*** Write Unprotect interface") self._wait_for_ack("0x73 write unprotect failed") @@ -256,7 +256,7 @@ def write_unprotect(self): def readout_protect(self): if not self.command(self.Command.READOUT_PROTECT): - raise CmdException("Readout protect (0x82) failed") + raise CommandException("Readout protect (0x82) failed") debug(10, "*** Readout protect interface") self._wait_for_ack("0x82 readout protect failed") @@ -265,7 +265,7 @@ def readout_protect(self): def readout_unprotect(self): if not self.command(self.Command.READOUT_UNPROTECT): - raise CmdException("Readout unprotect (0x92) failed") + raise CommandException("Readout unprotect (0x92) failed") debug(10, "*** Readout Unprotect interface") self._wait_for_ack("0x92 readout unprotect failed") @@ -344,13 +344,13 @@ def _wait_for_ack(self, info=""): try: ack = bytearray(self.serial.read())[0] except TypeError: - raise CmdException("Can't read port or timeout") + raise CommandException("Can't read port or timeout") if ack == self.Reply.NACK: - raise CmdException("NACK " + info) + raise CommandException("NACK " + info) if ack != self.Reply.ACK: - raise CmdException("Unknown response. " + info + ": " + hex(ack)) + raise CommandException("Unknown response. " + info + ": " + hex(ack)) return 1 @@ -451,22 +451,22 @@ def usage(): else: assert False, "unhandled option %s" % option - interface = CommandInterface( + bootloader = Stm32Bootloader( swap_rts_dtr=configuration['swap_rts_dtr'], reset_active_high=configuration['reset_active_high'], boot0_active_high=configuration['boot0_active_high'], ) - interface.open(configuration['port'], configuration['baud']) + bootloader.open(configuration['port'], configuration['baud']) debug(10, "Open port %(port)s, baud %(baud)d" % {'port': configuration['port'], 'baud': configuration['baud']}) try: try: - interface.reset_from_system_memory() + bootloader.reset_from_system_memory() except Exception: print("Can't init. Ensure that BOOT0 is enabled and reset device") - boot_version = interface.get() + boot_version = bootloader.get() debug(0, "Bootloader version %X" % boot_version) - device_id = interface.get_id() + device_id = bootloader.get_id() debug(0, "Chip id: 0x%x (%s)" % (device_id, CHIP_IDS.get(device_id, "Unknown"))) binary_data = None @@ -477,13 +477,13 @@ def usage(): binary_data = bytearray(read_file.read()) if configuration['erase']: - interface.erase_memory() + bootloader.erase_memory() if configuration['write']: - interface.write_memory_data(configuration['address'], binary_data) + bootloader.write_memory_data(configuration['address'], binary_data) if configuration['verify']: - read_data = interface.read_memory_data(configuration['address'], len(binary_data)) + read_data = bootloader.read_memory_data(configuration['address'], len(binary_data)) if binary_data == read_data: print("Verification OK") else: @@ -494,12 +494,12 @@ def usage(): print(hex(i) + ': ' + hex(binary_data[i]) + ' vs ' + hex(read_data[i])) if not configuration['write'] and configuration['read']: - read_data = interface.read_memory_data(configuration['address'], configuration['length']) + read_data = bootloader.read_memory_data(configuration['address'], configuration['length']) with open(data_file, 'wb') as out_file: out_file.write(read_data) if configuration['go_address'] != -1: - interface.go(configuration['go_address']) + bootloader.go(configuration['go_address']) finally: - interface.reset_from_flash() + bootloader.reset_from_flash() From 837f629d0557766cbe62c1018aad4254501a1676 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Sun, 18 Mar 2018 21:21:52 +0100 Subject: [PATCH 037/369] Print a nicer error message when connection fails --- stm32loader.py | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/stm32loader.py b/stm32loader.py index a0f609d..b0a8595 100755 --- a/stm32loader.py +++ b/stm32loader.py @@ -91,16 +91,27 @@ def __init__(self, swap_rts_dtr=False, reset_active_high=False, boot0_active_hig self._boot0_active_high = boot0_active_high def open(self, serial_port='/dev/tty.usbserial-ftCYPMYJ', baud_rate=115200): - self.serial = serial.Serial( - port=serial_port, - baudrate=baud_rate, - bytesize=8, # number of write_data bits - parity=serial.PARITY_EVEN, - stopbits=1, - xonxoff=0, # don't enable software flow control - rtscts=0, # don't enable RTS/CTS flow control - timeout=5 # set a timeout value, None for waiting forever - ) + try: + self.serial = serial.Serial( + port=serial_port, + baudrate=baud_rate, + bytesize=8, # number of write_data bits + parity=serial.PARITY_EVEN, + stopbits=1, + xonxoff=0, # don't enable software flow control + rtscts=0, # don't enable RTS/CTS flow control + timeout=5 # set a timeout value, None for waiting forever + ) + except serial.serialutil.SerialException as e: + sys.stderr.write(str(e) + "\n") + sys.stderr.write( + "Is the device connected and powered correctly?\n" + "Please use the -p option to select the correct serial port. Examples:\n" + " -p COM3\n" + " -p /dev/ttyS0" + " -p /dev/tty.usbserial-ftCYPMYJ\n" + ) + exit(1) def reset_from_system_memory(self): self._enable_boot0(True) From 77b8d9fb51f0e45ad19823db9ea47feec088dd18 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Sun, 18 Mar 2018 21:22:39 +0100 Subject: [PATCH 038/369] Clarify write/verify/read commands --- README.md | 12 ++++++------ stm32loader.py | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 5c77ddb..a326007 100644 --- a/README.md +++ b/README.md @@ -15,12 +15,12 @@ Usage -q Quiet -V Verbose -e Erase (note: this is required on previously written memory) - -w Write - -v Verify (recommended) - -r Read - -s Swap RTS and DTR: use RTS for reset and DTR for boot0. - -R Make reset active high. - -B Make boot0 active high. + -w Write file content to flash + -v Verify flash content versus local file (recommended) + -r Read from flash and store in local file + -s Swap RTS and DTR: use RTS for reset and DTR for boot0 + -R Make reset active high + -B Make boot0 active high -l length Length of read -p port Serial port (default: /dev/tty.usbserial-ftCYPMYJ) -b baud Baud speed (default: 115200) diff --git a/stm32loader.py b/stm32loader.py index b0a8595..4d2ca61 100755 --- a/stm32loader.py +++ b/stm32loader.py @@ -381,12 +381,12 @@ def usage(): -q Quiet -V Verbose -e Erase (note: this is required on previously written memory) - -w Write - -v Verify (recommended) - -r Read - -s Swap RTS and DTR: use RTS for reset and DTR for boot0. - -R Make reset active high. - -B Make boot0 active high. + -w Write file content to flash + -v Verify flash content versus local file (recommended) + -r Read from flash and store in local file + -s Swap RTS and DTR: use RTS for reset and DTR for boot0 + -R Make reset active high + -B Make boot0 active high -l length Length of read -p port Serial port (default: /dev/tty.usbserial-ftCYPMYJ) -b baud Baud speed (default: 115200) From 7d01982660974602ed98b262a640383011989c46 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Sun, 18 Mar 2018 21:26:32 +0100 Subject: [PATCH 039/369] Add a version number Choosing 0.3.0 since there were two main authors before (Ivan A-R, jsnyder). --- stm32loader.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/stm32loader.py b/stm32loader.py index 4d2ca61..501bcb6 100755 --- a/stm32loader.py +++ b/stm32loader.py @@ -32,6 +32,9 @@ import time +VERSION = (0, 3, 0) +__version__ = '.'.join(map(str, VERSION)) + VERBOSITY = 20 CHIP_IDS = { From 56b515f7dd9d86e057b4b42f94154ff544e7f8ce Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Sun, 18 Mar 2018 21:36:01 +0100 Subject: [PATCH 040/369] Document significance of args[0] --- stm32loader.py | 1 + 1 file changed, 1 insertion(+) diff --git a/stm32loader.py b/stm32loader.py index 501bcb6..a2e5302 100755 --- a/stm32loader.py +++ b/stm32loader.py @@ -484,6 +484,7 @@ def usage(): debug(0, "Chip id: 0x%x (%s)" % (device_id, CHIP_IDS.get(device_id, "Unknown"))) binary_data = None + # if there's a non-named argument left, that's a file name data_file = args[0] if args else None if configuration['write'] or configuration['verify']: From c51c096a3503eec909c8292a084368dfd69557fe Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Thu, 5 Apr 2018 09:51:09 +0200 Subject: [PATCH 041/369] Add changelog document --- CHANGELOG.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..fa3f5df --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,26 @@ + +# v0.3.0 +* Add version number. +* Add this changelog. + +# 2018-05 +* Make RTS/DTR (boot0/reset) configurable (polarity, swap). + +# 2018-04 +* Restore Python 2 compatibility. + +# 2018-03 +* Add support for Python 3. +* Remove Psyco and progressbar support. +* Fix checksum calculation bug for paged erase. + +# 2014-04 +* Add `-g
` (GO command). +* Add known chip IDs. +* Implement extended erase for STM32 F2/F4. + +# 2013-10 +* Add Windows compatibility. + +# 2009-04 +* Add GPL license. From 41de0e6bac3976b7f3c9f73c85c0bbad7ba866c1 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Thu, 5 Apr 2018 09:54:50 +0200 Subject: [PATCH 042/369] Give proper credit to the other authors --- README.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a326007..6fbbb69 100644 --- a/README.md +++ b/README.md @@ -42,8 +42,13 @@ This will pre-erase flash, write `somefile.bin` to the flash on the device, and Acknowledgement --------------- -Original Version by: Ivan A-R . +Original Version by Ivan A-R (tuxotronic.org). +Contributions by Domen Puncer, James Snyder, Floris Lambrechts. -Inspiration for the configurable RTS/DTR and polarity feature: -hhttps://github.com/pazzarpj/stm32loader . +Inspiration for features from: +* Configurable RTS/DTR and polarity, extended erase with sectors: + https://github.com/pazzarpj/stm32loader + +* Correct checksum calculation for sector erase: + https://github.com/jsnyder/stm32loader/pull/4 From 84456b7eb2de45465f5d248b2371730d1ddd50d4 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Sun, 18 Mar 2018 23:25:19 +0100 Subject: [PATCH 043/369] Make parity configurable for BlueNRG devices cfr AN 4872 and https://github.com/lchish/stm32loader/commit/4da025038ded6ee07c7ae551175dff5519a1fb2a --- README.md | 7 ++++--- stm32loader.py | 42 ++++++++++++++++++++++++++++++------------ 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 6fbbb69..6830135 100644 --- a/README.md +++ b/README.md @@ -12,16 +12,17 @@ Usage ``` ./stm32loader.py [-hqVewvr] [-l length] [-p port] [-b baud] [-a addr] [file.bin] -h This help - -q Quiet - -V Verbose + -q Quiet mode + -V Verbose mode -e Erase (note: this is required on previously written memory) -w Write file content to flash -v Verify flash content versus local file (recommended) -r Read from flash and store in local file + -l length Length of read -s Swap RTS and DTR: use RTS for reset and DTR for boot0 -R Make reset active high -B Make boot0 active high - -l length Length of read + -P parity Parity: "even" for STM32 (default), "none" for BlueNRG -p port Serial port (default: /dev/tty.usbserial-ftCYPMYJ) -b baud Baud speed (default: 115200) -a address Target address diff --git a/stm32loader.py b/stm32loader.py index a2e5302..1c2a8c1 100755 --- a/stm32loader.py +++ b/stm32loader.py @@ -85,6 +85,11 @@ class Reply: ACK = 0x79 NACK = 0x1F + PARITY = dict( + even=serial.PARITY_EVEN, + none=serial.PARITY_NONE, + ) + extended_erase = False def __init__(self, swap_rts_dtr=False, reset_active_high=False, boot0_active_high=False): @@ -93,17 +98,21 @@ def __init__(self, swap_rts_dtr=False, reset_active_high=False, boot0_active_hig self._reset_active_high = reset_active_high self._boot0_active_high = boot0_active_high - def open(self, serial_port='/dev/tty.usbserial-ftCYPMYJ', baud_rate=115200): + def open(self, serial_port, baud_rate=115200, parity=serial.PARITY_EVEN): try: self.serial = serial.Serial( port=serial_port, baudrate=baud_rate, - bytesize=8, # number of write_data bits - parity=serial.PARITY_EVEN, + # number of write_data bits + bytesize=8, + parity=parity, stopbits=1, - xonxoff=0, # don't enable software flow control - rtscts=0, # don't enable RTS/CTS flow control - timeout=5 # set a timeout value, None for waiting forever + # don't enable software flow control + xonxoff=0, + # don't enable RTS/CTS flow control + rtscts=0, + # set a timeout value, None for waiting forever + timeout=5, ) except serial.serialutil.SerialException as e: sys.stderr.write(str(e) + "\n") @@ -379,18 +388,19 @@ def _encode_address(address): def usage(): - help_text = """Usage: %s [-hqVewvrsRB] [-l length] [-p port] [-b baud] [-a address] [-g address] [file.bin] + help_text = """Usage: %s [-hqVewvrsRB] [-l length] [-p port] [-b baud] [-P parity] [-a address] [-g address] [file.bin] -h This help - -q Quiet - -V Verbose + -q Quiet mode + -V Verbose mode -e Erase (note: this is required on previously written memory) -w Write file content to flash -v Verify flash content versus local file (recommended) -r Read from flash and store in local file + -l length Length of read -s Swap RTS and DTR: use RTS for reset and DTR for boot0 -R Make reset active high -B Make boot0 active high - -l length Length of read + -P parity Parity: "even" for STM32 (default), "none" for BlueNRG -p port Serial port (default: /dev/tty.usbserial-ftCYPMYJ) -b baud Baud speed (default: 115200) -a address Target address @@ -407,6 +417,7 @@ def usage(): configuration = { 'port': '/dev/tty.usbserial-ftCYPMYJ', 'baud': 115200, + 'parity': serial.PARITY_EVEN, 'address': 0x08000000, 'erase': False, 'write': False, @@ -420,7 +431,7 @@ def usage(): try: # parse command-line arguments using getopt - opts, args = getopt.getopt(sys.argv[1:], "hqVewvrsRBp:b:a:l:g:") + opts, args = getopt.getopt(sys.argv[1:], "hqVewvrsRBP:p:b:a:l:g:") except getopt.GetoptError as err: # print help information and exit: # this print something like "option -a not recognized" @@ -456,6 +467,9 @@ def usage(): configuration['boot0_active_high'] = True elif option == '-b': configuration['baud'] = eval(value) + elif option == '-P': + assert value.lower() in Stm32Bootloader.PARITY, "Parity value not recognized: '{0}'.".format(value) + configuration['parity'] = Stm32Bootloader.PARITY[value.lower()] elif option == '-a': configuration['address'] = eval(value) elif option == '-g': @@ -470,7 +484,11 @@ def usage(): reset_active_high=configuration['reset_active_high'], boot0_active_high=configuration['boot0_active_high'], ) - bootloader.open(configuration['port'], configuration['baud']) + bootloader.open( + configuration['port'], + configuration['baud'], + configuration['parity'], + ) debug(10, "Open port %(port)s, baud %(baud)d" % {'port': configuration['port'], 'baud': configuration['baud']}) try: try: From 476b4d5a8952a0501651ca0a47e25052fe66ac7a Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Sun, 18 Mar 2018 23:25:37 +0100 Subject: [PATCH 044/369] Add example BlueNRG device ID cfr https://github.com/lchish/stm32loader/commit/4da025038ded6ee07c7ae551175dff5519a1fb2a --- stm32loader.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/stm32loader.py b/stm32loader.py index 1c2a8c1..71a306d 100755 --- a/stm32loader.py +++ b/stm32loader.py @@ -48,6 +48,10 @@ 0x416: "STM32 Medium-density ultralow power line", 0x411: "STM32F2xx", 0x413: "STM32F4xx", + + # see ST AN4872 + # requires parity None + 0x11103: "BlueNRG", } From 58cf0fd0d1e408fe4c87184ef6528ae945aab645 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Thu, 5 Apr 2018 09:33:34 +0200 Subject: [PATCH 045/369] Describe source of inspiration for BlueNRG support --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 6830135..5730d22 100644 --- a/README.md +++ b/README.md @@ -53,3 +53,6 @@ Inspiration for features from: * Correct checksum calculation for sector erase: https://github.com/jsnyder/stm32loader/pull/4 + +* ST BlueNRG chip support + https://github.com/lchish/stm32loader From 18f4376204771306948e46672f59a8bca6fa9593 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 21 Mar 2018 09:49:14 +0100 Subject: [PATCH 046/369] Add support for WizNet W7500 microcontroller (SweetPeas bootloader) It's not completely clear if the W7500 Cortex-M0 supports the bootloader protocol natively, or if this is added by the SweetPeas bootloader. --- CHANGELOG.md | 2 ++ README.md | 6 ++++++ stm32loader.py | 6 ++++++ 3 files changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa3f5df..5d9beb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ # v0.3.0 * Add version number. * Add this changelog. +* Support ST BlueNRG devices (configurable parity). +* Add Wiznet W7500 / SweetPeas bootloader chip ID. # 2018-05 * Make RTS/DTR (boot0/reset) configurable (polarity, swap). diff --git a/README.md b/README.md index 5730d22..f0bc0e1 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,9 @@ STM32Loader Python script which will talk to the STM32 bootloader to upload and download firmware. +Also supports ST BlueNRG devices, and the SweetPeas bootloader +for Wiznet W7500. + Compatible with Python version 2.6 to 2.7, 3.2 to 3.6. @@ -56,3 +59,6 @@ Inspiration for features from: * ST BlueNRG chip support https://github.com/lchish/stm32loader + +* Wiznet W7500 chip / SeetPeas custom bootloader support + https://github.com/Sweet-Peas/WiznetLoader diff --git a/stm32loader.py b/stm32loader.py index 71a306d..9f94d6b 100755 --- a/stm32loader.py +++ b/stm32loader.py @@ -52,6 +52,12 @@ # see ST AN4872 # requires parity None 0x11103: "BlueNRG", + + # other + + # Cortex-M0 MCU with hardware TCP/IP and MAC + # (SweetPeas custom bootloader) + 0x801: "Wiznet W7500", } From 0efb3db645c5892ed09e86312893596e5e7f2efc Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 4 Apr 2018 13:59:48 +0200 Subject: [PATCH 047/369] Swap DTR polarity This was incorrect to start with. --- stm32loader.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/stm32loader.py b/stm32loader.py index 9f94d6b..5d7bc64 100755 --- a/stm32loader.py +++ b/stm32loader.py @@ -352,8 +352,13 @@ def _reset(self): time.sleep(0.5) def _enable_reset(self, enable=True): - # active low unless otherwise specified - level = 0 if enable else 1 + # reset on the MCU is active low (0 Volt puts the MCU in reset) + # but RS-232 DTR is active low by itself so it inverts this + # (writing logical 1 outputs a low voltage) + level = 1 if enable else 0 + + # setting -R (reset active high) ensures that the MCU + # gets 3.3 Volt to enable reset if self._reset_active_high: level = 1 - level @@ -365,7 +370,9 @@ def _enable_reset(self, enable=True): def _enable_boot0(self, enable=True): # active low unless otherwise specified level = 0 if enable else 1 + if self._boot0_active_high: + # enabled by argument -B (boot0 active high) level = 1 - level if self._swap_RTS_DTR: From aede421350937d001e1526a5af9403fb1efd7b0b Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 4 Apr 2018 13:59:08 +0200 Subject: [PATCH 048/369] Read out flash size and device UID --- README.md | 3 +- stm32loader.py | 85 ++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 74 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index f0bc0e1..f51d56f 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Usage ----- ``` -./stm32loader.py [-hqVewvr] [-l length] [-p port] [-b baud] [-a addr] [file.bin] +./stm32loader.py [-hqVewvr] [-l length] [-p port] [-b baud] [-a addr] [-f family] [file.bin] -h This help -q Quiet mode -V Verbose mode @@ -30,6 +30,7 @@ Usage -b baud Baud speed (default: 115200) -a address Target address -g address Address to start running at (0x08000000, usually) + -f family Device family to read out device UID and flash size; e.g F1 for STM32F1xx ``` diff --git a/stm32loader.py b/stm32loader.py index 5d7bc64..837ff7e 100755 --- a/stm32loader.py +++ b/stm32loader.py @@ -39,15 +39,21 @@ CHIP_IDS = { # see ST AN2606 - 0x412: "STM32 Low-density", - 0x410: "STM32 Medium-density", - 0x414: "STM32 High-density", - 0x420: "STM32 Medium-density value line", - 0x428: "STM32 High-density value line", - 0x430: "STM32 XL-density", - 0x416: "STM32 Medium-density ultralow power line", - 0x411: "STM32F2xx", - 0x413: "STM32F4xx", + # 16 to 32 KiB + 0x412: "STM32F10x Low-density", + # 64 to 128 KiB + 0x410: "STM32F10x Medium-density", + 0x420: "STM32F10x Medium-density value line", + # 256 to 512 KiB (5128 Kbyte is probably a typo?) + 0x414: "STM32F10x High-density", + 0x428: "STM32F10x High-density value line", + # 768 to 1024 KiB + 0x430: "STM3210xx XL-density", + # flash size to be looked up + 0x416: "STM32L1xxx6(8/B) Medium-density ultralow power line", + 0x411: "STM32F2xxx", + 0x413: "STM32F40xxx/41xxx", + 0x419: "STM3242xxx/43xxx", # see ST AN4872 # requires parity None @@ -100,6 +106,24 @@ class Reply: none=serial.PARITY_NONE, ) + UID_ADDRESS = { + # ST RM0008 section 30.1 Unique device ID register + # F101, F102, F103, F105, F107 + 'F1': 0x1FFFF7E8, + # ST RM0090 section 39.1 Unique device ID register + # F405/415, F407/417, F427/437, F429/439 + 'F4': 0x1FFFF7A10, + } + + FLASH_SIZE_ADDRESS = { + # ST RM0008 section 30.2 Memory size registers + # F101, F102, F103, F105, F107 + 'F1': 0x1FFFF7E0, + # ST RM0090 section 39.2 Unique device ID register + # F405/415, F407/417, F427/437, F429/439 + 'F4': 0x1FFF7A22, + } + extended_erase = False def __init__(self, swap_rts_dtr=False, reset_active_high=False, boot0_active_high=False): @@ -190,6 +214,24 @@ def get_id(self): _device_id = reduce(lambda x, y: x * 0x100 + y, id_data) return _device_id + def get_flash_size(self, device_family): + flash_size_address = self.FLASH_SIZE_ADDRESS[device_family] + flash_size_bytes = self.read_memory(flash_size_address, 2) + flash_size = flash_size_bytes[0] + flash_size_bytes[1] * 256 + return flash_size + + def get_uid(self, device_id): + uid_address = self.UID_ADDRESS[device_id] + uid = self.read_memory(uid_address, 12) + return uid + + @staticmethod + def format_uid(uid): + UID_SWAP = [[1, 0], [3, 2], [7, 6, 5, 4], [11, 10, 9, 8]] + swapped_data = [[uid[b] for b in part] for part in UID_SWAP] + uid_string = '-'.join(''.join(format(b, '02X') for b in part) for part in swapped_data) + return uid_string + def read_memory(self, address, length): assert(length <= 256) if not self.command(self.Command.READ_MEMORY): @@ -405,7 +447,7 @@ def _encode_address(address): def usage(): - help_text = """Usage: %s [-hqVewvrsRB] [-l length] [-p port] [-b baud] [-P parity] [-a address] [-g address] [file.bin] + help_text = """Usage: %s [-hqVewvrsRB] [-l length] [-p port] [-b baud] [-P parity] [-a address] [-g address] [-f family] [file.bin] -h This help -q Quiet mode -V Verbose mode @@ -422,6 +464,7 @@ def usage(): -b baud Baud speed (default: 115200) -a address Target address -g address Address to start running at (0x08000000, usually) + -f family Device family to read out device UID and flash size; e.g F1 for STM32F1xx ./stm32loader.py -e -w -v example/main.bin """ @@ -444,11 +487,12 @@ def usage(): 'swap_rts_dtr': False, 'reset_active_high': False, 'boot0_active_high': False, + 'family': None, } try: # parse command-line arguments using getopt - opts, args = getopt.getopt(sys.argv[1:], "hqVewvrsRBP:p:b:a:l:g:") + opts, args = getopt.getopt(sys.argv[1:], "hqVewvrsRBP:f:p:b:a:l:g:") except getopt.GetoptError as err: # print help information and exit: # this print something like "option -a not recognized" @@ -493,6 +537,8 @@ def usage(): configuration['go_address'] = eval(value) elif option == '-l': configuration['length'] = eval(value) + elif option == '-f': + configuration['family'] = value else: assert False, "unhandled option %s" % option @@ -514,9 +560,22 @@ def usage(): print("Can't init. Ensure that BOOT0 is enabled and reset device") boot_version = bootloader.get() - debug(0, "Bootloader version %X" % boot_version) + high = (boot_version & 0xF0) >> 4 + low = boot_version & 0x0F + debug(0, "Bootloader version: V%d.%d" % (high, low)) device_id = bootloader.get_id() - debug(0, "Chip id: 0x%x (%s)" % (device_id, CHIP_IDS.get(device_id, "Unknown"))) + debug(0, "Chip ID: 0x%x (%s)" % (device_id, CHIP_IDS.get(device_id, "Unknown"))) + + family = configuration['family'] + if not family: + debug(0, "Supply -f [family] to see flash size and device UID, e.g: -f F1") + else: + device_uid = bootloader.get_uid(family) + device_uid_string = bootloader.format_uid(device_uid) + debug(0, "Device UID: %s" % device_uid_string) + + flash_size = bootloader.get_flash_size(family) + debug(0, "Flash size: %d KiB" % flash_size) binary_data = None # if there's a non-named argument left, that's a file name From 38c42a39513b5a42e328ae98981f01e87d63dc28 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 4 Apr 2018 13:59:40 +0200 Subject: [PATCH 049/369] Describe electrical connections --- README.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/README.md b/README.md index f51d56f..3202672 100644 --- a/README.md +++ b/README.md @@ -63,3 +63,27 @@ Inspiration for features from: * Wiznet W7500 chip / SeetPeas custom bootloader support https://github.com/Sweet-Peas/WiznetLoader + + +Electrically +------------ + +The below assumes you care connecting an STM32F10x. +For other chips, the serial pins and/or the BOOT0 / BOOT1 values +may differ. + +Make the following connections: + +- Serial adapter GND to MCU GND. +- Serial adapter power to MCU power or vice versa (either 3.3 or 5 Volt). +- Note if you're using 5 Volt signaling or 3V3 on the serial adapter. +- Serial TX to MCU RX (PA10). +- Serial RX to MCU TX (PA9). +- Serial DTR to MCU RESET. +- Serial RTS to MCU BOOT0 (or BOOT0 to 3.3V). +- MCU BOOT1 to GND. + +If either RTS or DTR are not available on your serial adapter, you'll have to +manually push buttons or work with jumpers. +When given a choice, set BOOT0 manually high and drive reset through the serial +adepter (it needs to toggle, whereas BOOT0 does not). From 5771c0de5c18976f56d9c076269a975d88c02c2b Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Thu, 5 Apr 2018 16:17:23 +0200 Subject: [PATCH 050/369] List more example serial ports --- stm32loader.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/stm32loader.py b/stm32loader.py index 837ff7e..3bb85e1 100755 --- a/stm32loader.py +++ b/stm32loader.py @@ -154,7 +154,8 @@ def open(self, serial_port, baud_rate=115200, parity=serial.PARITY_EVEN): "Is the device connected and powered correctly?\n" "Please use the -p option to select the correct serial port. Examples:\n" " -p COM3\n" - " -p /dev/ttyS0" + " -p /dev/ttyS0\n" + " -p /dev/ttyUSB0\n" " -p /dev/tty.usbserial-ftCYPMYJ\n" ) exit(1) From 71faa23f79e134240f15480e17d2ee889db0b51b Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 19 Mar 2018 00:11:43 +0100 Subject: [PATCH 051/369] Cosmetic: refer to commands as 'command' --- stm32loader.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/stm32loader.py b/stm32loader.py index 3bb85e1..82d4aa3 100755 --- a/stm32loader.py +++ b/stm32loader.py @@ -182,7 +182,7 @@ def command(self, command): def get(self): if not self.command(self.Command.GET): raise CommandException("Get (0x00) failed") - debug(10, "*** Get interface") + debug(10, "*** Get command") length = bytearray(self.serial.read())[0] version = bytearray(self.serial.read())[0] debug(10, " Bootloader version: " + hex(version)) @@ -197,7 +197,7 @@ def get_version(self): if not self.command(self.Command.GET_VERSION): raise CommandException("GetVersion (0x01) failed") - debug(10, "*** GetVersion interface") + debug(10, "*** GetVersion command") version = bytearray(self.serial.read())[0] self.serial.read(2) self._wait_for_ack("0x01 end") @@ -208,7 +208,7 @@ def get_id(self): if not self.command(self.Command.GET_ID): raise CommandException("GetID (0x02) failed") - debug(10, "*** GetID interface") + debug(10, "*** GetID command") length = bytearray(self.serial.read())[0] id_data = bytearray(self.serial.read(length + 1)) self._wait_for_ack("0x02 end") @@ -238,7 +238,7 @@ def read_memory(self, address, length): if not self.command(self.Command.READ_MEMORY): raise CommandException("ReadMemory (0x11) failed") - debug(10, "*** ReadMemory interface") + debug(10, "*** ReadMemory command") self.serial.write(self._encode_address(address)) self._wait_for_ack("0x11 address failed") nr_of_bytes = (length - 1) & 0xFF @@ -251,7 +251,7 @@ def go(self, address): if not self.command(self.Command.GO): raise CommandException("Go (0x21) failed") - debug(10, "*** Go interface") + debug(10, "*** Go command") self.serial.write(self._encode_address(address)) self._wait_for_ack("0x21 go failed") @@ -260,7 +260,7 @@ def write_memory(self, address, data): if not self.command(self.Command.WRITE_MEMORY): raise CommandException("Write memory (0x31) failed") - debug(10, "*** Write memory interface") + debug(10, "*** Write memory command") self.serial.write(self._encode_address(address)) self._wait_for_ack("0x31 address failed") nr_of_bytes = (len(data) - 1) & 0xFF @@ -281,7 +281,7 @@ def erase_memory(self, sectors=None): if not self.command(self.Command.ERASE): raise CommandException("Erase memory (0x43) failed") - debug(10, "*** Erase memory interface") + debug(10, "*** Erase memory command") if sectors: self._page_erase(sectors) @@ -294,7 +294,7 @@ def extended_erase_memory(self): if not self.command(self.Command.EXTENDED_ERASE): raise CommandException("Extended Erase memory (0x44) failed") - debug(10, "*** Extended Erase memory interface") + debug(10, "*** Extended Erase memory command") # Global mass erase and checksum byte self.serial.write(b'\xFF') self.serial.write(b'\xFF') @@ -310,7 +310,7 @@ def write_protect(self, pages): if not self.command(self.Command.WRITE_PROTECT): raise CommandException("Write Protect memory (0x63) failed") - debug(10, "*** Write protect interface") + debug(10, "*** Write protect command") nr_of_pages = (len(pages) - 1) & 0xFF self.serial.write(bytearray([nr_of_pages])) checksum = 0xFF @@ -325,7 +325,7 @@ def write_unprotect(self): if not self.command(self.Command.WRITE_UNPROTECT): raise CommandException("Write Unprotect (0x73) failed") - debug(10, "*** Write Unprotect interface") + debug(10, "*** Write Unprotect command") self._wait_for_ack("0x73 write unprotect failed") self._wait_for_ack("0x73 write unprotect 2 failed") debug(10, " Write Unprotect done") @@ -334,7 +334,7 @@ def readout_protect(self): if not self.command(self.Command.READOUT_PROTECT): raise CommandException("Readout protect (0x82) failed") - debug(10, "*** Readout protect interface") + debug(10, "*** Readout protect command") self._wait_for_ack("0x82 readout protect failed") self._wait_for_ack("0x82 readout protect 2 failed") debug(10, " Read protect done") From 987b0cf8f1af2929353d53a7c689938057dc00b3 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 20 Mar 2018 13:25:39 +0100 Subject: [PATCH 052/369] Reference ST application notes --- README.md | 8 ++++++++ stm32loader.py | 10 ++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3202672..531708f 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,14 @@ stm32loader.py -e -w -v somefile.bin This will pre-erase flash, write `somefile.bin` to the flash on the device, and then perform a verification after writing is finished. +Reference documents +------------------- + +* ST AN2606: STM32 microcontroller system memory boot mode +* ST AN3155: USART protocol used in the STM32 bootloader +* ST AN4872: BlueNRG-1 and BlueNRG-2 UART bootloader protocol + + Acknowledgement --------------- diff --git a/stm32loader.py b/stm32loader.py index 82d4aa3..9b31858 100755 --- a/stm32loader.py +++ b/stm32loader.py @@ -79,7 +79,7 @@ class CommandException(Exception): class Stm32Bootloader: class Command: - # See ST AN3155 + # See ST AN3155, AN4872 GET = 0x00 GET_VERSION = 0x01 GET_ID = 0x02 @@ -87,17 +87,19 @@ class Command: GO = 0x21 WRITE_MEMORY = 0x31 ERASE = 0x43 + READOUT_PROTECT = 0x82 + READOUT_UNPROTECT = 0x92 + # these not supported on BlueNRG EXTENDED_ERASE = 0x44 WRITE_PROTECT = 0x63 WRITE_UNPROTECT = 0x73 - READOUT_PROTECT = 0x82 - READOUT_UNPROTECT = 0x92 + # not really listed under commands, but still... # 'wake the bootloader' == 'activate USART' == 'synchronize' SYNCHRONIZE = 0x7F class Reply: - # See ST AN3155 + # See ST AN3155, AN4872 ACK = 0x79 NACK = 0x1F From fa9ac0871694cb693459ccc0d0568dd2c0bca59e Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 21 Mar 2018 09:58:10 +0100 Subject: [PATCH 053/369] Separate 'basic' options from 'advanced' ones --- README.md | 21 ++++++++++++--------- stm32loader.py | 25 ++++++++++++++----------- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 531708f..ed83f50 100644 --- a/README.md +++ b/README.md @@ -13,24 +13,27 @@ Usage ----- ``` -./stm32loader.py [-hqVewvr] [-l length] [-p port] [-b baud] [-a addr] [-f family] [file.bin] - -h This help - -q Quiet mode - -V Verbose mode +./stm32loader.py [-hqVewvrsRB] [-l length] [-p port] [-b baud] [-P parity] [-a address] [-g address] [-f family] [file.bin] -e Erase (note: this is required on previously written memory) -w Write file content to flash -v Verify flash content versus local file (recommended) -r Read from flash and store in local file -l length Length of read + -p port Serial port (default: /dev/tty.usbserial-ftCYPMYJ) + -b baud Baud speed (default: 115200) + -a address Target address (default: 0x08000000) + -g address Start executing from address (0x08000000, usually) + -f family Device family to read out device UID and flash size; e.g F1 for STM32F1xx + + -h Print this help text + -q Quiet mode + -V Verbose mode + -s Swap RTS and DTR: use RTS for reset and DTR for boot0 -R Make reset active high -B Make boot0 active high + -u Readout unprotect -P parity Parity: "even" for STM32 (default), "none" for BlueNRG - -p port Serial port (default: /dev/tty.usbserial-ftCYPMYJ) - -b baud Baud speed (default: 115200) - -a address Target address - -g address Address to start running at (0x08000000, usually) - -f family Device family to read out device UID and flash size; e.g F1 for STM32F1xx ``` diff --git a/stm32loader.py b/stm32loader.py index 9b31858..e4b122e 100755 --- a/stm32loader.py +++ b/stm32loader.py @@ -451,27 +451,30 @@ def _encode_address(address): def usage(): help_text = """Usage: %s [-hqVewvrsRB] [-l length] [-p port] [-b baud] [-P parity] [-a address] [-g address] [-f family] [file.bin] - -h This help - -q Quiet mode - -V Verbose mode -e Erase (note: this is required on previously written memory) -w Write file content to flash -v Verify flash content versus local file (recommended) -r Read from flash and store in local file -l length Length of read - -s Swap RTS and DTR: use RTS for reset and DTR for boot0 - -R Make reset active high - -B Make boot0 active high - -P parity Parity: "even" for STM32 (default), "none" for BlueNRG -p port Serial port (default: /dev/tty.usbserial-ftCYPMYJ) -b baud Baud speed (default: 115200) - -a address Target address - -g address Address to start running at (0x08000000, usually) + -a address Target address (default: 0x08000000) + -g address Start executing from address (0x08000000, usually) -f family Device family to read out device UID and flash size; e.g F1 for STM32F1xx - ./stm32loader.py -e -w -v example/main.bin + -h Print this help text + -q Quiet mode + -V Verbose mode + + -s Swap RTS and DTR: use RTS for reset and DTR for boot0 + -R Make reset active high + -B Make boot0 active high + -u Readout unprotect + -P parity Parity: "even" for STM32 (default), "none" for BlueNRG + + Example: ./%s -e -w -v example/main.bin """ - help_text = help_text % sys.argv[0] + help_text = help_text % (sys.argv[0], sys.argv[0]) print(help_text) From 454137962c6193b4f5a0e53a608c316690809d67 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 21 Mar 2018 13:41:09 +0100 Subject: [PATCH 054/369] Describe unsupported features --- README.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ed83f50..7f96b86 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ STM32Loader =========== -Python script which will talk to the STM32 bootloader to upload and download firmware. +Python script to upload or download firmware to / from +ST Microelectronics STM32 microcontrollers over UART. Also supports ST BlueNRG devices, and the SweetPeas bootloader for Wiznet W7500. @@ -98,3 +99,13 @@ If either RTS or DTR are not available on your serial adapter, you'll have to manually push buttons or work with jumpers. When given a choice, set BOOT0 manually high and drive reset through the serial adepter (it needs to toggle, whereas BOOT0 does not). + + +Not currently supported +----------------------- + +* Extended erase with specific sectors +* Command-line argument for readout protection +* Command-line argument for write protection/unprotection +* STM8 devices (ST UM0560) +* Other bootloader protocols (e.g. I2C, HEX -> implemented in stm32flash) From 60c967a43e8be75bcaabec8d785451233706b20d Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 19 Mar 2018 00:12:09 +0100 Subject: [PATCH 055/369] Don't wait for ack twice --- stm32loader.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/stm32loader.py b/stm32loader.py index e4b122e..9c3594c 100755 --- a/stm32loader.py +++ b/stm32loader.py @@ -329,7 +329,6 @@ def write_unprotect(self): debug(10, "*** Write Unprotect command") self._wait_for_ack("0x73 write unprotect failed") - self._wait_for_ack("0x73 write unprotect 2 failed") debug(10, " Write Unprotect done") def readout_protect(self): @@ -338,17 +337,19 @@ def readout_protect(self): debug(10, "*** Readout protect command") self._wait_for_ack("0x82 readout protect failed") - self._wait_for_ack("0x82 readout protect 2 failed") debug(10, " Read protect done") def readout_unprotect(self): if not self.command(self.Command.READOUT_UNPROTECT): raise CommandException("Readout unprotect (0x92) failed") - debug(10, "*** Readout Unprotect interface") + debug(10, "*** Readout Unprotect command") self._wait_for_ack("0x92 readout unprotect failed") - self._wait_for_ack("0x92 readout unprotect 2 failed") - debug(10, " Read Unprotect done") + debug(20, " Mass erase -- this may take a while") + time.sleep(20) + debug(20, " Unprotect / mass erase done") + debug(20, " Reset after automatic chip reset due to readout unprotect") + self.reset_from_system_memory() def read_memory_data(self, address, length): data = bytearray() From af9b53eff76eaef66f1bc3f6838409c188bceb59 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 19 Mar 2018 00:14:41 +0100 Subject: [PATCH 056/369] Properly reset the chip if communication fails --- stm32loader.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/stm32loader.py b/stm32loader.py index 9c3594c..3c674b3 100755 --- a/stm32loader.py +++ b/stm32loader.py @@ -565,6 +565,8 @@ def usage(): bootloader.reset_from_system_memory() except Exception: print("Can't init. Ensure that BOOT0 is enabled and reset device") + bootloader.reset_from_flash() + sys.exit(1) boot_version = bootloader.get() high = (boot_version & 0xF0) >> 4 From 475962990918a0b85732f9403987c1347fbc7bf0 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 19 Mar 2018 00:14:08 +0100 Subject: [PATCH 057/369] Add 'unprotect' command-line option --- README.md | 4 ++++ stm32loader.py | 30 +++++++++++++++++++++++++++--- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7f96b86..542e757 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ Usage ``` ./stm32loader.py [-hqVewvrsRB] [-l length] [-p port] [-b baud] [-P parity] [-a address] [-g address] [-f family] [file.bin] -e Erase (note: this is required on previously written memory) + -u Readout unprotect -w Write file content to flash -v Verify flash content versus local file (recommended) -r Read from flash and store in local file @@ -66,6 +67,9 @@ Inspiration for features from: * Configurable RTS/DTR and polarity, extended erase with sectors: https://github.com/pazzarpj/stm32loader + +* Memory unprotect + https://github.com/3drobotics/stm32loader * Correct checksum calculation for sector erase: https://github.com/jsnyder/stm32loader/pull/4 diff --git a/stm32loader.py b/stm32loader.py index 3c674b3..2bf6e4a 100755 --- a/stm32loader.py +++ b/stm32loader.py @@ -451,8 +451,9 @@ def _encode_address(address): def usage(): - help_text = """Usage: %s [-hqVewvrsRB] [-l length] [-p port] [-b baud] [-P parity] [-a address] [-g address] [-f family] [file.bin] + help_text = """Usage: %s [-hqVeuwvrsRB] [-l length] [-p port] [-b baud] [-P parity] [-a address] [-g address] [-f family] [file.bin] -e Erase (note: this is required on previously written memory) + -u Unprotect in case erase fails -w Write file content to flash -v Verify flash content versus local file (recommended) -r Read from flash and store in local file @@ -487,6 +488,7 @@ def usage(): 'parity': serial.PARITY_EVEN, 'address': 0x08000000, 'erase': False, + 'unprotect': False, 'write': False, 'verify': False, 'read': False, @@ -499,7 +501,7 @@ def usage(): try: # parse command-line arguments using getopt - opts, args = getopt.getopt(sys.argv[1:], "hqVewvrsRBP:f:p:b:a:l:g:") + opts, args = getopt.getopt(sys.argv[1:], "hqVeuwvrsRBP:f:p:b:a:l:g:") except getopt.GetoptError as err: # print help information and exit: # this print something like "option -a not recognized" @@ -519,6 +521,8 @@ def usage(): sys.exit(0) elif option == '-e': configuration['erase'] = True + elif option == '-u': + configuration['unprotect'] = True elif option == '-w': configuration['write'] = True elif option == '-v': @@ -594,8 +598,28 @@ def usage(): with open(data_file, 'rb') as read_file: binary_data = bytearray(read_file.read()) + if configuration['unprotect']: + try: + bootloader.readout_unprotect() + except CommandException: + # may be caused by readout protection + debug(0, "Erase failed -- probably due to readout protection") + debug(0, "Quit") + bootloader.reset_from_flash() + sys.exit(1) + if configuration['erase']: - bootloader.erase_memory() + try: + bootloader.erase_memory() + except CommandException: + # may be caused by readout protection + debug( + 0, + "Erase failed -- probably due to readout protection\n" + "consider using the -u (unprotect) option." + ) + bootloader.reset_from_flash() + sys.exit(1) if configuration['write']: bootloader.write_memory_data(configuration['address'], binary_data) From a21a582277d1d4e1d0e7b4752abf153a0eca14b5 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 21 Mar 2018 13:19:12 +0100 Subject: [PATCH 058/369] Split out __main__ functionality into separate methods --- stm32loader.py | 224 +++++++++++++++++++++++++++++++------------------ 1 file changed, 143 insertions(+), 81 deletions(-) diff --git a/stm32loader.py b/stm32loader.py index 2bf6e4a..cef1dc2 100755 --- a/stm32loader.py +++ b/stm32loader.py @@ -35,7 +35,7 @@ VERSION = (0, 3, 0) __version__ = '.'.join(map(str, VERSION)) -VERBOSITY = 20 +VERBOSITY = 5 CHIP_IDS = { # see ST AN2606 @@ -278,7 +278,7 @@ def write_memory(self, address, data): def erase_memory(self, sectors=None): if self.extended_erase: - return bootloader.extended_erase_memory() + return self.extended_erase_memory() if not self.command(self.Command.ERASE): raise CommandException("Erase memory (0x43) failed") @@ -480,8 +480,9 @@ def usage(): print(help_text) -if __name__ == "__main__": - +def _parse_arguments(): + global VERBOSITY + configuration = { 'port': '/dev/tty.usbserial-ftCYPMYJ', 'baud': 115200, @@ -498,7 +499,6 @@ def usage(): 'boot0_active_high': False, 'family': None, } - try: # parse command-line arguments using getopt opts, args = getopt.getopt(sys.argv[1:], "hqVeuwvrsRBP:f:p:b:a:l:g:") @@ -506,18 +506,16 @@ def usage(): # print help information and exit: # this print something like "option -a not recognized" print(str(err)) - usage() + _usage() sys.exit(2) - VERBOSITY = 5 - for option, value in opts: if option == '-V': VERBOSITY = 10 elif option == '-q': VERBOSITY = 0 elif option == '-h': - usage() + _usage() sys.exit(0) elif option == '-e': configuration['erase'] = True @@ -553,6 +551,12 @@ def usage(): else: assert False, "unhandled option %s" % option + configuration['data_file'] = args[0] if args else None + + return configuration + + +def _connect(configuration): bootloader = Stm32Bootloader( swap_rts_dtr=configuration['swap_rts_dtr'], reset_active_high=configuration['reset_active_high'], @@ -565,83 +569,141 @@ def usage(): ) debug(10, "Open port %(port)s, baud %(baud)d" % {'port': configuration['port'], 'baud': configuration['baud']}) try: + bootloader.reset_from_system_memory() + except Exception: + print("Can't init. Ensure that BOOT0 is enabled and reset device") + bootloader.reset_from_flash() + sys.exit(1) + + return bootloader + + +def _perform_commands(bootloader, configuration): + boot_version = bootloader.get() + debug(0, "Bootloader version %X" % boot_version) + device_id = bootloader.get_id() + debug(0, "Chip id: 0x%x (%s)" % (device_id, CHIP_IDS.get(device_id, "Unknown"))) + binary_data = None + # if there's a non-named argument left, that's a file name + if configuration['write'] or configuration['verify']: + with open(configuration['data_file'], 'rb') as read_file: + binary_data = bytearray(read_file.read()) + if configuration['unprotect']: + try: + bootloader.readout_unprotect() + except CommandException: + # may be caused by readout protection + debug(0, "Erase failed -- probably due to readout protection") + debug(0, "Quit") + bootloader.reset_from_flash() + sys.exit(1) + if configuration['erase']: + try: + bootloader.erase_memory() + except CommandException: + # may be caused by readout protection + debug( + 0, + "Erase failed -- probably due to readout protection\n" + "consider using the -u (unprotect) option." + ) + bootloader.reset_from_flash() + sys.exit(1) + if configuration['write']: + bootloader.write_memory_data(configuration['address'], binary_data) + if configuration['verify']: + read_data = bootloader.read_memory_data(configuration['address'], len(binary_data)) + if binary_data == read_data: + print("Verification OK") + else: + print("Verification FAILED") + print(str(len(binary_data)) + ' vs ' + str(len(read_data))) + for i in range(0, len(binary_data)): + if binary_data[i] != read_data[i]: + print(hex(i) + ': ' + hex(binary_data[i]) + ' vs ' + hex(read_data[i])) + if not configuration['write'] and configuration['read']: + read_data = bootloader.read_memory_data(configuration['address'], configuration['length']) + with open(configuration['data_file'], 'wb') as out_file: + out_file.write(read_data) + if configuration['go_address'] != -1: + bootloader.go(configuration['go_address']) + + boot_version = bootloader.get() + high = (boot_version & 0xF0) >> 4 + low = boot_version & 0x0F + debug(0, "Bootloader version: V%d.%d" % (high, low)) + device_id = bootloader.get_id() + debug(0, "Chip ID: 0x%x (%s)" % (device_id, CHIP_IDS.get(device_id, "Unknown"))) + + family = configuration['family'] + if not family: + debug(0, "Supply -f [family] to see flash size and device UID, e.g: -f F1") + else: + device_uid = bootloader.get_uid(family) + device_uid_string = bootloader.format_uid(device_uid) + debug(0, "Device UID: %s" % device_uid_string) + + flash_size = bootloader.get_flash_size(family) + debug(0, "Flash size: %d KiB" % flash_size) + + binary_data = None + # if there's a non-named argument left, that's a file name + data_file = args[0] if args else None + + if configuration['write'] or configuration['verify']: + with open(data_file, 'rb') as read_file: + binary_data = bytearray(read_file.read()) + + if configuration['unprotect']: + try: + bootloader.readout_unprotect() + except CommandException: + # may be caused by readout protection + debug(0, "Erase failed -- probably due to readout protection") + debug(0, "Quit") + bootloader.reset_from_flash() + sys.exit(1) + + if configuration['erase']: try: - bootloader.reset_from_system_memory() - except Exception: - print("Can't init. Ensure that BOOT0 is enabled and reset device") + bootloader.erase_memory() + except CommandException: + # may be caused by readout protection + debug( + 0, + "Erase failed -- probably due to readout protection\n" + "consider using the -u (unprotect) option." + ) bootloader.reset_from_flash() sys.exit(1) - boot_version = bootloader.get() - high = (boot_version & 0xF0) >> 4 - low = boot_version & 0x0F - debug(0, "Bootloader version: V%d.%d" % (high, low)) - device_id = bootloader.get_id() - debug(0, "Chip ID: 0x%x (%s)" % (device_id, CHIP_IDS.get(device_id, "Unknown"))) + if configuration['write']: + bootloader.write_memory_data(configuration['address'], binary_data) - family = configuration['family'] - if not family: - debug(0, "Supply -f [family] to see flash size and device UID, e.g: -f F1") + if configuration['verify']: + read_data = bootloader.read_memory_data(configuration['address'], len(binary_data)) + if binary_data == read_data: + print("Verification OK") else: - device_uid = bootloader.get_uid(family) - device_uid_string = bootloader.format_uid(device_uid) - debug(0, "Device UID: %s" % device_uid_string) - - flash_size = bootloader.get_flash_size(family) - debug(0, "Flash size: %d KiB" % flash_size) - - binary_data = None - # if there's a non-named argument left, that's a file name - data_file = args[0] if args else None - - if configuration['write'] or configuration['verify']: - with open(data_file, 'rb') as read_file: - binary_data = bytearray(read_file.read()) - - if configuration['unprotect']: - try: - bootloader.readout_unprotect() - except CommandException: - # may be caused by readout protection - debug(0, "Erase failed -- probably due to readout protection") - debug(0, "Quit") - bootloader.reset_from_flash() - sys.exit(1) - - if configuration['erase']: - try: - bootloader.erase_memory() - except CommandException: - # may be caused by readout protection - debug( - 0, - "Erase failed -- probably due to readout protection\n" - "consider using the -u (unprotect) option." - ) - bootloader.reset_from_flash() - sys.exit(1) - - if configuration['write']: - bootloader.write_memory_data(configuration['address'], binary_data) - - if configuration['verify']: - read_data = bootloader.read_memory_data(configuration['address'], len(binary_data)) - if binary_data == read_data: - print("Verification OK") - else: - print("Verification FAILED") - print(str(len(binary_data)) + ' vs ' + str(len(read_data))) - for i in range(0, len(binary_data)): - if binary_data[i] != read_data[i]: - print(hex(i) + ': ' + hex(binary_data[i]) + ' vs ' + hex(read_data[i])) - - if not configuration['write'] and configuration['read']: - read_data = bootloader.read_memory_data(configuration['address'], configuration['length']) - with open(data_file, 'wb') as out_file: - out_file.write(read_data) - - if configuration['go_address'] != -1: - bootloader.go(configuration['go_address']) + print("Verification FAILED") + print(str(len(binary_data)) + ' vs ' + str(len(read_data))) + for i in range(0, len(binary_data)): + if binary_data[i] != read_data[i]: + print(hex(i) + ': ' + hex(binary_data[i]) + ' vs ' + hex(read_data[i])) + + if not configuration['write'] and configuration['read']: + read_data = bootloader.read_memory_data(configuration['address'], configuration['length']) + with open(data_file, 'wb') as out_file: + out_file.write(read_data) + + if configuration['go_address'] != -1: + bootloader.go(configuration['go_address']) + +if __name__ == "__main__": + config = _parse_arguments() + chip = _connect(config) + try: + _perform_commands(chip, config) finally: - bootloader.reset_from_flash() + chip.reset_from_flash() From 17cc030f2332fdad024eb64fbf8e61584fc71d44 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 21 Mar 2018 13:32:57 +0100 Subject: [PATCH 059/369] Separate 'loader' methods into separate class This way, these methods can also be used by external code (i.e. not through command-line invocation). --- stm32loader.py | 404 ++++++++++++++++++++++--------------------------- 1 file changed, 177 insertions(+), 227 deletions(-) diff --git a/stm32loader.py b/stm32loader.py index cef1dc2..2ef864e 100755 --- a/stm32loader.py +++ b/stm32loader.py @@ -450,8 +450,160 @@ def _encode_address(address): return bytearray([byte0, byte1, byte2, byte3, checksum]) -def usage(): - help_text = """Usage: %s [-hqVeuwvrsRB] [-l length] [-p port] [-b baud] [-P parity] [-a address] [-g address] [-f family] [file.bin] +class Stm32Loader: + + def __init__(self): + self.bootloader = None + self.configuration = { + 'port': '/dev/tty.usbserial-ftCYPMYJ', + 'baud': 115200, + 'parity': serial.PARITY_EVEN, + 'family': None, + 'address': 0x08000000, + 'erase': False, + 'unprotect': False, + 'write': False, + 'verify': False, + 'read': False, + 'go_address': -1, + 'swap_rts_dtr': False, + 'reset_active_high': False, + 'boot0_active_high': False, + 'data_file': None, + } + + def parse_arguments(self, arguments): + global VERBOSITY + + try: + # parse command-line arguments using getopt + options, arguments = getopt.getopt(arguments, "hqVeuwvrsRBP:p:b:a:l:g:f:") + except getopt.GetoptError as err: + # print help information and exit: + # this print something like "option -a not recognized" + print(str(err)) + self.print_usage() + sys.exit(2) + + # if there's a non-named argument left, that's a file name + if arguments: + self.configuration['data_file'] = arguments[0] + + for option, value in options: + if option == '-V': + VERBOSITY = 10 + elif option == '-q': + VERBOSITY = 0 + elif option == '-h': + self.print_usage() + sys.exit(0) + elif option == '-e': + self.configuration['erase'] = True + elif option == '-u': + self.configuration['unprotect'] = True + elif option == '-w': + self.configuration['write'] = True + elif option == '-v': + self.configuration['verify'] = True + elif option == '-r': + self.configuration['read'] = True + elif option == '-p': + self.configuration['port'] = value + elif option == '-s': + self.configuration['swap_rts_dtr'] = True + elif option == '-R': + self.configuration['reset_active_high'] = True + elif option == '-B': + self.configuration['boot0_active_high'] = True + elif option == '-b': + self.configuration['baud'] = eval(value) + elif option == '-f': + self.configuration['family'] = value + elif option == '-P': + assert value.lower() in Stm32Bootloader.PARITY, "Parity value not recognized: '{0}'.".format(value) + self.configuration['parity'] = Stm32Bootloader.PARITY[value.lower()] + elif option == '-a': + self.configuration['address'] = eval(value) + elif option == '-g': + self.configuration['go_address'] = eval(value) + elif option == '-l': + self.configuration['length'] = eval(value) + else: + assert False, "unhandled option %s" % option + + def connect(self): + self.bootloader = Stm32Bootloader( + swap_rts_dtr=self.configuration['swap_rts_dtr'], + reset_active_high=self.configuration['reset_active_high'], + boot0_active_high=self.configuration['boot0_active_high'], + ) + self.bootloader.open( + self.configuration['port'], + self.configuration['baud'], + self.configuration['parity'], + ) + debug(10, "Open port %(port)s, baud %(baud)d" % { + 'port': self.configuration['port'], + 'baud': self.configuration['baud'] + }) + try: + self.bootloader.reset_from_system_memory() + except Exception: + print("Can't init. Ensure that BOOT0 is enabled and reset device") + self.bootloader.reset_from_flash() + sys.exit(1) + + def perform_commands(self): + binary_data = None + if self.configuration['write'] or self.configuration['verify']: + with open(self.configuration['data_file'], 'rb') as read_file: + binary_data = bytearray(read_file.read()) + if self.configuration['unprotect']: + try: + self.bootloader.readout_unprotect() + except CommandException: + # may be caused by readout protection + debug(0, "Erase failed -- probably due to readout protection") + debug(0, "Quit") + self.bootloader.reset_from_flash() + sys.exit(1) + if self.configuration['erase']: + try: + self.bootloader.erase_memory() + except CommandException: + # may be caused by readout protection + debug( + 0, + "Erase failed -- probably due to readout protection\n" + "consider using the -u (unprotect) option." + ) + self.bootloader.reset_from_flash() + sys.exit(1) + if self.configuration['write']: + self.bootloader.write_memory_data(self.configuration['address'], binary_data) + if self.configuration['verify']: + read_data = self.bootloader.read_memory_data(self.configuration['address'], len(binary_data)) + if binary_data == read_data: + print("Verification OK") + else: + print("Verification FAILED") + print(str(len(binary_data)) + ' vs ' + str(len(read_data))) + for i in range(0, len(binary_data)): + if binary_data[i] != read_data[i]: + print(hex(i) + ': ' + hex(binary_data[i]) + ' vs ' + hex(read_data[i])) + if not self.configuration['write'] and self.configuration['read']: + read_data = self.bootloader.read_memory_data(self.configuration['address'], self.configuration['length']) + with open(self.configuration['data_file'], 'wb') as out_file: + out_file.write(read_data) + if self.configuration['go_address'] != -1: + self.bootloader.go(self.configuration['go_address']) + + def reset(self): + self.bootloader.reset_from_flash() + + @staticmethod + def print_usage(): + help_text = """Usage: %s [-hqVeuwvrsRB] [-l length] [-p port] [-b baud] [-P parity] [-a address] [-g address] [-f family] [file.bin] -e Erase (note: this is required on previously written memory) -u Unprotect in case erase fails -w Write file content to flash @@ -475,235 +627,33 @@ def usage(): -P parity Parity: "even" for STM32 (default), "none" for BlueNRG Example: ./%s -e -w -v example/main.bin - """ - help_text = help_text % (sys.argv[0], sys.argv[0]) - print(help_text) - - -def _parse_arguments(): - global VERBOSITY - - configuration = { - 'port': '/dev/tty.usbserial-ftCYPMYJ', - 'baud': 115200, - 'parity': serial.PARITY_EVEN, - 'address': 0x08000000, - 'erase': False, - 'unprotect': False, - 'write': False, - 'verify': False, - 'read': False, - 'go_address': -1, - 'swap_rts_dtr': False, - 'reset_active_high': False, - 'boot0_active_high': False, - 'family': None, - } - try: - # parse command-line arguments using getopt - opts, args = getopt.getopt(sys.argv[1:], "hqVeuwvrsRBP:f:p:b:a:l:g:") - except getopt.GetoptError as err: - # print help information and exit: - # this print something like "option -a not recognized" - print(str(err)) - _usage() - sys.exit(2) - - for option, value in opts: - if option == '-V': - VERBOSITY = 10 - elif option == '-q': - VERBOSITY = 0 - elif option == '-h': - _usage() - sys.exit(0) - elif option == '-e': - configuration['erase'] = True - elif option == '-u': - configuration['unprotect'] = True - elif option == '-w': - configuration['write'] = True - elif option == '-v': - configuration['verify'] = True - elif option == '-r': - configuration['read'] = True - elif option == '-p': - configuration['port'] = value - elif option == '-s': - configuration['swap_rts_dtr'] = True - elif option == '-R': - configuration['reset_active_high'] = True - elif option == '-B': - configuration['boot0_active_high'] = True - elif option == '-b': - configuration['baud'] = eval(value) - elif option == '-P': - assert value.lower() in Stm32Bootloader.PARITY, "Parity value not recognized: '{0}'.".format(value) - configuration['parity'] = Stm32Bootloader.PARITY[value.lower()] - elif option == '-a': - configuration['address'] = eval(value) - elif option == '-g': - configuration['go_address'] = eval(value) - elif option == '-l': - configuration['length'] = eval(value) - elif option == '-f': - configuration['family'] = value - else: - assert False, "unhandled option %s" % option - - configuration['data_file'] = args[0] if args else None - - return configuration - - -def _connect(configuration): - bootloader = Stm32Bootloader( - swap_rts_dtr=configuration['swap_rts_dtr'], - reset_active_high=configuration['reset_active_high'], - boot0_active_high=configuration['boot0_active_high'], - ) - bootloader.open( - configuration['port'], - configuration['baud'], - configuration['parity'], - ) - debug(10, "Open port %(port)s, baud %(baud)d" % {'port': configuration['port'], 'baud': configuration['baud']}) - try: - bootloader.reset_from_system_memory() - except Exception: - print("Can't init. Ensure that BOOT0 is enabled and reset device") - bootloader.reset_from_flash() - sys.exit(1) - - return bootloader - - -def _perform_commands(bootloader, configuration): - boot_version = bootloader.get() - debug(0, "Bootloader version %X" % boot_version) - device_id = bootloader.get_id() - debug(0, "Chip id: 0x%x (%s)" % (device_id, CHIP_IDS.get(device_id, "Unknown"))) - binary_data = None - # if there's a non-named argument left, that's a file name - if configuration['write'] or configuration['verify']: - with open(configuration['data_file'], 'rb') as read_file: - binary_data = bytearray(read_file.read()) - if configuration['unprotect']: - try: - bootloader.readout_unprotect() - except CommandException: - # may be caused by readout protection - debug(0, "Erase failed -- probably due to readout protection") - debug(0, "Quit") - bootloader.reset_from_flash() - sys.exit(1) - if configuration['erase']: - try: - bootloader.erase_memory() - except CommandException: - # may be caused by readout protection - debug( - 0, - "Erase failed -- probably due to readout protection\n" - "consider using the -u (unprotect) option." - ) - bootloader.reset_from_flash() - sys.exit(1) - if configuration['write']: - bootloader.write_memory_data(configuration['address'], binary_data) - if configuration['verify']: - read_data = bootloader.read_memory_data(configuration['address'], len(binary_data)) - if binary_data == read_data: - print("Verification OK") - else: - print("Verification FAILED") - print(str(len(binary_data)) + ' vs ' + str(len(read_data))) - for i in range(0, len(binary_data)): - if binary_data[i] != read_data[i]: - print(hex(i) + ': ' + hex(binary_data[i]) + ' vs ' + hex(read_data[i])) - if not configuration['write'] and configuration['read']: - read_data = bootloader.read_memory_data(configuration['address'], configuration['length']) - with open(configuration['data_file'], 'wb') as out_file: - out_file.write(read_data) - if configuration['go_address'] != -1: - bootloader.go(configuration['go_address']) - - boot_version = bootloader.get() - high = (boot_version & 0xF0) >> 4 - low = boot_version & 0x0F - debug(0, "Bootloader version: V%d.%d" % (high, low)) - device_id = bootloader.get_id() - debug(0, "Chip ID: 0x%x (%s)" % (device_id, CHIP_IDS.get(device_id, "Unknown"))) - - family = configuration['family'] - if not family: - debug(0, "Supply -f [family] to see flash size and device UID, e.g: -f F1") - else: - device_uid = bootloader.get_uid(family) - device_uid_string = bootloader.format_uid(device_uid) - debug(0, "Device UID: %s" % device_uid_string) - - flash_size = bootloader.get_flash_size(family) - debug(0, "Flash size: %d KiB" % flash_size) - - binary_data = None - # if there's a non-named argument left, that's a file name - data_file = args[0] if args else None - - if configuration['write'] or configuration['verify']: - with open(data_file, 'rb') as read_file: - binary_data = bytearray(read_file.read()) - - if configuration['unprotect']: - try: - bootloader.readout_unprotect() - except CommandException: - # may be caused by readout protection - debug(0, "Erase failed -- probably due to readout protection") - debug(0, "Quit") - bootloader.reset_from_flash() - sys.exit(1) - - if configuration['erase']: - try: - bootloader.erase_memory() - except CommandException: - # may be caused by readout protection - debug( - 0, - "Erase failed -- probably due to readout protection\n" - "consider using the -u (unprotect) option." - ) - bootloader.reset_from_flash() - sys.exit(1) - - if configuration['write']: - bootloader.write_memory_data(configuration['address'], binary_data) - - if configuration['verify']: - read_data = bootloader.read_memory_data(configuration['address'], len(binary_data)) - if binary_data == read_data: - print("Verification OK") +""" + help_text = help_text % (sys.argv[0], sys.argv[0]) + print(help_text) + + def read_device_details(self): + boot_version = self.bootloader.get() + debug(0, "Bootloader version %X" % boot_version) + device_id = self.bootloader.get_id() + debug(0, "Chip id: 0x%x (%s)" % (device_id, CHIP_IDS.get(device_id, "Unknown"))) + family = self.configuration['family'] + if not family: + debug(0, "Supply -f [family] to see flash size and device UID, e.g: -f F1") else: - print("Verification FAILED") - print(str(len(binary_data)) + ' vs ' + str(len(read_data))) - for i in range(0, len(binary_data)): - if binary_data[i] != read_data[i]: - print(hex(i) + ': ' + hex(binary_data[i]) + ' vs ' + hex(read_data[i])) - - if not configuration['write'] and configuration['read']: - read_data = bootloader.read_memory_data(configuration['address'], configuration['length']) - with open(data_file, 'wb') as out_file: - out_file.write(read_data) + device_uid = self.bootloader.get_uid(family) + device_uid_string = self.bootloader.format_uid(device_uid) + debug(0, "Device UID: %s" % device_uid_string) - if configuration['go_address'] != -1: - bootloader.go(configuration['go_address']) + flash_size = self.bootloader.get_flash_size(family) + debug(0, "Flash size: %d KiB" % flash_size) if __name__ == "__main__": - config = _parse_arguments() - chip = _connect(config) + loader = Stm32Loader() + loader.parse_arguments(sys.argv[1:]) + loader.connect() try: - _perform_commands(chip, config) + loader.read_device_details() + loader.perform_commands() finally: - chip.reset_from_flash() + loader.reset() From b1e9700a86397c5d7062e4908fe64812d22ece6f Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Fri, 6 Apr 2018 22:53:53 +0200 Subject: [PATCH 060/369] Bring changelog up to date --- CHANGELOG.md | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d9beb6..d1f430e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,28 +1,41 @@ -# v0.3.0 +v0.3.0 +======= * Add version number. * Add this changelog. +* Improve documentation. * Support ST BlueNRG devices (configurable parity). * Add Wiznet W7500 / SweetPeas bootloader chip ID. +* Fix ack-related bugs in (un)protect methods. +* Add 'unprotect' command-line option. +* Read device UID. +* Read device flash size. +* Refactor __main__ functionality into methods. -# 2018-05 +2018-05 +======= * Make RTS/DTR (boot0/reset) configurable (polarity, swap). -# 2018-04 +2018-04 +======= * Restore Python 2 compatibility. -# 2018-03 +2018-03 +======= * Add support for Python 3. * Remove Psyco and progressbar support. * Fix checksum calculation bug for paged erase. -# 2014-04 +2014-04 +======= * Add `-g
` (GO command). * Add known chip IDs. * Implement extended erase for STM32 F2/F4. -# 2013-10 +2013-10 +======= * Add Windows compatibility. -# 2009-04 +2009-04 +======= * Add GPL license. From 6cbdebfdd57365e776f1693bbaa15a8c2e0639c7 Mon Sep 17 00:00:00 2001 From: Markus Wegmann Date: Thu, 5 Jul 2018 19:17:11 +0200 Subject: [PATCH 061/369] Add __init__.py to work in project --- __init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 __init__.py diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 From 2feccb057a4c311cdbf5a17018d1d764e7d885c5 Mon Sep 17 00:00:00 2001 From: Atokulus Date: Fri, 27 Jul 2018 13:20:23 +0200 Subject: [PATCH 062/369] Fix inefficient serial write The script took up to 0.3s for one 256 Byte block on legacy single board computers, due to invoking a write per byte, rather than writing the data block in bulk. Verified on STM32L443C6, STM32L433C6 --- stm32loader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stm32loader.py b/stm32loader.py index 2ef864e..5a89aa6 100755 --- a/stm32loader.py +++ b/stm32loader.py @@ -271,7 +271,7 @@ def write_memory(self, address, data): checksum = 0xFF for c in data: checksum = checksum ^ c - self.serial.write(bytearray([c])) + self.serial.write(bytearray([data])) self.serial.write(bytearray([checksum])) self._wait_for_ack("0x31 programming failed") debug(10, " Write memory done") From da6c4039f807ad5bff2f0947ed1345571a73648a Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Thu, 14 Jun 2018 10:59:27 +0200 Subject: [PATCH 063/369] Fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 542e757..28a9aa9 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ Inspiration for features from: Electrically ------------ -The below assumes you care connecting an STM32F10x. +The below assumes you are connecting an STM32F10x. For other chips, the serial pins and/or the BOOT0 / BOOT1 values may differ. From 1b76d81657521c4afc40cceefb206aac02a3bf4e Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 30 Jul 2018 21:39:38 +0200 Subject: [PATCH 064/369] Doc: credit Atokulus --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 28a9aa9..e9c92c4 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,8 @@ Acknowledgement --------------- Original Version by Ivan A-R (tuxotronic.org). -Contributions by Domen Puncer, James Snyder, Floris Lambrechts. +Contributions by Domen Puncer, James Snyder, Floris Lambrechts, +Atokulus. Inspiration for features from: From 4a8cdfc3bd6af12c75bfee46cffcbdefee21df39 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 30 Jul 2018 21:40:04 +0200 Subject: [PATCH 065/369] Fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e9c92c4..0dc5e6c 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ Inspiration for features from: * ST BlueNRG chip support https://github.com/lchish/stm32loader -* Wiznet W7500 chip / SeetPeas custom bootloader support +* Wiznet W7500 chip / SweetPeas custom bootloader support https://github.com/Sweet-Peas/WiznetLoader From 702122b9f1a15205449006dbbe3e5ee5885a63d0 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 30 Jul 2018 21:44:56 +0200 Subject: [PATCH 066/369] Move code into separate stm32loader folder So only python code is part of the folder containing __init__.py. --- __init__.py => stm32loader/__init__.py | 0 stm32loader.py => stm32loader/stm32loader.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename __init__.py => stm32loader/__init__.py (100%) rename stm32loader.py => stm32loader/stm32loader.py (100%) mode change 100755 => 100644 diff --git a/__init__.py b/stm32loader/__init__.py similarity index 100% rename from __init__.py rename to stm32loader/__init__.py diff --git a/stm32loader.py b/stm32loader/stm32loader.py old mode 100755 new mode 100644 similarity index 100% rename from stm32loader.py rename to stm32loader/stm32loader.py From 231f7595406a704862eac5378d438e5c8d2f21f4 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 30 Jul 2018 21:48:14 +0200 Subject: [PATCH 067/369] Add __version__.py and move version definition there This facilitates automating version increments. --- stm32loader/__init__.py | 3 +++ stm32loader/__version__.py | 2 ++ stm32loader/stm32loader.py | 3 --- 3 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 stm32loader/__version__.py diff --git a/stm32loader/__init__.py b/stm32loader/__init__.py index e69de29..87a852e 100644 --- a/stm32loader/__init__.py +++ b/stm32loader/__init__.py @@ -0,0 +1,3 @@ + +from .__version__ import VERSION +__version__ = '.'.join(map(str, VERSION)) diff --git a/stm32loader/__version__.py b/stm32loader/__version__.py new file mode 100644 index 0000000..edb426f --- /dev/null +++ b/stm32loader/__version__.py @@ -0,0 +1,2 @@ + +VERSION = (0, 3, 1) diff --git a/stm32loader/stm32loader.py b/stm32loader/stm32loader.py index 5a89aa6..1b5746e 100644 --- a/stm32loader/stm32loader.py +++ b/stm32loader/stm32loader.py @@ -32,9 +32,6 @@ import time -VERSION = (0, 3, 0) -__version__ = '.'.join(map(str, VERSION)) - VERBOSITY = 5 CHIP_IDS = { From 4d66bb434bb8a1f1ceef57f540244685d31dbac4 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 30 Jul 2018 21:49:19 +0200 Subject: [PATCH 068/369] Import all attributes in top-level module This is the most backwards-compatible thing we can do, and it's fairly light anyway. --- stm32loader/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/stm32loader/__init__.py b/stm32loader/__init__.py index 87a852e..f22b147 100644 --- a/stm32loader/__init__.py +++ b/stm32loader/__init__.py @@ -1,3 +1,5 @@ from .__version__ import VERSION __version__ = '.'.join(map(str, VERSION)) + +from .stm32loader import * From da01abc59b5e64d9a70a3bdfe8e18afcb701e833 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 30 Jul 2018 21:56:44 +0200 Subject: [PATCH 069/369] Rename COPYING to LICENSE (as is more common) --- COPYING3 => LICENSE | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename COPYING3 => LICENSE (100%) diff --git a/COPYING3 b/LICENSE similarity index 100% rename from COPYING3 rename to LICENSE From ac8974d32bf4851fa442aadc9c87bf258b528fa3 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 30 Jul 2018 21:58:49 +0200 Subject: [PATCH 070/369] Add setup.py for building distribution packages Taken from the excellent https://github.com/kennethreitz/setup.py --- MANIFEST.in | 1 + setup.py | 130 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 MANIFEST.in create mode 100644 setup.py diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..7152b80 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include README.md LICENSE \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..775596a --- /dev/null +++ b/setup.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Note: To use the 'upload' functionality of this file, you must: +# $ pip install twine + +import io +import os +import sys +from shutil import rmtree + +from setuptools import find_packages, setup, Command + +# Package meta-data. +NAME = 'mypackage' +DESCRIPTION = 'My short description for my project.' +URL = 'https://github.com/me/myproject' +EMAIL = 'me@example.com' +AUTHOR = 'Awesome Soul' +REQUIRES_PYTHON = '>=3.6.0' +VERSION = None + +# What packages are required for this module to be executed? +REQUIRED = [ + # 'requests', 'maya', 'records', +] + +# What packages are optional? +EXTRAS = { + # 'fancy feature': ['django'], +} + +# The rest you shouldn't have to touch too much :) +# ------------------------------------------------ +# Except, perhaps the License and Trove Classifiers! +# If you do change the License, remember to change the Trove Classifier for that! + +here = os.path.abspath(os.path.dirname(__file__)) + +# Import the README and use it as the long-description. +# Note: this will only work if 'README.md' is present in your MANIFEST.in file! +try: + with io.open(os.path.join(here, 'README.md'), encoding='utf-8') as f: + long_description = '\n' + f.read() +except FileNotFoundError: + long_description = DESCRIPTION + +# Load the package's __version__.py module as a dictionary. +about = {} +if not VERSION: + with open(os.path.join(here, NAME, '__version__.py')) as f: + exec (f.read(), about) +else: + about['__version__'] = VERSION + + +class UploadCommand(Command): + """Support setup.py upload.""" + + description = 'Build and publish the package.' + user_options = [] + + @staticmethod + def status(s): + """Prints things in bold.""" + print('\033[1m{0}\033[0m'.format(s)) + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + try: + self.status('Removing previous builds…') + rmtree(os.path.join(here, 'dist')) + except OSError: + pass + + self.status('Building Source and Wheel (universal) distribution…') + os.system('{0} setup.py sdist bdist_wheel --universal'.format(sys.executable)) + + self.status('Uploading the package to PyPI via Twine…') + os.system('twine upload dist/*') + + self.status('Pushing git tags…') + os.system('git tag v{0}'.format(about['__version__'])) + os.system('git push --tags') + + sys.exit() + + +# Where the magic happens: +setup( + name=NAME, + version=about['__version__'], + description=DESCRIPTION, + long_description=long_description, + long_description_content_type='text/markdown', + author=AUTHOR, + author_email=EMAIL, + python_requires=REQUIRES_PYTHON, + url=URL, + packages=find_packages(exclude=('tests',)), + # If your package is a single module, use this instead of 'packages': + # py_modules=['mypackage'], + + # entry_points={ + # 'console_scripts': ['mycli=mymodule:cli'], + # }, + install_requires=REQUIRED, + extras_require=EXTRAS, + include_package_data=True, + license='MIT', + classifiers=[ + # Trove classifiers + # Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers + 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: Implementation :: CPython', + 'Programming Language :: Python :: Implementation :: PyPy' + ], + # $ setup.py publish support. + cmdclass={ + 'upload': UploadCommand, + }, +) From aeddff953b5902f06c511cd8307d3e5e6bc47f76 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 30 Jul 2018 22:10:45 +0200 Subject: [PATCH 071/369] Customize setup.py for stm32loader --- MANIFEST.in | 2 +- setup.py | 45 +++++++++++++++++++-------------------------- 2 files changed, 20 insertions(+), 27 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 7152b80..e2e577a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1 @@ -include README.md LICENSE \ No newline at end of file +include README.md LICENSE CHANGELOG.md diff --git a/setup.py b/setup.py index 775596a..112e321 100644 --- a/setup.py +++ b/setup.py @@ -12,29 +12,21 @@ from setuptools import find_packages, setup, Command # Package meta-data. -NAME = 'mypackage' -DESCRIPTION = 'My short description for my project.' -URL = 'https://github.com/me/myproject' -EMAIL = 'me@example.com' -AUTHOR = 'Awesome Soul' -REQUIRES_PYTHON = '>=3.6.0' +NAME = 'stm32loader' +DESCRIPTION = 'Flash firmware to STM32 microcontrollers using Python.' +URL = 'https://github.com/florisla/stm32loader' +EMAIL = 'florisla@gmail.com' +AUTHOR = 'Floris Lambrechts' +REQUIRES_PYTHON = '>=2.6.0' VERSION = None -# What packages are required for this module to be executed? REQUIRED = [ - # 'requests', 'maya', 'records', + 'pyserial', ] -# What packages are optional? EXTRAS = { - # 'fancy feature': ['django'], } -# The rest you shouldn't have to touch too much :) -# ------------------------------------------------ -# Except, perhaps the License and Trove Classifiers! -# If you do change the License, remember to change the Trove Classifier for that! - here = os.path.abspath(os.path.dirname(__file__)) # Import the README and use it as the long-description. @@ -50,6 +42,7 @@ if not VERSION: with open(os.path.join(here, NAME, '__version__.py')) as f: exec (f.read(), about) + about['__version__'] = '.'.join(map(str, about['VERSION'])) else: about['__version__'] = VERSION @@ -103,25 +96,25 @@ def run(self): python_requires=REQUIRES_PYTHON, url=URL, packages=find_packages(exclude=('tests',)), - # If your package is a single module, use this instead of 'packages': - # py_modules=['mypackage'], - - # entry_points={ - # 'console_scripts': ['mycli=mymodule:cli'], - # }, install_requires=REQUIRED, extras_require=EXTRAS, include_package_data=True, - license='MIT', + license='GPL3', classifiers=[ - # Trove classifiers - # Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers - 'License :: OSI Approved :: MIT License', + 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: Implementation :: CPython', - 'Programming Language :: Python :: Implementation :: PyPy' + 'Programming Language :: Python :: Implementation :: PyPy', + 'Environment:: Console', + 'Natural Language :: English', + 'Operating System :: OS Independent', ], # $ setup.py publish support. cmdclass={ From 8b6f3fb4992ae3cd2662a2746e3a2e07033e482c Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 30 Jul 2018 22:14:51 +0200 Subject: [PATCH 072/369] Cleanup: ignore build results --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..25aacff --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +build/ +dist/ +*.egg-info/ From a2b233dee4922d42ecfdde42eee3a09d634b5ed4 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 30 Jul 2018 22:15:34 +0200 Subject: [PATCH 073/369] Update changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1f430e..eaa5583 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,9 @@ +v0.3.1 +====== +* Make stm32loader installable and importable as a package. +* Make write_memory faster (by Atokulus, see #1) + v0.3.0 ======= * Add version number. From 0d59758743fc1dee266c6132741f2d0f685dc185 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 31 Jul 2018 09:28:24 +0200 Subject: [PATCH 074/369] Remove Environment::Console classifier - it's not accepted by PyPI --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 112e321..3708312 100644 --- a/setup.py +++ b/setup.py @@ -112,7 +112,6 @@ def run(self): 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', - 'Environment:: Console', 'Natural Language :: English', 'Operating System :: OS Independent', ], From 2ebc2f987403748f5a0bbc9c88b5a689aa93d470 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 31 Jul 2018 09:32:23 +0200 Subject: [PATCH 075/369] Doc: declare compatibility with same Python versions as pyserial --- README.md | 2 +- setup.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0dc5e6c..fea2c12 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ ST Microelectronics STM32 microcontrollers over UART. Also supports ST BlueNRG devices, and the SweetPeas bootloader for Wiznet W7500. -Compatible with Python version 2.6 to 2.7, 3.2 to 3.6. +Compatible with Python version 3.2 to 3.7 or 2.7. Usage diff --git a/setup.py b/setup.py index 3708312..b87f5e5 100644 --- a/setup.py +++ b/setup.py @@ -106,6 +106,8 @@ def run(self): 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.2', + 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', From 639e87b03d7860149fbb7a2808b14aabf2f7b9d2 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 31 Jul 2018 09:43:23 +0200 Subject: [PATCH 076/369] Doc: add project URLs --- setup.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/setup.py b/setup.py index b87f5e5..bf4d139 100644 --- a/setup.py +++ b/setup.py @@ -19,6 +19,10 @@ AUTHOR = 'Floris Lambrechts' REQUIRES_PYTHON = '>=2.6.0' VERSION = None +PROJECT_URLS = { + "Bug Tracker": "https://github.com/florisla/stm32loader/issues", + "Source Code": "https://github.com/florisla/stm32loader", +} REQUIRED = [ 'pyserial', @@ -91,6 +95,7 @@ def run(self): description=DESCRIPTION, long_description=long_description, long_description_content_type='text/markdown', + project_urls=PROJECT_URLS, author=AUTHOR, author_email=EMAIL, python_requires=REQUIRES_PYTHON, From 25678650d4b3cf8623df66e2d4f5c59c81324617 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 31 Jul 2018 16:10:10 +0200 Subject: [PATCH 077/369] Make module executable Allowing 'python -m stm32loader'. Use the same main() method from the main script in __call__.py. --- stm32loader/__main__.py | 22 ++++++++++++++++++++++ stm32loader/stm32loader.py | 6 +++++- 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 stm32loader/__main__.py diff --git a/stm32loader/__main__.py b/stm32loader/__main__.py new file mode 100644 index 0000000..0fce34c --- /dev/null +++ b/stm32loader/__main__.py @@ -0,0 +1,22 @@ + +""" +Execute stm32loader as a module. + +This does exactly the same as manually calling 'python stm32loader.py'. +""" + +from .stm32loader import main as stm32loader_main + + +def main(): + """ + Separate main() method, different from stm32loader.main. + + This way it it can be used as an entry point for a console script. + :return None: + """ + stm32loader_main() + + +if __name__ == "__main__": + main() diff --git a/stm32loader/stm32loader.py b/stm32loader/stm32loader.py index 1b5746e..8765313 100644 --- a/stm32loader/stm32loader.py +++ b/stm32loader/stm32loader.py @@ -645,7 +645,7 @@ def read_device_details(self): debug(0, "Flash size: %d KiB" % flash_size) -if __name__ == "__main__": +def main(): loader = Stm32Loader() loader.parse_arguments(sys.argv[1:]) loader.connect() @@ -654,3 +654,7 @@ def read_device_details(self): loader.perform_commands() finally: loader.reset() + + +if __name__ == "__main__": + main() From 8f89d683c52af75dd066029387aa5e8fb8b99651 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 31 Jul 2018 16:10:36 +0200 Subject: [PATCH 078/369] Add a console script ('stm32loader') Advantage: this is available system-wide. On Windows, this also becomes an .exe file. --- setup.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup.py b/setup.py index bf4d139..e1bed03 100644 --- a/setup.py +++ b/setup.py @@ -101,6 +101,9 @@ def run(self): python_requires=REQUIRES_PYTHON, url=URL, packages=find_packages(exclude=('tests',)), + entry_points={ + 'console_scripts': ['stm32loader=stm32loader.__main__:main'], + }, install_requires=REQUIRED, extras_require=EXTRAS, include_package_data=True, From d2f4febe2f6beaea5575d6cc041a7bb600500b66 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 31 Jul 2018 16:47:47 +0200 Subject: [PATCH 079/369] Don't advertise support for Python 2.6 (it may work, but I don't test for it) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e1bed03..37f1210 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ URL = 'https://github.com/florisla/stm32loader' EMAIL = 'florisla@gmail.com' AUTHOR = 'Floris Lambrechts' -REQUIRES_PYTHON = '>=2.6.0' +REQUIRES_PYTHON = '>=2.7.0' VERSION = None PROJECT_URLS = { "Bug Tracker": "https://github.com/florisla/stm32loader/issues", From 66cbd9351ed6daa8029b90f6947147ef2a873d6e Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 31 Jul 2018 16:45:48 +0200 Subject: [PATCH 080/369] Include build-time dependencies in setup.py --- setup.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 37f1210..87135d7 100644 --- a/setup.py +++ b/setup.py @@ -28,8 +28,9 @@ 'pyserial', ] -EXTRAS = { -} +EXTRAS = dict( + dev=['setuptools', 'wheel', 'twine'], +) here = os.path.abspath(os.path.dirname(__file__)) From 995003ef79bbf8dfb8ada671d434b7599fe92bd7 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 31 Jul 2018 17:03:22 +0200 Subject: [PATCH 081/369] Include LICENSE file in the wheel package Now that we have a setup.cfg, also ensure that the wheel is always built as 'universal' package. --- setup.cfg | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 setup.cfg diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..ed8a958 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,5 @@ +[bdist_wheel] +universal = 1 + +[metadata] +license_file = LICENSE From f5a0506d416f80e7e3d2021f97946a0f43c87253 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 31 Jul 2018 16:15:12 +0200 Subject: [PATCH 082/369] Increment version number to 0.3.2 --- stm32loader/__version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stm32loader/__version__.py b/stm32loader/__version__.py index edb426f..67451df 100644 --- a/stm32loader/__version__.py +++ b/stm32loader/__version__.py @@ -1,2 +1,2 @@ -VERSION = (0, 3, 1) +VERSION = (0, 3, 2) From 5e4fda1f93579634ef8bbc585790832f54417546 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 31 Jul 2018 22:53:02 +0200 Subject: [PATCH 083/369] Describe changes in 0.3.2 --- CHANGELOG.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eaa5583..679c8f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,14 @@ +v0.3.2 +====== +* Publish on Python Package Index. +* Make stm32loader executable as a module. +* Expose stm32loader as a console script (stm32loader.exe on Windows). + v0.3.1 ====== * Make stm32loader installable and importable as a package. -* Make write_memory faster (by Atokulus, see #1) +* Make write_memory faster (by Atokulus, see #1). v0.3.0 ======= From d85b777a56c58b2ae1e3258616ab10cbd64aa0d9 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 1 Aug 2018 11:13:00 +0200 Subject: [PATCH 084/369] Automate version updates with bump2version --- .bumpversion.cfg | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .bumpversion.cfg diff --git a/.bumpversion.cfg b/.bumpversion.cfg new file mode 100644 index 0000000..c530510 --- /dev/null +++ b/.bumpversion.cfg @@ -0,0 +1,10 @@ +[bumpversion] +current_version = 0.3.2 +commit = True +tag = True +message = Release: bump version number from v{current_version} to v{new_version} + +[bumpversion:file:stm32loader/__version__.py] +serialize = ({major}, {minor}, {patch}) +parse = \((?P\d+),\s(?P\d+),\s(?P\d+)\) + From 455d379e79632204afc1b00b1c4bc996911c3a5d Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 8 Aug 2018 15:20:13 +0200 Subject: [PATCH 085/369] Bugfix by Atokulus: write data, not [data] Fixes #7 Fixes #6 --- stm32loader/stm32loader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stm32loader/stm32loader.py b/stm32loader/stm32loader.py index 8765313..ac16216 100644 --- a/stm32loader/stm32loader.py +++ b/stm32loader/stm32loader.py @@ -268,7 +268,7 @@ def write_memory(self, address, data): checksum = 0xFF for c in data: checksum = checksum ^ c - self.serial.write(bytearray([data])) + self.serial.write(bytearray(data)) self.serial.write(bytearray([checksum])) self._wait_for_ack("0x31 programming failed") debug(10, " Write memory done") From adb7b5830fc7f7650242564eb82d6ff5ad5c148c Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 8 Aug 2018 15:21:46 +0200 Subject: [PATCH 086/369] Release: bump version number from v0.3.2 to v0.3.3 --- .bumpversion.cfg | 2 +- stm32loader/__version__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index c530510..42336c9 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.3.2 +current_version = 0.3.3 commit = True tag = True message = Release: bump version number from v{current_version} to v{new_version} diff --git a/stm32loader/__version__.py b/stm32loader/__version__.py index 67451df..fce22f5 100644 --- a/stm32loader/__version__.py +++ b/stm32loader/__version__.py @@ -1,2 +1,2 @@ -VERSION = (0, 3, 2) +VERSION = (0, 3, 3) From 66296b0cb14e96356491f1368051cbd2edc4df7a Mon Sep 17 00:00:00 2001 From: Sam Bristow Date: Tue, 9 Oct 2018 19:52:47 +1300 Subject: [PATCH 087/369] Add support for STM32F7 processors --- .gitignore | 3 +++ stm32loader/stm32loader.py | 8 +++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 25aacff..849fcfe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ build/ dist/ *.egg-info/ + +__pycache__/ +*.py[cod] diff --git a/stm32loader/stm32loader.py b/stm32loader/stm32loader.py index ac16216..2e8cfde 100644 --- a/stm32loader/stm32loader.py +++ b/stm32loader/stm32loader.py @@ -51,6 +51,8 @@ 0x411: "STM32F2xxx", 0x413: "STM32F40xxx/41xxx", 0x419: "STM3242xxx/43xxx", + 0x449: "STM32F74xxx/75xxx", + 0x451: "STM32F76xxx/77xxx", # see ST AN4872 # requires parity None @@ -112,15 +114,19 @@ class Reply: # ST RM0090 section 39.1 Unique device ID register # F405/415, F407/417, F427/437, F429/439 'F4': 0x1FFFF7A10, + # ST RM0385 section 41.2 Unique device ID register + 'F7': 0x1FF0F420, } FLASH_SIZE_ADDRESS = { # ST RM0008 section 30.2 Memory size registers # F101, F102, F103, F105, F107 'F1': 0x1FFFF7E0, - # ST RM0090 section 39.2 Unique device ID register + # ST RM0090 section 39.2 Flash size # F405/415, F407/417, F427/437, F429/439 'F4': 0x1FFF7A22, + # ST RM0385 section 41.2 Flash size + 'F7': 0x1FF0F442, } extended_erase = False From a05d24a8d6af00e779c8fb3c847cd4b84dc7ed05 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 9 Oct 2018 13:58:35 +0200 Subject: [PATCH 088/369] Document the latest changes --- CHANGELOG.md | 8 ++++++++ README.md | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 679c8f0..9f72e7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,12 @@ +vnext +===== +* Add support for STM32F7 mcus. By sam-bristow. + +v0.3.3 +====== +* Bugfix: write data, not [data]. By Atokulus. + v0.3.2 ====== * Publish on Python Package Index. diff --git a/README.md b/README.md index fea2c12..293f366 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ Acknowledgement Original Version by Ivan A-R (tuxotronic.org). Contributions by Domen Puncer, James Snyder, Floris Lambrechts, -Atokulus. +Atokulus, sam-bristow. Inspiration for features from: From 1ec166ce22675f263a26df6a5faf1406f5b35ec5 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 9 Oct 2018 14:00:05 +0200 Subject: [PATCH 089/369] Refer to specific AN table with chip IDs --- stm32loader/stm32loader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stm32loader/stm32loader.py b/stm32loader/stm32loader.py index 2e8cfde..dc8b4ab 100644 --- a/stm32loader/stm32loader.py +++ b/stm32loader/stm32loader.py @@ -35,7 +35,7 @@ VERBOSITY = 5 CHIP_IDS = { - # see ST AN2606 + # see ST AN2606 Table 116 Bootloader device-dependent parameters # 16 to 32 KiB 0x412: "STM32F10x Low-density", # 64 to 128 KiB From a436b9e47f86a626f2a3b4693380f45571e31f15 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 9 Apr 2019 09:35:44 +0200 Subject: [PATCH 090/369] Support write with byte count smaller than 256 too As suggested by NINI1988 in #9. Data is padded with 0xFF to a multiple of 4 bytes. --- stm32loader/stm32loader.py | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/stm32loader/stm32loader.py b/stm32loader/stm32loader.py index dc8b4ab..c8e67a5 100644 --- a/stm32loader/stm32loader.py +++ b/stm32loader/stm32loader.py @@ -261,17 +261,27 @@ def go(self, address): self._wait_for_ack("0x21 go failed") def write_memory(self, address, data): - assert(len(data) <= 256) + nr_of_bytes = len(data) + assert nr_of_bytes <= 256 + if not self.command(self.Command.WRITE_MEMORY): raise CommandException("Write memory (0x31) failed") debug(10, "*** Write memory command") self.serial.write(self._encode_address(address)) self._wait_for_ack("0x31 address failed") - nr_of_bytes = (len(data) - 1) & 0xFF - debug(10, " %s bytes to write" % [nr_of_bytes + 1]) - self.serial.write(bytearray([nr_of_bytes])) - checksum = 0xFF + + # pad data length to multiple of 4 bytes + if nr_of_bytes % 4 != 0: + padding_bytes = 4 - (nr_of_bytes % 4) + nr_of_bytes += padding_bytes + # append value 0xFF: flash memory value after erase + data = bytearray(data) + data.extend([0xFF] * padding_bytes) + + debug(10, " %s bytes to write" % [nr_of_bytes]) + self.serial.write(bytearray([nr_of_bytes - 1])) + checksum = nr_of_bytes - 1 for c in data: checksum = checksum ^ c self.serial.write(bytearray(data)) @@ -361,9 +371,9 @@ def read_memory_data(self, address, length): data = data + self.read_memory(address, 256) address = address + 256 length = length - 256 - else: - debug(5, "Read %(len)d bytes at 0x%(address)X" % {'address': address, 'len': 256}) - data = data + self.read_memory(address, length) + if length: + debug(5, "Read %(len)d bytes at 0x%(address)X" % {'address': address, 'len': length}) + data = data + self.read_memory(address, length) return data def write_memory_data(self, address, data): @@ -375,9 +385,9 @@ def write_memory_data(self, address, data): offset += 256 address += 256 length -= 256 - else: - debug(5, "Write %(len)d bytes at 0x%(address)X" % {'address': address, 'len': 256}) - self.write_memory(address, data[offset:offset + length] + (b'\xff' * (256 - length))) + if length: + debug(5, "Write %(len)d bytes at 0x%(address)X" % {'address': address, 'len': length}) + self.write_memory(address, data[offset:offset + length]) def _global_erase(self): # global erase: n=255, see ST AN3155 From 09d26aa5c03c2c281050541c9f174f3673566da6 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Apr 2019 22:15:43 +0200 Subject: [PATCH 091/369] Start linting and formatting with pylint, flake8 and black --- .pylintrc | 16 ++++++++++++++++ pyproject.toml | 13 +++++++++++++ setup.cfg | 21 +++++++++++++++++++++ setup.py | 6 +++--- 4 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 .pylintrc create mode 100644 pyproject.toml diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..6db7898 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,16 @@ +[FORMAT] +max-line-length=98 + +[MESSAGES CONTROL] +disable= + fixme, # TO DOs are not errors + bad-continuation, # be compatible to black + +[REPORT] +score=no + +[BASIC] +good-names= + i, + e, + namespace, diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..2eac387 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,13 @@ + +[tool.black] +line-length = 98 +exclude = ''' +/( + \.git + | \.idea + | __pycache__ + | build + | dist + | .*\.egg-info +)/ +''' diff --git a/setup.cfg b/setup.cfg index ed8a958..0d874e4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,3 +3,24 @@ universal = 1 [metadata] license_file = LICENSE + +[isort] +line_length = 98 +line_width = 98 +multi_line_output = 2 + +[flake8] +max-line-length = 98 +max-doc-length = 78 +exclude = + .git, + .idea, + __pycache__, + build, + dist, + *.egg-info +ignore = + # be compatible to black + C812, # Missing trailing comma + E203, # Whitespace before ':' + diff --git a/setup.py b/setup.py index 87135d7..e6a7a6b 100644 --- a/setup.py +++ b/setup.py @@ -28,9 +28,9 @@ 'pyserial', ] -EXTRAS = dict( - dev=['setuptools', 'wheel', 'twine'], -) +EXTRAS = { + "dev": ['setuptools', 'wheel', 'twine', 'pylint', 'flake8', 'flake8-isort', 'black'], +} here = os.path.abspath(os.path.dirname(__file__)) From 4de4c797aaa62d72901f0f7b19cfbbc09ffa2df9 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Apr 2019 22:15:45 +0200 Subject: [PATCH 092/369] Cleanup: satisfy pylint, flake8 and blacken the code --- stm32loader/__init__.py | 7 +- stm32loader/__main__.py | 3 +- stm32loader/__version__.py | 1 - stm32loader/stm32loader.py | 385 +++++++++++++++++++++---------------- 4 files changed, 223 insertions(+), 173 deletions(-) diff --git a/stm32loader/__init__.py b/stm32loader/__init__.py index f22b147..ae0d495 100644 --- a/stm32loader/__init__.py +++ b/stm32loader/__init__.py @@ -1,5 +1,8 @@ +"""Flash firmware to STM32 microcontrollers over an RS-232 serial connection.""" from .__version__ import VERSION -__version__ = '.'.join(map(str, VERSION)) +__version__ = ".".join(map(str, VERSION)) -from .stm32loader import * +from .stm32loader import Stm32Loader, Stm32Bootloader, main + +__all__ = ["__version_info__", "__version__", "Stm32Loader", "Stm32Bootloader", "main"] diff --git a/stm32loader/__main__.py b/stm32loader/__main__.py index 0fce34c..6966c31 100644 --- a/stm32loader/__main__.py +++ b/stm32loader/__main__.py @@ -1,4 +1,3 @@ - """ Execute stm32loader as a module. @@ -10,7 +9,7 @@ def main(): """ - Separate main() method, different from stm32loader.main. + Separate main method, different from stm32loader.main. This way it it can be used as an entry point for a console script. :return None: diff --git a/stm32loader/__version__.py b/stm32loader/__version__.py index fce22f5..2300c84 100644 --- a/stm32loader/__version__.py +++ b/stm32loader/__version__.py @@ -1,2 +1 @@ - VERSION = (0, 3, 3) diff --git a/stm32loader/stm32loader.py b/stm32loader/stm32loader.py index c8e67a5..16a88a3 100644 --- a/stm32loader/stm32loader.py +++ b/stm32loader/stm32loader.py @@ -1,5 +1,4 @@ #!/usr/bin/env python - # -*- coding: utf-8 -*- # vim: sw=4:ts=4:si:et:enc=utf-8 @@ -22,17 +21,19 @@ # along with stm32loader; see the file COPYING3. If not see # . +"""Flash firmware to STM32 microcontrollers over an RS-232 serial connection.""" + from __future__ import print_function -from functools import reduce -import sys import getopt -import serial +import sys import time +from functools import reduce +import serial -VERBOSITY = 5 +DEFAULT_VERBOSITY = 5 CHIP_IDS = { # see ST AN2606 Table 116 Bootloader device-dependent parameters @@ -53,31 +54,33 @@ 0x419: "STM3242xxx/43xxx", 0x449: "STM32F74xxx/75xxx", 0x451: "STM32F76xxx/77xxx", - # see ST AN4872 # requires parity None 0x11103: "BlueNRG", - # other - # Cortex-M0 MCU with hardware TCP/IP and MAC # (SweetPeas custom bootloader) 0x801: "Wiznet W7500", } -def debug(level, message): - if VERBOSITY >= level: - print(message, file=sys.stderr) - - class CommandException(Exception): + """Error: a command in the STM32 native bootloader failed.""" + pass class Stm32Bootloader: + """Talk to the STM32 native bootloader.""" + + # pylint: disable=too-many-public-methods class Command: + """STM32 native bootloader command values.""" + + # pylint: disable=too-few-public-methods + # FIXME turn into intenum + # See ST AN3155, AN4872 GET = 0x00 GET_VERSION = 0x01 @@ -98,46 +101,62 @@ class Command: SYNCHRONIZE = 0x7F class Reply: + """STM32 native bootloader reply status codes.""" + + # pylint: disable=too-few-public-methods + # FIXME turn into intenum + # See ST AN3155, AN4872 ACK = 0x79 NACK = 0x1F - PARITY = dict( - even=serial.PARITY_EVEN, - none=serial.PARITY_NONE, - ) + PARITY = {"even": serial.PARITY_EVEN, "none": serial.PARITY_NONE} UID_ADDRESS = { # ST RM0008 section 30.1 Unique device ID register # F101, F102, F103, F105, F107 - 'F1': 0x1FFFF7E8, + "F1": 0x1FFFF7E8, # ST RM0090 section 39.1 Unique device ID register # F405/415, F407/417, F427/437, F429/439 - 'F4': 0x1FFFF7A10, + "F4": 0x1FFFF7A10, # ST RM0385 section 41.2 Unique device ID register - 'F7': 0x1FF0F420, + "F7": 0x1FF0F420, } + UID_SWAP = [[1, 0], [3, 2], [7, 6, 5, 4], [11, 10, 9, 8]] + FLASH_SIZE_ADDRESS = { # ST RM0008 section 30.2 Memory size registers # F101, F102, F103, F105, F107 - 'F1': 0x1FFFF7E0, + "F1": 0x1FFFF7E0, # ST RM0090 section 39.2 Flash size # F405/415, F407/417, F427/437, F429/439 - 'F4': 0x1FFF7A22, + "F4": 0x1FFF7A22, # ST RM0385 section 41.2 Flash size - 'F7': 0x1FF0F442, + "F7": 0x1FF0F442, } extended_erase = False - def __init__(self, swap_rts_dtr=False, reset_active_high=False, boot0_active_high=False): + def __init__( + self, + swap_rts_dtr=False, + reset_active_high=False, + boot0_active_high=False, + verbosity=DEFAULT_VERBOSITY, + ): + """ + Construct the Stm32Bootloader object. + + Call setup_connection() before doing any real work. + """ self.serial = None - self._swap_RTS_DTR = swap_rts_dtr + self._swap_rts_dtr = swap_rts_dtr self._reset_active_high = reset_active_high self._boot0_active_high = boot0_active_high + self.verbosity = verbosity - def open(self, serial_port, baud_rate=115200, parity=serial.PARITY_EVEN): + def setup_connection(self, serial_port, baud_rate=115_200, parity=serial.PARITY_EVEN): try: self.serial = serial.Serial( port=serial_port, @@ -165,6 +184,10 @@ def open(self, serial_port, baud_rate=115200, parity=serial.PARITY_EVEN): ) exit(1) + def debug(self, level, message): + if self.verbosity >= level: + print(message, file=sys.stderr) + def reset_from_system_memory(self): self._enable_boot0(True) self._reset() @@ -187,14 +210,14 @@ def command(self, command): def get(self): if not self.command(self.Command.GET): raise CommandException("Get (0x00) failed") - debug(10, "*** Get command") + self.debug(10, "*** Get command") length = bytearray(self.serial.read())[0] version = bytearray(self.serial.read())[0] - debug(10, " Bootloader version: " + hex(version)) + self.debug(10, " Bootloader version: " + hex(version)) data = bytearray(self.serial.read(length)) if self.Command.EXTENDED_ERASE in data: self.extended_erase = True - debug(10, " Available commands: " + ", ".join(hex(b) for b in data)) + self.debug(10, " Available commands: " + ", ".join(hex(b) for b in data)) self._wait_for_ack("0x00 end") return version @@ -202,18 +225,18 @@ def get_version(self): if not self.command(self.Command.GET_VERSION): raise CommandException("GetVersion (0x01) failed") - debug(10, "*** GetVersion command") + self.debug(10, "*** GetVersion command") version = bytearray(self.serial.read())[0] self.serial.read(2) self._wait_for_ack("0x01 end") - debug(10, " Bootloader version: " + hex(version)) + self.debug(10, " Bootloader version: " + hex(version)) return version def get_id(self): if not self.command(self.Command.GET_ID): raise CommandException("GetID (0x02) failed") - debug(10, "*** GetID command") + self.debug(10, "*** GetID command") length = bytearray(self.serial.read())[0] id_data = bytearray(self.serial.read(length + 1)) self._wait_for_ack("0x02 end") @@ -233,17 +256,16 @@ def get_uid(self, device_id): @staticmethod def format_uid(uid): - UID_SWAP = [[1, 0], [3, 2], [7, 6, 5, 4], [11, 10, 9, 8]] - swapped_data = [[uid[b] for b in part] for part in UID_SWAP] - uid_string = '-'.join(''.join(format(b, '02X') for b in part) for part in swapped_data) + swapped_data = [[uid[b] for b in part] for part in Stm32Bootloader.UID_SWAP] + uid_string = "-".join("".join(format(b, "02X") for b in part) for part in swapped_data) return uid_string def read_memory(self, address, length): - assert(length <= 256) + assert length <= 256 if not self.command(self.Command.READ_MEMORY): raise CommandException("ReadMemory (0x11) failed") - debug(10, "*** ReadMemory command") + self.debug(10, "*** ReadMemory command") self.serial.write(self._encode_address(address)) self._wait_for_ack("0x11 address failed") nr_of_bytes = (length - 1) & 0xFF @@ -253,10 +275,11 @@ def read_memory(self, address, length): return bytearray(self.serial.read(length)) def go(self, address): + # pylint: disable=invalid-name if not self.command(self.Command.GO): raise CommandException("Go (0x21) failed") - debug(10, "*** Go command") + self.debug(10, "*** Go command") self.serial.write(self._encode_address(address)) self._wait_for_ack("0x21 go failed") @@ -267,7 +290,7 @@ def write_memory(self, address, data): if not self.command(self.Command.WRITE_MEMORY): raise CommandException("Write memory (0x31) failed") - debug(10, "*** Write memory command") + self.debug(10, "*** Write memory command") self.serial.write(self._encode_address(address)) self._wait_for_ack("0x31 address failed") @@ -279,100 +302,105 @@ def write_memory(self, address, data): data = bytearray(data) data.extend([0xFF] * padding_bytes) - debug(10, " %s bytes to write" % [nr_of_bytes]) + self.debug(10, " %s bytes to write" % [nr_of_bytes]) self.serial.write(bytearray([nr_of_bytes - 1])) checksum = nr_of_bytes - 1 - for c in data: - checksum = checksum ^ c + for data_byte in data: + checksum = checksum ^ data_byte self.serial.write(bytearray(data)) self.serial.write(bytearray([checksum])) self._wait_for_ack("0x31 programming failed") - debug(10, " Write memory done") + self.debug(10, " Write memory done") def erase_memory(self, sectors=None): if self.extended_erase: - return self.extended_erase_memory() + self.extended_erase_memory() + return if not self.command(self.Command.ERASE): raise CommandException("Erase memory (0x43) failed") - debug(10, "*** Erase memory command") + self.debug(10, "*** Erase memory command") if sectors: self._page_erase(sectors) else: self._global_erase() self._wait_for_ack("0x43 erase failed") - debug(10, " Erase memory done") + self.debug(10, " Erase memory done") def extended_erase_memory(self): if not self.command(self.Command.EXTENDED_ERASE): raise CommandException("Extended Erase memory (0x44) failed") - debug(10, "*** Extended Erase memory command") + self.debug(10, "*** Extended Erase memory command") # Global mass erase and checksum byte - self.serial.write(b'\xFF') - self.serial.write(b'\xFF') - self.serial.write(b'\x00') + self.serial.write(b"\xFF") + self.serial.write(b"\xFF") + self.serial.write(b"\x00") previous_timeout_value = self.serial.timeout self.serial.timeout = 30 print("Extended erase (0x44), this can take ten seconds or more") self._wait_for_ack("0x44 erasing failed") self.serial.timeout = previous_timeout_value - debug(10, " Extended Erase memory done") + self.debug(10, " Extended Erase memory done") def write_protect(self, pages): if not self.command(self.Command.WRITE_PROTECT): raise CommandException("Write Protect memory (0x63) failed") - debug(10, "*** Write protect command") + self.debug(10, "*** Write protect command") nr_of_pages = (len(pages) - 1) & 0xFF self.serial.write(bytearray([nr_of_pages])) checksum = 0xFF - for c in pages: - checksum = checksum ^ c - self.serial.write(bytearray([c])) + for page_index in pages: + checksum = checksum ^ page_index + self.serial.write(bytearray([page_index])) self.serial.write(bytearray([checksum])) self._wait_for_ack("0x63 write protect failed") - debug(10, " Write protect done") + self.debug(10, " Write protect done") def write_unprotect(self): if not self.command(self.Command.WRITE_UNPROTECT): raise CommandException("Write Unprotect (0x73) failed") - debug(10, "*** Write Unprotect command") + self.debug(10, "*** Write Unprotect command") self._wait_for_ack("0x73 write unprotect failed") - debug(10, " Write Unprotect done") + self.debug(10, " Write Unprotect done") def readout_protect(self): if not self.command(self.Command.READOUT_PROTECT): raise CommandException("Readout protect (0x82) failed") - debug(10, "*** Readout protect command") + self.debug(10, "*** Readout protect command") self._wait_for_ack("0x82 readout protect failed") - debug(10, " Read protect done") + self.debug(10, " Read protect done") def readout_unprotect(self): if not self.command(self.Command.READOUT_UNPROTECT): raise CommandException("Readout unprotect (0x92) failed") - debug(10, "*** Readout Unprotect command") + self.debug(10, "*** Readout Unprotect command") self._wait_for_ack("0x92 readout unprotect failed") - debug(20, " Mass erase -- this may take a while") + self.debug(20, " Mass erase -- this may take a while") time.sleep(20) - debug(20, " Unprotect / mass erase done") - debug(20, " Reset after automatic chip reset due to readout unprotect") + self.debug(20, " Unprotect / mass erase done") + self.debug(20, " Reset after automatic chip reset due to readout unprotect") self.reset_from_system_memory() def read_memory_data(self, address, length): data = bytearray() while length > 256: - debug(5, "Read %(len)d bytes at 0x%(address)X" % {'address': address, 'len': 256}) + self.debug( + 5, "Read %(len)d bytes at 0x%(address)X" % {"address": address, "len": 256} + ) data = data + self.read_memory(address, 256) address = address + 256 length = length - 256 if length: - debug(5, "Read %(len)d bytes at 0x%(address)X" % {'address': address, 'len': length}) + self.debug( + 5, "Read %(len)d bytes at 0x%(address)X" % {"address": address, "len": length} + ) data = data + self.read_memory(address, length) return data @@ -380,19 +408,23 @@ def write_memory_data(self, address, data): length = len(data) offset = 0 while length > 256: - debug(5, "Write %(len)d bytes at 0x%(address)X" % {'address': address, 'len': 256}) - self.write_memory(address, data[offset:offset + 256]) + self.debug( + 5, "Write %(len)d bytes at 0x%(address)X" % {"address": address, "len": 256} + ) + self.write_memory(address, data[offset : offset + 256]) offset += 256 address += 256 length -= 256 if length: - debug(5, "Write %(len)d bytes at 0x%(address)X" % {'address': address, 'len': length}) - self.write_memory(address, data[offset:offset + length]) + self.debug( + 5, "Write %(len)d bytes at 0x%(address)X" % {"address": address, "len": length} + ) + self.write_memory(address, data[offset : offset + length]) def _global_erase(self): # global erase: n=255, see ST AN3155 - self.serial.write(b'\xff') - self.serial.write(b'\x00') + self.serial.write(b"\xff") + self.serial.write(b"\x00") def _page_erase(self, pages): # page erase, see ST AN3155 @@ -421,7 +453,7 @@ def _enable_reset(self, enable=True): if self._reset_active_high: level = 1 - level - if self._swap_RTS_DTR: + if self._swap_rts_dtr: self.serial.setRTS(level) else: self.serial.setDTR(level) @@ -434,7 +466,7 @@ def _enable_boot0(self, enable=True): # enabled by argument -B (boot0 active high) level = 1 - level - if self._swap_RTS_DTR: + if self._swap_rts_dtr: self.serial.setDTR(level) else: self.serial.setRTS(level) @@ -464,29 +496,36 @@ def _encode_address(address): class Stm32Loader: + """Main application: parse arguments and handle commands.""" def __init__(self): + """Construct Stm32Loader object with default settings.""" self.bootloader = None self.configuration = { - 'port': '/dev/tty.usbserial-ftCYPMYJ', - 'baud': 115200, - 'parity': serial.PARITY_EVEN, - 'family': None, - 'address': 0x08000000, - 'erase': False, - 'unprotect': False, - 'write': False, - 'verify': False, - 'read': False, - 'go_address': -1, - 'swap_rts_dtr': False, - 'reset_active_high': False, - 'boot0_active_high': False, - 'data_file': None, + "port": "/dev/tty.usbserial-ftCYPMYJ", + "baud": 115_200, + "parity": serial.PARITY_EVEN, + "family": None, + "address": 0x08000000, + "erase": False, + "unprotect": False, + "write": False, + "verify": False, + "read": False, + "go_address": -1, + "swap_rts_dtr": False, + "reset_active_high": False, + "boot0_active_high": False, + "data_file": None, } + self.verbosity = DEFAULT_VERBOSITY + + def debug(self, level, message): + if self.verbosity >= level: + print(message, file=sys.stderr) def parse_arguments(self, arguments): - global VERBOSITY + # pylint: disable=too-many-branches, eval-used try: # parse command-line arguments using getopt @@ -500,123 +539,132 @@ def parse_arguments(self, arguments): # if there's a non-named argument left, that's a file name if arguments: - self.configuration['data_file'] = arguments[0] + self.configuration["data_file"] = arguments[0] for option, value in options: - if option == '-V': - VERBOSITY = 10 - elif option == '-q': - VERBOSITY = 0 - elif option == '-h': + if option == "-V": + self.verbosity = 10 + elif option == "-q": + self.verbosity = 0 + elif option == "-h": self.print_usage() sys.exit(0) - elif option == '-e': - self.configuration['erase'] = True - elif option == '-u': - self.configuration['unprotect'] = True - elif option == '-w': - self.configuration['write'] = True - elif option == '-v': - self.configuration['verify'] = True - elif option == '-r': - self.configuration['read'] = True - elif option == '-p': - self.configuration['port'] = value - elif option == '-s': - self.configuration['swap_rts_dtr'] = True - elif option == '-R': - self.configuration['reset_active_high'] = True - elif option == '-B': - self.configuration['boot0_active_high'] = True - elif option == '-b': - self.configuration['baud'] = eval(value) - elif option == '-f': - self.configuration['family'] = value - elif option == '-P': - assert value.lower() in Stm32Bootloader.PARITY, "Parity value not recognized: '{0}'.".format(value) - self.configuration['parity'] = Stm32Bootloader.PARITY[value.lower()] - elif option == '-a': - self.configuration['address'] = eval(value) - elif option == '-g': - self.configuration['go_address'] = eval(value) - elif option == '-l': - self.configuration['length'] = eval(value) + elif option == "-e": + self.configuration["erase"] = True + elif option == "-u": + self.configuration["unprotect"] = True + elif option == "-w": + self.configuration["write"] = True + elif option == "-v": + self.configuration["verify"] = True + elif option == "-r": + self.configuration["read"] = True + elif option == "-p": + self.configuration["port"] = value + elif option == "-s": + self.configuration["swap_rts_dtr"] = True + elif option == "-R": + self.configuration["reset_active_high"] = True + elif option == "-B": + self.configuration["boot0_active_high"] = True + elif option == "-b": + self.configuration["baud"] = int(eval(value)) + elif option == "-f": + self.configuration["family"] = value + elif option == "-P": + assert ( + value.lower() in Stm32Bootloader.PARITY + ), "Parity value not recognized: '{0}'.".format(value) + self.configuration["parity"] = Stm32Bootloader.PARITY[value.lower()] + elif option == "-a": + self.configuration["address"] = int(eval(value)) + elif option == "-g": + self.configuration["go_address"] = int(eval(value)) + elif option == "-l": + self.configuration["length"] = int(eval(value)) else: assert False, "unhandled option %s" % option def connect(self): self.bootloader = Stm32Bootloader( - swap_rts_dtr=self.configuration['swap_rts_dtr'], - reset_active_high=self.configuration['reset_active_high'], - boot0_active_high=self.configuration['boot0_active_high'], + swap_rts_dtr=self.configuration["swap_rts_dtr"], + reset_active_high=self.configuration["reset_active_high"], + boot0_active_high=self.configuration["boot0_active_high"], + verbosity=self.verbosity, + ) + self.bootloader.setup_connection( + self.configuration["port"], self.configuration["baud"], self.configuration["parity"] ) - self.bootloader.open( - self.configuration['port'], - self.configuration['baud'], - self.configuration['parity'], + self.debug( + 10, + "Open port %(port)s, baud %(baud)d" + % {"port": self.configuration["port"], "baud": self.configuration["baud"]}, ) - debug(10, "Open port %(port)s, baud %(baud)d" % { - 'port': self.configuration['port'], - 'baud': self.configuration['baud'] - }) try: self.bootloader.reset_from_system_memory() - except Exception: + except BaseException: print("Can't init. Ensure that BOOT0 is enabled and reset device") self.bootloader.reset_from_flash() sys.exit(1) def perform_commands(self): + # pylint: disable=too-many-branches binary_data = None - if self.configuration['write'] or self.configuration['verify']: - with open(self.configuration['data_file'], 'rb') as read_file: + if self.configuration["write"] or self.configuration["verify"]: + with open(self.configuration["data_file"], "rb") as read_file: binary_data = bytearray(read_file.read()) - if self.configuration['unprotect']: + if self.configuration["unprotect"]: try: self.bootloader.readout_unprotect() except CommandException: # may be caused by readout protection - debug(0, "Erase failed -- probably due to readout protection") - debug(0, "Quit") + self.debug(0, "Erase failed -- probably due to readout protection") + self.debug(0, "Quit") self.bootloader.reset_from_flash() sys.exit(1) - if self.configuration['erase']: + if self.configuration["erase"]: try: self.bootloader.erase_memory() except CommandException: # may be caused by readout protection - debug( + self.debug( 0, "Erase failed -- probably due to readout protection\n" - "consider using the -u (unprotect) option." + "consider using the -u (unprotect) option.", ) self.bootloader.reset_from_flash() sys.exit(1) - if self.configuration['write']: - self.bootloader.write_memory_data(self.configuration['address'], binary_data) - if self.configuration['verify']: - read_data = self.bootloader.read_memory_data(self.configuration['address'], len(binary_data)) + if self.configuration["write"]: + self.bootloader.write_memory_data(self.configuration["address"], binary_data) + if self.configuration["verify"]: + read_data = self.bootloader.read_memory_data( + self.configuration["address"], len(binary_data) + ) if binary_data == read_data: print("Verification OK") else: print("Verification FAILED") - print(str(len(binary_data)) + ' vs ' + str(len(read_data))) - for i in range(0, len(binary_data)): - if binary_data[i] != read_data[i]: - print(hex(i) + ': ' + hex(binary_data[i]) + ' vs ' + hex(read_data[i])) - if not self.configuration['write'] and self.configuration['read']: - read_data = self.bootloader.read_memory_data(self.configuration['address'], self.configuration['length']) - with open(self.configuration['data_file'], 'wb') as out_file: + print(str(len(binary_data)) + " vs " + str(len(read_data))) + for address, data_pair in enumerate(zip(binary_data, read_data)): + binary_byte, read_byte = data_pair + if binary_byte != read_byte: + print(hex(address) + ": " + hex(binary_byte) + " vs " + hex(read_byte)) + if not self.configuration["write"] and self.configuration["read"]: + read_data = self.bootloader.read_memory_data( + self.configuration["address"], self.configuration["length"] + ) + with open(self.configuration["data_file"], "wb") as out_file: out_file.write(read_data) - if self.configuration['go_address'] != -1: - self.bootloader.go(self.configuration['go_address']) + if self.configuration["go_address"] != -1: + self.bootloader.go(self.configuration["go_address"]) def reset(self): self.bootloader.reset_from_flash() @staticmethod def print_usage(): - help_text = """Usage: %s [-hqVeuwvrsRB] [-l length] [-p port] [-b baud] [-P parity] [-a address] [-g address] [-f family] [file.bin] + help_text = """Usage: %s [-hqVeuwvrsRB] [-l length] [-p port] [-b baud] [-P parity] + [-a address] [-g address] [-f family] [file.bin] -e Erase (note: this is required on previously written memory) -u Unprotect in case erase fails -w Write file content to flash @@ -638,7 +686,7 @@ def print_usage(): -B Make boot0 active high -u Readout unprotect -P parity Parity: "even" for STM32 (default), "none" for BlueNRG - + Example: ./%s -e -w -v example/main.bin """ help_text = help_text % (sys.argv[0], sys.argv[0]) @@ -646,22 +694,23 @@ def print_usage(): def read_device_details(self): boot_version = self.bootloader.get() - debug(0, "Bootloader version %X" % boot_version) + self.debug(0, "Bootloader version %X" % boot_version) device_id = self.bootloader.get_id() - debug(0, "Chip id: 0x%x (%s)" % (device_id, CHIP_IDS.get(device_id, "Unknown"))) - family = self.configuration['family'] + self.debug(0, "Chip id: 0x%x (%s)" % (device_id, CHIP_IDS.get(device_id, "Unknown"))) + family = self.configuration["family"] if not family: - debug(0, "Supply -f [family] to see flash size and device UID, e.g: -f F1") + self.debug(0, "Supply -f [family] to see flash size and device UID, e.g: -f F1") else: device_uid = self.bootloader.get_uid(family) device_uid_string = self.bootloader.format_uid(device_uid) - debug(0, "Device UID: %s" % device_uid_string) + self.debug(0, "Device UID: %s" % device_uid_string) flash_size = self.bootloader.get_flash_size(family) - debug(0, "Flash size: %d KiB" % flash_size) + self.debug(0, "Flash size: %d KiB" % flash_size) def main(): + """Parse arguments and execute tasks.""" loader = Stm32Loader() loader.parse_arguments(sys.argv[1:]) loader.connect() From 2065daafecc42a618b0fcdd6b9d4ccaad19f30ab Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Apr 2019 22:15:46 +0200 Subject: [PATCH 093/369] Cleanup: simplify setup.py and satisfy linters --- setup.py | 71 +++++++------------------------------------------------- 1 file changed, 9 insertions(+), 62 deletions(-) diff --git a/setup.py b/setup.py index e6a7a6b..f877ace 100644 --- a/setup.py +++ b/setup.py @@ -6,19 +6,17 @@ import io import os -import sys -from shutil import rmtree -from setuptools import find_packages, setup, Command +from setuptools import find_packages, setup # Package meta-data. NAME = 'stm32loader' +VERSION = "0.3.3" DESCRIPTION = 'Flash firmware to STM32 microcontrollers using Python.' URL = 'https://github.com/florisla/stm32loader' EMAIL = 'florisla@gmail.com' AUTHOR = 'Floris Lambrechts' REQUIRES_PYTHON = '>=2.7.0' -VERSION = None PROJECT_URLS = { "Bug Tracker": "https://github.com/florisla/stm32loader/issues", "Source Code": "https://github.com/florisla/stm32loader", @@ -32,69 +30,22 @@ "dev": ['setuptools', 'wheel', 'twine', 'pylint', 'flake8', 'flake8-isort', 'black'], } -here = os.path.abspath(os.path.dirname(__file__)) +HERE = os.path.abspath(os.path.dirname(__file__)) # Import the README and use it as the long-description. # Note: this will only work if 'README.md' is present in your MANIFEST.in file! try: - with io.open(os.path.join(here, 'README.md'), encoding='utf-8') as f: - long_description = '\n' + f.read() + with io.open(os.path.join(HERE, 'README.md'), encoding='utf-8') as f: + LONG_DESCRIPTION = '\n' + f.read() except FileNotFoundError: - long_description = DESCRIPTION - -# Load the package's __version__.py module as a dictionary. -about = {} -if not VERSION: - with open(os.path.join(here, NAME, '__version__.py')) as f: - exec (f.read(), about) - about['__version__'] = '.'.join(map(str, about['VERSION'])) -else: - about['__version__'] = VERSION - - -class UploadCommand(Command): - """Support setup.py upload.""" - - description = 'Build and publish the package.' - user_options = [] - - @staticmethod - def status(s): - """Prints things in bold.""" - print('\033[1m{0}\033[0m'.format(s)) - - def initialize_options(self): - pass - - def finalize_options(self): - pass + LONG_DESCRIPTION = DESCRIPTION - def run(self): - try: - self.status('Removing previous builds…') - rmtree(os.path.join(here, 'dist')) - except OSError: - pass - - self.status('Building Source and Wheel (universal) distribution…') - os.system('{0} setup.py sdist bdist_wheel --universal'.format(sys.executable)) - - self.status('Uploading the package to PyPI via Twine…') - os.system('twine upload dist/*') - - self.status('Pushing git tags…') - os.system('git tag v{0}'.format(about['__version__'])) - os.system('git push --tags') - - sys.exit() - - -# Where the magic happens: +VERSION = None setup( name=NAME, - version=about['__version__'], + version=VERSION, description=DESCRIPTION, - long_description=long_description, + long_description=LONG_DESCRIPTION, long_description_content_type='text/markdown', project_urls=PROJECT_URLS, author=AUTHOR, @@ -126,8 +77,4 @@ def run(self): 'Natural Language :: English', 'Operating System :: OS Independent', ], - # $ setup.py publish support. - cmdclass={ - 'upload': UploadCommand, - }, ) From 703b4dcfa7ec1f7e98f940939823a15665b53117 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Apr 2019 22:15:48 +0200 Subject: [PATCH 094/369] Move version to __init__.py and track it independently in setup.py Since this is automated by bump2version, the version numbers will not get out of sync. --- .bumpversion.cfg | 28 +++++++++++++++++++++++----- setup.py | 2 +- stm32loader/__init__.py | 4 ++-- stm32loader/__version__.py | 1 - 4 files changed, 26 insertions(+), 9 deletions(-) delete mode 100644 stm32loader/__version__.py diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 42336c9..78bb466 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,10 +1,28 @@ [bumpversion] -current_version = 0.3.3 +current_version = 0.3.3-dev commit = True -tag = True +tag = False message = Release: bump version number from v{current_version} to v{new_version} +parse = (?P\d+)\.(?P\d+)\.(?P\d+)(-(?P.+))? +serialize = + {major}.{minor}.{patch}-{release} + {major}.{minor}.{patch} -[bumpversion:file:stm32loader/__version__.py] -serialize = ({major}, {minor}, {patch}) -parse = \((?P\d+),\s(?P\d+),\s(?P\d+)\) +[bumpversion:part:release] +optional_value = release +values = + dev + release + +[bumpversion:file:stm32loader/__init__.py] +parse = \((?P\d+),\s(?P\d+),\s(?P\d+)(\s*,\s*['\"](?P[^'\"]+)['\"])?\) +serialize = + ({major}, {minor}, {patch}, "{release}") + ({major}, {minor}, {patch}) +search = __version_info__ = {current_version} +replace = __version_info__ = {new_version} + +[bumpversion:file:setup.py] +search = VERSION = "{current_version}" +replace = VERSION = "{new_version}" diff --git a/setup.py b/setup.py index f877ace..f84ccfe 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ # Package meta-data. NAME = 'stm32loader' -VERSION = "0.3.3" +VERSION = "0.3.3-dev" DESCRIPTION = 'Flash firmware to STM32 microcontrollers using Python.' URL = 'https://github.com/florisla/stm32loader' EMAIL = 'florisla@gmail.com' diff --git a/stm32loader/__init__.py b/stm32loader/__init__.py index ae0d495..617c569 100644 --- a/stm32loader/__init__.py +++ b/stm32loader/__init__.py @@ -1,7 +1,7 @@ """Flash firmware to STM32 microcontrollers over an RS-232 serial connection.""" -from .__version__ import VERSION -__version__ = ".".join(map(str, VERSION)) +__version_info__ = (0, 3, 3, "dev") +__version__ = "-".join(str(part) for part in __version_info__).replace("-", ".", 2) from .stm32loader import Stm32Loader, Stm32Bootloader, main diff --git a/stm32loader/__version__.py b/stm32loader/__version__.py deleted file mode 100644 index 2300c84..0000000 --- a/stm32loader/__version__.py +++ /dev/null @@ -1 +0,0 @@ -VERSION = (0, 3, 3) From 59152ebca8652103ef26c490826f86751d07ba90 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Apr 2019 22:15:49 +0200 Subject: [PATCH 095/369] Doc: add 'to do' list --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index 293f366..02acca2 100644 --- a/README.md +++ b/README.md @@ -114,3 +114,17 @@ Not currently supported * Command-line argument for write protection/unprotection * STM8 devices (ST UM0560) * Other bootloader protocols (e.g. I2C, HEX -> implemented in stm32flash) + + +Future work +----------- +* Extract RS-232 details into separate class +* Allow to load STM32Bootloader without dependencies +* Allow to run main() with custom arguments from code +* Support custom non-RS232 links (e.g. I2C) +* Use intenum for command and reply +* Use proper logging instead of print statements +* Drop Python2 compatibility? +* Add unit tests +* Use nox or tox for checking Python version compatibility +* Use Travis or Azure pipelines for CI From 8ab2b2674cd629d46c7ba5e77f84bf8fc5f750a9 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Apr 2019 22:15:50 +0200 Subject: [PATCH 096/369] Extract rs-232 peculiarities into SerialConnection class --- README.md | 1 - stm32loader/serial.py | 96 ++++++++++++++++++ stm32loader/stm32loader.py | 194 +++++++++++++++---------------------- 3 files changed, 174 insertions(+), 117 deletions(-) create mode 100644 stm32loader/serial.py diff --git a/README.md b/README.md index 02acca2..8c4c261 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,6 @@ Not currently supported Future work ----------- -* Extract RS-232 details into separate class * Allow to load STM32Bootloader without dependencies * Allow to run main() with custom arguments from code * Support custom non-RS232 links (e.g. I2C) diff --git a/stm32loader/serial.py b/stm32loader/serial.py new file mode 100644 index 0000000..060a4e1 --- /dev/null +++ b/stm32loader/serial.py @@ -0,0 +1,96 @@ +# Author: Floris Lambrechts +# GitHub repository: https://github.com/florisla/stm32loader +# +# This file is part of stm32loader. +# +# stm32loader is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation; either version 3, or (at your option) any later +# version. +# +# stm32loader is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# for more details. +# +# You should have received a copy of the GNU General Public License +# along with stm32loader; see the file LICENSE. If not see +# . + +""" +Handle RS-232 serial communication through pyserial. + +Offer support for toggling RESET and BOOT0. +""" + +import serial + + +class SerialConnection: + """Wrap a serial.Serial connection and offer enable_reset and enable_boot0.""" + + # pylint: disable=too-many-instance-attributes + + def __init__(self, serial_port, baud_rate=115_200, parity="E"): + """Construct a SerialConnection (not yet connected).""" + self.serial_port = serial_port + self.baud_rate = baud_rate + self.parity = parity + + # advertise reset / boot0 toggle capability + self.can_toggle_reset = True + self.can_toggle_boot0 = True + + self.swap_rts_dtr = False + self.reset_active_high = False + self.boot0_active_high = False + + # call connect() to establish connection + self.serial_connection = None + + def connect(self): + self.serial_connection = serial.Serial( + port=self.serial_port, + baudrate=self.baud_rate, + # number of write_data bits + bytesize=8, + parity=self.parity, + stopbits=1, + # don't enable software flow control + xonxoff=0, + # don't enable RTS/CTS flow control + rtscts=0, + # set a timeout value, None for waiting forever + timeout=5, + ) + + def write(self, *args, **kwargs): + return self.serial_connection.write(*args, **kwargs) + + def read(self, *args, **kwargs): + return self.serial_connection.read(*args, **kwargs) + + def enable_reset(self, enable=True): + # reset on the STM32 is active low (0 Volt puts the MCU in reset) + # but the RS-232 DTR signal is active low by itself, so it inverts this + # (writing a logical 1 outputs a low voltage == reset enabled) + level = int(enable) + if self.reset_active_high: + level = 1 - level + + if self.swap_rts_dtr: + self.serial_connection.setRTS(level) + else: + self.serial_connection.setDTR(level) + + def enable_boot0(self, enable=True): + level = int(enable) + + # by default, this is active low + if not self.boot0_active_high: + level = 1 - level + + if self.swap_rts_dtr: + self.serial_connection.setDTR(level) + else: + self.serial_connection.setRTS(level) diff --git a/stm32loader/stm32loader.py b/stm32loader/stm32loader.py index 16a88a3..fb53e1e 100644 --- a/stm32loader/stm32loader.py +++ b/stm32loader/stm32loader.py @@ -31,7 +31,7 @@ import time from functools import reduce -import serial +from stm32loader.serial import SerialConnection DEFAULT_VERBOSITY = 5 @@ -110,8 +110,6 @@ class Reply: ACK = 0x79 NACK = 0x1F - PARITY = {"even": serial.PARITY_EVEN, "none": serial.PARITY_NONE} - UID_ADDRESS = { # ST RM0008 section 30.1 Unique device ID register # F101, F102, F103, F105, F107 @@ -138,52 +136,18 @@ class Reply: extended_erase = False - def __init__( - self, - swap_rts_dtr=False, - reset_active_high=False, - boot0_active_high=False, - verbosity=DEFAULT_VERBOSITY, - ): + def __init__(self, connection, verbosity=DEFAULT_VERBOSITY): """ Construct the Stm32Bootloader object. - Call setup_connection() before doing any real work. + :param connection: Object supporting read() and write(). E.g. serial.Serial(). + :param int verbosity: Verbosity level. 0 is quite, 10 is verbose. """ - self.serial = None - self._swap_rts_dtr = swap_rts_dtr - self._reset_active_high = reset_active_high - self._boot0_active_high = boot0_active_high + self.connection = connection + self._toggle_reset = getattr(connection, 'TOGGLES_RESET', False) + self._toggle_boot0 = getattr(connection, 'TOGGLES_BOOT0', False) self.verbosity = verbosity - def setup_connection(self, serial_port, baud_rate=115_200, parity=serial.PARITY_EVEN): - try: - self.serial = serial.Serial( - port=serial_port, - baudrate=baud_rate, - # number of write_data bits - bytesize=8, - parity=parity, - stopbits=1, - # don't enable software flow control - xonxoff=0, - # don't enable RTS/CTS flow control - rtscts=0, - # set a timeout value, None for waiting forever - timeout=5, - ) - except serial.serialutil.SerialException as e: - sys.stderr.write(str(e) + "\n") - sys.stderr.write( - "Is the device connected and powered correctly?\n" - "Please use the -p option to select the correct serial port. Examples:\n" - " -p COM3\n" - " -p /dev/ttyS0\n" - " -p /dev/ttyUSB0\n" - " -p /dev/tty.usbserial-ftCYPMYJ\n" - ) - exit(1) - def debug(self, level, message): if self.verbosity >= level: print(message, file=sys.stderr) @@ -191,7 +155,7 @@ def debug(self, level, message): def reset_from_system_memory(self): self._enable_boot0(True) self._reset() - self.serial.write(bytearray([self.Command.SYNCHRONIZE])) + self.connection.write(bytearray([self.Command.SYNCHRONIZE])) return self._wait_for_ack("Syncro") def reset_from_flash(self): @@ -202,8 +166,8 @@ def command(self, command): command_byte = bytearray([command]) control_byte = bytearray([command ^ 0xFF]) - self.serial.write(command_byte) - self.serial.write(control_byte) + self.connection.write(command_byte) + self.connection.write(control_byte) return self._wait_for_ack(hex(command)) @@ -211,10 +175,10 @@ def get(self): if not self.command(self.Command.GET): raise CommandException("Get (0x00) failed") self.debug(10, "*** Get command") - length = bytearray(self.serial.read())[0] - version = bytearray(self.serial.read())[0] + length = bytearray(self.connection.read())[0] + version = bytearray(self.connection.read())[0] self.debug(10, " Bootloader version: " + hex(version)) - data = bytearray(self.serial.read(length)) + data = bytearray(self.connection.read(length)) if self.Command.EXTENDED_ERASE in data: self.extended_erase = True self.debug(10, " Available commands: " + ", ".join(hex(b) for b in data)) @@ -226,8 +190,8 @@ def get_version(self): raise CommandException("GetVersion (0x01) failed") self.debug(10, "*** GetVersion command") - version = bytearray(self.serial.read())[0] - self.serial.read(2) + version = bytearray(self.connection.read())[0] + self.connection.read(2) self._wait_for_ack("0x01 end") self.debug(10, " Bootloader version: " + hex(version)) return version @@ -237,8 +201,8 @@ def get_id(self): raise CommandException("GetID (0x02) failed") self.debug(10, "*** GetID command") - length = bytearray(self.serial.read())[0] - id_data = bytearray(self.serial.read(length + 1)) + length = bytearray(self.connection.read())[0] + id_data = bytearray(self.connection.read(length + 1)) self._wait_for_ack("0x02 end") _device_id = reduce(lambda x, y: x * 0x100 + y, id_data) return _device_id @@ -266,13 +230,13 @@ def read_memory(self, address, length): raise CommandException("ReadMemory (0x11) failed") self.debug(10, "*** ReadMemory command") - self.serial.write(self._encode_address(address)) + self.connection.write(self._encode_address(address)) self._wait_for_ack("0x11 address failed") nr_of_bytes = (length - 1) & 0xFF checksum = nr_of_bytes ^ 0xFF - self.serial.write(bytearray([nr_of_bytes, checksum])) + self.connection.write(bytearray([nr_of_bytes, checksum])) self._wait_for_ack("0x11 length failed") - return bytearray(self.serial.read(length)) + return bytearray(self.connection.read(length)) def go(self, address): # pylint: disable=invalid-name @@ -280,7 +244,7 @@ def go(self, address): raise CommandException("Go (0x21) failed") self.debug(10, "*** Go command") - self.serial.write(self._encode_address(address)) + self.connection.write(self._encode_address(address)) self._wait_for_ack("0x21 go failed") def write_memory(self, address, data): @@ -291,7 +255,7 @@ def write_memory(self, address, data): raise CommandException("Write memory (0x31) failed") self.debug(10, "*** Write memory command") - self.serial.write(self._encode_address(address)) + self.connection.write(self._encode_address(address)) self._wait_for_ack("0x31 address failed") # pad data length to multiple of 4 bytes @@ -303,12 +267,12 @@ def write_memory(self, address, data): data.extend([0xFF] * padding_bytes) self.debug(10, " %s bytes to write" % [nr_of_bytes]) - self.serial.write(bytearray([nr_of_bytes - 1])) + self.connection.write(bytearray([nr_of_bytes - 1])) checksum = nr_of_bytes - 1 for data_byte in data: checksum = checksum ^ data_byte - self.serial.write(bytearray(data)) - self.serial.write(bytearray([checksum])) + self.connection.write(bytearray(data)) + self.connection.write(bytearray([checksum])) self._wait_for_ack("0x31 programming failed") self.debug(10, " Write memory done") @@ -335,14 +299,14 @@ def extended_erase_memory(self): self.debug(10, "*** Extended Erase memory command") # Global mass erase and checksum byte - self.serial.write(b"\xFF") - self.serial.write(b"\xFF") - self.serial.write(b"\x00") - previous_timeout_value = self.serial.timeout - self.serial.timeout = 30 + self.connection.write(b"\xFF") + self.connection.write(b"\xFF") + self.connection.write(b"\x00") + previous_timeout_value = self.connection.timeout + self.connection.timeout = 30 print("Extended erase (0x44), this can take ten seconds or more") self._wait_for_ack("0x44 erasing failed") - self.serial.timeout = previous_timeout_value + self.connection.timeout = previous_timeout_value self.debug(10, " Extended Erase memory done") def write_protect(self, pages): @@ -351,12 +315,12 @@ def write_protect(self, pages): self.debug(10, "*** Write protect command") nr_of_pages = (len(pages) - 1) & 0xFF - self.serial.write(bytearray([nr_of_pages])) + self.connection.write(bytearray([nr_of_pages])) checksum = 0xFF for page_index in pages: checksum = checksum ^ page_index - self.serial.write(bytearray([page_index])) - self.serial.write(bytearray([checksum])) + self.connection.write(bytearray([page_index])) + self.connection.write(bytearray([checksum])) self._wait_for_ack("0x63 write protect failed") self.debug(10, " Write protect done") @@ -423,57 +387,36 @@ def write_memory_data(self, address, data): def _global_erase(self): # global erase: n=255, see ST AN3155 - self.serial.write(b"\xff") - self.serial.write(b"\x00") + self.connection.write(b"\xff") + self.connection.write(b"\x00") def _page_erase(self, pages): # page erase, see ST AN3155 nr_of_pages = (len(pages) - 1) & 0xFF - self.serial.write(bytearray([nr_of_pages])) + self.connection.write(bytearray([nr_of_pages])) checksum = nr_of_pages for page_number in pages: - self.serial.write(bytearray([page_number])) + self.connection.write(bytearray([page_number])) checksum = checksum ^ page_number - self.serial.write(bytearray([checksum])) + self.connection.write(bytearray([checksum])) def _reset(self): - self._enable_reset(True) + if not self._toggle_reset: + return + self.connection.enable_reset(True) time.sleep(0.1) - self._enable_reset(False) + self.connection.enable_reset(False) time.sleep(0.5) - def _enable_reset(self, enable=True): - # reset on the MCU is active low (0 Volt puts the MCU in reset) - # but RS-232 DTR is active low by itself so it inverts this - # (writing logical 1 outputs a low voltage) - level = 1 if enable else 0 - - # setting -R (reset active high) ensures that the MCU - # gets 3.3 Volt to enable reset - if self._reset_active_high: - level = 1 - level - - if self._swap_rts_dtr: - self.serial.setRTS(level) - else: - self.serial.setDTR(level) - def _enable_boot0(self, enable=True): - # active low unless otherwise specified - level = 0 if enable else 1 - - if self._boot0_active_high: - # enabled by argument -B (boot0 active high) - level = 1 - level + if not self._toggle_boot0: + return - if self._swap_rts_dtr: - self.serial.setDTR(level) - else: - self.serial.setRTS(level) + self.connection.enable_boot0(enable) def _wait_for_ack(self, info=""): try: - ack = bytearray(self.serial.read())[0] + ack = bytearray(self.connection.read())[0] except TypeError: raise CommandException("Can't read port or timeout") @@ -498,13 +441,16 @@ def _encode_address(address): class Stm32Loader: """Main application: parse arguments and handle commands.""" + # serial link bit parity, compatible to pyserial serial.PARTIY_EVEN + PARITY = {"even": "E", "none": "N"} + def __init__(self): """Construct Stm32Loader object with default settings.""" self.bootloader = None self.configuration = { "port": "/dev/tty.usbserial-ftCYPMYJ", "baud": 115_200, - "parity": serial.PARITY_EVEN, + "parity": self.PARITY["even"], "family": None, "address": 0x08000000, "erase": False, @@ -573,9 +519,9 @@ def parse_arguments(self, arguments): self.configuration["family"] = value elif option == "-P": assert ( - value.lower() in Stm32Bootloader.PARITY + value.lower() in Stm32Loader.PARITY ), "Parity value not recognized: '{0}'.".format(value) - self.configuration["parity"] = Stm32Bootloader.PARITY[value.lower()] + self.configuration["parity"] = Stm32Loader.PARITY[value.lower()] elif option == "-a": self.configuration["address"] = int(eval(value)) elif option == "-g": @@ -586,24 +532,40 @@ def parse_arguments(self, arguments): assert False, "unhandled option %s" % option def connect(self): - self.bootloader = Stm32Bootloader( - swap_rts_dtr=self.configuration["swap_rts_dtr"], - reset_active_high=self.configuration["reset_active_high"], - boot0_active_high=self.configuration["boot0_active_high"], - verbosity=self.verbosity, - ) - self.bootloader.setup_connection( - self.configuration["port"], self.configuration["baud"], self.configuration["parity"] + serial_connection = SerialConnection( + self.configuration["port"], + self.configuration["baud"], + self.configuration["parity"], ) self.debug( 10, "Open port %(port)s, baud %(baud)d" % {"port": self.configuration["port"], "baud": self.configuration["baud"]}, ) + try: + serial_connection.connect() + except IOError as e: + sys.stderr.write(str(e) + "\n") + sys.stderr.write( + "Is the device connected and powered correctly?\n" + "Please use the -p option to select the correct serial port. Examples:\n" + " -p COM3\n" + " -p /dev/ttyS0\n" + " -p /dev/ttyUSB0\n" + " -p /dev/tty.usbserial-ftCYPMYJ\n" + ) + exit(1) + + serial_connection.swap_rts_dtr = self.configuration["swap_rts_dtr"] + serial_connection.reset_active_high = self.configuration["reset_active_high"] + serial_connection.boot0_active_high = self.configuration["boot0_active_high"] + + self.bootloader = Stm32Bootloader(serial_connection, verbosity=self.verbosity) + try: self.bootloader.reset_from_system_memory() except BaseException: - print("Can't init. Ensure that BOOT0 is enabled and reset device") + print("Can't init into bootloader. Ensure that BOOT0 is enabled and reset device.") self.bootloader.reset_from_flash() sys.exit(1) From 8c7427370b140492c00ae9c387f2c1b7eab064ce Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Apr 2019 22:15:51 +0200 Subject: [PATCH 097/369] Move Stm32Bootloader into separate file This makes it easier to keep the dependencies limited. --- README.md | 3 - stm32loader/__init__.py | 4 - stm32loader/bootloader.py | 438 +++++++++++++++++++++++++++++++++++++ stm32loader/stm32loader.py | 411 +--------------------------------- 4 files changed, 441 insertions(+), 415 deletions(-) create mode 100644 stm32loader/bootloader.py diff --git a/README.md b/README.md index 8c4c261..10abb0c 100644 --- a/README.md +++ b/README.md @@ -118,9 +118,6 @@ Not currently supported Future work ----------- -* Allow to load STM32Bootloader without dependencies -* Allow to run main() with custom arguments from code -* Support custom non-RS232 links (e.g. I2C) * Use intenum for command and reply * Use proper logging instead of print statements * Drop Python2 compatibility? diff --git a/stm32loader/__init__.py b/stm32loader/__init__.py index 617c569..53cfeb4 100644 --- a/stm32loader/__init__.py +++ b/stm32loader/__init__.py @@ -2,7 +2,3 @@ __version_info__ = (0, 3, 3, "dev") __version__ = "-".join(str(part) for part in __version_info__).replace("-", ".", 2) - -from .stm32loader import Stm32Loader, Stm32Bootloader, main - -__all__ = ["__version_info__", "__version__", "Stm32Loader", "Stm32Bootloader", "main"] diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py new file mode 100644 index 0000000..8bf9529 --- /dev/null +++ b/stm32loader/bootloader.py @@ -0,0 +1,438 @@ +# Authors: Ivan A-R, Floris Lambrechts +# GitHub repository: https://github.com/florisla/stm32loader +# +# This file is part of stm32loader. +# +# stm32loader is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation; either version 3, or (at your option) any later +# version. +# +# stm32loader is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# for more details. +# +# You should have received a copy of the GNU General Public License +# along with stm32loader; see the file LICENSE. If not see +# . + +"""Flash firmware to STM32 microcontrollers over an RS-232 serial connection.""" + + +from __future__ import print_function + +import sys +import time +from functools import reduce + +CHIP_IDS = { + # see ST AN2606 Table 116 Bootloader device-dependent parameters + # 16 to 32 KiB + 0x412: "STM32F10x Low-density", + # 64 to 128 KiB + 0x410: "STM32F10x Medium-density", + 0x420: "STM32F10x Medium-density value line", + # 256 to 512 KiB (5128 Kbyte is probably a typo?) + 0x414: "STM32F10x High-density", + 0x428: "STM32F10x High-density value line", + # 768 to 1024 KiB + 0x430: "STM3210xx XL-density", + # flash size to be looked up + 0x416: "STM32L1xxx6(8/B) Medium-density ultralow power line", + 0x411: "STM32F2xxx", + 0x413: "STM32F40xxx/41xxx", + 0x419: "STM3242xxx/43xxx", + 0x449: "STM32F74xxx/75xxx", + 0x451: "STM32F76xxx/77xxx", + # see ST AN4872 + # requires parity None + 0x11103: "BlueNRG", + # other + # Cortex-M0 MCU with hardware TCP/IP and MAC + # (SweetPeas custom bootloader) + 0x801: "Wiznet W7500", +} + + +class CommandException(IOError): + """Error: a command in the STM32 native bootloader failed.""" + + pass + + +class Stm32Bootloader: + """Talk to the STM32 native bootloader.""" + + # pylint: disable=too-many-public-methods + + class Command: + """STM32 native bootloader command values.""" + + # pylint: disable=too-few-public-methods + # FIXME turn into intenum + + # See ST AN3155, AN4872 + GET = 0x00 + GET_VERSION = 0x01 + GET_ID = 0x02 + READ_MEMORY = 0x11 + GO = 0x21 + WRITE_MEMORY = 0x31 + ERASE = 0x43 + READOUT_PROTECT = 0x82 + READOUT_UNPROTECT = 0x92 + # these not supported on BlueNRG + EXTENDED_ERASE = 0x44 + WRITE_PROTECT = 0x63 + WRITE_UNPROTECT = 0x73 + + # not really listed under commands, but still... + # 'wake the bootloader' == 'activate USART' == 'synchronize' + SYNCHRONIZE = 0x7F + + class Reply: + """STM32 native bootloader reply status codes.""" + + # pylint: disable=too-few-public-methods + # FIXME turn into intenum + + # See ST AN3155, AN4872 + ACK = 0x79 + NACK = 0x1F + + UID_ADDRESS = { + # ST RM0008 section 30.1 Unique device ID register + # F101, F102, F103, F105, F107 + "F1": 0x1FFFF7E8, + # ST RM0090 section 39.1 Unique device ID register + # F405/415, F407/417, F427/437, F429/439 + "F4": 0x1FFFF7A10, + # ST RM0385 section 41.2 Unique device ID register + "F7": 0x1FF0F420, + } + + UID_SWAP = [[1, 0], [3, 2], [7, 6, 5, 4], [11, 10, 9, 8]] + + FLASH_SIZE_ADDRESS = { + # ST RM0008 section 30.2 Memory size registers + # F101, F102, F103, F105, F107 + "F1": 0x1FFFF7E0, + # ST RM0090 section 39.2 Flash size + # F405/415, F407/417, F427/437, F429/439 + "F4": 0x1FFF7A22, + # ST RM0385 section 41.2 Flash size + "F7": 0x1FF0F442, + } + + extended_erase = False + + def __init__(self, connection, verbosity=5): + """ + Construct the Stm32Bootloader object. + + The supplied connection can be any object that supports + read() and write(). Optionally, it may also offer + enable_reset() and enable_boot0; it should advertize this by + setting TOGGLES_RESET and TOGGLES_BOOT0 to True. + + The default implementation is stm32loader.connection.SerialConnection, + but a straight pyserial serial.Serial object can also be used. + + + :param connection: Object supporting read() and write(). E.g. serial.Serial(). + :param int verbosity: Verbosity level. 0 is quite, 10 is verbose. + """ + self.connection = connection + self._toggle_reset = getattr(connection, "can_toggle_reset", False) + self._toggle_boot0 = getattr(connection, "can_toggle_boot0", False) + self.verbosity = verbosity + + def debug(self, level, message): + if self.verbosity >= level: + print(message, file=sys.stderr) + + def reset_from_system_memory(self): + self._enable_boot0(True) + self._reset() + self.connection.write(bytearray([self.Command.SYNCHRONIZE])) + return self._wait_for_ack("Syncro") + + def reset_from_flash(self): + self._enable_boot0(False) + self._reset() + + def command(self, command): + command_byte = bytearray([command]) + control_byte = bytearray([command ^ 0xFF]) + + self.connection.write(command_byte) + self.connection.write(control_byte) + + return self._wait_for_ack(hex(command)) + + def get(self): + if not self.command(self.Command.GET): + raise CommandException("Get (0x00) failed") + self.debug(10, "*** Get command") + length = bytearray(self.connection.read())[0] + version = bytearray(self.connection.read())[0] + self.debug(10, " Bootloader version: " + hex(version)) + data = bytearray(self.connection.read(length)) + if self.Command.EXTENDED_ERASE in data: + self.extended_erase = True + self.debug(10, " Available commands: " + ", ".join(hex(b) for b in data)) + self._wait_for_ack("0x00 end") + return version + + def get_version(self): + if not self.command(self.Command.GET_VERSION): + raise CommandException("GetVersion (0x01) failed") + + self.debug(10, "*** GetVersion command") + version = bytearray(self.connection.read())[0] + self.connection.read(2) + self._wait_for_ack("0x01 end") + self.debug(10, " Bootloader version: " + hex(version)) + return version + + def get_id(self): + if not self.command(self.Command.GET_ID): + raise CommandException("GetID (0x02) failed") + + self.debug(10, "*** GetID command") + length = bytearray(self.connection.read())[0] + id_data = bytearray(self.connection.read(length + 1)) + self._wait_for_ack("0x02 end") + _device_id = reduce(lambda x, y: x * 0x100 + y, id_data) + return _device_id + + def get_flash_size(self, device_family): + flash_size_address = self.FLASH_SIZE_ADDRESS[device_family] + flash_size_bytes = self.read_memory(flash_size_address, 2) + flash_size = flash_size_bytes[0] + flash_size_bytes[1] * 256 + return flash_size + + def get_uid(self, device_id): + uid_address = self.UID_ADDRESS[device_id] + uid = self.read_memory(uid_address, 12) + return uid + + @staticmethod + def format_uid(uid): + swapped_data = [[uid[b] for b in part] for part in Stm32Bootloader.UID_SWAP] + uid_string = "-".join("".join(format(b, "02X") for b in part) for part in swapped_data) + return uid_string + + def read_memory(self, address, length): + assert length <= 256 + if not self.command(self.Command.READ_MEMORY): + raise CommandException("ReadMemory (0x11) failed") + + self.debug(10, "*** ReadMemory command") + self.connection.write(self._encode_address(address)) + self._wait_for_ack("0x11 address failed") + nr_of_bytes = (length - 1) & 0xFF + checksum = nr_of_bytes ^ 0xFF + self.connection.write(bytearray([nr_of_bytes, checksum])) + self._wait_for_ack("0x11 length failed") + return bytearray(self.connection.read(length)) + + def go(self, address): + # pylint: disable=invalid-name + if not self.command(self.Command.GO): + raise CommandException("Go (0x21) failed") + + self.debug(10, "*** Go command") + self.connection.write(self._encode_address(address)) + self._wait_for_ack("0x21 go failed") + + def write_memory(self, address, data): + nr_of_bytes = len(data) + assert nr_of_bytes <= 256 + + if not self.command(self.Command.WRITE_MEMORY): + raise CommandException("Write memory (0x31) failed") + + self.debug(10, "*** Write memory command") + self.connection.write(self._encode_address(address)) + self._wait_for_ack("0x31 address failed") + + # pad data length to multiple of 4 bytes + if nr_of_bytes % 4 != 0: + padding_bytes = 4 - (nr_of_bytes % 4) + nr_of_bytes += padding_bytes + # append value 0xFF: flash memory value after erase + data = bytearray(data) + data.extend([0xFF] * padding_bytes) + + self.debug(10, " %s bytes to write" % [nr_of_bytes]) + self.connection.write(bytearray([nr_of_bytes - 1])) + checksum = nr_of_bytes - 1 + for data_byte in data: + checksum = checksum ^ data_byte + self.connection.write(bytearray(data)) + self.connection.write(bytearray([checksum])) + self._wait_for_ack("0x31 programming failed") + self.debug(10, " Write memory done") + + def erase_memory(self, sectors=None): + if self.extended_erase: + self.extended_erase_memory() + return + + if not self.command(self.Command.ERASE): + raise CommandException("Erase memory (0x43) failed") + + self.debug(10, "*** Erase memory command") + + if sectors: + self._page_erase(sectors) + else: + self._global_erase() + self._wait_for_ack("0x43 erase failed") + self.debug(10, " Erase memory done") + + def extended_erase_memory(self): + if not self.command(self.Command.EXTENDED_ERASE): + raise CommandException("Extended Erase memory (0x44) failed") + + self.debug(10, "*** Extended Erase memory command") + # Global mass erase and checksum byte + self.connection.write(b"\xFF") + self.connection.write(b"\xFF") + self.connection.write(b"\x00") + previous_timeout_value = self.connection.timeout + self.connection.timeout = 30 + print("Extended erase (0x44), this can take ten seconds or more") + self._wait_for_ack("0x44 erasing failed") + self.connection.timeout = previous_timeout_value + self.debug(10, " Extended Erase memory done") + + def write_protect(self, pages): + if not self.command(self.Command.WRITE_PROTECT): + raise CommandException("Write Protect memory (0x63) failed") + + self.debug(10, "*** Write protect command") + nr_of_pages = (len(pages) - 1) & 0xFF + self.connection.write(bytearray([nr_of_pages])) + checksum = 0xFF + for page_index in pages: + checksum = checksum ^ page_index + self.connection.write(bytearray([page_index])) + self.connection.write(bytearray([checksum])) + self._wait_for_ack("0x63 write protect failed") + self.debug(10, " Write protect done") + + def write_unprotect(self): + if not self.command(self.Command.WRITE_UNPROTECT): + raise CommandException("Write Unprotect (0x73) failed") + + self.debug(10, "*** Write Unprotect command") + self._wait_for_ack("0x73 write unprotect failed") + self.debug(10, " Write Unprotect done") + + def readout_protect(self): + if not self.command(self.Command.READOUT_PROTECT): + raise CommandException("Readout protect (0x82) failed") + + self.debug(10, "*** Readout protect command") + self._wait_for_ack("0x82 readout protect failed") + self.debug(10, " Read protect done") + + def readout_unprotect(self): + if not self.command(self.Command.READOUT_UNPROTECT): + raise CommandException("Readout unprotect (0x92) failed") + + self.debug(10, "*** Readout Unprotect command") + self._wait_for_ack("0x92 readout unprotect failed") + self.debug(20, " Mass erase -- this may take a while") + time.sleep(20) + self.debug(20, " Unprotect / mass erase done") + self.debug(20, " Reset after automatic chip reset due to readout unprotect") + self.reset_from_system_memory() + + def read_memory_data(self, address, length): + data = bytearray() + while length > 256: + self.debug( + 5, "Read %(len)d bytes at 0x%(address)X" % {"address": address, "len": 256} + ) + data = data + self.read_memory(address, 256) + address = address + 256 + length = length - 256 + if length: + self.debug( + 5, "Read %(len)d bytes at 0x%(address)X" % {"address": address, "len": length} + ) + data = data + self.read_memory(address, length) + return data + + def write_memory_data(self, address, data): + length = len(data) + offset = 0 + while length > 256: + self.debug( + 5, "Write %(len)d bytes at 0x%(address)X" % {"address": address, "len": 256} + ) + self.write_memory(address, data[offset : offset + 256]) + offset += 256 + address += 256 + length -= 256 + if length: + self.debug( + 5, "Write %(len)d bytes at 0x%(address)X" % {"address": address, "len": length} + ) + self.write_memory(address, data[offset : offset + length]) + + def _global_erase(self): + # global erase: n=255, see ST AN3155 + self.connection.write(b"\xff") + self.connection.write(b"\x00") + + def _page_erase(self, pages): + # page erase, see ST AN3155 + nr_of_pages = (len(pages) - 1) & 0xFF + self.connection.write(bytearray([nr_of_pages])) + checksum = nr_of_pages + for page_number in pages: + self.connection.write(bytearray([page_number])) + checksum = checksum ^ page_number + self.connection.write(bytearray([checksum])) + + def _reset(self): + if not self._toggle_reset: + return + self.connection.enable_reset(True) + time.sleep(0.1) + self.connection.enable_reset(False) + time.sleep(0.5) + + def _enable_boot0(self, enable=True): + if not self._toggle_boot0: + return + + self.connection.enable_boot0(enable) + + def _wait_for_ack(self, info=""): + try: + ack = bytearray(self.connection.read())[0] + except TypeError: + raise CommandException("Can't read port or timeout") + + if ack == self.Reply.NACK: + raise CommandException("NACK " + info) + + if ack != self.Reply.ACK: + raise CommandException("Unknown response. " + info + ": " + hex(ack)) + + return 1 + + @staticmethod + def _encode_address(address): + byte3 = (address >> 0) & 0xFF + byte2 = (address >> 8) & 0xFF + byte1 = (address >> 16) & 0xFF + byte0 = (address >> 24) & 0xFF + checksum = byte0 ^ byte1 ^ byte2 ^ byte3 + return bytearray([byte0, byte1, byte2, byte3, checksum]) diff --git a/stm32loader/stm32loader.py b/stm32loader/stm32loader.py index fb53e1e..bf634c3 100644 --- a/stm32loader/stm32loader.py +++ b/stm32loader/stm32loader.py @@ -18,7 +18,7 @@ # for more details. # # You should have received a copy of the GNU General Public License -# along with stm32loader; see the file COPYING3. If not see +# along with stm32loader; see the file COPYING. If not see # . """Flash firmware to STM32 microcontrollers over an RS-232 serial connection.""" @@ -28,415 +28,12 @@ import getopt import sys -import time -from functools import reduce +from stm32loader.bootloader import CHIP_IDS, CommandException, Stm32Bootloader from stm32loader.serial import SerialConnection DEFAULT_VERBOSITY = 5 -CHIP_IDS = { - # see ST AN2606 Table 116 Bootloader device-dependent parameters - # 16 to 32 KiB - 0x412: "STM32F10x Low-density", - # 64 to 128 KiB - 0x410: "STM32F10x Medium-density", - 0x420: "STM32F10x Medium-density value line", - # 256 to 512 KiB (5128 Kbyte is probably a typo?) - 0x414: "STM32F10x High-density", - 0x428: "STM32F10x High-density value line", - # 768 to 1024 KiB - 0x430: "STM3210xx XL-density", - # flash size to be looked up - 0x416: "STM32L1xxx6(8/B) Medium-density ultralow power line", - 0x411: "STM32F2xxx", - 0x413: "STM32F40xxx/41xxx", - 0x419: "STM3242xxx/43xxx", - 0x449: "STM32F74xxx/75xxx", - 0x451: "STM32F76xxx/77xxx", - # see ST AN4872 - # requires parity None - 0x11103: "BlueNRG", - # other - # Cortex-M0 MCU with hardware TCP/IP and MAC - # (SweetPeas custom bootloader) - 0x801: "Wiznet W7500", -} - - -class CommandException(Exception): - """Error: a command in the STM32 native bootloader failed.""" - - pass - - -class Stm32Bootloader: - """Talk to the STM32 native bootloader.""" - - # pylint: disable=too-many-public-methods - - class Command: - """STM32 native bootloader command values.""" - - # pylint: disable=too-few-public-methods - # FIXME turn into intenum - - # See ST AN3155, AN4872 - GET = 0x00 - GET_VERSION = 0x01 - GET_ID = 0x02 - READ_MEMORY = 0x11 - GO = 0x21 - WRITE_MEMORY = 0x31 - ERASE = 0x43 - READOUT_PROTECT = 0x82 - READOUT_UNPROTECT = 0x92 - # these not supported on BlueNRG - EXTENDED_ERASE = 0x44 - WRITE_PROTECT = 0x63 - WRITE_UNPROTECT = 0x73 - - # not really listed under commands, but still... - # 'wake the bootloader' == 'activate USART' == 'synchronize' - SYNCHRONIZE = 0x7F - - class Reply: - """STM32 native bootloader reply status codes.""" - - # pylint: disable=too-few-public-methods - # FIXME turn into intenum - - # See ST AN3155, AN4872 - ACK = 0x79 - NACK = 0x1F - - UID_ADDRESS = { - # ST RM0008 section 30.1 Unique device ID register - # F101, F102, F103, F105, F107 - "F1": 0x1FFFF7E8, - # ST RM0090 section 39.1 Unique device ID register - # F405/415, F407/417, F427/437, F429/439 - "F4": 0x1FFFF7A10, - # ST RM0385 section 41.2 Unique device ID register - "F7": 0x1FF0F420, - } - - UID_SWAP = [[1, 0], [3, 2], [7, 6, 5, 4], [11, 10, 9, 8]] - - FLASH_SIZE_ADDRESS = { - # ST RM0008 section 30.2 Memory size registers - # F101, F102, F103, F105, F107 - "F1": 0x1FFFF7E0, - # ST RM0090 section 39.2 Flash size - # F405/415, F407/417, F427/437, F429/439 - "F4": 0x1FFF7A22, - # ST RM0385 section 41.2 Flash size - "F7": 0x1FF0F442, - } - - extended_erase = False - - def __init__(self, connection, verbosity=DEFAULT_VERBOSITY): - """ - Construct the Stm32Bootloader object. - - :param connection: Object supporting read() and write(). E.g. serial.Serial(). - :param int verbosity: Verbosity level. 0 is quite, 10 is verbose. - """ - self.connection = connection - self._toggle_reset = getattr(connection, 'TOGGLES_RESET', False) - self._toggle_boot0 = getattr(connection, 'TOGGLES_BOOT0', False) - self.verbosity = verbosity - - def debug(self, level, message): - if self.verbosity >= level: - print(message, file=sys.stderr) - - def reset_from_system_memory(self): - self._enable_boot0(True) - self._reset() - self.connection.write(bytearray([self.Command.SYNCHRONIZE])) - return self._wait_for_ack("Syncro") - - def reset_from_flash(self): - self._enable_boot0(False) - self._reset() - - def command(self, command): - command_byte = bytearray([command]) - control_byte = bytearray([command ^ 0xFF]) - - self.connection.write(command_byte) - self.connection.write(control_byte) - - return self._wait_for_ack(hex(command)) - - def get(self): - if not self.command(self.Command.GET): - raise CommandException("Get (0x00) failed") - self.debug(10, "*** Get command") - length = bytearray(self.connection.read())[0] - version = bytearray(self.connection.read())[0] - self.debug(10, " Bootloader version: " + hex(version)) - data = bytearray(self.connection.read(length)) - if self.Command.EXTENDED_ERASE in data: - self.extended_erase = True - self.debug(10, " Available commands: " + ", ".join(hex(b) for b in data)) - self._wait_for_ack("0x00 end") - return version - - def get_version(self): - if not self.command(self.Command.GET_VERSION): - raise CommandException("GetVersion (0x01) failed") - - self.debug(10, "*** GetVersion command") - version = bytearray(self.connection.read())[0] - self.connection.read(2) - self._wait_for_ack("0x01 end") - self.debug(10, " Bootloader version: " + hex(version)) - return version - - def get_id(self): - if not self.command(self.Command.GET_ID): - raise CommandException("GetID (0x02) failed") - - self.debug(10, "*** GetID command") - length = bytearray(self.connection.read())[0] - id_data = bytearray(self.connection.read(length + 1)) - self._wait_for_ack("0x02 end") - _device_id = reduce(lambda x, y: x * 0x100 + y, id_data) - return _device_id - - def get_flash_size(self, device_family): - flash_size_address = self.FLASH_SIZE_ADDRESS[device_family] - flash_size_bytes = self.read_memory(flash_size_address, 2) - flash_size = flash_size_bytes[0] + flash_size_bytes[1] * 256 - return flash_size - - def get_uid(self, device_id): - uid_address = self.UID_ADDRESS[device_id] - uid = self.read_memory(uid_address, 12) - return uid - - @staticmethod - def format_uid(uid): - swapped_data = [[uid[b] for b in part] for part in Stm32Bootloader.UID_SWAP] - uid_string = "-".join("".join(format(b, "02X") for b in part) for part in swapped_data) - return uid_string - - def read_memory(self, address, length): - assert length <= 256 - if not self.command(self.Command.READ_MEMORY): - raise CommandException("ReadMemory (0x11) failed") - - self.debug(10, "*** ReadMemory command") - self.connection.write(self._encode_address(address)) - self._wait_for_ack("0x11 address failed") - nr_of_bytes = (length - 1) & 0xFF - checksum = nr_of_bytes ^ 0xFF - self.connection.write(bytearray([nr_of_bytes, checksum])) - self._wait_for_ack("0x11 length failed") - return bytearray(self.connection.read(length)) - - def go(self, address): - # pylint: disable=invalid-name - if not self.command(self.Command.GO): - raise CommandException("Go (0x21) failed") - - self.debug(10, "*** Go command") - self.connection.write(self._encode_address(address)) - self._wait_for_ack("0x21 go failed") - - def write_memory(self, address, data): - nr_of_bytes = len(data) - assert nr_of_bytes <= 256 - - if not self.command(self.Command.WRITE_MEMORY): - raise CommandException("Write memory (0x31) failed") - - self.debug(10, "*** Write memory command") - self.connection.write(self._encode_address(address)) - self._wait_for_ack("0x31 address failed") - - # pad data length to multiple of 4 bytes - if nr_of_bytes % 4 != 0: - padding_bytes = 4 - (nr_of_bytes % 4) - nr_of_bytes += padding_bytes - # append value 0xFF: flash memory value after erase - data = bytearray(data) - data.extend([0xFF] * padding_bytes) - - self.debug(10, " %s bytes to write" % [nr_of_bytes]) - self.connection.write(bytearray([nr_of_bytes - 1])) - checksum = nr_of_bytes - 1 - for data_byte in data: - checksum = checksum ^ data_byte - self.connection.write(bytearray(data)) - self.connection.write(bytearray([checksum])) - self._wait_for_ack("0x31 programming failed") - self.debug(10, " Write memory done") - - def erase_memory(self, sectors=None): - if self.extended_erase: - self.extended_erase_memory() - return - - if not self.command(self.Command.ERASE): - raise CommandException("Erase memory (0x43) failed") - - self.debug(10, "*** Erase memory command") - - if sectors: - self._page_erase(sectors) - else: - self._global_erase() - self._wait_for_ack("0x43 erase failed") - self.debug(10, " Erase memory done") - - def extended_erase_memory(self): - if not self.command(self.Command.EXTENDED_ERASE): - raise CommandException("Extended Erase memory (0x44) failed") - - self.debug(10, "*** Extended Erase memory command") - # Global mass erase and checksum byte - self.connection.write(b"\xFF") - self.connection.write(b"\xFF") - self.connection.write(b"\x00") - previous_timeout_value = self.connection.timeout - self.connection.timeout = 30 - print("Extended erase (0x44), this can take ten seconds or more") - self._wait_for_ack("0x44 erasing failed") - self.connection.timeout = previous_timeout_value - self.debug(10, " Extended Erase memory done") - - def write_protect(self, pages): - if not self.command(self.Command.WRITE_PROTECT): - raise CommandException("Write Protect memory (0x63) failed") - - self.debug(10, "*** Write protect command") - nr_of_pages = (len(pages) - 1) & 0xFF - self.connection.write(bytearray([nr_of_pages])) - checksum = 0xFF - for page_index in pages: - checksum = checksum ^ page_index - self.connection.write(bytearray([page_index])) - self.connection.write(bytearray([checksum])) - self._wait_for_ack("0x63 write protect failed") - self.debug(10, " Write protect done") - - def write_unprotect(self): - if not self.command(self.Command.WRITE_UNPROTECT): - raise CommandException("Write Unprotect (0x73) failed") - - self.debug(10, "*** Write Unprotect command") - self._wait_for_ack("0x73 write unprotect failed") - self.debug(10, " Write Unprotect done") - - def readout_protect(self): - if not self.command(self.Command.READOUT_PROTECT): - raise CommandException("Readout protect (0x82) failed") - - self.debug(10, "*** Readout protect command") - self._wait_for_ack("0x82 readout protect failed") - self.debug(10, " Read protect done") - - def readout_unprotect(self): - if not self.command(self.Command.READOUT_UNPROTECT): - raise CommandException("Readout unprotect (0x92) failed") - - self.debug(10, "*** Readout Unprotect command") - self._wait_for_ack("0x92 readout unprotect failed") - self.debug(20, " Mass erase -- this may take a while") - time.sleep(20) - self.debug(20, " Unprotect / mass erase done") - self.debug(20, " Reset after automatic chip reset due to readout unprotect") - self.reset_from_system_memory() - - def read_memory_data(self, address, length): - data = bytearray() - while length > 256: - self.debug( - 5, "Read %(len)d bytes at 0x%(address)X" % {"address": address, "len": 256} - ) - data = data + self.read_memory(address, 256) - address = address + 256 - length = length - 256 - if length: - self.debug( - 5, "Read %(len)d bytes at 0x%(address)X" % {"address": address, "len": length} - ) - data = data + self.read_memory(address, length) - return data - - def write_memory_data(self, address, data): - length = len(data) - offset = 0 - while length > 256: - self.debug( - 5, "Write %(len)d bytes at 0x%(address)X" % {"address": address, "len": 256} - ) - self.write_memory(address, data[offset : offset + 256]) - offset += 256 - address += 256 - length -= 256 - if length: - self.debug( - 5, "Write %(len)d bytes at 0x%(address)X" % {"address": address, "len": length} - ) - self.write_memory(address, data[offset : offset + length]) - - def _global_erase(self): - # global erase: n=255, see ST AN3155 - self.connection.write(b"\xff") - self.connection.write(b"\x00") - - def _page_erase(self, pages): - # page erase, see ST AN3155 - nr_of_pages = (len(pages) - 1) & 0xFF - self.connection.write(bytearray([nr_of_pages])) - checksum = nr_of_pages - for page_number in pages: - self.connection.write(bytearray([page_number])) - checksum = checksum ^ page_number - self.connection.write(bytearray([checksum])) - - def _reset(self): - if not self._toggle_reset: - return - self.connection.enable_reset(True) - time.sleep(0.1) - self.connection.enable_reset(False) - time.sleep(0.5) - - def _enable_boot0(self, enable=True): - if not self._toggle_boot0: - return - - self.connection.enable_boot0(enable) - - def _wait_for_ack(self, info=""): - try: - ack = bytearray(self.connection.read())[0] - except TypeError: - raise CommandException("Can't read port or timeout") - - if ack == self.Reply.NACK: - raise CommandException("NACK " + info) - - if ack != self.Reply.ACK: - raise CommandException("Unknown response. " + info + ": " + hex(ack)) - - return 1 - - @staticmethod - def _encode_address(address): - byte3 = (address >> 0) & 0xFF - byte2 = (address >> 8) & 0xFF - byte1 = (address >> 16) & 0xFF - byte0 = (address >> 24) & 0xFF - checksum = byte0 ^ byte1 ^ byte2 ^ byte3 - return bytearray([byte0, byte1, byte2, byte3, checksum]) - class Stm32Loader: """Main application: parse arguments and handle commands.""" @@ -533,9 +130,7 @@ def parse_arguments(self, arguments): def connect(self): serial_connection = SerialConnection( - self.configuration["port"], - self.configuration["baud"], - self.configuration["parity"], + self.configuration["port"], self.configuration["baud"], self.configuration["parity"] ) self.debug( 10, From 97357a469572d584b37a11bad250793b41a3e6c4 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Apr 2019 22:15:52 +0200 Subject: [PATCH 098/369] Allow to run main() from code --- stm32loader/stm32loader.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/stm32loader/stm32loader.py b/stm32loader/stm32loader.py index bf634c3..9385618 100644 --- a/stm32loader/stm32loader.py +++ b/stm32loader/stm32loader.py @@ -266,10 +266,17 @@ def read_device_details(self): self.debug(0, "Flash size: %d KiB" % flash_size) -def main(): - """Parse arguments and execute tasks.""" +def main(arguments=None): + """ + Parse arguments and execute tasks. + + If no arguments are supplied, use the ones from + sys.argv. + """ loader = Stm32Loader() - loader.parse_arguments(sys.argv[1:]) + if arguments is None: + arguments = sys.argv[1] + loader.parse_arguments(arguments) loader.connect() try: loader.read_device_details() From 3ac8d8c188ee375d9bd2e808538bc036dc376ee2 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Apr 2019 22:15:53 +0200 Subject: [PATCH 099/369] Boilerplate: improve header text --- stm32loader/stm32loader.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/stm32loader/stm32loader.py b/stm32loader/stm32loader.py index 9385618..3e4b549 100644 --- a/stm32loader/stm32loader.py +++ b/stm32loader/stm32loader.py @@ -1,9 +1,6 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -# vim: sw=4:ts=4:si:et:enc=utf-8 - -# Author: Ivan A-R -# GitHub repository: https://github.com/jsnyder/stm32loader +# Authors: Ivan A-R, Floris Lambrechts +# GitHub repository: https://github.com/florisla/stm32loader # # This file is part of stm32loader. # @@ -18,7 +15,7 @@ # for more details. # # You should have received a copy of the GNU General Public License -# along with stm32loader; see the file COPYING. If not see +# along with stm32loader; see the file LICENSE. If not see # . """Flash firmware to STM32 microcontrollers over an RS-232 serial connection.""" From e2d4f840efc626dd5b5a005b27500f505ee6964e Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Apr 2019 22:15:54 +0200 Subject: [PATCH 100/369] Add header to __main__.py --- stm32loader/__main__.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/stm32loader/__main__.py b/stm32loader/__main__.py index 6966c31..5a8f2ac 100644 --- a/stm32loader/__main__.py +++ b/stm32loader/__main__.py @@ -1,3 +1,21 @@ +# Author: Floris Lambrechts +# GitHub repository: https://github.com/florisla/stm32loader +# +# This file is part of stm32loader. +# +# stm32loader is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation; either version 3, or (at your option) any later +# version. +# +# stm32loader is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# for more details. +# +# You should have received a copy of the GNU General Public License +# along with stm32loader; see the file LICENSE. If not see +# . """ Execute stm32loader as a module. From ce92417380a7b44b422969466922b641044cf200 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Apr 2019 22:15:55 +0200 Subject: [PATCH 101/369] Use relative imports This is more flexible when not installed as a proper package. --- stm32loader/stm32loader.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stm32loader/stm32loader.py b/stm32loader/stm32loader.py index 3e4b549..b662459 100644 --- a/stm32loader/stm32loader.py +++ b/stm32loader/stm32loader.py @@ -26,8 +26,8 @@ import getopt import sys -from stm32loader.bootloader import CHIP_IDS, CommandException, Stm32Bootloader -from stm32loader.serial import SerialConnection +from .bootloader import CHIP_IDS, CommandException, Stm32Bootloader +from .serial import SerialConnection DEFAULT_VERBOSITY = 5 From de52681a42367ff73bd271265c004f7c2ad84ffb Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Apr 2019 22:15:56 +0200 Subject: [PATCH 102/369] Include all config files in the sdist package --- MANIFEST.in | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index e2e577a..fe8aed8 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1,5 @@ -include README.md LICENSE CHANGELOG.md +include *.md +include *.cfg +include *.toml +include LICENSE +include .pylintrc From fd72ce8a3c1a7bc3a571d14c54a4cc42b1dc45e4 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Apr 2019 22:15:57 +0200 Subject: [PATCH 103/369] Update docs for upcoming release v0.4.0 --- CHANGELOG.md | 11 ++++++++--- README.md | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f72e7f..e479cd3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,12 @@ -vnext -===== -* Add support for STM32F7 mcus. By sam-bristow. +v0.4.0 +====== +* #8: Add support for STM32F7 mcus. By sam-bristow. +* #9: Support data writes smaller than 256 bytes. By NINI1988. +* Start using code linting and unit tests. +* Allow to use stm32loader.main() as a library function. +* Allow to use Stm32Bootloader as a library class. +* Allow a custom connection to be used (e.g. I2C instead of UART). v0.3.3 ====== diff --git a/README.md b/README.md index 10abb0c..82cdfed 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ Acknowledgement Original Version by Ivan A-R (tuxotronic.org). Contributions by Domen Puncer, James Snyder, Floris Lambrechts, -Atokulus, sam-bristow. +Atokulus, sam-bristow, NINI1988. Inspiration for features from: From e7bd97915c037632cc9679cdf0e2e0b1d000dfe6 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Apr 2019 22:15:58 +0200 Subject: [PATCH 104/369] Ignore missing docstrings for now --- .pylintrc | 1 + README.md | 1 + setup.cfg | 1 + 3 files changed, 3 insertions(+) diff --git a/.pylintrc b/.pylintrc index 6db7898..20d8cdb 100644 --- a/.pylintrc +++ b/.pylintrc @@ -5,6 +5,7 @@ max-line-length=98 disable= fixme, # TO DOs are not errors bad-continuation, # be compatible to black + missing-docstring, [REPORT] score=no diff --git a/README.md b/README.md index 82cdfed..108bd5a 100644 --- a/README.md +++ b/README.md @@ -123,4 +123,5 @@ Future work * Drop Python2 compatibility? * Add unit tests * Use nox or tox for checking Python version compatibility +* Add docstrings to all public methods * Use Travis or Azure pipelines for CI diff --git a/setup.cfg b/setup.cfg index 0d874e4..3ce440a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -23,4 +23,5 @@ ignore = # be compatible to black C812, # Missing trailing comma E203, # Whitespace before ':' + D102, # Missing docstring in public method From 95f2336ef8dee51dab3d60b439ca55dd69c3ef66 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Apr 2019 22:15:59 +0200 Subject: [PATCH 105/369] Start using nox to test over all supported Python versions --- .gitignore | 1 + MANIFEST.in | 1 + README.md | 6 +----- noxfile.py | 45 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 48 insertions(+), 5 deletions(-) create mode 100644 noxfile.py diff --git a/.gitignore b/.gitignore index 849fcfe..056325d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ dist/ __pycache__/ *.py[cod] +.nox/ diff --git a/MANIFEST.in b/MANIFEST.in index fe8aed8..e7b7bc8 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,3 +3,4 @@ include *.cfg include *.toml include LICENSE include .pylintrc +include noxfile.py diff --git a/README.md b/README.md index 108bd5a..d73b092 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ ST Microelectronics STM32 microcontrollers over UART. Also supports ST BlueNRG devices, and the SweetPeas bootloader for Wiznet W7500. -Compatible with Python version 3.2 to 3.7 or 2.7. +Compatible with Python version 3.4 to 3.7 or 2.7. Usage @@ -118,10 +118,6 @@ Not currently supported Future work ----------- -* Use intenum for command and reply * Use proper logging instead of print statements -* Drop Python2 compatibility? -* Add unit tests -* Use nox or tox for checking Python version compatibility * Add docstrings to all public methods * Use Travis or Azure pipelines for CI diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 0000000..11156f9 --- /dev/null +++ b/noxfile.py @@ -0,0 +1,45 @@ +"""Run unit tests in a fresh virtualenv using nox.""" + +from shutil import rmtree + +import nox + + +@nox.session(python=["2.7", "3.4", "3.5", "3.6", "3.7"]) +def tests(session): + """ + Install stm32loader package and execute unit tests. + + Use chdir to move off of the current folder, so that + 'import stm32loader' imports the *installed* package, not + the local one from the repo. + """ + # setuptools does not like multiple .whl packages being present + # see https://github.com/pypa/setuptools/issues/1671 + rmtree("./dist", ignore_errors=True) + session.install(".") + session.install("pytest") + if session.python == "2.7": + session.install("mock") + session.chdir("tests") + session.run("pytest", "./") + + +@nox.session(python=["3.6"]) +def lint(session): + """ + Run code verification tools flake8, pylint and black. + + Do this in order of expected failures for performance reasons. + """ + session.install("black") + session.run("black", "--check", "stm32loader") + + session.install("pylint") + # pyserial for avoiding a complaint by pylint + session.install("pyserial") + session.run("pylint", "stm32loader") + + session.install("flake8", "flake8-isort") + # not sure why this needs an explicit --config + session.run("flake8", "stm32loader", "--config=setup.cfg") From 66e7712af1ff9f360131a158d3edb5a88e0afcf9 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Apr 2019 22:16:00 +0200 Subject: [PATCH 106/369] Add first unit tests --- MANIFEST.in | 1 + setup.cfg | 3 +++ stm32loader/bootloader.py | 2 ++ tests/test_bootloader.py | 55 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 61 insertions(+) create mode 100644 tests/test_bootloader.py diff --git a/MANIFEST.in b/MANIFEST.in index e7b7bc8..832d124 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,3 +4,4 @@ include *.toml include LICENSE include .pylintrc include noxfile.py +recursive-include tests *.py diff --git a/setup.cfg b/setup.cfg index 3ce440a..f745dd4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -24,4 +24,7 @@ ignore = C812, # Missing trailing comma E203, # Whitespace before ':' D102, # Missing docstring in public method +per-file-ignores = + # Missing docstring in public function + tests/*:D103, diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index 8bf9529..2e3396a 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -249,6 +249,8 @@ def go(self, address): def write_memory(self, address, data): nr_of_bytes = len(data) + if nr_of_bytes == 0: + return assert nr_of_bytes <= 256 if not self.command(self.Command.WRITE_MEMORY): diff --git a/tests/test_bootloader.py b/tests/test_bootloader.py new file mode 100644 index 0000000..8b3f230 --- /dev/null +++ b/tests/test_bootloader.py @@ -0,0 +1,55 @@ + +from stm32loader.bootloader import Stm32Bootloader + +import pytest + +try: + from unittest.mock import MagicMock +except ImportError: + # Python version <= 3.2 + from mock import MagicMock + + +@pytest.fixture +def connection(): + connection = MagicMock() + connection.read.return_value = [Stm32Bootloader.Reply.ACK] + return connection + + +@pytest.fixture +def write(connection): + def write_called_with_data(data): + data_call = (bytearray(data), ) + return 1 == sum(args[0] == data_call for args in connection.write.call_args_list) + connection.write.called_with_data = write_called_with_data + return connection.write + + +@pytest.fixture +def bootloader(connection): + return Stm32Bootloader(connection) + + +def test_constructor_with_connection_None_passes(): + Stm32Bootloader(connection=None) + + +def test_constructor_does_not_use_connection_directly(connection): + Stm32Bootloader(connection) + assert not connection.method_calls + + +def test_write_memory_with_zero_bytes_does_not_send_anything(bootloader, connection): + bootloader.write_memory(0, b'') + assert not connection.method_calls + + +def test_write_memory_with_single_byte_sends_four_data_bytes_padded_with_0xff(bootloader, write): + bootloader.write_memory(0, b'1') + assert write.called_with_data(b'1\xff\xff\xff') + + +def test_encode_address_returns_correct_bytes_with_checksum(): + encoded_address = Stm32Bootloader._encode_address(0x04030201) + assert bytes(encoded_address) == b'\x04\x03\x02\x01\x04' From c7dc471563e0d9a3a61427c4eb6577bc8cb2ea28 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Apr 2019 22:16:01 +0200 Subject: [PATCH 107/369] Cosmetic: satisfy flake max-doc-length It seems a previous flake8 version was not properly checking this. --- stm32loader/__init__.py | 2 +- stm32loader/__main__.py | 6 +++--- stm32loader/bootloader.py | 13 ++++++------- stm32loader/serial.py | 13 +++++++------ stm32loader/stm32loader.py | 8 ++++---- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/stm32loader/__init__.py b/stm32loader/__init__.py index 53cfeb4..c696ecb 100644 --- a/stm32loader/__init__.py +++ b/stm32loader/__init__.py @@ -1,4 +1,4 @@ -"""Flash firmware to STM32 microcontrollers over an RS-232 serial connection.""" +"""Flash firmware to STM32 microcontrollers over a serial connection.""" __version_info__ = (0, 3, 3, "dev") __version__ = "-".join(str(part) for part in __version_info__).replace("-", ".", 2) diff --git a/stm32loader/__main__.py b/stm32loader/__main__.py index 5a8f2ac..9b50634 100644 --- a/stm32loader/__main__.py +++ b/stm32loader/__main__.py @@ -8,9 +8,9 @@ # Software Foundation; either version 3, or (at your option) any later # version. # -# stm32loader is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# stm32loader is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License # for more details. # # You should have received a copy of the GNU General Public License diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index 2e3396a..16ea4c6 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -8,16 +8,16 @@ # Software Foundation; either version 3, or (at your option) any later # version. # -# stm32loader is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# stm32loader is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License # for more details. # # You should have received a copy of the GNU General Public License # along with stm32loader; see the file LICENSE. If not see # . -"""Flash firmware to STM32 microcontrollers over an RS-232 serial connection.""" +"""Talk to an STM32 native bootloader (see ST AN3155).""" from __future__ import print_function @@ -58,8 +58,6 @@ class CommandException(IOError): """Error: a command in the STM32 native bootloader failed.""" - pass - class Stm32Bootloader: """Talk to the STM32 native bootloader.""" @@ -140,7 +138,8 @@ def __init__(self, connection, verbosity=5): but a straight pyserial serial.Serial object can also be used. - :param connection: Object supporting read() and write(). E.g. serial.Serial(). + :param connection: Object supporting read() and write(). + E.g. serial.Serial(). :param int verbosity: Verbosity level. 0 is quite, 10 is verbose. """ self.connection = connection diff --git a/stm32loader/serial.py b/stm32loader/serial.py index 060a4e1..7d42f84 100644 --- a/stm32loader/serial.py +++ b/stm32loader/serial.py @@ -8,9 +8,9 @@ # Software Foundation; either version 3, or (at your option) any later # version. # -# stm32loader is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# stm32loader is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License # for more details. # # You should have received a copy of the GNU General Public License @@ -27,7 +27,7 @@ class SerialConnection: - """Wrap a serial.Serial connection and offer enable_reset and enable_boot0.""" + """Wrap a serial.Serial connection and toggle reset and boot0.""" # pylint: disable=too-many-instance-attributes @@ -72,8 +72,9 @@ def read(self, *args, **kwargs): def enable_reset(self, enable=True): # reset on the STM32 is active low (0 Volt puts the MCU in reset) - # but the RS-232 DTR signal is active low by itself, so it inverts this - # (writing a logical 1 outputs a low voltage == reset enabled) + # but the RS-232 DTR signal is active low by itself, so it + # inverts this (writing a logical 1 outputs a low voltage, i.e. + # enables reset) level = int(enable) if self.reset_active_high: level = 1 - level diff --git a/stm32loader/stm32loader.py b/stm32loader/stm32loader.py index b662459..332e69c 100644 --- a/stm32loader/stm32loader.py +++ b/stm32loader/stm32loader.py @@ -9,16 +9,16 @@ # Software Foundation; either version 3, or (at your option) any later # version. # -# stm32loader is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# stm32loader is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License # for more details. # # You should have received a copy of the GNU General Public License # along with stm32loader; see the file LICENSE. If not see # . -"""Flash firmware to STM32 microcontrollers over an RS-232 serial connection.""" +"""Flash firmware to STM32 microcontrollers over a serial connection.""" from __future__ import print_function From f1be8de9219dfd35c4668fc13789cb172f3052de Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Apr 2019 22:16:02 +0200 Subject: [PATCH 108/369] Cleanup: satisfy pylint --- stm32loader/stm32loader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stm32loader/stm32loader.py b/stm32loader/stm32loader.py index 332e69c..df1449b 100644 --- a/stm32loader/stm32loader.py +++ b/stm32loader/stm32loader.py @@ -156,7 +156,7 @@ def connect(self): try: self.bootloader.reset_from_system_memory() - except BaseException: + except CommandException: print("Can't init into bootloader. Ensure that BOOT0 is enabled and reset device.") self.bootloader.reset_from_flash() sys.exit(1) From 0006167a2ce8081697e27b458f675fd2e7341fc2 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Apr 2019 22:16:03 +0200 Subject: [PATCH 109/369] Use struct.pack instead of manually shifting bits --- stm32loader/bootloader.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index 16ea4c6..8502911 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -22,6 +22,8 @@ from __future__ import print_function +import operator +import struct import sys import time from functools import reduce @@ -431,9 +433,6 @@ def _wait_for_ack(self, info=""): @staticmethod def _encode_address(address): - byte3 = (address >> 0) & 0xFF - byte2 = (address >> 8) & 0xFF - byte1 = (address >> 16) & 0xFF - byte0 = (address >> 24) & 0xFF - checksum = byte0 ^ byte1 ^ byte2 ^ byte3 - return bytearray([byte0, byte1, byte2, byte3, checksum]) + address_bytes = bytearray(struct.pack(">I", address)) + checksum_byte = struct.pack("B", reduce(operator.xor, address_bytes)) + return address_bytes + checksum_byte From f2bd30b7fec3e21560b91628c5c1d7b376b796a9 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Apr 2019 22:16:04 +0200 Subject: [PATCH 110/369] Add convenience methods write() and write_and_ack() _page_erase and _global_erase have become so small that they're removed. --- stm32loader/bootloader.py | 89 +++++++++++++++------------------------ 1 file changed, 34 insertions(+), 55 deletions(-) diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index 8502911..7a41646 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -133,13 +133,12 @@ def __init__(self, connection, verbosity=5): The supplied connection can be any object that supports read() and write(). Optionally, it may also offer - enable_reset() and enable_boot0; it should advertize this by + enable_reset() and enable_boot0(); it should advertise this by setting TOGGLES_RESET and TOGGLES_BOOT0 to True. The default implementation is stm32loader.connection.SerialConnection, but a straight pyserial serial.Serial object can also be used. - :param connection: Object supporting read() and write(). E.g. serial.Serial(). :param int verbosity: Verbosity level. 0 is quite, 10 is verbose. @@ -149,6 +148,18 @@ def __init__(self, connection, verbosity=5): self._toggle_boot0 = getattr(connection, "can_toggle_boot0", False) self.verbosity = verbosity + def write(self, *data): + for data_bytes in data: + if isinstance(data_bytes, int): + data_bytes = struct.pack("B", data_bytes) + self.connection.write(data_bytes) + + def write_and_ack(self, message, *data): + # this is a separate method from write() because a keyword + # argument after *args is not possible in Python 2 + self.write(*data) + return self._wait_for_ack(message) + def debug(self, level, message): if self.verbosity >= level: print(message, file=sys.stderr) @@ -156,21 +167,14 @@ def debug(self, level, message): def reset_from_system_memory(self): self._enable_boot0(True) self._reset() - self.connection.write(bytearray([self.Command.SYNCHRONIZE])) - return self._wait_for_ack("Syncro") + return self.write_and_ack("Synchro", self.Command.SYNCHRONIZE) def reset_from_flash(self): self._enable_boot0(False) self._reset() def command(self, command): - command_byte = bytearray([command]) - control_byte = bytearray([command ^ 0xFF]) - - self.connection.write(command_byte) - self.connection.write(control_byte) - - return self._wait_for_ack(hex(command)) + return self.write_and_ack("Command", command, command ^ 0xFF) def get(self): if not self.command(self.Command.GET): @@ -231,12 +235,10 @@ def read_memory(self, address, length): raise CommandException("ReadMemory (0x11) failed") self.debug(10, "*** ReadMemory command") - self.connection.write(self._encode_address(address)) - self._wait_for_ack("0x11 address failed") + self.write_and_ack("0x11 address failed", self._encode_address(address)) nr_of_bytes = (length - 1) & 0xFF checksum = nr_of_bytes ^ 0xFF - self.connection.write(bytearray([nr_of_bytes, checksum])) - self._wait_for_ack("0x11 length failed") + self.write_and_ack("0x11 length failed", nr_of_bytes, checksum) return bytearray(self.connection.read(length)) def go(self, address): @@ -245,8 +247,7 @@ def go(self, address): raise CommandException("Go (0x21) failed") self.debug(10, "*** Go command") - self.connection.write(self._encode_address(address)) - self._wait_for_ack("0x21 go failed") + self.write_and_ack("0x21 go failed", self._encode_address(address)) def write_memory(self, address, data): nr_of_bytes = len(data) @@ -258,8 +259,7 @@ def write_memory(self, address, data): raise CommandException("Write memory (0x31) failed") self.debug(10, "*** Write memory command") - self.connection.write(self._encode_address(address)) - self._wait_for_ack("0x31 address failed") + self.write_and_ack("0x31 address failed", self._encode_address(address)) # pad data length to multiple of 4 bytes if nr_of_bytes % 4 != 0: @@ -270,17 +270,14 @@ def write_memory(self, address, data): data.extend([0xFF] * padding_bytes) self.debug(10, " %s bytes to write" % [nr_of_bytes]) - self.connection.write(bytearray([nr_of_bytes - 1])) - checksum = nr_of_bytes - 1 - for data_byte in data: - checksum = checksum ^ data_byte - self.connection.write(bytearray(data)) - self.connection.write(bytearray([checksum])) - self._wait_for_ack("0x31 programming failed") + checksum = reduce(operator.xor, data, nr_of_bytes - 1) + self.write_and_ack("0x31 programming failed", nr_of_bytes - 1, data, checksum) self.debug(10, " Write memory done") def erase_memory(self, sectors=None): if self.extended_erase: + if sectors: + raise ValueError("Extended erase can not erase specific sectors: %s." % sectors) self.extended_erase_memory() return @@ -290,9 +287,14 @@ def erase_memory(self, sectors=None): self.debug(10, "*** Erase memory command") if sectors: - self._page_erase(sectors) + # page erase, see ST AN3155 + page_count = len(sectors) + checksum = reduce(operator.xor, sectors, page_count) + self.write(page_count, sectors, checksum) else: - self._global_erase() + # global erase: n=255 (page count) + self.write(255, 0) + self._wait_for_ack("0x43 erase failed") self.debug(10, " Erase memory done") @@ -302,9 +304,7 @@ def extended_erase_memory(self): self.debug(10, "*** Extended Erase memory command") # Global mass erase and checksum byte - self.connection.write(b"\xFF") - self.connection.write(b"\xFF") - self.connection.write(b"\x00") + self.write(b'\xff\xff\x00') previous_timeout_value = self.connection.timeout self.connection.timeout = 30 print("Extended erase (0x44), this can take ten seconds or more") @@ -318,13 +318,8 @@ def write_protect(self, pages): self.debug(10, "*** Write protect command") nr_of_pages = (len(pages) - 1) & 0xFF - self.connection.write(bytearray([nr_of_pages])) - checksum = 0xFF - for page_index in pages: - checksum = checksum ^ page_index - self.connection.write(bytearray([page_index])) - self.connection.write(bytearray([checksum])) - self._wait_for_ack("0x63 write protect failed") + checksum = reduce(operator.xor, pages, nr_of_pages) + self.write_and_ack("0x63 write protect failed", nr_of_pages, pages, checksum) self.debug(10, " Write protect done") def write_unprotect(self): @@ -388,21 +383,6 @@ def write_memory_data(self, address, data): ) self.write_memory(address, data[offset : offset + length]) - def _global_erase(self): - # global erase: n=255, see ST AN3155 - self.connection.write(b"\xff") - self.connection.write(b"\x00") - - def _page_erase(self, pages): - # page erase, see ST AN3155 - nr_of_pages = (len(pages) - 1) & 0xFF - self.connection.write(bytearray([nr_of_pages])) - checksum = nr_of_pages - for page_number in pages: - self.connection.write(bytearray([page_number])) - checksum = checksum ^ page_number - self.connection.write(bytearray([checksum])) - def _reset(self): if not self._toggle_reset: return @@ -425,8 +405,7 @@ def _wait_for_ack(self, info=""): if ack == self.Reply.NACK: raise CommandException("NACK " + info) - - if ack != self.Reply.ACK: + elif ack != self.Reply.ACK: raise CommandException("Unknown response. " + info + ": " + hex(ack)) return 1 From 0c73a36ff7f427d5fd438b7948968d69d17293a7 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Apr 2019 22:16:05 +0200 Subject: [PATCH 111/369] Add unit tests for write() and methods that use it --- tests/test_bootloader.py | 109 +++++++++++++++++++++++++++++++++++---- 1 file changed, 99 insertions(+), 10 deletions(-) diff --git a/tests/test_bootloader.py b/tests/test_bootloader.py index 8b3f230..27f8b10 100644 --- a/tests/test_bootloader.py +++ b/tests/test_bootloader.py @@ -1,8 +1,9 @@ - -from stm32loader.bootloader import Stm32Bootloader +"""Unit tests for the Stm32Loader class.""" import pytest +from stm32loader.bootloader import CommandException, Stm32Bootloader + try: from unittest.mock import MagicMock except ImportError: @@ -19,10 +20,16 @@ def connection(): @pytest.fixture def write(connection): - def write_called_with_data(data): - data_call = (bytearray(data), ) - return 1 == sum(args[0] == data_call for args in connection.write.call_args_list) - connection.write.called_with_data = write_called_with_data + connection.write.written_data = bytearray() + + def log_written_data(data): + connection.write.written_data.extend(data) + + def data_was_written(data): + return data in connection.write.written_data + + connection.write.data_was_written = data_was_written + connection.write.side_effect = log_written_data return connection.write @@ -40,16 +47,98 @@ def test_constructor_does_not_use_connection_directly(connection): assert not connection.method_calls +def test_write_without_data_sends_no_bytes(bootloader, write): + bootloader.write() + assert len(write.written_data) == 0 + + +def test_write_with_bytes_sends_bytes_verbatim(bootloader, write): + bootloader.write(b'\x00\x11') + assert write.data_was_written(b'\x00\x11') + + +def test_write_with_integers_sends_integers_as_bytes(bootloader, write): + bootloader.write(0x03, 0x0a) + assert write.data_was_written(b'\x03\x0a') + + +def test_write_and_ack_with_nack_response_raises_commandexception(bootloader): + bootloader.connection.read = MagicMock() + bootloader.connection.read.return_value = [Stm32Bootloader.Reply.NACK] + with pytest.raises(CommandException, match="custom message"): + bootloader.write_and_ack("custom message", 0x00) + + def test_write_memory_with_zero_bytes_does_not_send_anything(bootloader, connection): - bootloader.write_memory(0, b'') + bootloader.write_memory(0, b"") assert not connection.method_calls def test_write_memory_with_single_byte_sends_four_data_bytes_padded_with_0xff(bootloader, write): - bootloader.write_memory(0, b'1') - assert write.called_with_data(b'1\xff\xff\xff') + bootloader.write_memory(0, b"1") + assert write.data_was_written(b"1\xff\xff\xff") + + +def test_write_memory_sends_correct_number_of_bytes(bootloader, write): + bootloader.write_memory(0, bytearray([0] * 4)) + # command byte, control byte, 4 address bytes, address checksum, + # length byte, 4 data bytes, checksum byte + byte_count = 2 + 4 + 1 + 1 + 4 + 1 + assert len(write.written_data) == byte_count + + +def test_read_memory_sends_address_with_checksum(bootloader, write): + bootloader.read_memory(0x0f, 4) + assert write.data_was_written(b'\x00\x00\x00\x0f\x0f') + + +def test_read_memory_sends_length_with_checksum(bootloader, write): + bootloader.read_memory(0, 0x0f + 1) + assert write.data_was_written(b'\x0f\xf0') + + +def test_read_memory_sends_length_with_checksum(bootloader, write): + bootloader.read_memory(0, 0x0f + 1) + assert write.data_was_written(b'\x0f\xf0') + + +def test_command_sends_command_and_control_bytes(bootloader, write): + bootloader.command(0x01) + assert write.data_was_written(b"\x01\xfe") + + +def test_reset_from_system_memory_sends_command_synchronize(bootloader, write): + bootloader.reset_from_system_memory() + synchro_command = Stm32Bootloader.Command.SYNCHRONIZE + assert write.data_was_written(bytearray([synchro_command])) def test_encode_address_returns_correct_bytes_with_checksum(): encoded_address = Stm32Bootloader._encode_address(0x04030201) - assert bytes(encoded_address) == b'\x04\x03\x02\x01\x04' + assert bytes(encoded_address) == b"\x04\x03\x02\x01\x04" + + +def test_erase_memory_without_sectors_sends_global_erase(bootloader, write): + bootloader.erase_memory() + assert write.data_was_written(b'\xff\x00') + + +def test_erase_memory_with_sectors_sends_sector_addresses_with_(bootloader, write): + bootloader.erase_memory([0x01, 0x02, 0x04, 0x08]) + assert write.data_was_written(b'\x01\x02\x04\x08\x0b') + + +def test_erase_memory_with_extended_erase_enabled_and_specific_sectors_raises_error(bootloader): + bootloader.extended_erase = True + with pytest.raises(ValueError, match="Extended erase .* specific sectors.*14, 16"): + bootloader.erase_memory([14, 16]) + + +def test_extended_erase_memory_sends_global_mass_erase(bootloader, write): + bootloader.extended_erase_memory() + assert write.data_was_written(b'\xff\xff\x00') + + +def test_write_protect_sends_page_addresses_and_checksum(bootloader, write): + bootloader.write_protect([0x01, 0x08]) + assert write.data_was_written(b'\x01\x08\x08') From 5583c365d7d1a44cea473c3b6523e7fa9b618ab3 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Apr 2019 22:16:05 +0200 Subject: [PATCH 112/369] Shorten command-calling code: move raise to command() --- stm32loader/bootloader.py | 69 ++++++++++----------------------------- tests/test_bootloader.py | 2 +- 2 files changed, 18 insertions(+), 53 deletions(-) diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index 7a41646..4796f83 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -173,13 +173,14 @@ def reset_from_flash(self): self._enable_boot0(False) self._reset() - def command(self, command): - return self.write_and_ack("Command", command, command ^ 0xFF) + def command(self, command, description): + self.debug(10, "*** Command: %s" % description) + ack_received = self.write_and_ack("Command", command, command ^ 0xFF) + if not ack_received: + raise CommandException("%s (%s) failed: no ack" % (description, command)) def get(self): - if not self.command(self.Command.GET): - raise CommandException("Get (0x00) failed") - self.debug(10, "*** Get command") + self.command(self.Command.GET, "Get") length = bytearray(self.connection.read())[0] version = bytearray(self.connection.read())[0] self.debug(10, " Bootloader version: " + hex(version)) @@ -191,10 +192,7 @@ def get(self): return version def get_version(self): - if not self.command(self.Command.GET_VERSION): - raise CommandException("GetVersion (0x01) failed") - - self.debug(10, "*** GetVersion command") + self.command(self.Command.GET_VERSION, "Get version") version = bytearray(self.connection.read())[0] self.connection.read(2) self._wait_for_ack("0x01 end") @@ -202,10 +200,7 @@ def get_version(self): return version def get_id(self): - if not self.command(self.Command.GET_ID): - raise CommandException("GetID (0x02) failed") - - self.debug(10, "*** GetID command") + self.command(self.Command.GET_ID, "Get ID") length = bytearray(self.connection.read())[0] id_data = bytearray(self.connection.read(length + 1)) self._wait_for_ack("0x02 end") @@ -231,10 +226,7 @@ def format_uid(uid): def read_memory(self, address, length): assert length <= 256 - if not self.command(self.Command.READ_MEMORY): - raise CommandException("ReadMemory (0x11) failed") - - self.debug(10, "*** ReadMemory command") + self.command(self.Command.READ_MEMORY, "Read memory") self.write_and_ack("0x11 address failed", self._encode_address(address)) nr_of_bytes = (length - 1) & 0xFF checksum = nr_of_bytes ^ 0xFF @@ -243,10 +235,7 @@ def read_memory(self, address, length): def go(self, address): # pylint: disable=invalid-name - if not self.command(self.Command.GO): - raise CommandException("Go (0x21) failed") - - self.debug(10, "*** Go command") + self.command(self.Command.GO, "Go") self.write_and_ack("0x21 go failed", self._encode_address(address)) def write_memory(self, address, data): @@ -254,11 +243,7 @@ def write_memory(self, address, data): if nr_of_bytes == 0: return assert nr_of_bytes <= 256 - - if not self.command(self.Command.WRITE_MEMORY): - raise CommandException("Write memory (0x31) failed") - - self.debug(10, "*** Write memory command") + self.command(self.Command.WRITE_MEMORY, "Write memory") self.write_and_ack("0x31 address failed", self._encode_address(address)) # pad data length to multiple of 4 bytes @@ -280,12 +265,7 @@ def erase_memory(self, sectors=None): raise ValueError("Extended erase can not erase specific sectors: %s." % sectors) self.extended_erase_memory() return - - if not self.command(self.Command.ERASE): - raise CommandException("Erase memory (0x43) failed") - - self.debug(10, "*** Erase memory command") - + self.command(self.Command.ERASE, "Erase memory") if sectors: # page erase, see ST AN3155 page_count = len(sectors) @@ -299,10 +279,7 @@ def erase_memory(self, sectors=None): self.debug(10, " Erase memory done") def extended_erase_memory(self): - if not self.command(self.Command.EXTENDED_ERASE): - raise CommandException("Extended Erase memory (0x44) failed") - - self.debug(10, "*** Extended Erase memory command") + self.command(self.Command.EXTENDED_ERASE, "Extended erase memory") # Global mass erase and checksum byte self.write(b'\xff\xff\x00') previous_timeout_value = self.connection.timeout @@ -313,36 +290,24 @@ def extended_erase_memory(self): self.debug(10, " Extended Erase memory done") def write_protect(self, pages): - if not self.command(self.Command.WRITE_PROTECT): - raise CommandException("Write Protect memory (0x63) failed") - - self.debug(10, "*** Write protect command") + self.command(self.Command.WRITE_PROTECT, "Write protect") nr_of_pages = (len(pages) - 1) & 0xFF checksum = reduce(operator.xor, pages, nr_of_pages) self.write_and_ack("0x63 write protect failed", nr_of_pages, pages, checksum) self.debug(10, " Write protect done") def write_unprotect(self): - if not self.command(self.Command.WRITE_UNPROTECT): - raise CommandException("Write Unprotect (0x73) failed") - - self.debug(10, "*** Write Unprotect command") + self.command(self.Command.WRITE_UNPROTECT, "Write unprotect") self._wait_for_ack("0x73 write unprotect failed") self.debug(10, " Write Unprotect done") def readout_protect(self): - if not self.command(self.Command.READOUT_PROTECT): - raise CommandException("Readout protect (0x82) failed") - - self.debug(10, "*** Readout protect command") + self.command(self.Command.READOUT_PROTECT, "Readout protect") self._wait_for_ack("0x82 readout protect failed") self.debug(10, " Read protect done") def readout_unprotect(self): - if not self.command(self.Command.READOUT_UNPROTECT): - raise CommandException("Readout unprotect (0x92) failed") - - self.debug(10, "*** Readout Unprotect command") + self.command(self.Command.READOUT_UNPROTECT, "Readout unprotect") self._wait_for_ack("0x92 readout unprotect failed") self.debug(20, " Mass erase -- this may take a while") time.sleep(20) diff --git a/tests/test_bootloader.py b/tests/test_bootloader.py index 27f8b10..88ba95a 100644 --- a/tests/test_bootloader.py +++ b/tests/test_bootloader.py @@ -103,7 +103,7 @@ def test_read_memory_sends_length_with_checksum(bootloader, write): def test_command_sends_command_and_control_bytes(bootloader, write): - bootloader.command(0x01) + bootloader.command(0x01, "bogus command") assert write.data_was_written(b"\x01\xfe") From b15b54da7d9a68ad5a78e62a9ae8121817b27fed Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Apr 2019 22:16:06 +0200 Subject: [PATCH 113/369] Add missing docstrings --- .pylintrc | 1 - README.md | 1 - setup.cfg | 1 - stm32loader/bootloader.py | 54 ++++++++++++++++++++++++++++++++++++++ stm32loader/serial.py | 5 ++++ stm32loader/stm32loader.py | 8 +++++- 6 files changed, 66 insertions(+), 4 deletions(-) diff --git a/.pylintrc b/.pylintrc index 20d8cdb..6db7898 100644 --- a/.pylintrc +++ b/.pylintrc @@ -5,7 +5,6 @@ max-line-length=98 disable= fixme, # TO DOs are not errors bad-continuation, # be compatible to black - missing-docstring, [REPORT] score=no diff --git a/README.md b/README.md index d73b092..f9ce93f 100644 --- a/README.md +++ b/README.md @@ -119,5 +119,4 @@ Not currently supported Future work ----------- * Use proper logging instead of print statements -* Add docstrings to all public methods * Use Travis or Azure pipelines for CI diff --git a/setup.cfg b/setup.cfg index f745dd4..c206fd8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -23,7 +23,6 @@ ignore = # be compatible to black C812, # Missing trailing comma E203, # Whitespace before ':' - D102, # Missing docstring in public method per-file-ignores = # Missing docstring in public function tests/*:D103, diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index 4796f83..3c557f2 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -149,37 +149,48 @@ def __init__(self, connection, verbosity=5): self.verbosity = verbosity def write(self, *data): + """Write the given data to the MCU.""" for data_bytes in data: if isinstance(data_bytes, int): data_bytes = struct.pack("B", data_bytes) self.connection.write(data_bytes) def write_and_ack(self, message, *data): + """Write data to the MCU and wait until it replies with ACK.""" # this is a separate method from write() because a keyword # argument after *args is not possible in Python 2 self.write(*data) return self._wait_for_ack(message) def debug(self, level, message): + """Print the given message if its level is low enough.""" if self.verbosity >= level: print(message, file=sys.stderr) def reset_from_system_memory(self): + """Reset the MCU with boot0 enabled to enter the bootloader.""" self._enable_boot0(True) self._reset() return self.write_and_ack("Synchro", self.Command.SYNCHRONIZE) def reset_from_flash(self): + """Reset the MCU with boot0 disabled.""" self._enable_boot0(False) self._reset() def command(self, command, description): + """ + Send the given command to the MCU. + + Raise CommandException if there's no ACK replied. + """ self.debug(10, "*** Command: %s" % description) ack_received = self.write_and_ack("Command", command, command ^ 0xFF) if not ack_received: raise CommandException("%s (%s) failed: no ack" % (description, command)) def get(self): + """Return the bootloader version and remember supported commands.""" self.command(self.Command.GET, "Get") length = bytearray(self.connection.read())[0] version = bytearray(self.connection.read())[0] @@ -192,6 +203,11 @@ def get(self): return version def get_version(self): + """ + Return the bootloader version. + + Read protection status readout is not yet implemented. + """ self.command(self.Command.GET_VERSION, "Get version") version = bytearray(self.connection.read())[0] self.connection.read(2) @@ -200,6 +216,7 @@ def get_version(self): return version def get_id(self): + """Send the 'Get ID' command and return the device (model) ID.""" self.command(self.Command.GET_ID, "Get ID") length = bytearray(self.connection.read())[0] id_data = bytearray(self.connection.read(length + 1)) @@ -208,23 +225,31 @@ def get_id(self): return _device_id def get_flash_size(self, device_family): + """Return the MCU's flash size in bytes.""" flash_size_address = self.FLASH_SIZE_ADDRESS[device_family] flash_size_bytes = self.read_memory(flash_size_address, 2) flash_size = flash_size_bytes[0] + flash_size_bytes[1] * 256 return flash_size def get_uid(self, device_id): + """Send the 'Get UID' command and return the device UID.""" uid_address = self.UID_ADDRESS[device_id] uid = self.read_memory(uid_address, 12) return uid @staticmethod def format_uid(uid): + """Return a readable string from the given UID.""" swapped_data = [[uid[b] for b in part] for part in Stm32Bootloader.UID_SWAP] uid_string = "-".join("".join(format(b, "02X") for b in part) for part in swapped_data) return uid_string def read_memory(self, address, length): + """ + Return the memory contents of flash at the given address. + + Supports maximum 256 bytes. + """ assert length <= 256 self.command(self.Command.READ_MEMORY, "Read memory") self.write_and_ack("0x11 address failed", self._encode_address(address)) @@ -234,11 +259,17 @@ def read_memory(self, address, length): return bytearray(self.connection.read(length)) def go(self, address): + """Send the 'Go' command to start execution of firmware.""" # pylint: disable=invalid-name self.command(self.Command.GO, "Go") self.write_and_ack("0x21 go failed", self._encode_address(address)) def write_memory(self, address, data): + """ + Write the given data to flash at the given address. + + Supports maximum 256 bytes. + """ nr_of_bytes = len(data) if nr_of_bytes == 0: return @@ -279,6 +310,7 @@ def erase_memory(self, sectors=None): self.debug(10, " Erase memory done") def extended_erase_memory(self): + """Send the extended erase command to erase the full flash content.""" self.command(self.Command.EXTENDED_ERASE, "Extended erase memory") # Global mass erase and checksum byte self.write(b'\xff\xff\x00') @@ -290,6 +322,7 @@ def extended_erase_memory(self): self.debug(10, " Extended Erase memory done") def write_protect(self, pages): + """Enable write protection on the given flash pages.""" self.command(self.Command.WRITE_PROTECT, "Write protect") nr_of_pages = (len(pages) - 1) & 0xFF checksum = reduce(operator.xor, pages, nr_of_pages) @@ -297,16 +330,23 @@ def write_protect(self, pages): self.debug(10, " Write protect done") def write_unprotect(self): + """Disable write protection of the flash memory.""" self.command(self.Command.WRITE_UNPROTECT, "Write unprotect") self._wait_for_ack("0x73 write unprotect failed") self.debug(10, " Write Unprotect done") def readout_protect(self): + """Enable readout protection of the flash memory.""" self.command(self.Command.READOUT_PROTECT, "Readout protect") self._wait_for_ack("0x82 readout protect failed") self.debug(10, " Read protect done") def readout_unprotect(self): + """ + Disable readout protection of the flash memory. + + Beware, this will erase the flash content. + """ self.command(self.Command.READOUT_UNPROTECT, "Readout unprotect") self._wait_for_ack("0x92 readout unprotect failed") self.debug(20, " Mass erase -- this may take a while") @@ -316,6 +356,11 @@ def readout_unprotect(self): self.reset_from_system_memory() def read_memory_data(self, address, length): + """ + Return flash content from the given address and byte count. + + Length may be more than 256 bytes. + """ data = bytearray() while length > 256: self.debug( @@ -332,6 +377,11 @@ def read_memory_data(self, address, length): return data def write_memory_data(self, address, data): + """ + Write the given data to flash. + + Data length may be more than 256 bytes. + """ length = len(data) offset = 0 while length > 256: @@ -349,6 +399,7 @@ def write_memory_data(self, address, data): self.write_memory(address, data[offset : offset + length]) def _reset(self): + """Enable or disable the reset IO line (if possible).""" if not self._toggle_reset: return self.connection.enable_reset(True) @@ -357,12 +408,14 @@ def _reset(self): time.sleep(0.5) def _enable_boot0(self, enable=True): + """Enable or disable the boot0 IO line (if possible).""" if not self._toggle_boot0: return self.connection.enable_boot0(enable) def _wait_for_ack(self, info=""): + """Read a byte and raise CommandException if it's not ACK.""" try: ack = bytearray(self.connection.read())[0] except TypeError: @@ -377,6 +430,7 @@ def _wait_for_ack(self, info=""): @staticmethod def _encode_address(address): + """Return the given address as big-endian bytes with a checksum.""" address_bytes = bytearray(struct.pack(">I", address)) checksum_byte = struct.pack("B", reduce(operator.xor, address_bytes)) return address_bytes + checksum_byte diff --git a/stm32loader/serial.py b/stm32loader/serial.py index 7d42f84..70178e1 100644 --- a/stm32loader/serial.py +++ b/stm32loader/serial.py @@ -49,6 +49,7 @@ def __init__(self, serial_port, baud_rate=115_200, parity="E"): self.serial_connection = None def connect(self): + """Connect to the RS-232 serial port.""" self.serial_connection = serial.Serial( port=self.serial_port, baudrate=self.baud_rate, @@ -65,12 +66,15 @@ def connect(self): ) def write(self, *args, **kwargs): + """Write the given data to the serial connection.""" return self.serial_connection.write(*args, **kwargs) def read(self, *args, **kwargs): + """Read the given amount of bytes from the serial connection.""" return self.serial_connection.read(*args, **kwargs) def enable_reset(self, enable=True): + """Enable or disable the reset IO line.""" # reset on the STM32 is active low (0 Volt puts the MCU in reset) # but the RS-232 DTR signal is active low by itself, so it # inverts this (writing a logical 1 outputs a low voltage, i.e. @@ -85,6 +89,7 @@ def enable_reset(self, enable=True): self.serial_connection.setDTR(level) def enable_boot0(self, enable=True): + """Enable or disable the boot0 IO line.""" level = int(enable) # by default, this is active low diff --git a/stm32loader/stm32loader.py b/stm32loader/stm32loader.py index df1449b..54f7da9 100644 --- a/stm32loader/stm32loader.py +++ b/stm32loader/stm32loader.py @@ -61,12 +61,13 @@ def __init__(self): self.verbosity = DEFAULT_VERBOSITY def debug(self, level, message): + """Log a message to stderror if its level is low enough.""" if self.verbosity >= level: print(message, file=sys.stderr) def parse_arguments(self, arguments): + """Parse the list of command-line arguments.""" # pylint: disable=too-many-branches, eval-used - try: # parse command-line arguments using getopt options, arguments = getopt.getopt(arguments, "hqVeuwvrsRBP:p:b:a:l:g:f:") @@ -126,6 +127,7 @@ def parse_arguments(self, arguments): assert False, "unhandled option %s" % option def connect(self): + """Connect to the RS-232 serial port.""" serial_connection = SerialConnection( self.configuration["port"], self.configuration["baud"], self.configuration["parity"] ) @@ -162,6 +164,7 @@ def connect(self): sys.exit(1) def perform_commands(self): + """Run all operations as defined by the configuration.""" # pylint: disable=too-many-branches binary_data = None if self.configuration["write"] or self.configuration["verify"]: @@ -213,10 +216,12 @@ def perform_commands(self): self.bootloader.go(self.configuration["go_address"]) def reset(self): + """Reset the microcontroller.""" self.bootloader.reset_from_flash() @staticmethod def print_usage(): + """Print help text explaining the command-line arguments.""" help_text = """Usage: %s [-hqVeuwvrsRB] [-l length] [-p port] [-b baud] [-P parity] [-a address] [-g address] [-f family] [file.bin] -e Erase (note: this is required on previously written memory) @@ -247,6 +252,7 @@ def print_usage(): print(help_text) def read_device_details(self): + """Show MCU details (bootloader version, chip ID, UID, flash size).""" boot_version = self.bootloader.get() self.debug(0, "Bootloader version %X" % boot_version) device_id = self.bootloader.get_id() From d8d04a58d17b3b67f9e0ad04dcdf51f4f351bebd Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Apr 2019 22:16:07 +0200 Subject: [PATCH 114/369] Only call extended erase if no specific sectors are specified --- stm32loader/bootloader.py | 9 ++++++--- tests/test_bootloader.py | 6 ------ 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index 3c557f2..f838ff3 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -291,9 +291,12 @@ def write_memory(self, address, data): self.debug(10, " Write memory done") def erase_memory(self, sectors=None): - if self.extended_erase: - if sectors: - raise ValueError("Extended erase can not erase specific sectors: %s." % sectors) + """ + Erase flash memory at the given sectors. + + Set sectors to None to erase the full memory. + """ + if self.extended_erase and not sectors: self.extended_erase_memory() return self.command(self.Command.ERASE, "Erase memory") diff --git a/tests/test_bootloader.py b/tests/test_bootloader.py index 88ba95a..83aeaa8 100644 --- a/tests/test_bootloader.py +++ b/tests/test_bootloader.py @@ -128,12 +128,6 @@ def test_erase_memory_with_sectors_sends_sector_addresses_with_(bootloader, writ assert write.data_was_written(b'\x01\x02\x04\x08\x0b') -def test_erase_memory_with_extended_erase_enabled_and_specific_sectors_raises_error(bootloader): - bootloader.extended_erase = True - with pytest.raises(ValueError, match="Extended erase .* specific sectors.*14, 16"): - bootloader.erase_memory([14, 16]) - - def test_extended_erase_memory_sends_global_mass_erase(bootloader, write): bootloader.extended_erase_memory() assert write.data_was_written(b'\xff\xff\x00') From 5acc09805af901a5add8484313c24196a2984ec5 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Apr 2019 22:16:08 +0200 Subject: [PATCH 115/369] Satisfy minor linting warnings --- stm32loader/bootloader.py | 4 ++-- tests/test_bootloader.py | 11 ++++------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index f838ff3..61b92e1 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -316,7 +316,7 @@ def extended_erase_memory(self): """Send the extended erase command to erase the full flash content.""" self.command(self.Command.EXTENDED_ERASE, "Extended erase memory") # Global mass erase and checksum byte - self.write(b'\xff\xff\x00') + self.write(b"\xff\xff\x00") previous_timeout_value = self.connection.timeout self.connection.timeout = 30 print("Extended erase (0x44), this can take ten seconds or more") @@ -426,7 +426,7 @@ def _wait_for_ack(self, info=""): if ack == self.Reply.NACK: raise CommandException("NACK " + info) - elif ack != self.Reply.ACK: + if ack != self.Reply.ACK: raise CommandException("Unknown response. " + info + ": " + hex(ack)) return 1 diff --git a/tests/test_bootloader.py b/tests/test_bootloader.py index 83aeaa8..fa00b59 100644 --- a/tests/test_bootloader.py +++ b/tests/test_bootloader.py @@ -10,6 +10,7 @@ # Python version <= 3.2 from mock import MagicMock +#pylint: disable=missing-docstring, redefined-outer-name @pytest.fixture def connection(): @@ -38,7 +39,7 @@ def bootloader(connection): return Stm32Bootloader(connection) -def test_constructor_with_connection_None_passes(): +def test_constructor_with_connection_none_passes(): Stm32Bootloader(connection=None) @@ -49,7 +50,7 @@ def test_constructor_does_not_use_connection_directly(connection): def test_write_without_data_sends_no_bytes(bootloader, write): bootloader.write() - assert len(write.written_data) == 0 + assert not write.written_data def test_write_with_bytes_sends_bytes_verbatim(bootloader, write): @@ -97,11 +98,6 @@ def test_read_memory_sends_length_with_checksum(bootloader, write): assert write.data_was_written(b'\x0f\xf0') -def test_read_memory_sends_length_with_checksum(bootloader, write): - bootloader.read_memory(0, 0x0f + 1) - assert write.data_was_written(b'\x0f\xf0') - - def test_command_sends_command_and_control_bytes(bootloader, write): bootloader.command(0x01, "bogus command") assert write.data_was_written(b"\x01\xfe") @@ -114,6 +110,7 @@ def test_reset_from_system_memory_sends_command_synchronize(bootloader, write): def test_encode_address_returns_correct_bytes_with_checksum(): + # pylint:disable=protected-access encoded_address = Stm32Bootloader._encode_address(0x04030201) assert bytes(encoded_address) == b"\x04\x03\x02\x01\x04" From ed9162bdaccb552f1b17a3f79635352972786675 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Apr 2019 22:16:09 +0200 Subject: [PATCH 116/369] Mention future support for PyPy --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index f9ce93f..99571ac 100644 --- a/README.md +++ b/README.md @@ -120,3 +120,4 @@ Future work ----------- * Use proper logging instead of print statements * Use Travis or Azure pipelines for CI +* Support PyPy, PyPy3 From 6fee650eeb93c014d26681551db603fdd806cb79 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Apr 2019 22:16:10 +0200 Subject: [PATCH 117/369] Rename serial to rs232 Otherwise it name-clashes with pyserial's serial on Python 2. --- stm32loader/{serial.py => rs232.py} | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) rename stm32loader/{serial.py => rs232.py} (95%) diff --git a/stm32loader/serial.py b/stm32loader/rs232.py similarity index 95% rename from stm32loader/serial.py rename to stm32loader/rs232.py index 70178e1..5097639 100644 --- a/stm32loader/serial.py +++ b/stm32loader/rs232.py @@ -23,6 +23,7 @@ Offer support for toggling RESET and BOOT0. """ +# not naming this file itself 'serial', becase that name-clashes in Python 2 import serial @@ -31,7 +32,7 @@ class SerialConnection: # pylint: disable=too-many-instance-attributes - def __init__(self, serial_port, baud_rate=115_200, parity="E"): + def __init__(self, serial_port, baud_rate=115200, parity="E"): """Construct a SerialConnection (not yet connected).""" self.serial_port = serial_port self.baud_rate = baud_rate From 6fe8cca12c38799aadd5d8c7e66cac05cfe8baf2 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Apr 2019 22:16:11 +0200 Subject: [PATCH 118/369] Supply sys.argv explicitly to main() --- stm32loader/__main__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/stm32loader/__main__.py b/stm32loader/__main__.py index 9b50634..664665d 100644 --- a/stm32loader/__main__.py +++ b/stm32loader/__main__.py @@ -22,7 +22,9 @@ This does exactly the same as manually calling 'python stm32loader.py'. """ -from .stm32loader import main as stm32loader_main +import sys + +from stm32loader.main import main as stm32loader_main def main(): @@ -32,7 +34,7 @@ def main(): This way it it can be used as an entry point for a console script. :return None: """ - stm32loader_main() + stm32loader_main(*sys.argv[1:]) if __name__ == "__main__": From bf8a9cbfc227e07da1b9f19dab78b374e8a362b0 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Apr 2019 22:16:12 +0200 Subject: [PATCH 119/369] Rename stm32loader.py to main.py Also allow to pass 'avoid_system_exit' kwarg to main(). --- stm32loader/{stm32loader.py => main.py} | 39 +++++++++++++------------ 1 file changed, 21 insertions(+), 18 deletions(-) rename stm32loader/{stm32loader.py => main.py} (93%) diff --git a/stm32loader/stm32loader.py b/stm32loader/main.py similarity index 93% rename from stm32loader/stm32loader.py rename to stm32loader/main.py index 54f7da9..6eb5477 100644 --- a/stm32loader/stm32loader.py +++ b/stm32loader/main.py @@ -27,7 +27,7 @@ import sys from .bootloader import CHIP_IDS, CommandException, Stm32Bootloader -from .serial import SerialConnection +from .rs232 import SerialConnection DEFAULT_VERBOSITY = 5 @@ -43,7 +43,7 @@ def __init__(self): self.bootloader = None self.configuration = { "port": "/dev/tty.usbserial-ftCYPMYJ", - "baud": 115_200, + "baud": 115200, "parity": self.PARITY["even"], "family": None, "address": 0x08000000, @@ -63,7 +63,7 @@ def __init__(self): def debug(self, level, message): """Log a message to stderror if its level is low enough.""" if self.verbosity >= level: - print(message, file=sys.stderr) + sys.stderr.write(message) def parse_arguments(self, arguments): """Parse the list of command-line arguments.""" @@ -231,7 +231,7 @@ def print_usage(): -r Read from flash and store in local file -l length Length of read -p port Serial port (default: /dev/tty.usbserial-ftCYPMYJ) - -b baud Baud speed (default: 115200) + -b baud Baudrate (default: 115200) -a address Target address (default: 0x08000000) -g address Start executing from address (0x08000000, usually) -f family Device family to read out device UID and flash size; e.g F1 for STM32F1xx @@ -246,9 +246,11 @@ def print_usage(): -u Readout unprotect -P parity Parity: "even" for STM32 (default), "none" for BlueNRG + Example: ./%s -p COM7 -f F1 Example: ./%s -e -w -v example/main.bin """ - help_text = help_text % (sys.argv[0], sys.argv[0]) + current_script = sys.argv[0] if sys.argv else "stm32loader" + help_text = help_text % (current_script, current_script, current_script) print(help_text) def read_device_details(self): @@ -269,24 +271,25 @@ def read_device_details(self): self.debug(0, "Flash size: %d KiB" % flash_size) -def main(arguments=None): +def main(*args, **kwargs): """ Parse arguments and execute tasks. - If no arguments are supplied, use the ones from - sys.argv. + Default usage is to supply *sys.argv[1:]. """ - loader = Stm32Loader() - if arguments is None: - arguments = sys.argv[1] - loader.parse_arguments(arguments) - loader.connect() try: - loader.read_device_details() - loader.perform_commands() - finally: - loader.reset() + loader = Stm32Loader() + loader.parse_arguments(args) + loader.connect() + try: + loader.read_device_details() + loader.perform_commands() + finally: + loader.reset() + except SystemExit: + if not kwargs.get("avoid_system_exit", False): + raise if __name__ == "__main__": - main() + main(*sys.argv[1:]) From e132d6c847c423fdf0ffc56fd2d61becc700ed44 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Apr 2019 22:16:13 +0200 Subject: [PATCH 120/369] Satisfy linters for Python 2 --- stm32loader/bootloader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index 61b92e1..29f2cd0 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -165,7 +165,7 @@ def write_and_ack(self, message, *data): def debug(self, level, message): """Print the given message if its level is low enough.""" if self.verbosity >= level: - print(message, file=sys.stderr) + sys.stderr.write(message) def reset_from_system_memory(self): """Reset the MCU with boot0 enabled to enter the bootloader.""" From 66f90c87fe2c8b0a83844a1465a2f633a832753f Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Apr 2019 22:16:14 +0200 Subject: [PATCH 121/369] Move unit tests into separate folder Making room for integration tests. --- tests/{ => unit}/test_bootloader.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) rename tests/{ => unit}/test_bootloader.py (98%) diff --git a/tests/test_bootloader.py b/tests/unit/test_bootloader.py similarity index 98% rename from tests/test_bootloader.py rename to tests/unit/test_bootloader.py index fa00b59..230735b 100644 --- a/tests/test_bootloader.py +++ b/tests/unit/test_bootloader.py @@ -10,7 +10,8 @@ # Python version <= 3.2 from mock import MagicMock -#pylint: disable=missing-docstring, redefined-outer-name +# pylint: disable=missing-docstring, redefined-outer-name + @pytest.fixture def connection(): From 0c0c95b8dd6973e63c7deb4415b09580c634050f Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Apr 2019 22:16:15 +0200 Subject: [PATCH 122/369] Add integration tests This covers basic functionality like help, erase, write and read Making use of actual hardware, so those tests are disabled by default. --- setup.cfg | 4 + tests/integration/test_stm32loader.py | 111 ++++++++++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 tests/integration/test_stm32loader.py diff --git a/setup.cfg b/setup.cfg index c206fd8..92b5e88 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,3 +27,7 @@ per-file-ignores = # Missing docstring in public function tests/*:D103, +[tool:pytest] +addopts = --strict -m "not hardware" +markers = + hardware diff --git a/tests/integration/test_stm32loader.py b/tests/integration/test_stm32loader.py new file mode 100644 index 0000000..a8a85b2 --- /dev/null +++ b/tests/integration/test_stm32loader.py @@ -0,0 +1,111 @@ +""" +Tests for the stm32loader executable and main() method. + +Several of these tests require an actual STM32 microcontroller to be +connected, and to be programmable (including RESET and BOOT0 toggling). + +These hardware tests are disabled by default. +To enable them, configure the device parameters below and +supply the following as argument to pytest: + + -m "hardware" + +""" + +import os +import subprocess + +import pytest + +from stm32loader.main import main + +HERE = os.path.split(os.path.abspath(__file__))[0] + +# Device dependant details +# HyTiny on Windows with FTDI adapter +STM32_CHIP_FAMILY = "F1" +STM32_CHIP_ID = "0x410" +STM32_CHIP_TYPE = "STM32F10x Medium-density" +SERIAL_PORT = "COM7" +# Flaky cable setup, cheap serial adapter... +BAUD_RATE = 9600 +KBYTE = 2 ** 10 +SIZE = 32 * KBYTE +DUMP_FILE = "dump.bin" +FIRMWARE_FILE = os.path.join(HERE, "../../firmware/generic_boot20_pc13.binary.bin") + +# pylint: disable=missing-docstring, redefined-outer-name + + +@pytest.fixture(scope="module") +def stm32loader(): + def main_with_default_arguments(*args): + main("-p", SERIAL_PORT, "-b", str(BAUD_RATE), "-q", *args, avoid_system_exit=True) + return main_with_default_arguments + + +@pytest.fixture +def dump_file(tmpdir): + return os.path.join(str(tmpdir), DUMP_FILE) + + +def test_stm32loader_is_executable(): + subprocess.call(["stm32loader", "--help"]) + + +@pytest.mark.parametrize( + "help_argument", ["-h", "--help"], +) +def test_argument_h_prints_help_info(help_argument, capsys): + main(help_argument, avoid_system_exit=True) + captured = capsys.readouterr() + assert "Example:" in captured.out + + +@pytest.mark.hardware +def test_argument_f_prints_chip_id_and_device_type(stm32loader, capsys): + stm32loader("-f", STM32_CHIP_FAMILY) + captured = capsys.readouterr() + assert STM32_CHIP_ID in captured.err + assert STM32_CHIP_TYPE in captured.err + + +@pytest.mark.hardware +def test_read_produces_file_of_correct_length(stm32loader, dump_file): + stm32loader("-r", "-l", "1024", dump_file) + assert os.stat(dump_file).st_size == 1024 + + +@pytest.mark.hardware +def test_erase_resets_memory_to_all_ones(stm32loader, dump_file): + # erase + stm32loader("-e") + # read all bytes and check if they're 0xFF + stm32loader("-r", "-l", "1024", dump_file) + # bytearray() is required for Python 2 + read_data = bytearray(open(dump_file, "rb").read()) + assert all(byte == 0xFF for byte in read_data) + + +@pytest.mark.hardware +def test_write_saves_correct_data(stm32loader, dump_file): + # erase and write + stm32loader("-e", "-w", FIRMWARE_FILE) + + # read and compare data with file on disk + stm32loader("-r", "-l", str(SIZE), dump_file) + read_data = open(dump_file, "rb").read() + original_data = open(FIRMWARE_FILE, "rb").read() + + for address, data in enumerate(zip(read_data, original_data)): + read_byte, original_byte = data + assert read_byte == original_byte, "Data mismatch at byte %s: %d vs %d" % ( + address, + read_byte, + original_byte, + ) + + +@pytest.mark.hardware +def test_erase_write_verify_passes(stm32loader): + stm32loader("-e", "-w", "-v", FIRMWARE_FILE) From 17255efad215c0b6472c03d0a8763c8b90c01b5a Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Apr 2019 22:16:15 +0200 Subject: [PATCH 123/369] Tell black to remain compatible to older Python versions Otherwise it replaces 0x0800000 with 0x0800_0000. --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 2eac387..dd2e434 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,9 @@ [tool.black] line-length = 98 +# not including py27 since that triggers black +# to change print("") into print ("") +target-version = ['py34', 'py35', 'py36', 'py37'] exclude = ''' /( \.git From 3ea6764b9d8f0d84e90c018407e9b68432beca66 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Apr 2019 22:16:16 +0200 Subject: [PATCH 124/369] Treat --help as an alias of -h Previously it would show the help, but still complain about an unknown argument. --- stm32loader/main.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/stm32loader/main.py b/stm32loader/main.py index 6eb5477..23daa96 100644 --- a/stm32loader/main.py +++ b/stm32loader/main.py @@ -70,10 +70,10 @@ def parse_arguments(self, arguments): # pylint: disable=too-many-branches, eval-used try: # parse command-line arguments using getopt - options, arguments = getopt.getopt(arguments, "hqVeuwvrsRBP:p:b:a:l:g:f:") + options, arguments = getopt.getopt(arguments, "hqVeuwvrsRBP:p:b:a:l:g:f:", ["help"]) except getopt.GetoptError as err: # print help information and exit: - # this print something like "option -a not recognized" + # this prints something like "option -a not recognized" print(str(err)) self.print_usage() sys.exit(2) @@ -87,7 +87,7 @@ def parse_arguments(self, arguments): self.verbosity = 10 elif option == "-q": self.verbosity = 0 - elif option == "-h": + elif option in ["-h", "--help"]: self.print_usage() sys.exit(0) elif option == "-e": From c7a794f65c9538c5611389ebf9c2e2f8cb2f5139 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Apr 2019 22:16:17 +0200 Subject: [PATCH 125/369] Do all printing with the print() function --- stm32loader/bootloader.py | 2 +- stm32loader/main.py | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index 29f2cd0..61b92e1 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -165,7 +165,7 @@ def write_and_ack(self, message, *data): def debug(self, level, message): """Print the given message if its level is low enough.""" if self.verbosity >= level: - sys.stderr.write(message) + print(message, file=sys.stderr) def reset_from_system_memory(self): """Reset the MCU with boot0 enabled to enter the bootloader.""" diff --git a/stm32loader/main.py b/stm32loader/main.py index 23daa96..727052a 100644 --- a/stm32loader/main.py +++ b/stm32loader/main.py @@ -63,7 +63,7 @@ def __init__(self): def debug(self, level, message): """Log a message to stderror if its level is low enough.""" if self.verbosity >= level: - sys.stderr.write(message) + print(message, file=sys.stderr) def parse_arguments(self, arguments): """Parse the list of command-line arguments.""" @@ -139,14 +139,15 @@ def connect(self): try: serial_connection.connect() except IOError as e: - sys.stderr.write(str(e) + "\n") - sys.stderr.write( + print(str(e) + "\n", file=sys.stderr) + print( "Is the device connected and powered correctly?\n" "Please use the -p option to select the correct serial port. Examples:\n" " -p COM3\n" " -p /dev/ttyS0\n" " -p /dev/ttyUSB0\n" - " -p /dev/tty.usbserial-ftCYPMYJ\n" + " -p /dev/tty.usbserial-ftCYPMYJ\n", + file=sys.stderr, ) exit(1) From af9cb5d84196012ff89dec9802c8162420c40e7d Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Apr 2019 22:16:18 +0200 Subject: [PATCH 126/369] Add some new feature ideas --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 99571ac..44a8aca 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,8 @@ Not currently supported Future work ----------- +* Allow to set default serial port through environment variable * Use proper logging instead of print statements * Use Travis or Azure pipelines for CI * Support PyPy, PyPy3 +* Drop Python2 support; start using intenum for commands and replies From d36e597dabc74c1810f7ccdc76fd31aab90e30f0 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Apr 2019 22:16:19 +0200 Subject: [PATCH 127/369] Show progress bar during read or write As was planned in #4. Making use of the 'progress' module, compatible to Python 2. --- CHANGELOG.md | 5 +- README.md | 1 + setup.cfg | 2 + setup.py | 1 + stm32loader/bootloader.py | 123 ++++++++++++++++++++++++++++++-------- stm32loader/main.py | 115 ++++++++++++++++++++--------------- 6 files changed, 171 insertions(+), 76 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e479cd3..727d141 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,10 +3,9 @@ v0.4.0 ====== * #8: Add support for STM32F7 mcus. By sam-bristow. * #9: Support data writes smaller than 256 bytes. By NINI1988. +* #10: Make stm32loader useful as a library. +* #4: Bring back support for progress bar. * Start using code linting and unit tests. -* Allow to use stm32loader.main() as a library function. -* Allow to use Stm32Bootloader as a library class. -* Allow a custom connection to be used (e.g. I2C instead of UART). v0.3.3 ====== diff --git a/README.md b/README.md index 44a8aca..6296063 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ Usage -R Make reset active high -B Make boot0 active high -u Readout unprotect + -n No progress: don't show progress bar -P parity Parity: "even" for STM32 (default), "none" for BlueNRG ``` diff --git a/setup.cfg b/setup.cfg index 92b5e88..6ca581e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,6 +26,8 @@ ignore = per-file-ignores = # Missing docstring in public function tests/*:D103, + # .next() is not a thing in Python 3 + stm32loader/bootloader.py:B305, [tool:pytest] addopts = --strict -m "not hardware" diff --git a/setup.py b/setup.py index f84ccfe..db5f470 100644 --- a/setup.py +++ b/setup.py @@ -24,6 +24,7 @@ REQUIRED = [ 'pyserial', + 'progress', ] EXTRAS = { diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index 61b92e1..cc7070d 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -22,6 +22,7 @@ from __future__ import print_function +import math import operator import struct import sys @@ -61,6 +62,68 @@ class CommandException(IOError): """Error: a command in the STM32 native bootloader failed.""" +class ShowProgress: + """ + Show progress through a progress bar, as a context manager. + + Return the progress bar object on context enter, allowing the + caller to to call next(). + + Allow to supply the desired progress bar as None, to disable + progress bar output. + """ + + class _NoProgressBar: + """ + Stub to replace a real progress.bar.Bar. + + Use this if you don't want progress bar output, or if + there's an ImportError of progress module. + """ + + def next(self): # noqa + """Do nothing; be compatible to progress.bar.Bar.""" + + def finish(self): + """Do nothing; be compatible to progress.bar.Bar.""" + + def __init__(self, progress_bar_type): + """ + Construct the context manager object. + + :param progress_bar_type type: Type of progress bar to use. + Set to None if you don't want progress bar output. + """ + self.progress_bar_type = progress_bar_type + self.progress_bar = None + + def __call__(self, message, maximum): + """ + Return a context manager for a progress bar. + + :param str message: Message to show next to the progress bar. + :param int maximum: Maximum value of the progress bar (value at 100%). + E.g. 256. + :return ShowProgress: Context manager object. + """ + if not self.progress_bar_type: + self.progress_bar = self._NoProgressBar() + else: + self.progress_bar = self.progress_bar_type( + message, max=maximum, suffix="%(index)d/%(max)d" + ) + + return self + + def __enter__(self): + """Enter context: return progress bar to allow calling next().""" + return self.progress_bar + + def __exit__(self, exc_type, exc_val, exc_tb): + """Exit context: clean up by finish()ing the progress bar.""" + self.progress_bar.finish() + + class Stm32Bootloader: """Talk to the STM32 native bootloader.""" @@ -127,7 +190,7 @@ class Reply: extended_erase = False - def __init__(self, connection, verbosity=5): + def __init__(self, connection, verbosity=5, show_progress=None): """ Construct the Stm32Bootloader object. @@ -142,11 +205,14 @@ def __init__(self, connection, verbosity=5): :param connection: Object supporting read() and write(). E.g. serial.Serial(). :param int verbosity: Verbosity level. 0 is quite, 10 is verbose. + :param ShowProgress show_progress: ShowProgress context manager. + Set to None to disable progress bar output. """ self.connection = connection self._toggle_reset = getattr(connection, "can_toggle_reset", False) self._toggle_boot0 = getattr(connection, "can_toggle_boot0", False) self.verbosity = verbosity + self.show_progress = show_progress or ShowProgress(None) def write(self, *data): """Write the given data to the MCU.""" @@ -365,18 +431,20 @@ def read_memory_data(self, address, length): Length may be more than 256 bytes. """ data = bytearray() - while length > 256: - self.debug( - 5, "Read %(len)d bytes at 0x%(address)X" % {"address": address, "len": 256} - ) - data = data + self.read_memory(address, 256) - address = address + 256 - length = length - 256 - if length: - self.debug( - 5, "Read %(len)d bytes at 0x%(address)X" % {"address": address, "len": length} - ) - data = data + self.read_memory(address, length) + page_count = int(math.ceil(length / 256.0)) + self.debug(5, "Read %d pages at address 0x%X..." % (page_count, address)) + with self.show_progress("Reading", maximum=page_count) as progress_bar: + while length: + read_length = min(length, 256) + self.debug( + 10, + "Read %(len)d bytes at 0x%(address)X" + % {"address": address, "len": read_length}, + ) + data = data + self.read_memory(address, read_length) + progress_bar.next() + length = length - read_length + address = address + read_length return data def write_memory_data(self, address, data): @@ -386,20 +454,23 @@ def write_memory_data(self, address, data): Data length may be more than 256 bytes. """ length = len(data) + page_count = int(math.ceil(length / 256.0)) offset = 0 - while length > 256: - self.debug( - 5, "Write %(len)d bytes at 0x%(address)X" % {"address": address, "len": 256} - ) - self.write_memory(address, data[offset : offset + 256]) - offset += 256 - address += 256 - length -= 256 - if length: - self.debug( - 5, "Write %(len)d bytes at 0x%(address)X" % {"address": address, "len": length} - ) - self.write_memory(address, data[offset : offset + length]) + self.debug(5, "Write %d pages at address 0x%X..." % (page_count, address)) + + with self.show_progress("Writing", maximum=page_count) as progress_bar: + while length: + write_length = min(length, 256) + self.debug( + 10, + "Write %(len)d bytes at 0x%(address)X" + % {"address": address, "len": write_length}, + ) + self.write_memory(address, data[offset : offset + write_length]) + progress_bar.next() + length -= write_length + offset += write_length + address += write_length def _reset(self): """Enable or disable the reset IO line (if possible).""" diff --git a/stm32loader/main.py b/stm32loader/main.py index 727052a..2fd875a 100644 --- a/stm32loader/main.py +++ b/stm32loader/main.py @@ -26,7 +26,8 @@ import getopt import sys -from .bootloader import CHIP_IDS, CommandException, Stm32Bootloader +from .bootloader import CHIP_IDS, CommandException, ShowProgress, \ + Stm32Bootloader from .rs232 import SerialConnection DEFAULT_VERBOSITY = 5 @@ -38,6 +39,20 @@ class Stm32Loader: # serial link bit parity, compatible to pyserial serial.PARTIY_EVEN PARITY = {"even": "E", "none": "N"} + BOOLEAN_FLAG_OPTIONS = { + "-e": "erase", + "-u": "unprotect", + "-w": "write", + "-v": "verify", + "-r": "read", + "-s": "swap_rts_dtr", + "-n": "hide_progress_bar", + "-R": "reset_active_high", + "-B": "boot0_active_high", + } + + INTEGER_OPTIONS = {"-b": "baud", "-a": "address", "-g": "go_address", "-l": "length"} + def __init__(self): """Construct Stm32Loader object with default settings.""" self.bootloader = None @@ -56,6 +71,7 @@ def __init__(self): "swap_rts_dtr": False, "reset_active_high": False, "boot0_active_high": False, + "hide_progress_bar": False, "data_file": None, } self.verbosity = DEFAULT_VERBOSITY @@ -67,10 +83,9 @@ def debug(self, level, message): def parse_arguments(self, arguments): """Parse the list of command-line arguments.""" - # pylint: disable=too-many-branches, eval-used try: # parse command-line arguments using getopt - options, arguments = getopt.getopt(arguments, "hqVeuwvrsRBP:p:b:a:l:g:f:", ["help"]) + options, arguments = getopt.getopt(arguments, "hqVeuwvrsnRBP:p:b:a:l:g:f:", ["help"]) except getopt.GetoptError as err: # print help information and exit: # this prints something like "option -a not recognized" @@ -82,49 +97,7 @@ def parse_arguments(self, arguments): if arguments: self.configuration["data_file"] = arguments[0] - for option, value in options: - if option == "-V": - self.verbosity = 10 - elif option == "-q": - self.verbosity = 0 - elif option in ["-h", "--help"]: - self.print_usage() - sys.exit(0) - elif option == "-e": - self.configuration["erase"] = True - elif option == "-u": - self.configuration["unprotect"] = True - elif option == "-w": - self.configuration["write"] = True - elif option == "-v": - self.configuration["verify"] = True - elif option == "-r": - self.configuration["read"] = True - elif option == "-p": - self.configuration["port"] = value - elif option == "-s": - self.configuration["swap_rts_dtr"] = True - elif option == "-R": - self.configuration["reset_active_high"] = True - elif option == "-B": - self.configuration["boot0_active_high"] = True - elif option == "-b": - self.configuration["baud"] = int(eval(value)) - elif option == "-f": - self.configuration["family"] = value - elif option == "-P": - assert ( - value.lower() in Stm32Loader.PARITY - ), "Parity value not recognized: '{0}'.".format(value) - self.configuration["parity"] = Stm32Loader.PARITY[value.lower()] - elif option == "-a": - self.configuration["address"] = int(eval(value)) - elif option == "-g": - self.configuration["go_address"] = int(eval(value)) - elif option == "-l": - self.configuration["length"] = int(eval(value)) - else: - assert False, "unhandled option %s" % option + self._parse_option_flags(options) def connect(self): """Connect to the RS-232 serial port.""" @@ -155,7 +128,11 @@ def connect(self): serial_connection.reset_active_high = self.configuration["reset_active_high"] serial_connection.boot0_active_high = self.configuration["boot0_active_high"] - self.bootloader = Stm32Bootloader(serial_connection, verbosity=self.verbosity) + show_progress = self._get_progress_bar(self.configuration["hide_progress_bar"]) + + self.bootloader = Stm32Bootloader( + serial_connection, verbosity=self.verbosity, show_progress=show_progress + ) try: self.bootloader.reset_from_system_memory() @@ -245,6 +222,7 @@ def print_usage(): -R Make reset active high -B Make boot0 active high -u Readout unprotect + -n No progress: don't show progress bar -P parity Parity: "even" for STM32 (default), "none" for BlueNRG Example: ./%s -p COM7 -f F1 @@ -271,6 +249,49 @@ def read_device_details(self): flash_size = self.bootloader.get_flash_size(family) self.debug(0, "Flash size: %d KiB" % flash_size) + def _parse_option_flags(self, options): + # pylint: disable=eval-used + for option, value in options: + if option == "-V": + self.verbosity = 10 + elif option == "-q": + self.verbosity = 0 + elif option in ["-h", "--help"]: + self.print_usage() + sys.exit(0) + elif option == "-p": + self.configuration["port"] = value + elif option == "-f": + self.configuration["family"] = value + elif option == "-P": + assert ( + value.lower() in Stm32Loader.PARITY + ), "Parity value not recognized: '{0}'.".format(value) + self.configuration["parity"] = Stm32Loader.PARITY[value.lower()] + elif option in self.INTEGER_OPTIONS: + self.configuration[self.INTEGER_OPTIONS[option]] = int(eval(value)) + elif option in self.BOOLEAN_FLAG_OPTIONS: + self.configuration[self.BOOLEAN_FLAG_OPTIONS[option]] = True + else: + assert False, "unhandled option %s" % option + + @staticmethod + def _get_progress_bar(hide_progress_bar=False): + if hide_progress_bar: + return None + desired_progress_bar = None + try: + from progress.bar import ChargingBar as desired_progress_bar + except ImportError: + # progress module is a package dependency, + # but not strictly required + pass + + if not desired_progress_bar: + return None + + return ShowProgress(desired_progress_bar) + def main(*args, **kwargs): """ From 59e9bd61f5f7449750846ec2fece942087714d95 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Apr 2019 22:16:20 +0200 Subject: [PATCH 128/369] Rename 'bootloader' to 'stm32' --- stm32loader/main.py | 39 +++++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/stm32loader/main.py b/stm32loader/main.py index 2fd875a..4c64462 100644 --- a/stm32loader/main.py +++ b/stm32loader/main.py @@ -26,8 +26,7 @@ import getopt import sys -from .bootloader import CHIP_IDS, CommandException, ShowProgress, \ - Stm32Bootloader +from .bootloader import CHIP_IDS, CommandException, ShowProgress, Stm32Bootloader from .rs232 import SerialConnection DEFAULT_VERBOSITY = 5 @@ -55,7 +54,7 @@ class Stm32Loader: def __init__(self): """Construct Stm32Loader object with default settings.""" - self.bootloader = None + self.stm32 = None self.configuration = { "port": "/dev/tty.usbserial-ftCYPMYJ", "baud": 115200, @@ -130,15 +129,15 @@ def connect(self): show_progress = self._get_progress_bar(self.configuration["hide_progress_bar"]) - self.bootloader = Stm32Bootloader( + self.stm32 = Stm32Bootloader( serial_connection, verbosity=self.verbosity, show_progress=show_progress ) try: - self.bootloader.reset_from_system_memory() + self.stm32.reset_from_system_memory() except CommandException: print("Can't init into bootloader. Ensure that BOOT0 is enabled and reset device.") - self.bootloader.reset_from_flash() + self.stm32.reset_from_flash() sys.exit(1) def perform_commands(self): @@ -150,16 +149,16 @@ def perform_commands(self): binary_data = bytearray(read_file.read()) if self.configuration["unprotect"]: try: - self.bootloader.readout_unprotect() + self.stm32.readout_unprotect() except CommandException: # may be caused by readout protection self.debug(0, "Erase failed -- probably due to readout protection") self.debug(0, "Quit") - self.bootloader.reset_from_flash() + self.stm32.reset_from_flash() sys.exit(1) if self.configuration["erase"]: try: - self.bootloader.erase_memory() + self.stm32.erase_memory() except CommandException: # may be caused by readout protection self.debug( @@ -167,12 +166,12 @@ def perform_commands(self): "Erase failed -- probably due to readout protection\n" "consider using the -u (unprotect) option.", ) - self.bootloader.reset_from_flash() + self.stm32.reset_from_flash() sys.exit(1) if self.configuration["write"]: - self.bootloader.write_memory_data(self.configuration["address"], binary_data) + self.stm32.write_memory_data(self.configuration["address"], binary_data) if self.configuration["verify"]: - read_data = self.bootloader.read_memory_data( + read_data = self.stm32.read_memory_data( self.configuration["address"], len(binary_data) ) if binary_data == read_data: @@ -185,17 +184,17 @@ def perform_commands(self): if binary_byte != read_byte: print(hex(address) + ": " + hex(binary_byte) + " vs " + hex(read_byte)) if not self.configuration["write"] and self.configuration["read"]: - read_data = self.bootloader.read_memory_data( + read_data = self.stm32.read_memory_data( self.configuration["address"], self.configuration["length"] ) with open(self.configuration["data_file"], "wb") as out_file: out_file.write(read_data) if self.configuration["go_address"] != -1: - self.bootloader.go(self.configuration["go_address"]) + self.stm32.go(self.configuration["go_address"]) def reset(self): """Reset the microcontroller.""" - self.bootloader.reset_from_flash() + self.stm32.reset_from_flash() @staticmethod def print_usage(): @@ -234,19 +233,19 @@ def print_usage(): def read_device_details(self): """Show MCU details (bootloader version, chip ID, UID, flash size).""" - boot_version = self.bootloader.get() + boot_version = self.stm32.get() self.debug(0, "Bootloader version %X" % boot_version) - device_id = self.bootloader.get_id() + device_id = self.stm32.get_id() self.debug(0, "Chip id: 0x%x (%s)" % (device_id, CHIP_IDS.get(device_id, "Unknown"))) family = self.configuration["family"] if not family: self.debug(0, "Supply -f [family] to see flash size and device UID, e.g: -f F1") else: - device_uid = self.bootloader.get_uid(family) - device_uid_string = self.bootloader.format_uid(device_uid) + device_uid = self.stm32.get_uid(family) + device_uid_string = self.stm32.format_uid(device_uid) self.debug(0, "Device UID: %s" % device_uid_string) - flash_size = self.bootloader.get_flash_size(family) + flash_size = self.stm32.get_flash_size(family) self.debug(0, "Flash size: %d KiB" % flash_size) def _parse_option_flags(self, options): From 5b01af7a936a173386b1f2a459a33dc6577e1e1b Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Apr 2019 22:16:20 +0200 Subject: [PATCH 129/369] Add more exception types --- stm32loader/bootloader.py | 24 ++++++++++++++++++++---- stm32loader/main.py | 21 +++++++++++++-------- tests/unit/test_bootloader.py | 5 +++-- 3 files changed, 36 insertions(+), 14 deletions(-) diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index cc7070d..a7d98d8 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -58,8 +58,24 @@ } -class CommandException(IOError): - """Error: a command in the STM32 native bootloader failed.""" +class Stm32LoaderError(Exception): + """Generic exception type for errors occurring in stm32loader.""" + + +class CommandError(Stm32LoaderError, IOError): + """Exception: a command in the STM32 native bootloader failed.""" + + +class PageIndexError(Stm32LoaderError, ValueError): + """Exception: invalid page index given.""" + + +class DataLengthError(Stm32LoaderError, ValueError): + """Exception: invalid data length given.""" + + +class DataMismatchError(Stm32LoaderError): + """Exception: data comparison failed.""" class ShowProgress: @@ -248,12 +264,12 @@ def command(self, command, description): """ Send the given command to the MCU. - Raise CommandException if there's no ACK replied. + Raise CommandError if there's no ACK replied. """ self.debug(10, "*** Command: %s" % description) ack_received = self.write_and_ack("Command", command, command ^ 0xFF) if not ack_received: - raise CommandException("%s (%s) failed: no ack" % (description, command)) + raise CommandError("%s (%s) failed: no ack" % (description, command)) def get(self): """Return the bootloader version and remember supported commands.""" diff --git a/stm32loader/main.py b/stm32loader/main.py index 4c64462..5675303 100644 --- a/stm32loader/main.py +++ b/stm32loader/main.py @@ -26,7 +26,7 @@ import getopt import sys -from .bootloader import CHIP_IDS, CommandException, ShowProgress, Stm32Bootloader +from . import bootloader from .rs232 import SerialConnection DEFAULT_VERBOSITY = 5 @@ -129,14 +129,17 @@ def connect(self): show_progress = self._get_progress_bar(self.configuration["hide_progress_bar"]) - self.stm32 = Stm32Bootloader( + self.stm32 = bootloader.Stm32Bootloader( serial_connection, verbosity=self.verbosity, show_progress=show_progress ) try: self.stm32.reset_from_system_memory() - except CommandException: - print("Can't init into bootloader. Ensure that BOOT0 is enabled and reset device.") + except bootloader.CommandError: + print( + "Can't init into bootloader. Ensure that BOOT0 is enabled and reset the device.", + file=sys.stderr, + ) self.stm32.reset_from_flash() sys.exit(1) @@ -150,7 +153,7 @@ def perform_commands(self): if self.configuration["unprotect"]: try: self.stm32.readout_unprotect() - except CommandException: + except bootloader.CommandError: # may be caused by readout protection self.debug(0, "Erase failed -- probably due to readout protection") self.debug(0, "Quit") @@ -159,7 +162,7 @@ def perform_commands(self): if self.configuration["erase"]: try: self.stm32.erase_memory() - except CommandException: + except bootloader.CommandError: # may be caused by readout protection self.debug( 0, @@ -236,7 +239,9 @@ def read_device_details(self): boot_version = self.stm32.get() self.debug(0, "Bootloader version %X" % boot_version) device_id = self.stm32.get_id() - self.debug(0, "Chip id: 0x%x (%s)" % (device_id, CHIP_IDS.get(device_id, "Unknown"))) + self.debug( + 0, "Chip id: 0x%x (%s)" % (device_id, bootloader.CHIP_IDS.get(device_id, "Unknown")) + ) family = self.configuration["family"] if not family: self.debug(0, "Supply -f [family] to see flash size and device UID, e.g: -f F1") @@ -289,7 +294,7 @@ def _get_progress_bar(hide_progress_bar=False): if not desired_progress_bar: return None - return ShowProgress(desired_progress_bar) + return bootloader.ShowProgress(desired_progress_bar) def main(*args, **kwargs): diff --git a/tests/unit/test_bootloader.py b/tests/unit/test_bootloader.py index 230735b..e0faeca 100644 --- a/tests/unit/test_bootloader.py +++ b/tests/unit/test_bootloader.py @@ -2,7 +2,8 @@ import pytest -from stm32loader.bootloader import CommandException, Stm32Bootloader +from stm32loader import bootloader as Stm32 +from stm32loader.bootloader import Stm32Bootloader try: from unittest.mock import MagicMock @@ -67,7 +68,7 @@ def test_write_with_integers_sends_integers_as_bytes(bootloader, write): def test_write_and_ack_with_nack_response_raises_commandexception(bootloader): bootloader.connection.read = MagicMock() bootloader.connection.read.return_value = [Stm32Bootloader.Reply.NACK] - with pytest.raises(CommandException, match="custom message"): + with pytest.raises(Stm32.CommandError, match="custom message"): bootloader.write_and_ack("custom message", 0x00) From 70f73d6a9e91171353ea887635b175cb001e0f06 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Apr 2019 22:16:21 +0200 Subject: [PATCH 130/369] Add additional command words (unused for now) --- stm32loader/bootloader.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index a7d98d8..88c2a48 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -166,6 +166,10 @@ class Command: WRITE_PROTECT = 0x63 WRITE_UNPROTECT = 0x73 + # not used so far + READOUT_PROTECT = 0x82 + READOUT_UNPROTECT = 0x92 + # not really listed under commands, but still... # 'wake the bootloader' == 'activate USART' == 'synchronize' SYNCHRONIZE = 0x7F From 7e6fb9a1e2a86a10f423e567230826fce3a5f0f8 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Apr 2019 22:16:22 +0200 Subject: [PATCH 131/369] Define page size as a class variable --- stm32loader/bootloader.py | 17 ++++++++++------- tests/unit/test_bootloader.py | 10 ++++++++++ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index 88c2a48..d5c66ac 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -209,6 +209,7 @@ class Reply: } extended_erase = False + PAGE_SIZE = 256 # bytes def __init__(self, connection, verbosity=5, show_progress=None): """ @@ -314,7 +315,7 @@ def get_flash_size(self, device_family): """Return the MCU's flash size in bytes.""" flash_size_address = self.FLASH_SIZE_ADDRESS[device_family] flash_size_bytes = self.read_memory(flash_size_address, 2) - flash_size = flash_size_bytes[0] + flash_size_bytes[1] * 256 + flash_size = flash_size_bytes[0] + flash_size_bytes[1] * self.PAGE_SIZE return flash_size def get_uid(self, device_id): @@ -336,7 +337,8 @@ def read_memory(self, address, length): Supports maximum 256 bytes. """ - assert length <= 256 + if length > self.PAGE_SIZE: + raise DataLengthError("Can not read more than 256 bytes at once.") self.command(self.Command.READ_MEMORY, "Read memory") self.write_and_ack("0x11 address failed", self._encode_address(address)) nr_of_bytes = (length - 1) & 0xFF @@ -359,7 +361,8 @@ def write_memory(self, address, data): nr_of_bytes = len(data) if nr_of_bytes == 0: return - assert nr_of_bytes <= 256 + if nr_of_bytes > self.PAGE_SIZE: + raise DataLengthError("Can not write more than 256 bytes at once.") self.command(self.Command.WRITE_MEMORY, "Write memory") self.write_and_ack("0x31 address failed", self._encode_address(address)) @@ -451,11 +454,11 @@ def read_memory_data(self, address, length): Length may be more than 256 bytes. """ data = bytearray() - page_count = int(math.ceil(length / 256.0)) + page_count = int(math.ceil(length / float(self.PAGE_SIZE))) self.debug(5, "Read %d pages at address 0x%X..." % (page_count, address)) with self.show_progress("Reading", maximum=page_count) as progress_bar: while length: - read_length = min(length, 256) + read_length = min(length, self.PAGE_SIZE) self.debug( 10, "Read %(len)d bytes at 0x%(address)X" @@ -474,13 +477,13 @@ def write_memory_data(self, address, data): Data length may be more than 256 bytes. """ length = len(data) - page_count = int(math.ceil(length / 256.0)) + page_count = int(math.ceil(length / float(self.PAGE_SIZE))) offset = 0 self.debug(5, "Write %d pages at address 0x%X..." % (page_count, address)) with self.show_progress("Writing", maximum=page_count) as progress_bar: while length: - write_length = min(length, 256) + write_length = min(length, self.PAGE_SIZE) self.debug( 10, "Write %(len)d bytes at 0x%(address)X" diff --git a/tests/unit/test_bootloader.py b/tests/unit/test_bootloader.py index e0faeca..655c51e 100644 --- a/tests/unit/test_bootloader.py +++ b/tests/unit/test_bootloader.py @@ -72,6 +72,11 @@ def test_write_and_ack_with_nack_response_raises_commandexception(bootloader): bootloader.write_and_ack("custom message", 0x00) +def test_write_memory_with_length_higher_than_256_raises_data_length_error(bootloader): + with pytest.raises(Stm32.DataLengthError, match=r"Can not write more than 256 bytes at once\."): + bootloader.write_memory(0, [1] * 257) + + def test_write_memory_with_zero_bytes_does_not_send_anything(bootloader, connection): bootloader.write_memory(0, b"") assert not connection.method_calls @@ -90,6 +95,11 @@ def test_write_memory_sends_correct_number_of_bytes(bootloader, write): assert len(write.written_data) == byte_count +def test_read_memory_with_length_higher_than_256_raises_data_length_error(bootloader): + with pytest.raises(Stm32.DataLengthError, match=r"Can not read more than 256 bytes at once\."): + bootloader.read_memory(0, length=257) + + def test_read_memory_sends_address_with_checksum(bootloader, write): bootloader.read_memory(0x0f, 4) assert write.data_was_written(b'\x00\x00\x00\x0f\x0f') From 43f8a9590b688c7ca215c02bb34aa42e712ce63c Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Fri, 19 Apr 2019 21:48:13 +0200 Subject: [PATCH 132/369] Rewrite erase functionality Extended_erase now also supports paged erase and uses proper two-byte addressing. --- CHANGELOG.md | 1 + README.md | 5 +- stm32loader/bootloader.py | 71 ++++++++++++++++++----- tests/integration/test_stm32bootloader.py | 68 ++++++++++++++++++++++ tests/unit/test_bootloader.py | 34 +++++++++-- 5 files changed, 156 insertions(+), 23 deletions(-) create mode 100644 tests/integration/test_stm32bootloader.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 727d141..030e474 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ v0.4.0 * #9: Support data writes smaller than 256 bytes. By NINI1988. * #10: Make stm32loader useful as a library. * #4: Bring back support for progress bar. +* #11: Support page erase in extended (two-byte addressing) erase mode * Start using code linting and unit tests. v0.3.3 diff --git a/README.md b/README.md index 6296063..6141c85 100644 --- a/README.md +++ b/README.md @@ -67,13 +67,13 @@ Atokulus, sam-bristow, NINI1988. Inspiration for features from: -* Configurable RTS/DTR and polarity, extended erase with sectors: +* Configurable RTS/DTR and polarity, extended erase with pages: https://github.com/pazzarpj/stm32loader * Memory unprotect https://github.com/3drobotics/stm32loader -* Correct checksum calculation for sector erase: +* Correct checksum calculation for paged erase: https://github.com/jsnyder/stm32loader/pull/4 * ST BlueNRG chip support @@ -110,7 +110,6 @@ adepter (it needs to toggle, whereas BOOT0 does not). Not currently supported ----------------------- -* Extended erase with specific sectors * Command-line argument for readout protection * Command-line argument for write protection/unprotection * STM8 devices (ST UM0560) diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index d5c66ac..b368fb6 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -208,7 +208,6 @@ class Reply: "F7": 0x1FF0F442, } - extended_erase = False PAGE_SIZE = 256 # bytes def __init__(self, connection, verbosity=5, show_progress=None): @@ -234,6 +233,7 @@ def __init__(self, connection, verbosity=5, show_progress=None): self._toggle_boot0 = getattr(connection, "can_toggle_boot0", False) self.verbosity = verbosity self.show_progress = show_progress or ShowProgress(None) + self.extended_erase = False def write(self, *data): """Write the given data to the MCU.""" @@ -379,21 +379,31 @@ def write_memory(self, address, data): self.write_and_ack("0x31 programming failed", nr_of_bytes - 1, data, checksum) self.debug(10, " Write memory done") - def erase_memory(self, sectors=None): + def erase_memory(self, pages=None): """ - Erase flash memory at the given sectors. + Erase flash memory at the given pages. - Set sectors to None to erase the full memory. + Set pages to None to erase the full memory. + :param iterable pages: Iterable of integer page addresses, zero-based. + Set to None to trigger global mass erase. """ - if self.extended_erase and not sectors: - self.extended_erase_memory() + if self.extended_erase: + # use erase with two-byte addresses + self.extended_erase_memory(pages) return + self.command(self.Command.ERASE, "Erase memory") - if sectors: + if pages: # page erase, see ST AN3155 - page_count = len(sectors) - checksum = reduce(operator.xor, sectors, page_count) - self.write(page_count, sectors, checksum) + if len(pages) > 255: + raise PageIndexError( + "Can not erase more than 255 pages at once.\n" + "Set pages to None to do global erase or supply fewer pages." + ) + page_count = (len(pages) - 1) & 0xFF + page_numbers = bytearray(pages) + checksum = reduce(operator.xor, page_numbers, page_count) + self.write(page_count, page_numbers, checksum) else: # global erase: n=255 (page count) self.write(255, 0) @@ -401,16 +411,45 @@ def erase_memory(self, sectors=None): self._wait_for_ack("0x43 erase failed") self.debug(10, " Erase memory done") - def extended_erase_memory(self): - """Send the extended erase command to erase the full flash content.""" + def extended_erase_memory(self, pages=None): + """ + Erase flash memory using two-byte addressing at the given pages. + + Set pages to None to erase the full memory. + + Not all devices support the extended erase command. + + :param iterable pages: Iterable of integer page addresses, zero-based. + Set to None to trigger global mass erase. + """ self.command(self.Command.EXTENDED_ERASE, "Extended erase memory") - # Global mass erase and checksum byte - self.write(b"\xff\xff\x00") + if pages: + # page erase, see ST AN3155 + if len(pages) > 65535: + raise PageIndexError( + "Can not erase more than 65535 pages at once.\n" + "Set pages to None to do global erase or supply fewer pages." + ) + page_count = (len(pages) & 0xFF) - 1 + page_count_bytes = bytearray(struct.pack(">H", page_count)) + page_bytes = bytearray(len(pages) * 2) + for i, page in enumerate(pages): + struct.pack_into(">H", page_bytes, i * 2, page) + checksum = reduce(operator.xor, page_count_bytes) + checksum = reduce(operator.xor, page_bytes, checksum) + self.write(page_count_bytes, page_bytes, checksum) + else: + # global mass erase: n=0xffff (page count) + checksum + # TO DO: support 0xfffe bank 1 erase / 0xfffe bank 2 erase + self.write(b"\xff\xff\x00") + previous_timeout_value = self.connection.timeout self.connection.timeout = 30 print("Extended erase (0x44), this can take ten seconds or more") - self._wait_for_ack("0x44 erasing failed") - self.connection.timeout = previous_timeout_value + try: + self._wait_for_ack("0x44 erasing failed") + finally: + self.connection.timeout = previous_timeout_value self.debug(10, " Extended Erase memory done") def write_protect(self, pages): diff --git a/tests/integration/test_stm32bootloader.py b/tests/integration/test_stm32bootloader.py new file mode 100644 index 0000000..af01776 --- /dev/null +++ b/tests/integration/test_stm32bootloader.py @@ -0,0 +1,68 @@ +""" +Tests for the stm32loader.bootloader. + +Several of these tests require an actual STM32 microcontroller to be +connected, and to be programmable (including RESET and BOOT0 toggling). + +These hardware tests are disabled by default. +To enable them, configure the device parameters below and +supply the following as argument to pytest: + + -m "hardware" + +""" + +from stm32loader.bootloader import Stm32Bootloader +from stm32loader.rs232 import SerialConnection + +import pytest + +SERIAL_PORT = "COM7" +BAUD_RATE = 9600 + +# pylint: disable=missing-docstring, redefined-outer-name + + +@pytest.fixture +def serial_connection(): + serial_connection = SerialConnection(SERIAL_PORT, BAUD_RATE) + serial_connection.connect() + return serial_connection + + +@pytest.fixture +def stm32(serial_connection): + stm32 = Stm32Bootloader(serial_connection) + return stm32 + + +@pytest.mark.hardware +def test_erase_with_page_erases_only_that_page(stm32): + stm32.reset_from_system_memory() + base = 0x08000000 + before, middle, after = base + 0, base + 256, base + 512 + + # erase full device + stm32.erase_memory() + + # check that erase was successful + assert all(byte == 0xFF for byte in stm32.read_memory(before, 16)) + assert all(byte == 0xFF for byte in stm32.read_memory(middle, 16)) + assert all(byte == 0xFF for byte in stm32.read_memory(after, 16)) + + # write zeros to three pages (and verify success) + stm32.write_memory(before, bytearray([0x00] * 16)) + stm32.write_memory(middle, bytearray([0x00] * 16)) + stm32.write_memory(after, bytearray([0x00] * 16)) + assert all(byte == 0x00 for byte in stm32.read_memory(before, 16)) + assert all(byte == 0x00 for byte in stm32.read_memory(middle, 16)) + assert all(byte == 0x00 for byte in stm32.read_memory(after, 16)) + + # erase only the middle page + stm32.erase_memory(pages=[0]) + + # check that middle page is erased, others are not + assert all(byte == 0x00 for byte in stm32.read_memory(before, 16)) + assert all(byte == 0xFF for byte in stm32.read_memory(middle, 256)) + assert all(byte == 0x00 for byte in stm32.read_memory(after, 16)) + diff --git a/tests/unit/test_bootloader.py b/tests/unit/test_bootloader.py index 655c51e..1f3eb30 100644 --- a/tests/unit/test_bootloader.py +++ b/tests/unit/test_bootloader.py @@ -127,21 +127,47 @@ def test_encode_address_returns_correct_bytes_with_checksum(): assert bytes(encoded_address) == b"\x04\x03\x02\x01\x04" -def test_erase_memory_without_sectors_sends_global_erase(bootloader, write): +def test_erase_memory_without_pages_sends_global_erase(bootloader, write): bootloader.erase_memory() assert write.data_was_written(b'\xff\x00') -def test_erase_memory_with_sectors_sends_sector_addresses_with_(bootloader, write): +def test_erase_memory_with_pages_sends_sector_count(bootloader, write): + bootloader.erase_memory([0x11, 0x12, 0x13, 0x14]) + assert write.data_was_written(b'\x03') + + +def test_erase_memory_with_pages_sends_sector_addresses_with_checksum(bootloader, write): bootloader.erase_memory([0x01, 0x02, 0x04, 0x08]) - assert write.data_was_written(b'\x01\x02\x04\x08\x0b') + print(write.written_data) + assert write.data_was_written(b'\x01\x02\x04\x08\x0c') + +def test_erase_memory_with_page_count_higher_than_255_raises_page_index_error(bootloader): + with pytest.raises(Stm32.PageIndexError, match="Can not erase more than 255 pages at once."): + bootloader.erase_memory([1] * 256) -def test_extended_erase_memory_sends_global_mass_erase(bootloader, write): + +def test_extended_erase_memory_without_pages_sends_global_mass_erase(bootloader, write): bootloader.extended_erase_memory() assert write.data_was_written(b'\xff\xff\x00') +def test_extended_erase_memory_with_page_count_higher_than_65535_raises_page_index_error(bootloader): + with pytest.raises(Stm32.PageIndexError, match="Can not erase more than 65535 pages at once."): + bootloader.extended_erase_memory([1] * 65536) + + +def test_extended_erase_memory_with_pages_sends_two_byte_sector_count(bootloader, write): + bootloader.extended_erase_memory([0x11, 0x12, 0x13, 0x14]) + assert write.data_was_written(b'\x00\x03') + + +def test_extended_erase_memory_with_pages_sends_two_byte_sector_addresses_with_single_byte_checksum(bootloader, write): + bootloader.extended_erase_memory([0x01, 0x02, 0x04, 0x0ff0]) + assert write.data_was_written(b'\x00\x01\x00\x02\x00\x04\x0f\xf0\xfb') + + def test_write_protect_sends_page_addresses_and_checksum(bootloader, write): bootloader.write_protect([0x01, 0x08]) assert write.data_was_written(b'\x01\x08\x08') From d7cebbc21b38d59c1c91acfef1220620d8241a4d Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Fri, 19 Apr 2019 21:48:17 +0200 Subject: [PATCH 133/369] Move verification into Stm32Bootloader This makes it easier for third parties to use it. --- stm32loader/bootloader.py | 30 ++++++++++++++++++++++++++++++ stm32loader/main.py | 13 +++++-------- tests/unit/test_bootloader.py | 14 ++++++++++++++ 3 files changed, 49 insertions(+), 8 deletions(-) diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index b368fb6..287860d 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -534,6 +534,36 @@ def write_memory_data(self, address, data): offset += write_length address += write_length + @staticmethod + def verify_data(read_data, reference_data): + """ + Raise an error if the given data does not match its reference. + + Error type is DataMismatchError. + + :param read_data: Data to compare. + :param reference_data: Data to compare, as reference. + :return None: + """ + if read_data == reference_data: + return + + if len(read_data) != len(reference_data): + raise DataMismatchError( + "Data length does not match: %d bytes vs %d bytes." + % (len(read_data), len(reference_data)) + ) + + # data differs; find out where and raise VerifyError + for address, data_pair in enumerate(zip(reference_data, read_data)): + reference_byte, read_byte = data_pair + if reference_byte != read_byte: + raise DataMismatchError( + "Verification data does not match read data. " + "First mismatch at address: 0x%X read 0x%X vs 0x%X expected." + % (address, bytearray([read_byte])[0], bytearray([reference_byte])[0]) + ) + def _reset(self): """Enable or disable the reset IO line (if possible).""" if not self._toggle_reset: diff --git a/stm32loader/main.py b/stm32loader/main.py index 5675303..2864afe 100644 --- a/stm32loader/main.py +++ b/stm32loader/main.py @@ -177,15 +177,12 @@ def perform_commands(self): read_data = self.stm32.read_memory_data( self.configuration["address"], len(binary_data) ) - if binary_data == read_data: + try: + bootloader.Stm32Bootloader.verify_data(read_data, binary_data) print("Verification OK") - else: - print("Verification FAILED") - print(str(len(binary_data)) + " vs " + str(len(read_data))) - for address, data_pair in enumerate(zip(binary_data, read_data)): - binary_byte, read_byte = data_pair - if binary_byte != read_byte: - print(hex(address) + ": " + hex(binary_byte) + " vs " + hex(read_byte)) + except bootloader.DataMismatchError as e: + print("Verification FAILED: %s" % e, file=sys.stdout) + sys.exit(1) if not self.configuration["write"] and self.configuration["read"]: read_data = self.stm32.read_memory_data( self.configuration["address"], self.configuration["length"] diff --git a/tests/unit/test_bootloader.py b/tests/unit/test_bootloader.py index 1f3eb30..bc7d2f4 100644 --- a/tests/unit/test_bootloader.py +++ b/tests/unit/test_bootloader.py @@ -171,3 +171,17 @@ def test_extended_erase_memory_with_pages_sends_two_byte_sector_addresses_with_s def test_write_protect_sends_page_addresses_and_checksum(bootloader, write): bootloader.write_protect([0x01, 0x08]) assert write.data_was_written(b'\x01\x08\x08') + + +def test_verify_data_with_identical_data_passes(): + Stm32Bootloader.verify_data(b'\x05', b'\x05') + + +def test_verify_data_with_different_byte_count_raises_verify_error_complaining_about_length_difference(): + with pytest.raises(Stm32.DataMismatchError, match=r"Data length does not match.*2.*vs.*1.*bytes"): + Stm32Bootloader.verify_data(b'\x05\x06', b'\x01') + + +def test_verify_data_with_non_identical_data_raises_verify_error_complaining_about_mismatched_byte(): + with pytest.raises(Stm32.DataMismatchError, match=r"Verification data does not match read data.*mismatch.*0x1.*0x6.*0x7"): + Stm32Bootloader.verify_data(b'\x05\x06', b'\x05\x07') From 8d70272a2265459cef7d90e3727b80bb98e5237a Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Fri, 19 Apr 2019 21:48:18 +0200 Subject: [PATCH 134/369] Don't crash when read() doesn't return data --- setup.cfg | 3 ++- stm32loader/bootloader.py | 19 +++++++++---------- tests/integration/test_stm32loader.py | 16 ++++++++++++++++ 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/setup.cfg b/setup.cfg index 6ca581e..bdc13aa 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,6 +30,7 @@ per-file-ignores = stm32loader/bootloader.py:B305, [tool:pytest] -addopts = --strict -m "not hardware" +addopts = --strict -m "not (hardware or hardware_missing)" markers = hardware + missing_hardware diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index 287860d..4f40996 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -581,16 +581,15 @@ def _enable_boot0(self, enable=True): self.connection.enable_boot0(enable) def _wait_for_ack(self, info=""): - """Read a byte and raise CommandException if it's not ACK.""" - try: - ack = bytearray(self.connection.read())[0] - except TypeError: - raise CommandException("Can't read port or timeout") - - if ack == self.Reply.NACK: - raise CommandException("NACK " + info) - if ack != self.Reply.ACK: - raise CommandException("Unknown response. " + info + ": " + hex(ack)) + """Read a byte and raise CommandError if it's not ACK.""" + read_data = bytearray(self.connection.read()) + if not read_data: + raise CommandError("Can't read port or timeout") + reply = read_data[0] + if reply == self.Reply.NACK: + raise CommandError("NACK " + info) + if reply != self.Reply.ACK: + raise CommandError("Unknown response. " + info + ": " + hex(reply)) return 1 diff --git a/tests/integration/test_stm32loader.py b/tests/integration/test_stm32loader.py index a8a85b2..23a02b6 100644 --- a/tests/integration/test_stm32loader.py +++ b/tests/integration/test_stm32loader.py @@ -62,6 +62,22 @@ def test_argument_h_prints_help_info(help_argument, capsys): assert "Example:" in captured.out +def test_unexisting_serial_port_prints_readable_error(capsys): + main("-p", "COM108", avoid_system_exit=True) + captured = capsys.readouterr() + assert "could not open port 'COM108'" in captured.err + assert "Is the device connected and powered correctly?" in captured.err + + +@pytest.mark.hardware +@pytest.mark.missing_hardware +def test_device_not_connected_prints_readable_error(stm32loader, capsys): + stm32loader() + captured = capsys.readouterr() + assert "Can't init into bootloader." in captured.err + assert "Ensure that BOOT0 is enabled and reset the device." in captured.err + + @pytest.mark.hardware def test_argument_f_prints_chip_id_and_device_type(stm32loader, capsys): stm32loader("-f", STM32_CHIP_FAMILY) From 8a1663b56f6513fca83e1cda7bd6c8b7195b93d2 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Fri, 19 Apr 2019 21:48:19 +0200 Subject: [PATCH 135/369] Get serial port from env var STM32LOADER_SERIAL_PORT --- CHANGELOG.md | 1 + README.md | 6 ++++-- stm32loader/main.py | 11 ++++++++++- tests/integration/test_stm32loader.py | 22 ++++++++++++++++++++++ 4 files changed, 37 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 030e474..1a8f15f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ v0.4.0 * #10: Make stm32loader useful as a library. * #4: Bring back support for progress bar. * #11: Support page erase in extended (two-byte addressing) erase mode +* #12: Allow to supply the serial port as an environment variable * Start using code linting and unit tests. v0.3.3 diff --git a/README.md b/README.md index 6141c85..858f47d 100644 --- a/README.md +++ b/README.md @@ -44,11 +44,14 @@ Example ------- ``` -stm32loader.py -e -w -v somefile.bin +stm32loader.py -p /dev/tty.usbserial-ftCYPMYJ -e -w -v somefile.bin ``` This will pre-erase flash, write `somefile.bin` to the flash on the device, and then perform a verification after writing is finished. +You can skip the `-p` option by configuring environment variable +`STM32LOADER_SERIAL_PORT`. + Reference documents ------------------- @@ -118,7 +121,6 @@ Not currently supported Future work ----------- -* Allow to set default serial port through environment variable * Use proper logging instead of print statements * Use Travis or Azure pipelines for CI * Support PyPy, PyPy3 diff --git a/stm32loader/main.py b/stm32loader/main.py index 2864afe..a7b5941 100644 --- a/stm32loader/main.py +++ b/stm32loader/main.py @@ -24,6 +24,7 @@ from __future__ import print_function import getopt +import os import sys from . import bootloader @@ -56,7 +57,7 @@ def __init__(self): """Construct Stm32Loader object with default settings.""" self.stm32 = None self.configuration = { - "port": "/dev/tty.usbserial-ftCYPMYJ", + "port": os.environ.get("STM32LOADER_SERIAL_PORT"), "baud": 115200, "parity": self.PARITY["even"], "family": None, @@ -98,6 +99,14 @@ def parse_arguments(self, arguments): self._parse_option_flags(options) + if not self.configuration["port"]: + print( + "No serial port configured. Supply the -p option " + "or configure environment variable STM32LOADER_SERIAL_PORT.", + file=sys.stderr, + ) + sys.exit(3) + def connect(self): """Connect to the RS-232 serial port.""" serial_connection = SerialConnection( diff --git a/tests/integration/test_stm32loader.py b/tests/integration/test_stm32loader.py index 23a02b6..de2d100 100644 --- a/tests/integration/test_stm32loader.py +++ b/tests/integration/test_stm32loader.py @@ -69,6 +69,28 @@ def test_unexisting_serial_port_prints_readable_error(capsys): assert "Is the device connected and powered correctly?" in captured.err +def test_missing_argument_p_prints_readable_error(capsys): + main(avoid_system_exit=True) + captured = capsys.readouterr() + assert "No serial port configured" in captured.err + assert "Supply the -p option" in captured.err + assert "environment variable STM32LOADER_SERIAL_PORT" in captured.err + + +def test_env_var_stm32loader_serial_port_defines_port(capsys): + os.environ['STM32LOADER_SERIAL_PORT'] = "COM109" + main(avoid_system_exit=True) + captured = capsys.readouterr() + assert "port 'COM109'" in captured.err + + +def test_argument_p_overrules_env_var_for_serial_port(capsys): + os.environ['STM32LOADER_SERIAL_PORT'] = "COM120" + main("-p", "COM121", avoid_system_exit=True) + captured = capsys.readouterr() + assert "port 'COM121'" in captured.err + + @pytest.mark.hardware @pytest.mark.missing_hardware def test_device_not_connected_prints_readable_error(stm32loader, capsys): From a6e7ac6de449c3134811e86fa53dcedf434284eb Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Fri, 19 Apr 2019 21:48:20 +0200 Subject: [PATCH 136/369] Start testing on Travis --- .travis.yml | 30 ++++++++++++++++++++++++++++++ CHANGELOG.md | 1 + README.md | 6 ++++-- 3 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..ea4f1c9 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,30 @@ +language: python + +matrix: + include: + # Lint using nox on native Python 3.6 + - python: "3.6" + env: NOXSESSION="lint" + + # Test using nox on native Python 3.5/3.6/3.7 + - python: "3.5" + env: NOXSESSION="tests-3.5" + - python: "3.6" + env: NOXSESSION="tests-3.6" + - python: "3.7" + env: NOXSESSION="tests-3.7" + dist: xenial # necessary for Python 3.7 + sudo: required # necessary for Python 3.7 + + # Test using nox on non-native Python version 3.6 + # (nox does not natively run on 2.7 / 3.4) + - python: "3.6" + env: NOXSESSION="tests-2.7" + - python: "3.6" + env: NOXSESSION="tests-3.4" + +install: + - pip install --upgrade pip setuptools nox pyserial progress + - pip install . + +script: nox --session "$NOXSESSION" diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a8f15f..2dfe5b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ v0.4.0 * #11: Support page erase in extended (two-byte addressing) erase mode * #12: Allow to supply the serial port as an environment variable * Start using code linting and unit tests. +* Start using Continuous Integration (Travis CI). v0.3.3 ====== diff --git a/README.md b/README.md index 858f47d..a709b2a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ STM32Loader -=========== +=========== + +[![Build Status](https://travis-ci.org/florisla/stm32loader.svg?branch=master)](https://travis-ci.org/florisla/stm32loader) Python script to upload or download firmware to / from ST Microelectronics STM32 microcontrollers over UART. @@ -122,6 +124,6 @@ Not currently supported Future work ----------- * Use proper logging instead of print statements -* Use Travis or Azure pipelines for CI +* Try Azure pipelines for CI * Support PyPy, PyPy3 * Drop Python2 support; start using intenum for commands and replies From 7181ec6002b407356a04f80785ae70eff76ab3c7 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Fri, 19 Apr 2019 21:48:22 +0200 Subject: [PATCH 137/369] Don't choke on missing quotes around the port name Seems to be a platform-specific difference in pyserial. On Windows it has the quote, on Linux is doesn't. --- tests/integration/test_stm32loader.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/integration/test_stm32loader.py b/tests/integration/test_stm32loader.py index de2d100..e451a29 100644 --- a/tests/integration/test_stm32loader.py +++ b/tests/integration/test_stm32loader.py @@ -65,7 +65,8 @@ def test_argument_h_prints_help_info(help_argument, capsys): def test_unexisting_serial_port_prints_readable_error(capsys): main("-p", "COM108", avoid_system_exit=True) captured = capsys.readouterr() - assert "could not open port 'COM108'" in captured.err + assert "could not open port " in captured.err + assert ("port 'COM108'" in captured.err or "port COM108" in captured.err) assert "Is the device connected and powered correctly?" in captured.err @@ -81,14 +82,14 @@ def test_env_var_stm32loader_serial_port_defines_port(capsys): os.environ['STM32LOADER_SERIAL_PORT'] = "COM109" main(avoid_system_exit=True) captured = capsys.readouterr() - assert "port 'COM109'" in captured.err + assert ("port 'COM109'" in captured.err or "port COM109" in captured.err) def test_argument_p_overrules_env_var_for_serial_port(capsys): os.environ['STM32LOADER_SERIAL_PORT'] = "COM120" main("-p", "COM121", avoid_system_exit=True) captured = capsys.readouterr() - assert "port 'COM121'" in captured.err + assert ("port 'COM121'" in captured.err or "port COM121" in captured.err) @pytest.mark.hardware From c9ec83176887c0013dc1bd186c6a7e2dcf444636 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Fri, 19 Apr 2019 21:48:23 +0200 Subject: [PATCH 138/369] Rename rs232 to uart This is the better name, given that 'serial' is ruled out and RS-232 is a protocol with non-compatible signal levels. --- stm32loader/main.py | 2 +- stm32loader/{rs232.py => uart.py} | 0 tests/integration/test_stm32bootloader.py | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename stm32loader/{rs232.py => uart.py} (100%) diff --git a/stm32loader/main.py b/stm32loader/main.py index a7b5941..1ce65e8 100644 --- a/stm32loader/main.py +++ b/stm32loader/main.py @@ -28,7 +28,7 @@ import sys from . import bootloader -from .rs232 import SerialConnection +from .uart import SerialConnection DEFAULT_VERBOSITY = 5 diff --git a/stm32loader/rs232.py b/stm32loader/uart.py similarity index 100% rename from stm32loader/rs232.py rename to stm32loader/uart.py diff --git a/tests/integration/test_stm32bootloader.py b/tests/integration/test_stm32bootloader.py index af01776..c102c6b 100644 --- a/tests/integration/test_stm32bootloader.py +++ b/tests/integration/test_stm32bootloader.py @@ -13,7 +13,7 @@ """ from stm32loader.bootloader import Stm32Bootloader -from stm32loader.rs232 import SerialConnection +from stm32loader.uart import SerialConnection import pytest From 970e3c11402f9640cd2cd4333f53a0e61efd9fff Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Fri, 19 Apr 2019 21:48:24 +0200 Subject: [PATCH 139/369] Read -f ('family') value also from environment --- README.md | 1 + stm32loader/main.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a709b2a..38e3e2e 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ This will pre-erase flash, write `somefile.bin` to the flash on the device, and You can skip the `-p` option by configuring environment variable `STM32LOADER_SERIAL_PORT`. +Similarly, `-f` may be supplied through `STM32LOADER_FAMILY`. Reference documents diff --git a/stm32loader/main.py b/stm32loader/main.py index 1ce65e8..bf8c72e 100644 --- a/stm32loader/main.py +++ b/stm32loader/main.py @@ -60,7 +60,7 @@ def __init__(self): "port": os.environ.get("STM32LOADER_SERIAL_PORT"), "baud": 115200, "parity": self.PARITY["even"], - "family": None, + "family": os.environ.get("STM32LOADER_FAMILY"), "address": 0x08000000, "erase": False, "unprotect": False, From dadab77ebb0e4143471e20f9a0083ce0eb11d456 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Fri, 19 Apr 2019 21:48:25 +0200 Subject: [PATCH 140/369] Add some more comments to _encode_address --- stm32loader/bootloader.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index 4f40996..cc3c344 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -596,6 +596,8 @@ def _wait_for_ack(self, info=""): @staticmethod def _encode_address(address): """Return the given address as big-endian bytes with a checksum.""" + # address in four bytes, big-endian address_bytes = bytearray(struct.pack(">I", address)) + # checksum as single byte checksum_byte = struct.pack("B", reduce(operator.xor, address_bytes)) return address_bytes + checksum_byte From 023ef024a2ce1069668c5de15e3f6004e5bd992b Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Fri, 19 Apr 2019 21:48:28 +0200 Subject: [PATCH 141/369] Bugfix: erase_memory erases in 1KiB pages, not 256 bytes This should be made configurable at some point, because this differs among STM32 parts. 256 Bytes remains the maximum data transfer length (chunk). --- CHANGELOG.md | 5 ++-- README.md | 2 ++ stm32loader/bootloader.py | 30 ++++++++++++----------- tests/integration/test_stm32bootloader.py | 15 ++++-------- 4 files changed, 26 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2dfe5b6..7f4d6a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,9 @@ v0.4.0 * #9: Support data writes smaller than 256 bytes. By NINI1988. * #10: Make stm32loader useful as a library. * #4: Bring back support for progress bar. -* #11: Support page erase in extended (two-byte addressing) erase mode -* #12: Allow to supply the serial port as an environment variable +* #12: Allow to supply the serial port as an environment variable. +* #11: Support paged erase in extended (two-byte addressing) erase mode. + Note: this is not yet tested on hardware. * Start using code linting and unit tests. * Start using Continuous Integration (Travis CI). diff --git a/README.md b/README.md index 38e3e2e..70d5935 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,7 @@ Not currently supported * Command-line argument for readout protection * Command-line argument for write protection/unprotection * STM8 devices (ST UM0560) +* Paged flash erase for devices with page size <> 1 KiB * Other bootloader protocols (e.g. I2C, HEX -> implemented in stm32flash) @@ -128,3 +129,4 @@ Future work * Try Azure pipelines for CI * Support PyPy, PyPy3 * Drop Python2 support; start using intenum for commands and replies +* Determine flash page size or make this configurable diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index cc3c344..11e59db 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -208,7 +208,8 @@ class Reply: "F7": 0x1FF0F442, } - PAGE_SIZE = 256 # bytes + DATA_TRANSFER_SIZE = 256 # bytes + FLASH_PAGE_SIZE = 1024 # bytes def __init__(self, connection, verbosity=5, show_progress=None): """ @@ -315,7 +316,7 @@ def get_flash_size(self, device_family): """Return the MCU's flash size in bytes.""" flash_size_address = self.FLASH_SIZE_ADDRESS[device_family] flash_size_bytes = self.read_memory(flash_size_address, 2) - flash_size = flash_size_bytes[0] + flash_size_bytes[1] * self.PAGE_SIZE + flash_size = flash_size_bytes[0] + flash_size_bytes[1] * self.DATA_TRANSFER_SIZE return flash_size def get_uid(self, device_id): @@ -337,7 +338,7 @@ def read_memory(self, address, length): Supports maximum 256 bytes. """ - if length > self.PAGE_SIZE: + if length > self.DATA_TRANSFER_SIZE: raise DataLengthError("Can not read more than 256 bytes at once.") self.command(self.Command.READ_MEMORY, "Read memory") self.write_and_ack("0x11 address failed", self._encode_address(address)) @@ -361,7 +362,7 @@ def write_memory(self, address, data): nr_of_bytes = len(data) if nr_of_bytes == 0: return - if nr_of_bytes > self.PAGE_SIZE: + if nr_of_bytes > self.DATA_TRANSFER_SIZE: raise DataLengthError("Can not write more than 256 bytes at once.") self.command(self.Command.WRITE_MEMORY, "Write memory") self.write_and_ack("0x31 address failed", self._encode_address(address)) @@ -456,8 +457,9 @@ def write_protect(self, pages): """Enable write protection on the given flash pages.""" self.command(self.Command.WRITE_PROTECT, "Write protect") nr_of_pages = (len(pages) - 1) & 0xFF - checksum = reduce(operator.xor, pages, nr_of_pages) - self.write_and_ack("0x63 write protect failed", nr_of_pages, pages, checksum) + page_numbers = bytearray(pages) + checksum = reduce(operator.xor, page_numbers, nr_of_pages) + self.write_and_ack("0x63 write protect failed", nr_of_pages, page_numbers, checksum) self.debug(10, " Write protect done") def write_unprotect(self): @@ -493,11 +495,11 @@ def read_memory_data(self, address, length): Length may be more than 256 bytes. """ data = bytearray() - page_count = int(math.ceil(length / float(self.PAGE_SIZE))) - self.debug(5, "Read %d pages at address 0x%X..." % (page_count, address)) - with self.show_progress("Reading", maximum=page_count) as progress_bar: + chunk_count = int(math.ceil(length / float(self.DATA_TRANSFER_SIZE))) + self.debug(5, "Read %d chunks at address 0x%X..." % (chunk_count, address)) + with self.show_progress("Reading", maximum=chunk_count) as progress_bar: while length: - read_length = min(length, self.PAGE_SIZE) + read_length = min(length, self.DATA_TRANSFER_SIZE) self.debug( 10, "Read %(len)d bytes at 0x%(address)X" @@ -516,13 +518,13 @@ def write_memory_data(self, address, data): Data length may be more than 256 bytes. """ length = len(data) - page_count = int(math.ceil(length / float(self.PAGE_SIZE))) + chunk_count = int(math.ceil(length / float(self.DATA_TRANSFER_SIZE))) offset = 0 - self.debug(5, "Write %d pages at address 0x%X..." % (page_count, address)) + self.debug(5, "Write %d chunks at address 0x%X..." % (chunk_count, address)) - with self.show_progress("Writing", maximum=page_count) as progress_bar: + with self.show_progress("Writing", maximum=chunk_count) as progress_bar: while length: - write_length = min(length, self.PAGE_SIZE) + write_length = min(length, self.DATA_TRANSFER_SIZE) self.debug( 10, "Write %(len)d bytes at 0x%(address)X" diff --git a/tests/integration/test_stm32bootloader.py b/tests/integration/test_stm32bootloader.py index c102c6b..6cb3b6b 100644 --- a/tests/integration/test_stm32bootloader.py +++ b/tests/integration/test_stm32bootloader.py @@ -40,17 +40,15 @@ def stm32(serial_connection): def test_erase_with_page_erases_only_that_page(stm32): stm32.reset_from_system_memory() base = 0x08000000 - before, middle, after = base + 0, base + 256, base + 512 + before, middle, after = base + 0, base + 1024, base + 2048 - # erase full device + # erase full device and check that it reset data bytes stm32.erase_memory() - - # check that erase was successful assert all(byte == 0xFF for byte in stm32.read_memory(before, 16)) assert all(byte == 0xFF for byte in stm32.read_memory(middle, 16)) assert all(byte == 0xFF for byte in stm32.read_memory(after, 16)) - # write zeros to three pages (and verify success) + # write zeros to three pages and verify data has changed stm32.write_memory(before, bytearray([0x00] * 16)) stm32.write_memory(middle, bytearray([0x00] * 16)) stm32.write_memory(after, bytearray([0x00] * 16)) @@ -58,11 +56,8 @@ def test_erase_with_page_erases_only_that_page(stm32): assert all(byte == 0x00 for byte in stm32.read_memory(middle, 16)) assert all(byte == 0x00 for byte in stm32.read_memory(after, 16)) - # erase only the middle page - stm32.erase_memory(pages=[0]) - - # check that middle page is erased, others are not + # erase only the middle page and check only that one's bytes are rest + stm32.erase_memory(pages=[1]) assert all(byte == 0x00 for byte in stm32.read_memory(before, 16)) assert all(byte == 0xFF for byte in stm32.read_memory(middle, 256)) assert all(byte == 0x00 for byte in stm32.read_memory(after, 16)) - From 9da03ca64adfb6d197e5ef07f79764d48b38933b Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Fri, 19 Apr 2019 21:55:27 +0200 Subject: [PATCH 142/369] Release: bump version number from v0.3.3-dev to v0.4.0-dev --- .bumpversion.cfg | 20 ++++++++++---------- setup.py | 2 +- stm32loader/__init__.py | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 78bb466..eae2f48 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,24 +1,24 @@ [bumpversion] -current_version = 0.3.3-dev +current_version = 0.4.0-dev commit = True tag = False message = Release: bump version number from v{current_version} to v{new_version} parse = (?P\d+)\.(?P\d+)\.(?P\d+)(-(?P.+))? -serialize = - {major}.{minor}.{patch}-{release} - {major}.{minor}.{patch} +serialize = + {major}.{minor}.{patch}-{release} + {major}.{minor}.{patch} [bumpversion:part:release] optional_value = release -values = - dev - release +values = + dev + release [bumpversion:file:stm32loader/__init__.py] parse = \((?P\d+),\s(?P\d+),\s(?P\d+)(\s*,\s*['\"](?P[^'\"]+)['\"])?\) -serialize = - ({major}, {minor}, {patch}, "{release}") - ({major}, {minor}, {patch}) +serialize = + ({major}, {minor}, {patch}, "{release}") + ({major}, {minor}, {patch}) search = __version_info__ = {current_version} replace = __version_info__ = {new_version} diff --git a/setup.py b/setup.py index db5f470..40e663d 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ # Package meta-data. NAME = 'stm32loader' -VERSION = "0.3.3-dev" +VERSION = "0.4.0-dev" DESCRIPTION = 'Flash firmware to STM32 microcontrollers using Python.' URL = 'https://github.com/florisla/stm32loader' EMAIL = 'florisla@gmail.com' diff --git a/stm32loader/__init__.py b/stm32loader/__init__.py index c696ecb..15ccb89 100644 --- a/stm32loader/__init__.py +++ b/stm32loader/__init__.py @@ -1,4 +1,4 @@ """Flash firmware to STM32 microcontrollers over a serial connection.""" -__version_info__ = (0, 3, 3, "dev") +__version_info__ = (0, 4, 0, "dev") __version__ = "-".join(str(part) for part in __version_info__).replace("-", ".", 2) From 72760e66add4e70c52e76cb96a4a97efcdfe0ed7 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Fri, 19 Apr 2019 21:55:33 +0200 Subject: [PATCH 143/369] Release: bump version number from v0.4.0-dev to v0.4.0 --- .bumpversion.cfg | 2 +- setup.py | 2 +- stm32loader/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index eae2f48..1579527 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.4.0-dev +current_version = 0.4.0 commit = True tag = False message = Release: bump version number from v{current_version} to v{new_version} diff --git a/setup.py b/setup.py index 40e663d..84bb9a7 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ # Package meta-data. NAME = 'stm32loader' -VERSION = "0.4.0-dev" +VERSION = "0.4.0" DESCRIPTION = 'Flash firmware to STM32 microcontrollers using Python.' URL = 'https://github.com/florisla/stm32loader' EMAIL = 'florisla@gmail.com' diff --git a/stm32loader/__init__.py b/stm32loader/__init__.py index 15ccb89..f739fca 100644 --- a/stm32loader/__init__.py +++ b/stm32loader/__init__.py @@ -1,4 +1,4 @@ """Flash firmware to STM32 microcontrollers over a serial connection.""" -__version_info__ = (0, 4, 0, "dev") +__version_info__ = (0, 4, 0) __version__ = "-".join(str(part) for part in __version_info__).replace("-", ".", 2) From c297af340b44cfc97234ed49085cd0cac92e712d Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Fri, 19 Apr 2019 22:03:19 +0200 Subject: [PATCH 144/369] Bugfix: VERSION was set to None --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 84bb9a7..d6a0d34 100644 --- a/setup.py +++ b/setup.py @@ -41,7 +41,6 @@ except FileNotFoundError: LONG_DESCRIPTION = DESCRIPTION -VERSION = None setup( name=NAME, version=VERSION, From b32c128e6760155e0658b78d74b78ced3e01fdea Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Fri, 19 Apr 2019 22:19:25 +0200 Subject: [PATCH 145/369] Release: bump version number from v0.4.0 to v0.4.1-dev --- .bumpversion.cfg | 2 +- setup.py | 2 +- stm32loader/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 1579527..206636c 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.4.0 +current_version = 0.4.1-dev commit = True tag = False message = Release: bump version number from v{current_version} to v{new_version} diff --git a/setup.py b/setup.py index d6a0d34..b639d0b 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ # Package meta-data. NAME = 'stm32loader' -VERSION = "0.4.0" +VERSION = "0.4.1-dev" DESCRIPTION = 'Flash firmware to STM32 microcontrollers using Python.' URL = 'https://github.com/florisla/stm32loader' EMAIL = 'florisla@gmail.com' diff --git a/stm32loader/__init__.py b/stm32loader/__init__.py index f739fca..74d2f4f 100644 --- a/stm32loader/__init__.py +++ b/stm32loader/__init__.py @@ -1,4 +1,4 @@ """Flash firmware to STM32 microcontrollers over a serial connection.""" -__version_info__ = (0, 4, 0) +__version_info__ = (0, 4, 1, "dev") __version__ = "-".join(str(part) for part in __version_info__).replace("-", ".", 2) From ca08caff3d40b4b5f74b851b4dd3f79426a5f57b Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 23 Apr 2019 16:05:28 +0200 Subject: [PATCH 146/369] Add some more badges to README --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 70d5935..7bb9931 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,10 @@ STM32Loader =========== +[![PyPI package](https://badge.fury.io/py/stm32loader.svg)](https://badge.fury.io/py/stm32loader) [![Build Status](https://travis-ci.org/florisla/stm32loader.svg?branch=master)](https://travis-ci.org/florisla/stm32loader) +[![License](https://img.shields.io/pypi/l/stm32loader.svg)](https://pypi.org/project/stm32loader/) +[![Downloads](https://pepy.tech/badge/stm32loader)](https://pepy.tech/project/stm32loader) Python script to upload or download firmware to / from ST Microelectronics STM32 microcontrollers over UART. From e546133c939fe0124ffe311536272547a1ab81e3 Mon Sep 17 00:00:00 2001 From: Omer Kilic Date: Thu, 2 May 2019 00:27:25 +0100 Subject: [PATCH 147/369] Add support for chip_id 0x444: STM32F03xx4/6 --- stm32loader/bootloader.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index 11e59db..2be1664 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -33,6 +33,7 @@ # see ST AN2606 Table 116 Bootloader device-dependent parameters # 16 to 32 KiB 0x412: "STM32F10x Low-density", + 0x444: "STM32F03xx4/6", # 64 to 128 KiB 0x410: "STM32F10x Medium-density", 0x420: "STM32F10x Medium-density value line", @@ -184,7 +185,10 @@ class Reply: ACK = 0x79 NACK = 0x1F + UID_NOT_SUPPORTED = -1 UID_ADDRESS = { + # No unique id for these parts + "F0": UID_NOT_SUPPORTED, # ST RM0008 section 30.1 Unique device ID register # F101, F102, F103, F105, F107 "F1": 0x1FFFF7E8, @@ -198,6 +202,9 @@ class Reply: UID_SWAP = [[1, 0], [3, 2], [7, 6, 5, 4], [11, 10, 9, 8]] FLASH_SIZE_ADDRESS = { + # ST RM0360 section 27.1 Memory size data register + # F030x4/x6/x8/xC, F070x6/xB + "F0": 0x1FFFF7CC, # ST RM0008 section 30.2 Memory size registers # F101, F102, F103, F105, F107 "F1": 0x1FFFF7E0, @@ -322,15 +329,21 @@ def get_flash_size(self, device_family): def get_uid(self, device_id): """Send the 'Get UID' command and return the device UID.""" uid_address = self.UID_ADDRESS[device_id] - uid = self.read_memory(uid_address, 12) - return uid + if uid_address == self.UID_NOT_SUPPORTED: + return self.UID_NOT_SUPPORTED + else: + uid = self.read_memory(uid_address, 12) + return uid @staticmethod def format_uid(uid): """Return a readable string from the given UID.""" - swapped_data = [[uid[b] for b in part] for part in Stm32Bootloader.UID_SWAP] - uid_string = "-".join("".join(format(b, "02X") for b in part) for part in swapped_data) - return uid_string + if uid == -1: + return "UID Not Supported" + else: + swapped_data = [[uid[b] for b in part] for part in Stm32Bootloader.UID_SWAP] + uid_string = "-".join("".join(format(b, "02X") for b in part) for part in swapped_data) + return uid_string def read_memory(self, address, length): """ From 3a96e4ec3e1a30c1a37f8661e6045e85e681a0f5 Mon Sep 17 00:00:00 2001 From: Omer Kilic Date: Thu, 2 May 2019 19:50:10 +0100 Subject: [PATCH 148/369] Tidy up get_uid() and format_uid() --- stm32loader/bootloader.py | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index 2be1664..7e17549 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -185,10 +185,9 @@ class Reply: ACK = 0x79 NACK = 0x1F - UID_NOT_SUPPORTED = -1 UID_ADDRESS = { # No unique id for these parts - "F0": UID_NOT_SUPPORTED, + "F0": None, # ST RM0008 section 30.1 Unique device ID register # F101, F102, F103, F105, F107 "F1": 0x1FFFF7E8, @@ -201,6 +200,11 @@ class Reply: UID_SWAP = [[1, 0], [3, 2], [7, 6, 5, 4], [11, 10, 9, 8]] + # Part does not support unique ID feature + UID_NOT_SUPPORTED = 0 + # stm32loader does not know the address for the unique ID + UID_ADDRESS_UNKNOWN = -1 + FLASH_SIZE_ADDRESS = { # ST RM0360 section 27.1 Memory size data register # F030x4/x6/x8/xC, F070x6/xB @@ -327,23 +331,27 @@ def get_flash_size(self, device_family): return flash_size def get_uid(self, device_id): - """Send the 'Get UID' command and return the device UID.""" - uid_address = self.UID_ADDRESS[device_id] - if uid_address == self.UID_NOT_SUPPORTED: - return self.UID_NOT_SUPPORTED + """Send the 'Get UID' command and return the device UID if the address for UID is known.""" + uid_address = self.UID_ADDRESS.get(device_id, self.UID_ADDRESS_UNKNOWN) + if uid_address is None: + uid = self.UID_NOT_SUPPORTED + elif uid_address == self.UID_ADDRESS_UNKNOWN: + uid = self.UID_ADDRESS_UNKNOWN else: uid = self.read_memory(uid_address, 12) - return uid + return uid - @staticmethod - def format_uid(uid): + @classmethod + def format_uid(self, uid): """Return a readable string from the given UID.""" - if uid == -1: - return "UID Not Supported" + if uid == self.UID_NOT_SUPPORTED: + uid_string = "UID not supported in this part" + elif uid == self.UID_ADDRESS_UNKNOWN: + uid_string = "UID address unknown" else: swapped_data = [[uid[b] for b in part] for part in Stm32Bootloader.UID_SWAP] uid_string = "-".join("".join(format(b, "02X") for b in part) for part in swapped_data) - return uid_string + return uid_string def read_memory(self, address, length): """ From f13a6e95663a2280aea00ff9feaa2607ef6f6e20 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Thu, 2 May 2019 23:53:18 +0200 Subject: [PATCH 149/369] Use early-return for the special UID cases This avoids indenting the 'normal flow' code into an else block. --- stm32loader/bootloader.py | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index 7e17549..84f8f0a 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -331,26 +331,36 @@ def get_flash_size(self, device_family): return flash_size def get_uid(self, device_id): - """Send the 'Get UID' command and return the device UID if the address for UID is known.""" + """ + Send the 'Get UID' command and return the device UID. + + Return UID_NOT_SUPPORTED if the device does not have + a UID. + Return UIT_ADDRESS_UNKNOWN if the address of the device's + UID is not known. + + :return byterary: UID bytes of the device, or 0 or -1 when + not available. + """ uid_address = self.UID_ADDRESS.get(device_id, self.UID_ADDRESS_UNKNOWN) if uid_address is None: - uid = self.UID_NOT_SUPPORTED - elif uid_address == self.UID_ADDRESS_UNKNOWN: - uid = self.UID_ADDRESS_UNKNOWN - else: - uid = self.read_memory(uid_address, 12) + return self.UID_NOT_SUPPORTED + if uid_address == self.UID_ADDRESS_UNKNOWN: + return self.UID_ADDRESS_UNKNOWN + + uid = self.read_memory(uid_address, 12) return uid @classmethod def format_uid(self, uid): """Return a readable string from the given UID.""" if uid == self.UID_NOT_SUPPORTED: - uid_string = "UID not supported in this part" - elif uid == self.UID_ADDRESS_UNKNOWN: - uid_string = "UID address unknown" - else: - swapped_data = [[uid[b] for b in part] for part in Stm32Bootloader.UID_SWAP] - uid_string = "-".join("".join(format(b, "02X") for b in part) for part in swapped_data) + return "UID not supported in this part" + if uid == self.UID_ADDRESS_UNKNOWN: + return "UID address unknown" + + swapped_data = [[uid[b] for b in part] for part in Stm32Bootloader.UID_SWAP] + uid_string = "-".join("".join(format(b, "02X") for b in part) for part in swapped_data) return uid_string def read_memory(self, address, length): From 524a198c14d9fc4f81ff55c9477d1c9d9c5103d6 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Thu, 2 May 2019 23:56:03 +0200 Subject: [PATCH 150/369] Rename 'self' to 'cls' for the classmethod --- stm32loader/bootloader.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index 84f8f0a..8ec61f3 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -352,11 +352,11 @@ def get_uid(self, device_id): return uid @classmethod - def format_uid(self, uid): + def format_uid(cls, uid): """Return a readable string from the given UID.""" - if uid == self.UID_NOT_SUPPORTED: + if uid == cls.UID_NOT_SUPPORTED: return "UID not supported in this part" - if uid == self.UID_ADDRESS_UNKNOWN: + if uid == cls.UID_ADDRESS_UNKNOWN: return "UID address unknown" swapped_data = [[uid[b] for b in part] for part in Stm32Bootloader.UID_SWAP] From 2b5567afc64a8e099d04138e7b906626449dfe00 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Thu, 2 May 2019 23:56:22 +0200 Subject: [PATCH 151/369] Add param docstring for get_uid 'device_id' --- stm32loader/bootloader.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index 8ec61f3..e7dbf69 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -339,6 +339,8 @@ def get_uid(self, device_id): Return UIT_ADDRESS_UNKNOWN if the address of the device's UID is not known. + :param str device_id: Device family name such as "F1". + See UID_ADDRESS. :return byterary: UID bytes of the device, or 0 or -1 when not available. """ From c6ea0aab99eb3f48115a670d77d4992be317d92a Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Fri, 3 May 2019 00:51:45 +0200 Subject: [PATCH 152/369] Add some tests on get_uid and format_uid --- tests/unit/test_bootloader.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/unit/test_bootloader.py b/tests/unit/test_bootloader.py index bc7d2f4..04c53fd 100644 --- a/tests/unit/test_bootloader.py +++ b/tests/unit/test_bootloader.py @@ -185,3 +185,35 @@ def test_verify_data_with_different_byte_count_raises_verify_error_complaining_a def test_verify_data_with_non_identical_data_raises_verify_error_complaining_about_mismatched_byte(): with pytest.raises(Stm32.DataMismatchError, match=r"Verification data does not match read data.*mismatch.*0x1.*0x6.*0x7"): Stm32Bootloader.verify_data(b'\x05\x06', b'\x05\x07') + + +@pytest.mark.parametrize( + "family", ["F1", "F4", "F7"], +) +def test_get_uid_for_known_family_reads_at_correct_address(bootloader, family): + bootloader.read_memory = MagicMock() + bootloader.get_uid("F1") + uid_address = bootloader.UID_ADDRESS[family] + assert bootloader.read_memory.called_once_with(uid_address) + + +def test_get_uid_for_family_without_uid_returns_uid_not_supported(bootloader): + assert bootloader.UID_NOT_SUPPORTED == bootloader.get_uid("F0") + + +def test_get_uid_for_unknown_family_returns_uid_address_unknown(bootloader): + assert bootloader.UID_ADDRESS_UNKNOWN == bootloader.get_uid("X") + + +@pytest.mark.parametrize( + "uid_string", + [ + (0, "UID not supported in this part"), + (-1, "UID address unknown"), + (b'\x12\x34\x56\x78\x9a\xbc\xde\x01\x12\x34\x56\x78', "3412-7856-01DEBC9A-78563412"), + ], +) +def test_format_uid_returns_correct_string(bootloader, uid_string): + uid, expected_description = uid_string + description = bootloader.format_uid(uid) + assert description == expected_description From 0ad7df1f4d27fb19920cab61fb3c07a5a32d5e33 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Fri, 3 May 2019 00:52:19 +0200 Subject: [PATCH 153/369] Docs: acknowledge Omer Kilic --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7bb9931..49abfe1 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ Acknowledgement Original Version by Ivan A-R (tuxotronic.org). Contributions by Domen Puncer, James Snyder, Floris Lambrechts, -Atokulus, sam-bristow, NINI1988. +Atokulus, sam-bristow, NINI1988, Omer Kilic. Inspiration for features from: From 09e8fd16ad5f36f68eba0e46080e9e5f8dd3e3a9 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Fri, 19 Apr 2019 22:20:10 +0200 Subject: [PATCH 154/369] Drop support for Python 3.2 and 3.3 in Trove classifiers Version 0.4.0 is already uploaded, so this is for next time. --- setup.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/setup.py b/setup.py index b639d0b..dbee5da 100644 --- a/setup.py +++ b/setup.py @@ -66,8 +66,6 @@ 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.2', - 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', From 0c0f6b6c75602e37d6ca18f49f0e2e7e1b96e13d Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Fri, 3 May 2019 00:54:55 +0200 Subject: [PATCH 155/369] Release: bump version number from v0.4.1-dev to v0.5.0-dev --- .bumpversion.cfg | 2 +- setup.py | 2 +- stm32loader/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 206636c..334322e 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.4.1-dev +current_version = 0.5.0-dev commit = True tag = False message = Release: bump version number from v{current_version} to v{new_version} diff --git a/setup.py b/setup.py index dbee5da..1452d20 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ # Package meta-data. NAME = 'stm32loader' -VERSION = "0.4.1-dev" +VERSION = "0.5.0-dev" DESCRIPTION = 'Flash firmware to STM32 microcontrollers using Python.' URL = 'https://github.com/florisla/stm32loader' EMAIL = 'florisla@gmail.com' diff --git a/stm32loader/__init__.py b/stm32loader/__init__.py index 74d2f4f..c3a816f 100644 --- a/stm32loader/__init__.py +++ b/stm32loader/__init__.py @@ -1,4 +1,4 @@ """Flash firmware to STM32 microcontrollers over a serial connection.""" -__version_info__ = (0, 4, 1, "dev") +__version_info__ = (0, 5, 0, "dev") __version__ = "-".join(str(part) for part in __version_info__).replace("-", ".", 2) From 053cc53372332778067b028a17ac5588bd520359 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Fri, 3 May 2019 01:02:46 +0200 Subject: [PATCH 156/369] Fix for Python 2 It's always Python 2 --- tests/unit/test_bootloader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_bootloader.py b/tests/unit/test_bootloader.py index 04c53fd..d5b49e0 100644 --- a/tests/unit/test_bootloader.py +++ b/tests/unit/test_bootloader.py @@ -210,7 +210,7 @@ def test_get_uid_for_unknown_family_returns_uid_address_unknown(bootloader): [ (0, "UID not supported in this part"), (-1, "UID address unknown"), - (b'\x12\x34\x56\x78\x9a\xbc\xde\x01\x12\x34\x56\x78', "3412-7856-01DEBC9A-78563412"), + (bytearray(b"\x12\x34\x56\x78\x9a\xbc\xde\x01\x12\x34\x56\x78"), "3412-7856-01DEBC9A-78563412"), ], ) def test_format_uid_returns_correct_string(bootloader, uid_string): From ee53162c9ef34e2ba7b285f373817a9bf623ab78 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Fri, 3 May 2019 01:06:04 +0200 Subject: [PATCH 157/369] Release: bump version number from v0.5.0-dev to v0.5.0 --- .bumpversion.cfg | 2 +- setup.py | 2 +- stm32loader/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 334322e..746c1ee 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.5.0-dev +current_version = 0.5.0 commit = True tag = False message = Release: bump version number from v{current_version} to v{new_version} diff --git a/setup.py b/setup.py index 1452d20..fa26a4b 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ # Package meta-data. NAME = 'stm32loader' -VERSION = "0.5.0-dev" +VERSION = "0.5.0" DESCRIPTION = 'Flash firmware to STM32 microcontrollers using Python.' URL = 'https://github.com/florisla/stm32loader' EMAIL = 'florisla@gmail.com' diff --git a/stm32loader/__init__.py b/stm32loader/__init__.py index c3a816f..934f19d 100644 --- a/stm32loader/__init__.py +++ b/stm32loader/__init__.py @@ -1,4 +1,4 @@ """Flash firmware to STM32 microcontrollers over a serial connection.""" -__version_info__ = (0, 5, 0, "dev") +__version_info__ = (0, 5, 0) __version__ = "-".join(str(part) for part in __version_info__).replace("-", ".", 2) From 0bf8d0dbc499b14ef7d37834420bb081bec08fee Mon Sep 17 00:00:00 2001 From: byq77 Date: Thu, 4 Jul 2019 21:37:38 +0200 Subject: [PATCH 158/369] mass memory erase fixed --- stm32loader/.gitignore | 1 + stm32loader/bootloader.py | 20 +++++++++++++------- stm32loader/main.py | 19 +++++++++++-------- stm32loader/uart.py | 18 +++++++++++++++++- 4 files changed, 42 insertions(+), 16 deletions(-) create mode 100644 stm32loader/.gitignore diff --git a/stm32loader/.gitignore b/stm32loader/.gitignore new file mode 100644 index 0000000..6937180 --- /dev/null +++ b/stm32loader/.gitignore @@ -0,0 +1 @@ +.vscode/settings.json \ No newline at end of file diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index e7dbf69..60182f7 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -45,8 +45,10 @@ # flash size to be looked up 0x416: "STM32L1xxx6(8/B) Medium-density ultralow power line", 0x411: "STM32F2xxx", - 0x413: "STM32F40xxx/41xxx", - 0x419: "STM3242xxx/43xxx", + 0x433: "STM32F4xxDE", + # RM0090 in ( 38.6.1 MCU device ID code ) + 0x413: "STM32F405xx/07xx and STM32F415xx/17xx", + 0x419: "STM32F42xxx and STM32F43xxx", 0x449: "STM32F74xxx/75xxx", 0x451: "STM32F76xxx/77xxx", # see ST AN4872 @@ -193,7 +195,7 @@ class Reply: "F1": 0x1FFFF7E8, # ST RM0090 section 39.1 Unique device ID register # F405/415, F407/417, F427/437, F429/439 - "F4": 0x1FFFF7A10, + "F4": 0x1FFF7A10, # ST RM0385 section 41.2 Unique device ID register "F7": 0x1FF0F420, } @@ -308,10 +310,14 @@ def get_version(self): Read protection status readout is not yet implemented. """ self.command(self.Command.GET_VERSION, "Get version") - version = bytearray(self.connection.read())[0] - self.connection.read(2) + data = bytearray(self.connection.read(3)) + version = data[0] + option_byte1 = data[1] + option_byte2 = data[2] self._wait_for_ack("0x01 end") self.debug(10, " Bootloader version: " + hex(version)) + self.debug(10, " Option byte 1: " + hex(option_byte1)) + self.debug(10, " Option byte 2: " + hex(option_byte2)) return version def get_id(self): @@ -327,7 +333,7 @@ def get_flash_size(self, device_family): """Return the MCU's flash size in bytes.""" flash_size_address = self.FLASH_SIZE_ADDRESS[device_family] flash_size_bytes = self.read_memory(flash_size_address, 2) - flash_size = flash_size_bytes[0] + flash_size_bytes[1] * self.DATA_TRANSFER_SIZE + flash_size = flash_size_bytes[0] + (flash_size_bytes[1]<<8) return flash_size def get_uid(self, device_id): @@ -349,7 +355,7 @@ def get_uid(self, device_id): return self.UID_NOT_SUPPORTED if uid_address == self.UID_ADDRESS_UNKNOWN: return self.UID_ADDRESS_UNKNOWN - + uid = self.read_memory(uid_address, 12) return uid diff --git a/stm32loader/main.py b/stm32loader/main.py index bf8c72e..da4566d 100644 --- a/stm32loader/main.py +++ b/stm32loader/main.py @@ -243,21 +243,24 @@ def print_usage(): def read_device_details(self): """Show MCU details (bootloader version, chip ID, UID, flash size).""" boot_version = self.stm32.get() - self.debug(0, "Bootloader version %X" % boot_version) + self.debug(0, "Bootloader version: 0x%X" % boot_version) device_id = self.stm32.get_id() self.debug( - 0, "Chip id: 0x%x (%s)" % (device_id, bootloader.CHIP_IDS.get(device_id, "Unknown")) + 0, "Chip id: 0x%X (%s)" % (device_id, bootloader.CHIP_IDS.get(device_id, "Unknown")) ) family = self.configuration["family"] if not family: self.debug(0, "Supply -f [family] to see flash size and device UID, e.g: -f F1") else: - device_uid = self.stm32.get_uid(family) - device_uid_string = self.stm32.format_uid(device_uid) - self.debug(0, "Device UID: %s" % device_uid_string) - - flash_size = self.stm32.get_flash_size(family) - self.debug(0, "Flash size: %d KiB" % flash_size) + try: + device_uid = self.stm32.get_uid(family) + flash_size = self.stm32.get_flash_size(family) + except bootloader.CommandError as e: + self.debug(0,"Something was wrong with reading chip family data: " + e.message) + else: + device_uid_string = self.stm32.format_uid(device_uid) + self.debug(0, "Device UID: %s" % device_uid_string) + self.debug(0, "Flash size: %d KiB" % flash_size) def _parse_option_flags(self, options): # pylint: disable=eval-used diff --git a/stm32loader/uart.py b/stm32loader/uart.py index 5097639..83b8197 100644 --- a/stm32loader/uart.py +++ b/stm32loader/uart.py @@ -49,6 +49,22 @@ def __init__(self, serial_port, baud_rate=115200, parity="E"): # call connect() to establish connection self.serial_connection = None + self._timeout = None + + # assigned using setter methods + self.timeout = 5 + + @property + def timeout(self): + """Get timeout.""" + return self._timeout + + @timeout.setter + def timeout(self, timeout): + """Set timeout.""" + self._timeout = timeout + self.serial_connection.timeout = timeout + def connect(self): """Connect to the RS-232 serial port.""" self.serial_connection = serial.Serial( @@ -63,7 +79,7 @@ def connect(self): # don't enable RTS/CTS flow control rtscts=0, # set a timeout value, None for waiting forever - timeout=5, + timeout=self._timeout, ) def write(self, *args, **kwargs): From 871a55b4f07b26ba7c1132a7895ff537ac0e6ac8 Mon Sep 17 00:00:00 2001 From: byq77 Date: Sat, 13 Jul 2019 23:46:59 +0200 Subject: [PATCH 159/369] setter fix, name fix for boot0 active state --- .vscode/settings.json | 3 +++ README.md | 2 +- stm32loader/main.py | 8 ++++---- stm32loader/uart.py | 15 +++++++-------- 4 files changed, 15 insertions(+), 13 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..d2a6c12 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.pythonPath": "/usr/bin/python" +} \ No newline at end of file diff --git a/README.md b/README.md index 49abfe1..9fdb51a 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ Usage -s Swap RTS and DTR: use RTS for reset and DTR for boot0 -R Make reset active high - -B Make boot0 active high + -B Make boot0 active low -u Readout unprotect -n No progress: don't show progress bar -P parity Parity: "even" for STM32 (default), "none" for BlueNRG diff --git a/stm32loader/main.py b/stm32loader/main.py index da4566d..e02adec 100644 --- a/stm32loader/main.py +++ b/stm32loader/main.py @@ -48,7 +48,7 @@ class Stm32Loader: "-s": "swap_rts_dtr", "-n": "hide_progress_bar", "-R": "reset_active_high", - "-B": "boot0_active_high", + "-B": "boot0_active_low", } INTEGER_OPTIONS = {"-b": "baud", "-a": "address", "-g": "go_address", "-l": "length"} @@ -70,7 +70,7 @@ def __init__(self): "go_address": -1, "swap_rts_dtr": False, "reset_active_high": False, - "boot0_active_high": False, + "boot0_active_low": False, "hide_progress_bar": False, "data_file": None, } @@ -134,7 +134,7 @@ def connect(self): serial_connection.swap_rts_dtr = self.configuration["swap_rts_dtr"] serial_connection.reset_active_high = self.configuration["reset_active_high"] - serial_connection.boot0_active_high = self.configuration["boot0_active_high"] + serial_connection.boot0_active_low = self.configuration["boot0_active_low"] show_progress = self._get_progress_bar(self.configuration["hide_progress_bar"]) @@ -228,7 +228,7 @@ def print_usage(): -s Swap RTS and DTR: use RTS for reset and DTR for boot0 -R Make reset active high - -B Make boot0 active high + -B Make boot0 active low -u Readout unprotect -n No progress: don't show progress bar -P parity Parity: "even" for STM32 (default), "none" for BlueNRG diff --git a/stm32loader/uart.py b/stm32loader/uart.py index 83b8197..81190b5 100644 --- a/stm32loader/uart.py +++ b/stm32loader/uart.py @@ -27,7 +27,9 @@ import serial -class SerialConnection: +# fixes the problem with setters methods +# https://stackoverflow.com/questions/598077/why-does-foo-setter-in-python-not-work-for-me +class SerialConnection(object) """Wrap a serial.Serial connection and toggle reset and boot0.""" # pylint: disable=too-many-instance-attributes @@ -44,15 +46,12 @@ def __init__(self, serial_port, baud_rate=115200, parity="E"): self.swap_rts_dtr = False self.reset_active_high = False - self.boot0_active_high = False + self.boot0_active_low = False # call connect() to establish connection self.serial_connection = None - self._timeout = None - - # assigned using setter methods - self.timeout = 5 + self._timeout = 5 @property def timeout(self): @@ -109,8 +108,8 @@ def enable_boot0(self, enable=True): """Enable or disable the boot0 IO line.""" level = int(enable) - # by default, this is active low - if not self.boot0_active_high: + # by default, this is active high + if not self.boot0_active_low: level = 1 - level if self.swap_rts_dtr: From 22e5a72b5fc2cd93ac566850803f6b3d89d61107 Mon Sep 17 00:00:00 2001 From: byq77 Date: Sun, 14 Jul 2019 00:31:54 +0200 Subject: [PATCH 160/369] fix --- .vscode/settings.json | 3 --- stm32loader/uart.py | 3 +-- 2 files changed, 1 insertion(+), 5 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index d2a6c12..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "python.pythonPath": "/usr/bin/python" -} \ No newline at end of file diff --git a/stm32loader/uart.py b/stm32loader/uart.py index 81190b5..176e72d 100644 --- a/stm32loader/uart.py +++ b/stm32loader/uart.py @@ -26,10 +26,9 @@ # not naming this file itself 'serial', becase that name-clashes in Python 2 import serial - # fixes the problem with setters methods # https://stackoverflow.com/questions/598077/why-does-foo-setter-in-python-not-work-for-me -class SerialConnection(object) +class SerialConnection(object): """Wrap a serial.Serial connection and toggle reset and boot0.""" # pylint: disable=too-many-instance-attributes From 27a4297801b294a7b835d6bf41de34b7563f2ad1 Mon Sep 17 00:00:00 2001 From: byq77 Date: Sun, 14 Jul 2019 19:05:53 +0200 Subject: [PATCH 161/369] -f option fix for F4 devices --- stm32loader/bootloader.py | 12 +++++++++++- stm32loader/main.py | 27 +++++++++++++++++++-------- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index 60182f7..400062b 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -45,7 +45,7 @@ # flash size to be looked up 0x416: "STM32L1xxx6(8/B) Medium-density ultralow power line", 0x411: "STM32F2xxx", - 0x433: "STM32F4xxDE", + 0x433: "STM32F4xxD/E", # RM0090 in ( 38.6.1 MCU device ID code ) 0x413: "STM32F405xx/07xx and STM32F415xx/17xx", 0x419: "STM32F42xxx and STM32F43xxx", @@ -336,6 +336,16 @@ def get_flash_size(self, device_family): flash_size = flash_size_bytes[0] + (flash_size_bytes[1]<<8) return flash_size + def get_flash_size_and_uid_f4(self): + """ Return device_uid and flash_size for F4 family.""" + data_start_addr = 0x1FFF7A00 + flash_size_lsb_addr = 0x22 + uid_lsb_addr = 0x10 + data = self.read_memory(data_start_addr, self.DATA_TRANSFER_SIZE) + device_uid = data[uid_lsb_addr:uid_lsb_addr+12] + flash_size = data[flash_size_lsb_addr] + data[flash_size_lsb_addr+1]<<8 + return device_uid, flash_size + def get_uid(self, device_id): """ Send the 'Get UID' command and return the device UID. diff --git a/stm32loader/main.py b/stm32loader/main.py index e02adec..311104d 100644 --- a/stm32loader/main.py +++ b/stm32loader/main.py @@ -252,15 +252,26 @@ def read_device_details(self): if not family: self.debug(0, "Supply -f [family] to see flash size and device UID, e.g: -f F1") else: - try: - device_uid = self.stm32.get_uid(family) - flash_size = self.stm32.get_flash_size(family) - except bootloader.CommandError as e: - self.debug(0,"Something was wrong with reading chip family data: " + e.message) + # special fix for F4 devices + if family == "F4": + try: + device_uid, flash_size = self.stm32.get_flash_size_and_uid_f4() + except bootloader.CommandError as e: + self.debug(0,"Something was wrong with reading chip family data: " + e.message) + else: + device_uid_string = self.stm32.format_uid(device_uid) + self.debug(0, "Device UID: %s" % device_uid_string) + self.debug(0, "Flash size: %d KiB" % flash_size) else: - device_uid_string = self.stm32.format_uid(device_uid) - self.debug(0, "Device UID: %s" % device_uid_string) - self.debug(0, "Flash size: %d KiB" % flash_size) + try: + flash_size = self.stm32.get_flash_size(family) + device_uid = self.stm32.get_uid(family) + except bootloader.CommandError as e: + self.debug(0,"Something was wrong with reading chip family data: " + e.message) + else: + device_uid_string = self.stm32.format_uid(device_uid) + self.debug(0, "Device UID: %s" % device_uid_string) + self.debug(0, "Flash size: %d KiB" % flash_size) def _parse_option_flags(self, options): # pylint: disable=eval-used From c13b027c8ca86b0dddb9b889d41484a9f1229ad3 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 12 Aug 2019 16:56:06 +0200 Subject: [PATCH 162/369] docs: Move comment on Python2 setters to within the class --- stm32loader/uart.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/stm32loader/uart.py b/stm32loader/uart.py index 176e72d..62ce688 100644 --- a/stm32loader/uart.py +++ b/stm32loader/uart.py @@ -26,11 +26,13 @@ # not naming this file itself 'serial', becase that name-clashes in Python 2 import serial -# fixes the problem with setters methods -# https://stackoverflow.com/questions/598077/why-does-foo-setter-in-python-not-work-for-me + class SerialConnection(object): """Wrap a serial.Serial connection and toggle reset and boot0.""" + # Note: inheriting from object is required for Python2 setters + # https://stackoverflow.com/questions/598077/why-does-foo-setter-in-python-not-work-for-me + # pylint: disable=too-many-instance-attributes def __init__(self, serial_port, baud_rate=115200, parity="E"): From 637d98c23e0f2c8db0dd3c2a923135053ee8286f Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 12 Aug 2019 16:57:20 +0200 Subject: [PATCH 163/369] refactor: Split up read_device_details into _id and _uid --- stm32loader/main.py | 45 ++++++++++++++++++++++----------------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/stm32loader/main.py b/stm32loader/main.py index 311104d..27c89b9 100644 --- a/stm32loader/main.py +++ b/stm32loader/main.py @@ -240,38 +240,36 @@ def print_usage(): help_text = help_text % (current_script, current_script, current_script) print(help_text) - def read_device_details(self): - """Show MCU details (bootloader version, chip ID, UID, flash size).""" + def read_device_id(self): + """Show chip ID and bootloader version.""" boot_version = self.stm32.get() self.debug(0, "Bootloader version: 0x%X" % boot_version) device_id = self.stm32.get_id() self.debug( 0, "Chip id: 0x%X (%s)" % (device_id, bootloader.CHIP_IDS.get(device_id, "Unknown")) ) + + def read_device_uid(self): + """Show chip UID and flash size.""" family = self.configuration["family"] if not family: self.debug(0, "Supply -f [family] to see flash size and device UID, e.g: -f F1") - else: - # special fix for F4 devices - if family == "F4": - try: - device_uid, flash_size = self.stm32.get_flash_size_and_uid_f4() - except bootloader.CommandError as e: - self.debug(0,"Something was wrong with reading chip family data: " + e.message) - else: - device_uid_string = self.stm32.format_uid(device_uid) - self.debug(0, "Device UID: %s" % device_uid_string) - self.debug(0, "Flash size: %d KiB" % flash_size) + return + + try: + if family != "F4": + flash_size = self.stm32.get_flash_size(family) + device_uid = self.stm32.get_uid(family) else: - try: - flash_size = self.stm32.get_flash_size(family) - device_uid = self.stm32.get_uid(family) - except bootloader.CommandError as e: - self.debug(0,"Something was wrong with reading chip family data: " + e.message) - else: - device_uid_string = self.stm32.format_uid(device_uid) - self.debug(0, "Device UID: %s" % device_uid_string) - self.debug(0, "Flash size: %d KiB" % flash_size) + # special fix for F4 devices + flash_size, device_uid = self.stm32.get_flash_size_and_uid_f4() + except bootloader.CommandError as e: + self.debug(0, "Something was wrong with reading chip family data: " + e.message) + return + + device_uid_string = self.stm32.format_uid(device_uid) + self.debug(0, "Device UID: %s" % device_uid_string) + self.debug(0, "Flash size: %d KiB" % flash_size) def _parse_option_flags(self, options): # pylint: disable=eval-used @@ -328,7 +326,8 @@ def main(*args, **kwargs): loader.parse_arguments(args) loader.connect() try: - loader.read_device_details() + loader.read_device_id() + loader.read_device_uid() loader.perform_commands() finally: loader.reset() From 73c44c57e2926dbb00cd9886f4080bdb1d0afbe7 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 12 Aug 2019 16:59:06 +0200 Subject: [PATCH 164/369] docs: add more context to docstring about F4 oddity Also, make the order of the return values match the function name. --- stm32loader/bootloader.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index 400062b..b7ec30b 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -337,14 +337,21 @@ def get_flash_size(self, device_family): return flash_size def get_flash_size_and_uid_f4(self): - """ Return device_uid and flash_size for F4 family.""" + """ + Return device_uid and flash_size for F4 family. + + For some reason, F4 (at least, NUCLEO F401RE) can't read the 12 or 2 + bytes for UID and flash size directly. + Reading a whole chunk of 256 bytes at 0x1FFFA700 does work and + requires some data extraction. + """ data_start_addr = 0x1FFF7A00 flash_size_lsb_addr = 0x22 uid_lsb_addr = 0x10 data = self.read_memory(data_start_addr, self.DATA_TRANSFER_SIZE) device_uid = data[uid_lsb_addr:uid_lsb_addr+12] flash_size = data[flash_size_lsb_addr] + data[flash_size_lsb_addr+1]<<8 - return device_uid, flash_size + return flash_size, device_uid def get_uid(self, device_id): """ From c67fa382083ba5585452341a1c1b6d653e640d43 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 12 Aug 2019 16:59:34 +0200 Subject: [PATCH 165/369] docs: Mention byq77 as a contributor --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9fdb51a..38e9a77 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ Acknowledgement Original Version by Ivan A-R (tuxotronic.org). Contributions by Domen Puncer, James Snyder, Floris Lambrechts, -Atokulus, sam-bristow, NINI1988, Omer Kilic. +Atokulus, sam-bristow, NINI1988, Omer Kilic, byq77. Inspiration for features from: From 9c73e8b01af5959ed9bd83d38dd709bb9fe4574c Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 12 Aug 2019 17:01:42 +0200 Subject: [PATCH 166/369] style(docs): Line length --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 38e9a77..d9423e4 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,8 @@ Example stm32loader.py -p /dev/tty.usbserial-ftCYPMYJ -e -w -v somefile.bin ``` -This will pre-erase flash, write `somefile.bin` to the flash on the device, and then perform a verification after writing is finished. +This will pre-erase flash, write `somefile.bin` to the flash on the device, and then +perform a verification after writing is finished. You can skip the `-p` option by configuring environment variable `STM32LOADER_SERIAL_PORT`. From 68db5eedd8c3967f958dc4ff7772ea49c9496712 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 13 May 2019 22:00:02 +0200 Subject: [PATCH 167/369] refactor: Simplify detection of reset/boot0 capability --- stm32loader/bootloader.py | 6 ++---- stm32loader/uart.py | 14 +++++--------- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index b7ec30b..da86246 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -243,8 +243,6 @@ def __init__(self, connection, verbosity=5, show_progress=None): Set to None to disable progress bar output. """ self.connection = connection - self._toggle_reset = getattr(connection, "can_toggle_reset", False) - self._toggle_boot0 = getattr(connection, "can_toggle_boot0", False) self.verbosity = verbosity self.show_progress = show_progress or ShowProgress(None) self.extended_erase = False @@ -624,7 +622,7 @@ def verify_data(read_data, reference_data): def _reset(self): """Enable or disable the reset IO line (if possible).""" - if not self._toggle_reset: + if not hasattr(self.connection, "enable_reset"): return self.connection.enable_reset(True) time.sleep(0.1) @@ -633,7 +631,7 @@ def _reset(self): def _enable_boot0(self, enable=True): """Enable or disable the boot0 IO line (if possible).""" - if not self._toggle_boot0: + if not hasattr(self.connection, "enable_boot0"): return self.connection.enable_boot0(enable) diff --git a/stm32loader/uart.py b/stm32loader/uart.py index 62ce688..f9dbc91 100644 --- a/stm32loader/uart.py +++ b/stm32loader/uart.py @@ -41,15 +41,11 @@ def __init__(self, serial_port, baud_rate=115200, parity="E"): self.baud_rate = baud_rate self.parity = parity - # advertise reset / boot0 toggle capability - self.can_toggle_reset = True - self.can_toggle_boot0 = True - self.swap_rts_dtr = False self.reset_active_high = False self.boot0_active_low = False - # call connect() to establish connection + # don't connect yet; caller should use connect() separately self.serial_connection = None self._timeout = 5 @@ -66,7 +62,7 @@ def timeout(self, timeout): self.serial_connection.timeout = timeout def connect(self): - """Connect to the RS-232 serial port.""" + """Connect to the UART serial port.""" self.serial_connection = serial.Serial( port=self.serial_port, baudrate=self.baud_rate, @@ -93,9 +89,9 @@ def read(self, *args, **kwargs): def enable_reset(self, enable=True): """Enable or disable the reset IO line.""" # reset on the STM32 is active low (0 Volt puts the MCU in reset) - # but the RS-232 DTR signal is active low by itself, so it - # inverts this (writing a logical 1 outputs a low voltage, i.e. - # enables reset) + # but the RS-232 modem control DTR and RTS signals are active low + # themselves, so these get inverted -- writing a logical 1 outputs + # a low voltage, i.e. enables reset) level = int(enable) if self.reset_active_high: level = 1 - level From a6b166c3b12af987eccb3f9e038b92af376496af Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 13 May 2019 22:00:14 +0200 Subject: [PATCH 168/369] fix(docs): Typo --- stm32loader/uart.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stm32loader/uart.py b/stm32loader/uart.py index f9dbc91..7824f5f 100644 --- a/stm32loader/uart.py +++ b/stm32loader/uart.py @@ -23,7 +23,7 @@ Offer support for toggling RESET and BOOT0. """ -# not naming this file itself 'serial', becase that name-clashes in Python 2 +# not naming this file itself 'serial', because that name-clashes in Python 2 import serial From feb0b0db0a1ebd9e226c320b5d2e4309a848f172 Mon Sep 17 00:00:00 2001 From: rdaforno Date: Mon, 19 Aug 2019 16:55:38 +0200 Subject: [PATCH 169/369] support for STM32L4 added --- stm32loader/bootloader.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index da86246..8bdcf87 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -49,6 +49,7 @@ # RM0090 in ( 38.6.1 MCU device ID code ) 0x413: "STM32F405xx/07xx and STM32F415xx/17xx", 0x419: "STM32F42xxx and STM32F43xxx", + 0x435: "STM32L4xx", 0x449: "STM32F74xxx/75xxx", 0x451: "STM32F76xxx/77xxx", # see ST AN4872 @@ -198,6 +199,8 @@ class Reply: "F4": 0x1FFF7A10, # ST RM0385 section 41.2 Unique device ID register "F7": 0x1FF0F420, + # ST RM0394 + 'L4': 0x1FFF7590, } UID_SWAP = [[1, 0], [3, 2], [7, 6, 5, 4], [11, 10, 9, 8]] @@ -219,6 +222,8 @@ class Reply: "F4": 0x1FFF7A22, # ST RM0385 section 41.2 Flash size "F7": 0x1FF0F442, + # ST RM0394 + 'L4': 0x1FFF75E0, } DATA_TRANSFER_SIZE = 256 # bytes From ce08cc1b2ad92dd2d0ca57136977fe79f280c9d2 Mon Sep 17 00:00:00 2001 From: Mikolaj Stawiski <26871388+stawiski@users.noreply.github.com> Date: Thu, 26 Sep 2019 16:12:03 +1000 Subject: [PATCH 170/369] Added some MCU ids. --- stm32loader/bootloader.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index da86246..072e067 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -55,6 +55,11 @@ # requires parity None 0x11103: "BlueNRG", # other + # STM32F0 + 0x440: "STM32F030x8", + 0x445: "STM32F070x6", + 0x448: "STM32F070xB", + 0x442: "STM32F030xC", # Cortex-M0 MCU with hardware TCP/IP and MAC # (SweetPeas custom bootloader) 0x801: "Wiznet W7500", From 2a24b6e1676b2c242098030570f3fbae5d9128ad Mon Sep 17 00:00:00 2001 From: Tyler Coy Date: Sun, 20 Oct 2019 18:15:54 -0500 Subject: [PATCH 171/369] Add support for STM32F3 --- stm32loader/bootloader.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index da86246..f9afb09 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -58,6 +58,12 @@ # Cortex-M0 MCU with hardware TCP/IP and MAC # (SweetPeas custom bootloader) 0x801: "Wiznet W7500", + + 0x432: "STM32F373xx/378xx", + 0x422: "STM32F302xB(C)/303xB(C)/358xx", + 0x439: "STM32F301xx/302x4(6/8)/318xx", + 0x438: "STM32F303x4(6/8)/334xx/328xx", + 0x446: "STM32F302xD(E)/303xD(E)/398xx", } @@ -193,6 +199,12 @@ class Reply: # ST RM0008 section 30.1 Unique device ID register # F101, F102, F103, F105, F107 "F1": 0x1FFFF7E8, + # ST RM0366 section 29.1 Unique device ID register + # ST RM0365 section 34.1 Unique device ID register + # ST RM0316 section 34.1 Unique device ID register + # ST RM0313 section 32.1 Unique device ID register + # F303/328/358/398, F301/318, F302, F37x + "F3": 0x1FFFF7AC, # ST RM0090 section 39.1 Unique device ID register # F405/415, F407/417, F427/437, F429/439 "F4": 0x1FFF7A10, @@ -214,6 +226,12 @@ class Reply: # ST RM0008 section 30.2 Memory size registers # F101, F102, F103, F105, F107 "F1": 0x1FFFF7E0, + # ST RM0366 section 29.2 Memory size data register + # ST RM0365 section 34.2 Memory size data register + # ST RM0316 section 34.2 Memory size data register + # ST RM0313 section 32.2 Flash memory size data register + # F303/328/358/398, F301/318, F302, F37x + "F3": 0x1FFFF7CC, # ST RM0090 section 39.2 Flash size # F405/415, F407/417, F427/437, F429/439 "F4": 0x1FFF7A22, From 380bd8876d0ab2cc8ae67cd63ec0b0a75e0f7028 Mon Sep 17 00:00:00 2001 From: alexklimaj Date: Sat, 21 Dec 2019 12:31:58 -0700 Subject: [PATCH 172/369] Add STM32G0 to ID --- stm32loader/bootloader.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index da86246..d8601c4 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -37,6 +37,7 @@ # 64 to 128 KiB 0x410: "STM32F10x Medium-density", 0x420: "STM32F10x Medium-density value line", + 0x460: "STM32G0x1", # 256 to 512 KiB (5128 Kbyte is probably a typo?) 0x414: "STM32F10x High-density", 0x428: "STM32F10x High-density value line", @@ -198,6 +199,8 @@ class Reply: "F4": 0x1FFF7A10, # ST RM0385 section 41.2 Unique device ID register "F7": 0x1FF0F420, + # ST RM0444 section 38.1 Unique device ID register + "G0": 0x1FFF7590, } UID_SWAP = [[1, 0], [3, 2], [7, 6, 5, 4], [11, 10, 9, 8]] @@ -219,6 +222,8 @@ class Reply: "F4": 0x1FFF7A22, # ST RM0385 section 41.2 Flash size "F7": 0x1FF0F442, + # ST RM0444 section 38.2 Flash memory size data register + "G0": 0x1FFF75E0, } DATA_TRANSFER_SIZE = 256 # bytes From 24ecbd9439a8636c108c80e45fe3b0559885cf4c Mon Sep 17 00:00:00 2001 From: Ondrej Mikle Date: Sat, 21 Dec 2019 22:28:48 +0100 Subject: [PATCH 173/369] Attempt to activate bootloader twice --- stm32loader/bootloader.py | 18 +++++++++++++++++- stm32loader/main.py | 1 + 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index da86246..c3dcbad 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -270,7 +270,23 @@ def reset_from_system_memory(self): """Reset the MCU with boot0 enabled to enter the bootloader.""" self._enable_boot0(True) self._reset() - return self.write_and_ack("Synchro", self.Command.SYNCHRONIZE) + + # Try the 0x7F synchronize that selects UART in bootloader (see ST application notes AN3155 and AN2606). + # If we are right after reset, it returns ACK, otherwise first time nothing, then NACK + # This is not documented in STM32 docs fully, but ST official tools use the same algorithm. + # Likely artifact/side effect of each command being 2-byte and having xor of bytes equal to 0xFF. + + for attempt in range(2): + self.write(self.Command.SYNCHRONIZE) + read_data = bytearray(self.connection.read()) + + if read_data: + reply = read_data[0] + if reply in (self.Reply.ACK, self.Reply.NACK): + return "" + print("Activation of bootloader timeouted, retrying", file=sys.stderr) + + raise CommandError("Bad reply from bootloader") def reset_from_flash(self): """Reset the MCU with boot0 disabled.""" diff --git a/stm32loader/main.py b/stm32loader/main.py index 27c89b9..5d5d38a 100644 --- a/stm32loader/main.py +++ b/stm32loader/main.py @@ -143,6 +143,7 @@ def connect(self): ) try: + print("Sending UART select to bootloader, attempting twice") self.stm32.reset_from_system_memory() except bootloader.CommandError: print( From 5150d4d9857ee86c05e1da1e9c93e357ac674a0e Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 31 Dec 2019 14:17:26 +0100 Subject: [PATCH 174/369] clean(style): Blacken using latest black black version 19.10b0 --- stm32loader/bootloader.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index da86246..6f2b03f 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -331,7 +331,7 @@ def get_flash_size(self, device_family): """Return the MCU's flash size in bytes.""" flash_size_address = self.FLASH_SIZE_ADDRESS[device_family] flash_size_bytes = self.read_memory(flash_size_address, 2) - flash_size = flash_size_bytes[0] + (flash_size_bytes[1]<<8) + flash_size = flash_size_bytes[0] + (flash_size_bytes[1] << 8) return flash_size def get_flash_size_and_uid_f4(self): @@ -347,8 +347,8 @@ def get_flash_size_and_uid_f4(self): flash_size_lsb_addr = 0x22 uid_lsb_addr = 0x10 data = self.read_memory(data_start_addr, self.DATA_TRANSFER_SIZE) - device_uid = data[uid_lsb_addr:uid_lsb_addr+12] - flash_size = data[flash_size_lsb_addr] + data[flash_size_lsb_addr+1]<<8 + device_uid = data[uid_lsb_addr : uid_lsb_addr + 12] + flash_size = data[flash_size_lsb_addr] + data[flash_size_lsb_addr + 1] << 8 return flash_size, device_uid def get_uid(self, device_id): @@ -370,7 +370,7 @@ def get_uid(self, device_id): return self.UID_NOT_SUPPORTED if uid_address == self.UID_ADDRESS_UNKNOWN: return self.UID_ADDRESS_UNKNOWN - + uid = self.read_memory(uid_address, 12) return uid From a912c766e7d9c6b7445bad01832e14d33c1d675a Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 31 Dec 2019 14:46:39 +0100 Subject: [PATCH 175/369] docs: Add references to STM32L4 manual --- stm32loader/bootloader.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index 8bdcf87..31f6f6c 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -49,9 +49,10 @@ # RM0090 in ( 38.6.1 MCU device ID code ) 0x413: "STM32F405xx/07xx and STM32F415xx/17xx", 0x419: "STM32F42xxx and STM32F43xxx", - 0x435: "STM32L4xx", 0x449: "STM32F74xxx/75xxx", 0x451: "STM32F76xxx/77xxx", + # RM0394 46.6.1 MCU device ID code + 0x435: "STM32L4xx", # see ST AN4872 # requires parity None 0x11103: "BlueNRG", @@ -199,7 +200,7 @@ class Reply: "F4": 0x1FFF7A10, # ST RM0385 section 41.2 Unique device ID register "F7": 0x1FF0F420, - # ST RM0394 + # ST RM0394 47.1 Unique device ID register (96 bits) 'L4': 0x1FFF7590, } From 4322196c4aa5012683ab607a8f712bb21ff55381 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 31 Dec 2019 14:54:50 +0100 Subject: [PATCH 176/369] docs: Add reference to RM0091 --- stm32loader/bootloader.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index 072e067..6a0de6f 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -54,8 +54,7 @@ # see ST AN4872 # requires parity None 0x11103: "BlueNRG", - # other - # STM32F0 + # STM32F0 RM0091 Table 136. DEV_ID and REV_ID field values 0x440: "STM32F030x8", 0x445: "STM32F070x6", 0x448: "STM32F070xB", From 3710b6dc3a478d67f0e4c2509cf3e54f7f487c6b Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 31 Dec 2019 15:06:40 +0100 Subject: [PATCH 177/369] clean: Re-shuffle the STM32F3 IDs a bit --- stm32loader/bootloader.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index f9afb09..d125b55 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -30,7 +30,7 @@ from functools import reduce CHIP_IDS = { - # see ST AN2606 Table 116 Bootloader device-dependent parameters + # see ST AN2606 Table 136 Bootloader device-dependent parameters # 16 to 32 KiB 0x412: "STM32F10x Low-density", 0x444: "STM32F03xx4/6", @@ -46,6 +46,12 @@ 0x416: "STM32L1xxx6(8/B) Medium-density ultralow power line", 0x411: "STM32F2xxx", 0x433: "STM32F4xxD/E", + # STM32F3 + 0x432: "STM32F373xx/378xx", + 0x422: "STM32F302xB(C)/303xB(C)/358xx", + 0x439: "STM32F301xx/302x4(6/8)/318xx", + 0x438: "STM32F303x4(6/8)/334xx/328xx", + 0x446: "STM32F302xD(E)/303xD(E)/398xx", # RM0090 in ( 38.6.1 MCU device ID code ) 0x413: "STM32F405xx/07xx and STM32F415xx/17xx", 0x419: "STM32F42xxx and STM32F43xxx", @@ -59,11 +65,6 @@ # (SweetPeas custom bootloader) 0x801: "Wiznet W7500", - 0x432: "STM32F373xx/378xx", - 0x422: "STM32F302xB(C)/303xB(C)/358xx", - 0x439: "STM32F301xx/302x4(6/8)/318xx", - 0x438: "STM32F303x4(6/8)/334xx/328xx", - 0x446: "STM32F302xD(E)/303xD(E)/398xx", } From 0baa51e47a9df55cdc14bae91a60d24c75afc2c3 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 31 Dec 2019 15:28:46 +0100 Subject: [PATCH 178/369] clean(style): Reshuffle a bit --- stm32loader/bootloader.py | 29 ++++++++++++++++++----------- stm32loader/main.py | 2 +- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index c3dcbad..6cc2c07 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -224,6 +224,8 @@ class Reply: DATA_TRANSFER_SIZE = 256 # bytes FLASH_PAGE_SIZE = 1024 # bytes + SYNCHRONIZE_ATTEMPTS = 2 + def __init__(self, connection, verbosity=5, show_progress=None): """ Construct the Stm32Bootloader object. @@ -271,21 +273,26 @@ def reset_from_system_memory(self): self._enable_boot0(True) self._reset() - # Try the 0x7F synchronize that selects UART in bootloader (see ST application notes AN3155 and AN2606). - # If we are right after reset, it returns ACK, otherwise first time nothing, then NACK - # This is not documented in STM32 docs fully, but ST official tools use the same algorithm. - # Likely artifact/side effect of each command being 2-byte and having xor of bytes equal to 0xFF. - - for attempt in range(2): + # Try the 0x7F synchronize that selects UART in bootloader mode + # (see ST application notes AN3155 and AN2606). + # If we are right after reset, it returns ACK, otherwise first + # time nothing, then NACK. + # This is not documented in STM32 docs fully, but ST official + # tools use the same algorithm. + # This is likely an artifact/side effect of each command being + # 2-bytes and having xor of bytes equal to 0xFF. + + for attempt in range(self.SYNCHRONIZE_ATTEMPTS): + if attempt: + print("Bootloader activation timeout -- retrying", file=sys.stderr) self.write(self.Command.SYNCHRONIZE) read_data = bytearray(self.connection.read()) - if read_data: - reply = read_data[0] - if reply in (self.Reply.ACK, self.Reply.NACK): - return "" - print("Activation of bootloader timeouted, retrying", file=sys.stderr) + if read_data and read_data[0] in (self.Reply.ACK, self.Reply.NACK): + # success + return + # not successful raise CommandError("Bad reply from bootloader") def reset_from_flash(self): diff --git a/stm32loader/main.py b/stm32loader/main.py index 5d5d38a..36c7ec1 100644 --- a/stm32loader/main.py +++ b/stm32loader/main.py @@ -143,7 +143,7 @@ def connect(self): ) try: - print("Sending UART select to bootloader, attempting twice") + print("Activating bootloader (select UART)") self.stm32.reset_from_system_memory() except bootloader.CommandError: print( From dc2e552d4f36b54b37057d91ccb3af9058cd3540 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 31 Dec 2019 15:31:44 +0100 Subject: [PATCH 179/369] Release: bump version number from v0.5.0 to v0.5.1-dev --- .bumpversion.cfg | 2 +- setup.py | 2 +- stm32loader/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 746c1ee..874d784 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.5.0 +current_version = 0.5.1-dev commit = True tag = False message = Release: bump version number from v{current_version} to v{new_version} diff --git a/setup.py b/setup.py index fa26a4b..128e527 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ # Package meta-data. NAME = 'stm32loader' -VERSION = "0.5.0" +VERSION = "0.5.1-dev" DESCRIPTION = 'Flash firmware to STM32 microcontrollers using Python.' URL = 'https://github.com/florisla/stm32loader' EMAIL = 'florisla@gmail.com' diff --git a/stm32loader/__init__.py b/stm32loader/__init__.py index 934f19d..2cfe18a 100644 --- a/stm32loader/__init__.py +++ b/stm32loader/__init__.py @@ -1,4 +1,4 @@ """Flash firmware to STM32 microcontrollers over a serial connection.""" -__version_info__ = (0, 5, 0) +__version_info__ = (0, 5, 1, "dev") __version__ = "-".join(str(part) for part in __version_info__).replace("-", ".", 2) From 948d2e026cb0387ffc467d5b93c7a6245a63b47c Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 31 Dec 2019 15:36:27 +0100 Subject: [PATCH 180/369] feat: Include version number in help text --- stm32loader/main.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/stm32loader/main.py b/stm32loader/main.py index 36c7ec1..48740ac 100644 --- a/stm32loader/main.py +++ b/stm32loader/main.py @@ -27,6 +27,7 @@ import os import sys +from stm32loader import __version__ from . import bootloader from .uart import SerialConnection @@ -209,7 +210,8 @@ def reset(self): @staticmethod def print_usage(): """Print help text explaining the command-line arguments.""" - help_text = """Usage: %s [-hqVeuwvrsRB] [-l length] [-p port] [-b baud] [-P parity] + help_text = """%s version %s +Usage: %s [-hqVeuwvrsRB] [-l length] [-p port] [-b baud] [-P parity] [-a address] [-g address] [-f family] [file.bin] -e Erase (note: this is required on previously written memory) -u Unprotect in case erase fails @@ -238,7 +240,7 @@ def print_usage(): Example: ./%s -e -w -v example/main.bin """ current_script = sys.argv[0] if sys.argv else "stm32loader" - help_text = help_text % (current_script, current_script, current_script) + help_text = help_text % (current_script, __version__, current_script, current_script, current_script) print(help_text) def read_device_id(self): From f9d652b6fb3ebac8483108113d1bcfa2c0802154 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 31 Dec 2019 15:41:20 +0100 Subject: [PATCH 181/369] feat: Add --version argument --- stm32loader/main.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/stm32loader/main.py b/stm32loader/main.py index 48740ac..87395c3 100644 --- a/stm32loader/main.py +++ b/stm32loader/main.py @@ -86,7 +86,7 @@ def parse_arguments(self, arguments): """Parse the list of command-line arguments.""" try: # parse command-line arguments using getopt - options, arguments = getopt.getopt(arguments, "hqVeuwvrsnRBP:p:b:a:l:g:f:", ["help"]) + options, arguments = getopt.getopt(arguments, "hqVeuwvrsnRBP:p:b:a:l:g:f:", ["help", "version"]) except getopt.GetoptError as err: # print help information and exit: # this prints something like "option -a not recognized" @@ -213,6 +213,7 @@ def print_usage(): help_text = """%s version %s Usage: %s [-hqVeuwvrsRB] [-l length] [-p port] [-b baud] [-P parity] [-a address] [-g address] [-f family] [file.bin] + --version Show version number and exit -e Erase (note: this is required on previously written memory) -u Unprotect in case erase fails -w Write file content to flash @@ -225,7 +226,7 @@ def print_usage(): -g address Start executing from address (0x08000000, usually) -f family Device family to read out device UID and flash size; e.g F1 for STM32F1xx - -h Print this help text + -h --help Print this help text -q Quiet mode -V Verbose mode @@ -284,6 +285,9 @@ def _parse_option_flags(self, options): elif option in ["-h", "--help"]: self.print_usage() sys.exit(0) + elif option == "--version": + print(__version__) + sys.exit(0) elif option == "-p": self.configuration["port"] = value elif option == "-f": From 116ddb554a93e112965d56c79692e17705956ee7 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 31 Dec 2019 15:50:48 +0100 Subject: [PATCH 182/369] docs: Show how to download firmware to file --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index d9423e4..6821f03 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,12 @@ You can skip the `-p` option by configuring environment variable `STM32LOADER_SERIAL_PORT`. Similarly, `-f` may be supplied through `STM32LOADER_FAMILY`. +To read out firmware and store it in a file: + +``` +stm32loader.py -r -p /dev/cu.usbserial-A5XK3RJT -f F1 -l 0x10000 -a 0x08000000 dump.bin +``` + Reference documents ------------------- From da17ba1996b5085bf99db96afedb66e779e54b3b Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 31 Dec 2019 15:55:47 +0100 Subject: [PATCH 183/369] feat: Support Python 3.8 --- .travis.yml | 6 +++++- README.md | 2 +- noxfile.py | 2 +- setup.py | 1 + 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index ea4f1c9..69c2ff7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,7 @@ matrix: - python: "3.6" env: NOXSESSION="lint" - # Test using nox on native Python 3.5/3.6/3.7 + # Test using nox on native Python 3.5/3.6/3.7/3.8 - python: "3.5" env: NOXSESSION="tests-3.5" - python: "3.6" @@ -15,6 +15,10 @@ matrix: env: NOXSESSION="tests-3.7" dist: xenial # necessary for Python 3.7 sudo: required # necessary for Python 3.7 + - python: "3.8" + env: NOXSESSION="tests-3.8" + dist: xenial # necessary for Python 3.8 + sudo: required # necessary for Python 3.8? # Test using nox on non-native Python version 3.6 # (nox does not natively run on 2.7 / 3.4) diff --git a/README.md b/README.md index 6821f03..c6daaa4 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ ST Microelectronics STM32 microcontrollers over UART. Also supports ST BlueNRG devices, and the SweetPeas bootloader for Wiznet W7500. -Compatible with Python version 3.4 to 3.7 or 2.7. +Compatible with Python version 3.4 to 3.8 or 2.7. Usage diff --git a/noxfile.py b/noxfile.py index 11156f9..10d1b12 100644 --- a/noxfile.py +++ b/noxfile.py @@ -5,7 +5,7 @@ import nox -@nox.session(python=["2.7", "3.4", "3.5", "3.6", "3.7"]) +@nox.session(python=["2.7", "3.4", "3.5", "3.6", "3.7", "3.8"]) def tests(session): """ Install stm32loader package and execute unit tests. diff --git a/setup.py b/setup.py index 128e527..69e53db 100644 --- a/setup.py +++ b/setup.py @@ -70,6 +70,7 @@ 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Natural Language :: English', From c8ca2f717aca8be6c479f81b60d1262fa936e425 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 31 Dec 2019 15:56:43 +0100 Subject: [PATCH 184/369] clean(lint) --- setup.cfg | 1 + stm32loader/bootloader.py | 5 ++--- stm32loader/main.py | 27 +++++++++++++++++++-------- stm32loader/uart.py | 2 +- 4 files changed, 23 insertions(+), 12 deletions(-) diff --git a/setup.cfg b/setup.cfg index bdc13aa..3d884c2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -23,6 +23,7 @@ ignore = # be compatible to black C812, # Missing trailing comma E203, # Whitespace before ':' + W503, # line break before binary operator per-file-ignores = # Missing docstring in public function tests/*:D103, diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index 2f65b48..450c713 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -71,7 +71,6 @@ # Cortex-M0 MCU with hardware TCP/IP and MAC # (SweetPeas custom bootloader) 0x801: "Wiznet W7500", - } @@ -219,7 +218,7 @@ class Reply: # ST RM0385 section 41.2 Unique device ID register "F7": 0x1FF0F420, # ST RM0394 47.1 Unique device ID register (96 bits) - 'L4': 0x1FFF7590, + "L4": 0x1FFF7590, # ST RM0444 section 38.1 Unique device ID register "G0": 0x1FFF7590, } @@ -250,7 +249,7 @@ class Reply: # ST RM0385 section 41.2 Flash size "F7": 0x1FF0F442, # ST RM0394 - 'L4': 0x1FFF75E0, + "L4": 0x1FFF75E0, # ST RM0444 section 38.2 Flash memory size data register "G0": 0x1FFF75E0, } diff --git a/stm32loader/main.py b/stm32loader/main.py index 87395c3..95c5846 100644 --- a/stm32loader/main.py +++ b/stm32loader/main.py @@ -27,9 +27,8 @@ import os import sys -from stm32loader import __version__ -from . import bootloader -from .uart import SerialConnection +from stm32loader import __version__, bootloader +from stm32loader.uart import SerialConnection DEFAULT_VERBOSITY = 5 @@ -86,7 +85,9 @@ def parse_arguments(self, arguments): """Parse the list of command-line arguments.""" try: # parse command-line arguments using getopt - options, arguments = getopt.getopt(arguments, "hqVeuwvrsnRBP:p:b:a:l:g:f:", ["help", "version"]) + options, arguments = getopt.getopt( + arguments, "hqVeuwvrsnRBP:p:b:a:l:g:f:", ["help", "version"] + ) except getopt.GetoptError as err: # print help information and exit: # this prints something like "option -a not recognized" @@ -131,7 +132,7 @@ def connect(self): " -p /dev/tty.usbserial-ftCYPMYJ\n", file=sys.stderr, ) - exit(1) + sys.exit(1) serial_connection.swap_rts_dtr = self.configuration["swap_rts_dtr"] serial_connection.reset_active_high = self.configuration["reset_active_high"] @@ -241,7 +242,13 @@ def print_usage(): Example: ./%s -e -w -v example/main.bin """ current_script = sys.argv[0] if sys.argv else "stm32loader" - help_text = help_text % (current_script, __version__, current_script, current_script, current_script) + help_text = help_text % ( + current_script, + __version__, + current_script, + current_script, + current_script, + ) print(help_text) def read_device_id(self): @@ -268,7 +275,9 @@ def read_device_uid(self): # special fix for F4 devices flash_size, device_uid = self.stm32.get_flash_size_and_uid_f4() except bootloader.CommandError as e: - self.debug(0, "Something was wrong with reading chip family data: " + e.message) + self.debug( + 0, "Something was wrong with reading chip family data: " + str(e), + ) return device_uid_string = self.stm32.format_uid(device_uid) @@ -310,7 +319,9 @@ def _get_progress_bar(hide_progress_bar=False): return None desired_progress_bar = None try: - from progress.bar import ChargingBar as desired_progress_bar + from progress.bar import ( # pylint: disable=import-outside-toplevel + ChargingBar as desired_progress_bar, + ) except ImportError: # progress module is a package dependency, # but not strictly required diff --git a/stm32loader/uart.py b/stm32loader/uart.py index 7824f5f..59d1cfe 100644 --- a/stm32loader/uart.py +++ b/stm32loader/uart.py @@ -27,7 +27,7 @@ import serial -class SerialConnection(object): +class SerialConnection(object): # pylint: disable=useless-object-inheritance """Wrap a serial.Serial connection and toggle reset and boot0.""" # Note: inheriting from object is required for Python2 setters From 3abca051c9c51d29eee20dfb533fb8fd381a423f Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 31 Dec 2019 16:42:44 +0100 Subject: [PATCH 185/369] docs: Update release notes --- CHANGELOG.md | 29 +++++++++++++++++++++++++++++ README.md | 3 ++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f4d6a5..bde8736 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,23 @@ +v0.5.1 +====== +* #25 Fix bug: Mass memory erase by byq77. +* #28 Add support for STM32L4 by rdaforno. +* #29 Add support for more STM32F0 ids by stawiski . +* #30 Add support for STM32F3 by float32. +* #32 Add support for STM32G0x1 by AlexKlimaj. +* #33 More robust bootloader activation by hiviah. +* #35 Support Python 3.8 +* #20 Add a 'read flash' example to README +* #34 Add --version argument + + +v0.5.0 +====== +* #17 Add support for STM32F03xx4/6 by omerk. +* Drop support for Python 3.2 and 3.3. + + v0.4.0 ====== * #8: Add support for STM32F7 mcus. By sam-bristow. @@ -11,21 +30,25 @@ v0.4.0 * Start using code linting and unit tests. * Start using Continuous Integration (Travis CI). + v0.3.3 ====== * Bugfix: write data, not [data]. By Atokulus. + v0.3.2 ====== * Publish on Python Package Index. * Make stm32loader executable as a module. * Expose stm32loader as a console script (stm32loader.exe on Windows). + v0.3.1 ====== * Make stm32loader installable and importable as a package. * Make write_memory faster (by Atokulus, see #1). + v0.3.0 ======= * Add version number. @@ -39,30 +62,36 @@ v0.3.0 * Read device flash size. * Refactor __main__ functionality into methods. + 2018-05 ======= * Make RTS/DTR (boot0/reset) configurable (polarity, swap). + 2018-04 ======= * Restore Python 2 compatibility. + 2018-03 ======= * Add support for Python 3. * Remove Psyco and progressbar support. * Fix checksum calculation bug for paged erase. + 2014-04 ======= * Add `-g
` (GO command). * Add known chip IDs. * Implement extended erase for STM32 F2/F4. + 2013-10 ======= * Add Windows compatibility. + 2009-04 ======= * Add GPL license. diff --git a/README.md b/README.md index c6daaa4..014d53b 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,8 @@ Acknowledgement Original Version by Ivan A-R (tuxotronic.org). Contributions by Domen Puncer, James Snyder, Floris Lambrechts, -Atokulus, sam-bristow, NINI1988, Omer Kilic, byq77. +Atokulus, sam-bristow, NINI1988, Omer Kilic, Szymon Szantula, rdaforno, +Mikolaj Stawiski, Tyler Coy, Alex Klimaj, Ondrej Mikle. Inspiration for features from: From 8c5aad82c107ed4d76fe9d02f1bbb305f100cf36 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 31 Dec 2019 16:43:02 +0100 Subject: [PATCH 186/369] Release: bump version number from v0.5.1-dev to v0.5.1 --- .bumpversion.cfg | 2 +- setup.py | 2 +- stm32loader/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 874d784..f87fcdf 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.5.1-dev +current_version = 0.5.1 commit = True tag = False message = Release: bump version number from v{current_version} to v{new_version} diff --git a/setup.py b/setup.py index 69e53db..77a09f5 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ # Package meta-data. NAME = 'stm32loader' -VERSION = "0.5.1-dev" +VERSION = "0.5.1" DESCRIPTION = 'Flash firmware to STM32 microcontrollers using Python.' URL = 'https://github.com/florisla/stm32loader' EMAIL = 'florisla@gmail.com' diff --git a/stm32loader/__init__.py b/stm32loader/__init__.py index 2cfe18a..a959570 100644 --- a/stm32loader/__init__.py +++ b/stm32loader/__init__.py @@ -1,4 +1,4 @@ """Flash firmware to STM32 microcontrollers over a serial connection.""" -__version_info__ = (0, 5, 1, "dev") +__version_info__ = (0, 5, 1) __version__ = "-".join(str(part) for part in __version_info__).replace("-", ".", 2) From 3f910b96785be3dfc7be9b00b9f56c6a9a7524fa Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 31 Dec 2019 16:43:52 +0100 Subject: [PATCH 187/369] Release: bump version number from v0.5.1 to v0.5.2-dev --- .bumpversion.cfg | 2 +- setup.py | 2 +- stm32loader/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index f87fcdf..0bfcb44 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.5.1 +current_version = 0.5.2-dev commit = True tag = False message = Release: bump version number from v{current_version} to v{new_version} diff --git a/setup.py b/setup.py index 77a09f5..6e80981 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ # Package meta-data. NAME = 'stm32loader' -VERSION = "0.5.1" +VERSION = "0.5.2-dev" DESCRIPTION = 'Flash firmware to STM32 microcontrollers using Python.' URL = 'https://github.com/florisla/stm32loader' EMAIL = 'florisla@gmail.com' diff --git a/stm32loader/__init__.py b/stm32loader/__init__.py index a959570..9429002 100644 --- a/stm32loader/__init__.py +++ b/stm32loader/__init__.py @@ -1,4 +1,4 @@ """Flash firmware to STM32 microcontrollers over a serial connection.""" -__version_info__ = (0, 5, 1) +__version_info__ = (0, 5, 2, "dev") __version__ = "-".join(str(part) for part in __version_info__).replace("-", ".", 2) From e1347a21d22c769bd310ac577dc2d8c905d8d75d Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Thu, 2 Jan 2020 21:16:21 +0100 Subject: [PATCH 188/369] docs: Mention --version argument in README --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 014d53b..c3f25dd 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,8 @@ Usage ----- ``` -./stm32loader.py [-hqVewvrsRB] [-l length] [-p port] [-b baud] [-P parity] [-a address] [-g address] [-f family] [file.bin] +./stm32loader [-hqVewvrsRB] [-l length] [-p port] [-b baud] [-P parity] [-a address] [-g address] [-f family] [file.bin] + --version Show version number and exit -e Erase (note: this is required on previously written memory) -u Readout unprotect -w Write file content to flash From 4e020f7bd501bf1f026d7e19c0e920925a48f4bb Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Thu, 2 Jan 2020 21:16:39 +0100 Subject: [PATCH 189/369] docs: Change stm32loader from Python file to executable --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c3f25dd..fb770d5 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ Example ------- ``` -stm32loader.py -p /dev/tty.usbserial-ftCYPMYJ -e -w -v somefile.bin +stm32loader -p /dev/tty.usbserial-ftCYPMYJ -e -w -v somefile.bin ``` This will pre-erase flash, write `somefile.bin` to the flash on the device, and then @@ -63,7 +63,7 @@ Similarly, `-f` may be supplied through `STM32LOADER_FAMILY`. To read out firmware and store it in a file: ``` -stm32loader.py -r -p /dev/cu.usbserial-A5XK3RJT -f F1 -l 0x10000 -a 0x08000000 dump.bin +stm32loader -r -p /dev/cu.usbserial-A5XK3RJT -f F1 -l 0x10000 -a 0x08000000 dump.bin ``` From 4f4c7840d8a2128d398e47e085dc9c5fe4605451 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Thu, 2 Jan 2020 21:24:04 +0100 Subject: [PATCH 190/369] clean: Stop announcing and testing Python 2 --- .travis.yml | 4 +--- README.md | 2 +- noxfile.py | 4 +--- pyproject.toml | 4 +--- setup.cfg | 2 +- setup.py | 4 +--- 6 files changed, 6 insertions(+), 14 deletions(-) diff --git a/.travis.yml b/.travis.yml index 69c2ff7..ad3d1ee 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,9 +21,7 @@ matrix: sudo: required # necessary for Python 3.8? # Test using nox on non-native Python version 3.6 - # (nox does not natively run on 2.7 / 3.4) - - python: "3.6" - env: NOXSESSION="tests-2.7" + # (nox does not natively run on 3.4) - python: "3.6" env: NOXSESSION="tests-3.4" diff --git a/README.md b/README.md index fb770d5..61388fc 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ ST Microelectronics STM32 microcontrollers over UART. Also supports ST BlueNRG devices, and the SweetPeas bootloader for Wiznet W7500. -Compatible with Python version 3.4 to 3.8 or 2.7. +Compatible with Python version 3.4 to 3.8. Usage diff --git a/noxfile.py b/noxfile.py index 10d1b12..fad9107 100644 --- a/noxfile.py +++ b/noxfile.py @@ -5,7 +5,7 @@ import nox -@nox.session(python=["2.7", "3.4", "3.5", "3.6", "3.7", "3.8"]) +@nox.session(python=["3.4", "3.5", "3.6", "3.7", "3.8"]) def tests(session): """ Install stm32loader package and execute unit tests. @@ -19,8 +19,6 @@ def tests(session): rmtree("./dist", ignore_errors=True) session.install(".") session.install("pytest") - if session.python == "2.7": - session.install("mock") session.chdir("tests") session.run("pytest", "./") diff --git a/pyproject.toml b/pyproject.toml index dd2e434..b0fb819 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,7 @@ [tool.black] line-length = 98 -# not including py27 since that triggers black -# to change print("") into print ("") -target-version = ['py34', 'py35', 'py36', 'py37'] +target-version = ['py34', 'py35', 'py36', 'py37', 'py38'] exclude = ''' /( \.git diff --git a/setup.cfg b/setup.cfg index 3d884c2..1cffdca 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bdist_wheel] -universal = 1 +universal = 0 [metadata] license_file = LICENSE diff --git a/setup.py b/setup.py index 6e80981..c416081 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ URL = 'https://github.com/florisla/stm32loader' EMAIL = 'florisla@gmail.com' AUTHOR = 'Floris Lambrechts' -REQUIRES_PYTHON = '>=2.7.0' +REQUIRES_PYTHON = '>=3.4.0' PROJECT_URLS = { "Bug Tracker": "https://github.com/florisla/stm32loader/issues", "Source Code": "https://github.com/florisla/stm32loader", @@ -63,8 +63,6 @@ classifiers=[ 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', From c9c0ba7182b8e11ad6037a9d0bd86a18e9a19b9c Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Thu, 2 Jan 2020 21:27:09 +0100 Subject: [PATCH 191/369] clean: Remove Python-2-isms At some point we should also remove superfluous bytearray() calls which were added in daf4ec7c6c3474d3eeb64946497c21a10993e9d0 . --- stm32loader/bootloader.py | 6 ++---- stm32loader/main.py | 2 -- stm32loader/uart.py | 7 ++----- tests/integration/test_stm32loader.py | 1 - 4 files changed, 4 insertions(+), 12 deletions(-) diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index 450c713..25df84a 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -20,8 +20,6 @@ """Talk to an STM32 native bootloader (see ST AN3155).""" -from __future__ import print_function - import math import operator import struct @@ -291,8 +289,8 @@ def write(self, *data): def write_and_ack(self, message, *data): """Write data to the MCU and wait until it replies with ACK.""" - # this is a separate method from write() because a keyword - # argument after *args is not possible in Python 2 + # Note: this is a separate method from write() because a keyword + # argument after *args was not possible in Python 2 self.write(*data) return self._wait_for_ack(message) diff --git a/stm32loader/main.py b/stm32loader/main.py index 95c5846..5766f3b 100644 --- a/stm32loader/main.py +++ b/stm32loader/main.py @@ -21,8 +21,6 @@ """Flash firmware to STM32 microcontrollers over a serial connection.""" -from __future__ import print_function - import getopt import os import sys diff --git a/stm32loader/uart.py b/stm32loader/uart.py index 59d1cfe..7732f4c 100644 --- a/stm32loader/uart.py +++ b/stm32loader/uart.py @@ -23,16 +23,13 @@ Offer support for toggling RESET and BOOT0. """ -# not naming this file itself 'serial', because that name-clashes in Python 2 +# This file not named 'serial' because that name-clashed in Python 2 import serial -class SerialConnection(object): # pylint: disable=useless-object-inheritance +class SerialConnection: """Wrap a serial.Serial connection and toggle reset and boot0.""" - # Note: inheriting from object is required for Python2 setters - # https://stackoverflow.com/questions/598077/why-does-foo-setter-in-python-not-work-for-me - # pylint: disable=too-many-instance-attributes def __init__(self, serial_port, baud_rate=115200, parity="E"): diff --git a/tests/integration/test_stm32loader.py b/tests/integration/test_stm32loader.py index e451a29..1c19d7d 100644 --- a/tests/integration/test_stm32loader.py +++ b/tests/integration/test_stm32loader.py @@ -121,7 +121,6 @@ def test_erase_resets_memory_to_all_ones(stm32loader, dump_file): stm32loader("-e") # read all bytes and check if they're 0xFF stm32loader("-r", "-l", "1024", dump_file) - # bytearray() is required for Python 2 read_data = bytearray(open(dump_file, "rb").read()) assert all(byte == 0xFF for byte in read_data) From a1ba0690e706e3d8033d0e12d98e277f23d2ff03 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Thu, 2 Jan 2020 21:46:03 +0100 Subject: [PATCH 192/369] docs: Describe basic contributor workflow --- DEVELOP.md | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 DEVELOP.md diff --git a/DEVELOP.md b/DEVELOP.md new file mode 100644 index 0000000..03cbc2a --- /dev/null +++ b/DEVELOP.md @@ -0,0 +1,56 @@ + +# How to contribute + + +## Installation + +Checkout the latest master. + + git clone https://github.com/florisla/stm32loader.git + +Install in editable mode with development tools (preferable in a virtual +environment). + + python -m venv .venv + .\.venv\bin\activate + pip uninstall stm32loader + pip install --editable .[dev] + + +## Testing + +Run pytest. + + pytest . + + +## Linting + +Run flake8, pylint and black. + + flake8 stm32loader + pylint stm32loader + black --check stm32loader + + +## Commit messages + +I try to follow the 'conventional commits' commit message style; +see https://www.conventionalcommits.org/ . + + +## Bump the version number + + bumpversion --new-version 1.0.8-dev bogus-part + + +## Tag a release + +First, bump the version number to a release version. +Then create the git tag. + + git tag -a "v1.0.9" -m "release: Tag version v1.0.9" + +Also push it to upstream. + + git push origin v1.0.9 From 64e9366197474cb3a820dd140f356ea14867c521 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Thu, 2 Jan 2020 21:46:17 +0100 Subject: [PATCH 193/369] fix: Also install bump2version as development tool --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index c416081..7fe422d 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ ] EXTRAS = { - "dev": ['setuptools', 'wheel', 'twine', 'pylint', 'flake8', 'flake8-isort', 'black'], + "dev": ['setuptools', 'wheel', 'twine', 'pylint', 'flake8', 'flake8-isort', 'black', 'bump2version'], } HERE = os.path.abspath(os.path.dirname(__file__)) From 794e8ac5bee51681281fb0c3a3c54faf96f9255a Mon Sep 17 00:00:00 2001 From: Emily Date: Thu, 16 Jan 2020 18:44:56 +0000 Subject: [PATCH 194/369] Port argument parsing to argparse optparse is deprecated, and argparse provides (semi-)automatic help formatting and much better error messages. I also added long versions of every option and converted the help strings to lowercase to conform with the conventions of the options automatically added by argparse. Fixes #19. Fixes #38. --- README.md | 67 +++++---- stm32loader/main.py | 332 ++++++++++++++++++++++++-------------------- 2 files changed, 223 insertions(+), 176 deletions(-) diff --git a/README.md b/README.md index 61388fc..4183ef9 100644 --- a/README.md +++ b/README.md @@ -19,30 +19,49 @@ Usage ----- ``` -./stm32loader [-hqVewvrsRB] [-l length] [-p port] [-b baud] [-P parity] [-a address] [-g address] [-f family] [file.bin] - --version Show version number and exit - -e Erase (note: this is required on previously written memory) - -u Readout unprotect - -w Write file content to flash - -v Verify flash content versus local file (recommended) - -r Read from flash and store in local file - -l length Length of read - -p port Serial port (default: /dev/tty.usbserial-ftCYPMYJ) - -b baud Baud speed (default: 115200) - -a address Target address (default: 0x08000000) - -g address Start executing from address (0x08000000, usually) - -f family Device family to read out device UID and flash size; e.g F1 for STM32F1xx - - -h Print this help text - -q Quiet mode - -V Verbose mode - - -s Swap RTS and DTR: use RTS for reset and DTR for boot0 - -R Make reset active high - -B Make boot0 active low - -u Readout unprotect - -n No progress: don't show progress bar - -P parity Parity: "even" for STM32 (default), "none" for BlueNRG +usage: stm32loader [-h] [-e] [-u] [-w] [-v] [-r] [-l LENGTH] -p PORT [-b BAUD] + [-a ADDRESS] [-g ADDRESS] [-f FAMILY] [-V] [-q] [-s] [-R] + [-B] [-n] [-P {even,none}] [--version] + [FILE.BIN] + +positional arguments: + FILE.BIN file to read from or store to flash + +optional arguments: + -h, --help show this help message and exit + -e, --erase erase (note: this is required on previously written + memory) + -u, --unprotect unprotect in case erase fails + -w, --write write file content to flash + -v, --verify verify flash content versus local file (recommended) + -r, --read read from flash and store in local file + -l LENGTH, --length LENGTH + length of read + -p PORT, --port PORT serial port (default: $STM32LOADER_SERIAL_PORT) + -b BAUD, --baud BAUD baudrate (default: 115200) + -a ADDRESS, --address ADDRESS + target address (default: 134217728) + -g ADDRESS, --go-address ADDRESS + start executing from address (0x08000000, usually) + -f FAMILY, --family FAMILY + device family to read out device UID and flash size; + e.g F1 for STM32F1xx (default: $STM32LOADER_FAMILY) + -V, --verbose verbose mode + -q, --quiet quiet mode + -s, --swap-rts-dtr swap RTS and DTR: use RTS for reset and DTR for boot0 + -R, --reset-active-high + make reset active high + -B, --boot0-active-low + make boot0 active low + -n, --no-progress don't show progress bar + -P {even,none}, --parity {even,none} + parity: "even" for STM32, "none" for BlueNRG (default: + even) + --version show program's version number and exit + +examples: + stm32loader -p COM7 -f F1 + stm32loader -e -w -v example/main.bin ``` diff --git a/stm32loader/main.py b/stm32loader/main.py index 5766f3b..01fce4b 100644 --- a/stm32loader/main.py +++ b/stm32loader/main.py @@ -21,9 +21,11 @@ """Flash firmware to STM32 microcontrollers over a serial connection.""" -import getopt import os import sys +import copy +import argparse +import atexit from stm32loader import __version__, bootloader from stm32loader.uart import SerialConnection @@ -31,91 +33,187 @@ DEFAULT_VERBOSITY = 5 +class HelpFormatter( + argparse.RawDescriptionHelpFormatter, + argparse.ArgumentDefaultsHelpFormatter +): + def _get_help_string(self, action): + action = copy.copy(action) + # Don't show "(default: None)" for arguments without defaults, + # or "(default: False)" for boolean flags, and hide the + # (default: 5) from --verbose's help because it's confusing. + if not action.default or action.dest == "verbosity": + action.default = argparse.SUPPRESS + return super()._get_help_string(action) + + def _format_actions_usage(self, actions, groups): + # Always treat -p/--port as required. See the note about the + # argparse hack in Stm32Loader.parse_arguments for why. + def tweak_action(action): + action = copy.copy(action) + if action.dest == "port": + action.required = True + return action + return super()._format_actions_usage(map(tweak_action, actions), groups) + class Stm32Loader: """Main application: parse arguments and handle commands.""" # serial link bit parity, compatible to pyserial serial.PARTIY_EVEN PARITY = {"even": "E", "none": "N"} - BOOLEAN_FLAG_OPTIONS = { - "-e": "erase", - "-u": "unprotect", - "-w": "write", - "-v": "verify", - "-r": "read", - "-s": "swap_rts_dtr", - "-n": "hide_progress_bar", - "-R": "reset_active_high", - "-B": "boot0_active_low", - } - - INTEGER_OPTIONS = {"-b": "baud", "-a": "address", "-g": "go_address", "-l": "length"} - def __init__(self): """Construct Stm32Loader object with default settings.""" self.stm32 = None - self.configuration = { - "port": os.environ.get("STM32LOADER_SERIAL_PORT"), - "baud": 115200, - "parity": self.PARITY["even"], - "family": os.environ.get("STM32LOADER_FAMILY"), - "address": 0x08000000, - "erase": False, - "unprotect": False, - "write": False, - "verify": False, - "read": False, - "go_address": -1, - "swap_rts_dtr": False, - "reset_active_high": False, - "boot0_active_low": False, - "hide_progress_bar": False, - "data_file": None, - } - self.verbosity = DEFAULT_VERBOSITY + self.configuration = None def debug(self, level, message): """Log a message to stderror if its level is low enough.""" - if self.verbosity >= level: + if self.configuration.verbosity >= level: print(message, file=sys.stderr) def parse_arguments(self, arguments): """Parse the list of command-line arguments.""" - try: - # parse command-line arguments using getopt - options, arguments = getopt.getopt( - arguments, "hqVeuwvrsnRBP:p:b:a:l:g:f:", ["help", "version"] - ) - except getopt.GetoptError as err: - # print help information and exit: - # this prints something like "option -a not recognized" - print(str(err)) - self.print_usage() - sys.exit(2) + parser = argparse.ArgumentParser( + epilog='\n'.join([ + 'examples:', + ' %(prog)s -p COM7 -f F1', + ' %(prog)s -e -w -v example/main.bin', + ]), + formatter_class=HelpFormatter) + + data_file_arg = parser.add_argument( + "data_file", metavar="FILE.BIN", type=str, nargs="?", + help="file to read from or store to flash") + + parser.add_argument( + "-e", "--erase", action="store_true", + help="erase (note: this is required on previously written memory)") + + parser.add_argument( + "-u", "--unprotect", action="store_true", + help="unprotect in case erase fails") + + parser.add_argument( + "-w", "--write", action="store_true", + help="write file content to flash") + + parser.add_argument( + "-v", "--verify", action="store_true", + help="verify flash content versus local file (recommended)") + + parser.add_argument( + "-r", "--read", action="store_true", + help="read from flash and store in local file") + + length_arg = parser.add_argument( + "-l", "--length", action="store", type=int, + help="length of read") + + default_port = os.environ.get("STM32LOADER_SERIAL_PORT") + port_arg = parser.add_argument( + "-p", "--port", action="store", type=str, # morally required=True + default=default_port, + help="serial port" + + ("" if default_port else " (default: $STM32LOADER_SERIAL_PORT)")) + + parser.add_argument( + "-b", "--baud", action="store", type=int, default=115200, + help="baudrate") + + address_arg = parser.add_argument( + "-a", "--address", action="store", type=int, default=0x08000000, + help="target address") + + parser.add_argument( + "-g", "--go-address", action="store", type=int, metavar="ADDRESS", + help="start executing from address (0x08000000, usually)") + + default_family = os.environ.get("STM32LOADER_FAMILY") + parser.add_argument( + "-f", "--family", action="store", type=str, + default=default_family, + help="device family to read out device UID and flash size; " + "e.g F1 for STM32F1xx" + + ("" if default_family else " (default: $STM32LOADER_FAMILY)")) + + parser.add_argument( + "-V", "--verbose", dest="verbosity", action="store_const", const=10, + default=DEFAULT_VERBOSITY, + help="verbose mode") + + parser.add_argument( + "-q", "--quiet", dest="verbosity", action="store_const", const=0, + help="quiet mode") + + parser.add_argument( + "-s", "--swap-rts-dtr", action="store_true", + help="swap RTS and DTR: use RTS for reset and DTR for boot0") + + parser.add_argument( + "-R", "--reset-active-high", action="store_true", + help="make reset active high") + + parser.add_argument( + "-B", "--boot0-active-low", action="store_true", + help="make boot0 active low") + + parser.add_argument( + "-n", "--no-progress", action="store_true", + help="don't show progress bar") + + parser.add_argument( + "-P", "--parity", action="store", type=str, default="even", + choices=self.PARITY.keys(), + help='parity: "even" for STM32, "none" for BlueNRG') + + parser.add_argument("--version", action="version", version=__version__) + + # Hack: We want certain arguments to be required when one + # of -rwv is specified, but argparse doesn't support + # conditional dependencies like that. Instead, we add the + # requirements post-facto and re-run the parse to get the error + # messages we want. A better solution would be to use + # subcommands instead of options for -rwv, but this would + # change the command-line interface. + # + # We also use this gross hack to provide a hint about the + # STM32LOADER_SERIAL_PORT environment variable when -p + # is omitted; we only set --port as required after the first + # parse so we can hook in a custom error message. + + self.configuration = parser.parse_args(arguments) + + if not self.configuration.port: + port_arg.required = True + atexit.register(lambda: print( + "{}: note: you can also set the environment " + "variable STM32LOADER_SERIAL_PORT".format(parser.prog), + file=sys.stderr, + )) - # if there's a non-named argument left, that's a file name - if arguments: - self.configuration["data_file"] = arguments[0] + if self.configuration.read or self.configuration.write or self.configuration.verify: + data_file_arg.nargs = None + data_file_arg.required = True - self._parse_option_flags(options) + if self.configuration.read: + length_arg.required = True + address_arg.required = True - if not self.configuration["port"]: - print( - "No serial port configured. Supply the -p option " - "or configure environment variable STM32LOADER_SERIAL_PORT.", - file=sys.stderr, - ) - sys.exit(3) + parser.parse_args(arguments) + + # parse successful, process options further + self.configuration.parity = Stm32Loader.PARITY[self.configuration.parity.lower()] def connect(self): """Connect to the RS-232 serial port.""" serial_connection = SerialConnection( - self.configuration["port"], self.configuration["baud"], self.configuration["parity"] + self.configuration.port, self.configuration.baud, self.configuration.parity ) self.debug( 10, "Open port %(port)s, baud %(baud)d" - % {"port": self.configuration["port"], "baud": self.configuration["baud"]}, + % {"port": self.configuration.port, "baud": self.configuration.baud}, ) try: serial_connection.connect() @@ -132,14 +230,16 @@ def connect(self): ) sys.exit(1) - serial_connection.swap_rts_dtr = self.configuration["swap_rts_dtr"] - serial_connection.reset_active_high = self.configuration["reset_active_high"] - serial_connection.boot0_active_low = self.configuration["boot0_active_low"] + serial_connection.swap_rts_dtr = self.configuration.swap_rts_dtr + serial_connection.reset_active_high = self.configuration.reset_active_high + serial_connection.boot0_active_low = self.configuration.boot0_active_low - show_progress = self._get_progress_bar(self.configuration["hide_progress_bar"]) + show_progress = self._get_progress_bar(self.configuration.no_progress) self.stm32 = bootloader.Stm32Bootloader( - serial_connection, verbosity=self.verbosity, show_progress=show_progress + serial_connection, + verbosity=self.configuration.verbosity, + show_progress=show_progress ) try: @@ -157,10 +257,10 @@ def perform_commands(self): """Run all operations as defined by the configuration.""" # pylint: disable=too-many-branches binary_data = None - if self.configuration["write"] or self.configuration["verify"]: - with open(self.configuration["data_file"], "rb") as read_file: + if self.configuration.write or self.configuration.verify: + with open(self.configuration.data_file, "rb") as read_file: binary_data = bytearray(read_file.read()) - if self.configuration["unprotect"]: + if self.configuration.unprotect: try: self.stm32.readout_unprotect() except bootloader.CommandError: @@ -169,7 +269,7 @@ def perform_commands(self): self.debug(0, "Quit") self.stm32.reset_from_flash() sys.exit(1) - if self.configuration["erase"]: + if self.configuration.erase: try: self.stm32.erase_memory() except bootloader.CommandError: @@ -181,11 +281,11 @@ def perform_commands(self): ) self.stm32.reset_from_flash() sys.exit(1) - if self.configuration["write"]: - self.stm32.write_memory_data(self.configuration["address"], binary_data) - if self.configuration["verify"]: + if self.configuration.write: + self.stm32.write_memory_data(self.configuration.address, binary_data) + if self.configuration.verify: read_data = self.stm32.read_memory_data( - self.configuration["address"], len(binary_data) + self.configuration.address, len(binary_data) ) try: bootloader.Stm32Bootloader.verify_data(read_data, binary_data) @@ -193,62 +293,19 @@ def perform_commands(self): except bootloader.DataMismatchError as e: print("Verification FAILED: %s" % e, file=sys.stdout) sys.exit(1) - if not self.configuration["write"] and self.configuration["read"]: + if not self.configuration.write and self.configuration.read: read_data = self.stm32.read_memory_data( - self.configuration["address"], self.configuration["length"] + self.configuration.address, self.configuration.length ) - with open(self.configuration["data_file"], "wb") as out_file: + with open(self.configuration.data_file, "wb") as out_file: out_file.write(read_data) - if self.configuration["go_address"] != -1: - self.stm32.go(self.configuration["go_address"]) + if self.configuration.go_address is not None: + self.stm32.go(self.configuration.go_address) def reset(self): """Reset the microcontroller.""" self.stm32.reset_from_flash() - @staticmethod - def print_usage(): - """Print help text explaining the command-line arguments.""" - help_text = """%s version %s -Usage: %s [-hqVeuwvrsRB] [-l length] [-p port] [-b baud] [-P parity] - [-a address] [-g address] [-f family] [file.bin] - --version Show version number and exit - -e Erase (note: this is required on previously written memory) - -u Unprotect in case erase fails - -w Write file content to flash - -v Verify flash content versus local file (recommended) - -r Read from flash and store in local file - -l length Length of read - -p port Serial port (default: /dev/tty.usbserial-ftCYPMYJ) - -b baud Baudrate (default: 115200) - -a address Target address (default: 0x08000000) - -g address Start executing from address (0x08000000, usually) - -f family Device family to read out device UID and flash size; e.g F1 for STM32F1xx - - -h --help Print this help text - -q Quiet mode - -V Verbose mode - - -s Swap RTS and DTR: use RTS for reset and DTR for boot0 - -R Make reset active high - -B Make boot0 active low - -u Readout unprotect - -n No progress: don't show progress bar - -P parity Parity: "even" for STM32 (default), "none" for BlueNRG - - Example: ./%s -p COM7 -f F1 - Example: ./%s -e -w -v example/main.bin -""" - current_script = sys.argv[0] if sys.argv else "stm32loader" - help_text = help_text % ( - current_script, - __version__, - current_script, - current_script, - current_script, - ) - print(help_text) - def read_device_id(self): """Show chip ID and bootloader version.""" boot_version = self.stm32.get() @@ -260,7 +317,7 @@ def read_device_id(self): def read_device_uid(self): """Show chip UID and flash size.""" - family = self.configuration["family"] + family = self.configuration.family if not family: self.debug(0, "Supply -f [family] to see flash size and device UID, e.g: -f F1") return @@ -282,38 +339,9 @@ def read_device_uid(self): self.debug(0, "Device UID: %s" % device_uid_string) self.debug(0, "Flash size: %d KiB" % flash_size) - def _parse_option_flags(self, options): - # pylint: disable=eval-used - for option, value in options: - if option == "-V": - self.verbosity = 10 - elif option == "-q": - self.verbosity = 0 - elif option in ["-h", "--help"]: - self.print_usage() - sys.exit(0) - elif option == "--version": - print(__version__) - sys.exit(0) - elif option == "-p": - self.configuration["port"] = value - elif option == "-f": - self.configuration["family"] = value - elif option == "-P": - assert ( - value.lower() in Stm32Loader.PARITY - ), "Parity value not recognized: '{0}'.".format(value) - self.configuration["parity"] = Stm32Loader.PARITY[value.lower()] - elif option in self.INTEGER_OPTIONS: - self.configuration[self.INTEGER_OPTIONS[option]] = int(eval(value)) - elif option in self.BOOLEAN_FLAG_OPTIONS: - self.configuration[self.BOOLEAN_FLAG_OPTIONS[option]] = True - else: - assert False, "unhandled option %s" % option - @staticmethod - def _get_progress_bar(hide_progress_bar=False): - if hide_progress_bar: + def _get_progress_bar(no_progress=False): + if no_progress: return None desired_progress_bar = None try: From 126ae5bbb439b16e0f31dcf2baa2efcd97c6971c Mon Sep 17 00:00:00 2001 From: denniszollo Date: Tue, 19 Jan 2021 19:57:58 -0800 Subject: [PATCH 195/369] Add lookup information for H7 devices; protocol should remain the same --- stm32loader/bootloader.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index 25df84a..f42a861 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -55,6 +55,7 @@ 0x413: "STM32F405xx/07xx and STM32F415xx/17xx", 0x419: "STM32F42xxx and STM32F43xxx", 0x449: "STM32F74xxx/75xxx", + 0x450: "STM32H76xxx/77xxx", 0x451: "STM32F76xxx/77xxx", # RM0394 46.6.1 MCU device ID code 0x435: "STM32L4xx", @@ -215,6 +216,8 @@ class Reply: "F4": 0x1FFF7A10, # ST RM0385 section 41.2 Unique device ID register "F7": 0x1FF0F420, + # ST RM0433 section 61.1 Unique device ID register + "H7": 0x1FF1E800, # ST RM0394 47.1 Unique device ID register (96 bits) "L4": 0x1FFF7590, # ST RM0444 section 38.1 Unique device ID register @@ -246,6 +249,8 @@ class Reply: "F4": 0x1FFF7A22, # ST RM0385 section 41.2 Flash size "F7": 0x1FF0F442, + # ST RM0433 61.2 Flash size + "H7": 0x1FF1E880, # ST RM0394 "L4": 0x1FFF75E0, # ST RM0444 section 38.2 Flash memory size data register From 88279c0efeb8ed46c61aef4c419e9934b87067b9 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Fri, 25 Feb 2022 21:36:19 +0100 Subject: [PATCH 196/369] feat: Add some unit tests on argument parsing --- tests/integration/test_stm32loader.py | 17 ---------- tests/unit/test_arguments.py | 45 +++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 17 deletions(-) create mode 100644 tests/unit/test_arguments.py diff --git a/tests/integration/test_stm32loader.py b/tests/integration/test_stm32loader.py index 1c19d7d..0b24a09 100644 --- a/tests/integration/test_stm32loader.py +++ b/tests/integration/test_stm32loader.py @@ -53,15 +53,6 @@ def test_stm32loader_is_executable(): subprocess.call(["stm32loader", "--help"]) -@pytest.mark.parametrize( - "help_argument", ["-h", "--help"], -) -def test_argument_h_prints_help_info(help_argument, capsys): - main(help_argument, avoid_system_exit=True) - captured = capsys.readouterr() - assert "Example:" in captured.out - - def test_unexisting_serial_port_prints_readable_error(capsys): main("-p", "COM108", avoid_system_exit=True) captured = capsys.readouterr() @@ -70,14 +61,6 @@ def test_unexisting_serial_port_prints_readable_error(capsys): assert "Is the device connected and powered correctly?" in captured.err -def test_missing_argument_p_prints_readable_error(capsys): - main(avoid_system_exit=True) - captured = capsys.readouterr() - assert "No serial port configured" in captured.err - assert "Supply the -p option" in captured.err - assert "environment variable STM32LOADER_SERIAL_PORT" in captured.err - - def test_env_var_stm32loader_serial_port_defines_port(capsys): os.environ['STM32LOADER_SERIAL_PORT'] = "COM109" main(avoid_system_exit=True) diff --git a/tests/unit/test_arguments.py b/tests/unit/test_arguments.py new file mode 100644 index 0000000..3a83f4c --- /dev/null +++ b/tests/unit/test_arguments.py @@ -0,0 +1,45 @@ + +import atexit + +import pytest + +from stm32loader.main import Stm32Loader + + +@pytest.fixture +def program(): + return Stm32Loader() + + +def test_parse_arguments_without_args_raises_typeerror(program): + with pytest.raises(TypeError, match="missing.*required.*argument"): + program.parse_arguments() + + +def test_parse_arguments_with_standard_args_passes(program): + program.parse_arguments(["-p", "port", "-b", "9600", "-q"]) + + +@pytest.mark.parametrize( + "help_argument", ["-h", "--help"], +) +def test_parse_arguments_with_help_raises_systemexit(program, help_argument): + with pytest.raises(SystemExit): + program.parse_arguments([help_argument]) + + +def test_parse_arguments_erase_without_port_complains_about_missing_argument(program, capsys): + try: + program.parse_arguments(["-e", "-w", "-v", "file.bin"]) + except SystemExit: + pass + + # Also call atexit functions so that the hint about using an env variable + # is printed. + atexit._run_exitfuncs() + + _output, error_output = capsys.readouterr() + if not error_output: + pytest.skip("Not sure why nothing is captured in some pytest runs?") + assert "arguments are required: -p/--port" in error_output + assert "STM32LOADER_SERIAL_PORT" in error_output From 3122583852966029fe265af7ca64577549c4372e Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Fri, 25 Feb 2022 21:36:49 +0100 Subject: [PATCH 197/369] clean(lint) --- .pylintrc | 5 +- stm32loader/main.py | 195 ++++++++++++++++++++++++++------------------ 2 files changed, 118 insertions(+), 82 deletions(-) diff --git a/.pylintrc b/.pylintrc index 6db7898..4d4958c 100644 --- a/.pylintrc +++ b/.pylintrc @@ -3,8 +3,9 @@ max-line-length=98 [MESSAGES CONTROL] disable= - fixme, # TO DOs are not errors - bad-continuation, # be compatible to black + fixme, # TO DOs are not errors. + consider-using-f-string, # We're not on Python >= 3.6 yet. + bad-continuation, # Be compatible to black. [REPORT] score=no diff --git a/stm32loader/main.py b/stm32loader/main.py index 01fce4b..3c15e21 100644 --- a/stm32loader/main.py +++ b/stm32loader/main.py @@ -21,11 +21,16 @@ """Flash firmware to STM32 microcontrollers over a serial connection.""" -import os -import sys -import copy import argparse import atexit +import copy +import os +import sys + +try: + from progress.bar import ChargingBar as progress_bar +except ImportError: + progress_bar = None from stm32loader import __version__, bootloader from stm32loader.uart import SerialConnection @@ -33,10 +38,9 @@ DEFAULT_VERBOSITY = 5 -class HelpFormatter( - argparse.RawDescriptionHelpFormatter, - argparse.ArgumentDefaultsHelpFormatter -): +class HelpFormatter(argparse.RawDescriptionHelpFormatter, argparse.ArgumentDefaultsHelpFormatter): + """Custom help formatter -- don't print confusing default values.""" + def _get_help_string(self, action): action = copy.copy(action) # Don't show "(default: None)" for arguments without defaults, @@ -54,8 +58,10 @@ def tweak_action(action): if action.dest == "port": action.required = True return action + return super()._format_actions_usage(map(tweak_action, actions), groups) + class Stm32Loader: """Main application: parse arguments and handle commands.""" @@ -75,97 +81,139 @@ def debug(self, level, message): def parse_arguments(self, arguments): """Parse the list of command-line arguments.""" parser = argparse.ArgumentParser( - epilog='\n'.join([ - 'examples:', - ' %(prog)s -p COM7 -f F1', - ' %(prog)s -e -w -v example/main.bin', - ]), - formatter_class=HelpFormatter) + epilog="\n".join( + [ + "examples:", + " %(prog)s -p COM7 -f F1", + " %(prog)s -e -w -v example/main.bin", + ] + ), + formatter_class=HelpFormatter, + ) data_file_arg = parser.add_argument( - "data_file", metavar="FILE.BIN", type=str, nargs="?", - help="file to read from or store to flash") + "data_file", + metavar="FILE.BIN", + type=str, + nargs="?", + help="file to read from or store to flash", + ) parser.add_argument( - "-e", "--erase", action="store_true", - help="erase (note: this is required on previously written memory)") + "-e", + "--erase", + action="store_true", + help="erase (note: this is required on previously written memory)", + ) parser.add_argument( - "-u", "--unprotect", action="store_true", - help="unprotect in case erase fails") + "-u", "--unprotect", action="store_true", help="unprotect in case erase fails" + ) parser.add_argument( - "-w", "--write", action="store_true", - help="write file content to flash") + "-w", "--write", action="store_true", help="write file content to flash" + ) parser.add_argument( - "-v", "--verify", action="store_true", - help="verify flash content versus local file (recommended)") + "-v", + "--verify", + action="store_true", + help="verify flash content versus local file (recommended)", + ) parser.add_argument( - "-r", "--read", action="store_true", - help="read from flash and store in local file") + "-r", "--read", action="store_true", help="read from flash and store in local file" + ) length_arg = parser.add_argument( - "-l", "--length", action="store", type=int, - help="length of read") + "-l", "--length", action="store", type=int, help="length of read" + ) default_port = os.environ.get("STM32LOADER_SERIAL_PORT") port_arg = parser.add_argument( - "-p", "--port", action="store", type=str, # morally required=True + "-p", + "--port", + action="store", + type=str, # morally required=True default=default_port, - help="serial port" + - ("" if default_port else " (default: $STM32LOADER_SERIAL_PORT)")) + help=( + "serial port" + ("" if default_port else " (default: $STM32LOADER_SERIAL_PORT)") + ), + ) parser.add_argument( - "-b", "--baud", action="store", type=int, default=115200, - help="baudrate") + "-b", "--baud", action="store", type=int, default=115200, help="baudrate" + ) address_arg = parser.add_argument( - "-a", "--address", action="store", type=int, default=0x08000000, - help="target address") + "-a", "--address", action="store", type=int, default=0x08000000, help="target address" + ) parser.add_argument( - "-g", "--go-address", action="store", type=int, metavar="ADDRESS", - help="start executing from address (0x08000000, usually)") + "-g", + "--go-address", + action="store", + type=int, + metavar="ADDRESS", + help="start executing from address (0x08000000, usually)", + ) default_family = os.environ.get("STM32LOADER_FAMILY") parser.add_argument( - "-f", "--family", action="store", type=str, + "-f", + "--family", + action="store", + type=str, default=default_family, - help="device family to read out device UID and flash size; " - "e.g F1 for STM32F1xx" + - ("" if default_family else " (default: $STM32LOADER_FAMILY)")) + help=( + "device family to read out device UID and flash size; " + "e.g F1 for STM32F1xx" + + ("" if default_family else " (default: $STM32LOADER_FAMILY)") + ), + ) parser.add_argument( - "-V", "--verbose", dest="verbosity", action="store_const", const=10, + "-V", + "--verbose", + dest="verbosity", + action="store_const", + const=10, default=DEFAULT_VERBOSITY, - help="verbose mode") + help="verbose mode", + ) parser.add_argument( - "-q", "--quiet", dest="verbosity", action="store_const", const=0, - help="quiet mode") + "-q", "--quiet", dest="verbosity", action="store_const", const=0, help="quiet mode" + ) parser.add_argument( - "-s", "--swap-rts-dtr", action="store_true", - help="swap RTS and DTR: use RTS for reset and DTR for boot0") + "-s", + "--swap-rts-dtr", + action="store_true", + help="swap RTS and DTR: use RTS for reset and DTR for boot0", + ) parser.add_argument( - "-R", "--reset-active-high", action="store_true", - help="make reset active high") + "-R", "--reset-active-high", action="store_true", help="make reset active high" + ) parser.add_argument( - "-B", "--boot0-active-low", action="store_true", - help="make boot0 active low") + "-B", "--boot0-active-low", action="store_true", help="make boot0 active low" + ) parser.add_argument( - "-n", "--no-progress", action="store_true", - help="don't show progress bar") + "-n", "--no-progress", action="store_true", help="don't show progress bar" + ) parser.add_argument( - "-P", "--parity", action="store", type=str, default="even", + "-P", + "--parity", + action="store", + type=str, + default="even", choices=self.PARITY.keys(), - help='parity: "even" for STM32, "none" for BlueNRG') + help='parity: "even" for STM32, "none" for BlueNRG', + ) parser.add_argument("--version", action="version", version=__version__) @@ -186,11 +234,13 @@ def parse_arguments(self, arguments): if not self.configuration.port: port_arg.required = True - atexit.register(lambda: print( - "{}: note: you can also set the environment " - "variable STM32LOADER_SERIAL_PORT".format(parser.prog), - file=sys.stderr, - )) + atexit.register( + lambda: print( + "{}: note: you can also set the environment " + "variable STM32LOADER_SERIAL_PORT".format(parser.prog), + file=sys.stderr, + ) + ) if self.configuration.read or self.configuration.write or self.configuration.verify: data_file_arg.nargs = None @@ -237,9 +287,7 @@ def connect(self): show_progress = self._get_progress_bar(self.configuration.no_progress) self.stm32 = bootloader.Stm32Bootloader( - serial_connection, - verbosity=self.configuration.verbosity, - show_progress=show_progress + serial_connection, verbosity=self.configuration.verbosity, show_progress=show_progress ) try: @@ -284,9 +332,7 @@ def perform_commands(self): if self.configuration.write: self.stm32.write_memory_data(self.configuration.address, binary_data) if self.configuration.verify: - read_data = self.stm32.read_memory_data( - self.configuration.address, len(binary_data) - ) + read_data = self.stm32.read_memory_data(self.configuration.address, len(binary_data)) try: bootloader.Stm32Bootloader.verify_data(read_data, binary_data) print("Verification OK") @@ -331,7 +377,8 @@ def read_device_uid(self): flash_size, device_uid = self.stm32.get_flash_size_and_uid_f4() except bootloader.CommandError as e: self.debug( - 0, "Something was wrong with reading chip family data: " + str(e), + 0, + "Something was wrong with reading chip family data: " + str(e), ) return @@ -341,22 +388,10 @@ def read_device_uid(self): @staticmethod def _get_progress_bar(no_progress=False): - if no_progress: - return None - desired_progress_bar = None - try: - from progress.bar import ( # pylint: disable=import-outside-toplevel - ChargingBar as desired_progress_bar, - ) - except ImportError: - # progress module is a package dependency, - # but not strictly required - pass - - if not desired_progress_bar: + if no_progress or not progress_bar: return None - return bootloader.ShowProgress(desired_progress_bar) + return bootloader.ShowProgress(progress_bar) def main(*args, **kwargs): From 2cab2182fbf00adc260a03836caf51d51870751c Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Sun, 27 Feb 2022 22:21:40 +0100 Subject: [PATCH 198/369] release: Drop Python 3.4, 3.5 and add 3.9 and 3.10 --- noxfile.py | 8 ++++++-- setup.cfg | 1 - setup.py | 8 ++++---- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/noxfile.py b/noxfile.py index fad9107..0508515 100644 --- a/noxfile.py +++ b/noxfile.py @@ -5,7 +5,11 @@ import nox -@nox.session(python=["3.4", "3.5", "3.6", "3.7", "3.8"]) +DEFAULT_PYTHON_VERSION = "3.9" +ALL_PYTHON_VERSIONS = ["3.6", "3.7", "3.8", "3.9", "3.10"] + + +@nox.session(python=ALL_PYTHON_VERSIONS) def tests(session): """ Install stm32loader package and execute unit tests. @@ -23,7 +27,7 @@ def tests(session): session.run("pytest", "./") -@nox.session(python=["3.6"]) +@nox.session(python=DEFAULT_PYTHON_VERSION) def lint(session): """ Run code verification tools flake8, pylint and black. diff --git a/setup.cfg b/setup.cfg index 1cffdca..1ba0626 100644 --- a/setup.cfg +++ b/setup.cfg @@ -6,7 +6,6 @@ license_file = LICENSE [isort] line_length = 98 -line_width = 98 multi_line_output = 2 [flake8] diff --git a/setup.py b/setup.py index 7fe422d..efa085a 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ URL = 'https://github.com/florisla/stm32loader' EMAIL = 'florisla@gmail.com' AUTHOR = 'Floris Lambrechts' -REQUIRES_PYTHON = '>=3.4.0' +REQUIRES_PYTHON = '>=3.6.0' PROJECT_URLS = { "Bug Tracker": "https://github.com/florisla/stm32loader/issues", "Source Code": "https://github.com/florisla/stm32loader", @@ -28,7 +28,7 @@ ] EXTRAS = { - "dev": ['setuptools', 'wheel', 'twine', 'pylint', 'flake8', 'flake8-isort', 'black', 'bump2version'], + "dev": ['setuptools', 'wheel', 'twine', 'pylint', 'flake8', 'flake8-isort', 'black', 'bump2version', 'nox'], } HERE = os.path.abspath(os.path.dirname(__file__)) @@ -64,11 +64,11 @@ 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', 'Programming Language :: Python', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Natural Language :: English', From 7501e16cb57ab8ec4cd6fac9f6827021901996fb Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 28 Feb 2022 09:37:21 +0100 Subject: [PATCH 199/369] feat: Flush input buffer after MCU reset --- stm32loader/bootloader.py | 6 ++++++ stm32loader/uart.py | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index f42a861..ff1d032 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -309,6 +309,12 @@ def reset_from_system_memory(self): self._enable_boot0(True) self._reset() + # Flush the input buffer to avoid reading old data. + # It's known that the CP2102N at high baudrate fails to flush + # its buffer when the port is opened. + if hasattr(self.connection, "flush_input_buffer"): + self.connection.flush_input_buffer() + # Try the 0x7F synchronize that selects UART in bootloader mode # (see ST application notes AN3155 and AN2606). # If we are right after reset, it returns ACK, otherwise first diff --git a/stm32loader/uart.py b/stm32loader/uart.py index 7732f4c..fa2f68e 100644 --- a/stm32loader/uart.py +++ b/stm32loader/uart.py @@ -110,3 +110,7 @@ def enable_boot0(self, enable=True): self.serial_connection.setDTR(level) else: self.serial_connection.setRTS(level) + + def flush_imput_buffer(self): + """Flush the input buffer to remove any stale read data.""" + self.serial_connection.reset_input_buffer() From 85bc03eb4169035868a45150c27478ed7b4a50bd Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 28 Feb 2022 09:37:39 +0100 Subject: [PATCH 200/369] docs: Improve code comments --- stm32loader/main.py | 2 +- stm32loader/uart.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/stm32loader/main.py b/stm32loader/main.py index 3c15e21..a196d8c 100644 --- a/stm32loader/main.py +++ b/stm32loader/main.py @@ -256,7 +256,7 @@ def parse_arguments(self, arguments): self.configuration.parity = Stm32Loader.PARITY[self.configuration.parity.lower()] def connect(self): - """Connect to the RS-232 serial port.""" + """Connect to the bootloader UART over an RS-232 serial port.""" serial_connection = SerialConnection( self.configuration.port, self.configuration.baud, self.configuration.parity ) diff --git a/stm32loader/uart.py b/stm32loader/uart.py index fa2f68e..fc0df3a 100644 --- a/stm32loader/uart.py +++ b/stm32loader/uart.py @@ -23,7 +23,9 @@ Offer support for toggling RESET and BOOT0. """ -# This file not named 'serial' because that name-clashed in Python 2 +# Note: this file not named 'serial' because that name-clashed in Python 2 + + import serial From 40fb05972ef87c72c870cecd1f1bc0bc43d22ad8 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 28 Feb 2022 09:58:49 +0100 Subject: [PATCH 201/369] fix: Don't limit page count to 256 in extended_erase_memory Lifted from https://github.com/florisla/stm32loader/pull/45 to fix https://github.com/florisla/stm32loader/issues/44 . --- stm32loader/bootloader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index ff1d032..67a94e4 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -550,7 +550,7 @@ def extended_erase_memory(self, pages=None): "Can not erase more than 65535 pages at once.\n" "Set pages to None to do global erase or supply fewer pages." ) - page_count = (len(pages) & 0xFF) - 1 + page_count = (len(pages) & 0xFFFF) - 1 page_count_bytes = bytearray(struct.pack(">H", page_count)) page_bytes = bytearray(len(pages) * 2) for i, page in enumerate(pages): From 21756b3812ee7952c3e8ab0829d0b12b7f9e8336 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 28 Feb 2022 10:26:59 +0100 Subject: [PATCH 202/369] docs: Refer to alternative (/similar) tools --- ALTERNATIVES.md | 55 +++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 7 +++++++ 2 files changed, 62 insertions(+) create mode 100644 ALTERNATIVES.md diff --git a/ALTERNATIVES.md b/ALTERNATIVES.md new file mode 100644 index 0000000..6bedc98 --- /dev/null +++ b/ALTERNATIVES.md @@ -0,0 +1,55 @@ + +# Alternative tools + + +## Stm32CubeProgrammer + +Official cross-platform tool by ST, with GUI and CLI versions. + +Supports all bootloader protocols (UART, I2C, SPI, USB DFU, CAN). +Supports OTP memory and option bytes. + +https://www.st.com/en/development-tools/stm32cubeprog.html + + +## STM32 Flash loader demonstrator (Flasher-stm32) + +Windows tool by ST. + +It comes with a command-line version and source code. + +https://www.st.com/en/development-tools/flasher-stm32.html + + +## STM32 ST-Link utility + +This is replaced by Stm32CubProgrammer (see above). + +https://www.st.com/en/development-tools/stsw-link004.html + + +## stm32flash + +Open source flashing tool in C. + +Supports UART and SPI, and covers many STM32 devices. +This is probably a good choice if you're looking for speed. + +https://sourceforge.net/projects/stm32flash/ + + +## ST-flash + +This is open re-implementation of the ST-Link tools. + +https://github.com/stlink-org/stlink st-flash + + +## stm32flash-lib + +Java library allowing to flash STM32 microcontrollers over UART. + +https://github.com/grevaillot/stm32flash-lib + + +## GigaDevice Windows ISP GUI diff --git a/README.md b/README.md index 4183ef9..9ab2eb6 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,13 @@ Inspiration for features from: https://github.com/Sweet-Peas/WiznetLoader +Alternatives +------------ + +If you don't need the flexibility of a Python tool, you can take +a look at other similar tools in `ALTERNATIVES.md`. + + Electrically ------------ From 1a496c06957f8ca7c608e170dba1fd17f938dfd6 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 28 Feb 2022 11:11:10 +0100 Subject: [PATCH 203/369] feat: Add metadata for L0 family --- stm32loader/bootloader.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index 67a94e4..552520e 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -42,6 +42,7 @@ # 768 to 1024 KiB 0x430: "STM3210xx XL-density", # flash size to be looked up + 0x417: "STM32L05xxx/06xxx", 0x416: "STM32L1xxx6(8/B) Medium-density ultralow power line", 0x411: "STM32F2xxx", 0x433: "STM32F4xxD/E", @@ -220,6 +221,8 @@ class Reply: "H7": 0x1FF1E800, # ST RM0394 47.1 Unique device ID register (96 bits) "L4": 0x1FFF7590, + # ST RM0451 25.2 Unique device ID register (96 bits) + "L0": 0x1FF80050, # ST RM0444 section 38.1 Unique device ID register "G0": 0x1FFF7590, } @@ -253,6 +256,8 @@ class Reply: "H7": 0x1FF1E880, # ST RM0394 "L4": 0x1FFF75E0, + # ST RM4510 25.1 Memory size register + "L0": 0x1FF8007C, # ST RM0444 section 38.2 Flash memory size data register "G0": 0x1FFF75E0, } From 71d93b8f3f66b607c5c7544f92332c680d285831 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 28 Feb 2022 11:15:11 +0100 Subject: [PATCH 204/369] feat: Pass in device_family to the constructor --- stm32loader/bootloader.py | 13 ++++++------- tests/unit/test_bootloader.py | 15 +++++++++------ 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index 552520e..31afbba 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -267,7 +267,7 @@ class Reply: SYNCHRONIZE_ATTEMPTS = 2 - def __init__(self, connection, verbosity=5, show_progress=None): + def __init__(self, connection, device_family=None, verbosity=5, show_progress=None): """ Construct the Stm32Bootloader object. @@ -289,6 +289,7 @@ def __init__(self, connection, verbosity=5, show_progress=None): self.verbosity = verbosity self.show_progress = show_progress or ShowProgress(None) self.extended_erase = False + self.device_family = device_family or "F1" def write(self, *data): """Write the given data to the MCU.""" @@ -397,9 +398,9 @@ def get_id(self): _device_id = reduce(lambda x, y: x * 0x100 + y, id_data) return _device_id - def get_flash_size(self, device_family): + def get_flash_size(self): """Return the MCU's flash size in bytes.""" - flash_size_address = self.FLASH_SIZE_ADDRESS[device_family] + flash_size_address = self.FLASH_SIZE_ADDRESS[self.device_family] flash_size_bytes = self.read_memory(flash_size_address, 2) flash_size = flash_size_bytes[0] + (flash_size_bytes[1] << 8) return flash_size @@ -421,7 +422,7 @@ def get_flash_size_and_uid_f4(self): flash_size = data[flash_size_lsb_addr] + data[flash_size_lsb_addr + 1] << 8 return flash_size, device_uid - def get_uid(self, device_id): + def get_uid(self): """ Send the 'Get UID' command and return the device UID. @@ -430,12 +431,10 @@ def get_uid(self, device_id): Return UIT_ADDRESS_UNKNOWN if the address of the device's UID is not known. - :param str device_id: Device family name such as "F1". - See UID_ADDRESS. :return byterary: UID bytes of the device, or 0 or -1 when not available. """ - uid_address = self.UID_ADDRESS.get(device_id, self.UID_ADDRESS_UNKNOWN) + uid_address = self.UID_ADDRESS.get(self.device_family, self.UID_ADDRESS_UNKNOWN) if uid_address is None: return self.UID_NOT_SUPPORTED if uid_address == self.UID_ADDRESS_UNKNOWN: diff --git a/tests/unit/test_bootloader.py b/tests/unit/test_bootloader.py index d5b49e0..1d99496 100644 --- a/tests/unit/test_bootloader.py +++ b/tests/unit/test_bootloader.py @@ -190,19 +190,22 @@ def test_verify_data_with_non_identical_data_raises_verify_error_complaining_abo @pytest.mark.parametrize( "family", ["F1", "F4", "F7"], ) -def test_get_uid_for_known_family_reads_at_correct_address(bootloader, family): +def test_get_uid_for_known_family_reads_at_correct_address(connection, family): + bootloader = Stm32Bootloader(connection, device_family=family) bootloader.read_memory = MagicMock() - bootloader.get_uid("F1") + bootloader.get_uid() uid_address = bootloader.UID_ADDRESS[family] assert bootloader.read_memory.called_once_with(uid_address) -def test_get_uid_for_family_without_uid_returns_uid_not_supported(bootloader): - assert bootloader.UID_NOT_SUPPORTED == bootloader.get_uid("F0") +def test_get_uid_for_family_without_uid_returns_uid_not_supported(connection): + bootloader = Stm32Bootloader(connection, device_family="F0") + assert bootloader.UID_NOT_SUPPORTED == bootloader.get_uid() -def test_get_uid_for_unknown_family_returns_uid_address_unknown(bootloader): - assert bootloader.UID_ADDRESS_UNKNOWN == bootloader.get_uid("X") +def test_get_uid_for_unknown_family_returns_uid_address_unknown(connection): + bootloader = Stm32Bootloader(connection, device_family="X") + assert bootloader.UID_ADDRESS_UNKNOWN == bootloader.get_uid() @pytest.mark.parametrize( From 18d038cdcafe8706ed9332d54912abf7a9a476c9 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 28 Feb 2022 11:15:11 +0100 Subject: [PATCH 205/369] feat: Derive data_transfer_size and flash_page_size from device_family --- stm32loader/bootloader.py | 68 ++++++++++++++++++++++++++++++++++----- 1 file changed, 60 insertions(+), 8 deletions(-) diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index 31afbba..18363f3 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -262,8 +262,58 @@ class Reply: "G0": 0x1FFF75E0, } - DATA_TRANSFER_SIZE = 256 # bytes - FLASH_PAGE_SIZE = 1024 # bytes + DATA_TRANSFER_SIZE = { + "default": 256, + # No unique id for these parts + "F0": 256, # bytes + # ST RM0008 section 30.1 Unique device ID register + # F101, F102, F103, F105, F107 + "F1": 256, # bytes + # ST RM0366 section 29.1 Unique device ID register + # ST RM0365 section 34.1 Unique device ID register + # ST RM0316 section 34.1 Unique device ID register + # ST RM0313 section 32.1 Unique device ID register + # F303/328/358/398, F301/318, F302, F37x + "F3": 256, # bytes + # ST RM0090 section 39.1 Unique device ID register + # F405/415, F407/417, F427/437, F429/439 + "F4": 256, # bytes + # ST RM0385 section 41.2 Unique device ID register + "F7": 256, # bytes + # ST RM0394 47.1 Unique device ID register (96 bits) + "L4": 256, # bytes + # ST RM0451 25.2 Unique device ID register (96 bits) + "L0": 128, # bytes + # ST RM0444 section 38.1 Unique device ID register + "G0": 256, # bytes + } + + FLASH_PAGE_SIZE = { + "default": 1024, + # ST RM0360 section 27.1 Memory size data register + # F030x4/x6/x8/xC, F070x6/xB + "F0": 1024, # bytes + # ST RM0008 section 30.2 Memory size registers + # F101, F102, F103, F105, F107 + "F1": 1024, # bytes + # ST RM0366 section 29.2 Memory size data register + # ST RM0365 section 34.2 Memory size data register + # ST RM0316 section 34.2 Memory size data register + # ST RM0313 section 32.2 Flash memory size data register + # F303/328/358/398, F301/318, F302, F37x + "F3": 2048, # bytes + # ST RM0090 section 39.2 Flash size + # F405/415, F407/417, F427/437, F429/439 + "F4": 1024, # bytes + # ST RM0385 section 41.2 Flash size + "F7": 1024, # bytes + # ST RM0394 + "L4": 1024, # bytes + # ST RM4510 25.1 Memory size register + "L0": 128, # bytes + # ST RM0444 section 38.2 Flash memory size data register + "G0": 1024, # bytes + } SYNCHRONIZE_ATTEMPTS = 2 @@ -289,6 +339,8 @@ def __init__(self, connection, device_family=None, verbosity=5, show_progress=No self.verbosity = verbosity self.show_progress = show_progress or ShowProgress(None) self.extended_erase = False + self.data_transfer_size = self.DATA_TRANSFER_SIZE.get(device_family or "default") + self.flash_page_size = self.FLASH_PAGE_SIZE.get(device_family or "default") self.device_family = device_family or "F1" def write(self, *data): @@ -461,7 +513,7 @@ def read_memory(self, address, length): Supports maximum 256 bytes. """ - if length > self.DATA_TRANSFER_SIZE: + if length > self.data_transfer_size: raise DataLengthError("Can not read more than 256 bytes at once.") self.command(self.Command.READ_MEMORY, "Read memory") self.write_and_ack("0x11 address failed", self._encode_address(address)) @@ -485,7 +537,7 @@ def write_memory(self, address, data): nr_of_bytes = len(data) if nr_of_bytes == 0: return - if nr_of_bytes > self.DATA_TRANSFER_SIZE: + if nr_of_bytes > self.data_transfer_size: raise DataLengthError("Can not write more than 256 bytes at once.") self.command(self.Command.WRITE_MEMORY, "Write memory") self.write_and_ack("0x31 address failed", self._encode_address(address)) @@ -618,11 +670,11 @@ def read_memory_data(self, address, length): Length may be more than 256 bytes. """ data = bytearray() - chunk_count = int(math.ceil(length / float(self.DATA_TRANSFER_SIZE))) + chunk_count = int(math.ceil(length / float(self.data_transfer_size))) self.debug(5, "Read %d chunks at address 0x%X..." % (chunk_count, address)) with self.show_progress("Reading", maximum=chunk_count) as progress_bar: while length: - read_length = min(length, self.DATA_TRANSFER_SIZE) + read_length = min(length, self.data_transfer_size) self.debug( 10, "Read %(len)d bytes at 0x%(address)X" @@ -641,13 +693,13 @@ def write_memory_data(self, address, data): Data length may be more than 256 bytes. """ length = len(data) - chunk_count = int(math.ceil(length / float(self.DATA_TRANSFER_SIZE))) + chunk_count = int(math.ceil(length / float(self.data_transfer_size))) offset = 0 self.debug(5, "Write %d chunks at address 0x%X..." % (chunk_count, address)) with self.show_progress("Writing", maximum=chunk_count) as progress_bar: while length: - write_length = min(length, self.DATA_TRANSFER_SIZE) + write_length = min(length, self.data_transfer_size) self.debug( 10, "Write %(len)d bytes at 0x%(address)X" From 1a9e3272518e3b4d16b88dd12add52adc8b2d656 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 28 Feb 2022 11:15:11 +0100 Subject: [PATCH 206/369] feat: Enable get_flash_size_and_uid also for L0 family --- stm32loader/bootloader.py | 30 ++++++++++++++++++++++-------- stm32loader/main.py | 10 +++++----- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index 18363f3..a197cb1 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -457,21 +457,35 @@ def get_flash_size(self): flash_size = flash_size_bytes[0] + (flash_size_bytes[1] << 8) return flash_size - def get_flash_size_and_uid_f4(self): + def get_flash_size_and_uid(self): """ - Return device_uid and flash_size for F4 family. + Return device_uid and flash_size for L0 and F4 family devices. For some reason, F4 (at least, NUCLEO F401RE) can't read the 12 or 2 bytes for UID and flash size directly. Reading a whole chunk of 256 bytes at 0x1FFFA700 does work and requires some data extraction. """ - data_start_addr = 0x1FFF7A00 - flash_size_lsb_addr = 0x22 - uid_lsb_addr = 0x10 - data = self.read_memory(data_start_addr, self.DATA_TRANSFER_SIZE) - device_uid = data[uid_lsb_addr : uid_lsb_addr + 12] - flash_size = data[flash_size_lsb_addr] + data[flash_size_lsb_addr + 1] << 8 + flash_size_address = self.FLASH_SIZE_ADDRESS[self.device_family] + uid_address = self.UID_ADDRESS.get(self.device_family) + + if uid_address is None: + return None, None + + data_start_address = uid_address & 0xFFFFFF00 + flash_size_lsb_address = flash_size_address - data_start_address + uid_lsb_address = uid_address - data_start_address + + self.debug(10, "flash_size_address = 0x%X" % flash_size_address) + self.debug(10, "uid_address = 0x%X" % uid_address) + # self.debug(10, 'data_start_address =0x%X' % data_start_address) + # self.debug(10, 'flashsizelsbaddress =0x%X' % flash_size_lsb_address) + # self.debug(10, 'uid_lsb_address = 0x%X' % uid_lsb_address) + + data = self.read_memory(data_start_address, self.data_transfer_size) + device_uid = data[uid_lsb_address : uid_lsb_address + 12] + flash_size = data[flash_size_lsb_address] + (data[flash_size_lsb_address + 1] << 8) + return flash_size, device_uid def get_uid(self): diff --git a/stm32loader/main.py b/stm32loader/main.py index a196d8c..174d004 100644 --- a/stm32loader/main.py +++ b/stm32loader/main.py @@ -369,12 +369,12 @@ def read_device_uid(self): return try: - if family != "F4": - flash_size = self.stm32.get_flash_size(family) - device_uid = self.stm32.get_uid(family) + if family not in ["F4", "L0"]: + flash_size = self.stm32.get_flash_size() + device_uid = self.stm32.get_uid() else: - # special fix for F4 devices - flash_size, device_uid = self.stm32.get_flash_size_and_uid_f4() + # special fix for F4 and L0 devices + flash_size, device_uid = self.stm32.get_flash_size_and_uid() except bootloader.CommandError as e: self.debug( 0, From 34fe027cafa997ddf2eefb97678ababc261f4f8b Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 28 Feb 2022 11:15:11 +0100 Subject: [PATCH 207/369] feat: Erase pages individually for L0 family --- stm32loader/bootloader.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index a197cb1..ad876ef 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -583,6 +583,12 @@ def erase_memory(self, pages=None): return self.command(self.Command.ERASE, "Erase memory") + + if not pages and self.device_family == "L0": + # Special case: L0 erase should do each page separately. + flash_size, _uid = self.get_flash_size_and_uid() + page_count = (flash_size * 1024) // self.flash_page_size + pages = range(page_count - 1) if pages: # page erase, see ST AN3155 if len(pages) > 255: From 611d871500bf6ddae720e16bbfbc681be69a55d7 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 28 Feb 2022 12:50:31 +0100 Subject: [PATCH 208/369] docs: Describe GigaDevice tools --- ALTERNATIVES.md | 5 ++++- EXTEND.md | 0 RUN.md | 0 3 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 EXTEND.md create mode 100644 RUN.md diff --git a/ALTERNATIVES.md b/ALTERNATIVES.md index 6bedc98..3f5b790 100644 --- a/ALTERNATIVES.md +++ b/ALTERNATIVES.md @@ -52,4 +52,7 @@ Java library allowing to flash STM32 microcontrollers over UART. https://github.com/grevaillot/stm32flash-lib -## GigaDevice Windows ISP GUI +## GigaDevice tools + +GigaDevice offers the GD-Link Programmer to work with the GD-Link debug adapter, +and GigaDevice MCU ISP Programmer. diff --git a/EXTEND.md b/EXTEND.md new file mode 100644 index 0000000..e69de29 diff --git a/RUN.md b/RUN.md new file mode 100644 index 0000000..e69de29 From 749aca1a586702ad373125de1c5d327a9f5dc052 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 28 Feb 2022 12:50:50 +0100 Subject: [PATCH 209/369] docs: Describe how to extend stm32loader --- EXTEND.md | 64 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/EXTEND.md b/EXTEND.md index e69de29..059d503 100644 --- a/EXTEND.md +++ b/EXTEND.md @@ -0,0 +1,64 @@ + +# Extending stm32loader + +You can create your own extensions on top of stm32loader's classes. + + +## Example: Use Raspberry Pi GPIO pins to toggle `BOOT0` and `RESET` + +Subclass the `SerialConnection` and override `enable_reset` and `enable_boot0`. + +```python3 + +from RPi import GPIO +from stm32loader.uart import SerialConnection + + +class RaspberrySerialWithGpio(SerialConnection): + # Configure which GPIO pins are connected to the STM32's BOOT0 and RESET pins. + BOOT0_PIN = 2 + RESET_PIN = 3 + + def __init__(self, serial_port, baud_rate, parity): + super().__init__(serial_port, baude_rate, parity) + + GPIO.setup(self.BOOT0_PIN, GPIO.OUT, initial=GPIO.LOW) + GPIO.setup(self.RESET_PIN, GPIO.OUT, initial=GPIO.HIGH) + + def enable_reset(self, enable=True): + """Enable or disable the reset IO line.""" + # Reset is active low. + # To enter reset, write a 0. + level = 1 - int(enable) + + GPIO.output(self.RESET_PIN, level) + + def enable_boot0(self, enable=True): + """Enable or disable the boot0 IO line.""" + level = int(enable) + + GPIO.output(self.BOOT0_PIN, level) +``` + +Connect to the UART and instantiate a Bootloader object. + +```python3 + +from stm32loader.bootloader import Stm32Bootloader + +from raspberrystm32 import RaspberrySerialWithGpio + + +connection = RaspberrySerialWithGpio("/dev/cu.usbserial-A5XK3RJT") +connection.connect() +stm32 = Stm32Bootloader(connection, device_family="F1") +``` + +Now you can use all of the Stm32Bootloader methods. + +```python3 +stm32.reset_from_system_memory() +print(stm32.get_version()) +print(stm32.get_id()) +print(stm32.get_flash_size()) +``` From 5247d3df8b6c37423cb2665c0a275c118a47c596 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 28 Feb 2022 12:51:15 +0100 Subject: [PATCH 210/369] docs: Show how to install from github --- README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9ab2eb6..f25a305 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,14 @@ for Wiznet W7500. Compatible with Python version 3.4 to 3.8. -Usage ------ +## Installation + + pip install stm32loader + +To install the latest development version: + + pip install git+https://github.com/florisla/stm32loader.git + ``` usage: stm32loader [-h] [-e] [-u] [-w] [-v] [-r] [-l LENGTH] -p PORT [-b BAUD] From ad8957afbb0dd92dae4eebff2347c3fa52c1e2e1 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 28 Feb 2022 12:52:31 +0100 Subject: [PATCH 211/369] docs: Describe the various ways to start stm32loader --- RUN.md | 52 +++++++++++++++++++++++++++++++++++++++++++++ stm32loader/main.py | 3 ++- stm32loader/uart.py | 7 ++++++ 3 files changed, 61 insertions(+), 1 deletion(-) diff --git a/RUN.md b/RUN.md index e69de29..f33ec11 100644 --- a/RUN.md +++ b/RUN.md @@ -0,0 +1,52 @@ + +# Running stm32loader + + +## Execute as a module + +After installing stm32loader with `pip`, it's available as a Python module. + +You can execute this with `python -m [modulename]`. + +```shell +python3 -m stm32loader +``` + + +## Execute as a module without installing + +You can also run `stm32loader` without installing it. You do need `pyserial` though. + +Make sure you are in the root of the repository, or the repository is in `PYTHONPATH`. + +```shell +python3 -m pip install pyserial --user +python3 -m stm32loader +``` + + +## Execute main.py directly + +The file `main.py` also runs the `stm32loader` program when executed. +Make sure the module can be found; add the folder of the repository to `PYTHONPATH`. + +```shell +PYTHONPATH=. python3 stm32loader/main.py +``` + + +## Use from Python + +You can use the classes of `stm32loader` from a Python script. + +Example: + +```python +from stm32loader.main import Stm32Loader + +loader = Stm32Loader() +loader.configuration.port = "/dev/cu.usbserial-A5XK3RJT" +loader.connect() +loader.stm32.readout_unprotect() +loader.disconnect() +``` diff --git a/stm32loader/main.py b/stm32loader/main.py index 174d004..e6ab455 100644 --- a/stm32loader/main.py +++ b/stm32loader/main.py @@ -26,6 +26,7 @@ import copy import os import sys +from types import SimpleNamespace try: from progress.bar import ChargingBar as progress_bar @@ -71,7 +72,7 @@ class Stm32Loader: def __init__(self): """Construct Stm32Loader object with default settings.""" self.stm32 = None - self.configuration = None + self.configuration = SimpleNamespace() def debug(self, level, message): """Log a message to stderror if its level is low enough.""" diff --git a/stm32loader/uart.py b/stm32loader/uart.py index fc0df3a..49d07dc 100644 --- a/stm32loader/uart.py +++ b/stm32loader/uart.py @@ -77,6 +77,13 @@ def connect(self): timeout=self._timeout, ) + def disconnect(self): + if not self.serial_connection: + return + + self.serial_connection.close() + self.serial_connection = None + def write(self, *args, **kwargs): """Write the given data to the serial connection.""" return self.serial_connection.write(*args, **kwargs) From b9bc1d4575ef1d8a83c00c0338a7b224c57ca434 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 28 Feb 2022 12:52:31 +0100 Subject: [PATCH 212/369] docs: Drop To Do items which are done --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index f25a305..af80920 100644 --- a/README.md +++ b/README.md @@ -172,5 +172,4 @@ Future work * Use proper logging instead of print statements * Try Azure pipelines for CI * Support PyPy, PyPy3 -* Drop Python2 support; start using intenum for commands and replies -* Determine flash page size or make this configurable +* Start using intenum for commands and replies From 4b3a86c160060155a79f30b0be1acf3790950327 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 28 Feb 2022 12:52:31 +0100 Subject: [PATCH 213/369] docs: Update Python versions --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index af80920..556d9e9 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ ST Microelectronics STM32 microcontrollers over UART. Also supports ST BlueNRG devices, and the SweetPeas bootloader for Wiznet W7500. -Compatible with Python version 3.4 to 3.8. +Compatible with Python version 3.6 to 3.10. ## Installation From f71daa23258c2be6982bd2914d9c5a7321cffe2d Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 28 Feb 2022 12:52:31 +0100 Subject: [PATCH 214/369] docs(cosmetic) --- README.md | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 556d9e9..b0625db 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ -STM32Loader -=========== +# STM32Loader [![PyPI package](https://badge.fury.io/py/stm32loader.svg)](https://badge.fury.io/py/stm32loader) [![Build Status](https://travis-ci.org/florisla/stm32loader.svg?branch=master)](https://travis-ci.org/florisla/stm32loader) @@ -24,6 +23,8 @@ To install the latest development version: pip install git+https://github.com/florisla/stm32loader.git +## Usage + ``` usage: stm32loader [-h] [-e] [-u] [-w] [-v] [-r] [-l LENGTH] -p PORT [-b BAUD] [-a ADDRESS] [-g ADDRESS] [-f FAMILY] [-V] [-q] [-s] [-R] @@ -71,8 +72,7 @@ examples: ``` -Example -------- +## Command-line example ``` stm32loader -p /dev/tty.usbserial-ftCYPMYJ -e -w -v somefile.bin @@ -92,16 +92,14 @@ stm32loader -r -p /dev/cu.usbserial-A5XK3RJT -f F1 -l 0x10000 -a 0x08000000 dump ``` -Reference documents -------------------- +## Reference documents * ST AN2606: STM32 microcontroller system memory boot mode * ST AN3155: USART protocol used in the STM32 bootloader * ST AN4872: BlueNRG-1 and BlueNRG-2 UART bootloader protocol -Acknowledgement ---------------- +## Acknowledgement Original Version by Ivan A-R (tuxotronic.org). Contributions by Domen Puncer, James Snyder, Floris Lambrechts, @@ -126,15 +124,13 @@ Inspiration for features from: https://github.com/Sweet-Peas/WiznetLoader -Alternatives ------------- +## Alternatives If you don't need the flexibility of a Python tool, you can take a look at other similar tools in `ALTERNATIVES.md`. -Electrically ------------- +## Electrically The below assumes you are connecting an STM32F10x. For other chips, the serial pins and/or the BOOT0 / BOOT1 values @@ -157,8 +153,7 @@ When given a choice, set BOOT0 manually high and drive reset through the serial adepter (it needs to toggle, whereas BOOT0 does not). -Not currently supported ------------------------ +## Not currently supported * Command-line argument for readout protection * Command-line argument for write protection/unprotection @@ -167,8 +162,8 @@ Not currently supported * Other bootloader protocols (e.g. I2C, HEX -> implemented in stm32flash) -Future work ------------ +## Future work + * Use proper logging instead of print statements * Try Azure pipelines for CI * Support PyPy, PyPy3 From d047a36d6463f08a18fbd87cf2b73e2ebc5faa68 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 28 Feb 2022 21:30:04 +0100 Subject: [PATCH 215/369] docs: Improve comments --- stm32loader/bootloader.py | 7 +++++-- stm32loader/uart.py | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index ad876ef..5a0d586 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -472,6 +472,8 @@ def get_flash_size_and_uid(self): if uid_address is None: return None, None + # Start address is the start of the 256-byte block + # containing uid_address and flash_size_address. data_start_address = uid_address & 0xFFFFFF00 flash_size_lsb_address = flash_size_address - data_start_address uid_lsb_address = uid_address - data_start_address @@ -573,12 +575,13 @@ def erase_memory(self, pages=None): """ Erase flash memory at the given pages. - Set pages to None to erase the full memory. + Set pages to None to erase the full memory ('global erase'). + :param iterable pages: Iterable of integer page addresses, zero-based. Set to None to trigger global mass erase. """ if self.extended_erase: - # use erase with two-byte addresses + # Use erase with two-byte addresses. self.extended_erase_memory(pages) return diff --git a/stm32loader/uart.py b/stm32loader/uart.py index 49d07dc..55de005 100644 --- a/stm32loader/uart.py +++ b/stm32loader/uart.py @@ -78,6 +78,7 @@ def connect(self): ) def disconnect(self): + """Close the connection.""" if not self.serial_connection: return From 53bc0ee552f1b2c16ffed66184b8e2791b269a63 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 28 Feb 2022 21:31:10 +0100 Subject: [PATCH 216/369] feat: Raise proper for L0 erase of page >= 256 --- stm32loader/bootloader.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index 5a0d586..adf3eb0 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -591,7 +591,9 @@ def erase_memory(self, pages=None): # Special case: L0 erase should do each page separately. flash_size, _uid = self.get_flash_size_and_uid() page_count = (flash_size * 1024) // self.flash_page_size - pages = range(page_count - 1) + if page_count > 255: + raise PageIndexError("Can not erase more than 255 pages for L0 family.") + pages = range(page_count) if pages: # page erase, see ST AN3155 if len(pages) > 255: @@ -599,7 +601,7 @@ def erase_memory(self, pages=None): "Can not erase more than 255 pages at once.\n" "Set pages to None to do global erase or supply fewer pages." ) - page_count = (len(pages) - 1) & 0xFF + page_count = len(pages) - 1 page_numbers = bytearray(pages) checksum = reduce(operator.xor, page_numbers, page_count) self.write(page_count, page_numbers, checksum) From 2a1f954e849823f038cd88c385e9437b98530914 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 28 Feb 2022 21:33:15 +0100 Subject: [PATCH 217/369] feat: Add unit tests for tricky erase and flash_size --- tests/unit/test_bootloader.py | 48 ++++++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/tests/unit/test_bootloader.py b/tests/unit/test_bootloader.py index 1d99496..b9e472c 100644 --- a/tests/unit/test_bootloader.py +++ b/tests/unit/test_bootloader.py @@ -2,15 +2,11 @@ import pytest +from unittest.mock import MagicMock + from stm32loader import bootloader as Stm32 from stm32loader.bootloader import Stm32Bootloader -try: - from unittest.mock import MagicMock -except ImportError: - # Python version <= 3.2 - from mock import MagicMock - # pylint: disable=missing-docstring, redefined-outer-name @@ -148,6 +144,21 @@ def test_erase_memory_with_page_count_higher_than_255_raises_page_index_error(bo bootloader.erase_memory([1] * 256) +def test_erase_memory_family_l0_without_pages_erases_individual_pages(connection, write): + bootloader = Stm32Bootloader(connection, device_family="L0") + bootloader.command = MagicMock() + bootloader.get_flash_size_and_uid = MagicMock() + bootloader.get_flash_size_and_uid.return_value = (16, 0x01) + bootloader.erase_memory() + + # Page count - 1. + assert write.written_data[0] == 127 + # Pages. + assert write.written_data[1:3] == b'\x00\x01' + # Length: command + byte count + page-addresses + CRC + assert len(write.written_data) == 130 + + def test_extended_erase_memory_without_pages_sends_global_mass_erase(bootloader, write): bootloader.extended_erase_memory() assert write.data_was_written(b'\xff\xff\x00') @@ -188,7 +199,7 @@ def test_verify_data_with_non_identical_data_raises_verify_error_complaining_abo @pytest.mark.parametrize( - "family", ["F1", "F4", "F7"], + "family", ["F1", "F3", "F7"], ) def test_get_uid_for_known_family_reads_at_correct_address(connection, family): bootloader = Stm32Bootloader(connection, device_family=family) @@ -208,6 +219,29 @@ def test_get_uid_for_unknown_family_returns_uid_address_unknown(connection): assert bootloader.UID_ADDRESS_UNKNOWN == bootloader.get_uid() +@pytest.mark.parametrize( + "family", ["F4", "L0"], +) +def test_get_flash_size_and_uid_for_exception_families_returns_size_and_uid(connection, family): + bootloader = Stm32Bootloader(connection, device_family=family) + bootloader.read_memory = MagicMock() + + memory_block = bytearray([0] * 256) + + # Set up the 'UID' value (12 bytes) + # and flash_size value (2 bytes). + uid_address = bootloader.UID_ADDRESS[family] & 0xFF + memory_block[uid_address: uid_address + 12] = b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c' + flash_size_address = bootloader.FLASH_SIZE_ADDRESS[family] & 0xFF + memory_block[flash_size_address: flash_size_address + 2] = b'\x01\x02' + bootloader.read_memory.return_value = memory_block + + flash_size, uid = bootloader.get_flash_size_and_uid() + + assert flash_size == 0x0201 + assert uid == b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c' + + @pytest.mark.parametrize( "uid_string", [ From 6776c2938fb8f160e08345bf89d6a2eccdfcd3cf Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 2 Mar 2022 15:43:51 +0100 Subject: [PATCH 218/369] feat: Use Tox and Github Actions for CI --- .github/workflows/ci.yaml | 37 +++++++++++++++++++++++++++++++++++++ tox.ini | 25 +++++++++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 .github/workflows/ci.yaml create mode 100644 tox.ini diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..b70e337 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,37 @@ +name: CI + +on: + - push + - pull_request + +defaults: + run: + shell: bash + +jobs: + tox: + strategy: + fail-fast: true + matrix: + os: + - ubuntu-latest + - windows-latest + python-version: + - "3.6" + - "3.7" + - "3.8" + - "3.9" + - "3.10" + - "pypy-3.7" + - "pypy-3.8" + - "pypy-3.9" + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install test dependencies + run: pip install tox tox-gh-actions pyserial pytest + - name: Run setup and tests as defined in tox.ini + run: tox diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..8d86ae6 --- /dev/null +++ b/tox.ini @@ -0,0 +1,25 @@ +[tox] +envlist = py36, py37, py38, py39, py310, pypy37 + +[testenv] +passenv = HOME +deps= + pytest + pyserial +commands= + pytest -r a [] tests + +[pytest] +minversion= 2.0 +norecursedirs= .git .github .tox .nox build dist tmp* tests/integration + +[gh-actions] +python = + 3.6: py36 + 3.7: py37 + 3.8: py38 + 3.9: py39 + 3.10: py310 + pypy-3.7: pypy37 + pypy-3.8: pypy38 + pypy-3.9: pypy39 From fa7d36ee77ca1ff474402f4cdb294869b41274f2 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Thu, 3 Mar 2022 09:19:36 +0100 Subject: [PATCH 219/369] Release: bump version number from v0.5.2-dev to v0.6.0-dev --- .bumpversion.cfg | 3 +-- setup.py | 2 +- stm32loader/__init__.py | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 0bfcb44..95e3eb2 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.5.2-dev +current_version = 0.6.0-dev commit = True tag = False message = Release: bump version number from v{current_version} to v{new_version} @@ -25,4 +25,3 @@ replace = __version_info__ = {new_version} [bumpversion:file:setup.py] search = VERSION = "{current_version}" replace = VERSION = "{new_version}" - diff --git a/setup.py b/setup.py index efa085a..c81539f 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ # Package meta-data. NAME = 'stm32loader' -VERSION = "0.5.2-dev" +VERSION = "0.6.0-dev" DESCRIPTION = 'Flash firmware to STM32 microcontrollers using Python.' URL = 'https://github.com/florisla/stm32loader' EMAIL = 'florisla@gmail.com' diff --git a/stm32loader/__init__.py b/stm32loader/__init__.py index 9429002..c202307 100644 --- a/stm32loader/__init__.py +++ b/stm32loader/__init__.py @@ -1,4 +1,4 @@ """Flash firmware to STM32 microcontrollers over a serial connection.""" -__version_info__ = (0, 5, 2, "dev") +__version_info__ = (0, 6, 0, "dev") __version__ = "-".join(str(part) for part in __version_info__).replace("-", ".", 2) From 30f6141845228a3e3a438cf185799c1c63e50cbc Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Thu, 3 Mar 2022 09:19:59 +0100 Subject: [PATCH 220/369] Release: bump version number from v0.6.0-dev to v0.6.0 --- .bumpversion.cfg | 2 +- setup.py | 2 +- stm32loader/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 95e3eb2..30b897a 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.6.0-dev +current_version = 0.6.0 commit = True tag = False message = Release: bump version number from v{current_version} to v{new_version} diff --git a/setup.py b/setup.py index c81539f..5cb84fc 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ # Package meta-data. NAME = 'stm32loader' -VERSION = "0.6.0-dev" +VERSION = "0.6.0" DESCRIPTION = 'Flash firmware to STM32 microcontrollers using Python.' URL = 'https://github.com/florisla/stm32loader' EMAIL = 'florisla@gmail.com' diff --git a/stm32loader/__init__.py b/stm32loader/__init__.py index c202307..c26ea20 100644 --- a/stm32loader/__init__.py +++ b/stm32loader/__init__.py @@ -1,4 +1,4 @@ """Flash firmware to STM32 microcontrollers over a serial connection.""" -__version_info__ = (0, 6, 0, "dev") +__version_info__ = (0, 6, 0) __version__ = "-".join(str(part) for part in __version_info__).replace("-", ".", 2) From 8e22a86cdb71b1d87337bfed42fc835dc19864da Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 8 Mar 2022 10:59:50 +0100 Subject: [PATCH 221/369] feat: Add GitHub actions file to run lint --- .github/workflows/lint.yaml | 61 ++++++++++++++++++++++++ .github/workflows/{ci.yaml => test.yaml} | 2 +- 2 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/lint.yaml rename .github/workflows/{ci.yaml => test.yaml} (98%) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 0000000..82a0f6a --- /dev/null +++ b/.github/workflows/lint.yaml @@ -0,0 +1,61 @@ +name: Lint + +on: + - push + - pull_request + +defaults: + run: + shell: bash + +jobs: + black: + strategy: + matrix: + os: + - ubuntu-latest + python-version: + - "3.10" + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + - uses: psf/black@stable + with: + options: "--check --verbose" + src: "./stm32loader" + flake8: + strategy: + fail-fast: true + matrix: + os: + - ubuntu-latest + python-version: + - "3.10" + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install lint dependencies + run: pip install flake8 pyserial + - name: Run flake8 + run: flake8 stm32loader + pylint: + strategy: + fail-fast: true + matrix: + os: + - ubuntu-latest + python-version: + - "3.10" + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install lint dependencies + run: pip install pylint pyserial + - name: Run pylint + run: pylint stm32loader diff --git a/.github/workflows/ci.yaml b/.github/workflows/test.yaml similarity index 98% rename from .github/workflows/ci.yaml rename to .github/workflows/test.yaml index b70e337..a115516 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/test.yaml @@ -1,4 +1,4 @@ -name: CI +name: Test on: - push From 9e126b6e279e3645a365ab646b730380e680db8c Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 8 Mar 2022 11:57:15 +0100 Subject: [PATCH 222/369] clean: Always use 'stm32loader' as the program name --- stm32loader/main.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/stm32loader/main.py b/stm32loader/main.py index e6ab455..fa5aa33 100644 --- a/stm32loader/main.py +++ b/stm32loader/main.py @@ -82,6 +82,8 @@ def debug(self, level, message): def parse_arguments(self, arguments): """Parse the list of command-line arguments.""" parser = argparse.ArgumentParser( + prog="stm32loader", + description="Flash firmware to STM32 microcontrollers.", epilog="\n".join( [ "examples:", From 730db6bf379ad2d2a1ae32d77545631871260a2d Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 8 Mar 2022 11:58:22 +0100 Subject: [PATCH 223/369] docs: Auto-update the help text in README using cog --- README.md | 33 ++++++++++++++++++++++----------- setup.py | 2 +- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index b0625db..86a8b9a 100644 --- a/README.md +++ b/README.md @@ -25,19 +25,32 @@ To install the latest development version: ## Usage + ``` -usage: stm32loader [-h] [-e] [-u] [-w] [-v] [-r] [-l LENGTH] -p PORT [-b BAUD] - [-a ADDRESS] [-g ADDRESS] [-f FAMILY] [-V] [-q] [-s] [-R] - [-B] [-n] [-P {even,none}] [--version] - [FILE.BIN] +usage: stm32loader [-h] [-e] [-u] [-w] [-v] [-r] [-l LENGTH] -p PORT [-b BAUD] [-a ADDRESS] [-g ADDRESS] [-f FAMILY] [-V] [-q] [-s] [-R] [-B] [-n] [-P {even,none}] [--version] [FILE.BIN] + +Flash firmware to STM32 microcontrollers. positional arguments: FILE.BIN file to read from or store to flash optional arguments: -h, --help show this help message and exit - -e, --erase erase (note: this is required on previously written - memory) + -e, --erase erase (note: this is required on previously written memory) -u, --unprotect unprotect in case erase fails -w, --write write file content to flash -v, --verify verify flash content versus local file (recommended) @@ -51,8 +64,7 @@ optional arguments: -g ADDRESS, --go-address ADDRESS start executing from address (0x08000000, usually) -f FAMILY, --family FAMILY - device family to read out device UID and flash size; - e.g F1 for STM32F1xx (default: $STM32LOADER_FAMILY) + device family to read out device UID and flash size; e.g F1 for STM32F1xx (default: $STM32LOADER_FAMILY) -V, --verbose verbose mode -q, --quiet quiet mode -s, --swap-rts-dtr swap RTS and DTR: use RTS for reset and DTR for boot0 @@ -62,15 +74,14 @@ optional arguments: make boot0 active low -n, --no-progress don't show progress bar -P {even,none}, --parity {even,none} - parity: "even" for STM32, "none" for BlueNRG (default: - even) + parity: "even" for STM32, "none" for BlueNRG (default: even) --version show program's version number and exit examples: stm32loader -p COM7 -f F1 stm32loader -e -w -v example/main.bin ``` - + ## Command-line example diff --git a/setup.py b/setup.py index 5cb84fc..15bc531 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ ] EXTRAS = { - "dev": ['setuptools', 'wheel', 'twine', 'pylint', 'flake8', 'flake8-isort', 'black', 'bump2version', 'nox'], + "dev": ['setuptools', 'wheel', 'twine', 'pylint', 'flake8', 'flake8-isort', 'black', 'bump2version', 'nox', 'cogapp'], } HERE = os.path.abspath(os.path.dirname(__file__)) From 0363f2eceadfe716e98434efba5d5c48527ec8f2 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 8 Mar 2022 11:58:59 +0100 Subject: [PATCH 224/369] docs: Describe recent changes in README and CHANGELOG --- CHANGELOG.md | 103 +++++++++++++++++++++++++++++---------------------- README.md | 27 +++++++------- 2 files changed, 72 insertions(+), 58 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bde8736..87e8bc8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,56 +1,75 @@ -v0.5.1 -====== -* #25 Fix bug: Mass memory erase by byq77. -* #28 Add support for STM32L4 by rdaforno. -* #29 Add support for more STM32F0 ids by stawiski . -* #30 Add support for STM32F3 by float32. -* #32 Add support for STM32G0x1 by AlexKlimaj. -* #33 More robust bootloader activation by hiviah. -* #35 Support Python 3.8 -* #20 Add a 'read flash' example to README -* #34 Add --version argument - - -v0.5.0 -====== -* #17 Add support for STM32F03xx4/6 by omerk. +# Changelog +What changed in which version. + + +## [0.6.0] - 2022-03-FIXME + +### Added +* `#59` Continuous Integration: start running tests and linters on GitHub Actions. +* `#42` `#43` Find flash size for non-standard MCUs (F4, L0). +* Support STM32H76xxx/77xxx series. +* Packaging: auto-generate the help output using `cog`. + +### Changed +* `#46` `#48` Flush the UART read buffer after MCU reset. +* Use argparse instead of optparse. +* Drop support for Python 2, 3.4, 3.5. + +### Fixed +* `#44` Support flash page count higher than 255. + +### Documented +* `#13` Describe how to extend Stm32Loader. +* `#52` Describe alternative was to execute the module. +* `#58` Add a list of similar tools. + + +## [0.5.1] - 2019-12-31 +* `#25` Fix bug: Mass memory erase by byq77. +* `#28` Add support for STM32L4 by rdaforno. +* `#29` Add support for more STM32F0 ids by stawiski . +* `#30` Add support for STM32F3 by float32. +* `#32` Add support for STM32G0x1 by AlexKlimaj. +* `#33` More robust bootloader activation by hiviah. +* `#35` Support Python 3.8 +* `#20` Add a 'read flash' example to README +* `#34` Add --version argument + + +## [0.5.0] - 2019-05-02 +* `#17` Add support for STM32F03xx4/6 by omerk. * Drop support for Python 3.2 and 3.3. -v0.4.0 -====== -* #8: Add support for STM32F7 mcus. By sam-bristow. -* #9: Support data writes smaller than 256 bytes. By NINI1988. -* #10: Make stm32loader useful as a library. -* #4: Bring back support for progress bar. -* #12: Allow to supply the serial port as an environment variable. -* #11: Support paged erase in extended (two-byte addressing) erase mode. +## [0.4.0] - 2019-04-19 +* `#8`: Add support for STM32F7 mcus. By sam-bristow. +* `#9`: Support data writes smaller than 256 bytes. By NINI1988. +* `#10`: Make stm32loader useful as a library. +* `#4`: Bring back support for progress bar. +* `#12`: Allow to supply the serial port as an environment variable. +* `#11`: Support paged erase in extended (two-byte addressing) erase mode. Note: this is not yet tested on hardware. * Start using code linting and unit tests. * Start using Continuous Integration (Travis CI). -v0.3.3 -====== +## [0.3.3] - 2018-08-08 * Bugfix: write data, not [data]. By Atokulus. -v0.3.2 -====== +## [0.3.2] - 2018-07-31 * Publish on Python Package Index. * Make stm32loader executable as a module. * Expose stm32loader as a console script (stm32loader.exe on Windows). -v0.3.1 -====== +## [0.3.1] -- 2018-07-31 * Make stm32loader installable and importable as a package. -* Make write_memory faster (by Atokulus, see #1). +* Make write_memory faster (by Atokulus, see `#1`). -v0.3.0 -======= +## [0.3.0] - 2018-04-27 * Add version number. * Add this changelog. * Improve documentation. @@ -63,35 +82,29 @@ v0.3.0 * Refactor __main__ functionality into methods. -2018-05 -======= +## 2018-05 * Make RTS/DTR (boot0/reset) configurable (polarity, swap). -2018-04 -======= +## 2018-04 * Restore Python 2 compatibility. -2018-03 -======= +## 2018-03 * Add support for Python 3. * Remove Psyco and progressbar support. * Fix checksum calculation bug for paged erase. -2014-04 -======= +## 2014-04 * Add `-g
` (GO command). * Add known chip IDs. * Implement extended erase for STM32 F2/F4. -2013-10 -======= +## 2013-10 * Add Windows compatibility. -2009-04 -======= +## 2009-04 * Add GPL license. diff --git a/README.md b/README.md index 86a8b9a..872962e 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,18 @@ # STM32Loader [![PyPI package](https://badge.fury.io/py/stm32loader.svg)](https://badge.fury.io/py/stm32loader) -[![Build Status](https://travis-ci.org/florisla/stm32loader.svg?branch=master)](https://travis-ci.org/florisla/stm32loader) +[![GitHub Actions](https://img.shields.io/github/workflow/status/florisla/stm32loader/Test?label=tests)](https://github.com/florisla/stm32loader/actions/workflows/test.yaml) +[![GitHub Actions](https://img.shields.io/github/workflow/status/florisla/stm32loader/Lint?label=lint)](https://github.com/florisla/stm32loader/actions/workflows/lint.yaml) [![License](https://img.shields.io/pypi/l/stm32loader.svg)](https://pypi.org/project/stm32loader/) [![Downloads](https://pepy.tech/badge/stm32loader)](https://pepy.tech/project/stm32loader) -Python script to upload or download firmware to / from +Python module to upload or download firmware to / from ST Microelectronics STM32 microcontrollers over UART. Also supports ST BlueNRG devices, and the SweetPeas bootloader for Wiznet W7500. -Compatible with Python version 3.6 to 3.10. +Compatible with Python version 3.6 to 3.10 and PyPy 3.7 to 3.9. ## Installation @@ -115,7 +116,8 @@ stm32loader -r -p /dev/cu.usbserial-A5XK3RJT -f F1 -l 0x10000 -a 0x08000000 dump Original Version by Ivan A-R (tuxotronic.org). Contributions by Domen Puncer, James Snyder, Floris Lambrechts, Atokulus, sam-bristow, NINI1988, Omer Kilic, Szymon Szantula, rdaforno, -Mikolaj Stawiski, Tyler Coy, Alex Klimaj, Ondrej Mikle. +Mikolaj Stawiski, Tyler Coy, Alex Klimaj, Ondrej Mikle, denniszollo, +emilzay, michsens, blueskull, Mattia Maldini. Inspiration for features from: @@ -166,16 +168,15 @@ adepter (it needs to toggle, whereas BOOT0 does not). ## Not currently supported -* Command-line argument for readout protection -* Command-line argument for write protection/unprotection -* STM8 devices (ST UM0560) -* Paged flash erase for devices with page size <> 1 KiB -* Other bootloader protocols (e.g. I2C, HEX -> implemented in stm32flash) +* Command-line argument for readout protection. +* Command-line argument for write protection/unprotection. +* STM8 devices (ST UM0560). +* Paged flash erase for devices with page size <> 1 KiB. +* Other bootloader protocols (e.g. I2C, HEX -> implemented in `stm32flash`). ## Future work -* Use proper logging instead of print statements -* Try Azure pipelines for CI -* Support PyPy, PyPy3 -* Start using intenum for commands and replies +* Use f-strings. +* Use proper logging instead of print statements. +* Start using intenum for commands and replies. From c766e63af65a846559e9cb00271cf1efb9f49a19 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 8 Mar 2022 11:59:13 +0100 Subject: [PATCH 225/369] clean: Ignore .tox folder --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 056325d..3493742 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ build/ dist/ *.egg-info/ +.tox/ __pycache__/ *.py[cod] From 647e6063c0f819eae7679bb1536686ccda81d994 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 8 Mar 2022 12:03:06 +0100 Subject: [PATCH 226/369] clean: Rename job 'tox' to 'pytest' --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index a115516..f2d66da 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -9,7 +9,7 @@ defaults: shell: bash jobs: - tox: + pytest: strategy: fail-fast: true matrix: From 6844fa7c5e9553cbbd7e1fb5d7041f07c9f9bb51 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 21 Jun 2022 10:38:44 +0200 Subject: [PATCH 227/369] docs(cosmetic): Capitalize help text --- README.md | 66 ++++++++++++++++++++++----------------------- stm32loader/main.py | 40 +++++++++++++-------------- 2 files changed, 53 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index 872962e..28aff92 100644 --- a/README.md +++ b/README.md @@ -47,35 +47,35 @@ usage: stm32loader [-h] [-e] [-u] [-w] [-v] [-r] [-l LENGTH] -p PORT [-b BAUD] [ Flash firmware to STM32 microcontrollers. positional arguments: - FILE.BIN file to read from or store to flash + FILE.BIN File to read from or store to flash. optional arguments: -h, --help show this help message and exit - -e, --erase erase (note: this is required on previously written memory) - -u, --unprotect unprotect in case erase fails - -w, --write write file content to flash - -v, --verify verify flash content versus local file (recommended) - -r, --read read from flash and store in local file + -e, --erase Erase (note: this is required on previously written memory). + -u, --unprotect Unprotect in case erase fails. + -w, --write Write file content to flash. + -v, --verify Verify flash content versus local file (recommended). + -r, --read Read from flash and store in local file. -l LENGTH, --length LENGTH - length of read - -p PORT, --port PORT serial port (default: $STM32LOADER_SERIAL_PORT) - -b BAUD, --baud BAUD baudrate (default: 115200) + Length of read. + -p PORT, --port PORT Serial port (default: $STM32LOADER_SERIAL_PORT). + -b BAUD, --baud BAUD Baudrate. (default: 115200) -a ADDRESS, --address ADDRESS - target address (default: 134217728) + Target address. (default: 134217728) -g ADDRESS, --go-address ADDRESS - start executing from address (0x08000000, usually) + Start executing from address (0x08000000, usually). -f FAMILY, --family FAMILY - device family to read out device UID and flash size; e.g F1 for STM32F1xx (default: $STM32LOADER_FAMILY) - -V, --verbose verbose mode - -q, --quiet quiet mode - -s, --swap-rts-dtr swap RTS and DTR: use RTS for reset and DTR for boot0 + Device family to read out device UID and flash size; e.g F1 for STM32F1xx (default: $STM32LOADER_FAMILY). + -V, --verbose Verbose mode. + -q, --quiet Quiet mode. + -s, --swap-rts-dtr Swap RTS and DTR: use RTS for reset and DTR for boot0. -R, --reset-active-high - make reset active high + Make RESET active high. -B, --boot0-active-low - make boot0 active low - -n, --no-progress don't show progress bar + Make BOOT0 active low. + -n, --no-progress Don't show progress bar. -P {even,none}, --parity {even,none} - parity: "even" for STM32, "none" for BlueNRG (default: even) + Parity: "even" for STM32, "none" for BlueNRG. (default: even) --version show program's version number and exit examples: @@ -106,9 +106,9 @@ stm32loader -r -p /dev/cu.usbserial-A5XK3RJT -f F1 -l 0x10000 -a 0x08000000 dump ## Reference documents -* ST AN2606: STM32 microcontroller system memory boot mode -* ST AN3155: USART protocol used in the STM32 bootloader -* ST AN4872: BlueNRG-1 and BlueNRG-2 UART bootloader protocol +* ST `AN2606`: STM32 microcontroller system memory boot mode +* ST `AN3155`: USART protocol used in the STM32 bootloader +* ST `AN4872`: BlueNRG-1 and BlueNRG-2 UART bootloader protocol ## Acknowledgement @@ -151,26 +151,26 @@ may differ. Make the following connections: -- Serial adapter GND to MCU GND. +- Serial adapter `GND` to MCU `GND`. - Serial adapter power to MCU power or vice versa (either 3.3 or 5 Volt). - Note if you're using 5 Volt signaling or 3V3 on the serial adapter. -- Serial TX to MCU RX (PA10). -- Serial RX to MCU TX (PA9). -- Serial DTR to MCU RESET. -- Serial RTS to MCU BOOT0 (or BOOT0 to 3.3V). -- MCU BOOT1 to GND. +- Serial `TX` to MCU `RX` (`PA10`). +- Serial `RX` to MCU `TX` (`PA9`). +- Serial `DTR` to MCU `RESET`. +- Serial `RTS` to MCU `BOOT0` (or `BOOT0` to 3.3V). +- MCU `BOOT1` to `GND`. -If either RTS or DTR are not available on your serial adapter, you'll have to +If either `RTS` or `DTR` are not available on your serial adapter, you'll have to manually push buttons or work with jumpers. -When given a choice, set BOOT0 manually high and drive reset through the serial -adepter (it needs to toggle, whereas BOOT0 does not). +When given a choice, set `BOOT0` manually high and drive `RESET` through the serial +adapter (it needs to toggle, whereas `BOOT0` does not). ## Not currently supported * Command-line argument for readout protection. * Command-line argument for write protection/unprotection. -* STM8 devices (ST UM0560). +* STM8 devices (ST `UM0560`). * Paged flash erase for devices with page size <> 1 KiB. * Other bootloader protocols (e.g. I2C, HEX -> implemented in `stm32flash`). @@ -179,4 +179,4 @@ adepter (it needs to toggle, whereas BOOT0 does not). * Use f-strings. * Use proper logging instead of print statements. -* Start using intenum for commands and replies. +* Start using `IntEnum` for commands and replies. diff --git a/stm32loader/main.py b/stm32loader/main.py index fa5aa33..69e1601 100644 --- a/stm32loader/main.py +++ b/stm32loader/main.py @@ -99,37 +99,37 @@ def parse_arguments(self, arguments): metavar="FILE.BIN", type=str, nargs="?", - help="file to read from or store to flash", + help="File to read from or store to flash.", ) parser.add_argument( "-e", "--erase", action="store_true", - help="erase (note: this is required on previously written memory)", + help="Erase (note: this is required on previously written memory).", ) parser.add_argument( - "-u", "--unprotect", action="store_true", help="unprotect in case erase fails" + "-u", "--unprotect", action="store_true", help="Unprotect in case erase fails." ) parser.add_argument( - "-w", "--write", action="store_true", help="write file content to flash" + "-w", "--write", action="store_true", help="Write file content to flash." ) parser.add_argument( "-v", "--verify", action="store_true", - help="verify flash content versus local file (recommended)", + help="Verify flash content versus local file (recommended).", ) parser.add_argument( - "-r", "--read", action="store_true", help="read from flash and store in local file" + "-r", "--read", action="store_true", help="Read from flash and store in local file." ) length_arg = parser.add_argument( - "-l", "--length", action="store", type=int, help="length of read" + "-l", "--length", action="store", type=int, help="Length of read." ) default_port = os.environ.get("STM32LOADER_SERIAL_PORT") @@ -140,16 +140,16 @@ def parse_arguments(self, arguments): type=str, # morally required=True default=default_port, help=( - "serial port" + ("" if default_port else " (default: $STM32LOADER_SERIAL_PORT)") + "Serial port" + ("." if default_port else " (default: $STM32LOADER_SERIAL_PORT).") ), ) parser.add_argument( - "-b", "--baud", action="store", type=int, default=115200, help="baudrate" + "-b", "--baud", action="store", type=int, default=115200, help="Baudrate." ) address_arg = parser.add_argument( - "-a", "--address", action="store", type=int, default=0x08000000, help="target address" + "-a", "--address", action="store", type=int, default=0x08000000, help="Target address." ) parser.add_argument( @@ -158,7 +158,7 @@ def parse_arguments(self, arguments): action="store", type=int, metavar="ADDRESS", - help="start executing from address (0x08000000, usually)", + help="Start executing from address (0x08000000, usually).", ) default_family = os.environ.get("STM32LOADER_FAMILY") @@ -169,9 +169,9 @@ def parse_arguments(self, arguments): type=str, default=default_family, help=( - "device family to read out device UID and flash size; " + "Device family to read out device UID and flash size; " "e.g F1 for STM32F1xx" - + ("" if default_family else " (default: $STM32LOADER_FAMILY)") + + ("." if default_family else " (default: $STM32LOADER_FAMILY).") ), ) @@ -182,30 +182,30 @@ def parse_arguments(self, arguments): action="store_const", const=10, default=DEFAULT_VERBOSITY, - help="verbose mode", + help="Verbose mode.", ) parser.add_argument( - "-q", "--quiet", dest="verbosity", action="store_const", const=0, help="quiet mode" + "-q", "--quiet", dest="verbosity", action="store_const", const=0, help="Quiet mode." ) parser.add_argument( "-s", "--swap-rts-dtr", action="store_true", - help="swap RTS and DTR: use RTS for reset and DTR for boot0", + help="Swap RTS and DTR: use RTS for reset and DTR for boot0.", ) parser.add_argument( - "-R", "--reset-active-high", action="store_true", help="make reset active high" + "-R", "--reset-active-high", action="store_true", help="Make RESET active high." ) parser.add_argument( - "-B", "--boot0-active-low", action="store_true", help="make boot0 active low" + "-B", "--boot0-active-low", action="store_true", help="Make BOOT0 active low." ) parser.add_argument( - "-n", "--no-progress", action="store_true", help="don't show progress bar" + "-n", "--no-progress", action="store_true", help="Don't show progress bar." ) parser.add_argument( @@ -215,7 +215,7 @@ def parse_arguments(self, arguments): type=str, default="even", choices=self.PARITY.keys(), - help='parity: "even" for STM32, "none" for BlueNRG', + help='Parity: "even" for STM32, "none" for BlueNRG.', ) parser.add_argument("--version", action="version", version=__version__) From 1e1a65007e7a4db2fb252c5ed133a808127d4b5e Mon Sep 17 00:00:00 2001 From: tosmaz Date: Tue, 3 Jan 2023 01:16:52 +0100 Subject: [PATCH 228/369] stm32l0 fix --- stm32loader/bootloader.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index adf3eb0..6fb09d9 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -71,6 +71,7 @@ # Cortex-M0 MCU with hardware TCP/IP and MAC # (SweetPeas custom bootloader) 0x801: "Wiznet W7500", + 0x457: "STM32L01xxx/02xxx" } @@ -623,6 +624,10 @@ def extended_erase_memory(self, pages=None): :param iterable pages: Iterable of integer page addresses, zero-based. Set to None to trigger global mass erase. """ + if not pages and self.device_family in ('L0', ): + flash_size, _uid = self.get_flash_size_and_uid() + pages = list(range(0, (flash_size*1024) // self.flash_page_size)) + self.command(self.Command.EXTENDED_ERASE, "Extended erase memory") if pages: # page erase, see ST AN3155 From 9f508d65d3ccb1e1fef366459dc01838e160f366 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 9 Oct 2023 11:51:40 +0200 Subject: [PATCH 229/369] doc: Add comment about special-case L0 mass erase --- stm32loader/bootloader.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index 6fb09d9..3a3eb65 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -68,10 +68,10 @@ 0x445: "STM32F070x6", 0x448: "STM32F070xB", 0x442: "STM32F030xC", + 0x457: "STM32L01xxx/02xxx" # Cortex-M0 MCU with hardware TCP/IP and MAC # (SweetPeas custom bootloader) 0x801: "Wiznet W7500", - 0x457: "STM32L01xxx/02xxx" } @@ -625,6 +625,8 @@ def extended_erase_memory(self, pages=None): Set to None to trigger global mass erase. """ if not pages and self.device_family in ('L0', ): + # L0 devices do not support mass erase. + # Instead, erase all pages individually. flash_size, _uid = self.get_flash_size_and_uid() pages = list(range(0, (flash_size*1024) // self.flash_page_size)) From 7a57203fce7042714b52cdeb435a551de4a93c6f Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 9 Oct 2023 11:57:37 +0200 Subject: [PATCH 230/369] Int fix, and family pass fix to Stm32Bootloader Done as by @etrommer --- stm32loader/main.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/stm32loader/main.py b/stm32loader/main.py index 69e1601..50015e3 100644 --- a/stm32loader/main.py +++ b/stm32loader/main.py @@ -81,6 +81,11 @@ def debug(self, level, message): def parse_arguments(self, arguments): """Parse the list of command-line arguments.""" + + def auto_int(x): + """Convert to int with automatic base detection.""" + return int(x, 0) + parser = argparse.ArgumentParser( prog="stm32loader", description="Flash firmware to STM32 microcontrollers.", @@ -129,7 +134,7 @@ def parse_arguments(self, arguments): ) length_arg = parser.add_argument( - "-l", "--length", action="store", type=int, help="Length of read." + "-l", "--length", action="store", type=auto_int, help="Length of read." ) default_port = os.environ.get("STM32LOADER_SERIAL_PORT") @@ -149,14 +154,14 @@ def parse_arguments(self, arguments): ) address_arg = parser.add_argument( - "-a", "--address", action="store", type=int, default=0x08000000, help="Target address." + "-a", "--address", action="store", type=auto_int, default=0x08000000, help="Target address." ) parser.add_argument( "-g", "--go-address", action="store", - type=int, + type=auto_int, metavar="ADDRESS", help="Start executing from address (0x08000000, usually).", ) From b890db23b245087dc7ad6fab6db35594a5f85c5b Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 9 Oct 2023 11:57:44 +0200 Subject: [PATCH 231/369] doc: Add comment --- stm32loader/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/stm32loader/main.py b/stm32loader/main.py index 50015e3..30427d9 100644 --- a/stm32loader/main.py +++ b/stm32loader/main.py @@ -84,6 +84,7 @@ def parse_arguments(self, arguments): def auto_int(x): """Convert to int with automatic base detection.""" + # This supports 0x10 == 16 and 10 == 10 return int(x, 0) parser = argparse.ArgumentParser( From 0af10903ba1e9943eac10a0346e7ab2b6cee87db Mon Sep 17 00:00:00 2001 From: tosmaz Date: Tue, 3 Jan 2023 01:38:26 +0100 Subject: [PATCH 232/369] fix: Pass device family to Stm32Bootloader Done as by @etrommer --- stm32loader/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stm32loader/main.py b/stm32loader/main.py index 30427d9..c1d9bd8 100644 --- a/stm32loader/main.py +++ b/stm32loader/main.py @@ -296,7 +296,7 @@ def connect(self): show_progress = self._get_progress_bar(self.configuration.no_progress) self.stm32 = bootloader.Stm32Bootloader( - serial_connection, verbosity=self.configuration.verbosity, show_progress=show_progress + serial_connection, verbosity=self.configuration.verbosity, show_progress=show_progress, device_family=self.configuration.family ) try: From 43d0a00878498fd76963d2b0f1210174708609f0 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 9 Oct 2023 12:09:47 +0200 Subject: [PATCH 233/369] feat: Add support for STM32WL As done by @etrommer --- stm32loader/bootloader.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index 3a3eb65..31bf1c4 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -68,7 +68,8 @@ 0x445: "STM32F070x6", 0x448: "STM32F070xB", 0x442: "STM32F030xC", - 0x457: "STM32L01xxx/02xxx" + 0x457: "STM32L01xxx/02xxx", + 0x497: "STM32WLE5xx/WL55xx", # Cortex-M0 MCU with hardware TCP/IP and MAC # (SweetPeas custom bootloader) 0x801: "Wiznet W7500", @@ -226,6 +227,8 @@ class Reply: "L0": 0x1FF80050, # ST RM0444 section 38.1 Unique device ID register "G0": 0x1FFF7590, + # ST RM0453 section 39.1.1 Unique device ID register + "WL": 0x1FFF7590, } UID_SWAP = [[1, 0], [3, 2], [7, 6, 5, 4], [11, 10, 9, 8]] @@ -261,6 +264,8 @@ class Reply: "L0": 0x1FF8007C, # ST RM0444 section 38.2 Flash memory size data register "G0": 0x1FFF75E0, + # ST RM0453 section 39.1.2 Flash size data register + "WL": 0x1FFF75E0, } DATA_TRANSFER_SIZE = { @@ -287,6 +292,7 @@ class Reply: "L0": 128, # bytes # ST RM0444 section 38.1 Unique device ID register "G0": 256, # bytes + "WL": 256, # bytes } FLASH_PAGE_SIZE = { @@ -314,6 +320,7 @@ class Reply: "L0": 128, # bytes # ST RM0444 section 38.2 Flash memory size data register "G0": 1024, # bytes + "WL": 1024, # bytes } SYNCHRONIZE_ATTEMPTS = 2 From 402c8ca7003c7d00707252f3181db4ab7194b8d0 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 9 Oct 2023 13:42:42 +0200 Subject: [PATCH 234/369] clean(lint) --- .pylintrc | 1 - noxfile.py | 4 ++-- setup.cfg | 17 +++++++++++------ stm32loader/bootloader.py | 4 ++-- stm32loader/main.py | 12 ++++++++++-- 5 files changed, 25 insertions(+), 13 deletions(-) diff --git a/.pylintrc b/.pylintrc index 4d4958c..0e18dc7 100644 --- a/.pylintrc +++ b/.pylintrc @@ -5,7 +5,6 @@ max-line-length=98 disable= fixme, # TO DOs are not errors. consider-using-f-string, # We're not on Python >= 3.6 yet. - bad-continuation, # Be compatible to black. [REPORT] score=no diff --git a/noxfile.py b/noxfile.py index 0508515..e814de4 100644 --- a/noxfile.py +++ b/noxfile.py @@ -5,8 +5,8 @@ import nox -DEFAULT_PYTHON_VERSION = "3.9" -ALL_PYTHON_VERSIONS = ["3.6", "3.7", "3.8", "3.9", "3.10"] +DEFAULT_PYTHON_VERSION = "3.11" +ALL_PYTHON_VERSIONS = ["3.8", "3.9", "3.10", "3.11"] @nox.session(python=ALL_PYTHON_VERSIONS) diff --git a/setup.cfg b/setup.cfg index 1ba0626..62ea56d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -18,15 +18,20 @@ exclude = build, dist, *.egg-info + +# be compatible to black +# Missing trailing comma +# Whitespace before ':' +# line break before binary operator ignore = - # be compatible to black - C812, # Missing trailing comma - E203, # Whitespace before ':' - W503, # line break before binary operator + C812, + E203, + W503, + +# Missing docstring in public function +# .next() is not a thing in Python 3 per-file-ignores = - # Missing docstring in public function tests/*:D103, - # .next() is not a thing in Python 3 stm32loader/bootloader.py:B305, [tool:pytest] diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index 31bf1c4..6941787 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -631,11 +631,11 @@ def extended_erase_memory(self, pages=None): :param iterable pages: Iterable of integer page addresses, zero-based. Set to None to trigger global mass erase. """ - if not pages and self.device_family in ('L0', ): + if not pages and self.device_family in ("L0",): # L0 devices do not support mass erase. # Instead, erase all pages individually. flash_size, _uid = self.get_flash_size_and_uid() - pages = list(range(0, (flash_size*1024) // self.flash_page_size)) + pages = list(range(0, (flash_size * 1024) // self.flash_page_size)) self.command(self.Command.EXTENDED_ERASE, "Extended erase memory") if pages: diff --git a/stm32loader/main.py b/stm32loader/main.py index c1d9bd8..075242e 100644 --- a/stm32loader/main.py +++ b/stm32loader/main.py @@ -155,7 +155,12 @@ def auto_int(x): ) address_arg = parser.add_argument( - "-a", "--address", action="store", type=auto_int, default=0x08000000, help="Target address." + "-a", + "--address", + action="store", + type=auto_int, + default=0x08000000, + help="Target address.", ) parser.add_argument( @@ -296,7 +301,10 @@ def connect(self): show_progress = self._get_progress_bar(self.configuration.no_progress) self.stm32 = bootloader.Stm32Bootloader( - serial_connection, verbosity=self.configuration.verbosity, show_progress=show_progress, device_family=self.configuration.family + serial_connection, + verbosity=self.configuration.verbosity, + show_progress=show_progress, + device_family=self.configuration.family, ) try: From fb4b6ed87d64fa9478f2062777b35de8d2cc06f2 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 9 Oct 2023 13:46:22 +0200 Subject: [PATCH 235/369] dev: Support Python versions 3.9/3.10/3.11 --- .github/workflows/lint.yaml | 6 +++--- .github/workflows/test.yaml | 6 +----- noxfile.py | 2 +- setup.py | 6 ++---- 4 files changed, 7 insertions(+), 13 deletions(-) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 82a0f6a..4a2d601 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -15,7 +15,7 @@ jobs: os: - ubuntu-latest python-version: - - "3.10" + - "3.11" runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v2 @@ -30,7 +30,7 @@ jobs: os: - ubuntu-latest python-version: - - "3.10" + - "3.11" runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v2 @@ -48,7 +48,7 @@ jobs: os: - ubuntu-latest python-version: - - "3.10" + - "3.11" runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index f2d66da..962d996 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -17,13 +17,9 @@ jobs: - ubuntu-latest - windows-latest python-version: - - "3.6" - - "3.7" - - "3.8" - "3.9" - "3.10" - - "pypy-3.7" - - "pypy-3.8" + - "3.11" - "pypy-3.9" runs-on: ${{ matrix.os }} steps: diff --git a/noxfile.py b/noxfile.py index e814de4..886a2d4 100644 --- a/noxfile.py +++ b/noxfile.py @@ -6,7 +6,7 @@ DEFAULT_PYTHON_VERSION = "3.11" -ALL_PYTHON_VERSIONS = ["3.8", "3.9", "3.10", "3.11"] +ALL_PYTHON_VERSIONS = ["3.9", "3.10", "3.11"] @nox.session(python=ALL_PYTHON_VERSIONS) diff --git a/setup.py b/setup.py index 15bc531..5dffab0 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ URL = 'https://github.com/florisla/stm32loader' EMAIL = 'florisla@gmail.com' AUTHOR = 'Floris Lambrechts' -REQUIRES_PYTHON = '>=3.6.0' +REQUIRES_PYTHON = '>=3.9.0' PROJECT_URLS = { "Bug Tracker": "https://github.com/florisla/stm32loader/issues", "Source Code": "https://github.com/florisla/stm32loader", @@ -64,11 +64,9 @@ 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', 'Programming Language :: Python', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Natural Language :: English', From 7fc0d2243627de94cb37774cab43bd094b4322c2 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 9 Oct 2023 22:01:37 +0200 Subject: [PATCH 236/369] doc: Update supported Python versions --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 28aff92..9c51081 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ ST Microelectronics STM32 microcontrollers over UART. Also supports ST BlueNRG devices, and the SweetPeas bootloader for Wiznet W7500. -Compatible with Python version 3.6 to 3.10 and PyPy 3.7 to 3.9. +Compatible with Python version 3.9 to 3.11 and PyPy 3.9. ## Installation From 5bf8cbb6537f6b07a893f6a6d4e56d62b58da8d3 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 9 Oct 2023 22:01:45 +0200 Subject: [PATCH 237/369] doc: Add new contributors --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9c51081..2ec6c2f 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,8 @@ Original Version by Ivan A-R (tuxotronic.org). Contributions by Domen Puncer, James Snyder, Floris Lambrechts, Atokulus, sam-bristow, NINI1988, Omer Kilic, Szymon Szantula, rdaforno, Mikolaj Stawiski, Tyler Coy, Alex Klimaj, Ondrej Mikle, denniszollo, -emilzay, michsens, blueskull, Mattia Maldini. +emilzay, michsens, blueskull, Mattia Maldini, etrommer, jadeaffenjaeger, +tosmaz. Inspiration for features from: From 13068c5d02c2ac884407d4dba7d74020e30c6a73 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 9 Oct 2023 22:02:26 +0200 Subject: [PATCH 238/369] feat: Support BlueNRG-1 and BlueNRG-2 --- stm32loader/bootloader.py | 36 ++++++++++++++++++++++++------------ stm32loader/main.py | 12 +++++++++++- 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index 6941787..a9ee145 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -60,9 +60,13 @@ 0x451: "STM32F76xxx/77xxx", # RM0394 46.6.1 MCU device ID code 0x435: "STM32L4xx", - # see ST AN4872 - # requires parity None - 0x11103: "BlueNRG", + # ST BlueNRG series; see ST AN4872. + # Three-byte ID where we mask out byte 1 (metal fix) and byte 2 (mask set). + # Requires parity None. + 0x000003: "BlueNRG-1 160kB", + 0x00000F: "BlueNRG-1 256kB", + 0x000023: "BlueNRG-2 160kB", + 0x00002F: "BlueNRG-2 256kB", # STM32F0 RM0091 Table 136. DEV_ID and REV_ID field values 0x440: "STM32F030x8", 0x445: "STM32F070x6", @@ -229,6 +233,8 @@ class Reply: "G0": 0x1FFF7590, # ST RM0453 section 39.1.1 Unique device ID register "WL": 0x1FFF7590, + # ST BlueNRG has DIE_ID register with PRODUCT, but no UID. + "NRG": None, } UID_SWAP = [[1, 0], [3, 2], [7, 6, 5, 4], [11, 10, 9, 8]] @@ -266,6 +272,8 @@ class Reply: "G0": 0x1FFF75E0, # ST RM0453 section 39.1.2 Flash size data register "WL": 0x1FFF75E0, + # ST BlueNRG-2 datasheet + "NRG": 0x40100014, } DATA_TRANSFER_SIZE = { @@ -293,34 +301,38 @@ class Reply: # ST RM0444 section 38.1 Unique device ID register "G0": 256, # bytes "WL": 256, # bytes + "NRG": 256, } FLASH_PAGE_SIZE = { + # In bytes. "default": 1024, # ST RM0360 section 27.1 Memory size data register # F030x4/x6/x8/xC, F070x6/xB - "F0": 1024, # bytes + "F0": 1024, # ST RM0008 section 30.2 Memory size registers # F101, F102, F103, F105, F107 - "F1": 1024, # bytes + "F1": 1024, # ST RM0366 section 29.2 Memory size data register # ST RM0365 section 34.2 Memory size data register # ST RM0316 section 34.2 Memory size data register # ST RM0313 section 32.2 Flash memory size data register # F303/328/358/398, F301/318, F302, F37x - "F3": 2048, # bytes + "F3": 2048, # ST RM0090 section 39.2 Flash size # F405/415, F407/417, F427/437, F429/439 - "F4": 1024, # bytes + "F4": 1024, # ST RM0385 section 41.2 Flash size - "F7": 1024, # bytes + "F7": 1024, # ST RM0394 - "L4": 1024, # bytes + "L4": 1024, # ST RM4510 25.1 Memory size register - "L0": 128, # bytes + "L0": 128, # ST RM0444 section 38.2 Flash memory size data register - "G0": 1024, # bytes - "WL": 1024, # bytes + "G0": 1024, + "WL": 1024, + # ST BlueNRG-2 data sheet: 128 pages of 8 * 64 * 4 bytes + "NRG": 2048, } SYNCHRONIZE_ATTEMPTS = 2 diff --git a/stm32loader/main.py b/stm32loader/main.py index 075242e..44d67d2 100644 --- a/stm32loader/main.py +++ b/stm32loader/main.py @@ -181,7 +181,7 @@ def auto_int(x): default=default_family, help=( "Device family to read out device UID and flash size; " - "e.g F1 for STM32F1xx" + "e.g F1 for STM32F1xx. Possible values: F0, F1, F3, F4, F7, H7, L4, L0, G0, NRG." + ("." if default_family else " (default: $STM32LOADER_FAMILY).") ), ) @@ -374,6 +374,16 @@ def read_device_id(self): boot_version = self.stm32.get() self.debug(0, "Bootloader version: 0x%X" % boot_version) device_id = self.stm32.get_id() + family = self.configuration.family + if family == "NRG": + # ST AN4872. + # Three bytes encode metal fix, mask set, BlueNRG-series + flash size. + metal_fix = (device_id & 0xFF0000) >> 16 + mask_set = (device_id & 0x00FF00) >> 8 + device_id = device_id & 0x0000FF + self.debug(0, "Metal fix: 0x%X" % metal_fix) + self.debug(0, "Mask set: 0x%X" % mask_set) + self.debug( 0, "Chip id: 0x%X (%s)" % (device_id, bootloader.CHIP_IDS.get(device_id, "Unknown")) ) From 7c21b4db7f358053b358636801a5414b6c449e16 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 9 Oct 2023 22:41:58 +0200 Subject: [PATCH 239/369] feat: Port to flit --- setup.cfg => .flake8 | 16 ---------- pyproject.toml | 66 ++++++++++++++++++++++++++++++++++++++ setup.py | 75 -------------------------------------------- 3 files changed, 66 insertions(+), 91 deletions(-) rename setup.cfg => .flake8 (64%) delete mode 100644 setup.py diff --git a/setup.cfg b/.flake8 similarity index 64% rename from setup.cfg rename to .flake8 index 62ea56d..24ba8f9 100644 --- a/setup.cfg +++ b/.flake8 @@ -1,13 +1,3 @@ -[bdist_wheel] -universal = 0 - -[metadata] -license_file = LICENSE - -[isort] -line_length = 98 -multi_line_output = 2 - [flake8] max-line-length = 98 max-doc-length = 78 @@ -33,9 +23,3 @@ ignore = per-file-ignores = tests/*:D103, stm32loader/bootloader.py:B305, - -[tool:pytest] -addopts = --strict -m "not (hardware or hardware_missing)" -markers = - hardware - missing_hardware diff --git a/pyproject.toml b/pyproject.toml index b0fb819..5eafe6b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,56 @@ +[build-system] +requires = ["flit_core >=3.2,<4"] +build-backend = "flit_core.buildapi" + +[project] +name = "stm32loader" +authors = [ + {name = "jsnyder"}, + {name = "Floris Lambrechts", email = "florisla@gmail.com"}, +] +readme = "README.md" +description = "Flash firmware to STM32 microcontrollers using Python." +license = {file = "LICENSE"} +requires-python = ">=3.9" +dependencies = [ + "pyserial", + "progress", +] +classifiers = [ + "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "Natural Language :: English", + "Operating System :: OS Independent", +] +dynamic = ["version", "description"] + +[project.optional-dependencies] +dev = [ + "wheel", + "twine", + "pylint", + "flake8", + "flake8-isort", + "black", + "bump2version", + "nox", + "cogapp", +] + +[project.scripts] +stm32loader = "stm32loader.__main__:main" + +[project.urls] +Home = "https://github.com/florisla/stm32loader" +BugTracker = "https://github.com/florisla/stm32loader/issues" +SourceCode = "https://github.com/florisla/stm32loader" + [tool.black] line-length = 98 @@ -12,3 +65,16 @@ exclude = ''' | .*\.egg-info )/ ''' + + +[tool.pytest.ini_options] +addopts = "--strict-markers -m 'not (hardware or hardware_missing)'" +markers = [ + "hardware", + "missing_hardware", +] + + +[tool.isort] +line_length = 98 +multi_line_output = 2 diff --git a/setup.py b/setup.py deleted file mode 100644 index 5dffab0..0000000 --- a/setup.py +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Note: To use the 'upload' functionality of this file, you must: -# $ pip install twine - -import io -import os - -from setuptools import find_packages, setup - -# Package meta-data. -NAME = 'stm32loader' -VERSION = "0.6.0" -DESCRIPTION = 'Flash firmware to STM32 microcontrollers using Python.' -URL = 'https://github.com/florisla/stm32loader' -EMAIL = 'florisla@gmail.com' -AUTHOR = 'Floris Lambrechts' -REQUIRES_PYTHON = '>=3.9.0' -PROJECT_URLS = { - "Bug Tracker": "https://github.com/florisla/stm32loader/issues", - "Source Code": "https://github.com/florisla/stm32loader", -} - -REQUIRED = [ - 'pyserial', - 'progress', -] - -EXTRAS = { - "dev": ['setuptools', 'wheel', 'twine', 'pylint', 'flake8', 'flake8-isort', 'black', 'bump2version', 'nox', 'cogapp'], -} - -HERE = os.path.abspath(os.path.dirname(__file__)) - -# Import the README and use it as the long-description. -# Note: this will only work if 'README.md' is present in your MANIFEST.in file! -try: - with io.open(os.path.join(HERE, 'README.md'), encoding='utf-8') as f: - LONG_DESCRIPTION = '\n' + f.read() -except FileNotFoundError: - LONG_DESCRIPTION = DESCRIPTION - -setup( - name=NAME, - version=VERSION, - description=DESCRIPTION, - long_description=LONG_DESCRIPTION, - long_description_content_type='text/markdown', - project_urls=PROJECT_URLS, - author=AUTHOR, - author_email=EMAIL, - python_requires=REQUIRES_PYTHON, - url=URL, - packages=find_packages(exclude=('tests',)), - entry_points={ - 'console_scripts': ['stm32loader=stm32loader.__main__:main'], - }, - install_requires=REQUIRED, - extras_require=EXTRAS, - include_package_data=True, - license='GPL3', - classifiers=[ - 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: Implementation :: CPython', - 'Programming Language :: Python :: Implementation :: PyPy', - 'Natural Language :: English', - 'Operating System :: OS Independent', - ], -) From 2e9e341b340b9035fb1907041091532c07344170 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 9 Oct 2023 22:42:33 +0200 Subject: [PATCH 240/369] clean(lint) --- stm32loader/bootloader.py | 3 ++- stm32loader/main.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index a9ee145..788609b 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -61,7 +61,8 @@ # RM0394 46.6.1 MCU device ID code 0x435: "STM32L4xx", # ST BlueNRG series; see ST AN4872. - # Three-byte ID where we mask out byte 1 (metal fix) and byte 2 (mask set). + # Three-byte ID where we mask out byte 1 (metal fix) + # and byte 2 (mask set). # Requires parity None. 0x000003: "BlueNRG-1 160kB", 0x00000F: "BlueNRG-1 256kB", diff --git a/stm32loader/main.py b/stm32loader/main.py index 44d67d2..f0f0ffb 100644 --- a/stm32loader/main.py +++ b/stm32loader/main.py @@ -377,7 +377,8 @@ def read_device_id(self): family = self.configuration.family if family == "NRG": # ST AN4872. - # Three bytes encode metal fix, mask set, BlueNRG-series + flash size. + # Three bytes encode metal fix, mask set, + # BlueNRG-series + flash size. metal_fix = (device_id & 0xFF0000) >> 16 mask_set = (device_id & 0x00FF00) >> 8 device_id = device_id & 0x0000FF From 2ae257de17c124fcb6abae96acd49ba503184985 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 9 Oct 2023 22:43:06 +0200 Subject: [PATCH 241/369] release: Bump version to 0.7.0 --- stm32loader/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stm32loader/__init__.py b/stm32loader/__init__.py index c26ea20..2eb7c51 100644 --- a/stm32loader/__init__.py +++ b/stm32loader/__init__.py @@ -1,4 +1,4 @@ """Flash firmware to STM32 microcontrollers over a serial connection.""" -__version_info__ = (0, 6, 0) +__version_info__ = (0, 7, 0) __version__ = "-".join(str(part) for part in __version_info__).replace("-", ".", 2) From 261edec036c76e8d94459ddac9989613d22f157e Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 9 Oct 2023 22:52:39 +0200 Subject: [PATCH 242/369] fix: Add description --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5eafe6b..6aac4f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ classifiers = [ "Natural Language :: English", "Operating System :: OS Independent", ] -dynamic = ["version", "description"] +dynamic = ["version"] [project.optional-dependencies] dev = [ From f99d628c78890cc2dc4787cbe0ff041f2c286053 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 9 Oct 2023 22:52:54 +0200 Subject: [PATCH 243/369] dev: Update list of supported Python versions --- tox.ini | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tox.ini b/tox.ini index 8d86ae6..076b28d 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py36, py37, py38, py39, py310, pypy37 +envlist = py39, py310, py311, pypy39 [testenv] passenv = HOME @@ -15,11 +15,7 @@ norecursedirs= .git .github .tox .nox build dist tmp* tests/integration [gh-actions] python = - 3.6: py36 - 3.7: py37 - 3.8: py38 3.9: py39 3.10: py310 - pypy-3.7: pypy37 - pypy-3.8: pypy38 + 3.11: py311 pypy-3.9: pypy39 From 600aa07df4350c81789c6baa3b34f9c361354b0e Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 10 Oct 2023 10:18:29 +0200 Subject: [PATCH 244/369] feat: Support uploading hex file Naive implementation, assuming the addresses start at 0 and increment. --- pyproject.toml | 3 +++ stm32loader/bootloader.py | 4 ++++ stm32loader/hexfile.py | 30 ++++++++++++++++++++++++++++++ stm32loader/main.py | 9 +++++++-- tests/data/small.hex | 2 ++ tests/unit/test_hexfile.py | 14 ++++++++++++++ 6 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 stm32loader/hexfile.py create mode 100644 tests/data/small.hex create mode 100644 tests/unit/test_hexfile.py diff --git a/pyproject.toml b/pyproject.toml index 6aac4f7..2604c40 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,9 @@ classifiers = [ dynamic = ["version"] [project.optional-dependencies] +hex = [ + "intelhex", +] dev = [ "wheel", "twine", diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index 788609b..c037705 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -101,6 +101,10 @@ class DataMismatchError(Stm32LoaderError): """Exception: data comparison failed.""" +class MissingDependencyError(Stm32LoaderError): + """Exception: required dependency is missing.""" + + class ShowProgress: """ Show progress through a progress bar, as a context manager. diff --git a/stm32loader/hexfile.py b/stm32loader/hexfile.py new file mode 100644 index 0000000..298af0c --- /dev/null +++ b/stm32loader/hexfile.py @@ -0,0 +1,30 @@ +"""Load binary data from a file in Intel hex format.""" + +from stm32loader.bootloader import MissingDependencyError + +try: + import intelhex +except ImportError: + intelhex = None + + +def load_hex(file_path: str) -> bytes: + """ + Return bytes from the given hex file. + + Addresses should start at zero and always increment. + """ + if intelhex is None: + raise MissingDependencyError( + "Please install package 'intelhex' in order to read .hex files." + ) + + hex_content = intelhex.IntelHex() + hex_content.loadhex(str(file_path)) + hex_dict = hex_content.todict() + + addresses = list(hex_dict.keys()) + assert addresses[0] == 0 + assert addresses[-1] == len(addresses) - 1 + + return bytes(hex_content.todict().values()) diff --git a/stm32loader/main.py b/stm32loader/main.py index f0f0ffb..30b1f42 100644 --- a/stm32loader/main.py +++ b/stm32loader/main.py @@ -27,6 +27,7 @@ import os import sys from types import SimpleNamespace +from pathlib import Path try: from progress.bar import ChargingBar as progress_bar @@ -35,6 +36,7 @@ from stm32loader import __version__, bootloader from stm32loader.uart import SerialConnection +from stm32loader.hexfile import load_hex DEFAULT_VERBOSITY = 5 @@ -323,8 +325,11 @@ def perform_commands(self): # pylint: disable=too-many-branches binary_data = None if self.configuration.write or self.configuration.verify: - with open(self.configuration.data_file, "rb") as read_file: - binary_data = bytearray(read_file.read()) + data_file_path = Path(self.configuration.data_file) + if data_file_path.suffix == ".hex": + binary_data = load_hex(data_file_path) + else: + binary_data = data_file_path.read_bytes() if self.configuration.unprotect: try: self.stm32.readout_unprotect() diff --git a/tests/data/small.hex b/tests/data/small.hex new file mode 100644 index 0000000..270a249 --- /dev/null +++ b/tests/data/small.hex @@ -0,0 +1,2 @@ +:10000000000102030405060708090A0B0C0D0E0F78 +:00000001FF diff --git a/tests/unit/test_hexfile.py b/tests/unit/test_hexfile.py new file mode 100644 index 0000000..4b81d3d --- /dev/null +++ b/tests/unit/test_hexfile.py @@ -0,0 +1,14 @@ + +from pathlib import Path + +HERE = Path(__file__).parent +DATA = HERE / "../data" + + +from stm32loader.hexfile import load_hex + + +def test_load_hex_delivers_bytes(): + small_hex_path = DATA / "small.hex" + data = load_hex(small_hex_path) + assert data == bytes(range(16)) From 22799ebb627314763124bb1d4263bb3032b3fa2a Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 10 Oct 2023 10:33:19 +0200 Subject: [PATCH 245/369] clean: Move argument parsing to separate file --- stm32loader/args.py | 223 +++++++++++++++++++++++++++++++++++++++++++ stm32loader/main.py | 226 ++------------------------------------------ 2 files changed, 230 insertions(+), 219 deletions(-) create mode 100644 stm32loader/args.py diff --git a/stm32loader/args.py b/stm32loader/args.py new file mode 100644 index 0000000..92e61e5 --- /dev/null +++ b/stm32loader/args.py @@ -0,0 +1,223 @@ +"""Parse command-line arguments.""" + +import argparse +import atexit +import copy +import os +import sys + +from stm32loader import __version__ + + +DEFAULT_VERBOSITY = 5 + + +class HelpFormatter(argparse.RawDescriptionHelpFormatter, argparse.ArgumentDefaultsHelpFormatter): + """Custom help formatter -- don't print confusing default values.""" + + def _get_help_string(self, action): + action = copy.copy(action) + # Don't show "(default: None)" for arguments without defaults, + # or "(default: False)" for boolean flags, and hide the + # (default: 5) from --verbose's help because it's confusing. + if not action.default or action.dest == "verbosity": + action.default = argparse.SUPPRESS + return super()._get_help_string(action) + + def _format_actions_usage(self, actions, groups): + # Always treat -p/--port as required. See the note about the + # argparse hack in Stm32Loader.parse_arguments for why. + def tweak_action(action): + action = copy.copy(action) + if action.dest == "port": + action.required = True + return action + + return super()._format_actions_usage(map(tweak_action, actions), groups) + + +def _auto_int(x): + """Convert to int with automatic base detection.""" + # This supports 0x10 == 16 and 10 == 10 + return int(x, 0) + + +def parse_arguments(arguments): + """Parse the given command-line arguments and return the configuration.""" + + parser = argparse.ArgumentParser( + prog="stm32loader", + description="Flash firmware to STM32 microcontrollers.", + epilog="\n".join( + [ + "examples:", + " %(prog)s -p COM7 -f F1", + " %(prog)s -e -w -v example/main.bin", + ] + ), + formatter_class=HelpFormatter, + ) + + data_file_arg = parser.add_argument( + "data_file", + metavar="FILE.BIN", + type=str, + nargs="?", + help="File to read from or store to flash.", + ) + + parser.add_argument( + "-e", + "--erase", + action="store_true", + help="Erase (note: this is required on previously written memory).", + ) + + parser.add_argument( + "-u", "--unprotect", action="store_true", help="Unprotect in case erase fails." + ) + + parser.add_argument("-w", "--write", action="store_true", help="Write file content to flash.") + + parser.add_argument( + "-v", + "--verify", + action="store_true", + help="Verify flash content versus local file (recommended).", + ) + + parser.add_argument( + "-r", "--read", action="store_true", help="Read from flash and store in local file." + ) + + length_arg = parser.add_argument( + "-l", "--length", action="store", type=_auto_int, help="Length of read." + ) + + default_port = os.environ.get("STM32LOADER_SERIAL_PORT") + port_arg = parser.add_argument( + "-p", + "--port", + action="store", + type=str, # morally required=True + default=default_port, + help=("Serial port" + ("." if default_port else " (default: $STM32LOADER_SERIAL_PORT).")), + ) + + parser.add_argument( + "-b", "--baud", action="store", type=int, default=115200, help="Baudrate." + ) + + address_arg = parser.add_argument( + "-a", + "--address", + action="store", + type=_auto_int, + default=0x08000000, + help="Target address.", + ) + + parser.add_argument( + "-g", + "--go-address", + action="store", + type=_auto_int, + metavar="ADDRESS", + help="Start executing from address (0x08000000, usually).", + ) + + default_family = os.environ.get("STM32LOADER_FAMILY") + parser.add_argument( + "-f", + "--family", + action="store", + type=str, + default=default_family, + help=( + "Device family to read out device UID and flash size; " + "e.g F1 for STM32F1xx. Possible values: F0, F1, F3, F4, F7, H7, L4, L0, G0, NRG." + + ("." if default_family else " (default: $STM32LOADER_FAMILY).") + ), + ) + + parser.add_argument( + "-V", + "--verbose", + dest="verbosity", + action="store_const", + const=10, + default=DEFAULT_VERBOSITY, + help="Verbose mode.", + ) + + parser.add_argument( + "-q", "--quiet", dest="verbosity", action="store_const", const=0, help="Quiet mode." + ) + + parser.add_argument( + "-s", + "--swap-rts-dtr", + action="store_true", + help="Swap RTS and DTR: use RTS for reset and DTR for boot0.", + ) + + parser.add_argument( + "-R", "--reset-active-high", action="store_true", help="Make RESET active high." + ) + + parser.add_argument( + "-B", "--boot0-active-low", action="store_true", help="Make BOOT0 active low." + ) + + parser.add_argument( + "-n", "--no-progress", action="store_true", help="Don't show progress bar." + ) + + parser.add_argument( + "-P", + "--parity", + action="store", + type=str, + default="even", + choices=["event", "none"], + help='Parity: "even" for STM32, "none" for BlueNRG.', + ) + + parser.add_argument("--version", action="version", version=__version__) + + # Hack: We want certain arguments to be required when one + # of -rwv is specified, but argparse doesn't support + # conditional dependencies like that. Instead, we add the + # requirements post-facto and re-run the parse to get the error + # messages we want. A better solution would be to use + # subcommands instead of options for -rwv, but this would + # change the command-line interface. + # + # We also use this gross hack to provide a hint about the + # STM32LOADER_SERIAL_PORT environment variable when -p + # is omitted; we only set --port as required after the first + # parse so we can hook in a custom error message. + + configuration = parser.parse_args(arguments) + + if not configuration.port: + port_arg.required = True + atexit.register( + lambda: print( + "{}: note: you can also set the environment " + "variable STM32LOADER_SERIAL_PORT".format(parser.prog), + file=sys.stderr, + ) + ) + + if configuration.read or configuration.write or configuration.verify: + data_file_arg.nargs = None + data_file_arg.required = True + + if configuration.read: + length_arg.required = True + address_arg.required = True + + parser.parse_args(arguments) + + return configuration diff --git a/stm32loader/main.py b/stm32loader/main.py index 30b1f42..50f424b 100644 --- a/stm32loader/main.py +++ b/stm32loader/main.py @@ -21,10 +21,6 @@ """Flash firmware to STM32 microcontrollers over a serial connection.""" -import argparse -import atexit -import copy -import os import sys from types import SimpleNamespace from pathlib import Path @@ -34,35 +30,10 @@ except ImportError: progress_bar = None -from stm32loader import __version__, bootloader +from stm32loader import args +from stm32loader import hexfile +from stm32loader import bootloader from stm32loader.uart import SerialConnection -from stm32loader.hexfile import load_hex - -DEFAULT_VERBOSITY = 5 - - -class HelpFormatter(argparse.RawDescriptionHelpFormatter, argparse.ArgumentDefaultsHelpFormatter): - """Custom help formatter -- don't print confusing default values.""" - - def _get_help_string(self, action): - action = copy.copy(action) - # Don't show "(default: None)" for arguments without defaults, - # or "(default: False)" for boolean flags, and hide the - # (default: 5) from --verbose's help because it's confusing. - if not action.default or action.dest == "verbosity": - action.default = argparse.SUPPRESS - return super()._get_help_string(action) - - def _format_actions_usage(self, actions, groups): - # Always treat -p/--port as required. See the note about the - # argparse hack in Stm32Loader.parse_arguments for why. - def tweak_action(action): - action = copy.copy(action) - if action.dest == "port": - action.required = True - return action - - return super()._format_actions_usage(map(tweak_action, actions), groups) class Stm32Loader: @@ -83,190 +54,7 @@ def debug(self, level, message): def parse_arguments(self, arguments): """Parse the list of command-line arguments.""" - - def auto_int(x): - """Convert to int with automatic base detection.""" - # This supports 0x10 == 16 and 10 == 10 - return int(x, 0) - - parser = argparse.ArgumentParser( - prog="stm32loader", - description="Flash firmware to STM32 microcontrollers.", - epilog="\n".join( - [ - "examples:", - " %(prog)s -p COM7 -f F1", - " %(prog)s -e -w -v example/main.bin", - ] - ), - formatter_class=HelpFormatter, - ) - - data_file_arg = parser.add_argument( - "data_file", - metavar="FILE.BIN", - type=str, - nargs="?", - help="File to read from or store to flash.", - ) - - parser.add_argument( - "-e", - "--erase", - action="store_true", - help="Erase (note: this is required on previously written memory).", - ) - - parser.add_argument( - "-u", "--unprotect", action="store_true", help="Unprotect in case erase fails." - ) - - parser.add_argument( - "-w", "--write", action="store_true", help="Write file content to flash." - ) - - parser.add_argument( - "-v", - "--verify", - action="store_true", - help="Verify flash content versus local file (recommended).", - ) - - parser.add_argument( - "-r", "--read", action="store_true", help="Read from flash and store in local file." - ) - - length_arg = parser.add_argument( - "-l", "--length", action="store", type=auto_int, help="Length of read." - ) - - default_port = os.environ.get("STM32LOADER_SERIAL_PORT") - port_arg = parser.add_argument( - "-p", - "--port", - action="store", - type=str, # morally required=True - default=default_port, - help=( - "Serial port" + ("." if default_port else " (default: $STM32LOADER_SERIAL_PORT).") - ), - ) - - parser.add_argument( - "-b", "--baud", action="store", type=int, default=115200, help="Baudrate." - ) - - address_arg = parser.add_argument( - "-a", - "--address", - action="store", - type=auto_int, - default=0x08000000, - help="Target address.", - ) - - parser.add_argument( - "-g", - "--go-address", - action="store", - type=auto_int, - metavar="ADDRESS", - help="Start executing from address (0x08000000, usually).", - ) - - default_family = os.environ.get("STM32LOADER_FAMILY") - parser.add_argument( - "-f", - "--family", - action="store", - type=str, - default=default_family, - help=( - "Device family to read out device UID and flash size; " - "e.g F1 for STM32F1xx. Possible values: F0, F1, F3, F4, F7, H7, L4, L0, G0, NRG." - + ("." if default_family else " (default: $STM32LOADER_FAMILY).") - ), - ) - - parser.add_argument( - "-V", - "--verbose", - dest="verbosity", - action="store_const", - const=10, - default=DEFAULT_VERBOSITY, - help="Verbose mode.", - ) - - parser.add_argument( - "-q", "--quiet", dest="verbosity", action="store_const", const=0, help="Quiet mode." - ) - - parser.add_argument( - "-s", - "--swap-rts-dtr", - action="store_true", - help="Swap RTS and DTR: use RTS for reset and DTR for boot0.", - ) - - parser.add_argument( - "-R", "--reset-active-high", action="store_true", help="Make RESET active high." - ) - - parser.add_argument( - "-B", "--boot0-active-low", action="store_true", help="Make BOOT0 active low." - ) - - parser.add_argument( - "-n", "--no-progress", action="store_true", help="Don't show progress bar." - ) - - parser.add_argument( - "-P", - "--parity", - action="store", - type=str, - default="even", - choices=self.PARITY.keys(), - help='Parity: "even" for STM32, "none" for BlueNRG.', - ) - - parser.add_argument("--version", action="version", version=__version__) - - # Hack: We want certain arguments to be required when one - # of -rwv is specified, but argparse doesn't support - # conditional dependencies like that. Instead, we add the - # requirements post-facto and re-run the parse to get the error - # messages we want. A better solution would be to use - # subcommands instead of options for -rwv, but this would - # change the command-line interface. - # - # We also use this gross hack to provide a hint about the - # STM32LOADER_SERIAL_PORT environment variable when -p - # is omitted; we only set --port as required after the first - # parse so we can hook in a custom error message. - - self.configuration = parser.parse_args(arguments) - - if not self.configuration.port: - port_arg.required = True - atexit.register( - lambda: print( - "{}: note: you can also set the environment " - "variable STM32LOADER_SERIAL_PORT".format(parser.prog), - file=sys.stderr, - ) - ) - - if self.configuration.read or self.configuration.write or self.configuration.verify: - data_file_arg.nargs = None - data_file_arg.required = True - - if self.configuration.read: - length_arg.required = True - address_arg.required = True - - parser.parse_args(arguments) + self.configuration = args.parse_arguments(arguments) # parse successful, process options further self.configuration.parity = Stm32Loader.PARITY[self.configuration.parity.lower()] @@ -327,7 +115,7 @@ def perform_commands(self): if self.configuration.write or self.configuration.verify: data_file_path = Path(self.configuration.data_file) if data_file_path.suffix == ".hex": - binary_data = load_hex(data_file_path) + binary_data = hexfile.load_hex(data_file_path) else: binary_data = data_file_path.read_bytes() if self.configuration.unprotect: @@ -427,7 +215,7 @@ def _get_progress_bar(no_progress=False): return bootloader.ShowProgress(progress_bar) -def main(*args, **kwargs): +def main(*arguments, **kwargs): """ Parse arguments and execute tasks. @@ -435,7 +223,7 @@ def main(*args, **kwargs): """ try: loader = Stm32Loader() - loader.parse_arguments(args) + loader.parse_arguments(arguments) loader.connect() try: loader.read_device_id() From 16a6f04560fe7bb59056eef626588496ab6206d5 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 10 Oct 2023 10:38:50 +0200 Subject: [PATCH 246/369] fix: Install intelhex for testing --- .github/workflows/lint.yaml | 4 ++-- .github/workflows/test.yaml | 2 +- noxfile.py | 2 ++ tox.ini | 1 + 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 4a2d601..1beac03 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -38,7 +38,7 @@ jobs: with: python-version: ${{ matrix.python-version }} - name: Install lint dependencies - run: pip install flake8 pyserial + run: pip install flake8 pyserial intelhex - name: Run flake8 run: flake8 stm32loader pylint: @@ -56,6 +56,6 @@ jobs: with: python-version: ${{ matrix.python-version }} - name: Install lint dependencies - run: pip install pylint pyserial + run: pip install pylint pyserial intelhex - name: Run pylint run: pylint stm32loader diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 962d996..5c23e58 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -28,6 +28,6 @@ jobs: with: python-version: ${{ matrix.python-version }} - name: Install test dependencies - run: pip install tox tox-gh-actions pyserial pytest + run: pip install tox tox-gh-actions pyserial pytest intelhex - name: Run setup and tests as defined in tox.ini run: tox diff --git a/noxfile.py b/noxfile.py index 886a2d4..4165c20 100644 --- a/noxfile.py +++ b/noxfile.py @@ -22,6 +22,7 @@ def tests(session): # see https://github.com/pypa/setuptools/issues/1671 rmtree("./dist", ignore_errors=True) session.install(".") + session.install("intelhex") session.install("pytest") session.chdir("tests") session.run("pytest", "./") @@ -40,6 +41,7 @@ def lint(session): session.install("pylint") # pyserial for avoiding a complaint by pylint session.install("pyserial") + session.install("intelhex") session.run("pylint", "stm32loader") session.install("flake8", "flake8-isort") diff --git a/tox.ini b/tox.ini index 076b28d..5ff6b13 100644 --- a/tox.ini +++ b/tox.ini @@ -6,6 +6,7 @@ passenv = HOME deps= pytest pyserial + intelhex commands= pytest -r a [] tests From 72287a9a32f71e973b4def75a066f8ed26b09d60 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 10 Oct 2023 15:57:13 +0200 Subject: [PATCH 247/369] feat: Allow to erase a specific region of the flash memory --- stm32loader/args.py | 4 ++-- stm32loader/bootloader.py | 17 ++++++++++++++++- stm32loader/main.py | 8 +++++++- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/stm32loader/args.py b/stm32loader/args.py index 92e61e5..819c11a 100644 --- a/stm32loader/args.py +++ b/stm32loader/args.py @@ -91,7 +91,7 @@ def parse_arguments(arguments): ) length_arg = parser.add_argument( - "-l", "--length", action="store", type=_auto_int, help="Length of read." + "-l", "--length", action="store", type=_auto_int, help="Length of read or erase." ) default_port = os.environ.get("STM32LOADER_SERIAL_PORT") @@ -114,7 +114,7 @@ def parse_arguments(arguments): action="store", type=_auto_int, default=0x08000000, - help="Target address.", + help="Target address for read, write or erase.", ) parser.add_argument( diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index c037705..860fb8d 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -596,7 +596,7 @@ def write_memory(self, address, data): self.write_and_ack("0x31 programming failed", nr_of_bytes - 1, data, checksum) self.debug(10, " Write memory done") - def erase_memory(self, pages=None): + def erase_memory(self, pages=None, from_to=None): """ Erase flash memory at the given pages. @@ -605,6 +605,21 @@ def erase_memory(self, pages=None): :param iterable pages: Iterable of integer page addresses, zero-based. Set to None to trigger global mass erase. """ + if pages is None and from_to is not None: + # Erase a specific memory region. + start_address, end_address = from_to + assert ( + start_address % self.flash_page_size == 0 + ), f"Erase start address should be at a flash page boundary: 0x{start_address:08X}." + assert ( + end_address % self.flash_page_size == 0 + ), f"Erase end address should be at a flash page boundary: 0x{end_address:08X}." + + # Assemble the list of pages to erase. + first_page = start_address / self.flash_page_size + last_page = end_address / self.flash_page_size + pages = list(range(first_page, last_page)) + if self.extended_erase: # Use erase with two-byte addresses. self.extended_erase_memory(pages) diff --git a/stm32loader/main.py b/stm32loader/main.py index 50f424b..362b7f9 100644 --- a/stm32loader/main.py +++ b/stm32loader/main.py @@ -129,7 +129,13 @@ def perform_commands(self): sys.exit(1) if self.configuration.erase: try: - self.stm32.erase_memory() + if self.configuration.address is None: + self.stm32.erase_memory() + else: + start_address = self.configuration.address + end_address = self.configuration.address + self.configuration.length + self.stm32.erase_memory(from_to=(start_address, end_address)) + except bootloader.CommandError: # may be caused by readout protection self.debug( From 3d5446766e44d8d0956d63db03c14977b3925f18 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 10 Oct 2023 15:56:48 +0200 Subject: [PATCH 248/369] fix(typo) --- stm32loader/args.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stm32loader/args.py b/stm32loader/args.py index 819c11a..b2c4966 100644 --- a/stm32loader/args.py +++ b/stm32loader/args.py @@ -179,7 +179,7 @@ def parse_arguments(arguments): action="store", type=str, default="even", - choices=["event", "none"], + choices=["even", "none"], help='Parity: "even" for STM32, "none" for BlueNRG.', ) From ef01b69f821086cb1c8a9fdd3bee54a093353ca1 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 10 Oct 2023 15:57:37 +0200 Subject: [PATCH 249/369] doc: Use long-length arguments in examples --- README.md | 8 ++++---- stm32loader/args.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 2ec6c2f..d463524 100644 --- a/README.md +++ b/README.md @@ -87,20 +87,20 @@ examples: ## Command-line example ``` -stm32loader -p /dev/tty.usbserial-ftCYPMYJ -e -w -v somefile.bin +stm32loader --port /dev/tty.usbserial-ftCYPMYJ --erase --write --verify somefile.bin ``` This will pre-erase flash, write `somefile.bin` to the flash on the device, and then perform a verification after writing is finished. -You can skip the `-p` option by configuring environment variable +You can skip the `--port` option by configuring environment variable `STM32LOADER_SERIAL_PORT`. -Similarly, `-f` may be supplied through `STM32LOADER_FAMILY`. +Similarly, `--family` may be supplied through `STM32LOADER_FAMILY`. To read out firmware and store it in a file: ``` -stm32loader -r -p /dev/cu.usbserial-A5XK3RJT -f F1 -l 0x10000 -a 0x08000000 dump.bin +stm32loader --read --port /dev/cu.usbserial-A5XK3RJT --family F1 --length 0x10000 --address 0x08000000 dump.bin ``` diff --git a/stm32loader/args.py b/stm32loader/args.py index b2c4966..657df9a 100644 --- a/stm32loader/args.py +++ b/stm32loader/args.py @@ -51,8 +51,8 @@ def parse_arguments(arguments): epilog="\n".join( [ "examples:", - " %(prog)s -p COM7 -f F1", - " %(prog)s -e -w -v example/main.bin", + " %(prog)s --port COM7 --family F1", + " %(prog)s --erase --write --verify example/main.bin", ] ), formatter_class=HelpFormatter, From f99cefab3f051715d59fad5b0275ae6805334c8c Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 10 Oct 2023 15:57:48 +0200 Subject: [PATCH 250/369] doc: Describe how to erase a section of memory --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index d463524..488cbb7 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,20 @@ stm32loader --read --port /dev/cu.usbserial-A5XK3RJT --family F1 --length 0x1000 ``` +To erase the full device: + +``` +stm32loader --erase --port /dev/cu.usbserial-A5XK3RJT +``` + +Or erase only a specific region of the flash: + +``` +stm32loader --erase --address 0x08000000 --length 0x2000 --port /dev/cu.usbserial-A5XK3RJT +``` + + + ## Reference documents * ST `AN2606`: STM32 microcontroller system memory boot mode From 77bfa9935afe36d059a99de2818ad5f24abf8fa7 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 10 Oct 2023 16:11:43 +0200 Subject: [PATCH 251/369] feat: Allow to un-protect flash from readout --- README.md | 20 ++++++++++++-------- stm32loader/args.py | 6 +++++- stm32loader/main.py | 11 +++++++++-- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 488cbb7..e76e0ed 100644 --- a/README.md +++ b/README.md @@ -42,30 +42,34 @@ sys.stdout.close() sys.stdout = sys.__stdout__ ]]] --> ``` -usage: stm32loader [-h] [-e] [-u] [-w] [-v] [-r] [-l LENGTH] -p PORT [-b BAUD] [-a ADDRESS] [-g ADDRESS] [-f FAMILY] [-V] [-q] [-s] [-R] [-B] [-n] [-P {even,none}] [--version] [FILE.BIN] +usage: stm32loader [-h] [-e] [-u] [-x] [-w] [-v] [-r] [-l LENGTH] -p PORT [-b BAUD] [-a ADDRESS] [-g ADDRESS] [-f FAMILY] + [-V] [-q] [-s] [-R] [-B] [-n] [-P {even,none}] [--version] + [FILE.BIN] Flash firmware to STM32 microcontrollers. positional arguments: FILE.BIN File to read from or store to flash. -optional arguments: +options: -h, --help show this help message and exit -e, --erase Erase (note: this is required on previously written memory). - -u, --unprotect Unprotect in case erase fails. + -u, --unprotect Unprotect flash from readout. + -x, --protect Protect flash against readout. -w, --write Write file content to flash. -v, --verify Verify flash content versus local file (recommended). -r, --read Read from flash and store in local file. -l LENGTH, --length LENGTH - Length of read. + Length of read or erase. -p PORT, --port PORT Serial port (default: $STM32LOADER_SERIAL_PORT). -b BAUD, --baud BAUD Baudrate. (default: 115200) -a ADDRESS, --address ADDRESS - Target address. (default: 134217728) + Target address for read, write or erase. (default: 134217728) -g ADDRESS, --go-address ADDRESS Start executing from address (0x08000000, usually). -f FAMILY, --family FAMILY - Device family to read out device UID and flash size; e.g F1 for STM32F1xx (default: $STM32LOADER_FAMILY). + Device family to read out device UID and flash size; e.g F1 for STM32F1xx. Possible values: F0, + F1, F3, F4, F7, H7, L4, L0, G0, NRG. (default: $STM32LOADER_FAMILY). -V, --verbose Verbose mode. -q, --quiet Quiet mode. -s, --swap-rts-dtr Swap RTS and DTR: use RTS for reset and DTR for boot0. @@ -79,8 +83,8 @@ optional arguments: --version show program's version number and exit examples: - stm32loader -p COM7 -f F1 - stm32loader -e -w -v example/main.bin + stm32loader --port COM7 --family F1 + stm32loader --erase --write --verify example/main.bin ``` diff --git a/stm32loader/args.py b/stm32loader/args.py index 657df9a..ec9148a 100644 --- a/stm32loader/args.py +++ b/stm32loader/args.py @@ -74,7 +74,11 @@ def parse_arguments(arguments): ) parser.add_argument( - "-u", "--unprotect", action="store_true", help="Unprotect in case erase fails." + "-u", "--unprotect", action="store_true", help="Unprotect flash from readout." + ) + + parser.add_argument( + "-x", "--protect", action="store_true", help="Protect flash against readout." ) parser.add_argument("-w", "--write", action="store_true", help="Write file content to flash.") diff --git a/stm32loader/main.py b/stm32loader/main.py index 362b7f9..9282a77 100644 --- a/stm32loader/main.py +++ b/stm32loader/main.py @@ -122,8 +122,15 @@ def perform_commands(self): try: self.stm32.readout_unprotect() except bootloader.CommandError: - # may be caused by readout protection - self.debug(0, "Erase failed -- probably due to readout protection") + self.debug(0, "Flash readout unprotect failed") + self.debug(0, "Quit") + self.stm32.reset_from_flash() + sys.exit(1) + if self.configuration.protect: + try: + self.stm32.readout_protect() + except bootloader.CommandError: + self.debug(0, "Flash readout protect failed") self.debug(0, "Quit") self.stm32.reset_from_flash() sys.exit(1) From 09e8ecc7ffde67ca134bbb340fbccb6b5dfd969b Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 10 Oct 2023 16:12:07 +0200 Subject: [PATCH 252/369] clean: Avoid duplicates command keys and values --- stm32loader/bootloader.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index 860fb8d..75a3ca5 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -20,6 +20,7 @@ """Talk to an STM32 native bootloader (see ST AN3155).""" +import enum import math import operator import struct @@ -172,7 +173,8 @@ class Stm32Bootloader: # pylint: disable=too-many-public-methods - class Command: + @enum.unique + class Command(enum.IntEnum): """STM32 native bootloader command values.""" # pylint: disable=too-few-public-methods @@ -193,10 +195,6 @@ class Command: WRITE_PROTECT = 0x63 WRITE_UNPROTECT = 0x73 - # not used so far - READOUT_PROTECT = 0x82 - READOUT_UNPROTECT = 0x92 - # not really listed under commands, but still... # 'wake the bootloader' == 'activate USART' == 'synchronize' SYNCHRONIZE = 0x7F From 148efdfd8709a311e60fc00b6a8f06cc9ace5872 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 10 Oct 2023 17:18:28 +0200 Subject: [PATCH 253/369] clean(lint) --- stm32loader/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/stm32loader/main.py b/stm32loader/main.py index 9282a77..0d006a1 100644 --- a/stm32loader/main.py +++ b/stm32loader/main.py @@ -111,6 +111,7 @@ def connect(self): def perform_commands(self): """Run all operations as defined by the configuration.""" # pylint: disable=too-many-branches + # pylint: disable=too-many-statements binary_data = None if self.configuration.write or self.configuration.verify: data_file_path = Path(self.configuration.data_file) From a26158de1fb6445f0bcd8b7f57a775ad3127e52c Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 11 Oct 2023 10:37:09 +0200 Subject: [PATCH 254/369] dev: Port to bump-my-version --- .bumpversion.cfg | 27 --------------------------- pyproject.toml | 31 ++++++++++++++++++++++++++++++- stm32loader/__init__.py | 2 +- 3 files changed, 31 insertions(+), 29 deletions(-) delete mode 100644 .bumpversion.cfg diff --git a/.bumpversion.cfg b/.bumpversion.cfg deleted file mode 100644 index 30b897a..0000000 --- a/.bumpversion.cfg +++ /dev/null @@ -1,27 +0,0 @@ -[bumpversion] -current_version = 0.6.0 -commit = True -tag = False -message = Release: bump version number from v{current_version} to v{new_version} -parse = (?P\d+)\.(?P\d+)\.(?P\d+)(-(?P.+))? -serialize = - {major}.{minor}.{patch}-{release} - {major}.{minor}.{patch} - -[bumpversion:part:release] -optional_value = release -values = - dev - release - -[bumpversion:file:stm32loader/__init__.py] -parse = \((?P\d+),\s(?P\d+),\s(?P\d+)(\s*,\s*['\"](?P[^'\"]+)['\"])?\) -serialize = - ({major}, {minor}, {patch}, "{release}") - ({major}, {minor}, {patch}) -search = __version_info__ = {current_version} -replace = __version_info__ = {new_version} - -[bumpversion:file:setup.py] -search = VERSION = "{current_version}" -replace = VERSION = "{new_version}" diff --git a/pyproject.toml b/pyproject.toml index 2604c40..bcb9f30 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,7 @@ dev = [ "flake8", "flake8-isort", "black", - "bump2version", + "bump-my-version", "nox", "cogapp", ] @@ -55,6 +55,35 @@ BugTracker = "https://github.com/florisla/stm32loader/issues" SourceCode = "https://github.com/florisla/stm32loader" +[tool.bumpversion] +current_version = "0.6.0" +commit = true +tag = true +message = "release: Bump version number from v{current_version} to v{new_version}" +parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)(-(?P.+))?" +serialize = [ + "{major}.{minor}.{patch}-{release}", + "{major}.{minor}.{patch}", +] + +[tool.bumpversion.parts.release] +optional_value = "release" +values = [ + "dev", + "release", +] + +[[tool.bumpversion.files]] +filename = "stm32loader/__init__.py" +parse = "((?P\\d+),\\s(?P\\d+),\\s(?P\\d+)(\\s*,\\s*'(?P[^']+)')?)" +serialize = [ + "({major}, {minor}, {patch}, '{release}')", + "({major}, {minor}, {patch})", +] +search = "__version_info__ = {current_version}" +replace = "__version_info__ = {new_version}" + + [tool.black] line-length = 98 target-version = ['py34', 'py35', 'py36', 'py37', 'py38'] diff --git a/stm32loader/__init__.py b/stm32loader/__init__.py index 2eb7c51..c26ea20 100644 --- a/stm32loader/__init__.py +++ b/stm32loader/__init__.py @@ -1,4 +1,4 @@ """Flash firmware to STM32 microcontrollers over a serial connection.""" -__version_info__ = (0, 7, 0) +__version_info__ = (0, 6, 0) __version__ = "-".join(str(part) for part in __version_info__).replace("-", ".", 2) From b0aed64587b34202078f6b6edd8e741c94015d4b Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 11 Oct 2023 10:37:45 +0200 Subject: [PATCH 255/369] release: Bump version number from v0.6.0 to v0.7.0-dev --- pyproject.toml | 2 +- stm32loader/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index bcb9f30..f9add50 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,7 +56,7 @@ SourceCode = "https://github.com/florisla/stm32loader" [tool.bumpversion] -current_version = "0.6.0" +current_version = "0.7.0-dev" commit = true tag = true message = "release: Bump version number from v{current_version} to v{new_version}" diff --git a/stm32loader/__init__.py b/stm32loader/__init__.py index c26ea20..b463799 100644 --- a/stm32loader/__init__.py +++ b/stm32loader/__init__.py @@ -1,4 +1,4 @@ """Flash firmware to STM32 microcontrollers over a serial connection.""" -__version_info__ = (0, 6, 0) +__version_info__ = (0, 7, 0, 'dev') __version__ = "-".join(str(part) for part in __version_info__).replace("-", ".", 2) From 39cd5d4592862ac62f1686f17fd0d1e594c70b8c Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 11 Oct 2023 22:57:13 +0200 Subject: [PATCH 256/369] fix: Configure H7 flash page size and data transfer size --- stm32loader/bootloader.py | 37 ++++++++++++++----------------------- 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index 75a3ca5..063784d 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -280,31 +280,20 @@ class Reply: } DATA_TRANSFER_SIZE = { + # In bytes. "default": 256, - # No unique id for these parts - "F0": 256, # bytes - # ST RM0008 section 30.1 Unique device ID register - # F101, F102, F103, F105, F107 - "F1": 256, # bytes - # ST RM0366 section 29.1 Unique device ID register - # ST RM0365 section 34.1 Unique device ID register - # ST RM0316 section 34.1 Unique device ID register - # ST RM0313 section 32.1 Unique device ID register - # F303/328/358/398, F301/318, F302, F37x - "F3": 256, # bytes - # ST RM0090 section 39.1 Unique device ID register - # F405/415, F407/417, F427/437, F429/439 - "F4": 256, # bytes - # ST RM0385 section 41.2 Unique device ID register - "F7": 256, # bytes - # ST RM0394 47.1 Unique device ID register (96 bits) - "L4": 256, # bytes - # ST RM0451 25.2 Unique device ID register (96 bits) - "L0": 128, # bytes - # ST RM0444 section 38.1 Unique device ID register - "G0": 256, # bytes - "WL": 256, # bytes + "F0": 256, + "F1": 256, + "F3": 256, + "F4": 256, + "F7": 256, + "L4": 256, + "L0": 128, + "G0": 256, + "WL": 256, "NRG": 256, + # ST RM0433 section 4.2 FLASH main features + "H7": 256, } FLASH_PAGE_SIZE = { @@ -336,6 +325,8 @@ class Reply: "WL": 1024, # ST BlueNRG-2 data sheet: 128 pages of 8 * 64 * 4 bytes "NRG": 2048, + # ST RM0433 section 4.2 FLASH main features + "H7": 128 * 1024, } SYNCHRONIZE_ATTEMPTS = 2 From f2f9a5b9cae6cbd667502ae81a777717b214a99a Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 11 Oct 2023 22:59:16 +0200 Subject: [PATCH 257/369] dev: Add 'releasenumber' part to bump config --- pyproject.toml | 10 +++++----- stm32loader/__init__.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f9add50..56236cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,13 +56,13 @@ SourceCode = "https://github.com/florisla/stm32loader" [tool.bumpversion] -current_version = "0.7.0-dev" +current_version = "0.7.0-dev0" commit = true tag = true message = "release: Bump version number from v{current_version} to v{new_version}" -parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)(-(?P.+))?" +parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)(-(?P[^\\d]+)(?P\\d+))?" serialize = [ - "{major}.{minor}.{patch}-{release}", + "{major}.{minor}.{patch}-{release}{devrelease}", "{major}.{minor}.{patch}", ] @@ -75,9 +75,9 @@ values = [ [[tool.bumpversion.files]] filename = "stm32loader/__init__.py" -parse = "((?P\\d+),\\s(?P\\d+),\\s(?P\\d+)(\\s*,\\s*'(?P[^']+)')?)" +parse = "((?P\\d+),\\s(?P\\d+),\\s(?P\\d+)(\\s*,\\s*'(?P[^']+)'\\s*,\\s*(?P\\d+))?)" serialize = [ - "({major}, {minor}, {patch}, '{release}')", + "({major}, {minor}, {patch}, '{release}', {devrelease})", "({major}, {minor}, {patch})", ] search = "__version_info__ = {current_version}" diff --git a/stm32loader/__init__.py b/stm32loader/__init__.py index b463799..3daa5d4 100644 --- a/stm32loader/__init__.py +++ b/stm32loader/__init__.py @@ -1,4 +1,4 @@ """Flash firmware to STM32 microcontrollers over a serial connection.""" -__version_info__ = (0, 7, 0, 'dev') +__version_info__ = (0, 7, 0, 'dev', 0) __version__ = "-".join(str(part) for part in __version_info__).replace("-", ".", 2) From 99c5f6a0f9537d43986e24e1f5abc716c1655406 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 11 Oct 2023 22:59:39 +0200 Subject: [PATCH 258/369] fix: Don't erase when only address or length is known --- stm32loader/main.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/stm32loader/main.py b/stm32loader/main.py index 0d006a1..9cfcbb7 100644 --- a/stm32loader/main.py +++ b/stm32loader/main.py @@ -137,9 +137,27 @@ def perform_commands(self): sys.exit(1) if self.configuration.erase: try: - if self.configuration.address is None: + if self.configuration.length is None: + if self.configuration.address is not None: + self.debug( + 0, + "Can not erase from specific address without specified length.\n" + "Consider using the --length argument.", + ) + self.stm32.reset_from_flash() + sys.exit(1) + # Address and length are given. self.stm32.erase_memory() else: + # Address is None. + if self.configuration.length is not None: + self.debug( + 0, + "Can not erase specific length without a start address.\n" + "Consider using the --address argument.", + ) + self.stm32.reset_from_flash() + sys.exit(1) start_address = self.configuration.address end_address = self.configuration.address + self.configuration.length self.stm32.erase_memory(from_to=(start_address, end_address)) From 71eb788aab5081b8dec67f794f582b375e0ea0fb Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 11 Oct 2023 23:18:11 +0200 Subject: [PATCH 259/369] fix: Use proper double quotes in __version_info__ --- pyproject.toml | 4 ++-- stm32loader/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 56236cc..2edab4d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -75,9 +75,9 @@ values = [ [[tool.bumpversion.files]] filename = "stm32loader/__init__.py" -parse = "((?P\\d+),\\s(?P\\d+),\\s(?P\\d+)(\\s*,\\s*'(?P[^']+)'\\s*,\\s*(?P\\d+))?)" +parse = "\\((?P\\d+),\\s(?P\\d+),\\s(?P\\d+)(\\s*,\\s*\"(?P[^\"]+)\"\\s*,\\s*(?P\\d+))?\\)" serialize = [ - "({major}, {minor}, {patch}, '{release}', {devrelease})", + "({major}, {minor}, {patch}, \"{release}\", {devrelease})", "({major}, {minor}, {patch})", ] search = "__version_info__ = {current_version}" diff --git a/stm32loader/__init__.py b/stm32loader/__init__.py index 3daa5d4..5a7ea87 100644 --- a/stm32loader/__init__.py +++ b/stm32loader/__init__.py @@ -1,4 +1,4 @@ """Flash firmware to STM32 microcontrollers over a serial connection.""" -__version_info__ = (0, 7, 0, 'dev', 0) +__version_info__ = (0, 7, 0, "dev", 0) __version__ = "-".join(str(part) for part in __version_info__).replace("-", ".", 2) From a709438ab26c45e2e681d84a05b29a0116abebe8 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 11 Oct 2023 23:17:41 +0200 Subject: [PATCH 260/369] clean: Drop older Python releases from black --- pyproject.toml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2edab4d..398b235 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -86,7 +86,11 @@ replace = "__version_info__ = {new_version}" [tool.black] line-length = 98 -target-version = ['py34', 'py35', 'py36', 'py37', 'py38'] +target-version = [ + "py39", + "py310", + "py311", +] exclude = ''' /( \.git From 32fef7c0f8c5c42aef11ef1941b1bae3fb4d1020 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 11 Oct 2023 23:18:43 +0200 Subject: [PATCH 261/369] release: Bump version number from v0.7.0-dev0 to v0.7.0-dev1 --- pyproject.toml | 2 +- stm32loader/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 398b235..e3d70eb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,7 +56,7 @@ SourceCode = "https://github.com/florisla/stm32loader" [tool.bumpversion] -current_version = "0.7.0-dev0" +current_version = "0.7.0-dev1" commit = true tag = true message = "release: Bump version number from v{current_version} to v{new_version}" diff --git a/stm32loader/__init__.py b/stm32loader/__init__.py index 5a7ea87..e17041a 100644 --- a/stm32loader/__init__.py +++ b/stm32loader/__init__.py @@ -1,4 +1,4 @@ """Flash firmware to STM32 microcontrollers over a serial connection.""" -__version_info__ = (0, 7, 0, "dev", 0) +__version_info__ = (0, 7, 0, "dev", 1) __version__ = "-".join(str(part) for part in __version_info__).replace("-", ".", 2) From 8f3b83003bb0d06213ac2a726734ae9186165d3f Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Thu, 12 Oct 2023 11:31:09 +0200 Subject: [PATCH 262/369] fix: Don't rely on address every being None --- stm32loader/main.py | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/stm32loader/main.py b/stm32loader/main.py index 9cfcbb7..c41b369 100644 --- a/stm32loader/main.py +++ b/stm32loader/main.py @@ -138,26 +138,10 @@ def perform_commands(self): if self.configuration.erase: try: if self.configuration.length is None: - if self.configuration.address is not None: - self.debug( - 0, - "Can not erase from specific address without specified length.\n" - "Consider using the --length argument.", - ) - self.stm32.reset_from_flash() - sys.exit(1) - # Address and length are given. + # Erase full device. self.stm32.erase_memory() else: - # Address is None. - if self.configuration.length is not None: - self.debug( - 0, - "Can not erase specific length without a start address.\n" - "Consider using the --address argument.", - ) - self.stm32.reset_from_flash() - sys.exit(1) + # Erase from address to address + length. start_address = self.configuration.address end_address = self.configuration.address + self.configuration.length self.stm32.erase_memory(from_to=(start_address, end_address)) From 1ec7d9240167555e35f91859aaa12e34c369eb56 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Thu, 12 Oct 2023 11:31:36 +0200 Subject: [PATCH 263/369] doc: Recommend to use --length with --erase --- stm32loader/args.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/stm32loader/args.py b/stm32loader/args.py index ec9148a..1cc064c 100644 --- a/stm32loader/args.py +++ b/stm32loader/args.py @@ -70,7 +70,10 @@ def parse_arguments(arguments): "-e", "--erase", action="store_true", - help="Erase (note: this is required on previously written memory).", + help=( + "Erase the full flash memory or a specific region (support --address and --length)." + " Note: this is required on previously written memory.", + ) ) parser.add_argument( @@ -118,7 +121,7 @@ def parse_arguments(arguments): action="store", type=_auto_int, default=0x08000000, - help="Target address for read, write or erase.", + help="Target address for read or write. For erase, this is used when you supply --length.", ) parser.add_argument( From df4f1cf9748e15b14ee35b45f8d9f42ce19cee6b Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Thu, 12 Oct 2023 11:32:20 +0200 Subject: [PATCH 264/369] release: Bump version number from v0.7.0-dev1 to v0.7.0-dev2 --- pyproject.toml | 2 +- stm32loader/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e3d70eb..8ac04cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,7 +56,7 @@ SourceCode = "https://github.com/florisla/stm32loader" [tool.bumpversion] -current_version = "0.7.0-dev1" +current_version = "0.7.0-dev2" commit = true tag = true message = "release: Bump version number from v{current_version} to v{new_version}" diff --git a/stm32loader/__init__.py b/stm32loader/__init__.py index e17041a..a5b3dd1 100644 --- a/stm32loader/__init__.py +++ b/stm32loader/__init__.py @@ -1,4 +1,4 @@ """Flash firmware to STM32 microcontrollers over a serial connection.""" -__version_info__ = (0, 7, 0, "dev", 1) +__version_info__ = (0, 7, 0, "dev", 2) __version__ = "-".join(str(part) for part in __version_info__).replace("-", ".", 2) From 03ca1fb43f957f84e9d77a3cc532b29876d291d2 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Thu, 12 Oct 2023 22:14:27 +0200 Subject: [PATCH 265/369] fix: Use correct device IDs for H7 series Error was spotted in https://github.com/florisla/stm32loader/issues/67 . --- stm32loader/bootloader.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index 063784d..a151db4 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -56,9 +56,13 @@ # RM0090 in ( 38.6.1 MCU device ID code ) 0x413: "STM32F405xx/07xx and STM32F415xx/17xx", 0x419: "STM32F42xxx and STM32F43xxx", + # AN2606 + 0x452: "STM32F72xxx/73xxx", 0x449: "STM32F74xxx/75xxx", - 0x450: "STM32H76xxx/77xxx", 0x451: "STM32F76xxx/77xxx", + 0x483: "STM32H72xxx/73xxx", + 0x450: "STM32H74xxx/75xxx", + 0x480: "STM32H7A3xx/B3xx", # RM0394 46.6.1 MCU device ID code 0x435: "STM32L4xx", # ST BlueNRG series; see ST AN4872. From 255e5718f9b67747c9ce7c73bb578eed48db5158 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Thu, 12 Oct 2023 22:15:02 +0200 Subject: [PATCH 266/369] doc: Use more long-form arguments --- stm32loader/main.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/stm32loader/main.py b/stm32loader/main.py index c41b369..acaad6b 100644 --- a/stm32loader/main.py +++ b/stm32loader/main.py @@ -75,11 +75,11 @@ def connect(self): print(str(e) + "\n", file=sys.stderr) print( "Is the device connected and powered correctly?\n" - "Please use the -p option to select the correct serial port. Examples:\n" - " -p COM3\n" - " -p /dev/ttyS0\n" - " -p /dev/ttyUSB0\n" - " -p /dev/tty.usbserial-ftCYPMYJ\n", + "Please use the --port option to select the correct serial port. Examples:\n" + " --port COM3\n" + " --port /dev/ttyS0\n" + " --port /dev/ttyUSB0\n" + " --port /dev/tty.usbserial-ftCYPMYJ\n", file=sys.stderr, ) sys.exit(1) @@ -150,8 +150,8 @@ def perform_commands(self): # may be caused by readout protection self.debug( 0, - "Erase failed -- probably due to readout protection\n" - "consider using the -u (unprotect) option.", + "Erase failed -- probably due to readout protection.\n" + "Consider using the --unprotect option.", ) self.stm32.reset_from_flash() sys.exit(1) @@ -202,7 +202,7 @@ def read_device_uid(self): """Show chip UID and flash size.""" family = self.configuration.family if not family: - self.debug(0, "Supply -f [family] to see flash size and device UID, e.g: -f F1") + self.debug(0, "Supply --family to see flash size and device UID, e.g: -f F1") return try: From a93005f276035567b04882644fcbed78e87b9e19 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Thu, 12 Oct 2023 22:18:09 +0200 Subject: [PATCH 267/369] dev: Use IntEnum for Reply --- README.md | 1 - stm32loader/bootloader.py | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e76e0ed..1973f1d 100644 --- a/README.md +++ b/README.md @@ -198,4 +198,3 @@ adapter (it needs to toggle, whereas `BOOT0` does not). * Use f-strings. * Use proper logging instead of print statements. -* Start using `IntEnum` for commands and replies. diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index a151db4..535fab6 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -203,7 +203,8 @@ class Command(enum.IntEnum): # 'wake the bootloader' == 'activate USART' == 'synchronize' SYNCHRONIZE = 0x7F - class Reply: + @enum.unique + class Reply(enum.IntEnum): """STM32 native bootloader reply status codes.""" # pylint: disable=too-few-public-methods From eabb05865abf4516f82cc8f7f78fdccc5685aa36 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Thu, 12 Oct 2023 22:18:32 +0200 Subject: [PATCH 268/369] doc: Drop since-implemented things from 'Not supported' --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 1973f1d..7638924 100644 --- a/README.md +++ b/README.md @@ -187,10 +187,8 @@ adapter (it needs to toggle, whereas `BOOT0` does not). ## Not currently supported -* Command-line argument for readout protection. * Command-line argument for write protection/unprotection. * STM8 devices (ST `UM0560`). -* Paged flash erase for devices with page size <> 1 KiB. * Other bootloader protocols (e.g. I2C, HEX -> implemented in `stm32flash`). From 78803480fc22e2954df119df6a7f494a8c52d0e2 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Thu, 12 Oct 2023 22:38:01 +0200 Subject: [PATCH 269/369] doc: Update release notes --- CHANGELOG.md | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87e8bc8..e2cc659 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,25 +3,48 @@ What changed in which version. -## [0.6.0] - 2022-03-FIXME +## [0.7.0] - 2023-10-12 + +### Added +* Support ST BlueNRG-1 and BlueNRG-2 devices. +* Support ST STM32H7 series devices. +* Allow to erase specific pages of flash memory. +* Add command-line switch to protect flash against readout. +* Support Intel hex file format. +* Adopt `flit` as build system. +* Adopt `bump-my-version` as version bumper. + + +### Cleaned +* Move argument-parsing code to separate file. +* Use long-form argument names in help text and error messages. +* Use IntEnum for commands and responses. + + + +## [0.6.0] - 2023-10-09 ### Added * `#59` Continuous Integration: start running tests and linters on GitHub Actions. * `#42` `#43` Find flash size for non-standard MCUs (F4, L0). -* Support STM32H76xxx/77xxx series. +* Support STM32H7 series. * Packaging: auto-generate the help output using `cog`. +* Support STM32WL. +* Support Python 3.9 - 3.11. ### Changed * `#46` `#48` Flush the UART read buffer after MCU reset. * Use argparse instead of optparse. -* Drop support for Python 2, 3.4, 3.5. +* Drop support for Python 2, 3.4 - 3.8. ### Fixed -* `#44` Support flash page count higher than 255. +* `#44` Support flash page size higher than 255. +* `#64` Properly parse address and length given as hexadecimal value. +* `#62` Properly pass device family argument. ### Documented * `#13` Describe how to extend Stm32Loader. -* `#52` Describe alternative was to execute the module. +* `#52` Describe alternative ways to execute the module. * `#58` Add a list of similar tools. From 97f570c2b2726a9ba4762147c2a9ec8ac14c7cbb Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Thu, 12 Oct 2023 22:50:15 +0200 Subject: [PATCH 270/369] fix(typo) --- stm32loader/args.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stm32loader/args.py b/stm32loader/args.py index 1cc064c..0e75496 100644 --- a/stm32loader/args.py +++ b/stm32loader/args.py @@ -72,8 +72,8 @@ def parse_arguments(arguments): action="store_true", help=( "Erase the full flash memory or a specific region (support --address and --length)." - " Note: this is required on previously written memory.", - ) + " Note: this is required on previously written memory." + ), ) parser.add_argument( From 7b34dbacee404b775405c5511f1aa7179386f734 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Thu, 12 Oct 2023 22:50:22 +0200 Subject: [PATCH 271/369] clean(lint) --- stm32loader/args.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/stm32loader/args.py b/stm32loader/args.py index 0e75496..893e3f5 100644 --- a/stm32loader/args.py +++ b/stm32loader/args.py @@ -121,7 +121,9 @@ def parse_arguments(arguments): action="store", type=_auto_int, default=0x08000000, - help="Target address for read or write. For erase, this is used when you supply --length.", + help=( + "Target address for read or write. For erase, this is used when you supply --length." + ), ) parser.add_argument( From 252a8aa28fa96600cf534a56acc2a1a6fdc7c889 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Thu, 12 Oct 2023 22:50:49 +0200 Subject: [PATCH 272/369] release: Bump version number from v0.7.0-dev2 to v0.7.0 --- pyproject.toml | 2 +- stm32loader/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8ac04cf..e3bc77d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,7 +56,7 @@ SourceCode = "https://github.com/florisla/stm32loader" [tool.bumpversion] -current_version = "0.7.0-dev2" +current_version = "0.7.0" commit = true tag = true message = "release: Bump version number from v{current_version} to v{new_version}" diff --git a/stm32loader/__init__.py b/stm32loader/__init__.py index a5b3dd1..2eb7c51 100644 --- a/stm32loader/__init__.py +++ b/stm32loader/__init__.py @@ -1,4 +1,4 @@ """Flash firmware to STM32 microcontrollers over a serial connection.""" -__version_info__ = (0, 7, 0, "dev", 2) +__version_info__ = (0, 7, 0) __version__ = "-".join(str(part) for part in __version_info__).replace("-", ".", 2) From d7addd5f3e8aac70e042c39dbc6f48500c5bc6b3 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Thu, 12 Oct 2023 22:55:17 +0200 Subject: [PATCH 273/369] doc: Describe the big bugfix of v0.7.0 --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2cc659..356b002 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ What changed in which version. * Adopt `flit` as build system. * Adopt `bump-my-version` as version bumper. +### Fixed +* Erasing was impossible due to --length not being supplied. ### Cleaned * Move argument-parsing code to separate file. @@ -24,6 +26,8 @@ What changed in which version. ## [0.6.0] - 2023-10-09 +Yanked on 2023-10-12 due to bug when erasing. Use 0.7.0 instead. + ### Added * `#59` Continuous Integration: start running tests and linters on GitHub Actions. * `#42` `#43` Find flash size for non-standard MCUs (F4, L0). From f98f476c44f0c9ad4e128e3787bbb496361a6dd4 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Fri, 13 Oct 2023 08:08:03 +0200 Subject: [PATCH 274/369] fix: Use integer division --- stm32loader/bootloader.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index 535fab6..4819330 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -610,8 +610,8 @@ def erase_memory(self, pages=None, from_to=None): ), f"Erase end address should be at a flash page boundary: 0x{end_address:08X}." # Assemble the list of pages to erase. - first_page = start_address / self.flash_page_size - last_page = end_address / self.flash_page_size + first_page = start_address // self.flash_page_size + last_page = end_address // self.flash_page_size pages = list(range(first_page, last_page)) if self.extended_erase: From f36b583293e8a2300bd774b3aabdf88f29c8b413 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Fri, 13 Oct 2023 08:08:06 +0200 Subject: [PATCH 275/369] release: Bump version number from v0.7.0 to v0.7.1-dev0 --- pyproject.toml | 2 +- stm32loader/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e3bc77d..1aa6eaf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,7 +56,7 @@ SourceCode = "https://github.com/florisla/stm32loader" [tool.bumpversion] -current_version = "0.7.0" +current_version = "0.7.1-dev0" commit = true tag = true message = "release: Bump version number from v{current_version} to v{new_version}" diff --git a/stm32loader/__init__.py b/stm32loader/__init__.py index 2eb7c51..37bb766 100644 --- a/stm32loader/__init__.py +++ b/stm32loader/__init__.py @@ -1,4 +1,4 @@ """Flash firmware to STM32 microcontrollers over a serial connection.""" -__version_info__ = (0, 7, 0) +__version_info__ = (0, 7, 1, "dev", 0) __version__ = "-".join(str(part) for part in __version_info__).replace("-", ".", 2) From 96290b73b94e661a86c859143026596b59bc01d7 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 18 Oct 2023 21:37:29 +0200 Subject: [PATCH 276/369] clean: Extract method for 'range to pages' calculation --- firmware/generic_boot20_pc13.binary.bin | 0 stm32loader/bootloader.py | 35 ++++++++++++++----------- stm32loader/devices.py | 0 stm32loader/main.py | 5 ++-- tests/emulate/erasewriteverify.py | 0 tests/unit/test_bootloader.py | 26 ++++++++++++++++-- 6 files changed, 46 insertions(+), 20 deletions(-) create mode 100644 firmware/generic_boot20_pc13.binary.bin create mode 100644 stm32loader/devices.py create mode 100644 tests/emulate/erasewriteverify.py diff --git a/firmware/generic_boot20_pc13.binary.bin b/firmware/generic_boot20_pc13.binary.bin new file mode 100644 index 0000000..e69de29 diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index 4819330..96a06b2 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -590,7 +590,7 @@ def write_memory(self, address, data): self.write_and_ack("0x31 programming failed", nr_of_bytes - 1, data, checksum) self.debug(10, " Write memory done") - def erase_memory(self, pages=None, from_to=None): + def erase_memory(self, pages=None): """ Erase flash memory at the given pages. @@ -599,21 +599,6 @@ def erase_memory(self, pages=None, from_to=None): :param iterable pages: Iterable of integer page addresses, zero-based. Set to None to trigger global mass erase. """ - if pages is None and from_to is not None: - # Erase a specific memory region. - start_address, end_address = from_to - assert ( - start_address % self.flash_page_size == 0 - ), f"Erase start address should be at a flash page boundary: 0x{start_address:08X}." - assert ( - end_address % self.flash_page_size == 0 - ), f"Erase end address should be at a flash page boundary: 0x{end_address:08X}." - - # Assemble the list of pages to erase. - first_page = start_address // self.flash_page_size - last_page = end_address // self.flash_page_size - pages = list(range(first_page, last_page)) - if self.extended_erase: # Use erase with two-byte addresses. self.extended_erase_memory(pages) @@ -806,6 +791,24 @@ def verify_data(read_data, reference_data): % (address, bytearray([read_byte])[0], bytearray([reference_byte])[0]) ) + def pages_from_range(self, start, end): + """Return page indices for the given memory range.""" + if start % self.flash_page_size != 0: + raise PageIndexError( + f"Erase start address should be at a flash page boundary: 0x{start:08X}." + ) + if end % self.flash_page_size != 0: + raise PageIndexError( + f"Erase end address should be at a flash page boundary: 0x{end:08X}." + ) + + # Assemble the list of pages to erase. + first_page = start // self.flash_page_size + last_page = end // self.flash_page_size + pages = list(range(first_page, last_page)) + + return pages + def _reset(self): """Enable or disable the reset IO line (if possible).""" if not hasattr(self.connection, "enable_reset"): diff --git a/stm32loader/devices.py b/stm32loader/devices.py new file mode 100644 index 0000000..e69de29 diff --git a/stm32loader/main.py b/stm32loader/main.py index acaad6b..793dd89 100644 --- a/stm32loader/main.py +++ b/stm32loader/main.py @@ -139,12 +139,13 @@ def perform_commands(self): try: if self.configuration.length is None: # Erase full device. - self.stm32.erase_memory() + self.stm32.erase_memory(pages=None) else: # Erase from address to address + length. start_address = self.configuration.address end_address = self.configuration.address + self.configuration.length - self.stm32.erase_memory(from_to=(start_address, end_address)) + pages = self.stm32.pages_from_range(start_address, end_address) + self.stm32.erase_memory(pages) except bootloader.CommandError: # may be caused by readout protection diff --git a/tests/emulate/erasewriteverify.py b/tests/emulate/erasewriteverify.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/test_bootloader.py b/tests/unit/test_bootloader.py index b9e472c..8b375c5 100644 --- a/tests/unit/test_bootloader.py +++ b/tests/unit/test_bootloader.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock from stm32loader import bootloader as Stm32 -from stm32loader.bootloader import Stm32Bootloader +from stm32loader.bootloader import Stm32Bootloader, PageIndexError # pylint: disable=missing-docstring, redefined-outer-name @@ -128,9 +128,16 @@ def test_erase_memory_without_pages_sends_global_erase(bootloader, write): assert write.data_was_written(b'\xff\x00') -def test_erase_memory_with_pages_sends_sector_count(bootloader, write): +def test_erase_memory_with_pages_sends_sector_count_and_eight_bit_page_indices(bootloader, write): bootloader.erase_memory([0x11, 0x12, 0x13, 0x14]) assert write.data_was_written(b'\x03') + assert write.data_was_written(b'\x11\x12\x13\x14') + + +def test_extended_erase_memory_with_pages_sends_sector_count_and_sixteen_bit_page_indices(bootloader, write): + bootloader.extended_erase_memory([0x11, 0x12, 0x13, 0x14]) + assert write.data_was_written(b'\x00\x03') + assert write.data_was_written(b'\x00\x11\x00\x12\x00\x13\x00\x14') def test_erase_memory_with_pages_sends_sector_addresses_with_checksum(bootloader, write): @@ -254,3 +261,18 @@ def test_format_uid_returns_correct_string(bootloader, uid_string): uid, expected_description = uid_string description = bootloader.format_uid(uid) assert description == expected_description + + +def test_get_pages_from_range_with_invalid_start_address_raises_page_index_error(bootloader): + with pytest.raises(PageIndexError, match=".*start address should be at a flash page boundary.*"): + bootloader.pages_from_range(10, 1024) + + +def test_get_pages_from_range_with_start_address_zero_returns_single_page(bootloader): + pages = bootloader.pages_from_range(0, 1024) + assert pages == [0] + + +def test_get_pages_from_large_range_returns_multiple_pages(bootloader): + pages = bootloader.pages_from_range(5*1024, 20*1024) + assert pages == list(range(5, 20)) From afd83d0b62031a57f15755174c148731b5c47e81 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 18 Oct 2023 21:36:04 +0200 Subject: [PATCH 277/369] fix: Print everything to stderr This is not optimal, but better than the stdout/stderr mix which has non-deterministic order. --- stm32loader/bootloader.py | 2 +- stm32loader/main.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index 96a06b2..fa9cc01 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -671,7 +671,7 @@ def extended_erase_memory(self, pages=None): previous_timeout_value = self.connection.timeout self.connection.timeout = 30 - print("Extended erase (0x44), this can take ten seconds or more") + print("Extended erase (0x44), this can take ten seconds or more", file=sys.stderr) try: self._wait_for_ack("0x44 erasing failed") finally: diff --git a/stm32loader/main.py b/stm32loader/main.py index 793dd89..3b2c754 100644 --- a/stm32loader/main.py +++ b/stm32loader/main.py @@ -162,9 +162,9 @@ def perform_commands(self): read_data = self.stm32.read_memory_data(self.configuration.address, len(binary_data)) try: bootloader.Stm32Bootloader.verify_data(read_data, binary_data) - print("Verification OK") + print("Verification OK", file=sys.stderr) except bootloader.DataMismatchError as e: - print("Verification FAILED: %s" % e, file=sys.stdout) + print("Verification FAILED: %s" % e, file=sys.stderr) sys.exit(1) if not self.configuration.write and self.configuration.read: read_data = self.stm32.read_memory_data( From c56da3ddf552df033f5445963b0a0fce0259125f Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 18 Oct 2023 21:39:21 +0200 Subject: [PATCH 278/369] clean: Use long-form arguments --- tests/integration/test_stm32loader.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/tests/integration/test_stm32loader.py b/tests/integration/test_stm32loader.py index 0b24a09..5127524 100644 --- a/tests/integration/test_stm32loader.py +++ b/tests/integration/test_stm32loader.py @@ -40,7 +40,7 @@ @pytest.fixture(scope="module") def stm32loader(): def main_with_default_arguments(*args): - main("-p", SERIAL_PORT, "-b", str(BAUD_RATE), "-q", *args, avoid_system_exit=True) + main("--port", SERIAL_PORT, "--baud", str(BAUD_RATE), "--quiet", *args, avoid_system_exit=True) return main_with_default_arguments @@ -68,9 +68,9 @@ def test_env_var_stm32loader_serial_port_defines_port(capsys): assert ("port 'COM109'" in captured.err or "port COM109" in captured.err) -def test_argument_p_overrules_env_var_for_serial_port(capsys): +def test_argument_port_overrules_env_var_for_serial_port(capsys): os.environ['STM32LOADER_SERIAL_PORT'] = "COM120" - main("-p", "COM121", avoid_system_exit=True) + main("--port", "COM121", avoid_system_exit=True) captured = capsys.readouterr() assert ("port 'COM121'" in captured.err or "port COM121" in captured.err) @@ -85,8 +85,8 @@ def test_device_not_connected_prints_readable_error(stm32loader, capsys): @pytest.mark.hardware -def test_argument_f_prints_chip_id_and_device_type(stm32loader, capsys): - stm32loader("-f", STM32_CHIP_FAMILY) +def test_argument_family_prints_chip_id_and_device_type(stm32loader, capsys): + stm32loader("--family", STM32_CHIP_FAMILY) captured = capsys.readouterr() assert STM32_CHIP_ID in captured.err assert STM32_CHIP_TYPE in captured.err @@ -94,14 +94,14 @@ def test_argument_f_prints_chip_id_and_device_type(stm32loader, capsys): @pytest.mark.hardware def test_read_produces_file_of_correct_length(stm32loader, dump_file): - stm32loader("-r", "-l", "1024", dump_file) + stm32loader("--read", "--length", "1024", dump_file) assert os.stat(dump_file).st_size == 1024 @pytest.mark.hardware def test_erase_resets_memory_to_all_ones(stm32loader, dump_file): # erase - stm32loader("-e") + stm32loader("--erase") # read all bytes and check if they're 0xFF stm32loader("-r", "-l", "1024", dump_file) read_data = bytearray(open(dump_file, "rb").read()) @@ -111,10 +111,10 @@ def test_erase_resets_memory_to_all_ones(stm32loader, dump_file): @pytest.mark.hardware def test_write_saves_correct_data(stm32loader, dump_file): # erase and write - stm32loader("-e", "-w", FIRMWARE_FILE) + stm32loader("--erase", "--write", FIRMWARE_FILE) # read and compare data with file on disk - stm32loader("-r", "-l", str(SIZE), dump_file) + stm32loader("--read", "--length", str(SIZE), dump_file) read_data = open(dump_file, "rb").read() original_data = open(FIRMWARE_FILE, "rb").read() @@ -129,4 +129,6 @@ def test_write_saves_correct_data(stm32loader, dump_file): @pytest.mark.hardware def test_erase_write_verify_passes(stm32loader): - stm32loader("-e", "-w", "-v", FIRMWARE_FILE) + stm32loader("--erase", "--write", "--verify", FIRMWARE_FILE) + + From 96f59b2984b0d0371b2da0360d6e8d94d0b39a68 Mon Sep 17 00:00:00 2001 From: ghpzin Date: Sat, 10 Aug 2024 14:50:56 +0300 Subject: [PATCH 279/369] fix: Use correct assert with mock object --- tests/unit/test_bootloader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_bootloader.py b/tests/unit/test_bootloader.py index 8b375c5..708c437 100644 --- a/tests/unit/test_bootloader.py +++ b/tests/unit/test_bootloader.py @@ -213,7 +213,7 @@ def test_get_uid_for_known_family_reads_at_correct_address(connection, family): bootloader.read_memory = MagicMock() bootloader.get_uid() uid_address = bootloader.UID_ADDRESS[family] - assert bootloader.read_memory.called_once_with(uid_address) + bootloader.read_memory.assert_called_once_with(uid_address, 12) def test_get_uid_for_family_without_uid_returns_uid_not_supported(connection): From b560be9869755d74a1a4e406cadd211633c7c0e4 Mon Sep 17 00:00:00 2001 From: Alexander Mueller Date: Fri, 24 Oct 2025 19:01:29 +0200 Subject: [PATCH 280/369] Add support for STM32G4 --- stm32loader/bootloader.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index fa9cc01..a52b80f 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -37,6 +37,9 @@ 0x410: "STM32F10x Medium-density", 0x420: "STM32F10x Medium-density value line", 0x460: "STM32G0x1", + 0x468: "STM32G431xx/STM32G441xx", + # 128 to 512 KiB, + 0x469: "STM32G47xxx/48xxx", # 256 to 512 KiB (5128 Kbyte is probably a typo?) 0x414: "STM32F10x High-density", 0x428: "STM32F10x High-density value line", @@ -243,6 +246,8 @@ class Reply(enum.IntEnum): "WL": 0x1FFF7590, # ST BlueNRG has DIE_ID register with PRODUCT, but no UID. "NRG": None, + # ST RM0440 section 48.1 Unique device ID register (96 bits) + "G4": 0x1FFF7590, } UID_SWAP = [[1, 0], [3, 2], [7, 6, 5, 4], [11, 10, 9, 8]] @@ -282,6 +287,8 @@ class Reply(enum.IntEnum): "WL": 0x1FFF75E0, # ST BlueNRG-2 datasheet "NRG": 0x40100014, + # ST RM0440 section 48.2 Flash size data register + "G4": 0x1FFF75E0, } DATA_TRANSFER_SIZE = { @@ -299,6 +306,7 @@ class Reply(enum.IntEnum): "NRG": 256, # ST RM0433 section 4.2 FLASH main features "H7": 256, + "G4": 256, } FLASH_PAGE_SIZE = { @@ -330,6 +338,8 @@ class Reply(enum.IntEnum): "WL": 1024, # ST BlueNRG-2 data sheet: 128 pages of 8 * 64 * 4 bytes "NRG": 2048, + # ST RM0440 section 3.3.1 Flash memory organization + "G4": 2048, # this is valid only for dual bank mode # ST RM0433 section 4.2 FLASH main features "H7": 128 * 1024, } From bb8c25a003b879b8012889a9438217fbd6d86a74 Mon Sep 17 00:00:00 2001 From: Adam DeMuri Date: Tue, 17 Feb 2026 22:42:11 -0700 Subject: [PATCH 281/369] Add .venv to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 3493742..e0b9ca5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ build/ dist/ *.egg-info/ .tox/ +.venv __pycache__/ *.py[cod] From b2261c663867a20fd1fc4eeae44fa90cf64b342d Mon Sep 17 00:00:00 2001 From: Rolando Bosch Date: Wed, 25 Feb 2026 12:01:28 -0800 Subject: [PATCH 282/369] chore: replace flake8, black and isort with ruff Migrate linting and formatting from flake8 + flake8-isort + black to ruff, as requested in #71. Ruff provides equivalent linting rules (pycodestyle E/W, pyflakes F, isort I) and formatting in a single tool. Changes: - Add [tool.ruff] configuration to pyproject.toml preserving line-length=98, max-doc-length=78 and py39 target - Replace flake8/flake8-isort/black with ruff in dev dependencies - Update CI workflow (.github/workflows/lint.yaml) to use ruff; bump actions/checkout to v4 and actions/setup-python to v5 - Update noxfile.py lint session to use ruff - Update DEVELOP.md linting instructions - Delete .flake8 config file (settings migrated to pyproject.toml) - Apply ruff format and import sorting to source files Pylint is retained as-is since ruff does not fully replace it. Closes #71 Co-Authored-By: Claude Opus 4.6 --- .flake8 | 25 --------------------- .github/workflows/lint.yaml | 35 ++++++++++------------------- DEVELOP.md | 6 ++--- noxfile.py | 11 ++++------ pyproject.toml | 44 ++++++++++++++++++------------------- stm32loader/args.py | 1 - stm32loader/bootloader.py | 3 +-- stm32loader/main.py | 7 ++---- stm32loader/uart.py | 1 - 9 files changed, 42 insertions(+), 91 deletions(-) delete mode 100644 .flake8 diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 24ba8f9..0000000 --- a/.flake8 +++ /dev/null @@ -1,25 +0,0 @@ -[flake8] -max-line-length = 98 -max-doc-length = 78 -exclude = - .git, - .idea, - __pycache__, - build, - dist, - *.egg-info - -# be compatible to black -# Missing trailing comma -# Whitespace before ':' -# line break before binary operator -ignore = - C812, - E203, - W503, - -# Missing docstring in public function -# .next() is not a thing in Python 3 -per-file-ignores = - tests/*:D103, - stm32loader/bootloader.py:B305, diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 1beac03..7e349b1 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -9,7 +9,7 @@ defaults: shell: bash jobs: - black: + ruff: strategy: matrix: os: @@ -18,29 +18,16 @@ jobs: - "3.11" runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v2 - - uses: psf/black@stable - with: - options: "--check --verbose" - src: "./stm32loader" - flake8: - strategy: - fail-fast: true - matrix: - os: - - ubuntu-latest - python-version: - - "3.11" - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - - name: Install lint dependencies - run: pip install flake8 pyserial intelhex - - name: Run flake8 - run: flake8 stm32loader + - name: Install ruff + run: pip install ruff + - name: Run ruff format check + run: ruff format --check stm32loader + - name: Run ruff lint check + run: ruff check stm32loader pylint: strategy: fail-fast: true @@ -51,8 +38,8 @@ jobs: - "3.11" runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install lint dependencies diff --git a/DEVELOP.md b/DEVELOP.md index 03cbc2a..b897d5f 100644 --- a/DEVELOP.md +++ b/DEVELOP.md @@ -26,11 +26,11 @@ Run pytest. ## Linting -Run flake8, pylint and black. +Run ruff and pylint. - flake8 stm32loader + ruff check stm32loader + ruff format --check stm32loader pylint stm32loader - black --check stm32loader ## Commit messages diff --git a/noxfile.py b/noxfile.py index 4165c20..fd27e0f 100644 --- a/noxfile.py +++ b/noxfile.py @@ -31,19 +31,16 @@ def tests(session): @nox.session(python=DEFAULT_PYTHON_VERSION) def lint(session): """ - Run code verification tools flake8, pylint and black. + Run code verification tools ruff and pylint. Do this in order of expected failures for performance reasons. """ - session.install("black") - session.run("black", "--check", "stm32loader") + session.install("ruff") + session.run("ruff", "format", "--check", "stm32loader") + session.run("ruff", "check", "stm32loader") session.install("pylint") # pyserial for avoiding a complaint by pylint session.install("pyserial") session.install("intelhex") session.run("pylint", "stm32loader") - - session.install("flake8", "flake8-isort") - # not sure why this needs an explicit --config - session.run("flake8", "stm32loader", "--config=setup.cfg") diff --git a/pyproject.toml b/pyproject.toml index 1aa6eaf..97b0c54 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,9 +38,7 @@ dev = [ "wheel", "twine", "pylint", - "flake8", - "flake8-isort", - "black", + "ruff", "bump-my-version", "nox", "cogapp", @@ -84,23 +82,28 @@ search = "__version_info__ = {current_version}" replace = "__version_info__ = {new_version}" -[tool.black] +[tool.ruff] line-length = 98 -target-version = [ - "py39", - "py310", - "py311", +target-version = "py39" +exclude = [ + ".git", + ".idea", + "__pycache__", + "build", + "dist", + "*.egg-info", ] -exclude = ''' -/( - \.git - | \.idea - | __pycache__ - | build - | dist - | .*\.egg-info -)/ -''' + +[tool.ruff.lint] +# E/W: pycodestyle, F: pyflakes, I: isort +# Replaces flake8 + flake8-isort; ruff format replaces black +select = ["E", "F", "W", "I"] + +[tool.ruff.lint.pycodestyle] +max-doc-length = 78 + +[tool.ruff.format] +docstring-code-format = true [tool.pytest.ini_options] @@ -109,8 +112,3 @@ markers = [ "hardware", "missing_hardware", ] - - -[tool.isort] -line_length = 98 -multi_line_output = 2 diff --git a/stm32loader/args.py b/stm32loader/args.py index 893e3f5..0e1c8ab 100644 --- a/stm32loader/args.py +++ b/stm32loader/args.py @@ -8,7 +8,6 @@ from stm32loader import __version__ - DEFAULT_VERBOSITY = 5 diff --git a/stm32loader/bootloader.py b/stm32loader/bootloader.py index a52b80f..da9ccea 100644 --- a/stm32loader/bootloader.py +++ b/stm32loader/bootloader.py @@ -19,7 +19,6 @@ """Talk to an STM32 native bootloader (see ST AN3155).""" - import enum import math import operator @@ -339,7 +338,7 @@ class Reply(enum.IntEnum): # ST BlueNRG-2 data sheet: 128 pages of 8 * 64 * 4 bytes "NRG": 2048, # ST RM0440 section 3.3.1 Flash memory organization - "G4": 2048, # this is valid only for dual bank mode + "G4": 2048, # this is valid only for dual bank mode # ST RM0433 section 4.2 FLASH main features "H7": 128 * 1024, } diff --git a/stm32loader/main.py b/stm32loader/main.py index 3b2c754..a3823d0 100644 --- a/stm32loader/main.py +++ b/stm32loader/main.py @@ -20,19 +20,16 @@ """Flash firmware to STM32 microcontrollers over a serial connection.""" - import sys -from types import SimpleNamespace from pathlib import Path +from types import SimpleNamespace try: from progress.bar import ChargingBar as progress_bar except ImportError: progress_bar = None -from stm32loader import args -from stm32loader import hexfile -from stm32loader import bootloader +from stm32loader import args, bootloader, hexfile from stm32loader.uart import SerialConnection diff --git a/stm32loader/uart.py b/stm32loader/uart.py index 55de005..db26790 100644 --- a/stm32loader/uart.py +++ b/stm32loader/uart.py @@ -25,7 +25,6 @@ # Note: this file not named 'serial' because that name-clashed in Python 2 - import serial From 539c60e9a0677033a1a019f0fe7b715ab4058642 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Fri, 27 Feb 2026 15:41:11 +0100 Subject: [PATCH 283/369] dev: Port to uv SQUASH: Drop twine, wheel as deve depencies --- .gitignore | 1 + .python-version | 1 + pyproject.toml | 29 +++++++++---------- {stm32loader => src/stm32loader}/.gitignore | 0 {stm32loader => src/stm32loader}/__init__.py | 0 {stm32loader => src/stm32loader}/__main__.py | 0 {stm32loader => src/stm32loader}/args.py | 0 .../stm32loader}/bootloader.py | 0 {stm32loader => src/stm32loader}/devices.py | 0 {stm32loader => src/stm32loader}/hexfile.py | 0 {stm32loader => src/stm32loader}/main.py | 0 {stm32loader => src/stm32loader}/uart.py | 0 12 files changed, 16 insertions(+), 15 deletions(-) create mode 100644 .python-version rename {stm32loader => src/stm32loader}/.gitignore (100%) rename {stm32loader => src/stm32loader}/__init__.py (100%) rename {stm32loader => src/stm32loader}/__main__.py (100%) rename {stm32loader => src/stm32loader}/args.py (100%) rename {stm32loader => src/stm32loader}/bootloader.py (100%) rename {stm32loader => src/stm32loader}/devices.py (100%) rename {stm32loader => src/stm32loader}/hexfile.py (100%) rename {stm32loader => src/stm32loader}/main.py (100%) rename {stm32loader => src/stm32loader}/uart.py (100%) diff --git a/.gitignore b/.gitignore index e0b9ca5..09e66b6 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ dist/ __pycache__/ *.py[cod] .nox/ +uv.lock diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..6324d40 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.14 diff --git a/pyproject.toml b/pyproject.toml index 97b0c54..e7c89e2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,21 +1,13 @@ -[build-system] -requires = ["flit_core >=3.2,<4"] -build-backend = "flit_core.buildapi" - [project] name = "stm32loader" +description = "Flash firmware to STM32 microcontrollers using Python." +readme = "README.md" authors = [ {name = "jsnyder"}, {name = "Floris Lambrechts", email = "florisla@gmail.com"}, ] -readme = "README.md" -description = "Flash firmware to STM32 microcontrollers using Python." -license = {file = "LICENSE"} requires-python = ">=3.9" -dependencies = [ - "pyserial", - "progress", -] +license = {file = "LICENSE"} classifiers = [ "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", "Programming Language :: Python", @@ -23,22 +15,29 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Natural Language :: English", "Operating System :: OS Independent", ] -dynamic = ["version"] +dependencies = [ + "pyserial", + "progress", +] +version = "0.8.0" + +[build-system] +requires = ["uv_build>=0.9.17,<0.10.0"] +build-backend = "uv_build" [project.optional-dependencies] hex = [ "intelhex", ] dev = [ - "wheel", - "twine", - "pylint", "ruff", + "pylint", "bump-my-version", "nox", "cogapp", diff --git a/stm32loader/.gitignore b/src/stm32loader/.gitignore similarity index 100% rename from stm32loader/.gitignore rename to src/stm32loader/.gitignore diff --git a/stm32loader/__init__.py b/src/stm32loader/__init__.py similarity index 100% rename from stm32loader/__init__.py rename to src/stm32loader/__init__.py diff --git a/stm32loader/__main__.py b/src/stm32loader/__main__.py similarity index 100% rename from stm32loader/__main__.py rename to src/stm32loader/__main__.py diff --git a/stm32loader/args.py b/src/stm32loader/args.py similarity index 100% rename from stm32loader/args.py rename to src/stm32loader/args.py diff --git a/stm32loader/bootloader.py b/src/stm32loader/bootloader.py similarity index 100% rename from stm32loader/bootloader.py rename to src/stm32loader/bootloader.py diff --git a/stm32loader/devices.py b/src/stm32loader/devices.py similarity index 100% rename from stm32loader/devices.py rename to src/stm32loader/devices.py diff --git a/stm32loader/hexfile.py b/src/stm32loader/hexfile.py similarity index 100% rename from stm32loader/hexfile.py rename to src/stm32loader/hexfile.py diff --git a/stm32loader/main.py b/src/stm32loader/main.py similarity index 100% rename from stm32loader/main.py rename to src/stm32loader/main.py diff --git a/stm32loader/uart.py b/src/stm32loader/uart.py similarity index 100% rename from stm32loader/uart.py rename to src/stm32loader/uart.py From 1e4811d8024e9cc911bd16d2320bfd5393d7c49d Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Fri, 27 Feb 2026 15:40:48 +0100 Subject: [PATCH 284/369] doc: Mention uv as alternative to pip --- DEVELOP.md | 18 +++++++++++------- README.md | 7 ++++++- noxfile.py | 8 +++++++- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/DEVELOP.md b/DEVELOP.md index b897d5f..bd10434 100644 --- a/DEVELOP.md +++ b/DEVELOP.md @@ -7,8 +7,12 @@ Checkout the latest master. git clone https://github.com/florisla/stm32loader.git - -Install in editable mode with development tools (preferable in a virtual + +Using `uv` you can simply run the tool + + uv run stm32loader + +Otherwise, install in editable mode with development tools (preferable in a virtual environment). python -m venv .venv @@ -21,16 +25,16 @@ environment). Run pytest. - pytest . + uv run pytest . ## Linting Run ruff and pylint. - ruff check stm32loader - ruff format --check stm32loader - pylint stm32loader + uv run ruff check stm32loader + uv run ruff format --check stm32loader + uv run pylint stm32loader ## Commit messages @@ -41,7 +45,7 @@ see https://www.conventionalcommits.org/ . ## Bump the version number - bumpversion --new-version 1.0.8-dev bogus-part + bump-my-version --new-version 1.0.8-dev bogus-part ## Tag a release diff --git a/README.md b/README.md index 7638924..f5d7598 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,12 @@ for Wiznet W7500. Compatible with Python version 3.9 to 3.11 and PyPy 3.9. -## Installation +## Run using `uvx` + + uvx stm32loader + + +## Installation with pip pip install stm32loader diff --git a/noxfile.py b/noxfile.py index fd27e0f..e8a84b6 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1,4 +1,10 @@ -"""Run unit tests in a fresh virtualenv using nox.""" +""" +Run unit tests in a fresh virtualenv using nox. + +Usage: + + uv run nox +""" from shutil import rmtree From a3b4468e0214a295db7ecf5ae19448d1d9de9e6b Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Fri, 27 Feb 2026 15:56:29 +0100 Subject: [PATCH 285/369] dev: Use nox-uv too This simplifies the noxfile as well. --- noxfile.py | 44 +++++++++++++------------------------------- pyproject.toml | 13 +++++++++++-- 2 files changed, 24 insertions(+), 33 deletions(-) diff --git a/noxfile.py b/noxfile.py index e8a84b6..29f331c 100644 --- a/noxfile.py +++ b/noxfile.py @@ -6,47 +6,29 @@ uv run nox """ -from shutil import rmtree +from nox import Session, options +from nox_uv import session -import nox +options.default_venv_backend = "uv" +PYTHON_VERSIONS = ["3.9", "3.10", "3.11"] DEFAULT_PYTHON_VERSION = "3.11" -ALL_PYTHON_VERSIONS = ["3.9", "3.10", "3.11"] -@nox.session(python=ALL_PYTHON_VERSIONS) -def tests(session): - """ - Install stm32loader package and execute unit tests. +@session(python=PYTHON_VERSIONS, uv_groups=("test", "hex")) +def tests(session: Session) -> None: + """Execute unit tests.""" + session.run("pytest") - Use chdir to move off of the current folder, so that - 'import stm32loader' imports the *installed* package, not - the local one from the repo. - """ - # setuptools does not like multiple .whl packages being present - # see https://github.com/pypa/setuptools/issues/1671 - rmtree("./dist", ignore_errors=True) - session.install(".") - session.install("intelhex") - session.install("pytest") - session.chdir("tests") - session.run("pytest", "./") - - -@nox.session(python=DEFAULT_PYTHON_VERSION) -def lint(session): + +@session(python=DEFAULT_PYTHON_VERSION, uv_groups=("lint", )) +def lint(session: Session) -> None: """ Run code verification tools ruff and pylint. Do this in order of expected failures for performance reasons. """ - session.install("ruff") - session.run("ruff", "format", "--check", "stm32loader") - session.run("ruff", "check", "stm32loader") - - session.install("pylint") - # pyserial for avoiding a complaint by pylint - session.install("pyserial") - session.install("intelhex") + session.run("ruff", "format", "--check", "src/stm32loader") + session.run("ruff", "check", "src/stm32loader") session.run("pylint", "stm32loader") diff --git a/pyproject.toml b/pyproject.toml index e7c89e2..e6153f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,16 +31,25 @@ version = "0.8.0" requires = ["uv_build>=0.9.17,<0.10.0"] build-backend = "uv_build" -[project.optional-dependencies] +[dependency-groups] hex = [ "intelhex", ] +lint = [ + "ruff", + "pylint", +] +test = [ + "pytest", +] dev = [ + "pytest", "ruff", "pylint", "bump-my-version", - "nox", "cogapp", + "nox>=2026.2.9", + "nox-uv>=0.7.1", ] [project.scripts] From a071bb5775fbb22d7db5fda163f7c7f1fab2786f Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Fri, 27 Feb 2026 16:00:55 +0100 Subject: [PATCH 286/369] dev: Officially support Python 3.12/3.13/3.14 --- .github/workflows/lint.yaml | 4 ++-- .github/workflows/test.yaml | 3 +++ MANIFEST.in | 7 ------- noxfile.py | 4 ++-- pyproject.toml | 2 ++ 5 files changed, 9 insertions(+), 11 deletions(-) delete mode 100644 MANIFEST.in diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 7e349b1..ca9708f 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -15,7 +15,7 @@ jobs: os: - ubuntu-latest python-version: - - "3.11" + - "3.14" runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -35,7 +35,7 @@ jobs: os: - ubuntu-latest python-version: - - "3.11" + - "3.14" runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 5c23e58..7388795 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -20,6 +20,9 @@ jobs: - "3.9" - "3.10" - "3.11" + - "3.12" + - "3.13" + - "3.14" - "pypy-3.9" runs-on: ${{ matrix.os }} steps: diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 832d124..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,7 +0,0 @@ -include *.md -include *.cfg -include *.toml -include LICENSE -include .pylintrc -include noxfile.py -recursive-include tests *.py diff --git a/noxfile.py b/noxfile.py index 29f331c..1de5fc3 100644 --- a/noxfile.py +++ b/noxfile.py @@ -12,8 +12,8 @@ options.default_venv_backend = "uv" -PYTHON_VERSIONS = ["3.9", "3.10", "3.11"] -DEFAULT_PYTHON_VERSION = "3.11" +PYTHON_VERSIONS = ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] +DEFAULT_PYTHON_VERSION = "3.14" @session(python=PYTHON_VERSIONS, uv_groups=("test", "hex")) diff --git a/pyproject.toml b/pyproject.toml index e6153f7..58a46cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,8 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Natural Language :: English", From 88d43deb5f555dfa5b22a8f4062a6a6979593e2f Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Fri, 27 Feb 2026 16:01:10 +0100 Subject: [PATCH 287/369] doc: Add placeholders to changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 356b002..5f91951 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,11 @@ What changed in which version. +## vnext + +uv, nox, bump-my-version, STM32G4, Python 3.12/3.13/3.14 + + ## [0.7.0] - 2023-10-12 ### Added From a6dd222b45d45390226cb777ad4c033a6519a040 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Fri, 27 Feb 2026 16:03:57 +0100 Subject: [PATCH 288/369] fix(ci): Make pylint and Tox work again in CI --- .github/workflows/lint.yaml | 6 +++--- src/stm32loader/args.py | 2 +- tox.ini | 3 +++ 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index ca9708f..179590b 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -25,9 +25,9 @@ jobs: - name: Install ruff run: pip install ruff - name: Run ruff format check - run: ruff format --check stm32loader + run: ruff format --check src/stm32loader - name: Run ruff lint check - run: ruff check stm32loader + run: ruff check src/stm32loader pylint: strategy: fail-fast: true @@ -45,4 +45,4 @@ jobs: - name: Install lint dependencies run: pip install pylint pyserial intelhex - name: Run pylint - run: pylint stm32loader + run: pylint src/stm32loader diff --git a/src/stm32loader/args.py b/src/stm32loader/args.py index 0e1c8ab..4710ecb 100644 --- a/src/stm32loader/args.py +++ b/src/stm32loader/args.py @@ -32,7 +32,7 @@ def tweak_action(action): action.required = True return action - return super()._format_actions_usage(map(tweak_action, actions), groups) + return super()._format_actions_usage(map(tweak_action, actions), groups) # pylint: disable=no-member def _auto_int(x): diff --git a/tox.ini b/tox.ini index 5ff6b13..b2e16b0 100644 --- a/tox.ini +++ b/tox.ini @@ -19,4 +19,7 @@ python = 3.9: py39 3.10: py310 3.11: py311 + 3.12: py312 + 3.13: py313 + 3.14: py314 pypy-3.9: pypy39 From 5fb7bfc23529ca1b4a496e692b6584d54c1c7ef2 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Fri, 27 Feb 2026 16:38:18 +0100 Subject: [PATCH 289/369] doc: Promote the STM32G4 family as supported --- README.md | 25 ++++++++++--------------- src/stm32loader/args.py | 2 +- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index f5d7598..d44c5d8 100644 --- a/README.md +++ b/README.md @@ -47,9 +47,7 @@ sys.stdout.close() sys.stdout = sys.__stdout__ ]]] --> ``` -usage: stm32loader [-h] [-e] [-u] [-x] [-w] [-v] [-r] [-l LENGTH] -p PORT [-b BAUD] [-a ADDRESS] [-g ADDRESS] [-f FAMILY] - [-V] [-q] [-s] [-R] [-B] [-n] [-P {even,none}] [--version] - [FILE.BIN] +usage: stm32loader [-h] [-e] [-u] [-x] [-w] [-v] [-r] [-l LENGTH] -p PORT [-b BAUD] [-a ADDRESS] [-g ADDRESS] [-f FAMILY] [-V] [-q] [-s] [-R] [-B] [-n] [-P {even,none}] [--version] [FILE.BIN] Flash firmware to STM32 microcontrollers. @@ -58,23 +56,20 @@ positional arguments: options: -h, --help show this help message and exit - -e, --erase Erase (note: this is required on previously written memory). + -e, --erase Erase the full flash memory or a specific region (support --address and --length). Note: this is required on previously written memory. -u, --unprotect Unprotect flash from readout. -x, --protect Protect flash against readout. -w, --write Write file content to flash. -v, --verify Verify flash content versus local file (recommended). -r, --read Read from flash and store in local file. - -l LENGTH, --length LENGTH - Length of read or erase. - -p PORT, --port PORT Serial port (default: $STM32LOADER_SERIAL_PORT). - -b BAUD, --baud BAUD Baudrate. (default: 115200) - -a ADDRESS, --address ADDRESS - Target address for read, write or erase. (default: 134217728) - -g ADDRESS, --go-address ADDRESS + -l, --length LENGTH Length of read or erase. + -p, --port PORT Serial port (default: $STM32LOADER_SERIAL_PORT). + -b, --baud BAUD Baudrate. (default: 115200) + -a, --address ADDRESS + Target address for read or write. For erase, this is used when you supply --length. (default: 134217728) + -g, --go-address ADDRESS Start executing from address (0x08000000, usually). - -f FAMILY, --family FAMILY - Device family to read out device UID and flash size; e.g F1 for STM32F1xx. Possible values: F0, - F1, F3, F4, F7, H7, L4, L0, G0, NRG. (default: $STM32LOADER_FAMILY). + -f, --family FAMILY Device family to read out device UID and flash size; e.g F1 for STM32F1xx. Possible values: F0, F1, F3, F4, F7, H7, L4, L0, G0, G4, NRG. (default: $STM32LOADER_FAMILY). -V, --verbose Verbose mode. -q, --quiet Quiet mode. -s, --swap-rts-dtr Swap RTS and DTR: use RTS for reset and DTR for boot0. @@ -83,7 +78,7 @@ options: -B, --boot0-active-low Make BOOT0 active low. -n, --no-progress Don't show progress bar. - -P {even,none}, --parity {even,none} + -P, --parity {even,none} Parity: "even" for STM32, "none" for BlueNRG. (default: even) --version show program's version number and exit diff --git a/src/stm32loader/args.py b/src/stm32loader/args.py index 4710ecb..eef78e7 100644 --- a/src/stm32loader/args.py +++ b/src/stm32loader/args.py @@ -143,7 +143,7 @@ def parse_arguments(arguments): default=default_family, help=( "Device family to read out device UID and flash size; " - "e.g F1 for STM32F1xx. Possible values: F0, F1, F3, F4, F7, H7, L4, L0, G0, NRG." + "e.g F1 for STM32F1xx. Possible values: F0, F1, F3, F4, F7, H7, L4, L0, G0, G4, NRG." + ("." if default_family else " (default: $STM32LOADER_FAMILY).") ), ) From 100223dcdd270e88025b7762040187f262cbfc9e Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Fri, 27 Feb 2026 16:38:32 +0100 Subject: [PATCH 290/369] doc: Mention stm32isp.py as an alternative tool --- ALTERNATIVES.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ALTERNATIVES.md b/ALTERNATIVES.md index 3f5b790..557bf4d 100644 --- a/ALTERNATIVES.md +++ b/ALTERNATIVES.md @@ -52,6 +52,13 @@ Java library allowing to flash STM32 microcontrollers over UART. https://github.com/grevaillot/stm32flash-lib +## stm32isp.py + +Simple, more limited too to flash STM32 over UART in Python. + +https://github.com/wagiminator/MCU-Flash-Tools?tab=readme-ov-file#stm32isp + + ## GigaDevice tools GigaDevice offers the GD-Link Programmer to work with the GD-Link debug adapter, From 9794dc9ad16986288b3acdc5cf37a391e80523da Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Fri, 27 Feb 2026 16:39:37 +0100 Subject: [PATCH 291/369] doc: Mention how to update --help info in the README --- DEVELOP.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/DEVELOP.md b/DEVELOP.md index bd10434..2e2ba4d 100644 --- a/DEVELOP.md +++ b/DEVELOP.md @@ -35,7 +35,12 @@ Run ruff and pylint. uv run ruff check stm32loader uv run ruff format --check stm32loader uv run pylint stm32loader - + + +## Updating --help info in the README + + uv run cog -r README.md + ## Commit messages From a179e7d01001571a053faa13f422c88ef3542908 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Fri, 27 Feb 2026 19:22:55 +0100 Subject: [PATCH 292/369] dev: Test against PyPy up to 3.11 --- .github/workflows/test.yaml | 2 ++ README.md | 2 +- noxfile.py | 2 +- tox.ini | 5 +++-- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 7388795..19b025b 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -24,6 +24,8 @@ jobs: - "3.13" - "3.14" - "pypy-3.9" + - "pypy-3.10" + - "pypy-3.11" runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v2 diff --git a/README.md b/README.md index d44c5d8..aa255a9 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ ST Microelectronics STM32 microcontrollers over UART. Also supports ST BlueNRG devices, and the SweetPeas bootloader for Wiznet W7500. -Compatible with Python version 3.9 to 3.11 and PyPy 3.9. +Compatible with Python version 3.9-3.14 and PyPy 3.10-3.11. ## Run using `uvx` diff --git a/noxfile.py b/noxfile.py index 1de5fc3..a022f7c 100644 --- a/noxfile.py +++ b/noxfile.py @@ -12,7 +12,7 @@ options.default_venv_backend = "uv" -PYTHON_VERSIONS = ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] +PYTHON_VERSIONS = ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "pypy-3.10", "pypy-3.11"] DEFAULT_PYTHON_VERSION = "3.14" diff --git a/tox.ini b/tox.ini index b2e16b0..d9cac12 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py39, py310, py311, pypy39 +envlist = py39, py310, py311, pypy39, pypy310, pypy311 [testenv] passenv = HOME @@ -22,4 +22,5 @@ python = 3.12: py312 3.13: py313 3.14: py314 - pypy-3.9: pypy39 + pypy-3.10: pypy310 + pypy-3.11: pypy311 From c8fa4071ffd0afb3e54e8867c551c710738c47b3 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 18 Oct 2023 21:47:13 +0200 Subject: [PATCH 293/369] doc: Describe release v0.7.1 --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f91951..ce87668 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,17 @@ What changed in which version. uv, nox, bump-my-version, STM32G4, Python 3.12/3.13/3.14 +## [0.7.1] - 2023-10-18 + +### Fixed +* Integer division error when erasing specific region of flash. +* Print erase/write/verify in correct order. + + +### Cleaned +* Extract method for range-to-pages calculation. + + ## [0.7.0] - 2023-10-12 ### Added From 5a0d2aa25644f374dca7a42648c05054fbdfaab2 Mon Sep 17 00:00:00 2001 From: Adam DeMuri Date: Fri, 27 Feb 2026 13:19:10 -0700 Subject: [PATCH 294/369] Ensure that intelhex is installed before running tests. This adds the `intelhex` dependency to the `test` group, and updates the `dev` group to reference the other groups with `include-group`. --- pyproject.toml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 58a46cc..39cc527 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,11 +43,12 @@ lint = [ ] test = [ "pytest", + "intelhex", ] dev = [ - "pytest", - "ruff", - "pylint", + { include-group = "test" }, + { include-group = "hex" }, + { include-group = "lint" }, "bump-my-version", "cogapp", "nox>=2026.2.9", From e1557d52451dcef746f0350ab49a457ce62b8c16 Mon Sep 17 00:00:00 2001 From: Adam DeMuri Date: Fri, 27 Feb 2026 13:49:30 -0700 Subject: [PATCH 295/369] Apply ruff fixes. This is the result of running `uv run ruff format .` and `uv run ruff check --fix .`. --- noxfile.py | 2 +- tests/integration/test_stm32bootloader.py | 4 +- tests/integration/test_stm32loader.py | 25 +++-- tests/unit/test_arguments.py | 4 +- tests/unit/test_bootloader.py | 110 +++++++++++++--------- tests/unit/test_hexfile.py | 1 - 6 files changed, 89 insertions(+), 57 deletions(-) diff --git a/noxfile.py b/noxfile.py index a022f7c..f943359 100644 --- a/noxfile.py +++ b/noxfile.py @@ -22,7 +22,7 @@ def tests(session: Session) -> None: session.run("pytest") -@session(python=DEFAULT_PYTHON_VERSION, uv_groups=("lint", )) +@session(python=DEFAULT_PYTHON_VERSION, uv_groups=("lint",)) def lint(session: Session) -> None: """ Run code verification tools ruff and pylint. diff --git a/tests/integration/test_stm32bootloader.py b/tests/integration/test_stm32bootloader.py index 6cb3b6b..b4443e4 100644 --- a/tests/integration/test_stm32bootloader.py +++ b/tests/integration/test_stm32bootloader.py @@ -12,11 +12,11 @@ """ +import pytest + from stm32loader.bootloader import Stm32Bootloader from stm32loader.uart import SerialConnection -import pytest - SERIAL_PORT = "COM7" BAUD_RATE = 9600 diff --git a/tests/integration/test_stm32loader.py b/tests/integration/test_stm32loader.py index 5127524..de97ba4 100644 --- a/tests/integration/test_stm32loader.py +++ b/tests/integration/test_stm32loader.py @@ -29,7 +29,7 @@ SERIAL_PORT = "COM7" # Flaky cable setup, cheap serial adapter... BAUD_RATE = 9600 -KBYTE = 2 ** 10 +KBYTE = 2**10 SIZE = 32 * KBYTE DUMP_FILE = "dump.bin" FIRMWARE_FILE = os.path.join(HERE, "../../firmware/generic_boot20_pc13.binary.bin") @@ -40,7 +40,16 @@ @pytest.fixture(scope="module") def stm32loader(): def main_with_default_arguments(*args): - main("--port", SERIAL_PORT, "--baud", str(BAUD_RATE), "--quiet", *args, avoid_system_exit=True) + main( + "--port", + SERIAL_PORT, + "--baud", + str(BAUD_RATE), + "--quiet", + *args, + avoid_system_exit=True, + ) + return main_with_default_arguments @@ -57,22 +66,22 @@ def test_unexisting_serial_port_prints_readable_error(capsys): main("-p", "COM108", avoid_system_exit=True) captured = capsys.readouterr() assert "could not open port " in captured.err - assert ("port 'COM108'" in captured.err or "port COM108" in captured.err) + assert "port 'COM108'" in captured.err or "port COM108" in captured.err assert "Is the device connected and powered correctly?" in captured.err def test_env_var_stm32loader_serial_port_defines_port(capsys): - os.environ['STM32LOADER_SERIAL_PORT'] = "COM109" + os.environ["STM32LOADER_SERIAL_PORT"] = "COM109" main(avoid_system_exit=True) captured = capsys.readouterr() - assert ("port 'COM109'" in captured.err or "port COM109" in captured.err) + assert "port 'COM109'" in captured.err or "port COM109" in captured.err def test_argument_port_overrules_env_var_for_serial_port(capsys): - os.environ['STM32LOADER_SERIAL_PORT'] = "COM120" + os.environ["STM32LOADER_SERIAL_PORT"] = "COM120" main("--port", "COM121", avoid_system_exit=True) captured = capsys.readouterr() - assert ("port 'COM121'" in captured.err or "port COM121" in captured.err) + assert "port 'COM121'" in captured.err or "port COM121" in captured.err @pytest.mark.hardware @@ -130,5 +139,3 @@ def test_write_saves_correct_data(stm32loader, dump_file): @pytest.mark.hardware def test_erase_write_verify_passes(stm32loader): stm32loader("--erase", "--write", "--verify", FIRMWARE_FILE) - - diff --git a/tests/unit/test_arguments.py b/tests/unit/test_arguments.py index 3a83f4c..d6d7551 100644 --- a/tests/unit/test_arguments.py +++ b/tests/unit/test_arguments.py @@ -1,4 +1,3 @@ - import atexit import pytest @@ -21,7 +20,8 @@ def test_parse_arguments_with_standard_args_passes(program): @pytest.mark.parametrize( - "help_argument", ["-h", "--help"], + "help_argument", + ["-h", "--help"], ) def test_parse_arguments_with_help_raises_systemexit(program, help_argument): with pytest.raises(SystemExit): diff --git a/tests/unit/test_bootloader.py b/tests/unit/test_bootloader.py index 708c437..d101edc 100644 --- a/tests/unit/test_bootloader.py +++ b/tests/unit/test_bootloader.py @@ -1,11 +1,11 @@ """Unit tests for the Stm32Loader class.""" -import pytest - from unittest.mock import MagicMock +import pytest + from stm32loader import bootloader as Stm32 -from stm32loader.bootloader import Stm32Bootloader, PageIndexError +from stm32loader.bootloader import PageIndexError, Stm32Bootloader # pylint: disable=missing-docstring, redefined-outer-name @@ -52,13 +52,13 @@ def test_write_without_data_sends_no_bytes(bootloader, write): def test_write_with_bytes_sends_bytes_verbatim(bootloader, write): - bootloader.write(b'\x00\x11') - assert write.data_was_written(b'\x00\x11') + bootloader.write(b"\x00\x11") + assert write.data_was_written(b"\x00\x11") def test_write_with_integers_sends_integers_as_bytes(bootloader, write): - bootloader.write(0x03, 0x0a) - assert write.data_was_written(b'\x03\x0a') + bootloader.write(0x03, 0x0A) + assert write.data_was_written(b"\x03\x0a") def test_write_and_ack_with_nack_response_raises_commandexception(bootloader): @@ -69,7 +69,9 @@ def test_write_and_ack_with_nack_response_raises_commandexception(bootloader): def test_write_memory_with_length_higher_than_256_raises_data_length_error(bootloader): - with pytest.raises(Stm32.DataLengthError, match=r"Can not write more than 256 bytes at once\."): + with pytest.raises( + Stm32.DataLengthError, match=r"Can not write more than 256 bytes at once\." + ): bootloader.write_memory(0, [1] * 257) @@ -92,18 +94,20 @@ def test_write_memory_sends_correct_number_of_bytes(bootloader, write): def test_read_memory_with_length_higher_than_256_raises_data_length_error(bootloader): - with pytest.raises(Stm32.DataLengthError, match=r"Can not read more than 256 bytes at once\."): + with pytest.raises( + Stm32.DataLengthError, match=r"Can not read more than 256 bytes at once\." + ): bootloader.read_memory(0, length=257) def test_read_memory_sends_address_with_checksum(bootloader, write): - bootloader.read_memory(0x0f, 4) - assert write.data_was_written(b'\x00\x00\x00\x0f\x0f') + bootloader.read_memory(0x0F, 4) + assert write.data_was_written(b"\x00\x00\x00\x0f\x0f") def test_read_memory_sends_length_with_checksum(bootloader, write): - bootloader.read_memory(0, 0x0f + 1) - assert write.data_was_written(b'\x0f\xf0') + bootloader.read_memory(0, 0x0F + 1) + assert write.data_was_written(b"\x0f\xf0") def test_command_sends_command_and_control_bytes(bootloader, write): @@ -125,25 +129,27 @@ def test_encode_address_returns_correct_bytes_with_checksum(): def test_erase_memory_without_pages_sends_global_erase(bootloader, write): bootloader.erase_memory() - assert write.data_was_written(b'\xff\x00') + assert write.data_was_written(b"\xff\x00") def test_erase_memory_with_pages_sends_sector_count_and_eight_bit_page_indices(bootloader, write): bootloader.erase_memory([0x11, 0x12, 0x13, 0x14]) - assert write.data_was_written(b'\x03') - assert write.data_was_written(b'\x11\x12\x13\x14') + assert write.data_was_written(b"\x03") + assert write.data_was_written(b"\x11\x12\x13\x14") -def test_extended_erase_memory_with_pages_sends_sector_count_and_sixteen_bit_page_indices(bootloader, write): +def test_extended_erase_memory_with_pages_sends_sector_count_and_sixteen_bit_page_indices( + bootloader, write +): bootloader.extended_erase_memory([0x11, 0x12, 0x13, 0x14]) - assert write.data_was_written(b'\x00\x03') - assert write.data_was_written(b'\x00\x11\x00\x12\x00\x13\x00\x14') + assert write.data_was_written(b"\x00\x03") + assert write.data_was_written(b"\x00\x11\x00\x12\x00\x13\x00\x14") def test_erase_memory_with_pages_sends_sector_addresses_with_checksum(bootloader, write): bootloader.erase_memory([0x01, 0x02, 0x04, 0x08]) print(write.written_data) - assert write.data_was_written(b'\x01\x02\x04\x08\x0c') + assert write.data_was_written(b"\x01\x02\x04\x08\x0c") def test_erase_memory_with_page_count_higher_than_255_raises_page_index_error(bootloader): @@ -161,52 +167,64 @@ def test_erase_memory_family_l0_without_pages_erases_individual_pages(connection # Page count - 1. assert write.written_data[0] == 127 # Pages. - assert write.written_data[1:3] == b'\x00\x01' + assert write.written_data[1:3] == b"\x00\x01" # Length: command + byte count + page-addresses + CRC assert len(write.written_data) == 130 def test_extended_erase_memory_without_pages_sends_global_mass_erase(bootloader, write): bootloader.extended_erase_memory() - assert write.data_was_written(b'\xff\xff\x00') + assert write.data_was_written(b"\xff\xff\x00") -def test_extended_erase_memory_with_page_count_higher_than_65535_raises_page_index_error(bootloader): - with pytest.raises(Stm32.PageIndexError, match="Can not erase more than 65535 pages at once."): +def test_extended_erase_memory_with_page_count_higher_than_65535_raises_page_index_error( + bootloader, +): + with pytest.raises( + Stm32.PageIndexError, match="Can not erase more than 65535 pages at once." + ): bootloader.extended_erase_memory([1] * 65536) def test_extended_erase_memory_with_pages_sends_two_byte_sector_count(bootloader, write): bootloader.extended_erase_memory([0x11, 0x12, 0x13, 0x14]) - assert write.data_was_written(b'\x00\x03') + assert write.data_was_written(b"\x00\x03") -def test_extended_erase_memory_with_pages_sends_two_byte_sector_addresses_with_single_byte_checksum(bootloader, write): - bootloader.extended_erase_memory([0x01, 0x02, 0x04, 0x0ff0]) - assert write.data_was_written(b'\x00\x01\x00\x02\x00\x04\x0f\xf0\xfb') +def test_extended_erase_memory_with_pages_sends_two_byte_sector_addresses_with_single_byte_checksum( + bootloader, write +): + bootloader.extended_erase_memory([0x01, 0x02, 0x04, 0x0FF0]) + assert write.data_was_written(b"\x00\x01\x00\x02\x00\x04\x0f\xf0\xfb") def test_write_protect_sends_page_addresses_and_checksum(bootloader, write): bootloader.write_protect([0x01, 0x08]) - assert write.data_was_written(b'\x01\x08\x08') + assert write.data_was_written(b"\x01\x08\x08") def test_verify_data_with_identical_data_passes(): - Stm32Bootloader.verify_data(b'\x05', b'\x05') + Stm32Bootloader.verify_data(b"\x05", b"\x05") def test_verify_data_with_different_byte_count_raises_verify_error_complaining_about_length_difference(): - with pytest.raises(Stm32.DataMismatchError, match=r"Data length does not match.*2.*vs.*1.*bytes"): - Stm32Bootloader.verify_data(b'\x05\x06', b'\x01') + with pytest.raises( + Stm32.DataMismatchError, match=r"Data length does not match.*2.*vs.*1.*bytes" + ): + Stm32Bootloader.verify_data(b"\x05\x06", b"\x01") def test_verify_data_with_non_identical_data_raises_verify_error_complaining_about_mismatched_byte(): - with pytest.raises(Stm32.DataMismatchError, match=r"Verification data does not match read data.*mismatch.*0x1.*0x6.*0x7"): - Stm32Bootloader.verify_data(b'\x05\x06', b'\x05\x07') + with pytest.raises( + Stm32.DataMismatchError, + match=r"Verification data does not match read data.*mismatch.*0x1.*0x6.*0x7", + ): + Stm32Bootloader.verify_data(b"\x05\x06", b"\x05\x07") @pytest.mark.parametrize( - "family", ["F1", "F3", "F7"], + "family", + ["F1", "F3", "F7"], ) def test_get_uid_for_known_family_reads_at_correct_address(connection, family): bootloader = Stm32Bootloader(connection, device_family=family) @@ -227,7 +245,8 @@ def test_get_uid_for_unknown_family_returns_uid_address_unknown(connection): @pytest.mark.parametrize( - "family", ["F4", "L0"], + "family", + ["F4", "L0"], ) def test_get_flash_size_and_uid_for_exception_families_returns_size_and_uid(connection, family): bootloader = Stm32Bootloader(connection, device_family=family) @@ -238,15 +257,17 @@ def test_get_flash_size_and_uid_for_exception_families_returns_size_and_uid(conn # Set up the 'UID' value (12 bytes) # and flash_size value (2 bytes). uid_address = bootloader.UID_ADDRESS[family] & 0xFF - memory_block[uid_address: uid_address + 12] = b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c' + memory_block[uid_address : uid_address + 12] = ( + b"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c" + ) flash_size_address = bootloader.FLASH_SIZE_ADDRESS[family] & 0xFF - memory_block[flash_size_address: flash_size_address + 2] = b'\x01\x02' + memory_block[flash_size_address : flash_size_address + 2] = b"\x01\x02" bootloader.read_memory.return_value = memory_block flash_size, uid = bootloader.get_flash_size_and_uid() assert flash_size == 0x0201 - assert uid == b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c' + assert uid == b"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c" @pytest.mark.parametrize( @@ -254,7 +275,10 @@ def test_get_flash_size_and_uid_for_exception_families_returns_size_and_uid(conn [ (0, "UID not supported in this part"), (-1, "UID address unknown"), - (bytearray(b"\x12\x34\x56\x78\x9a\xbc\xde\x01\x12\x34\x56\x78"), "3412-7856-01DEBC9A-78563412"), + ( + bytearray(b"\x12\x34\x56\x78\x9a\xbc\xde\x01\x12\x34\x56\x78"), + "3412-7856-01DEBC9A-78563412", + ), ], ) def test_format_uid_returns_correct_string(bootloader, uid_string): @@ -264,7 +288,9 @@ def test_format_uid_returns_correct_string(bootloader, uid_string): def test_get_pages_from_range_with_invalid_start_address_raises_page_index_error(bootloader): - with pytest.raises(PageIndexError, match=".*start address should be at a flash page boundary.*"): + with pytest.raises( + PageIndexError, match=".*start address should be at a flash page boundary.*" + ): bootloader.pages_from_range(10, 1024) @@ -274,5 +300,5 @@ def test_get_pages_from_range_with_start_address_zero_returns_single_page(bootlo def test_get_pages_from_large_range_returns_multiple_pages(bootloader): - pages = bootloader.pages_from_range(5*1024, 20*1024) + pages = bootloader.pages_from_range(5 * 1024, 20 * 1024) assert pages == list(range(5, 20)) diff --git a/tests/unit/test_hexfile.py b/tests/unit/test_hexfile.py index 4b81d3d..4af4c09 100644 --- a/tests/unit/test_hexfile.py +++ b/tests/unit/test_hexfile.py @@ -1,4 +1,3 @@ - from pathlib import Path HERE = Path(__file__).parent From 59f771557a4dc85ae5259f037f5af46965a5222f Mon Sep 17 00:00:00 2001 From: Adam DeMuri Date: Fri, 27 Feb 2026 13:51:03 -0700 Subject: [PATCH 296/369] Fix the paths for ruff and pylint in DEVELOP.md. --- DEVELOP.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/DEVELOP.md b/DEVELOP.md index 2e2ba4d..2d1e2f7 100644 --- a/DEVELOP.md +++ b/DEVELOP.md @@ -32,9 +32,9 @@ Run pytest. Run ruff and pylint. - uv run ruff check stm32loader - uv run ruff format --check stm32loader - uv run pylint stm32loader + uv run ruff check . + uv run ruff format --check . + uv run pylint . ## Updating --help info in the README From 88be0b4b57e50151df96e3f238721656509f03f3 Mon Sep 17 00:00:00 2001 From: Adam DeMuri Date: Thu, 19 Feb 2026 04:35:45 +0000 Subject: [PATCH 297/369] refactor(bootloader): unify flash size and UID retrieval logic --- src/stm32loader/bootloader.py | 79 ++++++++++++++++------------- src/stm32loader/main.py | 8 +-- tests/unit/test_bootloader.py | 93 ++++++++++++++++++++++++++++++++--- 3 files changed, 134 insertions(+), 46 deletions(-) diff --git a/src/stm32loader/bootloader.py b/src/stm32loader/bootloader.py index da9ccea..ac234e1 100644 --- a/src/stm32loader/bootloader.py +++ b/src/stm32loader/bootloader.py @@ -25,7 +25,7 @@ import struct import sys import time -from functools import reduce +from functools import lru_cache, reduce CHIP_IDS = { # see ST AN2606 Table 136 Bootloader device-dependent parameters @@ -479,20 +479,55 @@ def get_id(self): return _device_id def get_flash_size(self): - """Return the MCU's flash size in bytes.""" + """Return the MCU's flash size in kilobytes.""" + if self.device_family in ["F4", "L0"]: + flash_size, _uid = self._get_flash_size_and_uid_bulk() + return flash_size + return self._get_flash_size_raw() + + def get_uid(self): + """ + Send the 'Get UID' command and return the device UID. + + Return UID_NOT_SUPPORTED if the device does not have + a UID. + Return UIT_ADDRESS_UNKNOWN if the address of the device's + UID is not known. + + :return bytearray: UID bytes of the device, or 0 or -1 when + not available. + """ + if self.device_family in ["F4", "L0"]: + _flash_size, uid = self._get_flash_size_and_uid_bulk() + return uid + return self._get_uid_raw() + + @lru_cache(maxsize=2) + def _get_flash_size_raw(self): + """Perform a direct 2-byte read of the flash size.""" flash_size_address = self.FLASH_SIZE_ADDRESS[self.device_family] flash_size_bytes = self.read_memory(flash_size_address, 2) flash_size = flash_size_bytes[0] + (flash_size_bytes[1] << 8) return flash_size - def get_flash_size_and_uid(self): + @lru_cache(maxsize=2) + def _get_uid_raw(self): + """Perform a direct 12-byte read of the device UID.""" + uid_address = self.UID_ADDRESS.get(self.device_family, self.UID_ADDRESS_UNKNOWN) + if uid_address is None: + return self.UID_NOT_SUPPORTED + if uid_address == self.UID_ADDRESS_UNKNOWN: + return self.UID_ADDRESS_UNKNOWN + + uid = self.read_memory(uid_address, 12) + return uid + + @lru_cache(maxsize=2) + def _get_flash_size_and_uid_bulk(self): """ - Return device_uid and flash_size for L0 and F4 family devices. + Return device_uid and flash_size using a 256-byte bulk read. - For some reason, F4 (at least, NUCLEO F401RE) can't read the 12 or 2 - bytes for UID and flash size directly. - Reading a whole chunk of 256 bytes at 0x1FFFA700 does work and - requires some data extraction. + This workaround is used for F4 and L0 families. """ flash_size_address = self.FLASH_SIZE_ADDRESS[self.device_family] uid_address = self.UID_ADDRESS.get(self.device_family) @@ -508,9 +543,6 @@ def get_flash_size_and_uid(self): self.debug(10, "flash_size_address = 0x%X" % flash_size_address) self.debug(10, "uid_address = 0x%X" % uid_address) - # self.debug(10, 'data_start_address =0x%X' % data_start_address) - # self.debug(10, 'flashsizelsbaddress =0x%X' % flash_size_lsb_address) - # self.debug(10, 'uid_lsb_address = 0x%X' % uid_lsb_address) data = self.read_memory(data_start_address, self.data_transfer_size) device_uid = data[uid_lsb_address : uid_lsb_address + 12] @@ -518,27 +550,6 @@ def get_flash_size_and_uid(self): return flash_size, device_uid - def get_uid(self): - """ - Send the 'Get UID' command and return the device UID. - - Return UID_NOT_SUPPORTED if the device does not have - a UID. - Return UIT_ADDRESS_UNKNOWN if the address of the device's - UID is not known. - - :return byterary: UID bytes of the device, or 0 or -1 when - not available. - """ - uid_address = self.UID_ADDRESS.get(self.device_family, self.UID_ADDRESS_UNKNOWN) - if uid_address is None: - return self.UID_NOT_SUPPORTED - if uid_address == self.UID_ADDRESS_UNKNOWN: - return self.UID_ADDRESS_UNKNOWN - - uid = self.read_memory(uid_address, 12) - return uid - @classmethod def format_uid(cls, uid): """Return a readable string from the given UID.""" @@ -617,7 +628,7 @@ def erase_memory(self, pages=None): if not pages and self.device_family == "L0": # Special case: L0 erase should do each page separately. - flash_size, _uid = self.get_flash_size_and_uid() + flash_size = self.get_flash_size() page_count = (flash_size * 1024) // self.flash_page_size if page_count > 255: raise PageIndexError("Can not erase more than 255 pages for L0 family.") @@ -654,7 +665,7 @@ def extended_erase_memory(self, pages=None): if not pages and self.device_family in ("L0",): # L0 devices do not support mass erase. # Instead, erase all pages individually. - flash_size, _uid = self.get_flash_size_and_uid() + flash_size = self.get_flash_size() pages = list(range(0, (flash_size * 1024) // self.flash_page_size)) self.command(self.Command.EXTENDED_ERASE, "Extended erase memory") diff --git a/src/stm32loader/main.py b/src/stm32loader/main.py index a3823d0..826dc29 100644 --- a/src/stm32loader/main.py +++ b/src/stm32loader/main.py @@ -204,12 +204,8 @@ def read_device_uid(self): return try: - if family not in ["F4", "L0"]: - flash_size = self.stm32.get_flash_size() - device_uid = self.stm32.get_uid() - else: - # special fix for F4 and L0 devices - flash_size, device_uid = self.stm32.get_flash_size_and_uid() + flash_size = self.stm32.get_flash_size() + device_uid = self.stm32.get_uid() except bootloader.CommandError as e: self.debug( 0, diff --git a/tests/unit/test_bootloader.py b/tests/unit/test_bootloader.py index 708c437..87e7427 100644 --- a/tests/unit/test_bootloader.py +++ b/tests/unit/test_bootloader.py @@ -154,8 +154,8 @@ def test_erase_memory_with_page_count_higher_than_255_raises_page_index_error(bo def test_erase_memory_family_l0_without_pages_erases_individual_pages(connection, write): bootloader = Stm32Bootloader(connection, device_family="L0") bootloader.command = MagicMock() - bootloader.get_flash_size_and_uid = MagicMock() - bootloader.get_flash_size_and_uid.return_value = (16, 0x01) + bootloader._get_flash_size_and_uid_bulk = MagicMock() + bootloader._get_flash_size_and_uid_bulk.return_value = (16, 0x01) bootloader.erase_memory() # Page count - 1. @@ -243,10 +243,8 @@ def test_get_flash_size_and_uid_for_exception_families_returns_size_and_uid(conn memory_block[flash_size_address: flash_size_address + 2] = b'\x01\x02' bootloader.read_memory.return_value = memory_block - flash_size, uid = bootloader.get_flash_size_and_uid() - - assert flash_size == 0x0201 - assert uid == b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c' + assert bootloader.get_flash_size() == 0x0201 + assert bootloader.get_uid() == b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c' @pytest.mark.parametrize( @@ -276,3 +274,86 @@ def test_get_pages_from_range_with_start_address_zero_returns_single_page(bootlo def test_get_pages_from_large_range_returns_multiple_pages(bootloader): pages = bootloader.pages_from_range(5*1024, 20*1024) assert pages == list(range(5, 20)) + + +def test_get_flash_size_for_standard_family(connection): + bootloader = Stm32Bootloader(connection, device_family="F1") + bootloader.get_flash_size = MagicMock(return_value=128) + + assert bootloader.get_flash_size() == 128 + bootloader.get_flash_size.assert_called_once() + + +def test_get_uid_for_standard_family(connection): + bootloader = Stm32Bootloader(connection, device_family="F1") + bootloader.get_uid = MagicMock(return_value=b"some uid") + + assert bootloader.get_uid() == b"some uid" + bootloader.get_uid.assert_called_once() + + +def test_get_flash_size_for_exception_family_uses_bulk_read(connection): + bootloader = Stm32Bootloader(connection, device_family="F4") + bootloader._get_flash_size_and_uid_bulk = MagicMock(return_value=(512, b"bulk uid")) + + assert bootloader.get_flash_size() == 512 + bootloader._get_flash_size_and_uid_bulk.assert_called_once() + + +def test_get_uid_for_exception_family_uses_bulk_read(connection): + bootloader = Stm32Bootloader(connection, device_family="L0") + bootloader._get_flash_size_and_uid_bulk = MagicMock(return_value=(64, b"bulk uid")) + + assert bootloader.get_uid() == b"bulk uid" + bootloader._get_flash_size_and_uid_bulk.assert_called_once() + + +def test_get_flash_size_for_standard_family_uses_direct_read(connection): + bootloader = Stm32Bootloader(connection, device_family="F1") + bootloader.read_memory = MagicMock(return_value=b"\x80\x00") # 128 KiB + + assert bootloader.get_flash_size() == 128 + bootloader.read_memory.assert_called_once() + + +def test_flash_size_and_uid_are_cached(connection): + bootloader = Stm32Bootloader(connection, device_family="F1") + bootloader.read_memory = MagicMock() + bootloader.read_memory.side_effect = [b"\x80\x00", b"some uid 12b"] + + # First calls should trigger hardware reads via read_memory. + assert bootloader.get_flash_size() == 128 + assert bootloader.get_uid() == b"some uid 12b" + + assert bootloader.read_memory.call_count == 2 + + # Second calls should use cache, so read_memory should NOT be called again + assert bootloader.get_flash_size() == 128 + assert bootloader.get_uid() == b"some uid 12b" + + # read_memory should have been called exactly once for each (once for + # size, once for UID). + assert bootloader.read_memory.call_count == 2 + + +def test_get_flash_size_and_uid_bulk_populates_both_caches(connection): + bootloader = Stm32Bootloader(connection, device_family="F4") + # Mock the internal bulk read method via read_memory. + bootloader.read_memory = MagicMock() + memory_block = bytearray([0] * 256) + uid_offset = bootloader.UID_ADDRESS["F4"] & 0xFF + flash_size_offset = bootloader.FLASH_SIZE_ADDRESS["F4"] & 0xFF + memory_block[uid_offset: uid_offset + 12] = b"bulk uid 12b" + memory_block[flash_size_offset: flash_size_offset + 2] = b"\x00\x02" # 512 + bootloader.read_memory.return_value = memory_block + + assert bootloader.get_flash_size() == 512 + assert bootloader.get_uid() == b"bulk uid 12b" + bootloader.read_memory.assert_called_once() + + # Subsequent calls to individual methods should use cache. + assert bootloader.get_flash_size() == 512 + assert bootloader.get_uid() == b"bulk uid 12b" + + # read_memory should still have been called only once. + bootloader.read_memory.assert_called_once() From b255c7268ae753a5e47804fe8b3ea73df544b8bd Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Fri, 27 Feb 2026 20:22:44 +0100 Subject: [PATCH 298/369] clean: Drop deprecated FIXMEs --- src/stm32loader/bootloader.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/stm32loader/bootloader.py b/src/stm32loader/bootloader.py index ac234e1..cfc0816 100644 --- a/src/stm32loader/bootloader.py +++ b/src/stm32loader/bootloader.py @@ -184,7 +184,6 @@ class Command(enum.IntEnum): """STM32 native bootloader command values.""" # pylint: disable=too-few-public-methods - # FIXME turn into intenum # See ST AN3155, AN4872 GET = 0x00 @@ -210,7 +209,6 @@ class Reply(enum.IntEnum): """STM32 native bootloader reply status codes.""" # pylint: disable=too-few-public-methods - # FIXME turn into intenum # See ST AN3155, AN4872 ACK = 0x79 From bedc95c9af47b18e1d1724c93e30b5e1403d975b Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 2 Mar 2026 10:22:08 +0100 Subject: [PATCH 299/369] dev: Print details about erased pages SQUASH: Do page_count + 1 --- src/stm32loader/bootloader.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/stm32loader/bootloader.py b/src/stm32loader/bootloader.py index cfc0816..0a0d392 100644 --- a/src/stm32loader/bootloader.py +++ b/src/stm32loader/bootloader.py @@ -618,7 +618,7 @@ def erase_memory(self, pages=None): Set to None to trigger global mass erase. """ if self.extended_erase: - # Use erase with two-byte addresses. + # Use erase with two-byte addresses instead. self.extended_erase_memory(pages) return @@ -631,6 +631,7 @@ def erase_memory(self, pages=None): if page_count > 255: raise PageIndexError("Can not erase more than 255 pages for L0 family.") pages = range(page_count) + if pages: # page erase, see ST AN3155 if len(pages) > 255: @@ -641,9 +642,11 @@ def erase_memory(self, pages=None): page_count = len(pages) - 1 page_numbers = bytearray(pages) checksum = reduce(operator.xor, page_numbers, page_count) + self.debug(5, f"Flash erase {page_count} pages from {pages[0]} to {pages[-1]}") self.write(page_count, page_numbers, checksum) else: # global erase: n=255 (page count) + self.debug(5, "Flash global erase") self.write(255, 0) self._wait_for_ack("0x43 erase failed") @@ -667,6 +670,7 @@ def extended_erase_memory(self, pages=None): pages = list(range(0, (flash_size * 1024) // self.flash_page_size)) self.command(self.Command.EXTENDED_ERASE, "Extended erase memory") + if pages: # page erase, see ST AN3155 if len(pages) > 65535: @@ -681,10 +685,12 @@ def extended_erase_memory(self, pages=None): struct.pack_into(">H", page_bytes, i * 2, page) checksum = reduce(operator.xor, page_count_bytes) checksum = reduce(operator.xor, page_bytes, checksum) + self.debug(5, f"Flash erase {page_count + 1} pages from {pages[0]} to {pages[-1]}") self.write(page_count_bytes, page_bytes, checksum) else: # global mass erase: n=0xffff (page count) + checksum # TO DO: support 0xfffe bank 1 erase / 0xfffe bank 2 erase + self.debug(5, "Flash global erase") self.write(b"\xff\xff\x00") previous_timeout_value = self.connection.timeout From 8d458e14bd6e21ac39392676a0ec3d0dc1febdff Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 2 Mar 2026 10:33:11 +0100 Subject: [PATCH 300/369] doc: Expand changelog for v0.8.0 SQUASH: Mention ruff format SQUASH: Add PR/issue numbers SQUASH: Mention STM32H750 support in v0.7.1 --- CHANGELOG.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce87668..2e49945 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,18 +3,29 @@ What changed in which version. -## vnext +## [0.8.0] - TBD -uv, nox, bump-my-version, STM32G4, Python 3.12/3.13/3.14 +### Added +* `#81` Support STM32G4 family. +* `#87` Officially support Python 3.12/3.13/3.14, PyPy 3.10/3.11. +* `#69` Print details about erased pages. + +## Changed +* `#88` Use uv as build backend. +* `#88` Use uv in usage examples. +* `#71` Use `ruff` as formatter. ## [0.7.1] - 2023-10-18 + +### Added +* `#67` Support STM32H750. + ### Fixed * Integer division error when erasing specific region of flash. * Print erase/write/verify in correct order. - ### Cleaned * Extract method for range-to-pages calculation. From 92b2f4d4ff78b083fd0d5ea4151733f25bd4d9ef Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Fri, 27 Feb 2026 20:47:50 +0100 Subject: [PATCH 301/369] clean: Drop empty file --- src/stm32loader/devices.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/stm32loader/devices.py diff --git a/src/stm32loader/devices.py b/src/stm32loader/devices.py deleted file mode 100644 index e69de29..0000000 From 9ce77657534a8bebc38c903bd6c7c4b8ed76afd2 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 2 Mar 2026 10:22:38 +0100 Subject: [PATCH 302/369] dev: Better communicate required page size --- src/stm32loader/bootloader.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/stm32loader/bootloader.py b/src/stm32loader/bootloader.py index 0a0d392..d095ee6 100644 --- a/src/stm32loader/bootloader.py +++ b/src/stm32loader/bootloader.py @@ -819,11 +819,13 @@ def pages_from_range(self, start, end): """Return page indices for the given memory range.""" if start % self.flash_page_size != 0: raise PageIndexError( - f"Erase start address should be at a flash page boundary: 0x{start:08X}." + f"Erase start address should be at a flash page boundary: 0x{start:08X}" + f" (page size 0x{self.flash_page_size:04X}).", ) if end % self.flash_page_size != 0: raise PageIndexError( - f"Erase end address should be at a flash page boundary: 0x{end:08X}." + f"Erase end address should be at a flash page boundary: 0x{end:08X}" + f" (page size 0x{self.flash_page_size:04X}).", ) # Assemble the list of pages to erase. From f8d07b0bc2dcf5d6061480d84170638b07bbb10e Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 2 Mar 2026 10:08:00 +0100 Subject: [PATCH 303/369] dev: List tox as dev dependency We're mainly using Nox, but GitHub actions still uses tox --- pyproject.toml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 39cc527..55229ae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,15 +44,19 @@ lint = [ test = [ "pytest", "intelhex", + "nox>=2026.2.9", + "nox-uv>=0.7.1", + "tox>=4.30.3", +] +release = [ + "bump-my-version", + "cogapp", ] dev = [ { include-group = "test" }, { include-group = "hex" }, { include-group = "lint" }, - "bump-my-version", - "cogapp", - "nox>=2026.2.9", - "nox-uv>=0.7.1", + { include-group = "release" }, ] [project.scripts] From 56df080b7570304e545b4df4dd161bbbd70ce511 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 18 Oct 2023 21:40:57 +0200 Subject: [PATCH 304/369] dev: Add device table based on AN2606 --- src/stm32loader/devices.py | 168 +++++++++++++++++++++++++++++++++++++ tests/unit/test_devices.py | 51 +++++++++++ 2 files changed, 219 insertions(+) create mode 100644 src/stm32loader/devices.py create mode 100644 tests/unit/test_devices.py diff --git a/src/stm32loader/devices.py b/src/stm32loader/devices.py new file mode 100644 index 0000000..656361c --- /dev/null +++ b/src/stm32loader/devices.py @@ -0,0 +1,168 @@ +import enum + + +@enum.unique +class DeviceFamily(enum.Enum): + C0 = "C0" + F0 = "F0" + F1 = "F1" + F2 = "F2" + F3 = "F3" + F4 = "F4" + F7 = "F7" + G0 = "G0" + G4 = "G4" + H5 = "H5" + H7 = "H7" + L0 = "L0" + L1 = "L1" + L4 = "L4" + L5 = "L5" + WBA = "WBA" + WB = "WB" + WL = "WL" + U5 = "U5" + + +class DeviceInfo: + + def __init__(self, device_family, device_name, product_id, bootloader_id, ram, system_memory): + self.device_family = DeviceFamily[device_family] + self.device_name = device_name + self.product_id = product_id + self.bootloader_id = bootloader_id + self.ram = ram + self.system_memory = system_memory + + @property + def ram_size(self): + if self.ram is None: + return 0 + + assert isinstance(self.ram, tuple) + + if isinstance(self.ram[0], tuple): + # Multiple ranges. + ram_size = 0 + for ram_range in self.ram: + ram_size += ram_range[1] - ram_range[0] + + return ram_size + + start, end = self.ram + ram_size = end - start + return ram_size + + @property + def system_memory_size(self): + if self.system_memory is None: + return 0 + + assert isinstance(self.system_memory, tuple) + + if isinstance(self.system_memory[0], tuple): + # Multiple ranges. + flash_size = 0 + for flash_range in self.system_memory: + flash_size += flash_range[1] - flash_range[0] + + return flash_size + + start, end = self.system_memory + flash_size = end - start + return flash_size + + def __str__(self): + return self.device_name + + +DEVICE_DETAILS = [ + # Based on ST AN2606 section "Device-dependent bootloader parameters" + # Key: ProductID, BootloaderID + DeviceInfo("C0", "STM32C011xx", 0x443, 0x51, (0x_2000_0000, 0x_2000_3000), (0x_1FFF_0000, 0x_1FFF_1800)), + # Error in AN2606? Ram is mentioned as 0x_2000_2000 - 0x_2000_17FF + DeviceInfo("C0", "STM32C031xx", 0x453, 0x52, (0x_2000_2000, 0x_2000_2800), (0x_1FFF_0000, 0x_1FFF_1800)), + DeviceInfo("F0", "STM32F05xxx/STM32F030x8", 0x440, 0x21, (0x_2000_0800, 0x_2000_2000), (0x_1FFF_EC00, 0x_1FFF_F800)), + DeviceInfo("F0", "STM32F03xx4/6", 0x444, 0x10, (0x_2000_0800, 0x_2000_1000), (0x_1FFF_EC00, 0x_1FFF_F800)), + DeviceInfo("F0", "STM32F030xC", 0x442, 0x52, (0x_2000_1800, 0x_2000_8000), (0x_1FFF_D800, 0x_1FFF_F800)), + DeviceInfo("F0", "STM32F04xxx", 0x445, 0xA1, None, (0x_1FFF_C400, 0x_1FFF_F800)), + DeviceInfo("F0", "STM32F070x6", 0x445, 0xA2, None, (0x_1FFF_C400, 0x_1FFF_F800)), + DeviceInfo("F0", "STM32F070xB", 0x448, 0xA2, None, (0x_1FFF_C800, 0x_1FFF_F800)), + DeviceInfo("F0", "STM32F071xx/072xx", 0x448, 0xA1, (0x_2000_1800, 0x_2000_4000), (0x_1FFF_C800, 0x_1FFF_F800)), + DeviceInfo("F0", "STM32F09xxx", 0x442, 0x50, None, (0x_1FFF_D800, 0x_1FFF_F800)), + DeviceInfo("F1", "STM32F10xxx Low-density", 0x412, None, (0x_2000_0200, 0x_2000_2800), (0x_1FFF_F000, 0x_1FFF_F800)), + DeviceInfo("F1", "STM32F10xxx Medium-density", 0x410, None, (0x_2000_0200, 0x_2000_5000), (0x_1FFF_F000, 0x_1FFF_F800)), + DeviceInfo("F1", "STM32F10xxx High-density", 0x414, None, (0x_2000_0200, 0x_2001_0000), (0x_1FFF_F000, 0x_1FFF_F800)), + DeviceInfo("F1", "STM32F10xxx Medium-density value", 0x420, 0x10, (0x_2000_0200, 0x_2000_2000), (0x_1FFF_F000, 0x_1FFF_F800)), + DeviceInfo("F1", "STM32F10xxx High-density value", 0x428, 0x10, (0x_2000_0200, 0x_2000_8000), (0x_1FFF_F000, 0x_1FFF_F800)), + DeviceInfo("F1", "STM32F105xx/107xx", 0x418, None, (0x_2000_1000, 0x_2001_0000), (0x_1FFF_B000, 0x_1FFF_F800)), + DeviceInfo("F1", "STM32F10xxx XL-density", 0x430, 0x21, (0x_2000_0800, 0x_2001_8000), (0x_1FFF_E000, 0x_1FFF_F800)), + DeviceInfo("F2", "STM32F2xxxx", 0x411, 0x20, (0x_2000_2000, 0x_2002_0000), (0x_1FFF_0000, 0x_1FFF_7800)), + DeviceInfo("F2", "STM32F2xxxx", 0x411, 0x33, (0x_2000_2000, 0x_2002_0000), (0x_1FFF_0000, 0x_1FFF_7800)), + DeviceInfo("F3", "STM32F373xx", 0x432, 0x41, (0x_2000_1400, 0x_2000_8000), (0x_1FFF_D800, 0x_1FFF_F800)), + DeviceInfo("F3", "STM32F378xx", 0x432, 0x50, (0x_2000_1000, 0x_2000_8000), (0x_1FFF_D800, 0x_1FFF_F800)), + DeviceInfo("F3", "STM32F302xB(C)/303xB(C)", 0x422, 0x41, (0x_2000_1400, 0x_2000_A000), (0x_1FFF_D800, 0x_1FFF_F800)), + DeviceInfo("F3", "STM32F358xx", 0x422, 0x50, (0x_2000_1400, 0x_2000_A000), (0x_1FFF_D800, 0x_1FFF_F800)), + DeviceInfo("F3", "STM32F301xx/302x4(6/8)", 0x439, 0x40, (0x_2000_1800, 0x_2000_4000), (0x_1FFF_D800, 0x_1FFF_F800)), + DeviceInfo("F3", "STM32F318xx", 0x439, 0x50, (0x_2000_1800, 0x_2000_4000), (0x_1FFF_D800, 0x_1FFF_F800)), + DeviceInfo("F3", "STM32F303x4(6/8)/334xx/328xx", 0x438, 0x50, (0x_2000_1800, 0x_2000_3000), (0x_1FFF_D800, 0x_1FFF_F800)), + DeviceInfo("F3", "STM32F302xD(E)/303xD(E)", 0x446, 0x40, (0x_2000_1800, 0x_2001_0000), (0x_1FFF_D800, 0x_1FFF_F800)), + DeviceInfo("F3", "STM32F398xx", 0x446, 0x50, (0x_2000_1800, 0x_2001_0000), (0x_1FFF_D800, 0x_1FFF_F800)), + DeviceInfo("F4", "STM32F40xxx/41xxx", 0x413, 0x31, (0x_2000_2000, 0x_2002_0000), (0x_1FFF_0000, 0x_1FFF_7800)), + DeviceInfo("F4", "STM32F40xxx/41xxx", 0x413, 0x91, (0x_2000_3000, 0x_2002_0000), (0x_1FFF_0000, 0x_1FFF_7800)), + DeviceInfo("F4", "STM32F42xxx/43xxx", 0x419, 0x70, (0x_2000_3000, 0x_2003_0000), (0x_1FFF_0000, 0x_1FFF_7800)), + DeviceInfo("F4", "STM32F42xxx/43xxx", 0x419, 0x91, (0x_2000_3000, 0x_2003_0000), (0x_1FFF_0000, 0x_1FFF_7800)), + DeviceInfo("F4", "STM32F401xB(C)", 0x423, 0xD1, (0x_2000_3000, 0x_2001_8000), (0x_1FFF_0000, 0x_1FFF_7800)), + DeviceInfo("F4", "STM32F401xD(E)", 0x433, 0xD1, (0x_2000_3000, 0x_2000_8000), (0x_1FFF_0000, 0x_1FFF_7800)), + DeviceInfo("F4", "STM32F410xx", 0x458, 0xB1, (0x_2000_3000, 0x_2002_0000), (0x_1FFF_0000, 0x_1FFF_7800)), + DeviceInfo("F4", "STM32F411xx", 0x431, 0xD0, (0x_2000_3000, 0x_2002_0000), (0x_1FFF_0000, 0x_1FFF_7800)), + DeviceInfo("F4", "STM32F412xx", 0x441, 0x90, (0x_2000_3000, 0x_2004_0000), (0x_1FFF_0000, 0x_1FFF_7800)), + DeviceInfo("F4", "STM32F446xx", 0x421, 0x90, (0x_2000_3000, 0x_2002_0000), (0x_1FFF_0000, 0x_1FFF_7800)), + DeviceInfo("F4", "STM32F469xx/479xx", 0x434, 0x90, (0x_2000_3000, 0x_2006_0000), (0x_1FFF_0000, 0x_1FFF_7800)), + DeviceInfo("F4", "STM32F413xx/423xx", 0x463, 0x90, (0x_2000_3000, 0x_2005_0000), (0x_1FFF_0000, 0x_1FFF_7800)), + DeviceInfo("F7", "STM32F72xxx/73xxx", 0x452, 0x90, (0x_2000_4000, 0x_2004_0000), (0x_1FF0_0000, 0x_1FF0_EDC0)), + DeviceInfo("F7", "STM32F74xxx/75xxx", 0x449, 0x70, (0x_2000_4000, 0x_2005_0000), (0x_1FF0_0000, 0x_1FF0_EDC0)), + DeviceInfo("F7", "STM32F74xxx/75xxx", 0x449, 0x90, (0x_2000_4000, 0x_2005_0000), (0x_1FF0_0000, 0x_1FF0_EDC0)), + DeviceInfo("F7", "STM32F76xxx/77xxx", 0x451, 0x93, (0x_2000_4000, 0x_2008_0000), (0x_1FF0_0000, 0x_1FF0_EDC0)), + DeviceInfo("G0", "STM32G03xxx/04xxx", 0x466, 0x52, (0x_2000_1000, 0x_2000_2000), (0x_1FFF_0000, 0x_1FFF_2000)), + DeviceInfo("G0", "STM32G07xxx/08xxx", 0x460, 0xB3, (0x_2000_2700, 0x_2000_9000), (0x_1FFF_0000, 0x_1FFF_7000)), + DeviceInfo("G0", "STM32G0B0xx", 0x467, 0xD0, (0x_2000_4000, 0x_2002_0000), ((0x_1FFF_0000, 0x_1FFF_7000), (0x_1FFF_8000, 0x_1FFF_F000))), + DeviceInfo("G0", "STM32G0B1xx/0C1xx", 0x467, 0x92, (0x_2000_4000, 0x_2002_0000), ((0x_1FFF_0000, 0x_1FFF_6000), (0x_1FFF_8000, 0x_1FFF_F000))), + DeviceInfo("G0", "STM32G05xxx/061xx", 0x456, 0x51, (0x_2000_1000, 0x_2000_2000), (0x_1FFF_0000, 0x_1FFF_1000)), + DeviceInfo("G4", "STM32G431xx/441xx", 0x468, 0xD4, (0x_2000_4000, 0x_2000_5800), (0x_1FFF_0000, 0x_1FFF_7000)), + DeviceInfo("G4", "STM32G47xxx/48xxx", 0x469, 0xD5, (0x_2000_4000, 0x_2001_8000), (0x_1FFF_0000, 0x_1FFF_7000)), + DeviceInfo("G4", "STM32G491xx/A1xx", 0x479, 0xD2, (0x_2000_4000, 0x_2001_C000), (0x_1FFF_0000, 0x_1FFF_7000)), + DeviceInfo("H5", "STM32H503xx", 0x474, 0xE1, (0x_2000_4000, 0x_2000_8000), (0x_0BF8_7000, 0x_0BF9_0000)), + DeviceInfo("H5", "STM32H563xx/573xx", 0x484, 0xE3, (0x_2000_0000, 0x_200A_0000), (0x_0BF9_7000, 0x_0BFA_0000)), + DeviceInfo("H7", "STM32H72xxx/73xxx", 0x483, 0x93, ((0x_2000_4100, 0x_2002_0000), (0x_2400_4000, 0x_2405_0000)), (0x_1FF0_0000, 0x_1FF1_E800)), + DeviceInfo("H7", "STM32H74xxx/75xxx", 0x450, 0x91, ((0x_2000_4100, 0x_2002_0000), (0x_2400_5000, 0x_2408_0000)), (0x_1FF0_0000, 0x_1FF1_E800)), + DeviceInfo("H7", "STM32H7A3xx/B3xx", 0x480, 0x92, ((0x_2000_4100, 0x_2002_0000), (0x_2403_4000, 0x_2408_0000)), (0x_1FF0_0000, 0x_1FF1_4000)), + DeviceInfo("L0", "STM32L01xxx/02xxx", 0x457, 0xC3, None, (0x_1FF0_0000, 0x_1FF0_1000)), + DeviceInfo("L0", "STM32L031xx/041xx", 0x425, 0xC0, (0x_2000_1000, 0x_2000_2000), (0x_1FF0_0000, 0x_1FF0_1000)), + DeviceInfo("L0", "STM32L05xxx/06xxx", 0x417, 0xC0, (0x_2000_1000, 0x_2000_2000), (0x_1FF0_0000, 0x_1FF0_1000)), + DeviceInfo("L0", "STM32L07xxx/08xxx", 0x447, 0x41, (0x_2000_1000, 0x_2000_5000), (0x_1FF0_0000, 0x_1FF0_2000)), + DeviceInfo("L0", "STM32L07xxx/08xxx", 0x447, 0xB2, (0x_2000_1400, 0x_2000_5000), (0x_1FF0_0000, 0x_1FF0_2000)), + DeviceInfo("L1", "STM32L1xxx6(8/B)", 0x416, 0x20, (0x_2000_0800, 0x_2000_4000), (0x_1FF0_0000, 0x_1FF0_2000)), + DeviceInfo("L1", "STM32L1xxx6(8/B)A", 0x429, 0x20, (0x_2000_1000, 0x_2000_8000), (0x_1FF0_0000, 0x_1FF0_2000)), + DeviceInfo("L1", "STM32L1xxxC", 0x427, 0x40, (0x_2000_1000, 0x_2000_8000), (0x_1FF0_0000, 0x_1FF0_2000)), + DeviceInfo("L1", "STM32L1xxxD", 0x436, 0x45, (0x_2000_1000, 0x_2000_C000), (0x_1FF0_0000, 0x_1FF0_2000)), + DeviceInfo("L1", "STM32L1xxxE", 0x437, 0x40, (0x_2000_1000, 0x_2001_4000), (0x_1FF0_0000, 0x_1FF0_2000)), + DeviceInfo("L4", "STM32L412xx/422xx", 0x464, 0xD1, (0x_2000_2100, 0x_2000_8000), (0x_1FFF_0000, 0x_1FFF_7000)), + DeviceInfo("L4", "STM32L43xxx/44xxx", 0x435, 0x91, (0x_2000_3100, 0x_2000_C000), (0x_1FFF_0000, 0x_1FFF_7000)), + DeviceInfo("L4", "STM32L45xxx/46xxx", 0x462, 0x92, (0x_2000_3100, 0x_2002_0000), (0x_1FFF_0000, 0x_1FFF_7000)), + DeviceInfo("L4", "STM32L47xxx/48xxx", 0x415, 0xA3, (0x_2000_3000, 0x_2001_8000), (0x_1FFF_0000, 0x_1FFF_7000)), + DeviceInfo("L4", "STM32L47xxx/48xxx", 0x415, 0x92, (0x_2000_3100, 0x_2001_8000), (0x_1FFF_0000, 0x_1FFF_7000)), + DeviceInfo("L4", "STM32L496xx/4A6xx", 0x461, 0x93, (0x_2000_3100, 0x_2004_0000), (0x_1FFF_0000, 0x_1FFF_7000)), + DeviceInfo("L4", "STM32L4Rxx/4Sxx", 0x470, 0x95, (0x_2000_3200, 0x_200A_0000), (0x_1FFF_0000, 0x_1FFF_7000)), + DeviceInfo("L4", "STM32L4P5xx/Q5xx", 0x471, 0x90, (0x_2000_4000, 0x_2005_0000), (0x_1FFF_0000, 0x_1FFF_7000)), + DeviceInfo("L5", "L5STM32L552xx/562xx", 0x472, 0x92, (0x_2000_4000, 0x_2004_0000), (0x_0BF9_0000, 0x_0BF9_8000)), + DeviceInfo("WBA", "STM32WBA52xx", 0x492, 0xB0, (0x_2000_0000, 0x_2000_2000), (0x_0BF8_8000, 0x_0BF9_0000)), + DeviceInfo("WB", "STM32WB10xx/15xx", 0x494, 0xB1, (0x_2000_5000, 0x_2004_0000), (0x_1FFF_0000, 0x_1FFF_7000)), + DeviceInfo("WB", "STM32WB30xx/35xx/50xx/WB55xx", 0x495, 0xD5, (0x_2000_4000, 0x_2000_B000), (0x_1FFF_0000, 0x_1FFF_7000)), + DeviceInfo("WL", "STM32WLE5xx/WL55xx", 0x497, 0xC4, (0x_2000_2000, 0x_2001_0000), (0x_1FFF_0000, 0x_1FFF_4000)), + DeviceInfo("U5", "STM32U535xx/545xx", 0x455, 0x91, (0x_2000_4000, 0x_2024_0000), (0x_0BF9_0000, 0x_0BFA_0000)), + DeviceInfo("U5", "STM32U575xx/ STM32U585xx", 0x482, 0x92, (0x_2000_4000, 0x_200C_0000), (0x_0BF9_0000, 0x_0BFA_0000)), + DeviceInfo("U5", "STM32U595xx/599xx/5A9xx", 0x481, 0x92, (0x_2000_4000, 0x_2027_0000), (0x_0BF9_0000, 0x_0BFA_0000)), +] + +DEVICES = {(dev.product_id, dev.bootloader_id): dev for dev in DEVICE_DETAILS} diff --git a/tests/unit/test_devices.py b/tests/unit/test_devices.py new file mode 100644 index 0000000..a420f17 --- /dev/null +++ b/tests/unit/test_devices.py @@ -0,0 +1,51 @@ + +from stm32loader.devices import DEVICES + +KNOWN_DUPLICATE_DEVICE_NAMES = [ + "STM32F2xxxx", + "STM32F40xxx/41xxx", + "STM32F42xxx/43xxx", + "STM32F74xxx/75xxx", + "STM32L07xxx/08xxx", + "STM32L47xxx/48xxx", +] + + +def test_only_specific_names_occur_twice(): + all_names = set() + for dev in DEVICES.values(): + if dev.device_name in all_names: + assert dev.device_name in KNOWN_DUPLICATE_DEVICE_NAMES, dev.device_name + all_names.add(dev.device_name) + + +def test_product_id_and_bootloader_id_match_device_properties(): + for ids, dev in DEVICES.items(): + device_id, bootloader_id = ids + assert dev.product_id == device_id + assert dev.bootloader_id == bootloader_id + + +def test_ram_size_is_multiple_of_256(): + for dev in DEVICES.values(): + if dev.ram_size == 0: + continue + + assert isinstance(dev.ram_size, int) + assert dev.ram_size > 0, f"{dev} ram size not None but still too low: {dev.ram_size}" + assert dev.ram_size % 256 == 0, f"{dev} ram size not a multiple of 256: {dev.ram_size}" + + +def test_system_memory_size_multiple_of_64(): + for dev in DEVICES.values(): + if dev.system_memory_size == 0: + continue + + assert isinstance(dev.system_memory_size, int) + assert dev.system_memory_size > 0, f"{dev} flash size not None but still 0: {dev.system_memory_size}" + assert dev.system_memory_size % 64 == 0, f"{dev} flash size not a multiple of 64: {dev.system_memory_size}" + + +def test_device_name_does_not_contain_underscore(): + for dev in DEVICES.values(): + assert "_" not in dev.device_name, dev.device_name From 7e0fcd68423718316c005a286aa8fec213f4f495 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Fri, 20 Oct 2023 09:00:49 +0200 Subject: [PATCH 305/369] dev: Improve device table based on STM32flash's table --- src/stm32loader/devices.py | 188 +++++++++++++++++-------------- tests/unit/devices_stm32flash.py | 117 +++++++++++++++++++ tests/unit/test_devices.py | 60 ++++++++++ 3 files changed, 281 insertions(+), 84 deletions(-) create mode 100644 tests/unit/devices_stm32flash.py diff --git a/src/stm32loader/devices.py b/src/stm32loader/devices.py index 656361c..e0bf253 100644 --- a/src/stm32loader/devices.py +++ b/src/stm32loader/devices.py @@ -3,6 +3,7 @@ @enum.unique class DeviceFamily(enum.Enum): + # AN2606 C0 = "C0" F0 = "F0" F1 = "F1" @@ -22,13 +23,21 @@ class DeviceFamily(enum.Enum): WB = "WB" WL = "WL" U5 = "U5" + # Not sure if these really exist? + W = "W" + + # Non-STM devices. + NRG1 = "NRG1" + NRG2 = "NRG2" + WIZ = "WIZ" class DeviceInfo: - def __init__(self, device_family, device_name, product_id, bootloader_id, ram, system_memory): + def __init__(self, device_family, device_name, variant, product_line, product_id, bootloader_id, ram, system_memory): self.device_family = DeviceFamily[device_family] self.device_name = device_name + self.product_line = product_line self.product_id = product_id self.bootloader_id = bootloader_id self.ram = ram @@ -79,90 +88,101 @@ def __str__(self): DEVICE_DETAILS = [ # Based on ST AN2606 section "Device-dependent bootloader parameters" # Key: ProductID, BootloaderID - DeviceInfo("C0", "STM32C011xx", 0x443, 0x51, (0x_2000_0000, 0x_2000_3000), (0x_1FFF_0000, 0x_1FFF_1800)), + DeviceInfo("C0", "STM32C011xx", "", "", 0x443, 0x51, (0x_2000_0000, 0x_2000_3000), (0x_1FFF_0000, 0x_1FFF_1800)), # Error in AN2606? Ram is mentioned as 0x_2000_2000 - 0x_2000_17FF - DeviceInfo("C0", "STM32C031xx", 0x453, 0x52, (0x_2000_2000, 0x_2000_2800), (0x_1FFF_0000, 0x_1FFF_1800)), - DeviceInfo("F0", "STM32F05xxx/STM32F030x8", 0x440, 0x21, (0x_2000_0800, 0x_2000_2000), (0x_1FFF_EC00, 0x_1FFF_F800)), - DeviceInfo("F0", "STM32F03xx4/6", 0x444, 0x10, (0x_2000_0800, 0x_2000_1000), (0x_1FFF_EC00, 0x_1FFF_F800)), - DeviceInfo("F0", "STM32F030xC", 0x442, 0x52, (0x_2000_1800, 0x_2000_8000), (0x_1FFF_D800, 0x_1FFF_F800)), - DeviceInfo("F0", "STM32F04xxx", 0x445, 0xA1, None, (0x_1FFF_C400, 0x_1FFF_F800)), - DeviceInfo("F0", "STM32F070x6", 0x445, 0xA2, None, (0x_1FFF_C400, 0x_1FFF_F800)), - DeviceInfo("F0", "STM32F070xB", 0x448, 0xA2, None, (0x_1FFF_C800, 0x_1FFF_F800)), - DeviceInfo("F0", "STM32F071xx/072xx", 0x448, 0xA1, (0x_2000_1800, 0x_2000_4000), (0x_1FFF_C800, 0x_1FFF_F800)), - DeviceInfo("F0", "STM32F09xxx", 0x442, 0x50, None, (0x_1FFF_D800, 0x_1FFF_F800)), - DeviceInfo("F1", "STM32F10xxx Low-density", 0x412, None, (0x_2000_0200, 0x_2000_2800), (0x_1FFF_F000, 0x_1FFF_F800)), - DeviceInfo("F1", "STM32F10xxx Medium-density", 0x410, None, (0x_2000_0200, 0x_2000_5000), (0x_1FFF_F000, 0x_1FFF_F800)), - DeviceInfo("F1", "STM32F10xxx High-density", 0x414, None, (0x_2000_0200, 0x_2001_0000), (0x_1FFF_F000, 0x_1FFF_F800)), - DeviceInfo("F1", "STM32F10xxx Medium-density value", 0x420, 0x10, (0x_2000_0200, 0x_2000_2000), (0x_1FFF_F000, 0x_1FFF_F800)), - DeviceInfo("F1", "STM32F10xxx High-density value", 0x428, 0x10, (0x_2000_0200, 0x_2000_8000), (0x_1FFF_F000, 0x_1FFF_F800)), - DeviceInfo("F1", "STM32F105xx/107xx", 0x418, None, (0x_2000_1000, 0x_2001_0000), (0x_1FFF_B000, 0x_1FFF_F800)), - DeviceInfo("F1", "STM32F10xxx XL-density", 0x430, 0x21, (0x_2000_0800, 0x_2001_8000), (0x_1FFF_E000, 0x_1FFF_F800)), - DeviceInfo("F2", "STM32F2xxxx", 0x411, 0x20, (0x_2000_2000, 0x_2002_0000), (0x_1FFF_0000, 0x_1FFF_7800)), - DeviceInfo("F2", "STM32F2xxxx", 0x411, 0x33, (0x_2000_2000, 0x_2002_0000), (0x_1FFF_0000, 0x_1FFF_7800)), - DeviceInfo("F3", "STM32F373xx", 0x432, 0x41, (0x_2000_1400, 0x_2000_8000), (0x_1FFF_D800, 0x_1FFF_F800)), - DeviceInfo("F3", "STM32F378xx", 0x432, 0x50, (0x_2000_1000, 0x_2000_8000), (0x_1FFF_D800, 0x_1FFF_F800)), - DeviceInfo("F3", "STM32F302xB(C)/303xB(C)", 0x422, 0x41, (0x_2000_1400, 0x_2000_A000), (0x_1FFF_D800, 0x_1FFF_F800)), - DeviceInfo("F3", "STM32F358xx", 0x422, 0x50, (0x_2000_1400, 0x_2000_A000), (0x_1FFF_D800, 0x_1FFF_F800)), - DeviceInfo("F3", "STM32F301xx/302x4(6/8)", 0x439, 0x40, (0x_2000_1800, 0x_2000_4000), (0x_1FFF_D800, 0x_1FFF_F800)), - DeviceInfo("F3", "STM32F318xx", 0x439, 0x50, (0x_2000_1800, 0x_2000_4000), (0x_1FFF_D800, 0x_1FFF_F800)), - DeviceInfo("F3", "STM32F303x4(6/8)/334xx/328xx", 0x438, 0x50, (0x_2000_1800, 0x_2000_3000), (0x_1FFF_D800, 0x_1FFF_F800)), - DeviceInfo("F3", "STM32F302xD(E)/303xD(E)", 0x446, 0x40, (0x_2000_1800, 0x_2001_0000), (0x_1FFF_D800, 0x_1FFF_F800)), - DeviceInfo("F3", "STM32F398xx", 0x446, 0x50, (0x_2000_1800, 0x_2001_0000), (0x_1FFF_D800, 0x_1FFF_F800)), - DeviceInfo("F4", "STM32F40xxx/41xxx", 0x413, 0x31, (0x_2000_2000, 0x_2002_0000), (0x_1FFF_0000, 0x_1FFF_7800)), - DeviceInfo("F4", "STM32F40xxx/41xxx", 0x413, 0x91, (0x_2000_3000, 0x_2002_0000), (0x_1FFF_0000, 0x_1FFF_7800)), - DeviceInfo("F4", "STM32F42xxx/43xxx", 0x419, 0x70, (0x_2000_3000, 0x_2003_0000), (0x_1FFF_0000, 0x_1FFF_7800)), - DeviceInfo("F4", "STM32F42xxx/43xxx", 0x419, 0x91, (0x_2000_3000, 0x_2003_0000), (0x_1FFF_0000, 0x_1FFF_7800)), - DeviceInfo("F4", "STM32F401xB(C)", 0x423, 0xD1, (0x_2000_3000, 0x_2001_8000), (0x_1FFF_0000, 0x_1FFF_7800)), - DeviceInfo("F4", "STM32F401xD(E)", 0x433, 0xD1, (0x_2000_3000, 0x_2000_8000), (0x_1FFF_0000, 0x_1FFF_7800)), - DeviceInfo("F4", "STM32F410xx", 0x458, 0xB1, (0x_2000_3000, 0x_2002_0000), (0x_1FFF_0000, 0x_1FFF_7800)), - DeviceInfo("F4", "STM32F411xx", 0x431, 0xD0, (0x_2000_3000, 0x_2002_0000), (0x_1FFF_0000, 0x_1FFF_7800)), - DeviceInfo("F4", "STM32F412xx", 0x441, 0x90, (0x_2000_3000, 0x_2004_0000), (0x_1FFF_0000, 0x_1FFF_7800)), - DeviceInfo("F4", "STM32F446xx", 0x421, 0x90, (0x_2000_3000, 0x_2002_0000), (0x_1FFF_0000, 0x_1FFF_7800)), - DeviceInfo("F4", "STM32F469xx/479xx", 0x434, 0x90, (0x_2000_3000, 0x_2006_0000), (0x_1FFF_0000, 0x_1FFF_7800)), - DeviceInfo("F4", "STM32F413xx/423xx", 0x463, 0x90, (0x_2000_3000, 0x_2005_0000), (0x_1FFF_0000, 0x_1FFF_7800)), - DeviceInfo("F7", "STM32F72xxx/73xxx", 0x452, 0x90, (0x_2000_4000, 0x_2004_0000), (0x_1FF0_0000, 0x_1FF0_EDC0)), - DeviceInfo("F7", "STM32F74xxx/75xxx", 0x449, 0x70, (0x_2000_4000, 0x_2005_0000), (0x_1FF0_0000, 0x_1FF0_EDC0)), - DeviceInfo("F7", "STM32F74xxx/75xxx", 0x449, 0x90, (0x_2000_4000, 0x_2005_0000), (0x_1FF0_0000, 0x_1FF0_EDC0)), - DeviceInfo("F7", "STM32F76xxx/77xxx", 0x451, 0x93, (0x_2000_4000, 0x_2008_0000), (0x_1FF0_0000, 0x_1FF0_EDC0)), - DeviceInfo("G0", "STM32G03xxx/04xxx", 0x466, 0x52, (0x_2000_1000, 0x_2000_2000), (0x_1FFF_0000, 0x_1FFF_2000)), - DeviceInfo("G0", "STM32G07xxx/08xxx", 0x460, 0xB3, (0x_2000_2700, 0x_2000_9000), (0x_1FFF_0000, 0x_1FFF_7000)), - DeviceInfo("G0", "STM32G0B0xx", 0x467, 0xD0, (0x_2000_4000, 0x_2002_0000), ((0x_1FFF_0000, 0x_1FFF_7000), (0x_1FFF_8000, 0x_1FFF_F000))), - DeviceInfo("G0", "STM32G0B1xx/0C1xx", 0x467, 0x92, (0x_2000_4000, 0x_2002_0000), ((0x_1FFF_0000, 0x_1FFF_6000), (0x_1FFF_8000, 0x_1FFF_F000))), - DeviceInfo("G0", "STM32G05xxx/061xx", 0x456, 0x51, (0x_2000_1000, 0x_2000_2000), (0x_1FFF_0000, 0x_1FFF_1000)), - DeviceInfo("G4", "STM32G431xx/441xx", 0x468, 0xD4, (0x_2000_4000, 0x_2000_5800), (0x_1FFF_0000, 0x_1FFF_7000)), - DeviceInfo("G4", "STM32G47xxx/48xxx", 0x469, 0xD5, (0x_2000_4000, 0x_2001_8000), (0x_1FFF_0000, 0x_1FFF_7000)), - DeviceInfo("G4", "STM32G491xx/A1xx", 0x479, 0xD2, (0x_2000_4000, 0x_2001_C000), (0x_1FFF_0000, 0x_1FFF_7000)), - DeviceInfo("H5", "STM32H503xx", 0x474, 0xE1, (0x_2000_4000, 0x_2000_8000), (0x_0BF8_7000, 0x_0BF9_0000)), - DeviceInfo("H5", "STM32H563xx/573xx", 0x484, 0xE3, (0x_2000_0000, 0x_200A_0000), (0x_0BF9_7000, 0x_0BFA_0000)), - DeviceInfo("H7", "STM32H72xxx/73xxx", 0x483, 0x93, ((0x_2000_4100, 0x_2002_0000), (0x_2400_4000, 0x_2405_0000)), (0x_1FF0_0000, 0x_1FF1_E800)), - DeviceInfo("H7", "STM32H74xxx/75xxx", 0x450, 0x91, ((0x_2000_4100, 0x_2002_0000), (0x_2400_5000, 0x_2408_0000)), (0x_1FF0_0000, 0x_1FF1_E800)), - DeviceInfo("H7", "STM32H7A3xx/B3xx", 0x480, 0x92, ((0x_2000_4100, 0x_2002_0000), (0x_2403_4000, 0x_2408_0000)), (0x_1FF0_0000, 0x_1FF1_4000)), - DeviceInfo("L0", "STM32L01xxx/02xxx", 0x457, 0xC3, None, (0x_1FF0_0000, 0x_1FF0_1000)), - DeviceInfo("L0", "STM32L031xx/041xx", 0x425, 0xC0, (0x_2000_1000, 0x_2000_2000), (0x_1FF0_0000, 0x_1FF0_1000)), - DeviceInfo("L0", "STM32L05xxx/06xxx", 0x417, 0xC0, (0x_2000_1000, 0x_2000_2000), (0x_1FF0_0000, 0x_1FF0_1000)), - DeviceInfo("L0", "STM32L07xxx/08xxx", 0x447, 0x41, (0x_2000_1000, 0x_2000_5000), (0x_1FF0_0000, 0x_1FF0_2000)), - DeviceInfo("L0", "STM32L07xxx/08xxx", 0x447, 0xB2, (0x_2000_1400, 0x_2000_5000), (0x_1FF0_0000, 0x_1FF0_2000)), - DeviceInfo("L1", "STM32L1xxx6(8/B)", 0x416, 0x20, (0x_2000_0800, 0x_2000_4000), (0x_1FF0_0000, 0x_1FF0_2000)), - DeviceInfo("L1", "STM32L1xxx6(8/B)A", 0x429, 0x20, (0x_2000_1000, 0x_2000_8000), (0x_1FF0_0000, 0x_1FF0_2000)), - DeviceInfo("L1", "STM32L1xxxC", 0x427, 0x40, (0x_2000_1000, 0x_2000_8000), (0x_1FF0_0000, 0x_1FF0_2000)), - DeviceInfo("L1", "STM32L1xxxD", 0x436, 0x45, (0x_2000_1000, 0x_2000_C000), (0x_1FF0_0000, 0x_1FF0_2000)), - DeviceInfo("L1", "STM32L1xxxE", 0x437, 0x40, (0x_2000_1000, 0x_2001_4000), (0x_1FF0_0000, 0x_1FF0_2000)), - DeviceInfo("L4", "STM32L412xx/422xx", 0x464, 0xD1, (0x_2000_2100, 0x_2000_8000), (0x_1FFF_0000, 0x_1FFF_7000)), - DeviceInfo("L4", "STM32L43xxx/44xxx", 0x435, 0x91, (0x_2000_3100, 0x_2000_C000), (0x_1FFF_0000, 0x_1FFF_7000)), - DeviceInfo("L4", "STM32L45xxx/46xxx", 0x462, 0x92, (0x_2000_3100, 0x_2002_0000), (0x_1FFF_0000, 0x_1FFF_7000)), - DeviceInfo("L4", "STM32L47xxx/48xxx", 0x415, 0xA3, (0x_2000_3000, 0x_2001_8000), (0x_1FFF_0000, 0x_1FFF_7000)), - DeviceInfo("L4", "STM32L47xxx/48xxx", 0x415, 0x92, (0x_2000_3100, 0x_2001_8000), (0x_1FFF_0000, 0x_1FFF_7000)), - DeviceInfo("L4", "STM32L496xx/4A6xx", 0x461, 0x93, (0x_2000_3100, 0x_2004_0000), (0x_1FFF_0000, 0x_1FFF_7000)), - DeviceInfo("L4", "STM32L4Rxx/4Sxx", 0x470, 0x95, (0x_2000_3200, 0x_200A_0000), (0x_1FFF_0000, 0x_1FFF_7000)), - DeviceInfo("L4", "STM32L4P5xx/Q5xx", 0x471, 0x90, (0x_2000_4000, 0x_2005_0000), (0x_1FFF_0000, 0x_1FFF_7000)), - DeviceInfo("L5", "L5STM32L552xx/562xx", 0x472, 0x92, (0x_2000_4000, 0x_2004_0000), (0x_0BF9_0000, 0x_0BF9_8000)), - DeviceInfo("WBA", "STM32WBA52xx", 0x492, 0xB0, (0x_2000_0000, 0x_2000_2000), (0x_0BF8_8000, 0x_0BF9_0000)), - DeviceInfo("WB", "STM32WB10xx/15xx", 0x494, 0xB1, (0x_2000_5000, 0x_2004_0000), (0x_1FFF_0000, 0x_1FFF_7000)), - DeviceInfo("WB", "STM32WB30xx/35xx/50xx/WB55xx", 0x495, 0xD5, (0x_2000_4000, 0x_2000_B000), (0x_1FFF_0000, 0x_1FFF_7000)), - DeviceInfo("WL", "STM32WLE5xx/WL55xx", 0x497, 0xC4, (0x_2000_2000, 0x_2001_0000), (0x_1FFF_0000, 0x_1FFF_4000)), - DeviceInfo("U5", "STM32U535xx/545xx", 0x455, 0x91, (0x_2000_4000, 0x_2024_0000), (0x_0BF9_0000, 0x_0BFA_0000)), - DeviceInfo("U5", "STM32U575xx/ STM32U585xx", 0x482, 0x92, (0x_2000_4000, 0x_200C_0000), (0x_0BF9_0000, 0x_0BFA_0000)), - DeviceInfo("U5", "STM32U595xx/599xx/5A9xx", 0x481, 0x92, (0x_2000_4000, 0x_2027_0000), (0x_0BF9_0000, 0x_0BFA_0000)), + DeviceInfo("C0", "STM32C031xx", "", "", 0x453, 0x52, (0x_2000_2000, 0x_2000_2800), (0x_1FFF_0000, 0x_1FFF_1800)), + DeviceInfo("F0", "STM32F05xxx/STM32F030x8", "", "", 0x440, 0x21, (0x_2000_0800, 0x_2000_2000), (0x_1FFF_EC00, 0x_1FFF_F800)), + DeviceInfo("F0", "STM32F03xx4/6", "", "", 0x444, 0x10, (0x_2000_0800, 0x_2000_1000), (0x_1FFF_EC00, 0x_1FFF_F800)), + DeviceInfo("F0", "STM32F030xC", "", "", 0x442, 0x52, (0x_2000_1800, 0x_2000_8000), (0x_1FFF_D800, 0x_1FFF_F800)), + DeviceInfo("F0", "STM32F04xxx", "", "", 0x445, 0xA1, None, (0x_1FFF_C400, 0x_1FFF_F800)), + DeviceInfo("F0", "STM32F070x6", "", "", 0x445, 0xA2, None, (0x_1FFF_C400, 0x_1FFF_F800)), + DeviceInfo("F0", "STM32F070xB", "", "", 0x448, 0xA2, None, (0x_1FFF_C800, 0x_1FFF_F800)), + DeviceInfo("F0", "STM32F071xx/072xx", "", "", 0x448, 0xA1, (0x_2000_1800, 0x_2000_4000), (0x_1FFF_C800, 0x_1FFF_F800)), + DeviceInfo("F0", "STM32F09xxx", 0x442, "", "", 0x50, None, (0x_1FFF_D800, 0x_1FFF_F800)), + DeviceInfo("F1", "STM32F10xxx", "", "Low-density", 0x412, None, (0x_2000_0200, 0x_2000_2800), (0x_1FFF_F000, 0x_1FFF_F800)), + DeviceInfo("F1", "STM32F10xxx", "", "Medium-density", 0x410, None, (0x_2000_0200, 0x_2000_5000), (0x_1FFF_F000, 0x_1FFF_F800)), + DeviceInfo("F1", "STM32F10xxx", "", "High-density", 0x414, None, (0x_2000_0200, 0x_2001_0000), (0x_1FFF_F000, 0x_1FFF_F800)), + DeviceInfo("F1", "STM32F10xxx", "", "Medium-density value", 0x420, 0x10, (0x_2000_0200, 0x_2000_2000), (0x_1FFF_F000, 0x_1FFF_F800)), + DeviceInfo("F1", "STM32F10xxx", "", "High-density value", 0x428, 0x10, (0x_2000_0200, 0x_2000_8000), (0x_1FFF_F000, 0x_1FFF_F800)), + DeviceInfo("F1", "STM32F105xx/107xx", "", "Connectivity", 0x418, None, (0x_2000_1000, 0x_2001_0000), (0x_1FFF_B000, 0x_1FFF_F800)), + DeviceInfo("F1", "STM32F10xxx", "", "XL-density", 0x430, 0x21, (0x_2000_0800, 0x_2001_8000), (0x_1FFF_E000, 0x_1FFF_F800)), + DeviceInfo("F2", "STM32F2xxxx", "", "", 0x411, 0x20, (0x_2000_2000, 0x_2002_0000), (0x_1FFF_0000, 0x_1FFF_7800)), + DeviceInfo("F2", "STM32F2xxxx", "", "", 0x411, 0x33, (0x_2000_2000, 0x_2002_0000), (0x_1FFF_0000, 0x_1FFF_7800)), + DeviceInfo("F3", "STM32F373xx", "", "", 0x432, 0x41, (0x_2000_1400, 0x_2000_8000), (0x_1FFF_D800, 0x_1FFF_F800)), + DeviceInfo("F3", "STM32F378xx", "", "", 0x432, 0x50, (0x_2000_1000, 0x_2000_8000), (0x_1FFF_D800, 0x_1FFF_F800)), + DeviceInfo("F3", "STM32F302xB(C)/303xB(C)", "", "", 0x422, 0x41, (0x_2000_1400, 0x_2000_A000), (0x_1FFF_D800, 0x_1FFF_F800)), + DeviceInfo("F3", "STM32F358xx", "", "", 0x422, 0x50, (0x_2000_1400, 0x_2000_A000), (0x_1FFF_D800, 0x_1FFF_F800)), + DeviceInfo("F3", "STM32F301xx/302x4(6/8)", "", "", 0x439, 0x40, (0x_2000_1800, 0x_2000_4000), (0x_1FFF_D800, 0x_1FFF_F800)), + DeviceInfo("F3", "STM32F318xx", "", "", 0x439, 0x50, (0x_2000_1800, 0x_2000_4000), (0x_1FFF_D800, 0x_1FFF_F800)), + DeviceInfo("F3", "STM32F303x4(6/8)/334xx/328xx", "", "", 0x438, 0x50, (0x_2000_1800, 0x_2000_3000), (0x_1FFF_D800, 0x_1FFF_F800)), + DeviceInfo("F3", "STM32F302xD(E)/303xD(E)", "", "", 0x446, 0x40, (0x_2000_1800, 0x_2001_0000), (0x_1FFF_D800, 0x_1FFF_F800)), + DeviceInfo("F3", "STM32F398xx", "", "", 0x446, 0x50, (0x_2000_1800, 0x_2001_0000), (0x_1FFF_D800, 0x_1FFF_F800)), + DeviceInfo("F4", "STM32F40xxx/41xxx", "", "", 0x413, 0x31, (0x_2000_2000, 0x_2002_0000), (0x_1FFF_0000, 0x_1FFF_7800)), + DeviceInfo("F4", "STM32F40xxx/41xxx", "", "", 0x413, 0x91, (0x_2000_3000, 0x_2002_0000), (0x_1FFF_0000, 0x_1FFF_7800)), + DeviceInfo("F4", "STM32F42xxx/43xxx", "", "", 0x419, 0x70, (0x_2000_3000, 0x_2003_0000), (0x_1FFF_0000, 0x_1FFF_7800)), + DeviceInfo("F4", "STM32F42xxx/43xxx", "", "", 0x419, 0x91, (0x_2000_3000, 0x_2003_0000), (0x_1FFF_0000, 0x_1FFF_7800)), + # check ram upper end + DeviceInfo("F4", "STM32F401xB(C)", "", "", 0x423, 0xD1, (0x_2000_3000, 0x_2001_0000), (0x_1FFF_0000, 0x_1FFF_7800)), + DeviceInfo("F4", "STM32F401xD(E)", "", "", 0x433, 0xD1, (0x_2000_3000, 0x_2001_8000), (0x_1FFF_0000, 0x_1FFF_7800)), + DeviceInfo("F4", "STM32F410xx", "", "", 0x458, 0xB1, (0x_2000_3000, 0x_2000_8000), (0x_1FFF_0000, 0x_1FFF_7800)), + DeviceInfo("F4", "STM32F411xx", "", "", 0x431, 0xD0, (0x_2000_3000, 0x_2002_0000), (0x_1FFF_0000, 0x_1FFF_7800)), + DeviceInfo("F4", "STM32F412xx", "", "", 0x441, 0x90, (0x_2000_3000, 0x_2004_0000), (0x_1FFF_0000, 0x_1FFF_7800)), + DeviceInfo("F4", "STM32F446xx", "", "", 0x421, 0x90, (0x_2000_3000, 0x_2002_0000), (0x_1FFF_0000, 0x_1FFF_7800)), + DeviceInfo("F4", "STM32F469xx/479xx", "", "", 0x434, 0x90, (0x_2000_3000, 0x_2006_0000), (0x_1FFF_0000, 0x_1FFF_7800)), + DeviceInfo("F4", "STM32F413xx/423xx", "", "", 0x463, 0x90, (0x_2000_3000, 0x_2005_0000), (0x_1FFF_0000, 0x_1FFF_7800)), + DeviceInfo("F7", "STM32F72xxx/73xxx", "", "", 0x452, 0x90, (0x_2000_4000, 0x_2004_0000), (0x_1FF0_0000, 0x_1FF0_EDC0)), + DeviceInfo("F7", "STM32F74xxx/75xxx", "", "", 0x449, 0x70, (0x_2000_4000, 0x_2005_0000), (0x_1FF0_0000, 0x_1FF0_EDC0)), + DeviceInfo("F7", "STM32F74xxx/75xxx", "", "", 0x449, 0x90, (0x_2000_4000, 0x_2005_0000), (0x_1FF0_0000, 0x_1FF0_EDC0)), + DeviceInfo("F7", "STM32F76xxx/77xxx", "", "", 0x451, 0x93, (0x_2000_4000, 0x_2008_0000), (0x_1FF0_0000, 0x_1FF0_EDC0)), + DeviceInfo("G0", "STM32G03xxx/04xxx", "", "", 0x466, 0x52, (0x_2000_1000, 0x_2000_2000), (0x_1FFF_0000, 0x_1FFF_2000)), + DeviceInfo("G0", "STM32G07xxx/08xxx", "", "", 0x460, 0xB3, (0x_2000_2700, 0x_2000_9000), (0x_1FFF_0000, 0x_1FFF_7000)), + DeviceInfo("G0", "STM32G0B0xx", "", "", 0x467, 0xD0, (0x_2000_4000, 0x_2002_0000), ((0x_1FFF_0000, 0x_1FFF_7000), (0x_1FFF_8000, 0x_1FFF_F000))), + DeviceInfo("G0", "STM32G0B1xx/0C1xx", "", "", 0x467, 0x92, (0x_2000_4000, 0x_2002_0000), ((0x_1FFF_0000, 0x_1FFF_6000), (0x_1FFF_8000, 0x_1FFF_F000))), + # Note: STM32flash has 0x_2000_4800 as upper flash range. + DeviceInfo("G0", "STM32G05xxx/061xx", "", "", 0x456, 0x51, (0x_2000_1000, 0x_2000_2000), (0x_1FFF_0000, 0x_1FFF_1000)), + DeviceInfo("G4", "STM32G431xx/441xx", "", "", 0x468, 0xD4, (0x_2000_4000, 0x_2000_5800), (0x_1FFF_0000, 0x_1FFF_7000)), + DeviceInfo("G4", "STM32G47xxx/48xxx", "", "", 0x469, 0xD5, (0x_2000_4000, 0x_2001_8000), (0x_1FFF_0000, 0x_1FFF_7000)), + DeviceInfo("G4", "STM32G491xx/A1xx", "", "", 0x479, 0xD2, (0x_2000_4000, 0x_2001_C000), (0x_1FFF_0000, 0x_1FFF_7000)), + DeviceInfo("H5", "STM32H503xx", "", "", 0x474, 0xE1, (0x_2000_4000, 0x_2000_8000), (0x_0BF8_7000, 0x_0BF9_0000)), + DeviceInfo("H5", "STM32H563xx/573xx", "", "", 0x484, 0xE3, (0x_2000_0000, 0x_200A_0000), (0x_0BF9_7000, 0x_0BFA_0000)), + DeviceInfo("H7", "STM32H72xxx/73xxx", "", "", 0x483, 0x93, ((0x_2000_4100, 0x_2002_0000), (0x_2400_4000, 0x_2405_0000)), (0x_1FF0_0000, 0x_1FF1_E800)), + DeviceInfo("H7", "STM32H74xxx/75xxx", "", "", 0x450, 0x91, ((0x_2000_4100, 0x_2002_0000), (0x_2400_5000, 0x_2408_0000)), (0x_1FF0_0000, 0x_1FF1_E800)), + DeviceInfo("H7", "STM32H7A3xx/B3xx", "", "", 0x480, 0x92, ((0x_2000_4100, 0x_2002_0000), (0x_2403_4000, 0x_2408_0000)), (0x_1FF0_0000, 0x_1FF1_4000)), + DeviceInfo("L0", "STM32L01xxx/02xxx", "", "", 0x457, 0xC3, None, (0x_1FF0_0000, 0x_1FF0_1000)), + DeviceInfo("L0", "STM32L031xx/041xx", "", "", 0x425, 0xC0, (0x_2000_1000, 0x_2000_2000), (0x_1FF0_0000, 0x_1FF0_1000)), + DeviceInfo("L0", "STM32L05xxx/06xxx", "", "", 0x417, 0xC0, (0x_2000_1000, 0x_2000_2000), (0x_1FF0_0000, 0x_1FF0_1000)), + # Note: STM32flash has 0x_2000_2000 as lower flash range. + DeviceInfo("L0", "STM32L07xxx/08xxx", "", "", 0x447, 0x41, (0x_2000_1000, 0x_2000_5000), (0x_1FF0_0000, 0x_1FF0_2000)), + DeviceInfo("L0", "STM32L07xxx/08xxx", "", "", 0x447, 0xB2, (0x_2000_1400, 0x_2000_5000), (0x_1FF0_0000, 0x_1FF0_2000)), + DeviceInfo("L1", "STM32L1xxx6(8/B)", "", "Medium-density ULP", 0x416, 0x20, (0x_2000_0800, 0x_2000_4000), (0x_1FF0_0000, 0x_1FF0_2000)), + DeviceInfo("L1", "STM32L1xxx6(8/B)A", "", "", 0x429, 0x20, (0x_2000_1000, 0x_2000_8000), (0x_1FF0_0000, 0x_1FF0_2000)), + DeviceInfo("L1", "STM32L1xxxC", "", "", 0x427, 0x40, (0x_2000_1000, 0x_2000_8000), (0x_1FF0_0000, 0x_1FF0_2000)), + DeviceInfo("L1", "STM32L1xxxD", "", "", 0x436, 0x45, (0x_2000_1000, 0x_2000_C000), (0x_1FF0_0000, 0x_1FF0_2000)), + DeviceInfo("L1", "STM32L1xxxE", "", "", 0x437, 0x40, (0x_2000_1000, 0x_2001_4000), (0x_1FF0_0000, 0x_1FF0_2000)), + # Note: Stm32flash has 0x_2000_3100 as ram start. + DeviceInfo("L4", "STM32L412xx/422xx", "", "Low-density", 0x464, 0xD1, (0x_2000_2100, 0x_2000_8000), (0x_1FFF_0000, 0x_1FFF_7000)), + DeviceInfo("L4", "STM32L43xxx/44xxx", "", "", 0x435, 0x91, (0x_2000_3100, 0x_2000_C000), (0x_1FFF_0000, 0x_1FFF_7000)), + DeviceInfo("L4", "STM32L45xxx/46xxx", "", "", 0x462, 0x92, (0x_2000_3100, 0x_2002_0000), (0x_1FFF_0000, 0x_1FFF_7000)), + DeviceInfo("L4", "STM32L47xxx/48xxx", "", "", 0x415, 0xA3, (0x_2000_3000, 0x_2001_8000), (0x_1FFF_0000, 0x_1FFF_7000)), + DeviceInfo("L4", "STM32L47xxx/48xxx", "", "", 0x415, 0x92, (0x_2000_3100, 0x_2001_8000), (0x_1FFF_0000, 0x_1FFF_7000)), + DeviceInfo("L4", "STM32L496xx/4A6xx", "", "", 0x461, 0x93, (0x_2000_3100, 0x_2004_0000), (0x_1FFF_0000, 0x_1FFF_7000)), + DeviceInfo("L4", "STM32L4Rxx/4Sxx", "", "", 0x470, 0x95, (0x_2000_3200, 0x_200A_0000), (0x_1FFF_0000, 0x_1FFF_7000)), + DeviceInfo("L4", "STM32L4P5xx/Q5xx", "", "", 0x471, 0x90, (0x_2000_4000, 0x_2005_0000), (0x_1FFF_0000, 0x_1FFF_7000)), + DeviceInfo("L5", "L5STM32L552xx/562xx", "", "", 0x472, 0x92, (0x_2000_4000, 0x_2004_0000), (0x_0BF9_0000, 0x_0BF9_8000)), + DeviceInfo("WBA", "STM32WBA52xx", "", "", 0x492, 0xB0, (0x_2000_0000, 0x_2000_2000), (0x_0BF8_8000, 0x_0BF9_0000)), + DeviceInfo("WB", "STM32WB10xx/15xx", "", "", 0x494, 0xB1, (0x_2000_5000, 0x_2004_0000), (0x_1FFF_0000, 0x_1FFF_7000)), + DeviceInfo("WB", "STM32WB30xx/35xx/50xx/WB55xx", "", "", 0x495, 0xD5, (0x_2000_4000, 0x_2000_C000), (0x_1FFF_0000, 0x_1FFF_7000)), + DeviceInfo("WL", "STM32WLE5xx/WL55xx", "", "", 0x497, 0xC4, (0x_2000_2000, 0x_2001_0000), (0x_1FFF_0000, 0x_1FFF_4000)), + DeviceInfo("U5", "STM32U535xx/545xx", "", "", 0x455, 0x91, (0x_2000_4000, 0x_2024_0000), (0x_0BF9_0000, 0x_0BFA_0000)), + DeviceInfo("U5", "STM32U575xx/ STM32U585xx", "", "", 0x482, 0x92, (0x_2000_4000, 0x_200C_0000), (0x_0BF9_0000, 0x_0BFA_0000)), + DeviceInfo("U5", "STM32U595xx/599xx/5A9xx", "", "", 0x481, 0x92, (0x_2000_4000, 0x_2027_0000), (0x_0BF9_0000, 0x_0BFA_0000)), + + # Not yet in AN2606. Bootloader IDs are unknown. + # F1 is assumed here. + DeviceInfo("F1", "STM32F103x8/B", "", "Medium-density performance", 0x641, None, (0x20000200, 0x20005000), (0x1FFFF000, 0x1FFFF800)), + # WBA, WB, WL or simply 'W'? + DeviceInfo("W", "STM32W 128k", "", "", 0x9A8, None, (0x20000200, 0x20002000), (0x08040000, 0x08040800)), + DeviceInfo("W", "STM32W 256k", "", "", 0x9B0, None, (0x20000200, 0x20004000), (0x08040000, 0x08040800)), ] DEVICES = {(dev.product_id, dev.bootloader_id): dev for dev in DEVICE_DETAILS} diff --git a/tests/unit/devices_stm32flash.py b/tests/unit/devices_stm32flash.py new file mode 100644 index 0000000..82eaee6 --- /dev/null +++ b/tests/unit/devices_stm32flash.py @@ -0,0 +1,117 @@ +p_128 = 128 +p_256 = 128 +p_1k = 1024 +p_2k = 2 * 1024 +p_4k = 4 * 1024 +p_8k = 8 * 1024 +p_128k = 128 * 1024 +F_OBLL = "unknown" +f2f4 = "unknown" +f4db = "unknown" +f7 = "unknown" +F_NO_ME = "no-mass-erase" +F_PEMPTY = "unknown" + +DEVICE_TABLE = [ + # ID name SRAM-address-range FLASH-address-range PPS PSize Option-byte-addr-range System-mem-addr-range Flags */ + # C0 + # (0x443, "STM32C011xx" , 0x20001000, 0x20003000, 0x08000000, x , x, x , x , x , 0x1FFF0000, 0x1FFF1800, 0) + # (0x453, "STM32C031xx" , 0x20001000, 0x20001800, 0x08000000, x , x, x , x , x , 0x1FFF0000, 0x1FFF1800, 0) + # F0 + (0x440, "STM32F030x8/F05xxx" , 0x20000800, 0x20002000, 0x08000000, 0x08010000, 4, p_1k , 0x1FFFF800, 0x1FFFF80F, 0x1FFFEC00, 0x1FFFF800, 0), + (0x444, "STM32F03xx4/6" , 0x20000800, 0x20001000, 0x08000000, 0x08008000, 4, p_1k , 0x1FFFF800, 0x1FFFF80F, 0x1FFFEC00, 0x1FFFF800, 0), + (0x442, "STM32F030xC/F09xxx" , 0x20001800, 0x20008000, 0x08000000, 0x08040000, 2, p_2k , 0x1FFFF800, 0x1FFFF80F, 0x1FFFD800, 0x1FFFF800, F_OBLL), + (0x445, "STM32F04xxx/F070x6" , 0x20001800, 0x20001800, 0x08000000, 0x08008000, 4, p_1k , 0x1FFFF800, 0x1FFFF80F, 0x1FFFC400, 0x1FFFF800, 0), + (0x448, "STM32F070xB/F071xx/F72xx" , 0x20001800, 0x20004000, 0x08000000, 0x08020000, 2, p_2k , 0x1FFFF800, 0x1FFFF80F, 0x1FFFC800, 0x1FFFF800, 0), + # F1 + (0x412, "STM32F10xxx Low-density" , 0x20000200, 0x20002800, 0x08000000, 0x08008000, 4, p_1k , 0x1FFFF800, 0x1FFFF80F, 0x1FFFF000, 0x1FFFF800, 0), + (0x410, "STM32F10xxx Medium-density" , 0x20000200, 0x20005000, 0x08000000, 0x08020000, 4, p_1k , 0x1FFFF800, 0x1FFFF80F, 0x1FFFF000, 0x1FFFF800, 0), + (0x414, "STM32F10xxx High-density" , 0x20000200, 0x20010000, 0x08000000, 0x08080000, 2, p_2k , 0x1FFFF800, 0x1FFFF80F, 0x1FFFF000, 0x1FFFF800, 0), + (0x420, "STM32F10xxx Medium-density VL" , 0x20000200, 0x20002000, 0x08000000, 0x08020000, 4, p_1k , 0x1FFFF800, 0x1FFFF80F, 0x1FFFF000, 0x1FFFF800, 0), + (0x428, "STM32F10xxx High-density VL" , 0x20000200, 0x20008000, 0x08000000, 0x08080000, 2, p_2k , 0x1FFFF800, 0x1FFFF80F, 0x1FFFF000, 0x1FFFF800, 0), + (0x418, "STM32F105xx/F107xx" , 0x20001000, 0x20010000, 0x08000000, 0x08040000, 2, p_2k , 0x1FFFF800, 0x1FFFF80F, 0x1FFFB000, 0x1FFFF800, 0), + (0x430, "STM32F10xxx XL-density" , 0x20000800, 0x20018000, 0x08000000, 0x08100000, 2, p_2k , 0x1FFFF800, 0x1FFFF80F, 0x1FFFE000, 0x1FFFF800, 0), + # F2 + (0x411, "STM32F2xxxx" , 0x20002000, 0x20020000, 0x08000000, 0x08100000, 1, f2f4 , 0x1FFFC000, 0x1FFFC00F, 0x1FFF0000, 0x1FFF7800, 0), + # F3 + (0x432, "STM32F373xx/F378xx" , 0x20001400, 0x20008000, 0x08000000, 0x08040000, 2, p_2k , 0x1FFFF800, 0x1FFFF80F, 0x1FFFD800, 0x1FFFF800, 0), + (0x422, "STM32F302xB(C)/F303xB(C)/F358xx" , 0x20001400, 0x2000A000, 0x08000000, 0x08040000, 2, p_2k , 0x1FFFF800, 0x1FFFF80F, 0x1FFFD800, 0x1FFFF800, 0), + (0x439, "STM32F301xx/F302x4(6/8)/F318xx" , 0x20001800, 0x20004000, 0x08000000, 0x08010000, 2, p_2k , 0x1FFFF800, 0x1FFFF80F, 0x1FFFD800, 0x1FFFF800, 0), + (0x438, "STM32F303x4(6/8)/F334xx/F328xx" , 0x20001800, 0x20003000, 0x08000000, 0x08010000, 2, p_2k , 0x1FFFF800, 0x1FFFF80F, 0x1FFFD800, 0x1FFFF800, 0), + (0x446, "STM32F302xD(E)/F303xD(E)/F398xx" , 0x20001800, 0x20010000, 0x08000000, 0x08080000, 2, p_2k , 0x1FFFF800, 0x1FFFF80F, 0x1FFFD800, 0x1FFFF800, 0), + # F4 + (0x413, "STM32F40xxx/41xxx" , 0x20003000, 0x20020000, 0x08000000, 0x08100000, 1, f2f4 , 0x1FFFC000, 0x1FFFC00F, 0x1FFF0000, 0x1FFF7800, 0), + (0x419, "STM32F42xxx/43xxx" , 0x20003000, 0x20030000, 0x08000000, 0x08200000, 1, f4db , 0x1FFEC000, 0x1FFFC00F, 0x1FFF0000, 0x1FFF7800, 0), + (0x423, "STM32F401xB(C)" , 0x20003000, 0x20010000, 0x08000000, 0x08040000, 1, f2f4 , 0x1FFFC000, 0x1FFFC00F, 0x1FFF0000, 0x1FFF7800, 0), + (0x433, "STM32F401xD(E)" , 0x20003000, 0x20018000, 0x08000000, 0x08080000, 1, f2f4 , 0x1FFFC000, 0x1FFFC00F, 0x1FFF0000, 0x1FFF7800, 0), + (0x458, "STM32F410xx" , 0x20003000, 0x20008000, 0x08000000, 0x08020000, 1, f2f4 , 0x1FFFC000, 0x1FFFC00F, 0x1FFF0000, 0x1FFF7800, 0), + (0x431, "STM32F411xx" , 0x20003000, 0x20020000, 0x08000000, 0x08080000, 1, f2f4 , 0x1FFFC000, 0x1FFFC00F, 0x1FFF0000, 0x1FFF7800, 0), + (0x441, "STM32F412xx" , 0x20003000, 0x20040000, 0x08000000, 0x08100000, 1, f2f4 , 0x1FFFC000, 0x1FFFC00F, 0x1FFF0000, 0x1FFF7800, 0), + (0x421, "STM32F446xx" , 0x20003000, 0x20020000, 0x08000000, 0x08080000, 1, f2f4 , 0x1FFFC000, 0x1FFFC00F, 0x1FFF0000, 0x1FFF7800, 0), + (0x434, "STM32F469xx/479xx" , 0x20003000, 0x20060000, 0x08000000, 0x08200000, 1, f4db , 0x1FFEC000, 0x1FFFC00F, 0x1FFF0000, 0x1FFF7800, 0), + (0x463, "STM32F413xx/423xx" , 0x20003000, 0x20050000, 0x08000000, 0x08180000, 1, f2f4 , 0x1FFFC000, 0x1FFFC00F, 0x1FFF0000, 0x1FFF7800, 0), + # F7 + (0x452, "STM32F72xxx/73xxx" , 0x20004000, 0x20040000, 0x08000000, 0x08080000, 1, f2f4 , 0x1FFF0000, 0x1FFF001F, 0x1FF00000, 0x1FF0EDC0, 0), + (0x449, "STM32F74xxx/75xxx" , 0x20004000, 0x20050000, 0x08000000, 0x08100000, 1, f7 , 0x1FFF0000, 0x1FFF001F, 0x1FF00000, 0x1FF0EDC0, 0), + (0x451, "STM32F76xxx/77xxx" , 0x20004000, 0x20080000, 0x08000000, 0x08200000, 1, f7 , 0x1FFF0000, 0x1FFF001F, 0x1FF00000, 0x1FF0EDC0, 0), + # G0 + (0x466, "STM32G03xxx/04xxx" , 0x20001000, 0x20002000, 0x08000000, 0x08010000, 1, p_2k , 0x1FFF7800, 0x1FFF787F, 0x1FFF0000, 0x1FFF2000, 0), + (0x460, "STM32G07xxx/08xxx" , 0x20002700, 0x20009000, 0x08000000, 0x08020000, 1, p_2k , 0x1FFF7800, 0x1FFF787F, 0x1FFF0000, 0x1FFF7000, 0), + (0x467, "STM32G0B0/B1/C1xx" , 0x20004000, 0x20020000, 0x08000000, 0x08080000, 1, p_2k , 0x1FFF7800, 0x1FFF787F, 0x1FFF0000, 0x1FFF7000, 0), + (0x456, "STM32G05xxx/061xx" , 0x20001000, 0x20004800, 0x08000000, 0x08010000, 1, p_2k , 0x1FFF7800, 0x1FFF787F, 0x1FFF0000, 0x1FFF2000, 0), + # G4 + (0x468, "STM32G431xx/441xx" , 0x20004000, 0x20005800, 0x08000000, 0x08020000, 1, p_2k , 0x1FFF7800, 0x1FFF782F, 0x1FFF0000, 0x1FFF7000, 0), + (0x469, "STM32G47xxx/48xxx" , 0x20004000, 0x20018000, 0x08000000, 0x08080000, 1, p_2k , 0x1FFF7800, 0x1FFF782F, 0x1FFF0000, 0x1FFF7000, 0), + (0x479, "STM32G491xx/A1xx" , 0x20004000, 0x2001C000, 0x08000000, 0x08080000, 1, p_2k , 0x1FFF7800, 0x1FFF782F, 0x1FFF0000, 0x1FFF7000, 0), + # H7 + (0x483, "STM32H72xxx/73xxx" , 0x20004100, 0x20020000, 0x80000000, 0x08100000, 1, p_128k, 0 , 0 , 0x1FF00000, 0x1FF1E800, 0), + (0x450, "STM32H74xxx/75xxx" , 0x20004100, 0x20020000, 0x08000000, 0x08200000, 1, p_128k, 0 , 0 , 0x1FF00000, 0x1FF1E800, 0), + (0x480, "STM32H7A3xx/B3xx" , 0x20004100, 0x20020000, 0x08000000, 0x08100000, 1, p_8k , 0 , 0 , 0x1FF00000, 0x1FF14000, 0), + # L0 + (0x457, "STM32L01xxx/02xxx" , 0x20000800, 0x20000800, 0x08000000, 0x08004000, 32, p_128 , 0x1FF80000, 0x1FF8001F, 0x1FF00000, 0x1FF01000, F_NO_ME), + (0x425, "STM32L031xx/041xx" , 0x20001000, 0x20002000, 0x08000000, 0x08008000, 32, p_128 , 0x1FF80000, 0x1FF8001F, 0x1FF00000, 0x1FF01000, F_NO_ME), + (0x417, "STM32L05xxx/06xxx" , 0x20001000, 0x20002000, 0x08000000, 0x08010000, 32, p_128 , 0x1FF80000, 0x1FF8001F, 0x1FF00000, 0x1FF01000, F_NO_ME), + (0x447, "STM32L07xxx/08xxx" , 0x20002000, 0x20005000, 0x08000000, 0x08030000, 32, p_128 , 0x1FF80000, 0x1FF8001F, 0x1FF00000, 0x1FF02000, F_NO_ME), + # L1 + (0x416, "STM32L1xxx6(8/B)" , 0x20000800, 0x20004000, 0x08000000, 0x08020000, 16, p_256 , 0x1FF80000, 0x1FF8001F, 0x1FF00000, 0x1FF01000, F_NO_ME), + (0x429, "STM32L1xxx6(8/B)A" , 0x20001000, 0x20008000, 0x08000000, 0x08020000, 16, p_256 , 0x1FF80000, 0x1FF8001F, 0x1FF00000, 0x1FF01000, F_NO_ME), + (0x427, "STM32L1xxxC" , 0x20001000, 0x20008000, 0x08000000, 0x08040000, 16, p_256 , 0x1FF80000, 0x1FF8001F, 0x1FF00000, 0x1FF02000, F_NO_ME), + (0x436, "STM32L1xxxD" , 0x20001000, 0x2000C000, 0x08000000, 0x08060000, 16, p_256 , 0x1FF80000, 0x1FF8001F, 0x1FF00000, 0x1FF02000, F_NO_ME), + (0x437, "STM32L1xxxE" , 0x20001000, 0x20014000, 0x08000000, 0x08080000, 16, p_256 , 0x1FF80000, 0x1FF8001F, 0x1FF00000, 0x1FF02000, F_NO_ME), + # L4 + (0x464, "STM32L412xx/422xx" , 0x20003100, 0x20008000, 0x08000000, 0x08020000, 1, p_2k , 0x1FFF7800, 0x1FFF780F, 0x1FFF0000, 0x1FFF7000, 0), + (0x435, "STM32L43xxx/44xxx" , 0x20003100, 0x2000C000, 0x08000000, 0x08040000, 1, p_2k , 0x1FFF7800, 0x1FFF780F, 0x1FFF0000, 0x1FFF7000, 0), + (0x462, "STM32L45xxx/46xxx" , 0x20003100, 0x20020000, 0x08000000, 0x08080000, 1, p_2k , 0x1FFF7800, 0x1FFF780F, 0x1FFF0000, 0x1FFF7000, F_PEMPTY), + (0x415, "STM32L47xxx/48xxx" , 0x20003100, 0x20018000, 0x08000000, 0x08100000, 1, p_2k , 0x1FFF7800, 0x1FFFF80F, 0x1FFF0000, 0x1FFF7000, 0), + (0x461, "STM32L496xx/4A6xx" , 0x20003100, 0x20040000, 0x08000000, 0x08100000, 1, p_2k , 0x1FFF7800, 0x1FFFF80F, 0x1FFF0000, 0x1FFF7000, 0), + (0x470, "STM32L4Rxx/4Sxx" , 0x20003200, 0x200A0000, 0x08000000, 0x08100000, 1, p_2k , 0x1FFF7800, 0x1FFFF80F, 0x1FFF0000, 0x1FFF7000, 0), + (0x471, "STM32L4P5xx/Q5xx" , 0x20004000, 0x20050000, 0x08000000, 0x08100000, 1, p_4k , 0x1FF00000, 0x1FF0000F, 0x1FFF0000, 0x1FFF7000, 0), # dual-bank + # L5 + (0x472, "STM32L552xx/562xx" , 0x20004000, 0x20040000, 0x08000000, 0x08080000, 1, p_2k , 0 , 0 , 0x0BF90000, 0x0BF98000, 0), # dual-bank + # WB + (0x494, "STM32WB10xx/15xx" , 0x20005000, 0x20040000, 0x08000000, 0x08050000, 1, p_2k , 0x1FFF7800, 0x1FFF787F, 0x1FFF0000, 0x1FFF7000, 0), + (0x495, "STM32WB30(5)xx/50(5)xx" , 0x20004000, 0x2000C000, 0x08000000, 0x08100000, 1, p_4k , 0x1FFF8000, 0x1FFF807F, 0x1FFF0000, 0x1FFF7000, 0), + # WL + (0x497, "STM32WLE5xx/WL55xx" , 0x20002000, 0x20010000, 0x08000000, 0x08040000, 1, p_2k , 0x1FFF7800, 0x1FFF8000, 0x1FFF0000, 0x1FFF4000, 0), + # U5 + (0x482, "STM32U575xx/585xx" , 0x20004000, 0x200C0000, 0x08000000, 0x08200000, 1, p_8k , 0 , 0 , 0x0BF90000, 0x0BFA0000, 0), + # These are not (yet) in AN2606: + (0x641, "Medium_Density PL" , 0x20000200, 0x20005000, 0x08000000, 0x08020000, 4, p_1k , 0x1FFFF800, 0x1FFFF80F, 0x1FFFF000, 0x1FFFF800, 0), + (0x9a8, "STM32W-128K" , 0x20000200, 0x20002000, 0x08000000, 0x08020000, 4, p_1k , 0x08040800, 0x0804080F, 0x08040000, 0x08040800, 0), + (0x9b0, "STM32W-256K" , 0x20000200, 0x20004000, 0x08000000, 0x08040000, 4, p_2k , 0x08040800, 0x0804080F, 0x08040000, 0x08040800, 0), +] + +KEYS = [ + "product_id", "product_name", + "ram_start", "ram_end", + "flash_start", "flash_end", + "pages_per_sector", "page_size", + "option_start", "option_end", + "system_mem_start", "system_mem_end", + "flag", +] + +DEVICES = [ + dict(zip(KEYS, details)) + for details in DEVICE_TABLE +] \ No newline at end of file diff --git a/tests/unit/test_devices.py b/tests/unit/test_devices.py index a420f17..96c784c 100644 --- a/tests/unit/test_devices.py +++ b/tests/unit/test_devices.py @@ -1,5 +1,7 @@ from stm32loader.devices import DEVICES +from stm32loader.bootloader import CHIP_IDS +from devices_stm32flash import DEVICES as STM32FLASH_DEVICES KNOWN_DUPLICATE_DEVICE_NAMES = [ "STM32F2xxxx", @@ -8,6 +10,23 @@ "STM32F74xxx/75xxx", "STM32L07xxx/08xxx", "STM32L47xxx/48xxx", + "STM32F10xxx", + "BlueNRG-1", + "BlueNRG-2", +] + +KNOWN_RAM_EXCEPTIONS = [ + # Most of these have belong to the 'known duplicate' category, + # they share the same product ID (with differing bootloader ID). + # The others received a comment in the device table. + 0x442, + 0x448, + 0x432, + 0x413, + 0x456, + 0x447, + 0x464, + 0x415, ] @@ -49,3 +68,44 @@ def test_system_memory_size_multiple_of_64(): def test_device_name_does_not_contain_underscore(): for dev in DEVICES.values(): assert "_" not in dev.device_name, dev.device_name + + +def test_existing_product_ids_are_present_in_devices(): + all_product_ids = set(dev.product_id for dev in DEVICES.values()) + chip_ids = set(CHIP_IDS) + unknown_chip_ids = chip_ids - all_product_ids + assert len(unknown_chip_ids) == 0, unknown_chip_ids + + +def test_stm32flash_product_ids_are_present_in_devices(): + all_product_ids = set(dev.product_id for dev in DEVICES.values()) + chip_ids = set(dev["product_id"] for dev in STM32FLASH_DEVICES) + unknown_chip_ids = chip_ids - all_product_ids + assert len(unknown_chip_ids) == 0, unknown_chip_ids + + +def test_stm32flash_ram_addresses_match(): + for device in DEVICES.values(): + ref = None + for _ref in STM32FLASH_DEVICES: + if _ref["product_id"] == device.product_id: + ref = _ref + break + + if ref is None: + # not found + continue + + if device.product_id in KNOWN_RAM_EXCEPTIONS: + continue + + if device.ram is None: + assert ref["ram_start"] == ref["ram_end"], f"RAM size not 0 for device '{device.device_name}' 0x{device.product_id:03X}" + continue + + if isinstance(device.ram[0], tuple): + continue + + # print(hex(device.product_id), device, device.ram, ref) + assert device.ram[0] == ref["ram_start"], f"RAM start differs for device: '{device.device_name}' 0x{device.product_id:03X}: 0x{device.ram[0]:08X} vs 0x{ref['ram_start']:08X}." + assert device.ram[1] == ref["ram_end"], f"RAM end differs for device: '{device.device_name}' 0x{device.product_id:03X}: 0x{device.ram[1]:08X} vs 0x{ref['ram_end']:08X}." From afccee8dfe6b8ca89d4cae821034c496f484436c Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Thu, 19 Oct 2023 22:14:33 +0200 Subject: [PATCH 306/369] dev: Add BlueNRG and non-STM devices --- src/stm32loader/devices.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/stm32loader/devices.py b/src/stm32loader/devices.py index e0bf253..764aa0b 100644 --- a/src/stm32loader/devices.py +++ b/src/stm32loader/devices.py @@ -183,6 +183,15 @@ def __str__(self): # WBA, WB, WL or simply 'W'? DeviceInfo("W", "STM32W 128k", "", "", 0x9A8, None, (0x20000200, 0x20002000), (0x08040000, 0x08040800)), DeviceInfo("W", "STM32W 256k", "", "", 0x9B0, None, (0x20000200, 0x20004000), (0x08040000, 0x08040800)), + + # ST BlueNRG + DeviceInfo("NRG1", "BlueNRG-1", "160kB", "", 0x03, None, None, None), + DeviceInfo("NRG1", "BlueNRG-1", "256kB", "", 0x0F, None, None, None), + DeviceInfo("NRG2", "BlueNRG-1", "160kB", "", 0x23, None, None, None), + DeviceInfo("NRG2", "BlueNRG-1", "256kB", "", 0x2F, None, None, None), + + # Wiznet W7500 + DeviceInfo("WIZ", "Wiznet W7500", "", "", 0x801, None, None, None), ] DEVICES = {(dev.product_id, dev.bootloader_id): dev for dev in DEVICE_DETAILS} From 5d5247e07e3aa0f3f1737631cefda97840c59ebc Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Fri, 20 Oct 2023 09:01:27 +0200 Subject: [PATCH 307/369] dev: Track some device details through their family --- src/stm32loader/devices.py | 60 ++++++++++++++++++++++++++++++++++++++ tests/unit/test_devices.py | 37 +++++++++++++++++++++-- 2 files changed, 95 insertions(+), 2 deletions(-) diff --git a/src/stm32loader/devices.py b/src/stm32loader/devices.py index 764aa0b..5e94a62 100644 --- a/src/stm32loader/devices.py +++ b/src/stm32loader/devices.py @@ -32,6 +32,66 @@ class DeviceFamily(enum.Enum): WIZ = "WIZ" +class DeviceFamilyInfo: + + def __init__( + self, + uid_address=None, + flash_size_address=None, + flash_page_size=1024, + transfer_size=256, + mass_erase=True, + option_bytes=None, + ): + self.uid_address = uid_address + self.flash_size_address = flash_size_address + self.flash_page_size = flash_page_size + self.transfer_size = transfer_size + self.mass_erase = mass_erase + self.option_bytes = option_bytes + + +DEVICE_FAMILIES = { + DeviceFamily.C0: DeviceFamilyInfo(), + # RM0360 + DeviceFamily.F0: DeviceFamilyInfo(flash_size_address=0x1FFFF7CC, option_bytes=(0x1FFFF800, 0x1FFFF80F)), + # RM0008 + DeviceFamily.F1: DeviceFamilyInfo(uid_address=0x1FFFF7E8, flash_size_address=0x1FFFF7E0, option_bytes=(0x1FFFF800, 0x1FFFF80F)), + DeviceFamily.F2: DeviceFamilyInfo(option_bytes=(0x1FFFC000, 0x1FFFC00F)), + # RM0366, RM0365, RM0316, RM0313, RM4510 + DeviceFamily.F3: DeviceFamilyInfo(uid_address=0x1FFFF7AC, flash_size_address=0x1FFFF7CC, flash_page_size=2048), + # RM0090 + DeviceFamily.F4: DeviceFamilyInfo(uid_address=0x1FFF7A10, flash_size_address=0x1FFF7A22), + # RM0385 + DeviceFamily.F7: DeviceFamilyInfo(uid_address=0x1FF0F420, flash_size_address=0x1FF0F442), + # RM0444 + DeviceFamily.G0: DeviceFamilyInfo(uid_address=0x1FFF7590, flash_size_address=0x1FFF75E0), + DeviceFamily.G4: DeviceFamilyInfo(), + DeviceFamily.H5: DeviceFamilyInfo(), + # RM0433 + DeviceFamily.H7: DeviceFamilyInfo(uid_address=0x1FF1E800, flash_size_address=0x1FF1E880, flash_page_size=128 * 1024), + # FIXME TWO RMs? + # RM0451, RM4510 + DeviceFamily.L0: DeviceFamilyInfo(uid_address=0x1FF80050, flash_size_address=0x1FF8007C, transfer_size=128, flash_page_size=128, mass_erase=False), + DeviceFamily.L1: DeviceFamilyInfo(mass_erase=False), + # RM0394 + DeviceFamily.L4: DeviceFamilyInfo(uid_address=0x1FFF7590, flash_size_address=0x1FFF75E0), + DeviceFamily.L5: DeviceFamilyInfo(), + DeviceFamily.WBA: DeviceFamilyInfo(), + DeviceFamily.WB: DeviceFamilyInfo(), + # RM0453 + DeviceFamily.WL: DeviceFamilyInfo(uid_address=0x1FFF7590, flash_size_address=0x1FFF75E0), + DeviceFamily.U5: DeviceFamilyInfo(), + DeviceFamily.W: DeviceFamilyInfo(), + # ST BlueNRG has DIE_ID register with PRODUCT, but no UID. + # NRG BlueNRG-2 datasheet + # Flash page size: 128 pages of 8 * 64 * 4 bytes + DeviceFamily.NRG1: DeviceFamilyInfo(flash_size_address=0x40100014, flash_page_size=2048), + DeviceFamily.NRG2: DeviceFamilyInfo(flash_size_address=0x40100014, flash_page_size=2048), + DeviceFamily.WIZ: DeviceFamilyInfo(), +} + + class DeviceInfo: def __init__(self, device_family, device_name, variant, product_line, product_id, bootloader_id, ram, system_memory): diff --git a/tests/unit/test_devices.py b/tests/unit/test_devices.py index 96c784c..4c8f65d 100644 --- a/tests/unit/test_devices.py +++ b/tests/unit/test_devices.py @@ -1,6 +1,6 @@ -from stm32loader.devices import DEVICES -from stm32loader.bootloader import CHIP_IDS +from stm32loader.devices import DEVICES, DEVICE_FAMILIES, DeviceFamily +from stm32loader.bootloader import CHIP_IDS, Stm32Bootloader from devices_stm32flash import DEVICES as STM32FLASH_DEVICES KNOWN_DUPLICATE_DEVICE_NAMES = [ @@ -109,3 +109,36 @@ def test_stm32flash_ram_addresses_match(): # print(hex(device.product_id), device, device.ram, ref) assert device.ram[0] == ref["ram_start"], f"RAM start differs for device: '{device.device_name}' 0x{device.product_id:03X}: 0x{device.ram[0]:08X} vs 0x{ref['ram_start']:08X}." assert device.ram[1] == ref["ram_end"], f"RAM end differs for device: '{device.device_name}' 0x{device.product_id:03X}: 0x{device.ram[1]:08X} vs 0x{ref['ram_end']:08X}." + + +def test_family_uid_address_matches_existing(): + for family_code, uid_address in Stm32Bootloader.UID_ADDRESS.items(): + if family_code == "NRG": + continue + family = DeviceFamily[family_code] + family_uid_address = DEVICE_FAMILIES[family].uid_address + assert uid_address == family_uid_address, ( + f"Device family UID address does not match: '{family_code}': 0x{uid_address:08X} vs 0x{family_uid_address:08X}." + ) + + +def test_family_flash_size_address_matches_existing(): + for family_code, size_address in Stm32Bootloader.FLASH_SIZE_ADDRESS.items(): + if family_code == "NRG": + continue + family = DeviceFamily[family_code] + family_size_address = DEVICE_FAMILIES[family].flash_size_address + assert size_address == family_size_address, ( + f"Device family flash size address does not match: '{family_code}': 0x{size_address:08X} vs 0x{family_size_address:08X}." + ) + + +def test_family_transfer_size_matches_existing(): + for family_code, transfer_size in Stm32Bootloader.DATA_TRANSFER_SIZE.items(): + if family_code in ["default", "NRG"]: + continue + family = DeviceFamily[family_code] + family_transfer_size = DEVICE_FAMILIES[family].transfer_size + assert transfer_size == family_transfer_size, ( + f"Device family transfer size does not match: '{family_code}': 0x{transfer_size:08X} vs 0x{family_transfer_size:08X}." + ) From 4b70e2666f76a7cdccb852e8a84b45048b70bfc2 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Fri, 20 Oct 2023 09:29:04 +0200 Subject: [PATCH 308/369] fix: Typo in stm32flash device table --- tests/unit/devices_stm32flash.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/devices_stm32flash.py b/tests/unit/devices_stm32flash.py index 82eaee6..af352b6 100644 --- a/tests/unit/devices_stm32flash.py +++ b/tests/unit/devices_stm32flash.py @@ -22,7 +22,7 @@ (0x444, "STM32F03xx4/6" , 0x20000800, 0x20001000, 0x08000000, 0x08008000, 4, p_1k , 0x1FFFF800, 0x1FFFF80F, 0x1FFFEC00, 0x1FFFF800, 0), (0x442, "STM32F030xC/F09xxx" , 0x20001800, 0x20008000, 0x08000000, 0x08040000, 2, p_2k , 0x1FFFF800, 0x1FFFF80F, 0x1FFFD800, 0x1FFFF800, F_OBLL), (0x445, "STM32F04xxx/F070x6" , 0x20001800, 0x20001800, 0x08000000, 0x08008000, 4, p_1k , 0x1FFFF800, 0x1FFFF80F, 0x1FFFC400, 0x1FFFF800, 0), - (0x448, "STM32F070xB/F071xx/F72xx" , 0x20001800, 0x20004000, 0x08000000, 0x08020000, 2, p_2k , 0x1FFFF800, 0x1FFFF80F, 0x1FFFC800, 0x1FFFF800, 0), + (0x448, "STM32F070xB/F071xx/F072xx" , 0x20001800, 0x20004000, 0x08000000, 0x08020000, 2, p_2k , 0x1FFFF800, 0x1FFFF80F, 0x1FFFC800, 0x1FFFF800, 0), # F1 (0x412, "STM32F10xxx Low-density" , 0x20000200, 0x20002800, 0x08000000, 0x08008000, 4, p_1k , 0x1FFFF800, 0x1FFFF80F, 0x1FFFF000, 0x1FFFF800, 0), (0x410, "STM32F10xxx Medium-density" , 0x20000200, 0x20005000, 0x08000000, 0x08020000, 4, p_1k , 0x1FFFF800, 0x1FFFF80F, 0x1FFFF000, 0x1FFFF800, 0), From e2b41e61d472b4c584dcefa5ade1517425bd5f7c Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Fri, 20 Oct 2023 09:29:35 +0200 Subject: [PATCH 309/369] dev: Bring device names more in line with STM32flash --- src/stm32loader/devices.py | 14 +++++++------- tests/unit/devices_stm32flash.py | 2 +- tests/unit/test_devices.py | 24 ++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/src/stm32loader/devices.py b/src/stm32loader/devices.py index 5e94a62..f101bc4 100644 --- a/src/stm32loader/devices.py +++ b/src/stm32loader/devices.py @@ -151,14 +151,14 @@ def __str__(self): DeviceInfo("C0", "STM32C011xx", "", "", 0x443, 0x51, (0x_2000_0000, 0x_2000_3000), (0x_1FFF_0000, 0x_1FFF_1800)), # Error in AN2606? Ram is mentioned as 0x_2000_2000 - 0x_2000_17FF DeviceInfo("C0", "STM32C031xx", "", "", 0x453, 0x52, (0x_2000_2000, 0x_2000_2800), (0x_1FFF_0000, 0x_1FFF_1800)), - DeviceInfo("F0", "STM32F05xxx/STM32F030x8", "", "", 0x440, 0x21, (0x_2000_0800, 0x_2000_2000), (0x_1FFF_EC00, 0x_1FFF_F800)), + DeviceInfo("F0", "STM32F05xxx/030x8", "", "", 0x440, 0x21, (0x_2000_0800, 0x_2000_2000), (0x_1FFF_EC00, 0x_1FFF_F800)), DeviceInfo("F0", "STM32F03xx4/6", "", "", 0x444, 0x10, (0x_2000_0800, 0x_2000_1000), (0x_1FFF_EC00, 0x_1FFF_F800)), DeviceInfo("F0", "STM32F030xC", "", "", 0x442, 0x52, (0x_2000_1800, 0x_2000_8000), (0x_1FFF_D800, 0x_1FFF_F800)), DeviceInfo("F0", "STM32F04xxx", "", "", 0x445, 0xA1, None, (0x_1FFF_C400, 0x_1FFF_F800)), DeviceInfo("F0", "STM32F070x6", "", "", 0x445, 0xA2, None, (0x_1FFF_C400, 0x_1FFF_F800)), DeviceInfo("F0", "STM32F070xB", "", "", 0x448, 0xA2, None, (0x_1FFF_C800, 0x_1FFF_F800)), DeviceInfo("F0", "STM32F071xx/072xx", "", "", 0x448, 0xA1, (0x_2000_1800, 0x_2000_4000), (0x_1FFF_C800, 0x_1FFF_F800)), - DeviceInfo("F0", "STM32F09xxx", 0x442, "", "", 0x50, None, (0x_1FFF_D800, 0x_1FFF_F800)), + DeviceInfo("F0", "STM32F09xxx", "", "", 0x442, 0x50, None, (0x_1FFF_D800, 0x_1FFF_F800)), DeviceInfo("F1", "STM32F10xxx", "", "Low-density", 0x412, None, (0x_2000_0200, 0x_2000_2800), (0x_1FFF_F000, 0x_1FFF_F800)), DeviceInfo("F1", "STM32F10xxx", "", "Medium-density", 0x410, None, (0x_2000_0200, 0x_2000_5000), (0x_1FFF_F000, 0x_1FFF_F800)), DeviceInfo("F1", "STM32F10xxx", "", "High-density", 0x414, None, (0x_2000_0200, 0x_2001_0000), (0x_1FFF_F000, 0x_1FFF_F800)), @@ -228,21 +228,21 @@ def __str__(self): DeviceInfo("L4", "STM32L496xx/4A6xx", "", "", 0x461, 0x93, (0x_2000_3100, 0x_2004_0000), (0x_1FFF_0000, 0x_1FFF_7000)), DeviceInfo("L4", "STM32L4Rxx/4Sxx", "", "", 0x470, 0x95, (0x_2000_3200, 0x_200A_0000), (0x_1FFF_0000, 0x_1FFF_7000)), DeviceInfo("L4", "STM32L4P5xx/Q5xx", "", "", 0x471, 0x90, (0x_2000_4000, 0x_2005_0000), (0x_1FFF_0000, 0x_1FFF_7000)), - DeviceInfo("L5", "L5STM32L552xx/562xx", "", "", 0x472, 0x92, (0x_2000_4000, 0x_2004_0000), (0x_0BF9_0000, 0x_0BF9_8000)), + DeviceInfo("L5", "STM32L552xx/562xx", "", "", 0x472, 0x92, (0x_2000_4000, 0x_2004_0000), (0x_0BF9_0000, 0x_0BF9_8000)), DeviceInfo("WBA", "STM32WBA52xx", "", "", 0x492, 0xB0, (0x_2000_0000, 0x_2000_2000), (0x_0BF8_8000, 0x_0BF9_0000)), DeviceInfo("WB", "STM32WB10xx/15xx", "", "", 0x494, 0xB1, (0x_2000_5000, 0x_2004_0000), (0x_1FFF_0000, 0x_1FFF_7000)), - DeviceInfo("WB", "STM32WB30xx/35xx/50xx/WB55xx", "", "", 0x495, 0xD5, (0x_2000_4000, 0x_2000_C000), (0x_1FFF_0000, 0x_1FFF_7000)), + DeviceInfo("WB", "STM32WB30xx/35xx/50xx/55xx", "", "", 0x495, 0xD5, (0x_2000_4000, 0x_2000_C000), (0x_1FFF_0000, 0x_1FFF_7000)), DeviceInfo("WL", "STM32WLE5xx/WL55xx", "", "", 0x497, 0xC4, (0x_2000_2000, 0x_2001_0000), (0x_1FFF_0000, 0x_1FFF_4000)), DeviceInfo("U5", "STM32U535xx/545xx", "", "", 0x455, 0x91, (0x_2000_4000, 0x_2024_0000), (0x_0BF9_0000, 0x_0BFA_0000)), - DeviceInfo("U5", "STM32U575xx/ STM32U585xx", "", "", 0x482, 0x92, (0x_2000_4000, 0x_200C_0000), (0x_0BF9_0000, 0x_0BFA_0000)), + DeviceInfo("U5", "STM32U575xx/585xx", "", "", 0x482, 0x92, (0x_2000_4000, 0x_200C_0000), (0x_0BF9_0000, 0x_0BFA_0000)), DeviceInfo("U5", "STM32U595xx/599xx/5A9xx", "", "", 0x481, 0x92, (0x_2000_4000, 0x_2027_0000), (0x_0BF9_0000, 0x_0BFA_0000)), # Not yet in AN2606. Bootloader IDs are unknown. # F1 is assumed here. DeviceInfo("F1", "STM32F103x8/B", "", "Medium-density performance", 0x641, None, (0x20000200, 0x20005000), (0x1FFFF000, 0x1FFFF800)), # WBA, WB, WL or simply 'W'? - DeviceInfo("W", "STM32W 128k", "", "", 0x9A8, None, (0x20000200, 0x20002000), (0x08040000, 0x08040800)), - DeviceInfo("W", "STM32W 256k", "", "", 0x9B0, None, (0x20000200, 0x20004000), (0x08040000, 0x08040800)), + DeviceInfo("W", "STM32W", "128kB", "", 0x9A8, None, (0x20000200, 0x20002000), (0x08040000, 0x08040800)), + DeviceInfo("W", "STM32W", "256kB", "", 0x9B0, None, (0x20000200, 0x20004000), (0x08040000, 0x08040800)), # ST BlueNRG DeviceInfo("NRG1", "BlueNRG-1", "160kB", "", 0x03, None, None, None), diff --git a/tests/unit/devices_stm32flash.py b/tests/unit/devices_stm32flash.py index af352b6..fe39c6b 100644 --- a/tests/unit/devices_stm32flash.py +++ b/tests/unit/devices_stm32flash.py @@ -102,7 +102,7 @@ ] KEYS = [ - "product_id", "product_name", + "product_id", "device_name", "ram_start", "ram_end", "flash_start", "flash_end", "pages_per_sector", "page_size", diff --git a/tests/unit/test_devices.py b/tests/unit/test_devices.py index 4c8f65d..0e60171 100644 --- a/tests/unit/test_devices.py +++ b/tests/unit/test_devices.py @@ -84,6 +84,30 @@ def test_stm32flash_product_ids_are_present_in_devices(): assert len(unknown_chip_ids) == 0, unknown_chip_ids +def test_stm32flash_device_names_match(): + for device in DEVICES.values(): + stm32flash_device = None + for dev in STM32FLASH_DEVICES: + if dev["product_id"] == device.product_id: + stm32flash_device = dev + break + + # Some devices don't exist in STM32Flash. + if not stm32flash_device and device.product_id in [0x443, 0x453, 0x474, 0x484, 0x492, 0x455, 0x481, 0x003, 0x00F, 0x0023, 0x002F, 0x801]: + continue + + # Known / reviewed deviating names. + if device.product_id in [ + 0x440, 0x442, 0x445, 0x448, 0x412, 0x410, 0x414, 0x420, 0x428, 0x418, 0x430, 0x432, + 0x422, 0x439, 0X438, 0x446, 0x467, 0x495, 0x641, 0x9A8, 0x9B0, + ]: + continue + + assert stm32flash_device, f"{device.device_name} 0x{device.product_id:03X}" + assert stm32flash_device["device_name"] == device.device_name, f"{device.device_name} 0x{device.product_id:03X}" + + + def test_stm32flash_ram_addresses_match(): for device in DEVICES.values(): ref = None From 36ae57fb81f5d8619432f5301d75f64f88ff3e0d Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 23 Oct 2023 09:50:56 +0200 Subject: [PATCH 310/369] fix: Typo in stm32flash device table --- tests/unit/devices_stm32flash.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/devices_stm32flash.py b/tests/unit/devices_stm32flash.py index fe39c6b..7afb394 100644 --- a/tests/unit/devices_stm32flash.py +++ b/tests/unit/devices_stm32flash.py @@ -64,7 +64,7 @@ (0x469, "STM32G47xxx/48xxx" , 0x20004000, 0x20018000, 0x08000000, 0x08080000, 1, p_2k , 0x1FFF7800, 0x1FFF782F, 0x1FFF0000, 0x1FFF7000, 0), (0x479, "STM32G491xx/A1xx" , 0x20004000, 0x2001C000, 0x08000000, 0x08080000, 1, p_2k , 0x1FFF7800, 0x1FFF782F, 0x1FFF0000, 0x1FFF7000, 0), # H7 - (0x483, "STM32H72xxx/73xxx" , 0x20004100, 0x20020000, 0x80000000, 0x08100000, 1, p_128k, 0 , 0 , 0x1FF00000, 0x1FF1E800, 0), + (0x483, "STM32H72xxx/73xxx" , 0x20004100, 0x20020000, 0x08000000, 0x08100000, 1, p_128k, 0 , 0 , 0x1FF00000, 0x1FF1E800, 0), (0x450, "STM32H74xxx/75xxx" , 0x20004100, 0x20020000, 0x08000000, 0x08200000, 1, p_128k, 0 , 0 , 0x1FF00000, 0x1FF1E800, 0), (0x480, "STM32H7A3xx/B3xx" , 0x20004100, 0x20020000, 0x08000000, 0x08100000, 1, p_8k , 0 , 0 , 0x1FF00000, 0x1FF14000, 0), # L0 From 83a633c6e4ba3932836a685e5c16ffd5c2562c0e Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 23 Oct 2023 09:52:30 +0200 Subject: [PATCH 311/369] dev: Add flash information --- src/stm32loader/devices.py | 305 +++++++++++++++++++++++-------------- 1 file changed, 189 insertions(+), 116 deletions(-) diff --git a/src/stm32loader/devices.py b/src/stm32loader/devices.py index f101bc4..6da4ab7 100644 --- a/src/stm32loader/devices.py +++ b/src/stm32loader/devices.py @@ -54,54 +54,86 @@ def __init__( DEVICE_FAMILIES = { DeviceFamily.C0: DeviceFamilyInfo(), # RM0360 - DeviceFamily.F0: DeviceFamilyInfo(flash_size_address=0x1FFFF7CC, option_bytes=(0x1FFFF800, 0x1FFFF80F)), + DeviceFamily.F0: DeviceFamilyInfo(flash_size_address=0x_1FFF_F7CC, option_bytes=(0x_1FFF_F800, 0x_1FFF_F80F)), # RM0008 - DeviceFamily.F1: DeviceFamilyInfo(uid_address=0x1FFFF7E8, flash_size_address=0x1FFFF7E0, option_bytes=(0x1FFFF800, 0x1FFFF80F)), - DeviceFamily.F2: DeviceFamilyInfo(option_bytes=(0x1FFFC000, 0x1FFFC00F)), + DeviceFamily.F1: DeviceFamilyInfo(uid_address=0x_1FFF_F7E8, flash_size_address=0x_1FFF_F7E0, option_bytes=(0x_1FFF_F800, 0x_1FFF_F80F)), + DeviceFamily.F2: DeviceFamilyInfo(option_bytes=(0x_1FFF_C000, 0x_1FFF_C00F)), # RM0366, RM0365, RM0316, RM0313, RM4510 - DeviceFamily.F3: DeviceFamilyInfo(uid_address=0x1FFFF7AC, flash_size_address=0x1FFFF7CC, flash_page_size=2048), + DeviceFamily.F3: DeviceFamilyInfo(uid_address=0x_1FFF_F7AC, flash_size_address=0x_1FFF_F7CC, flash_page_size=2048), # RM0090 - DeviceFamily.F4: DeviceFamilyInfo(uid_address=0x1FFF7A10, flash_size_address=0x1FFF7A22), + DeviceFamily.F4: DeviceFamilyInfo(uid_address=0x_1FFF_7A10, flash_size_address=0x_1FFF_7A22), # RM0385 - DeviceFamily.F7: DeviceFamilyInfo(uid_address=0x1FF0F420, flash_size_address=0x1FF0F442), + DeviceFamily.F7: DeviceFamilyInfo(uid_address=0x_1FF0_F420, flash_size_address=0x_1FF0_F442), # RM0444 - DeviceFamily.G0: DeviceFamilyInfo(uid_address=0x1FFF7590, flash_size_address=0x1FFF75E0), + DeviceFamily.G0: DeviceFamilyInfo(uid_address=0x_1FFF_7590, flash_size_address=0x_1FFF_75E0), DeviceFamily.G4: DeviceFamilyInfo(), DeviceFamily.H5: DeviceFamilyInfo(), # RM0433 - DeviceFamily.H7: DeviceFamilyInfo(uid_address=0x1FF1E800, flash_size_address=0x1FF1E880, flash_page_size=128 * 1024), + DeviceFamily.H7: DeviceFamilyInfo(uid_address=0x_1FF1_E800, flash_size_address=0x_1FF1_E880, flash_page_size=128 * 1024), # FIXME TWO RMs? # RM0451, RM4510 - DeviceFamily.L0: DeviceFamilyInfo(uid_address=0x1FF80050, flash_size_address=0x1FF8007C, transfer_size=128, flash_page_size=128, mass_erase=False), + DeviceFamily.L0: DeviceFamilyInfo(uid_address=0x_1FF8_0050, flash_size_address=0x_1FF8_007C, transfer_size=128, flash_page_size=128, mass_erase=False), DeviceFamily.L1: DeviceFamilyInfo(mass_erase=False), # RM0394 - DeviceFamily.L4: DeviceFamilyInfo(uid_address=0x1FFF7590, flash_size_address=0x1FFF75E0), + DeviceFamily.L4: DeviceFamilyInfo(uid_address=0x_1FFF_7590, flash_size_address=0x_1FFF_75E0), DeviceFamily.L5: DeviceFamilyInfo(), DeviceFamily.WBA: DeviceFamilyInfo(), DeviceFamily.WB: DeviceFamilyInfo(), # RM0453 - DeviceFamily.WL: DeviceFamilyInfo(uid_address=0x1FFF7590, flash_size_address=0x1FFF75E0), + DeviceFamily.WL: DeviceFamilyInfo(uid_address=0x_1FFF_7590, flash_size_address=0x_1FFF_75E0), DeviceFamily.U5: DeviceFamilyInfo(), DeviceFamily.W: DeviceFamilyInfo(), # ST BlueNRG has DIE_ID register with PRODUCT, but no UID. # NRG BlueNRG-2 datasheet # Flash page size: 128 pages of 8 * 64 * 4 bytes - DeviceFamily.NRG1: DeviceFamilyInfo(flash_size_address=0x40100014, flash_page_size=2048), - DeviceFamily.NRG2: DeviceFamilyInfo(flash_size_address=0x40100014, flash_page_size=2048), + DeviceFamily.NRG1: DeviceFamilyInfo(flash_size_address=0x_4010_0014, flash_page_size=2048), + DeviceFamily.NRG2: DeviceFamilyInfo(flash_size_address=0x_4010_0014, flash_page_size=2048), DeviceFamily.WIZ: DeviceFamilyInfo(), } +k = 1024 + + +class Flash: + + # RM0090 4 sectors of 16 Kbytes, 1 sector of 64 Kbytes, 7 sectors of 128 Kbytes + F2_F4_PAGE_SIZE = (16 * k, 16 * k, 16 * k, 16 * k, 64 * k, 128 * k, 0) + # RM0090 4 sectors of 16 Kbytes, 1 sector of 64 Kbytes, 7 sectors of 128 Kbytes... per bank + F4_DUAL_BANK_PAGE_SIZE = ( + 16 * k, 16 * k, 16 * k, 16 * k, 64 * k, 128 * k, 128 * k, 128 * k, 128 * k, + 16 * k, 16 * k, 16 * k, 16 * k, 64 * k, 128 * k, 0, + ) + F7_PAGE_SIZE = (32 * k, 32 * k, 32 * k, 32 * k, 128 * k, 256 * k, 0) + + def __init__(self, start=None, end=None, page_size=None, pages_per_sector=None): + self.start = start + self.end = end + self.page_size = page_size + self.pages_per_sector = pages_per_sector + + @property + def size(self): + if self.start is None: + return 0 + + return self.end - self.start + + class DeviceInfo: - def __init__(self, device_family, device_name, variant, product_line, product_id, bootloader_id, ram, system_memory): + def __init__(self, device_family, device_name, pid, bid, variant=None, line=None, ram=None, flash=None, system=None, option=None, flags=None): self.device_family = DeviceFamily[device_family] self.device_name = device_name - self.product_line = product_line - self.product_id = product_id - self.bootloader_id = bootloader_id + self.product_id = pid + self.bootloader_id = bid + self.variant = variant + self.product_line = line self.ram = ram - self.system_memory = system_memory + self.flash = Flash(*(flash or [])) + self.system_memory = system + self.option_bytes = option + self.flags = flags @property def ram_size(self): @@ -122,6 +154,10 @@ def ram_size(self): ram_size = end - start return ram_size + @property + def flash_size(self): + return self.flash.size + @property def system_memory_size(self): if self.system_memory is None: @@ -142,116 +178,153 @@ def system_memory_size(self): return flash_size def __str__(self): - return self.device_name + name = self.device_name + if self.variant: + name += f"-{self.variant}" + if self.product_line: + name += f"-{self.product_line}" + return name + + +@enum.unique +class Flag(enum.IntEnum): + OBL_LAUNCH = 1 + CLEAR_PEMPTY = 2 DEVICE_DETAILS = [ - # Based on ST AN2606 section "Device-dependent bootloader parameters" - # Key: ProductID, BootloaderID - DeviceInfo("C0", "STM32C011xx", "", "", 0x443, 0x51, (0x_2000_0000, 0x_2000_3000), (0x_1FFF_0000, 0x_1FFF_1800)), + # Based on ST AN2606 section "Device-dependent bootloader parameters". + # Flash range, option bytes and flags gleaned from stm32flash, dev_table.c. + + # FIXME flash? + DeviceInfo("C0", "STM32C011xx", 0x443, 0x51, ram=(0x_2000_0000, 0x_2000_3000), system=(0x_1FFF_0000, 0x_1FFF_1800), flash=None, option=None), + # FIXME flash? # Error in AN2606? Ram is mentioned as 0x_2000_2000 - 0x_2000_17FF - DeviceInfo("C0", "STM32C031xx", "", "", 0x453, 0x52, (0x_2000_2000, 0x_2000_2800), (0x_1FFF_0000, 0x_1FFF_1800)), - DeviceInfo("F0", "STM32F05xxx/030x8", "", "", 0x440, 0x21, (0x_2000_0800, 0x_2000_2000), (0x_1FFF_EC00, 0x_1FFF_F800)), - DeviceInfo("F0", "STM32F03xx4/6", "", "", 0x444, 0x10, (0x_2000_0800, 0x_2000_1000), (0x_1FFF_EC00, 0x_1FFF_F800)), - DeviceInfo("F0", "STM32F030xC", "", "", 0x442, 0x52, (0x_2000_1800, 0x_2000_8000), (0x_1FFF_D800, 0x_1FFF_F800)), - DeviceInfo("F0", "STM32F04xxx", "", "", 0x445, 0xA1, None, (0x_1FFF_C400, 0x_1FFF_F800)), - DeviceInfo("F0", "STM32F070x6", "", "", 0x445, 0xA2, None, (0x_1FFF_C400, 0x_1FFF_F800)), - DeviceInfo("F0", "STM32F070xB", "", "", 0x448, 0xA2, None, (0x_1FFF_C800, 0x_1FFF_F800)), - DeviceInfo("F0", "STM32F071xx/072xx", "", "", 0x448, 0xA1, (0x_2000_1800, 0x_2000_4000), (0x_1FFF_C800, 0x_1FFF_F800)), - DeviceInfo("F0", "STM32F09xxx", "", "", 0x442, 0x50, None, (0x_1FFF_D800, 0x_1FFF_F800)), - DeviceInfo("F1", "STM32F10xxx", "", "Low-density", 0x412, None, (0x_2000_0200, 0x_2000_2800), (0x_1FFF_F000, 0x_1FFF_F800)), - DeviceInfo("F1", "STM32F10xxx", "", "Medium-density", 0x410, None, (0x_2000_0200, 0x_2000_5000), (0x_1FFF_F000, 0x_1FFF_F800)), - DeviceInfo("F1", "STM32F10xxx", "", "High-density", 0x414, None, (0x_2000_0200, 0x_2001_0000), (0x_1FFF_F000, 0x_1FFF_F800)), - DeviceInfo("F1", "STM32F10xxx", "", "Medium-density value", 0x420, 0x10, (0x_2000_0200, 0x_2000_2000), (0x_1FFF_F000, 0x_1FFF_F800)), - DeviceInfo("F1", "STM32F10xxx", "", "High-density value", 0x428, 0x10, (0x_2000_0200, 0x_2000_8000), (0x_1FFF_F000, 0x_1FFF_F800)), - DeviceInfo("F1", "STM32F105xx/107xx", "", "Connectivity", 0x418, None, (0x_2000_1000, 0x_2001_0000), (0x_1FFF_B000, 0x_1FFF_F800)), - DeviceInfo("F1", "STM32F10xxx", "", "XL-density", 0x430, 0x21, (0x_2000_0800, 0x_2001_8000), (0x_1FFF_E000, 0x_1FFF_F800)), - DeviceInfo("F2", "STM32F2xxxx", "", "", 0x411, 0x20, (0x_2000_2000, 0x_2002_0000), (0x_1FFF_0000, 0x_1FFF_7800)), - DeviceInfo("F2", "STM32F2xxxx", "", "", 0x411, 0x33, (0x_2000_2000, 0x_2002_0000), (0x_1FFF_0000, 0x_1FFF_7800)), - DeviceInfo("F3", "STM32F373xx", "", "", 0x432, 0x41, (0x_2000_1400, 0x_2000_8000), (0x_1FFF_D800, 0x_1FFF_F800)), - DeviceInfo("F3", "STM32F378xx", "", "", 0x432, 0x50, (0x_2000_1000, 0x_2000_8000), (0x_1FFF_D800, 0x_1FFF_F800)), - DeviceInfo("F3", "STM32F302xB(C)/303xB(C)", "", "", 0x422, 0x41, (0x_2000_1400, 0x_2000_A000), (0x_1FFF_D800, 0x_1FFF_F800)), - DeviceInfo("F3", "STM32F358xx", "", "", 0x422, 0x50, (0x_2000_1400, 0x_2000_A000), (0x_1FFF_D800, 0x_1FFF_F800)), - DeviceInfo("F3", "STM32F301xx/302x4(6/8)", "", "", 0x439, 0x40, (0x_2000_1800, 0x_2000_4000), (0x_1FFF_D800, 0x_1FFF_F800)), - DeviceInfo("F3", "STM32F318xx", "", "", 0x439, 0x50, (0x_2000_1800, 0x_2000_4000), (0x_1FFF_D800, 0x_1FFF_F800)), - DeviceInfo("F3", "STM32F303x4(6/8)/334xx/328xx", "", "", 0x438, 0x50, (0x_2000_1800, 0x_2000_3000), (0x_1FFF_D800, 0x_1FFF_F800)), - DeviceInfo("F3", "STM32F302xD(E)/303xD(E)", "", "", 0x446, 0x40, (0x_2000_1800, 0x_2001_0000), (0x_1FFF_D800, 0x_1FFF_F800)), - DeviceInfo("F3", "STM32F398xx", "", "", 0x446, 0x50, (0x_2000_1800, 0x_2001_0000), (0x_1FFF_D800, 0x_1FFF_F800)), - DeviceInfo("F4", "STM32F40xxx/41xxx", "", "", 0x413, 0x31, (0x_2000_2000, 0x_2002_0000), (0x_1FFF_0000, 0x_1FFF_7800)), - DeviceInfo("F4", "STM32F40xxx/41xxx", "", "", 0x413, 0x91, (0x_2000_3000, 0x_2002_0000), (0x_1FFF_0000, 0x_1FFF_7800)), - DeviceInfo("F4", "STM32F42xxx/43xxx", "", "", 0x419, 0x70, (0x_2000_3000, 0x_2003_0000), (0x_1FFF_0000, 0x_1FFF_7800)), - DeviceInfo("F4", "STM32F42xxx/43xxx", "", "", 0x419, 0x91, (0x_2000_3000, 0x_2003_0000), (0x_1FFF_0000, 0x_1FFF_7800)), - # check ram upper end - DeviceInfo("F4", "STM32F401xB(C)", "", "", 0x423, 0xD1, (0x_2000_3000, 0x_2001_0000), (0x_1FFF_0000, 0x_1FFF_7800)), - DeviceInfo("F4", "STM32F401xD(E)", "", "", 0x433, 0xD1, (0x_2000_3000, 0x_2001_8000), (0x_1FFF_0000, 0x_1FFF_7800)), - DeviceInfo("F4", "STM32F410xx", "", "", 0x458, 0xB1, (0x_2000_3000, 0x_2000_8000), (0x_1FFF_0000, 0x_1FFF_7800)), - DeviceInfo("F4", "STM32F411xx", "", "", 0x431, 0xD0, (0x_2000_3000, 0x_2002_0000), (0x_1FFF_0000, 0x_1FFF_7800)), - DeviceInfo("F4", "STM32F412xx", "", "", 0x441, 0x90, (0x_2000_3000, 0x_2004_0000), (0x_1FFF_0000, 0x_1FFF_7800)), - DeviceInfo("F4", "STM32F446xx", "", "", 0x421, 0x90, (0x_2000_3000, 0x_2002_0000), (0x_1FFF_0000, 0x_1FFF_7800)), - DeviceInfo("F4", "STM32F469xx/479xx", "", "", 0x434, 0x90, (0x_2000_3000, 0x_2006_0000), (0x_1FFF_0000, 0x_1FFF_7800)), - DeviceInfo("F4", "STM32F413xx/423xx", "", "", 0x463, 0x90, (0x_2000_3000, 0x_2005_0000), (0x_1FFF_0000, 0x_1FFF_7800)), - DeviceInfo("F7", "STM32F72xxx/73xxx", "", "", 0x452, 0x90, (0x_2000_4000, 0x_2004_0000), (0x_1FF0_0000, 0x_1FF0_EDC0)), - DeviceInfo("F7", "STM32F74xxx/75xxx", "", "", 0x449, 0x70, (0x_2000_4000, 0x_2005_0000), (0x_1FF0_0000, 0x_1FF0_EDC0)), - DeviceInfo("F7", "STM32F74xxx/75xxx", "", "", 0x449, 0x90, (0x_2000_4000, 0x_2005_0000), (0x_1FF0_0000, 0x_1FF0_EDC0)), - DeviceInfo("F7", "STM32F76xxx/77xxx", "", "", 0x451, 0x93, (0x_2000_4000, 0x_2008_0000), (0x_1FF0_0000, 0x_1FF0_EDC0)), - DeviceInfo("G0", "STM32G03xxx/04xxx", "", "", 0x466, 0x52, (0x_2000_1000, 0x_2000_2000), (0x_1FFF_0000, 0x_1FFF_2000)), - DeviceInfo("G0", "STM32G07xxx/08xxx", "", "", 0x460, 0xB3, (0x_2000_2700, 0x_2000_9000), (0x_1FFF_0000, 0x_1FFF_7000)), - DeviceInfo("G0", "STM32G0B0xx", "", "", 0x467, 0xD0, (0x_2000_4000, 0x_2002_0000), ((0x_1FFF_0000, 0x_1FFF_7000), (0x_1FFF_8000, 0x_1FFF_F000))), - DeviceInfo("G0", "STM32G0B1xx/0C1xx", "", "", 0x467, 0x92, (0x_2000_4000, 0x_2002_0000), ((0x_1FFF_0000, 0x_1FFF_6000), (0x_1FFF_8000, 0x_1FFF_F000))), - # Note: STM32flash has 0x_2000_4800 as upper flash range. - DeviceInfo("G0", "STM32G05xxx/061xx", "", "", 0x456, 0x51, (0x_2000_1000, 0x_2000_2000), (0x_1FFF_0000, 0x_1FFF_1000)), - DeviceInfo("G4", "STM32G431xx/441xx", "", "", 0x468, 0xD4, (0x_2000_4000, 0x_2000_5800), (0x_1FFF_0000, 0x_1FFF_7000)), - DeviceInfo("G4", "STM32G47xxx/48xxx", "", "", 0x469, 0xD5, (0x_2000_4000, 0x_2001_8000), (0x_1FFF_0000, 0x_1FFF_7000)), - DeviceInfo("G4", "STM32G491xx/A1xx", "", "", 0x479, 0xD2, (0x_2000_4000, 0x_2001_C000), (0x_1FFF_0000, 0x_1FFF_7000)), - DeviceInfo("H5", "STM32H503xx", "", "", 0x474, 0xE1, (0x_2000_4000, 0x_2000_8000), (0x_0BF8_7000, 0x_0BF9_0000)), - DeviceInfo("H5", "STM32H563xx/573xx", "", "", 0x484, 0xE3, (0x_2000_0000, 0x_200A_0000), (0x_0BF9_7000, 0x_0BFA_0000)), - DeviceInfo("H7", "STM32H72xxx/73xxx", "", "", 0x483, 0x93, ((0x_2000_4100, 0x_2002_0000), (0x_2400_4000, 0x_2405_0000)), (0x_1FF0_0000, 0x_1FF1_E800)), - DeviceInfo("H7", "STM32H74xxx/75xxx", "", "", 0x450, 0x91, ((0x_2000_4100, 0x_2002_0000), (0x_2400_5000, 0x_2408_0000)), (0x_1FF0_0000, 0x_1FF1_E800)), - DeviceInfo("H7", "STM32H7A3xx/B3xx", "", "", 0x480, 0x92, ((0x_2000_4100, 0x_2002_0000), (0x_2403_4000, 0x_2408_0000)), (0x_1FF0_0000, 0x_1FF1_4000)), - DeviceInfo("L0", "STM32L01xxx/02xxx", "", "", 0x457, 0xC3, None, (0x_1FF0_0000, 0x_1FF0_1000)), - DeviceInfo("L0", "STM32L031xx/041xx", "", "", 0x425, 0xC0, (0x_2000_1000, 0x_2000_2000), (0x_1FF0_0000, 0x_1FF0_1000)), - DeviceInfo("L0", "STM32L05xxx/06xxx", "", "", 0x417, 0xC0, (0x_2000_1000, 0x_2000_2000), (0x_1FF0_0000, 0x_1FF0_1000)), - # Note: STM32flash has 0x_2000_2000 as lower flash range. - DeviceInfo("L0", "STM32L07xxx/08xxx", "", "", 0x447, 0x41, (0x_2000_1000, 0x_2000_5000), (0x_1FF0_0000, 0x_1FF0_2000)), - DeviceInfo("L0", "STM32L07xxx/08xxx", "", "", 0x447, 0xB2, (0x_2000_1400, 0x_2000_5000), (0x_1FF0_0000, 0x_1FF0_2000)), - DeviceInfo("L1", "STM32L1xxx6(8/B)", "", "Medium-density ULP", 0x416, 0x20, (0x_2000_0800, 0x_2000_4000), (0x_1FF0_0000, 0x_1FF0_2000)), - DeviceInfo("L1", "STM32L1xxx6(8/B)A", "", "", 0x429, 0x20, (0x_2000_1000, 0x_2000_8000), (0x_1FF0_0000, 0x_1FF0_2000)), - DeviceInfo("L1", "STM32L1xxxC", "", "", 0x427, 0x40, (0x_2000_1000, 0x_2000_8000), (0x_1FF0_0000, 0x_1FF0_2000)), - DeviceInfo("L1", "STM32L1xxxD", "", "", 0x436, 0x45, (0x_2000_1000, 0x_2000_C000), (0x_1FF0_0000, 0x_1FF0_2000)), - DeviceInfo("L1", "STM32L1xxxE", "", "", 0x437, 0x40, (0x_2000_1000, 0x_2001_4000), (0x_1FF0_0000, 0x_1FF0_2000)), + DeviceInfo("C0", "STM32C031xx", 0x453, 0x52, ram=(0x_2000_2000, 0x_2000_2800), system=(0x_1FFF_0000, 0x_1FFF_1800), flash=None, option=None), + DeviceInfo("F0", "STM32F05xxx/030x8", 0x440, 0x21, ram=(0x_2000_0800, 0x_2000_2000), system=(0x_1FFF_EC00, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0801_0000, 1 * k, 4), option=(0x_1FFF_F800, 0x_1FFF_F80F)), + DeviceInfo("F0", "STM32F03xx4/6", 0x444, 0x10, ram=(0x_2000_0800, 0x_2000_1000), system=(0x_1FFF_EC00, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0800_8000, 1 * k, 4), option=(0x_1FFF_F800, 0x_1FFF_F80F)), + # FIXME different flash size for both devices with PID=0x442 ? + DeviceInfo("F0", "STM32F030xC", 0x442, 0x52, ram=(0x_2000_1800, 0x_2000_8000), system=(0x_1FFF_D800, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0804_0000, 2 * k, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F), flags=Flag.OBL_LAUNCH), + # FIXME different flash size for both devices with PID=0x445 ? + DeviceInfo("F0", "STM32F04xxx", 0x445, 0xA1, None, ram=None, system=(0x_1FFF_C400, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0800_8000, 1 * k, 4), option=(0x_1FFF_F800, 0x_1FFF_F80F)), + DeviceInfo("F0", "STM32F070x6", 0x445, 0xA2, None, ram=None, system=(0x_1FFF_C400, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0800_8000, 1 * k, 4), option=(0x_1FFF_F800, 0x_1FFF_F80F)), + # FIXME different flash size for both devices with PID=0x448 ? + DeviceInfo("F0", "STM32F070xB", 0x448, 0xA2, None, ram=(0x_1FFF_C800, 0x_1FFF_F800), system=None, flash=(0x_0800_0000, 0x_0802_0000, 2 * k, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F)), + DeviceInfo("F0", "STM32F071xx/072xx", 0x448, 0xA1, ram=(0x_2000_1800, 0x_2000_4000), system=(0x_1FFF_C800, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0802_0000, 2 * k, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F)), + DeviceInfo("F0", "STM32F09xxx", 0x442, 0x50, None, ram=(0x_1FFF_D800, 0x_1FFF_F800), system=None, flash=(0x_0800_0000, 0x_0804_0000, 2 * k, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F), flags=Flag.OBL_LAUNCH), + DeviceInfo("F1", "STM32F10xxx", line="Low-density", pid=0x412, bid=None, ram=(0x_2000_0200, 0x_2000_2800), system=(0x_1FFF_F000, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0800_8000, 1 * k, 4), option=(0x_1FFF_F800, 0x_1FFF_F80F)), + DeviceInfo("F1", "STM32F10xxx", line="Medium-density", pid=0x410, bid=None, ram=(0x_2000_0200, 0x_2000_5000), system=(0x_1FFF_F000, 0x_1FFF_F800), flash = (0x_0800_0000, 0x_0802_0000, 1 * k, 4), option = (0x_1FFF_F800, 0x_1FFF_F80F)), + DeviceInfo("F1", "STM32F10xxx", line="High-density", pid=0x414, bid=None, ram=(0x_2000_0200, 0x_2001_0000), system=(0x_1FFF_F000, 0x_1FFF_F800), flash = (0x_0800_0000, 0x_0808_0000, 2 * k, 2), option = (0x_1FFF_F800, 0x_1FFF_F80F)), + DeviceInfo("F1", "STM32F10xxx", line="Medium-density value", pid=0x420, bid=0x10, ram=(0x_2000_0200, 0x_2000_2000), system=(0x_1FFF_F000, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0802_0000, 1 * k, 4), option=(0x_1FFF_F800, 0x_1FFF_F80F)), + DeviceInfo("F1", "STM32F10xxx", line="High-density value", pid=0x428, bid=0x10, ram=(0x_2000_0200, 0x_2000_8000), system=(0x_1FFF_F000, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0808_0000, 2 * k, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F)), + DeviceInfo("F1", "STM32F105xx/107xx", line="Connectivity", pid=0x418, bid=None, ram=(0x_2000_1000, 0x_2001_0000), system=(0x_1FFF_B000, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0804_0000, 2 * k, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F)), + DeviceInfo("F1", "STM32F10xxx", line="XL-density", pid=0x430, bid=0x21, ram=(0x_2000_0800, 0x_2001_8000), system=(0x_1FFF_E000, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0810_0000, 2 * k, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F)), + # FIXME different flash size for both devices with PID=0x411 ? + DeviceInfo("F2", "STM32F2xxxx", 0x411, 0x20, ram=(0x_2000_2000, 0x_2002_0000), system=(0x_1FFF_0000, 0x_1FFF_7800), flash=(0x_0800_0000, 0x_0810_0000, Flash.F2_F4_PAGE_SIZE), option=(0x_1FFF_C000, 0x_1FFF_C00F)), + DeviceInfo("F2", "STM32F2xxxx", 0x411, 0x33, ram=(0x_2000_2000, 0x_2002_0000), system=(0x_1FFF_0000, 0x_1FFF_7800), flash=(0x_0800_0000, 0x_0810_0000, Flash.F2_F4_PAGE_SIZE), option=(0x_1FFF_C000, 0x_1FFF_C00F)), + # FIXME different flash size for both devices with PID=0x432 ? + DeviceInfo("F3", "STM32F373xx", 0x432, 0x41, ram=(0x_2000_1400, 0x_2000_8000), system=(0x_1FFF_D800, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0804_0000, 2 * k, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F)), + DeviceInfo("F3", "STM32F378xx", 0x432, 0x50, ram=(0x_2000_1000, 0x_2000_8000), system=(0x_1FFF_D800, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0804_0000, 2 * k, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F)), + # FIXME different flash size for both devices with PID=0x422 ? + DeviceInfo("F3", "STM32F302xB(C)/303xB(C)", 0x422, 0x41, ram=(0x_2000_1400, 0x_2000_A000), system=(0x_1FFF_D800, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0804_0000, 2 * k, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F)), + DeviceInfo("F3", "STM32F358xx", 0x422, 0x50, ram=(0x_2000_1400, 0x_2000_A000), system=(0x_1FFF_D800, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0804_0000, 2 * k, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F)), + # FIXME different flash size for both devices with PID=0x439 ? + DeviceInfo("F3", "STM32F301xx/302x4(6/8)", 0x439, 0x40, ram=(0x_2000_1800, 0x_2000_4000), system=(0x_1FFF_D800, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0801_0000, 2 * k, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F)), + DeviceInfo("F3", "STM32F318xx", 0x439, 0x50, ram=(0x_2000_1800, 0x_2000_4000), system=(0x_1FFF_D800, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0801_0000, 2 * k, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F)), + DeviceInfo("F3", "STM32F303x4(6/8)/334xx/328xx", 0x438, 0x50, ram=(0x_2000_1800, 0x_2000_3000), system=(0x_1FFF_D800, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0801_0000, 2 * k, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F)), + # FIXME different flash size for both devices with PID=0x446 ? + DeviceInfo("F3", "STM32F302xD(E)/303xD(E)", 0x446, 0x40, ram=(0x_2000_1800, 0x_2001_0000), system=(0x_1FFF_D800, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0808_0000, 2 * k, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F)), + DeviceInfo("F3", "STM32F398xx", 0x446, 0x50, ram=(0x_2000_1800, 0x_2001_0000), system=(0x_1FFF_D800, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0808_0000, 2 * k, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F)), + # FIXME different flash size for both devices with PID=0x413 ? + DeviceInfo("F4", "STM32F40xxx/41xxx", 0x413, 0x31, ram=(0x_2000_2000, 0x_2002_0000), system=(0x_1FFF_0000, 0x_1FFF_7800), flash=(0x_0800_0000, 0x_0810_0000, Flash.F2_F4_PAGE_SIZE), option=(0x_1FFF_C000, 0x_1FFF_C00F)), + DeviceInfo("F4", "STM32F40xxx/41xxx", 0x413, 0x91, ram=(0x_2000_3000, 0x_2002_0000), system=(0x_1FFF_0000, 0x_1FFF_7800), flash=(0x_0800_0000, 0x_0810_0000, Flash.F2_F4_PAGE_SIZE), option=(0x_1FFF_C000, 0x_1FFF_C00F)), + # FIXME different flash size for both devices with PID=0x419 ? + DeviceInfo("F4", "STM32F42xxx/43xxx", 0x419, 0x70, ram=(0x_2000_3000, 0x_2003_0000), system=(0x_1FFF_0000, 0x_1FFF_7800), flash=(0x_0800_0000, 0x_0820_0000, Flash.F4_DUAL_BANK_PAGE_SIZE), option=(0x_1FFE_C000, 0x_1FFF_C00F)), + DeviceInfo("F4", "STM32F42xxx/43xxx", 0x419, 0x91, ram=(0x_2000_3000, 0x_2003_0000), system=(0x_1FFF_0000, 0x_1FFF_7800), flash=(0x_0800_0000, 0x_0820_0000, Flash.F4_DUAL_BANK_PAGE_SIZE), option=(0x_1FFE_C000, 0x_1FFF_C00F)), + # FIXME Check RAM upper end. + DeviceInfo("F4", "STM32F401xB(C)", 0x423, 0xD1, ram=(0x_2000_3000, 0x_2001_0000), system=(0x_1FFF_0000, 0x_1FFF_7800), flash=(0x_0800_0000, 0x_0804_0000, Flash.F2_F4_PAGE_SIZE), option=(0x_1FFF_C000, 0x_1FFF_C00F)), + DeviceInfo("F4", "STM32F401xD(E)", 0x433, 0xD1, ram=(0x_2000_3000, 0x_2001_8000), system=(0x_1FFF_0000, 0x_1FFF_7800), flash=(0x_0800_0000, 0x_0808_0000, Flash.F2_F4_PAGE_SIZE), option=(0x_1FFF_C000, 0x_1FFF_C00F)), + DeviceInfo("F4", "STM32F410xx", 0x458, 0xB1, ram=(0x_2000_3000, 0x_2000_8000), system=(0x_1FFF_0000, 0x_1FFF_7800), flash=(0x_0800_0000, 0x_0802_0000, Flash.F2_F4_PAGE_SIZE), option=(0x_1FFF_C000, 0x_1FFF_C00F)), + DeviceInfo("F4", "STM32F411xx", 0x431, 0xD0, ram=(0x_2000_3000, 0x_2002_0000), system=(0x_1FFF_0000, 0x_1FFF_7800), flash=(0x_0800_0000, 0x_0808_0000, Flash.F2_F4_PAGE_SIZE), option=(0x_1FFF_C000, 0x_1FFF_C00F)), + DeviceInfo("F4", "STM32F412xx", 0x441, 0x90, ram=(0x_2000_3000, 0x_2004_0000), system=(0x_1FFF_0000, 0x_1FFF_7800), flash=(0x_0800_0000, 0x_0810_0000, Flash.F2_F4_PAGE_SIZE), option=(0x_1FFF_C000, 0x_1FFF_C00F)), + DeviceInfo("F4", "STM32F446xx", 0x421, 0x90, ram=(0x_2000_3000, 0x_2002_0000), system=(0x_1FFF_0000, 0x_1FFF_7800), flash=(0x_0800_0000, 0x_0808_0000, Flash.F2_F4_PAGE_SIZE), option=(0x_1FFF_C000, 0x_1FFF_C00F)), + DeviceInfo("F4", "STM32F469xx/479xx", 0x434, 0x90, ram=(0x_2000_3000, 0x_2006_0000), system=(0x_1FFF_0000, 0x_1FFF_7800), flash=(0x_0800_0000, 0x_0820_0000, Flash.F4_DUAL_BANK_PAGE_SIZE), option=(0x_1FFE_C000, 0x_1FFF_C00F)), + DeviceInfo("F4", "STM32F413xx/423xx", 0x463, 0x90, ram=(0x_2000_3000, 0x_2005_0000), system=(0x_1FFF_0000, 0x_1FFF_7800), flash=(0x_0800_0000, 0x_0818_0000, Flash.F2_F4_PAGE_SIZE), option=(0x_1FFF_C000, 0x_1FFF_C00F)), + DeviceInfo("F7", "STM32F72xxx/73xxx", 0x452, 0x90, ram=(0x_2000_4000, 0x_2004_0000), system=(0x_1FF0_0000, 0x_1FF0_EDC0), flash=(0x_0800_0000, 0x_0808_0000, Flash.F2_F4_PAGE_SIZE), option=(0x_1FFF_0000, 0x_1FFF_001F)), + # FIXME different flash size for both devices with PID=0x449 ? + DeviceInfo("F7", "STM32F74xxx/75xxx", 0x449, 0x70, ram=(0x_2000_4000, 0x_2005_0000), system=(0x_1FF0_0000, 0x_1FF0_EDC0), flash=(0x_0800_0000, 0x_0810_0000, Flash.F7_PAGE_SIZE), option=(0x_1FFF_0000, 0x_1FFF_001F)), + DeviceInfo("F7", "STM32F74xxx/75xxx", 0x449, 0x90, ram=(0x_2000_4000, 0x_2005_0000), system=(0x_1FF0_0000, 0x_1FF0_EDC0), flash=(0x_0800_0000, 0x_0810_0000, Flash.F7_PAGE_SIZE), option=(0x_1FFF_0000, 0x_1FFF_001F)), + DeviceInfo("F7", "STM32F76xxx/77xxx", 0x451, 0x93, ram=(0x_2000_4000, 0x_2008_0000), system=(0x_1FF0_0000, 0x_1FF0_EDC0), flash=(0x_0800_0000, 0x_0820_0000, Flash.F7_PAGE_SIZE), option=(0x_1FFF_0000, 0x_1FFF_001F)), + DeviceInfo("G0", "STM32G03xxx/04xxx", 0x466, 0x52, ram=(0x_2000_1000, 0x_2000_2000), system=(0x_1FFF_0000, 0x_1FFF_2000), flash=(0x_0800_0000, 0x_0801_0000, 2 * k), option=(0x_1FFF_7800, 0x_1FFF_787F)), + DeviceInfo("G0", "STM32G07xxx/08xxx", 0x460, 0xB3, ram=(0x_2000_2700, 0x_2000_9000), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0802_0000, 2 * k), option=(0x_1FFF_7800, 0x_1FFF_787F)), + # FIXME different flash size for both devices with PID=0x467 ? + # FIXME dual banks for system + DeviceInfo("G0", "STM32G0B0xx", 0x467, 0xD0, ram=(0x_2000_4000, 0x_2002_0000), system=((0x_1FFF_0000, 0x_1FFF_7000), (0x_1FFF_8000, 0x_1FFF_F000)), flash=(0x_0800_0000, 0x_0808_0000, 2 * k), option=(0x_1FFF_7800, 0x_1FFF_787F)), + # FIXME dual banks for system + DeviceInfo("G0", "STM32G0B1xx/0C1xx", 0x467, 0x92, ram=(0x_2000_4000, 0x_2002_0000), system=((0x_1FFF_0000, 0x_1FFF_6000), (0x_1FFF_8000, 0x_1FFF_F000)), flash=(0x_0800_0000, 0x_0808_0000, 2 * k), option=(0x_1FFF_7800, 0x_1FFF_787F)), + # FIXME: STM32flash has 0x_2000_4800 as upper system range. + DeviceInfo("G0", "STM32G05xxx/061xx", 0x456, 0x51, ram=(0x_2000_1000, 0x_2000_2000), system=(0x_1FFF_0000, 0x_1FFF_1000), flash=(0x_0800_0000, 0x_0801_0000, 2 * k), option=(0x_1FFF_7800, 0x_1FFF_787F)), + DeviceInfo("G4", "STM32G431xx/441xx", 0x468, 0xD4, ram=(0x_2000_4000, 0x_2000_5800), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0802_0000, 2 * k), option=(0x_1FFF_7800, 0x_1FFF_782F)), + DeviceInfo("G4", "STM32G47xxx/48xxx", 0x469, 0xD5, ram=(0x_2000_4000, 0x_2001_8000), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0808_0000, 2 * k), option=(0x_1FFF_7800, 0x_1FFF_782F)), + DeviceInfo("G4", "STM32G491xx/A1xx", 0x479, 0xD2, ram=(0x_2000_4000, 0x_2001_C000), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0808_0000, 2 * k), option=(0x_1FFF_7800, 0x_1FFF_782F)), + + # FIXME Flash and option bytes? + DeviceInfo("H5", "STM32H503xx", 0x474, 0xE1, ram=(0x_2000_4000, 0x_2000_8000), system=(0x_0BF8_7000, 0x_0BF9_0000)), + # FIXME Flash and option bytes? + DeviceInfo("H5", "STM32H563xx/573xx", 0x484, 0xE3, ram=(0x_2000_0000, 0x_200A_0000), system=(0x_0BF9_7000, 0x_0BFA_0000)), + DeviceInfo("H7", "STM32H72xxx/73xxx", 0x483, 0x93, ram=((0x_2000_4100, 0x_2002_0000), (0x_2400_4000, 0x_2405_0000)), system=(0x_1FF0_0000, 0x_1FF1_E800), flash=(0x_0800_0000, 0x_0810_0000, 128 * k), option=None), + DeviceInfo("H7", "STM32H74xxx/75xxx", 0x450, 0x91, ram=((0x_2000_4100, 0x_2002_0000), (0x_2400_5000, 0x_2408_0000)), system=(0x_1FF0_0000, 0x_1FF1_E800), flash=(0x_0800_0000, 0x_0820_0000, 128 * k), option=None), + DeviceInfo("H7", "STM32H7A3xx/B3xx", 0x480, 0x92, ram=((0x_2000_4100, 0x_2002_0000), (0x_2403_4000, 0x_2408_0000)), system=(0x_1FF0_0000, 0x_1FF1_4000), flash=(0x_0800_0000, 0x_0810_0000, 8 * k), option=None), + DeviceInfo("L0", "STM32L01xxx/02xxx", 0x457, 0xC3, ram=None, system=(0x_1FF0_0000, 0x_1FF0_1000), flash=(0x_0800_0000, 0x_0800_4000, 128, 32), option=(0x_1FF8_0000, 0x_1FF8_001F)), + DeviceInfo("L0", "STM32L031xx/041xx", 0x425, 0xC0, ram=(0x_2000_1000, 0x_2000_2000), system=(0x_1FF0_0000, 0x_1FF0_1000), flash=(0x_0800_0000, 0x_0800_8000, 128, 32), option=(0x_1FF8_0000, 0x_1FF8_001F)), + DeviceInfo("L0", "STM32L05xxx/06xxx", 0x417, 0xC0, ram=(0x_2000_1000, 0x_2000_2000), system=(0x_1FF0_0000, 0x_1FF0_1000), flash=(0x_0800_0000, 0x_0801_0000, 128, 32), option=(0x_1FF8_0000, 0x_1FF8_001F)), + # FIXME different flash size for both devices with PID=0x447 ? + # Note: STM32flash has 0x_2000_2000 as lower system range. + DeviceInfo("L0", "STM32L07xxx/08xxx", 0x447, 0x41, ram=(0x_2000_1000, 0x_2000_5000), system=(0x_1FF0_0000, 0x_1FF0_2000), flash=(0x_0800_0000, 0x_0803_0000, 128, 32), option=(0x_1FF8_0000, 0x_1FF8_001F)), + DeviceInfo("L0", "STM32L07xxx/08xxx", 0x447, 0xB2, ram=(0x_2000_1400, 0x_2000_5000), system=(0x_1FF0_0000, 0x_1FF0_2000), flash=(0x_0800_0000, 0x_0803_0000, 128, 32), option=(0x_1FF8_0000, 0x_1FF8_001F)), + DeviceInfo("L1", "STM32L1xxx6(8/B)", line="Medium-density ULP", pid=0x416, bid=0x20, ram=(0x_2000_0800, 0x_2000_4000), system=(0x_1FF0_0000, 0x_1FF0_2000), flash=(0x_0800_0000, 0x_0802_0000, 256, 16), option=(0x_1FF8_0000, 0x_1FF8_001F)), + DeviceInfo("L1", "STM32L1xxx6(8/B)A", 0x429, 0x20, ram=(0x_2000_1000, 0x_2000_8000), system=(0x_1FF0_0000, 0x_1FF0_2000), flash=(0x_0800_0000, 0x_0802_0000, 256, 16), option=(0x_1FF8_0000, 0x_1FF8_001F)), + DeviceInfo("L1", "STM32L1xxxC", 0x427, 0x40, ram=(0x_2000_1000, 0x_2000_8000), system=(0x_1FF0_0000, 0x_1FF0_2000), flash=(0x_0800_0000, 0x_0804_0000, 256, 16), option=(0x_1FF8_0000, 0x_1FF8_001F)), + DeviceInfo("L1", "STM32L1xxxD", 0x436, 0x45, ram=(0x_2000_1000, 0x_2000_C000), system=(0x_1FF0_0000, 0x_1FF0_2000), flash=(0x_0800_0000, 0x_0806_0000, 256, 16), option=(0x_1FF8_0000, 0x_1FF8_001F)), + DeviceInfo("L1", "STM32L1xxxE", 0x437, 0x40, ram=(0x_2000_1000, 0x_2001_4000), system=(0x_1FF0_0000, 0x_1FF0_2000), flash=(0x_0800_0000, 0x_0808_0000, 256, 16), option=(0x_1FF8_0000, 0x_1FF8_001F)), # Note: Stm32flash has 0x_2000_3100 as ram start. - DeviceInfo("L4", "STM32L412xx/422xx", "", "Low-density", 0x464, 0xD1, (0x_2000_2100, 0x_2000_8000), (0x_1FFF_0000, 0x_1FFF_7000)), - DeviceInfo("L4", "STM32L43xxx/44xxx", "", "", 0x435, 0x91, (0x_2000_3100, 0x_2000_C000), (0x_1FFF_0000, 0x_1FFF_7000)), - DeviceInfo("L4", "STM32L45xxx/46xxx", "", "", 0x462, 0x92, (0x_2000_3100, 0x_2002_0000), (0x_1FFF_0000, 0x_1FFF_7000)), - DeviceInfo("L4", "STM32L47xxx/48xxx", "", "", 0x415, 0xA3, (0x_2000_3000, 0x_2001_8000), (0x_1FFF_0000, 0x_1FFF_7000)), - DeviceInfo("L4", "STM32L47xxx/48xxx", "", "", 0x415, 0x92, (0x_2000_3100, 0x_2001_8000), (0x_1FFF_0000, 0x_1FFF_7000)), - DeviceInfo("L4", "STM32L496xx/4A6xx", "", "", 0x461, 0x93, (0x_2000_3100, 0x_2004_0000), (0x_1FFF_0000, 0x_1FFF_7000)), - DeviceInfo("L4", "STM32L4Rxx/4Sxx", "", "", 0x470, 0x95, (0x_2000_3200, 0x_200A_0000), (0x_1FFF_0000, 0x_1FFF_7000)), - DeviceInfo("L4", "STM32L4P5xx/Q5xx", "", "", 0x471, 0x90, (0x_2000_4000, 0x_2005_0000), (0x_1FFF_0000, 0x_1FFF_7000)), - DeviceInfo("L5", "STM32L552xx/562xx", "", "", 0x472, 0x92, (0x_2000_4000, 0x_2004_0000), (0x_0BF9_0000, 0x_0BF9_8000)), - DeviceInfo("WBA", "STM32WBA52xx", "", "", 0x492, 0xB0, (0x_2000_0000, 0x_2000_2000), (0x_0BF8_8000, 0x_0BF9_0000)), - DeviceInfo("WB", "STM32WB10xx/15xx", "", "", 0x494, 0xB1, (0x_2000_5000, 0x_2004_0000), (0x_1FFF_0000, 0x_1FFF_7000)), - DeviceInfo("WB", "STM32WB30xx/35xx/50xx/55xx", "", "", 0x495, 0xD5, (0x_2000_4000, 0x_2000_C000), (0x_1FFF_0000, 0x_1FFF_7000)), - DeviceInfo("WL", "STM32WLE5xx/WL55xx", "", "", 0x497, 0xC4, (0x_2000_2000, 0x_2001_0000), (0x_1FFF_0000, 0x_1FFF_4000)), - DeviceInfo("U5", "STM32U535xx/545xx", "", "", 0x455, 0x91, (0x_2000_4000, 0x_2024_0000), (0x_0BF9_0000, 0x_0BFA_0000)), - DeviceInfo("U5", "STM32U575xx/585xx", "", "", 0x482, 0x92, (0x_2000_4000, 0x_200C_0000), (0x_0BF9_0000, 0x_0BFA_0000)), - DeviceInfo("U5", "STM32U595xx/599xx/5A9xx", "", "", 0x481, 0x92, (0x_2000_4000, 0x_2027_0000), (0x_0BF9_0000, 0x_0BFA_0000)), + DeviceInfo("L4", "STM32L412xx/422xx", line="Low-density", pid=0x464, bid=0xD1, ram=(0x_2000_2100, 0x_2000_8000), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0802_0000, 2 * k), option=(0x_1FFF_7800, 0x_1FFF_780F)), + DeviceInfo("L4", "STM32L43xxx/44xxx", 0x435, 0x91, ram=(0x_2000_3100, 0x_2000_C000), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0804_0000, 2 * k), option=(0x_1FFF_7800, 0x_1FFF_780F)), + DeviceInfo("L4", "STM32L45xxx/46xxx", 0x462, 0x92, ram=(0x_2000_3100, 0x_2002_0000), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0808_0000, 2 * k), option=(0x_1FFF_7800, 0x_1FFF_780F), flags=Flag.CLEAR_PEMPTY), + # FIXME different flash size for both devices with PID=0x415 ? + DeviceInfo("L4", "STM32L47xxx/48xxx", 0x415, 0xA3, ram=(0x_2000_3000, 0x_2001_8000), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0810_0000, 2 * k), option=(0x_1FFF_7800, 0x_1FFF_F80F)), + DeviceInfo("L4", "STM32L47xxx/48xxx", 0x415, 0x92, ram=(0x_2000_3100, 0x_2001_8000), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0810_0000, 2 * k), option=(0x_1FFF_7800, 0x_1FFF_F80F)), + DeviceInfo("L4", "STM32L496xx/4A6xx", 0x461, 0x93, ram=(0x_2000_3100, 0x_2004_0000), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0810_0000, 2 * k), option=(0x_1FFF_7800, 0x_1FFF_F80F)), + DeviceInfo("L4", "STM32L4Rxx/4Sxx", 0x470, 0x95, ram=(0x_2000_3200, 0x_200A_0000), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0810_0000, 2 * k), option=(0x_1FFF_7800, 0x_1FFF_F80F)), + DeviceInfo("L4", "STM32L4P5xx/Q5xx", 0x471, 0x90, ram=(0x_2000_4000, 0x_2005_0000), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0810_0000, 4 * k), option=(0x_1FF0_0000, 0x_1FF0_000F)), + DeviceInfo("L5", "STM32L552xx/562xx", 0x472, 0x92, ram=(0x_2000_4000, 0x_2004_0000), system=(0x_0BF9_0000, 0x_0BF9_8000), flash=(0x_0800_0000, 0x_0808_0000, 2 * k), option=None), + # FIXME flash config ? + DeviceInfo("WBA", "STM32WBA52xx", 0x492, 0xB0, ram=(0x_2000_0000, 0x_2000_2000), system=(0x_0BF8_8000, 0x_0BF9_0000)), + DeviceInfo("WB", "STM32WB10xx/15xx", 0x494, 0xB1, ram=(0x_2000_5000, 0x_2004_0000), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0805_0000, 2 * k), option=(0x_1FFF_7800, 0x_1FFF_787F)), + DeviceInfo("WB", "STM32WB30xx/35xx/50xx/55xx", 0x495, 0xD5, ram=(0x_2000_4000, 0x_2000_C000), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0810_0000, 4 * k), option=(0x_1FFF_8000, 0x_1FFF_807F)), + DeviceInfo("WL", "STM32WLE5xx/WL55xx", 0x497, 0xC4, ram=(0x_2000_2000, 0x_2001_0000), system=(0x_1FFF_0000, 0x_1FFF_4000), flash=(0x_0800_0000, 0x_0804_0000, 2 * k), option=(0x_1FFF_7800, 0x_1FFF_8000)), + # FIXME flash config? + DeviceInfo("U5", "STM32U535xx/545xx", 0x455, 0x91, ram=(0x_2000_4000, 0x_2024_0000), system=(0x_0BF9_0000, 0x_0BFA_0000)), + DeviceInfo("U5", "STM32U575xx/585xx", 0x482, 0x92, ram=(0x_2000_4000, 0x_200C_0000), system=(0x_0BF9_0000, 0x_0BFA_0000), flash=(0x_0800_0000, 0x_0820_0000, 8 * k), option=None), + # FIXME flash config? + DeviceInfo("U5", "STM32U595xx/599xx/5A9xx", 0x481, 0x92, ram=(0x_2000_4000, 0x_2027_0000), system=(0x_0BF9_0000, 0x_0BFA_0000)), # Not yet in AN2606. Bootloader IDs are unknown. # F1 is assumed here. - DeviceInfo("F1", "STM32F103x8/B", "", "Medium-density performance", 0x641, None, (0x20000200, 0x20005000), (0x1FFFF000, 0x1FFFF800)), + DeviceInfo("F1", "STM32F103x8/B", line="Medium-density performance", pid=0x641, bid=None, ram=(0x_2000_0200, 0x_2000_5000), system=(0x_1FFF_F000, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0802_0000, 1 * k, 4), option=(0x_1FFF_F800, 0x_1FFF_F80F)), + # WBA, WB, WL or simply 'W'? - DeviceInfo("W", "STM32W", "128kB", "", 0x9A8, None, (0x20000200, 0x20002000), (0x08040000, 0x08040800)), - DeviceInfo("W", "STM32W", "256kB", "", 0x9B0, None, (0x20000200, 0x20004000), (0x08040000, 0x08040800)), + DeviceInfo("W", "STM32W", variant="128kB", pid=0x9A8, bid=None, ram=(0x_2000_0200, 0x_2000_2000), system=(0x_0804_0000, 0x_0804_0800), flash=(0x_0800_0000, 0x_0802_0000, 1 * k, 4), option=(0x_0804_0800, 0x_0804_080F)), + DeviceInfo("W", "STM32W", variant="256kB", pid=0x9B0, bid=None, ram=(0x_2000_0200, 0x_2000_4000), system=(0x_0804_0000, 0x_0804_0800), flash=(0x_0800_0000, 0x_0804_0000, 2 * k, 4), option=(0x_0804_0800, 0x_0804_080F)), - # ST BlueNRG - DeviceInfo("NRG1", "BlueNRG-1", "160kB", "", 0x03, None, None, None), - DeviceInfo("NRG1", "BlueNRG-1", "256kB", "", 0x0F, None, None, None), - DeviceInfo("NRG2", "BlueNRG-1", "160kB", "", 0x23, None, None, None), - DeviceInfo("NRG2", "BlueNRG-1", "256kB", "", 0x2F, None, None, None), + # ST BlueNRG; FIXME: ram/system/flash config? + DeviceInfo("NRG1", "BlueNRG-1", variant="160kB", pid=0x03, bid=None, ram=None, system=None), + DeviceInfo("NRG1", "BlueNRG-1", variant="256kB", pid=0x0F, bid=None, ram=None, system=None), + DeviceInfo("NRG2", "BlueNRG-1", variant="160kB", pid=0x23, bid=None, ram=None, system=None), + DeviceInfo("NRG2", "BlueNRG-1", variant="256kB", pid=0x2F, bid=None, ram=None, system=None), # Wiznet W7500 - DeviceInfo("WIZ", "Wiznet W7500", "", "", 0x801, None, None, None), + DeviceInfo("WIZ", "Wiznet W7500", 0x801, None, ram=None, system=None), ] DEVICES = {(dev.product_id, dev.bootloader_id): dev for dev in DEVICE_DETAILS} From 4037a6f936529097bfd1815b9d1b9c96778c8aeb Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 23 Oct 2023 09:52:06 +0200 Subject: [PATCH 312/369] dev: Extend tests of device table --- tests/unit/test_devices.py | 204 ++++++++++++++++++++++--------------- 1 file changed, 123 insertions(+), 81 deletions(-) diff --git a/tests/unit/test_devices.py b/tests/unit/test_devices.py index 0e60171..27f95c6 100644 --- a/tests/unit/test_devices.py +++ b/tests/unit/test_devices.py @@ -1,8 +1,11 @@ +import pytest + from stm32loader.devices import DEVICES, DEVICE_FAMILIES, DeviceFamily from stm32loader.bootloader import CHIP_IDS, Stm32Bootloader from devices_stm32flash import DEVICES as STM32FLASH_DEVICES + KNOWN_DUPLICATE_DEVICE_NAMES = [ "STM32F2xxxx", "STM32F40xxx/41xxx", @@ -13,10 +16,11 @@ "STM32F10xxx", "BlueNRG-1", "BlueNRG-2", + "STM32W" ] KNOWN_RAM_EXCEPTIONS = [ - # Most of these have belong to the 'known duplicate' category, + # Most of these belong to the 'known duplicate' category, # they share the same product ID (with differing bootloader ID). # The others received a comment in the device table. 0x442, @@ -30,7 +34,7 @@ ] -def test_only_specific_names_occur_twice(): +def test_only_specific_device_names_occur_twice(): all_names = set() for dev in DEVICES.values(): if dev.device_name in all_names: @@ -38,36 +42,67 @@ def test_only_specific_names_occur_twice(): all_names.add(dev.device_name) -def test_product_id_and_bootloader_id_match_device_properties(): - for ids, dev in DEVICES.items(): - device_id, bootloader_id = ids - assert dev.product_id == device_id - assert dev.bootloader_id == bootloader_id - - -def test_ram_size_is_multiple_of_256(): - for dev in DEVICES.values(): - if dev.ram_size == 0: - continue - - assert isinstance(dev.ram_size, int) - assert dev.ram_size > 0, f"{dev} ram size not None but still too low: {dev.ram_size}" - assert dev.ram_size % 256 == 0, f"{dev} ram size not a multiple of 256: {dev.ram_size}" - - -def test_system_memory_size_multiple_of_64(): - for dev in DEVICES.values(): - if dev.system_memory_size == 0: - continue - - assert isinstance(dev.system_memory_size, int) - assert dev.system_memory_size > 0, f"{dev} flash size not None but still 0: {dev.system_memory_size}" - assert dev.system_memory_size % 64 == 0, f"{dev} flash size not a multiple of 64: {dev.system_memory_size}" - - -def test_device_name_does_not_contain_underscore(): - for dev in DEVICES.values(): - assert "_" not in dev.device_name, dev.device_name +@pytest.mark.parametrize( + "ids", + DEVICES.keys(), + ids=lambda x: f"{x[0]}-{x[1]}", +) +def test_product_id_and_bootloader_id_match_device_properties(ids): + dev = DEVICES[ids] + device_id, bootloader_id = ids + assert dev.product_id == device_id + assert dev.bootloader_id == bootloader_id + + +@pytest.mark.parametrize( + "dev", + DEVICES.values(), + ids=lambda dev: str(dev).replace(" ", "-"), +) +def test_ram_size_is_multiple_of_256(dev): + if dev.ram_size == 0: + return + + assert isinstance(dev.ram_size, int) + assert dev.ram_size > 0, f"{dev} ram size not None but still too low: {dev.ram_size}" + assert dev.ram_size % 256 == 0, f"{dev} ram size not a multiple of 256: {dev.ram_size}" + + +@pytest.mark.parametrize( + "dev", + DEVICES.values(), + ids=lambda dev: str(dev).replace(" ", "-"), +) +def test_flash_size_multiple_of_16k(dev): + if dev.flash_size == 0: + return + + assert isinstance(dev.flash_size, int) + assert dev.flash_size > 0, f"{dev} flash size not None but still too low: {dev.flash_size}" + assert dev.flash_size % (16 * 1024) == 0, f"{dev} flash size not a multiple of 64: {dev.flash_size}" + + +@pytest.mark.parametrize( + "dev", + DEVICES.values(), + ids=lambda dev: str(dev).replace(" ", "-"), +) +def test_system_memory_size_multiple_of_64(dev): + if dev.system_memory_size == 0: + return + + assert isinstance(dev.system_memory_size, int) + assert dev.system_memory_size > 0, f"{dev} system memory size not None but still too low: {dev.system_memory_size}" + assert dev.system_memory_size % 64 == 0, f"{dev} system memory size not a multiple of 64: {dev.system_memory_size}" + + +@pytest.mark.parametrize( + "dev", + DEVICES.values(), + ids=lambda dev: str(dev).replace(" ", "-"), +) +def test_device_name_does_not_contain_underscore(dev): + assert "_" not in dev.device_name, dev.device_name def test_existing_product_ids_are_present_in_devices(): @@ -84,55 +119,62 @@ def test_stm32flash_product_ids_are_present_in_devices(): assert len(unknown_chip_ids) == 0, unknown_chip_ids -def test_stm32flash_device_names_match(): - for device in DEVICES.values(): - stm32flash_device = None - for dev in STM32FLASH_DEVICES: - if dev["product_id"] == device.product_id: - stm32flash_device = dev - break - - # Some devices don't exist in STM32Flash. - if not stm32flash_device and device.product_id in [0x443, 0x453, 0x474, 0x484, 0x492, 0x455, 0x481, 0x003, 0x00F, 0x0023, 0x002F, 0x801]: - continue - - # Known / reviewed deviating names. - if device.product_id in [ - 0x440, 0x442, 0x445, 0x448, 0x412, 0x410, 0x414, 0x420, 0x428, 0x418, 0x430, 0x432, - 0x422, 0x439, 0X438, 0x446, 0x467, 0x495, 0x641, 0x9A8, 0x9B0, - ]: - continue - - assert stm32flash_device, f"{device.device_name} 0x{device.product_id:03X}" - assert stm32flash_device["device_name"] == device.device_name, f"{device.device_name} 0x{device.product_id:03X}" - - - -def test_stm32flash_ram_addresses_match(): - for device in DEVICES.values(): - ref = None - for _ref in STM32FLASH_DEVICES: - if _ref["product_id"] == device.product_id: - ref = _ref - break - - if ref is None: - # not found - continue - - if device.product_id in KNOWN_RAM_EXCEPTIONS: - continue - - if device.ram is None: - assert ref["ram_start"] == ref["ram_end"], f"RAM size not 0 for device '{device.device_name}' 0x{device.product_id:03X}" - continue - - if isinstance(device.ram[0], tuple): - continue - - # print(hex(device.product_id), device, device.ram, ref) - assert device.ram[0] == ref["ram_start"], f"RAM start differs for device: '{device.device_name}' 0x{device.product_id:03X}: 0x{device.ram[0]:08X} vs 0x{ref['ram_start']:08X}." - assert device.ram[1] == ref["ram_end"], f"RAM end differs for device: '{device.device_name}' 0x{device.product_id:03X}: 0x{device.ram[1]:08X} vs 0x{ref['ram_end']:08X}." +@pytest.mark.parametrize( + "device", + DEVICES.values(), + ids=lambda device: str(device).replace(" ", "-"), +) +def test_stm32flash_device_names_match(device): + stm32flash_device = None + for dev in STM32FLASH_DEVICES: + if dev["product_id"] == device.product_id: + stm32flash_device = dev + break + + # Some devices don't exist in STM32Flash. + if not stm32flash_device and device.product_id in [0x443, 0x453, 0x474, 0x484, 0x492, 0x455, 0x481, 0x003, 0x00F, 0x0023, 0x002F, 0x801]: + return + + # Known / reviewed deviating names. + if device.product_id in [ + 0x440, 0x442, 0x445, 0x448, 0x412, 0x410, 0x414, 0x420, 0x428, 0x418, 0x430, 0x432, + 0x422, 0x439, 0X438, 0x446, 0x467, 0x495, 0x641, 0x9A8, 0x9B0, + ]: + return + + assert stm32flash_device, f"{device.device_name} 0x{device.product_id:03X}" + assert stm32flash_device["device_name"] == device.device_name, f"{device.device_name} 0x{device.product_id:03X}" + + +@pytest.mark.parametrize( + "device", + DEVICES.values(), + ids=lambda device: str(device).replace(" ", "-"), +) +def test_stm32flash_ram_addresses_match(device): + ref = None + for _ref in STM32FLASH_DEVICES: + if _ref["product_id"] == device.product_id: + ref = _ref + break + + if ref is None: + # not found + return + + if device.product_id in KNOWN_RAM_EXCEPTIONS: + return + + if device.ram is None: + assert ref["ram_start"] == ref["ram_end"], f"RAM size not 0 for device '{device.device_name}' 0x{device.product_id:03X}" + return + + if isinstance(device.ram[0], tuple): + return + + # print(hex(device.product_id), device, device.ram, ref) + assert device.ram[0] == ref["ram_start"], f"RAM start differs for device: '{device.device_name}' 0x{device.product_id:03X}: 0x{device.ram[0]:08X} vs 0x{ref['ram_start']:08X}." + assert device.ram[1] == ref["ram_end"], f"RAM end differs for device: '{device.device_name}' 0x{device.product_id:03X}: 0x{device.ram[1]:08X} vs 0x{ref['ram_end']:08X}." def test_family_uid_address_matches_existing(): From 730bc7d0232ba20c2f6561f99c0a52d49be44d10 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 18 Oct 2023 21:41:28 +0200 Subject: [PATCH 313/369] dev: Add emulation test --- tests/emulate/erasewriteverify.py | 83 +++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/tests/emulate/erasewriteverify.py b/tests/emulate/erasewriteverify.py index e69de29..c204a6a 100644 --- a/tests/emulate/erasewriteverify.py +++ b/tests/emulate/erasewriteverify.py @@ -0,0 +1,83 @@ +from pathlib import Path + +from stm32loader.main import Stm32Loader +from stm32loader.bootloader import Stm32Bootloader + +ACK = Stm32Bootloader.Reply.ACK.value + + +FIRMWARE_FILE = Path(__file__).parent / "../../firmware/generic_boot20_pc13.binary.bin" + + +class FakeConnection: + + def __init__(self): + self.is_command = True + self.next_return = [] + self.timeout = 2 + + def write(self, data): + if self.is_command: + # print("Command", data) + + if data == Stm32Bootloader.Command.GET: + # Return length, version, commands, ack + self.next_return.append(2) + self.next_return.append(0x11) + self.next_return.append(0x0) + self.next_return.append(Stm32Bootloader.Reply.ACK) + + self.is_command = not self.is_command + + def read(self, length=1): + if length == 121: + return bytes([0] * 121) + + if self.next_return: + value = self.next_return.pop(0) + if isinstance(value, int): + return [value] + return value + + return [Stm32Bootloader.Reply.ACK] + + +class FakeConfiguration: + + def __init__(self, erase, write, verify, firmware_file): + self.erase = erase + self.write = write + self.verify = verify + self.data_file = firmware_file + self.unprotect = False + self.protect = False + self.length = None + self.verbosity = 5 + self.address = 0x_0800_0000 + self.go_address = None + self.family = "F1" + + +def erase_write_verify(): + loader = Stm32Loader() + loader.configuration = FakeConfiguration(erase=True, write=True, verify=True, firmware_file=FIRMWARE_FILE) + loader.connection = FakeConnection() + loader.stm32 = Stm32Bootloader(loader.connection, device_family="F1") + + # GET + loader.connection.next_return.extend([ACK, 1, 0x05, 0x44, ACK]) + # Device ID + loader.connection.next_return.extend([ACK, 1, [0x04, 0x12], ACK]) + loader.read_device_id() + + # Read flash size + loader.connection.next_return.extend([ACK, ACK, ACK, [0x00, 0x01], ACK]) + # Read device UID + loader.connection.next_return.extend([ACK, ACK, list(range(12)), ACK]) + loader.read_device_uid() + + loader.perform_commands() + + +if __name__ == "__main__": + erase_write_verify() From 9189925668bb6a0f05fa55f3ceb1caf25dad9e1f Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 24 Oct 2023 13:17:10 +0200 Subject: [PATCH 314/369] dev: Improve emulation Read id, flash mass erase, flash write, flash read are now supported. --- tests/emulate/erasewriteverify.py | 116 +++++++++++++++++++++++------- 1 file changed, 91 insertions(+), 25 deletions(-) diff --git a/tests/emulate/erasewriteverify.py b/tests/emulate/erasewriteverify.py index c204a6a..8b00071 100644 --- a/tests/emulate/erasewriteverify.py +++ b/tests/emulate/erasewriteverify.py @@ -1,3 +1,5 @@ +import enum +import struct from pathlib import Path from stm32loader.main import Stm32Loader @@ -9,30 +11,104 @@ FIRMWARE_FILE = Path(__file__).parent / "../../firmware/generic_boot20_pc13.binary.bin" +Command = Stm32Bootloader.Command + + class FakeConnection: + COMMAND_RESPONSES = { + # Return length, bootloader version, commands + # ACK, 1, 0x05, 0x44, ACK + Command.GET: [1, 0x05, 0x44, ACK], + + # Product ID: 0x412 + Command.GET_ID: [1, [0x04, 0x12]], + } + + READ_RESPONSES = { + # Read flash size. + (0x_1FFF_F7E0, 2): [[0x00, 0x01]], + + # Read device UID. + (0x_1FFF_F7E8, 12): [[1, 0, 3, 2, 7, 6, 5, 4, 0xB, 0xA, 9, 8]], + } + def __init__(self): - self.is_command = True self.next_return = [] self.timeout = 2 + self.receiver = self.receive() + + self.flash_offset = 0x_0800_0000 + self.flash_size = 2 * 1024 * 1024 + self.flash_memory = bytearray(2 * 1024 * 1024) + + # Start coroutine. + next(self.receiver) + + def ack(self): + self.next_return.append(ACK) + + def receive(self): + while True: + # Receive a command coming in. + command_bytes = yield + command_value = struct.unpack("B", command_bytes)[0] + + # Receive CRC byte. + yield + self.ack() + + if command_value in self.COMMAND_RESPONSES: + self.next_return.extend(self.COMMAND_RESPONSES[command_value]) + elif command_value == Command.READ_MEMORY.value: + # Receive address with CRC. + address_bytes = yield + address = struct.unpack(">I", address_bytes[0:4])[0] + self.ack() + + # Receive number of bytes + length_bytes = yield + length = struct.unpack("B", length_bytes)[0] + 1 + + # Receive CRC + yield + self.ack() + + # Set up data to respond. + if self.flash_offset <= address < self.flash_offset + self.flash_size: + # Return flash data. + flash_offset = address - self.flash_offset + self.next_return.append(list(self.flash_memory[flash_offset: flash_offset + length])) + else: + self.next_return.extend(self.READ_RESPONSES[(address, length)]) + elif command_value == Command.EXTENDED_ERASE.value: + pages_bytes = yield + pages = struct.unpack(">H", pages_bytes[0:2]) + if pages == 0xFFFF: + # Erase all. + self.flash_memory[:] = 0xFF + self.next_return.append(ACK) + elif command_value == Command.WRITE_MEMORY.value: + address_bytes = yield + address = struct.unpack(">I", address_bytes[0:4])[0] + size_bytes = yield + byte_count = struct.unpack("B", size_bytes)[0] + 1 + data = yield + crc = yield + + assert len(data) == byte_count, f"Length does not match byte count: {len(data)} vs {byte_count}" + + # Record data in flash memory. + flash_offset = address - 0x_0800_0000 + self.flash_memory[flash_offset: flash_offset + byte_count] = data + else: + raise NotImplementedError() def write(self, data): - if self.is_command: - # print("Command", data) - - if data == Stm32Bootloader.Command.GET: - # Return length, version, commands, ack - self.next_return.append(2) - self.next_return.append(0x11) - self.next_return.append(0x0) - self.next_return.append(Stm32Bootloader.Reply.ACK) - - self.is_command = not self.is_command + # Send to coroutine. + self.receiver.send(data) def read(self, length=1): - if length == 121: - return bytes([0] * 121) - if self.next_return: value = self.next_return.pop(0) if isinstance(value, int): @@ -64,18 +140,8 @@ def erase_write_verify(): loader.connection = FakeConnection() loader.stm32 = Stm32Bootloader(loader.connection, device_family="F1") - # GET - loader.connection.next_return.extend([ACK, 1, 0x05, 0x44, ACK]) - # Device ID - loader.connection.next_return.extend([ACK, 1, [0x04, 0x12], ACK]) loader.read_device_id() - - # Read flash size - loader.connection.next_return.extend([ACK, ACK, ACK, [0x00, 0x01], ACK]) - # Read device UID - loader.connection.next_return.extend([ACK, ACK, list(range(12)), ACK]) loader.read_device_uid() - loader.perform_commands() From 6edaae0dfb334903a58e62624f053aa44824c27b Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 24 Oct 2023 13:17:24 +0200 Subject: [PATCH 315/369] clean(cosmetic) --- src/stm32loader/bootloader.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/stm32loader/bootloader.py b/src/stm32loader/bootloader.py index d095ee6..3aa7b4b 100644 --- a/src/stm32loader/bootloader.py +++ b/src/stm32loader/bootloader.py @@ -745,7 +745,7 @@ def read_memory_data(self, address, length): """ data = bytearray() chunk_count = int(math.ceil(length / float(self.data_transfer_size))) - self.debug(5, "Read %d chunks at address 0x%X..." % (chunk_count, address)) + self.debug(5, "Read %7d bytes in %3d chunks at address 0x%X..." % (length, chunk_count, address)) with self.show_progress("Reading", maximum=chunk_count) as progress_bar: while length: read_length = min(length, self.data_transfer_size) @@ -769,7 +769,7 @@ def write_memory_data(self, address, data): length = len(data) chunk_count = int(math.ceil(length / float(self.data_transfer_size))) offset = 0 - self.debug(5, "Write %d chunks at address 0x%X..." % (chunk_count, address)) + self.debug(5, "Write %6d bytes in %3d chunks at address 0x%X..." % (length, chunk_count, address)) with self.show_progress("Writing", maximum=chunk_count) as progress_bar: while length: From da4f62672add0b28795e8711b6d534752f50bd60 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 24 Oct 2023 13:20:53 +0200 Subject: [PATCH 316/369] dev: Use device table --- src/stm32loader/main.py | 7 ++++++- tests/emulate/erasewriteverify.py | 6 +++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/stm32loader/main.py b/src/stm32loader/main.py index 826dc29..0bd6674 100644 --- a/src/stm32loader/main.py +++ b/src/stm32loader/main.py @@ -30,6 +30,7 @@ progress_bar = None from stm32loader import args, bootloader, hexfile +# from stm32loader.devices import DEVICES from stm32loader.uart import SerialConnection @@ -177,11 +178,15 @@ def reset(self): self.stm32.reset_from_flash() def read_device_id(self): - """Show chip ID and bootloader version.""" + """Show product ID and bootloader version.""" boot_version = self.stm32.get() self.debug(0, "Bootloader version: 0x%X" % boot_version) device_id = self.stm32.get_id() + family = self.configuration.family + # if not family: + # family = + if family == "NRG": # ST AN4872. # Three bytes encode metal fix, mask set, diff --git a/tests/emulate/erasewriteverify.py b/tests/emulate/erasewriteverify.py index 8b00071..b76a13a 100644 --- a/tests/emulate/erasewriteverify.py +++ b/tests/emulate/erasewriteverify.py @@ -120,7 +120,7 @@ def read(self, length=1): class FakeConfiguration: - def __init__(self, erase, write, verify, firmware_file): + def __init__(self, erase, write, verify, firmware_file, family=None): self.erase = erase self.write = write self.verify = verify @@ -131,12 +131,12 @@ def __init__(self, erase, write, verify, firmware_file): self.verbosity = 5 self.address = 0x_0800_0000 self.go_address = None - self.family = "F1" + self.family = family def erase_write_verify(): loader = Stm32Loader() - loader.configuration = FakeConfiguration(erase=True, write=True, verify=True, firmware_file=FIRMWARE_FILE) + loader.configuration = FakeConfiguration(erase=True, write=True, verify=True, firmware_file=FIRMWARE_FILE, family=None) loader.connection = FakeConnection() loader.stm32 = Stm32Bootloader(loader.connection, device_family="F1") From 052ed976f8674cd6ac0735af9f24fe6ced619117 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 24 Oct 2023 13:21:19 +0200 Subject: [PATCH 317/369] dev: Add dummy data to bin file --- firmware/generic_boot20_pc13.binary.bin | Bin 0 -> 8192 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/firmware/generic_boot20_pc13.binary.bin b/firmware/generic_boot20_pc13.binary.bin index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..01173aaa0620b1754be138c456e7dbfa3a1d58ef 100644 GIT binary patch literal 8192 zcmZQzWMXDvWn<^yMC+6cQE@6%&_`l#-T_m6KOcR8m$^Ra4i{)Y8_`)zddH zG%_|ZH8Z!cw6eCbwX=6{baHlab#wRd^z!!c_45x13RUz zF>}`JIdkXDU$Ah|;w4L$Enl&6)#^2C*R9{Mant54TeofBv2)k%J$v`@QIXeD3I{rO6{y#eZF*^S Date: Tue, 16 Jan 2024 11:03:33 +0100 Subject: [PATCH 318/369] clean: Drop unused import --- src/stm32loader/bootloader.py | 1 - stm32loader/emulated/fake.py | 0 tests/emulate/erasewriteverify.py | 149 --------------------------- tests/integration/test_full_cycle.py | 24 +++++ 4 files changed, 24 insertions(+), 150 deletions(-) create mode 100644 stm32loader/emulated/fake.py delete mode 100644 tests/emulate/erasewriteverify.py create mode 100644 tests/integration/test_full_cycle.py diff --git a/src/stm32loader/bootloader.py b/src/stm32loader/bootloader.py index 3aa7b4b..4a3c06d 100644 --- a/src/stm32loader/bootloader.py +++ b/src/stm32loader/bootloader.py @@ -23,7 +23,6 @@ import math import operator import struct -import sys import time from functools import lru_cache, reduce diff --git a/stm32loader/emulated/fake.py b/stm32loader/emulated/fake.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/emulate/erasewriteverify.py b/tests/emulate/erasewriteverify.py deleted file mode 100644 index b76a13a..0000000 --- a/tests/emulate/erasewriteverify.py +++ /dev/null @@ -1,149 +0,0 @@ -import enum -import struct -from pathlib import Path - -from stm32loader.main import Stm32Loader -from stm32loader.bootloader import Stm32Bootloader - -ACK = Stm32Bootloader.Reply.ACK.value - - -FIRMWARE_FILE = Path(__file__).parent / "../../firmware/generic_boot20_pc13.binary.bin" - - -Command = Stm32Bootloader.Command - - -class FakeConnection: - - COMMAND_RESPONSES = { - # Return length, bootloader version, commands - # ACK, 1, 0x05, 0x44, ACK - Command.GET: [1, 0x05, 0x44, ACK], - - # Product ID: 0x412 - Command.GET_ID: [1, [0x04, 0x12]], - } - - READ_RESPONSES = { - # Read flash size. - (0x_1FFF_F7E0, 2): [[0x00, 0x01]], - - # Read device UID. - (0x_1FFF_F7E8, 12): [[1, 0, 3, 2, 7, 6, 5, 4, 0xB, 0xA, 9, 8]], - } - - def __init__(self): - self.next_return = [] - self.timeout = 2 - self.receiver = self.receive() - - self.flash_offset = 0x_0800_0000 - self.flash_size = 2 * 1024 * 1024 - self.flash_memory = bytearray(2 * 1024 * 1024) - - # Start coroutine. - next(self.receiver) - - def ack(self): - self.next_return.append(ACK) - - def receive(self): - while True: - # Receive a command coming in. - command_bytes = yield - command_value = struct.unpack("B", command_bytes)[0] - - # Receive CRC byte. - yield - self.ack() - - if command_value in self.COMMAND_RESPONSES: - self.next_return.extend(self.COMMAND_RESPONSES[command_value]) - elif command_value == Command.READ_MEMORY.value: - # Receive address with CRC. - address_bytes = yield - address = struct.unpack(">I", address_bytes[0:4])[0] - self.ack() - - # Receive number of bytes - length_bytes = yield - length = struct.unpack("B", length_bytes)[0] + 1 - - # Receive CRC - yield - self.ack() - - # Set up data to respond. - if self.flash_offset <= address < self.flash_offset + self.flash_size: - # Return flash data. - flash_offset = address - self.flash_offset - self.next_return.append(list(self.flash_memory[flash_offset: flash_offset + length])) - else: - self.next_return.extend(self.READ_RESPONSES[(address, length)]) - elif command_value == Command.EXTENDED_ERASE.value: - pages_bytes = yield - pages = struct.unpack(">H", pages_bytes[0:2]) - if pages == 0xFFFF: - # Erase all. - self.flash_memory[:] = 0xFF - self.next_return.append(ACK) - elif command_value == Command.WRITE_MEMORY.value: - address_bytes = yield - address = struct.unpack(">I", address_bytes[0:4])[0] - size_bytes = yield - byte_count = struct.unpack("B", size_bytes)[0] + 1 - data = yield - crc = yield - - assert len(data) == byte_count, f"Length does not match byte count: {len(data)} vs {byte_count}" - - # Record data in flash memory. - flash_offset = address - 0x_0800_0000 - self.flash_memory[flash_offset: flash_offset + byte_count] = data - else: - raise NotImplementedError() - - def write(self, data): - # Send to coroutine. - self.receiver.send(data) - - def read(self, length=1): - if self.next_return: - value = self.next_return.pop(0) - if isinstance(value, int): - return [value] - return value - - return [Stm32Bootloader.Reply.ACK] - - -class FakeConfiguration: - - def __init__(self, erase, write, verify, firmware_file, family=None): - self.erase = erase - self.write = write - self.verify = verify - self.data_file = firmware_file - self.unprotect = False - self.protect = False - self.length = None - self.verbosity = 5 - self.address = 0x_0800_0000 - self.go_address = None - self.family = family - - -def erase_write_verify(): - loader = Stm32Loader() - loader.configuration = FakeConfiguration(erase=True, write=True, verify=True, firmware_file=FIRMWARE_FILE, family=None) - loader.connection = FakeConnection() - loader.stm32 = Stm32Bootloader(loader.connection, device_family="F1") - - loader.read_device_id() - loader.read_device_uid() - loader.perform_commands() - - -if __name__ == "__main__": - erase_write_verify() diff --git a/tests/integration/test_full_cycle.py b/tests/integration/test_full_cycle.py new file mode 100644 index 0000000..2b89427 --- /dev/null +++ b/tests/integration/test_full_cycle.py @@ -0,0 +1,24 @@ +from pathlib import Path + +from stm32loader.main import Stm32Loader +from stm32loader.bootloader import Stm32Bootloader +from stm32loader.emulated.fake import FakeConfiguration, FakeConnection + + +FIRMWARE_FILE = Path(__file__).parent / "../../firmware/generic_boot20_pc13.binary.bin" + + +def erase_write_verify(): + loader = Stm32Loader() + loader.configuration = FakeConfiguration(erase=True, write=True, verify=True, firmware_file=FIRMWARE_FILE, family=None) + loader.connection = FakeConnection() + loader.stm32 = Stm32Bootloader(loader.connection, device_family="F1", verbosity=5) + + loader.detect_device() + loader.read_device_uid() + loader.read_flash_size() + loader.perform_commands() + + +if __name__ == "__main__": + erase_write_verify() From b0052525dd5b5f05bf249f969d3b7940548ff19a Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Jan 2024 14:27:01 +0100 Subject: [PATCH 319/369] dev: Import devices, deviceflag --- src/stm32loader/bootloader.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/stm32loader/bootloader.py b/src/stm32loader/bootloader.py index 4a3c06d..2d40efb 100644 --- a/src/stm32loader/bootloader.py +++ b/src/stm32loader/bootloader.py @@ -26,6 +26,9 @@ import time from functools import lru_cache, reduce +from stm32loader.devices import DEVICES, DeviceFlag + + CHIP_IDS = { # see ST AN2606 Table 136 Bootloader device-dependent parameters # 16 to 32 KiB From bf05cb60bceff77df79706e537ce83b0ace990ec Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Jan 2024 14:27:09 +0100 Subject: [PATCH 320/369] dev: Add devicedetectionerror --- src/stm32loader/bootloader.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/stm32loader/bootloader.py b/src/stm32loader/bootloader.py index 2d40efb..35560de 100644 --- a/src/stm32loader/bootloader.py +++ b/src/stm32loader/bootloader.py @@ -114,6 +114,11 @@ class MissingDependencyError(Stm32LoaderError): """Exception: required dependency is missing.""" +class DeviceDetectionError(Stm32LoaderError): + """Exception: could not detect device type.""" + + + class ShowProgress: """ Show progress through a progress bar, as a context manager. From 28ddbdf89c3c29d7b55ce0274a8514ce9aa17290 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Jan 2024 14:27:23 +0100 Subject: [PATCH 321/369] doc: Add more comments --- src/stm32loader/bootloader.py | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/src/stm32loader/bootloader.py b/src/stm32loader/bootloader.py index 35560de..007bbcc 100644 --- a/src/stm32loader/bootloader.py +++ b/src/stm32loader/bootloader.py @@ -188,13 +188,19 @@ class Stm32Bootloader: @enum.unique class Command(enum.IntEnum): - """STM32 native bootloader command values.""" + """ + STM32 native bootloader command values. + + Refer to ST AN3155, AN4872, AN2606. + """ # pylint: disable=too-few-public-methods - # See ST AN3155, AN4872 + # Get bootloader protocol version and supported commands. GET = 0x00 + # Get bootloader protocol version. GET_VERSION = 0x01 + # Get chip/product/device ID. ot available on STM32F103. GET_ID = 0x02 READ_MEMORY = 0x11 GO = 0x21 @@ -459,7 +465,7 @@ def get(self): def get_version(self): """ - Return the bootloader version. + Return the bootloader protocol version. Read protection status readout is not yet implemented. """ @@ -475,7 +481,7 @@ def get_version(self): return version def get_id(self): - """Send the 'Get ID' command and return the device (model) ID.""" + """Send the 'Get ID' command and return the chip/product/device ID.""" self.command(self.Command.GET_ID, "Get ID") length = bytearray(self.connection.read())[0] id_data = bytearray(self.connection.read(length + 1)) @@ -555,6 +561,25 @@ def _get_flash_size_and_uid_bulk(self): return flash_size, device_uid + def get_uid(self): + """ + Send the 'Get UID' command and return the device UID. + + Return UID_NOT_SUPPORTED if the device does not have + a UID. + + :return byterary: UID bytes of the device, or 0 or -1 when + not available. + """ + uid_address = self.UID_ADDRESS.get(self.device_family, self.UID_ADDRESS_UNKNOWN) + if uid_address is None: + return self.UID_NOT_SUPPORTED + if uid_address == self.UID_ADDRESS_UNKNOWN: + return self.UID_ADDRESS_UNKNOWN + + uid = self.read_memory(uid_address, 12) + return uid + @classmethod def format_uid(cls, uid): """Return a readable string from the given UID.""" From 798d5354cec723c07b84d456fbdbd838856370c1 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Jan 2024 14:27:44 +0100 Subject: [PATCH 322/369] dev: Add errno for flash_size_unknown --- src/stm32loader/bootloader.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/stm32loader/bootloader.py b/src/stm32loader/bootloader.py index 007bbcc..5aa99fb 100644 --- a/src/stm32loader/bootloader.py +++ b/src/stm32loader/bootloader.py @@ -262,11 +262,15 @@ class Reply(enum.IntEnum): UID_SWAP = [[1, 0], [3, 2], [7, 6, 5, 4], [11, 10, 9, 8]] - # Part does not support unique ID feature - UID_NOT_SUPPORTED = 0 # stm32loader does not know the address for the unique ID UID_ADDRESS_UNKNOWN = -1 + # Flash size can not be read. + FLASH_SIZE_UNKNOWN = -2 + + # Part does not support unique ID feature + UID_NOT_SUPPORTED = 3 + FLASH_SIZE_ADDRESS = { # ST RM0360 section 27.1 Memory size data register # F030x4/x6/x8/xC, F070x6/xB From 8cce94c36ef494ca30fc90fedef970461a3593fb Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Jan 2024 14:28:01 +0100 Subject: [PATCH 323/369] pass in device --- src/stm32loader/bootloader.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/stm32loader/bootloader.py b/src/stm32loader/bootloader.py index 5aa99fb..78f86c5 100644 --- a/src/stm32loader/bootloader.py +++ b/src/stm32loader/bootloader.py @@ -360,7 +360,7 @@ class Reply(enum.IntEnum): SYNCHRONIZE_ATTEMPTS = 2 - def __init__(self, connection, device_family=None, verbosity=5, show_progress=None): + def __init__(self, connection, device=None, device_family=None, verbosity=5, show_progress=None): """ Construct the Stm32Bootloader object. @@ -385,6 +385,7 @@ def __init__(self, connection, device_family=None, verbosity=5, show_progress=No self.data_transfer_size = self.DATA_TRANSFER_SIZE.get(device_family or "default") self.flash_page_size = self.FLASH_PAGE_SIZE.get(device_family or "default") self.device_family = device_family or "F1" + self.device = device def write(self, *data): """Write the given data to the MCU.""" From 2fc7613d9726145bdd322fbc2b587410c708bab3 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Jan 2024 14:28:13 +0100 Subject: [PATCH 324/369] dev: Don't print to stderr --- src/stm32loader/bootloader.py | 6 +++--- src/stm32loader/main.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/stm32loader/bootloader.py b/src/stm32loader/bootloader.py index 78f86c5..79507ef 100644 --- a/src/stm32loader/bootloader.py +++ b/src/stm32loader/bootloader.py @@ -404,7 +404,7 @@ def write_and_ack(self, message, *data): def debug(self, level, message): """Print the given message if its level is low enough.""" if self.verbosity >= level: - print(message, file=sys.stderr) + print(message) def reset_from_system_memory(self): """Reset the MCU with boot0 enabled to enter the bootloader.""" @@ -428,7 +428,7 @@ def reset_from_system_memory(self): for attempt in range(self.SYNCHRONIZE_ATTEMPTS): if attempt: - print("Bootloader activation timeout -- retrying", file=sys.stderr) + print("Bootloader activation timeout -- retrying") self.write(self.Command.SYNCHRONIZE) read_data = bytearray(self.connection.read()) @@ -732,7 +732,7 @@ def extended_erase_memory(self, pages=None): previous_timeout_value = self.connection.timeout self.connection.timeout = 30 - print("Extended erase (0x44), this can take ten seconds or more", file=sys.stderr) + print("Extended erase (0x44), this can take ten seconds or more") try: self._wait_for_ack("0x44 erasing failed") finally: diff --git a/src/stm32loader/main.py b/src/stm32loader/main.py index 0bd6674..e587f40 100644 --- a/src/stm32loader/main.py +++ b/src/stm32loader/main.py @@ -48,7 +48,7 @@ def __init__(self): def debug(self, level, message): """Log a message to stderror if its level is low enough.""" if self.configuration.verbosity >= level: - print(message, file=sys.stderr) + print(message) def parse_arguments(self, arguments): """Parse the list of command-line arguments.""" @@ -160,7 +160,7 @@ def perform_commands(self): read_data = self.stm32.read_memory_data(self.configuration.address, len(binary_data)) try: bootloader.Stm32Bootloader.verify_data(read_data, binary_data) - print("Verification OK", file=sys.stderr) + print("Verification OK") except bootloader.DataMismatchError as e: print("Verification FAILED: %s" % e, file=sys.stderr) sys.exit(1) From 57053ffc3d68e3291b894d4153ffb271c302eb90 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Jan 2024 14:28:45 +0100 Subject: [PATCH 325/369] dev: Remember supported_commands for later use --- src/stm32loader/bootloader.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/stm32loader/bootloader.py b/src/stm32loader/bootloader.py index 79507ef..e481d85 100644 --- a/src/stm32loader/bootloader.py +++ b/src/stm32loader/bootloader.py @@ -461,10 +461,10 @@ def get(self): length = bytearray(self.connection.read())[0] version = bytearray(self.connection.read())[0] self.debug(10, " Bootloader version: " + hex(version)) - data = bytearray(self.connection.read(length)) - if self.Command.EXTENDED_ERASE in data: - self.extended_erase = True - self.debug(10, " Available commands: " + ", ".join(hex(b) for b in data)) + supported_commands = bytearray(self.connection.read(length)) + self.supported_commands = {command: True for command in supported_commands} + self.extended_erase = self.Command.EXTENDED_ERASE in self.supported_commands + self.debug(10, " Available commands: " + ", ".join(hex(b) for b in self.supported_commands)) self._wait_for_ack("0x00 end") return version From 738e61066d32391e24f120d4fb1f387f55e5f354 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Jan 2024 14:29:55 +0100 Subject: [PATCH 326/369] dev: Get flash_size, UID also on long-UID devices --- src/stm32loader/bootloader.py | 12 ++++++------ tests/unit/test_bootloader.py | 36 +++++++++++++++++------------------ 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/stm32loader/bootloader.py b/src/stm32loader/bootloader.py index e481d85..f85fd8b 100644 --- a/src/stm32loader/bootloader.py +++ b/src/stm32loader/bootloader.py @@ -576,13 +576,13 @@ def get_uid(self): :return byterary: UID bytes of the device, or 0 or -1 when not available. """ - uid_address = self.UID_ADDRESS.get(self.device_family, self.UID_ADDRESS_UNKNOWN) - if uid_address is None: - return self.UID_NOT_SUPPORTED - if uid_address == self.UID_ADDRESS_UNKNOWN: - return self.UID_ADDRESS_UNKNOWN + if self.device.flags & DeviceFlag.LONG_UID_ACCESS: + _flash_size, uid = self.get_flash_size_and_uid() + else: + if not self.device.family.uid_address: + return self.UID_NOT_SUPPORTED + uid = self.read_memory(self.device.family.uid_address, 12) - uid = self.read_memory(uid_address, 12) return uid @classmethod diff --git a/tests/unit/test_bootloader.py b/tests/unit/test_bootloader.py index 98e8122..73ffe90 100644 --- a/tests/unit/test_bootloader.py +++ b/tests/unit/test_bootloader.py @@ -6,6 +6,7 @@ from stm32loader import bootloader as Stm32 from stm32loader.bootloader import PageIndexError, Stm32Bootloader +from stm32loader.devices import DEVICES # pylint: disable=missing-docstring, redefined-outer-name @@ -223,27 +224,29 @@ def test_verify_data_with_non_identical_data_raises_verify_error_complaining_abo @pytest.mark.parametrize( - "family", - ["F1", "F3", "F7"], + "pid_bid", [(0x412, None), (0x432, 0x50), (0x452, 0x90)] ) -def test_get_uid_for_known_family_reads_at_correct_address(connection, family): - bootloader = Stm32Bootloader(connection, device_family=family) +def test_get_uid_for_known_device_reads_at_correct_address(connection, pid_bid): + device = DEVICES.get(pid_bid) + bootloader = Stm32Bootloader(connection, device=device) + bootloader.read_memory = MagicMock() bootloader.get_uid() - uid_address = bootloader.UID_ADDRESS[family] - bootloader.read_memory.assert_called_once_with(uid_address, 12) + + uid_address = { + (0x412, None): 0x_1FFF_F7E8, + (0x432, 0x50): 0x_1FFF_F7AC, + (0x452, 0x90): 0x_1FF0_F420, + }[pid_bid] + bootloader.read_memory.assert_called_once_with(uid_address) def test_get_uid_for_family_without_uid_returns_uid_not_supported(connection): - bootloader = Stm32Bootloader(connection, device_family="F0") + device = DEVICES.get(( 0x443, 0x51)) + bootloader = Stm32Bootloader(connection, device=device) assert bootloader.UID_NOT_SUPPORTED == bootloader.get_uid() -def test_get_uid_for_unknown_family_returns_uid_address_unknown(connection): - bootloader = Stm32Bootloader(connection, device_family="X") - assert bootloader.UID_ADDRESS_UNKNOWN == bootloader.get_uid() - - @pytest.mark.parametrize( "family", ["F4", "L0"], @@ -271,12 +274,9 @@ def test_get_flash_size_and_uid_for_exception_families_returns_size_and_uid(conn @pytest.mark.parametrize( "uid_string", [ - (0, "UID not supported in this part"), - (-1, "UID address unknown"), - ( - bytearray(b"\x12\x34\x56\x78\x9a\xbc\xde\x01\x12\x34\x56\x78"), - "3412-7856-01DEBC9A-78563412", - ), + (Stm32Bootloader.UID_NOT_SUPPORTED, "UID not supported in this part"), + (Stm32Bootloader.UID_ADDRESS_UNKNOWN, "UID address unknown"), + (bytearray(b"\x12\x34\x56\x78\x9a\xbc\xde\x01\x12\x34\x56\x78"), "3412-7856-01DEBC9A-78563412"), ], ) def test_format_uid_returns_correct_string(bootloader, uid_string): From 45340030bafac55447b3c8071a94e9b8fdde03d7 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Jan 2024 14:31:11 +0100 Subject: [PATCH 327/369] dev: Add logic to detect device ID --- src/stm32loader/bootloader.py | 24 ++++++++++++++++++++++++ src/stm32loader/main.py | 33 ++++++++++++++++++++++----------- 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/src/stm32loader/bootloader.py b/src/stm32loader/bootloader.py index f85fd8b..8208c58 100644 --- a/src/stm32loader/bootloader.py +++ b/src/stm32loader/bootloader.py @@ -585,6 +585,30 @@ def get_uid(self): return uid + def detect_device(self): + product_id = self.get_id() + + # Look up device details based on ID *without* bootloader ID. + self.device = DEVICES.get((product_id, None)) + + if not self.device: + raise DeviceDetectionError(f"Unknown device type: no type known for product id: 0x{product_id:03X}") + + # Look up the product's bootloader ID. + bootloader_id = self.get_bootloader_id() + + # Now we can possibly *refine* the product: look up with product ID *and* bootloader ID. + self.device = DEVICES.get((product_id, bootloader_id), self.device) + + def get_bootloader_id(self): + if not self.device.bootloader_id_address: + return None + + bootloader_id_byte = self.read_memory_data(self.device.bootloader_id_address, 1) + bootloader_id = struct.unpack("B", bootloader_id_byte)[0] + + return bootloader_id + @classmethod def format_uid(cls, uid): """Return a readable string from the given UID.""" diff --git a/src/stm32loader/main.py b/src/stm32loader/main.py index e587f40..14ee6a4 100644 --- a/src/stm32loader/main.py +++ b/src/stm32loader/main.py @@ -177,17 +177,21 @@ def reset(self): """Reset the microcontroller.""" self.stm32.reset_from_flash() + def detect_device(self): + boot_version = self.stm32.get() + self.debug(0, "Bootloader version: 0x%X" % boot_version) + self.stm32.detect_device() + self.debug(5, 'Bootloader ID: 0x%02X' % self.stm32.device.bootloader_id) + self.debug(0, f"Chip ID: 0x{self.stm32.device.product_id:03X}") + self.debug(0, f"Chip model: {self.stm32.device.device_name}") + def read_device_id(self): """Show product ID and bootloader version.""" boot_version = self.stm32.get() self.debug(0, "Bootloader version: 0x%X" % boot_version) device_id = self.stm32.get_id() - family = self.configuration.family - # if not family: - # family = - - if family == "NRG": + if self.configuration.family == "NRG": # ST AN4872. # Three bytes encode metal fix, mask set, # BlueNRG-series + flash size. @@ -202,12 +206,7 @@ def read_device_id(self): ) def read_device_uid(self): - """Show chip UID and flash size.""" - family = self.configuration.family - if not family: - self.debug(0, "Supply --family to see flash size and device UID, e.g: -f F1") - return - + """Show chip UID.""" try: flash_size = self.stm32.get_flash_size() device_uid = self.stm32.get_uid() @@ -220,6 +219,18 @@ def read_device_uid(self): device_uid_string = self.stm32.format_uid(device_uid) self.debug(0, "Device UID: %s" % device_uid_string) + + def read_flash_size(self): + """Show chip flash size.""" + try: + flash_size = self.stm32.get_flash_size() + except bootloader.CommandError as e: + self.debug( + 0, + "Something was wrong with reading chip family data: " + str(e), + ) + return + self.debug(0, "Flash size: %d KiB" % flash_size) @staticmethod From 5b696a7b8a375a7595fd604312bab4b797f3720d Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Jan 2024 14:31:40 +0100 Subject: [PATCH 328/369] dev: Be less verbose --- src/stm32loader/bootloader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stm32loader/bootloader.py b/src/stm32loader/bootloader.py index 8208c58..c3bccb9 100644 --- a/src/stm32loader/bootloader.py +++ b/src/stm32loader/bootloader.py @@ -806,7 +806,7 @@ def read_memory_data(self, address, length): """ data = bytearray() chunk_count = int(math.ceil(length / float(self.data_transfer_size))) - self.debug(5, "Read %7d bytes in %3d chunks at address 0x%X..." % (length, chunk_count, address)) + self.debug(10, "Read %7d bytes in %3d chunks at address 0x%X..." % (length, chunk_count, address)) with self.show_progress("Reading", maximum=chunk_count) as progress_bar: while length: read_length = min(length, self.data_transfer_size) From a7fc82c2c9ee0590b5ae9b541e9a9b482f3fae57 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Jan 2024 14:31:49 +0100 Subject: [PATCH 329/369] dev: Add DeviceFlag --- src/stm32loader/devices.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/stm32loader/devices.py b/src/stm32loader/devices.py index 6da4ab7..07e059f 100644 --- a/src/stm32loader/devices.py +++ b/src/stm32loader/devices.py @@ -32,6 +32,18 @@ class DeviceFamily(enum.Enum): WIZ = "WIZ" +@enum.unique +class DeviceFlag(enum.IntEnum): + NONE = 0 + OBL_LAUNCH = 1 + CLEAR_PEMPTY = 2 + # For some reason, F4 (at least, NUCLEO F401RE) can't read the 12 or 2 + # bytes for UID and flash size directly. + # Reading a whole chunk of 256 bytes at 0x1FFFA700 does work and + # requires some data extraction. + LONG_UID_ACCESS = 8 + + class DeviceFamilyInfo: def __init__( From dac555d2f19dec458218686f3362c62a81d4814d Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Jan 2024 14:32:21 +0100 Subject: [PATCH 330/369] dev: Add device family name --- src/stm32loader/devices.py | 52 +++++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/src/stm32loader/devices.py b/src/stm32loader/devices.py index 07e059f..8a83ae4 100644 --- a/src/stm32loader/devices.py +++ b/src/stm32loader/devices.py @@ -48,59 +48,65 @@ class DeviceFamilyInfo: def __init__( self, + name, uid_address=None, flash_size_address=None, flash_page_size=1024, transfer_size=256, mass_erase=True, option_bytes=None, + bootloader_id_address=None, + flags=DeviceFlag.NONE, ): + self.name = name self.uid_address = uid_address self.flash_size_address = flash_size_address self.flash_page_size = flash_page_size self.transfer_size = transfer_size self.mass_erase = mass_erase self.option_bytes = option_bytes + self.bootloader_id_address = bootloader_id_address + self.family_default_flags = flags DEVICE_FAMILIES = { - DeviceFamily.C0: DeviceFamilyInfo(), + DeviceFamily.C0: DeviceFamilyInfo("C0", bootloader_id_address=0x_1FFF_17FE), # RM0360 - DeviceFamily.F0: DeviceFamilyInfo(flash_size_address=0x_1FFF_F7CC, option_bytes=(0x_1FFF_F800, 0x_1FFF_F80F)), + DeviceFamily.F0: DeviceFamilyInfo("F0", flash_size_address=0x_1FFF_F7CC, option_bytes=(0x_1FFF_F800, 0x_1FFF_F80F)), # RM0008 - DeviceFamily.F1: DeviceFamilyInfo(uid_address=0x_1FFF_F7E8, flash_size_address=0x_1FFF_F7E0, option_bytes=(0x_1FFF_F800, 0x_1FFF_F80F)), - DeviceFamily.F2: DeviceFamilyInfo(option_bytes=(0x_1FFF_C000, 0x_1FFF_C00F)), + DeviceFamily.F1: DeviceFamilyInfo("F1", uid_address=0x_1FFF_F7E8, flash_size_address=0x_1FFF_F7E0, option_bytes=(0x_1FFF_F800, 0x_1FFF_F80F)), + DeviceFamily.F2: DeviceFamilyInfo("F2", option_bytes=(0x_1FFF_C000, 0x_1FFF_C00F), bootloader_id_address=0x_1FFF_77DE), # RM0366, RM0365, RM0316, RM0313, RM4510 - DeviceFamily.F3: DeviceFamilyInfo(uid_address=0x_1FFF_F7AC, flash_size_address=0x_1FFF_F7CC, flash_page_size=2048), + DeviceFamily.F3: DeviceFamilyInfo("F3", uid_address=0x_1FFF_F7AC, flash_size_address=0x_1FFF_F7CC, flash_page_size=2048, bootloader_id_address=0x_1FFF_F796), # RM0090 - DeviceFamily.F4: DeviceFamilyInfo(uid_address=0x_1FFF_7A10, flash_size_address=0x_1FFF_7A22), + DeviceFamily.F4: DeviceFamilyInfo("F4", uid_address=0x_1FFF_7A10, flash_size_address=0x_1FFF_7A22, bootloader_id_address=0x_1FFF_76DE, flags=DeviceFlag.LONG_UID_ACCESS), # RM0385 - DeviceFamily.F7: DeviceFamilyInfo(uid_address=0x_1FF0_F420, flash_size_address=0x_1FF0_F442), + DeviceFamily.F7: DeviceFamilyInfo("F7", uid_address=0x_1FF0_F420, flash_size_address=0x_1FF0_F442, bootloader_id_address=0x_1FF0_EDBE), # RM0444 - DeviceFamily.G0: DeviceFamilyInfo(uid_address=0x_1FFF_7590, flash_size_address=0x_1FFF_75E0), - DeviceFamily.G4: DeviceFamilyInfo(), - DeviceFamily.H5: DeviceFamilyInfo(), + DeviceFamily.G0: DeviceFamilyInfo("G0", uid_address=0x_1FFF_7590, flash_size_address=0x_1FFF_75E0), + DeviceFamily.G4: DeviceFamilyInfo("G4", bootloader_id_address=0x_1FFF_6FFE), + DeviceFamily.H5: DeviceFamilyInfo("H5", ), # RM0433 - DeviceFamily.H7: DeviceFamilyInfo(uid_address=0x_1FF1_E800, flash_size_address=0x_1FF1_E880, flash_page_size=128 * 1024), + DeviceFamily.H7: DeviceFamilyInfo("H7", uid_address=0x_1FF1_E800, flash_size_address=0x_1FF1_E880, flash_page_size=128 * 1024), # FIXME TWO RMs? # RM0451, RM4510 - DeviceFamily.L0: DeviceFamilyInfo(uid_address=0x_1FF8_0050, flash_size_address=0x_1FF8_007C, transfer_size=128, flash_page_size=128, mass_erase=False), - DeviceFamily.L1: DeviceFamilyInfo(mass_erase=False), + DeviceFamily.L0: DeviceFamilyInfo("L0", uid_address=0x_1FF8_0050, flash_size_address=0x_1FF8_007C, transfer_size=128, flash_page_size=128, mass_erase=False, flags=DeviceFlag.LONG_UID_ACCESS), + DeviceFamily.L1: DeviceFamilyInfo("L1", mass_erase=False), # RM0394 - DeviceFamily.L4: DeviceFamilyInfo(uid_address=0x_1FFF_7590, flash_size_address=0x_1FFF_75E0), - DeviceFamily.L5: DeviceFamilyInfo(), - DeviceFamily.WBA: DeviceFamilyInfo(), - DeviceFamily.WB: DeviceFamilyInfo(), + DeviceFamily.L4: DeviceFamilyInfo("L4", uid_address=0x_1FFF_7590, flash_size_address=0x_1FFF_75E0, bootloader_id_address=0x_1FFF_6FFE), + DeviceFamily.L5: DeviceFamilyInfo("L5", ), + DeviceFamily.WBA: DeviceFamilyInfo("WBA", ), + DeviceFamily.WB: DeviceFamilyInfo("WB", ), # RM0453 - DeviceFamily.WL: DeviceFamilyInfo(uid_address=0x_1FFF_7590, flash_size_address=0x_1FFF_75E0), - DeviceFamily.U5: DeviceFamilyInfo(), - DeviceFamily.W: DeviceFamilyInfo(), + DeviceFamily.WL: DeviceFamilyInfo("WL", uid_address=0x_1FFF_7590, flash_size_address=0x_1FFF_75E0), + DeviceFamily.U5: DeviceFamilyInfo("U5", ), + DeviceFamily.W: DeviceFamilyInfo("W", ), # ST BlueNRG has DIE_ID register with PRODUCT, but no UID. # NRG BlueNRG-2 datasheet # Flash page size: 128 pages of 8 * 64 * 4 bytes - DeviceFamily.NRG1: DeviceFamilyInfo(flash_size_address=0x_4010_0014, flash_page_size=2048), - DeviceFamily.NRG2: DeviceFamilyInfo(flash_size_address=0x_4010_0014, flash_page_size=2048), - DeviceFamily.WIZ: DeviceFamilyInfo(), + DeviceFamily.NRG1: DeviceFamilyInfo("NRG1", flash_size_address=0x_4010_0014, flash_page_size=2048), + DeviceFamily.NRG2: DeviceFamilyInfo("NRG2", flash_size_address=0x_4010_0014, flash_page_size=2048), + DeviceFamily.WIZ: DeviceFamilyInfo("WIZ", ), } From 71f3de15485c4ebbc963f554eb114a9b8c921bd1 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Jan 2024 14:32:46 +0100 Subject: [PATCH 331/369] clean: Rename k to kB --- src/stm32loader/devices.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/stm32loader/devices.py b/src/stm32loader/devices.py index 8a83ae4..469c9c4 100644 --- a/src/stm32loader/devices.py +++ b/src/stm32loader/devices.py @@ -110,19 +110,19 @@ def __init__( } -k = 1024 +kB = 1024 class Flash: # RM0090 4 sectors of 16 Kbytes, 1 sector of 64 Kbytes, 7 sectors of 128 Kbytes - F2_F4_PAGE_SIZE = (16 * k, 16 * k, 16 * k, 16 * k, 64 * k, 128 * k, 0) + F2_F4_PAGE_SIZE = (16 * kB, 16 * kB, 16 * kB, 16 * kB, 64 * kB, 128 * kB, 0) # RM0090 4 sectors of 16 Kbytes, 1 sector of 64 Kbytes, 7 sectors of 128 Kbytes... per bank F4_DUAL_BANK_PAGE_SIZE = ( - 16 * k, 16 * k, 16 * k, 16 * k, 64 * k, 128 * k, 128 * k, 128 * k, 128 * k, - 16 * k, 16 * k, 16 * k, 16 * k, 64 * k, 128 * k, 0, + 16 * kB, 16 * kB, 16 * kB, 16 * kB, 64 * kB, 128 * kB, 128 * kB, 128 * kB, 128 * kB, + 16 * kB, 16 * kB, 16 * kB, 16 * kB, 64 * kB, 128 * kB, 0, ) - F7_PAGE_SIZE = (32 * k, 32 * k, 32 * k, 32 * k, 128 * k, 256 * k, 0) + F7_PAGE_SIZE = (32 * kB, 32 * kB, 32 * kB, 32 * kB, 128 * kB, 256 * kB, 0) def __init__(self, start=None, end=None, page_size=None, pages_per_sector=None): self.start = start From 2c717abb7071df1c17dc4ff6b17b7f4f7092be44 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Jan 2024 14:33:28 +0100 Subject: [PATCH 332/369] dev: Add device family, bootloader ID address --- src/stm32loader/devices.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/stm32loader/devices.py b/src/stm32loader/devices.py index 469c9c4..57f773b 100644 --- a/src/stm32loader/devices.py +++ b/src/stm32loader/devices.py @@ -140,8 +140,8 @@ def size(self): class DeviceInfo: - def __init__(self, device_family, device_name, pid, bid, variant=None, line=None, ram=None, flash=None, system=None, option=None, flags=None): - self.device_family = DeviceFamily[device_family] + def __init__(self, device_family, device_name, pid, bid, variant=None, line=None, ram=None, flash=None, system=None, option=None, bootloader_id_address=None, flags=DeviceFlag.NONE): + self.family = DEVICE_FAMILIES[DeviceFamily[device_family]] self.device_name = device_name self.product_id = pid self.bootloader_id = bid @@ -151,7 +151,8 @@ def __init__(self, device_family, device_name, pid, bid, variant=None, line=None self.flash = Flash(*(flash or [])) self.system_memory = system self.option_bytes = option - self.flags = flags + self.flags = flags | self.family.family_default_flags + self.bootloader_id_address = bootloader_id_address or self.family.bootloader_id_address @property def ram_size(self): From 31b8b459d850797b6b88cb7c199c6fe911d5ed27 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Jan 2024 14:33:36 +0100 Subject: [PATCH 333/369] dev: Add __repr__ --- src/stm32loader/devices.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/stm32loader/devices.py b/src/stm32loader/devices.py index 57f773b..309dd42 100644 --- a/src/stm32loader/devices.py +++ b/src/stm32loader/devices.py @@ -204,6 +204,9 @@ def __str__(self): name += f"-{self.product_line}" return name + def __repr__(self): + return f"DeviceInfo(device_name={self.device_name!r}, variant={self.product_line!r})" + @enum.unique class Flag(enum.IntEnum): From 34a872bda36aeabc2e0ee30dd11cdcdd1eb7ce41 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Jan 2024 14:33:59 +0100 Subject: [PATCH 334/369] dev: Add BootloaderSerialPeripheral --- src/stm32loader/devices.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/stm32loader/devices.py b/src/stm32loader/devices.py index 309dd42..5c2760c 100644 --- a/src/stm32loader/devices.py +++ b/src/stm32loader/devices.py @@ -209,9 +209,22 @@ def __repr__(self): @enum.unique -class Flag(enum.IntEnum): - OBL_LAUNCH = 1 - CLEAR_PEMPTY = 2 +class BootloaderSerialPeripherals(enum.Enum): + # AN2606 + USART = 1 + DUAL_USART = 2 + UART_CAN_DFU = 3 + USART_DFU = 4 + USART_I2C = 5 + I2C = 6 + I2C_CAN_DFU_I2C = 7 + I2C_SPI = 8 + USART_CAN_FDCAN_DFU_I2C_SPI = 9 + USART_DFU_FDCAN_SPI = 10 + USART_I2C_SPI = 11 + USART_SPI = 12 + USART_DFU_I2C_SPI = 13 + USART_DFU_I2C_I3C_FDCAN_SPI = 14 DEVICE_DETAILS = [ From 0567d0e37ec525987b34014603391811ac884850 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Jan 2024 14:35:13 +0100 Subject: [PATCH 335/369] dev: Extend device information --- src/stm32loader/devices.py | 154 ++++++++++++++++++++----------------- 1 file changed, 83 insertions(+), 71 deletions(-) diff --git a/src/stm32loader/devices.py b/src/stm32loader/devices.py index 5c2760c..1f3d88b 100644 --- a/src/stm32loader/devices.py +++ b/src/stm32loader/devices.py @@ -236,43 +236,44 @@ class BootloaderSerialPeripherals(enum.Enum): # FIXME flash? # Error in AN2606? Ram is mentioned as 0x_2000_2000 - 0x_2000_17FF DeviceInfo("C0", "STM32C031xx", 0x453, 0x52, ram=(0x_2000_2000, 0x_2000_2800), system=(0x_1FFF_0000, 0x_1FFF_1800), flash=None, option=None), - DeviceInfo("F0", "STM32F05xxx/030x8", 0x440, 0x21, ram=(0x_2000_0800, 0x_2000_2000), system=(0x_1FFF_EC00, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0801_0000, 1 * k, 4), option=(0x_1FFF_F800, 0x_1FFF_F80F)), - DeviceInfo("F0", "STM32F03xx4/6", 0x444, 0x10, ram=(0x_2000_0800, 0x_2000_1000), system=(0x_1FFF_EC00, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0800_8000, 1 * k, 4), option=(0x_1FFF_F800, 0x_1FFF_F80F)), + DeviceInfo("F0", "STM32F05xxx/030x8", 0x440, 0x21, ram=(0x_2000_0800, 0x_2000_2000), system=(0x_1FFF_EC00, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0801_0000, 1 * kB, 4), option=(0x_1FFF_F800, 0x_1FFF_F80F), bootloader_id_address=0x_1FFF_F7A6), + DeviceInfo("F0", "STM32F03xx4/6", 0x444, 0x10, ram=(0x_2000_0800, 0x_2000_1000), system=(0x_1FFF_EC00, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0800_8000, 1 * kB, 4), option=(0x_1FFF_F800, 0x_1FFF_F80F), bootloader_id_address=0x_1FFF_F7A6), # FIXME different flash size for both devices with PID=0x442 ? - DeviceInfo("F0", "STM32F030xC", 0x442, 0x52, ram=(0x_2000_1800, 0x_2000_8000), system=(0x_1FFF_D800, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0804_0000, 2 * k, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F), flags=Flag.OBL_LAUNCH), + DeviceInfo("F0", "STM32F030xC", 0x442, 0x52, ram=(0x_2000_1800, 0x_2000_8000), system=(0x_1FFF_D800, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0804_0000, 2 * kB, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F), bootloader_id_address=0x_1FFF_F796, flags=DeviceFlag.OBL_LAUNCH), # FIXME different flash size for both devices with PID=0x445 ? - DeviceInfo("F0", "STM32F04xxx", 0x445, 0xA1, None, ram=None, system=(0x_1FFF_C400, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0800_8000, 1 * k, 4), option=(0x_1FFF_F800, 0x_1FFF_F80F)), - DeviceInfo("F0", "STM32F070x6", 0x445, 0xA2, None, ram=None, system=(0x_1FFF_C400, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0800_8000, 1 * k, 4), option=(0x_1FFF_F800, 0x_1FFF_F80F)), + DeviceInfo("F0", "STM32F04xxx", 0x445, 0xA1, ram=None, system=(0x_1FFF_C400, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0800_8000, 1 * kB, 4), option=(0x_1FFF_F800, 0x_1FFF_F80F), bootloader_id_address=0x_1FFF_F6A6), + DeviceInfo("F0", "STM32F070x6", 0x445, 0xA2, ram=None, system=(0x_1FFF_C400, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0800_8000, 1 * kB, 4), option=(0x_1FFF_F800, 0x_1FFF_F80F), bootloader_id_address=0x_1FFF_F6A6), # FIXME different flash size for both devices with PID=0x448 ? - DeviceInfo("F0", "STM32F070xB", 0x448, 0xA2, None, ram=(0x_1FFF_C800, 0x_1FFF_F800), system=None, flash=(0x_0800_0000, 0x_0802_0000, 2 * k, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F)), - DeviceInfo("F0", "STM32F071xx/072xx", 0x448, 0xA1, ram=(0x_2000_1800, 0x_2000_4000), system=(0x_1FFF_C800, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0802_0000, 2 * k, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F)), - DeviceInfo("F0", "STM32F09xxx", 0x442, 0x50, None, ram=(0x_1FFF_D800, 0x_1FFF_F800), system=None, flash=(0x_0800_0000, 0x_0804_0000, 2 * k, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F), flags=Flag.OBL_LAUNCH), - DeviceInfo("F1", "STM32F10xxx", line="Low-density", pid=0x412, bid=None, ram=(0x_2000_0200, 0x_2000_2800), system=(0x_1FFF_F000, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0800_8000, 1 * k, 4), option=(0x_1FFF_F800, 0x_1FFF_F80F)), - DeviceInfo("F1", "STM32F10xxx", line="Medium-density", pid=0x410, bid=None, ram=(0x_2000_0200, 0x_2000_5000), system=(0x_1FFF_F000, 0x_1FFF_F800), flash = (0x_0800_0000, 0x_0802_0000, 1 * k, 4), option = (0x_1FFF_F800, 0x_1FFF_F80F)), - DeviceInfo("F1", "STM32F10xxx", line="High-density", pid=0x414, bid=None, ram=(0x_2000_0200, 0x_2001_0000), system=(0x_1FFF_F000, 0x_1FFF_F800), flash = (0x_0800_0000, 0x_0808_0000, 2 * k, 2), option = (0x_1FFF_F800, 0x_1FFF_F80F)), - DeviceInfo("F1", "STM32F10xxx", line="Medium-density value", pid=0x420, bid=0x10, ram=(0x_2000_0200, 0x_2000_2000), system=(0x_1FFF_F000, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0802_0000, 1 * k, 4), option=(0x_1FFF_F800, 0x_1FFF_F80F)), - DeviceInfo("F1", "STM32F10xxx", line="High-density value", pid=0x428, bid=0x10, ram=(0x_2000_0200, 0x_2000_8000), system=(0x_1FFF_F000, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0808_0000, 2 * k, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F)), - DeviceInfo("F1", "STM32F105xx/107xx", line="Connectivity", pid=0x418, bid=None, ram=(0x_2000_1000, 0x_2001_0000), system=(0x_1FFF_B000, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0804_0000, 2 * k, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F)), - DeviceInfo("F1", "STM32F10xxx", line="XL-density", pid=0x430, bid=0x21, ram=(0x_2000_0800, 0x_2001_8000), system=(0x_1FFF_E000, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0810_0000, 2 * k, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F)), + DeviceInfo("F0", "STM32F070xB", 0x448, 0xA3, ram=(0x_1FFF_C800, 0x_1FFF_F800), system=None, flash=(0x_0800_0000, 0x_0802_0000, 2 * kB, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F), bootloader_id_address=0x_1FFF_F6A6), + DeviceInfo("F0", "STM32F071xx/072xx", 0x448, 0xA1, ram=(0x_2000_1800, 0x_2000_4000), system=(0x_1FFF_C800, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0802_0000, 2 * kB, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F), bootloader_id_address=0x_1FFF_F6A6), + DeviceInfo("F0", "STM32F09xxx", 0x442, 0x50, ram=(0x_1FFF_D800, 0x_1FFF_F800), system=None, flash=(0x_0800_0000, 0x_0804_0000, 2 * kB, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F), bootloader_id_address=0x_1FFF_F796, flags=DeviceFlag.OBL_LAUNCH), + DeviceInfo("F1", "STM32F10xxx", line="Low-density", pid=0x412, bid=None, ram=(0x_2000_0200, 0x_2000_2800), system=(0x_1FFF_F000, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0800_8000, 1 * kB, 4), option=(0x_1FFF_F800, 0x_1FFF_F80F)), + DeviceInfo("F1", "STM32F10xxx", line="Medium-density", pid=0x410, bid=None, ram=(0x_2000_0200, 0x_2000_5000), system=(0x_1FFF_F000, 0x_1FFF_F800), flash = (0x_0800_0000, 0x_0802_0000, 1 * kB, 4), option = (0x_1FFF_F800, 0x_1FFF_F80F)), + DeviceInfo("F1", "STM32F10xxx", line="High-density", pid=0x414, bid=None, ram=(0x_2000_0200, 0x_2001_0000), system=(0x_1FFF_F000, 0x_1FFF_F800), flash = (0x_0800_0000, 0x_0808_0000, 2 * kB, 2), option = (0x_1FFF_F800, 0x_1FFF_F80F)), + DeviceInfo("F1", "STM32F10xxx", line="Medium-density value", pid=0x420, bid=0x10, ram=(0x_2000_0200, 0x_2000_2000), system=(0x_1FFF_F000, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0802_0000, 1 * kB, 4), option=(0x_1FFF_F800, 0x_1FFF_F80F), bootloader_id_address=0x_1FFF_F7D6), + DeviceInfo("F1", "STM32F10xxx", line="High-density value", pid=0x428, bid=0x10, ram=(0x_2000_0200, 0x_2000_8000), system=(0x_1FFF_F000, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0808_0000, 2 * kB, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F), bootloader_id_address=0x_1FFF_F7D6), + DeviceInfo("F1", "STM32F105xx/107xx", line="Connectivity", pid=0x418, bid=None, ram=(0x_2000_1000, 0x_2001_0000), system=(0x_1FFF_B000, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0804_0000, 2 * kB, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F)), + DeviceInfo("F1", "STM32F10xxx", line="XL-density", pid=0x430, bid=0x21, ram=(0x_2000_0800, 0x_2001_8000), system=(0x_1FFF_E000, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0810_0000, 2 * kB, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F), bootloader_id_address=0x_1FFF_F7D6), # FIXME different flash size for both devices with PID=0x411 ? DeviceInfo("F2", "STM32F2xxxx", 0x411, 0x20, ram=(0x_2000_2000, 0x_2002_0000), system=(0x_1FFF_0000, 0x_1FFF_7800), flash=(0x_0800_0000, 0x_0810_0000, Flash.F2_F4_PAGE_SIZE), option=(0x_1FFF_C000, 0x_1FFF_C00F)), DeviceInfo("F2", "STM32F2xxxx", 0x411, 0x33, ram=(0x_2000_2000, 0x_2002_0000), system=(0x_1FFF_0000, 0x_1FFF_7800), flash=(0x_0800_0000, 0x_0810_0000, Flash.F2_F4_PAGE_SIZE), option=(0x_1FFF_C000, 0x_1FFF_C00F)), # FIXME different flash size for both devices with PID=0x432 ? - DeviceInfo("F3", "STM32F373xx", 0x432, 0x41, ram=(0x_2000_1400, 0x_2000_8000), system=(0x_1FFF_D800, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0804_0000, 2 * k, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F)), - DeviceInfo("F3", "STM32F378xx", 0x432, 0x50, ram=(0x_2000_1000, 0x_2000_8000), system=(0x_1FFF_D800, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0804_0000, 2 * k, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F)), + DeviceInfo("F3", "STM32F373xx", 0x432, 0x41, ram=(0x_2000_1400, 0x_2000_8000), system=(0x_1FFF_D800, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0804_0000, 2 * kB, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F), bootloader_id_address=0x_1FFF_F7A6), + DeviceInfo("F3", "STM32F378xx", 0x432, 0x50, ram=(0x_2000_1000, 0x_2000_8000), system=(0x_1FFF_D800, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0804_0000, 2 * kB, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F), bootloader_id_address=0x_1FFF_F7A6), # FIXME different flash size for both devices with PID=0x422 ? - DeviceInfo("F3", "STM32F302xB(C)/303xB(C)", 0x422, 0x41, ram=(0x_2000_1400, 0x_2000_A000), system=(0x_1FFF_D800, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0804_0000, 2 * k, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F)), - DeviceInfo("F3", "STM32F358xx", 0x422, 0x50, ram=(0x_2000_1400, 0x_2000_A000), system=(0x_1FFF_D800, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0804_0000, 2 * k, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F)), + DeviceInfo("F3", "STM32F302xB(C)/303xB(C)", 0x422, 0x41, ram=(0x_2000_1400, 0x_2000_A000), system=(0x_1FFF_D800, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0804_0000, 2 * kB, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F)), + DeviceInfo("F3", "STM32F358xx", 0x422, 0x50, ram=(0x_2000_1400, 0x_2000_A000), system=(0x_1FFF_D800, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0804_0000, 2 * kB, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F)), # FIXME different flash size for both devices with PID=0x439 ? - DeviceInfo("F3", "STM32F301xx/302x4(6/8)", 0x439, 0x40, ram=(0x_2000_1800, 0x_2000_4000), system=(0x_1FFF_D800, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0801_0000, 2 * k, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F)), - DeviceInfo("F3", "STM32F318xx", 0x439, 0x50, ram=(0x_2000_1800, 0x_2000_4000), system=(0x_1FFF_D800, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0801_0000, 2 * k, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F)), - DeviceInfo("F3", "STM32F303x4(6/8)/334xx/328xx", 0x438, 0x50, ram=(0x_2000_1800, 0x_2000_3000), system=(0x_1FFF_D800, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0801_0000, 2 * k, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F)), + DeviceInfo("F3", "STM32F301xx/302x4(6/8)", 0x439, 0x40, ram=(0x_2000_1800, 0x_2000_4000), system=(0x_1FFF_D800, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0801_0000, 2 * kB, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F)), + DeviceInfo("F3", "STM32F318xx", 0x439, 0x50, ram=(0x_2000_1800, 0x_2000_4000), system=(0x_1FFF_D800, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0801_0000, 2 * kB, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F)), + DeviceInfo("F3", "STM32F303x4(6/8)/334xx/328xx", 0x438, 0x50, ram=(0x_2000_1800, 0x_2000_3000), system=(0x_1FFF_D800, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0801_0000, 2 * kB, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F)), # FIXME different flash size for both devices with PID=0x446 ? - DeviceInfo("F3", "STM32F302xD(E)/303xD(E)", 0x446, 0x40, ram=(0x_2000_1800, 0x_2001_0000), system=(0x_1FFF_D800, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0808_0000, 2 * k, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F)), - DeviceInfo("F3", "STM32F398xx", 0x446, 0x50, ram=(0x_2000_1800, 0x_2001_0000), system=(0x_1FFF_D800, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0808_0000, 2 * k, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F)), + DeviceInfo("F3", "STM32F302xD(E)/303xD(E)", 0x446, 0x40, ram=(0x_2000_1800, 0x_2001_0000), system=(0x_1FFF_D800, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0808_0000, 2 * kB, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F)), + DeviceInfo("F3", "STM32F398xx", 0x446, 0x50, ram=(0x_2000_1800, 0x_2001_0000), system=(0x_1FFF_D800, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0808_0000, 2 * kB, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F)), + # FIXME different flash size for both devices with PID=0x413 ? - DeviceInfo("F4", "STM32F40xxx/41xxx", 0x413, 0x31, ram=(0x_2000_2000, 0x_2002_0000), system=(0x_1FFF_0000, 0x_1FFF_7800), flash=(0x_0800_0000, 0x_0810_0000, Flash.F2_F4_PAGE_SIZE), option=(0x_1FFF_C000, 0x_1FFF_C00F)), - DeviceInfo("F4", "STM32F40xxx/41xxx", 0x413, 0x91, ram=(0x_2000_3000, 0x_2002_0000), system=(0x_1FFF_0000, 0x_1FFF_7800), flash=(0x_0800_0000, 0x_0810_0000, Flash.F2_F4_PAGE_SIZE), option=(0x_1FFF_C000, 0x_1FFF_C00F)), + DeviceInfo("F4", "STM32F40xxx/41xxx", 0x413, 0x31, ram=(0x_2000_2000, 0x_2002_0000), system=(0x_1FFF_0000, 0x_1FFF_7800), flash=(0x_0800_0000, 0x_0810_0000, Flash.F2_F4_PAGE_SIZE), option=(0x_1FFF_C000, 0x_1FFF_C00F), bootloader_id_address=0x_1FFF_77DE), + DeviceInfo("F4", "STM32F40xxx/41xxx", 0x413, 0x91, ram=(0x_2000_3000, 0x_2002_0000), system=(0x_1FFF_0000, 0x_1FFF_7800), flash=(0x_0800_0000, 0x_0810_0000, Flash.F2_F4_PAGE_SIZE), option=(0x_1FFF_C000, 0x_1FFF_C00F), bootloader_id_address=0x_1FFF_77DE), # FIXME different flash size for both devices with PID=0x419 ? DeviceInfo("F4", "STM32F42xxx/43xxx", 0x419, 0x70, ram=(0x_2000_3000, 0x_2003_0000), system=(0x_1FFF_0000, 0x_1FFF_7800), flash=(0x_0800_0000, 0x_0820_0000, Flash.F4_DUAL_BANK_PAGE_SIZE), option=(0x_1FFE_C000, 0x_1FFF_C00F)), DeviceInfo("F4", "STM32F42xxx/43xxx", 0x419, 0x91, ram=(0x_2000_3000, 0x_2003_0000), system=(0x_1FFF_0000, 0x_1FFF_7800), flash=(0x_0800_0000, 0x_0820_0000, Flash.F4_DUAL_BANK_PAGE_SIZE), option=(0x_1FFE_C000, 0x_1FFF_C00F)), @@ -290,76 +291,87 @@ class BootloaderSerialPeripherals(enum.Enum): DeviceInfo("F7", "STM32F74xxx/75xxx", 0x449, 0x70, ram=(0x_2000_4000, 0x_2005_0000), system=(0x_1FF0_0000, 0x_1FF0_EDC0), flash=(0x_0800_0000, 0x_0810_0000, Flash.F7_PAGE_SIZE), option=(0x_1FFF_0000, 0x_1FFF_001F)), DeviceInfo("F7", "STM32F74xxx/75xxx", 0x449, 0x90, ram=(0x_2000_4000, 0x_2005_0000), system=(0x_1FF0_0000, 0x_1FF0_EDC0), flash=(0x_0800_0000, 0x_0810_0000, Flash.F7_PAGE_SIZE), option=(0x_1FFF_0000, 0x_1FFF_001F)), DeviceInfo("F7", "STM32F76xxx/77xxx", 0x451, 0x93, ram=(0x_2000_4000, 0x_2008_0000), system=(0x_1FF0_0000, 0x_1FF0_EDC0), flash=(0x_0800_0000, 0x_0820_0000, Flash.F7_PAGE_SIZE), option=(0x_1FFF_0000, 0x_1FFF_001F)), - DeviceInfo("G0", "STM32G03xxx/04xxx", 0x466, 0x52, ram=(0x_2000_1000, 0x_2000_2000), system=(0x_1FFF_0000, 0x_1FFF_2000), flash=(0x_0800_0000, 0x_0801_0000, 2 * k), option=(0x_1FFF_7800, 0x_1FFF_787F)), - DeviceInfo("G0", "STM32G07xxx/08xxx", 0x460, 0xB3, ram=(0x_2000_2700, 0x_2000_9000), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0802_0000, 2 * k), option=(0x_1FFF_7800, 0x_1FFF_787F)), + DeviceInfo("G0", "STM32G03xxx/04xxx", 0x466, 0x53, ram=(0x_2000_1000, 0x_2000_2000), system=(0x_1FFF_0000, 0x_1FFF_2000), flash=(0x_0800_0000, 0x_0801_0000, 2 * kB), option=(0x_1FFF_7800, 0x_1FFF_787F), bootloader_id_address=0x_1FFF_1FFE), + DeviceInfo("G0", "STM32G07xxx/08xxx", 0x460, 0xB3, ram=(0x_2000_2700, 0x_2000_9000), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0802_0000, 2 * kB), option=(0x_1FFF_7800, 0x_1FFF_787F), bootloader_id_address=0x_1FFF_6FFE), # FIXME different flash size for both devices with PID=0x467 ? # FIXME dual banks for system - DeviceInfo("G0", "STM32G0B0xx", 0x467, 0xD0, ram=(0x_2000_4000, 0x_2002_0000), system=((0x_1FFF_0000, 0x_1FFF_7000), (0x_1FFF_8000, 0x_1FFF_F000)), flash=(0x_0800_0000, 0x_0808_0000, 2 * k), option=(0x_1FFF_7800, 0x_1FFF_787F)), + DeviceInfo("G0", "STM32G0B0xx", 0x467, 0xD0, ram=(0x_2000_4000, 0x_2002_0000), system=((0x_1FFF_0000, 0x_1FFF_7000), (0x_1FFF_8000, 0x_1FFF_F000)), flash=(0x_0800_0000, 0x_0808_0000, 2 * kB), option=(0x_1FFF_7800, 0x_1FFF_787F), bootloader_id_address=0x_1FFF_9FFE), # FIXME dual banks for system - DeviceInfo("G0", "STM32G0B1xx/0C1xx", 0x467, 0x92, ram=(0x_2000_4000, 0x_2002_0000), system=((0x_1FFF_0000, 0x_1FFF_6000), (0x_1FFF_8000, 0x_1FFF_F000)), flash=(0x_0800_0000, 0x_0808_0000, 2 * k), option=(0x_1FFF_7800, 0x_1FFF_787F)), + DeviceInfo("G0", "STM32G0B1xx/0C1xx", 0x467, 0x92, ram=(0x_2000_4000, 0x_2002_0000), system=((0x_1FFF_0000, 0x_1FFF_6000), (0x_1FFF_8000, 0x_1FFF_F000)), flash=(0x_0800_0000, 0x_0808_0000, 2 * kB), option=(0x_1FFF_7800, 0x_1FFF_787F), bootloader_id_address=0x_1FFF_9FFE), # FIXME: STM32flash has 0x_2000_4800 as upper system range. - DeviceInfo("G0", "STM32G05xxx/061xx", 0x456, 0x51, ram=(0x_2000_1000, 0x_2000_2000), system=(0x_1FFF_0000, 0x_1FFF_1000), flash=(0x_0800_0000, 0x_0801_0000, 2 * k), option=(0x_1FFF_7800, 0x_1FFF_787F)), - DeviceInfo("G4", "STM32G431xx/441xx", 0x468, 0xD4, ram=(0x_2000_4000, 0x_2000_5800), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0802_0000, 2 * k), option=(0x_1FFF_7800, 0x_1FFF_782F)), - DeviceInfo("G4", "STM32G47xxx/48xxx", 0x469, 0xD5, ram=(0x_2000_4000, 0x_2001_8000), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0808_0000, 2 * k), option=(0x_1FFF_7800, 0x_1FFF_782F)), - DeviceInfo("G4", "STM32G491xx/A1xx", 0x479, 0xD2, ram=(0x_2000_4000, 0x_2001_C000), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0808_0000, 2 * k), option=(0x_1FFF_7800, 0x_1FFF_782F)), - + DeviceInfo("G0", "STM32G05xxx/061xx", 0x456, 0x51, ram=(0x_2000_1000, 0x_2000_2000), system=(0x_1FFF_0000, 0x_1FFF_1000), flash=(0x_0800_0000, 0x_0801_0000, 2 * kB), option=(0x_1FFF_7800, 0x_1FFF_787F), bootloader_id_address=0x_1FFF_1FFE), + DeviceInfo("G4", "STM32G431xx/441xx", 0x468, 0xD4, ram=(0x_2000_4000, 0x_2000_5800), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0802_0000, 2 * kB), option=(0x_1FFF_7800, 0x_1FFF_782F)), + DeviceInfo("G4", "STM32G47xxx/48xxx", 0x469, 0xD5, ram=(0x_2000_4000, 0x_2001_8000), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0808_0000, 2 * kB), option=(0x_1FFF_7800, 0x_1FFF_782F)), + DeviceInfo("G4", "STM32G491xx/A1xx", 0x479, 0xD2, ram=(0x_2000_4000, 0x_2001_C000), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0808_0000, 2 * kB), option=(0x_1FFF_7800, 0x_1FFF_782F)), # FIXME Flash and option bytes? - DeviceInfo("H5", "STM32H503xx", 0x474, 0xE1, ram=(0x_2000_4000, 0x_2000_8000), system=(0x_0BF8_7000, 0x_0BF9_0000)), + DeviceInfo("H5", "STM32H503xx", 0x474, 0xE1, ram=(0x_2000_4000, 0x_2000_8000), system=(0x_0BF8_7000, 0x_0BF9_0000), bootloader_id_address=0x_0BF8_FFFE), # FIXME Flash and option bytes? - DeviceInfo("H5", "STM32H563xx/573xx", 0x484, 0xE3, ram=(0x_2000_0000, 0x_200A_0000), system=(0x_0BF9_7000, 0x_0BFA_0000)), - DeviceInfo("H7", "STM32H72xxx/73xxx", 0x483, 0x93, ram=((0x_2000_4100, 0x_2002_0000), (0x_2400_4000, 0x_2405_0000)), system=(0x_1FF0_0000, 0x_1FF1_E800), flash=(0x_0800_0000, 0x_0810_0000, 128 * k), option=None), - DeviceInfo("H7", "STM32H74xxx/75xxx", 0x450, 0x91, ram=((0x_2000_4100, 0x_2002_0000), (0x_2400_5000, 0x_2408_0000)), system=(0x_1FF0_0000, 0x_1FF1_E800), flash=(0x_0800_0000, 0x_0820_0000, 128 * k), option=None), - DeviceInfo("H7", "STM32H7A3xx/B3xx", 0x480, 0x92, ram=((0x_2000_4100, 0x_2002_0000), (0x_2403_4000, 0x_2408_0000)), system=(0x_1FF0_0000, 0x_1FF1_4000), flash=(0x_0800_0000, 0x_0810_0000, 8 * k), option=None), - DeviceInfo("L0", "STM32L01xxx/02xxx", 0x457, 0xC3, ram=None, system=(0x_1FF0_0000, 0x_1FF0_1000), flash=(0x_0800_0000, 0x_0800_4000, 128, 32), option=(0x_1FF8_0000, 0x_1FF8_001F)), - DeviceInfo("L0", "STM32L031xx/041xx", 0x425, 0xC0, ram=(0x_2000_1000, 0x_2000_2000), system=(0x_1FF0_0000, 0x_1FF0_1000), flash=(0x_0800_0000, 0x_0800_8000, 128, 32), option=(0x_1FF8_0000, 0x_1FF8_001F)), - DeviceInfo("L0", "STM32L05xxx/06xxx", 0x417, 0xC0, ram=(0x_2000_1000, 0x_2000_2000), system=(0x_1FF0_0000, 0x_1FF0_1000), flash=(0x_0800_0000, 0x_0801_0000, 128, 32), option=(0x_1FF8_0000, 0x_1FF8_001F)), + DeviceInfo("H5", "STM32H563xx/573xx", 0x484, 0xE3, ram=(0x_2000_0000, 0x_200A_0000), system=(0x_0BF9_7000, 0x_0BFA_0000), bootloader_id_address=0x_0BF9_FAFE), + DeviceInfo("H7", "STM32H72xxx/73xxx", 0x483, 0x93, ram=((0x_2000_4100, 0x_2002_0000), (0x_2400_4000, 0x_2405_0000)), system=(0x_1FF0_0000, 0x_1FF1_E800), flash=(0x_0800_0000, 0x_0810_0000, 128 * kB), option=None, bootloader_id_address=0x_1FF1_E7FE), + DeviceInfo("H7", "STM32H74xxx/75xxx", 0x450, 0x91, ram=((0x_2000_4100, 0x_2002_0000), (0x_2400_5000, 0x_2408_0000)), system=(0x_1FF0_0000, 0x_1FF1_E800), flash=(0x_0800_0000, 0x_0820_0000, 128 * kB), option=None, bootloader_id_address=0x_1FF1_E7FE), + DeviceInfo("H7", "STM32H7A3xx/B3xx", 0x480, 0x92, ram=((0x_2000_4100, 0x_2002_0000), (0x_2403_4000, 0x_2408_0000)), system=(0x_1FF0_0000, 0x_1FF1_4000), flash=(0x_0800_0000, 0x_0810_0000, 8 * kB), option=None, bootloader_id_address=0x_1FF1_3FFE), + + DeviceInfo("L0", "STM32L01xxx/02xxx", 0x457, 0xC3, ram=None, system=(0x_1FF0_0000, 0x_1FF0_1000), flash=(0x_0800_0000, 0x_0800_4000, 128, 32), option=(0x_1FF8_0000, 0x_1FF8_001F), bootloader_id_address=0x_1FF0_0FFE), + DeviceInfo("L0", "STM32L031xx/041xx", 0x425, 0xC0, ram=(0x_2000_1000, 0x_2000_2000), system=(0x_1FF0_0000, 0x_1FF0_1000), flash=(0x_0800_0000, 0x_0800_8000, 128, 32), option=(0x_1FF8_0000, 0x_1FF8_001F), bootloader_id_address=0x_1FF0_0FFE), + DeviceInfo("L0", "STM32L05xxx/06xxx", 0x417, 0xC0, ram=(0x_2000_1000, 0x_2000_2000), system=(0x_1FF0_0000, 0x_1FF0_1000), flash=(0x_0800_0000, 0x_0801_0000, 128, 32), option=(0x_1FF8_0000, 0x_1FF8_001F), bootloader_id_address=0x_1FF0_0FFE), # FIXME different flash size for both devices with PID=0x447 ? # Note: STM32flash has 0x_2000_2000 as lower system range. - DeviceInfo("L0", "STM32L07xxx/08xxx", 0x447, 0x41, ram=(0x_2000_1000, 0x_2000_5000), system=(0x_1FF0_0000, 0x_1FF0_2000), flash=(0x_0800_0000, 0x_0803_0000, 128, 32), option=(0x_1FF8_0000, 0x_1FF8_001F)), - DeviceInfo("L0", "STM32L07xxx/08xxx", 0x447, 0xB2, ram=(0x_2000_1400, 0x_2000_5000), system=(0x_1FF0_0000, 0x_1FF0_2000), flash=(0x_0800_0000, 0x_0803_0000, 128, 32), option=(0x_1FF8_0000, 0x_1FF8_001F)), - DeviceInfo("L1", "STM32L1xxx6(8/B)", line="Medium-density ULP", pid=0x416, bid=0x20, ram=(0x_2000_0800, 0x_2000_4000), system=(0x_1FF0_0000, 0x_1FF0_2000), flash=(0x_0800_0000, 0x_0802_0000, 256, 16), option=(0x_1FF8_0000, 0x_1FF8_001F)), - DeviceInfo("L1", "STM32L1xxx6(8/B)A", 0x429, 0x20, ram=(0x_2000_1000, 0x_2000_8000), system=(0x_1FF0_0000, 0x_1FF0_2000), flash=(0x_0800_0000, 0x_0802_0000, 256, 16), option=(0x_1FF8_0000, 0x_1FF8_001F)), - DeviceInfo("L1", "STM32L1xxxC", 0x427, 0x40, ram=(0x_2000_1000, 0x_2000_8000), system=(0x_1FF0_0000, 0x_1FF0_2000), flash=(0x_0800_0000, 0x_0804_0000, 256, 16), option=(0x_1FF8_0000, 0x_1FF8_001F)), - DeviceInfo("L1", "STM32L1xxxD", 0x436, 0x45, ram=(0x_2000_1000, 0x_2000_C000), system=(0x_1FF0_0000, 0x_1FF0_2000), flash=(0x_0800_0000, 0x_0806_0000, 256, 16), option=(0x_1FF8_0000, 0x_1FF8_001F)), - DeviceInfo("L1", "STM32L1xxxE", 0x437, 0x40, ram=(0x_2000_1000, 0x_2001_4000), system=(0x_1FF0_0000, 0x_1FF0_2000), flash=(0x_0800_0000, 0x_0808_0000, 256, 16), option=(0x_1FF8_0000, 0x_1FF8_001F)), + DeviceInfo("L0", "STM32L07xxx/08xxx", 0x447, 0x41, ram=(0x_2000_1000, 0x_2000_5000), system=(0x_1FF0_0000, 0x_1FF0_2000), flash=(0x_0800_0000, 0x_0803_0000, 128, 32), option=(0x_1FF8_0000, 0x_1FF8_001F), bootloader_id_address=0x_1FF0_1FFE), + DeviceInfo("L0", "STM32L07xxx/08xxx", 0x447, 0xB2, ram=(0x_2000_1400, 0x_2000_5000), system=(0x_1FF0_0000, 0x_1FF0_2000), flash=(0x_0800_0000, 0x_0803_0000, 128, 32), option=(0x_1FF8_0000, 0x_1FF8_001F), bootloader_id_address=0x_1FF0_1FFE), + + DeviceInfo("L1", "STM32L1xxx6(8/B)", line="Medium-density ULP", pid=0x416, bid=0x20, ram=(0x_2000_0800, 0x_2000_4000), system=(0x_1FF0_0000, 0x_1FF0_2000), flash=(0x_0800_0000, 0x_0802_0000, 256, 16), option=(0x_1FF8_0000, 0x_1FF8_001F), bootloader_id_address=0x_1FF0_0FFE), + DeviceInfo("L1", "STM32L1xxx6(8/B)A", 0x429, 0x20, ram=(0x_2000_1000, 0x_2000_8000), system=(0x_1FF0_0000, 0x_1FF0_2000), flash=(0x_0800_0000, 0x_0802_0000, 256, 16), option=(0x_1FF8_0000, 0x_1FF8_001F), bootloader_id_address=0x_1FF0_0FFE), + DeviceInfo("L1", "STM32L1xxxC", 0x427, 0x40, ram=(0x_2000_1000, 0x_2000_8000), system=(0x_1FF0_0000, 0x_1FF0_2000), flash=(0x_0800_0000, 0x_0804_0000, 256, 16), option=(0x_1FF8_0000, 0x_1FF8_001F), bootloader_id_address=0x_1FF0_1FFE), + DeviceInfo("L1", "STM32L1xxxD", 0x436, 0x45, ram=(0x_2000_1000, 0x_2000_C000), system=(0x_1FF0_0000, 0x_1FF0_2000), flash=(0x_0800_0000, 0x_0806_0000, 256, 16), option=(0x_1FF8_0000, 0x_1FF8_001F), bootloader_id_address=0x_1FF0_1FFE), + DeviceInfo("L1", "STM32L1xxxE", 0x437, 0x40, ram=(0x_2000_1000, 0x_2001_4000), system=(0x_1FF0_0000, 0x_1FF0_2000), flash=(0x_0800_0000, 0x_0808_0000, 256, 16), option=(0x_1FF8_0000, 0x_1FF8_001F), bootloader_id_address=0x_1FF0_1FFE), # Note: Stm32flash has 0x_2000_3100 as ram start. - DeviceInfo("L4", "STM32L412xx/422xx", line="Low-density", pid=0x464, bid=0xD1, ram=(0x_2000_2100, 0x_2000_8000), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0802_0000, 2 * k), option=(0x_1FFF_7800, 0x_1FFF_780F)), - DeviceInfo("L4", "STM32L43xxx/44xxx", 0x435, 0x91, ram=(0x_2000_3100, 0x_2000_C000), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0804_0000, 2 * k), option=(0x_1FFF_7800, 0x_1FFF_780F)), - DeviceInfo("L4", "STM32L45xxx/46xxx", 0x462, 0x92, ram=(0x_2000_3100, 0x_2002_0000), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0808_0000, 2 * k), option=(0x_1FFF_7800, 0x_1FFF_780F), flags=Flag.CLEAR_PEMPTY), + DeviceInfo("L4", "STM32L412xx/422xx", line="Low-density", pid=0x464, bid=0xD1, ram=(0x_2000_2100, 0x_2000_8000), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0802_0000, 2 * kB), option=(0x_1FFF_7800, 0x_1FFF_780F)), + DeviceInfo("L4", "STM32L43xxx/44xxx", 0x435, 0x91, ram=(0x_2000_3100, 0x_2000_C000), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0804_0000, 2 * kB), option=(0x_1FFF_7800, 0x_1FFF_780F)), + DeviceInfo("L4", "STM32L45xxx/46xxx", 0x462, 0x92, ram=(0x_2000_3100, 0x_2002_0000), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0808_0000, 2 * kB), option=(0x_1FFF_7800, 0x_1FFF_780F), flags=DeviceFlag.CLEAR_PEMPTY), # FIXME different flash size for both devices with PID=0x415 ? - DeviceInfo("L4", "STM32L47xxx/48xxx", 0x415, 0xA3, ram=(0x_2000_3000, 0x_2001_8000), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0810_0000, 2 * k), option=(0x_1FFF_7800, 0x_1FFF_F80F)), - DeviceInfo("L4", "STM32L47xxx/48xxx", 0x415, 0x92, ram=(0x_2000_3100, 0x_2001_8000), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0810_0000, 2 * k), option=(0x_1FFF_7800, 0x_1FFF_F80F)), - DeviceInfo("L4", "STM32L496xx/4A6xx", 0x461, 0x93, ram=(0x_2000_3100, 0x_2004_0000), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0810_0000, 2 * k), option=(0x_1FFF_7800, 0x_1FFF_F80F)), - DeviceInfo("L4", "STM32L4Rxx/4Sxx", 0x470, 0x95, ram=(0x_2000_3200, 0x_200A_0000), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0810_0000, 2 * k), option=(0x_1FFF_7800, 0x_1FFF_F80F)), - DeviceInfo("L4", "STM32L4P5xx/Q5xx", 0x471, 0x90, ram=(0x_2000_4000, 0x_2005_0000), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0810_0000, 4 * k), option=(0x_1FF0_0000, 0x_1FF0_000F)), - DeviceInfo("L5", "STM32L552xx/562xx", 0x472, 0x92, ram=(0x_2000_4000, 0x_2004_0000), system=(0x_0BF9_0000, 0x_0BF9_8000), flash=(0x_0800_0000, 0x_0808_0000, 2 * k), option=None), + DeviceInfo("L4", "STM32L47xxx/48xxx", 0x415, 0xA3, ram=(0x_2000_3000, 0x_2001_8000), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0810_0000, 2 * kB), option=(0x_1FFF_7800, 0x_1FFF_F80F)), + DeviceInfo("L4", "STM32L47xxx/48xxx", 0x415, 0x92, ram=(0x_2000_3100, 0x_2001_8000), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0810_0000, 2 * kB), option=(0x_1FFF_7800, 0x_1FFF_F80F)), + DeviceInfo("L4", "STM32L496xx/4A6xx", 0x461, 0x93, ram=(0x_2000_3100, 0x_2004_0000), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0810_0000, 2 * kB), option=(0x_1FFF_7800, 0x_1FFF_F80F)), + DeviceInfo("L4", "STM32L4Rxx/4Sxx", 0x470, 0x95, ram=(0x_2000_3200, 0x_200A_0000), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0810_0000, 2 * kB), option=(0x_1FFF_7800, 0x_1FFF_F80F)), + DeviceInfo("L4", "STM32L4P5xx/Q5xx", 0x471, 0x90, ram=(0x_2000_4000, 0x_2005_0000), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0810_0000, 4 * kB), option=(0x_1FF0_0000, 0x_1FF0_000F)), + DeviceInfo("L5", "STM32L552xx/562xx", 0x472, 0x92, ram=(0x_2000_4000, 0x_2004_0000), system=(0x_0BF9_0000, 0x_0BF9_8000), flash=(0x_0800_0000, 0x_0808_0000, 2 * kB), option=None, bootloader_id_address=0x_0BF9_7FFE), # FIXME flash config ? - DeviceInfo("WBA", "STM32WBA52xx", 0x492, 0xB0, ram=(0x_2000_0000, 0x_2000_2000), system=(0x_0BF8_8000, 0x_0BF9_0000)), - DeviceInfo("WB", "STM32WB10xx/15xx", 0x494, 0xB1, ram=(0x_2000_5000, 0x_2004_0000), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0805_0000, 2 * k), option=(0x_1FFF_7800, 0x_1FFF_787F)), - DeviceInfo("WB", "STM32WB30xx/35xx/50xx/55xx", 0x495, 0xD5, ram=(0x_2000_4000, 0x_2000_C000), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0810_0000, 4 * k), option=(0x_1FFF_8000, 0x_1FFF_807F)), - DeviceInfo("WL", "STM32WLE5xx/WL55xx", 0x497, 0xC4, ram=(0x_2000_2000, 0x_2001_0000), system=(0x_1FFF_0000, 0x_1FFF_4000), flash=(0x_0800_0000, 0x_0804_0000, 2 * k), option=(0x_1FFF_7800, 0x_1FFF_8000)), + DeviceInfo("WBA", "STM32WBA52xx", 0x492, 0xB0, ram=(0x_2000_0000, 0x_2000_2000), system=(0x_0BF8_8000, 0x_0BF9_0000), bootloader_id_address=0x_0BF8_FEFE), + DeviceInfo("WB", "STM32WB10xx/15xx", 0x494, 0xB1, ram=(0x_2000_5000, 0x_2004_0000), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0805_0000, 2 * kB), option=(0x_1FFF_7800, 0x_1FFF_787F), bootloader_id_address=0x_1FFF_6FFE), + DeviceInfo("WB", "STM32WB30xx/35xx/50xx/55xx", 0x495, 0xD5, ram=(0x_2000_4000, 0x_2000_C000), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0810_0000, 4 * kB), option=(0x_1FFF_8000, 0x_1FFF_807F), bootloader_id_address=0x_1FFF_6FFE), + DeviceInfo("WL", "STM32WLE5xx/WL55xx", 0x497, 0xC4, ram=(0x_2000_2000, 0x_2001_0000), system=(0x_1FFF_0000, 0x_1FFF_4000), flash=(0x_0800_0000, 0x_0804_0000, 2 * kB), option=(0x_1FFF_7800, 0x_1FFF_8000), bootloader_id_address=0x_1FFF_3EFE), # FIXME flash config? - DeviceInfo("U5", "STM32U535xx/545xx", 0x455, 0x91, ram=(0x_2000_4000, 0x_2024_0000), system=(0x_0BF9_0000, 0x_0BFA_0000)), - DeviceInfo("U5", "STM32U575xx/585xx", 0x482, 0x92, ram=(0x_2000_4000, 0x_200C_0000), system=(0x_0BF9_0000, 0x_0BFA_0000), flash=(0x_0800_0000, 0x_0820_0000, 8 * k), option=None), + DeviceInfo("U5", "STM32U535xx/545xx", 0x455, 0x91, ram=(0x_2000_4000, 0x_2024_0000), system=(0x_0BF9_0000, 0x_0BFA_0000), bootloader_id_address=0x_0BF9_9EFE), + # + DeviceInfo("U5", "STM32U575xx/585xx", 0x482, 0x92, ram=(0x_2000_4000, 0x_200C_0000), system=(0x_0BF9_0000, 0x_0BFA_0000), flash=(0x_0800_0000, 0x_0820_0000, 8 * kB), option=None, bootloader_id_address=0x_0BF9_9EFE), # FIXME flash config? - DeviceInfo("U5", "STM32U595xx/599xx/5A9xx", 0x481, 0x92, ram=(0x_2000_4000, 0x_2027_0000), system=(0x_0BF9_0000, 0x_0BFA_0000)), + DeviceInfo("U5", "STM32U595xx/599xx/5A9xx", 0x481, 0x92, ram=(0x_2000_4000, 0x_2027_0000), system=(0x_0BF9_0000, 0x_0BFA_0000), bootloader_id_address=0x_0BF9_9EFE), # Not yet in AN2606. Bootloader IDs are unknown. - # F1 is assumed here. - DeviceInfo("F1", "STM32F103x8/B", line="Medium-density performance", pid=0x641, bid=None, ram=(0x_2000_0200, 0x_2000_5000), system=(0x_1FFF_F000, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0802_0000, 1 * k, 4), option=(0x_1FFF_F800, 0x_1FFF_F80F)), + # Assumption: 'Medium-density performance' refers to F1 series, and F103. + # FIXME No bootloader ID address? + DeviceInfo("F1", "STM32F103x8/B", line="Medium-density performance", pid=0x641, bid=None, ram=(0x_2000_0200, 0x_2000_5000), system=(0x_1FFF_F000, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0802_0000, 1 * kB, 4), option=(0x_1FFF_F800, 0x_1FFF_F80F)), # WBA, WB, WL or simply 'W'? - DeviceInfo("W", "STM32W", variant="128kB", pid=0x9A8, bid=None, ram=(0x_2000_0200, 0x_2000_2000), system=(0x_0804_0000, 0x_0804_0800), flash=(0x_0800_0000, 0x_0802_0000, 1 * k, 4), option=(0x_0804_0800, 0x_0804_080F)), - DeviceInfo("W", "STM32W", variant="256kB", pid=0x9B0, bid=None, ram=(0x_2000_0200, 0x_2000_4000), system=(0x_0804_0000, 0x_0804_0800), flash=(0x_0800_0000, 0x_0804_0000, 2 * k, 4), option=(0x_0804_0800, 0x_0804_080F)), + # FIXME bootloader ID address? + DeviceInfo("W", "STM32W", variant="128kB", pid=0x9A8, bid=None, ram=(0x_2000_0200, 0x_2000_2000), system=(0x_0804_0000, 0x_0804_0800), flash=(0x_0800_0000, 0x_0802_0000, 1 * kB, 4), option=(0x_0804_0800, 0x_0804_080F)), + DeviceInfo("W", "STM32W", variant="256kB", pid=0x9B0, bid=None, ram=(0x_2000_0200, 0x_2000_4000), system=(0x_0804_0000, 0x_0804_0800), flash=(0x_0800_0000, 0x_0804_0000, 2 * kB, 4), option=(0x_0804_0800, 0x_0804_080F)), # ST BlueNRG; FIXME: ram/system/flash config? + # FIXME bootloader ID address? DeviceInfo("NRG1", "BlueNRG-1", variant="160kB", pid=0x03, bid=None, ram=None, system=None), DeviceInfo("NRG1", "BlueNRG-1", variant="256kB", pid=0x0F, bid=None, ram=None, system=None), DeviceInfo("NRG2", "BlueNRG-1", variant="160kB", pid=0x23, bid=None, ram=None, system=None), DeviceInfo("NRG2", "BlueNRG-1", variant="256kB", pid=0x2F, bid=None, ram=None, system=None), # Wiznet W7500 - DeviceInfo("WIZ", "Wiznet W7500", 0x801, None, ram=None, system=None), + DeviceInfo("WIZ", "Wiznet W7500", 0x801, bid=None, ram=None, system=None), ] DEVICES = {(dev.product_id, dev.bootloader_id): dev for dev in DEVICE_DETAILS} + +# If devices are not yet registered with bootloader_id == None, then do so. +for device in DEVICE_DETAILS: + if (device.product_id, None) in DEVICES: + continue + DEVICES[(device.product_id, None)] = device From ad90df6e80de16542f330c43f24c7fbb1dc5a47e Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Jan 2024 14:37:21 +0100 Subject: [PATCH 336/369] dev: Make devices tests pass --- tests/unit/test_devices.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_devices.py b/tests/unit/test_devices.py index 27f95c6..43c77eb 100644 --- a/tests/unit/test_devices.py +++ b/tests/unit/test_devices.py @@ -36,7 +36,9 @@ def test_only_specific_device_names_occur_twice(): all_names = set() - for dev in DEVICES.values(): + for (product_id, bootloader_id), dev in DEVICES.items(): + if bootloader_id is None: + continue if dev.device_name in all_names: assert dev.device_name in KNOWN_DUPLICATE_DEVICE_NAMES, dev.device_name all_names.add(dev.device_name) @@ -51,7 +53,8 @@ def test_product_id_and_bootloader_id_match_device_properties(ids): dev = DEVICES[ids] device_id, bootloader_id = ids assert dev.product_id == device_id - assert dev.bootloader_id == bootloader_id + if bootloader_id is not None: + assert dev.bootloader_id == bootloader_id @pytest.mark.parametrize( From 5cb6fcd496c9f15d54afcea03cdb0a3da35a3b55 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Jan 2024 14:38:04 +0100 Subject: [PATCH 337/369] clean(cosmetic) --- tests/integration/test_full_cycle.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/integration/test_full_cycle.py b/tests/integration/test_full_cycle.py index 2b89427..b6f7f74 100644 --- a/tests/integration/test_full_cycle.py +++ b/tests/integration/test_full_cycle.py @@ -8,7 +8,7 @@ FIRMWARE_FILE = Path(__file__).parent / "../../firmware/generic_boot20_pc13.binary.bin" -def erase_write_verify(): +def test_erase_write_verify_passes(): loader = Stm32Loader() loader.configuration = FakeConfiguration(erase=True, write=True, verify=True, firmware_file=FIRMWARE_FILE, family=None) loader.connection = FakeConnection() @@ -18,7 +18,3 @@ def erase_write_verify(): loader.read_device_uid() loader.read_flash_size() loader.perform_commands() - - -if __name__ == "__main__": - erase_write_verify() From e21e661a8c4276d61f58b76bfc205e271fb6c17d Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 17 Jan 2024 14:38:23 +0100 Subject: [PATCH 338/369] dev: Add fake to assist testing other code --- stm32loader/emulated/fake.py | 136 +++++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) diff --git a/stm32loader/emulated/fake.py b/stm32loader/emulated/fake.py index e69de29..4a49c6b 100644 --- a/stm32loader/emulated/fake.py +++ b/stm32loader/emulated/fake.py @@ -0,0 +1,136 @@ +import struct + +from stm32loader.bootloader import Stm32Bootloader + + +class FakeConnection: + + ACK = Stm32Bootloader.Reply.ACK.value + Command = Stm32Bootloader.Command + + COMMAND_RESPONSES = { + # Return length, bootloader version, commands + # Version 5, 0x0=GET 0x01=GET_VERSION 0x02=GET_ID 0x11=READ_MEMORY 0x31=WRITE_MEMORY + # 0x43=ERASE 0x44=EXTENDED_ERASE + Command.GET: [7, 0x05, [0x0, 0x01, 0x02, 0x11, 0x31, 0x43, 0x44], ACK], + # Product ID: 0x422 + Command.GET_ID: [1, [0x04, 0x22]], + } + + READ_RESPONSES = { + # Read flash size, F1. + (0x_1FFF_F7E0, 2): [[0x00, 0x01]], + + # Read flash size, F3. + (0x_1FFF_F7CC, 2): [[0x00, 0x01]], + + # Read device UID, F1. + (0x_1FFF_F7E8, 12): [[1, 0, 3, 2, 7, 6, 5, 4, 0xB, 0xA, 9, 8]], + + # Read device UID, F3. + (0x_1FFF_F7AC, 12): [[1, 0, 3, 2, 7, 6, 5, 4, 0xB, 0xA, 9, 8]], + + # Read Bootloader ID. + (0x_1FFF_F796, 1): [0x41], + } + + def __init__(self): + self.next_return = [] + self.timeout = 2 + self.receiver = self.receive() + + self.flash_offset = 0x_0800_0000 + self.flash_size = 2 * 1024 * 1024 + self.flash_memory = bytearray(2 * 1024 * 1024) + + # Start coroutine. + next(self.receiver) + + def ack(self): + self.next_return.append(self.ACK) + + def receive(self): + while True: + # Receive a command coming in. + command_bytes = yield + command_value = struct.unpack("B", command_bytes)[0] + + # Receive CRC byte. + yield + self.ack() + + if command_value in self.COMMAND_RESPONSES: + self.next_return.extend(self.COMMAND_RESPONSES[command_value]) + elif command_value == self.Command.READ_MEMORY.value: + # Receive address with CRC. + address_bytes = yield + address = struct.unpack(">I", address_bytes[0:4])[0] + self.ack() + + # Receive number of bytes + length_bytes = yield + length = struct.unpack("B", length_bytes)[0] + 1 + + # Receive CRC + yield + self.ack() + + # Set up data to respond. + if self.flash_offset <= address < self.flash_offset + self.flash_size: + # Return flash data. + flash_offset = address - self.flash_offset + self.next_return.append(list(self.flash_memory[flash_offset: flash_offset + length])) + else: + self.next_return.extend(self.READ_RESPONSES[(address, length)]) + elif command_value == self.Command.EXTENDED_ERASE.value: + pages_bytes = yield + pages = struct.unpack(">H", pages_bytes[0:2]) + if pages == 0xFFFF: + # Erase all. + self.flash_memory[:] = 0xFF + self.next_return.append(self.ACK) + elif command_value == self.Command.WRITE_MEMORY.value: + address_bytes = yield + address = struct.unpack(">I", address_bytes[0:4])[0] + size_bytes = yield + byte_count = struct.unpack("B", size_bytes)[0] + 1 + data = yield + crc = yield + + assert len(data) == byte_count, f"Length does not match byte count: {len(data)} vs {byte_count}" + + # Record data in flash memory. + flash_offset = address - 0x_0800_0000 + self.flash_memory[flash_offset: flash_offset + byte_count] = data + else: + raise NotImplementedError() + + def write(self, data): + # Send to coroutine. + self.receiver.send(data) + + def read(self, length=1): + if self.next_return: + value = self.next_return.pop(0) + if isinstance(value, int): + return [value] + return value + + return [self.ACK] + + +class FakeConfiguration: + + def __init__(self, erase, write, verify, firmware_file, family=None): + self.erase = erase + self.write = write + self.verify = verify + self.data_file = firmware_file + self.unprotect = False + self.protect = False + self.length = None + self.verbosity = 5 + self.address = 0x_0800_0000 + self.go_address = None + self.family = family + From 95102ad5f70ce24af248c9a98e5f7eb564610538 Mon Sep 17 00:00:00 2001 From: david-beinder Date: Wed, 17 Jan 2024 18:50:38 +0100 Subject: [PATCH 339/369] feat: BlueNRG-LP/LPS support --- src/stm32loader/bootloader.py | 8 ++++-- src/stm32loader/devices.py | 34 +++++++++++++---------- src/stm32loader/main.py | 52 ++++++++++++++++------------------- 3 files changed, 48 insertions(+), 46 deletions(-) diff --git a/src/stm32loader/bootloader.py b/src/stm32loader/bootloader.py index c3bccb9..2c7de27 100644 --- a/src/stm32loader/bootloader.py +++ b/src/stm32loader/bootloader.py @@ -26,7 +26,7 @@ import time from functools import lru_cache, reduce -from stm32loader.devices import DEVICES, DeviceFlag +from stm32loader.devices import DEVICES, DeviceFamily, DeviceFlag CHIP_IDS = { @@ -74,8 +74,6 @@ # and byte 2 (mask set). # Requires parity None. 0x000003: "BlueNRG-1 160kB", - 0x00000F: "BlueNRG-1 256kB", - 0x000023: "BlueNRG-2 160kB", 0x00002F: "BlueNRG-2 256kB", # STM32F0 RM0091 Table 136. DEV_ID and REV_ID field values 0x440: "STM32F030x8", @@ -588,6 +586,10 @@ def get_uid(self): def detect_device(self): product_id = self.get_id() + # BlueNRG devices have the silicon cut version in the upper bytes of the PID + if self.device_family == DeviceFamily.NRG.value: + product_id &= 0xFF + # Look up device details based on ID *without* bootloader ID. self.device = DEVICES.get((product_id, None)) diff --git a/src/stm32loader/devices.py b/src/stm32loader/devices.py index 1f3d88b..3dd0ab9 100644 --- a/src/stm32loader/devices.py +++ b/src/stm32loader/devices.py @@ -27,8 +27,7 @@ class DeviceFamily(enum.Enum): W = "W" # Non-STM devices. - NRG1 = "NRG1" - NRG2 = "NRG2" + NRG = "NRG" WIZ = "WIZ" @@ -41,7 +40,8 @@ class DeviceFlag(enum.IntEnum): # bytes for UID and flash size directly. # Reading a whole chunk of 256 bytes at 0x1FFFA700 does work and # requires some data extraction. - LONG_UID_ACCESS = 8 + LONG_UID_ACCESS = 8, + FORCE_PARITY_NONE = 16 class DeviceFamilyInfo: @@ -101,11 +101,17 @@ def __init__( DeviceFamily.WL: DeviceFamilyInfo("WL", uid_address=0x_1FFF_7590, flash_size_address=0x_1FFF_75E0), DeviceFamily.U5: DeviceFamilyInfo("U5", ), DeviceFamily.W: DeviceFamilyInfo("W", ), - # ST BlueNRG has DIE_ID register with PRODUCT, but no UID. - # NRG BlueNRG-2 datasheet - # Flash page size: 128 pages of 8 * 64 * 4 bytes - DeviceFamily.NRG1: DeviceFamilyInfo("NRG1", flash_size_address=0x_4010_0014, flash_page_size=2048), - DeviceFamily.NRG2: DeviceFamilyInfo("NRG2", flash_size_address=0x_4010_0014, flash_page_size=2048), + # ST BlueNRG series; see ST AN4872 (BlueNRG-1/2) and AN5471 (BlueNRG-LP/LPS). BlueNRG requires parity 'none'. + # Product ID: + # Byte 1: metal fix (masked out) + # Byte 2: mask set (masked out) + # Byte 3: 0xHL + # H: [0] BlueNRG-1, [2] BlueNRG-2, [3] BlueNRG-LP/LPS + # L: [3] 160kB, [B] 192kB, [F] 256kB + # There is no access to peripherals/system memory from bootloader, so flash size and UID can not be read + # NRG-1/2: flash_size_address=0x_4010_0014, uid_address=0x_1000_07F4 + # NRG-LP: flash_size_address=0x_4000_1014, uid_address=0x_1000_1EF0 + DeviceFamily.NRG: DeviceFamilyInfo("NRG", flags=DeviceFlag.FORCE_PARITY_NONE, flash_page_size=2048), DeviceFamily.WIZ: DeviceFamilyInfo("WIZ", ), } @@ -357,12 +363,12 @@ class BootloaderSerialPeripherals(enum.Enum): DeviceInfo("W", "STM32W", variant="128kB", pid=0x9A8, bid=None, ram=(0x_2000_0200, 0x_2000_2000), system=(0x_0804_0000, 0x_0804_0800), flash=(0x_0800_0000, 0x_0802_0000, 1 * kB, 4), option=(0x_0804_0800, 0x_0804_080F)), DeviceInfo("W", "STM32W", variant="256kB", pid=0x9B0, bid=None, ram=(0x_2000_0200, 0x_2000_4000), system=(0x_0804_0000, 0x_0804_0800), flash=(0x_0800_0000, 0x_0804_0000, 2 * kB, 4), option=(0x_0804_0800, 0x_0804_080F)), - # ST BlueNRG; FIXME: ram/system/flash config? - # FIXME bootloader ID address? - DeviceInfo("NRG1", "BlueNRG-1", variant="160kB", pid=0x03, bid=None, ram=None, system=None), - DeviceInfo("NRG1", "BlueNRG-1", variant="256kB", pid=0x0F, bid=None, ram=None, system=None), - DeviceInfo("NRG2", "BlueNRG-1", variant="160kB", pid=0x23, bid=None, ram=None, system=None), - DeviceInfo("NRG2", "BlueNRG-1", variant="256kB", pid=0x2F, bid=None, ram=None, system=None), + # ST BlueNRG + DeviceInfo("NRG", "BlueNRG-1", pid=0x03, bid=None, ram=(0x_2000_0000, 0x_2000_6000), system=(0x_1000_0000, 0x_1000_0800), flash=(0x_1004_0000, 0x_1006_8000, 2 * kB)), + DeviceInfo("NRG", "BlueNRG-2", pid=0x2F, bid=None, ram=(0x_2000_0000, 0x_2000_6000), system=(0x_1000_0000, 0x_1000_0800), flash=(0x_1004_0000, 0x_1008_0000, 2 * kB)), + # There is a 32kB RAM variant of BlueNRG-LP, but that can't be determined from bootloader + DeviceInfo("NRG", "BlueNRG-LP", pid=0x3F, bid=None, ram=(0x_2000_0000, 0x_2001_0000), system=(0x_1000_0000, 0x_1000_1800), flash=(0x_1004_0000, 0x_1008_0000, 2 * kB)), + DeviceInfo("NRG", "BlueNRG-LPS", pid=0x3B, bid=None, ram=(0x_2000_0000, 0x_2000_6000), system=(0x_1000_0000, 0x_1000_1800), flash=(0x_1004_0000, 0x_1007_0000, 2 * kB)), # Wiznet W7500 DeviceInfo("WIZ", "Wiznet W7500", 0x801, bid=None, ram=None, system=None), diff --git a/src/stm32loader/main.py b/src/stm32loader/main.py index 14ee6a4..5d0c1ea 100644 --- a/src/stm32loader/main.py +++ b/src/stm32loader/main.py @@ -24,13 +24,15 @@ from pathlib import Path from types import SimpleNamespace +import serial + try: from progress.bar import ChargingBar as progress_bar except ImportError: progress_bar = None from stm32loader import args, bootloader, hexfile -# from stm32loader.devices import DEVICES +from stm32loader.devices import DEVICE_FAMILIES, DeviceFlag, DeviceFamily from stm32loader.uart import SerialConnection @@ -38,7 +40,7 @@ class Stm32Loader: """Main application: parse arguments and handle commands.""" # serial link bit parity, compatible to pyserial serial.PARTIY_EVEN - PARITY = {"even": "E", "none": "N"} + PARITY = {"even": serial.PARITY_EVEN, "none": serial.PARITY_NONE} def __init__(self): """Construct Stm32Loader object with default settings.""" @@ -57,6 +59,12 @@ def parse_arguments(self, arguments): # parse successful, process options further self.configuration.parity = Stm32Loader.PARITY[self.configuration.parity.lower()] + if self.configuration.family: + family = DeviceFamily[self.configuration.family] + family_flags = DEVICE_FAMILIES[family].family_default_flags + if family_flags & DeviceFlag.FORCE_PARITY_NONE: + self.configuration.parity = serial.PARITY_NONE + def connect(self): """Connect to the bootloader UART over an RS-232 serial port.""" serial_connection = SerialConnection( @@ -137,12 +145,14 @@ def perform_commands(self): try: if self.configuration.length is None: # Erase full device. + self.debug(0, "Performing full erase...") self.stm32.erase_memory(pages=None) else: # Erase from address to address + length. start_address = self.configuration.address end_address = self.configuration.address + self.configuration.length pages = self.stm32.pages_from_range(start_address, end_address) + self.debug(0, f"Performing partial erase (0x{start_address:X} - 0x{end_address:X}, {len(pages)} pages)... ") self.stm32.erase_memory(pages) except bootloader.CommandError: @@ -181,30 +191,11 @@ def detect_device(self): boot_version = self.stm32.get() self.debug(0, "Bootloader version: 0x%X" % boot_version) self.stm32.detect_device() - self.debug(5, 'Bootloader ID: 0x%02X' % self.stm32.device.bootloader_id) + if self.stm32.device.bootloader_id is not None: + self.debug(5, f"Bootloader ID: 0x{self.stm32.device.bootloader_id:02X}") self.debug(0, f"Chip ID: 0x{self.stm32.device.product_id:03X}") self.debug(0, f"Chip model: {self.stm32.device.device_name}") - def read_device_id(self): - """Show product ID and bootloader version.""" - boot_version = self.stm32.get() - self.debug(0, "Bootloader version: 0x%X" % boot_version) - device_id = self.stm32.get_id() - - if self.configuration.family == "NRG": - # ST AN4872. - # Three bytes encode metal fix, mask set, - # BlueNRG-series + flash size. - metal_fix = (device_id & 0xFF0000) >> 16 - mask_set = (device_id & 0x00FF00) >> 8 - device_id = device_id & 0x0000FF - self.debug(0, "Metal fix: 0x%X" % metal_fix) - self.debug(0, "Mask set: 0x%X" % mask_set) - - self.debug( - 0, "Chip id: 0x%X (%s)" % (device_id, bootloader.CHIP_IDS.get(device_id, "Unknown")) - ) - def read_device_uid(self): """Show chip UID.""" try: @@ -213,12 +204,13 @@ def read_device_uid(self): except bootloader.CommandError as e: self.debug( 0, - "Something was wrong with reading chip family data: " + str(e), + "Something was wrong with reading chip UID: " + str(e), ) return - device_uid_string = self.stm32.format_uid(device_uid) - self.debug(0, "Device UID: %s" % device_uid_string) + if device_uid != bootloader.Stm32Bootloader.UID_NOT_SUPPORTED: + device_uid_string = self.stm32.format_uid(device_uid) + self.debug(0, "Device UID: %s" % device_uid_string) def read_flash_size(self): """Show chip flash size.""" @@ -227,11 +219,12 @@ def read_flash_size(self): except bootloader.CommandError as e: self.debug( 0, - "Something was wrong with reading chip family data: " + str(e), + "Something was wrong with reading chip flash size: " + str(e), ) return - self.debug(0, "Flash size: %d KiB" % flash_size) + if flash_size != bootloader.Stm32Bootloader.FLASH_SIZE_UNKNOWN: + self.debug(0, f"Flash size: {flash_size} kiB") @staticmethod def _get_progress_bar(no_progress=False): @@ -252,8 +245,9 @@ def main(*arguments, **kwargs): loader.parse_arguments(arguments) loader.connect() try: - loader.read_device_id() + loader.detect_device() loader.read_device_uid() + loader.read_flash_size() loader.perform_commands() finally: loader.reset() From 0d6e2ce5e93ad4f7b625bfabdf886ec6209b8b52 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Fri, 27 Feb 2026 22:00:52 +0100 Subject: [PATCH 340/369] fix(test): Ignore BlueNRG devices --- tests/unit/test_devices.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_devices.py b/tests/unit/test_devices.py index 43c77eb..ef1c0f0 100644 --- a/tests/unit/test_devices.py +++ b/tests/unit/test_devices.py @@ -135,7 +135,8 @@ def test_stm32flash_device_names_match(device): break # Some devices don't exist in STM32Flash. - if not stm32flash_device and device.product_id in [0x443, 0x453, 0x474, 0x484, 0x492, 0x455, 0x481, 0x003, 0x00F, 0x0023, 0x002F, 0x801]: + stm32loader_only = (0x443, 0x453, 0x474, 0x484, 0x492, 0x455, 0x481, 0x003, 0x00F, 0x0023, 0x002F, 0x801, 0x03B, 0x03F) + if not stm32flash_device and device.product_id in stm32loader_only: return # Known / reviewed deviating names. From 8d8330468b897928c3ada1578e02b3dab16e221c Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Fri, 27 Feb 2026 22:01:39 +0100 Subject: [PATCH 341/369] fix(test): Expect 12 bytes to be read for uid --- tests/unit/test_bootloader.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/tests/unit/test_bootloader.py b/tests/unit/test_bootloader.py index 73ffe90..5e52053 100644 --- a/tests/unit/test_bootloader.py +++ b/tests/unit/test_bootloader.py @@ -1,4 +1,3 @@ -"""Unit tests for the Stm32Loader class.""" from unittest.mock import MagicMock @@ -224,21 +223,21 @@ def test_verify_data_with_non_identical_data_raises_verify_error_complaining_abo @pytest.mark.parametrize( - "pid_bid", [(0x412, None), (0x432, 0x50), (0x452, 0x90)] + "pid_bid, uid_address", + [ + ((0x412, None), 0x_1FFF_F7E8), + ((0x432, 0x50), 0x_1FFF_F7AC), + ((0x452, 0x90), 0x_1FF0_F420), + ], + ids=("F1", "F3", "F7"), ) -def test_get_uid_for_known_device_reads_at_correct_address(connection, pid_bid): +def test_get_uid_for_known_device_reads_at_correct_address(connection, pid_bid, uid_address): device = DEVICES.get(pid_bid) bootloader = Stm32Bootloader(connection, device=device) - bootloader.read_memory = MagicMock() - bootloader.get_uid() - uid_address = { - (0x412, None): 0x_1FFF_F7E8, - (0x432, 0x50): 0x_1FFF_F7AC, - (0x452, 0x90): 0x_1FF0_F420, - }[pid_bid] - bootloader.read_memory.assert_called_once_with(uid_address) + bootloader.get_uid() + bootloader.read_memory.assert_called_once_with(uid_address, 12) def test_get_uid_for_family_without_uid_returns_uid_not_supported(connection): From f804c0cf0fdae773d85062e80902d040279aa742 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Fri, 27 Feb 2026 22:02:20 +0100 Subject: [PATCH 342/369] dev: Expand STM32G4 details --- src/stm32loader/devices.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stm32loader/devices.py b/src/stm32loader/devices.py index 3dd0ab9..63a153e 100644 --- a/src/stm32loader/devices.py +++ b/src/stm32loader/devices.py @@ -84,7 +84,7 @@ def __init__( DeviceFamily.F7: DeviceFamilyInfo("F7", uid_address=0x_1FF0_F420, flash_size_address=0x_1FF0_F442, bootloader_id_address=0x_1FF0_EDBE), # RM0444 DeviceFamily.G0: DeviceFamilyInfo("G0", uid_address=0x_1FFF_7590, flash_size_address=0x_1FFF_75E0), - DeviceFamily.G4: DeviceFamilyInfo("G4", bootloader_id_address=0x_1FFF_6FFE), + DeviceFamily.G4: DeviceFamilyInfo("G4", uid_address=0x1FFF7590, flash_size_address=0x1FFF75E0, bootloader_id_address=0x_1FFF_6FFE), DeviceFamily.H5: DeviceFamilyInfo("H5", ), # RM0433 DeviceFamily.H7: DeviceFamilyInfo("H7", uid_address=0x_1FF1_E800, flash_size_address=0x_1FF1_E880, flash_page_size=128 * 1024), From 4425f405d7edd96adb7a2d1a5fd9f1ddf8be89bc Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Fri, 27 Feb 2026 22:02:44 +0100 Subject: [PATCH 343/369] fix: Move to new location --- {stm32loader => src/stm32loader}/emulated/fake.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {stm32loader => src/stm32loader}/emulated/fake.py (100%) diff --git a/stm32loader/emulated/fake.py b/src/stm32loader/emulated/fake.py similarity index 100% rename from stm32loader/emulated/fake.py rename to src/stm32loader/emulated/fake.py From 70f5773c396398adb285f758808e82a04ef5ac81 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Fri, 27 Feb 2026 22:52:41 +0100 Subject: [PATCH 344/369] clean(lint) --- noxfile.py | 2 +- src/stm32loader/bootloader.py | 37 +- src/stm32loader/device_family.py | 216 +++++ src/stm32loader/device_info.py | 136 +++ src/stm32loader/devices.py | 1283 +++++++++++++++++++------- src/stm32loader/emulated/fake.py | 36 +- src/stm32loader/main.py | 11 +- tests/integration/test_full_cycle.py | 7 +- tests/unit/devices_stm32flash.py | 1106 ++++++++++++++++++++-- tests/unit/test_bootloader.py | 14 +- tests/unit/test_devices.py | 88 +- tests/unit/test_hexfile.py | 5 +- 12 files changed, 2458 insertions(+), 483 deletions(-) create mode 100644 src/stm32loader/device_family.py create mode 100644 src/stm32loader/device_info.py diff --git a/noxfile.py b/noxfile.py index f943359..25afcaf 100644 --- a/noxfile.py +++ b/noxfile.py @@ -17,7 +17,7 @@ @session(python=PYTHON_VERSIONS, uv_groups=("test", "hex")) -def tests(session: Session) -> None: +def test(session: Session) -> None: """Execute unit tests.""" session.run("pytest") diff --git a/src/stm32loader/bootloader.py b/src/stm32loader/bootloader.py index 2c7de27..50fa5eb 100644 --- a/src/stm32loader/bootloader.py +++ b/src/stm32loader/bootloader.py @@ -26,8 +26,8 @@ import time from functools import lru_cache, reduce -from stm32loader.devices import DEVICES, DeviceFamily, DeviceFlag - +from stm32loader.device_family import DeviceFamily, DeviceFlag +from stm32loader.devices import DEVICES CHIP_IDS = { # see ST AN2606 Table 136 Bootloader device-dependent parameters @@ -116,7 +116,6 @@ class DeviceDetectionError(Stm32LoaderError): """Exception: could not detect device type.""" - class ShowProgress: """ Show progress through a progress bar, as a context manager. @@ -179,7 +178,7 @@ def __exit__(self, exc_type, exc_val, exc_tb): self.progress_bar.finish() -class Stm32Bootloader: +class Stm32Bootloader: # pylint: disable=too-many-instance-attributes """Talk to the STM32 native bootloader.""" # pylint: disable=too-many-public-methods @@ -358,7 +357,9 @@ class Reply(enum.IntEnum): SYNCHRONIZE_ATTEMPTS = 2 - def __init__(self, connection, device=None, device_family=None, verbosity=5, show_progress=None): + def __init__( # pylint: disable=too-many-positional-arguments,too-many-arguments + self, connection, device=None, device_family=None, verbosity=5, show_progress=None + ): """ Construct the Stm32Bootloader object. @@ -462,7 +463,9 @@ def get(self): supported_commands = bytearray(self.connection.read(length)) self.supported_commands = {command: True for command in supported_commands} self.extended_erase = self.Command.EXTENDED_ERASE in self.supported_commands - self.debug(10, " Available commands: " + ", ".join(hex(b) for b in self.supported_commands)) + self.debug( + 10, " Available commands: " + ", ".join(hex(b) for b in self.supported_commands) + ) self._wait_for_ack("0x00 end") return version @@ -583,10 +586,12 @@ def get_uid(self): return uid - def detect_device(self): + def detect_device(self) -> None: + """Detect the device type and store in `device`.""" product_id = self.get_id() - # BlueNRG devices have the silicon cut version in the upper bytes of the PID + # BlueNRG devices have the silicon cut version in the + # upper bytes of the PID. if self.device_family == DeviceFamily.NRG.value: product_id &= 0xFF @@ -594,15 +599,19 @@ def detect_device(self): self.device = DEVICES.get((product_id, None)) if not self.device: - raise DeviceDetectionError(f"Unknown device type: no type known for product id: 0x{product_id:03X}") + raise DeviceDetectionError( + f"Unknown device type: no type known for product id: 0x{product_id:03X}" + ) # Look up the product's bootloader ID. bootloader_id = self.get_bootloader_id() - # Now we can possibly *refine* the product: look up with product ID *and* bootloader ID. + # Now we can possibly *refine* the product: look up + # with product ID *and* bootloader ID. self.device = DEVICES.get((product_id, bootloader_id), self.device) def get_bootloader_id(self): + """Get the bootloader ID by reading the 'bootloader ID' register.""" if not self.device.bootloader_id_address: return None @@ -808,7 +817,9 @@ def read_memory_data(self, address, length): """ data = bytearray() chunk_count = int(math.ceil(length / float(self.data_transfer_size))) - self.debug(10, "Read %7d bytes in %3d chunks at address 0x%X..." % (length, chunk_count, address)) + self.debug( + 10, "Read %7d bytes in %3d chunks at address 0x%X..." % (length, chunk_count, address) + ) with self.show_progress("Reading", maximum=chunk_count) as progress_bar: while length: read_length = min(length, self.data_transfer_size) @@ -832,7 +843,9 @@ def write_memory_data(self, address, data): length = len(data) chunk_count = int(math.ceil(length / float(self.data_transfer_size))) offset = 0 - self.debug(5, "Write %6d bytes in %3d chunks at address 0x%X..." % (length, chunk_count, address)) + self.debug( + 5, "Write %6d bytes in %3d chunks at address 0x%X..." % (length, chunk_count, address) + ) with self.show_progress("Writing", maximum=chunk_count) as progress_bar: while length: diff --git a/src/stm32loader/device_family.py b/src/stm32loader/device_family.py new file mode 100644 index 0000000..c1ba473 --- /dev/null +++ b/src/stm32loader/device_family.py @@ -0,0 +1,216 @@ +"""Offer information about STM32 device families.""" + +import enum + + +@enum.unique +class DeviceFamily(enum.Enum): + """Enumeration of STM32 device families.""" + + # AN2606 + C0 = "C0" + F0 = "F0" + F1 = "F1" + F2 = "F2" + F3 = "F3" + F4 = "F4" + F7 = "F7" + G0 = "G0" + G4 = "G4" + H5 = "H5" + H7 = "H7" + L0 = "L0" + L1 = "L1" + L4 = "L4" + L5 = "L5" + WBA = "WBA" + WB = "WB" + WL = "WL" + U5 = "U5" + # Not sure if these really exist? + W = "W" + + # Non-STM devices. + NRG = "NRG" + WIZ = "WIZ" + + +@enum.unique +class DeviceFlag(enum.IntEnum): + """Represent device functionality as composable flags.""" + + NONE = 0 + OBL_LAUNCH = 1 + CLEAR_PEMPTY = 2 + # For some reason, F4 (at least, NUCLEO F401RE) can't read the 12 or 2 + # bytes for UID and flash size directly. + # Reading a whole chunk of 256 bytes at 0x1FFFA700 does work and + # requires some data extraction. + LONG_UID_ACCESS = 8 + FORCE_PARITY_NONE = 16 + + +class DeviceFamilyInfo: # pylint: disable=too-few-public-methods,too-many-instance-attributes + """Hold info about an STM32 device family.""" + + def __init__( # pylint: disable=too-many-arguments,too-many-positional-arguments + self, + name, + uid_address=None, + flash_size_address=None, + flash_page_size=1024, + transfer_size=256, + mass_erase=True, + option_bytes=None, + bootloader_id_address=None, + flags=DeviceFlag.NONE, + ): + self.name = name + self.uid_address = uid_address + self.flash_size_address = flash_size_address + self.flash_page_size = flash_page_size + self.transfer_size = transfer_size + self.mass_erase = mass_erase + self.option_bytes = option_bytes + self.bootloader_id_address = bootloader_id_address + self.family_default_flags = flags + + +DEVICE_FAMILIES = { + DeviceFamily.C0: DeviceFamilyInfo("C0", bootloader_id_address=0x_1FFF_17FE), + # RM0360 + DeviceFamily.F0: DeviceFamilyInfo( + "F0", flash_size_address=0x_1FFF_F7CC, option_bytes=(0x_1FFF_F800, 0x_1FFF_F80F) + ), + # RM0008 + DeviceFamily.F1: DeviceFamilyInfo( + "F1", + uid_address=0x_1FFF_F7E8, + flash_size_address=0x_1FFF_F7E0, + option_bytes=(0x_1FFF_F800, 0x_1FFF_F80F), + ), + DeviceFamily.F2: DeviceFamilyInfo( + "F2", option_bytes=(0x_1FFF_C000, 0x_1FFF_C00F), bootloader_id_address=0x_1FFF_77DE + ), + # RM0366, RM0365, RM0316, RM0313, RM4510 + DeviceFamily.F3: DeviceFamilyInfo( + "F3", + uid_address=0x_1FFF_F7AC, + flash_size_address=0x_1FFF_F7CC, + flash_page_size=2048, + bootloader_id_address=0x_1FFF_F796, + ), + # RM0090 + DeviceFamily.F4: DeviceFamilyInfo( + "F4", + uid_address=0x_1FFF_7A10, + flash_size_address=0x_1FFF_7A22, + bootloader_id_address=0x_1FFF_76DE, + flags=DeviceFlag.LONG_UID_ACCESS, + ), + # RM0385 + DeviceFamily.F7: DeviceFamilyInfo( + "F7", + uid_address=0x_1FF0_F420, + flash_size_address=0x_1FF0_F442, + bootloader_id_address=0x_1FF0_EDBE, + ), + # RM0444 + DeviceFamily.G0: DeviceFamilyInfo( + "G0", uid_address=0x_1FFF_7590, flash_size_address=0x_1FFF_75E0 + ), + DeviceFamily.G4: DeviceFamilyInfo( + "G4", + uid_address=0x1FFF7590, + flash_size_address=0x1FFF75E0, + bootloader_id_address=0x_1FFF_6FFE, + ), + DeviceFamily.H5: DeviceFamilyInfo( + "H5", + ), + # RM0433 + DeviceFamily.H7: DeviceFamilyInfo( + "H7", + uid_address=0x_1FF1_E800, + flash_size_address=0x_1FF1_E880, + flash_page_size=128 * 1024, + ), + # FIXME TWO RMs? + # RM0451, RM4510 + DeviceFamily.L0: DeviceFamilyInfo( + "L0", + uid_address=0x_1FF8_0050, + flash_size_address=0x_1FF8_007C, + transfer_size=128, + flash_page_size=128, + mass_erase=False, + flags=DeviceFlag.LONG_UID_ACCESS, + ), + DeviceFamily.L1: DeviceFamilyInfo("L1", mass_erase=False), + # RM0394 + DeviceFamily.L4: DeviceFamilyInfo( + "L4", + uid_address=0x_1FFF_7590, + flash_size_address=0x_1FFF_75E0, + bootloader_id_address=0x_1FFF_6FFE, + ), + DeviceFamily.L5: DeviceFamilyInfo( + "L5", + ), + DeviceFamily.WBA: DeviceFamilyInfo( + "WBA", + ), + DeviceFamily.WB: DeviceFamilyInfo( + "WB", + ), + # RM0453 + DeviceFamily.WL: DeviceFamilyInfo( + "WL", uid_address=0x_1FFF_7590, flash_size_address=0x_1FFF_75E0 + ), + DeviceFamily.U5: DeviceFamilyInfo( + "U5", + ), + DeviceFamily.W: DeviceFamilyInfo( + "W", + ), + # ST BlueNRG series; see ST AN4872 (BlueNRG-1/2) + # and AN5471 (BlueNRG-LP/LPS). + # BlueNRG requires parity 'none'. + # Product ID: + # Byte 1: metal fix (masked out) + # Byte 2: mask set (masked out) + # Byte 3: 0xHL + # H: [0] BlueNRG-1, [2] BlueNRG-2, [3] BlueNRG-LP/LPS + # L: [3] 160kB, [B] 192kB, [F] 256kB + # There is no access to peripherals/system memory from bootloader, + # so flash size and UID can not be read. + # NRG-1/2: flash_size_address=0x_4010_0014, uid_address=0x_1000_07F4 + # NRG-LP: flash_size_address=0x_4000_1014, uid_address=0x_1000_1EF0 + DeviceFamily.NRG: DeviceFamilyInfo( + "NRG", flags=DeviceFlag.FORCE_PARITY_NONE, flash_page_size=2048 + ), + DeviceFamily.WIZ: DeviceFamilyInfo( + "WIZ", + ), +} + + +@enum.unique +class BootloaderSerialPeripherals(enum.IntEnum): + """Enumeration of bootloader serial interface peripherals.""" + + # AN2606 + USART = 1 + DUAL_USART = 2 + UART_CAN_DFU = 3 + USART_DFU = 4 + USART_I2C = 5 + I2C = 6 + I2C_CAN_DFU_I2C = 7 + I2C_SPI = 8 + USART_CAN_FDCAN_DFU_I2C_SPI = 9 + USART_DFU_FDCAN_SPI = 10 + USART_I2C_SPI = 11 + USART_SPI = 12 + USART_DFU_I2C_SPI = 13 + USART_DFU_I2C_I3C_FDCAN_SPI = 14 diff --git a/src/stm32loader/device_info.py b/src/stm32loader/device_info.py new file mode 100644 index 0000000..ae4e272 --- /dev/null +++ b/src/stm32loader/device_info.py @@ -0,0 +1,136 @@ +"""Hold information about different STM32 devices.""" + +from stm32loader.device_family import DEVICE_FAMILIES, DeviceFamily, DeviceFlag + +kB = 1024 # pylint: disable=invalid-name + + +class DeviceInfo: # pylint: disable=too-many-instance-attributes + """Hold info about""" + + def __init__( # pylint: disable=too-many-positional-arguments,too-many-arguments + self, + device_family, + device_name, + pid, + bid, + variant=None, + line=None, + ram=None, + flash=None, + system=None, + option=None, + bootloader_id_address=None, + flags=DeviceFlag.NONE, + ): + self.family = DEVICE_FAMILIES[DeviceFamily[device_family]] + self.device_name = device_name + self.product_id = pid + self.bootloader_id = bid + self.variant = variant + self.product_line = line + self.ram = ram + self.flash = Flash(*(flash or [])) + self.system_memory = system + self.option_bytes = option + self.flags = flags | self.family.family_default_flags + self.bootloader_id_address = bootloader_id_address or self.family.bootloader_id_address + + @property + def ram_size(self): + """Return the device's RAM memory size in bytes.""" + if self.ram is None: + return 0 + + assert isinstance(self.ram, tuple) + + if isinstance(self.ram[0], tuple): + # Multiple ranges. + ram_size = 0 + for ram_range in self.ram: + ram_size += ram_range[1] - ram_range[0] + + return ram_size + + start, end = self.ram + ram_size = end - start + return ram_size + + @property + def flash_size(self): + """Return the device's flash memory size in bytes.""" + return self.flash.size + + @property + def system_memory_size(self): + """Return the size of the system memory in bytes.""" + if self.system_memory is None: + return 0 + + assert isinstance(self.system_memory, tuple) + + if isinstance(self.system_memory[0], tuple): + # Multiple ranges. + flash_size = 0 + for flash_range in self.system_memory: + flash_size += flash_range[1] - flash_range[0] + + return flash_size + + start, end = self.system_memory + flash_size = end - start + return flash_size + + def __str__(self): + name = self.device_name + if self.variant: + name += f"-{self.variant}" + if self.product_line: + name += f"-{self.product_line}" + return name + + def __repr__(self): + return f"DeviceInfo(device_name={self.device_name!r}, variant={self.product_line!r})" + + +class Flash: # pylint: disable=too-few-public-methods + """Represent info about a device's flash layout.""" + + # RM0090 4 sectors of 16 Kbytes, 1 sector of 64 Kbytes, + # 7 sectors of 128 Kbytes + F2_F4_PAGE_SIZE = (16 * kB, 16 * kB, 16 * kB, 16 * kB, 64 * kB, 128 * kB, 0) + # RM0090 4 sectors of 16 Kbytes, 1 sector of 64 Kbytes, + # 7 sectors of 128 Kbytes but then per bank + F4_DUAL_BANK_PAGE_SIZE = ( + 16 * kB, + 16 * kB, + 16 * kB, + 16 * kB, + 64 * kB, + 128 * kB, + 128 * kB, + 128 * kB, + 128 * kB, + 16 * kB, + 16 * kB, + 16 * kB, + 16 * kB, + 64 * kB, + 128 * kB, + 0, + ) + F7_PAGE_SIZE = (32 * kB, 32 * kB, 32 * kB, 32 * kB, 128 * kB, 256 * kB, 0) + + def __init__(self, start=None, end=None, page_size=None, pages_per_sector=None): + self.start = start + self.end = end + self.page_size = page_size + self.pages_per_sector = pages_per_sector + + @property + def size(self) -> int: + """Return the size of the flash memory in bytes.""" + if self.start is None: + return 0 + + return self.end - self.start diff --git a/src/stm32loader/devices.py b/src/stm32loader/devices.py index 63a153e..c228824 100644 --- a/src/stm32loader/devices.py +++ b/src/stm32loader/devices.py @@ -1,375 +1,998 @@ -import enum +"""Offer information about the various STM32 device families.""" +from stm32loader.device_family import DeviceFlag +from stm32loader.device_info import DeviceInfo, Flash -@enum.unique -class DeviceFamily(enum.Enum): - # AN2606 - C0 = "C0" - F0 = "F0" - F1 = "F1" - F2 = "F2" - F3 = "F3" - F4 = "F4" - F7 = "F7" - G0 = "G0" - G4 = "G4" - H5 = "H5" - H7 = "H7" - L0 = "L0" - L1 = "L1" - L4 = "L4" - L5 = "L5" - WBA = "WBA" - WB = "WB" - WL = "WL" - U5 = "U5" - # Not sure if these really exist? - W = "W" +# pylint: disable=too-many-lines - # Non-STM devices. - NRG = "NRG" - WIZ = "WIZ" - - -@enum.unique -class DeviceFlag(enum.IntEnum): - NONE = 0 - OBL_LAUNCH = 1 - CLEAR_PEMPTY = 2 - # For some reason, F4 (at least, NUCLEO F401RE) can't read the 12 or 2 - # bytes for UID and flash size directly. - # Reading a whole chunk of 256 bytes at 0x1FFFA700 does work and - # requires some data extraction. - LONG_UID_ACCESS = 8, - FORCE_PARITY_NONE = 16 - - -class DeviceFamilyInfo: - - def __init__( - self, - name, - uid_address=None, - flash_size_address=None, - flash_page_size=1024, - transfer_size=256, - mass_erase=True, - option_bytes=None, - bootloader_id_address=None, - flags=DeviceFlag.NONE, - ): - self.name = name - self.uid_address = uid_address - self.flash_size_address = flash_size_address - self.flash_page_size = flash_page_size - self.transfer_size = transfer_size - self.mass_erase = mass_erase - self.option_bytes = option_bytes - self.bootloader_id_address = bootloader_id_address - self.family_default_flags = flags - - -DEVICE_FAMILIES = { - DeviceFamily.C0: DeviceFamilyInfo("C0", bootloader_id_address=0x_1FFF_17FE), - # RM0360 - DeviceFamily.F0: DeviceFamilyInfo("F0", flash_size_address=0x_1FFF_F7CC, option_bytes=(0x_1FFF_F800, 0x_1FFF_F80F)), - # RM0008 - DeviceFamily.F1: DeviceFamilyInfo("F1", uid_address=0x_1FFF_F7E8, flash_size_address=0x_1FFF_F7E0, option_bytes=(0x_1FFF_F800, 0x_1FFF_F80F)), - DeviceFamily.F2: DeviceFamilyInfo("F2", option_bytes=(0x_1FFF_C000, 0x_1FFF_C00F), bootloader_id_address=0x_1FFF_77DE), - # RM0366, RM0365, RM0316, RM0313, RM4510 - DeviceFamily.F3: DeviceFamilyInfo("F3", uid_address=0x_1FFF_F7AC, flash_size_address=0x_1FFF_F7CC, flash_page_size=2048, bootloader_id_address=0x_1FFF_F796), - # RM0090 - DeviceFamily.F4: DeviceFamilyInfo("F4", uid_address=0x_1FFF_7A10, flash_size_address=0x_1FFF_7A22, bootloader_id_address=0x_1FFF_76DE, flags=DeviceFlag.LONG_UID_ACCESS), - # RM0385 - DeviceFamily.F7: DeviceFamilyInfo("F7", uid_address=0x_1FF0_F420, flash_size_address=0x_1FF0_F442, bootloader_id_address=0x_1FF0_EDBE), - # RM0444 - DeviceFamily.G0: DeviceFamilyInfo("G0", uid_address=0x_1FFF_7590, flash_size_address=0x_1FFF_75E0), - DeviceFamily.G4: DeviceFamilyInfo("G4", uid_address=0x1FFF7590, flash_size_address=0x1FFF75E0, bootloader_id_address=0x_1FFF_6FFE), - DeviceFamily.H5: DeviceFamilyInfo("H5", ), - # RM0433 - DeviceFamily.H7: DeviceFamilyInfo("H7", uid_address=0x_1FF1_E800, flash_size_address=0x_1FF1_E880, flash_page_size=128 * 1024), - # FIXME TWO RMs? - # RM0451, RM4510 - DeviceFamily.L0: DeviceFamilyInfo("L0", uid_address=0x_1FF8_0050, flash_size_address=0x_1FF8_007C, transfer_size=128, flash_page_size=128, mass_erase=False, flags=DeviceFlag.LONG_UID_ACCESS), - DeviceFamily.L1: DeviceFamilyInfo("L1", mass_erase=False), - # RM0394 - DeviceFamily.L4: DeviceFamilyInfo("L4", uid_address=0x_1FFF_7590, flash_size_address=0x_1FFF_75E0, bootloader_id_address=0x_1FFF_6FFE), - DeviceFamily.L5: DeviceFamilyInfo("L5", ), - DeviceFamily.WBA: DeviceFamilyInfo("WBA", ), - DeviceFamily.WB: DeviceFamilyInfo("WB", ), - # RM0453 - DeviceFamily.WL: DeviceFamilyInfo("WL", uid_address=0x_1FFF_7590, flash_size_address=0x_1FFF_75E0), - DeviceFamily.U5: DeviceFamilyInfo("U5", ), - DeviceFamily.W: DeviceFamilyInfo("W", ), - # ST BlueNRG series; see ST AN4872 (BlueNRG-1/2) and AN5471 (BlueNRG-LP/LPS). BlueNRG requires parity 'none'. - # Product ID: - # Byte 1: metal fix (masked out) - # Byte 2: mask set (masked out) - # Byte 3: 0xHL - # H: [0] BlueNRG-1, [2] BlueNRG-2, [3] BlueNRG-LP/LPS - # L: [3] 160kB, [B] 192kB, [F] 256kB - # There is no access to peripherals/system memory from bootloader, so flash size and UID can not be read - # NRG-1/2: flash_size_address=0x_4010_0014, uid_address=0x_1000_07F4 - # NRG-LP: flash_size_address=0x_4000_1014, uid_address=0x_1000_1EF0 - DeviceFamily.NRG: DeviceFamilyInfo("NRG", flags=DeviceFlag.FORCE_PARITY_NONE, flash_page_size=2048), - DeviceFamily.WIZ: DeviceFamilyInfo("WIZ", ), -} - - -kB = 1024 - - -class Flash: - - # RM0090 4 sectors of 16 Kbytes, 1 sector of 64 Kbytes, 7 sectors of 128 Kbytes - F2_F4_PAGE_SIZE = (16 * kB, 16 * kB, 16 * kB, 16 * kB, 64 * kB, 128 * kB, 0) - # RM0090 4 sectors of 16 Kbytes, 1 sector of 64 Kbytes, 7 sectors of 128 Kbytes... per bank - F4_DUAL_BANK_PAGE_SIZE = ( - 16 * kB, 16 * kB, 16 * kB, 16 * kB, 64 * kB, 128 * kB, 128 * kB, 128 * kB, 128 * kB, - 16 * kB, 16 * kB, 16 * kB, 16 * kB, 64 * kB, 128 * kB, 0, - ) - F7_PAGE_SIZE = (32 * kB, 32 * kB, 32 * kB, 32 * kB, 128 * kB, 256 * kB, 0) - - def __init__(self, start=None, end=None, page_size=None, pages_per_sector=None): - self.start = start - self.end = end - self.page_size = page_size - self.pages_per_sector = pages_per_sector - - @property - def size(self): - if self.start is None: - return 0 - - return self.end - self.start - - -class DeviceInfo: - - def __init__(self, device_family, device_name, pid, bid, variant=None, line=None, ram=None, flash=None, system=None, option=None, bootloader_id_address=None, flags=DeviceFlag.NONE): - self.family = DEVICE_FAMILIES[DeviceFamily[device_family]] - self.device_name = device_name - self.product_id = pid - self.bootloader_id = bid - self.variant = variant - self.product_line = line - self.ram = ram - self.flash = Flash(*(flash or [])) - self.system_memory = system - self.option_bytes = option - self.flags = flags | self.family.family_default_flags - self.bootloader_id_address = bootloader_id_address or self.family.bootloader_id_address - - @property - def ram_size(self): - if self.ram is None: - return 0 - - assert isinstance(self.ram, tuple) - - if isinstance(self.ram[0], tuple): - # Multiple ranges. - ram_size = 0 - for ram_range in self.ram: - ram_size += ram_range[1] - ram_range[0] - - return ram_size - - start, end = self.ram - ram_size = end - start - return ram_size - - @property - def flash_size(self): - return self.flash.size - - @property - def system_memory_size(self): - if self.system_memory is None: - return 0 - - assert isinstance(self.system_memory, tuple) - - if isinstance(self.system_memory[0], tuple): - # Multiple ranges. - flash_size = 0 - for flash_range in self.system_memory: - flash_size += flash_range[1] - flash_range[0] - - return flash_size - - start, end = self.system_memory - flash_size = end - start - return flash_size - - def __str__(self): - name = self.device_name - if self.variant: - name += f"-{self.variant}" - if self.product_line: - name += f"-{self.product_line}" - return name - - def __repr__(self): - return f"DeviceInfo(device_name={self.device_name!r}, variant={self.product_line!r})" - - -@enum.unique -class BootloaderSerialPeripherals(enum.Enum): - # AN2606 - USART = 1 - DUAL_USART = 2 - UART_CAN_DFU = 3 - USART_DFU = 4 - USART_I2C = 5 - I2C = 6 - I2C_CAN_DFU_I2C = 7 - I2C_SPI = 8 - USART_CAN_FDCAN_DFU_I2C_SPI = 9 - USART_DFU_FDCAN_SPI = 10 - USART_I2C_SPI = 11 - USART_SPI = 12 - USART_DFU_I2C_SPI = 13 - USART_DFU_I2C_I3C_FDCAN_SPI = 14 +kB = 1024 # pylint: disable=invalid-name DEVICE_DETAILS = [ # Based on ST AN2606 section "Device-dependent bootloader parameters". - # Flash range, option bytes and flags gleaned from stm32flash, dev_table.c. - + # Flash range, option bytes and flags gleaned from + # stm32flash, dev_table.c. # FIXME flash? - DeviceInfo("C0", "STM32C011xx", 0x443, 0x51, ram=(0x_2000_0000, 0x_2000_3000), system=(0x_1FFF_0000, 0x_1FFF_1800), flash=None, option=None), + DeviceInfo( + "C0", + "STM32C011xx", + 0x443, + 0x51, + ram=(0x_2000_0000, 0x_2000_3000), + system=(0x_1FFF_0000, 0x_1FFF_1800), + flash=None, + option=None, + ), # FIXME flash? # Error in AN2606? Ram is mentioned as 0x_2000_2000 - 0x_2000_17FF - DeviceInfo("C0", "STM32C031xx", 0x453, 0x52, ram=(0x_2000_2000, 0x_2000_2800), system=(0x_1FFF_0000, 0x_1FFF_1800), flash=None, option=None), - DeviceInfo("F0", "STM32F05xxx/030x8", 0x440, 0x21, ram=(0x_2000_0800, 0x_2000_2000), system=(0x_1FFF_EC00, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0801_0000, 1 * kB, 4), option=(0x_1FFF_F800, 0x_1FFF_F80F), bootloader_id_address=0x_1FFF_F7A6), - DeviceInfo("F0", "STM32F03xx4/6", 0x444, 0x10, ram=(0x_2000_0800, 0x_2000_1000), system=(0x_1FFF_EC00, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0800_8000, 1 * kB, 4), option=(0x_1FFF_F800, 0x_1FFF_F80F), bootloader_id_address=0x_1FFF_F7A6), + DeviceInfo( + "C0", + "STM32C031xx", + 0x453, + 0x52, + ram=(0x_2000_2000, 0x_2000_2800), + system=(0x_1FFF_0000, 0x_1FFF_1800), + flash=None, + option=None, + ), + DeviceInfo( + "F0", + "STM32F05xxx/030x8", + 0x440, + 0x21, + ram=(0x_2000_0800, 0x_2000_2000), + system=(0x_1FFF_EC00, 0x_1FFF_F800), + flash=(0x_0800_0000, 0x_0801_0000, 1 * kB, 4), + option=(0x_1FFF_F800, 0x_1FFF_F80F), + bootloader_id_address=0x_1FFF_F7A6, + ), + DeviceInfo( + "F0", + "STM32F03xx4/6", + 0x444, + 0x10, + ram=(0x_2000_0800, 0x_2000_1000), + system=(0x_1FFF_EC00, 0x_1FFF_F800), + flash=(0x_0800_0000, 0x_0800_8000, 1 * kB, 4), + option=(0x_1FFF_F800, 0x_1FFF_F80F), + bootloader_id_address=0x_1FFF_F7A6, + ), # FIXME different flash size for both devices with PID=0x442 ? - DeviceInfo("F0", "STM32F030xC", 0x442, 0x52, ram=(0x_2000_1800, 0x_2000_8000), system=(0x_1FFF_D800, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0804_0000, 2 * kB, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F), bootloader_id_address=0x_1FFF_F796, flags=DeviceFlag.OBL_LAUNCH), + DeviceInfo( + "F0", + "STM32F030xC", + 0x442, + 0x52, + ram=(0x_2000_1800, 0x_2000_8000), + system=(0x_1FFF_D800, 0x_1FFF_F800), + flash=(0x_0800_0000, 0x_0804_0000, 2 * kB, 2), + option=(0x_1FFF_F800, 0x_1FFF_F80F), + bootloader_id_address=0x_1FFF_F796, + flags=DeviceFlag.OBL_LAUNCH, + ), # FIXME different flash size for both devices with PID=0x445 ? - DeviceInfo("F0", "STM32F04xxx", 0x445, 0xA1, ram=None, system=(0x_1FFF_C400, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0800_8000, 1 * kB, 4), option=(0x_1FFF_F800, 0x_1FFF_F80F), bootloader_id_address=0x_1FFF_F6A6), - DeviceInfo("F0", "STM32F070x6", 0x445, 0xA2, ram=None, system=(0x_1FFF_C400, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0800_8000, 1 * kB, 4), option=(0x_1FFF_F800, 0x_1FFF_F80F), bootloader_id_address=0x_1FFF_F6A6), + DeviceInfo( + "F0", + "STM32F04xxx", + 0x445, + 0xA1, + ram=None, + system=(0x_1FFF_C400, 0x_1FFF_F800), + flash=(0x_0800_0000, 0x_0800_8000, 1 * kB, 4), + option=(0x_1FFF_F800, 0x_1FFF_F80F), + bootloader_id_address=0x_1FFF_F6A6, + ), + DeviceInfo( + "F0", + "STM32F070x6", + 0x445, + 0xA2, + ram=None, + system=(0x_1FFF_C400, 0x_1FFF_F800), + flash=(0x_0800_0000, 0x_0800_8000, 1 * kB, 4), + option=(0x_1FFF_F800, 0x_1FFF_F80F), + bootloader_id_address=0x_1FFF_F6A6, + ), # FIXME different flash size for both devices with PID=0x448 ? - DeviceInfo("F0", "STM32F070xB", 0x448, 0xA3, ram=(0x_1FFF_C800, 0x_1FFF_F800), system=None, flash=(0x_0800_0000, 0x_0802_0000, 2 * kB, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F), bootloader_id_address=0x_1FFF_F6A6), - DeviceInfo("F0", "STM32F071xx/072xx", 0x448, 0xA1, ram=(0x_2000_1800, 0x_2000_4000), system=(0x_1FFF_C800, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0802_0000, 2 * kB, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F), bootloader_id_address=0x_1FFF_F6A6), - DeviceInfo("F0", "STM32F09xxx", 0x442, 0x50, ram=(0x_1FFF_D800, 0x_1FFF_F800), system=None, flash=(0x_0800_0000, 0x_0804_0000, 2 * kB, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F), bootloader_id_address=0x_1FFF_F796, flags=DeviceFlag.OBL_LAUNCH), - DeviceInfo("F1", "STM32F10xxx", line="Low-density", pid=0x412, bid=None, ram=(0x_2000_0200, 0x_2000_2800), system=(0x_1FFF_F000, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0800_8000, 1 * kB, 4), option=(0x_1FFF_F800, 0x_1FFF_F80F)), - DeviceInfo("F1", "STM32F10xxx", line="Medium-density", pid=0x410, bid=None, ram=(0x_2000_0200, 0x_2000_5000), system=(0x_1FFF_F000, 0x_1FFF_F800), flash = (0x_0800_0000, 0x_0802_0000, 1 * kB, 4), option = (0x_1FFF_F800, 0x_1FFF_F80F)), - DeviceInfo("F1", "STM32F10xxx", line="High-density", pid=0x414, bid=None, ram=(0x_2000_0200, 0x_2001_0000), system=(0x_1FFF_F000, 0x_1FFF_F800), flash = (0x_0800_0000, 0x_0808_0000, 2 * kB, 2), option = (0x_1FFF_F800, 0x_1FFF_F80F)), - DeviceInfo("F1", "STM32F10xxx", line="Medium-density value", pid=0x420, bid=0x10, ram=(0x_2000_0200, 0x_2000_2000), system=(0x_1FFF_F000, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0802_0000, 1 * kB, 4), option=(0x_1FFF_F800, 0x_1FFF_F80F), bootloader_id_address=0x_1FFF_F7D6), - DeviceInfo("F1", "STM32F10xxx", line="High-density value", pid=0x428, bid=0x10, ram=(0x_2000_0200, 0x_2000_8000), system=(0x_1FFF_F000, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0808_0000, 2 * kB, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F), bootloader_id_address=0x_1FFF_F7D6), - DeviceInfo("F1", "STM32F105xx/107xx", line="Connectivity", pid=0x418, bid=None, ram=(0x_2000_1000, 0x_2001_0000), system=(0x_1FFF_B000, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0804_0000, 2 * kB, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F)), - DeviceInfo("F1", "STM32F10xxx", line="XL-density", pid=0x430, bid=0x21, ram=(0x_2000_0800, 0x_2001_8000), system=(0x_1FFF_E000, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0810_0000, 2 * kB, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F), bootloader_id_address=0x_1FFF_F7D6), + DeviceInfo( + "F0", + "STM32F070xB", + 0x448, + 0xA3, + ram=(0x_1FFF_C800, 0x_1FFF_F800), + system=None, + flash=(0x_0800_0000, 0x_0802_0000, 2 * kB, 2), + option=(0x_1FFF_F800, 0x_1FFF_F80F), + bootloader_id_address=0x_1FFF_F6A6, + ), + DeviceInfo( + "F0", + "STM32F071xx/072xx", + 0x448, + 0xA1, + ram=(0x_2000_1800, 0x_2000_4000), + system=(0x_1FFF_C800, 0x_1FFF_F800), + flash=(0x_0800_0000, 0x_0802_0000, 2 * kB, 2), + option=(0x_1FFF_F800, 0x_1FFF_F80F), + bootloader_id_address=0x_1FFF_F6A6, + ), + DeviceInfo( + "F0", + "STM32F09xxx", + 0x442, + 0x50, + ram=(0x_1FFF_D800, 0x_1FFF_F800), + system=None, + flash=(0x_0800_0000, 0x_0804_0000, 2 * kB, 2), + option=(0x_1FFF_F800, 0x_1FFF_F80F), + bootloader_id_address=0x_1FFF_F796, + flags=DeviceFlag.OBL_LAUNCH, + ), + DeviceInfo( + "F1", + "STM32F10xxx", + line="Low-density", + pid=0x412, + bid=None, + ram=(0x_2000_0200, 0x_2000_2800), + system=(0x_1FFF_F000, 0x_1FFF_F800), + flash=(0x_0800_0000, 0x_0800_8000, 1 * kB, 4), + option=(0x_1FFF_F800, 0x_1FFF_F80F), + ), + DeviceInfo( + "F1", + "STM32F10xxx", + line="Medium-density", + pid=0x410, + bid=None, + ram=(0x_2000_0200, 0x_2000_5000), + system=(0x_1FFF_F000, 0x_1FFF_F800), + flash=(0x_0800_0000, 0x_0802_0000, 1 * kB, 4), + option=(0x_1FFF_F800, 0x_1FFF_F80F), + ), + DeviceInfo( + "F1", + "STM32F10xxx", + line="High-density", + pid=0x414, + bid=None, + ram=(0x_2000_0200, 0x_2001_0000), + system=(0x_1FFF_F000, 0x_1FFF_F800), + flash=(0x_0800_0000, 0x_0808_0000, 2 * kB, 2), + option=(0x_1FFF_F800, 0x_1FFF_F80F), + ), + DeviceInfo( + "F1", + "STM32F10xxx", + line="Medium-density value", + pid=0x420, + bid=0x10, + ram=(0x_2000_0200, 0x_2000_2000), + system=(0x_1FFF_F000, 0x_1FFF_F800), + flash=(0x_0800_0000, 0x_0802_0000, 1 * kB, 4), + option=(0x_1FFF_F800, 0x_1FFF_F80F), + bootloader_id_address=0x_1FFF_F7D6, + ), + DeviceInfo( + "F1", + "STM32F10xxx", + line="High-density value", + pid=0x428, + bid=0x10, + ram=(0x_2000_0200, 0x_2000_8000), + system=(0x_1FFF_F000, 0x_1FFF_F800), + flash=(0x_0800_0000, 0x_0808_0000, 2 * kB, 2), + option=(0x_1FFF_F800, 0x_1FFF_F80F), + bootloader_id_address=0x_1FFF_F7D6, + ), + DeviceInfo( + "F1", + "STM32F105xx/107xx", + line="Connectivity", + pid=0x418, + bid=None, + ram=(0x_2000_1000, 0x_2001_0000), + system=(0x_1FFF_B000, 0x_1FFF_F800), + flash=(0x_0800_0000, 0x_0804_0000, 2 * kB, 2), + option=(0x_1FFF_F800, 0x_1FFF_F80F), + ), + DeviceInfo( + "F1", + "STM32F10xxx", + line="XL-density", + pid=0x430, + bid=0x21, + ram=(0x_2000_0800, 0x_2001_8000), + system=(0x_1FFF_E000, 0x_1FFF_F800), + flash=(0x_0800_0000, 0x_0810_0000, 2 * kB, 2), + option=(0x_1FFF_F800, 0x_1FFF_F80F), + bootloader_id_address=0x_1FFF_F7D6, + ), # FIXME different flash size for both devices with PID=0x411 ? - DeviceInfo("F2", "STM32F2xxxx", 0x411, 0x20, ram=(0x_2000_2000, 0x_2002_0000), system=(0x_1FFF_0000, 0x_1FFF_7800), flash=(0x_0800_0000, 0x_0810_0000, Flash.F2_F4_PAGE_SIZE), option=(0x_1FFF_C000, 0x_1FFF_C00F)), - DeviceInfo("F2", "STM32F2xxxx", 0x411, 0x33, ram=(0x_2000_2000, 0x_2002_0000), system=(0x_1FFF_0000, 0x_1FFF_7800), flash=(0x_0800_0000, 0x_0810_0000, Flash.F2_F4_PAGE_SIZE), option=(0x_1FFF_C000, 0x_1FFF_C00F)), + DeviceInfo( + "F2", + "STM32F2xxxx", + 0x411, + 0x20, + ram=(0x_2000_2000, 0x_2002_0000), + system=(0x_1FFF_0000, 0x_1FFF_7800), + flash=(0x_0800_0000, 0x_0810_0000, Flash.F2_F4_PAGE_SIZE), + option=(0x_1FFF_C000, 0x_1FFF_C00F), + ), + DeviceInfo( + "F2", + "STM32F2xxxx", + 0x411, + 0x33, + ram=(0x_2000_2000, 0x_2002_0000), + system=(0x_1FFF_0000, 0x_1FFF_7800), + flash=(0x_0800_0000, 0x_0810_0000, Flash.F2_F4_PAGE_SIZE), + option=(0x_1FFF_C000, 0x_1FFF_C00F), + ), # FIXME different flash size for both devices with PID=0x432 ? - DeviceInfo("F3", "STM32F373xx", 0x432, 0x41, ram=(0x_2000_1400, 0x_2000_8000), system=(0x_1FFF_D800, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0804_0000, 2 * kB, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F), bootloader_id_address=0x_1FFF_F7A6), - DeviceInfo("F3", "STM32F378xx", 0x432, 0x50, ram=(0x_2000_1000, 0x_2000_8000), system=(0x_1FFF_D800, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0804_0000, 2 * kB, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F), bootloader_id_address=0x_1FFF_F7A6), + DeviceInfo( + "F3", + "STM32F373xx", + 0x432, + 0x41, + ram=(0x_2000_1400, 0x_2000_8000), + system=(0x_1FFF_D800, 0x_1FFF_F800), + flash=(0x_0800_0000, 0x_0804_0000, 2 * kB, 2), + option=(0x_1FFF_F800, 0x_1FFF_F80F), + bootloader_id_address=0x_1FFF_F7A6, + ), + DeviceInfo( + "F3", + "STM32F378xx", + 0x432, + 0x50, + ram=(0x_2000_1000, 0x_2000_8000), + system=(0x_1FFF_D800, 0x_1FFF_F800), + flash=(0x_0800_0000, 0x_0804_0000, 2 * kB, 2), + option=(0x_1FFF_F800, 0x_1FFF_F80F), + bootloader_id_address=0x_1FFF_F7A6, + ), # FIXME different flash size for both devices with PID=0x422 ? - DeviceInfo("F3", "STM32F302xB(C)/303xB(C)", 0x422, 0x41, ram=(0x_2000_1400, 0x_2000_A000), system=(0x_1FFF_D800, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0804_0000, 2 * kB, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F)), - DeviceInfo("F3", "STM32F358xx", 0x422, 0x50, ram=(0x_2000_1400, 0x_2000_A000), system=(0x_1FFF_D800, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0804_0000, 2 * kB, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F)), + DeviceInfo( + "F3", + "STM32F302xB(C)/303xB(C)", + 0x422, + 0x41, + ram=(0x_2000_1400, 0x_2000_A000), + system=(0x_1FFF_D800, 0x_1FFF_F800), + flash=(0x_0800_0000, 0x_0804_0000, 2 * kB, 2), + option=(0x_1FFF_F800, 0x_1FFF_F80F), + ), + DeviceInfo( + "F3", + "STM32F358xx", + 0x422, + 0x50, + ram=(0x_2000_1400, 0x_2000_A000), + system=(0x_1FFF_D800, 0x_1FFF_F800), + flash=(0x_0800_0000, 0x_0804_0000, 2 * kB, 2), + option=(0x_1FFF_F800, 0x_1FFF_F80F), + ), # FIXME different flash size for both devices with PID=0x439 ? - DeviceInfo("F3", "STM32F301xx/302x4(6/8)", 0x439, 0x40, ram=(0x_2000_1800, 0x_2000_4000), system=(0x_1FFF_D800, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0801_0000, 2 * kB, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F)), - DeviceInfo("F3", "STM32F318xx", 0x439, 0x50, ram=(0x_2000_1800, 0x_2000_4000), system=(0x_1FFF_D800, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0801_0000, 2 * kB, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F)), - DeviceInfo("F3", "STM32F303x4(6/8)/334xx/328xx", 0x438, 0x50, ram=(0x_2000_1800, 0x_2000_3000), system=(0x_1FFF_D800, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0801_0000, 2 * kB, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F)), + DeviceInfo( + "F3", + "STM32F301xx/302x4(6/8)", + 0x439, + 0x40, + ram=(0x_2000_1800, 0x_2000_4000), + system=(0x_1FFF_D800, 0x_1FFF_F800), + flash=(0x_0800_0000, 0x_0801_0000, 2 * kB, 2), + option=(0x_1FFF_F800, 0x_1FFF_F80F), + ), + DeviceInfo( + "F3", + "STM32F318xx", + 0x439, + 0x50, + ram=(0x_2000_1800, 0x_2000_4000), + system=(0x_1FFF_D800, 0x_1FFF_F800), + flash=(0x_0800_0000, 0x_0801_0000, 2 * kB, 2), + option=(0x_1FFF_F800, 0x_1FFF_F80F), + ), + DeviceInfo( + "F3", + "STM32F303x4(6/8)/334xx/328xx", + 0x438, + 0x50, + ram=(0x_2000_1800, 0x_2000_3000), + system=(0x_1FFF_D800, 0x_1FFF_F800), + flash=(0x_0800_0000, 0x_0801_0000, 2 * kB, 2), + option=(0x_1FFF_F800, 0x_1FFF_F80F), + ), # FIXME different flash size for both devices with PID=0x446 ? - DeviceInfo("F3", "STM32F302xD(E)/303xD(E)", 0x446, 0x40, ram=(0x_2000_1800, 0x_2001_0000), system=(0x_1FFF_D800, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0808_0000, 2 * kB, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F)), - DeviceInfo("F3", "STM32F398xx", 0x446, 0x50, ram=(0x_2000_1800, 0x_2001_0000), system=(0x_1FFF_D800, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0808_0000, 2 * kB, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F)), - + DeviceInfo( + "F3", + "STM32F302xD(E)/303xD(E)", + 0x446, + 0x40, + ram=(0x_2000_1800, 0x_2001_0000), + system=(0x_1FFF_D800, 0x_1FFF_F800), + flash=(0x_0800_0000, 0x_0808_0000, 2 * kB, 2), + option=(0x_1FFF_F800, 0x_1FFF_F80F), + ), + DeviceInfo( + "F3", + "STM32F398xx", + 0x446, + 0x50, + ram=(0x_2000_1800, 0x_2001_0000), + system=(0x_1FFF_D800, 0x_1FFF_F800), + flash=(0x_0800_0000, 0x_0808_0000, 2 * kB, 2), + option=(0x_1FFF_F800, 0x_1FFF_F80F), + ), # FIXME different flash size for both devices with PID=0x413 ? - DeviceInfo("F4", "STM32F40xxx/41xxx", 0x413, 0x31, ram=(0x_2000_2000, 0x_2002_0000), system=(0x_1FFF_0000, 0x_1FFF_7800), flash=(0x_0800_0000, 0x_0810_0000, Flash.F2_F4_PAGE_SIZE), option=(0x_1FFF_C000, 0x_1FFF_C00F), bootloader_id_address=0x_1FFF_77DE), - DeviceInfo("F4", "STM32F40xxx/41xxx", 0x413, 0x91, ram=(0x_2000_3000, 0x_2002_0000), system=(0x_1FFF_0000, 0x_1FFF_7800), flash=(0x_0800_0000, 0x_0810_0000, Flash.F2_F4_PAGE_SIZE), option=(0x_1FFF_C000, 0x_1FFF_C00F), bootloader_id_address=0x_1FFF_77DE), + DeviceInfo( + "F4", + "STM32F40xxx/41xxx", + 0x413, + 0x31, + ram=(0x_2000_2000, 0x_2002_0000), + system=(0x_1FFF_0000, 0x_1FFF_7800), + flash=(0x_0800_0000, 0x_0810_0000, Flash.F2_F4_PAGE_SIZE), + option=(0x_1FFF_C000, 0x_1FFF_C00F), + bootloader_id_address=0x_1FFF_77DE, + ), + DeviceInfo( + "F4", + "STM32F40xxx/41xxx", + 0x413, + 0x91, + ram=(0x_2000_3000, 0x_2002_0000), + system=(0x_1FFF_0000, 0x_1FFF_7800), + flash=(0x_0800_0000, 0x_0810_0000, Flash.F2_F4_PAGE_SIZE), + option=(0x_1FFF_C000, 0x_1FFF_C00F), + bootloader_id_address=0x_1FFF_77DE, + ), # FIXME different flash size for both devices with PID=0x419 ? - DeviceInfo("F4", "STM32F42xxx/43xxx", 0x419, 0x70, ram=(0x_2000_3000, 0x_2003_0000), system=(0x_1FFF_0000, 0x_1FFF_7800), flash=(0x_0800_0000, 0x_0820_0000, Flash.F4_DUAL_BANK_PAGE_SIZE), option=(0x_1FFE_C000, 0x_1FFF_C00F)), - DeviceInfo("F4", "STM32F42xxx/43xxx", 0x419, 0x91, ram=(0x_2000_3000, 0x_2003_0000), system=(0x_1FFF_0000, 0x_1FFF_7800), flash=(0x_0800_0000, 0x_0820_0000, Flash.F4_DUAL_BANK_PAGE_SIZE), option=(0x_1FFE_C000, 0x_1FFF_C00F)), + DeviceInfo( + "F4", + "STM32F42xxx/43xxx", + 0x419, + 0x70, + ram=(0x_2000_3000, 0x_2003_0000), + system=(0x_1FFF_0000, 0x_1FFF_7800), + flash=(0x_0800_0000, 0x_0820_0000, Flash.F4_DUAL_BANK_PAGE_SIZE), + option=(0x_1FFE_C000, 0x_1FFF_C00F), + ), + DeviceInfo( + "F4", + "STM32F42xxx/43xxx", + 0x419, + 0x91, + ram=(0x_2000_3000, 0x_2003_0000), + system=(0x_1FFF_0000, 0x_1FFF_7800), + flash=(0x_0800_0000, 0x_0820_0000, Flash.F4_DUAL_BANK_PAGE_SIZE), + option=(0x_1FFE_C000, 0x_1FFF_C00F), + ), # FIXME Check RAM upper end. - DeviceInfo("F4", "STM32F401xB(C)", 0x423, 0xD1, ram=(0x_2000_3000, 0x_2001_0000), system=(0x_1FFF_0000, 0x_1FFF_7800), flash=(0x_0800_0000, 0x_0804_0000, Flash.F2_F4_PAGE_SIZE), option=(0x_1FFF_C000, 0x_1FFF_C00F)), - DeviceInfo("F4", "STM32F401xD(E)", 0x433, 0xD1, ram=(0x_2000_3000, 0x_2001_8000), system=(0x_1FFF_0000, 0x_1FFF_7800), flash=(0x_0800_0000, 0x_0808_0000, Flash.F2_F4_PAGE_SIZE), option=(0x_1FFF_C000, 0x_1FFF_C00F)), - DeviceInfo("F4", "STM32F410xx", 0x458, 0xB1, ram=(0x_2000_3000, 0x_2000_8000), system=(0x_1FFF_0000, 0x_1FFF_7800), flash=(0x_0800_0000, 0x_0802_0000, Flash.F2_F4_PAGE_SIZE), option=(0x_1FFF_C000, 0x_1FFF_C00F)), - DeviceInfo("F4", "STM32F411xx", 0x431, 0xD0, ram=(0x_2000_3000, 0x_2002_0000), system=(0x_1FFF_0000, 0x_1FFF_7800), flash=(0x_0800_0000, 0x_0808_0000, Flash.F2_F4_PAGE_SIZE), option=(0x_1FFF_C000, 0x_1FFF_C00F)), - DeviceInfo("F4", "STM32F412xx", 0x441, 0x90, ram=(0x_2000_3000, 0x_2004_0000), system=(0x_1FFF_0000, 0x_1FFF_7800), flash=(0x_0800_0000, 0x_0810_0000, Flash.F2_F4_PAGE_SIZE), option=(0x_1FFF_C000, 0x_1FFF_C00F)), - DeviceInfo("F4", "STM32F446xx", 0x421, 0x90, ram=(0x_2000_3000, 0x_2002_0000), system=(0x_1FFF_0000, 0x_1FFF_7800), flash=(0x_0800_0000, 0x_0808_0000, Flash.F2_F4_PAGE_SIZE), option=(0x_1FFF_C000, 0x_1FFF_C00F)), - DeviceInfo("F4", "STM32F469xx/479xx", 0x434, 0x90, ram=(0x_2000_3000, 0x_2006_0000), system=(0x_1FFF_0000, 0x_1FFF_7800), flash=(0x_0800_0000, 0x_0820_0000, Flash.F4_DUAL_BANK_PAGE_SIZE), option=(0x_1FFE_C000, 0x_1FFF_C00F)), - DeviceInfo("F4", "STM32F413xx/423xx", 0x463, 0x90, ram=(0x_2000_3000, 0x_2005_0000), system=(0x_1FFF_0000, 0x_1FFF_7800), flash=(0x_0800_0000, 0x_0818_0000, Flash.F2_F4_PAGE_SIZE), option=(0x_1FFF_C000, 0x_1FFF_C00F)), - DeviceInfo("F7", "STM32F72xxx/73xxx", 0x452, 0x90, ram=(0x_2000_4000, 0x_2004_0000), system=(0x_1FF0_0000, 0x_1FF0_EDC0), flash=(0x_0800_0000, 0x_0808_0000, Flash.F2_F4_PAGE_SIZE), option=(0x_1FFF_0000, 0x_1FFF_001F)), + DeviceInfo( + "F4", + "STM32F401xB(C)", + 0x423, + 0xD1, + ram=(0x_2000_3000, 0x_2001_0000), + system=(0x_1FFF_0000, 0x_1FFF_7800), + flash=(0x_0800_0000, 0x_0804_0000, Flash.F2_F4_PAGE_SIZE), + option=(0x_1FFF_C000, 0x_1FFF_C00F), + ), + DeviceInfo( + "F4", + "STM32F401xD(E)", + 0x433, + 0xD1, + ram=(0x_2000_3000, 0x_2001_8000), + system=(0x_1FFF_0000, 0x_1FFF_7800), + flash=(0x_0800_0000, 0x_0808_0000, Flash.F2_F4_PAGE_SIZE), + option=(0x_1FFF_C000, 0x_1FFF_C00F), + ), + DeviceInfo( + "F4", + "STM32F410xx", + 0x458, + 0xB1, + ram=(0x_2000_3000, 0x_2000_8000), + system=(0x_1FFF_0000, 0x_1FFF_7800), + flash=(0x_0800_0000, 0x_0802_0000, Flash.F2_F4_PAGE_SIZE), + option=(0x_1FFF_C000, 0x_1FFF_C00F), + ), + DeviceInfo( + "F4", + "STM32F411xx", + 0x431, + 0xD0, + ram=(0x_2000_3000, 0x_2002_0000), + system=(0x_1FFF_0000, 0x_1FFF_7800), + flash=(0x_0800_0000, 0x_0808_0000, Flash.F2_F4_PAGE_SIZE), + option=(0x_1FFF_C000, 0x_1FFF_C00F), + ), + DeviceInfo( + "F4", + "STM32F412xx", + 0x441, + 0x90, + ram=(0x_2000_3000, 0x_2004_0000), + system=(0x_1FFF_0000, 0x_1FFF_7800), + flash=(0x_0800_0000, 0x_0810_0000, Flash.F2_F4_PAGE_SIZE), + option=(0x_1FFF_C000, 0x_1FFF_C00F), + ), + DeviceInfo( + "F4", + "STM32F446xx", + 0x421, + 0x90, + ram=(0x_2000_3000, 0x_2002_0000), + system=(0x_1FFF_0000, 0x_1FFF_7800), + flash=(0x_0800_0000, 0x_0808_0000, Flash.F2_F4_PAGE_SIZE), + option=(0x_1FFF_C000, 0x_1FFF_C00F), + ), + DeviceInfo( + "F4", + "STM32F469xx/479xx", + 0x434, + 0x90, + ram=(0x_2000_3000, 0x_2006_0000), + system=(0x_1FFF_0000, 0x_1FFF_7800), + flash=(0x_0800_0000, 0x_0820_0000, Flash.F4_DUAL_BANK_PAGE_SIZE), + option=(0x_1FFE_C000, 0x_1FFF_C00F), + ), + DeviceInfo( + "F4", + "STM32F413xx/423xx", + 0x463, + 0x90, + ram=(0x_2000_3000, 0x_2005_0000), + system=(0x_1FFF_0000, 0x_1FFF_7800), + flash=(0x_0800_0000, 0x_0818_0000, Flash.F2_F4_PAGE_SIZE), + option=(0x_1FFF_C000, 0x_1FFF_C00F), + ), + DeviceInfo( + "F7", + "STM32F72xxx/73xxx", + 0x452, + 0x90, + ram=(0x_2000_4000, 0x_2004_0000), + system=(0x_1FF0_0000, 0x_1FF0_EDC0), + flash=(0x_0800_0000, 0x_0808_0000, Flash.F2_F4_PAGE_SIZE), + option=(0x_1FFF_0000, 0x_1FFF_001F), + ), # FIXME different flash size for both devices with PID=0x449 ? - DeviceInfo("F7", "STM32F74xxx/75xxx", 0x449, 0x70, ram=(0x_2000_4000, 0x_2005_0000), system=(0x_1FF0_0000, 0x_1FF0_EDC0), flash=(0x_0800_0000, 0x_0810_0000, Flash.F7_PAGE_SIZE), option=(0x_1FFF_0000, 0x_1FFF_001F)), - DeviceInfo("F7", "STM32F74xxx/75xxx", 0x449, 0x90, ram=(0x_2000_4000, 0x_2005_0000), system=(0x_1FF0_0000, 0x_1FF0_EDC0), flash=(0x_0800_0000, 0x_0810_0000, Flash.F7_PAGE_SIZE), option=(0x_1FFF_0000, 0x_1FFF_001F)), - DeviceInfo("F7", "STM32F76xxx/77xxx", 0x451, 0x93, ram=(0x_2000_4000, 0x_2008_0000), system=(0x_1FF0_0000, 0x_1FF0_EDC0), flash=(0x_0800_0000, 0x_0820_0000, Flash.F7_PAGE_SIZE), option=(0x_1FFF_0000, 0x_1FFF_001F)), - DeviceInfo("G0", "STM32G03xxx/04xxx", 0x466, 0x53, ram=(0x_2000_1000, 0x_2000_2000), system=(0x_1FFF_0000, 0x_1FFF_2000), flash=(0x_0800_0000, 0x_0801_0000, 2 * kB), option=(0x_1FFF_7800, 0x_1FFF_787F), bootloader_id_address=0x_1FFF_1FFE), - DeviceInfo("G0", "STM32G07xxx/08xxx", 0x460, 0xB3, ram=(0x_2000_2700, 0x_2000_9000), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0802_0000, 2 * kB), option=(0x_1FFF_7800, 0x_1FFF_787F), bootloader_id_address=0x_1FFF_6FFE), + DeviceInfo( + "F7", + "STM32F74xxx/75xxx", + 0x449, + 0x70, + ram=(0x_2000_4000, 0x_2005_0000), + system=(0x_1FF0_0000, 0x_1FF0_EDC0), + flash=(0x_0800_0000, 0x_0810_0000, Flash.F7_PAGE_SIZE), + option=(0x_1FFF_0000, 0x_1FFF_001F), + ), + DeviceInfo( + "F7", + "STM32F74xxx/75xxx", + 0x449, + 0x90, + ram=(0x_2000_4000, 0x_2005_0000), + system=(0x_1FF0_0000, 0x_1FF0_EDC0), + flash=(0x_0800_0000, 0x_0810_0000, Flash.F7_PAGE_SIZE), + option=(0x_1FFF_0000, 0x_1FFF_001F), + ), + DeviceInfo( + "F7", + "STM32F76xxx/77xxx", + 0x451, + 0x93, + ram=(0x_2000_4000, 0x_2008_0000), + system=(0x_1FF0_0000, 0x_1FF0_EDC0), + flash=(0x_0800_0000, 0x_0820_0000, Flash.F7_PAGE_SIZE), + option=(0x_1FFF_0000, 0x_1FFF_001F), + ), + DeviceInfo( + "G0", + "STM32G03xxx/04xxx", + 0x466, + 0x53, + ram=(0x_2000_1000, 0x_2000_2000), + system=(0x_1FFF_0000, 0x_1FFF_2000), + flash=(0x_0800_0000, 0x_0801_0000, 2 * kB), + option=(0x_1FFF_7800, 0x_1FFF_787F), + bootloader_id_address=0x_1FFF_1FFE, + ), + DeviceInfo( + "G0", + "STM32G07xxx/08xxx", + 0x460, + 0xB3, + ram=(0x_2000_2700, 0x_2000_9000), + system=(0x_1FFF_0000, 0x_1FFF_7000), + flash=(0x_0800_0000, 0x_0802_0000, 2 * kB), + option=(0x_1FFF_7800, 0x_1FFF_787F), + bootloader_id_address=0x_1FFF_6FFE, + ), # FIXME different flash size for both devices with PID=0x467 ? # FIXME dual banks for system - DeviceInfo("G0", "STM32G0B0xx", 0x467, 0xD0, ram=(0x_2000_4000, 0x_2002_0000), system=((0x_1FFF_0000, 0x_1FFF_7000), (0x_1FFF_8000, 0x_1FFF_F000)), flash=(0x_0800_0000, 0x_0808_0000, 2 * kB), option=(0x_1FFF_7800, 0x_1FFF_787F), bootloader_id_address=0x_1FFF_9FFE), + DeviceInfo( + "G0", + "STM32G0B0xx", + 0x467, + 0xD0, + ram=(0x_2000_4000, 0x_2002_0000), + system=((0x_1FFF_0000, 0x_1FFF_7000), (0x_1FFF_8000, 0x_1FFF_F000)), + flash=(0x_0800_0000, 0x_0808_0000, 2 * kB), + option=(0x_1FFF_7800, 0x_1FFF_787F), + bootloader_id_address=0x_1FFF_9FFE, + ), # FIXME dual banks for system - DeviceInfo("G0", "STM32G0B1xx/0C1xx", 0x467, 0x92, ram=(0x_2000_4000, 0x_2002_0000), system=((0x_1FFF_0000, 0x_1FFF_6000), (0x_1FFF_8000, 0x_1FFF_F000)), flash=(0x_0800_0000, 0x_0808_0000, 2 * kB), option=(0x_1FFF_7800, 0x_1FFF_787F), bootloader_id_address=0x_1FFF_9FFE), + DeviceInfo( + "G0", + "STM32G0B1xx/0C1xx", + 0x467, + 0x92, + ram=(0x_2000_4000, 0x_2002_0000), + system=((0x_1FFF_0000, 0x_1FFF_6000), (0x_1FFF_8000, 0x_1FFF_F000)), + flash=(0x_0800_0000, 0x_0808_0000, 2 * kB), + option=(0x_1FFF_7800, 0x_1FFF_787F), + bootloader_id_address=0x_1FFF_9FFE, + ), # FIXME: STM32flash has 0x_2000_4800 as upper system range. - DeviceInfo("G0", "STM32G05xxx/061xx", 0x456, 0x51, ram=(0x_2000_1000, 0x_2000_2000), system=(0x_1FFF_0000, 0x_1FFF_1000), flash=(0x_0800_0000, 0x_0801_0000, 2 * kB), option=(0x_1FFF_7800, 0x_1FFF_787F), bootloader_id_address=0x_1FFF_1FFE), - DeviceInfo("G4", "STM32G431xx/441xx", 0x468, 0xD4, ram=(0x_2000_4000, 0x_2000_5800), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0802_0000, 2 * kB), option=(0x_1FFF_7800, 0x_1FFF_782F)), - DeviceInfo("G4", "STM32G47xxx/48xxx", 0x469, 0xD5, ram=(0x_2000_4000, 0x_2001_8000), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0808_0000, 2 * kB), option=(0x_1FFF_7800, 0x_1FFF_782F)), - DeviceInfo("G4", "STM32G491xx/A1xx", 0x479, 0xD2, ram=(0x_2000_4000, 0x_2001_C000), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0808_0000, 2 * kB), option=(0x_1FFF_7800, 0x_1FFF_782F)), + DeviceInfo( + "G0", + "STM32G05xxx/061xx", + 0x456, + 0x51, + ram=(0x_2000_1000, 0x_2000_2000), + system=(0x_1FFF_0000, 0x_1FFF_1000), + flash=(0x_0800_0000, 0x_0801_0000, 2 * kB), + option=(0x_1FFF_7800, 0x_1FFF_787F), + bootloader_id_address=0x_1FFF_1FFE, + ), + DeviceInfo( + "G4", + "STM32G431xx/441xx", + 0x468, + 0xD4, + ram=(0x_2000_4000, 0x_2000_5800), + system=(0x_1FFF_0000, 0x_1FFF_7000), + flash=(0x_0800_0000, 0x_0802_0000, 2 * kB), + option=(0x_1FFF_7800, 0x_1FFF_782F), + ), + DeviceInfo( + "G4", + "STM32G47xxx/48xxx", + 0x469, + 0xD5, + ram=(0x_2000_4000, 0x_2001_8000), + system=(0x_1FFF_0000, 0x_1FFF_7000), + flash=(0x_0800_0000, 0x_0808_0000, 2 * kB), + option=(0x_1FFF_7800, 0x_1FFF_782F), + ), + DeviceInfo( + "G4", + "STM32G491xx/A1xx", + 0x479, + 0xD2, + ram=(0x_2000_4000, 0x_2001_C000), + system=(0x_1FFF_0000, 0x_1FFF_7000), + flash=(0x_0800_0000, 0x_0808_0000, 2 * kB), + option=(0x_1FFF_7800, 0x_1FFF_782F), + ), # FIXME Flash and option bytes? - DeviceInfo("H5", "STM32H503xx", 0x474, 0xE1, ram=(0x_2000_4000, 0x_2000_8000), system=(0x_0BF8_7000, 0x_0BF9_0000), bootloader_id_address=0x_0BF8_FFFE), + DeviceInfo( + "H5", + "STM32H503xx", + 0x474, + 0xE1, + ram=(0x_2000_4000, 0x_2000_8000), + system=(0x_0BF8_7000, 0x_0BF9_0000), + bootloader_id_address=0x_0BF8_FFFE, + ), # FIXME Flash and option bytes? - DeviceInfo("H5", "STM32H563xx/573xx", 0x484, 0xE3, ram=(0x_2000_0000, 0x_200A_0000), system=(0x_0BF9_7000, 0x_0BFA_0000), bootloader_id_address=0x_0BF9_FAFE), - DeviceInfo("H7", "STM32H72xxx/73xxx", 0x483, 0x93, ram=((0x_2000_4100, 0x_2002_0000), (0x_2400_4000, 0x_2405_0000)), system=(0x_1FF0_0000, 0x_1FF1_E800), flash=(0x_0800_0000, 0x_0810_0000, 128 * kB), option=None, bootloader_id_address=0x_1FF1_E7FE), - DeviceInfo("H7", "STM32H74xxx/75xxx", 0x450, 0x91, ram=((0x_2000_4100, 0x_2002_0000), (0x_2400_5000, 0x_2408_0000)), system=(0x_1FF0_0000, 0x_1FF1_E800), flash=(0x_0800_0000, 0x_0820_0000, 128 * kB), option=None, bootloader_id_address=0x_1FF1_E7FE), - DeviceInfo("H7", "STM32H7A3xx/B3xx", 0x480, 0x92, ram=((0x_2000_4100, 0x_2002_0000), (0x_2403_4000, 0x_2408_0000)), system=(0x_1FF0_0000, 0x_1FF1_4000), flash=(0x_0800_0000, 0x_0810_0000, 8 * kB), option=None, bootloader_id_address=0x_1FF1_3FFE), - - DeviceInfo("L0", "STM32L01xxx/02xxx", 0x457, 0xC3, ram=None, system=(0x_1FF0_0000, 0x_1FF0_1000), flash=(0x_0800_0000, 0x_0800_4000, 128, 32), option=(0x_1FF8_0000, 0x_1FF8_001F), bootloader_id_address=0x_1FF0_0FFE), - DeviceInfo("L0", "STM32L031xx/041xx", 0x425, 0xC0, ram=(0x_2000_1000, 0x_2000_2000), system=(0x_1FF0_0000, 0x_1FF0_1000), flash=(0x_0800_0000, 0x_0800_8000, 128, 32), option=(0x_1FF8_0000, 0x_1FF8_001F), bootloader_id_address=0x_1FF0_0FFE), - DeviceInfo("L0", "STM32L05xxx/06xxx", 0x417, 0xC0, ram=(0x_2000_1000, 0x_2000_2000), system=(0x_1FF0_0000, 0x_1FF0_1000), flash=(0x_0800_0000, 0x_0801_0000, 128, 32), option=(0x_1FF8_0000, 0x_1FF8_001F), bootloader_id_address=0x_1FF0_0FFE), + DeviceInfo( + "H5", + "STM32H563xx/573xx", + 0x484, + 0xE3, + ram=(0x_2000_0000, 0x_200A_0000), + system=(0x_0BF9_7000, 0x_0BFA_0000), + bootloader_id_address=0x_0BF9_FAFE, + ), + DeviceInfo( + "H7", + "STM32H72xxx/73xxx", + 0x483, + 0x93, + ram=((0x_2000_4100, 0x_2002_0000), (0x_2400_4000, 0x_2405_0000)), + system=(0x_1FF0_0000, 0x_1FF1_E800), + flash=(0x_0800_0000, 0x_0810_0000, 128 * kB), + option=None, + bootloader_id_address=0x_1FF1_E7FE, + ), + DeviceInfo( + "H7", + "STM32H74xxx/75xxx", + 0x450, + 0x91, + ram=((0x_2000_4100, 0x_2002_0000), (0x_2400_5000, 0x_2408_0000)), + system=(0x_1FF0_0000, 0x_1FF1_E800), + flash=(0x_0800_0000, 0x_0820_0000, 128 * kB), + option=None, + bootloader_id_address=0x_1FF1_E7FE, + ), + DeviceInfo( + "H7", + "STM32H7A3xx/B3xx", + 0x480, + 0x92, + ram=((0x_2000_4100, 0x_2002_0000), (0x_2403_4000, 0x_2408_0000)), + system=(0x_1FF0_0000, 0x_1FF1_4000), + flash=(0x_0800_0000, 0x_0810_0000, 8 * kB), + option=None, + bootloader_id_address=0x_1FF1_3FFE, + ), + DeviceInfo( + "L0", + "STM32L01xxx/02xxx", + 0x457, + 0xC3, + ram=None, + system=(0x_1FF0_0000, 0x_1FF0_1000), + flash=(0x_0800_0000, 0x_0800_4000, 128, 32), + option=(0x_1FF8_0000, 0x_1FF8_001F), + bootloader_id_address=0x_1FF0_0FFE, + ), + DeviceInfo( + "L0", + "STM32L031xx/041xx", + 0x425, + 0xC0, + ram=(0x_2000_1000, 0x_2000_2000), + system=(0x_1FF0_0000, 0x_1FF0_1000), + flash=(0x_0800_0000, 0x_0800_8000, 128, 32), + option=(0x_1FF8_0000, 0x_1FF8_001F), + bootloader_id_address=0x_1FF0_0FFE, + ), + DeviceInfo( + "L0", + "STM32L05xxx/06xxx", + 0x417, + 0xC0, + ram=(0x_2000_1000, 0x_2000_2000), + system=(0x_1FF0_0000, 0x_1FF0_1000), + flash=(0x_0800_0000, 0x_0801_0000, 128, 32), + option=(0x_1FF8_0000, 0x_1FF8_001F), + bootloader_id_address=0x_1FF0_0FFE, + ), # FIXME different flash size for both devices with PID=0x447 ? # Note: STM32flash has 0x_2000_2000 as lower system range. - DeviceInfo("L0", "STM32L07xxx/08xxx", 0x447, 0x41, ram=(0x_2000_1000, 0x_2000_5000), system=(0x_1FF0_0000, 0x_1FF0_2000), flash=(0x_0800_0000, 0x_0803_0000, 128, 32), option=(0x_1FF8_0000, 0x_1FF8_001F), bootloader_id_address=0x_1FF0_1FFE), - DeviceInfo("L0", "STM32L07xxx/08xxx", 0x447, 0xB2, ram=(0x_2000_1400, 0x_2000_5000), system=(0x_1FF0_0000, 0x_1FF0_2000), flash=(0x_0800_0000, 0x_0803_0000, 128, 32), option=(0x_1FF8_0000, 0x_1FF8_001F), bootloader_id_address=0x_1FF0_1FFE), - - DeviceInfo("L1", "STM32L1xxx6(8/B)", line="Medium-density ULP", pid=0x416, bid=0x20, ram=(0x_2000_0800, 0x_2000_4000), system=(0x_1FF0_0000, 0x_1FF0_2000), flash=(0x_0800_0000, 0x_0802_0000, 256, 16), option=(0x_1FF8_0000, 0x_1FF8_001F), bootloader_id_address=0x_1FF0_0FFE), - DeviceInfo("L1", "STM32L1xxx6(8/B)A", 0x429, 0x20, ram=(0x_2000_1000, 0x_2000_8000), system=(0x_1FF0_0000, 0x_1FF0_2000), flash=(0x_0800_0000, 0x_0802_0000, 256, 16), option=(0x_1FF8_0000, 0x_1FF8_001F), bootloader_id_address=0x_1FF0_0FFE), - DeviceInfo("L1", "STM32L1xxxC", 0x427, 0x40, ram=(0x_2000_1000, 0x_2000_8000), system=(0x_1FF0_0000, 0x_1FF0_2000), flash=(0x_0800_0000, 0x_0804_0000, 256, 16), option=(0x_1FF8_0000, 0x_1FF8_001F), bootloader_id_address=0x_1FF0_1FFE), - DeviceInfo("L1", "STM32L1xxxD", 0x436, 0x45, ram=(0x_2000_1000, 0x_2000_C000), system=(0x_1FF0_0000, 0x_1FF0_2000), flash=(0x_0800_0000, 0x_0806_0000, 256, 16), option=(0x_1FF8_0000, 0x_1FF8_001F), bootloader_id_address=0x_1FF0_1FFE), - DeviceInfo("L1", "STM32L1xxxE", 0x437, 0x40, ram=(0x_2000_1000, 0x_2001_4000), system=(0x_1FF0_0000, 0x_1FF0_2000), flash=(0x_0800_0000, 0x_0808_0000, 256, 16), option=(0x_1FF8_0000, 0x_1FF8_001F), bootloader_id_address=0x_1FF0_1FFE), + DeviceInfo( + "L0", + "STM32L07xxx/08xxx", + 0x447, + 0x41, + ram=(0x_2000_1000, 0x_2000_5000), + system=(0x_1FF0_0000, 0x_1FF0_2000), + flash=(0x_0800_0000, 0x_0803_0000, 128, 32), + option=(0x_1FF8_0000, 0x_1FF8_001F), + bootloader_id_address=0x_1FF0_1FFE, + ), + DeviceInfo( + "L0", + "STM32L07xxx/08xxx", + 0x447, + 0xB2, + ram=(0x_2000_1400, 0x_2000_5000), + system=(0x_1FF0_0000, 0x_1FF0_2000), + flash=(0x_0800_0000, 0x_0803_0000, 128, 32), + option=(0x_1FF8_0000, 0x_1FF8_001F), + bootloader_id_address=0x_1FF0_1FFE, + ), + DeviceInfo( + "L1", + "STM32L1xxx6(8/B)", + line="Medium-density ULP", + pid=0x416, + bid=0x20, + ram=(0x_2000_0800, 0x_2000_4000), + system=(0x_1FF0_0000, 0x_1FF0_2000), + flash=(0x_0800_0000, 0x_0802_0000, 256, 16), + option=(0x_1FF8_0000, 0x_1FF8_001F), + bootloader_id_address=0x_1FF0_0FFE, + ), + DeviceInfo( + "L1", + "STM32L1xxx6(8/B)A", + 0x429, + 0x20, + ram=(0x_2000_1000, 0x_2000_8000), + system=(0x_1FF0_0000, 0x_1FF0_2000), + flash=(0x_0800_0000, 0x_0802_0000, 256, 16), + option=(0x_1FF8_0000, 0x_1FF8_001F), + bootloader_id_address=0x_1FF0_0FFE, + ), + DeviceInfo( + "L1", + "STM32L1xxxC", + 0x427, + 0x40, + ram=(0x_2000_1000, 0x_2000_8000), + system=(0x_1FF0_0000, 0x_1FF0_2000), + flash=(0x_0800_0000, 0x_0804_0000, 256, 16), + option=(0x_1FF8_0000, 0x_1FF8_001F), + bootloader_id_address=0x_1FF0_1FFE, + ), + DeviceInfo( + "L1", + "STM32L1xxxD", + 0x436, + 0x45, + ram=(0x_2000_1000, 0x_2000_C000), + system=(0x_1FF0_0000, 0x_1FF0_2000), + flash=(0x_0800_0000, 0x_0806_0000, 256, 16), + option=(0x_1FF8_0000, 0x_1FF8_001F), + bootloader_id_address=0x_1FF0_1FFE, + ), + DeviceInfo( + "L1", + "STM32L1xxxE", + 0x437, + 0x40, + ram=(0x_2000_1000, 0x_2001_4000), + system=(0x_1FF0_0000, 0x_1FF0_2000), + flash=(0x_0800_0000, 0x_0808_0000, 256, 16), + option=(0x_1FF8_0000, 0x_1FF8_001F), + bootloader_id_address=0x_1FF0_1FFE, + ), # Note: Stm32flash has 0x_2000_3100 as ram start. - DeviceInfo("L4", "STM32L412xx/422xx", line="Low-density", pid=0x464, bid=0xD1, ram=(0x_2000_2100, 0x_2000_8000), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0802_0000, 2 * kB), option=(0x_1FFF_7800, 0x_1FFF_780F)), - DeviceInfo("L4", "STM32L43xxx/44xxx", 0x435, 0x91, ram=(0x_2000_3100, 0x_2000_C000), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0804_0000, 2 * kB), option=(0x_1FFF_7800, 0x_1FFF_780F)), - DeviceInfo("L4", "STM32L45xxx/46xxx", 0x462, 0x92, ram=(0x_2000_3100, 0x_2002_0000), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0808_0000, 2 * kB), option=(0x_1FFF_7800, 0x_1FFF_780F), flags=DeviceFlag.CLEAR_PEMPTY), + DeviceInfo( + "L4", + "STM32L412xx/422xx", + line="Low-density", + pid=0x464, + bid=0xD1, + ram=(0x_2000_2100, 0x_2000_8000), + system=(0x_1FFF_0000, 0x_1FFF_7000), + flash=(0x_0800_0000, 0x_0802_0000, 2 * kB), + option=(0x_1FFF_7800, 0x_1FFF_780F), + ), + DeviceInfo( + "L4", + "STM32L43xxx/44xxx", + 0x435, + 0x91, + ram=(0x_2000_3100, 0x_2000_C000), + system=(0x_1FFF_0000, 0x_1FFF_7000), + flash=(0x_0800_0000, 0x_0804_0000, 2 * kB), + option=(0x_1FFF_7800, 0x_1FFF_780F), + ), + DeviceInfo( + "L4", + "STM32L45xxx/46xxx", + 0x462, + 0x92, + ram=(0x_2000_3100, 0x_2002_0000), + system=(0x_1FFF_0000, 0x_1FFF_7000), + flash=(0x_0800_0000, 0x_0808_0000, 2 * kB), + option=(0x_1FFF_7800, 0x_1FFF_780F), + flags=DeviceFlag.CLEAR_PEMPTY, + ), # FIXME different flash size for both devices with PID=0x415 ? - DeviceInfo("L4", "STM32L47xxx/48xxx", 0x415, 0xA3, ram=(0x_2000_3000, 0x_2001_8000), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0810_0000, 2 * kB), option=(0x_1FFF_7800, 0x_1FFF_F80F)), - DeviceInfo("L4", "STM32L47xxx/48xxx", 0x415, 0x92, ram=(0x_2000_3100, 0x_2001_8000), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0810_0000, 2 * kB), option=(0x_1FFF_7800, 0x_1FFF_F80F)), - DeviceInfo("L4", "STM32L496xx/4A6xx", 0x461, 0x93, ram=(0x_2000_3100, 0x_2004_0000), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0810_0000, 2 * kB), option=(0x_1FFF_7800, 0x_1FFF_F80F)), - DeviceInfo("L4", "STM32L4Rxx/4Sxx", 0x470, 0x95, ram=(0x_2000_3200, 0x_200A_0000), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0810_0000, 2 * kB), option=(0x_1FFF_7800, 0x_1FFF_F80F)), - DeviceInfo("L4", "STM32L4P5xx/Q5xx", 0x471, 0x90, ram=(0x_2000_4000, 0x_2005_0000), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0810_0000, 4 * kB), option=(0x_1FF0_0000, 0x_1FF0_000F)), - DeviceInfo("L5", "STM32L552xx/562xx", 0x472, 0x92, ram=(0x_2000_4000, 0x_2004_0000), system=(0x_0BF9_0000, 0x_0BF9_8000), flash=(0x_0800_0000, 0x_0808_0000, 2 * kB), option=None, bootloader_id_address=0x_0BF9_7FFE), + DeviceInfo( + "L4", + "STM32L47xxx/48xxx", + 0x415, + 0xA3, + ram=(0x_2000_3000, 0x_2001_8000), + system=(0x_1FFF_0000, 0x_1FFF_7000), + flash=(0x_0800_0000, 0x_0810_0000, 2 * kB), + option=(0x_1FFF_7800, 0x_1FFF_F80F), + ), + DeviceInfo( + "L4", + "STM32L47xxx/48xxx", + 0x415, + 0x92, + ram=(0x_2000_3100, 0x_2001_8000), + system=(0x_1FFF_0000, 0x_1FFF_7000), + flash=(0x_0800_0000, 0x_0810_0000, 2 * kB), + option=(0x_1FFF_7800, 0x_1FFF_F80F), + ), + DeviceInfo( + "L4", + "STM32L496xx/4A6xx", + 0x461, + 0x93, + ram=(0x_2000_3100, 0x_2004_0000), + system=(0x_1FFF_0000, 0x_1FFF_7000), + flash=(0x_0800_0000, 0x_0810_0000, 2 * kB), + option=(0x_1FFF_7800, 0x_1FFF_F80F), + ), + DeviceInfo( + "L4", + "STM32L4Rxx/4Sxx", + 0x470, + 0x95, + ram=(0x_2000_3200, 0x_200A_0000), + system=(0x_1FFF_0000, 0x_1FFF_7000), + flash=(0x_0800_0000, 0x_0810_0000, 2 * kB), + option=(0x_1FFF_7800, 0x_1FFF_F80F), + ), + DeviceInfo( + "L4", + "STM32L4P5xx/Q5xx", + 0x471, + 0x90, + ram=(0x_2000_4000, 0x_2005_0000), + system=(0x_1FFF_0000, 0x_1FFF_7000), + flash=(0x_0800_0000, 0x_0810_0000, 4 * kB), + option=(0x_1FF0_0000, 0x_1FF0_000F), + ), + DeviceInfo( + "L5", + "STM32L552xx/562xx", + 0x472, + 0x92, + ram=(0x_2000_4000, 0x_2004_0000), + system=(0x_0BF9_0000, 0x_0BF9_8000), + flash=(0x_0800_0000, 0x_0808_0000, 2 * kB), + option=None, + bootloader_id_address=0x_0BF9_7FFE, + ), # FIXME flash config ? - DeviceInfo("WBA", "STM32WBA52xx", 0x492, 0xB0, ram=(0x_2000_0000, 0x_2000_2000), system=(0x_0BF8_8000, 0x_0BF9_0000), bootloader_id_address=0x_0BF8_FEFE), - DeviceInfo("WB", "STM32WB10xx/15xx", 0x494, 0xB1, ram=(0x_2000_5000, 0x_2004_0000), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0805_0000, 2 * kB), option=(0x_1FFF_7800, 0x_1FFF_787F), bootloader_id_address=0x_1FFF_6FFE), - DeviceInfo("WB", "STM32WB30xx/35xx/50xx/55xx", 0x495, 0xD5, ram=(0x_2000_4000, 0x_2000_C000), system=(0x_1FFF_0000, 0x_1FFF_7000), flash=(0x_0800_0000, 0x_0810_0000, 4 * kB), option=(0x_1FFF_8000, 0x_1FFF_807F), bootloader_id_address=0x_1FFF_6FFE), - DeviceInfo("WL", "STM32WLE5xx/WL55xx", 0x497, 0xC4, ram=(0x_2000_2000, 0x_2001_0000), system=(0x_1FFF_0000, 0x_1FFF_4000), flash=(0x_0800_0000, 0x_0804_0000, 2 * kB), option=(0x_1FFF_7800, 0x_1FFF_8000), bootloader_id_address=0x_1FFF_3EFE), + DeviceInfo( + "WBA", + "STM32WBA52xx", + 0x492, + 0xB0, + ram=(0x_2000_0000, 0x_2000_2000), + system=(0x_0BF8_8000, 0x_0BF9_0000), + bootloader_id_address=0x_0BF8_FEFE, + ), + DeviceInfo( + "WB", + "STM32WB10xx/15xx", + 0x494, + 0xB1, + ram=(0x_2000_5000, 0x_2004_0000), + system=(0x_1FFF_0000, 0x_1FFF_7000), + flash=(0x_0800_0000, 0x_0805_0000, 2 * kB), + option=(0x_1FFF_7800, 0x_1FFF_787F), + bootloader_id_address=0x_1FFF_6FFE, + ), + DeviceInfo( + "WB", + "STM32WB30xx/35xx/50xx/55xx", + 0x495, + 0xD5, + ram=(0x_2000_4000, 0x_2000_C000), + system=(0x_1FFF_0000, 0x_1FFF_7000), + flash=(0x_0800_0000, 0x_0810_0000, 4 * kB), + option=(0x_1FFF_8000, 0x_1FFF_807F), + bootloader_id_address=0x_1FFF_6FFE, + ), + DeviceInfo( + "WL", + "STM32WLE5xx/WL55xx", + 0x497, + 0xC4, + ram=(0x_2000_2000, 0x_2001_0000), + system=(0x_1FFF_0000, 0x_1FFF_4000), + flash=(0x_0800_0000, 0x_0804_0000, 2 * kB), + option=(0x_1FFF_7800, 0x_1FFF_8000), + bootloader_id_address=0x_1FFF_3EFE, + ), # FIXME flash config? - DeviceInfo("U5", "STM32U535xx/545xx", 0x455, 0x91, ram=(0x_2000_4000, 0x_2024_0000), system=(0x_0BF9_0000, 0x_0BFA_0000), bootloader_id_address=0x_0BF9_9EFE), + DeviceInfo( + "U5", + "STM32U535xx/545xx", + 0x455, + 0x91, + ram=(0x_2000_4000, 0x_2024_0000), + system=(0x_0BF9_0000, 0x_0BFA_0000), + bootloader_id_address=0x_0BF9_9EFE, + ), # - DeviceInfo("U5", "STM32U575xx/585xx", 0x482, 0x92, ram=(0x_2000_4000, 0x_200C_0000), system=(0x_0BF9_0000, 0x_0BFA_0000), flash=(0x_0800_0000, 0x_0820_0000, 8 * kB), option=None, bootloader_id_address=0x_0BF9_9EFE), + DeviceInfo( + "U5", + "STM32U575xx/585xx", + 0x482, + 0x92, + ram=(0x_2000_4000, 0x_200C_0000), + system=(0x_0BF9_0000, 0x_0BFA_0000), + flash=(0x_0800_0000, 0x_0820_0000, 8 * kB), + option=None, + bootloader_id_address=0x_0BF9_9EFE, + ), # FIXME flash config? - DeviceInfo("U5", "STM32U595xx/599xx/5A9xx", 0x481, 0x92, ram=(0x_2000_4000, 0x_2027_0000), system=(0x_0BF9_0000, 0x_0BFA_0000), bootloader_id_address=0x_0BF9_9EFE), - + DeviceInfo( + "U5", + "STM32U595xx/599xx/5A9xx", + 0x481, + 0x92, + ram=(0x_2000_4000, 0x_2027_0000), + system=(0x_0BF9_0000, 0x_0BFA_0000), + bootloader_id_address=0x_0BF9_9EFE, + ), # Not yet in AN2606. Bootloader IDs are unknown. # Assumption: 'Medium-density performance' refers to F1 series, and F103. # FIXME No bootloader ID address? - DeviceInfo("F1", "STM32F103x8/B", line="Medium-density performance", pid=0x641, bid=None, ram=(0x_2000_0200, 0x_2000_5000), system=(0x_1FFF_F000, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0802_0000, 1 * kB, 4), option=(0x_1FFF_F800, 0x_1FFF_F80F)), - + DeviceInfo( + "F1", + "STM32F103x8/B", + line="Medium-density performance", + pid=0x641, + bid=None, + ram=(0x_2000_0200, 0x_2000_5000), + system=(0x_1FFF_F000, 0x_1FFF_F800), + flash=(0x_0800_0000, 0x_0802_0000, 1 * kB, 4), + option=(0x_1FFF_F800, 0x_1FFF_F80F), + ), # WBA, WB, WL or simply 'W'? # FIXME bootloader ID address? - DeviceInfo("W", "STM32W", variant="128kB", pid=0x9A8, bid=None, ram=(0x_2000_0200, 0x_2000_2000), system=(0x_0804_0000, 0x_0804_0800), flash=(0x_0800_0000, 0x_0802_0000, 1 * kB, 4), option=(0x_0804_0800, 0x_0804_080F)), - DeviceInfo("W", "STM32W", variant="256kB", pid=0x9B0, bid=None, ram=(0x_2000_0200, 0x_2000_4000), system=(0x_0804_0000, 0x_0804_0800), flash=(0x_0800_0000, 0x_0804_0000, 2 * kB, 4), option=(0x_0804_0800, 0x_0804_080F)), - + DeviceInfo( + "W", + "STM32W", + variant="128kB", + pid=0x9A8, + bid=None, + ram=(0x_2000_0200, 0x_2000_2000), + system=(0x_0804_0000, 0x_0804_0800), + flash=(0x_0800_0000, 0x_0802_0000, 1 * kB, 4), + option=(0x_0804_0800, 0x_0804_080F), + ), + DeviceInfo( + "W", + "STM32W", + variant="256kB", + pid=0x9B0, + bid=None, + ram=(0x_2000_0200, 0x_2000_4000), + system=(0x_0804_0000, 0x_0804_0800), + flash=(0x_0800_0000, 0x_0804_0000, 2 * kB, 4), + option=(0x_0804_0800, 0x_0804_080F), + ), # ST BlueNRG - DeviceInfo("NRG", "BlueNRG-1", pid=0x03, bid=None, ram=(0x_2000_0000, 0x_2000_6000), system=(0x_1000_0000, 0x_1000_0800), flash=(0x_1004_0000, 0x_1006_8000, 2 * kB)), - DeviceInfo("NRG", "BlueNRG-2", pid=0x2F, bid=None, ram=(0x_2000_0000, 0x_2000_6000), system=(0x_1000_0000, 0x_1000_0800), flash=(0x_1004_0000, 0x_1008_0000, 2 * kB)), - # There is a 32kB RAM variant of BlueNRG-LP, but that can't be determined from bootloader - DeviceInfo("NRG", "BlueNRG-LP", pid=0x3F, bid=None, ram=(0x_2000_0000, 0x_2001_0000), system=(0x_1000_0000, 0x_1000_1800), flash=(0x_1004_0000, 0x_1008_0000, 2 * kB)), - DeviceInfo("NRG", "BlueNRG-LPS", pid=0x3B, bid=None, ram=(0x_2000_0000, 0x_2000_6000), system=(0x_1000_0000, 0x_1000_1800), flash=(0x_1004_0000, 0x_1007_0000, 2 * kB)), - + DeviceInfo( + "NRG", + "BlueNRG-1", + pid=0x03, + bid=None, + ram=(0x_2000_0000, 0x_2000_6000), + system=(0x_1000_0000, 0x_1000_0800), + flash=(0x_1004_0000, 0x_1006_8000, 2 * kB), + ), + DeviceInfo( + "NRG", + "BlueNRG-2", + pid=0x2F, + bid=None, + ram=(0x_2000_0000, 0x_2000_6000), + system=(0x_1000_0000, 0x_1000_0800), + flash=(0x_1004_0000, 0x_1008_0000, 2 * kB), + ), + # There is a 32kB RAM variant of BlueNRG-LP, but + # that can't be determined from bootloader + DeviceInfo( + "NRG", + "BlueNRG-LP", + pid=0x3F, + bid=None, + ram=(0x_2000_0000, 0x_2001_0000), + system=(0x_1000_0000, 0x_1000_1800), + flash=(0x_1004_0000, 0x_1008_0000, 2 * kB), + ), + DeviceInfo( + "NRG", + "BlueNRG-LPS", + pid=0x3B, + bid=None, + ram=(0x_2000_0000, 0x_2000_6000), + system=(0x_1000_0000, 0x_1000_1800), + flash=(0x_1004_0000, 0x_1007_0000, 2 * kB), + ), # Wiznet W7500 DeviceInfo("WIZ", "Wiznet W7500", 0x801, bid=None, ram=None, system=None), ] diff --git a/src/stm32loader/emulated/fake.py b/src/stm32loader/emulated/fake.py index 4a49c6b..2268e67 100644 --- a/src/stm32loader/emulated/fake.py +++ b/src/stm32loader/emulated/fake.py @@ -1,17 +1,27 @@ +"""Fake bootloader connection for testing purposes.""" + import struct from stm32loader.bootloader import Stm32Bootloader +# pylint: disable=too-many-arguments +# pylint: disable=too-many-positional-arguments +# pylint: disable=too-many-instance-attributes +# pylint: disable=too-few-public-methods +# pylint: disable=missing-function-docstring + class FakeConnection: + """Emulate a bootloader connection.""" ACK = Stm32Bootloader.Reply.ACK.value Command = Stm32Bootloader.Command COMMAND_RESPONSES = { # Return length, bootloader version, commands - # Version 5, 0x0=GET 0x01=GET_VERSION 0x02=GET_ID 0x11=READ_MEMORY 0x31=WRITE_MEMORY - # 0x43=ERASE 0x44=EXTENDED_ERASE + # Version 5, 0x0=GET 0x01=GET_VERSION 0x02=GET_ID + # 0x11=READ_MEMORY 0x31=WRITE_MEMORY + # 0x43=ERASE 0x44=EXTENDED_ERASE Command.GET: [7, 0x05, [0x0, 0x01, 0x02, 0x11, 0x31, 0x43, 0x44], ACK], # Product ID: 0x422 Command.GET_ID: [1, [0x04, 0x22]], @@ -20,16 +30,12 @@ class FakeConnection: READ_RESPONSES = { # Read flash size, F1. (0x_1FFF_F7E0, 2): [[0x00, 0x01]], - # Read flash size, F3. (0x_1FFF_F7CC, 2): [[0x00, 0x01]], - # Read device UID, F1. - (0x_1FFF_F7E8, 12): [[1, 0, 3, 2, 7, 6, 5, 4, 0xB, 0xA, 9, 8]], - + (0x_1FFF_F7E8, 12): [[1, 0, 3, 2, 7, 6, 5, 4, 0xB, 0xA, 9, 8]], # Read device UID, F3. (0x_1FFF_F7AC, 12): [[1, 0, 3, 2, 7, 6, 5, 4, 0xB, 0xA, 9, 8]], - # Read Bootloader ID. (0x_1FFF_F796, 1): [0x41], } @@ -79,7 +85,9 @@ def receive(self): if self.flash_offset <= address < self.flash_offset + self.flash_size: # Return flash data. flash_offset = address - self.flash_offset - self.next_return.append(list(self.flash_memory[flash_offset: flash_offset + length])) + self.next_return.append( + list(self.flash_memory[flash_offset : flash_offset + length]) + ) else: self.next_return.extend(self.READ_RESPONSES[(address, length)]) elif command_value == self.Command.EXTENDED_ERASE.value: @@ -95,13 +103,15 @@ def receive(self): size_bytes = yield byte_count = struct.unpack("B", size_bytes)[0] + 1 data = yield - crc = yield + _crc = yield - assert len(data) == byte_count, f"Length does not match byte count: {len(data)} vs {byte_count}" + assert len(data) == byte_count, ( + f"Length does not match byte count: {len(data)} vs {byte_count}" + ) # Record data in flash memory. flash_offset = address - 0x_0800_0000 - self.flash_memory[flash_offset: flash_offset + byte_count] = data + self.flash_memory[flash_offset : flash_offset + byte_count] = data else: raise NotImplementedError() @@ -109,7 +119,7 @@ def write(self, data): # Send to coroutine. self.receiver.send(data) - def read(self, length=1): + def read(self, length=1): # pylint: disable=unused-argument if self.next_return: value = self.next_return.pop(0) if isinstance(value, int): @@ -120,6 +130,7 @@ def read(self, length=1): class FakeConfiguration: + """Represent a configuration for test purposes.""" def __init__(self, erase, write, verify, firmware_file, family=None): self.erase = erase @@ -133,4 +144,3 @@ def __init__(self, erase, write, verify, firmware_file, family=None): self.address = 0x_0800_0000 self.go_address = None self.family = family - diff --git a/src/stm32loader/main.py b/src/stm32loader/main.py index 5d0c1ea..4cea2ec 100644 --- a/src/stm32loader/main.py +++ b/src/stm32loader/main.py @@ -32,7 +32,7 @@ progress_bar = None from stm32loader import args, bootloader, hexfile -from stm32loader.devices import DEVICE_FAMILIES, DeviceFlag, DeviceFamily +from stm32loader.device_family import DEVICE_FAMILIES, DeviceFamily, DeviceFlag from stm32loader.uart import SerialConnection @@ -152,7 +152,11 @@ def perform_commands(self): start_address = self.configuration.address end_address = self.configuration.address + self.configuration.length pages = self.stm32.pages_from_range(start_address, end_address) - self.debug(0, f"Performing partial erase (0x{start_address:X} - 0x{end_address:X}, {len(pages)} pages)... ") + self.debug( + 0, + f"Performing partial erase (0x{start_address:X} - 0x{end_address:X}," + f" {len(pages)} pages)... ", + ) self.stm32.erase_memory(pages) except bootloader.CommandError: @@ -187,7 +191,8 @@ def reset(self): """Reset the microcontroller.""" self.stm32.reset_from_flash() - def detect_device(self): + def detect_device(self) -> None: + """Detect the STM32 device type by querying bootloader and regs.""" boot_version = self.stm32.get() self.debug(0, "Bootloader version: 0x%X" % boot_version) self.stm32.detect_device() diff --git a/tests/integration/test_full_cycle.py b/tests/integration/test_full_cycle.py index b6f7f74..18ca62f 100644 --- a/tests/integration/test_full_cycle.py +++ b/tests/integration/test_full_cycle.py @@ -1,16 +1,17 @@ from pathlib import Path -from stm32loader.main import Stm32Loader from stm32loader.bootloader import Stm32Bootloader from stm32loader.emulated.fake import FakeConfiguration, FakeConnection - +from stm32loader.main import Stm32Loader FIRMWARE_FILE = Path(__file__).parent / "../../firmware/generic_boot20_pc13.binary.bin" def test_erase_write_verify_passes(): loader = Stm32Loader() - loader.configuration = FakeConfiguration(erase=True, write=True, verify=True, firmware_file=FIRMWARE_FILE, family=None) + loader.configuration = FakeConfiguration( + erase=True, write=True, verify=True, firmware_file=FIRMWARE_FILE, family=None + ) loader.connection = FakeConnection() loader.stm32 = Stm32Bootloader(loader.connection, device_family="F1", verbosity=5) diff --git a/tests/unit/devices_stm32flash.py b/tests/unit/devices_stm32flash.py index 7afb394..7872016 100644 --- a/tests/unit/devices_stm32flash.py +++ b/tests/unit/devices_stm32flash.py @@ -12,106 +12,1022 @@ F_NO_ME = "no-mass-erase" F_PEMPTY = "unknown" +# ruff: noqa: E501 +# ruff: noqa: W505 + DEVICE_TABLE = [ - # ID name SRAM-address-range FLASH-address-range PPS PSize Option-byte-addr-range System-mem-addr-range Flags */ + # ID name SRAM-address-range FLASH-address-range PPS PSize Option-byte-addr-range System-mem-addr-range Flags */ # C0 - # (0x443, "STM32C011xx" , 0x20001000, 0x20003000, 0x08000000, x , x, x , x , x , 0x1FFF0000, 0x1FFF1800, 0) - # (0x453, "STM32C031xx" , 0x20001000, 0x20001800, 0x08000000, x , x, x , x , x , 0x1FFF0000, 0x1FFF1800, 0) - # F0 - (0x440, "STM32F030x8/F05xxx" , 0x20000800, 0x20002000, 0x08000000, 0x08010000, 4, p_1k , 0x1FFFF800, 0x1FFFF80F, 0x1FFFEC00, 0x1FFFF800, 0), - (0x444, "STM32F03xx4/6" , 0x20000800, 0x20001000, 0x08000000, 0x08008000, 4, p_1k , 0x1FFFF800, 0x1FFFF80F, 0x1FFFEC00, 0x1FFFF800, 0), - (0x442, "STM32F030xC/F09xxx" , 0x20001800, 0x20008000, 0x08000000, 0x08040000, 2, p_2k , 0x1FFFF800, 0x1FFFF80F, 0x1FFFD800, 0x1FFFF800, F_OBLL), - (0x445, "STM32F04xxx/F070x6" , 0x20001800, 0x20001800, 0x08000000, 0x08008000, 4, p_1k , 0x1FFFF800, 0x1FFFF80F, 0x1FFFC400, 0x1FFFF800, 0), - (0x448, "STM32F070xB/F071xx/F072xx" , 0x20001800, 0x20004000, 0x08000000, 0x08020000, 2, p_2k , 0x1FFFF800, 0x1FFFF80F, 0x1FFFC800, 0x1FFFF800, 0), - # F1 - (0x412, "STM32F10xxx Low-density" , 0x20000200, 0x20002800, 0x08000000, 0x08008000, 4, p_1k , 0x1FFFF800, 0x1FFFF80F, 0x1FFFF000, 0x1FFFF800, 0), - (0x410, "STM32F10xxx Medium-density" , 0x20000200, 0x20005000, 0x08000000, 0x08020000, 4, p_1k , 0x1FFFF800, 0x1FFFF80F, 0x1FFFF000, 0x1FFFF800, 0), - (0x414, "STM32F10xxx High-density" , 0x20000200, 0x20010000, 0x08000000, 0x08080000, 2, p_2k , 0x1FFFF800, 0x1FFFF80F, 0x1FFFF000, 0x1FFFF800, 0), - (0x420, "STM32F10xxx Medium-density VL" , 0x20000200, 0x20002000, 0x08000000, 0x08020000, 4, p_1k , 0x1FFFF800, 0x1FFFF80F, 0x1FFFF000, 0x1FFFF800, 0), - (0x428, "STM32F10xxx High-density VL" , 0x20000200, 0x20008000, 0x08000000, 0x08080000, 2, p_2k , 0x1FFFF800, 0x1FFFF80F, 0x1FFFF000, 0x1FFFF800, 0), - (0x418, "STM32F105xx/F107xx" , 0x20001000, 0x20010000, 0x08000000, 0x08040000, 2, p_2k , 0x1FFFF800, 0x1FFFF80F, 0x1FFFB000, 0x1FFFF800, 0), - (0x430, "STM32F10xxx XL-density" , 0x20000800, 0x20018000, 0x08000000, 0x08100000, 2, p_2k , 0x1FFFF800, 0x1FFFF80F, 0x1FFFE000, 0x1FFFF800, 0), - # F2 - (0x411, "STM32F2xxxx" , 0x20002000, 0x20020000, 0x08000000, 0x08100000, 1, f2f4 , 0x1FFFC000, 0x1FFFC00F, 0x1FFF0000, 0x1FFF7800, 0), - # F3 - (0x432, "STM32F373xx/F378xx" , 0x20001400, 0x20008000, 0x08000000, 0x08040000, 2, p_2k , 0x1FFFF800, 0x1FFFF80F, 0x1FFFD800, 0x1FFFF800, 0), - (0x422, "STM32F302xB(C)/F303xB(C)/F358xx" , 0x20001400, 0x2000A000, 0x08000000, 0x08040000, 2, p_2k , 0x1FFFF800, 0x1FFFF80F, 0x1FFFD800, 0x1FFFF800, 0), - (0x439, "STM32F301xx/F302x4(6/8)/F318xx" , 0x20001800, 0x20004000, 0x08000000, 0x08010000, 2, p_2k , 0x1FFFF800, 0x1FFFF80F, 0x1FFFD800, 0x1FFFF800, 0), - (0x438, "STM32F303x4(6/8)/F334xx/F328xx" , 0x20001800, 0x20003000, 0x08000000, 0x08010000, 2, p_2k , 0x1FFFF800, 0x1FFFF80F, 0x1FFFD800, 0x1FFFF800, 0), - (0x446, "STM32F302xD(E)/F303xD(E)/F398xx" , 0x20001800, 0x20010000, 0x08000000, 0x08080000, 2, p_2k , 0x1FFFF800, 0x1FFFF80F, 0x1FFFD800, 0x1FFFF800, 0), - # F4 - (0x413, "STM32F40xxx/41xxx" , 0x20003000, 0x20020000, 0x08000000, 0x08100000, 1, f2f4 , 0x1FFFC000, 0x1FFFC00F, 0x1FFF0000, 0x1FFF7800, 0), - (0x419, "STM32F42xxx/43xxx" , 0x20003000, 0x20030000, 0x08000000, 0x08200000, 1, f4db , 0x1FFEC000, 0x1FFFC00F, 0x1FFF0000, 0x1FFF7800, 0), - (0x423, "STM32F401xB(C)" , 0x20003000, 0x20010000, 0x08000000, 0x08040000, 1, f2f4 , 0x1FFFC000, 0x1FFFC00F, 0x1FFF0000, 0x1FFF7800, 0), - (0x433, "STM32F401xD(E)" , 0x20003000, 0x20018000, 0x08000000, 0x08080000, 1, f2f4 , 0x1FFFC000, 0x1FFFC00F, 0x1FFF0000, 0x1FFF7800, 0), - (0x458, "STM32F410xx" , 0x20003000, 0x20008000, 0x08000000, 0x08020000, 1, f2f4 , 0x1FFFC000, 0x1FFFC00F, 0x1FFF0000, 0x1FFF7800, 0), - (0x431, "STM32F411xx" , 0x20003000, 0x20020000, 0x08000000, 0x08080000, 1, f2f4 , 0x1FFFC000, 0x1FFFC00F, 0x1FFF0000, 0x1FFF7800, 0), - (0x441, "STM32F412xx" , 0x20003000, 0x20040000, 0x08000000, 0x08100000, 1, f2f4 , 0x1FFFC000, 0x1FFFC00F, 0x1FFF0000, 0x1FFF7800, 0), - (0x421, "STM32F446xx" , 0x20003000, 0x20020000, 0x08000000, 0x08080000, 1, f2f4 , 0x1FFFC000, 0x1FFFC00F, 0x1FFF0000, 0x1FFF7800, 0), - (0x434, "STM32F469xx/479xx" , 0x20003000, 0x20060000, 0x08000000, 0x08200000, 1, f4db , 0x1FFEC000, 0x1FFFC00F, 0x1FFF0000, 0x1FFF7800, 0), - (0x463, "STM32F413xx/423xx" , 0x20003000, 0x20050000, 0x08000000, 0x08180000, 1, f2f4 , 0x1FFFC000, 0x1FFFC00F, 0x1FFF0000, 0x1FFF7800, 0), - # F7 - (0x452, "STM32F72xxx/73xxx" , 0x20004000, 0x20040000, 0x08000000, 0x08080000, 1, f2f4 , 0x1FFF0000, 0x1FFF001F, 0x1FF00000, 0x1FF0EDC0, 0), - (0x449, "STM32F74xxx/75xxx" , 0x20004000, 0x20050000, 0x08000000, 0x08100000, 1, f7 , 0x1FFF0000, 0x1FFF001F, 0x1FF00000, 0x1FF0EDC0, 0), - (0x451, "STM32F76xxx/77xxx" , 0x20004000, 0x20080000, 0x08000000, 0x08200000, 1, f7 , 0x1FFF0000, 0x1FFF001F, 0x1FF00000, 0x1FF0EDC0, 0), - # G0 - (0x466, "STM32G03xxx/04xxx" , 0x20001000, 0x20002000, 0x08000000, 0x08010000, 1, p_2k , 0x1FFF7800, 0x1FFF787F, 0x1FFF0000, 0x1FFF2000, 0), - (0x460, "STM32G07xxx/08xxx" , 0x20002700, 0x20009000, 0x08000000, 0x08020000, 1, p_2k , 0x1FFF7800, 0x1FFF787F, 0x1FFF0000, 0x1FFF7000, 0), - (0x467, "STM32G0B0/B1/C1xx" , 0x20004000, 0x20020000, 0x08000000, 0x08080000, 1, p_2k , 0x1FFF7800, 0x1FFF787F, 0x1FFF0000, 0x1FFF7000, 0), - (0x456, "STM32G05xxx/061xx" , 0x20001000, 0x20004800, 0x08000000, 0x08010000, 1, p_2k , 0x1FFF7800, 0x1FFF787F, 0x1FFF0000, 0x1FFF2000, 0), - # G4 - (0x468, "STM32G431xx/441xx" , 0x20004000, 0x20005800, 0x08000000, 0x08020000, 1, p_2k , 0x1FFF7800, 0x1FFF782F, 0x1FFF0000, 0x1FFF7000, 0), - (0x469, "STM32G47xxx/48xxx" , 0x20004000, 0x20018000, 0x08000000, 0x08080000, 1, p_2k , 0x1FFF7800, 0x1FFF782F, 0x1FFF0000, 0x1FFF7000, 0), - (0x479, "STM32G491xx/A1xx" , 0x20004000, 0x2001C000, 0x08000000, 0x08080000, 1, p_2k , 0x1FFF7800, 0x1FFF782F, 0x1FFF0000, 0x1FFF7000, 0), - # H7 - (0x483, "STM32H72xxx/73xxx" , 0x20004100, 0x20020000, 0x08000000, 0x08100000, 1, p_128k, 0 , 0 , 0x1FF00000, 0x1FF1E800, 0), - (0x450, "STM32H74xxx/75xxx" , 0x20004100, 0x20020000, 0x08000000, 0x08200000, 1, p_128k, 0 , 0 , 0x1FF00000, 0x1FF1E800, 0), - (0x480, "STM32H7A3xx/B3xx" , 0x20004100, 0x20020000, 0x08000000, 0x08100000, 1, p_8k , 0 , 0 , 0x1FF00000, 0x1FF14000, 0), - # L0 - (0x457, "STM32L01xxx/02xxx" , 0x20000800, 0x20000800, 0x08000000, 0x08004000, 32, p_128 , 0x1FF80000, 0x1FF8001F, 0x1FF00000, 0x1FF01000, F_NO_ME), - (0x425, "STM32L031xx/041xx" , 0x20001000, 0x20002000, 0x08000000, 0x08008000, 32, p_128 , 0x1FF80000, 0x1FF8001F, 0x1FF00000, 0x1FF01000, F_NO_ME), - (0x417, "STM32L05xxx/06xxx" , 0x20001000, 0x20002000, 0x08000000, 0x08010000, 32, p_128 , 0x1FF80000, 0x1FF8001F, 0x1FF00000, 0x1FF01000, F_NO_ME), - (0x447, "STM32L07xxx/08xxx" , 0x20002000, 0x20005000, 0x08000000, 0x08030000, 32, p_128 , 0x1FF80000, 0x1FF8001F, 0x1FF00000, 0x1FF02000, F_NO_ME), - # L1 - (0x416, "STM32L1xxx6(8/B)" , 0x20000800, 0x20004000, 0x08000000, 0x08020000, 16, p_256 , 0x1FF80000, 0x1FF8001F, 0x1FF00000, 0x1FF01000, F_NO_ME), - (0x429, "STM32L1xxx6(8/B)A" , 0x20001000, 0x20008000, 0x08000000, 0x08020000, 16, p_256 , 0x1FF80000, 0x1FF8001F, 0x1FF00000, 0x1FF01000, F_NO_ME), - (0x427, "STM32L1xxxC" , 0x20001000, 0x20008000, 0x08000000, 0x08040000, 16, p_256 , 0x1FF80000, 0x1FF8001F, 0x1FF00000, 0x1FF02000, F_NO_ME), - (0x436, "STM32L1xxxD" , 0x20001000, 0x2000C000, 0x08000000, 0x08060000, 16, p_256 , 0x1FF80000, 0x1FF8001F, 0x1FF00000, 0x1FF02000, F_NO_ME), - (0x437, "STM32L1xxxE" , 0x20001000, 0x20014000, 0x08000000, 0x08080000, 16, p_256 , 0x1FF80000, 0x1FF8001F, 0x1FF00000, 0x1FF02000, F_NO_ME), - # L4 - (0x464, "STM32L412xx/422xx" , 0x20003100, 0x20008000, 0x08000000, 0x08020000, 1, p_2k , 0x1FFF7800, 0x1FFF780F, 0x1FFF0000, 0x1FFF7000, 0), - (0x435, "STM32L43xxx/44xxx" , 0x20003100, 0x2000C000, 0x08000000, 0x08040000, 1, p_2k , 0x1FFF7800, 0x1FFF780F, 0x1FFF0000, 0x1FFF7000, 0), - (0x462, "STM32L45xxx/46xxx" , 0x20003100, 0x20020000, 0x08000000, 0x08080000, 1, p_2k , 0x1FFF7800, 0x1FFF780F, 0x1FFF0000, 0x1FFF7000, F_PEMPTY), - (0x415, "STM32L47xxx/48xxx" , 0x20003100, 0x20018000, 0x08000000, 0x08100000, 1, p_2k , 0x1FFF7800, 0x1FFFF80F, 0x1FFF0000, 0x1FFF7000, 0), - (0x461, "STM32L496xx/4A6xx" , 0x20003100, 0x20040000, 0x08000000, 0x08100000, 1, p_2k , 0x1FFF7800, 0x1FFFF80F, 0x1FFF0000, 0x1FFF7000, 0), - (0x470, "STM32L4Rxx/4Sxx" , 0x20003200, 0x200A0000, 0x08000000, 0x08100000, 1, p_2k , 0x1FFF7800, 0x1FFFF80F, 0x1FFF0000, 0x1FFF7000, 0), - (0x471, "STM32L4P5xx/Q5xx" , 0x20004000, 0x20050000, 0x08000000, 0x08100000, 1, p_4k , 0x1FF00000, 0x1FF0000F, 0x1FFF0000, 0x1FFF7000, 0), # dual-bank - # L5 - (0x472, "STM32L552xx/562xx" , 0x20004000, 0x20040000, 0x08000000, 0x08080000, 1, p_2k , 0 , 0 , 0x0BF90000, 0x0BF98000, 0), # dual-bank - # WB - (0x494, "STM32WB10xx/15xx" , 0x20005000, 0x20040000, 0x08000000, 0x08050000, 1, p_2k , 0x1FFF7800, 0x1FFF787F, 0x1FFF0000, 0x1FFF7000, 0), - (0x495, "STM32WB30(5)xx/50(5)xx" , 0x20004000, 0x2000C000, 0x08000000, 0x08100000, 1, p_4k , 0x1FFF8000, 0x1FFF807F, 0x1FFF0000, 0x1FFF7000, 0), - # WL - (0x497, "STM32WLE5xx/WL55xx" , 0x20002000, 0x20010000, 0x08000000, 0x08040000, 1, p_2k , 0x1FFF7800, 0x1FFF8000, 0x1FFF0000, 0x1FFF4000, 0), - # U5 - (0x482, "STM32U575xx/585xx" , 0x20004000, 0x200C0000, 0x08000000, 0x08200000, 1, p_8k , 0 , 0 , 0x0BF90000, 0x0BFA0000, 0), - # These are not (yet) in AN2606: - (0x641, "Medium_Density PL" , 0x20000200, 0x20005000, 0x08000000, 0x08020000, 4, p_1k , 0x1FFFF800, 0x1FFFF80F, 0x1FFFF000, 0x1FFFF800, 0), - (0x9a8, "STM32W-128K" , 0x20000200, 0x20002000, 0x08000000, 0x08020000, 4, p_1k , 0x08040800, 0x0804080F, 0x08040000, 0x08040800, 0), - (0x9b0, "STM32W-256K" , 0x20000200, 0x20004000, 0x08000000, 0x08040000, 4, p_2k , 0x08040800, 0x0804080F, 0x08040000, 0x08040800, 0), + # (0x443, "STM32C011xx" , 0x20001000, 0x20003000, 0x08000000, x , x, x , x , x , 0x1FFF0000, 0x1FFF1800, 0) + # (0x453, "STM32C031xx" , 0x20001000, 0x20001800, 0x08000000, x , x, x , x , x , 0x1FFF0000, 0x1FFF1800, 0) + # F0 + ( + 0x440, + "STM32F030x8/F05xxx", + 0x20000800, + 0x20002000, + 0x08000000, + 0x08010000, + 4, + p_1k, + 0x1FFFF800, + 0x1FFFF80F, + 0x1FFFEC00, + 0x1FFFF800, + 0, + ), + ( + 0x444, + "STM32F03xx4/6", + 0x20000800, + 0x20001000, + 0x08000000, + 0x08008000, + 4, + p_1k, + 0x1FFFF800, + 0x1FFFF80F, + 0x1FFFEC00, + 0x1FFFF800, + 0, + ), + ( + 0x442, + "STM32F030xC/F09xxx", + 0x20001800, + 0x20008000, + 0x08000000, + 0x08040000, + 2, + p_2k, + 0x1FFFF800, + 0x1FFFF80F, + 0x1FFFD800, + 0x1FFFF800, + F_OBLL, + ), + ( + 0x445, + "STM32F04xxx/F070x6", + 0x20001800, + 0x20001800, + 0x08000000, + 0x08008000, + 4, + p_1k, + 0x1FFFF800, + 0x1FFFF80F, + 0x1FFFC400, + 0x1FFFF800, + 0, + ), + ( + 0x448, + "STM32F070xB/F071xx/F072xx", + 0x20001800, + 0x20004000, + 0x08000000, + 0x08020000, + 2, + p_2k, + 0x1FFFF800, + 0x1FFFF80F, + 0x1FFFC800, + 0x1FFFF800, + 0, + ), + # F1 + ( + 0x412, + "STM32F10xxx Low-density", + 0x20000200, + 0x20002800, + 0x08000000, + 0x08008000, + 4, + p_1k, + 0x1FFFF800, + 0x1FFFF80F, + 0x1FFFF000, + 0x1FFFF800, + 0, + ), + ( + 0x410, + "STM32F10xxx Medium-density", + 0x20000200, + 0x20005000, + 0x08000000, + 0x08020000, + 4, + p_1k, + 0x1FFFF800, + 0x1FFFF80F, + 0x1FFFF000, + 0x1FFFF800, + 0, + ), + ( + 0x414, + "STM32F10xxx High-density", + 0x20000200, + 0x20010000, + 0x08000000, + 0x08080000, + 2, + p_2k, + 0x1FFFF800, + 0x1FFFF80F, + 0x1FFFF000, + 0x1FFFF800, + 0, + ), + ( + 0x420, + "STM32F10xxx Medium-density VL", + 0x20000200, + 0x20002000, + 0x08000000, + 0x08020000, + 4, + p_1k, + 0x1FFFF800, + 0x1FFFF80F, + 0x1FFFF000, + 0x1FFFF800, + 0, + ), + ( + 0x428, + "STM32F10xxx High-density VL", + 0x20000200, + 0x20008000, + 0x08000000, + 0x08080000, + 2, + p_2k, + 0x1FFFF800, + 0x1FFFF80F, + 0x1FFFF000, + 0x1FFFF800, + 0, + ), + ( + 0x418, + "STM32F105xx/F107xx", + 0x20001000, + 0x20010000, + 0x08000000, + 0x08040000, + 2, + p_2k, + 0x1FFFF800, + 0x1FFFF80F, + 0x1FFFB000, + 0x1FFFF800, + 0, + ), + ( + 0x430, + "STM32F10xxx XL-density", + 0x20000800, + 0x20018000, + 0x08000000, + 0x08100000, + 2, + p_2k, + 0x1FFFF800, + 0x1FFFF80F, + 0x1FFFE000, + 0x1FFFF800, + 0, + ), + # F2 + ( + 0x411, + "STM32F2xxxx", + 0x20002000, + 0x20020000, + 0x08000000, + 0x08100000, + 1, + f2f4, + 0x1FFFC000, + 0x1FFFC00F, + 0x1FFF0000, + 0x1FFF7800, + 0, + ), + # F3 + ( + 0x432, + "STM32F373xx/F378xx", + 0x20001400, + 0x20008000, + 0x08000000, + 0x08040000, + 2, + p_2k, + 0x1FFFF800, + 0x1FFFF80F, + 0x1FFFD800, + 0x1FFFF800, + 0, + ), + ( + 0x422, + "STM32F302xB(C)/F303xB(C)/F358xx", + 0x20001400, + 0x2000A000, + 0x08000000, + 0x08040000, + 2, + p_2k, + 0x1FFFF800, + 0x1FFFF80F, + 0x1FFFD800, + 0x1FFFF800, + 0, + ), + ( + 0x439, + "STM32F301xx/F302x4(6/8)/F318xx", + 0x20001800, + 0x20004000, + 0x08000000, + 0x08010000, + 2, + p_2k, + 0x1FFFF800, + 0x1FFFF80F, + 0x1FFFD800, + 0x1FFFF800, + 0, + ), + ( + 0x438, + "STM32F303x4(6/8)/F334xx/F328xx", + 0x20001800, + 0x20003000, + 0x08000000, + 0x08010000, + 2, + p_2k, + 0x1FFFF800, + 0x1FFFF80F, + 0x1FFFD800, + 0x1FFFF800, + 0, + ), + ( + 0x446, + "STM32F302xD(E)/F303xD(E)/F398xx", + 0x20001800, + 0x20010000, + 0x08000000, + 0x08080000, + 2, + p_2k, + 0x1FFFF800, + 0x1FFFF80F, + 0x1FFFD800, + 0x1FFFF800, + 0, + ), + # F4 + ( + 0x413, + "STM32F40xxx/41xxx", + 0x20003000, + 0x20020000, + 0x08000000, + 0x08100000, + 1, + f2f4, + 0x1FFFC000, + 0x1FFFC00F, + 0x1FFF0000, + 0x1FFF7800, + 0, + ), + ( + 0x419, + "STM32F42xxx/43xxx", + 0x20003000, + 0x20030000, + 0x08000000, + 0x08200000, + 1, + f4db, + 0x1FFEC000, + 0x1FFFC00F, + 0x1FFF0000, + 0x1FFF7800, + 0, + ), + ( + 0x423, + "STM32F401xB(C)", + 0x20003000, + 0x20010000, + 0x08000000, + 0x08040000, + 1, + f2f4, + 0x1FFFC000, + 0x1FFFC00F, + 0x1FFF0000, + 0x1FFF7800, + 0, + ), + ( + 0x433, + "STM32F401xD(E)", + 0x20003000, + 0x20018000, + 0x08000000, + 0x08080000, + 1, + f2f4, + 0x1FFFC000, + 0x1FFFC00F, + 0x1FFF0000, + 0x1FFF7800, + 0, + ), + ( + 0x458, + "STM32F410xx", + 0x20003000, + 0x20008000, + 0x08000000, + 0x08020000, + 1, + f2f4, + 0x1FFFC000, + 0x1FFFC00F, + 0x1FFF0000, + 0x1FFF7800, + 0, + ), + ( + 0x431, + "STM32F411xx", + 0x20003000, + 0x20020000, + 0x08000000, + 0x08080000, + 1, + f2f4, + 0x1FFFC000, + 0x1FFFC00F, + 0x1FFF0000, + 0x1FFF7800, + 0, + ), + ( + 0x441, + "STM32F412xx", + 0x20003000, + 0x20040000, + 0x08000000, + 0x08100000, + 1, + f2f4, + 0x1FFFC000, + 0x1FFFC00F, + 0x1FFF0000, + 0x1FFF7800, + 0, + ), + ( + 0x421, + "STM32F446xx", + 0x20003000, + 0x20020000, + 0x08000000, + 0x08080000, + 1, + f2f4, + 0x1FFFC000, + 0x1FFFC00F, + 0x1FFF0000, + 0x1FFF7800, + 0, + ), + ( + 0x434, + "STM32F469xx/479xx", + 0x20003000, + 0x20060000, + 0x08000000, + 0x08200000, + 1, + f4db, + 0x1FFEC000, + 0x1FFFC00F, + 0x1FFF0000, + 0x1FFF7800, + 0, + ), + ( + 0x463, + "STM32F413xx/423xx", + 0x20003000, + 0x20050000, + 0x08000000, + 0x08180000, + 1, + f2f4, + 0x1FFFC000, + 0x1FFFC00F, + 0x1FFF0000, + 0x1FFF7800, + 0, + ), + # F7 + ( + 0x452, + "STM32F72xxx/73xxx", + 0x20004000, + 0x20040000, + 0x08000000, + 0x08080000, + 1, + f2f4, + 0x1FFF0000, + 0x1FFF001F, + 0x1FF00000, + 0x1FF0EDC0, + 0, + ), + ( + 0x449, + "STM32F74xxx/75xxx", + 0x20004000, + 0x20050000, + 0x08000000, + 0x08100000, + 1, + f7, + 0x1FFF0000, + 0x1FFF001F, + 0x1FF00000, + 0x1FF0EDC0, + 0, + ), + ( + 0x451, + "STM32F76xxx/77xxx", + 0x20004000, + 0x20080000, + 0x08000000, + 0x08200000, + 1, + f7, + 0x1FFF0000, + 0x1FFF001F, + 0x1FF00000, + 0x1FF0EDC0, + 0, + ), + # G0 + ( + 0x466, + "STM32G03xxx/04xxx", + 0x20001000, + 0x20002000, + 0x08000000, + 0x08010000, + 1, + p_2k, + 0x1FFF7800, + 0x1FFF787F, + 0x1FFF0000, + 0x1FFF2000, + 0, + ), + ( + 0x460, + "STM32G07xxx/08xxx", + 0x20002700, + 0x20009000, + 0x08000000, + 0x08020000, + 1, + p_2k, + 0x1FFF7800, + 0x1FFF787F, + 0x1FFF0000, + 0x1FFF7000, + 0, + ), + ( + 0x467, + "STM32G0B0/B1/C1xx", + 0x20004000, + 0x20020000, + 0x08000000, + 0x08080000, + 1, + p_2k, + 0x1FFF7800, + 0x1FFF787F, + 0x1FFF0000, + 0x1FFF7000, + 0, + ), + ( + 0x456, + "STM32G05xxx/061xx", + 0x20001000, + 0x20004800, + 0x08000000, + 0x08010000, + 1, + p_2k, + 0x1FFF7800, + 0x1FFF787F, + 0x1FFF0000, + 0x1FFF2000, + 0, + ), + # G4 + ( + 0x468, + "STM32G431xx/441xx", + 0x20004000, + 0x20005800, + 0x08000000, + 0x08020000, + 1, + p_2k, + 0x1FFF7800, + 0x1FFF782F, + 0x1FFF0000, + 0x1FFF7000, + 0, + ), + ( + 0x469, + "STM32G47xxx/48xxx", + 0x20004000, + 0x20018000, + 0x08000000, + 0x08080000, + 1, + p_2k, + 0x1FFF7800, + 0x1FFF782F, + 0x1FFF0000, + 0x1FFF7000, + 0, + ), + ( + 0x479, + "STM32G491xx/A1xx", + 0x20004000, + 0x2001C000, + 0x08000000, + 0x08080000, + 1, + p_2k, + 0x1FFF7800, + 0x1FFF782F, + 0x1FFF0000, + 0x1FFF7000, + 0, + ), + # H7 + ( + 0x483, + "STM32H72xxx/73xxx", + 0x20004100, + 0x20020000, + 0x08000000, + 0x08100000, + 1, + p_128k, + 0, + 0, + 0x1FF00000, + 0x1FF1E800, + 0, + ), + ( + 0x450, + "STM32H74xxx/75xxx", + 0x20004100, + 0x20020000, + 0x08000000, + 0x08200000, + 1, + p_128k, + 0, + 0, + 0x1FF00000, + 0x1FF1E800, + 0, + ), + ( + 0x480, + "STM32H7A3xx/B3xx", + 0x20004100, + 0x20020000, + 0x08000000, + 0x08100000, + 1, + p_8k, + 0, + 0, + 0x1FF00000, + 0x1FF14000, + 0, + ), + # L0 + ( + 0x457, + "STM32L01xxx/02xxx", + 0x20000800, + 0x20000800, + 0x08000000, + 0x08004000, + 32, + p_128, + 0x1FF80000, + 0x1FF8001F, + 0x1FF00000, + 0x1FF01000, + F_NO_ME, + ), + ( + 0x425, + "STM32L031xx/041xx", + 0x20001000, + 0x20002000, + 0x08000000, + 0x08008000, + 32, + p_128, + 0x1FF80000, + 0x1FF8001F, + 0x1FF00000, + 0x1FF01000, + F_NO_ME, + ), + ( + 0x417, + "STM32L05xxx/06xxx", + 0x20001000, + 0x20002000, + 0x08000000, + 0x08010000, + 32, + p_128, + 0x1FF80000, + 0x1FF8001F, + 0x1FF00000, + 0x1FF01000, + F_NO_ME, + ), + ( + 0x447, + "STM32L07xxx/08xxx", + 0x20002000, + 0x20005000, + 0x08000000, + 0x08030000, + 32, + p_128, + 0x1FF80000, + 0x1FF8001F, + 0x1FF00000, + 0x1FF02000, + F_NO_ME, + ), + # L1 + ( + 0x416, + "STM32L1xxx6(8/B)", + 0x20000800, + 0x20004000, + 0x08000000, + 0x08020000, + 16, + p_256, + 0x1FF80000, + 0x1FF8001F, + 0x1FF00000, + 0x1FF01000, + F_NO_ME, + ), + ( + 0x429, + "STM32L1xxx6(8/B)A", + 0x20001000, + 0x20008000, + 0x08000000, + 0x08020000, + 16, + p_256, + 0x1FF80000, + 0x1FF8001F, + 0x1FF00000, + 0x1FF01000, + F_NO_ME, + ), + ( + 0x427, + "STM32L1xxxC", + 0x20001000, + 0x20008000, + 0x08000000, + 0x08040000, + 16, + p_256, + 0x1FF80000, + 0x1FF8001F, + 0x1FF00000, + 0x1FF02000, + F_NO_ME, + ), + ( + 0x436, + "STM32L1xxxD", + 0x20001000, + 0x2000C000, + 0x08000000, + 0x08060000, + 16, + p_256, + 0x1FF80000, + 0x1FF8001F, + 0x1FF00000, + 0x1FF02000, + F_NO_ME, + ), + ( + 0x437, + "STM32L1xxxE", + 0x20001000, + 0x20014000, + 0x08000000, + 0x08080000, + 16, + p_256, + 0x1FF80000, + 0x1FF8001F, + 0x1FF00000, + 0x1FF02000, + F_NO_ME, + ), + # L4 + ( + 0x464, + "STM32L412xx/422xx", + 0x20003100, + 0x20008000, + 0x08000000, + 0x08020000, + 1, + p_2k, + 0x1FFF7800, + 0x1FFF780F, + 0x1FFF0000, + 0x1FFF7000, + 0, + ), + ( + 0x435, + "STM32L43xxx/44xxx", + 0x20003100, + 0x2000C000, + 0x08000000, + 0x08040000, + 1, + p_2k, + 0x1FFF7800, + 0x1FFF780F, + 0x1FFF0000, + 0x1FFF7000, + 0, + ), + ( + 0x462, + "STM32L45xxx/46xxx", + 0x20003100, + 0x20020000, + 0x08000000, + 0x08080000, + 1, + p_2k, + 0x1FFF7800, + 0x1FFF780F, + 0x1FFF0000, + 0x1FFF7000, + F_PEMPTY, + ), + ( + 0x415, + "STM32L47xxx/48xxx", + 0x20003100, + 0x20018000, + 0x08000000, + 0x08100000, + 1, + p_2k, + 0x1FFF7800, + 0x1FFFF80F, + 0x1FFF0000, + 0x1FFF7000, + 0, + ), + ( + 0x461, + "STM32L496xx/4A6xx", + 0x20003100, + 0x20040000, + 0x08000000, + 0x08100000, + 1, + p_2k, + 0x1FFF7800, + 0x1FFFF80F, + 0x1FFF0000, + 0x1FFF7000, + 0, + ), + ( + 0x470, + "STM32L4Rxx/4Sxx", + 0x20003200, + 0x200A0000, + 0x08000000, + 0x08100000, + 1, + p_2k, + 0x1FFF7800, + 0x1FFFF80F, + 0x1FFF0000, + 0x1FFF7000, + 0, + ), + ( + 0x471, + "STM32L4P5xx/Q5xx", + 0x20004000, + 0x20050000, + 0x08000000, + 0x08100000, + 1, + p_4k, + 0x1FF00000, + 0x1FF0000F, + 0x1FFF0000, + 0x1FFF7000, + 0, + ), # dual-bank + # L5 + ( + 0x472, + "STM32L552xx/562xx", + 0x20004000, + 0x20040000, + 0x08000000, + 0x08080000, + 1, + p_2k, + 0, + 0, + 0x0BF90000, + 0x0BF98000, + 0, + ), # dual-bank + # WB + ( + 0x494, + "STM32WB10xx/15xx", + 0x20005000, + 0x20040000, + 0x08000000, + 0x08050000, + 1, + p_2k, + 0x1FFF7800, + 0x1FFF787F, + 0x1FFF0000, + 0x1FFF7000, + 0, + ), + ( + 0x495, + "STM32WB30(5)xx/50(5)xx", + 0x20004000, + 0x2000C000, + 0x08000000, + 0x08100000, + 1, + p_4k, + 0x1FFF8000, + 0x1FFF807F, + 0x1FFF0000, + 0x1FFF7000, + 0, + ), + # WL + ( + 0x497, + "STM32WLE5xx/WL55xx", + 0x20002000, + 0x20010000, + 0x08000000, + 0x08040000, + 1, + p_2k, + 0x1FFF7800, + 0x1FFF8000, + 0x1FFF0000, + 0x1FFF4000, + 0, + ), + # U5 + ( + 0x482, + "STM32U575xx/585xx", + 0x20004000, + 0x200C0000, + 0x08000000, + 0x08200000, + 1, + p_8k, + 0, + 0, + 0x0BF90000, + 0x0BFA0000, + 0, + ), + # These are not (yet) in AN2606: + ( + 0x641, + "Medium_Density PL", + 0x20000200, + 0x20005000, + 0x08000000, + 0x08020000, + 4, + p_1k, + 0x1FFFF800, + 0x1FFFF80F, + 0x1FFFF000, + 0x1FFFF800, + 0, + ), + ( + 0x9A8, + "STM32W-128K", + 0x20000200, + 0x20002000, + 0x08000000, + 0x08020000, + 4, + p_1k, + 0x08040800, + 0x0804080F, + 0x08040000, + 0x08040800, + 0, + ), + ( + 0x9B0, + "STM32W-256K", + 0x20000200, + 0x20004000, + 0x08000000, + 0x08040000, + 4, + p_2k, + 0x08040800, + 0x0804080F, + 0x08040000, + 0x08040800, + 0, + ), ] KEYS = [ - "product_id", "device_name", - "ram_start", "ram_end", - "flash_start", "flash_end", - "pages_per_sector", "page_size", - "option_start", "option_end", - "system_mem_start", "system_mem_end", + "product_id", + "device_name", + "ram_start", + "ram_end", + "flash_start", + "flash_end", + "pages_per_sector", + "page_size", + "option_start", + "option_end", + "system_mem_start", + "system_mem_end", "flag", ] -DEVICES = [ - dict(zip(KEYS, details)) - for details in DEVICE_TABLE -] \ No newline at end of file +DEVICES = [dict(zip(KEYS, details)) for details in DEVICE_TABLE] diff --git a/tests/unit/test_bootloader.py b/tests/unit/test_bootloader.py index 5e52053..824918d 100644 --- a/tests/unit/test_bootloader.py +++ b/tests/unit/test_bootloader.py @@ -1,3 +1,4 @@ +"""Unit tests for the Stm32Loader class.""" from unittest.mock import MagicMock @@ -191,7 +192,7 @@ def test_extended_erase_memory_with_pages_sends_two_byte_sector_count(bootloader assert write.data_was_written(b"\x00\x03") -def test_extended_erase_memory_with_pages_sends_two_byte_sector_addresses_with_single_byte_checksum( +def test_extended_erase_with_pages_sends_two_byte_sector_addresses_with_single_byte_checksum( bootloader, write ): bootloader.extended_erase_memory([0x01, 0x02, 0x04, 0x0FF0]) @@ -207,14 +208,14 @@ def test_verify_data_with_identical_data_passes(): Stm32Bootloader.verify_data(b"\x05", b"\x05") -def test_verify_data_with_different_byte_count_raises_verify_error_complaining_about_length_difference(): +def test_verify_different_byte_count_raises_verify_error_complaining_about_length_difference(): with pytest.raises( Stm32.DataMismatchError, match=r"Data length does not match.*2.*vs.*1.*bytes" ): Stm32Bootloader.verify_data(b"\x05\x06", b"\x01") -def test_verify_data_with_non_identical_data_raises_verify_error_complaining_about_mismatched_byte(): +def test_verify_non_identical_data_raises_verify_error_complaining_about_mismatched_byte(): with pytest.raises( Stm32.DataMismatchError, match=r"Verification data does not match read data.*mismatch.*0x1.*0x6.*0x7", @@ -241,7 +242,7 @@ def test_get_uid_for_known_device_reads_at_correct_address(connection, pid_bid, def test_get_uid_for_family_without_uid_returns_uid_not_supported(connection): - device = DEVICES.get(( 0x443, 0x51)) + device = DEVICES.get((0x443, 0x51)) bootloader = Stm32Bootloader(connection, device=device) assert bootloader.UID_NOT_SUPPORTED == bootloader.get_uid() @@ -275,7 +276,10 @@ def test_get_flash_size_and_uid_for_exception_families_returns_size_and_uid(conn [ (Stm32Bootloader.UID_NOT_SUPPORTED, "UID not supported in this part"), (Stm32Bootloader.UID_ADDRESS_UNKNOWN, "UID address unknown"), - (bytearray(b"\x12\x34\x56\x78\x9a\xbc\xde\x01\x12\x34\x56\x78"), "3412-7856-01DEBC9A-78563412"), + ( + bytearray(b"\x12\x34\x56\x78\x9a\xbc\xde\x01\x12\x34\x56\x78"), + "3412-7856-01DEBC9A-78563412", + ), ], ) def test_format_uid_returns_correct_string(bootloader, uid_string): diff --git a/tests/unit/test_devices.py b/tests/unit/test_devices.py index ef1c0f0..0fae6e6 100644 --- a/tests/unit/test_devices.py +++ b/tests/unit/test_devices.py @@ -1,10 +1,9 @@ - import pytest - -from stm32loader.devices import DEVICES, DEVICE_FAMILIES, DeviceFamily -from stm32loader.bootloader import CHIP_IDS, Stm32Bootloader from devices_stm32flash import DEVICES as STM32FLASH_DEVICES +from stm32loader.bootloader import CHIP_IDS, Stm32Bootloader +from stm32loader.device_family import DEVICE_FAMILIES, DeviceFamily +from stm32loader.devices import DEVICES KNOWN_DUPLICATE_DEVICE_NAMES = [ "STM32F2xxxx", @@ -16,7 +15,7 @@ "STM32F10xxx", "BlueNRG-1", "BlueNRG-2", - "STM32W" + "STM32W", ] KNOWN_RAM_EXCEPTIONS = [ @@ -82,7 +81,9 @@ def test_flash_size_multiple_of_16k(dev): assert isinstance(dev.flash_size, int) assert dev.flash_size > 0, f"{dev} flash size not None but still too low: {dev.flash_size}" - assert dev.flash_size % (16 * 1024) == 0, f"{dev} flash size not a multiple of 64: {dev.flash_size}" + assert dev.flash_size % (16 * 1024) == 0, ( + f"{dev} flash size not a multiple of 64: {dev.flash_size}" + ) @pytest.mark.parametrize( @@ -95,8 +96,12 @@ def test_system_memory_size_multiple_of_64(dev): return assert isinstance(dev.system_memory_size, int) - assert dev.system_memory_size > 0, f"{dev} system memory size not None but still too low: {dev.system_memory_size}" - assert dev.system_memory_size % 64 == 0, f"{dev} system memory size not a multiple of 64: {dev.system_memory_size}" + assert dev.system_memory_size > 0, ( + f"{dev} system memory size not None but still too low: {dev.system_memory_size}" + ) + assert dev.system_memory_size % 64 == 0, ( + f"{dev} system memory size not a multiple of 64: {dev.system_memory_size}" + ) @pytest.mark.parametrize( @@ -135,19 +140,55 @@ def test_stm32flash_device_names_match(device): break # Some devices don't exist in STM32Flash. - stm32loader_only = (0x443, 0x453, 0x474, 0x484, 0x492, 0x455, 0x481, 0x003, 0x00F, 0x0023, 0x002F, 0x801, 0x03B, 0x03F) + stm32loader_only = ( + 0x443, + 0x453, + 0x474, + 0x484, + 0x492, + 0x455, + 0x481, + 0x003, + 0x00F, + 0x0023, + 0x002F, + 0x801, + 0x03B, + 0x03F, + ) if not stm32flash_device and device.product_id in stm32loader_only: return # Known / reviewed deviating names. if device.product_id in [ - 0x440, 0x442, 0x445, 0x448, 0x412, 0x410, 0x414, 0x420, 0x428, 0x418, 0x430, 0x432, - 0x422, 0x439, 0X438, 0x446, 0x467, 0x495, 0x641, 0x9A8, 0x9B0, + 0x440, + 0x442, + 0x445, + 0x448, + 0x412, + 0x410, + 0x414, + 0x420, + 0x428, + 0x418, + 0x430, + 0x432, + 0x422, + 0x439, + 0x438, + 0x446, + 0x467, + 0x495, + 0x641, + 0x9A8, + 0x9B0, ]: return assert stm32flash_device, f"{device.device_name} 0x{device.product_id:03X}" - assert stm32flash_device["device_name"] == device.device_name, f"{device.device_name} 0x{device.product_id:03X}" + assert stm32flash_device["device_name"] == device.device_name, ( + f"{device.device_name} 0x{device.product_id:03X}" + ) @pytest.mark.parametrize( @@ -170,15 +211,23 @@ def test_stm32flash_ram_addresses_match(device): return if device.ram is None: - assert ref["ram_start"] == ref["ram_end"], f"RAM size not 0 for device '{device.device_name}' 0x{device.product_id:03X}" + assert ref["ram_start"] == ref["ram_end"], ( + f"RAM size not 0 for device '{device.device_name}' 0x{device.product_id:03X}" + ) return if isinstance(device.ram[0], tuple): return # print(hex(device.product_id), device, device.ram, ref) - assert device.ram[0] == ref["ram_start"], f"RAM start differs for device: '{device.device_name}' 0x{device.product_id:03X}: 0x{device.ram[0]:08X} vs 0x{ref['ram_start']:08X}." - assert device.ram[1] == ref["ram_end"], f"RAM end differs for device: '{device.device_name}' 0x{device.product_id:03X}: 0x{device.ram[1]:08X} vs 0x{ref['ram_end']:08X}." + assert device.ram[0] == ref["ram_start"], ( + f"RAM start differs for device: '{device.device_name}' 0x{device.product_id:03X}:" + f" 0x{device.ram[0]:08X} vs 0x{ref['ram_start']:08X}." + ) + assert device.ram[1] == ref["ram_end"], ( + f"RAM end differs for device: '{device.device_name}' 0x{device.product_id:03X}:" + f" 0x{device.ram[1]:08X} vs 0x{ref['ram_end']:08X}." + ) def test_family_uid_address_matches_existing(): @@ -188,7 +237,8 @@ def test_family_uid_address_matches_existing(): family = DeviceFamily[family_code] family_uid_address = DEVICE_FAMILIES[family].uid_address assert uid_address == family_uid_address, ( - f"Device family UID address does not match: '{family_code}': 0x{uid_address:08X} vs 0x{family_uid_address:08X}." + f"Device family UID address does not match: '{family_code}':" + f" 0x{uid_address:08X} vs 0x{family_uid_address:08X}." ) @@ -199,7 +249,8 @@ def test_family_flash_size_address_matches_existing(): family = DeviceFamily[family_code] family_size_address = DEVICE_FAMILIES[family].flash_size_address assert size_address == family_size_address, ( - f"Device family flash size address does not match: '{family_code}': 0x{size_address:08X} vs 0x{family_size_address:08X}." + f"Device family flash size address does not match: '{family_code}':" + f" 0x{size_address:08X} vs 0x{family_size_address:08X}." ) @@ -210,5 +261,6 @@ def test_family_transfer_size_matches_existing(): family = DeviceFamily[family_code] family_transfer_size = DEVICE_FAMILIES[family].transfer_size assert transfer_size == family_transfer_size, ( - f"Device family transfer size does not match: '{family_code}': 0x{transfer_size:08X} vs 0x{family_transfer_size:08X}." + f"Device family transfer size does not match: '{family_code}':" + f" 0x{transfer_size:08X} vs 0x{family_transfer_size:08X}." ) diff --git a/tests/unit/test_hexfile.py b/tests/unit/test_hexfile.py index 4af4c09..8f03744 100644 --- a/tests/unit/test_hexfile.py +++ b/tests/unit/test_hexfile.py @@ -1,12 +1,11 @@ from pathlib import Path +from stm32loader.hexfile import load_hex + HERE = Path(__file__).parent DATA = HERE / "../data" -from stm32loader.hexfile import load_hex - - def test_load_hex_delivers_bytes(): small_hex_path = DATA / "small.hex" data = load_hex(small_hex_path) From 13fe1fa76e0e415078bd20628497bd7022dc21f6 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 2 Mar 2026 10:52:56 +0100 Subject: [PATCH 345/369] dev: Temporarily disable some tests which need porting to device table --- tests/unit/test_bootloader.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/unit/test_bootloader.py b/tests/unit/test_bootloader.py index 824918d..a18e943 100644 --- a/tests/unit/test_bootloader.py +++ b/tests/unit/test_bootloader.py @@ -252,6 +252,8 @@ def test_get_uid_for_family_without_uid_returns_uid_not_supported(connection): ["F4", "L0"], ) def test_get_flash_size_and_uid_for_exception_families_returns_size_and_uid(connection, family): + pytest.skip(reason="Ditch 'family") + bootloader = Stm32Bootloader(connection, device_family=family) bootloader.read_memory = MagicMock() @@ -330,6 +332,7 @@ def test_get_flash_size_for_exception_family_uses_bulk_read(connection): def test_get_uid_for_exception_family_uses_bulk_read(connection): + pytest.skip(reason="Ditch 'family") bootloader = Stm32Bootloader(connection, device_family="L0") bootloader._get_flash_size_and_uid_bulk = MagicMock(return_value=(64, b"bulk uid")) @@ -338,6 +341,7 @@ def test_get_uid_for_exception_family_uses_bulk_read(connection): def test_get_flash_size_for_standard_family_uses_direct_read(connection): + pytest.skip(reason="Ditch 'family") bootloader = Stm32Bootloader(connection, device_family="F1") bootloader.read_memory = MagicMock(return_value=b"\x80\x00") # 128 KiB @@ -346,6 +350,7 @@ def test_get_flash_size_for_standard_family_uses_direct_read(connection): def test_flash_size_and_uid_are_cached(connection): + pytest.skip(reason="Ditch 'family") bootloader = Stm32Bootloader(connection, device_family="F1") bootloader.read_memory = MagicMock() bootloader.read_memory.side_effect = [b"\x80\x00", b"some uid 12b"] @@ -366,6 +371,7 @@ def test_flash_size_and_uid_are_cached(connection): def test_get_flash_size_and_uid_bulk_populates_both_caches(connection): + pytest.skip(reason="Ditch 'family") bootloader = Stm32Bootloader(connection, device_family="F4") # Mock the internal bulk read method via read_memory. bootloader.read_memory = MagicMock() From 7ff5833be076a126a6d2179180ac7169d6220c4c Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 2 Mar 2026 10:59:22 +0100 Subject: [PATCH 346/369] doc: Expand changelog for vnext --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e49945..a0d76a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ What changed in which version. +## vnext + +### Changed +* `#91` Drop the `--family` argument; do auto-detect instead. +* `#90` Move docs to `docs` folder. + ## [0.8.0] - TBD ### Added From 918a1674112d5c6612fca369c1a97724c81b66cf Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 2 Mar 2026 11:00:21 +0100 Subject: [PATCH 347/369] doc: Move docs into `docs` folder Fixes #90 --- ALTERNATIVES.md => docs/ALTERNATIVES.md | 0 CHANGELOG.md => docs/CHANGELOG.md | 0 DEVELOP.md => docs/DEVELOP.md | 0 EXTEND.md => docs/EXTEND.md | 0 RUN.md => docs/RUN.md | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename ALTERNATIVES.md => docs/ALTERNATIVES.md (100%) rename CHANGELOG.md => docs/CHANGELOG.md (100%) rename DEVELOP.md => docs/DEVELOP.md (100%) rename EXTEND.md => docs/EXTEND.md (100%) rename RUN.md => docs/RUN.md (100%) diff --git a/ALTERNATIVES.md b/docs/ALTERNATIVES.md similarity index 100% rename from ALTERNATIVES.md rename to docs/ALTERNATIVES.md diff --git a/CHANGELOG.md b/docs/CHANGELOG.md similarity index 100% rename from CHANGELOG.md rename to docs/CHANGELOG.md diff --git a/DEVELOP.md b/docs/DEVELOP.md similarity index 100% rename from DEVELOP.md rename to docs/DEVELOP.md diff --git a/EXTEND.md b/docs/EXTEND.md similarity index 100% rename from EXTEND.md rename to docs/EXTEND.md diff --git a/RUN.md b/docs/RUN.md similarity index 100% rename from RUN.md rename to docs/RUN.md From 4f86edea3b359eebcd4a344bea2f1921fe32a1cc Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 2 Mar 2026 11:24:37 +0100 Subject: [PATCH 348/369] fix: Use device table in get_flash_size Verified on STM32G4. --- src/stm32loader/bootloader.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/stm32loader/bootloader.py b/src/stm32loader/bootloader.py index 50fa5eb..d6e479d 100644 --- a/src/stm32loader/bootloader.py +++ b/src/stm32loader/bootloader.py @@ -497,7 +497,8 @@ def get_id(self): def get_flash_size(self): """Return the MCU's flash size in kilobytes.""" - if self.device_family in ["F4", "L0"]: + if self.device.flags & DeviceFlag.LONG_UID_ACCESS: + # F4, L0 families. flash_size, _uid = self._get_flash_size_and_uid_bulk() return flash_size return self._get_flash_size_raw() @@ -522,7 +523,7 @@ def get_uid(self): @lru_cache(maxsize=2) def _get_flash_size_raw(self): """Perform a direct 2-byte read of the flash size.""" - flash_size_address = self.FLASH_SIZE_ADDRESS[self.device_family] + flash_size_address = self.device.family.flash_size_address flash_size_bytes = self.read_memory(flash_size_address, 2) flash_size = flash_size_bytes[0] + (flash_size_bytes[1] << 8) return flash_size From 53cec49116a8321a4dbc3b5ec52bc2a70ef4f983 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 2 Mar 2026 15:43:24 +0100 Subject: [PATCH 349/369] clean(lint) --- src/stm32loader/bootloader.py | 34 ++++++++++------------------------ src/stm32loader/main.py | 1 - 2 files changed, 10 insertions(+), 25 deletions(-) diff --git a/src/stm32loader/bootloader.py b/src/stm32loader/bootloader.py index d6e479d..ed243c4 100644 --- a/src/stm32loader/bootloader.py +++ b/src/stm32loader/bootloader.py @@ -509,16 +509,21 @@ def get_uid(self): Return UID_NOT_SUPPORTED if the device does not have a UID. - Return UIT_ADDRESS_UNKNOWN if the address of the device's + Return UID_ADDRESS_UNKNOWN if the address of the device's UID is not known. - :return bytearray: UID bytes of the device, or 0 or -1 when + :return byterary: UID bytes of the device, or 0 or -1 when not available. """ - if self.device_family in ["F4", "L0"]: + if self.device.flags & DeviceFlag.LONG_UID_ACCESS: + # F4 and L0 families. _flash_size, uid = self._get_flash_size_and_uid_bulk() - return uid - return self._get_uid_raw() + else: + if not self.device.family.uid_address: + return self.UID_NOT_SUPPORTED + uid = self.read_memory(self.device.family.uid_address, 12) + + return uid @lru_cache(maxsize=2) def _get_flash_size_raw(self): @@ -568,25 +573,6 @@ def _get_flash_size_and_uid_bulk(self): return flash_size, device_uid - def get_uid(self): - """ - Send the 'Get UID' command and return the device UID. - - Return UID_NOT_SUPPORTED if the device does not have - a UID. - - :return byterary: UID bytes of the device, or 0 or -1 when - not available. - """ - if self.device.flags & DeviceFlag.LONG_UID_ACCESS: - _flash_size, uid = self.get_flash_size_and_uid() - else: - if not self.device.family.uid_address: - return self.UID_NOT_SUPPORTED - uid = self.read_memory(self.device.family.uid_address, 12) - - return uid - def detect_device(self) -> None: """Detect the device type and store in `device`.""" product_id = self.get_id() diff --git a/src/stm32loader/main.py b/src/stm32loader/main.py index 4cea2ec..c91db02 100644 --- a/src/stm32loader/main.py +++ b/src/stm32loader/main.py @@ -204,7 +204,6 @@ def detect_device(self) -> None: def read_device_uid(self): """Show chip UID.""" try: - flash_size = self.stm32.get_flash_size() device_uid = self.stm32.get_uid() except bootloader.CommandError as e: self.debug( From 588c3519c74623e35c84f1668cfa86d0f46158c2 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 2 Mar 2026 15:45:17 +0100 Subject: [PATCH 350/369] dev: Disable some tests which need porting --- tests/unit/test_bootloader.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/unit/test_bootloader.py b/tests/unit/test_bootloader.py index a18e943..25e688a 100644 --- a/tests/unit/test_bootloader.py +++ b/tests/unit/test_bootloader.py @@ -159,6 +159,7 @@ def test_erase_memory_with_page_count_higher_than_255_raises_page_index_error(bo def test_erase_memory_family_l0_without_pages_erases_individual_pages(connection, write): + pytest.skip("Port to device-table") bootloader = Stm32Bootloader(connection, device_family="L0") bootloader.command = MagicMock() bootloader._get_flash_size_and_uid_bulk = MagicMock() @@ -324,6 +325,7 @@ def test_get_uid_for_standard_family(connection): def test_get_flash_size_for_exception_family_uses_bulk_read(connection): + pytest.skip("Port to device-table") bootloader = Stm32Bootloader(connection, device_family="F4") bootloader._get_flash_size_and_uid_bulk = MagicMock(return_value=(512, b"bulk uid")) From 42395f9b54486e53e819f84f91c5c660f9e2d756 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Mon, 2 Mar 2026 16:21:32 +0100 Subject: [PATCH 351/369] dev: Add some more device info Inspiration: stm32flash commit 2e0cbb52569bb462cd0c28ab8e21173f14822c30 . --- src/stm32loader/device_info.py | 2 +- src/stm32loader/devices.py | 26 ++++++++++++++++++++++---- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/stm32loader/device_info.py b/src/stm32loader/device_info.py index ae4e272..2bddeb5 100644 --- a/src/stm32loader/device_info.py +++ b/src/stm32loader/device_info.py @@ -6,7 +6,7 @@ class DeviceInfo: # pylint: disable=too-many-instance-attributes - """Hold info about""" + """Hold info about an STM32 device.""" def __init__( # pylint: disable=too-many-positional-arguments,too-many-arguments self, diff --git a/src/stm32loader/devices.py b/src/stm32loader/devices.py index c228824..46977c5 100644 --- a/src/stm32loader/devices.py +++ b/src/stm32loader/devices.py @@ -12,16 +12,34 @@ # Based on ST AN2606 section "Device-dependent bootloader parameters". # Flash range, option bytes and flags gleaned from # stm32flash, dev_table.c. + + # Other possibly known devices from AN2606 rev 66: + # 0x44C STM32C051xx + # 0x493 STM32C071xx + # 0x44D STM32C091xx/92xx + # 0x474 STM32H503xx + # 0x484 STM32H563xx/573xx + # 0x478 STM32H523xx/33xxx + # 0x485 STM32H7Rxxx/7Sxxx + # 0x459 STM32U031xx + # 0x489 STM32U073xx/83xx + # 0x454 STM32U375xx/85xx + # 0x455 STM32U535xx/545xx + # 0x481 STM32U595xx/599xx/5A9xx + # 0x476 STM32U5F7xx/5F9xx/5G7xx/5G9xx + # 0x492 STM32WBA52xx/54xx/55xx + # 0x4B0 STM32WBA62xx/64xx/65xx + # FIXME flash? DeviceInfo( "C0", "STM32C011xx", 0x443, 0x51, - ram=(0x_2000_0000, 0x_2000_3000), + ram=(0x_2000_1000, 0x_2000_3000), system=(0x_1FFF_0000, 0x_1FFF_1800), - flash=None, - option=None, + flash=(0x_0800_0000, 0x_0800_8000), + option=(0x_1FFF_7800, 0x_1FFF_787F), ), # FIXME flash? # Error in AN2606? Ram is mentioned as 0x_2000_2000 - 0x_2000_17FF @@ -881,7 +899,7 @@ ram=(0x_2000_2000, 0x_2001_0000), system=(0x_1FFF_0000, 0x_1FFF_4000), flash=(0x_0800_0000, 0x_0804_0000, 2 * kB), - option=(0x_1FFF_7800, 0x_1FFF_8000), + option=(0x_1FFF_7800, 0x_1FFF_7FFF), bootloader_id_address=0x_1FFF_3EFE, ), # FIXME flash config? From 51c6cceb6aba38e596a89833de897885aaf92a5a Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 3 Mar 2026 13:25:30 +0100 Subject: [PATCH 352/369] clean(lint) --- src/stm32loader/devices.py | 2 -- tests/unit/test_bootloader.py | 6 +++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/stm32loader/devices.py b/src/stm32loader/devices.py index 46977c5..55400eb 100644 --- a/src/stm32loader/devices.py +++ b/src/stm32loader/devices.py @@ -12,7 +12,6 @@ # Based on ST AN2606 section "Device-dependent bootloader parameters". # Flash range, option bytes and flags gleaned from # stm32flash, dev_table.c. - # Other possibly known devices from AN2606 rev 66: # 0x44C STM32C051xx # 0x493 STM32C071xx @@ -29,7 +28,6 @@ # 0x476 STM32U5F7xx/5F9xx/5G7xx/5G9xx # 0x492 STM32WBA52xx/54xx/55xx # 0x4B0 STM32WBA62xx/64xx/65xx - # FIXME flash? DeviceInfo( "C0", diff --git a/tests/unit/test_bootloader.py b/tests/unit/test_bootloader.py index 25e688a..44b94df 100644 --- a/tests/unit/test_bootloader.py +++ b/tests/unit/test_bootloader.py @@ -271,7 +271,7 @@ def test_get_flash_size_and_uid_for_exception_families_returns_size_and_uid(conn bootloader.read_memory.return_value = memory_block assert bootloader.get_flash_size() == 0x0201 - assert bootloader.get_uid() == b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c' + assert bootloader.get_uid() == b"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c" @pytest.mark.parametrize( @@ -380,8 +380,8 @@ def test_get_flash_size_and_uid_bulk_populates_both_caches(connection): memory_block = bytearray([0] * 256) uid_offset = bootloader.UID_ADDRESS["F4"] & 0xFF flash_size_offset = bootloader.FLASH_SIZE_ADDRESS["F4"] & 0xFF - memory_block[uid_offset: uid_offset + 12] = b"bulk uid 12b" - memory_block[flash_size_offset: flash_size_offset + 2] = b"\x00\x02" # 512 + memory_block[uid_offset : uid_offset + 12] = b"bulk uid 12b" + memory_block[flash_size_offset : flash_size_offset + 2] = b"\x00\x02" # 512 bootloader.read_memory.return_value = memory_block assert bootloader.get_flash_size() == 512 From aebf74b910bf5e20b2b1a6d730322b5021ea8408 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 3 Mar 2026 15:10:27 +0100 Subject: [PATCH 353/369] dev: Properly update version numbers in __init__.py --- pyproject.toml | 10 +++++++--- src/stm32loader/__init__.py | 4 ++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 55229ae..6f4a962 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,7 @@ [project] name = "stm32loader" description = "Flash firmware to STM32 microcontrollers using Python." +version = "1.0.0-dev0" readme = "README.md" authors = [ {name = "jsnyder"}, @@ -27,7 +28,6 @@ dependencies = [ "pyserial", "progress", ] -version = "0.8.0" [build-system] requires = ["uv_build>=0.9.17,<0.10.0"] @@ -69,7 +69,6 @@ SourceCode = "https://github.com/florisla/stm32loader" [tool.bumpversion] -current_version = "0.7.1-dev0" commit = true tag = true message = "release: Bump version number from v{current_version} to v{new_version}" @@ -87,7 +86,7 @@ values = [ ] [[tool.bumpversion.files]] -filename = "stm32loader/__init__.py" +filename = "src/stm32loader/__init__.py" parse = "\\((?P\\d+),\\s(?P\\d+),\\s(?P\\d+)(\\s*,\\s*\"(?P[^\"]+)\"\\s*,\\s*(?P\\d+))?\\)" serialize = [ "({major}, {minor}, {patch}, \"{release}\", {devrelease})", @@ -96,6 +95,11 @@ serialize = [ search = "__version_info__ = {current_version}" replace = "__version_info__ = {new_version}" +[[tool.bumpversion.files]] +filename = "src/stm32loader/__init__.py" +search = "__version__ = \"{current_version}\"" +replace = "__version__ = \"{new_version}\"" + [tool.ruff] line-length = 98 diff --git a/src/stm32loader/__init__.py b/src/stm32loader/__init__.py index 37bb766..2cf4269 100644 --- a/src/stm32loader/__init__.py +++ b/src/stm32loader/__init__.py @@ -1,4 +1,4 @@ """Flash firmware to STM32 microcontrollers over a serial connection.""" -__version_info__ = (0, 7, 1, "dev", 0) -__version__ = "-".join(str(part) for part in __version_info__).replace("-", ".", 2) +__version_info__ = (1, 0, 0, "dev", 0) +__version__ = "1.0.0-dev0" From e4cf24ecb3e47e9e7c11d1175dde933dff71e68d Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 3 Mar 2026 15:13:04 +0100 Subject: [PATCH 354/369] release: Bump version number from v1.0.0-dev0 to v1.0.0-dev1 --- pyproject.toml | 2 +- src/stm32loader/__init__.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6f4a962..3c6737e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "stm32loader" description = "Flash firmware to STM32 microcontrollers using Python." -version = "1.0.0-dev0" +version = "1.0.0-dev1" readme = "README.md" authors = [ {name = "jsnyder"}, diff --git a/src/stm32loader/__init__.py b/src/stm32loader/__init__.py index 2cf4269..d81accd 100644 --- a/src/stm32loader/__init__.py +++ b/src/stm32loader/__init__.py @@ -1,4 +1,4 @@ """Flash firmware to STM32 microcontrollers over a serial connection.""" -__version_info__ = (1, 0, 0, "dev", 0) -__version__ = "1.0.0-dev0" +__version_info__ = (1, 0, 0, "dev", 1) +__version__ = "1.0.0-dev1" From 2d9484709713dcd76c9bed5a9a1be7bd3c084ffc Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Wed, 4 Mar 2026 11:00:01 +0100 Subject: [PATCH 355/369] fix(doc): Replace lint/test badges --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index aa255a9..fd356b4 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # STM32Loader [![PyPI package](https://badge.fury.io/py/stm32loader.svg)](https://badge.fury.io/py/stm32loader) -[![GitHub Actions](https://img.shields.io/github/workflow/status/florisla/stm32loader/Test?label=tests)](https://github.com/florisla/stm32loader/actions/workflows/test.yaml) -[![GitHub Actions](https://img.shields.io/github/workflow/status/florisla/stm32loader/Lint?label=lint)](https://github.com/florisla/stm32loader/actions/workflows/lint.yaml) +[![Test](https://github.com/florisla/stm32loader/actions/workflows/test.yaml/badge.svg)](https://github.com/florisla/stm32loader/actions/workflows/test.yaml) +[![Lint](https://github.com/florisla/stm32loader/actions/workflows/lint.yaml/badge.svg)](https://github.com/florisla/stm32loader/actions/workflows/lint.yaml) [![License](https://img.shields.io/pypi/l/stm32loader.svg)](https://pypi.org/project/stm32loader/) [![Downloads](https://pepy.tech/badge/stm32loader)](https://pepy.tech/project/stm32loader) From 8e4daffe6e9e40492af4d0c9c0fe63b738614843 Mon Sep 17 00:00:00 2001 From: Adam DeMuri Date: Tue, 3 Mar 2026 23:30:21 -0700 Subject: [PATCH 356/369] Add support for write unprotection. --- README.md | 2 +- src/stm32loader/args.py | 4 ++++ src/stm32loader/bootloader.py | 4 ++++ src/stm32loader/emulated/fake.py | 13 +++++++++++-- src/stm32loader/main.py | 10 ++++++++++ tests/integration/test_full_cycle.py | 7 ++++++- tests/unit/test_arguments.py | 4 ++++ tests/unit/test_bootloader.py | 8 ++++++++ 8 files changed, 48 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index aa255a9..04cc17a 100644 --- a/README.md +++ b/README.md @@ -187,7 +187,7 @@ adapter (it needs to toggle, whereas `BOOT0` does not). ## Not currently supported -* Command-line argument for write protection/unprotection. +* Command-line argument for write protection. * STM8 devices (ST `UM0560`). * Other bootloader protocols (e.g. I2C, HEX -> implemented in `stm32flash`). diff --git a/src/stm32loader/args.py b/src/stm32loader/args.py index eef78e7..bb66210 100644 --- a/src/stm32loader/args.py +++ b/src/stm32loader/args.py @@ -83,6 +83,10 @@ def parse_arguments(arguments): "-x", "--protect", action="store_true", help="Protect flash against readout." ) + parser.add_argument( + "--write-unprotect", action="store_true", help="Disable write protection before flashing" + ) + parser.add_argument("-w", "--write", action="store_true", help="Write file content to flash.") parser.add_argument( diff --git a/src/stm32loader/bootloader.py b/src/stm32loader/bootloader.py index ed243c4..401820d 100644 --- a/src/stm32loader/bootloader.py +++ b/src/stm32loader/bootloader.py @@ -772,8 +772,12 @@ def write_protect(self, pages): def write_unprotect(self): """Disable write protection of the flash memory.""" + self.debug(10, "Disabling write protection") self.command(self.Command.WRITE_UNPROTECT, "Write unprotect") self._wait_for_ack("0x73 write unprotect failed") + + time.sleep(0.1) + self.reset_from_system_memory() self.debug(10, " Write Unprotect done") def readout_protect(self): diff --git a/src/stm32loader/emulated/fake.py b/src/stm32loader/emulated/fake.py index 2268e67..4f95bd3 100644 --- a/src/stm32loader/emulated/fake.py +++ b/src/stm32loader/emulated/fake.py @@ -61,6 +61,10 @@ def receive(self): command_bytes = yield command_value = struct.unpack("B", command_bytes)[0] + # No CRC is sent for SYNCHRONIZE + if command_value == self.Command.SYNCHRONIZE: + continue + # Receive CRC byte. yield self.ack() @@ -112,8 +116,12 @@ def receive(self): # Record data in flash memory. flash_offset = address - 0x_0800_0000 self.flash_memory[flash_offset : flash_offset + byte_count] = data + + elif command_value == self.Command.WRITE_UNPROTECT.value: + pass + else: - raise NotImplementedError() + raise NotImplementedError(hex(command_value)) def write(self, data): # Send to coroutine. @@ -132,11 +140,12 @@ def read(self, length=1): # pylint: disable=unused-argument class FakeConfiguration: """Represent a configuration for test purposes.""" - def __init__(self, erase, write, verify, firmware_file, family=None): + def __init__(self, erase, write, verify, write_unprotect, firmware_file, family=None): self.erase = erase self.write = write self.verify = verify self.data_file = firmware_file + self.write_unprotect = write_unprotect self.unprotect = False self.protect = False self.length = None diff --git a/src/stm32loader/main.py b/src/stm32loader/main.py index c91db02..0de3275 100644 --- a/src/stm32loader/main.py +++ b/src/stm32loader/main.py @@ -141,6 +141,16 @@ def perform_commands(self): self.debug(0, "Quit") self.stm32.reset_from_flash() sys.exit(1) + + if self.configuration.write_unprotect: + try: + self.stm32.write_unprotect() + except bootloader.CommandError: + self.debug(0, "Flash write unprotect failed") + self.debug(0, "Quit") + self.stm32.reset_from_flash() + sys.exit(1) + if self.configuration.erase: try: if self.configuration.length is None: diff --git a/tests/integration/test_full_cycle.py b/tests/integration/test_full_cycle.py index 18ca62f..cd6b91d 100644 --- a/tests/integration/test_full_cycle.py +++ b/tests/integration/test_full_cycle.py @@ -10,7 +10,12 @@ def test_erase_write_verify_passes(): loader = Stm32Loader() loader.configuration = FakeConfiguration( - erase=True, write=True, verify=True, firmware_file=FIRMWARE_FILE, family=None + erase=True, + write=True, + verify=True, + write_unprotect=True, + firmware_file=FIRMWARE_FILE, + family=None, ) loader.connection = FakeConnection() loader.stm32 = Stm32Bootloader(loader.connection, device_family="F1", verbosity=5) diff --git a/tests/unit/test_arguments.py b/tests/unit/test_arguments.py index d6d7551..ec93f31 100644 --- a/tests/unit/test_arguments.py +++ b/tests/unit/test_arguments.py @@ -43,3 +43,7 @@ def test_parse_arguments_erase_without_port_complains_about_missing_argument(pro pytest.skip("Not sure why nothing is captured in some pytest runs?") assert "arguments are required: -p/--port" in error_output assert "STM32LOADER_SERIAL_PORT" in error_output + + +def test_parse_arguments_write_unprotect(program): + program.parse_arguments(["--write-unprotect"]) diff --git a/tests/unit/test_bootloader.py b/tests/unit/test_bootloader.py index 44b94df..f7ae738 100644 --- a/tests/unit/test_bootloader.py +++ b/tests/unit/test_bootloader.py @@ -205,6 +205,14 @@ def test_write_protect_sends_page_addresses_and_checksum(bootloader, write): assert write.data_was_written(b"\x01\x08\x08") +def test_write_unprotect_sends_command(bootloader, write): + bootloader.write_unprotect() + assert write.data_was_written(b"\x73"), write.written_data + assert write.data_was_written(bytearray([Stm32Bootloader.Command.SYNCHRONIZE])), ( + write.written_data + ) + + def test_verify_data_with_identical_data_passes(): Stm32Bootloader.verify_data(b"\x05", b"\x05") From 52966985f4edceb8d90f3be5b61a68b53b821bf9 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Thu, 5 Mar 2026 11:44:57 +0100 Subject: [PATCH 357/369] dev: Use f-string instead of .format() --- src/stm32loader/args.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/stm32loader/args.py b/src/stm32loader/args.py index bb66210..6c8919a 100644 --- a/src/stm32loader/args.py +++ b/src/stm32loader/args.py @@ -216,8 +216,8 @@ def parse_arguments(arguments): port_arg.required = True atexit.register( lambda: print( - "{}: note: you can also set the environment " - "variable STM32LOADER_SERIAL_PORT".format(parser.prog), + f"{parser.prog}: note: you can also set the environment" + " variable STM32LOADER_SERIAL_PORT", file=sys.stderr, ) ) From 57d864b8378c8008bd31144d6f2aebcc59f8f3c3 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Thu, 5 Mar 2026 11:45:34 +0100 Subject: [PATCH 358/369] clean(doc): Drop 'future work' section Stuff like this is tracked in GitHub issues. --- README.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/README.md b/README.md index fa14924..36c2119 100644 --- a/README.md +++ b/README.md @@ -190,9 +190,3 @@ adapter (it needs to toggle, whereas `BOOT0` does not). * Command-line argument for write protection. * STM8 devices (ST `UM0560`). * Other bootloader protocols (e.g. I2C, HEX -> implemented in `stm32flash`). - - -## Future work - -* Use f-strings. -* Use proper logging instead of print statements. From 3aff4fd70521447711e793ae2bae154375ae2da4 Mon Sep 17 00:00:00 2001 From: Adam DeMuri Date: Tue, 3 Mar 2026 23:30:21 -0700 Subject: [PATCH 359/369] Add support for write protection. --- README.md | 2 +- src/stm32loader/args.py | 4 ++ src/stm32loader/bootloader.py | 70 +++++++++++++++++--- src/stm32loader/device_family.py | 18 ++++-- src/stm32loader/device_info.py | 92 ++++++++++++++++++-------- src/stm32loader/devices.py | 95 +++++++++++++++++++++------ src/stm32loader/emulated/fake.py | 17 ++++- src/stm32loader/main.py | 10 +++ tests/integration/test_full_cycle.py | 23 ++++++- tests/unit/test_arguments.py | 4 ++ tests/unit/test_bootloader.py | 96 +++++++++++++++++++++++++++- tests/unit/test_devices.py | 94 ++++++++++++++++++++++++++- 12 files changed, 458 insertions(+), 67 deletions(-) diff --git a/README.md b/README.md index 36c2119..cab735b 100644 --- a/README.md +++ b/README.md @@ -187,6 +187,6 @@ adapter (it needs to toggle, whereas `BOOT0` does not). ## Not currently supported -* Command-line argument for write protection. +* Command-line argument for write protection for some devices (e.g. those with dual-bank flash). * STM8 devices (ST `UM0560`). * Other bootloader protocols (e.g. I2C, HEX -> implemented in `stm32flash`). diff --git a/src/stm32loader/args.py b/src/stm32loader/args.py index 6c8919a..60f65d1 100644 --- a/src/stm32loader/args.py +++ b/src/stm32loader/args.py @@ -87,6 +87,10 @@ def parse_arguments(arguments): "--write-unprotect", action="store_true", help="Disable write protection before flashing" ) + parser.add_argument( + "--write-protect", action="store_true", help="Enable write protection after flashing" + ) + parser.add_argument("-w", "--write", action="store_true", help="Write file content to flash.") parser.add_argument( diff --git a/src/stm32loader/bootloader.py b/src/stm32loader/bootloader.py index 401820d..9eadfdd 100644 --- a/src/stm32loader/bootloader.py +++ b/src/stm32loader/bootloader.py @@ -26,7 +26,11 @@ import time from functools import lru_cache, reduce +# FIXME: remove and switch to '|' syntax when Python 3.10+ is required. +from typing import Optional + from stm32loader.device_family import DeviceFamily, DeviceFlag +from stm32loader.device_info import DeviceInfo from stm32loader.devices import DEVICES CHIP_IDS = { @@ -355,6 +359,8 @@ class Reply(enum.IntEnum): "H7": 128 * 1024, } + device: Optional[DeviceInfo] + SYNCHRONIZE_ATTEMPTS = 2 def __init__( # pylint: disable=too-many-positional-arguments,too-many-arguments @@ -372,10 +378,10 @@ def __init__( # pylint: disable=too-many-positional-arguments,too-many-argument but a straight pyserial serial.Serial object can also be used. :param connection: Object supporting read() and write(). - E.g. serial.Serial(). - :param int verbosity: Verbosity level. 0 is quite, 10 is verbose. + E.g. serial.Serial(). + :param int verbosity: Verbosity level. 0 is quiet, 10 is verbose. :param ShowProgress show_progress: ShowProgress context manager. - Set to None to disable progress bar output. + Set to None to disable progress bar output. """ self.connection = connection self.verbosity = verbosity @@ -761,16 +767,62 @@ def extended_erase_memory(self, pages=None): self.connection.timeout = previous_timeout_value self.debug(10, " Extended Erase memory done") - def write_protect(self, pages): + def write_protect(self, sectors=None) -> None: """Enable write protection on the given flash pages.""" + + if self.device is None: + raise Stm32LoaderError( + "Device type must be detected before write protection can be enabled." + ) + + if not self.device.write_protect_supported: + raise Stm32LoaderError( + f"Write protection support for '{self.device}' not currently implemented." + ) + + self.debug(10, "Enabling write protection") + if sectors is None: + if self.device.flash is None: + raise Stm32LoaderError( + f"Device flash info is missing for family '{self.device.family.name}'" + ) + if self.device.flash.num_pages() is None: + raise Stm32LoaderError( + f"Device flash page info is missing for device '{self.device.device_name}'" + ) + num_sectors = self.device.flash.num_sectors() + if num_sectors is None: + raise Stm32LoaderError( + f"Device flash sector info is missing for device '{self.device.device_name}'" + ) + # The 'number of sectors' is 0-based + num_sectors -= 1 + sectors = bytearray(list(range(0, num_sectors + 1))) + else: + num_sectors = len(sectors) - 1 + bad_sectors = [s for s in sectors if s < 0 or s > 255] + if len(bad_sectors) > 0: + raise PageIndexError( + "Write protection only supports sector indices up to 255, but got " + f"{bad_sectors}." + ) + + if num_sectors > 255: + raise DataLengthError("Write protection only supports up to 256 sectors.") + + self.debug( + 5, f"Write protecting {num_sectors} sectors, flash size: {self.device.flash.size}" + ) + self.command(self.Command.WRITE_PROTECT, "Write protect") - nr_of_pages = (len(pages) - 1) & 0xFF - page_numbers = bytearray(pages) - checksum = reduce(operator.xor, page_numbers, nr_of_pages) - self.write_and_ack("0x63 write protect failed", nr_of_pages, page_numbers, checksum) + checksum = reduce(operator.xor, sectors, num_sectors) + self.write_and_ack("0x63 write protect failed", num_sectors, sectors, checksum) + + time.sleep(0.1) + self.reset_from_system_memory() self.debug(10, " Write protect done") - def write_unprotect(self): + def write_unprotect(self) -> None: """Disable write protection of the flash memory.""" self.debug(10, "Disabling write protection") self.command(self.Command.WRITE_UNPROTECT, "Write unprotect") diff --git a/src/stm32loader/device_family.py b/src/stm32loader/device_family.py index c1ba473..0c479e0 100644 --- a/src/stm32loader/device_family.py +++ b/src/stm32loader/device_family.py @@ -24,6 +24,7 @@ class DeviceFamily(enum.Enum): L4 = "L4" L5 = "L5" WBA = "WBA" + WB0 = "WB0" WB = "WB" WL = "WL" U5 = "U5" @@ -80,7 +81,9 @@ def __init__( # pylint: disable=too-many-arguments,too-many-positional-argument DeviceFamily.C0: DeviceFamilyInfo("C0", bootloader_id_address=0x_1FFF_17FE), # RM0360 DeviceFamily.F0: DeviceFamilyInfo( - "F0", flash_size_address=0x_1FFF_F7CC, option_bytes=(0x_1FFF_F800, 0x_1FFF_F80F) + "F0", + flash_size_address=0x_1FFF_F7CC, + option_bytes=(0x_1FFF_F800, 0x_1FFF_F80F), ), # RM0008 DeviceFamily.F1: DeviceFamilyInfo( @@ -89,8 +92,11 @@ def __init__( # pylint: disable=too-many-arguments,too-many-positional-argument flash_size_address=0x_1FFF_F7E0, option_bytes=(0x_1FFF_F800, 0x_1FFF_F80F), ), + # RM0033 DeviceFamily.F2: DeviceFamilyInfo( - "F2", option_bytes=(0x_1FFF_C000, 0x_1FFF_C00F), bootloader_id_address=0x_1FFF_77DE + "F2", + option_bytes=(0x_1FFF_C000, 0x_1FFF_C00F), + bootloader_id_address=0x_1FFF_77DE, ), # RM0366, RM0365, RM0316, RM0313, RM4510 DeviceFamily.F3: DeviceFamilyInfo( @@ -100,7 +106,7 @@ def __init__( # pylint: disable=too-many-arguments,too-many-positional-argument flash_page_size=2048, bootloader_id_address=0x_1FFF_F796, ), - # RM0090 + # RM0090, RM0390, RM0383, RM0402, RM0401, RM0368, RM0430, RM0386 DeviceFamily.F4: DeviceFamilyInfo( "F4", uid_address=0x_1FFF_7A10, @@ -108,7 +114,7 @@ def __init__( # pylint: disable=too-many-arguments,too-many-positional-argument bootloader_id_address=0x_1FFF_76DE, flags=DeviceFlag.LONG_UID_ACCESS, ), - # RM0385 + # RM0385, RM0431 DeviceFamily.F7: DeviceFamilyInfo( "F7", uid_address=0x_1FF0_F420, @@ -163,6 +169,10 @@ def __init__( # pylint: disable=too-many-arguments,too-many-positional-argument DeviceFamily.WB: DeviceFamilyInfo( "WB", ), + DeviceFamily.WB0: DeviceFamilyInfo( + "WB0", + # TODO: mark write protection as not supported + ), # RM0453 DeviceFamily.WL: DeviceFamilyInfo( "WL", uid_address=0x_1FFF_7590, flash_size_address=0x_1FFF_75E0 diff --git a/src/stm32loader/device_info.py b/src/stm32loader/device_info.py index 2bddeb5..4b796b7 100644 --- a/src/stm32loader/device_info.py +++ b/src/stm32loader/device_info.py @@ -1,5 +1,8 @@ """Hold information about different STM32 devices.""" +# FIXME: remove and switch to '|' syntax when Python 3.10+ is required. +from typing import Optional, Union + from stm32loader.device_family import DEVICE_FAMILIES, DeviceFamily, DeviceFlag kB = 1024 # pylint: disable=invalid-name @@ -8,6 +11,8 @@ class DeviceInfo: # pylint: disable=too-many-instance-attributes """Hold info about an STM32 device.""" + write_protect_supported: bool + def __init__( # pylint: disable=too-many-positional-arguments,too-many-arguments self, device_family, @@ -22,6 +27,7 @@ def __init__( # pylint: disable=too-many-positional-arguments,too-many-argument option=None, bootloader_id_address=None, flags=DeviceFlag.NONE, + write_protect_supported=False, ): self.family = DEVICE_FAMILIES[DeviceFamily[device_family]] self.device_name = device_name @@ -35,6 +41,7 @@ def __init__( # pylint: disable=too-many-positional-arguments,too-many-argument self.option_bytes = option self.flags = flags | self.family.family_default_flags self.bootloader_id_address = bootloader_id_address or self.family.bootloader_id_address + self.write_protect_supported = write_protect_supported @property def ram_size(self): @@ -96,41 +103,76 @@ def __repr__(self): class Flash: # pylint: disable=too-few-public-methods """Represent info about a device's flash layout.""" + start: Optional[int] + end: Optional[int] + page_size: Union[int, list[int], None] + pages_per_sector: Optional[int] + max_write_protection_sectors: int + # RM0090 4 sectors of 16 Kbytes, 1 sector of 64 Kbytes, # 7 sectors of 128 Kbytes - F2_F4_PAGE_SIZE = (16 * kB, 16 * kB, 16 * kB, 16 * kB, 64 * kB, 128 * kB, 0) + F2_F4_PAGE_SIZE = 4 * [16 * kB] + [64 * kB] + 7 * [128 * kB] + + F4_EXTENDED_PAGE_SIZE = 4 * [16 * kB] + [64 * kB] + 11 * [128 * kB] + # RM0090 4 sectors of 16 Kbytes, 1 sector of 64 Kbytes, # 7 sectors of 128 Kbytes but then per bank - F4_DUAL_BANK_PAGE_SIZE = ( - 16 * kB, - 16 * kB, - 16 * kB, - 16 * kB, - 64 * kB, - 128 * kB, - 128 * kB, - 128 * kB, - 128 * kB, - 16 * kB, - 16 * kB, - 16 * kB, - 16 * kB, - 64 * kB, - 128 * kB, - 0, - ) - F7_PAGE_SIZE = (32 * kB, 32 * kB, 32 * kB, 32 * kB, 128 * kB, 256 * kB, 0) - - def __init__(self, start=None, end=None, page_size=None, pages_per_sector=None): + F4_DUAL_BANK_PAGE_SIZE = (4 * [16 * kB] + [64 * kB] + 7 * [128 * kB]) * 2 + + F7_PAGE_SIZE = 4 * [32 * kB] + [128 * kB] + 7 * [256 * kB] + + def __init__( + self, + start=None, + end=None, + page_size=None, + pages_per_sector=None, + max_write_protection_sectors=63, + ): # pylint: disable=too-many-arguments,too-many-positional-arguments self.start = start self.end = end self.page_size = page_size self.pages_per_sector = pages_per_sector + # Some devices, like the STM32F101, use a regular scheme up to 62 + # sectors, and the 63rd "sector" applies to the remaining flash. This + # parameter allows for devices which have a different max number of + # sectors. + self.max_write_protection_sectors = max_write_protection_sectors @property - def size(self) -> int: + def size(self) -> Optional[int]: """Return the size of the flash memory in bytes.""" - if self.start is None: - return 0 + if self.start is None or self.end is None: + return None return self.end - self.start + + def num_pages(self) -> Optional[int]: + """Return the number of pages in the flash memory.""" + if self.size is None or self.page_size is None: + return None + + if isinstance(self.page_size, int): + return self.size // self.page_size + + flash_size = self.size + + num_pages = 0 + for page_size in self.page_size: + flash_size -= page_size + num_pages += 1 + if flash_size <= 0: + return num_pages + + raise ValueError( + "Flash size is larger than the total size of all pages. " + f"Flash size: {self.size}, total page size: {sum(self.page_size)}" + ) + + def num_sectors(self) -> Optional[int]: + """Return the number of sectors in the flash memory.""" + num_pages = self.num_pages() + if num_pages is None or self.pages_per_sector is None: + return None + + return min(num_pages // self.pages_per_sector, self.max_write_protection_sectors) diff --git a/src/stm32loader/devices.py b/src/stm32loader/devices.py index 55400eb..83379a1 100644 --- a/src/stm32loader/devices.py +++ b/src/stm32loader/devices.py @@ -61,6 +61,7 @@ flash=(0x_0800_0000, 0x_0801_0000, 1 * kB, 4), option=(0x_1FFF_F800, 0x_1FFF_F80F), bootloader_id_address=0x_1FFF_F7A6, + write_protect_supported=True, ), DeviceInfo( "F0", @@ -72,6 +73,7 @@ flash=(0x_0800_0000, 0x_0800_8000, 1 * kB, 4), option=(0x_1FFF_F800, 0x_1FFF_F80F), bootloader_id_address=0x_1FFF_F7A6, + write_protect_supported=True, ), # FIXME different flash size for both devices with PID=0x442 ? DeviceInfo( @@ -85,6 +87,7 @@ option=(0x_1FFF_F800, 0x_1FFF_F80F), bootloader_id_address=0x_1FFF_F796, flags=DeviceFlag.OBL_LAUNCH, + write_protect_supported=True, ), # FIXME different flash size for both devices with PID=0x445 ? DeviceInfo( @@ -97,6 +100,7 @@ flash=(0x_0800_0000, 0x_0800_8000, 1 * kB, 4), option=(0x_1FFF_F800, 0x_1FFF_F80F), bootloader_id_address=0x_1FFF_F6A6, + write_protect_supported=True, ), DeviceInfo( "F0", @@ -108,6 +112,7 @@ flash=(0x_0800_0000, 0x_0800_8000, 1 * kB, 4), option=(0x_1FFF_F800, 0x_1FFF_F80F), bootloader_id_address=0x_1FFF_F6A6, + write_protect_supported=True, ), # FIXME different flash size for both devices with PID=0x448 ? DeviceInfo( @@ -120,6 +125,7 @@ flash=(0x_0800_0000, 0x_0802_0000, 2 * kB, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F), bootloader_id_address=0x_1FFF_F6A6, + write_protect_supported=True, ), DeviceInfo( "F0", @@ -131,6 +137,7 @@ flash=(0x_0800_0000, 0x_0802_0000, 2 * kB, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F), bootloader_id_address=0x_1FFF_F6A6, + write_protect_supported=True, ), DeviceInfo( "F0", @@ -143,6 +150,7 @@ option=(0x_1FFF_F800, 0x_1FFF_F80F), bootloader_id_address=0x_1FFF_F796, flags=DeviceFlag.OBL_LAUNCH, + write_protect_supported=True, ), DeviceInfo( "F1", @@ -154,6 +162,7 @@ system=(0x_1FFF_F000, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0800_8000, 1 * kB, 4), option=(0x_1FFF_F800, 0x_1FFF_F80F), + write_protect_supported=True, ), DeviceInfo( "F1", @@ -165,6 +174,7 @@ system=(0x_1FFF_F000, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0802_0000, 1 * kB, 4), option=(0x_1FFF_F800, 0x_1FFF_F80F), + write_protect_supported=True, ), DeviceInfo( "F1", @@ -188,6 +198,7 @@ flash=(0x_0800_0000, 0x_0802_0000, 1 * kB, 4), option=(0x_1FFF_F800, 0x_1FFF_F80F), bootloader_id_address=0x_1FFF_F7D6, + write_protect_supported=True, ), DeviceInfo( "F1", @@ -200,6 +211,7 @@ flash=(0x_0800_0000, 0x_0808_0000, 2 * kB, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F), bootloader_id_address=0x_1FFF_F7D6, + write_protect_supported=True, ), DeviceInfo( "F1", @@ -211,6 +223,7 @@ system=(0x_1FFF_B000, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0804_0000, 2 * kB, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F), + write_protect_supported=True, ), DeviceInfo( "F1", @@ -223,6 +236,7 @@ flash=(0x_0800_0000, 0x_0810_0000, 2 * kB, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F), bootloader_id_address=0x_1FFF_F7D6, + write_protect_supported=True, ), # FIXME different flash size for both devices with PID=0x411 ? DeviceInfo( @@ -232,8 +246,9 @@ 0x20, ram=(0x_2000_2000, 0x_2002_0000), system=(0x_1FFF_0000, 0x_1FFF_7800), - flash=(0x_0800_0000, 0x_0810_0000, Flash.F2_F4_PAGE_SIZE), + flash=(0x_0800_0000, 0x_0810_0000, Flash.F2_F4_PAGE_SIZE, 1), option=(0x_1FFF_C000, 0x_1FFF_C00F), + write_protect_supported=True, ), DeviceInfo( "F2", @@ -242,8 +257,9 @@ 0x33, ram=(0x_2000_2000, 0x_2002_0000), system=(0x_1FFF_0000, 0x_1FFF_7800), - flash=(0x_0800_0000, 0x_0810_0000, Flash.F2_F4_PAGE_SIZE), + flash=(0x_0800_0000, 0x_0810_0000, Flash.F2_F4_PAGE_SIZE, 1), option=(0x_1FFF_C000, 0x_1FFF_C00F), + write_protect_supported=True, ), # FIXME different flash size for both devices with PID=0x432 ? DeviceInfo( @@ -256,6 +272,7 @@ flash=(0x_0800_0000, 0x_0804_0000, 2 * kB, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F), bootloader_id_address=0x_1FFF_F7A6, + write_protect_supported=True, ), DeviceInfo( "F3", @@ -267,6 +284,7 @@ flash=(0x_0800_0000, 0x_0804_0000, 2 * kB, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F), bootloader_id_address=0x_1FFF_F7A6, + write_protect_supported=True, ), # FIXME different flash size for both devices with PID=0x422 ? DeviceInfo( @@ -278,6 +296,7 @@ system=(0x_1FFF_D800, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0804_0000, 2 * kB, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F), + write_protect_supported=True, ), DeviceInfo( "F3", @@ -288,6 +307,7 @@ system=(0x_1FFF_D800, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0804_0000, 2 * kB, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F), + write_protect_supported=True, ), # FIXME different flash size for both devices with PID=0x439 ? DeviceInfo( @@ -299,6 +319,7 @@ system=(0x_1FFF_D800, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0801_0000, 2 * kB, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F), + write_protect_supported=True, ), DeviceInfo( "F3", @@ -309,6 +330,7 @@ system=(0x_1FFF_D800, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0801_0000, 2 * kB, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F), + write_protect_supported=True, ), DeviceInfo( "F3", @@ -319,6 +341,7 @@ system=(0x_1FFF_D800, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0801_0000, 2 * kB, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F), + write_protect_supported=True, ), # FIXME different flash size for both devices with PID=0x446 ? DeviceInfo( @@ -330,6 +353,7 @@ system=(0x_1FFF_D800, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0808_0000, 2 * kB, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F), + write_protect_supported=True, ), DeviceInfo( "F3", @@ -340,6 +364,7 @@ system=(0x_1FFF_D800, 0x_1FFF_F800), flash=(0x_0800_0000, 0x_0808_0000, 2 * kB, 2), option=(0x_1FFF_F800, 0x_1FFF_F80F), + write_protect_supported=True, ), # FIXME different flash size for both devices with PID=0x413 ? DeviceInfo( @@ -360,9 +385,10 @@ 0x91, ram=(0x_2000_3000, 0x_2002_0000), system=(0x_1FFF_0000, 0x_1FFF_7800), - flash=(0x_0800_0000, 0x_0810_0000, Flash.F2_F4_PAGE_SIZE), + flash=(0x_0800_0000, 0x_0810_0000, Flash.F2_F4_PAGE_SIZE, 1), option=(0x_1FFF_C000, 0x_1FFF_C00F), bootloader_id_address=0x_1FFF_77DE, + write_protect_supported=True, ), # FIXME different flash size for both devices with PID=0x419 ? DeviceInfo( @@ -393,8 +419,9 @@ 0xD1, ram=(0x_2000_3000, 0x_2001_0000), system=(0x_1FFF_0000, 0x_1FFF_7800), - flash=(0x_0800_0000, 0x_0804_0000, Flash.F2_F4_PAGE_SIZE), + flash=(0x_0800_0000, 0x_0804_0000, Flash.F2_F4_PAGE_SIZE, 1), option=(0x_1FFF_C000, 0x_1FFF_C00F), + write_protect_supported=True, ), DeviceInfo( "F4", @@ -403,8 +430,9 @@ 0xD1, ram=(0x_2000_3000, 0x_2001_8000), system=(0x_1FFF_0000, 0x_1FFF_7800), - flash=(0x_0800_0000, 0x_0808_0000, Flash.F2_F4_PAGE_SIZE), + flash=(0x_0800_0000, 0x_0808_0000, Flash.F2_F4_PAGE_SIZE, 1), option=(0x_1FFF_C000, 0x_1FFF_C00F), + write_protect_supported=True, ), DeviceInfo( "F4", @@ -413,8 +441,9 @@ 0xB1, ram=(0x_2000_3000, 0x_2000_8000), system=(0x_1FFF_0000, 0x_1FFF_7800), - flash=(0x_0800_0000, 0x_0802_0000, Flash.F2_F4_PAGE_SIZE), + flash=(0x_0800_0000, 0x_0802_0000, Flash.F2_F4_PAGE_SIZE, 1), option=(0x_1FFF_C000, 0x_1FFF_C00F), + write_protect_supported=True, ), DeviceInfo( "F4", @@ -423,8 +452,9 @@ 0xD0, ram=(0x_2000_3000, 0x_2002_0000), system=(0x_1FFF_0000, 0x_1FFF_7800), - flash=(0x_0800_0000, 0x_0808_0000, Flash.F2_F4_PAGE_SIZE), + flash=(0x_0800_0000, 0x_0808_0000, Flash.F2_F4_PAGE_SIZE, 1), option=(0x_1FFF_C000, 0x_1FFF_C00F), + write_protect_supported=True, ), DeviceInfo( "F4", @@ -433,8 +463,9 @@ 0x90, ram=(0x_2000_3000, 0x_2004_0000), system=(0x_1FFF_0000, 0x_1FFF_7800), - flash=(0x_0800_0000, 0x_0810_0000, Flash.F2_F4_PAGE_SIZE), + flash=(0x_0800_0000, 0x_0810_0000, Flash.F2_F4_PAGE_SIZE, 1), option=(0x_1FFF_C000, 0x_1FFF_C00F), + write_protect_supported=True, ), DeviceInfo( "F4", @@ -443,8 +474,9 @@ 0x90, ram=(0x_2000_3000, 0x_2002_0000), system=(0x_1FFF_0000, 0x_1FFF_7800), - flash=(0x_0800_0000, 0x_0808_0000, Flash.F2_F4_PAGE_SIZE), + flash=(0x_0800_0000, 0x_0808_0000, Flash.F2_F4_PAGE_SIZE, 1), option=(0x_1FFF_C000, 0x_1FFF_C00F), + write_protect_supported=True, ), DeviceInfo( "F4", @@ -463,7 +495,7 @@ 0x90, ram=(0x_2000_3000, 0x_2005_0000), system=(0x_1FFF_0000, 0x_1FFF_7800), - flash=(0x_0800_0000, 0x_0818_0000, Flash.F2_F4_PAGE_SIZE), + flash=(0x_0800_0000, 0x_0818_0000, Flash.F4_EXTENDED_PAGE_SIZE), option=(0x_1FFF_C000, 0x_1FFF_C00F), ), DeviceInfo( @@ -473,8 +505,9 @@ 0x90, ram=(0x_2000_4000, 0x_2004_0000), system=(0x_1FF0_0000, 0x_1FF0_EDC0), - flash=(0x_0800_0000, 0x_0808_0000, Flash.F2_F4_PAGE_SIZE), + flash=(0x_0800_0000, 0x_0808_0000, Flash.F2_F4_PAGE_SIZE, 1), option=(0x_1FFF_0000, 0x_1FFF_001F), + write_protect_supported=True, ), # FIXME different flash size for both devices with PID=0x449 ? DeviceInfo( @@ -484,8 +517,9 @@ 0x70, ram=(0x_2000_4000, 0x_2005_0000), system=(0x_1FF0_0000, 0x_1FF0_EDC0), - flash=(0x_0800_0000, 0x_0810_0000, Flash.F7_PAGE_SIZE), + flash=(0x_0800_0000, 0x_0810_0000, Flash.F7_PAGE_SIZE, 1), option=(0x_1FFF_0000, 0x_1FFF_001F), + write_protect_supported=True, ), DeviceInfo( "F7", @@ -494,8 +528,9 @@ 0x90, ram=(0x_2000_4000, 0x_2005_0000), system=(0x_1FF0_0000, 0x_1FF0_EDC0), - flash=(0x_0800_0000, 0x_0810_0000, Flash.F7_PAGE_SIZE), + flash=(0x_0800_0000, 0x_0810_0000, Flash.F7_PAGE_SIZE, 1), option=(0x_1FFF_0000, 0x_1FFF_001F), + write_protect_supported=True, ), DeviceInfo( "F7", @@ -504,8 +539,9 @@ 0x93, ram=(0x_2000_4000, 0x_2008_0000), system=(0x_1FF0_0000, 0x_1FF0_EDC0), - flash=(0x_0800_0000, 0x_0820_0000, Flash.F7_PAGE_SIZE), + flash=(0x_0800_0000, 0x_0820_0000, Flash.F7_PAGE_SIZE, 1), option=(0x_1FFF_0000, 0x_1FFF_001F), + write_protect_supported=True, ), DeviceInfo( "G0", @@ -514,9 +550,10 @@ 0x53, ram=(0x_2000_1000, 0x_2000_2000), system=(0x_1FFF_0000, 0x_1FFF_2000), - flash=(0x_0800_0000, 0x_0801_0000, 2 * kB), + flash=(0x_0800_0000, 0x_0801_0000, 2 * kB, 1), option=(0x_1FFF_7800, 0x_1FFF_787F), bootloader_id_address=0x_1FFF_1FFE, + write_protect_supported=True, ), DeviceInfo( "G0", @@ -525,9 +562,10 @@ 0xB3, ram=(0x_2000_2700, 0x_2000_9000), system=(0x_1FFF_0000, 0x_1FFF_7000), - flash=(0x_0800_0000, 0x_0802_0000, 2 * kB), + flash=(0x_0800_0000, 0x_0802_0000, 2 * kB, 1), option=(0x_1FFF_7800, 0x_1FFF_787F), bootloader_id_address=0x_1FFF_6FFE, + write_protect_supported=True, ), # FIXME different flash size for both devices with PID=0x467 ? # FIXME dual banks for system @@ -623,9 +661,10 @@ 0x93, ram=((0x_2000_4100, 0x_2002_0000), (0x_2400_4000, 0x_2405_0000)), system=(0x_1FF0_0000, 0x_1FF1_E800), - flash=(0x_0800_0000, 0x_0810_0000, 128 * kB), + flash=(0x_0800_0000, 0x_0810_0000, 128 * kB, 1), option=None, bootloader_id_address=0x_1FF1_E7FE, + write_protect_supported=True, ), DeviceInfo( "H7", @@ -634,9 +673,10 @@ 0x91, ram=((0x_2000_4100, 0x_2002_0000), (0x_2400_5000, 0x_2408_0000)), system=(0x_1FF0_0000, 0x_1FF1_E800), - flash=(0x_0800_0000, 0x_0820_0000, 128 * kB), + flash=(0x_0800_0000, 0x_0820_0000, 128 * kB, 1), option=None, bootloader_id_address=0x_1FF1_E7FE, + write_protect_supported=True, ), DeviceInfo( "H7", @@ -659,6 +699,7 @@ flash=(0x_0800_0000, 0x_0800_4000, 128, 32), option=(0x_1FF8_0000, 0x_1FF8_001F), bootloader_id_address=0x_1FF0_0FFE, + write_protect_supported=True, ), DeviceInfo( "L0", @@ -670,6 +711,7 @@ flash=(0x_0800_0000, 0x_0800_8000, 128, 32), option=(0x_1FF8_0000, 0x_1FF8_001F), bootloader_id_address=0x_1FF0_0FFE, + write_protect_supported=True, ), DeviceInfo( "L0", @@ -681,6 +723,7 @@ flash=(0x_0800_0000, 0x_0801_0000, 128, 32), option=(0x_1FF8_0000, 0x_1FF8_001F), bootloader_id_address=0x_1FF0_0FFE, + write_protect_supported=True, ), # FIXME different flash size for both devices with PID=0x447 ? # Note: STM32flash has 0x_2000_2000 as lower system range. @@ -694,6 +737,7 @@ flash=(0x_0800_0000, 0x_0803_0000, 128, 32), option=(0x_1FF8_0000, 0x_1FF8_001F), bootloader_id_address=0x_1FF0_1FFE, + write_protect_supported=True, ), DeviceInfo( "L0", @@ -705,6 +749,7 @@ flash=(0x_0800_0000, 0x_0803_0000, 128, 32), option=(0x_1FF8_0000, 0x_1FF8_001F), bootloader_id_address=0x_1FF0_1FFE, + write_protect_supported=True, ), DeviceInfo( "L1", @@ -717,6 +762,7 @@ flash=(0x_0800_0000, 0x_0802_0000, 256, 16), option=(0x_1FF8_0000, 0x_1FF8_001F), bootloader_id_address=0x_1FF0_0FFE, + write_protect_supported=True, ), DeviceInfo( "L1", @@ -728,6 +774,7 @@ flash=(0x_0800_0000, 0x_0802_0000, 256, 16), option=(0x_1FF8_0000, 0x_1FF8_001F), bootloader_id_address=0x_1FF0_0FFE, + write_protect_supported=True, ), DeviceInfo( "L1", @@ -739,6 +786,7 @@ flash=(0x_0800_0000, 0x_0804_0000, 256, 16), option=(0x_1FF8_0000, 0x_1FF8_001F), bootloader_id_address=0x_1FF0_1FFE, + write_protect_supported=True, ), DeviceInfo( "L1", @@ -750,6 +798,7 @@ flash=(0x_0800_0000, 0x_0806_0000, 256, 16), option=(0x_1FF8_0000, 0x_1FF8_001F), bootloader_id_address=0x_1FF0_1FFE, + write_protect_supported=True, ), DeviceInfo( "L1", @@ -761,6 +810,7 @@ flash=(0x_0800_0000, 0x_0808_0000, 256, 16), option=(0x_1FF8_0000, 0x_1FF8_001F), bootloader_id_address=0x_1FF0_1FFE, + write_protect_supported=True, ), # Note: Stm32flash has 0x_2000_3100 as ram start. DeviceInfo( @@ -771,8 +821,9 @@ bid=0xD1, ram=(0x_2000_2100, 0x_2000_8000), system=(0x_1FFF_0000, 0x_1FFF_7000), - flash=(0x_0800_0000, 0x_0802_0000, 2 * kB), + flash=(0x_0800_0000, 0x_0802_0000, 2 * kB, 1, 256), option=(0x_1FFF_7800, 0x_1FFF_780F), + write_protect_supported=True, ), DeviceInfo( "L4", @@ -781,8 +832,9 @@ 0x91, ram=(0x_2000_3100, 0x_2000_C000), system=(0x_1FFF_0000, 0x_1FFF_7000), - flash=(0x_0800_0000, 0x_0804_0000, 2 * kB), + flash=(0x_0800_0000, 0x_0804_0000, 2 * kB, 1, 256), option=(0x_1FFF_7800, 0x_1FFF_780F), + write_protect_supported=True, ), DeviceInfo( "L4", @@ -791,9 +843,10 @@ 0x92, ram=(0x_2000_3100, 0x_2002_0000), system=(0x_1FFF_0000, 0x_1FFF_7000), - flash=(0x_0800_0000, 0x_0808_0000, 2 * kB), + flash=(0x_0800_0000, 0x_0808_0000, 2 * kB, 1, 256), option=(0x_1FFF_7800, 0x_1FFF_780F), flags=DeviceFlag.CLEAR_PEMPTY, + write_protect_supported=True, ), # FIXME different flash size for both devices with PID=0x415 ? DeviceInfo( diff --git a/src/stm32loader/emulated/fake.py b/src/stm32loader/emulated/fake.py index 4f95bd3..6223d0f 100644 --- a/src/stm32loader/emulated/fake.py +++ b/src/stm32loader/emulated/fake.py @@ -117,6 +117,15 @@ def receive(self): flash_offset = address - 0x_0800_0000 self.flash_memory[flash_offset : flash_offset + byte_count] = data + elif command_value == self.Command.WRITE_PROTECT.value: + number_of_pages_bytes = yield + number_of_pages = struct.unpack("B", number_of_pages_bytes)[0] + _page_numbers_bytes = yield + assert len(_page_numbers_bytes) == number_of_pages + 1, ( + f"{len(_page_numbers_bytes)} != {number_of_pages + 1}" + ) + _crc = yield + elif command_value == self.Command.WRITE_UNPROTECT.value: pass @@ -140,12 +149,16 @@ def read(self, length=1): # pylint: disable=unused-argument class FakeConfiguration: """Represent a configuration for test purposes.""" - def __init__(self, erase, write, verify, write_unprotect, firmware_file, family=None): + def __init__( + self, erase, write, verify, write_protect, write_unprotect, firmware_file, family=None + ): self.erase = erase self.write = write + self.read = False self.verify = verify - self.data_file = firmware_file + self.write_protect = write_protect self.write_unprotect = write_unprotect + self.data_file = firmware_file self.unprotect = False self.protect = False self.length = None diff --git a/src/stm32loader/main.py b/src/stm32loader/main.py index 0de3275..4bbccc1 100644 --- a/src/stm32loader/main.py +++ b/src/stm32loader/main.py @@ -180,6 +180,16 @@ def perform_commands(self): sys.exit(1) if self.configuration.write: self.stm32.write_memory_data(self.configuration.address, binary_data) + + if self.configuration.write_protect: + try: + self.stm32.write_protect(sectors=None) + except bootloader.CommandError: + self.debug(0, "Flash write protect failed") + self.debug(0, "Quit") + self.stm32.reset_from_flash() + sys.exit(1) + if self.configuration.verify: read_data = self.stm32.read_memory_data(self.configuration.address, len(binary_data)) try: diff --git a/tests/integration/test_full_cycle.py b/tests/integration/test_full_cycle.py index cd6b91d..c0dbf6a 100644 --- a/tests/integration/test_full_cycle.py +++ b/tests/integration/test_full_cycle.py @@ -13,7 +13,8 @@ def test_erase_write_verify_passes(): erase=True, write=True, verify=True, - write_unprotect=True, + write_protect=False, + write_unprotect=False, firmware_file=FIRMWARE_FILE, family=None, ) @@ -24,3 +25,23 @@ def test_erase_write_verify_passes(): loader.read_device_uid() loader.read_flash_size() loader.perform_commands() + + +def test_write_protect_passes(): + loader = Stm32Loader() + loader.configuration = FakeConfiguration( + erase=False, + write=False, + verify=False, + write_protect=True, + write_unprotect=True, + firmware_file=None, + family=None, + ) + loader.connection = FakeConnection() + loader.stm32 = Stm32Bootloader(loader.connection, verbosity=5) + + loader.detect_device() + loader.read_device_uid() + loader.read_flash_size() + loader.perform_commands() diff --git a/tests/unit/test_arguments.py b/tests/unit/test_arguments.py index ec93f31..c2690ca 100644 --- a/tests/unit/test_arguments.py +++ b/tests/unit/test_arguments.py @@ -47,3 +47,7 @@ def test_parse_arguments_erase_without_port_complains_about_missing_argument(pro def test_parse_arguments_write_unprotect(program): program.parse_arguments(["--write-unprotect"]) + + +def test_parse_arguments_write_protect(program): + program.parse_arguments(["--write-protect"]) diff --git a/tests/unit/test_bootloader.py b/tests/unit/test_bootloader.py index f7ae738..677e6c6 100644 --- a/tests/unit/test_bootloader.py +++ b/tests/unit/test_bootloader.py @@ -6,6 +6,7 @@ from stm32loader import bootloader as Stm32 from stm32loader.bootloader import PageIndexError, Stm32Bootloader +from stm32loader.device_info import DeviceInfo from stm32loader.devices import DEVICES # pylint: disable=missing-docstring, redefined-outer-name @@ -35,7 +36,9 @@ def data_was_written(data): @pytest.fixture def bootloader(connection): - return Stm32Bootloader(connection) + device = DEVICES.get((0x422, 0x50)) + assert device is not None, "Device not found in DEVICES mapping" + return Stm32Bootloader(connection, device=device) def test_constructor_with_connection_none_passes(): @@ -200,9 +203,96 @@ def test_extended_erase_with_pages_sends_two_byte_sector_addresses_with_single_b assert write.data_was_written(b"\x00\x01\x00\x02\x00\x04\x0f\xf0\xfb") -def test_write_protect_sends_page_addresses_and_checksum(bootloader, write): +def test_write_protect_sends_command_page_addresses_and_checksum(bootloader, write): + bootloader.get_flash_size = MagicMock() + bootloader.get_flash_size.return_value = 16 bootloader.write_protect([0x01, 0x08]) - assert write.data_was_written(b"\x01\x08\x08") + assert write.data_was_written(b"\x63\x9c\x01\x01\x08\x08\x7f"), write.written_data + + +def test_write_protect_fails_when_no_device(bootloader): + bootloader = Stm32Bootloader(connection, device_family="F0", device=None) + with pytest.raises( + Stm32.Stm32LoaderError, + match="Device type must be detected before write protection can be enabled.", + ): + bootloader.write_protect([0x01, 0x08]) + + +def test_write_protect_fails_when_not_supported(bootloader): + device = DeviceInfo( + device_family="F0", device_name="STM32F012", pid=0, bid=0, write_protect_supported=False + ) + bootloader = Stm32Bootloader(connection, device_family="F0", device=device) + bootloader.get_flash_size = MagicMock() + bootloader.get_flash_size.return_value = 16 + with pytest.raises( + Stm32.Stm32LoaderError, + match="Write protection support for 'STM32F012' not currently implemented.", + ): + bootloader.write_protect([0x01, 0x08]) + + +def test_write_protect_fails_on_invalid_sector_index(bootloader): + bootloader.get_flash_size = MagicMock() + bootloader.get_flash_size.return_value = 16 + with pytest.raises(Stm32.PageIndexError): + bootloader.write_protect([0x01, 0x08, 0x100]) + + +def test_write_protect_fails_on_too_many_sectors(bootloader): + bootloader.get_flash_size = MagicMock() + bootloader.get_flash_size.return_value = 16 + with pytest.raises(Stm32.DataLengthError): + bootloader.write_protect(list(range(256)) + [0, 1]) + + +def test_write_protect_fails_on_no_flash_info(connection, bootloader): + device = DeviceInfo( + device_family="F0", device_name="STM32F012", pid=0, bid=0, write_protect_supported=True + ) + device.flash = None + bootloader = Stm32Bootloader(connection, device_family="F0", device=device) + + bootloader.get_flash_size = MagicMock() + bootloader.get_flash_size.return_value = 16 + with pytest.raises( + Stm32.Stm32LoaderError, match="Device flash info is missing for family 'F0'" + ): + bootloader.write_protect() + + +def test_write_protect_fails_on_no_flash_sector_info(connection, bootloader): + device = DeviceInfo( + device_family="F0", device_name="STM32F012", pid=0, bid=0, write_protect_supported=True + ) + device.flash = MagicMock() + device.flash.num_sectors = MagicMock(return_value=None) + bootloader = Stm32Bootloader(connection, device_family="F0", device=device) + + bootloader.get_flash_size = MagicMock() + bootloader.get_flash_size.return_value = 16 + with pytest.raises( + Stm32.Stm32LoaderError, match="Device flash sector info is missing for device 'STM32F012'" + ): + bootloader.write_protect() + + +def test_write_protect_no_pages_specified(connection, write): + device = DEVICES.get((0x417, 0xC0)) + assert device is not None, "Device not found in DEVICES mapping" + + bootloader = Stm32Bootloader(connection, device_family="L0", device=device) + bootloader.get_flash_size = MagicMock() + bootloader.get_flash_size.return_value = 8 + # bootloader.write = write + bootloader.write_protect() + assert write.data_was_written(b"\x63"), write.written_data + # Write protect sector size is 4KB, flash size is 64KB + # 64KB / 4KB = 16 sectors (index 1) + # Checksum is 0x0f + sectors = bytes(range(0, 16)) + assert write.data_was_written(b"\x63\x9c\x0f" + sectors + b"\x0f\x7f"), write.written_data def test_write_unprotect_sends_command(bootloader, write): diff --git a/tests/unit/test_devices.py b/tests/unit/test_devices.py index 0fae6e6..66089d8 100644 --- a/tests/unit/test_devices.py +++ b/tests/unit/test_devices.py @@ -3,6 +3,7 @@ from stm32loader.bootloader import CHIP_IDS, Stm32Bootloader from stm32loader.device_family import DEVICE_FAMILIES, DeviceFamily +from stm32loader.device_info import Flash from stm32loader.devices import DEVICES KNOWN_DUPLICATE_DEVICE_NAMES = [ @@ -76,7 +77,7 @@ def test_ram_size_is_multiple_of_256(dev): ids=lambda dev: str(dev).replace(" ", "-"), ) def test_flash_size_multiple_of_16k(dev): - if dev.flash_size == 0: + if dev.flash_size is None: return assert isinstance(dev.flash_size, int) @@ -230,6 +231,97 @@ def test_stm32flash_ram_addresses_match(device): ) +@pytest.mark.parametrize( + "device", + DEVICES.values(), + ids=lambda device: str(device).replace(" ", "-"), +) +def test_num_pages(device): + if device.flash is None: + return + + if device.flash.size is None or device.flash.page_size is None: + assert device.flash.num_pages() is None + return + + num_pages = device.flash.num_pages() + assert num_pages is not None, f"{device} flash page info is missing" + assert num_pages > 0, f"{device} flash page count is not positive: {num_pages}" + + +def test_flash_num_pages(): + # Like the STM32F301 + flash = Flash( + start=0x08000000, end=0x08000000 + 64 * 1024, page_size=2 * 1024, pages_per_sector=2 + ) + + assert flash.num_pages() == 32 + + +def test_flash_num_pages_mixed_page_sizes(): + flash = Flash( + start=0x08000000, + end=0x08000000 + 16 * 1024, + page_size=[2 * 1024] * 4 + [4 * 1024] * 2, + pages_per_sector=1, + ) + + assert flash.num_pages() == 6 + + +def test_flash_num_pages_no_flash_size(): + # Like the STM32F301 + flash = Flash(start=None, end=None, page_size=2 * 1024, pages_per_sector=2) + + assert flash.num_pages() is None + + +@pytest.mark.parametrize( + "device", + DEVICES.values(), + ids=lambda device: str(device).replace(" ", "-"), +) +def test_num_sectors_is_populated_when_write_protection_supported(device): + if device.flash is None or device.write_protect_supported is False: + return + + assert device.flash.num_sectors() is not None, f"{device} flash sector info is missing" + + +def test_flash_num_sectors(): + # Like the STM32F301 + flash = Flash( + start=0x08000000, end=0x08000000 + 64 * 1024, page_size=2 * 1024, pages_per_sector=2 + ) + + assert flash.num_sectors() == 16 + + +def test_flash_num_sectors_mixed_page_sizes(): + flash = Flash( + start=0x08000000, + end=0x08000000 + 16 * 1024, + page_size=[2 * 1024] * 4 + [4 * 1024] * 2, + pages_per_sector=1, + ) + + assert flash.num_sectors() == 6 + + +def test_flash_num_sectors_no_flash_size(): + flash = Flash(start=None, end=None, page_size=2 * 1024, pages_per_sector=2) + + assert flash.num_sectors() is None + + +def test_flash_num_sectors_with_max_num_sectors(): + flash = Flash( + start=0x08000000, end=0x08000000 + 1024 * 1024, page_size=2 * 1024, pages_per_sector=2 + ) + + assert flash.num_sectors() == 63 + + def test_family_uid_address_matches_existing(): for family_code, uid_address in Stm32Bootloader.UID_ADDRESS.items(): if family_code == "NRG": From 4055c986ca68cff18d8f575c082cfb82488d97ca Mon Sep 17 00:00:00 2001 From: dbeinder Date: Sun, 8 Mar 2026 19:49:10 +0100 Subject: [PATCH 360/369] fix/expand BlueNRG lineage devices --- src/stm32loader/bootloader.py | 10 +++++----- src/stm32loader/device_family.py | 33 ++++++++++++++++++++------------ src/stm32loader/devices.py | 24 +++++++++++++++++++---- tests/unit/test_devices.py | 4 ++-- 4 files changed, 48 insertions(+), 23 deletions(-) diff --git a/src/stm32loader/bootloader.py b/src/stm32loader/bootloader.py index 401820d..4cd3477 100644 --- a/src/stm32loader/bootloader.py +++ b/src/stm32loader/bootloader.py @@ -492,6 +492,9 @@ def get_id(self): length = bytearray(self.connection.read())[0] id_data = bytearray(self.connection.read(length + 1)) self._wait_for_ack("0x02 end") + if self.device_family == DeviceFamily.NRG.value: + # BlueNRG-lineage devices hold the PID in the 3rd byte + return id_data[2] _device_id = reduce(lambda x, y: x * 0x100 + y, id_data) return _device_id @@ -501,6 +504,8 @@ def get_flash_size(self): # F4, L0 families. flash_size, _uid = self._get_flash_size_and_uid_bulk() return flash_size + if self.device.family.flash_size_address is None: + return int(self.device.flash_size / 1024) return self._get_flash_size_raw() def get_uid(self): @@ -577,11 +582,6 @@ def detect_device(self) -> None: """Detect the device type and store in `device`.""" product_id = self.get_id() - # BlueNRG devices have the silicon cut version in the - # upper bytes of the PID. - if self.device_family == DeviceFamily.NRG.value: - product_id &= 0xFF - # Look up device details based on ID *without* bootloader ID. self.device = DEVICES.get((product_id, None)) diff --git a/src/stm32loader/device_family.py b/src/stm32loader/device_family.py index c1ba473..a46a9e9 100644 --- a/src/stm32loader/device_family.py +++ b/src/stm32loader/device_family.py @@ -30,8 +30,10 @@ class DeviceFamily(enum.Enum): # Not sure if these really exist? W = "W" - # Non-STM devices. + # BlueNRG-lineage devices NRG = "NRG" + + # Non-STM devices. WIZ = "WIZ" @@ -173,19 +175,26 @@ def __init__( # pylint: disable=too-many-arguments,too-many-positional-argument DeviceFamily.W: DeviceFamilyInfo( "W", ), - # ST BlueNRG series; see ST AN4872 (BlueNRG-1/2) - # and AN5471 (BlueNRG-LP/LPS). - # BlueNRG requires parity 'none'. - # Product ID: - # Byte 1: metal fix (masked out) - # Byte 2: mask set (masked out) - # Byte 3: 0xHL - # H: [0] BlueNRG-1, [2] BlueNRG-2, [3] BlueNRG-LP/LPS - # L: [3] 160kB, [B] 192kB, [F] 256kB + # BlueNRG-lineage devices + # AN4872: BlueNRG-1/2 + # AN5471: STM32WB05/06/07/09 (previously sold as BlueNRG-LP/LPS) + # AN5920: STM32WL3x + # The bootloader requires parity 'none'! + # The GetID command returns 3 or 4 bytes: + # Byte 1: Silicon metal fix version + # Byte 2: Silicon mask set version + # Byte 3: Product ID + # [03] BlueNRG-1 + # [2F] BlueNRG-2 + # [3B] STM32WB05 (BlueNRG-LPS) + # [3F] STM32WB06/07 (BlueNRG-LP) + # [5F] STM32WL3 + # [06] STM32WB09 + # Byte 4: (0x1F, only on STM32WB09) # There is no access to peripherals/system memory from bootloader, - # so flash size and UID can not be read. + # so flash size and UID can not be read directly # NRG-1/2: flash_size_address=0x_4010_0014, uid_address=0x_1000_07F4 - # NRG-LP: flash_size_address=0x_4000_1014, uid_address=0x_1000_1EF0 + # others: flash_size_address=0x_4000_1014, uid_address=0x_1000_1EF0 DeviceFamily.NRG: DeviceFamilyInfo( "NRG", flags=DeviceFlag.FORCE_PARITY_NONE, flash_page_size=2048 ), diff --git a/src/stm32loader/devices.py b/src/stm32loader/devices.py index 55400eb..51959f1 100644 --- a/src/stm32loader/devices.py +++ b/src/stm32loader/devices.py @@ -989,11 +989,9 @@ system=(0x_1000_0000, 0x_1000_0800), flash=(0x_1004_0000, 0x_1008_0000, 2 * kB), ), - # There is a 32kB RAM variant of BlueNRG-LP, but - # that can't be determined from bootloader DeviceInfo( "NRG", - "BlueNRG-LP", + "STM32WB06/07 (BlueNRG-LP)", pid=0x3F, bid=None, ram=(0x_2000_0000, 0x_2001_0000), @@ -1002,13 +1000,31 @@ ), DeviceInfo( "NRG", - "BlueNRG-LPS", + "STM32WB05 (BlueNRG-LPS)", pid=0x3B, bid=None, ram=(0x_2000_0000, 0x_2000_6000), system=(0x_1000_0000, 0x_1000_1800), flash=(0x_1004_0000, 0x_1007_0000, 2 * kB), ), + DeviceInfo( + "NRG", + "STM32WB09", + pid=0x06, + bid=None, + ram=(0x_2000_0000, 0x_2001_0000), + system=(0x_1000_0000, 0x_1000_1800), + flash=(0x_1004_0000, 0x_100C_0000, 2 * kB), + ), + DeviceInfo( + "NRG", + "STM32WL3x", + pid=0x5F, + bid=None, + ram=(0x_2000_0000, 0x_2000_8000), + system=(0x_1000_0000, 0x_1000_1800), + flash=(0x_1004_0000, 0x_1008_0000, 2 * kB), + ), # Wiznet W7500 DeviceInfo("WIZ", "Wiznet W7500", 0x801, bid=None, ram=None, system=None), ] diff --git a/tests/unit/test_devices.py b/tests/unit/test_devices.py index 0fae6e6..6298466 100644 --- a/tests/unit/test_devices.py +++ b/tests/unit/test_devices.py @@ -13,8 +13,6 @@ "STM32L07xxx/08xxx", "STM32L47xxx/48xxx", "STM32F10xxx", - "BlueNRG-1", - "BlueNRG-2", "STM32W", ] @@ -155,6 +153,8 @@ def test_stm32flash_device_names_match(device): 0x801, 0x03B, 0x03F, + 0x05F, + 0x006 ) if not stm32flash_device and device.product_id in stm32loader_only: return From 3762ae9c0c6b1b2cef86fea86180a0f03909bd50 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 17 Mar 2026 16:23:34 +0100 Subject: [PATCH 361/369] clean: Use __future__ annotations SQUASH: More annotations SQUASH: Format --- src/stm32loader/bootloader.py | 7 +++---- src/stm32loader/device_info.py | 17 ++++++++--------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/stm32loader/bootloader.py b/src/stm32loader/bootloader.py index 9eadfdd..1b871fc 100644 --- a/src/stm32loader/bootloader.py +++ b/src/stm32loader/bootloader.py @@ -19,6 +19,8 @@ """Talk to an STM32 native bootloader (see ST AN3155).""" +from __future__ import annotations + import enum import math import operator @@ -26,9 +28,6 @@ import time from functools import lru_cache, reduce -# FIXME: remove and switch to '|' syntax when Python 3.10+ is required. -from typing import Optional - from stm32loader.device_family import DeviceFamily, DeviceFlag from stm32loader.device_info import DeviceInfo from stm32loader.devices import DEVICES @@ -359,7 +358,7 @@ class Reply(enum.IntEnum): "H7": 128 * 1024, } - device: Optional[DeviceInfo] + device: DeviceInfo | None SYNCHRONIZE_ATTEMPTS = 2 diff --git a/src/stm32loader/device_info.py b/src/stm32loader/device_info.py index 4b796b7..c8173a5 100644 --- a/src/stm32loader/device_info.py +++ b/src/stm32loader/device_info.py @@ -1,7 +1,6 @@ """Hold information about different STM32 devices.""" -# FIXME: remove and switch to '|' syntax when Python 3.10+ is required. -from typing import Optional, Union +from __future__ import annotations from stm32loader.device_family import DEVICE_FAMILIES, DeviceFamily, DeviceFlag @@ -103,10 +102,10 @@ def __repr__(self): class Flash: # pylint: disable=too-few-public-methods """Represent info about a device's flash layout.""" - start: Optional[int] - end: Optional[int] - page_size: Union[int, list[int], None] - pages_per_sector: Optional[int] + start: int | None + end: int | None + page_size: int | list[int] | None + pages_per_sector: int | None max_write_protection_sectors: int # RM0090 4 sectors of 16 Kbytes, 1 sector of 64 Kbytes, @@ -140,14 +139,14 @@ def __init__( self.max_write_protection_sectors = max_write_protection_sectors @property - def size(self) -> Optional[int]: + def size(self) -> int | None: """Return the size of the flash memory in bytes.""" if self.start is None or self.end is None: return None return self.end - self.start - def num_pages(self) -> Optional[int]: + def num_pages(self) -> int | None: """Return the number of pages in the flash memory.""" if self.size is None or self.page_size is None: return None @@ -169,7 +168,7 @@ def num_pages(self) -> Optional[int]: f"Flash size: {self.size}, total page size: {sum(self.page_size)}" ) - def num_sectors(self) -> Optional[int]: + def num_sectors(self) -> int | None: """Return the number of sectors in the flash memory.""" num_pages = self.num_pages() if num_pages is None or self.pages_per_sector is None: From c72dc3a5a9a4a531a6394c46fac502831e6abeb8 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 17 Mar 2026 16:21:27 +0100 Subject: [PATCH 362/369] clean(cosmetic) --- src/stm32loader/bootloader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stm32loader/bootloader.py b/src/stm32loader/bootloader.py index 1b871fc..6b193a5 100644 --- a/src/stm32loader/bootloader.py +++ b/src/stm32loader/bootloader.py @@ -767,7 +767,7 @@ def extended_erase_memory(self, pages=None): self.debug(10, " Extended Erase memory done") def write_protect(self, sectors=None) -> None: - """Enable write protection on the given flash pages.""" + """Enable write protection on the given flash sectors.""" if self.device is None: raise Stm32LoaderError( From b8bb68c148b69ca46eb4cda2ca1b82207d99e91b Mon Sep 17 00:00:00 2001 From: kaidegit <2857693944@qq.com> Date: Wed, 18 Mar 2026 23:26:24 +0800 Subject: [PATCH 363/369] feat(bootloader): update transfer info after detect device --- src/stm32loader/bootloader.py | 36 ++++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/src/stm32loader/bootloader.py b/src/stm32loader/bootloader.py index 7fb206e..3a959b8 100644 --- a/src/stm32loader/bootloader.py +++ b/src/stm32loader/bootloader.py @@ -307,7 +307,8 @@ class Reply(enum.IntEnum): DATA_TRANSFER_SIZE = { # In bytes. - "default": 256, + # Some devices(like L0) may support only 128 bytes each transfer + "default": 128, "F0": 256, "F1": 256, "F3": 256, @@ -386,10 +387,28 @@ def __init__( # pylint: disable=too-many-positional-arguments,too-many-argument self.verbosity = verbosity self.show_progress = show_progress or ShowProgress(None) self.extended_erase = False - self.data_transfer_size = self.DATA_TRANSFER_SIZE.get(device_family or "default") - self.flash_page_size = self.FLASH_PAGE_SIZE.get(device_family or "default") - self.device_family = device_family or "F1" - self.device = device + if device or device_family: # using given device or device family + # give both device and device_family, use device first + if device: + self.device = device + self.device_family = device.family.name + if device_family and device_family != self.device_family: + self.debug(0, f"Device family mismatch with the given device family: {device_family}") + self.debug(0, f"Use device family: {self.device_family}") + elif device_family: + self.device_family = device_family + self.device = None + else: + self.device_family = None + self.device = None + self.update_transfer_info() + + def update_transfer_info(self): + """Update transfer info based on the device family.""" + self.data_transfer_size = self.DATA_TRANSFER_SIZE.get(self.device_family or "default") + self.flash_page_size = self.FLASH_PAGE_SIZE.get(self.device_family or "default") + self.debug(6, f"Data transfer size updated: {self.data_transfer_size} bytes") + self.debug(6, f"Flash page size updated: {self.flash_page_size} bytes") def write(self, *data): """Write the given data to the MCU.""" @@ -602,6 +621,13 @@ def detect_device(self) -> None: # with product ID *and* bootloader ID. self.device = DEVICES.get((product_id, bootloader_id), self.device) + if self.device: + if self.device_family is None: + self.device_family = self.device.family.name + else: + self.debug(1, f"Device family is already set to {self.device_family}. Not updating.") + self.update_transfer_info() + def get_bootloader_id(self): """Get the bootloader ID by reading the 'bootloader ID' register.""" if not self.device.bootloader_id_address: From 0079cad9e42d5ae00a28f0361a47c3f4167d240c Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Thu, 19 Mar 2026 10:39:18 +0100 Subject: [PATCH 364/369] clean(cosmetic) --- src/stm32loader/bootloader.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/stm32loader/bootloader.py b/src/stm32loader/bootloader.py index 3a959b8..cbeac50 100644 --- a/src/stm32loader/bootloader.py +++ b/src/stm32loader/bootloader.py @@ -387,18 +387,20 @@ def __init__( # pylint: disable=too-many-positional-arguments,too-many-argument self.verbosity = verbosity self.show_progress = show_progress or ShowProgress(None) self.extended_erase = False - if device or device_family: # using given device or device family - # give both device and device_family, use device first - if device: - self.device = device - self.device_family = device.family.name - if device_family and device_family != self.device_family: - self.debug(0, f"Device family mismatch with the given device family: {device_family}") - self.debug(0, f"Use device family: {self.device_family}") - elif device_family: - self.device_family = device_family - self.device = None + + # Try to use given device or device family. + if device: + # When given both device and device_family, use device first. + self.device = device + self.device_family = device.family.name + if device_family and device_family != self.device_family: + self.debug(0, f"Device family mismatch with the given device family: {device_family}") + self.debug(0, f"Use device family: {self.device_family}") + elif device_family: + self.device_family = device_family + self.device = None else: + # No device or device_family given. self.device_family = None self.device = None self.update_transfer_info() @@ -623,8 +625,10 @@ def detect_device(self) -> None: if self.device: if self.device_family is None: + # Device family is not manually set; take from auto-detected info. self.device_family = self.device.family.name else: + # Device family is manually set. self.debug(1, f"Device family is already set to {self.device_family}. Not updating.") self.update_transfer_info() From c859f4f4fd246a58b41c670a631ffd87018aa92a Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Thu, 19 Mar 2026 11:02:21 +0100 Subject: [PATCH 365/369] fix(test): Take page size 2KiB into account for STM32F358xx --- tests/unit/test_bootloader.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_bootloader.py b/tests/unit/test_bootloader.py index 677e6c6..3ad9f24 100644 --- a/tests/unit/test_bootloader.py +++ b/tests/unit/test_bootloader.py @@ -36,6 +36,7 @@ def data_was_written(data): @pytest.fixture def bootloader(connection): + # STM32F358xx device = DEVICES.get((0x422, 0x50)) assert device is not None, "Device not found in DEVICES mapping" return Stm32Bootloader(connection, device=device) @@ -397,13 +398,20 @@ def test_get_pages_from_range_with_invalid_start_address_raises_page_index_error def test_get_pages_from_range_with_start_address_zero_returns_single_page(bootloader): - pages = bootloader.pages_from_range(0, 1024) + pages = bootloader.pages_from_range(0, 2048) assert pages == [0] +def test_get_pages_from_range_with_end_too_small_raises_page_index_error(bootloader): + with pytest.raises( + PageIndexError, match="Erase .* address should be at a flash page boundary:.*0400.*0800" + ): + bootloader.pages_from_range(0, 1024) + + def test_get_pages_from_large_range_returns_multiple_pages(bootloader): - pages = bootloader.pages_from_range(5 * 1024, 20 * 1024) - assert pages == list(range(5, 20)) + pages = bootloader.pages_from_range(4 * 1024, 20 * 1024) + assert pages == list(range(2, 10)) def test_get_flash_size_for_standard_family(connection): From 9917ebc1710b39cabe08839b9d1ab4958dd6b5c7 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Thu, 19 Mar 2026 11:02:39 +0100 Subject: [PATCH 366/369] clean(lint) --- src/stm32loader/bootloader.py | 14 +++++++++++--- tests/unit/test_devices.py | 2 +- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/stm32loader/bootloader.py b/src/stm32loader/bootloader.py index cbeac50..899b0ac 100644 --- a/src/stm32loader/bootloader.py +++ b/src/stm32loader/bootloader.py @@ -32,6 +32,8 @@ from stm32loader.device_info import DeviceInfo from stm32loader.devices import DEVICES +# pylint: disable=too-many-lines + CHIP_IDS = { # see ST AN2606 Table 136 Bootloader device-dependent parameters # 16 to 32 KiB @@ -387,6 +389,7 @@ def __init__( # pylint: disable=too-many-positional-arguments,too-many-argument self.verbosity = verbosity self.show_progress = show_progress or ShowProgress(None) self.extended_erase = False + self.supported_commands = {} # Try to use given device or device family. if device: @@ -394,7 +397,9 @@ def __init__( # pylint: disable=too-many-positional-arguments,too-many-argument self.device = device self.device_family = device.family.name if device_family and device_family != self.device_family: - self.debug(0, f"Device family mismatch with the given device family: {device_family}") + self.debug( + 0, f"Device family mismatch with the given device family: {device_family}" + ) self.debug(0, f"Use device family: {self.device_family}") elif device_family: self.device_family = device_family @@ -625,11 +630,14 @@ def detect_device(self) -> None: if self.device: if self.device_family is None: - # Device family is not manually set; take from auto-detected info. + # Device family is not manually set. + # Take from auto-detected info. self.device_family = self.device.family.name else: # Device family is manually set. - self.debug(1, f"Device family is already set to {self.device_family}. Not updating.") + self.debug( + 1, f"Device family is already set to {self.device_family}. Not updating." + ) self.update_transfer_info() def get_bootloader_id(self): diff --git a/tests/unit/test_devices.py b/tests/unit/test_devices.py index 45e8aab..6bc673b 100644 --- a/tests/unit/test_devices.py +++ b/tests/unit/test_devices.py @@ -155,7 +155,7 @@ def test_stm32flash_device_names_match(device): 0x03B, 0x03F, 0x05F, - 0x006 + 0x006, ) if not stm32flash_device and device.product_id in stm32loader_only: return From bb1c9b796213bd4f60da04620006b47c6c28e1ef Mon Sep 17 00:00:00 2001 From: kaidegit <2857693944@qq.com> Date: Thu, 19 Mar 2026 21:09:17 +0800 Subject: [PATCH 367/369] feat: support gd32vw55x --- src/stm32loader/bootloader.py | 43 ++++++++++++++- src/stm32loader/device_family.py | 8 +++ src/stm32loader/devices.py | 91 ++++++++++++++++++++++++++++++++ 3 files changed, 141 insertions(+), 1 deletion(-) diff --git a/src/stm32loader/bootloader.py b/src/stm32loader/bootloader.py index 899b0ac..f4a18f1 100644 --- a/src/stm32loader/bootloader.py +++ b/src/stm32loader/bootloader.py @@ -90,6 +90,16 @@ # Cortex-M0 MCU with hardware TCP/IP and MAC # (SweetPeas custom bootloader) 0x801: "Wiznet W7500", + # GigaDevice GD32VW55x series + # Uses 0x06 command to get part number (pid is 4-byte ASCII in little-endian) + 0x50494B36: "GD32VW553KIQ6", + 0x504D4B36: "GD32VW553KMQ6", + 0x50494836: "GD32VW553HIQ6", + 0x504D4836: "GD32VW553HMQ6", + 0x50494B37: "GD32VW553KIQ7", + 0x504D4B37: "GD32VW553KMQ7", + 0x50494837: "GD32VW553HIQ7", + 0x504D4837: "GD32VW553HMQ7", } @@ -215,6 +225,9 @@ class Command(enum.IntEnum): WRITE_PROTECT = 0x63 WRITE_UNPROTECT = 0x73 + # GD-specific command to get part number (GD32VW553 series) + GET_GD_ID = 0x06 + # not really listed under commands, but still... # 'wake the bootloader' == 'activate USART' == 'synchronize' SYNCHRONIZE = 0x7F @@ -324,6 +337,10 @@ class Reply(enum.IntEnum): # ST RM0433 section 4.2 FLASH main features "H7": 256, "G4": 256, + # GigaDevice GD32VW553 series. + # In GD32 ISP Tool they send 240 bytes per transfer. + # In AN027 they suggest 252 bytes per transfer. + "GD32VW55x": 240, } FLASH_PAGE_SIZE = { @@ -359,6 +376,8 @@ class Reply(enum.IntEnum): "G4": 2048, # this is valid only for dual bank mode # ST RM0433 section 4.2 FLASH main features "H7": 128 * 1024, + # GD32VW55x series + "GD32VW55x": 4096, } device: DeviceInfo | None @@ -529,6 +548,24 @@ def get_id(self): _device_id = reduce(lambda x, y: x * 0x100 + y, id_data) return _device_id + def get_gd_id(self): + """Send the 'Get GD ID' command and return the chip/product/device ID.""" + self.command(self.Command.GET_GD_ID, "Get GD ID") + length = bytearray(self.connection.read())[0] + # GD32 0x06 command returns N+1 bytes where N is the count, + # but the last byte is ACK, not part of data + id_data = bytearray(self.connection.read(length)) + self._wait_for_ack("0x06 end") + # Convert 4-byte part number to pid (little-endian 32-bit integer) + # For GD32 devices, the part number is returned as ASCII characters + # Example: "7HMP" -> 0x37 0x48 0x4D 0x50 -> 0x504D4837 + _part_number_to_pid = lambda data: ( + (data[3] << 24) | (data[2] << 16) | (data[1] << 8) | data[0] + if len(data) >= 4 else 0 + ) + _device_id = _part_number_to_pid(id_data) + return _device_id + def get_flash_size(self): """Return the MCU's flash size in kilobytes.""" if self.device.flags & DeviceFlag.LONG_UID_ACCESS: @@ -611,7 +648,11 @@ def _get_flash_size_and_uid_bulk(self): def detect_device(self) -> None: """Detect the device type and store in `device`.""" - product_id = self.get_id() + product_id = None + try: + product_id = self.get_id() + except CommandError: + product_id = self.get_gd_id() # Look up device details based on ID *without* bootloader ID. self.device = DEVICES.get((product_id, None)) diff --git a/src/stm32loader/device_family.py b/src/stm32loader/device_family.py index 013f208..9b540a6 100644 --- a/src/stm32loader/device_family.py +++ b/src/stm32loader/device_family.py @@ -36,6 +36,7 @@ class DeviceFamily(enum.Enum): # Non-STM devices. WIZ = "WIZ" + GD32VW55x = "GD32VW55x" @enum.unique @@ -211,6 +212,13 @@ def __init__( # pylint: disable=too-many-arguments,too-many-positional-argument DeviceFamily.WIZ: DeviceFamilyInfo( "WIZ", ), + # GigaDevice GD32VW55x series. + # Uses 0x06 command to get part number instead of standard 0x02. + DeviceFamily.GD32VW55x: DeviceFamilyInfo( + "GD32VW55x", + flash_page_size=4096, + transfer_size=240, + ), } diff --git a/src/stm32loader/devices.py b/src/stm32loader/devices.py index 00ad731..7dd722c 100644 --- a/src/stm32loader/devices.py +++ b/src/stm32loader/devices.py @@ -1080,6 +1080,97 @@ ), # Wiznet W7500 DeviceInfo("WIZ", "Wiznet W7500", 0x801, bid=None, ram=None, system=None), + # GigaDevice GD32VW553 series + # Uses 0x06 command to get part number (pid is 4-byte ASCII in little-endian) + # Find these pid in GD32_ISP_CLI(Linux)->libGD_MCU_DLL.so->BuildMap_GD32103 + # pid "6KIP" = 0x36 0x4B 0x49 0x50 -> 0x50494B36 + DeviceInfo( + "GD32VW55x", + "GD32VW553KIQ6", + pid=0x50494B36, + bid=None, + ram=(0x20000000, 0x20050000), # 320KB + system=(0x0BF40000, 0x0BF80000), + flash=(0x08000000, 0x08200000, 4 * kB), # 2MB, 4KB pages + option=(0x0FFC0000, 0x0FFC0100), + ), + # pid "6KMP" = 0x36 0x4B 0x4D 0x50 -> 0x504D4B36 + DeviceInfo( + "GD32VW55x", + "GD32VW553KMQ6", + pid=0x504D4B36, + bid=None, + ram=(0x20000000, 0x20050000), # 320KB + system=(0x0BF40000, 0x0BF80000), + flash=(0x08000000, 0x08400000, 4 * kB), # 4MB, 4KB pages + option=(0x0FFC0000, 0x0FFC0100), + ), + # pid "6HIP" = 0x36 0x48 0x49 0x50 -> 0x50494836 + DeviceInfo( + "GD32VW55x", + "GD32VW553HIQ6", + pid=0x50494836, + bid=None, + ram=(0x20000000, 0x20050000), # 320KB + system=(0x0BF40000, 0x0BF80000), + flash=(0x08000000, 0x08200000, 4 * kB), # 2MB, 4KB pages + option=(0x0FFC0000, 0x0FFC0100), + ), + # pid "6HMP" = 0x36 0x48 0x4D 0x50 -> 0x504D4836 + DeviceInfo( + "GD32VW55x", + "GD32VW553HMQ6", + pid=0x504D4836, + bid=None, + ram=(0x20000000, 0x20050000), # 320KB + system=(0x0BF40000, 0x0BF80000), + flash=(0x08000000, 0x08400000, 4 * kB), # 4MB, 4KB pages + option=(0x0FFC0000, 0x0FFC0100), + ), + # pid "7KIP" = 0x37 0x4B 0x49 0x50 -> 0x50494B37 + DeviceInfo( + "GD32VW55x", + "GD32VW553KIQ7", + pid=0x50494B37, + bid=None, + ram=(0x20000000, 0x20050000), # 320KB + system=(0x0BF40000, 0x0BF80000), + flash=(0x08000000, 0x08200000, 4 * kB), # 2MB, 4KB pages + option=(0x0FFC0000, 0x0FFC0100), + ), + # pid "7KMP" = 0x37 0x4B 0x4D 0x50 -> 0x504D4B37 + DeviceInfo( + "GD32VW55x", + "GD32VW553KMQ7", + pid=0x504D4B37, + bid=None, + ram=(0x20000000, 0x20050000), # 320KB + system=(0x0BF40000, 0x0BF80000), + flash=(0x08000000, 0x08400000, 4 * kB), # 4MB, 4KB pages + option=(0x0FFC0000, 0x0FFC0100), + ), + # pid "7HIP" = 0x37 0x48 0x49 0x50 -> 0x50494837 + DeviceInfo( + "GD32VW55x", + "GD32VW553HIQ7", + pid=0x50494837, + bid=None, + ram=(0x20000000, 0x20050000), # 320KB + system=(0x0BF40000, 0x0BF80000), + flash=(0x08000000, 0x08200000, 4 * kB), # 2MB, 4KB pages + option=(0x0FFC0000, 0x0FFC0100), + ), + # pid "7HMP" = 0x37 0x48 0x4D 0x50 -> 0x504D4837 + DeviceInfo( + "GD32VW55x", + "GD32VW553HMQ7", + pid=0x504D4837, + bid=None, + ram=(0x20000000, 0x20050000), # 320KB + system=(0x0BF40000, 0x0BF80000), + flash=(0x08000000, 0x08400000, 4 * kB), # 4MB, 4KB pages + option=(0x0FFC0000, 0x0FFC0100), + ), ] DEVICES = {(dev.product_id, dev.bootloader_id): dev for dev in DEVICE_DETAILS} From 410fc81cfa6731b80df45b36396a3ce426d978cb Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Thu, 19 Mar 2026 08:55:12 +0100 Subject: [PATCH 368/369] fix(test): Ignore GigaDevice parts when comparing devices --- tests/unit/test_devices.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/unit/test_devices.py b/tests/unit/test_devices.py index 6bc673b..b06ec7e 100644 --- a/tests/unit/test_devices.py +++ b/tests/unit/test_devices.py @@ -156,6 +156,14 @@ def test_stm32flash_device_names_match(device): 0x03F, 0x05F, 0x006, + 0x50494B36, + 0x504D4B36, + 0x50494836, + 0x504D4836, + 0x50494B37, + 0x504D4B37, + 0x50494837, + 0x504D4837, ) if not stm32flash_device and device.product_id in stm32loader_only: return From b736df5577afab62d23fc97763f34ce47efd81dc Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Fri, 20 Mar 2026 11:00:13 +0100 Subject: [PATCH 369/369] clean(lint) --- src/stm32loader/bootloader.py | 28 ++++++++++++++++------------ src/stm32loader/device_family.py | 6 +++--- src/stm32loader/devices.py | 22 ++++++++++++---------- 3 files changed, 31 insertions(+), 25 deletions(-) diff --git a/src/stm32loader/bootloader.py b/src/stm32loader/bootloader.py index f4a18f1..d90bcd3 100644 --- a/src/stm32loader/bootloader.py +++ b/src/stm32loader/bootloader.py @@ -91,7 +91,8 @@ # (SweetPeas custom bootloader) 0x801: "Wiznet W7500", # GigaDevice GD32VW55x series - # Uses 0x06 command to get part number (pid is 4-byte ASCII in little-endian) + # Uses 0x06 command to get part number + # (pid is 4-byte ASCII in little-endian) 0x50494B36: "GD32VW553KIQ6", 0x504D4B36: "GD32VW553KMQ6", 0x50494836: "GD32VW553HIQ6", @@ -340,7 +341,7 @@ class Reply(enum.IntEnum): # GigaDevice GD32VW553 series. # In GD32 ISP Tool they send 240 bytes per transfer. # In AN027 they suggest 252 bytes per transfer. - "GD32VW55x": 240, + "GD32VW55X": 240, } FLASH_PAGE_SIZE = { @@ -377,7 +378,7 @@ class Reply(enum.IntEnum): # ST RM0433 section 4.2 FLASH main features "H7": 128 * 1024, # GD32VW55x series - "GD32VW55x": 4096, + "GD32VW55X": 4096, } device: DeviceInfo | None @@ -549,21 +550,14 @@ def get_id(self): return _device_id def get_gd_id(self): - """Send the 'Get GD ID' command and return the chip/product/device ID.""" + """Send the 'Get GD ID' command and return the device ID.""" self.command(self.Command.GET_GD_ID, "Get GD ID") length = bytearray(self.connection.read())[0] # GD32 0x06 command returns N+1 bytes where N is the count, # but the last byte is ACK, not part of data id_data = bytearray(self.connection.read(length)) self._wait_for_ack("0x06 end") - # Convert 4-byte part number to pid (little-endian 32-bit integer) - # For GD32 devices, the part number is returned as ASCII characters - # Example: "7HMP" -> 0x37 0x48 0x4D 0x50 -> 0x504D4837 - _part_number_to_pid = lambda data: ( - (data[3] << 24) | (data[2] << 16) | (data[1] << 8) | data[0] - if len(data) >= 4 else 0 - ) - _device_id = _part_number_to_pid(id_data) + _device_id = self._gd_part_number_to_pid(id_data) return _device_id def get_flash_size(self): @@ -1069,3 +1063,13 @@ def _encode_address(address): # checksum as single byte checksum_byte = struct.pack("B", reduce(operator.xor, address_bytes)) return address_bytes + checksum_byte + + @staticmethod + def _gd_part_number_to_pid(data): + # Convert 4-byte part number to pid (little-endian 32-bit integer) + # For GD32 devices, the part number is returned as ASCII characters + # Example: "7HMP" -> 0x37 0x48 0x4D 0x50 -> 0x504D4837 + pid = 0 + if len(data) >= 4: + pid = (data[3] << 24) | (data[2] << 16) | (data[1] << 8) | data[0] + return pid diff --git a/src/stm32loader/device_family.py b/src/stm32loader/device_family.py index 9b540a6..e9fca3a 100644 --- a/src/stm32loader/device_family.py +++ b/src/stm32loader/device_family.py @@ -36,7 +36,7 @@ class DeviceFamily(enum.Enum): # Non-STM devices. WIZ = "WIZ" - GD32VW55x = "GD32VW55x" + GD32VW55X = "GD32VW55X" @enum.unique @@ -214,8 +214,8 @@ def __init__( # pylint: disable=too-many-arguments,too-many-positional-argument ), # GigaDevice GD32VW55x series. # Uses 0x06 command to get part number instead of standard 0x02. - DeviceFamily.GD32VW55x: DeviceFamilyInfo( - "GD32VW55x", + DeviceFamily.GD32VW55X: DeviceFamilyInfo( + "GD32VW55X", flash_page_size=4096, transfer_size=240, ), diff --git a/src/stm32loader/devices.py b/src/stm32loader/devices.py index 7dd722c..e7c8bbb 100644 --- a/src/stm32loader/devices.py +++ b/src/stm32loader/devices.py @@ -1081,11 +1081,13 @@ # Wiznet W7500 DeviceInfo("WIZ", "Wiznet W7500", 0x801, bid=None, ram=None, system=None), # GigaDevice GD32VW553 series - # Uses 0x06 command to get part number (pid is 4-byte ASCII in little-endian) - # Find these pid in GD32_ISP_CLI(Linux)->libGD_MCU_DLL.so->BuildMap_GD32103 + # Uses 0x06 command to get part number (pid is 4-byte ASCII + # in little-endian). + # Find these pid in GD32_ISP_CLI(Linux)->libGD_MCU_DLL.so + # ->BuildMap_GD32103 # pid "6KIP" = 0x36 0x4B 0x49 0x50 -> 0x50494B36 DeviceInfo( - "GD32VW55x", + "GD32VW55X", "GD32VW553KIQ6", pid=0x50494B36, bid=None, @@ -1096,7 +1098,7 @@ ), # pid "6KMP" = 0x36 0x4B 0x4D 0x50 -> 0x504D4B36 DeviceInfo( - "GD32VW55x", + "GD32VW55X", "GD32VW553KMQ6", pid=0x504D4B36, bid=None, @@ -1107,7 +1109,7 @@ ), # pid "6HIP" = 0x36 0x48 0x49 0x50 -> 0x50494836 DeviceInfo( - "GD32VW55x", + "GD32VW55X", "GD32VW553HIQ6", pid=0x50494836, bid=None, @@ -1118,7 +1120,7 @@ ), # pid "6HMP" = 0x36 0x48 0x4D 0x50 -> 0x504D4836 DeviceInfo( - "GD32VW55x", + "GD32VW55X", "GD32VW553HMQ6", pid=0x504D4836, bid=None, @@ -1129,7 +1131,7 @@ ), # pid "7KIP" = 0x37 0x4B 0x49 0x50 -> 0x50494B37 DeviceInfo( - "GD32VW55x", + "GD32VW55X", "GD32VW553KIQ7", pid=0x50494B37, bid=None, @@ -1140,7 +1142,7 @@ ), # pid "7KMP" = 0x37 0x4B 0x4D 0x50 -> 0x504D4B37 DeviceInfo( - "GD32VW55x", + "GD32VW55X", "GD32VW553KMQ7", pid=0x504D4B37, bid=None, @@ -1151,7 +1153,7 @@ ), # pid "7HIP" = 0x37 0x48 0x49 0x50 -> 0x50494837 DeviceInfo( - "GD32VW55x", + "GD32VW55X", "GD32VW553HIQ7", pid=0x50494837, bid=None, @@ -1162,7 +1164,7 @@ ), # pid "7HMP" = 0x37 0x48 0x4D 0x50 -> 0x504D4837 DeviceInfo( - "GD32VW55x", + "GD32VW55X", "GD32VW553HMQ7", pid=0x504D4837, bid=None,