From 2f2db6045c3a4e4dc8fc44a70cb37fcddb09212d Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:23:29 +0200 Subject: [PATCH] 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 --- examples/companion_radio/ui-lvgl/ChannelScreen.h | 2 +- examples/companion_radio/ui-lvgl/ChatList.h | 4 ++-- examples/companion_radio/ui-lvgl/DiagScreen.h | 6 +++--- examples/companion_radio/ui-lvgl/GpsScreen.h | 2 +- examples/companion_radio/ui-lvgl/HomeScreen.h | 2 +- examples/companion_radio/ui-lvgl/LvglPort.h | 6 ++++-- examples/companion_radio/ui-lvgl/MapScreen.h | 10 +++++----- examples/companion_radio/ui-lvgl/NavMap.h | 8 ++++---- examples/companion_radio/ui-lvgl/ProfileView.h | 4 ++-- examples/companion_radio/ui-lvgl/SettingsPages.h | 6 +++--- examples/companion_radio/ui-lvgl/UITask.cpp | 2 +- examples/companion_radio/ui-lvgl/UITask.h | 8 ++++++++ examples/companion_radio/ui-lvgl/WifiScreen.h | 2 +- examples/companion_radio/ui-lvgl/map/TileCache.h | 5 ++++- examples/companion_radio/ui-lvgl/map/VectorLabels.h | 2 +- examples/companion_radio/ui-lvgl/map/VectorPacks.h | 2 +- solo/wio-tracker-l2/idf/sdkconfig | 7 +++++++ 17 files changed, 49 insertions(+), 29 deletions(-) diff --git a/examples/companion_radio/ui-lvgl/ChannelScreen.h b/examples/companion_radio/ui-lvgl/ChannelScreen.h index 8170d72f..7b33ffa5 100644 --- a/examples/companion_radio/ui-lvgl/ChannelScreen.h +++ b/examples/companion_radio/ui-lvgl/ChannelScreen.h @@ -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; diff --git a/examples/companion_radio/ui-lvgl/ChatList.h b/examples/companion_radio/ui-lvgl/ChatList.h index 705754a5..4ec25a7e 100644 --- a/examples/companion_radio/ui-lvgl/ChatList.h +++ b/examples/companion_radio/ui-lvgl/ChatList.h @@ -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(CONTACT_ROWS_MAX); static uint16_t* s_contact_raw = psramBuf(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); diff --git a/examples/companion_radio/ui-lvgl/DiagScreen.h b/examples/companion_radio/ui-lvgl/DiagScreen.h index a82478fb..6d6bfc70 100644 --- a/examples/companion_radio/ui-lvgl/DiagScreen.h +++ b/examples/companion_radio/ui-lvgl/DiagScreen.h @@ -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 static void chartOf(int k, const uicore::History& 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); diff --git a/examples/companion_radio/ui-lvgl/GpsScreen.h b/examples/companion_radio/ui-lvgl/GpsScreen.h index 6698e4ff..260a5f55 100644 --- a/examples/companion_radio/ui-lvgl/GpsScreen.h +++ b/examples/companion_radio/ui-lvgl/GpsScreen.h @@ -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; diff --git a/examples/companion_radio/ui-lvgl/HomeScreen.h b/examples/companion_radio/ui-lvgl/HomeScreen.h index 152a6046..f3432bc8 100644 --- a/examples/companion_radio/ui-lvgl/HomeScreen.h +++ b/examples/companion_radio/ui-lvgl/HomeScreen.h @@ -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; diff --git a/examples/companion_radio/ui-lvgl/LvglPort.h b/examples/companion_radio/ui-lvgl/LvglPort.h index 5ced7daa..cc7e4ca1 100644 --- a/examples/companion_radio/ui-lvgl/LvglPort.h +++ b/examples/companion_radio/ui-lvgl/LvglPort.h @@ -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; } diff --git a/examples/companion_radio/ui-lvgl/MapScreen.h b/examples/companion_radio/ui-lvgl/MapScreen.h index c1b92a19..d0fd33ca 100644 --- a/examples/companion_radio/ui-lvgl/MapScreen.h +++ b/examples/companion_radio/ui-lvgl/MapScreen.h @@ -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(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); diff --git a/examples/companion_radio/ui-lvgl/NavMap.h b/examples/companion_radio/ui-lvgl/NavMap.h index 26e88348..4a793c44 100644 --- a/examples/companion_radio/ui-lvgl/NavMap.h +++ b/examples/companion_radio/ui-lvgl/NavMap.h @@ -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++) { diff --git a/examples/companion_radio/ui-lvgl/ProfileView.h b/examples/companion_radio/ui-lvgl/ProfileView.h index 9471c71f..fe38063d 100644 --- a/examples/companion_radio/ui-lvgl/ProfileView.h +++ b/examples/companion_radio/ui-lvgl/ProfileView.h @@ -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) { diff --git a/examples/companion_radio/ui-lvgl/SettingsPages.h b/examples/companion_radio/ui-lvgl/SettingsPages.h index 6185e60d..71c665c5 100644 --- a/examples/companion_radio/ui-lvgl/SettingsPages.h +++ b/examples/companion_radio/ui-lvgl/SettingsPages.h @@ -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); diff --git a/examples/companion_radio/ui-lvgl/UITask.cpp b/examples/companion_radio/ui-lvgl/UITask.cpp index 4939755d..28280aa0 100644 --- a/examples/companion_radio/ui-lvgl/UITask.cpp +++ b/examples/companion_radio/ui-lvgl/UITask.cpp @@ -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++) { diff --git a/examples/companion_radio/ui-lvgl/UITask.h b/examples/companion_radio/ui-lvgl/UITask.h index c6a6a747..48f077eb 100644 --- a/examples/companion_radio/ui-lvgl/UITask.h +++ b/examples/companion_radio/ui-lvgl/UITask.h @@ -9,6 +9,14 @@ #include #include #include +// 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 + #define PSRAM_BSS EXT_RAM_BSS_ATTR +#else + #define PSRAM_BSS +#endif #ifdef PIN_BUZZER #include #endif diff --git a/examples/companion_radio/ui-lvgl/WifiScreen.h b/examples/companion_radio/ui-lvgl/WifiScreen.h index 641e7e5d..fea221ba 100644 --- a/examples/companion_radio/ui-lvgl/WifiScreen.h +++ b/examples/companion_radio/ui-lvgl/WifiScreen.h @@ -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(); } diff --git a/examples/companion_radio/ui-lvgl/map/TileCache.h b/examples/companion_radio/ui-lvgl/map/TileCache.h index 4da4b79b..297b9712 100644 --- a/examples/companion_radio/ui-lvgl/map/TileCache.h +++ b/examples/companion_radio/ui-lvgl/map/TileCache.h @@ -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; diff --git a/examples/companion_radio/ui-lvgl/map/VectorLabels.h b/examples/companion_radio/ui-lvgl/map/VectorLabels.h index c538aa84..cf792e54 100644 --- a/examples/companion_radio/ui-lvgl/map/VectorLabels.h +++ b/examples/companion_radio/ui-lvgl/map/VectorLabels.h @@ -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. diff --git a/examples/companion_radio/ui-lvgl/map/VectorPacks.h b/examples/companion_radio/ui-lvgl/map/VectorPacks.h index 7e86a06a..682dae97 100644 --- a/examples/companion_radio/ui-lvgl/map/VectorPacks.h +++ b/examples/companion_radio/ui-lvgl/map/VectorPacks.h @@ -206,6 +206,6 @@ private: } }; -static VectorPacks s_vpacks; +static PSRAM_BSS VectorPacks s_vpacks; } // namespace mapview diff --git a/solo/wio-tracker-l2/idf/sdkconfig b/solo/wio-tracker-l2/idf/sdkconfig index d480cc3d..c4c55aaf 100644 --- a/solo/wio-tracker-l2/idf/sdkconfig +++ b/solo/wio-tracker-l2/idf/sdkconfig @@ -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