Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 17 additions & 6 deletions docs/opentitan/otptool.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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}
Expand All @@ -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:
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
42 changes: 38 additions & 4 deletions python/qemu/ot/otp/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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('<H', self._data)
if self._header_comments:
if verbose:
# for some reason original format starts with an empty comment
print('//', file=vfp)
print('\n'.join(self._header_comments), file=vfp)
for addr, (dword, ecc) in enumerate(zip(dsrc, self._ecc)):
print(f'@{addr:06x} {ecc:02x}{dword[0]:04x}', file=vfp)
if verbose:
fields = self.document_partitions()
for addr, (dword, ecc, doc) in enumerate(
zip(dsrc, self._ecc, fields)):
print(f'@{addr:06x} {ecc:02x}{dword[0]:04x} // {doc}', file=vfp)
else:
for addr, (dword, ecc) in enumerate(zip(dsrc, self._ecc)):
print(f'@{addr:06x} {ecc:02x}{dword[0]:04x}', file=vfp)

def load_lifecycle(self, lcext: OtpLifecycleExtension) -> None:
"""Load lifecyle values."""
Expand Down Expand Up @@ -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())
Expand Down
15 changes: 10 additions & 5 deletions python/qemu/ot/otp/map.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,18 +44,22 @@ 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:
raise ImportError('HJSON module is required')
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]:
Expand Down Expand Up @@ -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)]
Expand All @@ -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:
Expand Down
32 changes: 31 additions & 1 deletion python/qemu/ot/otp/partition.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down
20 changes: 14 additions & 6 deletions scripts/opentitan/otptool.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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')
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand Down
Loading