B4R Code Snippet esp32 PSRAM

I was tried to think how to correctly use PSRAM of the esp32-S3 MCU - and prepared these functions, but ... actually i do not understand how to use it correctly in B4R that to make internal main SRAM more free.
Help to understand me, please, if you are interested.

Main:
#Region Project Attributes
    #AutoFlushLogs: True
    #CheckArrayBounds: True
    #StackBufferSize: 3000
#End Region
Sub Process_Globals
    ' These variables are accessible across all modules
    Public Serial1 As Serial
    Private Timer1 As Timer
    Private TestIndex As Int = 0
    Private MaxTests As Int = 1000 ' Number of test entries
    Private bc As ByteConverter
    Private TotalSavedBytes As ULong
 
End Sub

Private Sub AppStart
    Serial1.Initialize(115200)
    Delay(3000)
    Log("AppStart")
 
    If esp_psram.Initialize Then
        Log("PSRAM initialized. Free: ", esp_psram.GetFreePSRAM, " bytes")
        Log("Total PSRAM: ", esp_psram.GetTotalPSRAM, " bytes")
     
        ' Start test with delay to avoid interfering logs
        Timer1.Initialize("Timer1_Tick", 2) '2 ms
        Timer1.Enabled = True
     
    Else
        Log("PSRAM initialization failed or PSRAM not available")
        Return
    End If
 
End Sub

Sub Timer1_Tick
    Timer1.Enabled = False
 
    If TestIndex = 0 Then
        Log("=== Starting test: writing ", MaxTests, " strings to PSRAM ===")
        TotalSavedBytes = 0
    End If
 
    ' Create a large string (~200 bytes)
    Dim Key As String = JoinStrings(Array As String("key_", TestIndex))
    Dim Value As String = getRNDValue
 
 
    ' Write to PSRAM
    If esp_psram.SaveStringToPSRAM(Key, Value) Then
        ' Read back for verification (optional, can be commented out for speed)
        'Log("Wrote ", Key)

        TotalSavedBytes = TotalSavedBytes + Value.Length + Key.Length
        'Dim ReadBack As String = esp_psram.ReadStringFromPSRAM(Key)
        'Log(ReadBack)
        'If ReadBack <> Value Then Log("Read error for key: ", Key)
    Else
        Log("Write error for key: ", Key)
     
    End If
 
    TestIndex = TestIndex + 1
 
    If TestIndex < MaxTests Then
        ' Continue with next tick
        Timer1.Enabled = True
    Else
        Log("Saved ", TotalSavedBytes, " bytes")
        Log("=== Writing completed. Verification and deletion ===")
        Log("Free after writing: ", esp_psram.GetFreePSRAM, " bytes")
     
        ' Verification: read first, middle and last
        Log("key_0: ", esp_psram.ReadStringFromPSRAM("key_0"))
        'Log("key_99: ", esp_psram.ReadStringFromPSRAM("key_99"))
        Log("key_500: ", esp_psram.ReadStringFromPSRAM("key_500"))
        Log("key_999: ", esp_psram.ReadStringFromPSRAM("key_999"))
     
        ' Delete
        Log("Next - deleting all...")
        esp_psram.ClearAllStrings
     
        Log("Free after deletion: ", esp_psram.GetFreePSRAM, " bytes")
     
        ' Final check
        Log("=== Test completed ===")
        Delay(1000)
        Log("Again")
        TestIndex = 0
        Timer1.Enabled = True
    End If
End Sub

Sub getRNDValue As String
    Return JoinStrings(Array As String("TestData_", TestIndex, "_", CreateRandomString(180)))
End Sub

Sub getKeyName(num As Int) As String
    Return JoinStrings(Array As String("key_", num))
End Sub

' Helper function: generate random string
Sub CreateRandomString(Length As Int) As String
    Dim Chars As String = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
    Dim CharsBytes() As Byte = Chars.GetBytes
    Dim Result(Length) As Byte

    For i = 0 To Length - 1
        Dim SingleByte As Byte
        SingleByte = CharsBytes(Rnd(0, CharsBytes.Length))
        Result(i) = SingleByte
    Next
 
    Return bc.StringFromBytes(Result)
End Sub


module name: esp_psram:
'module name: esp_psram
'PSRAM Storage Module for ESP32/ESP32-S3
'v.0.207
'Activate PSRAM in the configuration

Sub Process_Globals
    Private bc As ByteConverter
    Public Const PSRAM_PAGE_SIZE As Int = 256
    Public PSRAM_Available As Boolean = False
    Public StringFromPSRAM(PSRAM_PAGE_SIZE) As Byte    'ignore
    Private Const MAX_KEY_LENGTH As Int = 31
 
    'Define a type for key-value pairs
    Type KeyValueType(key() As Byte, value() As Byte)
End Sub

'Checks if board has PSRAM hardware
Public Sub IsPSRAMSupported As Boolean
    Return RunNative("isPsramSupported", Null)
End Sub

'Initializes PSRAM subsystem
Public Sub Initialize As Boolean
    If Not(IsPSRAMSupported) Then
        Log("PSRAM not supported on this board")
        Return False
    End If
 
    PSRAM_Available = RunNative("psramFound", Null)
    Log("PSRAM Available: ", PSRAM_Available)
 
    If PSRAM_Available Then
        RunNative("psram_internal_initialize", Null)
        Log("PSRAM Initialized. Free: ", GetFreePSRAM, " of ", GetTotalPSRAM)
    End If
    Return PSRAM_Available
End Sub

'Saves string to PSRAM using KeyValueType structure
Public Sub SaveStringToPSRAM(Key As String, Value As String) As Boolean
    If Not(PSRAM_Available) Then Return False
    If Key.Length > MAX_KEY_LENGTH Then
        Log("Key too long (max ", MAX_KEY_LENGTH, " chars)")
        Return False
    End If
 
    Dim kv As KeyValueType
    kv.key = Key.GetBytes
    kv.value = Value.GetBytes
 
    Return RunNative("psramSaveString", kv)
End Sub

'Reads string from PSRAM
Public Sub ReadStringFromPSRAM(Key As String) As String
    If Not(PSRAM_Available) Then Return ""
    For i = 0 To PSRAM_PAGE_SIZE - 1
        StringFromPSRAM(i) = 0
    Next
    RunNative("psramReadString", Key.GetBytes)
    Return bc.StringFromBytes(StringFromPSRAM)
End Sub

'Deletes string from PSRAM
Public Sub DeleteStringFromPSRAM(Key As String) As Boolean
    If Not(PSRAM_Available) Then Return False
    Return RunNative("psramDeleteString", Key.GetBytes).As(Boolean)
End Sub

'Gets free PSRAM in bytes
Public Sub GetFreePSRAM() As ULong
    If Not(PSRAM_Available) Then Return 0
    Return RunNative("esp_get_free_psram_size", Null)
End Sub

'Gets total PSRAM in bytes
Public Sub GetTotalPSRAM() As ULong
    If Not(PSRAM_Available) Then Return 0
    Return RunNative("esp_get_psram_size", Null)
End Sub

'Очищает ВСЕ сохранённые строки в PSRAM
Public Sub ClearAllStrings As Boolean
    If Not(PSRAM_Available) Then Return False
    Return RunNative("psramClearAll", Null).As(Boolean)
End Sub

#if C
#include <string.h>
#include <stdlib.h>
#include <Arduino.h>
#include <esp_heap_caps.h>
#include "rCore.h"

typedef struct {
    char key[32];
    size_t length;
    void* ptr;
} PSRAM_StringEntry;

static PSRAM_StringEntry* stringTable = NULL;
static uint16_t stringTableSize = 0;
static uint16_t stringTableCapacity = 0;

static int findStringIndex(const char* key) {
    for (int i = 0; i < stringTableSize; i++) {
        if (strcmp(stringTable[i].key, key) == 0) {
            return i;
        }
    }
    return -1;
}

// Helper: wrap result into B4R::Object*
B4R::Object* wrapBoolean(bool value) {
    return (new B4R::Object())->wrapNumber((byte)value);
}

B4R::Object* wrapULong(ULong value) {
    return (new B4R::Object())->wrapNumber(value);
}

