[sw] Introduce link-time dependency injection

This change unifies the two existing Meson tres into one by introducing
"link time dependency injection," described in #1162.

Signed-off-by: Miguel Young de la Sota <mcyoung@google.com>
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index a554da8..1475457 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -145,8 +145,7 @@
   - bash: |
       . util/build_consts.sh
       ./meson_init.sh -A
-      ninja -C "$(sw_obj_dir sim-verilator)" all
-      ninja -C "$(sw_obj_dir fpga)" all
+      ninja -C "$OBJ_DIR" all
     displayName: 'Build embedded targets'
   - template: 'ci/upload-artifacts-template.yml'
     parameters:
@@ -260,7 +259,7 @@
       mkdir -p "$OBJ_DIR/hw"
       mkdir -p "$BIN_DIR/hw/top_earlgrey"
 
-      BOOTROM_VMEM="$BIN_DIR/sw/device/fpga/boot_rom/boot_rom.vmem"
+      BOOTROM_VMEM="$BIN_DIR/sw/device/boot_rom/boot_rom_fpga_nexysvideo.vmem"
       test -f "$BOOTROM_VMEM"
 
       . /opt/xilinx/Vivado/2018.3/settings64.sh
diff --git a/ci/run_verilator_pytest.sh b/ci/run_verilator_pytest.sh
index 4ff4fa7..eaa26de 100755
--- a/ci/run_verilator_pytest.sh
+++ b/ci/run_verilator_pytest.sh
@@ -7,19 +7,19 @@
 . util/build_consts.sh
 
 readonly VERILATED_SYSTEM_DEFAULT="build/lowrisc_systems_top_earlgrey_verilator_0.1/sim-verilator/Vtop_earlgrey_verilator"
-readonly SW_BUILD_DEFAULT="$(sw_bin_dir sim-verilator)"
+readonly SW_BUILD_DEFAULT="$DEV_BIN_DIR"
 
 VERILATED_SYSTEM_PATH="${VERILATED_SYSTEM_PATH:-$VERILATED_SYSTEM_DEFAULT}"
 SW_BUILD_PATH="${SW_BUILD_PATH:-$SW_BUILD_DEFAULT}"
 
-BOOT_ROM_TARGET="boot_rom/boot_rom.elf"
+BOOT_ROM_TARGET="boot_rom/boot_rom_sim_verilator.elf"
 
 TEST_TARGETS=(
-  "examples/hello_usbdev/hello_usbdev.elf"
-  "tests/aes/aes_test.elf"
-  "tests/flash_ctrl/flash_test.elf"
-  "tests/hmac/sha256_test.elf"
-  "tests/rv_timer/rv_timer_test.elf"
+  "examples/hello_usbdev/hello_usbdev_sim_verilator.elf"
+  "tests/aes/aes_test_sim_verilator.elf"
+  "tests/flash_ctrl/flash_test_sim_verilator.elf"
+  "tests/hmac/sha256_test_sim_verilator.elf"
+  "tests/rv_timer/rv_timer_test_sim_verilator.elf"
 )
 
 if [[ ! -z ${MAKE_BUILD+x} ]]; then
diff --git a/doc/rm/ref_manual_fpga.md b/doc/rm/ref_manual_fpga.md
index 72dcf71..bda4755 100644
--- a/doc/rm/ref_manual_fpga.md
+++ b/doc/rm/ref_manual_fpga.md
@@ -63,9 +63,9 @@
 ```console
 $ cd ${REPO_TOP}
 $ ./meson_init.sh
-$ ninja -C build-out/sw/fpga all
+$ ninja -C build-out
 $ build-bin/sw/host/spiflash/spiflash \ 
-    --input build-bin/sw/device/fpga/examples/hello_world/hello_world.bin
+    --input build-bin/sw/device/examples/hello_world/hello_world_fpga_nexysvideo.bin
 
 Running SPI flash update.
 Image divided into 6 frames.
diff --git a/doc/ug/getting_started_fpga.md b/doc/ug/getting_started_fpga.md
index f5f97d8..7963ada 100644
--- a/doc/ug/getting_started_fpga.md
+++ b/doc/ug/getting_started_fpga.md
@@ -33,7 +33,7 @@
 ```console
 $ cd $REPO_TOP
 $ ./meson_init.sh
-$ ninja -C build-out/sw/fpga sw/device/boot_rom/boot_rom_export
+$ ninja -C build-out sw/device/boot_rom/boot_rom_export_fpga_nexysvideo
 ```
 
 In the following example we synthesize the Earl Grey design for the Nexys Video board using Xilinx Vivado 2018.3.
@@ -42,7 +42,7 @@
 $ . /tools/xilinx/Vivado/2018.3/settings64.sh
 $ cd $REPO_TOP
 $ ./meson_init.sh
-$ ninja -C build-out/sw/fpga sw/device/boot_rom/boot_rom_export
+$ ninja -C build-out sw/device/boot_rom/boot_rom_export_fpga_nexysvideo
 $ fusesoc --cores-root . run --target=synth lowrisc:systems:top_earlgrey_nexysvideo
 ```
 
@@ -123,10 +123,10 @@
   ```console
   $ cd ${REPO_TOP}
   $ ./meson_init.sh
-  $ ninja -C build-out/sw/fpga sw/device/examples/hello_world/hello_world_export
-  $ ninja -C build-out/sw/fpga sw/host/spiflash/spiflash_export
+  $ ninja -C build-out sw/device/examples/hello_world/hello_world_export_fpga_nexysvideo
+  $ ninja -C build-out sw/host/spiflash/spiflash_export
   $ build-bin/sw/host/spiflash/spiflash \
-      --input build-bin/sw/device/fpga/examples/hello_world/hello_world.bin
+      --input build-bin/sw/device/fpga/examples/hello_world/hello_world_fpga_nexysvideo.bin
   ```
 
   which should report how the binary is split into frames:
diff --git a/doc/ug/getting_started_sw.md b/doc/ug/getting_started_sw.md
index 15303fd..3e0ea56 100644
--- a/doc/ug/getting_started_sw.md
+++ b/doc/ug/getting_started_sw.md
@@ -17,13 +17,16 @@
 $ ./meson_init.sh
 
 # Build the two targets we care about, specifically.
-$ ninja -C build-out/sw/fpga sw/device/boot_rom/boot_rom_export
-$ ninja -C build-out/sw/fpga sw/device/examples/hello_world/hello_world_export
+$ ninja -C build-out sw/device/boot_rom/boot_rom_export_fpga_nexysvideo
+$ ninja -C build-out sw/device/examples/hello_world/hello_world_export_fpga_nexysvideo
 
-# Build *everything*.
-$ ninja -C build-out/sw/fpga all
+# Build *everything*, including targets for other devices.
+$ ninja -C build-out all
 ```
 
+Note that specific targets are followed by the device they are built for.
+OpenTitan needs to link the same device executable for multiple devices, so each executable target is duplicated one for each device we support.
+
 If your RISC-V toolchain isn't located in the default `/tools/riscv` location you use the `TOOLCHAIN_PATH` environment variable to set a different location before running `meson_init.sh`:
 
 ```console
@@ -43,11 +46,12 @@
 `./meson_init.sh` itself is idempotent, but this behavior can be changed with additional flags; see `./meson_init.sh` for more information.
 For this reason, most examples involving Meson will include a call to `./meson_init.sh`, but you will rarely need to run it more than once per checkout.
 
-Building an executable `foo` destined to run on an OpenTitan device (i.e., under `sw/device`) will output the following files under `build-bin/sw/device`:
-* `foo.elf`: the linked program, in ELF format.
-* `foo.bin`: the linked program, as a plain binary with ELF debug information removed.
-* `foo.dis`: the disassembled program with inline source code.
-* `foo.vmem`: a Verilog memory file which can be read by `$readmemh()` in Verilog code.
+Building an executable `foo` destined to run on the OpenTitan device `$DEVICE` will output the following files under `build-bin/sw/device`:
+* `foo_$DEVICE.elf`: the linked program, in ELF format.
+* `foo_$DEVICE.bin`: the linked program, as a plain binary with ELF debug information removed.
+* `foo_$DEVICE.dis`: the disassembled program with inline source code.
+* `foo_$DEVICE.vmem`: a Verilog memory file which can be read by `$readmemh()` in Verilog code.
+
+In general, this executable is built by building the `foo_export_$DEVICE` target.
 
 Building an executable destined to run on a host machine (i.e., under `sw/host`) will output a host excecutable under `build-bin/sw/host`, which can be run directly.
-Currently, each "platform" (`fpga`, `sim-verilator`, etc) have their own copies of all host targets; this is a limitation of our Meson setup, and they are otherwise indistinguishable.
diff --git a/doc/ug/getting_started_verilator.md b/doc/ug/getting_started_verilator.md
index 77e7d0f..c5be81d 100644
--- a/doc/ug/getting_started_verilator.md
+++ b/doc/ug/getting_started_verilator.md
@@ -31,7 +31,7 @@
 ```console
 $ cd $REPO_TOP
 $ ./meson_init.sh
-$ ninja -C build-out/sw/sim-verilator all
+$ ninja -C build-out all
 ```
 
 Now the simulation can be run.
@@ -40,8 +40,8 @@
 ```console
 $ cd $REPO_TOP
 $ build/lowrisc_systems_top_earlgrey_verilator_0.1/sim-verilator/Vtop_earlgrey_verilator \
-  --meminit=rom,build-bin/sw/device/sim-verilator/boot_rom/boot_rom.elf \
-  --meminit=flash,build-bin/sw/device/sim-verilator/examples/hello_world/hello_world.elf
+  --meminit=rom,build-bin/sw/device/boot_rom/boot_rom_sim_verilator.elf \
+  --meminit=flash,build-bin/sw/device/examples/hello_world/hello_world_sim_verilator.elf
 ```
 
 To stop the simulation press CTRL-c.
diff --git a/meson.build b/meson.build
index 5433a19..4cbd214 100644
--- a/meson.build
+++ b/meson.build
@@ -22,22 +22,11 @@
   ],
 )
 
-target = get_option('target')
-if target == 'undef'
-  error('target option not set. Please run meson with a valid build target option.')
-endif
-
 ot_version = get_option('ot_version')
 if ot_version == 'undef'
   error('ot_version option not set. Please run meson with a valid OpenTitan version option.')
 endif
 
-if target == 'sim-verilator'
-  # TODO: Consider using extra args array if using this flag globally is no
-  # longer OK.
-  add_project_arguments('-DSIMULATION', language: 'c')
-endif
-
 dev_bin_dir = get_option('dev_bin_dir')
 host_bin_dir = get_option('host_bin_dir')
 if dev_bin_dir == 'undef' or host_bin_dir == 'undef'
diff --git a/meson_init.sh b/meson_init.sh
index f8d7391..e627ef2 100755
--- a/meson_init.sh
+++ b/meson_init.sh
@@ -120,30 +120,24 @@
   perl -pi -e 's#-I[^/][^@ ]+ # #g' -- "$ninja_file"
 }
 
-for platform in ${PLATFORMS[@]}; do
-  obj_dir="$(sw_obj_dir "$platform")"
-  reconf="${FLAGS_reconfigure}"
+reconf="${FLAGS_reconfigure}"
 
-  if [[ ! -d "$obj_dir" ]]; then
-    echo "Output directory for $platform does not exist at $obj_dir; creating." >&2
-    mkdir -p "$obj_dir"
-    reconf=""
-  elif [[ -z "$reconf" ]]; then
-    echo "Output directory for $platform already exists at $obj_dir; skipping." >&2
-    continue
-  fi
+if [[ ! -d "$OBJ_DIR" ]]; then
+  echo "Output directory does not exist at $OBJ_DIR; creating." >&2
+  mkdir -p "$OBJ_DIR"
+  reconf=""
+elif [[ -z "$reconf" ]]; then
+  echo "Output directory already exists at $OBJ_DIR; skipping." >&2
+  continue
+fi
 
