6.10. Adding a DMA Device
DMA devices are Tilelink widgets which act as masters. In other words, DMA devices can send their own read and write requests to the chip’s memory system.
For IO devices or accelerators (like a disk or network driver), instead of having the CPU poll data from the device, we may want to have the device write directly to the coherent memory system instead. For example, here is a device that writes zeros to the memory at a configured address.
package chipyard.example
import chisel3._
import chisel3.util._
import freechips.rocketchip.subsystem.{BaseSubsystem, CacheBlockBytes, FBUS}
import org.chipsalliance.cde.config.{Parameters, Field, Config}
import freechips.rocketchip.diplomacy.{LazyModule, LazyModuleImp, IdRange}
import freechips.rocketchip.tilelink._
case class InitZeroConfig(base: BigInt, size: BigInt)
case object InitZeroKey extends Field[Option[InitZeroConfig]](None)
class InitZero(implicit p: Parameters) extends LazyModule {
val node = TLClientNode(Seq(TLMasterPortParameters.v1(Seq(TLClientParameters(
name = "init-zero", sourceId = IdRange(0, 1))))))
lazy val module = new InitZeroModuleImp(this)
}
class InitZeroModuleImp(outer: InitZero) extends LazyModuleImp(outer) {
val config = p(InitZeroKey).get
val (mem, edge) = outer.node.out(0)
val addrBits = edge.bundle.addressBits
val blockBytes = p(CacheBlockBytes)
require(config.size % blockBytes == 0)
val s_init :: s_write :: s_resp :: s_done :: Nil = Enum(4)
val state = RegInit(s_init)
val addr = Reg(UInt(addrBits.W))
val bytesLeft = Reg(UInt(log2Ceil(config.size+1).W))
mem.a.valid := state === s_write
mem.a.bits := edge.Put(
fromSource = 0.U,
toAddress = addr,
lgSize = log2Ceil(blockBytes).U,
data = 0.U)._2
mem.d.ready := state === s_resp
when (state === s_init) {
addr := config.base.U
bytesLeft := config.size.U
state := s_write
}
when (edge.done(mem.a)) {
addr := addr + blockBytes.U
bytesLeft := bytesLeft - blockBytes.U
state := s_resp
}
when (mem.d.fire) {
state := Mux(bytesLeft === 0.U, s_done, s_write)
}
}
trait CanHavePeripheryInitZero { this: BaseSubsystem =>
implicit val p: Parameters
p(InitZeroKey) .map { k =>
val fbus = locateTLBusWrapper(FBUS)
val initZero = fbus { LazyModule(new InitZero()(p)) }
fbus.coupleFrom("init-zero") { _ := initZero.node }
}
}
// DOC include start: WithInitZero
class WithInitZero(base: BigInt, size: BigInt) extends Config((site, here, up) => {
case InitZeroKey => Some(InitZeroConfig(base, size))
})
// DOC include end: WithInitZero
class DigitalTop(implicit p: Parameters) extends ChipyardSystem
with testchipip.tsi.CanHavePeripheryUARTTSI // Enables optional UART-based TSI transport
with testchipip.boot.CanHavePeripheryCustomBootPin // Enables optional custom boot pin
with testchipip.boot.CanHavePeripheryBootAddrReg // Use programmable boot address register
with testchipip.cosim.CanHaveTraceIO // Enables optionally adding trace IO
with testchipip.soc.CanHaveBankedScratchpad // Enables optionally adding a banked scratchpad
with testchipip.iceblk.CanHavePeripheryBlockDevice // Enables optionally adding the block device
with testchipip.serdes.CanHavePeripheryTLSerial // Enables optionally adding the tl-serial interface
with testchipip.serdes.old.CanHavePeripheryTLSerial // Enables optionally adding the DEPRECATED tl-serial interface
with testchipip.soc.CanHavePeripheryChipIdPin // Enables optional pin to set chip id for multi-chip configs
with sifive.blocks.devices.i2c.HasPeripheryI2C // Enables optionally adding the sifive I2C
with sifive.blocks.devices.timer.HasPeripheryTimer // Enables optionally adding the timer device
with sifive.blocks.devices.pwm.HasPeripheryPWM // Enables optionally adding the sifive PWM
with sifive.blocks.devices.uart.HasPeripheryUART // Enables optionally adding the sifive UART
with sifive.blocks.devices.gpio.HasPeripheryGPIO // Enables optionally adding the sifive GPIOs
with sifive.blocks.devices.spi.HasPeripherySPIFlash // Enables optionally adding the sifive SPI flash controller
with sifive.blocks.devices.spi.HasPeripherySPI // Enables optionally adding the sifive SPI port
with icenet.CanHavePeripheryIceNIC // Enables optionally adding the IceNIC for FireSim
with chipyard.example.CanHavePeripheryInitZero // Enables optionally adding the initzero example widget
with chipyard.example.CanHavePeripheryGCD // Enables optionally adding the GCD example widget
with chipyard.example.CanHavePeripheryStreamingFIR // Enables optionally adding the DSPTools FIR example widget
with chipyard.example.CanHavePeripheryStreamingPassthrough // Enables optionally adding the DSPTools streaming-passthrough example widget
with nvidia.blocks.dla.CanHavePeripheryNVDLA // Enables optionally having an NVDLA
with chipyard.clocking.HasChipyardPRCI // Use Chipyard reset/clock distribution
with chipyard.clocking.CanHaveClockTap // Enables optionally adding a clock tap output port
with fftgenerator.CanHavePeripheryFFT // Enables optionally having an MMIO-based FFT block
with constellation.soc.CanHaveGlobalNoC // Support instantiating a global NoC interconnect
with rerocc.CanHaveReRoCCTiles // Support tiles that instantiate rerocc-attached accelerators
{
override lazy val module = new DigitalTopModule(this)
}
class DigitalTopModule(l: DigitalTop) extends ChipyardSystemModule(l)
with freechips.rocketchip.util.DontTouch
We use TLClientNode
to create a TileLink client node for us.
We then connect the client node to the memory system through the front bus (fbus).
For more info on creating TileLink client nodes, take a look at Client Node.
Once we’ve created our top-level module including the DMA widget, we can create a configuration for it as we did before.
class WithInitZero(base: BigInt, size: BigInt) extends Config((site, here, up) => {
case InitZeroKey => Some(InitZeroConfig(base, size))
})
class InitZeroRocketConfig extends Config(
new chipyard.example.WithInitZero(0x88000000L, 0x1000L) ++ // add InitZero
new freechips.rocketchip.rocket.WithNHugeCores(1) ++
new chipyard.config.AbstractConfig)