From 1f8c6d0f575ad251fdc7e858c510ee589a0323a8 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Thu, 23 Oct 2025 10:29:24 +0100 Subject: [PATCH 1/2] [ot] scripts/opentitan: otptool.py: add an option to document fields in VMEM file Signed-off-by: Emmanuel Blot --- docs/opentitan/otptool.md | 18 +++++++++----- python/qemu/ot/otp/image.py | 42 +++++++++++++++++++++++++++++---- python/qemu/ot/otp/map.py | 6 ++++- python/qemu/ot/otp/partition.py | 32 ++++++++++++++++++++++++- scripts/opentitan/otptool.py | 16 +++++++++---- 5 files changed, 97 insertions(+), 17 deletions(-) diff --git a/docs/opentitan/otptool.md b/docs/opentitan/otptool.md index 002c1a35faad..a863b9e3c28c 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] + [--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} @@ -83,7 +86,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 +183,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. 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..eb3f932cde31 100644 --- a/python/qemu/ot/otp/map.py +++ b/python/qemu/ot/otp/map.py @@ -52,7 +52,11 @@ 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() 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..190572744d1b 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', @@ -233,7 +235,7 @@ 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') @@ -448,9 +450,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) From 0652ce01b1a107ebd77d139ea9a6ba9d70a94418 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Mon, 27 Oct 2025 13:38:21 +0100 Subject: [PATCH 2/2] [ot] scripts/opentitan: otptool.py: add an option to force absorption Input HJSON file already contains defined absorbed free space allocation, so this feature should no longer be useful. Leave an option to force dispatching of free space; this option may be deprecated in a future version. Signed-off-by: Emmanuel Blot --- docs/opentitan/otptool.md | 7 ++++++- python/qemu/ot/otp/map.py | 9 +++++---- scripts/opentitan/otptool.py | 4 +++- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/docs/opentitan/otptool.md b/docs/opentitan/otptool.md index a863b9e3c28c..c0ef158e2a7b 100644 --- a/docs/opentitan/otptool.md +++ b/docs/opentitan/otptool.md @@ -9,7 +9,7 @@ controller virtual device. usage: otptool.py [-h] [-j HJSON] [-m VMEM] [-l SV] [-o FILE] [-r RAW] [-x VMEM] [-X VMEM] [-k {auto,otp,fuz}] [-e BITS] [-C CONFIG] [-c INT] [-i INT] [-w] [-n] [-f PART:FIELD] - [--no-version] [-s] [-E] [-D] [-U] + [--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] @@ -43,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: @@ -215,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/map.py b/python/qemu/ot/otp/map.py index eb3f932cde31..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: @@ -59,7 +59,7 @@ def load(self, hfp: TextIO) -> None: '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]: @@ -181,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)] @@ -202,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/scripts/opentitan/otptool.py b/scripts/opentitan/otptool.py index 190572744d1b..a8c70e32fbbb 100755 --- a/scripts/opentitan/otptool.py +++ b/scripts/opentitan/otptool.py @@ -128,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') @@ -241,7 +243,7 @@ def main(): 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()