Skip to content
code0.HDL

Embedding RTL in a BSV design

The import "BVI" contract between typed BSV and external RTL, value and Action method mappings with enable and ready, default_clock and default_reset, schedule relations for every method pair, static parameters, and how to locate a failure in BSC compilation, Verilog elaboration or simulation.

In this chapter

  • Map a value or Action method onto RTL ports and say what each omitted enable or ready clause promises about the RTL
  • Declare CF, SB, SBR or C for every method pair of a wrapper and predict what a false relation lets bsc generate
  • Forward a static BSV argument to an RTL parameter and explain why a register value fails with G0053
  • Classify a wrapper failure by the phase that reports it before changing the mapping or the schedule
Concepts in this chapter
  • Embedding RTL in a BSV design
Prerequisite concepts

import "BVI" lets typed BSV code instantiate an existing Verilog or VHDL module, such as a vendor primitive or a block that is already verified. The import does not implement that module. It is a contract that says how BSV values, method calls, clocks, resets, and scheduling facts correspond to the external RTL's ports. bsc relies on the contract but cannot look inside the RTL to check it.

You need interfaces, value and Action methods, the Ready and Enable signals from Guiding the compiler with attributes, rules, and the same-cycle scheduling relations from Static and dynamic semantics. The previous chapter's question carries over: which facts does the compiler prove, and which does it trust?

Every example contains a BSV file and a Verilog file. Run compiles the BSV with bsc, links the generated Verilog with the external RTL, and simulates the result with Icarus Verilog.

1. The BVI declaration

1.1 A boundary with three phases

A BVI wrapper is a declarative adapter between the typed BSV world and the untyped RTL world:

BSV caller -> typed interface -> BVI contract -> generated port wiring -> RTL

Different facts are checked at different times, and some are never checked.

PhaseWhat is checkedWhat you must guarantee
BSC compilation and elaborationmethod types and widths, static parameters, schedule consistencythat the mapped ports and protocol match the real RTL
Verilog elaborationthat the module and port names existcorrect module names, port names, and widths
Simulation and hardwarenothing beyond the RTL itselfthat the RTL really follows the declared ready, enable, clock, reset, and schedule

A wrapper can therefore compile and link while lying about readiness, reset polarity, or legal concurrency.

1.2 Declaration form

Syntax

import "BVI" RtlModuleName =
module mkWrapperName#(arguments)(InterfaceType);
  // optional ordinary module statements
  // parameter, clock, reset, port, method, schedule, path statements
endmodule
interface AddIfc;
  method Bit#(8) add(Bit#(8) a, Bit#(8) b);
endinterface

import "BVI" ExternalAdder =
module mkExternalAdder(AddIfc);
  default_clock no_clock;
  default_reset no_reset;
  method SUM add(A, B);
  schedule (add) CF (add);
endmodule
PartMeaning
import "BVI" ExternalAdder =the RTL module to instantiate; if omitted, the BSV module name is used
module mkExternalAdder(AddIfc)an ordinary BSV module name and interface for callers
default_clock, default_resetwhich clock and reset the RTL uses
method SUM add(A, B);which RTL ports carry the method's arguments and result
schedule (add) CF (add);how calls to the methods may be combined in one cycle

Wrapper arguments and interface values need Bits instances or a supported boundary type such as Clock, Reset, or Inout.

2. Value methods

2.1 Mapping a value method

For a value method, write the result port before the method name and the argument ports in argument order.

Syntax

method RESULT_PORT methodName(ARG_PORT1, ARG_PORT2, ...);
// BSV interface: method Bit#(8) add(Bit#(8) a, Bit#(8) b);
method SUM add(A, B);
BSV eventBVI mappingRTL event
a rule evaluates add(19, 23)A = 19, B = 23two 8-bit input ports are driven
the rule uses the resultSUM is the result portthe RTL drives an 8-bit result

The method type in the interface gives bsc the widths of all three ports.

