riscv: pmp: new implementation

This is the core code to manage PMP entries with only the global entries
initialisation for now. It is not yet linked into the build.

Signed-off-by: Nicolas Pitre <npitre@baylibre.com>
diff --git a/arch/riscv/core/pmp.S b/arch/riscv/core/pmp.S
new file mode 100644
index 0000000..313fa5c
--- /dev/null
+++ b/arch/riscv/core/pmp.S
@@ -0,0 +1,107 @@
+/*
+ * Copyright (c) 2022 BayLibre, SAS
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include <toolchain.h>
+#include <linker/sections.h>
+#include <arch/cpu.h>
+#include "asm_macros.inc"
+
+#define CSR_PMPCFG_BASE  0x3a0
+#define CSR_PMPADDR_BASE 0x3b0
+
+/*
+ * Prototype:
+ *
+ * void z_riscv_write_pmp_entries(unsigned int start,		// a0
+ *                                unsigned int end,		// a1
+ *                                bool clear_trailing_entries,	// a2
+ *                                ulong_t *pmp_addr,		// a3
+ *                                ulong_t *pmp_cfg)		// a4
+ *
+ * Called from pmp.c to write a range of PMP entries.
+ *
+ * PMP registers are accessed with the csr instruction which only takes an
+ * immediate value as the actual register. In order to avoid traversing
+ * the whole register list, we use the start index to jump directly to the
+ * location corresponding to the start of the wanted range. For this to work
+ * we disallow compressed instructions so the update block sizes are easily
+ * known (luckily they're all power-of-2's simplifying the code further).
+ *
+ * start < end && end <= CONFIG_PMP_SLOT must be true.
+ */
+
+GTEXT(z_riscv_write_pmp_entries)
+SECTION_FUNC(TEXT, z_riscv_write_pmp_entries)
+
+	la t0, pmpaddr_store
+	slli t1, a0, 4  /* 16-byte instruction blocks */
+	add t0, t0, t1
+	jr t0
+
+pmpaddr_store:
+
+	.option push
+	.option norvc
+	.set _index, 0
+	.rept CONFIG_PMP_SLOT
+	lr t0, (RV_REGSIZE * _index)(a3)
+	li t1, _index + 1
+	csrw (CSR_PMPADDR_BASE + _index), t0
+	beq t1, a1, pmpaddr_done
+	.set _index, _index + 1
+	.endr
+	.option pop
+
+pmpaddr_done:
+
+	/*
+	 * Move to the pmpcfg space:
+	 * a0 = a0 / RV_REGSIZE
+	 * a1 = (a1 + RV_REGSIZE - 1) / RV_REGSIZE
+	 */
+	la t0, pmpcfg_store
+	srli a0, a0, RV_REGSHIFT
+	slli t1, a0, 4  /* 16-byte instruction blocks */
+	add t0, t0, t1
+	addi a1, a1, RV_REGSIZE - 1
+	srli a1, a1, RV_REGSHIFT
+	jr t0
+
+pmpcfg_store:
+
+	.option push
+	.option norvc
+	.set _index, 0
+	.rept (CONFIG_PMP_SLOT / RV_REGSIZE)
+	lr t0, (RV_REGSIZE * _index)(a4)
+	addi a0, a0, 1
+	csrw (CSR_PMPCFG_BASE + RV_REGSIZE/4 * _index), t0
+	beq a0, a1, pmpcfg_done
+	.set _index, _index + 1
+	.endr
+	.option pop
+
+pmpcfg_done:
+
+	beqz a2, done
+
+	la t0, pmpcfg_zerotail
+	slli a0, a0, 2  /* 4-byte instruction blocks */
+	add t0, t0, a0
+	jr t0
+
+pmpcfg_zerotail:
+
+	.option push
+	.option norvc
+	.set _index, 0
+	.rept (CONFIG_PMP_SLOT / RV_REGSIZE)
+	csrw (CSR_PMPCFG_BASE + RV_REGSIZE/4 * _index), zero
+	.set _index, _index + 1
+	.endr
+	.option pop
+
+done:	ret
diff --git a/arch/riscv/core/pmp.c b/arch/riscv/core/pmp.c
new file mode 100644
index 0000000..dbc52bd
--- /dev/null
+++ b/arch/riscv/core/pmp.c
@@ -0,0 +1,258 @@
+/*
+ * Copyright (c) 2022 BayLibre, SAS
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Physical Memory Protection (PMP) is RISC-V parlance for an MPU.
+ *
+ * The PMP is comprized of a number of entries or slots. This number depends
+ * on the hardware design. For each slot there is an address register and
+ * a configuration register. While each address register is matched to an
+ * actual CSR register, configuration registers are small and therefore
+ * several of them are bundled in a few additional CSR registers.
+ *
+ * PMP slot configurations are updated in memory to avoid read-modify-write
+ * cycles on corresponding CSR registers. Relevant CSR registers are always
+ * written in batch from their shadow copy in RAM for better efficiency.
+ */
+
+#include <kernel.h>
+#include <kernel_internal.h>
+#include <linker/linker-defs.h>
+#include <pmp.h>
+#include <sys/arch_interface.h>
+#include <arch/riscv/csr.h>
+
+#define LOG_LEVEL CONFIG_MPU_LOG_LEVEL
+#include <logging/log.h>
+LOG_MODULE_REGISTER(mpu);
+
+#define PMP_DEBUG_DUMP 0
+
+#ifdef CONFIG_64BIT
+# define PR_ADDR "0x%016lx"
+#else
+# define PR_ADDR "0x%08lx"
+#endif
+
+#define PMPCFG_STRIDE sizeof(ulong_t)
+
+#define PMP_ADDR(addr)			((addr) >> 2)
+#define NAPOT_RANGE(size)		(((size) - 1) >> 1)
+#define PMP_ADDR_NAPOT(addr, size)	PMP_ADDR(addr | NAPOT_RANGE(size))
+
+#define PMP_NONE 0
+
+static void print_pmp_entries(unsigned int start, unsigned int end,
+			      ulong_t *pmp_addr, ulong_t *pmp_cfg,
+			      const char *banner)
+{
+	uint8_t *pmp_n_cfg = (uint8_t *)pmp_cfg;
+	unsigned int index;
+
+	LOG_DBG("PMP %s:", banner);
+	for (index = start; index < end; index++) {
+		LOG_DBG("%3d: "PR_ADDR" 0x%02x", index,
+			pmp_addr[index],
+			pmp_n_cfg[index]);
+	}
+}
+
+static void dump_pmp_regs(const char *banner)
+{
+	ulong_t pmp_addr[CONFIG_PMP_SLOT];
+	ulong_t pmp_cfg[CONFIG_PMP_SLOT / PMPCFG_STRIDE];
+
+#define PMPADDR_READ(x) pmp_addr[x] = csr_read(pmpaddr##x)
+
+	FOR_EACH(PMPADDR_READ, (;), 0, 1, 2, 3, 4, 5, 6, 7);
+#if CONFIG_PMP_SLOT > 8
+	FOR_EACH(PMPADDR_READ, (;), 8, 9, 10, 11, 12, 13, 14, 15);
+#endif
+
+#undef PMPADDR_READ
+
+#ifdef CONFIG_64BIT
+	pmp_cfg[0] = csr_read(pmpcfg0);
+#if CONFIG_PMP_SLOT > 8
+	pmp_cfg[1] = csr_read(pmpcfg2);
+#endif
+#else
+	pmp_cfg[0] = csr_read(pmpcfg0);
+	pmp_cfg[1] = csr_read(pmpcfg1);
+#if CONFIG_PMP_SLOT > 8
+	pmp_cfg[2] = csr_read(pmpcfg2);
+	pmp_cfg[3] = csr_read(pmpcfg3);
+#endif
+#endif
+
+	print_pmp_entries(0, CONFIG_PMP_SLOT, pmp_addr, pmp_cfg, banner);
+}
+
+/**
+ * @brief Set PMP shadow register values in memory
+ *
+ * Register content is built using this function which selects the most
+ * appropriate address matching mode automatically. Note that the special
+ * case start=0 size=0 is valid and means the whole address range.
+ *
+ * @param index_p Location of the current PMP slot index to use. This index
+ *                will be updated according to the number of slots used.
+ * @param perm PMP permission flags
+ * @param start Start address of the memory area to cover
+ * @param size Size of the memory area to cover
+ * @param pmp_addr Array of pmpaddr values (starting at entry 0).
+ * @param pmp_cfg Array of pmpcfg values (starting at entry 0).
+ * @param index_limit Index value representing the size of the provided arrays.
+ * @return true on success, false when out of free PMP slots.
+ */
+static bool set_pmp_entry(unsigned int *index_p, uint8_t perm,
+			  uintptr_t start, size_t size,
+			  ulong_t *pmp_addr, ulong_t *pmp_cfg,
+			  unsigned int index_limit)
+{
+	uint8_t *pmp_n_cfg = (uint8_t *)pmp_cfg;
+	unsigned int index = *index_p;
+	bool ok = true;
+
+	__ASSERT((start & 0x3) == 0, "misaligned start address");
+	__ASSERT((size & 0x3) == 0, "misaligned size");
+
+	if (index >= index_limit) {
+		LOG_ERR("out of PMP slots");
+		ok = false;
+	} else if ((index == 0 && start == 0) ||
+		   (index != 0 && pmp_addr[index - 1] == PMP_ADDR(start))) {
+		/* We can use TOR using only one additional slot */
+		pmp_addr[index] = PMP_ADDR(start + size);
+		pmp_n_cfg[index] = perm | PMP_TOR;
+		index += 1;
+	} else if (((size  & (size - 1)) == 0) /* power of 2 */ &&
+		   ((start & (size - 1)) == 0) /* naturally aligned */) {
+		pmp_addr[index] = PMP_ADDR_NAPOT(start, size);
+		pmp_n_cfg[index] = perm | (size == 4 ? PMP_NA4 : PMP_NAPOT);
+		index += 1;
+	} else if (index + 1 >= index_limit) {
+		LOG_ERR("out of PMP slots");
+		ok = false;
+	} else {
+		pmp_addr[index] = PMP_ADDR(start);
+		pmp_n_cfg[index] = 0;
+		index += 1;
+		pmp_addr[index] = PMP_ADDR(start + size);
+		pmp_n_cfg[index] = perm | PMP_TOR;
+		index += 1;
+	}
+
+	*index_p = index;
+	return ok;
+}
+
+/**
+ * @brief Write a range of PMP entries to corresponding PMP registers
+ *
+ * PMP registers are accessed with the csr instruction which only takes an
+ * immediate value as the actual register. This is performed more efficiently
+ * in assembly code (pmp.S) than what is possible with C code.
+ *
+ * Requirement: start < end && end <= CONFIG_PMP_SLOT
+ *
+ * @param start Start of the PMP range to be written
+ * @param end End (exclusive) of the PMP range to be written
+ * @param clear_trailing_entries True if trailing entries must be turned off
+ * @param pmp_addr Array of pmpaddr values (starting at entry 0).
+ * @param pmp_cfg Array of pmpcfg values (starting at entry 0).
+ */
+extern void z_riscv_write_pmp_entries(unsigned int start, unsigned int end,
+				      bool clear_trailing_entries,
+				      ulong_t *pmp_addr, ulong_t *pmp_cfg);
+
+/**
+ * @brief Write a range of PMP entries to corresponding PMP registers
+ *
+ * This performs some sanity checks before calling z_riscv_write_pmp_entries().
+ *
+ * @param start Start of the PMP range to be written
+ * @param end End (exclusive) of the PMP range to be written
+ * @param clear_trailing_entries True if trailing entries must be turned off
+ * @param pmp_addr Array of pmpaddr values (starting at entry 0).
+ * @param pmp_cfg Array of pmpcfg values (starting at entry 0).
+ * @param index_limit Index value representing the size of the provided arrays.
+ */
+static void write_pmp_entries(unsigned int start, unsigned int end,
+			      bool clear_trailing_entries,
+			      ulong_t *pmp_addr, ulong_t *pmp_cfg,
+			      unsigned int index_limit)
+{
+	__ASSERT(start < end && end <= index_limit &&
+		 index_limit <= CONFIG_PMP_SLOT,
+		 "bad PMP range (start=%u end=%u)", start, end);
+
+	/* Be extra paranoid in case assertions are disabled */
+	if (start >= end || end > index_limit) {
+		k_panic();
+	}
+
+	if (clear_trailing_entries) {
+		/*
+		 * There are many config entries per pmpcfg register.
+		 * Make sure to clear trailing garbage in the last
+		 * register to be written if any. Remaining registers
+		 * will be cleared in z_riscv_write_pmp_entries().
+		 */
+		uint8_t *pmp_n_cfg = (uint8_t *)pmp_cfg;
+		unsigned int index;
+
+		for (index = end; index % PMPCFG_STRIDE != 0; index++) {
+			pmp_n_cfg[index] = 0;
+		}
+	}
+
+	print_pmp_entries(start, end, pmp_addr, pmp_cfg, "register write");
+
+	z_riscv_write_pmp_entries(start, end, clear_trailing_entries,
+				  pmp_addr, pmp_cfg);
+}
+
+/*
+ * This is used to seed thread PMP copies with global m-mode cfg entries
+ * sharing the same cfg register. Locked entries aren't modifiable but
+ * we could have non-locked entries here too.
+ */
+static ulong_t global_pmp_cfg[1];
+
+/* End of global PMP entry range */
+static unsigned int global_pmp_end_index;
+
+/**
+ * @Brief Initialize the PMP with global entries on each CPU
+ */
+void z_riscv_pmp_init(void)
+{
+	ulong_t pmp_addr[4];
+	ulong_t pmp_cfg[1];
+	unsigned int index = 0;
+
+	/* The read-only area is always there for every mode */
+	set_pmp_entry(&index, PMP_R | PMP_X | PMP_L,
+		      (uintptr_t)__rom_region_start,
+		      (size_t)__rom_region_size,
+		      pmp_addr, pmp_cfg, ARRAY_SIZE(pmp_addr));
+
+	write_pmp_entries(0, index, true, pmp_addr, pmp_cfg, ARRAY_SIZE(pmp_addr));
+
+#ifdef CONFIG_SMP
+	/* Make sure secondary CPUs produced the same values */
+	if (global_pmp_end_index != 0) {
+		__ASSERT(global_pmp_end_index == index, "");
+		__ASSERT(global_pmp_cfg[0] == pmp_cfg[0], "");
+	}
+#endif
+
+	global_pmp_cfg[0] = pmp_cfg[0];
+	global_pmp_end_index = index;
+
+	if (PMP_DEBUG_DUMP) {
+		dump_pmp_regs("initial register dump");
+	}
+}
diff --git a/arch/riscv/include/pmp.h b/arch/riscv/include/pmp.h
new file mode 100644
index 0000000..0cc899e
--- /dev/null
+++ b/arch/riscv/include/pmp.h
@@ -0,0 +1,12 @@
+/*
+ * Copyright (c) 2022 BayLibre, SAS
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#ifndef PMP_H_
+#define PMP_H_
+
+void z_riscv_pmp_init(void);
+
+#endif /* PMP_H_ */