diff --git a/docs/TileLink-Diplomacy-Reference/Register-Router.rst b/docs/TileLink-Diplomacy-Reference/Register-Router.rst index d1efa49f..2e713e17 100644 --- a/docs/TileLink-Diplomacy-Reference/Register-Router.rst +++ b/docs/TileLink-Diplomacy-Reference/Register-Router.rst @@ -1,2 +1,200 @@ Register Router =============== + +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 by extending +the ``TLRegisterRouter`` class, as shown in :ref:`Adding An Accelerator/Device`, +or you can create a regular LazyModule and instantiate a ``TLRegisterNode``. +This section will focus on the second method. + +Basic Usage +----------- + +.. code-block:: scala + + import chisel3._ + import chisel3.util._ + import freechips.rocketchip.config.Parameters + import freechips.rocketchip.diplomacy.{SimpleDevice, AddressSet} + import freechips.rocketchip.tilelink.TLRegisterNode + + class MyDeviceController(implicit p: Parameters) extends LazyModule { + val device = new SimpleDevice("my-device", Seq("tutorial,my-device0")) + val node = TLRegisterNode( + address = Seq(AddressSet(0x10019000, 0xfff)), + device = device, + beatBytes = 8, + concurrency = 1) + + lazy val module = new LazyModuleImp(this) { + val bigReg = RegInit(0.U(64.W)) + val mediumReg = RegInit(0.U(32.W)) + val smallReg = RegInit(0.U(16.W)) + + val tinyReg0 = RegInit(0.U(4.W)) + val tinyReg1 = RegInit(0.U(4.W)) + + node.regmap( + 0x00 -> Seq(RegField(64, bigReg)), + 0x08 -> Seq(RegField(32, mediumReg)), + 0x0C -> Seq(RegField(16, smallReg)), + 0x0E -> Seq( + RegField(4, tinyReg0), + RegField(4, tinyReg1))) + } + } + +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:`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, ``smallReg0`` will get the value 0xB and ``smallReg1`` 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. + +.. code-block:: scala + + // 4-entry 64-bit queue + val queue = Module(new Queue(UInt(64.W), 4)) + + node.regmap( + 0x00 -> Seq(RegField(64, queue.io.deq, queue.io.enq))) + +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. + +.. code-block:: scala + + node.regmap( + 0x00 -> Seq(RegField.r(64, queue.io.deq)), + 0x08 -> Seq(RegField.w(64, queue.io.enq))) + +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. + +.. code-block:: scala + + val counter = RegInit(0.U(64.W)) + + def readCounter(ready: Bool): (Bool, UInt) = { + when (ready) { counter := counter - 1.U } + (true.B, counter) + } + + def writeCounter(valid: Bool, bits: UInt): Bool = { + when (valid) { counter := counter + 1.U } + // Ignore bits + true.B + } + + node.regmap( + 0x00 -> Seq(RegField.r(64, readCounter(_))), + 0x08 -> Seq(RegField.w(64, writeCounter(_, _)))) + +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. + +.. code-block:: scala + + val counter = RegInit(0.U(64.W)) + + def readCounter(ivalid: Bool, oready: Bool): (Bool, Bool, UInt) = { + val responding = RegInit(false.B) + + when (ivalid && !responding) { responding := true.B } + + when (responding && oready) { + counter := counter - 1.U + responding := false.B + } + + (!responding, responding, counter) + } + + def writeCounter(ivalid: Bool, bits: UInt, oready: Bool): (Bool, Bool) = { + val responding = RegInit(false.B) + + when (ivalid && !responding) { responding := true.B } + + when (responding && oready) { + counter := counter + 1.U + responding := false.B + } + + (!responding, responding) + } + + node.regmap( + 0x00 -> Seq(RegField.r(64, readCounter(_, _))), + 0x08 -> Seq(RegField.w(64, writeCounter(_, _, _)))) + +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 different 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.