PSS: Getting Outside the Box
11 Feb 2025
In the last post, we showed a SystemVerilog implementation of a PSS model that printed “Hello World!”. Interesting, perhaps, but quite a ways from being useful. In order to be useful, our PSS model needs to interact with the environment surrounding it.
This post will cover more details about how a PSS model interacts with the environment around it, and look at an object-oriented interface between PSS and a SystemVerilog environment.
Both PSS and SystemVerilog are object-oriented languages. With language interoperability, our goal is to keep each language’s view of inteacting with the “other” consistent with its own norms and conventions. Because both languages are object-oriented, we want SystemVerilog to see its interactions with PSS in object-oriented terms, and vice versa.
While we’re looking at an API in the context of our Zuspec PSS to SystemVerilog transpiler, the goal is to define a language interoperability approach that will work with many PSS tools.
Essentially, what we want is this:
In other words, we want an integration mechanism that supports:
- Multiple, independent, instances of PSS model implementations that run concurrently.
- Multiple “logical streams” within each PSS model instance that interact with the SystemVerilog testbench
The biggest obstacle to achieving this is that both PSS and SystemVerilog use global functions to implement interactions with the outside world. Global functions do not allow us to leverage object-oriented language constructs, so we will need to add some infrastructure on top.
The Basics
PSS provides import
functions to allow the PSS model to interact
with the outside world.
import target function void bfm_write(bit[32] addr, bit[32] data);
import target function bit[32] bfm_read(bit[32] addr);
component bfm_c {
action write {
//
exec body {
bfm_write(...);
}
}
}
In the example above, two functions are declared – one to perform a read via a BFM, and one to perform a write. These are global functions, accessible from all PSS contexts.
The PSS LRM specifies how function parameter and return types are
mapped to SystemVerilog and C. Theoretically, we could map
the functions themselves to export
tasks and functions
in SystemVerilog.
interface bfm;
automatic task bfm_write(int unsigned addr, int unsigned data);
endtask
export "DPI-C" task bfm_write;
automatic task bfm_read(output int unsigned data, input int unsigned addr);
endtask
export "DPI-C" task bfm_read;
endinterface
The example above shows SystemVerilog export
tasks that mirror the
PSS import
functions. Conceptually, calling bfm_write
in PSS
would translate into a call to the bfm_write
task in SystemVerilog.
If we do that, though, we have no awareness of multiple PSS model
instances, and little implementation flexibility. Fortunately, a
little methodology and a little code generation can help us
get the object-oriented interfaces that we want!
Introducing the API Class
Zuspec-SV (our PSS to SV transpiler) defines an Import API class that contains a virtual method definition for each and every Import function in the PSS model.
class pss_import_api extends backend_api;
virtual task bfm_write(int unsigned addr, int unsigned data);
endtask
virtual task bfm_read(output int unsigned data, input int unsigned addr);
endtask
endclass
The code above shows what would be produced for the bfm_write
and
bfm_read
functions shown earlier. The import
API class inherits
from another API class that defines built-in functions that the PSS
model needs to access. Implementing the API can be done simply by
creating a class that inherits from pss_import_api
and providing
implementations of the tasks and functions.
Connecting our API Implementation
Once we have a SystemVerilog class with properly-implemented methods, we need to connect the PSS model implementation to it. This is where things get a bit tool-specific.
PSS defines a scenario model as the combination of a tree of
components and a hierarchy of actions that execute in the
context of the components. Zuspec-SV
refers to this
component/action combination as an Actor. An Actor is
implemented as a class that accepts the import API class
as an argument to its constructor.
class pss_top__Entry_actor extends actor_c;
pss_top comp_tree;
pss_import_api api;
executor_base_c default_executor;
function new(pss_import_api api=null);
...
endfunction
...
endclass
As we saw in the Hello World example, we run a PSS model by creating an instance of the Actor and calling the run task.
Full Example
Let’s take a step-by-step look at the simple API implementation example
in zuspec-examples
. You can find the full example
here.
If you want to try this example yourself, be sure to update your Zuspec-SV
version. You can do so in the zuspec-examples
project by running the
following command:
% ./packages/python/bin/pip install -U zuspec-sv
You will need at least version 0.0.9 to run this example.
Let’s start with the PSS code:
import target function void bfm_write(input bit [32] addr, input bit [32] data);
import target function bit[32] bfm_read(input bit [32] addr);
component pss_top {
action Entry {
exec body {
bit[32] data;
bfm_write(0, 0x12345678);
bfm_write(4, 0x12345678);
data = bfm_read(4);
message(LOW, "PSS read data %d", data);
}
}
}
We declare two import functions – one that writes data via a bus functional model (BFM), and one that reads data via a bus functional model.
We then declare a (very) simple PSS Action that calls the write
function
twice, calls the read
function once, and displays the return value.
Implementing the API
Zuspec-SV
creates the following API class based on the import functions
declared within the PSS model:
class pss_import_api #(type BaseT=zsp_sv::empty_t) extends backend_api #(BaseT);
virtual task bfm_write(
input int unsigned addr,
input int unsigned data);
`ZSP_FATAL(("Import function bfm_write is not implemented"));
endtask
virtual task bfm_read(
output int unsigned __retval,
input int unsigned addr);
`ZSP_FATAL(("Import function bfm_read is not implemented"));
endtask
endclass
Note that the signature of the bfm_read
task is a bit different. This
is because SystemVerilog tasks do not support a return value, so the
result must be returned via an output parameter. Fortunately, this is
all well-defined by the rules in the PSS LRM.
package simple_read_write_pkg;
import pss_types::*;
class api_impl extends pss_import_api;
virtual task bfm_write(
input int unsigned addr,
input int unsigned data);
$display("bfm_write: 'h%08h 'h%08h", addr, data);
endtask
virtual task bfm_read(
output int unsigned __retval,
input int unsigned addr);
$display("bfm_read: 'h%08h", addr);
__retval = 42;
endtask
endclass
endpackage
Our testbench environment is responsible for providing code, like
that shown above, to provide an implementation for the import functions.
Our implementation, here, is quite simple: We print a message when either
task is called, and return the value 42
from the read
function.
module simple_read_write;
import simple_read_write_pkg::*;
import pss_top__Entry_pkg::*;
initial begin
automatic api_impl api = new();
pss_top__Entry actor = new(api);
actor.run();
end
endmodule
Finally, we can put everything together and run our PSS model. The
code above creates an instance of our implementation of the API
class, and passes it to the constructor of our PSS Actor
class.
When we run the simulation, we should see something like the following:
bfm_write: 'h00000000 'h12345678
bfm_write: 'h00000004 'h12345678
bfm_read: 'h00000004
PSS read data 42
Summary and What’s Next
We’ve looked at the funadamentals of a strategy to integrate two object-oriented languages, via global functions, in an object-oriented way. This approach gives us flexibility in changing how APIs are implemented using the standard object-oriented approaches that we’re used to.
But, we’re not done just yet. You can likely imagine how this approach supports multiple indepdent PSS model instances. But, how does it support API implementations coming from different sources, and multiple independent streams of activity within one PSS model? In the next post, we’ll start to dig into how to interface PSS to verifcation IP (VIP) and bus functional models (BFMs).