Notes / esp32arduinowifim5stackcardputer
ESP32-S3 without PSRAM: HTTPS fetch returns 0 bytes or connect() fails "connection refused" (TLS out of memory)
On a PSRAM-less ESP32-S3 (M5Stack Cardputer-ADV) an HTTPS fetch returned 0 bytes and later TLS connects failed with "connection refused". mbedTLS could not get a large enough contiguous heap block. Freeing the audio decoder, JSON document and 64 KB display sprite before TLS fixed it.
Symptoms #
An Arduino MP3 player sketch on an M5Stack Cardputer-ADV fetches a JSON feed over HTTPS, then downloads several files over HTTPS to the microSD card. On this board:
- the feed fetch returned 0 bytes, and
- once the feed had been parsed, the per-file downloads failed because
client.connect()reported "connection refused", although the server was fine.
Nothing was wrong with the network or the server. Both were memory failures that do not look like memory failures. (The "connection refused" text is what the sketch's connect failure reported; the exact log line was not recorded.)
Root cause #
The Cardputer-ADV's ESP32-S3 has no PSRAM, only about 320 KB of internal SRAM, and a TLS handshake needs large contiguous heap blocks. With the audio decoder, the audio output object, a parsed JSON document and a 64 KB display sprite all allocated, only about 10 KB contiguous was left after the mbedTLS handshake, and new TLS connections could not get the buffers they needed.
The number that settled it was the largest free heap block before connecting: about
33 KB → TLS fails, about 64 KB → TLS works. To watch it yourself, log
heap_caps_get_largest_free_block(MALLOC_CAP_8BIT) (or ESP.getMaxAllocHeap()) just
before client.connect(); total free heap is misleading here because fragmentation is
the problem.
Fix #
Free the big allocations before any TLS work, and keep large data out of RAM:
- Free the MP3 decoder and the audio output object before starting TLS.
- Stream the response to a temporary file on SD, then parse the JSON from the file, instead of reading the whole body into RAM.
- Free the JSON document before the per-file downloads. Without this step,
client.connect()fails with "connection refused". - Delete the 64 KB display sprite for the duration of Wi-Fi/TLS. It turned out to be the single largest allocation: once the app grew a little (more globals), steps 1–3 alone were no longer enough. While the sprite is gone, draw status text straight to the display.
cppsprite.deleteSprite(); // M5Canvas back buffer, ~64 KB
// ... connect, download, write to SD; draw progress directly on the display ...
ESP.restart(); // come back up clean and recreate the sprite
Restarting at the end of the sync is a simple way to get every freed object (decoder, sprite) back in a known state. With all four steps, the largest free block went from about 33 KB to about 64 KB and the HTTPS fetches and downloads worked.