feat(fpga): Add supporting IPs for Kelvin SoC

Change-Id: I127d67f8c0e87d13972c7b804ba8db8dc4ffc202
diff --git a/fpga/ip/kelvin_tlul/BUILD b/fpga/ip/kelvin_tlul/BUILD
new file mode 100644
index 0000000..d78f918
--- /dev/null
+++ b/fpga/ip/kelvin_tlul/BUILD
@@ -0,0 +1,23 @@
+# 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
+#
+#     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(default_visibility = ["//visibility:public"])
+
+filegroup(
+    name = "rtl_files",
+    srcs = glob([
+        "*.sv",
+        "*.core",
+    ]),
+)
diff --git a/fpga/ip/kelvin_tlul/kelvin_tlul.core b/fpga/ip/kelvin_tlul/kelvin_tlul.core
new file mode 100644
index 0000000..37336d3
--- /dev/null
+++ b/fpga/ip/kelvin_tlul/kelvin_tlul.core
@@ -0,0 +1,21 @@
+CAPI=2:
+name: "kelvinv2:ip:kelvin_tlul:0.1"
+description: "Kelvin TL-UL components"
+
+filesets:
+  rtl:
+    depend:
+      - "lowrisc:tlul:headers"
+    files:
+      - kelvin_tlul_pkg_128.sv
+      - kelvin_tlul_pkg_32.sv
+      - tlul_fifo_async_128.sv
+      - tlul_fifo_sync_128.sv
+      - tlul_socket_1n_128.sv
+      - tlul_socket_m1_128.sv
+    file_type: systemVerilogSource
+
+targets:
+  default:
+    filesets:
+      - rtl
diff --git a/fpga/ip/kelvin_tlul/kelvin_tlul_pkg_128.sv b/fpga/ip/kelvin_tlul/kelvin_tlul_pkg_128.sv
new file mode 100644
index 0000000..9239448
--- /dev/null
+++ b/fpga/ip/kelvin_tlul/kelvin_tlul_pkg_128.sv
@@ -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
+//
+//     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_tlul_pkg_128;
+  import tlul_pkg::*;
+  import top_pkg::*;
+
+  parameter ArbiterImpl = "PPC";
+
+  localparam int TL_DW = 128;
+  localparam int TL_DBW = TL_DW / 8;
+  localparam int TL_SZW = $clog2(TL_DW);
+  localparam int TL_AIW = 8;
+
+  typedef struct packed {
+    logic [RsvdWidth - 1 : 0] rsvd;
+    prim_mubi_pkg::mubi4_t instr_type;
+    logic [H2DCmdIntgWidth - 1 : 0] cmd_intg;
+    logic [DataIntgWidth - 1 : 0] data_intg;
+  } tl_a_user_t;
+
+  typedef struct packed {
+    logic a_valid;
+    tl_a_op_e a_opcode;
+    logic [2 : 0] a_param;
+    logic [TL_SZW - 1 : 0] a_size;
+    logic [TL_AIW - 1 : 0] a_source;
+    logic [TL_AW - 1 : 0] a_address;
+    logic [TL_DBW - 1 : 0] a_mask;
+    logic [TL_DW - 1 : 0] a_data;
+    tl_a_user_t a_user;
+    logic d_ready;
+  } tl_h2d_t;
+
+  typedef struct packed {
+    logic d_valid;
+    tl_d_op_e d_opcode;
+    logic [2 : 0] d_param;
+    logic [TL_SZW - 1 : 0] d_size;
+    logic [TL_AIW - 1 : 0] d_source;
+    logic [TL_DIW - 1 : 0] d_sink;
+    logic [TL_DW - 1 : 0] d_data;
+    tl_d_user_t d_user;
+    logic d_error;
+    logic a_ready;
+  } tl_d2h_t;
+
+  localparam logic [top_pkg::TL_DW - 1 : 0] BlankedAData = {
+                                                top_pkg::TL_DW{1'b1}};
+
+  // 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;
+    cmd_intg = get_cmd_intg(tl);
+    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
diff --git a/fpga/ip/kelvin_tlul/kelvin_tlul_pkg_32.sv b/fpga/ip/kelvin_tlul/kelvin_tlul_pkg_32.sv
new file mode 100644
index 0000000..5a989db
--- /dev/null
+++ b/fpga/ip/kelvin_tlul/kelvin_tlul_pkg_32.sv
@@ -0,0 +1,45 @@
+// 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
+//
+//     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_tlul_pkg_32;
+
+  import tlul_pkg::*;
+  import top_pkg::*;
+
+  typedef struct packed {
+    logic a_valid;
+    tl_a_op_e a_opcode;
+    logic [2 : 0] a_param;
+    logic [TL_SZW - 1 : 0] a_size;
+    logic [TL_AIW - 1 : 0] a_source;
+    logic [TL_AW - 1 : 0] a_address;
+    logic [TL_DBW - 1 : 0] a_mask;
+    logic [TL_DW - 1 : 0] a_data;
+    tl_a_user_t a_user;
+    logic d_ready;
+  } tl_h2d_t;
+
+  typedef struct packed {
+    logic d_valid;
+    tl_d_op_e d_opcode;
+    logic [2 : 0] d_param;
+    logic [TL_SZW - 1 : 0] d_size;
+    logic [TL_AIW - 1 : 0] d_source;
+    logic [TL_DIW - 1 : 0] d_sink;
+    logic [TL_DW - 1 : 0] d_data;
+    tl_d_user_t d_user;
+    logic d_error;
+    logic a_ready;
+  } tl_d2h_t;
+endpackage
diff --git a/fpga/ip/kelvin_tlul/tlul_fifo_async_128.sv b/fpga/ip/kelvin_tlul/tlul_fifo_async_128.sv
new file mode 100644
index 0000000..22894bb
--- /dev/null
+++ b/fpga/ip/kelvin_tlul/tlul_fifo_async_128.sv
@@ -0,0 +1,71 @@
+// Copyright lowRISC contributors (OpenTitan project).
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+//
+// TL-UL fifo, used to add elasticity or an asynchronous clock crossing
+// to an TL-UL bus.  This instantiates two FIFOs, one for the request side,
+// and one for the response side.
+
+`include "prim_assert.sv"
+
+module tlul_fifo_async_128
+    #(parameter int unsigned ReqDepth = 4,
+      parameter int unsigned RspDepth = 4)
+    (input clk_h_i,
+     input rst_h_ni,
+     input clk_d_i,
+     input rst_d_ni,
+     input kelvin_tlul_pkg_128::tl_h2d_t tl_h_i,
+     output kelvin_tlul_pkg_128::tl_d2h_t tl_h_o,
+     output kelvin_tlul_pkg_128::tl_h2d_t tl_d_o,
+     input kelvin_tlul_pkg_128::tl_d2h_t tl_d_i);
+
+  // Put everything on the request side into one FIFO
+  localparam int unsigned REQFIFO_WIDTH =
+                              $bits(kelvin_tlul_pkg_128::tl_h2d_t) - 2;
+
+  prim_fifo_async #(.Width(REQFIFO_WIDTH),
+                    .Depth(ReqDepth),
+                    .OutputZeroIfInvalid(1))
+      reqfifo(.clk_wr_i(clk_h_i),
+              .rst_wr_ni(rst_h_ni),
+              .clk_rd_i(clk_d_i),
+              .rst_rd_ni(rst_d_ni),
+              .wvalid_i(tl_h_i.a_valid),
+              .wready_o(tl_h_o.a_ready),
+              .wdata_i({tl_h_i.a_opcode, tl_h_i.a_param, tl_h_i.a_size,
+                        tl_h_i.a_source, tl_h_i.a_address, tl_h_i.a_mask,
+                        tl_h_i.a_data, tl_h_i.a_user}),
+              .rvalid_o(tl_d_o.a_valid),
+              .rready_i(tl_d_i.a_ready),
+              .rdata_o({tl_d_o.a_opcode, tl_d_o.a_param, tl_d_o.a_size,
+                        tl_d_o.a_source, tl_d_o.a_address, tl_d_o.a_mask,
+                        tl_d_o.a_data, tl_d_o.a_user}),
+              .wdepth_o(),
+              .rdepth_o());
+
+  // Put everything on the response side into the other FIFO
+
+  localparam int unsigned RSPFIFO_WIDTH =
+                              $bits(kelvin_tlul_pkg_128::tl_d2h_t) - 2;
+
+  prim_fifo_async #(.Width(RSPFIFO_WIDTH),
+                    .Depth(RspDepth),
+                    .OutputZeroIfInvalid(1))
+      rspfifo(.clk_wr_i(clk_d_i),
+              .rst_wr_ni(rst_d_ni),
+              .clk_rd_i(clk_h_i),
+              .rst_rd_ni(rst_h_ni),
+              .wvalid_i(tl_d_i.d_valid),
+              .wready_o(tl_d_o.d_ready),
+              .wdata_i({tl_d_i.d_opcode, tl_d_i.d_param, tl_d_i.d_size,
+                        tl_d_i.d_source, tl_d_i.d_sink, tl_d_i.d_data,
+                        tl_d_i.d_user, tl_d_i.d_error}),
+              .rvalid_o(tl_h_o.d_valid),
+              .rready_i(tl_h_i.d_ready),
+              .rdata_o({tl_h_o.d_opcode, tl_h_o.d_param, tl_h_o.d_size,
+                        tl_h_o.d_source, tl_h_o.d_sink, tl_h_o.d_data,
+                        tl_h_o.d_user, tl_h_o.d_error}),
+              .wdepth_o(),
+              .rdepth_o());
+endmodule
diff --git a/fpga/ip/kelvin_tlul/tlul_fifo_sync_128.sv b/fpga/ip/kelvin_tlul/tlul_fifo_sync_128.sv
new file mode 100644
index 0000000..af404f1
--- /dev/null
+++ b/fpga/ip/kelvin_tlul/tlul_fifo_sync_128.sv
@@ -0,0 +1,78 @@
+// Copyright lowRISC contributors (OpenTitan project).
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+//
+// TL-UL fifo, used to add elasticity or an asynchronous clock crossing
+// to an TL-UL bus.  This instantiates two FIFOs, one for the request side,
+// and one for the response side.
+
+module tlul_fifo_sync_128
+    #(parameter bit ReqPass = 1'b1,
+      parameter bit RspPass = 1'b1,
+      parameter int unsigned ReqDepth = 2,
+      parameter int unsigned RspDepth = 2,
+      parameter int unsigned SpareReqW = 1,
+      parameter int unsigned SpareRspW = 1)
+    (input clk_i,
+     input rst_ni,
+     input kelvin_tlul_pkg_128::tl_h2d_t tl_h_i,
+     output kelvin_tlul_pkg_128::tl_d2h_t tl_h_o,
+     output kelvin_tlul_pkg_128::tl_h2d_t tl_d_o,
+     input kelvin_tlul_pkg_128::tl_d2h_t tl_d_i,
+     input [SpareReqW - 1 : 0] spare_req_i,
+     output [SpareReqW - 1 : 0] spare_req_o,
+     input [SpareRspW - 1 : 0] spare_rsp_i,
+     output [SpareRspW - 1 : 0] spare_rsp_o);
+  import kelvin_tlul_pkg_128::*;
+  // Put everything on the request side into one FIFO
+  localparam int unsigned REQFIFO_WIDTH = $bits(kelvin_tlul_pkg_128::tl_h2d_t) -
+                                          2 + SpareReqW;
+
+  prim_fifo_sync #(.Width(REQFIFO_WIDTH),
+                   .Pass(ReqPass),
+                   .Depth(ReqDepth))
+      reqfifo(.clk_i,
+              .rst_ni,
+              .clr_i(1'b0),
+              .wvalid_i(tl_h_i.a_valid),
+              .wready_o(tl_h_o.a_ready),
+              .wdata_i({tl_h_i.a_opcode, tl_h_i.a_param, tl_h_i.a_size,
+                        tl_h_i.a_source, tl_h_i.a_address, tl_h_i.a_mask,
+                        tl_h_i.a_data, tl_h_i.a_user, spare_req_i}),
+              .rvalid_o(tl_d_o.a_valid),
+              .rready_i(tl_d_i.a_ready),
+              .rdata_o({tl_d_o.a_opcode, tl_d_o.a_param, tl_d_o.a_size,
+                        tl_d_o.a_source, tl_d_o.a_address, tl_d_o.a_mask,
+                        tl_d_o.a_data, tl_d_o.a_user, spare_req_o}),
+              .full_o(),
+              .depth_o(),
+              .err_o());
+
+  // Put everything on the response side into the other FIFO
+
+  localparam int unsigned RSPFIFO_WIDTH = $bits(kelvin_tlul_pkg_128::tl_d2h_t) -
+                                          2 + SpareRspW;
+
+  prim_fifo_sync #(.Width(RSPFIFO_WIDTH),
+                   .Pass(RspPass),
+                   .Depth(RspDepth))
+      rspfifo(.clk_i,
+              .rst_ni,
+              .clr_i(1'b0),
+              .wvalid_i(tl_d_i.d_valid),
+              .wready_o(tl_d_o.d_ready),
+              .wdata_i({tl_d_i.d_opcode, tl_d_i.d_param, tl_d_i.d_size,
+                        tl_d_i.d_source, tl_d_i.d_sink,
+                        (tl_d_i.d_opcode == tlul_pkg::AccessAckData)
+                            ? tl_d_i.d_data
+                            : {kelvin_tlul_pkg_128::TL_DW{1'b0}},
+                        tl_d_i.d_user, tl_d_i.d_error, spare_rsp_i}),
+              .rvalid_o(tl_h_o.d_valid),
+              .rready_i(tl_h_i.d_ready),
+              .rdata_o({tl_h_o.d_opcode, tl_h_o.d_param, tl_h_o.d_size,
+                        tl_h_o.d_source, tl_h_o.d_sink, tl_h_o.d_data,
+                        tl_h_o.d_user, tl_h_o.d_error, spare_rsp_o}),
+              .full_o(),
+              .depth_o(),
+              .err_o());
+endmodule
diff --git a/fpga/ip/kelvin_tlul/tlul_socket_1n_128.sv b/fpga/ip/kelvin_tlul/tlul_socket_1n_128.sv
new file mode 100644
index 0000000..2b333f0
--- /dev/null
+++ b/fpga/ip/kelvin_tlul/tlul_socket_1n_128.sv
@@ -0,0 +1,243 @@
+// Copyright lowRISC contributors (OpenTitan project).
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+//
+// TL-UL socket 1:N module
+//
+// configuration settings
+//   device_count: 4
+//
+// Verilog parameters
+//   HReqPass:      if 1 then host requests can pass through on empty fifo,
+//                  default 1
+//   HRspPass:      if 1 then host responses can pass through on empty fifo,
+//                  default 1
+//   DReqPass:      (one per device_count) if 1 then device i requests can
+//                  pass through on empty fifo, default 1
+//   DRspPass:      (one per device_count) if 1 then device i responses can
+//                  pass through on empty fifo, default 1
+//   HReqDepth:     Depth of host request FIFO, default 2
+//   HRspDepth:     Depth of host response FIFO, default 2
+//   DReqDepth:     (one per device_count) Depth of device i request FIFO,
+//                  default 2
+//   DRspDepth:     (one per device_count) Depth of device i response FIFO,
+//                  default 2
+//   ExplicitErrs:  This module always returns a request error if dev_select_i
+//                  is greater than N-1. If ExplicitErrs is set then the width
+//                  of the dev_select_i signal will be chosen to make sure that
+//                  this is possible. This only makes a difference if N is a
+//                  power of 2.
+//
+// Requests must stall to one device until all responses from other devices
+// have returned.  Need to keep a counter of all outstanding requests and
+// wait until that counter is zero before switching devices.
+//
+// This module will return a request error if the input value of 'dev_select_i'
+// is not within the range 0..N-1. Thus the instantiator of the socket
+// can indicate error by any illegal value of dev_select_i. 4'b1111 is
+// recommended for visibility
+//
+// The maximum value of N is 63
+
+`include "prim_assert.sv"
+
+module tlul_socket_1n_128
+    #(parameter int unsigned N = 4,
+      parameter bit HReqPass = 1'b1,
+      parameter bit HRspPass = 1'b1,
+      parameter bit [N - 1 : 0] DReqPass = {N{1'b1}},
+      parameter bit [N - 1 : 0] DRspPass = {N{1'b1}},
+      parameter bit [3 : 0] HReqDepth = 4'h1,
+      parameter bit [3 : 0] HRspDepth = 4'h1,
+      parameter bit [N * 4 - 1 : 0] DReqDepth = {N{4'h1}},
+      parameter bit [N * 4 - 1 : 0] DRspDepth = {N{4'h1}},
+      parameter bit ExplicitErrs = 1'b1,
+
+      // The width of dev_select_i. We must be able to select any of the N
+      // devices (i.e. values 0..N-1). If ExplicitErrs is set, we also need to
+      // be able to represent N.
+      localparam int unsigned NWD = $clog2(ExplicitErrs ? N + 1 : N))
+    (input clk_i,
+     input rst_ni,
+     input kelvin_tlul_pkg_128::tl_h2d_t tl_h_i,
+     output kelvin_tlul_pkg_128::tl_d2h_t tl_h_o,
+     output kelvin_tlul_pkg_128::tl_h2d_t tl_d_o[N],
+     input kelvin_tlul_pkg_128::tl_d2h_t tl_d_i[N],
+     input [NWD - 1 : 0] dev_select_i);
+
+  `ASSERT_INIT(maxN, N < 64)
+
+  // Since our steering is done after potential FIFOing, we need to
+  // shove our device select bits into spare bits of reqfifo
+
+  // instantiate the host fifo, create intermediate bus 't'
+
+  // FIFO'd version of device select
+  logic [NWD - 1 : 0] dev_select_t;
+
+  kelvin_tlul_pkg_128::tl_h2d_t tl_t_o;
+  kelvin_tlul_pkg_128::tl_d2h_t tl_t_i;
+
+  tlul_fifo_sync_128 #(.ReqPass(HReqPass),
+                       .RspPass(HRspPass),
+                       .ReqDepth(HReqDepth),
+                       .RspDepth(HRspDepth),
+                       .SpareReqW(NWD))
+      fifo_h(.clk_i,
+             .rst_ni,
+             .tl_h_i,
+             .tl_h_o,
+             .tl_d_o(tl_t_o),
+             .tl_d_i(tl_t_i),
+             .spare_req_i(dev_select_i),
+             .spare_req_o(dev_select_t),
+             .spare_rsp_i(1'b0),
+             .spare_rsp_o());
+
+  // We need to keep track of how many requests are outstanding,
+  // and to which device. New requests are compared to this and
+  // stall until that number is zero.
+  localparam int MaxOutstanding =
+                     2 ** top_pkg::TL_AIW;  // Up to 256 outstanding
+  localparam int OutstandingW = $clog2(MaxOutstanding + 1);
+  logic [OutstandingW - 1 : 0] num_req_outstanding;
+  logic [NWD - 1 : 0] dev_select_outstanding;
+  logic hold_all_requests;
+  logic accept_t_req, accept_t_rsp;
+
+  assign accept_t_req = tl_t_o.a_valid & tl_t_i.a_ready;
+  assign accept_t_rsp = tl_t_i.d_valid & tl_t_o.d_ready;
+
+  always_ff @(posedge clk_i or negedge rst_ni) begin
+    if (!rst_ni) begin
+      num_req_outstanding <= '0;
+      dev_select_outstanding <= '0;
+    end else if (accept_t_req) begin
+      if (!accept_t_rsp) begin
+        num_req_outstanding <= num_req_outstanding + 1'b1;
+      end
+      dev_select_outstanding <= dev_select_t;
+    end else if (accept_t_rsp) begin
+      num_req_outstanding <= num_req_outstanding - 1'b1;
+    end
+  end
+
+  `ASSERT(NotOverflowed_A, accept_t_req && !accept_t_rsp ->
+          num_req_outstanding <= MaxOutstanding)
+
+  assign hold_all_requests = (num_req_outstanding != '0) &
+                             (dev_select_t != dev_select_outstanding);
+
+  // Make N copies of 't' request side with modified reqvalid, call
+  // them 'u[0]' .. 'u[n-1]'.
+
+  kelvin_tlul_pkg_128::tl_h2d_t tl_u_o[N + 1];
+  kelvin_tlul_pkg_128::tl_d2h_t tl_u_i[N + 1];
+
+  // ensure that when a device is not selected, both command
+  // data integrity can never match
+  kelvin_tlul_pkg_128::tl_a_user_t blanked_auser;
+  assign blanked_auser = '{
+    rsvd: tl_t_o.a_user.rsvd,
+    instr_type: tl_t_o.a_user.instr_type,
+    cmd_intg: kelvin_tlul_pkg_128::get_bad_cmd_intg(tl_t_o),
+    data_intg: kelvin_tlul_pkg_128::get_bad_data_intg(
+        kelvin_tlul_pkg_128::BlankedAData)
+  };
+
+  // if a host is not selected, or if requests are held off, blank the bus
+  for (genvar i = 0; i < N; i++) begin : gen_u_o
+    logic dev_select;
+    assign dev_select = dev_select_t == NWD'(i) & ~hold_all_requests;
+
+    assign tl_u_o[i].a_valid = tl_t_o.a_valid & dev_select;
+    assign tl_u_o[i].a_opcode = tl_t_o.a_opcode;
+    assign tl_u_o[i].a_param = tl_t_o.a_param;
+    assign tl_u_o[i].a_size = tl_t_o.a_size;
+    assign tl_u_o[i].a_source = tl_t_o.a_source;
+    assign tl_u_o[i].a_address = tl_t_o.a_address;
+    assign tl_u_o[i].a_mask = tl_t_o.a_mask;
+    assign tl_u_o[i].a_data =
+               dev_select ? tl_t_o.a_data : kelvin_tlul_pkg_128::BlankedAData;
+    assign tl_u_o[i].a_user = dev_select ? tl_t_o.a_user : blanked_auser;
+
+    assign tl_u_o[i].d_ready = tl_t_o.d_ready;
+  end
+
+  kelvin_tlul_pkg_128::tl_d2h_t tl_t_p;
+
+  // for the returning reqready, only look at the device we're addressing
+  logic hfifo_reqready;
+  always_comb begin
+    hfifo_reqready = tl_u_i[N].a_ready;  // default to error
+    for (int idx = 0; idx < N; idx++) begin
+      // if (dev_select_outstanding == NWD'(idx)) hfifo_reqready =
+      // tl_u_i[idx].a_ready;
+      if (dev_select_t == NWD'(idx)) hfifo_reqready = tl_u_i[idx].a_ready;
+    end
+    if (hold_all_requests) hfifo_reqready = 1'b0;
+  end
+  // Adding a_valid as a qualifier. This prevents the a_ready from having
+  // unknown value when the address is unknown and the Host TL-UL FIFO is bypass
+  // mode.
+  assign tl_t_i.a_ready = tl_t_o.a_valid & hfifo_reqready;
+
+  always_comb begin
+    tl_t_p = tl_u_i[N];
+    for (int idx = 0; idx < N; idx++) begin
+      if (dev_select_outstanding == NWD'(idx)) tl_t_p = tl_u_i[idx];
+    end
+  end
+  assign tl_t_i.d_valid = tl_t_p.d_valid;
+  assign tl_t_i.d_opcode = tl_t_p.d_opcode;
+  assign tl_t_i.d_param = tl_t_p.d_param;
+  assign tl_t_i.d_size = tl_t_p.d_size;
+  assign tl_t_i.d_source = tl_t_p.d_source;
+  assign tl_t_i.d_sink = tl_t_p.d_sink;
+  assign tl_t_i.d_data = tl_t_p.d_data;
+  assign tl_t_i.d_user = tl_t_p.d_user;
+  assign tl_t_i.d_error = tl_t_p.d_error;
+
+  // Instantiate all the device FIFOs
+  for (genvar i = 0; i < N; i++) begin : gen_dfifo
+    tlul_fifo_sync_128 #(.ReqPass(DReqPass[i]),
+                         .RspPass(DRspPass[i]),
+                         .ReqDepth(DReqDepth[i * 4 +: 4]),
+                         .RspDepth(DRspDepth[i * 4 +: 4]))
+        fifo_d(.clk_i,
+               .rst_ni,
+               .tl_h_i(tl_u_o[i]),
+               .tl_h_o(tl_u_i[i]),
+               .tl_d_o(tl_d_o[i]),
+               .tl_d_i(tl_d_i[i]),
+               .spare_req_i(1'b0),
+               .spare_req_o(),
+               .spare_rsp_i(1'b0),
+               .spare_rsp_o());
+  end
+
+  // Instantiate the error responder. It's only needed if a value greater than
+  // N-1 is actually representable in NWD bits.
+  if ($clog2(N + 1) <= NWD) begin : gen_err_resp
+    assign tl_u_o[N].d_ready = tl_t_o.d_ready;
+    assign tl_u_o[N].a_valid =
+               tl_t_o.a_valid & (dev_select_t >= NWD'(N)) & ~hold_all_requests;
+    assign tl_u_o[N].a_opcode = tl_t_o.a_opcode;
+    assign tl_u_o[N].a_param = tl_t_o.a_param;
+    assign tl_u_o[N].a_size = tl_t_o.a_size;
+    assign tl_u_o[N].a_source = tl_t_o.a_source;
+    assign tl_u_o[N].a_address = tl_t_o.a_address;
+    assign tl_u_o[N].a_mask = tl_t_o.a_mask;
+    assign tl_u_o[N].a_data = tl_t_o.a_data;
+    assign tl_u_o[N].a_user = tl_t_o.a_user;
+    tlul_err_resp err_resp(.clk_i,
+                           .rst_ni,
+                           .tl_h_i(tl_u_o[N]),
+                           .tl_h_o(tl_u_i[N]));
+  end else begin : gen_no_err_resp  // block: gen_err_resp
+    assign tl_u_o[N] = '0;
+    assign tl_u_i[N] = '0;
+    logic unused_sig;
+    assign unused_sig = ^tl_u_o[N];
+  end
+endmodule
diff --git a/fpga/ip/kelvin_tlul/tlul_socket_m1_128.sv b/fpga/ip/kelvin_tlul/tlul_socket_m1_128.sv
new file mode 100644
index 0000000..cded907
--- /dev/null
+++ b/fpga/ip/kelvin_tlul/tlul_socket_m1_128.sv
@@ -0,0 +1,243 @@
+// Copyright lowRISC contributors (OpenTitan project).
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+//
+// TL-UL socket M:1 module
+//
+// Verilog parameters
+//   M:             Number of host ports.
+//   HReqPass:      M bit array to allow requests to pass through the host i
+//                  FIFO with no clock delay if the request FIFO is empty. If
+//                  1'b0, at least one clock cycle of latency is created.
+//                  Default is 1'b1.
+//   HRspPass:      Same as HReqPass but for host response FIFO.
+//   HReqDepth:     Mx4 bit array. bit[i*4+:4] is depth of host i request FIFO.
+//                  Depth of zero is allowed if ReqPass is true. A maximum value
+//                  of 16 is allowed, default is 2.
+//   HRspDepth:     Same as HReqDepth but for host response FIFO.
+//   DReqPass:      Same as HReqPass but for device request FIFO.
+//   DRspPass:      Same as HReqPass but for device response FIFO.
+//   DReqDepth:     Same as HReqDepth but for device request FIFO.
+//   DRspDepth:     Same as HReqDepth but for device response FIFO.
+
+`include "prim_assert.sv"
+
+module tlul_socket_m1_128
+    #(parameter int unsigned M = 4,
+      parameter bit [M - 1 : 0] HReqPass = {M{1'b1}},
+      parameter bit [M - 1 : 0] HRspPass = {M{1'b1}},
+      parameter bit [M * 4 - 1 : 0] HReqDepth = {M{4'h1}},
+      parameter bit [M * 4 - 1 : 0] HRspDepth = {M{4'h1}},
+      parameter bit DReqPass = 1'b1,
+      parameter bit DRspPass = 1'b1,
+      parameter bit [3 : 0] DReqDepth = 4'h1,
+      parameter bit [3 : 0] DRspDepth = 4'h1)
+    (input clk_i,
+     input rst_ni,
+
+     input kelvin_tlul_pkg_128::tl_h2d_t tl_h_i[M],
+     output kelvin_tlul_pkg_128::tl_d2h_t tl_h_o[M],
+
+     output kelvin_tlul_pkg_128::tl_h2d_t tl_d_o,
+     input kelvin_tlul_pkg_128::tl_d2h_t tl_d_i);
+
+  `ASSERT_INIT(maxM, M < 16)
+
+  // Signals
+  //
+  //  tl_h_i/o[0] |  tl_h_i/o[1] | ... |  tl_h_i/o[M-1]
+  //      |              |                    |
+  // u_hostfifo[0]  u_hostfifo[1]        u_hostfifo[M-1]
+  //      |              |                    |
+  //       hreq_fifo_o(i) / hrsp_fifo_i(i)
+  //     ---------------------------------------
+  //     |       request/grant/req_data        |
+  //     |                                     |
+  //     |           PRIM_ARBITER              |
+  //     |                                     |
+  //     |  arb_valid / arb_ready / arb_data   |
+  //     ---------------------------------------
+  //                     |
+  //                dreq_fifo_i / drsp_fifo_o
+  //                     |
+  //                u_devicefifo
+  //                     |
+  //                  tl_d_o/i
+  //
+  // Required ID width to distinguish between host ports
+  //  Used in response steering
+  localparam int unsigned IDW = top_pkg::TL_AIW;
+  localparam int unsigned STIDW = $clog2(M);
+
+  kelvin_tlul_pkg_128::tl_h2d_t hreq_fifo_o[M];
+  kelvin_tlul_pkg_128::tl_d2h_t hrsp_fifo_i[M];
+
+  logic [M - 1 : 0] hrequest;
+  logic [M - 1 : 0] hgrant;
+
+  kelvin_tlul_pkg_128::tl_h2d_t dreq_fifo_i;
+  kelvin_tlul_pkg_128::tl_d2h_t drsp_fifo_o;
+
+  logic arb_valid;
+  logic arb_ready;
+  kelvin_tlul_pkg_128::tl_h2d_t arb_data;
+
+  // Host Req/Rsp FIFO
+  for (genvar i = 0; i < M; i++) begin : gen_host_fifo
+    kelvin_tlul_pkg_128::tl_h2d_t hreq_fifo_i;
+
+    // ID Shifting
+    logic [STIDW - 1 : 0] reqid_sub;
+    logic [IDW - 1 : 0] shifted_id;
+    assign reqid_sub = i;  // can cause conversion error?
+    assign shifted_id = {tl_h_i[i].a_source[0 +: (IDW - STIDW)], reqid_sub};
+
+    `ASSERT(idInRange, tl_h_i[i].a_valid |->
+            tl_h_i[i].a_source[IDW - 1 -: STIDW] == '0)
+
+    // assign not connected bits to nc_* signal to make lint happy
+    logic [IDW - 1 : IDW - STIDW] unused_tl_h_source;
+    assign unused_tl_h_source = tl_h_i[i].a_source[IDW - 1 -: STIDW];
+
+    // Put shifted ID
+    assign hreq_fifo_i = '{
+      a_valid: tl_h_i[i].a_valid,
+      a_opcode: tl_h_i[i].a_opcode,
+      a_param: tl_h_i[i].a_param,
+      a_size: tl_h_i[i].a_size,
+      a_source: shifted_id,
+      a_address: tl_h_i[i].a_address,
+      a_mask: tl_h_i[i].a_mask,
+      a_data: tl_h_i[i].a_data,
+      a_user: tl_h_i[i].a_user,
+      d_ready: tl_h_i[i].d_ready
+    };
+
+    tlul_fifo_sync_128 #(.ReqPass(HReqPass[i]),
+                         .RspPass(HRspPass[i]),
+                         .ReqDepth(HReqDepth[i * 4 +: 4]),
+                         .RspDepth(HRspDepth[i * 4 +: 4]),
+                         .SpareReqW(1))
+        u_hostfifo(.clk_i,
+                   .rst_ni,
+                   .tl_h_i(hreq_fifo_i),
+                   .tl_h_o(tl_h_o[i]),
+                   .tl_d_o(hreq_fifo_o[i]),
+                   .tl_d_i(hrsp_fifo_i[i]),
+                   .spare_req_i(1'b0),
+                   .spare_req_o(),
+                   .spare_rsp_i(1'b0),
+                   .spare_rsp_o());
+  end
+
+  // Device Req/Rsp FIFO
+  tlul_fifo_sync_128 #(.ReqPass(DReqPass),
+                       .RspPass(DRspPass),
+                       .ReqDepth(DReqDepth),
+                       .RspDepth(DRspDepth),
+                       .SpareReqW(1))
+      u_devicefifo(.clk_i,
+                   .rst_ni,
+                   .tl_h_i(dreq_fifo_i),
+                   .tl_h_o(drsp_fifo_o),
+                   .tl_d_o(tl_d_o),
+                   .tl_d_i(tl_d_i),
+                   .spare_req_i(1'b0),
+                   .spare_req_o(),
+                   .spare_rsp_i(1'b0),
+                   .spare_rsp_o());
+
+  // Request Arbiter
+  for (genvar i = 0; i < M; i++) begin : gen_arbreqgnt
+    assign hrequest[i] = hreq_fifo_o[i].a_valid;
+  end
+
+  assign arb_ready = drsp_fifo_o.a_ready;
+
+  if (kelvin_tlul_pkg_128::ArbiterImpl == "PPC") begin : gen_arb_ppc
+    prim_arbiter_ppc #(.N(M),
+                       .DW($bits(kelvin_tlul_pkg_128::tl_h2d_t)))
+        u_reqarb(.clk_i,
+                 .rst_ni,
+                 .req_chk_i(1'b0),
+                 // TL-UL allows dropping valid without ready. See #3354.
+                 .req_i(hrequest),
+                 .data_i(hreq_fifo_o),
+                 .gnt_o(hgrant),
+                 .idx_o(),
+                 .valid_o(arb_valid),
+                 .data_o(arb_data),
+                 .ready_i(arb_ready));
+  end else if (kelvin_tlul_pkg_128::ArbiterImpl == "BINTREE")
+  begin : gen_tree_arb
+    prim_arbiter_tree #(.N(M),
+                        .DW($bits(kelvin_tlul_pkg_128::tl_h2d_t)))
+        u_reqarb(.clk_i,
+                 .rst_ni,
+                 .req_chk_i(1'b0),
+                 // TL-UL allows dropping valid without ready. See #3354.
+                 .req_i(hrequest),
+                 .data_i(hreq_fifo_o),
+                 .gnt_o(hgrant),
+                 .idx_o(),
+                 .valid_o(arb_valid),
+                 .data_o(arb_data),
+                 .ready_i(arb_ready));
+  end else begin : gen_unknown
+    `ASSERT_INIT(UnknownArbImpl_A, 0)
+  end
+
+  logic [M - 1 : 0] hfifo_rspvalid;
+  logic [M - 1 : 0] dfifo_rspready;
+  logic [IDW - 1 : 0] hfifo_rspid;
+  logic dfifo_rspready_merged;
+
+  // arb_data --> dreq_fifo_i
+  //   dreq_fifo_i.hd_rspready <= dfifo_rspready
+
+  assign dfifo_rspready_merged = |dfifo_rspready;
+  assign dreq_fifo_i = '{
+    a_valid: arb_valid,
+    a_opcode: arb_data.a_opcode,
+    a_param: arb_data.a_param,
+    a_size: arb_data.a_size,
+    a_source: arb_data.a_source,
+    a_address: arb_data.a_address,
+    a_mask: arb_data.a_mask,
+    a_data: arb_data.a_data,
+    a_user: arb_data.a_user,
+
+    d_ready: dfifo_rspready_merged
+  };
+
+  // Response ID steering
+  // drsp_fifo_o --> hrsp_fifo_i[i]
+
+  // Response ID shifting before put into host fifo
+  assign hfifo_rspid = {{STIDW{1'b0}}, drsp_fifo_o.d_source[IDW - 1 : STIDW]};
+  for (genvar i = 0; i < M; i++) begin : gen_idrouting
+    assign hfifo_rspvalid[i] =
+               drsp_fifo_o.d_valid & (drsp_fifo_o.d_source[0 +: STIDW] == i);
+    assign dfifo_rspready[i] = hreq_fifo_o[i].d_ready &
+                               (drsp_fifo_o.d_source[0 +: STIDW] == i) &
+                               drsp_fifo_o.d_valid;
+
+    assign hrsp_fifo_i[i] = '{
+      d_valid: hfifo_rspvalid[i],
+      d_opcode: drsp_fifo_o.d_opcode,
+      d_param: drsp_fifo_o.d_param,
+      d_size: drsp_fifo_o.d_size,
+      d_source: hfifo_rspid,
+      d_sink: drsp_fifo_o.d_sink,
+      d_data: drsp_fifo_o.d_data,
+      d_user: drsp_fifo_o.d_user,
+      d_error: drsp_fifo_o.d_error,
+      a_ready: hgrant[i]
+    };
+  end
+
+  // this assertion fails when rspid[0+:STIDW] not in [0..M-1]
+  `ASSERT(rspIdInRange, drsp_fifo_o.d_valid |->
+          drsp_fifo_o.d_source[0 +: STIDW] >= 0 &&
+              drsp_fifo_o.d_source[0 +: STIDW] < M)
+endmodule
diff --git a/fpga/ip/rvv_core_mini_tlul/BUILD b/fpga/ip/rvv_core_mini_tlul/BUILD
new file mode 100644
index 0000000..e8edeb8
--- /dev/null
+++ b/fpga/ip/rvv_core_mini_tlul/BUILD
@@ -0,0 +1,42 @@
+# 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
+#
+#     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(default_visibility = ["//visibility:public"])
+
+genrule(
+    name = "rvv_core_mini_tlul_verilog",
+    srcs = ["//hdl/chisel/src/kelvin:RvvCoreMiniTlul.sv"],
+    outs = ["RvvCoreMiniTlul.sv"],
+    cmd = "cp $< $@",
+)
+
+genrule(
+    name = "rvv_core_mini_tlul_core",
+    srcs = [
+        "rvv_core_mini_tlul.core.tpl",
+        ":rvv_core_mini_tlul_verilog",
+    ],
+    outs = ["rvv_core_mini_tlul.core"],
+    cmd = "sed 's|__VERILOG_FILE__|RvvCoreMiniTlul.sv|' $(location rvv_core_mini_tlul.core.tpl) > $@",
+)
+
+filegroup(
+    name = "rtl_files",
+    srcs = glob([
+        "*.sv",
+        "*.core",
+    ]) + [
+        ":RvvCoreMiniTlul.sv",
+    ],
+)
diff --git a/fpga/ip/rvv_core_mini_tlul/rvv_core_mini_tlul.core b/fpga/ip/rvv_core_mini_tlul/rvv_core_mini_tlul.core
new file mode 100644
index 0000000..0b58a34
--- /dev/null
+++ b/fpga/ip/rvv_core_mini_tlul/rvv_core_mini_tlul.core
@@ -0,0 +1,31 @@
+CAPI=2:
+# 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
+#
+#     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.
+
+name: "google:kelvin:rvv_core_mini_tlul"
+description: "RvvCoreMini with TileLink interface"
+
+filesets:
+  files_rtl:
+    depend:
+      - "lowrisc:prim:all"
+      - "lowrisc:prim_generic:all"
+    files:
+      - RvvCoreMiniTlul.sv: { file_type: systemVerilogSource }
+    file_type: systemVerilogSource
+
+targets:
+  default:
+    filesets:
+      - files_rtl
diff --git a/fpga/ip/rvv_core_mini_tlul/rvv_core_mini_tlul.core.tpl b/fpga/ip/rvv_core_mini_tlul/rvv_core_mini_tlul.core.tpl
new file mode 100644
index 0000000..b48b939
--- /dev/null
+++ b/fpga/ip/rvv_core_mini_tlul/rvv_core_mini_tlul.core.tpl
@@ -0,0 +1,31 @@
+CAPI=2:
+# 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
+#
+#     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.
+
+name: "google:kelvin:rvv_core_mini_tlul"
+description: "RvvCoreMini with TileLink interface"
+
+filesets:
+  files_rtl:
+    depend:
+      - "lowrisc:prim:all"
+      - "lowrisc:prim_generic:all"
+    files:
+      - __VERILOG_FILE__: { file_type: systemVerilogSource }
+    file_type: systemVerilogSource
+
+targets:
+  default:
+    filesets:
+      - files_rtl
diff --git a/fpga/ip/sram/BUILD b/fpga/ip/sram/BUILD
new file mode 100644
index 0000000..d78f918
--- /dev/null
+++ b/fpga/ip/sram/BUILD
@@ -0,0 +1,23 @@
+# 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
+#
+#     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(default_visibility = ["//visibility:public"])
+
+filegroup(
+    name = "rtl_files",
+    srcs = glob([
+        "*.sv",
+        "*.core",
+    ]),
+)
diff --git a/fpga/ip/sram/Sram.sv b/fpga/ip/sram/Sram.sv
new file mode 100644
index 0000000..6cc1e3f
--- /dev/null
+++ b/fpga/ip/sram/Sram.sv
@@ -0,0 +1,40 @@
+// A simple SRAM model, rewritten for BRAM inference
+module Sram
+    #(parameter int Width = 32,
+      parameter int Depth = 1024)
+    (input clk_i,
+     input req_i,
+     input we_i,
+     input [$clog2(Depth) - 1 : 0] addr_i,
+     input [Width - 1 : 0] wdata_i,
+     input [Width / 8 - 1 : 0] wmask_i,
+     output logic [Width - 1 : 0] rdata_o,
+     output logic rvalid_o);
+
+  logic [Width - 1 : 0] mem[Depth - 1 : 0];
+  logic [$clog2(Depth) - 1 : 0] raddr;
+
+  assign rdata_o = mem[raddr];
+
+  always_ff @(posedge clk_i) begin
+    if (req_i) begin
+      if (we_i) begin
+        for (int i = 0; i < Width / 8; i++) begin
+          if (wmask_i[i]) begin
+            mem[addr_i][i * 8 +: 8] <= wdata_i[i * 8 +: 8];
+          end
+        end
+      end
+      // The read address is registered to ensure a synchronous read.
+      raddr <= addr_i;
+    end
+  end
+
+  // The rvalid signal is simply a delayed version of req_i.
+  always_ff @(posedge clk_i) begin
+    rvalid_o <= req_i;
+  end
+
+  localparam MemInitFile = "";
+`include "prim_util_memload.svh"
+endmodule
\ No newline at end of file
diff --git a/fpga/ip/sram/sram.core b/fpga/ip/sram/sram.core
new file mode 100644
index 0000000..0efd821
--- /dev/null
+++ b/fpga/ip/sram/sram.core
@@ -0,0 +1,14 @@
+CAPI=2:
+name: "kelvinv2:ip:sram:0.1"
+description: "SRAM for Kelvin"
+
+filesets:
+  rtl:
+    files:
+      - Sram.sv
+    file_type: systemVerilogSource
+
+targets:
+  default:
+    filesets:
+      - rtl
diff --git a/fpga/ip/tlul_width_bridge/BUILD b/fpga/ip/tlul_width_bridge/BUILD
new file mode 100644
index 0000000..d78f918
--- /dev/null
+++ b/fpga/ip/tlul_width_bridge/BUILD
@@ -0,0 +1,23 @@
+# 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
+#
+#     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(default_visibility = ["//visibility:public"])
+
+filegroup(
+    name = "rtl_files",
+    srcs = glob([
+        "*.sv",
+        "*.core",
+    ]),
+)
diff --git a/fpga/ip/tlul_width_bridge/tlul_device_downsizer.core b/fpga/ip/tlul_width_bridge/tlul_device_downsizer.core
new file mode 100644
index 0000000..5cc6203
--- /dev/null
+++ b/fpga/ip/tlul_width_bridge/tlul_device_downsizer.core
@@ -0,0 +1,32 @@
+CAPI=2:
+# 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
+#
+#     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.
+
+name: "kelvinv2:ip:tlul_device_downsizer"
+description: "TL-UL Device Downsizer"
+
+filesets:
+  files_rtl:
+    depend:
+      - "lowrisc:prim:all"
+      - "lowrisc:tlul:headers"
+      - "kelvinv2:ip:kelvin_tlul:0.1"
+    files:
+      - tlul_device_downsizer.sv: { file_type: systemVerilogSource }
+    file_type: systemVerilogSource
+
+targets:
+  default:
+    filesets:
+      - files_rtl
diff --git a/fpga/ip/tlul_width_bridge/tlul_device_downsizer.sv b/fpga/ip/tlul_width_bridge/tlul_device_downsizer.sv
new file mode 100644
index 0000000..524fb6b
--- /dev/null
+++ b/fpga/ip/tlul_width_bridge/tlul_device_downsizer.sv
@@ -0,0 +1,145 @@
+// 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
+//
+//     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 "prim_assert.sv"
+
+module tlul_device_downsizer
+    #(parameter int AddrWidth = 32)
+    (input clk_i,
+     input rst_ni,
+
+     // Slave (Upsizer-facing) TL-UL Interface
+     input kelvin_tlul_pkg_128::tl_h2d_t s_tl_i,
+     output kelvin_tlul_pkg_128::tl_d2h_t s_tl_o,
+
+     // Master (Crossbar-facing) TL-UL Interface
+     output kelvin_tlul_pkg_32::tl_h2d_t m_tl_o,
+     input kelvin_tlul_pkg_32::tl_d2h_t m_tl_i);
+
+  localparam int SlaveDataWidth = 128;
+  localparam int MasterDataWidth = 32;
+  localparam int LaneWidth = MasterDataWidth / 8;
+  localparam int NumLanes = SlaveDataWidth / MasterDataWidth;
+  localparam int LaneIndexWidth = $clog2(NumLanes);
+
+  // Response path skid buffer
+  kelvin_tlul_pkg_128::tl_d2h_t d_skid_reg;
+  logic d_skid_valid_q, d_skid_valid_d;
+  logic d_skid_ready;
+
+  // Internal signals
+  logic [LaneIndexWidth - 1 : 0] lane_idx;
+  logic [LaneIndexWidth - 1 : 0] lane_idx_reg;
+  logic [MasterDataWidth - 1 : 0] m_a_data;
+  logic [MasterDataWidth / 8 - 1 : 0] m_a_mask;
+  logic [1 : 0] a_size_from_mask;
+
+  // Lane index calculation from mask
+  always_comb begin
+    // Priority encode the mask to find the active lane
+    unique case (1'b1)
+      |s_tl_i.a_mask[3 : 0]:
+        lane_idx = 2'b00;
+      |s_tl_i.a_mask[7 : 4]:
+        lane_idx = 2'b01;
+      |s_tl_i.a_mask[11 : 8]:
+        lane_idx = 2'b10;
+      |s_tl_i.a_mask[15 : 12]:
+        lane_idx = 2'b11;
+      default:
+        lane_idx = 2'b00;  // Should not happen for valid requests
+    endcase
+  end
+
+  // Master port data and mask generation
+  always_comb begin
+    m_a_data = s_tl_i.a_data >> (lane_idx * MasterDataWidth);
+    m_a_mask = s_tl_i.a_mask >> (lane_idx * LaneWidth);
+  end
+
+  // Calculate master port a_size from mask
+  always_comb begin
+    case ($countones(m_a_mask))
+      1:
+        a_size_from_mask = 2'h0;
+      2:
+        a_size_from_mask = 2'h1;
+      3, 4:
+        a_size_from_mask = 2'h2;
+      default:
+        a_size_from_mask = 2'h0;
+    endcase
+  end
+
+  logic [15 : 0] dbg_s_tl_i_a_size = s_tl_i.a_size;
+  // Master port connections
+  assign m_tl_o.a_valid = s_tl_i.a_valid;
+  assign m_tl_o.a_opcode = s_tl_i.a_opcode;
+  assign m_tl_o.a_param = s_tl_i.a_param;
+  assign m_tl_o.a_size =
+             (s_tl_i.a_opcode == tlul_pkg::Get) ? 2 : a_size_from_mask;
+  assign m_tl_o.a_address = {s_tl_i.a_address[AddrWidth - 1 : 4], lane_idx,
+                             2'b00};
+  assign m_tl_o.a_source = s_tl_i.a_source;
+  assign m_tl_o.a_data = m_a_data;
+  assign m_tl_o.a_mask = m_a_mask;
+  assign m_tl_o.a_user = s_tl_i.a_user;
+  assign m_tl_o.d_ready = d_skid_ready;
+
+  // Slave port connections
+  assign s_tl_o.d_opcode = d_skid_reg.d_opcode;
+  assign s_tl_o.d_param = d_skid_reg.d_param;
+  assign s_tl_o.d_sink = d_skid_reg.d_sink;
+  assign s_tl_o.d_source = d_skid_reg.d_source;
+  assign s_tl_o.d_data = d_skid_reg.d_data;
+  assign s_tl_o.d_error = d_skid_reg.d_error;
+  assign s_tl_o.d_user = d_skid_reg.d_user;
+  assign s_tl_o.d_size = d_skid_reg.d_size;
+  assign s_tl_o.d_valid = d_skid_valid_q;
+  assign s_tl_o.a_ready = m_tl_i.a_ready;
+  logic d_skid_reg_size = d_skid_reg.d_size;
+
+  // Skid buffer logic
+  assign d_skid_ready = !d_skid_valid_q || s_tl_i.d_ready;
+
+  always_comb begin
+    d_skid_valid_d = d_skid_valid_q;
+    if (d_skid_ready) begin
+      d_skid_valid_d = m_tl_i.d_valid;
+    end
+  end
+
+  always_ff @(posedge clk_i or negedge rst_ni) begin
+    if (!rst_ni) begin
+      d_skid_valid_q <= 1'b0;
+      d_skid_reg <= '0;
+      lane_idx_reg <= '0;
+    end else begin
+      d_skid_valid_q <= d_skid_valid_d;
+      if (d_skid_ready && m_tl_i.d_valid) begin
+        d_skid_reg.d_opcode <= m_tl_i.d_opcode;
+        d_skid_reg.d_param <= m_tl_i.d_param;
+        d_skid_reg.d_sink <= m_tl_i.d_sink;
+        d_skid_reg.d_source <= m_tl_i.d_source;
+        d_skid_reg.d_data <= m_tl_i.d_data << (lane_idx_reg * MasterDataWidth);
+        d_skid_reg.d_error <= m_tl_i.d_error;
+        d_skid_reg.d_user <= m_tl_i.d_user;
+        d_skid_reg.d_size <= m_tl_i.d_size;
+      end
+      if (s_tl_i.a_valid && s_tl_o.a_ready) begin
+        lane_idx_reg <= lane_idx;
+      end
+    end
+  end
+endmodule
diff --git a/fpga/ip/tlul_width_bridge/tlul_host_upsizer.core b/fpga/ip/tlul_width_bridge/tlul_host_upsizer.core
new file mode 100644
index 0000000..3c25112
--- /dev/null
+++ b/fpga/ip/tlul_width_bridge/tlul_host_upsizer.core
@@ -0,0 +1,32 @@
+CAPI=2:
+# 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
+#
+#     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.
+
+name: "kelvinv2:ip:tlul_host_upsizer"
+description: "TL-UL Host Upsizer"
+
+filesets:
+  files_rtl:
+    depend:
+      - "lowrisc:prim:all"
+      - "lowrisc:tlul:headers"
+      - "kelvinv2:ip:kelvin_tlul:0.1"
+    files:
+      - tlul_host_upsizer.sv: { file_type: systemVerilogSource }
+    file_type: systemVerilogSource
+
+targets:
+  default:
+    filesets:
+      - files_rtl
diff --git a/fpga/ip/tlul_width_bridge/tlul_host_upsizer.sv b/fpga/ip/tlul_width_bridge/tlul_host_upsizer.sv
new file mode 100644
index 0000000..6958dfd
--- /dev/null
+++ b/fpga/ip/tlul_width_bridge/tlul_host_upsizer.sv
@@ -0,0 +1,116 @@
+// 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
+//
+//     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 "prim_assert.sv"
+
+module tlul_host_upsizer
+    #(parameter int AddrWidth = 32)
+    (input clk_i,
+     input rst_ni,
+
+     // Slave (Host-facing) TL-UL Interface
+     input kelvin_tlul_pkg_32::tl_h2d_t s_tl_i,
+     output kelvin_tlul_pkg_32::tl_d2h_t s_tl_o,
+
+     // Master (Crossbar-facing) TL-UL Interface
+     output kelvin_tlul_pkg_128::tl_h2d_t m_tl_o,
+     input kelvin_tlul_pkg_128::tl_d2h_t m_tl_i);
+
+  localparam int SlaveDataWidth = 32;
+  localparam int MasterDataWidth = 128;
+  localparam int LaneWidth = SlaveDataWidth / 8;
+  localparam int NumLanes = MasterDataWidth / SlaveDataWidth;
+  localparam int LaneIndexWidth = $clog2(NumLanes);
+
+  // Response path skid buffer
+  kelvin_tlul_pkg_32::tl_d2h_t d_skid_reg;
+  logic d_skid_valid_q, d_skid_valid_d;
+  logic d_skid_ready;
+
+  // Internal signals
+  logic [LaneIndexWidth - 1 : 0] lane_idx;
+  logic [LaneIndexWidth - 1 : 0] lane_idx_reg;
+  logic [MasterDataWidth - 1 : 0] m_a_data;
+  logic [MasterDataWidth / 8 - 1 : 0] m_a_mask;
+
+  // Lane index calculation
+  assign lane_idx = s_tl_i.a_address[LaneIndexWidth + 1 : 2];
+
+  // Master port data and mask generation
+  always_comb begin
+    m_a_data = '0;
+    m_a_mask = '0;
+    m_a_data[lane_idx * SlaveDataWidth +: SlaveDataWidth] = s_tl_i.a_data;
+    m_a_mask[lane_idx * LaneWidth +: LaneWidth] = s_tl_i.a_mask;
+  end
+
+  // Master port connections
+  assign m_tl_o.a_valid = s_tl_i.a_valid;
+  assign m_tl_o.a_opcode = s_tl_i.a_opcode;
+  assign m_tl_o.a_param = s_tl_i.a_param;
+  assign m_tl_o.a_size = s_tl_i.a_size;
+  assign m_tl_o.a_address = {
+             s_tl_i.a_address[AddrWidth - 1 : LaneIndexWidth + 2],
+             {(LaneIndexWidth + 2){1'b0}}};
+  assign m_tl_o.a_source = s_tl_i.a_source;
+  assign m_tl_o.a_data = m_a_data;
+  assign m_tl_o.a_mask = m_a_mask;
+  assign m_tl_o.a_user = s_tl_i.a_user;
+  assign m_tl_o.d_ready = d_skid_ready;
+
+  // Slave port connections
+  assign s_tl_o.d_opcode = d_skid_reg.d_opcode;
+  assign s_tl_o.d_param = d_skid_reg.d_param;
+  assign s_tl_o.d_sink = d_skid_reg.d_sink;
+  assign s_tl_o.d_source = d_skid_reg.d_source;
+  assign s_tl_o.d_data = d_skid_reg.d_data;
+  assign s_tl_o.d_error = d_skid_reg.d_error;
+  assign s_tl_o.d_user = d_skid_reg.d_user;
+  assign s_tl_o.d_size = d_skid_reg.d_size;
+  assign s_tl_o.d_valid = d_skid_valid_q;
+  assign s_tl_o.a_ready = m_tl_i.a_ready;
+
+  // Skid buffer logic
+  assign d_skid_ready = !d_skid_valid_q || s_tl_i.d_ready;
+
+  always_comb begin
+    d_skid_valid_d = d_skid_valid_q;
+    if (d_skid_ready) begin
+      d_skid_valid_d = m_tl_i.d_valid;
+    end
+  end
+
+  always_ff @(posedge clk_i or negedge rst_ni) begin
+    if (!rst_ni) begin
+      d_skid_valid_q <= 1'b0;
+      d_skid_reg <= '0;
+      lane_idx_reg <= '0;
+    end else begin
+      d_skid_valid_q <= d_skid_valid_d;
+      if (d_skid_ready && m_tl_i.d_valid) begin
+        d_skid_reg.d_opcode <= m_tl_i.d_opcode;
+        d_skid_reg.d_param <= m_tl_i.d_param;
+        d_skid_reg.d_sink <= m_tl_i.d_sink;
+        d_skid_reg.d_source <= m_tl_i.d_source;
+        d_skid_reg.d_data <= m_tl_i.d_data >> (lane_idx_reg * SlaveDataWidth);
+        d_skid_reg.d_error <= m_tl_i.d_error;
+        d_skid_reg.d_user <= m_tl_i.d_user;
+        d_skid_reg.d_size <= m_tl_i.d_size;
+      end
+      if (s_tl_i.a_valid && s_tl_o.a_ready) begin
+        lane_idx_reg <= lane_idx;
+      end
+    end
+  end
+endmodule
\ No newline at end of file