Skip to content

risc-v/esp32p4: BUILD_PROTECTED support, with PSRAM in the user heap - #20125

Open
NevynUK wants to merge 2 commits into
apache:masterfrom
NevynUK:AddESP32P4KernelBuild
Open

risc-v/esp32p4: BUILD_PROTECTED support, with PSRAM in the user heap#20125
NevynUK wants to merge 2 commits into
apache:masterfrom
NevynUK:AddESP32P4KernelBuild

Conversation

@NevynUK

@NevynUK NevynUK commented Sep 12, 2026

Copy link
Copy Markdown

Summary

  • Adds CONFIG_BUILD_PROTECTED for 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 an esp32p4-tab5:knsh config.
  • Two commits, each of which builds and boots standalone:
    1. Add BUILD_PROTECTED support.
    2. Add PSRAM to the user heap in a protected build.
  • No related NuttX or nuttx-apps issue.

Why the arch change is needed. 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 discards it - correct on a CLINT part, lossy here, because whatever the last trap left in the CSR is what the next mret consumes. Two changes follow, and both are required; with either alone the port does not reach NSH.

  1. mcause is saved and restored with the trap frame. REG_MCAUSE_NDX = 33, so INT_XCPT_REGS becomes 34. It is restored before mstatus: mstatus.MPP/MPIE alias mcause[29:28]/[27], and synthesised frames leave
    the slot zero, so writing it after mstatus drops kernel threads to U-mode and faults on the first instruction fetch of kernel text.
  2. 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 it, 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 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; and knsh/defconfig set neither ESPRESSIF_SPIRAM nor enough MM_REGIONS. kumm_addregion() resolves to mm_addregion(USR_HEAP, ...) and USR_HEAP is (*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) to SOC_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/Makefile pulls the ROM linker scripts for memcpy/strlen, but deliberately not esp32p4.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 as exit() -> fflush(NULL) faulting.
  • kernel-space.ld pulls in the flat sections script, so a board's Make.defs must not also add esp32p4_sections.ld, or .flash.text is emitted twice and _stext/_etext collapse onto the second, empty section.
  • esp32p4_sections{,.rev3}.ld: 189 lines of *libarch.a: -> *arch.a:. The protected kernel archive is libkarch.a; the widened pattern matches both, so flat builds are unaffected.
  • ESPRESSIF_KERNEL_OWNS_PMP compiles the HAL's cpu_region_protect.c with PMP_L cleared. The HAL locks every entry from bootloader_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

  • New feature added? YES. CONFIG_BUILD_PROTECTED on ESP32-P4 plus the esp32p4-tab5:knsh config. Off by default.
  • Impact on user? NO for existing configs; a protected build is opt-in.
  • Impact on build? NO. No new host tools or build steps.
  • Impact on hardware? YES, scoped. Shared RISC-V files (irq.h, riscv_swint.c, riscv_doirq.c, riscv_schedulesigaction.c, riscv_internal.h, riscv_exception_common.S) gain lines only inside CONFIG_RISCV_FRAME_TRACE, REG_MCAUSE or CONFIG_ARCH_CHIP_ESP32P4, all false on every other port. The flat ESP32-P4 build comes out byte-for-byte identical in size with identical free output.
  • Impact on documentation? YES, provided. esp32p4-tab5/index.rst gains knsh and a protected-build flashing section.
  • Impact on security? YES, positive in a protected build: user code loses the blanket PMP grant over kernel data that the HAL leaves behind. Note the trade-off in ESPRESSIF_KERNEL_OWNS_PMP - unlocked entries do not constrain M-mode, so it is depends on !BUILD_FLAT.
  • Impact on compatibility? NO for other ports. The trap-frame ABI change (INT_XCPT_REGS 33 -> 34) is gated to ARCH_CHIP_ESP32P4 && !BUILD_FLAT.
  • Anything else? Two new Kconfig options, both default n and fully compiled out when off (0 symbols): RISCV_FRAME_TRACE (trap-frame ring, generic because the hooks are in shared files) and ESPRESSIF_P4DBG (IRQ/idle/timer counters plus bring-up markers).

Testing

Changes are verified on local setup and work as intended.

  • Build host: macOS (Darwin 25.6.0), arm64, riscv32-esp-elf-gcc 14.2.0 from the ESP-IDF v5.5.4 toolchain.
  • Target: RISC-V, ESP32-P4 rev v1.0, M5Stack Tab5; esp32p4-tab5:knsh and esp32p4-tab5:nsh.
  • Reproduce: ./tools/configure.sh esp32p4-tab5:knsh && make, flash nuttx.bin at 0x2000 and nuttx_user.bin at 0x110000, console UART0 115200.

All builds clean, zero compiler warnings.

