blob: e33fe2f9cbc2e696401609193148e7458621b91f [file]
#!/usr/bin/env python3
# Licensed under the Apache-2.0 license
# SPDX-License-Identifier: Apache-2.0
"""OpenTitan QEMU configuration dumper to JSON.
Based on cfggen.py from lowRISC QEMU.
"""
from argparse import ArgumentParser
from configparser import ConfigParser
from logging import getLogger
from os.path import (
abspath,
basename,
dirname,
isdir,
isfile,
join as joinpath,
normpath,
)
from traceback import format_exc
from typing import NamedTuple, Optional, TextIO
import sys
import json
import os
# Robust runfiles path resolution for Bazel
runfiles_dir = os.environ.get("RUNFILES_DIR")
if runfiles_dir:
found = False
for root, dirs, files in os.walk(runfiles_dir):
if root.endswith(os.path.join("python", "qemu")) and "ot" in dirs:
sys.path.append(root)
found = True
break
if not found:
print("Warning: Could not find qemu python path in runfiles", file=sys.stderr)
else:
# Fallback
QEMU_PYPATH = joinpath(
dirname(dirname(dirname(normpath(__file__)))), "python", "qemu"
)
sys.path.append(QEMU_PYPATH)
try:
_HJSON_ERROR = None
from hjson import load as hjload
except ImportError as hjson_exc:
_HJSON_ERROR = str(hjson_exc)
def hjload(*_, **__): # noqa: E301
"""dummy func if HJSON module is not available"""
return {}
from ot.lc_ctrl.const import LcCtrlConstants
from ot.otp.const import OtpConstants
from ot.otp.secret import OtpSecretConstants
from ot.top import OpenTitanTop
from ot.util.arg import ArgError
from ot.util.log import configure_loggers
from ot.util.misc import alphanum_key, retrieve_git_version, to_bool
OtParamRegex = str
"""Definition of a parameter to seek and how to shorten it."""
class OtClock(NamedTuple):
"""Clock definition."""
name: str
"""Clock signal name."""
frequency: int
"""Clock frequency in Hz."""
aon: bool
"""Whether the clock is always on."""
ref: bool
"""Whether the clock is a reference clock."""
class OtDerivedClock(NamedTuple):
"""Clock derived from a top level clock definition."""
name: str
"""Clock signal name."""
source: str
"""Clock source signal name."""
div: int
"""Divider."""
class OtClockGroup(NamedTuple):
"""Clock logicial group definition."""
name: str
"""Group name."""
sources: list[str]
"""Clock source signal names."""
sw_cg: bool
"""Whether clock group can be managed by SW."""
hint: bool
"""Whether clock group can be hinted by SW."""
class OtConfiguration:
"""QEMU configuration file generator."""
MODULES = {
"rom_ctrl": (True, r"RndCnstScr(.*)"),
"otp_ctrl": (False, r"RndCnst(.*)Init"),
"lc_ctrl": (False, r"RndCnstLcKeymgrDiv(.*)"),
"keymgr": (
False,
r"RndCnst((?:.*)Seed)",
# The CDI keymgr seed does not match 'RndCnst.*Seed'
r"RndCnst(Cdi)",
),
"keymgr_dpe": (False, r"RndCnst((?:.*)Seed)"),
}
TRANSLATIONS = {"keymgr": {"cdi": "cdi_seed"}}
def __init__(self):
self._log = getLogger("cfggen.cfg")
self._otpconst = OtpConstants()
self._lcconst = LcCtrlConstants()
self._constants: dict[str, dict[Optional[int], dict[str, str]]] = {}
self._top_clocks: dict[str, OtClock] = {}
self._sub_clocks: dict[str, OtDerivedClock] = {}
self._clock_groups: dict[str, OtClockGroup] = {}
self._mod_clocks: dict[str, list[str]] = {}
self._top_name: Optional[str] = None
self._git_version: Optional[str] = None
self._exclusions: dict[str, set[str]] = {}
@property
def top_name(self) -> Optional[str]:
"""Return the name of the top as defined in a configuration file."""
return self._top_name
def load_config(self, toppath: str) -> None:
"""Load data from HJSON configuration file."""
assert not _HJSON_ERROR
with open(toppath, "rt") as tfp:
cfg = hjload(tfp, object_pairs_hook=dict)
self._top_name = cfg.get("name")
topbase = basename(toppath)
self._git_version = retrieve_git_version(toppath)
for module in cfg.get("module") or []:
modtype = module.get("type")
moddefs = self.MODULES.get(modtype)
if not moddefs:
continue
multi, regexes = moddefs[0], moddefs[1:]
consts = {}
OtpSecretConstants.load_values(module, consts, multi, *regexes)
if not consts:
continue
for cname, tname in self.TRANSLATIONS.get(modtype, {}).items():
if cname in consts:
consts[tname] = consts.pop(cname)
self._log.debug("Constants for %s loaded from %s", modtype, topbase)
exist = modtype in self._constants
if multi:
if not exist:
self._constants[modtype] = consts
else:
self._constants[modtype].update(consts)
else:
if exist:
raise ValueError(f"Redefinition of {modtype}")
self._constants[modtype] = {None: consts}
clocks = cfg.get("clocks", {})
for clock in clocks.get("srcs", []):
name = clock["name"]
aon = to_bool(clock["aon"], False)
ref = to_bool(clock["ref"], False)
freq = int(clock["freq"])
self._top_clocks[name] = OtClock(name, freq, aon, ref)
for clock in clocks.get("derived_srcs", []):
name = clock["name"]
src = clock["src"]
aon = to_bool(clock["aon"], False)
freq = int(clock["freq"])
div = int(clock["div"])
src_clock = self._top_clocks.get(src)
if not src_clock:
raise ValueError(f"Invalid top clock {src} " f"referenced from {name}")
if src_clock.frequency // div != freq:
raise ValueError(
f"Incoherent derived clock {name} frequency: "
f"{src_clock.frequency}/{div} != {freq}"
)
if aon and not src_clock.aon:
raise ValueError(f"Incoherent derived clock {name} AON")
self._sub_clocks[name] = OtDerivedClock(name, src, div)
clock_names = set(self._top_clocks.keys())
clock_names.update(set(self._sub_clocks.keys()))
for group in clocks.get("groups", []):
ext = group["src"] == "ext"
if ext:
continue
name = group["name"]
hint = group["sw_cg"] == "hint"
sw_cg = not hint and to_bool(group["sw_cg"], False)
clk_srcs = []
for clk_name, clk_src in group.get("clocks", {}).items():
if not hint:
exp_name = f"clk_{clk_src}_{name}"
if clk_name != exp_name:
raise ValueError(
f"Unexpected clock {clk_name} in group"
f" {name} (exp: {exp_name})"
)
clk_srcs.append(clk_src)
else:
exp_prefix = f"clk_{clk_src}_"
if not clk_name.startswith(exp_prefix):
raise ValueError(
f"Unexpected clock {clk_name} in group" f" {name}"
)
src_name = clk_name.removeprefix(exp_prefix)
clk_srcs.append(src_name)
if src_name in self._sub_clocks:
raise ValueError(f"Refinition of clock {src_name}")
self._sub_clocks[src_name] = OtDerivedClock(src_name, clk_src, 1)
self._clock_groups[name] = OtClockGroup(name, clk_srcs, sw_cg, hint)
modules = cfg.get("module", [])
mod_clocks = {}
for module in modules:
type_ = module["type"]
if type_ in ("ast", "clkmgr"):
continue
name = module["name"]
clk_srcs = module.get("clock_srcs", {})
clk_grp = module.get("clock_group", "")
clocks = []
for clk in clk_srcs.values():
if isinstance(clk, dict):
clocks.append(f'{clk["group"]}.{clk["clock"]}')
else:
clocks.append(f"{clk_grp}.{clk}")
mod_clocks[name] = clocks
self._mod_clocks = mod_clocks
def load_lifecycle(self, svpath: str) -> None:
"""Load LifeCycle data from RTL file."""
with open(svpath, "rt") as cfp:
self._log.debug("Loading LC constants from %s", svpath)
self._lcconst.load_sv(cfp)
def load_otp_constants(self, svpath: str) -> None:
"""Load OTP data from RTL file."""
with open(svpath, "rt") as cfp:
self._log.debug("Loading OTP constants from %s", svpath)
self._otpconst.load_sv(cfp)
def load_constants(self, hjpath: Optional[str]) -> None:
"""Load definitions from HJSON file."""
if not hjpath:
return
assert not _HJSON_ERROR
self._log.debug("Loading secrets from %s", hjpath)
hjbase = basename(hjpath)
with open(hjpath, "rt") as tfp:
cfg = hjload(tfp, object_pairs_hook=dict)
for module in cfg.get("module") or []:
modtype = module.get("type")
moddefs = self.MODULES.get(modtype)
if not moddefs:
continue
multi, regexes = moddefs[0], moddefs[1:]
consts = {}
OtpSecretConstants.load_values(module, consts, multi, *regexes)
if not consts:
continue
for cname, tname in self.TRANSLATIONS.get(modtype, {}).items():
if cname in consts:
consts[tname] = consts.pop(cname)
self._log.debug("Constants for %s loaded from %s", modtype, hjbase)
exist = modtype in self._constants
if multi:
if not exist:
self._constants[modtype] = consts
else:
self._constants[modtype].update(consts)
else:
if exist:
raise ValueError(f"Redefinition of {modtype}")
self._constants[modtype] = {None: consts}
self._otpconst.load_secrets(cfg)
def prepare(self) -> None:
"""Prepare generation of data, aggregating several sources."""
digests = {
"cnsty_digest": "digest",
"flash_data_key": "flash_data",
"flash_addr_key": "flash_addr",
"sram_data_key": "sram",
}
avail_digests = self._otpconst.get_digests()
otp_ctrl = self._constants["otp_ctrl"][None]
for digest, prefix in digests.items():
if digest not in avail_digests:
continue
pair = self._otpconst.get_digest_pair(digest, prefix)
otp_ctrl.update(pair)
for key in self._otpconst.get_scrambling_keys():
key_value = self._otpconst.get_scrambling_key(key)
key = key.removesuffix("key") + "_scramble_key"
otp_ctrl[key] = key_value
idx = 0
while True:
try:
defaults = self._otpconst.get_partition_inv_defaults(idx)
if defaults:
otp_ctrl[f"inv_default_part_{idx}"] = defaults
idx += 1
except ValueError:
break
lc_ctrl = self._constants["lc_ctrl"][None]
lc_ctrl.update(self._lcconst.tokens)
def exclude(self, exclusions: list[str]) -> None:
"""Add property exclusions."""
for exclude in exclusions:
try:
dev, prop = exclude.split(".")
except ValueError as exc:
raise ArgError(f"Invalid exclusion format: {exclude}") from exc
if dev not in self._exclusions:
self._exclusions[dev] = set()
self._exclusions[dev].add(prop)
def dump_json(self, variant: str, ofp: TextIO) -> None:
"""Dump all configuration constants to JSON format."""
data = {
"variant": variant,
"git_version": self._git_version,
"rom_ctrl": self._constants.get("rom_ctrl", {}),
"otp_ctrl": self._constants.get("otp_ctrl", {}).get(None, {}),
"lc_ctrl": self._constants.get("lc_ctrl", {}).get(None, {}),
"keymgr": {},
"lc_states": {
name: list(states) for name, states in self._lcconst.states.items()
},
"top_clocks": {
c.name: {"frequency": c.frequency, "aon": c.aon, "ref": c.ref}
for c in self._top_clocks.values()
},
"sub_clocks": {
c.name: {"source": c.source, "div": c.div}
for c in self._sub_clocks.values()
},
"clock_groups": {
g.name: {"sources": g.sources, "sw_cg": g.sw_cg, "hint": g.hint}
for g in self._clock_groups.values()
},
}
for keymgr_name in ("keymgr", "keymgr_dpe"):
if keymgr_name in self._constants:
data["keymgr"] = {
"name": keymgr_name,
"values": self._constants[keymgr_name].get(None, {}),
}
break
json.dump(data, ofp, indent=2)
def main():
"""Main routine"""
debug = True
try:
argparser = ArgumentParser(description="Dump OpenTitan constants to JSON.")
files = argparser.add_argument_group(title="Files")
files.add_argument(
"opentitan", nargs="?", metavar="OTDIR", help="OpenTitan root directory"
)
files.add_argument(
"-T", "--top", choices=OpenTitanTop.names, help="OpenTitan top name"
)
files.add_argument(
"-o",
"--out",
metavar="JSON",
required=True,
help="Filename of the JSON file to generate",
)
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(
"-S", "--secrets", metavar="HJSON", help="Secret HJSON 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(
"-x",
"--exclude",
action="append",
metavar="DEVICE.NAME",
default=[],
help="Discard any property from DEVICE that starts "
"with NAME (may be repeated)",
)
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
log = configure_loggers(args.verbose, "cfggen", "lc", "otp")[0]
if _HJSON_ERROR:
argparser.error(f"Missing HJSON module: {_HJSON_ERROR}")
cfg = OtConfiguration()
topcfg = args.topcfg
ot_dir = args.opentitan
if not topcfg:
if not args.opentitan:
argparser.error("OTDIR is required is no top file is specified")
if not isdir(ot_dir):
argparser.error("Invalid OpenTitan root directory")
ot_dir = abspath(ot_dir)
if not args.top:
argparser.error("Top name is required if no top file is " "specified")
top = f"top_{args.top}"
topvar = OpenTitanTop.short_name(args.top)
topcfg = joinpath(ot_dir, f"hw/{top}/data/autogen/{top}.gen.hjson")
if not isfile(topcfg):
argparser.error(f"No such file '{topcfg}'")
log.info("Top config: '%s'", topcfg)
cfg.load_config(topcfg)
else:
if not isfile(topcfg):
argparser.error(f"No such top file: {topcfg}")
cfg.load_config(topcfg)
ltop = cfg.top_name
if not ltop:
argparser.error("Unknown top name")
log.info("Top: '%s'", ltop)
ltop = ltop.lower()
topvar = OpenTitanTop.short_name(cfg.top_name)
if not topvar:
argparser.error(f"Unsupported top name: {cfg.top_name}")
top = f"top_{ltop}"
if not ot_dir:
check_dir = f"hw/{top}/data"
cur_dir = dirname(topcfg)
while cur_dir:
check_path = joinpath(cur_dir, check_dir)
if isdir(check_path):
ot_dir = cur_dir
break
cur_dir = dirname(cur_dir)
if not ot_dir:
argparser.error("Cannot find OT root directory")
elif not isdir(ot_dir):
argparser.error("Invalid OpenTitan root directory")
ot_dir = abspath(ot_dir)
log.info("OT directory: '%s'", ot_dir)
log.info("Variant: '%s'", topvar)
top_dir = joinpath(ot_dir, "hw", top)
lcfilename = "lc_ctrl_state_pkg.sv"
lcpath = args.lifecycle
if not lcpath:
lc_constant_locations = [
joinpath(top_dir, f"rtl/autogen/testing/{lcfilename}"),
joinpath(top_dir, f"rtl/autogen/dev/{lcfilename}"),
joinpath(top_dir, f"rtl/autogen/{lcfilename}"),
joinpath(ot_dir, f"hw/ip/lc_ctrl/rtl/{lcfilename}"),
]
for maybe_lcpath in lc_constant_locations:
if isfile(maybe_lcpath):
lcpath = maybe_lcpath
break
if not lcpath:
argparser.error(f"Unknown location for '{lcfilename}'")
if not isfile(lcpath):
argparser.error(f"No such file '{lcpath}'")
log.debug(f"'{lcfilename}' location: '%s'", lcpath)
ocfilename = "otp_ctrl_part_pkg.sv"
ocpath = args.otpconst
if not ocpath:
otp_constant_locations = [
joinpath(top_dir, f"ip_autogen/otp_ctrl/rtl/{ocfilename}"),
joinpath(ot_dir, f"hw/ip/otp_ctrl/rtl/{ocfilename}"),
]
for maybe_ocpath in otp_constant_locations:
if isfile(maybe_ocpath):
ocpath = maybe_ocpath
break
if not ocpath:
argparser.error(f"Unknown location for '{ocfilename}'")
if not isfile(ocpath):
argparser.error(f"No such file '{ocpath}'")
log.debug(f"'{ocfilename}' location: '%s'", ocpath)
secpath = args.secrets
if secpath:
if not isfile(secpath):
argparser.error("No such secret file: {secpath}")
else:
sec_constant_locations = [
joinpath(top_dir, f"data/autogen/{top}.secrets.testing.gen.hjson"),
joinpath(top_dir, f"data/autogen/{top}.secrets.dev.gen.hjson"),
]
for maybe_secpath in sec_constant_locations:
if isfile(maybe_secpath):
secpath = maybe_secpath
break
cfg.load_lifecycle(lcpath)
cfg.load_otp_constants(ocpath)
cfg.load_constants(secpath)
cfg.prepare()
cfg.exclude(args.exclude)
with open(args.out, "wt") as ofp:
cfg.dump_json(topvar, ofp)
except (ArgError, IOError, ValueError, ImportError) as exc:
print(f"\nError: {exc}", file=sys.stderr)
if debug:
print(format_exc(chain=False), file=sys.stderr)
sys.exit(1)
except KeyboardInterrupt:
sys.exit(2)
if __name__ == "__main__":
main()