コンテンツにスキップ
ESP32シリーズの性能を比較してみる (M5Stack Core2 vs CoreS3 - SRAM容量編)

ESP32シリーズの性能を比較してみる (M5Stack Core2 vs CoreS3 - SRAM容量編)

こんにちは。ハードウェアチームの今井 @lovyan03 です。

前回の記事 ESP32シリーズの性能を比較してみる (M5Stack Core2 vs CoreS3 - PSRAMアクセス性能編) の続編となります。 前回の記事から、約一年もお待たせしましたことをお詫び申し上げます…。

さて、今回は M5Stack の SRAM の仕様について比較して行こうと思います。まずスペック表に記載されている SRAM の容量を比較すると、以下のようになっています。

Core2 CoreS3
SoC ESP32-D0WDQ6-V3 ESP32-S3
SRAM 520 KB 512 KB

この値は搭載されている ESP32 と ESP32-S3 それぞれの SRAM 容量の公称値の通りですが、こうして比較表として見ると、 『CoreS3 の方が後継モデルなのに SRAM は増えないどころか 8 KByte 減っているのか…』と読み取られることと思います。

ハードウェア仕様としてはこの容量の通りで間違いではありません。

しかし、初めて ESP32 シリーズを使う方にとっては、以下のような疑問が生じることかと思います。
  • 『後継モデルの方が SRAM の容量が減っているのは何故ですか?』
  • 『この 500 KB 以上のSRAM はすべてユーザーが自由に使えるのですか?』

そこで今回は、こうした疑問に回答するべく、ESP32 と ESP32-S3 の SRAM の容量に着目して、M5Stack Core2 と CoreS3 を用いて検証・比較してみようと思います。

まずは気になる検証結果から

実際にユーザープログラムからどれだけ SRAM 容量を確保できるか、検証を行いました。
確保可能な最大容量を確保する処理を繰り返すプログラムを実行し、実際に確保できた容量をまとめた結果が以下のグラフです。

確保できた領域の合計容量の比較グラフ
確保できた領域それぞれの容量の比較グラフ
※ 以降、容量の単位として 1024 Byte = 1 KiB を使用
※ PSRAM は無効、フレームワークの各種オプションは初期値を使用

使用したプログラムは本記事の最後にありますので、ご興味がある方はご確認ください。
処理内容としては、 heap_caps_get_largest_free_block 関数を用いて、確保できる容量を調べ、 heap_caps_malloc 関数を用いて実際に確保します。メモリが断片化している可能性を考慮して、確保できなくなるまで、この処理をループで繰り返しました。

1番目のグラフは、データ領域として確保できた容量の総合計の値になります。
2番目のグラフは、その内訳になります。heap_caps_malloc 関数で実際に確保できた容量の上位5件を積み上げたグラフになります。

今回の検証結果は以下のようになっていることがわかります。
  • 確保できた合計容量 : ESP32-S3 の方が 80 KiB ほど多い。
  • 確保できた最大容量 : ESP32-S3 の方が倍程度大きい。
確保できた合計容量、確保できた最大容量、いずれも ESP32 よりも ESP32-S3 の方が大きい結果となりました。

ESP32 と ESP32-S3 に大きな違いが生じる理由について

さて、スペックの比較表では ESP32-S3 の方が SRAM の容量が減っているにもかかわらず、どうしてこのような結果になったのでしょうか。
ざっくり簡単に述べると『後継モデルの ESP32-S3 では SRAM に関する構造が変更され、効率よく利用できるようになった』ということになります。

ESP32 と ESP32-S3 の SRAM の構造について

Espressif 社が公開している ESP32 シリーズの TRM (テクニカル・リファレンス・マニュアル) を確認すると、 ESP32 および ESP32-S3 の SRAM は、ひとつの連続したメモリではなく、以下のように 3つのブロックに分かれており、 各ブロックで利用用途が異なっていることが説明されています。

ESP32 ESP32-S3 利用用途
SRAM 0 192 KiB 32 KiB プログラム実行用
SRAM 1 128 KiB 416 KiB プログラム実行/データ保持
SRAM 2 200 KiB 64 KiB データ保持用
合計容量 520 KiB 512 KiB

SRAMのブロックごとの容量

ESP32 と比べて ESP32-S3 は SRAM 0 と SRAM 2 が減り、 SRAM 1 が大幅に増えたことがわかります。

データ保持に利用できる SRAM の合計容量

今回の検証で確保している SRAM は、データ保持用の SRAM が該当します。
SRAM 0 はデータ保持に利用できませんから、 SRAM 1 と SRAM 2 から確保できた容量を比較している、ということです。 したがって、データ保持に利用できる SRAM の合計容量は以下の通りとなります。
ESP32 ESP32-S3
SRAM 1 128 KiB 416 KiB
SRAM 2 200 KiB 64 KiB
合計容量 328 KiB 480 KiB

