Memory-mapped Registers

In this tutorial, we will create a device which pulls in data from an externally-connected input stream and writes the data to memory. We’ll create our device in the file src/main/scala/example/InputStream.scala. The first thing we need to do is set up some memory-mapped control registers that the CPU can use to communicate with the device. The easiest way to do this is by creating a TLRegisterNode, which provides a regmap method that can be used to generate the hardware for reading and writing to RTL registers.

class InputStream(
    address: BigInt,
    val beatBytes: Int = 8)
    (implicit p: Parameters) extends LazyModule {

  val device = new SimpleDevice("input-stream", Seq("example,input-stream"))
  val regnode = TLRegisterNode(
    address = Seq(AddressSet(address, 0x3f)),
    device = device,
    beatBytes = beatBytes)

  lazy val module = new InputStreamModuleImp(this)
}

We want to specify or override three arguments in the TLRegisterNode constructor. The first is the address of the device in the memory map. The address is specified as an AddressSet containing two values, a base address and a mask. The system bus will route all addresses that match the base address on the bits not set in the mask. In this case, we set the mask to 0x3f, which sets the lower six bits. This means that a 64 byte region starting from the base address will be routed to this device.

The second argument to TLRegisterNode is a SimpleDevice object, which provides the name and compatibility of the device table entry that will be created for the peripheral. We won’t show how this is used in this tutorial, but it will be important if you want to create a Linux kernel driver for the device.

The third argument to TLRegisterNode is beatBytes, which specifies the width of the TileLink interface. We will just pass this through from a class argument.

We want the device to be able to write a specified amount of bytes to a specified location in memory, so we’ll provide addr and len registers. We will also want a running register for the CPU to signal that the device should start operation and a complete register for the device to signal to the CPU that it has completed.

class InputStreamModuleImp(outer: InputStream) extends LazyModuleImp(outer) {
    val addrBits = 64
    val w = 64
    val io = IO(new Bundle {
        // Not used yet
        val in = Flipped(Decoupled(UInt(w.W)))
    }
    val addr = Reg(UInt(addrBits.W))
    val len = Reg(UInt(addrBits.W))
    val running = RegInit(false.B)
    val complete = RegInit(false.B)

    outer.regnode.regmap(
        0x00 -> Seq(RegField(addrBits, addr)),
        0x08 -> Seq(RegField(addrBits, len)),
        0x10 -> Seq(RegField(1, running)),
        0x18 -> Seq(RegField(1, complete)))
}

The arguments to regmap should be a series of mappings from address offsets to sequences of RegField objects. The RegField constructor takes two arguments, the width of the register field and the RTL register itself.