2.2 Blocks without a clock

A purely combinational block has no clock or reset. Two declarations say so explicitly.

Syntax

default_clock no_clock;
default_reset no_reset;

A method mapping with no ready clause promises that the method is always ready. That promise is true for an adder, which can produce a sum in every cycle.

2.3 Example: an external adder

BVIAdder.bsv wraps the Verilog module ExternalAdder. Read AddIfc and the BVI mapping first, then compare A, B, and SUM with the port list in ExternalAdder.v. The rule show calls add once and prints the result.

Predict before you run.

  1. What decimal sum does add(8'd19, 8'd23) produce?
  2. Does the result need a clock edge, or is it available in the same cycle?
  3. Where do the 8-bit widths of A, B, and SUM come from on the BSV side?

Check

The rule show calls adder.add(8'd19, 8'd23) and prints the result in the same rule. What is printed, and does the result need a clock edge?

Your answer

BVIAdder.bsv
package BVIAdder;

interface AddIfc;
  method Bit#(8) add(Bit#(8) a, Bit#(8) b);
endinterface

import "BVI" ExternalAdder =
module mkExternalAdder(AddIfc);
  default_clock no_clock;
  default_reset no_reset;
  // Map BSV method arguments and result to combinational RTL ports.
  method SUM add(A, B);
  schedule (add) CF (add);
endmodule

(* synthesize *)
module mkTop(Empty);
  AddIfc adder <- mkExternalAdder;

  rule show;
    $display("sum = %0d", adder.add(8'd19, 8'd23));
    $finish(0);
  endrule
endmodule

endpackage
ExternalAdder.v
module ExternalAdder (
  input  wire [7:0] A,
  input  wire [7:0] B,
  output wire [7:0] SUM
);
  assign SUM = A + B;
endmodule

Check the result.

ObservationWhat it proves
sum = 42the arguments reached A and B, and SUM came back as the method result
one rule both calls and printsthe path through the RTL is combinational; no state was needed

The panel may also show warning S0073 about a duplicate -vsearch directory. It comes from the command line and does not affect the result.

Try it yourself.

  1. Change the arguments to 8'd200 and 8'd100. The output is 44, not 300. The RTL adds two 8-bit values into an 8-bit SUM, and the method type promised exactly that width. The wraparound happens in the external logic, and bsc has nothing to object to.
  2. Restore the arguments and change the mapping to method S add(A, B);. BSC still compiles the wrapper, because it never reads the RTL header. The failure appears one phase later: Verilog elaboration reports that S is not a port of the adder instance. A name mismatch is a link-time error, not a BSV type error.

3. Action methods and clocked RTL

3.1 Mapping an Action method

An Action or ActionValue method maps its call protocol as well as its data.

Syntax

method methodName(ARG_PORT, ...) enable(ENABLE_PORT) ready(READY_PORT);
// BSV interface: method Action write(Bit#(8) value);
method write(D_IN) enable(EN) ready(RDY);
ClauseMeaning
(D_IN)the argument drives D_IN
enable(EN)EN is asserted in every cycle the method is called
ready(RDY)RDY becomes the method's implicit condition; callers fire only while it is high
Omitted clauseWhat the omission promises
readythe method is always ready
enable on an Action methodthe RTL does not need a per-call pulse

Note. Leaving out a clause does not generate the missing protocol logic. The omission is itself a promise about the RTL.

3.2 Clock and reset

Syntax

default_clock clockName(CLOCK_PORT);
default_reset resetName(RESET_PORT);
default_clock clk(CLK);
default_reset rst(RST_N);

These statements connect the wrapper's default clock to the RTL port CLK and the default reset to RST_N. BVI reset inputs are assumed active low, which matches the common RST_N convention.

Check

A variant of ExternalRegister resets while its port RST is high (if (RST) Q_OUT <= 0). The wrapper says default_reset rst(RST); and the test rules are unchanged. What happens?

