ESP32で外部ライブラリ等でのPSRAM割当制御

概要

ESP32でPSRAMを有効にすると、外部ライブラリなどで意図しないデータがPSRAMに配置されて性能劣化することがあると思います。そのため、なるべく外部ライブラリなどに影響なくPSRAMの割り振りを制御する方法を検証してみました。

PSRAMとは

ESP32のPSRAMとはSPIで接続された外部メモリです。通常のメモリと同じように利用可能ですがSPI接続ですので速度は遅くなります。また、LCDなどのSPI接続しているデバイスにメモリ内容を転送する場合にはPSRAMから一度メインメモリに転送し、メインメモリからLCDに転送するなどの必要があります(処理的には直接転送可能ですが、DMA転送が使えません)。

PSRAMの割当条件

  1. PSRAMが搭載されていること
  2. Arduino IDEにてPSRAMがEnableになっていること
  3. setup()が開始されていること
  4. 一定以上のサイズであること

ぱっと検証したところ、上記が前提条件でした。1は当たり前で、搭載していないESP32ではPSRAMを利用することができません。

2は上記のメニューにあるPSRAMの項目になります。これがDisabledの場合にはPSRAMが利用できません。ただし、搭載していても常に有効になっているボードがあり、無効に設定できない場合もあるようです。

3はちょっとむずかしいのですが、setup()が動く前にボードの初期化処理をしていてその中でPSRAMの有効化も実行されています。なので、グローバルに定義されたクラスのコンストラクタなどは初期化処理が終わる前に呼び出されるのでPSRAMは使われません。

4は手元だと1Kバイトの場合には常にメインメモリから割当られていました。10KバイトだとPSRAMを使っていましたので、小さいすぎるサイズの場合にはメインメモリが使われるようです。

#define CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL 4096

おそらく上記の設定で4096バイトで処理がわかれています。

実験スケッチ

#define PSRAM_LOCK_MAX_COUNT 16

class PSRAMLock {
  private:
    const int maxLockCount = PSRAM_LOCK_MAX_COUNT;
    int lockCount;
    void* _lockList[PSRAM_LOCK_MAX_COUNT];

  public:
    PSRAMLock() {
      lockCount = 0;
      for (int i = 0; i < maxLockCount; i++) {
        _lockList[i] = NULL;
      }
    }

    void Lock(void) {
      for (int i = 0; i < maxLockCount; i++) {
        _lockList[i] = heap_caps_malloc(heap_caps_get_largest_free_block(MALLOC_CAP_SPIRAM), MALLOC_CAP_SPIRAM);
        Serial.printf(" _lockList[i] = %8lX\n", _lockList[i]);
        if (_lockList[i] == NULL) {
          break;
        }
        lockCount++;
      }
    }

    void Unlock(void) {
      for (int i = 0; i < lockCount; i++) {
        free(_lockList[i]);
        _lockList[i] = NULL;
      }
      lockCount = 0;
    }
};

PSRAMLock PsramLock;

void printMem() {
  Serial.printf("heap_caps_get_free_size(MALLOC_CAP_SPIRAM)            : %6d\n", heap_caps_get_free_size(MALLOC_CAP_SPIRAM) );
  Serial.printf("heap_caps_get_free_size(MALLOC_CAP_INTERNAL)          : %6d\n", heap_caps_get_free_size(MALLOC_CAP_INTERNAL) );
  Serial.printf("heap_caps_get_free_size(MALLOC_CAP_DEFAULT)           : %6d\n", heap_caps_get_free_size(MALLOC_CAP_DEFAULT) );
  Serial.printf("heap_caps_get_largest_free_block(MALLOC_CAP_SPIRAM)   : %6d\n", heap_caps_get_largest_free_block(MALLOC_CAP_SPIRAM) );
  Serial.printf("heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL) : %6d\n", heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL) );
  Serial.printf("heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT)  : %6d\n", heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT) );
  Serial.println();
}

void setup() {
  void *p;

  Serial.begin(115200);
  delay(1000);

  Serial.println("setup");
  printMem();

  Serial.println("malloc");
  p = malloc(10000);
  Serial.printf(" p = %8lX\n", p);
  printMem();

  Serial.println("lock");
  PsramLock.Lock();
  printMem();

  Serial.println("malloc");
  p = malloc(10000);
  Serial.printf(" p = %8lX\n", p);
  printMem();

  Serial.println("unlock");
  PsramLock.Unlock();
  printMem();

  Serial.println("malloc");
  p = malloc(10000);
  Serial.printf(" p = %8lX\n", p);
  printMem();

  Serial.println("lock");
  PsramLock.Lock();
  printMem();

  Serial.println("malloc");
  p = malloc(10000);
  Serial.printf(" p = %8lX\n", p);
  printMem();
}

void loop() {
}

PSRAMをロックするクラスを実験的に作ってみました。ロックは単にPSRAMのメモリをすべて確保する処理となっています。

