Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[util] Add a withShadowLayer Queue #4589

Merged
merged 13 commits into from
Jan 7, 2025
Merged

Conversation

seldridge
Copy link
Member

@seldridge seldridge commented Jan 3, 2025

Add a method to the Queue$ object that can be used to create a Queue and include a second queue that contains some data which will be tracked alongside the first Queue, but this will be kept in a layer.

This is added to help address a use case where design verification wants to track some extra, sidecar data that should be added to some data pipeline. However, this sidecar data is expected to be removed from the design. This would normally be addressed with a generator-time parameter. However, the generator-time parameter prevents removal of the sidecar data unless the design is re-elaborated.

E.g., below, this can be used to create a shadow queue outside the module which contains a queue. The shadow queue will operate in lockstep with the original queue:

class Bar extends Module {
  val in = IO(Flipped(Decoupled(UInt(8.W))))
  val out = IO(Decoupled(UInt(8.W)))

  val (queue, shadow) = Queue.withShadow(enq = in)
  out :<>= queue
}

class Foo extends Module {
  val in = IO(Flipped(Decoupled(UInt(8.W))))
  val out = IO(Decoupled(UInt(8.W)))
  val outShadow = IO(probe.Probe(Valid(UInt(4.W)), layers.Verification))

  val bar = Module(new Bar)
  bar.in :<>= in
  out :<>= bar.out

  private val id = Wire(probe.Probe(UInt(4.W), layers.Verification))
  layer.block(layers.Verification) {
    val (_id, _) = Counter(in.fire, 15)
    probe.define(id, probe.ProbeValue(_id))
  }
  probe.define(outShadow, bar.shadow(probe.read(id), layers.Verification))
}

This compiles to the following Verilog:

