Merge branch 'riscv/sbi' into 'master'

riscv: Added SBI tests for IPI, SUSP, and HSM

See merge request kvm-unit-tests/kvm-unit-tests!70
diff --git a/lib/cpumask.h b/lib/cpumask.h
index 37d3607..b4cd83a 100644
--- a/lib/cpumask.h
+++ b/lib/cpumask.h
@@ -72,6 +72,19 @@
 	return !lastmask || !((cpumask_bits(src1)[i] & ~cpumask_bits(src2)[i]) & lastmask);
 }
 
+static inline bool cpumask_equal(const struct cpumask *src1, const struct cpumask *src2)
+{
+	unsigned long lastmask = BIT_MASK(nr_cpus) - 1;
+	int i;
+
+	for (i = 0; i < BIT_WORD(nr_cpus); ++i) {
+		if (cpumask_bits(src1)[i] != cpumask_bits(src2)[i])
+			return false;
+	}
+
+	return !lastmask || (cpumask_bits(src1)[i] & lastmask) == (cpumask_bits(src2)[i] & lastmask);
+}
+
 static inline bool cpumask_empty(const cpumask_t *mask)
 {
 	unsigned long lastmask = BIT_MASK(nr_cpus) - 1;
diff --git a/lib/riscv/asm/asm.h b/lib/riscv/asm/asm.h
index 763b28e..107b5bb 100644
--- a/lib/riscv/asm/asm.h
+++ b/lib/riscv/asm/asm.h
@@ -14,6 +14,9 @@
 #define REG_S	__REG_SEL(sd, sw)
 #define SZREG	__REG_SEL(8, 4)
 
+/* ASMARR() may be used with arrays of longs */
+#define ASMARR(reg, idx)	((idx) * SZREG)(reg)
+
 #define FP_SIZE 16
 
 #endif /* _ASMRISCV_ASM_H_ */
diff --git a/lib/riscv/asm/sbi.h b/lib/riscv/asm/sbi.h
index 1319439..98a9b09 100644
--- a/lib/riscv/asm/sbi.h
+++ b/lib/riscv/asm/sbi.h
@@ -22,6 +22,7 @@
 	SBI_EXT_HSM = 0x48534d,
 	SBI_EXT_SRST = 0x53525354,
 	SBI_EXT_DBCN = 0x4442434E,
+	SBI_EXT_SUSP = 0x53555350,
 };
 
 enum sbi_ext_base_fid {
@@ -87,6 +88,7 @@
 struct sbiret sbi_send_ipi(unsigned long hart_mask, unsigned long hart_mask_base);
 struct sbiret sbi_send_ipi_cpu(int cpu);
 struct sbiret sbi_send_ipi_cpumask(const cpumask_t *mask);
+struct sbiret sbi_send_ipi_broadcast(void);
 struct sbiret sbi_set_timer(unsigned long stime_value);
 long sbi_probe(int ext);
 
diff --git a/lib/riscv/sbi.c b/lib/riscv/sbi.c
index 8972e76..02dd338 100644
--- a/lib/riscv/sbi.c
+++ b/lib/riscv/sbi.c
@@ -62,13 +62,18 @@
 	return sbi_send_ipi(1UL, cpus[cpu].hartid);
 }
 