setup
heap_caps_get_free_size(MALLOC_CAP_SPIRAM)            : 4192139
heap_caps_get_free_size(MALLOC_CAP_INTERNAL)          : 339924
heap_caps_get_free_size(MALLOC_CAP_DEFAULT)           : 4463871
heap_caps_get_largest_free_block(MALLOC_CAP_SPIRAM)   : 4128756
heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL) : 118772
heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT)  : 4128756

malloc
 p = 3F800874
heap_caps_get_free_size(MALLOC_CAP_SPIRAM)            : 4182127
heap_caps_get_free_size(MALLOC_CAP_INTERNAL)          : 339924
heap_caps_get_free_size(MALLOC_CAP_DEFAULT)           : 4453859
heap_caps_get_largest_free_block(MALLOC_CAP_SPIRAM)   : 4128756
heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL) : 118772
heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT)  : 4128756

lock
 _lockList[i] = 3F802F94
 _lockList[i] = 3FBF2F98
 _lockList[i] = 3FBFFF9C
 _lockList[i] =        0
heap_caps_get_free_size(MALLOC_CAP_SPIRAM)            :     11
heap_caps_get_free_size(MALLOC_CAP_INTERNAL)          : 339924
heap_caps_get_free_size(MALLOC_CAP_DEFAULT)           : 271743
heap_caps_get_largest_free_block(MALLOC_CAP_SPIRAM)   :      0
heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL) : 118772
heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT)  : 118772

malloc
 p = 3FFB28B8
heap_caps_get_free_size(MALLOC_CAP_SPIRAM)            :     11
heap_caps_get_free_size(MALLOC_CAP_INTERNAL)          : 329912
heap_caps_get_free_size(MALLOC_CAP_DEFAULT)           : 261731
heap_caps_get_largest_free_block(MALLOC_CAP_SPIRAM)   :      0
heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL) : 118772
heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT)  : 118772

unlock
heap_caps_get_free_size(MALLOC_CAP_SPIRAM)            : 4182127
heap_caps_get_free_size(MALLOC_CAP_INTERNAL)          : 329912
heap_caps_get_free_size(MALLOC_CAP_DEFAULT)           : 4443847
heap_caps_get_largest_free_block(MALLOC_CAP_SPIRAM)   : 4128756
heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL) : 118772
heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT)  : 4128756

malloc
 p = 3F802F94
heap_caps_get_free_size(MALLOC_CAP_SPIRAM)            : 4172115
heap_caps_get_free_size(MALLOC_CAP_INTERNAL)          : 329912
heap_caps_get_free_size(MALLOC_CAP_DEFAULT)           : 4433835
heap_caps_get_largest_free_block(MALLOC_CAP_SPIRAM)   : 4128756
heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL) : 118772
heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT)  : 4128756

lock
 _lockList[i] = 3F8056B4
 _lockList[i] = 3FBF56B8
 _lockList[i] = 3FBFFEBC
 _lockList[i] =        0
heap_caps_get_free_size(MALLOC_CAP_SPIRAM)            :     15
heap_caps_get_free_size(MALLOC_CAP_INTERNAL)          : 329912
heap_caps_get_free_size(MALLOC_CAP_DEFAULT)           : 261735
heap_caps_get_largest_free_block(MALLOC_CAP_SPIRAM)   :      0
heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL) : 118772
heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT)  : 118772

malloc
 p = 3FFC2980
heap_caps_get_free_size(MALLOC_CAP_SPIRAM)            :     15
heap_caps_get_free_size(MALLOC_CAP_INTERNAL)          : 319900
heap_caps_get_free_size(MALLOC_CAP_DEFAULT)           : 251723
heap_caps_get_largest_free_block(MALLOC_CAP_SPIRAM)   :      0
heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL) : 110580
heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT)  : 110580

実行結果は上記です。デフォルトの状態ではPSRAMからmallocされていますが、Lockするとメインメモリからmallocされています。

        _lockList[i] = heap_caps_malloc(heap_caps_get_largest_free_block(MALLOC_CAP_SPIRAM), MALLOC_CAP_SPIRAM);

ロックで肝となる処理は上記です。heap_caps_malloc関数を使って、常にPSRAMから確保するように限定しています。これをしないと4096以下の場合にはメインメモリからも確保されてしまいます。確保しているサイズはheap_caps_get_largest_free_block(MALLOC_CAP_SPIRAM)で、これは最大で確保できるPSRAMのサイズを指定しています。

 _lockList[i] = 3F802F94
 _lockList[i] = 3FBF2F98
 _lockList[i] = 3FBFFF9C
 _lockList[i] =        0

上記のように3ブロック分メモリが確保できています。これはPSRAMのメモリが分断されているため、一度に確保できなかったためになります。何度か確保して、途中だけ開放するとより分断が進んでいくと思われます。

まとめ

自分のプログラムであればheap_caps_malloc関数をつかって、どこからメモリを確保するのかを制御することが可能だと思います。ただし、外部ライブラリなどで制御不可能な場合にはこのように先にPSRAMを確保してしまい、メインメモリから使うような制御も可能そうでした。

コメント