Probes
Probes are a way to encode a reference to hardware that will be later referred to by-name. Mechanistically, probes are a way to genereate SystemVerilog which includes hierarchical names (see: Section 23.6 of the SystemVerilog 2023 specification).
Probes are typically used to expose a "verification interface" to a unit for debugging, testing, or inspection without adding ports to the final hardware. When combined with layers, they may be "layer-colored" and optionally exist in a design based on Verilog compilation-time decisions.
Probes are not shadow dataflow. They are not a mechanism to connect arbitrary hardware while avoiding ports. They are closer to "references" in a traditional programming language, except that they have extra restrictions. Namely, a probe will eventually be accessed by-name and that name must, at its access site, resolve to the probed value unambiguously.
Overview
There are two kinds of probes based on the type of access that a user wants to have to a piece of hardware. A read probe allows for read-only access to hardware. A read--write probe allows for both read and write access.
Read probes are typically used for passive verification (e.g., assertions or monitors) or debugging (e.g., building an architectural debug "view" of a microarchitecture). Read--write probes are typically used for more active verification (e.g., injecting faults to test fault recovery mechanisms or as a means of closing difficult to reach coverage).
APIs for working with probes are in the chisel3.probe
package.
Read Probes
To create a read probe of a hardware value, use the ProbeValue
API. To create
a read probe type, use the Probe
API. Probes are legal types for ports and
wires, but not for stateful elements (e.g., registers or memories).
It may be surprising the a probe is a legal type for a wire. However, wires in Chisel behave more like variables than they do like "hardware wires" (or Verilog net types). With this view, it is natural that a probe (a reference) may be passed through a variable.
Probes are different from normal Chisel hardware types. Whereas normal Chisel
hardware may be connected to multiple times with the last connection "winning"
via so-called "last-connect semantics", probe types may only be defined
exactly once. The API to define a probe is, unsurprisingly, called define
.
This is used to "forward" a probe up through the hierarchy, e.g., to define a
probe port with the probed value of a wire.
For convenience, you may alternatively use the standard Chisel connection
operators which will, under-the-hood, use the define
operator automatically
for you.
To read the value of a probe type, use the read
API.
The following example shows a circuit that uses all of the APIs introduced
previously. Both define
and standard Chisel connection operators are shown.
Careful use of dontTouch
is used to prevent optimization across probes so that
the output is not trivially simple.
import chisel3._
import chisel3.probe.{Probe, ProbeValue, define, read}
class Bar extends RawModule {
val a_port = IO(Probe(Bool()))
val b_port = IO(Probe(Bool()))
private val a = dontTouch(WireInit(Bool(), true.B))
private val a_probe = ProbeValue(a)
define(a_port, a_probe)
b_port :<= a_probe
}
class Foo extends RawModule {
private val bar = Module(new Bar)
private val a_read = dontTouch(WireInit(read(bar.a_port)))
private val b_read = dontTouch(WireInit(read(bar.b_port)))
}
The SystemVerilog for the above circuit is shown below:
// Generated by CIRCT firtool-1.113.0
module Bar();
wire a = 1'h1;
wire a_probe = a;
endmodule
module Foo();
wire a_read = Foo.bar.a_probe;
wire b_read = Foo.bar.a_probe;
Bar bar ();
endmodule
There are several things that are worth highlighting in the above SystemVerilog:
-
The wires
a_read
andb_read
are driven with hierarchical names that reach into moduleBar
. There are no ports created on moduleBar
. This is intended to support design verification use cases where certain signals are made available fromBar
via probe ports that are then used to, e.g., connect to assertions, monitors, or verification intellectual property (IP). If hardware ports were used this would change the interface of the design unfavorably. -
Observability, via probes, is not free. While the above circuit is contrived in its simplicity, if hardware is probed, that may limit the ability of the compiler to optimize that hardware. Read probes are generally more amenable to optimization than read--write probes. However, they still have effects.
Read--write Probes
To create a read--write probe of a hardware value, use the RWProbeValue
API.
To create a read--write probe type, use the RWProbe
API. As with read
probes, read--write probes are legal types for ports and wires, but not for
stateful elements (e.g., registers or memories).
As with read probes, read--write probes forward references using the define
API or the standard Chisel connection operators.
A read--write probe can be read using the same read
API that is used for read
probes. Multiple different operations are provided for writing to a read--write
probe. The force
and forceInitial
APIs are used to overwrite the value of
read--write probed hardware. The release
and releaseInitial
APIs are used
to stop overwriting a read--write probed hardware value.
All writing of read--write probes is done through APIs which lower to System
Verilog force
/release
statements (see: Section 10.6 of the SystemVerilog
2023 specification). It is intentionally not possible to use normal Chisel
connects to write to read--write probes. Put differently, read--write probes do
not participate in last-connect semantics.
The following example shows a circuit that uses all of the APIs introduced
previously. Both define
and standard Chisel connection operators are shown.
Careful use of dontTouch
is used to prevent optimization across probes so that
the output is not trivially simple.
import chisel3._
import chisel3.probe.{RWProbe, RWProbeValue, force, forceInitial, read, release, releaseInitial}
class Bar extends RawModule {
val a_port = IO(RWProbe(Bool()))
val b_port = IO(RWProbe(UInt(8.W)))
private val a = WireInit(Bool(), true.B)
a_port :<= RWProbeValue(a)
private val b = WireInit(UInt(8.W), 0.U)
b_port :<= RWProbeValue(b)
}
class Foo extends Module {
val cond = IO(Input(Bool()))
private val bar = Module(new Bar)
// Example usage of forceInitial/releaseInitial:
forceInitial(bar.a_port, false.B)
releaseInitial(bar.a_port)
// Example usage of force/release:
when (cond) {
force(bar.b_port, 42.U)
}.otherwise {
release(bar.b_port)
}
// The read API may still be used:
private val a_read = dontTouch(WireInit(read(bar.a_port)))
}
The SystemVerilog for the above circuit is shown below:
// Generated by CIRCT firtool-1.113.0
module Bar();
wire a = 1'h1;
wire [7:0] b = 8'h0;
endmodule
module Foo(
input clock,
reset,
cond
);
reg hasBeenResetReg;
reg hasBeenResetReg_0;
initial begin
hasBeenResetReg = 1'bx;
hasBeenResetReg_0 = 1'bx;
end // initial
always @(posedge clock) begin
if (reset) begin
hasBeenResetReg <= 1'h1;
hasBeenResetReg_0 <= 1'h1;
end
end // always @(posedge)
`ifndef SYNTHESIS
initial begin
force Foo.bar.a = 1'h0;
release Foo.bar.a;
end // initial
always @(posedge clock) begin
automatic logic _GEN = reset === 1'h0;
if (cond & hasBeenResetReg === 1'h1 & _GEN)
force Foo.bar.b = 8'h2A;
if (~cond & hasBeenResetReg_0 === 1'h1 & _GEN)
release Foo.bar.b;
end // always @(posedge)
`endif // not def SYNTHESIS
wire a_read = Foo.bar.a;
Bar bar ();
endmodule
Several things are worth commenting on in the above SystemVerilog:
-
Writability is very invasive. In order to compile a write probe, all optimizations on its target must be blocked and any optimizations through the target are not possible. This is because any writes to a read--write probe must affect downstream users.
-
The APIs for writing to read--write probes (e.g.,
force
) are extremely low-level and very tightly coupled to SystemVerilog. Take great care when using these APIs and validating that the resulting SystemVerilog does what you want.
Not all simulators correctly implement force and release as described in the SystemVerilog spec! Be careful when using read--write probes. You may need to use a SystemVerilog-compliant simulator.
Verilog ABI
Earlier examples only show probes being used internal to a circuit. However, probes also compile to SystemVerilog in such a way that they are usable external to the circuit.
Consider the following example circuit. In this, an internal register's value is exposed via a read probe.
import chisel3._
import chisel3.probe.{Probe, ProbeValue}
class Foo extends Module {
val d = IO(Input(UInt(32.W)))
val q = IO(Output(UInt(32.W)))
val r_probe = IO(Output(Probe(UInt(32.W))))
private val r = Reg(UInt(32.W))
q :<= r
r_probe :<= ProbeValue(r)
}
The SystemVerilog for the above circuit is shown below:
// Generated by CIRCT firtool-1.113.0
module Foo(
input clock,
reset,
input [31:0] d,
output [31:0] q
);
wire [31:0] _GEN = 32'h0;
assign q = 32'h0;
endmodule
// ----- 8< ----- FILE "ref_Foo.sv" ----- 8< -----
// Generated by CIRCT firtool-1.113.0
`define ref_Foo_r_probe _GEN
As part of the compilation, this is guaranteed to produce an additional file for
each public module with a specific filename: ref_<module-name>.sv
. In this
file, there will be one SystemVerilog text macro definition for each probe port
of that public module. The define will have a text macro name derived from the
module name and the probe port name: ref_<module-name>_<probe-name>
.
Using this ABI, the module may be instantiated elsewhere (e.g., by a SystemVerilog testbench) and its probed internals accessed.
For the exact definition of the port lowering ABI for probes, see the FIRRTL ABI Specification.
Layer-colored Probes
Probes are allowed to be layer-colored. I.e., this is a mechanism to declare
that a probe's existence is contingent on a specific layer being enabled. To
declare a probe as being layer-colored, the Probe
or RWProbe
type takes an
optional argument indicating what the layer coloring is. The following example
decalres two probe ports with different layer colors:
import chisel3._
import chisel3.layer.{Layer, LayerConfig}
import chisel3.probe.{Probe, ProbeValue}
object A extends Layer(LayerConfig.Extract())
object B extends Layer(LayerConfig.Extract())
class Foo extends Module {
val a = IO(Output(Probe(Bool(), A)))
val b = IO(Output(Probe(UInt(8.W), B)))
}
For more information on layer-colored probes see the appropriate subsection of the layers documentation.
Why Input Probes are Not Allowed
Input probes (of either read or read--write kind) are disallowed. This is an intentional decision that stems from requirements of both what probes are and how probes can be compiled to SystemVerilog.
First, probes are references. They refer to hardware which exists somewhere else. They are not hardware wires. They are not "shadow" ports. They do not represent "shadow" dataflow.
Second, a probe always comes with two pieces: the actual probed hardware and the operation which uses the reference to the probed hardware. The operation that uses the probe must, at its specific location, be able to refer unambiguously to the probed hardware. As the example below will show, this is problematic with input probes.
Consider the following illegal Chisel which uses hypothetical input probes:
import chisel3._
import chisel3.probe.{Probe, ProbeValue, read}
module Baz extends RawModule {
val probe = IO(Input(Probe(Bool())))
val b = WireInit(read(probe))
}
module Bar extends RawModule {
val probe = IO(Input(Probe(Bool())))
val baz = Module(new Baz)
baz.probe :<= probe
}
module Foo extends RawModule {
val w = Wire(Bool())
val bar = Module(new Bar)
bar.probe :<= ProbeValue(w)
}
This could be compiled to the following SystemVerilog:
module Baz();
wire b = Foo.a;
endmodule
module Bar();
Baz baz();
endmodule
module Foo();
wire a;
Bar bar();
endmodule
SystemVerilog provides an algorithm for resolving upwards hierarchical names
(see: Section 23.8 of the SystemVerilog 2023 specification). This works by
looking in the current scope for a match for the root of the name (Foo
) and if
it fails, it moves up one level and tries to look aagin. This then repeats
until a name is found (or errors if the top of the circuit is reached).
However, this algorithm places harsh naming constraints on intermediary modules.
E.g., in the example above, no name Foo
can exist in Baz
or in an
intervening modules between Baz
and Foo
. This can easily run afoul of
names which cannot be changed, e.g., public modules or public module ports.
Additionally, any use of a hierarchical name that resolves upwards means that
the module that uses that upwards reference is limited in its ability to be
freely instantiated. In the circuit above, Baz
is singly instantiated.
However, if Baz
was multiply instantiated, it could be given two different
input probes. This would mean that Baz
could not be compiled to a single
Verilog module. It must be duplicated for each unique hierarchical name that it
contains. This can have cascading duplication effects where parent modules,
their parents, etc. must be duplicated. The unpredictability of this is not
viewed as tolerable by users.
Both of these constraints (the constraints on names in intevening modules and duplication to resolve hierarchical names) make the use of input probes problematic. While they could be compiled, the results will be unpredictable and difficult for a user to debug when things go wrong.
Due to these problems, input probes were rejected as a design point and are not planned to be implemented.
BoringUtils
Probes are an intentionally a low-level API. E.g., if a design needs to expose a probe port, it may need to add probe ports to all intervening modules between it and a probed value.
For a more flexible API, consider using chisel3.util.experimental.BoringUtils
.
This provides higher-level APIs that automatically create probe ports for the
user:
rwTap
: creates a read--write probe of a signal and routes it to the call sitetap
: creates a read probe of a signal and routes it to the call sitetapAndRead
: `creates a read probe of a signal, routes it to the call site, and reads it (converts from probe to real hardware)
E.g., consider the original example shown for read probes. This can be
rewritten using BoringUtils
to be more terse:
import chisel3._
import chisel3.util.experimental.BoringUtils
class Bar extends RawModule {
val a = dontTouch(WireInit(Bool(), true.B))
}
class Foo extends RawModule {
private val bar = Module(new Bar)
private val a_read = dontTouch(WireInit(BoringUtils.tapAndRead(bar.a)))
}
The SystemVerilog for the above circuit is shown below:
// Generated by CIRCT firtool-1.113.0
module Bar();
wire a = 1'h1;
wire a_probe = a;
endmodule
module Foo();
wire a_read = Foo.bar.a_probe;
Bar bar ();
endmodule
In order to do this, it requires that the tapped target is public from Scala's perspective.
BoringUtils
is only suitable for use within a compilation unit.
Additionally, excessive use of BoringUtils
can result in very confusing
hardware generators where the port-level interfaces are unpredictable.
If a BoringUtils
API is used in a situation which would create an input probe,
it will instead create a non-probe input port.