Almost every BSV design is built from the same handful of library pieces:
sized bit vectors, tuples, registers, FIFOs, and the simulator's system tasks.
This chapter teaches each one as a contract you can rely on: what its type
fixes before any hardware exists, what its methods do within one clock cycle,
and when a call makes a rule wait. The numeric types and their explicit width
conversions have their own chapter,
Types, widths and explicit conversion,
and so do mkReg and mkRegU, in
Registers and one rule. Here we
take up split and tuples, FIFO and FIFOF, and the system tasks.
You should be comfortable with Bit#(n), provisos such as Add from
Typeclasses and provisos, and the
difference between value methods, Action methods, and atomic rules from
Methods: value, Action, ActionValue.
Implicit conditions, from Guards: hold and stop,
matter most in the FIFO sections.
Every example compiles with bsc, generates Verilog, and simulates it with
Icarus Verilog. Run performs all three steps in order.
1. split and tuples
1.1 split takes its boundary from the result type
split divides one vector into a high part and a low part. It has no argument
that says where to cut. The caller writes the result type, and the compiler
solves the two widths from it.
Syntax
function Tuple2#(Bit#(m), Bit#(n)) split(Bit#(mn) value)
provisos (Add#(m, n, mn));Bit#(8) a = 8'hA5; // 1010_0101
Tuple2#(Bit#(2), Bit#(6)) parts = split(a);
// tpl_1(parts) is 2'b10 and tpl_2(parts) is 6'b100101| Part | Role |
|---|---|
Bit#(m) | the first result: the high m bits |
Bit#(n) | the second result: the low n bits |
Bit#(mn) | the input vector |
Add#(m, n, mn) | requires m + n to equal the input width |
If the two result widths do not add up to the input width, no m and n can
satisfy Add, and compilation stops before any hardware is generated.
1.2 Tuples group a few values by position
Tuple2 through Tuple8 hold a small, fixed number of values without a
dedicated type. You build them with tuple2, tuple3, and so on, and read
them with tpl_1, tpl_2, and so on, or bind every member at once with
match, the pattern form from Pattern matching.
Syntax
Tuple2#(type1, type2) name = tuple2(value1, value2);
type1 first = tpl_1(name);
match {.x, .y} = name;let pair = tuple2(8'h12, True);
Bit#(8) data = tpl_1(pair); // 8'h12
Bool valid = tpl_2(pair); // True
match {.d, .v} = pair; // d is 8'h12, v is True| Construct | Role |
|---|---|
tuple2(a, b) | builds a two-member tuple |
tpl_1(t), tpl_2(t) | read a member by position |
match {.x, .y} = t; | declares x and y and binds them by position |
Members have no names, only positions. Tuples suit short-lived results such as
the output of split. When the values describe a reusable concept, a struct
with named fields documents itself and cannot be read in the wrong order.
1.3 Example: splitting a 12-bit frame
SplitVector.bsv divides a 12-bit frame into a 4-bit kind and an 8-bit
payload. Read the signature of splitFrame first: its result type is the
only place the boundary appears. Then read the match declaration in the
rule show.
The input 12'ha5c is 1010 0101 1100 in binary. The result type
Tuple2#(Bit#(4), Bit#(8)) gives m = 4 and n = 8, and Add#(4, 8, 12)
holds.
Predict before you run.
- Which bits, 11..8 or 3..0, become
kind? - What two hex values are printed?
- Does the body of
splitFramemention the numbers 4 or 8?
Check
splitFrame returns Tuple2#(Bit#(4), Bit#(8)) and is called with 12'ha5c. What does the kind field print?
package SplitVector;
function Tuple2#(Bit#(4), Bit#(8)) splitFrame(Bit#(12) frame);
// The result widths determine where split divides the vector.
return split(frame);
endfunction
(* synthesize *)
module mkTop(Empty);
rule show;
match {.kind, .payload} = splitFrame(12'ha5c);
$display("kind=%h payload=%h", kind, payload);
$finish(0);
endrule
endmodule
endpackagePractice this concept
Use this concept in a short BSV problem as your next step.
Check the result.
| Output field | What it proves |
|---|---|
kind=a | the high four bits, 1010, became the first tuple member |
payload=5c | the low eight bits, 0101 1100, became the second member |
The boundary sits four bits from the top, exactly as the first result width says, and nothing in the function body names it.
Try it yourself.
- Change the result type to
Tuple2#(Bit#(8), Bit#(4)). The boundary moves to four bits from the bottom, and the line becomeskind=a5 payload=c. The function body stays the same. - Change it to
Tuple2#(Bit#(4), Bit#(4))while the input staysBit#(12). Compilation stops withbscerrorT0031, which reports that no instance ofAdd#(4, 4, 12)exists and points at thesplitcall. The fix is to make the result widths add up to 12, not to truncate anything.
2. FIFO: order and implicit conditions
2.1 The four methods
A FIFO holds values in arrival order. FIFO#(a) from the FIFO package
provides four methods.
Syntax
import FIFO::*;
FIFO#(type) name <- mkFIFO; // depth chosen by the library
FIFO#(type) name <- mkSizedFIFO(depth); // explicit depth
FIFO#(type) name <- mkFIFO1; // depth 1FIFO#(Bit#(8)) queue <- mkSizedFIFO(4);
rule produce;
queue.enq(8'h12);
endrule
rule consume;
Bit#(8) oldest = queue.first;
queue.deq;
endrule| Method | Kind | Effect | Ready when |
|---|---|---|---|
enq(x) | Action | appends x at the back | the FIFO has room |
first | value method | returns the oldest entry without removing it | the FIFO holds an entry |
deq | Action | removes the oldest entry | the FIFO holds an entry |
clear | Action | removes every entry | always |
2.2 first and deq in the same rule
On mkFIFO, mkSizedFIFO and mkFIFO1, first reports the FIFO as it was at
the start of the cycle, and deq takes effect at the end (mkLFIFO and
mkBypassFIFO order their methods differently). When one rule calls both,
first returns exactly the entry that deq removes. The order of the two
statements inside the rule does not change that, because a rule is atomic, as
Rules that share state showed.
2.3 Implicit conditions make callers wait
None of the methods needs an explicit "is it full?" or "is it empty?" test.
Each method's readiness becomes part of the guard of any rule that calls it.
In the snippet above, produce simply does not fire while the FIFO is full,
and consume does not fire while it is empty.
Note. A rule that waits on an implicit condition produces no error. It just never fires. If nothing else can change the FIFO, the design stops making progress.
2.4 Example: two values leave in arrival order
FIFOOrder.bsv enqueues two bytes and removes them again. Note the depth in
mkSizedFIFO(2), then read the four rules in step order. Each rule is
guarded by a different step value, so exactly one rule runs per cycle.
| Cycle | Rule | FIFO before (oldest first) | Effect |
|---|---|---|---|
| 0 | enqueueFirst | empty | enqueue 12 |
| 1 | enqueueSecond | 12 | enqueue 34 |
| 2 | dequeueFirst | 12, 34 | first reads 12, deq removes it |
| 3 | dequeueSecond | 34 | first reads 34, deq removes it, finish |
Predict before you run.
- Which value does
dequeueFirstprint, and which doesdequeueSecondprint? - Does any rule test whether the FIFO is empty or full?
- If
$displayindequeueFirstmoved belowqueue.deq, would the printed value change?
Check
dequeueFirst prints queue.first and then calls queue.deq. The FIFO holds 12 then 34. What does it print, and would moving the $display below queue.deq change it?
package FIFOOrder;
import FIFO::*;
(* synthesize *)
module mkTop(Empty);
FIFO#(Bit#(8)) queue <- mkSizedFIFO(2);
Reg#(UInt#(3)) step <- mkReg(0);
rule enqueueFirst (step == 0);
queue.enq(8'h12);
step <= 1;
endrule
rule enqueueSecond (step == 1);
queue.enq(8'h34);
step <= 2;
endrule
// first reads the oldest entry and deq removes that same entry.
rule dequeueFirst (step == 2);
$display("first=%h", queue.first);
queue.deq;
step <= 3;
endrule
rule dequeueSecond (step == 3);
$display("second=%h", queue.first);
queue.deq;
$finish(0);
endrule
endmodule
endpackagePractice this concept
Use this concept in a short BSV problem as your next step.
Check the result.
| Output line | What it proves |
|---|---|
first=12 | the first value enqueued is the first value out |
second=34 | the second value follows in arrival order |
No rule tested for empty or full; the implicit conditions handled it. Moving
$display below queue.deq still prints first=12, because statement order
inside one atomic rule does not make the dequeue visible early.
Try it yourself.
- Change the depth to
mkSizedFIFO(1). In cycle 1,enqueueSecondcallsenqon a full FIFO, so its implicit condition is false and the rule cannot fire.stepstays at 1, no other rule is enabled, and the design deadlocks. Nothing is printed and$finishis never reached, so the run ends only when the execution time limit stops it. Silence without an error is the typical symptom of a stalled implicit condition.
mkFIFO leaves the capacity to the library, while mkSizedFIFO(depth) sets it.
Start from mkFIFO when correctness must not depend on depth, and choose a
sized FIFO when buffering is part of the design, as the deadlock shows it can
be.
2.5 Example: a full queue holds the producer, an empty one the consumer
In FIFOOrder.bsv a phase register made sure no call ever met a full or
empty FIFO. FIFOBackpressure.bsv lets both happen. tick counts cycles.
produce enqueues 0, 1, 2 and 3 while sent < 4 and never asks whether the
queue is full. consume has the explicit guard cycle >= 3 and never asks
whether the queue is empty. The queue is mkSizedFIFO(2). Every printed line
starts with the cycle number, so a cycle in which a rule did not fire shows up
as a missing line; stop ends the run in cycle 9.
package FIFOBackpressure;
import FIFO::*;
(* synthesize *)
module mkTop(Empty);
FIFO#(UInt#(8)) queue <- mkSizedFIFO(2);
Reg#(UInt#(8)) cycle <- mkReg(0);
Reg#(UInt#(8)) sent <- mkReg(0);
rule tick;
cycle <= cycle + 1;
endrule
// No full test here: enq's implicit condition stops this rule while the queue is full.
rule produce (sent < 4);
queue.enq(sent);
sent <= sent + 1;
$display("cycle %0d: enq %0d", cycle, sent);
endrule
// The explicit guard opens at cycle 3; deq still waits for an entry.
rule consume (cycle >= 3);
queue.deq;
$display("cycle %0d: deq %0d", cycle, queue.first);
endrule
rule stop (cycle == 9);
$display("cycle %0d: stop", cycle);
$finish(0);
endrule
endmodule
endpackagePredict before you run. For each cycle, write how many entries the queue holds at the start, whether each rule's explicit guard is true, whether its FIFO method is ready, and which lines appear.
Check
Which lines start with cycle 2?
Check
Cycle 3 starts with the queue full, and consume dequeues in that cycle. Does produce enqueue in the same cycle?
Check
In cycles 7 and 8, consume's explicit guard cycle >= 3 is true. Does consume fire?
Check the result. Compare every line with this trace. In each guard/ready pair the first value is the explicit guard and the second is the readiness of the FIFO method; the rule fires only when both are true.
| Cycle | Entries at start | produce: sent < 4 / enq ready | consume: cycle >= 3 / deq ready | Lines printed |
|---|---|---|---|---|
| 0 | 0 | true / true: fires | false / false | cycle 0: enq 0 |
| 1 | 1 | true / true: fires | false / true | cycle 1: enq 1 |
| 2 | 2 | true / false | false / true | none |
| 3 | 2 | true / false | true / true: fires | cycle 3: deq 0 |
| 4 | 1 | true / true: fires | true / true: fires | cycle 4: enq 2, cycle 4: deq 1 |
| 5 | 1 | true / true: fires | true / true: fires | cycle 5: enq 3, cycle 5: deq 2 |
| 6 | 1 | false / true | true / true: fires | cycle 6: deq 3 |
| 7, 8 | 0 | false / true | true / false | none |
| 9 | 0 | false / true | true / false | cycle 9: stop |
The values leave in the order they arrived, 0 to 3, and no value was lost or
repeated, although neither rule tests the queue. The silent cycles have
different causes that look identical in the output: in cycle 2 the producer
waits on enq (and keeps waiting through cycle 3), and in cycles 7 and 8 the
consumer waits on deq.
Try it yourself.
- Change the depth to
mkSizedFIFO(4). The producer is never blocked: the lines areenq 0toenq 3in cycles 0 to 3, withcycle 3: deq 0in the same cycle asenq 3, and thendeq 1,deq 2anddeq 3in cycles 4, 5 and 6. - Restore depth 2 and change
consume's guard tocycle >= 1. Predict the cycle of the lastdeqline before you run.
Check
With depth 2 and consume guarded by cycle >= 1, in which cycle is deq 3 printed?
3. FIFOF: readiness as values
3.1 notFull and notEmpty
FIFOF#(a) from the FIFOF package has every FIFO method plus two value
methods. They let control logic look at the FIFO's state without waiting on
it. On mkSizedFIFOF they report the state at the start of the cycle, like its
other value methods (bypass and mkCReg-based elements can show same-cycle writes).
Syntax
import FIFOF::*;
FIFOF#(type) name <- mkSizedFIFOF(depth);
Bool roomLeft = name.notFull; // enq could proceed
Bool hasData = name.notEmpty; // first and deq could proceedFIFOF#(Bit#(8)) queue <- mkSizedFIFOF(4);
Reg#(UInt#(8)) stalls <- mkReg(0);
rule countStalls (!queue.notFull);
stalls <= stalls + 1; // one count per cycle spent full
endrule| Flag | True when | Who should check it |
|---|---|---|
notFull | at least one free slot | a producer, before enq |
notEmpty | at least one stored entry | a consumer, before first and deq |
countStalls never calls enq or deq, so it has no implicit condition from
the FIFO. It fires in every cycle that begins with the queue full, which is
exactly the kind of "do something else" behavior the flags are for.
3.2 Example: two flags across fill levels
FIFOFStatus.bsv prints both flags while the FIFO holds 0, 2, 1, and 0
entries. Check the depth in mkSizedFIFOF(2), then follow step through the
five rules. Each $display runs before that rule's own enq, deq, or
clear takes effect.
| Cycle | Rule | Entries at start of cycle | Effect |
|---|---|---|---|
| 0 | enqueueFirst | 0 | print, then enqueue aa |
| 1 | enqueueSecond | 1 | enqueue bb |
| 2 | removeOne | 2 | print, then dequeue |
| 3 | clearRest | 1 | print, then clear |
| 4 | finish | 0 | print and finish |
Predict before you run. Write the pair (notFull, notEmpty) for each of
the four printed lines.
- What does an empty two-entry FIFOF report?
- What does it report with two entries?
- With one entry, can both flags be true at once?
Check
A depth-2 FIFOF holds exactly one entry at the start of a cycle. What pair (notFull, notEmpty) does the one: line print?
package FIFOFStatus;
import FIFOF::*;
(* synthesize *)
module mkTop(Empty);
FIFOF#(Bit#(8)) queue <- mkSizedFIFOF(2);
Reg#(UInt#(3)) step <- mkReg(0);
rule enqueueFirst (step == 0);
$display("empty: notFull=%0d notEmpty=%0d", queue.notFull, queue.notEmpty);
queue.enq(8'haa);
step <= 1;
endrule
rule enqueueSecond (step == 1);
queue.enq(8'hbb);
step <= 2;
endrule
// A two-entry FIFOF reports notFull false after two enqueues.
rule removeOne (step == 2);
$display("full: notFull=%0d notEmpty=%0d", queue.notFull, queue.notEmpty);
queue.deq;
step <= 3;
endrule
rule clearRest (step == 3);
$display("one: notFull=%0d notEmpty=%0d", queue.notFull, queue.notEmpty);
queue.clear;
step <= 4;
endrule
rule finish (step == 4);
$display("cleared: notFull=%0d notEmpty=%0d", queue.notFull, queue.notEmpty);
$finish(0);
endrule
endmodule
endpackagePractice this concept
Use this concept in a short BSV problem as your next step.
Check the result.
| Output line | (notFull, notEmpty) | What it proves |
|---|---|---|
empty: | (1, 0) | an empty FIFO has room and nothing to read |
full: | (0, 1) | two entries fill a depth-2 FIFOF |
one: | (1, 1) | with one entry, a producer and a consumer could both proceed |
cleared: | (1, 0) | clear in the previous cycle removed the last entry |
The two flags move independently. A producer whose rule is guarded by notEmpty
would have its explicit guard true at the wrong times: false in front of an
empty FIFO, and true in front of a full one, where enq's own implicit
condition then keeps the rule from being enabled at all. A true explicit guard
is not the same as an enabled rule.
Try it yourself.
- Change the depth to
mkSizedFIFOF(3). Two entries no longer fill the FIFO, so thefull:line printsnotFull=1 notEmpty=1, and its label becomes wrong. The flags describe capacity relative to the chosen depth, not the number ofenqcalls in the code.
3.3 Implicit conditions or flags?
Decide by what the caller should do when the FIFO is not ready.
| When the FIFO is not ready, the caller should | Use |
|---|---|
| simply wait | FIFO and its implicit conditions |
| report backpressure, choose another source, or combine availability with other control | FIFOF and notFull or notEmpty |
4. System tasks
4.1 Tasks for the simulator
System tasks begin with $. They are actions or values provided by the
simulator, used in testbenches and for debugging.
Syntax
$display(format, arguments...); // print one line
$write(format, arguments...); // print without a newline
$finish(exitCode); // end the simulation
let now <- $time; // current simulation timerule report (count == 3);
let now <- $time;
$display("time=%0t count=%0d", now, count);
$finish(0);
endrule| Task | Role |
|---|---|
$display, $write | print formatted text |
$finish | end the simulation |
$time | an ActionValue that returns the current simulation time |
$dumpvars and related tasks | write a VCD waveform file |
4.2 System tasks are not hardware
bsc emits system tasks inside a Verilog block wrapped in
synopsys translate_off and translate_on comments, and synthesis tools skip
that block. A module whose result is visible only as printed text therefore
has no behavior in silicon. Keep system tasks in testbenches or clearly
simulation-only code, and expose a module's results through its interface and
state.
5. Common mistakes
5.1 split result widths that do not add up
Bit#(12) frame = 12'ha5c;
// Wrong: 4 + 4 is not 12, so Add#(4, 4, 12) has no instance (T0031)
Tuple2#(Bit#(4), Bit#(4)) bad = split(frame);
// Right: the result widths sum to the input width
Tuple2#(Bit#(4), Bit#(8)) good = split(frame);The error points at the split call, but the thing to fix is the result type.
Write the field widths down first and check that they sum to the input width.
5.2 Guarding a producer with notEmpty
// Wrong: a producer that waits for data instead of room
rule produce (queue.notEmpty);
queue.enq(x);
endrule
// Right: a producer asks whether there is room
rule produce (queue.notFull);
queue.enq(x);
endrulenotEmpty answers the consumer's question. With the wrong flag, the producer
stays idle while the FIFO is empty, which is exactly when it should run.
5.3 Expecting deq to change first inside the same rule
rule takeTwo;
Bit#(8) a = queue.first;
queue.deq;
Bit#(8) b = queue.first; // still the same entry as a
endruleBoth reads of first see the start of the cycle, so a and b are equal.
Taking two entries needs a different structure, such as two FIFOs, or calls
spread over two cycles.
6. Summary
| Topic | Key point |
|---|---|
split | the result type sets the boundary; widths that do not sum fail with T0031 |
| Tuples | members are read by position with tpl_1, tpl_2, or match |
FIFO | preserves order; enq, first, and deq make their callers wait through implicit conditions |
FIFOF | adds notFull for producers and notEmpty for consumers, measured against the chosen depth |
| System tasks | simulation only; synthesis skips them |
7. Check your understanding
-
Where does
splitget its boundary, and at which stage is a wrong boundary reported?Show answer
From the result type. The compiler solves
mandnfrom the two result widths and checksAdd#(m, n, mn). Widths that do not sum to the input width fail during type checking withT0031, before any hardware exists. -
Why does
firstreturn the entry thatdeqremoves when one rule calls both?Show answer
firstis a value method and, onmkFIFO, reports the state at the start of the cycle.deqis anActionwhose effect commits at the end of the cycle. Inside one atomic rule, statement order does not make the removal visible early. -
A two-entry
FIFOFholds one entry. What arenotFullandnotEmpty, and which one should a producer check?Show answer
Both are true: there is room for one more entry and one entry to read. A producer checks
notFull;notEmptybelongs to the consumer. -
With
mkSizedFIFO(1), which rule stalls inFIFOOrder.bsv, and why is there no error message?Show answer
enqueueSecondstalls, because itsenqtargets a full FIFO and the implicit condition is false. A false implicit condition only keeps a rule from firing; it is not an error. Sincestepnever advances, no other rule fires either, and the run ends at the time limit.
Each Practice problem below reuses one contract from this chapter: a method that divides a 12-bit frame into a 4-bit kind and an 8-bit payload, a queue method that must return and remove the oldest buffered byte, and an elastic queue that reports it cannot accept data even while it is empty.