Skip to content

Commit

Permalink
More documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
haugoug authored Nov 26, 2023
1 parent c9f1b12 commit 195d5c7
Show file tree
Hide file tree
Showing 8 changed files with 317 additions and 122 deletions.
1 change: 1 addition & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ jobs:
run: |
make all TARGETS=rv64
make all TARGETS=pulp-open
make all TARGETS=pulp.spatz.spatz
- name: Run examples
run: |
./install/bin/gvsoc --target=pulp-open --binary examples/pulp-open/hello image flash run
Expand Down
6 changes: 3 additions & 3 deletions .readthedocs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,6 @@ sphinx:
# Optional but recommended, declare the Python requirements required
# to build your documentation
# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
# python:
# install:
# - requirements: docs/requirements.txt
python:
install:
- requirements: requirements.txt
82 changes: 77 additions & 5 deletions docs/developer/block.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,27 @@ Block object
Overview
........

As seen on the following picture, components are mostly used to propagate JSON configuration from Python generators,
and to communicate with other components through bindings.

.. image:: images/block.png

All the other features are provided by the *vp::Block* object, and each Component is inheriting from. It provides the following features:

- **Lifecycle**: A set of methods for managing the state of blocks like start, reset, stop and so on.
- **Introspection**: Get name parent and childs.
- **Time**: features for executing callbacks at specific timestamps.
- **Clock**: features for executing callbacks at specific cycles.
- **Traces**: features for dumping system traces, used for debugging models and simulated software.
- **Power**: features for modeling power consumption.

Contrary to components which are instantiated by Python generators, blocks can be instantiated by C++ models in
order to build a hierarchy of blocks.

Lifecycle methods
.................

Here is the list of lifecyle methods which can be overloaded by the model to implement actions at specific events:

.. code-block:: cpp
Expand Down Expand Up @@ -54,6 +70,8 @@ Lifecycle methods
- Called when the power supplied has changed. Can be overloaded to take some actions
like changing power consumption.

Here is an example of a *start*, which is often used to start a thread once the whole system has been
instantiated:

.. code-block:: cpp
Expand All @@ -65,6 +83,9 @@ Lifecycle methods
}
}
An example of *reset*, which is often used to clear everything when it is asserted, and to start something when
it is deasserted:

.. code-block:: cpp
void Memory::reset(bool active)
Expand All @@ -82,6 +103,11 @@ Lifecycle methods
Event-based modeling
....................

As seen on the next figure, the whole system simulation is based on event-based modeling.

The idea is that each model can be broken down into small callbacks, which gets executed at specific timestamps in order
to simulate the expected hardware behavior.

.. image:: images/event_based_model.png


Expand All @@ -91,11 +117,30 @@ Clock model
Overview
########

In order to ease the modeling of clock domains, models are most of the time clocked, so that they just have
to care about cycles.

For that, each fequency domain is organized around a clock engine. Each component of the clock domain is connected
to the clock engine and can interact with it in order to execute callbacks at spicific cycles.

The clock engine is then in charge of converting the cycles to timestamps according to the clock domain frequency
so that overall, they execute at the expected timestamp. This way, models do not have to worry about frequency changes.

Each clock engine interact with the global time engine to execute the callbacks at the right timestamp.

There are also stubs for bindings which are crossing frequency domains. The stubs are in charge of synchronizing the clock
engines, and to do cycle conversion.

.. image:: images/clock_domains.png

Clock events
############

Clock events are used to enqueue callbacks to be executed at specific cycles.

They must be declared with class *vp::ClockEvent* and must be associated a callback which is a static
method of the class, like for ports.

.. code-block:: cpp
class MyComp : public vp::Component
Expand All @@ -111,13 +156,17 @@ Clock events
vp::ClockEvent event;
};
Clock events must be configured with their callbacks:

.. code-block:: cpp
MyComp::MyComp(vp::ComponentConf &config)
: vp::Component(config), event(this, MyComp::handle_event)
{
}
They can be enqueue by giving the number of cycles after which they must be executed:

.. code-block:: cpp
vp::IoReqStatus MyComp::handle_request(vp::Block *__this, vp::IoReq *req)
Expand All @@ -138,6 +187,13 @@ Clock events
req->get_resp_port()->resp(req);
}
The clock engine will make sure the callback gets called at the right timestamp.

Clock events which are enqueued are executed only once.

It is also possible to enable them so that they execute at every cycle, which can be faster for some models
like the ISS:

.. code-block:: cpp
void Exec::reset(bool active)
Expand All @@ -152,13 +208,17 @@ Clock events
}
}
In this case, it is still possible to skip some cycles, due to stall by calling this method:

.. code-block:: cpp
inline void Timing::stall_cycles_account(int cycles)
{
this->iss.exec.instr_event->stall_cycle_inc(cycles);
}
Here are all the methods available for clock events:

.. code-block:: cpp
inline void set_callback(ClockEventMeth *meth);
Expand All @@ -175,23 +235,27 @@ Clock events
inline void stall_cycle_inc(int64_t inc);
inline int64_t stall_cycle_get();
Stubs
#####


