Merge "Add floating point regfile."
diff --git a/hdl/chisel/BUILD b/hdl/chisel/BUILD
index 88cf0d0..f32c099 100644
--- a/hdl/chisel/BUILD
+++ b/hdl/chisel/BUILD
@@ -17,14 +17,14 @@
     srcs = [
         "//hdl/chisel/src/matcha:Kelvin.sv",
         "//hdl/chisel/src/matcha:Crossbar.sv",
-        "//hdl/verilog:ClockGate.v",
+        "//hdl/verilog:ClockGate.sv",
         "//hdl/verilog:Sram_1rw_256x256.v",
         "//hdl/verilog:Sram_1rwm_256x288.v",
     ],
     outs = [
         "kelvin.sv",
         "crossbar.sv",
-        "ClockGate.v",
+        "ClockGate.sv",
         "Sram_1rw_256x256.v",
         "Sram_1rwm_256x288.v",
     ],
@@ -41,7 +41,7 @@
 \\`define ASSERT_VERBOSE_COND 0
 " > $(location crossbar.sv)
     cat $(location //hdl/chisel/src/matcha:Crossbar.sv) >> $(location crossbar.sv)
-    cp -f $(location //hdl/verilog:ClockGate.v) $(location ClockGate.v)
+    cp -f $(location //hdl/verilog:ClockGate.sv) $(location ClockGate.sv)
     cp -f $(location //hdl/verilog:Sram_1rw_256x256.v) $(location Sram_1rw_256x256.v)
     cp -f $(location //hdl/verilog:Sram_1rwm_256x288.v) $(location Sram_1rwm_256x288.v)
     """,
diff --git a/hdl/chisel/kelvin.core.in b/hdl/chisel/kelvin.core.in
index e8d61c6..95f7fa3 100644
--- a/hdl/chisel/kelvin.core.in
+++ b/hdl/chisel/kelvin.core.in
@@ -8,7 +8,7 @@
   files_rtl:
     files:
       - kelvin.sv
-      - ClockGate.v
+      - ClockGate.sv
       - Sram_1rw_256x256.v
       - Sram_1rwm_256x288.v
     file_type: systemVerilogSource
diff --git a/hdl/chisel/src/chai/BUILD b/hdl/chisel/src/chai/BUILD
new file mode 100644
index 0000000..b4e1c48
--- /dev/null
+++ b/hdl/chisel/src/chai/BUILD
@@ -0,0 +1,45 @@
+# Copyright 2024 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
+#
+#     http://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.
+
+load("@kelvin_hw//rules:chisel.bzl", "chisel_cc_library", "chisel_library")
+
+package(default_visibility = ["//visibility:public"])
+
+chisel_library(
+    name = "chai",
+    srcs = [
+        "ChAI.scala",
+        "KelvinToTlul.scala",
+        "TlulAdapterSram.scala",
+        "Uart.scala",
+    ],
+    deps = [
+        "//hdl/chisel/src/matcha:matcha",
+        "//hdl/chisel/src/kelvin:kelvin",
+    ],
+)
+
+chisel_cc_library(
+    name = "chai_cc_library",
+    chisel_lib = ":chai",
+    emit_class = "chai.EmitChAI",
+    module_name = "ChAI",
+    verilog_deps = [
+        "//hdl/verilog:clock_gate",
+        "//hdl/verilog:sram_1rw_256x256",
+        "//hdl/verilog:sram_1rw_256x288",
+        "//hdl/verilog:tlul_adapter_sram",
+        "//hdl/verilog:uart",
+    ],
+)
diff --git a/hdl/chisel/src/chai/ChAI.scala b/hdl/chisel/src/chai/ChAI.scala
new file mode 100644
index 0000000..97ce896
--- /dev/null
+++ b/hdl/chisel/src/chai/ChAI.scala
@@ -0,0 +1,119 @@
+// Copyright 2024 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
+//
+//     http://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.
+
+package chai
+
+import chisel3._
+import chisel3.util._
+import _root_.circt.stage.ChiselStage
+
+case class Parameters() {
+  val sramReadPorts = 1
+  val sramWritePorts = 1
+  val sramReadWritePorts = 0
+  val sramDataBits = 256
+  val sramBytes = 4 * 1024 * 1024
+  def sramDataEntries(): Int = {
+    ((sramBytes * 8) / sramDataBits)
+  }
+  def sramAddrBits(): Int = {
+    log2Ceil(sramDataEntries())
+  }
+}
+
+object ChAI {
+  def apply(p: Parameters): ChAI = {
+    return Module(new ChAI(p))
+  }
+}
+
+class ChAI(p: Parameters) extends RawModule {
+  val io = IO(new Bundle {
+    val clk_i = Input(Clock())
+    val rst_ni = Input(AsyncReset())
+    val sram = new Bundle {
+      val write_address = Input(UInt(p.sramAddrBits().W))
+      val write_enable = Input(Bool())
+      val write_data = Input(UInt(p.sramDataBits.W))
+    }
+    val finish = Output(Bool())
+    val fault = Output(Bool())
+    val freeze = Input(Bool())
+
+    val uart_rx = Input(Bool())
+    val uart_tx = Output(Bool())
+  })
+
+  // TODO(atv): Compute that we don't have any overlaps in regions.
+  val memoryRegions = Seq(
+    new kelvin.MemoryRegion(0, 4 * 1024 * 1024, true, 256), // SRAM
+    new kelvin.MemoryRegion(4 * 1024 * 1024, 4 * 1024 * 1024, false, 256) // UART
+  )
+  val kelvin_p = kelvin.Parameters(memoryRegions)
+  val rst_i = (!io.rst_ni.asBool).asAsyncReset
+
+  val u_kelvin = matcha.Kelvin(kelvin_p)
+  u_kelvin.clk_i := io.clk_i
+  u_kelvin.rst_ni := io.rst_ni
+  u_kelvin.clk_freeze := io.freeze
+  u_kelvin.ml_reset := 0.U
+  u_kelvin.pc_start := 0.U
+  u_kelvin.volt_sel := 0.U
+
+  io.finish := u_kelvin.finish
+  io.fault := u_kelvin.fault
+
+  withClockAndReset(io.clk_i, rst_i) {
+    val tlul_p = new kelvin.TLULParameters()
+    val kelvin_to_tlul = chai.KelvinToTlul(tlul_p, kelvin_p)
+    kelvin_to_tlul.io.kelvin <> u_kelvin.mem
+
+    val tlul_sram =
+      SRAM(p.sramDataEntries(), UInt(p.sramDataBits.W), p.sramReadPorts, p.sramWritePorts, p.sramReadWritePorts)
+    val tlul_adapter_sram = Module(new chai.TlulAdapterSram())
+    tlul_adapter_sram.io.clk_i := io.clk_i
+    tlul_adapter_sram.io.rst_ni := io.rst_ni
+    tlul_adapter_sram.io.en_ifetch_i := 9.U // MuBi4False
+    tlul_sram.readPorts(0).enable := tlul_adapter_sram.io.req_o
+    tlul_sram.readPorts(0).address := tlul_adapter_sram.io.addr_o
+    tlul_sram.writePorts(0).enable := Mux(io.freeze, io.sram.write_enable, tlul_adapter_sram.io.we_o)
+    tlul_sram.writePorts(0).address := Mux(io.freeze, io.sram.write_address, tlul_adapter_sram.io.addr_o)
+    tlul_sram.writePorts(0).data := Mux(io.freeze, io.sram.write_data, tlul_adapter_sram.io.wdata_o)
+    tlul_adapter_sram.io.gnt_i := 1.U
+    tlul_adapter_sram.io.rdata_i := tlul_sram.readPorts(0).data
+    tlul_adapter_sram.io.rvalid_i := 1.U
+    tlul_adapter_sram.io.rerror_i := 0.U
+
+    val uart = Module(new chai.Uart(tlul_p))
+    uart.io.clk_i := io.clk_i
+    uart.io.rst_ni := io.rst_ni
+    uart.io.alert_rx_i := 0.U
+    uart.io.cio_rx_i := io.uart_rx
+    io.uart_tx := uart.io.cio_tx_o
+
+    val crossbar =
+      Module(new kelvin.TileLinkUL(tlul_p, kelvin_p.m, /* hosts= */ 1))
+    crossbar.io.hosts_a(0) <> kelvin_to_tlul.io.tl_o
+    crossbar.io.hosts_d(0) <> kelvin_to_tlul.io.tl_i
+    crossbar.io.devices_a(0) <> tlul_adapter_sram.io.tl_i
+    crossbar.io.devices_d(0) <> tlul_adapter_sram.io.tl_o
+    crossbar.io.devices_a(1) <> uart.io.tl_i
+    crossbar.io.devices_d(1) <> uart.io.tl_o
+  }
+}
+
+object EmitChAI extends App {
+  val p = new Parameters()
+  ChiselStage.emitSystemVerilogFile(new ChAI(p), args)
+}
diff --git a/hdl/chisel/src/chai/KelvinToTlul.scala b/hdl/chisel/src/chai/KelvinToTlul.scala
new file mode 100644
index 0000000..c8573c2
--- /dev/null
+++ b/hdl/chisel/src/chai/KelvinToTlul.scala
@@ -0,0 +1,114 @@
+// Copyright 2024 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
+//
+//     http://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.
+
+package chai
+
+import chisel3._
+import chisel3.util._
+
+object KelvinToTlul {
+  object State extends ChiselEnum {
+    val sIdle, sWaitForReady, sWaitForResponse = Value
+  }
+
+  def apply(tlul_p: kelvin.TLULParameters, kelvin_p: kelvin.Parameters): KelvinToTlul = {
+    return Module(new KelvinToTlul(tlul_p, kelvin_p))
+  }
+}
+
+class KelvinToTlul(tlul_p: kelvin.TLULParameters, kelvin_p: kelvin.Parameters) extends Module {
+  import KelvinToTlul.State
+  import KelvinToTlul.State._
+
+  val io = IO(new Bundle {
+    val tl_i = Input(new _root_.kelvin.TileLinkULIO_D2H(tlul_p))
+    val tl_o = Output(new _root_.kelvin.TileLinkULIO_H2D(tlul_p))
+    val kelvin = Flipped(new matcha.KelvinMemIO(kelvin_p))
+  })
+  val state = RegInit(sIdle)
+
+  val wmask_width = io.kelvin.wmask.getWidth
+  val wmask_bits = (0 until wmask_width)
+    .map(x => Mux(io.kelvin.wmask(x), 0xff.U(wmask_width.W) << (x * 8).U, 0.U(wmask_width.W)))
+    .reduce(_ | _)
+
+  io.tl_o := 0.U.asTypeOf(new kelvin.TileLinkULIO_H2D(tlul_p))
+  io.tl_o.a_user.instr_type := 9.U
+  io.tl_o.a_source := 0.U
+  io.tl_o.d_ready := true.B
+  io.kelvin.rid := 0.U
+  io.kelvin.cready := true.B
+  io.kelvin.rvalid := false.B
+  io.kelvin.rdata := 0.U
+  io.tl_o.a_mask := -1.S(io.tl_o.a_mask.getWidth.W).asUInt
+  io.tl_o.a_param := 0.U
+  io.tl_o.a_size := 5.U
+
+  // state            | transition       | next state
+  // sIdle            | valid & a_ready  | sWaitForResponse
+  // sIdle            | valid & !a_ready | sWaitForReady
+  // sWaitForReady    | a_ready          | sWaitForResponse
+  // sWaitForResponse | d_valid          | sIdle
+  switch(state) {
+    is(sIdle) {
+      io.kelvin.cready := true.B
+      when(io.kelvin.cvalid) {
+        io.kelvin.cready := false.B
+        io.tl_o.a_valid := true.B
+        io.tl_o.a_address := io.kelvin.caddr
+        val cwrite = io.kelvin.cwrite
+        io.tl_o.a_opcode := Mux(cwrite, kelvin.TLULOpcodesA.PutFullData.asUInt, kelvin.TLULOpcodesA.Get.asUInt)
+        io.tl_o.a_data := Mux(cwrite, io.kelvin.wdata & wmask_bits, 0.U)
+        state := Mux(io.tl_i.a_ready, sWaitForResponse, sWaitForReady)
+      }
+    }
+    is(sWaitForReady) {
+      when(io.tl_i.a_ready) {
+        state := sWaitForResponse
+      }
+    }
+    is(sWaitForResponse) {
+      io.tl_o.a_valid := false.B
+      when(io.tl_i.d_valid) {
+        val (value, valid) = kelvin.TLULOpcodesD.safe(io.tl_i.d_opcode)
+        val valid2 = valid && (value =/= kelvin.TLULOpcodesD.End)
+        assert(valid2, "Received invalid TLUL-D opcode\n")
+
+        val rdata = chisel3.util.MuxLookup(value, 0.U(32.W))(
+          Array(
+            kelvin.TLULOpcodesD.AccessAck -> 0.U,
+            kelvin.TLULOpcodesD.AccessAckData -> io.tl_i.d_data
+          )
+        )
+        val rvalid = chisel3.util.MuxLookup(value, false.B)(
+          Array(
+            kelvin.TLULOpcodesD.AccessAck -> false.B,
+            kelvin.TLULOpcodesD.AccessAckData -> true.B
+          )
+        )
+        val rid = chisel3.util.MuxLookup(value, 0.U)(
+          Array(
+            kelvin.TLULOpcodesD.AccessAck -> 0.U,
+            kelvin.TLULOpcodesD.AccessAckData -> io.kelvin.cid
+          )
+        )
+        io.kelvin.rvalid := rvalid
+        io.kelvin.rdata := rdata
+        io.kelvin.cready := true.B
+        io.kelvin.rid := rid
+        state := sIdle
+      }
+    }
+  }
+}
diff --git a/hdl/chisel/src/chai/TlulAdapterSram.scala b/hdl/chisel/src/chai/TlulAdapterSram.scala
new file mode 100644
index 0000000..345db11
--- /dev/null
+++ b/hdl/chisel/src/chai/TlulAdapterSram.scala
@@ -0,0 +1,55 @@
+// Copyright 2024 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
+//
+//     http://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.
+
+package chai
+
+import chisel3._
+import chisel3.util._
+
+package object sram_params {
+  val SramAw = 17
+  val SramDw = 256
+  val Outstanding = 1
+  val ByteAccess = 1
+  val ErrOnRead = 0
+  val EnableDataIntgPt = 0
+}
+
+class TlulAdapterSram extends BlackBox {
+  val tlul_p = new kelvin.TLULParameters()
+  val io = IO(new Bundle {
+    val clk_i = Input(Clock())
+    val rst_ni = Input(AsyncReset())
+
+    // TL-UL
+    val tl_i = Input(new kelvin.TileLinkULIO_H2D(tlul_p))
+    val tl_o = Output(new kelvin.TileLinkULIO_D2H(tlul_p))
+
+    // control
+    val en_ifetch_i = Input(UInt(4.W)) // mubi4_t
+
+    // SRAM interface
+    val req_o = Output(Bool())
+    val req_type_o = Output(UInt(4.W)) // mubi4_t
+    val gnt_i = Input(Bool())
+    val we_o = Output(Bool())
+    val addr_o = Output(UInt(sram_params.SramAw.W))
+    val wdata_o = Output(UInt(sram_params.SramDw.W))
+    val wmask_o = Output(UInt(sram_params.SramDw.W))
+    val intg_error_o = Output(Bool())
+    val rdata_i = Input(UInt(sram_params.SramDw.W))
+    val rvalid_i = Input(Bool())
+    val rerror_i = Input(UInt(2.W))
+  })
+}
diff --git a/hdl/chisel/src/chai/Uart.scala b/hdl/chisel/src/chai/Uart.scala
new file mode 100644
index 0000000..4fa3090
--- /dev/null
+++ b/hdl/chisel/src/chai/Uart.scala
@@ -0,0 +1,45 @@
+// Copyright 2024 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
+//
+//     http://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.
+
+package chai
+
+import chisel3._
+import chisel3.util._
+
+class Uart(tlul_p: kelvin.TLULParameters) extends BlackBox {
+  val io = IO(new Bundle {
+    val clk_i = Input(Clock())
+    val rst_ni = Input(AsyncReset())
+
+    val tl_i = Input(new kelvin.TileLinkULIO_H2D(tlul_p))
+    val tl_o = Output(new kelvin.TileLinkULIO_D2H(tlul_p))
+
+    // These have some alert_{rx|tx}_t types.
+    val alert_rx_i = Input(UInt(4.W))
+    val alert_tx_o = Output(UInt(2.W))
+
+    val cio_rx_i = Input(Bool())
+    val cio_tx_o = Output(Bool())
+    val cio_tx_en_o = Output(Bool())
+
+    val intr_tx_watermark_o = Output(Bool())
+    val intr_rx_watermark_o = Output(Bool())
+    val intr_tx_empty_o = Output(Bool())
+    val intr_rx_overflow_o = Output(Bool())
+    val intr_rx_frame_err_o = Output(Bool())
+    val intr_rx_break_err_o = Output(Bool())
+    val intr_rx_timeout_o = Output(Bool())
+    val intr_rx_parity_err_o = Output(Bool())
+  })
+}
diff --git a/hdl/chisel/src/kelvin/BUILD b/hdl/chisel/src/kelvin/BUILD
index be34cc6..a8eb9d6 100644
--- a/hdl/chisel/src/kelvin/BUILD
+++ b/hdl/chisel/src/kelvin/BUILD
@@ -50,6 +50,7 @@
         "L1ICache.scala",
         "Library.scala",
         "Parameters.scala",
+        "TileLinkUL.scala",
         "scalar/Alu.scala",
         "scalar/Bru.scala",
         "scalar/Csr.scala",
diff --git a/hdl/chisel/src/kelvin/Parameters.scala b/hdl/chisel/src/kelvin/Parameters.scala
index bb8a149..7c4b983 100644
--- a/hdl/chisel/src/kelvin/Parameters.scala
+++ b/hdl/chisel/src/kelvin/Parameters.scala
@@ -17,7 +17,21 @@
 import chisel3._
 import chisel3.util._
 
-case class Parameters() {
+class MemoryRegion(
+  val memStart: Int,
+  val memSize: Int,
+  val cacheable: Boolean,
+  val dataWidthBits: Int,
+) {
+
+def contains(addr: UInt): Bool = {
+  val addrWidth = addr.getWidth.W
+  (addr >= memStart.U(addrWidth)) && (addr < memStart.U(addrWidth) + memSize.U(addrWidth))
+}
+
+}
+
+case class Parameters(m: Seq[MemoryRegion] = Seq()) {
   case object Core {
     val tiny = 0
     val little = 1
diff --git a/hdl/chisel/src/kelvin/TileLinkUL.scala b/hdl/chisel/src/kelvin/TileLinkUL.scala
new file mode 100644
index 0000000..155fa3d
--- /dev/null
+++ b/hdl/chisel/src/kelvin/TileLinkUL.scala
@@ -0,0 +1,127 @@
+// Copyright 2024 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
+//
+//     http://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.
+
+package kelvin
+
+import chisel3._
+import chisel3.util._
+import _root_.circt.stage.ChiselStage
+
+case class TLULParameters() {
+  val w = 32
+  val a = 32
+  val z = 6
+  val o = 10
+  val i = 1
+}
+
+object TLULOpcodesA extends ChiselEnum {
+  val PutFullData = Value(0.U(3.W))
+  val PutPartialData = Value(1.U(3.W))
+  val Get = Value(4.U(3.W))
+  val End = Value(7.U(3.W))
+}
+
+object TLULOpcodesD extends ChiselEnum {
+  val AccessAck = Value(0.U(3.W))
+  val AccessAckData = Value(1.U(3.W))
+  val End = Value(7.U(3.W))
+}
+
+class TileLinkULIO_H2D(p: TLULParameters) extends Bundle {
+  val a_valid = (Bool())
+  val a_opcode = (UInt(3.W))
+  val a_param = (UInt(3.W))
+  val a_size = (UInt(p.z.W))
+  val a_source = (UInt(p.o.W))
+  val a_address = (UInt(p.a.W))
+  val a_mask = (UInt(p.w.W))
+  val a_data = (UInt((8 * p.w).W))
+  val a_user = new Bundle {
+    val rsvd = UInt(5.W)
+    val instr_type = UInt(4.W) // mubi4_t
+    val cmd_intg = UInt(7.W)
+    val data_intg = UInt(7.W)
+  }
+  val d_ready = (Bool())
+}
+
+class TileLinkULIO_D2H(p: TLULParameters) extends Bundle {
+  val d_valid = (Bool())
+  val d_opcode = (UInt(3.W))
+  val d_param = (UInt(3.W))
+  val d_size = (UInt(p.z.W))
+  val d_source = (UInt(p.o.W))
+  val d_sink = (UInt(p.i.W))
+  val d_data = (UInt((8 * p.w).W))
+  val d_user = new Bundle {
+    val rsp_intg = UInt(7.W)
+    val data_intg = UInt(7.W)
+  }
+  val d_error = (Bool())
+  val a_ready = (Bool())
+}
+
+class TileLinkUL(p: TLULParameters, m: Seq[MemoryRegion], hosts: Int) extends Module {
+  val devices = m.length
+  val io = IO(new Bundle {
+    val hosts_a = Vec(hosts, Input(new TileLinkULIO_H2D(p)))
+    val hosts_d = Vec(hosts, Output(new TileLinkULIO_D2H(p)))
+    val devices_a = Vec(devices, Output(new TileLinkULIO_H2D(p)))
+    val devices_d = Vec(devices, Input(new TileLinkULIO_D2H(p)))
+  })
+
+
+  for (i <- 0 until hosts) {
+    io.hosts_d(i) := 0.U.asTypeOf(new TileLinkULIO_D2H(p))
+  }
+  for (i <- 0 until devices) {
+    io.devices_a(i) := 0.U.asTypeOf(new TileLinkULIO_H2D(p))
+  }
+
+  val aValids = io.hosts_a.map(x => x.a_valid)
+  val anyAValid = PopCount(aValids) > 0.U
+  when(anyAValid) {
+    val aValidIndex = PriorityEncoder(aValids)
+    val host_a = io.hosts_a(aValidIndex)
+    val host_d = io.hosts_d(aValidIndex)
+    val address = host_a.a_address
+
+    val addressOk = (0 until devices).map(x => m(x).contains(address))
+    val startAddresses = VecInit(m.map(x => x.memStart.U(p.a.W)))
+    assert(PopCount(addressOk) === 1.U)
+    val deviceIndex = PriorityEncoder(addressOk)
+    val device_a = io.devices_a(deviceIndex)
+    val device_d = io.devices_d(deviceIndex)
+
+    device_a :<>= host_a
+    device_a.a_address := host_a.a_address - startAddresses(deviceIndex)
+    host_d :<>= device_d
+  }
+
+  val dValids = io.devices_d.map(x => x.d_valid)
+  val anyDValid = dValids.reduce(_ || _)
+  when(anyDValid) {
+    val dValidIndex = PriorityEncoder(dValids)
+    val device_d = io.devices_d(dValidIndex)
+    val device_a = io.devices_a(dValidIndex)
+    val source = device_d.d_source
+    val sink = device_d.d_sink
+    val host_d = io.hosts_d(source)
+    val host_a = io.hosts_a(source)
+
+    host_d :<>= device_d
+    device_a :<>= host_a
+  }
+}
diff --git a/hdl/chisel/src/kelvin/scalar/Lsu.scala b/hdl/chisel/src/kelvin/scalar/Lsu.scala
index 520693b..60aa158 100644
--- a/hdl/chisel/src/kelvin/scalar/Lsu.scala
+++ b/hdl/chisel/src/kelvin/scalar/Lsu.scala
@@ -136,8 +136,10 @@
   // Control Port Inputs.
   ctrl.io.in.valid := io.req.map(_.valid).reduce(_||_)
 
+  val uncacheable = p.m.filter(x => !x.cacheable)
   for (i <- 0 until 4) {
-    val uncached = io.busPort.addr(i)(31)
+    val uncached = io.busPort.addr(i)(31) ||
+      (if (uncacheable.length > 0) uncacheable.map(x => (io.busPort.addr(i) >= x.memStart.U) && (io.busPort.addr(i) < (x.memStart + x.memSize).U)).reduce(_||_) else false.B)
 
     val opstore = io.req(i).op(lsu.SW) || io.req(i).op(lsu.SH) || io.req(i).op(lsu.SB)
     val opiload = io.req(i).op(lsu.LW) || io.req(i).op(lsu.LH) || io.req(i).op(lsu.LB) || io.req(i).op(lsu.LHU) || io.req(i).op(lsu.LBU)
@@ -218,7 +220,6 @@
   io.ubus.size  := ctrl.io.out.bits.size
   io.ubus.wdata := wdata
   io.ubus.wmask := wmask
-  assert(!(io.ubus.valid && !ctrl.io.out.bits.addr(31)))
   assert(!(io.ubus.valid && io.dbus.addr(31)))
   assert(!(io.ubus.valid && io.dbus.adrx(31)))
 
diff --git a/hdl/chisel/src/matcha/Kelvin.scala b/hdl/chisel/src/matcha/Kelvin.scala
index 65c8da0..b8ed6cf 100644
--- a/hdl/chisel/src/matcha/Kelvin.scala
+++ b/hdl/chisel/src/matcha/Kelvin.scala
@@ -19,6 +19,19 @@
 import common._
 import _root_.circt.stage.ChiselStage
 
+class KelvinMemIO(p: kelvin.Parameters) extends Bundle {
+  val cvalid = (Output(Bool()))
+  val cready = (Input(Bool()))
+  val cwrite = (Output(Bool()))
+  val caddr  = (Output(UInt(p.axiSysAddrBits.W)))
+  val cid    = (Output(UInt(p.axiSysIdBits.W)))
+  val wdata  = (Output(UInt(p.axiSysDataBits.W)))
+  val wmask  = (Output(UInt((p.axiSysDataBits / 8).W)))
+  val rvalid = (Input(Bool()))
+  val rid    = (Input(UInt(p.axiSysIdBits.W)))
+  val rdata  = (Input(UInt(p.axiSysDataBits.W)))
+}
+
 object Kelvin {
   def apply(p: kelvin.Parameters): Kelvin = {
     return Module(new Kelvin(p))
@@ -30,16 +43,7 @@
   val clk_i  = IO(Input(Clock()))
   val rst_ni = IO(Input(AsyncReset()))
 
-  val cvalid = IO(Output(Bool()))
-  val cready = IO(Input(Bool()))
-  val cwrite = IO(Output(Bool()))
-  val caddr  = IO(Output(UInt(p.axiSysAddrBits.W)))
-  val cid    = IO(Output(UInt(p.axiSysIdBits.W)))
-  val wdata  = IO(Output(UInt(p.axiSysDataBits.W)))
-  val wmask  = IO(Output(UInt((p.axiSysDataBits / 8).W)))
-  val rvalid = IO(Input(Bool()))
-  val rid    = IO(Input(UInt(p.axiSysIdBits.W)))
-  val rdata  = IO(Input(UInt(p.axiSysDataBits.W)))
+  val mem = IO(new KelvinMemIO(p))
 
   val clk_freeze = IO(Input(Bool()))
   val ml_reset   = IO(Input(Bool()))
@@ -118,16 +122,16 @@
 
     // -------------------------------------------------------------------------
     // SRAM bridge.
-    cvalid := bus.io.out.cvalid
-    bus.io.out.cready := cready
-    cwrite := bus.io.out.cwrite
-    caddr  := bus.io.out.caddr
-    cid    := bus.io.out.cid
-    wdata  := bus.io.out.wdata
-    wmask  := bus.io.out.wmask
-    bus.io.out.rvalid := rvalid
-    bus.io.out.rid := rid
-    bus.io.out.rdata := rdata
+    mem.cvalid := bus.io.out.cvalid
+    bus.io.out.cready := mem.cready
+    mem.cwrite := bus.io.out.cwrite
+    mem.caddr  := bus.io.out.caddr
+    mem.cid    := bus.io.out.cid
+    mem.wdata  := bus.io.out.wdata
+    mem.wmask  := bus.io.out.wmask
+    bus.io.out.rvalid := mem.rvalid
+    bus.io.out.rid := mem.rid
+    bus.io.out.rdata := mem.rdata
 
     // -------------------------------------------------------------------------
     // Command interface.
@@ -140,6 +144,6 @@
 }
 
 object EmitKelvin extends App {
-  val p = new kelvin.Parameters()
+  val p = new kelvin.Parameters(Seq())
   ChiselStage.emitSystemVerilogFile(new Kelvin(p), args)
 }
diff --git a/hdl/verilog/BUILD b/hdl/verilog/BUILD
index 802e2db..440262a 100644
--- a/hdl/verilog/BUILD
+++ b/hdl/verilog/BUILD
@@ -16,16 +16,21 @@
 
 exports_files(
     srcs = [
-        "ClockGate.v",
+        "ClockGate.sv",
         "Sram_1rw_256x256.v",
         "Sram_1rwm_256x288.v",
+        "TlulAdapterSram.sv",
+        "Uart.sv",
     ],
     visibility = ["//visibility:public"],
 )
 
 verilog_library(
     name = "clock_gate",
-    srcs = ["ClockGate.v"],
+    srcs = ["ClockGate.sv"],
+    deps = [
+        "//third_party/ip/lowrisc:prim",
+    ],
     visibility = ["//visibility:public"],
 )
 
@@ -40,3 +45,22 @@
     srcs = ["Sram_1rwm_256x288.v"],
     visibility = ["//visibility:public"],
 )
+
+verilog_library(
+    name = "tlul_adapter_sram",
+    srcs = ["TlulAdapterSram.sv"],
+    deps = [
+        "//third_party/ip/lowrisc:prim",
+        "//third_party/ip/lowrisc:tlul_adapter_sram",
+    ],
+    visibility = ["//visibility:public"],
+)
+
+verilog_library(
+    name = "uart",
+    srcs = ["Uart.sv"],
+    deps = [
+        "//third_party/ip/lowrisc:uart",
+    ],
+    visibility = ["//visibility:public"],
+)
diff --git a/hdl/verilog/ClockGate.v b/hdl/verilog/ClockGate.sv
similarity index 67%
rename from hdl/verilog/ClockGate.v
rename to hdl/verilog/ClockGate.sv
index f6b9e7c..02c7f09 100644
--- a/hdl/verilog/ClockGate.v
+++ b/hdl/verilog/ClockGate.sv
@@ -17,22 +17,12 @@
   input         enable,  // '1' passthrough, '0' disable.
   output        clk_o
 );
-// Note: Bypass clock gate for now. It causes FPGA build failures and
-// simulation issues
-assign clk_o = clk_i;
-/*
-reg clk_en;
-`ifdef FPGA
-    assign clk_o = clk_i;
-`else
-    // Capture 'enable' during low phase of the clock.
-    always @(clk_i or enable)
-    begin
-      if (~clk_i)
-      clk_en = enable;
-    end
 
-  assign clk_o = clk_i & clk_en;
-`endif
-*/
+prim_clock_gating u_cg(
+  .clk_i(clk_i),
+  .en_i(enable),
+  .test_en_i('0),
+  .clk_o(clk_o)
+);
+
 endmodule  // ClockGate
diff --git a/hdl/verilog/TlulAdapterSram.sv b/hdl/verilog/TlulAdapterSram.sv
new file mode 100644
index 0000000..e1a295c
--- /dev/null
+++ b/hdl/verilog/TlulAdapterSram.sv
@@ -0,0 +1,113 @@
+// Copyright 2024 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
+//
+//     http://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.
+
+module TlulAdapterSram
+import prim_mubi_pkg::mubi4_t;
+(
+  input clk_i,
+  input rst_ni,
+  input tl_i_a_valid,
+  input [2:0] tl_i_a_opcode,
+  input [2:0] tl_i_a_param,
+  input [5:0] tl_i_a_size,
+  input [9:0] tl_i_a_source,
+  input [31:0] tl_i_a_address,
+  input [31:0] tl_i_a_mask,
+  input [255:0] tl_i_a_data,
+  input [4:0] tl_i_a_user_rsvd,
+  input [3:0] tl_i_a_user_instr_type,
+  input [6:0] tl_i_a_user_cmd_intg,
+  input [6:0] tl_i_a_user_data_intg,
+  input tl_i_d_ready,
+  input [3:0] en_ifetch_i,
+  input gnt_i,
+  input [255:0] rdata_i,
+  input rvalid_i,
+  input [1:0] rerror_i,
+  output tl_o_d_valid,
+  output [2:0] tl_o_d_opcode,
+  output [2:0] tl_o_d_param,
+  output [5:0] tl_o_d_size,
+  output [9:0] tl_o_d_source,
+  output tl_o_d_sink,
+  output [255:0] tl_o_d_data,
+  output [6:0] tl_o_d_user_rsp_intg,
+  output [6:0] tl_o_d_user_data_intg,
+  output tl_o_d_error,
+  output tl_o_a_ready,
+  output req_o,
+  output [3:0] req_type_o,
+  output we_o,
+  output [16:0] addr_o,
+  output [255:0] wdata_o,
+  output [255:0] wmask_o,
+  output intg_error_o
+);
+
+mubi4_t en_ifetch_i_ = mubi4_t'(en_ifetch_i);
+
+tlul_adapter_sram #(
+  .SramAw(17),
+  .SramDw(256),
+  .Outstanding(1),
+  .ByteAccess(1),
+  .ErrOnRead(0),
+  .EnableDataIntgPt(0)
+) u_tlul_adapter_sram (
+  .clk_i(clk_i),
+  .rst_ni(rst_ni),
+  .tl_i(
+    {
+        tl_i_a_valid,
+        tl_i_a_opcode,
+        tl_i_a_param,
+        tl_i_a_size,
+        tl_i_a_source,
+        tl_i_a_address,
+        tl_i_a_mask,
+        tl_i_a_data,
+        tl_i_a_user_rsvd,
+        tl_i_a_user_instr_type,
+        tl_i_a_user_cmd_intg,
+        tl_i_a_user_data_intg,
+        tl_i_d_ready
+    }),
+  .tl_o({
+    tl_o_d_valid,
+    tl_o_d_opcode,
+    tl_o_d_param,
+    tl_o_d_size,
+    tl_o_d_source,
+    tl_o_d_sink,
+    tl_o_d_data,
+    tl_o_d_user_rsp_intg,
+    tl_o_d_user_data_intg,
+    tl_o_d_error,
+    tl_o_a_ready
+  }),
+  .en_ifetch_i(en_ifetch_i_),
+  .req_o(req_o),
+  .req_type_o(req_type_o),
+  .gnt_i(gnt_i),
+  .we_o(we_o),
+  .addr_o(addr_o),
+  .wdata_o(wdata_o),
+  .wmask_o(wmask_o),
+  .intg_error_o(intg_error_o),
+  .rdata_i(rdata_i),
+  .rvalid_i(rvalid_i),
+  .rerror_i(rerror_i)
+);
+
+endmodule
\ No newline at end of file
diff --git a/hdl/verilog/Uart.sv b/hdl/verilog/Uart.sv
new file mode 100644
index 0000000..b00a4b4
--- /dev/null
+++ b/hdl/verilog/Uart.sv
@@ -0,0 +1,109 @@
+// Copyright 2024 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
+//
+//     http://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.
+
+module Uart(
+  input clk_i,
+  input rst_ni,
+  input tl_i_a_valid,
+  input [2:0] tl_i_a_opcode,
+  input [2:0] tl_i_a_param,
+  input [5:0] tl_i_a_size,
+  input [9:0] tl_i_a_source,
+  input [31:0] tl_i_a_address,
+  input [31:0] tl_i_a_mask,
+  input [255:0] tl_i_a_data,
+  input [4:0] tl_i_a_user_rsvd,
+  input [3:0] tl_i_a_user_instr_type,
+  input [6:0] tl_i_a_user_cmd_intg,
+  input [6:0] tl_i_a_user_data_intg,
+  input tl_i_d_ready,
+  output tl_o_d_valid,
+  output [2:0] tl_o_d_opcode,
+  output [2:0] tl_o_d_param,
+  output [5:0] tl_o_d_size,
+  output [9:0] tl_o_d_source,
+  output tl_o_d_sink,
+  output [255:0] tl_o_d_data,
+  output [6:0] tl_o_d_user_rsp_intg,
+  output [6:0] tl_o_d_user_data_intg,
+  output tl_o_d_error,
+  output tl_o_a_ready,
+
+  input [3:0] alert_rx_i,
+  output [1:0] alert_tx_o,
+
+  input cio_rx_i,
+  output cio_tx_o,
+  output cio_tx_en_o,
+
+  output intr_tx_watermark_o,
+  output intr_rx_watermark_o,
+  output intr_tx_empty_o,
+  output intr_rx_overflow_o,
+  output intr_rx_frame_err_o,
+  output intr_rx_break_err_o,
+  output intr_rx_timeout_o,
+  output intr_rx_parity_err_o
+);
+
+uart #() u_uart (
+  .clk_i(clk_i),
+  .rst_ni(rst_ni),
+  .tl_i(
+    {
+        tl_i_a_valid,
+        tl_i_a_opcode,
+        tl_i_a_param,
+        tl_i_a_size,
+        tl_i_a_source,
+        tl_i_a_address,
+        tl_i_a_mask,
+        tl_i_a_data,
+        tl_i_a_user_rsvd,
+        tl_i_a_user_instr_type,
+        tl_i_a_user_cmd_intg,
+        tl_i_a_user_data_intg,
+        tl_i_d_ready
+    }),
+  .tl_o({
+    tl_o_d_valid,
+    tl_o_d_opcode,
+    tl_o_d_param,
+    tl_o_d_size,
+    tl_o_d_source,
+    tl_o_d_sink,
+    tl_o_d_data,
+    tl_o_d_user_rsp_intg,
+    tl_o_d_user_data_intg,
+    tl_o_d_error,
+    tl_o_a_ready
+  }),
+  .alert_rx_i(alert_rx_i),
+  .alert_tx_o(alert_tx_o),
+
+  .cio_rx_i(cio_rx_i),
+  .cio_tx_o(cio_tx_o),
+  .cio_tx_en_o(cio_tx_en_o),
+
+  .intr_tx_watermark_o(intr_tx_watermark_o),
+  .intr_rx_watermark_o(intr_rx_watermark_o),
+  .intr_tx_empty_o(intr_tx_empty_o),
+  .intr_rx_overflow_o(intr_rx_overflow_o),
+  .intr_rx_frame_err_o(intr_rx_frame_err_o),
+  .intr_rx_break_err_o(intr_rx_break_err_o),
+  .intr_rx_timeout_o(intr_rx_timeout_o),
+  .intr_rx_parity_err_o(intr_rx_parity_err_o)
+);
+
+endmodule
\ No newline at end of file
diff --git a/rules/repos.bzl b/rules/repos.bzl
index b6e8e55..6a70d75 100644
--- a/rules/repos.bzl
+++ b/rules/repos.bzl
@@ -80,3 +80,16 @@
         urls = ["https://repo1.maven.org/maven2/org/chipsalliance/llvm-firtool/1.52.0/llvm-firtool-1.52.0.jar"],
         build_file = "@kelvin_hw//third_party/llvm-firtool:BUILD.bazel",
     )
+
+    http_archive(
+        name = "lowrisc_opentitan",
+        sha256 = "cffed2c3c9c026ecb0b14a48b6cc300aa145bb2a316903dcb4cb7976ca8857af",
+        strip_prefix = "opentitan-f243e6802143374741739d2c164c4f2f61697669",
+        urls = ["https://github.com/lowrisc/opentitan/archive/f243e6802143374741739d2c164c4f2f61697669.zip"],
+        patches = [
+            "@kelvin_hw//third_party/ip/lowrisc:0001-Add-BUILD.bazel.patch",
+            "@kelvin_hw//third_party/ip/lowrisc:0002-Modify-TLUL-and-SRAM-adapter-for-ChAI.patch",
+            "@kelvin_hw//third_party/ip/lowrisc:0003-Modify-UART-for-ChAI.patch",
+        ],
+        patch_args = ["-p1"],
+    )
diff --git a/rules/verilator.bzl b/rules/verilator.bzl
index c00bad3..2f8fdd9 100644
--- a/rules/verilator.bzl
+++ b/rules/verilator.bzl
@@ -140,10 +140,16 @@
     args.add("--Mdir", verilator_output.path)
     args.add("--top-module", ctx.attr.module_top)
     args.add("--prefix", prefix)
+    verilog_dirs = dict()
+    for file in verilog_files:
+        verilog_dirs[file.dirname] = None
+    for vdir in verilog_dirs:
+        args.add("-I" + vdir)
     if ctx.attr.trace:
         args.add("--trace")
     for verilog_file in verilog_files:
         args.add(verilog_file.path)
+    args.add("-Wno-UNOPTFLAT")
     args.add_all(ctx.attr.vopts, expand_directories = False)
 
     ctx.actions.run(
diff --git a/tests/verilator_sim/BUILD b/tests/verilator_sim/BUILD
index 681096f..10c126e 100644
--- a/tests/verilator_sim/BUILD
+++ b/tests/verilator_sim/BUILD
@@ -66,6 +66,25 @@
     ],
 )
 
+cc_binary(
+    name = "chai_sim",
+    srcs = [
+        "chai/chai_if.h",
+        "chai/chai_tb.cc",
+    ],
+    deps = [
+        ":kelvin_if",
+        ":sim_libs",
+        "//hdl/chisel/src/chai:chai_cc_library",
+        "@com_google_absl//absl/flags:flag",
+        "@com_google_absl//absl/flags:parse",
+        "@com_google_absl//absl/flags:usage",
+        "@com_google_absl//absl/log",
+        "@com_google_absl//absl/log:check",
+    ],
+)
+
+
 cc_test(
     name = "dbus2axi_tb",
     size = "small",
diff --git a/tests/verilator_sim/chai/chai_if.h b/tests/verilator_sim/chai/chai_if.h
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/verilator_sim/chai/chai_if.h
diff --git a/tests/verilator_sim/chai/chai_tb.cc b/tests/verilator_sim/chai/chai_tb.cc
new file mode 100644
index 0000000..74fa133
--- /dev/null
+++ b/tests/verilator_sim/chai/chai_tb.cc
@@ -0,0 +1,245 @@
+// Copyright 2024 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
+//
+//     http://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.
+
+#include <fcntl.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+
+#include <algorithm>
+
+#include "VChAI.h"  // Generated
+#include "absl/flags/flag.h"
+#include "absl/flags/parse.h"
+#include "absl/flags/usage.h"
+#include "absl/log/check.h"
+#include "absl/log/log.h"
+#include "tests/verilator_sim/sysc_module.h"
+#include "tests/verilator_sim/sysc_tb.h"
+
+ABSL_FLAG(int, cycles, 10'000'000, "Simulation cycles");
+ABSL_FLAG(bool, trace, false, "Enable tracing");
+
+namespace {
+
+struct ChAI_tb : Sysc_tb {
+  sc_in<bool> io_halted;
+  sc_in<bool> io_fault;
+  using Sysc_tb::Sysc_tb;  // constructor
+
+  void posedge() {
+    check(!io_fault, "io_fault");
+    if (io_halted) sc_stop();
+  }
+};
+
+struct Memory : Sysc_module {
+  sc_out<sc_bv<17> > write_address;
+  sc_out<bool> write_enable;
+  sc_out<sc_bv<256> > write_data;
+  sc_out<bool> loadedn;
+
+  sc_bv<256> wdata = 0;
+
+  Memory(sc_module_name n, const char* path)
+      : Sysc_module(n), path_(path), offset_(0) {
+    int fd = open(path, 0);
+    CHECK(fd > 0);
+    struct stat sb;
+    CHECK(fstat(fd, &sb) == 0);
+    LOG(INFO) << "Input file size: " << sb.st_size;
+    size_ = sb.st_size;
+    if (size_ % 256 != 0) {
+      LOG(FATAL) << "Please align your file size to 256 bytes.";
+    }
+    void* data = mmap(nullptr, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
+    CHECK(data != MAP_FAILED);
+    close(fd);
+    data_ = reinterpret_cast<uint8_t*>(data);
+    data32_ = reinterpret_cast<uint32_t*>(data_);
+  }
+
+  ~Memory() {
+    LOG(INFO) << "Cycles at teardown: " << cycle_;
+    munmap(data_, size_);
+    data_ = nullptr;
+    data32_ = nullptr;
+  }
+
+  void eval() {
+    if (reset) {
+      cycle_ = 0;
+      loadedn = true;
+      write_address = 0;
+      write_enable = false;
+      write_data = 0;
+    }
+    if (clock->posedge()) {
+      cycle_++;
+      if (offset_ == size_) {
+        static bool logged = false;
+        if (!logged) {
+          LOG(INFO) << "[" << cycle_ << "] setting loadedn to false";
+          logged = true;
+        }
+        loadedn = false;
+        write_enable = false;
+      }
+      if (offset_ < size_) {
+        const size_t wordsPerWrite = 32 / sizeof(uint32_t);  // 32B / 4B
+        for (size_t i = 0; i < wordsPerWrite; i++) {
+          uint32_t val = data32_[(offset_ / sizeof(uint32_t)) + i];
+          wdata.set_word(i, val);
+        }
+        write_data.write(wdata);
+        write_enable = true;
+        write_address = offset_ >> 5;
+        offset_ += 32;  // 32 bytes == 8 words
+      }
+    }
+    if (cycle_ % 10000 == 0) {
+      LOG(INFO) << "Cycle " << cycle_;
+    }
+  }
+
+ private:
+  uint32_t cycle_ = 0;
+  const char* path_;
+  size_t offset_;     // bytes
+  size_t size_;       // bytes
+  uint8_t* data_;     // bytes
+  uint32_t* data32_;  // words
+};
+
+struct Uart : Sysc_module {
+  sc_in<bool> rx;
+  sc_out<bool> tx;
+  Uart(sc_module_name n) : Sysc_module(n) {}
+  uint64_t kBaudrate = 115200;
+  uint64_t kFrequencyHz = 10000000; // 10MHz
+  uint32_t nco_rx = static_cast<uint32_t>((kBaudrate << 20) / kFrequencyHz);
+  void eval() {
+    if (reset) {
+      last_rx_val_ = true;
+      uart_baud_ctr_ = 0;
+      baud_cnt_ = 0;
+      s_ = State::sIdle;
+      bit_in_pkt_ = 0;
+      rx_data_ = 0;
+    }
+    if (clock->posedge()) {
+      if (uart_baud_ctr_ & 0x10000) {
+        baud_cnt_++;
+      }
+      if (baud_cnt_ == 16) {
+        bool rx_val = rx;
+        bool edge = rx_val != last_rx_val_;
+        switch (s_) {
+          case State::sIdle: {
+            if (edge) {
+              s_ = State::sStarted;
+              bit_in_pkt_ = 0;
+            }
+            break;
+          }
+          case State::sStarted: {
+            if (bit_in_pkt_ == 8) {
+              LOG(INFO) << "UART val: " << (char)rx_data_;
+              rx_data_ = 0;
+              s_ = State::sIdle;
+            }
+            rx_data_ = (rx_data_ >> 1) | ((uint8_t)rx_val << 7);
+            bit_in_pkt_++;
+            break;
+          }
+        }
+        last_rx_val_ = rx_val;
+        baud_cnt_ = 0;
+      }
+      uart_baud_ctr_ = (uart_baud_ctr_ & 0xFFFF) + nco_rx;
+    }
+  }
+
+ private:
+  enum class State { sIdle, sStarted };
+  bool last_rx_val_ = true;
+  size_t uart_baud_ctr_ = 0;
+  size_t baud_cnt_ = 0;
+  State s_ = State::sIdle;
+  size_t bit_in_pkt_ = 0;
+  uint8_t rx_data_ = 0;
+};
+
+void ChAI_run(const char* name, const char* path, const int cycles,
+              const bool trace) {
+  VChAI chai(name);
+  ChAI_tb tb("ChAI_tb", cycles, /* random= */ false);
+  Memory mem("ChAI_mem", path);
+  Uart uart("ChAI_uart");
+
+  sc_signal<bool> io_halted, io_fault;
+  sc_signal<sc_bv<17> > io_sram_write_address;
+  sc_signal<bool> io_sram_write_enable;
+  sc_signal<sc_bv<256> > io_sram_write_data;
+  sc_signal<bool> mem_loadedn;
+  sc_signal<bool> uart_tx;  // Output from ChAI
+  sc_signal<bool> uart_rx;  // Input to ChAI
+
+  tb.io_halted(io_halted);
+  tb.io_fault(io_fault);
+
+  chai.io_clk_i(tb.clock);
+  chai.io_rst_ni(tb.resetn);
+  chai.io_sram_write_address(io_sram_write_address);
+  chai.io_sram_write_enable(io_sram_write_enable);
+  chai.io_sram_write_data(io_sram_write_data);
+  chai.io_finish(io_halted);
+  chai.io_fault(io_fault);
+  chai.io_freeze(mem_loadedn);
+  chai.io_uart_tx(uart_tx);
+  chai.io_uart_rx(uart_rx);
+
+  mem.clock(tb.clock);
+  mem.reset(tb.reset);
+  mem.write_address(io_sram_write_address);
+  mem.write_enable(io_sram_write_enable);
+  mem.write_data(io_sram_write_data);
+  mem.loadedn(mem_loadedn);
+
+  uart.clock(tb.clock);
+  uart.reset(tb.reset);
+  uart.rx(uart_tx);
+  uart.tx(uart_rx);
+
+  if (trace) {
+    tb.trace(chai);
+  }
+
+  tb.start();
+}
+
+}  // namespace
+
+extern "C" int sc_main(int argc, char** argv) {
+  absl::SetProgramUsageMessage("ChAI sim");
+  auto out_args = absl::ParseCommandLine(argc, argv);
+  argc = out_args.size();
+  argv = &out_args[0];
+  if (argc < 2) {
+    LOG(FATAL) << "Need an input file";
+  }
+  const char* path = argv[1];
+  ChAI_run(Sysc_tb::get_name(argv[0]), path, absl::GetFlag(FLAGS_cycles),
+           absl::GetFlag(FLAGS_trace));
+  return 0;
+}
\ No newline at end of file
diff --git a/tests/verilator_sim/matcha/kelvin_tb.cc b/tests/verilator_sim/matcha/kelvin_tb.cc
index fe1a765..5211117 100644
--- a/tests/verilator_sim/matcha/kelvin_tb.cc
+++ b/tests/verilator_sim/matcha/kelvin_tb.cc
@@ -78,16 +78,16 @@
   core.slog_addr(slog_addr);
   core.slog_data(slog_data);
 
-  core.cvalid(cvalid);
-  core.cready(cready);
-  core.cwrite(cwrite);
-  core.caddr(caddr);
-  core.cid(cid);
-  core.wdata(wdata);
-  core.wmask(wmask);
-  core.rvalid(rvalid);
-  core.rid(rid);
-  core.rdata(rdata);
+  core.mem_cvalid(cvalid);
+  core.mem_cready(cready);
+  core.mem_cwrite(cwrite);
+  core.mem_caddr(caddr);
+  core.mem_cid(cid);
+  core.mem_wdata(wdata);
+  core.mem_wmask(wmask);
+  core.mem_rvalid(rvalid);
+  core.mem_rid(rid);
+  core.mem_rdata(rdata);
 
   mif.clock(tb.clock);
   mif.reset(tb.reset);
diff --git a/third_party/ip/lowrisc/0001-Add-BUILD.bazel.patch b/third_party/ip/lowrisc/0001-Add-BUILD.bazel.patch
new file mode 100644
index 0000000..2ab35be
--- /dev/null
+++ b/third_party/ip/lowrisc/0001-Add-BUILD.bazel.patch
@@ -0,0 +1,37 @@
+From 35d1dec2153a5d5e9f45d86215708d046d05f85a Mon Sep 17 00:00:00 2001
+From: Alex Van Damme <atv@google.com>
+Date: Thu, 15 Feb 2024 11:25:11 -0800
+Subject: [PATCH 1/3] Add BUILD.bazel
+
+---
+ hw/BUILD.bazel | 18 ++++++++++++++++++
+ 1 file changed, 18 insertions(+)
+ create mode 100644 hw/BUILD.bazel
+
+diff --git a/hw/BUILD.bazel b/hw/BUILD.bazel
+new file mode 100644
+index 0000000000..7f502f35ea
+--- /dev/null
++++ b/hw/BUILD.bazel
+@@ -0,0 +1,18 @@
++# Copyright 2024 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
++#
++#     http://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.
++
++exports_files(
++    glob(["vendor/lowrisc_ibex/dv/uvm/core_ibex/common/prim/*.sv*"]),
++)
++
+-- 
+2.43.0.687.g38aa6559b0-goog
+
diff --git a/third_party/ip/lowrisc/0002-Modify-TLUL-and-SRAM-adapter-for-ChAI.patch b/third_party/ip/lowrisc/0002-Modify-TLUL-and-SRAM-adapter-for-ChAI.patch
new file mode 100644
index 0000000..3d104a1
--- /dev/null
+++ b/third_party/ip/lowrisc/0002-Modify-TLUL-and-SRAM-adapter-for-ChAI.patch
@@ -0,0 +1,180 @@
+From 617fc52a30d18725e50080e64bc6459fa24ca709 Mon Sep 17 00:00:00 2001
+From: Alex Van Damme <atv@google.com>
+Date: Thu, 15 Feb 2024 11:24:49 -0800
+Subject: [PATCH 2/3] Modify TLUL and SRAM adapter for ChAI
+
+---
+ hw/ip/tlul/rtl/tlul_adapter_reg.sv  | 12 +++++-------
+ hw/ip/tlul/rtl/tlul_adapter_sram.sv | 21 +++------------------
+ hw/ip/tlul/rtl/tlul_err.sv          | 15 ++++++++++-----
+ hw/ip/tlul/rtl/tlul_pkg.sv          | 18 ------------------
+ 4 files changed, 18 insertions(+), 48 deletions(-)
+
+diff --git a/hw/ip/tlul/rtl/tlul_adapter_reg.sv b/hw/ip/tlul/rtl/tlul_adapter_reg.sv
+index ac48750129..85e89d63f4 100644
+--- a/hw/ip/tlul/rtl/tlul_adapter_reg.sv
++++ b/hw/ip/tlul/rtl/tlul_adapter_reg.sv
+@@ -82,8 +82,8 @@ module tlul_adapter_reg
+ 
+   assign we_o    = wr_req & ~err_internal;
+   assign re_o    = rd_req & ~err_internal;
+-  assign wdata_o = tl_i.a_data;
+-  assign be_o    = tl_i.a_mask;
++  assign wdata_o = tl_i.a_data[RegDw-1:0];
++  assign be_o    = tl_i.a_mask[RegBw-1:0];
+ 
+   if (RegAw <= 2) begin : gen_only_one_reg
+     assign addr_o  = '0;
+@@ -161,7 +161,7 @@ module tlul_adapter_reg
+     d_size:   reqsz_q,
+     d_source: reqid_q,
+     d_sink:   '0,
+-    d_data:   rdata,
++    d_data:   {224'b0, rdata},
+     d_user:   '0,
+     d_error:  error
+   };
+@@ -201,14 +201,12 @@ module tlul_adapter_reg
+ 
+   // An instruction type transaction is only valid if en_ifetch is enabled
+   // If the instruction type is completely invalid, also considered an instruction error
+-  assign instr_error = prim_mubi_pkg::mubi4_test_invalid(tl_i.a_user.instr_type) |
+-                       (prim_mubi_pkg::mubi4_test_true_strict(tl_i.a_user.instr_type) &
+-                        prim_mubi_pkg::mubi4_test_false_loose(en_ifetch_i));
++  assign instr_error = '0;
+ 
+   assign err_internal = addr_align_err | malformed_meta_err | tl_err | instr_error | intg_error;
+ 
+   // Don't allow unsupported values.
+-  assign malformed_meta_err = tl_a_user_chk(tl_i.a_user);
++  assign malformed_meta_err = '0;
+ 
+   // addr_align_err
+   //    Raised if addr isn't aligned with the size
+diff --git a/hw/ip/tlul/rtl/tlul_adapter_sram.sv b/hw/ip/tlul/rtl/tlul_adapter_sram.sv
+index ec764cbefa..8f41f38581 100644
+--- a/hw/ip/tlul/rtl/tlul_adapter_sram.sv
++++ b/hw/ip/tlul/rtl/tlul_adapter_sram.sv
+@@ -106,7 +106,7 @@ module tlul_adapter_sram
+   //    Here it checks any partial write if ByteAccess isn't allowed.
+   assign wr_attr_error = (tl_i.a_opcode == PutFullData || tl_i.a_opcode == PutPartialData)
+                          ? ((ByteAccess == 0) ?
+-                           (tl_i.a_mask != '1 || tl_i.a_size != 2'h2) : 1'b0)
++                           (tl_i.a_mask != '1 || tl_i.a_size != 6'h2) : 1'b0)
+                            : 1'b0;
+ 
+   // An instruction type transaction is only valid if en_ifetch is enabled
+@@ -268,24 +268,9 @@ module tlul_adapter_sram
+                                  DataWhenInstrError :
+                                  DataWhenError;
+ 
+-  // Since DataWhenInstrError and DataWhenError can be arbitrary parameters
+-  // we statically calculate the correct integrity values for these parameters here so that
+-  // they do not have to be supplied externally.
+-  logic [top_pkg::TL_DW-1:0] unused_instr, unused_data;
+-  logic [DataIntgWidth-1:0] error_instr_integ, error_data_integ;
+-  tlul_data_integ_enc u_tlul_data_integ_enc_instr (
+-    .data_i(DataMaxWidth'(DataWhenInstrError)),
+-    .data_intg_o({error_instr_integ, unused_instr})
+-  );
+-  tlul_data_integ_enc u_tlul_data_integ_enc_data (
+-    .data_i(DataMaxWidth'(DataWhenError)),
+-    .data_intg_o({error_data_integ, unused_data})
+-  );
+ 
+   logic [DataIntgWidth-1:0] error_blanking_integ;
+-  assign error_blanking_integ = (prim_mubi_pkg::mubi4_test_true_strict(reqfifo_rdata.instr_type)) ?
+-                                 error_instr_integ :
+-                                 error_data_integ;
++  assign error_blanking_integ = '0;
+ 
+   logic [top_pkg::TL_DW-1:0] d_data;
+   assign d_data = (vld_rd_rsp & ~d_error) ? rspfifo_rdata.data   // valid read
+@@ -445,7 +430,7 @@ module tlul_adapter_sram
+     error     : rerror_i[1] // Only care for Uncorrectable error
+   };
+   assign rspfifo_rready = (reqfifo_rdata.op == OpRead & ~reqfifo_rdata.error)
+-                        ? reqfifo_rready : 1'b0 ;
++                        ? reqfifo_rready : 1'b1 ;
+ 
+   // This module only cares about uncorrectable errors.
+   logic unused_rerror;
+diff --git a/hw/ip/tlul/rtl/tlul_err.sv b/hw/ip/tlul/rtl/tlul_err.sv
+index c887cc387b..6cb3ea380a 100644
+--- a/hw/ip/tlul/rtl/tlul_err.sv
++++ b/hw/ip/tlul/rtl/tlul_err.sv
+@@ -29,11 +29,10 @@ module tlul_err import tlul_pkg::*; (
+ 
+   // An instruction type transaction cannot be write
+   logic instr_wr_err;
+-  assign instr_wr_err = prim_mubi_pkg::mubi4_test_true_strict(tl_i.a_user.instr_type) &
+-                        (op_full | op_partial);
++  assign instr_wr_err = '0;
+ 
+   logic instr_type_err;
+-  assign instr_type_err = prim_mubi_pkg::mubi4_test_invalid(tl_i.a_user.instr_type);
++  assign instr_type_err = '0;
+ 
+   // Anything that doesn't fall into the permitted category, it raises an error
+   assign err_o = ~(opcode_allowed & a_config_allowed) | instr_wr_err | instr_type_err;
+@@ -68,8 +67,8 @@ module tlul_err import tlul_pkg::*; (
+         'h1: begin // 2 Byte
+           addr_sz_chk  = ~tl_i.a_address[0];
+           // check inactive lanes if lower 2B, check a_mask[3:2], if uppwer 2B, a_mask[1:0]
+-          mask_chk     = (tl_i.a_address[1]) ? ~|(tl_i.a_mask & 4'b0011)
+-                       : ~|(tl_i.a_mask & 4'b1100);
++          mask_chk     = (tl_i.a_address[1]) ? ~|(tl_i.a_mask & 32'b0011)
++                       : ~|(tl_i.a_mask & 32'b1100);
+           fulldata_chk = (tl_i.a_address[1]) ? &tl_i.a_mask[3:2] : &tl_i.a_mask[1:0] ;
+         end
+ 
+@@ -79,6 +78,12 @@ module tlul_err import tlul_pkg::*; (
+           fulldata_chk = &tl_i.a_mask[3:0];
+         end
+ 
++        'h5: begin  // 32 Byte
++          addr_sz_chk  = 1'b1;
++          mask_chk     = 1'b1;
++          fulldata_chk = 1'b1;
++        end
++
+         default: begin // else
+           addr_sz_chk  = 1'b0;
+           mask_chk     = 1'b0;
+diff --git a/hw/ip/tlul/rtl/tlul_pkg.sv b/hw/ip/tlul/rtl/tlul_pkg.sv
+index 4e9401fdf9..dae9671f25 100644
+--- a/hw/ip/tlul/rtl/tlul_pkg.sv
++++ b/hw/ip/tlul/rtl/tlul_pkg.sv
+@@ -179,17 +179,6 @@ package tlul_pkg;
+    return cmd_intg;
+   endfunction  // get_cmd_intg
+ 
+-  // calculate ecc for data checking
+-  function automatic logic [DataIntgWidth-1:0] get_data_intg(logic [top_pkg::TL_DW-1:0] data);
+-    logic [DataIntgWidth-1:0] data_intg;
+-    logic [top_pkg::TL_DW-1:0] unused_data;
+-    logic [DataIntgWidth + top_pkg::TL_DW - 1 : 0] enc_data;
+-    enc_data = prim_secded_pkg::prim_secded_inv_39_32_enc(data);
+-    data_intg = enc_data[DataIntgWidth + top_pkg::TL_DW - 1 : top_pkg::TL_DW];
+-    unused_data = enc_data[top_pkg::TL_DW - 1 : 0];
+-    return data_intg;
+-  endfunction  // get_data_intg
+-
+   // return inverted integrity for command payload
+   function automatic logic [H2DCmdIntgWidth-1:0] get_bad_cmd_intg(tl_h2d_t tl);
+     logic [H2DCmdIntgWidth-1:0] cmd_intg;
+@@ -197,11 +186,4 @@ package tlul_pkg;
+     return ~cmd_intg;
+   endfunction // get_bad_cmd_intg
+ 
+-  // return inverted integrity for data payload
+-  function automatic logic [H2DCmdIntgWidth-1:0] get_bad_data_intg(logic [top_pkg::TL_DW-1:0] data);
+-    logic [H2DCmdIntgWidth-1:0] data_intg;
+-    data_intg = get_data_intg(data);
+-    return ~data_intg;
+-  endfunction // get_bad_data_intg
+-
+ endpackage
+-- 
+2.43.0.687.g38aa6559b0-goog
+
diff --git a/third_party/ip/lowrisc/0003-Modify-UART-for-ChAI.patch b/third_party/ip/lowrisc/0003-Modify-UART-for-ChAI.patch
new file mode 100644
index 0000000..a68a3e7
--- /dev/null
+++ b/third_party/ip/lowrisc/0003-Modify-UART-for-ChAI.patch
@@ -0,0 +1,84 @@
+From 37adcc443c2d7879079ca6e634f2992dbc9bfac8 Mon Sep 17 00:00:00 2001
+From: Alex Van Damme <atv@google.com>
+Date: Wed, 14 Feb 2024 15:29:54 -0800
+Subject: [PATCH 3/3] Modify UART for ChAI
+
+---
+ hw/ip/uart/rtl/uart_reg_pkg.sv | 28 ++++++++++++++--------------
+ hw/ip/uart/rtl/uart_reg_top.sv |  7 ++-----
+ 2 files changed, 16 insertions(+), 19 deletions(-)
+
+diff --git a/hw/ip/uart/rtl/uart_reg_pkg.sv b/hw/ip/uart/rtl/uart_reg_pkg.sv
+index 355afaff1b..8a44cd5af8 100644
+--- a/hw/ip/uart/rtl/uart_reg_pkg.sv
++++ b/hw/ip/uart/rtl/uart_reg_pkg.sv
+@@ -10,7 +10,7 @@ package uart_reg_pkg;
+   parameter int NumAlerts = 1;
+ 
+   // Address widths within the block
+-  parameter int BlockAw = 6;
++  parameter int BlockAw = 9;
+ 
+   ////////////////////////////
+   // Typedefs for registers //
+@@ -324,19 +324,19 @@ package uart_reg_pkg;
+   } uart_hw2reg_t;
+ 
+   // Register offsets
+-  parameter logic [BlockAw-1:0] UART_INTR_STATE_OFFSET = 6'h 0;
+-  parameter logic [BlockAw-1:0] UART_INTR_ENABLE_OFFSET = 6'h 4;
+-  parameter logic [BlockAw-1:0] UART_INTR_TEST_OFFSET = 6'h 8;
+-  parameter logic [BlockAw-1:0] UART_ALERT_TEST_OFFSET = 6'h c;
+-  parameter logic [BlockAw-1:0] UART_CTRL_OFFSET = 6'h 10;
+-  parameter logic [BlockAw-1:0] UART_STATUS_OFFSET = 6'h 14;
+-  parameter logic [BlockAw-1:0] UART_RDATA_OFFSET = 6'h 18;
+-  parameter logic [BlockAw-1:0] UART_WDATA_OFFSET = 6'h 1c;
+-  parameter logic [BlockAw-1:0] UART_FIFO_CTRL_OFFSET = 6'h 20;
+-  parameter logic [BlockAw-1:0] UART_FIFO_STATUS_OFFSET = 6'h 24;
+-  parameter logic [BlockAw-1:0] UART_OVRD_OFFSET = 6'h 28;
+-  parameter logic [BlockAw-1:0] UART_VAL_OFFSET = 6'h 2c;
+-  parameter logic [BlockAw-1:0] UART_TIMEOUT_CTRL_OFFSET = 6'h 30;
++  parameter logic [BlockAw-1:0] UART_INTR_STATE_OFFSET = 9'h 0;
++  parameter logic [BlockAw-1:0] UART_INTR_ENABLE_OFFSET = 9'h 20;
++  parameter logic [BlockAw-1:0] UART_INTR_TEST_OFFSET = 9'h 40;
++  parameter logic [BlockAw-1:0] UART_ALERT_TEST_OFFSET = 9'h 60;
++  parameter logic [BlockAw-1:0] UART_CTRL_OFFSET = 9'h 80;
++  parameter logic [BlockAw-1:0] UART_STATUS_OFFSET = 9'h a0;
++  parameter logic [BlockAw-1:0] UART_RDATA_OFFSET = 9'h c0;
++  parameter logic [BlockAw-1:0] UART_WDATA_OFFSET = 9'h e0;
++  parameter logic [BlockAw-1:0] UART_FIFO_CTRL_OFFSET = 9'h 100;
++  parameter logic [BlockAw-1:0] UART_FIFO_STATUS_OFFSET = 9'h120 ;
++  parameter logic [BlockAw-1:0] UART_OVRD_OFFSET = 9'h 140;
++  parameter logic [BlockAw-1:0] UART_VAL_OFFSET = 9'h 160;
++  parameter logic [BlockAw-1:0] UART_TIMEOUT_CTRL_OFFSET = 9'h180;
+ 
+   // Reset values for hwext registers and their fields
+   parameter logic [7:0] UART_INTR_TEST_RESVAL = 8'h 0;
+diff --git a/hw/ip/uart/rtl/uart_reg_top.sv b/hw/ip/uart/rtl/uart_reg_top.sv
+index 6b6952f5d8..b105550cc4 100644
+--- a/hw/ip/uart/rtl/uart_reg_top.sv
++++ b/hw/ip/uart/rtl/uart_reg_top.sv
+@@ -24,7 +24,7 @@ module uart_reg_top (
+ 
+   import uart_reg_pkg::* ;
+ 
+-  localparam int AW = 6;
++  localparam int AW = 9;
+   localparam int DW = 32;
+   localparam int DBW = DW/8;                    // Byte Width
+ 
+@@ -48,10 +48,7 @@ module uart_reg_top (
+ 
+   // incoming payload check
+   logic intg_err;
+-  tlul_cmd_intg_chk u_chk (
+-    .tl_i(tl_i),
+-    .err_o(intg_err)
+-  );
++  assign intg_err = '0;
+ 
+   // also check for spurious write enables
+   logic reg_we_err;
+-- 
+2.43.0.687.g38aa6559b0-goog
+
diff --git a/third_party/ip/lowrisc/BUILD b/third_party/ip/lowrisc/BUILD
new file mode 100644
index 0000000..0a97a9c
--- /dev/null
+++ b/third_party/ip/lowrisc/BUILD
@@ -0,0 +1,136 @@
+# Copyright 2024 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
+#
+#     http://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.
+
+load("@rules_hdl//verilog:providers.bzl", "verilog_library")
+
+verilog_library(
+    name = "prim_generic",
+    srcs = [
+        "@lowrisc_opentitan//hw/ip/prim_generic:rtl/prim_generic_buf.sv",
+        "@lowrisc_opentitan//hw/ip/prim_generic:rtl/prim_generic_clock_gating.sv",
+        "@lowrisc_opentitan//hw/ip/prim_generic:rtl/prim_generic_flop.sv",
+    ],
+    visibility = ["//visibility:public"],
+)
+
+verilog_library(
+    name = "prim_xilinx",
+    srcs = [
+        "@lowrisc_opentitan//hw/ip/prim_xilinx:rtl/prim_xilinx_clock_gating.sv",
+    ],
+    visibility = ["//visibility:public"],
+)
+
+verilog_library(
+    name = "prim",
+    # Do not sort: "*_pkg" comes first.
+    srcs = [
+        "@lowrisc_opentitan//hw:vendor/lowrisc_ibex/dv/uvm/core_ibex/common/prim/prim_pkg.sv",
+        "@lowrisc_opentitan//hw/ip/prim:rtl/prim_alert_pkg.sv",
+        "@lowrisc_opentitan//hw/ip/prim:rtl/prim_mubi_pkg.sv",
+        "@lowrisc_opentitan//hw/ip/prim:rtl/prim_secded_pkg.sv",
+        "@lowrisc_opentitan//hw/ip/prim:rtl/prim_subreg_pkg.sv",
+        "@lowrisc_opentitan//hw/ip/prim:rtl/prim_util_pkg.sv",
+        "@lowrisc_opentitan//hw/ip/prim:rtl/prim_alert_sender.sv",
+        "@lowrisc_opentitan//hw/ip/prim:rtl/prim_assert.sv",
+        # Verilator
+        "@lowrisc_opentitan//hw/ip/prim:rtl/prim_assert_dummy_macros.svh",
+        # Synth
+        # "@lowrisc_opentitan//hw/ip/prim:rtl/prim_assert_standard_macros.svh",
+        "@lowrisc_opentitan//hw/ip/prim:rtl/prim_assert_sec_cm.svh",
+        "@lowrisc_opentitan//hw:vendor/lowrisc_ibex/dv/uvm/core_ibex/common/prim/prim_buf.sv",
+        "@lowrisc_opentitan//hw:vendor/lowrisc_ibex/dv/uvm/core_ibex/common/prim/prim_clock_gating.sv",
+        "@lowrisc_opentitan//hw/ip/prim:rtl/prim_count.sv",
+        "@lowrisc_opentitan//hw/ip/prim:rtl/prim_diff_decode.sv",
+        "@lowrisc_opentitan//hw/ip/prim:rtl/prim_fifo_sync.sv",
+        "@lowrisc_opentitan//hw/ip/prim:rtl/prim_fifo_sync_cnt.sv",
+        "@lowrisc_opentitan//hw:vendor/lowrisc_ibex/dv/uvm/core_ibex/common/prim/prim_flop.sv",
+        "@lowrisc_opentitan//hw/ip/prim:rtl/prim_flop_2sync.sv",
+        "@lowrisc_opentitan//hw/ip/prim:rtl/prim_flop_macros.sv",
+        "@lowrisc_opentitan//hw/ip/prim:rtl/prim_intr_hw.sv",
+        "@lowrisc_opentitan//hw/ip/prim:rtl/prim_onehot_check.sv",
+        "@lowrisc_opentitan//hw/ip/prim:rtl/prim_reg_we_check.sv",
+        "@lowrisc_opentitan//hw/ip/prim:rtl/prim_sec_anchor_buf.sv",
+        "@lowrisc_opentitan//hw/ip/prim:rtl/prim_sec_anchor_flop.sv",
+        "@lowrisc_opentitan//hw/ip/prim:rtl/prim_secded_inv_64_57_dec.sv",
+        "@lowrisc_opentitan//hw/ip/prim:rtl/prim_secded_inv_39_32_dec.sv",
+        "@lowrisc_opentitan//hw/ip/prim:rtl/prim_secded_inv_39_32_enc.sv",
+        "@lowrisc_opentitan//hw/ip/prim:rtl/prim_secded_inv_64_57_enc.sv",
+        "@lowrisc_opentitan//hw/ip/prim:rtl/prim_subreg.sv",
+        "@lowrisc_opentitan//hw/ip/prim:rtl/prim_subreg_arb.sv",
+        "@lowrisc_opentitan//hw/ip/prim:rtl/prim_subreg_ext.sv",
+    ],
+    deps = [
+        ":prim_generic",
+        ":prim_xilinx",
+    ],
+    visibility = ["//visibility:public"],
+)
+
+verilog_library(
+    name = "top",
+    srcs = [
+        "top_pkg.sv",
+    ],
+    visibility = ["//visibility:public"],
+)
+
+verilog_library(
+    name = "tlul",
+    # Do not sort: "tlul_pkg" comes first.
+    srcs = [
+        "@lowrisc_opentitan//hw/ip/tlul:rtl/tlul_pkg.sv",
+        "@lowrisc_opentitan//hw/ip/tlul:rtl/tlul_adapter_reg.sv",
+        "@lowrisc_opentitan//hw/ip/tlul:rtl/tlul_cmd_intg_chk.sv",
+        "@lowrisc_opentitan//hw/ip/tlul:rtl/tlul_data_integ_dec.sv",
+        "@lowrisc_opentitan//hw/ip/tlul:rtl/tlul_data_integ_enc.sv",
+        "@lowrisc_opentitan//hw/ip/tlul:rtl/tlul_err.sv",
+        "@lowrisc_opentitan//hw/ip/tlul:rtl/tlul_rsp_intg_gen.sv",
+    ],
+    deps = [
+        ":top",
+        ":prim",
+    ],
+    visibility = ["//visibility:public"],
+)
+
+verilog_library(
+    name = "tlul_adapter_sram",
+    srcs = [
+        "@lowrisc_opentitan//hw/ip/tlul:rtl/tlul_adapter_sram.sv",
+        "@lowrisc_opentitan//hw/ip/tlul:rtl/tlul_sram_byte.sv",
+    ],
+    deps = [
+        ":prim",
+        ":tlul",
+    ],
+    visibility = ["//visibility:public"],
+)
+
+verilog_library(
+    name = "uart",
+    srcs = [
+        "@lowrisc_opentitan//hw/ip/uart:rtl/uart_reg_pkg.sv",
+        "@lowrisc_opentitan//hw/ip/uart:rtl/uart_core.sv",
+        "@lowrisc_opentitan//hw/ip/uart:rtl/uart_reg_top.sv",
+        "@lowrisc_opentitan//hw/ip/uart:rtl/uart_rx.sv",
+        "@lowrisc_opentitan//hw/ip/uart:rtl/uart_tx.sv",
+        "@lowrisc_opentitan//hw/ip/uart:rtl/uart.sv",
+    ],
+    deps = [
+        ":prim",
+        ":tlul",
+    ],
+    visibility = ["//visibility:public"],
+)
diff --git a/third_party/ip/lowrisc/top_pkg.sv b/third_party/ip/lowrisc/top_pkg.sv
new file mode 100644
index 0000000..0af4c75
--- /dev/null
+++ b/third_party/ip/lowrisc/top_pkg.sv
@@ -0,0 +1,27 @@
+// Copyright 2024 Google LLC
+// Copyright lowRISC contributors
+//
+// 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
+//
+//      http://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.
+
+package top_pkg;
+
+localparam int TL_AW=32;
+localparam int TL_DW=256;    // = TL_DBW * 8; TL_DBW must be a power-of-two
+localparam int TL_AIW=10;    // a_source, d_source
+localparam int TL_DIW=1;    // d_sink
+localparam int TL_AUW=21;   // a_user
+localparam int TL_DUW=14;   // d_user
+localparam int TL_DBW=(TL_DW>>3);
+localparam int TL_SZW=6;
+
+endpackage