risc-v/esp32p4: BUILD_PROTECTED support, with PSRAM in the user heap - #20125
Open
NevynUK wants to merge 2 commits into
Open
risc-v/esp32p4: BUILD_PROTECTED support, with PSRAM in the user heap#20125NevynUK wants to merge 2 commits into
NevynUK wants to merge 2 commits into
Conversation
Add CONFIG_BUILD_PROTECTED for the ESP32-P4, linking the kernel and the
user applications as two separate images: the kernel runs in machine
mode, user code in user mode, and the PMP enforces the split. Adds an
esp32p4-tab5:knsh configuration that exercises it.
The P4 uses a CLIC, where mcause is not only a cause register. It
carries mpp[29:28], mpie[27], minhv[30] and mpil[23:16], and mret
restores privilege from mpp and mintstatus.mil from mpil. Upstream
reads mcause for dispatch and then discards it, which is correct on a
CLINT part and lossy here: whatever the last trap left in the CSR is
what the next mret consumes. Two changes follow.
* mcause is saved and restored with the trap frame. REG_MCAUSE_NDX is
33, so INT_XCPT_REGS becomes 34. This is an ABI change to the trap
frame and is therefore gated to ARCH_CHIP_ESP32P4 && !BUILD_FLAT;
every other RISC-V port keeps 33 and an unchanged frame layout. It
uses the existing ARCH_RISCV_INTXCPT_EXTREGS extension slot, so
REG_INT_CTX and everything below it keep their offsets. It is
restored before mstatus: mstatus.MPP/MPIE are aliased into
mcause[29:28]/[27], and synthesised frames leave the slot zero, so
writing mcause after mstatus would drop kernel threads to U-mode and
fault on the first instruction fetch of kernel text.
* On a return to U-mode, mcause.interrupt (31) and minhv (30) are
cleared. The frame's mcause is the value latched for the trap that
saved the frame, but after a context switch the frame is restored by
a different trap, so interrupt = 1 tells the CLIC it is returning
from an interrupt that is not in flight. Kernel returns are left
alone.
Both changes are required; with either one alone the port does not
reach NSH.
There is no impact on other RISC-V ports. Every line added to the
shared files sits inside CONFIG_RISCV_FRAME_TRACE, REG_MCAUSE or
CONFIG_ARCH_CHIP_ESP32P4, all of which are false elsewhere.
Two new Kconfig options, both default n and fully compiled out when
off:
* RISCV_FRAME_TRACE - a ring of trap frames recorded where the kernel
chooses which frame to resume. Generic rather than Espressif-local
because the hooks are in shared files.
* ESPRESSIF_P4DBG - counters in the IRQ, idle and timer paths plus
bring-up markers via ROM printf.
Tested on an M5Stack Tab5 (ESP32-P4 rev v1.0), ostest run from NSH on
hardware:
esp32p4-tab5:knsh 224,280 B kernel + 144,544 B user
ostest status 0, 154 sections, 99.5 s
knsh + both debug options 225,176 B kernel
ostest status 0, 154 sections, 99.3 s
esp32p4-tab5:nsh (flat) ostest status 0, 162 sections,
125.5 s
The flat configuration is unchanged by this commit. TESTING_OSTEST is
not in its shipped defconfig and was enabled for that run only.
Known limitation: the USB Serial/JTAG console does not come up in a
protected build, so knsh uses UART0 at 115200. The flat nsh over USB
is unaffected. This is under investigation and is not a regression.
Assisted-by: Claude:claude-opus-5
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Mark Stevens <mark@thepcsite.co.uk>
Enable external PSRAM for esp32p4-tab5:knsh and place all 32 MB of it in
the user heap. The user heap grows from 259,740 to 33,814,172 bytes;
the kernel heap stays in internal SRAM and is unchanged.
The flat build has had PSRAM for some time, but three separate things
kept it out of a protected build, and all three had to be addressed:
* riscv_addregion() guarded its PSRAM block with
!defined(CONFIG_MM_KERNEL_HEAP), which is never true in a protected
build. The guard is widened to cover BUILD_PROTECTED as well.
kumm_addregion() resolves to mm_addregion(USR_HEAP, ...), and USR_HEAP
is the single heap in a flat build and (*USERSPACE->us_data->us_heap)
in the kernel phase of a protected one, so the same call reaches
user-accessible memory in both. A flat build that keeps a separate
kernel heap is deliberately left as it was.
* configure_mpu() described UIROM, UDROM, ROM and UDRAM only, so PSRAM
matched no PMP entry and user access to it faulted. A read/write TOR
pair now covers the window, bounded by the HAL's runtime
esp_psram_extram_vaddr_start() and _end().
* knsh/defconfig did not enable CONFIG_ESPRESSIF_SPIRAM, and its
CONFIG_MM_REGIONS of 2 left no room for a second user region.
Ordering of the PMP entries matters more than it first appears, and the
region table is now in ascending address order rather than having the
new pair appended. PMP resolves an access to the lowest-numbered entry
that matches it, so a low-numbered gap silently shadows any grant above
it: appending PSRAM after UDRAM leaves the gap at entry 4, which spans
UDROM_END (0x40380000) up to SOC_IROM_MASK_LOW (0x4fc00000), swallowing
0x48000000 and denying user access however the higher entries are
programmed. It presents as a store access fault on the first user-mode
touch of PSRAM, with the heap none the wiser. The comment in
configure_mpu() records this so the ordering is not "tidied" later.
Enabling SPIRAM selects ESPRESSIF_SPIRAM_USER_HEAP, which in turn
selects ESPRESSIF_DONT_USE_ROM_LIBC and drops esp32p4.rom.newlib.ld from
the kernel link. The user image already omits that script deliberately,
so this aligns the two links rather than diverging them.
No impact on the flat build: it comes out byte for byte identical in
size with identical free(1) output, because the widened guard evaluates
exactly as before when CONFIG_MM_KERNEL_HEAP is unset.
Tested on an M5Stack Tab5 (ESP32-P4 rev v1.0), on hardware:
esp32p4-tab5:knsh 233,240 B kernel + 146,098 B user
Kmem 217,908 B
Umem 33,814,172 B, maxfree 33,554,416 B, 3 regions
ostest status 0, 154 sections, 99.3 s
esp32p4-tab5:nsh text 183,906 B, Umem 34,039,356 B
unchanged by this commit
ostest alone does not prove the PMP grant, so the window was also
exercised directly from user mode with ramtest at its base, middle and
top (0x48000000, 0x49000000, 0x49ff0000): marching ones, marching
zeroes, three pattern tests and address-in-address, all passing. The
same three runs pass identically on the flat build as a control.
TESTING_RAMTEST is enabled in the knsh defconfig so the grant can be
checked on the board; it is not in the flat nsh defconfig and was
enabled there only for that control run.
Note for anyone repeating this: ramtest writes over whatever occupies
the addresses it is given, so running it against the base of a
heap-backed region destroys the allocator's region header. It is sound
as an access-permission probe, but the board wants a reset afterwards.
Assisted-by: Claude:claude-opus-5
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Mark Stevens <mark@thepcsite.co.uk>
NevynUK
requested review from
GUIDINGLI,
Ouss4,
anchao,
eren-terzioglu,
fdcavalcanti,
gustavonihei,
jerpelea,
lupyuen,
masayuki2009,
pkarashchenko,
pussuw,
tmedicci,
xiaoxiang781216 and
yf13
as code owners
September 12, 2026 13:25
Author
The style issues are from upstream code and not related to this change. |
acassis
approved these changes
Sep 12, 2026
| * so REG_INT_CTX and everything below it keep their existing offsets. | ||
| */ | ||
|
|
||
| #if defined(CONFIG_ARCH_CHIP_ESP32P4) && !defined(CONFIG_BUILD_FLAT) |
Contributor
There was a problem hiding this comment.
not good to check CONFIG_ARCH_CHIP_ESP32P4 in the common code
| } | ||
| #endif | ||
|
|
||
| #ifdef CONFIG_RISCV_FRAME_TRACE |
Contributor
There was a problem hiding this comment.
could you move the trace frame to new pr
| * value. | ||
| */ | ||
|
|
||
| riscv_trace_frame(RISCV_TRACE_TAG_SIG_SCHED, tcb, tcb->xcp.regs); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
CONFIG_BUILD_PROTECTEDfor the ESP32-P4. Kernel and user applications link as two images, the kernel runs M-mode and user code U-mode, and the PMP enforces the split. Adds anesp32p4-tab5:knshconfig.Add BUILD_PROTECTED support.Add PSRAM to the user heap in a protected build.Why the arch change is needed. The P4 uses a CLIC, where
mcauseis not only a cause register: it carriesmpp[29:28],mpie[27],minhv[30]andmpil[23:16], andmretrestores privilege frommppandmintstatus.milfrommpil. Upstream readsmcausefor dispatch and discards it - correct on a CLINT part, lossy here, because whatever the last trap left in the CSR is what the nextmretconsumes. Two changes follow, and both are required; with either alone the port does not reach NSH.mcauseis saved and restored with the trap frame.REG_MCAUSE_NDX = 33, soINT_XCPT_REGSbecomes 34. It is restored beforemstatus:mstatus.MPP/MPIEaliasmcause[29:28]/[27], and synthesised frames leavethe slot zero, so writing it after
mstatusdrops kernel threads to U-mode and faults on the first instruction fetch of kernel text.mcause.interrupt(31) andminhv(30) are cleared. The frame'smcauseis the value latched for the trap that saved it, but after a context switch the frame is restored by a different trap, sointerrupt = 1tells the CLIC it is returning from an interrupt that is not in flight. Kernel returns are untouched.PSRAM. Three things kept it out of a protected build:
riscv_addregion()guarded its PSRAM block with!defined(CONFIG_MM_KERNEL_HEAP), never true here;configure_mpu()described IROM/UDROM/ROM/UDRAM only, so PSRAM matched no PMP entry; andknsh/defconfigset neitherESPRESSIF_SPIRAMnor enoughMM_REGIONS.kumm_addregion()resolves tomm_addregion(USR_HEAP, ...)andUSR_HEAPis(*USERSPACE->us_data->us_heap)in the kernel phase, so no new syscall or userspace gateway is needed.PMP entry order is load-bearing. The region table is in ascending address order, not merely each TOR pair internally. PMP resolves an access to the lowest-numbered matching entry, so a low-numbered gap shadows any grant above it: appending PSRAM after UDRAM leaves the gap at entry 4 spanning
UDROM_END(0x40380000) toSOC_IROM_MASK_LOW(0x4fc00000), which swallows 0x48000000 and denies user access however the higher entries are programmed. It presents as a store access fault on the first user-mode touch, with the heap none the wiser.configure_mpu()carries this comment so the order is not "tidied" later.Other notable points.
common/kernel/Makefilepulls the ROM linker scripts formemcpy/strlen, but deliberately notesp32p4.rom.newlib.ld: it defines newlib's stdio symbols absolutely, so user mode would jump into ROM where it has no PMP grant. It surfaced asexit()->fflush(NULL)faulting.kernel-space.ldpulls in the flat sections script, so a board'sMake.defsmust not also addesp32p4_sections.ld, or.flash.textis emitted twice and_stext/_etextcollapse onto the second, empty section.esp32p4_sections{,.rev3}.ld: 189 lines of*libarch.a:->*arch.a:. The protected kernel archive islibkarch.a; the widened pattern matches both, so flat builds are unaffected.ESPRESSIF_KERNEL_OWNS_PMPcompiles the HAL'scpu_region_protect.cwithPMP_Lcleared. The HAL locks every entry frombootloader_init()and the P4 has no Smepmp, so locked entries are unreclaimable - and one of them grants U-mode RW across all kernel data.Impact
CONFIG_BUILD_PROTECTEDon ESP32-P4 plus theesp32p4-tab5:knshconfig. Off by default.irq.h,riscv_swint.c,riscv_doirq.c,riscv_schedulesigaction.c,riscv_internal.h,riscv_exception_common.S) gain lines only insideCONFIG_RISCV_FRAME_TRACE,REG_MCAUSEorCONFIG_ARCH_CHIP_ESP32P4, all false on every other port. The flat ESP32-P4 build comes out byte-for-byte identical in size with identicalfreeoutput.esp32p4-tab5/index.rstgainsknshand a protected-build flashing section.ESPRESSIF_KERNEL_OWNS_PMP- unlocked entries do not constrain M-mode, so it isdepends on !BUILD_FLAT.INT_XCPT_REGS33 -> 34) is gated toARCH_CHIP_ESP32P4 && !BUILD_FLAT.default nand fully compiled out when off (0 symbols):RISCV_FRAME_TRACE(trap-frame ring, generic because the hooks are in shared files) andESPRESSIF_P4DBG(IRQ/idle/timer counters plus bring-up markers).Testing
Changes are verified on local setup and work as intended.
riscv32-esp-elf-gcc14.2.0 from the ESP-IDF v5.5.4 toolchain.esp32p4-tab5:knshandesp32p4-tab5:nsh../tools/configure.sh esp32p4-tab5:knsh && make, flashnuttx.binat 0x2000 andnuttx_user.binat 0x110000, console UART0 115200.All builds clean, zero compiler warnings.
knshcommit 1knshcommit 2knsh+P4DBG+FRAME_TRACEnshflatostestdoes not prove the PMP grant on its own, so the PSRAM window is also exercised from user mode withramtestat its base, middle and top (0x48000000, 0x49000000, 0x49ff0000). The same runs pass on the flat build as a control.TESTING_RAMTESTships inknsh; it is not in the flatnshdefconfig and was enabled there only for that control run, as wasTESTING_OSTEST.Testing logs before change (
esp32p4-tab5:nsh, unchanged by this PR):Testing logs after change (
esp32p4-tab5:knsh):Known limitations, neither a regression:
knshuses UART0 at 115200. Flatnshover USB is unaffected.SELECTS_REV_LESS_V3boot warning.PR verification Self-Check
Attribution
This work was developed with AI assistance. Both commits carry
Assisted-by: Claude:claude-opus-5above the signature, per CONTRIBUTING.md section 1.5 and the ASF generative tooling guidance, alongside aCo-Authored-Bytrailer. All changes were reviewed and tested on hardware by the author, who certifies them under the DCO viaSigned-off-by.🤖 Generated with Claude Code