連続領域として確保できる SRAM の最大容量

SRAM 1 と SRAM 2 を、ブロックを跨いで一度に確保することはできません。したがって、連続領域として確保できる SRAM の最大容量は以下のようになります。

ESP32 ESP32-S3
最大のSRAMブロック 200 KiB
(SRAM 2)
416 KiB
(SRAM 1)

このように、ESP32 と ESP32-S3 とで SRAM の構造が異なっていることが、検証結果に大きな差が生じた一番の理由となります。

実際に利用できる容量が更に減少している理由について

さて、 SRAM のブロックのうち、SRAM 0 を除いた SRAM 1 + SRAM 2 の合計容量がデータ保持に利用できる最大容量となると説明しました。
しかし、先ほど説明で述べた容量と、検証で実際に確保できた容量との間には、以下のように大きな差があります。

ESP32 ESP32-S3
データ保持に利用できる
SRAM の合計容量
328 KiB 480 KiB
検証結果の合計容量
Arduino v2.0.17
281.34 KiB (-46.66 KiB) 369.80 KiB (-110.20 KiB)
検証結果の合計容量
ESP-IDF v5.4.0
297.92 KiB (-30.08 KiB) 373.60 KiB (-106.40 KiB)

また、こうして比較すると、 ESP32 よりも ESP32-S3 の方が減少量が大きいことがわかります。
この差はどのようにして生じたのでしょうか。

理由としては以下のように、 ESP32 では SRAM 0 を割り当てていた用途が、 ESP32-S3 では SRAM 1 および SRAM 2 を割り当てているために、確保できるメモリ容量の減少量に差が生じている、ということです。
ESP32 ESP32-S3
外部メモリのデータ領域用の
キャッシュ
SRAM 0 SRAM 2
フレームワークが使用する
プログラム実行用メモリ
SRAM 0 SRAM 1
以下でもう少し詳しく説明していきます。

外部メモリ用キャッシュの仕様の相違について


ESP32 シリーズは フラッシュROM や PSRAM を用いることで、数メガバイト規模のデータを扱うことができます。これらは CPU コアの視点では SPI バスの先に繋がった外部メモリという位置づけであり、SRAM のように即時アクセスはできません。一旦 SPI 通信を経由して SRAM 上にデータを展開する領域が必要になります。この領域が外部メモリ用のキャッシュです。

このキャッシュの仕様が、ESP32 と ESP32-S3 とで以下のように異なっているのです。
ESP32 の場合
  • SRAM 0 から 64 KiB が外部メモリ用のキャッシュとして確保される。
  • このキャッシュはプログラム領域用としても、データ領域用としても利用される。
ESP32-S3 の場合
  • SRAM 0 から 16 KiB がプログラム領域用のキャッシュとして確保される。
  • SRAM 2 から 32 KiB がデータ領域用のキャッシュとして確保される。
※ ESP-IDF の menuconfig にて多少調整することができます。

上記のように、 ESP32 は SRAM 0 のみから確保しているのに対し、 ESP32-S3 は SRAM 2 からも 32 KiB を確保しているため、 ユーザープログラムが確保できる容量が 32 KiB 減少します。

フレームワークやコンポーネントが使用するメモリについて

確保できる容量が減少している別の理由としては、FreeRTOS・ESP-IDF・Arduino の各種フレームワーク及びコンポーネントの動作のためにメモリが確保されていることが挙げられます。
Arduino v2.0.17 と ESP-IDF v5.4 での検証結果に差異が生じている理由でもあります。

これにはデータ保持用の SRAM だけでなく、プログラム実行用の SRAM に配置されるものも多数含まれており、 ESP32 では SRAM 0 が使用されますが、ESP32-S3 では SRAM 1 が使用されます。 このためユーザープログラムが確保できる容量が減少します。

この部分をさらに掘り下げるには、フレームワークのバージョンの相違や コンポーネントの有効・無効の状態の差異など、条件を変えて多岐にわたる検証を行う必要が生じるため、 今回の記事では掘り下げませんが、 例えば WiFi や Bluetooth などの高機能のコンポーネントを有効化した場合は、 かなり多く (数十 KiB 単位) の SRAM が使用されます。

まとめ


以上、 ESP32 と ESP32-S3 の SRAM の容量について検証を行いました。

このように、ユーザーが利用できる SRAM の容量は、ハードウェアのスペックの比較表だけでは判断できない理由が見えてきたと思います。