B4R::Object* isPsramSupported(B4R::Object* arg) {
#ifdef BOARD_HAS_PSRAM
    return wrapBoolean(true);
#endif
    return wrapBoolean(false);

}

B4R::Object* psramFound(B4R::Object* arg) {
#ifdef BOARD_HAS_PSRAM
    return wrapBoolean(ESP.getPsramSize() > 0);
#endif
    return wrapBoolean(false);

}

B4R::Object* psram_internal_initialize(B4R::Object* arg) {
    stringTableCapacity = 16;
    stringTable = (PSRAM_StringEntry*)malloc(stringTableCapacity * sizeof(PSRAM_StringEntry));
    if (stringTable == NULL) return wrapBoolean(false);
    memset(stringTable, 0, stringTableCapacity * sizeof(PSRAM_StringEntry));
    stringTableSize = 0;
    return wrapBoolean(true);
}

B4R::Object* psramSaveString(B4R::Object* o) {
    static unsigned long lastSaveTime = 0;
    const unsigned long minSaveInterval = 5;
 
    _keyvaluetype* kv = (_keyvaluetype*)B4R::Object::toPointer(o);
    //if (!kv || !psramFound(NULL)->toBoolean()) return wrapBoolean(false);
    if (!kv) return wrapBoolean(false);

    B4R::Array* keyArr = kv->key;
    B4R::Array* valueArr = kv->value;
    if (!keyArr || !valueArr) return wrapBoolean(false);

    char* key = (char*)keyArr->data;
    char* value = (char*)valueArr->data;
    size_t valueLen = strlen(value) + 1;
 
    unsigned long currentTime = millis();
    if (currentTime - lastSaveTime < minSaveInterval) {
        return wrapBoolean(false); // too often
    }

    if (heap_caps_get_free_size(MALLOC_CAP_SPIRAM) < valueLen) return wrapBoolean(false);

    int index = findStringIndex(key);
    if (index >= 0) {
        heap_caps_free(stringTable[index].ptr);
    } else {
        if (stringTableSize >= stringTableCapacity) {
            uint16_t newCapacity = stringTableCapacity * 2;
            PSRAM_StringEntry* newTable = (PSRAM_StringEntry*)realloc(stringTable,
                                newCapacity * sizeof(PSRAM_StringEntry));
            if (newTable == NULL) return wrapBoolean(false);
            stringTable = newTable;
            stringTableCapacity = newCapacity;
        }
        index = stringTableSize++;
    }

    void* ptr = heap_caps_malloc(valueLen, MALLOC_CAP_SPIRAM);
    if (ptr == NULL) return wrapBoolean(false);
    memcpy(ptr, value, valueLen);
    strncpy(stringTable[index].key, key, 31);
    stringTable[index].key[31] = '\0';
    stringTable[index].length = valueLen;
    stringTable[index].ptr = ptr;

    lastSaveTime = currentTime;
    return wrapBoolean(true);
}

void psramReadString(B4R::Object* o) {
    B4R::Array* b = (B4R::Array*)B4R::Object::toPointer(o);
    const char* key = (const char*)b->data;
    int index = findStringIndex(key);

    // Вalways zeroing
    if (b4r_esp_psram::_stringfrompsram && b4r_esp_psram::_stringfrompsram->data) {
        memset(b4r_esp_psram::_stringfrompsram->data, 0, b4r_esp_psram::_stringfrompsram->length);
    }

    if (index < 0) return;

    // !!!
    if (stringTable[index].ptr == NULL) return;

    char* valuePtr = (char*)stringTable[index].ptr;
    size_t length = stringTable[index].length;
    size_t copyLength = length < b4r_esp_psram::_stringfrompsram->length ? length : b4r_esp_psram::_stringfrompsram->length;
    memcpy(b4r_esp_psram::_stringfrompsram->data, valuePtr, copyLength);
    if (copyLength < b4r_esp_psram::_stringfrompsram->length)
        ((uint8_t*)b4r_esp_psram::_stringfrompsram->data)[copyLength] = 0;
}

