|
1 #!/usr/bin/env python3 |
|
2 |
|
3 # Microsoft UF2 |
|
4 # |
|
5 # The MIT License (MIT) |
|
6 # |
|
7 # Copyright (c) Microsoft Corporation |
|
8 # |
|
9 # All rights reserved. |
|
10 # |
|
11 # Permission is hereby granted, free of charge, to any person obtaining a copy |
|
12 # of this software and associated documentation files (the "Software"), to deal |
|
13 # in the Software without restriction, including without limitation the rights |
|
14 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|
15 # copies of the Software, and to permit persons to whom the Software is |
|
16 # furnished to do so, subject to the following conditions: |
|
17 # |
|
18 # The above copyright notice and this permission notice shall be included in all |
|
19 # copies or substantial portions of the Software. |
|
20 # |
|
21 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|
22 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|
23 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|
24 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|
25 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|
26 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
|
27 # SOFTWARE. |
|
28 |
|
29 import sys |
|
30 import struct |
|
31 import subprocess |
|
32 import re |
|
33 import os |
|
34 import os.path |
|
35 import argparse |
|
36 import json |
|
37 |
|
38 |
|
39 UF2_MAGIC_START0 = 0x0A324655 # "UF2\n" |
|
40 UF2_MAGIC_START1 = 0x9E5D5157 # Randomly selected |
|
41 UF2_MAGIC_END = 0x0AB16F30 # Ditto |
|
42 |
|
43 INFO_FILE = "/INFO_UF2.TXT" |
|
44 |
|
45 appstartaddr = 0x2000 |
|
46 familyid = 0x0 |
|
47 |
|
48 |
|
49 def is_uf2(buf): |
|
50 w = struct.unpack("<II", buf[0:8]) |
|
51 return w[0] == UF2_MAGIC_START0 and w[1] == UF2_MAGIC_START1 |
|
52 |
|
53 |
|
54 def is_hex(buf): |
|
55 try: |
|
56 w = buf[0:30].decode("utf-8") |
|
57 except UnicodeDecodeError: |
|
58 return False |
|
59 if w[0] == ":" and re.match(b"^[:0-9a-fA-F\r\n]+$", buf): |
|
60 return True |
|
61 return False |
|
62 |
|
63 |
|
64 def convert_from_uf2(buf): |
|
65 global appstartaddr |
|
66 global familyid |
|
67 numblocks = len(buf) // 512 |
|
68 curraddr = None |
|
69 currfamilyid = None |
|
70 families_found = {} |
|
71 prev_flag = None |
|
72 all_flags_same = True |
|
73 outp = [] |
|
74 for blockno in range(numblocks): |
|
75 ptr = blockno * 512 |
|
76 block = buf[ptr : ptr + 512] |
|
77 hd = struct.unpack(b"<IIIIIIII", block[0:32]) |
|
78 if hd[0] != UF2_MAGIC_START0 or hd[1] != UF2_MAGIC_START1: |
|
79 print("Skipping block at " + ptr + "; bad magic") |
|
80 continue |
|
81 if hd[2] & 1: |
|
82 # NO-flash flag set; skip block |
|
83 continue |
|
84 datalen = hd[4] |
|
85 if datalen > 476: |
|
86 assert False, "Invalid UF2 data size at " + ptr |
|
87 newaddr = hd[3] |
|
88 if (hd[2] & 0x2000) and (currfamilyid == None): |
|
89 currfamilyid = hd[7] |
|
90 if curraddr == None or ((hd[2] & 0x2000) and hd[7] != currfamilyid): |
|
91 currfamilyid = hd[7] |
|
92 curraddr = newaddr |
|
93 if familyid == 0x0 or familyid == hd[7]: |
|
94 appstartaddr = newaddr |
|
95 padding = newaddr - curraddr |
|
96 if padding < 0: |
|
97 assert False, "Block out of order at " + ptr |
|
98 if padding > 10 * 1024 * 1024: |
|
99 assert False, "More than 10M of padding needed at " + ptr |
|
100 if padding % 4 != 0: |
|
101 assert False, "Non-word padding size at " + ptr |
|
102 while padding > 0: |
|
103 padding -= 4 |
|
104 outp += b"\x00\x00\x00\x00" |
|
105 if familyid == 0x0 or ((hd[2] & 0x2000) and familyid == hd[7]): |
|
106 outp.append(block[32 : 32 + datalen]) |
|
107 curraddr = newaddr + datalen |
|
108 if hd[2] & 0x2000: |
|
109 if hd[7] in families_found.keys(): |
|
110 if families_found[hd[7]] > newaddr: |
|
111 families_found[hd[7]] = newaddr |
|
112 else: |
|
113 families_found[hd[7]] = newaddr |
|
114 if prev_flag == None: |
|
115 prev_flag = hd[2] |
|
116 if prev_flag != hd[2]: |
|
117 all_flags_same = False |
|
118 if blockno == (numblocks - 1): |
|
119 print("--- UF2 File Header Info ---") |
|
120 families = load_families() |
|
121 for family_hex in families_found.keys(): |
|
122 family_short_name = "" |
|
123 for name, value in families.items(): |
|
124 if value == family_hex: |
|
125 family_short_name = name |
|
126 print( |
|
127 "Family ID is {:s}, hex value is 0x{:08x}".format( |
|
128 family_short_name, family_hex |
|
129 ) |
|
130 ) |
|
131 print("Target Address is 0x{:08x}".format(families_found[family_hex])) |
|
132 if all_flags_same: |
|
133 print("All block flag values consistent, 0x{:04x}".format(hd[2])) |
|
134 else: |
|
135 print("Flags were not all the same") |
|
136 print("----------------------------") |
|
137 if len(families_found) > 1 and familyid == 0x0: |
|
138 outp = [] |
|
139 appstartaddr = 0x0 |
|
140 return b"".join(outp) |
|
141 |
|
142 |
|
143 def convert_to_carray(file_content): |
|
144 outp = "const unsigned long bindata_len = %d;\n" % len(file_content) |
|
145 outp += "const unsigned char bindata[] __attribute__((aligned(16))) = {" |
|
146 for i in range(len(file_content)): |
|
147 if i % 16 == 0: |
|
148 outp += "\n" |
|
149 outp += "0x%02x, " % file_content[i] |
|
150 outp += "\n};\n" |
|
151 return bytes(outp, "utf-8") |
|
152 |
|
153 |
|
154 def convert_to_uf2(file_content): |
|
155 global familyid |
|
156 datapadding = b"" |
|
157 while len(datapadding) < 512 - 256 - 32 - 4: |
|
158 datapadding += b"\x00\x00\x00\x00" |
|
159 numblocks = (len(file_content) + 255) // 256 |
|
160 outp = [] |
|
161 for blockno in range(numblocks): |
|
162 ptr = 256 * blockno |
|
163 chunk = file_content[ptr : ptr + 256] |
|
164 flags = 0x0 |
|
165 if familyid: |
|
166 flags |= 0x2000 |
|
167 hd = struct.pack( |
|
168 b"<IIIIIIII", |
|
169 UF2_MAGIC_START0, |
|
170 UF2_MAGIC_START1, |
|
171 flags, |
|
172 ptr + appstartaddr, |
|
173 256, |
|
174 blockno, |
|
175 numblocks, |
|
176 familyid, |
|
177 ) |
|
178 while len(chunk) < 256: |
|
179 chunk += b"\x00" |
|
180 block = hd + chunk + datapadding + struct.pack(b"<I", UF2_MAGIC_END) |
|
181 assert len(block) == 512 |
|
182 outp.append(block) |
|
183 return b"".join(outp) |
|
184 |
|
185 |
|
186 class Block: |
|
187 def __init__(self, addr): |
|
188 self.addr = addr |
|
189 self.bytes = bytearray(256) |
|
190 |
|
191 def encode(self, blockno, numblocks): |
|
192 global familyid |
|
193 flags = 0x0 |
|
194 if familyid: |
|
195 flags |= 0x2000 |
|
196 hd = struct.pack( |
|
197 "<IIIIIIII", |
|
198 UF2_MAGIC_START0, |
|
199 UF2_MAGIC_START1, |
|
200 flags, |
|
201 self.addr, |
|
202 256, |
|
203 blockno, |
|
204 numblocks, |
|
205 familyid, |
|
206 ) |
|
207 hd += self.bytes[0:256] |
|
208 while len(hd) < 512 - 4: |
|
209 hd += b"\x00" |
|
210 hd += struct.pack("<I", UF2_MAGIC_END) |
|
211 return hd |
|
212 |
|
213 |
|
214 def convert_from_hex_to_uf2(buf): |
|
215 global appstartaddr |
|
216 appstartaddr = None |
|
217 upper = 0 |
|
218 currblock = None |
|
219 blocks = [] |
|
220 for line in buf.split("\n"): |
|
221 if line[0] != ":": |
|
222 continue |
|
223 i = 1 |
|
224 rec = [] |
|
225 while i < len(line) - 1: |
|
226 rec.append(int(line[i : i + 2], 16)) |
|
227 i += 2 |
|
228 tp = rec[3] |
|
229 if tp == 4: |
|
230 upper = ((rec[4] << 8) | rec[5]) << 16 |
|
231 elif tp == 2: |
|
232 upper = ((rec[4] << 8) | rec[5]) << 4 |
|
233 elif tp == 1: |
|
234 break |
|
235 elif tp == 0: |
|
236 addr = upper + ((rec[1] << 8) | rec[2]) |
|
237 if appstartaddr == None: |
|
238 appstartaddr = addr |
|
239 i = 4 |
|
240 while i < len(rec) - 1: |
|
241 if not currblock or currblock.addr & ~0xFF != addr & ~0xFF: |
|
242 currblock = Block(addr & ~0xFF) |
|
243 blocks.append(currblock) |
|
244 currblock.bytes[addr & 0xFF] = rec[i] |
|
245 addr += 1 |
|
246 i += 1 |
|
247 numblocks = len(blocks) |
|
248 resfile = b"" |
|
249 for i in range(0, numblocks): |
|
250 resfile += blocks[i].encode(i, numblocks) |
|
251 return resfile |
|
252 |
|
253 |
|
254 def to_str(b): |
|
255 return b.decode("utf-8") |
|
256 |
|
257 |
|
258 def get_drives(): |
|
259 drives = [] |
|
260 if sys.platform == "win32": |
|
261 r = subprocess.check_output( |
|
262 [ |
|
263 "wmic", |
|
264 "PATH", |
|
265 "Win32_LogicalDisk", |
|
266 "get", |
|
267 "DeviceID,", |
|
268 "VolumeName,", |
|
269 "FileSystem,", |
|
270 "DriveType", |
|
271 ] |
|
272 ) |
|
273 for line in to_str(r).split("\n"): |
|
274 words = re.split("\s+", line) |
|
275 if len(words) >= 3 and words[1] == "2" and words[2] == "FAT": |
|
276 drives.append(words[0]) |
|
277 else: |
|
278 rootpath = "/media" |
|
279 if sys.platform == "darwin": |
|
280 rootpath = "/Volumes" |
|
281 elif sys.platform == "linux": |
|
282 tmp = rootpath + "/" + os.environ["USER"] |
|
283 if os.path.isdir(tmp): |
|
284 rootpath = tmp |
|
285 for d in os.listdir(rootpath): |
|
286 drives.append(os.path.join(rootpath, d)) |
|
287 |
|
288 def has_info(d): |
|
289 try: |
|
290 return os.path.isfile(d + INFO_FILE) |
|
291 except: |
|
292 return False |
|
293 |
|
294 return list(filter(has_info, drives)) |
|
295 |
|
296 |
|
297 def board_id(path): |
|
298 with open(path + INFO_FILE, mode="r") as file: |
|
299 file_content = file.read() |
|
300 return re.search("Board-ID: ([^\r\n]*)", file_content).group(1) |
|
301 |
|
302 |
|
303 def list_drives(): |
|
304 for d in get_drives(): |
|
305 print(d, board_id(d)) |
|
306 |
|
307 |
|
308 def write_file(name, buf): |
|
309 with open(name, "wb") as f: |
|
310 f.write(buf) |
|
311 print("Wrote %d bytes to %s" % (len(buf), name)) |
|
312 |
|
313 |
|
314 def load_families(): |
|
315 # The expectation is that the `uf2families.json` file is in the same |
|
316 # directory as this script. Make a path that works using `__file__` |
|
317 # which contains the full path to this script. |
|
318 filename = "uf2families.json" |
|
319 pathname = os.path.join(os.path.dirname(os.path.abspath(__file__)), filename) |
|
320 with open(pathname) as f: |
|
321 raw_families = json.load(f) |
|
322 |
|
323 families = {} |
|
324 for family in raw_families: |
|
325 families[family["short_name"]] = int(family["id"], 0) |
|
326 |
|
327 return families |
|
328 |
|
329 |
|
330 def main(): |
|
331 global appstartaddr, familyid |
|
332 |
|
333 def error(msg): |
|
334 print(msg) |
|
335 sys.exit(1) |
|
336 |
|
337 parser = argparse.ArgumentParser(description="Convert to UF2 or flash directly.") |
|
338 parser.add_argument( |
|
339 "input", metavar="INPUT", type=str, nargs="?", help="input file (HEX, BIN or UF2)" |
|
340 ) |
|
341 parser.add_argument( |
|
342 "-b", |
|
343 "--base", |
|
344 dest="base", |
|
345 type=str, |
|
346 default="0x2000", |
|
347 help="set base address of application for BIN format (default: 0x2000)", |
|
348 ) |
|
349 parser.add_argument( |
|
350 "-o", |
|
351 "--output", |
|
352 metavar="FILE", |
|
353 dest="output", |
|
354 type=str, |
|
355 help='write output to named file; defaults to "flash.uf2" or "flash.bin" where sensible', |
|
356 ) |
|
357 parser.add_argument("-d", "--device", dest="device_path", help="select a device path to flash") |
|
358 parser.add_argument("-l", "--list", action="store_true", help="list connected devices") |
|
359 parser.add_argument("-c", "--convert", action="store_true", help="do not flash, just convert") |
|
360 parser.add_argument("-D", "--deploy", action="store_true", help="just flash, do not convert") |
|
361 parser.add_argument( |
|
362 "-f", |
|
363 "--family", |
|
364 dest="family", |
|
365 type=str, |
|
366 default="0x0", |
|
367 help="specify familyID - number or name (default: 0x0)", |
|
368 ) |
|
369 parser.add_argument( |
|
370 "-C", "--carray", action="store_true", help="convert binary file to a C array, not UF2" |
|
371 ) |
|
372 parser.add_argument( |
|
373 "-i", |
|
374 "--info", |
|
375 action="store_true", |
|
376 help="display header information from UF2, do not convert", |
|
377 ) |
|
378 args = parser.parse_args() |
|
379 appstartaddr = int(args.base, 0) |
|
380 |
|
381 families = load_families() |
|
382 |
|
383 if args.family.upper() in families: |
|
384 familyid = families[args.family.upper()] |
|
385 else: |
|
386 try: |
|
387 familyid = int(args.family, 0) |
|
388 except ValueError: |
|
389 error("Family ID needs to be a number or one of: " + ", ".join(families.keys())) |
|
390 |
|
391 if args.list: |
|
392 list_drives() |
|
393 else: |
|
394 if not args.input: |
|
395 error("Need input file") |
|
396 with open(args.input, mode="rb") as f: |
|
397 inpbuf = f.read() |
|
398 from_uf2 = is_uf2(inpbuf) |
|
399 ext = "uf2" |
|
400 if args.deploy: |
|
401 outbuf = inpbuf |
|
402 elif from_uf2 and not args.info: |
|
403 outbuf = convert_from_uf2(inpbuf) |
|
404 ext = "bin" |
|
405 elif from_uf2 and args.info: |
|
406 outbuf = "" |
|
407 convert_from_uf2(inpbuf) |
|
408 elif is_hex(inpbuf): |
|
409 outbuf = convert_from_hex_to_uf2(inpbuf.decode("utf-8")) |
|
410 elif args.carray: |
|
411 outbuf = convert_to_carray(inpbuf) |
|
412 ext = "h" |
|
413 else: |
|
414 outbuf = convert_to_uf2(inpbuf) |
|
415 if not args.deploy and not args.info: |
|
416 print( |
|
417 "Converted to %s, output size: %d, start address: 0x%x" |
|
418 % (ext, len(outbuf), appstartaddr) |
|
419 ) |
|
420 if args.convert or ext != "uf2": |
|
421 drives = [] |
|
422 if args.output == None: |
|
423 args.output = "flash." + ext |
|
424 else: |
|
425 drives = get_drives() |
|
426 |
|
427 if args.output: |
|
428 write_file(args.output, outbuf) |
|
429 else: |
|
430 if len(drives) == 0: |
|
431 error("No drive to deploy.") |
|
432 for d in drives: |
|
433 print("Flashing %s (%s)" % (d, board_id(d))) |
|
434 write_file(d + "/NEW.UF2", outbuf) |
|
435 |
|
436 |
|
437 if __name__ == "__main__": |
|
438 main() |