Skip to content

Commit 6bb2b92

Browse files
KonstantinKondrashovradimkarnis
authored andcommitted
feat(espefuse): Adds support extend efuse table by user CSV file
1 parent fc2856a commit 6bb2b92

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+600
-70
lines changed

docs/en/espefuse/index.rst

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ Optional General Arguments Of Commands
5656
- ``--virt`` - For host tests. The tool will work in the virtual mode (without connecting to a chip).
5757
- ``--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``.
5858
- ``--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.
59+
- ``--extend-efuse-table`` - CSV file from `ESP-IDF <https://docs.espressif.com/projects/esp-idf/>`_ (esp_efuse_custom_table.csv).
5960

6061
Virtual mode
6162
^^^^^^^^^^^^
@@ -113,6 +114,58 @@ The example below shows how to use the two commands ``burn_key_digest`` and ``bu
113114
burn_key_digest secure_images/ecdsa256_secure_boot_signing_key_v2.pem \
114115
burn_key BLOCK_KEY0 images/efuse/128bit_key.bin XTS_AES_128_KEY_DERIVED_FROM_128_EFUSE_BITS
115116
117+
Extend Efuse Table
118+
------------------
119+
120+
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.
121+
122+
.. code-block:: none
123+
124+
> espefuse.py -c esp32 --extend-efuse-table path/esp_efuse_custom_table.csv summary
125+
126+
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``:
127+
128+
.. code-block:: none
129+
130+
MODULE_VERSION, EFUSE_BLK3, 56, 8, Module version
131+
DEVICE_ROLE, EFUSE_BLK3, 64, 3, Device role
132+
SETTING_1, EFUSE_BLK3, 67, 6, [SETTING_1_ALT_NAME] Setting 1
133+
SETTING_2, EFUSE_BLK3, 73, 5, Setting 2
134+
ID_NUM, EFUSE_BLK3, 140, 8, [MY_ID_NUM] comment
135+
, EFUSE_BLK3, 132, 8, [MY_ID_NUM] comment
136+
, EFUSE_BLK3, 122, 8, [MY_ID_NUM] comment
137+
CUSTOM_SECURE_VERSION, EFUSE_BLK3, 78, 16, Custom secure version
138+
ID_NUMK, EFUSE_BLK3, 150, 8, [MY_ID_NUMK] comment
139+
, EFUSE_BLK3, 182, 8, [MY_ID_NUMK] comment
140+
MY_DATA, EFUSE_BLK3, 190, 10, My data
141+
MY_DATA.FIELD1, EFUSE_BLK3, 190, 7, Field1
142+
143+
When you include this CSV file, the tool will generate a new section in the summary called ``User fuses``.
144+
145+
.. code-block:: none
146+
147+
User fuses:
148+
MODULE_VERSION (BLOCK3) Module version (56-63) = 0 R/W (0x00)
149+
DEVICE_ROLE (BLOCK3) Device role (64-66) = 0 R/W (0b000)
150+
SETTING_1 (BLOCK3) [SETTING_1_ALT_NAME] Setting 1 (67-72) = 0 R/W (0b000000)
151+
SETTING_2 (BLOCK3) Setting 2 (73-77) = 0 R/W (0b00000)
152+
ID_NUM_0 (BLOCK3) [MY_ID_NUM] comment (140-147) = 0 R/W (0x00)
153+
ID_NUM_1 (BLOCK3) [MY_ID_NUM] comment (132-139) = 0 R/W (0x00)
154+
ID_NUM_2 (BLOCK3) [MY_ID_NUM] comment (122-129) = 0 R/W (0x00)
155+
CUSTOM_SECURE_VERSION (BLOCK3) Custom secure version (78-93) = 0 R/W (0x0000)
156+
ID_NUMK_0 (BLOCK3) [MY_ID_NUMK] comment (150-157) = 0 R/W (0x00)
157+
ID_NUMK_1 (BLOCK3) [MY_ID_NUMK] comment (182-189) = 0 R/W (0x00)
158+
MY_DATA (BLOCK3) My data (190-199) = 0 R/W (0b0000000000)
159+
MY_DATA_FIELD1 (BLOCK3) Field1 (190-196) = 0 R/W (0b0000000)
160+
161+
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.
162+
163+
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.
164+
165+
For convenience, the espefuse summary command includes the used bit range of the field in a comment, such as ``(150-157)`` len = 8 bits.
166+
167+
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.
168+
116169
Recommendations
117170
---------------
118171

