diff options
Diffstat (limited to 'util')
-rwxr-xr-x | util/uf2conv.py | 319 |
1 files changed, 319 insertions, 0 deletions
diff --git a/util/uf2conv.py b/util/uf2conv.py new file mode 100755 index 000000000..044a7f231 --- /dev/null +++ b/util/uf2conv.py | |||
@@ -0,0 +1,319 @@ | |||
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 | |||
10 | |||
11 | UF2_MAGIC_START0 = 0x0A324655 # "UF2\n" | ||
12 | UF2_MAGIC_START1 = 0x9E5D5157 # Randomly selected | ||
13 | UF2_MAGIC_END = 0x0AB16F30 # Ditto | ||
14 | |||
15 | families = { | ||
16 | 'SAMD21': 0x68ed2b88, | ||
17 | 'SAML21': 0x1851780a, | ||
18 | 'SAMD51': 0x55114460, | ||
19 | 'NRF52': 0x1b57745f, | ||
20 | 'STM32F0': 0x647824b6, | ||
21 | 'STM32F1': 0x5ee21072, | ||
22 | 'STM32F2': 0x5d1a0a2e, | ||
23 | 'STM32F3': 0x6b846188, | ||
24 | 'STM32F4': 0x57755a57, | ||
25 | 'STM32F7': 0x53b80f00, | ||
26 | 'STM32G0': 0x300f5633, | ||
27 | 'STM32G4': 0x4c71240a, | ||
28 | 'STM32H7': 0x6db66082, | ||
29 | 'STM32L0': 0x202e3a91, | ||
30 | 'STM32L1': 0x1e1f432d, | ||
31 | 'STM32L4': 0x00ff6919, | ||
32 | 'STM32L5': 0x04240bdf, | ||
33 | 'STM32WB': 0x70d16653, | ||
34 | 'STM32WL': 0x21460ff0, | ||
35 | 'ATMEGA32': 0x16573617, | ||
36 | 'MIMXRT10XX': 0x4FB2D5BD, | ||
37 | 'LPC55': 0x2abc77ec, | ||
38 | 'GD32F350': 0x31D228C6, | ||
39 | 'ESP32S2': 0xbfdd4eee, | ||
40 | 'RP2040': 0xe48bff56 | ||
41 | } | ||
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 | def is_hex(buf): | ||
54 | try: | ||
55 | w = buf[0:30].decode("utf-8") | ||
56 | except UnicodeDecodeError: | ||
57 | return False | ||
58 | if w[0] == ':' and re.match(b"^[:0-9a-fA-F\r\n]+$", buf): | ||
59 | return True | ||
60 | return False | ||
61 | |||
62 | def convert_from_uf2(buf): | ||
63 | global appstartaddr | ||
64 | numblocks = len(buf) // 512 | ||
65 | curraddr = None | ||
66 | outp = [] | ||
67 | for blockno in range(numblocks): | ||
68 | ptr = blockno * 512 | ||
69 | block = buf[ptr:ptr + 512] | ||
70 | hd = struct.unpack(b"<IIIIIIII", block[0:32]) | ||
71 | if hd[0] != UF2_MAGIC_START0 or hd[1] != UF2_MAGIC_START1: | ||
72 | print("Skipping block at " + ptr + "; bad magic") | ||
73 | continue | ||
74 | if hd[2] & 1: | ||
75 | # NO-flash flag set; skip block | ||
76 | continue | ||
77 | datalen = hd[4] | ||
78 | if datalen > 476: | ||
79 | assert False, "Invalid UF2 data size at " + ptr | ||
80 | newaddr = hd[3] | ||
81 | if curraddr == None: | ||
82 | appstartaddr = newaddr | ||
83 | curraddr = newaddr | ||
84 | padding = newaddr - curraddr | ||
85 | if padding < 0: | ||
86 | assert False, "Block out of order at " + ptr | ||
87 | if padding > 10*1024*1024: | ||
88 | assert False, "More than 10M of padding needed at " + ptr | ||
89 | if padding % 4 != 0: | ||
90 | assert False, "Non-word padding size at " + ptr | ||
91 | while padding > 0: | ||
92 | padding -= 4 | ||
93 | outp += b"\x00\x00\x00\x00" | ||
94 | outp.append(block[32 : 32 + datalen]) | ||
95 | curraddr = newaddr + datalen | ||
96 | return b"".join(outp) | ||
97 | |||
98 | def convert_to_carray(file_content): | ||
99 | outp = "const unsigned long bindata_len = %d;\n" % len(file_content) | ||
100 | outp += "const unsigned char bindata[] __attribute__((aligned(16))) = {" | ||
101 | for i in range(len(file_content)): | ||
102 | if i % 16 == 0: | ||
103 | outp += "\n" | ||
104 | outp += "0x%02x, " % file_content[i] | ||
105 | outp += "\n};\n" | ||
106 | return bytes(outp, "utf-8") | ||
107 | |||
108 | def convert_to_uf2(file_content): | ||
109 | global familyid | ||
110 | datapadding = b"" | ||
111 | while len(datapadding) < 512 - 256 - 32 - 4: | ||
112 | datapadding += b"\x00\x00\x00\x00" | ||
113 | numblocks = (len(file_content) + 255) // 256 | ||
114 | outp = [] | ||
115 | for blockno in range(numblocks): | ||
116 | ptr = 256 * blockno | ||
117 | chunk = file_content[ptr:ptr + 256] | ||
118 | flags = 0x0 | ||
119 | if familyid: | ||
120 | flags |= 0x2000 | ||
121 | hd = struct.pack(b"<IIIIIIII", | ||
122 | UF2_MAGIC_START0, UF2_MAGIC_START1, | ||
123 | flags, ptr + appstartaddr, 256, blockno, numblocks, familyid) | ||
124 | while len(chunk) < 256: | ||
125 | chunk += b"\x00" | ||
126 | block = hd + chunk + datapadding + struct.pack(b"<I", UF2_MAGIC_END) | ||
127 | assert len(block) == 512 | ||
128 | outp.append(block) | ||
129 | return b"".join(outp) | ||
130 | |||
131 | class Block: | ||
132 | def __init__(self, addr): | ||
133 | self.addr = addr | ||
134 | self.bytes = bytearray(256) | ||
135 | |||
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 | |||
150 | def convert_from_hex_to_uf2(buf): | ||
151 | global appstartaddr | ||
152 | appstartaddr = None | ||
153 | upper = 0 | ||
154 | currblock = None | ||
155 | blocks = [] | ||
156 | for line in buf.split('\n'): | ||
157 | if line[0] != ":": | ||
158 | continue | ||
159 | i = 1 | ||
160 | rec = [] | ||
161 | while i < len(line) - 1: | ||
162 | rec.append(int(line[i:i+2], 16)) | ||
163 | i += 2 | ||
164 | tp = rec[3] | ||
165 | if tp == 4: | ||
166 | upper = ((rec[4] << 8) | rec[5]) << 16 | ||
167 | elif tp == 2: | ||
168 | upper = ((rec[4] << 8) | rec[5]) << 4 | ||
169 | assert (upper & 0xffff) == 0 | ||
170 | elif tp == 1: | ||
171 | break | ||
172 | elif tp == 0: | ||
173 | addr = upper | (rec[1] << 8) | rec[2] | ||
174 | if appstartaddr == None: | ||
175 | appstartaddr = addr | ||
176 | i = 4 | ||
177 | while i < len(rec) - 1: | ||
178 | if not currblock or currblock.addr & ~0xff != addr & ~0xff: | ||
179 | currblock = Block(addr & ~0xff) | ||
180 | blocks.append(currblock) | ||
181 | currblock.bytes[addr & 0xff] = rec[i] | ||
182 | addr += 1 | ||
183 | i += 1 | ||
184 | numblocks = len(blocks) | ||
185 | resfile = b"" | ||
186 | for i in range(0, numblocks): | ||
187 | resfile += blocks[i].encode(i, numblocks) | ||
188 | return resfile | ||
189 | |||
190 | def to_str(b): | ||
191 | return b.decode("utf-8") | ||
192 | |||
193 | def get_drives(): | ||
194 | drives = [] | ||
195 | if sys.platform == "win32": | ||
196 | r = subprocess.check_output(["wmic", "PATH", "Win32_LogicalDisk", | ||
197 | "get", "DeviceID,", "VolumeName,", | ||
198 | "FileSystem,", "DriveType"]) | ||
199 | for line in to_str(r).split('\n'): | ||
200 | words = re.split('\s+', line) | ||
201 | if len(words) >= 3 and words[1] == "2" and words[2] == "FAT": | ||
202 | drives.append(words[0]) | ||
203 | else: | ||
204 | rootpath = "/media" | ||
205 | if sys.platform == "darwin": | ||
206 | rootpath = "/Volumes" | ||
207 | elif sys.platform == "linux": | ||
208 | tmp = rootpath + "/" + os.environ["USER"] | ||
209 | if os.path.isdir(tmp): | ||
210 | rootpath = tmp | ||
211 | for d in os.listdir(rootpath): | ||
212 | drives.append(os.path.join(rootpath, d)) | ||
213 | |||
214 | |||
215 | def has_info(d): | ||
216 | try: | ||
217 | return os.path.isfile(d + INFO_FILE) | ||
218 | except: | ||
219 | return False | ||
220 | |||
221 | return list(filter(has_info, drives)) | ||
222 | |||
223 | |||
224 | def board_id(path): | ||
225 | with open(path + INFO_FILE, mode='r') as file: | ||
226 | file_content = file.read() | ||
227 | return re.search("Board-ID: ([^\r\n]*)", file_content).group(1) | ||
228 | |||
229 | |||
230 | def list_drives(): | ||
231 | for d in get_drives(): | ||
232 | print(d, board_id(d)) | ||
233 | |||
234 | |||
235 | def write_file(name, buf): | ||
236 | with open(name, "wb") as f: | ||
237 | f.write(buf) | ||
238 | print("Wrote %d bytes to %s" % (len(buf), name)) | ||
239 | |||
240 | |||
241 | def main(): | ||
242 | global appstartaddr, familyid | ||
243 | def error(msg): | ||
244 | print(msg) | ||
245 | sys.exit(1) | ||
246 | parser = argparse.ArgumentParser(description='Convert to UF2 or flash directly.') | ||
247 | parser.add_argument('input', metavar='INPUT', type=str, nargs='?', | ||
248 | help='input file (HEX, BIN or UF2)') | ||
249 | parser.add_argument('-b' , '--base', dest='base', type=str, | ||
250 | default="0x2000", | ||
251 | help='set base address of application for BIN format (default: 0x2000)') | ||
252 | parser.add_argument('-o' , '--output', metavar="FILE", dest='output', type=str, | ||
253 | help='write output to named file; defaults to "flash.uf2" or "flash.bin" where sensible') | ||
254 | parser.add_argument('-d' , '--device', dest="device_path", | ||
255 | help='select a device path to flash') | ||
256 | parser.add_argument('-l' , '--list', action='store_true', | ||
257 | help='list connected devices') | ||
258 | parser.add_argument('-c' , '--convert', action='store_true', | ||
259 | help='do not flash, just convert') | ||
260 | parser.add_argument('-D' , '--deploy', action='store_true', | ||
261 | help='just flash, do not convert') | ||
262 | parser.add_argument('-f' , '--family', dest='family', type=str, | ||
263 | default="0x0", | ||
264 | help='specify familyID - number or name (default: 0x0)') | ||
265 | parser.add_argument('-C' , '--carray', action='store_true', | ||
266 | help='convert binary file to a C array, not UF2') | ||
267 | args = parser.parse_args() | ||
268 | appstartaddr = int(args.base, 0) | ||
269 | |||
270 | if args.family.upper() in families: | ||
271 | familyid = families[args.family.upper()] | ||
272 | else: | ||
273 | try: | ||
274 | familyid = int(args.family, 0) | ||
275 | except ValueError: | ||
276 | error("Family ID needs to be a number or one of: " + ", ".join(families.keys())) | ||
277 | |||
278 | if args.list: | ||
279 | list_drives() | ||
280 | else: | ||
281 | if not args.input: | ||
282 | error("Need input file") | ||
283 | with open(args.input, mode='rb') as f: | ||
284 | inpbuf = f.read() | ||
285 | from_uf2 = is_uf2(inpbuf) | ||
286 | ext = "uf2" | ||
287 | if args.deploy: | ||
288 | outbuf = inpbuf | ||
289 | elif from_uf2: | ||
290 | outbuf = convert_from_uf2(inpbuf) | ||
291 | ext = "bin" | ||
292 | elif is_hex(inpbuf): | ||
293 | outbuf = convert_from_hex_to_uf2(inpbuf.decode("utf-8")) | ||
294 | elif args.carray: | ||
295 | outbuf = convert_to_carray(inpbuf) | ||
296 | ext = "h" | ||
297 | else: | ||
298 | outbuf = convert_to_uf2(inpbuf) | ||
299 | print("Converting to %s, output size: %d, start address: 0x%x" % | ||
300 | (ext, len(outbuf), appstartaddr)) | ||
301 | if args.convert or ext != "uf2": | ||
302 | drives = [] | ||
303 | if args.output == None: | ||
304 | args.output = "flash." + ext | ||
305 | else: | ||
306 | drives = get_drives() | ||
307 | |||
308 | if args.output: | ||
309 | write_file(args.output, outbuf) | ||
310 | else: | ||
311 | if len(drives) == 0: | ||
312 | error("No drive to deploy.") | ||
313 | for d in drives: | ||
314 | print("Flashing %s (%s)" % (d, board_id(d))) | ||
315 | write_file(d + "/NEW.UF2", outbuf) | ||
316 | |||
317 | |||
318 | if __name__ == "__main__": | ||
319 | main() | ||