diff --git a/configure b/configure
index ada6512702a1..05e6702eab06 100755
--- a/configure
+++ b/configure
@@ -200,6 +200,11 @@ arch_name=$arch
 [ "$arch_name" = "arm64" ] && arch_name="aarch64"
 arch_libdir=$arch
 
+if [ "$arch" = "riscv" ]; then
+    echo "riscv32 or riscv64 must be specified"
+    exit 1
+fi
+
 if [ -z "$target" ]; then
     target="qemu"
 else
@@ -307,6 +312,9 @@ elif [ "$arch" = "ppc64" ]; then
         echo "You must provide endianness (big or little)!"
         usage
     fi
+elif [ "$arch" = "riscv32" ] || [ "$arch" = "riscv64" ]; then
+    testdir=riscv
+    arch_libdir=riscv
 else
     testdir=$arch
 fi
@@ -438,6 +446,12 @@ cat <<EOF >> lib/config.h
 #define CONFIG_ERRATA_FORCE ${errata_force}
 #define CONFIG_PAGE_SIZE _AC(${page_size}, UL)
 
+EOF
+elif [ "$arch" = "riscv32" ] || [ "$arch" = "riscv64" ]; then
+cat <<EOF >> lib/config.h
+
+#define CONFIG_UART_EARLY_BASE 0x10000000
+
 EOF
 fi
 echo "#endif" >> lib/config.h
