gen_esp32part.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581
  1. #!/usr/bin/env python
  2. #
  3. # ESP32 partition table generation tool
  4. #
  5. # Converts partition tables to/from CSV and binary formats.
  6. #
  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.
  9. #
  10. # Copyright 2015-2016 Espressif Systems (Shanghai) PTE LTD
  11. #
  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
  15. #
  16. # http:#www.apache.org/licenses/LICENSE-2.0
  17. #
  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 division, print_function, unicode_literals
  24. import argparse
  25. import binascii
  26. import errno
  27. import hashlib
  28. import os
  29. import re
  30. import struct
  31. import sys
  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
  35. MIN_PARTITION_SUBTYPE_APP_OTA = 0x10
  36. NUM_PARTITION_SUBTYPE_APP_OTA = 16
  37. __version__ = '1.2'
  38. APP_TYPE = 0x00
  39. DATA_TYPE = 0x01
  40. TYPES = {
  41. 'app': APP_TYPE,
  42. 'data': DATA_TYPE,
  43. }
  44. def get_ptype_as_int(ptype):
  45. """ Convert a string which might be numeric or the name of a partition type to an integer """
  46. try:
  47. return TYPES[ptype]
  48. except KeyError:
  49. try:
  50. return int(ptype, 0)
  51. except TypeError:
  52. return ptype
  53. # Keep this map in sync with esp_partition_subtype_t enum in esp_partition.h
  54. SUBTYPES = {
  55. APP_TYPE: {
  56. 'factory': 0x00,
  57. 'test': 0x20,
  58. },
  59. DATA_TYPE: {
  60. 'ota': 0x00,
  61. 'phy': 0x01,
  62. 'nvs': 0x02,
  63. 'coredump': 0x03,
  64. 'nvs_keys': 0x04,
  65. 'efuse': 0x05,
  66. 'undefined': 0x06,
  67. 'esphttpd': 0x80,
  68. 'fat': 0x81,
  69. 'spiffs': 0x82,
  70. },
  71. }
  72. def get_subtype_as_int(ptype, subtype):
  73. """ Convert a string which might be numeric or the name of a partition subtype to an integer """
  74. try:
  75. return SUBTYPES[get_ptype_as_int(ptype)][subtype]
  76. except KeyError:
  77. try:
  78. return int(subtype, 0)
  79. except TypeError:
  80. return subtype
  81. ALIGNMENT = {
  82. APP_TYPE: 0x10000,
  83. DATA_TYPE: 0x4,
  84. }
  85. STRICT_DATA_ALIGNMENT = 0x1000
  86. def get_alignment_for_type(ptype):
  87. return ALIGNMENT.get(ptype, ALIGNMENT[DATA_TYPE])
  88. quiet = False
  89. md5sum = True
  90. secure = False
  91. offset_part_table = 0
  92. def status(msg):
  93. """ Print status message to stderr """
  94. if not quiet:
  95. critical(msg)
  96. def critical(msg):
  97. """ Print critical message to stderr """
  98. sys.stderr.write(msg)
  99. sys.stderr.write('\n')
  100. class PartitionTable(list):
  101. def __init__(self):
  102. super(PartitionTable, self).__init__(self)
  103. @classmethod
  104. def from_file(cls, f):
  105. data = f.read()
  106. data_is_binary = data[0:2] == PartitionDefinition.MAGIC_BYTES
  107. if data_is_binary:
  108. status('Parsing binary partition input...')
  109. return cls.from_binary(data), True
  110. data = data.decode()
  111. status('Parsing CSV input...')
  112. return cls.from_csv(data), False
  113. @classmethod
  114. def from_csv(cls, csv_contents):
  115. res = PartitionTable()
  116. lines = csv_contents.splitlines()
  117. def expand_vars(f):
  118. f = os.path.expandvars(f)
  119. m = re.match(r'(?<!\\)\$([A-Za-z_][A-Za-z0-9_]*)', f)
  120. if m:
  121. raise InputError("unknown variable '%s'" % m.group(1))
  122. return f
  123. for line_no in range(len(lines)):
  124. line = expand_vars(lines[line_no]).strip()
  125. if line.startswith('#') or len(line) == 0:
  126. continue
  127. try:
  128. res.append(PartitionDefinition.from_csv(line, line_no + 1))
  129. except InputError as err:
  130. raise InputError('Error at line %d: %s' % (line_no + 1, err))
  131. except Exception:
  132. critical('Unexpected error parsing CSV line %d: %s' % (line_no + 1, line))
  133. raise
  134. # fix up missing offsets & negative sizes
  135. last_end = offset_part_table + PARTITION_TABLE_SIZE # first offset after partition table
  136. for e in res:
  137. if e.offset is not None and e.offset < last_end:
  138. if e == res[0]:
  139. raise InputError('CSV Error: First partition offset 0x%x overlaps end of partition table 0x%x'
  140. % (e.offset, last_end))
  141. else:
  142. raise InputError('CSV Error: Partitions overlap. Partition at line %d sets offset 0x%x. Previous partition ends 0x%x'
  143. % (e.line_no, e.offset, last_end))
  144. if e.offset is None:
  145. pad_to = get_alignment_for_type(e.type)
  146. if last_end % pad_to != 0:
  147. last_end += pad_to - (last_end % pad_to)
  148. e.offset = last_end
  149. if e.size < 0:
  150. e.size = -e.size - e.offset
  151. last_end = e.offset + e.size
  152. return res
  153. def __getitem__(self, item):
  154. """ Allow partition table access via name as well as by
  155. numeric index. """
  156. if isinstance(item, str):
  157. for x in self:
  158. if x.name == item:
  159. return x
  160. raise ValueError("No partition entry named '%s'" % item)
  161. else:
  162. return super(PartitionTable, self).__getitem__(item)
  163. def find_by_type(self, ptype, subtype):
  164. """ Return a partition by type & subtype, returns
  165. None if not found """
  166. # convert ptype & subtypes names (if supplied this way) to integer values
  167. ptype = get_ptype_as_int(ptype)
  168. subtype = get_subtype_as_int(ptype, subtype)
  169. for p in self:
  170. if p.type == ptype and p.subtype == subtype:
  171. yield p
  172. return
  173. def find_by_name(self, name):
  174. for p in self:
  175. if p.name == name:
  176. return p
  177. return None
  178. def verify(self):
  179. # verify each partition individually
  180. for p in self:
  181. p.verify()
  182. # check on duplicate name
  183. names = [p.name for p in self]
  184. duplicates = set(n for n in names if names.count(n) > 1)
  185. # print sorted duplicate partitions by name
  186. if len(duplicates) != 0:
  187. critical('A list of partitions that have the same name:')
  188. for p in sorted(self, key=lambda x:x.name):
  189. if len(duplicates.intersection([p.name])) != 0:
  190. critical('%s' % (p.to_csv()))
  191. raise InputError('Partition names must be unique')
  192. # check for overlaps
  193. last = None
  194. for p in sorted(self, key=lambda x:x.offset):
  195. if p.offset < offset_part_table + PARTITION_TABLE_SIZE:
  196. raise InputError('Partition offset 0x%x is below 0x%x' % (p.offset, offset_part_table + PARTITION_TABLE_SIZE))
  197. if last is not None and p.offset < last.offset + last.size:
  198. raise InputError('Partition at 0x%x overlaps 0x%x-0x%x' % (p.offset, last.offset, last.offset + last.size - 1))
  199. last = p
  200. # check that otadata should be unique
  201. otadata_duplicates = [p for p in self if p.type == TYPES['data'] and p.subtype == SUBTYPES[DATA_TYPE]['ota']]
  202. if len(otadata_duplicates) > 1:
  203. for p in otadata_duplicates:
  204. critical('%s' % (p.to_csv()))
  205. raise InputError('Found multiple otadata partitions. Only one partition can be defined with type="data"(1) and subtype="ota"(0).')
  206. if len(otadata_duplicates) == 1 and otadata_duplicates[0].size != 0x2000:
  207. p = otadata_duplicates[0]
  208. critical('%s' % (p.to_csv()))
  209. raise InputError('otadata partition must have size = 0x2000')
  210. def flash_size(self):
  211. """ Return the size that partitions will occupy in flash
  212. (ie the offset the last partition ends at)
  213. """
  214. try:
  215. last = sorted(self, reverse=True)[0]
  216. except IndexError:
  217. return 0 # empty table!
  218. return last.offset + last.size
  219. @classmethod
  220. def from_binary(cls, b):
  221. md5 = hashlib.md5()
  222. result = cls()
  223. for o in range(0,len(b),32):
  224. data = b[o:o + 32]
  225. if len(data) != 32:
  226. raise InputError('Partition table length must be a multiple of 32 bytes')
  227. if data == b'\xFF' * 32:
  228. return result # got end marker
  229. if md5sum and data[:2] == MD5_PARTITION_BEGIN[:2]: # check only the magic number part
  230. if data[16:] == md5.digest():
  231. continue # the next iteration will check for the end marker
  232. else:
  233. raise InputError("MD5 checksums don't match! (computed: 0x%s, parsed: 0x%s)" % (md5.hexdigest(), binascii.hexlify(data[16:])))
  234. else:
  235. md5.update(data)
  236. result.append(PartitionDefinition.from_binary(data))
  237. raise InputError('Partition table is missing an end-of-table marker')
  238. def to_binary(self):
  239. result = b''.join(e.to_binary() for e in self)
  240. if md5sum:
  241. result += MD5_PARTITION_BEGIN + hashlib.md5(result).digest()
  242. if len(result) >= MAX_PARTITION_LENGTH:
  243. raise InputError('Binary partition table length (%d) longer than max' % len(result))
  244. result += b'\xFF' * (MAX_PARTITION_LENGTH - len(result)) # pad the sector, for signing
  245. return result
  246. def to_csv(self, simple_formatting=False):
  247. rows = ['# ESP-IDF Partition Table',
  248. '# Name, Type, SubType, Offset, Size, Flags']
  249. rows += [x.to_csv(simple_formatting) for x in self]
  250. return '\n'.join(rows) + '\n'
  251. class PartitionDefinition(object):
  252. MAGIC_BYTES = b'\xAA\x50'
  253. # dictionary maps flag name (as used in CSV flags list, property name)
  254. # to bit set in flags words in binary format
  255. FLAGS = {
  256. 'encrypted': 0
  257. }
  258. # add subtypes for the 16 OTA slot values ("ota_XX, etc.")
  259. for ota_slot in range(NUM_PARTITION_SUBTYPE_APP_OTA):
  260. SUBTYPES[TYPES['app']]['ota_%d' % ota_slot] = MIN_PARTITION_SUBTYPE_APP_OTA + ota_slot
  261. def __init__(self):
  262. self.name = ''
  263. self.type = None
  264. self.subtype = None
  265. self.offset = None
  266. self.size = None
  267. self.encrypted = False
  268. @classmethod
  269. def from_csv(cls, line, line_no):
  270. """ Parse a line from the CSV """
  271. line_w_defaults = line + ',,,,' # lazy way to support default fields
  272. fields = [f.strip() for f in line_w_defaults.split(',')]
  273. res = PartitionDefinition()
  274. res.line_no = line_no
  275. res.name = fields[0]
  276. res.type = res.parse_type(fields[1])
  277. res.subtype = res.parse_subtype(fields[2])
  278. res.offset = res.parse_address(fields[3])
  279. res.size = res.parse_address(fields[4])
  280. if res.size is None:
  281. raise InputError("Size field can't be empty")
  282. flags = fields[5].split(':')
  283. for flag in flags:
  284. if flag in cls.FLAGS:
  285. setattr(res, flag, True)
  286. elif len(flag) > 0:
  287. raise InputError("CSV flag column contains unknown flag '%s'" % (flag))
  288. return res
  289. def __eq__(self, other):
  290. return self.name == other.name and self.type == other.type \
  291. and self.subtype == other.subtype and self.offset == other.offset \
  292. and self.size == other.size
  293. def __repr__(self):
  294. def maybe_hex(x):
  295. return '0x%x' % x if x is not None else 'None'
  296. return "PartitionDefinition('%s', 0x%x, 0x%x, %s, %s)" % (self.name, self.type, self.subtype or 0,
  297. maybe_hex(self.offset), maybe_hex(self.size))
  298. def __str__(self):
  299. return "Part '%s' %d/%d @ 0x%x size 0x%x" % (self.name, self.type, self.subtype, self.offset or -1, self.size or -1)
  300. def __cmp__(self, other):
  301. return self.offset - other.offset
  302. def __lt__(self, other):
  303. return self.offset < other.offset
  304. def __gt__(self, other):
  305. return self.offset > other.offset
  306. def __le__(self, other):
  307. return self.offset <= other.offset
  308. def __ge__(self, other):
  309. return self.offset >= other.offset
  310. def parse_type(self, strval):
  311. if strval == '':
  312. raise InputError("Field 'type' can't be left empty.")
  313. return parse_int(strval, TYPES)
  314. def parse_subtype(self, strval):
  315. if strval == '':
  316. if self.type == TYPES['app']:
  317. raise InputError('App partition cannot have an empty subtype')
  318. return SUBTYPES[DATA_TYPE]['undefined']
  319. return parse_int(strval, SUBTYPES.get(self.type, {}))
  320. def parse_address(self, strval):
  321. if strval == '':
  322. return None # PartitionTable will fill in default
  323. return parse_int(strval)
  324. def verify(self):
  325. if self.type is None:
  326. raise ValidationError(self, 'Type field is not set')
  327. if self.subtype is None:
  328. raise ValidationError(self, 'Subtype field is not set')
  329. if self.offset is None:
  330. raise ValidationError(self, 'Offset field is not set')
  331. align = get_alignment_for_type(self.type)
  332. if self.offset % align:
  333. raise ValidationError(self, 'Offset 0x%x is not aligned to 0x%x' % (self.offset, align))
  334. # The alignment requirement for non-app partition is 4 bytes, but it should be 4 kB.
  335. # Print a warning for now, make it an error in IDF 5.0 (IDF-3742).
  336. if self.type != APP_TYPE and self.offset % STRICT_DATA_ALIGNMENT:
  337. critical('WARNING: Partition %s not aligned to 0x%x.'
  338. 'This is deprecated and will be considered an error in the future release.' % (self.name, STRICT_DATA_ALIGNMENT))
  339. if self.size % align and secure and self.type == APP_TYPE:
  340. raise ValidationError(self, 'Size 0x%x is not aligned to 0x%x' % (self.size, align))
  341. if self.size is None:
  342. raise ValidationError(self, 'Size field is not set')
  343. if self.name in TYPES and TYPES.get(self.name, '') != self.type:
  344. critical("WARNING: Partition has name '%s' which is a partition type, but does not match this partition's "
  345. 'type (0x%x). Mistake in partition table?' % (self.name, self.type))
  346. all_subtype_names = []
  347. for names in (t.keys() for t in SUBTYPES.values()):
  348. all_subtype_names += names
  349. if self.name in all_subtype_names and SUBTYPES.get(self.type, {}).get(self.name, '') != self.subtype:
  350. critical("WARNING: Partition has name '%s' which is a partition subtype, but this partition has "
  351. 'non-matching type 0x%x and subtype 0x%x. Mistake in partition table?' % (self.name, self.type, self.subtype))
  352. STRUCT_FORMAT = b'<2sBBLL16sL'
  353. @classmethod
  354. def from_binary(cls, b):
  355. if len(b) != 32:
  356. raise InputError('Partition definition length must be exactly 32 bytes. Got %d bytes.' % len(b))
  357. res = cls()
  358. (magic, res.type, res.subtype, res.offset,
  359. res.size, res.name, flags) = struct.unpack(cls.STRUCT_FORMAT, b)
  360. if b'\x00' in res.name: # strip null byte padding from name string
  361. res.name = res.name[:res.name.index(b'\x00')]
  362. res.name = res.name.decode()
  363. if magic != cls.MAGIC_BYTES:
  364. raise InputError('Invalid magic bytes (%r) for partition definition' % magic)
  365. for flag,bit in cls.FLAGS.items():
  366. if flags & (1 << bit):
  367. setattr(res, flag, True)
  368. flags &= ~(1 << bit)
  369. if flags != 0:
  370. critical('WARNING: Partition definition had unknown flag(s) 0x%08x. Newer binary format?' % flags)
  371. return res
  372. def get_flags_list(self):
  373. return [flag for flag in self.FLAGS.keys() if getattr(self, flag)]
  374. def to_binary(self):
  375. flags = sum((1 << self.FLAGS[flag]) for flag in self.get_flags_list())
  376. return struct.pack(self.STRUCT_FORMAT,
  377. self.MAGIC_BYTES,
  378. self.type, self.subtype,
  379. self.offset, self.size,
  380. self.name.encode(),
  381. flags)
  382. def to_csv(self, simple_formatting=False):
  383. def addr_format(a, include_sizes):
  384. if not simple_formatting and include_sizes:
  385. for (val, suffix) in [(0x100000, 'M'), (0x400, 'K')]:
  386. if a % val == 0:
  387. return '%d%s' % (a // val, suffix)
  388. return '0x%x' % a
  389. def lookup_keyword(t, keywords):
  390. for k,v in keywords.items():
  391. if simple_formatting is False and t == v:
  392. return k
  393. return '%d' % t
  394. def generate_text_flags():
  395. """ colon-delimited list of flags """
  396. return ':'.join(self.get_flags_list())
  397. return ','.join([self.name,
  398. lookup_keyword(self.type, TYPES),
  399. lookup_keyword(self.subtype, SUBTYPES.get(self.type, {})),
  400. addr_format(self.offset, False),
  401. addr_format(self.size, True),
  402. generate_text_flags()])
  403. def parse_int(v, keywords={}):
  404. """Generic parser for integer fields - int(x,0) with provision for
  405. k/m/K/M suffixes and 'keyword' value lookup.
  406. """
  407. try:
  408. for letter, multiplier in [('k', 1024), ('m', 1024 * 1024)]:
  409. if v.lower().endswith(letter):
  410. return parse_int(v[:-1], keywords) * multiplier
  411. return int(v, 0)
  412. except ValueError:
  413. if len(keywords) == 0:
  414. raise InputError('Invalid field value %s' % v)
  415. try:
  416. return keywords[v.lower()]
  417. except KeyError:
  418. raise InputError("Value '%s' is not valid. Known keywords: %s" % (v, ', '.join(keywords)))
  419. def main():
  420. global quiet
  421. global md5sum
  422. global offset_part_table
  423. global secure
  424. parser = argparse.ArgumentParser(description='ESP32 partition table utility')
  425. parser.add_argument('--flash-size', help='Optional flash size limit, checks partition table fits in flash',
  426. nargs='?', choices=['1MB', '2MB', '4MB', '8MB', '16MB'])
  427. parser.add_argument('--disable-md5sum', help='Disable md5 checksum for the partition table', default=False, action='store_true')
  428. parser.add_argument('--no-verify', help="Don't verify partition table fields", action='store_true')
  429. parser.add_argument('--verify', '-v', help='Verify partition table fields (deprecated, this behaviour is '
  430. 'enabled by default and this flag does nothing.', action='store_true')
  431. parser.add_argument('--quiet', '-q', help="Don't print non-critical status messages to stderr", action='store_true')
  432. parser.add_argument('--offset', '-o', help='Set offset partition table', default='0x8000')
  433. parser.add_argument('--secure', help='Require app partitions to be suitable for secure boot', action='store_true')
  434. parser.add_argument('input', help='Path to CSV or binary file to parse.', type=argparse.FileType('rb'))
  435. parser.add_argument('output', help='Path to output converted binary or CSV file. Will use stdout if omitted.',
  436. nargs='?', default='-')
  437. args = parser.parse_args()
  438. quiet = args.quiet
  439. md5sum = not args.disable_md5sum
  440. secure = args.secure
  441. offset_part_table = int(args.offset, 0)
  442. table, input_is_binary = PartitionTable.from_file(args.input)
  443. if not args.no_verify:
  444. status('Verifying table...')
  445. table.verify()
  446. if args.flash_size:
  447. size_mb = int(args.flash_size.replace('MB', ''))
  448. size = size_mb * 1024 * 1024 # flash memory uses honest megabytes!
  449. table_size = table.flash_size()
  450. if size < table_size:
  451. raise InputError("Partitions defined in '%s' occupy %.1fMB of flash (%d bytes) which does not fit in configured "
  452. "flash size %dMB. Change the flash size in menuconfig under the 'Serial Flasher Config' menu." %
  453. (args.input.name, table_size / 1024.0 / 1024.0, table_size, size_mb))
  454. # Make sure that the output directory is created
  455. output_dir = os.path.abspath(os.path.dirname(args.output))
  456. if not os.path.exists(output_dir):
  457. try:
  458. os.makedirs(output_dir)
  459. except OSError as exc:
  460. if exc.errno != errno.EEXIST:
  461. raise
  462. if input_is_binary:
  463. output = table.to_csv()
  464. with sys.stdout if args.output == '-' else open(args.output, 'w') as f:
  465. f.write(output)
  466. else:
  467. output = table.to_binary()
  468. try:
  469. stdout_binary = sys.stdout.buffer # Python 3
  470. except AttributeError:
  471. stdout_binary = sys.stdout
  472. with stdout_binary if args.output == '-' else open(args.output, 'wb') as f:
  473. f.write(output)
  474. class InputError(RuntimeError):
  475. def __init__(self, e):
  476. super(InputError, self).__init__(e)
  477. class ValidationError(InputError):
  478. def __init__(self, partition, message):
  479. super(ValidationError, self).__init__(
  480. 'Partition %s invalid: %s' % (partition.name, message))
  481. if __name__ == '__main__':
  482. try:
  483. main()
  484. except InputError as e:
  485. print(e, file=sys.stderr)
  486. sys.exit(2)