espefuse/__init__.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,12 +101,20 @@ def get_esp(
101101
return esp
102102

103103

104-
def get_efuses(esp, skip_connect=False, debug_mode=False, do_not_confirm=False):
104+
def get_efuses(
105+
esp,
106+
skip_connect=False,
107+
debug_mode=False,
108+
do_not_confirm=False,
109+
extend_efuse_table=None,
110+
):
105111
for name in SUPPORTED_CHIPS:
106112
if SUPPORTED_CHIPS[name].chip_name == esp.CHIP_NAME:
107113
efuse = SUPPORTED_CHIPS[name].efuse_lib
108114
return (
109-
efuse.EspEfuses(esp, skip_connect, debug_mode, do_not_confirm),
115+
efuse.EspEfuses(
116+
esp, skip_connect, debug_mode, do_not_confirm, extend_efuse_table
117+
),
110118
efuse.operations,
111119
)
112120
else:
@@ -228,6 +236,12 @@ def main(custom_commandline=None, esp=None):
228236
"(efuses which disable access to blocks or chip).",
229237
action="store_true",
230238
)
239+
init_parser.add_argument(
240+
"--extend-efuse-table",
241+
help="CSV file from ESP-IDF (esp_efuse_custom_table.csv)",
242+
type=argparse.FileType("r"),
243+
default=None,
244+
)
231245

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

259273
efuses, efuse_operations = get_efuses(
260-
esp, just_print_help, debug_mode, common_args.do_not_confirm
274+
esp,
275+
just_print_help,
276+
debug_mode,
277+
common_args.do_not_confirm,
278+
common_args.extend_efuse_table,
261279
)
262280

263281
parser = argparse.ArgumentParser(parents=[init_parser])