B4R::Object* psramDeleteString(B4R::Object* o) {
    B4R::Array* b = (B4R::Array*)B4R::Object::toPointer(o);
    const char* key = (const char*)b->data;
    if (!key) return wrapBoolean(false);

    int index = findStringIndex(key);
    if (index < 0) return wrapBoolean(false);

    // Free and zeroing
    if (stringTable[index].ptr != NULL) {
        heap_caps_free(stringTable[index].ptr);
        stringTable[index].ptr = NULL;
    }

    for (int i = index; i < stringTableSize - 1; i++) {
        stringTable[i] = stringTable[i + 1];
    }
    stringTableSize--;

    return wrapBoolean(true);
}

B4R::Object* esp_get_free_psram_size(B4R::Object* arg) {
#ifdef BOARD_HAS_PSRAM
    return wrapULong(heap_caps_get_free_size(MALLOC_CAP_SPIRAM));
#endif
    return wrapULong(0);

}

B4R::Object* esp_get_psram_size(B4R::Object* arg) {
#ifdef BOARD_HAS_PSRAM
    return wrapULong(ESP.getPsramSize());
#endif
    return wrapULong(0);

}

B4R::Object* psramClearAll(B4R::Object* arg) {
    // Free all memory
    for (int i = 0; i < stringTableSize; i++) {
        if (stringTable[i].ptr != NULL) {
            heap_caps_free(stringTable[i].ptr);
            stringTable[i].ptr = NULL;
        }
    }
 
    stringTableSize = 0;
    // stringTable is not cleared, add again later
    return wrapBoolean(true);
}

#end if

AppStart
PSRAM Available: 1
PSRAM Initialized. Free: 8386096 of 8388608
PSRAM initialized. Free: 8386096 bytes
Total PSRAM: 8388608 bytes
=== Starting test: writing 1000 strings to PSRAM ===
Saved 199780 bytes
=== Writing completed. Verification and deletion ===
Free after writing: 8132140 bytes
key_0: TestData_0_vqna12cuFh..............
key_500: TestData_500_xauzUtiNqAs7Zh.........
key_999: TestData_999_5gDxpV3ATBmbD....
Next - deleting all...
Free after deletion: 8344108 bytes
=== Test completed ===
Again
=== Starting test: writing 1000 strings to PSRAM ===
Saved 199780 bytes
=== Writing completed. Verification and deletion ===
Free after writing: 8132148 bytes
key_0: TestData_0_vA3dIExB6s...........
key_500: TestData_500_0mv5HUpsjAa2mkUbQw...........
key_999: TestData_999_WmscRMYuJW............
Next - deleting all...
Free after deletion: 8344108 bytes
=== Test completed ===

If PSRAM is not enabled:

AppStart
PSRAM not supported on this board
PSRAM initialization failed or PSRAM not available
 

Attachments

  • esp_psram_0.207.zip
    4.5 KB · Views: 26
Last edited:

peacemaker

Expert
Licensed User
Longtime User
As i can understand - PSRAM can be useful only if to have functions to use JoinStrings on PSRAM, and somehow to use the stored strings later... But how if main SRAM is ... limited...

BTW, "OPI PSRAM" (octal SPI, 8-bit data bus) is just 25% slower than regular internal SRAM. But simple QSPI PSRAM (regular 4-wire SPI interface) is ... 10 times more slow than SRAM !
 

peacemaker

Expert
Licensed User
Longtime User
Let me save these functions here, for anyone, maybe will be useful, i used them for generating JSON test in PSRAM memory.
But finally it's more simple to use my module https://www.b4x.com/android/forum/threads/167557/#content to generate JSON on-the-fly and send by the chunked-POST: tested 3-5 KB JSON is sent OK.
So, here it's just example.

B4X:
Sub Process_Globals
    'These global variables will be declared once when the application starts.
    'Public variables can be accessed from all modules.
    Dim result() As Byte    'ignore
    Dim psram_json_size As Int      ' full generated JSON size, bytes
    Dim psramChunk(512) As Byte     ' SRAM buffer for single chunk (check with HttpJob.chunkLength)
    Dim psramChunkSize As Int       ' how many copied
    Dim psram_read_start As Int     ' start index
    Dim psram_read_len As Int       'length to be copied
