Skip to content

PSRAM cache trashed on ESP32 M5 Stick S3 #12480

Description

@davepl

Board

ESP32 M5 Stick S3

Device Description

ESP32 M5 Stick S3

Hardware Configuration

Defaults, see ini

Version

v3.3.6

Type

Task

IDE Name

VSCode

Operating System

MacOS

Flash frequency

40MHz

PSRAM enabled

yes

Upload speed

2000000

Description

I'm getting data corruption in discarded cache pages on the M5 S3 Stick only:

What's proven:

PSRAM is Octal (QSPI driver can't even see the chip)
qio_opi initializes successfully, 8MB detected
Data in cache (≤32KB) reads back perfectly
Data evicted from cache and re-read from PSRAM is corrupt
This means the cache↔PSRAM data path is broken, not the PSRAM chip itself

[dev_m5stick_c_s3]
extends = dev_esp32_s3
board = m5stick-s3-n8r8 ; Requires the M5 Stick S3
upload_speed = 2000000
build_flags = -DM5STICKS3=1
-DESP32S3
-DARDUINO_USB_CDC_ON_BOOT=1
-DARDUINO_USB_MODE=1
${dev_m5.build_flags}
${psram_flags.build_flags}
lib_deps = ${dev_esp32_s3.lib_deps}
${base.bounce_deps}
m5stack/M5Unified @ ^0.2.7
M5Unified=https://github.com/m5stack/M5Unified
M5PM1=https://github.com/m5stack/M5PM1

The bug:

Every single Arduino ESP32 2.x qio_opi precompiled variant ships with CONFIG_ESP32S3_DATA_CACHE_LINE_SIZE=32. Whether or not the ESP-IDF Kconfig enforces 64B lines for OPI PSRAM, the ESP32-S3 TRM specifies that Octal PSRAM DDR mode uses 64-byte wrap bursts. A 32-byte cache line mismatch causes garbled data on writeback/fill. This may work "by luck" on WROOM-2 modules with external PSRAM but fail on the PICO-1 SiP package where timing margins are tighter.

If someone has an arduino project that uses PSRAM on the M5 S3, please let me know. For now, it appears broken.

Sketch

// Minimal ESP32-S3 OPI PSRAM cache-eviction bug reproducer
//
// Board: Any ESP32-S3 with Octal PSRAM (e.g. M5StickS3 / ESP32-S3-PICO-1-N8R8)
// Platform: Arduino ESP32 2.x (espressif32 @ ^6.x) with qio_opi memory_type
//
// Expected: All tests pass
// Actual:   Small test (4KB, fits in 32KB DCache) passes;
//           Large test (128KB, exceeds DCache) fails with 100% data corruption
//           on every word beyond the cache footprint.
//
// Build with PlatformIO:
//   board_build.arduino.memory_type = qio_opi
//   build_flags = -DBOARD_HAS_PSRAM -DUSE_PSRAM=1

#include <Arduino.h>
#include <esp_heap_caps.h>
#include <esp_psram.h>

static void test_psram(size_t size_bytes, const char *label)
{
    Serial.printf("\n--- %s: %u bytes ---\n", label, (unsigned)size_bytes);

    void *buf = heap_caps_malloc(size_bytes, MALLOC_CAP_SPIRAM);
    if (!buf) {
        Serial.printf("  FAIL: could not allocate\n");
        return;
    }
    Serial.printf("  Buffer at %p\n", buf);

    volatile uint32_t *p = (volatile uint32_t *)buf;
    size_t words = size_bytes / sizeof(uint32_t);

    // Write known pattern
    for (size_t i = 0; i < words; i++)
        p[i] = (uint32_t)(i ^ 0xCAFEBABE);

    // Read back and verify
    int errors = 0;
    for (size_t i = 0; i < words; i++) {
        uint32_t got = p[i];
        uint32_t exp = (uint32_t)(i ^ 0xCAFEBABE);
        if (got != exp) {
            if (errors < 4)
                Serial.printf("  ERR word[%u] @byte 0x%x: expected 0x%08X got 0x%08X\n",
                              (unsigned)i, (unsigned)(i * 4), exp, got);
            errors++;
        }
    }

    if (errors == 0)
        Serial.printf("  PASS: 0 errors in %u words\n", (unsigned)words);
    else
        Serial.printf("  FAIL: %d errors in %u words (%.1f%%)\n",
                      errors, (unsigned)words, 100.0 * errors / words);

    // Intentionally not freeing — heap metadata in PSRAM may itself be corrupt
}

void setup()
{
    Serial.begin(115200);
    delay(2000);

    Serial.printf("\n\n=== ESP32-S3 OPI PSRAM cache-eviction test ===\n");
    Serial.printf("CPU freq:  %u MHz\n", getCpuFrequencyMhz());
    Serial.printf("PSRAM init: %s\n", psramFound() ? "OK" : "FAIL");
    Serial.printf("PSRAM size: %u bytes\n", (unsigned)ESP.getPsramSize());
    Serial.printf("PSRAM free: %u bytes\n", (unsigned)ESP.getFreePsram());
    Serial.printf("DCache config: line=%d  size=0x%x\n",
                  CONFIG_ESP32S3_DATA_CACHE_LINE_SIZE,
                  CONFIG_ESP32S3_DATA_CACHE_SIZE);
    Serial.printf("SPIRAM mode: %s\n",
#if defined(CONFIG_SPIRAM_MODE_OCT)
                  "OCT (OPI)"
#elif defined(CONFIG_SPIRAM_MODE_QUAD)
                  "QUAD (QSPI)"
#else
                  "UNKNOWN"
#endif
    );

    // Test 1: 4KB — fits entirely in DCache (32KB), never touches actual PSRAM chip
    test_psram(4 * 1024, "4KB (cache-only)");

    // Test 2: 128KB — 4x DCache size, forces eviction and re-read from PSRAM
    test_psram(128 * 1024, "128KB (exceeds 32KB DCache)");

    // Test 3: 1MB — well beyond cache, exercises bulk PSRAM access
    test_psram(1024 * 1024, "1MB (bulk PSRAM)");

    Serial.printf("\n=== Done ===\n");
    Serial.printf("If 4KB passes but larger tests fail, the DCache<->PSRAM\n");
    Serial.printf("writeback/fill path is broken. All Arduino ESP32 2.x\n");
    Serial.printf("qio_opi variants ship with CONFIG_ESP32S3_DATA_CACHE_LINE_SIZE=32.\n");
    Serial.printf("This may be incompatible with Octal DDR PSRAM on some modules.\n");
}

void loop()
{
    delay(10000);
}

Debug Message

SPIWP:0xee
mode:DIO, clock div:1
load:0x3fce3808,len:0x4bc
load:0x403c9700,len:0xbd8
load:0x403cc700,len:0x2a0c
entry 0x403c98d0
E (396) esp_core_dump_flash: No core dump partition found!
E (396) esp_core_dump_flash: No core dump partition found!
Replacing Idle Tasks with TaskManager...
(I) (PrintOutputHeader)(C1) NightDriverStrip
(I) 
(I) (PrintOutputHeader)(C1) ------------------------------------------------------------------------------------------------------------
(I) (PrintOutputHeader)(C1) M5STICKC: 0, USE_M5DISPLAY: 1, USE_TFTSPI: 0, USE_LCD: 0, AUDIO_ENABLED: 1, ENABLE_REMOTE: 0
(I) (PrintOutputHeader)(C1) ESP32 PSRAM Init: OK
(I) (PrintOutputHeader)(C1) Version 40: Wifi SSID: "Your SSID" - ESP32 Free Memory: 292956, PSRAM:8385815, PSRAM Free: 8360643
(I) (PrintOutputHeader)(C1) ESP32 Clock Freq : 240 MHz
> (I) (setup)(C1) Startup!
(I) (setup)(C1) Starting DebugLoopTaskEntry
>> Launching Debug Thread.  Mem: 292956, LargestBlk: 262132, PSRAM Free: 8360643/8385815, (I) (ReadWiFiConfig)(C1) Retrieved SSID and Password for source 1 from NVS: "Your SSID", "********"
(E) (ReadWiFiConfig)(C1) Could not read WiFi_ssid for source 0 from NVS
(W) (setup)(C1) Could not read Improv WiFI credentials, falling back to persisted compile-time credentials
(I) (ReadWiFiConfig)(C1) Retrieved SSID and Password for source 1 from NVS: "Your SSID", "********"
(W) (setup)(C1) Starting ImprovSerial for ESP32-S3
[   973][E][vfs_api.cpp:182] remove(): /improv.log does not exists or is directory
>> Launching JSON Writer Thread.  Mem: 284388, LargestBlk: 262132, PSRAM Free: 8360435/8385783, (I) (LoadJSONFile)(C1) Attempting to read JSON file /device.cfg
(I) (DeviceConfig)(C1) Loading DeviceConfig from JSON

=== PSRAM DIAGNOSTICS (before M5.begin) ===
DRAM heap OK before M5.begin
PSRAM buf at 0x3d808738, testing 4096 bytes
  Single-word test: wrote 0xDEADBEEF, read 0xDEADBEEF OK
  Pre-flush readback: [0]=0xCAFEBABE [1]=0xCAFEBABF [2]=0xCAFEBABC [3]=0xCAFEBABD [4]=0xCAFEBABA [5]=0xCAFEBABB [6]=0xCAFEBAB8 [7]=0xCAFEBAB9
  Sweep: 0 errors in 1024 words
  Byte test: wrote AA,55,0F,F0 read AA,55,0F,F0
  Walking-1 data bus test:
  Walking-1 done
  Address bus test:
  Address bus done
  (skipping free to avoid crash)
=== END PSRAM DIAGNOSTICS ===


assert failed: spinlock_acquire spinlock.h:122 (result == core_id || result == SPINLOCK_FREE)


Backtrace: 0x40379136:0x3fceb4e0 0x4037e871:0x3fceb500 0x40385a91:0x3fceb520 0x40381d25:0x3fceb650 0x40385531:0x3fceb690 0x40385701:0x3fceb6b0 0x403797d3:0x3fceb6d0 0x40379b7d:0x3fceb700 0x4207ea14:0x3fceb720 0x4207c643:0x3fceb770 0x4207d482:0x3fceb7d0 0x420668ca:0x3fceb820 0x4212560d:0x3fceb8b0 0x42121da2:0x3fceb8f0 0x42122069:0x3fcebb40 0x4204f41f:0x3fcebb70 0x42077e7a:0x3fcebc40
  #0  0x40379136 in panic_abort at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/esp_system/panic.c:408
  #1  0x4037e871 in esp_system_abort at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/esp_system/esp_system.c:137
  #2  0x40385a91 in __assert_func at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/newlib/assert.c:85
  #3  0x40381d25 in spinlock_acquire at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/esp_hw_support/include/soc/spinlock.h:122
      (inlined by) xPortEnterCriticalTimeout at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/freertos/port/xtensa/port.c:301
  #4  0x40385531 in vPortEnterCritical at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/freertos/port/xtensa/include/freertos/portmacro.h:578 (discriminator 1)
      (inlined by) multi_heap_internal_lock at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/heap/multi_heap.c:142 (discriminator 1)
  #5  0x40385701 in multi_heap_malloc at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/heap/multi_heap_poisoning.c:233
      (inlined by) multi_heap_malloc at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/heap/multi_heap_poisoning.c:223
  #6  0x403797d3 in heap_caps_malloc_base at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/heap/heap_caps.c:175
      (inlined by) heap_caps_malloc_base at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/heap/heap_caps.c:120
  #7  0x40379b7d in heap_caps_calloc_base at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/heap/heap_caps.c:487
      (inlined by) heap_caps_calloc at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/heap/heap_caps.c:496
  #8  0x4207ea14 in gdma_new_channel at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/driver/gdma.c:142
  #9  0x4207c643 in alloc_dma_chan at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/driver/spi_common.c:243
  #10 0x4207d482 in spi_bus_initialize at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/driver/spi_common.c:786
  #11 0x420668ca in lgfx::v1::spi::init(int, int, int, int, int) at .pio/libdeps/m5s3demo/M5GFX/src/lgfx/v1/platforms/esp32/common.cpp:604
  #12 0x4212560d in lgfx::v1::Bus_SPI::init() at .pio/libdeps/m5s3demo/M5GFX/src/lgfx/v1/platforms/esp32/Bus_SPI.cpp:175
  #13 0x42121da2 in m5gfx::M5GFX::autodetect(bool, lgfx::boards::board_t) at .pio/libdeps/m5s3demo/M5GFX/src/M5GFX.cpp:1970
  #14 0x42122069 in m5gfx::M5GFX::init_impl(bool, bool) at .pio/libdeps/m5s3demo/M5GFX/src/M5GFX.cpp:685
  #15 0x4204f41f in lgfx::v1::LGFX_Device::init() at .pio/libdeps/m5s3demo/M5GFX/src/lgfx/v1/LGFXBase.hpp:1410
      (inlined by) m5::M5Unified::begin(m5::M5Unified::config_t) at .pio/libdeps/m5s3demo/M5Unified@src-b5271dcd0e6cdc47a62f4d903b9ba433/src/M5Unified.hpp:347
      (inlined by) m5::M5Unified::begin() at .pio/libdeps/m5s3demo/M5Unified@src-b5271dcd0e6cdc47a62f4d903b9ba433/src/M5Unified.hpp:326
      (inlined by) setup() at src/main.cpp:592
  #16 0x42077e7a in loopTask(void*) at /Users/dave/.platformio/packages/framework-arduinoespressif32/cores/esp32/main.cpp:42

Other Steps to Reproduce

No response

I have checked existing issues, online documentation and the Troubleshooting Guide

  • I confirm I have checked existing issues, online documentation and Troubleshooting guide.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions