From efb3e3db4f170f286288e700ad4a5163954bce22 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Fri, 13 Sep 2024 18:17:30 +0200 Subject: [PATCH] [ot] scripts/opentitan: cfggen.py: new tool to generate QEMU OT config file This tool may be used to parse an OpenTitan repository and generate a QEMU configuration file that can be used to initialize sensitive data such as the keys, nonce, tokens, etc. Signed-off-by: Emmanuel Blot --- docs/opentitan/cfggen.md | 76 +++++++ docs/opentitan/otcfg.md | 5 + docs/opentitan/tools.md | 2 + scripts/opentitan/cfggen.py | 271 +++++++++++++++++++++++++ scripts/opentitan/ot/otp/const.py | 80 ++++++++ scripts/opentitan/ot/otp/descriptor.py | 4 +- scripts/opentitan/ot/otp/image.py | 2 +- scripts/opentitan/ot/otp/lifecycle.py | 150 ++++++++++++++ scripts/opentitan/ot/otp/map.py | 2 +- scripts/opentitan/ot/otp/partition.py | 109 +--------- scripts/opentitan/ot/util/misc.py | 7 + scripts/opentitan/otptool.py | 2 +- 12 files changed, 602 insertions(+), 108 deletions(-) create mode 100644 docs/opentitan/cfggen.md create mode 100755 scripts/opentitan/cfggen.py create mode 100644 scripts/opentitan/ot/otp/const.py create mode 100644 scripts/opentitan/ot/otp/lifecycle.py diff --git a/docs/opentitan/cfggen.md b/docs/opentitan/cfggen.md new file mode 100644 index 000000000000..cceba072dbaf --- /dev/null +++ b/docs/opentitan/cfggen.md @@ -0,0 +1,76 @@ +# `cfggen.py` + +`cfggen.py` is a helper tool that can generate a QEMU OT configuration file, +for use with QEMU's `-readconfig` option, populated with sensitive data for +the ROM controller(s), the OTP controller, the Life Cycle controller, etc. + +It heurastically parses configuration and generated RTL files to extract from +them the required keys, seeds, nonces and other tokens that are not stored in +the QEMU binary. + +## Usage + +````text +usage: cfggen.py [-h] [-o CFG] [-T TOP] [-c SV] [-l SV] [-t HJSON] [-s SOCID] + [-C COUNT] [-v] [-d] + TOPDIR + +OpenTitan QEMU configuration file generator. + +options: + -h, --help show this help message and exit + +Files: + TOPDIR OpenTitan top directory + -o CFG, --out CFG Filename of the config file to generate + -T TOP, --top TOP OpenTitan Top name (default: darjeeling) + -c SV, --otpconst SV OTP Constant SV file (default: auto) + -l SV, --lifecycle SV + LifeCycle SV file (default: auto) + -t HJSON, --topcfg HJSON + OpenTitan top HJSON config file (default: auto) + +Modifiers: + -s SOCID, --socid SOCID + SoC identifier, if any + -C COUNT, --count COUNT + SoC count (default: 1) + +Extras: + -v, --verbose increase verbosity + -d, --debug enable debug mode +```` + + +### Arguments + +`TOPDIR` is a required positional argument which should point to the top-level directory of the +OpenTitan repository to analyze. It is used to generate the path towards the required files to +parse, each of which can be overidden with options `-c`, `-l` and `-t`. + +* `-C` specify how many SoCs are used on the platform + +* `-c` alternative path to the `otp_ctrl_part_pkg.sv` file + +* `-d` only useful to debug the script, reports any Python traceback to the standard error stream. + +* `-l` alternative path to the `lc_ctrl_state_pkg.sv.sv` file + +* `-o` the filename of the configuration file to generate. It not specified, the generated content + is printed out to the standard output. + +* `-s` specify a SoC identifier for OT platforms with mulitple SoCs + +* `-T` specify the OpenTitan _top_ name, such as `Darjeeling`, `EarlGrey`, ... This option is + case-insensitive. + +* `-t` alternative path to the `top_.gen.hjson` file + +* `-v` can be repeated to increase verbosity of the script, mostly for debug purpose. + + +### Examples + +````sh +./scripts/opentitan/cfggen.py ../opentitan-integrated -o opentitan.cfg +```` diff --git a/docs/opentitan/otcfg.md b/docs/opentitan/otcfg.md index 7379962a5f78..09cda557fc49 100644 --- a/docs/opentitan/otcfg.md +++ b/docs/opentitan/otcfg.md @@ -48,6 +48,11 @@ an_integer = 0x1234 a_boolean = true ``` +## Generation + +It is possible to delegate the generation of an OpenTitan configuration file to the [`cfggen.py`](cfggen.md) +script, using an existing OpenTitan repository. + ## Configurable constants Constants can usually be retrieved from the OpenTitan autogenerated "top" HSJON file, from the diff --git a/docs/opentitan/tools.md b/docs/opentitan/tools.md index e2ac9eb63669..5d6062dce891 100644 --- a/docs/opentitan/tools.md +++ b/docs/opentitan/tools.md @@ -17,6 +17,8 @@ directory to help with these tasks. ## Companion file management +* [`cfggen.py`](cfggen.md) can be used to generate an OpenTitan [configuration file](otcfg.md) from + an existing OpenTitan repository. * [`otpdm.py`](otpdm.md) can be used to access the OTP Controller over a JTAG/DTM/DM link. It reads out partition's item values and can update those items. * [`otptool.py`](otptool.md) can be used to generate an OTP image from a OTP VMEM file and can be diff --git a/scripts/opentitan/cfggen.py b/scripts/opentitan/cfggen.py new file mode 100755 index 000000000000..9f00e18df42f --- /dev/null +++ b/scripts/opentitan/cfggen.py @@ -0,0 +1,271 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2024 Rivos, Inc. +# SPDX-License-Identifier: Apache2 + +"""OpenTitan QEMU configuration file generator. + + :author: Emmanuel Blot +""" + +from argparse import ArgumentParser +from configparser import ConfigParser +from logging import getLogger +from os.path import isdir, isfile, join as joinpath, normpath +from re import match, search +from sys import exit as sysexit, modules, stderr +from traceback import format_exc +from typing import Optional + +try: + _HJSON_ERROR = None + from hjson import load as hjload +except ImportError as hjson_exc: + _HJSON_ERROR = str(hjson_exc) + +from ot.util.log import configure_loggers +from ot.util.misc import camel_to_snake_case +from ot.otp.const import OtpConstants +from ot.otp.lifecycle import OtpLifecycle + + +OtParamRegex = str +"""Definition of a parameter to seek and how to shorten it.""" + + +class OtConfiguration: + """QEMU configuration file generator.""" + + def __init__(self): + self._log = getLogger('cfggen.cfg') + self._lc_states: tuple[str, str] = ('', '') + self._lc_transitions: tuple[str, str] = ('', '') + self._roms: dict[Optional[int], dict[str, str]] = {} + self._otp: dict[str, str] = {} + self._lc: dict[str, str] = {} + + def load_top_config(self, toppath: str) -> None: + """Load data from HJSON top configuration file.""" + with open(toppath, 'rt') as tfp: + cfg = hjload(tfp) + for module in cfg.get('module') or []: + modtype = module.get('type') + if modtype == 'rom_ctrl': + self._load_top_values(module, self._roms, True, + r'RndCnstScr(.*)') + continue + if modtype == 'otp_ctrl': + self._load_top_values(module, self._otp, False, + r'RndCnst(.*)Init') + continue + + def load_lifecycle(self, lcpath: str) -> None: + """Load LifeCycle data from RTL file.""" + lcext = OtpLifecycle() + with open(lcpath, 'rt') as lfp: + lcext.load(lfp) + states = lcext.get_configuration('LC_STATE') + if not states: + raise ValueError('Cannot obtain LifeCycle states') + for raw in {s for s in states if int(s, 16) == 0}: + del states[raw] + ostates = list(states) + self._lc_states = ostates[0], ostates[-1] + self._log.info("States first: '%s', last '%s'", + states[self._lc_states[0]], states[self._lc_states[1]]) + trans = lcext.get_configuration('LC_TRANSITION_CNT') + if not trans: + raise ValueError('Cannot obtain LifeCycle transitions') + for raw in {s for s in trans if int(s, 16) == 0}: + del trans[raw] + otrans = list(trans) + self._lc_transitions = otrans[0], otrans[-1] + self._log.info('Transitions first : %d, last %d', + int(trans[self._lc_transitions[0]]), + int(trans[self._lc_transitions[1]])) + self._lc.update(lcext.get_tokens(False, False)) + + def load_otp_constants(self, otppath: str) -> None: + """Load OTP data from RTL file.""" + otpconst = OtpConstants() + with open(otppath, 'rt') as cfp: + otpconst.load(cfp) + self._otp.update(otpconst.get_digest_pair('cnsty_digest', 'digest')) + self._otp.update(otpconst.get_digest_pair('sram_data_key', 'sram')) + + def save(self, socid: Optional[str] = None, count: Optional[int] = 1, + outpath: Optional[str] = None) \ + -> None: + """Save QEMU configuration file using a INI-like file format, + compatible with the `-readconfig` option of QEMU. + """ + cfg = ConfigParser() + self._generate_roms(cfg, socid, count) + self._generate_otp(cfg, socid) + self._generate_life_cycle(cfg, socid) + if outpath: + with open(outpath, 'wt') as ofp: + cfg.write(ofp) + else: + cfg.write(stderr) + + @classmethod + def add_pair(cls, data: dict[str, str], kname: str, value: str) -> None: + """Helper to create key, value pair entries.""" + data[f' {kname}'] = f'"{value}"' + + def _load_top_values(self, module: dict, odict: dict, multi: bool, + *regexes: list[OtParamRegex]) -> None: + modname = module.get('name') + if not modname: + return + for params in module.get('param_list', []): + if not isinstance(params, dict): + continue + for regex in regexes: # TODO: camelcase to lower snake case + pmo = match(regex, params['name']) + if not pmo: + continue + value = params.get('default') + if not value: + continue + if value.startswith('0x'): + value = value[2:] + kname = camel_to_snake_case(pmo.group(1)) + if multi: + imo = search(r'(\d+)$', modname) + idx = int(imo.group(1)) if imo else 'None' + if idx not in odict: + odict[idx] = {} + odict[idx][kname] = value + else: + odict[kname] = value + + def _generate_roms(self, cfg: ConfigParser, socid: Optional[str] = None, + count: int = 1) -> None: + for cnt in range(count): + for rom, data in self._roms.items(): + nameargs = ['ot-rom_ctrl'] + if socid: + if count > 1: + nameargs.append(f'{socid}{cnt}') + else: + nameargs.append(socid) + if rom is not None: + nameargs.append(f'rom{rom}') + romname = '.'.join(nameargs) + romdata = {} + for kname, val in data.items(): + self.add_pair(romdata, kname, val) + cfg[f'ot_device "{romname}"'] = romdata + + def _generate_otp(self, cfg: ConfigParser, socid: Optional[str] = None) \ + -> None: + nameargs = ['ot-otp-dj'] + if socid: + nameargs.append(socid) + otpname = '.'.join(nameargs) + otpdata = {} + self.add_pair(otpdata, 'lc_state_first', self._lc_states[0]) + self.add_pair(otpdata, 'lc_state_last', self._lc_states[-1]) + self.add_pair(otpdata, 'lc_trscnt_first', self._lc_transitions[0]) + self.add_pair(otpdata, 'lc_trscnt_last', self._lc_transitions[-1]) + for kname, val in self._otp.items(): + self.add_pair(otpdata, kname, val) + cfg[f'ot_device "{otpname}"'] = otpdata + + def _generate_life_cycle(self, cfg: ConfigParser, + socid: Optional[str] = None) -> None: + nameargs = ['ot-lc_ctrl'] + if socid: + nameargs.append(socid) + lcname = '.'.join(nameargs) + lcdata = {} + for kname, value in self._lc.items(): + self.add_pair(lcdata, kname, value) + cfg[f'ot_device "{lcname}"'] = lcdata + + +def main(): + """Main routine""" + debug = True + default_top = 'darjeeling' + try: + desc = modules[__name__].__doc__.split('.', 1)[0].strip() + argparser = ArgumentParser(description=f'{desc}.') + files = argparser.add_argument_group(title='Files') + files.add_argument('opentitan', nargs=1, metavar='TOPDIR', + help='OpenTitan top directory') + files.add_argument('-o', '--out', metavar='CFG', + help='Filename of the config file to generate') + files.add_argument('-T', '--top', default=default_top, + help=f'OpenTitan Top name (default: {default_top})') + files.add_argument('-c', '--otpconst', metavar='SV', + help='OTP Constant SV file (default: auto)') + files.add_argument('-l', '--lifecycle', metavar='SV', + help='LifeCycle SV file (default: auto)') + files.add_argument('-t', '--topcfg', metavar='HJSON', + help='OpenTitan top HJSON config file ' + '(default: auto)') + mods = argparser.add_argument_group(title='Modifiers') + mods.add_argument('-s', '--socid', + help='SoC identifier, if any') + mods.add_argument('-C', '--count', default=1, type=int, + help='SoC count (default: 1)') + extra = argparser.add_argument_group(title='Extras') + extra.add_argument('-v', '--verbose', action='count', + help='increase verbosity') + extra.add_argument('-d', '--debug', action='store_true', + help='enable debug mode') + args = argparser.parse_args() + debug = args.debug + + configure_loggers(args.verbose, 'cfggen', 'otp') + + if _HJSON_ERROR: + argparser.error('Missing HSJON module: {_HJSON_ERROR}') + + topdir = args.opentitan[0] + if not isdir(topdir): + argparser.error('Invalid OpenTitan top directory') + ot_dir = normpath(topdir) + top = f'top_{args.top.lower()}' + + if not args.topcfg: + cfgpath = joinpath(ot_dir, f'hw/{top}/data/autogen/{top}.gen.hjson') + else: + cfgpath = args.topcfg + if not isfile(cfgpath): + argparser.error(f"No such file '{cfgpath}'") + + if not args.lifecycle: + lcpath = joinpath(ot_dir, 'hw/ip/lc_ctrl/rtl/lc_ctrl_state_pkg.sv') + else: + lcpath = args.lifecycle + if not isfile(lcpath): + argparser.error(f"No such file '{lcpath}'") + + if not args.otpconst: + ocpath = joinpath(ot_dir, 'hw/ip/otp_ctrl/rtl/otp_ctrl_part_pkg.sv') + else: + ocpath = args.otpconst + if not isfile(lcpath): + argparser.error(f"No such file '{ocpath}'") + + cfg = OtConfiguration() + cfg.load_top_config(cfgpath) + cfg.load_lifecycle(lcpath) + cfg.load_otp_constants(ocpath) + cfg.save(args.socid, args.count, args.out) + + except (IOError, ValueError, ImportError) as exc: + print(f'\nError: {exc}', file=stderr) + if debug: + print(format_exc(chain=False), file=stderr) + sysexit(1) + except KeyboardInterrupt: + sysexit(2) + + +if __name__ == '__main__': + main() diff --git a/scripts/opentitan/ot/otp/const.py b/scripts/opentitan/ot/otp/const.py new file mode 100644 index 000000000000..705ef81f63ab --- /dev/null +++ b/scripts/opentitan/ot/otp/const.py @@ -0,0 +1,80 @@ +# Copyright (c) 2023-2024 Rivos, Inc. +# SPDX-License-Identifier: Apache2 + +"""Lifecycle helpers. + + :author: Emmanuel Blot +""" + +from logging import getLogger +from re import finditer +from typing import TextIO + +from ot.util.misc import camel_to_snake_case + + +class OtpConstants: + """OTP constant manager. + """ + + def __init__(self): + self._log = getLogger('otp.const') + self._consts: dict[str, list[str]] = {} + self._enums: dict[str, dict[str, int]] = {} + + def load(self, svp: TextIO): + """Decode OTP information. + + :param svp: System Verilog stream with OTP definitions. + """ + svdata = svp.read() + for smo in finditer(r"\stypedef\s+enum\s+logic\s+\[[^]]+\]\s" + r"{((?:\s+\w+,?)+)\s*}\s(\w+)_sel_e;", svdata): + values, name = smo.groups() + if name in self._consts: + raise ValueError(f'Multiple definitions of enumeration {name}') + enums = self._enums[name] = {} + for emo in finditer(r"\s+(\w+),?", values): + vname = camel_to_snake_case(emo.group(1)) + enums[vname] = len(enums) + + for amo in finditer(r"\s+parameter\s+(\w+)_array_t\s+(\w+)\s+=\s+" + r"{(\s+(?:(?:64|128)'h[0-9A-F]+,?\s+)+)};", + svdata): + _type, name, values = amo.groups() + sc_name = camel_to_snake_case(name) + sc_parts = sc_name.split('_') + if sc_parts[0] == 'rnd': + sc_parts.pop(0) + if sc_parts[0] == 'cnst': + sc_parts.pop(0) + name = '_'.join(sc_parts) + if name in self._consts: + raise ValueError(f'Multiple definitions of constant {name}') + consts = self._consts[name] = [] + for cmo in finditer(r"(64|128)'h([0-9A-F]+),?", values): + consts.append(cmo.group(2).lower()) + # RTL order in array is reversed + consts.reverse() + + def get_enums(self) -> list[str]: + """Return a list of parsed enumerations.""" + return list(self._enums.keys()) + + def get_digest_pair(self, name: str, prefix: str) -> dict[str, str]: + """Return a dict of digest pair. + :param name: one of the enumerated values, see #get_enums + :param prefix: the prefix to add to each dict key. + """ + try: + idx = self._enums['digest'][name] + except KeyError as exc: + raise ValueError(f'Unknown digest pair {name}') from exc + odict = {} + for kname, values in self._consts.items(): + if kname.startswith('digest_'): + if len(values) < idx: + raise ValueError(f'No such digest {name}') + oname = f"{prefix}_{kname.split('_', 1)[-1]}" + odict[oname] = values[idx] + return odict diff --git a/scripts/opentitan/ot/otp/descriptor.py b/scripts/opentitan/ot/otp/descriptor.py index a4281d6aef7b..ab6aa724d82a 100644 --- a/scripts/opentitan/ot/otp/descriptor.py +++ b/scripts/opentitan/ot/otp/descriptor.py @@ -34,7 +34,7 @@ class OTPPartitionDesc: } def __init__(self, otpmap: 'OtpMap'): - self._log = getLogger('otptool.partdesc') + self._log = getLogger('otp.partdesc') self._otpmap = otpmap def save(self, hjname: str, scriptname: str, cfp: TextIO) -> None: @@ -115,7 +115,7 @@ class OTPRegisterDef: """OTP Partition register generator.""" def __init__(self, otpmap: 'OtpMap'): - self._log = getLogger('otptool.reg') + self._log = getLogger('otp.reg') self._otpmap = otpmap def save(self, hjname: str, scriptname: str, cfp: TextIO) -> None: diff --git a/scripts/opentitan/ot/otp/image.py b/scripts/opentitan/ot/otp/image.py index 360ffc9aef73..1743da4a1ba8 100644 --- a/scripts/opentitan/ot/otp/image.py +++ b/scripts/opentitan/ot/otp/image.py @@ -53,7 +53,7 @@ class OtpImage: ) def __init__(self, ecc_bits: Optional[int] = None): - self._log = getLogger('otptool.img') + self._log = getLogger('otp.img') self._header: dict[str, Any] = {} self._magic = b'' self._data = bytearray() diff --git a/scripts/opentitan/ot/otp/lifecycle.py b/scripts/opentitan/ot/otp/lifecycle.py new file mode 100644 index 000000000000..9456ee3259bc --- /dev/null +++ b/scripts/opentitan/ot/otp/lifecycle.py @@ -0,0 +1,150 @@ +# Copyright (c) 2023-2024 Rivos, Inc. +# SPDX-License-Identifier: Apache2 + +"""Lifecycle helpers. + + :author: Emmanuel Blot +""" + +from binascii import unhexlify +from io import StringIO +from logging import getLogger +from os.path import basename +from re import finditer, match +from textwrap import fill +from typing import TextIO + +from ot.util.misc import camel_to_snake_case + + +class OtpLifecycle: + """Decoder for Lifecyle bytes sequences. + """ + + EXTRA_SLOTS = { + 'lc_state': { + 'post_transition': None, + 'escalate': None, + 'invalid': None, + } + } + + def __init__(self): + self._log = getLogger('otp.lc') + self._tables: dict[str, dict[str, str]] = {} + self._tokens: dict[str, str] = {} + + def load(self, svp: TextIO): + """Decode LifeCycle information. + + :param svp: System Verilog stream with OTP definitions. + """ + ab_re = (r"\s*parameter\s+logic\s+\[\d+:\d+\]\s+" + r"([ABCD]\d+|ZRO)\s+=\s+\d+'(b(?:[01]+)|h(?:[0-9a-fA-F]+));") + tbl_re = r"\s*Lc(St|Cnt)(\w+)\s+=\s+\{([^\}]+)\}\s*,?" + codes: dict[str, int] = {} + sequences: dict[str, list[str]] = {} + svp = StringIO(svp.read()) + for line in svp: + cmt = line.find('//') + if cmt >= 0: + line = line[:cmt] + line = line.strip() + abmo = match(ab_re, line) + if not sequences and abmo: + name = abmo.group(1) + sval = abmo.group(2) + val = int(sval[1:], 2 if sval.startswith('b') else 16) + if name in codes: + self._log.error('Redefinition of %s', name) + continue + codes[name] = val + continue + smo = match(tbl_re, line) + if smo: + kind = smo.group(1).lower() + name = smo.group(2) + seq = smo.group(3) + items = [x.strip() for x in seq.split(',')] + inv = [it for it in items if it not in codes] + if inv: + self._log.error('Unknown state seq: %s', ', '.join(inv)) + if kind not in sequences: + sequences[kind] = {} + sequences[kind][name] = items + continue + svp.seek(0) + for tmo in finditer(r"\s+parameter\s+lc_token_t\s+(\w+)\s+=" + r"\s+\{\s+128'h([0-9A-F]+)\s+\};", + svp.getvalue()): + token, value = tmo.group(1), tmo.group(2) + if token in self._tokens: + raise ValueError(f'Multiple definitions of token {token}') + self._tokens[token] = value.lower() + for kind, seqs in sequences.items(): + mkind, conv = {'st': ('LC_STATE', str), + 'cnt': ('LC_TRANSITION_CNT', int)}[kind] + self._tables[mkind] = {} + for ref, seq in seqs.items(): + seq = ''.join((f'{x:04x}'for x in map(codes.get, seq))) + self._tables[mkind][seq] = conv(ref) + + def save(self, cfp: TextIO): + """Save OTP life cycle definitions as a C file. + + :param cfp: output text stream + """ + print(f'/* Section auto-generated with {basename(__file__)} ' + f'script */', file=cfp) + for kind, table in self._tables.items(): + enum_io = StringIO() + array_io = StringIO() + count = len(table) + length = max(len(x) for x in table.keys())//2 + print(f'static const char {kind.lower()}s[{count}u][{length}u]' + f' = {{', file=array_io) + pad = ' ' * 8 + for seq, ref in table.items(): + if isinstance(ref, str): + slot = f'{kind}_{ref}'.upper() + print(f' {slot},', file=enum_io) + else: + slot = f'{ref}u' + seqstr = ', '.join((f'0x{b:02x}u' for b in + reversed(unhexlify(seq)))) + defstr = fill(seqstr, width=80, initial_indent=pad, + subsequent_indent=pad) + print(f' [{slot}] = {{\n{defstr}\n }},', + file=array_io) + print('};', file=array_io) + for extra in self.EXTRA_SLOTS.get(kind.lower(), {}): + slot = f'{kind}_{extra}'.upper() + print(f' {slot},', file=enum_io) + enum_str = enum_io.getvalue() + if enum_str: + # likely to be moved to a header file + print(f'enum {kind.lower()} {{\n{enum_str}}};\n', file=cfp) + print(f'{array_io.getvalue()}', file=cfp) + print('/* End of auto-generated section */', file=cfp) + + def get_configuration(self, name: str) -> dict[str, str]: + """Provide a dictionary of configurable elements for QEMU.""" + return self._tables.get(name, {}) + + def get_tokens(self, hashed: bool, zero: bool) -> dict[str, str]: + """Return a dictionary of parsed tokens.""" + tokens = {} + for token, value in self._tokens.items(): + sltoken = camel_to_snake_case(token) + token_parts = sltoken.split('_') + if token_parts[0] == 'rnd': + token_parts.pop(0) + if token_parts[0] == 'cnst': + token_parts.pop(0) + if not hashed and token_parts[-1] == 'hashed': + continue + if not zero and token_parts[-2] == 'zero': + continue + tkname = '_'.join(token_parts) + tokens[tkname] = value + return tokens diff --git a/scripts/opentitan/ot/otp/map.py b/scripts/opentitan/ot/otp/map.py index cb0e54df1d72..fe9d513e0556 100644 --- a/scripts/opentitan/ot/otp/map.py +++ b/scripts/opentitan/ot/otp/map.py @@ -38,7 +38,7 @@ class OtpMap: } def __init__(self): - self._log = getLogger('otptool.map') + self._log = getLogger('otp.map') self._map: dict = {} self._otp_size = 0 self._partitions: list[OtpPartition] = [] diff --git a/scripts/opentitan/ot/otp/partition.py b/scripts/opentitan/ot/otp/partition.py index 55e177db2789..7d1db8e41e35 100644 --- a/scripts/opentitan/ot/otp/partition.py +++ b/scripts/opentitan/ot/otp/partition.py @@ -6,14 +6,13 @@ :author: Emmanuel Blot """ -from binascii import hexlify, unhexlify -from io import BytesIO, StringIO +from binascii import hexlify +from io import BytesIO from logging import getLogger -from os.path import basename -from re import match as re_match -from textwrap import fill from typing import BinaryIO, Optional, TextIO +from .lifecycle import OtpLifecycle + try: # try to load Present if available from present import Present @@ -45,7 +44,7 @@ class OtpPartition: def __init__(self, params): self.__dict__.update(params) self._decoder = None - self._log = getLogger('otptool.part') + self._log = getLogger('otp.part') self._data = b'' self._digest_bytes: Optional[bytes] = None @@ -181,109 +180,13 @@ def emit(fmt, *args): hexlify(self._digest_bytes).decode()) -class OtpLifecycleExtension(OtpPartitionDecoder): +class OtpLifecycleExtension(OtpLifecycle, OtpPartitionDecoder): """Decoder for Lifecyle bytes sequences. """ - EXTRA_SLOTS = { - 'lc_state': { - 'post_transition': None, - 'escalate': None, - 'invalid': None, - } - } - - def __init__(self): - self._log = getLogger('otptool.lc') - self._tables: dict[str, dict[str, str]] = {} - def decode(self, category: str, seq: str) -> Optional[str | int]: return self._tables.get(category, {}).get(seq, None) - def load(self, svp: TextIO): - """Decode LifeCycle information. - - :param svp: System Verilog stream with OTP definitions. - """ - ab_re = (r"\s*parameter\s+logic\s+\[\d+:\d+\]\s+" - r"([ABCD]\d+|ZRO)\s+=\s+\d+'(b(?:[01]+)|h(?:[0-9a-fA-F]+));") - tbl_re = r"\s*Lc(St|Cnt)(\w+)\s+=\s+\{([^\}]+)\}\s*,?" - codes: dict[str, int] = {} - sequences: dict[str, list[str]] = {} - for line in svp: - cmt = line.find('//') - if cmt >= 0: - line = line[:cmt] - line = line.strip() - abmo = re_match(ab_re, line) - if not sequences and abmo: - name = abmo.group(1) - sval = abmo.group(2) - val = int(sval[1:], 2 if sval.startswith('b') else 16) - if name in codes: - self._log.error('Redefinition of %s', name) - continue - codes[name] = val - continue - smo = re_match(tbl_re, line) - if smo: - kind = smo.group(1).lower() - name = smo.group(2) - seq = smo.group(3) - items = [x.strip() for x in seq.split(',')] - inv = [it for it in items if it not in codes] - if inv: - self._log.error('Unknown state seq: %s', ', '.join(inv)) - if kind not in sequences: - sequences[kind] = {} - sequences[kind][name] = items - continue - for kind, seqs in sequences.items(): - mkind, conv = {'st': ('LC_STATE', str), - 'cnt': ('LC_TRANSITION_CNT', int)}[kind] - self._tables[mkind] = {} - for ref, seq in seqs.items(): - seq = ''.join((f'{x:04x}'for x in map(codes.get, seq))) - self._tables[mkind][seq] = conv(ref) - - def save(self, cfp: TextIO): - """Save OTP life cycle definitions as a C file. - - :param cfp: output text stream - """ - print(f'/* Section auto-generated with {basename(__file__)} ' - f'script */', file=cfp) - for kind, table in self._tables.items(): - enum_io = StringIO() - array_io = StringIO() - count = len(table) - length = max(len(x) for x in table.keys())//2 - print(f'static const char {kind.lower()}s[{count}u][{length}u]' - f' = {{', file=array_io) - pad = ' ' * 8 - for seq, ref in table.items(): - if isinstance(ref, str): - slot = f'{kind}_{ref}'.upper() - print(f' {slot},', file=enum_io) - else: - slot = f'{ref}u' - seqstr = ', '.join((f'0x{b:02x}u' for b in - reversed(unhexlify(seq)))) - defstr = fill(seqstr, width=80, initial_indent=pad, - subsequent_indent=pad) - print(f' [{slot}] = {{\n{defstr}\n }},', - file=array_io) - print('};', file=array_io) - for extra in self.EXTRA_SLOTS.get(kind.lower(), {}): - slot = f'{kind}_{extra}'.upper() - print(f' {slot},', file=enum_io) - enum_str = enum_io.getvalue() - if enum_str: - # likely to be moved to a header file - print(f'enum {kind.lower()} {{\n{enum_str}}};\n', file=cfp) - print(f'{array_io.getvalue()}', file=cfp) - print('/* End of auto-generated section */', file=cfp) - # imported here to avoid Python circular dependency issue # pylint: disable=cyclic-import diff --git a/scripts/opentitan/ot/util/misc.py b/scripts/opentitan/ot/util/misc.py index 4dac0feb9469..a60de0a7a038 100644 --- a/scripts/opentitan/ot/util/misc.py +++ b/scripts/opentitan/ot/util/misc.py @@ -7,6 +7,7 @@ """ from io import BytesIO +from re import sub from sys import stdout from typing import Any, Iterable, Optional, TextIO @@ -101,3 +102,9 @@ def dump_buffer(buffer: Buffer, addr: int = 0, file: Optional[TextIO] = None) \ def round_up(value: int, rnd: int) -> int: """Round up a integer value.""" return (value + rnd - 1) & -rnd + + +def camel_to_snake_case(camel: str) -> str: + """Convert CamelString string into snake_case lower string.""" + pattern = r'(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])' + return sub(pattern, '_', camel).lower() diff --git a/scripts/opentitan/otptool.py b/scripts/opentitan/otptool.py index a246b03fc079..913819d87080 100755 --- a/scripts/opentitan/otptool.py +++ b/scripts/opentitan/otptool.py @@ -94,7 +94,7 @@ def main(): args = argparser.parse_args() debug = args.debug - configure_loggers(args.verbose, 'otptool') + configure_loggers(args.verbose, 'otptool', 'otp') otp = OtpImage(args.ecc)