End sub

'Reading JSON chunk from PSRAM to psramChunk
Public Sub ReadFromPSRAM(startIndex As Int, length As Int) As Int
    psram_read_start = startIndex
    psram_read_len = length
    Return RunNative("read_from_psram", Null)
End Sub

Sub PrepareJSONinPSRAM
    ' ==== Generating JSON inside PSRAM ====
    RunNative("prepare_json_psram", Null)
End Sub



#IF C
#ifdef ESP32
#include <Arduino.h>
#include <malloc.h>
#include <stdlib.h>
#include <stdio.h>
#endif

static char* json_psram = nullptr;
uint32_t psram_json_size = 0;

void prepare_json_psram(B4R::Object* o) {
    if (json_psram) {
        free(json_psram);
        json_psram = nullptr;
    }

    size_t buf_size = 16384;
    json_psram = (char*)ps_malloc(buf_size);
    if (!json_psram) {
        Serial.println("PSRAM alloc failed for JSON");
        if (b4r_esp_vibro_fft::_result) {
            b4r_esp_vibro_fft::_result->length = 0;
            b4r_esp_vibro_fft::_result->data = nullptr;
        }
        return;
    }

    size_t len = 0;
    auto append_raw = [&](const char* s) {
        if (!s) return;
        size_t sl = strlen(s);
        if (len + sl + 1 >= buf_size) return;
        memcpy(json_psram + len, s, sl);
        len += sl;
    };
    auto append_freq_item = [&](float f, float mag) {
        char tmp[64];
        snprintf(tmp, sizeof(tmp), "\"%.0f\":%.6f", (double)f, (double)mag);
        append_raw(tmp);
    };
    auto remove_trailing_comma = [&]() {
        if (len > 0 && json_psram[len - 1] == ',') len--;
    };
    auto append_float = [&](const char* key, float val) {
        char tmp[64];
        snprintf(tmp, sizeof(tmp), ",\"%s\":%.6f", key, (double)val);
        append_raw(tmp);
    };
    auto append_bool = [&](const char* key, bool v) {
        char tmp[64];
        snprintf(tmp, sizeof(tmp), ",\"%s\":%d", key, v ? 1 : 0);
        append_raw(tmp);
    };
    auto append_int = [&](const char* key, long v) {
        char tmp[64];
        snprintf(tmp, sizeof(tmp), ",\"%s\":%ld", key, v);
        append_raw(tmp);
    };

    // === X axis ===
    append_raw("{\"x\":{");
    if (b4r_esp_vibro_fft::_freqx && b4r_esp_vibro_fft::_magx) {
        float* fx = (float*)b4r_esp_vibro_fft::_freqx->data;
        float* mx = (float*)b4r_esp_vibro_fft::_magx->data;
        int n = min(b4r_esp_vibro_fft::_freqx->length, b4r_esp_vibro_fft::_magx->length);
        for (int i = 0; i < n; i++) {
            if (fx[i] > 0.0f && mx[i] != 0.0f) {
                append_freq_item(fx[i], mx[i]);
                append_raw(",");
            }
        }
        remove_trailing_comma();
    }
    append_raw("}");

    // === Y axis ===
    append_raw(",\"y\":{");
    if (b4r_esp_vibro_fft::_freqy && b4r_esp_vibro_fft::_magy) {
        float* fy = (float*)b4r_esp_vibro_fft::_freqy->data;
        float* my = (float*)b4r_esp_vibro_fft::_magy->data;
        int n = min(b4r_esp_vibro_fft::_freqy->length, b4r_esp_vibro_fft::_magy->length);
        for (int i = 0; i < n; i++) {
            if (fy[i] > 0.0f && my[i] != 0.0f) {
                append_freq_item(fy[i], my[i]);
                append_raw(",");
            }
        }
        remove_trailing_comma();
    }
    append_raw("}");

    // === Z axis ===
    append_raw(",\"z\":{");
    if (b4r_esp_vibro_fft::_freqz && b4r_esp_vibro_fft::_magz) {
        float* fz = (float*)b4r_esp_vibro_fft::_freqz->data;
        float* mz = (float*)b4r_esp_vibro_fft::_magz->data;
        int n = min(b4r_esp_vibro_fft::_freqz->length, b4r_esp_vibro_fft::_magz->length);
        for (int i = 0; i < n; i++) {
            if (fz[i] > 0.0f && mz[i] != 0.0f) {
                append_freq_item(fz[i], mz[i]);
                append_raw(",");
            }
        }
        remove_trailing_comma();
    }
    append_raw("}");

    // RMS + temperature
    append_float("rms_x", (float)b4r_esp_vibro_fft::_rmsx);
    append_float("rms_y", (float)b4r_esp_vibro_fft::_rmsy);
    append_float("rms_z", (float)b4r_esp_vibro_fft::_rmsz);
    append_float("temperature", (float)b4r_esp_vibro_fft::_tmp);

    // === All viban_* ===
    append_float("viban_rotationfreq", (float)b4r_esp_vibro_fft::_viban_rotationfreq);
    append_float("viban_rmsx", (float)b4r_esp_vibro_fft::_viban_rmsx);
    append_float("viban_rmsy", (float)b4r_esp_vibro_fft::_viban_rmsy);
    append_float("viban_rmsz", (float)b4r_esp_vibro_fft::_viban_rmsz);
    append_bool("viban_xycorr", b4r_esp_vibro_fft::_viban_xycorr);
    append_bool("viban_xzcorr", b4r_esp_vibro_fft::_viban_xzcorr);
    ....

    append_raw("}"); // Close freq object

    json_psram[len] = '\0';
    if (b4r_esp_vibro_fft::_result) {
        b4r_esp_vibro_fft::_result->data = (uint8_t*)json_psram;
        b4r_esp_vibro_fft::_result->length = (int)len;
        b4r_esp_vibro_fft::_psram_json_size = (int)len;
    }

    Serial.printf("JSON ready in PSRAM, size=%u\n", (unsigned)len);
}



