Blueprints

This document is a guide to the usage and design of the Simics blueprint framework.

1 Introduction

The Simics configuration system consists of classes, objects and attributes on objects. To set up a configuration involving more than one object, some code must be written. There is typically a need to also share the configuration setup and let others add more objects to it in a controlled manner. Therefore, the problem that arises is that of creating re-usable parameterized sub-systems. These sub-systems typically correspond to hardware blocks, and hence this is a modelling problem.

Examples of sub-systems are:

In principle, sub-systems can be created in any way, but the type of problems that arise when modelling sub-systems are fundamentally different than when modelling devices, i.e. individual pieces of hardware. Experience has also shown that it is not natural or convenient to create sub-systems in the same style as devices, e.g. using DML or C.

Instead, experience has shown that Python is well-suited for this type of programming problem, and it is also convenient, since Simics includes Python as a built-in supported programming language, and most of the Simics API is available there.

To aid in sub-system modelling, and to standardise it, for easier sharing and other benefits, Simics includes a Python framework for sub-system modelling, called blueprints.

The two most basic requirements in sub-system modelling are the ability to “connect” different sub-systems to create larger systems, and the ability to parameterize a sub-system in order to facilitate re-use.

2 Motivation

To show how the blueprint framework can help, we can look at examples which create small Simics configurations.

Consider this small baseline configuration. We have a PCIe bus target with a virtio block device and associated image.

def create_pci_blk_device0(name):
    top = simics.pre_conf_object(name, 'namespace')
    top.mem = simics.pre_conf_object('memory-space')
    top.bus = simics.pre_conf_object('pcie-downstream-port',
                                     upstream_target=top.mem)
    top.blk = simics.pre_conf_object('virtio_pcie_blk',
                                     upstream_target=top.bus)
    top.blk.image = simics.pre_conf_object('image', size=1024)
    top.blk.attr.image = top.blk.image
    top.bus.devices = [[1, 0, top.blk]]
    simics.SIM_add_configuration([top], None)
simics> @create_pci_blk_device0("bus0")
simics> list-objects -tree namespace = bus0
┐
├ blk ┐
│     ├ image 
│     └ upstream_ingress_targets ┐
│                                ├ config 
│                                ├ io 
│                                ├ memory 
│                                └ message 
├ bus ┐
│     ├ cfg_space 
│     ├ io_space 
│     ├ mem_space 
│     └ msg_space 
└ mem 

This small configuration can be created easily using the Simics API directly, or even in the Simics static configuration format. But normally we need to extend and parameterise our configuration.

Consider an improved example. The target has four physical PCIe slots 0 - 3 corresponding to PCI device ID 1, 2, 5, and 8 on the bus. We also want to allow different sizes of the block device. Easy, we add two more parameters to the create-function:

def id_from_slot(slot):
    return [1, 2, 5, 8][slot]

def create_pci_blk_device1(name, size, pci_slot):
    top = simics.pre_conf_object(name, 'namespace')
    top.mem = simics.pre_conf_object('memory-space')
    top.bus = simics.pre_conf_object('pcie-downstream-port',
                                     upstream_target=top.mem)
    top.blk = simics.pre_conf_object('virtio_pcie_blk',
                                     upstream_target=top.bus)
    top.blk.image = simics.pre_conf_object('image', size=size)
    top.blk.attr.image = top.blk.image
    top.bus.devices = [[id_from_slot(pci_slot), 0, top.blk]]
    simics.SIM_add_configuration([top], None)
simics> @create_pci_blk_device1("bus1", 4096, 2)
simics> list-objects -tree namespace = bus1
┐
├ blk ┐
│     ├ image 
│     └ upstream_ingress_targets ┐
│                                ├ config 
│                                ├ io 
│                                ├ memory 
│                                └ message 
├ bus ┐
│     ├ cfg_space 
│     ├ io_space 
│     ├ mem_space 
│     └ msg_space 
└ mem 

Now consider an even more advanced case.

# Data about a single PCIe device
class PCIEDevice(NamedTuple):
    device_id: int
    function_id: int
    dev: ConfObject

# State shared between PCIe bus and PCIe device blueprints
class PCIEBusData(State):
    bus: ConfObject = None
    devices: list[PCIEDevice] = []

# Blueprint creating a PCIe bus
def pcie_bus(builder: Builder, name: Namespace, data: PCIEBusData):
    # Register creation of PCIe bus
    data.bus = builder.obj(name, "pcie-downstream-port",
                           upstream_target=name.mem,
                           devices=data.devices)
    # Register creation of associated devices
    builder.obj(name.mem, "memory-space")

# Blueprint for PCIe device
def pcie_device(builder: Builder, name: Namespace, data: PCIEBusData,
                size=0, device_id=0):
    # Register creation of PCIe device
    builder.obj(name, "virtio_pcie_blk", upstream_target=data.bus)
    builder.set(name.attr, image=name.image)
    data.devices.extend([PCIEDevice(device_id, 0, ConfObject(name))])
    # Register creation of associated devices
    builder.obj(name.image, "image", size=size)

# Whole system state
class Fabric(State):
    pcie_bus = PCIEBusData()

# Blueprint for whole system
def system(builder: Builder, name: Namespace):
    # Create whole system state
    fabric = builder.expose_state(name, Fabric)
    # Expose PCIe bus state
    builder.expose_state(name, fabric.pcie_bus)
    # Expand PCIe bus blueprint in the PCIe object hierarchy
    builder.expand(name, "pci", pcie_bus)
    # Expand PCIe device blueprint
    builder.expand(name, "blk", pcie_device,
                   size=4096, device_id=id_from_slot(2))
simics> @instantiate("bus2", system)
simics> list-objects -tree namespace = bus2
┐
├ blk ┐
│     ├ image 
│     └ upstream_ingress_targets ┐
│                                ├ config 
│                                ├ io 
│                                ├ memory 
│                                └ message 
└ pci ┐
      ├ cfg_space 
      ├ io_space 
      ├ mem 
      ├ mem_space 
      └ msg_space 

Notice how the creation of the PCI bus and the devices are separated into different blueprints. Hence we can replace any of them with a different variant, as long as it uses the same type of state. Only at the system level are all pieces put together.

3 Description

In this section we will describe the blueprint framework concepts, how they work, and how to use them.

3.1 Example

Consider the following small example of a blueprint.

class UARTState(State):
    uart: ConfObject = None
    console: ConfObject = None

def uart(builder: Builder, name: Namespace):
    con = builder.read_state(name, UARTState)
    con.uart = builder.obj(name, "NS16550", console=con.console)

def console(builder: Builder, name: Namespace):
    con = builder.read_state(name, UARTState)
    con.console = builder.obj(name, "textcon", device=con.uart,
                              recorder=name.recorder)
    builder.obj(name.recorder, "recorder")

def board(builder: Builder, name: Namespace):
    builder.expose_state(name, UARTState)
    builder.obj(name, "blueprint-namespace", info="UART example")
    builder.expand(name, "uart", uart)
    builder.expand(name, "console", console)
simics> @builder = expand("board", board)
simics> @builder.instantiate()
simics> list-objects -tree -with-class-name substr = board max-depth = 2
┐
└ board (blueprint-namespace) ┐
                              ├ console (textcon) 
                              └ uart (NS16550) 
Verify blueprint attribute value
board

The intention of this code is to obtain a Simics configuration with a text console and a UART object, pointing at each other via attributes. Exactly how this happens will become clear in the following sections.

The example illustrates three core concepts of the blueprint framework: the blueprints themselves, the State and the expansion.

3.2 Blueprint functions

The blueprints are Python functions. As can be seen in the example, they have some fixed parameters mandated by the framework but can also take other parameters if needed. The blueprint functions can:

In addition, a blueprint can provide information which can be extracted from the complete configuration.

Note that a blueprint describes how a specific part of the system should be built. A blueprint is not a representation of a part of the built system, i.e. the way the model is split into different blueprints have no direct connection to how the Simics configuration looks like in terms of its object hierarchy. The blueprints themselves do not leave any trace in the resulting configuration.

Note: A blueprint function is actually a pure function, without side-effects. At first glance, this is not readily apparent. However, the builder.expand statement is actually just a convenient way to compile a set (the “return value”) with objects and blueprints to be constructed.

3.3 Namespaces

Blueprint functions take a namespace parameter, usually called name. The namespace is the location in the Simics object hierarchy where the blueprint is expanded, i.e. the sub-tree where the blueprint is expected to register its objects, or expose or read State. The blueprint should not access the rest of the hierarchy.

The Namespace class is basically a string with some extra functionality to make it easy to create derived object names in a convenient manner, e.g.:

    ns = Namespace("qsp.system.mb")

    str(ns) == "qsp.system.mb"
    str(ns.kappa) == "qsp.system.mb.kappa"
    str(ns.kappa[1][2]) == "qsp.system.mb.kappa[1][2]"
    str(ns.dev@2) == f"qsp.system.mb.dev2"

3.4 State

The State class is used for defining the shared state used by the blueprints. As seen in the example, it is read or written by the blueprints involved and used to set attributes on the created Simics objects. The blueprint functions themselves have no state. A State structure should be thought of as a Python dict and is basically a pure data container.

Essentially, all attribute values on registered objects should come from State data members, if the value is not computed from other data members or if it is something local to a single blueprint. In the example above, the name of the recorder object is not part of any State since it is not needed by any other blueprint.

Blueprint functions do not have access to internals of other blueprints. In particular, a blueprint cannot access attributes of objects registered in another blueprint. All such accesses are done by having those values coming from a State structure. Only values defined as a data member of a State can be shared by multiple blueprints.

A state class often includes connectivity data, like in the example above, but can hold arbitrary data that needs to be passed between different blueprints.

In the example we define UARTState which carries the necessary state of an UART connection, namely the objects which are the end points of the connection: the UART device in the machine and the text console.

The ConfObject class used in the example is a trivial wrapper of the Namespace class, providing improved typing (for details, see the typing section).

As seen in the example, blueprints can both read and write state fields. At first glance, this seems to violate the principle that blueprints are pure functions without side effects. However, setting a State field actually results in a “return” value, where the returned value is an encoding that the field in a specific instance of the State sub-class should be set to a specific value. The state in question is not owned by the blueprint, but is handled by the framework.

The blueprints can create and pick up instances of the particular State sub-class that they need. The builder.expose_state() method creates an instance and makes it available in the sub-tree rooted at the specified namespace location. The builder.read_state() method looks for a state instance in the specified namespace location.

This hierarchical way of passing state implies:

3.5 Expansion

Blueprint expansion is the process of running the specified blueprint function (and any blueprints expanded recursively) at the specified namespace location and create a list of pre-objects from it. It is done via the global expand() function as well as builder.expand().

Expansion is iterative: since the blueprint framework handles all state, it can keep track of when the state changes, i.e. when the blueprints write to State member fields. It will re-run the blueprint tree, i.e. the function given to expand() and all its descendants added using builder.expand(), until the state no longer changes.

In fact, the state used in each iteration is immutable. When blueprints “change” the state, they write to the state that will be used in the next iteration of the expansion. I.e. updated state fields are not immediately visible. Instead, the entire blueprint tree is re-expanded from scratch, this time using updated field values in the state structures. This ensures that the expansion result is consistent:

This iterative behaviour of expansion is the key to why blueprints can be written in a declarative manner: it does not matter exactly in which order the state members are read or written. In the example above, one blueprint writes to the state member that is used by the other blueprint and this will work since in some iteration the state will be updated and the reading blueprint will obtain correct data.

The expansion can be performed many times. The same result is obtained every time. Moreover, expansion does not necessitate instantiation of the pre-object tree. For instance, expanding blueprints is useful in order to extract data from the configuration.

3.6 State sub-types

As mentioned above, the State class is used for defining shared state that can be read or written by the blueprints involved in the expansion.

If one wants to define input configuration parameters to the model, this can be done in a similar way, but sub-classing Config instead of State. The meaning of configuration parameter is a value that does not change during blueprint expansion, it is only an input value used to configure the model, i.e. used in the blueprint function logic or passed directly to attributes.

Hence a Config sub-class instance is read-only in the blueprints.

To create an instance of a Config sub-class, the blueprints use builder.create_config(), which is similar to builder.expose_state(). The equivalent of builder.read_state() is builder.get_config().

Another situation is when one wants to define 1-1 channels, some part of the state should only be used by exactly one reader blueprint. For this case, one can sub-class Binding instead of State. It is illegal for multiple blueprints to do builder.read_state() on a particular Binding instance.

3.7 State passing shorthand

In the example above, the blueprints do explicit builder.read_state() to obtain in necessary instances. The blueprint framework has syntactic sugar for this. If a blueprint function parameter is of a sub-type to State, then the framework will do an automatic builder.read_state() and provide the instance as that parameter, without the caller blueprint (the one doing builder.expand()) having to do anything. The uart blueprint in the example can therefore be written like this:

def uart(builder: Builder, name: Namespace, con: UARTState):
    con.uart = builder.obj(name, "NS16550", console=con.console)

In a similar way, if a function argument is a sub-type of Config, the framework will do an automatic builder.get_config() and provide the instance.

3.8 Time queue assignment

Simics objects have a queue attribute, which should be set to the processor or clock object that the object is related to. A common case is that all objects in each cell are connected to one of the processor objects in the cell.

When creating Simics objects, each object will inherit the queue attribute value from its parent object, unless explicitly set. A modeller writing a blueprint for a sub-system can therefore make sure the attribute is set for the whole hierarchy by setting the attribute on the top object.

Moreover, the blueprint framework defines a standard state class named Queue. Each top blueprint in a sub-system should typically expose this state on the top level namespace. This facilitates letting another blueprint easily picking up the same queue object when integrating several blueprints (by writing wrapper blueprints).

An example illustrating this:

class ClockParams(Config):
    freq_mhz = 42

class SystemFabric(State):
    clk: ConfObject = None

def my_clock(builder: Builder, name: Namespace,
             params: ClockParams, fabric: SystemFabric):
    fabric.clk = builder.obj(name, "clock",
                             freq_mhz=params.freq_mhz)

def board(builder: Builder, name: Namespace, clock_freq=10):
    # Expose main state
    fabric = builder.expose_state(name, SystemFabric)
    # Expose Queue state, useful for external blueprints
    queue = builder.expose_state(name, Queue)
    # Set queue on whole hierarchy
    builder.obj(name, "blueprint-namespace", queue=queue.queue)
    # Create config for sub-blueprints
    clk_conf = builder.create_config(name.clk, ClockParams)
    clk_conf.freq_mhz = clock_freq
    queue.queue = fabric.clk
    # Expand sub-blueprints
    builder.expand(name, "clk", my_clock)
simics> @instantiate("board", board)
simics> board.clk->freq_mhz
<
10.0
simics> board.clk->queue
<
board.clk

3.9 Blueprint parameters

As seen above, blueprints are easily made small and modular. They are regular Python functions and can declare additional function parameters if necessary. This is useful when blueprints expand other blueprints within a certain subsystem.

Blueprints can also declare user-facing parameters, which are exposed outside of the blueprint world, e.g. to Simics targets.

An example of how to define blueprint parameters:

class CPUParams(Config):
    freq_mhz = 10

class SystemBackplane(Config):
    member = 4711

# Define parameters for all Config members 
@blueprint(params_from_config(CPUParams))
def cpu_bp(builder: Builder, name: Namespace, config: CPUParams):
    # State classes can be converted to a Python dict
    builder.obj(name, "my-cpu-class", **config.asdict())

@blueprint([
    Param(name="member", desc="Parameter description", config=SystemBackplane),
    ParamGroup(name="cpu", desc="CPU parameters", import_bp="cpu_bp"),
])
def board(builder: Builder, name: Namespace, plane: SystemBackplane):
    # Namespace must match the parameter group name
    builder.expand(name, "cpu", cpu_bp)

The parameters form a tree. Each parameter definition can either be an actual parameter (a leaf node) or a parameter group declaration, which is used to import parameters from another blueprint.

The user-facing name of a parameter definition is of the form <parent>:<name> where <parent> is the user-facing name of the parent node and <name> is the name provided in the parameter definition.

Each parameter is tied to a specific data member in some Config, and obtains its default value and type from there. If the default value is None an explicit type annotation must be used. When a blueprint import parameters from another blueprint, it must also add that blueprint (using builder.expand) and the namespace must match the parameter group that is used in the import.

When expanding and/or instantiating a blueprint, values of the user-facing parameters can be provided as a Python dictionary, where the user-facing names are the keys. This results in the corresponding Config data members being overridden. Example:

builder = expand("board", board, 
                 params={"member": 42, "cpu:freq_mhz": 100}})

However, it is rarely needed to do this explicitly since it is handled by the target parameter framework, as explained in the next section.

Two cases of dynamic parameters are supported:

  1. A blueprint X adds N copies of a blueprint Y in a loop, where N is a user-facing parameter defined by X. In this case, X should define a parameter group declaration that imports from Y and set the count constructor argument of ParamGroup to the name of the N parameter, which must be of integer type.

  2. A blueprint X adds a blueprint Y if a flag is set, and the flag is a user-facing parameter defined by X. In this case, X should define a parameter group declaration that imports from Y and set the enable constructor argument of ParamGroup to the name of the flag parameter, which must be of boolean type.

3.10 Target parameter connection

The primary use of blueprint parameters is for inclusion in Simics targets. The target parameter framework has built-in support for directly importing blueprint parameters, as mentioned in the Target parameters reference Technology Guide. The result is that the blueprint parameters become target parameters for the target importing the blueprint, and the parameters can therefore be inspected using the target parameter Simics commands.

The target parameter framework also handles expansion and instantiation of the imported blueprints. Loading a target therefore has three steps:

  1. The target parameter framework parses the YAML scripts that the target consists of (via script imports) and collects all imported blueprints. All blueprint parameters and their default values are inserted in the target parameter tree at their corresponding nodes.

  2. All the blueprints are expanded using the same blueprint Builder, allowing them to expose and read state from each other. In this expansion, all the target parameter overrides coming from any preset files or command line arguments are used to set blueprint parameters. The result of the blueprint expansion is instantiated.

  3. The target script code is executed in the usual manner.

This process facilitates integrating sub-systems modelled as blueprints with targets on top of them. The natural way of integrating blueprint models is to create a wrapper blueprint that does builder.expand() but there may be a situation where the sub-system model inherently consists of a blueprint with a target script on top that has important logic, so that the integration entry point is the target script.

In that case, two such sub-systems can be integrated by creating a wrapper target script that imports the sub-system scripts. By the above mentioned process when loading the target, the blueprints in the sub-systems will be expanded together so that they can share state, and hence “connect”.

3.11 Simics CLI support

The blueprint framework provides some Simics CLI commands for inspection:

Note that apart from the first command, these work on blueprints that have been registered with Simics. The registration is done by the same Python decorator blueprint that is used to define blueprint parameters (i.e. the parameter list is optional). The decorator has an optional parameter name for assigning the name used by the CLI commands, defaulting to the Python function name.

3.12 Blueprint meta-data

When blueprints register object creation using builder.obj(), they do not need to register objects at every level of the tree. At tree nodes which have no explicit object registered, the blueprint framework will automatically register objects of class namespace.

The exception to this rule is the top level in each blueprint, i.e. the node of the given name argument. If no object is registered at this node, an object of class blueprint-namespace is automatically registered.

This Simics class has an attribute blueprint which records the name of the blueprint. The attribute is set automatically on all such objects that are registered at the top level in each blueprint, whether or not the registration is done explicitly in the blueprint or by the framework.

The class also has other attributes that are used to store meta-data about the sub-system modelled by the blueprint, and these are used by e.g. the Simics command print-target-info. Those attributes are not set on objects automatically registered by the blueprint framework, so if specific values are needed, then the blueprint must register the blueprint-namespace objects explicitly. This is why it is done in the example above.

3.13 Typing

The blueprint framework makes full use of the typing system in Python.

State structures are defined using the preferred syntax to declare typed named tuples in Python. The main difference is that default values are required for all fields.

With a properly configured editor, the blueprint writer has

Standard Python types are used.

When assigned to an object attribute, a Namespace object name is converted to the corresponding Simics pre-object. The Namespace type is also wrapped in some other pre-defined types:

3.14 Debugability

What a blueprint does is completely determined by looking at all values in the state (this is available through the print-blueprint-state command).

To debug a blueprint, it is sufficient to look at the state that the blueprint uses, and to look at what the blueprint does itself. It does not interact with anything else.

The state structures themselves provide a bus-centric view of the system, describing exactly how the system is connected.

3.15 Hotplugging

Hotplugging is the act of connecting already instantiated blueprints or objects. The blueprint framework does not try to unify hotplugging and the task of creating the initial configuration. There are several reasons for this:

The requirements for hotplugging/initial configuration are generally quite different:

  1. Pre-objects vs configured Simics objects:

    • The initial configuration is built from scratch using pre-objects.
    • Hotplugging modifies the state of existing Simics objects.
  2. A hotplug connect/disconnect is a simulation event:

    • Inserting e.g. a cable should generally result in a signal being raised.
    • Creating a system in a connected state should not result in a hotplug signal.
  3. Blueprint size:

    • Hotplugging is generally used to connect large blocks with each other.
    • Blueprints are usually much smaller (e.g. a single functional unit in a SOC).
  4. Connection kind:

    • Hotplug connections often use a natural 1-1 connection style.
    • Blueprints within a SOC often have a much more complex structure, with spider-like connections.
  5. Simplicity:

    • Keeping the blueprint framework as something that is only used during the initial construction of the configuration keeps both the hotplug system and the blueprint framework simple.

Hotplugging is currently handling using the existing framework for connectors.

In general, hotplugging should be reserved for things that can be modified while the simulation is running, while the blueprint framework is used to build constituents.

The blueprint framework can also build configurations that contain either connected or disconnected hotplug connections, with minimal double work.

4 Expansion example

Here we again consider the example above, i.e. keep the definition of the blueprint function board, but look more closely at the expansion. The expand() function has a parameter logger which can be set to a standard Python logging.Logger object; the default is to use the Python root logger object. The expansion uses this object for logging.

simics> @logging.basicConfig(format="%(message)s", stream=sys.stdout, level=logging.INFO)
simics> @expand("board", board)
Expansion of blueprint "board" at node "board"
Expansion roots:
[(board, 'board', {})]
Expansion presets:
[]
Iteration start: 0

Iteration end: 0
Iteration start: 1
UARTState[board]
  uart                    None
  console                 None
Iteration end: 1
Iteration start: 2
UARTState[board]
  uart                    board.uart
  console                 board.console
Iteration end: 2

At the start of the first iteration, the state is of course empty. Since the framework does another iteration, the blueprints must have exposed more state or written something new to the state. At the start of the second iteration, we see that there is now the expected UARTState state exposed at the node board but it is empty

Since the framework does a third iteration, the blueprints must again have written something new to the state during the second iteration. Indeed, at the start of the third iteration, we see that the state is now populated with the expected values. It is clear from the blueprint code these will not change and indeed the expansion is finished after this iteration.

5 Blueprint Python import

The blueprint framework is distributed as a Simics module, but it has a bespoke Python import hook. When importing Python functions from the blueprint framework, the usual simmod syntax should not be used. Imports should be done like in this example:

from blueprints import expand, Builder, Namespace

def top(builder: Builder, name: Namespace):
    builder.obj(name.clk, "clock", freq_mhz=1)
    
expand("top", top)

Note that there is also no need to explicitly load the blueprint Simics module beforehand.

6 Blueprint sub-system integration

A blueprint is written in Python and included in a Simics module like other Python code. Delivering a model or sub-system written as a blueprint is no different than delivering other Simics modules.

If the blueprint is not a model of a whole system but of a sub-system, then it typically does builder.read_state() or builder.get_config() somewhere to pick up a State or Config instance which it reads or writes from.

To integrate such a sub-system into a larger system written as a blueprint, the sub-system must document which State or Config it expects to have exposed and on what namespace nodes. As mentioned in earlier sections, the sub-system should also typically either expose or read a Queue state (depending on if it creates clocks/processors or expects them from elsewhere). The integrator can then write wrapper blueprints that exposes the necessary state.

The integrator can reference blueprint functions in Python files in other Simics modules using the simmod syntax that Simics provides, like in this example:

# Refer to blueprint function board in board.py 
from simmod.qsp_x86_bp.board import board as qsp_board

def top(builder: Builder, name: Namespace):
    builder.expand(name, "qsp", qsp_board)

The Simics module containing a blueprint should make sure to import the Python files from its module_load.py. This makes sure that any usage of the Python decorator blueprint mentioned earlier is run, so that the blueprints are registered with Simics and the blueprint CLI commands can refer to the blueprint after the Simics module is loaded.

7 Comparison with components

The blueprint framework solves similar problems as the older component system, which it is meant to replace.

The component system has the connector concept. A component can expose connection points of certain types and separate connector objects are implemented for each type. A connector type defines which connection data can be transferred using the connector.

Moreover, connectors have direction and a component either exposes the connection point as “up” or “down”. An “up” component con connect to a “down” component that has a connection point of the same type. When they connect they can transfer some data.

The connector concept was meant for external connections, typically of hotplugging type, such as USB or serial connectors, and worked well for this use case. However it was the only way to transfer data between components and was therefore used for all kinds of connections. In many such cases the connector concept was too opinionated and hence difficult to work with. Connector data was hard to change and connectors required lots of boilerplate code.

As explained in earlier sections, the blueprint framework introduces the shared state concept. This is the replacement of the connectors. It allows blueprints to transfer data, by reading and writing to the shared state. This is a much more light weight mechanism than connectors, and therefore easier to use for “internal” connections within a sub-system modelled as several blueprints.

The up/down nature of connectors came from a general assumption in the component system that the modelled system consists of sub-systems in a tree structure. This assumption is generally true in the use cases that connectors were meant for, but in modern systems it is frequently not true.

The blueprint framework makes no such assumptions on the model. The shared state can be read or written by any blueprint that picks it up. There are still two hierarchies: the Simics object hierarchy which is related to where state is exposed, and also the hierarchy of blueprints expanding other blueprints. But this is not directly related to how blueprints “connect”. Two blueprints anywhere in these hierarchies can “connect” if they can pick up the same shared state. As a result, the blueprint framework is less strict in how it enforces the data flow in the model. It makes it easier to split up the model into smaller blueprints, which can become reusable parts.

The component system was written in an object oriented style, and required a fair amount of boilerplate code, not just in connectors but also to e.g. define component parameters. The blueprint framework is instead based on a functional style and each blueprint is very terse, as can be seen from the example above.

7.1 Porting from components to blueprints

The blueprint framework is quite different from the component system. To make porting a platform from components to blueprints easier, the blueprint framework includes an adapter that exposes a blueprint (and child blueprints) as a component. It is only intended to be used to support step-by-step porting.

The adapter converts exposed state, that is never read during the blueprint expansion, to down connectors. State that is read but not exposed anywhere is assumed to be input and is converted to up connectors.

Blueprint parameters are converted to component configuration attributes.

To make the adapter work, one has to potentially implement methods on the State sub-classes that define the connector type and data. The methods are:

# return connector type
def legacy_type(self) -> str:

# similar to get_connect_data() on connectors
def legacy_data(self, is_up, comp, cnt, data) -> list:

# similar to connect() on connectors
def legacy_connect(self, is_up, comp, cnt, attr) -> list:

Here is an example of using the adapter:

@bp_legacy_comp([ParamGroup("board", "", import_bp="system")])
def qsp_config_comp(builder: Builder, name: Namespace):
    builder.expand(name, "board", system)

cli.global_cmds.new_qsp_config_comp(name="qsp", **{'board_memory_size_mb': 8192, 'board_disk_bp': "clear_linux"})

As a general rule, a platform that includes a tree of components is likely most easily ported to blueprints in a bottom-up manner.

  1. Start with the leaf components (without down connections) and rewrite them into blueprints. Surrounding code has to be changed to instantiate a blueprint instead of a component. In particular, a component one level up in the tree has to be modified to not expect component connections. The blueprint-component adapter may be useful in this case to make the blueprint still look like a component, if the component one level up is hard to update.

  2. Continue in the same way up the component hierarchy and rewrite components to blueprints. These blueprints will likely expand the blueprints already ported.

  3. When the whole tree has been converted to blueprints, any use of the adapter can be removed, and the platform can be simplified since it is now not dependent on the structure imposed by the component system.