This small virtual testbench is for testing digital filter designs and other DSP functions.
The VirtualDSPtestbench contains a C-based signal generator whose behavior is selectable from the command line viauvm_set_config_int. One of the functions includes a C-based file read, values from which are brought into a sequence as part of the testbench stimulus. So you can sing into EDA playground and filter it and write it back out to GNUplot or to a wav file. That's why I call it a "virtual" DSP testbench as it's in lieu of having the design in physical hardware. And like a physical bench with test equipment, it's designed so that one can drop in a DSP type DUT with minimal effort and and apply a standard array of stimuli to it: impulse, step, square, sinusoid, sinusoid sweep, random, and data streams of your own choosing.
The input stimulus uses signed values, so the design uses signed values as well.
I'm going to need a lot of these multipliers ➞
So there is also a Perl script for turning
values like "-0.0014567" into Verilog
multiplier components suitable for use in
fixed-point signed arithmetic implementations.
This is just in the beginning stage. The most recent testbench is on EDAplayground here:![]()

subop.

y, y[n]. The stream comes from a signal generator which is implemented in a C program integrated via the DPI. Testing of ALU type components is done via the tests:
From the base test OP_ALUbase_test,
|
x[n] and the output is on y[n]. The stream comes from a signal generator which is implemented in a C program integrated via the DPI. Testing of ALU type components is done via the tests:
From the base test OP_ContinuousSampleBase_test
|
x[n], which is buffered, computations can then be performed, and the output is on XM[n] (Magnitude) and XP[n] (Phase). The stream comes from a signal generator which is implemented in a C program integrated via the DPI. Testing of Block type components is done via the tests:
From the base test OP_BlockTXFRBase_test
|
+UVM_TESTNAME=OP_LPFfir1_test (the default is a sine wave).
+UVM_TESTNAME=OP_LPFfir1_test +uvm_set_config_int=*,SIGNAL,0 (DC)
+uvm_set_config_int=*,SIGNAL,1 (IMPULSE)
+uvm_set_config_int=*,SIGNAL,2 (STEP)
+uvm_set_config_int=*,SIGNAL,3 (SQUARE)
+uvm_set_config_int=*,SIGNAL,4 (SINE)
+uvm_set_config_int=*,SIGNAL,5 (SWEEP)
+uvm_set_config_int=*,SIGNAL,6 (KSP135N246)
+uvm_set_config_int=*,SIGNAL,7 (FILE_READ)
+uvm_set_config_int=*,SIGNAL,8 (RANDOM)
+uvm_set_config_int=*,SIGNAL,9 (MINSAMPLES, 4 samples/cycle)
+uvm_set_config_int=*,SIGNAL,10 (MAXSAMPLES, 1 (ish) sine wave per 1024 samples)
+uvm_set_config_int=*,SIGNAL,11 (MIN_MAX, burst of '9' then '10')
|
SIGNAL are defined in OP_TBdefines_pkg.svh:
package TBdefs_pkg ;
`define SEQUENCE_LENGTH 1024
// Here's something odd. I've got the sequence looking at the SIG_type to
// determine if it should call a single input function or a randomized two-input
// transaction. It does that with a compare of (signal < TWOINPUT). So, all
// enum functions for single signal sequences for x[n] should be put to the
// left of "TWOINPUT".
// The inputs up to but not including TWOINPUT come from the signal_generator.c
// function and are intended to be used as a single sampled data stream of +/- integers.
// The TWOINPUT is just a switch to use randomized transactions for computational
// units that require a pair of values. Like an ALU might want.
typedef enum bit[5:0] { DC, IMPULSE, STEP, SQUARE,
SINE, SWEEP, KSP135N246, FILE_READ, RANDOM, MINSAMPLES, MAXSAMPLES, MIN_MAX,
TWOINPUT } SIG_type ;
endpackage
|
SEQUENCE_LENGTH number of calls the signal generator like so:
in_tx.x_in = signal_generator(signal, amplitude, offset, period, phase, samplenumber, sg_maxval, sg_minval) ;
amplitude.
0 except for a single value of amplitude in the middle of SEQUENCE_LENGTH.
0 for the first half of SEQUENCE_LENGTH then apply the value of amplitude for the remainder.
0 + offset and amplitude+offset alternately for 1/8 of SEQUENCE_LENGTH.
offset+amplitude * sin (2πfARB*samplenumber + phase). See note below on fARB.
offset+amplitude * sin (2πfmint + phase) to
offset+amplitude * sin (2πfmaxt + phase)
samplenumber goes from 0 to SEQUENCE_LENGTH. See note below on fmin and fmax.
return (int) amplitude * sin ((double)samplenumber * (double)(sweepmult++)/1304.0) ;
SEQUENCE_LENGTH and 2 x the Nyquist frequency. This is a linear sweep where the argument to the sine function increases by a factor, M, of the sweepcount with each sample.
samplenumber * sweepmult * M = 1023 * 1023 * M, and
samplenumber * sweepmult * M = 1024 * 1024 * M.
2048 * M < 1.57 , M > 1304
offset+
(amplitude/1) * sin (1*2πfARB*samplenumber + phase) +
(amplitude/3) * sin (3*2πfARB*samplenumber + phase) +
(amplitude/5) * sin (5*2πfARB*samplenumber + phase) +
(-amplitude/2) * sin (2*2πfARB*samplenumber + phase) +
(-amplitude/4) * sin (4*2πfARB*samplenumber + phase) +
(-amplitude/6) * sin (6*2πfARB*samplenumber + phase).
fARB.
audiovalues.txt contains one integer value per line, so it's an array audiofile[line number].
audiofile[samplenumber]. That file contains whatever I last felt like recording into the microphone and converting and uploading as that file. I'll make that selectable from the command line soon.
amplitude.
offset+amplitude * sin (2πfmaxt + phase). See note below on fmax.
offset+amplitude * sin (2πfmint + phase). See note below on fmin.
offset+amplitude * sin (2πfmaxt + phase) for 24 samples then return
offset+amplitude * sin (2πfmint + phase) to the end of SEQUENCE_LENGTH.
MIN_MAX sequence showing both fmax and fmin sine waves in the current SEQUENCE_LENGTH of 1024 samples. The green is the input, the blue the output. The noddy LPF has some amplitude issues with the higher frequency but passes nicely with the lower.
fARB is just the value to provide about 25 cycles in a SEQUENCE_LENGTH of 1024.
fmax is just the value to provide 4 samples / cycles. That's twice the Nyquist rate and again, it's just so I can see a few samples per cycle and not just know that with enough signal analysis I could detect the highest frequency sine wave.
fmin is just the value to provide about 1 cycle in a SEQUENCE_LENGTH of 1024.
SEQUENCE_LENGTH to 64K.
subop for changing the input source.
subops at the present time.
subops as the DUT needs and in an order determined by the tester. Remember, this is meant to be like a physical test setup, where you can turn a dial and push a button and flip a DIP switch and have different stimulus and different DUT behavior. Therefore there will be a new uvm_test for each Block operation. Extend from the base test OP_BlockTXFRBase_test which will always do an initialization, a block write, call some computation task or set of tasks with dothese(), and finish with a block read:
task run_phase(uvm_phase phase);
phase.raise_objection(this);
begin
InitBlock_sequence in_seq;
in_seq = InitBlock_sequence::type_id::create("in_seq");
in_seq.start(env.opagent.opsequencer);
end
repeat (sequence_length) begin
WriteBlock_sequence in_seq;
in_seq = WriteBlock_sequence::type_id::create("in_seq");
in_seq.start(env.opagent.opsequencer);
end
dothese() ; // override this in the extended test
repeat (sequence_length) begin
ReadBlock_sequence in_seq ;
in_seq = ReadBlock_sequence::type_id::create("in_seq");
in_seq.start(env.opagent.opsequencer);
end
phase.drop_objection(this);
endtask
|
dothese() in the extended test. For example, here's what the test OP_FFT_0DC_HANN_padZ_rdx2_nrml_test does for an implementation of dothese(). Each sequence in the task sets subop to a new value, then asserts compute, and awaits done:
task dothese() ;
remove_DC_offset_sequence in_seq ;
...
apply_HANN_window_sequence in_seq ;
...
padZeros_sequence in_seq ;
...
doFFTradx2_sequence in_seq ;
...
normalizeFFT_sequence in_seq ;
...
endtask
|
subop value, assert compute until done is asserted, set the next subop value, assert compute until done is asserted, ..., continue until you want to read out the results. The only implementation at this early stage is a collection of dummy activity meant to support testbench development. I mean, we all build the testbench first, don't we?


% ./base10to32bitandQscale.pl a0=0.00019897136836160073
//-------------------------------------------------------------
Mult16bitByConstFixed a0_inst ( // a0 = 0.00019897136836160073
.Q ('d23), //
.coeff (16'b0000011010000101), // (a0base2) x 2^23
.B (BBBBBBBa0), //
.y (yyyyyyya0) // y = coeff*B >>> Q
) ;
./base10to32bitandQscale.pl a1=-0.085198971368361
//-------------------------------------------------------------
Mult16bitByConstFixed a1_inst ( // a1 = -0.085198971368361
.Q ('d14), //
.coeff (16'b1111101010001101), // (a1base2) x 2^14
.B (BBBBBBBa1), //
.y (yyyyyyya1) // y = coeff*B >>> Q
) ;
|
OP_BiQuadLPF_1_test and here is the oputput for different "settings" of the signal generator:
+UVM_TESTNAME=OP_BiQuadLPF_1_test +uvm_set_config_int=*,SIGNAL,2
+UVM_TESTNAME=OP_BiQuadLPF_1_test +uvm_set_config_int=*,SIGNAL,3
+UVM_TESTNAME=OP_BiQuadLPF_1_test +uvm_set_config_int=*,SIGNAL,7