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.
subop
s 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