9.1. TileLink Node Types
Diplomacy represents the different components of an SoC as nodes of a directed acyclic graph. TileLink nodes can come in several different types.
9.1.1. Client Node
TileLink clients are modules that initiate TileLink transactions by sending requests on the A channel and receive responses on the D channel. If the client implements TL-C, it will receive probes on the B channel, send releases on the C channel, and send grant acknowledgements on the E channel.
The L1 caches and DMA devices in RocketChip/Chipyard have client nodes.
You can add a TileLink client node to your LazyModule like so:
class MyClient(implicit p: Parameters) extends LazyModule {
val node = TLClientNode(Seq(TLMasterPortParameters.v1(Seq(TLClientParameters(
name = "my-client",
sourceId = IdRange(0, 4),
requestFifo = true,
visibility = Seq(AddressSet(0x10000, 0xffff)))))))
lazy val module = new LazyModuleImp(this) {
val (tl, edge) = node.out(0)
// Rest of code here
}
}
The name
argument identifies the node in the Diplomacy graph. It is the
only required argument for TLClientParameters.
The sourceId
argument specifies the range of source identifiers that this
client will use. Since we have set the range to [0, 4) here, this client will
be able to send up to four requests in flight at a time. Each request will
have a distinct value in its source field. The default value for this field
is IdRange(0, 1)
, which means it would only be able to send a single
request inflight.
The requestFifo
argument is a boolean option which defaults to false.
If it is set to true, the client will request that downstream managers that
support it send responses in FIFO order (that is, in the same order the
corresponding requests were sent).
The visibility
argument specifies the address ranges that the client will
access. By default it is set to include all addresses. In this example, we set
it to contain a single address range AddressSet(0x10000, 0xffff)
, which
means that the client will only be able to access addresses from 0x10000 to
0x1ffff. normally do not specify this, but it can help downstream crossbar
generators optimize the hardware by not arbitrating the client to managers with
address ranges that don’t overlap with its visibility.
Inside your lazy module implementation, you can call node.out
to get a
list of bundle/edge pairs.
The tl
bundle is a Chisel hardware bundle that connects to the IO of this
module. It contains two (in the case of TL-UL and TL-UH) or five (in the case
of TL-C) decoupled bundles corresponding to the TileLink channels. This is
what you should connect your hardware logic to in order to actually send/receive
TileLink messages.
The edge
object represents the edge of the Diplomacy graph. It contains
some useful helper functions which will be documented in
TileLink Edge Object Methods.
9.1.2. Manager Node
TileLink managers take requests from clients on the A channel and send responses back on the D channel. You can create a manager like so:
class MyManager(implicit p: Parameters) extends LazyModule {
val device = new SimpleDevice("my-device", Seq("tutorial,my-device0"))
val beatBytes = 8
val node = TLManagerNode(Seq(TLSlavePortParameters.v1(Seq(TLManagerParameters(
address = Seq(AddressSet(0x20000, 0xfff)),
resources = device.reg,
regionType = RegionType.UNCACHED,
executable = true,
supportsArithmetic = TransferSizes(1, beatBytes),
supportsLogical = TransferSizes(1, beatBytes),
supportsGet = TransferSizes(1, beatBytes),
supportsPutFull = TransferSizes(1, beatBytes),
supportsPutPartial = TransferSizes(1, beatBytes),
supportsHint = TransferSizes(1, beatBytes),
fifoId = Some(0))), beatBytes)))
lazy val module = new LazyModuleImp(this) {
val (tl, edge) = node.in(0)
}
}
The makeManagerNode
method takes two arguments. The first is beatBytes
,
which is the physical width of the TileLink interface in bytes. The second
is a TLManagerParameters object.
The only required argument for TLManagerParameters
is the address
,
which is the set of address ranges that this manager will serve.
This information is used to route requests from the clients. In this example,
the manager will only take requests for addresses from 0x20000 to 0x20fff.
The second argument in AddressSet
is a mask, not a size.
You should generally set it to be one less than a power of two. Otherwise,
the addressing behavior may not be what you expect.
The second argument is resources
, which is usually retrieved from a
Device
object. In this case, we use a SimpleDevice
object.
This argument is necessary if you want to add an entry to the DeviceTree in
the BootROM so that it can be read by a Linux driver. The two arguments to
SimpleDevice
are the name and compatibility list for the device tree
entry. For this manager, then, the device tree entry would look like
L12: my-device@20000 {
compatible = "tutorial,my-device0";
reg = <0x20000 0x1000>;
};
The next argument is regionType
, which gives some information about
the caching behavior of the manager. There are seven region types, listed below:
CACHED
- An intermediate agent may have cached a copy of the region for you.TRACKED
- The region may have been cached by another master, but coherence is being provided.UNCACHED
- The region has not been cached yet, but should be cached when possible.IDEMPOTENT
- Gets return most recently put content, but content should not be cached.VOLATILE
- Content may change without a put, but puts and gets have no side effects.PUT_EFFECTS
- Puts produce side effects and so must not be combined/delayed.GET_EFFECTS
- Gets produce side effects and so must not be issued speculatively.
Next is the executable
argument, which determines if the CPU is allowed to
fetch instructions from this manager. By default it is false, which is what
most MMIO peripherals should set it to.
The next six arguments start with support
and determine the different
A channel message types that the manager can accept. The definitions of the
message types are explained in TileLink Edge Object Methods.
The TransferSizes
case class specifies the range of logical sizes (in bytes)
that the manager can accept for the particular message type. This is an inclusive
range and all logical sizes must be powers of two. So in this case, the manager
can accept requests with sizes of 1, 2, 4, or 8 bytes.
The final argument shown here is the fifoId
setting, which determines
which FIFO domain (if any) the manager is in. If this argument is set to None
(the default), the manager will not guarantee any ordering of the responses.
If the fifoId
is set, it will share a FIFO domain with all other managers
that specify the same fifoId
. This means that client requests sent to
that FIFO domain will see responses in the same order.
9.1.3. Register Node
While you can directly specify a manager node and write all of the logic
to handle TileLink requests, it is usually much easier to use a register node.
This type of node provides a regmap
method that allows you to specify
control/status registers and automatically generates the logic to handle the
TileLink protocol. More information about how to use register nodes can be
found in Register Node.
9.1.4. Identity Node
Unlike the previous node types, which had only inputs or only outputs, the identity node has both. As its name suggests, it simply connects the inputs to the outputs unchanged. This node is mainly used to combine multiple nodes into a single node with multiple edges. For instance, say we have two client lazy modules, each with their own client node.
class MyClient1(implicit p: Parameters) extends LazyModule {
val node = TLClientNode(Seq(TLMasterPortParameters.v1(Seq(TLClientParameters(
"my-client1", IdRange(0, 1))))))
lazy val module = new LazyModuleImp(this) {
// ...
}
}
class MyClient2(implicit p: Parameters) extends LazyModule {
val node = TLClientNode(Seq(TLMasterPortParameters.v1(Seq(TLClientParameters(
"my-client2", IdRange(0, 1))))))
lazy val module = new LazyModuleImp(this) {
// ...
}
}
Now we instantiate these two clients in another lazy module and expose their nodes as a single node.
class MyClientGroup(implicit p: Parameters) extends LazyModule {
val client1 = LazyModule(new MyClient1)
val client2 = LazyModule(new MyClient2)
val node = TLIdentityNode()
node := client1.node
node := client2.node
lazy val module = new LazyModuleImp(this) {
// Nothing to do here
}
}
We can also do the same for managers.
class MyManager1(beatBytes: Int)(implicit p: Parameters) extends LazyModule {
val node = TLManagerNode(Seq(TLSlavePortParameters.v1(Seq(TLManagerParameters(
address = Seq(AddressSet(0x0, 0xfff)))), beatBytes)))
lazy val module = new LazyModuleImp(this) {
// ...
}
}
class MyManager2(beatBytes: Int)(implicit p: Parameters) extends LazyModule {
val node = TLManagerNode(Seq(TLSlavePortParameters.v1(Seq(TLManagerParameters(
address = Seq(AddressSet(0x1000, 0xfff)))), beatBytes)))
lazy val module = new LazyModuleImp(this) {
// ...
}
}
class MyManagerGroup(beatBytes: Int)(implicit p: Parameters) extends LazyModule {
val man1 = LazyModule(new MyManager1(beatBytes))
val man2 = LazyModule(new MyManager2(beatBytes))
val node = TLIdentityNode()
man1.node := node
man2.node := node
lazy val module = new LazyModuleImp(this) {
// Nothing to do here
}
}
If we want to connect the client and manager groups together, we can now do this.
class MyClientManagerComplex(implicit p: Parameters) extends LazyModule {
val client = LazyModule(new MyClientGroup)
val manager = LazyModule(new MyManagerGroup(8))
manager.node :=* client.node
lazy val module = new LazyModuleImp(this) {
// Nothing to do here
}
}
The meaning of the :=*
operator is explained in more detail in the
Diplomacy Connectors section. In summary, it connects two nodes together
using multiple edges. The edges in the identity node are assigned in order,
so in this case client1.node
will eventually connect to manager1.node
and client2.node
will connect to manager2.node
.
The number of inputs to an identity node should match the number of outputs. A mismatch will cause an elaboration error.
9.1.5. Adapter Node
Like the identity node, the adapter node takes some number of inputs and produces the same number of outputs. However, unlike the identity node, the adapter node does not simply pass the connections through unchanged. It can change the logical and physical interfaces between input and output and rewrite messages going through. RocketChip provides a library of adapters, which are catalogued in Diplomatic Widgets.
You will rarely need to create an adapter node yourself, but the invocation is as follows.
val node = TLAdapterNode(
clientFn = { cp =>
// ..
},
managerFn = { mp =>
// ..
})
The clientFn
is a function that takes the TLClientPortParameters
of
the input as an argument and returns the corresponding parameters for the
output. The managerFn
takes the TLManagerPortParameters
of the output
as an argument and returns the corresponding parameters for the input.
9.1.6. Nexus Node
The nexus node is similar to the adapter node in that it has a different
output interface than input interface. But it can also have a different
number of inputs than it does outputs. This node type is mainly used by
the TLXbar
widget, which provides a TileLink crossbar generator. You will
also likely not need to define this node type manually, but its invocation is
as follows.
val node = TLNexusNode(
clientFn = { seq =>
// ..
},
managerFn = { seq =>
// ..
})
This has similar arguments as the adapter node’s constructor, but instead of taking single parameters objects as arguments and returning single objects as results, the functions take and return sequences of parameters. And as you might expect, the size of the returned sequence need not be the same size as the input sequence.