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._import org.chipsalliance.cde.config.{Parameters, Field, Config}import freechips.rocketchip.diplomacy.{LazyModule, LazyModuleImp, IdRange}import freechips.rocketchip.tilelink._import testchipip.soc.{SubsystemInjector, SubsystemInjectorKey}
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) }}
case object InitZeroInjector extends SubsystemInjector((p, baseSubsystem) => { p(InitZeroKey) .map { k => implicit val q: Parameters = p val fbus = baseSubsystem.locateTLBusWrapper(FBUS) val initZero = fbus { LazyModule(new InitZero()(p)) } fbus.coupleFrom("init-zero") { _ := initZero.node } }})
// DOC include start: WithInitZeroclass WithInitZero(base: BigInt, size: BigInt) extends Config((site, here, up) => { case InitZeroKey => Some(InitZeroConfig(base, size)) case SubsystemInjectorKey => up(SubsystemInjectorKey) + InitZeroInjector})// DOC include end: WithInitZeroclass 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.cosim.CanHaveTraceIO // Enables optionally adding trace IO with testchipip.soc.CanHaveSubsystemInjectors // Enables the subsystem injector API with testchipip.soc.CanHaveSwitchableOffchipBus // Enables optional off-chip-bus with interface-switch 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.CanHavePeripheryGCD // Enables optionally adding the GCD example widget with chipyard.clocking.HasChipyardPRCI // Use Chipyard reset/clock distribution with chipyard.clocking.CanHaveClockTap // Enables optionally adding a clock tap output port with constellation.soc.CanHaveGlobalNoC // Support instantiating a global NoC interconnect with rerocc.CanHaveReRoCCTiles // Support tiles that instantiate rerocc-attached accelerators with testchipip.ctc.CanHavePeripheryCTC // Support optional CTC link{ override lazy val module = new DigitalTopModule(this)}
class DigitalTopModule(l: DigitalTop) extends ChipyardSystemModule(l) with freechips.rocketchip.util.DontTouchWe 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)) case SubsystemInjectorKey => up(SubsystemInjectorKey) + InitZeroInjector})class InitZeroRocketConfig extends Config( new chipyard.example.WithInitZero(0x88000000L, 0x1000L) ++ // add InitZero new freechips.rocketchip.rocket.WithNHugeCores(1) ++ new chipyard.config.AbstractConfig)For an example of a simple DMA engine integrated into a Chipyard generator, see Mempress.