Distributing task calls between SV implementations in "generate" blocks

In our “ram” library element, we compile in accessor tasks during simulation. Currently, we globally define LIB_VENDOR or LIB_BEHAVIORAL to control implementation. So we have code like:

module ram; 
  `ifdef LIB_VENDOR
     vendor_ram uINST ();
     task RamWrite (input a, output b);
     begin uINST.VendorRamWrite(a, b); end
     endtask
  `else
     behavioral_ram uINST ();
     task RamWrite (input a, output b);
     begin uINST.BehavioralRamWrite(a, b); end
     endtask
  `endif
endmodule

We use the RamWrite task to normalize the task names, arguments, etc, between the various implementations (or to print an error if the implementation doesn’t support a backdoor task).

Now we’d like to migrate to having the implementation controlled by parameter (for example, we may want to sometimes use “RTL RAM” for small memories). I was thinking that the following should work:

module ram #(parameter Vendor = 0) ();
  generate
    if (Vendor) begin : impl_vendor
      vendor_ram uINST ();
    end
    else begin : impl_rtl
      behavioral_ram uINST ();
    end

    task RamWrite (input a, output b);
      begin
        if (Vendor) impl_vendor.uINST.VendorRamWrite(a, b);
        else impl_rtl.uINST.BehavioralRamWrite(a, b);
      end
    endtask // RamWrite

  endgenerate
endmodule

module vendor_ram;
  task VendorRamWrite (input a, output b);
    begin $display("vendor ram write"); end
  endtask // VendorRamWrite
endmodule

module behavioral_ram;
  task BehavioralRamWrite (input a, output b);
    begin $display("behav ram write"); end
  endtask // BehavioralRamWrite
endmodule

… But this doesn’t work. It seems that SV, for the instances, only “goes into” one of two paths, but within the ram.RamWrite task, it wants both to be there. This doesn’t seem necessary … I believe I understand the classic issues of calling a task within an array of instances, etc, but in this case I thought it should work. Does this seem like a vendor-specific tool issue, and if not, what’s the “recommended” way to do this kind of thing? Note this is design-side SV code, so it would be nice to keep the verif-side complexities (UVM, classes, etc) out of it … if possible.

In reply to simonsabato:

SystemVerilog requires all hierarchical references to be bound at elaboration. There would have to be optimization rules to skip the binding. This might be doable at some point in the future with a generate-if construct, but more difficult with a procedural-if construct.

A suggestion for you is creating two ram module wrappers, one wrapping each implementation, and using a Verilog config construct to select the ram on a per instance basis. See See section 33 of 1800-2017 LRM and this paper: https://lcdm-eng.com/papers/snug01_verilog2000.pdf

In reply to dave_59:

Thanks Dave, that was helpful. The contrast of “generate-if” vs “procedural-if” is what I was missing.

As an aside, while waiting for the response I was playing around and found that (at least on one tool) the following works:

module ram #(parameter Vendor = 0) ();
  generate
    if (Vendor) begin : impl
      vendor_ram uINST ();
    end
    else begin : impl
      behavioral_ram uINST ();
    end
    task RamWrite (input a, output b);
      begin
        if (Vendor) impl.uINST.RamWrite(a, b);
        else impl.uINST.RamWrite(a, b);
      end
    endtask // RamWrite
  endgenerate
endmodule

module vendor_ram;
  task RamWrite (input a, output b);
    begin $display("vendor ram write"); end
  endtask // VendorRamWrite
endmodule

module behavioral_ram;
  task RamWrite (input a, output b);
    begin $display("behav ram write"); end
  endtask // BehavioralRamWrite
endmodule

Basically, name the two generate blocks the same, name the tasks the same, and ensure tasks have the same arguments. This probably doesn’t solve my original problem because I need to be able to call different subtasks.

I’m asking this followup because I’m not sure if this is expected to work (cross-vendor etc). It feels a bit like I’m “fooling” the tool. I did run it and verified that it calls the right sub-module’s task when I change the top level parameter.

If this is legal, I could imagine building an additional layer of “distributor” tasks (within vendor_ram and behavioral_ram) which have the same name/arguments (RamWrite), which then call the local versions (VendorRamWrite & BehavioralRamWrite). If that is somehow valid it’s another way to get this to work.

I’ll still look into the config files. AFAIK those are the only way to swap in a really different implementation (behavioral, gate level, etc) so it’s a powerful technique. It’s not ideal for the problem I was trying to solve as I’m trying to make a very reusable library and I don’t want the “integration” person to be in the loop for these RAM mappings. If the config file can be setup hierarchically that could work…

In reply to simonsabato:
Yes, what you wrote is legal. I think Example 4 in LRM section 27.5 is very similar to what you are doing.

And yes, you can build up config declarations hierarchically.

In reply to dave_59:

Dave,

THANKS!

That was exactly the shove that I needed. The following code shows a working solution to the original question. It does use a layer of “distributor” tasks. But since the “distribution layer” is within the wrapper, it doesn’t require modifying the vendor_ram / behavioral_ram implementations, which is key (they may be 3rd party, encrypted etc).

Specifically the two things I was missing:

  • LRM 27.5 discussion on how multiple generate blocks can use the same name if only one will be included
  • Putting tasks within the conditional generate blocks (next to the RAM instances) allows for a unique task for each possible implementation, all having the same name/args, such that the top level RamWrite task can just call impl.RamWrite unconditionally.
module ram #(parameter Vendor = 0) ();
  generate
    if (Vendor) begin : impl
      vendor_ram uINST ();
      task RamWrite (input a, output b);
        uINST.VendorRamWrite(a, b);
      endtask // RamWrite
    end
    else begin : impl
      behavioral_ram uINST ();
      task RamWrite (input a, output b);
        uINST.BehavioralRamWrite(a, b, 0, 1);
      endtask // RamWrite
    end
    task RamWrite (input a, output b);
      impl.RamWrite(a, b);
    endtask // RamWrite
  endgenerate
endmodule

module vendor_ram;
  task VendorRamWrite (input a, output b);
    $display("vendor ram write");
  endtask // VendorRamWrite
endmodule

module behavioral_ram;
  task BehavioralRamWrite (input a, output b, input x, input y);
    $display("behav ram write");
  endtask // BehavioralRamWrite
endmodule