diff --git a/.gitignore b/.gitignore index 2376557b..26a4d23b 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,5 @@ __pycache__/ .doit.db docs/_build +docs/autoapi .cache diff --git a/chipflow_lib/common/sim/main.cc.jinja b/chipflow_lib/common/sim/main.cc.jinja new file mode 100644 index 00000000..cd72b861 --- /dev/null +++ b/chipflow_lib/common/sim/main.cc.jinja @@ -0,0 +1,78 @@ +#undef NDEBUG + +#include +#include +#include "sim_soc.h" +{% for include in includes %} +#include "{{include}}" +{% endfor %} + +#include +#include + +using namespace cxxrtl::time_literals; +using namespace cxxrtl_design; +using namespace chipflow; + +int main(int argc, char **argv) { + p_sim__top top; + + {% for initialiser in initialisers %} + {{initialiser}}; + {% endfor %} + + cxxrtl::agent agent(cxxrtl::spool("spool.bin"), top); + if (getenv("DEBUG")) // can also be done when a condition is violated, etc + std::cerr << "Waiting for debugger on " << agent.start_debugging() << std::endl; + + open_event_log(BUILD_DIR "/sim/events.json"); + open_input_commands(PROJECT_ROOT "/design/tests/input.json"); + + unsigned timestamp = 0; + auto tick = [&]() { + {% for interface in interfaces %} + {{interface}}.step(timestamp); + {% endfor %} + + // FIXME: Currently we tick all clocks together, this need fixing.. + {% for clock in clocks %} + top.{{clock}}.set(false); + {% endfor %} + agent.step(); + agent.advance(1_us); + ++timestamp; + + {% for clock in clocks %} + top.{{clock}}.set(true); + {% endfor %} + agent.step(); + agent.advance(1_us); + ++timestamp; + + // if (timestamp == 10) + // agent.breakpoint(CXXRTL_LOCATION); + }; + + {% for data in data_load %} + {{data.model_name}}.load_data("{{data.file_name}}", {{data.args | join(', ')}}); + {% endfor %} + + agent.step(); + agent.advance(1_us); + + {% for reset in resets %} + top.{{reset}}.set(false); + {% endfor %} + + tick(); + + {% for reset in resets %} + top.{{reset}}.set(true); + {% endfor %} + + for (int i = 0; i < {{num_steps}}; i++) + tick(); + + close_event_log(); + return 0; +} diff --git a/chipflow_lib/common/sim/models.cc b/chipflow_lib/common/sim/models.cc index 171dc21b..213aaeb6 100644 --- a/chipflow_lib/common/sim/models.cc +++ b/chipflow_lib/common/sim/models.cc @@ -9,7 +9,7 @@ #include #include "models.h" -namespace cxxrtl_design { +namespace chipflow { // Helper functions @@ -134,8 +134,10 @@ void close_event_log() { } } +namespace models { + // SPI flash -void spiflash_model::load_data(const std::string &filename, unsigned offset) { +void spiflash::load_data(const std::string &filename, unsigned offset) { std::ifstream in(filename, std::ifstream::binary); if (offset >= data.size()) { throw std::out_of_range("flash: offset beyond end"); @@ -145,7 +147,7 @@ void spiflash_model::load_data(const std::string &filename, unsigned offset) { } in.read(reinterpret_cast(data.data() + offset), (data.size() - offset)); } -void spiflash_model::step(unsigned timestamp) { +void spiflash::step(unsigned timestamp) { auto process_byte = [&]() { s.out_buffer = 0; if (s.byte_count == 0) { @@ -221,7 +223,7 @@ void spiflash_model::step(unsigned timestamp) { // UART -void uart_model::step(unsigned timestamp) { +void uart::step(unsigned timestamp) { for (auto action : get_pending_actions(name)) { if (action.event == "tx") { @@ -274,41 +276,8 @@ void uart_model::step(unsigned timestamp) { } } -// GPIO - -void gpio_model::step(unsigned timestamp) { - uint32_t o_value = o.get(); - uint32_t oe_value = oe.get(); - - for (auto action : get_pending_actions(name)) { - if (action.event == "set") { - auto bin = std::string(action.payload); - input_data = 0; - for (unsigned i = 0; i < width; i++) { - if (bin.at((width - 1) - i) == '1') - input_data |= (1U << i); - } - } - } - - if (o_value != s.o_last || oe_value != s.oe_last) { - std::string formatted_value; - for (int i = width - 1; i >= 0; i--) { - if (oe_value & (1U << unsigned(i))) - formatted_value += (o_value & (1U << unsigned(i))) ? '1' : '0'; - else - formatted_value += 'Z'; - } - log_event(timestamp, name, "change", json(formatted_value)); - } - - i.set((input_data & ~oe_value) | (o_value & oe_value)); - s.o_last = o_value; - s.oe_last = oe_value; -} - // Generic SPI model -void spi_model::step(unsigned timestamp) { +void spi::step(unsigned timestamp) { for (auto action : get_pending_actions(name)) { if (action.event == "set_data") { s.out_buffer = s.send_data = uint32_t(action.payload); @@ -341,7 +310,7 @@ void spi_model::step(unsigned timestamp) { } // Generic I2C model -void i2c_model::step(unsigned timestamp) { +void i2c::step(unsigned timestamp) { bool sda = !bool(sda_oe), scl = !bool(scl_oe); for (auto action : get_pending_actions(name)) { @@ -403,4 +372,5 @@ void i2c_model::step(unsigned timestamp) { scl_i.set(scl); } -} +} //chipflow::models +} //chipflow diff --git a/chipflow_lib/common/sim/models.h b/chipflow_lib/common/sim/models.h index 0af3ded8..8147bddc 100644 --- a/chipflow_lib/common/sim/models.h +++ b/chipflow_lib/common/sim/models.h @@ -10,7 +10,7 @@ #include "vendor/nlohmann/json.hpp" -namespace cxxrtl_design { +namespace chipflow { using namespace cxxrtl; @@ -30,9 +30,12 @@ void log_event(unsigned timestamp, const std::string &peripheral, const std::str std::vector get_pending_actions(const std::string &peripheral); void close_event_log(); -struct spiflash_model { +namespace models { + + +struct spiflash { std::string name; - spiflash_model(const std::string &name, const value<1> &clk, const value<1> &csn, const value<4> &d_o, const value<4> &d_oe, value<4> &d_i) : + spiflash(const std::string &name, const value<1> &clk, const value<1> &csn, const value<4> &d_o, const value<4> &d_oe, value<4> &d_i) : name(name), clk(clk), csn(csn), d_o(d_o), d_oe(d_oe), d_i(d_i) { data.resize(16*1024*1024); std::fill(data.begin(), data.end(), 0xFF); // flash starting value @@ -61,9 +64,9 @@ struct spiflash_model { } s; }; -struct uart_model { +struct uart { std::string name; - uart_model(const std::string &name, const value<1> &tx, value<1> &rx, unsigned baud_div = 25000000/115200) : name(name), tx(tx), rx(rx), baud_div(baud_div) {}; + uart(const std::string &name, const value<1> &tx, value<1> &rx, unsigned baud_div = 25000000/115200) : name(name), tx(tx), rx(rx), baud_div(baud_div) {}; void step(unsigned timestamp); private: @@ -82,18 +85,19 @@ struct uart_model { } s; }; -struct gpio_model { - static constexpr unsigned width = 8; +template +struct gpio { std::string name; - gpio_model(const std::string &name, const value &o, const value &oe, value &i) : name(name), o(o), oe(oe), i(i) {}; + + gpio(const std::string &name, const value &o, const value &oe, value &i) : name(name), o(o), oe(oe), i(i) {}; void step(unsigned timestamp); private: uint32_t input_data = 0; - const value &o; - const value &oe; - value &i; + const value &o; + const value &oe; + value &i; struct { uint32_t o_last = 0; uint32_t oe_last = 0; @@ -101,9 +105,42 @@ struct gpio_model { }; -struct spi_model { +// GPIO +template +void gpio::step(unsigned timestamp) { + uint32_t o_value = o.template get(); + uint32_t oe_value = oe.template get(); + + for (auto action : get_pending_actions(name)) { + if (action.event == "set") { + auto bin = std::string(action.payload); + input_data = 0; + for (unsigned i = 0; i < pin_count; i++) { + if (bin.at((pin_count - 1) - i) == '1') + input_data |= (1U << i); + } + } + } + + if (o_value != s.o_last || oe_value != s.oe_last) { + std::string formatted_value; + for (int i = pin_count - 1; i >= 0; i--) { + if (oe_value & (1U << unsigned(i))) + formatted_value += (o_value & (1U << unsigned(i))) ? '1' : '0'; + else + formatted_value += 'Z'; + } + log_event(timestamp, name, "change", json(formatted_value)); + } + + i.set((input_data & ~oe_value) | (o_value & oe_value)); + s.o_last = o_value; + s.oe_last = oe_value; +} + +struct spi { std::string name; - spi_model(const std::string &name, const value<1> &clk, const value<1> &csn, const value<1> &copi, value<1> &cipo) : + spi(const std::string &name, const value<1> &clk, const value<1> &copi, value<1> &cipo, const value<1> &csn) : name(name), clk(clk), csn(csn), copi(copi), cipo(cipo) { }; @@ -125,9 +162,9 @@ struct spi_model { } s; }; -struct i2c_model { +struct i2c { std::string name; - i2c_model(const std::string &name, const value<1> &sda_oe, value<1> &sda_i, const value<1> &scl_oe, value<1> &scl_i) : name(name), sda_oe(sda_oe), sda_i(sda_i), scl_oe(scl_oe), scl_i(scl_i) {}; + i2c(const std::string &name, const value<1> &scl_o, const value<1> &scl_oe, value<1> &scl_i, const value<1> &sda_o, const value<1> &sda_oe, value<1> &sda_i) : name(name), sda_oe(sda_oe), sda_i(sda_i), scl_oe(scl_oe), scl_i(scl_i) {}; void step(unsigned timestamp); private: @@ -150,6 +187,7 @@ struct i2c_model { }; -} +} //chipflow::simulation +} //chipflow #endif diff --git a/chipflow_lib/config_models.py b/chipflow_lib/config_models.py index 8c1cd417..bbfc1f21 100644 --- a/chipflow_lib/config_models.py +++ b/chipflow_lib/config_models.py @@ -19,6 +19,8 @@ class SiliconConfig(BaseModel): debug: Optional[Dict[str, bool]] = None # This is still kept around to allow forcing pad locations. +class SimulationConfig(BaseModel): + num_steps: int = 3000000 class ChipFlowConfig(BaseModel): """Root configuration for chipflow.toml.""" @@ -26,6 +28,7 @@ class ChipFlowConfig(BaseModel): top: Dict[str, Any] = {} steps: Optional[Dict[str, str]] = None silicon: Optional[SiliconConfig] = None + simulation: SimulationConfig = SimulationConfig() clock_domains: Optional[List[str]] = None diff --git a/chipflow_lib/platforms/__init__.py b/chipflow_lib/platforms/__init__.py index f4f6fefa..4d98e554 100644 --- a/chipflow_lib/platforms/__init__.py +++ b/chipflow_lib/platforms/__init__.py @@ -14,11 +14,17 @@ ) from ._packages import PACKAGE_DEFINITIONS from ._sky130 import Sky130DriveMode +from ._signatures import ( + JTAGSignature, SPISignature, I2CSignature, UARTSignature, GPIOSignature, QSPIFlashSignature, + attach_simulation_data + ) __all__ = ['IO_ANNOTATION_SCHEMA', 'IOSignature', 'IOModel', 'IOModelOptions', 'IOTripPoint', 'OutputIOSignature', 'InputIOSignature', 'BidirIOSignature', 'SiliconPlatformPort', 'SiliconPlatform', 'SimPlatform', + 'JTAGSignature', 'SPISignature', 'I2CSignature', 'UARTSignature', 'GPIOSignature', 'QSPIFlashSignature', + 'attach_simulation_data', 'Sky130DriveMode', 'PACKAGE_DEFINITIONS'] diff --git a/chipflow_lib/platforms/_annotate.py b/chipflow_lib/platforms/_annotate.py new file mode 100644 index 00000000..2dbeef1f --- /dev/null +++ b/chipflow_lib/platforms/_annotate.py @@ -0,0 +1,78 @@ +from types import MethodType +import pydantic +from typing import TypeVar +from typing_extensions import is_typeddict +_T_TypedDict = TypeVar('_T_TypedDict') + +def amaranth_annotate(modeltype: type['_T_TypedDict'], schema_id: str, member='__chipflow_annotation__', decorate_object = False): + if not is_typeddict(modeltype): + raise TypeError(f'''amaranth_annotate must be passed a TypedDict, not {modeltype}''') + + # interesting pydantic issue gets hit if arbitrary_types_allowed is False + if hasattr(modeltype, '__pydantic_config__'): + config = getattr(modeltype, '__pydantic_config__') + config['arbitrary_types_allowed'] = True + else: + config = pydantic.ConfigDict() + config['arbitrary_types_allowed'] = True + setattr(modeltype, '__pydantic_config__', config) + PydanticModel = pydantic.TypeAdapter(modeltype) + + def annotation_schema(): + schema = PydanticModel.json_schema() + schema['$schema'] = 'https://json-schema.org/draft/2020-12/schema' + schema['$id'] = schema_id + return schema + + class Annotation: + 'Generated annotation class' + schema = annotation_schema() + + def __init__(self, parent): + self.parent = parent + + def origin(self): + return self.parent + + def as_json(self): + return PydanticModel.dump_python(getattr(self.parent, member)) + + def decorate_class(klass): + if hasattr(klass, 'annotations'): + old_annotations = klass.annotations + else: + old_annotations = None + + def annotations(self, obj): + if old_annotations: + annotations = old_annotations(self, obj) + else: + annotations = super(klass, obj).annotations(obj) + annotation = Annotation(self) + return annotations + (annotation,) + + klass.annotations = annotations + return klass + + def decorate_obj(obj): + if hasattr(obj, 'annotations'): + old_annotations = obj.annotations + else: + old_annotations = None + + def annotations(self = None, origin = None): + if old_annotations: + annotations = old_annotations(origin) + else: + annotations = super(obj.__class__, obj).annotations(obj) + annotation = Annotation(self) + return annotations + (annotation,) + + setattr(obj, 'annotations', MethodType(annotations, obj)) + return obj + + if decorate_object: + return decorate_obj + else: + return decorate_class + diff --git a/chipflow_lib/platforms/_signatures.py b/chipflow_lib/platforms/_signatures.py new file mode 100644 index 00000000..fdfda458 --- /dev/null +++ b/chipflow_lib/platforms/_signatures.py @@ -0,0 +1,130 @@ +# SPDX-License-Identifier: BSD-2-Clause + +import re +from typing import List, Tuple, Any +from typing_extensions import Unpack, TypedDict + +from amaranth.lib import wiring +from amaranth.lib.wiring import Out + +from ._utils import InputIOSignature, OutputIOSignature, BidirIOSignature, IOModelOptions, _chipflow_schema_uri +from ._annotate import amaranth_annotate + +SIM_ANNOTATION_SCHEMA = str(_chipflow_schema_uri("simulatable-interface", 0)) +SIM_DATA_SCHEMA = str(_chipflow_schema_uri("simulatable-data", 0)) + +class SimInterface(TypedDict): + uid: str + parameters: List[Tuple[str, Any]] + +class SimData(TypedDict): + file_name: str + offset: int + +_VALID_UID = re.compile('[a-zA-Z_.]').search + +def _unpack_dict(d: dict) -> str: + params = [ f"{k}={repr(v)}" for k,v in d.items()] + return ', '.join(params) + +""" +Attributes: + __chipflow_parameters__: list of tuples (name, value). + It is expected that a model that takes parameters is implmemted as a template, with the parameters in the order + given. +""" +def simulatable_interface(base="com.chipflow.chipflow_lib"): + def decorate(klass): + assert _VALID_UID(base) + dec = amaranth_annotate(SimInterface, SIM_ANNOTATION_SCHEMA) + klass = dec(klass) + + def new_init(self,*args, **kwargs): + original_init(self, *args, **kwargs) + self.__chipflow_annotation__ = { + "uid": klass.__chipflow_uid__, + "parameters": self.__chipflow_parameters__(), + } + + def repr(self) -> str: + return f"{klass.__name__}({_unpack_dict(self.__chipflow_parameters__())}, {_unpack_dict(self._options)})" + + original_init = klass.__init__ + klass.__init__ = new_init + klass.__chipflow_uid__ = f"{base}.{klass.__name__}" + if not hasattr(klass, '__chipflow_parameters__'): + klass.__chipflow_parameters__ = lambda self: [] + if not klass.__repr__: + klass.__repr__ = repr + return klass + return decorate + + +@simulatable_interface() +class JTAGSignature(wiring.Signature): + def __init__(self, **kwargs: Unpack[IOModelOptions]): + super().__init__({ + "trst": Out(InputIOSignature(1)), + "tck": Out(InputIOSignature(1)), + "tms": Out(InputIOSignature(1)), + "tdi": Out(InputIOSignature(1)), + "tdo": Out(OutputIOSignature(1)), + }) + + +@simulatable_interface() +class SPISignature(wiring.Signature): + def __init__(self, **kwargs: Unpack[IOModelOptions]): + super().__init__({ + "sck": Out(OutputIOSignature(1)), + "copi": Out(OutputIOSignature(1)), + "cipo": Out(InputIOSignature(1)), + "csn": Out(OutputIOSignature(1)), + }) + +@simulatable_interface() +class QSPIFlashSignature(wiring.Signature): + def __init__(self, **kwargs: Unpack[IOModelOptions]): + super().__init__({ + "clk": Out(OutputIOSignature(1)), + "csn": Out(OutputIOSignature(1)), + "d": Out(BidirIOSignature(4, individual_oe=True)), + }) + +@simulatable_interface() +class UARTSignature(wiring.Signature): + def __init__(self, **kwargs: Unpack[IOModelOptions]): + super().__init__({ + "tx": Out(OutputIOSignature(1)), + "rx": Out(InputIOSignature(1)), + }) + +@simulatable_interface() +class I2CSignature(wiring.Signature): + def __init__(self, **kwargs: Unpack[IOModelOptions]): + super().__init__({ + "scl": Out(BidirIOSignature(1)), + "sda": Out(BidirIOSignature(1)) + }) + self._options = kwargs + + +@simulatable_interface() +class GPIOSignature(wiring.Signature): + + def __init__(self, pin_count=1, **kwargs: Unpack[IOModelOptions]): + self._pin_count = pin_count + self._options = kwargs + kwargs['individual_oe'] = True + super().__init__({ + "gpio": Out(BidirIOSignature(pin_count, **kwargs)) + }) + + def __chipflow_parameters__(self): + return [('pin_count',self._pin_count)] + + +def attach_simulation_data(c: wiring.Component, **kwargs: Unpack[SimData]): + setattr(c.signature, '__chipflow_simulation_data__', kwargs) + amaranth_annotate(SimData, SIM_DATA_SCHEMA, '__chipflow_simulation_data__', decorate_object=True)(c.signature) + diff --git a/chipflow_lib/platforms/_utils.py b/chipflow_lib/platforms/_utils.py index d15f6a12..a1fd54d9 100644 --- a/chipflow_lib/platforms/_utils.py +++ b/chipflow_lib/platforms/_utils.py @@ -21,16 +21,17 @@ ) -from amaranth.lib import wiring, io, meta +from amaranth.lib import wiring, io from amaranth.lib.wiring import In, Out from pydantic import ( - ConfigDict, TypeAdapter, PlainSerializer, + ConfigDict, PlainSerializer, WrapValidator ) from .. import ChipFlowError, _ensure_chipflow_root, _get_cls_by_reference from .._appresponse import AppResponseModel, OmitIfNone +from ._annotate import amaranth_annotate from ._sky130 import Sky130DriveMode if TYPE_CHECKING: @@ -83,7 +84,6 @@ class IOTripPoint(StrEnum): IO_ANNOTATION_SCHEMA = str(_chipflow_schema_uri("pin-annotation", 0)) -@pydantic.with_config(ConfigDict(arbitrary_types_allowed=True)) # type: ignore[reportCallIssue] class IOModelOptions(TypedDict): """ Options for an IO pad/pin. @@ -130,32 +130,8 @@ class IOModel(IOModelOptions): width: int direction: Annotated[io.Direction, PlainSerializer(lambda x: x.value)] -def io_annotation_schema(): - class Model(pydantic.BaseModel): - data_td: IOModel - - PydanticModel = TypeAdapter(IOModel) - schema = PydanticModel.json_schema() - schema['$schema'] = "https://json-schema.org/draft/2020-12/schema" - schema['$id'] = IO_ANNOTATION_SCHEMA - return schema - - -class _IOAnnotation(meta.Annotation): - "Infrastructure for `Amaranth annotations `" - schema = io_annotation_schema() - - def __init__(self, model:IOModel): - self._model = model - - @property - def origin(self): # type: ignore - return self._model - - def as_json(self): # type: ignore - return TypeAdapter(IOModel).dump_python(self._model) - +@amaranth_annotate(IOModel, IO_ANNOTATION_SCHEMA, '_model') class IOSignature(wiring.Signature): """An :py:obj:`Amaranth Signature ` used to decorate wires that would usually be brought out onto a port on the package. This class is generally not directly used. Instead, you would typically utilize the more specific @@ -225,13 +201,6 @@ def options(self) -> IOModelOptions: """ return self._model - def annotations(self, *args): # type: ignore - annotations = wiring.Signature.annotations(self, *args) # type: ignore - - io_annotation = _IOAnnotation(self._model) - return annotations + (io_annotation,) # type: ignore - - def __repr__(self): return f"IOSignature({','.join('{0}={1!r}'.format(k,v) for k,v in self._model.items())})" @@ -372,17 +341,17 @@ def _group_consecutive_items(ordering: PinList, lst: PinList) -> OrderedDict[int last = lst[0] current_group = [last] - logger.debug(f"_group_consecutive_items starting with {current_group}") + #logger.debug(f"_group_consecutive_items starting with {current_group}") for item in lst[1:]: idx = ordering.index(last) next = ordering[idx + 1] if idx < len(ordering) - 1 else None - logger.debug(f"inspecting {item}, index {idx}, next {next}") + #logger.debug(f"inspecting {item}, index {idx}, next {next}") if item == next: current_group.append(item) - logger.debug("found consecutive, adding to current group") + #logger.debug("found consecutive, adding to current group") else: - logger.debug("found nonconsecutive, creating new group") + #logger.debug("found nonconsecutive, creating new group") grouped.append(current_group) current_group = [item] last = item @@ -624,6 +593,7 @@ def register_component(self, name: str, component: wiring.Component) -> None: component: Amaranth `wiring.Component` to allocate """ + print(f"registering {component}") self._components[name] = component self._interfaces[name] = component.metadata.as_json() diff --git a/chipflow_lib/platforms/silicon.py b/chipflow_lib/platforms/silicon.py index b2261e67..d154f580 100644 --- a/chipflow_lib/platforms/silicon.py +++ b/chipflow_lib/platforms/silicon.py @@ -303,6 +303,8 @@ def instantiate_toplevel(self): def wire_up(self, m, wire): super().wire_up(m, wire) + # wire up drive mode bits + if hasattr(wire, 'drive_mode'): m.d.comb += self.drive_mode.eq(wire.drive_mode) diff --git a/chipflow_lib/platforms/sim.py b/chipflow_lib/platforms/sim.py index 56b5701f..16892fb5 100644 --- a/chipflow_lib/platforms/sim.py +++ b/chipflow_lib/platforms/sim.py @@ -3,36 +3,172 @@ import logging import os import sys + +from dataclasses import dataclass +from enum import StrEnum from pathlib import Path +from typing import Dict, List, Optional, Type -from amaranth import Module, ClockDomain, ClockSignal, ResetSignal -from amaranth.lib import io +from amaranth import Module, ClockSignal, ResetSignal, ClockDomain +from amaranth.lib import io, wiring from amaranth.back import rtlil # type: ignore[reportAttributeAccessIssue] from amaranth.hdl._ir import PortDirection from amaranth.lib.cdc import FFSynchronizer +from jinja2 import Environment, PackageLoader, select_autoescape +from pydantic import BaseModel -from ._utils import load_pinlock +from .. import ChipFlowError, _ensure_chipflow_root +from ._signatures import ( + I2CSignature, GPIOSignature, UARTSignature, SPISignature, QSPIFlashSignature, + SIM_ANNOTATION_SCHEMA, SIM_DATA_SCHEMA, SimInterface + ) +from ._utils import load_pinlock, Interface -__all__ = ["SimPlatform"] logger = logging.getLogger(__name__) +__all__ = ["SimPlatform", "BasicCxxBuilder"] -class SimPlatform: +class SimModelCapability(StrEnum): + LOAD_DATA = "load-data" + + +@dataclass +class SimModel: + """ + Description of a model available from a Builder + Attributes: + name: the model name + signature: the wiring connection of the model. This is also used to match with interfaces. + capabilities: List of capabilities of the model. + """ + name: str + namespace: str + signature: Type[wiring.Signature] + capabilities: Optional[List[SimModelCapability]] = None + + def __post_init__(self): + if not hasattr(self.signature, '__chipflow_uid__'): + raise ChipFlowError(f"Signature {self.signature} must be decorated with `sim_annotate()` to use as a simulation model identifier") + + +def cxxrtlmangle(name, ispublic=True): + # RTLIL allows any characters in names other than whitespace. This presents an issue for generating C++ code + # because C++ identifiers may be only alphanumeric, cannot clash with C++ keywords, and cannot clash with cxxrtl + # identifiers. This issue can be solved with a name mangling scheme. We choose a name mangling scheme that results + # in readable identifiers, does not depend on an up-to-date list of C++ keywords, and is easy to apply. Its rules: + # 1. All generated identifiers start with `_`. + # 1a. Generated identifiers for public names (beginning with `\`) start with `p_`. + # 1b. Generated identifiers for internal names (beginning with `$`) start with `i_`. + # 2. An underscore is escaped with another underscore, i.e. `__`. + # 3. Any other non-alnum character is escaped with underscores around its lowercase hex code, e.g. `@` as `_40_`. + out = '' + if name.startswith('\\'): + out = 'p_' + name = name[1:] + elif name.startswith('$'): + out = 'i_' + name = name[1:] + elif ispublic: + out = 'p_' + for c in name: + if c.isalnum(): + out += c + elif c == '_': + out += '__' + else: + out += f'_{ord(c):x}_' + return out + + +class BasicCxxBuilder(BaseModel): + """ + Represents an object built from C++, where the compilation is simply done with a collection of + cpp and hpp files, simply compiled and linked together with no dependencies + + Assumes model name corresponds to the c++ class name and that the class constructors take + a name followed by the wires of the interface. + + Attributes: + cpp_files: C++ files used to define the model + hpp_files: C++ header files to define the model interfaces + """ + models: List[SimModel] + cpp_files: List[Path] + hpp_files: Optional[List[Path]] = None + hpp_dirs: Optional[List[Path]] = None + + def model_post_init(self, *args, **kwargs): + self._table = { getattr(m.signature,'__chipflow_uid__'): m for m in self.models } + + def instantiate_model(self, interface: str, sim_interface: SimInterface, interface_desc: Interface, ports: Dict[str, io.SimulationPort]) -> str: + uid = sim_interface['uid'] + parameters = sim_interface['parameters'] + assert uid in self._table + + model = self._table[uid] + sig = model.signature(**dict(parameters)) + members = list(sig.flatten(sig.create())) + + sig_names = [ path for path, _, _ in members ] + port_names = { n: interface_desc[n].port_name for n in interface_desc.keys()} + + names = [f"\\io${port_names[str(n)]}${d}" for n,d in sig_names] + params = [f"top.{cxxrtlmangle(n)}" for n in names] + cpp_class = f"{model.namespace}::{model.name}" + if len(parameters): + template_params = [] + for p,v in parameters: + template_params.append(f"{v}") + cpp_class = cpp_class + '<' + ', '.join(template_params) + '>' + out = f"{cpp_class} {interface}(\"{interface}\", " + out += ', '.join(list(params)) + out += ')\n' + return out + +def find_builder(builders: List[BasicCxxBuilder], sim_interface: SimInterface): + uid = sim_interface['uid'] + for b in builders: + if uid in b._table: + return b + logger.warn(f"Unable to find builder for '{uid}'") + return None + + +_COMMON_BUILDER = BasicCxxBuilder( + models=[ + SimModel('spi', 'chipflow::models', SPISignature), + SimModel('spiflash', 'chipflow::models', QSPIFlashSignature, [SimModelCapability.LOAD_DATA]), + SimModel('uart', 'chipflow::models', UARTSignature), + SimModel('i2c', 'chipflow::models', I2CSignature), + SimModel('gpio', 'chipflow::models', GPIOSignature), + ], + cpp_files=[ Path('{COMMON_DIR}', 'models.cc') ], + hpp_files=[ Path('models.h') ], + hpp_dirs=[Path("{COMMON_DIR}")], + ) + + +class SimPlatform: def __init__(self, config): self.build_dir = os.path.join(os.environ['CHIPFLOW_ROOT'], 'build', 'sim') self.extra_files = dict() self.sim_boxes = dict() - self._ports = {} + self._ports: Dict[str, io.SimulationPort] = {} self._config = config + self._clocks = {} + self._resets = {} + self._builders: List[BasicCxxBuilder] = [ _COMMON_BUILDER ] + self._top_sim = {} + self._sim_data = {} def add_file(self, filename, content): if not isinstance(content, (str, bytes)): content = content.read() self.extra_files[filename] = content - def build(self, e): + def build(self, e, top): Path(self.build_dir).mkdir(parents=True, exist_ok=True) ports = [] @@ -44,7 +180,7 @@ def build(self, e): if port.direction is io.Direction.Bidir: ports.append((f"io${port_name}$oe", port.oe, PortDirection.Output)) - print("elaborating design") + print("Generating RTLIL from design") output = rtlil.convert(e, name="sim_top", ports=ports, platform=self) top_rtlil = Path(self.build_dir) / "sim_soc.il" @@ -67,6 +203,45 @@ def build(self, e): print("read_rtlil sim_soc.il", file=yosys_file) print("hierarchy -top sim_top", file=yosys_file) print("write_cxxrtl -header sim_soc.cc", file=yosys_file) + main = Path(self.build_dir) / "main.cc" + + metadata = {} + for key in top.keys(): + metadata[key] = getattr(e.submodules, key).metadata.as_json() + for component, iface in metadata.items(): + for interface, interface_desc in iface['interface']['members'].items(): + annotations = interface_desc['annotations'] + + if SIM_DATA_SCHEMA in annotations: + self._sim_data[interface] = annotations[SIM_DATA_SCHEMA] + + data_load = [] + for i,d in self._sim_data.items(): + args = [f"0x{d['offset']:X}U"] + p = Path(d['file_name']) + if not p.is_absolute(): + p = _ensure_chipflow_root() / p + data_load.append({'model_name': i, 'file_name': p, 'args': args}) + + + env = Environment( + loader=PackageLoader("chipflow_lib", "common/sim"), + autoescape=select_autoescape() + ) + template = env.get_template("main.cc.jinja") + + with main.open("w") as main_file: + print(template.render( + includes = [hpp for b in self._builders if b.hpp_files for hpp in b.hpp_files ], + initialisers = [exp for exp in self._top_sim.values()], + interfaces = [exp for exp in self._top_sim.keys()], + clocks = [cxxrtlmangle(f"io${clk}$i") for clk in self._clocks.keys()], + resets = [cxxrtlmangle(f"io${rst}$i") for rst in self._resets.keys()], + data_load = data_load, + num_steps = self._config.chipflow.simulation.num_steps, + ), + file=main_file) + def instantiate_ports(self, m: Module): if hasattr(self, "_pinlock"): @@ -74,11 +249,20 @@ def instantiate_ports(self, m: Module): pinlock = load_pinlock() for component, iface in pinlock.port_map.ports.items(): - for k, v in iface.items(): - for name, port_desc in v.items(): - logger.debug(f"Instantiating port {port_desc.port_name}: {port_desc}") - invert = port_desc.invert if port_desc.invert else False - self._ports[port_desc.port_name] = io.SimulationPort(port_desc.direction, port_desc.width, invert=invert, name=port_desc.port_name) + for interface, interface_desc in iface.items(): + for name, port_desc in interface_desc.items(): + logger.debug(f"Instantiating port {port_desc.port_name}: {port_desc}") + invert = port_desc.invert if port_desc.invert else False + self._ports[port_desc.port_name] = io.SimulationPort(port_desc.direction, port_desc.width, invert=invert, name=port_desc.port_name) + if component.startswith('_'): + continue + annotations = pinlock.metadata[component]['interface']['members'][interface]['annotations'] + + if SIM_ANNOTATION_SCHEMA in annotations: + sim_interface = annotations[SIM_ANNOTATION_SCHEMA] + builder = find_builder(self._builders, sim_interface) + if builder: + self._top_sim[interface] = builder.instantiate_model(interface, sim_interface, interface_desc, self._ports) for clock in pinlock.port_map.get_clocks(): assert 'clock_domain' in clock.iomodel @@ -88,6 +272,7 @@ def instantiate_ports(self, m: Module): clk_buffer = io.Buffer(clock.direction, self._ports[clock.port_name]) setattr(m.submodules, "clk_buffer_" + clock.port_name, clk_buffer) m.d.comb += ClockSignal().eq(clk_buffer.i) # type: ignore[reportAttributeAccessIssue] + self._clocks[clock.port_name] = self._ports[clock.port_name] for reset in pinlock.port_map.get_resets(): assert 'clock_domain' in reset.iomodel @@ -97,6 +282,7 @@ def instantiate_ports(self, m: Module): setattr(m.submodules, reset.port_name, rst_buffer) ffsync = FFSynchronizer(rst_buffer.i, ResetSignal()) # type: ignore[reportAttributeAccessIssue] setattr(m.submodules, reset.port_name + "_sync", ffsync) + self._resets[reset.port_name] = self._ports[reset.port_name] self._pinlock = pinlock @@ -114,7 +300,8 @@ def instantiate_ports(self, m: Module): "name": "build_sim", "actions": [ "{ZIG_CXX} {CXXFLAGS} {INCLUDES} {DEFINES} -o {OUTPUT_DIR}/sim_soc{EXE} " - "{OUTPUT_DIR}/sim_soc.cc {SOURCE_DIR}/main.cc {COMMON_DIR}/models.cc" + "{OUTPUT_DIR}/sim_soc.cc {OUTPUT_DIR}/main.cc " + + " ".join([str(p) for p in _COMMON_BUILDER.cpp_files]) ], "targets": [ "{OUTPUT_DIR}/sim_soc{EXE}" @@ -122,12 +309,10 @@ def instantiate_ports(self, m: Module): "file_dep": [ "{OUTPUT_DIR}/sim_soc.cc", "{OUTPUT_DIR}/sim_soc.h", - "{SOURCE_DIR}/main.cc", - "{COMMON_DIR}/models.cc", - "{COMMON_DIR}/models.h", + "{OUTPUT_DIR}/main.cc", "{COMMON_DIR}/vendor/nlohmann/json.hpp", "{COMMON_DIR}/vendor/cxxrtl/cxxrtl_server.h", - ], + ] + [str(p) for p in _COMMON_BUILDER.cpp_files], } SIM_CXXRTL = { diff --git a/chipflow_lib/software/soft_gen.py b/chipflow_lib/software/soft_gen.py index 310f35ae..891e148a 100644 --- a/chipflow_lib/software/soft_gen.py +++ b/chipflow_lib/software/soft_gen.py @@ -11,7 +11,7 @@ def __init__(self, *, rom_start, rom_size, ram_start, ram_size): self.defines = [] self.periphs = [] self.extra_init = [] - print("initialed SoftwareGenerator") + print("initialised SoftwareGenerator") def generate(self, out_dir): Path(out_dir).mkdir(parents=True, exist_ok=True) diff --git a/chipflow_lib/steps/sim.py b/chipflow_lib/steps/sim.py index 02cba580..b496d875 100644 --- a/chipflow_lib/steps/sim.py +++ b/chipflow_lib/steps/sim.py @@ -4,7 +4,6 @@ from contextlib import contextmanager from pathlib import Path -from pprint import pformat from doit.cmd_base import TaskLoader2, loader from doit.doit_cmd import DoitMain @@ -68,7 +67,6 @@ def load_tasks(self, cmd, pos_args): d[k.format(**self.subs)] = [i.format(**self.subs) for i in v] case _: raise ChipFlowError("Unexpected task definition") - print(f"adding task: {pformat(d)}") task_list.append(dict_to_task(d)) return task_list @@ -78,7 +76,7 @@ def __init__(self, config): self._config = config def build(self, *args): - print("building sim") + print("Building simulation...") m = Module() self._platform.instantiate_ports(m) @@ -95,7 +93,7 @@ def build(self, *args): _wire_up_ports(m, top, self._platform) #FIXME: common source for build dir - self._platform.build(m) + self._platform.build(m, top) with common() as common_dir, source() as source_dir, runtime() as runtime_dir: context = { "COMMON_DIR": common_dir, @@ -107,5 +105,5 @@ def build(self, *args): } for k,v in VARIABLES.items(): context[k] = v.format(**context) - print(f"substituting:\n{pformat(context)}") - DoitMain(ContextTaskLoader(DOIT_CONFIG, TASKS, context)).run(["build_sim"]) + if DoitMain(ContextTaskLoader(DOIT_CONFIG, TASKS, context)).run(["build_sim"]) !=0: + raise ChipFlowError("Failed building simulator") diff --git a/chipflow_lib/steps/software.py b/chipflow_lib/steps/software.py index 5cf8475f..984daefb 100644 --- a/chipflow_lib/steps/software.py +++ b/chipflow_lib/steps/software.py @@ -27,5 +27,5 @@ def doit_build(self): def build(self, *args): "Build the software for your design" - print("building software") + print("Building software...") self.doit_build()