espefuse/efuse/csv_table_parser.py

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
# This file helps to parse CSV eFuse tables
2+
#
3+
# SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
4+
#
5+
# SPDX-License-Identifier: GPL-2.0-or-later
6+
7+
import os
8+
import re
9+
import sys
10+
11+
12+
class CSVFuseTable(list):
13+
@classmethod
14+
def from_csv(cls, csv_contents):
15+
res = CSVFuseTable()
16+
lines = csv_contents.splitlines()
17+
18+
def expand_vars(f):
19+
f = os.path.expandvars(f)
20+
m = re.match(r"(?<!\\)\$([A-Za-z_]\w*)", f)
21+
if m:
22+
raise InputError(f"unknown variable '{m.group(1)}'")
23+
return f
24+
25+
for line_no, line in enumerate(lines):
26+
line = expand_vars(line).strip()
27+
if line.startswith("#") or len(line) == 0:
28+
continue
29+
try:
30+
res.append(FuseDefinition.from_csv(line))
31+
except InputError as err:
32+
raise InputError(f"Error at line {line_no + 1}: {err}")
33+
except Exception:
34+
sys.stderr.write(f"Unexpected error parsing line {line_no + 1}: {line}")
35+
raise
36+
37+
# fix up missing bit_start
38+
last_efuse_block = None
39+
for i in res:
40+
if last_efuse_block != i.efuse_block:
41+
last_end = 0
42+
if i.bit_start is None:
43+
i.bit_start = last_end
44+
last_end = i.bit_start + i.bit_count
45+
last_efuse_block = i.efuse_block
46+
47+
res.verify_duplicate_name()
48+
49+
# fix up missing field_name
50+
last_field = None
51+
for i in res:
52+
if i.field_name == "":
53+
if last_field is None:
54+
raise InputError(
55+
f"Error at line {line_no + 1}: {i} missing field name"
56+
)
57+
elif last_field is not None:
58+
i.field_name = last_field.field_name
59+
last_field = i
60+
61+
# fill group
62+
names = [p.field_name for p in res]
63+
duplicates = set(n for n in names if names.count(n) > 1)
64+
for dname in duplicates:
65+
i_count = 0
66+
for p in res:
67+
if p.field_name != dname:
68+
continue
69+
if len(duplicates.intersection([p.field_name])) != 0:
70+
p.field_name = f"{p.field_name}_{i_count}"
71+
if p.alt_names:
72+
p.alt_names = f"{p.alt_names}_{i_count}"
73+
i_count += 1
74+
else:
75+
i_count = 0
76+
77+
for p in res:
78+
p.field_name = p.field_name.replace(".", "_")
79+
if p.alt_names:
80+
p.alt_names = p.alt_names.replace(".", "_")
81+
res.verify_duplicate_name()
82+
return res
83+
84+
def verify_duplicate_name(self):
85+
# check on duplicate name
86+
names = [p.field_name for p in self]
87+
names += [name.replace(".", "_") for name in names if "." in name]
88+
duplicates = set(n for n in names if names.count(n) > 1)
89+
90+
# print sorted duplicate partitions by name
91+
if len(duplicates) != 0:
92+
fl_error = False
93+
for p in self:
94+
field_name = p.field_name + p.group
95+
if field_name != "" and len(duplicates.intersection([field_name])) != 0:
96+
fl_error = True
97+
print(
98+
f"Field at {p.field_name}, {p.efuse_block}, "
99+
f"{p.bit_start}, {p.bit_count} have duplicate field_name"
100+
)
101+
if fl_error is True:
102+
raise InputError("Field names must be unique")
103+
104+
def check_struct_field_name(self):
105+
# check that structured fields have a root field
106+
for p in self:
107+
if "." in p.field_name:
108+
name = ""
109+
for sub in p.field_name.split(".")[:-1]:
110+
name = sub if name == "" else name + "." + sub
111+
missed_name = True
112+
for d in self:
113+
if (
114+
p is not d
115+
and p.efuse_block == d.efuse_block
116+
and name == d.field_name
117+
):
118+
missed_name = False
119+
if missed_name:
120+
raise InputError(f"{name} is not found")
121+
122+
def verify(self, type_table=None):
123+
def check(p, n):
124+
left = n.bit_start
125+
right = n.bit_start + n.bit_count - 1
126+
start = p.bit_start
127+
end = p.bit_start + p.bit_count - 1
128+
if left <= start <= right:
129+
if left <= end <= right:
130+
return "included in" # [n [p...p] n]
131+
return "intersected with" # [n [p..n]..p]
132+
if left <= end <= right:
133+
return "intersected with" # [p..[n..p] n]
134+
if start <= left and right <= end:
135+
return "wraps" # [p [n...n] p]
136+
return "ok" # [p] [n] or [n] [p]
137+
138+
def print_error(p, n, state):
139+
raise InputError(
140+
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}"
141+
)
142+
143+
for p in self:
144+
p.verify(type_table)
145+
146+
self.verify_duplicate_name()
147+
if type_table != "custom_table":
148+
# check will be done for common and custom tables together
149+
self.check_struct_field_name()
150+
151+
# check for overlaps
152+
for p in self:
153+
for n in self:
154+
if p is not n and p.efuse_block == n.efuse_block:
155+
state = check(p, n)
156+
if state != "ok":
157+
if "." in p.field_name:
158+
name = ""
159+
for sub in p.field_name.split("."):
160+
name = sub if name == "" else name + "." + sub
161+
for d in self:
162+
if (
163+
p is not d
164+
and p.efuse_block == d.efuse_block
165+
and name == d.field_name
166+
):
167+
state = check(p, d)
168+
if state == "included in":
169+
break
170+
elif state != "intersected with":
171+
state = "out of range"
172+
print_error(p, d, state)
173+
continue
174+
elif "." in n.field_name:
175+
continue
176+
print_error(p, n, state)
177+
178+
179+
class FuseDefinition(object):
180+
def __init__(self):
181+
self.field_name = ""
182+
self.group = ""
183+
self.efuse_block = ""
184+
self.bit_start = None
185+
self.bit_count = None
186+
self.define = None
187+
self.comment = ""
188+
self.alt_names = ""
189+
self.MAX_BITS_OF_BLOCK = 256
190+
191+
@classmethod
192+
def from_csv(cls, line):
193+
"""Parse a line from the CSV"""
194+
line_w_defaults = line + ",,,,"
195+
fields = [f.strip() for f in line_w_defaults.split(",")]
196+
197+
res = FuseDefinition()
198+
res.field_name = fields[0]
199+
res.efuse_block = res.parse_block(fields[1])
200+
res.bit_start = res.parse_num(fields[2])
201+
res.bit_count = res.parse_bit_count(fields[3])
202+
if res.bit_count is None or res.bit_count == 0:
203+
raise InputError("Field bit_count can't be empty")
204+
res.comment = fields[4].rstrip("\\").rstrip()
205+
res.comment += f" ({res.bit_start}-{res.bit_start + res.bit_count - 1})"
206+
res.alt_names = res.get_alt_names(res.comment)
207+
return res
208+
209+
def parse_num(self, strval):
210+
if strval == "":
211+
return None
212+
return self.parse_int(strval)
213+
214+
def parse_bit_count(self, strval):
215+
if strval == "MAX_BLK_LEN":
216+
self.define = strval
217+
return self.MAX_BITS_OF_BLOCK
218+
else:
219+
return self.parse_num(strval)
220+
221+
def parse_int(self, v):
222+
try:
223+
return int(v, 0)
224+
except ValueError:
225+
raise InputError(f"Invalid field value {v}")
226+
227+
def parse_block(self, strval):
228+
if strval == "":
229+
raise InputError("Field 'efuse_block' can't be left empty.")
230+
return self.parse_int(strval.lstrip("EFUSE_BLK"))
231+
232+
def verify(self, type_table):
233+
if self.efuse_block is None:
234+
raise ValidationError(self, "efuse_block field is not set")
235+
if self.bit_count is None:
236+
raise ValidationError(self, "bit_count field is not set")
237+
max_bits = self.MAX_BITS_OF_BLOCK
238+
if self.bit_start + self.bit_count > max_bits:
239+
raise ValidationError(
240+
self,
241+
f"The field is outside the boundaries(max_bits = {max_bits}) of the {self.efuse_block} block",
242+
)
243+
244+
def get_bit_count(self, check_define=True):
245+
if check_define is True and self.define is not None:
246+
return self.define
247+
else:
248+
return self.bit_count
249+
250+
def get_alt_names(self, comment):
251+
result = re.search(r"^\[(.*?)\]", comment)
252+
if result:
253+
return result.group(1)
254+
return ""
255+
256+
257+
class InputError(RuntimeError):
258+
def __init__(self, e):
259+
super(InputError, self).__init__(e)
260+
261+
262+
class ValidationError(InputError):
263+
def __init__(self, p, message):
264+
super(ValidationError, self).__init__(
265+
f"Entry {p.field_name} invalid: {message}"
266+
)

espefuse/efuse/esp32/emulate_efuse_controller.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class EmulateEfuseController(EmulateEfuseControllerBase):
1919

2020
def __init__(self, efuse_file=None, debug=False):
2121
self.Blocks = EfuseDefineBlocks
22-
self.Fields = EfuseDefineFields()
22+
self.Fields = EfuseDefineFields(None)
2323
self.REGS = EfuseDefineRegisters
2424
super(EmulateEfuseController, self).__init__(efuse_file, debug)
2525

0 commit comments

Comments
 (0)