Creating Simulation Model¶
So far, we’ve been using a fixed input stream model to test our device. But, ideally, we’d like an input stream that is defined by a software model and configurable at runtime. We’d like to put the input data in a file and pass it in as a command-line argument. We can’t do that in Chisel. We’ll have to create the model in Verilog and call out to C++ using the Verilog DPI-C API.
First, how do we include Verilog code in a Chisel codebase? We can do this using the Chisel BlackBox class. BlackBox modules can be used like regular Chisel modules and have defined IO ports, but the internal implementation is left to Verilog.
class SimInputStream(w: Int) extends BlackBox(Map("DATA_BITS" -> IntParam(w))) {
val io = IO(new Bundle {
val clock = Input(Clock())
val reset = Input(Bool())
val out = Decoupled(UInt(w.W))
})
}
One key difference in the IO bundle definition is that the implicit clock
and reset
signals must be explicitly defined in a BlackBox. The BlackBox
class also takes a map that defines parameters that will be passed to the
verilog implementation. To connect the BlackBox in the test harness, we should
create a connectSimInput
method in the HasPeripheryInputStreamModuleImp
trait.
def connectSimInput(clock: Clock, reset: Bool) {
val sim = Module(new SimInputStream(outer.streamWidth))
sim.io.clock := clock
sim.io.reset := reset
stream_in <> sim.io.out
}
We then add a new configuration class in
src/main/scala/example/Configs.scala
that calls the connectSimInput
method.
class WithSimInputStream extends Config((site, here, up) => {
case BuildTop => (clock: Clock, reset: Bool, p: Parameters) => {
val top = Module(LazyModule(new ExampleTopWithInputStream()(p)).module)
top.connectSimInput(clock, reset)
top
}
})
class SimInputStreamConfig extends Config(
new WithSimInputStream ++ new BaseExampleConfig)
Now we need to create the verilog implementation of the SimInputStream
module. Make a new directory src/main/resources
and add vsrc
and csrc
subdirectories under it.
$ mkdir -p src/main/resources/{vsrc,csrc}
In the vsrc
directory, create a file called SimInputStream.v
and add
the following code.
import "DPI-C" function void input_stream_init
(
input string filename,
input int data_bits
);
import "DPI-C" function void input_stream_tick
(
output bit out_valid,
input bit out_ready,
output longint out_bits
);
module SimInputStream #(DATA_BITS=64) (
input clock,
input reset,
output out_valid,
input out_ready,
output [DATA_BITS-1:0] out_bits
);
bit __out_valid;
longint __out_bits;
string filename;
int data_bits;
reg __out_valid_reg;
reg [DATA_BITS-1:0] __out_bits_reg;
initial begin
data_bits = DATA_BITS;
if ($value$plusargs("instream=%s", filename)) begin
input_stream_init(filename, data_bits);
end
end
always @(posedge clock) begin
if (reset) begin
__out_valid = 0;
__out_bits = 0;
__out_valid_reg <= 0;
__out_bits_reg <= 0;
end else begin
input_stream_tick(
__out_valid,
out_ready,
__out_bits);
__out_valid_reg <= __out_valid;
__out_bits_reg <= __out_bits;
end
end
assign out_valid = __out_valid_reg;
assign out_bits = __out_bits_reg;
endmodule
The verilog defines its inputs and outputs to match the definition in the
Chisel BlackBox. But most of the implementation is left to C++ through the
DPI functions input_stream_init
and input_stream_tick
. We define
these functions in a SimInputStream.cc
file in the csrc
directory.
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
class InputStream {
public:
InputStream(const char *filename, int nbytes);
~InputStream(void);
bool out_valid() { return !complete; }
uint64_t out_bits() { return data; }
void tick(bool out_ready);
private:
void read_next(void);
bool complete;
FILE *file;
int nbytes;
uint64_t data;
};
InputStream::InputStream(const char *filename, int nbytes)
{
this->nbytes = nbytes;
this->file = fopen(filename, "r");
if (this->file == NULL) {
fprintf(stderr, "Could not open %s\n", filename);
abort();
}
read_next();
}
InputStream::~InputStream(void)
{
fclose(this->file);
}
void InputStream::read_next(void)
{
int res;
this->data = 0;
res = fread(&this->data, this->nbytes, 1, this->file);
if (res < 0) {
perror("fread");
abort();
}
this->complete = (res == 0);
}
void InputStream::tick(bool out_ready)
{
int res;
if (out_valid() && out_ready)
read_next();
}
InputStream *stream = NULL;
extern "C" void input_stream_init(const char *filename, int data_bits)
{
stream = new InputStream(filename, data_bits/8);
}
extern "C" void input_stream_tick(
unsigned char *out_valid,
unsigned char out_ready,
long long *out_bits)
{
stream->tick(out_ready);
*out_valid = stream->out_valid();
*out_bits = stream->out_bits();
}
In the C++ file, we implement an InputStream
class that takes a file name
as its argument. It opens the file and reads nbytes
from it for every
ready-valid handshake. The input_stream_init
function constructs an
InputStream
class and assigns it to a global pointer. The
input_stream_tick
function updates the state by calling the tick
method, passing in the inputs from verilog. It then assigns values to the
verilog outputs.
You can now build this new configuration in VCS.
$ cd vsim
$ make CONFIG=SimInputStreamConfig
Now create a file that can be used as the input stream data. Just getting
random bytes from /dev/urandom
would work. Pass this to your simulation
through the +instream=
flag, and you should see the data get printed
out in the input-stream.riscv
test.
$ dd if=/dev/urandom of=instream.img bs=32 count=1
$ hexdump instream.img
0000000 189b f12a 1cc1 9eb5 b65d bbef 96b6 4949
0000010 f8c8 636c 76fe 15f3 0665 0ef9 8c5d 3011
0000020
$ ./simv-example-SimInputStreamConfig +instream=instream.img ../tests/input-stream.riscv
9eb51cc1f12a189b
494996b6bbefb65d
15f376fe636cf8c8
30118c5d0ef90665