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.

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.

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>classInputStream{public:InputStream(constchar*filename,intnbytes);~InputStream(void);boolout_valid(){return!complete;}uint64_tout_bits(){returndata;}voidtick(boolout_ready);private:voidread_next(void);boolcomplete;FILE*file;intnbytes;uint64_tdata;};InputStream::InputStream(constchar*filename,intnbytes){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);}voidInputStream::read_next(void){intres;this->data=0;res=fread(&this->data,this->nbytes,1,this->file);if(res<0){perror("fread");abort();}this->complete=(res==0);}voidInputStream::tick(boolout_ready){intres;if(out_valid()&&out_ready)read_next();}InputStream*stream=NULL;extern"C"voidinput_stream_init(constchar*filename,intdata_bits){stream=newInputStream(filename,data_bits/8);}extern"C"voidinput_stream_tick(unsignedchar*out_valid,unsignedcharout_ready,longlong*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.