File: dds_timed_tb.sv

package info (click to toggle)
uhd 4.8.0.0%2Bds1-2
  • links: PTS, VCS
  • area: main
  • in suites: trixie
  • size: 183,172 kB
  • sloc: cpp: 279,415; python: 109,850; ansic: 103,348; vhdl: 57,230; tcl: 20,007; xml: 8,581; makefile: 2,863; sh: 2,797; pascal: 230; javascript: 120; csh: 94; asm: 20; perl: 11
file content (886 lines) | stat: -rw-r--r-- 30,281 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
//
// Copyright 2021 Ettus Research, A National Instruments Brand
//
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// Module: dds_timed_tb
//

`default_nettype none


module dds_timed_tb;

  // Include macros and time declarations for use with PkgTestExec
  `include "test_exec.svh"

  import PkgTestExec::*;
  import PkgAxiStreamBfm::*;
  import PkgComplex::*;
  import PkgMath::*;
  import PkgRandom::*;

  //---------------------------------------------------------------------------
  // Testbench Configuration
  //---------------------------------------------------------------------------

  localparam real CLK_PERIOD = 10.0;

  // Values needed by the DUT (use the same values as the DUC)
  localparam int SR_FREQ_ADDR      = 132;
  localparam int SR_SCALE_IQ_ADDR  = 133;
  localparam int SR_AWIDTH         = 8;
  localparam int SR_DWIDTH         = 32;
  localparam int SR_TWIDTH         = 64;
  localparam int PHASE_ACCUM_WIDTH = 32;
  localparam int SCALING_WIDTH     = 18;

  // Bit widths for our sample size
  localparam int FRAC_W   = 15;       // Number of fixed point fractional bits
  localparam int COMP_W   = 16;       // Width of just the imag/real part
  localparam int SAMPLE_W = 2*COMP_W; // Width of a complex sample

  // Max min possible values for the components of a sample
  localparam bit signed [COMP_W-1:0] MAX_COMP =  2**(COMP_W-1) - 1;
  localparam bit signed [COMP_W-1:0] MIN_COMP = -2**(COMP_W-1);

  // Max/min possible values for the scale register
  localparam real MAX_SCALE = +(2**(SCALING_WIDTH-1) - 1) / (2.0**FRAC_W);
  localparam real MIN_SCALE = -(2**(SCALING_WIDTH-1)    ) / (2.0**FRAC_W);

  // TUSER bit positions
  localparam int HAS_TIME_POS  = 125;
  localparam int EOB_POS       = 124;
  localparam int TIMESTAMP_POS = 0;

  // AXI-Stream data bus parameters
  localparam int DATA_W = SAMPLE_W;
  localparam int USER_W = 128;

  // Amount of rounding error to allow (in ULPs). We generally expect the error
  // in computation to be +/- 1 ULP, but since the DUT performs several
  // computations, error can accumulate. The testbench computations also
  // introduce some error. In particular, using the scale register in the DUT
  // also scales the error. The MAX_ERROR can be reduced to 2 if you keep the
  // scale register < 1.0.
  localparam MAX_ERROR = 8;


  //---------------------------------------------------------------------------
  // Type Definitions
  //---------------------------------------------------------------------------

  // Burst test configuration
  typedef struct {
    int     spp;             // Samples per packet to generate.
    int     spp_last;        // Length of last packet, if different from SPP.
                             // Set to -1 to use spp value.
    int     num_packets;     // Number of packets in each burst.
    int     num_bursts;      // Number of bursts to send.
    real    amp;             // Amplitude of the test signal to generate.
    real    freq;            // Normalized frequency of test signal to generate.
    bit     timed;           // Set to 1 for timed packet, 0 for non-timed. If
                             // doing a timed tune, this must be 1.
    real    scale;           // Scale value to use, in the range [-4,4).
    real    freq_shift;      // Initial frequency shift to use.
    real    tune_freq_shift; // New frequency shift to tune to.
    longint tune_time;       // Time after which to tune the frequency (set a
                             // new frequency shift). Set to -1 to disable.
  } burst_cfg_t;

  typedef AxiStreamPacket #(.DATA_WIDTH(DATA_W), .USER_WIDTH(USER_W)) axis_pkt_t;
  typedef axis_pkt_t axis_pkt_queue_t[$];

  // Default settings to use for burst_cfg_t. This creates a nice complex
  // sinusoid and the output should match the input, unchanged.
  localparam burst_cfg_t DEFAULT_BURST_CFG = '{
    spp             : 256,
    spp_last        : -1,
    num_packets     : 1,
    num_bursts      : 1,
    freq            : 1.0/16.0,
    amp             : 0.75,
    timed           : 1,
    scale           : 1.0,
    freq_shift      : 0.0,
    tune_freq_shift : 0.0,
    tune_time       : -1.0
  };


  //---------------------------------------------------------------------------
  // Clocks and Resets
  //---------------------------------------------------------------------------

  bit clk, rst;

  sim_clock_gen #(CLK_PERIOD) clk_gen (clk, rst);


  //---------------------------------------------------------------------------
  // AXI-Stream BFM
  //---------------------------------------------------------------------------

  // AXI-Stream interfaces to/from DUT
  AxiStreamIf #(.DATA_WIDTH(DATA_W), .USER_WIDTH(USER_W), .TKEEP(0))
    to_dut (clk, rst);
  AxiStreamIf #(.DATA_WIDTH(DATA_W), .USER_WIDTH(USER_W), .TKEEP(0))
    from_dut (clk, rst);

  // BFM for the AXI-Stream interface to DUT
  AxiStreamBfm #(.DATA_WIDTH(DATA_W), .USER_WIDTH(USER_W), .TKEEP(0))
    axis_bfm = new(to_dut, from_dut);


  //---------------------------------------------------------------------------
  // DUT
  //---------------------------------------------------------------------------

  logic                 clear = 1'b0;
  logic                 timed_cmd_fifo_full;
  logic                 set_stb = 1'b0;
  logic [SR_AWIDTH-1:0] set_addr;
  logic [SR_DWIDTH-1:0] set_data;
  logic [SR_TWIDTH-1:0] set_time;
  logic                 set_has_time;
  logic [ SAMPLE_W-1:0] i_tdata;
  logic                 i_tlast;
  logic                 i_tvalid;
  logic                 i_tready;
  logic [   USER_W-1:0] i_tuser;
  logic [ SAMPLE_W-1:0] o_tdata;
  logic                 o_tlast;
  logic                 o_tvalid;
  logic                 o_tready;
  logic [   USER_W-1:0] o_tuser;

  dds_timed #(
    .SR_FREQ_ADDR      (SR_FREQ_ADDR),
    .SR_SCALE_IQ_ADDR  (SR_SCALE_IQ_ADDR),
    .PHASE_ACCUM_WIDTH (PHASE_ACCUM_WIDTH),
    .SCALING_WIDTH     (SCALING_WIDTH),
    .SR_AWIDTH         (SR_AWIDTH),
    .SR_DWIDTH         (SR_DWIDTH),
    .SR_TWIDTH         (SR_TWIDTH)
  ) dds_timed_i (
    .clk                 (clk),
    .reset               (rst),
    .clear               (clear),
    .timed_cmd_fifo_full (timed_cmd_fifo_full),
    .set_stb             (set_stb),
    .set_addr            (set_addr),
    .set_data            (set_data),
    .set_time            (set_time),
    .set_has_time        (set_has_time),
    .i_tdata             (to_dut.tdata),
    .i_tlast             (to_dut.tlast),
    .i_tvalid            (to_dut.tvalid),
    .i_tready            (to_dut.tready),
    .i_tuser             (to_dut.tuser),
    .o_tdata             (from_dut.tdata),
    .o_tlast             (from_dut.tlast),
    .o_tvalid            (from_dut.tvalid),
    .o_tready            (from_dut.tready),
    .o_tuser             (from_dut.tuser)
  );


  //---------------------------------------------------------------------------
  // Timer
  //---------------------------------------------------------------------------
  //
  // Count the samples going into the DUT so we have something that tracks
  // packet timestamp in the testbench.
  //
  //---------------------------------------------------------------------------

  longint current_time = 0;

  always @(posedge clk) begin
    if (to_dut.tvalid && to_dut.tready) begin
      current_time <= current_time + 1;
    end
  end


  //---------------------------------------------------------------------------
  // Expected Output
  //---------------------------------------------------------------------------
  //
  // This assigns the expected output to a signal so we can visualize what the
  // testbench is expecting. Error checking isn't done here. This is only to
  // aid in debug.
  //
  //---------------------------------------------------------------------------

  mailbox #(axis_pkt_t) exp_pkts_mb = new();
  bit exp_data_mismatch = 0;
  bit exp_user_mismatch = 0;

  logic [SAMPLE_W-1:0] exp_tdata;
  logic [  USER_W-1:0] exp_tuser;

  always @(posedge clk) begin
    if (rst) begin
      exp_tdata  = 'X;
      exp_tuser  = 'X;
    end else begin
      static axis_pkt_t exp_pkt = null;
      static bit        out_valid = 0;

      // Give time for the DUT to update its status, so we know what to do.
      #(0.01ns);

      // Output the next expected sample if we haven't done so already
      if (from_dut.tvalid && !out_valid) begin
        int rval;

        // Get the next packet from the mailbox if needed
        if (exp_pkt == null) begin
          rval = exp_pkts_mb.try_get(exp_pkt);
          `ASSERT_ERROR(rval, "Couldn't get first packet from exp_pkts_mb.");
        end else if (exp_pkt.data.size() == 0) begin
          rval = exp_pkts_mb.try_get(exp_pkt);
          `ASSERT_ERROR(rval, "Couldn't get next packet from exp_pkts_mb.");
        end

        // Output the next sample
        `ASSERT_ERROR(exp_pkt.data.size(), "exp_pkt.data is empty");
        exp_tdata = exp_pkt.data.pop_front();
        `ASSERT_ERROR(exp_pkt.user.size(), "exp_pkt.user is empty");
        exp_tuser = exp_pkt.user.pop_front();
        out_valid = 1;
      end

      exp_data_mismatch = compare_samples(exp_tdata, from_dut.tdata);
      exp_user_mismatch = compare_samples(exp_tuser, from_dut.tuser);

      // Check if the output has been accepted and needs to update
      if (from_dut.tvalid && from_dut.tready) begin
        out_valid = 0;
      end
    end
  end


  //---------------------------------------------------------------------------
  // Helper Functions
  //---------------------------------------------------------------------------

  // Round a floating point number to num_bits bits of precision.
  function automatic real round_bits(real num, int num_bits);
    return real'(longint'(num * (2.0**num_bits))) / (2.0**num_bits);
  endfunction : round_bits


  // Compare the samples a and b to see if either component differs by more
  // than MAX_ERROR.
  function automatic bit compare_samples(sc16_t a, sc16_t b);
    Math #(s16_t) m;
    sc16_t diff;
    diff = sub_sc16(a, b);
    if (m.abs(diff.re) > MAX_ERROR || m.abs(diff.im) > MAX_ERROR) return 1;
    return 0;
  endfunction : compare_samples


  // Compare the packets, sample by sample. Returns a string error message
  // explaining the nature of the mismatch. If packets match, an empty string
  // is returned.
  function automatic string compare_packets(axis_pkt_t actual, axis_pkt_t expected);
    if (actual.data.size() != expected.data.size()) begin
      return $sformatf("Packet lengths do not match. Actual is %0d, expected is %0d.",
        actual.data.size(), expected.data.size());
    end

    foreach(actual.data[i]) begin
      sc16_t a, b;

      // Check the samples in TDATA.
      // Calculate the difference between the actual end expected values.
      a = actual.data[i];
      b = expected.data[i];
      if (compare_samples(a, b)) begin
        `ASSERT_WARNING(0, "compare_packets: Skipping rest of packet due to mismatch.")
        return $sformatf("Word %0d in packet TDATA does not match. Actual is 0x%X, expected is 0x%X.",
          i, actual.data[i], expected.data[i]);
      end

      // Check TUSER. This is only guaranteed to be valid on the last sample of
      // each packet due to the way it's currently implemented.
      if (i == actual.data.size()-1 && actual.user[i] != expected.user[i]) begin
        string fields;
        if (actual.user[i][EOB_POS] != expected.user[i][EOB_POS]) begin
          fields = {fields, "(EOB)"};
        end
        if (actual.user[i][HAS_TIME_POS] != expected.user[i][HAS_TIME_POS]) begin
          fields = {fields, "(HAS_TIME)"};
        end
        if (actual.user[i][TIMESTAMP_POS+:64] != expected.user[i][TIMESTAMP_POS+:64]) begin
          fields = {fields, "(TIMESTAMP)"};
        end
        if (fields == "") fields = "<None>";
        `ASSERT_WARNING(0, "compare_packets: Skipping rest of packet due to mismatch.")
        return $sformatf({
          "Word %0d in packet TUSER does not match. ",
          "Fields not matching: %s. ",
          "Actual is %X, expected is %X."},
          i, fields, actual.user[i], expected.user[i]);
      end
    end
    // Return empty string if all is well
    return "";
  endfunction : compare_packets


  // Generate a test packet containing a complex sinusoid signal e^(j∙2π∙f∙t)
  // and return it.
  //
  //   length:     The length of the packet to generate in samples.
  //   freq:       Normalized frequency of the signal to generate.
  //   eob:        EOB flag for the packet.
  //   timed:      Set to 1 for a timed packet, 0 for non-timed. Timed is the
  //               default.
  //   timestamp:  Timestamp for the first packet. Leave at the default value
  //               to continue from the time of the previous packet.
  //   init:       Initial phase value to use (t in e^jt). Leave at the default
  //               value to use the last value of the previous packet. Must be
  //               in the range [0,1), where 1.0 corresponds to 2*pi radians.
  //
  function automatic axis_pkt_t gen_test_packet(
    int      length,
    real     freq,
    real     amp       = 0.75,
    bit      eob       = 0,
    longint  timed     = 1,
    longint  timestamp = -1,
    real     init      = -1.0
  );
    static real phase;
    static longint next_time = 0;
    bit signed [COMP_W-1:0] re, im;
    int re_int, im_int;
    logic [USER_W-1:0] user;
    axis_pkt_t packet;

    if (init != -1.0) begin
      phase = init;
    end

    if (timestamp >= 0) begin
      next_time = timestamp;
    end

    packet = new();
    for (int sample_num = 0; sample_num < length; sample_num++) begin
      // Calculate I/Q
      re_int = $cos(phase*TAU) * amp * 2**FRAC_W;
      im_int = $sin(phase*TAU) * amp * 2**FRAC_W;

      // Saturate
      if(re_int > MAX_COMP) re = MAX_COMP;
      else if(re_int < MIN_COMP) re = MIN_COMP;
      else re = re_int;
      if(im_int > MAX_COMP) im = MAX_COMP;
      else if(im_int < MIN_COMP) im = MIN_COMP;
      else im = im_int;

      // Calculate TUSER (header)
      user                      = '0;
      user[EOB_POS]             = eob;
      user[HAS_TIME_POS]        = timed;
      user[TIMESTAMP_POS +: 64] = timed ? next_time : 'X;

      // Enqueue the sample
      packet.data.push_back({re, im});
      packet.user.push_back(user);
      phase += freq;
    end

    // Calculate the timestamp for the next packet
    next_time += length;

    return packet;
  endfunction : gen_test_packet


  // Apply a frequency shift to the packet data, by multiplying each sample by
  // the output of a complex NCO. The implementation here models the HDL so
  // that we don't accumulate error over time.
  //
  //   packet       : Input packet with the samples to frequency shift.
  //   freq         : Normalized frequency shift to apply.
  //   reset_nco    : If 1, reset the NCO to 0 before beginning. Otherwise
  //                  continue from previous value.
  //   first_sample : First sample to frequency shift
  //   last_sample  : Last sample to frequency shift (inclusive)
  //
  //   Returns: A new packet with the frequency-shifted data.
  //
  function automatic axis_pkt_t freq_shift_pkt(
    axis_pkt_t packet,
    real       freq,
    bit        reset_nco = 0,
    int        first_sample = 0,
    int        last_sample = -1
  );
    // Normalized phase angle in the range [0,1), corresponding to [0,2π)
    // radians.
    static bit [PHASE_ACCUM_WIDTH-1:0] phase = 0;
    bit [PHASE_ACCUM_WIDTH-1:0] phase_inc;
    axis_pkt_t new_packet;

    new_packet = packet.copy();

    phase_inc = freq * (2.0**PHASE_ACCUM_WIDTH);
    if (reset_nco) begin
      phase = 0;
    end

    if (packet == null) return null;

    last_sample = last_sample < 0 ? packet.data.size()-1 : last_sample;
    for (int i = first_sample; i <= last_sample; i++) begin
      // There are a lot of redundant variables in this loop. This was done to
      // aid in debugging so we can correlate what's calculated here to what
      // the DUT computes, and to have both fixed-point and floating point
      // values.
      sc16_t in_sc16, out_sc16;
      complex_t nco;
      complex_t in_c, out_c;
      real phase_real;

      // Get the next input sample and convert it
      in_sc16 = packet.data[i];
      in_c = sc16_to_complex(in_sc16);

      // Convert the phase
      phase_real = real'(phase) / (2.0**PHASE_ACCUM_WIDTH);

      // Compute the new NCO value: nco = exp(j∙2π∙phase)
      nco = polar_to_complex(1.0, TAU * phase_real);

      // Compute the new data output: sample_out = nco * sample_in
      out_c = mul(nco, in_c);
      out_sc16 = complex_to_sc16(out_c);
      new_packet.data[i] = out_sc16;

      // Update the phase for the next iteration
      phase = phase + phase_inc;
    end
    return new_packet;
  endfunction : freq_shift_pkt

  // Return a scaled version of the input data packet. That is, where each
  // sample is multiplied by scale. This models the precision provided by the
  // scaler in the DUT.
  function automatic axis_pkt_t scale_packet(axis_pkt_t packet, real scale);
    bit        [SAMPLE_W-1:0] sample;
    bit signed [COMP_W-1:0]   re, im, a, b;
    int                       re_tmp, im_tmp;
    axis_pkt_t                new_packet;

    // Make sure scale is in the range supported by hardware
    if (scale > MAX_SCALE) scale = MAX_SCALE;
    else if (scale < MIN_SCALE) scale = MIN_SCALE;

    new_packet = packet.copy();
    foreach (packet.data[i]) begin
      sample = packet.data[i];
      re = sample[1*COMP_W +: COMP_W];
      im = sample[0*COMP_W +: COMP_W];
      // Scale with full precision
      re_tmp = re * scale;
      im_tmp = im * scale;
      // Saturate the values
      if (re_tmp > MAX_COMP) re = MAX_COMP;
      else if (re_tmp < MIN_COMP) re = MIN_COMP;
      else re = re_tmp;
      if (im_tmp > MAX_COMP) im = MAX_COMP;
      else if (im_tmp < MIN_COMP) im = MIN_COMP;
      else im = im_tmp;
      new_packet.data[i] = { re, im };
    end
    return new_packet;
  endfunction : scale_packet

  // Generate the output packets we expect from the DUT given the provided
  // burst of packets and configuration.
  //
  //   cfg     : Burst test configuration used
  //   packets : Queue of packets that were input to the DUT
  //
  //   returns : Expected packets from DUT
  //
  function automatic axis_pkt_queue_t generate_expected(
    burst_cfg_t      cfg,
    axis_pkt_queue_t packets
  );
    static longint timestamp = 0;
    axis_pkt_t expected[$];
    axis_pkt_t packet;
    bit reset_nco;
    int first_sample;
    int last_sample;
    real freq_shift;

    freq_shift = cfg.freq_shift;

    foreach(packets[i]) begin
      // Make a copy of the input
      packet = packets[i].copy();

      // Check if we're supposed to tune the frequency in this packet
      first_sample = 0;
      if (cfg.timed && timestamp <= cfg.tune_time &&
          timestamp + packet.data.size() > cfg.tune_time) begin
        last_sample = cfg.tune_time - timestamp;
      end else begin
        last_sample = -1;
      end

      // Apply a frequency shift (reset the NCO before each burst)
      reset_nco = i % cfg.num_packets == 0;
      packet = freq_shift_pkt(packet, freq_shift, reset_nco, first_sample, last_sample);

      // If there was a tune, shift the rest of the packet differently
      if (last_sample >= 0 && last_sample < packet.data.size()) begin
        freq_shift = cfg.tune_freq_shift;
        reset_nco = 1;
        first_sample = last_sample + 1;
        last_sample = -1;
        packet = freq_shift_pkt(packet, freq_shift, reset_nco, first_sample, last_sample);
      end

      // Multiply packet samples by a scaler
      packet = scale_packet(packet, cfg.scale);

      // Add this packet to the queue
      expected.push_back(packet);

      // Send this packet to the expected packets mailbox, for debug
      `ASSERT_ERROR(exp_pkts_mb.try_put(packet.copy()), "Unable to put expected packet");

      // Calculate new timestamp
      timestamp += packet.data.size();
    end

    return expected;
  endfunction : generate_expected


  // Generate a queue of packets modeled after the burst test configuration
  // defined by cfg.
  function automatic axis_pkt_queue_t generate_bursts(burst_cfg_t cfg);
    axis_pkt_t packets[$];

    // Reset initial phase and time to 0 in generated packets by calling the
    // generator with init and timestamp set to 0.
    void'(gen_test_packet(.length(0), .freq(0), .init(0)));

    // Build the packets to send
    for (int burst_num = 0; burst_num < cfg.num_bursts; burst_num++) begin
      for (int packet_num = 0; packet_num < cfg.num_packets; packet_num++) begin
        axis_pkt_t packet;
        bit eob;
        int length;

        // Set EOB and use spp_last for the last packet
        if (packet_num == cfg.num_packets-1) begin
          eob = 1;
          length = (cfg.spp_last > 0) ? cfg.spp_last : cfg.spp;
        end else begin
          eob = 0;
          length = cfg.spp;
        end

        packet = gen_test_packet(
          .length    (length),
          .freq      (cfg.freq),
          .amp       (cfg.amp),
          .eob       (eob),
          .timed     (cfg.timed));
        packets.push_back(packet);
      end
    end

    return packets;
  endfunction : generate_bursts


  // Write a value to a settings register.
  //
  //  addr      : Address of the register to write to.
  //  value     : Value to write to the register.
  //  timestamp : Timestamp to provide with the write. Set to -1 if the write
  //              should not be timed.
  //
  task automatic write_reg(
    bit [SR_AWIDTH-1:0] addr,
    bit [SR_DWIDTH-1:0] value,
    longint             timestamp = -1
  );
    @(posedge clk);
    set_stb      <= 1;
    set_addr     <= addr;
    set_data     <= value;
    set_time     <= (timestamp > 0) ? timestamp : 'X;
    set_has_time <= (timestamp > 0);
    @(posedge clk);
    set_stb      <= 0;
    set_addr     <= 'X;
    set_data     <= 'X;
    set_time     <= 'X;
    set_has_time <= 'X;
    @(posedge clk);
  endtask : write_reg


  // Write a value to the frequency register.
  //
  //  freq      : Normalized frequency to write to the register. E.g., in the
  //              range [-0.5,0.5) or [0,1). Numerically, either works.
  //  timestamp : Timestamp to provide with the write. Set to -1 if the write
  //              should not be timed.
  //
  task automatic write_reg_freq(real freq, longint timestamp = -1);
    write_reg(SR_FREQ_ADDR, freq * (2.0**PHASE_ACCUM_WIDTH), timestamp);
  endtask : write_reg_freq


  // Write a value to the scale register.
  //
  //  scale     : Scaler to write to the register, in the range [-4,4).
  //  timestamp : Timestamp to provide with the write. Set to -1 if the write
  //              should not be timed.
  //
  task automatic write_reg_scale(real scale);
    // Saturate to the range allowed by the register
    scale = scale > MAX_SCALE ? MAX_SCALE : scale;
    scale = scale < MIN_SCALE ? MIN_SCALE : scale;
    write_reg(SR_SCALE_IQ_ADDR, scale * (2.0**FRAC_W));
  endtask : write_reg_scale


  // Check that the output matches what we would expect.
  //
  //   cfg:     Test configuration
  //   packets: The packets that were input to the DUT
  //
  task automatic verify_output(burst_cfg_t cfg, axis_pkt_queue_t packets);
    axis_pkt_t expected[$];

    expected = generate_expected(cfg, packets);

    foreach(packets[i]) begin
      axis_pkt_t recvd;
      string msg;
      axis_bfm.get(recvd);
      msg = compare_packets(recvd, expected[i]);
      `ASSERT_ERROR(msg == "",
        $sformatf("Error in packet %0d: %s", i, msg));
    end
  endtask : verify_output


  // Test a burst (i.e., multiple packets ending with EOB) through the DUT
  // using the provided configuration.
  task automatic test_bursts(burst_cfg_t cfg);
    axis_pkt_t packets[$];

    // Are we doing timed packets?
    cfg.timed = (cfg.timed || cfg.tune_time >= 0);

    // Set the registers
    write_reg_scale(cfg.scale);
    write_reg_freq(cfg.freq_shift);

    // Schedule a timed tune, if requested
    if (cfg.tune_time >= 0) begin
      write_reg_freq(cfg.tune_freq_shift, cfg.tune_time);
    end

    // Wait a bit for the register changes to take effect
    clk_gen.clk_wait_r(10);


    // Generate test packets to send
    packets = generate_bursts(cfg);

    // Send the packets
    foreach (packets[i]) axis_bfm.put(packets[i]);

    // Check the results
    verify_output(cfg, packets);
  endtask : test_bursts


  //---------------------------------------------------------------------------
  // Test Procedures
  //---------------------------------------------------------------------------

  // This performs a few directed test as a sanity check and to test a few
  // corner cases.
  task automatic directed_tests();
    burst_cfg_t cfg;

    // Iterate over different flow control settings to exercise different
    // scenarios.
    for (int bfm_config = 0; bfm_config < 4; bfm_config++) begin
      case (bfm_config)
        0 : begin
          // No stalls: on input or output to DUT
          axis_bfm.set_master_stall_prob(0);
          axis_bfm.set_slave_stall_prob(0);
        end
        1 : begin
          // Overflow: Input to DUT faster than output
          axis_bfm.set_master_stall_prob(10);
          axis_bfm.set_slave_stall_prob(30);
        end
        2 : begin
          // Underflow: Input to DUT slower than output
          axis_bfm.set_master_stall_prob(30);
          axis_bfm.set_slave_stall_prob(10);
        end
        3 : begin
          // Lots of stalls: Input and output stall frequently
          axis_bfm.set_master_stall_prob(40);
          axis_bfm.set_slave_stall_prob(40);
        end
      endcase

      //-------------------------------
      // Test Basic Configurations
      //-------------------------------

      // Test the default configuration
      cfg = DEFAULT_BURST_CFG;
      test.start_test($sformatf("Directed Test: bfm_config: %0d, %p", bfm_config, cfg));
      test_bursts(cfg);
      test.end_test();

      // Test a somewhat arbitrary but different configuration
      cfg = DEFAULT_BURST_CFG;
      cfg.spp         = 97;
      cfg.spp_last    = 33;
      cfg.num_bursts  = 2;
      cfg.num_packets = 3;
      cfg.amp         = 0.5;
      cfg.scale       = 1.25;
      cfg.freq        = 0.23;
      cfg.freq_shift  = 0.17;
      test.start_test($sformatf("Directed Test: bfm_config: %0d, %p", bfm_config, cfg));
      test_bursts(cfg);
      test.end_test();

      // Repeat with a single-sample packet
      cfg.spp      = 1;
      cfg.spp_last = 1;
      test.start_test($sformatf("Directed Test: bfm_config: %0d, %p", bfm_config, cfg));
      test_bursts(cfg);
      test.end_test();

      //-------------------------------
      // Test timed tunes
      //-------------------------------

      cfg = DEFAULT_BURST_CFG;
      cfg.spp             = 135;
      cfg.freq            = 1.0/32.0;
      cfg.freq_shift      = 0.0;
      cfg.num_bursts      = 1;
      cfg.num_packets     = 3;
      cfg.scale           = 0.75;
      cfg.freq_shift      = 0.0;    // Initial frequency shift
      cfg.tune_freq_shift = 0.13;   // New frequency shift

      // Test tuning in the middle of a packet
      cfg.tune_time = current_time + cfg.num_packets*cfg.spp/2;
      test.start_test($sformatf("Directed Test: bfm_config: %0d, %p", bfm_config, cfg));
      test_bursts(cfg);
      test.end_test();

      // Test tuning at the end of the first packet
      cfg.tune_time = current_time + cfg.spp-1;
      test.start_test($sformatf("Directed Test: bfm_config: %0d, %p", bfm_config, cfg));
      test_bursts(cfg);
      test.end_test();

      // Test tuning at the beginning of a packet
      cfg.tune_time = current_time + cfg.spp;
      test.start_test($sformatf("Directed Test: bfm_config: %0d, %p", bfm_config, cfg));
      test_bursts(cfg);
      test.end_test();
    end
  endtask : directed_tests


  // This generates a randomized configuration exercises the DUT with that
  // configuration. This is repeated num_tests times, with a unique
  // configuration each time.
  task automatic random_tests(int num_tests);
    burst_cfg_t cfg;
    int master_stall_prob, slave_stall_prob;

    repeat (num_tests) begin
      // Choose random values for this run. Round the floating point numbers to
      // a smaller number of bits to reduce rounding differences between the
      // testbench and the DUT.
      cfg = DEFAULT_BURST_CFG;
      cfg.spp             = $urandom_range(1, 64);
      cfg.spp_last        = $urandom_range(1, 64);
      cfg.num_packets     = $urandom_range(1, 3);
      cfg.num_bursts      = $urandom_range(1, 2);
      cfg.amp             = round_bits(frand_range(1.0/16.0, 15.0/16.0), 15);
      cfg.freq            = frand(0.5);
      cfg.timed           = $urandom_range(0, 1);
      cfg.scale           = round_bits(frand_range(-4.0, 4.0), 15);
      cfg.freq_shift      = round_bits(frand(0.5), 32);
      cfg.tune_freq_shift = round_bits(frand(0.5), 32);
      if (cfg.timed) begin
        cfg.tune_time = current_time +
          $urandom_range(0, (cfg.num_packets-1)*cfg.spp + cfg.spp_last - 1);
      end
      master_stall_prob = $urandom_range(0, 50);
      slave_stall_prob  = $urandom_range(0, 50);

      // Run the test
      test.start_test($sformatf("Random Test: InStall: %0d, OutStall: %0d, %p",
        master_stall_prob, slave_stall_prob, cfg));
      axis_bfm.set_master_stall_prob(master_stall_prob);
      axis_bfm.set_slave_stall_prob(slave_stall_prob);
      test_bursts(cfg);
      test.end_test();
    end
  endtask : random_tests


  //---------------------------------------------------------------------------
  // Main Test Process
  //---------------------------------------------------------------------------

  initial begin : main
    test.start_tb("dds_timed_tb");

    // Start the BFMs running
    axis_bfm.run();

    // Reset
    clk_gen.reset();
    @(negedge rst);

    //-------------------------------
    // Run Tests
    //-------------------------------

    directed_tests();
    random_tests(200);

    test.end_tb();
  end

endmodule


`default_nettype wire