3 # ESP32 partition table generation tool
5 # Converts partition tables to/from CSV and binary formats.
7 # See https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/partition-tables.html
8 # for explanation of partition table structure and uses.
10 # Copyright 2015-2016 Espressif Systems (Shanghai) PTE LTD
12 # Licensed under the Apache License, Version 2.0 (the "License");
13 # you may not use this file except in compliance with the License.
14 # You may obtain a copy of the License at
16 # http:#www.apache.org/licenses/LICENSE-2.0
18 # Unless required by applicable law or agreed to in writing, software
19 # distributed under the License is distributed on an "AS IS" BASIS,
20 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21 # See the License for the specific language governing permissions and
22 # limitations under the License.
23 from __future__ import print_function, division
32 MAX_PARTITION_LENGTH = 0xC00 # 3K for partition data (96 entries) leaves 1K in a 4K sector for signature
33 MD5_PARTITION_BEGIN = b"\xEB\xEB" + b"\xFF" * 14 # The first 2 bytes are like magic numbers for MD5 sum
34 PARTITION_TABLE_SIZE = 0x1000 # Size of partition table
43 """ Print status message to stderr """
48 """ Print critical message to stderr """
51 sys.stderr.write('\n')
53 class PartitionTable(list):
55 super(PartitionTable, self).__init__(self)
58 def from_csv(cls, csv_contents):
59 res = PartitionTable()
60 lines = csv_contents.splitlines()
63 f = os.path.expandvars(f)
64 m = re.match(r'(?<!\\)\$([A-Za-z_][A-Za-z0-9_]*)', f)
66 raise InputError("unknown variable '%s'" % m.group(1))
69 for line_no in range(len(lines)):
70 line = expand_vars(lines[line_no]).strip()
71 if line.startswith("#") or len(line) == 0:
74 res.append(PartitionDefinition.from_csv(line))
75 except InputError as e:
76 raise InputError("Error at line %d: %s" % (line_no+1, e))
78 critical("Unexpected error parsing line %d: %s" % (line_no+1, line))
81 # fix up missing offsets & negative sizes
82 last_end = offset_part_table + PARTITION_TABLE_SIZE # first offset after partition table
84 if offset_part_table != 0 and e.offset is not None and e.offset < last_end:
85 critical("WARNING: 0x%x address in the partition table is below 0x%x" % (e.offset, last_end))
88 pad_to = 0x10000 if e.type == PartitionDefinition.APP_TYPE else 4
89 if last_end % pad_to != 0:
90 last_end += pad_to - (last_end % pad_to)
93 e.size = -e.size - e.offset
94 last_end = e.offset + e.size
98 def __getitem__(self, item):
99 """ Allow partition table access via name as well as by
101 if isinstance(item, str):
105 raise ValueError("No partition entry named '%s'" % item)
107 return super(PartitionTable, self).__getitem__(item)
109 def find_by_type(self, ptype, subtype):
110 """ Return a partition by type & subtype, returns
111 None if not found """
112 TYPES = PartitionDefinition.TYPES
113 SUBTYPES = PartitionDefinition.SUBTYPES
114 # convert ptype & subtypes names (if supplied this way) to integer values
119 ptypes = int(ptype, 0)
123 subtype = SUBTYPES[int(ptype)][subtype]
126 ptypes = int(ptype, 0)
131 if p.type == ptype and p.subtype == subtype:
135 def find_by_name(self, name):
142 # verify each partition individually
147 for p in sorted(self, key=lambda x:x.offset):
148 if p.offset < offset_part_table + PARTITION_TABLE_SIZE:
149 raise InputError("Partition offset 0x%x is below 0x%x" % (p.offset, offset_part_table + PARTITION_TABLE_SIZE))
150 if last is not None and p.offset < last.offset + last.size:
151 raise InputError("Partition at 0x%x overlaps 0x%x-0x%x" % (p.offset, last.offset, last.offset+last.size-1))
154 def flash_size(self):
155 """ Return the size that partitions will occupy in flash
156 (ie the offset the last partition ends at)
159 last = sorted(self, reverse=True)[0]
161 return 0 # empty table!
162 return last.offset + last.size
165 def from_binary(cls, b):
168 for o in range(0,len(b),32):
171 raise InputError("Partition table length must be a multiple of 32 bytes")
172 if data == b'\xFF'*32:
173 return result # got end marker
174 if md5sum and data[:2] == MD5_PARTITION_BEGIN[:2]: #check only the magic number part
175 if data[16:] == md5.digest():
176 continue # the next iteration will check for the end marker
178 raise InputError("MD5 checksums don't match! (computed: 0x%s, parsed: 0x%s)" % (md5.hexdigest(), binascii.hexlify(data[16:])))
181 result.append(PartitionDefinition.from_binary(data))
182 raise InputError("Partition table is missing an end-of-table marker")
185 result = b"".join(e.to_binary() for e in self)
187 result += MD5_PARTITION_BEGIN + hashlib.md5(result).digest()
188 if len(result )>= MAX_PARTITION_LENGTH:
189 raise InputError("Binary partition table length (%d) longer than max" % len(result))
190 result += b"\xFF" * (MAX_PARTITION_LENGTH - len(result)) # pad the sector, for signing
193 def to_csv(self, simple_formatting=False):
194 rows = [ "# Espressif ESP32 Partition Table",
195 "# Name, Type, SubType, Offset, Size, Flags" ]
196 rows += [ x.to_csv(simple_formatting) for x in self ]
197 return "\n".join(rows) + "\n"
199 class PartitionDefinition(object):
207 # Keep this map in sync with esp_partition_subtype_t enum in esp_partition.h
224 MAGIC_BYTES = b"\xAA\x50"
231 # dictionary maps flag name (as used in CSV flags list, property name)
232 # to bit set in flags words in binary format
237 # add subtypes for the 16 OTA slot values ("ota_XX, etc.")
238 for ota_slot in range(16):
239 SUBTYPES[TYPES["app"]]["ota_%d" % ota_slot] = 0x10 + ota_slot
247 self.encrypted = False
250 def from_csv(cls, line):
251 """ Parse a line from the CSV """
252 line_w_defaults = line + ",,,," # lazy way to support default fields
253 fields = [ f.strip() for f in line_w_defaults.split(",") ]
255 res = PartitionDefinition()
257 res.type = res.parse_type(fields[1])
258 res.subtype = res.parse_subtype(fields[2])
259 res.offset = res.parse_address(fields[3])
260 res.size = res.parse_address(fields[4])
262 raise InputError("Size field can't be empty")
264 flags = fields[5].split(":")
266 if flag in cls.FLAGS:
267 setattr(res, flag, True)
269 raise InputError("CSV flag column contains unknown flag '%s'" % (flag))
273 def __eq__(self, other):
274 return self.name == other.name and self.type == other.type \
275 and self.subtype == other.subtype and self.offset == other.offset \
276 and self.size == other.size
280 return "0x%x" % x if x is not None else "None"
281 return "PartitionDefinition('%s', 0x%x, 0x%x, %s, %s)" % (self.name, self.type, self.subtype or 0,
282 maybe_hex(self.offset), maybe_hex(self.size))
285 return "Part '%s' %d/%d @ 0x%x size 0x%x" % (self.name, self.type, self.subtype, self.offset or -1, self.size or -1)
287 def __cmp__(self, other):
288 return self.offset - other.offset
290 def __lt__(self, other):
291 return self.offset < other.offset
293 def __gt__(self, other):
294 return self.offset > other.offset
296 def __le__(self, other):
297 return self.offset <= other.offset
299 def __ge__(self, other):
300 return self.offset >= other.offset
302 def parse_type(self, strval):
304 raise InputError("Field 'type' can't be left empty.")
305 return parse_int(strval, self.TYPES)
307 def parse_subtype(self, strval):
310 return parse_int(strval, self.SUBTYPES.get(self.type, {}))
312 def parse_address(self, strval):
314 return None # PartitionTable will fill in default
315 return parse_int(strval)
318 if self.type is None:
319 raise ValidationError(self, "Type field is not set")
320 if self.subtype is None:
321 raise ValidationError(self, "Subtype field is not set")
322 if self.offset is None:
323 raise ValidationError(self, "Offset field is not set")
324 align = self.ALIGNMENT.get(self.type, 4)
325 if self.offset % align:
326 raise ValidationError(self, "Offset 0x%x is not aligned to 0x%x" % (self.offset, align))
327 if self.size is None:
328 raise ValidationError(self, "Size field is not set")
330 STRUCT_FORMAT = "<2sBBLL16sL"
333 def from_binary(cls, b):
335 raise InputError("Partition definition length must be exactly 32 bytes. Got %d bytes." % len(b))
337 (magic, res.type, res.subtype, res.offset,
338 res.size, res.name, flags) = struct.unpack(cls.STRUCT_FORMAT, b)
339 if b"\x00" in res.name: # strip null byte padding from name string
340 res.name = res.name[:res.name.index(b"\x00")]
341 res.name = res.name.decode()
342 if magic != cls.MAGIC_BYTES:
343 raise InputError("Invalid magic bytes (%r) for partition definition" % magic)
344 for flag,bit in cls.FLAGS.items():
346 setattr(res, flag, True)
349 critical("WARNING: Partition definition had unknown flag(s) 0x%08x. Newer binary format?" % flags)
352 def get_flags_list(self):
353 return [ flag for flag in self.FLAGS.keys() if getattr(self, flag) ]
356 flags = sum((1 << self.FLAGS[flag]) for flag in self.get_flags_list())
357 return struct.pack(self.STRUCT_FORMAT,
359 self.type, self.subtype,
360 self.offset, self.size,
364 def to_csv(self, simple_formatting=False):
365 def addr_format(a, include_sizes):
366 if not simple_formatting and include_sizes:
367 for (val, suffix) in [ (0x100000, "M"), (0x400, "K") ]:
369 return "%d%s" % (a // val, suffix)
372 def lookup_keyword(t, keywords):
373 for k,v in keywords.items():
374 if simple_formatting == False and t == v:
378 def generate_text_flags():
379 """ colon-delimited list of flags """
380 return ":".join(self.get_flags_list())
382 return ",".join([ self.name,
383 lookup_keyword(self.type, self.TYPES),
384 lookup_keyword(self.subtype, self.SUBTYPES.get(self.type, {})),
385 addr_format(self.offset, False),
386 addr_format(self.size, True),
387 generate_text_flags()])
390 def parse_int(v, keywords={}):
391 """Generic parser for integer fields - int(x,0) with provision for
392 k/m/K/M suffixes and 'keyword' value lookup.
395 for letter, multiplier in [ ("k",1024), ("m",1024*1024) ]:
396 if v.lower().endswith(letter):
397 return parse_int(v[:-1], keywords) * multiplier
400 if len(keywords) == 0:
401 raise InputError("Invalid field value %s" % v)
403 return keywords[v.lower()]
405 raise InputError("Value '%s' is not valid. Known keywords: %s" % (v, ", ".join(keywords)))
410 global offset_part_table
411 parser = argparse.ArgumentParser(description='ESP32 partition table utility')
413 parser.add_argument('--flash-size', help='Optional flash size limit, checks partition table fits in flash',
414 nargs='?', choices=[ '1MB', '2MB', '4MB', '8MB', '16MB' ])
415 parser.add_argument('--disable-md5sum', help='Disable md5 checksum for the partition table', default=False, action='store_true')
416 parser.add_argument('--verify', '-v', help='Verify partition table fields', default=True, action='store_false')
417 parser.add_argument('--quiet', '-q', help="Don't print status messages to stderr", action='store_true')
418 parser.add_argument('--offset', '-o', help='Set offset partition table', default='0x8000')
420 parser.add_argument('input', help='Path to CSV or binary file to parse.', type=argparse.FileType('rb'))
421 parser.add_argument('output', help='Path to output converted binary or CSV file. Will use stdout if omitted.',
422 nargs='?', default='-')
424 args = parser.parse_args()
427 md5sum = not args.disable_md5sum
428 offset_part_table = int(args.offset, 0)
429 input = args.input.read()
430 input_is_binary = input[0:2] == PartitionDefinition.MAGIC_BYTES
432 status("Parsing binary partition input...")
433 table = PartitionTable.from_binary(input)
435 input = input.decode()
436 status("Parsing CSV input...")
437 table = PartitionTable.from_csv(input)
440 status("Verifying table...")
444 size_mb = int(args.flash_size.replace("MB", ""))
445 size = size_mb * 1024 * 1024 # flash memory uses honest megabytes!
446 table_size = table.flash_size()
447 if size < table_size:
448 raise InputError("Partitions defined in '%s' occupy %.1fMB of flash (%d bytes) which does not fit in configured flash size %dMB. Change the flash size in menuconfig under the 'Serial Flasher Config' menu." %
449 (args.input.name, table_size / 1024.0 / 1024.0, table_size, size_mb))
452 output = table.to_csv()
453 with sys.stdout if args.output == '-' else open(args.output, 'w') as f:
456 output = table.to_binary()
458 stdout_binary = sys.stdout.buffer # Python 3
459 except AttributeError:
460 stdout_binary = sys.stdout
461 with stdout_binary if args.output == '-' else open(args.output, 'wb') as f:
465 class InputError(RuntimeError):
466 def __init__(self, e):
467 super(InputError, self).__init__(e)
470 class ValidationError(InputError):
471 def __init__(self, partition, message):
472 super(ValidationError, self).__init__(
473 "Partition %s invalid: %s" % (partition.name, message))
476 if __name__ == '__main__':
479 except InputError as e:
480 print(e, file=sys.stderr)