Your answer

3.3 Example: writing and reading an external register

ClockedRegister.bsv wraps ExternalRegister, an 8-bit register with an enable. The write mapping has no ready, so the register is promised to be always writable. Read the BVI declaration before the test rules and check this mapping.

BSV declaration or callRTL port behavior
default_clock clk(CLK)the wrapper clock drives CLK
default_reset rst(RST_N)BSV reset appears on the active-low RST_N
imported.write(8'h5a)D_IN carries the value and EN marks the call
imported.readQ_OUT supplies the result

The cycle trace is:

  1. Reset sets Q_OUT and the BSV step register to zero.
  2. With step == 0, writeValue calls write. D_IN carries 5a and EN is high, so the external register stores 5a at the clock edge. step becomes 1 at the same edge.
  3. In the next cycle only showValue is enabled. read returns the value on Q_OUT, and the simulation finishes.

Predict before you run.

  1. Which value does showValue print?
  2. Why is it not the reset value 00?
  3. Which RTL port carries the call protocol for write?

Check

writeValue calls imported.write(8'h5a) while step == 0, and showValue reads imported.read while step == 1. What does showValue print?

Your answer

ClockedRegister.bsv
package ClockedRegister;

interface RegisterIfc;
  method Action write(Bit#(8) value);
  method Bit#(8) read;
endinterface

import "BVI" ExternalRegister =
module mkExternalRegister(RegisterIfc);
  // Bind the imported sequential logic to the wrapper clock and reset.
  default_clock clk(CLK);
  default_reset rst(RST_N);
  method write(D_IN) enable(EN);
  method Q_OUT read;
  schedule (read) CF (read);
  schedule (read) SB (write);
  schedule (write) C (write);
endmodule

(* synthesize *)
module mkTop(Empty);
  RegisterIfc imported <- mkExternalRegister;
  Reg#(Bit#(2)) step <- mkReg(0);

  rule writeValue (step == 0);
    imported.write(8'h5a);
    step <= 1;
  endrule

  rule showValue (step == 1);
    $display("stored = %h", imported.read);
    $finish(0);
  endrule
endmodule

endpackage
ExternalRegister.v
module ExternalRegister (
  input  wire       CLK,
  input  wire       RST_N,
  input  wire [7:0] D_IN,
  input  wire       EN,
  output reg  [7:0] Q_OUT
);
  always @(posedge CLK) begin
    if (!RST_N)
      Q_OUT <= 8'h00;
    else if (EN)
      Q_OUT <= D_IN;
  end
endmodule

Practice this concept

Use this concept in a short BSV problem as your next step.

Check the result.

ObservationWhat it proves
stored = 5aD_IN and EN reached the RTL, and the value was stored at the edge
the value is not 00the read happened one cycle after the write's clock edge

Try it yourself.

  1. Change the guard of showValue to step == 0 so that the read and write are requested in the same cycle. The schedule declares read SB write, so bsc fires both rules in one cycle with the read logically first. The read sees Q_OUT before the edge, and the run prints 00. That matches the RTL, which updates only at the edge. Had the wrapper declared write SB read, bsc would assume the read sees the new value, and the declaration would misdescribe the hardware.

Map a module you have not seen. ExtBuffer is a one-entry buffer. It accepts data while ENQ_RDY is high, offers it on FIRST while DEQ_RDY is high, and empties itself when DEQ_EN is pulsed.

module ExtBuffer (
  input  wire       CLK,
  input  wire       RST_N,
  input  wire [7:0] ENQ_DATA,
  input  wire       ENQ_EN,
  output wire       ENQ_RDY,   // high while the buffer is empty
  input  wire       DEQ_EN,
  output wire       DEQ_RDY,   // high while the buffer is full
  output wire [7:0] FIRST
);
interface BufIfc;
  method Action enq(Bit#(8) x);
  method Action deq;
  method Bit#(8) first;
endinterface

Check

Which method mapping matches ExtBuffer, assuming default_clock clk(CLK); and default_reset rst(RST_N);?

Your answer

Check

Keep the right mapping but drop ready(ENQ_RDY) from enq. A rule produce calls enq every cycle; a rule consume calls first and deq; the schedule says enq conflicts (C) with first and deq. What do you see?

Your answer

4. schedule declarations

4.1 Relating method pairs

bsc cannot infer how external methods interact. A schedule statement relates every method in its left list to every method in its right list, using the same relations that library primitives declare in Static and dynamic semantics.

Syntax

schedule (method1, method2, ...) OPERATOR (method3, ...);
schedule (read) CF (read);
schedule (read) SB (write);
schedule (write) C (write);
OperatorMeaning
CFconflict free; the calls may happen together in any order
SBthe left method is sequenced before the right; both may be used in one rule
SBRleft before right, but the two cannot be used in the same rule
Cthe calls conflict and cannot happen in the same cycle

4.2 Reading the register's schedule

Call combinationAllowed?Why
read + readyes, CFboth only observe Q_OUT
read + writeyes, read first (SB)the read sees the old value and the write lands at the edge
write + writeno, Cthere is one D_IN and one EN, with no arbitration

Note. A schedule is a promise, not an optimization hint. Declaring CF for operations that share an unarbitrated resource lets bsc generate invalid concurrency. Declaring C where the RTL is independent stays correct but blocks useful parallelism.

If a pair of methods has no relation at all, bsc warns with P0200, "No scheduling annotation given", and assumes a conflict. Declaring the same pair twice with different operators is an error. Check every pair of methods, including each method with itself.

Check

The ExtBuffer wrapper declares schedule (first) CF (first); (first) SB (deq); (first, deq) C (enq); (enq) C (enq); and nothing else. What does bsc report?

Your answer

5. Static parameters

5.1 Forwarding a parameter

BSV constructor arguments and RTL parameters are unrelated until a parameter statement connects them.

Syntax

parameter RTL_PARAMETER = staticExpression;
import "BVI" ExternalLimiter =
module mkExternalLimiter#(Integer limit)(LimiterIfc);
  parameter LIMIT = limit;
  ...
endmodule

The right side must be a value known at compile time: a String, an Integer, or a Bit#(n) constant. A numeric type can be turned into a value with valueOf. A register's contents cannot become a parameter: passing a register value fails with error G0053, which says the parameter is instantiated with a dynamic expression.

CarriesFixed whenCan change while running?
parameterelaborationno; a different value is different hardware
method argumentevery callyes

5.2 Example: a fixed clamping limit

ParameterizedLimiter.bsv passes the BSV argument limit to the Verilog parameter LIMIT. Read the constructor call in mkTop, then the parameter statement, then the comparison in ExternalLimiter.v.

The static trace is: BSV elaborates mkExternalLimiter(100), the generated Verilog instantiates ExternalLimiter with LIMIT = 100, and at run time clamp presents 140 to that fixed instance.

Predict before you run.

  1. What does clamp(8'd140) return when LIMIT is 100?
  2. Could a rule change LIMIT after the instance exists?

Check

mkTop instantiates mkExternalLimiter(100) and calls limiter.clamp(8'd140). What is printed?

Your answer

ParameterizedLimiter.bsv
package ParameterizedLimiter;

interface LimiterIfc;
  method Bit#(8) clamp(Bit#(8) value);
endinterface

import "BVI" ExternalLimiter =
module mkExternalLimiter#(Integer limit)(LimiterIfc);
  // Forward a compile-time BSV argument to the RTL parameter.
  parameter LIMIT = limit;
  default_clock no_clock;
  default_reset no_reset;
  method OUT clamp(IN);
  schedule (clamp) CF (clamp);
endmodule

(* synthesize *)
module mkTop(Empty);
  LimiterIfc limiter <- mkExternalLimiter(100);

  rule show;
    $display("clamped = %0d", limiter.clamp(8'd140));
    $finish(0);
  endrule
endmodule

endpackage
ExternalLimiter.v
module ExternalLimiter #(
  parameter integer LIMIT = 255
) (
  input  wire [7:0] IN,
  output wire [7:0] OUT
);
  assign OUT = (IN > LIMIT) ? LIMIT[7:0] : IN;
endmodule

Practice this concept

Use this concept in a short BSV problem as your next step.

Check the result.

ObservationWhat it proves
clamped = 100140 exceeded the limit, so the RTL returned LIMIT
the limit came from mkExternalLimiter(100)the BSV argument reached the RTL parameter during elaboration

Try it yourself.

  1. Change the constructor argument to 150. The generated instance now has LIMIT = 150, 140 passes through, and the run prints 140. The method call is unchanged; elaboration produced different hardware.

If the limit had to change while the design runs, a parameter would be the wrong tool. You would need a setLimit method, and the RTL would need storage and a write protocol for it.

6. Other boundary statements

6.1 Standalone ports, clocks, and resets

port MODE = 2'b01;
input_clock fastClk(FAST_CLK) = clk2;
input_reset fastRst(FAST_RST_N) clocked_by(fastClk) = rst2;
StatementPurpose
port NAME = expression;drives an RTL input that belongs to no method, such as a mode wire
input_clockconnects a BSV Clock argument to an RTL clock port
output_clockexposes a clock that the RTL generates
input_reset, output_resetthe same for resets; BVI resets are assumed active low
ancestor, same_familydeclare that clocks are derived from each other or share a phase relationship
no_clock, no_resetexplicitly remove the default domains

Use clocked_by and reset_by on a method or port that does not belong to the default domain.

Note. Clock relationship declarations only let bsc check cross-domain connections. They do not build a synchronizer.

6.2 Combinational paths, subinterfaces, and inout

Syntax

path (INPUT_PORT, OUTPUT_PORT);

A path statement reports that an input port affects an output port in the same cycle. bsc uses it to detect combinational loops across the module boundary. A missing real path can hide a loop; an invented path can reject a legal design.

Nested interface statements group mapped ports into a BSV subinterface. inout imports a bidirectional RTL port as an Inout#(t), and ifc_inout connects an existing BSV Inout argument. Because these wires cross the boundary directly, state their width and domain explicitly.

7. Diagnose the phase before changing the wrapper

Classify a failure by phase before editing schedules or wiring.

PhaseVisible symptomLikely causeFirst check
BSC compilationtype, static-value, or schedule diagnosticwidth mismatch, dynamic parameter (G0053), duplicate relationmethod types and BVI statements
Verilog elaborationunknown module or "is not a port" errorRTL file missing, module or port name wronggenerated instance against the RTL header
Simulation, no progressa rule never fireswrong or permanently low ready, wrong reset bindingReady and reset waveforms
Simulation, wrong datacalls happen but values or order differswapped ports, false CF or SB, width loss, reset polarityone call traced from method to ports
Hardware onlyintermittent corruption between clock domainswrong clock-family claim, missing synchronizerthe physical domains and CDC design

A wrong port name is not fixed by weakening a schedule. A false always-ready claim may compile and link cleanly yet break the real protocol.

Check

BSC and Verilog elaboration are both clean, but a rule that calls start never fires in simulation. Which check comes first?

Your answer

Keep a BVI wrapper as thin as possible. It should map parameters, map methods and ports, declare clocks and resets, and state complete schedule and path relations. Put buffering, arbitration, and policy in an ordinary BSV module around it, where bsc can check them.

8. Common mistakes

8.1 A port name that does not exist in the RTL

// Wrong: the RTL output is called Q_OUT, not Q
method Q read;

// Right: copy the name from the RTL port list
method Q_OUT read;

bsc compiles both versions, because it never reads the RTL. Icarus Verilog then stops elaboration with an error saying that Q is not a port of the instance. Compare every name in the mapping with the RTL header.

8.2 A register value as an RTL parameter

// Wrong: G0053, the parameter is instantiated with a dynamic expression
Reg#(Bit#(8)) lim <- mkReg(100);
LimiterIfc limiter <- mkExternalLimiter(lim);

// Right: pass a value known during elaboration
LimiterIfc limiter <- mkExternalLimiter(100);

A parameter is fixed when the hardware is generated. A value that can change while running must travel through a method.

8.3 Omitting ready when the RTL has a ready output

// Wrong: the RTL drives RDY low while busy, but the wrapper ignores it
method start(CMD) enable(GO);

// Right: RDY becomes the implicit condition of start
method start(CMD) enable(GO) ready(RDY);

Without ready, the wrapper promises that start is always ready, so callers fire even while the RTL is busy. Nothing reports an error; commands are simply lost.

8.4 A schedule that contradicts the RTL timing

// Wrong: claims a same-cycle read sees the value written in that cycle
schedule (write) SB (read);

// Right: an edge-triggered register shows the old value
schedule (read) SB (write);

bsc believes the schedule and builds the design around it. If the RTL updates only at the clock edge, the read must be ordered first.

8.5 Leaving a method pair without a relation

// Incomplete: write with write has no relation, warning P0200
schedule (read) CF (read);
schedule (read) SB (write);

// Complete: every pair is stated
schedule (read) CF (read);
schedule (read) SB (write);
schedule (write) C (write);

The missing relation is treated as a conflict, which happens to be correct for this register. Write it down anyway, so the reader and the compiler see the same contract.

9. Summary

TopicKey point
BVI declarationimport "BVI" RtlName = module ... describes an RTL boundary that bsc cannot inspect
Value method mappingmethod RESULT name(ARGS);; the interface type sets the widths
Action method mappingenable marks the call, ready becomes the implicit condition; omitting either is a promise
Clock and resetdefault_clock, default_reset; no_clock and no_reset for combinational blocks; resets are active low
scheduleCF, SB, SBR, C for every method pair; a missing pair warns P0200 and assumes conflict
parameterstatic values only (G0053 otherwise); a different value is different hardware
Failure phaseBSC compilation, Verilog elaboration, or simulation, each pointing to a different cause

10. Check your understanding

  1. Which parts of a BVI contract can bsc check, and which must you guarantee yourself?

    Show answer

    bsc checks method types and widths, that parameters are static, and that schedule statements are consistent. You must guarantee that port names, readiness, enable behavior, reset polarity, and the declared schedule match the real RTL.

  2. Why does omitting ready change a protocol promise rather than just the port list?

    Show answer

    Without ready, the method has no implicit condition, so callers may fire in every cycle. If the RTL can actually be busy, those calls arrive when the RTL cannot accept them.

  3. In the register example, what does a same-cycle read return, and which schedule statement decides it?

    Show answer

    The old value, 00 right after reset. schedule (read) SB (write) orders the read before the write, matching an edge-triggered register.

  4. Why does changing an RTL parameter require new hardware, while changing a method argument does not?

    Show answer

    A parameter is fixed during elaboration and shapes the generated instance. A method argument is a value on input ports that can differ on every call.

  5. At which phase does a misspelled RTL port name appear, and at which phase would a false CF claim show up?

    Show answer

    A misspelled port name stops Verilog elaboration, after BSC has already succeeded. A false CF claim passes compilation and elaboration and shows up only in simulation or hardware as wrong data or ordering.

The Practice problems below work on these wrappers: completing the value mapping a fixed imported-register wrapper uses for writes, finding why a result mapping hides the values stored in the imported RTL, and passing a required initial value through a fixed BVI parameter mapping.

Checkpoint

Run the example and compare it with your prediction

Sources

Licences and notices for the sources this site follows