-  bin_dir="$(sw_bin_dir "$platform")"
-  mkdir -p "$bin_dir"
-
-  set -x
-  meson $reconf \
-    -Dtarget="$platform" \
-    -Dot_version="$OT_VERSION" \
-    -Ddev_bin_dir="$bin_dir" \
-    -Dhost_bin_dir="$HOST_BIN_DIR" \
-    --cross-file="$CROSS_FILE" \
-    "$obj_dir"
-  { set +x; } 2>/dev/null
-  purge_includes "$obj_dir"
-done
+mkdir -p "$DEV_BIN_DIR"
+set -x
+meson $reconf \
+  -Dot_version="$OT_VERSION" \
+  -Ddev_bin_dir="$DEV_BIN_DIR" \
+  -Dhost_bin_dir="$HOST_BIN_DIR" \
+  --cross-file="$CROSS_FILE" \
+  "$OBJ_DIR"
+{ set +x; } 2>/dev/null
+purge_includes "$OBJ_DIR"
diff --git a/meson_options.txt b/meson_options.txt
index 80f43d2..91713f2 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -1,15 +1,4 @@
 option(
-  'target',
-  type: 'combo',
-  choices: [
-    'sim-verilator',
-    'fpga',
-    'undef',
-  ],
-  value: 'undef',
-)
-
-option(
   'dev_bin_dir',
   type: 'string',
   value: 'undef',
diff --git a/sw/README.md b/sw/README.md
index 2dc2c80..b540f9c 100644
--- a/sw/README.md
+++ b/sw/README.md
@@ -10,10 +10,10 @@
 ```console
 $ cd "$REPO_TOP"
 $ ./meson_init.sh
-$ ninja -C build-out/sw/fpga sw/device/examples/hello_world/hello_world_export
+$ ninja -C build-out sw/device/examples/hello_world/hello_world_export_fpga_nexysvideo
 ```
 
-The resulting binaries will be located at `build-bin/sw/device/fpga/examples/hello_world`. For more information, check out [the relevant User Guide](../doc/ug/getting_started_sw.md).
+The resulting binaries will be located at `build-bin/sw/device/examples/hello_world`. For more information, check out [the relevant User Guide](../doc/ug/getting_started_sw.md).
 
 The location of the RISC-V toolchain is /tools/riscv by default.
 If your toolchain is located elsewhere set the `TOOLCHAIN_PATH` to that path before running `meson_init.sh`
diff --git a/sw/device/benchmarks/coremark/README.md b/sw/device/benchmarks/coremark/README.md
index ecdbe3d..56dfc20 100644
--- a/sw/device/benchmarks/coremark/README.md
+++ b/sw/device/benchmarks/coremark/README.md
@@ -5,10 +5,10 @@
 ```sh
 cd "${REPO_TOP}"
 ./meson_init.sh
-ninja -C build-out/sw/${TARGET} sw/device/benchmarks/coremark/coremark_export
+ninja -C build-out sw/device/benchmarks/coremark/coremark_export_${DEVICE}
 ```
 
-Where ${TARGET} is one of 'sim-verilator' or 'fpga'
+Where ${DEVICE} is one of 'sim_verilator' or 'fpga_nexysvideo'
 
 This will give you a .bin and .elf file (suitable for either spiflash or
 giving directly to `--meminit` for Verilator) which can be found in
diff --git a/sw/device/benchmarks/coremark/meson.build b/sw/device/benchmarks/coremark/meson.build
index 8e05ca8..53a159c 100644
--- a/sw/device/benchmarks/coremark/meson.build
+++ b/sw/device/benchmarks/coremark/meson.build
@@ -2,50 +2,53 @@
 # Licensed under the Apache License, Version 2.0, see LICENSE for details.
 # SPDX-License-Identifier: Apache-2.0
 
-if target == 'sim-verilator'
-  coremark_iterations = 1
-else
-  coremark_iterations = 100
-endif
+foreach device_name, device_lib : sw_lib_arch_core_devices
+  if device_name == 'sim_verilator'
+    coremark_iterations = 1
+  else
+    coremark_iterations = 100
+  endif
 
-coremark_top_earlgrey_elf = executable(
-  'coremark_top_earlgrey',
-  sources: [
-    'top_earlgrey/core_portme.c',
-    'top_earlgrey/ee_printf.c',
-    vendor_coremark_base_files,
-  ],
-  name_suffix: 'elf',
-  dependencies: [
-    sw_lib_uart,
-    sw_lib_mem,
-    riscv_crt,
-  ],
-  # Set up coremark-specific defines.
-  c_args: [
-    '-DITERATIONS=@0@'.format(coremark_iterations),
-    '-DPERFORMANCE_RUN=1',
-    '-DTOTAL_DATA_SIZE=2000',
-    '-DMAIN_HAS_NOARGC=1',
-  ],
-)
+  coremark_top_earlgrey_elf = executable(
+    'coremark_top_earlgrey_' + device_name,
+    sources: [
+      'top_earlgrey/core_portme.c',
+      'top_earlgrey/ee_printf.c',
+      vendor_coremark_base_files,
+    ],
+    name_suffix: 'elf',
+    dependencies: [
+      sw_lib_uart,
+      sw_lib_mem,
+      riscv_crt,
+      device_lib,
+    ],
+    # Set up coremark-specific defines.
+    c_args: [
+      '-DITERATIONS=@0@'.format(coremark_iterations),
+      '-DPERFORMANCE_RUN=1',
+      '-DTOTAL_DATA_SIZE=2000',
+      '-DMAIN_HAS_NOARGC=1',
+    ],
+  )
 
-coremark_top_earlgrey_embedded = custom_target(
-  'coremark_top_earlgrey',
-  command: make_embedded_target,
-  input: coremark_top_earlgrey_elf,
-  output: make_embedded_target_outputs,
-  build_by_default: true,
-)
+  coremark_top_earlgrey_embedded = custom_target(
+    'coremark_top_earlgrey_' + device_name,
+    command: make_embedded_target,
+    input: coremark_top_earlgrey_elf,
+    output: make_embedded_target_outputs,
+    build_by_default: true,
+  )
 
-custom_target(
-  'coremark_top_earlgrey_export',
-  command: export_embedded_target,
-  input: [
-    coremark_top_earlgrey_elf,
-    coremark_top_earlgrey_embedded,
-  ],
-  output: 'coremark_top_earlgrey_export',
-  build_always_stale: true,
-  build_by_default: true,
-)
+  custom_target(
+    'coremark_top_earlgrey_export_' + device_name,
+    command: export_embedded_target,
+    input: [
+      coremark_top_earlgrey_elf,
+      coremark_top_earlgrey_embedded,
+    ],
+    output: 'coremark_top_earlgrey_export_' + device_name,
+    build_always_stale: true,
+    build_by_default: true,
+  )
+endforeach
\ No newline at end of file
diff --git a/sw/device/benchmarks/coremark/top_earlgrey/core_portme.c b/sw/device/benchmarks/coremark/top_earlgrey/core_portme.c
index 4511446..2e8e142 100644
--- a/sw/device/benchmarks/coremark/top_earlgrey/core_portme.c
+++ b/sw/device/benchmarks/coremark/top_earlgrey/core_portme.c
@@ -7,6 +7,7 @@
 */
 #include "sw/device/benchmarks/coremark/top_earlgrey/core_portme.h"
 
+#include "sw/device/lib/arch/device.h"
 #include "sw/device/lib/base/stdasm.h"
 #include "sw/vendor/eembc_coremark/coremark.h"
 
@@ -106,7 +107,7 @@
         Test for some common mistakes.
 */
 void portable_init(core_portable *p, int *argc, char *argv[]) {
-  uart_init(UART_BAUD_RATE);
+  uart_init(kUartBaudrate);
 
   if (sizeof(ee_ptr_int) != sizeof(ee_u8 *)) {
     ee_printf(
diff --git a/sw/device/boot_rom/boot_rom.c b/sw/device/boot_rom/boot_rom.c
index a5b8644..d8b49ea 100644
--- a/sw/device/boot_rom/boot_rom.c
+++ b/sw/device/boot_rom/boot_rom.c
@@ -4,6 +4,7 @@
 
 #include "sw/device/boot_rom/bootstrap.h"
 #include "sw/device/boot_rom/chip_info.h"  // Generated.
+#include "sw/device/lib/arch/device.h"
 #include "sw/device/lib/base/stdasm.h"
 #include "sw/device/lib/common.h"
 #include "sw/device/lib/flash_ctrl.h"
@@ -27,7 +28,7 @@
 
 void _boot_start(void) {
   pinmux_init();
-  uart_init(UART_BAUD_RATE);
+  uart_init(kUartBaudrate);
   uart_send_str((char *)chip_info);
 
   int bootstrap_err = bootstrap();
diff --git a/sw/device/boot_rom/bootstrap.c b/sw/device/boot_rom/bootstrap.c
index 57855bd..30258fe 100644
--- a/sw/device/boot_rom/bootstrap.c
+++ b/sw/device/boot_rom/bootstrap.c
@@ -4,6 +4,7 @@
 
 #include "sw/device/boot_rom/bootstrap.h"
 
+#include "sw/device/lib/arch/device.h"
 #include "sw/device/lib/common.h"
 #include "sw/device/lib/flash_ctrl.h"
 #include "sw/device/lib/gpio.h"
@@ -14,14 +15,13 @@
 /* Checks if flash is blank to determine if bootstrap is needed. */
 /* TODO: Update this to check bootstrap pin instead in Verilator. */
 static int bootstrap_requested(void) {
-// The following flash empty-sniff-check is done this way due to the lack of
-// clear eflash reset in SIM environments.
-#if defined(SIMULATION)
-  return !!(REG32(FLASH_MEM_BASE_ADDR) == 0 ||
-            REG32(FLASH_MEM_BASE_ADDR) == 0xFFFFFFFF);
-#else
+  // The following flash empty-sniff-check is done this way due to the lack of
+  // clear eflash reset in SIM environments.
+  if (kDeviceType == kDeviceSimVerilator) {
+    return !!(REG32(FLASH_MEM_BASE_ADDR) == 0 ||
+              REG32(FLASH_MEM_BASE_ADDR) == 0xFFFFFFFF);
+  }
   return !!(gpio_read() & GPIO_BOOTSTRAP_BIT_MASK);
-#endif
 }
 
 /* Erase all flash, and verify blank. */
diff --git a/sw/device/boot_rom/meson.build b/sw/device/boot_rom/meson.build
index 7f26417..25ab123 100644
--- a/sw/device/boot_rom/meson.build
+++ b/sw/device/boot_rom/meson.build
@@ -21,44 +21,47 @@
 rom_link_args = ['-Wl,-T,@0@/@1@'.format(meson.source_root(), rom_linkfile[0]), '-Wl,--build-id=none']
 rom_link_deps = [rom_linkfile]
 
-boot_rom_elf = executable(
-  'boot_rom',
-  sources: [
-    'boot_rom.c',
-    'bootstrap.c',
-    'irq_vector.S',
-    'rom_crt.S',
-  ],
-  name_suffix: 'elf',
-  link_args: rom_link_args,
-  link_depends: rom_link_deps,
-  dependencies: [
-    chip_info_h,
-    sw_lib_runtime_hart,
-    sw_lib_flash_ctrl,
-    sw_lib_pinmux,
-    sw_lib_gpio,
-    sw_lib_hmac,
-    sw_lib_spi_device,
-    sw_lib_uart,
-    sw_lib_log,
-  ],
-)
+foreach device_name, device_lib : sw_lib_arch_core_devices
+  boot_rom_elf = executable(
+    'boot_rom_' + device_name,
+    sources: [
+      'boot_rom.c',
+      'bootstrap.c',
+      'irq_vector.S',
+      'rom_crt.S',
+    ],
+    name_suffix: 'elf',
+    link_args: rom_link_args,
+    link_depends: rom_link_deps,
+    dependencies: [
+      chip_info_h,
+      sw_lib_runtime_hart,
+      sw_lib_flash_ctrl,
+      sw_lib_pinmux,
+      sw_lib_gpio,
+      sw_lib_hmac,
+      sw_lib_spi_device,
+      sw_lib_uart,
+      sw_lib_log,
+      device_lib,
+    ],
+  )
 
-boot_rom_embedded = custom_target(
-  'boot_rom',
-  command: make_embedded_target,
-  input: boot_rom_elf,
-  output: make_embedded_target_outputs,
-  build_by_default: true,
-)
+  boot_rom_embedded = custom_target(
+    'boot_rom_' + device_name,
+    command: make_embedded_target,
+    input: boot_rom_elf,
+    output: make_embedded_target_outputs,
+    build_by_default: true,
+  )
 
-custom_target(
-  'boot_rom_export',
-  command: export_embedded_target,
-  input: [boot_rom_elf, boot_rom_embedded],
-  output: 'boot_rom_export',
-  build_always_stale: true,
-  build_by_default: true,
-)
+  custom_target(
+    'boot_rom_export_' + device_name,
+    command: export_embedded_target,
+    input: [boot_rom_elf, boot_rom_embedded],
+    output: 'boot_rom_export_' + device_name,
+    build_always_stale: true,
+    build_by_default: true,
+  )
+endforeach
 
diff --git a/sw/device/examples/hello_usbdev/hello_usbdev.c b/sw/device/examples/hello_usbdev/hello_usbdev.c
index 3182b3e..8994c2b 100644
--- a/sw/device/examples/hello_usbdev/hello_usbdev.c
+++ b/sw/device/examples/hello_usbdev/hello_usbdev.c
@@ -3,6 +3,8 @@
 // SPDX-License-Identifier: Apache-2.0
 
 #include <stdbool.h>
+
+#include "sw/device/lib/arch/device.h"
 #include "sw/device/lib/base/stdasm.h"
 #include "sw/device/lib/common.h"
 #include "sw/device/lib/gpio.h"
@@ -73,7 +75,7 @@
 }
 
 int main(int argc, char **argv) {
-  uart_init(UART_BAUD_RATE);
+  uart_init(kUartBaudrate);
 
   pinmux_init();
   // Enable GPIO: 0-7 and 16 is input, 8-15 is output
diff --git a/sw/device/examples/hello_usbdev/meson.build b/sw/device/examples/hello_usbdev/meson.build
index 86bc516..149b52d 100644
--- a/sw/device/examples/hello_usbdev/meson.build
+++ b/sw/device/examples/hello_usbdev/meson.build
@@ -2,36 +2,39 @@
 # Licensed under the Apache License, Version 2.0, see LICENSE for details.
 # SPDX-License-Identifier: Apache-2.0
 
-hello_usbdev_elf = executable(
-  'hello_usbdev',
-  sources: ['hello_usbdev.c'],
-  name_suffix: 'elf',
-  dependencies: [
+foreach device_name, device_lib : sw_lib_arch_core_devices
+  hello_usbdev_elf = executable(
+    'hello_usbdev_' + device_name,
+    sources: ['hello_usbdev.c'],
+    name_suffix: 'elf',
+    dependencies: [
     sw_lib_runtime_hart,
     sw_lib_pinmux,
     sw_lib_gpio,
     sw_lib_irq,
     sw_lib_spi_device,
     sw_lib_uart,
-    sw_lib_usb,
-    riscv_crt,
-    sw_lib_irq_handlers,
-  ],
-)
+      sw_lib_usb,
+      riscv_crt,
+      sw_lib_irq_handlers,
+      device_lib,
+    ],
+  )
 
-hello_usbdev_embedded = custom_target(
-  'hello_usbdev',
-  command: make_embedded_target,
-  input: hello_usbdev_elf,
-  output: make_embedded_target_outputs,
-  build_by_default: true,
-)
+  hello_usbdev_embedded = custom_target(
+    'hello_usbdev_' + device_name,
+    command: make_embedded_target,
+    input: hello_usbdev_elf,
+    output: make_embedded_target_outputs,
+    build_by_default: true,
+  )
 
-custom_target(
-  'hello_usbdev_export',
-  command: export_embedded_target,
-  input: [hello_usbdev_elf, hello_usbdev_embedded],
-  output: 'hello_usbdev_export',
-  build_always_stale: true,
-  build_by_default: true,
-)
+  custom_target(
+    'hello_usbdev_export_' + device_name,
+    command: export_embedded_target,
+    input: [hello_usbdev_elf, hello_usbdev_embedded],
+    output: 'hello_usbdev_export_' + device_name,
+    build_always_stale: true,
+    build_by_default: true,
+  )
+endforeach
diff --git a/sw/device/examples/hello_world/hello_world.c b/sw/device/examples/hello_world/hello_world.c
index b91aa7d..4afaea7 100644
--- a/sw/device/examples/hello_world/hello_world.c
+++ b/sw/device/examples/hello_world/hello_world.c
@@ -2,6 +2,7 @@
 // Licensed under the Apache License, Version 2.0, see LICENSE for details.
 // SPDX-License-Identifier: Apache-2.0
 
+#include "sw/device/lib/arch/device.h"
 #include "sw/device/lib/base/stdasm.h"
 #include "sw/device/lib/common.h"
 #include "sw/device/lib/gpio.h"
@@ -28,7 +29,7 @@
 #define MK_PRINT(c) (((c < 32) || (c > 126)) ? '_' : c)
 
 int main(int argc, char **argv) {
-  uart_init(UART_BAUD_RATE);
+  uart_init(kUartBaudrate);
 
   pinmux_init();
   // Enable GPIO: 0-7 and 16 is input, 8-15 is output
diff --git a/sw/device/examples/hello_world/meson.build b/sw/device/examples/hello_world/meson.build
index 11c3099..b02d3c9 100644
--- a/sw/device/examples/hello_world/meson.build
+++ b/sw/device/examples/hello_world/meson.build
@@ -2,35 +2,38 @@
 # Licensed under the Apache License, Version 2.0, see LICENSE for details.
 # SPDX-License-Identifier: Apache-2.0
 
-hello_world_elf = executable(
-  'hello_world',
-  sources: ['hello_world.c'],
-  name_suffix: 'elf',
-  dependencies: [
-    sw_lib_runtime_hart,
-    sw_lib_pinmux,
-    sw_lib_gpio,
-    sw_lib_irq,
-    sw_lib_spi_device,
-    sw_lib_uart,
-    riscv_crt,
-    sw_lib_irq_handlers,
-  ],
-)
+foreach device_name, device_lib : sw_lib_arch_core_devices
+  hello_world_elf = executable(
+    'hello_world_' + device_name,
+    sources: ['hello_world.c'],
+    name_suffix: 'elf',
+    dependencies: [
+      sw_lib_runtime_hart,
+      sw_lib_pinmux,
+      sw_lib_gpio,
+      sw_lib_irq,
+      sw_lib_spi_device,
+      sw_lib_uart,
+      riscv_crt,
+      sw_lib_irq_handlers,
+      device_lib,
+    ],
+  )
 
-hello_world_embedded = custom_target(
-  'hello_world',
-  command: make_embedded_target,
-  input: hello_world_elf,
-  output: make_embedded_target_outputs,
-  build_by_default: true,
-)
+  hello_world_embedded = custom_target(
+    'hello_world_' + device_name,
+    command: make_embedded_target,
+    input: hello_world_elf,
+    output: make_embedded_target_outputs,
+    build_by_default: true,
+  )
 
-custom_target(
-  'hello_world_export',
-  command: export_embedded_target,
-  input: [hello_world_elf, hello_world_embedded],
-  output: 'hello_world_export',
-  build_always_stale: true,
-  build_by_default: true,
-)
+  custom_target(
+    'hello_world_export_' + device_name,
+    command: export_embedded_target,
+    input: [hello_world_elf, hello_world_embedded],
+    output: 'hello_world_export_' + device_name,
+    build_always_stale: true,
+    build_by_default: true,
+  )
+endforeach
diff --git a/sw/device/lib/arch/README.md b/sw/device/lib/arch/README.md
new file mode 100644
index 0000000..3ebfc18
--- /dev/null
+++ b/sw/device/lib/arch/README.md
@@ -0,0 +1,56 @@
+# Device-specific Symbols
+
+This subtree provides the header `device.h`, which contains declarations for symbols that represent device-specific information, like the clock frequency.
+
+## Using `device.h`
+
+When a library needs to make use of device-specific information, it should only pull in the header itself, and not depend directly on any of the libraries in this directory.
+Instead, the symbols' definitions will be provided at link time by the executable's build rule, which should depend on exactly one of the libraries in this directory (failing to do so is Undefined Behavior).
+
+If an executable is designed to be device-independent (i.e., obtains all of its device-specific information from `device.h`), the following Meson template may be used to generate executable targets for most devices:
+```meson
+foreach device_name, device_lib : sw_lib_arch_core_devices
+  executable(
+    'my_binary_' + device_name,
+    sources: [...],
+    dependencies: [
+      ...,
+      device_lib,
+    ],
+  )
+  
+  # ...
+endforeach
+```
+Note that this will not generate targets for some specialized devices, such as DV testbenches.
+
+## Adding a new device
+
+It is sometimes necessary to add a new device. The following considerations should be taken:
+- Make sure to add a new entry to `device_type_t`. Multiple variants of the same device (e.g., a DV testbench with different settings) may use the same `device_type_t`.
+- If your device is not "specialized" (i.e., not a DV testbench), it should be added to `sw_lib_arch_core_devices`.
+- Users should only ever link in exactly one target from this directory.
+  If multiple devices share a lot of the same symbol definitions, those can be factored into a separate `.c` file, but the targets should, ultimately, look like this:
+  ```meson
+  sw_lib_arch_my_dev1 = declare_dependency(
+    link_with: static_library(
+      'my_dev1',
+      sources: [
+        'device_my_dev_base.c'
+        'device_my_dev1.c'
+      ],
+    ),
+  )
+
+  sw_lib_arch_my_dev2 = declare_dependency(
+    link_with: static_library(
+      'my_dev2',
+      sources: [
+        'device_my_dev_base.c'
+        'device_my_dev2.c'
+      ],
+    ),
+  )
+  ```
+- Your device may be specialized and require symbols that are straight-up not meaningful on other devices.
+  These symbols should be specified in a separate header in this subtree, and targets that use that header should not be declared using the `sw_lib_arch_core_devices` shorthand above.
\ No newline at end of file
diff --git a/sw/device/lib/arch/device.h b/sw/device/lib/arch/device.h
new file mode 100644
index 0000000..8941eff
--- /dev/null
+++ b/sw/device/lib/arch/device.h
@@ -0,0 +1,56 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+#ifndef OPENTITAN_SW_DEVICE_LIB_ARCH_DEVICE_H_
+#define OPENTITAN_SW_DEVICE_LIB_ARCH_DEVICE_H_
+
+#include <stdint.h>
+
+/**
+ * This header contains "device-specific" declarations, i.e., information that
+ * all devices are known to provide, but which is specific to the particular
+ * choice of platform, which can range from a software simulation, like
+ * Verilator or a DV testbench, to real harware, like an FPGA or ASIC.
+ *
+ * Definitions for these symbols can be found in other files in this directory,
+ * which should be linked in depending on which platform an executable is
+ * intended for.
+ */
+
+/**
+ * A |device_type_t| represents a particular device type for which
+ * device-specific symbols are available.
+ */
+typedef enum device_type {
+  /**
+   * Represents the "Verilator" device, i.e., a synthesis of the OpenTitan
+   * design by Verilator into C++.
+   */
+  kDeviceSimVerilator,
+  /**
+   * Represents the "Nexys Video FPGA" device, i.e., the particular FPGA board
+   * blessed for OpenTitan development, containing a Xilinx FPGA.
+   */
+  kDeviceFpgaNexysVideo,
+} device_type_t;
+
+/**
+ * Indicates the device that this program has been linked for.
+ *
+ * This can be used, for example, for conditioning an operation on the precise
+ * device type.
+ */
+extern const device_type_t kDeviceType;
+
+/**
+ * The clock frequency of the device, in hertz.
+ */
+extern const uint64_t kClockFreqHz;
+
+/**
+ * The baudrate of the UART peripheral (if such a thing is present).
+ */
+extern const uint64_t kUartBaudrate;
+
+#endif  // OPENTITAN_SW_DEVICE_LIB_ARCH_DEVICE_H_
diff --git a/sw/device/lib/arch/device_fpga_nexysvideo.c b/sw/device/lib/arch/device_fpga_nexysvideo.c
new file mode 100644
index 0000000..f7ee37a
--- /dev/null
+++ b/sw/device/lib/arch/device_fpga_nexysvideo.c
@@ -0,0 +1,15 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+#include "sw/device/lib/arch/device.h"
+
+/**
+ * Device-specific symbol definitions for the Nexysvideo device.
+ */
+
+const device_type_t kDeviceType = kDeviceFpgaNexysVideo;
+
+const uint64_t kClockFreqHz = 50 * 1000 * 1000;  // 50MHz
+
+const uint64_t kUartBaudrate = 230400;
diff --git a/sw/device/lib/arch/device_sim_verilator.c b/sw/device/lib/arch/device_sim_verilator.c
new file mode 100644
index 0000000..fa84314
--- /dev/null
+++ b/sw/device/lib/arch/device_sim_verilator.c
@@ -0,0 +1,15 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+#include "sw/device/lib/arch/device.h"
+
+/**
+ * Device-specific symbol definitions for the Verilator device.
+ */
+
+const device_type_t kDeviceType = kDeviceSimVerilator;
+
+const uint64_t kClockFreqHz = 500 * 1000;  // 500kHz
+
+const uint64_t kUartBaudrate = 9600;
diff --git a/sw/device/lib/arch/meson.build b/sw/device/lib/arch/meson.build
new file mode 100644
index 0000000..10654cf
--- /dev/null
+++ b/sw/device/lib/arch/meson.build
@@ -0,0 +1,26 @@
+# Copyright lowRISC contributors.
+# Licensed under the Apache License, Version 2.0, see LICENSE for details.
+# SPDX-License-Identifier: Apache-2.0
+
+sw_lib_arch_sim_verilator = declare_dependency(
+  link_with: static_library(
+    'device_sim_verilator',
+    sources: ['device_sim_verilator.c'],
+  ),
+)
+
+sw_lib_arch_fpga_nexysvideo = declare_dependency(
+  link_with: static_library(
+    'device_fpga_nexysvideo',
+    sources: ['device_fpga_nexysvideo.c'],
+  ),
+)
+
+# Devices that are considered "basic" or "generic", and which
+# can run general purpose programs.
+# Effectively, this is all devices that are not specialized DV
+# testbenches.
+sw_lib_arch_core_devices = {
+  'sim_verilator': sw_lib_arch_sim_verilator,
+  'fpga_nexysvideo': sw_lib_arch_fpga_nexysvideo,
+}
\ No newline at end of file
diff --git a/sw/device/lib/common.h b/sw/device/lib/common.h
index 75f2e30..73fd67a 100644
--- a/sw/device/lib/common.h
+++ b/sw/device/lib/common.h
@@ -5,12 +5,6 @@
 #ifndef OPENTITAN_SW_DEVICE_LIB_COMMON_H_
 #define OPENTITAN_SW_DEVICE_LIB_COMMON_H_
 
-#ifdef SIMULATION
-static const unsigned long UART_BAUD_RATE = 9600;
-#else
-static const unsigned long UART_BAUD_RATE = 230400;
-#endif
-
 // Flash memory base defines, _SZ are presented in bytes
 #define FLASH_MEM_BASE_ADDR 0x20000000
 #define FLASH_WORDS_PER_PAGE 256
diff --git a/sw/device/lib/meson.build b/sw/device/lib/meson.build
index 90f6dd4..07e1d43 100644
--- a/sw/device/lib/meson.build
+++ b/sw/device/lib/meson.build
@@ -3,6 +3,7 @@
 # SPDX-License-Identifier: Apache-2.0
 
 subdir('base')
+subdir('arch')
 subdir('runtime')
 subdir('testing')
 subdir('dif')
diff --git a/sw/device/lib/runtime/hart.c b/sw/device/lib/runtime/hart.c
index 24d4e32..c9a1a08 100644
--- a/sw/device/lib/runtime/hart.c
+++ b/sw/device/lib/runtime/hart.c
@@ -6,12 +6,13 @@
 
 #include <stdbool.h>
 
+#include "sw/device/lib/arch/device.h"
 #include "sw/device/lib/runtime/ibex.h"
 
 extern void wait_for_interrupt(void);
 
 void usleep(uint32_t usec) {
-  uint64_t cycles = (uint64_t)kIbexClockFreqHz * usec / 1000000;
+  uint64_t cycles = kClockFreqHz * usec / 1000000;
   uint64_t start = ibex_mcycle_read();
   while ((ibex_mcycle_read() - start) < cycles) {
   }
diff --git a/sw/device/lib/runtime/ibex.c b/sw/device/lib/runtime/ibex.c
index 8e06570..7a6e1f6 100644
--- a/sw/device/lib/runtime/ibex.c
+++ b/sw/device/lib/runtime/ibex.c
@@ -4,10 +4,4 @@
 
 #include "sw/device/lib/runtime/ibex.h"
 
-#ifdef SIMULATION
-const size_t kIbexClockFreqHz = 500 * 1000;  // 500 kHz
-#else
-const size_t kIbexClockFreqHz = 50 * 1000 * 1000;  // 50 MHz
-#endif
-
 extern uint64_t ibex_mcycle_read();
diff --git a/sw/device/lib/runtime/ibex.h b/sw/device/lib/runtime/ibex.h
index b5b6fe9..d6ca643 100644
--- a/sw/device/lib/runtime/ibex.h
+++ b/sw/device/lib/runtime/ibex.h
@@ -10,16 +10,11 @@
 #include "sw/device/lib/base/stdasm.h"
 
 /**
- * This header provides Ibex-specific functions, such as the clock frequency and
- * cycle-accurate busy loops.
+ * This header provides Ibex-specific functions, such as cycle-accurate busy
+ * loops.
  */
 
 /**
- * The clock frequency of the Ibex core, in hertz.
- */
-extern const size_t kIbexClockFreqHz;
-
-/**
  * Read the cycle counter.
  *
  * The value of the counter is stored across two 32-bit registers: |mcycle| and
diff --git a/sw/device/lib/rv_timer.c b/sw/device/lib/rv_timer.c
index f6f1b4a..aa8d8db 100644
--- a/sw/device/lib/rv_timer.c
+++ b/sw/device/lib/rv_timer.c
@@ -5,6 +5,7 @@
 #include "sw/device/lib/rv_timer.h"
 
 #include "rv_timer_regs.h"  // Generated.
+#include "sw/device/lib/arch/device.h"
 #include "sw/device/lib/common.h"
 #include "sw/device/lib/runtime/ibex.h"
 
@@ -14,7 +15,7 @@
 static const uint32_t NS_IN_S = 1000 * 1000 * 1000;
 
 void rv_timer_set_us_tick(uint32_t hart) {
-  uint32_t ticks_per_us = (uint32_t)((1000 * kIbexClockFreqHz) / NS_IN_S) - 1;
+  uint32_t ticks_per_us = (uint32_t)((1000 * kClockFreqHz) / NS_IN_S) - 1;
 
   REG32(RV_TIMER_CFG0(0) + hart * 4) =
       (ticks_per_us & RV_TIMER_CFG0_PRESCALE_MASK) |
diff --git a/sw/device/lib/uart.c b/sw/device/lib/uart.c
index 7254b80..5ed7da7 100644
--- a/sw/device/lib/uart.c
+++ b/sw/device/lib/uart.c
@@ -4,6 +4,7 @@
 
 #include "sw/device/lib/uart.h"
 
+#include "sw/device/lib/arch/device.h"
 #include "sw/device/lib/common.h"
 #include "sw/device/lib/dif/dif_uart.h"
 #include "sw/device/lib/runtime/ibex.h"
@@ -14,7 +15,7 @@
 void uart_init(unsigned int baud) {
   dif_uart_config_t config = {
       .baudrate = baud,
-      .clk_freq_hz = kIbexClockFreqHz,
+      .clk_freq_hz = kClockFreqHz,
       .parity_enable = kDifUartDisable,
       .parity = kDifUartParityEven,
   };
diff --git a/sw/device/tests/aes/aes_test.c b/sw/device/tests/aes/aes_test.c
index 9c54895..c3cb3f0 100644
--- a/sw/device/tests/aes/aes_test.c
+++ b/sw/device/tests/aes/aes_test.c
@@ -3,6 +3,8 @@
 // SPDX-License-Identifier: Apache-2.0
 
 #include "sw/device/lib/aes.h"
+
+#include "sw/device/lib/arch/device.h"
 #include "sw/device/lib/common.h"
 #include "sw/device/lib/uart.h"
 
@@ -32,7 +34,7 @@
 
   uint8_t buffer[16];
 
-  uart_init(UART_BAUD_RATE);
+  uart_init(kUartBaudrate);
   uart_send_str("Running AES test\r\n");
 
   // Setup AES config
diff --git a/sw/device/tests/aes/meson.build b/sw/device/tests/aes/meson.build
index 12668a4..64795f6 100644
--- a/sw/device/tests/aes/meson.build
+++ b/sw/device/tests/aes/meson.build
@@ -2,31 +2,34 @@
 # Licensed under the Apache License, Version 2.0, see LICENSE for details.
 # SPDX-License-Identifier: Apache-2.0
 
-aes_test_elf = executable(
-  'aes_test',
-  sources: ['aes_test.c'],
-  name_suffix: 'elf',
-  dependencies: [
-    sw_lib_aes,
-    sw_lib_uart,
-    sw_lib_mem,
-    riscv_crt,
-  ],
-)
+foreach device_name, device_lib : sw_lib_arch_core_devices
+  aes_test_elf = executable(
+    'aes_test_' + device_name,
+    sources: ['aes_test.c'],
+    name_suffix: 'elf',
+    dependencies: [
+      sw_lib_aes,
+      sw_lib_uart,
+      sw_lib_mem,
+      riscv_crt,
+      device_lib,
+    ],
+  )
 
-aes_test_embedded = custom_target(
-  'aes_test',
-  command: make_embedded_target,
-  input: aes_test_elf,
-  output: make_embedded_target_outputs,
-  build_by_default: true,
-)
+  aes_test_embedded = custom_target(
+    'aes_test_' + device_name,
+    command: make_embedded_target,
+    input: aes_test_elf,
+    output: make_embedded_target_outputs,
+    build_by_default: true,
+  )
 
-custom_target(
-  'aes_test_export',
-  command: export_embedded_target,
-  input: [aes_test_elf, aes_test_embedded],
-  output: 'aes_test',
-  build_always_stale: true,
-  build_by_default: true,
-)
+  custom_target(
+    'aes_test_export_' + device_name,
+    command: export_embedded_target,
+    input: [aes_test_elf, aes_test_embedded],
+    output: 'aes_test_' + device_name,
+    build_always_stale: true,
+    build_by_default: true,
+  )
+endforeach
diff --git a/sw/device/tests/flash_ctrl/flash_test.c b/sw/device/tests/flash_ctrl/flash_test.c
index 473547c..08cdd5a 100644
--- a/sw/device/tests/flash_ctrl/flash_test.c
+++ b/sw/device/tests/flash_ctrl/flash_test.c
@@ -2,6 +2,7 @@
 // Licensed under the Apache License, Version 2.0, see LICENSE for details.
 // SPDX-License-Identifier: Apache-2.0
 
+#include "sw/device/lib/arch/device.h"
 #include "sw/device/lib/base/stdasm.h"
 #include "sw/device/lib/common.h"
 #include "sw/device/lib/flash_ctrl.h"
@@ -38,7 +39,7 @@
   uint32_t bank0_last_page =
       FLASH_MEM_BASE_ADDR + (FLASH_PAGES_PER_BANK - 1) * FLASH_PAGE_SZ;
 
-  uart_init(UART_BAUD_RATE);
+  uart_init(kUartBaudrate);
   flash_init_block();
 
   // enable all access
diff --git a/sw/device/tests/flash_ctrl/meson.build b/sw/device/tests/flash_ctrl/meson.build
index 4be3f42..429e7ef 100644
--- a/sw/device/tests/flash_ctrl/meson.build
+++ b/sw/device/tests/flash_ctrl/meson.build
@@ -2,35 +2,37 @@
 # Licensed under the Apache License, Version 2.0, see LICENSE for details.
 # SPDX-License-Identifier: Apache-2.0
 
-flash_test_elf = executable(
-  'flash_test',
-  sources: ['flash_test.c'],
-  name_suffix: 'elf',
-  dependencies: [
-    sw_lib_runtime_hart,
-    sw_lib_flash_ctrl,
-    sw_lib_gpio,
-    sw_lib_irq,
-    sw_lib_uart,
-    riscv_crt,
-    sw_lib_irq_handlers,
-  ],
-)
+foreach device_name, device_lib : sw_lib_arch_core_devices
+  flash_test_elf = executable(
+    'flash_test_' + device_name,
+    sources: ['flash_test.c'],
+    name_suffix: 'elf',
+    dependencies: [
+      sw_lib_runtime_hart,
+      sw_lib_flash_ctrl,
+      sw_lib_gpio,
+      sw_lib_irq,
+      sw_lib_uart,
+      riscv_crt,
+      sw_lib_irq_handlers,
+      device_lib,
+    ],
+  )
 
-flash_test_embedded = custom_target(
-  'flash_test',
-  command: make_embedded_target,
-  input: flash_test_elf,
-  output: make_embedded_target_outputs,
-  build_by_default: true,
-)
+  flash_test_embedded = custom_target(
+    'flash_test_' + device_name,
+    command: make_embedded_target,
+    input: flash_test_elf,
+    output: make_embedded_target_outputs,
+    build_by_default: true,
+  )
 
-custom_target(
-  'flash_test_export',
-  command: export_embedded_target,
-  input: [flash_test_elf, flash_test_embedded],
-  output: 'flash_test',
-  build_always_stale: true,
-  build_by_default: true,
-)
-
+  custom_target(
+    'flash_test_export_' + device_name,
+    command: export_embedded_target,
+    input: [flash_test_elf, flash_test_embedded],
+    output: 'flash_test_' + device_name,
+    build_always_stale: true,
+    build_by_default: true,
+  )
+endforeach
diff --git a/sw/device/tests/hmac/meson.build b/sw/device/tests/hmac/meson.build
index 90bb33f..3e0c57b 100644
--- a/sw/device/tests/hmac/meson.build
+++ b/sw/device/tests/hmac/meson.build
@@ -2,34 +2,36 @@
 # Licensed under the Apache License, Version 2.0, see LICENSE for details.
 # SPDX-License-Identifier: Apache-2.0
 
-sha256_test_elf = executable(
-  'sha256_test',
-  sources: ['sha256_test.c'],
-  name_suffix: 'elf',
-  dependencies: [
-    sw_lib_flash_ctrl,
-    sw_lib_hmac,
-    sw_lib_irq,
-    sw_lib_uart,
-    riscv_crt,
-    sw_lib_irq_handlers,
-  ],
-)
+foreach device_name, device_lib : sw_lib_arch_core_devices
+  sha256_test_elf = executable(
+    'sha256_test_' + device_name,
+    sources: ['sha256_test.c'],
+    name_suffix: 'elf',
+    dependencies: [
+      sw_lib_flash_ctrl,
+      sw_lib_hmac,
+      sw_lib_irq,
+      sw_lib_uart,
+      riscv_crt,
+      sw_lib_irq_handlers,
+      device_lib,
+    ],
+  )
 
-sha256_test_embedded = custom_target(
-  'sha256_test',
-  command: make_embedded_target,
-  input: sha256_test_elf,
-  output: make_embedded_target_outputs,
-  build_by_default: true,
-)
+  sha256_test_embedded = custom_target(
+    'sha256_test_' + device_name,
+    command: make_embedded_target,
+    input: sha256_test_elf,
+    output: make_embedded_target_outputs,
+    build_by_default: true,
+  )
 
-custom_target(
-  'sha256_test_export',
-  command: export_embedded_target,
-  input: [sha256_test_elf, sha256_test_embedded],
-  output: 'sha256_test',
-  build_always_stale: true,
-  build_by_default: true,
-)
-
+  custom_target(
+    'sha256_test_export_' + device_name,
+    command: export_embedded_target,
+    input: [sha256_test_elf, sha256_test_embedded],
+    output: 'sha256_test_' + device_name,
+    build_always_stale: true,
+    build_by_default: true,
+  )
+endforeach
diff --git a/sw/device/tests/hmac/sha256_test.c b/sw/device/tests/hmac/sha256_test.c
index e360e82..7c9ec8e 100644
--- a/sw/device/tests/hmac/sha256_test.c
+++ b/sw/device/tests/hmac/sha256_test.c
@@ -2,6 +2,7 @@
 // Licensed under the Apache License, Version 2.0, see LICENSE for details.
 // SPDX-License-Identifier: Apache-2.0
 
+#include "sw/device/lib/arch/device.h"
 #include "sw/device/lib/common.h"
 #include "sw/device/lib/flash_ctrl.h"
 #include "sw/device/lib/hw_sha256.h"
@@ -19,7 +20,7 @@
                                             0x19f5cff7, 0x4ec9c6d6};
 
 int main(int argc, char **argv) {
-  uart_init(UART_BAUD_RATE);
+  uart_init(kUartBaudrate);
   uart_send_str("Running SHA256 test\r\n");
 
   uint32_t digest[8];
diff --git a/sw/device/tests/rv_timer/meson.build b/sw/device/tests/rv_timer/meson.build
index d4ad549..4e1177d 100644
--- a/sw/device/tests/rv_timer/meson.build
+++ b/sw/device/tests/rv_timer/meson.build
@@ -2,34 +2,37 @@
 # Licensed under the Apache License, Version 2.0, see LICENSE for details.
 # SPDX-License-Identifier: Apache-2.0
 
-rv_timer_test_elf = executable(
-  'rv_timer_test',
-  sources: ['rv_timer_test.c'],
-  name_suffix: 'elf',
-  dependencies: [
-    sw_lib_irq,
-    sw_lib_rv_timer,
-    sw_lib_uart,
-    sw_lib_gpio,
-    sw_lib_pinmux,
-    riscv_crt,
-    sw_lib_irq_handlers,
-  ],
-)
+foreach device_name, device_lib : sw_lib_arch_core_devices
+  rv_timer_test_elf = executable(
+    'rv_timer_test_' + device_name,
+    sources: ['rv_timer_test.c'],
+    name_suffix: 'elf',
+    dependencies: [
+      sw_lib_irq,
+      sw_lib_rv_timer,
+      sw_lib_uart,
+      sw_lib_gpio,
+      sw_lib_pinmux,
+      riscv_crt,
+      sw_lib_irq_handlers,
+      device_lib,
+    ],
+  )
 
-rv_timer_test_embedded = custom_target(
-  'rv_timer_test',
-  command: make_embedded_target,
-  input: rv_timer_test_elf,
-  output: make_embedded_target_outputs,
-  build_by_default: true,
-)
+  rv_timer_test_embedded = custom_target(
+    'rv_timer_test_' + device_name,
+    command: make_embedded_target,
+    input: rv_timer_test_elf,
+    output: make_embedded_target_outputs,
+    build_by_default: true,
+  )
 
-custom_target(
-  'rv_timer_test_export',
-  command: export_embedded_target,
-  input: [rv_timer_test_elf, rv_timer_test_embedded],
-  output: 'rv_timer_test',
-  build_always_stale: true,
-  build_by_default: true,
-)
+  custom_target(
+    'rv_timer_test_export_' + device_name,
+    command: export_embedded_target,
+    input: [rv_timer_test_elf, rv_timer_test_embedded],
+    output: 'rv_timer_test_' + device_name,
+    build_always_stale: true,
+    build_by_default: true,
+  )
+endforeach
\ No newline at end of file
diff --git a/sw/device/tests/rv_timer/rv_timer_test.c b/sw/device/tests/rv_timer/rv_timer_test.c
index 017e747..79d739e 100644
--- a/sw/device/tests/rv_timer/rv_timer_test.c
+++ b/sw/device/tests/rv_timer/rv_timer_test.c
@@ -4,6 +4,7 @@
 
 #include "sw/device/lib/rv_timer.h"
 
+#include "sw/device/lib/arch/device.h"
 #include "sw/device/lib/common.h"
 #include "sw/device/lib/gpio.h"
 #include "sw/device/lib/irq.h"
@@ -16,7 +17,7 @@
 int main(int argc, char **argv) {
   const uint64_t cmp = 0x000000000000000F;
 
-  uart_init(UART_BAUD_RATE);
+  uart_init(kUartBaudrate);
 
   pinmux_init();
   // Enable GPIO: 0-7 and 16 is input, 8-15 is output
diff --git a/sw/host/spiflash/README.md b/sw/host/spiflash/README.md
index af32b7c..4630645 100644
--- a/sw/host/spiflash/README.md
+++ b/sw/host/spiflash/README.md
@@ -21,7 +21,7 @@
 ```console
 $ cd ${REPO_TOP}
 $ ./meson_init.sh
-$ ninja -C build-out/sw/fpga sw/host/spiflash/spiflash_export
+$ ninja -C build-out sw/host/spiflash/spiflash_export
 ```
 
 ## Setup instructions for Verilator and FPGA
@@ -29,21 +29,21 @@
 
 ## Build boot ROM and demo program
 
-_If building for verilator, build in `build-out/sw/sim-verilator` instead._
-
 Build `boot_rom`:
 ```console
 $ cd ${REPO_TOP}
 $ ./meson_init.sh
-$ ninja -C build-out/sw/fpga sw/device/boot_rom/boot_rom_export
+$ ninja -C build-out sw/device/boot_rom/boot_rom_export_${DEVICE}
 ```
 
 Build the `hello_world` program:
 ```console
 $ cd ${REPO_TOP}
-$ ninja -C build-out/sw/fpga sw/device/examples/hello_world/hello_world_export
+$ ninja -C build-out sw/device/examples/hello_world/hello_world_export_${DEVICE}
 ```
 
+Where ${DEVICE} is one of 'sim_verilator' or 'fpga_nexysvideo'
+
 ## Run the tool in Verilator
 
 Run Verilator with boot_rom enabled:
@@ -51,7 +51,7 @@
 ```console
 $ cd ${REPO_TOP}
 $ build/lowrisc_systems_top_earlgrey_verilator_0.1/sim-verilator/Vtop_earlgrey_verilator \
-  --rominit build-bin/sw/device/sim-verilator/boot_rom/boot_rom.vmem
+  --rominit build-bin/sw/device/boot_rom/boot_rom_sim_verilator.vmem
 ```
 
 Run spiflash. In this example we use SPI device `/dev/pts/3` as an example.
@@ -60,7 +60,7 @@
 ```console
 $ cd ${REPO_TOP}
 $ build-bin/sw/host/spiflash/spiflash \
-  --input     build-bin/sw/device/sim-verilator/examples/hello_world/hello_world.bin \
+  --input     build-bin/sw/device/examples/hello_world/hello_world_sim_verilator.bin \
   --verilator /dev/pts/3
 ```
 
@@ -74,5 +74,5 @@
 ```console
 $ cd ${REPO_TOP}
 $ build-bin/sw/host/spiflash/spiflash \
-  --input build-bin/sw/device/fpga/examples/hello_world/hello_world.bin
+  --input build-bin/sw/device/examples/hello_world/hello_world_fpga_nexysvideo.bin
 ```
diff --git a/util/build_consts.sh b/util/build_consts.sh
index 59d4f53..326b9dc 100644
--- a/util/build_consts.sh
+++ b/util/build_consts.sh
@@ -41,28 +41,9 @@
 readonly OBJ_DIR="$BUILD_ROOT/build-out"
 readonly BIN_DIR="$BUILD_ROOT/build-bin"
 
-# PLATFORMS is an array of all of the "device platforms" which OpenTitan
-# software can be built for. These include:
-# - 'sim-verilator', i.e., Verilator.
-# - 'fpga', i.e., a NexysVideo FPGA board.
-readonly PLATFORMS=(
-  'sim-verilator'
-  'fpga'
-)
-
-# sw_obj_dir takes a platform name as an argument and produces a path to a
-# subdirectory of $OBJ_DIR where its build action artifacts should be written.
-#
-# The output of this function should be considered scratch space and not stable.
-function sw_obj_dir() {
-  echo "$OBJ_DIR/sw/$1"
-}
-
-# sw_bin_dir takes a platform name as an argument and produces a path to the
-# subdirectory of $BIN_DIR where its completed build outputs should be written.
-function sw_bin_dir() {
-  echo "$BIN_DIR/sw/device/$1"
-}
+# $DEV_BIN_DIR is a subdirectory of $BIN_DIR where device build outputs (i.e.,
+# compiled programs that should run on the OpenTitan SoC) should be written.
+DEV_BIN_DIR="$BIN_DIR/sw/device"
 
 # $HOST_BIN_DIR is a subdirectory of $BIN_DIR where host build outputs (i.e.,
 # compiled programs that should run on a host workstation or server) should be
diff --git a/util/fpga/splice_nexysvideo.sh b/util/fpga/splice_nexysvideo.sh
index 68e65b9..b68b536 100755
--- a/util/fpga/splice_nexysvideo.sh
+++ b/util/fpga/splice_nexysvideo.sh
@@ -16,14 +16,13 @@
 
 . util/build_consts.sh
 
-BUILD_DIR="$(sw_obj_dir fpga)"
-TARGET_PREFIX="sw/device/boot_rom/boot_rom"
-TARGET="${BUILD_DIR}/${TARGET_PREFIX}"
+TARGET_PREFIX="sw/device/boot_rom/boot_rom_fpga_nexysvideo"
+TARGET="${DEV_BIN_DIR}/${TARGET_PREFIX}"
 FPGA_BUILD_DIR=build/lowrisc_systems_top_earlgrey_nexysvideo_0.1/synth-vivado/
 FPGA_BIT_NAME=lowrisc_systems_top_earlgrey_nexysvideo_0.1
 
 ./meson_init.sh
-ninja -C "$BUILD_DIR" "${TARGET_PREFIX}.bin"
+ninja -C "$DEV_BIN_DIR" "${TARGET_PREFIX}.bin"
 
 srec_cat "${TARGET}.bin" -binary -offset 0x0 -o "${TARGET}.brammem" \
   -vmem -Output_Block_Size 4;