arduino-pico/tools/uf2conv.py
Earle F. Philhower, III 75bab41e39
Make uf2conf.py flush STDOUT for real-time updates (#1194)
Without flushing STDOUT, the upload script's output aften are buffered
and not displayed until it completes.  Add in flush commands to allow
the IDE to display status as it changes.
2023-02-15 12:13:55 -08:00

442 lines
15 KiB
Python
Executable file

#!/usr/bin/env python3
# UF2 upload utility taken from https://github.com/microsoft/uf2/blob/master/utils/uf2conv.py
# Microsoft UF2
# The MIT License (MIT)
# Copyright (c) Microsoft Corporation
# All rights reserved.
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import sys
import struct
import subprocess
import re
import os
import os.path
import argparse
import time
import glob
toolspath = os.path.dirname(os.path.realpath(__file__)).replace('\\', '/') # CWD in UNIX format
try:
sys.path.insert(0, toolspath + "/pyserial") # Add pyserial dir to search path
import serial # If this fails, we can't continue and will bomb below
except Exception:
sys.stderr.write("pyserial directory not found next to this upload.py tool.\n")
sys.exit(1)
UF2_MAGIC_START0 = 0x0A324655 # "UF2\n"
UF2_MAGIC_START1 = 0x9E5D5157 # Randomly selected
UF2_MAGIC_END = 0x0AB16F30 # Ditto
families = {
'SAMD21': 0x68ed2b88,
'SAML21': 0x1851780a,
'SAMD51': 0x55114460,
'NRF52': 0x1b57745f,
'STM32F0': 0x647824b6,
'STM32F1': 0x5ee21072,
'STM32F2': 0x5d1a0a2e,
'STM32F3': 0x6b846188,
'STM32F4': 0x57755a57,
'STM32F7': 0x53b80f00,
'STM32G0': 0x300f5633,
'STM32G4': 0x4c71240a,
'STM32H7': 0x6db66082,
'STM32L0': 0x202e3a91,
'STM32L1': 0x1e1f432d,
'STM32L4': 0x00ff6919,
'STM32L5': 0x04240bdf,
'STM32WB': 0x70d16653,
'STM32WL': 0x21460ff0,
'ATMEGA32': 0x16573617,
'MIMXRT10XX': 0x4FB2D5BD,
'LPC55': 0x2abc77ec,
'GD32F350': 0x31D228C6,
'ESP32S2': 0xbfdd4eee,
'RP2040': 0xe48bff56
}
INFO_FILE = "/INFO_UF2.TXT"
appstartaddr = 0x2000
familyid = 0x0
def is_uf2(buf):
w = struct.unpack("<II", buf[0:8])
return w[0] == UF2_MAGIC_START0 and w[1] == UF2_MAGIC_START1
def is_hex(buf):
try:
w = buf[0:30].decode("utf-8")
except UnicodeDecodeError:
return False
if w[0] == ':' and re.match(b"^[:0-9a-fA-F\r\n]+$", buf):
return True
return False
def convert_from_uf2(buf):
global appstartaddr
numblocks = len(buf) // 512
curraddr = None
outp = []
for blockno in range(numblocks):
ptr = blockno * 512
block = buf[ptr:ptr + 512]
hd = struct.unpack(b"<IIIIIIII", block[0:32])
if hd[0] != UF2_MAGIC_START0 or hd[1] != UF2_MAGIC_START1:
print("Skipping block at " + ptr + "; bad magic")
continue
if hd[2] & 1:
# NO-flash flag set; skip block
continue
datalen = hd[4]
if datalen > 476:
assert False, "Invalid UF2 data size at " + ptr
newaddr = hd[3]
if curraddr == None:
appstartaddr = newaddr
curraddr = newaddr
padding = newaddr - curraddr
if padding < 0:
assert False, "Block out of order at " + ptr
if padding > 10*1024*1024:
assert False, "More than 10M of padding needed at " + ptr
if padding % 4 != 0:
assert False, "Non-word padding size at " + ptr
while padding > 0:
padding -= 4
outp += b"\x00\x00\x00\x00"
outp.append(block[32 : 32 + datalen])
curraddr = newaddr + datalen
return b"".join(outp)
def convert_to_carray(file_content):
outp = "const unsigned long bindata_len = %d;\n" % len(file_content)
outp += "const unsigned char bindata[] __attribute__((aligned(16))) = {"
for i in range(len(file_content)):
if i % 16 == 0:
outp += "\n"
outp += "0x%02x, " % file_content[i]
outp += "\n};\n"
return bytes(outp, "utf-8")
def convert_to_uf2(file_content):
global familyid
datapadding = b""
while len(datapadding) < 512 - 256 - 32 - 4:
datapadding += b"\x00\x00\x00\x00"
numblocks = (len(file_content) + 255) // 256
outp = []
for blockno in range(numblocks):
ptr = 256 * blockno
chunk = file_content[ptr:ptr + 256]
flags = 0x0
if familyid:
flags |= 0x2000
hd = struct.pack(b"<IIIIIIII",
UF2_MAGIC_START0, UF2_MAGIC_START1,
flags, ptr + appstartaddr, 256, blockno, numblocks, familyid)
while len(chunk) < 256:
chunk += b"\x00"
block = hd + chunk + datapadding + struct.pack(b"<I", UF2_MAGIC_END)
assert len(block) == 512
outp.append(block)
return b"".join(outp)
class Block:
def __init__(self, addr):
self.addr = addr
self.bytes = bytearray(256)
def encode(self, blockno, numblocks):
global familyid
flags = 0x0
if familyid:
flags |= 0x2000
hd = struct.pack("<IIIIIIII",
UF2_MAGIC_START0, UF2_MAGIC_START1,
flags, self.addr, 256, blockno, numblocks, familyid)
hd += self.bytes[0:256]
while len(hd) < 512 - 4:
hd += b"\x00"
hd += struct.pack("<I", UF2_MAGIC_END)
return hd
def convert_from_hex_to_uf2(buf):
global appstartaddr
appstartaddr = None
upper = 0
currblock = None
blocks = []
for line in buf.split('\n'):
if line[0] != ":":
continue
i = 1
rec = []
while i < len(line) - 1:
rec.append(int(line[i:i+2], 16))
i += 2
tp = rec[3]
if tp == 4:
upper = ((rec[4] << 8) | rec[5]) << 16
elif tp == 2:
upper = ((rec[4] << 8) | rec[5]) << 4
assert (upper & 0xffff) == 0
elif tp == 1:
break
elif tp == 0:
addr = upper | (rec[1] << 8) | rec[2]
if appstartaddr == None:
appstartaddr = addr
i = 4
while i < len(rec) - 1:
if not currblock or currblock.addr & ~0xff != addr & ~0xff:
currblock = Block(addr & ~0xff)
blocks.append(currblock)
currblock.bytes[addr & 0xff] = rec[i]
addr += 1
i += 1
numblocks = len(blocks)
resfile = b""
for i in range(0, numblocks):
resfile += blocks[i].encode(i, numblocks)
return resfile
def to_str(b):
return b.decode("utf-8", "replace")
def possibly_add(p, q):
if p not in q:
if os.path.isdir(p):
if os.access(p, os.W_OK):
q.append(p)
def possibly_anydir(p, q):
if os.path.isdir(p):
if os.access(p, os.R_OK):
r = glob.glob(p + "/*")
for t in r:
possibly_add(t, q)
def possibly_any(p, q, r):
possibly_anydir(p, q)
possibly_anydir(p + r, q)
def get_drives():
drives = []
if sys.platform == "win32":
try:
r = subprocess.check_output(["wmic", "PATH", "Win32_LogicalDisk",
"get", "DeviceID,", "VolumeName,",
"FileSystem,", "DriveType"])
except:
try:
nul = open("nul:", "r")
r = subprocess.check_output(["powershell", "-NonInteractive", "-Command",
"Get-WmiObject -class Win32_LogicalDisk | "
"Format-Table -Property DeviceID, DriveType, Filesystem, VolumeName"],
stdin = nul)
nul.close()
except:
print("Unable to build drive list");
sys.exit(1)
for line in to_str(r).split('\n'):
words = re.split('\s+', line)
if len(words) >= 3 and words[1] == "2" and words[2] == "FAT":
drives.append(words[0])
else:
if sys.platform == "darwin":
possibly_anydir("/Volumes", drives)
elif sys.platform == "linux":
globexpr = "/dev/disk/by-id/usb-RPI_RP2*-part1"
rpidisk = glob.glob(globexpr)
if len(rpidisk) > 0:
try:
cmd = ["udisksctl", "mount", "--block-device", os.path.realpath(rpidisk[0])]
proc_out = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if proc_out.returncode == 0:
stdoutput = proc_out.stdout.decode("UTF-8")
match = re.search(r'Mounted\s+.*\s+at\s+([^\.\r\n]*)', stdoutput)
if match:
drives = [match.group(1)]
except Exception as ex:
print("Exception executing udisksctl. Exception: {}".format(ex))
# If it fails, no problem since it was a heroic attempt
'''
Generate a list and scan those too.
First add the usual suspects.
Then Scan a returned list.
'''
u="/" + os.environ["USER"]
possibly_anydir("/mnt", drives)
possibly_any("/media", drives, u)
possibly_any("/opt/media", drives, u)
possibly_any("/run/media", drives, u)
possibly_any("/var/run/media", drives, u)
# Add from udisksctl info?
# Add from /proc/mounts?
def has_info(d):
try:
return os.path.isfile(d + INFO_FILE)
except:
return False
return list(filter(has_info, drives))
def board_id(path):
with open(path + INFO_FILE, mode='r') as file:
file_content = file.read()
return re.search("Board-ID: ([^\r\n]*)", file_content).group(1)
def list_drives():
for d in get_drives():
print(d, board_id(d))
def write_file(name, buf):
with open(name, "wb") as f:
f.write(buf)
print("Wrote %d bytes to %s" % (len(buf), name))
def main():
global appstartaddr, familyid
def error(msg):
print(msg)
sys.exit(1)
parser = argparse.ArgumentParser(description='Convert to UF2 or flash directly.')
parser.add_argument('input', metavar='INPUT', type=str, nargs='?',
help='input file (HEX, BIN or UF2)')
parser.add_argument('-b' , '--base', dest='base', type=str,
default="0x2000",
help='set base address of application for BIN format (default: 0x2000)')
parser.add_argument('-o' , '--output', metavar="FILE", dest='output', type=str,
help='write output to named file; defaults to "flash.uf2" or "flash.bin" where sensible')
parser.add_argument('-d' , '--device', dest="device_path",
help='select a device path to flash')
parser.add_argument('-l' , '--list', action='store_true',
help='list connected devices')
parser.add_argument('-c' , '--convert', action='store_true',
help='do not flash, just convert')
parser.add_argument('-D' , '--deploy', action='store_true',
help='just flash, do not convert')
parser.add_argument('-f' , '--family', dest='family', type=str,
default="0x0",
help='specify familyID - number or name (default: 0x0)')
parser.add_argument('-C' , '--carray', action='store_true',
help='convert binary file to a C array, not UF2')
parser.add_argument('-s', '--serial', dest='serial', help='Serial port to reset before upload')
args = parser.parse_args()
appstartaddr = int(args.base, 0)
if args.family.upper() in families:
familyid = families[args.family.upper()]
else:
try:
familyid = int(args.family, 0)
except ValueError:
error("Family ID needs to be a number or one of: " + ", ".join(families.keys()))
if args.serial:
if str(args.serial).startswith("/dev/tty") or str(args.serial).startswith("COM") or str(args.serial).startswith("/dev/cu"):
try:
print("Resetting " + str(args.serial))
sys.stdout.flush()
try:
ser = serial.Serial()
ser.port = args.serial
ser.open()
ser.baudrate = 9600
ser.dtr = True
time.sleep(0.1)
ser.dtr = False
ser.baudrate = 1200
ser.close()
except:
pass # Ignore error in the case it is already in upload mode
except:
pass
if args.list:
list_drives()
else:
if not args.input:
error("Need input file")
with open(args.input, mode='rb') as f:
inpbuf = f.read()
from_uf2 = is_uf2(inpbuf)
ext = "uf2"
if args.deploy:
outbuf = inpbuf
elif from_uf2:
outbuf = convert_from_uf2(inpbuf)
ext = "bin"
elif is_hex(inpbuf):
outbuf = convert_from_hex_to_uf2(inpbuf.decode("utf-8"))
elif args.carray:
outbuf = convert_to_carray(inpbuf)
ext = "h"
else:
outbuf = convert_to_uf2(inpbuf)
print("Converting to %s, output size: %d, start address: 0x%x" %
(ext, len(outbuf), appstartaddr))
sys.stdout.flush()
if args.convert or ext != "uf2":
drives = []
if args.output == None:
args.output = "flash." + ext
else:
print("Scanning for RP2040 devices")
sys.stdout.flush()
now = time.time()
drives = []
while (time.time() - now < 10.0) and (len(drives) == 0):
time.sleep(1.0) # Avoid 100% CPU use while waiting for drive to appear
drives = get_drives()
if args.output:
write_file(args.output, outbuf)
else:
if len(drives) == 0:
error("No drive to deploy.")
for d in drives:
print("Flashing %s (%s)" % (d, board_id(d)))
sys.stdout.flush()
write_file(d + "/NEW.UF2", outbuf)
# Wait until serial port (if defined) re-appears, or 2s timeout unless UF2 drive direct upload
try:
if args.serial != "UF2 Board":
timeout = time.time() + 2.0
while time.time() < timeout:
if os.access(args.serial, os.W_OK):
break
time.sleep(0.2)
except:
pass
if __name__ == "__main__":
main()