Permalink
Cannot retrieve contributors at this time
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Espruino/scripts/stm32loader.py
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
executable file
610 lines (520 sloc)
19.9 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
# -*- coding: utf-8 -*- | |
# vim: sw=4:ts=4:si:et:enc=utf-8 | |
# Author: Ivan A-R <ivan@tuxotronic.org> | |
# With hacky error recovery by Gordon Williams <gw@pur3.co.uk> | |
# Project page: http://tuxotronic.org/wiki/projects/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 COPYING3. If not see | |
# <http://www.gnu.org/licenses/>. | |
from __future__ import print_function | |
import sys, getopt | |
import serial | |
import time | |
import glob | |
import time | |
import tempfile | |
import os | |
import subprocess | |
try: | |
from progressbar import * | |
usepbar = 1 | |
except: | |
usepbar = 0 | |
# Verbose level | |
QUIET = 5 | |
def mdebug(level, message): | |
if QUIET >= level: | |
print(message, file=sys.stderr) | |
# Takes chip IDs (obtained via Get ID command) to human-readable names | |
CHIP_ID_STRS = {0x410: 'STM32F1, performance, medium-density', | |
0x411: 'STM32F2', | |
0x412: 'STM32F1, performance, low-density', | |
0x413: 'STM32F4', | |
0x414: 'STM32F1, performance, high-density', | |
0x416: 'STM32L1, performance, medium-density', | |
0x418: 'STM32F1, connectivity', | |
0x420: 'STM32F1, value, medium-density', | |
0x428: 'STM32F1, value, high-density', | |
0x430: 'STM32F1, performance, XL-density'} | |
class CmdException(Exception): | |
pass | |
class CommandInterface(object): | |
def open(self, aport='/dev/tty.usbserial-FTD3TMCH', abaudrate=115200) : | |
self.sp = serial.Serial( | |
port=aport, | |
baudrate=abaudrate, # baudrate | |
bytesize=8, # number of databits | |
parity=serial.PARITY_EVEN, | |
stopbits=1, | |
xonxoff=0, # enable software flow control | |
rtscts=0, # disable RTS/CTS flow control | |
timeout=0.5 # set a timeout value, None for waiting forever | |
) | |
def _wait_for_ack(self, info="", timeout=0): | |
stop = time.time() + timeout | |
got = None | |
while not got: | |
got = self.sp.read(1) | |
if time.time() > stop: | |
break | |
if not got: | |
raise CmdException("No response to %s" % info) | |
# wait for ask | |
ask = ord(got) | |
if ask == 0x79: | |
# ACK | |
return 1 | |
elif ask == 0x1F: | |
# NACK | |
raise CmdException("Chip replied with a NACK during %s" % info) | |
# Unknown response | |
raise CmdException("Unrecognised response 0x%x to %s" % (ask, info)) | |
def reset(self): | |
self.sp.setDTR(0) | |
time.sleep(0.1) | |
self.sp.setDTR(1) | |
time.sleep(0.5) | |
def initChip(self): | |
# Set boot | |
self.sp.setRTS(0) | |
self.reset() | |
# Be a bit more persistent when trying to initialise the chip | |
stop = time.time() + 5.0 | |
while time.time() <= stop: | |
self.sp.write('\x7f') | |
got = self.sp.read() | |
# The chip will ACK a sync the very first time and | |
# NACK it every time afterwards | |
if got and got in '\x79\x1f': | |
# Synced up | |
return | |
raise CmdException('No response while trying to sync') | |
def releaseChip(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_ack(hex(cmd)) | |
def cmdGet(self): | |
if self.cmdGeneric(0x00): | |
mdebug(10, "*** Get command"); | |
len = 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)) | |
mdebug(10, " Available commands: "+str(dat)) | |
self._wait_for_ack("0x00 end") | |
return version | |
else: | |
raise CmdException("Get (0x00) failed") | |
def cmdGetVersion(self): | |
if self.cmdGeneric(0x01): | |
mdebug(10, "*** GetVersion command") | |
version = ord(self.sp.read()) | |
self.sp.read(2) | |
self._wait_for_ack("0x01 end") | |
mdebug(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) | |
self._wait_for_ack("0x02 end") | |
return 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 | |
crc = byte0 ^ byte1 ^ byte2 ^ byte3 | |
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)) | |
self._wait_for_ack("0x11 address failed") | |
N = (lng - 1) & 0xFF | |
crc = N ^ 0xFF | |
self.sp.write(chr(N) + chr(crc)) | |
self._wait_for_ack("0x11 length failed") | |
return map(lambda c: ord(c), self.sp.read(lng)) | |
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)) | |
self._wait_for_ack("0x21 go failed") | |
else: | |
raise CmdException("Go (0x21) failed") | |
def cmdWriteMemory(self, addr, data): | |
assert(len(data) <= 256) | |
if self.cmdGeneric(0x31): | |
mdebug(10, "*** Write memory command") | |
self.sp.write(self._encode_addr(addr)) | |
self._wait_for_ack("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 | |
crc = 0xFF | |
try: | |
datastr = "" | |
for c in data: | |
crc = crc ^ c | |
datastr = datastr+chr(c) | |
datastr = datastr + chr(crc) | |
self.sp.write(datastr) | |
self._wait_for_ack("0x31 programming failed") | |
mdebug(10, " Write memory done") | |
except: | |
mdebug(5, " WRITE FAIL - try and recover") | |
for c in data: | |
self.sp.write(chr(255)) | |
mdebug(5, " WRITE FAIL - wait") | |
stop = time.time() + 1 | |
while time.time() < stop: | |
if self.sp.inWaiting()>0: self.sp.read(self.sp.inWaiting()) | |
mdebug(5, " WRITE FAIL - retry") | |
self.cmdWriteMemory(addr, data) | |
else: | |
raise CmdException("Write memory (0x31) failed") | |
def cmdEraseMemory(self, sectors = None): | |
if self.cmdGeneric(0x43): | |
mdebug(10, "*** Erase memory command") | |
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_ack("0x43 erasing failed") | |
mdebug(10, " Erase memory done") | |
else: | |
raise CmdException("Erase memory (0x43) failed") | |
# TODO support for non-global mass erase | |
GLOBAL_ERASE_TIMEOUT_SECONDS = 20 # This takes a while | |
def cmdExtendedEraseMemory(self): | |
if self.cmdGeneric(0x44): | |
mdebug(10, "*** Extended erase memory command") | |
# Global mass erase | |
mdebug(5, "Global mass erase; this may take a while") | |
self.sp.write(chr(0xFF)) | |
self.sp.write(chr(0xFF)) | |
# Checksum | |
self.sp.write(chr(0x00)) | |
self._wait_for_ack("0x44 extended erase failed", | |
timeout=self.GLOBAL_ERASE_TIMEOUT_SECONDS) | |
mdebug(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") | |
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_ack("0x63 write protect failed") | |
mdebug(10, " Write protect done") | |
else: | |
raise CmdException("Write Protect memory (0x63) failed") | |
def cmdWriteUnprotect(self): | |
if self.cmdGeneric(0x73): | |
mdebug(10, "*** Write Unprotect command") | |
self._wait_for_ack("0x73 write unprotect failed") | |
self._wait_for_ack("0x73 write unprotect 2 failed") | |
mdebug(10, " Write Unprotect done") | |
else: | |
raise CmdException("Write Unprotect (0x73) failed") | |
def cmdReadoutProtect(self): | |
if self.cmdGeneric(0x82): | |
mdebug(10, "*** Readout protect command") | |
self._wait_for_ack("0x82 readout protect failed") | |
self._wait_for_ack("0x82 readout protect 2 failed") | |
mdebug(10, " Read protect done") | |
else: | |
raise CmdException("Readout protect (0x82) failed") | |
def cmdReadoutUnprotect(self): | |
if self.cmdGeneric(0x92): | |
mdebug(10, "*** Readout Unprotect command") | |
self._wait_for_ack("0x92 readout unprotect failed") | |
self._wait_for_ack("0x92 readout unprotect 2 failed") | |
mdebug(10, " Read Unprotect done") | |
else: | |
raise CmdException("Readout unprotect (0x92) failed") | |
# Complex commands section | |
def readMemory(self, addr, lng): | |
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() | |
else: | |
mdebug(5, "Read %(len)d bytes at 0x%(addr)X" % {'addr': addr, 'len': 256}) | |
data = data + self.cmdReadMemory(addr, lng) | |
return data | |
def writeMemory(self, addr, data): | |
lng = len(data) | |
mdebug(5, "Writing %(lng)d bytes to start address 0x%(addr)X" % | |
{ 'lng': lng, 'addr': addr}) | |
if usepbar: | |
widgets = ['Writing: ', Percentage(),' ', ETA(), ' ', Bar()] | |
pbar = ProgressBar(widgets=widgets, maxval=lng, term_width=79).start() | |
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]) | |
offs = offs + 256 | |
addr = addr + 256 | |
lng = lng - 256 | |
if usepbar: | |
pbar.update(pbar.maxval-lng) | |
pbar.finish() | |
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 PCLKHack(self): | |
RCC_CFGR = 0x40021004 | |
mdebug(5, "Modifying PCLK speed at 0x%(addr)X" % {'addr': RCC_CFGR}) | |
# reg = self.cmdReadMemory(RCC_CFGR, 4) | |
# reg[1] = (reg[1] & 0xF8) | 0x04 | |
reg = [10, 60, 29, 0] | |
# self.cmdWriteMemory(RCC_CFGR, reg) | |
if self.cmdGeneric(0x31): | |
self.sp.write(self._encode_addr(RCC_CFGR)) | |
self._wait_for_ack("0x31 address failed") | |
self.sp.write(chr(3)) # len really | |
self.sp.write(chr(reg[0])) | |
self.sp.write(chr(reg[1])) | |
self.sp.write(chr(reg[2])) | |
self.sp.write(chr(reg[3])) | |
crc = 3^reg[0]^reg[1]^reg[2]^reg[3]; | |
self.sp.write(chr(crc)) | |
self._wait_for_ack("0x31 programming failed") | |
mdebug(10, " PCLK write memory done") | |
def resetDevice(self): | |
AIRCR = 0xE000ED0C | |
mdebug(5, "Writing to Reset Register") | |
reg = [0x04,0x00,0xFA,0x05] | |
if self.cmdGeneric(0x31): | |
self.sp.write(self._encode_addr(AIRCR)) | |
self._wait_for_ack("0x31 address failed") | |
self.sp.write(chr(3)) # len really | |
self.sp.write(chr(reg[0])) | |
self.sp.write(chr(reg[1])) | |
self.sp.write(chr(reg[2])) | |
self.sp.write(chr(reg[3])) | |
crc = 3^reg[0]^reg[1]^reg[2]^reg[3]; | |
self.sp.write(chr(crc)) | |
# don't wait for ack - device will have rebooted | |
mdebug(10, " reset done") | |
def usage(): | |
print("""Usage: %s [-hqVewvrX] [-l length] [-p port] [-b baud] [-a addr] [file.bin] | |
-h This help | |
-q Quiet | |
-V Verbose | |
-e Erase | |
-w Write | |
-v Verify | |
-X Reset after | |
-r Read | |
-l length Length of read | |
-p port Serial port (default: first USB-like port in /dev) | |
-b baud Baud speed (default: 115200) | |
-a addr Target address | |
-s n Skip writing N bytes from beginning of the binary (does not affect start address) | |
-k Change PCLK frequency to make USB stable on Espruino 1v43 bootloaders | |
./stm32loader.py -e -w -v example/main.bin | |
""" % sys.argv[0]) | |
def read(filename): | |
"""Read the file to be programmed and turn it into a binary""" | |
with open(filename, 'rb') as f: | |
bytes = f.read() | |
if bytes.startswith('\x7FELF'): | |
# Actually an ELF file. Convert to binary | |
handle, path = tempfile.mkstemp(suffix='.bin', prefix='stm32loader') | |
try: | |
os.close(handle) | |
# Try a couple of options for objcopy | |
for name in ['arm-none-eabi-objcopy', 'arm-linux-gnueabi-objcopy']: | |
try: | |
code = subprocess.call([name, '-Obinary', filename, path]) | |
if code == 0: | |
return read(path) | |
except OSError: | |
pass | |
else: | |
raise Exception('Error %d while converting to a binary file' % code) | |
finally: | |
# Remove the temporary file | |
os.unlink(path) | |
else: | |
return [ord(x) for x in bytes] | |
if __name__ == "__main__": | |
had_error = False | |
conf = { | |
'port': 'auto', | |
'baud': 115200, | |
'address': 0x08000000, | |
'skip' : 0, | |
'erase': 0, | |
'write': 0, | |
'verify': 0, | |
'read': 0, | |
'reset': 0, | |
'len': 1000, | |
'fname':'', | |
'pclk_hack':0, | |
} | |
# http://www.python.org/doc/2.5.2/lib/module-getopt.html | |
try: | |
opts, args = getopt.getopt(sys.argv[1:], "hqVewvrXp:b:a:s:l:k") | |
except getopt.GetoptError as err: | |
# print help information and exit: | |
print(str(err)) # will print something like "option -a not recognized" | |
usage() | |
sys.exit(2) | |
for o, a in opts: | |
if o == '-V': | |
QUIET = 10 | |
elif o == '-q': | |
QUIET = 0 | |
elif o == '-h': | |
usage() | |
sys.exit(0) | |
elif o == '-e': | |
conf['erase'] = 1 | |
elif o == '-w': | |
conf['write'] = 1 | |
elif o == '-v': | |
conf['verify'] = 1 | |
elif o == '-r': | |
conf['read'] = 1 | |
elif o == '-X': | |
conf['reset'] = 1 | |
elif o == '-p': | |
conf['port'] = a | |
elif o == '-b': | |
conf['baud'] = eval(a) | |
elif o == '-a': | |
conf['address'] = eval(a) | |
elif o == '-s': | |
conf['skip'] = eval(a) | |
elif o == '-l': | |
conf['len'] = eval(a) | |
elif o == '-k': | |
conf['pclk_hack'] = 1 | |
else: | |
assert False, "unhandled option" | |
# Try and find the port automatically | |
if conf['port'] == 'auto': | |
ports = [] | |
# Get a list of all USB-like names in /dev | |
for name in ['tty.usbserial', 'ttyUSB']: | |
ports.extend(glob.glob('/dev/%s*' % name)) | |
ports = sorted(ports) | |
if ports: | |
# Found something - take it | |
conf['port'] = ports[0] | |
cmd = CommandInterface() | |
cmd.open(conf['port'], conf['baud']) | |
mdebug(10, "Open port %(port)s, baud %(baud)d" % {'port':conf['port'], | |
'baud':conf['baud']}) | |
try: | |
if (conf['write'] or conf['verify']): | |
mdebug(5, "Reading data from %s" % args[0]) | |
data = read(args[0]) | |
if conf['skip']: | |
mdebug(5, "Skipping %d bytes" % conf['skip']) | |
data = data[conf['skip']:] | |
try: | |
cmd.initChip() | |
except CmdException: | |
print("Can't init. Ensure BOOT0=1, BOOT1=0, and reset device") | |
bootversion = cmd.cmdGet() | |
mdebug(0, "Bootloader version 0x%X" % bootversion) | |
if bootversion < 20 or bootversion >= 100: | |
raise Exception('Unreasonable bootloader version %d' % bootversion) | |
chip_id = cmd.cmdGetID() | |
assert len(chip_id) == 2, "Unreasonable chip id: %s" % repr(chip_id) | |
chip_id_num = (ord(chip_id[0]) << 8) | ord(chip_id[1]) | |
chip_id_str = CHIP_ID_STRS.get(chip_id_num, None) | |
if chip_id_str is None: | |
mdebug(0, 'Warning: unrecognised chip ID 0x%x' % chip_id_num) | |
else: | |
mdebug(0, "Chip id 0x%x, %s" % (chip_id_num, chip_id_str)) | |
if conf['pclk_hack']: | |
cmd.PCLKHack() | |
if conf['erase']: | |
# Pre-3.0 bootloaders use the erase memory | |
# command. Starting with 3.0, extended erase memory | |
# replaced this command. | |
if bootversion < 0x30: | |
cmd.cmdEraseMemory() | |
else: | |
cmd.cmdExtendedEraseMemory() | |
if conf['write']: | |
cmd.writeMemory(conf['address'], data) | |
if conf['verify']: | |
verify = cmd.readMemory(conf['address'], len(data)) | |
if(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])) | |
had_error = True | |
if not conf['write'] and conf['read']: | |
rdata = cmd.readMemory(conf['address'], conf['len']) | |
file(args[0], 'wb').write(''.join(map(chr,rdata))) | |
if conf['reset']: | |
cmd.resetDevice() | |
finally: | |
if not conf['reset']: | |
cmd.releaseChip() | |
if had_error: exit(1) | |