Asynchronous blocks
...................


Overview
########

Some models do not have any clock and needs to enqueue callback execution at timestamps instead of cycles. Time events
can be used for such components.

In this case, there is no clock engine, and the component is enqueueing callbacks directly to the time engine.

.. image:: images/time_events.png

Time events
###########

Time events are very similar to clock events except that they are enqueued with a timestamp.

They are declared the same way:

.. code-block:: cpp
class MyComp : public vp::Component
Expand All @@ -213,6 +277,8 @@ Time events
{
}
The enqueue is very similar, the timestamp is given in picoseconds:

.. code-block:: cpp
void Exec::reset(bool active)
Expand All @@ -223,12 +289,18 @@ Time events
}
}
As for clock events, the event callback gets executed when the time engine reaches the event timestamp:

.. code-block:: cpp
void MyComp::handle_event(vp::Block *__this, vp::ClockEvent *event)
{
MyComp *_this = (MyComp *)__this;
_this->event.enqueue(10000);
}
Here is the full list of methods for time events:

.. code-block:: cpp
inline void set_callback(TimeEventMeth *meth);
Expand Down
67 changes: 66 additions & 1 deletion docs/developer/component_model.rst
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,14 @@ C++ component model
Module constructor
##################

The C++ code of a component must have a special function which will be called
by the upper-level component to instantiate this one.

This function must return a new instance of the C++ class of the component and pass it the configuration
that the function received.

Here you can see an example.

.. code-block:: cpp
extern "C" vp::Component *gv_new(vp::ComponentConf &config)
Expand All @@ -215,6 +223,9 @@ Module constructor
Class declaration
#################

The class of a primitive component must inherit from *vp::Component*, as seen on this example:


.. code-block:: cpp
#include <vp/vp.hpp>
Expand All @@ -226,6 +237,9 @@ Class declaration
Memory(vp::ComponentConf &config);
};
In case a composite needs to include some C++, it must then inherit from *vp::Composite*, like in
this example:

.. code-block:: cpp
#include <utils/composite.hpp>
Expand All @@ -241,6 +255,8 @@ Class declaration
Class constructor
#################

Here are examples of constructors for both primitives and composites:

.. code-block:: cpp
Memory::Memory(vp::ComponentConf &config)
Expand All @@ -259,6 +275,13 @@ Class constructor
Port declaration
################

The ports of the component must be first declared in the component class.

For each signature, there is a pair of C++ classes, one for master port and one
for slave port, which can be used to declare the port with the right signature.

Some signature like the wire interface, are templates.

.. code-block:: cpp
class Memory : public vp::Component
Expand All @@ -275,19 +298,27 @@ Port declaration
vp::WireMaster<bool> notif_itf;
};
Then the ports must be configured. The name given here is the one that the Python generator
should return.

All the slave interfaces must be associated callbacks, which are methods which will
get called, when the port on the other side is called. The slave is supposed
to implement the associated activity in this callback.

Master ports can sometime also have callbacks, to make the binding bidirectional.

.. code-block:: cpp
Memory::Memory(vp::ComponentConf &config)
: vp::Component(config)
{
this->request_itf.set_req_meth(&Memory::request_handler);
this->new_slave_port("input", &this->request_itf);
this->new_master_port("notif", &this->notif_itf);
}
Here is the list of available port signatures.

.. list-table:: Available port signature
:header-rows: 1
Expand Down Expand Up @@ -330,9 +361,32 @@ Port declaration
- UartMaster -> UartSlave


Port method implementation
##########################

As seen earlier, ports methods must be implemented with class methods, which will get
called when the remote port is called.

They must be static methods, as we can see on this example:
.. code-block:: cpp
static vp::IoReqStatus req(vp::Block *__this, vp::IoReq *req);
The class instance is always passed as first argument and can be casted to the component class:

.. code-block:: cpp
vp::IoReqStatus Memory::req(vp::Block *__this, vp::IoReq *req)
{
Memory *_this = (Memory *)__this;
Port method call
################

On the caller side, the port is called simply by calling the right method on the port.
Each port has his own set of methods coming from the signature.

.. code-block:: cpp
void Memory::reset(bool active)
Expand All @@ -346,6 +400,11 @@ Port method call
Component JSON configuration
############################

Each Python component has a set of properties which are passed to the C++ model through
a JSON configuration.

They usualy come from the Python wrapper parameters and needs to be propagated as properties:

.. code-block:: python
def __init__(self, parent: gvsoc.systree.Component, name: str, size: int, width_log2: int=2,
Expand All @@ -362,6 +421,9 @@ Component JSON configuration
'align': align
})
This is the JSON file generated:

.. code-block:: json
"mem": {
Expand All @@ -376,6 +438,9 @@ Component JSON configuration
]
}
The properties can then be retrieved from C++ using the js::Config class which provides
a set of methods for accessing the properties according to their type:

.. code-block:: cpp
Memory::Memory(vp::ComponentConf &config)
Expand Down
Loading

0 comments on commit 195d5c7

Please sign in to comment.