特に、連続した領域として確保できる容量が ESP32-S3 で大きく改善していることは重要なポイントです。

例えば、『 M5Stack の 320 × 240 ピクセルの LCD に表示する画像を SRAM 上に作成する 』 という使用方法を想定してみると、 16bit カラー画像ならば 150 KiB の容量が必要になります。 ESP32-S3 ならば充分に確保できますが、 ESP32 では条件がかなり厳しく、簡単には確保できないはずです。

今回の検証結果が皆様の製品選択の一助となれば幸いです。

最後に、検証に使用したプログラムを以下に示します。

bench_002.cpp

#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <sdkconfig.h>
#include <esp_heap_caps.h>

#if __has_include(<esp_arduino_version.h>)
    #include <esp_arduino_version.h>
#endif


struct memory_map_info_t {
    uintptr_t start;
    uintptr_t end;
    const char* name;
};

#if defined ( CONFIG_IDF_TARGET_ESP32 )
static constexpr const memory_map_info_t memmap[] =
{
    { 0x00000000, 0x3F3FFFFF, "Data:Reserved" },
    { 0x3F400000, 0x3F7FFFFF, "Data:External Flash" },
    { 0x3F800000, 0x3FBFFFFF, "Data:External SRAM" },
    { 0x3FC00000, 0x3FEFFFFF, "Data:Reserved" },
    { 0x3FF00000, 0x3FF7FFFF, "Data:Peripheral" },
    { 0x3FF80000, 0x3FF81FFF, "Data:RTC FAST Memory (PRO_CPU Only)" },
    { 0x3FF82000, 0x3FF8FFFF, "Data:Reserved" },
    { 0x3FF90000, 0x3FF9FFFF, "Data:Internal ROM1" },
    { 0x3FFA0000, 0x3FFADFFF, "Data:Reserved" },
    { 0x3FFAE000, 0x3FFDFFFF, "Data:Internal SRAM2 (DMA)" },
    { 0x3FFE0000, 0x3FFFFFFF, "Data:Internal SRAM1 (DMA)" },
    { 0x40000000, 0x40007FFF, "Inst:Internal ROM0 (Remap)" },
    { 0x40008000, 0x4005FFFF, "Inst:Internal ROM0" },
    { 0x40060000, 0x4006FFFF, "Inst:Reserved" },
    { 0x40070000, 0x4007FFFF, "Inst:Internal SRAM0 (Cache)" },
    { 0x40080000, 0x4009FFFF, "Inst:Internal SRAM0" },
    { 0x400A0000, 0x400AFFFF, "Inst:Internal SRAM1" },
    { 0x400B0000, 0x400B7FFF, "Inst:Internal SRAM1 (Remap)" },
    { 0x400B8000, 0x400BFFFF, "Inst:Internal SRAM1" },
    { 0x400C0000, 0x400C1FFF, "Inst:RTC FAST Memory (PRO_CPU Only)" },
    { 0x400C2000, 0x40BFFFFF, "Inst:External Flash" },
    { 0x40C00000, 0x4FFFFFFF, "Inst:Reserved" },
    { 0x50000000, 0x50001FFF, "D/I :RTC SLOW Memory" },
    { 0x50002000, 0xFFFFFFFF, "    :Reserved" },
};
#elif defined ( CONFIG_IDF_TARGET_ESP32S3 )
static constexpr const memory_map_info_t memmap[] =
{
    { 0x00000000, 0x3BFFFFFF, "Data:Reserved" },
    { 0x3C000000, 0x3DFFFFFF, "Data:External Memory" },
    { 0x3E000000, 0x3FC87FFF, "Data:Reserved" },
    { 0x3FC88000, 0x3FCEFFFF, "Data:Internal SRAM1 (DMA)" },
    { 0x3FCF0000, 0x3FCFFFFF, "Data:Internal SRAM2 (DMA)" },
    { 0x3FD00000, 0x3FEFFFFF, "Data:Reserved" },
    { 0x3FF00000, 0x3FF1FFFF, "Data:Internal ROM1" },
    { 0x3FF20000, 0x3FFFFFFF, "Data:Reserved" },
    { 0x40000000, 0x4003FFFF, "Inst:Internal ROM0" },
    { 0x40040000, 0x4005FFFF, "Inst:Internal ROM1" },
    { 0x40060000, 0x4036FFFF, "Inst:Reserved" },
    { 0x40370000, 0x40377FFF, "Inst:Internal SRAM0" },
    { 0x40378000, 0x403DFFFF, "Inst:Internal SRAM1" },
    { 0x403E0000, 0x41FFFFFF, "Inst:Reserved" },
    { 0x42000000, 0x43FFFFFF, "Inst:External Memory" },
    { 0x44000000, 0x4FFFFFFF, "Inst:Reserved" },
    { 0x50000000, 0x50001FFF, "D/I :RTC SLOW Memory" },
    { 0x50002000, 0x5FFFFFFF, "    :Reserved" },
    { 0x60000000, 0x600D0FFF, "    :Periperals" },
    { 0x600D1000, 0x600FDFFF, "    :Reserved" },
    { 0x600FE000, 0x600FFFFF, "D/I :RTC FAST Memory" },
    { 0x60100000, 0xFFFFFFFF, "    :Reserved" },
};
#else
#error "Unsupported target"
#endif

