1# This file includes the common operations with eFuses for chips
2#
3# SPDX-FileCopyrightText: 2020-2022 Espressif Systems (Shanghai) CO LTD
4#
5# SPDX-License-Identifier: GPL-2.0-or-later
6
7import argparse
8import json
9import sys
10
11from bitstring import BitStream
12
13import esptool
14
15from . import base_fields
16from . import util
17
18
19def add_common_commands(subparsers, efuses):
20    class ActionEfuseValuePair(argparse.Action):
21        def __init__(self, option_strings, dest, nargs=None, **kwargs):
22            self._nargs = nargs
23            self._choices = kwargs.get("efuse_choices")
24            self.efuses = kwargs.get("efuses")
25            del kwargs["efuse_choices"]
26            del kwargs["efuses"]
27            super(ActionEfuseValuePair, self).__init__(
28                option_strings, dest, nargs=nargs, **kwargs
29            )
30
31        def __call__(self, parser, namespace, values, option_string=None):
32            def check_efuse_name(efuse_name, efuse_list):
33                if efuse_name not in self._choices:
34                    raise esptool.FatalError(
35                        "Invalid the efuse name '{}'. "
36                        "Available the efuse names: {}".format(
37                            efuse_name, self._choices
38                        )
39                    )
40
41            efuse_value_pairs = {}
42            if len(values) > 1:
43                if len(values) % 2:
44                    raise esptool.FatalError(
45                        "The list does not have a valid pair (name value) {}".format(
46                            values
47                        )
48                    )
49                for i in range(0, len(values), 2):
50                    efuse_name, new_value = values[i : i + 2 :]
51                    check_efuse_name(efuse_name, self._choices)
52                    check_arg = base_fields.CheckArgValue(self.efuses, efuse_name)
53                    efuse_value_pairs[efuse_name] = check_arg(new_value)
54            else:
55                # For the case of compatibility, when only the efuse_name is given
56                # Fields with 'bitcount' and 'bool' types can be without new_value arg
57                efuse_name = values[0]
58                check_efuse_name(efuse_name, self._choices)
59                check_arg = base_fields.CheckArgValue(self.efuses, efuse_name)
60                efuse_value_pairs[efuse_name] = check_arg(None)
61            setattr(namespace, self.dest, efuse_value_pairs)
62
63    burn = subparsers.add_parser(
64        "burn_efuse", help="Burn the efuse with the specified name"
65    )
66    burn.add_argument(
67        "name_value_pairs",
68        help="Name of efuse field and new value pairs to burn. EFUSE_NAME: "
69        "[{}].".format(", ".join([e.name for e in efuses.efuses])),
70        action=ActionEfuseValuePair,
71        nargs="+",
72        metavar="[EFUSE_NAME VALUE]",
73        efuse_choices=[e.name for e in efuses.efuses]
74        + [name for e in efuses.efuses for name in e.alt_names if name != ""],
75        efuses=efuses,
76    )
77
78    read_protect_efuse = subparsers.add_parser(
79        "read_protect_efuse",
80        help="Disable readback for the efuse with the specified name",
81    )
82    read_protect_efuse.add_argument(
83        "efuse_name",
84        help="Name of efuse register to burn",
85        nargs="+",
86        choices=[e.name for e in efuses.efuses if e.read_disable_bit is not None]
87        + [
88            name
89            for e in efuses.efuses
90            if e.read_disable_bit is not None
91            for name in e.alt_names
92            if name != ""
93        ],
94    )
95
96    write_protect_efuse = subparsers.add_parser(
97        "write_protect_efuse",
98        help="Disable writing to the efuse with the specified name",
99    )
100    write_protect_efuse.add_argument(
101        "efuse_name",
102        help="Name of efuse register to burn",
103        nargs="+",
104        choices=[e.name for e in efuses.efuses if e.write_disable_bit is not None]
105        + [
106            name
107            for e in efuses.efuses
108            if e.write_disable_bit is not None
109            for name in e.alt_names
110            if name != ""
111        ],
112    )
113
114    burn_block_data = subparsers.add_parser(
115        "burn_block_data",
116        help="Burn non-key data to EFUSE blocks. "
117        "(Don't use this command to burn key data for Flash Encryption or "
118        "ESP32 Secure Boot V1, as the byte order of keys is swapped (use burn_key)).",
119    )
120    add_force_write_always(burn_block_data)
121    burn_block_data.add_argument(
122        "--offset", "-o", help="Byte offset in the efuse block", type=int, default=0
123    )
124    burn_block_data.add_argument(
125        "block",
126        help="Efuse block to burn.",
127        action="append",
128        choices=efuses.BURN_BLOCK_DATA_NAMES,
129    )
130    burn_block_data.add_argument(
131        "datafile",
132        help="File containing data to burn into the efuse block",
133        action="append",
134        type=argparse.FileType("rb"),
135    )
136    for _ in range(0, len(efuses.BURN_BLOCK_DATA_NAMES)):
137        burn_block_data.add_argument(
138            "block",
139            help="Efuse block to burn.",
140            metavar="BLOCK",
141            nargs="?",
142            action="append",
143            choices=efuses.BURN_BLOCK_DATA_NAMES,
144        )
145        burn_block_data.add_argument(
146            "datafile",
147            nargs="?",
148            help="File containing data to burn into the efuse block",
149            metavar="DATAFILE",
150            action="append",
151            type=argparse.FileType("rb"),
152        )
153
154    set_bit_cmd = subparsers.add_parser("burn_bit", help="Burn bit in the efuse block.")
155    add_force_write_always(set_bit_cmd)
156    set_bit_cmd.add_argument(
157        "block", help="Efuse block to burn.", choices=efuses.BURN_BLOCK_DATA_NAMES
158    )
159    set_bit_cmd.add_argument(
160        "bit_number",
161        help="Bit number in the efuse block [0..BLK_LEN-1]",
162        nargs="+",
163        type=int,
164    )
165
166    subparsers.add_parser(
167        "adc_info",
168        help="Display information about ADC calibration data stored in efuse.",
169    )
170
171    dump_cmd = subparsers.add_parser("dump", help="Dump raw hex values of all efuses")
172    dump_cmd.add_argument(
173        "--file_name",
174        help="Saves dump for each block into separate file. Provide the common "
175        "path name /path/blk.bin, it will create: blk0.bin, blk1.bin ... blkN.bin. "
176        "Use burn_block_data to write it back to another chip.",
177    )
178
179    summary_cmd = subparsers.add_parser(
180        "summary", help="Print human-readable summary of efuse values"
181    )
182    summary_cmd.add_argument(
183        "--format",
184        help="Select the summary format",
185        choices=["summary", "json", "value_only"],
186        default="summary",
187    )
188    summary_cmd.add_argument(
189        "--file",
190        help="File to save the efuse summary",
191        type=argparse.FileType("w"),
192        default=sys.stdout,
193    )
194    summary_cmd.add_argument(
195        "efuses_to_show",
196        help="The efuses to show. If not provided, all efuses will be shown.",
197        nargs="*",
198    )
199
200    execute_scripts = subparsers.add_parser(
201        "execute_scripts", help="Executes scripts to burn at one time."
202    )
203    execute_scripts.add_argument(
204        "scripts",
205        help="The special format of python scripts.",
206        nargs="+",
207        type=argparse.FileType("r"),
208    )
209    execute_scripts.add_argument(
210        "--index",
211        help="integer index. "
212        "It allows to retrieve unique data per chip from configfiles "
213        "and then burn them (ex. CUSTOM_MAC, UNIQUE_ID).",
214        type=int,
215    )
216    execute_scripts.add_argument(
217        "--configfiles",
218        help="List of configfiles with data",
219        nargs="?",
220        action="append",
221        type=argparse.FileType("r"),
222    )
223
224    check_error_cmd = subparsers.add_parser("check_error", help="Checks eFuse errors")
225    check_error_cmd.add_argument(
226        "--recovery",
227        help="Recovery of BLOCKs after encoding errors",
228        action="store_true",
229    )
230
231
232def add_force_write_always(p):
233    p.add_argument(
234        "--force-write-always",
235        help="Write the efuse even if it looks like it's already been written, "
236        "or is write protected. Note that this option can't disable write protection, "
237        "or clear any bit which has already been set.",
238        action="store_true",
239    )
240
241
242def add_show_sensitive_info_option(p):
243    p.add_argument(
244        "--show-sensitive-info",
245        help="Show data to be burned (may expose sensitive data). "
246        "Enabled if --debug is used.",
247        action="store_true",
248        default=False,
249    )
250
251
252def summary(esp, efuses, args):
253    """Print a human-readable or json summary of efuse contents"""
254    ROW_FORMAT = "%-50s %-50s%s = %s %s %s"
255    human_output = args.format in ["summary", "value_only"]
256    value_only = args.format == "value_only"
257    if value_only and len(args.efuses_to_show) != 1:
258        raise esptool.FatalError(
259            "The 'value_only' format can be used exactly for one efuse."
260        )
261    do_filtering = bool(args.efuses_to_show)
262    json_efuse = {}
263    summary_efuse = []
264    if args.file != sys.stdout:
265        print("Saving efuse values to " + args.file.name)
266    if human_output and not value_only:
267        summary_efuse.append(
268            ROW_FORMAT.replace("-50", "-12")
269            % (
270                "EFUSE_NAME (Block)",
271                "Description",
272                "",
273                "[Meaningful Value]",
274                "[Readable/Writeable]",
275                "(Hex Value)",
276            )
277        )
278        summary_efuse.append("-" * 88)
279    for category in sorted(set(e.category for e in efuses), key=lambda c: c.title()):
280        if human_output and not value_only:
281            summary_efuse.append(f"{category.title()} fuses:")
282        for e in (e for e in efuses if e.category == category):
283            if e.efuse_type.startswith("bytes"):
284                raw = ""
285            else:
286                raw = "({})".format(e.get_bitstring())
287            (readable, writeable) = (e.is_readable(), e.is_writeable())
288            if readable and writeable:
289                perms = "R/W"
290            elif readable:
291                perms = "R/-"
292            elif writeable:
293                perms = "-/W"
294            else:
295                perms = "-/-"
296            base_value = e.get_meaning()
297            value = str(base_value)
298            if not readable:
299                count_read_disable_bits = e.get_count_read_disable_bits()
300                if count_read_disable_bits == 2:
301                    # On the C2 chip, BLOCK_KEY0 has two read protection bits [0, 1]
302                    # related to the lower and higher part of the block.
303                    v = [value[: (len(value) // 2)], value[(len(value) // 2) :]]
304                    for i in range(count_read_disable_bits):
305                        if not e.is_readable(blk_part=i):
306                            v[i] = v[i].replace("0", "?")
307                    value = "".join(v)
308                else:
309                    value = value.replace("0", "?")
310            if (
311                human_output
312                and (not do_filtering or e.name in args.efuses_to_show)
313                and not value_only
314            ):
315                summary_efuse.append(
316                    ROW_FORMAT
317                    % (
318                        e.get_info(),
319                        e.description[:50],
320                        "\n  " if len(value) > 20 else "",
321                        value,
322                        perms,
323                        raw,
324                    )
325                )
326                desc_len = len(e.description[50:])
327                if desc_len:
328                    desc_len += 50
329                    for i in range(50, desc_len, 50):
330                        summary_efuse.append(
331                            f"{'':<50} {e.description[i : (50 + i)]:<50}"
332                        )
333            elif human_output and value_only and e.name in args.efuses_to_show:
334                summary_efuse.append(f"{value}")
335            elif args.format == "json" and (
336                not do_filtering or e.name in args.efuses_to_show
337            ):
338                json_efuse[e.name] = {
339                    "name": e.name,
340                    "value": base_value if readable else value,
341                    "readable": readable,
342                    "writeable": writeable,
343                    "description": e.description,
344                    "category": e.category,
345                    "block": e.block,
346                    "word": e.word,
347                    "pos": e.pos,
348                    "efuse_type": e.efuse_type,
349                    "bit_len": e.bit_len,
350                }
351        if human_output and not value_only:
352            # Remove empty category if efuses are filtered and there are none to show
353            if do_filtering and summary_efuse[-1] == f"{category.title()} fuses:":
354                summary_efuse.pop()
355            else:
356                summary_efuse.append("")
357    if human_output and not value_only:
358        summary_efuse.append(efuses.summary())
359        warnings = efuses.get_coding_scheme_warnings()
360        if warnings:
361            summary_efuse.append(
362                "WARNING: Coding scheme has encoding bit error warnings"
363            )
364    if human_output:
365        for line in summary_efuse:
366            print(line, file=args.file)
367        if args.file != sys.stdout:
368            args.file.close()
369            print("Done")
370    elif args.format == "json":
371        json.dump(json_efuse, args.file, sort_keys=True, indent=4)
372        print("")
373
374
375def dump(esp, efuses, args):
376    """Dump raw efuse data registers"""
377    # Using --debug option allows to print dump.
378    # Nothing to do here. The log will be printed
379    # during EspEfuses.__init__() in self.read_blocks()
380    if args.file_name:
381        # save dump to the file
382        for block in efuses.blocks:
383            file_dump_name = args.file_name
384            place_for_index = file_dump_name.find(".bin")
385            file_dump_name = (
386                file_dump_name[:place_for_index]
387                + str(block.id)
388                + file_dump_name[place_for_index:]
389            )
390            print(file_dump_name)
391            with open(file_dump_name, "wb") as f:
392                block.get_bitstring().byteswap()
393                block.get_bitstring().tofile(f)
394
395
396def burn_efuse(esp, efuses, args):
397    def print_attention(blocked_efuses_after_burn):
398        if len(blocked_efuses_after_burn):
399            print(
400                "    ATTENTION! This BLOCK uses NOT the NONE coding scheme "
401                "and after 'BURN', these efuses can not be burned in the feature:"
402            )
403            for i in range(0, len(blocked_efuses_after_burn), 5):
404                print(
405                    "              ",
406                    "".join("{}".format(blocked_efuses_after_burn[i : i + 5 :])),
407                )
408
409    efuse_name_list = [name for name in args.name_value_pairs.keys()]
410    burn_efuses_list = [efuses[name] for name in efuse_name_list]
411    old_value_list = [efuses[name].get_raw() for name in efuse_name_list]
412    new_value_list = [value for value in args.name_value_pairs.values()]
413    util.check_duplicate_name_in_list(efuse_name_list)
414
415    attention = ""
416    print("The efuses to burn:")
417    for block in efuses.blocks:
418        burn_list_a_block = [e for e in burn_efuses_list if e.block == block.id]
419        if len(burn_list_a_block):
420            print("  from BLOCK%d" % (block.id))
421            for field in burn_list_a_block:
422                print("     - %s" % (field.name))
423                if (
424                    efuses.blocks[field.block].get_coding_scheme()
425                    != efuses.REGS.CODING_SCHEME_NONE
426                ):
427                    using_the_same_block_names = [
428                        e.name for e in efuses if e.block == field.block
429                    ]
430                    wr_names = [e.name for e in burn_list_a_block]
431                    blocked_efuses_after_burn = [
432                        name
433                        for name in using_the_same_block_names
434                        if name not in wr_names
435                    ]
436                    attention = " (see 'ATTENTION!' above)"
437            if attention:
438                print_attention(blocked_efuses_after_burn)
439
440    print("\nBurning efuses{}:".format(attention))
441    for efuse, new_value in zip(burn_efuses_list, new_value_list):
442        print(
443            "\n    - '{}' ({}) {} -> {}".format(
444                efuse.name,
445                efuse.description,
446                efuse.get_bitstring(),
447                efuse.convert_to_bitstring(new_value),
448            )
449        )
450        efuse.save(new_value)
451
452    print()
453    if "ENABLE_SECURITY_DOWNLOAD" in efuse_name_list:
454        print(
455            "ENABLE_SECURITY_DOWNLOAD -> 1: eFuses will not be read back "
456            "for confirmation because this mode disables "
457            "any SRAM and register operations."
458        )
459        print("                               espefuse will not work.")
460        print("                               esptool can read/write only flash.")
461
462    if "DIS_DOWNLOAD_MODE" in efuse_name_list:
463        print(
464            "DIS_DOWNLOAD_MODE -> 1: eFuses will not be read back for "
465            "confirmation because this mode disables any communication with the chip."
466        )
467        print(
468            "                        espefuse/esptool will not work because "
469            "they will not be able to connect to the chip."
470        )
471
472    if (
473        esp.CHIP_NAME == "ESP32"
474        and esp.get_chip_revision() >= 300
475        and "UART_DOWNLOAD_DIS" in efuse_name_list
476    ):
477        print(
478            "UART_DOWNLOAD_DIS -> 1: eFuses will be read for confirmation, "
479            "but after that connection to the chip will become impossible."
480        )
481        print("                        espefuse/esptool will not work.")
482
483    if not efuses.burn_all(check_batch_mode=True):
484        return
485
486    print("Checking efuses...")
487    raise_error = False
488    for efuse, old_value, new_value in zip(
489        burn_efuses_list, old_value_list, new_value_list
490    ):
491        if not efuse.is_readable():
492            print(
493                "Efuse %s is read-protected. Read back the burn value is not possible."
494                % efuse.name
495            )
496        else:
497            new_value = efuse.convert_to_bitstring(new_value)
498            burned_value = efuse.get_bitstring()
499            if burned_value != new_value:
500                print(
501                    burned_value,
502                    "->",
503                    new_value,
504                    "Efuse %s failed to burn. Protected?" % efuse.name,
505                )
506                raise_error = True
507    if raise_error:
508        raise esptool.FatalError("The burn was not successful.")
509    else:
510        print("Successful")
511
512
513def read_protect_efuse(esp, efuses, args):
514    util.check_duplicate_name_in_list(args.efuse_name)
515
516    for efuse_name in args.efuse_name:
517        efuse = efuses[efuse_name]
518        if not efuse.is_readable():
519            print("Efuse %s is already read protected" % efuse.name)
520        else:
521            if esp.CHIP_NAME == "ESP32":
522                if (
523                    efuse_name == "BLOCK2"
524                    and not efuses["ABS_DONE_0"].get()
525                    and esp.get_chip_revision() >= 300
526                ):
527                    if efuses["ABS_DONE_1"].get():
528                        raise esptool.FatalError(
529                            "Secure Boot V2 is on (ABS_DONE_1 = True), "
530                            "BLOCK2 must be readable, stop this operation!"
531                        )
532                    else:
533                        print(
534                            "If Secure Boot V2 is used, BLOCK2 must be readable, "
535                            "please stop this operation!"
536                        )
537            elif esp.CHIP_NAME == "ESP32-C2":
538                error = (
539                    not efuses["XTS_KEY_LENGTH_256"].get()
540                    and efuse_name == "BLOCK_KEY0"
541                )
542                error |= efuses["SECURE_BOOT_EN"].get() and efuse_name in [
543                    "BLOCK_KEY0",
544                    "BLOCK_KEY0_HI_128",
545                ]
546                if error:
547                    raise esptool.FatalError(
548                        "%s must be readable, stop this operation!" % efuse_name
549                    )
550            else:
551                for block in efuses.Blocks.BLOCKS:
552                    block = efuses.Blocks.get(block)
553                    if block.name == efuse_name and block.key_purpose is not None:
554                        if not efuses[block.key_purpose].need_rd_protect(
555                            efuses[block.key_purpose].get()
556                        ):
557                            raise esptool.FatalError(
558                                "%s must be readable, stop this operation!" % efuse_name
559                            )
560                        break
561            # make full list of which efuses will be disabled
562            # (ie share a read disable bit)
563            all_disabling = [
564                e for e in efuses if e.read_disable_bit == efuse.read_disable_bit
565            ]
566            names = ", ".join(e.name for e in all_disabling)
567            print(
568                "Permanently read-disabling efuse%s %s"
569                % ("s" if len(all_disabling) > 1 else "", names)
570            )
571            efuse.disable_read()
572
573    if not efuses.burn_all(check_batch_mode=True):
574        return
575
576    print("Checking efuses...")
577    raise_error = False
578    for efuse_name in args.efuse_name:
579        efuse = efuses[efuse_name]
580        if efuse.is_readable():
581            print("Efuse %s is not read-protected." % efuse.name)
582            raise_error = True
583    if raise_error:
584        raise esptool.FatalError("The burn was not successful.")
585    else:
586        print("Successful")
587
588
589def write_protect_efuse(esp, efuses, args):
590    util.check_duplicate_name_in_list(args.efuse_name)
591    for efuse_name in args.efuse_name:
592        efuse = efuses[efuse_name]
593        if not efuse.is_writeable():
594            print("Efuse %s is already write protected" % efuse.name)
595        else:
596            # make full list of which efuses will be disabled
597            # (ie share a write disable bit)
598            all_disabling = [
599                e for e in efuses if e.write_disable_bit == efuse.write_disable_bit
600            ]
601            names = ", ".join(e.name for e in all_disabling)
602            print(
603                "Permanently write-disabling efuse%s %s"
604                % ("s" if len(all_disabling) > 1 else "", names)
605            )
606            efuse.disable_write()
607
608    if not efuses.burn_all(check_batch_mode=True):
609        return
610
611    print("Checking efuses...")
612    raise_error = False
613    for efuse_name in args.efuse_name:
614        efuse = efuses[efuse_name]
615        if efuse.is_writeable():
616            print("Efuse %s is not write-protected." % efuse.name)
617            raise_error = True
618    if raise_error:
619        raise esptool.FatalError("The burn was not successful.")
620    else:
621        print("Successful")
622
623
624def burn_block_data(esp, efuses, args):
625    block_name_list = args.block[
626        0 : len([name for name in args.block if name is not None]) :
627    ]
628    datafile_list = args.datafile[
629        0 : len([name for name in args.datafile if name is not None]) :
630    ]
631    efuses.force_write_always = args.force_write_always
632
633    util.check_duplicate_name_in_list(block_name_list)
634    if args.offset and len(block_name_list) > 1:
635        raise esptool.FatalError(
636            "The 'offset' option is not applicable when a few blocks are passed. "
637            "With 'offset', should only one block be used."
638        )
639    else:
640        offset = args.offset
641        if offset:
642            num_block = efuses.get_index_block_by_name(block_name_list[0])
643            block = efuses.blocks[num_block]
644            num_bytes = block.get_block_len()
645            if offset >= num_bytes:
646                raise esptool.FatalError(
647                    "Invalid offset: the block%d only holds %d bytes."
648                    % (block.id, num_bytes)
649                )
650    if len(block_name_list) != len(datafile_list):
651        raise esptool.FatalError(
652            "The number of block_name (%d) and datafile (%d) should be the same."
653            % (len(block_name_list), len(datafile_list))
654        )
655
656    for block_name, datafile in zip(block_name_list, datafile_list):
657        num_block = efuses.get_index_block_by_name(block_name)
658        block = efuses.blocks[num_block]
659        data = datafile.read()
660        num_bytes = block.get_block_len()
661        if offset != 0:
662            data = (b"\x00" * offset) + data
663            data = data + (b"\x00" * (num_bytes - len(data)))
664        if len(data) != num_bytes:
665            raise esptool.FatalError(
666                "Data does not fit: the block%d size is %d bytes, "
667                "data file is %d bytes, offset %d"
668                % (block.id, num_bytes, len(data), offset)
669            )
670        print(
671            "[{:02}] {:20} size={:02} bytes, offset={:02} - > [{}].".format(
672                block.id, block.name, len(data), offset, util.hexify(data, " ")
673            )
674        )
675        block.save(data)
676
677    if not efuses.burn_all(check_batch_mode=True):
678        return
679    print("Successful")
680
681
682def burn_bit(esp, efuses, args):
683    efuses.force_write_always = args.force_write_always
684    num_block = efuses.get_index_block_by_name(args.block)
685    block = efuses.blocks[num_block]
686    data_block = BitStream(block.get_block_len() * 8)
687    data_block.set(0)
688    try:
689        data_block.set(True, args.bit_number)
690    except IndexError:
691        raise esptool.FatalError(
692            "%s has bit_number in [0..%d]" % (args.block, data_block.len - 1)
693        )
694    data_block.reverse()
695    print(
696        "bit_number:   "
697        "[%-03d]........................................................[0]"
698        % (data_block.len - 1)
699    )
700    print("BLOCK%-2d   :" % block.id, data_block)
701    block.print_block(data_block, "regs_to_write", debug=True)
702    block.save(data_block.bytes[::-1])
703
704    if not efuses.burn_all(check_batch_mode=True):
705        return
706    print("Successful")
707
708
709def get_error_summary(efuses):
710    efuses.get_coding_scheme_warnings()
711    error_in_blocks = any(blk.fail or blk.num_errors != 0 for blk in efuses.blocks)
712    if not error_in_blocks:
713        return False
714    writable = True
715    for blk in efuses.blocks:
716        if blk.fail or blk.num_errors:
717            if blk.id == 0:
718                for field in efuses:
719                    if field.block == blk.id and (field.fail or field.num_errors):
720                        wr = "writable" if field.is_writeable() else "not writable"
721                        writable &= wr == "writable"
722                        name = field.name
723                        val = field.get()
724                        print(f"BLOCK{field.block:<2}: {name:<40} = {val:<8} ({wr})")
725            else:
726                wr = "writable" if blk.is_writeable() else "not writable"
727                writable &= wr == "writable"
728                name = f"{blk.name} [ERRORS:{blk.num_errors} FAIL:{int(blk.fail)}]"
729                val = str(blk.get_bitstring())
730                print(f"BLOCK{blk.id:<2}: {name:<40} = {val:<8} ({wr})")
731    if not writable and error_in_blocks:
732        print("Not all errors can be fixed because some fields are write-protected!")
733    return True
734
735
736def check_error(esp, efuses, args):
737    error_in_blocks = get_error_summary(efuses)
738    if args.recovery and error_in_blocks:
739        confirmed = False
740        for block in reversed(efuses.blocks):
741            if block.fail or block.num_errors > 0:
742                if not block.get_bitstring().all(False):
743                    block.save(block.get_bitstring().bytes[::-1])
744                    if not confirmed:
745                        confirmed = True
746                        efuses.confirm(
747                            "Recovery of block coding errors", args.do_not_confirm
748                        )
749                    block.burn()
750        if confirmed:
751            efuses.update_efuses()
752        error_in_blocks = get_error_summary(efuses)
753    if error_in_blocks:
754        raise esptool.FatalError("Error(s) were detected in eFuses")
755    print("No errors detected")
756