Skip to content

Commit

Permalink
feat(espefuse): Adds support extend efuse table by user CSV file
Browse files Browse the repository at this point in the history
  • Loading branch information
KonstantinKondrashov authored and radimkarnis committed Jun 18, 2024
1 parent fc2856a commit 6bb2b92
Show file tree
Hide file tree
Showing 45 changed files with 600 additions and 70 deletions.
53 changes: 53 additions & 0 deletions docs/en/espefuse/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ Optional General Arguments Of Commands
- ``--virt`` - For host tests. The tool will work in the virtual mode (without connecting to a chip).
- ``--path-efuse-file`` - For host tests. Use it together with ``--virt`` option. The tool will work in the virtual mode (without connecting to a chip) and save eFuse memory to a given file. If the file does not exists the tool creates it. To reset written eFuses just delete the file. Usage: ``--path-efuse-file efuse_memory.bin``.
- ``--do-not-confirm`` - Do not pause for confirmation before permanently writing eFuses. Use with caution. If this option is not used, a manual confirmation step is required, you need to enter the word ``BURN`` to continue burning.
- ``--extend-efuse-table`` - CSV file from `ESP-IDF <https://docs.espressif.com/projects/esp-idf/>`_ (esp_efuse_custom_table.csv).

Virtual mode
^^^^^^^^^^^^
Expand Down Expand Up @@ -113,6 +114,58 @@ The example below shows how to use the two commands ``burn_key_digest`` and ``bu
burn_key_digest secure_images/ecdsa256_secure_boot_signing_key_v2.pem \
burn_key BLOCK_KEY0 images/efuse/128bit_key.bin XTS_AES_128_KEY_DERIVED_FROM_128_EFUSE_BITS
Extend Efuse Table
------------------

This tool supports the use of `CSV files <https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/system/efuse.html#description-csv-file>`_ from the `ESP-IDF <https://docs.espressif.com/projects/esp-idf/>`_ (e.g., ``esp_efuse_custom_table.csv``) to add custom eFuse fields. You can use this argument with any supported commands to access these custom eFuses.

.. code-block:: none
> espefuse.py -c esp32 --extend-efuse-table path/esp_efuse_custom_table.csv summary
Below is an example of an ``esp_efuse_custom_table.csv`` file. This example demonstrates how to define single eFuse fields, ``structured eFuse fields`` and ``non-sequential bit fields``:

.. code-block:: none
MODULE_VERSION, EFUSE_BLK3, 56, 8, Module version
DEVICE_ROLE, EFUSE_BLK3, 64, 3, Device role
SETTING_1, EFUSE_BLK3, 67, 6, [SETTING_1_ALT_NAME] Setting 1
SETTING_2, EFUSE_BLK3, 73, 5, Setting 2
ID_NUM, EFUSE_BLK3, 140, 8, [MY_ID_NUM] comment
, EFUSE_BLK3, 132, 8, [MY_ID_NUM] comment
, EFUSE_BLK3, 122, 8, [MY_ID_NUM] comment
CUSTOM_SECURE_VERSION, EFUSE_BLK3, 78, 16, Custom secure version
ID_NUMK, EFUSE_BLK3, 150, 8, [MY_ID_NUMK] comment
, EFUSE_BLK3, 182, 8, [MY_ID_NUMK] comment
MY_DATA, EFUSE_BLK3, 190, 10, My data
MY_DATA.FIELD1, EFUSE_BLK3, 190, 7, Field1
When you include this CSV file, the tool will generate a new section in the summary called ``User fuses``.