// Generated by CIRCT firtool-1.99.2
// VCS coverage exclude_file
module ram_2x8(
  input        R0_addr,
               R0_en,
               R0_clk,
  output [7:0] R0_data,
  input        W0_addr,
               W0_en,
               W0_clk,
  input  [7:0] W0_data
);

  reg [7:0] Memory[0:1];
  always @(posedge W0_clk) begin
    if (W0_en & 1'h1)
      Memory[W0_addr] <= W0_data;
  end // always @(posedge)
  assign R0_data = R0_en ? Memory[R0_addr] : 8'bx;
endmodule

module Queue2_UInt8(
  input        clock,
               reset,
  output       io_enq_ready,
  input        io_enq_valid,
  input  [7:0] io_enq_bits,
  input        io_deq_ready,
  output       io_deq_valid,
  output [7:0] io_deq_bits
);

  reg  wrap;
  reg  wrap_1;
  reg  maybe_full;
  wire ptr_match = wrap == wrap_1;
  wire empty = ptr_match & ~maybe_full;
  wire full = ptr_match & maybe_full;
  wire do_enq = ~full & io_enq_valid;
  always @(posedge clock) begin
    if (reset) begin
      wrap <= 1'h0;
      wrap_1 <= 1'h0;
      maybe_full <= 1'h0;
    end
    else begin
      automatic logic do_deq = io_deq_ready & ~empty;
      if (do_enq)
        wrap <= wrap - 1'h1;
      if (do_deq)
        wrap_1 <= wrap_1 - 1'h1;
      if (~(do_enq == do_deq))
        maybe_full <= do_enq;
    end
  end // always @(posedge)
  ram_2x8 ram_ext (
    .R0_addr (wrap_1),
    .R0_en   (1'h1),
    .R0_clk  (clock),
    .R0_data (io_deq_bits),
    .W0_addr (wrap),
    .W0_en   (do_enq),
    .W0_clk  (clock),
    .W0_data (io_enq_bits)
  );
  assign io_enq_ready = ~full;
  assign io_deq_valid = ~empty;
endmodule

module Bar(
  input        clock,
               reset,
  output       in_ready,
  input        in_valid,
  input  [7:0] in_bits,
  input        out_ready,
  output       out_valid,
  output [7:0] out_bits
);

  wire out_ready_probe = out_ready;
  Queue2_UInt8 deq_q (
    .clock        (clock),
    .reset        (reset),
    .io_enq_ready (in_ready),
    .io_enq_valid (in_valid),
    .io_enq_bits  (in_bits),
    .io_deq_ready (out_ready),
    .io_deq_valid (out_valid),
    .io_deq_bits  (out_bits)
  );
endmodule

// VCS coverage exclude_file
module ram_2x4(
  input        R0_addr,
               R0_en,
               R0_clk,
  output [3:0] R0_data,
  input        W0_addr,
               W0_en,
               W0_clk,
  input  [3:0] W0_data
);

  reg [3:0] Memory[0:1];
  always @(posedge W0_clk) begin
    if (W0_en & 1'h1)
      Memory[W0_addr] <= W0_data;
  end // always @(posedge)
  assign R0_data = R0_en ? Memory[R0_addr] : 4'bx;
endmodule

module Foo(
  input        clock,
               reset,
  output       in_ready,
  input        in_valid,
  input  [7:0] in_bits,
  input        out_ready,
  output       out_valid,
  output [7:0] out_bits
);

  wire _bar_in_ready;
  Bar bar (
    .clock     (clock),
    .reset     (reset),
    .in_ready  (_bar_in_ready),
    .in_valid  (in_valid),
    .in_bits   (in_bits),
    .out_ready (out_ready),
    .out_valid (out_valid),
    .out_bits  (out_bits)
  );
  assign in_ready = _bar_in_ready;
endmodule


// ----- 8< ----- FILE "verification/layers-Foo-Verification.sv" ----- 8< -----

// Generated by CIRCT firtool-1.99.2
`ifndef layers_Foo_Verification
`define layers_Foo_Verification
bind Foo Foo_Verification verification (
  .in_ready                      (_bar_in_ready),
  .in_valid                      (in_valid),
  .clock                         (clock),
  .reset                         (reset),
  .bar_in_ready                  (_bar_in_ready),
  .bar_q_io_deq_ready_bore_ready (Foo.bar.out_ready_probe)
);
`endif // layers_Foo_Verification

// ----- 8< ----- FILE "verification/Queue2_UInt4.sv" ----- 8< -----

// Generated by CIRCT firtool-1.99.2
module Queue2_UInt4(
  input        clock,
               reset,
               io_enq_valid,
  input  [3:0] io_enq_bits,
  input        io_deq_ready,
  output       io_deq_valid,
  output [3:0] io_deq_bits
);

  wire io_enq_ready;
  reg  wrap;
  reg  wrap_1;
  reg  maybe_full;
  wire ptr_match = wrap == wrap_1;
  wire empty = ptr_match & ~maybe_full;
  wire do_enq = io_enq_ready & io_enq_valid;
  assign io_enq_ready = ~(ptr_match & maybe_full);
  always @(posedge clock) begin
    if (reset) begin
      wrap <= 1'h0;
      wrap_1 <= 1'h0;
      maybe_full <= 1'h0;
    end
    else begin
      automatic logic do_deq = io_deq_ready & ~empty;
      if (do_enq)
        wrap <= wrap - 1'h1;
      if (do_deq)
        wrap_1 <= wrap_1 - 1'h1;
      if (~(do_enq == do_deq))
        maybe_full <= do_enq;
    end
  end // always @(posedge)
  ram_2x4 ram_ext (
    .R0_addr (wrap_1),
    .R0_en   (1'h1),
    .R0_clk  (clock),
    .R0_data (io_deq_bits),
    .W0_addr (wrap),
    .W0_en   (do_enq),
    .W0_clk  (clock),
    .W0_data (io_enq_bits)
  );
  assign io_deq_valid = ~empty;
endmodule


// ----- 8< ----- FILE "verification/Foo_Verification.sv" ----- 8< -----

// Generated by CIRCT firtool-1.99.2
module Foo_Verification(
  input in_ready,
        in_valid,
        clock,
        reset,
        bar_in_ready,
        bar_q_io_deq_ready_bore_ready
);

  wire [3:0] _shadowDeq_bits_probe;
  wire       _shadowDeq_valid_probe;
  reg  [3:0] _id;
  wire [3:0] _id_probe = _id;
  always @(posedge clock) begin
    if (reset)
      _id <= 4'h0;
    else if (in_ready & in_valid)
      _id <= _id == 4'hE ? 4'h0 : _id + 4'h1;
  end // always @(posedge)
  Queue2_UInt4 shadowQueue_q (
    .clock        (clock),
    .reset        (reset),
    .io_enq_valid (bar_in_ready & in_valid),
    .io_enq_bits  (Foo_Verification._id_probe),
    .io_deq_ready (bar_q_io_deq_ready_bore_ready),
    .io_deq_valid (_shadowDeq_valid_probe),
    .io_deq_bits  (_shadowDeq_bits_probe)
  );
endmodule


// ----- 8< ----- FILE "ref_Foo.sv" ----- 8< -----

// Generated by CIRCT firtool-1.99.2
`define ref_Foo_outShadow_valid verification._shadowDeq_valid_probe
`define ref_Foo_outShadow_bits verification._shadowDeq_bits_probe

Release Notes

  • Add Queue.withShadow utility that assists with adding data to a queue that will be relegated to a specific layer.

@seldridge seldridge requested review from mwachs5 and azidar January 3, 2025 21:00
@seldridge seldridge added the Feature New feature, will be included in release notes label Jan 3, 2025
@mwachs5
Copy link
Contributor

mwachs5 commented Jan 3, 2025

I think with my suggested tweak this would become much more useful...

The Probe.apply API is supposed to take a second argument of type `Layer`.
However, this was incorrectly taking an argument of type `Option[Layer]`.
Revert to the former.

Signed-off-by: Schuyler Eldridge <[email protected]>
Add a method to the `Queue$` object that can be used to create a `Queue`
and include a second queue that contains some data which will be tracked
alongside the first `Queue`, but this will be kept in a layer.

This is added to help address a use case where design verification wants
to track some extra, sidecar data that should be added to some data
pipeline.  However, this sidecar data is expected to be removed from the
design.  This would normally be addressed with a generator-time parameter.
However, the generator-time parameter prevents removal of the sidecar data
unless the design is re-elaborated.

Signed-off-by: Schuyler Eldridge <[email protected]>
@seldridge seldridge force-pushed the dev/seldridge/shadow-queue-util branch from c5ff54a to a56480f Compare January 4, 2025 03:18
@seldridge seldridge marked this pull request as ready for review January 4, 2025 03:41
val shadowQueue = Queue(shadowEnq, entries, pipe, flow, useSyncReadMem, flush.map(BoringUtils.tapAndRead))

val _shadowDeq = Wire(Valid(chiselTypeOf(data)))
_shadowDeq.valid :<= shadowQueue.valid
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we assert that this valid always equals the original valid...? Is there a reason they should not?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should always be the same. If they're not then it's a bug in this generator.

Maybe we want something that is a layer for the purposes of testing the generator itself. 🧐 I'm going to take no action on this for now.

src/main/scala/chisel3/util/Queue.scala Outdated Show resolved Hide resolved
* @return output (dequeue) interface from the queue and a [[ShadowFactory]]
* for creating shadow [[Queue]]s
*/
def withShadow[T <: Data](
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this have two data types? Shadow is probably not the same type as T...?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interestingly, no, due to how ShadowFactory works. A ShadowFactory takes a type parameter that is the same as the queue it was created from. However, it has a method that has a different type parameter that is the type of the shadow.

I.e., if you start with a Queue[UInt], you get a ShadowFactory[UInt]. However, that has a method apply[A <: Data] where A is the type of the new shadow queue. This A isn't fixed, however. I.e., if you create a ShadowFactory, you then want to create an A that can be a UInt, SInt, or any user-defined Bundle.

If withShadow took a second type parameter, then it would be locked in to creating shadow queues of only that type (or it would be something overly pessimistic like Data requiring asTypeOf casts). (I originally ran into this problem and switched this to the ShadowFactory approach to avoid it!)

Sound good?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure, I agree with what you've said that we don't need to pass the desired shadow type through, though it still seems like the ShadowFactory type might as well just be Data... I am not sure how the fact that the original queue was of type T is used at all by the code in ShadowFactory? not blocking

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh! i see you have changed this so that ShadowFactory no longer takes the T parameter 👍

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, your intuition was right here in that it doesn't need the type parameter at all. That was a nice suggestion.

src/main/scala/chisel3/util/Queue.scala Outdated Show resolved Hide resolved
@seldridge seldridge requested a review from mwachs5 January 6, 2025 17:10
@seldridge
Copy link
Member Author

95a3370 includes a shadow method for the Queue module as a complement to the path of Queue$.withShadow. This does not include a test of the former as the code path is the same as with the latter.

@seldridge
Copy link
Member Author

82c46c2 adds clock/reset capturing to make sure that when the shadow queue is created it is using the same clock and reset that the original queue is. There is no way to override this, however, it would be very unsafe to do so.

@seldridge seldridge enabled auto-merge (squash) January 7, 2025 02:00
@seldridge seldridge merged commit 69463d8 into main Jan 7, 2025
15 checks passed
@seldridge seldridge deleted the dev/seldridge/shadow-queue-util branch January 7, 2025 02:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Feature New feature, will be included in release notes
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants