feat(bus): Add TileLink-UL primitives
This commit introduces a collection of primitive modules for building
TileLink-UL interconnects, including FIFOs, sockets, and a width
bridge.
The new modules are:
- TlulFifoSync: A synchronous TileLink FIFO with optional spare
side-channels.
- TlulFifoAsync: An asynchronous TileLink FIFO for clock domain
crossing, built on the rocket-chip AsyncQueue.
- TlulSocket1N: A 1-to-N socket for steering requests from a single
host to one of N devices.
- TlulSocketM1: An M-to-1 socket that arbitrates requests from M
hosts to a single device using a round-robin arbiter.
- TlulWidthBridge: A bridge for connecting TileLink-UL buses of
different widths.
Each of these modules is accompanied by a comprehensive cocotb test
suite to ensure its correctness.
Change-Id: I2ca34caad9332b0621a68957c043a91deee45999
diff --git a/hdl/chisel/src/bus/BUILD b/hdl/chisel/src/bus/BUILD
index 3e2be71..d793ef6 100644
--- a/hdl/chisel/src/bus/BUILD
+++ b/hdl/chisel/src/bus/BUILD
@@ -37,12 +37,18 @@
"KelvinToTlul.scala",
"SecdedEncoderTestbench.scala",
"TileLinkUL.scala",
+ "TlulFifoAsync.scala",
+ "TlulFifoSync.scala",
"TlulIntegrity.scala",
"TlulIntegrityTestbench.scala",
+ "TlulSocket1N.scala",
+ "TlulSocketM1.scala",
+ "TlulWidthBridge.scala",
],
deps = [
- "//hdl/chisel/src/kelvin:kelvin_params",
"//hdl/chisel/src/common",
+ "//hdl/chisel/src/kelvin:kelvin_params",
+ "@chipsalliance_rocket_chip//:asyncqueue",
],
)
@@ -60,6 +66,65 @@
module_name = "TLUL2Axi",
)
+chisel_cc_library(
+ name = "tlul_fifo_async_128_cc_library",
+ chisel_lib = ":bus",
+ emit_class = "bus.TlulFifoAsync128Emitter",
+ module_name = "TlulFifoAsync128",
+)
+
+chisel_cc_library(
+ name = "tlul_socket_1n_128_cc_library",
+ chisel_lib = ":bus",
+ emit_class = "bus.TlulSocket1N_128Emitter",
+ module_name = "TlulSocket1N_128",
+)
+
+chisel_cc_library(
+ name = "tlul_fifo_sync_cc_library",
+ chisel_lib = ":bus",
+ emit_class = "bus.TlulFifoSyncEmitter",
+ module_name = "TlulFifoSync",
+)
+
+chisel_cc_library(
+ name = "tlul_socket_m1_2_128_cc_library",
+ chisel_lib = ":bus",
+ emit_class = "bus.TlulSocketM1_2_128Emitter",
+ module_name = "TlulSocketM1_2_128",
+)
+
+chisel_cc_library(
+ name = "tlul_socket_m1_3_128_cc_library",
+ chisel_lib = ":bus",
+ emit_class = "bus.TlulSocketM1_3_128Emitter",
+ module_name = "TlulSocketM1_3_128",
+)
+
+verilator_cocotb_model(
+ name = "tlul_socket_m1_2_128_model",
+ cflags = VERILATOR_BUILD_ARGS,
+ hdl_toplevel = "TlulSocketM1_2_128",
+ trace = True,
+ verilog_source = "//hdl/chisel/src/bus:TlulSocketM1_2_128.sv",
+)
+
+verilator_cocotb_model(
+ name = "tlul_fifo_async_128_model",
+ cflags = VERILATOR_BUILD_ARGS,
+ hdl_toplevel = "TlulFifoAsync128",
+ trace = True,
+ verilog_source = "//hdl/chisel/src/bus:TlulFifoAsync128.sv",
+)
+
+verilator_cocotb_model(
+ name = "tlul_fifo_sync_model",
+ cflags = VERILATOR_BUILD_ARGS,
+ hdl_toplevel = "TlulFifoSync",
+ trace = True,
+ verilog_source = "//hdl/chisel/src/bus:TlulFifoSync.sv",
+)
+
verilator_cocotb_model(
name = "axi2tlul_model",
cflags = VERILATOR_BUILD_ARGS,
@@ -91,6 +156,14 @@
verilog_source = "//hdl/chisel/src/bus:TlulIntegrityTestbench.sv",
)
+verilator_cocotb_model(
+ name = "tlul_socket_1n_128_model",
+ cflags = VERILATOR_BUILD_ARGS,
+ hdl_toplevel = "TlulSocket1N_128",
+ trace = True,
+ verilog_source = "//hdl/chisel/src/bus:TlulSocket1N_128.sv",
+)
+
chisel_cc_library(
name = "secded_encoder_testbench_cc_library",
chisel_lib = ":bus",
diff --git a/hdl/chisel/src/bus/TlulFifoAsync.scala b/hdl/chisel/src/bus/TlulFifoAsync.scala
new file mode 100644
index 0000000..98ad167
--- /dev/null
+++ b/hdl/chisel/src/bus/TlulFifoAsync.scala
@@ -0,0 +1,62 @@
+package bus
+
+import chisel3._
+import freechips.rocketchip.util.{AsyncQueue, AsyncQueueParams}
+import kelvin.Parameters
+
+class TlulFifoAsync(
+ p: TLULParameters,
+ reqDepth: Int = 4,
+ rspDepth: Int = 4,
+ moduleName: String = "TlulFifoAsync"
+) extends RawModule {
+ override val desiredName = moduleName
+
+ val io = IO(new Bundle {
+ val clk_h_i = Input(Clock())
+ val rst_h_i = Input(Bool())
+ val clk_d_i = Input(Clock())
+ val rst_d_i = Input(Bool())
+ val tl_h = Flipped(new OpenTitanTileLink.Host2Device(p))
+ val tl_d = new OpenTitanTileLink.Host2Device(p)
+ })
+
+ val req_queue = Module(new AsyncQueue(new OpenTitanTileLink.A_Channel(p), AsyncQueueParams(depth = reqDepth)))
+ req_queue.io.enq_clock := io.clk_h_i
+ req_queue.io.enq_reset := io.rst_h_i
+ req_queue.io.deq_clock := io.clk_d_i
+ req_queue.io.deq_reset := io.rst_d_i
+ req_queue.io.enq <> io.tl_h.a
+ io.tl_d.a <> req_queue.io.deq
+
+ val rsp_queue = Module(new AsyncQueue(new OpenTitanTileLink.D_Channel(p), AsyncQueueParams(depth = rspDepth)))
+ rsp_queue.io.enq_clock := io.clk_d_i
+ rsp_queue.io.enq_reset := io.rst_d_i
+ rsp_queue.io.deq_clock := io.clk_h_i
+ rsp_queue.io.deq_reset := io.rst_h_i
+ rsp_queue.io.enq <> io.tl_d.d
+ io.tl_h.d <> rsp_queue.io.deq
+}
+
+import _root_.circt.stage.{ChiselStage, FirtoolOption}
+import chisel3.stage.ChiselGeneratorAnnotation
+import scala.annotation.nowarn
+
+@nowarn
+object TlulFifoAsync128Emitter extends App {
+ val p = new Parameters
+ p.lsuDataBits = 128
+ (new ChiselStage).execute(
+ Array("--target", "systemverilog") ++ args,
+ Seq(
+ ChiselGeneratorAnnotation(() =>
+ new TlulFifoAsync(
+ p = new bus.TLULParameters(p),
+ reqDepth = 1,
+ rspDepth = 1,
+ moduleName = "TlulFifoAsync128"
+ )
+ )
+ ) ++ Seq(FirtoolOption("-enable-layers=Verification"))
+ )
+}
diff --git a/hdl/chisel/src/bus/TlulFifoSync.scala b/hdl/chisel/src/bus/TlulFifoSync.scala
new file mode 100644
index 0000000..92b45ba
--- /dev/null
+++ b/hdl/chisel/src/bus/TlulFifoSync.scala
@@ -0,0 +1,121 @@
+package bus
+
+import chisel3._
+import chisel3.util._
+
+class TlulFifoSync(
+ p: TLULParameters,
+ reqDepth: Int = 2,
+ rspDepth: Int = 2,
+ reqPass: Boolean = true, // Equivalent to flow=true in Queue
+ rspPass: Boolean = true, // Equivalent to flow=true in Queue
+ spareReqW: Int = 1,
+ spareRspW: Int = 1,
+ moduleName: String = "TlulFifoSync"
+) extends Module {
+ require(reqDepth > 0 || reqPass, "reqDepth cannot be 0 if reqPass is false")
+ require(rspDepth > 0 || rspPass, "rspDepth cannot be 0 if rspPass is false")
+
+ override val desiredName = moduleName
+ val io = IO(new Bundle {
+ // Host-facing interface
+ val host = Flipped(new OpenTitanTileLink.Host2Device(p))
+
+ // Device-facing interface
+ val device = new OpenTitanTileLink.Host2Device(p)
+
+ // Spare side channels
+ val spare_req_i = Input(UInt(spareReqW.W))
+ val spare_req_o = Output(UInt(spareReqW.W))
+ val spare_rsp_i = Input(UInt(spareRspW.W))
+ val spare_rsp_o = Output(UInt(spareRspW.W))
+ })
+
+ // A bundle to hold the TileLink A channel data plus the spare bits
+ class AChannelWithSpare extends Bundle {
+ val a = new OpenTitanTileLink.A_Channel(p)
+ val spare = UInt(spareReqW.W)
+ }
+
+ // A bundle to hold the TileLink D channel data plus the spare bits
+ class DChannelWithSpare extends Bundle {
+ val d = new OpenTitanTileLink.D_Channel(p)
+ val spare = UInt(spareRspW.W)
+ }
+
+ // Request FIFO (Host to Device)
+ if (reqDepth > 0) {
+ val reqFifo = Module(new Queue(new AChannelWithSpare, reqDepth, flow = reqPass))
+ reqFifo.io.enq.valid := io.host.a.valid
+ io.host.a.ready := reqFifo.io.enq.ready
+ reqFifo.io.enq.bits.a := io.host.a.bits
+ reqFifo.io.enq.bits.spare := io.spare_req_i
+
+ io.device.a.valid := reqFifo.io.deq.valid
+ reqFifo.io.deq.ready := io.device.a.ready
+ io.device.a.bits := reqFifo.io.deq.bits.a
+ io.spare_req_o := reqFifo.io.deq.bits.spare
+ } else {
+ io.device.a.valid := io.host.a.valid
+ io.host.a.ready := io.device.a.ready
+ io.device.a.bits := io.host.a.bits
+ io.spare_req_o := io.spare_req_i
+ }
+
+ // Response FIFO (Device to Host)
+ val device_d_bits_sanitized = Wire(chiselTypeOf(io.device.d.bits))
+ device_d_bits_sanitized := io.device.d.bits
+ device_d_bits_sanitized.data := Mux(
+ io.device.d.bits.opcode === TLULOpcodesD.AccessAckData.asUInt,
+ io.device.d.bits.data,
+ 0.U
+ )
+
+ if (rspDepth > 0) {
+ val rspFifo =
+ Module(new Queue(new DChannelWithSpare, rspDepth, flow = rspPass))
+ rspFifo.io.enq.valid := io.device.d.valid
+ io.device.d.ready := rspFifo.io.enq.ready
+ rspFifo.io.enq.bits.d := device_d_bits_sanitized
+ rspFifo.io.enq.bits.spare := io.spare_rsp_i
+
+ io.host.d.valid := rspFifo.io.deq.valid
+ rspFifo.io.deq.ready := io.host.d.ready
+ io.host.d.bits := rspFifo.io.deq.bits.d
+ io.spare_rsp_o := rspFifo.io.deq.bits.spare
+ } else {
+ io.host.d.valid := io.device.d.valid
+ io.device.d.ready := io.host.d.ready
+ io.host.d.bits := device_d_bits_sanitized
+ io.spare_rsp_o := io.spare_rsp_i
+ }
+}
+
+import _root_.circt.stage.{ChiselStage, FirtoolOption}
+import chisel3.stage.ChiselGeneratorAnnotation
+import scala.annotation.nowarn
+
+@nowarn
+object TlulFifoSyncEmitter extends App {
+ val p = new kelvin.Parameters
+ (new ChiselStage).execute(
+ Array("--target", "systemverilog") ++ args,
+ Seq(ChiselGeneratorAnnotation(() => new TlulFifoSync(new bus.TLULParameters(p)))) ++ Seq(FirtoolOption("-enable-layers=Verification"))
+ )
+}
+
+@nowarn
+object EmitTlulFifoSyncDepth0 extends App {
+ val p = new kelvin.Parameters
+ p.lsuDataBits = 128
+ (new ChiselStage).execute(
+ Array("--target", "systemverilog") ++ args,
+ Seq(ChiselGeneratorAnnotation(() => new TlulFifoSync(
+ p = new bus.TLULParameters(p),
+ reqDepth = 0,
+ rspDepth = 0,
+ spareReqW = 4,
+ moduleName = "TlulFifoSync_Depth0"
+ ))) ++ Seq(FirtoolOption("-enable-layers=Verification"))
+ )
+}
diff --git a/hdl/chisel/src/bus/TlulSocket1N.scala b/hdl/chisel/src/bus/TlulSocket1N.scala
new file mode 100644
index 0000000..046b3fd
--- /dev/null
+++ b/hdl/chisel/src/bus/TlulSocket1N.scala
@@ -0,0 +1,219 @@
+package bus
+
+import chisel3._
+import chisel3.util._
+import common.MakeInvalid
+import kelvin.Parameters
+
+// A simple error responder that immediately generates an error response
+// for any incoming request.
+class TlulErrorResponder(p: TLULParameters) extends Module {
+ val io = IO(new Bundle {
+ val tl_h = Flipped(new OpenTitanTileLink.Host2Device(p))
+ })
+
+ io.tl_h.a.ready := true.B
+
+ val d = RegInit(MakeInvalid(new OpenTitanTileLink.D_Channel(p)))
+
+ d.valid := io.tl_h.a.fire
+ d.bits.size := Mux(io.tl_h.a.fire, io.tl_h.a.bits.size, d.bits.size)
+ d.bits.source := Mux(io.tl_h.a.fire, io.tl_h.a.bits.source, d.bits.source)
+ d.bits.opcode := TLULOpcodesD.AccessAck.asUInt
+ d.bits.param := 0.U
+ d.bits.sink := 0.U
+ d.bits.data := 0.U
+ d.bits.error := true.B
+ d.bits.user.rsp_intg := 0.U
+ d.bits.user.data_intg := 0.U
+
+ io.tl_h.d.valid := d.valid
+ io.tl_h.d.bits := d.bits
+}
+
+class TlulSocket1N(
+ p: TLULParameters,
+ N: Int = 4,
+ HReqPass: Boolean = true,
+ HRspPass: Boolean = true,
+ DReqPass: Seq[Boolean] = Nil,
+ DRspPass: Seq[Boolean] = Nil,
+ HReqDepth: Int = 1,
+ HRspDepth: Int = 1,
+ DReqDepth: Seq[Int] = Nil,
+ DRspDepth: Seq[Int] = Nil,
+ ExplicitErrs: Boolean = true,
+ moduleName: String = "TlulSocket1N"
+) extends Module {
+ val DReqPass_ = if (DReqPass.isEmpty) Seq.fill(N)(true) else DReqPass
+ val DRspPass_ = if (DRspPass.isEmpty) Seq.fill(N)(true) else DRspPass
+ val DReqDepth_ = if (DReqDepth.isEmpty) Seq.fill(N)(1) else DReqDepth
+ val DRspDepth_ = if (DRspDepth.isEmpty) Seq.fill(N)(1) else DRspDepth
+ override val desiredName = moduleName
+ val NWD = if (ExplicitErrs) log2Ceil(N + 1) else log2Ceil(N)
+
+ val io = IO(new Bundle {
+ val tl_h = Flipped(new OpenTitanTileLink.Host2Device(p))
+ val tl_d = Vec(N, new OpenTitanTileLink.Host2Device(p))
+ val dev_select_i = Input(UInt(NWD.W))
+ })
+
+ // Host-side FIFO
+ val fifo_h = Module(
+ new TlulFifoSync(
+ p,
+ reqDepth = HReqDepth,
+ rspDepth = HRspDepth,
+ reqPass = HReqPass,
+ rspPass = HRspPass,
+ spareReqW = NWD
+ )
+ )
+
+ fifo_h.io.host <> io.tl_h
+ fifo_h.io.spare_req_i := io.dev_select_i
+ fifo_h.io.spare_rsp_i := 0.U // Tie off unused spare port
+ val dev_select_t = fifo_h.io.spare_req_o
+
+ // Outstanding request tracking
+ val maxOutstanding = 1 << p.o
+ val outstandingW = log2Ceil(maxOutstanding + 1)
+ val num_req_outstanding = RegInit(0.U(outstandingW.W))
+ val dev_select_outstanding = RegInit(0.U(NWD.W))
+ val accept_t_req = fifo_h.io.device.a.fire
+ val accept_t_rsp = fifo_h.io.device.d.fire
+
+ when(accept_t_req) {
+ dev_select_outstanding := dev_select_t
+ when(!accept_t_rsp) {
+ num_req_outstanding := num_req_outstanding + 1.U
+ }
+ }.elsewhen(accept_t_rsp) {
+ num_req_outstanding := num_req_outstanding - 1.U
+ }
+
+ val hold_all_requests =
+ (num_req_outstanding =/= 0.U) && (dev_select_t =/= dev_select_outstanding)
+
+ // Device-side FIFOs and steering logic
+ val tl_u_o = Wire(Vec(N + 1, new OpenTitanTileLink.Host2Device(p)))
+ val tl_u_i = Wire(Vec(N + 1, new OpenTitanTileLink.Host2Device(p)))
+
+ val blanked_auser = Wire(new OpenTitanTileLink_A_User)
+ blanked_auser.rsvd := fifo_h.io.device.a.bits.user.rsvd
+ blanked_auser.instr_type := fifo_h.io.device.a.bits.user.instr_type
+ blanked_auser.cmd_intg := 0.U // Simplified for now
+ blanked_auser.data_intg := 0.U // Simplified for now
+
+ for (i <- 0 until N) {
+ val dev_select = (dev_select_t === i.U) && !hold_all_requests
+
+ tl_u_o(i).a.valid := fifo_h.io.device.a.valid && dev_select
+ tl_u_o(i).a.bits := fifo_h.io.device.a.bits
+ tl_u_o(i).a.bits.user := Mux(
+ dev_select,
+ fifo_h.io.device.a.bits.user,
+ blanked_auser
+ )
+ tl_u_o(i).d.ready := fifo_h.io.device.d.ready
+
+ val fifo_d = Module(
+ new TlulFifoSync(
+ p,
+ reqDepth = DReqDepth_(i),
+ rspDepth = DRspDepth_(i),
+ reqPass = DReqPass_(i),
+ rspPass = DRspPass_(i)
+ )
+ )
+ fifo_d.io.host.a <> tl_u_o(i).a
+ io.tl_d(i).a <> fifo_d.io.device.a
+ tl_u_i(i).a := fifo_d.io.device.a
+
+ tl_u_o(i).d <> fifo_d.io.host.d
+ io.tl_d(i).d <> fifo_d.io.device.d
+ tl_u_i(i).d := fifo_d.io.device.d
+
+ fifo_d.io.spare_req_i := 0.U
+ fifo_d.io.spare_rsp_i := 0.U
+ }
+
+ // Error responder instantiation
+ if (ExplicitErrs && (1 << NWD) > N) {
+ val err_resp = Module(new TlulErrorResponder(p))
+ tl_u_o(N).a.valid := fifo_h.io.device.a.valid && (dev_select_t >= N.U) && !hold_all_requests
+ tl_u_o(N).a.bits := fifo_h.io.device.a.bits
+ tl_u_o(N).d.ready := fifo_h.io.device.d.ready
+ err_resp.io.tl_h.a <> tl_u_o(N).a
+ tl_u_o(N).d <> err_resp.io.tl_h.d
+
+ tl_u_i(N).a.ready := err_resp.io.tl_h.a.ready
+ tl_u_i(N).d <> err_resp.io.tl_h.d
+ tl_u_i(N).d.ready := true.B
+
+ // Tie off unused outputs of the wire to prevent "not fully initialized" errors
+ tl_u_i(N).a.valid := false.B
+ tl_u_i(N).a.bits := 0.U.asTypeOf(new OpenTitanTileLink.A_Channel(p))
+ } else {
+ tl_u_o(N).a.valid := false.B
+ tl_u_o(N).a.bits := DontCare
+ tl_u_o(N).d.ready := false.B
+ tl_u_i(N).a.ready := false.B
+ tl_u_i(N).d.valid := false.B
+ tl_u_i(N).d.bits := DontCare
+ tl_u_i(N).d.ready := false.B
+ }
+
+ // Response path selection
+ val hfifo_reqready = Mux(
+ hold_all_requests,
+ false.B,
+ MuxCase(
+ // Default to error responder ready if it exists
+ if (ExplicitErrs && (1 << NWD) > N) tl_u_o(N).a.ready else true.B,
+ (0 until N).map(i => (dev_select_t === i.U) -> tl_u_o(i).a.ready)
+ )
+ )
+ fifo_h.io.device.a.ready := fifo_h.io.device.a.valid && hfifo_reqready
+
+ val tl_t_p = MuxCase(
+ // Default to error responder if it exists
+ tl_u_i(N).d.bits,
+ (0 until N).map(i =>
+ (dev_select_outstanding === i.U) -> tl_u_i(i).d.bits
+ )
+ )
+ val d_valid = MuxCase(
+ tl_u_i(N).d.valid,
+ (0 until N).map(i => (dev_select_outstanding === i.U) -> tl_u_i(i).d.valid)
+ )
+
+ fifo_h.io.device.d.valid := d_valid
+ fifo_h.io.device.d.bits := tl_t_p
+}
+
+import _root_.circt.stage.{ChiselStage, FirtoolOption}
+import chisel3.stage.ChiselGeneratorAnnotation
+import scala.annotation.nowarn
+
+@nowarn
+object TlulSocket1N_128Emitter extends App {
+ val p = new Parameters
+ p.lsuDataBits = 128
+ (new ChiselStage).execute(
+ Array("--target", "systemverilog") ++ args,
+ Seq(
+ ChiselGeneratorAnnotation(() =>
+ new TlulSocket1N(
+ p = new bus.TLULParameters(p),
+ N = 4, // Default value, will be overridden at instantiation
+ DReqPass = Seq.fill(4)(true),
+ DRspPass = Seq.fill(4)(true),
+ DReqDepth = Seq.fill(4)(1),
+ DRspDepth = Seq.fill(4)(1),
+ moduleName = "TlulSocket1N_128"
+ )
+ )
+ ) ++ Seq(FirtoolOption("-enable-layers=Verification"))
+ )
+}
diff --git a/hdl/chisel/src/bus/TlulSocketM1.scala b/hdl/chisel/src/bus/TlulSocketM1.scala
new file mode 100644
index 0000000..a00789d
--- /dev/null
+++ b/hdl/chisel/src/bus/TlulSocketM1.scala
@@ -0,0 +1,155 @@
+package bus
+
+import chisel3._
+import chisel3.util._
+import common.KelvinRRArbiter
+import kelvin.Parameters
+
+
+class TlulFifoSync_(p: TLULParameters,
+ reqDepth: Int,
+ rspDepth: Int,
+ reqPass: Boolean,
+ rspPass: Boolean,
+ socketName: String)
+ extends TlulFifoSync(p, reqDepth, rspDepth, reqPass, rspPass) {
+ override val desiredName = s"${socketName}_TlulFifoSync_d${reqDepth}r${rspDepth}"
+}
+
+
+
+class TlulSocketM1(
+ p: TLULParameters,
+ M: Int = 4,
+ HReqPass: Seq[Boolean] = Nil,
+ HRspPass: Seq[Boolean] = Nil,
+ HReqDepth: Seq[Int] = Nil,
+ HRspDepth: Seq[Int] = Nil,
+ DReqPass: Boolean = true,
+ DRspPass: Boolean = true,
+ DReqDepth: Int = 1,
+ DRspDepth: Int = 1,
+ moduleName: String = "TlulSocketM1"
+) extends Module {
+ val HReqPass_ = if (HReqPass.isEmpty) Seq.fill(M)(true) else HReqPass
+ val HRspPass_ = if (HRspPass.isEmpty) Seq.fill(M)(true) else HRspPass
+ val HReqDepth_ = if (HReqDepth.isEmpty) Seq.fill(M)(1) else HReqDepth
+ val HRspDepth_ = if (HRspDepth.isEmpty) Seq.fill(M)(1) else HRspDepth
+ override val desiredName = moduleName
+ val StIdW = log2Ceil(M)
+
+ val io = IO(new Bundle {
+ val tl_h = Flipped(Vec(M, new OpenTitanTileLink.Host2Device(p)))
+ val tl_d = new OpenTitanTileLink.Host2Device(p)
+ })
+
+ // Host-side FIFOs
+ val hreq_fifo_o = Wire(Vec(M, Decoupled(new OpenTitanTileLink.A_Channel(p))))
+ val hrsp_fifo_i = Wire(Vec(M, Flipped(Decoupled(new OpenTitanTileLink.D_Channel(p)))))
+
+ for (i <- 0 until M) {
+ val hreq_fifo_i = Wire(new OpenTitanTileLink.A_Channel(p))
+ hreq_fifo_i := io.tl_h(i).a.bits
+ hreq_fifo_i.source := Cat(io.tl_h(i).a.bits.source, i.U(StIdW.W))
+
+ val fifo = Module(new TlulFifoSync_(
+ p,
+ reqDepth = HReqDepth_(i),
+ rspDepth = HRspDepth_(i),
+ reqPass = HReqPass_(i),
+ rspPass = HRspPass_(i),
+ socketName = moduleName
+ ))
+ fifo.io.host.a.valid := io.tl_h(i).a.valid
+ fifo.io.host.a.bits := hreq_fifo_i
+ io.tl_h(i).a.ready := fifo.io.host.a.ready
+
+ hreq_fifo_o(i) <> fifo.io.device.a
+
+ io.tl_h(i).d <> fifo.io.host.d
+ fifo.io.device.d <> hrsp_fifo_i(i)
+
+ fifo.io.spare_req_i := 0.U
+ fifo.io.spare_rsp_i := 0.U
+ }
+
+ // Arbiter
+ val arb = Module(new KelvinRRArbiter(new OpenTitanTileLink.A_Channel(p), M, moduleName = Some(s"${moduleName}_KelvinRRArbiter_${M}")))
+ for (i <- 0 until M) {
+ arb.io.in(i) <> hreq_fifo_o(i)
+ }
+
+ // Device-side FIFO
+ val dfifo = Module(new TlulFifoSync_(
+ p,
+ reqDepth = DReqDepth,
+ rspDepth = DRspDepth,
+ reqPass = DReqPass,
+ rspPass = DRspPass,
+ socketName = moduleName
+ ))
+
+ dfifo.io.host.a <> arb.io.out
+ io.tl_d.a <> dfifo.io.device.a
+ dfifo.io.device.d <> io.tl_d.d
+ dfifo.io.spare_req_i := 0.U
+ dfifo.io.spare_rsp_i := 0.U
+
+ // Response steering
+ val rsp_arb_grant = Mux(io.tl_d.d.valid, UIntToOH(io.tl_d.d.bits.source(StIdW - 1, 0)), 0.U(M.W))
+ for (i <- 0 until M) {
+ hrsp_fifo_i(i).valid := io.tl_d.d.valid && rsp_arb_grant(i)
+ hrsp_fifo_i(i).bits := io.tl_d.d.bits
+ hrsp_fifo_i(i).bits.source := io.tl_d.d.bits.source >> StIdW
+ }
+ io.tl_d.d.ready := (VecInit(hrsp_fifo_i.map(_.ready)).asUInt & rsp_arb_grant).orR
+ dfifo.io.host.d.ready := (VecInit(hrsp_fifo_i.map(_.ready)).asUInt & rsp_arb_grant).orR
+}
+
+import _root_.circt.stage.{ChiselStage, FirtoolOption}
+import chisel3.stage.ChiselGeneratorAnnotation
+import scala.annotation.nowarn
+
+@nowarn
+object TlulSocketM1_2_128Emitter extends App {
+ val p = new Parameters
+ p.lsuDataBits = 128
+ (new ChiselStage).execute(
+ Array("--target", "systemverilog") ++ args,
+ Seq(
+ ChiselGeneratorAnnotation(() =>
+ new TlulSocketM1(
+ p = new bus.TLULParameters(p),
+ M = 2,
+ HReqDepth = Seq.fill(2)(0),
+ HRspDepth = Seq.fill(2)(0),
+ DReqDepth = 0,
+ DRspDepth = 0,
+ moduleName = "TlulSocketM1_2_128"
+ )
+ )
+ ) ++ Seq(FirtoolOption("-enable-layers=Verification"))
+ )
+}
+
+@nowarn
+object TlulSocketM1_3_128Emitter extends App {
+ val p = new Parameters
+ p.lsuDataBits = 128
+ (new ChiselStage).execute(
+ Array("--target", "systemverilog") ++ args,
+ Seq(
+ ChiselGeneratorAnnotation(() =>
+ new TlulSocketM1(
+ p = new bus.TLULParameters(p),
+ M = 3,
+ HReqDepth = Seq.fill(3)(0),
+ HRspDepth = Seq.fill(3)(0),
+ DReqDepth = 0,
+ DRspDepth = 0,
+ moduleName = "TlulSocketM1_3_128"
+ )
+ )
+ ) ++ Seq(FirtoolOption("-enable-layers=Verification"))
+ )
+}
diff --git a/hdl/chisel/src/bus/TlulWidthBridge.scala b/hdl/chisel/src/bus/TlulWidthBridge.scala
new file mode 100644
index 0000000..4feecee
--- /dev/null
+++ b/hdl/chisel/src/bus/TlulWidthBridge.scala
@@ -0,0 +1,218 @@
+package bus
+
+import chisel3._
+import chisel3.util._
+import common.FifoX
+
+class TlulWidthBridge(val host_p: TLULParameters, val device_p: TLULParameters) extends RawModule {
+ val io = IO(new Bundle {
+ val clk_i = Input(Clock())
+ val rst_ni = Input(Reset())
+
+ val tl_h = Flipped(new OpenTitanTileLink.Host2Device(host_p))
+ val tl_d = new OpenTitanTileLink.Host2Device(device_p)
+
+ val fault_a_o = Output(Bool())
+ val fault_d_o = Output(Bool())
+ })
+
+ withClockAndReset(io.clk_i, !io.rst_ni.asBool) {
+ // ==========================================================================
+ // Parameters and Constants
+ // ==========================================================================
+ val hostWidth = host_p.w * 8
+ val deviceWidth = device_p.w * 8
+
+ // Default fault outputs
+ io.fault_a_o := false.B
+ io.fault_d_o := false.B
+
+ // ==========================================================================
+ // Wide to Narrow Path (e.g., 128-bit host to 32-bit device)
+ // ==========================================================================
+ if (hostWidth > deviceWidth) {
+ val ratio = hostWidth / deviceWidth
+ val narrowBytes = deviceWidth / 8
+ val hostBytes = hostWidth / 8
+
+ // ------------------------------------------------------------------------
+ // Response Path (D Channel): Assemble narrow responses into a wide one
+ // ------------------------------------------------------------------------
+ val d_data_reg = RegInit(VecInit(Seq.fill(ratio)(0.U(deviceWidth.W))))
+ val d_resp_reg = RegInit(0.U.asTypeOf(new OpenTitanTileLink.D_Channel(host_p)))
+ val d_valid_reg = RegInit(false.B)
+ val beat_count = RegInit(0.U(log2Ceil(ratio+1).W))
+ val d_fault_reg = RegInit(false.B)
+
+ val d_check = Module(new ResponseIntegrityCheck(device_p))
+ d_check.io.d_i := io.tl_d.d.bits
+ io.fault_d_o := d_fault_reg
+
+ val d_gen = Module(new ResponseIntegrityGen(host_p))
+ val wide_resp = Wire(new OpenTitanTileLink.D_Channel(host_p))
+ wide_resp := d_resp_reg
+
+ val req_info_q = Module(new Queue(new Bundle {
+ val source = UInt(host_p.o.W)
+ val beats = UInt(log2Ceil(ratio+1).W)
+ val offset = UInt(log2Ceil(hostBytes).W)
+ val size = UInt(host_p.z.W)
+ }, 2))
+
+ wide_resp.source := req_info_q.io.deq.bits.source
+ wide_resp.size := req_info_q.io.deq.bits.size
+ d_gen.io.d_i := wide_resp
+
+ io.tl_d.d.ready := !d_valid_reg
+ io.tl_h.d.valid := d_valid_reg
+ io.tl_h.d.bits := d_gen.io.d_o
+ io.tl_h.d.bits.data := (d_data_reg.asUInt >> (req_info_q.io.deq.bits.offset << 3.U)).asUInt
+ io.tl_h.d.bits.error := d_resp_reg.error || d_fault_reg
+
+ when(io.tl_d.d.fire) {
+ // On the first beat, clear any fault and check for a new one.
+ // On subsequent beats, make the fault sticky.
+ when(beat_count === 0.U) {
+ d_fault_reg := d_check.io.fault
+ }.otherwise {
+ when(d_check.io.fault) {
+ d_fault_reg := true.B
+ }
+ }
+
+ val beat_index = (io.tl_d.d.bits.source - req_info_q.io.deq.bits.source)(log2Ceil(ratio)-1, 0)
+ d_data_reg(beat_index) := io.tl_d.d.bits.data
+ d_resp_reg := io.tl_d.d.bits
+ d_resp_reg.size := req_info_q.io.deq.bits.size
+ beat_count := beat_count + 1.U
+ when(beat_count === (req_info_q.io.deq.bits.beats - 1.U)) {
+ d_valid_reg := true.B
+ }
+ }
+
+ when(io.tl_h.d.fire) {
+ d_valid_reg := false.B
+ d_fault_reg := false.B
+ beat_count := 0.U
+ req_info_q.io.deq.ready := true.B
+ }.otherwise {
+ req_info_q.io.deq.ready := false.B
+ }
+
+ // ------------------------------------------------------------------------
+ // Request Path (A Channel): Split wide request into multiple narrow ones
+ // ------------------------------------------------------------------------
+ val a_check = Module(new RequestIntegrityCheck(host_p))
+ a_check.io.a_i := io.tl_h.a.bits
+ io.fault_a_o := a_check.io.fault
+
+ val req_fifo = Module(new FifoX(new OpenTitanTileLink.A_Channel(device_p), ratio, ratio + 1))
+
+ val beats = Wire(Vec(ratio, Valid(new OpenTitanTileLink.A_Channel(device_p))))
+ req_fifo.io.in.bits := beats
+
+ val is_write = io.tl_h.a.bits.opcode === TLULOpcodesA.PutFullData.asUInt ||
+ io.tl_h.a.bits.opcode === TLULOpcodesA.PutPartialData.asUInt
+
+ val align_mask = (~(hostBytes - 1).U(host_p.a.W))
+ val aligned_address = io.tl_h.a.bits.address & align_mask
+ val address_offset = io.tl_h.a.bits.address(log2Ceil(hostBytes) - 1, 0)
+
+ val size_in_bytes = 1.U << io.tl_h.a.bits.size
+ val read_mask = (((1.U << size_in_bytes) - 1.U) << address_offset)(hostBytes - 1, 0)
+ val effective_mask = Mux(is_write, io.tl_h.a.bits.mask, read_mask)
+
+ for (i <- 0 until ratio) {
+ val req_gen = Module(new RequestIntegrityGen(device_p))
+
+ val narrow_req = Wire(new OpenTitanTileLink.A_Channel(device_p))
+ narrow_req.opcode := Mux(is_write, TLULOpcodesA.PutPartialData.asUInt, io.tl_h.a.bits.opcode)
+ narrow_req.param := io.tl_h.a.bits.param
+ narrow_req.size := log2Ceil(device_p.w).U
+ narrow_req.source := io.tl_h.a.bits.source + i.U
+ narrow_req.address := aligned_address + (i * narrowBytes).U
+ val narrow_mask = (effective_mask >> (i * narrowBytes)).asUInt(narrowBytes-1, 0)
+ narrow_req.mask := narrow_mask
+ narrow_req.data := (io.tl_h.a.bits.data >> (i * deviceWidth)).asUInt
+ narrow_req.user := io.tl_h.a.bits.user
+
+ req_gen.io.a_i := narrow_req
+ beats(i).bits := req_gen.io.a_o
+ beats(i).valid := narrow_mask =/= 0.U
+ }
+
+ io.tl_d.a <> req_fifo.io.out
+ req_fifo.io.in.valid := io.tl_h.a.valid && !a_check.io.fault && req_info_q.io.enq.ready
+ io.tl_h.a.ready := req_fifo.io.in.ready && !a_check.io.fault && req_info_q.io.enq.ready
+
+ val total_beats = PopCount(beats.map(_.valid))
+
+ req_info_q.io.enq.valid := io.tl_h.a.fire
+ req_info_q.io.enq.bits.source := io.tl_h.a.bits.source
+ req_info_q.io.enq.bits.beats := total_beats
+ req_info_q.io.enq.bits.offset := address_offset
+ req_info_q.io.enq.bits.size := io.tl_h.a.bits.size
+ assert(!req_info_q.io.enq.valid || req_info_q.io.enq.ready)
+
+ // ==========================================================================
+ // Narrow to Wide Path (e.g., 32-bit host to 128-bit device)
+ // ==========================================================================
+ } else if (hostWidth < deviceWidth) {
+ val wideBytes = deviceWidth / 8
+ val numSourceIds = 1 << host_p.i
+ val addr_lsb_width = log2Ceil(wideBytes)
+ val index_width = log2Ceil(numSourceIds)
+ val addr_lsb_regs = RegInit(VecInit(Seq.fill(numSourceIds)(0.U(addr_lsb_width.W))))
+
+ val req_addr_lsb = io.tl_h.a.bits.address(addr_lsb_width - 1, 0)
+
+ when (io.tl_h.a.fire) {
+ addr_lsb_regs(io.tl_h.a.bits.source(index_width-1, 0)) := req_addr_lsb
+ }
+
+ val a_check = Module(new RequestIntegrityCheck(host_p))
+ a_check.io.a_i := io.tl_h.a.bits
+ io.fault_a_o := a_check.io.fault
+
+ val a_gen = Module(new RequestIntegrityGen(device_p))
+ val wide_req = Wire(new OpenTitanTileLink.A_Channel(device_p))
+ wide_req.opcode := io.tl_h.a.bits.opcode
+ wide_req.param := io.tl_h.a.bits.param
+ wide_req.size := io.tl_h.a.bits.size
+ wide_req.source := io.tl_h.a.bits.source
+ wide_req.address := io.tl_h.a.bits.address
+ wide_req.user := io.tl_h.a.bits.user
+ wide_req.mask := (io.tl_h.a.bits.mask.asUInt << req_addr_lsb).asUInt
+ wide_req.data := (io.tl_h.a.bits.data.asUInt << (req_addr_lsb << 3.U)).asUInt
+ a_gen.io.a_i := wide_req
+
+ io.tl_d.a.valid := io.tl_h.a.valid && !a_check.io.fault
+ io.tl_d.a.bits := a_gen.io.a_o
+ io.tl_h.a.ready := io.tl_d.a.ready && !a_check.io.fault
+
+ val d_check = Module(new ResponseIntegrityCheck(device_p))
+ d_check.io.d_i := io.tl_d.d.bits
+ io.fault_d_o := d_check.io.fault
+
+ val d_gen = Module(new ResponseIntegrityGen(host_p))
+ val narrow_resp = Wire(new OpenTitanTileLink.D_Channel(host_p))
+ val resp_addr_lsb = addr_lsb_regs(io.tl_d.d.bits.source(index_width-1, 0))
+ narrow_resp := io.tl_d.d.bits
+ narrow_resp.source := io.tl_d.d.bits.source
+ narrow_resp.data := (io.tl_d.d.bits.data >> (resp_addr_lsb << 3.U)).asUInt
+ narrow_resp.error := io.tl_d.d.bits.error || d_check.io.fault
+ d_gen.io.d_i := narrow_resp
+
+ io.tl_h.d.valid := io.tl_d.d.valid
+ io.tl_h.d.bits := d_gen.io.d_o
+ io.tl_d.d.ready := io.tl_h.d.ready
+
+ // ==========================================================================
+ // Equal Widths Path
+ // ==========================================================================
+ } else {
+ // Widths are equal, just pass through
+ io.tl_d <> io.tl_h
+ }
+ }
+}
diff --git a/hdl/chisel/src/common/KelvinArbiter.scala b/hdl/chisel/src/common/KelvinArbiter.scala
index f3271ae..bf878ba 100644
--- a/hdl/chisel/src/common/KelvinArbiter.scala
+++ b/hdl/chisel/src/common/KelvinArbiter.scala
@@ -49,4 +49,6 @@
when(validMask(i)) { choice := i.asUInt }
}
-class KelvinRRArbiter[T <: Data](val gen: T, val n: Int) extends InitedLockingRRArbiter[T](gen, n, 1)
+class KelvinRRArbiter[T <: Data](val gen: T, val n: Int, moduleName: Option[String] = None) extends InitedLockingRRArbiter[T](gen, n, 1) {
+ override val desiredName = moduleName.getOrElse(super.desiredName)
+}
diff --git a/tests/cocotb/tlul/BUILD b/tests/cocotb/tlul/BUILD
index 4740777..33c324b 100644
--- a/tests/cocotb/tlul/BUILD
+++ b/tests/cocotb/tlul/BUILD
@@ -91,6 +91,111 @@
vcs_defines = VCS_DEFINES,
)
+# BEGIN_TESTCASES_FOR_tlul_fifo_async_128_cocotb_test
+TLUL_FIFO_ASYNC_TESTCASES = [
+ "test_async_crossing",
+]
+# END_TESTCASES_FOR_tlul_fifo_async_128_cocotb_test
+
+cocotb_test_suite(
+ name = "tlul_fifo_async_128_cocotb_test",
+ simulators = ["verilator", "vcs"],
+ testcases = TLUL_FIFO_ASYNC_TESTCASES,
+ testcases_vname = "TLUL_FIFO_ASYNC_TESTCASES",
+ tests_kwargs = {
+ "hdl_toplevel": "TlulFifoAsync128",
+ "test_module": ["test_tlul_fifo_async.py"],
+ "deps": [
+ "//kelvin_test_utils:TileLinkULInterface",
+ ],
+ "waves": True,
+ },
+ verilator_model = "//hdl/chisel/src/bus:tlul_fifo_async_128_model",
+ vcs_verilog_sources = ["//hdl/chisel/src/bus:tlul_fifo_async_128_cc_library_verilog"],
+ vcs_build_args = VCS_BUILD_ARGS,
+ vcs_test_args = VCS_TEST_ARGS,
+ vcs_defines = VCS_DEFINES,
+)
+
+# BEGIN_TESTCASES_FOR_tlul_fifo_sync_cocotb_test
+TLUL_FIFO_SYNC_TESTCASES = [
+ "test_passthrough_with_spare",
+]
+# END_TESTCASES_FOR_tlul_fifo_sync_cocotb_test
+
+cocotb_test_suite(
+ name = "tlul_fifo_sync_cocotb_test",
+ simulators = ["verilator", "vcs"],
+ testcases = TLUL_FIFO_SYNC_TESTCASES,
+ testcases_vname = "TLUL_FIFO_SYNC_TESTCASES",
+ tests_kwargs = {
+ "hdl_toplevel": "TlulFifoSync",
+ "test_module": ["test_tlul_fifo_sync.py"],
+ "deps": [
+ "//kelvin_test_utils:TileLinkULInterface",
+ ],
+ "waves": True,
+ },
+ verilator_model = "//hdl/chisel/src/bus:tlul_fifo_sync_model",
+ vcs_verilog_sources = ["//hdl/chisel/src/bus:tlul_fifo_sync_cc_library_verilog"],
+ vcs_build_args = VCS_BUILD_ARGS,
+ vcs_test_args = VCS_TEST_ARGS,
+ vcs_defines = VCS_DEFINES,
+)
+
+# BEGIN_TESTCASES_FOR_tlul_socket_1n_128_cocotb_test
+TLUL_SOCKET_1N_TESTCASES = [
+ "test_steering",
+ "test_error_response",
+]
+# END_TESTCASES_FOR_tlul_socket_1n_128_cocotb_test
+
+cocotb_test_suite(
+ name = "tlul_socket_1n_128_cocotb_test",
+ simulators = ["verilator", "vcs"],
+ testcases = TLUL_SOCKET_1N_TESTCASES,
+ testcases_vname = "TLUL_SOCKET_1N_TESTCASES",
+ tests_kwargs = {
+ "hdl_toplevel": "TlulSocket1N_128",
+ "test_module": ["test_tlul_socket_1n.py"],
+ "deps": [
+ "//kelvin_test_utils:TileLinkULInterface",
+ ],
+ "waves": True,
+ },
+ verilator_model = "//hdl/chisel/src/bus:tlul_socket_1n_128_model",
+ vcs_verilog_sources = ["//hdl/chisel/src/bus:tlul_socket_1n_128_cc_library_verilog"],
+ vcs_build_args = VCS_BUILD_ARGS,
+ vcs_test_args = VCS_TEST_ARGS,
+ vcs_defines = VCS_DEFINES,
+)
+
+# BEGIN_TESTCASES_FOR_tlul_socket_m1_2_128_cocotb_test
+TLUL_SOCKET_M1_2_TESTCASES = [
+ "test_arbitration",
+]
+# END_TESTCASES_FOR_tlul_socket_m1_2_128_cocotb_test
+
+cocotb_test_suite(
+ name = "tlul_socket_m1_2_128_cocotb_test",
+ simulators = ["verilator", "vcs"],
+ testcases = TLUL_SOCKET_M1_2_TESTCASES,
+ testcases_vname = "TLUL_SOCKET_M1_2_TESTCASES",
+ tests_kwargs = {
+ "hdl_toplevel": "TlulSocketM1_2_128",
+ "test_module": ["test_tlul_socket_m1.py"],
+ "deps": [
+ "//kelvin_test_utils:TileLinkULInterface",
+ ],
+ "waves": True,
+ },
+ verilator_model = "//hdl/chisel/src/bus:tlul_socket_m1_2_128_model",
+ vcs_verilog_sources = ["//hdl/chisel/src/bus:tlul_socket_m1_2_128_cc_library_verilog"],
+ vcs_build_args = VCS_BUILD_ARGS,
+ vcs_test_args = VCS_TEST_ARGS,
+ vcs_defines = VCS_DEFINES,
+)
+
# BEGIN_TESTCASES_FOR_tlul_integrity_cocotb_test
TLUL_INTEGRITY_TESTCASES = [
"test_request_integrity_gen",
@@ -121,8 +226,6 @@
vcs_defines = VCS_DEFINES,
)
-
-
# BEGIN_TESTCASES_FOR_secded_encoder_cocotb_test
SECDED_ENCODER_TESTCASES = [
"test_secded_encoder",
diff --git a/tests/cocotb/tlul/test_tlul_fifo_async.py b/tests/cocotb/tlul/test_tlul_fifo_async.py
new file mode 100644
index 0000000..e4972e0
--- /dev/null
+++ b/tests/cocotb/tlul/test_tlul_fifo_async.py
@@ -0,0 +1,77 @@
+# Copyright 2025 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import cocotb
+from cocotb.clock import Clock
+from cocotb.triggers import RisingEdge, ClockCycles
+
+from kelvin_test_utils.TileLinkULInterface import TileLinkULInterface, create_a_channel_req
+
+
+async def setup_dut(dut):
+ """Common setup for all tests."""
+ h_clock = Clock(dut.io_clk_h_i, 10)
+ d_clock = Clock(dut.io_clk_d_i, 13) # Asymmetric clocks
+ cocotb.start_soon(h_clock.start())
+ cocotb.start_soon(d_clock.start())
+
+ dut.io_rst_h_i.value = 1
+ dut.io_rst_d_i.value = 1
+ await ClockCycles(dut.io_clk_h_i, 2)
+ await ClockCycles(dut.io_clk_d_i, 2)
+ dut.io_rst_h_i.value = 0
+ dut.io_rst_d_i.value = 0
+ await RisingEdge(dut.io_clk_h_i)
+ await RisingEdge(dut.io_clk_d_i)
+
+
+@cocotb.test()
+async def test_async_crossing(dut):
+ """Verify requests are arbitrated and responses are routed correctly."""
+ await setup_dut(dut)
+
+ host_if = TileLinkULInterface(dut,
+ host_if_name="io_tl_h",
+ clock_name="io_clk_h_i",
+ reset_name="io_rst_h_i")
+ device_if = TileLinkULInterface(dut,
+ device_if_name="io_tl_d",
+ clock_name="io_clk_d_i",
+ reset_name="io_rst_d_i")
+
+ req = create_a_channel_req(address=0x1000,
+ data=0x11223344,
+ mask=0xF,
+ source=1)
+
+ # Start a concurrent task to handle the device-side interaction
+ async def device_responder():
+ req_seen = await device_if.device_get_request()
+ assert req_seen["source"] == req["source"]
+ await device_if.device_respond(opcode=0,
+ param=0,
+ size=req_seen["size"],
+ source=req_seen["source"])
+
+ device_task = cocotb.start_soon(device_responder())
+
+ # Send the request from the host
+ await host_if.host_put(req)
+
+ # Wait for the response on the host side
+ response = await host_if.host_get_response()
+ assert response["source"] == req["source"]
+
+ # Wait for the device task to complete
+ await device_task
diff --git a/tests/cocotb/tlul/test_tlul_fifo_sync.py b/tests/cocotb/tlul/test_tlul_fifo_sync.py
new file mode 100644
index 0000000..a71844b
--- /dev/null
+++ b/tests/cocotb/tlul/test_tlul_fifo_sync.py
@@ -0,0 +1,104 @@
+# Copyright 2025 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import cocotb
+from cocotb.clock import Clock
+from cocotb.triggers import RisingEdge, ClockCycles, Event
+
+from kelvin_test_utils.TileLinkULInterface import TileLinkULInterface, create_a_channel_req
+
+
+async def setup_dut(dut):
+ """Common setup for all tests."""
+ cocotb.start_soon(Clock(dut.clock, 10, unit="us").start())
+
+ dut.reset.value = 1
+ await ClockCycles(dut.clock, 2)
+ dut.reset.value = 0
+ await RisingEdge(dut.clock)
+
+
+@cocotb.test()
+async def test_passthrough_with_spare(dut):
+ """Test basic data transfer and spare channels through the FIFO."""
+ await setup_dut(dut)
+ host_if = TileLinkULInterface(dut, host_if_name="io_host")
+ device_if = TileLinkULInterface(dut, device_if_name="io_device")
+
+ # Create a simple PutFullData request
+ a_data = create_a_channel_req(address=0x1000,
+ data=0x11223344,
+ mask=0xF,
+ width=32)
+ spare_req_val = 1
+ spare_rsp_val = 0
+
+ # Create a concurrent task that acts as the device model
+ async def device_model():
+ # Wait for the request from the DUT (coming from the host)
+ req = await device_if.device_get_request()
+
+ # Verify the request is what we expect
+ assert req["opcode"] == a_data["opcode"], f"Request opcode mismatch"
+ assert req["param"] == a_data["param"], f"Request param mismatch"
+ assert req["size"] == a_data["size"], f"Request size mismatch"
+ assert req["source"] == a_data["source"], f"Request source mismatch"
+ assert req["address"] == a_data["address"], f"Request address mismatch"
+ assert req["mask"] == a_data["mask"], f"Request mask mismatch"
+ assert req["data"] == a_data["data"], f"Request data mismatch"
+ for field, value in a_data["user"].items():
+ assert req["user"][
+ field] == value, f"Request user.{field} mismatch"
+
+ # Check spare request channel
+ assert dut.io_spare_req_o.value == spare_req_val, "Spare request data mismatch"
+
+ # Drive spare response channel before sending the main response
+ dut.io_spare_rsp_i.value = spare_rsp_val
+
+ # Send a simple AccessAck response
+ await device_if.device_respond(
+ opcode=0, # AccessAck
+ param=0,
+ size=req["size"],
+ source=req["source"])
+
+ # Start the device model task
+ device_task = cocotb.start_soon(device_model())
+
+ # Drive spare request channel before sending the main request
+ dut.io_spare_req_i.value = spare_req_val
+
+ # Drive the transaction from the host side
+ await host_if.host_put(a_data)
+
+ # Wait for the response on the host side
+ response = await host_if.host_get_response()
+
+ # Verify the response
+ assert response["opcode"] == 0, "Response opcode mismatch"
+ assert response["param"] == 0, "Response param mismatch"
+ assert response["size"] == a_data["size"], "Response size mismatch"
+ assert response["source"] == a_data["source"], "Response source mismatch"
+ assert response["sink"] == 0, "Response sink mismatch"
+ assert response["data"] == 0, "Response data mismatch"
+ assert response["error"] == 0, "Response error mismatch"
+ assert response["user"]["rsp_intg"] != 0, "Response user.rsp_intg should not be zero"
+ assert response["user"]["data_intg"] != 0, "Response user.data_intg should not be zero"
+
+ # Check spare response channel
+ assert dut.io_spare_rsp_o.value == spare_rsp_val, "Spare response data mismatch"
+
+ # Ensure the device model task completed successfully
+ await device_task
diff --git a/tests/cocotb/tlul/test_tlul_socket_1n.py b/tests/cocotb/tlul/test_tlul_socket_1n.py
new file mode 100644
index 0000000..80ab150
--- /dev/null
+++ b/tests/cocotb/tlul/test_tlul_socket_1n.py
@@ -0,0 +1,92 @@
+# Copyright 2025 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import cocotb
+from cocotb.clock import Clock
+from cocotb.triggers import FallingEdge, RisingEdge, ClockCycles, with_timeout
+import random
+
+from kelvin_test_utils.TileLinkULInterface import TileLinkULInterface, create_a_channel_req
+
+
+async def setup_dut(dut):
+ """Common setup for all tests."""
+ clock = Clock(dut.clock, 10)
+ cocotb.start_soon(clock.start())
+ dut.reset.value = 1
+ await ClockCycles(dut.clock, 2)
+ dut.reset.value = 0
+ await RisingEdge(dut.clock)
+
+
+@cocotb.test()
+async def test_steering(dut):
+ """Verify requests are steered to the correct device port."""
+ await setup_dut(dut)
+
+ N = 4 # This is hardcoded in the Chisel emitter for now
+ host_if = TileLinkULInterface(dut, host_if_name="io_tl_h")
+ device_ifs = [
+ TileLinkULInterface(dut, device_if_name=f"io_tl_d_{i}")
+ for i in range(N)
+ ]
+
+ async def device_responder(device_if, i):
+ req_seen = await device_if.device_get_request()
+ await device_if.device_respond(opcode=0,
+ param=0,
+ size=req_seen["size"],
+ source=req_seen["source"])
+
+ # Start all device responders
+ for i in range(N):
+ cocotb.start_soon(device_responder(device_ifs[i], i))
+
+ for i in range(N):
+ dut.io_dev_select_i.value = i
+ req = create_a_channel_req(address=0x1000 + i * 0x100,
+ data=0x11223344 + i,
+ mask=0xF,
+ source=i)
+
+ await host_if.host_put(req)
+ response = await host_if.host_get_response()
+
+ assert response["source"] == i
+ # TODO(atv): Can we do this better?
+ # Allow some time for the device responder to process the request
+ await ClockCycles(dut.clock, 5)
+
+
+@cocotb.test()
+async def test_error_response(dut):
+ """Verify error response for out-of-bounds dev_select."""
+ await setup_dut(dut)
+
+ N = 4 # This is hardcoded in the Chisel emitter for now
+ host_if = TileLinkULInterface(dut, host_if_name="io_tl_h")
+
+ # dev_select_i is NWD bits wide, where NWD = ceil(log2(N+1))
+ # So, a value of N should be out of bounds and trigger an error
+ dut.io_dev_select_i.value = N
+ req = create_a_channel_req(address=0xBAD,
+ data=0xBAD,
+ mask=0xF,
+ source=(1 << 6) - 1)
+
+ await host_if.host_put(req)
+ response = await host_if.host_get_response()
+
+ assert response["error"] == 1
+ assert response["source"] == req["source"]
diff --git a/tests/cocotb/tlul/test_tlul_socket_m1.py b/tests/cocotb/tlul/test_tlul_socket_m1.py
new file mode 100644
index 0000000..6061ef2
--- /dev/null
+++ b/tests/cocotb/tlul/test_tlul_socket_m1.py
@@ -0,0 +1,81 @@
+# Copyright 2025 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import cocotb
+from cocotb.clock import Clock
+from cocotb.triggers import RisingEdge, ClockCycles, with_timeout
+import math
+import random
+
+from kelvin_test_utils.TileLinkULInterface import TileLinkULInterface, create_a_channel_req
+
+
+async def setup_dut(dut):
+ """Common setup for all tests."""
+ clock = Clock(dut.clock, 10)
+ cocotb.start_soon(clock.start())
+ dut.reset.value = 1
+ await ClockCycles(dut.clock, 2)
+ dut.reset.value = 0
+ await RisingEdge(dut.clock)
+
+
+@cocotb.test()
+async def test_arbitration(dut):
+ """Verify requests are arbitrated and responses are routed correctly."""
+ await setup_dut(dut)
+
+ M = 0
+ while hasattr(dut, f"io_tl_h_{M}_a_valid"):
+ M += 1
+
+ StIdW = math.ceil(math.log2(M))
+
+ host_ifs = [
+ TileLinkULInterface(dut, host_if_name=f"io_tl_h_{i}") for i in range(M)
+ ]
+ device_if = TileLinkULInterface(dut, device_if_name="io_tl_d")
+
+ reqs = {
+ i:
+ create_a_channel_req(address=0x1000 + i * 0x100,
+ data=0x11223344 + i,
+ mask=0xF,
+ source=i)
+ for i in range(M)
+ }
+ received_reqs = {}
+
+ async def device_responder():
+ while len(received_reqs) < M:
+ req_seen = await device_if.device_get_request()
+ host_index = req_seen["source"].to_unsigned() & ((1 << StIdW) - 1)
+ assert req_seen["source"].to_unsigned(
+ ) >> StIdW == reqs[host_index]["source"]
+ received_reqs[host_index] = req_seen
+ await device_if.device_respond(opcode=0,
+ param=0,
+ size=req_seen["size"],
+ source=req_seen["source"])
+
+ device_task = cocotb.start_soon(device_responder())
+
+ for i in range(M):
+ await host_ifs[i].host_put(reqs[i])
+
+ for i in range(M):
+ response = await host_ifs[i].host_get_response()
+ assert response["source"] == reqs[i]["source"]
+
+ await with_timeout(device_task, 1000)