const char* get_name_by_addr(const void* ptr)
{
    auto addr = (uintptr_t)ptr;
    for (const auto& m : memmap)
    {
    if (m.start <= addr && addr <= m.end)
    {
        return m.name;
    }
    }
    return "Unknown";
}

void dump(const void* ptr)
{
    auto data = (const uint32_t*)ptr;
    for (int i = 0; i < 16; ++i) {
    printf("%08x:", ((uintptr_t)data) + i * 16);
    for (int j = 0; j < 4; ++j) {
        printf(" %08lx", data[j + i*4]);
    }
    printf("\n");
    }
}

void memory_test(uint32_t caps)
{
    printf("start,end,size,size(KiB),name\n");
    unsigned int total = 0;
    for (;;) {
    // 確保できる最大のメモリサイズを取得
    auto size = heap_caps_get_largest_free_block(caps);
    if (size == 0) { break; }
    // 実際にメモリを確保
    auto pointer = (uint8_t*)heap_caps_malloc(size, caps);
    if (pointer == nullptr) { break; }
    total += size;

    // 確保したメモリの情報をシリアルモニタに出力
    auto address = (uintptr_t)pointer;
    printf("%08x,%08x,%7u,%7.2f,%s\n", address, address + size, size, size / 1024.0f, get_name_by_addr(pointer));
    }
    printf("        ,  TOTAL ,%7u,%7.2f,\n", total, total / 1024.0f);
}

void setup(void)
{
#if defined ( CONFIG_IDF_TARGET_ESP32 )
    printf("CONFIG_IDF_TARGET_ESP32\n");
#elif defined ( CONFIG_IDF_TARGET_ESP32S3 )
    printf("CONFIG_IDF_TARGET_ESP32S3\n");
#endif

    printf("ESP-IDF VERSION: %d.%d.%d\n", ESP_IDF_VERSION_MAJOR, ESP_IDF_VERSION_MINOR, ESP_IDF_VERSION_PATCH);
#if defined ( ESP_ARDUINO_VERSION_MAJOR )
    printf("ARDUINO VERSION: %d.%d.%d\n", ESP_ARDUINO_VERSION_MAJOR, ESP_ARDUINO_VERSION_MINOR, ESP_ARDUINO_VERSION_PATCH);
#endif

    // MALLOC_CAP_DMA を指定することで、DMAアクセスが可能なデータ領域用の SRAM を確保する。
    printf("MALLOC_CAP_DMA:\n");
    memory_test(MALLOC_CAP_DMA);
    // ここまでで DMAアクセス可能なメモリは全て確保済です。
    // 本文にある検証の内容はここまでです。

    // 以下は興味のある方むけの検証コードです。

    // MALLOC_CAP_8BIT を指定することで、DMA不可かつ8ビットアクセス可能なメモリを確保する。
    // (ESP32-S3 の RTC FAST MEMORY が確保可能)
    // このメモリはアクセス速度が遅いので速度を求める用途には不向きです。
    printf("MALLOC_CAP_8BIT:\n");
    memory_test(MALLOC_CAP_8BIT);

    // MALLOC_CAP_32BIT を指定することで、DMA不可かつ32ビットアクセスのメモリを確保する。
    // (ESP32 の SRAM 0 から確保可能)
    // このメモリを読書きするには 4 Byte 単位でアクセスする必要があります。
    // 具体的には uint32_t の配列に利用することは可能ですが、 uint8_t や int16_t の配列に使用するとクラッシュします。
    printf("MALLOC_CAP_32BIT:\n");
    memory_test(MALLOC_CAP_32BIT);
}
    
void loop(void)
{
    vTaskDelay(1);
}

extern "C" {
    // weak属性を付与することで、ArduinoESP32では無効化し、ESP-IDFの場合は有効化する。
    __attribute__((weak))
    void app_main()
    {
    setup();
      for (;;) {
        loop();
      }
    }
}

前の記事 Seeed SenseCAP Watcherを使ってみよう
次の記事 [新商品]ESP32-C3-WROOM-02ピッチ変換用基板3種ほか5点