Using PROGMEM for string arrays
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
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)?
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 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
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.
Something is wrong with the compiler if its ignoring include guards, perhaps include in the main source file and see if that changes it.
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.
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