From bac75da42f5b93922f72974ecd28cc2b9146a9a9 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Wed, 22 Oct 2025 19:30:18 +0200 Subject: [PATCH 1/8] [ot] python/qemu: ot.util.mbb: fix multibit bool values Signed-off-by: Emmanuel Blot --- python/qemu/ot/util/mbb.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/qemu/ot/util/mbb.py b/python/qemu/ot/util/mbb.py index 215f2d7a8539..995e82baf684 100644 --- a/python/qemu/ot/util/mbb.py +++ b/python/qemu/ot/util/mbb.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024 Rivos, Inc. +# Copyright (c) 2024-2025 Rivos, Inc. # SPDX-License-Identifier: Apache2 """MultiBit Boolean values @@ -11,5 +11,5 @@ MB4_MASK = 0xf MB8_TRUE = 0x96 -MB8_FALSE = 0x89 +MB8_FALSE = 0x69 MB8_MASK = 0xff From fea05fce888587f354ef8d5ab6e05d2217c0e06a Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Wed, 22 Oct 2025 19:29:41 +0200 Subject: [PATCH 2/8] [ot] python/qemu: ot.devproxy: use multibit bool values from ot.util.mbb Signed-off-by: Emmanuel Blot --- python/qemu/ot/devproxy.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/python/qemu/ot/devproxy.py b/python/qemu/ot/devproxy.py index 96c5021d36e0..b9e095f86f36 100644 --- a/python/qemu/ot/devproxy.py +++ b/python/qemu/ot/devproxy.py @@ -1,4 +1,4 @@ -# Copyright (c) 2023-2024 Rivos, Inc. +# Copyright (c) 2023-2025 Rivos, Inc. # SPDX-License-Identifier: Apache2 """Device proxy for OpenTitan devices and peripherals @@ -19,6 +19,8 @@ from typing import Any, Callable, Iterator, NamedTuple, Optional, Union from .mailbox.doe import DOEHeader +from .util.mbb import MB4_FALSE, MB4_TRUE + try: from serial import Serial, serial_for_url @@ -148,12 +150,6 @@ class DeviceProxy: :paran regcount: count of 32-bit registers in the remote device """ - MB4_TRUE = 0x6 - """Multibit bool true value.""" - - MB4_FALSE = 0x9 - """Multibit bool false value.""" - NO_ROLE = 0xf """Disabled role.""" @@ -643,13 +639,13 @@ def lock_address_range(self) -> bool: """ self._log.debug('') self.write_word(self._role, self.REGS['ADDRESS_RANGE_REGWEN'], - self.MB4_FALSE) + MB4_FALSE) def is_locked_address_range(self) -> bool: """Lock address range (base and limit registers). """ res = self.read_word(self._role, self.REGS['ADDRESS_RANGE_REGWEN']) \ - != self.MB4_TRUE + != MB4_TRUE self._log.debug('%d', res) return res From 26c3eef32832eec68e290e6e05c1982250d60000 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Wed, 22 Oct 2025 19:29:59 +0200 Subject: [PATCH 3/8] [ot] python/qemu: ot.otp.map: use multibit bool values from ot.util.mbb Signed-off-by: Emmanuel Blot --- python/qemu/ot/otp/map.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/python/qemu/ot/otp/map.py b/python/qemu/ot/otp/map.py index 3b4fb2992513..3bbaf136d36d 100644 --- a/python/qemu/ot/otp/map.py +++ b/python/qemu/ot/otp/map.py @@ -1,4 +1,4 @@ -# Copyright (c) 2023-2024 Rivos, Inc. +# Copyright (c) 2023-2025 Rivos, Inc. # SPDX-License-Identifier: Apache2 """OTP map. @@ -15,6 +15,7 @@ except ImportError: hjload = None +from ot.util.mbb import MB8_FALSE, MB8_TRUE from ot.util.misc import retrieve_git_version, round_up @@ -32,8 +33,8 @@ class OtpMap: } MUBI8_BOOLEANS = { - 0x96: False, - 0x69: True, + MB8_FALSE: False, + MB8_TRUE: True, 0x00: None } From 578898c46d4ffa6fc11e2b9bee3a71b3718c0147 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Thu, 23 Oct 2025 18:16:53 +0200 Subject: [PATCH 4/8] [ot] python/qemu: ot.util.misc: silence git when retrieve_git_version fails It does not help to report `No names found: cannot describe anything` to the parent stderr. Signed-off-by: Emmanuel Blot --- python/qemu/ot/util/misc.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/python/qemu/ot/util/misc.py b/python/qemu/ot/util/misc.py index 236d9c3ca218..46a821f3a5cc 100644 --- a/python/qemu/ot/util/misc.py +++ b/python/qemu/ot/util/misc.py @@ -9,7 +9,7 @@ from collections.abc import Callable from io import BytesIO from os.path import abspath, dirname, exists, isdir, isfile, join as joinpath -from subprocess import SubprocessError, check_output +from subprocess import DEVNULL, SubprocessError, check_output from sys import stdout from textwrap import dedent, indent from typing import Any, Iterable, Optional, TextIO, Union @@ -253,7 +253,7 @@ def retrieve_git_version(path: str, max_tag_dist: int = 100) \ assert cfgdir is not None try: descstr = check_output(['git', 'describe', '--long', '--dirty'], - text=True, cwd=cfgdir).strip() + text=True, cwd=cfgdir, stderr=DEVNULL).strip() gmo = re.match(r'^(?P.*)-(?P\d+)-g(?P[0-9a-f]+)' r'(?:-(?Pdirty))?$', descstr) if not gmo: @@ -269,9 +269,9 @@ def retrieve_git_version(path: str, max_tag_dist: int = 100) \ pass try: change = check_output(['git', 'status', '--porcelain'], - text=True, cwd=cfgdir).strip() + text=True, cwd=cfgdir, stderr=DEVNULL).strip() descstr = check_output(['git', 'rev-parse', '--short', 'HEAD'], - text=True, cwd=cfgdir).strip() + text=True, cwd=cfgdir, stderr=DEVNULL).strip() if len(change) > 1: descstr = f'{descstr}-dirty' return descstr From 3cc69076cd58bc6a8afd080634d7a18b979b31b2 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Tue, 21 Oct 2025 17:43:15 +0200 Subject: [PATCH 5/8] [ot] scripts/opentitan: verilate.py: support the same short option for log-time as pyot.py Signed-off-by: Emmanuel Blot --- docs/opentitan/verilate.md | 7 ++++--- scripts/opentitan/verilate.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/opentitan/verilate.md b/docs/opentitan/verilate.md index 582b29996314..7df7e3561acc 100644 --- a/docs/opentitan/verilate.md +++ b/docs/opentitan/verilate.md @@ -9,8 +9,7 @@ binaries as QEMU and comparing the outcome of each simulation environment. ````text usage: verilate.py [-h] [-V VERILATOR] [-R FILE] [-M FILE] [-F FILE] [-O VMEM] [-K] [-D TMP_DIR] [-c CFG] [-a PREFIX] [-C CYCLES] [-I] - [-k SECONDS] [-l] [-P FILE] [-w] [-x] [-v] [-d] - [--log-time] + [-k SECONDS] [-l] [-P FILE] [-w] [-x] [-v] [-d] [-G] [ELF ...] Verilator wrapper. @@ -49,7 +48,7 @@ Verilator: Extras: -v, --verbose increase verbosity -d, --debug enable debug mode - --log-time show local time in log messages + -G, --log-time show local time in log messages ```` ### Arguments @@ -79,6 +78,8 @@ Extras: simulated platform supports multiple embedded flash partitions, in which case the specified files are loaded in partition order. See option `-I` for a list of supported devices. +* `-G` / `--log-time` show local time before each logged message + * `-K` / `--keep-tmp` do not automatically remove temporary files and directories on exit. The user is in charge of discarding any generated files and directories after execution. The paths to the generated items are emitted as warning messages. diff --git a/scripts/opentitan/verilate.py b/scripts/opentitan/verilate.py index bf474b058ab3..dce875db1071 100755 --- a/scripts/opentitan/verilate.py +++ b/scripts/opentitan/verilate.py @@ -83,7 +83,7 @@ def main(): help='increase verbosity') extra.add_argument('-d', '--debug', action='store_true', help='enable debug mode') - extra.add_argument('--log-time', action='store_true', + extra.add_argument('-G', '--log-time', action='store_true', help='show local time in log messages') args = argparser.parse_args() debug = args.debug From bba81282694918af8881d1b56ca686a237b03f21 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Tue, 21 Oct 2025 17:42:38 +0200 Subject: [PATCH 6/8] [ot] scripts/opentitan: autoreg.py: improve support for compact multireg registers Signed-off-by: Emmanuel Blot --- scripts/opentitan/autoreg.py | 57 +++++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 20 deletions(-) diff --git a/scripts/opentitan/autoreg.py b/scripts/opentitan/autoreg.py index de8fb8bd8538..eed998da2fcd 100755 --- a/scripts/opentitan/autoreg.py +++ b/scripts/opentitan/autoreg.py @@ -12,7 +12,7 @@ from enum import StrEnum, auto from io import StringIO from logging import getLogger -from os.path import basename, dirname, join as joinpath, normpath +from os.path import basename, dirname, join as joinpath, normpath, splitext from time import localtime from traceback import format_exception from typing import Any, NamedTuple, Optional, TextIO, Union @@ -27,7 +27,8 @@ from ot.util.eval import safe_eval from ot.util.log import configure_loggers from ot.util.misc import (HexInt, camel_to_snake_case, camel_to_snake_uppercase, - classproperty, flatten, redent, retrieve_git_version) + classproperty, flatten, redent, retrieve_git_version, + to_bool) try: _HJSON_ERROR = None @@ -60,6 +61,11 @@ class AutoField(NamedTuple): access: AutoAccess reset: int = 0 + def __str__(self) -> str: + """Return a compact representation of a field.""" + return (f'{self.__class__.__name__}({self.offset}:{self.name}:' + f'{self.width} {self.access})') + class AutoRegister(NamedTuple): """Device register.""" @@ -156,26 +162,33 @@ def load(self, hfp: TextIO, win_lim: Optional[int] = None) -> None: continue if 'multireg' in item: item = item['multireg'] - compact = item.get('compact', True) + # compact field can either be a string, a boolean, an integer + # and may be present or not + force_compact = item.get('compact') reg = self._parse_register(address, item) count = safe_eval(item['count'], self._parameters) + if force_compact is not None: + compact = to_bool(force_compact) + else: + compact = len(reg.fields) == 1 if compact: # single register with multiple fields with the same prefix if len(reg.fields) > 1: - # not sure if the following case may exists, anyway for - # now it is not supported + # this case is not yet supported raise NotImplementedError(f'Too many compact fields for' f' {reg.name}') field = reg.fields[0] - if count <= self._regwidth: - bitcount = min(count, self._regwidth) + fwidth = field.offset + field.width + fcount = min(count, self._regwidth // fwidth) + if fcount > 1: reg = reg._replace(fields=[ - field._replace(name=f'{field.name}_{pos}', offset=pos) - for pos in range(bitcount) + field._replace(name=f'{field.name}_{pos}', + offset=pos) + for pos in range(fcount) ]) else: reg = reg._replace(fields=[]) - count = (count + self._regwidth - 1) // self._regwidth + count = count // fcount if count > 1: # multiple registers with the same name prefix reg = reg._replace(count=count) @@ -206,6 +219,7 @@ def load(self, hfp: TextIO, win_lim: Optional[int] = None) -> None: address += addr_inc * count continue reg = self._parse_register(address, item) + if reg: if reg.name in regnames: prefix = reg.name.split('_')[0] @@ -1152,14 +1166,13 @@ def main(): files.add_argument('-C', '--copyright', help='define copyright string') params.add_argument('-g', '--generate', action='append', - choices=generators, required=True, + choices=generators, help='what to generate') params.add_argument('-k', '--out-kind', choices=outkinds, default=outkinds[0], help=f'output file format ' f'(default: {outkinds[0]})') - params.add_argument('-n', '--name', default='foo', - help='device name') + params.add_argument('-n', '--name', help='device name') params.add_argument('-p', '--ignore', metavar='PREFIX', help='ignore register/fields starting with prefix') params.add_argument('-r', '--reset', action='append', @@ -1181,18 +1194,22 @@ def main(): if _HJSON_ERROR: argparser.error(f'Missing HJSON module: {_HJSON_ERROR}') - configure_loggers(args.verbose, 'autoreg') + log = configure_loggers(args.verbose, 'autoreg')[0] reggen = autoregs[args.out_kind] - areg = reggen(args.name, args.ignore, args.reset) + name = args.name or splitext(basename(args.config.name))[0] + areg = reggen(name, args.ignore, args.reset) if args.copyright: areg.copyright = args.copyright areg.load(args.config, args.win_limit) - for gen in args.generate or []: - generate = getattr(areg, f'generate_{gen}', None) - if not generate: - argparser.error(f'{gen} is not supported for {args.out_kind}') - generate(args.output or sys.stdout) + if args.generate: + for gen in args.generate: + generate = getattr(areg, f'generate_{gen}', None) + if not generate: + argparser.error(f'{gen} is not supported for {args.out_kind}') + generate(args.output or sys.stdout) + else: + log.warning('No generation requested') except (IOError, ValueError, ImportError) as exc: print(f'\nError: {exc}', file=sys.stderr) From 53aee1f2fdab94b95d73f0bca602e0acd9ff8b69 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Tue, 21 Oct 2025 20:14:39 +0200 Subject: [PATCH 7/8] [ot] scripts/opentitan: autoreg.py: add register group splitting feature Signed-off-by: Emmanuel Blot --- scripts/opentitan/autoreg.py | 507 ++++++++++++++++++++++++----------- 1 file changed, 357 insertions(+), 150 deletions(-) diff --git a/scripts/opentitan/autoreg.py b/scripts/opentitan/autoreg.py index eed998da2fcd..b801eb636589 100755 --- a/scripts/opentitan/autoreg.py +++ b/scripts/opentitan/autoreg.py @@ -26,9 +26,11 @@ # ruff: noqa: E402 from ot.util.eval import safe_eval from ot.util.log import configure_loggers +from ot.util import mbb from ot.util.misc import (HexInt, camel_to_snake_case, camel_to_snake_uppercase, classproperty, flatten, redent, retrieve_git_version, to_bool) +from ot.util.arg import ArgError try: _HJSON_ERROR = None @@ -106,6 +108,11 @@ def __init__(self, devname: str, ignore_prefix: Optional[str] = None, self._regwidth = 0 # bits self._registers: list[AutoRegister] = [] self._copyright = '@todo Add copyright attributions' + self._reg_groups: dict[str, set[str]] = {} # group, reg names + self._group_offsets: dict[str, int] = {} + self._irq_count = 0 + self._alert_count = 0 + self._nibcount = 0 @classproperty def generators(cls): @@ -232,6 +239,10 @@ def load(self, hfp: TextIO, win_lim: Optional[int] = None) -> None: address += addr_inc registers.sort(key=lambda r: r.address) self._registers = registers + self._reg_groups[''] = {reg.name for reg in registers} + self._group_offsets[''] = 0 + max_addr = max(reg.address for reg in registers) + self._nibcount = (max_addr.bit_length() + 3) // 4 @property def copyright(self) -> str: @@ -245,14 +256,50 @@ def copyright(self, copyright_str: str) -> None: """ self._copyright = copyright_str + def segment(self, segdescs: list[str]) -> None: + """Create register segmentation.""" + renum_names: dict[str, str] = {} + for segdesc in segdescs: + try: + regname, segment = segdesc.split(':') + except ValueError as exc: + raise ArgError(f'Invalid segment declaration: ' + f'{segdesc}') from exc + regname = regname.upper() + if regname in renum_names: + raise ArgError(f'Multiple segmentation on {regname}') + if segment in renum_names.values(): + raise ArgError(f'Multiple definition of {segment}') + renum_names[regname] = segment.upper() + reg_names = {reg.name for reg in self._registers} + missing = set(renum_names) - reg_names + if missing: + raise ArgError(f'Unknown registers: {", ".join(missing)}') + upregs: list[AutoRegister] = [] + addr_inc = self._regwidth // 8 + address = 0 + group = self._reg_groups.popitem()[0] + del self._group_offsets[group] + for reg in self._registers: + new_group = renum_names.get(reg.name) + if new_group: + self._log.info('Reset address base on %s', reg.name) + group = new_group + address = 0 + if group not in self._reg_groups: + self._reg_groups[group] = [] + self._group_offsets[group] = reg.address + self._reg_groups[group].append(reg.name) + upregs.append(reg._replace(address=address)) + address += addr_inc * reg.count + self._registers = upregs + def generate_register(self, tfp: TextIO) -> None: """Generate the register map. :param tfp: output file stream """ - max_addr = max(reg.address for reg in self._registers) - nibcount = (max_addr.bit_length() + 3) // 4 - self._generate_registers(nibcount, tfp) + self._generate_registers(tfp) def _parse_interrupts(self, hjson: dict[str, Any], address: int, addr_inc: int) -> list[AutoRegister]: @@ -265,6 +312,7 @@ def _parse_interrupts(self, hjson: dict[str, Any], address: int, fieldargs = [item['name'].upper(), item['desc'], fpos, 1, AutoAccess(access), 0] fields.append(AutoField(*fieldargs)) + self._irq_count = len(fields) if not fields: return [] registers: list[AutoRegister] = [] @@ -308,6 +356,7 @@ def _parse_alerts(self, hjson:dict[str, Any], address: int) \ for fpos, item in enumerate(hjson.get('alert_list', [])): fieldargs = [item['name'].upper(), item['desc'], fpos, 1, wo_acc, 0] fields.append(AutoField(*fieldargs)) + self._alert_count = len(fields) if not fields: return [] # pylint: disable=use-dict-literal @@ -374,8 +423,18 @@ def _parse_reg_fields(self, reg: dict[str, Any]) -> list[AutoField]: except KeyError as exc: raise RuntimeError(f'Cannot find swaccess for {name}') from exc resval = field.get('resval') - reset = HexInt.parse(resval, accept_int=True) \ - if resval else HexInt(0) + mubi = field.get('mubi', False) + if isinstance(resval, bool): + if mubi: + mubi_symbol = f'MB{width}_{str(resval).upper()}' + reset = getattr(mbb, mubi_symbol, None) + if reset is None: + raise ValueError(f'Unknown value for {mubi_symbol}') + else: + reset = int(resval) + else: + reset = HexInt.parse(resval, accept_int=True) \ + if resval else HexInt(0) fname = camel_to_snake_case(name).upper() fieldargs = [fname, desc, offset, width, access, reset] fields.append(AutoField(*fieldargs)) @@ -430,7 +489,7 @@ def _build_reg_access(self, fields): return access.pop() return AutoAccess.UNDEF - def _generate_registers(self, nibcount: int, tfp: TextIO) -> None: + def _generate_registers(self, tfp: TextIO) -> None: raise NotImplementedError("Abstract base class") @@ -443,6 +502,7 @@ def generate_all(self, tfp: TextIO) -> None: :param tfp: output file stream """ self.generate_header(tfp) + self.generate_param(tfp) self.generate_register(tfp) self.generate_mask(tfp) self.generate_regname(tfp) @@ -504,12 +564,33 @@ def generate_struct(self, tfp: TextIO) -> None: """ dname = self._devname hdname = dname.title().replace('_', '') - code = f''' + header = f''' struct {hdname}State {{ SysBusDevice parent_obj; MemoryRegion mmio; - + ''' + extras = [] + indent = header.rsplit('\n', 1)[-1] + if len(self._reg_groups) > 1: + for group in self._reg_groups: + extras.append(f' MemoryRegion mmio_{group.lower()};' + f'\n{indent}') + if self._irq_count or self._alert_count: + extras.append(f'\n{indent}') + if self._irq_count: + extras.append(f' IbexIRQ irqs[NUM_IRQS];\n{indent}') + if self._alert_count: + extras.append(f' IbexIRQ alerts[NUM_ALERTS];\n{indent}') + extras.append(f'\n{indent}') + if len(self._reg_groups) == 1: + extras.append(f' uint{self._regwidth}_t *regs;\n{indent}') + else: + for group in self._reg_groups: + sregs = f'regs_{group.lower()}' + extras.append(f' uint{self._regwidth}_t *{sregs};\n{indent}') + extra = ''.join(extras) + footer = f''' char *ot_id; }}; @@ -519,6 +600,7 @@ def generate_struct(self, tfp: TextIO) -> None: }}; ''' + code = f'{header}{extra}{footer}' print(redent(code), file=tfp) def generate_param(self, tfp: TextIO) -> None: @@ -529,7 +611,12 @@ def generate_param(self, tfp: TextIO) -> None: cat_params: dict[str, dict[str, int]] = {} max_length = 0 name_first = ('NUM', ) - for name, value in self._parameters.items(): + parameters = dict(self._parameters) + if self._irq_count: + parameters['NUM_IRQS'] = self._irq_count + if self._alert_count: + parameters['NUM_ALERTS'] = self._alert_count + for name, value in parameters.items(): ucname = camel_to_snake_uppercase(name) parts = ucname.split('_') pre = parts[0] @@ -560,77 +647,59 @@ def generate_param(self, tfp: TextIO) -> None: for cat in sorted(cat_params): params = cat_params[cat] for name, value in params.items(): + if isinstance(value, bool): + # no use case for this kind of parameter + continue imod = 'u' if isinstance(value, int) and value >= 0 else '' - print(f'#define {name:<{max_length}s} {value}{imod}', - file=tfp) - print(file=tfp) + print(f'#define {name:<{max_length}s} {value}{imod}', file=tfp) + print(file=tfp) def generate_reset(self, tfp: TextIO) -> None: """Generate reset initialization values. :param tfp: output file stream """ - nibcount = self._regwidth // 4 - for reg in self._registers: - self._generate_reg_reset(reg, nibcount, tfp) + for group, regs in self._build_reg_groups(): + for reg in regs: + self._generate_reg_reset(group, reg, tfp) def generate_mask(self, tfp: TextIO) -> None: """Generate the write mask associated with the register map. :param tfp: output file stream """ - for reg in self._registers: - if len(reg.fields) < 2: - return - bitmask = sum(((1 << bf.width) - 1) << bf.offset - for bf in reg.fields) - bitfield = (1 << self._regwidth) - 1 - if bitfield & ~bitmask == 0: - return - if not reg.shared_fields: - masks: list[str] = [ - f'R_{reg.name}_{field.name}_MASK' for field in reg.fields - if field.access not in (AutoAccess.RO,) - ] - else: - name = '_'.join(reg.name.rsplit('_', 1)[:-1]) or reg.name - masks: list[str] = [ - f'{name}_{field.name}_MASK' for field in reg.fields - if field.access not in (AutoAccess.RO,) - ] - if not masks: - return - maskstr = ' | \\\n '.join(masks) - if not reg.shared_fields: - name = reg.name - else: - name = '_'.join(reg.name.rsplit('_', 1)[:-1]) or reg.name - print(f'#define {name}_WMASK \\\n ({maskstr})\n', file=tfp) - # special cast for interrupt testing with mixed fields - if (reg.name == 'INTR_STATE' and reg.shared_fields and - reg.access == AutoAccess.UNDEF): - masks: list[str] = [ - f'{name}_{field.name}_MASK' for field in reg.fields - ] - maskstr = ' | \\\n '.join(masks) - print(f'#define INTR_TEST_WMASK \\\n ({maskstr})\n', - file=tfp) + for _, regs in self._build_reg_groups(): + self._generate_mask(regs, tfp) + print(file=tfp) def generate_io(self, tfp: TextIO) -> None: """Generate the skeleton for the MMIO read and write functions. :param tfp: output file stream """ - self._generate_io(self._registers, False, tfp) - self._generate_io(self._registers, True, tfp) + groups = self._build_reg_groups() + for group, regs in groups: + self._generate_io(group, regs, False, tfp) + for group, regs in groups: + self._generate_io(group, regs, True, tfp) def generate_regname(self, tfp: TextIO) -> None: """Generate the array of register names. :param tfp: output file stream """ - self._generate_reg_wrappers(self._registers, tfp) - self._generate_regname_array(self._registers, tfp) + print(f'#define R{self._regwidth}_OFF(_r_) ((_r_) / ' + f'sizeof(uint{self._regwidth}_t))\n', file=tfp) + groups = self._build_reg_groups() + for group, regs in groups: + self._generate_reg_wrappers(group, regs, tfp) + print('#define REG_NAME_ENTRY(_reg_) [R_##_reg_] = stringify(_reg_)', + file=tfp) + for pos, (group, regs) in enumerate(groups): + self._generate_regname_array(group, regs, tfp) + if pos < len(groups) - 1: + print('', file=tfp) + print('#undef REG_NAME_ENTRY\n', file=tfp) def generate_device(self, tfp: TextIO) -> None: """Generate the skeleton for the device initialization functions. @@ -646,11 +715,33 @@ def generate_device(self, tfp: TextIO) -> None: self._generate_class_init(tfp) self._generate_type(tfp) - def _generate_registers(self, nibcount: int, tfp: TextIO) -> None: + @classmethod + def tweak_field_name(cls, regname: str, fieldname: str) -> str: + """Attempt to simplify field names to avoid useless duplication + of the register name in the field name. + """ + last = regname.split('_')[-1] + if regname != fieldname: + if last != fieldname: + return fieldname + if last in ('ENABLE', 'REGWEN'): + return 'EN' + return 'VAL' + + def _build_reg_groups(self) -> list[tuple[str, list[AutoRegister]]]: + """Build list of register grouped by segment groups + + :return: a list of (group, ordered(AutoRegister)) + """ + return [(group, [reg for reg in self._registers + if reg.name in regnames]) + for group, regnames in self._reg_groups.items()] + + def _generate_registers(self, tfp: TextIO) -> None: print('/* clang-format off */', file=tfp) for reg in self._registers: if reg.count == 1: - self._generate_register(reg, nibcount, tfp) + self._generate_register(reg, tfp) continue address = reg.address addr_inc = self._regwidth // 8 @@ -659,27 +750,26 @@ def _generate_registers(self, nibcount: int, tfp: TextIO) -> None: address=address, count=1, fields=reg.fields if rpos == 0 else [], shared_fields=rpos == 0) - self._generate_register(rreg, nibcount, tfp) + self._generate_register(rreg, tfp) address += addr_inc print('/* clang-format on */\n', file=tfp) - def _generate_register(self, reg: AutoRegister, nibcount: int, - tfp: TextIO) -> None: - print(f'REG{self._regwidth}({reg.name}, 0x{reg.address:0{nibcount}x}u)', + def _generate_register(self, reg: AutoRegister, tfp: TextIO) -> None: + print(f'REG{self._regwidth}({reg.name}, ' + f'0x{reg.address:0{self._nibcount}x}u)', file=tfp) if not reg.shared_fields: if len(reg.fields) > 1: for field in reg.fields: - print(f' FIELD({reg.name}, {field.name}, ' + fdname = self.tweak_field_name(reg.name, field.name) + print(f' FIELD({reg.name}, {fdname}, ' f'{field.offset}u, ' f'{field.width}u)', file=tfp) elif reg.fields: field = reg.fields[0] if field.width != self._regwidth: - if reg.name != field.name: - fdname = field.name - else: - fdname = field.name.rsplit('_', 1)[-1] + # do not emit a field if the field covers the entire reg + fdname = self.tweak_field_name(reg.name, field.name) print(f' FIELD({reg.name}, {fdname}, {field.offset}u, ' f'{field.width}u)', file=tfp) else: @@ -692,6 +782,7 @@ def _generate_register(self, reg: AutoRegister, nibcount: int, elif reg.fields: field = reg.fields[0] if field.width != self._regwidth: + # do not emit a field if the field covers the entire reg if name != field.name: shname = f'{name}_{field.name}' else: @@ -700,13 +791,52 @@ def _generate_register(self, reg: AutoRegister, nibcount: int, f'{field.offset}u, ' f'{field.width}u)', file=tfp) - def _generate_reg_reset(self, reg: AutoRegister, nibcount: int, + def _generate_reg_reset(self, group: str, reg: AutoRegister, tfp: TextIO) -> None: if reg.reset: - print(f' s->regs[R_{reg.name}] = 0x{reg.reset:0{nibcount}x}u;', + regs = f'regs_{group.lower()}' if group else 'regs' + print(f' s->{regs}[R_{reg.name}] = 0x{reg.reset:x}u;', file=tfp) - def _generate_io(self, regs: list[AutoRegister], write: bool, + def _generate_mask(self, regs: list[AutoRegister], tfp: TextIO) -> None: + for reg in regs: + if len(reg.fields) < 2: + continue + bitmask = sum(((1 << bf.width) - 1) << bf.offset + for bf in reg.fields) + bitfield = (1 << self._regwidth) - 1 + if bitfield & ~bitmask == 0: + continue + if not reg.shared_fields: + masks: list[str] = [ + f'{reg.name}_{field.name}_MASK' for field in reg.fields + if field.access not in (AutoAccess.RO,) + ] + else: + name = '_'.join(reg.name.rsplit('_', 1)[:-1]) or reg.name + masks: list[str] = [ + f'{name}_{field.name}_MASK' for field in reg.fields + if field.access not in (AutoAccess.RO,) + ] + if not masks: + continue + maskstr = ' | \\\n '.join(masks) + if not reg.shared_fields: + name = reg.name + else: + name = '_'.join(reg.name.rsplit('_', 1)[:-1]) or reg.name + print(f'#define {name}_WMASK \\\n ({maskstr})', file=tfp) + # special cast for interrupt testing with mixed fields + if (reg.name == 'INTR_STATE' and reg.shared_fields and + reg.access == AutoAccess.UNDEF): + masks: list[str] = [ + f'{name}_{field.name}_MASK' for field in reg.fields + ] + maskstr = ' | \\\n '.join(masks) + print(f'#define INTR_TEST_WMASK \\\n ({maskstr})', + file=tfp) + + def _generate_io(self, group: str, regs: list[AutoRegister], write: bool, tfp: TextIO) -> None: unfold_regs: list[AutoRegister] = [] for reg in regs: @@ -739,7 +869,16 @@ def _generate_io(self, regs: list[AutoRegister], write: bool, reg_names -= rc_names rwidth = self._regwidth dname = self._devname - hdname = dname.title().replace('_', '') + if group: + lgroup = group.lower() + larg = f'"{lgroup}", ' + sregs = f's->regs_{lgroup}' + else: + sregs = 's->regs' + larg = '' + dpad = ' ' * (len(dname) - 7) + hsname = self._devname.title().replace('_', '') + rnm = '_'.join(filter(None, ('REG', group, 'NAME'))) vnm = f'val{rwidth}' if write: if rwidth < 64: @@ -748,16 +887,16 @@ def _generate_io(self, regs: list[AutoRegister], write: bool, vcast = '' code = f''' static void {dname}_regs_write(void *opaque, hwaddr addr, - uint64_t val64, unsigned size) + {dpad}uint64_t val64, unsigned size) {{ - {hdname}State *s = opaque; + {hsname}State *s = opaque; (void)size; {vcast} hwaddr reg = R{rwidth}_OFF(addr); uint32_t pc = ibex_get_current_pc(); - trace_{dname}_io_write(s->ot_id, (uint32_t)addr, REG_NAME(reg), - val{rwidth}, pc); + trace_{dname}_io_write(s->ot_id, {larg}(uint32_t)addr, + {dpad}{rnm}(reg), val{rwidth}, pc); ''' else: if rwidth < 64: @@ -766,9 +905,9 @@ def _generate_io(self, regs: list[AutoRegister], write: bool, vdef = '' code = f''' static uint64_t {dname}_regs_read(void *opaque, hwaddr addr, - unsigned size) + {dpad}unsigned size) {{ - {hdname}State *s = opaque; + {hsname}State *s = opaque; (void)size; {vdef} @@ -781,12 +920,7 @@ def _generate_io(self, regs: list[AutoRegister], write: bool, if not write: for rname in sorted(reg_names, key=lambda r: nregs[r].address): lines.append(f'case R_{rname}:') - lines.append(f' {vnm} = s->regs[reg];') - lines.append(' break;') - for rname in sorted(rc_names, key=lambda r: nregs[r].address): - lines.append(f'case R_{rname}:') - lines.append(f' {vnm} = s->regs[reg];') - lines.append(' s->regs[reg] = 0;') + lines.append(f' {vnm} = {sregs}[reg];') lines.append(' break;') else: for rname in sorted(reg_names, key=lambda r: nregs[r].address): @@ -795,12 +929,12 @@ def _generate_io(self, regs: list[AutoRegister], write: bool, if bitfield & rbm: lines.append(f' {vnm} &= R_{rname}_WMASK;') if racc in (AutoAccess.RW1C, AutoAccess.R0W1C): - lines.append(f' s->regs[reg] &= ~{vnm}; ' + lines.append(f' {sregs}[reg] &= ~{vnm}; ' f'/* {racc.upper()} */') elif racc == AutoAccess.RW0C: - lines.append(f' s->regs[reg] &= {vnm}; /* RW0C */') + lines.append(f' {sregs}[reg] &= {vnm}; /* RW0C */') elif racc == AutoAccess.RW1S: - lines.append(f' s->regs[reg] |= {vnm}; /* RW1S */') + lines.append(f' {sregs}[reg] |= {vnm}; /* RW1S */') else: if racc == AutoAccess.UNDEF: self._log.warning( @@ -809,7 +943,7 @@ def _generate_io(self, regs: list[AutoRegister], write: bool, rname) lines.append(' /* @todo handle multiple access type ' 'bitfield */') - lines.append(f' s->regs[reg] = {vnm};') + lines.append(f' {sregs}[reg] = {vnm};') lines.append(' break;') if noaccess_names: for rname in sorted(noaccess_names, key=lambda r: nregs[r].address): @@ -818,54 +952,63 @@ def _generate_io(self, regs: list[AutoRegister], write: bool, lines.append(redent(f''' qemu_log_mask(LOG_GUEST_ERROR, "%s: %s: {a}/O register 0x%02" HWADDR_PRIx " (%s)\\n", - __func__, s->ot_id, addr, REG_NAME(reg)); + __func__, s->ot_id, addr, {rnm}(reg)); ''', 4, True)) + if not write: + lines.append(' val32 = 0;') lines.append(' break;') lines.append('default:') + lines.append(redent(''' + qemu_log_mask(LOG_GUEST_ERROR, "%s: %s: Bad offset 0x%" HWADDR_PRIx "\\n", + __func__, s->ot_id, addr); + ''', 4, True)) + if not write: + lines.append(' val32 = 0;') lines.append(' break;') lines.append('}') print('\n '.join(lines), file=tfp) if write: code = '};\n' else: - rval = '(uint64_t)val{rwidth}' if rwidth < 64 else 'val64' + rval = f'(uint64_t)val{rwidth}' if rwidth < 64 else 'val64' code = f''' uint32_t pc = ibex_get_current_pc(); - trace_{dname}_io_read_out(s->ot_id, (uint32_t)addr, REG_NAME(reg), - val{rwidth}, pc); + trace_{dname}_io_read_out(s->ot_id, {larg}(uint32_t)addr, + {dpad}{rnm}(reg), val{rwidth}, pc); return {rval}; }} ''' print(redent(code), '', file=tfp) - def _generate_reg_wrappers(self, regs: list[AutoRegister], - tfp: TextIO) -> None: + def _generate_reg_wrappers(self, group: Optional[str], + regs: list[AutoRegister], tfp: TextIO) -> None: last = regs[-1] + sep = f'_{group}_' if group else '_' lines = [] - lines.append(f'#define R{self._regwidth}_OFF(_r_) ((_r_) / ' - f'sizeof(uint{self._regwidth}_t))') - lines.append('') - if last.count: - lines.append(f'#define R_LAST_REG (R_{last.name}_{last.count-1})') + if last.count > 1: + lines.append(f'#define R_LAST{sep}REG (R_{last.name}_{last.count-1})') else: - lines.append(f'#define R_LAST_REG (R_{last.name})') - lines.append('#define REGS_COUNT (R_LAST_REG + 1u)') - lines.append(f'#define REGS_SIZE (REGS_COUNT * ' + lines.append(f'#define R_LAST{sep}REG (R_{last.name})') + lines.append(f'#define REGS{sep}COUNT (R_LAST{sep}REG + 1u)') + lines.append(f'#define REGS{sep}SIZE (REGS{sep}COUNT * ' f'sizeof(uint{self._regwidth}_t))') - lines.append('#define REG_NAME(_reg_) \\') - lines.append(' ((((_reg_) <= REGS_COUNT) && REG_NAMES[_reg_]) ? ' - 'REG_NAMES[_reg_] : "?")') + if len(self._reg_groups) > 1: + base = self._group_offsets[group] + lines.append(f'#define REGS{sep}BASE 0x{base:0{self._nibcount}x}u') + lines.append(f'#define REG{sep}NAME(_reg_) \\') + lines.append(f' ((((_reg_) <= REGS{sep}COUNT) && ' + f'REG{sep}NAMES[_reg_]) ? REG{sep}NAMES[_reg_] : "?")') lines.append('') print('\n'.join(lines), file=tfp) - def _generate_regname_array(self, regs: list[AutoRegister], - tfp: TextIO) -> None: + def _generate_regname_array(self, group: Optional[str], + regs: list[AutoRegister], tfp: TextIO) -> None: lines = [] - lines.append( - '#define REG_NAME_ENTRY(_reg_) [R_##_reg_] = stringify(_reg_)') - lines.append('static const char *REG_NAMES[REGS_COUNT] = {') + reg_names = '_'.join(filter(None, ('REG', group, 'NAMES'))) + reg_count = '_'.join(filter(None, ('REG', group, 'COUNT'))) + lines.append(f'static const char *{reg_names}[{reg_count}] = {{') lines.append(' /* clang-format off */') for reg in regs: if reg.count > 1: @@ -875,24 +1018,26 @@ def _generate_regname_array(self, regs: list[AutoRegister], lines.append(f' REG_NAME_ENTRY({reg.name}),') lines.append(' /* clang-format on */') lines.append('};') - lines.append('#undef REG_NAME_ENTRY') - lines.append('') print('\n'.join(lines), file=tfp) def _generate_mr_ops(self, tfp: TextIO) -> None: - dname = self._devname - code = f''' - static const MemoryRegionOps {dname}_ops = {{ - .read = &{dname}_io_read, - .write = &{dname}_io_write, - .endianness = DEVICE_LITTLE_ENDIAN, - .impl = {{ - .min_access_size = 4u, - .max_access_size = 4u, - }}, - }}; - ''' - print(redent(code), file=tfp) + for group in self._reg_groups: + if group: + dname = f'{self._devname}_{group.lower()}' + else: + dname = self._devname + code = f''' + static const MemoryRegionOps {dname}_ops = {{ + .read = &{dname}_regs_read, + .write = &{dname}_regs_write, + .endianness = DEVICE_LITTLE_ENDIAN, + .impl = {{ + .min_access_size = 4u, + .max_access_size = 4u, + }}, + }}; + ''' + print(redent(code), file=tfp) def _generate_props(self, tfp: TextIO) -> None: dname = self._devname @@ -910,9 +1055,10 @@ def _generate_reset(self, tfp: TextIO, reset_type: str) -> None: hdname = dname.title().replace('_', '') uname = dname.upper() regio = StringIO() - nibcount = self._regwidth // 4 - for reg in self._registers: - self._generate_reg_reset(reg, nibcount, regio) + groups = self._build_reg_groups() + for group, regs in groups: + for reg in regs: + self._generate_reg_reset(group, reg, regio) regcode = redent(regio.getvalue(), 12, strip_end=True).lstrip() code = f''' @@ -925,15 +1071,16 @@ def _generate_reset(self, tfp: TextIO, reset_type: str) -> None: c->parent_phases.{reset_type}(obj, type); }} ''' + indent = code.rsplit('\n', 1)[-1] + lines = [f'\n{indent}'] if reset_type == "enter": - code += ''' - memset(s->regs, 0, REG_SIZE); - - ''' - spacer = ' ' * 8 - code += f'{regcode}\n{spacer}}}\n' - else: - code += '}\n' + for group in self._reg_groups: + regname = f'regs_{group.lower()}' if group else 'regs' + lines.append(f' memset(s->{regname}, 0, ' + f'{regname.upper()}_SIZE);\n{indent}') + lines.append(f'\n{indent} {regcode}\n{indent}') + lines.append('}\n') + code = f'{code}{"".join(lines)}' print(redent(code), file=tfp) def _generate_realize(self, tfp: TextIO) -> None: @@ -953,19 +1100,70 @@ def _generate_init(self, tfp: TextIO) -> None: dname = self._devname hdname = dname.title().replace('_', '') uname = dname.upper() - code = f''' - static void {dname}_init(Object *obj) - {{ - {hdname}State *s = {uname}(obj); - - memory_region_init_io(&s->mmio, obj, &{hdname}_regs_ops, s, - TYPE_{uname}, REGS_SIZE); - sysbus_init_mmio(SYS_BUS_DEVICE(s), &s->mmio); - - s->regs = g_new0(uint{self._regwidth}_t, REGS_COUNT); - }} - ''' - print(redent(code), file=tfp) + lines: list[str] = [ + f'static void {dname}_init(Object *obj)', + '{' + ] + multi = len(self._reg_groups) > 1 + lines.append(f'{hdname}State *s = {uname}(obj);') + lines.append('') + if not multi: + lines.append( + f'memory_region_init_io(&s->mmio, obj, &{dname}_regs_ops, s,') + lines.append( + f' TYPE_{uname}, REGS_SIZE);') + else: + # need to map up to the very last register + aperture = self._registers[-1].address + self._regwidth // 8 + # apertures are defined as power-of-2 + aperture = 1 << (aperture - 1).bit_length() + lines.append(f'#define {uname}_APERTURE 0x{aperture:x}u') + lines.append('') + lines.append( + f'memory_region_init(&s->mmio, obj, TYPE_{uname} "-regs",') + lines.append( + f' {uname}_APERTURE);') + lines.append('sysbus_init_mmio(SYS_BUS_DEVICE(s), &s->mmio);') + lines.append('') + if multi: + for group in self._reg_groups: + lgroup = group.lower() + lines.append( + f'memory_region_init_io(&s->mmio_{lgroup}, obj, ' + f'&{dname}_{lgroup}_ops, s,' + ) + lines.append( + f' TYPE_{uname} "-regs-{lgroup}", ' + f'REGS_{group}_SIZE);' + ) + lines.append( + f'memory_region_add_subregion(&s->mmio, REGS_{group}_BASE, ' + f'&s->mmio_{lgroup});' + ) + lines.append('') + for group in self._reg_groups: + regname = f'regs_{group.lower()}' if group else 'regs' + lines.append(f's->{regname} = g_new0(uint{self._regwidth}_t, ' + f'{regname.upper()}_COUNT);') + lines.append('') + if self._irq_count: + lines.append('for (unsigned ix = 0; ix < NUM_IRQS; ix++) {') + lines.append(' ibex_sysbus_init_irq(obj, &s->irqs[ix]);') + lines.append('}') + if self._alert_count: + lines.append('') + lines.append('for (unsigned ix = 0; ix < NUM_ALERTS; ix++) {') + lines.append(' ibex_qdev_init_irq(obj, &s->alerts[ix], ' + 'OT_DEVICE_ALERT);') + lines.append('}') + lines.append('}') + last_line = len(lines) + for lno, line in enumerate(lines, 1): + if lno in (1, 2, last_line) or line.startswith('#'): + print(line, file=tfp) + else: + print(f' {line}'.rstrip(), file=tfp) + print(file=tfp) def _generate_class_init(self, tfp: TextIO) -> None: dname = self._devname @@ -1062,7 +1260,7 @@ def generate_struct(self, tfp: TextIO) -> None: ''' print(redent(code), file=tfp) - def _generate_registers(self, nibcount: int, tfp: TextIO) -> None: + def _generate_registers(self, tfp: TextIO) -> None: dname = self._devname hdname = dname.title().replace('_', '') print('register_structs! {', file=tfp) @@ -1086,6 +1284,7 @@ def _generate_registers(self, nibcount: int, tfp: TextIO) -> None: reg = None address = 0 rsvcnt = 0 + nibcount = self._nibcount for reg in self._registers: if reg.address > address: rsvcnt += 1 @@ -1175,6 +1374,10 @@ def main(): params.add_argument('-n', '--name', help='device name') params.add_argument('-p', '--ignore', metavar='PREFIX', help='ignore register/fields starting with prefix') + params.add_argument('-S', '--segment', metavar='REGNAME:SEGMENT', + action='append', default=[], + help='Creage a segment on specified register ' + '(may be repeated)') params.add_argument('-r', '--reset', action='append', choices=AutoReg.RESETS, help='generate reset code') @@ -1197,20 +1400,24 @@ def main(): log = configure_loggers(args.verbose, 'autoreg')[0] reggen = autoregs[args.out_kind] - name = args.name or splitext(basename(args.config.name))[0] + name = args.name or f'ot_{splitext(basename(args.config.name))[0]}' areg = reggen(name, args.ignore, args.reset) if args.copyright: areg.copyright = args.copyright areg.load(args.config, args.win_limit) if args.generate: + areg.segment(args.segment) for gen in args.generate: generate = getattr(areg, f'generate_{gen}', None) if not generate: - argparser.error(f'{gen} is not supported for {args.out_kind}') + argparser.error(f'{gen} is not supported for ' + f'{args.out_kind}') generate(args.output or sys.stdout) else: log.warning('No generation requested') + except ArgError as exc: + argparser.error(str(exc)) except (IOError, ValueError, ImportError) as exc: print(f'\nError: {exc}', file=sys.stderr) if debug: From 20697551bd065b8ea57e6b813be241e1a98e1f3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20P=C3=B3=C5=82ch=C5=82opek?= Date: Thu, 23 Oct 2025 14:15:25 +0200 Subject: [PATCH 8/8] [ot] scripts/opentitan: autoreg.py: fix non-numeric bits field issue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In opentitan repo the "bits" field for register in hjson for specific IP should be text value, like: ... { bits: "0" name: "increment" resval: 0x0 ... If there is a mistake and "bits" field is integer: ... { bits: 0 name: "increment" resval: 0x0 ... the scripts/autoreg.py script fails. The typo should be fixed in OpenTitan repo but add small prevention to handle such cases. Signed-off-by: Mateusz Półchłopek --- scripts/opentitan/autoreg.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/opentitan/autoreg.py b/scripts/opentitan/autoreg.py index b801eb636589..247c1d86a0af 100755 --- a/scripts/opentitan/autoreg.py +++ b/scripts/opentitan/autoreg.py @@ -404,7 +404,7 @@ def _parse_reg_fields(self, reg: dict[str, Any]) -> list[AutoField]: continue desc = field.get('desc') bits = field.get('bits') - if ':' in bits: + if isinstance(bits, str) and ':' in bits: hibit, lobit = (self._parse_bits(x) for x in bits.split(':')) offset = lobit width = hibit-lobit+1 @@ -445,7 +445,7 @@ def _parse_reg_fields(self, reg: dict[str, Any]) -> list[AutoField]: f'0x{bitmask:0{nibcount}x}') return fields - def _parse_bits(self, value: str) -> int: + def _parse_bits(self, value: Union[int, str]) -> int: try: # simple integer return int(value)