.. code-block:: none
User fuses:
MODULE_VERSION (BLOCK3) Module version (56-63) = 0 R/W (0x00)
DEVICE_ROLE (BLOCK3) Device role (64-66) = 0 R/W (0b000)
SETTING_1 (BLOCK3) [SETTING_1_ALT_NAME] Setting 1 (67-72) = 0 R/W (0b000000)
SETTING_2 (BLOCK3) Setting 2 (73-77) = 0 R/W (0b00000)
ID_NUM_0 (BLOCK3) [MY_ID_NUM] comment (140-147) = 0 R/W (0x00)
ID_NUM_1 (BLOCK3) [MY_ID_NUM] comment (132-139) = 0 R/W (0x00)
ID_NUM_2 (BLOCK3) [MY_ID_NUM] comment (122-129) = 0 R/W (0x00)
CUSTOM_SECURE_VERSION (BLOCK3) Custom secure version (78-93) = 0 R/W (0x0000)
ID_NUMK_0 (BLOCK3) [MY_ID_NUMK] comment (150-157) = 0 R/W (0x00)
ID_NUMK_1 (BLOCK3) [MY_ID_NUMK] comment (182-189) = 0 R/W (0x00)
MY_DATA (BLOCK3) My data (190-199) = 0 R/W (0b0000000000)
MY_DATA_FIELD1 (BLOCK3) Field1 (190-196) = 0 R/W (0b0000000)
You can reference these fields using the names and aliases provided in the CSV file. For non-sequential bits, the names are modified slightly with the addition of _0 and _1 postfixes for every sub-field, to ensure safer handling.

For the current example, you can reference the custom fields with the following names: MODULE_VERSION, DEVICE_ROLE, SETTING_1, SETTING_2, ID_NUM_0, ID_NUM_1, ID_NUM_2, CUSTOM_SECURE_VERSION, ID_NUMK_0, ID_NUMK_1, MY_DATA, MY_DATA_FIELD1; and alises: SETTING_1_ALT_NAME, MY_ID_NUM_0, MY_ID_NUM_1, MY_ID_NUM_2, MY_ID_NUMK_0, MY_ID_NUMK_1.

For convenience, the espefuse summary command includes the used bit range of the field in a comment, such as ``(150-157)`` len = 8 bits.

For more details on the structure and usage of the CSV file, refer to the `eFuse Manager <https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/system/efuse.html#description-csv-file>`_ chapter in the ESP-IDF documentation.

Recommendations
---------------

Expand Down
24 changes: 21 additions & 3 deletions espefuse/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,20 @@ def get_esp(
return esp


def get_efuses(esp, skip_connect=False, debug_mode=False, do_not_confirm=False):
def get_efuses(
esp,
skip_connect=False,
debug_mode=False,
do_not_confirm=False,
extend_efuse_table=None,
):
for name in SUPPORTED_CHIPS:
if SUPPORTED_CHIPS[name].chip_name == esp.CHIP_NAME:
efuse = SUPPORTED_CHIPS[name].efuse_lib
return (
efuse.EspEfuses(esp, skip_connect, debug_mode, do_not_confirm),
efuse.EspEfuses(
esp, skip_connect, debug_mode, do_not_confirm, extend_efuse_table
),
efuse.operations,
)
else:
Expand Down Expand Up @@ -228,6 +236,12 @@ def main(custom_commandline=None, esp=None):
"(efuses which disable access to blocks or chip).",
action="store_true",
)
init_parser.add_argument(
"--extend-efuse-table",
help="CSV file from ESP-IDF (esp_efuse_custom_table.csv)",
type=argparse.FileType("r"),
default=None,
)

common_args, remaining_args = init_parser.parse_known_args(custom_commandline)
debug_mode = common_args.debug
Expand Down Expand Up @@ -257,7 +271,11 @@ def main(custom_commandline=None, esp=None):
# TODO: Require the --port argument in the next major release, ESPTOOL-490

efuses, efuse_operations = get_efuses(
esp, just_print_help, debug_mode, common_args.do_not_confirm
esp,
just_print_help,
debug_mode,
common_args.do_not_confirm,
common_args.extend_efuse_table,
)

