diff --git a/docs/opentitan/otptool.md b/docs/opentitan/otptool.md index 002c1a35faad..c0ef158e2a7b 100644 --- a/docs/opentitan/otptool.md +++ b/docs/opentitan/otptool.md @@ -7,10 +7,11 @@ controller virtual device. ````text usage: otptool.py [-h] [-j HJSON] [-m VMEM] [-l SV] [-o FILE] [-r RAW] - [-x EXPORT] [-k {auto,otp,fuz}] [-e BITS] [-C CONFIG] - [-c INT] [-i INT] [-w] [-n] [-f PART:FIELD] [--no-version] - [-s] [-E] [-D] [-U] [-g {LCVAL,LCTPL,PARTS,REGS}] [-F] - [-G PART] [--change PART:FIELD=VALUE] [--empty PARTITION] + [-x VMEM] [-X VMEM] [-k {auto,otp,fuz}] [-e BITS] + [-C CONFIG] [-c INT] [-i INT] [-w] [-n] [-f PART:FIELD] + [--force-absorb] [--no-version] [-s] [-E] [-D] [-U] + [-g {LCVAL,LCTPL,PARTS,REGS}] [-F] [-G PART] + [--change PART:FIELD=VALUE] [--empty PARTITION] [--erase PART:FIELD] [--clear-bit CLEAR_BIT] [--set-bit SET_BIT] [--toggle-bit TOGGLE_BIT] [--write ADDR/HEXBYTES] [--patch-token NAME=VALUE] @@ -27,7 +28,9 @@ Files: -l, --lifecycle SV input lifecycle system verilog file -o, --output FILE output filename (default to stdout) -r, --raw RAW QEMU OTP raw image file - -x, --export EXPORT Export data to a VMEM file + -x, --export VMEM Export data to a VMEM file + -X, --export-verbose VMEM + Export data to a VMEM file with field info Parameters: -k, --kind {auto,otp,fuz} @@ -40,6 +43,7 @@ Parameters: -n, --no-decode do not attempt to decode OTP fields -f, --filter PART:FIELD filter which OTP fields are shown + --force-absorb force absorption --no-version do not report the OTP image version Commands: @@ -83,7 +87,7 @@ This script can be used for several purposes: 2. Showing and decoding the content of OTP image files, whether it is a pristine generated file or a file that has been modified by the QEMU machine, 3. Verifying the Digest of the OTP partitions that support HW digest (using Present scrambling), -4. Create QEMU C source files containining definition values that replicate the ones generated when +4. Create QEMU C source files containing definition values that replicate the ones generated when the OT HW is built. Please note that only the first feature is supported for Fuse (non-OpenTitan) images. @@ -180,6 +184,9 @@ Fuse RAW images only use the v1 type. contain long sequence of bytes. If repeated, the empty long fields are also printed in full, as a sequence of empty bytes. +* `-X` similar to `-x` option, with extra comment in generated file to document each generated VMEM + line with the partition and field description. + * `-x` export the current data and ECC content into a text file using the VMEM 24 encoding format, _i.e._ 24-bit hex chunks where the first byte depicts the 6-bit ECC and the remaining two bytes contain a 16-bit value. @@ -209,6 +216,10 @@ Fuse RAW images only use the v1 type. * `--erase` reset a specific field within a partition. The flag may be repeated. +* `--force-absorb` force allocation of aborbable free space. Input HJSON map files already define + absorbable space allocated to absorbable partitions. This option forces absorbable space + allocation. It may be removed in a future version. + * `--no-version` disable OTP image version reporting when `-s` is used. * `--out-kind` define the output format for code generation for the `-g` option. Note that only a diff --git a/python/qemu/ot/otp/image.py b/python/qemu/ot/otp/image.py index bdfdeb0a9b86..f569da83597b 100644 --- a/python/qemu/ot/otp/image.py +++ b/python/qemu/ot/otp/image.py @@ -15,9 +15,10 @@ from typing import Any, BinaryIO, Optional, Sequence, TextIO, Union import re +from ot.util.misc import HexInt, classproperty, split_map_join + from .map import OtpMap from .partition import OtpPartition, OtpLifecycleExtension -from ..util.misc import HexInt, classproperty class OtpImage: @@ -216,13 +217,25 @@ def load_vmem(self, vfp: TextIO, vmem_kind: Optional[str] = None, self._magic = f'v{vkind[:3].upper()}'.encode() self._changed = False - def save_vmem(self, vfp: TextIO) -> None: + def save_vmem(self, vfp: TextIO, verbose: bool = False) -> None: """Save a VMEM '24' text stream.""" + if verbose and not (self._partitions and self._part_offsets): + self._log.warning('Verbose mode disabled as no OTP map is known') + verbose = False dsrc = iunpack(' None: """Load lifecyle values.""" @@ -670,6 +683,27 @@ def decode_ecc_22_16(cls, data: int, ecc: int) -> tuple[int, int]: return err, odata + def document_partitions(self) -> list[str]: + """Return the documentation for each 16-bit word. + + Try to match the exact comment syntax from OpenTitan tool. + + :return: the meaning of each half-words + """ + fields: list[str] = [] + for part in self._partitions: + for field in part.document_fields(): + if not field: + fields.append('unallocated') + continue + if ',' in field: + # pylint: disable=cell-var-from-loop + fields.append(split_map_join(', ', field, + lambda fld: f'{part.name}: {fld}')) + continue + fields.append(f'{part.name}: {field}') + return fields + def _load_header(self, bfp: BinaryIO) -> dict[str, Any]: hfmt = self.HEADER_FORMAT fhfmt = ''.join(hfmt.values()) diff --git a/python/qemu/ot/otp/map.py b/python/qemu/ot/otp/map.py index 3b4fb2992513..52d092c2ce9c 100644 --- a/python/qemu/ot/otp/map.py +++ b/python/qemu/ot/otp/map.py @@ -44,7 +44,7 @@ def __init__(self): self._partitions: list[OtpPartition] = [] self._git_version: Optional[str] = None - def load(self, hfp: TextIO) -> None: + def load(self, hfp: TextIO, absorb: Optional[bool] = False) -> None: """Parse a HJSON configuration file, typically otp_ctrl_mmap.hjson """ if hjload is None: @@ -52,10 +52,14 @@ def load(self, hfp: TextIO) -> None: self._map = hjload(hfp, object_pairs_hook=dict) if hfp.name and isinstance(hfp.name, str): self._git_version = retrieve_git_version(hfp.name) - otp = self._map['otp'] + try: + otp = self._map['otp'] + except KeyError as exc: + raise ValueError('Unable to find OTP description; ' + 'wrong input file kind?') from exc self._otp_size = int(otp['width']) * int(otp['depth']) self._generate_partitions() - self._compute_locations() + self._compute_locations(absorb) @property def partitions(self) -> dict[str, Any]: @@ -177,7 +181,7 @@ def _check_keymgr_materials(self, partname: str, items: dict[str, dict]) \ enable = any(kms[kind]) return f'{kmprefix}{kind}', enable - def _compute_locations(self) -> None: + def _compute_locations(self, absorb: bool) -> None: """Update partitions with their location within the OTP map.""" absorb_parts = [p for p in self._partitions if getattr(p, 'absorb', False)] @@ -198,7 +202,8 @@ def _compute_locations(self) -> None: extra_blocks -= 1 self._log.info('Partition %s size augmented from %u to %u bytes', part.name, psize, part.size) - part.dispatch_absorb() + if absorb: + part.dispatch_absorb() for part in self._partitions: part_offset = 0 for part in self._partitions: diff --git a/python/qemu/ot/otp/partition.py b/python/qemu/ot/otp/partition.py index 852f139380f6..ac42ad0ac3e9 100644 --- a/python/qemu/ot/otp/partition.py +++ b/python/qemu/ot/otp/partition.py @@ -422,7 +422,7 @@ def build_digest(self, digest_iv: int, digest_constant: int, erase: bool) \ for a, b in zip(self._digest_bytes, bdigest)) def has_field(self, field: str) -> bool: - """Tell whehther the partition has a field by its name. + """Tell whether the partition has a field by its name. :param field: the name of the field to locate :return: true if the field is defined, false otherwise @@ -433,6 +433,36 @@ def has_field(self, field: str) -> bool: except ValueError: return False + def document_fields(self) -> list[str]: + """Return the documentation of each 16-bit word. + + :return: the meaning of each half-words + """ + fields: list[str] = [] + offset = 0 + for itname, itdef in self.items.items(): + itsize = itdef['size'] + if itsize < 2: + if offset & 1: + fields[-1] = f'{fields[-1]}, {itname}' + else: + fields.append(itname) + else: + assert itsize & 1 == 0 + span = itsize // 2 + fields.extend([itname] * span) + offset += itdef['size'] + hsize = len(self._data) + if offset < hsize: + if offset & 1: + offset += 1 + fields.extend([''] * ((hsize - offset) // 2)) + if self.has_digest: + fields.extend([f'{self.name}_DIGEST'] * (self.DIGEST_SIZE // 2)) + if self.is_zeroizable: + fields.extend([f'{self.name}_ZER'] * (self.ZER_SIZE // 2)) + return fields + def _retrieve_properties(self, field: str) -> tuple[int, int]: is_digest = self.has_digest and field.upper() == 'DIGEST' if not is_digest: diff --git a/scripts/opentitan/otptool.py b/scripts/opentitan/otptool.py index ddc7b173bba9..a8c70e32fbbb 100755 --- a/scripts/opentitan/otptool.py +++ b/scripts/opentitan/otptool.py @@ -100,8 +100,10 @@ def main(): help='output filename (default to stdout)') files.add_argument('-r', '--raw', help='QEMU OTP raw image file') - files.add_argument('-x', '--export', + files.add_argument('-x', '--export', metavar='VMEM', help='Export data to a VMEM file') + files.add_argument('-X', '--export-verbose', metavar='VMEM', + help='Export data to a VMEM file with field info') params = argparser.add_argument_group(title='Parameters') # pylint: disable=unsubscriptable-object params.add_argument('-k', '--kind', @@ -126,6 +128,8 @@ def main(): params.add_argument('-f', '--filter', action='append', metavar='PART:FIELD', help='filter which OTP fields are shown') + params.add_argument('--force-absorb', action='store_true', + help='force absorption') params.add_argument('--no-version', action='store_true', help='do not report the OTP image version') commands = argparser.add_argument_group(title='Commands') @@ -233,13 +237,13 @@ def main(): if args.generate in ('PARTS', 'REGS'): argparser.error('Generator requires an OTP map') for feat in ('show', 'digest', 'empty', 'change', 'erase', - 'fix_digest', 'patch_token'): + 'fix_digest', 'patch_token', 'export_verbose'): if not getattr(args, feat): continue argparser.error('Specified option requires an OTP map') else: otpmap = OtpMap() - otpmap.load(args.otp_map) + otpmap.load(args.otp_map, args.force_absorb) if args.lifecycle: lcext = OtpLifecycleExtension() @@ -448,9 +452,13 @@ def main(): if not args.update and check_update: log.warning('OTP content modified, image file not updated') - if args.export: - with open(args.export, 'wt') as xfp: - otp.save_vmem(xfp) + xp_name = args.export or args.export_verbose + if xp_name: + if args.export and args.export_verbose: + argparser.error('export options are mutually exclusive') + with open(xp_name, 'wt') if xp_name != '-' else \ + sys.stdout as xfp: + otp.save_vmem(xfp, bool(args.export_verbose)) except (IOError, ValueError, ImportError) as exc: print(f'\nError: {exc}', file=sys.stderr)