diff --git a/lib/riscv/.gitignore b/lib/riscv/.gitignore
new file mode 100644
index 000000000000..82da12e6bd4e
--- /dev/null
+++ b/lib/riscv/.gitignore
@@ -0,0 +1 @@
+/asm-offsets.[hs]
diff --git a/lib/riscv/asm-offsets.c b/lib/riscv/asm-offsets.c
new file mode 100644
index 000000000000..4a74df9e4a09
--- /dev/null
+++ b/lib/riscv/asm-offsets.c
@@ -0,0 +1,6 @@
+// SPDX-License-Identifier: GPL-2.0-only
+
+int main(void)
+{
+	return 0;
+}
diff --git a/lib/riscv/asm/asm-offsets.h b/lib/riscv/asm/asm-offsets.h
new file mode 100644
index 000000000000..d370ee36a182
--- /dev/null
+++ b/lib/riscv/asm/asm-offsets.h
@@ -0,0 +1 @@
+#include <generated/asm-offsets.h>
diff --git a/lib/riscv/asm/barrier.h b/lib/riscv/asm/barrier.h
new file mode 100644
index 000000000000..c6a09066b2c7
--- /dev/null
+++ b/lib/riscv/asm/barrier.h
@@ -0,0 +1,13 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef _ASMRISCV_BARRIER_H_
+#define _ASMRISCV_BARRIER_H_
+
+#define RISCV_FENCE(p, s) \
+	__asm__ __volatile__ ("fence " #p "," #s : : : "memory")
+
+/* These barriers need to enforce ordering on both devices or memory. */
+#define mb()		RISCV_FENCE(iorw,iorw)
+#define rmb()		RISCV_FENCE(ir,ir)
+#define wmb()		RISCV_FENCE(ow,ow)
+
+#endif /* _ASMRISCV_BARRIER_H_ */
diff --git a/lib/riscv/asm/csr.h b/lib/riscv/asm/csr.h
new file mode 100644
index 000000000000..5c4f2de34f64
--- /dev/null
+++ b/lib/riscv/asm/csr.h
@@ -0,0 +1,7 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef _ASMRISCV_CSR_H_
+#define _ASMRISCV_CSR_H_
+
+#define CSR_SSCRATCH		0x140
+
+#endif /* _ASMRISCV_CSR_H_ */
diff --git a/lib/riscv/asm/io.h b/lib/riscv/asm/io.h
new file mode 100644
index 000000000000..d2eb3acc9fda
--- /dev/null
+++ b/lib/riscv/asm/io.h
@@ -0,0 +1,78 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * From Linux arch/riscv/include/asm/mmio.h
+ */
+#ifndef _ASMRISCV_IO_H_
+#define _ASMRISCV_IO_H_
+#include <libcflat.h>
+
+#define __iomem
+
+/* Generic IO read/write.  These perform native-endian accesses. */
+#define __raw_writeb __raw_writeb
+static inline void __raw_writeb(u8 val, volatile void __iomem *addr)
+{
+	asm volatile("sb %0, 0(%1)" : : "r" (val), "r" (addr));
+}
+
+#define __raw_writew __raw_writew
+static inline void __raw_writew(u16 val, volatile void __iomem *addr)
+{
+	asm volatile("sh %0, 0(%1)" : : "r" (val), "r" (addr));
+}
+
+#define __raw_writel __raw_writel
+static inline void __raw_writel(u32 val, volatile void __iomem *addr)
+{
+	asm volatile("sw %0, 0(%1)" : : "r" (val), "r" (addr));
+}
+
+#ifdef CONFIG_64BIT
+#define __raw_writeq __raw_writeq
+static inline void __raw_writeq(u64 val, volatile void __iomem *addr)
+{
+	asm volatile("sd %0, 0(%1)" : : "r" (val), "r" (addr));
+}
+#endif
+
+#define __raw_readb __raw_readb
+static inline u8 __raw_readb(const volatile void __iomem *addr)
+{
+	u8 val;
+
+	asm volatile("lb %0, 0(%1)" : "=r" (val) : "r" (addr));
+	return val;
+}
+
+#define __raw_readw __raw_readw
+static inline u16 __raw_readw(const volatile void __iomem *addr)
+{
+	u16 val;
+
+	asm volatile("lh %0, 0(%1)" : "=r" (val) : "r" (addr));
+	return val;
+}
+
+#define __raw_readl __raw_readl
+static inline u32 __raw_readl(const volatile void __iomem *addr)
+{
+	u32 val;
+
+	asm volatile("lw %0, 0(%1)" : "=r" (val) : "r" (addr));
+	return val;
+}
+
+#ifdef CONFIG_64BIT
+#define __raw_readq __raw_readq
+static inline u64 __raw_readq(const volatile void __iomem *addr)
+{
+	u64 val;
+
+	asm volatile("ld %0, 0(%1)" : "=r" (val) : "r" (addr));
+	return val;
+}
+#endif
+
+#include <asm-generic/io.h>
+
+#endif /* _ASMRISCV_IO_H_ */
diff --git a/lib/riscv/asm/page.h b/lib/riscv/asm/page.h
new file mode 100644
index 000000000000..7d7c9191605a
--- /dev/null
+++ b/lib/riscv/asm/page.h
@@ -0,0 +1,7 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef _ASMRISCV_PAGE_H_
+#define _ASMRISCV_PAGE_H_
+
+#include <asm-generic/page.h>
+
+#endif /* _ASMRISCV_PAGE_H_ */
diff --git a/lib/riscv/asm/setup.h b/lib/riscv/asm/setup.h
new file mode 100644
index 000000000000..385455f341cc
--- /dev/null
+++ b/lib/riscv/asm/setup.h
@@ -0,0 +1,7 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef _ASMRISCV_SETUP_H_
+#define _ASMRISCV_SETUP_H_
+
+void setup(const void *fdt, phys_addr_t freemem_start);
+
+#endif /* _ASMRISCV_SETUP_H_ */
diff --git a/lib/riscv/asm/spinlock.h b/lib/riscv/asm/spinlock.h
new file mode 100644
index 000000000000..6e2b3009abf3
--- /dev/null
+++ b/lib/riscv/asm/spinlock.h
@@ -0,0 +1,7 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef _ASMRISCV_SPINLOCK_H_
+#define _ASMRISCV_SPINLOCK_H_
+
+#include <asm-generic/spinlock.h>
+
+#endif /* _ASMRISCV_SPINLOCK_H_ */
diff --git a/lib/riscv/asm/stack.h b/lib/riscv/asm/stack.h
new file mode 100644
index 000000000000..d081d0716d7b
--- /dev/null
+++ b/lib/riscv/asm/stack.h
@@ -0,0 +1,9 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef _ASMRISCV_STACK_H_
+#define _ASMRISCV_STACK_H_
+
+#ifndef _STACK_H_
+#error Do not directly include <asm/stack.h>. Just use <stack.h>.
+#endif
+
+#endif
diff --git a/lib/riscv/io.c b/lib/riscv/io.c
new file mode 100644
index 000000000000..3cfc235d19a6
--- /dev/null
+++ b/lib/riscv/io.c
@@ -0,0 +1,44 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Each architecture must implement puts() and exit() with the I/O
+ * devices exposed from QEMU, e.g. ns16550a.
+ *
+ * Copyright (C) 2023, Ventana Micro Systems Inc., Andrew Jones <ajones@ventanamicro.com>
+ */
+#include <libcflat.h>
+#include <config.h>
+#include <asm/io.h>
+#include <asm/spinlock.h>
+
+/*
+ * Use this guess for the uart base in order to make an attempt at
+ * having earlier printf support. We'll overwrite it with the real
+ * base address that we read from the device tree later. This is
+ * the address we expect the virtual machine manager to put in
+ * its generated device tree.
+ */
+#define UART_EARLY_BASE ((u8 *)(unsigned long)CONFIG_UART_EARLY_BASE)
+static volatile u8 *uart0_base = UART_EARLY_BASE;
+static struct spinlock uart_lock;
+
+void puts(const char *s)
+{
+	spin_lock(&uart_lock);
+	while (*s)
+		writeb(*s++, uart0_base);
+	spin_unlock(&uart_lock);
+}
+
+/*
+ * Defining halt to take 'code' as an argument guarantees that it will
+ * be in a0 when we halt. That gives us a final chance to see the exit
+ * status while inspecting the halted unit test state.
+ */
+void halt(int code);
+
+void exit(int code)
+{
+	printf("\nEXIT: STATUS=%d\n", ((code) << 1) | 1);
+	halt(code);
+	__builtin_unreachable();
+}
diff --git a/lib/riscv/setup.c b/lib/riscv/setup.c
new file mode 100644
index 000000000000..8937525ccb7f
--- /dev/null
+++ b/lib/riscv/setup.c
@@ -0,0 +1,12 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Initialize machine setup information and I/O.
+ *
+ * Copyright (C) 2023, Ventana Micro Systems Inc., Andrew Jones <ajones@ventanamicro.com>
+ */
+#include <libcflat.h>
+#include <asm/setup.h>
+
+void setup(const void *fdt, phys_addr_t freemem_start)
+{
+}
diff --git a/riscv/Makefile b/riscv/Makefile
new file mode 100644
index 000000000000..f2e89f0e4c38
--- /dev/null
+++ b/riscv/Makefile
@@ -0,0 +1,83 @@
+#
+# riscv makefile
+#
+# Authors: Andrew Jones <ajones@ventanamicro.com>
+#
+
+ifeq ($(CONFIG_EFI),y)
+exe = efi
+else
+exe = flat
+endif
+
+tests =
+tests += $(TEST_DIR)/selftest.$(exe)
+#tests += $(TEST_DIR)/sieve.$(exe)
+
+all: $(tests)
+
+$(TEST_DIR)/sieve.elf: AUXFLAGS = 0x1
+
+cstart.o = $(TEST_DIR)/cstart.o
+
+cflatobjs += lib/riscv/io.o
+cflatobjs += lib/riscv/setup.o
+
+########################################
+
+OBJDIRS += lib/riscv
+FLATLIBS = $(libcflat) $(LIBFDT_archive)
+
+AUXFLAGS ?= 0x0
+
+# stack.o relies on frame pointers.
+KEEP_FRAME_POINTER := y
+
+# We want to keep intermediate files
+.PRECIOUS: %.elf %.o
+
+define arch_elf_check =
+	$(if $(shell ! $(READELF) -rW $(1) >&/dev/null && echo "nok"),
+		$(error $(shell $(READELF) -rW $(1) 2>&1)))
+	$(if $(shell $(READELF) -rW $(1) | grep R_ | grep -v R_RISCV_RELATIVE),
+		$(error $(1) has unsupported reloc types))
+endef
+
+ifeq ($(ARCH),riscv64)
+CFLAGS += -DCONFIG_64BIT
+endif
+CFLAGS += -DCONFIG_RELOC
+CFLAGS += -mcmodel=medany
+CFLAGS += -mstrict-align
+CFLAGS += -std=gnu99
+CFLAGS += -ffreestanding
+CFLAGS += -O2
+CFLAGS += -I $(SRCDIR)/lib -I $(SRCDIR)/lib/libfdt
+
+asm-offsets = lib/riscv/asm-offsets.h
+include $(SRCDIR)/scripts/asm-offsets.mak
+
+ifeq ($(CONFIG_EFI),y)
+	# TODO
+else
+%.elf: LDFLAGS += -pie -n -z notext
+%.elf: %.o $(FLATLIBS) $(SRCDIR)/riscv/flat.lds $(cstart.o)
+	$(CC) $(CFLAGS) -c -o $(@:.elf=.aux.o) $(SRCDIR)/lib/auxinfo.c \
+		-DPROGNAME=\"$(notdir $(@:.elf=.flat))\" -DAUXFLAGS=$(AUXFLAGS)
+	$(LD) $(LDFLAGS) -o $@ -T $(SRCDIR)/riscv/flat.lds \
+		$(filter %.o, $^) $(FLATLIBS) $(@:.elf=.aux.o)
+	$(RM) $(@:.elf=.aux.o)
+	@chmod a-x $@
+
+%.flat: %.elf
+	$(call arch_elf_check, $^)
+	$(OBJCOPY) -O binary $^ $@
+	@chmod a-x $@
+endif
+
+generated-files = $(asm-offsets)
+$(tests:.$(exe)=.o) $(cstart.o) $(cflatobjs): $(generated-files)
+
+arch_clean: asm_offsets_clean
+	$(RM) $(TEST_DIR)/*.{o,flat,elf,so,efi,debug} \
+	      $(TEST_DIR)/.*.d $(TEST_DIR)/efi/.*.d lib/riscv/.*.d
diff --git a/riscv/cstart.S b/riscv/cstart.S
new file mode 100644
index 000000000000..a28d75e8021e
--- /dev/null
+++ b/riscv/cstart.S
@@ -0,0 +1,92 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Boot entry point and assembler functions for riscv.
+ *
+ * Copyright (C) 2023, Ventana Micro Systems Inc., Andrew Jones <ajones@ventanamicro.com>
+ */
+#include <asm/csr.h>
+
+.macro zero_range, tmp1, tmp2
+9998:	beq	\tmp1, \tmp2, 9997f
+	sd	zero, 0(\tmp1)
+	addi	\tmp1, \tmp1, 8
+	j	9998b
+9997:
+.endm
+
+	.section .init
+
+/*
+ * The hartid of the current core is in a0
+ * The address of the devicetree is in a1
+ *
+ * See Linux kernel doc Documentation/riscv/boot.rst
+ */
+.global start
+start:
+	/*
+	 * Stash the hartid in scratch and shift the dtb
+	 * address into a0
+	 */
+	csrw	CSR_SSCRATCH, a0
+	mv	a0, a1
+
+	/*
+	 * Update all R_RISCV_RELATIVE relocations using the table
+	 * of Elf64_Rela entries between reloc_start/end. The build
+	 * will not emit other relocation types.
+	 *
+	 * struct Elf64_Rela {
+	 * 	uint64_t r_offset;
+	 * 	uint64_t r_info;
+	 * 	int64_t  r_addend;
+	 * }
+	 */
+	la	a1, reloc_start
+	la	a2, reloc_end
+	la	a3, start			// base
+1:
+	bge	a1, a2, 1f
+	ld	a4, 0(a1)			// r_offset
+	ld	a5, 16(a1)			// r_addend
+	add	a4, a3, a4			// addr = base + r_offset
+	add	a5, a3, a5			// val = base + r_addend
+	sd	a5, 0(a4)			// *addr = val
+	addi	a1, a1, 24
+	j	1b
+
+1:
+	/* zero BSS */
+	la	a1, bss
+	la	a2, ebss
+	zero_range a1, a2
+
+	/* zero and set up stack */
+	la	sp, stacktop
+	li	a1, -8192
+	add	a1, sp, a1
+	zero_range a1, sp
+
+	/* set up exception handling */
+	//TODO
+
+	/* complete setup */
+	la	a1, stacktop			// a1 is the base of free memory
+	call	setup				// a0 is the addr of the dtb
+
+	/* run the test */
+	la	a0, __argc
+	ld	a0, 0(a0)
+	la	a1, __argv
+	la	a2, __environ
+	call	main
+	call	exit
+	j	halt
+
+	.text
+
+.balign 4
+.global halt
+halt:
+1:	wfi
+	j	1b
diff --git a/riscv/flat.lds b/riscv/flat.lds
new file mode 100644
index 000000000000..d4853f82ba1c
--- /dev/null
+++ b/riscv/flat.lds
@@ -0,0 +1,75 @@
+/*
+ * init::start will pass stacktop to setup() as the base of free memory.
+ * setup() will then move the FDT and initrd to that base before calling
+ * mem_init(). With those movements and this linker script, we'll end up
+ * having the following memory layout:
+ *
+ *    +----------------------+   <-- top of physical memory
+ *    |                      |
+ *    ~                      ~
+ *    |                      |
+ *    +----------------------+   <-- top of initrd
+ *    |                      |
+ *    +----------------------+   <-- top of FDT
+ *    |                      |
+ *    +----------------------+   <-- top of cpu0's stack
+ *    |                      |
+ *    +----------------------+   <-- top of text/data/bss sections
+ *    |                      |
+ *    |                      |
+ *    +----------------------+   <-- load address
+ *    |                      |
+ *    +----------------------+   <-- physical address 0x0
+ */
+
+PHDRS
+{
+    text PT_LOAD FLAGS(5);
+    data PT_LOAD FLAGS(6);
+}
+
+SECTIONS
+{
+    PROVIDE(_text = .);
+    .text : { *(.init) *(.text) *(.text.*) } :text
+    . = ALIGN(4K);
+    PROVIDE(_etext = .);
+
+    PROVIDE(reloc_start = .);
+    .rela.dyn : { *(.rela.dyn) }
+    PROVIDE(reloc_end = .);
+    .dynsym   : { *(.dynsym) }
+    .dynstr   : { *(.dynstr) }
+    .hash     : { *(.hash) }
+    .gnu.hash : { *(.gnu.hash) }
+    .got      : { *(.got) *(.got.plt) }
+    .eh_frame : { *(.eh_frame) }
+
+    .rodata   : { *(.rodata*) } :data
+    .data     : { *(.data) } :data
+    . = ALIGN(16);
+    PROVIDE(bss = .);
+    .bss      : { *(.bss) }
+    . = ALIGN(16);
+    PROVIDE(ebss = .);
+    . = ALIGN(4K);
+    PROVIDE(edata = .);
+
+    /*
+     * stack depth is 8K and sp must be 16 byte aligned
+     * sp must always be strictly less than the true stacktop
+     */
+    . += 12K;
+    . = ALIGN(4K);
+    PROVIDE(stackptr = . - 16);
+    PROVIDE(stacktop = .);
+
+    /DISCARD/ : {
+        *(.note*)
+        *(.interp)
+        *(.comment)
+        *(.dynamic)
+    }
+}
+
+ENTRY(start)
diff --git a/riscv/selftest.c b/riscv/selftest.c
new file mode 100644
index 000000000000..88afa732649e
--- /dev/null
+++ b/riscv/selftest.c
@@ -0,0 +1,13 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Test the framework itself. These tests confirm that setup works.
+ *
+ * Copyright (C) 2023, Ventana Micro Systems Inc., Andrew Jones <ajones@ventanamicro.com>
+ */
+#include <libcflat.h>
+
+int main(void)
+{
+	puts("Hello, world\n");
+	return 0;
+}
