Introduce tests for SBI system suspend. The basic test makes sure it works and other tests make sure it fails as expected with invalid entry criteria. To test on QEMU or hardware the firmware needs to support system suspend. For QEMU, OpenSBI can be told to enable its system suspend test mode by creating a new DTB which has opensbi-config { compatible = "opensbi,config"; system-suspend-test; }; added to the 'chosen' node. Then, run with '-dtb susp.dtb'. Signed-off-by: Andrew Jones <andrew.jones@xxxxxxxxx> --- lib/riscv/asm/asm.h | 3 + lib/riscv/asm/sbi.h | 1 + riscv/sbi-asm.S | 55 +++++++++ riscv/sbi-tests.h | 19 +++ riscv/sbi.c | 278 ++++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 344 insertions(+), 12 deletions(-) diff --git a/lib/riscv/asm/asm.h b/lib/riscv/asm/asm.h index 763b28e6ad3c..107b5bb7e981 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 1319439b7118..4e72f125fb43 100644 --- a/lib/riscv/asm/sbi.h +++ b/lib/riscv/asm/sbi.h @@ -22,6 +22,7 @@ enum sbi_ext_id { SBI_EXT_HSM = 0x48534d, SBI_EXT_SRST = 0x53525354, SBI_EXT_DBCN = 0x4442434E, + SBI_EXT_SUSP = 0x53555350, }; enum sbi_ext_base_fid { diff --git a/riscv/sbi-asm.S b/riscv/sbi-asm.S index 9606a13e5f3a..e871ea506f07 100644 --- a/riscv/sbi-asm.S +++ b/riscv/sbi-asm.S @@ -5,6 +5,7 @@ * Copyright (C) 2024, James Raphael Tiovalen <jamestiotio@xxxxxxxxx> */ #define __ASSEMBLY__ +#include <asm/asm.h> #include <asm/csr.h> #include "sbi-tests.h" @@ -69,3 +70,57 @@ sbi_hsm_check_hart_start: 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 index f5cc8635d2aa..d0a7561a47b3 100644 --- a/riscv/sbi-tests.h +++ b/riscv/sbi-tests.h @@ -2,9 +2,28 @@ #ifndef _RISCV_SBI_TESTS_H_ #define _RISCV_SBI_TESTS_H_ +#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_HSM_TEST_DONE (1 << 0) #define SBI_HSM_TEST_HARTID_A1 (1 << 1) #define SBI_HSM_TEST_SATP (1 << 2) #define SBI_HSM_TEST_SIE (1 << 3) +#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 + +#define SBI_SUSP_MAGIC 0x505b + +#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 + #endif /* _RISCV_SBI_TESTS_H_ */ diff --git a/riscv/sbi.c b/riscv/sbi.c index 1e7314ec8d98..44c76692dad4 100644 --- a/riscv/sbi.c +++ b/riscv/sbi.c @@ -6,11 +6,14 @@ */ #include <libcflat.h> #include <alloc_page.h> +#include <limits.h> +#include <memregions.h> +#include <on-cpus.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> @@ -22,6 +25,8 @@ #include <asm/smp.h> #include <asm/timer.h> +#include "sbi-tests.h" + #define HIGH_ADDR_BOUNDARY ((phys_addr_t)1 << 32) static void help(void) @@ -47,6 +52,22 @@ static struct sbiret sbi_dbcn_write_byte(uint8_t byte) return sbi_ecall(SBI_EXT_DBCN, SBI_EXT_DBCN_CONSOLE_WRITE_BYTE, byte, 0, 0, 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 failed to stop with sbiret.error %ld", smp_processor_id(), ret.error); +} + static void split_phys_addr(phys_addr_t paddr, unsigned long *hi, unsigned long *lo) { *lo = (unsigned long)paddr; @@ -98,6 +119,22 @@ static bool env_or_skip(const char *env) 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) { @@ -358,7 +395,6 @@ static void check_dbcn(void) { 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; @@ -407,15 +443,7 @@ static void check_dbcn(void) /* Bytes are read from memory and written to the console */ report_prefix_push("invalid parameter"); - if (env_enabled("INVALID_ADDR_AUTO")) { - 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); @@ -438,6 +466,231 @@ static void check_dbcn(void) 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, ¶ms)) { + 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, ¶ms); + + report_prefix_pop(); + } + + report_prefix_pop(); +} + int main(int argc, char **argv) { if (argc > 1 && !strcmp(argv[1], "-h")) { @@ -449,6 +702,7 @@ int main(int argc, char **argv) check_base(); check_time(); check_dbcn(); + check_susp(); return report_summary(); } -- 2.47.0