+struct sbiret sbi_send_ipi_broadcast(void)
+{
+	return sbi_send_ipi(0, -1UL);
+}
+
 struct sbiret sbi_send_ipi_cpumask(const cpumask_t *mask)
 {
 	struct sbiret ret;
 	cpumask_t tmp;
 
 	if (cpumask_full(mask))
-		return sbi_send_ipi(0, -1UL);
+		return sbi_send_ipi_broadcast();
 
 	cpumask_copy(&tmp, mask);
 
@@ -107,7 +112,7 @@
 	struct sbiret ret;
 
 	ret = sbi_ecall(SBI_EXT_BASE, SBI_EXT_BASE_GET_SPEC_VERSION, 0, 0, 0, 0, 0, 0);
-	assert(!ret.error && ret.value >= 2);
+	assert(!ret.error && (ret.value & 0x7ffffffful) >= 2);
 
 	ret = sbi_ecall(SBI_EXT_BASE, SBI_EXT_BASE_PROBE_EXT, ext, 0, 0, 0, 0, 0);
 	assert(!ret.error);
diff --git a/lib/riscv/setjmp.S b/lib/riscv/setjmp.S
new file mode 100644
index 0000000..38b0f1c
--- /dev/null
+++ b/lib/riscv/setjmp.S
@@ -0,0 +1,50 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#define __ASSEMBLY__
+#include <asm/asm.h>
+
+.section .text
+
+.balign 4
+.global setjmp
+setjmp:
+	REG_S	ra, (0 * SZREG)(a0)
+	REG_S	s0, (1 * SZREG)(a0)
+	REG_S	s1, (2 * SZREG)(a0)
+	REG_S	s2, (3 * SZREG)(a0)
+	REG_S	s3, (4 * SZREG)(a0)
+	REG_S	s4, (5 * SZREG)(a0)
+	REG_S	s5, (6 * SZREG)(a0)
+	REG_S	s6, (7 * SZREG)(a0)
+	REG_S	s7, (8 * SZREG)(a0)
+	REG_S	s8, (9 * SZREG)(a0)
+	REG_S	s9, (10 * SZREG)(a0)
+	REG_S	s10, (11 * SZREG)(a0)
+	REG_S	s11, (12 * SZREG)(a0)
+	REG_S	sp, (13 * SZREG)(a0)
+	REG_S	gp, (14 * SZREG)(a0)
+	REG_S	tp, (15 * SZREG)(a0)
+	li	a0, 0
+	ret
+
+.balign 4
+.global longjmp
+longjmp:
+	REG_L	ra, (0 * SZREG)(a0)
+	REG_L	s0, (1 * SZREG)(a0)
+	REG_L	s1, (2 * SZREG)(a0)
+	REG_L	s2, (3 * SZREG)(a0)
+	REG_L	s3, (4 * SZREG)(a0)
+	REG_L	s4, (5 * SZREG)(a0)
+	REG_L	s5, (6 * SZREG)(a0)
+	REG_L	s6, (7 * SZREG)(a0)
+	REG_L	s7, (8 * SZREG)(a0)
+	REG_L	s8, (9 * SZREG)(a0)
+	REG_L	s9, (10 * SZREG)(a0)
+	REG_L	s10, (11 * SZREG)(a0)
+	REG_L	s11, (12 * SZREG)(a0)
+	REG_L	sp, (13 * SZREG)(a0)
+	REG_L	gp, (14 * SZREG)(a0)
+	REG_L	tp, (15 * SZREG)(a0)
+	seqz	a0, a1
+	add	a0, a0, a1
+	ret
diff --git a/lib/setjmp.h b/lib/setjmp.h
index 6afdf66..f878ad8 100644
--- a/lib/setjmp.h
+++ b/lib/setjmp.h
@@ -8,7 +8,11 @@
 #define _LIBCFLAT_SETJMP_H_
 
 typedef struct jmp_buf_tag {
+#if defined(__i386__) || defined(__x86_64__)
 	long int regs[8];
+#elif defined(__riscv)
+	long int regs[16];
+#endif
 } jmp_buf[1];
 
 extern int setjmp (struct jmp_buf_tag env[1]);
diff --git a/riscv/Makefile b/riscv/Makefile
index 22fd273..28b0415 100644
--- a/riscv/Makefile
+++ b/riscv/Makefile
@@ -36,6 +36,7 @@
 cflatobjs += lib/riscv/mmu.o
 cflatobjs += lib/riscv/processor.o
 cflatobjs += lib/riscv/sbi.o
+cflatobjs += lib/riscv/setjmp.o
 cflatobjs += lib/riscv/setup.o
 cflatobjs += lib/riscv/smp.o
 cflatobjs += lib/riscv/stack.o
@@ -43,6 +44,7 @@
 ifeq ($(ARCH),riscv32)
 cflatobjs += lib/ldiv32.o
 endif
+cflatobjs += riscv/sbi-asm.o
 
 ########################################
 
@@ -82,7 +84,7 @@
 CFLAGS += -std=gnu99
 CFLAGS += -ffreestanding
 CFLAGS += -O2
-CFLAGS += -I $(SRCDIR)/lib -I $(SRCDIR)/lib/libfdt -I lib
+CFLAGS += -I $(SRCDIR)/lib -I $(SRCDIR)/lib/libfdt -I lib -I $(SRCDIR)/riscv
 
 asm-offsets = lib/riscv/asm-offsets.h
 include $(SRCDIR)/scripts/asm-offsets.mak
diff --git a/riscv/sbi-asm.S b/riscv/sbi-asm.S
new file mode 100644
index 0000000..923c2ce
--- /dev/null
+++ b/riscv/sbi-asm.S
@@ -0,0 +1,131 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Helper assembly code routines for RISC-V SBI extension tests.
+ *
+ * Copyright (C) 2024, James Raphael Tiovalen <jamestiotio@gmail.com>
+ */
+#define __ASSEMBLY__
+#include <asm/asm.h>
+#include <asm/csr.h>
+
+#include "sbi-tests.h"
+
+.section .text
+
+/*
+ * sbi_hsm_check
+ *   a0 and a1 are set by SBI HSM start/suspend
+ *   s1 is the address of the results array
+ * Doesn't return.
+ *
+ * This function is only called from HSM start and on resumption
+ * from HSM suspend which means we can do whatever we like with
+ * all registers. So, to avoid complicated register agreements with
+ * other assembly functions called, we just always use the saved
+ * registers for anything that should be maintained across calls.
+ */
+#define HSM_RESULTS_ARRAY	s1
+#define HSM_RESULTS_MAP		s2
+#define HSM_CPU_INDEX		s3
+.balign 4
+sbi_hsm_check:
+	li	HSM_RESULTS_MAP, 0
+	REG_L	t0, ASMARR(a1, SBI_HSM_MAGIC_IDX)
+	li	t1, SBI_HSM_MAGIC
+	bne	t0, t1, 1f
+	ori	HSM_RESULTS_MAP, HSM_RESULTS_MAP, SBI_HSM_TEST_MAGIC_A1
+1:	REG_L	t0, ASMARR(a1, SBI_HSM_HARTID_IDX)
+	bne	a0, t0, 2f
+	ori	HSM_RESULTS_MAP, HSM_RESULTS_MAP, SBI_HSM_TEST_HARTID_A0
+2:	csrr	t0, CSR_SATP
+	bnez	t0, 3f
+	ori	HSM_RESULTS_MAP, HSM_RESULTS_MAP, SBI_HSM_TEST_SATP
+3:	csrr	t0, CSR_SSTATUS
+	andi	t0, t0, SR_SIE
+	bnez	t0, 4f
+	ori	HSM_RESULTS_MAP, HSM_RESULTS_MAP, SBI_HSM_TEST_SIE
+4:	call	hartid_to_cpu
+	mv	HSM_CPU_INDEX, a0
+	li	t0, -1
+	bne	HSM_CPU_INDEX, t0, 6f
+5:	pause
+	j	5b
+6:	ori	HSM_RESULTS_MAP, HSM_RESULTS_MAP, SBI_HSM_TEST_DONE
+	add	t0, HSM_RESULTS_ARRAY, HSM_CPU_INDEX
+	sb	HSM_RESULTS_MAP, 0(t0)
+	la	t1, sbi_hsm_stop_hart
+	add	t1, t1, HSM_CPU_INDEX
+7:	lb	t0, 0(t1)
+	pause
+	beqz	t0, 7b
+	li	a7, 0x48534d	/* SBI_EXT_HSM */
+	li	a6, 1		/* SBI_EXT_HSM_HART_STOP */
+	ecall
+8:	pause
+	j	8b
+
+.balign 4
+.global sbi_hsm_check_hart_start
+sbi_hsm_check_hart_start:
+	la	HSM_RESULTS_ARRAY, sbi_hsm_hart_start_checks
+	j	sbi_hsm_check
+
+.balign 4
+.global sbi_hsm_check_non_retentive_suspend
+sbi_hsm_check_non_retentive_suspend:
+	la	HSM_RESULTS_ARRAY, sbi_hsm_non_retentive_hart_suspend_checks
+	j	sbi_hsm_check
+
+.balign 4
+restore_csrs:
+	REG_L	a1, ASMARR(a0, SBI_CSR_SSTATUS_IDX)
+	csrw	CSR_SSTATUS, a1
+	REG_L	a1, ASMARR(a0, SBI_CSR_SIE_IDX)
+	csrw	CSR_SIE, a1
+	REG_L	a1, ASMARR(a0, SBI_CSR_STVEC_IDX)
+	csrw	CSR_STVEC, a1
+	REG_L	a1, ASMARR(a0, SBI_CSR_SSCRATCH_IDX)
+	csrw	CSR_SSCRATCH, a1
+	REG_L	a1, ASMARR(a0, SBI_CSR_SATP_IDX)
+	sfence.vma
+	csrw	CSR_SATP, a1
+	ret
+
+/*
+ * sbi_susp_resume
+ *
+ * State is as specified by "SUSP System Resume Register State" of the SBI spec
+ *   a0 is the hartid
+ *   a1 is the opaque parameter (here, it's the context array defined in check_susp())
+ * Doesn't return.
+ */
+#define SUSP_CTX		s1
+#define SUSP_RESULTS_MAP	s2
+.balign 4
+.global sbi_susp_resume
+sbi_susp_resume:
+	li	SUSP_RESULTS_MAP, 0
+	mv	SUSP_CTX, a1
+	REG_L	t0, ASMARR(SUSP_CTX, SBI_SUSP_MAGIC_IDX)
+	li	t1, SBI_SUSP_MAGIC
+	beq	t0, t1, 2f
+1:	pause
+	j	1b
+2:	csrr	t0, CSR_SATP
+	bnez	t0, 3f
+	ori	SUSP_RESULTS_MAP, SUSP_RESULTS_MAP, SBI_SUSP_TEST_SATP
+3:	csrr	t0, CSR_SSTATUS
+	andi	t0, t0, SR_SIE
+	bnez	t0, 4f
+	ori	SUSP_RESULTS_MAP, SUSP_RESULTS_MAP, SBI_SUSP_TEST_SIE
+4:	REG_L	t0, ASMARR(SUSP_CTX, SBI_SUSP_HARTID_IDX)
+	bne	t0, a0, 5f
+	ori	SUSP_RESULTS_MAP, SUSP_RESULTS_MAP, SBI_SUSP_TEST_HARTID
+5:	REG_S	SUSP_RESULTS_MAP, ASMARR(SUSP_CTX, SBI_SUSP_RESULTS_IDX)
+	REG_L	a0, ASMARR(SUSP_CTX, SBI_SUSP_CSRS_IDX)
+	call	restore_csrs
+	la	a0, sbi_susp_jmp
+	REG_L	a1, ASMARR(SUSP_CTX, SBI_SUSP_TESTNUM_IDX)
+	call	longjmp
+6:	pause	/* unreachable */
+	j	6b
diff --git a/riscv/sbi-tests.h b/riscv/sbi-tests.h
new file mode 100644
index 0000000..ce12996
--- /dev/null
+++ b/riscv/sbi-tests.h
@@ -0,0 +1,36 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef _RISCV_SBI_TESTS_H_
+#define _RISCV_SBI_TESTS_H_
+
+#define SBI_HSM_TEST_DONE	(1 << 0)
+#define SBI_HSM_TEST_MAGIC_A1	(1 << 1)
+#define SBI_HSM_TEST_HARTID_A0	(1 << 2)
+#define SBI_HSM_TEST_SATP	(1 << 3)
+#define SBI_HSM_TEST_SIE	(1 << 4)
+
+#define SBI_HSM_MAGIC		0x453
+
+#define SBI_HSM_MAGIC_IDX	0
+#define SBI_HSM_HARTID_IDX	1
+#define SBI_HSM_NUM_OF_PARAMS	2
+
+#define SBI_SUSP_MAGIC_IDX	0
+#define SBI_SUSP_CSRS_IDX	1
+#define SBI_SUSP_HARTID_IDX	2
+#define SBI_SUSP_TESTNUM_IDX	3
+#define SBI_SUSP_RESULTS_IDX	4
+
+#define SBI_CSR_SSTATUS_IDX	0
+#define SBI_CSR_SIE_IDX		1
+#define SBI_CSR_STVEC_IDX	2
+#define SBI_CSR_SSCRATCH_IDX	3
+#define SBI_CSR_SATP_IDX	4
+
+#define SBI_SUSP_MAGIC		0x505b
+
+#define SBI_SUSP_TEST_SATP	(1 << 0)
+#define SBI_SUSP_TEST_SIE	(1 << 1)
+#define SBI_SUSP_TEST_HARTID	(1 << 2)
+#define SBI_SUSP_TEST_MASK	7
+
+#endif /* _RISCV_SBI_TESTS_H_ */
diff --git a/riscv/sbi.c b/riscv/sbi.c
index 52434e0..6f4ddaf 100644
--- a/riscv/sbi.c
+++ b/riscv/sbi.c
@@ -6,23 +6,37 @@
  */
 #include <libcflat.h>
 #include <alloc_page.h>
+#include <cpumask.h>
+#include <limits.h>
+#include <memregions.h>
+#include <on-cpus.h>
+#include <rand.h>
+#include <setjmp.h>
 #include <stdlib.h>
 #include <string.h>
-#include <limits.h>
 #include <vmalloc.h>
-#include <memregions.h>
+
 #include <asm/barrier.h>
 #include <asm/csr.h>
 #include <asm/delay.h>
 #include <asm/io.h>
 #include <asm/mmu.h>
+#include <asm/page.h>
 #include <asm/processor.h>
 #include <asm/sbi.h>
+#include <asm/setup.h>
 #include <asm/smp.h>
 #include <asm/timer.h>
 
+#include "sbi-tests.h"
+
 #define	HIGH_ADDR_BOUNDARY	((phys_addr_t)1 << 32)
 
+static long __labs(long a)
+{
+	return __builtin_labs(a);
+}
+
 static void help(void)
 {
 	puts("Test SBI\n");
@@ -46,6 +60,42 @@
 	return sbi_ecall(SBI_EXT_DBCN, SBI_EXT_DBCN_CONSOLE_WRITE_BYTE, byte, 0, 0, 0, 0, 0);
 }
 
+static struct sbiret sbi_hart_suspend(uint32_t suspend_type, unsigned long resume_addr, unsigned long opaque)
+{
+	return sbi_ecall(SBI_EXT_HSM, SBI_EXT_HSM_HART_SUSPEND, suspend_type, resume_addr, opaque, 0, 0, 0);
+}
+
+static struct sbiret sbi_system_suspend(uint32_t sleep_type, unsigned long resume_addr, unsigned long opaque)
+{
+	return sbi_ecall(SBI_EXT_SUSP, 0, sleep_type, resume_addr, opaque, 0, 0, 0);
+}
+
+static void start_cpu(void *data)
+{
+	/* nothing to do */
+}
+
+static void stop_cpu(void *data)
+{
+	struct sbiret ret = sbi_hart_stop();
+	assert_msg(0, "cpu%d (hartid = %lx) failed to stop with sbiret.error %ld",
+		   smp_processor_id(), current_thread_info()->hartid, ret.error);
+}
+
+static int rand_online_cpu(prng_state *ps)
+{
+	int cpu, me = smp_processor_id();
+
+	for (;;) {
+		cpu = prng32(ps) % nr_cpus;
+		cpu = cpumask_next(cpu - 1, &cpu_present_mask);
+		if (cpu != nr_cpus && cpu != me && cpu_present(cpu))
+			break;
+	}
+
+	return cpu;
+}
+
 static void split_phys_addr(phys_addr_t paddr, unsigned long *hi, unsigned long *lo)
 {
 	*lo = (unsigned long)paddr;
@@ -73,6 +123,13 @@
 	return highest_end - 1;
 }
 
+static bool env_enabled(const char *env)
+{
+	char *s = getenv(env);
+
+	return s && (*s == '1' || *s == 'y' || *s == 'Y');
+}
+
 static bool env_or_skip(const char *env)
 {
 	if (!getenv(env)) {
@@ -83,6 +140,22 @@
 	return true;
 }
 
+static bool get_invalid_addr(phys_addr_t *paddr, bool allow_default)
+{
+	if (env_enabled("INVALID_ADDR_AUTO")) {
+		*paddr = get_highest_addr() + 1;
+		return true;
+	} else if (allow_default && !getenv("INVALID_ADDR")) {
+		*paddr = -1ul;
+		return true;
+	} else if (env_or_skip("INVALID_ADDR")) {
+		*paddr = strtoull(getenv("INVALID_ADDR"), NULL, 0);
+		return true;
+	}
+
+	return false;
+}
+
 static void gen_report(struct sbiret *ret,
 		       long expected_error, long expected_value)
 {
@@ -105,18 +178,23 @@
 	report_prefix_push("base");
 
 	ret = sbi_base(SBI_EXT_BASE_GET_SPEC_VERSION, 0);
-	if (ret.error || ret.value < 2) {
-		report_skip("SBI spec version 0.2 or higher required");
-		return;
-	}
 
 	report_prefix_push("spec_version");
 	if (env_or_skip("SBI_SPEC_VERSION")) {
 		expected = (long)strtoul(getenv("SBI_SPEC_VERSION"), NULL, 0);
+		assert_msg(!(expected & BIT(31)), "SBI spec version bit 31 must be zero");
+		assert_msg(__riscv_xlen == 32 || !(expected >> 32), "SBI spec version bits greater than 31 must be zero");
 		gen_report(&ret, 0, expected);
 	}
 	report_prefix_pop();
 
+	ret.value &= 0x7ffffffful;
+
+	if (ret.error || ret.value < 2) {
+		report_skip("SBI spec version 0.2 or higher required");
+		return;
+	}
+
 	report_prefix_push("impl_id");
 	if (env_or_skip("SBI_IMPL_ID")) {
 		expected = (long)strtoul(getenv("SBI_IMPL_ID"), NULL, 0);
@@ -281,6 +359,735 @@
 	report_prefix_popn(2);
 }
 
+static bool ipi_received[NR_CPUS];
+static bool ipi_timeout[NR_CPUS];
+static cpumask_t ipi_done;
+
+static void ipi_timeout_handler(struct pt_regs *regs)
+{
+	timer_stop();
+	ipi_timeout[smp_processor_id()] = true;
+}
+
+static void ipi_irq_handler(struct pt_regs *regs)
+{
+	ipi_ack();
+	ipi_received[smp_processor_id()] = true;
+}
+
+static void ipi_hart_wait(void *data)
+{
+	unsigned long timeout = (unsigned long)data;
+	int me = smp_processor_id();
+
+	install_irq_handler(IRQ_S_SOFT, ipi_irq_handler);
+	install_irq_handler(IRQ_S_TIMER, ipi_timeout_handler);
+	local_ipi_enable();
+	timer_irq_enable();
+	local_irq_enable();
+
+	timer_start(timeout);
+	while (!READ_ONCE(ipi_received[me]) && !READ_ONCE(ipi_timeout[me]))
+		cpu_relax();
+	local_irq_disable();
+	timer_stop();
+	local_ipi_disable();
+	timer_irq_disable();
+
+	cpumask_set_cpu(me, &ipi_done);
+}
+
+static void ipi_hart_check(cpumask_t *mask)
+{
+	int cpu;
+
+	for_each_cpu(cpu, mask) {
+		if (ipi_timeout[cpu]) {
+			const char *rec = ipi_received[cpu] ? "but was still received"
+							    : "and has still not been received";
+			report_fail("ipi timed out on cpu%d %s", cpu, rec);
+		}
+
+		ipi_timeout[cpu] = false;
+		ipi_received[cpu] = false;
+	}
+}
+
+static void check_ipi(void)
+{
+	unsigned long d = getenv("SBI_IPI_TIMEOUT") ? strtol(getenv("SBI_IPI_TIMEOUT"), NULL, 0) : 200000;
+	int nr_cpus_present = cpumask_weight(&cpu_present_mask);
+	int me = smp_processor_id();
+	unsigned long max_hartid = 0;
+	unsigned long hartid1, hartid2;
+	cpumask_t ipi_receivers;
+	static prng_state ps;
+	struct sbiret ret;
+	int cpu, cpu2;
+
+	ps = prng_init(0xDEADBEEF);
+
+	report_prefix_push("ipi");
+
+	if (!sbi_probe(SBI_EXT_IPI)) {
+		report_skip("ipi extension not available");
+		report_prefix_pop();
+		return;
+	}
+
+	if (nr_cpus_present < 2) {
+		report_skip("At least 2 cpus required");
+		report_prefix_pop();
+		return;
+	}
+
+	report_prefix_push("random hart");
+	cpumask_clear(&ipi_done);
+	cpumask_clear(&ipi_receivers);
+	cpu = rand_online_cpu(&ps);
+	cpumask_set_cpu(cpu, &ipi_receivers);
+	on_cpu_async(cpu, ipi_hart_wait, (void *)d);
+	ret = sbi_send_ipi_cpu(cpu);
+	report(ret.error == SBI_SUCCESS, "ipi returned success");
+	while (!cpumask_equal(&ipi_done, &ipi_receivers))
+		cpu_relax();
+	ipi_hart_check(&ipi_receivers);
+	report_prefix_pop();
+
+	report_prefix_push("two in hart_mask");
+
+	if (nr_cpus_present < 3) {
+		report_skip("3 cpus required");
+		goto end_two;
+	}
+
+	cpu = rand_online_cpu(&ps);
+	hartid1 = cpus[cpu].hartid;
+	hartid2 = 0;
+	for_each_present_cpu(cpu2) {
+		if (cpu2 == cpu || cpu2 == me)
+			continue;
+		hartid2 = cpus[cpu2].hartid;
+		if (__labs(hartid2 - hartid1) < BITS_PER_LONG)
+			break;
+	}
+	if (cpu2 == nr_cpus) {
+		report_skip("hartids are too sparse");
+		goto end_two;
+	}
+
+	cpumask_clear(&ipi_done);
+	cpumask_clear(&ipi_receivers);
+	cpumask_set_cpu(cpu, &ipi_receivers);
+	cpumask_set_cpu(cpu2, &ipi_receivers);
+	on_cpu_async(cpu, ipi_hart_wait, (void *)d);
+	on_cpu_async(cpu2, ipi_hart_wait, (void *)d);
+	ret = sbi_send_ipi((1UL << __labs(hartid2 - hartid1)) | 1UL, hartid1 < hartid2 ? hartid1 : hartid2);
+	report(ret.error == SBI_SUCCESS, "ipi returned success");
+	while (!cpumask_equal(&ipi_done, &ipi_receivers))
+		cpu_relax();
+	ipi_hart_check(&ipi_receivers);
+end_two:
+	report_prefix_pop();
+
+	report_prefix_push("broadcast");
+	cpumask_clear(&ipi_done);
+	cpumask_copy(&ipi_receivers, &cpu_present_mask);
+	cpumask_clear_cpu(me, &ipi_receivers);
+	on_cpumask_async(&ipi_receivers, ipi_hart_wait, (void *)d);
+	ret = sbi_send_ipi_broadcast();
+	report(ret.error == SBI_SUCCESS, "ipi returned success");
+	while (!cpumask_equal(&ipi_done, &ipi_receivers))
+		cpu_relax();
+	ipi_hart_check(&ipi_receivers);
+	report_prefix_pop();
+
+	report_prefix_push("invalid parameters");
+
+	for_each_present_cpu(cpu) {
+		if (cpus[cpu].hartid > max_hartid)
+			max_hartid = cpus[cpu].hartid;
+	}
+
+	/* Try the next higher hartid than the max */
+	ret = sbi_send_ipi(2, max_hartid);
+	report_kfail(true, ret.error == SBI_ERR_INVALID_PARAM, "hart_mask got expected error (%ld)", ret.error);
+	ret = sbi_send_ipi(1, max_hartid + 1);
+	report_kfail(true, ret.error == SBI_ERR_INVALID_PARAM, "hart_mask_base got expected error (%ld)", ret.error);
+
+	report_prefix_pop();
+
+	report_prefix_pop();
+}
+
+unsigned char sbi_hsm_stop_hart[NR_CPUS];
+unsigned char sbi_hsm_hart_start_checks[NR_CPUS];
+unsigned char sbi_hsm_non_retentive_hart_suspend_checks[NR_CPUS];
+
+static const char * const hart_state_str[] = {
+	[SBI_EXT_HSM_STARTED] = "started",
+	[SBI_EXT_HSM_STOPPED] = "stopped",
+	[SBI_EXT_HSM_SUSPENDED] = "suspended",
+};
+struct hart_state_transition_info {
+	enum sbi_ext_hsm_sid initial_state;
+	enum sbi_ext_hsm_sid intermediate_state;
+	enum sbi_ext_hsm_sid final_state;
+};
+static cpumask_t sbi_hsm_started_hart_checks;
+static bool sbi_hsm_invalid_hartid_check;
+static bool sbi_hsm_timer_fired;
+extern void sbi_hsm_check_hart_start(void);
+extern void sbi_hsm_check_non_retentive_suspend(void);
+
+static void hsm_timer_irq_handler(struct pt_regs *regs)
+{
+	timer_stop();
+	sbi_hsm_timer_fired = true;
+}
+
+static void hsm_timer_setup(void)
+{
+	install_irq_handler(IRQ_S_TIMER, hsm_timer_irq_handler);
+	timer_irq_enable();
+}
+
+static void hsm_timer_teardown(void)
+{
+	timer_irq_disable();
+	install_irq_handler(IRQ_S_TIMER, NULL);
+}
+
+static void hart_check_already_started(void *data)
+{
+	struct sbiret ret;
+	unsigned long hartid = current_thread_info()->hartid;
+	int me = smp_processor_id();
+
+	ret = sbi_hart_start(hartid, virt_to_phys(&start_cpu), 0);
+
+	if (ret.error == SBI_ERR_ALREADY_AVAILABLE)
+		cpumask_set_cpu(me, &sbi_hsm_started_hart_checks);
+}
+
+static void hart_start_invalid_hartid(void *data)
+{
+	struct sbiret ret;
+
+	ret = sbi_hart_start(-1UL, virt_to_phys(&start_cpu), 0);
+
+	if (ret.error == SBI_ERR_INVALID_PARAM)
+		sbi_hsm_invalid_hartid_check = true;
+}
+
+static void hart_retentive_suspend(void *data)
+{
+	unsigned long hartid = current_thread_info()->hartid;
+	struct sbiret ret = sbi_hart_suspend(SBI_EXT_HSM_HART_SUSPEND_RETENTIVE, 0, 0);
+
+	if (ret.error)
+		report_fail("failed to retentive suspend cpu%d (hartid = %lx) (error=%ld)",
+			    smp_processor_id(), hartid, ret.error);
+}
+
+static void hart_non_retentive_suspend(void *data)
+{
+	unsigned long hartid = current_thread_info()->hartid;
+	unsigned long params[] = {
+		[SBI_HSM_MAGIC_IDX] = SBI_HSM_MAGIC,
+		[SBI_HSM_HARTID_IDX] = hartid,
+	};
+	struct sbiret ret = sbi_hart_suspend(SBI_EXT_HSM_HART_SUSPEND_NON_RETENTIVE,
+					     virt_to_phys(&sbi_hsm_check_non_retentive_suspend),
+					     virt_to_phys(params));
+
+	report_fail("failed to non-retentive suspend cpu%d (hartid = %lx) (error=%ld)",
+		    smp_processor_id(), hartid, ret.error);
+}
+
+/* This test function is only being run on RV64 to verify that upper bits of suspend_type are ignored */
+static void hart_retentive_suspend_with_msb_set(void *data)
+{
+	unsigned long hartid = current_thread_info()->hartid;
+	unsigned long suspend_type = SBI_EXT_HSM_HART_SUSPEND_RETENTIVE | (_AC(1, UL) << (__riscv_xlen - 1));
+	struct sbiret ret = sbi_ecall(SBI_EXT_HSM, SBI_EXT_HSM_HART_SUSPEND, suspend_type, 0, 0, 0, 0, 0);
+
+	if (ret.error)
+		report_fail("failed to retentive suspend cpu%d (hartid = %lx) with MSB set (error=%ld)",
+			    smp_processor_id(), hartid, ret.error);
+}
+
+/* This test function is only being run on RV64 to verify that upper bits of suspend_type are ignored */
+static void hart_non_retentive_suspend_with_msb_set(void *data)
+{
+	unsigned long hartid = current_thread_info()->hartid;
+	unsigned long suspend_type = SBI_EXT_HSM_HART_SUSPEND_NON_RETENTIVE | (_AC(1, UL) << (__riscv_xlen - 1));
+	unsigned long params[] = {
+		[SBI_HSM_MAGIC_IDX] = SBI_HSM_MAGIC,
+		[SBI_HSM_HARTID_IDX] = hartid,
+	};
+
+	struct sbiret ret = sbi_ecall(SBI_EXT_HSM, SBI_EXT_HSM_HART_SUSPEND, suspend_type,
+				      virt_to_phys(&sbi_hsm_check_non_retentive_suspend), virt_to_phys(params),
+				      0, 0, 0);
+
+	report_fail("failed to non-retentive suspend cpu%d (hartid = %lx) with MSB set (error=%ld)",
+		    smp_processor_id(), hartid, ret.error);
+}
+
+static bool hart_wait_on_status(unsigned long hartid, enum sbi_ext_hsm_sid status, unsigned long duration)
+{
+	struct sbiret ret;
+
+	sbi_hsm_timer_fired = false;
+	timer_start(duration);
+
+	ret = sbi_hart_get_status(hartid);
+
+	while (!ret.error && ret.value == status && !sbi_hsm_timer_fired) {
+		cpu_relax();
+		ret = sbi_hart_get_status(hartid);
+	}
+
+	timer_stop();
+
+	if (sbi_hsm_timer_fired)
+		report_info("timer fired while waiting on status %u for hartid %lx", status, hartid);
+	else if (ret.error)
+		report_fail("got %ld while waiting on status %u for hartid %lx", ret.error, status, hartid);
+
+	return !sbi_hsm_timer_fired && !ret.error;
+}
+
+static int hart_wait_state_transition(cpumask_t *mask, unsigned long duration,
+				      struct hart_state_transition_info *states)
+{
+	struct sbiret ret;
+	unsigned long hartid;
+	int cpu, count = 0;
+
+	for_each_cpu(cpu, mask) {
+		hartid = cpus[cpu].hartid;
+		if (!hart_wait_on_status(hartid, states->initial_state, duration))
+			continue;
+		if (!hart_wait_on_status(hartid, states->intermediate_state, duration))
+			continue;
+
+		ret = sbi_hart_get_status(hartid);
+		if (ret.error)
+			report_info("hartid %lx get status failed (error=%ld)", hartid, ret.error);
+		else if (ret.value != states->final_state)
+			report_info("hartid %lx status is not '%s' (ret.value=%ld)", hartid,
+				    hart_state_str[states->final_state], ret.value);
+		else
+			count++;
+	}
+
+	return count;
+}
+
+static void hart_wait_until_idle(cpumask_t *mask, unsigned long duration)
+{
+	sbi_hsm_timer_fired = false;
+	timer_start(duration);
+
+	while (!cpumask_subset(mask, &cpu_idle_mask) && !sbi_hsm_timer_fired)
+		cpu_relax();
+
+	timer_stop();
+
+	if (sbi_hsm_timer_fired)
+		report_info("hsm timer fired before all cpus became idle");
+}
+
+static void check_hsm(void)
+{
+	struct sbiret ret;
+	unsigned long hartid;
+	cpumask_t secondary_cpus_mask, mask;
+	struct hart_state_transition_info transition_states;
+	bool ipi_unavailable = false;
+	int cpu, me = smp_processor_id();
+	int max_cpus = getenv("SBI_MAX_CPUS") ? strtol(getenv("SBI_MAX_CPUS"), NULL, 0) : nr_cpus;
+	unsigned long hsm_timer_duration = getenv("SBI_HSM_TIMER_DURATION")
+					 ? strtol(getenv("SBI_HSM_TIMER_DURATION"), NULL, 0) : 200000;
+	unsigned long sbi_hsm_hart_start_params[NR_CPUS * SBI_HSM_NUM_OF_PARAMS];
+	int count, check;
+
+	max_cpus = MIN(MIN(max_cpus, nr_cpus), cpumask_weight(&cpu_present_mask));
+
+	report_prefix_push("hsm");
+
+	if (!sbi_probe(SBI_EXT_HSM)) {
+		report_skip("hsm extension not available");
+		report_prefix_pop();
+		return;
+	}
+
+	report_prefix_push("hart_get_status");
+
+	hartid = current_thread_info()->hartid;
+	ret = sbi_hart_get_status(hartid);
+
+	if (ret.error) {
+		report_fail("failed to get status of current hart (error=%ld)", ret.error);
+		report_prefix_popn(2);
+		return;
+	} else if (ret.value != SBI_EXT_HSM_STARTED) {
+		report_fail("current hart is not started (ret.value=%ld)", ret.value);
+		report_prefix_popn(2);
+		return;
+	}
+
+	report_pass("status of current hart is started");
+
+	report_prefix_pop();
+
+	if (max_cpus < 2) {
+		report_skip("no other cpus to run the remaining hsm tests on");
+		report_prefix_pop();
+		return;
+	}
+
+	report_prefix_push("hart_stop");
+
+	cpumask_copy(&secondary_cpus_mask, &cpu_present_mask);
+	cpumask_clear_cpu(me, &secondary_cpus_mask);
+	hsm_timer_setup();
+	local_irq_enable();
+
+	/* Assume that previous tests have not cleaned up and stopped the secondary harts */
+	on_cpumask_async(&secondary_cpus_mask, stop_cpu, NULL);
+
+	transition_states = (struct hart_state_transition_info) {
+		.initial_state = SBI_EXT_HSM_STARTED,
+		.intermediate_state = SBI_EXT_HSM_STOP_PENDING,
+		.final_state = SBI_EXT_HSM_STOPPED,
+	};
+	count = hart_wait_state_transition(&secondary_cpus_mask, hsm_timer_duration, &transition_states);
+
+	report(count == max_cpus - 1, "all secondary harts stopped");
+
+	report_prefix_pop();
+
+	report_prefix_push("hart_start");
+
+	for_each_cpu(cpu, &secondary_cpus_mask) {
+		hartid = cpus[cpu].hartid;
+		sbi_hsm_hart_start_params[cpu * SBI_HSM_NUM_OF_PARAMS + SBI_HSM_MAGIC_IDX] = SBI_HSM_MAGIC;
+		sbi_hsm_hart_start_params[cpu * SBI_HSM_NUM_OF_PARAMS + SBI_HSM_HARTID_IDX] = hartid;
+
+		ret = sbi_hart_start(hartid, virt_to_phys(&sbi_hsm_check_hart_start),
+				     virt_to_phys(&sbi_hsm_hart_start_params[cpu * SBI_HSM_NUM_OF_PARAMS]));
+		if (ret.error) {
+			report_fail("failed to start test on cpu%d (hartid = %lx) (error=%ld)", cpu, hartid, ret.error);
+			continue;
+		}
+	}
+
+	transition_states = (struct hart_state_transition_info) {
+		.initial_state = SBI_EXT_HSM_STOPPED,
+		.intermediate_state = SBI_EXT_HSM_START_PENDING,
+		.final_state = SBI_EXT_HSM_STARTED,
+	};
+	count = hart_wait_state_transition(&secondary_cpus_mask, hsm_timer_duration, &transition_states);
+	check = 0;
+
+	for_each_cpu(cpu, &secondary_cpus_mask) {
+		sbi_hsm_timer_fired = false;
+		timer_start(hsm_timer_duration);
+
+		while (!(READ_ONCE(sbi_hsm_hart_start_checks[cpu]) & SBI_HSM_TEST_DONE) && !sbi_hsm_timer_fired)
+			cpu_relax();
+
+		timer_stop();
+
+		if (sbi_hsm_timer_fired) {
+			report_info("hsm timer fired before cpu%d (hartid = %lx) is done with start checks", cpu, hartid);
+			continue;
+		}
+
+		if (!(sbi_hsm_hart_start_checks[cpu] & SBI_HSM_TEST_SATP))
+			report_info("satp is not zero for test on cpu%d (hartid = %lx)", cpu, hartid);
+		else if (!(sbi_hsm_hart_start_checks[cpu] & SBI_HSM_TEST_SIE))
+			report_info("sstatus.SIE is not zero for test on cpu%d (hartid = %lx)", cpu, hartid);
+		else if (!(sbi_hsm_hart_start_checks[cpu] & SBI_HSM_TEST_MAGIC_A1))
+			report_info("a1 does not start with magic for test on cpu%d (hartid = %lx)", cpu, hartid);
+		else if (!(sbi_hsm_hart_start_checks[cpu] & SBI_HSM_TEST_HARTID_A0))
+			report_info("a0 is not hartid for test on cpu %d (hartid = %lx)", cpu, hartid);
+		else
+			check++;
+	}
+
+	report(count == max_cpus - 1, "all secondary harts started");
+	report(check == max_cpus - 1, "all secondary harts have expected register values after hart start");
+
+	report_prefix_pop();
+
+	report_prefix_push("hart_stop");
+
+	memset(sbi_hsm_stop_hart, 1, sizeof(sbi_hsm_stop_hart));
+
+	transition_states = (struct hart_state_transition_info) {
+		.initial_state = SBI_EXT_HSM_STARTED,
+		.intermediate_state = SBI_EXT_HSM_STOP_PENDING,
+		.final_state = SBI_EXT_HSM_STOPPED,
+	};
+	count = hart_wait_state_transition(&secondary_cpus_mask, hsm_timer_duration, &transition_states);
+
+	report(count == max_cpus - 1, "all secondary harts stopped");
+
+	/* Reset the stop flags so that we can reuse them after suspension tests */
+	memset(sbi_hsm_stop_hart, 0, sizeof(sbi_hsm_stop_hart));
+
+	report_prefix_pop();
+
+	report_prefix_push("hart_start");
+
+	/* Select just one secondary cpu to run the invalid hartid test */
+	on_cpu(cpumask_next(-1, &secondary_cpus_mask), hart_start_invalid_hartid, NULL);
+
+	report(sbi_hsm_invalid_hartid_check, "secondary hart refuse to start with invalid hartid");
+
+	on_cpumask_async(&secondary_cpus_mask, hart_check_already_started, NULL);
+
+	transition_states = (struct hart_state_transition_info) {
+		.initial_state = SBI_EXT_HSM_STOPPED,
+		.intermediate_state = SBI_EXT_HSM_START_PENDING,
+		.final_state = SBI_EXT_HSM_STARTED,
+	};
+	count = hart_wait_state_transition(&secondary_cpus_mask, hsm_timer_duration, &transition_states);
+
+	report(count == max_cpus - 1, "all secondary harts started");
+
+	hart_wait_until_idle(&secondary_cpus_mask, hsm_timer_duration);
+
+	report(cpumask_weight(&sbi_hsm_started_hart_checks) == max_cpus - 1,
+	       "all secondary harts are already started");
+
+	report_prefix_pop();
+
+	report_prefix_push("hart_suspend");
+
+	if (!sbi_probe(SBI_EXT_IPI)) {
+		report_skip("skipping suspension tests since ipi extension is unavailable");
+		report_prefix_pop();
+		ipi_unavailable = true;
+		goto sbi_hsm_hart_stop_tests;
+	}
+
+	on_cpumask_async(&secondary_cpus_mask, hart_retentive_suspend, NULL);
+
+	transition_states = (struct hart_state_transition_info) {
+		.initial_state = SBI_EXT_HSM_STARTED,
+		.intermediate_state = SBI_EXT_HSM_SUSPEND_PENDING,
+		.final_state = SBI_EXT_HSM_SUSPENDED,
+	};
+	count = hart_wait_state_transition(&secondary_cpus_mask, hsm_timer_duration, &transition_states);
+
+	report(count == max_cpus - 1, "all secondary harts retentive suspended");
+
+	/* Ignore the return value since we check the status of each hart anyway */
+	sbi_send_ipi_cpumask(&secondary_cpus_mask);
+
+	transition_states = (struct hart_state_transition_info) {
+		.initial_state = SBI_EXT_HSM_SUSPENDED,
+		.intermediate_state = SBI_EXT_HSM_RESUME_PENDING,
+		.final_state = SBI_EXT_HSM_STARTED,
+	};
+	count = hart_wait_state_transition(&secondary_cpus_mask, hsm_timer_duration, &transition_states);
+
+	report(count == max_cpus - 1, "all secondary harts retentive resumed");
+
+	hart_wait_until_idle(&secondary_cpus_mask, hsm_timer_duration);
+
+	on_cpumask_async(&secondary_cpus_mask, hart_non_retentive_suspend, NULL);
+
+	transition_states = (struct hart_state_transition_info) {
+		.initial_state = SBI_EXT_HSM_STARTED,
+		.intermediate_state = SBI_EXT_HSM_SUSPEND_PENDING,
+		.final_state = SBI_EXT_HSM_SUSPENDED,
+	};
+	count = hart_wait_state_transition(&secondary_cpus_mask, hsm_timer_duration, &transition_states);
+
+	report(count == max_cpus - 1, "all secondary harts non-retentive suspended");
+
+	/* Ignore the return value since we check the status of each hart anyway */
+	sbi_send_ipi_cpumask(&secondary_cpus_mask);
+
+	transition_states = (struct hart_state_transition_info) {
+		.initial_state = SBI_EXT_HSM_SUSPENDED,
+		.intermediate_state = SBI_EXT_HSM_RESUME_PENDING,
+		.final_state = SBI_EXT_HSM_STARTED,
+	};
+	count = hart_wait_state_transition(&secondary_cpus_mask, hsm_timer_duration, &transition_states);
+	check = 0;
+
+	for_each_cpu(cpu, &secondary_cpus_mask) {
+		sbi_hsm_timer_fired = false;
+		timer_start(hsm_timer_duration);
+
+		while (!(READ_ONCE(sbi_hsm_non_retentive_hart_suspend_checks[cpu]) & SBI_HSM_TEST_DONE) && !sbi_hsm_timer_fired)
+			cpu_relax();
+
+		timer_stop();
+
+		if (sbi_hsm_timer_fired) {
+			report_info("hsm timer fired before hart %ld is done with non-retentive resume checks", hartid);
+			continue;
+		}
+
+		if (!(sbi_hsm_non_retentive_hart_suspend_checks[cpu] & SBI_HSM_TEST_SATP))
+			report_info("satp is not zero for test on cpu%d (hartid = %lx)", cpu, hartid);
+		else if (!(sbi_hsm_non_retentive_hart_suspend_checks[cpu] & SBI_HSM_TEST_SIE))
+			report_info("sstatus.SIE is not zero for test on cpu%d (hartid = %lx)", cpu, hartid);
+		else if (!(sbi_hsm_non_retentive_hart_suspend_checks[cpu] & SBI_HSM_TEST_MAGIC_A1))
+			report_info("a1 does not start with magic for test on cpu%d (hartid = %lx)", cpu, hartid);
+		else if (!(sbi_hsm_non_retentive_hart_suspend_checks[cpu] & SBI_HSM_TEST_HARTID_A0))
+			report_info("a0 is not hartid for test on cpu%d (hartid = %lx)", cpu, hartid);
+		else
+			check++;
+	}
+
+	report(count == max_cpus - 1, "all secondary harts non-retentive resumed");
+	report(check == max_cpus - 1, "all secondary harts have expected register values after non-retentive resume");
+
+	report_prefix_pop();
+
+sbi_hsm_hart_stop_tests:
+	report_prefix_push("hart_stop");
+
+	if (ipi_unavailable)
+		on_cpumask_async(&secondary_cpus_mask, stop_cpu, NULL);
+	else
+		memset(sbi_hsm_stop_hart, 1, sizeof(sbi_hsm_stop_hart));
+
+	transition_states = (struct hart_state_transition_info) {
+		.initial_state = SBI_EXT_HSM_STARTED,
+		.intermediate_state = SBI_EXT_HSM_STOP_PENDING,
+		.final_state = SBI_EXT_HSM_STOPPED,
+	};
+	count = hart_wait_state_transition(&secondary_cpus_mask, hsm_timer_duration, &transition_states);
+
+	report(count == max_cpus - 1, "all secondary harts stopped");
+
+	report_prefix_pop();
+
+	if (__riscv_xlen == 32 || ipi_unavailable) {
+		local_irq_disable();
+		hsm_timer_teardown();
+		report_prefix_pop();
+		return;
+	}
+
+	report_prefix_push("hart_suspend");
+
+	/* Select just one secondary cpu to run suspension tests with MSB of suspend type being set */
+	cpu = cpumask_next(-1, &secondary_cpus_mask);
+	hartid = cpus[cpu].hartid;
+	cpumask_clear(&mask);
+	cpumask_set_cpu(cpu, &mask);
+
+	/* Boot up the secondary cpu and let it proceed to the idle loop */
+	on_cpu(cpu, start_cpu, NULL);
+
+	on_cpu_async(cpu, hart_retentive_suspend_with_msb_set, NULL);
+
+	transition_states = (struct hart_state_transition_info) {
+		.initial_state = SBI_EXT_HSM_STARTED,
+		.intermediate_state = SBI_EXT_HSM_SUSPEND_PENDING,
+		.final_state = SBI_EXT_HSM_SUSPENDED,
+	};
+	count = hart_wait_state_transition(&mask, hsm_timer_duration, &transition_states);
+
+	report(count, "secondary hart retentive suspended with MSB set");
+
+	/* Ignore the return value since we manually validate the status of the hart anyway */
+	sbi_send_ipi_cpu(cpu);
+
+	transition_states = (struct hart_state_transition_info) {
+		.initial_state = SBI_EXT_HSM_SUSPENDED,
+		.intermediate_state = SBI_EXT_HSM_RESUME_PENDING,
+		.final_state = SBI_EXT_HSM_STARTED,
+	};
+	count = hart_wait_state_transition(&mask, hsm_timer_duration, &transition_states);
+
+	report(count, "secondary hart retentive resumed with MSB set");
+
+	/* Reset these flags so that we can reuse them for the non-retentive suspension test */
+	sbi_hsm_stop_hart[cpu] = 0;
+	sbi_hsm_non_retentive_hart_suspend_checks[cpu] = 0;
+
+	on_cpu_async(cpu, hart_non_retentive_suspend_with_msb_set, NULL);
+
+	transition_states = (struct hart_state_transition_info) {
+		.initial_state = SBI_EXT_HSM_STARTED,
+		.intermediate_state = SBI_EXT_HSM_SUSPEND_PENDING,
+		.final_state = SBI_EXT_HSM_SUSPENDED,
+	};
+	count = hart_wait_state_transition(&mask, hsm_timer_duration, &transition_states);
+
+	report(count, "secondary hart non-retentive suspended with MSB set");
+
+	/* Ignore the return value since we manually validate the status of the hart anyway */
+	sbi_send_ipi_cpu(cpu);
+
+	transition_states = (struct hart_state_transition_info) {
+		.initial_state = SBI_EXT_HSM_SUSPENDED,
+		.intermediate_state = SBI_EXT_HSM_RESUME_PENDING,
+		.final_state = SBI_EXT_HSM_STARTED,
+	};
+	count = hart_wait_state_transition(&mask, hsm_timer_duration, &transition_states);
+	check = 0;
+
+	if (count) {
+		sbi_hsm_timer_fired = false;
+		timer_start(hsm_timer_duration);
+
+		while (!(READ_ONCE(sbi_hsm_non_retentive_hart_suspend_checks[cpu]) & SBI_HSM_TEST_DONE) && !sbi_hsm_timer_fired)
+			cpu_relax();
+
+		timer_stop();
+
+		if (sbi_hsm_timer_fired) {
+			report_info("hsm timer fired before cpu%d (hartid = %lx) is done with non-retentive resume checks", cpu, hartid);
+		} else {
+			if (!(sbi_hsm_non_retentive_hart_suspend_checks[cpu] & SBI_HSM_TEST_SATP))
+				report_info("satp is not zero for test on cpu%d (hartid = %lx)", cpu, hartid);
+			else if (!(sbi_hsm_non_retentive_hart_suspend_checks[cpu] & SBI_HSM_TEST_SIE))
+				report_info("sstatus.SIE is not zero for test on cpu%d (hartid = %lx)", cpu, hartid);
+			else if (!(sbi_hsm_non_retentive_hart_suspend_checks[cpu] & SBI_HSM_TEST_MAGIC_A1))
+				report_info("a1 does not start with magic for test on cpu%d (hartid = %lx)", cpu, hartid);
+			else if (!(sbi_hsm_non_retentive_hart_suspend_checks[cpu] & SBI_HSM_TEST_HARTID_A0))
+				report_info("a0 is not hartid for test on cpu%d (hartid = %lx)", cpu, hartid);
+			else
+				check = 1;
+		}
+	}
+
+	report(count, "secondary hart non-retentive resumed with MSB set");
+	report(check, "secondary hart has expected register values after non-retentive resume with MSB set");
+
+	report_prefix_pop();
+
+	report_prefix_push("hart_stop");
+
+	sbi_hsm_stop_hart[cpu] = 1;
+
+	transition_states = (struct hart_state_transition_info) {
+		.initial_state = SBI_EXT_HSM_STARTED,
+		.intermediate_state = SBI_EXT_HSM_STOP_PENDING,
+		.final_state = SBI_EXT_HSM_STOPPED,
+	};
+	count = hart_wait_state_transition(&mask, hsm_timer_duration, &transition_states);
+
+	report(count, "secondary hart stopped after suspension tests with MSB set");
+
+	local_irq_disable();
+	hsm_timer_teardown();
+	report_prefix_popn(2);
+}
+
 #define DBCN_WRITE_TEST_STRING		"DBCN_WRITE_TEST_STRING\n"
 #define DBCN_WRITE_BYTE_TEST_BYTE	((u8)'a')
 
@@ -334,11 +1141,9 @@
 {
 	unsigned long num_bytes = strlen(DBCN_WRITE_TEST_STRING);
 	unsigned long base_addr_lo, base_addr_hi;
-	bool do_invalid_addr = false;
 	bool highmem_supported = true;
 	phys_addr_t paddr;
 	struct sbiret ret;
-	const char *tmp;
 	char *buf;
 
 	report_prefix_push("dbcn");
@@ -361,13 +1166,11 @@
 	dbcn_write_test(&buf[PAGE_SIZE - num_bytes / 2], num_bytes, false);
 	report_prefix_pop();
 
-	tmp = getenv("SBI_HIGHMEM_NOT_SUPPORTED");
-	if (tmp && atol(tmp) != 0)
+	if (env_enabled("SBI_HIGHMEM_NOT_SUPPORTED"))
 		highmem_supported = false;
 
 	report_prefix_push("high boundary");
-	tmp = getenv("SBI_DBCN_SKIP_HIGH_BOUNDARY");
-	if (!tmp || atol(tmp) == 0)
+	if (!env_enabled("SBI_DBCN_SKIP_HIGH_BOUNDARY"))
 		dbcn_high_write_test(DBCN_WRITE_TEST_STRING, num_bytes,
 				     HIGH_ADDR_BOUNDARY - PAGE_SIZE, PAGE_SIZE - num_bytes / 2,
 				     highmem_supported);
@@ -376,12 +1179,8 @@
 	report_prefix_pop();
 
 	report_prefix_push("high page");
-	tmp = getenv("SBI_DBCN_SKIP_HIGH_PAGE");
-	if (!tmp || atol(tmp) == 0) {
-		paddr = HIGH_ADDR_BOUNDARY;
-		tmp = getenv("HIGH_PAGE");
-		if (tmp)
-			paddr = strtoull(tmp, NULL, 0);
+	if (!env_enabled("SBI_DBCN_SKIP_HIGH_PAGE")) {
+		paddr = getenv("HIGH_PAGE") ? strtoull(getenv("HIGH_PAGE"), NULL, 0) : HIGH_ADDR_BOUNDARY;
 		dbcn_high_write_test(DBCN_WRITE_TEST_STRING, num_bytes, paddr, 0, highmem_supported);
 	} else {
 		report_skip("user disabled");
@@ -390,16 +1189,7 @@
 
 	/* Bytes are read from memory and written to the console */
 	report_prefix_push("invalid parameter");
-	tmp = getenv("INVALID_ADDR_AUTO");
-	if (tmp && atol(tmp) == 1) {
-		paddr = get_highest_addr() + 1;
-		do_invalid_addr = true;
-	} else if (env_or_skip("INVALID_ADDR")) {
-		paddr = strtoull(getenv("INVALID_ADDR"), NULL, 0);
-		do_invalid_addr = true;
-	}
-
-	if (do_invalid_addr) {
+	if (get_invalid_addr(&paddr, false)) {
 		split_phys_addr(paddr, &base_addr_hi, &base_addr_lo);
 		ret = sbi_dbcn_write(1, base_addr_lo, base_addr_hi);
 		report(ret.error == SBI_ERR_INVALID_PARAM, "address (error=%ld)", ret.error);
@@ -422,6 +1212,231 @@
 	report_prefix_popn(2);
 }
 
+void sbi_susp_resume(unsigned long hartid, unsigned long opaque);
+jmp_buf sbi_susp_jmp;
+
+struct susp_params {
+	unsigned long sleep_type;
+	unsigned long resume_addr;
+	unsigned long opaque;
+	bool returns;
+	struct sbiret ret;
+};
+
+static bool susp_basic_prep(unsigned long ctx[], struct susp_params *params)
+{
+	int cpu, me = smp_processor_id();
+	struct sbiret ret;
+	cpumask_t mask;
+
+	memset(params, 0, sizeof(*params));
+	params->sleep_type = 0; /* suspend-to-ram */
+	params->resume_addr = virt_to_phys(sbi_susp_resume);
+	params->opaque = virt_to_phys(ctx);
+	params->returns = false;
+
+	cpumask_copy(&mask, &cpu_present_mask);
+	cpumask_clear_cpu(me, &mask);
+	on_cpumask_async(&mask, stop_cpu, NULL);
+
+	/* Wait up to 1s for all harts to stop */
+	for (int i = 0; i < 100; i++) {
+		int count = 1;
+
+		udelay(10000);
+
+		for_each_present_cpu(cpu) {
+			if (cpu == me)
+				continue;
+			ret = sbi_hart_get_status(cpus[cpu].hartid);
+			if (!ret.error && ret.value == SBI_EXT_HSM_STOPPED)
+				++count;
+		}
+		if (count == cpumask_weight(&cpu_present_mask))
+			break;
+	}
+
+	for_each_present_cpu(cpu) {
+		ret = sbi_hart_get_status(cpus[cpu].hartid);
+		if (cpu == me) {
+			assert_msg(!ret.error && ret.value == SBI_EXT_HSM_STARTED,
+				   "cpu%d is not started", cpu);
+		} else {
+			assert_msg(!ret.error && ret.value == SBI_EXT_HSM_STOPPED,
+				   "cpu%d is not stopped", cpu);
+		}
+	}
+
+	return true;
+}
+
+static void susp_basic_check(unsigned long ctx[], struct susp_params *params)
+{
+	if (ctx[SBI_SUSP_RESULTS_IDX] == SBI_SUSP_TEST_MASK) {
+		report_pass("suspend and resume");
+	} else {
+		if (!(ctx[SBI_SUSP_RESULTS_IDX] & SBI_SUSP_TEST_SATP))
+			report_fail("SATP set to zero on resume");
+		if (!(ctx[SBI_SUSP_RESULTS_IDX] & SBI_SUSP_TEST_SIE))
+			report_fail("sstatus.SIE clear on resume");
+		if (!(ctx[SBI_SUSP_RESULTS_IDX] & SBI_SUSP_TEST_HARTID))
+			report_fail("a0 is hartid on resume");
+	}
+}
+
+static bool susp_type_prep(unsigned long ctx[], struct susp_params *params)
+{
+	bool r;
+
+	r = susp_basic_prep(ctx, params);
+	assert(r);
+	params->sleep_type = 1;
+	params->returns = true;
+	params->ret.error = SBI_ERR_INVALID_PARAM;
+
+	return true;
+}
+
+static bool susp_badaddr_prep(unsigned long ctx[], struct susp_params *params)
+{
+	phys_addr_t badaddr;
+	bool r;
+
+	if (!get_invalid_addr(&badaddr, false))
+		return false;
+
+	r = susp_basic_prep(ctx, params);
+	assert(r);
+	params->resume_addr = badaddr;
+	params->returns = true;
+	params->ret.error = SBI_ERR_INVALID_ADDRESS;
+
+	return true;
+}
+
+static bool susp_one_prep(unsigned long ctx[], struct susp_params *params)
+{
+	int started = 0, cpu, me = smp_processor_id();
+	struct sbiret ret;
+	bool r;
+
+	if (cpumask_weight(&cpu_present_mask) < 2) {
+		report_skip("At least 2 cpus required");
+		return false;
+	}
+
+	r = susp_basic_prep(ctx, params);
+	assert(r);
+	params->returns = true;
+	params->ret.error = SBI_ERR_DENIED;
+
+	for_each_present_cpu(cpu) {
+		if (cpu == me)
+			continue;
+		break;
+	}
+
+	on_cpu(cpu, start_cpu, NULL);
+
+	for_each_present_cpu(cpu) {
+		ret = sbi_hart_get_status(cpus[cpu].hartid);
+		assert_msg(!ret.error, "HSM get status failed for cpu%d", cpu);
+		if (ret.value == SBI_EXT_HSM_STARTED)
+			started++;
+	}
+
+	assert(started == 2);
+
+	return true;
+}
+
+static void check_susp(void)
+{
+	unsigned long csrs[] = {
+		[SBI_CSR_SSTATUS_IDX] = csr_read(CSR_SSTATUS),
+		[SBI_CSR_SIE_IDX] = csr_read(CSR_SIE),
+		[SBI_CSR_STVEC_IDX] = csr_read(CSR_STVEC),
+		[SBI_CSR_SSCRATCH_IDX] = csr_read(CSR_SSCRATCH),
+		[SBI_CSR_SATP_IDX] = csr_read(CSR_SATP),
+	};
+	unsigned long ctx[] = {
+		[SBI_SUSP_MAGIC_IDX] = SBI_SUSP_MAGIC,
+		[SBI_SUSP_CSRS_IDX] = (unsigned long)csrs,
+		[SBI_SUSP_HARTID_IDX] = current_thread_info()->hartid,
+		[SBI_SUSP_TESTNUM_IDX] = 0,
+		[SBI_SUSP_RESULTS_IDX] = 0,
+	};
+	enum {
+#define SUSP_FIRST_TESTNUM 1
+		SUSP_BASIC = SUSP_FIRST_TESTNUM,
+		SUSP_TYPE,
+		SUSP_BAD_ADDR,
+		SUSP_ONE_ONLINE,
+		NR_SUSP_TESTS,
+	};
+	struct susp_test {
+		const char *name;
+		bool (*prep)(unsigned long ctx[], struct susp_params *params);
+		void (*check)(unsigned long ctx[], struct susp_params *params);
+	} susp_tests[] = {
+		[SUSP_BASIC]		= { "basic",		susp_basic_prep,	susp_basic_check,	},
+		[SUSP_TYPE]		= { "sleep_type",	susp_type_prep,					},
+		[SUSP_BAD_ADDR]		= { "bad addr",		susp_badaddr_prep,				},
+		[SUSP_ONE_ONLINE]	= { "one cpu online",	susp_one_prep,					},
+	};
+	struct susp_params params;
+	struct sbiret ret;
+	int testnum, i;
+
+	local_irq_disable();
+	timer_stop();
+
+	report_prefix_push("susp");
+
+	ret = sbi_ecall(SBI_EXT_SUSP, 1, 0, 0, 0, 0, 0, 0);
+	report(ret.error == SBI_ERR_NOT_SUPPORTED, "funcid != 0 not supported");
+
+	for (i = SUSP_FIRST_TESTNUM; i < NR_SUSP_TESTS; i++) {
+		report_prefix_push(susp_tests[i].name);
+
+		ctx[SBI_SUSP_TESTNUM_IDX] = i;
+		ctx[SBI_SUSP_RESULTS_IDX] = 0;
+
+		assert(susp_tests[i].prep);
+		if (!susp_tests[i].prep(ctx, &params)) {
+			report_prefix_pop();
+			continue;
+		}
+
+		if ((testnum = setjmp(sbi_susp_jmp)) == 0) {
+			ret = sbi_system_suspend(params.sleep_type, params.resume_addr, params.opaque);
+
+			if (!params.returns && ret.error == SBI_ERR_NOT_SUPPORTED) {
+				report_skip("SUSP not supported?");
+				report_prefix_popn(2);
+				return;
+			} else if (!params.returns) {
+				report_fail("unexpected return with error: %ld, value: %ld", ret.error, ret.value);
+			} else {
+				report(ret.error == params.ret.error, "expected sbi.error");
+				if (ret.error != params.ret.error)
+					report_info("expected error %ld, received %ld", params.ret.error, ret.error);
+			}
+
+			report_prefix_pop();
+			continue;
+		}
+		assert(testnum == i);
+
+		if (susp_tests[i].check)
+			susp_tests[i].check(ctx, &params);
+
+		report_prefix_pop();
+	}
+
+	report_prefix_pop();
+}
+
 int main(int argc, char **argv)
 {
 	if (argc > 1 && !strcmp(argv[1], "-h")) {
@@ -432,7 +1447,10 @@
 	report_prefix_push("sbi");
 	check_base();
 	check_time();
+	check_ipi();
+	check_hsm();
 	check_dbcn();
+	check_susp();
 
 	return report_summary();
 }
diff --git a/riscv/unittests.cfg b/riscv/unittests.cfg
index cbd36bf..2eb760e 100644
--- a/riscv/unittests.cfg
+++ b/riscv/unittests.cfg
@@ -16,4 +16,5 @@
 # Set $FIRMWARE_OVERRIDE to /path/to/firmware to select the SBI implementation.
 [sbi]
 file = sbi.flat
+smp = $MAX_SMP
 groups = sbi