// -----------------------------------------------------------------------------
// read_from_psram
// Reads a chunk from PSRAM into psramChunk (Byte array in B4R)
// Uses parameters _psram_read_start and _psram_read_len from B4R
// -----------------------------------------------------------------------------
B4R::Object* read_from_psram(B4R::Object* o) {
    static B4R::Object ret;
    int start = b4r_esp_vibro_fft::_psram_read_start;
    int length = b4r_esp_vibro_fft::_psram_read_len;

    Serial.printf("[read_from_psram] Chunk read request: start=%d length=%d json_psram=%p total_size=%d\n",
                  start, length, json_psram, b4r_esp_vibro_fft::_psram_json_size);

    // Check parameter validity
    if (!json_psram || start < 0 || length <= 0) {
        b4r_esp_vibro_fft::_psramchunksize = 0;
        Serial.println("[read_from_psram] ERROR: invalid parameters or json_psram not initialized!");
        return ret.wrapNumber((Int)0);
    }

    // If start is beyond data — return 0
    if (start >= b4r_esp_vibro_fft::_psram_json_size) {
        b4r_esp_vibro_fft::_psramchunksize = 0;
        Serial.printf("[read_from_psram] WARNING: start beyond data (start=%d, total=%d)\n",
                      start, b4r_esp_vibro_fft::_psram_json_size);
        return ret.wrapNumber((Int)0);
    }

    // If requested length exceeds bounds — adjust
    if (start + length > b4r_esp_vibro_fft::_psram_json_size) {
        length = b4r_esp_vibro_fft::_psram_json_size - start;
        Serial.printf("[read_from_psram] Adjusting chunk length: new_length=%d\n", length);
    }

    // Copy data from PSRAM to B4R buffer psramChunk
    memcpy(b4r_esp_vibro_fft::_psramchunk->data, json_psram + start, length);
    b4r_esp_vibro_fft::_psramchunksize = length;

    // Log first bytes of chunk
    Serial.printf("[read_from_psram] Chunk successfully copied: size=%d, first 60 chars: %.60s\n",
                  length, b4r_esp_vibro_fft::_psramchunk->data);

    // Return length of read chunk
    return ret.wrapNumber((Int)length);
}


#End If
 
Last edited:
Top