feat(fpga): Add Kelvin SoC top-level and build infrastructure

Change-Id: I93885002bc8675f17f62d75440fa39ece7ddc3e0
diff --git a/fpga/BUILD b/fpga/BUILD
index 36a1e8d..f221189 100644
--- a/fpga/BUILD
+++ b/fpga/BUILD
@@ -12,3 +12,227 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+load("@bazel_skylib//rules:common_settings.bzl", "string_list_flag")
+load("@lowrisc_opentitan_gh//rules:fusesoc.bzl", "fusesoc_build")
+load("//rules:kelvin_v2.bzl", "kelvin_v2_binary")
+load("//rules:utils.bzl", "cc_embed_data")
+load(":rules.bzl", "tlgen_rule")
+
+package(default_visibility = ["//visibility:public"])
+
+_CLOCK_FREQUENCY_MHZ = "10"
+
+# This is the tlgen.py script itself
+py_binary(
+    name = "tlgen_tool",
+    srcs = ["@lowrisc_opentitan_gh//util:tlgen.py"],
+    main = "@lowrisc_opentitan_gh//util:tlgen.py",
+    deps = [
+        "@kelvin_pip_deps_hjson//:pkg",
+        "@lowrisc_opentitan_gh//util/tlgen",
+    ],
+)
+
+tlgen_rule(
+    name = "tl_crossbar_generated",
+    topcfg = "tl_config.hjson",
+)
+
+py_binary(
+    name = "post_process_xbar",
+    srcs = ["post_process_xbar.py"],
+)
+
+genrule(
+    name = "tl_crossbar_processed",
+    srcs = [":tl_crossbar_generated"],
+    outs = [
+        "tl_crossbar_processed_out",
+    ],
+    cmd = "$(execpath :post_process_xbar) " +
+          "--input-dir $(location :tl_crossbar_generated) " +
+          "--output-dir $(location tl_crossbar_processed_out) " +
+          "--cores kelvinv2:ip:kelvin_tlul:0.1",
+    tools = [":post_process_xbar"],
+)
+
+filegroup(
+    name = "tl_crossbar",
+    srcs = [
+        ":tl_crossbar_processed_out",
+    ],
+)
+
+filegroup(
+    name = "tl_crossbar_core",
+    srcs = [":tl_crossbar"],
+)
+
+filegroup(
+    name = "rtl_files",
+    srcs = glob(["**/*.sv"]) + glob(["**/*.core"]) + [
+        "//fpga/ip/kelvin_tlul:rtl_files",
+        "//fpga/ip/rv_core_ibex:rtl_files",
+        "//fpga/ip/rvv_core_mini_tlul:rtl_files",
+        "//fpga/ip/sram:rtl_files",
+        "//fpga/ip/tlul_width_bridge:rtl_files",
+        "//fpga/rtl:rtl_files",
+    ],
+)
+
+string_list_flag(
+    name = "verilator_options",
+    build_setting_default = [
+        "-Wno-ALWCOMBORDER",
+        "-Wno-WIDTHEXPAND",
+        "-Wno-WIDTHTRUNC",
+        "-Wno-UNUSEDSIGNAL",
+        "-Wno-UNUSEDPARAM",
+        "-Wno-VARHIDDEN",
+        # TODO: The MULTIDRIVEN warnings are caused by the `prim_arbiter_fixed`
+        # module and the `tlul_fifo_sync` module. We should investigate these
+        # modules and fix the underlying issues.
+        # "-Wno-MULTIDRIVEN",
+        "-Wno-UNOPTTHREADS",
+        "-Wno-GENUNNAMED",
+        "-DRVFI",
+        "-DUSE_GENERIC",
+    ],
+)
+
+string_list_flag(
+    name = "make_options",
+    build_setting_default = [
+        "-j16",
+    ],
+)
+
+cc_embed_data(
+    name = "add_uint32_m1_bin_header",
+    srcs = [":add_uint32_m1_bin"],
+    var_name = "add_uint32_m1_bin",
+)
+
+kelvin_v2_binary(
+    name = "ibex_boot_rom",
+    srcs = [
+        "sw/ibex_boot_rom.S",
+        "sw/main.cc",
+    ],
+    hdrs = [":add_uint32_m1_bin_header"],
+    copts = ["-DCLOCK_FREQUENCY_MHZ=" + _CLOCK_FREQUENCY_MHZ],
+    linker_script = "sw/ibex_boot_rom.ld",
+)
+
+kelvin_v2_binary(
+    name = "add_uint32_m1",
+    srcs = ["sw/add_uint32_m1.cc"],
+    copts = ["-DCLOCK_FREQUENCY_MHZ=" + _CLOCK_FREQUENCY_MHZ],
+)
+
+filegroup(
+    name = "ibex_boot_rom_bin",
+    srcs = [":ibex_boot_rom"],
+    output_group = "bin_file",
+)
+
+filegroup(
+    name = "add_uint32_m1_bin",
+    srcs = [":add_uint32_m1"],
+    output_group = "bin_file",
+)
+
+KELVIN_SOC_CORES = [
+    "//fpga/ip/rv_core_ibex:rv_core_ibex.core",
+    ":kelvin_soc.core",
+    ":kelvin_soc_pkg.core",
+    ":racl_pkg.core",
+    ":tl_crossbar_core",
+    "@lowrisc_opentitan_gh//hw:check_tool_requirements.core",
+]
+
+KELVIN_SOC_SRCS = [
+    ":rtl_files",
+    "@lowrisc_opentitan_gh//hw/dv/sv:dv_macros",
+    "@lowrisc_opentitan_gh//hw/dv:verilator_files",
+    "@lowrisc_opentitan_gh//hw:check_tool_requirements.py",
+    "@lowrisc_opentitan_gh//hw:lint/tools/verilator/common.vlt",
+    "@lowrisc_opentitan_gh//hw:lint/tools/verilator/comportable.vlt",
+    "@lowrisc_opentitan_gh//hw:rtl_files",
+    "@lowrisc_opentitan_gh//hw:tool_requirements.py",
+    "@lowrisc_opentitan_gh//hw:vendor/lint/pulp_riscv_dbg.vlt",
+    "@lowrisc_opentitan_gh//hw:vendor/pulp_riscv_dbg/debug_rom/debug_rom.sv",
+    "@lowrisc_opentitan_gh//hw:vendor/pulp_riscv_dbg/debug_rom/debug_rom_one_scratch.sv",
+    "@lowrisc_opentitan_gh//hw:vendor/pulp_riscv_dbg/src/dm_csrs.sv",
+    "@lowrisc_opentitan_gh//hw:vendor/pulp_riscv_dbg/src/dm_mem.sv",
+    "@lowrisc_opentitan_gh//hw:vendor/pulp_riscv_dbg/src/dm_pkg.sv",
+    "@lowrisc_opentitan_gh//hw:vendor/pulp_riscv_dbg/src/dm_sba.sv",
+    "@lowrisc_opentitan_gh//hw:vendor/pulp_riscv_dbg/src/dm_top.sv",
+    "@lowrisc_opentitan_gh//hw:vendor/pulp_riscv_dbg/src/dmi_cdc.sv",
+    "@lowrisc_opentitan_gh//hw:vendor/pulp_riscv_dbg/src/dmi_jtag.sv",
+    "@lowrisc_opentitan_gh//hw:vendor/pulp_riscv_dbg/src/dmi_jtag_tap.sv",
+    "@lowrisc_opentitan_gh//hw:verilator_files",
+    "main.cc",
+]
+
+fusesoc_build(
+    name = "build_chip_verilator",
+    srcs = KELVIN_SOC_SRCS + [
+        ":ibex_boot_rom.vmem",
+        "@lowrisc_opentitan_gh//hw:dpi_files",
+    ],
+    cores = KELVIN_SOC_CORES + [
+        ":chip_verilator.core",
+        "@lowrisc_opentitan_gh//hw/dv:dpi/uartdpi/uartdpi.core",
+        "@lowrisc_opentitan_gh//hw/dv:dpi/uartdpi/uartdpi_sv.core",
+    ],
+    flags = [
+        "--MemInitFile=$(location :ibex_boot_rom.vmem)",
+        "--ClockFrequencyMhz=" + _CLOCK_FREQUENCY_MHZ,
+    ],
+    make_options = ":make_options",
+    output_groups = {
+        "binary": ["com.google.kelvin_fpga_chip_verilator_0.1/sim-verilator/Vchip_verilator"],
+    },
+    systems = ["com.google.kelvin:fpga:chip_verilator:0.1"],
+    target = "sim",
+    verilator_options = ":verilator_options",
+    tags = ["manual"],
+)
+
+_PREFIX = "../../../../../../../../.."
+
+#          "../../../../../../../"bazel-out/k8-fastbuild-ST-be77d280135c/bin/fpga
+#          "../../../../../../../"bazel-out/k8-fastbuild-ST-be77d280135c/bin/fpga/wfi.vmem
+_IBEX_BOOT_ROM_VMEM = ":ibex_boot_rom.vmem"
+
+IBEX_BOOT_ROM_VMEM_PATH = "{}/$(location {})".format(_PREFIX, _IBEX_BOOT_ROM_VMEM)
+
+fusesoc_build(
+    name = "build_chip_nexus_bitstream",
+    srcs = KELVIN_SOC_SRCS + [
+        "pins.xdc",
+        "vivado_setup_hooks.tcl",
+        ":ibex_boot_rom.vmem",
+    ],
+    cores = KELVIN_SOC_CORES + [":chip_nexus.core"],
+    flags = [
+        "--MemInitFile=" + IBEX_BOOT_ROM_VMEM_PATH,
+        "--ClockFrequencyMhz=" + _CLOCK_FREQUENCY_MHZ,
+    ],
+    output_groups = {
+        # /home/atv/.cache/bazel/_bazel_atv/cb485e413f28624e05d5dee28237de6d/sandbox/
+        # linux-sandbox/34/execroot/kelvin_hw/bazel-out/k8-fastbuild/bin/fpga/
+        # build.build_chip_nexus_bitstream/
+        # com.google.kelvin_fpga_chip_nexus_0.1/synth-vivado/com.google.kelvin_fpga_chip_nexus_0.1.runs/impl_1/chip_nexus.bit
+        "bitstream": ["com.google.kelvin_fpga_chip_nexus_0.1/synth-vivado/com.google.kelvin_fpga_chip_nexus_0.1.runs/impl_1/chip_nexus.bit"],
+        # /home/atv/.cache/bazel/_bazel_atv/cb485e413f28624e05d5dee28237de6d/sandbox/
+        # linux-sandbox/34/execroot/kelvin_hw/bazel-out/k8-fastbuild/bin/fpga/
+        # build.build_chip_nexus_bitstream/
+        # com.google.kelvin_fpga_chip_nexus_0.1/synth-vivado/com.google.kelvin_fpga_chip_nexus_0.1.runs/
+        "logs": ["com.google.kelvin_fpga_chip_nexus_0.1/synth-vivado/com.google.kelvin_fpga_chip_nexus_0.1.runs/"],
+    },
+    systems = ["com.google.kelvin:fpga:chip_nexus:0.1"],
+    target = "synth",
+    tags = ["manual"],
+)
diff --git a/fpga/chip_nexus.core b/fpga/chip_nexus.core
new file mode 100644
index 0000000..912ddaf
--- /dev/null
+++ b/fpga/chip_nexus.core
@@ -0,0 +1,63 @@
+CAPI=2:
+name: "com.google.kelvin:fpga:chip_nexus:0.1"
+description: "Nexus-specific top-level for Kelvin."
+
+filesets:
+  files_rtl:
+    depend:
+      - com.google.kelvin:fpga:kelvin_soc
+    files:
+      - rtl/chip_nexus.sv
+      - rtl/clkgen_wrapper.sv
+      - rtl/clkgen_xilultrascaleplus.sv
+    file_type: systemVerilogSource
+
+  files_constraints:
+    files:
+      - pins.xdc
+    file_type: xdc
+
+  files_tcl:
+    files:
+      - vivado_setup_hooks.tcl: { file_type: tclSource }
+
+parameters:
+  ClockFrequencyMhz:
+    datatype: int
+    description: "Target clock frequency in MHz."
+    default: 10
+    paramtype: vlogparam
+  MemInitFile:
+    datatype: str
+    description: Path to ROM
+    default: "fpga/wfi.bin"
+    paramtype: vlogparam
+  USE_GENERIC:
+    datatype: bool
+    description: "Use generic primitives"
+    default: false
+    paramtype: vlogdefine
+  FPGA_XILINX:
+    datatype: bool
+    description: "Use Xilinx FPGA primitives"
+    default: false
+    paramtype: vlogdefine
+
+targets:
+  default: &default_target
+    filesets:
+      - files_rtl
+      - files_constraints
+      - files_tcl
+  synth:
+    <<: *default_target
+    toplevel: chip_nexus
+    default_tool: vivado
+    parameters:
+      - ClockFrequencyMhz
+      - MemInitFile
+      - USE_GENERIC=true
+      - FPGA_XILINX=true
+    tools:
+      vivado:
+        part: "xcvu13p-fhga2104-2-e"
diff --git a/fpga/chip_verilator.core b/fpga/chip_verilator.core
new file mode 100644
index 0000000..0971b5a
--- /dev/null
+++ b/fpga/chip_verilator.core
@@ -0,0 +1,60 @@
+CAPI=2:
+name: "com.google.kelvin:fpga:chip_verilator:0.1"
+description: "Verilator-specific top-level for Kelvin."
+
+filesets:
+  files_rtl:
+    depend:
+      - com.google.kelvin:fpga:kelvin_soc:0.2
+      - kelvinv2:ip:sram:0.1
+      - kelvinv2:ip:kelvin_tlul:0.1
+      - lowrisc:dv_dpi_c:uartdpi:0.1
+      - lowrisc:dv_dpi_sv:uartdpi:0.1
+    files:
+      - rtl/chip_verilator.sv
+    file_type: systemVerilogSource
+  sim_src:
+    depend:
+      - lowrisc:dv_verilator:memutil_verilator
+      - lowrisc:dv_verilator:simutil_verilator
+    files:
+      - main.cc
+    file_type: cppSource
+
+parameters:
+  ClockFrequencyMhz:
+    datatype: int
+    description: "Target clock frequency in MHz."
+    default: 10
+    paramtype: vlogparam
+  MemInitFile:
+    datatype: str
+    description: Path to ROM
+    default: "fpga/wfi.bin"
+    paramtype: vlogparam
+
+targets:
+  default: &default_target
+    filesets:
+      - files_rtl
+  sim:
+    <<: *default_target
+    filesets:
+      - files_rtl
+      - sim_src
+    toplevel: chip_verilator
+    default_tool: verilator
+    parameters:
+      - ClockFrequencyMhz
+      - MemInitFile
+    tools:
+      verilator:
+        mode: cc
+        verilator_options:
+          - '--trace'
+          - '--trace-fst'
+          - '-CFLAGS "-DTOPLEVEL_NAME=chip_verilator -std=c++17 -Wall -DVM_TRACE_FMT_FST"'
+          - '-LDFLAGS "-lelf -lutil"'
+          - '-Wall'
+          - '--threads 16'
+          - '-Wno-fatal'
diff --git a/fpga/kelvin_soc.core b/fpga/kelvin_soc.core
new file mode 100644
index 0000000..3db97b4
--- /dev/null
+++ b/fpga/kelvin_soc.core
@@ -0,0 +1,99 @@
+CAPI=2:
+name: "com.google.kelvin:fpga:kelvin_soc:0.2"
+description: "The Kelvin SoC for FPGA."
+
+filesets:
+  rtl:
+    depend:
+      - lowrisc:ip:xbar_kelvin_soc_xbar:0.1
+      - com.google.kelvin:fpga:kelvin_soc_pkg:0.1
+      - com.google.kelvin:fpga:racl_pkg:0.1
+      - lowrisc:ip:uart:0.1
+      - lowrisc:prim:rom_adv
+      - lowrisc:prim_generic:rom
+      - lowrisc:tlul:adapter_sram
+      - lowrisc:ip:spi_device:0.1
+      - lowrisc:kelvin_ip:rv_core_ibex:0.1
+      - google:kelvin:rvv_core_mini_tlul
+      - kelvinv2:ip:tlul_host_upsizer
+      - kelvinv2:ip:tlul_device_downsizer
+      - lowrisc:ibex:ibex_tracer
+      - kelvinv2:ip:sram:0.1
+      - kelvinv2:ip:kelvin_tlul:0.1
+    files:
+      - rtl/kelvin_soc.sv
+    file_type: systemVerilogSource
+
+  sim_src:
+    depend:
+      - lowrisc:dv_verilator:memutil_verilator
+      - lowrisc:dv_verilator:simutil_verilator
+    files:
+      - main.cc
+    file_type: cppSource
+
+mapping:
+  "lowrisc:virtual_constants:top_racl_pkg": "com.google.kelvin:fpga:racl_pkg:0.1"
+
+parameters:
+  ClockFrequencyMhz:
+    datatype: int
+    description: "Target clock frequency in MHz."
+    default: 10
+    paramtype: vlogparam
+  MemInitFile:
+    datatype: str
+    description: Path to ROM
+    default: "fpga/wfi.bin"
+    paramtype: vlogparam
+  USE_GENERIC:
+    datatype: bool
+    description: "Use generic primitives"
+    default: false
+    paramtype: vlogdefine
+  FPGA_XILINX:
+    datatype: bool
+    description: "Use Xilinx FPGA primitives"
+    default: false
+    paramtype: vlogdefine
+
+targets:
+  default: &default
+    filesets:
+      - rtl
+
+  sim:
+    <<: *default
+    filesets:
+      - rtl
+      - sim_src
+    default_tool: verilator
+    description: "Simulation target for the Kelvin SoC"
+    parameters:
+      - MemInitFile
+    tools:
+      verilator:
+        mode: cc
+        verilator_options:
+          - '--trace'
+          - '--trace-fst'
+          - '-CFLAGS "-DTOPLEVEL_NAME=kelvin_soc -std=c++17 -Wall -DVM_TRACE_FMT_FST"'
+          - '-LDFLAGS "-lelf"'
+          - '-Wall'
+          - '--threads 16'
+          - '-Wno-fatal'
+
+  synth:
+    <<: *default
+    default_tool: vivado
+    filesets:
+      - rtl
+    parameters:
+      - MemInitFile
+      - USE_GENERIC=true
+      - FPGA_XILINX=true
+    tools:
+      vivado:
+        part: "xcvu13p-fhga2104-2-e"
+        STEPS.SYNTH_DESIGN.TCL.PRE:
+          - vivado_pre_synth.tcl
\ No newline at end of file
diff --git a/fpga/kelvin_soc_pkg.core b/fpga/kelvin_soc_pkg.core
new file mode 100644
index 0000000..937c982
--- /dev/null
+++ b/fpga/kelvin_soc_pkg.core
@@ -0,0 +1,30 @@
+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: "com.google.kelvin:fpga:kelvin_soc_pkg:0.1"
+description: "Toplevel-wide constants for the Kelvin SoC"
+virtual:
+  - lowrisc:virtual_constants:top_pkg
+
+filesets:
+  files_rtl:
+    files:
+      - rtl/top_pkg.sv
+    file_type: systemVerilogSource
+
+targets:
+  default:
+    filesets:
+      - files_rtl
diff --git a/fpga/main.cc b/fpga/main.cc
new file mode 100644
index 0000000..3e1d352
--- /dev/null
+++ b/fpga/main.cc
@@ -0,0 +1,56 @@
+// 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 <iostream>
+
+#include "verilated_toplevel.h"
+#include "verilator_memutil.h"
+#include "verilator_sim_ctrl.h"
+
+int main(int argc, char **argv) {
+  chip_verilator top;
+  VerilatorMemUtil memutil;
+  VerilatorSimCtrl &simctrl = VerilatorSimCtrl::GetInstance();
+  simctrl.SetTop(&top, &top.clk_i, &top.rst_ni,
+                 VerilatorSimCtrlFlags::ResetPolarityNegative);
+
+  // NB: Final parameter here is "width" of your memory, penultimate parameter
+  // is "depth".
+  MemArea rom("TOP.chip_verilator.i_kelvin_soc.i_rom.u_prim_rom", 0x8000 / 4,
+              4);
+  MemArea sram("TOP.chip_verilator.i_kelvin_soc.i_sram", 0x400000 / 4, 4);
+  MemArea itcm(
+      "TOP.chip_verilator.i_kelvin_soc.i_kelvin_core.coreAxi.itcm.sram."
+      "sramModules_0",
+      0x2000 / 16, 16);
+  MemArea dtcm(
+      "TOP.chip_verilator.i_kelvin_soc.i_kelvin_core.coreAxi.dtcm.sram."
+      "sramModules_0",
+      0x8000 / 16, 16);
+
+  memutil.RegisterMemoryArea("rom", 0x10000000, &rom);
+  memutil.RegisterMemoryArea("sram", 0x20000000, &sram);
+  memutil.RegisterMemoryArea("itcm", 0x00000000, &itcm);
+  memutil.RegisterMemoryArea("dtcm", 0x00010000, &dtcm);
+  simctrl.RegisterExtension(&memutil);
+
+  simctrl.SetInitialResetDelay(20000);
+  simctrl.SetResetDuration(10);
+
+  std::cout << "Simulation of Kelvin SoC" << std::endl
+            << "======================" << std::endl
+            << std::endl;
+
+  return simctrl.Exec(argc, argv).first;
+}
diff --git a/fpga/pins.xdc b/fpga/pins.xdc
new file mode 100644
index 0000000..1f881ac
--- /dev/null
+++ b/fpga/pins.xdc
@@ -0,0 +1,35 @@
+# Clock Signal
+create_clock -period 10.00 -name sys_clk_pin -waveform {0 5} [get_ports clk_p_i]
+set_property -dict { PACKAGE_PIN U13 IOSTANDARD DIFF_SSTL18_I } [get_ports { clk_p_i }];
+set_property -dict { PACKAGE_PIN T13 IOSTANDARD DIFF_SSTL18_I } [get_ports { clk_n_i }];
+
+# Generated Clocks
+create_generated_clock -name clk_main [get_pin i_clkgen/i_clkgen/pll/CLKOUT0]
+create_generated_clock -name clk_48MHz [get_pin i_clkgen/i_clkgen/pll/CLKOUT1]
+create_generated_clock -name clk_aon [get_pin i_clkgen/i_clkgen/pll/CLKOUT4]
+
+# Reset
+set_property -dict { PACKAGE_PIN AR19 IOSTANDARD LVCMOS18 } [get_ports { rst_ni }];
+
+# SPI
+create_clock -period 83.333 -name spi_clk_i -waveform {0 41.667} [get_ports spi_clk_i]
+set_property -dict { PACKAGE_PIN AV19 IOSTANDARD LVCMOS18 } [get_ports { spi_clk_i }];
+
+# UART0
+set_property -dict { PACKAGE_PIN BF20 IOSTANDARD LVCMOS18 } [get_ports { uart_tx_o[0] }];
+set_property -dict { PACKAGE_PIN BD20 IOSTANDARD LVCMOS18 } [get_ports { uart_rx_i[0] }];
+
+# UART1
+set_property -dict { PACKAGE_PIN R23 IOSTANDARD LVCMOS18 } [get_ports { uart_tx_o[1] }];
+set_property -dict { PACKAGE_PIN T23 IOSTANDARD LVCMOS18 } [get_ports { uart_rx_i[1] }];
+
+# LEDs
+set_property -dict { PACKAGE_PIN T31 DRIVE 8 IOSTANDARD LVCMOS12 } [get_ports { io_halted }];
+set_property -dict { PACKAGE_PIN P31 DRIVE 8 IOSTANDARD LVCMOS12 } [get_ports { io_fault }];
+set_property -dict { PACKAGE_PIN N37 DRIVE 8 IOSTANDARD LVCMOS12 } [get_ports { io_halted_n }];
+set_property -dict { PACKAGE_PIN M38 DRIVE 8 IOSTANDARD LVCMOS12 } [get_ports { io_fault_n }];
+
+# Asynchronous Clock Groups
+set_clock_groups -asynchronous \
+  -group {clk_main clk_48MHz clk_aon} \
+  -group {spi_clk_i}
diff --git a/fpga/post_process_xbar.py b/fpga/post_process_xbar.py
new file mode 100644
index 0000000..20179ab
--- /dev/null
+++ b/fpga/post_process_xbar.py
@@ -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.
+"""A script to post-process the output of tlgen.
+
+This script takes an input directory and an output directory. It copies the
+contents of the input directory to the output directory, and then modifies the
+generated SystemVerilog file to use the correct TileLink types.
+"""
+
+import argparse
+import os
+import re
+import shutil
+import stat
+
+
+def main():
+    parser = argparse.ArgumentParser(description=__doc__)
+    parser.add_argument(
+        "--input-dir",
+        required=True,
+        help="The input directory.",
+    )
+    parser.add_argument(
+        "--output-dir",
+        required=True,
+        help="The output directory.",
+    )
+    parser.add_argument(
+        "--cores",
+        nargs="+",
+        required=True,
+        help="The cores to add as dependencies.",
+    )
+    args = parser.parse_args()
+
+    # Copy the contents of the input directory to the output directory.
+    if os.path.exists(args.output_dir):
+        shutil.rmtree(args.output_dir)
+    shutil.copytree(args.input_dir, args.output_dir)
+
+    # Find the generated SystemVerilog file.
+    sv_file = None
+    for root, _, files in os.walk(args.output_dir):
+        for f in files:
+            if f == "xbar_kelvin_soc_xbar.sv":
+                sv_file = os.path.join(root, f)
+                break
+        if sv_file:
+            break
+
+    if sv_file is None:
+        raise RuntimeError("Could not find generated SystemVerilog file.")
+
+    # Make the file writable.
+    os.chmod(sv_file, stat.S_IWRITE | stat.S_IREAD)
+
+    # Read the file and perform the replacements.
+    with open(sv_file, "r") as f:
+        content = f.read()
+    original_content = content
+    content = content.replace("tlul_pkg::tl_h2d_t",
+                              "kelvin_tlul_pkg_128::tl_h2d_t")
+    content = content.replace("tlul_pkg::tl_d2h_t",
+                              "kelvin_tlul_pkg_128::tl_d2h_t")
+    content = content.replace("import tlul_pkg::*",
+                              "import kelvin_tlul_pkg_128::*")
+    content = content.replace("tlul_socket_1n #", "tlul_socket_1n_128 #")
+    content = content.replace("tlul_socket_m1 #", "tlul_socket_m1_128 #")
+    content = content.replace("tlul_fifo_async #", "tlul_fifo_async_128 #")
+
+    if original_content == content:
+        print("Warning: No replacements made.")
+    else:
+        print("Success: Replacements made.")
+
+    with open(sv_file, "w") as f:
+        f.write(content)
+
+    core_file = None
+    for root, _, files in os.walk(args.output_dir):
+        for f in files:
+            if f == "xbar_kelvin_soc_xbar.core":
+                core_file = os.path.join(root, f)
+                break
+            if core_file:
+                break
+    if core_file is None:
+        raise RuntimeError("Could not find generated core file.")
+    os.chmod(core_file, stat.S_IWRITE | stat.S_IREAD)
+    with open(core_file, "r") as f:
+        content = f.read()
+    original_content = content
+    for core in args.cores:
+        content = re.sub(r"(\s+)depend:", r"\1depend:\n\1  - " + core, content)
+    if original_content == content:
+        print("Warning: No replacements made (core).")
+    else:
+        print("Success: replacesments made (core).")
+    with open(core_file, "w") as f:
+        f.write(content)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/fpga/racl_pkg.core b/fpga/racl_pkg.core
new file mode 100644
index 0000000..26f2249
--- /dev/null
+++ b/fpga/racl_pkg.core
@@ -0,0 +1,34 @@
+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: "com.google.kelvin:fpga:racl_pkg:0.1"
+description: "Toplevel-wide RAC-L constants for the Kelvin SoC"
+virtual:
+  - lowrisc:virtual_constants:top_racl_pkg
+
+filesets:
+  files_rtl:
+    depend:
+      - com.google.kelvin:fpga:kelvin_soc_pkg:0.1
+      - lowrisc:tlul:headers
+      - lowrisc:prim:util
+    files:
+      - rtl/top_racl_pkg.sv
+    file_type: systemVerilogSource
+
+targets:
+  default:
+    filesets:
+      - files_rtl
diff --git a/fpga/rtl/BUILD b/fpga/rtl/BUILD
new file mode 100644
index 0000000..d78f918
--- /dev/null
+++ b/fpga/rtl/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/rtl/chip_nexus.sv b/fpga/rtl/chip_nexus.sv
new file mode 100644
index 0000000..30f8dff
--- /dev/null
+++ b/fpga/rtl/chip_nexus.sv
@@ -0,0 +1,65 @@
+// 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.
+
+module chip_nexus
+    #(parameter MemInitFile = "",
+      parameter int ClockFrequencyMhz = 10)
+    (input clk_p_i,
+     input clk_n_i,
+     input rst_ni,
+     input spi_clk_i,
+     output [1 : 0] uart_tx_o,
+     input [1 : 0] uart_rx_i,
+     output logic io_halted,
+     output logic io_fault,
+     output logic io_halted_n,
+     output logic io_fault_n);
+
+  logic clk;
+  logic rst_n;
+  logic clk_48MHz;
+  logic clk_aon;
+
+  top_pkg::uart_sideband_i_t[1 : 0] uart_sideband_i;
+  top_pkg::uart_sideband_o_t[1 : 0] uart_sideband_o;
+
+  assign uart_sideband_i[0].cio_rx = uart_rx_i[0];
+  assign uart_sideband_i[1].cio_rx = uart_rx_i[1];
+  assign uart_tx_o[0] = uart_sideband_o[0].cio_tx;
+  assign uart_tx_o[1] = uart_sideband_o[1].cio_tx;
+
+  assign io_halted_n = ~io_halted;
+  assign io_fault_n = ~io_fault;
+
+  clkgen_wrapper #(.ClockFrequencyMhz(ClockFrequencyMhz))
+      i_clkgen(.clk_p_i(clk_p_i),
+               .clk_n_i(clk_n_i),
+               .rst_ni(rst_ni),
+               .srst_ni(rst_ni),
+               .clk_main_o(clk),
+               .clk_48MHz_o(clk_48MHz),
+               .clk_aon_o(clk_aon),
+               .rst_no(rst_n));
+
+  kelvin_soc #(.MemInitFile(MemInitFile),
+               .ClockFrequencyMhz(ClockFrequencyMhz))
+      i_kelvin_soc(.clk_i(clk),
+                   .rst_ni(rst_n),
+                   .spi_clk_i(spi_clk_i),
+                   .scanmode_i(prim_mubi_pkg::MuBi4False),
+                   .uart_sideband_i(uart_sideband_i),
+                   .uart_sideband_o(uart_sideband_o),
+                   .io_halted(io_halted),
+                   .io_fault(io_fault));
+endmodule
diff --git a/fpga/rtl/chip_verilator.sv b/fpga/rtl/chip_verilator.sv
new file mode 100644
index 0000000..b94ffcb
--- /dev/null
+++ b/fpga/rtl/chip_verilator.sv
@@ -0,0 +1,65 @@
+// 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.
+
+module chip_verilator
+    #(parameter MemInitFile = "",
+      parameter int ClockFrequencyMhz = 10)
+    (input clk_i,
+     input rst_ni,
+     input spi_clk_i,
+     input prim_mubi_pkg::mubi4_t scanmode_i,
+     input top_pkg::uart_sideband_i_t[1 : 0] uart_sideband_i,
+     output top_pkg::uart_sideband_o_t[1 : 0] uart_sideband_o);
+
+  logic uart0_rx;
+  logic uart0_tx;
+
+  uartdpi #(.BAUD(115200),
+            .FREQ(ClockFrequencyMhz * 1_000_000),
+            .NAME("uart0"),
+            .EXIT_STRING("EXIT"))
+      i_uartdpi0(.clk_i(clk_i),
+                 .rst_ni(rst_ni),
+                 .active(1'b1),
+                 .tx_o(uart0_rx),
+                 .rx_i(uart0_tx));
+
+  logic uart1_rx;
+  logic uart1_tx;
+
+  uartdpi #(.BAUD(115200),
+            .FREQ(ClockFrequencyMhz * 1_000_000),
+            .NAME("uart1"),
+            .EXIT_STRING("EXIT"))
+      i_uartdpi1(.clk_i(clk_i),
+                 .rst_ni(rst_ni),
+                 .active(1'b1),
+                 .tx_o(uart1_rx),
+                 .rx_i(uart1_tx));
+
+  kelvin_soc #(.MemInitFile(MemInitFile),
+               .ClockFrequencyMhz(ClockFrequencyMhz))
+      i_kelvin_soc(.clk_i(clk_i),
+                   .rst_ni(rst_ni),
+                   .spi_clk_i(spi_clk_i),
+                   .scanmode_i(scanmode_i),
+                   .uart_sideband_i(
+                       '{'{cio_rx: uart0_rx}, '{cio_rx: uart1_rx}}),
+                   .uart_sideband_o(uart_sideband_o),
+                   .io_halted(),
+                   .io_fault());
+
+  assign uart0_tx = uart_sideband_o[0].cio_tx;
+  assign uart1_tx = uart_sideband_o[1].cio_tx;
+endmodule
diff --git a/fpga/rtl/clkgen_wrapper.sv b/fpga/rtl/clkgen_wrapper.sv
new file mode 100644
index 0000000..55339ef
--- /dev/null
+++ b/fpga/rtl/clkgen_wrapper.sv
@@ -0,0 +1,25 @@
+// Copyright lowRISC contributors (OpenTitan project).
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+module clkgen_wrapper
+    #(parameter int ClockFrequencyMhz = 10)
+    (input clk_p_i,
+     input clk_n_i,
+     input rst_ni,
+     input srst_ni,
+     output clk_main_o,
+     output clk_48MHz_o,
+     output clk_aon_o,
+     output rst_no);
+
+  clkgen_xilultrascaleplus #(.ClockFrequencyMhz(ClockFrequencyMhz))
+      i_clkgen(.clk_i(clk_p_i),
+               .clk_n_i(clk_n_i),
+               .rst_ni(rst_ni),
+               .srst_ni(srst_ni),
+               .clk_main_o(clk_main_o),
+               .clk_48MHz_o(clk_48MHz_o),
+               .clk_aon_o(clk_aon_o),
+               .rst_no(rst_no));
+endmodule
\ No newline at end of file
diff --git a/fpga/rtl/clkgen_xilultrascaleplus.sv b/fpga/rtl/clkgen_xilultrascaleplus.sv
new file mode 100644
index 0000000..9817265
--- /dev/null
+++ b/fpga/rtl/clkgen_xilultrascaleplus.sv
@@ -0,0 +1,137 @@
+// Copyright 2023 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.
+
+module clkgen_xilultrascaleplus
+    #(parameter int ClockFrequencyMhz = 10,
+      // Add BUFG if not done by downstream logic
+      parameter bit AddClkBuf = 1)
+    (input clk_i,
+     input clk_n_i,
+     input rst_ni,
+     input srst_ni,
+     output clk_main_o,
+     output clk_48MHz_o,
+     output clk_aon_o,
+     output rst_no);
+  logic locked_pll;
+  logic io_clk_buf;
+  logic io_rst_buf_n;
+  logic clk_10_buf;
+  logic clk_10_unbuf;
+  logic clk_fb_buf;
+  logic clk_fb_unbuf;
+  logic clk_48_buf;
+  logic clk_48_unbuf;
+  logic clk_aon_buf;
+  logic clk_aon_unbuf;
+  logic clk_ibufds_o;
+
+  // Input IBUFDS conver diff-pair to single-end
+  IBUFDS clk_ibufds(.I(clk_i),
+                    .IB(clk_n_i),
+                    .O(clk_ibufds_o));
+
+  localparam real CLKOUT0_DIVIDE_F_CALC = 1200.0 / ClockFrequencyMhz;
+
+  MMCME2_ADV #(
+          .BANDWIDTH("OPTIMIZED"),
+          .COMPENSATION("ZHOLD"),
+          .STARTUP_WAIT("FALSE"),
+          .DIVCLK_DIVIDE(1),
+          .CLKFBOUT_MULT_F(12.000),
+          .CLKFBOUT_PHASE(0.000),
+          .CLKOUT0_DIVIDE_F(CLKOUT0_DIVIDE_F_CALC),
+          .CLKOUT0_PHASE(0.000),
+          .CLKOUT0_DUTY_CYCLE(0.500),
+          .CLKOUT1_DIVIDE(25),
+          .CLKOUT1_PHASE(0.000),
+          .CLKOUT1_DUTY_CYCLE(0.500),
+          // With CLKOUT4_CASCADE, CLKOUT6's divider is an input to CLKOUT4's
+          // divider. The effective ratio is a multiplication of the two.
+          .CLKOUT4_DIVIDE(40),
+          .CLKOUT4_PHASE(0.000),
+          .CLKOUT4_DUTY_CYCLE(0.500),
+          .CLKOUT4_CASCADE("TRUE"),
+          .CLKOUT6_DIVIDE(120),
+          .CLKIN1_PERIOD(10.000))
+      pll(.CLKFBOUT(clk_fb_unbuf),
+          .CLKFBOUTB(),
+          .CLKOUT0(clk_10_unbuf),
+          .CLKOUT0B(),
+          .CLKOUT1(clk_48_unbuf),
+          .CLKOUT1B(),
+          .CLKOUT2(),
+          .CLKOUT2B(),
+          .CLKOUT3(),
+          .CLKOUT3B(),
+          .CLKOUT4(clk_aon_unbuf),
+          .CLKOUT5(),
+          .CLKOUT6(),
+          // Input clock control
+          .CLKFBIN(clk_fb_buf),
+          .CLKIN1(clk_ibufds_o),
+          .CLKIN2(1'b0),
+          // Tied to always select the primary input clock
+          .CLKINSEL(1'b1),
+          // Ports for dynamic reconfiguration
+          .DADDR(7'h0),
+          .DCLK(1'b0),
+          .DEN(1'b0),
+          .DI(16'h0),
+          .DO(),
+          .DRDY(),
+          .DWE(1'b0),
+          // Phase shift signals
+          .PSCLK(1'b0),
+          .PSEN(1'b0),
+          .PSINCDEC(1'b0),
+          .PSDONE(),
+          // Other control and status signals
+          .CLKFBSTOPPED(),
+          .CLKINSTOPPED(),
+          .LOCKED(locked_pll),
+          .PWRDWN(1'b0),
+          // Do not reset MMCM on external reset, otherwise ILA disconnects at a
+          // reset
+          .RST(1'b0));
+
+  // output buffering
+  BUFGCE clk_fb_bufgce(.I(clk_fb_unbuf),
+                       .O(clk_fb_buf));
+
+  BUFGCE clk_aon_bufgce(.I(clk_aon_unbuf),
+                        .O(clk_aon_buf));
+
+  if (AddClkBuf == 1) begin : gen_clk_bufs
+    BUFGCE clk_10_bufgce(.I(clk_10_unbuf),
+                         .O(clk_10_buf));
+
+    BUFGCE clk_48_bufgce(.I(clk_48_unbuf),
+                         .O(clk_48_buf));
+  end else begin : gen_no_clk_bufs
+    // BUFGs added by downstream modules, no need to add here
+    assign clk_10_buf = clk_10_unbuf;
+    assign clk_48_buf = clk_48_unbuf;
+  end
+
+  // outputs
+  // clock
+  assign clk_main_o = clk_10_buf;
+  assign clk_48MHz_o = clk_48_buf;
+  assign clk_aon_o = clk_aon_buf;
+
+  // reset
+  assign rst_no = locked_pll & rst_ni & srst_ni;
+endmodule
diff --git a/fpga/rtl/kelvin_soc.sv b/fpga/rtl/kelvin_soc.sv
new file mode 100644
index 0000000..1c23def
--- /dev/null
+++ b/fpga/rtl/kelvin_soc.sv
@@ -0,0 +1,639 @@
+// 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.
+
+module kelvin_soc
+    #(parameter MemInitFile = "",
+      parameter int ClockFrequencyMhz = 10)
+    (input clk_i,
+     input rst_ni,
+     input spi_clk_i,
+     input prim_mubi_pkg::mubi4_t scanmode_i,
+     input top_pkg::uart_sideband_i_t[1 : 0] uart_sideband_i,
+     output top_pkg::uart_sideband_o_t[1 : 0] uart_sideband_o,
+     output logic io_halted,
+     output logic io_fault);
+
+  import tlul_pkg::*;
+  import top_pkg::*;
+
+  kelvin_tlul_pkg_128::tl_h2d_t tl_kelvin_core_i;
+  kelvin_tlul_pkg_128::tl_d2h_t tl_kelvin_core_o;
+  kelvin_tlul_pkg_128::tl_h2d_t tl_kelvin_device_o;
+  kelvin_tlul_pkg_128::tl_d2h_t tl_kelvin_device_i;
+
+  kelvin_tlul_pkg_32::tl_h2d_t tl_ibex_core_i_o_32;
+  kelvin_tlul_pkg_32::tl_d2h_t tl_ibex_core_i_i_32;
+  kelvin_tlul_pkg_128::tl_h2d_t tl_ibex_core_i_o_xbar;
+  kelvin_tlul_pkg_128::tl_d2h_t tl_ibex_core_i_i_xbar;
+
+  tlul_host_upsizer i_ibex_core_i_upsizer(.clk_i(clk_i),
+                                          .rst_ni(rst_ni),
+                                          .s_tl_i(tl_ibex_core_i_o_32),
+                                          .s_tl_o(tl_ibex_core_i_i_32),
+                                          .m_tl_o(tl_ibex_core_i_o_xbar),
+                                          .m_tl_i(tl_ibex_core_i_i_xbar));
+
+  kelvin_tlul_pkg_32::tl_h2d_t tl_rom_o_32;
+  kelvin_tlul_pkg_32::tl_d2h_t tl_rom_i_32;
+  kelvin_tlul_pkg_128::tl_h2d_t tl_rom_o_xbar;
+  kelvin_tlul_pkg_128::tl_d2h_t tl_rom_i_xbar;
+  tlul_device_downsizer i_rom_downsizer(.clk_i(clk_i),
+                                        .rst_ni(rst_ni),
+                                        .s_tl_i(tl_rom_o_xbar),
+                                        .s_tl_o(tl_rom_i_xbar),
+                                        .m_tl_o(tl_rom_o_32),
+                                        .m_tl_i(tl_rom_i_32));
+
+  kelvin_tlul_pkg_32::tl_h2d_t tl_ibex_core_d_o_32;
+  kelvin_tlul_pkg_32::tl_d2h_t tl_ibex_core_d_i_32;
+
+  kelvin_tlul_pkg_128::tl_h2d_t tl_ibex_core_d_o_xbar;
+  kelvin_tlul_pkg_128::tl_d2h_t tl_ibex_core_d_i_xbar;
+  tlul_host_upsizer i_ibex_core_d_upsizer(.clk_i(clk_i),
+                                          .rst_ni(rst_ni),
+                                          .s_tl_i(tl_ibex_core_d_o_32),
+                                          .s_tl_o(tl_ibex_core_d_i_32),
+                                          .m_tl_o(tl_ibex_core_d_o_xbar),
+                                          .m_tl_i(tl_ibex_core_d_i_xbar));
+
+  kelvin_tlul_pkg_128::tl_h2d_t tl_sram_o_xbar;
+  kelvin_tlul_pkg_128::tl_d2h_t tl_sram_i_xbar;
+  tl_h2d_t tl_sram_o;
+  tl_d2h_t tl_sram_i;
+  tlul_device_downsizer i_sram_downsizer(.clk_i(clk_i),
+                                         .rst_ni(rst_ni),
+                                         .s_tl_i(tl_sram_o_xbar),
+                                         .s_tl_o(tl_sram_i_xbar),
+                                         .m_tl_o(tl_sram_o),
+                                         .m_tl_i(tl_sram_i));
+  kelvin_tlul_pkg_128::tl_h2d_t tl_uart0_o_xbar;
+  kelvin_tlul_pkg_128::tl_d2h_t tl_uart0_i_xbar;
+  tl_h2d_t tl_uart0_o;
+  tl_d2h_t tl_uart0_i;
+  tlul_device_downsizer i_uart0_downsizer(.clk_i(clk_i),
+                                          .rst_ni(rst_ni),
+                                          .s_tl_i(tl_uart0_o_xbar),
+                                          .s_tl_o(tl_uart0_i_xbar),
+                                          .m_tl_o(tl_uart0_o),
+                                          .m_tl_i(tl_uart0_i));
+  kelvin_tlul_pkg_128::tl_h2d_t tl_uart1_o_xbar;
+  kelvin_tlul_pkg_128::tl_d2h_t tl_uart1_i_xbar;
+  tl_h2d_t tl_uart1_o;
+  tl_d2h_t tl_uart1_i;
+  tlul_device_downsizer i_uart1_downsizer(.clk_i(clk_i),
+                                          .rst_ni(rst_ni),
+                                          .s_tl_i(tl_uart1_o_xbar),
+                                          .s_tl_o(tl_uart1_i_xbar),
+                                          .m_tl_o(tl_uart1_o),
+                                          .m_tl_i(tl_uart1_i));
+  kelvin_tlul_pkg_128::tl_h2d_t tl_spi0_o_xbar;
+  kelvin_tlul_pkg_128::tl_d2h_t tl_spi0_i_xbar;
+  tl_h2d_t tl_spi0_o;
+  tl_d2h_t tl_spi0_i;
+  tlul_device_downsizer i_spi0_downsizer(.clk_i(clk_i),
+                                         .rst_ni(rst_ni),
+                                         .s_tl_i(tl_spi0_o_xbar),
+                                         .s_tl_o(tl_spi0_i_xbar),
+                                         .m_tl_o(tl_spi0_o),
+                                         .m_tl_i(tl_spi0_i));
+
+  xbar_kelvin_soc_xbar i_xbar(.clk_i(clk_i),
+                              .rst_ni(rst_ni),
+                              .spi_clk_i(spi_clk_i),
+                              .scanmode_i(scanmode_i),
+                              .tl_kelvin_core_i(tl_kelvin_core_i),
+                              .tl_kelvin_core_o(tl_kelvin_core_o),
+                              .tl_ibex_core_i_o(tl_ibex_core_i_i_xbar),
+                              .tl_ibex_core_i_i(tl_ibex_core_i_o_xbar),
+                              .tl_ibex_core_d_o(tl_ibex_core_d_i_xbar),
+                              .tl_ibex_core_d_i(tl_ibex_core_d_o_xbar),
+                              .tl_kelvin_device_o(tl_kelvin_device_o),
+                              .tl_kelvin_device_i(tl_kelvin_device_i),
+                              .tl_rom_o(tl_rom_o_xbar),
+                              .tl_rom_i(tl_rom_i_xbar),
+                              .tl_sram_o(tl_sram_o_xbar),
+                              .tl_sram_i(tl_sram_i_xbar),
+                              .tl_uart0_o(tl_uart0_o_xbar),
+                              .tl_uart0_i(tl_uart0_i_xbar),
+                              .tl_uart1_o(tl_uart1_o_xbar),
+                              .tl_uart1_i(tl_uart1_i_xbar),
+                              .tl_spi0_o(tl_spi0_o_xbar),
+                              .tl_spi0_i(tl_spi0_i_xbar));
+
+  uart i_uart0(.clk_i(clk_i),
+               .rst_ni(rst_ni),
+               .tl_i(tl_uart0_o),
+               .tl_o(tl_uart0_i),
+               .alert_rx_i(1'b0),
+               .alert_tx_o(),
+               .racl_policies_i(1'b0),
+               .racl_error_o(),
+               .cio_rx_i(uart_sideband_i[0].cio_rx),
+               .cio_tx_o(uart_sideband_o[0].cio_tx),
+               .cio_tx_en_o(uart_sideband_o[0].cio_tx_en),
+               .intr_tx_watermark_o(uart_sideband_o[0].intr_tx_watermark),
+               .intr_tx_empty_o(uart_sideband_o[0].intr_tx_empty),
+               .intr_rx_watermark_o(uart_sideband_o[0].intr_rx_watermark),
+               .intr_tx_done_o(uart_sideband_o[0].intr_tx_done),
+               .intr_rx_overflow_o(uart_sideband_o[0].intr_rx_overflow),
+               .intr_rx_frame_err_o(uart_sideband_o[0].intr_rx_frame_err),
+               .intr_rx_break_err_o(uart_sideband_o[0].intr_rx_break_err),
+               .intr_rx_timeout_o(uart_sideband_o[0].intr_rx_timeout),
+               .intr_rx_parity_err_o(uart_sideband_o[0].intr_rx_parity_err),
+               .lsio_trigger_o(uart_sideband_o[0].lsio_trigger));
+
+  uart i_uart1(.clk_i(clk_i),
+               .rst_ni(rst_ni),
+               .tl_i(tl_uart1_o),
+               .tl_o(tl_uart1_i),
+               .alert_rx_i(1'b0),
+               .alert_tx_o(),
+               .racl_policies_i(1'b0),
+               .racl_error_o(),
+               .cio_rx_i(uart_sideband_i[1].cio_rx),
+               .cio_tx_o(uart_sideband_o[1].cio_tx),
+               .cio_tx_en_o(uart_sideband_o[1].cio_tx_en),
+               .intr_tx_watermark_o(uart_sideband_o[1].intr_tx_watermark),
+               .intr_tx_empty_o(uart_sideband_o[1].intr_tx_empty),
+               .intr_rx_watermark_o(uart_sideband_o[1].intr_rx_watermark),
+               .intr_tx_done_o(uart_sideband_o[1].intr_tx_done),
+               .intr_rx_overflow_o(uart_sideband_o[1].intr_rx_overflow),
+               .intr_rx_frame_err_o(uart_sideband_o[1].intr_rx_frame_err),
+               .intr_rx_break_err_o(uart_sideband_o[1].intr_rx_break_err),
+               .intr_rx_timeout_o(uart_sideband_o[1].intr_rx_timeout),
+               .intr_rx_parity_err_o(uart_sideband_o[1].intr_rx_parity_err),
+               .lsio_trigger_o(uart_sideband_o[1].lsio_trigger));
+
+  logic rom_req;
+  logic [10 : 0] rom_addr;
+  logic [31 : 0] rom_rdata;
+  logic rom_we;
+  logic [31 : 0] rom_wdata;
+  logic [3 : 0] rom_wmask;
+  logic rom_rvalid;
+
+  tlul_adapter_sram #(.SramAw(11),
+                      .SramDw(32),
+                      .ErrOnWrite(1),
+                      .CmdIntgCheck(1'b1),
+                      .EnableRspIntgGen(1'b1),
+                      .EnableDataIntgGen(1'b1))
+      i_rom_adapter(.clk_i(clk_i),
+                    .rst_ni(rst_ni),
+                    .tl_i(tl_rom_o_32),
+                    .tl_o(tl_rom_i_32),
+                    .req_o(rom_req),
+                    .we_o(rom_we),
+                    .addr_o(rom_addr),
+                    .wdata_o(rom_wdata),
+                    .wmask_o(rom_wmask),
+                    .rdata_i(rom_rdata),
+                    .gnt_i(1'b1),
+                    .rvalid_i(rom_rvalid),
+                    .en_ifetch_i(prim_mubi_pkg::MuBi4True),
+                    .req_type_o(),
+                    .intg_error_o(),
+                    .user_rsvd_o(),
+                    .rerror_i(2'b0),
+                    .compound_txn_in_progress_o(),
+                    .readback_en_i(4'b0),
+                    .readback_error_o(),
+                    .wr_collision_i(1'b0),
+                    .write_pending_i(1'b0));
+
+  prim_rom_adv #(.Width(32),
+                 .Depth(2048),
+                 .MemInitFile(MemInitFile))
+      i_rom(.clk_i(clk_i),
+            .rst_ni(rst_ni),
+            .req_i(rom_req),
+            .addr_i(rom_addr),
+            .rvalid_o(rom_rvalid),
+            .rdata_o(rom_rdata),
+            .cfg_i('0));
+
+  logic sram_req;
+  logic sram_we;
+  logic [11 : 0] sram_addr;
+  logic [31 : 0] sram_wdata;
+  logic [3 : 0] sram_wmask;
+  logic [31 : 0] sram_rdata;
+  logic sram_rvalid;
+
+  tlul_adapter_sram #(.SramAw(12),
+                      .SramDw(32),
+                      .CmdIntgCheck(1'b1),
+                      .EnableRspIntgGen(1'b1),
+                      .EnableDataIntgGen(1'b1))
+      i_sram_adapter(.clk_i(clk_i),
+                     .rst_ni(rst_ni),
+                     .tl_i(tl_sram_o),
+                     .tl_o(tl_sram_i),
+                     .req_o(sram_req),
+                     .we_o(sram_we),
+                     .addr_o(sram_addr),
+                     .wdata_o(sram_wdata),
+                     .wmask_o(sram_wmask),
+                     .rdata_i(sram_rdata),
+                     .gnt_i(1'b1),
+                     .rvalid_i(sram_rvalid),
+                     .en_ifetch_i(prim_mubi_pkg::MuBi4True),
+                     .req_type_o(),
+                     .intg_error_o(),
+                     .user_rsvd_o(),
+                     .rerror_i(2'b0),
+                     .compound_txn_in_progress_o(),
+                     .readback_en_i(4'b0),
+                     .readback_error_o(),
+                     .wr_collision_i(1'b0),
+                     .write_pending_i(1'b0));
+
+  Sram #(.Width(32),
+         .Depth(4096))
+      i_sram(.clk_i(clk_i),
+             .req_i(sram_req),
+             .we_i(sram_we),
+             .addr_i(sram_addr),
+             .wdata_i(sram_wdata),
+             .wmask_i(sram_wmask),
+             .rdata_o(sram_rdata),
+             .rvalid_o(sram_rvalid));
+
+  // SPI Device Instantiation
+  spi_device i_spi_device(.clk_i(clk_i),
+                          .rst_ni(rst_ni),
+                          .tl_i(tl_spi0_o),
+                          .tl_o(tl_spi0_i),
+                          .cio_sck_i(spi_clk_i),
+                          .cio_csb_i(1'b1),
+                          .cio_sd_o(),
+                          .cio_sd_en_o(),
+                          .cio_sd_i(4'b0),
+                          // Tie off unused ports
+                          .alert_rx_i('{default: '0}),
+                          .alert_tx_o(),
+                          .racl_policies_i('0),
+                          .racl_error_o(),
+                          .cio_tpm_csb_i(1'b1),
+                          .passthrough_o(),
+                          .passthrough_i('0),
+                          .intr_upload_cmdfifo_not_empty_o(),
+                          .intr_upload_payload_not_empty_o(),
+                          .intr_upload_payload_overflow_o(),
+                          .intr_readbuf_watermark_o(),
+                          .intr_readbuf_flip_o(),
+                          .intr_tpm_header_not_empty_o(),
+                          .intr_tpm_rdfifo_cmd_end_o(),
+                          .intr_tpm_rdfifo_drop_o(),
+                          .ram_cfg_sys2spi_i('0),
+                          .ram_cfg_rsp_sys2spi_o(),
+                          .ram_cfg_spi2sys_i('0),
+                          .ram_cfg_rsp_spi2sys_o(),
+                          .sck_monitor_o(),
+                          .mbist_en_i(1'b0),
+                          .scan_clk_i(1'b0),
+                          .scan_rst_ni(1'b1),
+                          .scanmode_i(4'b0));
+
+  logic rst_cpu_n;
+
+  // Data and Response integrity generation for Kelvin Device Port
+  localparam int XbarSourceWidth = kelvin_tlul_pkg_128::TL_AIW;
+  localparam int XbarSourceCount = 1 << XbarSourceWidth;
+  logic [1 : 0] host_lane_reg[XbarSourceCount - 1 : 0];
+
+  logic [38 : 0] dev_ecc_full_0, dev_ecc_full_1, dev_ecc_full_2, dev_ecc_full_3;
+  logic [6 : 0] dev_ecc_0, dev_ecc_1, dev_ecc_2, dev_ecc_3;
+  logic [6 : 0] dev_selected_ecc;
+  tl_d2h_rsp_intg_t dev_rsp_metadata;
+  logic [63 : 0] dev_rsp_ecc_full;
+  logic [6 : 0] dev_rsp_ecc;
+
+  assign dev_ecc_0 = dev_ecc_full_0[38 : 32];
+  assign dev_ecc_1 = dev_ecc_full_1[38 : 32];
+  assign dev_ecc_2 = dev_ecc_full_2[38 : 32];
+  assign dev_ecc_3 = dev_ecc_full_3[38 : 32];
+  assign dev_rsp_ecc = dev_rsp_ecc_full[63 : 57];
+
+  prim_secded_inv_39_32_enc dev_enc0(.data_i(tl_kelvin_device_i.d_data[31 : 0]),
+                                     .data_o(dev_ecc_full_0));
+  prim_secded_inv_39_32_enc dev_enc1(.data_i(
+                                         tl_kelvin_device_i.d_data[63 : 32]),
+                                     .data_o(dev_ecc_full_1));
+  prim_secded_inv_39_32_enc dev_enc2(.data_i(
+                                         tl_kelvin_device_i.d_data[95 : 64]),
+                                     .data_o(dev_ecc_full_2));
+  prim_secded_inv_39_32_enc dev_enc3(.data_i(
+                                         tl_kelvin_device_i.d_data[127 : 96]),
+                                     .data_o(dev_ecc_full_3));
+
+  always_ff @(posedge clk_i or negedge rst_ni) begin
+    if (!rst_ni) begin
+      for (int i = 0; i < XbarSourceCount; i++) begin
+        host_lane_reg[i] <= 2'b0;
+      end
+    end else begin
+      // Capture lane index from Ibex data core requests
+      if (tl_ibex_core_d_o_xbar.a_valid && tl_ibex_core_d_i_xbar.a_ready) begin
+        unique case (4'hF)
+          tl_ibex_core_d_o_xbar.a_mask[3 : 0]:
+            host_lane_reg[tl_ibex_core_d_o_xbar.a_source] <= 2'b00;
+          tl_ibex_core_d_o_xbar.a_mask[7 : 4]:
+            host_lane_reg[tl_ibex_core_d_o_xbar.a_source] <= 2'b01;
+          tl_ibex_core_d_o_xbar.a_mask[11 : 8]:
+            host_lane_reg[tl_ibex_core_d_o_xbar.a_source] <= 2'b10;
+          tl_ibex_core_d_o_xbar.a_mask[15 : 12]:
+            host_lane_reg[tl_ibex_core_d_o_xbar.a_source] <= 2'b11;
+        endcase
+      end
+
+      // Capture lane index from Ibex instruction core requests
+      if (tl_ibex_core_i_o_xbar.a_valid && tl_ibex_core_i_i_xbar.a_ready) begin
+        unique case (4'hF)
+          tl_ibex_core_i_o_xbar.a_mask[3 : 0]:
+            host_lane_reg[tl_ibex_core_i_o_xbar.a_source] <= 2'b00;
+          tl_ibex_core_i_o_xbar.a_mask[7 : 4]:
+            host_lane_reg[tl_ibex_core_i_o_xbar.a_source] <= 2'b01;
+          tl_ibex_core_i_o_xbar.a_mask[11 : 8]:
+            host_lane_reg[tl_ibex_core_i_o_xbar.a_source] <= 2'b10;
+          tl_ibex_core_i_o_xbar.a_mask[15 : 12]:
+            host_lane_reg[tl_ibex_core_i_o_xbar.a_source] <= 2'b11;
+        endcase
+      end
+
+      // Capture lane index from Kelvin core requests
+      if (tl_kelvin_core_i.a_valid && tl_kelvin_core_o.a_ready) begin
+        unique case (4'hF)
+          tl_kelvin_core_i.a_mask[3 : 0]:
+            host_lane_reg[tl_kelvin_core_i.a_source] <= 2'b00;
+          tl_kelvin_core_i.a_mask[7 : 4]:
+            host_lane_reg[tl_kelvin_core_i.a_source] <= 2'b01;
+          tl_kelvin_core_i.a_mask[11 : 8]:
+            host_lane_reg[tl_kelvin_core_i.a_source] <= 2'b10;
+          tl_kelvin_core_i.a_mask[15 : 12]:
+            host_lane_reg[tl_kelvin_core_i.a_source] <= 2'b11;
+        endcase
+      end
+    end
+  end
+
+  always_comb begin
+    logic [1 : 0] lane_idx;
+    lane_idx = host_lane_reg[tl_from_kelvin_core.d_source];
+    case (lane_idx)
+      2'b00:
+        dev_selected_ecc = dev_ecc_0;
+      2'b01:
+        dev_selected_ecc = dev_ecc_1;
+      2'b10:
+        dev_selected_ecc = dev_ecc_2;
+      2'b11:
+        dev_selected_ecc = dev_ecc_3;
+      default:
+        dev_selected_ecc = dev_ecc_0;
+    endcase
+  end
+
+  assign dev_rsp_metadata = '{
+    opcode: tl_from_kelvin_core.d_opcode,
+    size: 2'b10,
+    error: tl_from_kelvin_core.d_error
+  };
+
+  prim_secded_inv_64_57_enc dev_enc_rsp(.data_i(
+                                            D2HRspMaxWidth'(dev_rsp_metadata)),
+                                        .data_o(dev_rsp_ecc_full));
+
+  // Kelvin Core Instantiation
+  logic kelvin_halted, kelvin_fault, kelvin_wfi;
+  kelvin_tlul_pkg_128::tl_d2h_t tl_from_kelvin_core;
+
+  assign io_halted = kelvin_halted;
+  assign io_fault = kelvin_fault;
+
+  // Assign all fields for the device D-channel from the Kelvin core's output,
+  // except for the user integrity bits, which we override with our generated
+  // ECC.
+  assign tl_kelvin_device_i.d_valid = tl_from_kelvin_core.d_valid;
+  assign tl_kelvin_device_i.d_opcode = tl_from_kelvin_core.d_opcode;
+  assign tl_kelvin_device_i.d_param = tl_from_kelvin_core.d_param;
+  assign tl_kelvin_device_i.d_size = tl_from_kelvin_core.d_size;
+  assign tl_kelvin_device_i.d_source = tl_from_kelvin_core.d_source;
+  assign tl_kelvin_device_i.d_sink = tl_from_kelvin_core.d_sink;
+  assign tl_kelvin_device_i.d_data = tl_from_kelvin_core.d_data;
+  assign tl_kelvin_device_i.d_error = tl_from_kelvin_core.d_error;
+  assign tl_kelvin_device_i.a_ready = tl_from_kelvin_core.a_ready;
+  assign tl_kelvin_device_i.d_user.rsp_intg = dev_rsp_ecc;
+  assign tl_kelvin_device_i.d_user.data_intg = dev_selected_ecc;
+
+  // Command and Data integrity generation for Kelvin Host Port
+  logic [38 : 0] host_a_data_ecc_full_0, host_a_data_ecc_full_1,
+                 host_a_data_ecc_full_2, host_a_data_ecc_full_3;
+  logic [6 : 0] host_a_data_ecc_0, host_a_data_ecc_1, host_a_data_ecc_2,
+                host_a_data_ecc_3;
+  logic [6 : 0] host_a_data_selected_ecc;
+  tl_h2d_cmd_intg_t host_a_cmd_metadata;
+  logic [63 : 0] host_a_cmd_ecc_full;
+  logic [6 : 0] host_a_cmd_ecc;
+
+  assign host_a_data_ecc_0 = host_a_data_ecc_full_0[38 : 32];
+  assign host_a_data_ecc_1 = host_a_data_ecc_full_1[38 : 32];
+  assign host_a_data_ecc_2 = host_a_data_ecc_full_2[38 : 32];
+  assign host_a_data_ecc_3 = host_a_data_ecc_full_3[38 : 32];
+  assign host_a_cmd_ecc = host_a_cmd_ecc_full[63 : 57];
+
+  prim_secded_inv_39_32_enc host_a_data_enc0(
+                                    .data_i(tl_kelvin_core_i.a_data[31 : 0]),
+                                    .data_o(host_a_data_ecc_full_0));
+  prim_secded_inv_39_32_enc host_a_data_enc1(
+                                    .data_i(tl_kelvin_core_i.a_data[63 : 32]),
+                                    .data_o(host_a_data_ecc_full_1));
+  prim_secded_inv_39_32_enc host_a_data_enc2(
+                                    .data_i(tl_kelvin_core_i.a_data[95 : 64]),
+                                    .data_o(host_a_data_ecc_full_2));
+  prim_secded_inv_39_32_enc host_a_data_enc3(
+                                    .data_i(tl_kelvin_core_i.a_data[127 : 96]),
+                                    .data_o(host_a_data_ecc_full_3));
+
+  logic [top_pkg::TL_DBW - 1 : 0] host_a_cmd_mask;
+
+  localparam logic [top_pkg::TL_AW - 1 : 0] Uart1BaseAddr = 32'h40010000;
+  logic [15 : 0] computed_mask;
+  logic [3 : 0] host_a_cmd_mask_4b;
+  logic [1 : 0] host_a_cmd_lane;
+  tl_h2d_cmd_intg_t host_a_cmd_payload;
+  logic [15 : 0] kelvin_core_i_a_mask;
+
+  always_comb begin
+    if (tl_kelvin_core_i.a_opcode == tlul_pkg::Get) begin
+      computed_mask = ((1 << (1 << tl_kelvin_core_i.a_size)) - 1)
+                      << (tl_kelvin_core_i.a_address[3 : 0]);
+    end else begin
+      computed_mask = kelvin_core_i_a_mask;
+    end
+    host_a_data_selected_ecc = 7'b0;
+    host_a_cmd_mask_4b = '0;
+    host_a_cmd_lane = '0;
+    // This is a priority mux, which is what we want.
+    if (|computed_mask[3 : 0]) begin
+      host_a_data_selected_ecc = host_a_data_ecc_0;
+      host_a_cmd_mask_4b = computed_mask[3 : 0];
+      host_a_cmd_lane = 2'b00;
+    end else if (|computed_mask[7 : 4]) begin
+      host_a_data_selected_ecc = host_a_data_ecc_1;
+      host_a_cmd_mask_4b = computed_mask[7 : 4];
+      host_a_cmd_lane = 2'b01;
+    end else if (|computed_mask[11 : 8]) begin
+      host_a_data_selected_ecc = host_a_data_ecc_2;
+      host_a_cmd_mask_4b = computed_mask[11 : 8];
+      host_a_cmd_lane = 2'b10;
+    end else if (|computed_mask[15 : 12]) begin
+      host_a_data_selected_ecc = host_a_data_ecc_3;
+      host_a_cmd_mask_4b = computed_mask[15 : 12];
+      host_a_cmd_lane = 2'b11;
+    end
+  end
+
+  // Manually pack the command integrity payload to match the 32-bit
+  // peripheral's view. The packing order is derived from the tl_h2d_cmd_intg_t
+  // struct definition.
+  assign host_a_cmd_payload = '{
+    instr_type: prim_mubi_pkg::MuBi4False,  // instr_type (4 bits)
+    addr: tl_kelvin_core_i.a_address,       // addr (32 bits)
+    opcode: tl_kelvin_core_i.a_opcode,      // opcode (3 bits)
+    mask: host_a_cmd_mask_4b                // mask (4 bits)
+  };
+  logic [31 : 0] dbg_uart1_addr = host_a_cmd_payload.addr;
+  logic [2 : 0] dbg_uart1_opcode = host_a_cmd_payload.opcode;
+  logic [3 : 0] dbg_uart1_mask = host_a_cmd_payload.mask;
+  logic [3 : 0] dbg_uart1_instr_type = host_a_cmd_payload.instr_type;
+
+  prim_secded_inv_64_57_enc host_a_cmd_enc(.data_i(H2DCmdMaxWidth'(
+                                               host_a_cmd_payload)),
+                                           .data_o(host_a_cmd_ecc_full));
+
+  assign tl_kelvin_core_i.a_user.cmd_intg = host_a_cmd_ecc;
+  assign tl_kelvin_core_i.a_user.data_intg = host_a_data_selected_ecc;
+  assign tl_kelvin_core_i.a_user.instr_type = prim_mubi_pkg::MuBi4False;
+  assign tl_kelvin_core_i.a_mask = computed_mask;
+
+  RvvCoreMiniTlul
+      i_kelvin_core(
+              .io_clk(clk_i),
+              .io_rst_ni(rst_ni),
+              .io_tl_host_a_ready(tl_kelvin_core_o.a_ready),
+              .io_tl_host_a_valid(tl_kelvin_core_i.a_valid),
+              .io_tl_host_a_bits_opcode(tl_kelvin_core_i.a_opcode),
+              .io_tl_host_a_bits_param(tl_kelvin_core_i.a_param),
+              .io_tl_host_a_bits_size(tl_kelvin_core_i.a_size),
+              .io_tl_host_a_bits_source(tl_kelvin_core_i.a_source),
+              .io_tl_host_a_bits_address(tl_kelvin_core_i.a_address),
+              .io_tl_host_a_bits_mask(kelvin_core_i_a_mask),
+              .io_tl_host_a_bits_data(tl_kelvin_core_i.a_data),
+              .io_tl_host_a_bits_user_rsvd(tl_kelvin_core_i.a_user.rsvd),
+              .io_tl_host_a_bits_user_instr_type(),
+              .io_tl_host_a_bits_user_cmd_intg(),
+              .io_tl_host_a_bits_user_data_intg(),
+              .io_tl_host_d_ready(tl_kelvin_core_i.d_ready),
+              .io_tl_host_d_valid(tl_kelvin_core_o.d_valid),
+              .io_tl_host_d_bits_opcode(tl_kelvin_core_o.d_opcode),
+              .io_tl_host_d_bits_param(tl_kelvin_core_o.d_param),
+              .io_tl_host_d_bits_size(tl_kelvin_core_o.d_size),
+              .io_tl_host_d_bits_source(tl_kelvin_core_o.d_source),
+              .io_tl_host_d_bits_sink(tl_kelvin_core_o.d_sink),
+              .io_tl_host_d_bits_data(tl_kelvin_core_o.d_data),
+              .io_tl_host_d_bits_error(tl_kelvin_core_o.d_error),
+              .io_tl_host_d_bits_user_rsp_intg(
+                  tl_kelvin_core_o.d_user.rsp_intg),
+              .io_tl_host_d_bits_user_data_intg(
+                  tl_kelvin_core_o.d_user.data_intg),
+              .io_tl_device_a_valid(tl_kelvin_device_o.a_valid),
+              .io_tl_device_a_bits_opcode(tl_kelvin_device_o.a_opcode),
+              .io_tl_device_a_bits_param(tl_kelvin_device_o.a_param),
+              .io_tl_device_a_bits_size(tl_kelvin_device_o.a_size),
+              .io_tl_device_a_bits_source(tl_kelvin_device_o.a_source),
+              .io_tl_device_a_bits_address(tl_kelvin_device_o.a_address),
+              .io_tl_device_a_bits_mask(tl_kelvin_device_o.a_mask),
+              .io_tl_device_a_bits_data(tl_kelvin_device_o.a_data),
+              .io_tl_device_a_bits_user_rsvd(tl_kelvin_device_o.a_user.rsvd),
+              .io_tl_device_a_bits_user_instr_type(
+                  tl_kelvin_device_o.a_user.instr_type),
+              .io_tl_device_a_bits_user_cmd_intg(
+                  tl_kelvin_device_o.a_user.cmd_intg),
+              .io_tl_device_a_bits_user_data_intg(
+                  tl_kelvin_device_o.a_user.data_intg),
+              .io_tl_device_d_ready(tl_kelvin_device_o.d_ready),
+              .io_tl_device_a_ready(tl_from_kelvin_core.a_ready),
+              .io_tl_device_d_valid(tl_from_kelvin_core.d_valid),
+              .io_tl_device_d_bits_opcode(tl_from_kelvin_core.d_opcode),
+              .io_tl_device_d_bits_param(tl_from_kelvin_core.d_param),
+              .io_tl_device_d_bits_size(tl_from_kelvin_core.d_size),
+              .io_tl_device_d_bits_source(tl_from_kelvin_core.d_source),
+              .io_tl_device_d_bits_sink(tl_from_kelvin_core.d_sink),
+              .io_tl_device_d_bits_data(tl_from_kelvin_core.d_data),
+              .io_tl_device_d_bits_error(tl_from_kelvin_core.d_error),
+              .io_tl_device_d_bits_user_rsp_intg(),
+              .io_tl_device_d_bits_user_data_intg(),
+              .io_halted(kelvin_halted),
+              .io_fault(kelvin_fault),
+              .io_wfi(kelvin_wfi),
+              .io_irq(1'b0),
+              .io_te(1'b0));
+
+  // Ibex Core Instantiation
+  rv_core_ibex #(.PipeLine(1'b1),
+                 .PMPEnable(1'b0))
+      i_ibex_core(.clk_i(clk_i),
+                  .rst_ni(rst_ni),
+                  .corei_tl_h_o(tl_ibex_core_i_o_32),
+                  .corei_tl_h_i(tl_ibex_core_i_i_32),
+                  .cored_tl_h_o(tl_ibex_core_d_o_32),
+                  .cored_tl_h_i(tl_ibex_core_d_i_32),
+                  // Tie off unused ports
+                  .clk_edn_i(1'b0),
+                  .rst_edn_ni(1'b1),
+                  .clk_esc_i(1'b0),
+                  .rst_esc_ni(1'b1),
+                  .rst_cpu_n_o(rst_cpu_n),
+                  .ram_cfg_icache_tag_i('0),
+                  .ram_cfg_rsp_icache_tag_o(),
+                  .ram_cfg_icache_data_i('0),
+                  .ram_cfg_rsp_icache_data_o(),
+                  .hart_id_i(32'b0),
+                  .boot_addr_i(32'h10000000),
+                  .irq_software_i(1'b0),
+                  .irq_timer_i(1'b0),
+                  .irq_external_i(1'b0),
+                  .esc_tx_i('0),
+                  .esc_rx_o(),
+                  .nmi_wdog_i(1'b0),
+                  .debug_req_i(1'b0),
+                  .crash_dump_o(),
+                  .lc_cpu_en_i(lc_ctrl_pkg::On),
+                  .pwrmgr_cpu_en_i(lc_ctrl_pkg::On),
+                  .pwrmgr_o(),
+                  .scan_rst_ni(1'b1),
+                  .scanmode_i(4'b0),
+                  .cfg_tl_d_i('0),
+                  .cfg_tl_d_o(),
+                  .edn_o(),
+                  .edn_i('0),
+                  .clk_otp_i(1'b0),
+                  .rst_otp_ni(1'b1),
+                  .icache_otp_key_o(),
+                  .icache_otp_key_i('0),
+                  .fpga_info_i(32'b0),
+                  .alert_rx_i('{default: '0}),
+                  .alert_tx_o());
+endmodule
diff --git a/fpga/rtl/top_pkg.sv b/fpga/rtl/top_pkg.sv
new file mode 100644
index 0000000..25013ab
--- /dev/null
+++ b/fpga/rtl/top_pkg.sv
@@ -0,0 +1,49 @@
+// 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 top_pkg;
+
+  // This is a placeholder file.
+  // Toplevel constants for the Kelvin SoC will be added here.
+  localparam int TL_AW = 32;
+  localparam int TL_DW = 32;
+  localparam int TL_AIW = 8;
+  localparam int TL_DIW = 1;
+  localparam int TL_AUW = 23;
+  localparam int TL_DUW = 14;
+  localparam int TL_DBW = (TL_DW >> 3);
+  localparam int TL_SZW = $clog2($clog2(TL_DBW) + 1);
+  localparam int NrRaclBits = 1;
+
+  typedef logic[NrRaclBits - 1 : 0] ctn_uid_t;
+
+  typedef struct packed {
+    logic cio_rx;
+  } uart_sideband_i_t;
+
+  typedef struct packed {
+    logic cio_tx;
+    logic cio_tx_en;
+    logic intr_tx_watermark;
+    logic intr_tx_empty;
+    logic intr_rx_watermark;
+    logic intr_tx_done;
+    logic intr_rx_overflow;
+    logic intr_rx_frame_err;
+    logic intr_rx_break_err;
+    logic intr_rx_timeout;
+    logic intr_rx_parity_err;
+    logic lsio_trigger;
+  } uart_sideband_o_t;
+endpackage
diff --git a/fpga/rtl/top_racl_pkg.sv b/fpga/rtl/top_racl_pkg.sv
new file mode 100644
index 0000000..50a0f10
--- /dev/null
+++ b/fpga/rtl/top_racl_pkg.sv
@@ -0,0 +1,89 @@
+// 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 top_racl_pkg;
+  import tlul_pkg::*;
+
+  // This is a placeholder file.
+  // Toplevel RAC-L constants for the Kelvin SoC will be added here.
+  parameter int unsigned NrRaclPolicies = 1;
+
+  // RACL Policy selector bits
+  parameter int unsigned RaclPolicySelLen =
+                             prim_util_pkg::vbits(NrRaclPolicies);
+
+  // Number of RACL bits transferred
+  parameter int unsigned NrRaclBits = 1;
+
+  // Number of CTN UID bits transferred
+  parameter int unsigned NrCtnUidBits = 1;
+  // CTN UID assigned the bus originator
+  typedef logic[NrCtnUidBits - 1 : 0] ctn_uid_t;
+
+  // RACL Policy selector type
+  typedef logic[RaclPolicySelLen - 1 : 0] racl_policy_sel_t;
+
+  // RACL role type binary encoded
+  typedef logic[NrRaclBits - 1 : 0] racl_role_t;
+  // RACL permission: A one-hot encoded role vector
+  typedef logic[(2 ** NrRaclBits) - 1 : 0] racl_role_vec_t;
+
+  // RACL policy containing a read and write permission
+  typedef struct packed {
+    racl_role_vec_t write_perm;  // Write permission (upper bits)
+    racl_role_vec_t read_perm;   // Read permission (lower bits)
+  } racl_policy_t;
+  typedef racl_policy_t[NrRaclPolicies - 1 : 0] racl_policy_vec_t;
+
+  // RACL information logged in case of a denial
+  typedef struct packed {
+    logic valid;     // Error information is valid
+    logic overflow;  // Error overflow, More than 1 RACL error at a time
+    racl_role_t racl_role;
+    ctn_uid_t ctn_uid;
+    logic read_access;  // 0: Write access, 1: Read access
+    logic [top_pkg::TL_AW - 1 : 0] request_address;
+  } racl_error_log_t;
+
+  // RACL range used to protect a range of addresses with a RACL policy (e.g.,
+  // for sram).
+  typedef struct packed {
+    logic [top_pkg::TL_AW - 1 : 0] base;   // Start address of range
+    logic [top_pkg::TL_AW - 1 : 0] limit;  // End address of range (inclusive)
+    racl_policy_sel_t policy_sel;          // Policy selector
+    logic enable;  // 0: Range is disabled, 1: Range is enabled
+  } racl_range_t;
+
+  function automatic racl_role_t tlul_extract_racl_role_bits
+      (logic [tlul_pkg::RsvdWidth - 1 : 0] rsvd);
+    // Waive unused bits
+    logic unused_rsvd_bits;
+    unused_rsvd_bits = ^{
+      rsvd
+    };
+
+    return racl_role_t'(rsvd[0 : 0]);
+  endfunction
+
+  function automatic ctn_uid_t tlul_extract_ctn_uid_bits
+      (logic [tlul_pkg::RsvdWidth - 1 : 0] rsvd);
+    // Waive unused bits
+    logic unused_rsvd_bits;
+    unused_rsvd_bits = ^{
+      rsvd
+    };
+
+    return ctn_uid_t'(rsvd[0 : 0]);
+  endfunction
+endpackage
diff --git a/fpga/rules.bzl b/fpga/rules.bzl
new file mode 100644
index 0000000..df307ad
--- /dev/null
+++ b/fpga/rules.bzl
@@ -0,0 +1,43 @@
+"""Starlark rules for FPGA development."""
+
+def _tlgen_impl(ctx):
+    """Implementation of the tlgen_rule."""
+    topcfg = ctx.file.topcfg
+    out_dir = ctx.actions.declare_directory(ctx.label.name + "_out")
+    core_file = ctx.actions.declare_file(ctx.label.name + "_out/" + "xbar_kelvin_soc_xbar.core")
+
+    ctx.actions.run(
+        outputs = [out_dir, core_file],
+        inputs = [topcfg],
+        executable = ctx.executable._tool,
+        arguments = [
+            "--topcfg",
+            topcfg.path,
+            "--outdir",
+            out_dir.path,
+        ],
+        progress_message = "Running tlgen and extracting core for %s" % topcfg.short_path,
+    )
+
+    return [
+        DefaultInfo(files = depset([out_dir])),
+        OutputGroupInfo(
+            core_file_output = depset([core_file, out_dir]),
+        ),
+    ]
+
+tlgen_rule = rule(
+    implementation = _tlgen_impl,
+    attrs = {
+        "topcfg": attr.label(
+            allow_single_file = True,
+            mandatory = True,
+            doc = "HJSON top-level configuration file.",
+        ),
+        "_tool": attr.label(
+            default = Label("//fpga:tlgen_tool"),
+            executable = True,
+            cfg = "exec",
+        ),
+    },
+)
diff --git a/fpga/sw/.gitignore b/fpga/sw/.gitignore
new file mode 100644
index 0000000..dbfe247
--- /dev/null
+++ b/fpga/sw/.gitignore
@@ -0,0 +1,4 @@
+# Ignore build artifacts
+*.o
+*.bin
+*.elf
diff --git a/fpga/sw/add_uint32_m1.cc b/fpga/sw/add_uint32_m1.cc
new file mode 100644
index 0000000..fb2c2dd
--- /dev/null
+++ b/fpga/sw/add_uint32_m1.cc
@@ -0,0 +1,61 @@
+/*
+ * 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 <riscv_vector.h>
+
+uint32_t in_buf_1[16] __attribute__((aligned(16)));
+uint32_t in_buf_2[16] __attribute__((aligned(16)));
+uint32_t out_buf[16] __attribute__((aligned(16)));
+
+void add_u32_m1(const uint32_t *in_buf_1, const uint32_t *in_buf_2,
+                uint32_t *out_buf) {
+  vuint32m1_t input_v1 = __riscv_vle32_v_u32m1(in_buf_1, 4);
+  vuint32m1_t input_v2 = __riscv_vle32_v_u32m1(in_buf_2, 4);
+  vuint32m1_t add_result = __riscv_vadd_vv_u32m1(input_v1, input_v2, 4);
+  __riscv_vse32_v_u32m1(out_buf, add_result, 4);
+}
+
+int main(int argc, char **argv) {
+  add_u32_m1(in_buf_1, in_buf_2, out_buf);
+
+  // Configure UART1.
+  // The NCO is calculated as: (baud_rate * 2^20) / clock_frequency
+  // In our case: (115200 * 2^20) / (CLOCK_FREQUENCY_MHZ * 1000000)
+  volatile unsigned int *uart_ctrl =
+      reinterpret_cast<volatile unsigned int *>(0x40010010);
+  const uint64_t uart_ctrl_nco =
+      ((uint64_t)115200 << 20) / (CLOCK_FREQUENCY_MHZ * 1000000);
+  // Enable TX and RX, and set the NCO value.
+  *uart_ctrl = (uart_ctrl_nco << 16) | 3;
+
+  auto uart_print = [](const char *str) {
+    volatile char *uart_wdata = reinterpret_cast<volatile char *>(0x4001001c);
+    volatile unsigned int *uart_status =
+        reinterpret_cast<volatile unsigned int *>(0x40010014);
+
+    while (*str) {
+      // Wait until TX FIFO is not full.
+      while (*uart_status & 1) {
+        asm volatile("nop");
+      }
+      *uart_wdata = *str++;
+    }
+  };
+
+  uart_print("Hello from Kelvin!\n");
+
+  return 0;
+}
diff --git a/fpga/sw/ibex_boot_rom.S b/fpga/sw/ibex_boot_rom.S
new file mode 100644
index 0000000..1c2bfe8
--- /dev/null
+++ b/fpga/sw/ibex_boot_rom.S
@@ -0,0 +1,45 @@
+// A simple program that sets up a stack and calls main.
+.section .text
+.globl _start
+.org 0x80
+_start:
+  // Set up the stack pointer
+  la sp, _stack_start
+
+  // Clear registers
+  mv   tp, zero
+  mv   t1, zero
+  mv   t2, zero
+  mv   s0, zero
+  mv   s1, zero
+  mv   a1, zero
+  mv   a2, zero
+  mv   a3, zero
+  mv   a4, zero
+  mv   a5, zero
+  mv   a6, zero
+  mv   a7, zero
+  mv   s2, zero
+  mv   s3, zero
+  mv   s4, zero
+  mv   s5, zero
+  mv   s6, zero
+  mv   s7, zero
+  mv   s8, zero
+  mv   s9, zero
+  mv   s10, zero
+  mv   s11, zero
+  mv   t3, zero
+  mv   t4, zero
+  mv   t5, zero
+  mv   t6, zero
+
+  // Call main
+  call main
+
+  // Wait for interrupt
+  wfi
+
+// Infinite loop
+_hang:
+  j _hang
diff --git a/fpga/sw/ibex_boot_rom.ld b/fpga/sw/ibex_boot_rom.ld
new file mode 100644
index 0000000..95ab232
--- /dev/null
+++ b/fpga/sw/ibex_boot_rom.ld
@@ -0,0 +1,20 @@
+MEMORY {
+  rom (rx) : ORIGIN = 0x10000000, LENGTH = 0x8000
+  sram (rwx) : ORIGIN = 0x20000000, LENGTH = 0x400000
+}
+
+ENTRY(_start)
+
+SECTIONS {
+  . = 0x10000080;
+  .text : {
+    *(.text)
+    *(.text.*)
+  } > rom
+
+  .stack (NOLOAD) : {
+    . = ALIGN(16);
+    . = . + 4K;
+    _stack_start = .;
+  } > sram
+}
diff --git a/fpga/sw/main.cc b/fpga/sw/main.cc
new file mode 100644
index 0000000..3ff561f
--- /dev/null
+++ b/fpga/sw/main.cc
@@ -0,0 +1,69 @@
+#include <stdint.h>
+
+#include <cstring>
+
+#include "fpga/add_uint32_m1_bin_header.h"
+
+extern "C" int main() {
+  // Copy the embedded binary to Kelvin's ITCM at 0x0.
+  void *itcm_base = reinterpret_cast<void *>(static_cast<uintptr_t>(0x0));
+  memcpy(itcm_base, add_uint32_m1_bin, add_uint32_m1_bin_len);
+
+  // Kelvin run sequence
+  volatile unsigned int *kelvin_reset_csr =
+      reinterpret_cast<volatile unsigned int *>(
+          static_cast<uintptr_t>(0x00030000));
+
+  // Release clock gate
+  *kelvin_reset_csr = 1;
+
+  // Wait one cycle
+  __asm__ volatile("nop");
+
+  // Release reset
+  *kelvin_reset_csr = 0;
+
+  volatile unsigned int *kelvin_status_csr =
+      reinterpret_cast<volatile unsigned int *>(
+          static_cast<uintptr_t>(0x00030008));
+  // Wait for Kelvin to halt
+  while (!(*kelvin_status_csr & 1)) {
+    for (int i = 0; i < 1000; ++i) {
+      __asm__ volatile("nop");
+    }
+  }
+
+  // Configure UART0.
+  // The NCO is calculated as: (baud_rate * 2^20) / clock_frequency
+  // In our case: (115200 * 2^20) / (CLOCK_FREQUENCY_MHZ * 1000000)
+  volatile unsigned int *uart_ctrl =
+      reinterpret_cast<volatile unsigned int *>(0x40000010);
+  const uint64_t uart_ctrl_nco =
+      ((uint64_t)115200 << 20) / (CLOCK_FREQUENCY_MHZ * 1000000);
+  // Enable TX and RX, and set the NCO value.
+  *uart_ctrl = (uart_ctrl_nco << 16) | 3;
+
+  auto uart_print = [](const char *str) {
+    volatile char *uart_wdata = reinterpret_cast<volatile char *>(0x4000001c);
+    volatile unsigned int *uart_status =
+        reinterpret_cast<volatile unsigned int *>(0x40000014);
+
+    while (*str) {
+      // Wait until TX FIFO is not full.
+      while (*uart_status & 1) {
+        asm volatile("nop");
+      }
+      *uart_wdata = *str++;
+    }
+  };
+
+  uart_print("Kelvin halted, as expected.\n");
+
+  volatile unsigned int *sram = (volatile unsigned int *)0x20000000;
+  *sram = 0xdeadbeef;
+  while (*sram != 0xdeadbeef) {
+    asm volatile("nop");
+  }
+
+  return 0;
+}
\ No newline at end of file
diff --git a/fpga/tl_config.hjson b/fpga/tl_config.hjson
new file mode 100644
index 0000000..82f5a83
--- /dev/null
+++ b/fpga/tl_config.hjson
@@ -0,0 +1,95 @@
+{
+  // Top-level configuration for the Kelvin SoC crossbar.
+  name: "kelvin_soc_xbar",
+  clock: "clk_i",
+  reset: "rst_ni",
+
+  // Define all the hosts (masters) and devices (slaves) in the system.
+  nodes: [
+    // Hosts (CPU Cores)
+    { name: "kelvin_core", type: "host", clock: "clk_i", reset: "rst_ni", addr_space: "asid0" },
+    { name: "ibex_core_i", type: "host", clock: "clk_i", reset: "rst_ni", addr_space: "asid0" },
+    { name: "ibex_core_d", type: "host", clock: "clk_i", reset: "rst_ni", addr_space: "asid0" },
+
+    // Devices (Peripherals and Memory)
+    {
+      name: "kelvin_device",
+      type: "device",
+      clock: "clk_i",
+      reset: "rst_ni",
+      xbar: false,
+      addr_range: [
+        { base_addrs: {"asid0": "0x00000000"}, size_byte: "0x2000" }, // 8kB
+        { base_addrs: {"asid0": "0x00010000"}, size_byte: "0x8000" }, // 32kB
+        { base_addrs: {"asid0": "0x00030000"}, size_byte: "0x1000" }  // 4kB
+      ]
+    },
+    {
+      name: "rom",
+      type: "device",
+      clock: "clk_i",
+      reset: "rst_ni",
+      xbar: false,
+      addr_range: [{ base_addrs: {"asid0": "0x10000000"}, size_byte: "0x8000" }] // 32kB
+    },
+    {
+      name: "sram",
+      type: "device",
+      clock: "clk_i",
+      reset: "rst_ni",
+      xbar: false,
+      addr_range: [{ base_addrs: {"asid0": "0x20000000"}, size_byte: "0x400000" }] // 4MB
+    },
+    {
+      name: "uart0",
+      type: "device",
+      clock: "clk_i",
+      reset: "rst_ni",
+      xbar: false,
+      addr_range: [{ base_addrs: {"asid0": "0x40000000"}, size_byte: "0x1000" }]
+    },
+    {
+      name: "uart1",
+      type: "device",
+      clock: "clk_i",
+      reset: "rst_ni",
+      xbar: false,
+      addr_range: [{ base_addrs: {"asid0": "0x40010000"}, size_byte: "0x1000" }]
+    },
+    {
+      name: "spi0",
+      type: "device",
+      clock: "spi_clk_i", // Using a separate clock for the SPI peripheral
+      reset: "rst_ni",
+      xbar: false,
+      addr_range: [{ base_addrs: {"asid0": "0x40020000"}, size_byte: "0x1000" }]
+    }
+  ],
+
+  // Define which hosts can access which devices.
+  connections: {
+    kelvin_core: [ "sram", "uart1", "spi0" ],
+    ibex_core_i: [ "rom", "sram" ],
+    ibex_core_d: [ "rom", "sram", "uart0", "kelvin_device" ]
+  },
+
+  // Define clock connections for all components.
+  clock_connections: {
+    clk_i: [
+      "kelvin_core",
+      "ibex_core_i",
+      "ibex_core_d",
+      "kelvin_device",
+      "rom",
+      "sram",
+      "uart0",
+      "uart1"
+    ],
+    spi_clk_i: [
+      "spi0"
+    ]
+  },
+  reset_connections: {
+    rst_ni: "rst_ni"
+  }
+}
diff --git a/fpga/vivado_setup_hooks.tcl b/fpga/vivado_setup_hooks.tcl
new file mode 100644
index 0000000..28182d2
--- /dev/null
+++ b/fpga/vivado_setup_hooks.tcl
@@ -0,0 +1,2 @@
+## Elevate warning for not finding file for readmemh to ERROR.
+set_msg_config -id {[Synth 8-4445]} -new_severity ERROR
\ No newline at end of file