fix(l2): WiFi fails to start or join - big statics and task stacks to PSRAM

WiFi starts and joins on internal RAM: ~42 KB the first time, ~25 KB after,
more for WPA. Free internal RAM had shrunk to ~20 KB after opening the map
(the tile decoder's 12 KB stack, the 32 KB I-cache, the DMA draw buffers,
UI statics), so a scan failed at once (esp_wifi_init: no memory) and a join
during a tile download timed out (AUTH_EXPIRE).

- ESP-IDF: CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY, and the esp_timer
  task stack 8 -> 4 KB (it uses ~0.4 KB). Rebuild the framework libs:
  pio run -e Wio_Tracker_L2_idf_libs.
- PSRAM_BSS (EXT_RAM_BSS_ATTR) on the UI's big statics: QR codes, map
  state, charts, lists. .dram0.bss 115 -> 66 KB.
- The tile decoder's and fetcher's task stacks in PSRAM.

Free internal RAM, idle: 48 -> 105 KB; WiFi up: 57 KB, 29 KB at the
lowest while downloading tiles.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-10-14 17:23:29 +02:00
co-authored by Claude Opus 5.5
parent 3825dcc077
commit 2f2db6045c
17 changed files with 49 additions and 29 deletions
@@ -27,7 +27,7 @@ static lv_obj_t* s_hex_sw = nullptr;
static lv_obj_t* s_hint = nullptr;
static lv_obj_t* s_status = nullptr;
static lv_obj_t* s_kb = nullptr;
static char s_scope_opts[256];
static PSRAM_BSS char s_scope_opts[256];
static void clearForm() {
s_types = s_name = s_secret = s_hex_sw = s_hint = s_status = s_kb = nullptr;
+2 -2
View File
@@ -10,12 +10,12 @@ static void onOpenChannel(lv_event_t* e) {
}
// DM rows carry a 4-byte prefix; kept in a static table the rows point into.
static uint8_t s_dm_rows[MessageHistory::DM_HIST_MAX][4];
static PSRAM_BSS uint8_t s_dm_rows[MessageHistory::DM_HIST_MAX][4];
static const int CONTACT_ROWS_MAX = 256; // "All" with a full contact table stays usable
static uint8_t (*s_contact_rows)[PUB_KEY_SIZE] = psramBuf<uint8_t[PUB_KEY_SIZE]>(CONTACT_ROWS_MAX);
static uint16_t* s_contact_raw = psramBuf<uint16_t>(CONTACT_ROWS_MAX); // their raw table index, for the name
static uint16_t s_dm_pos[MessageHistory::DM_HIST_MAX]; // its newest message in the DM ring
static PSRAM_BSS uint16_t s_dm_pos[MessageHistory::DM_HIST_MAX]; // its newest message in the DM ring
static bool s_dm_known[MessageHistory::DM_HIST_MAX]; // a contact (has options on hold)
static void onOpenDMRow(lv_event_t* e) {
intptr_t r = vlist::arg(e);
@@ -24,7 +24,7 @@ static LiveVal s_vals[diag::MAX_ROWS + EXTRA];
static int s_rows = 0;
// The history, a sample every 5 minutes from boot (UITask::sampleHistory()).
static uicore::StatusHistory<96, 300000UL> s_hist;
static PSRAM_BSS uicore::StatusHistory<96, 300000UL> s_hist;
static uint32_t s_hist_shown = 0; // the history's version the charts show
static lv_obj_t* s_ch_obj[4]; // History's charts and their values now
static lv_obj_t* s_ch_now[4];
@@ -38,7 +38,7 @@ static uint32_t totalRecv() {
// One chart's samples as drawn: a copy, so the draw callback needn't know
// which ring (of which size) they came from. What the axis and the pill
// say is set once (setupCharts()).
static chart::Chart s_charts[4];
static PSRAM_BSS chart::Chart s_charts[4];
template <int N>
static void chartOf(int k, const uicore::History<N>& h) {
chart::Chart& c = s_charts[k];
@@ -218,7 +218,7 @@ static void onNoiseRun(lv_event_t* e) { (void)e; s_ui->diagNoiseRun(); }
// A sweep as bars (Chart.h), the step nearest the mesh's frequency in the
// accent, then its quietest / loudest values.
static chart::Chart s_sweep_chart;
static PSRAM_BSS chart::Chart s_sweep_chart;
static char s_sweep_under[3][12];
static void plotSweep(lv_obj_t* list, const char* title, const Sweep& sw, float mesh_f) {
sectionTitle(list, title);
+1 -1
View File
@@ -38,7 +38,7 @@ static const int SKY = 168;
static const int BAR_PITCH = 26, BAR_W = 16, BARS_H = 120, AXIS_W = 22;
// Snapshot the screen draws from, sorted: used first, then by signal.
static GpsSky::Sat s_sats[GpsSky::MAX_SATS];
static PSRAM_BSS GpsSky::Sat s_sats[GpsSky::MAX_SATS];
static bool s_used_flag[GpsSky::MAX_SATS];
static int s_n = 0;
@@ -141,7 +141,7 @@ static lv_obj_t* s_caption = nullptr;
static const int MAX_PTS = LiveTrackStore::CAPACITY + 2;
enum : uint8_t { P_ME, P_LAST, P_TARGET, P_LIVE }; // how a point's dot looks
struct Pt { int32_t lat, lon; uint8_t kind; lv_obj_t* obj; };
static Pt s_pts[MAX_PTS]; // [0] you, then the target, then live shares
static PSRAM_BSS Pt s_pts[MAX_PTS]; // [0] you, then the target, then live shares
static int s_npts = 0;
static bool s_fitted = false; // homeMapFit() ran for this map
static int s_z = mapview::DEFAULT_Z;
+4 -2
View File
@@ -55,7 +55,7 @@ static void shotCopy(const lv_area_t* a, const uint8_t* px) {
// once read; wifiStoreLoad() / wifiStoreWrite() (per board, below) hold them.
static const int WIFI_SAVED_MAX = 8;
struct WifiNet { char ssid[33]; char pass[65]; };
static WifiNet s_nets[WIFI_SAVED_MAX];
static PSRAM_BSS WifiNet s_nets[WIFI_SAVED_MAX];
static int s_net_count = -1; // -1: not read yet
static void wifiStoreLoad();
static void wifiStoreWrite();
@@ -760,7 +760,9 @@ static void fetchTask(void*) {
// memory for the worker task.
static int fetchStart(const char* url) {
if (s_fstate != F_IDLE) return 0;
if (!s_ftask && xTaskCreatePinnedToCore(fetchTask, "tilefetch", 10240, nullptr, tskIDLE_PRIORITY, &s_ftask, 0) != pdPASS) {
// The stack in PSRAM, as the tile decoder's (TileCache.h): WiFi needs the internal RAM.
if (!s_ftask && xTaskCreatePinnedToCoreWithCaps(fetchTask, "tilefetch", 10240, nullptr, tskIDLE_PRIORITY, &s_ftask, 0,
MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT) != pdPASS) {
s_ftask = nullptr;
return -1;
}
+5 -5
View File
@@ -26,12 +26,12 @@
namespace mapview {
static RasterTileProvider s_raster("/sdcard/maps", LIVE_ROOT);
static VectorTileProvider s_vector(s_raster); // spike: vector data where there is some, raster elsewhere
static PSRAM_BSS RasterTileProvider s_raster("/sdcard/maps", LIVE_ROOT);
static PSRAM_BSS VectorTileProvider s_vector(s_raster); // spike: vector data where there is some, raster elsewhere
static TileProvider* s_provider = &s_raster; // Map tools > Vector map (test) swaps it
static TileCache& s_cache = *new (psramBuf<TileCache>(1)) TileCache(); // decoded tiles, in PSRAM
static TileDownloader s_dl("/sdcard/maps");
static AreaStore s_areas("/sdcard/maps");
static PSRAM_BSS TileDownloader s_dl("/sdcard/maps");
static PSRAM_BSS AreaStore s_areas("/sdcard/maps");
static const uint32_t AVG_TILE_BYTES = 22 * 1024; // OpenTopoMap-ish, for the size estimate
// Hand a tile to the decoder (on the other core); false while one is in flight.
@@ -1036,7 +1036,7 @@ void UITask::layoutMapMarks(double left, double top, int w, int h) {
const int half = MARK_D / 2;
const int lh = lv_font_get_line_height(THEME_FONT_SMALL) + 2;
struct Box { int16_t x1, y1, x2, y2; };
static Box used[6 + MAX_MARKS + MARK_LABELS_MAX]; // the map's controls, the dots, the names
static PSRAM_BSS Box used[6 + MAX_MARKS + MARK_LABELS_MAX]; // the map's controls, the dots, the names
int nu = 0;
const int bottom = mapBottomH(); // the target's bar, or the route's
const int16_t W = (int16_t)w, H = (int16_t)(h - bottom);
+4 -4
View File
@@ -41,7 +41,7 @@ static const int PTS_MAX = TrailStore::CAPACITY + 2 * CHUNKS + TRAIL_SEGS + 4;
static lv_point_precise_t* s_pts = nullptr;
static int s_pts_n = 0;
static int32_t s_pts_x0 = 0, s_pts_y0 = 0; // the trail object's screen origin s_pts was made for
static int s_seg_first[TRAIL_SEGS], s_segs = 0;
static PSRAM_BSS int s_seg_first[TRAIL_SEGS], s_segs = 0;
// What navTrailSync() has converted so far (s_sync_gen reset: all of it again).
static uint32_t s_sync_gen = 0xFFFFFFFF, s_sync_seq = 0, s_sync_full_at = 0;
static int s_sync_n = 0;
@@ -794,7 +794,7 @@ enum : uint8_t { TL_TRAIL_TOGGLE, TL_TRAIL_SAVE, TL_TRAIL_LOAD, TL_TRAIL_RESET,
static const int SHARE_TARGETS = MAX_GROUP_CHANNELS + 16;
static uint8_t s_share_kind[SHARE_TARGETS]; // 0 = channel, 1 = contact
static uint8_t s_share_ch[SHARE_TARGETS];
static uint8_t s_share_key[SHARE_TARGETS][NodePrefs::FAVOURITE_PREFIX_LEN];
static PSRAM_BSS uint8_t s_share_key[SHARE_TARGETS][NodePrefs::FAVOURITE_PREFIX_LEN];
static int s_share_n = 0;
// Print sink over a FILE* for TrailStore's GPX writer (same duck type as
@@ -825,7 +825,7 @@ struct FileRW {
// list also offers -- the low-battery auto-save still goes there.
static const char* const TRAILS_DIR = "/sdcard/trails";
static const int ST_MAX = 40;
static char s_st_names[ST_MAX][28]; // newest first
static PSRAM_BSS char s_st_names[ST_MAX][28]; // newest first
static int s_st_n = 0;
static int s_st_sel = -2; // the one the popup is about; -1 = the internal slot
static lv_obj_t* s_st_del_lbl = nullptr;
@@ -928,7 +928,7 @@ void UITask::navToolsPopup(bool back) {
// Worst case: "\n" + a 31-byte name per channel, "\n" + star (3) + space +
// 31 bytes per favourite. Static: ~1.9 KB is a lot for the loop's stack, and
// the dropdown copies it.
static char opts[MAX_GROUP_CHANNELS * 32 + 16 * 36 + 1];
static PSRAM_BSS char opts[MAX_GROUP_CHANNELS * 32 + 16 * 36 + 1];
int o = 0, sel = 0;
navmap::s_share_n = 0;
for (int i = 0; i < MAX_GROUP_CHANNELS && navmap::s_share_n < navmap::SHARE_TARGETS; i++) {
@@ -9,8 +9,8 @@
namespace profileview {
static trailprofile::Sampler s_prof; // what the chart on screen shows
static chart::Chart s_chart;
static PSRAM_BSS trailprofile::Sampler s_prof; // what the chart on screen shows
static PSRAM_BSS chart::Chart s_chart;
static char s_under[3][12];
static void distText(float m, char* out, size_t n) {
@@ -374,7 +374,7 @@ static void qrImage(lv_image_dsc_t& dsc, uint8_t* data, size_t size, int n, int
// The Solo site's QR code (ui-core/SiteQr.h), built once at 5 px a module.
static lv_obj_t* siteQr(lv_obj_t* parent) {
static const int S = 5, PX = (siteqr::N + 2 * siteqr::QUIET) * S;
static uint8_t data[8 + (PX + 7) / 8 * PX];
static PSRAM_BSS uint8_t data[8 + (PX + 7) / 8 * PX];
static lv_image_dsc_t dsc;
if (!dsc.data) qrImage(dsc, data, sizeof(data), siteqr::N, S, siteqr::dark);
lv_obj_t* img = lv_image_create(parent);
@@ -386,9 +386,9 @@ static lv_obj_t* siteQr(lv_obj_t* parent) {
// shown (the name can change), 3 px a module; null when it doesn't fit.
static lv_obj_t* contactQr(lv_obj_t* parent) {
static const int S = 3, PX = (contactqr::MAX_VERSION * 4 + 17 + 2 * siteqr::QUIET) * S;
static uint8_t data[8 + (PX + 7) / 8 * PX];
static PSRAM_BSS uint8_t data[8 + (PX + 7) / 8 * PX];
static lv_image_dsc_t dsc;
static contactqr::Code code;
static PSRAM_BSS contactqr::Code code;
if (!contactqr::make(code)) return nullptr;
qrImage(dsc, data, sizeof(data), code.n, S, [](int x, int y) { return code.dark(x, y); });
lv_obj_t* img = lv_image_create(parent);
+1 -1
View File
@@ -876,7 +876,7 @@ void UITask::takeScreenshot() {
put32(2, size); put32(10, 54); put32(14, 40); put32(18, W); put32(22, H);
hdr[26] = 1; hdr[28] = 24; put32(34, row * H);
ok = fwrite(hdr, 1, 54, f) == 54;
static uint8_t line[320 * 3]; // not on the loop task's stack
static PSRAM_BSS uint8_t line[320 * 3]; // not on the loop task's stack
for (int32_t y = H - 1; ok && y >= 0; y--) { // bottom-up, BGR
const uint16_t* s = px + y * W;
for (int32_t x = 0; x < W && x < 320; x++) {
@@ -9,6 +9,14 @@
#include <helpers/SensorManager.h>
#include <helpers/BaseSerialInterface.h>
#include <lvgl.h>
// A big zero-initialised static in PSRAM (ESP32): the internal RAM is what
// WiFi starts and joins on. Not for what DMA or an interrupt touches.
#if defined(ESP32)
#include <esp_attr.h>
#define PSRAM_BSS EXT_RAM_BSS_ATTR
#else
#define PSRAM_BSS
#endif
#ifdef PIN_BUZZER
#include <helpers/ui/buzzer.h>
#endif
@@ -9,7 +9,7 @@
//
// Single-TU fragment: included by ui-lvgl/UITask.cpp after MapScreen.h.
static char s_wifi_names[lvport::WIFI_SCAN_MAX][33];
static PSRAM_BSS char s_wifi_names[lvport::WIFI_SCAN_MAX][33];
static void onWifiScan(lv_event_t* e) { (void)e; s_ui->wifiScan(); }
static void onWifiSave(lv_event_t* e) { (void)e; s_ui->wifiSave(); }
@@ -55,7 +55,10 @@ public:
_job_stale = false;
_job_state = JOB_RUNNING;
#if defined(ESP32)
if (!_task && xTaskCreatePinnedToCore(jobTask, "tiles", 12288, this, tskIDLE_PRIORITY + 1, &_task, 0) != pdPASS) {
// The stack in PSRAM: internal RAM is what WiFi starts on (it fails to
// with ~25 KB left), and the task only reads the SD card, never the flash.
if (!_task && xTaskCreatePinnedToCoreWithCaps(jobTask, "tiles", 12288, this, tskIDLE_PRIORITY + 1, &_task, 0,
MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT) != pdPASS) {
_task = nullptr;
runJob(); // no task: here, as before
return true;
@@ -147,7 +147,7 @@ static void layout(double left, double top, int w, int h, int z, bool on, int bo
// Greedy placement: the icon, then the text right / left / above / below it.
struct Box { int16_t x1, y1, x2, y2; };
static Box used[MAX_PLACED * 2 + 8];
static PSRAM_BSS Box used[MAX_PLACED * 2 + 8];
int nu = 0;
// The map's controls: button columns, the zoom pill between them, scale /
// credit and the centre button above the bottom bar, the bar.
@@ -206,6 +206,6 @@ private:
}
};
static VectorPacks s_vpacks;
static PSRAM_BSS VectorPacks s_vpacks;
} // namespace mapview
+7
View File
@@ -25,3 +25,10 @@ CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER=y
CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER_NUM=32
CONFIG_ESP_WIFI_RX_BA_WIN=6
CONFIG_LWIP_TCP_OOSEQ_MAX_PBUFS=4
# Internal RAM is what WiFi starts and joins on (~25 KB, more for WPA): big
# zero-initialised statics marked EXT_RAM_BSS_ATTR (QR codes, map state,
# charts...) go to PSRAM instead.
CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY=y
# Its callbacks are short (~0.4 KB of stack used): half the framework's 8 KB.
CONFIG_ESP_TIMER_TASK_STACK_SIZE=4096