WiFiManager icon indicating copy to clipboard operation
WiFiManager copied to clipboard

Using PROGMEM for string arrays

Open dalbert2 opened this issue 2 years ago • 7 comments

Basic Infos

string arrays in wm_consts_en.h aren't being placed in PROGMEM properly

Hardware

WiFimanager Branch/Release: Master

Esp8266/Esp32: ESP8266

Hardware: ESP-12e

Core Version: 3.0.5

Description

Moving strings into PROGMEM is essential for ESP8266 where IRAM is extremely tight.

This code is incorrect (see wm_consts_en.h):

const uint8_t _nummenutokens = 11;
const char * const _menutokens[_nummenutokens] PROGMEM = {
    "wifi",
    "wifinoscan",
    "info",
    "param",
    "close",
    "restart",
    "exit",
    "erase",
    "update",
    "sep",
    "custom"
};

The above code places the pointers to the string constants in PROGMEM, but the string constants themselves land in RAM. The below code shows how to put both the pointers and the strings themselves in PROGMEM:

static const char _wifi_token[]       PROGMEM = "wifi";
static const char _wifinoscan_token[] PROGMEM = "wifinoscan";
static const char _info_token[]       PROGMEM = "info";
static const char _param_token[]      PROGMEM = "param";
static const char _close_token[]      PROGMEM = "close";
static const char _restart_token[]    PROGMEM = "restart";
static const char _exit_token[]       PROGMEM = "exit";
static const char _erase_token[]      PROGMEM = "erase";
static const char _update_token[]     PROGMEM = "update";
static const char _sep_token[]        PROGMEM = "sep";
static const char _custom_token[]     PROGMEM = "custom";
static PGM_P _menutokens[] PROGMEM = {
    _wifi_token,
    _wifinoscan_token,
    _info_token,
    _param_token,
    _close_token,
    _restart_token,
    _exit_token,
    _erase_token,
    _update_token,
    _sep_token,
    _custom_token
};
const uint8_t _nummenutokens = (sizeof(_menutokens) / sizeof(PGM_P));

I know this is ugly, but it works; if someone knows a prettier solution, please share it. At present, WiFiManager uses 1344 of IRAM even if you don't instantiate an instance of the class which is a problem for many applications on the ESP8266.

Settings in IDE

Sketch

Debug Messages

dalbert2 avatar Apr 14 '23 23:04 dalbert2

Even worse, the string constants are all declared in header files which are included by WiFiManager.h so any class or module that includes WiFiManager.h creates a copy of all of the string constants. Why are these not defined in a compilation unit (.c or .cpp)?

dalbert2 avatar Apr 14 '23 23:04 dalbert2

What am I missing here? I am not seeing this memory waste

RAM:   [====      ]  40.0% (used 32732 bytes from 81920 bytes)
Flash: [====      ]  36.9% (used 385916 bytes from 1044464 bytes)
.pio/build/nodemcuv2/firmware.elf  :
section            size         addr
.data              1612   1073643520
.noinit              60   1073645132
.text               463   1074790400
.irom0.text      351480   1075843088
.text1            27993   1074790864
.rodata            4368   1073645200
.bss              26752   1073649568
.comment           5066            0
.xtensa.info         56            0                                                                                                                                                                              56            0

total 1366237

------------------------------------
BEFORE

RAM:   [====      ]  40.0% (used 32732 bytes from 81920 bytes)
Flash: [====      ]  36.9% (used 385836 bytes from 1044464 bytes)
.pio/build/nodemcuv2/firmware.elf  :
section            size         addr
.data              1612   1073643520
.noinit              60   1073645132
.text               463   1074790400
.irom0.text      351400   1075843088
.text1            27993   1074790864
.rodata            4368   1073645200
.bss              26752   1073649568
.comment           5066            0
.xtensa.info         56            0

total 1366157

tablatronix avatar Apr 15 '23 03:04 tablatronix

@tablatronix please see: https://arduino-esp8266.readthedocs.io/en/latest/PROGMEM.html#how-do-i-declare-arrays-of-strings-in-progmem-and-retrieve-an-element-from-it

If you do this with the string arrays, you will see free heap increase.

I also think the strings need to be moved into a separate compilation unit (e.g. wm_strings.cpp). You can still use the pre-processor to select a language and a header file to define extern references for strings needed by other compilation units. Example:

strings.h

#pragma once
extern const char *_str_table[];

strings.c

#include "strings.h"

#ifdef LANG_ES
    static const char _hello[] = "hola";
    static const char _world[] = "mundo";
#else
    static const char _hello[] = "hello";
    static const char _world[] = "world";
#endif

const char *_str_table[] = { _hello, _world };

main.c:

#include <stdio.h>

#include "strings.h"

int main(int argc, char **argv) {
    printf("%s %s\n", _str_table[0], _str_table[1]);
    return 0;
}

Compile for english:gcc strings.c main.c Compile for spanish: gcc strings.c main.c -DLANG_ES

dalbert2 avatar Apr 15 '23 13:04 dalbert2

What am I missing here? I am not seeing this memory waste ...

I checked the contents of the compiled code in the .o, .elf and .bin files using strings(1). When two cpp files (typically WiFiManager.cpp and the calling code) include WiFiManager.h and hence wm_strings_en.h there are indeed multiple copies of each of the flash memory strings compiled into the .o files and linked into the .elf file. However the process that converts the .elf file to the .bin file deduplicates them such that the final executable does not contain duplicates, as observed by @tablatronix.

I checked this with both of Arduino IDE rather than PlatformIO, specifically ESP8266 3.1.2 and ESP32 3.2.0. The WiFiManager version I used was commit 32655b722601ce8bf3c03974bac06a96ceed42e2 (HEAD, origin/master, origin/HEAD, master).

Per @dalbert2, usual practice would be to put only declarations in a .h file and definitions in a .cpp file. In this case, only WiFiManager.cpp needs to reference these strings, so that could be done simply by including the strings file in WiFiManager.cpp rather than WiFiManager.h and moving the few initialisations that use flash strings in WiFiManager.h from there to WiFiManagerInit() in WiFiManager.cpp. As shown above, it doesn't buy us any memory, though it would decrease compile times by some unknown amount.

timr49 avatar Jul 31 '25 08:07 timr49

Something is wrong with the compiler if its ignoring include guards, perhaps include in the main source file and see if that changes it.

tablatronix avatar Jul 31 '25 23:07 tablatronix

Something is wrong with the compiler if its ignoring include guards, perhaps include in the main source file and see if that changes it.

I disagree. Include guards only ever stop one .cpp file from including the same .h file more than once. They cannot stop multiple .cpp files from each including the same .h file once each, which is what is happening.

As suggested, including the strings file in WiFiManager.cpp rather than WiFiManager.h is preferable.

timr49 avatar Jul 31 '25 23:07 timr49

I can try it, i do not recall the decision to use headers, I think arduino auto included .cpp in folder and it was an issue

tablatronix avatar Aug 04 '25 18:08 tablatronix