parser = argparse.ArgumentParser(parents=[init_parser])
Expand Down
266 changes: 266 additions & 0 deletions espefuse/efuse/csv_table_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
# This file helps to parse CSV eFuse tables
#
# SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
#
# SPDX-License-Identifier: GPL-2.0-or-later

import os
import re
import sys


class CSVFuseTable(list):
@classmethod
def from_csv(cls, csv_contents):
res = CSVFuseTable()
lines = csv_contents.splitlines()

def expand_vars(f):
f = os.path.expandvars(f)
m = re.match(r"(?<!\\)\$([A-Za-z_]\w*)", f)
if m:
raise InputError(f"unknown variable '{m.group(1)}'")
return f

for line_no, line in enumerate(lines):
line = expand_vars(line).strip()
if line.startswith("#") or len(line) == 0:
continue
try:
res.append(FuseDefinition.from_csv(line))
except InputError as err:
raise InputError(f"Error at line {line_no + 1}: {err}")
except Exception:
sys.stderr.write(f"Unexpected error parsing line {line_no + 1}: {line}")
raise

# fix up missing bit_start
last_efuse_block = None
for i in res:
if last_efuse_block != i.efuse_block:
last_end = 0
if i.bit_start is None:
i.bit_start = last_end
last_end = i.bit_start + i.bit_count
last_efuse_block = i.efuse_block

res.verify_duplicate_name()

# fix up missing field_name
last_field = None
for i in res:
if i.field_name == "":
if last_field is None:
raise InputError(
f"Error at line {line_no + 1}: {i} missing field name"
)
elif last_field is not None:
i.field_name = last_field.field_name
last_field = i

# fill group
names = [p.field_name for p in res]
duplicates = set(n for n in names if names.count(n) > 1)
for dname in duplicates:
i_count = 0
for p in res:
if p.field_name != dname:
continue
if len(duplicates.intersection([p.field_name])) != 0:
p.field_name = f"{p.field_name}_{i_count}"
if p.alt_names:
p.alt_names = f"{p.alt_names}_{i_count}"
i_count += 1
else:
i_count = 0

for p in res:
p.field_name = p.field_name.replace(".", "_")
if p.alt_names:
p.alt_names = p.alt_names.replace(".", "_")
res.verify_duplicate_name()
return res

def verify_duplicate_name(self):
# check on duplicate name
names = [p.field_name for p in self]
names += [name.replace(".", "_") for name in names if "." in name]
duplicates = set(n for n in names if names.count(n) > 1)

# print sorted duplicate partitions by name
if len(duplicates) != 0:
fl_error = False
for p in self:
field_name = p.field_name + p.group
if field_name != "" and len(duplicates.intersection([field_name])) != 0:
fl_error = True
print(
f"Field at {p.field_name}, {p.efuse_block}, "
f"{p.bit_start}, {p.bit_count} have duplicate field_name"
)
if fl_error is True:
raise InputError("Field names must be unique")

def check_struct_field_name(self):
# check that structured fields have a root field
for p in self:
if "." in p.field_name:
name = ""
for sub in p.field_name.split(".")[:-1]:
name = sub if name == "" else name + "." + sub
missed_name = True
for d in self:
if (
p is not d
and p.efuse_block == d.efuse_block
and name == d.field_name
):
missed_name = False
if missed_name:
raise InputError(f"{name} is not found")

def verify(self, type_table=None):
def check(p, n):
left = n.bit_start
right = n.bit_start + n.bit_count - 1
start = p.bit_start
end = p.bit_start + p.bit_count - 1
if left <= start <= right:
if left <= end <= right:
return "included in" # [n [p...p] n]
return "intersected with" # [n [p..n]..p]
if left <= end <= right:
return "intersected with" # [p..[n..p] n]
if start <= left and right <= end:
return "wraps" # [p [n...n] p]
return "ok" # [p] [n] or [n] [p]

def print_error(p, n, state):
raise InputError(
f"Field at {p.field_name}, {p.efuse_block}, {p.bit_start}, {p.bit_count} {state} {n.field_name}, {n.efuse_block}, {n.bit_start}, {n.bit_count}"
)

