Update MMIO peripheral docs
This commit is contained in:
138
docs/TileLink-Diplomacy-Reference/Register-Node.rst
Normal file
138
docs/TileLink-Diplomacy-Reference/Register-Node.rst
Normal file
@@ -0,0 +1,138 @@
|
||||
Register Node
|
||||
===============
|
||||
|
||||
Memory-mapped devices generally follow a common pattern. They expose a set
|
||||
of registers to the CPUs. By writing to a register, the CPU can change the
|
||||
device's settings or send a command. By reading from a register, the CPU can
|
||||
query the device's state or retrieve results.
|
||||
|
||||
While designers can manually instantiate a manager node and write the logic
|
||||
for exposing registers themselves, it's much easier to use RocketChip's
|
||||
``regmap`` interface, which can generate most of the glue logic.
|
||||
|
||||
For TileLink devices, you can use the ``regmap`` interface of the ``TLRegisterNode``.
|
||||
|
||||
Basic Usage
|
||||
-----------
|
||||
|
||||
.. literalinclude:: ../../generators/chipyard/src/main/scala/example/RegisterNodeExample.scala
|
||||
:language: scala
|
||||
:start-after: DOC include start: MyDeviceController
|
||||
:end-before: DOC include end: MyDeviceController
|
||||
|
||||
The code example above shows a simple lazy module that uses the ``TLRegisterNode``
|
||||
to memory map hardware registers of different sizes. The constructor has
|
||||
two required arguments: ``address``, which is the base address of the registers,
|
||||
and ``device``, which is the device tree entry. There are also two optional
|
||||
arguments. The ``beatBytes`` argument is the interface width in bytes.
|
||||
The default value is 4 bytes. The ``concurrency`` argument is the size of the
|
||||
internal queue for TileLink requests. By default, this value is 0, which means
|
||||
there will be no queue. This value must be greater than 0 if you wish to
|
||||
decoupled requests and responses for register accesses. This is discussed
|
||||
in :ref:`TileLink-Diplomacy-Reference/Register-Node:Using Functions`.
|
||||
|
||||
The main way to interact with the node is to call the ``regmap`` method, which
|
||||
takes a sequence of pairs. The first element of the pair is an offset from the
|
||||
base address. The second is a sequence of ``RegField`` objects, each of
|
||||
which maps a different register. The ``RegField`` constructor takes two
|
||||
arguments. The first argument is the width of the register in bits.
|
||||
The second is the register itself.
|
||||
|
||||
Since the argument is a sequence, you can associate multiple ``RegField``
|
||||
objects with an offset. If you do, the registers are read or written in parallel
|
||||
when the offset is accessed. The registers are in little endian order, so the
|
||||
first register in the list corresponds to the least significant bits in the
|
||||
value written. In this example, if the CPU wrote to offset 0x0E with the value
|
||||
0xAB, ``tinyReg0`` will get the value 0xB and ``tinyReg1`` would get 0xA.
|
||||
|
||||
Decoupled Interfaces
|
||||
--------------------
|
||||
|
||||
Sometimes you may want to do something other than read and write from a hardware
|
||||
register. The ``RegField`` interface also provides support for reading
|
||||
and writing ``DecoupledIO`` interfaces. For instance, you can implement a
|
||||
hardware FIFO like so.
|
||||
|
||||
.. literalinclude:: ../../generators/chipyard/src/main/scala/example/RegisterNodeExample.scala
|
||||
:language: scala
|
||||
:start-after: DOC include start: MyQueueRegisters
|
||||
:end-before: DOC include end: MyQueueRegisters
|
||||
|
||||
This variant of the ``RegField`` constructor takes three arguments instead of
|
||||
two. The first argument is still the bit width. The second is the decoupled
|
||||
interface to read from. The third is the decoupled interface to write to.
|
||||
In this example, writing to the "register" will push the data into the queue
|
||||
and reading from it will pop data from the queue.
|
||||
|
||||
You need not specify both read and write for a register. You can also create
|
||||
read-only or write-only registers. So for the previous example, if you wanted
|
||||
enqueue and dequeue to use different addresses, you could write the following.
|
||||
|
||||
.. literalinclude:: ../../generators/chipyard/src/main/scala/example/RegisterNodeExample.scala
|
||||
:language: scala
|
||||
:start-after: DOC include start: MySeparateQueueRegisters
|
||||
:end-before: DOC include end: MySeparateQueueRegisters
|
||||
|
||||
The read-only register function can also be used to read signals
|
||||
that aren't registers.
|
||||
|
||||
.. code-block:: scala
|
||||
|
||||
val constant = 0xf00d.U
|
||||
|
||||
node.regmap(
|
||||
0x00 -> Seq(RegField.r(8, constant)))
|
||||
|
||||
Using Functions
|
||||
---------------
|
||||
|
||||
You can also create registers using functions. Say, for instance, that you
|
||||
want to create a counter that gets incremented on a write and decremented on
|
||||
a read.
|
||||
|
||||
.. literalinclude:: ../../generators/chipyard/src/main/scala/example/RegisterNodeExample.scala
|
||||
:language: scala
|
||||
:start-after: DOC include start: MyCounterRegisters
|
||||
:end-before: DOC include end: MyCounterRegisters
|
||||
|
||||
The functions here are essentially the same as a decoupled interface.
|
||||
The read function gets passed the ``ready`` signal and returns the
|
||||
``valid`` and ``bits`` signals. The write function gets passed ``valid`` and
|
||||
``bits`` and returns ``ready``.
|
||||
|
||||
You can also pass functions that decouple the read/write request and response.
|
||||
The request will appear as a decoupled input and the response as a decoupled
|
||||
output. So for instance, if we wanted to do this for the previous example.
|
||||
|
||||
.. literalinclude:: ../../generators/chipyard/src/main/scala/example/RegisterNodeExample.scala
|
||||
:language: scala
|
||||
:start-after: DOC include start: MyCounterReqRespRegisters
|
||||
:end-before: DOC include end: MyCounterReqRespRegisters
|
||||
|
||||
In each function, we set up a state variable ``responding``. The function
|
||||
is ready to take requests when this is false and is sending a response when
|
||||
this is true.
|
||||
|
||||
In this variant, both read and write take an input valid and return an
|
||||
output ready. The only difference is that bits is an input for read and an
|
||||
output for write.
|
||||
|
||||
In order to use this variant, you need to set ``concurrency`` to a value
|
||||
larger than 0.
|
||||
|
||||
Register Nodes for Other Protocols
|
||||
------------------------------------
|
||||
|
||||
One useful feature of the register node interface is that you can easily
|
||||
change the protocol being used. For instance, in the first example in
|
||||
:ref:`TileLink-Diplomacy-Reference/Register-Node:Basic Usage`, you could simply change the ``TLRegisterNode`` to
|
||||
and ``AXI4RegisterNode``.
|
||||
|
||||
.. literalinclude:: ../../generators/chipyard/src/main/scala/example/RegisterNodeExample.scala
|
||||
:language: scala
|
||||
:start-after: DOC include start: MyAXI4DeviceController
|
||||
:end-before: DOC include end: MyAXI4DeviceController
|
||||
|
||||
Other than the fact that AXI4 nodes don't take a ``device`` argument, and can
|
||||
only have a single AddressSet instead of multiple, everything else is
|
||||
unchanged.
|
||||
Reference in New Issue
Block a user