uf2conv.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. #!/usr/bin/env python3
  2. import sys
  3. import struct
  4. import subprocess
  5. import re
  6. import os
  7. import os.path
  8. import argparse
  9. import json
  10. from time import sleep
  11. UF2_MAGIC_START0 = 0x0A324655 # "UF2\n"
  12. UF2_MAGIC_START1 = 0x9E5D5157 # Randomly selected
  13. UF2_MAGIC_END = 0x0AB16F30 # Ditto
  14. INFO_FILE = "/INFO_UF2.TXT"
  15. appstartaddr = 0x2000
  16. familyid = 0x0
  17. def is_uf2(buf):
  18. w = struct.unpack("<II", buf[0:8])
  19. return w[0] == UF2_MAGIC_START0 and w[1] == UF2_MAGIC_START1
  20. def is_hex(buf):
  21. try:
  22. w = buf[0:30].decode("utf-8")
  23. except UnicodeDecodeError:
  24. return False
  25. if w[0] == ':' and re.match(rb"^[:0-9a-fA-F\r\n]+$", buf):
  26. return True
  27. return False
  28. def convert_from_uf2(buf):
  29. global appstartaddr
  30. global familyid
  31. numblocks = len(buf) // 512
  32. curraddr = None
  33. currfamilyid = None
  34. families_found = {}
  35. prev_flag = None
  36. all_flags_same = True
  37. outp = []
  38. for blockno in range(numblocks):
  39. ptr = blockno * 512
  40. block = buf[ptr:ptr + 512]
  41. hd = struct.unpack(b"<IIIIIIII", block[0:32])
  42. if hd[0] != UF2_MAGIC_START0 or hd[1] != UF2_MAGIC_START1:
  43. print("Skipping block at " + ptr + "; bad magic")
  44. continue
  45. if hd[2] & 1:
  46. # NO-flash flag set; skip block
  47. continue
  48. datalen = hd[4]
  49. if datalen > 476:
  50. assert False, "Invalid UF2 data size at " + ptr
  51. newaddr = hd[3]
  52. if (hd[2] & 0x2000) and (currfamilyid == None):
  53. currfamilyid = hd[7]
  54. if curraddr == None or ((hd[2] & 0x2000) and hd[7] != currfamilyid):
  55. currfamilyid = hd[7]
  56. curraddr = newaddr
  57. if familyid == 0x0 or familyid == hd[7]:
  58. appstartaddr = newaddr
  59. padding = newaddr - curraddr
  60. if padding < 0:
  61. assert False, "Block out of order at " + ptr
  62. if padding > 10*1024*1024:
  63. assert False, "More than 10M of padding needed at " + ptr
  64. if padding % 4 != 0:
  65. assert False, "Non-word padding size at " + ptr
  66. while padding > 0:
  67. padding -= 4
  68. outp.append(b"\x00\x00\x00\x00")
  69. if familyid == 0x0 or ((hd[2] & 0x2000) and familyid == hd[7]):
  70. outp.append(block[32 : 32 + datalen])
  71. curraddr = newaddr + datalen
  72. if hd[2] & 0x2000:
  73. if hd[7] in families_found.keys():
  74. if families_found[hd[7]] > newaddr:
  75. families_found[hd[7]] = newaddr
  76. else:
  77. families_found[hd[7]] = newaddr
  78. if prev_flag == None:
  79. prev_flag = hd[2]
  80. if prev_flag != hd[2]:
  81. all_flags_same = False
  82. if blockno == (numblocks - 1):
  83. print("--- UF2 File Header Info ---")
  84. families = load_families()
  85. for family_hex in families_found.keys():
  86. family_short_name = ""
  87. for name, value in families.items():
  88. if value == family_hex:
  89. family_short_name = name
  90. print("Family ID is {:s}, hex value is 0x{:08x}".format(family_short_name,family_hex))
  91. print("Target Address is 0x{:08x}".format(families_found[family_hex]))
  92. if all_flags_same:
  93. print("All block flag values consistent, 0x{:04x}".format(hd[2]))
  94. else:
  95. print("Flags were not all the same")
  96. print("----------------------------")
  97. if len(families_found) > 1 and familyid == 0x0:
  98. outp = []
  99. appstartaddr = 0x0
  100. return b"".join(outp)
  101. def convert_to_carray(file_content):
  102. outp = "const unsigned long bindata_len = %d;\n" % len(file_content)
  103. outp += "const unsigned char bindata[] __attribute__((aligned(16))) = {"
  104. for i in range(len(file_content)):
  105. if i % 16 == 0:
  106. outp += "\n"
  107. outp += "0x%02x, " % file_content[i]
  108. outp += "\n};\n"
  109. return bytes(outp, "utf-8")
  110. def convert_to_uf2(file_content):
  111. global familyid
  112. datapadding = b""
  113. while len(datapadding) < 512 - 256 - 32 - 4:
  114. datapadding += b"\x00\x00\x00\x00"
  115. numblocks = (len(file_content) + 255) // 256
  116. outp = []
  117. for blockno in range(numblocks):
  118. ptr = 256 * blockno
  119. chunk = file_content[ptr:ptr + 256]
  120. flags = 0x0
  121. if familyid:
  122. flags |= 0x2000
  123. hd = struct.pack(b"<IIIIIIII",
  124. UF2_MAGIC_START0, UF2_MAGIC_START1,
  125. flags, ptr + appstartaddr, 256, blockno, numblocks, familyid)
  126. while len(chunk) < 256:
  127. chunk += b"\x00"
  128. block = hd + chunk + datapadding + struct.pack(b"<I", UF2_MAGIC_END)
  129. assert len(block) == 512
  130. outp.append(block)
  131. return b"".join(outp)
  132. class Block:
  133. def __init__(self, addr):
  134. self.addr = addr
  135. self.bytes = bytearray(256)
  136. def encode(self, blockno, numblocks):
  137. global familyid
  138. flags = 0x0
  139. if familyid:
  140. flags |= 0x2000
  141. hd = struct.pack("<IIIIIIII",
  142. UF2_MAGIC_START0, UF2_MAGIC_START1,
  143. flags, self.addr, 256, blockno, numblocks, familyid)
  144. hd += self.bytes[0:256]
  145. while len(hd) < 512 - 4:
  146. hd += b"\x00"
  147. hd += struct.pack("<I", UF2_MAGIC_END)
  148. return hd
  149. def convert_from_hex_to_uf2(buf):
  150. global appstartaddr
  151. appstartaddr = None
  152. upper = 0
  153. currblock = None
  154. blocks = []
  155. for line in buf.split('\n'):
  156. if line[0] != ":":
  157. continue
  158. i = 1
  159. rec = []
  160. while i < len(line) - 1:
  161. rec.append(int(line[i:i+2], 16))
  162. i += 2
  163. tp = rec[3]
  164. if tp == 4:
  165. upper = ((rec[4] << 8) | rec[5]) << 16
  166. elif tp == 2:
  167. upper = ((rec[4] << 8) | rec[5]) << 4
  168. elif tp == 1:
  169. break
  170. elif tp == 0:
  171. addr = upper + ((rec[1] << 8) | rec[2])
  172. if appstartaddr == None:
  173. appstartaddr = addr
  174. i = 4
  175. while i < len(rec) - 1:
  176. if not currblock or currblock.addr & ~0xff != addr & ~0xff:
  177. currblock = Block(addr & ~0xff)
  178. blocks.append(currblock)
  179. currblock.bytes[addr & 0xff] = rec[i]
  180. addr += 1
  181. i += 1
  182. numblocks = len(blocks)
  183. resfile = b""
  184. for i in range(0, numblocks):
  185. resfile += blocks[i].encode(i, numblocks)
  186. return resfile
  187. def to_str(b):
  188. return b.decode("utf-8")
  189. def get_drives():
  190. drives = []
  191. if sys.platform == "win32":
  192. r = subprocess.check_output(["wmic", "PATH", "Win32_LogicalDisk",
  193. "get", "DeviceID,", "VolumeName,",
  194. "FileSystem,", "DriveType"])
  195. for line in to_str(r).split('\n'):
  196. words = re.split(r'\s+', line)
  197. if len(words) >= 3 and words[1] == "2" and words[2] == "FAT":
  198. drives.append(words[0])
  199. else:
  200. searchpaths = ["/media"]
  201. if sys.platform == "darwin":
  202. searchpaths = ["/Volumes"]
  203. elif sys.platform == "linux":
  204. searchpaths += ["/media/" + os.environ["USER"], '/run/media/' + os.environ["USER"]]
  205. for rootpath in searchpaths:
  206. if os.path.isdir(rootpath):
  207. for d in os.listdir(rootpath):
  208. if os.path.isdir(rootpath):
  209. drives.append(os.path.join(rootpath, d))
  210. def has_info(d):
  211. try:
  212. return os.path.isfile(d + INFO_FILE)
  213. except:
  214. return False
  215. return list(filter(has_info, drives))
  216. def board_id(path):
  217. with open(path + INFO_FILE, mode='r') as file:
  218. file_content = file.read()
  219. return re.search(r"Board-ID: ([^\r\n]*)", file_content).group(1)
  220. def list_drives():
  221. for d in get_drives():
  222. print(d, board_id(d))
  223. def write_file(name, buf):
  224. with open(name, "wb") as f:
  225. f.write(buf)
  226. print("Wrote %d bytes to %s" % (len(buf), name))
  227. def load_families():
  228. # The expectation is that the `uf2families.json` file is in the same
  229. # directory as this script. Make a path that works using `__file__`
  230. # which contains the full path to this script.
  231. filename = "uf2families.json"
  232. pathname = os.path.join(os.path.dirname(os.path.abspath(__file__)), filename)
  233. with open(pathname) as f:
  234. raw_families = json.load(f)
  235. families = {}
  236. for family in raw_families:
  237. families[family["short_name"]] = int(family["id"], 0)
  238. return families
  239. def main():
  240. global appstartaddr, familyid
  241. def error(msg):
  242. print(msg, file=sys.stderr)
  243. sys.exit(1)
  244. parser = argparse.ArgumentParser(description='Convert to UF2 or flash directly.')
  245. parser.add_argument('input', metavar='INPUT', type=str, nargs='?',
  246. help='input file (HEX, BIN or UF2)')
  247. parser.add_argument('-b', '--base', dest='base', type=str,
  248. default="0x2000",
  249. help='set base address of application for BIN format (default: 0x2000)')
  250. parser.add_argument('-f', '--family', dest='family', type=str,
  251. default="0x0",
  252. help='specify familyID - number or name (default: 0x0)')
  253. parser.add_argument('-o', '--output', metavar="FILE", dest='output', type=str,
  254. help='write output to named file; defaults to "flash.uf2" or "flash.bin" where sensible')
  255. parser.add_argument('-d', '--device', dest="device_path",
  256. help='select a device path to flash')
  257. parser.add_argument('-l', '--list', action='store_true',
  258. help='list connected devices')
  259. parser.add_argument('-c', '--convert', action='store_true',
  260. help='do not flash, just convert')
  261. parser.add_argument('-D', '--deploy', action='store_true',
  262. help='just flash, do not convert')
  263. parser.add_argument('-w', '--wait', action='store_true',
  264. help='wait for device to flash')
  265. parser.add_argument('-C', '--carray', action='store_true',
  266. help='convert binary file to a C array, not UF2')
  267. parser.add_argument('-i', '--info', action='store_true',
  268. help='display header information from UF2, do not convert')
  269. args = parser.parse_args()
  270. appstartaddr = int(args.base, 0)
  271. families = load_families()
  272. if args.family.upper() in families:
  273. familyid = families[args.family.upper()]
  274. else:
  275. try:
  276. familyid = int(args.family, 0)
  277. except ValueError:
  278. error("Family ID needs to be a number or one of: " + ", ".join(families.keys()))
  279. if args.list:
  280. list_drives()
  281. else:
  282. if not args.input:
  283. error("Need input file")
  284. with open(args.input, mode='rb') as f:
  285. inpbuf = f.read()
  286. from_uf2 = is_uf2(inpbuf)
  287. ext = "uf2"
  288. if args.deploy:
  289. outbuf = inpbuf
  290. elif from_uf2 and not args.info:
  291. outbuf = convert_from_uf2(inpbuf)
  292. ext = "bin"
  293. elif from_uf2 and args.info:
  294. outbuf = ""
  295. convert_from_uf2(inpbuf)
  296. elif is_hex(inpbuf):
  297. outbuf = convert_from_hex_to_uf2(inpbuf.decode("utf-8"))
  298. elif args.carray:
  299. outbuf = convert_to_carray(inpbuf)
  300. ext = "h"
  301. else:
  302. outbuf = convert_to_uf2(inpbuf)
  303. if not args.deploy and not args.info:
  304. print("Converted to %s, output size: %d, start address: 0x%x" %
  305. (ext, len(outbuf), appstartaddr))
  306. if args.convert or ext != "uf2":
  307. if args.output == None:
  308. args.output = "flash." + ext
  309. if args.output:
  310. write_file(args.output, outbuf)
  311. if ext == "uf2" and not args.convert and not args.info:
  312. drives = get_drives()
  313. if len(drives) == 0:
  314. if args.wait:
  315. print("Waiting for drive to deploy...")
  316. while len(drives) == 0:
  317. sleep(0.1)
  318. drives = get_drives()
  319. elif not args.output:
  320. error("No drive to deploy.")
  321. for d in drives:
  322. print("Flashing %s (%s)" % (d, board_id(d)))
  323. write_file(d + "/NEW.UF2", outbuf)
  324. if __name__ == "__main__":
  325. main()