for p in self:
p.verify(type_table)

self.verify_duplicate_name()
if type_table != "custom_table":
# check will be done for common and custom tables together
self.check_struct_field_name()

# check for overlaps
for p in self:
for n in self:
if p is not n and p.efuse_block == n.efuse_block:
state = check(p, n)
if state != "ok":
if "." in p.field_name:
name = ""
for sub in p.field_name.split("."):
name = sub if name == "" else name + "." + sub
for d in self:
if (
p is not d
and p.efuse_block == d.efuse_block
and name == d.field_name
):
state = check(p, d)
if state == "included in":
break
elif state != "intersected with":
state = "out of range"
print_error(p, d, state)
continue
elif "." in n.field_name:
continue
print_error(p, n, state)


class FuseDefinition(object):
def __init__(self):
self.field_name = ""
self.group = ""
self.efuse_block = ""
self.bit_start = None
self.bit_count = None
self.define = None
self.comment = ""
self.alt_names = ""
self.MAX_BITS_OF_BLOCK = 256

@classmethod
def from_csv(cls, line):
"""Parse a line from the CSV"""
line_w_defaults = line + ",,,,"
fields = [f.strip() for f in line_w_defaults.split(",")]

res = FuseDefinition()
res.field_name = fields[0]
res.efuse_block = res.parse_block(fields[1])
res.bit_start = res.parse_num(fields[2])
res.bit_count = res.parse_bit_count(fields[3])
if res.bit_count is None or res.bit_count == 0:
raise InputError("Field bit_count can't be empty")
res.comment = fields[4].rstrip("\\").rstrip()
res.comment += f" ({res.bit_start}-{res.bit_start + res.bit_count - 1})"
res.alt_names = res.get_alt_names(res.comment)
return res

def parse_num(self, strval):
if strval == "":
return None
return self.parse_int(strval)

def parse_bit_count(self, strval):
if strval == "MAX_BLK_LEN":
self.define = strval
return self.MAX_BITS_OF_BLOCK
else:
return self.parse_num(strval)

def parse_int(self, v):
try:
return int(v, 0)
except ValueError:
raise InputError(f"Invalid field value {v}")

def parse_block(self, strval):
if strval == "":
raise InputError("Field 'efuse_block' can't be left empty.")
return self.parse_int(strval.lstrip("EFUSE_BLK"))

def verify(self, type_table):
if self.efuse_block is None:
raise ValidationError(self, "efuse_block field is not set")
if self.bit_count is None:
raise ValidationError(self, "bit_count field is not set")
max_bits = self.MAX_BITS_OF_BLOCK
if self.bit_start + self.bit_count > max_bits:
raise ValidationError(
self,
f"The field is outside the boundaries(max_bits = {max_bits}) of the {self.efuse_block} block",
)

def get_bit_count(self, check_define=True):
if check_define is True and self.define is not None:
return self.define
else:
return self.bit_count

def get_alt_names(self, comment):
result = re.search(r"^\[(.*?)\]", comment)
if result:
return result.group(1)
return ""


class InputError(RuntimeError):
def __init__(self, e):
super(InputError, self).__init__(e)


class ValidationError(InputError):
def __init__(self, p, message):
super(ValidationError, self).__init__(
f"Entry {p.field_name} invalid: {message}"
)
2 changes: 1 addition & 1 deletion espefuse/efuse/esp32/emulate_efuse_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class EmulateEfuseController(EmulateEfuseControllerBase):

def __init__(self, efuse_file=None, debug=False):
self.Blocks = EfuseDefineBlocks
self.Fields = EfuseDefineFields()
self.Fields = EfuseDefineFields(None)
self.REGS = EfuseDefineRegisters
super(EmulateEfuseController, self).__init__(efuse_file, debug)

Expand Down
Loading

0 comments on commit 6bb2b92

Please sign in to comment.