diff --git a/README.md b/README.md index 22c7fe98..f0d624a0 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ BAR projects generally use these components with [chipyard](https://chipyard.rea ## ``testchipip.ctc`` * ``CTC``: Simple, 32-bit chip-to-chip interface +* ``CTCMem``: SerialRAM-like backing memory for CTC ## ``testchipip.dram`` * ``SimDRAM``: DRAMSim-backed AXI-4 memory model @@ -35,6 +36,7 @@ BAR projects generally use these components with [chipyard](https://chipyard.rea ## ``testchipip.soc`` * ``OffchipBus``: Custom bus for interfacing with off-chip memory +* ``OffchipRouter``: Routes requests to the die-to-die ports * ``Scratchpad``: TileLink SRAM-backed on-chip scratchpad memory * ``SimDTM``: Simulation model for interacting with on-chip debug module * ``TLNetwork``: DEPRECATED mechanism for creating a TileLink network-on-chip. Use Constellation NoC instead diff --git a/src/main/resources/testchipip/csrc/SimTSI.cc b/src/main/resources/testchipip/csrc/SimTSI.cc index 6fe37895..f56f52a8 100644 --- a/src/main/resources/testchipip/csrc/SimTSI.cc +++ b/src/main/resources/testchipip/csrc/SimTSI.cc @@ -49,7 +49,7 @@ extern "C" int tsi_tick( remove_vcs_simv_opt(info.argc, info.argv); // TODO: We should somehow inspect whether or not our backing memory supports loadmem, instead of unconditionally setting it to true - tsis[chip_id] = new testchip_tsi_t(info.argc, info.argv, true); + tsis[chip_id] = new testchip_tsi_t(info.argc, info.argv, true, chip_id); } testchip_tsi_t* tsi = tsis[chip_id]; diff --git a/src/main/resources/testchipip/csrc/testchip_tsi.cc b/src/main/resources/testchipip/csrc/testchip_tsi.cc index 8a3e45cc..17eaee0b 100644 --- a/src/main/resources/testchipip/csrc/testchip_tsi.cc +++ b/src/main/resources/testchipip/csrc/testchip_tsi.cc @@ -1,7 +1,7 @@ #include "testchip_tsi.h" #include -testchip_tsi_t::testchip_tsi_t(int argc, char** argv, bool can_have_loadmem) : tsi_t(argc, argv) +testchip_tsi_t::testchip_tsi_t(int argc, char** argv, bool can_have_loadmem, int chip_id) : tsi_t(argc, argv) { has_loadmem = false; init_accesses = std::vector(); @@ -14,6 +14,27 @@ testchip_tsi_t::testchip_tsi_t(int argc, char** argv, bool can_have_loadmem) : t has_loadmem = can_have_loadmem; if (arg.find("+cflush_addr=0x") == 0) cflush_addr = strtoull(arg.substr(15).c_str(), 0, 16); + // We cannot use init_write to program the chip ID directly because each chip id needs to be programmed individually, + // and init_write does not distinguish between chips. + if (arg.find("+chip_id") == 0) { + static const std::string prefix = "+chip_id"; + auto eq = arg.find('='); + if (eq == std::string::npos || eq <= prefix.size() || arg.find("=0x", eq) != eq) { + throw std::invalid_argument("Improperly formatted +chip_id argument"); + } + unsigned long arg_chip_id = strtoul(arg.substr(prefix.size(), eq - prefix.size()).c_str(), 0, 0); + if ((int)arg_chip_id == chip_id) { + auto d = arg.find(":0x", eq + 3); + if (d == std::string::npos) { + throw std::invalid_argument("Improperly formatted +chip_id argument"); + } + uint64_t addr = strtoull(arg.substr(eq + 3, d - (eq + 3)).c_str(), 0, 16); + uint32_t val = strtoull(arg.substr(d + 3).c_str(), 0, 16); + init_access_t access = { .address=addr, .stdata=val, .store=true }; + init_accesses.push_back(access); + } + } + } testchip_htif_t::parse_htif_args(args); diff --git a/src/main/resources/testchipip/csrc/testchip_tsi.h b/src/main/resources/testchipip/csrc/testchip_tsi.h index 7195139b..53c21f34 100644 --- a/src/main/resources/testchipip/csrc/testchip_tsi.h +++ b/src/main/resources/testchipip/csrc/testchip_tsi.h @@ -10,7 +10,7 @@ class testchip_tsi_t : public tsi_t, public testchip_htif_t { public: - testchip_tsi_t(int argc, char** argv, bool has_loadmem); + testchip_tsi_t(int argc, char** argv, bool has_loadmem, int chip_id); virtual ~testchip_tsi_t() {}; void write_chunk(addr_t taddr, size_t nbytes, const void* src) override; diff --git a/src/main/scala/ctc/CTC.scala b/src/main/scala/ctc/CTC.scala index 71b1128e..7ea4a25f 100644 --- a/src/main/scala/ctc/CTC.scala +++ b/src/main/scala/ctc/CTC.scala @@ -14,7 +14,6 @@ import testchipip.soc._ import testchipip.serdes._ - object CTC { val INNER_WIDTH = 32 val INNER_WIDTH_BYTES = INNER_WIDTH / 8 @@ -29,28 +28,141 @@ object CTCCommand { } case class CTCParams( - translationParams: AddressTranslatorParams = OutwardAddressTranslatorParams(onchipAddr = 0x1000000000L, offchipAddr = 0x0L, size = ((1L << 32) - 1)), + translationParams: Option[AddressTranslatorParams] = Some(OutwardAddressTranslatorParams(onchipAddr = 0x1000000000L, offchipAddr = 0x0L, size = ((1L << 32) - 1))), offchip: Seq[AddressSet] = Nil, - managerBus: Option[TLBusWrapperLocation] = Some(SBUS), - clientBus: Option[TLBusWrapperLocation] = Some(SBUS), - phyParams: Option[SerialPhyParams] = Some(CreditedSourceSyncSerialPhyParams(phitWidth = CTC.OUTER_WIDTH, flitWidth = CTC.INNER_WIDTH, freqMHz = 100, flitBufferSz = 16)) // Set to None to disable PHY -) { - def offchipRange = translationParams match { - case OutwardAddressTranslatorParams(_,_,_) => Seq(translationParams.offchipRange) - case InwardAddressTranslatorParams(_,_,_) => { - require(offchip != Nil) - offchip + managerBus: TLBusWrapperLocation = SBUS, + clientBus: TLBusWrapperLocation = SBUS, + phyParams: Option[SerialPhyParams] = Some(CreditedSourceSyncSerialPhyParams(phitWidth = CTC.OUTER_WIDTH, flitWidth = CTC.INNER_WIDTH, freqMHz = 100, flitBufferSz = 16)) // Set to None to remove PHY +) extends ChipletLinkParams + with ChipletLinkWrapperInstantiationLike + { + def offchipRange = translationParams.map { tp => + tp match { + case OutwardAddressTranslatorParams(_,_,_) => Seq(tp.offchipRange) + case InwardAddressTranslatorParams(_,_,_) => { + require(offchip != Nil) + offchip + } } - } + }.getOrElse(offchip) + + def managerBusWhere = managerBus + def controlManagerBusWhere = None + def instantiate(params: OffchipSubsystemParams, id: Int)(implicit p: Parameters): ChipletLinkWrapper = LazyModule(new CTCChipletLink(this, params, id)) } +case object CTCKey extends Field[Seq[CTCParams]](Nil) + // For using CTC in a chiplet firesim config with no PHY -class CTCBridgeIO extends Bundle { +class CTCBridgeIO extends ChipletIO { val client_flit = new DecoupledFlitIO(CTC.INNER_WIDTH) // Driven by client/ctc2tl val manager_flit = new DecoupledFlitIO(CTC.INNER_WIDTH) // Driven by manager/tl2ctc + + def tieoff: Unit = { + manager_flit := DontCare + manager_flit.in.valid := false.B + manager_flit.out.ready := false.B + client_flit := DontCare + client_flit.in.valid := false.B + client_flit.out.ready := false.B + } + + def connect(io: ChipletIO): Unit = io match { + case io: CTCBridgeIO => { + client_flit.in <> io.manager_flit.out + io.client_flit.in <> manager_flit.out + manager_flit.in <> io.client_flit.out + io.manager_flit.in <> client_flit.out + } + case _ => assert(false, s"IO does not match CTCBridgeIO: ${io.getClass}") + } + + def loopback: Unit = { + client_flit.in <> manager_flit.out + manager_flit.in <> client_flit.out + } } -case object CTCKey extends Field[Seq[CTCParams]](Nil) +class CTCMemIO(phitWidth: Int, offchip: Seq[AddressSet], phyParams: SerialPhyParams) extends DecoupledInternalSyncPhitIO(phitWidth) { + def connectRAM(implicit p: Parameters): Unit = { + withClock(clock_out) { + val ram = Module(LazyModule(new CTCMem(offchip, phyParams)(p)).module) + ram.io.ser.in <> out + in <> ram.io.ser.out + } + } +} + +case class CTCMemSerialPhyParams( + phitWidth: Int = CTC.OUTER_WIDTH, + flitWidth: Int = CTC.INNER_WIDTH, + flitBufferSz: Int = 16, + offchip: Seq[AddressSet]) extends SerialPhyParams { + def genIO = new CTCMemIO(phitWidth, offchip, this) +} + +class CTCChipletLink(val params: CTCParams, val sys_params: OffchipSubsystemParams, val id: Int)(implicit p: Parameters) extends ChipletLinkWrapper { + // a TL master/client device + val ctc2tl = LazyModule(new CTCToTileLink(portId=id)(p)) + // a TL slave/manager device + val tl2ctc = LazyModule(new TileLinkToCTC(addrRegion=sys_params.managerRegion)(p)) + + val client_node = ctc2tl.node + val manager_node = tl2ctc.node + val control_manager_node = None + val clock_node = None + val top_IO = params.phyParams match { + case Some(pP) => BundleBridgeSource(() => pP.genIO.asInstanceOf[ChipletIO]) + case None => BundleBridgeSource(() => new CTCBridgeIO) + } + override lazy val module = new CTCChipletLinkImpl(this) +} + +class CTCChipletLinkImpl(outer: CTCChipletLink) extends LazyModuleImp(outer) { + val io = outer.top_IO.out(0)._1 + + io match { + case io: CreditedSourceSyncPhitIO => { + val outgoing_clock = clock + val outgoing_reset = ResetCatchAndSync(outgoing_clock, reset.asBool) + val incoming_clock = io.clock_in + val incoming_reset = ResetCatchAndSync(incoming_clock, io.reset_in.asBool) + io.clock_out := outgoing_clock + io.reset_out := outgoing_reset.asAsyncReset + val phy = Module(new CreditedSerialPhy(2, outer.params.phyParams.get)) + phy.io.incoming_clock := incoming_clock + phy.io.incoming_reset := incoming_reset + phy.io.outgoing_clock := outgoing_clock + phy.io.outgoing_reset := outgoing_reset + phy.io.inner_clock := outer.ctc2tl.module.clock + phy.io.inner_reset := outer.ctc2tl.module.reset + phy.io.inner_ser(0).in <> outer.ctc2tl.module.io.flit.in + phy.io.inner_ser(0).out <> outer.tl2ctc.module.io.flit.out + phy.io.inner_ser(1).in <> outer.tl2ctc.module.io.flit.in + phy.io.inner_ser(1).out <> outer.ctc2tl.module.io.flit.out + phy.io.outer_ser <> io.viewAsSupertype(new ValidPhitIO(outer.params.phyParams.get.phitWidth)) + } + case io: CTCMemIO => { + val outgoing_clock = clock + io.clock_out := outgoing_clock + val phy = Module(new DecoupledSerialPhy(2, outer.params.phyParams.get)) + phy.io.outer_clock := outgoing_clock + phy.io.outer_reset := ResetCatchAndSync(outgoing_clock, reset.asBool) + phy.io.inner_clock := outer.ctc2tl.module.clock + phy.io.inner_reset := outer.ctc2tl.module.reset + phy.io.inner_ser(0).in <> outer.ctc2tl.module.io.flit.in + phy.io.inner_ser(0).out <> outer.tl2ctc.module.io.flit.out + phy.io.inner_ser(1).in <> outer.tl2ctc.module.io.flit.in + phy.io.inner_ser(1).out <> outer.ctc2tl.module.io.flit.out + phy.io.outer_ser <> io.viewAsSupertype(new DecoupledPhitIO(outer.params.phyParams.get.phitWidth)) + } + case io: CTCBridgeIO => { + io.manager_flit <> outer.tl2ctc.module.io.flit + io.client_flit <> outer.ctc2tl.module.io.flit + } + case _ => assert(false, s"IO unsupported for CTC: ${io.getClass}") + } +} trait CanHavePeripheryCTC { this: BaseSubsystem => private val portName = "ctc" @@ -65,15 +177,15 @@ trait CanHavePeripheryCTC { this: BaseSubsystem => assert(pP.flitWidth == CTC.INNER_WIDTH) } - lazy val slave_bus = locateTLBusWrapper(params.managerBus.get) - lazy val master_bus = locateTLBusWrapper(params.clientBus.get) + lazy val slave_bus = locateTLBusWrapper(params.managerBus) + lazy val master_bus = locateTLBusWrapper(params.clientBus) val ctc_domain = LazyModule(new ClockSinkDomain(name=Some(s"CTC$id"))) ctc_domain.clockNode := slave_bus.fixedClockNode - val translator = ctc_domain { - LazyModule(AddressTranslator(params.translationParams)(p)) - } + val translator = ctc_domain { params.translationParams.map { tp => + LazyModule(AddressTranslator(tp)(p)) + }} require(slave_bus.dtsFrequency.isDefined, s"Slave bus ${slave_bus.busName} must provide a frequency") @@ -89,14 +201,18 @@ trait CanHavePeripheryCTC { this: BaseSubsystem => params.translationParams match { // Translate outgoing requests - case OutwardAddressTranslatorParams(_,_,_) => { - slave_bus.coupleTo(portName) { translator(tl2ctc.node) := TLBuffer() := _ } + case Some(OutwardAddressTranslatorParams(_,_,_)) => { + slave_bus.coupleTo(portName) { translator.get(tl2ctc.node) := TLBuffer() := _ } master_bus.coupleFrom(portName) { _ := TLBuffer() := ctc2tl.node } } // Translate incoming requests - case InwardAddressTranslatorParams(_,_,_) => { + case Some(InwardAddressTranslatorParams(_,_,_)) => { + slave_bus.coupleTo(portName) { tl2ctc.node := TLBuffer() := _ } + master_bus.coupleFrom(portName) { _ := TLBuffer() := translator.get(ctc2tl.node) } + } + case None => { slave_bus.coupleTo(portName) { tl2ctc.node := TLBuffer() := _ } - master_bus.coupleFrom(portName) { _ := TLBuffer() := translator(ctc2tl.node) } + master_bus.coupleFrom(portName) { _ := TLBuffer() := ctc2tl.node } } } diff --git a/src/main/scala/ctc/CTCMem.scala b/src/main/scala/ctc/CTCMem.scala index a85a0e69..827df9be 100644 --- a/src/main/scala/ctc/CTCMem.scala +++ b/src/main/scala/ctc/CTCMem.scala @@ -8,19 +8,18 @@ import freechips.rocketchip.tilelink._ import freechips.rocketchip.devices.tilelink._ import freechips.rocketchip.diplomacy._ import freechips.rocketchip.util._ -import testchipip.serdes.{DecoupledPhitIO, DecoupledSerialPhy} +import testchipip.serdes.{DecoupledPhitIO, DecoupledSerialPhy, SerialPhyParams} import testchipip.ctc._ -// A test memory like SerialRAM but for CTC -// Takes in DecoupledFlitIO, puts it through CTCToTileLink, then connects the TileLink node to TLRAM -class CTCMem(params: CTCParams)(implicit p: Parameters) extends LazyModule { +// A test memory like SerialRAM but for CTC, used for modeling chiplets as RAM +class CTCMem(offchip: Seq[AddressSet], phyParams: SerialPhyParams)(implicit p: Parameters) extends LazyModule { val beatBytes = 8 val ctc2tl = LazyModule(new CTCToTileLink(sourceIds = 1, portId = 0)) val xbar = TLXbar() // Use smaller memory size for testing - val testmems = params.offchip.map(address => LazyModule(new TLRAM(address = AddressSet(address.base, 0xFFFFFL), beatBytes = beatBytes) {override lazy val desiredName = "CTCMem"})) + val testmems = offchip.map(address => LazyModule(new TLRAM(address = AddressSet(address.base, 0xFFFFFL), beatBytes = beatBytes) {override lazy val desiredName = "CTCMem"})) testmems.foreach { s => (s.node := TLBuffer() := TLFragmenter(beatBytes, p(CacheBlockBytes), nameSuffix = Some("CTCMem")) @@ -35,7 +34,7 @@ class CTCMem(params: CTCParams)(implicit p: Parameters) extends LazyModule { val ser = new DecoupledPhitIO(CTC.OUTER_WIDTH) }) - val phy = Module(new DecoupledSerialPhy(2, params.phyParams.get)) + val phy = Module(new DecoupledSerialPhy(2, phyParams)) phy.io.outer_clock := clock phy.io.outer_reset := reset phy.io.inner_clock := clock diff --git a/src/main/scala/serdes/Bundles.scala b/src/main/scala/serdes/Bundles.scala index 7b440855..19b60c26 100644 --- a/src/main/scala/serdes/Bundles.scala +++ b/src/main/scala/serdes/Bundles.scala @@ -4,6 +4,7 @@ import chisel3._ import chisel3.util._ import freechips.rocketchip.diplomacy._ import org.chipsalliance.cde.config._ +import testchipip.soc.ChipletIO class Flit(val flitWidth: Int) extends Bundle { val flit = UInt(flitWidth.W) @@ -34,11 +35,44 @@ trait HasClockIn { this: Bundle => // A decoupled flow-control serial interface where all signals are synchronous to // a locally-produced clock -class DecoupledInternalSyncPhitIO(phitWidth: Int) extends DecoupledPhitIO(phitWidth) with HasClockOut +class DecoupledInternalSyncPhitIO(phitWidth: Int) extends DecoupledPhitIO(phitWidth) with HasClockOut with ChipletIO { + def tieoff: Unit = { + out.ready := false.B + in.valid := false.B + in.bits := DontCare + } + def connect(io: ChipletIO): Unit = io match { + case that: DecoupledExternalSyncPhitIO => + that.clock_in := this.clock_out + that.out <> this.in + this.out <> that.in + case _ => assert(false, s"IO does not match DecoupledExternalSyncPhitIO: ${io.getClass}") + } + def loopback: Unit = { + assert(false, "DecoupledInternalSyncPhitIO does not support loopback") + } +} // A decoupled flow-control serial interface where all signals are synchronous to // an externally produced clock -class DecoupledExternalSyncPhitIO(phitWidth: Int) extends DecoupledPhitIO(phitWidth) with HasClockIn +class DecoupledExternalSyncPhitIO(phitWidth: Int) extends DecoupledPhitIO(phitWidth) with HasClockIn with ChipletIO { + def tieoff: Unit = { + clock_in := false.B.asClock + out.ready := false.B + in.valid := false.B + in.bits := DontCare + } + def connect(io: ChipletIO): Unit = io match { + case that: DecoupledInternalSyncPhitIO => + that.clock_out := this.clock_in + that.out <> this.in + this.out <> that.in + case _ => assert(false, s"IO does not match DecoupledInternalSyncPhitIO: ${io.getClass}") + } + def loopback: Unit = { + assert(false, "DecoupledExternalSyncPhitIO does not support loopback") + } +} class ValidPhitIO(val phitWidth: Int) extends Bundle { val in = Input(Valid(new Phit(phitWidth))) @@ -52,11 +86,34 @@ class ValidFlitIO(val flitWidth: Int) extends Bundle { // A credited flow-control serial interface where all signals are synchronous to // a slock provided by the transmitter of that signal -class CreditedSourceSyncPhitIO(phitWidth: Int) extends ValidPhitIO(phitWidth) { +class CreditedSourceSyncPhitIO(phitWidth: Int) extends ValidPhitIO(phitWidth) with ChipletIO { val clock_in = Input(Clock()) val reset_out = Output(AsyncReset()) val clock_out = Output(Clock()) val reset_in = Input(AsyncReset()) + + def tieoff: Unit = { + clock_in := false.B.asClock + reset_in := false.B.asAsyncReset + in := DontCare + } + + def connect(io: ChipletIO): Unit = io match { + case that: CreditedSourceSyncPhitIO => + this.in := that.out + that.in := this.out + this.clock_in := that.clock_out + that.clock_in := this.clock_out + this.reset_in := that.reset_out + that.reset_in := this.reset_out + case _ => assert(false, s"IO does not match CreditedSourceSyncPhitIO: ${io.getClass}") + } + + def loopback: Unit = { + in := out + clock_in := clock_out + reset_in := reset_out + } } class SerialIO(val w: Int) extends Bundle { diff --git a/src/main/scala/serdes/Parameters.scala b/src/main/scala/serdes/Parameters.scala index 88c2b8d1..4613836a 100644 --- a/src/main/scala/serdes/Parameters.scala +++ b/src/main/scala/serdes/Parameters.scala @@ -5,12 +5,13 @@ import chisel3.util._ import freechips.rocketchip.diplomacy._ import org.chipsalliance.cde.config._ import freechips.rocketchip.tilelink._ +import testchipip.soc.ChipletIO trait SerialPhyParams { val phitWidth: Int val flitWidth: Int val flitBufferSz: Int - def genIO: Bundle + def genIO: ChipletIO } case class DecoupledInternalSyncSerialPhyParams( diff --git a/src/main/scala/serdes/PeripheryTLSerial.scala b/src/main/scala/serdes/PeripheryTLSerial.scala index 64af78c0..4af98871 100644 --- a/src/main/scala/serdes/PeripheryTLSerial.scala +++ b/src/main/scala/serdes/PeripheryTLSerial.scala @@ -11,7 +11,7 @@ import freechips.rocketchip.diplomacy._ import freechips.rocketchip.util._ import freechips.rocketchip.prci._ import testchipip.util.{ClockedIO} -import testchipip.soc.{OBUS} +import testchipip.soc._ // Parameters for a read-only-memory that appears over serial-TL case class ManagerROMParams( @@ -57,6 +57,160 @@ case class SerialTLParams( manager: Option[SerialTLManagerParams] = None, phyParams: SerialPhyParams = DecoupledExternalSyncSerialPhyParams(), bundleParams: TLBundleParameters = TLSerdesser.STANDARD_TLBUNDLE_PARAMS) +extends ChipletLinkParams with ChipletLinkWrapperInstantiationLike { + def managerBusWhere = client.map(_.masterWhere).getOrElse(SBUS) // This is the bus driven by the SerialTL port client node + def controlManagerBusWhere = None + def instantiate(params: OffchipSubsystemParams, id: Int)(implicit p: Parameters): ChipletLinkWrapper = LazyModule(new SerialTLChipletLink(this, params, id)) +} + +class SerialTLWrapper(val params: SerialTLParams, val sys_params: OffchipSubsystemParams, val id: Int, val namePrefix: String = "serial_tl")(implicit p: Parameters) extends LazyModule { + val tlChannels = 5 + + val portName = s"${namePrefix}_$id" + val clientPortParams = params.client.map { c => TLMasterPortParameters.v1( + clients = Seq.tabulate(1 << c.cacheIdBits){ i => TLMasterParameters.v1( + name = s"${portName}_${i}", + sourceId = IdRange(i << (c.totalIdBits - c.cacheIdBits), (i + 1) << (c.totalIdBits - c.cacheIdBits)), + supportsProbe = if (c.supportsProbe) TransferSizes(sys_params.clientBlockBytes, sys_params.clientBlockBytes) else TransferSizes.none + )} + )} + + val managerPortParams = params.manager.map { m => + val memParams = m.memParams + val romParams = m.romParams + val cohParams = m.cohParams + val memDevice = if (m.isMemoryDevice) new MemoryDevice else new SimpleDevice("lbwif-readwrite", Nil) + val romDevice = new SimpleDevice("lbwif-readonly", Nil) + val blockBytes = sys_params.managerBlockBytes + TLSlavePortParameters.v1( + managers = memParams.zipWithIndex.map { case (memParams, i) => TLSlaveParameters.v1( + address = AddressSet.misaligned(memParams.address, memParams.size), + resources = memDevice.reg, + regionType = RegionType.UNCACHED, // cacheable + executable = true, + supportsGet = TransferSizes(1, blockBytes), + supportsPutFull = TransferSizes(1, blockBytes), + supportsPutPartial = TransferSizes(1, blockBytes) + ).v2copy(name = Some(s"${portName}_mem_${i}")) + } ++ romParams.zipWithIndex.map { case (romParams, i) => TLSlaveParameters.v1( + address = List(AddressSet(romParams.address, romParams.size-1)), + resources = romDevice.reg, + regionType = RegionType.UNCACHED, // cacheable + executable = true, + supportsGet = TransferSizes(1, blockBytes), + fifoId = Some(0) + ).v2copy(name = Some(s"${portName}_rom_${i}")) + } ++ cohParams.zipWithIndex.map { case (cohParams, i) => TLSlaveParameters.v1( + address = AddressSet.misaligned(cohParams.address, cohParams.size), + regionType = RegionType.TRACKED, // cacheable + executable = true, + supportsAcquireT = TransferSizes(1, blockBytes), + supportsAcquireB = TransferSizes(1, blockBytes), + supportsGet = TransferSizes(1, blockBytes), + supportsPutFull = TransferSizes(1, blockBytes), + supportsPutPartial = TransferSizes(1, blockBytes) + ).v2copy(name = Some(s"${portName}_coh_${i}")) + }, + beatBytes = sys_params.managerBeatBytes, + endSinkId = if (cohParams.isEmpty) 0 else (1 << m.sinkIdBits), + minLatency = 1 + ) + } + + val serdesser = LazyModule(new TLSerdesser( + flitWidth = params.phyParams.flitWidth, + clientPortParams = clientPortParams, + managerPortParams = managerPortParams, + bundleParams = params.bundleParams, + nameSuffix = Some(portName) + )) + + // If we provide a clock, generate a clock domain for the outgoing clock + val serial_tl_clock_freqMHz = params.phyParams match { + case params: DecoupledInternalSyncSerialPhyParams => Some(params.freqMHz) + case params: DecoupledExternalSyncSerialPhyParams => None + case params: CreditedSourceSyncSerialPhyParams => Some(params.freqMHz) + } + val serial_tl_clock_node = serial_tl_clock_freqMHz.map { f => + ClockSinkNode(Seq(ClockSinkParameters(take=Some(ClockParameters(f))))) + } + + // Expose IO via BundleBridge + val top_IO = BundleBridgeSource(() => params.phyParams.genIO.asInstanceOf[ChipletIO]) + val debug_IO = BundleBridgeSource(() => new SerdesDebugIO) + + override lazy val module = new SerialTLWrapperModule(this) +} + +class SerialTLWrapperModule(outer: SerialTLWrapper) extends LazyModuleImp(outer) { + val io = outer.top_IO.out(0)._1 + val debug = outer.debug_IO.out(0)._1 + + debug := outer.serdesser.module.io.debug + + io match { + case io: DecoupledInternalSyncPhitIO => { + val outer_clock = outer.serial_tl_clock_node.get.in.head._1.clock + io.clock_out := outer_clock + val phy = Module(new DecoupledSerialPhy(outer.tlChannels, outer.params.phyParams)) + phy.io.outer_clock := outer_clock + phy.io.outer_reset := ResetCatchAndSync(outer_clock, outer.serdesser.module.reset.asBool) + phy.io.inner_clock := outer.serdesser.module.clock + phy.io.inner_reset := outer.serdesser.module.reset + phy.io.outer_ser <> io.viewAsSupertype(new DecoupledPhitIO(io.phitWidth)) + phy.io.inner_ser <> outer.serdesser.module.io.ser + } + case io: DecoupledExternalSyncPhitIO => { + val outer_clock = io.clock_in + val phy = Module(new DecoupledSerialPhy(outer.tlChannels, outer.params.phyParams)) + phy.io.outer_clock := outer_clock + phy.io.outer_reset := ResetCatchAndSync(outer_clock, outer.serdesser.module.reset.asBool) + phy.io.inner_clock := outer.serdesser.module.clock + phy.io.inner_reset := outer.serdesser.module.reset + phy.io.outer_ser <> io.viewAsSupertype(new DecoupledPhitIO(outer.params.phyParams.phitWidth)) + phy.io.inner_ser <> outer.serdesser.module.io.ser + } + case io: CreditedSourceSyncPhitIO => { + val outgoing_clock = outer.serial_tl_clock_node.get.in.head._1.clock + val outgoing_reset = ResetCatchAndSync(outgoing_clock, outer.serdesser.module.reset.asBool) + val incoming_clock = io.clock_in + val incoming_reset = ResetCatchAndSync(incoming_clock, io.reset_in.asBool) + io.clock_out := outgoing_clock + io.reset_out := outgoing_reset.asAsyncReset + val phy = Module(new CreditedSerialPhy(outer.tlChannels, outer.params.phyParams)) + phy.io.incoming_clock := incoming_clock + phy.io.incoming_reset := incoming_reset + phy.io.outgoing_clock := outgoing_clock + phy.io.outgoing_reset := outgoing_reset + phy.io.inner_clock := outer.serdesser.module.clock + phy.io.inner_reset := outer.serdesser.module.reset + phy.io.inner_ser <> outer.serdesser.module.io.ser + phy.io.outer_ser <> io.viewAsSupertype(new ValidPhitIO(outer.params.phyParams.phitWidth)) + } + } +} + +class SerialTLChipletLink(val params: SerialTLParams, val sys_params: OffchipSubsystemParams, val id: Int)(implicit p: Parameters) extends ChipletLinkWrapper { + require(params.manager.isDefined, "Chip-to-chip SerialTL must have a manager") + require(params.client.isDefined, "Chip-to-chip SerialTL must have a client") + + // Override memParams with address regions from the offchip subsystem + val chipletParams = params.copy( + manager = params.manager.map(_.copy( + memParams = sys_params.managerRegion.map(as => ManagerRAMParams(address = as.base, size = as.mask + 1)) + )) + ) + val wrapper = LazyModule(new SerialTLWrapper(chipletParams, sys_params, id, namePrefix = "d2d_serial_tl")) + + val client_node = wrapper.serdesser.clientNode.get + val manager_node = wrapper.serdesser.managerNode.get + val control_manager_node = None + val clock_node = wrapper.serial_tl_clock_node + val top_IO = wrapper.top_IO + val debug_IO = wrapper.debug_IO + + override lazy val module = new LazyModuleImp(this) { } +} case object SerialTLKey extends Field[Seq[SerialTLParams]](Nil) diff --git a/src/main/scala/soc/ChipletLink.scala b/src/main/scala/soc/ChipletLink.scala new file mode 100644 index 00000000..ac1fc832 --- /dev/null +++ b/src/main/scala/soc/ChipletLink.scala @@ -0,0 +1,52 @@ +package testchipip.soc + +import chisel3._ +import chisel3.util._ +import org.chipsalliance.cde.config.{Parameters, Field} +import freechips.rocketchip.subsystem._ +import freechips.rocketchip.tilelink._ +import freechips.rocketchip.devices.tilelink._ +import freechips.rocketchip.diplomacy._ +import freechips.rocketchip.util._ +import freechips.rocketchip.prci._ +import freechips.rocketchip.diplomacy.BundleBridgeSource + +case object OffchipAddressRange extends Field[Seq[AddressSet]](AddressSet.misaligned(0x100000000L, 0x1000000000L)) + +// Link params should extend this trait +trait ChipletLinkParams{ + def managerBusWhere: TLBusWrapperLocation // Where the link client node is connected + def controlManagerBusWhere: Option[TLBusWrapperLocation] // Where the link control node is connected +} + +case class OffchipSubsystemParams( + val managerRegion: Seq[AddressSet], + val clientBeatBytes: Int, + val clientBlockBytes: Int, + val managerBeatBytes: Int, + val managerBlockBytes: Int +) + +abstract class ChipletLinkWrapper(implicit p: Parameters) extends LazyModule { + val client_node: TLClientNode + val manager_node: TLManagerNode + val control_manager_node: Option[TLRegisterNode] + val clock_node: Option[ClockNode] + val top_IO: BundleBridgeSource[ChipletIO] +} + +abstract class ChipletLinkWrapperImpl(outer: ChipletLinkWrapper) extends LazyModuleImp(outer) { + val io: ChipletIO +} + +// Links should "with" this trait +trait ChipletLinkWrapperInstantiationLike { + def instantiate(params: OffchipSubsystemParams, id: Int)(implicit p: Parameters): ChipletLinkWrapper +} + +trait ChipletIO extends Bundle { + def tieoff: Unit + def connect(io: ChipletIO): Unit + def loopback: Unit +} + diff --git a/src/main/scala/soc/Configs.scala b/src/main/scala/soc/Configs.scala index e65e9029..dd32769e 100644 --- a/src/main/scala/soc/Configs.scala +++ b/src/main/scala/soc/Configs.scala @@ -5,6 +5,7 @@ import org.chipsalliance.cde.config.{Parameters, Config} import freechips.rocketchip.subsystem._ import freechips.rocketchip.tilelink.{TLBusWrapperTopology} import freechips.rocketchip.diplomacy.{BufferParams, AddressSet} +import testchipip.ctc.CTCParams //------------------------- // Scratchpad Configs @@ -98,3 +99,24 @@ class WithRingSystemBus( } ) }) + +//------------------------- +// Chiplet Routing Configs +//------------------------- + +// Adds a router and translator for a single CTC port +class WithChipletRouting(params: ChipletRoutingParams = ChipletRoutingParams(ports=Seq(CTCParams(phyParams = None)))) extends Config((site, here, up) => { + case ChipletRoutingKey => Some(params) +}) + +class WithD2DPorts(ports: Seq[ChipletLinkParams]) extends Config((site, here, up) => { + case ChipletRoutingKey => Some(up(ChipletRoutingKey).getOrElse(ChipletRoutingParams(ports=Nil)).copy(ports=ports)) +}) + +class WithND2DPorts(n: Int, portParams: ChipletLinkParams) extends Config((site, here, up) => { + case ChipletRoutingKey => Some(up(ChipletRoutingKey).getOrElse(ChipletRoutingParams(ports=Nil)).copy(ports=Seq.fill(n)(portParams))) +}) + +class WithOffchipAddressRange(range: Seq[AddressSet]) extends Config((site, here, up) => { + case OffchipAddressRange => range +}) \ No newline at end of file diff --git a/src/main/scala/soc/OffchipRouter.scala b/src/main/scala/soc/OffchipRouter.scala new file mode 100644 index 00000000..8816123e --- /dev/null +++ b/src/main/scala/soc/OffchipRouter.scala @@ -0,0 +1,334 @@ +package testchipip.soc + +import chisel3._ +import chisel3.util._ +import org.chipsalliance.cde.config.{Parameters, Field, Config} +import freechips.rocketchip.subsystem._ +import freechips.rocketchip.tilelink._ +import freechips.rocketchip.devices.debug.HasPeripheryDebug +import freechips.rocketchip.devices.tilelink._ +import freechips.rocketchip.diplomacy._ +import freechips.rocketchip.util._ +import freechips.rocketchip.prci._ +import freechips.rocketchip.regmapper._ +import testchipip.ctc.CTCBridgeIO // TODO: remove this later +import scala.math.min +import testchipip.util.{TLSwitch} + +case class ChipletRoutingParams( + clientBusWhere: TLBusWrapperLocation = SBUS, + controlBusWhere: TLBusWrapperLocation = CBUS, + routerParams: OffchipRouterParams = OffchipRouterParams(), + ports: Seq[ChipletLinkParams] +) { + def idWidth = log2Ceil(routerParams.tableEntries) + 1 +} + +case object ChipletRoutingKey extends Field[Option[ChipletRoutingParams]](None) + +case class OffchipRouterParams( + tableAddress: BigInt = 0x4000L, + tableEntries: Int = 16 +) + +// The offchip router uses a table to route requests to the correct die-to-die port +class OffchipRouter(val params: ChipletRoutingParams, val beatBytes: Int = 8)(implicit p: Parameters) extends LazyModule { + def unifyManagers(mgrs: Seq[Seq[TLSlaveParameters]]): Seq[TLSlaveParameters] = { + mgrs.flatten.groupBy(_.sortedAddress.head).map { case (_, m) => + require(m.forall(_.address == m.head.address), "Require homogeneous address ranges") + require(m.forall(_.regionType == m.head.regionType), "Require homogeneous regionType") + require(m.forall(_.supports == m.head.supports), "Require homogeneous supported operations") + m.head + }.toSeq + } + + val node = new TLNexusNode( + clientFn = { c => + require(c.size == 1, s"Only one ClientPort supported in TLSwitch, not $c") + c.head + }, + managerFn = { m => + require(m.flatMap(_.responseFields).size == 0, "ResponseFields not supported in TLSwitch") + require(m.flatMap(_.requestKeys).size == 0, "RequestKeys not supported in TLSwitch") + require(m.forall(_.beatBytes == m.head.beatBytes), "Homogeneous beatBytes required") + TLSlavePortParameters.v1( + beatBytes = m.head.beatBytes, + managers = unifyManagers(m.map(_.sortedSlaves)), + endSinkId = m.map(_.endSinkId).max, + minLatency = m.map(_.minLatency).min, + responseFields = Nil, + requestKeys = Nil, + ) + } + ) + + val routing_table_node = TLRegisterNode( + address = AddressSet.misaligned(params.routerParams.tableAddress, 0x1000), + device = new SimpleDevice("routing-table", Nil), + beatBytes = beatBytes + ) + + override lazy val module = new OffchipRouterImpl(this) +} + +class OffchipRouterImpl(outer: OffchipRouter) extends LazyModuleImp(outer) { + + val (bundleIn, edgeIn) = outer.node.in(0) + val bundlesOut = outer.node.out.map(_._1) + + val nPorts = bundlesOut.size + + val io = IO(new Bundle { + val chip_id = Output(Vec(nPorts, UInt(outer.params.idWidth.W))) + }) + + val tableEntries = outer.params.routerParams.tableEntries + val idWidth = outer.params.idWidth + val addressWidth = bundleIn.a.bits.address.getWidth + val portWidth = if (nPorts == 1) 1 else log2Ceil(nPorts) + + val routing_mode_reg = RegInit(true.B) // true: routing mode, false: bypass mode + val bypass_port_reg = RegInit(0.U(log2Ceil(nPorts).W)) // The port to bypass to when routing mode is disabled + val translation_mode_reg = RegInit(false.B) // true: per-port translation mode, false: chip id translation mode + val translation_table_reg = RegInit(VecInit(Seq.fill(nPorts)(0.U(idWidth.W)))) // Index with port id + + val chipIdReg = RegInit(0.U(idWidth.W)) + for (i <- 0 until nPorts) { + io.chip_id(i) := Mux(translation_mode_reg, translation_table_reg(i), chipIdReg) + } + + val routing_table = RegInit(VecInit(Seq.fill(tableEntries)(0.U.asTypeOf(new RoutingTableEntry(idWidth, portWidth))))) + + // 4 because 3 is an ugly number + val regsPerEntry = 4 + + val mapped_entries = (0 until tableEntries).flatMap { i => + val base = i * regsPerEntry * outer.beatBytes + Seq( + (base + 0 * outer.beatBytes) -> Seq(RegField(1, routing_table(i).valid.asUInt, RegFieldDesc(s"entry_${i}_valid", "Valid bit"))), + (base + 1 * outer.beatBytes) -> Seq(RegField(idWidth, routing_table(i).chipID, RegFieldDesc(s"entry_${i}_chipID", "Chip ID"))), + (base + 2 * outer.beatBytes) -> Seq(RegField(portWidth, routing_table(i).port, RegFieldDesc(s"entry_${i}_port", "Port"))), + ) + } + + val chip_id = Seq((tableEntries * regsPerEntry * outer.beatBytes) -> Seq(RegField(idWidth, chipIdReg, RegFieldDesc("chip_id", "Chip ID for this chip")))) + val routing_mode = Seq((tableEntries * regsPerEntry * outer.beatBytes + (1 * outer.beatBytes)) -> Seq(RegField(1, routing_mode_reg, RegFieldDesc("routing_mode", "Routing mode")))) + val bypass_port = Seq((tableEntries * regsPerEntry * outer.beatBytes + (2 * outer.beatBytes)) -> Seq(RegField(log2Ceil(nPorts), bypass_port_reg, RegFieldDesc("bypass_port", "Bypass port")))) + val translation_mode = Seq((tableEntries * regsPerEntry * outer.beatBytes + (3 * outer.beatBytes)) -> Seq(RegField(1, translation_mode_reg, RegFieldDesc("translation_mode", "Translation mode")))) + val translation_table = (0 until nPorts).map { i => + (tableEntries * regsPerEntry * outer.beatBytes + ((4 + i) * outer.beatBytes)) -> Seq(RegField(idWidth, translation_table_reg(i), RegFieldDesc(s"translation_table_$i", s"Translation table entry $i"))) + } + outer.routing_table_node.regmap(mapped_entries ++ chip_id ++ routing_mode ++ bypass_port ++ translation_mode ++ translation_table :_*) + + // Select offchip port from routing table + val addr_top_bits = bundleIn.a.bits.address(addressWidth-1, log2Ceil(p(OffchipAddressRange).map(_.base).min)) + val matches = Wire(Vec(tableEntries, Bool())) + val port_match = Wire(UInt(log2Ceil(nPorts).W)) + port_match := 0.U + matches := VecInit(Seq.fill(tableEntries)(false.B)) + for (i <- 0 until tableEntries) { + when (routing_table(i).valid && (addr_top_bits === routing_table(i).chipID)) { + port_match := routing_table(i).port + matches(i) := true.B + } + } + + val sel = Mux(routing_mode_reg, port_match, bypass_port_reg) + + // Assert that only one match is found + assert(!routing_mode_reg || !bundleIn.a.valid || ((matches.asUInt & (matches.asUInt - 1.U)) === 0.U), "Multiple matches found in routing table") // TODO: check + + // Send an error response if no match is found + val illegal = Wire(Bool()) + illegal := bundleIn.a.valid && !matches.reduce(_ || _) && routing_mode_reg + + val errActive = RegInit(false.B) + val errOpcode = Reg(bundleIn.a.bits.opcode.cloneType) + val errSize = Reg(bundleIn.a.bits.size.cloneType) + val errSource = Reg(bundleIn.a.bits.source.cloneType) + + val a_last = edgeIn.last(bundleIn.a) + + // Start an error response when we accept the LAST beat of an illegal request + when (!errActive && bundleIn.a.fire && a_last && illegal) { + errActive := true.B + errOpcode := bundleIn.a.bits.opcode + errSize := bundleIn.a.bits.size + errSource := bundleIn.a.bits.source + } + + // Generate an error response on the D channel + val dErr = Wire(chiselTypeOf(bundleIn.d)) + val (_, d_last, _) = edgeIn.firstlast(dErr) + + dErr.valid := errActive + dErr.bits.opcode := TLMessages.adResponse(errOpcode) + dErr.bits.param := 0.U + dErr.bits.size := errSize + dErr.bits.source := errSource + dErr.bits.sink := 0.U + dErr.bits.denied := true.B + dErr.bits.data := 0.U + dErr.bits.corrupt := edgeIn.hasData(dErr.bits) + + // Done when last D beat fires + when (dErr.fire && d_last) { errActive := false.B } + + // Switch the request to the correct port + bundlesOut.zipWithIndex.foreach { case (out, i) => + val selected = i.U === sel + + out.a.valid := bundleIn.a.valid && selected + out.a.bits := bundleIn.a.bits + + out.c.valid := bundleIn.c.valid && selected + out.c.bits := bundleIn.c.bits + + out.e.valid := bundleIn.e.valid && selected + out.e.bits := bundleIn.e.bits + } + + bundleIn.a.ready := VecInit(bundlesOut.map(_.a.ready))(sel) && !errActive + bundleIn.c.ready := VecInit(bundlesOut.map(_.c.ready))(sel) + bundleIn.e.ready := VecInit(bundlesOut.map(_.e.ready))(sel) + + // Arbitrate responses from d channels (locks for the duration of multi-beat messages) + TLArbiter.robin(edgeIn, bundleIn.d, (bundlesOut.map(_.d) :+ dErr):_*) + + // Arbitrate responses from b channels + // TODO: Error checking for coherent requests (coherent requests are currently unsupported) + val response_arbiter_b = Module(new RRArbiter(chiselTypeOf(bundleIn.b.bits), nPorts)) + for (i <- 0 until nPorts) { + response_arbiter_b.io.in(i) <> bundlesOut(i).b + } + bundleIn.b <> Queue(response_arbiter_b.io.out, nPorts) + +} + +class RoutingTableEntry(idWidth: Int, portWidth: Int) extends Bundle { + val valid = Bool() + val chipID = UInt(idWidth.W) + val port = UInt(portWidth.W) +} + +case class ChipletAddressTranslator(val params: ChipletRoutingParams)(implicit p: Parameters) extends LazyModule { + val node = TLAdapterNode(clientFn = c => c, managerFn = m => m) + val offset = p(OffchipAddressRange).map(_.base).min + + assert(offset.bitCount == 1, "Offset must only have 1 bit set") + + override lazy val module = new ChipletAddressTranslatorImpl(this) +} + +class ChipletAddressTranslatorImpl(outer: ChipletAddressTranslator) extends LazyModuleImp(outer) { + val io = IO(new Bundle { + val chip_id = Input(UInt(outer.params.idWidth.W)) + }) + + def trimTag(address: UInt) = { + val topBits = log2Ceil(outer.offset) + val addressWidth = address.getWidth + val tagMatch = address(addressWidth-1, topBits) === io.chip_id + val mask = (1.U(addressWidth.W) << topBits) - 1.U + Mux(tagMatch, address & mask, address) + } + + (outer.node.in.map(_._1) zip outer.node.out.map(_._1)).foreach { case (in, out) => + out.a <> in.a + out.a.bits.address := trimTag(in.a.bits.address) + in.b <> out.b + in.b.bits.address := trimTag(out.b.bits.address) + out.c <> in.c + out.c.bits.address := trimTag(in.c.bits.address) + in.d <> out.d + out.e <> in.e + } +} + +trait CanHaveChipletRouting { this: BaseSubsystem => + val d2d_port_ios = p(ChipletRoutingKey).map { params => + + require(params.ports.nonEmpty, "At least one D2D port must be specified") + + val cbus = locateTLBusWrapper(params.controlBusWhere) + val client_bus = locateTLBusWrapper(params.clientBusWhere) + + val all_freqs = { + Seq(client_bus.dtsFrequency) ++ + params.ports.map { pP => locateTLBusWrapper(pP.managerBusWhere).dtsFrequency } ++ + Seq(cbus.dtsFrequency) ++ + params.ports.flatMap { pP => + pP.controlManagerBusWhere match { + case Some(where) => Some(locateTLBusWrapper(where).dtsFrequency) + case None => None + } + } + } + require(all_freqs.forall(_.isDefined), "All buses must provide a frequency") + require(all_freqs.forall(_ == all_freqs.head), "All buses must have the same frequency") + + val router_domain = LazyModule(new ClockSinkDomain(name=Some("offchip_router"))) + router_domain.clockNode := client_bus.fixedClockNode + + val router = router_domain { LazyModule(new OffchipRouter(params, beatBytes=cbus.beatBytes)) } + + // The bus drives the router's manager node + client_bus.coupleTo(s"offchip_router") { router.node := TLBuffer() := _ } + + router.routing_table_node := cbus.coupleTo(s"offchip_router_mmio") { TLBuffer() := TLFragmenter(cbus) := _ } + + val port_ios = params.ports.zipWithIndex.map { case (pP, id) => + val link_manager_bus = locateTLBusWrapper(pP.managerBusWhere) + + val sys_params = OffchipSubsystemParams( + managerRegion = p(OffchipAddressRange), + clientBeatBytes = client_bus.beatBytes, + clientBlockBytes = client_bus.blockBytes, + managerBeatBytes = link_manager_bus.beatBytes, + managerBlockBytes = link_manager_bus.blockBytes + ) + + val port = router_domain { pP.asInstanceOf[ChipletLinkWrapperInstantiationLike].instantiate(sys_params, id)(p).suggestName(s"d2d${id}_port") } + + val translator = router_domain { + LazyModule(ChipletAddressTranslator(params)) + } + + router_domain { + InModuleBody { + translator.module.io.chip_id := router.module.io.chip_id(id) + } + } + + // Connect PHY clock node and sink debug IO if the port has one (e.g. SerialTL) + port match { + case sertl: testchipip.serdes.SerialTLChipletLink => + val debug_ioSink = BundleBridgeSink[testchipip.serdes.SerdesDebugIO]() + debug_ioSink := sertl.debug_IO + case _ => + } + + val shrinker = router_domain { TLSourceShrinker(1 << 8) } + port.manager_node :*= shrinker := router.node + port.control_manager_node.foreach { node => + cbus.coupleTo(s"${port.name}_control") { node := TLWidthWidget(cbus.beatBytes) := TLBuffer() := _ } + } + link_manager_bus.coupleFrom(s"${port.name}") { _ := TLBuffer() := translator.node :=* port.client_node } + + port.clock_node.foreach(_ := ClockGroup()(p, ValName(s"d2d${id}_clock")) := allClockGroupsNode) + + val port_ioSink = BundleBridgeSink[ChipletIO]() + port_ioSink := port.top_IO + + val outer_io = InModuleBody { + val outer_io = IO(chiselTypeOf(port_ioSink.in(0)._1)).suggestName(s"d2d${id}_port") + outer_io <> port_ioSink.in(0)._1 + outer_io + } + outer_io + + } + port_ios + } +} + diff --git a/src/main/scala/test/Unittests.scala b/src/main/scala/test/Unittests.scala index ea3ce92f..c8b8f3e9 100644 --- a/src/main/scala/test/Unittests.scala +++ b/src/main/scala/test/Unittests.scala @@ -677,6 +677,285 @@ class NetworkXbarTest extends UnitTest { io.finished := finished.reduce(_ && _) } +case class TLRequestDescriptor( + address: BigInt, + isWrite: Boolean, + data: BigInt = 0, + size: Int = 3 // log2(8) = 3 => 8 bytes by default +) + +class OffchipRouterTestDriver(reqs: Seq[TLRequestDescriptor])(implicit p: Parameters) extends LazyModule { + val node = TLClientNode(Seq(TLMasterPortParameters.v1(Seq(TLClientParameters( + name = "offchip_router_test_driver", sourceId = IdRange(0, 1)))))) + + lazy val module = new Impl + class Impl extends LazyModuleImp(this) { + val io = IO(new Bundle { + val start = Input(Bool()) + val finished = Output(Bool()) + }) + + val (tl, edge) = node.out(0) + val nReqs = reqs.size + + val reqIdx = RegInit(0.U(log2Ceil(nReqs + 1).W)) + + val addresses = VecInit(reqs.map(r => r.address.U(edge.bundle.addressBits.W))) + val writes = VecInit(reqs.map(r => r.isWrite.B)) + val datas = VecInit(reqs.map(r => r.data.U(edge.bundle.dataBits.W))) + val sizes = VecInit(reqs.map(r => r.size.U(edge.bundle.sizeBits.W))) + + val (s_idle :: s_req :: s_resp :: s_done :: Nil) = Enum(4) + val state = RegInit(s_idle) + + when (state === s_idle && io.start) { state := s_req } + + // A channel: send requests + val currAddr = addresses(reqIdx) + val currWrite = writes(reqIdx) + val currData = datas(reqIdx) + val currSize = sizes(reqIdx) + + val putBits = edge.Put(0.U, currAddr, currSize, currData)._2 + val getBits = edge.Get(0.U, currAddr, currSize)._2 + + tl.a.valid := state === s_req + tl.a.bits := Mux(currWrite, putBits, getBits) + + // D channel: accept responses + tl.d.ready := state === s_resp + + when (tl.a.fire) { state := s_resp } + when (tl.d.fire) { + reqIdx := reqIdx + 1.U + when (reqIdx === (nReqs - 1).U) { + state := s_done + }.otherwise { + state := s_req + } + } + + tl.b.ready := false.B + tl.c.valid := false.B + tl.c.bits := DontCare + tl.e.valid := false.B + tl.e.bits := DontCare + + io.finished := state === s_done + } +} + +class OffchipRouterTestChecker(portId: Int, expectedReqs: Seq[TLRequestDescriptor])(implicit p: Parameters) extends LazyModule { + val node = TLManagerNode(Seq(TLSlavePortParameters.v1( + managers = Seq(TLSlaveParameters.v1( + address = p(OffchipAddressRange), + supportsGet = TransferSizes(1, 64), + supportsPutFull = TransferSizes(1, 64))), + beatBytes = 8 + ))) + + lazy val module = new Impl + class Impl extends LazyModuleImp(this) { + val io = IO(new Bundle { + val finished = Output(Bool()) + }) + + val (tl, edge) = node.in(0) + val nExpected = expectedReqs.size + + if (nExpected == 0) { + // No requests expected on this port — just tie off and assert nothing arrives + tl.a.ready := false.B + tl.d.valid := false.B + tl.d.bits := DontCare + tl.b.valid := false.B + tl.b.bits := DontCare + tl.c.ready := false.B + tl.e.ready := false.B + assert(!tl.a.valid, s"Checker $portId: unexpected request on port with no expected traffic") + io.finished := true.B + } else { + val reqIdx = RegInit(0.U(log2Ceil(nExpected + 1).W)) + + val expAddrs = VecInit(expectedReqs.map(r => r.address.U(edge.bundle.addressBits.W))) + val expWrites = VecInit(expectedReqs.map(r => r.isWrite.B)) + val expDatas = VecInit(expectedReqs.map(r => r.data.U(edge.bundle.dataBits.W))) + val expSizes = VecInit(expectedReqs.map(r => r.size.U(edge.bundle.sizeBits.W))) + + val (s_wait_req :: s_send_resp :: s_done :: Nil) = Enum(3) + val state = RegInit(s_wait_req) + + // Capture request info for response + val savedOpcode = Reg(chiselTypeOf(tl.a.bits.opcode)) + val savedSize = Reg(chiselTypeOf(tl.a.bits.size)) + val savedSource = Reg(chiselTypeOf(tl.a.bits.source)) + + // A channel: accept and check requests + tl.a.ready := state === s_wait_req + + when (tl.a.fire) { + // Check address + assert(tl.a.bits.address === expAddrs(reqIdx), + s"Checker $portId: address mismatch") + // Check opcode (PutFullData = 0, Get = 4) + when (expWrites(reqIdx)) { + assert(tl.a.bits.opcode === TLMessages.PutFullData, + s"Checker $portId: expected Put") + assert(tl.a.bits.data === expDatas(reqIdx), + s"Checker $portId: write data mismatch") + }.otherwise { + assert(tl.a.bits.opcode === TLMessages.Get, + s"Checker $portId: expected Get") + } + // Check size + assert(tl.a.bits.size === expSizes(reqIdx), + s"Checker $portId: size mismatch") + + savedOpcode := tl.a.bits.opcode + savedSize := tl.a.bits.size + savedSource := tl.a.bits.source + state := s_send_resp + } + + // D channel: send response + tl.d.valid := state === s_send_resp + tl.d.bits := edge.AccessAck(savedSource, savedSize) + // Override for Get responses (need AccessAckData) + when (savedOpcode === TLMessages.Get) { + tl.d.bits := edge.AccessAck(savedSource, savedSize, 0.U) + } + + when (tl.d.fire) { + reqIdx := reqIdx + 1.U + when (reqIdx === (nExpected - 1).U) { + state := s_done + }.otherwise { + state := s_wait_req + } + } + + tl.b.valid := false.B + tl.b.bits := DontCare + tl.c.ready := false.B + tl.e.ready := false.B + + io.finished := state === s_done + } + } +} + +class OffchipRouterTest(val nChips: Int, val nPorts: Int, val reqsPerChip: Int = 4)(implicit p: Parameters) extends LazyModule { + + require(Integer.bitCount(nChips) == 1, "nChips must be a power of 2") + require(nPorts > 0, "must have at least one port") + + val params = new ChipletRoutingParams( + routerParams = OffchipRouterParams(tableAddress = 0x1000L, tableEntries = nChips), + ports = Nil + ) + val beatBytes = 8 + val tableBase = params.routerParams.tableAddress + val tableEntries = params.routerParams.tableEntries + val regsPerEntry = 4 + + val router = LazyModule(new OffchipRouter(params, beatBytes = beatBytes)) + + // Build MMIO request sequence: program chip ID, then program routing table + val rand = new Random() + val chipId = rand.nextInt(nChips) + 1 // this chip's ID (1 to nChips) + // All other chip IDs (1 to nChips, excluding this chip), each mapped to a random port + val otherChipIds = (1 to nChips).filter(_ != chipId) + val tableData = otherChipIds.map { cid => (cid, rand.nextInt(nPorts)) } // (chipID, port) + + val mmioReqs: Seq[TLRequestDescriptor] = { + // Each regmap field is beat-aligned (offset * beatBytes), so full-word writes work + + // Program chip ID register + val chipIdAddr = tableBase + (tableEntries * regsPerEntry) * beatBytes + val progChipId = Seq(TLRequestDescriptor(chipIdAddr, isWrite = true, data = chipId)) + + // Program routing table entries (valid, chipID, port for each remote chip) + val progTable = tableData.zipWithIndex.flatMap { case ((cid, port), idx) => + val baseAddr = tableBase + (idx * regsPerEntry) * beatBytes + Seq( + TLRequestDescriptor(baseAddr + 0 * beatBytes, isWrite = true, data = 1), // valid + TLRequestDescriptor(baseAddr + 1 * beatBytes, isWrite = true, data = cid), // chipID + TLRequestDescriptor(baseAddr + 2 * beatBytes, isWrite = true, data = port), // port + ) + } + + progChipId ++ progTable + } + + // Build data request sequence: reqsPerChip random accesses to each remote chip, shuffled + val dataReqs: Seq[TLRequestDescriptor] = { + val offset = p(OffchipAddressRange).map(_.base).min + val offsetBits = log2Ceil(offset) + val reqs = otherChipIds.flatMap { cid => + Seq.fill(reqsPerChip) { + val baseAddr = BigInt(cid) << offsetBits + val addr = baseAddr + (BigInt(offsetBits, rand) & ~BigInt(7)) // align to 8 bytes + TLRequestDescriptor(addr, isWrite = rand.nextBoolean(), data = BigInt(64, rand)) + } + } + rand.shuffle(reqs) + } + + // Build chipID -> port lookup from tableData + val chipIdToPort: Map[Int, Int] = tableData.map { case (cid, port) => cid -> port }.toMap + + // Determine expected port for each dataReq based on its address tag + val offset = p(OffchipAddressRange).map(_.base).min + val offsetBits = log2Ceil(offset) + + def reqPort(r: TLRequestDescriptor): Int = { + val addrChipId = (r.address >> offsetBits).toInt + chipIdToPort(addrChipId) + } + + // Partition dataReqs by port, preserving order within each port + val reqsByPort: Seq[Seq[TLRequestDescriptor]] = Seq.tabulate(nPorts) { port => + dataReqs.filter(r => reqPort(r) == port) + } + + val mmio_driver = LazyModule(new OffchipRouterTestDriver(mmioReqs)) + val driver = LazyModule(new OffchipRouterTestDriver(dataReqs)) + + val checkers = Seq.tabulate(nPorts) { id => + LazyModule(new OffchipRouterTestChecker(id, reqsByPort(id))) + } + + router.node := TLBuffer() := driver.node + router.routing_table_node := TLBuffer() := mmio_driver.node + checkers.foreach(_.node := TLBuffer() := router.node) + + lazy val module = new Impl + class Impl extends LazyModuleImp(this) { + val io = IO(new Bundle { + val finished = Output(Bool()) + }) + + // Phase 1: MMIO driver programs chip ID + routing table + val mmioFinished = mmio_driver.module.io.finished + mmio_driver.module.io.start := true.B + + // Verify chip ID was programmed correctly + when (mmioFinished) { + assert(router.module.io.chip_id(0) === chipId.U, "Chip ID mismatch after programming") + } + + // Phase 2: Data driver sends requests through the router + driver.module.io.start := mmioFinished + io.finished := driver.module.io.finished + + } +} + +class OffchipRouterTestWrapper(nChips: Int, nPorts: Int, reqsPerChip: Int = 4, timeout: Int = 8192)(implicit p: Parameters) extends UnitTest(timeout) { + val test = Module(LazyModule(new OffchipRouterTest(nChips, nPorts, reqsPerChip)).module) + io.finished := test.io.finished +} + object TestChipUnitTests { def apply(implicit p: Parameters): Seq[UnitTest] = Seq( @@ -688,6 +967,8 @@ object TestChipUnitTests { Module(new StreamWidthAdapterTest), Module(new NetworkXbarTest), Module(new TLRingNetworkTestWrapper), - Module(new TLCTCTestWrapper(CreditedSourceSyncSerialPhyParams(flitWidth = 32, phitWidth = 4), 20000)) + Module(new TLCTCTestWrapper(CreditedSourceSyncSerialPhyParams(flitWidth = 32, phitWidth = 4), 20000)), + Module(new OffchipRouterTestWrapper(nChips = 4, nPorts = 2)), + Module(new OffchipRouterTestWrapper(nChips = 8, nPorts = 3, reqsPerChip = 8)) ) }