config image ostest
knsh commit 1 224,280 B kernel + 144,544 B user status 0, 154 sections, 99.5 s
knsh commit 2 233,240 B kernel + 146,098 B user status 0, 154 sections, 99.3 s
knsh + P4DBG + FRAME_TRACE 233,876 B kernel status 0, 154 sections, 99.3 s
nsh flat 183,906 B text status 0, 162 sections, 125.5 s

ostest does not prove the PMP grant on its own, so the PSRAM window is also exercised from user mode with ramtest at its base, middle and top (0x48000000, 0x49000000, 0x49ff0000). The same runs pass on the flat build as a control. TESTING_RAMTEST ships in knsh; it is not in the flat nsh defconfig and was enabled there only for that control run, as was TESTING_OSTEST.

Testing logs before change (esp32p4-tab5:nsh, unchanged by this PR):

NuttShell (NSH) NuttX-3.6.1
nsh> free
      total       used       free    maxused    maxfree  nused  nfree name
   34039356      22988   34016368      23376   33554416     81      4 Umem
nsh> ostest
...
ostest_main: Exiting with status 0

Testing logs after change (esp32p4-tab5:knsh):

userspace: drom vma=0x40300080 lma=0x80 size=0xaa00
userspace: irom vma=0x40210000 lma=0x10000 size=0x13ab2
userspace: entry=0x4021091e heap=0x4ff40960..0x4ff80000
userspace: ready
I (875) cpu_start: cpu freq: 360000000 Hz
I (879) esp_psram: Adding pool of 32768K of PSRAM memory to heap allocator

NuttShell (NSH) NuttX-3.6.1
nsh> free
      total       used       free    maxused    maxfree  nused  nfree name
     217924       2644     215280      20664     215080     30      2 Kmem
   33814172       4300   33809872      19768   33554416     10      3 Umem
nsh> ostest
...
ostest_main: Exiting with status 0
nsh> ramtest -w -a 0x48000000 -s 65536
RAMTest: Marching ones: 48000000 65536
RAMTest: Marching zeroes: 48000000 65536
RAMTest: Pattern test: 48000000 65536 55555555 aaaaaaaa
RAMTest: Pattern test: 48000000 65536 66666666 99999999
RAMTest: Pattern test: 48000000 65536 33333333 cccccccc
RAMTest: Address-in-address test: 48000000 65536

Known limitations, neither a regression:

  • The USB Serial/JTAG console does not come up in a protected build, so knsh uses UART0 at 115200. Flat nsh over USB is unaffected.
  • Chip revision v1.0 prints the usual SELECTS_REV_LESS_V3 boot warning.

PR verification Self-Check

  • This PR introduces only one functional change.
  • I have updated all required description fields above.
  • My PR adheres to Contributing Guidelines and Documentation (git commit title and message, coding standard, etc).
  • My PR is still work in progress (not ready for review).
  • My PR is ready for review and can be safely merged into a codebase.

Attribution

This work was developed with AI assistance. Both commits carry Assisted-by: Claude:claude-opus-5 above the signature, per CONTRIBUTING.md section 1.5 and the ASF generative tooling guidance, alongside a Co-Authored-By trailer. All changes were reviewed and tested on hardware by the author, who certifies them under the DCO via Signed-off-by.

🤖 Generated with Claude Code

NevynUK and others added 2 commits September 12, 2026 12:41
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>
@github-actions github-actions Bot added Area: Documentation Improvements or additions to documentation Arch: risc-v Issues related to the RISC-V (32-bit or 64-bit) architecture Size: XL The size of the change in this PR is very large. Consider breaking down the PR into smaller pieces. Board: risc-v labels Sep 12, 2026
@github-actions

Copy link
Copy Markdown

MemBrowse Memory Report

No memory changes detected for:

@acassis acassis left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @NevynUK please fix the nxstyle issues on esp_idle.c

@NevynUK

NevynUK commented Sep 12, 2026

Copy link
Copy Markdown
Author

Hi @NevynUK please fix the nxstyle issues on esp_idle.c

The style issues are from upstream code and not related to this change.

Comment thread arch/risc-v/include/irq.h
* so REG_INT_CTX and everything below it keep their existing offsets.
*/

#if defined(CONFIG_ARCH_CHIP_ESP32P4) && !defined(CONFIG_BUILD_FLAT)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not good to check CONFIG_ARCH_CHIP_ESP32P4 in the common code

}
#endif

#ifdef CONFIG_RISCV_FRAME_TRACE

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you move the trace frame to new pr

* value.
*/

riscv_trace_frame(RISCV_TRACE_TAG_SIG_SCHED, tcb, tcb->xcp.regs);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Arch: risc-v Issues related to the RISC-V (32-bit or 64-bit) architecture Area: Documentation Improvements or additions to documentation Board: risc-v Size: XL The size of the change in this PR is very large. Consider breaking down the PR into smaller pieces.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants